[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # yuliskov\npatreon: smarttube\nopen_collective: # smarttubeapp\nko_fi: # smarttube\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # smarttube\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/1-bug_report.yml",
    "content": "name: Bug Report\ndescription: Create a bug report to help us improve\ntitle: \"[BUG]: \"\nlabels: [bug]\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for helping to make SmartTube better by reporting a bug.\n\n        Please fill in as much information as possible about your bug.\n\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: \"Checklist\"\n      options:\n        - label: \"I made sure that there are *no existing issues* - [open](https://github.com/yuliskov/SmartTube/issues) or [closed](https://github.com/yuliskov/SmartTube/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to.\"\n          required: true\n        - label: \"I have read the [FAQ](https://github.com/yuliskov/SmartTube#faq) and my problem isn't listed.\"\n          required: true\n        - label: \"I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise.\"\n          required: true\n        - label: \"This issue contains only one bug.\"\n          required: true\n\n  - type: input\n    id: app-version\n    attributes:\n     label: Affected version\n     description: \"In which SmartTube version did you encounter the bug?\"\n     placeholder: \"x.xx.x - Can be seen in the app from the 'About' section in Settings\"\n    validations:\n      required: true\n\n  - type: dropdown\n    id: device-type\n    attributes:\n      label: Device Type\n      description: Is it Smart TV/Box or Phone\n      options:\n        - \"Smart TV/Box\"\n        - \"Phone/Tablet\"\n    validations:\n      required: true\n\n  - type: input\n    id: device-os-info\n    attributes:\n     label: Affected Android\n     description: |\n      With what operating system (+ version) did you encounter the bug?\n     placeholder: \"Example: Android TV 10\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: steps-to-reproduce\n    attributes:\n      label: Steps to reproduce the bug\n      description: |\n        What did you do for the bug to show up?\n\n        If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug.\n      placeholder: |\n        1. Go to '...'\n        2. Press on '....'\n    validations:\n      required: true\n\n  - type: textarea\n    id: actual-behavior\n    attributes:\n      label: Actual behavior\n      description: |\n        Tell us what happens with the steps given above.\n\n  - type: textarea\n    id: additional-information\n    attributes:\n      label: Additional information\n      description: |\n        Any other information you'd like to include, for instance that\n        * Device (NVIDIA Shield, Xiaomi Mi Box, etc)\n        * screenshot\n        * your cat disabled your network connection\n        * ..."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/2-feature-request.yml",
    "content": "name: Feature Request\ndescription: Create a feature request for SmartTube\ntitle: '[Feature Request]: '\nlabels: ['enhancement']\nbody:\n  - type: markdown\n    attributes:\n      value: Thanks for taking the time to file a feature request! Please fill out this form as completely as possible.\n  - type: textarea\n    attributes:\n      label: Describe the feature you'd like to request\n      description: A clear and concise description of what you want and what your use case is.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Describe the solution you'd like\n      description: A clear and concise description of what you want to happen.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Describe alternatives you've considered\n      description: A clear and concise description of any alternative solutions or features you've considered.\n    validations:\n      required: true\n  - type: textarea\n    id: additional-information\n    attributes:\n      label: Additional information\n      description: |\n        Any other information you'd like to include, for instance\n        * Relevant issues\n        * Mockup images\n        * The feature would make your cat happy\n        * ..."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Telegram group (international)\n    url: http://t.me/SmartTubeEN\n    about: \"The international group is in **English only**. But don't worry if your English is not perfect, we have a friendly international community.\"\n  - name: Telegram group (RU/UA)\n    url: http://t.me/SmartTubeUA\n    about: \"Group for Russian and Ukrainian speakers.\""
  },
  {
    "path": ".github/workflows/CI.yml",
    "content": "name: Build Debug APK\n\non:\n  push:\n    branches: [ \"master\" ]\n  workflow_dispatch:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    env:\n      HAS_SIGNING_KEY: ${{ secrets.SIGNING_KEY != '' }}\n      HAS_VT_KEY: ${{ secrets.VIRUS_TOTAL_API_KEY != '' }}\n\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v6\n        with:\n          submodules: recursive\n\n      - name: Set up JDK 17\n        uses: actions/setup-java@v5\n        with:\n          java-version: '17'\n          distribution: 'temurin'\n          cache: 'gradle'\n\n      - name: Update Version Name\n        id: get_version\n        run: |\n          # Keep the original code to make the nightly and original builds interchangeable\n          sed -i \"s/versionName \\\"\\(.*\\)\\\"/versionName \\\"\\1-nightly-${{ github.run_number }}\\\"/\" smarttubetv/build.gradle\n          echo \"VERSION_NAME=$(grep \"versionName\" smarttubetv/build.gradle | head -n 1 | awk -F'\"' '{print $2}')\" >> $GITHUB_OUTPUT\n\n      - name: Configure Build Signing\n        if: ${{ env.HAS_SIGNING_KEY == 'true' }}\n        run: |\n          echo \"storePassword=${{ secrets.KEY_STORE_PASSWORD }}\" > keystore.properties\n          echo \"keyAlias=${{ secrets.ALIAS }}\" >> keystore.properties\n          echo \"keyPassword=${{ secrets.KEY_PASSWORD }}\" >> keystore.properties\n          echo \"storeFile=${{ github.workspace }}/key.jks\" >> keystore.properties\n          echo \"${{ secrets.SIGNING_KEY }}\" | base64 --decode > ${{ github.workspace }}/key.jks\n    \n      - name: Grant Gradle permissions\n        run: chmod +x gradlew\n\n      - name: Lint with Gradle\n        run: ./gradlew lintStbetaRelease\n\n      - name: Upload Lint Report\n        uses: actions/upload-artifact@v7\n        if: always()\n        with:\n          name: lint-report\n          path: '**/build/reports/lint-results-*.html'\n          if-no-files-found: warn\n\n      - name: Build with Gradle\n        run: ./gradlew clean assembleStbetaRelease\n\n      - name: VirusTotal Scan\n        if: ${{ env.HAS_VT_KEY == 'true' }}\n        id: vt\n        uses: crazy-max/ghaction-virustotal@v5\n        with:\n          vt_api_key: ${{ secrets.VIRUS_TOTAL_API_KEY }}\n          files: |\n            ./smarttubetv/build/outputs/apk/stbeta/release/*.apk\n          request_rate: 4\n\n      - name: VirusTotal Summary\n        if: steps.vt.outcome == 'success'\n        run: |\n          echo \"Waiting 150s for VirusTotal engines to report...\"\n          sleep 150\n\n          echo \"### Security Scan Results\" >> $GITHUB_STEP_SUMMARY\n          echo \"| Artifact Name | VirusTotal Status | Detailed Report |\" >> $GITHUB_STEP_SUMMARY\n          echo \"| :--- | :--- | :--- |\" >> $GITHUB_STEP_SUMMARY\n\n          for apk in ./smarttubetv/build/outputs/apk/stbeta/release/*.apk; do\n            filename=$(basename \"$apk\")\n            sha256=$(sha256sum \"$apk\" | awk '{print $1}')\n\n            # Construct the dynamic badge URL using the hash\n            badge_url=\"https://badges.cssnr.com/vt/id/$sha256?start=green&end=red&n=8\"\n            vt_link=\"https://www.virustotal.com/gui/file/$sha256\"\n\n            echo \"| $filename | [![$filename]($badge_url)]($vt_link) | [View Report]($vt_link) |\" >> $GITHUB_STEP_SUMMARY\n          done\n\n      - name: Upload ARM64 APK\n        uses: actions/upload-artifact@v7\n        with:\n          name: SmartTube_${{ steps.get_version.outputs.VERSION_NAME }}_arm64\n          path: ./smarttubetv/build/outputs/apk/stbeta/release/*_arm64-v8a.apk\n          if-no-files-found: error\n\n      - name: Upload ARMv7 APK\n        uses: actions/upload-artifact@v7\n        with:\n          name: SmartTube_${{ steps.get_version.outputs.VERSION_NAME }}_armeabi-v7a\n          path: ./smarttubetv/build/outputs/apk/stbeta/release/*_armeabi-v7a.apk\n          if-no-files-found: error\n\n      - name: Upload Universal APK\n        uses: actions/upload-artifact@v7\n        with:\n          name: SmartTube_${{ steps.get_version.outputs.VERSION_NAME }}_universal\n          path: ./smarttubetv/build/outputs/apk/stbeta/release/*_universal.apk\n          if-no-files-found: error\n\n      - name: Upload x86 APK\n        uses: actions/upload-artifact@v7\n        with:\n          name: SmartTube_${{ steps.get_version.outputs.VERSION_NAME }}_x86\n          path: ./smarttubetv/build/outputs/apk/stbeta/release/*_x86.apk\n          if-no-files-found: error\n"
  },
  {
    "path": ".github/workflows/cleanup.yml",
    "content": "name: Cleanup old workflow runs\r\n\r\non:\r\n  schedule:\r\n    - cron: '0 3 * * *'\r\n  workflow_dispatch:\r\n\r\njobs:\r\n  cleanup:\r\n    runs-on: ubuntu-latest\r\n    if: github.event.repository.fork == false\r\n    permissions:\r\n      actions: write\r\n    steps:\r\n      - uses: actions/github-script@v8\r\n        with:\r\n          script: |\r\n            const KEEP = 0;\r\n            const workflowNames = [\"VirusTotal Scan\", \"Cleanup old workflow runs\"];\r\n\r\n            // Get workflow ID\r\n            const workflows = await github.rest.actions.listRepoWorkflows({\r\n              owner: context.repo.owner,\r\n              repo: context.repo.repo\r\n            });\r\n\r\n            for (const name of workflowNames) {\r\n              const wf = workflows.data.workflows.find(w => w.name === name);\r\n              if (!wf) {\r\n                core.info(`Workflow \"${name}\" not found, skipping`);\r\n                continue;\r\n              }\r\n\r\n              // List runs\r\n              const runs = await github.rest.actions.listWorkflowRuns({\r\n                owner: context.repo.owner,\r\n                repo: context.repo.repo,\r\n                workflow_id: wf.id,\r\n                per_page: 100\r\n              });\r\n\r\n              const toDelete = runs.data.workflow_runs.slice(KEEP);\r\n              core.info(`Deleting ${toDelete.length} old runs`);\r\n\r\n              for (const run of toDelete) {\r\n                try {\r\n                  await github.rest.actions.deleteWorkflowRun({\r\n                    owner: context.repo.owner,\r\n                    repo: context.repo.repo,\r\n                    run_id: run.id\r\n                  });\r\n                } catch (e) {\r\n                  core.info(`Skip run ${run.id}: ${e.message}`);\r\n                }\r\n              } \r\n            }\r\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: \"Close stale issues\"\non:\n  workflow_dispatch:\n    branches:\n    - master\n     \njobs:\n  stale:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      actions: write\n      issues: write\n    steps:\n    - uses: actions/stale@v10\n      with:\n        repo-token: ${{ secrets.GITHUB_TOKEN }}\n        operations-per-run: 3000 # This may result in rate limiting, could we reduce and run in batches?\n        days-before-stale: 420 # 1st Jan 2025, as of 25th Feb 2026\n        days-before-close: 0\n        ignore-updates: true\n        stale-issue-message: ''\n        close-issue-message: | \n          **Due to a high volume of stale issues, all issues older than January 1st 2025 are being closed automatically.**\n          \n          Please feel free to resubmit your issue if you believe it has not been appropriately dealt with, and tag this issue in the \"Additional Information\" section."
  },
  {
    "path": ".github/workflows/virustotal_scan.yml",
    "content": "name: VirusTotal Scan\r\n\r\non:\r\n  release:\r\n    types: [published]\r\n  workflow_dispatch:\r\n    inputs:\r\n      release_tag:\r\n        description: 'Release tag to scan'\r\n        required: true\r\n\r\njobs:\r\n  virustotal_scan:\r\n    permissions:\r\n      contents: write\r\n    runs-on: ubuntu-latest\r\n    env:\r\n      HAS_VT_KEY: ${{ secrets.VIRUS_TOTAL_API_KEY != '' }}\r\n\r\n    steps:\r\n      - name: Set tag variable\r\n        run: |\r\n          if [ \"${{ github.event_name }}\" = \"workflow_dispatch\" ]; then\r\n            echo \"TAG_NAME=${{ github.event.inputs.release_tag }}\" >> $GITHUB_ENV\r\n          else\r\n            echo \"TAG_NAME=${{ github.event.release.tag_name }}\" >> $GITHUB_ENV\r\n          fi\r\n\r\n      - name: Set report marker variable\r\n        run: |\r\n          echo -e \"MARKER=\\t\\t\\t\" >> $GITHUB_ENV\r\n\r\n      - name: Checkout code\r\n        uses: actions/checkout@v6\r\n\r\n      - name: Download Release Assets\r\n        env:\r\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\r\n        run: |\r\n          mkdir -p release_assets\r\n          gh release download \"$TAG_NAME\" --dir release_assets --pattern \"*.apk\"\r\n\r\n      - name: VirusTotal Scan\r\n        if: ${{ env.HAS_VT_KEY == 'true' }}\r\n        id: vt\r\n        uses: crazy-max/ghaction-virustotal@v5\r\n        with:\r\n          vt_api_key: ${{ secrets.VIRUS_TOTAL_API_KEY }}\r\n          files: |\r\n            release_assets/*.apk\r\n          request_rate: 4\r\n\r\n      - name: Generate Custom Badge Report\r\n        if: steps.vt.outcome == 'success'\r\n        run: |\r\n          echo \"Waiting 150s for VirusTotal engines to report...\"\r\n          sleep 150\r\n\r\n          echo -e \"$MARKER\\n## 🛡️ VirusTotal Analysis\" > vt_report.txt\r\n          echo \"\" >> vt_report.txt\r\n\r\n          echo \"| Build Variant | VirusTotal Status | Detailed Report |\" >> vt_report.txt\r\n          echo \"| :--- | :--- | :--- |\" >> vt_report.txt\r\n          \r\n          for file in release_assets/*.apk; do\r\n            [ -e \"$file\" ] || continue\r\n            filename=$(basename \"$file\")\r\n            sha256=$(sha256sum \"$file\" | awk '{print $1}')\r\n\r\n            vt_link=\"https://www.virustotal.com/gui/file/$sha256/detection\"\r\n            badge_url=\"https://badges.cssnr.com/vt/id/$sha256?start=green&end=red&n=8\"\r\n\r\n            echo \"Purging badge cache for $filename...\"\r\n            curl -s -X POST $badge_url\r\n            \r\n            asset_link=\"https://github.com/${{ github.repository }}/releases/download/${{ env.TAG_NAME }}/$filename\"\r\n            \r\n            echo \"| [$filename]($asset_link) | [![]($badge_url&v=$(date +%s))]($vt_link) | [View Report]($vt_link) |\" >> vt_report.txt\r\n          done\r\n\r\n      - name: Update Release Notes\r\n        env:\r\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\r\n        run: |\r\n          gh release view \"$TAG_NAME\" --json body -q .body > current_notes.txt || echo \"\" > current_notes.txt\r\n\r\n          # If the header exists, delete from that line to the end\r\n          if grep -q \"$MARKER\" current_notes.txt; then\r\n            echo \"Previous report found. Cleaning up...\"\r\n            sed -i \"/$MARKER/,\\$d\" current_notes.txt\r\n          fi\r\n          \r\n          {\r\n            cat current_notes.txt\r\n            cat vt_report.txt\r\n          } > final_notes.txt\r\n          \r\n          gh release edit \"$TAG_NAME\" --notes-file final_notes.txt\r\n"
  },
  {
    "path": ".gitignore",
    "content": "# built application files\n*.apk\n*.ap_\n\n# files for the dex VM\n*.dex\n\n# Java class files\n*.class\n\n# generated files\nbin/\ngen/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Windows thumbnail db\nThumbs.db\n\n# OSX files\n.DS_Store\n\n# Eclipse project files\n.classpath\n.project\n\n# Android Studio\n.idea\n*.iml\n\n#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.\n.gradle\n.google\nbuild/\n\n# my other stuff\n/smartyoutubetv2/release\n/smarttube/release\n/smarttubetv/release\n# gradle 6 fix\n/smarttube/output\n/smarttubetv/output\n/smarttubemobile/release\n# google-services.json\n*.hprof\nlog.txt\nandroid.keystore\nandroid-upd2.keystore\nbuild.log\nnotes.txt\nout.txt\nout1.txt\ndeps.txt\nlogcat.txt\n/releases\n/files\n/misc\n/other\n/TODO.txt\ntmp/\n*_bak*\n*_tmp\n*.bak*\n*.tmp\n*.7z\n.DS_Store\n/captures\n.externalNativeBuild\nfabric.properties\n*BAK.java\nhs_err_pid*.log\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"SharedModules\"]\n\tpath = SharedModules\n\turl = https://github.com/yuliskov/SharedModules\n[submodule \"MediaServiceCore\"]\n\tpath = MediaServiceCore\n\turl = https://github.com/yuliskov/MediaServiceCore\n"
  },
  {
    "path": ".reuse/dep5",
    "content": "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\r\nUpstream-Name: SmartTube\r\nSource: https://github.com/yuliskov/SmartTube\r\n\r\nFiles: *\r\nCopyright: 2020-present yuliskov\r\nLicense: MIT\r\n\r\nLicense: MIT\r\n Permission is hereby granted, free of charge, to any person obtaining a copy\r\n of this software and associated documentation files (the \"Software\"), to deal\r\n in the Software without restriction, including without limitation the rights\r\n to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r\n copies of the Software, and to permit persons to whom the Software is\r\n furnished to do so, subject to the following conditions:\r\n .\r\n The above copyright notice and this permission notice shall be included in all\r\n copies or substantial portions of the Software.\r\n .\r\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\r\n SOFTWARE."
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020-present yuliskov\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\r\n  <a href=\"https://f-droid.org/packages/app.smarttube.fdroid/\">\r\n    <img src=\"https://fdroid.gitlab.io/artwork/badge/get-it-on-en.svg\" alt=\"Get it on F-Droid\" width=\"206\" style=\"max-width:100%; height:auto;\"/>\r\n  </a>\r\n</p>\r\n\r\n# Important announcement about the app\r\n\r\nMy development environment was infected by unknown malicious software, as a result of which a few builds may have been affected. Once the issue was detected, I secured everything with a full disk wipe, restored a clean setup, and now all builds are scanned with VirusTotal. The F-Droid version will also be verified before release.\r\n\r\nPublic keys may have been compromised, which is why I am sharing this issue. You can download the new version and the new public key below, and instructions for restoring backups are provided.\r\n\r\nNo extra actions are required since the app uses **one-time connection codes**. These codes have very limited permissions (for example, they cannot change your password). Still, you can revoke them if you want full peace of mind.\r\n\r\n# How to revoke access:\r\n\r\n1. Open [myaccount.google.com/security](https://myaccount.google.com/security)\r\n2. Find **“Your connections to third-party apps & services”**\r\n3. Tap **“See all connection”** and locate **YouTube TV** or **Google Drive**\r\n4. Select the app → **“Remove access”**\r\n\r\nPlease keep built-in security features enabled to stay protected.\r\n\r\n# SmartTube\r\n  \r\n<!-- <img width=\"100\" src=\"https://github.com/yuliskov/SmartTube/blob/master/smarttubetv/src/ststable/res/mipmap-nodpi/app_icon.png\" alt=\"logo\"/> -->\r\n\r\n![The app screenshot](./images/browse_home.png)\r\n\r\nSmartTube is a free and open-source advanced media player for Android TVs and TV boxes. It allows you to play content from various public sources.\r\n\r\n### ✅ Features\r\n- No ads  \r\n- SponsorBlock integration  \r\n- Adjustable playback speed  \r\n- 8K resolution support  \r\n- 60fps playback  \r\n- HDR compatibility  \r\n- View live chat  \r\n- Customizable buttons  \r\n- Does not require Google Services  \r\n- Helpful international community\r\n\r\n### ❌ Limitations\r\n- Not supported on phones and tablets  \r\n- Comment functionality is unstable  \r\n- Voice search and casting performance may be inferior to official apps, depending on your device  \r\n\r\nGive it a try!\r\n\r\n**Do you have any question?** Ctrl+F or ⌘F this readme first!\r\n\r\n[**Installation**](#installation) | [**Official Site**](https://smarttubeapp.github.io) | [**Donation**](#donation) | [**FAQ**](#faq) | [Support / Chat](#support) | [Build](#build) | [Translate the app](https://jtbrinkmann.de/tools/android-strings.xml-translator.html) | [Changelog](https://t.me/s/SmartTubeNewsEN) | [Liability](#liability)\r\n\r\n\r\n## Device support\r\n> [!IMPORTANT]  \r\n> Starting in October 2025 new Amazon FireTV devices no longer run Android under the hood. SmartTube will **not** be compatibile with the Fire Stick 4k Select and newer devices which run Amazon's own VegaOS.\r\n\r\n![Device support image](images/new/compatibility.png)\r\n* **Supported:** all Android TVs and TV boxes (incl. All FireTV devices released before Oct. 2025, NVIDIA Shield & Chromecast with Google TV), even older ones with Android 4.3 (Kitkat).\r\n* **Not supported:** Smartphones, non-Android platforms like Samsung Tizen, LG webOS, Apple TV, etc.\r\n\r\n## Installation\r\n\r\n> [video of the installation](images/new/zPV0imF.mp4) (note: download url changed to `kutt.to/stn_beta` or `kutt.to/stn_stable`)\r\n\r\n**Do not** download SmartTube from any **app store**, APK websites or blogs; these were uploaded by other people and may contain malware or ads. SmartTube is not officially published on any app store. Sadly, the Google PlayStore does not allow ad-free Youtube apps using unofficial APIs.\r\n\r\nThere is a **beta release** (recommended) and a **stable release**. Beta gets new features and bugfixes faster than the stable release.\r\n\r\nYou can use either of the following methods to install the app:\r\n\r\n- (**Easiest**) Install [Downloader by AFTVnews](https://www.aftvnews.com/downloader/) on your Android TV, open it and enter `kutt.to/stn_beta` or `kutt.to/stn_stable`, then read, understand and confirm the security prompts. (<small>You can also enter [**79015**](https://aftv.news/79015) (for beta) or [**28544**](https://aftv.news/28544) (for stable), but this requires an extra step to install the AFTVnews Downloader browser addon if you haven't already.</small>)\r\n- Install a file transfer app on your Android TV, download the APK on your phone or computer and transfer it to your TV (e.g. [_Send Files to TV_](https://sendfilestotv.app/) from the Google Play Store / Amazon AppStore)\r\n- Download the APK onto a USB stick, put the USB stick into your TV and use a file manager app from the Google Play Store / Amazon AppStore (e.g. [_FX File Explorer_](https://play.google.com/store/apps/details?id=nextapp.fx) or [_X-plore_](https://play.google.com/store/apps/details?id=com.lonelycatgames.Xplore)). Android's preinstalled file manager does not work! Do **not** get the ad-infested _FileCommander_.\r\n- If you are an advanced user, you can install it using ADB. [guide](https://fossbytes.com/side-load-apps-android-tv/#h-how-to-sideload-apps-on-your-android-tv-using-adb) | [alternative guide](https://www.aftvnews.com/sideload/)\r\n\r\n**Troubleshooting:** See device specific notes below. If installation fails, either your **disk space is full** or the APK file didn't download correctly; clear up space and try downloading again. If the app installed, but crashes when opening, make sure to install it to internal memory, not to an SD card / external storage.\r\n\r\n**The app has a built-in updater** with changelog. You can also find all releases and the **changelog** on the [Telegram channel @SmartTubeNewsEN](https://t.me/s/SmartTubeNewsEN) (readable without account) or on [Github](https://github.com/yuliskov/SmartTube/releases/).\r\n\r\n> latest [**beta download**](https://github.com/yuliskov/SmartTube/releases/download/latest/smarttube_beta.apk)\r\n>\r\n> latest [stable download](https://github.com/yuliskov/SmartTube/releases/download/latest/smarttube_stable.apk)\r\n\r\n\r\n### Installation (Chromecast with Google TV)\r\n\r\nOn **Chromecast with Google TV**, installation of apps is blocked by default, so an extra step is required:\r\n\r\n> **4.1. Enable Developer Options**\r\n>\r\n> On your Chromecast, open the side menu and go to _Settings > System > About_. Scroll down to the _Android TV OS build_ section and click that repeatedly. A toast message will appear, explaining that you are a few steps away from being a developer. Continue clicking until you trigger it.\r\n>\r\n>\r\n> **4.2. Turn on the \"unknown sources\" setting**\r\n>\r\n> Go back to the main _Settings_ page and select _Apps > Security & Restrictions > Unknown sources_. Turn on the toggle for \\[_Downloader by AFTVnews_ or\\] whichever file browser you decided to use [...].\r\n>\r\n> [[source & picture guide](https://www.androidpolice.com/2021/02/07/how-to-sideload-any-apk-on-the-chromecast-with-android-tv/#install-the-apk)]\r\n\r\nAfter this, you can follow the [general installation guide](#installation) above.\r\n\r\n\r\n### Installation (Xiaomi devices with Chinese firmware)\r\n\r\nXiaomi's **Chinese firmware** might block the installation **of the beta version**. The international firmware is not affected. Solutions:\r\n1. Use SmartTube's **stable version** instead (**recommended**)\r\n2. Use the international firmware for your device\r\n3. (if your device is from 2020 or before) You can do a factory reset and then install SmartTube beta before doing any system updates. You can then safely update your system, SmartTube should continue working.\r\n\r\n\r\n### Updating\r\n\r\nThe app has a built-in updater. You only need to follow the installation procedure **once**. A few seconds after launching SmartTube, it will notify you if there is any update and also show a changelog. You can disable automatic update checks or manually update in the settings under \"about\".\r\n\r\nIf the installation fails, either your **disk space is full** or the update didn't download correctly; clear up space and try updating again (_Settings > About > Check for updates_).\r\n\r\n\r\n## Compatibility\r\n\r\nSmartTube requires Android 4.3 or above. It does not work on non-Android devices (incl. LG or Samsung TVs). On unsupported TVs, you can use a TV stick or TV box. Though this app technically runs on smartphones and tablets, it is not optimized for such and offers no official support!\r\n\r\nIt has been successfully tested on TVs, TV boxes and TV sticks that are based on Android, including:\r\n\r\n- Android TVs & Google TVs (e.g. Philips, Sony)\r\n- Chromecast with Google TV & TVs with _Chromecast built-in_\r\n- Amazon FireTV stick (all generations)\r\n- NVIDIA Shield\r\n- TV boxes running Android (many cheap chinese no-name boxes)\r\n- Xiaomi Mi Box\r\n\r\n\r\n## Features\r\n\r\n### Adblocking\r\n\r\nSmartTube does not show any ad banners, preroll ads or ad intermissions. It not just tries to prevent them, it is literally programmed to be completely **unable** to display any ads, so YouTube cannot slip anything in. This also means you cannot allow ads or whitelist channels. Some YouTube channels include sponsored messages in their videos, these can also be skipped, see [SponsorBlock](#SponsorBlock) below.\r\n\r\n\r\n### SponsorBlock\r\n\r\nSmartTube includes a SponsorBlock integration. From the [SponsorBlock website](https://sponsor.ajay.app/):\r\n\r\n> SponsorBlock is an open-source crowdsourced browser extension and open API for **skipping sponsor segments** in YouTube videos. [...] the extension automatically skips sponsors **it knows about** using a privacy preserving query system. It also supports skipping **other categories**, such as intros, outros and reminders to subscribe [and non-music parts in music videos].\r\n\r\nYou can select which categories you want to skip in the settings. Unlike the browser addon, in SmartTube you cannot submit new segments (TVs and TV remotes aren't great devices for such precise operations). Note that SponsorBlock is a free and voluntary project based on user submissions, so don't expect it to 100% work every time. Sometimes, sponsor segments are not yet submitted to the database, sometimes the SponsorBlock servers are offline/overloaded.\r\n\r\n\r\n### Casting\r\n\r\nTo cast videos from your phone (or other devices), you must link that device to your TV. Unlike the original YouTube app, SmartTube does not automatically show up when you are in the same wifi network. How to link your smartphone and TV:\r\n\r\n1. open SmartTube and go to settings\r\n2. go to \"Remote control\" (2nd option)\r\n3. open your YouTube app on your phone, go to settings > General > watch on TV\r\n4. click on _connect using TV-code_ and enter the code from your TV\r\n\r\n[**Screenshot guide**](https://t.me/SmartTubeEN/8514)\r\n\r\nDue to technical limitations, you need to open the app on the TV before casting; SmartTube cannot automatically wake up the TV.\r\n\r\n\r\n### Picture-in-Picture (PiP)\r\n\r\nSmartTube supports playing videos in PiP mode. This needs to be enabled under _Settings > General > Background playback > Picture in picture_. The video will go into PiP mode when you press home while playing a video, and also when you press _back_ if enabled in _Settings > General > Background playback (activation)_.\r\n\r\n\r\n### Adjust Speed\r\n\r\nYou can adjust the playback speed pressing the speed-indicator icon (gauge) in the top row of the player. This is remembered across videos. Some speeds may case frame drops, this is a known issue.\r\n\r\n\r\n### Voice Search\r\n\r\nTo enable global voice search, an additional app must be installed alongside SmartTube. This _bridge app_ can intercept the System's attempts to open the original YouTube app and open SmartTube instead. For this to work, you must uninstall the original YouTube app. We know this sucks, but you can always reinstall it if you change your mind. The _bridge app_ will not show up in your launcher and you cannot launch it directly; it is only used internally by the system's voice search. On some devices, you need to explicitly say \"Youtube\" when searching (e.g. say \"youtube cute cats\" instead of just \"cute cats\").\r\n\r\n**On Amazon Fire TV**: \r\n\r\n1. Uninstall the original YouTube app (no root required)\r\n2. Download and install the Amazon Bridge SmartTube app: https://kutt.to/stn_bridge_amazon (e.g. via _Downloader by AFTVnews_)\r\n\r\n\r\n**On Google Chromecast with Google TV**: \r\n\r\n1. Uninstall the original YouTube app (no root required)\r\n2. Download and install the ATV Bridge SmartTube app: https://kutt.to/stn_bridge_atv (e.g. via _Downloader by AFTVnews_)\r\n\r\n\r\n**On all other Android devices**, sadly root is required to enable this:\r\n\r\n1. Root your device (search for a guide for your specific device)\r\n2. Uninstall the official YouTube app using root (`adb shell pm uninstall com.google.android.youtube.tv`)\r\n3. Download and install the ATV Bridge SmartTube app: https://kutt.to/stn_bridge_atv (e.g. via _Downloader by AFTVnews_)\r\n\r\n\r\n## Donation\r\n\r\nIf you want to support my developments you are welcome to buy me a cup of coffee :)\r\n\r\n- [**Patreon (Visa, Mastercard, PayPal)**](https://www.patreon.com/smarttube)  \r\n- **PayPal**: firsth<!-- abc@def -->ash@gmai<!-- @abc.com -->l.com  \r\n<!-- > [**Buy me a coffee**](https://www.buymeacoffee.com/stube) -->  \r\n- **BTC**: 1JAT5VVWarVBkpVbNDn8UA8HXNdrukuBSx  \r\n- **LTC**: ltc1qgc24eq9jl9cq78qnd5jpqhemkajg9vudwyd8pw  \r\n- **ETH**: 0xe455E21a085ae195a097cd4F456051A9916A5064  \r\n- **ETC**: 0x209eCd33Fa61fA92167595eB3Aea92EE1905c815  \r\n- **TRX**: TJNPY794aSGZf3WGHTna2VCWm2G5Yua7E8  \r\n- **USDT (TRC20)**: TJNPY794aSGZf3WGHTna2VCWm2G5Yua7E8  \r\n- **USDT (BEP20)**: 0x64B28da787BE6ac5889D276A5638d4f077840eC5  \r\n- **USDT (ERC20)**: 0xe455e21a085ae195a097cd4f456051a9916a5064   \r\n- **TON**: UQAc9zgnnzwS8yb5wxAu5CB0RddmjPBjWI-n46oQ7XfCQrgI  \r\n- **XMR**: 48QsMjqfkeW54vkgKyRnjodtYxdmLk6HXfTWPSZoaFPEDpoHDwFUciGCe1QC9VAeGrgGw4PKNAksX9RW7myFqYJQDN5cHGT    \r\n\r\n\r\n## Support\r\n\r\n**Please check the [FAQ](#faq) first!** Also at least have a short look at the recent chat history.\r\n\r\nYou can report in our Telegram group or via [issue tracker on Github](https://github.com/yuliskov/SmartTube/issues) (account required).\r\n\r\n- **Telegram group (international)**: [@SmartTubeEN](http://t.me/SmartTubeEN)  \r\n- **Discord group (international)**: [SmartTube Official](https://discord.gg/Wt8HDDej5z)  \r\n- **Telegram group (RU/UA)**: [@SmartTubeUA](http://t.me/SmartTubeUA)  \r\n- **Email**: firsth<!-- abc@def -->ash@gmai<!-- @abc.com -->l.com\r\n\r\nThe international group is in **English only**. But don't worry if your English is not perfect, we have a friendly international community.\r\n\r\n\r\n## Team\r\n\r\nSmartTube is developed single-handedly; there is no larger team or company behind this. This is an open source, hobby project. Several others have helped with translations, some of which can be seen on [Github](https://github.com/yuliskov/SmartTube/graphs/contributors), some have sent their translations directly to Yurii. There are also helpful people in the support chat.\r\n\r\n\r\n## Build\r\n    \r\n**NOTE: OpenJDK 14 or older (!) is required. Newer JDK could cause app crash!**  \r\nTo build and install debug version, run these commands:\r\n\r\n```\r\ngit clone https://github.com/yuliskov/SmartTube.git\r\ncd SmartTube\r\ngit submodule update --init\r\nadb connect <device_ip_address>\r\ngradlew clean installStstableDebug\r\n```\r\n\r\n\r\n## Video codecs\r\n\r\nVideo codecs are the algorithms used for video compression.\r\n\r\n\r\n### Which codec to choose / overview\r\n\r\n|          | recommendation                               | hardware support             | compression, bitrate\\*              | quality |\r\n|:--------:|:---------------------------------------------|:---------------------------- |:----------------------:|:-------:|\r\n| **AV01** aka. AV1 | best choice, **if your device supports** | first devices started coming in **2020**   | **best** <small>(e.g. 1.6 Mbps)</small>   | same   |\r\n| **VP9**  | **best choice on most devices**         | most devices **since 2015** | **better** <small>(e.g. 2.1 Mbps)</small> | same   |\r\n| AVC  | only for old or slow hardware | **all** devices             | good <small>(e.g. 2.7 Mbps)</small>   | same   |\r\n\r\n\r\n<small>\\* Examples taken from the video-only track at 1080p @ 25fps for this video: [Dua Lipa - New Rules (Official Music Video)](https://youtube.com/watch?v=k2qgadSvNyU)</small>\r\n\r\nAt the same resolution, a **lower bitrate is better!** YouTube explicitly targets the **same quality** regardless of the codec. Older codecs have a higher bitrate only because they are less efficient. On Youtube, you **do not** get better quality by simply choosing a higher bitrate. Newer codecs have a better compression = lower bitrate = use less bandwidth = save the environment. This is a feature, not a bug. You should use the newest codec that works smoothly on your device, not the least efficient one. AVC usually has the highest bitrate. This is bad, not good.\r\n\r\n\r\n### Which quality to choose?\r\n\r\nCurrently, there is no automatic mode based on your bandwidth. But you can configure a default video preset yourself under settings \\> video player \\> video presets. The first option (\"none\") will remember your last selection within the video player. Any other preset is used initially for each video; if the selected profile is not available, the next best available option is used. You can still override the profile on each video individually within the player.\r\n\r\nTo decide the optional resolution / video quality for you, you need to consider a few limiting factors:\r\n- Your bandwidth (choose only up to the bitrate that your bandwidth can handle; you can do a speedtest using [fast.com](https://fast.com) by Netflix)\r\n- Your TV's display resolution (the quality **might slightly** improve, if you select the next higher resolution, e.g. 1080p on a 720p display; but don't expect a big difference)\r\n- Your TV's capabilities (e.g. HDR, 60fps)\r\n\r\nGenerally 60fps is an improvement, but if you personally don't notice (or mind) the difference, you can save bandwidth (and the environment) by not choosing 60fps.\r\n\r\n\r\n### HDR\r\n\r\nHDR works only **if your hardware supports it**. It's a complicated mess:\r\n- Your TV must support it\r\n- If you use a TV box, that TV box **and** your TV cable **and** the TV must support HDR\r\n- Yes, there truly are different HDMI cable versions with different HDR-support, it's complicated\r\n- some devices (like the **NVIDIA Shield**) generally support HDR, but **not** the specific HDR format that is used on YouTube :cry:\r\n \r\nIf HDR videos look looked dim or washed out, then check [this article](https://www.wired.com/story/hdr-too-dark-how-to-fix-it/). **If HDR is not working**, it's probably not this app's fault. You might need to search on the web for \"HDR\" and your device name for any help.\r\n\r\n## Liability\r\n\r\nWe take no responsibility for the use of our tool, or external instances\r\nprovided by third parties. We strongly recommend you abide by the valid\r\nofficial regulations in your country. Furthermore, we refuse liability\r\nfor any inappropriate use of Invidious, such as illegal downloading.\r\nThis tool is provided to you in the spirit of free, open software.\r\n\r\nYou may view the LICENSE in which this software is provided to you [here](./LICENSE).\r\n\r\n>   16. Limitation of Liability.\r\n>\r\n> IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\r\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\r\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\r\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\r\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\r\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\r\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\r\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\r\nSUCH DAMAGES.\r\n\r\n## FAQ\r\n\r\n### Q: Videos buffer a LOT\r\n\r\nA: Try to switch to encrypted DNS like NextDNS. You can set-up such DNS either automatically or manually. To automatic set-up you can use the [Intra apk at fdroid](https://f-droid.org/en/packages/app.intra/) and the [\"AutoStart - No root\"](https://play.google.com/store/apps/details?id=com.autostart) apk to make it autolaunch after every TV restart. For manual set-up [use this guide](https://www.reddit.com/r/MiBox/s/7esEVGtAAa).\r\n\r\n### Q: There is no result for the search that I say (Android 11)\r\n\r\nA: They're some reports that the latest update for \"Google app for Android TV\" could cause this bug. Deleting the update should fix the problem.\r\n\r\n\r\n### Q: AV01 does not play / Why is VP9 slow on my device?\r\n\r\nA: Because AV01 is very new, **most** TVs and TV boxes **do not** offer hardware support and **cannot** play AV01 **at all**.\r\n\r\nIf your device has hardware support for a codec, videos using that codec should play smoothly. High resolutions might also be slow in VP9 on cheap TV boxes that don't officially support 4k. Your device probably can play VP9 videos even without hardware support, however this requires a powerful CPU to run smoothly. Fixing AV01 without hardware support is technically possible, but currently not planned and probably not efficient enough.\r\n\r\n\r\n### Q: Can you make SmartTube look like the original app?\r\n\r\nA: Compared to SmartTube's UI, Stock Youtube and YT Kids are far ahead. However, we'd need someone who's skilled and willing to dedicate enough time and energy into making it. And into maintaining it longterm (incl. new features, bug fixes). All of this for free. If you are / got someone like that, please help.\r\nNot to mention that SmartTube follows Google's official template & recommendations for Android TV apps. It's Google's fault that the template is somewhat ugly. 😂\r\n\r\n\r\n### Q: Can the search page be improved?\r\n\r\nA: It can be, but it takes someone to do it, similar to the above FAQ-entry. SmartTube is following Google's officially recommended design/template for TV apps and is using the official, preinstalled Android TV keyboard. Sadly, Google did a really bad job regarding the search page and keyboard. Maybe a future SmartTube update can add an embedded keyboard, similar to the original YouTube or other major Android TV apps. Maybe it can improve the looks to be as good or better than in the official YouTube app. But for now, it is the way it is due to lack of time and due to Google's official recommendations being bad.\r\n\r\n\r\n### Q: Can I install this on a Samsung Tizen TV / LG webOS TV / Roku / iOS / toaster?\r\n\r\nA: No, this only works on **Android** devices. If you look at an Android TV's product page, it usually says clearly that it's based on Android. The app **cannot** easily be ported over to other plattforms and we have no plans to even try. **Please do not ask**. Instead, you can connect a separate TV stick or TV box to your TV.\r\n\r\n\r\n### Q: Can I install this on a smartphone? / Can you add portrait mode? / Scrolling doesn't work.\r\n\r\nA: **Big No**. This app is **not** for smartphones, we offer **zero support** for that.\r\n\r\nYou **can cast** videos **from** your smartphone to a TV / TV box running SmartTube, though. Just use the official YouTube app or [ReVanced](https://github.com/ReVanced), see [the casting section](#casting) for more information.\r\n\r\n**There will not be a phone version.** You can use [ReVanced](https://github.com/ReVanced), [Pure Tuber](https://play.google.com/store/apps/details?id=free.tube.premium.advanced.tuber), [NewPipe](https://newpipe.schabi.org), or [NewPipe x SponsorBlock](https://github.com/polymorphicshade/NewPipe#newpipe-x-sponsorblock) instead. Please go to their respective support chats for help.\r\n\r\n\r\n### Q: Can I install this on a tablet / car screen / smartphone with docking station?\r\n\r\nYes... maybe.. Requirements:\r\n\r\n- It is an Android device\r\n- It has a large screen\r\n- It has a TV remote, controller, or keyboard\r\n  **Touch input is not supported.** Mouse/touchpad scrolling neither. You cannot properly use SmartTube with only touch or mouse input.\r\n\r\nSome users reported great success (incl. on a [car entertainment system](https://t.me/SmartTubeEN/6060)). **Please share your success stories with us.**\r\n\r\n\r\n### Q: I get \"unknown codec\" / \"can't download video\" errors\r\n\r\nA: please wait 5 seconds for the video to play. If that doesn't help, press the play button. Some users reported, that this issue only appears when they have a USB audio device attached or if their disk storage is full.\r\n\r\n\r\n### Q: I get \"the video profile is not supported\"\r\n\r\nA. Press the \"HQ\"-button in the bottom-left, select _video formats_ and select anything other than AV01. AV01 is **not supported** on most devices (apparently including yours), so select VP9 instead. See [the section on video codecs](#Video-codecs) for more information.\r\n\r\n\r\n### Q: I get \"video unavailable\" when watching unlisted videos / my own videos\r\n\r\nA: Right, that's currently a bug.\r\n\r\n\r\n### Q: It doesn't show up on my casting list\r\n\r\nA: Please read the [Casting](#casting) section.\r\n\r\n### Q: I get an error saying \"Sign in to confirm you're not a bot\"\r\n\r\nA: Your IP address range might be temporaily/permanently blocked by YouTube from watching videos if you not signed in to your account.\r\n\r\n### Q: The video is buffering a lot\r\n\r\nA: The issue might not be specific to SmartTube, as other unofficial YouTube apps also report this issue. It seems uncommon nowadays, but was very present in the 2nd quarter of 2021. Some users or devices seem to be more affected then others. The official YouTube app & website are apparently only rarely affected. The root cause of the issue is currently unclear, but it appears to be a server-side thing on YouTube's end. Possibly, YouTube is discriminating 3rd party apps.\r\n\r\nFor now, try to see if it helps to:\r\n\r\n- Reduce the resolution (or chance it back)\r\n- Change the video format to AVC\r\n- Increase the buffer in the settings\r\n- Hit the back button and try playing the video again\r\n\r\n\r\n### Q: The debug information says my display is 1080p, but I have a 4k/UHD display!\r\n\r\nA: Do not worry, **the debug information is incorrect.** SmartTube works fine even above 1080p and you should be able to see that, when you play a video in 4k or UHD.\r\nAlso do not worry if it says \"720p\" and you have a 1080p display.\r\n\r\n\r\n### Q: Why does it not autoselect highest quality?\r\n\r\nA: **It does** (by default). If you set a _video profile_ under settings, that acts as a maximum for automatic selection. Check if you configured a video profile, you can unset it by choosing \"none\".\r\n\r\n**Please do not confuse quality with bitrate**. See [the section on video codecs](#Video-codecs) for more information.\r\n\r\n\r\n### Q: Can I set a (maximum) resolution by default?\r\n\r\nA: SmartTube automatically select the highest available quality for your video, up to a maximum resolution that you can set in the settings under \"video profile\". If available, SmartTube will pick the selected video profile, or otherwise the next best one available will be used. You can still always change the video profile while watching videos.\r\n\r\n\r\n### Q: Can it set the resolution to \"auto\", depending on my available bandwidth?\r\n\r\nA: This is planned, but not available yet (sorry 🙇‍♀️). However, you can set a maximum resolution to something that should work for your bandwidth. See above for details.\r\n\r\n\r\n### Q: Why does it skip video segments?\r\n\r\nA: SmartTube has a feature called **SponsorBlock**. You can select categories should be skipped, if any. See the [SponsorBlock section](#sponsorblock) for more details.\r\n\r\n\r\n### Q: How to start the next video automatically / stop after every video?\r\n\r\nA: You can switch between different autoplay-modes using the loop-button 🔁\r\n\r\n[![screenshot showing the loop-button](images/new/V3GHGvWprmdE1w.jpg)](https://t.me/SmartTubeEN/24953)\r\n\r\n\r\n### Q: How to remove recommended videos (e.g. news) that are unrelated to me?\r\n\r\nA: Recommended videos are defined by YouTube and not by the app, we cannot change the algorithm. They are based on your country, which you can change in the settings. If you are logged in, they are based on your watch history, user profile data, and whatever else Google might use. If you are not logged in, you are like in \"incognito mode\", so your watch history does not influence your recommendations. Maybe a future version will add optional user profiling without logging in.\r\n\r\n\r\n### Q: Does HDR work?\r\n\r\nA: Yes, HDR works **if your hardware** supports it. The **NVIDIA Shield** does not. See [the section on HDR](#HDR) for more information.\r\n\r\n\r\n### Q: Why do some updates say \"don't update if satisfied with the current version\" in the changelog?\r\n\r\nA: These updates change a lot of code, trying to fix bugs that only affect a few users/devices. Only the affected users should update. For anyone else, there is nothing to gain from updating; however there is the chance of causing new bugs. Do not worry if you updated anyways.\r\n\r\n\r\n### Q: When playing at other speeds, frames are skipped!\r\n\r\nA: We currently cannot fix this, sorry.\r\n\r\n\r\n### Q: What is AFR?\r\n\r\nA: \"Auto Frame Rate\". It adjusts the refresh rate of your TV to match the content you're watching. It can slightly improve the smoothness, but the difference is very small; most people barely notice it. It does not work well on every hardware. If you don't know what it does and don't want to test it out yourself, you can safely keep it off.\r\n\r\n**Recommendation:** You can turn it on to see if it works on your device; if it causes issues (or if you don't care to test), turn it **off**.\r\n\r\n\r\n### Q: Should I choose high or low buffer?\r\n\r\nA: The higher your buffer, the more of a video will be preloaded ahead of your current position. A low buffer might minimally reduce your bandwidth usage, if you often close videos before they end. A high buffer can smooth out network issues and prevent the video from pausing to buffer. A higher buffer increases RAM usage, however this shouldn't be an issue.\r\n\r\n**Recommendation: high**.\r\n\r\n\r\n### Q: Can I retain the buffer when seeking back?\r\n\r\nA: No, when you seek back (e.g. jump back 5 seconds), SmartTube will have to rebuffer. This might be improved in a future update.\r\n\r\n\r\n### Q: My device freezes when watching YouTube\r\n\r\nA: That's a firmware or Android issue. If you are using a custom rom, maybe that rom is buggy. Because this issue is nearly impossible for the developer to debug, we cannot help you, sorry. You can try the usual workarounds: rebooting, clearing cache, reinstalling the app, or factory resetting the device.\r\n\r\n\r\n### Q: Can I download videos?\r\n\r\nA: Not with SmartTube\r\n\r\n\r\n### Q: Can updates be installed automatically?\r\n\r\nA: No, this is technically not possible. Only the preinstalled app manager (usually Google PlayStore, Amazon AppStore, etc) has the required permission. All other apps, incl. SmartTube can only show open installation prompt. A workaround using root would be possible, but hasn't been implemented yet.\r\n\r\n\r\n### Q: Can I whitelist ads on some channels?\r\n\r\nA: No, this is not possible. SmartTube does not have any code to display ads. Adding this functionality would actually take time and effort, which is instead spent on adding useful features and fixing bugs.\r\n\r\n\r\n\r\n"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\napply from: gradle.ext.sharedModulesConstants\n\n// NOT working\n// Running 'gradle wrapper' will generate gradlew\nwrapper {\n    gradleVersion = gradleVersion\n    distributionType = Wrapper.DistributionType.BIN\n}\n\nbuildscript {\n    apply from: gradle.ext.sharedModulesConstants\n\n    repositories {\n        google()\n        mavenCentral()\n        // IntelliJ 'Read timed out' (error 403)\n        // https://stackoverflow.com/questions/74258160/is-jcenter-down-permanently-31-oct\n        //jcenter()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:7.4.2'\n        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:' + kotlinVersion\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        google()\n        mavenCentral()\n        maven { url 'https://jitpack.io' }\n        //flatDir { dirs 'libs' } // include duktape-my to all subprojects\n        // IntelliJ 'Read timed out' (error 403)\n        // https://stackoverflow.com/questions/74258160/is-jcenter-down-permanently-31-oct\n        //jcenter()\n    }\n    configurations.all {\n        // WorkManager conflict resolution\n        resolutionStrategy.force 'androidx.lifecycle:lifecycle-livedata-core:' + liveDataVersion\n        resolutionStrategy.force 'androidx.lifecycle:lifecycle-livedata:' + liveDataVersion\n        resolutionStrategy.force 'androidx.lifecycle:lifecycle-runtime:' + liveDataVersion\n        // Force Android 4 compatible okHttp version\n        resolutionStrategy.force 'com.squareup.okhttp3:okhttp:' + okhttpVersion\n        // Downgrade kotlin version for WorkManager\n        resolutionStrategy.force 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:' + kotlinVersion\n        resolutionStrategy.force 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + kotlinVersion\n        resolutionStrategy.force 'org.jetbrains.kotlin:kotlin-stdlib:' + kotlinVersion\n        resolutionStrategy.force 'org.jetbrains.kotlinx:kotlinx-coroutines-android:' + kotlinxVersion\n        resolutionStrategy.force 'androidx.core:core-ktx:' + kotlinCoreXVersion\n        resolutionStrategy.force 'androidx.core:core:' + coreXVersion\n        resolutionStrategy.force 'androidx.annotation:annotation:' + annotationXVersion\n        // Downgrade Cronet version for cronet-okhttp\n        resolutionStrategy.force 'org.chromium.net:cronet-api:' + cronetApiVersion\n        resolutionStrategy.force 'androidx.multidex:multidex:' + multiDexVersion\n\n        // Replace with modded library (leanback-1.0.0)\n        exclude group: 'androidx.leanback', module: 'leanback'\n        // Replace with modded library (fragment-1.1.0)\n        exclude group: 'androidx.fragment', module: 'fragment'\n\n        // Don't work! Replace with modded library (leanback-1.0.0)\n        // resolutionStrategy.dependencySubstitution {\n        //     // substitute remote dependency with local module\n        //     substitute module('androidx.leanback:leanback') using project(':leanback-1.0.0')\n        // }\n    }\n}"
  },
  {
    "path": "chatkit/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "chatkit/LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "chatkit/build.gradle",
    "content": "apply from: gradle.ext.sharedModulesConstants\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.properties.compileSdkVersion\n    buildToolsVersion project.properties.buildToolsVersion\n\n    defaultConfig {\n        minSdkVersion project.properties.minSdkVersion\n        targetSdkVersion project.properties.targetSdkVersion\n        versionCode 1\n        versionName '0.4.1'\n        consumerProguardFiles 'proguard.txt'\n    }\n    android {\n        lintOptions {\n            abortOnError false\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n\n    implementation project(':sharedutils')\n\n    implementation 'androidx.appcompat:appcompat:' + appCompatXVersion\n    implementation 'com.google.android.material:material:' + materialVersion\n    implementation \"com.google.android.flexbox:flexbox:\" + flexboxVersion\n    implementation 'androidx.recyclerview:recyclerview:' + recyclerviewXVersion\n}\n"
  },
  {
    "path": "chatkit/proguard.txt",
    "content": "# ViewHolder constructors are resolved by reflection\n-keepclassmembers class * extends com.stfalcon.chatkit.commons.ViewHolder {\n   public <init>(android.view.View);\n}"
  },
  {
    "path": "chatkit/src/main/AndroidManifest.xml",
    "content": "<manifest package=\"com.stfalcon.chatkit\">\n\n</manifest>\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/commons/DebouncedOnClickListener.java",
    "content": "package com.stfalcon.chatkit.commons;\r\n\r\nimport android.os.SystemClock;\r\nimport android.view.View;\r\n\r\nimport java.util.Map;\r\nimport java.util.WeakHashMap;\r\n\r\n/**\r\n * A Debounced OnClickListener\r\n * Rejects clicks that are too close together in time.\r\n * This class is safe to use as an OnClickListener for multiple views, and will debounce each one separately.\r\n */\r\npublic abstract class DebouncedOnClickListener implements View.OnClickListener {\r\n\r\n    private final long minimumIntervalMillis;\r\n    private final Map<View, Long> lastClickMap;\r\n\r\n    /**\r\n     * Implement this in your subclass instead of onClick\r\n     *\r\n     * @param v The view that was clicked\r\n     */\r\n    public abstract void onDebouncedClick(View v);\r\n\r\n    /**\r\n     * The one and only constructor\r\n     *\r\n     * @param minimumIntervalMillis The minimum allowed time between clicks - any click sooner than this after a previous click will be rejected\r\n     */\r\n    public DebouncedOnClickListener(long minimumIntervalMillis) {\r\n        this.minimumIntervalMillis = minimumIntervalMillis;\r\n        this.lastClickMap = new WeakHashMap<>();\r\n    }\r\n\r\n    @Override\r\n    public void onClick(View clickedView) {\r\n        Long previousClickTimestamp = lastClickMap.get(clickedView);\r\n        long currentTimestamp = SystemClock.uptimeMillis();\r\n\r\n        if (previousClickTimestamp == null || Math.abs(currentTimestamp - previousClickTimestamp) > minimumIntervalMillis) {\r\n            onDebouncedClick(clickedView);\r\n            lastClickMap.put(clickedView, currentTimestamp);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/commons/ImageLoader.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.commons;\n\nimport android.widget.ImageView;\n\nimport androidx.annotation.Nullable;\n\n/**\n * Callback for implementing images loading in message list\n */\npublic interface ImageLoader {\n\n    void loadImage(ImageView imageView, @Nullable String url, @Nullable Object payload);\n\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/commons/InputTrackingRecyclerViewAdapter.java",
    "content": "package com.stfalcon.chatkit.commons;\r\n\r\nimport android.view.KeyEvent;\r\nimport android.view.View;\r\nimport androidx.recyclerview.widget.RecyclerView;\r\n\r\n/**\r\n * Created by vektor on 31/05/16.\r\n */\r\npublic abstract class InputTrackingRecyclerViewAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {\r\n    private int mSelectedItem = 0;\r\n    private RecyclerView mRecyclerView;\r\n\r\n    @Override\r\n    public void onAttachedToRecyclerView(final RecyclerView recyclerView) {\r\n        super.onAttachedToRecyclerView(recyclerView);\r\n\r\n        mRecyclerView = recyclerView;\r\n        // Handle key up and key down and attempt to move selection\r\n        recyclerView.setOnKeyListener(new View.OnKeyListener() {\r\n            @Override\r\n            public boolean onKey(View v, int keyCode, KeyEvent event) {\r\n                RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();\r\n\r\n                // Return false if scrolled to the bounds and allow focus to move off the list\r\n                if (event.getAction() == KeyEvent.ACTION_DOWN) {\r\n                    if (isConfirmButton(event)) {\r\n                        if ((event.getFlags() & KeyEvent.FLAG_LONG_PRESS) == KeyEvent.FLAG_LONG_PRESS) {\r\n                            mRecyclerView.findViewHolderForAdapterPosition(mSelectedItem).itemView.performLongClick();\r\n                        } else {\r\n                            event.startTracking();\r\n                        }\r\n                        return true;\r\n                    } else {\r\n                        if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {\r\n                            return tryMoveSelection(lm, 1);\r\n                        } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {\r\n                            return tryMoveSelection(lm, -1);\r\n                        }\r\n                    }\r\n                } else if (event.getAction() == KeyEvent.ACTION_UP && isConfirmButton(event) && ((event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != KeyEvent.FLAG_LONG_PRESS)) {\r\n                    mRecyclerView.findViewHolderForAdapterPosition(mSelectedItem).itemView.performClick();\r\n                    return true;\r\n                }\r\n                return false;\r\n            }\r\n        });\r\n    }\r\n\r\n    private boolean tryMoveSelection(RecyclerView.LayoutManager lm, int direction) {\r\n        int nextSelectItem = mSelectedItem + direction;\r\n\r\n        // If still within valid bounds, move the selection, notify to redraw, and scroll\r\n        if (nextSelectItem >= 0 && nextSelectItem < getItemCount()) {\r\n            notifyItemChanged(mSelectedItem);\r\n            mSelectedItem = nextSelectItem;\r\n            notifyItemChanged(mSelectedItem);\r\n            //lm.scrollToPosition(mSelectedItem);\r\n            mRecyclerView.smoothScrollToPosition(mSelectedItem);\r\n            return true;\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    public int getSelectedItem() {\r\n        return mSelectedItem;\r\n    }\r\n\r\n    public void setSelectedItem(int selectedItem) {\r\n        mSelectedItem = selectedItem;\r\n    }\r\n\r\n    public RecyclerView getRecyclerView() {\r\n        return mRecyclerView;\r\n    }\r\n\r\n    @Override\r\n    public void onBindViewHolder(VH holder, int position) {\r\n        onBindViewHolder(holder, position);\r\n    }\r\n\r\n    public static boolean isConfirmButton(KeyEvent event) {\r\n        switch (event.getKeyCode()) {\r\n            case KeyEvent.KEYCODE_ENTER:\r\n            case KeyEvent.KEYCODE_DPAD_CENTER:\r\n            case KeyEvent.KEYCODE_BUTTON_A:\r\n                return true;\r\n            default:\r\n                return false;\r\n        }\r\n    }\r\n\r\n}\r\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/commons/Style.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.commons;\n\nimport android.content.Context;\nimport android.content.res.Resources;\nimport android.content.res.TypedArray;\nimport android.graphics.drawable.Drawable;\nimport android.os.Build.VERSION;\nimport android.util.AttributeSet;\nimport android.util.TypedValue;\n\nimport androidx.annotation.AttrRes;\nimport androidx.annotation.ColorRes;\nimport androidx.annotation.DimenRes;\nimport androidx.annotation.DrawableRes;\nimport androidx.core.content.ContextCompat;\n\nimport com.stfalcon.chatkit.R;\n\n/**\n * Base class for chat component styles\n */\npublic abstract class Style {\n\n    protected Context context;\n    protected Resources resources;\n    protected AttributeSet attrs;\n\n    protected Style(Context context, AttributeSet attrs) {\n        this.context = context;\n        this.resources = context.getResources();\n        this.attrs = attrs;\n    }\n\n    protected final int getSystemAccentColor() {\n        return getSystemColor(VERSION.SDK_INT >= 21 ? android.R.attr.colorAccent : R.attr.colorAccent);\n    }\n\n    protected final int getSystemPrimaryColor() {\n        return getSystemColor(VERSION.SDK_INT >= 21 ? android.R.attr.colorPrimary : R.attr.colorPrimary);\n    }\n\n    protected final int getSystemPrimaryDarkColor() {\n        return getSystemColor(VERSION.SDK_INT >= 21 ? android.R.attr.colorPrimaryDark : R.attr.colorPrimaryDark);\n    }\n\n    protected final int getSystemPrimaryTextColor() {\n        return getSystemColor(android.R.attr.textColorPrimary);\n    }\n\n    protected final int getSystemHintColor() {\n        return getSystemColor(android.R.attr.textColorHint);\n    }\n\n    protected final int getSystemColor(@AttrRes int attr) {\n        TypedValue typedValue = new TypedValue();\n\n        TypedArray a = context.obtainStyledAttributes(typedValue.data, new int[]{attr});\n        // MOD: Invisible link fix on old devices (provide the default color)\n        int color = a.getColor(0, getColor(R.color.dark_red));\n        a.recycle();\n\n        return color;\n    }\n\n    protected final int getDimension(@DimenRes int dimen) {\n        return resources.getDimensionPixelSize(dimen);\n    }\n\n    protected final int getColor(@ColorRes int color) {\n        return ContextCompat.getColor(context, color);\n    }\n\n    protected final Drawable getDrawable(@DrawableRes int drawable) {\n        return ContextCompat.getDrawable(context, drawable);\n    }\n\n    protected final Drawable getVectorDrawable(@DrawableRes int drawable) {\n        return ContextCompat.getDrawable(context, drawable);\n    }\n\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/commons/ViewHolder.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.commons;\n\nimport android.view.View;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * Base ViewHolder\n */\npublic abstract class ViewHolder<DATA> extends RecyclerView.ViewHolder {\n\n    public abstract void onBind(DATA data);\n\n    public ViewHolder(View itemView) {\n        super(itemView);\n    }\n\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/commons/models/IDialog.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.commons.models;\n\nimport java.util.List;\n\n/**\n * For implementing by real dialog model\n */\n\npublic interface IDialog<MESSAGE extends IMessage> {\n\n    String getId();\n\n    String getDialogPhoto();\n\n    String getDialogName();\n\n    List<? extends IUser> getUsers();\n\n    MESSAGE getLastMessage();\n\n    void setLastMessage(MESSAGE message);\n\n    int getUnreadCount();\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/commons/models/IMessage.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.commons.models;\n\nimport java.util.Date;\n\n/**\n * For implementing by real message model\n */\npublic interface IMessage {\n\n    /**\n     * Returns message identifier\n     *\n     * @return the message id\n     */\n    String getId();\n\n    /**\n     * Returns message text\n     *\n     * @return the message text\n     */\n    CharSequence getText();\n\n    /**\n     * Returns message author. See the {@link IUser} for more details\n     *\n     * @return the message author\n     */\n    IUser getUser();\n\n    /**\n     * Returns message creation date\n     *\n     * @return the message creation date\n     */\n    Date getCreatedAt();\n\n    static boolean checkMessage(IMessage message) {\n        return message != null && message.getId() != null && message.getUser() != null && message.getUser().getId() != null\n                && message.getText() != null;\n    }\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/commons/models/IUser.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.commons.models;\n\n/**\n * For implementing by real user model\n */\npublic interface IUser {\n\n    /**\n     * Returns the user's id\n     *\n     * @return the user's id\n     */\n    String getId();\n\n    /**\n     * Returns the user's name\n     *\n     * @return the user's name\n     */\n    String getName();\n\n    /**\n     * Returns the user's avatar image url\n     *\n     * @return the user's avatar image url\n     */\n    String getAvatar();\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/commons/models/MessageContentType.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.commons.models;\n\nimport androidx.annotation.Nullable;\n\nimport com.stfalcon.chatkit.messages.MessageHolders;\n\n/*\n * Created by troy379 on 28.03.17.\n */\n\n/**\n * Interface used to mark messages as custom content types. For its representation see {@link MessageHolders}\n */\n\npublic interface MessageContentType extends IMessage {\n\n    /**\n     * Default media type for image message.\n     */\n    interface Image extends IMessage {\n        @Nullable\n        String getImageUrl();\n    }\n\n    // other default types will be here\n\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/commons/widgets/FocusFixRelativeLayout.java",
    "content": "package com.stfalcon.chatkit.commons.widgets;\r\n\r\nimport android.content.Context;\r\nimport android.util.AttributeSet;\r\nimport android.widget.RelativeLayout;\r\n\r\n/**\r\n * https://stackoverflow.com/questions/34277425/recyclerview-items-lose-focus\r\n */\r\npublic class FocusFixRelativeLayout extends RelativeLayout {\r\n    public FocusFixRelativeLayout(Context context) {\r\n        super(context);\r\n    }\r\n\r\n    public FocusFixRelativeLayout(Context context, AttributeSet attrs) {\r\n        super(context, attrs);\r\n    }\r\n\r\n    public FocusFixRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {\r\n        super(context, attrs, defStyleAttr);\r\n    }\r\n\r\n    @SuppressWarnings(\"NewApi\")\r\n    public FocusFixRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {\r\n        super(context, attrs, defStyleAttr, defStyleRes);\r\n    }\r\n\r\n    @Override\r\n    public void clearFocus() {\r\n        if (getParent() != null) {\r\n            super.clearFocus();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/commons/widgets/WrapWidthTextView.java",
    "content": "package com.stfalcon.chatkit.commons.widgets;\r\n\r\nimport android.content.Context;\r\nimport android.text.Layout;\r\nimport android.util.AttributeSet;\r\nimport androidx.annotation.NonNull;\r\nimport androidx.annotation.Nullable;\r\nimport androidx.appcompat.widget.AppCompatTextView;\r\n\r\n/**\r\n * Alter multiline TextView behavior to wrap content exactly.<br/>\r\n * <a href=\"https://stackoverflow.com/questions/7439748/why-is-wrap-content-in-multiple-line-textview-filling-parent\">Original discussion 1</a><br/>\r\n * <a href=\"https://stackoverflow.com/questions/10913384/how-to-make-textview-wrap-its-multiline-content-exactly\">Original discussion 2</a><br/>\r\n */\r\npublic class WrapWidthTextView extends AppCompatTextView {\r\n    public WrapWidthTextView(@NonNull Context context) {\r\n        super(context);\r\n    }\r\n\r\n    public WrapWidthTextView(@NonNull Context context, @Nullable AttributeSet attrs) {\r\n        super(context, attrs);\r\n    }\r\n\r\n    public WrapWidthTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\r\n        super(context, attrs, defStyleAttr);\r\n    }\r\n\r\n    @Override\r\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\r\n        super.onMeasure(widthMeasureSpec, heightMeasureSpec);\r\n\r\n        Layout layout = getLayout();\r\n        if (layout != null) {\r\n            int width = (int) Math.ceil(getMaxLineWidth(layout))\r\n                    + getCompoundPaddingLeft() + getCompoundPaddingRight();\r\n            int height = getMeasuredHeight();\r\n            setMeasuredDimension(width, height);\r\n        }\r\n    }\r\n\r\n    private float getMaxLineWidth(Layout layout) {\r\n        float max_width = 0.0f;\r\n        int lines = layout.getLineCount();\r\n        for (int i = 0; i < lines; i++) {\r\n            if (layout.getLineWidth(i) > max_width) {\r\n                max_width = layout.getLineWidth(i);\r\n            }\r\n        }\r\n        return max_width;\r\n    }\r\n}\r\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/dialogs/DialogListStyle.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.dialogs;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Typeface;\nimport android.util.AttributeSet;\n\nimport com.stfalcon.chatkit.R;\nimport com.stfalcon.chatkit.commons.Style;\n\n/**\n * Style for DialogList customization by xml attributes\n */\n@SuppressWarnings(\"WeakerAccess\")\nclass DialogListStyle extends Style {\n\n    private int dialogTitleTextColor;\n    private int dialogTitleTextSize;\n    private int dialogTitleTextStyle;\n    private int dialogUnreadTitleTextColor;\n    private int dialogUnreadTitleTextStyle;\n\n    private int dialogMessageTextColor;\n    private int dialogMessageTextSize;\n    private int dialogMessageTextStyle;\n    private int dialogUnreadMessageTextColor;\n    private int dialogUnreadMessageTextStyle;\n\n    private int dialogDateColor;\n    private int dialogDateSize;\n    private int dialogDateStyle;\n    private int dialogUnreadDateColor;\n    private int dialogUnreadDateStyle;\n\n    private boolean dialogUnreadBubbleEnabled;\n    private int dialogUnreadBubbleTextColor;\n    private int dialogUnreadBubbleTextSize;\n    private int dialogUnreadBubbleTextStyle;\n    private int dialogUnreadBubbleBackgroundColor;\n\n    private int dialogAvatarWidth;\n    private int dialogAvatarHeight;\n\n    private boolean dialogMessageAvatarEnabled;\n    private int dialogMessageAvatarWidth;\n    private int dialogMessageAvatarHeight;\n\n    private boolean dialogDividerEnabled;\n    private int dialogDividerColor;\n    private int dialogDividerLeftPadding;\n    private int dialogDividerRightPadding;\n\n    private int dialogItemBackground;\n    private int dialogUnreadItemBackground;\n\n    static DialogListStyle parse(Context context, AttributeSet attrs) {\n        DialogListStyle style = new DialogListStyle(context, attrs);\n\n        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DialogsList);\n\n        //Item background\n        style.dialogItemBackground = typedArray.getColor(R.styleable.DialogsList_dialogItemBackground,\n                style.getColor(R.color.transparent));\n        style.dialogUnreadItemBackground = typedArray.getColor(R.styleable.DialogsList_dialogUnreadItemBackground,\n                style.getColor(R.color.transparent));\n\n        //Title text\n        style.dialogTitleTextColor = typedArray.getColor(R.styleable.DialogsList_dialogTitleTextColor,\n                style.getColor(R.color.dialog_title_text));\n        style.dialogTitleTextSize = typedArray.getDimensionPixelSize(R.styleable.DialogsList_dialogTitleTextSize,\n                context.getResources().getDimensionPixelSize(R.dimen.dialog_title_text_size));\n        style.dialogTitleTextStyle = typedArray.getInt(R.styleable.DialogsList_dialogTitleTextStyle, Typeface.NORMAL);\n\n        //Title unread text\n        style.dialogUnreadTitleTextColor = typedArray.getColor(R.styleable.DialogsList_dialogUnreadTitleTextColor,\n                style.getColor(R.color.dialog_title_text));\n        style.dialogUnreadTitleTextStyle = typedArray.getInt(R.styleable.DialogsList_dialogUnreadTitleTextStyle, Typeface.NORMAL);\n\n        //Message text\n        style.dialogMessageTextColor = typedArray.getColor(R.styleable.DialogsList_dialogMessageTextColor,\n                style.getColor(R.color.dialog_message_text));\n        style.dialogMessageTextSize = typedArray.getDimensionPixelSize(R.styleable.DialogsList_dialogMessageTextSize,\n                context.getResources().getDimensionPixelSize(R.dimen.dialog_message_text_size));\n        style.dialogMessageTextStyle = typedArray.getInt(R.styleable.DialogsList_dialogMessageTextStyle, Typeface.NORMAL);\n\n        //Message unread text\n        style.dialogUnreadMessageTextColor = typedArray.getColor(R.styleable.DialogsList_dialogUnreadMessageTextColor,\n                style.getColor(R.color.dialog_message_text));\n        style.dialogUnreadMessageTextStyle = typedArray.getInt(R.styleable.DialogsList_dialogUnreadMessageTextStyle, Typeface.NORMAL);\n\n        //Date text\n        style.dialogDateColor = typedArray.getColor(R.styleable.DialogsList_dialogDateColor,\n                style.getColor(R.color.dialog_date_text));\n        style.dialogDateSize = typedArray.getDimensionPixelSize(R.styleable.DialogsList_dialogDateSize,\n                context.getResources().getDimensionPixelSize(R.dimen.dialog_date_text_size));\n        style.dialogDateStyle = typedArray.getInt(R.styleable.DialogsList_dialogDateStyle, Typeface.NORMAL);\n\n        //Date unread text\n        style.dialogUnreadDateColor = typedArray.getColor(R.styleable.DialogsList_dialogUnreadDateColor,\n                style.getColor(R.color.dialog_date_text));\n        style.dialogUnreadDateStyle = typedArray.getInt(R.styleable.DialogsList_dialogUnreadDateStyle, Typeface.NORMAL);\n\n        //Unread bubble\n        style.dialogUnreadBubbleEnabled = typedArray.getBoolean(R.styleable.DialogsList_dialogUnreadBubbleEnabled, true);\n        style.dialogUnreadBubbleBackgroundColor = typedArray.getColor(R.styleable.DialogsList_dialogUnreadBubbleBackgroundColor,\n                style.getColor(R.color.dialog_unread_bubble));\n\n        //Unread bubble text\n        style.dialogUnreadBubbleTextColor = typedArray.getColor(R.styleable.DialogsList_dialogUnreadBubbleTextColor,\n                style.getColor(R.color.dialog_unread_text));\n        style.dialogUnreadBubbleTextSize = typedArray.getDimensionPixelSize(R.styleable.DialogsList_dialogUnreadBubbleTextSize,\n                context.getResources().getDimensionPixelSize(R.dimen.dialog_unread_bubble_text_size));\n        style.dialogUnreadBubbleTextStyle = typedArray.getInt(R.styleable.DialogsList_dialogUnreadBubbleTextStyle, Typeface.NORMAL);\n\n        //Avatar\n        style.dialogAvatarWidth = typedArray.getDimensionPixelSize(R.styleable.DialogsList_dialogAvatarWidth,\n                context.getResources().getDimensionPixelSize(R.dimen.dialog_avatar_width));\n        style.dialogAvatarHeight = typedArray.getDimensionPixelSize(R.styleable.DialogsList_dialogAvatarHeight,\n                context.getResources().getDimensionPixelSize(R.dimen.dialog_avatar_height));\n\n        //Last message avatar\n        style.dialogMessageAvatarEnabled = typedArray.getBoolean(R.styleable.DialogsList_dialogMessageAvatarEnabled, true);\n        style.dialogMessageAvatarWidth = typedArray.getDimensionPixelSize(R.styleable.DialogsList_dialogMessageAvatarWidth,\n                context.getResources().getDimensionPixelSize(R.dimen.dialog_last_message_avatar_width));\n        style.dialogMessageAvatarHeight = typedArray.getDimensionPixelSize(R.styleable.DialogsList_dialogMessageAvatarHeight,\n                context.getResources().getDimensionPixelSize(R.dimen.dialog_last_message_avatar_height));\n\n        //Divider\n        style.dialogDividerEnabled = typedArray.getBoolean(R.styleable.DialogsList_dialogDividerEnabled, true);\n        style.dialogDividerColor = typedArray.getColor(R.styleable.DialogsList_dialogDividerColor, style.getColor(R.color.dialog_divider));\n        style.dialogDividerLeftPadding = typedArray.getDimensionPixelSize(R.styleable.DialogsList_dialogDividerLeftPadding,\n                context.getResources().getDimensionPixelSize(R.dimen.dialog_divider_margin_left));\n        style.dialogDividerRightPadding = typedArray.getDimensionPixelSize(R.styleable.DialogsList_dialogDividerRightPadding,\n                context.getResources().getDimensionPixelSize(R.dimen.dialog_divider_margin_right));\n\n        typedArray.recycle();\n\n        return style;\n    }\n\n    private DialogListStyle(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    protected int getDialogTitleTextColor() {\n        return dialogTitleTextColor;\n    }\n\n    protected int getDialogTitleTextSize() {\n        return dialogTitleTextSize;\n    }\n\n    protected int getDialogTitleTextStyle() {\n        return dialogTitleTextStyle;\n    }\n\n    protected int getDialogUnreadTitleTextColor() {\n        return dialogUnreadTitleTextColor;\n    }\n\n    protected int getDialogUnreadTitleTextStyle() {\n        return dialogUnreadTitleTextStyle;\n    }\n\n    protected int getDialogMessageTextColor() {\n        return dialogMessageTextColor;\n    }\n\n    protected int getDialogMessageTextSize() {\n        return dialogMessageTextSize;\n    }\n\n    protected int getDialogMessageTextStyle() {\n        return dialogMessageTextStyle;\n    }\n\n    protected int getDialogUnreadMessageTextColor() {\n        return dialogUnreadMessageTextColor;\n    }\n\n    protected int getDialogUnreadMessageTextStyle() {\n        return dialogUnreadMessageTextStyle;\n    }\n\n    protected int getDialogDateColor() {\n        return dialogDateColor;\n    }\n\n    protected int getDialogDateSize() {\n        return dialogDateSize;\n    }\n\n    protected int getDialogDateStyle() {\n        return dialogDateStyle;\n    }\n\n    protected int getDialogUnreadDateColor() {\n        return dialogUnreadDateColor;\n    }\n\n    protected int getDialogUnreadDateStyle() {\n        return dialogUnreadDateStyle;\n    }\n\n    protected boolean isDialogUnreadBubbleEnabled() {\n        return dialogUnreadBubbleEnabled;\n    }\n\n    protected int getDialogUnreadBubbleTextColor() {\n        return dialogUnreadBubbleTextColor;\n    }\n\n    protected int getDialogUnreadBubbleTextSize() {\n        return dialogUnreadBubbleTextSize;\n    }\n\n    protected int getDialogUnreadBubbleTextStyle() {\n        return dialogUnreadBubbleTextStyle;\n    }\n\n    protected int getDialogUnreadBubbleBackgroundColor() {\n        return dialogUnreadBubbleBackgroundColor;\n    }\n\n    protected int getDialogAvatarWidth() {\n        return dialogAvatarWidth;\n    }\n\n    protected int getDialogAvatarHeight() {\n        return dialogAvatarHeight;\n    }\n\n    protected boolean isDialogDividerEnabled() {\n        return dialogDividerEnabled;\n    }\n\n    protected int getDialogDividerColor() {\n        return dialogDividerColor;\n    }\n\n    protected int getDialogDividerLeftPadding() {\n        return dialogDividerLeftPadding;\n    }\n\n    protected int getDialogDividerRightPadding() {\n        return dialogDividerRightPadding;\n    }\n\n    protected int getDialogItemBackground() {\n        return dialogItemBackground;\n    }\n\n    protected int getDialogUnreadItemBackground() {\n        return dialogUnreadItemBackground;\n    }\n\n    protected boolean isDialogMessageAvatarEnabled() {\n        return dialogMessageAvatarEnabled;\n    }\n\n    protected int getDialogMessageAvatarWidth() {\n        return dialogMessageAvatarWidth;\n    }\n\n    protected int getDialogMessageAvatarHeight() {\n        return dialogMessageAvatarHeight;\n    }\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/dialogs/DialogsList.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.dialogs;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport androidx.annotation.Nullable;\nimport androidx.recyclerview.widget.DefaultItemAnimator;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.SimpleItemAnimator;\n\nimport com.stfalcon.chatkit.commons.models.IDialog;\n\n/**\n * Component for displaying list of dialogs\n */\npublic class DialogsList extends RecyclerView {\n\n    private DialogListStyle dialogStyle;\n\n    public DialogsList(Context context) {\n        super(context);\n    }\n\n    public DialogsList(Context context, @Nullable AttributeSet attrs) {\n        super(context, attrs);\n        parseStyle(context, attrs);\n    }\n\n    public DialogsList(Context context, @Nullable AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        parseStyle(context, attrs);\n    }\n\n    @Override\n    protected void onAttachedToWindow() {\n        super.onAttachedToWindow();\n\n        LinearLayoutManager layout = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);\n        SimpleItemAnimator animator = new DefaultItemAnimator();\n\n        setLayoutManager(layout);\n        setItemAnimator(animator);\n    }\n\n    /**\n     * Don't use this method for setting your adapter, otherwise exception will by thrown.\n     * Call {@link #setAdapter(DialogsListAdapter)} instead.\n     */\n    @Override\n    public void setAdapter(Adapter adapter) {\n        throw new IllegalArgumentException(\"You can't set adapter to DialogsList. Use #setAdapter(DialogsListAdapter) instead.\");\n    }\n\n    /**\n     * Sets adapter for DialogsList\n     *\n     * @param adapter  Adapter. Must extend DialogsListAdapter\n     * @param <DIALOG> Dialog model class\n     */\n    public <DIALOG extends IDialog<?>>\n    void setAdapter(DialogsListAdapter<DIALOG> adapter) {\n        setAdapter(adapter, false);\n    }\n\n    /**\n     * Sets adapter for DialogsList\n     *\n     * @param adapter       Adapter. Must extend DialogsListAdapter\n     * @param reverseLayout weather to use reverse layout for layout manager.\n     * @param <DIALOG>      Dialog model class\n     */\n    public <DIALOG extends IDialog<?>>\n    void setAdapter(DialogsListAdapter<DIALOG> adapter, boolean reverseLayout) {\n        SimpleItemAnimator itemAnimator = new DefaultItemAnimator();\n        itemAnimator.setSupportsChangeAnimations(false);\n\n        LinearLayoutManager layoutManager = new LinearLayoutManager(getContext(),\n                LinearLayoutManager.VERTICAL, reverseLayout);\n\n        setItemAnimator(itemAnimator);\n        setLayoutManager(layoutManager);\n\n        adapter.setStyle(dialogStyle);\n\n        super.setAdapter(adapter);\n    }\n\n    @SuppressWarnings(\"ResourceType\")\n    private void parseStyle(Context context, AttributeSet attrs) {\n        dialogStyle = DialogListStyle.parse(context, attrs);\n    }\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/dialogs/DialogsListAdapter.java",
    "content": "/******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.dialogs;\n\nimport android.graphics.Typeface;\nimport android.graphics.drawable.GradientDrawable;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.annotation.LayoutRes;\nimport androidx.annotation.Nullable;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.stfalcon.chatkit.R;\nimport com.stfalcon.chatkit.commons.ImageLoader;\nimport com.stfalcon.chatkit.commons.ViewHolder;\nimport com.stfalcon.chatkit.commons.models.IDialog;\nimport com.stfalcon.chatkit.commons.models.IMessage;\nimport com.stfalcon.chatkit.utils.DateFormatter;\n\nimport java.lang.reflect.Constructor;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.Date;\nimport java.util.List;\n\nimport static android.view.View.GONE;\nimport static android.view.View.VISIBLE;\n\n/**\n * Adapter for {@link DialogsList}\n */\n@SuppressWarnings(\"WeakerAccess\")\npublic class DialogsListAdapter<DIALOG extends IDialog>\n        extends RecyclerView.Adapter<DialogsListAdapter.BaseDialogViewHolder> {\n\n    protected List<DIALOG> items = new ArrayList<>();\n    private int itemLayoutId;\n    private Class<? extends BaseDialogViewHolder> holderClass;\n    private ImageLoader imageLoader;\n    private OnDialogClickListener<DIALOG> onDialogClickListener;\n    private OnDialogViewClickListener<DIALOG> onDialogViewClickListener;\n    private OnDialogLongClickListener<DIALOG> onLongItemClickListener;\n    private OnDialogViewLongClickListener<DIALOG> onDialogViewLongClickListener;\n    private DialogListStyle dialogStyle;\n    private DateFormatter.Formatter datesFormatter;\n\n    /**\n     * For default list item layout and view holder\n     *\n     * @param imageLoader image loading method\n     */\n    public DialogsListAdapter(ImageLoader imageLoader) {\n        this(R.layout.item_dialog, DialogViewHolder.class, imageLoader);\n    }\n\n    /**\n     * For custom list item layout and default view holder\n     *\n     * @param itemLayoutId custom list item resource id\n     * @param imageLoader  image loading method\n     */\n    public DialogsListAdapter(@LayoutRes int itemLayoutId, ImageLoader imageLoader) {\n        this(itemLayoutId, DialogViewHolder.class, imageLoader);\n    }\n\n    /**\n     * For custom list item layout and custom view holder\n     *\n     * @param itemLayoutId custom list item resource id\n     * @param holderClass  custom view holder class\n     * @param imageLoader  image loading method\n     */\n    public DialogsListAdapter(@LayoutRes int itemLayoutId, Class<? extends BaseDialogViewHolder> holderClass,\n                              ImageLoader imageLoader) {\n        this.itemLayoutId = itemLayoutId;\n        this.holderClass = holderClass;\n        this.imageLoader = imageLoader;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    public void onBindViewHolder(BaseDialogViewHolder holder, int position) {\n        holder.setImageLoader(imageLoader);\n        holder.setOnDialogClickListener(onDialogClickListener);\n        holder.setOnDialogViewClickListener(onDialogViewClickListener);\n        holder.setOnLongItemClickListener(onLongItemClickListener);\n        holder.setOnDialogViewLongClickListener(onDialogViewLongClickListener);\n        holder.setDatesFormatter(datesFormatter);\n        holder.onBind(items.get(position));\n    }\n\n    @Override\n    public BaseDialogViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {\n        View v = LayoutInflater.from(parent.getContext()).inflate(itemLayoutId, parent, false);\n        //create view holder by reflation\n        try {\n            Constructor<? extends BaseDialogViewHolder> constructor = holderClass.getDeclaredConstructor(View.class);\n            constructor.setAccessible(true);\n            BaseDialogViewHolder baseDialogViewHolder = constructor.newInstance(v);\n            if (baseDialogViewHolder instanceof DialogViewHolder) {\n                ((DialogViewHolder) baseDialogViewHolder).setDialogStyle(dialogStyle);\n            }\n            return baseDialogViewHolder;\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    /**\n     * @return size of dialogs list\n     */\n    @Override\n    public int getItemCount() {\n        return items.size();\n    }\n\n    /**\n     * remove item with id\n     *\n     * @param id dialog i\n     */\n    public void deleteById(String id) {\n        for (int i = 0; i < items.size(); i++) {\n            if (items.get(i).getId().equals(id)) {\n                items.remove(i);\n                notifyItemRemoved(i);\n            }\n        }\n    }\n\n    /**\n     * Returns {@code true} if, and only if, dialogs count in adapter is non-zero.\n     *\n     * @return {@code true} if size is 0, otherwise {@code false}\n     */\n    public boolean isEmpty() {\n        return items.isEmpty();\n    }\n\n    /**\n     * clear dialogs list\n     */\n    public void clear() {\n        if (items != null) {\n            items.clear();\n        }\n        notifyDataSetChanged();\n    }\n\n    /**\n     * Set dialogs list\n     *\n     * @param items dialogs list\n     */\n    public void setItems(List<DIALOG> items) {\n        this.items = items;\n        notifyDataSetChanged();\n    }\n\n    /**\n     * Add dialogs items\n     *\n     * @param newItems new dialogs list\n     */\n    public void addItems(List<DIALOG> newItems) {\n        if (newItems != null) {\n            if (items == null) {\n                items = new ArrayList<>();\n            }\n            int curSize = items.size();\n            items.addAll(newItems);\n            notifyItemRangeInserted(curSize, items.size());\n        }\n    }\n\n    /**\n     * Add dialog to the end of dialogs list\n     *\n     * @param dialog dialog item\n     */\n    public void addItem(DIALOG dialog) {\n        items.add(dialog);\n        notifyItemInserted(items.size() - 1);\n    }\n\n    /**\n     * Add dialog to dialogs list\n     *\n     * @param dialog   dialog item\n     * @param position position in dialogs list\n     */\n    public void addItem(int position, DIALOG dialog) {\n        items.add(position, dialog);\n        notifyItemInserted(position);\n    }\n\n    /**\n     * Move an item\n     *\n     * @param fromPosition the actual position of the item\n     * @param toPosition   the new position of the item\n     */\n    public void moveItem(int fromPosition, int toPosition) {\n        DIALOG dialog = items.remove(fromPosition);\n        items.add(toPosition, dialog);\n        notifyItemMoved(fromPosition, toPosition);\n    }\n\n    /**\n     * Update dialog by position in dialogs list\n     *\n     * @param position position in dialogs list\n     * @param item     new dialog item\n     */\n    public void updateItem(int position, DIALOG item) {\n        if (items == null) {\n            items = new ArrayList<>();\n        }\n        items.set(position, item);\n        notifyItemChanged(position);\n    }\n\n    /**\n     * Update dialog by dialog id\n     *\n     * @param item new dialog item\n     */\n    public void updateItemById(DIALOG item) {\n        if (items == null) {\n            items = new ArrayList<>();\n        }\n        for (int i = 0; i < items.size(); i++) {\n            if (items.get(i).getId().equals(item.getId())) {\n                items.set(i, item);\n                notifyItemChanged(i);\n                break;\n            }\n        }\n    }\n\n    /**\n     * Upsert dialog in dialogs list or add it to then end of dialogs list\n     *\n     * @param item dialog item\n     */\n    public void upsertItem(DIALOG item) {\n        boolean updated = false;\n        for (int i = 0; i < items.size(); i++) {\n            if (items.get(i).getId().equals(item.getId())) {\n                items.set(i, item);\n                notifyItemChanged(i);\n                updated = true;\n                break;\n            }\n        }\n        if (!updated) {\n            addItem(item);\n        }\n    }\n\n    /**\n     * Find an item by its id\n     *\n     * @param id the wanted item's id\n     * @return the found item, or null\n     */\n    @Nullable\n    public DIALOG getItemById(String id) {\n        if (items == null) {\n            items = new ArrayList<>();\n        }\n        for (DIALOG item : items) {\n            if (item.getId() == null && id == null) {\n                return item;\n            } else if (item.getId() != null && item.getId().equals(id)) {\n                return item;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Update last message in dialog and swap item to top of list.\n     *\n     * @param dialogId Dialog ID\n     * @param message  New message\n     * @return false if dialog doesn't exist.\n     */\n    @SuppressWarnings(\"unchecked\")\n    public boolean updateDialogWithMessage(String dialogId, IMessage message) {\n        boolean dialogExist = false;\n        for (int i = 0; i < items.size(); i++) {\n            if (items.get(i).getId().equals(dialogId)) {\n                items.get(i).setLastMessage(message);\n                notifyItemChanged(i);\n                if (i != 0) {\n                    Collections.swap(items, i, 0);\n                    notifyItemMoved(i, 0);\n                }\n                dialogExist = true;\n                break;\n            }\n        }\n        return dialogExist;\n    }\n\n    /**\n     * Sort dialog by last message date\n     */\n    public void sortByLastMessageDate() {\n        Collections.sort(items, (o1, o2) -> {\n            if (o1.getLastMessage().getCreatedAt().after(o2.getLastMessage().getCreatedAt())) {\n                return -1;\n            } else if (o1.getLastMessage().getCreatedAt().before(o2.getLastMessage().getCreatedAt())) {\n                return 1;\n            } else return 0;\n        });\n        notifyDataSetChanged();\n    }\n\n    /**\n     * Sort items with rules of comparator\n     *\n     * @param comparator Comparator\n     */\n    public void sort(Comparator<DIALOG> comparator) {\n        Collections.sort(items, comparator);\n        notifyDataSetChanged();\n    }\n\n    /**\n     * @return registered image loader\n     */\n    public ImageLoader getImageLoader() {\n        return imageLoader;\n    }\n\n    /**\n     * Register a callback to be invoked when image need to load.\n     *\n     * @param imageLoader image loading method\n     */\n    public void setImageLoader(ImageLoader imageLoader) {\n        this.imageLoader = imageLoader;\n    }\n\n    /**\n     * @return the item click callback.\n     */\n    public OnDialogClickListener getOnDialogClickListener() {\n        return onDialogClickListener;\n    }\n\n    /**\n     * Register a callback to be invoked when item is clicked.\n     *\n     * @param onDialogClickListener on click item callback\n     */\n    public void setOnDialogClickListener(OnDialogClickListener<DIALOG> onDialogClickListener) {\n        this.onDialogClickListener = onDialogClickListener;\n    }\n\n    /**\n     * @return the view click callback.\n     */\n    public OnDialogViewClickListener getOnDialogViewClickListener() {\n        return onDialogViewClickListener;\n    }\n\n    /**\n     * Register a callback to be invoked when dialog view is clicked.\n     *\n     * @param clickListener on click item callback\n     */\n    public void setOnDialogViewClickListener(OnDialogViewClickListener<DIALOG> clickListener) {\n        this.onDialogViewClickListener = clickListener;\n    }\n\n    /**\n     * @return on long click item callback\n     */\n    public OnDialogLongClickListener getOnLongItemClickListener() {\n        return onLongItemClickListener;\n    }\n\n    /**\n     * Register a callback to be invoked when item is long clicked.\n     *\n     * @param onLongItemClickListener on long click item callback\n     */\n    public void setOnDialogLongClickListener(OnDialogLongClickListener<DIALOG> onLongItemClickListener) {\n        this.onLongItemClickListener = onLongItemClickListener;\n    }\n\n    /**\n     * @return on view long click callback\n     */\n    public OnDialogViewLongClickListener<DIALOG> getOnDialogViewLongClickListener() {\n        return onDialogViewLongClickListener;\n    }\n\n    /**\n     * Register a callback to be invoked when item view is long clicked.\n     *\n     * @param clickListener on long click item callback\n     */\n    public void setOnDialogViewLongClickListener(OnDialogViewLongClickListener<DIALOG> clickListener) {\n        this.onDialogViewLongClickListener = clickListener;\n    }\n\n    /**\n     * Sets custom {@link DateFormatter.Formatter} for text representation of last message date.\n     */\n    public void setDatesFormatter(DateFormatter.Formatter datesFormatter) {\n        this.datesFormatter = datesFormatter;\n    }\n\n    //TODO ability to set style programmatically\n    void setStyle(DialogListStyle dialogStyle) {\n        this.dialogStyle = dialogStyle;\n    }\n\n    /**\n     * @return the position of a dialog in the dialogs list.\n     */\n    public int getDialogPosition(DIALOG dialog) {\n        return this.items.indexOf(dialog);\n    }\n\n    /*\n     * LISTENERS\n     * */\n    public interface OnDialogClickListener<DIALOG extends IDialog> {\n        void onDialogClick(DIALOG dialog);\n    }\n\n    public interface OnDialogViewClickListener<DIALOG extends IDialog> {\n        void onDialogViewClick(View view, DIALOG dialog);\n    }\n\n    public interface OnDialogLongClickListener<DIALOG extends IDialog> {\n        void onDialogLongClick(DIALOG dialog);\n    }\n\n    public interface OnDialogViewLongClickListener<DIALOG extends IDialog> {\n        void onDialogViewLongClick(View view, DIALOG dialog);\n    }\n\n    /*\n     * HOLDERS\n     * */\n    public abstract static class BaseDialogViewHolder<DIALOG extends IDialog>\n            extends ViewHolder<DIALOG> {\n\n        protected ImageLoader imageLoader;\n        protected OnDialogClickListener<DIALOG> onDialogClickListener;\n        protected OnDialogLongClickListener<DIALOG> onLongItemClickListener;\n        protected OnDialogViewClickListener<DIALOG> onDialogViewClickListener;\n        protected OnDialogViewLongClickListener<DIALOG> onDialogViewLongClickListener;\n        protected DateFormatter.Formatter datesFormatter;\n\n        public BaseDialogViewHolder(View itemView) {\n            super(itemView);\n        }\n\n        void setImageLoader(ImageLoader imageLoader) {\n            this.imageLoader = imageLoader;\n        }\n\n        protected void setOnDialogClickListener(OnDialogClickListener<DIALOG> onDialogClickListener) {\n            this.onDialogClickListener = onDialogClickListener;\n        }\n\n        protected void setOnDialogViewClickListener(OnDialogViewClickListener<DIALOG> onDialogViewClickListener) {\n            this.onDialogViewClickListener = onDialogViewClickListener;\n        }\n\n        protected void setOnLongItemClickListener(OnDialogLongClickListener<DIALOG> onLongItemClickListener) {\n            this.onLongItemClickListener = onLongItemClickListener;\n        }\n\n        protected void setOnDialogViewLongClickListener(OnDialogViewLongClickListener<DIALOG> onDialogViewLongClickListener) {\n            this.onDialogViewLongClickListener = onDialogViewLongClickListener;\n        }\n\n        public void setDatesFormatter(DateFormatter.Formatter dateHeadersFormatter) {\n            this.datesFormatter = dateHeadersFormatter;\n        }\n    }\n\n    public static class DialogViewHolder<DIALOG extends IDialog> extends BaseDialogViewHolder<DIALOG> {\n        protected DialogListStyle dialogStyle;\n        protected ViewGroup container;\n        protected ViewGroup root;\n        protected TextView tvName;\n        protected TextView tvDate;\n        protected ImageView ivAvatar;\n        protected ImageView ivLastMessageUser;\n        protected TextView tvLastMessage;\n        protected TextView tvBubble;\n        protected ViewGroup dividerContainer;\n        protected View divider;\n\n        public DialogViewHolder(View itemView) {\n            super(itemView);\n            root = itemView.findViewById(R.id.dialogRootLayout);\n            container = itemView.findViewById(R.id.dialogContainer);\n            tvName = itemView.findViewById(R.id.dialogName);\n            tvDate = itemView.findViewById(R.id.dialogDate);\n            tvLastMessage = itemView.findViewById(R.id.dialogLastMessage);\n            tvBubble = itemView.findViewById(R.id.dialogUnreadBubble);\n            ivLastMessageUser = itemView.findViewById(R.id.dialogLastMessageUserAvatar);\n            ivAvatar = itemView.findViewById(R.id.dialogAvatar);\n            dividerContainer = itemView.findViewById(R.id.dialogDividerContainer);\n            divider = itemView.findViewById(R.id.dialogDivider);\n\n        }\n\n        private void applyStyle() {\n            if (dialogStyle != null) {\n                //Texts\n                if (tvName != null) {\n                    tvName.setTextSize(TypedValue.COMPLEX_UNIT_PX, dialogStyle.getDialogTitleTextSize());\n                }\n\n                if (tvLastMessage != null) {\n                    tvLastMessage.setTextSize(TypedValue.COMPLEX_UNIT_PX, dialogStyle.getDialogMessageTextSize());\n                }\n\n                if (tvDate != null) {\n                    tvDate.setTextSize(TypedValue.COMPLEX_UNIT_PX, dialogStyle.getDialogDateSize());\n                }\n\n                //Divider\n                if (divider != null)\n                    divider.setBackgroundColor(dialogStyle.getDialogDividerColor());\n                if (dividerContainer != null)\n                    dividerContainer.setPadding(dialogStyle.getDialogDividerLeftPadding(), 0,\n                            dialogStyle.getDialogDividerRightPadding(), 0);\n                //Avatar\n                if (ivAvatar != null) {\n                    ivAvatar.getLayoutParams().width = dialogStyle.getDialogAvatarWidth();\n                    ivAvatar.getLayoutParams().height = dialogStyle.getDialogAvatarHeight();\n                }\n\n                //Last message user avatar\n                if (ivLastMessageUser != null) {\n                    ivLastMessageUser.getLayoutParams().width = dialogStyle.getDialogMessageAvatarWidth();\n                    ivLastMessageUser.getLayoutParams().height = dialogStyle.getDialogMessageAvatarHeight();\n                }\n\n                //Unread bubble\n                if (tvBubble != null) {\n                    GradientDrawable bgShape = (GradientDrawable) tvBubble.getBackground();\n                    bgShape.setColor(dialogStyle.getDialogUnreadBubbleBackgroundColor());\n                    tvBubble.setVisibility(dialogStyle.isDialogDividerEnabled() ? VISIBLE : GONE);\n                    tvBubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, dialogStyle.getDialogUnreadBubbleTextSize());\n                    tvBubble.setTextColor(dialogStyle.getDialogUnreadBubbleTextColor());\n                    tvBubble.setTypeface(tvBubble.getTypeface(), dialogStyle.getDialogUnreadBubbleTextStyle());\n                }\n            }\n        }\n\n\n        private void applyDefaultStyle() {\n            if (dialogStyle != null) {\n                if (root != null) {\n                    root.setBackgroundColor(dialogStyle.getDialogItemBackground());\n                }\n\n                if (tvName != null) {\n                    tvName.setTextColor(dialogStyle.getDialogTitleTextColor());\n                    tvName.setTypeface(Typeface.DEFAULT, dialogStyle.getDialogTitleTextStyle());\n                }\n\n                if (tvDate != null) {\n                    tvDate.setTextColor(dialogStyle.getDialogDateColor());\n                    tvDate.setTypeface(Typeface.DEFAULT, dialogStyle.getDialogDateStyle());\n                }\n\n                if (tvLastMessage != null) {\n                    tvLastMessage.setTextColor(dialogStyle.getDialogMessageTextColor());\n                    tvLastMessage.setTypeface(Typeface.DEFAULT, dialogStyle.getDialogMessageTextStyle());\n                }\n            }\n        }\n\n        private void applyUnreadStyle() {\n            if (dialogStyle != null) {\n                if (root != null) {\n                    root.setBackgroundColor(dialogStyle.getDialogUnreadItemBackground());\n                }\n\n                if (tvName != null) {\n                    tvName.setTextColor(dialogStyle.getDialogUnreadTitleTextColor());\n                    tvName.setTypeface(Typeface.DEFAULT, dialogStyle.getDialogUnreadTitleTextStyle());\n                }\n\n                if (tvDate != null) {\n                    tvDate.setTextColor(dialogStyle.getDialogUnreadDateColor());\n                    tvDate.setTypeface(Typeface.DEFAULT, dialogStyle.getDialogUnreadDateStyle());\n                }\n\n                if (tvLastMessage != null) {\n                    tvLastMessage.setTextColor(dialogStyle.getDialogUnreadMessageTextColor());\n                    tvLastMessage.setTypeface(Typeface.DEFAULT, dialogStyle.getDialogUnreadMessageTextStyle());\n                }\n            }\n        }\n\n\n        @Override\n        public void onBind(final DIALOG dialog) {\n            if (dialog.getUnreadCount() > 0) {\n                applyUnreadStyle();\n            } else {\n                applyDefaultStyle();\n            }\n\n            //Set Name\n            tvName.setText(dialog.getDialogName());\n\n            //Set Date\n            String formattedDate = null;\n\n            if (dialog.getLastMessage() != null) {\n                Date lastMessageDate = dialog.getLastMessage().getCreatedAt();\n                if (datesFormatter != null) formattedDate = datesFormatter.format(lastMessageDate);\n                tvDate.setText(formattedDate == null\n                        ? getDateString(lastMessageDate)\n                        : formattedDate);\n            } else {\n                tvDate.setText(null);\n            }\n\n            //Set Dialog avatar\n            if (imageLoader != null) {\n                imageLoader.loadImage(ivAvatar, dialog.getDialogPhoto(), null);\n            }\n\n            //Set Last message user avatar with check if there is last message\n            if (imageLoader != null && dialog.getLastMessage() != null) {\n                imageLoader.loadImage(ivLastMessageUser, dialog.getLastMessage().getUser().getAvatar(), null);\n            }\n            ivLastMessageUser.setVisibility(dialogStyle.isDialogMessageAvatarEnabled()\n                    && dialog.getUsers().size() > 1\n                    && dialog.getLastMessage() != null ? VISIBLE : GONE);\n\n            //Set Last message text\n            if (dialog.getLastMessage() != null) {\n                tvLastMessage.setText(dialog.getLastMessage().getText());\n            } else {\n                tvLastMessage.setText(null);\n            }\n\n            //Set Unread message count bubble\n            tvBubble.setText(String.valueOf(dialog.getUnreadCount()));\n            tvBubble.setVisibility(dialogStyle.isDialogUnreadBubbleEnabled() &&\n                    dialog.getUnreadCount() > 0 ? VISIBLE : GONE);\n\n            container.setOnClickListener(view -> {\n                if (onDialogClickListener != null) {\n                    onDialogClickListener.onDialogClick(dialog);\n                }\n                if (onDialogViewClickListener != null) {\n                    onDialogViewClickListener.onDialogViewClick(view, dialog);\n                }\n            });\n\n\n            container.setOnLongClickListener(view -> {\n                if (onLongItemClickListener != null) {\n                    onLongItemClickListener.onDialogLongClick(dialog);\n                }\n                if (onDialogViewLongClickListener != null) {\n                    onDialogViewLongClickListener.onDialogViewLongClick(view, dialog);\n                }\n                return onLongItemClickListener != null || onDialogViewLongClickListener != null;\n            });\n        }\n\n        protected String getDateString(Date date) {\n            return DateFormatter.format(date, DateFormatter.Template.TIME);\n        }\n\n        protected DialogListStyle getDialogStyle() {\n            return dialogStyle;\n        }\n\n        protected void setDialogStyle(DialogListStyle dialogStyle) {\n            this.dialogStyle = dialogStyle;\n            applyStyle();\n        }\n    }\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/messages/MessageHolders.java",
    "content": "package com.stfalcon.chatkit.messages;\n\nimport android.annotation.SuppressLint;\nimport android.text.Spannable;\nimport android.text.method.LinkMovementMethod;\nimport android.util.SparseArray;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.LayoutRes;\nimport androidx.annotation.NonNull;\nimport androidx.core.view.ViewCompat;\n\nimport com.stfalcon.chatkit.R;\nimport com.stfalcon.chatkit.commons.ImageLoader;\nimport com.stfalcon.chatkit.commons.ViewHolder;\nimport com.stfalcon.chatkit.commons.models.IMessage;\nimport com.stfalcon.chatkit.commons.models.MessageContentType;\nimport com.stfalcon.chatkit.utils.DateFormatter;\nimport com.stfalcon.chatkit.utils.RoundedImageView;\n\nimport java.lang.reflect.Constructor;\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.List;\n\n/*\n * Created by troy379 on 31.03.17.\n */\n@SuppressWarnings(\"WeakerAccess\")\npublic class MessageHolders {\n\n    private static final short VIEW_TYPE_DATE_HEADER = 130;\n    private static final short VIEW_TYPE_STRING_HEADER = 133;\n    private static final short VIEW_TYPE_TEXT_MESSAGE = 131;\n    private static final short VIEW_TYPE_IMAGE_MESSAGE = 132;\n\n    private Class<? extends ViewHolder<Date>> dateHeaderHolder;\n    private Class<? extends ViewHolder<String>> stringHeaderHolder;\n    private int dateHeaderLayout;\n\n    private HolderConfig<IMessage> incomingTextConfig;\n    private HolderConfig<IMessage> outcomingTextConfig;\n    private HolderConfig<MessageContentType.Image> incomingImageConfig;\n    private HolderConfig<MessageContentType.Image> outcomingImageConfig;\n\n    private List<ContentTypeConfig> customContentTypes = new ArrayList<>();\n    private ContentChecker contentChecker;\n\n    public MessageHolders() {\n        this.dateHeaderHolder = DefaultDateHeaderViewHolder.class;\n        this.dateHeaderLayout = R.layout.item_date_header;\n\n        this.stringHeaderHolder = DefaultStringHeaderViewHolder.class;\n\n        this.incomingTextConfig = new HolderConfig<>(DefaultIncomingTextMessageViewHolder.class, R.layout.item_incoming_text_message);\n        this.outcomingTextConfig = new HolderConfig<>(DefaultOutcomingTextMessageViewHolder.class, R.layout.item_outcoming_text_message);\n        this.incomingImageConfig = new HolderConfig<>(DefaultIncomingImageMessageViewHolder.class, R.layout.item_incoming_image_message);\n        this.outcomingImageConfig = new HolderConfig<>(DefaultOutcomingImageMessageViewHolder.class, R.layout.item_outcoming_image_message);\n    }\n\n    /**\n     * Sets both of custom view holder class and layout resource for incoming text message.\n     *\n     * @param holder holder class.\n     * @param layout layout resource.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setIncomingTextConfig(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends IMessage>> holder,\n            @LayoutRes int layout) {\n        this.incomingTextConfig.holder = holder;\n        this.incomingTextConfig.layout = layout;\n        return this;\n    }\n\n    /**\n     * Sets both of custom view holder class and layout resource for incoming text message.\n     *\n     * @param holder  holder class.\n     * @param layout  layout resource.\n     * @param payload custom data.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setIncomingTextConfig(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends IMessage>> holder,\n            @LayoutRes int layout,\n            Object payload) {\n        this.incomingTextConfig.holder = holder;\n        this.incomingTextConfig.layout = layout;\n        this.incomingTextConfig.payload = payload;\n        return this;\n    }\n\n    /**\n     * Sets custom view holder class for incoming text message.\n     *\n     * @param holder holder class.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setIncomingTextHolder(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends IMessage>> holder) {\n        this.incomingTextConfig.holder = holder;\n        return this;\n    }\n\n    /**\n     * Sets custom view holder class for incoming text message.\n     *\n     * @param holder  holder class.\n     * @param payload custom data.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setIncomingTextHolder(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends IMessage>> holder,\n            Object payload) {\n        this.incomingTextConfig.holder = holder;\n        this.incomingTextConfig.payload = payload;\n        return this;\n    }\n\n    /**\n     * Sets custom layout resource for incoming text message.\n     *\n     * @param layout layout resource.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setIncomingTextLayout(@LayoutRes int layout) {\n        this.incomingTextConfig.layout = layout;\n        return this;\n    }\n\n    /**\n     * Sets custom layout resource for incoming text message.\n     *\n     * @param layout  layout resource.\n     * @param payload custom data.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setIncomingTextLayout(@LayoutRes int layout, Object payload) {\n        this.incomingTextConfig.layout = layout;\n        this.incomingTextConfig.payload = payload;\n        return this;\n    }\n\n    /**\n     * Sets both of custom view holder class and layout resource for outcoming text message.\n     *\n     * @param holder holder class.\n     * @param layout layout resource.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setOutcomingTextConfig(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends IMessage>> holder,\n            @LayoutRes int layout) {\n        this.outcomingTextConfig.holder = holder;\n        this.outcomingTextConfig.layout = layout;\n        return this;\n    }\n\n    /**\n     * Sets both of custom view holder class and layout resource for outcoming text message.\n     *\n     * @param holder  holder class.\n     * @param layout  layout resource.\n     * @param payload custom data.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setOutcomingTextConfig(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends IMessage>> holder,\n            @LayoutRes int layout,\n            Object payload) {\n        this.outcomingTextConfig.holder = holder;\n        this.outcomingTextConfig.layout = layout;\n        this.outcomingTextConfig.payload = payload;\n        return this;\n    }\n\n    /**\n     * Sets custom view holder class for outcoming text message.\n     *\n     * @param holder holder class.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setOutcomingTextHolder(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends IMessage>> holder) {\n        this.outcomingTextConfig.holder = holder;\n        return this;\n    }\n\n    /**\n     * Sets custom view holder class for outcoming text message.\n     *\n     * @param holder  holder class.\n     * @param payload custom data.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setOutcomingTextHolder(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends IMessage>> holder,\n            Object payload) {\n        this.outcomingTextConfig.holder = holder;\n        this.outcomingTextConfig.payload = payload;\n        return this;\n    }\n\n    /**\n     * Sets custom layout resource for outcoming text message.\n     *\n     * @param layout layout resource.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setOutcomingTextLayout(@LayoutRes int layout) {\n        this.outcomingTextConfig.layout = layout;\n        return this;\n    }\n\n    /**\n     * Sets custom layout resource for outcoming text message.\n     *\n     * @param layout  layout resource.\n     * @param payload custom data.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setOutcomingTextLayout(@LayoutRes int layout, Object payload) {\n        this.outcomingTextConfig.layout = layout;\n        this.outcomingTextConfig.payload = payload;\n        return this;\n    }\n\n    /**\n     * Sets both of custom view holder class and layout resource for incoming image message.\n     *\n     * @param holder holder class.\n     * @param layout layout resource.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setIncomingImageConfig(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends MessageContentType.Image>> holder,\n            @LayoutRes int layout) {\n        this.incomingImageConfig.holder = holder;\n        this.incomingImageConfig.layout = layout;\n        return this;\n    }\n\n    /**\n     * Sets both of custom view holder class and layout resource for incoming image message.\n     *\n     * @param holder  holder class.\n     * @param layout  layout resource.\n     * @param payload custom data.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setIncomingImageConfig(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends MessageContentType.Image>> holder,\n            @LayoutRes int layout,\n            Object payload) {\n        this.incomingImageConfig.holder = holder;\n        this.incomingImageConfig.layout = layout;\n        this.incomingImageConfig.payload = payload;\n        return this;\n    }\n\n    /**\n     * Sets custom view holder class for incoming image message.\n     *\n     * @param holder holder class.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setIncomingImageHolder(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends MessageContentType.Image>> holder) {\n        this.incomingImageConfig.holder = holder;\n        return this;\n    }\n\n    /**\n     * Sets custom view holder class for incoming image message.\n     *\n     * @param holder  holder class.\n     * @param payload custom data.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setIncomingImageHolder(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends MessageContentType.Image>> holder,\n            Object payload) {\n        this.incomingImageConfig.holder = holder;\n        this.incomingImageConfig.payload = payload;\n        return this;\n    }\n\n    /**\n     * Sets custom layout resource for incoming image message.\n     *\n     * @param layout layout resource.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setIncomingImageLayout(@LayoutRes int layout) {\n        this.incomingImageConfig.layout = layout;\n        return this;\n    }\n\n    /**\n     * Sets custom layout resource for incoming image message.\n     *\n     * @param layout  layout resource.\n     * @param payload custom data.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setIncomingImageLayout(@LayoutRes int layout, Object payload) {\n        this.incomingImageConfig.layout = layout;\n        this.incomingImageConfig.payload = payload;\n        return this;\n    }\n\n    /**\n     * Sets both of custom view holder class and layout resource for outcoming image message.\n     *\n     * @param holder holder class.\n     * @param layout layout resource.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setOutcomingImageConfig(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends MessageContentType.Image>> holder,\n            @LayoutRes int layout) {\n        this.outcomingImageConfig.holder = holder;\n        this.outcomingImageConfig.layout = layout;\n        return this;\n    }\n\n    /**\n     * Sets both of custom view holder class and layout resource for outcoming image message.\n     *\n     * @param holder  holder class.\n     * @param layout  layout resource.\n     * @param payload custom data.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setOutcomingImageConfig(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends MessageContentType.Image>> holder,\n            @LayoutRes int layout,\n            Object payload) {\n        this.outcomingImageConfig.holder = holder;\n        this.outcomingImageConfig.layout = layout;\n        this.outcomingImageConfig.payload = payload;\n        return this;\n    }\n\n    /**\n     * Sets custom view holder class for outcoming image message.\n     *\n     * @param holder holder class.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setOutcomingImageHolder(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends MessageContentType.Image>> holder) {\n        this.outcomingImageConfig.holder = holder;\n        return this;\n    }\n\n    /**\n     * Sets custom view holder class for outcoming image message.\n     *\n     * @param holder  holder class.\n     * @param payload custom data.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setOutcomingImageHolder(\n            @NonNull Class<? extends BaseMessageViewHolder<? extends MessageContentType.Image>> holder,\n            Object payload) {\n        this.outcomingImageConfig.holder = holder;\n        this.outcomingImageConfig.payload = payload;\n        return this;\n    }\n\n    /**\n     * Sets custom layout resource for outcoming image message.\n     *\n     * @param layout layout resource.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setOutcomingImageLayout(@LayoutRes int layout) {\n        this.outcomingImageConfig.layout = layout;\n        return this;\n    }\n\n    /**\n     * Sets custom layout resource for outcoming image message.\n     *\n     * @param layout  layout resource.\n     * @param payload custom data.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setOutcomingImageLayout(@LayoutRes int layout, Object payload) {\n        this.outcomingImageConfig.layout = layout;\n        this.outcomingImageConfig.payload = payload;\n        return this;\n    }\n\n    /**\n     * Sets both of custom view holder class and layout resource for date header.\n     *\n     * @param holder holder class.\n     * @param layout layout resource.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setDateHeaderConfig(\n            @NonNull Class<? extends ViewHolder<Date>> holder,\n            @LayoutRes int layout) {\n        this.dateHeaderHolder = holder;\n        this.dateHeaderLayout = layout;\n        return this;\n    }\n\n    /**\n     * Sets custom view holder class for date header.\n     *\n     * @param holder holder class.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setDateHeaderHolder(@NonNull Class<? extends ViewHolder<Date>> holder) {\n        this.dateHeaderHolder = holder;\n        return this;\n    }\n\n    /**\n     * Sets custom layout reource for date header.\n     *\n     * @param layout layout resource.\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public MessageHolders setDateHeaderLayout(@LayoutRes int layout) {\n        this.dateHeaderLayout = layout;\n        return this;\n    }\n\n    /**\n     * Registers custom content type (e.g. multimedia, events etc.)\n     *\n     * @param type            unique id for content type\n     * @param holder          holder class for incoming and outcoming messages\n     * @param incomingLayout  layout resource for incoming message\n     * @param outcomingLayout layout resource for outcoming message\n     * @param contentChecker  {@link ContentChecker} for registered type\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public <TYPE extends MessageContentType>\n    MessageHolders registerContentType(\n            byte type, @NonNull Class<? extends BaseMessageViewHolder<TYPE>> holder,\n            @LayoutRes int incomingLayout,\n            @LayoutRes int outcomingLayout,\n            @NonNull ContentChecker contentChecker) {\n\n        return registerContentType(type,\n                holder, incomingLayout,\n                holder, outcomingLayout,\n                contentChecker);\n    }\n\n    /**\n     * Registers custom content type (e.g. multimedia, events etc.)\n     *\n     * @param type            unique id for content type\n     * @param incomingHolder  holder class for incoming message\n     * @param outcomingHolder holder class for outcoming message\n     * @param incomingLayout  layout resource for incoming message\n     * @param outcomingLayout layout resource for outcoming message\n     * @param contentChecker  {@link ContentChecker} for registered type\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public <TYPE extends MessageContentType>\n    MessageHolders registerContentType(\n            byte type,\n            @NonNull Class<? extends BaseMessageViewHolder<TYPE>> incomingHolder, @LayoutRes int incomingLayout,\n            @NonNull Class<? extends BaseMessageViewHolder<TYPE>> outcomingHolder, @LayoutRes int outcomingLayout,\n            @NonNull ContentChecker contentChecker) {\n\n        if (type == 0)\n            throw new IllegalArgumentException(\"content type must be greater or less than '0'!\");\n\n        customContentTypes.add(\n                new ContentTypeConfig<>(type,\n                        new HolderConfig<>(incomingHolder, incomingLayout),\n                        new HolderConfig<>(outcomingHolder, outcomingLayout)));\n        this.contentChecker = contentChecker;\n        return this;\n    }\n\n    /**\n     * Registers custom content type (e.g. multimedia, events etc.)\n     *\n     * @param type             unique id for content type\n     * @param incomingHolder   holder class for incoming message\n     * @param outcomingHolder  holder class for outcoming message\n     * @param incomingPayload  payload for incoming message\n     * @param outcomingPayload payload for outcoming message\n     * @param incomingLayout   layout resource for incoming message\n     * @param outcomingLayout  layout resource for outcoming message\n     * @param contentChecker   {@link MessageHolders.ContentChecker} for registered type\n     * @return {@link MessageHolders} for subsequent configuration.\n     */\n    public <TYPE extends MessageContentType>\n    MessageHolders registerContentType(\n            byte type,\n            @NonNull Class<? extends MessageHolders.BaseMessageViewHolder<TYPE>> incomingHolder, Object incomingPayload, @LayoutRes int incomingLayout,\n            @NonNull Class<? extends MessageHolders.BaseMessageViewHolder<TYPE>> outcomingHolder, Object outcomingPayload, @LayoutRes int outcomingLayout,\n            @NonNull MessageHolders.ContentChecker contentChecker) {\n\n        if (type == 0)\n            throw new IllegalArgumentException(\"content type must be greater or less than '0'!\");\n\n        customContentTypes.add(\n                new MessageHolders.ContentTypeConfig<>(type,\n                        new HolderConfig<>(incomingHolder, incomingLayout, incomingPayload),\n                        new HolderConfig<>(outcomingHolder, outcomingLayout, outcomingPayload)));\n        this.contentChecker = contentChecker;\n        return this;\n    }\n\n    /*\n     * INTERFACES\n     * */\n\n    /**\n     * The interface, which contains logic for checking the availability of content.\n     */\n    public interface ContentChecker<MESSAGE extends IMessage> {\n\n        /**\n         * Checks the availability of content.\n         *\n         * @param message current message in list.\n         * @param type    content type, for which content availability is determined.\n         * @return weather the message has content for the current message.\n         */\n        boolean hasContentFor(MESSAGE message, byte type);\n    }\n\n    /*\n     * PRIVATE METHODS\n     * */\n\n    protected ViewHolder getHolder(ViewGroup parent, int viewType, MessagesListStyle messagesListStyle) {\n        switch (viewType) {\n            case VIEW_TYPE_DATE_HEADER:\n                return getHolder(parent, dateHeaderLayout, dateHeaderHolder, messagesListStyle, null);\n            case VIEW_TYPE_STRING_HEADER:\n                return getHolder(parent, dateHeaderLayout, stringHeaderHolder, messagesListStyle, null);\n            case VIEW_TYPE_TEXT_MESSAGE:\n                return getHolder(parent, incomingTextConfig, messagesListStyle);\n            case -VIEW_TYPE_TEXT_MESSAGE:\n                return getHolder(parent, outcomingTextConfig, messagesListStyle);\n            case VIEW_TYPE_IMAGE_MESSAGE:\n                return getHolder(parent, incomingImageConfig, messagesListStyle);\n            case -VIEW_TYPE_IMAGE_MESSAGE:\n                return getHolder(parent, outcomingImageConfig, messagesListStyle);\n            default:\n                for (ContentTypeConfig typeConfig : customContentTypes) {\n                    if (Math.abs(typeConfig.type) == Math.abs(viewType)) {\n                        if (viewType > 0)\n                            return getHolder(parent, typeConfig.incomingConfig, messagesListStyle);\n                        else\n                            return getHolder(parent, typeConfig.outcomingConfig, messagesListStyle);\n                    }\n                }\n        }\n        throw new IllegalStateException(\"Wrong message view type. Please, report this issue on GitHub with full stacktrace in description.\");\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    protected void bind(final ViewHolder holder, final Object item, boolean isSelected,\n                        final ImageLoader imageLoader,\n                        final View.OnClickListener onMessageClickListener,\n                        final View.OnLongClickListener onMessageLongClickListener,\n                        final View.OnFocusChangeListener onMessageFocusChangeListener,\n                        final DateFormatter.Formatter dateHeadersFormatter,\n                        final SparseArray<MessagesListAdapter.OnMessageViewClickListener> clickListenersArray) {\n\n        if (item instanceof IMessage) {\n            ((MessageHolders.BaseMessageViewHolder) holder).isSelected = isSelected;\n            ((MessageHolders.BaseMessageViewHolder) holder).imageLoader = imageLoader;\n            holder.itemView.setOnLongClickListener(onMessageLongClickListener);\n            holder.itemView.setOnClickListener(onMessageClickListener);\n            holder.itemView.setOnFocusChangeListener(onMessageFocusChangeListener);\n\n            for (int i = 0; i < clickListenersArray.size(); i++) {\n                final int key = clickListenersArray.keyAt(i);\n                final View view = holder.itemView.findViewById(key);\n                if (view != null) {\n                    view.setOnClickListener(v -> clickListenersArray.get(key).onMessageViewClick(view, (IMessage) item));\n                }\n            }\n        } else if (item instanceof Date) {\n            ((MessageHolders.DefaultDateHeaderViewHolder) holder).dateHeadersFormatter = dateHeadersFormatter;\n        }\n\n        holder.onBind(item);\n    }\n\n\n    protected int getViewType(Object item, String senderId) {\n        boolean isOutcoming = false;\n        int viewType;\n\n        if (item instanceof IMessage) {\n            IMessage message = (IMessage) item;\n            isOutcoming = message.getUser().getId().contentEquals(senderId);\n            viewType = getContentViewType(message);\n\n        } else if (item instanceof String) {\n            viewType = VIEW_TYPE_STRING_HEADER;\n        } else viewType = VIEW_TYPE_DATE_HEADER;\n\n        return isOutcoming ? viewType * -1 : viewType;\n    }\n\n    private ViewHolder getHolder(ViewGroup parent, HolderConfig holderConfig,\n                                 MessagesListStyle style) {\n        return getHolder(parent, holderConfig.layout, holderConfig.holder, style, holderConfig.payload);\n    }\n\n    private <HOLDER extends ViewHolder>\n    ViewHolder getHolder(ViewGroup parent, @LayoutRes int layout, Class<HOLDER> holderClass,\n                         MessagesListStyle style, Object payload) {\n\n        View v = LayoutInflater.from(parent.getContext()).inflate(layout, parent, false);\n        try {\n            Constructor<HOLDER> constructor = null;\n            HOLDER holder;\n            try {\n                constructor = holderClass.getDeclaredConstructor(View.class, Object.class);\n                constructor.setAccessible(true);\n                holder = constructor.newInstance(v, payload);\n            } catch (NoSuchMethodException e) {\n                constructor = holderClass.getDeclaredConstructor(View.class);\n                constructor.setAccessible(true);\n                holder = constructor.newInstance(v);\n            }\n            if (holder instanceof DefaultMessageViewHolder && style != null) {\n                ((DefaultMessageViewHolder) holder).applyStyle(style);\n            }\n            return holder;\n        } catch (Exception e) {\n            throw new UnsupportedOperationException(\"Somehow we couldn't create the ViewHolder for message. Please, report this issue on GitHub with full stacktrace in description.\", e);\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private short getContentViewType(IMessage message) {\n        if (message instanceof MessageContentType.Image\n                && ((MessageContentType.Image) message).getImageUrl() != null) {\n            return VIEW_TYPE_IMAGE_MESSAGE;\n        }\n\n        // other default types will be here\n\n        if (message instanceof MessageContentType) {\n            for (int i = 0; i < customContentTypes.size(); i++) {\n                ContentTypeConfig config = customContentTypes.get(i);\n                if (contentChecker == null) {\n                    throw new IllegalArgumentException(\"ContentChecker cannot be null when using custom content types!\");\n                }\n                boolean hasContent = contentChecker.hasContentFor(message, config.type);\n                if (hasContent) return config.type;\n            }\n        }\n\n        return VIEW_TYPE_TEXT_MESSAGE;\n    }\n\n    /*\n     * HOLDERS\n     * */\n\n    /**\n     * The base class for view holders for incoming and outcoming message.\n     * You can extend it to create your own holder in conjuction with custom layout or even using default layout.\n     */\n    public static abstract class BaseMessageViewHolder<MESSAGE extends IMessage> extends ViewHolder<MESSAGE> {\n\n        boolean isSelected;\n\n        /**\n         * For setting custom data to ViewHolder\n         */\n        protected Object payload;\n\n        /**\n         * Callback for implementing images loading in message list\n         */\n        protected ImageLoader imageLoader;\n\n        @Deprecated\n        public BaseMessageViewHolder(View itemView) {\n            super(itemView);\n        }\n\n        public BaseMessageViewHolder(View itemView, Object payload) {\n            super(itemView);\n            this.payload = payload;\n        }\n\n        /**\n         * Returns whether is item selected\n         *\n         * @return weather is item selected.\n         */\n        public boolean isSelected() {\n            return isSelected;\n        }\n\n        /**\n         * Returns weather is selection mode enabled\n         *\n         * @return weather is selection mode enabled.\n         */\n        public boolean isSelectionModeEnabled() {\n            return MessagesListAdapter.isSelectionModeEnabled;\n        }\n\n        /**\n         * Getter for {@link #imageLoader}\n         *\n         * @return image loader interface.\n         */\n        public ImageLoader getImageLoader() {\n            return imageLoader;\n        }\n\n        protected void configureLinksBehavior(final TextView text) {\n            text.setLinksClickable(false);\n            text.setMovementMethod(new LinkMovementMethod() {\n                @Override\n                public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {\n                    boolean result = false;\n                    if (!MessagesListAdapter.isSelectionModeEnabled) {\n                        result = super.onTouchEvent(widget, buffer, event);\n                    }\n                    itemView.onTouchEvent(event);\n                    return result;\n                }\n            });\n        }\n    }\n\n    /**\n     * Default view holder implementation for incoming text message\n     */\n    public static class IncomingTextMessageViewHolder<MESSAGE extends IMessage>\n            extends BaseIncomingMessageViewHolder<MESSAGE> {\n\n        protected ViewGroup bubble;\n        protected TextView text;\n        // See: item_incoming_text_message.xml\n        protected RelativeLayout wrapper;\n\n        @Deprecated\n        public IncomingTextMessageViewHolder(View itemView) {\n            super(itemView);\n            init(itemView);\n        }\n\n        public IncomingTextMessageViewHolder(View itemView, Object payload) {\n            super(itemView, payload);\n            init(itemView);\n        }\n\n        @Override\n        public void onBind(MESSAGE message) {\n            super.onBind(message);\n            if (bubble != null) {\n                bubble.setSelected(isSelected());\n            }\n\n            if (text != null) {\n                text.setText(message.getText());\n            }\n        }\n\n        @SuppressLint(\"WrongConstant\")\n        @Override\n        public void applyStyle(MessagesListStyle style) {\n            super.applyStyle(style);\n            if (bubble != null) {\n                bubble.setPadding(style.getIncomingDefaultBubblePaddingLeft(),\n                        style.getIncomingDefaultBubblePaddingTop(),\n                        style.getIncomingDefaultBubblePaddingRight(),\n                        style.getIncomingDefaultBubblePaddingBottom());\n                ViewCompat.setBackground(bubble, style.getIncomingBubbleDrawable());\n            }\n\n            if (text != null) {\n                text.setTextColor(style.getIncomingTextColor());\n                text.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.getIncomingTextSize());\n                text.setTypeface(text.getTypeface(), style.getIncomingTextStyle());\n                text.setAutoLinkMask(style.getTextAutoLinkMask());\n                text.setLinkTextColor(style.getIncomingTextLinkColor());\n\n                // Link configurator makes textView focusable.\n                if (style.isMessageFocusable()) {\n                    configureLinksBehavior(text);\n\n                    //text.setFocusable(true);\n                    //text.setFocusableInTouchMode(true);\n                    //text.setClickable(true);\n                    //text.setBackgroundResource(R.drawable.bgchange);\n                    //text.requestFocus();\n\n                    wrapper.setFocusable(true);\n                    //wrapper.setFocusableInTouchMode(true);\n                    //wrapper.setClickable(true);\n                    //wrapper.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);\n                    //wrapper.requestFocus();\n                    //wrapper.setBackgroundResource(R.drawable.bgchange);\n\n                    //wrapper.setOnFocusChangeListener((v, hasFocus) -> {\n                    //    //text.setBackgroundResource(hasFocus ? R.color.tg_selected_bg : R.color.transparent);\n                    //    bubble.setBackgroundResource(hasFocus ? R.drawable.shape_incoming_message_focused : R.drawable.shape_incoming_message);\n                    //});\n                }\n            }\n        }\n\n        private void init(View itemView) {\n            bubble = itemView.findViewById(R.id.bubble);\n            text = itemView.findViewById(R.id.messageText);\n            wrapper = (RelativeLayout) itemView;\n        }\n    }\n\n    /**\n     * Default view holder implementation for outcoming text message\n     */\n    public static class OutcomingTextMessageViewHolder<MESSAGE extends IMessage>\n            extends BaseOutcomingMessageViewHolder<MESSAGE> {\n\n        protected ViewGroup bubble;\n        protected TextView text;\n        // See: item_outcoming_text_message.xml\n        protected RelativeLayout wrapper;\n\n        @Deprecated\n        public OutcomingTextMessageViewHolder(View itemView) {\n            super(itemView);\n            init(itemView);\n        }\n\n        public OutcomingTextMessageViewHolder(View itemView, Object payload) {\n            super(itemView, payload);\n            init(itemView);\n        }\n\n        @Override\n        public void onBind(MESSAGE message) {\n            super.onBind(message);\n            if (bubble != null) {\n                bubble.setSelected(isSelected());\n            }\n\n            if (text != null) {\n                text.setText(message.getText());\n            }\n        }\n\n        @SuppressLint(\"WrongConstant\")\n        @Override\n        public final void applyStyle(MessagesListStyle style) {\n            super.applyStyle(style);\n            if (bubble != null) {\n                bubble.setPadding(style.getOutcomingDefaultBubblePaddingLeft(),\n                        style.getOutcomingDefaultBubblePaddingTop(),\n                        style.getOutcomingDefaultBubblePaddingRight(),\n                        style.getOutcomingDefaultBubblePaddingBottom());\n                ViewCompat.setBackground(bubble, style.getOutcomingBubbleDrawable());\n            }\n\n            if (text != null) {\n                text.setTextColor(style.getOutcomingTextColor());\n                text.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.getOutcomingTextSize());\n                text.setTypeface(text.getTypeface(), style.getOutcomingTextStyle());\n                text.setAutoLinkMask(style.getTextAutoLinkMask());\n                text.setLinkTextColor(style.getOutcomingTextLinkColor());\n\n                // Link configurator makes textView focusable.\n                if (style.isMessageFocusable()) {\n                    configureLinksBehavior(text);\n                    wrapper.setFocusable(true);\n                    wrapper.setFocusableInTouchMode(true);\n                    wrapper.setClickable(true);\n                    wrapper.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);\n                    wrapper.requestFocus();\n                }\n            }\n        }\n\n        private void init(View itemView) {\n            bubble = itemView.findViewById(R.id.bubble);\n            text = itemView.findViewById(R.id.messageText);\n            wrapper = (RelativeLayout) itemView;\n        }\n    }\n\n    /**\n     * Default view holder implementation for incoming image message\n     */\n    public static class IncomingImageMessageViewHolder<MESSAGE extends MessageContentType.Image>\n            extends BaseIncomingMessageViewHolder<MESSAGE> {\n\n        protected ImageView image;\n        protected View imageOverlay;\n\n        @Deprecated\n        public IncomingImageMessageViewHolder(View itemView) {\n            super(itemView);\n            init(itemView);\n        }\n\n        public IncomingImageMessageViewHolder(View itemView, Object payload) {\n            super(itemView, payload);\n            init(itemView);\n        }\n\n        @Override\n        public void onBind(MESSAGE message) {\n            super.onBind(message);\n            if (image != null && imageLoader != null) {\n                imageLoader.loadImage(image, message.getImageUrl(), getPayloadForImageLoader(message));\n            }\n\n            if (imageOverlay != null) {\n                imageOverlay.setSelected(isSelected());\n            }\n        }\n\n        @SuppressLint(\"WrongConstant\")\n        @Override\n        public final void applyStyle(MessagesListStyle style) {\n            super.applyStyle(style);\n            if (time != null) {\n                time.setTextColor(style.getIncomingImageTimeTextColor());\n                time.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.getIncomingImageTimeTextSize());\n                time.setTypeface(time.getTypeface(), style.getIncomingImageTimeTextStyle());\n            }\n\n            if (imageOverlay != null) {\n                ViewCompat.setBackground(imageOverlay, style.getIncomingImageOverlayDrawable());\n            }\n        }\n\n        /**\n         * Override this method to have ability to pass custom data in ImageLoader for loading image(not avatar).\n         *\n         * @param message Message with image\n         */\n        protected Object getPayloadForImageLoader(MESSAGE message) {\n            return null;\n        }\n\n        private void init(View itemView) {\n            image = itemView.findViewById(R.id.image);\n            imageOverlay = itemView.findViewById(R.id.imageOverlay);\n\n            if (image instanceof RoundedImageView) {\n                ((RoundedImageView) image).setCorners(\n                        R.dimen.message_bubble_corners_radius,\n                        R.dimen.message_bubble_corners_radius,\n                        R.dimen.message_bubble_corners_radius,\n                        0\n                );\n            }\n        }\n    }\n\n    /**\n     * Default view holder implementation for outcoming image message\n     */\n    public static class OutcomingImageMessageViewHolder<MESSAGE extends MessageContentType.Image>\n            extends BaseOutcomingMessageViewHolder<MESSAGE> {\n\n        protected ImageView image;\n        protected View imageOverlay;\n\n        @Deprecated\n        public OutcomingImageMessageViewHolder(View itemView) {\n            super(itemView);\n            init(itemView);\n        }\n\n        public OutcomingImageMessageViewHolder(View itemView, Object payload) {\n            super(itemView, payload);\n            init(itemView);\n        }\n\n        @Override\n        public void onBind(MESSAGE message) {\n            super.onBind(message);\n            if (image != null && imageLoader != null) {\n                imageLoader.loadImage(image, message.getImageUrl(), getPayloadForImageLoader(message));\n            }\n\n            if (imageOverlay != null) {\n                imageOverlay.setSelected(isSelected());\n            }\n        }\n\n        @SuppressLint(\"WrongConstant\")\n        @Override\n        public final void applyStyle(MessagesListStyle style) {\n            super.applyStyle(style);\n            if (time != null) {\n                time.setTextColor(style.getOutcomingImageTimeTextColor());\n                time.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.getOutcomingImageTimeTextSize());\n                time.setTypeface(time.getTypeface(), style.getOutcomingImageTimeTextStyle());\n            }\n\n            if (imageOverlay != null) {\n                ViewCompat.setBackground(imageOverlay, style.getOutcomingImageOverlayDrawable());\n            }\n        }\n\n        /**\n         * Override this method to have ability to pass custom data in ImageLoader for loading image(not avatar).\n         *\n         * @param message Message with image\n         */\n        protected Object getPayloadForImageLoader(MESSAGE message) {\n            return null;\n        }\n\n        private void init(View itemView) {\n            image = itemView.findViewById(R.id.image);\n            imageOverlay = itemView.findViewById(R.id.imageOverlay);\n\n            if (image instanceof RoundedImageView) {\n                ((RoundedImageView) image).setCorners(\n                        R.dimen.message_bubble_corners_radius,\n                        R.dimen.message_bubble_corners_radius,\n                        0,\n                        R.dimen.message_bubble_corners_radius\n                );\n            }\n        }\n    }\n\n    /**\n     * Default view holder implementation for date header\n     */\n    public static class DefaultDateHeaderViewHolder extends ViewHolder<Date>\n            implements DefaultMessageViewHolder {\n\n        protected TextView text;\n        protected String dateFormat;\n        protected DateFormatter.Formatter dateHeadersFormatter;\n\n        public DefaultDateHeaderViewHolder(View itemView) {\n            super(itemView);\n            text = itemView.findViewById(R.id.messageText);\n        }\n\n        @Override\n        public void onBind(Date date) {\n            if (text != null) {\n                String formattedDate = null;\n                if (dateHeadersFormatter != null) formattedDate = dateHeadersFormatter.format(date);\n                text.setText(formattedDate == null ? DateFormatter.format(date, dateFormat) : formattedDate);\n            }\n        }\n\n        @SuppressLint(\"WrongConstant\")\n        @Override\n        public void applyStyle(MessagesListStyle style) {\n            if (text != null) {\n                text.setTextColor(style.getDateHeaderTextColor());\n                text.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.getDateHeaderTextSize());\n                text.setTypeface(text.getTypeface(), style.getDateHeaderTextStyle());\n                text.setPadding(style.getDateHeaderPadding(), style.getDateHeaderPadding(),\n                        style.getDateHeaderPadding(), style.getDateHeaderPadding());\n            }\n            dateFormat = style.getDateHeaderFormat();\n            dateFormat = dateFormat == null ? DateFormatter.Template.STRING_DAY_MONTH_YEAR.get() : dateFormat;\n        }\n    }\n\n    public static class DefaultStringHeaderViewHolder extends ViewHolder<String>\n            implements DefaultMessageViewHolder {\n\n        protected TextView text;\n\n        public DefaultStringHeaderViewHolder(View itemView) {\n            super(itemView);\n            text = itemView.findViewById(R.id.messageText);\n        }\n\n        @Override\n        public void onBind(String message) {\n            if (text != null) {\n                text.setText(message);\n            }\n        }\n\n        @SuppressLint(\"WrongConstant\")\n        @Override\n        public void applyStyle(MessagesListStyle style) {\n            if (text != null) {\n                text.setTextColor(style.getDateHeaderTextColor());\n                text.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.getDateHeaderTextSize());\n                text.setTypeface(text.getTypeface(), style.getDateHeaderTextStyle());\n                text.setPadding(style.getDateHeaderPadding(), style.getDateHeaderPadding(),\n                        style.getDateHeaderPadding(), style.getDateHeaderPadding());\n            }\n        }\n    }\n\n    /**\n     * Base view holder for incoming message\n     */\n    public abstract static class BaseIncomingMessageViewHolder<MESSAGE extends IMessage>\n            extends BaseMessageViewHolder<MESSAGE> implements DefaultMessageViewHolder {\n\n        protected TextView time;\n        protected ImageView userAvatar;\n\n        @Deprecated\n        public BaseIncomingMessageViewHolder(View itemView) {\n            super(itemView);\n            init(itemView);\n        }\n\n        public BaseIncomingMessageViewHolder(View itemView, Object payload) {\n            super(itemView, payload);\n            init(itemView);\n        }\n\n        @Override\n        public void onBind(MESSAGE message) {\n            if (time != null) {\n                time.setText(DateFormatter.format(message.getCreatedAt(), DateFormatter.Template.TIME));\n            }\n\n            if (userAvatar != null) {\n                boolean isAvatarExists = imageLoader != null\n                        && message.getUser().getAvatar() != null\n                        && !message.getUser().getAvatar().isEmpty();\n\n                userAvatar.setVisibility(isAvatarExists ? View.VISIBLE : View.GONE);\n                if (isAvatarExists) {\n                    imageLoader.loadImage(userAvatar, message.getUser().getAvatar(), null);\n                }\n            }\n        }\n\n        @SuppressLint(\"WrongConstant\")\n        @Override\n        public void applyStyle(MessagesListStyle style) {\n            if (time != null) {\n                time.setVisibility(style.showIncomingTime() ? View.VISIBLE : View.GONE);\n                time.setTextColor(style.getIncomingTimeTextColor());\n                time.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.getIncomingTimeTextSize());\n                time.setTypeface(time.getTypeface(), style.getIncomingTimeTextStyle());\n            }\n\n            if (userAvatar != null) {\n                userAvatar.getLayoutParams().width = style.getIncomingAvatarWidth();\n                userAvatar.getLayoutParams().height = style.getIncomingAvatarHeight();\n            }\n\n        }\n\n        private void init(View itemView) {\n            time = itemView.findViewById(R.id.messageTime);\n            userAvatar = itemView.findViewById(R.id.messageUserAvatar);\n        }\n    }\n\n    /**\n     * Base view holder for outcoming message\n     */\n    public abstract static class BaseOutcomingMessageViewHolder<MESSAGE extends IMessage>\n            extends BaseMessageViewHolder<MESSAGE> implements DefaultMessageViewHolder {\n\n        protected TextView time;\n\n        @Deprecated\n        public BaseOutcomingMessageViewHolder(View itemView) {\n            super(itemView);\n            init(itemView);\n        }\n\n        public BaseOutcomingMessageViewHolder(View itemView, Object payload) {\n            super(itemView, payload);\n            init(itemView);\n        }\n\n        @Override\n        public void onBind(MESSAGE message) {\n            if (time != null) {\n                time.setText(DateFormatter.format(message.getCreatedAt(), DateFormatter.Template.TIME));\n            }\n        }\n\n        @SuppressLint(\"WrongConstant\")\n        @Override\n        public void applyStyle(MessagesListStyle style) {\n            if (time != null) {\n                time.setVisibility(style.showOutcomingTime() ? View.VISIBLE : View.GONE);\n                time.setTextColor(style.getOutcomingTimeTextColor());\n                time.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.getOutcomingTimeTextSize());\n                time.setTypeface(time.getTypeface(), style.getOutcomingTimeTextStyle());\n            }\n        }\n\n        private void init(View itemView) {\n            time = itemView.findViewById(R.id.messageTime);\n        }\n    }\n\n    /*\n     * DEFAULTS\n     * */\n\n    interface DefaultMessageViewHolder {\n        void applyStyle(MessagesListStyle style);\n    }\n\n    private static class DefaultIncomingTextMessageViewHolder\n            extends IncomingTextMessageViewHolder<IMessage> {\n\n        public DefaultIncomingTextMessageViewHolder(View itemView) {\n            super(itemView, null);\n        }\n    }\n\n    private static class DefaultOutcomingTextMessageViewHolder\n            extends OutcomingTextMessageViewHolder<IMessage> {\n\n        public DefaultOutcomingTextMessageViewHolder(View itemView) {\n            super(itemView, null);\n        }\n    }\n\n    private static class DefaultIncomingImageMessageViewHolder\n            extends IncomingImageMessageViewHolder<MessageContentType.Image> {\n\n        public DefaultIncomingImageMessageViewHolder(View itemView) {\n            super(itemView, null);\n        }\n    }\n\n    private static class DefaultOutcomingImageMessageViewHolder\n            extends OutcomingImageMessageViewHolder<MessageContentType.Image> {\n\n        public DefaultOutcomingImageMessageViewHolder(View itemView) {\n            super(itemView, null);\n        }\n    }\n\n    private static class ContentTypeConfig<TYPE extends MessageContentType> {\n\n        private byte type;\n        private HolderConfig<TYPE> incomingConfig;\n        private HolderConfig<TYPE> outcomingConfig;\n\n        private ContentTypeConfig(\n                byte type, HolderConfig<TYPE> incomingConfig, HolderConfig<TYPE> outcomingConfig) {\n\n            this.type = type;\n            this.incomingConfig = incomingConfig;\n            this.outcomingConfig = outcomingConfig;\n        }\n    }\n\n    private static class HolderConfig<T extends IMessage> {\n\n        protected Class<? extends BaseMessageViewHolder<? extends T>> holder;\n        protected int layout;\n        protected Object payload;\n\n        HolderConfig(Class<? extends BaseMessageViewHolder<? extends T>> holder, int layout) {\n            this.holder = holder;\n            this.layout = layout;\n        }\n\n        HolderConfig(Class<? extends BaseMessageViewHolder<? extends T>> holder, int layout, Object payload) {\n            this.holder = holder;\n            this.layout = layout;\n            this.payload = payload;\n        }\n    }\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/messages/MessageInput.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.messages;\n\nimport android.content.Context;\nimport android.graphics.drawable.Drawable;\nimport android.os.Build;\nimport android.text.Editable;\nimport android.text.TextWatcher;\nimport android.util.AttributeSet;\nimport android.util.TypedValue;\nimport android.view.View;\nimport android.widget.EditText;\nimport android.widget.ImageButton;\nimport android.widget.RelativeLayout;\nimport android.widget.Space;\nimport android.widget.TextView;\n\nimport androidx.core.view.ViewCompat;\n\nimport com.stfalcon.chatkit.R;\n\nimport java.lang.reflect.Field;\n\n/**\n * Component for input outcoming messages\n */\n@SuppressWarnings({\"WeakerAccess\", \"unused\"})\npublic class MessageInput extends RelativeLayout\n        implements View.OnClickListener, TextWatcher, View.OnFocusChangeListener {\n\n    protected EditText messageInput;\n    protected ImageButton messageSendButton;\n    protected ImageButton attachmentButton;\n    protected Space sendButtonSpace, attachmentButtonSpace;\n\n    private CharSequence input;\n    private InputListener inputListener;\n    private AttachmentsListener attachmentsListener;\n    private boolean isTyping;\n    private TypingListener typingListener;\n    private int delayTypingStatusMillis;\n    private Runnable typingTimerRunnable = new Runnable() {\n        @Override\n        public void run() {\n            if (isTyping) {\n                isTyping = false;\n                if (typingListener != null) typingListener.onStopTyping();\n            }\n        }\n    };\n    private boolean lastFocus;\n\n    public MessageInput(Context context) {\n        super(context);\n        init(context);\n    }\n\n    public MessageInput(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public MessageInput(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    /**\n     * Sets callback for 'submit' button.\n     *\n     * @param inputListener input callback\n     */\n    public void setInputListener(InputListener inputListener) {\n        this.inputListener = inputListener;\n    }\n\n    /**\n     * Sets callback for 'add' button.\n     *\n     * @param attachmentsListener input callback\n     */\n    public void setAttachmentsListener(AttachmentsListener attachmentsListener) {\n        this.attachmentsListener = attachmentsListener;\n    }\n\n    /**\n     * Returns EditText for messages input\n     *\n     * @return EditText\n     */\n    public EditText getInputEditText() {\n        return messageInput;\n    }\n\n    /**\n     * Returns `submit` button\n     *\n     * @return ImageButton\n     */\n    public ImageButton getButton() {\n        return messageSendButton;\n    }\n\n    @Override\n    public void onClick(View view) {\n        int id = view.getId();\n        if (id == R.id.messageSendButton) {\n            boolean isSubmitted = onSubmit();\n            if (isSubmitted) {\n                messageInput.setText(\"\");\n            }\n            removeCallbacks(typingTimerRunnable);\n            post(typingTimerRunnable);\n        } else if (id == R.id.attachmentButton) {\n            onAddAttachments();\n        }\n    }\n\n    /**\n     * This method is called to notify you that, within s,\n     * the count characters beginning at start have just replaced old text that had length before\n     */\n    @Override\n    public void onTextChanged(CharSequence s, int start, int count, int after) {\n        input = s;\n        messageSendButton.setEnabled(input.length() > 0);\n        if (s.length() > 0) {\n            if (!isTyping) {\n                isTyping = true;\n                if (typingListener != null) typingListener.onStartTyping();\n            }\n            removeCallbacks(typingTimerRunnable);\n            postDelayed(typingTimerRunnable, delayTypingStatusMillis);\n        }\n    }\n\n    /**\n     * This method is called to notify you that, within s,\n     * the count characters beginning at start are about to be replaced by new text with length after.\n     */\n    @Override\n    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {\n        //do nothing\n    }\n\n    /**\n     * This method is called to notify you that, somewhere within s, the text has been changed.\n     */\n    @Override\n    public void afterTextChanged(Editable editable) {\n        //do nothing\n    }\n\n    @Override\n    public void onFocusChange(View v, boolean hasFocus) {\n        if (lastFocus && !hasFocus && typingListener != null) {\n            typingListener.onStopTyping();\n        }\n        lastFocus = hasFocus;\n    }\n\n    private boolean onSubmit() {\n        return inputListener != null && inputListener.onSubmit(input);\n    }\n\n    private void onAddAttachments() {\n        if (attachmentsListener != null) attachmentsListener.onAddAttachments();\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        init(context);\n        MessageInputStyle style = MessageInputStyle.parse(context, attrs);\n\n        this.messageInput.setMaxLines(style.getInputMaxLines());\n        this.messageInput.setHint(style.getInputHint());\n        this.messageInput.setText(style.getInputText());\n        this.messageInput.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.getInputTextSize());\n        this.messageInput.setTextColor(style.getInputTextColor());\n        this.messageInput.setHintTextColor(style.getInputHintColor());\n        ViewCompat.setBackground(this.messageInput, style.getInputBackground());\n        setCursor(style.getInputCursorDrawable());\n\n        this.attachmentButton.setVisibility(style.showAttachmentButton() ? VISIBLE : GONE);\n        this.attachmentButton.setImageDrawable(style.getAttachmentButtonIcon());\n        this.attachmentButton.getLayoutParams().width = style.getAttachmentButtonWidth();\n        this.attachmentButton.getLayoutParams().height = style.getAttachmentButtonHeight();\n        ViewCompat.setBackground(this.attachmentButton, style.getAttachmentButtonBackground());\n\n        this.attachmentButtonSpace.setVisibility(style.showAttachmentButton() ? VISIBLE : GONE);\n        this.attachmentButtonSpace.getLayoutParams().width = style.getAttachmentButtonMargin();\n\n        this.messageSendButton.setImageDrawable(style.getInputButtonIcon());\n        this.messageSendButton.getLayoutParams().width = style.getInputButtonWidth();\n        this.messageSendButton.getLayoutParams().height = style.getInputButtonHeight();\n        ViewCompat.setBackground(messageSendButton, style.getInputButtonBackground());\n        this.sendButtonSpace.getLayoutParams().width = style.getInputButtonMargin();\n\n        if (getPaddingLeft() == 0\n                && getPaddingRight() == 0\n                && getPaddingTop() == 0\n                && getPaddingBottom() == 0) {\n            setPadding(\n                    style.getInputDefaultPaddingLeft(),\n                    style.getInputDefaultPaddingTop(),\n                    style.getInputDefaultPaddingRight(),\n                    style.getInputDefaultPaddingBottom()\n            );\n        }\n        this.delayTypingStatusMillis = style.getDelayTypingStatus();\n    }\n\n    private void init(Context context) {\n        inflate(context, R.layout.view_message_input, this);\n\n        messageInput = findViewById(R.id.messageInput);\n        messageSendButton = findViewById(R.id.messageSendButton);\n        attachmentButton = findViewById(R.id.attachmentButton);\n        sendButtonSpace = findViewById(R.id.sendButtonSpace);\n        attachmentButtonSpace = findViewById(R.id.attachmentButtonSpace);\n\n        messageSendButton.setOnClickListener(this);\n        attachmentButton.setOnClickListener(this);\n        messageInput.addTextChangedListener(this);\n        messageInput.setText(\"\");\n        messageInput.setOnFocusChangeListener(this);\n    }\n\n    private void setCursor(Drawable drawable) {\n        if (drawable == null) return;\n\n        try {\n            final Field drawableResField = TextView.class.getDeclaredField(\"mCursorDrawableRes\");\n            drawableResField.setAccessible(true);\n\n            final Object drawableFieldOwner;\n            final Class<?> drawableFieldClass;\n            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {\n                drawableFieldOwner = this.messageInput;\n                drawableFieldClass = TextView.class;\n            } else {\n                final Field editorField = TextView.class.getDeclaredField(\"mEditor\");\n                editorField.setAccessible(true);\n                drawableFieldOwner = editorField.get(this.messageInput);\n                drawableFieldClass = drawableFieldOwner.getClass();\n            }\n            final Field drawableField = drawableFieldClass.getDeclaredField(\"mCursorDrawable\");\n            drawableField.setAccessible(true);\n            drawableField.set(drawableFieldOwner, new Drawable[]{drawable, drawable});\n        } catch (Exception ignored) {\n        }\n    }\n\n    public void setTypingListener(TypingListener typingListener) {\n        this.typingListener = typingListener;\n    }\n\n    /**\n     * Interface definition for a callback to be invoked when user pressed 'submit' button\n     */\n    public interface InputListener {\n\n        /**\n         * Fires when user presses 'send' button.\n         *\n         * @param input input entered by user\n         * @return if input text is valid, you must return {@code true} and input will be cleared, otherwise return false.\n         */\n        boolean onSubmit(CharSequence input);\n    }\n\n    /**\n     * Interface definition for a callback to be invoked when user presses 'add' button\n     */\n    public interface AttachmentsListener {\n\n        /**\n         * Fires when user presses 'add' button.\n         */\n        void onAddAttachments();\n    }\n\n    /**\n     * Interface definition for a callback to be invoked when user typing\n     */\n    public interface TypingListener {\n\n        /**\n         * Fires when user presses start typing\n         */\n        void onStartTyping();\n\n        /**\n         * Fires when user presses stop typing\n         */\n        void onStopTyping();\n\n    }\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/messages/MessageInputStyle.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.messages;\n\nimport android.content.Context;\nimport android.content.res.ColorStateList;\nimport android.content.res.TypedArray;\nimport android.graphics.drawable.Drawable;\nimport android.util.AttributeSet;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.DrawableRes;\nimport androidx.core.graphics.drawable.DrawableCompat;\n\nimport com.stfalcon.chatkit.R;\nimport com.stfalcon.chatkit.commons.Style;\n\n/**\n * Style for MessageInputStyle customization by xml attributes\n */\n@SuppressWarnings(\"WeakerAccess\")\nclass MessageInputStyle extends Style {\n\n    private static final int DEFAULT_MAX_LINES = 5;\n    private static final int DEFAULT_DELAY_TYPING_STATUS = 1500;\n\n    private boolean showAttachmentButton;\n\n    private int attachmentButtonBackground;\n    private int attachmentButtonDefaultBgColor;\n    private int attachmentButtonDefaultBgPressedColor;\n    private int attachmentButtonDefaultBgDisabledColor;\n\n    private int attachmentButtonIcon;\n    private int attachmentButtonDefaultIconColor;\n    private int attachmentButtonDefaultIconPressedColor;\n    private int attachmentButtonDefaultIconDisabledColor;\n\n    private int attachmentButtonWidth;\n    private int attachmentButtonHeight;\n    private int attachmentButtonMargin;\n\n    private int inputButtonBackground;\n    private int inputButtonDefaultBgColor;\n    private int inputButtonDefaultBgPressedColor;\n    private int inputButtonDefaultBgDisabledColor;\n\n    private int inputButtonIcon;\n    private int inputButtonDefaultIconColor;\n    private int inputButtonDefaultIconPressedColor;\n    private int inputButtonDefaultIconDisabledColor;\n\n    private int inputButtonWidth;\n    private int inputButtonHeight;\n    private int inputButtonMargin;\n\n    private int inputMaxLines;\n    private String inputHint;\n    private String inputText;\n\n    private int inputTextSize;\n    private int inputTextColor;\n    private int inputHintColor;\n\n    private Drawable inputBackground;\n    private Drawable inputCursorDrawable;\n\n    private int inputDefaultPaddingLeft;\n    private int inputDefaultPaddingRight;\n    private int inputDefaultPaddingTop;\n    private int inputDefaultPaddingBottom;\n\n    private int delayTypingStatus;\n\n    static MessageInputStyle parse(Context context, AttributeSet attrs) {\n        MessageInputStyle style = new MessageInputStyle(context, attrs);\n        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MessageInput);\n\n        style.showAttachmentButton = typedArray.getBoolean(R.styleable.MessageInput_showAttachmentButton, false);\n\n        style.attachmentButtonBackground = typedArray.getResourceId(R.styleable.MessageInput_attachmentButtonBackground, -1);\n        style.attachmentButtonDefaultBgColor = typedArray.getColor(R.styleable.MessageInput_attachmentButtonDefaultBgColor,\n                style.getColor(R.color.white_four));\n        style.attachmentButtonDefaultBgPressedColor = typedArray.getColor(R.styleable.MessageInput_attachmentButtonDefaultBgPressedColor,\n                style.getColor(R.color.white_five));\n        style.attachmentButtonDefaultBgDisabledColor = typedArray.getColor(R.styleable.MessageInput_attachmentButtonDefaultBgDisabledColor,\n                style.getColor(R.color.transparent));\n\n        style.attachmentButtonIcon = typedArray.getResourceId(R.styleable.MessageInput_attachmentButtonIcon, -1);\n        style.attachmentButtonDefaultIconColor = typedArray.getColor(R.styleable.MessageInput_attachmentButtonDefaultIconColor,\n                style.getColor(R.color.cornflower_blue_two));\n        style.attachmentButtonDefaultIconPressedColor = typedArray.getColor(R.styleable.MessageInput_attachmentButtonDefaultIconPressedColor,\n                style.getColor(R.color.cornflower_blue_two_dark));\n        style.attachmentButtonDefaultIconDisabledColor = typedArray.getColor(R.styleable.MessageInput_attachmentButtonDefaultIconDisabledColor,\n                style.getColor(R.color.cornflower_blue_light_40));\n\n        style.attachmentButtonWidth = typedArray.getDimensionPixelSize(R.styleable.MessageInput_attachmentButtonWidth, style.getDimension(R.dimen.input_button_width));\n        style.attachmentButtonHeight = typedArray.getDimensionPixelSize(R.styleable.MessageInput_attachmentButtonHeight, style.getDimension(R.dimen.input_button_height));\n        style.attachmentButtonMargin = typedArray.getDimensionPixelSize(R.styleable.MessageInput_attachmentButtonMargin, style.getDimension(R.dimen.input_button_margin));\n\n        style.inputButtonBackground = typedArray.getResourceId(R.styleable.MessageInput_inputButtonBackground, -1);\n        style.inputButtonDefaultBgColor = typedArray.getColor(R.styleable.MessageInput_inputButtonDefaultBgColor,\n                style.getColor(R.color.cornflower_blue_two));\n        style.inputButtonDefaultBgPressedColor = typedArray.getColor(R.styleable.MessageInput_inputButtonDefaultBgPressedColor,\n                style.getColor(R.color.cornflower_blue_two_dark));\n        style.inputButtonDefaultBgDisabledColor = typedArray.getColor(R.styleable.MessageInput_inputButtonDefaultBgDisabledColor,\n                style.getColor(R.color.white_four));\n\n        style.inputButtonIcon = typedArray.getResourceId(R.styleable.MessageInput_inputButtonIcon, -1);\n        style.inputButtonDefaultIconColor = typedArray.getColor(R.styleable.MessageInput_inputButtonDefaultIconColor,\n                style.getColor(R.color.white));\n        style.inputButtonDefaultIconPressedColor = typedArray.getColor(R.styleable.MessageInput_inputButtonDefaultIconPressedColor,\n                style.getColor(R.color.white));\n        style.inputButtonDefaultIconDisabledColor = typedArray.getColor(R.styleable.MessageInput_inputButtonDefaultIconDisabledColor,\n                style.getColor(R.color.warm_grey));\n\n        style.inputButtonWidth = typedArray.getDimensionPixelSize(R.styleable.MessageInput_inputButtonWidth, style.getDimension(R.dimen.input_button_width));\n        style.inputButtonHeight = typedArray.getDimensionPixelSize(R.styleable.MessageInput_inputButtonHeight, style.getDimension(R.dimen.input_button_height));\n        style.inputButtonMargin = typedArray.getDimensionPixelSize(R.styleable.MessageInput_inputButtonMargin, style.getDimension(R.dimen.input_button_margin));\n\n        style.inputMaxLines = typedArray.getInt(R.styleable.MessageInput_inputMaxLines, DEFAULT_MAX_LINES);\n        style.inputHint = typedArray.getString(R.styleable.MessageInput_inputHint);\n        style.inputText = typedArray.getString(R.styleable.MessageInput_inputText);\n\n        style.inputTextSize = typedArray.getDimensionPixelSize(R.styleable.MessageInput_inputTextSize, style.getDimension(R.dimen.input_text_size));\n        style.inputTextColor = typedArray.getColor(R.styleable.MessageInput_inputTextColor, style.getColor(R.color.dark_grey_two));\n        style.inputHintColor = typedArray.getColor(R.styleable.MessageInput_inputHintColor, style.getColor(R.color.warm_grey_three));\n\n        style.inputBackground = typedArray.getDrawable(R.styleable.MessageInput_inputBackground);\n        style.inputCursorDrawable = typedArray.getDrawable(R.styleable.MessageInput_inputCursorDrawable);\n\n        style.delayTypingStatus = typedArray.getInt(R.styleable.MessageInput_delayTypingStatus, DEFAULT_DELAY_TYPING_STATUS);\n\n        typedArray.recycle();\n\n        style.inputDefaultPaddingLeft = style.getDimension(R.dimen.input_padding_left);\n        style.inputDefaultPaddingRight = style.getDimension(R.dimen.input_padding_right);\n        style.inputDefaultPaddingTop = style.getDimension(R.dimen.input_padding_top);\n        style.inputDefaultPaddingBottom = style.getDimension(R.dimen.input_padding_bottom);\n\n        return style;\n    }\n\n    private MessageInputStyle(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    private Drawable getSelector(@ColorInt int normalColor, @ColorInt int pressedColor,\n                                 @ColorInt int disabledColor, @DrawableRes int shape) {\n\n        Drawable drawable = DrawableCompat.wrap(getVectorDrawable(shape)).mutate();\n        DrawableCompat.setTintList(\n                drawable,\n                new ColorStateList(\n                        new int[][]{\n                                new int[]{android.R.attr.state_enabled, -android.R.attr.state_pressed},\n                                new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed},\n                                new int[]{-android.R.attr.state_enabled}\n                        },\n                        new int[]{normalColor, pressedColor, disabledColor}\n                ));\n        return drawable;\n    }\n\n    protected boolean showAttachmentButton() {\n        return showAttachmentButton;\n    }\n\n    protected Drawable getAttachmentButtonBackground() {\n        if (attachmentButtonBackground == -1) {\n            return getSelector(attachmentButtonDefaultBgColor, attachmentButtonDefaultBgPressedColor,\n                    attachmentButtonDefaultBgDisabledColor, R.drawable.mask);\n        } else {\n            return getDrawable(attachmentButtonBackground);\n        }\n    }\n\n    protected Drawable getAttachmentButtonIcon() {\n        if (attachmentButtonIcon == -1) {\n            return getSelector(attachmentButtonDefaultIconColor, attachmentButtonDefaultIconPressedColor,\n                    attachmentButtonDefaultIconDisabledColor, R.drawable.ic_add_attachment);\n        } else {\n            return getDrawable(attachmentButtonIcon);\n        }\n    }\n\n    protected int getAttachmentButtonWidth() {\n        return attachmentButtonWidth;\n    }\n\n    protected int getAttachmentButtonHeight() {\n        return attachmentButtonHeight;\n    }\n\n    protected int getAttachmentButtonMargin() {\n        return attachmentButtonMargin;\n    }\n\n    protected Drawable getInputButtonBackground() {\n        if (inputButtonBackground == -1) {\n            return getSelector(inputButtonDefaultBgColor, inputButtonDefaultBgPressedColor,\n                    inputButtonDefaultBgDisabledColor, R.drawable.mask);\n        } else {\n            return getDrawable(inputButtonBackground);\n        }\n    }\n\n    protected Drawable getInputButtonIcon() {\n        if (inputButtonIcon == -1) {\n            return getSelector(inputButtonDefaultIconColor, inputButtonDefaultIconPressedColor,\n                    inputButtonDefaultIconDisabledColor, R.drawable.ic_send);\n        } else {\n            return getDrawable(inputButtonIcon);\n        }\n    }\n\n    protected int getInputButtonMargin() {\n        return inputButtonMargin;\n    }\n\n    protected int getInputButtonWidth() {\n        return inputButtonWidth;\n    }\n\n    protected int getInputButtonHeight() {\n        return inputButtonHeight;\n    }\n\n    protected int getInputMaxLines() {\n        return inputMaxLines;\n    }\n\n    protected String getInputHint() {\n        return inputHint;\n    }\n\n    protected String getInputText() {\n        return inputText;\n    }\n\n    protected int getInputTextSize() {\n        return inputTextSize;\n    }\n\n    protected int getInputTextColor() {\n        return inputTextColor;\n    }\n\n    protected int getInputHintColor() {\n        return inputHintColor;\n    }\n\n    protected Drawable getInputBackground() {\n        return inputBackground;\n    }\n\n    protected Drawable getInputCursorDrawable() {\n        return inputCursorDrawable;\n    }\n\n    protected int getInputDefaultPaddingLeft() {\n        return inputDefaultPaddingLeft;\n    }\n\n    protected int getInputDefaultPaddingRight() {\n        return inputDefaultPaddingRight;\n    }\n\n    protected int getInputDefaultPaddingTop() {\n        return inputDefaultPaddingTop;\n    }\n\n    protected int getInputDefaultPaddingBottom() {\n        return inputDefaultPaddingBottom;\n    }\n\n    int getDelayTypingStatus() {\n        return delayTypingStatus;\n    }\n\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/messages/MessagesList.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.messages;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport androidx.annotation.Nullable;\nimport androidx.recyclerview.widget.DefaultItemAnimator;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.SimpleItemAnimator;\n\nimport com.stfalcon.chatkit.commons.models.IMessage;\n\n/**\n * Component for displaying list of messages\n */\npublic class MessagesList extends RecyclerView {\n    private MessagesListStyle messagesListStyle;\n\n    public MessagesList(Context context) {\n        super(context);\n    }\n\n    public MessagesList(Context context, @Nullable AttributeSet attrs) {\n        super(context, attrs);\n        parseStyle(context, attrs);\n    }\n\n    public MessagesList(Context context, @Nullable AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        parseStyle(context, attrs);\n    }\n\n    /**\n     * Don't use this method for setting your adapter, otherwise exception will by thrown.\n     * Call {@link #setAdapter(MessagesListAdapter)} instead.\n     */\n    @Override\n    public void setAdapter(Adapter adapter) {\n        throw new IllegalArgumentException(\"You can't set adapter to MessagesList. Use #setAdapter(MessagesListAdapter) instead.\");\n    }\n\n    /**\n     * Sets adapter for MessagesList\n     *\n     * @param adapter   Adapter. Must extend MessagesListAdapter\n     * @param <MESSAGE> Message model class\n     */\n    public <MESSAGE extends IMessage>\n    void setAdapter(MessagesListAdapter<MESSAGE> adapter) {\n        setAdapter(adapter, true);\n    }\n\n    /**\n     * Sets adapter for MessagesList\n     *\n     * @param adapter       Adapter. Must extend MessagesListAdapter\n     * @param reverseLayout weather to use reverse layout for layout manager.\n     * @param <MESSAGE>     Message model class\n     */\n    public <MESSAGE extends IMessage>\n    void setAdapter(MessagesListAdapter<MESSAGE> adapter, boolean reverseLayout) {\n        SimpleItemAnimator itemAnimator = new DefaultItemAnimator();\n        itemAnimator.setSupportsChangeAnimations(false);\n\n        LinearLayoutManager layoutManager = new LinearLayoutManager(getContext(),\n                LinearLayoutManager.VERTICAL, reverseLayout);\n\n        // The solution for aligning items to the top instead of bottom\n        // https://stackoverflow.com/questions/46168245/recyclerview-reverse-order\n        //layoutManager.setStackFromEnd(true);\n\n        setItemAnimator(itemAnimator);\n        setLayoutManager(layoutManager);\n        adapter.setLayoutManager(layoutManager);\n        adapter.setStyle(messagesListStyle);\n\n        addOnScrollListener(new RecyclerScrollMoreListener(layoutManager, adapter));\n        super.setAdapter(adapter);\n    }\n\n    @SuppressWarnings(\"ResourceType\")\n    private void parseStyle(Context context, AttributeSet attrs) {\n        messagesListStyle = MessagesListStyle.parse(context, attrs);\n    }\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/messages/MessagesListAdapter.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.messages;\n\nimport android.annotation.SuppressLint;\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.LayerDrawable;\nimport android.text.Spannable;\nimport android.text.method.LinkMovementMethod;\nimport android.util.SparseArray;\nimport android.util.TypedValue;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\nimport androidx.annotation.LayoutRes;\nimport androidx.core.content.ContextCompat;\nimport androidx.core.graphics.drawable.DrawableCompat;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.liskovsoft.sharedutils.helpers.Helpers;\nimport com.stfalcon.chatkit.R;\nimport com.stfalcon.chatkit.commons.DebouncedOnClickListener;\nimport com.stfalcon.chatkit.commons.ImageLoader;\nimport com.stfalcon.chatkit.commons.ViewHolder;\nimport com.stfalcon.chatkit.commons.models.IMessage;\nimport com.stfalcon.chatkit.utils.DateFormatter;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * Adapter for {@link MessagesList}.\n */\n@SuppressWarnings(\"WeakerAccess\")\npublic class MessagesListAdapter<MESSAGE extends IMessage>\n        extends RecyclerView.Adapter<ViewHolder<MESSAGE>>\n        implements RecyclerScrollMoreListener.OnLoadMoreListener {\n\n    protected static boolean isSelectionModeEnabled;\n\n    protected List<Wrapper> items;\n    private MessageHolders holders;\n    private String senderId;\n\n    private int selectedItemsCount;\n    private int maxItemsCount;\n    private SelectionListener selectionListener;\n\n    private OnLoadMoreListener loadMoreListener;\n    private OnMessageClickListener<MESSAGE> onMessageClickListener;\n    private OnMessageViewClickListener<MESSAGE> onMessageViewClickListener;\n    private OnMessageViewFocusListener<MESSAGE> onMessageViewFocusListener;\n    private OnMessageLongClickListener<MESSAGE> onMessageLongClickListener;\n    private OnMessageViewLongClickListener<MESSAGE> onMessageViewLongClickListener;\n    private ImageLoader imageLoader;\n    private RecyclerView.LayoutManager layoutManager;\n    private MessagesListStyle messagesListStyle;\n    private DateFormatter.Formatter dateHeadersFormatter;\n    private SparseArray<OnMessageViewClickListener> viewClickListenersArray = new SparseArray<>();\n    private boolean isDateHeaderEnabled;\n    private MESSAGE focusedMessage;\n\n    /**\n     * For default list item layout and view holder.\n     *\n     * @param senderId    identifier of sender.\n     * @param imageLoader image loading method.\n     */\n    public MessagesListAdapter(String senderId, ImageLoader imageLoader) {\n        this(senderId, new MessageHolders(), imageLoader);\n    }\n\n    /**\n     * For default list item layout and view holder.\n     *\n     * @param senderId    identifier of sender.\n     * @param holders     custom layouts and view holders. See {@link MessageHolders} documentation for details\n     * @param imageLoader image loading method.\n     */\n    public MessagesListAdapter(String senderId, MessageHolders holders,\n                               ImageLoader imageLoader) {\n        this.senderId = senderId;\n        this.holders = holders;\n        this.imageLoader = imageLoader;\n        this.items = new ArrayList<>();\n    }\n\n    @Override\n    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {\n        return holders.getHolder(parent, viewType, messagesListStyle);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    public void onBindViewHolder(ViewHolder holder, int position) {\n        Wrapper wrapper = items.get(position);\n        holders.bind(holder, wrapper.item, wrapper.isSelected, imageLoader,\n                getMessageClickListener(wrapper),\n                getMessageLongClickListener(wrapper),\n                getMessageFocusChangeListener(wrapper),\n                dateHeadersFormatter,\n                viewClickListenersArray);\n\n        // Default focus on top message\n        if (wrapper.item == focusedMessage && holder.itemView.isFocusable()) {\n            holder.itemView.requestFocus();\n            focusedMessage = null;\n        }\n    }\n\n    @Override\n    public int getItemCount() {\n        return items.size();\n    }\n\n    @Override\n    public int getItemViewType(int position) {\n        return holders.getViewType(items.get(position).item, senderId);\n    }\n\n    @Override\n    public void onLoadMore(int page, int total) {\n        if (loadMoreListener != null) {\n            loadMoreListener.onLoadMore(page, total);\n        }\n    }\n\n    @Override\n    public int getMessagesCount() {\n        int count = 0;\n        for (Wrapper item : items) {\n            if (item.item instanceof IMessage) {\n                count++;\n            }\n        }\n        return count;\n    }\n\n    /*\n     * PUBLIC METHODS\n     * */\n\n    /**\n     * Adds message to bottom of list and scroll if needed.\n     *\n     * @param message message to add.\n     * @param scroll  {@code true} if need to scroll list to bottom when message added.\n     */\n    public void addToStart(MESSAGE message, boolean scroll) {\n        if (!IMessage.checkMessage(message)) {\n            return;\n        }\n\n        removeLoadingMessageIfNeeded();\n\n        boolean isNewMessageToday = isDateHeaderEnabled && !isPreviousSameDate(0, message.getCreatedAt());\n        if (isNewMessageToday) {\n            items.add(0, new Wrapper<>(message.getCreatedAt()));\n        }\n        Wrapper<MESSAGE> element = new Wrapper<>(message);\n        items.add(0, element);\n        notifyItemRangeInserted(0, isNewMessageToday ? 2 : 1);\n        if (layoutManager != null && scroll) {\n            layoutManager.scrollToPosition(0);\n        }\n\n        trimEnd();\n    }\n\n    /**\n     * Adds messages list in chronological order. Use this method to add history.\n     *\n     * @param messages messages from history.\n     * @param reverse  {@code true} if need to reverse messages before adding.\n     */\n    public void addToEnd(List<MESSAGE> messages, boolean reverse) {\n        if (messages == null || messages.isEmpty()) return;\n\n        if (reverse) Collections.reverse(messages);\n\n        removeLoadingMessageIfNeeded();\n\n        if (!items.isEmpty()) {\n            int lastItemPosition = items.size() - 1;\n            if (items.get(lastItemPosition).item instanceof Date) {\n                Date lastItem = (Date) items.get(lastItemPosition).item;\n                if (DateFormatter.isSameDay(messages.get(0).getCreatedAt(), lastItem)) {\n                    items.remove(lastItemPosition);\n                    notifyItemRemoved(lastItemPosition);\n                }\n            }\n        }\n\n        int oldSize = items.size();\n        generateDateHeaders(messages);\n        notifyItemRangeInserted(oldSize, items.size() - oldSize);\n    }\n\n    /**\n     * Updates message by its id.\n     *\n     * @param message updated message object.\n     */\n    public boolean update(MESSAGE message) {\n        return update(message.getId(), message);\n    }\n\n    /**\n     * Updates message by old identifier (use this method if id has changed). Otherwise use {@link #update(IMessage)}\n     *\n     * @param oldId      an identifier of message to update.\n     * @param newMessage new message object.\n     */\n    public boolean update(String oldId, MESSAGE newMessage) {\n        int position = getMessagePositionById(oldId);\n        if (position >= 0) {\n            Wrapper<MESSAGE> element = new Wrapper<>(newMessage);\n            items.set(position, element);\n            notifyItemChanged(position);\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    /**\n     * Moves the elements position from current to start\n     *\n     * @param newMessage new message object.\n     */\n    public void updateAndMoveToStart(MESSAGE newMessage) {\n        int position = getMessagePositionById(newMessage.getId());\n        if (position >= 0) {\n            Wrapper<MESSAGE> element = new Wrapper<>(newMessage);\n            items.remove(position);\n            items.add(0, element);\n            notifyItemMoved(position, 0);\n            notifyItemChanged(0);\n        }\n    }\n\n    /**\n     * Updates message by its id if it exists, add to start if not\n     *\n     * @param message message object to insert or update.\n     */\n    public void upsert(MESSAGE message) {\n        if (!update(message)) {\n            addToStart(message, false);\n        }\n    }\n\n    /**\n     * Updates and moves to start if message by its id exists and if specified move to start, if not\n     * specified the item stays at current position and updated\n     *\n     * @param message message object to insert or update.\n     */\n    public void upsert(MESSAGE message, boolean moveToStartIfUpdate) {\n        if (moveToStartIfUpdate) {\n            if (getMessagePositionById(message.getId()) > 0) {\n                updateAndMoveToStart(message);\n            } else {\n                upsert(message);\n            }\n        } else {\n            upsert(message);\n        }\n    }\n\n    /**\n     * Deletes message.\n     *\n     * @param message message to delete.\n     */\n    public void delete(MESSAGE message) {\n        deleteById(message.getId());\n    }\n\n    /**\n     * Deletes messages list.\n     *\n     * @param messages messages list to delete.\n     */\n    public void delete(List<MESSAGE> messages) {\n        boolean result = false;\n        for (MESSAGE message : messages) {\n            int index = getMessagePositionById(message.getId());\n            if (index >= 0) {\n                items.remove(index);\n                notifyItemRemoved(index);\n                result = true;\n            }\n        }\n        if (result) {\n            recountDateHeaders();\n        }\n    }\n\n    /**\n     * Deletes message by its identifier.\n     *\n     * @param id identifier of message to delete.\n     */\n    public void deleteById(String id) {\n        int index = getMessagePositionById(id);\n        if (index >= 0) {\n            items.remove(index);\n            notifyItemRemoved(index);\n            recountDateHeaders();\n        }\n    }\n\n    /**\n     * Deletes messages by its identifiers.\n     *\n     * @param ids array of identifiers of messages to delete.\n     */\n    public void deleteByIds(String[] ids) {\n        boolean result = false;\n        for (String id : ids) {\n            int index = getMessagePositionById(id);\n            if (index >= 0) {\n                items.remove(index);\n                notifyItemRemoved(index);\n                result = true;\n            }\n        }\n        if (result) {\n            recountDateHeaders();\n        }\n    }\n\n    public void setMaxItemsCount(int maxItemsCount) {\n        this.maxItemsCount = maxItemsCount;\n    }\n\n    public void setFocusedMessage(MESSAGE message) {\n        this.focusedMessage = message;\n    }\n\n    /**\n     * Returns {@code true} if, and only if, messages count in adapter is non-zero.\n     *\n     * @return {@code true} if size is 0, otherwise {@code false}\n     */\n    public boolean isEmpty() {\n        return items.isEmpty();\n    }\n\n    /**\n     * Clears the messages list. With notifyDataSetChanged\n     */\n    public void clear() {\n        clear(true);\n    }\n\n    /**\n     * Clears the messages list.\n     */\n    public void clear(boolean notifyDataSetChanged) {\n        if (items != null) {\n            items.clear();\n            if (notifyDataSetChanged) {\n                notifyDataSetChanged();\n            }\n        }\n    }\n\n    public void scrollToPosition(int position) {\n        if (position != -1 && layoutManager != null && !items.isEmpty()) {\n            layoutManager.scrollToPosition(position);\n        }\n    }\n\n    public void scrollToTop() {\n        if (layoutManager != null && !items.isEmpty()) {\n            layoutManager.scrollToPosition(items.size() - 1);\n        }\n    }\n\n    public void scrollToBottom() {\n        if (layoutManager != null) {\n            layoutManager.scrollToPosition(0);\n        }\n    }\n\n    /**\n     * Enables selection mode.\n     *\n     * @param selectionListener listener for selected items count. To get selected messages use {@link #getSelectedMessages()}.\n     */\n    public void enableSelectionMode(SelectionListener selectionListener) {\n        if (selectionListener == null) {\n            throw new IllegalArgumentException(\"SelectionListener must not be null. Use `disableSelectionMode()` if you want tp disable selection mode\");\n        } else {\n            this.selectionListener = selectionListener;\n        }\n    }\n\n    /**\n     * Disables selection mode and removes {@link SelectionListener}.\n     */\n    public void disableSelectionMode() {\n        this.selectionListener = null;\n        unselectAllItems();\n    }\n\n    public void enableDateHeader(boolean enable) {\n        isDateHeaderEnabled = enable;\n    }\n\n    public void enableStackFromEnd(boolean enable) {\n        // The solution for aligning items to the top instead of bottom\n        // https://stackoverflow.com/questions/46168245/recyclerview-reverse-order\n        ((LinearLayoutManager) layoutManager).setStackFromEnd(enable);\n    }\n\n    public void setLoadingMessage(String message) {\n        removeLoadingMessageIfNeeded();\n\n        if (message == null || !items.isEmpty()) {\n            return;\n        }\n\n        items.add(new Wrapper<>(message));\n        notifyItemInserted(0);\n    }\n\n    private void removeLoadingMessageIfNeeded() {\n        if (items.size() == 1 && items.get(0).item instanceof String) {\n            items.remove(0);\n            notifyItemRemoved(0);\n        }\n    }\n\n    /**\n     * Returns the list of selected messages.\n     *\n     * @return list of selected messages. Empty list if nothing was selected or selection mode is disabled.\n     */\n    @SuppressWarnings(\"unchecked\")\n    public ArrayList<MESSAGE> getSelectedMessages() {\n        ArrayList<MESSAGE> selectedMessages = new ArrayList<>();\n        for (Wrapper wrapper : items) {\n            if (wrapper.item instanceof IMessage && wrapper.isSelected) {\n                selectedMessages.add((MESSAGE) wrapper.item);\n            }\n        }\n        return selectedMessages;\n    }\n\n    public ArrayList<MESSAGE> getMessages() {\n        ArrayList<MESSAGE> messages = new ArrayList<>();\n        for (Wrapper wrapper : items) {\n            if (wrapper.item instanceof IMessage) {\n                messages.add((MESSAGE) wrapper.item);\n            }\n        }\n        return messages;\n    }\n\n    /**\n     * Returns selected messages text and do {@link #unselectAllItems()} for you.\n     *\n     * @param formatter The formatter that allows you to format your message model when copying.\n     * @param reverse   Change ordering when copying messages.\n     * @return formatted text by {@link Formatter}. If it's {@code null} - {@code MESSAGE#toString()} will be used.\n     */\n    public String getSelectedMessagesText(Formatter<MESSAGE> formatter, boolean reverse) {\n        String copiedText = getSelectedText(formatter, reverse);\n        unselectAllItems();\n        return copiedText;\n    }\n\n    /**\n     * Copies text to device clipboard and returns selected messages text. Also it does {@link #unselectAllItems()} for you.\n     *\n     * @param context   The context.\n     * @param formatter The formatter that allows you to format your message model when copying.\n     * @param reverse   Change ordering when copying messages.\n     * @return formatted text by {@link Formatter}. If it's {@code null} - {@code MESSAGE#toString()} will be used.\n     */\n    public String copySelectedMessagesText(Context context, Formatter<MESSAGE> formatter, boolean reverse) {\n        String copiedText = getSelectedText(formatter, reverse);\n        copyToClipboard(context, copiedText);\n        unselectAllItems();\n        return copiedText;\n    }\n\n    /**\n     * Unselect all of the selected messages. Notifies {@link SelectionListener} with zero count.\n     */\n    public void unselectAllItems() {\n        for (int i = 0; i < items.size(); i++) {\n            Wrapper wrapper = items.get(i);\n            if (wrapper.isSelected) {\n                wrapper.isSelected = false;\n                notifyItemChanged(i);\n            }\n        }\n        isSelectionModeEnabled = false;\n        selectedItemsCount = 0;\n        notifySelectionChanged();\n    }\n\n    /**\n     * Deletes all of the selected messages and disables selection mode.\n     * Call {@link #getSelectedMessages()} before calling this method to delete messages from your data source.\n     */\n    public void deleteSelectedMessages() {\n        List<MESSAGE> selectedMessages = getSelectedMessages();\n        delete(selectedMessages);\n        unselectAllItems();\n    }\n\n    /**\n     * Sets click listener for item. Fires ONLY if list is not in selection mode.\n     *\n     * @param onMessageClickListener click listener.\n     */\n    public void setOnMessageClickListener(OnMessageClickListener<MESSAGE> onMessageClickListener) {\n        this.onMessageClickListener = onMessageClickListener;\n    }\n\n    /**\n     * Sets click listener for message view. Fires ONLY if list is not in selection mode.\n     *\n     * @param onMessageViewClickListener click listener.\n     */\n    public void setOnMessageViewClickListener(OnMessageViewClickListener<MESSAGE> onMessageViewClickListener) {\n        this.onMessageViewClickListener = onMessageViewClickListener;\n    }\n\n    /**\n     * Registers click listener for view by id\n     *\n     * @param viewId                     view\n     * @param onMessageViewClickListener click listener.\n     */\n    public void registerViewClickListener(int viewId, OnMessageViewClickListener<MESSAGE> onMessageViewClickListener) {\n        this.viewClickListenersArray.append(viewId, onMessageViewClickListener);\n    }\n\n    /**\n     * Sets long click listener for item. Fires only if selection mode is disabled.\n     *\n     * @param onMessageLongClickListener long click listener.\n     */\n    public void setOnMessageLongClickListener(OnMessageLongClickListener<MESSAGE> onMessageLongClickListener) {\n        this.onMessageLongClickListener = onMessageLongClickListener;\n    }\n\n    /**\n     * Sets long click listener for message view. Fires ONLY if selection mode is disabled.\n     *\n     * @param onMessageViewLongClickListener long click listener.\n     */\n    public void setOnMessageViewLongClickListener(OnMessageViewLongClickListener<MESSAGE> onMessageViewLongClickListener) {\n        this.onMessageViewLongClickListener = onMessageViewLongClickListener;\n    }\n\n    public void setOnMessageViewFocusListener(OnMessageViewFocusListener<MESSAGE> onMessageViewFocusListener) {\n        this.onMessageViewFocusListener = onMessageViewFocusListener;\n    }\n\n    /**\n     * Set callback to be invoked when list scrolled to top.\n     *\n     * @param loadMoreListener listener.\n     */\n    public void setLoadMoreListener(OnLoadMoreListener loadMoreListener) {\n        this.loadMoreListener = loadMoreListener;\n    }\n\n    /**\n     * Sets custom {@link DateFormatter.Formatter} for text representation of date headers.\n     */\n    public void setDateHeadersFormatter(DateFormatter.Formatter dateHeadersFormatter) {\n        this.dateHeadersFormatter = dateHeadersFormatter;\n    }\n\n    /*\n     * PRIVATE METHODS\n     * */\n    private void recountDateHeaders() {\n        List<Integer> indicesToDelete = new ArrayList<>();\n\n        for (int i = 0; i < items.size(); i++) {\n            Wrapper wrapper = items.get(i);\n            if (wrapper.item instanceof Date) {\n                if (i == 0) {\n                    indicesToDelete.add(i);\n                } else {\n                    if (items.get(i - 1).item instanceof Date) {\n                        indicesToDelete.add(i);\n                    }\n                }\n            }\n        }\n\n        Collections.reverse(indicesToDelete);\n        for (int i : indicesToDelete) {\n            items.remove(i);\n            notifyItemRemoved(i);\n        }\n    }\n\n    protected void generateDateHeaders(List<MESSAGE> messages) {\n        for (int i = 0; i < messages.size(); i++) {\n            MESSAGE message = messages.get(i);\n            this.items.add(new Wrapper<>(message));\n\n            if (!isDateHeaderEnabled) {\n                continue;\n            }\n\n            if (messages.size() > i + 1) {\n                MESSAGE nextMessage = messages.get(i + 1);\n                if (!DateFormatter.isSameDay(message.getCreatedAt(), nextMessage.getCreatedAt())) {\n                    this.items.add(new Wrapper<>(message.getCreatedAt()));\n                }\n            } else {\n                this.items.add(new Wrapper<>(message.getCreatedAt()));\n            }\n        }\n    }\n\n    public int getMessagePosition(MESSAGE message) {\n        if (message == null) {\n            return -1;\n        }\n\n        return getMessagePositionById(message.getId());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private int getMessagePositionById(String id) {\n        for (int i = 0; i < items.size(); i++) {\n            Wrapper wrapper = items.get(i);\n            if (wrapper.item instanceof IMessage) {\n                MESSAGE message = (MESSAGE) wrapper.item;\n                if (message.getId().contentEquals(id)) {\n                    return i;\n                }\n            }\n        }\n        return -1;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private boolean isPreviousSameDate(int position, Date dateToCompare) {\n        if (items.size() <= position) return false;\n        if (items.get(position).item instanceof IMessage) {\n            Date previousPositionDate = ((MESSAGE) items.get(position).item).getCreatedAt();\n            return DateFormatter.isSameDay(dateToCompare, previousPositionDate);\n        } else return false;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private boolean isPreviousSameAuthor(String id, int position) {\n        int prevPosition = position + 1;\n        if (items.size() <= prevPosition) return false;\n        else return items.get(prevPosition).item instanceof IMessage\n                && ((MESSAGE) items.get(prevPosition).item).getUser().getId().contentEquals(id);\n    }\n\n    private void incrementSelectedItemsCount() {\n        selectedItemsCount++;\n        notifySelectionChanged();\n    }\n\n    private void decrementSelectedItemsCount() {\n        selectedItemsCount--;\n        isSelectionModeEnabled = selectedItemsCount > 0;\n\n        notifySelectionChanged();\n    }\n\n    private void notifySelectionChanged() {\n        if (selectionListener != null) {\n            selectionListener.onSelectionChanged(selectedItemsCount);\n        }\n    }\n\n    private void notifyMessageClicked(MESSAGE message) {\n        if (onMessageClickListener != null) {\n            onMessageClickListener.onMessageClick(message);\n        }\n    }\n\n    private void notifyMessageViewClicked(View view, MESSAGE message) {\n        if (onMessageViewClickListener != null) {\n            onMessageViewClickListener.onMessageViewClick(view, message);\n        }\n    }\n\n    private void notifyMessageLongClicked(MESSAGE message) {\n        if (onMessageLongClickListener != null) {\n            onMessageLongClickListener.onMessageLongClick(message);\n        }\n    }\n\n    private void notifyMessageViewLongClicked(View view, MESSAGE message) {\n        if (onMessageViewLongClickListener != null) {\n            onMessageViewLongClickListener.onMessageViewLongClick(view, message);\n        }\n    }\n\n    private void notifyMessageViewFocused(View view, MESSAGE message) {\n        if (onMessageViewFocusListener != null) {\n            onMessageViewFocusListener.onMessageViewFocus(view, message);\n        }\n    }\n\n    private View.OnClickListener getMessageClickListener(final Wrapper<MESSAGE> wrapper) {\n        return new DebouncedOnClickListener(3_000) {\n            @Override\n            public void onDebouncedClick(View view) {\n                if (selectionListener != null && isSelectionModeEnabled) {\n                    wrapper.isSelected = !wrapper.isSelected;\n\n                    if (wrapper.isSelected) incrementSelectedItemsCount();\n                    else decrementSelectedItemsCount();\n\n                    MESSAGE message = (wrapper.item);\n                    notifyItemChanged(getMessagePositionById(message.getId()));\n                } else {\n                    notifyMessageClicked(wrapper.item);\n                    notifyMessageViewClicked(view, wrapper.item);\n                }\n            }\n        };\n    }\n\n    private View.OnLongClickListener getMessageLongClickListener(final Wrapper<MESSAGE> wrapper) {\n        return view -> {\n            if (selectionListener == null) {\n                notifyMessageLongClicked(wrapper.item);\n                notifyMessageViewLongClicked(view, wrapper.item);\n            } else {\n                isSelectionModeEnabled = true;\n                view.performClick();\n            }\n            return true;\n        };\n    }\n\n    private View.OnFocusChangeListener getMessageFocusChangeListener(final Wrapper<MESSAGE> wrapper) {\n        return (v, hasFocus) -> {\n            // Change background of the focused message\n            // NOTE: you can have only one focus listener\n            View bubble = v.findViewById(R.id.bubble);\n            //bubble.setBackgroundResource(hasFocus ? R.drawable.shape_incoming_message_focused : R.drawable.shape_incoming_message);\n\n            // Save the current padding (API 19 fix)\n            int paddingLeft = bubble.getPaddingLeft();\n            int paddingTop = bubble.getPaddingTop();\n            int paddingRight = bubble.getPaddingRight();\n            int paddingBottom = bubble.getPaddingBottom();\n\n            if (hasFocus) {\n                // Invert text and bg color\n                Drawable originalBackground = messagesListStyle.getIncomingBubbleDrawable();\n                Drawable shapeBackground = messagesListStyle.getIncomingBubbleSelectedDrawable();\n                bubble.setBackground(new LayerDrawable(new Drawable[]{originalBackground, shapeBackground}));\n            } else {\n                // Revert to original\n                Drawable originalBackground = messagesListStyle.getIncomingBubbleDrawable();\n                bubble.setBackground(originalBackground);\n            }\n\n            // Restore the padding (API 19 fix)\n            bubble.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);\n\n            if (hasFocus) {\n                notifyMessageViewFocused(v, wrapper.item);\n            }\n        };\n    }\n\n    private String getSelectedText(Formatter<MESSAGE> formatter, boolean reverse) {\n        StringBuilder builder = new StringBuilder();\n\n        ArrayList<MESSAGE> selectedMessages = getSelectedMessages();\n        if (reverse) Collections.reverse(selectedMessages);\n\n        for (MESSAGE message : selectedMessages) {\n            builder.append(formatter == null\n                    ? message.toString()\n                    : formatter.format(message));\n            builder.append(\"\\n\\n\");\n        }\n        builder.replace(builder.length() - 2, builder.length(), \"\");\n\n        return builder.toString();\n    }\n\n    private void copyToClipboard(Context context, String copiedText) {\n        ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);\n        ClipData clip = ClipData.newPlainText(copiedText, copiedText);\n        clipboard.setPrimaryClip(clip);\n    }\n\n    private void trimEnd() {\n        if (maxItemsCount > 0) {\n            int messagesCount = getMessagesCount();\n            int leftoversCount = messagesCount - maxItemsCount;\n            if (leftoversCount > 0) {\n                int size = items.size();\n                int firstIndex = size - leftoversCount;\n                int lastIndex = size;\n\n                items.subList(firstIndex, lastIndex).clear();\n                notifyItemRangeRemoved(firstIndex, leftoversCount);\n                recountDateHeaders();\n            }\n        }\n    }\n\n    void setLayoutManager(RecyclerView.LayoutManager layoutManager) {\n        this.layoutManager = layoutManager;\n    }\n\n    void setStyle(MessagesListStyle style) {\n        this.messagesListStyle = style;\n    }\n\n    /*\n     * WRAPPER\n     * */\n    public static class Wrapper<DATA> {\n        public DATA item;\n        public boolean isSelected;\n\n        Wrapper(DATA item) {\n            this.item = item;\n        }\n    }\n\n    /*\n     * LISTENERS\n     * */\n\n    /**\n     * Interface definition for a callback to be invoked when next part of messages need to be loaded.\n     */\n    public interface OnLoadMoreListener {\n\n        /**\n         * Fires when user scrolled to the end of list.\n         *\n         * @param page            next page to download.\n         * @param totalItemsCount current items count.\n         */\n        void onLoadMore(int page, int totalItemsCount);\n    }\n\n    /**\n     * Interface definition for a callback to be invoked when selected messages count is changed.\n     */\n    public interface SelectionListener {\n\n        /**\n         * Fires when selected items count is changed.\n         *\n         * @param count count of selected items.\n         */\n        void onSelectionChanged(int count);\n    }\n\n    /**\n     * Interface definition for a callback to be invoked when message item is clicked.\n     */\n    public interface OnMessageClickListener<MESSAGE extends IMessage> {\n\n        /**\n         * Fires when message is clicked.\n         *\n         * @param message clicked message.\n         */\n        void onMessageClick(MESSAGE message);\n    }\n\n    /**\n     * Interface definition for a callback to be invoked when message view is clicked.\n     */\n    public interface OnMessageViewClickListener<MESSAGE extends IMessage> {\n\n        /**\n         * Fires when message view is clicked.\n         *\n         * @param message clicked message.\n         */\n        void onMessageViewClick(View view, MESSAGE message);\n    }\n\n    public interface OnMessageViewFocusListener<MESSAGE extends IMessage> {\n        void onMessageViewFocus(View view, MESSAGE message);\n    }\n\n    /**\n     * Interface definition for a callback to be invoked when message item is long clicked.\n     */\n    public interface OnMessageLongClickListener<MESSAGE extends IMessage> {\n\n        /**\n         * Fires when message is long clicked.\n         *\n         * @param message clicked message.\n         */\n        void onMessageLongClick(MESSAGE message);\n    }\n\n    /**\n     * Interface definition for a callback to be invoked when message view is long clicked.\n     */\n    public interface OnMessageViewLongClickListener<MESSAGE extends IMessage> {\n\n        /**\n         * Fires when message view is long clicked.\n         *\n         * @param message clicked message.\n         */\n        void onMessageViewLongClick(View view, MESSAGE message);\n    }\n\n    /**\n     * Interface used to format your message model when copying.\n     */\n    public interface Formatter<MESSAGE> {\n\n        /**\n         * Formats an string representation of the message object.\n         *\n         * @param message The object that should be formatted.\n         * @return Formatted text.\n         */\n        String format(MESSAGE message);\n    }\n\n    /**\n     * This class is deprecated. Use {@link MessageHolders} instead.\n     */\n    @Deprecated\n    public static class HoldersConfig extends MessageHolders {\n\n        /**\n         * This method is deprecated. Use {@link MessageHolders#setIncomingTextConfig(Class, int)} instead.\n         *\n         * @param holder holder class.\n         * @param layout layout resource.\n         */\n        @Deprecated\n        public void setIncoming(Class<? extends BaseMessageViewHolder<? extends IMessage>> holder, @LayoutRes int layout) {\n            super.setIncomingTextConfig(holder, layout);\n        }\n\n        /**\n         * This method is deprecated. Use {@link MessageHolders#setIncomingTextHolder(Class)} instead.\n         *\n         * @param holder holder class.\n         */\n        @Deprecated\n        public void setIncomingHolder(Class<? extends BaseMessageViewHolder<? extends IMessage>> holder) {\n            super.setIncomingTextHolder(holder);\n        }\n\n        /**\n         * This method is deprecated. Use {@link MessageHolders#setIncomingTextLayout(int)} instead.\n         *\n         * @param layout layout resource.\n         */\n        @Deprecated\n        public void setIncomingLayout(@LayoutRes int layout) {\n            super.setIncomingTextLayout(layout);\n        }\n\n        /**\n         * This method is deprecated. Use {@link MessageHolders#setOutcomingTextConfig(Class, int)} instead.\n         *\n         * @param holder holder class.\n         * @param layout layout resource.\n         */\n        @Deprecated\n        public void setOutcoming(Class<? extends BaseMessageViewHolder<? extends IMessage>> holder, @LayoutRes int layout) {\n            super.setOutcomingTextConfig(holder, layout);\n        }\n\n        /**\n         * This method is deprecated. Use {@link MessageHolders#setOutcomingTextHolder(Class)} instead.\n         *\n         * @param holder holder class.\n         */\n        @Deprecated\n        public void setOutcomingHolder(Class<? extends BaseMessageViewHolder<? extends IMessage>> holder) {\n            super.setOutcomingTextHolder(holder);\n        }\n\n        /**\n         * This method is deprecated. Use {@link MessageHolders#setOutcomingTextLayout(int)} instead.\n         *\n         * @param layout layout resource.\n         */\n        @Deprecated\n        public void setOutcomingLayout(@LayoutRes int layout) {\n            this.setOutcomingTextLayout(layout);\n        }\n\n        /**\n         * This method is deprecated. Use {@link MessageHolders#setDateHeaderConfig(Class, int)} instead.\n         *\n         * @param holder holder class.\n         * @param layout layout resource.\n         */\n        @Deprecated\n        public void setDateHeader(Class<? extends ViewHolder<Date>> holder, @LayoutRes int layout) {\n            super.setDateHeaderConfig(holder, layout);\n        }\n    }\n\n    /**\n     * This class is deprecated. Use {@link MessageHolders.BaseMessageViewHolder} instead.\n     */\n    @Deprecated\n    public static abstract class BaseMessageViewHolder<MESSAGE extends IMessage>\n            extends MessageHolders.BaseMessageViewHolder<MESSAGE> {\n\n        private boolean isSelected;\n\n        /**\n         * Callback for implementing images loading in message list\n         */\n        protected ImageLoader imageLoader;\n\n        public BaseMessageViewHolder(View itemView) {\n            super(itemView);\n        }\n\n        /**\n         * Returns whether is item selected\n         *\n         * @return weather is item selected.\n         */\n        public boolean isSelected() {\n            return isSelected;\n        }\n\n        /**\n         * Returns weather is selection mode enabled\n         *\n         * @return weather is selection mode enabled.\n         */\n        public boolean isSelectionModeEnabled() {\n            return isSelectionModeEnabled;\n        }\n\n        /**\n         * Getter for {@link #imageLoader}\n         *\n         * @return image loader interface.\n         */\n        public ImageLoader getImageLoader() {\n            return imageLoader;\n        }\n\n        protected void configureLinksBehavior(final TextView text) {\n            text.setLinksClickable(false);\n            text.setMovementMethod(new LinkMovementMethod() {\n                @Override\n                public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {\n                    boolean result = false;\n                    if (!isSelectionModeEnabled) {\n                        result = super.onTouchEvent(widget, buffer, event);\n                    }\n                    itemView.onTouchEvent(event);\n                    return result;\n                }\n            });\n        }\n\n    }\n\n    /**\n     * This class is deprecated. Use {@link MessageHolders.DefaultDateHeaderViewHolder} instead.\n     */\n    @Deprecated\n    public static class DefaultDateHeaderViewHolder extends ViewHolder<Date>\n            implements MessageHolders.DefaultMessageViewHolder {\n\n        protected TextView text;\n        protected String dateFormat;\n        protected DateFormatter.Formatter dateHeadersFormatter;\n\n        public DefaultDateHeaderViewHolder(View itemView) {\n            super(itemView);\n            text = itemView.findViewById(R.id.messageText);\n        }\n\n        @Override\n        public void onBind(Date date) {\n            if (text != null) {\n                String formattedDate = null;\n                if (dateHeadersFormatter != null) formattedDate = dateHeadersFormatter.format(date);\n                text.setText(formattedDate == null ? DateFormatter.format(date, dateFormat) : formattedDate);\n            }\n        }\n\n        @SuppressLint(\"WrongConstant\")\n        @Override\n        public void applyStyle(MessagesListStyle style) {\n            if (text != null) {\n                text.setTextColor(style.getDateHeaderTextColor());\n                text.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.getDateHeaderTextSize());\n                text.setTypeface(text.getTypeface(), style.getDateHeaderTextStyle());\n                text.setPadding(style.getDateHeaderPadding(), style.getDateHeaderPadding(),\n                        style.getDateHeaderPadding(), style.getDateHeaderPadding());\n            }\n            dateFormat = style.getDateHeaderFormat();\n            dateFormat = dateFormat == null ? DateFormatter.Template.STRING_DAY_MONTH_YEAR.get() : dateFormat;\n        }\n    }\n\n    /**\n     * This class is deprecated. Use {@link MessageHolders.IncomingTextMessageViewHolder} instead.\n     */\n    @Deprecated\n    public static class IncomingMessageViewHolder<MESSAGE extends IMessage>\n            extends MessageHolders.IncomingTextMessageViewHolder<MESSAGE>\n            implements MessageHolders.DefaultMessageViewHolder {\n\n        public IncomingMessageViewHolder(View itemView) {\n            super(itemView);\n        }\n    }\n\n    /**\n     * This class is deprecated. Use {@link MessageHolders.OutcomingTextMessageViewHolder} instead.\n     */\n    @Deprecated\n    public static class OutcomingMessageViewHolder<MESSAGE extends IMessage>\n            extends MessageHolders.OutcomingTextMessageViewHolder<MESSAGE> {\n\n        public OutcomingMessageViewHolder(View itemView) {\n            super(itemView);\n        }\n    }\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/messages/MessagesListStyle.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.messages;\n\nimport android.content.Context;\nimport android.content.res.ColorStateList;\nimport android.content.res.TypedArray;\nimport android.graphics.Color;\nimport android.graphics.Typeface;\nimport android.graphics.drawable.Drawable;\nimport android.util.AttributeSet;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.DrawableRes;\nimport androidx.core.graphics.drawable.DrawableCompat;\n\nimport com.stfalcon.chatkit.R;\nimport com.stfalcon.chatkit.commons.Style;\n\n/**\n * Style for MessagesListStyle customization by xml attributes\n */\n@SuppressWarnings(\"WeakerAccess\")\nclass MessagesListStyle extends Style {\n\n    private int textAutoLinkMask;\n    private int incomingTextLinkColor;\n    private int outcomingTextLinkColor;\n\n    private int incomingAvatarWidth;\n    private int incomingAvatarHeight;\n\n    private int incomingBubbleDrawable;\n    private int incomingDefaultBubbleColor;\n    private int incomingDefaultBubblePressedColor;\n    private int incomingDefaultBubbleSelectedColor;\n\n    private int incomingImageOverlayDrawable;\n    private int incomingDefaultImageOverlayPressedColor;\n    private int incomingDefaultImageOverlaySelectedColor;\n\n    private int incomingDefaultBubblePaddingLeft;\n    private int incomingDefaultBubblePaddingRight;\n    private int incomingDefaultBubblePaddingTop;\n    private int incomingDefaultBubblePaddingBottom;\n\n    private int incomingTextColor;\n    private int incomingTextSize;\n    private int incomingTextStyle;\n\n    private boolean showIncomingTime;\n    private int incomingTimeTextColor;\n    private int incomingTimeTextSize;\n    private int incomingTimeTextStyle;\n\n    private int incomingImageTimeTextColor;\n    private int incomingImageTimeTextSize;\n    private int incomingImageTimeTextStyle;\n\n    private int outcomingBubbleDrawable;\n    private int outcomingDefaultBubbleColor;\n    private int outcomingDefaultBubblePressedColor;\n    private int outcomingDefaultBubbleSelectedColor;\n\n    private int outcomingImageOverlayDrawable;\n    private int outcomingDefaultImageOverlayPressedColor;\n    private int outcomingDefaultImageOverlaySelectedColor;\n\n    private int outcomingDefaultBubblePaddingLeft;\n    private int outcomingDefaultBubblePaddingRight;\n    private int outcomingDefaultBubblePaddingTop;\n    private int outcomingDefaultBubblePaddingBottom;\n\n    private int outcomingTextColor;\n    private int outcomingTextSize;\n    private int outcomingTextStyle;\n\n    private boolean showOutcomingTime;\n    private int outcomingTimeTextColor;\n    private int outcomingTimeTextSize;\n    private int outcomingTimeTextStyle;\n\n    private int outcomingImageTimeTextColor;\n    private int outcomingImageTimeTextSize;\n    private int outcomingImageTimeTextStyle;\n\n    private int dateHeaderPadding;\n    private String dateHeaderFormat;\n    private int dateHeaderTextColor;\n    private int dateHeaderTextSize;\n    private int dateHeaderTextStyle;\n\n    private boolean isMessageFocusable;\n\n    static MessagesListStyle parse(Context context, AttributeSet attrs) {\n        MessagesListStyle style = new MessagesListStyle(context, attrs);\n        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MessagesList);\n\n        style.textAutoLinkMask = typedArray.getInt(R.styleable.MessagesList_textAutoLink, 0);\n\n        style.incomingTextLinkColor = typedArray.getColor(R.styleable.MessagesList_incomingTextLinkColor,\n                style.getSystemAccentColor());\n        style.outcomingTextLinkColor = typedArray.getColor(R.styleable.MessagesList_outcomingTextLinkColor,\n                style.getSystemAccentColor());\n\n        style.incomingAvatarWidth = typedArray.getDimensionPixelSize(R.styleable.MessagesList_incomingAvatarWidth,\n                style.getDimension(R.dimen.message_avatar_width));\n        style.incomingAvatarHeight = typedArray.getDimensionPixelSize(R.styleable.MessagesList_incomingAvatarHeight,\n                style.getDimension(R.dimen.message_avatar_height));\n\n        style.incomingBubbleDrawable = typedArray.getResourceId(R.styleable.MessagesList_incomingBubbleDrawable, -1);\n        style.incomingDefaultBubbleColor = typedArray.getColor(R.styleable.MessagesList_incomingDefaultBubbleColor,\n                style.getColor(R.color.white_two));\n        style.incomingDefaultBubblePressedColor = typedArray.getColor(R.styleable.MessagesList_incomingDefaultBubblePressedColor,\n                style.getColor(R.color.white_two));\n        style.incomingDefaultBubbleSelectedColor = typedArray.getColor(R.styleable.MessagesList_incomingDefaultBubbleSelectedColor,\n                style.getColor(R.color.cornflower_blue_two_24));\n\n        style.incomingImageOverlayDrawable = typedArray.getResourceId(R.styleable.MessagesList_incomingImageOverlayDrawable, -1);\n        style.incomingDefaultImageOverlayPressedColor = typedArray.getColor(R.styleable.MessagesList_incomingDefaultImageOverlayPressedColor,\n                style.getColor(R.color.transparent));\n        style.incomingDefaultImageOverlaySelectedColor = typedArray.getColor(R.styleable.MessagesList_incomingDefaultImageOverlaySelectedColor,\n                style.getColor(R.color.cornflower_blue_light_40));\n\n        style.incomingDefaultBubblePaddingLeft = typedArray.getDimensionPixelSize(R.styleable.MessagesList_incomingBubblePaddingLeft,\n                style.getDimension(R.dimen.message_padding_left));\n        style.incomingDefaultBubblePaddingRight = typedArray.getDimensionPixelSize(R.styleable.MessagesList_incomingBubblePaddingRight,\n                style.getDimension(R.dimen.message_padding_right));\n        style.incomingDefaultBubblePaddingTop = typedArray.getDimensionPixelSize(R.styleable.MessagesList_incomingBubblePaddingTop,\n                style.getDimension(R.dimen.message_padding_top));\n        style.incomingDefaultBubblePaddingBottom = typedArray.getDimensionPixelSize(R.styleable.MessagesList_incomingBubblePaddingBottom,\n                style.getDimension(R.dimen.message_padding_bottom));\n        style.incomingTextColor = typedArray.getColor(R.styleable.MessagesList_incomingTextColor,\n                style.getColor(R.color.dark_grey_two));\n        style.incomingTextSize = typedArray.getDimensionPixelSize(R.styleable.MessagesList_incomingTextSize,\n                style.getDimension(R.dimen.message_text_size));\n        style.incomingTextStyle = typedArray.getInt(R.styleable.MessagesList_incomingTextStyle, Typeface.NORMAL);\n\n        style.showIncomingTime = typedArray.getBoolean(R.styleable.MessagesList_showIncomingTime, true);\n        style.incomingTimeTextColor = typedArray.getColor(R.styleable.MessagesList_incomingTimeTextColor,\n                style.getColor(R.color.warm_grey_four));\n        style.incomingTimeTextSize = typedArray.getDimensionPixelSize(R.styleable.MessagesList_incomingTimeTextSize,\n                style.getDimension(R.dimen.message_time_text_size));\n        style.incomingTimeTextStyle = typedArray.getInt(R.styleable.MessagesList_incomingTimeTextStyle, Typeface.NORMAL);\n\n        style.incomingImageTimeTextColor = typedArray.getColor(R.styleable.MessagesList_incomingImageTimeTextColor,\n                style.getColor(R.color.warm_grey_four));\n        style.incomingImageTimeTextSize = typedArray.getDimensionPixelSize(R.styleable.MessagesList_incomingImageTimeTextSize,\n                style.getDimension(R.dimen.message_time_text_size));\n        style.incomingImageTimeTextStyle = typedArray.getInt(R.styleable.MessagesList_incomingImageTimeTextStyle, Typeface.NORMAL);\n\n        style.outcomingBubbleDrawable = typedArray.getResourceId(R.styleable.MessagesList_outcomingBubbleDrawable, -1);\n        style.outcomingDefaultBubbleColor = typedArray.getColor(R.styleable.MessagesList_outcomingDefaultBubbleColor,\n                style.getColor(R.color.cornflower_blue_two));\n        style.outcomingDefaultBubblePressedColor = typedArray.getColor(R.styleable.MessagesList_outcomingDefaultBubblePressedColor,\n                style.getColor(R.color.cornflower_blue_two));\n        style.outcomingDefaultBubbleSelectedColor = typedArray.getColor(R.styleable.MessagesList_outcomingDefaultBubbleSelectedColor,\n                style.getColor(R.color.cornflower_blue_two_24));\n\n        style.outcomingImageOverlayDrawable = typedArray.getResourceId(R.styleable.MessagesList_outcomingImageOverlayDrawable, -1);\n        style.outcomingDefaultImageOverlayPressedColor = typedArray.getColor(R.styleable.MessagesList_outcomingDefaultImageOverlayPressedColor,\n                style.getColor(R.color.transparent));\n        style.outcomingDefaultImageOverlaySelectedColor = typedArray.getColor(R.styleable.MessagesList_outcomingDefaultImageOverlaySelectedColor,\n                style.getColor(R.color.cornflower_blue_light_40));\n\n        style.outcomingDefaultBubblePaddingLeft = typedArray.getDimensionPixelSize(R.styleable.MessagesList_outcomingBubblePaddingLeft,\n                style.getDimension(R.dimen.message_padding_left));\n        style.outcomingDefaultBubblePaddingRight = typedArray.getDimensionPixelSize(R.styleable.MessagesList_outcomingBubblePaddingRight,\n                style.getDimension(R.dimen.message_padding_right));\n        style.outcomingDefaultBubblePaddingTop = typedArray.getDimensionPixelSize(R.styleable.MessagesList_outcomingBubblePaddingTop,\n                style.getDimension(R.dimen.message_padding_top));\n        style.outcomingDefaultBubblePaddingBottom = typedArray.getDimensionPixelSize(R.styleable.MessagesList_outcomingBubblePaddingBottom,\n                style.getDimension(R.dimen.message_padding_bottom));\n        style.outcomingTextColor = typedArray.getColor(R.styleable.MessagesList_outcomingTextColor,\n                style.getColor(R.color.white));\n        style.outcomingTextSize = typedArray.getDimensionPixelSize(R.styleable.MessagesList_outcomingTextSize,\n                style.getDimension(R.dimen.message_text_size));\n        style.outcomingTextStyle = typedArray.getInt(R.styleable.MessagesList_outcomingTextStyle, Typeface.NORMAL);\n\n        style.showOutcomingTime = typedArray.getBoolean(R.styleable.MessagesList_showOutcomingTime, true);\n        style.outcomingTimeTextColor = typedArray.getColor(R.styleable.MessagesList_outcomingTimeTextColor,\n                style.getColor(R.color.white60));\n        style.outcomingTimeTextSize = typedArray.getDimensionPixelSize(R.styleable.MessagesList_outcomingTimeTextSize,\n                style.getDimension(R.dimen.message_time_text_size));\n        style.outcomingTimeTextStyle = typedArray.getInt(R.styleable.MessagesList_outcomingTimeTextStyle, Typeface.NORMAL);\n\n        style.outcomingImageTimeTextColor = typedArray.getColor(R.styleable.MessagesList_outcomingImageTimeTextColor,\n                style.getColor(R.color.warm_grey_four));\n        style.outcomingImageTimeTextSize = typedArray.getDimensionPixelSize(R.styleable.MessagesList_outcomingImageTimeTextSize,\n                style.getDimension(R.dimen.message_time_text_size));\n        style.outcomingImageTimeTextStyle = typedArray.getInt(R.styleable.MessagesList_outcomingImageTimeTextStyle, Typeface.NORMAL);\n\n        style.dateHeaderPadding = typedArray.getDimensionPixelSize(R.styleable.MessagesList_dateHeaderPadding,\n                style.getDimension(R.dimen.message_date_header_padding));\n        style.dateHeaderFormat = typedArray.getString(R.styleable.MessagesList_dateHeaderFormat);\n        style.dateHeaderTextColor = typedArray.getColor(R.styleable.MessagesList_dateHeaderTextColor,\n                style.getColor(R.color.warm_grey_two));\n        style.dateHeaderTextSize = typedArray.getDimensionPixelSize(R.styleable.MessagesList_dateHeaderTextSize,\n                style.getDimension(R.dimen.message_date_header_text_size));\n        style.dateHeaderTextStyle = typedArray.getInt(R.styleable.MessagesList_dateHeaderTextStyle, Typeface.NORMAL);\n\n        style.isMessageFocusable = typedArray.getBoolean(R.styleable.MessagesList_isMessageFocusable, true);\n\n        typedArray.recycle();\n\n        return style;\n    }\n\n    private MessagesListStyle(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    private Drawable getMessageSelector(@ColorInt int normalColor, @ColorInt int selectedColor,\n                                        @ColorInt int pressedColor, @DrawableRes int shape) {\n\n        Drawable drawable = DrawableCompat.wrap(getVectorDrawable(shape)).mutate();\n        DrawableCompat.setTintList(\n                drawable,\n                new ColorStateList(\n                        new int[][]{\n                                new int[]{android.R.attr.state_selected},\n                                new int[]{android.R.attr.state_pressed},\n                                new int[]{-android.R.attr.state_pressed, -android.R.attr.state_selected}\n                        },\n                        new int[]{selectedColor, pressedColor, normalColor}\n                ));\n        return drawable;\n    }\n\n    protected int getTextAutoLinkMask() {\n        return textAutoLinkMask;\n    }\n\n    protected int getIncomingTextLinkColor() {\n        return incomingTextLinkColor;\n    }\n\n    protected int getOutcomingTextLinkColor() {\n        return outcomingTextLinkColor;\n    }\n\n    protected int getIncomingAvatarWidth() {\n        return incomingAvatarWidth;\n    }\n\n    protected int getIncomingAvatarHeight() {\n        return incomingAvatarHeight;\n    }\n\n    protected int getIncomingDefaultBubblePaddingLeft() {\n        return incomingDefaultBubblePaddingLeft;\n    }\n\n    protected int getIncomingDefaultBubblePaddingRight() {\n        return incomingDefaultBubblePaddingRight;\n    }\n\n    protected int getIncomingDefaultBubblePaddingTop() {\n        return incomingDefaultBubblePaddingTop;\n    }\n\n    protected int getIncomingDefaultBubblePaddingBottom() {\n        return incomingDefaultBubblePaddingBottom;\n    }\n\n    protected int getIncomingTextColor() {\n        return incomingTextColor;\n    }\n\n    protected int getIncomingTextSize() {\n        return incomingTextSize;\n    }\n\n    protected int getIncomingTextStyle() {\n        return incomingTextStyle;\n    }\n\n    protected Drawable getOutcomingBubbleDrawable() {\n        if (outcomingBubbleDrawable == -1) {\n            return getMessageSelector(outcomingDefaultBubbleColor, outcomingDefaultBubbleSelectedColor,\n                    outcomingDefaultBubblePressedColor, R.drawable.shape_outcoming_message);\n        } else {\n            return getDrawable(outcomingBubbleDrawable);\n        }\n    }\n\n    protected Drawable getOutcomingImageOverlayDrawable() {\n        if (outcomingImageOverlayDrawable == -1) {\n            return getMessageSelector(Color.TRANSPARENT, outcomingDefaultImageOverlaySelectedColor,\n                    outcomingDefaultImageOverlayPressedColor, R.drawable.shape_outcoming_message);\n        } else {\n            return getDrawable(outcomingImageOverlayDrawable);\n        }\n    }\n\n    protected int getOutcomingDefaultBubblePaddingLeft() {\n        return outcomingDefaultBubblePaddingLeft;\n    }\n\n    protected int getOutcomingDefaultBubblePaddingRight() {\n        return outcomingDefaultBubblePaddingRight;\n    }\n\n    protected int getOutcomingDefaultBubblePaddingTop() {\n        return outcomingDefaultBubblePaddingTop;\n    }\n\n    protected int getOutcomingDefaultBubblePaddingBottom() {\n        return outcomingDefaultBubblePaddingBottom;\n    }\n\n    protected int getOutcomingTextColor() {\n        return outcomingTextColor;\n    }\n\n    protected int getOutcomingTextSize() {\n        return outcomingTextSize;\n    }\n\n    protected int getOutcomingTextStyle() {\n        return outcomingTextStyle;\n    }\n\n    protected boolean showOutcomingTime() {\n        return showOutcomingTime;\n    }\n\n    protected int getOutcomingTimeTextColor() {\n        return outcomingTimeTextColor;\n    }\n\n    protected int getOutcomingTimeTextSize() {\n        return outcomingTimeTextSize;\n    }\n\n    protected int getOutcomingTimeTextStyle() {\n        return outcomingTimeTextStyle;\n    }\n\n    protected int getOutcomingImageTimeTextColor() {\n        return outcomingImageTimeTextColor;\n    }\n\n    protected int getOutcomingImageTimeTextSize() {\n        return outcomingImageTimeTextSize;\n    }\n\n    protected int getOutcomingImageTimeTextStyle() {\n        return outcomingImageTimeTextStyle;\n    }\n\n    protected int getDateHeaderTextColor() {\n        return dateHeaderTextColor;\n    }\n\n    protected int getDateHeaderTextSize() {\n        return dateHeaderTextSize;\n    }\n\n    protected int getDateHeaderTextStyle() {\n        return dateHeaderTextStyle;\n    }\n\n    protected int getDateHeaderPadding() {\n        return dateHeaderPadding;\n    }\n\n    protected String getDateHeaderFormat() {\n        return dateHeaderFormat;\n    }\n\n    protected boolean showIncomingTime() {\n        return showIncomingTime;\n    }\n\n    protected int getIncomingTimeTextSize() {\n        return incomingTimeTextSize;\n    }\n\n    protected int getIncomingTimeTextStyle() {\n        return incomingTimeTextStyle;\n    }\n\n    protected int getIncomingTimeTextColor() {\n        return incomingTimeTextColor;\n    }\n\n    protected int getIncomingImageTimeTextColor() {\n        return incomingImageTimeTextColor;\n    }\n\n    protected int getIncomingImageTimeTextSize() {\n        return incomingImageTimeTextSize;\n    }\n\n    protected int getIncomingImageTimeTextStyle() {\n        return incomingImageTimeTextStyle;\n    }\n\n    protected Drawable getIncomingBubbleDrawable() {\n        if (incomingBubbleDrawable == -1) {\n            return getMessageSelector(incomingDefaultBubbleColor, incomingDefaultBubbleSelectedColor,\n                    incomingDefaultBubblePressedColor, R.drawable.shape_incoming_message);\n        } else {\n            return getDrawable(incomingBubbleDrawable);\n        }\n    }\n\n    protected Drawable getIncomingBubbleSelectedDrawable() {\n        return getMessageSelector(incomingDefaultBubbleSelectedColor, incomingDefaultBubbleSelectedColor,\n                incomingDefaultBubblePressedColor, R.drawable.shape_incoming_message);\n    }\n\n    protected Drawable getIncomingImageOverlayDrawable() {\n        if (incomingImageOverlayDrawable == -1) {\n            return getMessageSelector(Color.TRANSPARENT, incomingDefaultImageOverlaySelectedColor,\n                    incomingDefaultImageOverlayPressedColor, R.drawable.shape_incoming_message);\n        } else {\n            return getDrawable(incomingImageOverlayDrawable);\n        }\n    }\n\n    protected boolean isMessageFocusable() {\n        return isMessageFocusable;\n    }\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/messages/RecyclerScrollMoreListener.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.messages;\n\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager;\n\nclass RecyclerScrollMoreListener\n        extends RecyclerView.OnScrollListener {\n\n    private OnLoadMoreListener loadMoreListener;\n    private int currentPage = 0;\n    private int previousTotalItemCount = 0;\n    private int currentScrollPos = 0;\n    private int maxScrollPos = 0;\n    private boolean loading = true;\n\n    private RecyclerView.LayoutManager mLayoutManager;\n\n    RecyclerScrollMoreListener(LinearLayoutManager layoutManager, OnLoadMoreListener loadMoreListener) {\n        this.mLayoutManager = layoutManager;\n        this.loadMoreListener = loadMoreListener;\n    }\n\n    private int getLastVisibleItem(int[] lastVisibleItemPositions) {\n        int maxSize = 0;\n        for (int i = 0; i < lastVisibleItemPositions.length; i++) {\n            if (i == 0) {\n                maxSize = lastVisibleItemPositions[i];\n            } else if (lastVisibleItemPositions[i] > maxSize) {\n                maxSize = lastVisibleItemPositions[i];\n            }\n        }\n        return maxSize;\n    }\n\n    // MODIFIED: fully custom\n    // Fix: lastVisibleItemPositions is wrong. Solution: remove it altogether.\n    @Override\n    public void onScrolled(RecyclerView view, int dx, int dy) {\n        if (loadMoreListener != null) {\n            // MODIFIED: throttle calls\n            // Swallow scrolling up. Continue on scroll down.\n            currentScrollPos += dy;\n            if (currentScrollPos <= maxScrollPos) {\n                return;\n            }\n            maxScrollPos += dy;\n\n            int totalItemCount = mLayoutManager.getItemCount();\n\n            if (totalItemCount < previousTotalItemCount) {\n                this.currentPage = 0;\n                this.previousTotalItemCount = totalItemCount;\n                if (totalItemCount == 0) {\n                    this.loading = true;\n                }\n            }\n\n            if (loading && (totalItemCount > previousTotalItemCount)) {\n                loading = false;\n                previousTotalItemCount = totalItemCount;\n            }\n\n            if (!loading) {\n                currentPage++;\n                loadMoreListener.onLoadMore(loadMoreListener.getMessagesCount(), totalItemCount);\n                loading = true;\n            }\n        }\n    }\n\n    // ORIGIN\n    //@Override\n    //public void onScrolled(RecyclerView view, int dx, int dy) {\n    //    if (loadMoreListener != null) {\n    //        int lastVisibleItemPosition = 0;\n    //        int totalItemCount = mLayoutManager.getItemCount();\n    //\n    //        if (mLayoutManager instanceof StaggeredGridLayoutManager) {\n    //            int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions(null);\n    //            lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions);\n    //        } else if (mLayoutManager instanceof LinearLayoutManager) {\n    //            lastVisibleItemPosition = ((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition();\n    //        } else if (mLayoutManager instanceof GridLayoutManager) {\n    //            lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition();\n    //        }\n    //\n    //        if (totalItemCount < previousTotalItemCount) {\n    //            this.currentPage = 0;\n    //            this.previousTotalItemCount = totalItemCount;\n    //            if (totalItemCount == 0) {\n    //                this.loading = true;\n    //            }\n    //        }\n    //\n    //        if (loading && (totalItemCount > previousTotalItemCount)) {\n    //            loading = false;\n    //            previousTotalItemCount = totalItemCount;\n    //        }\n    //\n    //        int visibleThreshold = 5;\n    //        if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {\n    //            currentPage++;\n    //            loadMoreListener.onLoadMore(loadMoreListener.getMessagesCount(), totalItemCount);\n    //            loading = true;\n    //        }\n    //    }\n    //}\n\n    interface OnLoadMoreListener {\n        void onLoadMore(int page, int total);\n\n        int getMessagesCount();\n    }\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/utils/DateFormatter.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.utils;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.Locale;\n\npublic final class DateFormatter {\n    private DateFormatter() {\n        throw new AssertionError();\n    }\n\n    public static String format(Date date, Template template) {\n        return format(date, template.get());\n    }\n\n    public static String format(Date date, String format) {\n        if (date == null) return \"\";\n        return new SimpleDateFormat(format, Locale.getDefault())\n                .format(date);\n    }\n\n    public static boolean isSameDay(Date date1, Date date2) {\n        if (date1 == null || date2 == null) {\n            throw new IllegalArgumentException(\"Dates must not be null\");\n        }\n        Calendar cal1 = Calendar.getInstance();\n        cal1.setTime(date1);\n        Calendar cal2 = Calendar.getInstance();\n        cal2.setTime(date2);\n        return isSameDay(cal1, cal2);\n    }\n\n    public static boolean isSameDay(Calendar cal1, Calendar cal2) {\n        if (cal1 == null || cal2 == null) {\n            throw new IllegalArgumentException(\"Dates must not be null\");\n        }\n        return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) &&\n                cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&\n                cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR));\n    }\n\n    public static boolean isSameYear(Date date1, Date date2) {\n        if (date1 == null || date2 == null) {\n            throw new IllegalArgumentException(\"Dates must not be null\");\n        }\n        Calendar cal1 = Calendar.getInstance();\n        cal1.setTime(date1);\n        Calendar cal2 = Calendar.getInstance();\n        cal2.setTime(date2);\n        return isSameYear(cal1, cal2);\n    }\n\n    public static boolean isSameYear(Calendar cal1, Calendar cal2) {\n        if (cal1 == null || cal2 == null) {\n            throw new IllegalArgumentException(\"Dates must not be null\");\n        }\n        return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) &&\n                cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR));\n    }\n\n    public static boolean isToday(Calendar calendar) {\n        return isSameDay(calendar, Calendar.getInstance());\n    }\n\n    public static boolean isToday(Date date) {\n        return isSameDay(date, Calendar.getInstance().getTime());\n    }\n\n    public static boolean isYesterday(Calendar calendar) {\n        Calendar yesterday = Calendar.getInstance();\n        yesterday.add(Calendar.DAY_OF_MONTH, -1);\n        return isSameDay(calendar, yesterday);\n    }\n\n    public static boolean isYesterday(Date date) {\n        Calendar yesterday = Calendar.getInstance();\n        yesterday.add(Calendar.DAY_OF_MONTH, -1);\n        return isSameDay(date, yesterday.getTime());\n    }\n\n    public static boolean isCurrentYear(Date date) {\n        return isSameYear(date, Calendar.getInstance().getTime());\n    }\n\n    public static boolean isCurrentYear(Calendar calendar) {\n        return isSameYear(calendar, Calendar.getInstance());\n    }\n\n    /**\n     * Interface used to format dates before they were displayed (e.g. dialogs time, messages date headers etc.).\n     */\n    public interface Formatter {\n\n        /**\n         * Formats an string representation of the date object.\n         *\n         * @param date The date that should be formatted.\n         * @return Formatted text.\n         */\n        String format(Date date);\n    }\n\n    public enum Template {\n        STRING_DAY_MONTH_YEAR(\"d MMMM yyyy\"),\n        STRING_DAY_MONTH(\"d MMMM\"),\n        TIME(\"HH:mm\");\n\n        private String template;\n\n        Template(String template) {\n            this.template = template;\n        }\n\n        public String get() {\n            return template;\n        }\n    }\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/utils/RoundedImageView.java",
    "content": "package com.stfalcon.chatkit.utils;\n\nimport android.content.Context;\nimport android.content.res.Resources;\nimport android.content.res.Resources.NotFoundException;\nimport android.graphics.Bitmap;\nimport android.graphics.Bitmap.Config;\nimport android.graphics.BitmapShader;\nimport android.graphics.Canvas;\nimport android.graphics.ColorFilter;\nimport android.graphics.Matrix;\nimport android.graphics.Paint;\nimport android.graphics.Path;\nimport android.graphics.PixelFormat;\nimport android.graphics.RectF;\nimport android.graphics.Shader;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.LayerDrawable;\nimport android.net.Uri;\nimport android.util.AttributeSet;\n\nimport androidx.annotation.DimenRes;\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.widget.AppCompatImageView;\nimport androidx.core.content.ContextCompat;\n\n/**\n * Thanks to Joonho Kim (https://github.com/pungrue26) for his lightweight SelectableRoundedImageView,\n * that was used as default image message representation\n */\npublic class RoundedImageView extends AppCompatImageView {\n\n    private int mResource = 0;\n    private Drawable mDrawable;\n\n    private float[] mRadii = new float[]{0, 0, 0, 0, 0, 0, 0, 0};\n\n    public RoundedImageView(Context context) {\n        super(context);\n    }\n\n    public RoundedImageView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public RoundedImageView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n    }\n\n    @Override\n    protected void drawableStateChanged() {\n        super.drawableStateChanged();\n        invalidate();\n    }\n\n    @Override\n    public void setImageDrawable(Drawable drawable) {\n        mResource = 0;\n        mDrawable = RoundedCornerDrawable.fromDrawable(drawable, getResources());\n        super.setImageDrawable(mDrawable);\n        updateDrawable();\n    }\n\n    @Override\n    public void setImageBitmap(Bitmap bm) {\n        mResource = 0;\n        mDrawable = RoundedCornerDrawable.fromBitmap(bm, getResources());\n        super.setImageDrawable(mDrawable);\n        updateDrawable();\n    }\n\n    @Override\n    public void setImageResource(int resId) {\n        if (mResource != resId) {\n            mResource = resId;\n            mDrawable = resolveResource();\n            super.setImageDrawable(mDrawable);\n            updateDrawable();\n        }\n    }\n\n    @Override\n    public void setImageURI(Uri uri) {\n        super.setImageURI(uri);\n        setImageDrawable(getDrawable());\n    }\n\n    public void setCorners(@DimenRes int leftTop, @DimenRes int rightTop,\n                           @DimenRes int rightBottom, @DimenRes int leftBottom) {\n        setCorners(\n                leftTop == 0 ? 0 : getResources().getDimension(leftTop),\n                rightTop == 0 ? 0 : getResources().getDimension(rightTop),\n                rightBottom == 0 ? 0 : getResources().getDimension(rightBottom),\n                leftBottom == 0 ? 0 : getResources().getDimension(leftBottom)\n        );\n    }\n\n    public void setCorners(float leftTop, float rightTop, float rightBottom, float leftBottom) {\n        mRadii = new float[]{\n                leftTop, leftTop,\n                rightTop, rightTop,\n                rightBottom, rightBottom,\n                leftBottom, leftBottom};\n\n        updateDrawable();\n    }\n\n    private Drawable resolveResource() {\n        Drawable d = null;\n\n        if (mResource != 0) {\n            try {\n                d = ContextCompat.getDrawable(getContext(), mResource);\n            } catch (NotFoundException e) {\n                mResource = 0;\n            }\n        }\n        return RoundedCornerDrawable.fromDrawable(d, getResources());\n    }\n\n    private void updateDrawable() {\n        if (mDrawable == null) return;\n\n        ((RoundedCornerDrawable) mDrawable).setCornerRadii(mRadii);\n    }\n\n    private static class RoundedCornerDrawable extends Drawable {\n        private RectF mBounds = new RectF();\n\n        private final RectF mBitmapRect = new RectF();\n        private final int mBitmapWidth;\n        private final int mBitmapHeight;\n\n        private final Paint mBitmapPaint;\n\n        private float[] mRadii = new float[]{0, 0, 0, 0, 0, 0, 0, 0};\n\n        private Path mPath = new Path();\n        private Bitmap mBitmap;\n        private boolean mBoundsConfigured = false;\n\n        private RoundedCornerDrawable(Bitmap bitmap, Resources r) {\n            mBitmap = bitmap;\n            BitmapShader mBitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);\n\n            mBitmapWidth = bitmap.getScaledWidth(r.getDisplayMetrics());\n            mBitmapHeight = bitmap.getScaledHeight(r.getDisplayMetrics());\n\n            mBitmapRect.set(0, 0, mBitmapWidth, mBitmapHeight);\n\n            mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n            mBitmapPaint.setStyle(Paint.Style.FILL);\n            mBitmapPaint.setShader(mBitmapShader);\n        }\n\n        private static RoundedCornerDrawable fromBitmap(Bitmap bitmap, Resources r) {\n            if (bitmap != null) return new RoundedCornerDrawable(bitmap, r);\n            else return null;\n        }\n\n        private static Drawable fromDrawable(Drawable drawable, Resources r) {\n            if (drawable != null) {\n                if (drawable instanceof RoundedCornerDrawable) {\n                    return drawable;\n                } else if (drawable instanceof LayerDrawable) {\n                    LayerDrawable ld = (LayerDrawable) drawable;\n                    final int num = ld.getNumberOfLayers();\n                    for (int i = 0; i < num; i++) {\n                        Drawable d = ld.getDrawable(i);\n                        ld.setDrawableByLayerId(ld.getId(i), fromDrawable(d, r));\n                    }\n                    return ld;\n                }\n\n                Bitmap bm = drawableToBitmap(drawable);\n                if (bm != null) return new RoundedCornerDrawable(bm, r);\n            }\n            return drawable;\n        }\n\n        private static Bitmap drawableToBitmap(Drawable drawable) {\n            if (drawable == null) return null;\n\n            if (drawable instanceof BitmapDrawable) {\n                return ((BitmapDrawable) drawable).getBitmap();\n            }\n\n            Bitmap bitmap;\n            int width = Math.max(drawable.getIntrinsicWidth(), 2);\n            int height = Math.max(drawable.getIntrinsicHeight(), 2);\n            try {\n                bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);\n                Canvas canvas = new Canvas(bitmap);\n                drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());\n                drawable.draw(canvas);\n            } catch (IllegalArgumentException e) {\n                e.printStackTrace();\n                bitmap = null;\n            }\n            return bitmap;\n        }\n\n        private void configureBounds(Canvas canvas) {\n            Matrix canvasMatrix = canvas.getMatrix();\n\n            applyScaleToRadii(canvasMatrix);\n            mBounds.set(mBitmapRect);\n        }\n\n        private void applyScaleToRadii(Matrix m) {\n            float[] values = new float[9];\n            m.getValues(values);\n            for (int i = 0; i < mRadii.length; i++) {\n                mRadii[i] = mRadii[i] / values[0];\n            }\n        }\n\n        @Override\n        public void draw(@NonNull Canvas canvas) {\n            canvas.save();\n            if (!mBoundsConfigured) {\n                configureBounds(canvas);\n                mBoundsConfigured = true;\n            }\n            mPath.addRoundRect(mBounds, mRadii, Path.Direction.CW);\n            canvas.drawPath(mPath, mBitmapPaint);\n            canvas.restore();\n        }\n\n        void setCornerRadii(float[] radii) {\n            if (radii == null) return;\n            if (radii.length != 8)\n                throw new ArrayIndexOutOfBoundsException(\"radii[] needs 8 values\");\n\n            System.arraycopy(radii, 0, mRadii, 0, radii.length);\n        }\n\n        @Override\n        public int getOpacity() {\n            return (mBitmap == null || mBitmap.hasAlpha() || mBitmapPaint.getAlpha() < 255)\n                    ? PixelFormat.TRANSLUCENT\n                    : PixelFormat.OPAQUE;\n        }\n\n        @Override\n        public void setAlpha(int alpha) {\n            mBitmapPaint.setAlpha(alpha);\n            invalidateSelf();\n        }\n\n        @Override\n        public void setColorFilter(ColorFilter cf) {\n            mBitmapPaint.setColorFilter(cf);\n            invalidateSelf();\n        }\n\n        @Override\n        public void setDither(boolean dither) {\n            mBitmapPaint.setDither(dither);\n            invalidateSelf();\n        }\n\n        @Override\n        public void setFilterBitmap(boolean filter) {\n            mBitmapPaint.setFilterBitmap(filter);\n            invalidateSelf();\n        }\n\n        @Override\n        public int getIntrinsicWidth() {\n            return mBitmapWidth;\n        }\n\n        @Override\n        public int getIntrinsicHeight() {\n            return mBitmapHeight;\n        }\n    }\n}\n"
  },
  {
    "path": "chatkit/src/main/java/com/stfalcon/chatkit/utils/ShapeImageView.java",
    "content": "/*******************************************************************************\n * Copyright 2016 stfalcon.com\n * <p>\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * <p>\n * http://www.apache.org/licenses/LICENSE-2.0\n * <p>\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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.stfalcon.chatkit.utils;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Path;\nimport android.util.AttributeSet;\nimport android.view.View;\n\n/**\n * ImageView with mask what described with Bézier Curves\n */\n\npublic class ShapeImageView extends androidx.appcompat.widget.AppCompatImageView {\n    private Path path;\n\n    public ShapeImageView(Context context) {\n        super(context);\n        setLayerType(View.LAYER_TYPE_SOFTWARE, null);\n    }\n\n    public ShapeImageView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        setLayerType(View.LAYER_TYPE_SOFTWARE, null);\n    }\n\n    @Override\n    protected void onSizeChanged(int w, int h, int oldw, int oldh) {\n        super.onSizeChanged(w, h, oldw, oldh);\n        path = new Path();\n        float halfWidth = (float) w / 2f;\n        float firstParam = (float) w * 0.1f;\n        float secondParam = (float) w * 0.8875f;\n\n        //Bézier Curves\n        path.moveTo(halfWidth, (float) w);\n        path.cubicTo(firstParam, (float) w, 0, secondParam, 0, halfWidth);\n        path.cubicTo(0, firstParam, firstParam, 0, halfWidth, 0);\n        path.cubicTo(secondParam, 0, (float) w, firstParam, (float) w, halfWidth);\n        path.cubicTo((float) w, secondParam, secondParam, (float) w, halfWidth, (float) w);\n        path.close();\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        if (path.isEmpty()) {\n            super.onDraw(canvas);\n            return;\n        }\n\n        int saveCount = canvas.save();\n        canvas.clipPath(path);\n        super.onDraw(canvas);\n        canvas.restoreToCount(saveCount);\n    }\n}\n"
  },
  {
    "path": "chatkit/src/main/res/color/textchange.xml",
    "content": "<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\r\n    <!--<item android:state_selected=\"true\" android:color=\"@android:color/white\" />-->\r\n    <item android:state_focused=\"true\" android:color=\"@android:color/white\" />\r\n    <!--<item android:state_pressed=\"true\" android:color=\"@android:color/white\" />-->\r\n    <!--<item android:color=\"#504f4f\" /> &lt;!&ndash; default case &ndash;&gt;-->\r\n</selector>"
  },
  {
    "path": "chatkit/src/main/res/drawable/bgchange.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\r\n    <item android:state_focused=\"true\" android:drawable=\"@color/tg_selected_bg\"/>\r\n    <!--<item android:drawable=\"@color/white\"/> &lt;!&ndash; default &ndash;&gt;-->\r\n    <!--<item android:state_focused=\"true\" android:color=\"#000000\"/>-->\r\n    <!--<item android:color=\"#FFFFFF\" />  &lt;!&ndash; default &ndash;&gt;-->\r\n</selector>"
  },
  {
    "path": "chatkit/src/main/res/drawable/bubble_circle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n    <solid android:color=\"@color/white\" />\n    <stroke\n        android:width=\"2dp\"\n        android:color=\"@color/white\" />\n    <size\n        android:width=\"20dp\"\n        android:height=\"20dp\" />\n</shape>"
  },
  {
    "path": "chatkit/src/main/res/drawable/shape_incoming_message.xml",
    "content": "<!-- MODIFIED -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <!-- Bubble corner: android:bottomLeftRadius=\"0dp\"->\"@dimen/message_bubble_corners_radius\" -->\n    <corners\n        android:bottomLeftRadius=\"@dimen/message_bubble_corners_radius\"\n        android:bottomRightRadius=\"@dimen/message_bubble_corners_radius\"\n        android:topLeftRadius=\"@dimen/message_bubble_corners_radius\"\n        android:topRightRadius=\"@dimen/message_bubble_corners_radius\" />\n\n    <solid android:color=\"@color/white\" />\n\n</shape>"
  },
  {
    "path": "chatkit/src/main/res/drawable/shape_incoming_message_focused.xml",
    "content": "<!-- MODIFIED -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <!-- Bubble corner: android:bottomLeftRadius=\"0dp\"->\"@dimen/message_bubble_corners_radius\" -->\n    <corners\n        android:bottomLeftRadius=\"@dimen/message_bubble_corners_radius\"\n        android:bottomRightRadius=\"@dimen/message_bubble_corners_radius\"\n        android:topLeftRadius=\"@dimen/message_bubble_corners_radius\"\n        android:topRightRadius=\"@dimen/message_bubble_corners_radius\" />\n\n    <solid android:color=\"@color/tg_selected_bg\" />\n\n</shape>"
  },
  {
    "path": "chatkit/src/main/res/drawable/shape_outcoming_message.xml",
    "content": "<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <corners\n        android:bottomLeftRadius=\"@dimen/message_bubble_corners_radius\"\n        android:bottomRightRadius=\"0dp\"\n        android:topLeftRadius=\"@dimen/message_bubble_corners_radius\"\n        android:topRightRadius=\"@dimen/message_bubble_corners_radius\" />\n\n    <solid android:color=\"@color/white\" />\n\n</shape>"
  },
  {
    "path": "chatkit/src/main/res/layout/item_date_header.xml",
    "content": "<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@id/messageText\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center_horizontal\"\n    android:padding=\"16dp\" />"
  },
  {
    "path": "chatkit/src/main/res/layout/item_dialog.xml",
    "content": "<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@id/dialogRootLayout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <RelativeLayout\n        android:id=\"@id/dialogContainer\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?attr/selectableItemBackground\">\n\n        <com.stfalcon.chatkit.utils.ShapeImageView\n            android:id=\"@id/dialogAvatar\"\n            android:layout_width=\"@dimen/dialog_avatar_width\"\n            android:layout_height=\"@dimen/dialog_avatar_height\"\n            android:layout_margin=\"16dp\" />\n\n        <TextView\n            android:id=\"@id/dialogName\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginEnd=\"16dp\"\n            android:layout_marginRight=\"16dp\"\n            android:layout_marginTop=\"19dp\"\n            android:layout_toEndOf=\"@id/dialogAvatar\"\n            android:layout_toLeftOf=\"@id/dialogDate\"\n            android:layout_toRightOf=\"@id/dialogAvatar\"\n            android:layout_toStartOf=\"@id/dialogDate\"\n            android:ellipsize=\"end\"\n            android:fontFamily=\"@string/font_fontFamily_medium\"\n            android:includeFontPadding=\"false\"\n            android:maxLines=\"1\" />\n\n        <TextView\n            android:id=\"@id/dialogDate\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_alignParentRight=\"true\"\n            android:layout_marginEnd=\"16dp\"\n            android:layout_marginRight=\"16dp\"\n            android:layout_marginTop=\"16dp\"\n            android:ellipsize=\"end\"\n            android:maxLines=\"1\" />\n\n        <RelativeLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_below=\"@id/dialogName\"\n            android:layout_marginEnd=\"16dp\"\n            android:layout_marginRight=\"16dp\"\n            android:layout_marginTop=\"7dp\"\n            android:layout_toEndOf=\"@id/dialogAvatar\"\n            android:layout_toRightOf=\"@id/dialogAvatar\">\n\n            <com.stfalcon.chatkit.utils.ShapeImageView\n                android:id=\"@id/dialogLastMessageUserAvatar\"\n                android:layout_width=\"24dp\"\n                android:layout_height=\"24dp\"\n                android:layout_marginEnd=\"7dp\"\n                android:layout_marginRight=\"7dp\" />\n\n\n            <TextView\n                android:id=\"@id/dialogLastMessage\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_centerVertical=\"true\"\n                android:layout_toEndOf=\"@id/dialogLastMessageUserAvatar\"\n                android:layout_toRightOf=\"@id/dialogLastMessageUserAvatar\"\n                android:ellipsize=\"end\"\n                android:gravity=\"top\"\n                android:maxLines=\"1\" />\n\n        </RelativeLayout>\n\n        <TextView\n            android:id=\"@id/dialogUnreadBubble\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignEnd=\"@id/dialogAvatar\"\n            android:layout_alignRight=\"@id/dialogAvatar\"\n            android:layout_alignTop=\"@id/dialogAvatar\"\n            android:layout_marginEnd=\"-5dp\"\n            android:layout_marginRight=\"-5dp\"\n            android:layout_marginTop=\"-5dp\"\n            android:background=\"@drawable/bubble_circle\"\n            android:ellipsize=\"end\"\n            android:fontFamily=\"@string/font_fontFamily_medium\"\n            android:gravity=\"center\"\n            android:lines=\"1\" />\n\n        <FrameLayout\n            android:id=\"@id/dialogDividerContainer\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentBottom=\"true\"\n            android:layout_marginTop=\"16dp\"\n            android:paddingLeft=\"@dimen/dialog_divider_margin_left\"\n            android:paddingStart=\"@dimen/dialog_divider_margin_left\">\n\n            <View\n                android:id=\"@id/dialogDivider\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"1dp\"\n                android:background=\"@color/dialog_divider\" />\n\n        </FrameLayout>\n\n    </RelativeLayout>\n\n</FrameLayout>\n"
  },
  {
    "path": "chatkit/src/main/res/layout/item_incoming_image_message.xml",
    "content": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginBottom=\"8dp\"\n    android:layout_marginLeft=\"16dp\"\n    android:layout_marginRight=\"16dp\"\n    android:layout_marginTop=\"8dp\">\n\n    <com.stfalcon.chatkit.utils.ShapeImageView\n        android:id=\"@id/messageUserAvatar\"\n        android:layout_width=\"40dp\"\n        android:layout_height=\"40dp\"\n        android:layout_above=\"@id/messageTime\"\n        android:layout_marginEnd=\"8dp\"\n        android:layout_marginRight=\"8dp\" />\n\n    <com.stfalcon.chatkit.utils.RoundedImageView\n        android:id=\"@id/image\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:adjustViewBounds=\"true\"\n        android:layout_marginEnd=\"@dimen/message_incoming_bubble_margin_right\"\n        android:layout_marginRight=\"@dimen/message_incoming_bubble_margin_right\"\n        android:layout_toEndOf=\"@id/messageUserAvatar\"\n        android:layout_toRightOf=\"@id/messageUserAvatar\" />\n\n    <View\n        android:id=\"@id/imageOverlay\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignBottom=\"@id/image\"\n        android:layout_alignEnd=\"@id/image\"\n        android:layout_alignLeft=\"@id/image\"\n        android:layout_alignRight=\"@id/image\"\n        android:layout_alignStart=\"@id/image\"\n        android:layout_alignTop=\"@id/image\" />\n\n    <TextView\n        android:id=\"@id/messageTime\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignEnd=\"@id/image\"\n        android:layout_alignRight=\"@id/image\"\n        android:layout_below=\"@id/image\" />\n\n</RelativeLayout>"
  },
  {
    "path": "chatkit/src/main/res/layout/item_incoming_text_message.xml",
    "content": "<!-- MOD: -->\n<!-- Android 9 message focused bug fix: RelativeLayout focusable=\"false\" -->\n<!-- Decrease space between messages: RelativeLayout android:layout_marginBottom=\"8dp\"->\"3dp\" android:layout_marginTop=\"8dp\"->\"0dp\" -->\n<!-- Margins: android:layout_marginRight=\"16dp\"->\"3dp\" -->\n<!-- Margins: android:layout_marginBottom=\"3dp\"->\"7dp\" -->\n<com.stfalcon.chatkit.commons.widgets.FocusFixRelativeLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginBottom=\"10dp\"\n    android:layout_marginLeft=\"16dp\"\n    android:layout_marginRight=\"3dp\"\n    android:layout_marginTop=\"0dp\"\n    android:focusable=\"false\">\n\n    <!-- Move author icon to the top android:layout_alignParentBottom=\"true\"->android:layout_alignParentTop=\"true\", add new android:layout_marginTop=\"2dp\" -->\n    <com.stfalcon.chatkit.utils.ShapeImageView\n        android:id=\"@id/messageUserAvatar\"\n        android:layout_width=\"40dp\"\n        android:layout_height=\"40dp\"\n        android:layout_alignParentTop=\"true\"\n        android:layout_marginTop=\"2dp\"\n        android:layout_marginEnd=\"8dp\"\n        android:layout_marginRight=\"8dp\" />\n\n    <!-- Stretch bubble: android:layout_width=\"wrap_content\"->\"match_parent\" -->\n    <!-- Stretch bubble: app:justifyContent=\"flex_end\"->\"flex_start\" -->\n    <com.google.android.flexbox.FlexboxLayout\n        android:id=\"@id/bubble\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginEnd=\"@dimen/message_incoming_bubble_margin_right\"\n        android:layout_marginRight=\"@dimen/message_incoming_bubble_margin_right\"\n        android:layout_toEndOf=\"@id/messageUserAvatar\"\n        android:layout_toRightOf=\"@id/messageUserAvatar\"\n        android:orientation=\"vertical\"\n        app:alignContent=\"stretch\"\n        app:alignItems=\"stretch\"\n        app:flexWrap=\"wrap\"\n        app:justifyContent=\"flex_start\">\n\n        <!-- Tweaks from video info dialog: leanback_list_preference_fragment.xml:26 -->\n        <!-- More info: com.liskovsoft.smartyoutubetv2.tv.ui.mod.leanback.misc.LeanbackListPreferenceDialogFragment -->\n        <!-- Text: add android:fontFamily=\"sans-serif-condensed\" -->\n        <!-- Text: add android:lineSpacingMultiplier=\"0.9\" -->\n        <!-- Messages exceeding certain length cause crashes. RecycleView bug? -->\n        <!-- add android:maxLines=\"130\" (max 130, optimal for reading 50) -->\n        <TextView\n            android:id=\"@id/messageText\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:fontFamily=\"sans-serif-condensed\"\n            android:lineSpacingMultiplier=\"0.9\"\n            android:maxLines=\"50\" />\n\n        <TextView\n            android:id=\"@id/messageTime\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_below=\"@id/messageText\"\n            android:layout_marginLeft=\"8dp\"\n            android:layout_marginStart=\"8dp\"\n            app:layout_alignSelf=\"center\" />\n\n    </com.google.android.flexbox.FlexboxLayout>\n\n</com.stfalcon.chatkit.commons.widgets.FocusFixRelativeLayout>"
  },
  {
    "path": "chatkit/src/main/res/layout/item_outcoming_image_message.xml",
    "content": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginBottom=\"8dp\"\n    android:layout_marginLeft=\"16dp\"\n    android:layout_marginRight=\"16dp\"\n    android:layout_marginTop=\"8dp\">\n\n    <com.stfalcon.chatkit.utils.RoundedImageView\n        android:id=\"@id/image\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:adjustViewBounds=\"true\"\n        android:layout_marginLeft=\"@dimen/message_outcoming_bubble_margin_left\"\n        android:layout_marginStart=\"@dimen/message_outcoming_bubble_margin_left\" />\n\n    <View\n        android:id=\"@id/imageOverlay\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignBottom=\"@id/image\"\n        android:layout_alignEnd=\"@id/image\"\n        android:layout_alignLeft=\"@id/image\"\n        android:layout_alignRight=\"@id/image\"\n        android:layout_alignStart=\"@id/image\"\n        android:layout_alignTop=\"@id/image\" />\n\n    <TextView\n        android:id=\"@id/messageTime\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignEnd=\"@id/image\"\n        android:layout_alignRight=\"@id/image\"\n        android:layout_below=\"@id/image\" />\n\n</RelativeLayout>"
  },
  {
    "path": "chatkit/src/main/res/layout/item_outcoming_text_message.xml",
    "content": "<!-- MODIFIED: Android 9 message focused bug fix (RelativeLayout focusable=\"false\") -->\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginBottom=\"8dp\"\n    android:layout_marginLeft=\"16dp\"\n    android:layout_marginRight=\"16dp\"\n    android:layout_marginTop=\"8dp\"\n    android:focusable=\"false\">\n\n    <com.google.android.flexbox.FlexboxLayout\n        android:id=\"@id/bubble\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentEnd=\"true\"\n        android:layout_alignParentRight=\"true\"\n        android:layout_marginLeft=\"@dimen/message_outcoming_bubble_margin_left\"\n        android:layout_marginStart=\"@dimen/message_outcoming_bubble_margin_left\"\n        app:alignContent=\"stretch\"\n        app:alignItems=\"stretch\"\n        app:flexWrap=\"wrap\"\n        app:justifyContent=\"flex_end\">\n\n        <TextView\n            android:id=\"@id/messageText\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignWithParentIfMissing=\"true\" />\n\n        <TextView\n            android:id=\"@id/messageTime\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_below=\"@id/messageText\"\n            android:layout_marginLeft=\"8dp\"\n            android:layout_marginStart=\"8dp\"\n            app:layout_alignSelf=\"center\"\n            app:layout_order=\"1\" />\n\n    </com.google.android.flexbox.FlexboxLayout>\n\n</RelativeLayout>"
  },
  {
    "path": "chatkit/src/main/res/layout/view_message_input.xml",
    "content": "<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <ImageButton\n        android:id=\"@id/attachmentButton\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_centerVertical=\"true\" />\n\n    <Space\n        android:id=\"@id/attachmentButtonSpace\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:layout_toEndOf=\"@id/attachmentButton\"\n        android:layout_toRightOf=\"@id/attachmentButton\" />\n\n    <EditText\n        android:id=\"@id/messageInput\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_centerVertical=\"true\"\n        android:layout_toEndOf=\"@id/attachmentButtonSpace\"\n        android:layout_toLeftOf=\"@id/sendButtonSpace\"\n        android:layout_toRightOf=\"@id/attachmentButtonSpace\"\n        android:layout_toStartOf=\"@id/sendButtonSpace\"\n        android:inputType=\"textAutoCorrect|textAutoComplete|textMultiLine|textCapSentences\" />\n\n    <Space\n        android:id=\"@id/sendButtonSpace\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        android:layout_toLeftOf=\"@id/messageSendButton\"\n        android:layout_toStartOf=\"@id/messageSendButton\" />\n\n    <ImageButton\n        android:id=\"@id/messageSendButton\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentEnd=\"true\"\n        android:layout_alignParentRight=\"true\"\n        android:layout_centerVertical=\"true\" />\n\n</merge>"
  },
  {
    "path": "chatkit/src/main/res/values/attrs.xml",
    "content": "<resources>\n\n    <attr name=\"MessageInput\" format=\"reference\" />\n\n    <declare-styleable name=\"MessageInput\">\n        <attr name=\"inputButtonBackground\" format=\"reference\" />\n        <attr name=\"inputButtonDefaultBgColor\" format=\"color|reference\" />\n        <attr name=\"inputButtonDefaultBgPressedColor\" format=\"color|reference\" />\n        <attr name=\"inputButtonDefaultBgDisabledColor\" format=\"color|reference\" />\n\n        <attr name=\"inputButtonIcon\" format=\"reference\" />\n        <attr name=\"inputButtonDefaultIconColor\" format=\"color|reference\" />\n        <attr name=\"inputButtonDefaultIconPressedColor\" format=\"color|reference\" />\n        <attr name=\"inputButtonDefaultIconDisabledColor\" format=\"color|reference\" />\n\n        <attr name=\"inputButtonMargin\" format=\"dimension|reference\" />\n        <attr name=\"inputButtonWidth\" format=\"dimension|reference\" />\n        <attr name=\"inputButtonHeight\" format=\"dimension|reference\" />\n\n        <attr name=\"showAttachmentButton\" format=\"boolean\" />\n        <attr name=\"attachmentButtonBackground\" format=\"reference\" />\n        <attr name=\"attachmentButtonDefaultBgColor\" format=\"color|reference\" />\n        <attr name=\"attachmentButtonDefaultBgPressedColor\" format=\"color|reference\" />\n        <attr name=\"attachmentButtonDefaultBgDisabledColor\" format=\"color|reference\" />\n\n        <attr name=\"attachmentButtonIcon\" format=\"reference\" />\n        <attr name=\"attachmentButtonDefaultIconColor\" format=\"color|reference\" />\n        <attr name=\"attachmentButtonDefaultIconPressedColor\" format=\"color|reference\" />\n        <attr name=\"attachmentButtonDefaultIconDisabledColor\" format=\"color|reference\" />\n\n        <attr name=\"attachmentButtonMargin\" format=\"dimension|reference\" />\n        <attr name=\"attachmentButtonWidth\" format=\"dimension|reference\" />\n        <attr name=\"attachmentButtonHeight\" format=\"dimension|reference\" />\n\n        <attr name=\"inputMaxLines\" format=\"integer\" />\n        <attr name=\"inputHint\" format=\"string\" />\n\n        <attr name=\"inputText\" format=\"string\" />\n        <attr name=\"inputTextSize\" format=\"dimension|reference\" />\n        <attr name=\"inputTextColor\" format=\"color|reference\" />\n        <attr name=\"inputHintColor\" format=\"color|reference\" />\n\n        <attr name=\"inputBackground\" format=\"reference\" />\n        <attr name=\"inputCursorDrawable\" format=\"reference\" />\n\n        <attr name=\"delayTypingStatus\" format=\"integer\" />\n    </declare-styleable>\n\n    <attr name=\"MessagesList\" format=\"reference\" />\n\n    <declare-styleable name=\"MessagesList\">\n\n        <attr name=\"textAutoLink\">\n            <flag name=\"all\" value=\"15\" />\n            <flag name=\"email\" value=\"2\" />\n            <flag name=\"map\" value=\"8\" />\n            <flag name=\"none\" value=\"0\" />\n            <flag name=\"phone\" value=\"4\" />\n            <flag name=\"web\" value=\"1\" />\n        </attr>\n\n        <attr name=\"incomingTextLinkColor\" format=\"color|reference\" />\n        <attr name=\"outcomingTextLinkColor\" format=\"color|reference\" />\n\n        <attr name=\"incomingAvatarWidth\" format=\"dimension|reference\" />\n        <attr name=\"incomingAvatarHeight\" format=\"dimension|reference\" />\n\n        <attr name=\"incomingBubbleDrawable\" format=\"reference\" />\n        <attr name=\"incomingDefaultBubbleColor\" format=\"color|reference\" />\n        <attr name=\"incomingDefaultBubblePressedColor\" format=\"color|reference\" />\n        <attr name=\"incomingDefaultBubbleSelectedColor\" format=\"color|reference\" />\n\n        <attr name=\"incomingImageOverlayDrawable\" format=\"reference\" />\n        <attr name=\"incomingDefaultImageOverlayPressedColor\" format=\"color|reference\" />\n        <attr name=\"incomingDefaultImageOverlaySelectedColor\" format=\"color|reference\" />\n\n        <attr name=\"incomingBubblePaddingLeft\" format=\"dimension|reference\" />\n        <attr name=\"incomingBubblePaddingRight\" format=\"dimension|reference\" />\n        <attr name=\"incomingBubblePaddingTop\" format=\"dimension|reference\" />\n        <attr name=\"incomingBubblePaddingBottom\" format=\"dimension|reference\" />\n\n        <attr name=\"incomingTextColor\" format=\"color|reference\" />\n        <attr name=\"incomingTextSize\" format=\"dimension|reference\" />\n        <attr name=\"incomingTextStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n\n        <attr name=\"showIncomingTime\" format=\"boolean|reference\" />\n        <attr name=\"incomingTimeTextColor\" format=\"color|reference\" />\n        <attr name=\"incomingTimeTextSize\" format=\"dimension|reference\" />\n        <attr name=\"incomingTimeTextStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n\n        <attr name=\"incomingImageTimeTextColor\" format=\"color|reference\" />\n        <attr name=\"incomingImageTimeTextSize\" format=\"dimension|reference\" />\n        <attr name=\"incomingImageTimeTextStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n\n        <attr name=\"outcomingBubbleDrawable\" format=\"reference\" />\n        <attr name=\"outcomingDefaultBubbleColor\" format=\"color|reference\" />\n        <attr name=\"outcomingDefaultBubblePressedColor\" format=\"color|reference\" />\n        <attr name=\"outcomingDefaultBubbleSelectedColor\" format=\"color|reference\" />\n\n        <attr name=\"outcomingImageOverlayDrawable\" format=\"reference\" />\n        <attr name=\"outcomingDefaultImageOverlayPressedColor\" format=\"color|reference\" />\n        <attr name=\"outcomingDefaultImageOverlaySelectedColor\" format=\"color|reference\" />\n\n        <attr name=\"outcomingBubblePaddingLeft\" format=\"dimension|reference\" />\n        <attr name=\"outcomingBubblePaddingRight\" format=\"dimension|reference\" />\n        <attr name=\"outcomingBubblePaddingTop\" format=\"dimension|reference\" />\n        <attr name=\"outcomingBubblePaddingBottom\" format=\"dimension|reference\" />\n\n        <attr name=\"outcomingTextColor\" format=\"color|reference\" />\n        <attr name=\"outcomingTextSize\" format=\"dimension|reference\" />\n        <attr name=\"outcomingTextStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n\n        <attr name=\"showOutcomingTime\" format=\"boolean|reference\" />\n        <attr name=\"outcomingTimeTextColor\" format=\"color|reference\" />\n        <attr name=\"outcomingTimeTextSize\" format=\"dimension|reference\" />\n        <attr name=\"outcomingTimeTextStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n\n        <attr name=\"outcomingImageTimeTextColor\" format=\"color|reference\" />\n        <attr name=\"outcomingImageTimeTextSize\" format=\"dimension|reference\" />\n        <attr name=\"outcomingImageTimeTextStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n\n        <attr name=\"dateHeaderPadding\" format=\"dimension|reference\" />\n        <attr name=\"dateHeaderFormat\" format=\"string|reference\" />\n        <attr name=\"dateHeaderTextColor\" format=\"color|reference\" />\n        <attr name=\"dateHeaderTextSize\" format=\"dimension|reference\" />\n        <attr name=\"dateHeaderTextStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n\n        <attr name=\"isMessageFocusable\" format=\"boolean|reference\" />\n\n    </declare-styleable>\n\n    <attr name=\"DialogsListView\" format=\"reference\" />\n\n    <declare-styleable name=\"DialogsList\">\n        <attr name=\"dialogTitleTextColor\" format=\"color|reference\" />\n        <attr name=\"dialogTitleTextSize\" format=\"dimension|reference\" />\n        <attr name=\"dialogTitleTextStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n        <attr name=\"dialogUnreadTitleTextColor\" format=\"color|reference\" />\n        <attr name=\"dialogUnreadTitleTextStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n\n        <attr name=\"dialogMessageTextColor\" format=\"color|reference\" />\n        <attr name=\"dialogMessageTextSize\" format=\"dimension|reference\" />\n        <attr name=\"dialogMessageTextStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n        <attr name=\"dialogUnreadMessageTextColor\" format=\"color|reference\" />\n        <attr name=\"dialogUnreadMessageTextStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n\n        <attr name=\"dialogDateColor\" format=\"color|reference\" />\n        <attr name=\"dialogDateSize\" format=\"dimension|reference\" />\n        <attr name=\"dialogDateStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n        <attr name=\"dialogUnreadDateColor\" format=\"color|reference\" />\n        <attr name=\"dialogUnreadDateStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n\n        <attr name=\"dialogUnreadBubbleEnabled\" format=\"boolean\" />\n        <attr name=\"dialogUnreadBubbleTextColor\" format=\"color|reference\" />\n        <attr name=\"dialogUnreadBubbleTextSize\" format=\"dimension|reference\" />\n        <attr name=\"dialogUnreadBubbleTextStyle\">\n            <flag name=\"normal\" value=\"0\" />\n            <flag name=\"bold\" value=\"1\" />\n            <flag name=\"italic\" value=\"2\" />\n        </attr>\n        <attr name=\"dialogUnreadBubbleBackgroundColor\" format=\"color|reference\" />\n\n        <attr name=\"dialogAvatarWidth\" format=\"dimension|reference\" />\n        <attr name=\"dialogAvatarHeight\" format=\"dimension|reference\" />\n\n        <attr name=\"dialogMessageAvatarEnabled\" format=\"boolean\" />\n        <attr name=\"dialogMessageAvatarWidth\" format=\"dimension|reference\" />\n        <attr name=\"dialogMessageAvatarHeight\" format=\"dimension|reference\" />\n\n        <attr name=\"dialogDividerEnabled\" format=\"boolean\" />\n        <attr name=\"dialogDividerColor\" format=\"color|reference\" />\n        <attr name=\"dialogDividerLeftPadding\" format=\"dimension|reference\" />\n        <attr name=\"dialogDividerRightPadding\" format=\"dimension|reference\" />\n\n        <attr name=\"dialogItemBackground\" format=\"color|reference\" />\n        <attr name=\"dialogUnreadItemBackground\" format=\"color|reference\" />\n\n    </declare-styleable>\n\n</resources>"
  },
  {
    "path": "chatkit/src/main/res/values/colors.xml",
    "content": "<resources>\n    <color name=\"dark_red\">#ffcc0000</color>\n    <color name=\"light_red\">#ffff4444</color>\n    <color name=\"white\">#ffffff</color>\n    <color name=\"white60\">#a0ffffff</color>\n    <color name=\"white_two\">#efefef</color>\n    <color name=\"white_three\">#ebebeb</color>\n    <color name=\"white_four\">#e0e0e0</color>\n    <color name=\"white_five\">#dadada</color>\n    <color name=\"blue\">#031cd7</color>\n    <color name=\"black\">#000000</color>\n    <color name=\"transparent\">#00000000</color>\n    <color name=\"gray\">#333333</color>\n    <color name=\"dark_gray\">#282a2b</color>\n    <color name=\"dark_grey_two\">#191a1b</color>\n    <color name=\"warm_grey\">#7f7f7f</color>\n    <color name=\"warm_grey_two\">#9c9c9c</color>\n    <color name=\"warm_grey_three\">#8b8b8b</color>\n    <color name=\"warm_grey_four\">#979797</color>\n    <color name=\"dark_mint\">#51c05c</color>\n    <color name=\"cornflower_blue_two\">#4f62d7</color>\n    <color name=\"cornflower_blue_two_24\">#3d4f62d7</color>\n    <color name=\"cornflower_blue_two_dark\">#475bd4</color>\n    <color name=\"cornflower_blue_light_40\">#64bec5f7</color>\n\n    <color name=\"dialog_divider\">@color/white_three</color>\n    <color name=\"dialog_title_text\">@color/dark_gray</color>\n    <color name=\"dialog_message_text\">@color/warm_grey</color>\n    <color name=\"dialog_date_text\">@color/warm_grey_two</color>\n    <color name=\"dialog_unread_text\">@color/white</color>\n    <color name=\"dialog_unread_bubble\">@color/dark_mint</color>\n    <color name=\"tg_selected_bg\">#1EC2DCF2</color>\n\n</resources>"
  },
  {
    "path": "chatkit/src/main/res/values/dimens.xml",
    "content": "<resources>\n    <dimen name=\"input_button_width\">36dp</dimen>\n    <dimen name=\"input_button_height\">36dp</dimen>\n    <dimen name=\"input_button_margin\">16dp</dimen>\n    <dimen name=\"input_text_size\">17sp</dimen>\n    <dimen name=\"input_padding_left\">16sp</dimen>\n    <dimen name=\"input_padding_right\">16sp</dimen>\n    <dimen name=\"input_padding_top\">8sp</dimen>\n    <dimen name=\"input_padding_bottom\">8sp</dimen>\n\n    <dimen name=\"message_bubble_corners_radius\">15dp</dimen>\n\n    <dimen name=\"dialog_avatar_width\">56dp</dimen>\n    <dimen name=\"dialog_avatar_height\">56dp</dimen>\n    <dimen name=\"dialog_last_message_avatar_width\">24dp</dimen>\n    <dimen name=\"dialog_last_message_avatar_height\">24dp</dimen>\n    <dimen name=\"dialog_title_text_size\">18sp</dimen>\n    <dimen name=\"dialog_message_text_size\">16sp</dimen>\n    <dimen name=\"dialog_date_text_size\">14sp</dimen>\n    <dimen name=\"dialog_unread_bubble_text_size\">13sp</dimen>\n    <dimen name=\"dialog_divider_margin_left\">88dp</dimen>\n    <dimen name=\"dialog_divider_margin_right\">0dp</dimen>\n\n    <dimen name=\"message_avatar_width\">40dp</dimen>\n    <dimen name=\"message_avatar_height\">40dp</dimen>\n    <dimen name=\"message_padding_left\">16dp</dimen>\n    <dimen name=\"message_padding_right\">16dp</dimen>\n    <dimen name=\"message_padding_top\">16dp</dimen>\n    <dimen name=\"message_padding_bottom\">16dp</dimen>\n    <!-- Incoming/Outcoming text message size -->\n    <!-- MODIFIED: message_text_size: \"17sp\"->\"14sp\" -->\n    <dimen name=\"message_text_size\">16sp</dimen>\n    <dimen name=\"message_time_text_size\">14sp</dimen>\n    <dimen name=\"message_date_header_text_size\">16sp</dimen>\n    <dimen name=\"message_date_header_padding\">16dp</dimen>\n    <!--<dimen name=\"message_incoming_bubble_margin_right\">40dp</dimen>-->\n    <!-- Make messages to stick exactly to the right edge -->\n    <dimen name=\"message_incoming_bubble_margin_right\">0dp</dimen>\n    <dimen name=\"message_outcoming_bubble_margin_left\">88dp</dimen>\n</resources>"
  },
  {
    "path": "chatkit/src/main/res/values/fonts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"font_fontFamily_medium\">sans-serif</string>\n</resources>"
  },
  {
    "path": "chatkit/src/main/res/values/ids.xml",
    "content": "<resources>\n\n    <!-- MESSAGE LIST ITEM -->\n    <item name=\"bubble\" type=\"id\" />\n    <item name=\"messageText\" type=\"id\" />\n    <item name=\"messageUserAvatar\" type=\"id\" />\n    <item name=\"messageTime\" type=\"id\" />\n    <item name=\"image\" type=\"id\" />\n    <item name=\"imageOverlay\" type=\"id\" />\n\n    <!-- MESSAGE INPUT -->\n    <item name=\"attachmentButton\" type=\"id\" />\n    <item name=\"messageSendButton\" type=\"id\" />\n    <item name=\"attachmentButtonSpace\" type=\"id\" />\n    <item name=\"sendButtonSpace\" type=\"id\" />\n    <item name=\"messageInput\" type=\"id\" />\n\n    <!-- DIALOGS LIST ITEM -->\n    <item name=\"dialogRootLayout\" type=\"id\" />\n    <item name=\"dialogContainer\" type=\"id\" />\n    <item name=\"dialogName\" type=\"id\" />\n    <item name=\"dialogDate\" type=\"id\" />\n    <item name=\"dialogLastMessage\" type=\"id\" />\n    <item name=\"dialogUnreadBubble\" type=\"id\" />\n    <item name=\"dialogAvatar\" type=\"id\" />\n    <item name=\"dialogLastMessageUserAvatar\" type=\"id\" />\n    <item name=\"dialogDividerContainer\" type=\"id\" />\n    <item name=\"dialogDivider\" type=\"id\" />\n\n</resources>"
  },
  {
    "path": "chatkit/src/main/res/values/strings.xml",
    "content": "<resources />\n"
  },
  {
    "path": "chatkit/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"ChatTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n        <item name=\"windowActionModeOverlay\">true</item>\n    </style>\n\n    <style name=\"ChatMessageInfo\">\n        <item name=\"android:textSize\">11sp</item>\n    </style>\n\n    <style name=\"ChatHeaderTitle\">\n        <item name=\"android:textStyle\">bold</item>\n    </style>\n\n    <style name=\"ChatWhoIsTyping\">\n        <item name=\"android:lines\">1</item>\n        <item name=\"android:ellipsize\">end</item>\n    </style>\n\n    <style name=\"ChatSendButton\">\n        <item name=\"android:maxLines\">5</item>\n    </style>\n\n    <style name=\"ChatInputEditText\">\n        <item name=\"android:background\">@null</item>\n        <item name=\"android:textAllCaps\">true</item>\n    </style>\n\n    <style name=\"ChatMessageLayoutOutcoming\">\n        <item name=\"android:paddingLeft\">8dp</item>\n        <item name=\"android:paddingTop\">8dp</item>\n        <item name=\"android:paddingRight\">16dp</item>\n        <item name=\"android:paddingBottom\">4dp</item>\n        <item name=\"android:gravity\">end|right</item>\n        <item name=\"android:layout_marginTop\">4dp</item>\n        <item name=\"android:layout_marginBottom\">4dp</item>\n    </style>\n\n    <style name=\"ChatMessageLayoutIncoming\">\n        <item name=\"android:paddingLeft\">16dp</item>\n        <item name=\"android:paddingTop\">8dp</item>\n        <item name=\"android:paddingRight\">8dp</item>\n        <item name=\"android:paddingBottom\">4dp</item>\n        <item name=\"android:gravity\">end|right</item>\n        <item name=\"android:layout_marginTop\">4dp</item>\n        <item name=\"android:layout_marginBottom\">4dp</item>\n    </style>\n\n    <style name=\"ChatHeaderLayout\">\n        <item name=\"android:padding\">8dp</item>\n        <item name=\"android:layout_marginTop\">4dp</item>\n        <item name=\"android:layout_marginBottom\">4dp</item>\n        <item name=\"android:gravity\">center</item>\n    </style>\n\n</resources>"
  },
  {
    "path": "chatkit/src/main/res/values-v21/fonts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"font_fontFamily_medium\">sans-serif-medium</string>\n</resources>"
  },
  {
    "path": "common/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "common/build.gradle",
    "content": "apply from: gradle.ext.sharedModulesConstants\r\napply plugin: 'kotlin-android'\r\napply plugin: 'com.android.library'\r\n\r\nandroid {\r\n    // FIX: Default interface methods are only supported starting with Android N (--min-api 24)\r\n    compileOptions {\r\n        sourceCompatibility JavaVersion.VERSION_1_8\r\n        targetCompatibility JavaVersion.VERSION_1_8\r\n    }\r\n\r\n    compileSdkVersion project.properties.compileSdkVersion\r\n    buildToolsVersion project.properties.buildToolsVersion\r\n    testOptions.unitTests.includeAndroidResources = true\r\n\r\n    defaultConfig {\r\n        minSdkVersion project.properties.minSdkVersion\r\n        targetSdkVersion project.properties.targetSdkVersion\r\n        versionCode 10\r\n        versionName \"1.0\"\r\n\r\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\r\n        consumerProguardFiles 'consumer-rules.pro'\r\n\r\n        // More info: http://myhexaville.com/2017/03/10/android-multidex/\r\n        // Additionally, you should extend your application from MultiDexApplication\r\n        multiDexEnabled = true\r\n    }\r\n\r\n    buildTypes {\r\n        release {\r\n            minifyEnabled false\r\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\r\n        }\r\n    }\r\n\r\n    lintOptions {\r\n        abortOnError true\r\n        disable 'MissingTranslation'\r\n        disable 'MissingQuantity'\r\n    }\r\n\r\n    // gradle 4.6 migration: disable dimensions mechanism\r\n    // more: https://proandroiddev.com/advanced-android-flavors-part-4-a-new-version-fc2ad80c01bb\r\n    flavorDimensions \"default\"\r\n\r\n    productFlavors {\r\n        stbeta {}\r\n        ststable {}\r\n        stfdroid {}\r\n    }\r\n}\r\n\r\ndependencies {\r\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\r\n\r\n    implementation 'androidx.appcompat:appcompat:' + appCompatXVersion\r\n    testImplementation 'junit:junit:' + junitVersion\r\n    testImplementation 'org.robolectric:robolectric:' + robolectricVersion\r\n\r\n    implementation project(':sharedutils')\r\n    implementation project(':fragment-1.1.0')\r\n    implementation project(':mediaserviceinterfaces')\r\n    implementation project(':youtubeapi')\r\n    implementation project(':filepicker-lib')\r\n\r\n    implementation 'io.reactivex.rxjava2:rxandroid:' + rxAndroidVersion\r\n    implementation 'io.reactivex.rxjava2:rxjava:' + rxJavaVersion\r\n\r\n    //////// BEGIN EXOPLAYER /////////\r\n\r\n    implementation project(':exoplayer-library')\r\n    implementation project(':exoplayer-extension-okhttp')\r\n    implementation project(':exoplayer-extension-cronet')\r\n\r\n    // implementation 'com.amazon.android:exoplayer:' + amazonExoplayerVersion\r\n    // implementation 'com.amazon.android:extension-okhttp:' + amazonExoplayerVersion\r\n\r\n    // implementation 'com.google.android.exoplayer:exoplayer:' + exoplayerVersion\r\n    // implementation 'com.google.android.exoplayer:extension-okhttp:' + exoplayerVersion\r\n\r\n    // implementation 'com.github.amzn:exoplayer-amazon-port:' + amazonExoplayerJitpackVersion\r\n\r\n    //////// END EXOPLAYER //////////\r\n\r\n    implementation 'androidx.media:media:' + mediaXVersion // exoplayer fix\r\n\r\n    implementation 'com.github.bumptech.glide:glide:' + glideVersion\r\n\r\n    implementation 'androidx.work:work-runtime:' + workVersion\r\n    implementation 'com.google.guava:guava:' + guavaVersion // Work library deps\r\n\r\n    implementation 'androidx.browser:browser:' + browserXVersion\r\n\r\n    implementation 'androidx.core:core-ktx:' + kotlinCoreXVersion\r\n    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + kotlinVersion\r\n    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:' + kotlinxVersion\r\n\r\n    implementation 'com.jakewharton:process-phoenix:' + phoenixVersion\r\n\r\n    implementation project(':leanbackassistant')\r\n\r\n    implementation project(':appupdatechecker2')\r\n\r\n    implementation project(':slidableactivity') // https://github.com/r0adkll/Slidr\r\n}\r\n"
  },
  {
    "path": "common/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "common/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\r\n# You can control the set of applied configuration files using the\r\n# proguardFiles setting in build.gradle.\r\n#\r\n# For more details, see\r\n#   http://developer.android.com/guide/developing/tools/proguard.html\r\n\r\n# If your project uses WebView with JS, uncomment the following\r\n# and specify the fully qualified class name to the JavaScript interface\r\n# class:\r\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\r\n#   public *;\r\n#}\r\n\r\n# Uncomment this to preserve the line number information for\r\n# debugging stack traces.\r\n#-keepattributes SourceFile,LineNumberTable\r\n\r\n# If you keep the line number information, uncomment this to\r\n# hide the original source file name.\r\n#-renamesourcefileattribute SourceFile\r\n"
  },
  {
    "path": "common/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\r\n          package=\"com.liskovsoft.smartyoutubetv2.common\">\r\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\r\n    <!-- Android 14 (API 34) -->\r\n    <!-- <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE\" /> -->\r\n    <!-- API level 34 -->\r\n    <!-- <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_DATA_SYNC\" /> -->\r\n    <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\" />\r\n    <uses-permission android:name=\"android.permission.QUICKBOOT_POWERON\" />\r\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>\r\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>\r\n    <!-- Android 10 and up. Start from background permission. Settings/Apps/Draw over other apps (disabled by default) -->\r\n    <!-- https://developer.android.com/guide/components/activities/background-starts -->\r\n    <uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\" />\r\n    <uses-permission android:name=\"android.permission.ACTION_MANAGE_OVERLAY_PERMISSION\" />\r\n    <!-- <uses-permission android:name=\"android.permission.READ_PRIVILEGED_PHONE_STATE\" /> -->\r\n    <!-- Android 9+: needed to run package uninstaller (ATV, Amazon bride) -->\r\n    <uses-permission android:name=\"android.permission.REQUEST_DELETE_PACKAGES\"/>\r\n    <!-- Starting targetSDK 33+, foreground services require explicit permissions in the manifest. -->\r\n    <!-- NOTE: only works with 'mediaPlayback' type service. Others types will fail. -->\r\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK\"/>\r\n\r\n    <queries>\r\n        <!-- Android 11+: access package info (works only for specified apps) -->\r\n        <package android:name=\"com.google.android.youtube.tv\" />\r\n        <package android:name=\"com.amazon.firetv.youtube\" />\r\n        <!-- Android 11+: access the main icon (works for all apps) -->\r\n        <intent>\r\n            <action android:name=\"android.intent.action.MAIN\"/>\r\n        </intent>\r\n    </queries>\r\n\r\n    <!-- Add requestLegacyExternalStorage to fix unavailable storage on Android 10 (doesn't work on Android 11) -->\r\n    <!-- More info: https://stackoverflow.com/questions/63364476/requestlegacyexternalstorage-is-not-working-in-android-11-api-30 -->\r\n    <application android:requestLegacyExternalStorage=\"true\">\r\n        <receiver\r\n            android:name=\"com.liskovsoft.smartyoutubetv2.common.misc.RemoteControlReceiver\"\r\n            android:enabled=\"true\"\r\n            android:exported=\"true\">\r\n            <intent-filter>\r\n                <category android:name=\"android.intent.category.DEFAULT\" />\r\n                <action android:name=\"android.intent.action.BOOT_COMPLETED\" />\r\n                <action android:name=\"android.intent.action.QUICKBOOT_POWERON\" />\r\n                <action android:name=\"android.intent.action.SCREEN_ON\" />\r\n                <!-- For HTC devices -->\r\n                <action android:name=\"com.htc.intent.action.QUICKBOOT_POWERON\" />\r\n                <!-- Fix: Permission Denial: not allowed to send broadcast android.intent.action.BOOT_COMPLETED -->\r\n                <action android:name=\"android.intent.action.ACTION_BOOT_COMPLETED\" />\r\n                <!-- Autostart fix on some devices? -->\r\n                <action android:name=\"android.intent.action.TIME_SET\" />\r\n                <action android:name=\"android.intent.action.TIMEZONE_CHANGED\" />\r\n                <!-- Nvidia Shield autostart fix? -->\r\n                <action android:name=\"android.intent.action.REBOOT\" />\r\n                <action android:name=\"android.intent.action.ACTION_SHUTDOWN\" />\r\n                <action android:name=\"android.intent.action.LOCKED_BOOT_COMPLETED\" />\r\n                <action android:name=\"android.intent.action.ACTION_POWER_CONNECTED\" />\r\n            </intent-filter>\r\n        </receiver>\r\n        <!-- NOTE: Starting from Android 12 (api 31) foreground service with type 'connectedDevice' not supported -->\r\n        <!-- Use 'mediaPlayback' type instead -->\r\n        <service\r\n            android:name=\"com.liskovsoft.smartyoutubetv2.common.misc.RemoteControlService\"\r\n            android:enabled=\"true\"\r\n            android:foregroundServiceType=\"mediaPlayback\"/>\r\n        <service\r\n            android:name=\"com.liskovsoft.smartyoutubetv2.common.misc.BackgroundPlaybackService\"\r\n            android:enabled=\"true\"/>\r\n\r\n        <activity android:name=\".misc.BackupReceiverActivity\"\r\n            android:exported=\"true\">\r\n            <intent-filter>\r\n                <action android:name=\"android.intent.action.SEND\"/>\r\n                <category android:name=\"android.intent.category.DEFAULT\"/>\r\n                <data android:mimeType=\"application/zip\"/>\r\n            </intent-filter>\r\n        </activity>\r\n    </application>\r\n</manifest>\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/data/BrowseSection.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.data;\r\n\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\n\r\npublic class BrowseSection {\r\n    public static final int TYPE_GRID = 0;\r\n    public static final int TYPE_ROW = 1;\r\n    public static final int TYPE_SETTINGS_GRID = 2;\r\n    public static final int TYPE_MULTI_GRID = 3;\r\n    public static final int TYPE_ERROR = 4;\r\n    public static final int TYPE_SHORTS_GRID = 5;\r\n    private static final int MAX_TITLE_LENGTH_CHARS = 30;\r\n    private final int mId;\r\n    private String mTitle;\r\n    private final int mResId;\r\n    private final String mIconUrl;\r\n    private final boolean mIsAuthOnly;\r\n    private final Object mData;\r\n    private boolean mEnabled = true;\r\n    private int mType;\r\n\r\n    public BrowseSection(int id, String title, int type, int resId) {\r\n        this(id, title, type, resId, false);\r\n    }\r\n\r\n    public BrowseSection(int id, String title, int type, String iconUrl) {\r\n        this(id, title, type, iconUrl, false);\r\n    }\r\n\r\n    public BrowseSection(int id, String title, int type, String iconUrl, boolean isAuthOnly) {\r\n        this(id, title, type, -1, iconUrl, isAuthOnly, null);\r\n    }\r\n\r\n    public BrowseSection(int id, String title, int type, String iconUrl, boolean isAuthOnly, Object data) {\r\n        this(id, title, type, -1, iconUrl, isAuthOnly, data);\r\n    }\r\n\r\n    public BrowseSection(int id, String title, int type, int resId, boolean isAuthOnly) {\r\n        this(id, title, type, resId, null, isAuthOnly, null);\r\n    }\r\n\r\n    public BrowseSection(int id, String title, int type, int resId, boolean isAuthOnly, Object data) {\r\n        this(id, title, type, resId, null, isAuthOnly, data);\r\n    }\r\n\r\n    public BrowseSection(int id, String title, int type, int resId, String iconUrl, boolean isAuthOnly, Object data) {\r\n        mId = id;\r\n        mTitle = Helpers.abbreviate(title, MAX_TITLE_LENGTH_CHARS);\r\n        mType = type;\r\n        mResId = resId;\r\n        mIconUrl = iconUrl;\r\n        mIsAuthOnly = isAuthOnly;\r\n        mData = data;\r\n    }\r\n\r\n    public String getTitle() {\r\n        return mTitle;\r\n    }\r\n\r\n    public void setTitle(String title) {\r\n        mTitle = title;\r\n    }\r\n\r\n    public int getId() {\r\n        return mId;\r\n    }\r\n\r\n    public int getType() {\r\n        return mType;\r\n    }\r\n\r\n    public void setType(int type) {\r\n        mType = type;\r\n    }\r\n\r\n    public int getResId() {\r\n        return mResId;\r\n    }\r\n\r\n    public String getIconUrl() {\r\n        return mIconUrl;\r\n    }\r\n\r\n    public boolean isAuthOnly() {\r\n        return mIsAuthOnly;\r\n    }\r\n\r\n    public void setEnabled(boolean enabled) {\r\n        mEnabled = enabled;\r\n    }\r\n\r\n    public boolean isEnabled() {\r\n        return mEnabled;\r\n    }\r\n\r\n    public Object getData() {\r\n        return mData;\r\n    }\r\n\r\n    /**\r\n     * Check reserved ids range for default (built-in) sections\r\n     */\r\n    public boolean isDefault() {\r\n        return mId < 30;\r\n    }\r\n\r\n    @Override\r\n    public boolean equals(@Nullable Object obj) {\r\n        return obj instanceof BrowseSection && ((BrowseSection) obj).getId() == getId();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/data/Playlist.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.data;\n\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Manages a playlist of videos.\n */\npublic class Playlist {\n    private static final int LOW_RAM_PLAYLIST_MAX_SIZE = 50;\n    private static final int HIGH_RAM_PLAYLIST_MAX_SIZE = 300;\n    private final int mPlaylistMaxSize;\n    private final List<Video> mPlaylist;\n    private final List<Video> mSyncedItems;\n    private int mCurrentIndex;\n    private static Playlist sInstance;\n\n    private Playlist() {\n        mPlaylist = new ArrayList<>();\n        mSyncedItems = new ArrayList<>();\n        mCurrentIndex = -1;\n        mPlaylistMaxSize = Utils.isEnoughRam() ? HIGH_RAM_PLAYLIST_MAX_SIZE : LOW_RAM_PLAYLIST_MAX_SIZE;\n    }\n\n    public static Playlist instance() {\n        if (sInstance == null) {\n            sInstance = new Playlist();\n        }\n\n        return sInstance;\n    }\n\n    /**\n     * Clears the videos from the playlist.\n     */\n    public void clear() {\n        mPlaylist.clear();\n        mCurrentIndex = -1;\n    }\n\n    public void clearPosition() {\n        mCurrentIndex = -1;\n    }\n\n    /**\n     * Used to sync list with remotely added items\n     */\n    public void addAll(List<Video> videos) {\n        mPlaylist.removeAll(videos);\n        mPlaylist.addAll(videos);\n    }\n\n    /**\n     * Adds a video to the end of the playlist.\n     *\n     * @param video to be added to the playlist.\n     */\n    public void add(Video video) {\n        if (Video.isEmpty(video)) {\n            return;\n        }\n\n        Video current = getCurrent();\n\n        // Skip add currently playing item\n        // And replace to correct position sync in fragments\n        if (video.equals(current)) {\n            replace(current, video);\n            return;\n        }\n\n        boolean isLastElement = !mPlaylist.isEmpty() && video.equals(mPlaylist.get(mPlaylist.size() - 1));\n\n        remove(video);\n\n        mPlaylist.add(video);\n\n        // Replacing last element? Increase index then.\n        if (isLastElement) {\n            mCurrentIndex++;\n        }\n\n        // Video opened from the browser or suggestions.\n        // In this case remove all next items.\n        trimPlaylist();\n        stripPrevItem();\n    }\n\n    public void next(Video video) {\n        if (Video.isEmpty(video)) {\n            return;\n        }\n\n        remove(video);\n\n        int nextIdx = mPlaylist.size() > mCurrentIndex ? mCurrentIndex + 1 : mPlaylist.size() - 1;\n\n        // IndexOutOfBoundsException fix\n        if (nextIdx < 0) {\n            return;\n        }\n\n        mPlaylist.add(nextIdx, video);\n\n        // Video opened from the browser or suggestions.\n        // In this case remove all next items.\n        trimPlaylist();\n        stripPrevItem();\n    }\n\n    public void remove(Video video) {\n        if (Video.isEmpty(video)) {\n            return;\n        }\n\n        // Skip remove currently playing item\n        //if (video.equals(getCurrent())) {\n        //    return;\n        //}\n\n        int index = mPlaylist.indexOf(video);\n\n        // If contains\n        if (index >= 0) {\n            mPlaylist.remove(video);\n\n            // Shift video stack index if needed\n            // Don't remove current index. Except this is the last element.\n            // Give a chance to replace current element.\n            if (index < mCurrentIndex) {\n                mCurrentIndex--;\n            }\n\n            // Index out of bounds as the result of previous operation.\n            // Select last element in this case.\n            if (mCurrentIndex >= mPlaylist.size()) {\n                mCurrentIndex = mPlaylist.size() - 1;\n            }\n        }\n    }\n\n    public boolean contains(Video video) {\n        if (Video.isEmpty(video)) {\n            return false;\n        }\n\n        return mPlaylist.contains(video);\n    }\n\n    public boolean containsAfterCurrent(Video video) {\n        if (Video.isEmpty(video)) {\n            return false;\n        }\n\n        List<Video> afterCurrent = getAllAfterCurrent();\n\n        return afterCurrent != null && afterCurrent.contains(video);\n    }\n\n    ///**\n    // * Trim playlist if one exceeds needed size or current element not last in the list\n    // */\n    //private void trimPlaylist() {\n    //    int fromIndex = 0;\n    //    int toIndex = mCurrentIndex + 1;\n    //\n    //    boolean isLastElement = mCurrentIndex == (mPlaylist.size() - 1);\n    //    boolean playlistTooBig = mPlaylist.size() > PLAYLIST_MAX_SIZE;\n    //\n    //    if (playlistTooBig) {\n    //        fromIndex = mPlaylist.size() - PLAYLIST_MAX_SIZE;\n    //    }\n    //\n    //    if (!isLastElement || playlistTooBig) {\n    //        mPlaylist = mPlaylist.subList(fromIndex, toIndex);\n    //        mCurrentIndex = mPlaylist.size() - 1;\n    //    }\n    //}\n\n    /**\n     * Moves to the next video in the playlist. If already at the end of the playlist, null will\n     * be returned and the position will not change.\n     *\n     * @return The next video in the playlist.\n     */\n    public Video getNext() {\n        if (mCurrentIndex >= 0 && (mCurrentIndex + 1) < mPlaylist.size()) {\n            return mPlaylist.get(mCurrentIndex + 1);\n        }\n\n        return null;\n    }\n\n    /**\n     * Moves to the previous video in the playlist. If the playlist is already at the beginning,\n     * null will be returned and the position will not change.\n     *\n     * @return The previous video in the playlist.\n     */\n    public Video getPrevious() {\n        if ((mCurrentIndex - 1) >= 0) {\n            return mPlaylist.get(mCurrentIndex - 1);\n        }\n\n        return null;\n    }\n\n    public void setCurrent(Video video) {\n        if (Video.isEmpty(video)) {\n            return;\n        }\n\n        int currentPosition = mPlaylist.indexOf(video);\n\n        if (currentPosition >= 0) {\n            mCurrentIndex = currentPosition;\n        } else {\n            add(video);\n            mCurrentIndex = mPlaylist.size() - 1;\n        }\n    }\n\n    public Video getCurrent() {\n        if (mCurrentIndex < mPlaylist.size() && mCurrentIndex >= 0) {\n            return mPlaylist.get(mCurrentIndex);\n        }\n\n        return null;\n    }\n\n    public List<Video> getAll() {\n        return Collections.unmodifiableList(mPlaylist);\n    }\n\n    public List<Video> getChangedItems() {\n        return Collections.unmodifiableList(mSyncedItems);\n    }\n\n    public boolean hasNext() {\n        return getNext() != null;\n    }\n\n    public List<Video> getAllAfterCurrent() {\n        if (mCurrentIndex == -1) {\n            return mPlaylist;\n        }\n\n        int fromIndex = mCurrentIndex + 1;\n        if (fromIndex > 0 && fromIndex < mPlaylist.size()) {\n            return mPlaylist.subList(fromIndex, mPlaylist.size());\n        }\n\n        return null;\n    }\n\n    public void removeAllAfterCurrent() {\n        if (mCurrentIndex == -1) {\n            return;\n        }\n\n        int fromIndex = mCurrentIndex + 1;\n        int size = mPlaylist.size();\n        if (fromIndex > 0 && fromIndex < size) {\n            //mPlaylist = mPlaylist.subList(0, fromIndex);\n            mPlaylist.subList(fromIndex, size).clear();\n        }\n    }\n\n    /**\n     * Trim playlist if one exceeds needed size or current element not last in the list\n     */\n    private void trimPlaylist() {\n        int size = mPlaylist.size();\n        boolean playlistTooBig = size > mPlaylistMaxSize;\n\n        if (playlistTooBig) {\n            int toIndex = size - mPlaylistMaxSize;\n            mPlaylist.subList(0, toIndex).clear();\n            mCurrentIndex -= toIndex;\n        }\n    }\n\n    /**\n     * Do some cleanup to prevent possible OOM exception\n     */\n    private void stripPrevItem() {\n        if (mCurrentIndex == -1) {\n            return;\n        }\n\n        int prevPosition = mCurrentIndex - 1;\n\n        if (prevPosition < mPlaylist.size() && prevPosition >= 0) {\n            Video prevItem = mPlaylist.get(prevPosition);\n            if (prevItem != null) {\n                prevItem.mediaItem = null;\n                prevItem.nextMediaItem = null;\n                prevItem.shuffleMediaItem = null;\n            }\n        }\n    }\n\n    private void replace(Video origin, Video newItem) {\n        int index = mPlaylist.indexOf(origin);\n\n        if (index != -1) {\n            mPlaylist.set(index, newItem);\n        }\n    }\n\n    public void onNewSession() {\n        mSyncedItems.clear();\n    }\n\n    /**\n     * Since all items are clones (saves memory) we need to sync sometimes.\n     */\n    public void sync(Video origin) {\n        if (origin == null) {\n            return;\n        }\n\n        // Sync to maintain order. Item may be cloned.\n        for (Video video : mPlaylist) {\n            if (video.equals(origin)) {\n                video.sync(origin);\n                break;\n            }\n        }\n\n        // Replace if exists. Item may be cloned.\n        mSyncedItems.remove(origin);\n        mSyncedItems.add(origin);\n    }\n}"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/data/SettingsGroup.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.data;\r\n\r\nimport java.util.List;\r\n\r\npublic class SettingsGroup {\r\n    private List<SettingsItem> mItems;\r\n    private BrowseSection mCategory;\r\n\r\n    public static SettingsGroup from(List<SettingsItem> items, BrowseSection category) {\r\n        SettingsGroup settingsGroup = new SettingsGroup();\r\n        settingsGroup.mItems = items;\r\n        settingsGroup.mCategory = category;\r\n\r\n        return settingsGroup;\r\n    }\r\n\r\n    public List<SettingsItem> getItems() {\r\n        return mItems;\r\n    }\r\n\r\n    public BrowseSection getCategory() {\r\n        return mCategory;\r\n    }\r\n\r\n    public boolean isEmpty() {\r\n        return mItems == null || mItems.size() == 0;\r\n    }\r\n\r\n    public String getTitle() {\r\n        return mCategory.getTitle();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/data/SettingsItem.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.data;\r\n\r\npublic class SettingsItem {\r\n    public final String title;\r\n    public final Runnable onClick;\r\n    public int imageResId;\r\n\r\n    public SettingsItem(String title, Runnable onClick) {\r\n        this(title, onClick, -1);\r\n    }\r\n\r\n    public SettingsItem(String title, Runnable onClick, int imageResId) {\r\n        this.title = title;\r\n        this.onClick = onClick;\r\n        this.imageResId = imageResId;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/data/SimpleMediaItem.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.data;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItem;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\r\n\r\npublic final class SimpleMediaItem implements MediaItem {\r\n    private int mId;\r\n    private String mVideoId;\r\n    private String mPlaylistId;\r\n    private String mTitle;\r\n    private String mReloadPageKey;\r\n    private String mChannelId;\r\n    private String mCardImageUrl;\r\n    private String mParams;\r\n    private CharSequence mSecondTitle;\r\n    private String mContentType;\r\n    private int mType;\r\n    private String mBackgroundImageUrl;\r\n    private String mAuthor;\r\n    private int mPercentWatched;\r\n    private int mStartTimeSeconds;\r\n    private String mBadgeText;\r\n    private boolean mHaseNewContent;\r\n    private String mVideoPreviewUrl;\r\n    private int mPlaylistIndex;\r\n    private boolean mIsLive;\r\n    private boolean mIsUpcoming;\r\n    private boolean mIsMovie;\r\n    private String mClickTrackingParams;\r\n\r\n    private SimpleMediaItem() {\r\n    }\r\n\r\n    public static MediaItem from(MediaItemMetadata metadata) {\r\n        SimpleMediaItem mediaItem = new SimpleMediaItem();\r\n\r\n        mediaItem.mTitle = metadata.getTitle();\r\n        mediaItem.mSecondTitle = metadata.getSecondTitle();\r\n        mediaItem.mVideoId = metadata.getVideoId();\r\n        mediaItem.mPlaylistId = metadata.getPlaylistInfo() != null ?\r\n                metadata.getPlaylistInfo().getPlaylistId() : metadata.getNextVideo() != null ?\r\n                metadata.getNextVideo().getPlaylistId() : null;\r\n        mediaItem.mParams = metadata.getParams();\r\n        mediaItem.mChannelId = metadata.getChannelId();\r\n\r\n        return mediaItem;\r\n    }\r\n\r\n    public static MediaItem from(Video video) {\r\n        SimpleMediaItem mediaItem = new SimpleMediaItem();\r\n\r\n        mediaItem.mId = video.id;\r\n        mediaItem.mTitle = video.getTitle();\r\n        mediaItem.mSecondTitle = video.getSecondTitle();\r\n        mediaItem.mContentType = video.category;\r\n        mediaItem.mType = video.itemType;\r\n        mediaItem.mVideoId = video.videoId;\r\n        mediaItem.mChannelId = video.channelId;\r\n        mediaItem.mBackgroundImageUrl = video.bgImageUrl;\r\n        mediaItem.mCardImageUrl = video.cardImageUrl;\r\n        mediaItem.mAuthor = video.author;\r\n        mediaItem.mPercentWatched = (int) video.percentWatched;\r\n        mediaItem.mStartTimeSeconds = video.startTimeSeconds;\r\n        mediaItem.mBadgeText = video.badge;\r\n        mediaItem.mHaseNewContent = video.hasNewContent;\r\n        mediaItem.mVideoPreviewUrl = video.previewUrl;\r\n        mediaItem.mPlaylistId = video.playlistId;\r\n        mediaItem.mPlaylistIndex = video.playlistIndex;\r\n        mediaItem.mParams = video.playlistParams;\r\n        mediaItem.mReloadPageKey = video.reloadPageKey;\r\n        mediaItem.mIsLive = video.isLive;\r\n        mediaItem.mIsUpcoming = video.isUpcoming;\r\n        mediaItem.mIsMovie = video.isMovie;\r\n        mediaItem.mClickTrackingParams = video.clickTrackingParams;\r\n\r\n        return mediaItem;\r\n    }\r\n\r\n    @Override\r\n    public int getType() {\r\n        return mType;\r\n    }\r\n\r\n    @Override\r\n    public boolean isLive() {\r\n        return mIsLive;\r\n    }\r\n\r\n    @Override\r\n    public boolean isUpcoming() {\r\n        return mIsUpcoming;\r\n    }\r\n\r\n    @Override\r\n    public boolean isShorts() {\r\n        return false;\r\n    }\r\n\r\n    @Override\r\n    public boolean isMovie() {\r\n        return mIsMovie;\r\n    }\r\n\r\n    @Override\r\n    public int getPercentWatched() {\r\n        return mPercentWatched;\r\n    }\r\n\r\n    @Override\r\n    public int getStartTimeSeconds() {\r\n        return mStartTimeSeconds;\r\n    }\r\n\r\n    @Override\r\n    public String getAuthor() {\r\n        return mAuthor;\r\n    }\r\n\r\n    @Override\r\n    public String getFeedbackToken() {\r\n        return null;\r\n    }\r\n\r\n    @Override\r\n    public String getFeedbackToken2() {\r\n        return null;\r\n    }\r\n\r\n    @Override\r\n    public String getPlaylistId() {\r\n        return mPlaylistId;\r\n    }\r\n\r\n    @Override\r\n    public int getPlaylistIndex() {\r\n        return mPlaylistIndex;\r\n    }\r\n\r\n    @Override\r\n    public String getParams() {\r\n        return mParams;\r\n    }\r\n\r\n    @Override\r\n    public String getReloadPageKey() {\r\n        return mReloadPageKey;\r\n    }\r\n\r\n    @Override\r\n    public boolean hasNewContent() {\r\n        return mHaseNewContent;\r\n    }\r\n\r\n    @Override\r\n    public int getId() {\r\n        return mId;\r\n    }\r\n\r\n    @Override\r\n    public String getTitle() {\r\n        return mTitle;\r\n    }\r\n\r\n    @Override\r\n    public CharSequence getSecondTitle() {\r\n        return mSecondTitle;\r\n    }\r\n\r\n    @Override\r\n    public String getVideoId() {\r\n        return mVideoId;\r\n    }\r\n\r\n    @Override\r\n    public String getContentType() {\r\n        return mContentType;\r\n    }\r\n\r\n    @Override\r\n    public long getDurationMs() {\r\n        return 0;\r\n    }\r\n\r\n    @Override\r\n    public String getBadgeText() {\r\n        return mBadgeText;\r\n    }\r\n\r\n    @Override\r\n    public String getProductionDate() {\r\n        return null;\r\n    }\r\n\r\n    @Override\r\n    public long getPublishedDate() {\r\n        return 0;\r\n    }\r\n\r\n    @Override\r\n    public String getCardImageUrl() {\r\n        return mCardImageUrl;\r\n    }\r\n\r\n    @Override\r\n    public String getBackgroundImageUrl() {\r\n        return mBackgroundImageUrl;\r\n    }\r\n\r\n    @Override\r\n    public int getWidth() {\r\n        return 0;\r\n    }\r\n\r\n    @Override\r\n    public int getHeight() {\r\n        return 0;\r\n    }\r\n\r\n    @Override\r\n    public String getChannelId() {\r\n        return mChannelId;\r\n    }\r\n\r\n    @Override\r\n    public String getVideoPreviewUrl() {\r\n        return mVideoPreviewUrl;\r\n    }\r\n\r\n    @Override\r\n    public String getAudioChannelConfig() {\r\n        return null;\r\n    }\r\n\r\n    @Override\r\n    public String getPurchasePrice() {\r\n        return null;\r\n    }\r\n\r\n    @Override\r\n    public String getRentalPrice() {\r\n        return null;\r\n    }\r\n\r\n    @Override\r\n    public int getRatingStyle() {\r\n        return 0;\r\n    }\r\n\r\n    @Override\r\n    public double getRatingScore() {\r\n        return 0;\r\n    }\r\n\r\n    @Override\r\n    public boolean hasUploads() {\r\n        return false;\r\n    }\r\n\r\n    @Override\r\n    public String getClickTrackingParams() {\r\n        return mClickTrackingParams;\r\n    }\r\n\r\n    @Override\r\n    public String getSearchQuery() {\r\n        return null;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/data/Video.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.data;\n\nimport android.content.Context;\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.liskovsoft.mediaserviceinterfaces.data.ItemGroup;\nimport com.liskovsoft.mediaserviceinterfaces.data.ChapterItem;\nimport com.liskovsoft.mediaserviceinterfaces.data.DislikeData;\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItem;\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemFormatInfo;\nimport com.liskovsoft.mediaserviceinterfaces.data.ItemGroup.Item;\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\nimport com.liskovsoft.mediaserviceinterfaces.data.NotificationState;\nimport com.liskovsoft.mediaserviceinterfaces.data.PlaylistInfo;\nimport com.liskovsoft.sharedutils.helpers.DateHelper;\nimport com.liskovsoft.sharedutils.helpers.Helpers;\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService;\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\nimport com.liskovsoft.googlecommon.common.helpers.ServiceHelper;\nimport com.liskovsoft.googlecommon.common.helpers.YouTubeHelper;\n\nimport java.lang.ref.WeakReference;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Video is an object that holds the various metadata associated with a single video.\n */\npublic final class Video {\n    public static final String PLAYLIST_LIKED_MUSIC = \"LM\";\n    public static final String TERTIARY_TEXT_DELIM = \"•\";\n    public static final long MAX_LIVE_DURATION_MS = 24 * 60 * 60 * 1_000;\n    private static final int MAX_AUTHOR_LENGTH_CHARS = 20;\n    private static final String BLACK_PLACEHOLDER_URL = \"https://via.placeholder.com/1280x720/000000/000000\";\n    private static final float RESTORE_POSITION_PERCENTS = 10; // min value for immediately closed videos\n    public int id;\n    public String title;\n    public String deArrowTitle;\n    public CharSequence secondTitle;\n    private String metadataTitle;\n    private CharSequence metadataSecondTitle;\n    public String description;\n    public String category;\n    public int itemType = -1;\n    public String channelId;\n    public String videoId;\n    public String playlistId;\n    public String remotePlaylistId;\n    public int playlistIndex = -1;\n    public String playlistParams;\n    public String reloadPageKey;\n    public String bgImageUrl;\n    public String cardImageUrl;\n    public String altCardImageUrl;\n    public String author;\n    public String badge;\n    public String previewUrl;\n    public float percentWatched = -1;\n    public int startTimeSeconds;\n    public MediaItem mediaItem;\n    public MediaItem nextMediaItem;\n    public MediaItem shuffleMediaItem;\n    public PlaylistInfo playlistInfo;\n    public boolean hasNewContent;\n    public boolean isLive;\n    public boolean isUpcoming;\n    public boolean isUnplayable;\n    public boolean isShorts;\n    public boolean isChapter;\n    public boolean isMovie;\n    public boolean isSubscribed;\n    public boolean isRemote;\n    public int groupPosition = -1; // group position in multi-grid fragments\n    public String clickTrackingParams;\n    public boolean isSynced;\n    public final long timestamp = System.currentTimeMillis();\n    public int sectionId = -1;\n    public String channelGroupId;\n    public long startTimeMs;\n    public long pendingPosMs;\n    public boolean fromQueue;\n    public boolean isPending;\n    public boolean finishOnEnded;\n    public boolean incognito;\n    public String likeCount;\n    public String dislikeCount;\n    public String subscriberCount;\n    public float volume = 1.0f;\n    public boolean deArrowProcessed;\n    public boolean isLiveEnd;\n    public boolean isShuffled;\n    public String searchQuery;\n    private int startSegmentNum;\n    private long liveDurationMs = -1;\n    private long durationMs = -1;\n    private WeakReference<VideoGroup> group; // Memory leak fix. Used to get next page when scrolling.\n    public List<NotificationState> notificationStates;\n\n    public Video() {\n       // NOP\n    }\n\n    private Video(\n            final int id,\n            final String category,\n            final String title,\n            final String desc,\n            final String videoId,\n            final String bgImageUrl,\n            final String cardImageUrl,\n            final String author) {\n        this.id = id;\n        this.category = category;\n        this.title = title;\n        this.secondTitle = desc;\n        this.videoId = videoId;\n        this.bgImageUrl = bgImageUrl;\n        this.cardImageUrl = cardImageUrl;\n        this.author = author;\n    }\n\n    public static Video from(MediaItem item) {\n        if (item == null) {\n            return null;\n        }\n\n        Video video = new Video();\n\n        video.id = item.getId();\n        video.title = item.getTitle();\n        video.secondTitle = item.getSecondTitle();\n        video.category = item.getContentType();\n        video.itemType = item.getType();\n        video.videoId = item.getVideoId();\n        video.channelId = item.getChannelId();\n        video.bgImageUrl = item.getBackgroundImageUrl();\n        video.cardImageUrl = item.getCardImageUrl();\n        video.author = item.getAuthor();\n        video.percentWatched = item.getPercentWatched();\n        video.startTimeSeconds = item.getStartTimeSeconds();\n        video.badge = item.getBadgeText();\n        video.hasNewContent = item.hasNewContent();\n        video.previewUrl = item.getVideoPreviewUrl();\n        video.playlistId = item.getPlaylistId();\n        video.playlistIndex = item.getPlaylistIndex();\n        video.playlistParams = item.getParams();\n        video.reloadPageKey = item.getReloadPageKey();\n        video.isLive = item.isLive();\n        video.isUpcoming = item.isUpcoming();\n        video.isShorts = item.isShorts();\n        video.isMovie = item.isMovie();\n        video.clickTrackingParams = item.getClickTrackingParams();\n        video.durationMs = item.getDurationMs();\n        video.searchQuery = item.getSearchQuery();\n        video.mediaItem = item;\n\n        return video;\n    }\n\n    public static Video from(Video item) {\n        Video video = new Video();\n\n        video.id = item.id;\n        video.title = item.title;\n        video.category = item.category;\n        video.itemType = item.itemType;\n        video.secondTitle = item.secondTitle;\n        video.videoId = item.videoId;\n        video.channelId = item.channelId;\n        video.bgImageUrl = item.bgImageUrl;\n        video.cardImageUrl = item.cardImageUrl;\n        video.author = item.author;\n        video.percentWatched = item.percentWatched;\n        video.badge = item.badge;\n        video.hasNewContent = item.hasNewContent;\n        video.previewUrl = item.previewUrl;\n        video.playlistId = item.playlistId;\n        video.playlistIndex = item.playlistIndex;\n        video.playlistParams = item.playlistParams;\n        video.reloadPageKey = item.getReloadPageKey();\n        video.isLive = item.isLive;\n        video.isUpcoming = item.isUpcoming;\n        video.clickTrackingParams = item.clickTrackingParams;\n        video.mediaItem = item.mediaItem;\n        video.group = item.group;\n\n        return video;\n    }\n\n    public static Video from(String videoId) {\n        Video video = new Video();\n        video.videoId = videoId;\n        return video;\n    }\n\n    public static Video from(ChapterItem chapter) {\n        Video video = new Video();\n        video.isChapter = true;\n        video.title = chapter.getTitle();\n        video.cardImageUrl = chapter.getCardImageUrl();\n        video.startTimeMs = chapter.getStartTimeMs();\n        video.badge = ServiceHelper.millisToTimeText(chapter.getStartTimeMs());\n        return video;\n    }\n\n    public static Video from(ItemGroup group) {\n        Video video = new Video();\n        video.title = group.getTitle();\n        video.cardImageUrl = group.getIconUrl();\n        video.channelGroupId = group.getId();\n        return video;\n    }\n\n    public static Video from(Item channel) {\n        Video video = new Video();\n        video.title = channel.getTitle();\n        video.cardImageUrl = channel.getIconUrl();\n        video.channelId = channel.getChannelId();\n        return video;\n    }\n    ///**\n    // * Don't change the logic from equality by reference!<br/>\n    // * Or adapters won't work properly (same video may appear twice).\n    // */\n    //@Override\n    //public boolean equals(@Nullable Object obj) {\n    //    return super.equals(obj);\n    //}\n\n    /**\n     * Use with caution.<br/>\n     * Old logic is equality by reference!<br/>\n     * Adapters may not work properly when detecting scroll position (same video may appear twice).\n     */\n    @Override\n    public boolean equals(@Nullable Object obj) {\n        if (obj instanceof Video) {\n            Video video = (Video) obj;\n\n            return hashCode() == video.hashCode() && isMix() == video.isMix();\n        }\n\n        return false;\n    }\n\n    /**\n     * NOTE: hashCode is used generate id that should be the same if contents of items is the same\n     */\n    @Override\n    public int hashCode() {\n        // NOTE: With full hash code won't jump to last known position\n        int hashCode = Helpers.hashCodeAny(videoId, playlistId, reloadPageKey, playlistParams, channelId, sectionId, channelGroupId, mediaItem);\n        return hashCode != -1 ? hashCode : super.hashCode();\n    }\n\n    public static void printDebugInfo(Context context, Video item) {\n        MessageHelpers.showLongMessage(context,\n                String.format(\"videoId=%s, playlistId=%s, reloadPageKey=%s, playlistParams=%s, channelId=%s, mediaItem=%s, extra=%s\",\n                        item.videoId, item.playlistId, item.reloadPageKey, item.playlistParams, item.channelId, item.mediaItem, item.sectionId)\n        );\n    }\n    \n    public static boolean equals(Video video1, Video video2) {\n        if (video1 == null) {\n            return false;\n        }\n\n        return video1.equals(video2);\n    }\n\n    public static boolean isEmpty(Video video) {\n        return video == null || video.videoId == null;\n    }\n\n    public int getId() {\n        if (id == 0 || id == -1) {\n            id = hashCode();\n        }\n\n        return id;\n    }\n\n    public String getTitle() {\n        return deArrowTitle != null ? deArrowTitle : title;\n    }\n\n    public String getTitleFull() {\n        return deArrowTitle != null ? deArrowTitle : metadataTitle != null ? metadataTitle : title;\n    }\n\n    public CharSequence getSecondTitle() {\n        return secondTitle;\n    }\n\n    public CharSequence getSecondTitleFull() {\n        // Don't sync future translation because of not precise info\n        return metadataSecondTitle != null && !isUpcoming ? metadataSecondTitle : secondTitle;\n    }\n\n    public String getPlaylistId() {\n        return isRemote && remotePlaylistId != null ? remotePlaylistId : playlistId;\n    }\n\n    public String getCardImageUrl() {\n        return altCardImageUrl != null ? altCardImageUrl : cardImageUrl;\n    }\n\n    public String getAuthor() {\n        if (author != null) {\n            return author;\n        }\n\n        String mainTitle = metadataTitle != null ? metadataTitle : title;\n        CharSequence subtitle = metadataSecondTitle != null ? metadataSecondTitle : secondTitle;\n        return hasVideo() ? extractAuthor(subtitle) : Helpers.toString(YouTubeHelper.createInfo(mainTitle, subtitle)); // BAD idea\n    }\n\n    public VideoGroup getGroup() {\n        return group != null ? group.get() : null;\n    }\n\n    public void setGroup(VideoGroup group) {\n        this.group = new WeakReference<>(group);\n    }\n\n    public int getPositionInsideGroup() {\n        return getGroup() != null && !getGroup().isEmpty() ? getGroup().getVideos().indexOf(this) : -1;\n    }\n\n    private static String extractAuthor(CharSequence secondTitle) {\n        return extractAuthor(Helpers.toString(secondTitle));\n    }\n\n    private static String extractAuthor(String secondTitle) {\n        String result = null;\n\n        if (secondTitle != null) {\n            secondTitle = secondTitle.replace(TERTIARY_TEXT_DELIM + \" LIVE\", \"\"); // remove special marks\n            String[] split = secondTitle.split(TERTIARY_TEXT_DELIM);\n\n            if (split.length <= 1) {\n                result = secondTitle;\n            } else {\n                // First part may be a special label (4K, Stream, New etc)\n                // Two cases to detect label: 1) Description is long (4 items); 2) First item of info is too short (2 letters)\n                result = split.length < 4 && split[0].trim().length() > 2 ? split[0] : split[1];\n            }\n        }\n\n        // Skip subtitles starting with number of views (e.g. 1.4M views)\n        return !TextUtils.isEmpty(result) && !Helpers.isNumeric(result.substring(0, 1)) ? Helpers.abbreviate(result.trim(), MAX_AUTHOR_LENGTH_CHARS) : null;\n    }\n\n    public static List<Video> findVideosByAuthor(VideoGroup group, String author) {\n        List<Video> result = new ArrayList<>();\n\n        if (group != null && group.getVideos() != null) {\n            for (Video video : group.getVideos()) {\n                if (Helpers.equals(video.getAuthor(), author)) {\n                    result.add(video);\n                }\n            }\n        }\n\n        return result;\n    }\n\n    public int describeContents() {\n        return 0;\n    }\n\n    public static Video fromString(String spec) {\n        if (spec == null) {\n            return null;\n        }\n\n        String[] split = Helpers.splitObj(spec);\n\n        // 'playlistParams' backward compatibility\n        if (split.length == 10) {\n            split = Helpers.appendArray(split, new String[]{null});\n        }\n\n        // 'extra' backward compatibility\n        if (split.length == 11) {\n            split = Helpers.appendArray(split, new String[]{\"-1\"});\n        }\n\n        // 'reloadPageKey' backward compatibility\n        if (split.length == 12) {\n            split = Helpers.appendArray(split, new String[]{null});\n        }\n\n        // 'type' backward compatibility\n        if (split.length == 13) {\n            split = Helpers.appendArray(split, new String[]{\"-1\"});\n        }\n\n        if (split.length == 14) {\n            split = Helpers.appendArray(split, new String[]{null});\n        }\n\n        if (split.length == 15) {\n            split = Helpers.appendArray(split, new String[]{null});\n        }\n\n        if (split.length == 16) {\n            split = Helpers.appendArray(split, new String[]{\"-1\"});\n        }\n\n        if (split.length == 17) {\n            split = Helpers.appendArray(split, new String[]{null});\n        }\n\n        if (split.length == 18) {\n            split = Helpers.appendArray(split, new String[]{null});\n        }\n\n        if (split.length == 19) {\n            split = Helpers.appendArray(split, new String[]{null});\n        }\n\n        if (split.length == 20) {\n            split = Helpers.appendArray(split, new String[]{\"false\"});\n        }\n\n        if (split.length == 21) {\n            split = Helpers.appendArray(split, new String[]{\"-1\"});\n        }\n\n        if (split.length == 22) {\n            split = Helpers.appendArray(split, new String[]{null});\n        }\n\n        if (split.length != 23) {\n            return null;\n        }\n\n        Video result = new Video();\n\n        result.id = Helpers.parseInt(split[0]);\n        result.category = Helpers.parseStr(split[1]);\n        result.title = Helpers.parseStr(split[2]);\n        result.videoId = Helpers.parseStr(split[3]);\n        //result.videoUrl = Helpers.parseStr(split[4]);\n        result.playlistId = Helpers.parseStr(split[5]);\n        result.channelId = Helpers.parseStr(split[6]);\n        result.bgImageUrl = Helpers.parseStr(split[7]);\n        result.cardImageUrl = Helpers.parseStr(split[8]);\n        //result.mediaItem = YouTubeMediaItem.deserializeMediaItem(Helpers.parseStr(split[9]));\n        result.playlistParams = Helpers.parseStr(split[10]);\n        result.sectionId = Helpers.parseInt(split[11]);\n        result.reloadPageKey = Helpers.parseStr(split[12]);\n        result.itemType = Helpers.parseInt(split[13]);\n        result.secondTitle = Helpers.parseStr(split[14]);\n        result.previewUrl = Helpers.parseStr(split[15]);\n        result.percentWatched = Helpers.parseFloat(split[16]);\n        result.metadataTitle = Helpers.parseStr(split[17]);\n        result.metadataSecondTitle = Helpers.parseStr(split[18]);\n        result.badge = Helpers.parseStr(split[19]);\n        result.isLive = Helpers.parseBoolean(split[20]);\n        result.channelGroupId = Helpers.parseStr(split[21]);\n        result.searchQuery = Helpers.parseStr(split[22]);\n\n        // Reset old type (int)\n        if (Helpers.equals(result.channelGroupId, \"-1\")) {\n            result.channelGroupId = null;\n        }\n\n        return result;\n    }\n\n    @NonNull\n    @Override\n    public String toString() {\n        return Helpers.mergeObj(id, category, title, videoId, null, playlistId, channelId, bgImageUrl, cardImageUrl,\n                null, playlistParams, sectionId, getReloadPageKey(), itemType, secondTitle, previewUrl, percentWatched,\n                metadataTitle, metadataSecondTitle, badge, isLive, channelGroupId, searchQuery);\n    }\n\n    public boolean hasVideo() {\n        return videoId != null;\n    }\n\n    public boolean hasChannel() {\n        return channelId != null;\n    }\n\n    /**\n     * NOTE: Channels section uses <em>playlistParams</em> instead of <em>playlistId</em>\n     */\n    public boolean hasPlaylist() {\n        return playlistId != null;\n    }\n\n    //public boolean hasPlaylist() {\n    //    return playlistId != null || (playlistParams != null && !Helpers.containsAny(playlistParams, sNotPlaylistParams));\n    //}\n\n    public boolean hasNextPlaylist() {\n        return hasNextItem() && getPlaylistId() != null && getPlaylistId().equals(nextMediaItem.getPlaylistId());\n    }\n\n    /**\n     * Persist on Channels and User playlists sections\n     */\n    public boolean hasReloadPageKey() {\n        return getReloadPageKey() != null;\n    }\n\n    public boolean hasNextPageKey() {\n        return getNextPageKey() != null;\n    }\n\n    public boolean hasNextItem() {\n        return nextMediaItem != null;\n    }\n\n    public boolean hasNestedItems() {\n        return mediaItem != null && mediaItem.hasUploads();\n    }\n\n    public boolean hasPlaylistIndex() {\n        return playlistIndex > 0;\n    }\n\n    public boolean isChannel() {\n        return videoId == null && playlistId == null && channelId != null;\n    }\n\n    /**\n     * Special type of channels that work as playlist\n     */\n    public boolean isPlaylistAsChannel() {\n        return videoId == null && channelId != null && itemType == MediaItem.TYPE_PLAYLIST;\n    }\n\n    /**\n     * A single playlist item inside a channel. Usually has a badge (e.g. 20 videos)\n     */\n    public boolean isBadgePlaylistInChannel() {\n        return belongsToChannel() && hasPlaylist() && !belongsToSamePlaylistGroup();\n    }\n\n    public boolean isMix() {\n        // NOTE: don't check for 'mediaItem != null'. some objects are lightweight\n        return !isLive && !isLiveEnd && Helpers.hasWords(badge) &&\n                (durationMs <= 0 || isSynced) && (hasPlaylist() || hasChannel() || hasNestedItems());\n    }\n\n    public boolean isFullLive() {\n        return isLive && startSegmentNum == 0;\n    }\n\n    public boolean isEmpty() {\n        if (isChapter) {\n            return false;\n        }\n\n        // NOTE: Movies labeled as \"Free with Ads\" not supported yet\n        return Helpers.allNulls(videoId, playlistId, reloadPageKey, playlistParams, channelId, searchQuery) || isMovie;\n    }\n\n    public String getGroupTitle() {\n        return getGroup() != null ? getGroup().getTitle() : null;\n    }\n\n    /**\n     * Persist on Channels and User playlists sections\n     */\n    public String getReloadPageKey() {\n        return reloadPageKey != null ? reloadPageKey :\n                getGroup() != null ? getGroup().getReloadPageKey() : null;\n    }\n\n    public String getNextPageKey() {\n        return getGroup() != null ? getGroup().getNextPageKey() : null;\n    }\n\n    public String getBackgroundUrl() {\n        return bgImageUrl != null ? bgImageUrl : BLACK_PLACEHOLDER_URL;\n    }\n\n    public boolean belongsToSameAuthorGroup() {\n        if (getGroup() == null || getGroup().getSize() < 2) {\n            return false;\n        }\n\n        Video first = getGroup().get(0);\n        Video last = getGroup().get(getGroup().getSize() - 1);\n\n        String author1 = extractAuthor(first.getSecondTitle());\n        String author2 = extractAuthor(last.getSecondTitle());\n\n        return author1 != null && author2 != null && Helpers.equals(author1, author2);\n    }\n\n    public boolean belongsToSamePlaylistGroup() {\n        if (getGroup() == null || getGroup().getSize() < 2) {\n            return false;\n        }\n\n        // Some items may not have a playlistId (e.g. movies)\n        List<Video> filtered = Helpers.filter(getGroup().getVideos(), item -> item.getPlaylistId() != null || item.playlistParams != null, 2);\n\n        if (filtered == null || filtered.size() < 2) {\n            return false;\n        }\n\n        Video first = filtered.get(0);\n        Video second = filtered.get(1);\n\n        String playlist1 = first.getPlaylistId() != null ? first.getPlaylistId() : first.playlistParams;\n        String playlist2 = second.getPlaylistId() != null ? second.getPlaylistId() : second.playlistParams;\n\n        return playlist1 != null && playlist2 != null && Helpers.equals(playlist1, playlist2);\n    }\n\n    private boolean checkAllVideosHasPlaylist() {\n        if (getGroup() == null || getGroup().getSize() < 2) {\n            return false;\n        }\n\n        return playlistId != null && getGroup().get(0).playlistId != null && getGroup().get(1).playlistId != null && getGroup().get(getGroup().getSize() - 1).playlistId != null;\n    }\n\n    public boolean belongsToHome() {\n        return belongsToGroup(MediaGroup.TYPE_HOME);\n    }\n\n    public boolean belongsToChannel() {\n        return belongsToGroup(MediaGroup.TYPE_CHANNEL);\n    }\n\n    public boolean belongsToChannelUploads() {\n        return belongsToGroup(MediaGroup.TYPE_CHANNEL_UPLOADS);\n    }\n\n    public boolean belongsToSubscriptions() {\n        return belongsToGroup(MediaGroup.TYPE_SUBSCRIPTIONS);\n    }\n\n    public boolean belongsToHistory() {\n        return belongsToGroup(MediaGroup.TYPE_HISTORY);\n    }\n\n    public boolean belongsToMusic() {\n        return belongsToGroup(MediaGroup.TYPE_MUSIC);\n    }\n\n    public boolean belongsToShorts() {\n        return belongsToGroup(MediaGroup.TYPE_SHORTS);\n    }\n\n    public boolean belongsToShortsGroup() {\n        return isShorts && (belongsToShorts() || belongsToHome());\n    }\n\n    public boolean belongsToSearch() {\n        return belongsToGroup(MediaGroup.TYPE_SEARCH);\n    }\n\n    public boolean belongsToNotifications() {\n        return belongsToGroup(MediaGroup.TYPE_NOTIFICATIONS);\n    }\n\n    public boolean belongsToPlaybackQueue() {\n        return belongsToGroup(MediaGroup.TYPE_PLAYBACK_QUEUE);\n    }\n\n    public boolean belongsToSuggestions() {\n        return belongsToGroup(MediaGroup.TYPE_SUGGESTIONS);\n    }\n\n    public boolean belongsToUserPlaylists() {\n        return belongsToGroup(MediaGroup.TYPE_USER_PLAYLISTS);\n    }\n\n    public boolean belongsToUndefined() {\n        return belongsToGroup(MediaGroup.TYPE_UNDEFINED);\n    }\n\n    public boolean belongsToBlockedChannels() {\n        return belongsToGroup(MediaGroup.TYPE_BLOCKED_CHANNELS);\n    }\n\n    /**\n     * Get the appropriate channel ID for blocking.\n     * Tries channelId first, then falls back to author name.\n     *\n     * @return The channel ID to use for blocking, or null if not available\n     */\n    public String getChannelIdOrName() {\n        if (channelId != null && !channelId.isEmpty()) {\n            return channelId;\n        }\n\n        // Fallback: use author name if no ID available (less reliable but better than\n        // nothing)\n        String authorName = getAuthor();\n        return authorName != null && !authorName.isEmpty() ? authorName : null;\n    }\n\n    private boolean belongsToGroup(int groupId) {\n        return getGroup() != null && getGroup().getType() == groupId;\n    }\n\n    public boolean belongsToSection() {\n        return getGroup() != null && getGroup().getSection() != null;\n    }\n\n    public void sync(Video video) {\n        if (video == null) {\n            return;\n        }\n\n        percentWatched = video.percentWatched;\n    }\n\n    public void sync(MediaItemMetadata metadata) {\n        if (metadata == null) {\n            return;\n        }\n\n        if (isLive && !metadata.isLive()) {\n            isLiveEnd = true;\n        }\n\n        // NOTE: Skip upcoming (no media) because default title more informative (e.g. has scheduled time).\n        // NOTE: Upcoming videos metadata wrongly reported as live\n        metadataTitle = metadata.getTitle();\n        metadataSecondTitle = metadata.getSecondTitle();\n        // NOTE: Upcoming videos metadata wrongly reported as live (live == true, upcoming == false)\n        isLive = metadata.isLive();\n        isUpcoming = metadata.isUpcoming();\n\n        // No checks. This data wasn't existed before sync.\n        if (metadata.getDescription() != null) {\n            description = metadata.getDescription();\n        }\n        channelId = metadata.getChannelId();\n        nextMediaItem = findNextVideo(metadata);\n        shuffleMediaItem = metadata.getShuffleVideo();\n        playlistInfo = metadata.getPlaylistInfo();\n        isSubscribed = metadata.isSubscribed();\n        likeCount = metadata.getLikeCount();\n        dislikeCount = metadata.getDislikeCount();\n        subscriberCount = metadata.getSubscriberCount();\n        notificationStates = metadata.getNotificationStates();\n        author = metadata.getAuthor();\n        durationMs = metadata.getDurationMs();\n        isSynced = true;\n    }\n\n    public void sync(MediaItemFormatInfo formatInfo) {\n        if (formatInfo == null) {\n            return;\n        }\n        \n        isLive = formatInfo.isLive();\n\n        if (description == null) {\n            description = formatInfo.getDescription();\n        }\n\n        // Published time used on live videos only\n        if (formatInfo.isLive()) {\n            startTimeMs = formatInfo.getStartTimeMs() > 0 ? formatInfo.getStartTimeMs() : DateHelper.toUnixTimeMs(formatInfo.getStartTimestamp());\n            startSegmentNum = formatInfo.getStartSegmentNum();\n        }\n\n        volume = formatInfo.getVolumeLevel();\n        isUnplayable = formatInfo.isUnplayable();\n    }\n\n    public void sync(DislikeData dislikeData) {\n        if (dislikeData == null) {\n            return;\n        }\n\n        String likeCountNew = dislikeData.getLikeCount();\n        String dislikeCountNew = dislikeData.getDislikeCount();\n        if (likeCountNew != null) {\n            likeCount = likeCountNew;\n        }\n        if (dislikeCountNew != null) {\n            dislikeCount = dislikeCountNew;\n        }\n    }\n\n    /**\n     * Creating lightweight copy of origin.\n     */\n    public Video copy() {\n        Video video = new Video();\n        video.videoId = videoId;\n        video.playlistId = playlistId;\n        video.playlistIndex = playlistIndex;\n        video.channelId = channelId;\n        video.title = title;\n        video.metadataTitle = metadataTitle;\n        video.secondTitle = secondTitle;\n        video.metadataSecondTitle = metadataSecondTitle;\n        video.percentWatched = percentWatched;\n        video.cardImageUrl = cardImageUrl;\n        video.fromQueue = fromQueue;\n        video.bgImageUrl = bgImageUrl;\n        video.isLive = isLive;\n        video.isUpcoming = isUpcoming;\n        video.nextMediaItem = nextMediaItem;\n        video.shuffleMediaItem = shuffleMediaItem;\n        video.durationMs = durationMs;\n\n        if (getGroup() != null) {\n            video.setGroup(getGroup().copy()); // Needed for proper multi row fragments sync (row id == group id)\n        }\n\n        return video;\n    }\n\n    private MediaItem findNextVideo(MediaItemMetadata metadata) {\n        if (metadata == null) {\n            return null;\n        }\n\n        MediaItem nextVideo = metadata.getNextVideo();\n\n        // BUGFIX: player closed after last video from the remote queue\n        if (nextVideo == null && isRemote) {\n            List<MediaGroup> suggestions = metadata.getSuggestions();\n\n            if (suggestions != null && suggestions.size() > 1) {\n                List<MediaItem> mediaItems = suggestions.get(1).getMediaItems();\n                nextVideo = Helpers.findFirst(mediaItems, item -> item.getVideoId() != null);\n            }\n        }\n\n        return nextVideo;\n    }\n\n    public void markFullyViewed() {\n        percentWatched = 100;\n        startTimeSeconds = (int)(getDurationMs() / 1_000);\n    }\n\n    public void markNotViewed() {\n        percentWatched = 0;\n        startTimeSeconds = 0;\n    }\n\n    public long getLiveDurationMs() {\n        if (startTimeMs == 0) {\n            return 0;\n        }\n\n        // Disable updates if stream ended while watching\n        if (!isLive) {\n            // Stream ended. We can use real duration now.\n            return durationMs > 0 ? durationMs : liveDurationMs;\n        }\n\n        // Is stream real length may exceeds calculated length???\n        liveDurationMs = System.currentTimeMillis() - startTimeMs;\n        return liveDurationMs > 0 ? liveDurationMs : 0;\n    }\n\n    public long getDurationMs() {\n        return durationMs;\n    }\n\n    public long getPositionMs() {\n        if (startTimeSeconds > 0) {\n            return startTimeSeconds * 1_000L;\n        }\n\n        return getPositionFromPercentWatched();\n    }\n\n    private long getPositionFromPercentWatched() {\n        // Ignore up to 10% watched because the video might be opened on phone and closed immediately.\n        if (percentWatched <= RESTORE_POSITION_PERCENTS || percentWatched >= 100) {\n            return 0;\n        }\n\n        long posMs = (long) (durationMs / 100f * percentWatched);\n        return posMs > 0 && posMs < durationMs ? posMs : 0;\n    }\n\n    public MediaItem toMediaItem() {\n        return SimpleMediaItem.from(this);\n    }\n\n    public void sync(VideoStateService.State state) {\n        if (state != null) {\n            percentWatched = state.positionMs / (state.durationMs / 100f);\n        }\n    }\n\n    ///**\n    // * The section playlist intended (as a backup replacement) for cases when regular playlist not available\n    // */\n    //public boolean isSectionPlaylistEnabled(Context context) {\n    //    return PlayerTweaksData.instance(context).isSectionPlaylistEnabled() && !belongsToSuggestions()\n    //            && (!checkAllVideosHasPlaylist() || PLAYLIST_LIKED_MUSIC.equals(playlistId) || nextMediaItem == null\n    //                   || (!isMix() && !belongsToSamePlaylistGroup())) // skip hidden playlists (music videos usually)\n    //            && (!isRemote || remotePlaylistId == null);\n    //}\n\n    /**\n     * The section playlist intended (as a backup replacement) for cases when regular playlist not available\n     */\n    public boolean isSectionPlaylistEnabled(Context context) {\n        return PlayerTweaksData.instance(context).isSectionPlaylistEnabled() && !belongsToSuggestions() && !belongsToPlaybackQueue()\n                && (!checkAllVideosHasPlaylist() || nextMediaItem == null || !isMix()) // skip hidden playlists (music videos usually)\n                && (!isRemote || remotePlaylistId == null);\n    }\n\n    public String createPlaylistTitle() {\n        if (!hasPlaylist()) {\n            return null;\n        }\n        \n        // Trying to properly format channel playlists, mixes etc\n        boolean isChannelPlaylistItem = getGroupTitle() != null && belongsToSameAuthorGroup() && belongsToSamePlaylistGroup();\n        boolean isUserPlaylistItem = getGroupTitle() != null && belongsToSamePlaylistGroup();\n        String title = isChannelPlaylistItem ? getAuthor() : isUserPlaylistItem ? null : getTitle();\n        String subtitle = isChannelPlaylistItem || isUserPlaylistItem || belongsToUserPlaylists() ? getGroupTitle() : getAuthor();\n        return title != null && subtitle != null ? String.format(\"%s - %s\", title, subtitle) : String.format(\"%s\", title != null ? title : subtitle);\n    }\n\n    public String createChannelTitle() {\n        if (!hasReloadPageKey() && !hasChannel()) {\n            return null;\n        }\n        \n        // Trying to properly format channel playlists, mixes etc\n        boolean hasChannel = hasChannel() && !isChannel();\n        boolean isUserPlaylistItem = getGroupTitle() != null && belongsToSamePlaylistGroup();\n        String title = hasChannel ? getAuthor() : isUserPlaylistItem ? null : getTitle();\n        String subtitle = isUserPlaylistItem ? getGroupTitle() : hasChannel || isChannel() ? null : getAuthor();\n        return title != null && subtitle != null ? String.format(\"%s - %s\", title, subtitle) : String.format(\"%s\", title != null ? title : subtitle);\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/data/VideoGroup.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.data;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.ChapterItem;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItem;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.prefs.GlobalPreferences;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService.State;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.BlockedChannelData;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Collections;\r\nimport java.util.ConcurrentModificationException;\r\nimport java.util.List;\r\n\r\npublic class VideoGroup {\r\n    /**\r\n     * Add at the end of the existing group\r\n     */\r\n    public static final int ACTION_APPEND = 0;\r\n    /**\r\n     * Clear whole fragment and then add this group\r\n     */\r\n    public static final int ACTION_REPLACE = 1;\r\n    public static final int ACTION_REMOVE = 2;\r\n    public static final int ACTION_REMOVE_AUTHOR = 3;\r\n    public static final int ACTION_SYNC = 4;\r\n    /**\r\n     * Add at the begin of the existing group\r\n     */\r\n    public static final int ACTION_PREPEND = 5;\r\n    private static final String TAG = VideoGroup.class.getSimpleName();\r\n    private int mId;\r\n    private String mTitle;\r\n    private List<Video> mVideos;\r\n    private MediaGroup mMediaGroup;\r\n    private BrowseSection mSection;\r\n    private int mPosition = -1;\r\n    private int mAction = ACTION_APPEND;\r\n    private int mType = -1;\r\n    public boolean isQueue;\r\n\r\n    public static VideoGroup from(BrowseSection section) {\r\n        return from((MediaGroup) null, section);\r\n    }\r\n\r\n    public static VideoGroup from(MediaGroup mediaGroup) {\r\n        return from(mediaGroup, (BrowseSection) null);\r\n    }\r\n\r\n    public static VideoGroup from(BrowseSection section, int groupPosition) {\r\n        return from((MediaGroup) null, section, groupPosition);\r\n    }\r\n\r\n    public static VideoGroup from(MediaGroup mediaGroup, BrowseSection section) {\r\n        return from(mediaGroup, section, -1);\r\n    }\r\n\r\n    public static VideoGroup from(Video item) {\r\n        return from(item, extractGroupPosition(item));\r\n    }\r\n\r\n    public static VideoGroup from(Video item, int groupPosition) {\r\n        return from(new ArrayList<>(Collections.singletonList(item)), null, groupPosition);\r\n    }\r\n\r\n    public static VideoGroup from(List<Video> items) {\r\n        return from(items, null);\r\n    }\r\n\r\n    public static VideoGroup from(List<Video> items, BrowseSection section) {\r\n        return from(items, section, extractGroupPosition(items));\r\n    }\r\n\r\n    public static VideoGroup from(List<Video> items, BrowseSection section, int groupPosition) {\r\n        VideoGroup videoGroup = new VideoGroup();\r\n        // Getting topmost element. Could help when syncing multi rows fragments.\r\n        Video topItem = findTopmostItemWithGroup(items);\r\n        if (topItem != null && topItem.getGroup() != null) {\r\n            videoGroup.mId = topItem.getGroup().getId();\r\n            videoGroup.mTitle = topItem.getGroup().getTitle();\r\n        }\r\n        videoGroup.mVideos = items;\r\n        videoGroup.mPosition = groupPosition;\r\n        videoGroup.mSection = section;\r\n\r\n        for (Video item : items) {\r\n            // Section as playlist fix. Don't change the root.\r\n            if (item.getGroup() == null || section != null) {\r\n                item.setGroup(videoGroup);\r\n            }\r\n        }\r\n\r\n        return videoGroup;\r\n    }\r\n\r\n    public static VideoGroup from(MediaGroup mediaGroup, BrowseSection section, int groupPosition) {\r\n        VideoGroup videoGroup = new VideoGroup();\r\n        videoGroup.mSection = section;\r\n        videoGroup.mPosition = groupPosition;\r\n        videoGroup.mVideos = new ArrayList<>();\r\n        videoGroup.mMediaGroup = mediaGroup;\r\n        videoGroup.mTitle = mediaGroup != null && mediaGroup.getTitle() != null ?\r\n                mediaGroup.getTitle() : section != null ? section.getTitle() : null;\r\n        // Fix duplicated rows e.g. Shorts\r\n        //videoGroup.mId = !TextUtils.isEmpty(videoGroup.mTitle) ? videoGroup.mTitle.hashCode() : videoGroup.hashCode();\r\n        videoGroup.mId = videoGroup.hashCode();\r\n\r\n        if (mediaGroup == null) {\r\n            return videoGroup;\r\n        }\r\n\r\n        if (mediaGroup.getMediaItems() == null) {\r\n            Log.e(TAG, \"MediaGroup doesn't contain media items. Title: \" + mediaGroup.getTitle());\r\n            return videoGroup;\r\n        }\r\n\r\n        for (MediaItem item : mediaGroup.getMediaItems()) {\r\n            Video video = Video.from(item);\r\n\r\n            videoGroup.add(video);\r\n        }\r\n\r\n        return videoGroup;\r\n    }\r\n\r\n    public static VideoGroup from(VideoGroup baseGroup, MediaGroup mediaGroup) {\r\n        baseGroup.mMediaGroup = mediaGroup;\r\n\r\n        if (mediaGroup == null) {\r\n            return baseGroup;\r\n        }\r\n\r\n        if (mediaGroup.getMediaItems() == null) {\r\n            Log.e(TAG, \"MediaGroup doesn't contain media items. Title: \" + mediaGroup.getTitle());\r\n            return baseGroup;\r\n        }\r\n\r\n        for (MediaItem item : mediaGroup.getMediaItems()) {\r\n            Video video = Video.from(item);\r\n\r\n            baseGroup.add(video);\r\n        }\r\n\r\n        baseGroup.mAction = ACTION_APPEND;\r\n\r\n        return baseGroup;\r\n    }\r\n\r\n    public static VideoGroup from(VideoGroup baseGroup, VideoGroup newGroup) {\r\n        baseGroup.mMediaGroup = newGroup.mMediaGroup;\r\n\r\n        if (newGroup.mMediaGroup == null) {\r\n            return baseGroup;\r\n        }\r\n\r\n        if (newGroup.getVideos() == null) {\r\n            Log.e(TAG, \"MediaGroup doesn't contain media items. Title: \" + newGroup.getTitle());\r\n            return baseGroup;\r\n        }\r\n\r\n        for (Video video : newGroup.getVideos()) {\r\n            baseGroup.add(video);\r\n        }\r\n\r\n        baseGroup.mAction = ACTION_APPEND;\r\n\r\n        return baseGroup;\r\n    }\r\n\r\n    public static VideoGroup fromChapters(List<ChapterItem> chapters, String title) {\r\n        VideoGroup videoGroup = new VideoGroup();\r\n        videoGroup.mTitle = title;\r\n        videoGroup.mVideos = new ArrayList<>();\r\n\r\n        for (ChapterItem chapter : chapters) {\r\n            Video video = Video.from(chapter);\r\n\r\n            videoGroup.add(video);\r\n        }\r\n\r\n        return videoGroup;\r\n    }\r\n\r\n    public List<Video> getVideos() {\r\n        // NOTE: Don't make the collection read only\r\n        // The collection will be filtered inside VideoGroupObjectAdapter\r\n        return Collections.unmodifiableList(mVideos);\r\n    }\r\n\r\n    public String getTitle() {\r\n        return mTitle;\r\n    }\r\n\r\n    /**\r\n     * The title is converted to unique row id.\r\n     */\r\n    public void setTitle(String title) {\r\n        mTitle = title;\r\n\r\n        //if (!TextUtils.isEmpty(title) && (mId == 0 || mId == hashCode())) {\r\n        //    mId = title.hashCode();\r\n        //}\r\n    }\r\n\r\n    public int getId() {\r\n        return mId;\r\n    }\r\n\r\n    public void setId(int id) {\r\n        mId = id;\r\n    }\r\n\r\n    public MediaGroup getMediaGroup() {\r\n        return mMediaGroup;\r\n    }\r\n\r\n    public BrowseSection getSection() {\r\n        return mSection;\r\n    }\r\n\r\n    public boolean isShorts() {\r\n        if (isEmpty()) {\r\n            return false;\r\n        }\r\n\r\n        for (int i = 0; i < Math.min(8, mVideos.size()); i++) {\r\n             if (!mVideos.get(i).isShorts)\r\n                 return false;\r\n        }\r\n\r\n        return true;\r\n    }\r\n\r\n    /**\r\n     * Group position in multi-grid fragments<br/>\r\n     * It isn't used on other types of fragments.\r\n     */\r\n    public int getPosition() {\r\n        return mPosition;\r\n    }\r\n\r\n    public void setPosition(int position) {\r\n        mPosition = position;\r\n    }\r\n\r\n    public int getAction() {\r\n        return mAction;\r\n    }\r\n\r\n    public void setAction(int action) {\r\n        mAction = action;\r\n\r\n        if (action == ACTION_PREPEND) {\r\n            mPosition = 0;\r\n        }\r\n    }\r\n\r\n    public int getType() {\r\n        return mType != -1 ? mType : getMediaGroup() != null ? getMediaGroup().getType() : mSection != null ? mSection.getId() : -1;\r\n    }\r\n\r\n    public void setType(int type) {\r\n        mType = type;\r\n    }\r\n\r\n    public String getReloadPageKey() {\r\n        return getMediaGroup() != null ? getMediaGroup().getReloadPageKey() : null;\r\n    }\r\n\r\n    public String getNextPageKey() {\r\n        return getMediaGroup() != null ? getMediaGroup().getNextPageKey() : null;\r\n    }\r\n\r\n    /**\r\n     * Lightweight copy (without nested videos)\r\n     */\r\n    public VideoGroup copy() {\r\n        VideoGroup videoGroup = new VideoGroup();\r\n        videoGroup.mId = mId;\r\n        videoGroup.mTitle = mTitle;\r\n        videoGroup.mPosition = mPosition;\r\n\r\n        return videoGroup;\r\n    }\r\n\r\n    /**\r\n     * Getting topmost element. Could help when syncing multi rows fragments.\r\n     */\r\n    private static Video findTopmostItemWithGroup(List<Video> items) {\r\n        if (items.isEmpty()) {\r\n            return null;\r\n        }\r\n\r\n        for (int i = (items.size() - 1); i >= 0; i--) {\r\n            Video video = items.get(i);\r\n            if (video.getGroup() != null) {\r\n                return video;\r\n            }\r\n        }\r\n\r\n        return items.get(items.size() - 1); // No group. Fallback to last item then.\r\n    }\r\n\r\n    private static int extractGroupPosition(List<Video> items) {\r\n        if (items == null || items.isEmpty()) {\r\n            return -1;\r\n        }\r\n\r\n        return extractGroupPosition(findTopmostItemWithGroup(items));\r\n    }\r\n\r\n    private static int extractGroupPosition(Video item) {\r\n        int groupPosition = -1;\r\n\r\n        if (item != null) {\r\n            groupPosition = item.groupPosition;\r\n        }\r\n\r\n        return groupPosition;\r\n    }\r\n\r\n    public void removeAllBefore(Video video) {\r\n        if (mVideos == null) {\r\n            return;\r\n        }\r\n\r\n        removeAllBefore(mVideos.indexOf(video));\r\n    }\r\n\r\n    public void removeAllBefore(int index) {\r\n        if (mVideos == null) {\r\n            return;\r\n        }\r\n\r\n        if (index <= 0 || index >= mVideos.size()) {\r\n            return;\r\n        }\r\n\r\n        mVideos = mVideos.subList(index, mVideos.size());\r\n    }\r\n\r\n    /**\r\n     * Remove playlist id from all videos\r\n     */\r\n    public void stripPlaylistInfo() {\r\n        if (mVideos == null) {\r\n            return;\r\n        }\r\n\r\n        for (Video video : mVideos) {\r\n            video.playlistId = null;\r\n            video.remotePlaylistId = null;\r\n        }\r\n    }\r\n\r\n    public Video findVideoById(String videoId) {\r\n        if (mVideos == null) {\r\n            return null;\r\n        }\r\n\r\n        Video result = null;\r\n\r\n        for (Video video : mVideos) {\r\n            if (Helpers.equals(videoId, video.videoId)) {\r\n                result = video;\r\n                break;\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    public void clear() {\r\n        if (mVideos == null) {\r\n            return;\r\n        }\r\n\r\n        mVideos.clear();\r\n    }\r\n\r\n    public boolean contains(Video video) {\r\n        if (mVideos == null) {\r\n            return false;\r\n        }\r\n\r\n        return mVideos.contains(video);\r\n    }\r\n\r\n    public int getSize() {\r\n        if (mVideos == null) {\r\n            return -1;\r\n        }\r\n\r\n        return mVideos.size();\r\n    }\r\n\r\n    public int indexOf(Video video) {\r\n        if (mVideos == null) {\r\n            return -1;\r\n        }\r\n\r\n        return mVideos.indexOf(video);\r\n    }\r\n\r\n    public Video get(int idx) {\r\n        if (mVideos == null) {\r\n            return null;\r\n        }\r\n\r\n        return mVideos.get(idx);\r\n    }\r\n\r\n    public void remove(Video video) {\r\n        if (mVideos == null) {\r\n            return;\r\n        }\r\n\r\n        try {\r\n            // ConcurrentModificationException fix?\r\n            mVideos.remove(video);\r\n        } catch (UnsupportedOperationException | ConcurrentModificationException e) { // read only collection\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n\r\n    public boolean isEmpty() {\r\n        try {\r\n            return mVideos == null || mVideos.isEmpty();\r\n        } catch (ConcurrentModificationException e) {\r\n            e.printStackTrace();\r\n        }\r\n\r\n        return true;\r\n    }\r\n\r\n    public void add(Video video) {\r\n        // TODO: remove the hack someday.\r\n        // Dirty hack for avoiding group duplication.\r\n        // Duplicated items suddenly appeared in Home, Subscriptions and History.\r\n        // See: VideoGroupObjectAdapter.mVideoItems\r\n        if (mVideos != null && mVideos.contains(video)) {\r\n            return;\r\n        }\r\n\r\n        int size = getSize();\r\n        add(size != -1 ? size : 0, video);\r\n    }\r\n\r\n    public void add(int idx, Video video) {\r\n        if (video == null || video.isEmpty() || isChannelBlocked(video)) {\r\n            return;\r\n        }\r\n\r\n        if (mVideos == null) {\r\n            mVideos = new ArrayList<>();\r\n        }\r\n\r\n        // Group position in multi-grid fragments\r\n        video.groupPosition = mPosition;\r\n        video.setGroup(this);\r\n\r\n        VideoStateService stateService = VideoStateService.instance(null);\r\n        if (stateService != null && (video.percentWatched == -1 || video.percentWatched == 100)) {\r\n            State state = stateService.getByVideoId(video.videoId);\r\n            video.sync(state);\r\n        }\r\n\r\n        mVideos.add(idx, video);\r\n    }\r\n\r\n    private boolean isChannelBlocked(Video video) {\r\n        // Filter out videos from blacklisted channels\r\n        String channelId = video.getChannelIdOrName();\r\n        if (channelId != null) {\r\n            try {\r\n                BlockedChannelData blockedChannelData = BlockedChannelData.instance(GlobalPreferences.context());\r\n                return blockedChannelData != null && blockedChannelData.containsChannel(channelId);\r\n            } catch (Exception e) {\r\n                // If BlockedChannelData isn't initialized yet, allow the video through\r\n                // This can happen during early app startup\r\n            }\r\n        }\r\n\r\n        return false;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/errors/CategoryEmptyError.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.errors;\r\n\r\nimport android.content.Context;\r\n\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.YTSignInPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\npublic class CategoryEmptyError implements ErrorFragmentData {\r\n    private final Context mContext;\r\n    private final Throwable mError;\r\n\r\n    public CategoryEmptyError(Context context, @Nullable Throwable error) {\r\n        mContext = context;\r\n        mError = error;\r\n    }\r\n\r\n    @Override\r\n    public void onAction() {\r\n        YTSignInPresenter.instance(mContext).start();\r\n    }\r\n\r\n    @Override\r\n    public String getMessage() {\r\n        String result = mContext.getString(R.string.msg_cant_load_content);\r\n        if (mError != null && !Helpers.containsAny(mError.getMessage(), \"fromNullable result is null\")) {\r\n            String className = mError.getClass().getSimpleName();\r\n            result = String.format(\"%s: %s\", className, Utils.getStackTraceAsString(mError));\r\n            //result = mError.getMessage();\r\n        }\r\n        return result;\r\n    }\r\n\r\n    @Override\r\n    public String getActionText() {\r\n        return mError != null && Helpers.startsWith(mError.getMessage(), \"AuthError\") ? mContext.getString(R.string.action_signin) : null;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/errors/ErrorFragmentData.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.errors;\r\n\r\npublic interface ErrorFragmentData {\r\n    void onAction();\r\n    String getMessage();\r\n    String getActionText();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/errors/PasswordError.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.errors;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.AccountSettingsPresenter;\r\n\r\npublic class PasswordError implements ErrorFragmentData {\r\n    private final Context mContext;\r\n\r\n    public PasswordError(Context context) {\r\n        mContext = context;\r\n    }\r\n\r\n    @Override\r\n    public void onAction() {\r\n        AccountSettingsPresenter.instance(mContext).showCheckPasswordDialog();\r\n    }\r\n\r\n    @Override\r\n    public String getMessage() {\r\n        return null;\r\n    }\r\n\r\n    @Override\r\n    public String getActionText() {\r\n        return mContext.getString(R.string.enter_account_password);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/errors/SignInError.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.errors;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.YTSignInPresenter;\r\n\r\npublic class SignInError implements ErrorFragmentData {\r\n    private final Context mContext;\r\n\r\n    public SignInError(Context context) {\r\n        mContext = context;\r\n    }\r\n\r\n    @Override\r\n    public void onAction() {\r\n        YTSignInPresenter.instance(mContext).start();\r\n    }\r\n\r\n    @Override\r\n    public String getMessage() {\r\n        return mContext.getString(R.string.library_signin_to_show_more);\r\n    }\r\n\r\n    @Override\r\n    public String getActionText() {\r\n        return mContext.getString(R.string.action_signin);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/BasePlayerController.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback;\r\n\r\nimport android.app.Activity;\r\nimport android.content.Context;\r\nimport android.view.Gravity;\r\n\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.liskovsoft.mediaserviceinterfaces.CommentsService;\r\nimport com.liskovsoft.mediaserviceinterfaces.ContentService;\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.mediaserviceinterfaces.NotificationsService;\r\nimport com.liskovsoft.mediaserviceinterfaces.SignInService;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.listener.PlayerEventListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.SearchPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.PlaybackView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ViewManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MotherActivity;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.ScreensaverManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.SponsorBlockData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.RemoteControlData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.SearchData;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\nimport com.liskovsoft.youtubeapi.service.internal.MediaServiceData;\r\n\r\npublic abstract class BasePlayerController implements PlayerEventListener {\r\n    private PlaybackPresenter mMainController;\r\n    private Context mContext;\r\n    private final Runnable mFitVideoStart = () -> {\r\n        AppDialogPresenter settingsPresenter = getAppDialogPresenter();\r\n        if (getPlayer() == null || settingsPresenter.isOverlay() || getPlayerTweaksData().isDontResizeVideoToFitDialogEnabled()) {\r\n            return;\r\n        }\r\n        FormatItem videoFormat = getPlayer().getVideoFormat();\r\n        Format format = videoFormat != null && videoFormat.getTrack() != null ? videoFormat.getTrack().format : null;\r\n        if (format == null) {\r\n            return;\r\n        }\r\n        getPlayer().showControls(false);\r\n        // Dialog takes up 37% of the screen space\r\n        float dialogWidth = 37 * getMainUIData().getUIScale();\r\n        float initialZoom = 100;\r\n        float totalZoom = initialZoom - dialogWidth;\r\n        float ratio = format.width / (float) format.height;\r\n        float targetRatio = 16/9f;\r\n        float multiplier = targetRatio / ratio;\r\n        if (multiplier > 1) { // skip cinema ratio\r\n            totalZoom *= multiplier;\r\n        }\r\n        if (totalZoom > 130) {\r\n            return; // shorts overzoom fix\r\n        }\r\n        getPlayer().setZoomPercents(Math.round(totalZoom));\r\n        getPlayer().setVideoGravity(settingsPresenter.isComments() && getPlayerTweaksData().isCommentsPlacedLeft() ?\r\n                Gravity.END | Gravity.CENTER_VERTICAL : Gravity.START | Gravity.CENTER_VERTICAL);\r\n    };\r\n    private final Runnable mFitVideoFinish = () -> {\r\n        if (getPlayer() == null || getPlayerTweaksData().isDontResizeVideoToFitDialogEnabled()) {\r\n            return;\r\n        }\r\n        getPlayer().setZoomPercents(getPlayerData().getZoomPercents());\r\n        getPlayer().setVideoGravity(Gravity.CENTER);\r\n    };\r\n\r\n    public void setMainController(PlaybackPresenter mainController) {\r\n        mMainController = mainController;\r\n    }\r\n\r\n    protected PlayerEventListener getMainController() {\r\n        return mMainController;\r\n    }\r\n\r\n    protected <T extends PlayerEventListener> T getController(Class<T> clazz) {\r\n        return mMainController != null ? mMainController.getController(clazz) : null;\r\n    }\r\n\r\n    @Nullable\r\n    public PlaybackView getPlayer() {\r\n        return mMainController != null ? mMainController.getPlayer() : null;\r\n    }\r\n\r\n    @Nullable\r\n    public Video getVideo() {\r\n        return mMainController != null ? mMainController.getVideo() : null;\r\n    }\r\n\r\n    protected void setAltContext(Context context) {\r\n        mContext = context;\r\n    }\r\n\r\n    public Context getContext() {\r\n        return mMainController != null ? mMainController.getContext() : mContext;\r\n    }\r\n\r\n    public Activity getActivity() {\r\n        return mMainController != null ? mMainController.getActivity() : null;\r\n    }\r\n    \r\n    @Override\r\n    public void onInit() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onNewVideo(Video item) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onSuggestionItemClicked(Video item) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onSuggestionItemLongClicked(Video item) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onScrollEnd(Video item) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public boolean onPreviousClicked() {\r\n        // NOP\r\n        return false;\r\n    }\r\n\r\n    @Override\r\n    public boolean onNextClicked() {\r\n        // NOP\r\n        return false;\r\n    }\r\n\r\n    @Override\r\n    public void onViewCreated() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onViewDestroyed() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onViewPaused() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onViewResumed() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onFinish() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onSourceChanged(Video item) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onVideoLoaded(Video item) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onEngineInitialized() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onEngineReleased() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onEngineError(int type, int rendererIndex, Throwable error) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onPlay() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onPause() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onPlayClicked() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onPauseClicked() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onSeekEnd() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onSeekPositionChanged(long positionMs) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onSpeedChanged(float speed) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onPlayEnd() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onBuffering() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onControlsShown(boolean shown) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public boolean onKeyDown(int keyCode) {\r\n        // NOP\r\n        return false;\r\n    }\r\n\r\n    @Override\r\n    public void onTrackSelected(FormatItem track) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onTrackChanged(FormatItem track) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onButtonClicked(int buttonId, int buttonState) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onButtonLongClicked(int buttonId, int buttonState) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onTickle() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onMetadata(MediaItemMetadata metadata) {\r\n        // NOP\r\n    }\r\n\r\n    protected PlayerData getPlayerData() {\r\n        return PlayerData.instance(getContext());\r\n    }\r\n\r\n    protected GeneralData getGeneralData() {\r\n        return GeneralData.instance(getContext());\r\n    }\r\n\r\n    protected MediaServiceData getMediaServiceData() {\r\n        return MediaServiceData.instance();\r\n    }\r\n\r\n    protected PlayerTweaksData getPlayerTweaksData() {\r\n        return PlayerTweaksData.instance(getContext());\r\n    }\r\n\r\n    protected RemoteControlData getRemoteControlData() {\r\n        return RemoteControlData.instance(getContext());\r\n    }\r\n\r\n    protected VideoStateService getStateService() {\r\n        return VideoStateService.instance(getContext());\r\n    }\r\n\r\n    protected SponsorBlockData getSponsorBlockData() {\r\n        return SponsorBlockData.instance(getContext());\r\n    }\r\n\r\n    protected SearchData getSearchData() {\r\n        return SearchData.instance(getContext());\r\n    }\r\n\r\n    protected MainUIData getMainUIData() {\r\n        return MainUIData.instance(getContext());\r\n    }\r\n\r\n    protected MediaServiceManager getServiceManager() {\r\n        return MediaServiceManager.instance();\r\n    }\r\n\r\n    protected ViewManager getViewManager() {\r\n        return ViewManager.instance(getContext());\r\n    }\r\n\r\n    protected AppDialogPresenter getAppDialogPresenter() {\r\n        return AppDialogPresenter.instance(getContext());\r\n    }\r\n\r\n    protected CommentsService getCommentsService() {\r\n        return YouTubeServiceManager.instance().getCommentsService();\r\n    }\r\n\r\n    protected ContentService getContentService() {\r\n        return YouTubeServiceManager.instance().getContentService();\r\n    }\r\n\r\n    protected SignInService getSignInService() {\r\n        return YouTubeServiceManager.instance().getSignInService();\r\n    }\r\n\r\n    protected NotificationsService getNotificationsService() {\r\n        return YouTubeServiceManager.instance().getNotificationsService();\r\n    }\r\n\r\n    protected MediaItemService getMediaItemService() {\r\n        return YouTubeServiceManager.instance().getMediaItemService();\r\n    }\r\n\r\n    protected SearchPresenter getSearchPresenter() {\r\n        return SearchPresenter.instance(getContext());\r\n    }\r\n\r\n    protected PlaybackPresenter getPlaybackPresenter() {\r\n        return PlaybackPresenter.instance(getContext());\r\n    }\r\n\r\n    protected boolean isEmbedPlayer() {\r\n        return getPlayer() != null && getPlayer().isEmbed();\r\n    }\r\n\r\n    protected ScreensaverManager getScreensaverManager() {\r\n        Activity activity = getActivity();\r\n        if (activity instanceof MotherActivity) {\r\n            return ((MotherActivity) activity).getScreensaverManager();\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    protected void fitVideoIntoDialog() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        AppDialogPresenter settingsPresenter = getAppDialogPresenter();\r\n\r\n        settingsPresenter.setOnStart(mFitVideoStart);\r\n\r\n        settingsPresenter.setOnFinish(mFitVideoFinish);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/AutoFrameRateController.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.BasePlayerController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionCategory;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.autoframerate.AutoFrameRateHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.autoframerate.ModeSyncManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.autoframerate.internal.DisplayHolder.Mode;\r\nimport com.liskovsoft.smartyoutubetv2.common.autoframerate.internal.DisplaySyncHelper.AutoFrameRateListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.autoframerate.internal.UhdHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.TvQuickActions;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Arrays;\r\nimport java.util.List;\r\n\r\npublic class AutoFrameRateController extends BasePlayerController implements AutoFrameRateListener {\r\n    private static final String TAG = AutoFrameRateController.class.getSimpleName();\r\n    private static final long AUTO_FRAME_RATE_DELAY_MS = 500;\r\n    private static final int AUTO_FRAME_RATE_ID = 21;\r\n    private static final int AUTO_FRAME_RATE_VIDEO_PAUSE_ID = 22;\r\n    private static final int AUTO_FRAME_RATE_MODES_ID = 23;\r\n    private static final long SHORTS_DURATION_MIN_MS = 30 * 1_000;\r\n    private static final long SHORTS_DURATION_MAX_MS = 61 * 1_000;\r\n    private final AutoFrameRateHelper mAutoFrameRateHelper;\r\n    private final ModeSyncManager mModeSyncManager;\r\n    private final Runnable mApplyAfr = this::applyAfr;\r\n    private final Runnable mApplyAfrStop = this::applyAfrStop;\r\n    private boolean mIsPlay;\r\n    private VideoStateController mStateController;\r\n    private HQDialogController mHQDialogController;\r\n    private final Runnable mPlaybackResumeHandler = () -> {\r\n        if (getPlayer() != null) {\r\n            restorePlayback();\r\n        }\r\n    };\r\n\r\n    public AutoFrameRateController() {\r\n        mAutoFrameRateHelper = AutoFrameRateHelper.instance(null);\r\n        mAutoFrameRateHelper.setListener(this);\r\n        mModeSyncManager = ModeSyncManager.instance();\r\n        mModeSyncManager.setAfrHelper(mAutoFrameRateHelper);\r\n    }\r\n\r\n    @Override\r\n    public void onInit() {\r\n        mAutoFrameRateHelper.saveOriginalState(getActivity());\r\n        mStateController = getController(VideoStateController.class);\r\n        mHQDialogController = getController(HQDialogController.class);\r\n    }\r\n\r\n    @Override\r\n    public void onViewResumed() {\r\n        mAutoFrameRateHelper.setFpsCorrectionEnabled(getPlayerData().isAfrFpsCorrectionEnabled());\r\n        mAutoFrameRateHelper.setResolutionSwitchEnabled(getPlayerData().isAfrResSwitchEnabled(), false);\r\n        mAutoFrameRateHelper.setDoubleRefreshRateEnabled(getPlayerData().isDoubleRefreshRateEnabled());\r\n        mAutoFrameRateHelper.setSkip24RateEnabled(getPlayerData().isSkip24RateEnabled());\r\n\r\n        addUiOptions();\r\n    }\r\n\r\n    @Override\r\n    public void onVideoLoaded(Video item) {\r\n        savePlayback();\r\n\r\n        // Sometimes AFR is not working on activity startup. Trying to fix with delay.\r\n        applyAfrDelayed();\r\n        //applyAfr();\r\n    }\r\n\r\n    @Override\r\n    public void onModeStart(Mode newMode) {\r\n        if (getContext() == null || getPlayerData() == null) {\r\n            return;\r\n        }\r\n\r\n        // Ugoos already displays this message on each mode switch\r\n        @SuppressLint(\"StringFormatMatches\")\r\n        String message = getContext().getString(\r\n                R.string.auto_frame_rate_applying,\r\n                newMode.getPhysicalWidth(),\r\n                newMode.getPhysicalHeight(),\r\n                newMode.getRefreshRate());\r\n        Log.d(TAG, message);\r\n        //MessageHelpers.showLongMessage(getActivity(), message);\r\n        maybePausePlayback();\r\n        getPlayerData().setAfrSwitchTimeMs(System.currentTimeMillis());\r\n    }\r\n\r\n    @Override\r\n    public void onModeError(Mode newMode) {\r\n        if (getContext() == null) {\r\n            return;\r\n        }\r\n\r\n        // This error could appear even on success.\r\n        String msg = getContext().getString(R.string.msg_mode_switch_error, newMode != null ? UhdHelper.toResolution(newMode) : null);\r\n        Log.e(TAG, msg);\r\n\r\n        // Seems that the device doesn't support direct mode switching.\r\n        // Use tvQuickActions instead.\r\n        maybePausePlayback();\r\n    }\r\n\r\n    @Override\r\n    public void onModeCancel() {\r\n        restorePlayback();\r\n    }\r\n\r\n    @Override\r\n    public void onEngineReleased() {\r\n        if (getPlayerData().isAfrEnabled()) {\r\n            applyAfrStopDelayed();\r\n        }\r\n    }\r\n\r\n    private void applyAfrStopDelayed() {\r\n        Utils.postDelayed(mApplyAfrStop, 200);\r\n    }\r\n\r\n    private void applyAfrStop() {\r\n        // Send data to AFR daemon via tvQuickActions app\r\n        TvQuickActions.sendStopAFR(getContext());\r\n    }\r\n\r\n    private void onFpsCorrectionClick() {\r\n        mAutoFrameRateHelper.setFpsCorrectionEnabled(getPlayerData().isAfrFpsCorrectionEnabled());\r\n    }\r\n\r\n    private void onResolutionSwitchClick() {\r\n        mAutoFrameRateHelper.setResolutionSwitchEnabled(getPlayerData().isAfrResSwitchEnabled(), getPlayerData().isAfrEnabled());\r\n    }\r\n\r\n    private void onDoubleRefreshRateClick() {\r\n        mAutoFrameRateHelper.setDoubleRefreshRateEnabled(getPlayerData().isDoubleRefreshRateEnabled());\r\n    }\r\n\r\n    public void onSkip24RateClick() {\r\n        mAutoFrameRateHelper.setSkip24RateEnabled(getPlayerData().isSkip24RateEnabled());\r\n    }\r\n\r\n    private void applyAfrWrapper() {\r\n        if (getPlayerData().isAfrEnabled()) {\r\n            AppDialogPresenter.instance(getContext()).showDialogMessage(\"Applying AFR...\", this::applyAfr, 1_000);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Sometimes AFR is not working on activity startup. Trying to fix with delay.\r\n     */\r\n    private void applyAfrDelayed() {\r\n        Utils.postDelayed(mApplyAfr, AUTO_FRAME_RATE_DELAY_MS);\r\n    }\r\n\r\n    public void applyAfr() {\r\n        if (!skipAfr() && getPlayerData().isAfrEnabled()) {\r\n            FormatItem videoFormat = getPlayer().getVideoFormat();\r\n            applyAfr(videoFormat, false);\r\n            // Send data to AFR daemon via tvQuickActions app\r\n            TvQuickActions.sendStartAFR(getContext(), videoFormat);\r\n        } else {\r\n            restoreAfr();\r\n        }\r\n    }\r\n\r\n    private void restoreAfr() {\r\n        String msg = \"Restoring original frame rate...\";\r\n        Log.d(TAG, msg);\r\n        mAutoFrameRateHelper.restoreOriginalState(getActivity());\r\n        mModeSyncManager.save(null);\r\n    }\r\n\r\n    private void applyAfr(FormatItem videoFormat, boolean force) {\r\n        if (videoFormat != null) {\r\n            String msg = String.format(\"Applying afr... fps: %s, resolution: %sx%s, activity: %s\",\r\n                    videoFormat.getFrameRate(),\r\n                    videoFormat.getWidth(),\r\n                    videoFormat.getHeight(),\r\n                    getContext().getClass().getSimpleName()\r\n            );\r\n            Log.d(TAG, msg);\r\n\r\n            mAutoFrameRateHelper.apply(getActivity(), videoFormat, force);\r\n        }\r\n    }\r\n\r\n    private void maybePausePlayback() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        int delayMs = 5_000;\r\n\r\n        if (getPlayerData().getAfrPauseMs() > 0) {\r\n            getPlayer().setPlayWhenReady(false);\r\n            delayMs = getPlayerData().getAfrPauseMs();\r\n        }\r\n\r\n        Utils.postDelayed(mPlaybackResumeHandler, delayMs);\r\n    }\r\n\r\n    private void savePlayback() {\r\n        if (!skipAfr() && mAutoFrameRateHelper.isSupported() && getPlayerData().isAfrEnabled() && getPlayerData().getAfrPauseMs() > 0) {\r\n            mStateController.blockPlay(true);\r\n        }\r\n\r\n        mIsPlay = mStateController.getPlayEnabled();\r\n    }\r\n\r\n    private void restorePlayback() {\r\n        // Fix restore after disable afr: don't do afr enabled check\r\n        if (!skipAfr() && mAutoFrameRateHelper.isSupported() && getPlayerData().getAfrPauseMs() > 0) {\r\n            mStateController.blockPlay(false);\r\n            getPlayer().setPlayWhenReady(mIsPlay);\r\n        }\r\n    }\r\n\r\n    // Avoid nested dialogs. They have problems with timings. So player controls may hide without user interaction.\r\n    private void addUiOptions() {\r\n        if (mAutoFrameRateHelper.isSupported() && getContext() != null) {\r\n            OptionCategory afrCategory = createAutoFrameRateCategory(\r\n                    getContext(), PlayerData.instance(getContext()),\r\n                    () -> {}, this::onResolutionSwitchClick, this::onFpsCorrectionClick, this::onDoubleRefreshRateClick, this::onSkip24RateClick);\r\n\r\n            OptionCategory afrPauseCategory = createAutoFrameRatePauseCategory(\r\n                    getContext(), PlayerData.instance(getContext()));\r\n\r\n            OptionCategory modesCategory = createAutoFrameRateModesCategory(getContext());\r\n\r\n            // Create nested dialogs\r\n\r\n            List<OptionItem> options = new ArrayList<>();\r\n            options.add(UiOptionItem.from(afrCategory.title, optionItem -> {\r\n                AppDialogPresenter dialogPresenter = AppDialogPresenter.instance(getContext());\r\n                dialogPresenter.appendCategory(afrCategory);\r\n                dialogPresenter.showDialog(afrCategory.title);\r\n            }));\r\n            options.add(UiOptionItem.from(afrPauseCategory.title, optionItem -> {\r\n                AppDialogPresenter dialogPresenter = AppDialogPresenter.instance(getContext());\r\n                dialogPresenter.appendCategory(afrPauseCategory);\r\n                dialogPresenter.showDialog(afrPauseCategory.title);\r\n            }));\r\n            options.add(UiOptionItem.from(modesCategory.title, optionItem -> {\r\n                AppDialogPresenter dialogPresenter = AppDialogPresenter.instance(getContext());\r\n                dialogPresenter.appendCategory(modesCategory);\r\n                dialogPresenter.showDialog(modesCategory.title);\r\n            }));\r\n\r\n            mHQDialogController.addCategory(OptionCategory.from(AUTO_FRAME_RATE_ID, OptionCategory.TYPE_STRING_LIST, getContext().getString(R.string.auto_frame_rate), options));\r\n            mHQDialogController.addOnDialogHide(mApplyAfr); // Apply NEW Settings on dialog close\r\n        } else {\r\n            mHQDialogController.removeCategory(AUTO_FRAME_RATE_ID);\r\n            mHQDialogController.removeOnDialogHide(mApplyAfr);\r\n        }\r\n    }\r\n\r\n    public static OptionCategory createAutoFrameRateCategory(Context context, PlayerData playerData) {\r\n        return createAutoFrameRateCategory(context, playerData, () -> {}, () -> {}, () -> {}, () -> {}, () -> {});\r\n    }\r\n\r\n    private static OptionCategory createAutoFrameRateCategory(Context context, PlayerData playerData,\r\n            Runnable onAfrCallback, Runnable onResolutionCallback, Runnable onFpsCorrectionCallback,\r\n            Runnable onDoubleRefreshRateCallback, Runnable onSkip24RateCallback) {\r\n        String afrEnable = context.getString(R.string.auto_frame_rate);\r\n        String afrEnableDesc = context.getString(R.string.auto_frame_rate_desc);\r\n        String fpsCorrection = context.getString(R.string.frame_rate_correction, \"24->23.97, 30->29.97, 60->59.94\");\r\n        String resolutionSwitch = context.getString(R.string.resolution_switch);\r\n        String doubleRefreshRate = context.getString(R.string.double_refresh_rate);\r\n        String skip24Rate = context.getString(R.string.skip_24_rate);\r\n        String skipShorts = context.getString(R.string.skip_shorts);\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        OptionItem afrEnableOption = UiOptionItem.from(afrEnable, afrEnableDesc, optionItem -> {\r\n            playerData.setAfrEnabled(optionItem.isSelected());\r\n            onAfrCallback.run();\r\n        }, playerData.isAfrEnabled());\r\n        OptionItem afrResSwitchOption = UiOptionItem.from(resolutionSwitch, optionItem -> {\r\n            playerData.setAfrResSwitchEnabled(optionItem.isSelected());\r\n            onResolutionCallback.run();\r\n        }, playerData.isAfrResSwitchEnabled());\r\n        OptionItem afrFpsCorrectionOption = UiOptionItem.from(fpsCorrection, optionItem -> {\r\n            playerData.setAfrFpsCorrectionEnabled(optionItem.isSelected());\r\n            onFpsCorrectionCallback.run();\r\n        }, playerData.isAfrFpsCorrectionEnabled());\r\n        OptionItem doubleRefreshRateOption = UiOptionItem.from(doubleRefreshRate, optionItem -> {\r\n            playerData.setDoubleRefreshRateEnabled(optionItem.isSelected());\r\n            onDoubleRefreshRateCallback.run();\r\n        }, playerData.isDoubleRefreshRateEnabled());\r\n        OptionItem skip24RateOption = UiOptionItem.from(skip24Rate, optionItem -> {\r\n            playerData.setSkip24RateEnabled(optionItem.isSelected());\r\n            onSkip24RateCallback.run();\r\n        }, playerData.isSkip24RateEnabled());\r\n        OptionItem skipShortsOption = UiOptionItem.from(skipShorts, optionItem -> {\r\n            playerData.setSkipShortsEnabled(optionItem.isSelected());\r\n        }, playerData.isSkipShortsEnabled());\r\n\r\n        afrResSwitchOption.setRequired(afrEnableOption);\r\n        afrFpsCorrectionOption.setRequired(afrEnableOption);\r\n        doubleRefreshRateOption.setRequired(afrEnableOption);\r\n        skip24RateOption.setRequired(afrEnableOption);\r\n        skipShortsOption.setRequired(afrEnableOption);\r\n\r\n        options.add(afrEnableOption);\r\n        options.add(afrResSwitchOption);\r\n        options.add(afrFpsCorrectionOption);\r\n        options.add(doubleRefreshRateOption);\r\n        options.add(skip24RateOption);\r\n        options.add(skipShortsOption);\r\n\r\n        return OptionCategory.from(AUTO_FRAME_RATE_ID, OptionCategory.TYPE_CHECKBOX_LIST, afrEnable, options);\r\n    }\r\n\r\n    public static OptionCategory createAutoFrameRatePauseCategory(Context context, PlayerData playerData) {\r\n        String title = context.getString(R.string.auto_frame_rate_pause);\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int pauseMs : Helpers.range(0, 15_000, 250)) {\r\n            @SuppressLint(\"StringFormatMatches\")\r\n            String optionTitle = pauseMs == 0 ? context.getString(R.string.option_never) : context.getString(R.string.auto_frame_rate_sec, pauseMs / 1_000f);\r\n            options.add(UiOptionItem.from(optionTitle,\r\n                    optionItem -> {\r\n                        playerData.setAfrPauseMs(pauseMs);\r\n                        playerData.setAfrEnabled(true);\r\n                    },\r\n                    pauseMs == playerData.getAfrPauseMs()));\r\n        }\r\n\r\n        return OptionCategory.from(AUTO_FRAME_RATE_VIDEO_PAUSE_ID, OptionCategory.TYPE_RADIO_LIST, title, options);\r\n    }\r\n\r\n    public static OptionCategory createAutoFrameRateModesCategory(Context context) {\r\n        String title = context.getString(R.string.auto_frame_rate_modes);\r\n\r\n        UhdHelper uhdHelper = new UhdHelper(context);\r\n\r\n        Mode[] supportedModes = uhdHelper.getSupportedModes();\r\n        Arrays.sort(supportedModes);\r\n\r\n        StringBuilder result = new StringBuilder();\r\n\r\n        for (Mode mode : supportedModes) {\r\n            result.append(String.format(\"%sx%s@%s\\n\", mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate()));\r\n        }\r\n\r\n        return OptionCategory.from(AUTO_FRAME_RATE_MODES_ID, OptionCategory.TYPE_LONG_TEXT, title, UiOptionItem.from(result.toString()));\r\n    }\r\n\r\n    private boolean skipAfr() {\r\n        if (getPlayerData() == null || getPlayer() == null || getPlayer().getVideo() == null) {\r\n            return true;\r\n        }\r\n\r\n        // NOTE: Avoid detecting shorts by Video.isShorts. Because this is working only in certain places (e.g. Shorts section).\r\n        return isEmbedPlayer() || getPlayer().getDurationMs() <= SHORTS_DURATION_MIN_MS || isSkipShortsPrefs();\r\n    }\r\n\r\n    private boolean isSkipShortsPrefs() {\r\n        return getPlayerData().isSkipShortsEnabled() && (getPlayer().getVideo().isShorts || getPlayer().getDurationMs() <= SHORTS_DURATION_MAX_MS);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/ChatController.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.LiveChatService;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.ChatItem;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.BasePlayerController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerUI;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.ChatReceiver;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.ChatReceiverImpl;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class ChatController extends BasePlayerController {\r\n    private static final String TAG = ChatController.class.getSimpleName();\r\n    /**\r\n     * NOTE: Don't remove duplicates! They contain different chars.\r\n     */\r\n    private static final String[] BLACK_LIST = {\". XYZ\", \". ХYZ\", \"⠄XYZ\", \"⠄ХYZ\", \"Ricardo Merlino\", \"⠄СОM\", \".COM\", \".СОM\", \". COM\"};\r\n    private LiveChatService mChatService;\r\n    private Disposable mChatAction;\r\n    private String mLiveChatKey;\r\n\r\n    @Override\r\n    public void onInit() {\r\n        mChatService = YouTubeServiceManager.instance().getLiveChatService();\r\n    }\r\n\r\n    @Override\r\n    public void onMetadata(MediaItemMetadata metadata) {\r\n        mLiveChatKey = metadata != null ? metadata.getLiveChatKey() : null;\r\n\r\n        if (mLiveChatKey != null) {\r\n            getPlayer().setButtonState(R.id.action_chat, getPlayerData().isLiveChatEnabled() ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n        }\r\n\r\n        if (getPlayerData().isLiveChatEnabled()) {\r\n            openLiveChat();\r\n        }\r\n    }\r\n\r\n    private void openLiveChat() {\r\n        disposeActions();\r\n\r\n        if (mLiveChatKey == null) {\r\n            return;\r\n        }\r\n\r\n        ChatReceiver chatReceiver = new ChatReceiverImpl();\r\n        getPlayer().setChatReceiver(chatReceiver);\r\n\r\n        mChatAction = mChatService.openLiveChatObserve(mLiveChatKey)\r\n                .subscribe(\r\n                        chatItem -> {\r\n                            Log.d(TAG, chatItem.getMessage());\r\n                            if (checkItem(chatItem)) {\r\n                                chatReceiver.addChatItem(chatItem);\r\n                            }\r\n                        },\r\n                        error -> {\r\n                            Log.e(TAG, error.getMessage());\r\n                            error.printStackTrace();\r\n                        },\r\n                        () -> Log.e(TAG, \"Live chat session has been closed\")\r\n                );\r\n    }\r\n\r\n    @Override\r\n    public void onButtonClicked(int buttonId, int buttonState) {\r\n        if (buttonId == R.id.action_chat) {\r\n            if (mLiveChatKey != null) {\r\n                enableLiveChat(buttonState != PlayerUI.BUTTON_ON);\r\n            }\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onButtonLongClicked(int buttonId, int buttonState) {\r\n        if (buttonId == R.id.action_chat) {\r\n            String chatCategoryTitle = getContext().getString(R.string.open_chat);\r\n\r\n            AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n            List<OptionItem> options = new ArrayList<>();\r\n\r\n            options.add(UiOptionItem.from(getContext().getString(R.string.option_disabled),\r\n                    optionItem -> {\r\n                        enableLiveChat(false);\r\n                        settingsPresenter.closeDialog();\r\n                    },\r\n                    !getPlayerData().isLiveChatEnabled()));\r\n\r\n            options.add(UiOptionItem.from(getContext().getString(R.string.chat_left),\r\n                    optionItem -> {\r\n                        placeChatLeft(true);\r\n                        enableLiveChat(true);\r\n                        settingsPresenter.closeDialog();\r\n                    },\r\n                    getPlayerData().isLiveChatEnabled() && isChatPlacedLeft()));\r\n\r\n            options.add(UiOptionItem.from(getContext().getString(R.string.chat_right),\r\n                    optionItem -> {\r\n                        placeChatLeft(false);\r\n                        enableLiveChat(true);\r\n                        settingsPresenter.closeDialog();\r\n                    },\r\n                    getPlayerData().isLiveChatEnabled() && !isChatPlacedLeft()));\r\n\r\n            settingsPresenter.appendRadioCategory(chatCategoryTitle, options);\r\n\r\n            settingsPresenter.showDialog(chatCategoryTitle);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onEngineReleased() {\r\n        disposeActions();\r\n    }\r\n\r\n    @Override\r\n    public void onFinish() {\r\n        disposeActions();\r\n    }\r\n\r\n    private void disposeActions() {\r\n        if (RxHelper.isAnyActionRunning(mChatAction)) {\r\n            RxHelper.disposeActions(mChatAction);\r\n            getPlayer().setChatReceiver(null);\r\n        }\r\n    }\r\n\r\n    private boolean checkItem(ChatItem chatItem) {\r\n        if (chatItem == null || chatItem.getAuthorName() == null) {\r\n            return false;\r\n        }\r\n\r\n        String authorName = chatItem.getAuthorName();\r\n\r\n        for (String spammer : BLACK_LIST) {\r\n            if (authorName.toLowerCase().contains(spammer.toLowerCase())) {\r\n                return false;\r\n            }\r\n        }\r\n\r\n        return true;\r\n    }\r\n\r\n    private void enableLiveChat(boolean enabled) {\r\n        if (enabled) {\r\n            openLiveChat();\r\n        } else {\r\n            disposeActions();\r\n        }\r\n        getPlayerData().setLiveChatEnabled(enabled);\r\n\r\n        if (mLiveChatKey != null) {\r\n            getPlayer().setButtonState(R.id.action_chat, enabled ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n        }\r\n    }\r\n\r\n    private void placeChatLeft(boolean left) {\r\n        if (mLiveChatKey != null) {\r\n            getPlayerTweaksData().setChatPlacedLeft(left);\r\n        } else {\r\n            getPlayerTweaksData().setCommentsPlacedLeft(left);\r\n        }\r\n    }\r\n\r\n    private boolean isChatPlacedLeft() {\r\n        return mLiveChatKey != null ? getPlayerTweaksData().isChatPlacedLeft() : getPlayerTweaksData().isCommentsPlacedLeft();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/CommentsController.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers;\r\n\r\nimport android.content.Context;\r\nimport android.util.Pair;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.CommentGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.CommentItem;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.BasePlayerController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.CommentsReceiver;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.CommentsReceiver.Backup;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.AbstractCommentsReceiver;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\npublic class CommentsController extends BasePlayerController {\r\n    private static final String TAG = CommentsController.class.getSimpleName();\r\n    private Disposable mCommentsAction;\r\n    private String mLiveChatKey;\r\n    private String mCommentsKey;\r\n    private String mTitle;\r\n    private Pair<String, Backup> mBackup;\r\n\r\n    public CommentsController() {\r\n    }\r\n\r\n    public CommentsController(Context context, MediaItemMetadata metadata) {\r\n        setAltContext(context);\r\n        onInit();\r\n        onMetadata(metadata);\r\n    }\r\n\r\n    @Override\r\n    public void onMetadata(MediaItemMetadata metadata) {\r\n        mLiveChatKey = metadata != null && metadata.getLiveChatKey() != null ? metadata.getLiveChatKey() : null;\r\n        mCommentsKey = metadata != null && metadata.getCommentsKey() != null ? metadata.getCommentsKey() : null;\r\n        mTitle = metadata != null ? metadata.getTitle() : null;\r\n        if (mBackup != null && !Helpers.equals(mBackup.first, mCommentsKey)) {\r\n            mBackup = null;\r\n        }\r\n    }\r\n\r\n    private void openCommentsDialog() {\r\n        fitVideoIntoDialog();\r\n\r\n        disposeActions();\r\n\r\n        if (mCommentsKey == null) {\r\n            return;\r\n        }\r\n\r\n        final String backupKey = mCommentsKey;\r\n\r\n        if (getPlayer() != null) {\r\n            getPlayer().showControls(false);\r\n        }\r\n\r\n        String title = getPlayer() != null && getPlayer().getVideo() != null ? getPlayer().getVideo().getTitleFull() : mTitle;\r\n\r\n        CommentsReceiver commentsReceiver = new AbstractCommentsReceiver(getContext()) {\r\n            @Override\r\n            public void onLoadMore(CommentGroup commentGroup) {\r\n                loadComments(this, commentGroup.getNextCommentsKey());\r\n            }\r\n\r\n            @Override\r\n            public void onStart() {\r\n                if (mBackup != null && Helpers.equals(mBackup.first, mCommentsKey)) {\r\n                    loadBackup(mBackup.second);\r\n                    return;\r\n                }\r\n\r\n                loadComments(this, mCommentsKey);\r\n            }\r\n\r\n            @Override\r\n            public void onCommentClicked(CommentItem commentItem) {\r\n                if (commentItem.isEmpty()) {\r\n                    return;\r\n                }\r\n\r\n                CommentsReceiver nestedReceiver = new AbstractCommentsReceiver(getContext()) {\r\n                    @Override\r\n                    public void onLoadMore(CommentGroup commentGroup) {\r\n                        loadComments(this, commentGroup.getNextCommentsKey());\r\n                    }\r\n\r\n                    @Override\r\n                    public void onStart() {\r\n                        loadComments(this, commentItem.getNestedCommentsKey());\r\n                    }\r\n\r\n                    @Override\r\n                    public void onCommentLongClicked(CommentItem commentItem) {\r\n                        toggleLike(this, commentItem);\r\n                    }\r\n                };\r\n\r\n                showDialog(nestedReceiver, title);\r\n            }\r\n\r\n            @Override\r\n            public void onCommentLongClicked(CommentItem commentItem) {\r\n                toggleLike(this, commentItem);\r\n            }\r\n\r\n            @Override\r\n            public void onFinish(Backup backup) {\r\n                if (Helpers.equals(backupKey, mCommentsKey)) {\r\n                    mBackup = new Pair<>(mCommentsKey, backup);\r\n                }\r\n            }\r\n        };\r\n\r\n        showDialog(commentsReceiver, title);\r\n    }\r\n\r\n    @Override\r\n    public void onButtonClicked(int buttonId, int buttonState) {\r\n        if (buttonId == R.id.action_chat) {\r\n            if (mCommentsKey != null && mLiveChatKey == null) {\r\n                openCommentsDialog();\r\n            }\r\n\r\n            if (mCommentsKey == null && mLiveChatKey == null) {\r\n                MessageHelpers.showMessage(getContext(), R.string.section_is_empty);\r\n            }\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onEngineReleased() {\r\n        disposeActions();\r\n        mBackup = null;\r\n    }\r\n\r\n    @Override\r\n    public void onFinish() {\r\n        disposeActions();\r\n    }\r\n\r\n    private void disposeActions() {\r\n        RxHelper.disposeActions(mCommentsAction);\r\n    }\r\n\r\n    private void loadComments(CommentsReceiver receiver, String commentsKey) {\r\n        disposeActions();\r\n\r\n        mCommentsAction = getCommentsService().getCommentsObserve(commentsKey)\r\n                .subscribe(\r\n                        receiver::addCommentGroup,\r\n                        error -> {\r\n                            Log.e(TAG, error.getMessage());\r\n                            receiver.addCommentGroup(null); // remove loading message\r\n                        }\r\n                );\r\n    }\r\n\r\n    private void showDialog(CommentsReceiver receiver, String title) {\r\n        AppDialogPresenter appDialogPresenter = getAppDialogPresenter();\r\n\r\n        appDialogPresenter.appendCommentsCategory(title, UiOptionItem.from(title, receiver));\r\n        //appDialogPresenter.enableTransparent(true);\r\n        appDialogPresenter.showDialog();\r\n    }\r\n\r\n    private void toggleLike(CommentsReceiver receiver, CommentItem commentItem) {\r\n        MyCommentItem myCommentItem = MyCommentItem.from(commentItem);\r\n        myCommentItem.setLiked(!myCommentItem.isLiked());\r\n\r\n        receiver.sync(myCommentItem);\r\n\r\n        RxHelper.execute(\r\n                getCommentsService().toggleLikeObserve(commentItem.getNestedCommentsKey()), e -> MessageHelpers.showMessage(getContext(), e.getMessage()));\r\n    }\r\n\r\n    private static final class MyCommentItem implements CommentItem {\r\n        private final String mId;\r\n        private final String mMessage;\r\n        private final String mAuthorName;\r\n        private final String mAuthorPhoto;\r\n        private final String mPublishedDate;\r\n        private final String mNestedCommentsKey;\r\n        private boolean mIsLiked;\r\n        private String mLikeCount;\r\n        private final String mReplyCount;\r\n        private final boolean mIsEmpty;\r\n\r\n        private MyCommentItem(\r\n                String id, String message, String authorName, String authorPhoto, String publishedDate,\r\n                String nestedCommentsKey, boolean isLiked, String likeCount, String replyCount, boolean isEmpty) {\r\n            mId = id;\r\n            mMessage = message;\r\n            mAuthorName = authorName;\r\n            mAuthorPhoto = authorPhoto;\r\n            mPublishedDate = publishedDate;\r\n            mNestedCommentsKey = nestedCommentsKey;\r\n            mIsLiked = isLiked;\r\n            mLikeCount = likeCount;\r\n            mReplyCount = replyCount;\r\n            mIsEmpty = isEmpty;\r\n        }\r\n\r\n        @Override\r\n        public String getId() {\r\n            return mId;\r\n        }\r\n\r\n        @Override\r\n        public String getMessage() {\r\n            return mMessage;\r\n        }\r\n\r\n        @Override\r\n        public String getAuthorName() {\r\n            return mAuthorName;\r\n        }\r\n\r\n        @Override\r\n        public String getAuthorPhoto() {\r\n            return mAuthorPhoto;\r\n        }\r\n\r\n        @Override\r\n        public String getPublishedDate() {\r\n            return mPublishedDate;\r\n        }\r\n\r\n        @Override\r\n        public String getNestedCommentsKey() {\r\n            return mNestedCommentsKey;\r\n        }\r\n\r\n        @Override\r\n        public boolean isLiked() {\r\n            return mIsLiked;\r\n        }\r\n\r\n        public void setLiked(boolean isLiked) {\r\n            if (mIsLiked == isLiked) {\r\n                return;\r\n            }\r\n\r\n            mIsLiked = isLiked;\r\n\r\n            if (mLikeCount == null) {\r\n                mLikeCount = String.valueOf(0);\r\n            }\r\n\r\n            if (Helpers.isInteger(getLikeCount())) {\r\n                int likeCount = Helpers.parseInt(getLikeCount());\r\n                int count = isLiked ? ++likeCount : --likeCount;\r\n                mLikeCount = count > 0 ? String.valueOf(count) : null;\r\n            }\r\n        }\r\n\r\n        @Override\r\n        public String getLikeCount() {\r\n            return mLikeCount;\r\n        }\r\n\r\n        @Override\r\n        public String getReplyCount() {\r\n            return mReplyCount;\r\n        }\r\n\r\n        @Override\r\n        public boolean isEmpty() {\r\n            return mIsEmpty;\r\n        }\r\n\r\n        public static MyCommentItem from(CommentItem commentItem) {\r\n            return new MyCommentItem(commentItem.getId(), commentItem.getMessage(), commentItem.getAuthorName(),\r\n                    commentItem.getAuthorPhoto(), commentItem.getPublishedDate(), commentItem.getNestedCommentsKey(),\r\n                    commentItem.isLiked(), commentItem.getLikeCount(), commentItem.getReplyCount(), commentItem.isEmpty());\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/HQDialogController.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.BasePlayerController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionCategory;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\n\r\nimport java.util.HashSet;\r\nimport java.util.LinkedHashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\nimport java.util.Set;\r\n\r\npublic class HQDialogController extends BasePlayerController {\r\n    private static final String TAG = HQDialogController.class.getSimpleName();\r\n    private static final int VIDEO_FORMATS_ID = 132;\r\n    private static final int AUDIO_FORMATS_ID = 133;\r\n    // NOTE: using map, because same item could be changed time to time\r\n    private final Map<Integer, OptionCategory> mCategories = new LinkedHashMap<>();\r\n    private final Map<Integer, OptionCategory> mCategoriesInt = new LinkedHashMap<>();\r\n    private final Set<Runnable> mHideListeners = new HashSet<>();\r\n    private AppDialogPresenter mAppDialogPresenter;\r\n\r\n    @Override\r\n    public void onInit() {\r\n        mAppDialogPresenter = AppDialogPresenter.instance(getContext());\r\n    }\r\n\r\n    @Override\r\n    public void onViewResumed() {\r\n        //updateBackgroundPlayback();\r\n    }\r\n\r\n    @Override\r\n    public void onButtonClicked(int buttonId, int buttonState) {\r\n        if (buttonId == R.id.lb_control_high_quality) {\r\n            onHighQualityClicked();\r\n        }\r\n    }\r\n\r\n    private void onHighQualityClicked() {\r\n        fitVideoIntoDialog();\r\n\r\n        addQualityCategories();\r\n        addAudioLanguage();\r\n        addPresetsCategory();\r\n        addVideoZoomCategory();\r\n        addNetworkEngine();\r\n        addVideoBufferCategory();\r\n        addAudioDelayCategory();\r\n        addPitchEffectCategory();\r\n        addSleepTimerCategory();\r\n        //addBackgroundPlaybackCategory();\r\n\r\n        appendOptions(mCategoriesInt);\r\n        appendOptions(mCategories);\r\n\r\n        mAppDialogPresenter.showDialog(getContext().getString(R.string.playback_settings), this::onDialogHide);\r\n    }\r\n\r\n    private void addQualityCategories() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        List<FormatItem> videoFormats = getPlayer().getVideoFormats();\r\n        String videoFormatsTitle = getContext().getString(R.string.title_video_formats);\r\n\r\n        List<FormatItem> audioFormats = getPlayer().getAudioFormats();\r\n        String audioFormatsTitle = getContext().getString(R.string.title_audio_formats);\r\n\r\n        addCategoryInt(OptionCategory.from(\r\n                VIDEO_FORMATS_ID,\r\n                OptionCategory.TYPE_RADIO_LIST,\r\n                videoFormatsTitle,\r\n                UiOptionItem.from(videoFormats, this::selectFormatOption, getContext().getString(R.string.option_disabled))));\r\n        addCategoryInt(OptionCategory.from(\r\n                AUDIO_FORMATS_ID,\r\n                OptionCategory.TYPE_RADIO_LIST,\r\n                audioFormatsTitle,\r\n                UiOptionItem.from(audioFormats, this::selectFormatOption, getContext().getString(R.string.option_disabled))));\r\n    }\r\n\r\n    private void selectFormatOption(OptionItem option) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        FormatItem formatItem = UiOptionItem.toFormat(option);\r\n        getPlayer().setFormat(formatItem);\r\n        persistFormat(formatItem);\r\n\r\n        if (getPlayerData().getFormat(formatItem.getType()).isPreset()) {\r\n            // Preset currently active. Show warning about format reset.\r\n            MessageHelpers.showMessage(getContext(), R.string.video_preset_enabled);\r\n        }\r\n\r\n        if (!getPlayer().containsMedia()) {\r\n            getPlayer().reloadPlayback();\r\n        }\r\n\r\n        // Make result easily be spotted by the user\r\n        if (formatItem.getType() == FormatItem.TYPE_VIDEO) {\r\n            getPlayer().showOverlay(false);\r\n        }\r\n    }\r\n\r\n    private void persistFormat(FormatItem formatItem) {\r\n        if (formatItem.getType() == FormatItem.TYPE_VIDEO) {\r\n            if (!getPlayerData().getFormat(FormatItem.TYPE_VIDEO).isPreset()) {\r\n                getPlayerData().setFormat(formatItem);\r\n            } else {\r\n                getPlayerData().setTempVideoFormat(formatItem);\r\n            }\r\n        } else {\r\n            getPlayerData().setFormat(formatItem);\r\n        }\r\n    }\r\n\r\n    private void addVideoBufferCategory() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n        addCategoryInt(AppDialogUtil.createVideoBufferCategory(getContext(),\r\n                () -> getPlayer().restartEngine()));\r\n    }\r\n\r\n    private void addAudioDelayCategory() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n        addCategoryInt(AppDialogUtil.createAudioShiftCategory(getContext(),\r\n                () -> getPlayer().restartEngine()));\r\n    }\r\n\r\n    private void addPitchEffectCategory() {\r\n        addCategoryInt(AppDialogUtil.createPitchEffectCategory(getContext()));\r\n    }\r\n\r\n    private void addSleepTimerCategory() {\r\n        addCategoryInt(AppDialogUtil.createSleepTimerCategory(getContext()));\r\n    }\r\n\r\n    private void addAudioLanguage() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n        addCategoryInt(AppDialogUtil.createAudioLanguageCategory(getContext(),\r\n                () -> getPlayer().restartEngine()));\r\n    }\r\n\r\n    private void addNetworkEngine() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n        addCategoryInt(AppDialogUtil.createNetworkEngineCategory(getContext(),\r\n                () -> getPlayer().restartEngine()));\r\n    }\r\n\r\n    private void onDialogHide() {\r\n        //updateBackgroundPlayback();\r\n\r\n        for (Runnable listener : mHideListeners) {\r\n            listener.run();\r\n        }\r\n    }\r\n\r\n    //private void updateBackgroundPlayback() {\r\n    //    ViewManager.instance(getContext()).blockTop(null);\r\n    //\r\n    //    if (getPlayer() != null) {\r\n    //        getPlayer().setBackgroundMode(getPlayerData().getBackgroundMode());\r\n    //    }\r\n    //}\r\n\r\n    //private void addBackgroundPlaybackCategory() {\r\n    //    OptionCategory category =\r\n    //            AppDialogUtil.createBackgroundPlaybackCategory(getContext(), getPlayerData(), GeneralData.instance(getContext()), this::updateBackgroundPlayback);\r\n    //\r\n    //    addCategoryInt(category);\r\n    //}\r\n\r\n    private void addPresetsCategory() {\r\n        addCategoryInt(AppDialogUtil.createVideoPresetsCategory(\r\n                getContext(), () -> {\r\n                    if (getPlayer() == null) {\r\n                        return;\r\n                    }\r\n\r\n                    FormatItem format = getPlayerData().getFormat(FormatItem.TYPE_VIDEO);\r\n                    getPlayer().setFormat(format);\r\n\r\n                    if (!getPlayer().containsMedia()) {\r\n                        getPlayer().reloadPlayback();\r\n                    }\r\n\r\n                    // Make result easily be spotted by the user\r\n                    getPlayer().showOverlay(false);\r\n                }\r\n        ));\r\n    }\r\n\r\n    private void addVideoZoomCategory() {\r\n        addCategoryInt(AppDialogUtil.createVideoZoomCategory(\r\n                getContext(), () -> {\r\n                    getPlayer().setResizeMode(getPlayerData().getResizeMode());\r\n                    getPlayer().setZoomPercents(getPlayerData().getZoomPercents());\r\n\r\n                    // Make result easily be spotted by the user\r\n                    getPlayer().showOverlay(false);\r\n                }));\r\n    }\r\n\r\n    private void removeCategoryInt(int id) {\r\n        mCategoriesInt.remove(id);\r\n    }\r\n\r\n    private void addCategoryInt(OptionCategory category) {\r\n        mCategoriesInt.put(category.id, category);\r\n    }\r\n\r\n    public void removeCategory(int id) {\r\n        mCategories.remove(id);\r\n    }\r\n\r\n    public void addCategory(OptionCategory category) {\r\n        mCategories.put(category.id, category);\r\n    }\r\n\r\n    public void addOnDialogHide(Runnable listener) {\r\n        mHideListeners.add(listener);\r\n    }\r\n\r\n    public void removeOnDialogHide(Runnable listener) {\r\n        mHideListeners.remove(listener);\r\n    }\r\n\r\n    private void appendOptions(Map<Integer, OptionCategory> categories) {\r\n        for (OptionCategory category : categories.values()) {\r\n            mAppDialogPresenter.appendCategory(category);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/PlayerUIController.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers;\r\n\r\nimport android.os.Handler;\r\nimport android.os.Looper;\r\nimport android.view.KeyEvent;\r\n\r\nimport com.liskovsoft.googlecommon.common.helpers.ServiceHelper;\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItem;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.NotificationState;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.PlaylistInfo;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.KeyHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.BasePlayerController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerConstants;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerUI;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionCategory;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.ChannelPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.SearchPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter.VideoMenuCallback;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.AutoFrameRateSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.SubtitleTrack;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.ScreensaverManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.SearchData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeSignInService;\r\nimport io.reactivex.Observable;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class PlayerUIController extends BasePlayerController {\r\n    private static final String TAG = PlayerUIController.class.getSimpleName();\r\n    private static final long SUGGESTIONS_RESET_TIMEOUT_MS = 500;\r\n    private final Handler mHandler;\r\n    private final MediaItemService mMediaItemService;\r\n    private SuggestionsController mSuggestionsController;\r\n    private List<PlaylistInfo> mPlaylistInfos;\r\n    private FormatItem mAudioFormat = FormatItem.AUDIO_HQ_MP4A;\r\n    private boolean mEngineReady;\r\n    private boolean mDebugViewEnabled;\r\n    private boolean mIsMetadataLoaded;\r\n    private long mOverlayHideTimeMs;\r\n    private final Runnable mSuggestionsResetHandler = () -> {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n        getPlayer().resetSuggestedPosition();\r\n    };\r\n    private final Runnable mUiAutoHideHandler = () -> {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        // Playing the video and dialog overlay isn't shown\r\n        if (getPlayer().isPlaying() && !getAppDialogPresenter().isDialogShown()) {\r\n            if (getPlayer().isControlsShown()) { // don't hide when suggestions is shown\r\n                getPlayer().showOverlay(false);\r\n                mOverlayHideTimeMs = System.currentTimeMillis();\r\n            }\r\n        } else {\r\n            // in seeking state? doing recheck...\r\n            enableUiAutoHideTimeout();\r\n        }\r\n    };\r\n    private final Runnable mSetSubtitleButtonState = this::setSubtitleButtonState;\r\n    private final Runnable mSetPlaylistAddButtonState = this::setPlaylistAddButtonState;\r\n\r\n    public PlayerUIController() {\r\n        mHandler = new Handler(Looper.getMainLooper());\r\n\r\n        ServiceManager service = YouTubeServiceManager.instance();\r\n        mMediaItemService = service.getMediaItemService();\r\n    }\r\n\r\n    @Override\r\n    public void onInit() {\r\n        mSuggestionsController = getController(SuggestionsController.class);\r\n\r\n        if (getPlayer() != null) {\r\n            // Could be set once per activity creation (view layout stuff)\r\n            getPlayer().setResizeMode(getPlayerData().getResizeMode());\r\n            getPlayer().setZoomPercents(getPlayerData().getZoomPercents());\r\n            getPlayer().setAspectRatio(getPlayerData().getAspectRatio());\r\n            getPlayer().setRotationAngle(getPlayerData().getRotationAngle());\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onNewVideo(Video item) {\r\n        enableUiAutoHideTimeout();\r\n\r\n        if (item != null && !item.equals(getVideo())) {\r\n            mIsMetadataLoaded = false; // metadata isn't loaded yet at this point\r\n            resetButtonStates();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onControlsShown(boolean shown) {\r\n        disableUiAutoHideTimeout();\r\n\r\n        if (shown) {\r\n            enableUiAutoHideTimeout();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public boolean onKeyDown(int keyCode) {\r\n        disableUiAutoHideTimeout();\r\n        disableSuggestionsResetTimeout();\r\n\r\n        if (getPlayer() == null) {\r\n            return false;\r\n        }\r\n\r\n        boolean isHandled = handleBackKey(keyCode) || handleMenuKey(keyCode) ||\r\n                handleConfirmKey(keyCode) || handleStopKey(keyCode) || handleNumKeys(keyCode) ||\r\n                handlePlayPauseKey(keyCode) || handleLeftRightSkip(keyCode) || handleUpDownSkip(keyCode);\r\n\r\n        if (isHandled) {\r\n            return true; // don't show UI\r\n        }\r\n\r\n        enableUiAutoHideTimeout();\r\n\r\n        return false;\r\n    }\r\n\r\n    private void onSubtitleClicked(int buttonState) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        boolean enabled = buttonState == PlayerUI.BUTTON_ON;\r\n\r\n        // First run\r\n        if (FormatItem.SUBTITLE_NONE.equals(getPlayerData().getLastSubtitleFormat())) {\r\n            onSubtitleLongClicked();\r\n            return;\r\n        }\r\n\r\n        // Only default in the list\r\n        if (getPlayer().getSubtitleFormats() == null || getPlayer().getSubtitleFormats().size() == 1) {\r\n            return;\r\n        }\r\n\r\n        FormatItem matchedFormat = null;\r\n\r\n        for (FormatItem item : getPlayerData().getLastSubtitleFormats()) {\r\n            if (getPlayer().getSubtitleFormats().contains(item)) {\r\n                matchedFormat = item;\r\n                break;\r\n            }\r\n        }\r\n\r\n        // Match found\r\n        if (matchedFormat != null) {\r\n            FormatItem format = enabled ? FormatItem.SUBTITLE_NONE : matchedFormat;\r\n            getPlayer().setFormat(format);\r\n            getPlayerData().setFormat(format);\r\n            getPlayer().setButtonState(R.id.lb_control_closed_captioning, !FormatItem.SUBTITLE_NONE.equals(matchedFormat) && !enabled ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n            enableSubtitleForChannel(!enabled);\r\n        } else {\r\n            // Match not found\r\n            onSubtitleLongClicked();\r\n        }\r\n    }\r\n\r\n    private void onSubtitleLongClicked() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        fitVideoIntoDialog();\r\n\r\n        String subtitlesOrigCategoryTitle = getContext().getString(R.string.subtitle_category_title);\r\n        String subtitlesAutoCategoryTitle = subtitlesOrigCategoryTitle + \" (\" + getContext().getString(R.string.autogenerated) + \")\";\r\n\r\n        AppDialogPresenter settingsPresenter = getAppDialogPresenter();\r\n\r\n        settingsPresenter.appendSingleButton(UiOptionItem.from(subtitlesOrigCategoryTitle, optionItem -> {\r\n            List<FormatItem> subtitleFormats = getPlayer().getSubtitleFormats();\r\n            List<FormatItem> subtitleOrigFormats = Helpers.filter(subtitleFormats,\r\n                    value -> value.isDefault() || !SubtitleTrack.isAuto(value.getLanguage()));\r\n            reorderSubtitles(subtitleOrigFormats);\r\n            settingsPresenter.appendRadioCategory(subtitlesOrigCategoryTitle,\r\n                    UiOptionItem.from(subtitleOrigFormats,\r\n                            option -> {\r\n                                FormatItem format = UiOptionItem.toFormat(option);\r\n                                enableSubtitleForChannel(!format.isDefault());\r\n                                getPlayer().setFormat(format);\r\n                                getPlayerData().setFormat(format);\r\n                            },\r\n                            getContext().getString(R.string.subtitles_disabled)));\r\n            settingsPresenter.showDialog();\r\n        }));\r\n\r\n        settingsPresenter.appendSingleButton(UiOptionItem.from(subtitlesAutoCategoryTitle, optionItem -> {\r\n            List<FormatItem> subtitleFormats = getPlayer().getSubtitleFormats();\r\n            List<FormatItem> subtitleAutoFormats = Helpers.filter(subtitleFormats,\r\n                    value -> value.isDefault() || SubtitleTrack.isAuto(value.getLanguage()));\r\n            reorderSubtitles(subtitleAutoFormats);\r\n            settingsPresenter.appendRadioCategory(subtitlesAutoCategoryTitle,\r\n                    UiOptionItem.from(subtitleAutoFormats,\r\n                            option -> {\r\n                                FormatItem format = UiOptionItem.toFormat(option);\r\n                                enableSubtitleForChannel(!format.isDefault());\r\n                                getPlayer().setFormat(format);\r\n                                getPlayerData().setFormat(format);\r\n                            },\r\n                            getContext().getString(R.string.subtitles_disabled)));\r\n            settingsPresenter.showDialog();\r\n        }));\r\n\r\n        settingsPresenter.appendSingleSwitch(AppDialogUtil.createSubtitleChannelOption(getContext()));\r\n\r\n        OptionCategory stylesCategory = AppDialogUtil.createSubtitleStylesCategory(getContext());\r\n        settingsPresenter.appendRadioCategory(stylesCategory.title, stylesCategory.options);\r\n\r\n        OptionCategory sizeCategory = AppDialogUtil.createSubtitleSizeCategory(getContext());\r\n        settingsPresenter.appendRadioCategory(sizeCategory.title, sizeCategory.options);\r\n\r\n        OptionCategory positionCategory = AppDialogUtil.createSubtitlePositionCategory(getContext());\r\n        settingsPresenter.appendRadioCategory(positionCategory.title, positionCategory.options);\r\n\r\n        settingsPresenter.showDialog(subtitlesOrigCategoryTitle, mSetSubtitleButtonState);\r\n    }\r\n\r\n    private void onPlaylistAddClicked() {\r\n        fitVideoIntoDialog();\r\n\r\n        if (mPlaylistInfos == null) {\r\n            AppDialogUtil.showAddToPlaylistDialog(getContext(), getVideo(),\r\n                    null);\r\n        } else {\r\n            AppDialogUtil.showAddToPlaylistDialog(getContext(), getVideo(),\r\n                    null, mPlaylistInfos, mSetPlaylistAddButtonState);\r\n        }\r\n    }\r\n\r\n    private void onDebugInfoClicked(int buttonState) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        boolean enabled = buttonState == PlayerUI.BUTTON_ON;\r\n\r\n        mDebugViewEnabled = !enabled;\r\n        getPlayer().showDebugInfo(mDebugViewEnabled);\r\n        getPlayer().setButtonState(R.id.action_video_stats, mDebugViewEnabled ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n    }\r\n\r\n    @Override\r\n    public void onEngineInitialized() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        mEngineReady = true;\r\n\r\n        if (isEmbedPlayer()) {\r\n            return;\r\n        }\r\n\r\n        if (getAppDialogPresenter().isDialogShown()) {\r\n            // Activate debug infos/show ui after engine restarting (buffering, sound shift, error?).\r\n            getPlayer().showOverlay(true);\r\n            getPlayer().showDebugInfo(mDebugViewEnabled);\r\n            getPlayer().setButtonState(R.id.action_video_stats, mDebugViewEnabled ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n        }\r\n        \r\n        if (getPlayerTweaksData().isScreenOffTimeoutEnabled() || getPlayerTweaksData().isBootScreenOffEnabled()) {\r\n            prepareScreenOff();\r\n            applyScreenOff(PlayerUI.BUTTON_OFF);\r\n            applyScreenOffTimeout(PlayerUI.BUTTON_OFF);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onVideoLoaded(Video item) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        getPlayer().updateEndingTime();\r\n        applySoundOffButtonState();\r\n    }\r\n\r\n    @Override\r\n    public void onSeekEnd() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        getPlayer().updateEndingTime();\r\n    }\r\n\r\n    @Override\r\n    public void onViewResumed() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        // Reset temp mode.\r\n        getSearchData().setTempBackgroundModeClass(null);\r\n\r\n        // Activate debug infos when restoring after PIP.\r\n        getPlayer().showDebugInfo(mDebugViewEnabled);\r\n        getPlayer().setButtonState(R.id.action_video_stats, mDebugViewEnabled ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n        getPlayer().showSubtitles(true);\r\n\r\n        // Maybe dialog just closed. Reset timeout just in case.\r\n        enableUiAutoHideTimeout();\r\n        applySoundOffButtonState();\r\n    }\r\n\r\n    @Override\r\n    public void onViewPaused() {\r\n        if (getPlayer() != null && getPlayer().isInPIPMode()) {\r\n            // UI couldn't be properly displayed in PIP mode\r\n            getPlayer().showOverlay(false);\r\n            getPlayer().showDebugInfo(false);\r\n            getPlayer().setButtonState(R.id.action_video_stats, PlayerUI.BUTTON_OFF);\r\n            getPlayer().showSubtitles(false);\r\n        }\r\n    }\r\n\r\n    private void resetButtonStates() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        getPlayer().setButtonState(R.id.action_thumbs_up, PlayerUI.BUTTON_OFF);\r\n        getPlayer().setButtonState(R.id.action_thumbs_down, PlayerUI.BUTTON_OFF);\r\n        getPlayer().setChannelIcon(null);\r\n        getPlayer().setButtonState(R.id.action_playlist_add, PlayerUI.BUTTON_OFF);\r\n        getPlayer().setButtonState(R.id.lb_control_closed_captioning, PlayerUI.BUTTON_OFF);\r\n        getPlayer().setButtonState(R.id.action_video_speed, PlayerUI.BUTTON_OFF);\r\n        getPlayer().setButtonState(R.id.action_chat, PlayerUI.BUTTON_OFF);\r\n        getPlayer().setButtonState(R.id.action_subscribe, PlayerUI.BUTTON_OFF);\r\n    }\r\n\r\n    @Override\r\n    public void onEngineReleased() {\r\n        Log.d(TAG, \"Engine released. Disabling all callbacks...\");\r\n        mEngineReady = false;\r\n\r\n        disposeTimeouts();\r\n    }\r\n\r\n    @Override\r\n    public void onMetadata(MediaItemMetadata metadata) {\r\n        mIsMetadataLoaded = true;\r\n        if (getPlayerData().getSeekPreviewMode() != PlayerData.SEEK_PREVIEW_NONE) {\r\n            getPlayer().loadStoryboard();\r\n        }\r\n        getPlayer().setButtonState(R.id.action_thumbs_up, metadata.getLikeStatus() == MediaItemMetadata.LIKE_STATUS_LIKE ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n        getPlayer().setButtonState(R.id.action_thumbs_down, metadata.getLikeStatus() == MediaItemMetadata.LIKE_STATUS_DISLIKE ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n        if (getPlayerTweaksData().isRealChannelIconEnabled()) {\r\n            getPlayer().setChannelIcon(metadata.getAuthorImageUrl());\r\n        }\r\n        setPlaylistAddButtonStateCached();\r\n        setSubtitleButtonState();\r\n        getPlayer().setButtonState(R.id.action_rotate, getPlayerData().getRotationAngle() == 0 ? PlayerUI.BUTTON_OFF : PlayerUI.BUTTON_ON);\r\n        getPlayer().setButtonState(R.id.action_subscribe, metadata.isSubscribed() ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n        getPlayer().setButtonState(R.id.action_afr, getPlayerData().isAfrEnabled() ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n    }\r\n\r\n    @Override\r\n    public void onSuggestionItemLongClicked(Video item) {\r\n        VideoMenuPresenter.instance(getContext()).showMenu(item, (videoItem, action) -> {\r\n            if (getPlayer() == null || item.getGroup() == null)\r\n                return;\r\n\r\n            if (action == VideoMenuCallback.ACTION_REMOVE_FROM_QUEUE\r\n                    || action == VideoMenuCallback.ACTION_REMOVE_FROM_PLAYLIST\r\n                    || action == VideoMenuCallback.ACTION_REMOVE) {\r\n                int id = item.getGroup().getId();\r\n                VideoGroup group = VideoGroup.from(videoItem);\r\n                group.setId(id);\r\n                getPlayer().removeSuggestions(group);\r\n            } else if (action == VideoMenuCallback.ACTION_ADD_TO_QUEUE || action == VideoMenuCallback.ACTION_PLAY_NEXT) {\r\n                String title = getContext().getString(R.string.action_playback_queue);\r\n                int id = title.hashCode();\r\n                Video newItem = videoItem.copy();\r\n                VideoGroup group = VideoGroup.from(newItem, 0);\r\n                group.setTitle(title);\r\n                group.setId(id);\r\n                group.setType(MediaGroup.TYPE_PLAYBACK_QUEUE);\r\n                newItem.setGroup(group);\r\n                if (action == VideoMenuCallback.ACTION_PLAY_NEXT) {\r\n                    group.setAction(VideoGroup.ACTION_PREPEND);\r\n                }\r\n                getPlayer().updateSuggestions(group);\r\n                getPlayer().setNextTitle(mSuggestionsController.getNext());\r\n            }\r\n        });\r\n    }\r\n\r\n    private void onDislikeClicked(int buttonState) {\r\n        if (getPlayer() == null)\r\n            return;\r\n\r\n        boolean dislike = buttonState == PlayerUI.BUTTON_ON;\r\n\r\n        getPlayer().setButtonState(R.id.action_thumbs_down, !dislike ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n\r\n        if (!mIsMetadataLoaded) {\r\n            MessageHelpers.showMessage(getContext(), R.string.wait_data_loading);\r\n            return;\r\n        }\r\n\r\n        if (!YouTubeSignInService.instance().isSigned()) {\r\n            getPlayer().setButtonState(R.id.action_thumbs_down, PlayerUI.BUTTON_OFF);\r\n            MessageHelpers.showMessage(getContext(), R.string.msg_signed_users_only);\r\n            return;\r\n        }\r\n\r\n        if (!dislike) {\r\n            callMediaItemObservable(mMediaItemService::setDislikeObserve);\r\n        } else {\r\n            callMediaItemObservable(mMediaItemService::removeDislikeObserve);\r\n        }\r\n    }\r\n\r\n    private void onLikeClicked(int buttonState) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        boolean like = buttonState == PlayerUI.BUTTON_ON;\r\n\r\n        getPlayer().setButtonState(R.id.action_thumbs_up, !like ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n\r\n        if (!mIsMetadataLoaded) {\r\n            MessageHelpers.showMessage(getContext(), R.string.wait_data_loading);\r\n            return;\r\n        }\r\n\r\n        if (!YouTubeSignInService.instance().isSigned()) {\r\n            getPlayer().setButtonState(R.id.action_thumbs_up, PlayerUI.BUTTON_OFF);\r\n            MessageHelpers.showMessage(getContext(), R.string.msg_signed_users_only);\r\n            return;\r\n        }\r\n\r\n        if (!like) {\r\n            callMediaItemObservable(mMediaItemService::setLikeObserve);\r\n        } else {\r\n            callMediaItemObservable(mMediaItemService::removeLikeObserve);\r\n        }\r\n    }\r\n\r\n    private void onSeekInterval() {\r\n        fitVideoIntoDialog();\r\n\r\n        AppDialogPresenter settingsPresenter = getAppDialogPresenter();\r\n\r\n        AppDialogUtil.appendSeekIntervalDialogItems(getContext(), settingsPresenter, getPlayerData(), true);\r\n\r\n        settingsPresenter.showDialog();\r\n    }\r\n\r\n    private void onVideoInfoClicked() {\r\n        fitVideoIntoDialog();\r\n\r\n        if (!mIsMetadataLoaded) {\r\n            MessageHelpers.showMessage(getContext(), R.string.wait_data_loading);\r\n            return;\r\n        }\r\n\r\n        Video video = getVideo();\r\n\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        String description = video.description;\r\n\r\n        if (description == null || description.isEmpty()) {\r\n            MessageHelpers.showMessage(getContext(), R.string.description_not_found);\r\n            return;\r\n        }\r\n\r\n        AppDialogPresenter dialogPresenter = getAppDialogPresenter();\r\n\r\n        String title = String.format(\"%s - %s\", video.getTitleFull(), video.getAuthor());\r\n\r\n        dialogPresenter.appendLongTextCategory(title, UiOptionItem.from(description));\r\n\r\n        dialogPresenter.showDialog(title);\r\n    }\r\n\r\n    private void onShareLink() {\r\n        fitVideoIntoDialog();\r\n\r\n        Video video = getVideo();\r\n\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        AppDialogPresenter dialogPresenter = getAppDialogPresenter();\r\n\r\n        int positionSec = Utils.toSec(getPlayer().getPositionMs());\r\n        AppDialogUtil.appendShareLinkDialogItem(getContext(), dialogPresenter, getVideo(), positionSec);\r\n        AppDialogUtil.appendShareQRLinkDialogItem(getContext(), dialogPresenter, getVideo(), positionSec);\r\n        AppDialogUtil.appendShareEmbedLinkDialogItem(getContext(), dialogPresenter, getVideo(), positionSec);\r\n\r\n        dialogPresenter.showDialog(getVideo().getTitle());\r\n    }\r\n\r\n    private void onSearchClicked() {\r\n        startTempBackgroundMode(SearchPresenter.class);\r\n        SearchPresenter.instance(getContext()).startSearch(null);\r\n    }\r\n    \r\n    private void onVideoZoom() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        OptionCategory videoZoomCategory = AppDialogUtil.createVideoZoomCategory(\r\n                getContext(), () -> {\r\n                    getPlayer().setResizeMode(getPlayerData().getResizeMode());\r\n                    getPlayer().setZoomPercents(getPlayerData().getZoomPercents());\r\n                    getPlayer().showControls(false);\r\n                });\r\n\r\n        OptionCategory videoAspectCategory = AppDialogUtil.createVideoAspectCategory(\r\n                getContext(), getPlayerData(), () -> getPlayer().setAspectRatio(getPlayerData().getAspectRatio()));\r\n\r\n        OptionCategory videoRotateCategory = AppDialogUtil.createVideoRotateCategory(\r\n                getContext(), getPlayerData(), () -> getPlayer().setRotationAngle(getPlayerData().getRotationAngle()));\r\n\r\n        AppDialogPresenter settingsPresenter = getAppDialogPresenter();\r\n        settingsPresenter.appendRadioCategory(videoAspectCategory.title, videoAspectCategory.options);\r\n        settingsPresenter.appendRadioCategory(videoZoomCategory.title, videoZoomCategory.options);\r\n        settingsPresenter.appendRadioCategory(videoRotateCategory.title, videoRotateCategory.options);\r\n        settingsPresenter.showDialog(getContext().getString(R.string.video_aspect));\r\n    }\r\n\r\n    private void onPipClicked() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        getPlayer().showOverlay(false);\r\n        getPlayer().blockEngine(true);\r\n        getPlayer().finish();\r\n    }\r\n\r\n    @Override\r\n    public void onButtonClicked(int buttonId, int buttonState) {\r\n        if (getPlayer() == null || getPlayer().getButtonState(buttonId) != buttonState) {\r\n            return;\r\n        }\r\n\r\n        if (buttonId == R.id.action_rotate) {\r\n            onRotate();\r\n        } else if (buttonId == R.id.action_flip) {\r\n            onFlip();\r\n        } else if (buttonId == R.id.action_screen_dimming) {\r\n            prepareScreenOff();\r\n            applyScreenOff(buttonState);\r\n            applyScreenOffTimeout(buttonState);\r\n        } else if (buttonId == R.id.action_subscribe) {\r\n            onSubscribe(buttonState);\r\n        } else if (buttonId == R.id.action_sound_off) {\r\n            applySoundOff(buttonState);\r\n        } else if (buttonId == R.id.action_afr) {\r\n            applyAfr(buttonState);\r\n        } else if (buttonId == R.id.action_repeat) {\r\n            applyRepeatMode(buttonState);\r\n        } else if (buttonId == R.id.action_channel) {\r\n            openChannel();\r\n        } else if (buttonId == R.id.action_playback_queue) {\r\n            AppDialogUtil.showPlaybackQueueDialog(getContext(), item -> getMainController().onNewVideo(item));\r\n        } else if (buttonId == R.id.action_video_zoom) {\r\n            onVideoZoom();\r\n        } else if (buttonId == R.id.action_seek_interval) {\r\n            onSeekInterval();\r\n        } else if (buttonId == R.id.action_share) {\r\n            onShareLink();\r\n        } else if (buttonId == R.id.action_info) {\r\n            onVideoInfoClicked();\r\n        } else if (buttonId == R.id.action_pip) {\r\n            onPipClicked();\r\n        } else if (buttonId == R.id.action_search) {\r\n            onSearchClicked();\r\n        } else if (buttonId == R.id.action_video_stats) {\r\n            onDebugInfoClicked(buttonState);\r\n        } else if (buttonId == R.id.action_playlist_add) {\r\n            onPlaylistAddClicked();\r\n        } else if (buttonId == R.id.lb_control_closed_captioning) {\r\n            onSubtitleClicked(buttonState);\r\n        } else if (buttonId == R.id.action_thumbs_down) {\r\n            onDislikeClicked(buttonState);\r\n        } else if (buttonId == R.id.action_thumbs_up) {\r\n            onLikeClicked(buttonState);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onButtonLongClicked(int buttonId, int buttonState) {\r\n        if (buttonId == R.id.action_screen_dimming) {\r\n            showScreenOffDialog();\r\n        } else if (buttonId == R.id.action_subscribe || buttonId == R.id.action_channel) {\r\n            showNotificationsDialog();\r\n        } else if (buttonId == R.id.action_sound_off) {\r\n            showSoundOffDialog();\r\n        } else if (buttonId == R.id.action_afr) {\r\n            AutoFrameRateSettingsPresenter.instance(getContext()).show(() -> applyAfr(getPlayerData().isAfrEnabled() ? PlayerUI.BUTTON_OFF : PlayerUI.BUTTON_ON));\r\n        } else if (buttonId == R.id.action_repeat) {\r\n            showPlaybackModeDialog();\r\n        } else if (buttonId == R.id.lb_control_closed_captioning) {\r\n            onSubtitleLongClicked();\r\n        }\r\n    }\r\n\r\n    private void disableUiAutoHideTimeout() {\r\n        Log.d(TAG, \"Stopping auto hide ui timer...\");\r\n        mHandler.removeCallbacks(mUiAutoHideHandler);\r\n    }\r\n\r\n    private void enableUiAutoHideTimeout() {\r\n        Log.d(TAG, \"Starting auto hide ui timer...\");\r\n        disableUiAutoHideTimeout();\r\n        if (mEngineReady && getPlayerData().getUiHideTimeoutSec() > 0) {\r\n            mHandler.postDelayed(mUiAutoHideHandler, getPlayerData().getUiHideTimeoutSec() * 1_000L);\r\n        }\r\n    }\r\n\r\n    private void disableSuggestionsResetTimeout() {\r\n        Log.d(TAG, \"Stopping reset position timer...\");\r\n        mHandler.removeCallbacks(mSuggestionsResetHandler);\r\n    }\r\n\r\n    private void enableSuggestionsResetTimeout() {\r\n        Log.d(TAG, \"Starting reset position timer...\");\r\n        disableSuggestionsResetTimeout();\r\n        if (mEngineReady) {\r\n            mHandler.postDelayed(mSuggestionsResetHandler, SUGGESTIONS_RESET_TIMEOUT_MS);\r\n        }\r\n    }\r\n\r\n    private void disposeTimeouts() {\r\n        disableUiAutoHideTimeout();\r\n        disableSuggestionsResetTimeout();\r\n    }\r\n\r\n    private void callMediaItemObservable(MediaItemObservable callable) {\r\n        Video video = getVideo();\r\n\r\n        if (video == null) {\r\n            Log.e(TAG, \"Seems that video isn't initialized yet. Cancelling...\");\r\n            return;\r\n        }\r\n\r\n        Observable<Void> observable = callable.call(video.mediaItem != null ? video.mediaItem : video.toMediaItem());\r\n\r\n        RxHelper.execute(observable);\r\n    }\r\n\r\n    private boolean handleBackKey(int keyCode) {\r\n        if (KeyHelpers.isBackKey(keyCode)) {\r\n            enableSuggestionsResetTimeout();\r\n\r\n            // Close unplayable videos with single back click\r\n            // Cause the background playback bugs if not to check for upcoming or unplayable!!!\r\n            // To reproduce the bug:\r\n            // 1) Set bg black to \"Only audio when pressing HOME\"\r\n            // 2) Enable \"keep finished activities\"\r\n            // 3) Close the video when it fully finished and ready to skip to the next\r\n            if (getVideo() != null && getPlayer() != null &&\r\n                    (getVideo().isUnplayable || getVideo().isUpcoming) && getPlayer().isControlsShown()) {\r\n                getPlayer().finish();\r\n            }\r\n\r\n            // Back key cooling\r\n            if (System.currentTimeMillis() - mOverlayHideTimeMs < 1_000) {\r\n                return true;\r\n            }\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    private boolean handleMenuKey(int keyCode) {\r\n        boolean controlsShown = getPlayer().isOverlayShown();\r\n        boolean suggestionsShown = getPlayer().isSuggestionsShown();\r\n\r\n        if (KeyHelpers.isMenuKey(keyCode) && !suggestionsShown) {\r\n            getPlayer().showOverlay(!controlsShown);\r\n\r\n            if (controlsShown) {\r\n                enableSuggestionsResetTimeout();\r\n            }\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    private boolean handleConfirmKey(int keyCode) {\r\n        if (getPlayer() == null) {\r\n            return false;\r\n        }\r\n\r\n        boolean controlsShown = getPlayer().isOverlayShown();\r\n\r\n        if (KeyHelpers.isConfirmKey(keyCode) && !controlsShown) {\r\n            switch (getPlayerData().getOKButtonBehavior()) {\r\n                case PlayerData.OK_ONLY_UI:\r\n                    getPlayer().showOverlay(true);\r\n                    return true; // don't show ui\r\n                case PlayerData.OK_UI_AND_PAUSE:\r\n                    // NOP\r\n                    break;\r\n                case PlayerData.OK_ONLY_PAUSE:\r\n                    getPlayer().setPlayWhenReady(!getPlayer().getPlayWhenReady());\r\n                    return true; // don't show ui\r\n                case PlayerData.OK_TOGGLE_SPEED:\r\n                    getMainController().onButtonClicked(R.id.action_video_speed, getPlayer().getButtonState(R.id.action_video_speed));\r\n                    float speed = getPlayerData().getSpeed();\r\n                    MessageHelpers.showMessage(getContext(), String.format(\"%sx\", speed));\r\n                    return true;\r\n            }\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    private boolean handleStopKey(int keyCode) {\r\n        if (KeyHelpers.isStopKey(keyCode)) {\r\n            if (getPlayer() != null) {\r\n                getPlayer().finish();\r\n                return true;\r\n            }\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    private boolean handleNumKeys(int keyCode) {\r\n        if (getPlayerData().isNumberKeySeekEnabled() && keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {\r\n            if (getPlayer() != null && getPlayer().getDurationMs() > 0) {\r\n                float seekPercent = (keyCode - KeyEvent.KEYCODE_0) / 10f;\r\n                long positionMs = (long) (getPlayer().getDurationMs() * seekPercent);\r\n                getPlayer().setPositionMs(positionMs);\r\n                MessageHelpers.showMessage(getContext(), ServiceHelper.millisToTimeText(positionMs));\r\n            }\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    private boolean handlePlayPauseKey(int keyCode) {\r\n        if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE && getPlayer() != null) {\r\n            getPlayer().setPlayWhenReady(!getPlayer().getPlayWhenReady());\r\n            enableUiAutoHideTimeout(); // TODO: move out somehow\r\n            return true;\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    private boolean handleLeftRightSkip(int keyCode) {\r\n        if (getPlayer() == null || getPlayer().isOverlayShown() || getVideo() == null ||\r\n                (getVideo().belongsToShortsGroup() && !getPlayerTweaksData().isQuickSkipShortsEnabled() ||\r\n                (!getVideo().belongsToShortsGroup() && !getPlayerTweaksData().isQuickSkipVideosEnabled()))) {\r\n            return false;\r\n        }\r\n\r\n        if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {\r\n            getMainController().onNextClicked();\r\n            return true; // hide ui\r\n        }\r\n\r\n        if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {\r\n            getMainController().onPreviousClicked();\r\n            return true; // hide ui\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    private boolean handleUpDownSkip(int keyCode) {\r\n        if (getPlayer() == null || getPlayer().isOverlayShown() || getVideo() == null ||\r\n                (getVideo().belongsToShortsGroup() && !getPlayerTweaksData().isQuickSkipShortsAltEnabled() ||\r\n                        (!getVideo().belongsToShortsGroup() && !getPlayerTweaksData().isQuickSkipVideosAltEnabled()))) {\r\n            return false;\r\n        }\r\n\r\n        if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {\r\n            getMainController().onNextClicked();\r\n            return true; // hide ui\r\n        }\r\n\r\n        if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {\r\n            getMainController().onPreviousClicked();\r\n            return true; // hide ui\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    private interface MediaItemObservable {\r\n        Observable<Void> call(MediaItem item);\r\n    }\r\n\r\n    private void setPlaylistAddButtonStateCached() {\r\n        if (getVideo() == null) {\r\n            return;\r\n        }\r\n\r\n        String videoId = getVideo().videoId;\r\n        mPlaylistInfos = null;\r\n        Disposable playlistsInfoAction =\r\n                YouTubeServiceManager.instance().getMediaItemService().getPlaylistsInfoObserve(videoId)\r\n                        .subscribe(\r\n                                videoPlaylistInfos -> {\r\n                                    mPlaylistInfos = videoPlaylistInfos;\r\n                                    setPlaylistAddButtonState();\r\n                                },\r\n                                error -> Log.e(TAG, \"Add to recent playlist error: %s\", error.getMessage())\r\n                        );\r\n    }\r\n\r\n    private void setPlaylistAddButtonState() {\r\n        if (mPlaylistInfos == null || getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        boolean isSelected = false;\r\n        for (PlaylistInfo playlistInfo : mPlaylistInfos) {\r\n            if (playlistInfo.isSelected()) {\r\n                isSelected = true;\r\n                break;\r\n            }\r\n        }\r\n\r\n        getPlayer().setButtonState(R.id.action_playlist_add, isSelected ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n    }\r\n\r\n    private void setSubtitleButtonState() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        getPlayer().setButtonState(R.id.lb_control_closed_captioning, isSubtitleEnabled() && isSubtitleSelected() ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n    }\r\n\r\n    private void startTempBackgroundMode(Class<?> clazz) {\r\n        SearchData searchData = getSearchData();\r\n        if (searchData.isTempBackgroundModeEnabled()) {\r\n            searchData.setTempBackgroundModeClass(clazz);\r\n            onPipClicked();\r\n        }\r\n    }\r\n\r\n    private boolean isSubtitleSelected() {\r\n        if (getPlayer() == null) {\r\n            return false;\r\n        }\r\n\r\n        List<FormatItem> subtitleFormats = getPlayer().getSubtitleFormats();\r\n\r\n        if (subtitleFormats == null) {\r\n            return false;\r\n        }\r\n\r\n        boolean isSelected = false;\r\n\r\n        for (FormatItem subtitle : subtitleFormats) {\r\n            if (subtitle.isSelected() && !subtitle.isDefault()) {\r\n                isSelected = true;\r\n                break;\r\n            }\r\n        }\r\n\r\n        return isSelected;\r\n    }\r\n\r\n    private boolean isSubtitleEnabled() {\r\n        return !getPlayerData().isSubtitlesPerChannelEnabled() || getPlayerData().isSubtitlesPerChannelEnabled(getChannelId());\r\n    }\r\n\r\n    private void enableSubtitleForChannel(boolean enable) {\r\n        if (getPlayer() == null || !getPlayerData().isSubtitlesPerChannelEnabled()) {\r\n            return;\r\n        }\r\n\r\n        String channelId = getChannelId();\r\n        if (enable) {\r\n            getPlayerData().enableSubtitlesPerChannel(channelId);\r\n        } else {\r\n            getPlayerData().disableSubtitlesPerChannel(channelId);\r\n        }\r\n    }\r\n\r\n    private String getChannelId() {\r\n        return getVideo() != null ? getVideo().channelId : null;\r\n    }\r\n\r\n    private void applyScreenOff(int buttonState) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        ScreensaverManager manager = getScreensaverManager();\r\n\r\n        if (manager == null) {\r\n            return;\r\n        }\r\n\r\n        if (getPlayerTweaksData().getScreenOffTimeoutSec() == 0) {\r\n            boolean isPartialDimming = getPlayerTweaksData().getScreenOffDimmingPercents() < 100;\r\n            getPlayerTweaksData().setBootScreenOffEnabled(buttonState == PlayerUI.BUTTON_OFF && isPartialDimming);\r\n            if (buttonState == PlayerUI.BUTTON_OFF) {\r\n                manager.doScreenOff();\r\n                manager.setBlocked(isPartialDimming);\r\n                getPlayer().setButtonState(R.id.action_screen_dimming, isPartialDimming ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n            }\r\n        }\r\n    }\r\n\r\n    private void applyScreenOffTimeout(int buttonState) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        if (getPlayerTweaksData().getScreenOffTimeoutSec() > 0) {\r\n            getPlayerTweaksData().setScreenOffTimeoutEnabled(buttonState == PlayerUI.BUTTON_OFF);\r\n            getPlayer().setButtonState(R.id.action_screen_dimming, buttonState == PlayerUI.BUTTON_OFF ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n        }\r\n    }\r\n\r\n    private void prepareScreenOff() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        ScreensaverManager manager = getScreensaverManager();\r\n\r\n        if (manager == null) {\r\n            return;\r\n        }\r\n\r\n        manager.setBlocked(false);\r\n        manager.disable();\r\n        getPlayer().setButtonState(R.id.action_screen_dimming, PlayerUI.BUTTON_OFF);\r\n    }\r\n\r\n    private void onRotate() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        int oldRotation = getPlayerData().getRotationAngle();\r\n        int rotation = oldRotation == 0 ? 90 : oldRotation == 90 ? 180 : oldRotation == 180 ? 270 : 0;\r\n        getPlayer().setRotationAngle(rotation);\r\n        getPlayer().setButtonState(R.id.action_rotate, rotation == 0 ? PlayerUI.BUTTON_OFF : PlayerUI.BUTTON_ON);\r\n        getPlayerData().setRotationAngle(rotation);\r\n    }\r\n\r\n    private void onFlip() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        boolean flipEnabled = getPlayerData().isVideoFlipEnabled();\r\n        boolean newFlipEnabled = !flipEnabled;\r\n        getPlayer().setVideoFlipEnabled(newFlipEnabled);\r\n        getPlayer().setButtonState(R.id.action_flip, newFlipEnabled ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n        getPlayerData().setVideoFlipEnabled(newFlipEnabled);\r\n    }\r\n\r\n    private void onSubscribe(int buttonState) {\r\n        if (getPlayer() == null || getVideo() == null) {\r\n            return;\r\n        }\r\n\r\n        if (!mIsMetadataLoaded) {\r\n            MessageHelpers.showMessage(getContext(), R.string.wait_data_loading);\r\n            return;\r\n        }\r\n\r\n        if (buttonState == PlayerUI.BUTTON_OFF) {\r\n            callMediaItemObservable(mMediaItemService::subscribeObserve);\r\n        } else {\r\n            callMediaItemObservable(mMediaItemService::unsubscribeObserve);\r\n        }\r\n\r\n        getVideo().isSubscribed = buttonState == PlayerUI.BUTTON_OFF;\r\n        getPlayer().setButtonState(R.id.action_subscribe, buttonState == PlayerUI.BUTTON_OFF ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n    }\r\n\r\n    private void applySoundOff(int buttonState) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        if (buttonState == PlayerUI.BUTTON_OFF) {\r\n            mAudioFormat = getPlayer().getAudioFormat();\r\n            getPlayer().setFormat(FormatItem.NO_AUDIO);\r\n            getPlayerData().setFormat(FormatItem.NO_AUDIO);\r\n        } else {\r\n            getPlayer().setFormat(mAudioFormat);\r\n            getPlayerData().setFormat(mAudioFormat);\r\n        }\r\n\r\n        getPlayer().setButtonState(R.id.action_sound_off, buttonState == PlayerUI.BUTTON_OFF ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n    }\r\n\r\n    private void applySoundOffButtonState() {\r\n        if (getPlayer() != null && getPlayer().getAudioFormat() != null) {\r\n            getPlayer().setButtonState(R.id.action_sound_off,\r\n                    (getPlayer().getAudioFormat().isDefault() || getPlayerData().getPlayerVolume() == 0) ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n        }\r\n    }\r\n\r\n    private void applyAfr(int buttonState) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        getPlayerData().setAfrEnabled(buttonState == PlayerUI.BUTTON_OFF);\r\n        getController(AutoFrameRateController.class).applyAfr();\r\n        getPlayer().setButtonState(R.id.action_afr, buttonState == PlayerUI.BUTTON_OFF ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n    }\r\n\r\n    private void applyRepeatMode(int buttonState) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        int nextMode = getNextRepeatMode(buttonState);\r\n\r\n        getPlayerData().setPlaybackMode(nextMode);\r\n        getPlayer().setButtonState(R.id.action_repeat, nextMode);\r\n    }\r\n\r\n    private void showPlaybackModeDialog() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        OptionCategory category = AppDialogUtil.createPlaybackModeCategory(\r\n                getContext(), () -> {\r\n                    getPlayer().setButtonState(R.id.action_repeat, getPlayerData().getPlaybackMode());\r\n                });\r\n\r\n        AppDialogPresenter settingsPresenter = getAppDialogPresenter();\r\n        settingsPresenter.appendRadioCategory(category.title, category.options);\r\n        settingsPresenter.showDialog();\r\n    }\r\n\r\n    private int getNextRepeatMode(int buttonState) {\r\n        Integer[] modeList = {PlayerConstants.PLAYBACK_MODE_ALL, PlayerConstants.PLAYBACK_MODE_ONE, PlayerConstants.PLAYBACK_MODE_SHUFFLE,\r\n                PlayerConstants.PLAYBACK_MODE_LIST, PlayerConstants.PLAYBACK_MODE_REVERSE_LIST, PlayerConstants.PLAYBACK_MODE_PAUSE, PlayerConstants.PLAYBACK_MODE_CLOSE};\r\n        int nextMode = Helpers.getNextValue(modeList, buttonState);\r\n        return nextMode;\r\n    }\r\n\r\n    private void reorderSubtitles(List<FormatItem> subtitleFormats) {\r\n        if (subtitleFormats == null || subtitleFormats.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        // Move last format to the top\r\n        int begin = subtitleFormats.get(0).isDefault() ? 1 : 0;\r\n        List<FormatItem> topSubtitles = new ArrayList<>();\r\n        for (FormatItem item : getPlayerData().getLastSubtitleFormats()) {\r\n            if (item == null || item.getLanguage() == null) { // skip empty formats\r\n                continue;\r\n            }\r\n            int index = 0;\r\n            while (index != -1) {\r\n                index = subtitleFormats.indexOf(item);\r\n                if (index != -1) {\r\n                    topSubtitles.add(subtitleFormats.remove(index));\r\n                }\r\n            }\r\n        }\r\n        subtitleFormats.addAll(subtitleFormats.size() < begin ? 0 : begin, topSubtitles);\r\n    }\r\n\r\n    private void showNotificationsDialog() {\r\n        if (getPlayer() == null || getVideo() == null || getVideo().notificationStates == null) {\r\n            return;\r\n        }\r\n\r\n        AppDialogPresenter settingsPresenter = getAppDialogPresenter();\r\n\r\n        List<OptionItem> items = new ArrayList<>();\r\n\r\n        for (NotificationState item : getVideo().notificationStates) {\r\n            items.add(UiOptionItem.from(item.getTitle(), optionItem -> {\r\n                if (optionItem.isSelected()) {\r\n                    MediaServiceManager.instance().setNotificationState(item, error -> MessageHelpers.showMessage(getContext(), error.getLocalizedMessage()));\r\n                    getVideo().isSubscribed = true;\r\n                    getPlayer().setButtonState(R.id.action_subscribe, PlayerUI.BUTTON_ON);\r\n                }\r\n            }, item.isSelected()));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.header_notifications), items);\r\n        settingsPresenter.showDialog(getContext().getString(R.string.header_notifications));\r\n    }\r\n\r\n    private void showScreenOffDialog() {\r\n        AppDialogPresenter settingsPresenter = getAppDialogPresenter();\r\n        OptionCategory dimmingCategory =\r\n                AppDialogUtil.createPlayerScreenOffDimmingCategory(getContext(), () -> {\r\n                    prepareScreenOff();\r\n                    applyScreenOff(PlayerUI.BUTTON_OFF);\r\n                });\r\n        OptionCategory category =\r\n                AppDialogUtil.createPlayerScreenOffTimeoutCategory(getContext(), () -> {\r\n                    prepareScreenOff();\r\n                    applyScreenOffTimeout(PlayerUI.BUTTON_OFF);\r\n                });\r\n        settingsPresenter.appendRadioCategory(dimmingCategory.title, dimmingCategory.options);\r\n        settingsPresenter.appendRadioCategory(category.title, category.options);\r\n        settingsPresenter.showDialog(getContext().getString(R.string.screen_dimming));\r\n    }\r\n\r\n    private void showSoundOffDialog() {\r\n        AppDialogPresenter settingsPresenter = getAppDialogPresenter();\r\n        OptionCategory audioVolumeCategory = AppDialogUtil.createAudioVolumeCategory(getContext(), () -> {\r\n            applySoundOff(getPlayerData().getPlayerVolume() == 0 ? PlayerUI.BUTTON_OFF : PlayerUI.BUTTON_ON);\r\n            getPlayer().setVolume(getPlayerData().getPlayerVolume());\r\n        });\r\n        OptionCategory pitchEffectCategory = AppDialogUtil.createPitchEffectCategory(getContext());\r\n        settingsPresenter.appendCategory(audioVolumeCategory);\r\n        settingsPresenter.appendCategory(pitchEffectCategory);\r\n        settingsPresenter.showDialog(getContext().getString(R.string.player_volume));\r\n    }\r\n\r\n    private void openChannel() {\r\n        startTempBackgroundMode(ChannelPresenter.class);\r\n        ChannelPresenter.instance(getContext()).openChannel(getVideo());\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/RemoteController.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers;\r\n\r\nimport android.content.Context;\r\nimport android.database.ContentObserver;\r\nimport android.os.Handler;\r\nimport android.os.Looper;\r\nimport android.view.KeyEvent;\r\nimport androidx.annotation.Nullable;\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.mediaserviceinterfaces.RemoteControlService;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.Command;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.BasePlayerController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerConstants;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerUI;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.common.DataChangeBase.OnDataChange;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.RemoteControlData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\nimport io.reactivex.disposables.Disposable;\r\nimport java.util.List;\r\nimport java.util.Locale;\r\n\r\npublic class RemoteController extends BasePlayerController implements OnDataChange {\r\n    private static final String TAG = RemoteController.class.getSimpleName();\r\n    private static final long APP_INIT_DELAY_MS = 10_000;\r\n    private final Runnable mStartListeningInt = this::startListeningInt;\r\n    private final RemoteControlService mRemoteControlService;\r\n    private final RemoteControlData mRemoteControlData;\r\n    private Disposable mListeningAction;\r\n    private Disposable mPostStartPlayAction;\r\n    private Disposable mPostStateAction;\r\n    private Disposable mPostVolumeAction;\r\n    private boolean mConnected;\r\n    private long mNewVideoPositionMs;\r\n    private Disposable mActionDown;\r\n    private Disposable mActionUp;\r\n    private ContentObserver mVolumeObserver;\r\n    private long mVolumeSelfChangeMs;\r\n\r\n    public RemoteController(Context context) {\r\n        // Start receiving a commands as early as possible\r\n        ServiceManager service = YouTubeServiceManager.instance();\r\n        mRemoteControlService = service.getRemoteControlService();\r\n        mRemoteControlData = RemoteControlData.instance(context);\r\n        mRemoteControlData.setOnChange(this);\r\n        tryListening();\r\n    }\r\n\r\n    @Override\r\n    public void onDataChange() {\r\n        tryListening();\r\n    }\r\n\r\n    @Override\r\n    public void onNewVideo(Video item) {\r\n        if (item != null) {\r\n            Log.d(TAG, \"Open video. Is remote connected: %s\", mConnected);\r\n            item.isRemote = mConnected;\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onInit() {\r\n        tryListening();\r\n    }\r\n\r\n    @Override\r\n    public void onViewResumed() {\r\n        tryListening();\r\n    }\r\n\r\n    @Override\r\n    public void onVideoLoaded(Video item) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        if (mNewVideoPositionMs > 0) {\r\n            getPlayer().setPositionMs(mNewVideoPositionMs);\r\n            mNewVideoPositionMs = 0;\r\n        }\r\n\r\n        postStartPlaying(item, getPlayer().getPlayWhenReady() ? RemoteControlService.STATE_PLAYING : RemoteControlService.STATE_PAUSED);\r\n        if (mConnected) {\r\n            mRemoteControlData.setLastVideo(item);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onPlay() {\r\n        postPlayState(RemoteControlService.STATE_PLAYING);\r\n    }\r\n\r\n    @Override\r\n    public void onPause() {\r\n        postPlayState(RemoteControlService.STATE_PAUSED);\r\n    }\r\n\r\n    @Override\r\n    public void onPlayEnd() {\r\n        switch (getPlayerData().getPlaybackMode()) {\r\n            // The IDLE state just hangs phone's current video (spinning circle)\r\n            case PlayerConstants.PLAYBACK_MODE_CLOSE:\r\n            case PlayerConstants.PLAYBACK_MODE_PAUSE:\r\n            case PlayerConstants.PLAYBACK_MODE_ALL:\r\n                postPlayState(RemoteControlService.STATE_PAUSED);\r\n                break;\r\n            case PlayerConstants.PLAYBACK_MODE_ONE:\r\n                postStartPlaying(getVideo(), RemoteControlService.STATE_PLAYING);\r\n                break;\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onTrackSelected(FormatItem track) {\r\n        if (track.getType() == FormatItem.TYPE_SUBTITLE) {\r\n            if (getPlayer() == null) {\r\n                return;\r\n            }\r\n            Log.d(TAG, \"Subtitle track changed, %s\", track);\r\n            // Post subtitle change immediately when user select subtitle track.\r\n            postSubtitleChange(track.getFormatId(), track.getLanguage());\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onEngineReleased() {\r\n        // The IDLE state just hangs phone's current video (spinning circle)\r\n        postPlayState(RemoteControlService.STATE_PAUSED);\r\n        // Below doesn't work on Vanced\r\n        //postStartPlaying(null);\r\n    }\r\n\r\n    @Override\r\n    public void onFinish() {\r\n        // User action detected. Hide remote playlist.\r\n        mConnected = false;\r\n    }\r\n\r\n    private void postStartPlaying(@Nullable Video item, int state) {\r\n        if (isRemoteDisabled()) {\r\n            return;\r\n        }\r\n\r\n        String videoId = null;\r\n        long positionMs = -1;\r\n        long durationMs = -1;\r\n\r\n        if (item != null && getPlayer() != null) {\r\n            videoId = item.videoId;\r\n            positionMs = getPlayer().getPositionMs();\r\n            durationMs = getPlayer().getDurationMs();\r\n        }\r\n\r\n        postStartPlaying(videoId, positionMs, durationMs, state);\r\n    }\r\n\r\n    private void postStartPlaying(String videoId, long positionMs, long durationMs, int state) {\r\n        if (isRemoteDisabled()) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mPostStartPlayAction);\r\n\r\n        mPostStartPlayAction = RxHelper.execute(\r\n                mRemoteControlService.postStartPlayingObserve(videoId, positionMs, durationMs, state)\r\n        );\r\n    }\r\n\r\n    private void postState(long positionMs, long durationMs, int state) {\r\n        if (isRemoteDisabled()) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mPostStateAction);\r\n\r\n        mPostStateAction = RxHelper.execute(\r\n                mRemoteControlService.postStateChangeObserve(positionMs, durationMs, state)\r\n        );\r\n    }\r\n\r\n    private void postVolumeChange(int volume) {\r\n        if (isRemoteDisabled()) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mPostVolumeAction);\r\n\r\n        mPostVolumeAction = RxHelper.execute(\r\n                mRemoteControlService.postVolumeChangeObserve(volume)\r\n        );\r\n    }\r\n\r\n    private void postSubtitleChange(String vssId, String languageCode) {\r\n        if (isRemoteDisabled()) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mPostStateAction);\r\n        mPostStateAction = RxHelper.execute(\r\n                mRemoteControlService.postSubtitleChangeObserve(vssId, languageCode)\r\n        );\r\n    }\r\n\r\n    private void postPlayState(int state) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        postState(getPlayer().getPositionMs(), getPlayer().getDurationMs(), state);\r\n    }\r\n\r\n    private void postSeek(long positionMs) {\r\n        postState(positionMs, getPlayer().getDurationMs(), getPlayer().isPlaying() ? RemoteControlService.STATE_PLAYING : RemoteControlService.STATE_PAUSED);\r\n    }\r\n\r\n    private void postIdle() {\r\n        postState(-1, -1, RemoteControlService.STATE_IDLE);\r\n    }\r\n\r\n    private void tryListening() {\r\n        if (mRemoteControlData.isDeviceLinkEnabled()) {\r\n            startListening();\r\n        } else {\r\n            stopListening();\r\n        }\r\n    }\r\n\r\n    private void startListening() {\r\n        if (mListeningAction != null && !mListeningAction.isDisposed()) {\r\n            return;\r\n        }\r\n\r\n        Utils.postDelayed(mStartListeningInt, APP_INIT_DELAY_MS);\r\n    }\r\n\r\n    private void startListeningInt() {\r\n        if (mListeningAction != null && !mListeningAction.isDisposed()) {\r\n            return;\r\n        }\r\n\r\n        mListeningAction = mRemoteControlService.getCommandObserve()\r\n                .subscribe(\r\n                        this::processCommand,\r\n                        error -> {\r\n                            String msg = \"startListening error: \" + error.getMessage();\r\n                            Log.e(TAG, msg);\r\n                            MessageHelpers.showLongMessage(getContext(), msg);\r\n                        },\r\n                        () -> {\r\n                            // Some users seeing this.\r\n                            // This msg couldn't appear in normal situation.\r\n                            Log.d(TAG, \"Remote session has been closed\");\r\n                            //MessageHelpers.showMessage(getActivity(), R.string.remote_session_closed);\r\n                        }\r\n                );\r\n    }\r\n\r\n    private void stopListening() {\r\n        RxHelper.disposeActions(mListeningAction, mPostStartPlayAction, mPostStateAction, mPostVolumeAction);\r\n        unregisterVolumeObserver();\r\n        Utils.removeCallbacks(mStartListeningInt);\r\n    }\r\n\r\n    private void processCommand(Command command) {\r\n        switch (command.getType()) {\r\n            case Command.TYPE_IDLE:\r\n            case Command.TYPE_UNDEFINED:\r\n            case Command.TYPE_UPDATE_PLAYLIST:\r\n                break;\r\n            case Command.TYPE_STOP:\r\n            case Command.TYPE_DISCONNECTED:\r\n                mConnected = false;\r\n                break;\r\n            default:\r\n                mConnected = true;\r\n        }\r\n\r\n        Log.d(TAG, \"Is remote connected: %s, command type: %s\", mConnected, command.getType());\r\n\r\n        switch (command.getType()) {\r\n            case Command.TYPE_OPEN_VIDEO:\r\n                if (getPlayer() != null) {\r\n                    getPlayer().showOverlay(false);\r\n                }\r\n                movePlayerToForeground();\r\n                Video newVideo = Video.from(command.getVideoId());\r\n                newVideo.remotePlaylistId = command.getPlaylistId();\r\n                newVideo.playlistIndex = command.getPlaylistIndex();\r\n                newVideo.isRemote = true;\r\n                mNewVideoPositionMs = command.getCurrentTimeMs();\r\n                openNewVideo(newVideo);\r\n                break;\r\n            case Command.TYPE_SUBTITLES:\r\n                if (getPlayer() != null) {\r\n                    getPlayer().showOverlay(false);\r\n                }\r\n                movePlayerToForeground();\r\n                Video newVideo2 = Video.from(command.getVideoId());\r\n                newVideo2.remotePlaylistId = command.getPlaylistId();\r\n                newVideo2.playlistIndex = command.getPlaylistIndex();\r\n                newVideo2.isRemote = true;\r\n                mNewVideoPositionMs = command.getCurrentTimeMs();\r\n\r\n                String langCode = command.getSubLanguageCode();\r\n                if (langCode != null && !langCode.trim().isEmpty() && getPlayer() != null) {\r\n                    List<FormatItem> subs = getPlayer().getSubtitleFormats();\r\n                    if (subs != null) {\r\n                        FormatItem selected = null;\r\n                        for (FormatItem item : subs) {\r\n                            Locale languageLocale = new Locale(langCode);\r\n                            String currentLocale = languageLocale.getDisplayLanguage().toLowerCase();\r\n                            if (item.getLanguage() != null && item.getLanguage().toLowerCase().contains(currentLocale)) {\r\n                                selected = item;\r\n                                break;\r\n                            }\r\n                        }\r\n\r\n                        if (selected != null) {\r\n                            getPlayer().setFormat(selected);\r\n                        }\r\n                    }\r\n                    getMainController().onButtonClicked(R.id.lb_control_closed_captioning, PlayerUI.BUTTON_OFF);\r\n                 } else if (getPlayer() != null) {\r\n                    getMainController().onButtonClicked(R.id.lb_control_closed_captioning, PlayerUI.BUTTON_ON);\r\n                 }\r\n                 openNewVideo(newVideo2);\r\n                 break;\r\n            case Command.TYPE_UPDATE_PLAYLIST:\r\n                if (getPlayer() != null && mConnected) {\r\n                    Video video = getVideo();\r\n                    // Ensure that remote playlist already playing\r\n                    if (video != null && video.remotePlaylistId != null) {\r\n                        video.remotePlaylistId = command.getPlaylistId();\r\n                        video.playlistParams = null;\r\n                        video.isRemote = true;\r\n                        getController(SuggestionsController.class).loadSuggestions(video);\r\n                    }\r\n                }\r\n                break;\r\n            case Command.TYPE_SEEK:\r\n                if (getPlayer() != null) {\r\n                    getPlayer().showOverlay(false);\r\n                    movePlayerToForeground();\r\n                    getPlayer().setPositionMs(command.getCurrentTimeMs());\r\n                    postSeek(command.getCurrentTimeMs());\r\n                } else {\r\n                    openNewVideo(getVideo());\r\n                }\r\n                break;\r\n            case Command.TYPE_PLAY:\r\n                if (getPlayer() != null) {\r\n                    movePlayerToForeground();\r\n                    getPlayer().setPlayWhenReady(true);\r\n                    //postStartPlaying(getController().getVideo(), true);\r\n                    postPlayState(RemoteControlService.STATE_PLAYING);\r\n                } else {\r\n                    // Already connected\r\n                    openNewVideo(getVideo() != null ? getVideo() : mRemoteControlData.getLastVideo());\r\n                }\r\n                break;\r\n            case Command.TYPE_PAUSE:\r\n                if (getPlayer() != null) {\r\n                    movePlayerToForeground();\r\n                    getPlayer().setPlayWhenReady(false);\r\n                    //postStartPlaying(getController().getVideo(), false);\r\n                    postPlayState(RemoteControlService.STATE_PAUSED);\r\n                } else {\r\n                    // Already connected\r\n                    openNewVideo(getVideo() != null ? getVideo() : mRemoteControlData.getLastVideo());\r\n                }\r\n                break;\r\n            case Command.TYPE_NEXT:\r\n                if (getMainController() != null) {\r\n                    movePlayerToForeground();\r\n                    getController(VideoLoaderController.class).loadNext();\r\n                } else {\r\n                    openNewVideo(getVideo());\r\n                }\r\n                break;\r\n            case Command.TYPE_PREVIOUS:\r\n                if (getMainController() != null && getPlayer() != null) {\r\n                    movePlayerToForeground();\r\n                    // Switch immediately. Skip position reset logic.\r\n                    getController(VideoLoaderController.class).loadPrevious();\r\n                } else {\r\n                    openNewVideo(getVideo());\r\n                }\r\n                break;\r\n            case Command.TYPE_GET_STATE:\r\n                if (getPlayer() != null) {\r\n                    getViewManager().moveAppToForeground();\r\n                    postStartPlaying(getVideo(), getPlayer().isPlaying() ? RemoteControlService.STATE_PLAYING : RemoteControlService.STATE_PAUSED);\r\n                } else {\r\n                    postStartPlaying(null, RemoteControlService.STATE_PAUSED);\r\n                }\r\n                break;\r\n            case Command.TYPE_VOLUME:\r\n                int volume = command.getVolume();\r\n\r\n                if (command.getDelta() != -1) { // using phone volume sliders\r\n                    volume = Utils.getVolume(getContext(), getPlayer()) + command.getDelta();\r\n                }\r\n\r\n                Utils.setVolume(getContext(), getPlayer(), volume);\r\n                mVolumeSelfChangeMs = System.currentTimeMillis();\r\n\r\n                if (command.getDelta() != -1) { // using phone volume sliders\r\n                    postVolumeChange(Utils.getVolume(getContext(), getPlayer()));\r\n                } else {\r\n                    postVolumeChange(volume);\r\n                }\r\n                break;\r\n            case Command.TYPE_STOP:\r\n                // Close player\r\n                if (getPlayer() != null) {\r\n                    getPlayer().finish();\r\n                }\r\n                //// Finish the app\r\n                //if (getActivity() != null) {\r\n                //    ViewManager.instance(getActivity()).properlyFinishTheApp(getActivity());\r\n                //}\r\n                break;\r\n            case Command.TYPE_CONNECTED:\r\n                //movePlayerToForeground();\r\n                // NOTE: there are possible false calls when mobile client unloaded from the memory.\r\n                //if (getActivity() != null && mRemoteControlData.isFinishOnDisconnectEnabled()) {\r\n                //    // NOTE: It's not a good idea to remember connection state (mConnected) at this point.\r\n                //    Utils.moveAppToForeground(getActivity());\r\n                //    MessageHelpers.showLongMessage(getActivity(), getActivity().getString(R.string.device_connected, command.getDeviceName()));\r\n                //}\r\n                //if (mRemoteControlData.isConnectMessagesEnabled()) {\r\n                //    MessageHelpers.showLongMessage(getActivity(), getActivity().getString(R.string.device_connected, command.getDeviceName()));\r\n                //}\r\n                registerVolumeObserver();\r\n                mRemoteControlData.setConnectedBefore(true);\r\n                break;\r\n            case Command.TYPE_DISCONNECTED:\r\n                // NOTE: there are possible false calls when mobile client unloaded from the memory.\r\n                if (getContext() != null && mRemoteControlData.isFinishOnDisconnectEnabled()) {\r\n                    // NOTE: It's not a good idea to remember connection state (mConnected) at this point.\r\n                    MessageHelpers.showLongMessage(getContext(), getContext().getString(R.string.device_disconnected, command.getDeviceName()));\r\n                    Utils.properlyFinishTheApp(getContext());\r\n                }\r\n                //if (mRemoteControlData.isConnectMessagesEnabled()) {\r\n                //    MessageHelpers.showLongMessage(getContext(), getContext().getString(R.string.device_disconnected, command.getDeviceName()));\r\n                //}\r\n                unregisterVolumeObserver();\r\n                mRemoteControlData.setConnectedBefore(false);\r\n                break;\r\n            case Command.TYPE_IDLE:\r\n                // Already connected\r\n                if (isConnectedBefore()) {\r\n                    registerVolumeObserver();\r\n                }\r\n                break;\r\n            case Command.TYPE_DPAD:\r\n                int key = KeyEvent.KEYCODE_UNKNOWN;\r\n                boolean isLongAction = false;\r\n                switch (command.getKey()) {\r\n                    case Command.KEY_UP:\r\n                        key = KeyEvent.KEYCODE_DPAD_UP;\r\n                        break;\r\n                    case Command.KEY_DOWN:\r\n                        key = KeyEvent.KEYCODE_DPAD_DOWN;\r\n                        break;\r\n                    case Command.KEY_LEFT:\r\n                        key = KeyEvent.KEYCODE_DPAD_LEFT;\r\n                        isLongAction = true; // enable fast seeking\r\n                        break;\r\n                    case Command.KEY_RIGHT:\r\n                        key = KeyEvent.KEYCODE_DPAD_RIGHT;\r\n                        isLongAction = true; // enable fast seeking\r\n                        break;\r\n                    case Command.KEY_ENTER:\r\n                        key = KeyEvent.KEYCODE_DPAD_CENTER;\r\n                        break;\r\n                    case Command.KEY_BACK:\r\n                        key = KeyEvent.KEYCODE_BACK;\r\n                        break;\r\n                }\r\n                if (key != KeyEvent.KEYCODE_UNKNOWN) {\r\n                    RxHelper.disposeActions(mActionDown, mActionUp);\r\n\r\n                    final int resultKey = key;\r\n\r\n                    if (isLongAction) {\r\n                        mActionDown = RxHelper.runAsync(() ->\r\n                                Utils.sendKey(new KeyEvent(KeyEvent.ACTION_DOWN, resultKey)));\r\n                        mActionUp = RxHelper.runAsync(() ->\r\n                                Utils.sendKey(new KeyEvent(KeyEvent.ACTION_UP, resultKey)), 500);\r\n                    } else {\r\n                        mActionDown = RxHelper.runAsync(() -> Utils.sendKey(resultKey));\r\n                    }\r\n                }\r\n                break;\r\n            case Command.TYPE_VOICE:\r\n                if (command.isVoiceStarted()) {\r\n                    getSearchPresenter().startVoice();\r\n                } else {\r\n                    getSearchPresenter().forceFinish();\r\n                }\r\n                break;\r\n        }\r\n    }\r\n\r\n    private void openNewVideo(Video newVideo) {\r\n        if (Video.equals(getVideo(), newVideo) && getViewManager().isPlayerInForeground() && getPlayer() != null) { // same video already playing\r\n            if (mNewVideoPositionMs > 0) {\r\n                getPlayer().setPositionMs(mNewVideoPositionMs);\r\n                mNewVideoPositionMs = 0;\r\n            }\r\n            postStartPlaying(getVideo(), getPlayer().isPlaying() ? RemoteControlService.STATE_PLAYING : RemoteControlService.STATE_PAUSED);\r\n        } else if (newVideo != null) {\r\n            newVideo.isRemote = true;\r\n            getPlaybackPresenter().openVideo(newVideo);\r\n        }\r\n    }\r\n\r\n    private void movePlayerToForeground() {\r\n        getViewManager().movePlayerToForeground();\r\n        // Device wake fix when player isn't started yet or been closed\r\n        if (getPlayer() == null || !Utils.checkActivity(getActivity())) {\r\n            //new Handler(Looper.myLooper()).postDelayed(() -> getViewManager().movePlayerToForeground(), 5_000);\r\n            Utils.postDelayed(() -> getViewManager().movePlayerToForeground(), 5_000);\r\n        }\r\n    }\r\n\r\n    private void registerVolumeObserver() {\r\n        if (mVolumeObserver != null) {\r\n            return;\r\n        }\r\n\r\n        mVolumeObserver = new ContentObserver(Utils.sHandler) {\r\n            @Override\r\n            public void onChange(boolean selfChange) {\r\n                if (System.currentTimeMillis() - mVolumeSelfChangeMs > 1_000) {\r\n                    postVolumeChange(Utils.getVolume(getContext(), getPlayer()));\r\n                }\r\n            }\r\n        };\r\n        Utils.registerAudioObserver(getContext(), mVolumeObserver);\r\n\r\n        postVolumeChange(Utils.getVolume(getContext(), getPlayer()));\r\n    }\r\n\r\n    private void unregisterVolumeObserver() {\r\n        if (mVolumeObserver != null) {\r\n            Utils.unregisterAudioObserver(getContext(), mVolumeObserver);\r\n            mVolumeObserver = null;\r\n        }\r\n    }\r\n\r\n    private boolean isConnectedBefore() {\r\n        return mConnected || mRemoteControlData.isConnectedBefore();\r\n    }\r\n\r\n    private boolean isRemoteDisabled() {\r\n        return !mRemoteControlData.isDeviceLinkEnabled() || !isConnectedBefore() || isEmbedPlayer();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/SponsorBlockController.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers;\r\n\r\nimport androidx.annotation.NonNull;\r\nimport androidx.core.content.ContextCompat;\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.SponsorSegment;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.BasePlayerController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerUI;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.SeekBarSegment;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.SponsorBlockSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.SponsorBlockData;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\nimport io.reactivex.Observable;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\nimport java.util.concurrent.TimeUnit;\r\n\r\npublic class SponsorBlockController extends BasePlayerController {\r\n    private static final String TAG = SponsorBlockController.class.getSimpleName();\r\n    private static final long POLL_INTERVAL_MS = 1_000;\r\n    private static final int CONTENT_BLOCK_ID = 144;\r\n    private MediaItemService mMediaItemService;\r\n    private List<SponsorSegment> mOriginalSegments;\r\n    private List<SponsorSegment> mActiveSegments;\r\n    private long mLastSkipPosMs;\r\n    private boolean mSkipExclude;\r\n    private Disposable mSegmentsAction;\r\n    private Observable<List<SponsorSegment>> mCachedSegmentsAction;\r\n    private String mVideoId;\r\n\r\n    public static class SegmentAction {\r\n        public String segmentCategory;\r\n        public int actionType;\r\n\r\n        public static SegmentAction from(String spec) {\r\n            if (spec == null) {\r\n                return null;\r\n            }\r\n\r\n            String[] split = spec.split(\",\");\r\n\r\n            if (split.length != 2) {\r\n                return null;\r\n            }\r\n\r\n            String name = Helpers.parseStr(split[0]);\r\n            int action = Helpers.parseInt(split[1]);\r\n\r\n            return from(name, action);\r\n        }\r\n\r\n        public static SegmentAction from(String name, int action) {\r\n            SegmentAction blockedSegment = new SegmentAction();\r\n            blockedSegment.segmentCategory = name;\r\n            blockedSegment.actionType = action;\r\n\r\n            return blockedSegment;\r\n        }\r\n\r\n        @NonNull\r\n        @Override\r\n        public String toString() {\r\n            return String.format(\"%s,%s\", segmentCategory, actionType);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onInit() {\r\n        ServiceManager service = YouTubeServiceManager.instance();\r\n        mMediaItemService = service.getMediaItemService();\r\n    }\r\n\r\n    @Override\r\n    public void onNewVideo(Video item) {\r\n        mSkipExclude = false;\r\n        if (getPlayer() != null) {\r\n            getPlayer().setSeekBarSegments(null); // reset colors\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onVideoLoaded(Video item) {\r\n        disposeActions();\r\n\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        boolean enabled = getSponsorBlockData().isSponsorBlockEnabled() && checkVideo(item) && !isChannelExcluded(item.channelId);\r\n        getPlayer().setButtonState(R.id.action_content_block, enabled ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n\r\n        if (enabled) {\r\n            updateSponsorSegmentsAndWatch(item);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onMetadata(MediaItemMetadata metadata) {\r\n        // Disable sponsor for the live streams.\r\n        // Fix when using remote control.\r\n        if (!getSponsorBlockData().isSponsorBlockEnabled() || !checkVideo(getPlayer().getVideo())) {\r\n            disposeActions();\r\n        } else if (isChannelExcluded(metadata.getChannelId())) { // got channel id. check the exclusions\r\n            getPlayer().setButtonState(R.id.action_content_block, PlayerUI.BUTTON_OFF);\r\n            disposeActions();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onEngineReleased() {\r\n        disposeActions();\r\n    }\r\n\r\n    @Override\r\n    public void onButtonClicked(int buttonId, int buttonState) {\r\n        if (buttonId == R.id.action_content_block) {\r\n            List<SponsorSegment> foundSegments = findMatchedSegments(getPlayer().getPositionMs(), mOriginalSegments, true);\r\n\r\n            if (foundSegments != null) {\r\n                SponsorSegment lastSegment = foundSegments.get(foundSegments.size() - 1);\r\n                setPositionMs(lastSegment.getEndMs());\r\n                return;\r\n            }\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onButtonLongClicked(int buttonId, int buttonState) {\r\n        if (buttonId == R.id.action_content_block) {\r\n            SponsorBlockSettingsPresenter.instance(getContext()).show(() -> {\r\n                if (getPlayer() != null) {\r\n                    onVideoLoaded(getPlayer().getVideo());\r\n                }\r\n            });\r\n        }\r\n    }\r\n\r\n    private boolean checkVideo(Video video) {\r\n        //return video != null && !video.isLive && !video.isUpcoming;\r\n        return video != null;\r\n    }\r\n\r\n    private void updateSponsorSegmentsAndWatch(Video item) {\r\n        if (item == null || item.videoId == null || item.isLive || getSponsorBlockData().getEnabledCategories().isEmpty()) {\r\n            mActiveSegments = mOriginalSegments = null;\r\n            mCachedSegmentsAction = null;\r\n            return;\r\n        }\r\n\r\n        if (!Helpers.equals(mVideoId, item.videoId) || mCachedSegmentsAction == null) {\r\n            // NOTE: SponsorBlock (when happened java.net.SocketTimeoutException) could block whole application with Schedulers.io()\r\n            // Because Schedulers.io() reuses blocked threads in RxJava 2: https://github.com/ReactiveX/RxJava/issues/6542\r\n            mCachedSegmentsAction = mMediaItemService.getSponsorSegmentsObserve(item.videoId, getSponsorBlockData().getEnabledCategories())\r\n                    .cache();\r\n            mVideoId = item.videoId;\r\n        }\r\n\r\n        mSegmentsAction = mCachedSegmentsAction\r\n                .flatMap(this::startSponsorWatcher)\r\n                .subscribe(\r\n                        this::skipSegment,\r\n                        error -> Log.d(TAG, \"It's ok. Nothing to block in this video. Error msg: %s\", error.getMessage())\r\n                );\r\n    }\r\n\r\n    private Observable<Long> startSponsorWatcher(List<SponsorSegment> segments) {\r\n        if (segments == null || segments.isEmpty()) {\r\n            mActiveSegments = mOriginalSegments = null;\r\n            return Observable.empty();\r\n        }\r\n\r\n        mOriginalSegments = segments;\r\n\r\n        mActiveSegments = new ArrayList<>(segments);\r\n\r\n        if (getSponsorBlockData().isColorMarkersEnabled()) {\r\n            getPlayer().setSeekBarSegments(toSeekBarSegments(segments));\r\n        }\r\n        if (getSponsorBlockData().isActionsEnabled()) {\r\n            // Warn. Try to not access player object here.\r\n            // Or you'll get \"Player is accessed on the wrong thread\" error.\r\n            return RxHelper.interval(POLL_INTERVAL_MS, TimeUnit.MILLISECONDS);\r\n        } else {\r\n            return Observable.empty();\r\n        }\r\n    }\r\n\r\n    private void disposeActions() {\r\n        RxHelper.disposeActions(mSegmentsAction);\r\n\r\n        // Note, removes all segments at once\r\n        //getPlayer().setSeekBarSegments(null); // reset colors\r\n\r\n        // Reset previously found segment (fix no dialog popup)\r\n        mLastSkipPosMs = 0;\r\n    }\r\n\r\n    private void skipSegment(long interval) {\r\n        if (mActiveSegments == null || mActiveSegments.isEmpty() || getVideo() == null || !Helpers.equals(mVideoId, getVideo().videoId)) {\r\n            disposeActions();\r\n            return;\r\n        }\r\n\r\n        // Fix looping messages at the end of the video (playback mode: pause at the end of the video)\r\n        if (!getPlayer().isPlaying()) {\r\n            return;\r\n        }\r\n\r\n        long positionMs = getPlayer().getPositionMs();\r\n\r\n        List<SponsorSegment> foundSegments = findMatchedSegments(positionMs, mActiveSegments, false);\r\n\r\n        applyActions(foundSegments);\r\n\r\n        // Skip each segment only once\r\n        if (foundSegments != null && getSponsorBlockData().isDontSkipSegmentAgainEnabled()) {\r\n            mActiveSegments.removeAll(foundSegments);\r\n        }\r\n    }\r\n\r\n    private boolean isPositionInsideSegment(long positionMs, SponsorSegment segment, boolean fullMatch) {\r\n        // NOTE: in case of using Player.setSeekParameters (inaccurate seeking) increase sponsor segment window\r\n        // int seekShift = 1_000;\r\n        // return positionMs >= (segment.getStartMs() - seekShift) && positionMs <= (segment.getEndMs() + seekShift);\r\n\r\n        if (fullMatch) {\r\n            return positionMs >= segment.getStartMs() && positionMs <= segment.getEndMs();\r\n        } else {\r\n            long windowSizeMs = (long) (2_000 * getPlayer().getSpeed());\r\n            return positionMs >= segment.getStartMs() && positionMs <= Math.min(segment.getStartMs() + windowSizeMs, segment.getEndMs());\r\n        }\r\n    }\r\n\r\n    private void simpleSkip(long skipPosMs) {\r\n        if (mLastSkipPosMs == skipPosMs) {\r\n            return;\r\n        }\r\n\r\n        setPositionMs(skipPosMs);\r\n        closeTransparentDialog();\r\n    }\r\n\r\n    private void messageSkip(long skipPosMs, String category) {\r\n        if (mLastSkipPosMs == skipPosMs) {\r\n            return;\r\n        }\r\n\r\n        MessageHelpers.showMessage(getContext(),\r\n                String.format(\"%s: %s\", getContext().getString(R.string.content_block_provider), getContext().getString(R.string.msg_skipping_segment, category)));\r\n        setPositionMs(skipPosMs);\r\n        closeTransparentDialog();\r\n    }\r\n\r\n    private void confirmSkip(long skipPosMs, String category) {\r\n        if (mLastSkipPosMs == skipPosMs) {\r\n            return;\r\n        }\r\n\r\n        AppDialogPresenter dialogPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        if (dialogPresenter.isDialogShown() || getPlayer().isSuggestionsShown()) {\r\n            // Another dialog is opened. Don't distract a user.\r\n            return;\r\n        }\r\n\r\n        getPlayer().showControls(false);\r\n\r\n        OptionItem acceptOption = UiOptionItem.from(\r\n                getContext().getString(R.string.confirm_segment_skip, category),\r\n                option -> {\r\n                    // return to previous dialog or close if no other dialogs in stack\r\n                    dialogPresenter.goBack();\r\n                    setPositionMs(skipPosMs);\r\n                }\r\n        );\r\n\r\n        dialogPresenter.appendSingleButton(acceptOption);\r\n        dialogPresenter.setCloseTimeoutMs((long) ((skipPosMs - getPlayer().getPositionMs()) * getPlayer().getSpeed()));\r\n\r\n        dialogPresenter.enableTransparent(true);\r\n        dialogPresenter.enableOverlay(true);\r\n        dialogPresenter.enableExpandable(false);\r\n        dialogPresenter.setId(CONTENT_BLOCK_ID);\r\n        dialogPresenter.showDialog(getContext().getString(R.string.content_block_provider));\r\n    }\r\n\r\n    private List<SeekBarSegment> toSeekBarSegments(List<SponsorSegment> segments) {\r\n        if (segments == null) {\r\n            return null;\r\n        }\r\n\r\n        List<SeekBarSegment> result = new ArrayList<>();\r\n\r\n        for (SponsorSegment sponsorSegment : segments) {\r\n            if (!getSponsorBlockData().isColorMarkerEnabled(sponsorSegment.getCategory())) {\r\n                continue;\r\n            }\r\n\r\n            SeekBarSegment seekBarSegment = new SeekBarSegment();\r\n            float startRatio = (float) sponsorSegment.getStartMs() / getPlayer().getDurationMs(); // Range: [0, 1]\r\n            float endRatio = (float) sponsorSegment.getEndMs() / getPlayer().getDurationMs(); // Range: [0, 1]\r\n            seekBarSegment.startProgress = startRatio;\r\n            seekBarSegment.endProgress = endRatio;\r\n            seekBarSegment.color = ContextCompat.getColor(getContext(), getSponsorBlockData().getColorRes(sponsorSegment.getCategory()));\r\n            result.add(seekBarSegment);\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    /**\r\n     * Sponsor block fix. Position may exceed real media length.\r\n     */\r\n    private void setPositionMs(long positionMs) {\r\n        long durationMs = getPlayer().getDurationMs();\r\n\r\n        // Sponsor block fix. Position may exceed real media length.\r\n        getPlayer().setPositionMs(Math.min(positionMs, durationMs));\r\n    }\r\n\r\n    /**\r\n     * @param fullMatch Match only the beginning or the full segment length\r\n     */\r\n    private List<SponsorSegment> findMatchedSegments(long positionMs, List<SponsorSegment> segments, boolean fullMatch) {\r\n        if (segments == null) {\r\n            return null;\r\n        }\r\n\r\n        List<SponsorSegment> foundSegment = null;\r\n\r\n        for (SponsorSegment segment : segments) {\r\n            int action = getSponsorBlockData().getAction(segment.getCategory());\r\n            boolean isSkipAction = action == SponsorBlockData.ACTION_SKIP_ONLY ||\r\n                    action == SponsorBlockData.ACTION_SKIP_WITH_TOAST;\r\n            if (foundSegment == null) {\r\n                if (isPositionInsideSegment(positionMs, segment, fullMatch)) {\r\n                    foundSegment = new ArrayList<>();\r\n                    foundSegment.add(segment);\r\n\r\n                    // Action grouping aren't supported for dialogs\r\n                    if (!isSkipAction) {\r\n                        break;\r\n                    }\r\n                }\r\n            } else {\r\n                SponsorSegment lastSegment = foundSegment.get(foundSegment.size() - 1);\r\n                if (isSkipAction && isPositionInsideSegment(lastSegment.getEndMs() + 3_000, segment, fullMatch)) {\r\n                    foundSegment.add(segment);\r\n                }\r\n            }\r\n        }\r\n\r\n        return foundSegment;\r\n    }\r\n\r\n    private void applyActions(List<SponsorSegment> foundSegments) {\r\n        if (getPlayer() == null || foundSegments == null) {\r\n            mLastSkipPosMs = 0;\r\n            return;\r\n        }\r\n\r\n        SponsorSegment lastSegment = foundSegments.get(foundSegments.size() - 1);\r\n\r\n        Integer resId = getSponsorBlockData().getLocalizedRes(lastSegment.getCategory());\r\n        String skipMessage = resId != null ? getContext().getString(resId) : lastSegment.getCategory();\r\n\r\n        int type = getSponsorBlockData().getAction(lastSegment.getCategory());\r\n\r\n        long skipPosMs = lastSegment.getEndMs();\r\n        // Fix infinite skip loop by ignoring short segments. TextureView has a seek bug.\r\n        long skipDurationMs = Math.min(skipPosMs, getPlayer().getDurationMs()) - getPlayer().getPositionMs();\r\n        boolean isDurationIgnored = (skipDurationMs < 10_000 && getPlayerTweaksData().isTextureViewEnabled())\r\n                || (skipDurationMs < getSponsorBlockData().getIgnoredDurationMs());\r\n\r\n        if (!isDurationIgnored) {\r\n            if (type == SponsorBlockData.ACTION_SKIP_ONLY || getPlayer().isInPIPMode() || Utils.isScreenOff(getContext()) || isEmbedPlayer()) {\r\n                simpleSkip(skipPosMs);\r\n            } else if (type == SponsorBlockData.ACTION_SKIP_WITH_TOAST) {\r\n                messageSkip(skipPosMs, skipMessage);\r\n            } else if (type == SponsorBlockData.ACTION_SHOW_DIALOG) {\r\n                confirmSkip(skipPosMs, skipMessage);\r\n            }\r\n        }\r\n\r\n        mLastSkipPosMs = skipPosMs;\r\n    }\r\n\r\n    private void closeTransparentDialog() {\r\n        AppDialogPresenter dialogPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        if (dialogPresenter.isDialogShown() && dialogPresenter.getId() == CONTENT_BLOCK_ID) {\r\n            dialogPresenter.closeDialog();\r\n        }\r\n    }\r\n\r\n    private boolean isChannelExcluded(String channelId) {\r\n        return !mSkipExclude && getSponsorBlockData().isChannelExcluded(channelId);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/SuggestionsController.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers;\r\n\r\nimport android.text.TextUtils;\r\nimport android.util.Pair;\r\n\r\nimport androidx.core.content.ContextCompat;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.ContentService;\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.ChapterItem;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.DislikeData;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Playlist;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.BasePlayerController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerConstants;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.SeekBarSegment;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.BrowseProcessorManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\nimport io.reactivex.Observable;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class SuggestionsController extends BasePlayerController {\r\n    private static final String TAG = SuggestionsController.class.getSimpleName();\r\n    private final List<Disposable> mActions = new ArrayList<>();\r\n    private MediaItemService mMediaItemService;\r\n    private ContentService mContentService;\r\n    private BrowseProcessorManager mBrowseProcessor;\r\n    private Video mNextSectionVideo;\r\n    private int mFocusCount;\r\n    private int mNextRetryCount;\r\n    private List<ChapterItem> mChapters;\r\n    private final Runnable mChapterHandler = this::startChapterNotificationServiceIfNeededInt;\r\n    private static final int MAX_PLAYLIST_CONTINUATIONS = 20;\r\n    private static final int CHAPTER_NOTIFICATION_Id = 565;\r\n\r\n    private interface OnVideoGroup {\r\n        void onVideoGroup(VideoGroup group);\r\n    }\r\n\r\n    private interface OnMetadata {\r\n        void onMetadata(MediaItemMetadata metadata);\r\n    }\r\n\r\n    @Override\r\n    public void onInit() {\r\n        mBrowseProcessor = new BrowseProcessorManager(getContext(), PlaybackPresenter.instance(getContext())::syncItem);\r\n        mMediaItemService = YouTubeServiceManager.instance().getMediaItemService();\r\n        mContentService = YouTubeServiceManager.instance().getContentService();\r\n    }\r\n\r\n    @Override\r\n    public void onNewVideo(Video video) {\r\n        // Remote control fix. Slow network fix. Suggestions may still be loading.\r\n        // This could lead to changing current video info (title, id etc) to wrong one.\r\n        disposeActions();\r\n        //mCurrentGroup = video.getGroup(); // disable garbage collected\r\n        //appendNextSectionVideoIfNeeded(video); // ConcurrentModificationException error\r\n    }\r\n\r\n    /**\r\n     * Improve video load time by running a fetch after load event\r\n     */\r\n    @Override\r\n    public void onVideoLoaded(Video item) {\r\n        loadSuggestions(item);\r\n    }\r\n\r\n    // Could make negative impact on the video load time.\r\n    //@Override\r\n    //public void onSourceChanged(Video item) {\r\n    //    loadSuggestions(item);\r\n    //}\r\n\r\n    @Override\r\n    public void onEngineReleased() {\r\n        disposeActions();\r\n    }\r\n\r\n    @Override\r\n    public void onFinish() {\r\n        disposeActions();\r\n    }\r\n\r\n    @Override\r\n    public void onScrollEnd(Video item) {\r\n        if (item == null) {\r\n            Log.e(TAG, \"Can't scroll. Video is null.\");\r\n            return;\r\n        }\r\n\r\n        VideoGroup group = item.getGroup();\r\n\r\n        continueGroup(group);\r\n    }\r\n\r\n    @Override\r\n    public void onSuggestionItemClicked(Video item) {\r\n        markAsQueueIfNeeded(item);\r\n    }\r\n\r\n    @Override\r\n    public void onControlsShown(boolean shown) {\r\n        if (shown) {\r\n            focusCurrentChapter();\r\n        } else {\r\n            startChapterNotificationServiceIfNeeded();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onSeekEnd() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        if (getPlayer().isControlsShown()) {\r\n            focusCurrentChapter();\r\n        } else {\r\n            startChapterNotificationServiceIfNeeded();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onSeekPositionChanged(long positionMs) {\r\n        if (getPlayer().isControlsShown()) {\r\n            updateSeekPreviewTitle(positionMs);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onTickle() {\r\n        updateLiveDescription();\r\n    }\r\n\r\n    private void updateLiveDescription() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        Video video = getPlayer().getVideo();\r\n\r\n        if (video == null || !video.isLive || RxHelper.isAnyActionRunning(mActions)) {\r\n            return;\r\n        }\r\n\r\n        loadMetadata(video, metadata -> syncCurrentVideo(metadata, video));\r\n    }\r\n\r\n    private void continueGroup(VideoGroup group) {\r\n        continueGroup(group, null, true);\r\n    }\r\n\r\n    private void continueGroup(VideoGroup group, boolean showLoading) {\r\n        continueGroup(group, null, showLoading);\r\n    }\r\n\r\n    private void continueGroup(VideoGroup group, OnVideoGroup callback, boolean showLoading) {\r\n        if (getPlayer() == null || group == null) {\r\n            Log.e(TAG, \"Can't continue group. The group is null.\");\r\n            return;\r\n        }\r\n\r\n        Log.d(TAG, \"continueGroup: start continue group: \" + group.getTitle());\r\n\r\n        if (showLoading) {\r\n            getPlayer().showProgressBar(true);\r\n        }\r\n\r\n        MediaGroup mediaGroup = group.getMediaGroup();\r\n\r\n        Disposable continueAction = mContentService.continueGroupObserve(mediaGroup)\r\n                .subscribe(\r\n                        continueMediaGroup -> {\r\n                            getPlayer().showProgressBar(false);\r\n\r\n                            VideoGroup videoGroup = VideoGroup.from(group, continueMediaGroup);\r\n                            getPlayer().updateSuggestions(videoGroup);\r\n                            mBrowseProcessor.process(videoGroup);\r\n\r\n                            mergeUserAndRemoteQueue(videoGroup);\r\n\r\n                            if (callback != null) {\r\n                                callback.onVideoGroup(videoGroup);\r\n                            } else {\r\n                                continueGroupIfNeeded(videoGroup);\r\n                            }\r\n                        },\r\n                        error -> {\r\n                            Log.e(TAG, \"continueGroup error: %s\", error.getMessage());\r\n                            if (getPlayer() != null) {\r\n                                getPlayer().showProgressBar(false);\r\n                            }\r\n                        },\r\n                        () -> {\r\n                            if (getPlayer() != null) {\r\n                                getPlayer().showProgressBar(false);\r\n                            }\r\n                        }\r\n                );\r\n\r\n        mActions.add(continueAction);\r\n    }\r\n\r\n    private void syncCurrentVideo(MediaItemMetadata mediaItemMetadata, Video video) {\r\n        //if (getPlayer().containsMedia()) {\r\n        //    video.isUpcoming = false; // live stream started\r\n        //}\r\n\r\n        // NOTE: Skip upcoming or unplayable (no media) because default title more informative (e.g. has scheduled time).\r\n        // NOTE: Upcoming videos metadata wrongly reported as live\r\n        if (!getPlayer().containsMedia()) {\r\n            return;\r\n        }\r\n\r\n        video.sync(mediaItemMetadata);\r\n        getPlayer().setVideo(video);\r\n\r\n        getPlayer().setNextTitle(getNext());\r\n\r\n        appendDislikes(video);\r\n    }\r\n\r\n    public void loadSuggestions(Video video) {\r\n        if (isEmbedPlayer()) {\r\n            return;\r\n        }\r\n\r\n        clearSuggestionsIfNeeded(video);\r\n        loadMetadata(video, metadata -> updateSuggestions(metadata, video));\r\n    }\r\n\r\n    private void loadMetadata(Video video, OnMetadata callback) {\r\n        disposeActions();\r\n\r\n        if (video == null) {\r\n            Log.e(TAG, \"loadSuggestions: video is null\");\r\n            return;\r\n        }\r\n\r\n        Observable<MediaItemMetadata> observable;\r\n\r\n        // NOTE: Load suggestions from mediaItem isn't robust. Because playlistId may be initialized from RemoteControlManager.\r\n        // Video might be loaded from Channels section (has playlistParams)\r\n        observable = mMediaItemService.getMetadataObserve(video.videoId, video.getPlaylistId(), video.playlistIndex, video.playlistParams);\r\n\r\n        Disposable metadataAction = observable\r\n                .subscribe(\r\n                        callback::onMetadata,\r\n                        error -> {\r\n                            // Usual errors here is something with title parsing\r\n                            String message = error.getMessage();\r\n                            Log.e(TAG, \"loadSuggestions error: %s\", message);\r\n                            if (!Helpers.containsAny(message, \"fromNullable result is null\")) {\r\n                                MessageHelpers.showLongMessage(getContext(), \"loadSuggestions error: %s\", message);\r\n                            }\r\n                            error.printStackTrace();\r\n                        }\r\n                );\r\n\r\n        mActions.add(metadataAction);\r\n    }\r\n\r\n    public Video getNext() {\r\n        Video result = null;\r\n        Video next = Playlist.instance().getNext();\r\n        Video current = getPlayer().getVideo();\r\n\r\n        if (next != null) {\r\n            next.fromQueue = true;\r\n            result = next;\r\n        } else if (mNextSectionVideo != null) {\r\n            result = mNextSectionVideo;\r\n        } else if (current != null && current.nextMediaItem != null) {\r\n            result = Video.from(current.nextMediaItem);\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    public Video getPrevious() {\r\n        Video result = getPreviousFromGroup(getPlayer().getVideo());\r\n\r\n        if (result == null) {\r\n            Video previous = Playlist.instance().getPrevious();\r\n\r\n            if (previous != null) {\r\n                previous.fromQueue = true;\r\n                result = previous;\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    private Video getPreviousFromGroup(Video current) {\r\n        Video result = null;\r\n\r\n        if (current != null) {\r\n            VideoGroup group = current.getGroup();\r\n\r\n            if (group != null && !group.isEmpty()) {\r\n                Video previous = null;\r\n\r\n                for (Video item : group.getVideos()) {\r\n                    if (item.equals(current)) {\r\n                        result = previous;\r\n                        break;\r\n                    }\r\n\r\n                    if (item.hasVideo() && !item.isUpcoming) {\r\n                        previous = item;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    private void clearSuggestionsIfNeeded(Video video) {\r\n        if (video == null || getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        // Frees a lot of memory\r\n        if (video.isRemote || !getPlayer().isSuggestionsShown()) {\r\n            getPlayer().clearSuggestions();\r\n        }\r\n    }\r\n\r\n    private void updateSuggestions(MediaItemMetadata mediaItemMetadata, Video video) {\r\n        syncCurrentVideo(mediaItemMetadata, video);\r\n\r\n        appendSuggestions(video, mediaItemMetadata);\r\n\r\n        // After video suggestions\r\n        callListener(mediaItemMetadata);\r\n    }\r\n\r\n    private void appendSuggestions(Video video, MediaItemMetadata mediaItemMetadata) {\r\n        if (video == null || getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        if (!video.isRemote && getPlayer().isSuggestionsShown()) {\r\n            Log.d(TAG, \"Suggestions is opened. Seems that user want to stay here.\");\r\n            return;\r\n        }\r\n\r\n        getPlayer().clearSuggestions(); // clear previous videos\r\n\r\n        appendChaptersIfNeeded(mediaItemMetadata);\r\n\r\n        mergePlaybackAndRemoteQueueIfNeeded(video, mediaItemMetadata);\r\n\r\n        appendSectionPlaylistIfNeeded(video);\r\n\r\n        List<MediaGroup> suggestions = mediaItemMetadata.getSuggestions();\r\n\r\n        if (suggestions == null) {\r\n            String msg = \"loadSuggestions: Can't obtain suggestions for video: \" + video.getTitle();\r\n            Log.e(TAG, msg);\r\n            return;\r\n        }\r\n\r\n        int groupIndex = -1;\r\n        int suggestRows = -1;\r\n\r\n        if (GeneralData.instance(getContext()).isChildModeEnabled() || getPlayerTweaksData().isSuggestionsDisabled()) {\r\n            suggestRows = video.hasPlaylist() ? 1 : 0;\r\n        }\r\n\r\n        for (MediaGroup group : suggestions) {\r\n            groupIndex++;\r\n\r\n            if (groupIndex == suggestRows) {\r\n                break;\r\n            }\r\n\r\n            // Remove duplicated playlist\r\n            if (groupIndex == 0 && video.isSectionPlaylistEnabled(getContext()) && video.belongsToSamePlaylistGroup()) {\r\n                continue;\r\n            }\r\n\r\n            if (group != null && !group.isEmpty()) {\r\n                VideoGroup videoGroup = VideoGroup.from(group);\r\n\r\n                if (TextUtils.isEmpty(videoGroup.getTitle())) {\r\n                    videoGroup.setTitle(getContext().getString(R.string.suggestions));\r\n                    if (getPlayerTweaksData().isSuggestionsHorizontallyScrolled()) {\r\n                        videoGroup.setId(videoGroup.getTitle().hashCode()); // merge by the id\r\n                    }\r\n                }\r\n\r\n                getPlayer().updateSuggestions(videoGroup);\r\n                mBrowseProcessor.process(videoGroup);\r\n\r\n                if (groupIndex == 0) {\r\n                    focusAndContinueIfNeeded(videoGroup);\r\n                } else {\r\n                    continueGroupIfNeeded(videoGroup);\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Merge remote queue with player's queue (when phone cast just started or user clicked on playlist item)\r\n     */\r\n    private void mergePlaybackAndRemoteQueueIfNeeded(Video video, MediaItemMetadata metadata) {\r\n        // Ensure that the user pressed video thumb on the phone\r\n        if (video.isRemote && video.remotePlaylistId != null) {\r\n            // Create user queue from remote queue\r\n\r\n            List<MediaGroup> suggestions = metadata.getSuggestions();\r\n\r\n            if (suggestions != null && !suggestions.isEmpty()) {\r\n                MediaGroup remoteRow = suggestions.get(0);\r\n\r\n                VideoGroup remoteGroup = VideoGroup.from(remoteRow);\r\n\r\n                suggestions.remove(remoteRow);\r\n\r\n                appendRemoteQueueIfNeeded(video, remoteGroup);\r\n            }\r\n        } else {\r\n            appendPlaybackQueueIfNeeded();\r\n        }\r\n    }\r\n\r\n    private void mergeUserAndRemoteQueue(VideoGroup videoGroup) {\r\n        if (getPlayer() == null)\r\n            return;\r\n\r\n        Video video = getPlayer().getVideo();\r\n        if (videoGroup.isQueue) {\r\n            Playlist.instance().addAll(videoGroup.getVideos());\r\n            Playlist.instance().setCurrent(video);\r\n        }\r\n    }\r\n\r\n    private void appendPlaybackQueueIfNeeded() {\r\n        if (getPlayer() == null)\r\n            return;\r\n\r\n        Playlist playlist = Playlist.instance();\r\n\r\n        if (playlist.hasNext()) {\r\n            List<Video> queue = playlist.getAllAfterCurrent();\r\n\r\n            VideoGroup videoGroup = VideoGroup.from(queue);\r\n            videoGroup.setTitle(getContext().getString(R.string.action_playback_queue));\r\n            videoGroup.setId(videoGroup.getTitle().hashCode());\r\n            videoGroup.setType(MediaGroup.TYPE_PLAYBACK_QUEUE);\r\n\r\n            getPlayer().updateSuggestions(videoGroup);\r\n        }\r\n    }\r\n\r\n    private void appendRemoteQueueIfNeeded(Video video, VideoGroup remoteGroup) {\r\n        if (getPlayer() == null)\r\n            return;\r\n\r\n        remoteGroup.removeAllBefore(video);\r\n        remoteGroup.stripPlaylistInfo(); // prefer user queue even when a phone disconnected\r\n\r\n        if (remoteGroup.contains(video)) {\r\n            Playlist playlist = Playlist.instance();\r\n            playlist.removeAllAfterCurrent();\r\n            playlist.addAll(remoteGroup.getVideos());\r\n            playlist.setCurrent(video);\r\n        }\r\n\r\n        remoteGroup.setTitle(getContext().getString(R.string.action_playback_queue));\r\n        remoteGroup.setId(remoteGroup.getTitle().hashCode());\r\n        remoteGroup.setType(MediaGroup.TYPE_PLAYBACK_QUEUE);\r\n        remoteGroup.isQueue = true;\r\n\r\n        remoteGroup.setAction(VideoGroup.ACTION_REPLACE);\r\n        getPlayer().updateSuggestions(remoteGroup);\r\n\r\n        if (!remoteGroup.contains(video) && remoteGroup.getSize() < 100) {\r\n            continueGroup(remoteGroup, group -> appendRemoteQueueIfNeeded(video, group), false);\r\n        }\r\n    }\r\n\r\n    private void addChapterMarkersIfNeeded() {\r\n        if (getPlayer() == null || mChapters == null) {\r\n            return;\r\n        }\r\n\r\n        getPlayer().setSeekBarSegments(toSeekBarSegments(mChapters));\r\n    }\r\n\r\n    private void appendChapterSuggestionsIfNeeded() {\r\n        if (getPlayer() == null || mChapters == null) {\r\n            return;\r\n        }\r\n\r\n        VideoGroup videoGroup = VideoGroup.fromChapters(mChapters, getContext().getString(R.string.chapters));\r\n\r\n        getPlayer().updateSuggestions(videoGroup);\r\n    }\r\n\r\n    private void startChapterNotificationServiceIfNeeded() {\r\n        if (getPlayerTweaksData().isChapterNotificationEnabled()) {\r\n            Utils.postDelayed(mChapterHandler, 1_000); // small delay to give a chance to complete dialog transitions\r\n        }\r\n    }\r\n\r\n    private void startChapterNotificationServiceIfNeededInt() {\r\n        Utils.removeCallbacks(mChapterHandler);\r\n\r\n        Pair<ChapterItem, Integer> currentChapter = getCurrentChapter();\r\n        showChapterDialog(currentChapter != null ? currentChapter.first : null);\r\n\r\n        if (mChapters == null) {\r\n            return;\r\n        }\r\n\r\n        long positionMs = getPlayer().getPositionMs();\r\n\r\n        ChapterItem chapter = getNextChapter();\r\n\r\n        if (chapter != null) {\r\n            Utils.postDelayed(mChapterHandler, (long) ((chapter.getStartTimeMs() - positionMs) * getPlayer().getSpeed()));\r\n        }\r\n    }\r\n\r\n    private void appendChaptersIfNeeded(MediaItemMetadata mediaItemMetadata) {\r\n        mChapters = mediaItemMetadata.getChapters();\r\n        \r\n        addChapterMarkersIfNeeded();\r\n        appendChapterSuggestionsIfNeeded();\r\n        startChapterNotificationServiceIfNeeded();\r\n        focusCurrentChapter();\r\n    }\r\n\r\n    private void appendSectionPlaylistIfNeeded(Video video) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        if (!video.isSectionPlaylistEnabled(getContext())) {\r\n            // Important fix. Gives priority to playlist or suggestion.\r\n            mNextSectionVideo = null;\r\n            return;\r\n        }\r\n\r\n        getPlayer().updateSuggestions(video.getGroup());\r\n        focusAndContinueIfNeeded(video.getGroup(), () -> findNextSectionVideoIfNeeded(video));\r\n    }\r\n\r\n    private void markAsQueueIfNeeded(Video item) {\r\n        List<Video> afterCurrent = Playlist.instance().getAllAfterCurrent();\r\n\r\n        if (afterCurrent != null && afterCurrent.contains(item)) {\r\n            item.fromQueue = true;\r\n        }\r\n    }\r\n\r\n    private void focusCurrentChapter() {\r\n        if (getPlayer() == null || !getPlayer().isControlsShown()) {\r\n            return;\r\n        }\r\n\r\n        VideoGroup group = getPlayer().getSuggestionsByIndex(0);\r\n\r\n        if (group == null || group.isEmpty() || !group.getVideos().get(0).isChapter) {\r\n            return;\r\n        }\r\n\r\n        Pair<ChapterItem, Integer> currentChapter = getCurrentChapter();\r\n\r\n        if (currentChapter != null) {\r\n            getPlayer().focusSuggestedItem(currentChapter.second);\r\n            getPlayer().setSeekPreviewTitle(currentChapter.first.getTitle());\r\n        }\r\n    }\r\n\r\n    private void updateSeekPreviewTitle(long positionMs) {\r\n        if (getPlayer() == null || !getPlayer().isControlsShown()) {\r\n            return;\r\n        }\r\n\r\n        Pair<ChapterItem, Integer> currentChapter = getCurrentChapter(positionMs);\r\n\r\n        if (currentChapter != null) {\r\n            getPlayer().setSeekPreviewTitle(currentChapter.first.getTitle());\r\n        }\r\n    }\r\n\r\n    private List<SeekBarSegment> toSeekBarSegments(List<ChapterItem> chapters) {\r\n        if (chapters == null) {\r\n            return null;\r\n        }\r\n\r\n        List<SeekBarSegment> result = new ArrayList<>();\r\n        long markLengthMs = getPlayer().getDurationMs() / 10000;\r\n\r\n        for (ChapterItem chapter : chapters) {\r\n            if (chapter.getStartTimeMs() == 0) {\r\n                continue;\r\n            }\r\n\r\n            SeekBarSegment seekBarSegment = new SeekBarSegment();\r\n            float startRatio = (float) chapter.getStartTimeMs() / getPlayer().getDurationMs(); // Range: [0, 1]\r\n            float endRatio = (float) (chapter.getStartTimeMs() + markLengthMs) / getPlayer().getDurationMs(); // Range: [0, 1]\r\n            seekBarSegment.startProgress = startRatio;\r\n            seekBarSegment.endProgress = endRatio;\r\n            seekBarSegment.color = ContextCompat.getColor(getContext(), R.color.black);\r\n            result.add(seekBarSegment);\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    /**\r\n     * Most tiny ui has 8 cards in a row or 24 in grid.\r\n     */\r\n    private void continueGroupIfNeeded(VideoGroup group) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        if (MediaServiceManager.instance().shouldContinueRowGroup(getContext(), group)) {\r\n            continueGroup(group, getPlayer().isSuggestionsShown());\r\n        }\r\n    }\r\n\r\n    private void focusAndContinueIfNeeded(VideoGroup group) {\r\n       focusAndContinueIfNeeded(group, () -> {});\r\n    }\r\n\r\n    private void focusAndContinueIfNeeded(VideoGroup group, Runnable onDone) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        Video video = getPlayer().getVideo();\r\n\r\n        if (group == null || group.isEmpty() || video == null || !video.hasVideo()) {\r\n            return;\r\n        }\r\n\r\n        int index = group.getVideos().indexOf(video);\r\n\r\n        if (index >= 0) { // continuation group starts with zero index\r\n            Log.d(TAG, \"Found current video index: %s\", index);\r\n            Video found = group.getVideos().get(index);\r\n            if (!found.isMix() || video.isSectionPlaylistEnabled(getContext())) {\r\n                getPlayer().focusSuggestedItem(found);\r\n            }\r\n            mFocusCount = 0; // Stop the continuation loop\r\n            onDone.run();\r\n        } else if (mFocusCount > MAX_PLAYLIST_CONTINUATIONS || !video.hasPlaylist()) {\r\n            // Stop the continuation loop. Maybe the video isn't there.\r\n            mFocusCount = 0;\r\n            onDone.run();\r\n        } else {\r\n            // load more and repeat\r\n            continueGroup(group, newGroup -> focusAndContinueIfNeeded(newGroup, onDone), getPlayer().isSuggestionsShown());\r\n            mFocusCount++;\r\n        }\r\n    }\r\n\r\n    private void findNextSectionVideoIfNeeded(Video video) {\r\n        if (getPlayerData().getPlaybackMode() == PlayerConstants.PLAYBACK_MODE_SHUFFLE) {\r\n            findRandomSectionVideo(video);\r\n        } else {\r\n            findNextSectionVideo(video);\r\n        }\r\n    }\r\n\r\n    private void findRandomSectionVideo(Video video) {\r\n        mNextSectionVideo = null;\r\n\r\n        VideoGroup group = video.getGroup();\r\n\r\n        if (group == null || group.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        int currentIdx = group.indexOf(video);\r\n\r\n        int nextIdx = Utils.getRandomIndex(currentIdx, group.getSize());\r\n\r\n        mNextSectionVideo = group.get(nextIdx);\r\n        getPlayer().setNextTitle(mNextSectionVideo);\r\n    }\r\n\r\n    private void findNextSectionVideo(Video video) {\r\n        mNextSectionVideo = null;\r\n\r\n        VideoGroup group = video.getGroup();\r\n\r\n        if (group == null || group.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        List<Video> videos = group.getVideos();\r\n        boolean found = false;\r\n\r\n        for (Video current : videos) {\r\n            if (found && current.hasVideo() && !current.isUpcoming) {\r\n                mNextRetryCount = 0;\r\n                mNextSectionVideo = current;\r\n                getPlayer().setNextTitle(mNextSectionVideo);\r\n                return;\r\n            }\r\n\r\n            if (current.equals(video)) {\r\n                found = true;\r\n            }\r\n        }\r\n\r\n        if (mNextRetryCount > 0) {\r\n            mNextRetryCount = 0;\r\n        } else {\r\n            continueGroup(group, continuation -> findNextSectionVideoIfNeeded(video), getPlayer().isSuggestionsShown());\r\n            mNextRetryCount++;\r\n        }\r\n    }\r\n\r\n    private void showChapterDialog(ChapterItem chapter) {\r\n        AppDialogPresenter dialogPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        if (dialogPresenter.isDialogShown() && dialogPresenter.getId() != CHAPTER_NOTIFICATION_Id) {\r\n            // Another dialog is opened. Don't distract a user.\r\n            return;\r\n        }\r\n\r\n        if (dialogPresenter.isDialogShown() && getPlayer() != null && !getPlayer().isPlaying()) {\r\n            return;\r\n        }\r\n\r\n        dialogPresenter.closeDialog(); // remove previous dialog\r\n\r\n        if (chapter == null || getPlayer() == null || getPlayer().isOverlayShown() || getPlayer().isInPIPMode() ||\r\n                Utils.isScreenOff(getContext())) {\r\n            return;\r\n        }\r\n\r\n        OptionItem acceptOption = UiOptionItem.from(\r\n                chapter.getTitle(),\r\n                option -> {\r\n                    // return to previous dialog or close if no other dialogs in stack\r\n                    dialogPresenter.closeDialog();\r\n                    ChapterItem nextChapter = getNextChapter();\r\n                    getPlayer().setPositionMs(nextChapter != null ? nextChapter.getStartTimeMs() : getPlayer().getDurationMs());\r\n                }\r\n        );\r\n\r\n        dialogPresenter.appendSingleButton(acceptOption);\r\n\r\n        dialogPresenter.enableTransparent(true);\r\n        dialogPresenter.enableOverlay(true);\r\n        dialogPresenter.enableExpandable(false);\r\n        dialogPresenter.setId(CHAPTER_NOTIFICATION_Id);\r\n        dialogPresenter.showDialog();\r\n    }\r\n\r\n    private ChapterItem getNextChapter() {\r\n        if (getPlayer() == null || mChapters == null) {\r\n            return null;\r\n        }\r\n\r\n        long positionMs = getPlayer().getPositionMs();\r\n        for (ChapterItem chapter : mChapters) {\r\n            if (chapter.getStartTimeMs() > (positionMs + 3_000)) {\r\n                return chapter;\r\n            }\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    private Pair<ChapterItem, Integer> getCurrentChapter() {\r\n        if (getPlayer() == null || mChapters == null) {\r\n            return null;\r\n        }\r\n\r\n        return getCurrentChapter(getPlayer().getPositionMs());\r\n    }\r\n\r\n    private Pair<ChapterItem, Integer> getCurrentChapter(long positionMs) {\r\n        if (mChapters == null) {\r\n            return null;\r\n        }\r\n\r\n        ChapterItem currentChapter = null;\r\n        int idx = -1;\r\n\r\n        for (ChapterItem chapter : mChapters) {\r\n            if (chapter.getStartTimeMs() > (positionMs + 3_000)) {\r\n                break;\r\n            }\r\n            currentChapter = chapter;\r\n            idx++;\r\n        }\r\n\r\n        return currentChapter != null ? new Pair<>(currentChapter, idx) : null;\r\n    }\r\n\r\n    private void callListener(MediaItemMetadata mediaItemMetadata) {\r\n        if (mediaItemMetadata != null) {\r\n            getMainController().onMetadata(mediaItemMetadata);\r\n        }\r\n    }\r\n\r\n    private void disposeActions() {\r\n        RxHelper.disposeActions(mActions);\r\n        mChapters = null;\r\n        mNextSectionVideo = null;\r\n        if (mBrowseProcessor != null) {\r\n            mBrowseProcessor.dispose();\r\n        }\r\n    }\r\n\r\n    private void appendDislikes(Video video) {\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        if (!getPlayerTweaksData().isLikesCounterEnabled()) {\r\n            video.likeCount = null;\r\n            video.dislikeCount = null;\r\n            getPlayer().setVideo(video);\r\n            return;\r\n        }\r\n\r\n        Observable<DislikeData> dislikeDataObserve = mMediaItemService.getDislikeDataObserve(video.videoId);\r\n\r\n        Disposable dislikeAction = dislikeDataObserve.subscribe(\r\n                dislikeData -> {\r\n                    video.sync(dislikeData);\r\n                    getPlayer().setVideo(video);\r\n                },\r\n                error -> Log.e(TAG, \"Dislike not working...\")\r\n        );\r\n\r\n        mActions.add(dislikeAction);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/VideoLoaderController.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.util.Pair;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaFormat;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemFormatInfo;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Playlist;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.SimpleMediaItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.BasePlayerController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.listener.PlayerEventListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerConstants;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.VideoActionPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.PlaybackView;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\n\r\nimport io.reactivex.disposables.Disposable;\r\n\r\nimport java.util.Collections;\r\nimport java.util.List;\r\n\r\npublic class VideoLoaderController extends BasePlayerController {\r\n    private static final String TAG = VideoLoaderController.class.getSimpleName();\r\n    private static final long STREAM_END_THRESHOLD_MS = 180_000;\r\n    private static final long BUFFERING_THRESHOLD_MS = 3_000;\r\n    private static final long BUFFERING_WINDOW_MS = 60_000;\r\n    private static final long BUFFERING_RECURRENCE_COUNT = 5;\r\n    private static final long BUFFERING_CONTINUATION_MS = 20_000;\r\n    private final Playlist mPlaylist;\r\n    private Video mPendingVideo;\r\n    private int mLastErrorType = -1;\r\n    private SuggestionsController mSuggestionsController;\r\n    private long mSleepTimerStartMs;\r\n    private Disposable mFormatInfoAction;\r\n    private final Runnable mReloadVideo = () -> {\r\n        getMainController().onNewVideo(getVideo());\r\n    };\r\n    private final Runnable mLoadNext = this::loadNext;\r\n    private final Runnable mMetadataSync = () -> {\r\n        if (getPlayer() != null) {\r\n            waitMetadataSync(getVideo(), false);\r\n        }\r\n    };\r\n    private final Runnable mRestartEngine = () -> {\r\n        if (getPlayer() != null) {\r\n            getPlayer().restartEngine(); // properly save position of the current track\r\n        }\r\n    };\r\n    private final Runnable mOnLongBuffering = this::updateBufferingCountIfNeeded;\r\n\r\n    private final Runnable mRebootApp = () -> {\r\n        Video video = getVideo();\r\n        if (getPlayer() != null) {\r\n            Utils.restartTheApp(getContext(), video, getPlayer().getPositionMs());\r\n        }\r\n    };\r\n    private final Runnable mOnApplyPlaybackMode = () -> {\r\n        if (getPlayer() != null && getPlayer().getPositionMs() >= getPlayer().getDurationMs()) {\r\n            applyPlaybackMode(getPlaybackMode());\r\n        }\r\n    };\r\n    private Pair<Integer, Long> mBufferingCount;\r\n\r\n    public VideoLoaderController() {\r\n        mPlaylist = Playlist.instance();\r\n    }\r\n\r\n    @Override\r\n    public void onInit() {\r\n        mSuggestionsController = getController(SuggestionsController.class);\r\n        mSleepTimerStartMs = System.currentTimeMillis();\r\n    }\r\n\r\n    @Override\r\n    public void onNewVideo(Video item) {\r\n        if (item == null) {\r\n            return;\r\n        }\r\n\r\n        if (!item.fromQueue && !item.belongsToPlaybackQueue()) {\r\n            mPlaylist.add(item);\r\n        } else {\r\n            item.fromQueue = false;\r\n        }\r\n\r\n        if (getPlayer() != null && getPlayer().isEngineInitialized()) { // player is initialized\r\n            // Fix improperly resized video after exit from PIP (Device Formuler Z8 Pro)\r\n            loadVideo(item); // force play immediately even the same video\r\n        } else {\r\n            mPendingVideo = item;\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onBuffering() {\r\n        Utils.postDelayed(mOnLongBuffering, BUFFERING_THRESHOLD_MS);\r\n    }\r\n\r\n    @Override\r\n    public void onSeekEnd() {\r\n        // Reset buffering stats\r\n        mBufferingCount = null;\r\n    }\r\n\r\n    private void onLongBuffering() {\r\n        if (getPlayer() == null || getVideo() == null) {\r\n            return;\r\n        }\r\n\r\n        // Stream end check (hangs on buffering)\r\n        if (getPlayerTweaksData().isHighBitrateFormatsEnabled()) {\r\n            getPlayerTweaksData().setHighBitrateFormatsEnabled(false); // Response code: 429\r\n            reloadVideo();\r\n        } else if ((!getVideo().isLive || getVideo().isLiveEnd)\r\n                && getPlayer().getDurationMs() - getPlayer().getPositionMs() < STREAM_END_THRESHOLD_MS) {\r\n            getMainController().onPlayEnd();\r\n        } else if (!getVideo().isLive && !getVideo().isLiveEnd) {\r\n            if (isSubtitlesEnabled()) {\r\n                // Long loading subtitles cause hangs\r\n                disableSubtitles();\r\n                reloadVideo();\r\n            } else if (!getPlayerTweaksData().isNetworkErrorFixingDisabled()) {\r\n                // Faster source may differ across a devices. Try them one by one.\r\n                switchNextEngine();\r\n                restartEngine();\r\n            }\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onEngineInitialized() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n        \r\n        loadVideo(Helpers.firstNonNull(mPendingVideo, getVideo()));\r\n        getPlayer().setButtonState(R.id.action_repeat, getPlayerData().getPlaybackMode());\r\n        mSleepTimerStartMs = System.currentTimeMillis();\r\n        mPendingVideo = null;\r\n    }\r\n\r\n    @Override\r\n    public void onEngineReleased() {\r\n        disposeActions();\r\n    }\r\n\r\n    @Override\r\n    public void onEngineError(int type, int rendererIndex, Throwable error) {\r\n        Log.e(TAG, \"Player error occurred: %s. Trying to fix…\", type);\r\n\r\n        mLastErrorType = type;\r\n        runEngineErrorAction(type, rendererIndex, error);\r\n    }\r\n\r\n    @Override\r\n    public void onVideoLoaded(Video video) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        mLastErrorType = -1;\r\n        getPlayer().setButtonState(R.id.action_repeat, video.finishOnEnded ? PlayerConstants.PLAYBACK_MODE_CLOSE : getPlayerData().getPlaybackMode());\r\n        // Can't set title at this point\r\n        //checkSleepTimer();\r\n    }\r\n\r\n    @Override\r\n    public boolean onPreviousClicked() {\r\n        loadPrevious();\r\n\r\n        return true;\r\n    }\r\n\r\n    @Override\r\n    public boolean onNextClicked() {\r\n        if (getGeneralData().isChildModeEnabled()) {\r\n            onPlayEnd();\r\n        } else {\r\n            loadNext();\r\n        }\r\n\r\n        return true;\r\n    }\r\n\r\n    public void loadPrevious() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        openVideoInt(mSuggestionsController.getPrevious());\r\n\r\n        if (getPlayerTweaksData().isPlayerUiOnNextEnabled()) {\r\n            getPlayer().showOverlay(true);\r\n        }\r\n    }\r\n\r\n    public void loadNext() {\r\n        if (getPlayer() == null || getVideo() == null) {\r\n            return;\r\n        }\r\n\r\n        Video next = mSuggestionsController.getNext();\r\n\r\n        if (next != null) {\r\n            next.isShuffled = getVideo().isShuffled;\r\n            openVideoInt(next);\r\n        } else {\r\n            waitMetadataSync(getVideo(), true);\r\n        }\r\n\r\n        if (getPlayerTweaksData().isPlayerUiOnNextEnabled()) {\r\n            getPlayer().showOverlay(true);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onPlayEnd() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        // Stop the playback if the user is browsing options or reading comments\r\n        int playbackMode = getPlaybackMode();\r\n        if (getAppDialogPresenter().isDialogShown() && !getAppDialogPresenter().isOverlay() && playbackMode != PlayerConstants.PLAYBACK_MODE_ONE) {\r\n            getAppDialogPresenter().setOnFinish(mOnApplyPlaybackMode);\r\n        } else {\r\n            applyPlaybackMode(playbackMode);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onSuggestionItemClicked(Video item) {\r\n        openVideoInt(item);\r\n\r\n        if (getPlayer() != null)\r\n            getPlayer().showControls(false);\r\n    }\r\n\r\n    @Override\r\n    public boolean onKeyDown(int keyCode) {\r\n        mSleepTimerStartMs = System.currentTimeMillis();\r\n\r\n        // Remove error msg if needed\r\n        if (getPlayer() != null && getPlayerData().getSleepTimerHours() > 0) {\r\n            getPlayer().setVideo(getVideo());\r\n        }\r\n\r\n        Utils.removeCallbacks(mRestartEngine, mRebootApp);\r\n\r\n        return false;\r\n    }\r\n\r\n    @Override\r\n    public void onTickle() {\r\n        checkSleepTimer();\r\n    }\r\n\r\n    private void checkSleepTimer() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        float sleepHours = getPlayerData().getSleepTimerHours();\r\n        if (sleepHours > 0 && System.currentTimeMillis() - mSleepTimerStartMs > sleepHours * 60 * 60 * 1_000) {\r\n            getPlayer().setPlayWhenReady(false);\r\n            getPlayer().setTitle(getContext().getString(R.string.sleep_timer));\r\n            getPlayer().showOverlay(true);\r\n            Helpers.enableScreensaver(getActivity());\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Force load and play!\r\n     */\r\n    private void loadVideo(Video item) {\r\n        if (getPlayer() != null && item != null) {\r\n            mPlaylist.setCurrent(item);\r\n            getPlayer().setVideo(item);\r\n            getPlayer().resetPlayerState();\r\n            loadFormatInfo(item);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Force load suggestions.\r\n     */\r\n    private void loadSuggestions(Video item) {\r\n        if (item != null) {\r\n            mPlaylist.setCurrent(item);\r\n            getPlayer().setVideo(item);\r\n            mSuggestionsController.loadSuggestions(item);\r\n        }\r\n    }\r\n\r\n    private void waitMetadataSync(Video current, boolean showLoadingMsg) {\r\n        if (current == null) {\r\n            return;\r\n        }\r\n\r\n        if (current.nextMediaItem != null) {\r\n            openVideoInt(Video.from(current.nextMediaItem));\r\n        } else if (!current.isSynced) { // Maybe there's nothing left. E.g. when casting from phone\r\n            // Wait in a loop while suggestions have been loaded...\r\n            if (showLoadingMsg) {\r\n                MessageHelpers.showMessage(getContext(), R.string.wait_data_loading);\r\n            }\r\n            // Short videos next fix (suggestions aren't loaded yet)\r\n            boolean isEnded = getPlayer() != null && Math.abs(getPlayer().getDurationMs() - getPlayer().getPositionMs()) < 100;\r\n            if (isEnded) {\r\n                Utils.postDelayed(mMetadataSync, 1_000);\r\n            }\r\n        }\r\n    }\r\n\r\n    private void loadFormatInfo(Video video) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        getPlayer().showProgressBar(true);\r\n        disposeActions();\r\n\r\n        ServiceManager service = YouTubeServiceManager.instance();\r\n        MediaItemService mediaItemManager = service.getMediaItemService();\r\n        mFormatInfoAction = mediaItemManager.getFormatInfoObserve(video.videoId)\r\n                .subscribe(this::processFormatInfo,\r\n                           error -> {\r\n                               getPlayer().showProgressBar(false);\r\n                               runFormatErrorAction(error);\r\n                           });\r\n    }\r\n\r\n    private void processFormatInfo(MediaItemFormatInfo formatInfo) {\r\n        PlaybackView player = getPlayer();\r\n\r\n        if (player == null || getVideo() == null) {\r\n            return;\r\n        }\r\n\r\n        String bgImageUrl = null;\r\n\r\n        getVideo().sync(formatInfo);\r\n\r\n        // Fix stretched video for a couple milliseconds (before the onVideoSizeChanged gets called)\r\n        applyAspectRatio(formatInfo);\r\n\r\n        if (formatInfo.getPaidContentText() != null && getSponsorBlockData().isPaidContentNotificationEnabled()) {\r\n            MessageHelpers.showMessage(getContext(), formatInfo.getPaidContentText());\r\n        }\r\n\r\n        if (formatInfo.isUnplayable()) {\r\n            if (isEmbedPlayer()) {\r\n                player.finish();\r\n                return;\r\n            }\r\n\r\n            player.setTitle(formatInfo.getPlayabilityReason());\r\n            player.showProgressBar(false);\r\n            mSuggestionsController.loadSuggestions(getVideo());\r\n            bgImageUrl = getVideo().getBackgroundUrl();\r\n\r\n            // 18+ video or the video is hidden/removed\r\n            scheduleNextVideoTimer(5_000);\r\n\r\n            //if (formatInfo.isUnknownError()) { // the bot error or the video not available\r\n            //    scheduleRebootAppTimer(5_000);\r\n            //} else { // 18+ video or the video is hidden/removed\r\n            //    scheduleNextVideoTimer(5_000);\r\n            //}\r\n        } else if (acceptAdaptiveFormats(formatInfo) && formatInfo.containsDashFormats()) {\r\n            Log.d(TAG, \"Loading regular video in dash format...\");\r\n\r\n            if (getPlayerTweaksData().isHighBitrateFormatsEnabled() && formatInfo.hasExtendedHlsFormats()) {\r\n                player.openMerged(formatInfo, formatInfo.getHlsManifestUrl());\r\n            } else {\r\n                player.openDash(formatInfo);\r\n            }\r\n        } else if (acceptAdaptiveFormats(formatInfo) && formatInfo.containsSabrFormats()) {\r\n            Log.d(TAG, \"Loading video in sabr format...\");\r\n            player.openSabr(formatInfo);\r\n        } else if (acceptDashLive(formatInfo)) {\r\n            Log.d(TAG, \"Loading live video (current or past live stream) in dash format...\");\r\n            player.openDashUrl(formatInfo.getDashManifestUrl());\r\n        } else if (formatInfo.isLive() && formatInfo.containsHlsUrl()) {\r\n            Log.d(TAG, \"Loading live video (current or past live stream) in hls format...\");\r\n            player.openHlsUrl(formatInfo.getHlsManifestUrl());\r\n        } else if (formatInfo.containsUrlFormats()) {\r\n            Log.d(TAG, \"Loading url list video. This is always LQ...\");\r\n            player.openUrlList(applyFix(formatInfo.createUrlList()));\r\n        } else {\r\n            Log.d(TAG, \"Empty format info received. Seems future live translation. No video data to pass to the player.\");\r\n            player.setTitle(formatInfo.getPlayabilityReason());\r\n            player.showProgressBar(false);\r\n            mSuggestionsController.loadSuggestions(getVideo());\r\n            bgImageUrl = getVideo().getBackgroundUrl();\r\n            scheduleReloadVideoTimer(30 * 1_000);\r\n        }\r\n\r\n        player.showBackground(bgImageUrl); // remove bg (if video playing) or set another bg\r\n    }\r\n\r\n    private void scheduleReloadVideoTimer(int delayMs) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        if (getPlayer().isEngineInitialized()) {\r\n            Log.d(TAG, \"Reloading the video...\");\r\n            getPlayer().showOverlay(true);\r\n            Utils.postDelayed(mReloadVideo, delayMs);\r\n        }\r\n    }\r\n\r\n    private void scheduleNextVideoTimer(int delayMs) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        if (getPlayer().isEngineInitialized()) {\r\n            Log.d(TAG, \"Starting the next video...\");\r\n            getPlayer().showOverlay(true);\r\n            Utils.postDelayed(mLoadNext, delayMs);\r\n        }\r\n    }\r\n\r\n    private void scheduleRebootAppTimer(int delayMs) {\r\n        if (getPlayer() != null) {\r\n            Log.d(TAG, \"Rebooting the app...\");\r\n            getPlayer().showOverlay(true);\r\n            Utils.postDelayed(mRebootApp, delayMs);\r\n        }\r\n    }\r\n\r\n    private void scheduleRestartEngineTimer(int delayMs) {\r\n        if (getPlayer() != null) {\r\n            Log.d(TAG, \"Restarting the engine...\");\r\n            getPlayer().showOverlay(true);\r\n            Utils.postDelayed(mRestartEngine, delayMs);\r\n        }\r\n    }\r\n\r\n    private void openVideoInt(Video item) {\r\n        if (item == null) {\r\n            return;\r\n        }\r\n\r\n        disposeActions();\r\n\r\n        if (item.hasVideo()) {\r\n            // NOTE: Next clicked: instant playback even a mix\r\n            // NOTE: Bypass PIP fullscreen on next caused by startView\r\n            getMainController().onNewVideo(item);\r\n            //getPlayer().showOverlay(true);\r\n        } else {\r\n            VideoActionPresenter.instance(getContext()).apply(item);\r\n        }\r\n    }\r\n\r\n    private boolean isActionsRunning() {\r\n        return RxHelper.isAnyActionRunning(mFormatInfoAction);\r\n    }\r\n\r\n    private void disposeActions() {\r\n        mBufferingCount = null;\r\n        MediaServiceManager.instance().disposeActions();\r\n        RxHelper.disposeActions(mFormatInfoAction);\r\n        Utils.removeCallbacks(mReloadVideo, mLoadNext, mRestartEngine, mMetadataSync, mOnLongBuffering, mRebootApp);\r\n    }\r\n\r\n    private void runFormatErrorAction(Throwable error) {\r\n        if (isEmbedPlayer()) {\r\n            if (getPlayer() != null) {\r\n                getPlayer().finish();\r\n            }\r\n            return;\r\n        }\r\n\r\n        String message = error.getMessage();\r\n        String className = error.getClass().getSimpleName();\r\n        String fullMsg = String.format(\"loadFormatInfo error: %s: %s\", className, Utils.getStackTraceAsString(error));\r\n        Log.e(TAG, fullMsg);\r\n\r\n        if (!Helpers.containsAny(message, \"fromNullable result is null\")) {\r\n            MessageHelpers.showLongMessage(getContext(), fullMsg);\r\n        }\r\n\r\n        if (Helpers.containsAny(message, \"Unexpected token\", \"Syntax error\", \"invalid argument\") || // temporal fix\r\n                Helpers.equalsAny(className, \"PoTokenException\", \"BadWebViewException\")) {\r\n            YouTubeServiceManager.instance().applyNoPlaybackFix();\r\n            reloadVideo();\r\n        } else if (Helpers.containsAny(message, \"is not defined\")) {\r\n            YouTubeServiceManager.instance().invalidateCache();\r\n            reloadVideo();\r\n        } else {\r\n            Log.e(TAG, \"Probably no internet connection\");\r\n            scheduleReloadVideoTimer(1_000);\r\n        }\r\n    }\r\n    \r\n    private void runEngineErrorAction(int type, int rendererIndex, Throwable error) {\r\n        // Hide begin errors in embed mode (e.g. wrong date/time: unable to connect to...)\r\n        if (isEmbedPlayer() && getPlayer() != null && getPlayer().getPositionMs() == 0) {\r\n            getPlayer().finish();\r\n            return;\r\n        }\r\n\r\n        if (getVideo() != null && getVideo().isLiveEnd) {\r\n            // Url no longer works (e.g. live stream ended)\r\n            getMainController().onPlayEnd();\r\n            return;\r\n        }\r\n\r\n        boolean restart = applyEngineErrorAction(type, rendererIndex, error);\r\n\r\n        if (restart) {\r\n            restartEngine();\r\n        } else {\r\n            reloadVideo();\r\n        }\r\n    }\r\n\r\n    private boolean applyEngineErrorAction(int type, int rendererIndex, Throwable error) {\r\n        boolean restartEngine = true;\r\n        boolean showMessage = true;\r\n        String errorContent = error != null ? error.getMessage() : null;\r\n        String errorTitle = getErrorTitle(type, rendererIndex);\r\n        String errorMessage = errorTitle + \"\\n\" + errorContent;\r\n\r\n        if (Helpers.startsWithAny(errorContent, \"Unable to connect to\")) {\r\n            // No internet connection or WRONG DATE on the device\r\n            // Recently this message starting to show for other reasons\r\n            YouTubeServiceManager.instance().applyNoPlaybackFix(); // ?\r\n            //switchNextEngine(); // ?\r\n            //restartEngine = false;\r\n        } else if (error instanceof OutOfMemoryError || (error != null && error.getCause() instanceof OutOfMemoryError)) {\r\n            if (getPlayerTweaksData().getPlayerDataSource() == PlayerTweaksData.PLAYER_DATA_SOURCE_OKHTTP) {\r\n                // OkHttp has memory leak problems\r\n                enableFasterDataSource();\r\n            } else if (getPlayerData().getVideoBufferType() == PlayerData.BUFFER_HIGH || getPlayerData().getVideoBufferType() == PlayerData.BUFFER_HIGHEST) {\r\n                getPlayerData().setVideoBufferType(PlayerData.BUFFER_MEDIUM);\r\n            } else {\r\n                getPlayerTweaksData().setSectionPlaylistEnabled(false);\r\n                restartEngine = false;\r\n            }\r\n        } else if (Helpers.containsAny(errorContent, \"Exception in CronetUrlRequest\", \"Response code: 503\") && !getPlayerTweaksData().isNetworkErrorFixingDisabled()) {\r\n            if (getVideo() != null && !getVideo().isLive) { // Finished live stream may provoke errors in Cronet\r\n                getPlayerTweaksData().setPlayerDataSource(PlayerTweaksData.PLAYER_DATA_SOURCE_DEFAULT);\r\n            } else {\r\n                restartEngine = false;\r\n            }\r\n        } else if (type == PlayerEventListener.ERROR_TYPE_SOURCE && rendererIndex == PlayerEventListener.RENDERER_INDEX_UNKNOWN) {\r\n            // NOTE: Starts with any (url deciphered incorrectly)\r\n            // \"Response code: 403\" (poToken error, forbidden)\r\n            // \"Response code: 404\" (not sure whether below helps)\r\n            // \"Response code: 503\" (not sure whether below helps)\r\n            // \"Response code: 400\" (not sure whether below helps)\r\n            // \"Response code: 429\" (subtitle error, too many requests)\r\n            // \"Response code: 500\" (subtitle error, generic server error)\r\n\r\n            // NOTE: Fixing too many requests or network issues\r\n            // NOTE: All these errors have unknown renderer (-1)\r\n            // \"Unable to connect to\", \"Invalid NAL length\", \"Response code: 421\",\r\n            // \"Response code: 404\", \"Response code: 429\", \"Invalid integer size\",\r\n            // \"Unexpected ArrayIndexOutOfBoundsException\", \"Unexpected IndexOutOfBoundsException\"\r\n            if (Helpers.startsWithAny(errorContent, \"Response code: 403\")) {\r\n                YouTubeServiceManager.instance().applyNoPlaybackFix();\r\n            } else if (isSubtitlesEnabled()) {\r\n                disableSubtitles(); // Response code: 429\r\n            } else if (getPlayerTweaksData().isHighBitrateFormatsEnabled()) {\r\n                getPlayerTweaksData().setHighBitrateFormatsEnabled(false); // Response code: 429\r\n            } else {\r\n                YouTubeServiceManager.instance().applyNoPlaybackFix(); // Response code: 403\r\n            }\r\n            restartEngine = false;\r\n            showMessage = false;\r\n        } else if (type == PlayerEventListener.ERROR_TYPE_RENDERER && rendererIndex == PlayerEventListener.RENDERER_INDEX_SUBTITLE) {\r\n            // \"Response code: 429\" (subtitle error)\r\n            // \"Response code: 500\" (subtitle error)\r\n            disableSubtitles();\r\n            restartEngine = false;\r\n        } else if (type == PlayerEventListener.ERROR_TYPE_RENDERER && rendererIndex == PlayerEventListener.RENDERER_INDEX_VIDEO) {\r\n            getPlayerData().setFormat(FormatItem.VIDEO_FHD_AVC_30);\r\n            if (getPlayerTweaksData().isSWDecoderForced()) {\r\n                getPlayerTweaksData().setSWDecoderForced(false);\r\n            } else {\r\n                restartEngine = false;\r\n            }\r\n        } else if (type == PlayerEventListener.ERROR_TYPE_RENDERER && rendererIndex == PlayerEventListener.RENDERER_INDEX_AUDIO) {\r\n            getPlayerData().setFormat(FormatItem.AUDIO_HQ_MP4A);\r\n            restartEngine = false;\r\n        } else if (type == PlayerEventListener.ERROR_TYPE_UNEXPECTED) {\r\n            // Hide unknown errors on all devices\r\n            showMessage = false;\r\n        }\r\n\r\n        if (showMessage) {\r\n            MessageHelpers.showLongMessage(getContext(), errorMessage);\r\n        }\r\n\r\n        return restartEngine;\r\n    }\r\n\r\n    @SuppressLint(\"StringFormatMatches\")\r\n    private String getErrorTitle(int type, int rendererIndex) {\r\n        String errorTitle;\r\n        int msgResId;\r\n\r\n        switch (type) {\r\n            // Some ciphered data could be outdated.\r\n            // Might happen when the app wasn't used quite a long time.\r\n            case PlayerEventListener.ERROR_TYPE_SOURCE:\r\n                switch (rendererIndex) {\r\n                    case PlayerEventListener.RENDERER_INDEX_VIDEO:\r\n                        msgResId = R.string.msg_player_error_video_source;\r\n                        break;\r\n                    case PlayerEventListener.RENDERER_INDEX_AUDIO:\r\n                        msgResId = R.string.msg_player_error_audio_source;\r\n                        break;\r\n                    case PlayerEventListener.RENDERER_INDEX_SUBTITLE:\r\n                        msgResId = R.string.msg_player_error_subtitle_source;\r\n                        break;\r\n                    default:\r\n                        msgResId = R.string.unknown_source_error;\r\n                }\r\n                errorTitle = getContext().getString(msgResId);\r\n                break;\r\n            case PlayerEventListener.ERROR_TYPE_RENDERER:\r\n                switch (rendererIndex) {\r\n                    case PlayerEventListener.RENDERER_INDEX_VIDEO:\r\n                        msgResId = R.string.msg_player_error_video_renderer;\r\n                        break;\r\n                    case PlayerEventListener.RENDERER_INDEX_AUDIO:\r\n                        msgResId = R.string.msg_player_error_audio_renderer;\r\n                        break;\r\n                    case PlayerEventListener.RENDERER_INDEX_SUBTITLE:\r\n                        msgResId = R.string.msg_player_error_subtitle_renderer;\r\n                        break;\r\n                    default:\r\n                        msgResId = R.string.unknown_renderer_error;\r\n                }\r\n                errorTitle = getContext().getString(msgResId);\r\n                break;\r\n            case PlayerEventListener.ERROR_TYPE_UNEXPECTED:\r\n                errorTitle = getContext().getString(R.string.msg_player_error_unexpected);\r\n                break;\r\n            default:\r\n                errorTitle = getContext().getString(R.string.msg_player_error, type);\r\n                break;\r\n        }\r\n\r\n        return errorTitle;\r\n    }\r\n\r\n    private void restartEngine() {\r\n        scheduleRestartEngineTimer(1_000);\r\n    }\r\n\r\n    private void reloadVideo() {\r\n        scheduleReloadVideoTimer(1_000);\r\n    }\r\n\r\n    private void rebootApp() {\r\n        scheduleRebootAppTimer(1_000);\r\n    }\r\n\r\n    private List<String> applyFix(List<String> urlList) {\r\n        // Sometimes top url cannot be played\r\n        if (mLastErrorType == PlayerEventListener.ERROR_TYPE_SOURCE) {\r\n            Collections.reverse(urlList);\r\n        }\r\n\r\n        return urlList;\r\n    }\r\n\r\n    private void applyPlaybackMode(int playbackMode) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        Video video = getVideo();\r\n        // Fix simultaneous videos loading (e.g. when playback ends and user opens new video)\r\n        if (video == null || isActionsRunning()) {\r\n            return;\r\n        }\r\n\r\n        if (isEmbedPlayer()) {\r\n            playbackMode = PlayerConstants.PLAYBACK_MODE_CLOSE;\r\n        }\r\n\r\n        switch (playbackMode) {\r\n            case PlayerConstants.PLAYBACK_MODE_REVERSE_LIST:\r\n                if (video.hasPlaylist() || video.belongsToChannelUploads() || video.belongsToChannel()) {\r\n                    VideoGroup group = video.getGroup();\r\n                    if (group != null && group.indexOf(video) != 0) { // stop after first\r\n                        onPreviousClicked();\r\n                    }\r\n                    break;\r\n                }\r\n            case PlayerConstants.PLAYBACK_MODE_ALL:\r\n            case PlayerConstants.PLAYBACK_MODE_SHUFFLE:\r\n                loadNext();\r\n                break;\r\n            case PlayerConstants.PLAYBACK_MODE_ONE:\r\n                getPlayer().setPositionMs(100); // fix frozen image on Android 4?\r\n                break;\r\n            case PlayerConstants.PLAYBACK_MODE_CLOSE:\r\n                // Close player if suggestions not shown\r\n                // Except when playing from queue\r\n                if (mPlaylist.getNext() != null && !getPlayerTweaksData().isQueueRespectsPlaybackMode()) {\r\n                    loadNext();\r\n                } else {\r\n                    AppDialogPresenter dialog = getAppDialogPresenter();\r\n                    if (!getPlayer().isSuggestionsShown() && (!dialog.isDialogShown() || dialog.isOverlay())) {\r\n                        dialog.closeDialog();\r\n                        getPlayer().finishReally();\r\n                    }\r\n                }\r\n                break;\r\n            case PlayerConstants.PLAYBACK_MODE_PAUSE:\r\n                // Stop player after each video.\r\n                // Except when playing from queue\r\n                if (mPlaylist.getNext() != null && !getPlayerTweaksData().isQueueRespectsPlaybackMode()) {\r\n                    loadNext();\r\n                } else {\r\n                    stopPlayback();\r\n                }\r\n                break;\r\n            case PlayerConstants.PLAYBACK_MODE_LIST:\r\n                // if video has a playlist load next or restart playlist\r\n                if (video.hasNextPlaylist() || mPlaylist.getNext() != null) {\r\n                    loadNext();\r\n                } else {\r\n                    //restartPlaylistIfNeeded();\r\n                    stopPlayback();\r\n                }\r\n                break;\r\n            default:\r\n                Log.e(TAG, \"Undetected repeat mode \" + playbackMode);\r\n                break;\r\n        }\r\n    }\r\n\r\n    private void stopPlayback() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        getPlayer().setPositionMs(getPlayer().getDurationMs());\r\n        getPlayer().setPlayWhenReady(false);\r\n        getPlayer().showSuggestions(true);\r\n    }\r\n\r\n    private void restartPlaylistIfNeeded() {\r\n        if (getPlayer() == null || getVideo() == null) {\r\n            return;\r\n        }\r\n        \r\n        VideoGroup group = getVideo().getGroup(); // Get the VideoGroup (playlist)\r\n\r\n        if (group != null && !group.isEmpty() && getVideo().belongsToSamePlaylistGroup()) {\r\n            openVideoInt(group.get(0));\r\n        } else {\r\n            Log.e(TAG, \"VideoGroup is null or empty. Can't restart playlist.\");\r\n            stopPlayback();\r\n        }\r\n    }\r\n\r\n    private boolean acceptAdaptiveFormats(MediaItemFormatInfo formatInfo) {\r\n        if (getPlayerData().isLegacyCodecsForced() && formatInfo.containsUrlFormats()) {\r\n            return false;\r\n        }\r\n\r\n        if (getPlayerTweaksData().isHlsStreamsForced() && formatInfo.isLive() && formatInfo.containsHlsUrl()) {\r\n            return false;\r\n        }\r\n\r\n        // Not enough info for full length live streams\r\n        if (formatInfo.isLive() && formatInfo.getStartTimeMs() == 0) {\r\n            return false;\r\n        }\r\n\r\n        // Live dash url doesn't work with None buffer\r\n        //if (formatInfo.isLive() && (getPlayerTweaksData().isDashUrlStreamsForced() || getPlayerData().getVideoBufferType() == PlayerData.BUFFER_NONE)) {\r\n        if (formatInfo.isLive() && getPlayerTweaksData().isDashUrlStreamsForced() && formatInfo.containsDashUrl()) {\r\n            return false;\r\n        }\r\n\r\n        if (formatInfo.isLive() && getPlayerTweaksData().isHlsStreamsForced() && formatInfo.containsHlsUrl()) {\r\n            return false;\r\n        }\r\n\r\n        return true;\r\n    }\r\n\r\n    private boolean acceptDashLive(MediaItemFormatInfo formatInfo) {\r\n        if (getPlayerTweaksData().isHlsStreamsForced() && formatInfo.isLive() && formatInfo.containsHlsUrl()) {\r\n            return false;\r\n        }\r\n\r\n        return formatInfo.isLive() && formatInfo.containsDashUrl();\r\n    }\r\n\r\n    @Override\r\n    public void onMetadata(MediaItemMetadata metadata) {\r\n        loadRandomNext();\r\n    }\r\n\r\n    @Override\r\n    public void onPlay() {\r\n        Utils.removeCallbacks(mOnLongBuffering);\r\n    }\r\n\r\n    @Override\r\n    public void onPause() {\r\n        Utils.removeCallbacks(mOnLongBuffering);\r\n    }\r\n\r\n    private void loadRandomNext() {\r\n        MediaServiceManager.instance().disposeActions();\r\n\r\n        if (getPlayer() == null || getPlayerData() == null || getVideo() == null || getVideo().playlistInfo == null ||\r\n                getPlayerData().getPlaybackMode() != PlayerConstants.PLAYBACK_MODE_SHUFFLE) {\r\n            return;\r\n        }\r\n\r\n        if (getVideo().playlistInfo.getSize() != -1) {\r\n            Video video = new Video();\r\n            video.playlistId = getVideo().playlistId;\r\n            video.playlistIndex = Utils.getRandomIndex(getVideo().playlistInfo.getCurrentIndex(), getVideo().playlistInfo.getSize());\r\n            MediaServiceManager.instance().loadMetadata(video, randomMetadata -> {\r\n                if (randomMetadata.getNextVideo() == null) {\r\n                    return;\r\n                }\r\n\r\n                getVideo().nextMediaItem = SimpleMediaItem.from(randomMetadata);\r\n                getPlayer().setNextTitle(Video.from(getVideo().nextMediaItem));\r\n            });\r\n        } else {\r\n            VideoGroup topRow = getPlayer().getSuggestionsByIndex(0); // the playlist row\r\n            if (topRow != null) {\r\n                int currentIdx = topRow.indexOf(getVideo());\r\n                int randomIndex = Utils.getRandomIndex(currentIdx, topRow.getSize());\r\n\r\n                if (randomIndex != -1) {\r\n                    Video nextVideo = topRow.get(randomIndex);\r\n                    getVideo().nextMediaItem = SimpleMediaItem.from(nextVideo);\r\n                    getPlayer().setNextTitle(nextVideo);\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    private void loadRandomNext2() {\r\n        if (getPlayer() == null || getPlayerData() == null || getVideo() == null || getVideo().isShuffled ||\r\n                getVideo().shuffleMediaItem == null || getPlayerData().getPlaybackMode() != PlayerConstants.PLAYBACK_MODE_SHUFFLE) {\r\n            return;\r\n        }\r\n\r\n        getVideo().isShuffled = true;\r\n        getVideo().playlistParams = getVideo().shuffleMediaItem.getParams();\r\n        getController(SuggestionsController.class).loadSuggestions(getVideo());\r\n    }\r\n\r\n    private void updateBufferingCountIfNeeded() {\r\n        updateBufferingCount();\r\n        if (isBufferingRecurrent()) {\r\n            mBufferingCount = null;\r\n            onLongBuffering();\r\n        } else {\r\n            // Count continuous buffering as a new occurrences....\r\n            Utils.postDelayed(mOnLongBuffering, BUFFERING_CONTINUATION_MS);\r\n        }\r\n    }\r\n\r\n    private void updateBufferingCount() {\r\n        final long currentTimeMs = System.currentTimeMillis();\r\n        int bufferingCount = 0;\r\n        long previousTimeMs = 0;\r\n\r\n        if (mBufferingCount != null) {\r\n            bufferingCount = mBufferingCount.first;\r\n            previousTimeMs = mBufferingCount.second;\r\n        }\r\n\r\n        if (currentTimeMs - previousTimeMs < BUFFERING_WINDOW_MS) {\r\n            bufferingCount++;\r\n        } else {\r\n            bufferingCount = 1;\r\n        }\r\n\r\n        mBufferingCount = new Pair<>(bufferingCount, currentTimeMs);\r\n    }\r\n\r\n    private boolean isBufferingRecurrent() {\r\n        return mBufferingCount != null && mBufferingCount.first > BUFFERING_RECURRENCE_COUNT;\r\n    }\r\n\r\n    private void switchNextEngine() {\r\n        getPlayerTweaksData().setPlayerDataSource(getNextEngine());\r\n    }\r\n\r\n    private int getNextEngine() {\r\n        int currentEngine = getPlayerTweaksData().getPlayerDataSource();\r\n        Integer[] engineList = Utils.skipCronet() ?\r\n                new Integer[] { PlayerTweaksData.PLAYER_DATA_SOURCE_DEFAULT, PlayerTweaksData.PLAYER_DATA_SOURCE_OKHTTP } :\r\n                new Integer[] { PlayerTweaksData.PLAYER_DATA_SOURCE_CRONET, PlayerTweaksData.PLAYER_DATA_SOURCE_DEFAULT, PlayerTweaksData.PLAYER_DATA_SOURCE_OKHTTP };\r\n        return Helpers.getNextValue(engineList, currentEngine);\r\n    }\r\n\r\n    private static int getFasterDataSource() {\r\n        return Utils.skipCronet() ? PlayerTweaksData.PLAYER_DATA_SOURCE_DEFAULT : PlayerTweaksData.PLAYER_DATA_SOURCE_CRONET;\r\n    }\r\n\r\n    /**\r\n     * Bad idea. Faster source is different among devices\r\n     */\r\n    private void enableFasterDataSource() {\r\n        if (isFasterDataSourceEnabled()) {\r\n            return;\r\n        }\r\n\r\n        getPlayerTweaksData().setPlayerDataSource(getFasterDataSource());\r\n    }\r\n\r\n    /**\r\n     * Bad idea. Faster source is different among devices\r\n     */\r\n    private boolean isFasterDataSourceEnabled() {\r\n        int fasterDataSource = getFasterDataSource();\r\n        return getPlayerTweaksData().getPlayerDataSource() == fasterDataSource;\r\n    }\r\n\r\n    private int getPlaybackMode() {\r\n        int playbackMode = getPlayerData().getPlaybackMode();\r\n\r\n        Video video = getVideo();\r\n        if (video != null && video.finishOnEnded) {\r\n            playbackMode = PlayerConstants.PLAYBACK_MODE_CLOSE;\r\n        } else if (video != null && video.belongsToShortsGroup() && getPlayerTweaksData().isLoopShortsEnabled()) {\r\n            playbackMode = PlayerConstants.PLAYBACK_MODE_ONE;\r\n        }\r\n        return playbackMode;\r\n    }\r\n\r\n    /**\r\n     * Fix stretched video for a couple milliseconds (before the onVideoSizeChanged gets called)\r\n     */\r\n    private void applyAspectRatio(MediaItemFormatInfo formatInfo) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        // Fix stretched video for a couple milliseconds (before the onVideoSizeChanged gets called)\r\n        if (formatInfo.containsDashFormats()) {\r\n            MediaFormat format = formatInfo.getAdaptiveFormats().get(0);\r\n            int width = format.getWidth();\r\n            int height = format.getHeight();\r\n            boolean isShorts = width < height;\r\n            if (width > 0 && height > 0 && (getPlayerData().getAspectRatio() == PlayerData.ASPECT_RATIO_DEFAULT || isShorts)) {\r\n                getPlayer().setAspectRatio((float) width / height);\r\n            } else {\r\n                getPlayer().setAspectRatio(getPlayerData().getAspectRatio());\r\n            }\r\n        }\r\n    }\r\n\r\n    private void preloadNextVideoIfNeeded() {\r\n        if (isEmbedPlayer() || getPlayer() == null || getVideo() == null || getVideo().isLive) {\r\n            return;\r\n        }\r\n\r\n        if (getPlayer().getDurationMs() - getPlayer().getPositionMs() < 50_000) {\r\n            MediaServiceManager.instance().loadFormatInfo(mSuggestionsController.getNext(), formatInfo -> {});\r\n        }\r\n    }\r\n\r\n    private boolean isSubtitlesEnabled() {\r\n        return getPlayer() != null && !FormatItem.SUBTITLE_NONE.equals(getPlayer().getSubtitleFormat());\r\n    }\r\n\r\n    private void disableSubtitles() {\r\n        //if (getVideo() != null) {\r\n        //    getPlayerData().disableSubtitlesPerChannel(getVideo().channelId);\r\n        //}\r\n\r\n        getPlayerData().setSubtitlesPerChannelEnabled(false); // Important!\r\n        getPlayerData().setFormat(FormatItem.SUBTITLE_NONE);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/controllers/VideoStateController.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Playlist;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.BasePlayerController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerUI;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService.State;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.ScreensaverManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.internal.MediaServiceData;\r\n\r\npublic class VideoStateController extends BasePlayerController {\r\n    private static final String TAG = VideoStateController.class.getSimpleName();\r\n    private static final long MUSIC_VIDEO_MAX_DURATION_MS = 6 * 60 * 1000;\r\n    private static final long RESTORE_LIVE_BUFFER_MS = 60_000;\r\n    private static final long DEFAULT_LIVE_BUFFER_MS = 60_000; // Minimum issues\r\n    private static final long OFFICIAL_LIVE_BUFFER_MS = 15_000; // Official app buffer\r\n    private static final long LIVE_BUFFER_MS = OFFICIAL_LIVE_BUFFER_MS;\r\n    private static final long SHORT_LIVE_BUFFER_MS = 0; // Note, on buffer lower than the 60sec you'll notice segment skip\r\n    private static final long BEGIN_THRESHOLD_MS = 10_000;\r\n    private static final long EMBED_THRESHOLD_MS = 30_000;\r\n    private static final int HISTORY_UPDATE_INTERVAL_MINUTES = 3; // Sync history every x minutes\r\n    private boolean mIsPlayEnabled;\r\n    private boolean mIsPlayBlocked;\r\n    private int mTickleLeft;\r\n    private boolean mIncognito;\r\n    private final Runnable mUpdateHistory = this::saveState;\r\n    private long mNewVideoTimeMs;\r\n\r\n    /**\r\n     * Fired after user clicked on video in browse activity<br/>\r\n     * or video is opened from the intent\r\n     */\r\n    @Override\r\n    public void onNewVideo(Video item) {\r\n        // Ensure that we aren't running on presenter init stage\r\n        if (getPlayer() != null && getPlayer().containsMedia()) {\r\n            //if (!item.equals(getVideo())) { // a video might be opened twice (when remote connection enabled). Fix for that.\r\n\r\n            // NOTE: even for the same videos it's good to save state (switch from embed, video reload etc)\r\n            // Reset auto-save history timer\r\n            mTickleLeft = 0;\r\n            // Save state of the previous video.\r\n            // In case video opened from phone and other stuff.\r\n            saveState();\r\n        }\r\n\r\n        if (!item.equals(getVideo())) {\r\n            mNewVideoTimeMs = System.currentTimeMillis();\r\n        }\r\n\r\n        setPlayEnabled(true); // video just added\r\n\r\n        getPlayerData().setTempVideoFormat(null);\r\n\r\n        enableIncognitoIfNeeded(item);\r\n\r\n        // Don't do reset on videoLoaded state because this will influences minimized music videos.\r\n        resetPositionIfNeeded(item);\r\n        resetGlobalSpeedIfNeeded();\r\n    }\r\n\r\n    @Override\r\n    public boolean onPreviousClicked() {\r\n        // Seek to the start on prev\r\n        if (getPlayer() != null && getPlayer().getPositionMs() > BEGIN_THRESHOLD_MS) {\r\n            saveState(); // in case the user wants to go to previous video\r\n            getPlayer().setPositionMs(100);\r\n            return true;\r\n        }\r\n\r\n        // Pass to others handlers\r\n        return false;\r\n    }\r\n\r\n    @Override\r\n    public boolean onNextClicked() {\r\n        // Seek to the actual live position on next\r\n        if (getVideo() != null && getPlayer() != null\r\n                && getVideo().isLive && (getPlayer().getDurationMs() - getPlayer().getPositionMs() > getLiveThreshold())) {\r\n            getPlayer().setPositionMs(getPlayer().getDurationMs() - getLiveBuffer());\r\n            return true;\r\n        }\r\n\r\n        setPlayEnabled(true);\r\n\r\n        saveState();\r\n\r\n        clearStateOfNextVideo();\r\n\r\n        return false;\r\n    }\r\n\r\n    @Override\r\n    public void onSuggestionItemClicked(Video item) {\r\n        setPlayEnabled(true); // autoplay video from suggestions\r\n\r\n        saveState();\r\n    }\r\n\r\n    @Override\r\n    public void onEngineInitialized() {\r\n        // Reset auto-save history timer\r\n        mTickleLeft = 0;\r\n\r\n        // Restore before video loaded.\r\n        // This way we override auto track selection mechanism.\r\n        //restoreFormats();\r\n\r\n        // Show user info instead of black screen.\r\n        if (!getPlayEnabled()) {\r\n            getPlayer().showOverlay(true);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onEngineReleased() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        // Save previous state\r\n        if (getPlayer().containsMedia()) {\r\n            setPlayEnabled(getPlayer().getPlayWhenReady());\r\n            saveState();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onTickle() {\r\n        if (getPlayer() == null || !getPlayer().isEngineInitialized()) {\r\n            return;\r\n        }\r\n\r\n        if (++mTickleLeft > HISTORY_UPDATE_INTERVAL_MINUTES && getPlayer().isPlaying()) {\r\n            mTickleLeft = 0;\r\n            saveState();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onMetadata(MediaItemMetadata metadata) {\r\n        // Channel info should be loaded at this point\r\n        restoreSubtitleFormat();\r\n\r\n        // Need to contain channel id\r\n        restoreSpeedAndPositionIfNeeded();\r\n\r\n        // NOTE: needed for the restore after oom crash?\r\n        saveState(); // start watching?\r\n    }\r\n\r\n    @Override\r\n    public void onEngineError(int type, int rendererIndex, Throwable error) {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        // Oops. Error happens while playing (network lost etc).\r\n        if (getPlayer().getPositionMs() > 1_000) {\r\n            saveState();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onVideoLoaded(Video item) {\r\n        // Actual video that match currently loaded one.\r\n        //mVideo = item;\r\n\r\n        // Restore formats again.\r\n        // Maybe this could help with Shield format problem.\r\n        // NOTE: produce multi thread exception:\r\n        // Attempt to read from field 'java.util.TreeMap$TreeMapEntry java.util.TreeMap$TreeMapEntry.left' on a null object reference (TrackSelectorManager.java:181)\r\n        //restoreFormats();\r\n\r\n        // In this state video length is not undefined.\r\n        restoreState();\r\n    }\r\n\r\n    @Override\r\n    public void onPlay() {\r\n        setPlayEnabled(true);\r\n        showHideScreensaver(false);\r\n        // throttle seeking calls\r\n        Utils.removeCallbacks(mUpdateHistory);\r\n    }\r\n\r\n    @Override\r\n    public void onPause() {\r\n        setPlayEnabled(false);\r\n        showHideScreensaver(true);\r\n        // throttle seeking calls\r\n        Utils.postDelayed(mUpdateHistory, 10_000);\r\n    }\r\n\r\n    @Override\r\n    public void onTrackSelected(FormatItem track) {\r\n        //if (!getPlayer().isInPIPMode()) {\r\n        //    if (track.getType() == FormatItem.TYPE_VIDEO) {\r\n        //        if (getPlayerData().getFormat(FormatItem.TYPE_VIDEO).isPreset()) {\r\n        //            mTempVideoFormat = track;\r\n        //        } else {\r\n        //            mTempVideoFormat = null;\r\n        //            getPlayerData().setFormat(track);\r\n        //        }\r\n        //    } else {\r\n        //        getPlayerData().setFormat(track);\r\n        //    }\r\n        //}\r\n\r\n        //if (!getPlayer().isInPIPMode()) {\r\n        //    if (track.getType() == FormatItem.TYPE_VIDEO) {\r\n        //        mTempVideoFormat = getPlayerData().getFormat(FormatItem.TYPE_VIDEO).isPreset() ? track : null;\r\n        //    }\r\n        //}\r\n    }\r\n\r\n    @Override\r\n    public void onPlayEnd() {\r\n        saveState();\r\n\r\n        // Don't enable screensaver here or you'll broke 'screen off' logic.\r\n        showHideScreensaver(true);\r\n    }\r\n\r\n    @Override\r\n    public void onBuffering() {\r\n        // Restore speed on LIVE end or after seek\r\n        restoreSpeedAndPositionIfNeeded();\r\n\r\n        // Live stream starts to buffer after the end\r\n        showHideScreensaver(true);\r\n    }\r\n\r\n    @Override\r\n    public void onSourceChanged(Video item) {\r\n        // At this stage video isn't loaded yet. So format switch isn't take any resources.\r\n        restoreFormats();\r\n    }\r\n\r\n    @Override\r\n    public void onSpeedChanged(float speed) {\r\n        if (getVideo() == null) {\r\n            return;\r\n        }\r\n\r\n        getPlayerData().setSpeed(getVideo().channelId, speed);\r\n    }\r\n\r\n    @Override\r\n    public void onButtonClicked(int buttonId, int buttonState) {\r\n        if (buttonId == R.id.action_video_speed) {\r\n            onSpeedClicked(buttonState == PlayerUI.BUTTON_ON);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onButtonLongClicked(int buttonId, int buttonState) {\r\n        if (buttonId == R.id.action_video_speed) {\r\n            onSpeedLongClicked(buttonState == PlayerUI.BUTTON_ON);\r\n        }\r\n    }\r\n\r\n    private void onSpeedClicked(boolean enabled) {\r\n        if (getPlayer() == null || getVideo() == null) {\r\n            return;\r\n        }\r\n\r\n        fitVideoIntoDialog();\r\n\r\n        float lastSpeed = getPlayerData().getSpeed(getVideo().channelId);\r\n        if (Helpers.floatEquals(lastSpeed, 1.0f)) {\r\n            lastSpeed = getPlayerData().getLastSpeed();\r\n        }\r\n        State state = getStateService().getByVideoId(getVideo() != null ? getVideo().videoId : null);\r\n        if (state != null && getPlayerData().isSpeedPerVideoEnabled()) {\r\n            lastSpeed = !Helpers.floatEquals(1.0f, state.speed) ? state.speed : lastSpeed;\r\n            getStateService().save(new State(state.video, state.positionMs, state.durationMs, enabled ? 1.0f : lastSpeed));\r\n        }\r\n\r\n        if (Helpers.floatEquals(lastSpeed, 1.0f) || getPlayerTweaksData().isSpeedButtonOldBehaviorEnabled()) {\r\n            onSpeedLongClicked(enabled);\r\n        } else {\r\n            getPlayer().setSpeed(enabled ? 1.0f : lastSpeed);\r\n        }\r\n    }\r\n\r\n    private void onSpeedLongClicked(boolean enabled) {\r\n        fitVideoIntoDialog();\r\n\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        // suppose live stream if buffering near the end\r\n        // boolean isStream = Math.abs(player.getDuration() - player.getCurrentPosition()) < 10_000;\r\n        settingsPresenter.appendCategory(AppDialogUtil.createSpeedListCategory(getContext(), getPlayer()));\r\n\r\n        settingsPresenter.showDialog(getContext().getString(R.string.video_speed), () -> {\r\n            State state = getStateService().getByVideoId(getVideo() != null ? getVideo().videoId : null);\r\n            if (state != null && getPlayerData().isSpeedPerVideoEnabled()) {\r\n                getStateService().save(new State(state.video, state.positionMs, state.durationMs, getPlayerData().getSpeed(getVideo().channelId)));\r\n            }\r\n        });\r\n    }\r\n\r\n    @Override\r\n    public void onFinish() {\r\n        mIncognito = false;\r\n    }\r\n\r\n    private void clearStateOfNextVideo() {\r\n        if (getVideo() != null && getVideo().nextMediaItem != null) {\r\n            resetPosition(Video.from(getVideo().nextMediaItem));\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Reset position of currently opened music and live videos.\r\n     */\r\n    private void resetPositionIfNeeded(Video item) {\r\n        if (getStateService() == null || item == null) {\r\n            return;\r\n        }\r\n\r\n        State state = getStateService().getByVideoId(item.videoId);\r\n\r\n        // Reset position of music videos\r\n        boolean isShort = state != null && state.durationMs < MUSIC_VIDEO_MAX_DURATION_MS\r\n                && !getPlayerTweaksData().isRememberPositionOfShortVideosEnabled();\r\n        boolean isVideoEnded = state != null && state.durationMs - state.positionMs < 3_000;\r\n        boolean isLive = item.isLive;\r\n\r\n        if (getPlayerTweaksData().isRememberPositionOfLiveVideosEnabled() && item.isFullLive()) {\r\n            isLive = false;\r\n        }\r\n\r\n        // Don't reset if doing switch from the embed to the fullscreen one\r\n        boolean sameVideo = item.equals(getVideo());\r\n        if (sameVideo) {\r\n            isShort = false;\r\n        }\r\n\r\n        if (isShort || isVideoEnded || isLive) {\r\n            resetPosition(item);\r\n        }\r\n    }\r\n\r\n    private void resetGlobalSpeedIfNeeded() {\r\n        if (!getPlayerData().isAllSpeedEnabled()) {\r\n            getPlayerData().setSpeed(1.0f);\r\n        }\r\n    }\r\n\r\n    private void resetPosition(Video video) {\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        video.markNotViewed();\r\n        State state = getStateService().getByVideoId(video.videoId);\r\n\r\n        if (state != null) {\r\n            if (getPlayerData().isSpeedPerVideoEnabled()) {\r\n                getStateService().save(new State(video, 0, state.durationMs, state.speed));\r\n            } else {\r\n                getStateService().removeByVideoId(video.videoId);\r\n            }\r\n        }\r\n    }\r\n\r\n    private void enableIncognitoIfNeeded(Video item) {\r\n        if (item == null) {\r\n            return;\r\n        }\r\n\r\n        // Enable incognito per session\r\n        // Reset to default when player finished\r\n        if (item.incognito) {\r\n            mIncognito = true;\r\n            item.incognito = false;\r\n        }\r\n    }\r\n\r\n    private void restoreVideoFormat() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        if (getPlayerData().getTempVideoFormat() != null) {\r\n            getPlayer().setFormat(getPlayerData().getTempVideoFormat());\r\n        } else {\r\n            getPlayer().setFormat(getPlayerData().getFormat(FormatItem.TYPE_VIDEO));\r\n        }\r\n    }\r\n\r\n    private void restoreAudioFormat() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        getPlayer().setFormat(getPlayerData().getFormat(FormatItem.TYPE_AUDIO));\r\n    }\r\n\r\n    private void restoreSubtitleFormat() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        FormatItem result = getPlayerData().getFormat(FormatItem.TYPE_SUBTITLE);\r\n\r\n        if (getPlayerData().isSubtitlesPerChannelEnabled()) {\r\n            result = getPlayerData().isSubtitlesPerChannelEnabled(getPlayer().getVideo().channelId)\r\n                    ? getPlayerData().getLastSubtitleFormat() : FormatItem.SUBTITLE_NONE;\r\n        }\r\n\r\n        getPlayer().setFormat(result);\r\n    }\r\n\r\n    private void saveState() {\r\n        // Skip mini player, but don't save for the previews (mute enabled)\r\n        if (isMutedEmbed()) {\r\n            return;\r\n        }\r\n\r\n        savePosition();\r\n\r\n        if (!isBeginEmbed()) {\r\n            updateHistory();\r\n            syncWithPlaylists();\r\n        }\r\n    }\r\n\r\n    private void restoreState() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        restorePosition();\r\n        restorePendingPosition();\r\n        // Player thinks that subs not enabled if I enable it too early (e.g. on source change event).\r\n        //restoreSubtitleFormat();\r\n\r\n        restoreVolume();\r\n        restorePitch();\r\n    }\r\n\r\n    //private void persistState() {\r\n    //    // Skip mini player, but don't save for the previews (mute enabled)\r\n    //    if (isMutedEmbed() || isBeginEmbed()) {\r\n    //        return;\r\n    //    }\r\n    //\r\n    //    getStateService().persistState();\r\n    //}\r\n\r\n    private void savePosition() {\r\n        Video video = getVideo();\r\n\r\n        if (video == null || getPlayer() == null || !getPlayer().containsMedia()) {\r\n            return;\r\n        }\r\n\r\n        // Exceptional cases:\r\n        // 1) Track is ended\r\n        // 2) Pause on end enabled\r\n        // 3) Watching live stream in real time\r\n        long durationMs = getPlayer().getDurationMs();\r\n        long positionMs = getPlayer().getPositionMs();\r\n        long remainsMs = durationMs - positionMs;\r\n        boolean isPositionActual = remainsMs > 1_000;\r\n        boolean isLiveBroken = video.isLive && durationMs <= 30_000; // the live without a history\r\n        if (isPositionActual && !isLiveBroken) { // partially viewed\r\n            State state = new State(video, positionMs, durationMs, getPlayer().getSpeed());\r\n            getStateService().save(state);\r\n            // Sync video. You could safely use it later to restore state.\r\n            video.sync(state);\r\n        } else { // fully viewed\r\n            // Mark video as fully viewed. This could help to restore proper progress marker on the video card later.\r\n            getStateService().save(new State(video, durationMs, durationMs, getPlayer().getSpeed()));\r\n            video.markFullyViewed();\r\n        }\r\n\r\n        Playlist.instance().sync(video);\r\n    }\r\n\r\n    private void restorePosition() {\r\n        Video item = getVideo();\r\n\r\n        if (getPlayer() == null || item == null) {\r\n            return;\r\n        }\r\n\r\n        State state = getStateService().getByVideoId(item.videoId);\r\n\r\n        boolean stateIsOutdated = isStateOutdated(state, item);\r\n        if (stateIsOutdated) { // check that the user logged in\r\n            // Web state is buggy on short videos (e.g. video clips)\r\n            boolean isLongVideo = getPlayer().getDurationMs() > MUSIC_VIDEO_MAX_DURATION_MS;\r\n            if (isLongVideo) {\r\n                state = new State(item, item.getPositionMs());\r\n            }\r\n        }\r\n\r\n        // Set actual position for live videos with uncommon length\r\n        if (item.isLive && (state == null || state.durationMs - state.positionMs < Math.max(RESTORE_LIVE_BUFFER_MS, getLiveThreshold()))) {\r\n            // Add buffer. Should I take into account segment offset???\r\n            state = new State(item, getPlayer().getDurationMs() - getLiveBuffer());\r\n        }\r\n\r\n        // Do I need to check that item isn't live? (state != null && !item.isLive)\r\n        if (state != null) {\r\n            getPlayer().setPositionMs(state.positionMs);\r\n        }\r\n\r\n        if (!mIsPlayBlocked) {\r\n            getPlayer().setPlayWhenReady(getPlayEnabled());\r\n        }\r\n    }\r\n\r\n    private void updateHistory() {\r\n        Video video = getVideo();\r\n\r\n        if (video == null || mIncognito || getPlayer() == null || !getPlayer().containsMedia()\r\n                || (video.isRemote && getRemoteControlData().isRemoteHistoryDisabled())\r\n                || getGeneralData().getHistoryState() == GeneralData.HISTORY_DISABLED) {\r\n            return;\r\n        }\r\n\r\n        MediaServiceManager.instance().updateHistory(video, Math.max(getPlayer().getPositionMs(), 3_000)); // 0 == fully watched\r\n    }\r\n\r\n    /**\r\n     * Restore position from description time code\r\n     */\r\n    private void restorePendingPosition() {\r\n        if (getPlayer() == null || getVideo() == null) {\r\n            return;\r\n        }\r\n\r\n        Video item = getVideo();\r\n\r\n        if (item.pendingPosMs > 0) {\r\n            getPlayer().setPositionMs(item.pendingPosMs);\r\n            item.pendingPosMs = 0;\r\n        }\r\n    }\r\n\r\n    private void restoreSpeedAndPositionIfNeeded() {\r\n        if (getPlayer() == null || getVideo() == null) {\r\n            return;\r\n        }\r\n\r\n        Video item = getVideo();\r\n\r\n        boolean liveEnd = isLiveEnd();\r\n\r\n        // Position\r\n        if (liveEnd) {\r\n            getPlayer().setPositionMs(getPlayer().getDurationMs() - getLiveBuffer());\r\n        }\r\n\r\n        // Speed\r\n        if (liveEnd || isMusicVideo()) {\r\n            getPlayer().setSpeed(1.0f);\r\n        } else {\r\n            State state = getStateService().getByVideoId(item.videoId);\r\n            float speed = getPlayerData().getSpeed(item.channelId);\r\n            getPlayer().setSpeed(\r\n                    state != null && getPlayerData().isSpeedPerVideoEnabled() ? state.speed :\r\n                            getPlayerData().isAllSpeedEnabled() || item.channelId != null ? speed : 1.0f\r\n            );\r\n        }\r\n    }\r\n\r\n    public void blockPlay(boolean block) {\r\n        mIsPlayBlocked = block;\r\n    }\r\n\r\n    public void setPlayEnabled(boolean isPlayEnabled) {\r\n        Log.d(TAG, \"setPlayEnabled %s\", isPlayEnabled);\r\n        mIsPlayEnabled = isPlayEnabled;\r\n    }\r\n\r\n    public boolean getPlayEnabled() {\r\n        return mIsPlayEnabled;\r\n    }\r\n\r\n    private void restoreVolume() {\r\n        if (getVideo() == null || getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        float newVolume = getPlayerData().getPlayerVolume();\r\n\r\n        if (getPlayerTweaksData().isPlayerAutoVolumeEnabled()) {\r\n            //newVolume *= getVideo().volume;\r\n            newVolume = getVideo().volume;\r\n        }\r\n\r\n        if (getVideo().isShorts) {\r\n            newVolume /= 2;\r\n        }\r\n\r\n        getPlayer().setVolume(newVolume);\r\n    }\r\n\r\n    private void restorePitch() {\r\n        if (getPlayer() == null) {\r\n            return;\r\n        }\r\n\r\n        getPlayer().setPitch(getPlayerData().getPitch());\r\n    }\r\n\r\n    private void restoreFormats() {\r\n        restoreVideoFormat();\r\n        restoreAudioFormat();\r\n        // We don't know yet do we really need a subs.\r\n        // NOTE: Some subs can hang the app.\r\n        restoreSubtitleFormat();\r\n    }\r\n\r\n    private void showHideScreensaver(boolean show) {\r\n        ScreensaverManager screensaverManager = getScreensaverManager();\r\n\r\n        if (screensaverManager == null) {\r\n            return;\r\n        }\r\n\r\n        if (show) {\r\n            screensaverManager.enableChecked();\r\n        } else {\r\n            screensaverManager.disableChecked();\r\n        }\r\n    }\r\n\r\n    private boolean isMusicVideo() {\r\n        Video item = getVideo();\r\n        return item.belongsToMusic();\r\n    }\r\n\r\n    //private void setPositionMs(long positionMs) {\r\n    //    boolean samePositions = Math.abs(positionMs - getPlayer().getPositionMs()) < BEGIN_THRESHOLD_MS;\r\n    //    if (!samePositions) {\r\n    //        getPlayer().setPositionMs(positionMs);\r\n    //    }\r\n    //}\r\n\r\n    private boolean isStateOutdated(State state, Video item) {\r\n        if (state == null) {\r\n            return true;\r\n        }\r\n\r\n        // Web live position is broken. Ignore it.\r\n        if (item.isLive || item.getPositionMs() <= 0) {\r\n            return false;\r\n        }\r\n\r\n        float posPercents1 = state.positionMs * 100f / state.durationMs;\r\n        float posPercents2 = item.getPositionMs() * 100f / item.getDurationMs();\r\n\r\n        return (posPercents2 != 0 && Math.abs(posPercents1 - posPercents2) > 3) && state.timestamp < item.timestamp;\r\n    }\r\n\r\n    private void syncWithPlaylists() {\r\n        Video video = getVideo();\r\n\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        if (getMediaServiceData().isContentHidden(MediaServiceData.CONTENT_WATCHED_WATCH_LATER) && video.percentWatched > 95) { // remove fully watched\r\n            MediaServiceManager.instance().removeFromWatchLaterPlaylist(video);\r\n        }\r\n\r\n        if (getGeneralData().isHideWatchedFromNotificationsEnabled()) { // remove any watched length\r\n            MediaServiceManager.instance().hideNotification(video);\r\n        }\r\n    }\r\n\r\n    private boolean isLiveEnd() {\r\n        if (getPlayer() == null || getVideo() == null || !getVideo().isLive) {\r\n            return false;\r\n        }\r\n\r\n        return getPlayer().getDurationMs() - getPlayer().getPositionMs() <= 1_000;\r\n    }\r\n\r\n    private long getLiveThreshold() {\r\n        return getLiveBuffer() + 5_000;\r\n    }\r\n\r\n    private long getLiveBuffer() {\r\n        return getPlayerTweaksData().isBufferOnStreamsDisabled() ? SHORT_LIVE_BUFFER_MS : LIVE_BUFFER_MS;\r\n    }\r\n\r\n    private boolean isMutedEmbed() {\r\n        return isEmbedPlayer() && getPlayer() != null && Helpers.floatEquals(getPlayer().getVolume(), 0);\r\n    }\r\n\r\n    private boolean isBeginEmbed() {\r\n        return isEmbedPlayer() && System.currentTimeMillis() - mNewVideoTimeMs <= EMBED_THRESHOLD_MS &&\r\n                getPlayer() != null && getPlayer().getPositionMs() < getPlayer().getDurationMs();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/listener/PlayerEngineEventListener.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.listener;\r\n\r\nimport com.google.android.exoplayer2.ExoPlaybackException;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.TrackSelectorManager;\r\n\r\npublic interface PlayerEngineEventListener {\r\n    /**\r\n     * The error occurred loading data\r\n     */\r\n    int ERROR_TYPE_SOURCE = ExoPlaybackException.TYPE_SOURCE;\r\n    /**\r\n     * The error occurred in a renderer.\r\n     */\r\n    int ERROR_TYPE_RENDERER = ExoPlaybackException.TYPE_RENDERER;\r\n    /**\r\n     * The error was an unexpected\r\n     */\r\n    int ERROR_TYPE_UNEXPECTED = ExoPlaybackException.TYPE_UNEXPECTED;\r\n    /**\r\n     * The error occurred in a remote component.\r\n     */\r\n    int ERROR_TYPE_REMOTE = ExoPlaybackException.TYPE_REMOTE;\r\n    /**\r\n     * The error was an {@link OutOfMemoryError}.\r\n     */\r\n    int ERROR_TYPE_OUT_OF_MEMORY = ExoPlaybackException.TYPE_OUT_OF_MEMORY;\r\n    int RENDERER_INDEX_UNKNOWN = TrackSelectorManager.RENDERER_INDEX_UNKNOWN;\r\n    int RENDERER_INDEX_VIDEO = TrackSelectorManager.RENDERER_INDEX_VIDEO;\r\n    int RENDERER_INDEX_AUDIO = TrackSelectorManager.RENDERER_INDEX_AUDIO;\r\n    int RENDERER_INDEX_SUBTITLE = TrackSelectorManager.RENDERER_INDEX_SUBTITLE;\r\n    void onPlay();\r\n    void onPause();\r\n    void onPlayEnd();\r\n    void onBuffering();\r\n    void onSeekEnd();\r\n    void onSeekPositionChanged(long positionMs);\r\n    void onSpeedChanged(float speed);\r\n    void onSourceChanged(Video item);\r\n    void onVideoLoaded(Video item);\r\n    void onEngineInitialized();\r\n    void onEngineReleased();\r\n    void onEngineError(int type, int rendererIndex, Throwable error);\r\n    void onTrackChanged(FormatItem track);\r\n    void onTrackSelected(FormatItem track);\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/listener/PlayerEventListener.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.listener;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.TickleManager.TickleListener;\r\n\r\npublic interface PlayerEventListener extends PlayerUiEventListener, PlayerEngineEventListener, ViewEventListener, TickleListener {\r\n    void onNewVideo(Video item);\r\n    void onMetadata(MediaItemMetadata metadata);\r\n    /**\r\n     * Called after creation of {@link PlayerManager}\r\n     */\r\n    void onInit();\r\n    void onFinish();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/listener/PlayerUiEventListener.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.listener;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\n\r\npublic interface PlayerUiEventListener {\r\n    void onSuggestionItemClicked(Video item);\r\n    void onSuggestionItemLongClicked(Video item);\r\n    void onScrollEnd(Video item);\r\n    boolean onPreviousClicked();\r\n    boolean onNextClicked();\r\n    void onPlayClicked();\r\n    void onPauseClicked();\r\n    boolean onKeyDown(int keyCode);\r\n    void onButtonClicked(int buttonId, int buttonState);\r\n    void onButtonLongClicked(int buttonId, int buttonState);\r\n    void onControlsShown(boolean shown);\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/listener/ViewEventListener.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.listener;\r\n\r\npublic interface ViewEventListener {\r\n    void onViewCreated();\r\n    void onViewDestroyed();\r\n    void onViewPaused();\r\n    void onViewResumed();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/manager/PlayerConstants.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager;\r\n\r\nimport com.google.android.exoplayer2.ui.AspectRatioFrameLayout;\r\n\r\npublic interface PlayerConstants {\r\n    int PLAYBACK_MODE_PAUSE = 0;\r\n    int PLAYBACK_MODE_CLOSE = 1;\r\n    int PLAYBACK_MODE_ALL = 2;\r\n    int PLAYBACK_MODE_ONE = 3;\r\n    int PLAYBACK_MODE_SHUFFLE = 4;\r\n    int PLAYBACK_MODE_LIST = 5;\r\n    int PLAYBACK_MODE_REVERSE_LIST = 6;\r\n    int BACKGROUND_MODE_DEFAULT = 0;\r\n    int BACKGROUND_MODE_SOUND = 1;\r\n    int BACKGROUND_MODE_PIP = 2;\r\n    int BACKGROUND_MODE_PLAY_BEHIND = 3;\r\n    int BUFFER_LOW = 3;\r\n    int BUFFER_MEDIUM = 0;\r\n    int BUFFER_HIGH = 1;\r\n    int BUFFER_HIGHEST = 2;\r\n    int RESIZE_MODE_DEFAULT = AspectRatioFrameLayout.RESIZE_MODE_FIT;\r\n    int RESIZE_MODE_FIT_WIDTH = AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH;\r\n    int RESIZE_MODE_FIT_HEIGHT = AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT;\r\n    int RESIZE_MODE_FIT_BOTH = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;\r\n    int RESIZE_MODE_STRETCH = AspectRatioFrameLayout.RESIZE_MODE_FILL;\r\n    float ASPECT_RATIO_DEFAULT = 0;\r\n    float ASPECT_RATIO_1_1 = 1f;\r\n    float ASPECT_RATIO_4_3 = 1.33f;\r\n    float ASPECT_RATIO_5_4 = 1.25f;\r\n    float ASPECT_RATIO_16_9 = 1.77f;\r\n    float ASPECT_RATIO_16_10 = 1.6f;\r\n    float ASPECT_RATIO_21_9 = 2.33f;\r\n    float ASPECT_RATIO_64_27 = 2.37f;\r\n    float ASPECT_RATIO_221_1 = 2.21f;\r\n    float ASPECT_RATIO_235_1 = 2.35f;\r\n    float ASPECT_RATIO_239_1 = 2.39f;\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/manager/PlayerEngine.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemFormatInfo;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\n\r\nimport java.io.InputStream;\r\nimport java.util.List;\r\n\r\npublic interface PlayerEngine extends PlayerConstants {\r\n    void openSabr(MediaItemFormatInfo formatInfo);\r\n    void openDash(MediaItemFormatInfo formatInfo);\r\n    void openDash(InputStream dashManifest);\r\n    void openDashUrl(String dashManifestUrl);\r\n    void openHlsUrl(String hlsPlaylistUrl);\r\n    void openUrlList(List<String> urlList);\r\n    void openMerged(MediaItemFormatInfo formatInfo, String hlsPlaylistUrl);\r\n    void openMerged(InputStream dashManifest, String hlsPlaylistUrl);\r\n    long getPositionMs();\r\n    void setPositionMs(long positionMs);\r\n    long getDurationMs();\r\n    void setPlayWhenReady(boolean play);\r\n    boolean getPlayWhenReady();\r\n    boolean isPlaying();\r\n    boolean isLoading();\r\n    List<FormatItem> getVideoFormats();\r\n    List<FormatItem> getAudioFormats();\r\n    List<FormatItem> getSubtitleFormats();\r\n    void setFormat(FormatItem option);\r\n    FormatItem getVideoFormat();\r\n    FormatItem getAudioFormat();\r\n    FormatItem getSubtitleFormat();\r\n    boolean isEngineInitialized();\r\n    void restartEngine();\r\n    void reloadPlayback();\r\n    void blockEngine(boolean block);\r\n    boolean isEngineBlocked();\r\n    boolean isInPIPMode();\r\n    boolean containsMedia();\r\n    void setSpeed(float speed);\r\n    float getSpeed();\r\n    void setPitch(float pitch);\r\n    float getPitch();\r\n    void setVolume(float volume);\r\n    float getVolume();\r\n    void setResizeMode(int mode);\r\n    int getResizeMode();\r\n    void setZoomPercents(int percents);\r\n    void setAspectRatio(float ratio);\r\n    void setRotationAngle(int angle);\r\n    void setVideoFlipEnabled(boolean enabled);\r\n    void setVideoGravity(int gravity);\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/manager/PlayerManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\n\r\n// is paused, position, tracks (audio, video, subs), codecs, aspect, speed\r\n// title, subtitle (description), subscribed/liked nums, published date, toggle buttons, simple buttons\r\npublic interface PlayerManager extends PlayerEngine, PlayerUI {\r\n    void setVideo(Video item);\r\n    Video getVideo();\r\n    void finish();\r\n    void finishReally();\r\n    void showBackground(String url);\r\n    void showBackgroundColor(int colorResId);\r\n    void resetPlayerState();\r\n    boolean isEmbed();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/manager/PlayerUI.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.SeekBarSegment;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.ChatReceiver;\r\n\r\nimport java.util.List;\r\n\r\npublic interface PlayerUI {\r\n    int BUTTON_OFF = 0;\r\n    int BUTTON_ON = 1;\r\n    int BUTTON_DISABLED = -1;\r\n    void updateSuggestions(VideoGroup group);\r\n    void removeSuggestions(VideoGroup group);\r\n    int getSuggestionsIndex(VideoGroup group);\r\n    VideoGroup getSuggestionsByIndex(int index);\r\n    void focusSuggestedItem(int index);\r\n    void focusSuggestedItem(Video video);\r\n    void resetSuggestedPosition();\r\n    boolean isSuggestionsEmpty();\r\n    void clearSuggestions();\r\n    void showOverlay(boolean show);\r\n    boolean isOverlayShown();\r\n    void showSuggestions(boolean show);\r\n    boolean isSuggestionsShown();\r\n    void showControls(boolean show);\r\n    boolean isControlsShown();\r\n    int getButtonState(int buttonId);\r\n    void setButtonState(int buttonId, int buttonState);\r\n    void setChannelIcon(String iconUrl);\r\n    void setSeekPreviewTitle(String title);\r\n    void setNextTitle(Video nextVideo);\r\n    void showDebugInfo(boolean show);\r\n    void showSubtitles(boolean show);\r\n    void loadStoryboard();\r\n    void setTitle(String title);\r\n    void showProgressBar(boolean show);\r\n    void setSeekBarSegments(List<SeekBarSegment> segments);\r\n    void updateEndingTime();\r\n    void setChatReceiver(ChatReceiver chatReceiver);\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/service/VideoStateService.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.service;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport androidx.annotation.NonNull;\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs.ProfileChangeListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.List;\r\n\r\npublic class VideoStateService implements ProfileChangeListener {\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static VideoStateService sInstance;\r\n    private static final int LOW_RAM_STATE_MAX_SIZE = 50;\r\n    private static final int HIGH_RAM_STATE_MAX_SIZE = 300;\r\n    private static final long PERSIST_DELAY_MS = 10_000;\r\n    // Don't store state inside Video object.\r\n    // As one video might correspond to multiple Video objects.\r\n    //private final Map<String, State> mStates = Helpers.createLRUMap(MAX_PERSISTENT_STATE_SIZE);\r\n    private final List<State> mStates;\r\n    private final AppPrefs mPrefs;\r\n    private static final String DELIM = \"&si;\";\r\n    private boolean mIsHistoryBroken;\r\n    private final Runnable mPersistStateInt = this::persistStateInt;\r\n    private long mSessionStartTimeMs;\r\n\r\n    private VideoStateService(Context context) {\r\n        mPrefs = AppPrefs.instance(context);\r\n        mPrefs.addListener(this);\r\n        mStates = Helpers.createSafeLRUList(\r\n                Utils.isEnoughRam() ? HIGH_RAM_STATE_MAX_SIZE : LOW_RAM_STATE_MAX_SIZE);\r\n        restoreState();\r\n    }\r\n\r\n    public static VideoStateService instance(Context context) {\r\n        if (sInstance == null && context != null) {\r\n            sInstance = new VideoStateService(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public List<State> getStates() {\r\n        return mStates;\r\n    }\r\n\r\n    public @Nullable State getLastState() {\r\n        if (isEmpty()) {\r\n            return null;\r\n        }\r\n\r\n        return mStates.get(mStates.size() - 1);\r\n    }\r\n\r\n    public State getByVideoId(String videoId) {\r\n        for (State state : mStates) {\r\n            if (Helpers.equals(videoId, state.video.videoId)) {\r\n                return state;\r\n            }\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    public void removeByVideoId(String videoId) {\r\n        Helpers.removeIf(mStates, state -> Helpers.equals(state.video.videoId, videoId));\r\n        persistState();\r\n    }\r\n\r\n    public boolean isEmpty() {\r\n        return mStates.isEmpty();\r\n    }\r\n\r\n    public void save(State state) {\r\n        mStates.add(state);\r\n        persistState();\r\n    }\r\n\r\n    public void clear() {\r\n        mStates.clear();\r\n        persistState();\r\n    }\r\n\r\n    public void setHistoryBroken(boolean isBroken) {\r\n        mIsHistoryBroken = isBroken;\r\n    }\r\n\r\n    public boolean isHistoryBroken() {\r\n        return mIsHistoryBroken;\r\n    }\r\n\r\n    public long getSessionStartTimeMs() {\r\n        return mSessionStartTimeMs;\r\n    }\r\n\r\n    private void restoreState() {\r\n        mStates.clear();\r\n        String data = mPrefs.getStateUpdaterData();\r\n\r\n        String[] split = Helpers.splitData(data);\r\n\r\n        setStateData(Helpers.parseStr(split, 0));\r\n        mIsHistoryBroken = Helpers.parseBoolean(split, 1);\r\n        mSessionStartTimeMs = System.currentTimeMillis();\r\n    }\r\n\r\n    private void persistStateInt() {\r\n        if (mIsHistoryBroken) {\r\n            mPrefs.setStateUpdaterData(Helpers.mergeData(getStateData(), mIsHistoryBroken));\r\n        } else {\r\n            // Eliminate additional string creation with the merge\r\n            mPrefs.setStateUpdaterData(getStateData());\r\n        }\r\n    }\r\n\r\n    public void persistNow() {\r\n        Utils.post(mPersistStateInt);\r\n    }\r\n\r\n    private void persistState() {\r\n        // Improve memory and disc usage\r\n        Utils.postDelayed(mPersistStateInt, PERSIST_DELAY_MS);\r\n    }\r\n\r\n    public static class State {\r\n        private static final String DELIM = \"&sf;\";\r\n        public final Video video;\r\n        public final long positionMs;\r\n        public final long durationMs;\r\n        public final float speed;\r\n        public final long timestamp = System.currentTimeMillis();\r\n\r\n        public State(Video video, long positionMs) {\r\n            this(video, positionMs, -1);\r\n        }\r\n\r\n        public State(Video video, long positionMs, long durationMs) {\r\n            this(video, positionMs, durationMs, 1.0f);\r\n        }\r\n\r\n        public State(Video video, long positionMs, long durationMs, float speed) {\r\n            this.video = video;\r\n            this.positionMs = positionMs;\r\n            this.durationMs = durationMs;\r\n            this.speed = speed;\r\n        }\r\n\r\n        public static State from(String spec) {\r\n            if (spec == null) {\r\n                return null;\r\n            }\r\n\r\n            String[] split = Helpers.split(spec, DELIM);\r\n\r\n            String videoId = Helpers.parseStr(split, 0);\r\n            long positionMs = Helpers.parseLong(split, 1);\r\n            long lengthMs = Helpers.parseLong(split, 2);\r\n            float speed = Helpers.parseFloat(split, 3);\r\n\r\n            Video video = Video.fromString(videoId);\r\n\r\n            // backward compatibility\r\n            if (video == null) {\r\n                video = new Video();\r\n                video.videoId = videoId;\r\n            }\r\n\r\n            video.percentWatched = (positionMs * 100f) / lengthMs;\r\n\r\n            return new State(video, positionMs, lengthMs, speed);\r\n        }\r\n\r\n        @NonNull\r\n        @Override\r\n        public String toString() {\r\n            return Helpers.merge(DELIM, video, positionMs, durationMs, speed);\r\n        }\r\n\r\n        @Override\r\n        public boolean equals(@Nullable Object obj) {\r\n            if (obj instanceof State) {\r\n                return Helpers.equals(video, ((State) obj).video);\r\n            }\r\n\r\n            return false;\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onProfileChanged() {\r\n        restoreState();\r\n    }\r\n\r\n    private void setStateDataSafe(String data) {\r\n        try {\r\n            setStateData(data);\r\n        } catch (ArrayIndexOutOfBoundsException e) { // weird issue (NVidia Shield)\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n\r\n    private void setStateData(String data) {\r\n        if (data != null) {\r\n            String[] split = Helpers.split(data, DELIM);\r\n\r\n            for (String spec : split) {\r\n                State state = State.from(spec);\r\n\r\n                if (state != null) {\r\n                    mStates.add(state);\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    private String getStateData() {\r\n        StringBuilder sb = new StringBuilder();\r\n\r\n        for (State state : mStates) {\r\n            if (sb.length() != 0) {\r\n                sb.append(DELIM);\r\n            }\r\n\r\n            sb.append(state);\r\n        }\r\n\r\n        return sb.toString();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/ui/AbstractCommentsReceiver.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.CommentGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.CommentItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\n\r\npublic abstract class AbstractCommentsReceiver implements CommentsReceiver {\r\n    private final Context mContext;\r\n    private Callback mCallback;\r\n\r\n    public AbstractCommentsReceiver(Context context) {\r\n        mContext = context;\r\n    }\r\n\r\n    @Override\r\n    public void addCommentGroup(CommentGroup commentGroup) {\r\n        if (mCallback != null) {\r\n            mCallback.onCommentGroup(commentGroup);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void loadBackup(Backup backup) {\r\n        if (mCallback != null) {\r\n            mCallback.onBackup(backup);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void sync(CommentItem commentItem) {\r\n        if (mCallback != null) {\r\n            mCallback.onSync(commentItem);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void setCallback(Callback callback) {\r\n        mCallback = callback;\r\n    }\r\n\r\n    @Override\r\n    public void onLoadMore(CommentGroup commentGroup) {\r\n\r\n    }\r\n\r\n    @Override\r\n    public void onCommentClicked(CommentItem commentItem) {\r\n\r\n    }\r\n\r\n    @Override\r\n    public void onCommentLongClicked(CommentItem commentItem) {\r\n        \r\n    }\r\n\r\n    @Override\r\n    public void onFinish(Backup backup) {\r\n        \r\n    }\r\n\r\n    @Override\r\n    public String getLoadingMessage() {\r\n        if (mContext == null) {\r\n            return null;\r\n        }\r\n\r\n        return mContext.getString(R.string.loading);\r\n    }\r\n\r\n    @Override\r\n    public String getErrorMessage() {\r\n        return mContext.getString(R.string.section_is_empty);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/ui/ChatReceiver.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.ChatItem;\r\n\r\npublic interface ChatReceiver {\r\n    interface Callback {\r\n        void onChatItem(ChatItem chatItem);\r\n    }\r\n    void addChatItem(ChatItem chatItem);\r\n    void setCallback(Callback callback);\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/ui/ChatReceiverImpl.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.ChatItem;\r\n\r\npublic class ChatReceiverImpl implements ChatReceiver {\r\n    private Callback mCallback;\r\n\r\n    @Override\r\n    public void addChatItem(ChatItem chatItem) {\r\n        if (mCallback != null) {\r\n            mCallback.onChatItem(chatItem);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void setCallback(Callback callback) {\r\n        mCallback = callback;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/ui/CommentsReceiver.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.CommentGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.CommentItem;\r\n\r\npublic interface CommentsReceiver {\r\n    interface Callback {\r\n        void onCommentGroup(CommentGroup commentGroup);\r\n        void onBackup(Backup backup);\r\n        void onSync(CommentItem commentItem);\r\n    }\r\n    interface Backup {}\r\n    void addCommentGroup(CommentGroup commentGroup);\r\n    void loadBackup(Backup backup);\r\n    void sync(CommentItem commentItem);\r\n    void setCallback(Callback callback);\r\n    void onLoadMore(CommentGroup commentGroup);\r\n    void onStart();\r\n    void onCommentClicked(CommentItem commentItem);\r\n    void onCommentLongClicked(CommentItem commentItem);\r\n    void onFinish(Backup backup);\r\n    String getLoadingMessage();\r\n    String getErrorMessage();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/ui/OptionCallback.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui;\r\n\r\npublic interface OptionCallback {\r\n    void onSelect(OptionItem optionItem);\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/ui/OptionCategory.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Collections;\r\nimport java.util.List;\r\n\r\npublic class OptionCategory {\r\n    public static OptionCategory radioList(String title, List<OptionItem> items) {\r\n        return new OptionCategory(title, items, TYPE_RADIO_LIST);\r\n    }\r\n\r\n    public static OptionCategory checkedList(String title, List<OptionItem> items) {\r\n        return new OptionCategory(title, items, TYPE_CHECKBOX_LIST);\r\n    }\r\n\r\n    public static OptionCategory stringList(String title, List<OptionItem> items) {\r\n        return new OptionCategory(title, items, TYPE_STRING_LIST);\r\n    }\r\n\r\n    public static OptionCategory longText(String title, OptionItem item) {\r\n        return new OptionCategory(title, Collections.singletonList(item), TYPE_LONG_TEXT);\r\n    }\r\n\r\n    public static OptionCategory chat(String title, OptionItem item) {\r\n        return new OptionCategory(title, Collections.singletonList(item), TYPE_CHAT);\r\n    }\r\n\r\n    public static OptionCategory comments(String title, OptionItem item) {\r\n        return new OptionCategory(title, Collections.singletonList(item), TYPE_COMMENTS);\r\n    }\r\n\r\n    public static OptionCategory singleSwitch(OptionItem item) {\r\n        ArrayList<OptionItem> items = new ArrayList<>();\r\n        items.add(item);\r\n        return new OptionCategory(null, items, TYPE_SINGLE_SWITCH);\r\n    }\r\n\r\n    public static OptionCategory singleButton(OptionItem item) {\r\n        ArrayList<OptionItem> items = new ArrayList<>();\r\n        items.add(item);\r\n        return new OptionCategory(null, items, TYPE_SINGLE_BUTTON);\r\n    }\r\n\r\n    public static OptionCategory from(int id, int type, String title, List<OptionItem> options) {\r\n        return new OptionCategory(title, options, type, id);\r\n    }\r\n\r\n    public static OptionCategory from(int id, int type, String title, OptionItem option) {\r\n        return new OptionCategory(title, Collections.singletonList(option), type, id);\r\n    }\r\n\r\n    private OptionCategory(String title, List<OptionItem> options, int type) {\r\n        this(title, options, type, -1);\r\n    }\r\n\r\n    private OptionCategory(String title, List<OptionItem> options, int type, int id) {\r\n        this.id = id;\r\n        this.type = type;\r\n        this.title = title;\r\n        this.options = options;\r\n    }\r\n\r\n    public static final int TYPE_RADIO_LIST = 0;\r\n    public static final int TYPE_CHECKBOX_LIST = 1;\r\n    public static final int TYPE_SINGLE_SWITCH = 2;\r\n    public static final int TYPE_SINGLE_BUTTON = 3;\r\n    public static final int TYPE_STRING_LIST = 4;\r\n    public static final int TYPE_LONG_TEXT = 5;\r\n    public static final int TYPE_CHAT = 6;\r\n    public static final int TYPE_COMMENTS = 7;\r\n    public final int id;\r\n    public final int type;\r\n    public final String title;\r\n    public final List<OptionItem> options;\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/ui/OptionItem.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui;\r\n\r\npublic interface OptionItem {\r\n    int getId();\r\n    CharSequence getTitle();\r\n    CharSequence getDescription();\r\n    boolean isSelected();\r\n    void onSelect(boolean isSelected);\r\n    Object getData();\r\n    void setRequired(OptionItem... items);\r\n    OptionItem[] getRequired();\r\n    void setRadio(OptionItem... items);\r\n    OptionItem[] getRadio();\r\n    ChatReceiver getChatReceiver();\r\n    CommentsReceiver getCommentsReceiver();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/ui/SeekBarSegment.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui;\r\n\r\nimport android.graphics.Color;\r\n\r\npublic class SeekBarSegment {\r\n    public float startProgress;\r\n    public float endProgress;\r\n    public int color = Color.GREEN;\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/playback/ui/UiOptionItem.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class UiOptionItem implements OptionItem {\r\n    private int mId;\r\n    private CharSequence mTitle;\r\n    private CharSequence mDescription;\r\n    private boolean mIsSelected;\r\n    private FormatItem mFormat;\r\n    private OptionCallback mCallback;\r\n    private Object mData;\r\n    private OptionItem[] mRequiredItems;\r\n    private OptionItem[] mRadioItems;\r\n    private ChatReceiver mChatReceiver;\r\n    private CommentsReceiver mCommentsReceiver;\r\n\r\n    public static List<OptionItem> from(List<FormatItem> formats, OptionCallback callback) {\r\n        return from(formats, callback, null);\r\n    }\r\n\r\n    public static List<OptionItem> from(List<FormatItem> formats, OptionCallback callback, String defaultTitle) {\r\n        if (formats == null) {\r\n            return null;\r\n        }\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (FormatItem format : formats) {\r\n            options.add(from(format, callback, defaultTitle));\r\n        }\r\n\r\n        return options;\r\n    }\r\n\r\n    public static OptionItem from(FormatItem format, OptionCallback callback) {\r\n        return from(format, callback, null);\r\n    }\r\n\r\n    public static OptionItem from(FormatItem format, OptionCallback callback, String defaultTitle) {\r\n        if (format == null) {\r\n            return null;\r\n        }\r\n\r\n        UiOptionItem uiOptionItem = new UiOptionItem();\r\n\r\n        uiOptionItem.mTitle = format.isDefault() ? defaultTitle : format.getTitle();\r\n        uiOptionItem.mIsSelected = format.isSelected();\r\n        uiOptionItem.mFormat = format;\r\n        uiOptionItem.mCallback = callback;\r\n\r\n        return uiOptionItem;\r\n    }\r\n\r\n    public static OptionItem from(CharSequence title) {\r\n        return from(title, (OptionCallback) null);\r\n    }\r\n\r\n    public static OptionItem from(CharSequence title, OptionCallback callback) {\r\n        return from(title, callback, false);\r\n    }\r\n\r\n    public static OptionItem from(CharSequence title, OptionCallback callback, boolean isChecked) {\r\n        return from(title, callback, isChecked, null);\r\n    }\r\n\r\n    public static OptionItem from(CharSequence title, CharSequence description, OptionCallback callback, boolean isChecked) {\r\n        return from(title, description, callback, isChecked, null);\r\n    }\r\n\r\n    public static OptionItem from(CharSequence title, OptionCallback callback, boolean isChecked, Object data) {\r\n        return from(title, null, callback, isChecked, data);\r\n    }\r\n\r\n    public static OptionItem from(CharSequence title, CharSequence description, OptionCallback callback, boolean isChecked, Object data) {\r\n        UiOptionItem uiOptionItem = new UiOptionItem();\r\n\r\n        uiOptionItem.mTitle = title;\r\n        uiOptionItem.mDescription = description;\r\n        uiOptionItem.mIsSelected = isChecked;\r\n        uiOptionItem.mCallback = callback;\r\n        uiOptionItem.mData = data;\r\n\r\n        return uiOptionItem;\r\n    }\r\n\r\n    public static OptionItem from(CharSequence title, ChatReceiver chatReceiver) {\r\n        UiOptionItem uiOptionItem = new UiOptionItem();\r\n        uiOptionItem.mTitle = title;\r\n        uiOptionItem.mChatReceiver = chatReceiver;\r\n\r\n        return uiOptionItem;\r\n    }\r\n\r\n    public static OptionItem from(CharSequence title, CommentsReceiver commentsReceiver) {\r\n        UiOptionItem uiOptionItem = new UiOptionItem();\r\n        uiOptionItem.mTitle = title;\r\n        uiOptionItem.mCommentsReceiver = commentsReceiver;\r\n\r\n        return uiOptionItem;\r\n    }\r\n\r\n    public static FormatItem toFormat(OptionItem option) {\r\n        if (option instanceof UiOptionItem) {\r\n            return ((UiOptionItem) option).mFormat;\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    @Override\r\n    public int getId() {\r\n        return mId;\r\n    }\r\n\r\n    @Override\r\n    public CharSequence getTitle() {\r\n        return mTitle;\r\n    }\r\n\r\n    @Override\r\n    public CharSequence getDescription() {\r\n        return mDescription;\r\n    }\r\n\r\n    @Override\r\n    public boolean isSelected() {\r\n        return mIsSelected;\r\n    }\r\n\r\n    @Override\r\n    public void onSelect(boolean isSelected) {\r\n        mIsSelected = isSelected;\r\n\r\n        if (mCallback != null) {\r\n            mCallback.onSelect(this);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public Object getData() {\r\n        return mData;\r\n    }\r\n\r\n    @Override\r\n    public void setRequired(OptionItem... items) {\r\n        if (items == null || items.length == 0) {\r\n            mRequiredItems = null;\r\n        }\r\n\r\n        mRequiredItems = items;\r\n    }\r\n\r\n    @Override\r\n    public OptionItem[] getRequired() {\r\n        return mRequiredItems;\r\n    }\r\n\r\n    @Override\r\n    public void setRadio(OptionItem... items) {\r\n        if (items == null || items.length == 0) {\r\n            mRadioItems = null;\r\n        }\r\n\r\n        mRadioItems = items;\r\n    }\r\n\r\n    @Override\r\n    public OptionItem[] getRadio() {\r\n        return mRadioItems;\r\n    }\r\n\r\n    @Override\r\n    public ChatReceiver getChatReceiver() {\r\n        return mChatReceiver;\r\n    }\r\n\r\n    @Override\r\n    public CommentsReceiver getCommentsReceiver() {\r\n        return mCommentsReceiver;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/search/MediaServiceSearchTagProvider.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.search;\r\n\r\nimport android.text.TextUtils;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.ContentService;\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.search.vineyard.Tag;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\npublic class MediaServiceSearchTagProvider implements SearchTagsProvider {\r\n    private static final String TAG = MediaServiceSearchTagProvider.class.getSimpleName();\r\n    private final ContentService mContentService;\r\n    private final boolean mIgnoreEmptyQuery;\r\n    private Disposable mTagsAction;\r\n\r\n    public MediaServiceSearchTagProvider(boolean ignoreEmptyQuery) {\r\n        mIgnoreEmptyQuery = ignoreEmptyQuery;\r\n        ServiceManager service = YouTubeServiceManager.instance();\r\n        mContentService = service.getContentService();\r\n    }\r\n\r\n    @Override\r\n    public void search(String query, ResultsCallback callback) {\r\n        RxHelper.disposeActions(mTagsAction);\r\n\r\n        if (mIgnoreEmptyQuery && TextUtils.isEmpty(query)) {\r\n            callback.onResults(null);\r\n            return;\r\n        }\r\n\r\n        mTagsAction = mContentService.getSearchTagsObserve(query)\r\n                .subscribe(\r\n                        tags -> callback.onResults(Tag.from(tags)),\r\n                        error -> Log.e(TAG, \"Result is empty. Just ignore it. Error msg: %s\", error.getMessage())\r\n                );\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/search/PrefsSearchTagsProvider.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.search;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.search.vineyard.Tag;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class PrefsSearchTagsProvider implements SearchTagsProvider {\r\n    @Override\r\n    public void search(String query, ResultsCallback callback) {\r\n        List<Tag> tags = new ArrayList<>();\r\n        tags.add(new Tag(\"One\"));\r\n        tags.add(new Tag(\"Two\"));\r\n        tags.add(new Tag(\"Three\"));\r\n        tags.add(new Tag(\"Four\"));\r\n        tags.add(new Tag(\"Five\"));\r\n\r\n        callback.onResults(tags);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/search/SearchTagsProvider.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.search;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.search.vineyard.Tag;\r\n\r\nimport java.util.List;\r\n\r\npublic interface SearchTagsProvider {\r\n    interface ResultsCallback {\r\n        void onResults(List<Tag> results);\r\n    }\r\n    void search(String query, ResultsCallback callback);\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/search/vineyard/Option.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.search.vineyard;\n\npublic class Option {\n    public String title;\n    public String value;\n    public int iconResource;\n\n    public Option(String title, String value, int iconResource) {\n        this.title = title;\n        this.value = value;\n        this.iconResource = iconResource;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/search/vineyard/Tag.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.search.vineyard;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.NonNull;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Tag implements Comparable<Tag>, Parcelable {\n    public long tagId;\n    public String tag;\n    public long postCount;\n\n    public Tag() {\n    }\n\n    @Override\n    public int compareTo(@NonNull Tag another) {\n        return (int) (postCount - another.postCount);\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.writeLong(this.tagId);\n        dest.writeString(this.tag);\n        dest.writeLong(this.postCount);\n    }\n\n    public Tag(String tag) {\n        this.tag = tag;\n    }\n\n    protected Tag(Parcel in) {\n        this.tagId = in.readLong();\n        this.tag = in.readString();\n        this.postCount = in.readLong();\n    }\n\n    public static final Creator<Tag> CREATOR = new Creator<Tag>() {\n        public Tag createFromParcel(Parcel source) {\n            return new Tag(source);\n        }\n\n        public Tag[] newArray(int size) {\n            return new Tag[size];\n        }\n    };\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        Tag tag1 = (Tag) o;\n\n        if (tagId != tag1.tagId) return false;\n        if (postCount != tag1.postCount) return false;\n        return !(tag != null ? !tag.equals(tag1.tag) : tag1.tag != null);\n\n    }\n\n    @Override\n    public int hashCode() {\n        int result = (int) (tagId ^ (tagId >>> 32));\n        result = 31 * result + (tag != null ? tag.hashCode() : 0);\n        result = 31 * result + (int) (postCount ^ (postCount >>> 32));\n        return result;\n    }\n\n    public static List<Tag> from(List<String> tags) {\n        if (tags == null || tags.isEmpty()) {\n            return null;\n        }\n\n        List<Tag> result = new ArrayList<>();\n\n        for (String tag : tags) {\n            result.add(new Tag(tag));\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/models/search/vineyard/User.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.models.search.vineyard;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\npublic class User implements Parcelable {\n    public String username;\n    public int following;\n    public int followerCount;\n    public int verified;\n    public String description;\n    public String avatarUrl;\n    public int twitterId;\n    public String userId;\n    public int twitterConnected;\n    public int likeCount;\n    public int facebookConnected;\n    public int postCount;\n    public String phoneNumber;\n    public String location;\n    public int followingCount;\n    public String email;\n    public String error;\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(this.username);\n        dest.writeInt(this.following);\n        dest.writeInt(this.followerCount);\n        dest.writeInt(this.verified);\n        dest.writeString(this.description);\n        dest.writeString(this.avatarUrl);\n        dest.writeInt(this.twitterId);\n        dest.writeString(this.userId);\n        dest.writeInt(this.twitterConnected);\n        dest.writeInt(this.likeCount);\n        dest.writeInt(this.facebookConnected);\n        dest.writeInt(this.postCount);\n        dest.writeString(this.phoneNumber);\n        dest.writeString(this.location);\n        dest.writeInt(this.followingCount);\n        dest.writeString(this.email);\n        dest.writeString(this.error);\n    }\n\n    public User() {\n    }\n\n    protected User(Parcel in) {\n        this.username = in.readString();\n        this.following = in.readInt();\n        this.followerCount = in.readInt();\n        this.verified = in.readInt();\n        this.description = in.readString();\n        this.avatarUrl = in.readString();\n        this.twitterId = in.readInt();\n        this.userId = in.readString();\n        this.twitterConnected = in.readInt();\n        this.likeCount = in.readInt();\n        this.facebookConnected = in.readInt();\n        this.postCount = in.readInt();\n        this.phoneNumber = in.readString();\n        this.location = in.readString();\n        this.followingCount = in.readInt();\n        this.email = in.readString();\n        this.error = in.readString();\n    }\n\n    public static final Creator<User> CREATOR = new Creator<User>() {\n        public User createFromParcel(Parcel source) {\n            return new User(source);\n        }\n\n        public User[] newArray(int size) {\n            return new User[size];\n        }\n    };\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        User user = (User) o;\n\n        if (following != user.following) return false;\n        if (followerCount != user.followerCount) return false;\n        if (verified != user.verified) return false;\n        if (twitterId != user.twitterId) return false;\n        if (twitterConnected != user.twitterConnected) return false;\n        if (likeCount != user.likeCount) return false;\n        if (facebookConnected != user.facebookConnected) return false;\n        if (postCount != user.postCount) return false;\n        if (followingCount != user.followingCount) return false;\n        if (username != null ? !username.equals(user.username) : user.username != null)\n            return false;\n        if (description != null ? !description.equals(user.description) : user.description != null)\n            return false;\n        if (avatarUrl != null ? !avatarUrl.equals(user.avatarUrl) : user.avatarUrl != null)\n            return false;\n        if (userId != null ? !userId.equals(user.userId) : user.userId != null) return false;\n        if (phoneNumber != null ? !phoneNumber.equals(user.phoneNumber) : user.phoneNumber != null)\n            return false;\n        if (location != null ? !location.equals(user.location) : user.location != null)\n            return false;\n        if (email != null ? !email.equals(user.email) : user.email != null) return false;\n        return !(error != null ? !error.equals(user.error) : user.error != null);\n\n    }\n\n    @Override\n    public int hashCode() {\n        int result = username != null ? username.hashCode() : 0;\n        result = 31 * result + following;\n        result = 31 * result + followerCount;\n        result = 31 * result + verified;\n        result = 31 * result + (description != null ? description.hashCode() : 0);\n        result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0);\n        result = 31 * result + twitterId;\n        result = 31 * result + (userId != null ? userId.hashCode() : 0);\n        result = 31 * result + twitterConnected;\n        result = 31 * result + likeCount;\n        result = 31 * result + facebookConnected;\n        result = 31 * result + postCount;\n        result = 31 * result + (phoneNumber != null ? phoneNumber.hashCode() : 0);\n        result = 31 * result + (location != null ? location.hashCode() : 0);\n        result = 31 * result + followingCount;\n        result = 31 * result + (email != null ? email.hashCode() : 0);\n        result = 31 * result + (error != null ? error.hashCode() : 0);\n        return result;\n    }\n}"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/AddDevicePresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.AddDeviceView;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\npublic class AddDevicePresenter extends BasePresenter<AddDeviceView> {\r\n    private static final String TAG = AddDevicePresenter.class.getSimpleName();\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static AddDevicePresenter sInstance;\r\n    private final ServiceManager mService;\r\n    private Disposable mDeviceCodeAction;\r\n\r\n    private AddDevicePresenter(Context context) {\r\n        super(context);\r\n        mService = YouTubeServiceManager.instance();\r\n    }\r\n\r\n    public static AddDevicePresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new AddDevicePresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void unhold() {\r\n        RxHelper.disposeActions(mDeviceCodeAction);\r\n        sInstance = null;\r\n    }\r\n\r\n    @Override\r\n    public void onViewDestroyed() {\r\n        super.onViewDestroyed();\r\n        unhold();\r\n    }\r\n\r\n    @Override\r\n    public void onViewInitialized() {\r\n        RxHelper.disposeActions(mDeviceCodeAction);\r\n        updateDeviceCode();\r\n    }\r\n\r\n    public void onActionClicked() {\r\n        if (getView() != null) {\r\n            getView().close();\r\n        }\r\n    }\r\n\r\n    private void updateDeviceCode() {\r\n        mDeviceCodeAction = mService.getRemoteControlService().getPairingCodeObserve()\r\n                .subscribe(\r\n                        deviceCode -> getView().showCode(deviceCode),\r\n                        error -> {\r\n                            Log.e(TAG, \"Get pairing code error: %s\", error.getMessage());\r\n                            if (getView() != null) {\r\n                                getView().showCode(error.getMessage());\r\n                            }\r\n                        }\r\n                );\r\n    }\r\n\r\n    public void start() {\r\n        RxHelper.disposeActions(mDeviceCodeAction);\r\n        getViewManager().startView(AddDeviceView.class);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/AppDialogPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport android.os.Handler;\r\nimport android.os.Looper;\r\n\r\nimport com.liskovsoft.sharedutils.misc.WeakHashSet;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionCategory;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.AppDialogView;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class AppDialogPresenter extends BasePresenter<AppDialogView> {\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static AppDialogPresenter sInstance;\r\n    private final Handler mHandler;\r\n    private final Runnable mCloseDialog = this::closeDialog;\r\n    private final WeakHashSet<Runnable> mOnStart = new WeakHashSet<>();\r\n    private final WeakHashSet<Runnable> mOnFinish = new WeakHashSet<>();\r\n    private String mTitle;\r\n    private long mTimeoutMs;\r\n    private boolean mIsTransparent;\r\n    private boolean mIsOverlay;\r\n    private List<OptionCategory> mCategories;\r\n    private boolean mIsExpandable = true;\r\n    private int mId;\r\n\r\n    private String mBackupTitle;\r\n    private List<OptionCategory> mBackupCategories;\r\n    private int mBackupId;\r\n    private boolean mBackupIsTransparent;\r\n    private boolean mBackupIsOverlay;\r\n    private boolean mBackupIsExpandable;\r\n\r\n    public AppDialogPresenter(Context context) {\r\n        super(context);\r\n        mCategories = new ArrayList<>();\r\n        mHandler = new Handler(Looper.getMainLooper());\r\n    }\r\n\r\n    public static AppDialogPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new AppDialogPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    /**\r\n     * Called when user pressed back button.\r\n     */\r\n    @Override\r\n    public void onFinish() {\r\n        super.onFinish();\r\n        clear();\r\n\r\n        Utils.runMyCallbacks(mOnFinish);\r\n    }\r\n\r\n    private void clear() {\r\n        mTimeoutMs = 0;\r\n        mHandler.removeCallbacks(mCloseDialog);\r\n        resetData();\r\n    }\r\n\r\n    /**\r\n     * Doubled items fix / Empty dialog fix (multiple overlapped dialogs)\r\n     */\r\n    private void backupData() {\r\n        mBackupCategories = mCategories;\r\n        mBackupTitle = mTitle;\r\n        mBackupId = mId;\r\n        mBackupIsExpandable = mIsExpandable;\r\n        mBackupIsTransparent = mIsTransparent;\r\n        mBackupIsOverlay = mIsOverlay;\r\n    }\r\n\r\n    private void resetData() {\r\n        mCategories = new ArrayList<>();\r\n        mIsExpandable = true;\r\n        mIsTransparent = false;\r\n        mIsOverlay = false;\r\n        mId = 0;\r\n        mTitle = null;\r\n    }\r\n\r\n    @Override\r\n    public void onViewInitialized() {\r\n        getView().show(mBackupCategories, mBackupTitle, mBackupIsExpandable, mBackupIsTransparent, mBackupIsOverlay, mBackupId);\r\n        Utils.runMyCallbacks(mOnStart);\r\n    }\r\n\r\n    /**\r\n     * Called after {@link #onFinish}\r\n     */\r\n    @Override\r\n    public void onViewDestroyed() {\r\n        super.onViewDestroyed();\r\n        clear();\r\n    }\r\n\r\n    public void showDialog() {\r\n        showDialog(null, null);\r\n    }\r\n\r\n    public void showDialog(String dialogTitle) {\r\n        showDialog(dialogTitle, null);\r\n    }\r\n\r\n    public void showDialog(Runnable onFinish) {\r\n        showDialog(null, onFinish);\r\n    }\r\n\r\n    public void showDialog(String dialogTitle, Runnable onFinish) {\r\n        mTitle = dialogTitle;\r\n        mOnFinish.add(onFinish);\r\n        \r\n        backupData(); // overlapped dialog fix\r\n        resetData(); // prepare to new call\r\n\r\n        if (getView() != null) {\r\n            onViewInitialized();\r\n        }\r\n\r\n        getViewManager().startView(AppDialogView.class, true);\r\n\r\n        setupTimeout();\r\n    }\r\n\r\n    public void closeDialog() {\r\n        if (getView() != null) {\r\n            getView().finish();\r\n        }\r\n    }\r\n\r\n    public void goBack() {\r\n        if (getView() != null) {\r\n            getView().goBack();\r\n        }\r\n    }\r\n\r\n    public void clearBackstack() {\r\n        if (getView() != null) {\r\n            getView().clearBackstack();\r\n        }\r\n    }\r\n\r\n    public boolean isDialogShown() {\r\n        // Also check that current dialog almost closed (new view start is pending from a menu item)\r\n        // Hmm. Maybe current dialog is pending. Check that view is null.\r\n        // Also check that we aren't started the same view (nested dialog).\r\n        return (getViewManager().isVisible(getView()) && getView() != null && !getView().isPaused()) ||\r\n                getViewManager().isViewPending(AppDialogView.class);\r\n    }\r\n\r\n    public boolean isCommentsDialogShown() {\r\n        return isDialogShown() && isTypeComments();\r\n    }\r\n\r\n    public void appendCategory(OptionCategory category) {\r\n        mCategories.add(category);\r\n    }\r\n\r\n    public void appendRadioCategory(String categoryTitle, List<OptionItem> items) {\r\n        mCategories.add(OptionCategory.radioList(categoryTitle, items));\r\n    }\r\n\r\n    public void appendCheckedCategory(String categoryTitle, List<OptionItem> items) {\r\n        mCategories.add(OptionCategory.checkedList(categoryTitle, items));\r\n    }\r\n\r\n    public void appendStringsCategory(String categoryTitle, List<OptionItem> items) {\r\n        mCategories.add(OptionCategory.stringList(categoryTitle, items));\r\n    }\r\n\r\n    public void appendLongTextCategory(String categoryTitle, OptionItem item) {\r\n        mCategories.add(OptionCategory.longText(categoryTitle, item));\r\n    }\r\n\r\n    public void appendChatCategory(String categoryTitle, OptionItem item) {\r\n        mCategories.add(OptionCategory.chat(categoryTitle, item));\r\n    }\r\n\r\n    public void appendCommentsCategory(String categoryTitle, OptionItem item) {\r\n        mCategories.add(OptionCategory.comments(categoryTitle, item));\r\n    }\r\n\r\n    public void appendSingleSwitch(OptionItem optionItem) {\r\n        mCategories.add(OptionCategory.singleSwitch(optionItem));\r\n    }\r\n\r\n    public void appendSingleButton(OptionItem optionItem) {\r\n        mCategories.add(OptionCategory.singleButton(optionItem));\r\n    }\r\n\r\n    public void showDialogMessage(String dialogTitle, Runnable onClose, int timeoutMs) {\r\n        showDialog(dialogTitle, onClose);\r\n\r\n        new Handler(Looper.getMainLooper()).postDelayed(() -> {\r\n            if (getView() != null) {\r\n                getView().finish();\r\n            }\r\n        }, timeoutMs);\r\n    }\r\n\r\n    public void setCloseTimeoutMs(long timeoutMs) {\r\n        mTimeoutMs = timeoutMs;\r\n    }\r\n\r\n    public void setOnStart(Runnable onStart) {\r\n        Utils.addMyCallback(mOnStart, onStart);\r\n    }\r\n\r\n    public void setOnFinish(Runnable onFinish) {\r\n        Utils.addMyCallback(mOnFinish, onFinish);\r\n    }\r\n\r\n    public void enableTransparent(boolean enable) {\r\n        mIsTransparent = enable;\r\n    }\r\n\r\n    /**\r\n     * Close the dialog on movements keys\r\n     */\r\n    public void enableOverlay(boolean enable) {\r\n        mIsOverlay = enable;\r\n    }\r\n\r\n    public boolean isTransparent() {\r\n        return getView() != null && getView().isTransparent();\r\n    }\r\n\r\n    public boolean isOverlay() {\r\n        return getView() != null && getView().isOverlay();\r\n    }\r\n\r\n    public boolean isComments() {\r\n        if (mBackupCategories == null || mBackupCategories.isEmpty()) {\r\n            return false;\r\n        }\r\n\r\n        OptionCategory optionCategory = mBackupCategories.get(0);\r\n\r\n        return optionCategory.type == OptionCategory.TYPE_COMMENTS;\r\n    }\r\n\r\n    /**\r\n     * Show a category contents instead of title if a single category has been added\r\n     */\r\n    public void enableExpandable(boolean enable) {\r\n        mIsExpandable = enable;\r\n    }\r\n\r\n    public void setId(int id) {\r\n        mId = id;\r\n    }\r\n\r\n    public int getId() {\r\n        return getView() != null ? getView().getViewId() : mId;\r\n    }\r\n\r\n    public boolean isEmpty() {\r\n        return mCategories == null || mCategories.isEmpty();\r\n    }\r\n\r\n    private void setupTimeout() {\r\n        mHandler.removeCallbacks(mCloseDialog);\r\n\r\n        if (mTimeoutMs > 0) {\r\n            mHandler.postDelayed(mCloseDialog, mTimeoutMs);\r\n        }\r\n    }\r\n\r\n    private boolean isTypeComments() {\r\n        if (mBackupCategories == null) {\r\n            return false;\r\n        }\r\n\r\n        boolean isComments = false;\r\n\r\n        for (OptionCategory category : mBackupCategories) {\r\n            if (category.type == OptionCategory.TYPE_COMMENTS) {\r\n                isComments = true;\r\n                break;\r\n            }\r\n        }\r\n\r\n        return isComments;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/BrowsePresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.text.TextUtils;\n\nimport androidx.annotation.Nullable;\n\nimport com.liskovsoft.mediaserviceinterfaces.oauth.Account;\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\nimport com.liskovsoft.sharedutils.helpers.Helpers;\nimport com.liskovsoft.sharedutils.locale.LocaleUtility;\nimport com.liskovsoft.sharedutils.mylogger.Log;\nimport com.liskovsoft.sharedutils.rx.RxHelper;\nimport com.liskovsoft.smartyoutubetv2.common.R;\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.BrowseSection;\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Playlist;\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.SettingsGroup;\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.SettingsItem;\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\nimport com.liskovsoft.smartyoutubetv2.common.app.models.errors.CategoryEmptyError;\nimport com.liskovsoft.smartyoutubetv2.common.app.models.errors.ErrorFragmentData;\nimport com.liskovsoft.smartyoutubetv2.common.app.models.errors.PasswordError;\nimport com.liskovsoft.smartyoutubetv2.common.app.models.errors.SignInError;\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService;\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService.State;\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.VideoActionPresenter;\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.ChannelUploadsMenuPresenter;\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.SectionMenuPresenter;\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter;\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter.VideoMenuCallback;\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.channelgroup.ChannelGroupServiceWrapper;\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.interfaces.SectionPresenter;\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.interfaces.VideoGroupPresenter;\nimport com.liskovsoft.smartyoutubetv2.common.app.views.BrowseView;\nimport com.liskovsoft.smartyoutubetv2.common.misc.AppDataSourceManager;\nimport com.liskovsoft.smartyoutubetv2.common.misc.BrowseProcessorManager;\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager.AccountChangeListener;\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AccountsData;\nimport com.liskovsoft.smartyoutubetv2.common.prefs.BlockedChannelData;\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.Callable;\n\nimport io.reactivex.Observable;\nimport io.reactivex.disposables.Disposable;\n\npublic class BrowsePresenter extends BasePresenter<BrowseView> implements SectionPresenter, VideoGroupPresenter, AccountChangeListener {\n    private static final String TAG = BrowsePresenter.class.getSimpleName();\n    @SuppressLint(\"StaticFieldLeak\")\n    private static BrowsePresenter sInstance;\n    private final List<BrowseSection> mSections;\n    private final List<BrowseSection> mErrorSections;\n    private final Map<Integer, Observable<MediaGroup>> mGridMapping;\n    private final Map<Integer, Observable<List<MediaGroup>>> mRowMapping;\n    private final Map<Integer, Callable<List<SettingsItem>>> mSettingsGridMapping;\n    private final Map<Integer, Callable<List<Video>>> mLocalGridMappings;\n    private final Map<Integer, BrowseSection> mSectionsMapping;\n    private final AppDataSourceManager mDataSourcePresenter;\n    private final BrowseProcessorManager mBrowseProcessor;\n    private final List<Disposable> mActions;\n    private final Runnable mRefreshSection = this::refresh;\n    private BrowseSection mCurrentSection;\n    private Video mCurrentVideo;\n    private long mLastUpdateTimeMs = -1;\n    private int mBootSectionIndex;\n    private int mBootstrapSectionId = -1;\n\n    private BrowsePresenter(Context context) {\n        super(context);\n        mDataSourcePresenter = AppDataSourceManager.instance();\n        mSections = new ArrayList<>();\n        mErrorSections = new ArrayList<>();\n        mGridMapping = new HashMap<>();\n        mRowMapping = new HashMap<>();\n        mSettingsGridMapping = new HashMap<>();\n        mLocalGridMappings = new HashMap<>();\n        mSectionsMapping = new HashMap<>();\n        MediaServiceManager.instance().addAccountListener(this);\n\n        mBrowseProcessor = new BrowseProcessorManager(getContext(), this::syncItem);\n        mActions = new ArrayList<>();\n\n        initSectionMappings();\n        updateChannelSorting();\n        updatePlaylistsStyle();\n    }\n\n    public static BrowsePresenter instance(Context context) {\n        if (sInstance == null) {\n            sInstance = new BrowsePresenter(context);\n        }\n\n        sInstance.setContext(context);\n\n        return sInstance;\n    }\n\n    public static void unhold() {\n        sInstance = null;\n    }\n\n    @Override\n    public void onViewInitialized() {\n        super.onViewInitialized();\n\n        if (getView() == null) {\n            return;\n        }\n\n        updateSections();\n\n        // Move default focus\n        int selectedSectionIndex = findSectionIndex(mCurrentSection != null ? mCurrentSection.getId() : mBootstrapSectionId);\n        mBootstrapSectionId = -1;\n        getView().selectSection(selectedSectionIndex != -1 ? selectedSectionIndex : mBootSectionIndex, true);\n    }\n\n    @Override\n    public void onViewPaused() {\n        super.onViewPaused();\n\n        saveSelectedItems();\n    }\n\n    @Override\n    public void onViewResumed() {\n        super.onViewResumed();\n\n        refreshIfNeeded();\n    }\n\n    private void refreshIfNeeded() {\n        if (getView() == null || !isHomeSection() || mLastUpdateTimeMs == -1 || System.currentTimeMillis() - mLastUpdateTimeMs < 3 * 60 * 60 * 1_000) {\n            return;\n        }\n\n        refresh(false);\n    }\n\n    private void saveSelectedItems() {\n        // Fix position reset when jumping between sections\n        if (mCurrentVideo != null && mCurrentVideo.getPositionInsideGroup() == 0 && (System.currentTimeMillis() - mCurrentVideo.timestamp) < 10_000) {\n            return;\n        }\n\n        if ((isSubscriptionsSection() && getGeneralData().isRememberSubscriptionsPositionEnabled()) ||\n                (isPinnedSection() && getGeneralData().isRememberPinnedPositionEnabled())) {\n            getGeneralData().setSelectedItem(mCurrentSection.getId(), mCurrentVideo);\n        }\n    }\n\n    private void restoreSelectedItems() {\n        if (getView() == null) {\n            return;\n        }\n\n        if ((isSubscriptionsSection() && getGeneralData().isRememberSubscriptionsPositionEnabled()) ||\n                (isPinnedSection() && getGeneralData().isRememberPinnedPositionEnabled())) {\n            getView().selectSectionItem(getGeneralData().getSelectedItem(mCurrentSection.getId()));\n        }\n    }\n\n    private void initSectionMappings() {\n        initSectionMapping();\n\n        initRowAndGridMapping();\n\n        initSettingsGridMapping();\n        initLocalGridMapping();\n    }\n\n    private void initSectionMapping() {\n        String country = LocaleUtility.getCurrentLocale(getContext()).getCountry();\n        int uploadsType = getMainUIData().isUploadsOldLookEnabled() ? BrowseSection.TYPE_GRID : BrowseSection.TYPE_MULTI_GRID;\n\n        mSectionsMapping.put(MediaGroup.TYPE_HOME, new BrowseSection(MediaGroup.TYPE_HOME, getContext().getString(R.string.header_home), BrowseSection.TYPE_ROW, R.drawable.icon_home, false));\n        mSectionsMapping.put(MediaGroup.TYPE_SHORTS, new BrowseSection(MediaGroup.TYPE_SHORTS, getContext().getString(R.string.header_shorts), BrowseSection.TYPE_SHORTS_GRID, R.drawable.icon_shorts));\n        mSectionsMapping.put(MediaGroup.TYPE_TRENDING, new BrowseSection(MediaGroup.TYPE_TRENDING, getContext().getString(R.string.header_trending), BrowseSection.TYPE_ROW, R.drawable.icon_trending));\n        mSectionsMapping.put(MediaGroup.TYPE_KIDS_HOME, new BrowseSection(MediaGroup.TYPE_KIDS_HOME, getContext().getString(R.string.header_kids_home), BrowseSection.TYPE_ROW, R.drawable.icon_kids_home));\n        mSectionsMapping.put(MediaGroup.TYPE_SPORTS, new BrowseSection(MediaGroup.TYPE_SPORTS, getContext().getString(R.string.header_sports), BrowseSection.TYPE_ROW, R.drawable.icon_sports));\n        mSectionsMapping.put(MediaGroup.TYPE_LIVE, new BrowseSection(MediaGroup.TYPE_LIVE, getContext().getString(R.string.badge_live), BrowseSection.TYPE_ROW, R.drawable.icon_live));\n        mSectionsMapping.put(MediaGroup.TYPE_MY_VIDEOS, new BrowseSection(MediaGroup.TYPE_MY_VIDEOS, getContext().getString(R.string.my_videos), BrowseSection.TYPE_GRID, R.drawable.icon_playlist));\n        mSectionsMapping.put(MediaGroup.TYPE_GAMING, new BrowseSection(MediaGroup.TYPE_GAMING, getContext().getString(R.string.header_gaming), BrowseSection.TYPE_ROW, R.drawable.icon_gaming));\n        if (!Helpers.equalsAny(country, \"RU\", \"BY\")) {\n            mSectionsMapping.put(MediaGroup.TYPE_NEWS, new BrowseSection(MediaGroup.TYPE_NEWS, getContext().getString(R.string.header_news), BrowseSection.TYPE_ROW, R.drawable.icon_news));\n        }\n        mSectionsMapping.put(MediaGroup.TYPE_MUSIC, new BrowseSection(MediaGroup.TYPE_MUSIC, getContext().getString(R.string.header_music), BrowseSection.TYPE_ROW, R.drawable.icon_music));\n        mSectionsMapping.put(MediaGroup.TYPE_CHANNEL_UPLOADS, new BrowseSection(MediaGroup.TYPE_CHANNEL_UPLOADS, getContext().getString(R.string.header_channels), uploadsType, R.drawable.icon_channels, false));\n        mSectionsMapping.put(MediaGroup.TYPE_SUBSCRIPTIONS, new BrowseSection(MediaGroup.TYPE_SUBSCRIPTIONS, getContext().getString(R.string.header_subscriptions), BrowseSection.TYPE_GRID, R.drawable.icon_subscriptions, false));\n        mSectionsMapping.put(MediaGroup.TYPE_HISTORY, new BrowseSection(MediaGroup.TYPE_HISTORY, getContext().getString(R.string.header_history), BrowseSection.TYPE_GRID, R.drawable.icon_history, true));\n        mSectionsMapping.put(MediaGroup.TYPE_BLOCKED_CHANNELS,\n                new BrowseSection(MediaGroup.TYPE_BLOCKED_CHANNELS, getContext().getString(R.string.header_blocked_channels), BrowseSection.TYPE_GRID, R.drawable.icon_blocked_channels, false));\n        mSectionsMapping.put(MediaGroup.TYPE_USER_PLAYLISTS, new BrowseSection(MediaGroup.TYPE_USER_PLAYLISTS, getContext().getString(R.string.header_playlists), BrowseSection.TYPE_ROW, R.drawable.icon_playlist, false));\n        mSectionsMapping.put(MediaGroup.TYPE_NOTIFICATIONS, new BrowseSection(MediaGroup.TYPE_NOTIFICATIONS, getContext().getString(R.string.header_notifications), BrowseSection.TYPE_GRID, R.drawable.icon_notification, false));\n        mSectionsMapping.put(MediaGroup.TYPE_PLAYBACK_QUEUE, new BrowseSection(MediaGroup.TYPE_PLAYBACK_QUEUE, getContext().getString(R.string.playback_queue_category_title), BrowseSection.TYPE_GRID, R.drawable.icon_queue, false));\n\n        if (getSidebarService().isSettingsSectionEnabled()) {\n            mSectionsMapping.put(MediaGroup.TYPE_SETTINGS, new BrowseSection(MediaGroup.TYPE_SETTINGS, getContext().getString(R.string.header_settings), BrowseSection.TYPE_SETTINGS_GRID, R.drawable.icon_settings));\n        }\n    }\n\n    private void initRowAndGridMapping() {\n        mRowMapping.put(MediaGroup.TYPE_HOME, getContentService().getHomeObserve());\n        mRowMapping.put(MediaGroup.TYPE_TRENDING, getContentService().getTrendingObserve());\n        mRowMapping.put(MediaGroup.TYPE_KIDS_HOME, getContentService().getKidsHomeObserve());\n        mRowMapping.put(MediaGroup.TYPE_SPORTS, getContentService().getSportsObserve());\n        mRowMapping.put(MediaGroup.TYPE_LIVE, getContentService().getLiveObserve());\n        mRowMapping.put(MediaGroup.TYPE_NEWS, getContentService().getNewsObserve());\n        mRowMapping.put(MediaGroup.TYPE_MUSIC, getContentService().getMusicObserve());\n        mRowMapping.put(MediaGroup.TYPE_GAMING, getContentService().getGamingObserve());\n        mRowMapping.put(MediaGroup.TYPE_USER_PLAYLISTS, getContentService().getPlaylistRowsObserve());\n\n        mGridMapping.put(MediaGroup.TYPE_SHORTS, getContentService().getShortsObserve());\n        mGridMapping.put(MediaGroup.TYPE_SUBSCRIPTIONS, getContentService().getSubscriptionsObserve());\n        mGridMapping.put(MediaGroup.TYPE_HISTORY, getContentService().getHistoryObserve());\n        mGridMapping.put(MediaGroup.TYPE_CHANNEL_UPLOADS, getContentService().getSubscribedChannelsByNewContentObserve());\n        mGridMapping.put(MediaGroup.TYPE_NOTIFICATIONS, getNotificationsService().getNotificationItemsObserve());\n        mGridMapping.put(MediaGroup.TYPE_MY_VIDEOS, getContentService().getMyVideosObserve());\n    }\n\n    private void initPinnedSections() {\n        mSections.clear();\n\n        Collection<Video> pinnedItems = getSidebarService().getPinnedItems();\n\n        for (Video item : pinnedItems) {\n            if (item != null) {\n                if (item.sectionId == -1) { // pinned channel or playlist\n                    BrowseSection section = createPinnedSection(item);\n                    mSections.add(section);\n                } else {\n                    BrowseSection section = mSectionsMapping.get(item.sectionId);\n\n                    if (section != null) {\n                        mSections.add(section);\n                    }\n                }\n            }\n        }\n    }\n\n    private void initPinnedCallbacks() {\n        Collection<Video> pinnedItems = getSidebarService().getPinnedItems();\n\n        for (Video item : pinnedItems) {\n            if (item != null && item.sectionId == -1) {\n                createPinnedMapping(item);\n            }\n        }\n    }\n\n    private void initSettingsGridMapping() {\n        mSettingsGridMapping.put(MediaGroup.TYPE_SETTINGS, () -> mDataSourcePresenter.getSettingItems(getContext()));\n    }\n\n    private void initLocalGridMapping() {\n        mLocalGridMappings.put(MediaGroup.TYPE_PLAYBACK_QUEUE, () -> Playlist.instance().getAll());\n        mLocalGridMappings.put(MediaGroup.TYPE_BLOCKED_CHANNELS, this::getBlockedChannels);\n    }\n\n    private List<Video> getBlockedChannels() {\n        BlockedChannelData blockedChannelData = BlockedChannelData.instance(getContext());\n        List<Video> videos = new ArrayList<>();\n\n        for (Map.Entry<String, String> entry : blockedChannelData.getChannelIdsWithNames().entrySet()) {\n            Video video = new Video();\n            video.channelId = entry.getKey();\n            video.title = entry.getValue() != null ? entry.getValue() : entry.getKey();\n            videos.add(video);\n        }\n\n        return videos;\n    }\n\n    public void updateSections() {\n        if (getView() == null) {\n            return;\n        }\n\n        initPinnedData();\n\n        refreshSections();\n    }\n\n    private void refreshSections() {\n        if (getView() == null) {\n            return;\n        }\n\n        // clean up (profile changed etc)\n        getView().removeAllSections();\n\n        int bootSectionId = getSidebarService().getBootSectionId();\n\n        // Empty Home on first run fix. Switch to something non-empty.\n        if (!getSignInService().isSigned() && VideoStateService.instance(getContext()).isEmpty()) {\n            bootSectionId = MediaGroup.TYPE_MUSIC;\n        }\n\n        int index = 0;\n\n        for (BrowseSection section : mErrorSections) {\n            getView().addSection(index++, section);\n        }\n\n        for (BrowseSection section : mSections) { // contains sections and pinned items!\n            if (section.getId() == MediaGroup.TYPE_SETTINGS) {\n                section.setEnabled(true);\n            }\n\n            if (section.isEnabled()) {\n                if (section.getId() == bootSectionId) {\n                    mBootSectionIndex = index;\n                }\n                getView().addSection(index++, section);\n            } else {\n                getView().removeSection(section);\n            }\n        }\n\n        // Refresh and restore last focus\n        int selectedSectionIndex = findSectionIndex(mCurrentSection != null ? mCurrentSection.getId() : -1);\n        getView().selectSection(selectedSectionIndex != -1 ? selectedSectionIndex : mBootSectionIndex, false);\n    }\n\n    private void initPinnedData() {\n        initPinnedSections();\n        initPinnedCallbacks();\n        initPasswordSection();\n    }\n\n    private void sortSections() {\n        // NOTE: Comparator.comparingInt API >= 24\n        Collections.sort(mSections, (o1, o2) -> {\n            return getSidebarService().getSectionIndex(o1.getId()) - getSidebarService().getSectionIndex(o2.getId());\n        });\n    }\n\n    public void updateChannelSorting() {\n        int sortingType = getMainUIData().getChannelCategorySorting();\n\n        switch (sortingType) {\n            case MainUIData.CHANNEL_SORTING_DEFAULT:\n                mGridMapping.put(MediaGroup.TYPE_CHANNEL_UPLOADS, getContentService().getSubscribedChannelsObserve());\n                break;\n            case MainUIData.CHANNEL_SORTING_NAME2:\n            case MainUIData.CHANNEL_SORTING_NAME:\n                mGridMapping.put(MediaGroup.TYPE_CHANNEL_UPLOADS, getContentService().getSubscribedChannelsByNameObserve());\n                break;\n            case MainUIData.CHANNEL_SORTING_NEW_CONTENT:\n                mGridMapping.put(MediaGroup.TYPE_CHANNEL_UPLOADS, getContentService().getSubscribedChannelsByNewContentObserve());\n                break;\n            case MainUIData.CHANNEL_SORTING_LAST_VIEWED:\n                mGridMapping.put(MediaGroup.TYPE_CHANNEL_UPLOADS, getContentService().getSubscribedChannelsByLastViewedObserve());\n                break;\n        }\n    }\n\n    public void updatePlaylistsStyle() {\n        int playlistsStyle = getMainUIData().getPlaylistsStyle();\n\n        switch (playlistsStyle) {\n            case MainUIData.PLAYLISTS_STYLE_GRID:\n                mRowMapping.remove(MediaGroup.TYPE_USER_PLAYLISTS);\n                mGridMapping.put(MediaGroup.TYPE_USER_PLAYLISTS, getContentService().getPlaylistsObserve());\n                updateCategoryType(MediaGroup.TYPE_USER_PLAYLISTS, BrowseSection.TYPE_GRID);\n                break;\n            case MainUIData.PLAYLISTS_STYLE_ROWS:\n                mGridMapping.remove(MediaGroup.TYPE_USER_PLAYLISTS);\n                mRowMapping.put(MediaGroup.TYPE_USER_PLAYLISTS, getContentService().getPlaylistRowsObserve());\n                updateCategoryType(MediaGroup.TYPE_USER_PLAYLISTS, BrowseSection.TYPE_ROW);\n                break;\n        }\n    }\n\n    private void updateCategoryType(int categoryId, int categoryType) {\n        if (categoryType == -1 || categoryId == -1 || mSections == null) {\n            return;\n        }\n\n        BrowseSection section = mSectionsMapping.get(categoryId);\n\n        if (section != null) {\n            section.setType(categoryType);\n        }\n\n        for (BrowseSection category : mSections) {\n            if (category.getId() == categoryId) {\n                category.setType(categoryType);\n                break;\n            }\n        }\n    }\n\n    @Override\n    public void onViewDestroyed() {\n        super.onViewDestroyed();\n        disposeActions();\n        saveSelectedItems();\n    }\n\n    @Override\n    public void onVideoItemSelected(Video item) {\n        if (getView() == null) {\n            return;\n        }\n\n        if (belongsToChannelUploadsMultiGrid(item)) {\n            if (getMainUIData().isUploadsAutoLoadEnabled()) {\n                updateChannelUploadsMultiGrid(item);\n            } else {\n                updateChannelUploadsMultiGrid(null); // clear\n            }\n        }\n\n        mCurrentVideo = item;\n    }\n\n    @Override\n    public void onVideoItemClicked(Video item) {\n        if (getContext() == null) {\n            return;\n        }\n\n        // Check that channels new look enabled and we're on the first columnAdd commentMore actions\n        if (belongsToChannelUploadsMultiGrid(item)) {\n            if (getMainUIData().isUploadsAutoLoadEnabled()) {\n                VideoActionPresenter.instance(getContext()).apply(item);\n            } else {\n                updateChannelUploadsMultiGrid(item);\n            }\n        } else {\n            VideoActionPresenter.instance(getContext()).apply(item);\n        }\n    }\n\n    @Override\n    public void onVideoItemLongClicked(Video item) {\n        if (getContext() == null) {\n            return;\n        }\n\n        if (belongsToChannelUploads(item)) { // We need to be sure we exactly on Channels section\n            ChannelUploadsMenuPresenter.instance(getContext()).showMenu(item, (videoItem, action) -> {\n                if (action == VideoMenuCallback.ACTION_UNSUBSCRIBE) { // works with any uploads section look\n                    removeItem(item);\n                }\n            });\n        } else {\n            VideoMenuPresenter.instance(getContext()).showMenu(item, (videoItem, action) -> {\n                if (action == VideoMenuCallback.ACTION_REMOVE ||\n                    action == VideoMenuCallback.ACTION_REMOVE_FROM_PLAYLIST ||\n                    action == VideoMenuCallback.ACTION_REMOVE_FROM_QUEUE) {\n                    removeItem(videoItem);\n                } else if (action == VideoMenuCallback.ACTION_UNSUBSCRIBE && isMultiGridChannelUploadsSection()) {\n                    removeItem(mCurrentVideo);\n                    VideoMenuPresenter.instance(getContext()).closeDialog();\n                } else if (action == VideoMenuCallback.ACTION_UNSUBSCRIBE && isSubscriptionsSection()) {\n                    removeItemAuthor(videoItem);\n                    VideoMenuPresenter.instance(getContext()).closeDialog();\n                } else if (action == VideoMenuCallback.ACTION_REMOVE_AUTHOR) {\n                    removeItemAuthor(videoItem);\n                }\n            });\n        }\n    }\n\n    @Override\n    public void onScrollEnd(Video item) {\n        if (item == null) {\n            Log.e(TAG, \"Can't scroll. Video is null.\");\n            return;\n        }\n\n        VideoGroup group = item.getGroup();\n\n        continueGroup(group);\n    }\n\n    @Override\n    public void onSectionFocused(int sectionId) {\n        saveSelectedItems(); // save previous state\n        mCurrentSection = findSectionById(sectionId);\n        mCurrentVideo = null; // fast scroll through the sections (fix empty selected item)\n        updateCurrentSection();\n        restoreSelectedItems(); // Don't place anywhere else\n    }\n\n    @Override\n    public void onSectionLongPressed(int sectionId) {\n        SectionMenuPresenter.instance(getContext()).showMenu(findSectionById(sectionId));\n    }\n\n    @Override\n    public boolean hasPendingActions() {\n        return RxHelper.isAnyActionRunning(mActions);\n    }\n\n    public boolean isItemPinned(Video item) {\n        Collection<Video> items = getSidebarService().getPinnedItems();\n\n        return items.contains(item);\n    }\n\n    public void moveSectionUp(BrowseSection section) {\n        mCurrentSection = section; // move current focus\n        getSidebarService().moveSectionUp(section.getId());\n        updateSections();\n    }\n\n    public void moveSectionDown(BrowseSection section) {\n        mCurrentSection = section; // move current focus\n        getSidebarService().moveSectionDown(section.getId());\n        updateSections();\n    }\n\n    public void renameSection(BrowseSection section) {\n        mCurrentSection = section; // move current focus\n        getSidebarService().renameSection(section.getId(), section.getTitle());\n        updateSections();\n    }\n\n    public void renameSection(Video section) {\n        getSidebarService().renameSection(section.getId(), section.getTitle());\n        updateSections();\n    }\n\n    public void enableAllSections(boolean enable) {\n        enableSection(MediaGroup.TYPE_HISTORY, enable);\n        enableSection(MediaGroup.TYPE_USER_PLAYLISTS, enable);\n        enableSection(MediaGroup.TYPE_SUBSCRIPTIONS, enable);\n        enableSection(MediaGroup.TYPE_CHANNEL_UPLOADS, enable);\n        enableSection(MediaGroup.TYPE_GAMING, enable);\n        enableSection(MediaGroup.TYPE_MUSIC, enable);\n        enableSection(MediaGroup.TYPE_NEWS, enable);\n        enableSection(MediaGroup.TYPE_HOME, enable);\n        enableSection(MediaGroup.TYPE_TRENDING, enable);\n        enableSection(MediaGroup.TYPE_SHORTS, enable);\n    }\n\n    public void enableSection(int sectionId, boolean enable) {\n        getSidebarService().enableSection(sectionId, enable);\n\n        if (!enable && mCurrentSection != null && mCurrentSection.getId() == sectionId) {\n            mCurrentSection = findNearestSection(sectionId);\n        }\n\n        updateSections();\n    }\n\n    public void pinItem(Video item) {\n        if (getView() == null) {\n            return;\n        }\n\n        int idx = getSidebarService().addPinnedItem(item);\n\n        createPinnedMapping(item);\n\n        BrowseSection newSection = createPinnedSection(item);\n        if (!mSections.contains(newSection)) {\n            if (idx != -1) {\n                mSections.add(idx, newSection);\n            } else {\n                mSections.add(newSection);\n            }\n        }\n        getView().addSection(idx, newSection);\n    }\n\n    public void pinItem(String title, int resId, ErrorFragmentData data) {\n        if (getView() == null) {\n            return;\n        }\n\n        BrowseSection newSection = new BrowseSection(title.hashCode(), title, BrowseSection.TYPE_ERROR, resId, false, data);\n        Helpers.removeIf(mErrorSections, section -> section.getId() == newSection.getId());\n        mErrorSections.add(newSection);\n        getView().addSection(0, newSection);\n    }\n\n    private void appendToSections(String title, int resId, ErrorFragmentData data) {\n        int id = title.hashCode();\n        Helpers.removeIf(mSections, section -> section.getId() == id);\n        mSections.add(new BrowseSection(id, title, BrowseSection.TYPE_ERROR, resId, false, data));\n    }\n\n    public void unpinItem(Video item) {\n        getSidebarService().removePinnedItem(item);\n        getGeneralData().removeSelectedItem(item.getId());\n\n        BrowseSection section = null;\n\n        for (BrowseSection cat : mSections) {\n            if (cat.getId() == item.getId()) {\n                section = cat;\n                break;\n            }\n        }\n\n        mGridMapping.remove(item.getId());\n\n        if (getView() != null) {\n            getView().removeSection(section);\n        }\n    }\n\n    public void refresh() {\n        refresh(true);\n    }\n\n    public void refresh(boolean focusOnContent) {\n        updateCurrentSection();\n        if (focusOnContent && getView() != null) {\n            getView().focusOnContent();\n        }\n    }\n\n    private void updateRefreshTime() {\n        mLastUpdateTimeMs = System.currentTimeMillis();\n    }\n\n    private void updateCurrentSection() {\n        disposeActions();\n\n        if (getView() == null || mCurrentSection == null) {\n            return;\n        }\n\n        Log.d(TAG, \"Update section %s\", mCurrentSection.getTitle());\n        updateSection(mCurrentSection);\n    }\n\n    private void updateSection(BrowseSection section) {\n        switch (section.getType()) {\n            case BrowseSection.TYPE_GRID:\n            case BrowseSection.TYPE_SHORTS_GRID:\n                if (mGridMapping.containsKey(section.getId())) {\n                    Observable<MediaGroup> group = mGridMapping.get(section.getId());\n                    updateVideoGrid(section, group, section.isAuthOnly());\n                } else if (mLocalGridMappings.containsKey(section.getId())) {\n                    Callable<List<Video>> localVideos = mLocalGridMappings.get(section.getId());\n                    updateLocalGrid(section, localVideos);\n                }\n                break;\n            case BrowseSection.TYPE_ROW:\n                Observable<List<MediaGroup>> groups = mRowMapping.get(section.getId());\n                updateVideoRows(section, groups, section.isAuthOnly());\n                break;\n            case BrowseSection.TYPE_SETTINGS_GRID:\n                Callable<List<SettingsItem>> items = mSettingsGridMapping.get(section.getId());\n                updateSettingsGrid(section, items);\n                break;\n            case BrowseSection.TYPE_MULTI_GRID:\n                Observable<MediaGroup> group2 = mGridMapping.get(section.getId());\n                updateVideoGrid(section, group2, 0, section.isAuthOnly());\n                break;\n            case BrowseSection.TYPE_ERROR:\n                getView().showProgressBar(false);\n                break;\n        }\n\n        updateRefreshTime();\n    }\n\n    private void updateSettingsGrid(BrowseSection section, Callable<List<SettingsItem>> items) {\n        getView().updateSection(SettingsGroup.from(Helpers.get(items), section));\n        getView().showProgressBar(false);\n    }\n\n    private void updateLocalGrid(BrowseSection section, Callable<List<Video>> items) {\n        VideoGroup videoGroup = VideoGroup.from(Helpers.get(items), section);\n        videoGroup.setAction(VideoGroup.ACTION_REPLACE);\n        videoGroup.setId(videoGroup.hashCode());\n        videoGroup.setTitle(section.getTitle());\n        getView().updateSection(videoGroup);\n        getView().showProgressBar(false);\n    }\n\n    private void updateVideoRows(BrowseSection section, Observable<List<MediaGroup>> groups, boolean authCheck) {\n        Log.d(TAG, \"loadRowsHeader: Start loading section: \" + section.getTitle());\n\n        authCheck(authCheck, () -> updateVideoRows(section, groups));\n    }\n\n    private void updateVideoGrid(BrowseSection section, Observable<MediaGroup> group, boolean authCheck) {\n        updateVideoGrid(section, group, -1, authCheck);\n    }\n\n    private void updateVideoGrid(BrowseSection section, Observable<MediaGroup> group, int column, boolean authCheck) {\n        Log.d(TAG, \"loadMultiGridHeader: Start loading section: \" + section.getTitle());\n\n        authCheck(authCheck, () -> updateVideoGrid(section, group, column));\n    }\n\n    private void updateVideoRows(BrowseSection section, Observable<List<MediaGroup>> groups) {\n        Log.d(TAG, \"updateRowsHeader: Start loading section: \" + section.getTitle());\n\n        disposeActions();\n\n        if (getView() == null) {\n            Log.e(TAG, \"Browse view has been unloaded from the memory. Low RAM?\");\n            getViewManager().startView(BrowseView.class);\n            return;\n        }\n        \n        getView().showProgressBar(true);\n\n        VideoGroup firstGroup = VideoGroup.from(section);\n        firstGroup.setAction(VideoGroup.ACTION_REPLACE);\n        getView().updateSection(firstGroup);\n\n        if (groups == null) {\n            // No group. Maybe just clear.\n            getView().showProgressBar(false);\n            return;\n        }\n\n        Disposable updateAction = groups\n                .subscribe(\n                        mediaGroups -> {\n                            getView().showProgressBar(false);\n\n                            filterHomeIfNeeded(mediaGroups);\n\n                            for (MediaGroup mediaGroup : mediaGroups) {\n                                if (mediaGroup.isEmpty()) {\n                                    Log.e(TAG, \"loadRowsHeader: MediaGroup is empty. Group Name: \" + mediaGroup.getTitle());\n                                    continue;\n                                }\n\n                                VideoGroup videoGroup = VideoGroup.from(mediaGroup, section);\n\n                                if (TextUtils.isEmpty(videoGroup.getTitle())) {\n                                    videoGroup.setTitle(getContext().getString(R.string.suggestions));\n                                }\n\n                                getView().updateSection(videoGroup);\n                                mBrowseProcessor.process(videoGroup);\n\n                                continueGroupIfNeeded(videoGroup, false);\n                            }\n                        },\n                        error -> {\n                            Log.e(TAG, \"updateRowsHeader error: %s\", error.getMessage());\n                            handleLoadError(error);\n                        }, () -> handleLoadError(null));\n\n        mActions.add(updateAction);\n    }\n\n    private void updateVideoGrid(BrowseSection section, Observable<MediaGroup> group, int column) {\n        disposeActions();\n\n        if (getView() == null) {\n            Log.e(TAG, \"Browse view has been unloaded from the memory. Low RAM?\");\n            getViewManager().startView(BrowseView.class);\n            return;\n        }\n\n        Log.d(TAG, \"updateGridHeader: Start loading section: \" + section.getTitle());\n\n        getView().showProgressBar(true);\n\n        // Stay on the same group in case of multiple subscribe calls\n        VideoGroup baseGroup = VideoGroup.from(section, column);\n        baseGroup.setAction(VideoGroup.ACTION_REPLACE);\n        getView().updateSection(baseGroup);\n\n        if (group == null) {\n            // No group. Maybe just clear.\n            getView().showProgressBar(false);\n            return;\n        }\n\n        Disposable updateAction = group\n                .subscribe(\n                        mediaGroup -> {\n                            getView().showProgressBar(false);\n\n                            if (getView() == null) {\n                                Log.e(TAG, \"Browse view has been unloaded from the memory. Low RAM?\");\n                                getViewManager().startView(BrowseView.class);\n                                return;\n                            }\n\n                            VideoGroup videoGroup = VideoGroup.from(baseGroup, mediaGroup);\n                            appendLocalHistory(videoGroup);\n                            getView().updateSection(videoGroup);\n                            mBrowseProcessor.process(videoGroup);\n\n                            continueGroupIfNeeded(videoGroup);\n                        },\n                        error -> {\n                            Log.e(TAG, \"updateGridHeader error: %s\", error.getMessage());\n                            handleLoadError(error);\n                        }, () -> handleLoadError(null));\n\n        mActions.add(updateAction);\n    }\n\n    private void continueGroup(VideoGroup group) {\n        continueGroup(group, true);\n    }\n\n    private void continueGroup(VideoGroup group, boolean showLoading) {\n        if (getView() == null) {\n            Log.e(TAG, \"Can't continue group. The view is null.\");\n            return;\n        }\n\n        if (group == null) {\n            Log.e(TAG, \"Can't continue group. The group is null.\");\n            return;\n        }\n\n        if (getCurrentSection() != null && mLocalGridMappings.containsKey(getCurrentSection().getId())) {\n            Log.d(TAG, \"Local grid section doesn't assume a continuation...\");\n            return;\n        }\n\n        Log.d(TAG, \"continueGroup: start continue group: \" + group.getTitle());\n\n        // Small amount of items == small load time. Loading bar are useless?\n        if (showLoading) {\n            getView().showProgressBar(true);\n        }\n\n        MediaGroup mediaGroup = group.getMediaGroup();\n\n        Observable<MediaGroup> continuation;\n\n        //if (mediaGroup.getType() == MediaGroup.TYPE_SUGGESTIONS) { // Pinned playlist\n        //    continuation = mItemService.continueGroupObserve(mediaGroup);\n        //} else {\n        //    continuation = getContentService().continueGroupObserve(mediaGroup);\n        //}\n\n        continuation = getContentService().continueGroupObserve(mediaGroup);\n\n        Disposable continueAction = continuation\n                .subscribe(\n                        continueGroup -> {\n                            getView().showProgressBar(false);\n\n                            VideoGroup videoGroup = VideoGroup.from(group, continueGroup);\n                            getView().updateSection(videoGroup);\n                            mBrowseProcessor.process(videoGroup);\n\n                            continueGroupIfNeeded(videoGroup, showLoading);\n                        },\n                        error -> {\n                            Log.e(TAG, \"continueGroup error: %s\", error.getMessage());\n                            if (getView() != null) {\n                                getView().showProgressBar(false);\n                            }\n                        },\n                        () -> {\n                            if (getView() != null) {\n                                getView().showProgressBar(false);\n                            }\n                        }\n                );\n\n        mActions.add(continueAction);\n    }\n\n    private void authCheck(boolean check, Runnable callback) {\n        if (!check) {\n            callback.run();\n            return;\n        }\n\n        getView().showProgressBar(true);\n\n        if (getSignInService().isSigned()) {\n            callback.run();\n        } else if (getView() != null) {\n            if (isHistorySection() && !VideoStateService.instance(getContext()).isEmpty()) {\n                getView().showProgressBar(false);\n                VideoGroup videoGroup = VideoGroup.from(getCurrentSection());\n                appendLocalHistory(videoGroup);\n                getView().updateSection(videoGroup);\n            } else {\n                getView().showProgressBar(false);\n                getView().showError(new SignInError(getContext()));\n            }\n        }\n    }\n\n    /**\n     * Most tiny ui has 8 cards in a row or 24 in grid.\n     */\n    private void continueGroupIfNeeded(VideoGroup group) {\n        continueGroupIfNeeded(group, true);\n    }\n\n    /**\n     * Most tiny ui has 8 cards in a row or 24 in grid.\n     */\n    private void continueGroupIfNeeded(VideoGroup group, boolean showLoading) {\n        if (MediaServiceManager.instance().shouldContinueTheGroup(getContext(), group, isGridSection())) {\n            continueGroup(group, showLoading);\n        }\n    }\n\n    private void disposeActions() {\n        RxHelper.disposeActions(mActions);\n        Utils.removeCallbacks(mRefreshSection);\n        mLastUpdateTimeMs = -1;\n        mBrowseProcessor.dispose();\n    }\n\n    private void updateChannelUploadsMultiGrid(Video item) {\n        if (mCurrentSection == null) {\n            return;\n        }\n\n        updateVideoGrid(mCurrentSection, ChannelUploadsPresenter.instance(getContext()).obtainUploadsObservable(item), 1, false);\n    }\n\n    private boolean belongsToChannelUploadsMultiGrid(Video item) {\n        return isMultiGridChannelUploadsSection() && belongsToChannelUploads(item);\n    }\n\n    private boolean belongsToChannelUploads(Video item) {\n        return item.belongsToChannelUploads() && !item.hasVideo();\n    }\n\n    @Nullable\n    public BrowseSection getCurrentSection() {\n        return mCurrentSection;\n    }\n\n    private BrowseSection findSectionById(int sectionId) {\n        for (BrowseSection section : mErrorSections) {\n            if (section.getId() == sectionId) {\n                return section;\n            }\n        }\n\n        for (BrowseSection section : mSections) {\n            if (section.getId() == sectionId) {\n                return section;\n            }\n        }\n\n        return null;\n    }\n\n    private int findSectionIndex(int sectionId) {\n        if (sectionId == -1) {\n            return -1;\n        }\n\n        int sectionIndex = -1;\n\n        for (BrowseSection section : mErrorSections) {\n            if (section.isEnabled()) {\n                sectionIndex++;\n                if (section.getId() == sectionId) {\n                    return sectionIndex;\n                }\n            }\n        }\n\n        for (BrowseSection section : mSections) {\n            if (section.isEnabled()) {\n                sectionIndex++;\n                if (section.getId() == sectionId) {\n                    return sectionIndex;\n                }\n            }\n        }\n\n        return -1;\n    }\n\n    private BrowseSection findNearestSection(int sectionId) {\n        BrowseSection result = findNearestSection(mErrorSections, sectionId);\n\n        if (result == null) {\n            result = findNearestSection(mSections, sectionId);\n        }\n\n        return result;\n    }\n\n    private BrowseSection findNearestSection(List<BrowseSection> sections, int sectionId) {\n        BrowseSection result = null;\n        BrowseSection previousSection = null;\n        boolean found = false;\n        for (BrowseSection section : sections) {\n            if (section.getId() == sectionId) {\n                found = true;\n                continue;\n            }\n            if (section.isEnabled()) {\n                if (found) {\n                    result = section;\n                    break;\n                }\n                previousSection = section;\n            }\n        }\n\n        return result != null ? result : previousSection;\n    }\n\n    private void filterHomeIfNeeded(List<MediaGroup> mediaGroups) {\n        if (mediaGroups == null || !isHomeSection()) {\n            return;\n        }\n\n        Helpers.removeIf(mediaGroups, value -> Helpers.containsAny(\n                value.getTitle(),\n                \"Primetime\", // Free movies and shows row\n                \"News\", // Top news\n                \"news\", // Top news\n                \"NBA TV\", // Sports\n                \"The Life of a Showgirl\" // Taylor Swift ADS\n        ) || Helpers.equalsAny(\n                value.getTitle(),\n                //getContext().getString(R.string.news_row_name),\n                getContext().getString(R.string.breaking_news_row_name),\n                getContext().getString(R.string.covid_news_row_name)\n        ));\n    }\n\n    private int moveToTopIfNeeded(MediaGroup mediaGroup) {\n        if (mediaGroup == null) {\n            return -1;\n        }\n\n        return Helpers.equalsAny(mediaGroup.getTitle(), getContext().getString(R.string.trending_row_name)) ? 0 : -1;\n    }\n\n    private Observable<MediaGroup> createPinnedGridAction(Video item) {\n        if (item.channelGroupId != null) {\n            return getContentService().getRssFeedObserve(ChannelGroupServiceWrapper.instance(getContext()).findChannelIdsForGroup(item.channelGroupId));\n        }\n\n        return ChannelUploadsPresenter.instance(getContext()).obtainUploadsObservable(item);\n    }\n\n    private Observable<List<MediaGroup>> createPinnedRowAction(Video item) {\n        return ChannelPresenter.instance(getContext()).obtainChannelObservable(item.channelId);\n    }\n\n    /**\n     * Is Channels new look enabled?\n     */\n    public boolean isMultiGridChannelUploadsSection() {\n        return mCurrentSection != null && mCurrentSection.getType() == BrowseSection.TYPE_MULTI_GRID && mCurrentSection.getId() == MediaGroup.TYPE_CHANNEL_UPLOADS;\n    }\n\n    public boolean isSettingsSection() {\n        return isSection(MediaGroup.TYPE_SETTINGS);\n    }\n\n    public boolean isPlaylistsSection() {\n        return isSection(MediaGroup.TYPE_USER_PLAYLISTS);\n    }\n\n    public boolean isHomeSection() {\n        return isSection(MediaGroup.TYPE_HOME);\n    }\n\n    public boolean isHistorySection() {\n        return isSection(MediaGroup.TYPE_HISTORY);\n    }\n\n    public boolean isSubscriptionsSection() {\n        return isSection(MediaGroup.TYPE_SUBSCRIPTIONS);\n    }\n    \n    public boolean isPlaybackQueueSection() {\n        return isSection(MediaGroup.TYPE_PLAYBACK_QUEUE);\n    }\n\n    public boolean isPinnedSection() {\n        return mCurrentSection != null && isPinnedId(mCurrentSection.getId());\n    }\n\n    private boolean isPinnedId(int id) {\n        return id > 100;\n    }\n\n    private boolean isSection(int sectionId) {\n        return mCurrentSection != null && mCurrentSection.getId() == sectionId;\n    }\n\n    public void selectSection(int sectionId) {\n        getViewManager().startView(BrowseView.class); // focus view\n\n        if (getView() == null) {\n            mBootstrapSectionId = sectionId;\n            return;\n        }\n\n        int sectionIndex = findSectionIndex(sectionId);\n\n        if (sectionIndex == -1) {\n            enableSection(sectionId, true);\n            sectionIndex = findSectionIndex(sectionId);\n            getSidebarService().enableSection(sectionId, false); // enable temporally (till restart)\n        }\n\n        if (sectionIndex != -1) {\n            getView().selectSection(sectionIndex, true);\n        }\n    }\n\n    public boolean inForeground() {\n        return getViewManager().getTopView() == BrowseView.class;\n    }\n\n    private boolean isGridSection() {\n        return mCurrentSection != null && mCurrentSection.getType() != BrowseSection.TYPE_ROW;\n    }\n\n    @Override\n    public void onAccountChanged(Account account) {\n        Log.d(TAG, \"On account changed\");\n\n        if (getView() == null) {\n            return;\n        }\n\n        initSectionMappings();\n        updateChannelSorting();\n        updatePlaylistsStyle();\n        updateSections();\n    }\n\n    public Video getCurrentVideo() {\n        return mCurrentVideo;\n    }\n\n    private void initPasswordSection() {\n        AccountsData accountsData = AccountsData.instance(getContext());\n        if (accountsData.getAccountPassword() == null || accountsData.isPasswordAccepted()) {\n            return;\n        }\n\n        mSections.clear();\n        appendToSections(getContext().getString(R.string.header_notifications), R.drawable.icon_notification, new PasswordError(getContext()));\n    }\n\n    private void createPinnedMapping(Video item) {\n        if (enableRows(item)) {\n            mRowMapping.put(item.getId(), createPinnedRowAction(item));\n        } else {\n            mGridMapping.put(item.getId(), createPinnedGridAction(item));\n        }\n    }\n\n    private BrowseSection createPinnedSection(Video item) {\n        return new BrowseSection(\n                item.getId(), item.getTitle(), enableRows(item) ? BrowseSection.TYPE_ROW : BrowseSection.TYPE_GRID, R.drawable.icon_pin, item.getCardImageUrl(), false, item);\n    }\n\n    private boolean enableRows(Video item) {\n        return getMainUIData().isPinnedChannelRowsEnabled() && item.hasChannel() && !item.isPlaylistAsChannel();\n    }\n\n    private void handleLoadError(Throwable error) {\n        if (getView() == null) {\n            return;\n        }\n\n        getView().showProgressBar(false);\n\n        if (getView().isEmpty() || error != null) {\n            ErrorFragmentData errorFragmentData;\n            if (error != null && !Helpers.containsAny(error.getMessage(), \"fromNullable result is null\")) {\n                errorFragmentData = new CategoryEmptyError(getContext(), error);\n            } else if (getSignInService().isSigned()) {\n                errorFragmentData = new CategoryEmptyError(getContext(), null);\n            } else {\n                errorFragmentData = new SignInError(getContext());\n            }\n\n            // TODO: should we find a better place e.g. RetrofitHelper\n            // java.net.UnknownHostException: Unable to resolve host \"www.youtube.com\": No address associated with hostname\n            if (error != null && Helpers.contains(error.getMessage(), \"No address associated with hostname\")) {\n                PlayerTweaksData playerTweaksData = PlayerTweaksData.instance(getContext());\n                if (playerTweaksData.getPreferredDnsType() != PlayerTweaksData.DNS_TYPE_IPV4) {\n                    playerTweaksData.setPreferredDnsType(PlayerTweaksData.DNS_TYPE_IPV4);\n                    // Restart app to reinit okhttp internal objects\n                    Utils.restartTheApp(getContext());\n                }\n            }\n\n            getView().showError(errorFragmentData);\n            Utils.postDelayed(mRefreshSection, 30_000);\n        }\n    }\n\n    private void appendLocalHistory(VideoGroup videoGroup) {\n        if (!isHistorySection()) {\n            return;\n        }\n\n        VideoStateService stateService = VideoStateService.instance(getContext());\n\n        if (stateService.isEmpty() || (!stateService.isHistoryBroken() && !videoGroup.isEmpty())) {\n            return;\n        }\n\n        Video lastHistoryItem = videoGroup.isEmpty() ? null : videoGroup.get(0);\n        State lastState = stateService.getLastState();\n\n        if (lastState == null || Helpers.equals(lastHistoryItem, lastState.video)) {\n            return;\n        }\n\n        for (State state : stateService.getStates()) {\n            if (lastHistoryItem == null || state.timestamp > stateService.getSessionStartTimeMs()) {\n                videoGroup.add(0, state.video);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/ChannelPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItem;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.VideoActionPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.interfaces.VideoGroupPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ChannelView;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.BrowseProcessorManager;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.LoadingManager;\r\nimport io.reactivex.Observable;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class ChannelPresenter extends BasePresenter<ChannelView> implements VideoGroupPresenter {\r\n    private static final String TAG = ChannelPresenter.class.getSimpleName();\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static ChannelPresenter sInstance;\r\n    private final BrowseProcessorManager mBrowseProcessor;\r\n    private String mChannelId;\r\n    private final List<List<MediaGroup>> mPendingGroups = new ArrayList<>();\r\n    private Disposable mUpdateAction;\r\n    private Disposable mScrollAction;\r\n    private int mSortIdx;\r\n    private Video mChannel;\r\n\r\n    private interface OnChannelId {\r\n        void onChannelId(String channelId);\r\n    }\r\n\r\n    public interface OnUploadsRow {\r\n        void onUploadsRow(Observable<MediaGroup> row);\r\n    }\r\n\r\n    public ChannelPresenter(Context context) {\r\n        super(context);\r\n        mBrowseProcessor = new BrowseProcessorManager(getContext(), this::syncItem);\r\n    }\r\n\r\n    public static ChannelPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new ChannelPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    @Override\r\n    public void onViewInitialized() {\r\n        super.onViewInitialized();\r\n\r\n        if (mChannelId != null) {\r\n            getView().clear();\r\n            updateRows(obtainChannelObservable(mChannelId));\r\n        } else if (!mPendingGroups.isEmpty()) {\r\n            getView().clear();\r\n            for (List<MediaGroup> group : mPendingGroups) {\r\n                updateRows(group);\r\n            }\r\n            mPendingGroups.clear();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onViewDestroyed() {\r\n        super.onViewDestroyed();\r\n        disposeActions();\r\n    }\r\n\r\n    @Override\r\n    public void onFinish() {\r\n        super.onFinish();\r\n\r\n        // Destroy the cache only (!) when user pressed back (e.g. wants to explicitly kill the activity)\r\n        // Otherwise keep the cache to easily restore in case activity is killed by the system.\r\n        mChannelId = null;\r\n        mPendingGroups.clear();\r\n        disposeActions();\r\n    }\r\n\r\n    @Override\r\n    public void onVideoItemSelected(Video item) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onVideoItemClicked(Video item) {\r\n        VideoActionPresenter.instance(getContext()).apply(item);\r\n    }\r\n\r\n    @Override\r\n    public void onVideoItemLongClicked(Video item) {\r\n        VideoMenuPresenter.instance(getContext()).showMenu(item);\r\n    }\r\n\r\n    @Override\r\n    public void onScrollEnd(Video item) {\r\n        if (item == null) {\r\n            Log.e(TAG, \"Can't scroll. Video is null.\");\r\n            return;\r\n        }\r\n\r\n        if (item.getGroup() == null) {\r\n            Log.e(TAG, \"Can't scroll. Video group is null.\");\r\n            return;\r\n        }\r\n\r\n        VideoGroup group = item.getGroup();\r\n\r\n        Log.d(TAG, \"onScrollEnd: Group title: \" + group.getTitle());\r\n\r\n        continueGroup(group);\r\n    }\r\n\r\n    @Override\r\n    public boolean hasPendingActions() {\r\n        return RxHelper.isAnyActionRunning(mScrollAction, mUpdateAction);\r\n    }\r\n\r\n    public static boolean canOpenChannel(Video item) {\r\n        if (item == null) {\r\n            return false;\r\n        }\r\n\r\n        return item.videoId != null || item.channelId != null || item.belongsToChannelUploads();\r\n    }\r\n\r\n    public void openChannel(Video item) {\r\n        mChannel = item;\r\n        extractChannelId(item, this::openChannel);\r\n    }\r\n\r\n    public void openChannel(String channelId) {\r\n        if (channelId == null) {\r\n            return;\r\n        }\r\n\r\n        disposeActions();\r\n\r\n        mChannelId = channelId;\r\n\r\n        if (getView() != null) {\r\n            getView().clear();\r\n            updateRows(obtainChannelObservable(channelId));\r\n            // Fix double results. Prevent from doing the same in onViewInitialized()\r\n            //mChannelId = null;\r\n        }\r\n\r\n        getViewManager().startView(ChannelView.class);\r\n    }\r\n\r\n    public String getChannelId() {\r\n        return mChannel != null && mChannel.channelId != null ? mChannel.channelId : mChannelId;\r\n    }\r\n\r\n    public void setChannelId(String channelId) {\r\n        mChannelId = channelId;\r\n    }\r\n\r\n    public Video getChannel() {\r\n        return mChannel;\r\n    }\r\n\r\n    public void setChannel(Video channel) {\r\n        mChannel = channel;\r\n    }\r\n\r\n    private void disposeActions() {\r\n        RxHelper.disposeActions(mUpdateAction, mScrollAction);\r\n        getServiceManager().disposeActions();\r\n        mSortIdx = 0;\r\n        mBrowseProcessor.dispose();\r\n    }\r\n\r\n    private void updateRows(Observable<List<MediaGroup>> group) {\r\n        Log.d(TAG, \"updateRows: Start loading...\");\r\n\r\n        disposeActions();\r\n\r\n        getView().showProgressBar(true);\r\n\r\n        mUpdateAction = group\r\n                .subscribe(\r\n                        this::updateRows,\r\n                        error -> {\r\n                            Log.e(TAG, \"updateRows error: %s\", error.getMessage());\r\n                            getView().showProgressBar(false);\r\n                        }\r\n                 );\r\n    }\r\n\r\n    public Observable<List<MediaGroup>> obtainChannelObservable(String channelId) {\r\n        return getContentService().getChannelObserve(channelId);\r\n    }\r\n\r\n    public void updateRows(List<MediaGroup> mediaGroups) {\r\n        if (getView() == null) { // starting from outside (e.g. MediaServiceManager)\r\n            mChannelId = null;\r\n            mPendingGroups.add(mediaGroups);\r\n            getViewManager().startView(ChannelView.class);\r\n            return;\r\n        }\r\n\r\n        // The view could be running in the background\r\n        getViewManager().startView(ChannelView.class);\r\n\r\n        for (MediaGroup mediaGroup : mediaGroups) {\r\n            if (mediaGroup.getMediaItems() == null) {\r\n                Log.e(TAG, \"updateRowsHeader: MediaGroup is empty. Group Name: \" + mediaGroup.getTitle());\r\n                continue;\r\n            }\r\n\r\n            VideoGroup group = VideoGroup.from(mediaGroup);\r\n            getView().update(group);\r\n            mBrowseProcessor.process(group);\r\n        }\r\n\r\n        getView().showProgressBar(false);\r\n    }\r\n\r\n    private void continueGroup(VideoGroup group) {\r\n        boolean scrollInProgress = mScrollAction != null && !mScrollAction.isDisposed();\r\n\r\n        if (scrollInProgress) {\r\n            return;\r\n        }\r\n\r\n        if (getView() == null) {\r\n            Log.e(TAG, \"Can't continue group. The view is null.\");\r\n            return;\r\n        }\r\n\r\n        if (group == null) {\r\n            Log.e(TAG, \"Can't continue group. The group is null.\");\r\n            return;\r\n        }\r\n\r\n        Log.d(TAG, \"continueGroup: start continue group: \" + group.getTitle());\r\n\r\n        getView().showProgressBar(true);\r\n\r\n        MediaGroup mediaGroup = group.getMediaGroup();\r\n\r\n        mScrollAction = getContentService().continueGroupObserve(mediaGroup)\r\n                .subscribe(\r\n                        continueMediaGroup -> {\r\n                            VideoGroup newGroup = VideoGroup.from(group, continueMediaGroup);\r\n                            getView().update(newGroup);\r\n                            mBrowseProcessor.process(newGroup);\r\n                        },\r\n                        error -> {\r\n                            Log.e(TAG, \"continueGroup error: %s\", error.getMessage());\r\n                            if (getView() != null) {\r\n                                getView().showProgressBar(false);\r\n                            }\r\n                        },\r\n                        () -> getView().showProgressBar(false)\r\n                );\r\n    }\r\n\r\n    /**\r\n     * Sort channel content: move Uploads on top.\r\n     */\r\n    private void moveToTopIfNeeded(List<MediaGroup> mediaGroups) {\r\n        moveToTop(mediaGroups, R.string.playlists_row_name);\r\n        moveToTop(mediaGroups, R.string.popular_uploads_row_name);\r\n        moveToTop(mediaGroups, R.string.uploads_row_name);\r\n        moveToTop(mediaGroups, R.string.live_now_row_name);\r\n    }\r\n\r\n    private void moveToTop(List<MediaGroup> mediaGroups, int rowNameResId) {\r\n        if (rowNameResId <= 0) {\r\n            return;\r\n        }\r\n\r\n        String rowName = getContext().getString(rowNameResId);\r\n\r\n        List<MediaGroup> group = Helpers.removeIf(mediaGroups, value -> rowName.equals(value.getTitle()));\r\n\r\n        if (group != null) {\r\n            mediaGroups.addAll(0, group);\r\n        }\r\n    }\r\n\r\n    public void clear() {\r\n        if (getView() != null) {\r\n            getView().clear();\r\n        }\r\n        mChannel = null;\r\n        mChannelId = null;\r\n    }\r\n\r\n    private void extractChannelId(Video item, OnChannelId callback) {\r\n        if (item != null) {\r\n            if (item.channelId != null) {\r\n                callback.onChannelId(item.channelId);\r\n            } else if (item.videoId != null) {\r\n                LoadingManager.showLoading(getContext(), true);\r\n                getServiceManager().loadMetadata(item, metadata -> {\r\n                    LoadingManager.showLoading(getContext(), false);\r\n                    callback.onChannelId(metadata.getChannelId());\r\n                    item.channelId = metadata.getChannelId();\r\n                },\r\n                e -> LoadingManager.showLoading(getContext(), false),\r\n                () -> LoadingManager.showLoading(getContext(), false));\r\n            } else if (item.belongsToChannelUploads()) {\r\n                LoadingManager.showLoading(getContext(), true);\r\n                // Maybe this is subscribed items view\r\n                ChannelUploadsPresenter.instance(getContext())\r\n                        .obtainGroup(item, group -> {\r\n                            LoadingManager.showLoading(getContext(), false);\r\n                            // Some uploads groups doesn't contain channel button.\r\n                            // Use data from first item instead.\r\n                            if (group.getChannelId() == null) {\r\n                                List<MediaItem> mediaItems = group.getMediaItems();\r\n\r\n                                // Filter collaborative items\r\n                                MediaItem first = Helpers.findFirst(mediaItems, mediaItem -> Helpers.startsWith(mediaItem.getAuthor(), item.getAuthor()));\r\n\r\n                                if (first == null && mediaItems != null && !mediaItems.isEmpty()) {\r\n                                    first = mediaItems.get(0);\r\n                                }\r\n\r\n                                if (first != null) {\r\n                                    extractChannelId(Video.from(first), callback);\r\n                                }\r\n\r\n                                return;\r\n                            }\r\n\r\n                            callback.onChannelId(group.getChannelId());\r\n                            item.channelId = group.getChannelId();\r\n                        },\r\n                        e -> LoadingManager.showLoading(getContext(), false),\r\n                        () -> LoadingManager.showLoading(getContext(), false));\r\n            }\r\n        }\r\n    }\r\n\r\n    public void onSearchSettingsClicked() {\r\n        Observable<List<MediaGroup>> sorting = getContentService().getChannelSortingOptionsObserve(getChannelId());\r\n        Disposable result = sorting.subscribe(\r\n                items -> {\r\n                    AppDialogPresenter dialogPresenter = AppDialogPresenter.instance(getContext());\r\n                    List<OptionItem> options = new ArrayList<>();\r\n                    int idx = 0;\r\n                    for (MediaGroup group : items) {\r\n                        final int tempIdx = idx;\r\n                        options.add(UiOptionItem.from(group.getTitle(), item -> {\r\n                            //dialogPresenter.closeDialog();\r\n                            Observable<MediaGroup> continuation = getContentService().continueGroupObserve(group);\r\n                            Disposable result2 = continuation.subscribe(mediaGroup -> {\r\n                                if (getView() == null) {\r\n                                    return;\r\n                                }\r\n\r\n                                VideoGroup replace = VideoGroup.from(mediaGroup);\r\n                                replace.setId(144);\r\n                                replace.setPosition(0);\r\n                                replace.setAction(VideoGroup.ACTION_REPLACE);\r\n                                getView().update(replace);\r\n                                //getView().setPosition(1);\r\n                                mSortIdx = tempIdx;\r\n                            });\r\n                        }, mSortIdx == idx));\r\n                        idx++;\r\n                    }\r\n                    dialogPresenter.appendRadioCategory(getContext().getString(R.string.search_sorting), options);\r\n                    dialogPresenter.showDialog();\r\n                },\r\n                error -> Log.e(TAG, \"onSearchSettingsClicked error: %s\", error.getMessage())\r\n        );\r\n    }\r\n\r\n    public boolean onSearchSubmit(String query) {\r\n        Observable<MediaGroup> search = getContentService().getChannelSearchObserve(getChannelId(), query);\r\n        Disposable result = search.subscribe(\r\n                items -> {\r\n                    if (getView() == null) {\r\n                        return;\r\n                    }\r\n\r\n                    VideoGroup update = VideoGroup.from(items);\r\n\r\n                    if (update.isEmpty()) {\r\n                        MessageHelpers.showMessage(getContext(), R.string.nothing_found);\r\n                        return;\r\n                    }\r\n\r\n                    update.setId(112);\r\n                    update.setPosition(0);\r\n                    update.setAction(VideoGroup.ACTION_REPLACE);\r\n                    getView().update(update);\r\n                    getView().setPosition(1);\r\n                },\r\n                error -> Log.e(TAG, \"onSearchSubmit error: %s\", error.getMessage())\r\n        );\r\n\r\n        return true;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/ChannelUploadsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport android.text.TextUtils;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItem;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.SimpleMediaItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.VideoActionPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter.VideoMenuCallback;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.interfaces.VideoGroupPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ChannelUploadsView;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.BrowseProcessorManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager.OnComplete;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager.OnError;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager.OnMediaGroup;\r\n\r\nimport io.reactivex.Observable;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\nimport java.util.List;\r\n\r\npublic class ChannelUploadsPresenter extends BasePresenter<ChannelUploadsView> implements VideoGroupPresenter {\r\n    private static final String TAG = ChannelUploadsPresenter.class.getSimpleName();\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static ChannelUploadsPresenter sInstance;\r\n    private final BrowseProcessorManager mBrowseProcessor;\r\n    private Disposable mUpdateAction;\r\n    private Disposable mScrollAction;\r\n    private Video mChannel;\r\n    private MediaGroup mPendingGroup;\r\n    private VideoGroup mBaseGroup;\r\n\r\n    public ChannelUploadsPresenter(Context context) {\r\n        super(context);\r\n        mBrowseProcessor = new BrowseProcessorManager(getContext(), this::syncItem);\r\n    }\r\n\r\n    public static ChannelUploadsPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new ChannelUploadsPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    @Override\r\n    public void onViewInitialized() {\r\n        super.onViewInitialized();\r\n\r\n        refresh();\r\n    }\r\n\r\n    @Override\r\n    public void onViewDestroyed() {\r\n        super.onViewDestroyed();\r\n        disposeActions();\r\n    }\r\n\r\n    @Override\r\n    public void onFinish() {\r\n        super.onFinish();\r\n\r\n        // Destroy the cache only (!) when user pressed back (e.g. wants to explicitly kill the activity)\r\n        // Otherwise keep the cache to easily restore in case activity is killed by the system.\r\n        disposeActions();\r\n        mChannel = null;\r\n        mPendingGroup = null;\r\n        mBaseGroup = null;\r\n    }\r\n\r\n    @Override\r\n    public void onVideoItemSelected(Video item) {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onVideoItemClicked(Video item) {\r\n        VideoActionPresenter.instance(getContext()).apply(item);\r\n    }\r\n\r\n    @Override\r\n    public void onVideoItemLongClicked(Video item) {\r\n        VideoMenuPresenter.instance(getContext()).showMenu(item, (videoItem, action) -> {\r\n            if (action == VideoMenuCallback.ACTION_REMOVE_FROM_PLAYLIST) {\r\n                removeItem(videoItem);\r\n            } else if (action == VideoMenuCallback.ACTION_UNSUBSCRIBE) {\r\n                MessageHelpers.showMessage(getContext(), R.string.unsubscribed_from_channel);\r\n            }\r\n        });\r\n    }\r\n\r\n    @Override\r\n    public void onScrollEnd(Video item) {\r\n        if (item == null) {\r\n            Log.e(TAG, \"Can't scroll. Video is null.\");\r\n            return;\r\n        }\r\n\r\n        VideoGroup group = item.getGroup();\r\n\r\n        if (group == null) {\r\n            Log.e(TAG, \"Can't scroll. VideoGroup is null.\");\r\n            return;\r\n        }\r\n\r\n        Log.d(TAG, \"onScrollEnd: Group title: \" + group.getTitle());\r\n\r\n        boolean scrollInProgress = mScrollAction != null && !mScrollAction.isDisposed();\r\n\r\n        if (!scrollInProgress) {\r\n            continueGroup(group);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public boolean hasPendingActions() {\r\n        return RxHelper.isAnyActionRunning(mScrollAction, mUpdateAction);\r\n    }\r\n\r\n    public void openChannel(Video item) {\r\n        // Working with uploads or playlists\r\n        if (item == null || (!item.hasNestedItems() && !item.hasPlaylist())) {\r\n            return;\r\n        }\r\n\r\n        clear();\r\n\r\n        mChannel = item;\r\n\r\n        getViewManager().startView(ChannelUploadsView.class);\r\n\r\n        if (getView() != null) {\r\n            update(item);\r\n        }\r\n    }\r\n\r\n    public void obtainGroup(Video item, OnMediaGroup callback) {\r\n        obtainGroup(item, callback, null, null);\r\n    }\r\n\r\n    public void obtainGroup(Video item, OnMediaGroup callback, OnError onError, OnComplete onComplete) {\r\n        if (item != null && item.mediaItem != null) {\r\n            obtainGroup(item.mediaItem, callback, onError, onComplete);\r\n        }\r\n    }\r\n\r\n    public Observable<MediaGroup> obtainUploadsObservable(Video item) {\r\n        if (item == null) {\r\n            return null;\r\n        }\r\n\r\n        if (item.mediaItem == null) {\r\n            item.mediaItem = SimpleMediaItem.from(item);\r\n        }\r\n\r\n        disposeActions();\r\n\r\n        return item.hasNestedItems() || item.isChannel() ?\r\n               getContentService().getGroupObserve(item.mediaItem != null ? item.mediaItem : SimpleMediaItem.from(item)) :\r\n               item.hasReloadPageKey() ?\r\n               getContentService().getGroupObserve(item.getReloadPageKey()) :\r\n               getMediaItemService().getMetadataObserve(item.videoId, item.playlistId, 0, item.playlistParams)\r\n                       .flatMap(mediaItemMetadata -> Observable.just(findPlaylistRow(mediaItemMetadata)));\r\n    }\r\n\r\n    public Video getChannel() {\r\n        return mChannel;\r\n    }\r\n\r\n    public void setChannel(Video channel) {\r\n        mChannel = channel;\r\n    }\r\n\r\n    private void disposeActions() {\r\n        RxHelper.disposeActions(mUpdateAction, mScrollAction);\r\n        MediaServiceManager.instance().disposeActions();\r\n        mBrowseProcessor.dispose();\r\n    }\r\n\r\n    private void continueGroup(VideoGroup group) {\r\n        disposeActions();\r\n\r\n        if (getView() == null) {\r\n            Log.e(TAG, \"Can't continue group. The view is null.\");\r\n            return;\r\n        }\r\n\r\n        if (group == null) {\r\n            Log.e(TAG, \"Can't continue group. The group is null.\");\r\n            return;\r\n        }\r\n\r\n        Log.d(TAG, \"continueGroup: start continue group: \" + group.getTitle());\r\n\r\n        getView().showProgressBar(true);\r\n\r\n        MediaGroup mediaGroup = group.getMediaGroup();\r\n\r\n        Observable<MediaGroup> continuation;\r\n\r\n        continuation = getContentService().continueGroupObserve(mediaGroup);\r\n\r\n        mScrollAction = continuation\r\n                .subscribe(\r\n                        continueMediaGroup -> {\r\n                            VideoGroup newGroup = VideoGroup.from(group, continueMediaGroup);\r\n                            getView().update(newGroup);\r\n                            mBrowseProcessor.process(newGroup);\r\n                        },\r\n                        error -> {\r\n                            Log.e(TAG, \"continueGroup error: %s\", error.getMessage());\r\n                            if (getView() != null) {\r\n                                getView().showProgressBar(false);\r\n                            }\r\n                        },\r\n                        () -> getView().showProgressBar(false)\r\n                );\r\n    }\r\n\r\n    private void update(Video item) {\r\n        // Liked music fix - not all videos displayed. The behavior with other playlists is buggy.\r\n        if (Helpers.equals(item.playlistId, Video.PLAYLIST_LIKED_MUSIC)) {\r\n            update(item.getGroup());\r\n        } else {\r\n            update(obtainUploadsObservable(item));\r\n        }\r\n    }\r\n\r\n    private void update(Observable<MediaGroup> group) {\r\n        Log.d(TAG, \"update: Start loading a group...\");\r\n\r\n        disposeActions();\r\n\r\n        getView().showProgressBar(true);\r\n\r\n        mUpdateAction = group\r\n                .subscribe(\r\n                        this::update,\r\n                        error -> {\r\n                            Log.e(TAG, \"update error: %s\", error.getMessage());\r\n                            getView().showProgressBar(false);\r\n                        },\r\n                        () -> getView().showProgressBar(false)\r\n                );\r\n    }\r\n\r\n    public void update(MediaGroup mediaGroup) {\r\n        // The view could be running in the background\r\n        getViewManager().startView(ChannelUploadsView.class);\r\n\r\n        if (getView() == null) { // starting from outside (e.g. MediaServiceManager)\r\n            mPendingGroup = mediaGroup; // start loading from this group\r\n            return;\r\n        }\r\n\r\n        mBaseGroup = mBaseGroup != null ? VideoGroup.from(mBaseGroup, mediaGroup) : VideoGroup.from(mediaGroup);\r\n        if (mChannel != null && TextUtils.isEmpty(mBaseGroup.getTitle())) {\r\n            mBaseGroup.setTitle(mChannel.getTitle());\r\n        }\r\n        update(mBaseGroup);\r\n    }\r\n\r\n    private void update(VideoGroup group) {\r\n        disposeActions();\r\n\r\n        if (getView() == null || group == null) {\r\n            return;\r\n        }\r\n\r\n        getView().update(group);\r\n        mBrowseProcessor.process(group);\r\n\r\n        // Hide loading as long as first group received\r\n        if (!group.isEmpty()) {\r\n            getView().showProgressBar(false);\r\n        }\r\n    }\r\n\r\n    private void obtainGroup(MediaItem mediaItem, OnMediaGroup callback, OnError onError, OnComplete onComplete) {\r\n        Log.d(TAG, \"obtainGroup: Start loading group...\");\r\n\r\n        disposeActions();\r\n\r\n        mUpdateAction = obtainUploadsObservable(Video.from(mediaItem))\r\n                .subscribe(\r\n                        callback::onMediaGroup,\r\n                        error -> {\r\n                            Log.e(TAG, \"obtainGroup error: %s\", error.getMessage());\r\n                            if (onError != null) {\r\n                                onError.onError(error);\r\n                            }\r\n                        },\r\n                        () -> {\r\n                            if (onComplete != null) {\r\n                                onComplete.onComplete();\r\n                            }\r\n                        }\r\n                );\r\n    }\r\n\r\n    /**\r\n     * Playlist usually is the first row with media items.<br/>\r\n     * NOTE: before playlist may be the video description row\r\n     */\r\n    private MediaGroup findPlaylistRow(MediaItemMetadata mediaItemMetadata) {\r\n        if (mediaItemMetadata == null || mediaItemMetadata.getSuggestions() == null) {\r\n            return null;\r\n        }\r\n\r\n        for (MediaGroup group : mediaItemMetadata.getSuggestions()) {\r\n            List<MediaItem> mediaItems = group.getMediaItems();\r\n            if (mediaItems != null && !mediaItems.isEmpty()) {\r\n                return group;\r\n            }\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    public void clear() {\r\n        disposeActions();\r\n        if (getView() != null) {\r\n            getView().clear();\r\n        }\r\n        mChannel = null;\r\n        mPendingGroup = null;\r\n        mBaseGroup = null;\r\n    }\r\n\r\n    public void refresh() {\r\n        if (getView() == null) {\r\n            return;\r\n        }\r\n\r\n        if (mPendingGroup != null) {\r\n            getView().clear();\r\n            update(mPendingGroup);\r\n        } else if (mChannel != null) {\r\n            getView().clear();\r\n            update(mChannel);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/DetailsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.DetailsView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ViewManager;\r\n\r\npublic class DetailsPresenter extends BasePresenter<DetailsView> {\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static DetailsPresenter sInstance;\r\n    private Video mVideo;\r\n\r\n    private DetailsPresenter(Context context) {\r\n        super(context);\r\n    }\r\n\r\n    public static DetailsPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new DetailsPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    @Override\r\n    public void onViewInitialized() {\r\n        getView().openVideo(mVideo);\r\n    }\r\n\r\n    public void openVideo(Video item) {\r\n        mVideo = item;\r\n        getViewManager().startView(DetailsView.class);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/GoogleSignInPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.googleapi.oauth2.impl.GoogleSignInService;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\n\r\nimport io.reactivex.disposables.Disposable;\r\n\r\npublic class GoogleSignInPresenter extends SignInPresenter {\r\n    private static final String TAG = GoogleSignInPresenter.class.getSimpleName();\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static GoogleSignInPresenter sInstance;\r\n    private final GoogleSignInService mSignInService;\r\n    private Disposable mSignInAction;\r\n    private Runnable mCallback;\r\n\r\n    private GoogleSignInPresenter(Context context) {\r\n        super(context);\r\n        mSignInService = GoogleSignInService.instance();\r\n    }\r\n\r\n    public static GoogleSignInPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new GoogleSignInPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void unhold() {\r\n        RxHelper.disposeActions(mSignInAction);\r\n        sInstance = null;\r\n    }\r\n\r\n    @Override\r\n    public void onViewDestroyed() {\r\n        super.onViewDestroyed();\r\n        unhold();\r\n    }\r\n\r\n    @Override\r\n    public void onViewInitialized() {\r\n        super.onViewInitialized();\r\n        RxHelper.disposeActions(mSignInAction);\r\n        updateUserCode();\r\n    }\r\n\r\n    @Override\r\n    public void onActionClicked() {\r\n        if (getView() != null) {\r\n            getView().close();\r\n        }\r\n    }\r\n\r\n    private void updateUserCode() {\r\n        mSignInAction = mSignInService.signInObserve()\r\n                .subscribe(\r\n                        code -> getView().showCode(code.getSignInCode(), code.getSignInUrl()),\r\n                        error -> {\r\n                            Log.e(TAG, \"Sign in error: %s\", error.getMessage());\r\n                            if (getView() != null) {\r\n                                getView().showCode(error.getMessage(), \"\");\r\n                            }\r\n                        },\r\n                        () -> {\r\n                            // Success\r\n                            if (getView() != null) {\r\n                                getView().close();\r\n                            }\r\n\r\n                            if (mCallback != null) {\r\n                                mCallback.run();\r\n                            }\r\n                        }\r\n                 );\r\n    }\r\n\r\n    @Override\r\n    public void start() {\r\n        super.start();\r\n        RxHelper.disposeActions(mSignInAction);\r\n    }\r\n\r\n    public void start(Runnable onSuccess) {\r\n        super.start();\r\n        mCallback = onSuccess;\r\n        RxHelper.disposeActions(mSignInAction);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/PlaybackPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.app.Activity;\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Playlist;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.BasePlayerController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.AutoFrameRateController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.ChatController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.CommentsController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.SponsorBlockController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.HQDialogController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.PlayerUIController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.RemoteController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.SuggestionsController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.VideoLoaderController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.VideoStateController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.listener.PlayerEventListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.listener.ViewEventListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.PlaybackView;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils.ChainProcessor;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils.Processor;\r\nimport com.liskovsoft.googlecommon.common.helpers.ServiceHelper;\r\n\r\nimport java.lang.ref.WeakReference;\r\nimport java.util.List;\r\nimport java.util.concurrent.CopyOnWriteArrayList;\r\n\r\npublic class PlaybackPresenter extends BasePresenter<PlaybackView> implements PlayerEventListener {\r\n    private static final String TAG = PlaybackPresenter.class.getSimpleName();\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static PlaybackPresenter sInstance;\r\n    private final List<PlayerEventListener> mEventListeners = new CopyOnWriteArrayList<PlayerEventListener>() {\r\n        @Override\r\n        public boolean add(PlayerEventListener listener) {\r\n            ((BasePlayerController) listener).setMainController(PlaybackPresenter.this);\r\n\r\n            return super.add(listener);\r\n        }\r\n    };\r\n    private WeakReference<Video> mVideo;\r\n    // Fix for using destroyed view\r\n    private WeakReference<PlaybackView> mPlayer = new WeakReference<>(null);\r\n    private boolean mIsEmbedPlayerStarted;\r\n\r\n    private PlaybackPresenter(Context context) {\r\n        super(context);\r\n\r\n        // NOTE: position matters!!!\r\n        mEventListeners.add(new VideoStateController());\r\n        mEventListeners.add(new SuggestionsController());\r\n        mEventListeners.add(new VideoLoaderController());\r\n        mEventListeners.add(new PlayerUIController());\r\n        mEventListeners.add(new RemoteController(context));\r\n        mEventListeners.add(new SponsorBlockController());\r\n        mEventListeners.add(new AutoFrameRateController());\r\n        mEventListeners.add(new HQDialogController());\r\n        mEventListeners.add(new ChatController());\r\n        mEventListeners.add(new CommentsController());\r\n    }\r\n\r\n    public static PlaybackPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new PlaybackPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    @Override\r\n    public void onViewInitialized() {\r\n        super.onViewInitialized();\r\n        \r\n        initControllers();\r\n    }\r\n\r\n    private void initControllers() {\r\n        // Re-init after app exit\r\n        process(PlayerEventListener::onInit);\r\n    }\r\n\r\n    public void openVideo(String videoId) {\r\n        openVideo(videoId, false, -1, false);\r\n    }\r\n\r\n    /**\r\n     * Opens video item from splash view\r\n     */\r\n    public void openVideo(String videoId, boolean finishOnEnded, long timeMs, boolean incognito) {\r\n        if (videoId == null) {\r\n            return;\r\n        }\r\n\r\n        Video video = Video.from(videoId);\r\n        video.finishOnEnded = finishOnEnded;\r\n        video.pendingPosMs = timeMs;\r\n        video.incognito = incognito;\r\n        openVideo(video);\r\n    }\r\n\r\n    public void openVideo(Video video) {\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        if (getView() != null && getView().isEmbed()) { // switching from the embed player to the fullscreen one\r\n            // The embed player doesn't disposed properly\r\n            // NOTE: don't release after init check because this depends on timings\r\n            getView().finishReally();\r\n            setView(null);\r\n            //getController(VideoStateController.class).saveState();\r\n        }\r\n\r\n        onNewVideo(video);\r\n\r\n        getViewManager().startView(PlaybackView.class);\r\n        mIsEmbedPlayerStarted = false;\r\n    }\r\n\r\n    public Video getVideo() {\r\n        return mVideo != null ? mVideo.get() : null;\r\n    }\r\n\r\n    public boolean isRunningInBackground() {\r\n        return getView() != null &&\r\n                getView().isEngineBlocked() &&\r\n                //getView().getBackgroundMode() != PlayerEngine.BACKGROUND_MODE_DEFAULT &&\r\n                getView().isEngineInitialized() &&\r\n                !getViewManager().isPlayerInForeground() &&\r\n                getContext() instanceof Activity && Utils.checkActivity((Activity) getContext()); // Check that activity is not in Finishing state\r\n    }\r\n\r\n    public boolean isInPipMode() {\r\n        return getView() != null && getView().isInPIPMode();\r\n    }\r\n\r\n    public boolean isOverlayShown() {\r\n        return getView() != null && getView().isOverlayShown();\r\n    }\r\n\r\n    public boolean isPlaying() {\r\n        return getView() != null && getView().isPlaying();\r\n    }\r\n\r\n    public boolean isEngineBlocked() {\r\n        return getView() != null && getView().isEngineBlocked();\r\n    }\r\n\r\n    public boolean isEngineInitialized() {\r\n        return getView() != null && getView().isEngineInitialized();\r\n    }\r\n\r\n    //public int getBackgroundMode() {\r\n    //    return getView() != null ? getView().getBackgroundMode() : -1;\r\n    //}\r\n\r\n    public void forceFinish() {\r\n        if (getView() != null) {\r\n            getView().finishReally();\r\n        }\r\n    }\r\n\r\n    public void setPosition(String timeCode) {\r\n        setPosition(ServiceHelper.timeTextToMillis(timeCode));\r\n    }\r\n\r\n    public void setPosition(long positionMs) {\r\n        // Check that the user isn't open context menu on suggestion item\r\n        // if (Utils.isPlayerInForeground(getContext()) && getView() != null && !getView().getController().isSuggestionsShown()) {\r\n        if (getViewManager().isPlayerInForeground() && getView() != null) {\r\n            getView().setPositionMs(positionMs);\r\n            getView().setPlayWhenReady(true);\r\n            getView().showOverlay(false);\r\n        } else {\r\n            Video video = VideoMenuPresenter.sVideoHolder.get();\r\n            if (video != null) {\r\n                video.pendingPosMs = positionMs;\r\n                openVideo(video);\r\n            }\r\n        }\r\n    }\r\n\r\n    // Controller methods\r\n\r\n    @Override\r\n    public void setView(PlaybackView view) {\r\n        super.setView(view);\r\n        mPlayer = new WeakReference<>(view);\r\n\r\n        // Fix playing the previous video when switching between embed and fullscreen players.\r\n        // E.g. when the user pressed back on the Channel content screen\r\n        if (view != null && view.getVideo() != null && mIsEmbedPlayerStarted) {\r\n            mVideo = new WeakReference<>(view.getVideo());\r\n            Playlist.instance().add(view.getVideo()); // don't show queue\r\n        }\r\n    }\r\n\r\n    public PlaybackView getPlayer() {\r\n        return mPlayer.get(); // return view even if the one is destroyed\r\n    }\r\n\r\n    public Activity getActivity() {\r\n        return getContext() instanceof Activity ? (Activity) getContext() : null;\r\n    }\r\n\r\n    @SuppressWarnings(\"unchecked\")\r\n    public <T extends PlayerEventListener> T getController(Class<T> clazz) {\r\n        for (PlayerEventListener listener : mEventListeners) {\r\n            if (clazz.isInstance(listener)) {\r\n                return (T) listener;\r\n            }\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    // Core events\r\n\r\n    @Override\r\n    public void onNewVideo(Video video) {\r\n        process(listener -> listener.onNewVideo(video));\r\n        mVideo = new WeakReference<>(video);\r\n        mIsEmbedPlayerStarted = true;\r\n    }\r\n\r\n    @Override\r\n    public void onFinish() {\r\n        process(PlayerEventListener::onFinish);\r\n    }\r\n\r\n    @Override\r\n    public void onInit() {\r\n        // NOP. Internal event.\r\n    }\r\n\r\n    @Override\r\n    public void onMetadata(MediaItemMetadata metadata) {\r\n        process(listener -> listener.onMetadata(metadata));\r\n    }\r\n\r\n    // End core events\r\n\r\n    // Helpers\r\n\r\n    private boolean chainProcess(ChainProcessor<PlayerEventListener> processor) {\r\n        return Utils.chainProcess(mEventListeners, processor);\r\n    }\r\n\r\n    private void process(Processor<PlayerEventListener> processor) {\r\n        Utils.process(mEventListeners, processor);\r\n    }\r\n\r\n    // End Helpers\r\n\r\n    // Common events\r\n\r\n    @Override\r\n    public void onViewCreated() {\r\n        process(ViewEventListener::onViewCreated);\r\n    }\r\n\r\n    @Override\r\n    public void onViewDestroyed() {\r\n        process(ViewEventListener::onViewDestroyed);\r\n    }\r\n\r\n    @Override\r\n    public void onViewPaused() {\r\n        super.onViewPaused();\r\n\r\n        process(ViewEventListener::onViewPaused);\r\n    }\r\n\r\n    @Override\r\n    public void onViewResumed() {\r\n        super.onViewResumed();\r\n\r\n        process(ViewEventListener::onViewResumed);\r\n    }\r\n\r\n    // End common events\r\n\r\n    // Start engine events\r\n\r\n    @Override\r\n    public void onSourceChanged(Video item) {\r\n        process(listener -> listener.onSourceChanged(item));\r\n    }\r\n\r\n    @Override\r\n    public void onEngineInitialized() {\r\n        getTickleManager().addListener(this);\r\n\r\n        process(PlayerEventListener::onEngineInitialized);\r\n    }\r\n\r\n    @Override\r\n    public void onEngineReleased() {\r\n        getTickleManager().removeListener(this);\r\n\r\n        process(PlayerEventListener::onEngineReleased);\r\n    }\r\n\r\n    @Override\r\n    public void onEngineError(int type, int rendererIndex, Throwable error) {\r\n        process(listener -> listener.onEngineError(type, rendererIndex, error));\r\n    }\r\n\r\n    @Override\r\n    public void onPlay() {\r\n        process(PlayerEventListener::onPlay);\r\n    }\r\n\r\n    @Override\r\n    public void onPause() {\r\n        process(PlayerEventListener::onPause);\r\n    }\r\n\r\n    @Override\r\n    public void onPlayClicked() {\r\n        process(PlayerEventListener::onPlayClicked);\r\n    }\r\n\r\n    @Override\r\n    public void onPauseClicked() {\r\n        process(PlayerEventListener::onPauseClicked);\r\n    }\r\n\r\n    @Override\r\n    public void onSeekEnd() {\r\n        process(PlayerEventListener::onSeekEnd);\r\n    }\r\n\r\n    @Override\r\n    public void onSeekPositionChanged(long positionMs) {\r\n        process(listener -> listener.onSeekPositionChanged(positionMs));\r\n    }\r\n\r\n    @Override\r\n    public void onSpeedChanged(float speed) {\r\n        process(listener -> listener.onSpeedChanged(speed));\r\n    }\r\n\r\n    @Override\r\n    public void onPlayEnd() {\r\n        process(PlayerEventListener::onPlayEnd);\r\n    }\r\n\r\n    @Override\r\n    public void onBuffering() {\r\n        process(PlayerEventListener::onBuffering);\r\n    }\r\n\r\n    @Override\r\n    public boolean onKeyDown(int keyCode) {\r\n        return chainProcess(listener -> listener.onKeyDown(keyCode));\r\n    }\r\n\r\n    @Override\r\n    public void onVideoLoaded(Video item) {\r\n        process(listener -> listener.onVideoLoaded(item));\r\n    }\r\n\r\n    @Override\r\n    public void onTickle() {\r\n        process(PlayerEventListener::onTickle);\r\n    }\r\n\r\n    // End engine events\r\n\r\n    // Start UI events\r\n\r\n    @Override\r\n    public void onSuggestionItemClicked(Video item) {\r\n        process(listener -> listener.onSuggestionItemClicked(item));\r\n    }\r\n\r\n    @Override\r\n    public void onSuggestionItemLongClicked(Video item) {\r\n        process(listener -> listener.onSuggestionItemLongClicked(item));\r\n    }\r\n\r\n    @Override\r\n    public void onScrollEnd(Video item) {\r\n        process(listener -> listener.onScrollEnd(item));\r\n    }\r\n\r\n    @Override\r\n    public boolean onPreviousClicked() {\r\n        return chainProcess(PlayerEventListener::onPreviousClicked);\r\n    }\r\n\r\n    @Override\r\n    public boolean onNextClicked() {\r\n        return chainProcess(PlayerEventListener::onNextClicked);\r\n    }\r\n\r\n    @Override\r\n    public void onTrackSelected(FormatItem track) {\r\n        process(listener -> listener.onTrackSelected(track));\r\n    }\r\n\r\n    @Override\r\n    public void onControlsShown(boolean shown) {\r\n        process(listener -> listener.onControlsShown(shown));\r\n    }\r\n\r\n    @Override\r\n    public void onTrackChanged(FormatItem track) {\r\n        process(listener -> listener.onTrackChanged(track));\r\n    }\r\n\r\n    @Override\r\n    public void onButtonClicked(int buttonId, int buttonState) {\r\n        process(listener -> listener.onButtonClicked(buttonId, buttonState));\r\n    }\r\n\r\n    @Override\r\n    public void onButtonLongClicked(int buttonId, int buttonState) {\r\n        process(listener -> listener.onButtonLongClicked(buttonId, buttonState));\r\n    }\r\n\r\n    // End UI events\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/SearchPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport com.liskovsoft.mediaserviceinterfaces.ContentService;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.SearchOptions;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.search.MediaServiceSearchTagProvider;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.search.vineyard.Tag;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.VideoActionPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.interfaces.VideoGroupPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.SearchView;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.BrowseProcessorManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AccountsData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class SearchPresenter extends BasePresenter<SearchView> implements VideoGroupPresenter {\r\n    private static final String TAG = SearchPresenter.class.getSimpleName();\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static SearchPresenter sInstance;\r\n    private final BrowseProcessorManager mBrowseProcessor;\r\n    private Disposable mScrollAction;\r\n    private Disposable mLoadAction;\r\n    private String mSearchText;\r\n    private boolean mIsVoice;\r\n    private boolean mStartPlay;\r\n    private int mUploadDateOptions;\r\n    private int mDurationOptions;\r\n    private int mTypeOptions;\r\n    private int mFeatureOptions;\r\n    private int mSortingOptions;\r\n    private Video mCurrentVideo;\r\n\r\n    private SearchPresenter(Context context) {\r\n        super(context);\r\n        mBrowseProcessor = new BrowseProcessorManager(getContext(), this::syncItem);\r\n    }\r\n\r\n    public static SearchPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new SearchPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    @Override\r\n    public void onViewInitialized() {\r\n        if (!AccountsData.instance(getContext()).isPasswordAccepted()) {\r\n            getView().finishReally();\r\n            return;\r\n        }\r\n\r\n        getView().setTagsProvider(new MediaServiceSearchTagProvider(getSearchData().isSearchHistoryDisabled()));\r\n\r\n        startSearchInt();\r\n    }\r\n\r\n    @Override\r\n    public void onViewDestroyed() {\r\n        super.onViewDestroyed();\r\n        disposeActions();\r\n    }\r\n\r\n    @Override\r\n    public void onFinish() {\r\n        super.onFinish();\r\n\r\n        mSearchText = null;\r\n        mUploadDateOptions = 0;\r\n        mDurationOptions = 0;\r\n        mTypeOptions = 0;\r\n        mFeatureOptions = 0;\r\n        mSortingOptions = 0;\r\n        mIsVoice = false;\r\n    }\r\n\r\n    @Override\r\n    public void onVideoItemSelected(Video item) {\r\n        mCurrentVideo = item;\r\n    }\r\n\r\n    @Override\r\n    public void onVideoItemClicked(Video item) {\r\n        if (getView() == null) {\r\n            return;\r\n        }\r\n\r\n        VideoActionPresenter.instance(getContext()).apply(item);\r\n    }\r\n\r\n    @Override\r\n    public void onVideoItemLongClicked(Video item) {\r\n        if (getView() == null) {\r\n            return;\r\n        }\r\n\r\n        VideoMenuPresenter.instance(getContext()).showMenu(item);\r\n    }\r\n\r\n    public void onTagLongClicked(Tag item) {\r\n        if (getView() == null) {\r\n            return;\r\n        }\r\n\r\n        AppDialogUtil.showConfirmationDialog(\r\n                getContext(),\r\n                getContext().getString(R.string.clear_search_history),\r\n                () -> {\r\n                    MediaServiceManager.instance().clearSearchHistory();\r\n                    getView().clearSearchTags();\r\n                });\r\n    }\r\n\r\n    @Override\r\n    public boolean hasPendingActions() {\r\n        return RxHelper.isAnyActionRunning(mLoadAction, mScrollAction);\r\n    }\r\n\r\n    public void onSearch(String searchText) {\r\n        // Restore the search in case the view unloaded from the memory\r\n        mSearchText = searchText;\r\n\r\n        if (getView() == null) {\r\n            Log.e(TAG, \"Search view has been unloaded from the memory. Low RAM?\");\r\n            startSearch(searchText);\r\n            return;\r\n        }\r\n\r\n        loadSearchResult(searchText);\r\n    }\r\n\r\n    private void loadSearchResult(String searchText) {\r\n        Log.d(TAG, \"Start search for '%s'\", searchText);\r\n\r\n        disposeActions();\r\n        getView().showProgressBar(true);\r\n\r\n        ContentService contentService = getContentService();\r\n\r\n        getView().clearSearch();\r\n\r\n        mLoadAction = contentService.getSearchObserve(searchText,\r\n                mUploadDateOptions | mDurationOptions | mTypeOptions | mFeatureOptions | mSortingOptions)\r\n                .subscribe(\r\n                        mediaGroups -> {\r\n                            Log.d(TAG, \"Receiving results for '%s'\", searchText);\r\n                            for (MediaGroup mediaGroup : mediaGroups) {\r\n                                VideoGroup group = VideoGroup.from(mediaGroup);\r\n                                startPlayFirstVideo(group);\r\n                                getView().updateSearch(group);\r\n                                mBrowseProcessor.process(group);\r\n                            }\r\n                        },\r\n                        error -> {\r\n                            Log.e(TAG, \"loadSearchData error: %s\", error.getMessage());\r\n                            if (getView() != null) {\r\n                                getView().showProgressBar(false);\r\n                            }\r\n                        },\r\n                        () -> {\r\n                            if (getView() != null) {\r\n                                getView().showProgressBar(false);\r\n                            }\r\n                        }\r\n                );\r\n    }\r\n\r\n    private void continueGroup(VideoGroup group) {\r\n        if (RxHelper.isAnyActionRunning(mScrollAction)) {\r\n            return;\r\n        }\r\n\r\n        if (getView() == null) {\r\n            return;\r\n        }\r\n\r\n        Log.d(TAG, \"continueGroup: start continue group: \" + group.getTitle());\r\n\r\n        getView().showProgressBar(true);\r\n\r\n        MediaGroup mediaGroup = group.getMediaGroup();\r\n\r\n        ContentService contentService = getContentService();\r\n\r\n        mScrollAction = contentService.continueGroupObserve(mediaGroup)\r\n                .subscribe(\r\n                        continueMediaGroup -> {\r\n                            VideoGroup newGroup = VideoGroup.from(group, continueMediaGroup);\r\n                            getView().updateSearch(newGroup);\r\n                            mBrowseProcessor.process(newGroup);\r\n                        },\r\n                        error -> {\r\n                            Log.e(TAG, \"continueGroup error: %s\", error.getMessage());\r\n                            if (getView() != null) {\r\n                                getView().showProgressBar(false);\r\n                            }\r\n                        },\r\n                        () -> {\r\n                            if (getView() != null) {\r\n                                getView().showProgressBar(false);\r\n                            }\r\n                        }\r\n                );\r\n    }\r\n\r\n    @Override\r\n    public void onScrollEnd(Video item) {\r\n        if (item == null) {\r\n            Log.e(TAG, \"Can't scroll. Video is null.\");\r\n            return;\r\n        }\r\n\r\n        if (item.getGroup() == null) {\r\n            Log.e(TAG, \"Can't scroll. Video group is null.\");\r\n            return;\r\n        }\r\n\r\n        VideoGroup group = item.getGroup();\r\n\r\n        Log.d(TAG, \"onScrollEnd: Group title: \" + group.getTitle());\r\n\r\n        continueGroup(group);\r\n    }\r\n\r\n    public void startVoice() {\r\n        startSearch(null, true, false);\r\n    }\r\n\r\n    public void startSearch(String searchText) {\r\n        startSearch(searchText, false, false);\r\n    }\r\n\r\n    public void startPlay(String searchText) {\r\n        startSearch(searchText, false, true);\r\n    }\r\n\r\n    public Video getCurrentVideo() {\r\n        return mCurrentVideo;\r\n    }\r\n\r\n    private void startSearch(String searchText, boolean isVoice, boolean startPlay) {\r\n        mSearchText = searchText;\r\n        mIsVoice = isVoice;\r\n        mStartPlay = startPlay;\r\n\r\n        getViewManager().startView(SearchView.class);\r\n        startSearchInt();\r\n    }\r\n\r\n    private void startSearchInt() {\r\n        if (getView() == null) {\r\n            return;\r\n        }\r\n\r\n        if ((mIsVoice || getSearchData().isInstantVoiceSearchEnabled()) && mSearchText == null) {\r\n            getView().startVoiceRecognition();\r\n        } else {\r\n            getView().startSearch(mSearchText);\r\n        }\r\n    }\r\n\r\n    public void onSearchSettingsClicked() {\r\n        if (getView() == null) {\r\n            return;\r\n        }\r\n\r\n        showSettingsDialog();\r\n    }\r\n\r\n    public void disposeActions() {\r\n        RxHelper.disposeActions(mLoadAction, mScrollAction);\r\n        if (getView() != null) {\r\n            getView().showProgressBar(false);\r\n        }\r\n        if (getSearchData().isSearchHistoryDisabled()) {\r\n            MediaServiceManager.instance().clearSearchHistory();\r\n        }\r\n        mBrowseProcessor.dispose();\r\n    }\r\n\r\n    private void showSettingsDialog() {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendFilterByDateCategory(settingsPresenter);\r\n        appendFilterByDurationCategory(settingsPresenter);\r\n        appendFilterByTypeCategory(settingsPresenter);\r\n        appendFilterByFeatureCategory(settingsPresenter);\r\n        appendSortByCategory(settingsPresenter);\r\n\r\n        settingsPresenter.showDialog(getContext().getString(R.string.settings_search));\r\n    }\r\n\r\n    private void appendFilterByDateCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.upload_date_any, 0},\r\n                {R.string.upload_date_last_hour, SearchOptions.UPLOAD_DATE_LAST_HOUR},\r\n                {R.string.upload_date_today, SearchOptions.UPLOAD_DATE_TODAY},\r\n                {R.string.upload_date_this_week, SearchOptions.UPLOAD_DATE_THIS_WEEK},\r\n                {R.string.upload_date_this_month, SearchOptions.UPLOAD_DATE_THIS_MONTH},\r\n                {R.string.upload_date_this_year, SearchOptions.UPLOAD_DATE_THIS_YEAR}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]),\r\n                    optionItem -> {\r\n                        mUploadDateOptions = pair[1];\r\n                        loadSearchResult();\r\n                    },\r\n                    mUploadDateOptions == pair[1]));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.upload_date), options);\r\n    }\r\n\r\n    private void appendFilterByDurationCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.video_duration_any, 0},\r\n                {R.string.video_duration_under_4, SearchOptions.DURATION_UNDER_4},\r\n                {R.string.video_duration_between_4_20, SearchOptions.DURATION_BETWEEN_4_20},\r\n                {R.string.video_duration_over_20, SearchOptions.DURATION_OVER_20}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]),\r\n                    optionItem -> {\r\n                        mDurationOptions = pair[1];\r\n                        loadSearchResult();\r\n                    },\r\n                    mDurationOptions == pair[1]));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.video_duration), options);\r\n    }\r\n\r\n    private void appendFilterByTypeCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.content_type_any, 0},\r\n                {R.string.content_type_video, SearchOptions.TYPE_VIDEO},\r\n                {R.string.content_type_channel, SearchOptions.TYPE_CHANNEL},\r\n                {R.string.content_type_playlist, SearchOptions.TYPE_PLAYLIST},\r\n                {R.string.content_type_movie, SearchOptions.TYPE_MOVIE}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]),\r\n                    optionItem -> {\r\n                        mTypeOptions = pair[1];\r\n                        loadSearchResult();\r\n                    },\r\n                    mTypeOptions == pair[1]));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.content_type), options);\r\n    }\r\n\r\n    private void appendFilterByFeatureCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.video_feature_live, SearchOptions.FEATURE_LIVE},\r\n                {R.string.video_feature_4k, SearchOptions.FEATURE_4K},\r\n                {R.string.video_feature_hdr, SearchOptions.FEATURE_HDR}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]),\r\n                    optionItem -> {\r\n                        mFeatureOptions = optionItem.isSelected() ? mFeatureOptions | pair[1] : mFeatureOptions & ~pair[1];\r\n                        loadSearchResult();\r\n                    },\r\n                    (mFeatureOptions & pair[1]) == pair[1]));\r\n        }\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.video_features), options);\r\n    }\r\n\r\n    private void appendSortByCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.sort_by_relevance, 0},\r\n                {R.string.sort_by_date, SearchOptions.SORT_BY_UPLOAD_DATE},\r\n                {R.string.sort_by_views, SearchOptions.SORT_BY_VIEW_COUNT},\r\n                {R.string.sort_by_rating, SearchOptions.SORT_BY_RATING}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]),\r\n                    optionItem -> {\r\n                        mSortingOptions = pair[1];\r\n                        loadSearchResult();\r\n                    },\r\n                    mSortingOptions == pair[1]));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.search_sorting), options);\r\n    }\r\n\r\n    public void forceFinish() {\r\n        if (getView() != null) {\r\n            getView().finishReally();\r\n        }\r\n    }\r\n\r\n    private void startPlayFirstVideo(VideoGroup group) {\r\n        if (!mStartPlay || group == null || group.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        mStartPlay = false;\r\n\r\n        for (Video video : group.getVideos()) {\r\n            if (video.videoId != null) {\r\n                PlaybackPresenter.instance(getContext()).openVideo(video);\r\n                break;\r\n            }\r\n        }\r\n    }\r\n\r\n    private void loadSearchResult() {\r\n        if (getView() == null) {\r\n            return;\r\n        }\r\n\r\n        String searchText = getView().getSearchText();\r\n\r\n        if (searchText != null && !searchText.isEmpty()) {\r\n            loadSearchResult(searchText);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/SignInPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.SignInView;\r\n\r\npublic class SignInPresenter extends BasePresenter<SignInView> {\r\n    private static final String TAG = SignInPresenter.class.getSimpleName();\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static SignInPresenter sInstance;\r\n    private SignInPresenter mPresenter;\r\n    private boolean mIsWaiting;\r\n\r\n    protected SignInPresenter(Context context) {\r\n        super(context);\r\n    }\r\n\r\n    public static SignInPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new SignInPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void unhold() {\r\n        sInstance = null;\r\n    }\r\n\r\n    @Override\r\n    public void onViewDestroyed() {\r\n        super.onViewDestroyed();\r\n        unhold();\r\n    }\r\n\r\n    @Override\r\n    public void onViewInitialized() {\r\n        if (this.getClass() != SignInPresenter.class) {\r\n            doWait(false);\r\n            return;\r\n        }\r\n\r\n        if (YTSignInPresenter.instance(getContext()).isWaiting()) {\r\n            mPresenter = YTSignInPresenter.instance(getContext());\r\n        } else if (GoogleSignInPresenter.instance(getContext()).isWaiting()) {\r\n            mPresenter = GoogleSignInPresenter.instance(getContext());\r\n        }\r\n\r\n        if (mPresenter == null) {\r\n            mPresenter = YTSignInPresenter.instance(getContext());\r\n            //throw new IllegalStateException(\"At least one nested sign in presenter should be initialized.\");\r\n        }\r\n\r\n        mPresenter.setView(getView());\r\n        mPresenter.onViewInitialized();\r\n    }\r\n\r\n    public void onActionClicked() {\r\n        if (mPresenter != null) {\r\n            mPresenter.onActionClicked();\r\n        }\r\n    }\r\n\r\n    private void doWait(boolean doWait) {\r\n        mIsWaiting = doWait;\r\n    }\r\n\r\n    protected final boolean isWaiting() {\r\n        return mIsWaiting;\r\n    }\r\n\r\n    public void start() {\r\n        getViewManager().startView(SignInView.class);\r\n        doWait(true);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/SplashPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport android.content.Intent;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.oauth.Account;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.prefs.GlobalPreferences;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.AccountSelectionPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.BootDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.BrowseView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.SplashView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ViewManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.GDriveBackupWorker;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.LocalDriveBackupWorker;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.StreamReminderService;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AccountsData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.proxy.ProxyManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.IntentExtractor;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.SimpleEditDialog;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class SplashPresenter extends BasePresenter<SplashView> {\r\n    private static final String TAG = SplashPresenter.class.getSimpleName();\r\n    private static final long APP_INIT_DELAY_MS = 10_000;\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static SplashPresenter sInstance;\r\n    private static boolean sRunOnce;\r\n    private boolean mRunPerInstance;\r\n    private final List<IntentProcessor> mIntentChain = new ArrayList<>();\r\n    private String mBridgePackageName;\r\n    private final Runnable mRunBackgroundTasks = this::runBackgroundTasks;\r\n    private final Runnable mCheckForUpdates = this::checkForUpdates;\r\n\r\n    private interface IntentProcessor {\r\n        boolean process(Intent intent);\r\n    }\r\n\r\n    private SplashPresenter(Context context) {\r\n        super(context);\r\n    }\r\n\r\n    public static SplashPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new SplashPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public static void unhold() {\r\n        if (sInstance != null) {\r\n            Utils.removeCallbacks(sInstance.mRunBackgroundTasks);\r\n        }\r\n        sInstance = null;\r\n    }\r\n\r\n    @Override\r\n    public void onViewInitialized() {\r\n        if (getView() == null) {\r\n            return;\r\n        }\r\n\r\n        Utils.cancelFinishTheApp(getContext());\r\n\r\n        runOneTimeTasks();\r\n        runPerInstanceTasks();\r\n        runPerViewTasks();\r\n    }\r\n\r\n    private void runOneTimeTasks() {\r\n        if (!sRunOnce) {\r\n            sRunOnce = true;\r\n            RxHelper.setupGlobalErrorHandler();\r\n            initGlobalPrefs();\r\n            initProxy();\r\n            initVideoStateService();\r\n            initStreamReminderService();\r\n        }\r\n    }\r\n\r\n    private void runPerInstanceTasks() {\r\n        if (!mRunPerInstance) {\r\n            mRunPerInstance = true;\r\n            Utils.postDelayed(mRunBackgroundTasks, APP_INIT_DELAY_MS);\r\n            initIntentChain();\r\n        }\r\n    }\r\n\r\n    private void runPerViewTasks() {\r\n        Utils.postDelayed(mCheckForUpdates, APP_INIT_DELAY_MS);\r\n        Utils.updateRemoteControlService(getContext());\r\n\r\n        checkMasterPassword(() -> applyNewIntent(getView().getNewIntent()));\r\n\r\n        showAccountSelectionIfNeeded(); // should be placed after Intent chain\r\n        checkAccountPassword();\r\n    }\r\n\r\n    private void runBackgroundTasks() {\r\n        YouTubeServiceManager.instance().refreshCacheIfNeeded(); // warm up player engine\r\n        enableHistoryIfNeeded();\r\n        Utils.updateChannels(getContext());\r\n        GDriveBackupWorker.schedule(getContext());\r\n        LocalDriveBackupWorker.schedule(getContext());\r\n    }\r\n\r\n    private void showAccountSelectionIfNeeded() {\r\n        AccountSelectionPresenter.instance(getContext()).show();\r\n    }\r\n\r\n    private void checkAccountPassword() {\r\n        AccountsData data = AccountsData.instance(getContext());\r\n        // Block even if the password was accepted before\r\n        if (data.getAccountPassword() != null) {\r\n            data.setPasswordAccepted(false);\r\n            PlaybackPresenter.instance(getContext()).forceFinish();\r\n            BrowsePresenter.instance(getContext()).updateSections();\r\n        }\r\n    }\r\n\r\n    private void checkForUpdates() {\r\n        BootDialogPresenter updatePresenter = BootDialogPresenter.instance(getContext());\r\n        updatePresenter.start();\r\n    }\r\n\r\n    private void initVideoStateService() {\r\n        if (getContext() != null) {\r\n            VideoStateService.instance(getContext());\r\n        }\r\n    }\r\n\r\n    private void initStreamReminderService() {\r\n        if (getContext() != null) {\r\n            StreamReminderService.instance(getContext()).start();\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Need to be the first line and executed on earliest stage once.<br/>\r\n     * Do init media service language and context.<br/>\r\n     * NOTE: this command should run before using any of the media service api.\r\n     */\r\n    private void initGlobalPrefs() {\r\n        Log.d(TAG, \"initGlobalData called...\");\r\n\r\n        if (getContext() != null) {\r\n            // 1) Auth token storage init\r\n            // 2) Media service language setup (I assume that context has proper language)\r\n            GlobalPreferences.instance(getContext());\r\n        }\r\n    }\r\n\r\n    private void initProxy() {\r\n        if (getContext() != null) {\r\n            // Apply proxy config after global prefs but before starting networking.\r\n            if (GeneralData.instance(getContext()).isProxyEnabled()) {\r\n                new ProxyManager(getContext()).configureSystemProxy();\r\n            }\r\n        }\r\n    }\r\n\r\n    private void enableHistoryIfNeeded() {\r\n        // Account history might be turned off (common issue).\r\n        GeneralData generalData = GeneralData.instance(getContext());\r\n        if (generalData.getHistoryState() != GeneralData.HISTORY_AUTO) {\r\n            MediaServiceManager.instance().enableHistory(generalData.isHistoryEnabled());\r\n        }\r\n    }\r\n\r\n    public String getBridgePackageName() {\r\n        return mBridgePackageName;\r\n    }\r\n\r\n    private void initIntentChain() {\r\n        mIntentChain.add(intent -> {\r\n            String accountName = IntentExtractor.extractAccountName(intent);\r\n\r\n            if (accountName != null) {\r\n                List<Account> accounts = getSignInService().getAccounts();\r\n                for (Account account : accounts) {\r\n                    if (Helpers.equals(account.getName(), accountName)) {\r\n                        AccountSelectionPresenter.instance(getContext()).selectAccount(account);\r\n                        break;\r\n                    }\r\n                }\r\n            }\r\n\r\n            return false;\r\n        });\r\n\r\n        mIntentChain.add(intent -> {\r\n            String searchText = IntentExtractor.extractSearchText(intent);\r\n\r\n            if (searchText != null || IntentExtractor.isStartVoiceCommand(intent)) {\r\n                SearchPresenter searchPresenter = SearchPresenter.instance(getContext());\r\n                if (IntentExtractor.isInstantPlayCommand(intent)) {\r\n                    searchPresenter.startPlay(searchText);\r\n                } else {\r\n                    searchPresenter.startSearch(searchText);\r\n                }\r\n                return true;\r\n            }\r\n\r\n            return false;\r\n        });\r\n\r\n        mIntentChain.add(intent -> {\r\n            String channelId = null;\r\n\r\n            try {\r\n                channelId = IntentExtractor.extractChannelId(intent);\r\n            } catch (IllegalArgumentException e) {\r\n                MessageHelpers.showLongMessage(getContext(), e.getMessage());\r\n            }\r\n\r\n            if (channelId != null) {\r\n                ChannelPresenter channelPresenter = ChannelPresenter.instance(getContext());\r\n                channelPresenter.openChannel(channelId);\r\n                return true;\r\n            }\r\n\r\n            return false;\r\n        });\r\n\r\n        mIntentChain.add(intent -> {\r\n            String playlistId = IntentExtractor.extractPlaylistId(intent);\r\n\r\n            if (playlistId != null) {\r\n                Video video = new Video();\r\n                video.playlistId = playlistId;\r\n                ChannelUploadsPresenter.instance(getContext()).openChannel(video);\r\n                return true;\r\n            }\r\n\r\n            return false;\r\n        });\r\n\r\n        // Should come after playlist\r\n        mIntentChain.add(intent -> {\r\n            String videoId = IntentExtractor.extractVideoId(intent);\r\n\r\n            if (videoId != null) {\r\n                long timeMs = IntentExtractor.extractVideoTimeMs(intent);\r\n                PlaybackPresenter playbackPresenter = PlaybackPresenter.instance(getContext());\r\n                boolean finishOnEnded = IntentExtractor.hasFinishOnEndedFlag(intent);\r\n                boolean incognito = IntentExtractor.isIncognitoIntent(intent);\r\n                playbackPresenter.openVideo(videoId, finishOnEnded, timeMs, incognito);\r\n\r\n                enablePlayerOnlyModeIfNeeded(intent);\r\n\r\n                return true;\r\n            }\r\n\r\n            return false;\r\n        });\r\n\r\n        // NOTE: doesn't work very well. E.g. there's problems with focus or conflicts with 'boot to' section option.\r\n        mIntentChain.add(intent -> {\r\n            if (!GeneralData.instance(getContext()).isSelectChannelSectionEnabled()) {\r\n                return false;\r\n            }\r\n\r\n            int sectionId = -1;\r\n\r\n            // ATV channel icon clicked\r\n            if (IntentExtractor.isSubscriptionsUrl(intent)) {\r\n                sectionId = MediaGroup.TYPE_SUBSCRIPTIONS;\r\n            } else if (IntentExtractor.isHistoryUrl(intent)) {\r\n                sectionId = MediaGroup.TYPE_HISTORY;\r\n            } else if (IntentExtractor.isRecommendedUrl(intent)) {\r\n                sectionId = MediaGroup.TYPE_HOME;\r\n            }\r\n\r\n            if (sectionId != -1) {\r\n                getViewManager().startDefaultView(); // Nvidia Shield fix\r\n                BrowsePresenter.instance(getContext()).selectSection(sectionId);\r\n\r\n                return true;\r\n            }\r\n\r\n            return false;\r\n        });\r\n\r\n        // Should come last\r\n        mIntentChain.add(intent -> {\r\n            ViewManager viewManager = getViewManager();\r\n            viewManager.startDefaultView();\r\n\r\n            // For debug purpose when using ATV bridge.\r\n            if (IntentExtractor.hasData(intent) && !IntentExtractor.isATVChannelUrl(intent) && !IntentExtractor.isRootUrl(intent)) {\r\n                MessageHelpers.showLongMessage(getContext(), String.format(\"Can't process intent: %s\", Helpers.toString(intent)));\r\n            }\r\n\r\n            return true;\r\n        });\r\n    }\r\n\r\n    public void applyNewIntent(Intent intent) {\r\n        if (intent != null) {\r\n            String oldBridgeName = mBridgePackageName;\r\n            mBridgePackageName = intent.getStringExtra(\"bridge_package_name\");\r\n            if (!Helpers.equals(oldBridgeName, mBridgePackageName)) {\r\n                updateBadgeIcon();\r\n            }\r\n        }\r\n\r\n        for (IntentProcessor processor : mIntentChain) {\r\n            if (processor.process(intent)) {\r\n                break;\r\n            }\r\n        }\r\n    }\r\n\r\n    private void checkMasterPassword(Runnable onSuccess) {\r\n        String password = GeneralData.instance(getContext()).getMasterPassword();\r\n\r\n        // No passwd or the app already started\r\n        if (password == null || getViewManager().getTopView() != null) {\r\n            onSuccess.run();\r\n            getView().finishView(); // critical part, fix black screen on app exit\r\n        } else {\r\n            SimpleEditDialog.showPassword(\r\n                    getContext(),\r\n                    getContext().getString(R.string.enter_master_password),\r\n                    null,\r\n                    newValue -> {\r\n                        if (Utils.passwordMatch(password, newValue)) {\r\n                            onSuccess.run();\r\n                            return true;\r\n                        }\r\n\r\n                        return false;\r\n                    },\r\n                    () -> getView().finishView() // critical part, fix black screen on app exit\r\n            );\r\n        }\r\n    }\r\n\r\n    private void enablePlayerOnlyModeIfNeeded(Intent intent) {\r\n        ViewManager viewManager = getViewManager();\r\n\r\n        boolean isRestartIntent = IntentExtractor.isRestartIntent(intent);\r\n        boolean isATVIntent = IntentExtractor.isATVIntent(intent);\r\n        boolean isExternalIntent = !isRestartIntent && !isATVIntent && !viewManager.isTopViewVisible();\r\n\r\n        viewManager.enablePlayerOnlyMode((isATVIntent && GeneralData.instance(getContext()).isReturnToLauncherEnabled()) || isExternalIntent);\r\n    }\r\n\r\n    private void updateBadgeIcon() {\r\n        BrowseView browseView = BrowsePresenter.instance(getContext()).getView();\r\n\r\n        if (browseView != null) {\r\n            browseView.updateBadge();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/WebBrowserPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.WebBrowserView;\r\n\r\npublic class WebBrowserPresenter extends BasePresenter<WebBrowserView> {\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static WebBrowserPresenter sInstance;\r\n    private String mUrl;\r\n\r\n    private WebBrowserPresenter(Context context) {\r\n        super(context);\r\n    }\r\n\r\n    public static WebBrowserPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new WebBrowserPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public static void unhold() {\r\n        sInstance = null;\r\n    }\r\n\r\n    @Override\r\n    public void onViewInitialized() {\r\n        if (mUrl != null) {\r\n            getView().loadUrl(mUrl);\r\n        }\r\n    }\r\n\r\n    public void loadUrl(String url) {\r\n        mUrl = url;\r\n\r\n        getViewManager().startView(WebBrowserView.class);\r\n\r\n        if (getView() != null) {\r\n            getView().loadUrl(url);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/YTSignInPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.AccountSelectionPresenter;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\n\r\nimport io.reactivex.disposables.Disposable;\r\n\r\npublic class YTSignInPresenter extends SignInPresenter {\r\n    private static final String TAG = YTSignInPresenter.class.getSimpleName();\r\n    private static final String SIGN_IN_URL = \"https://yt.be/activate\"; // 18+, no search history\r\n    //private static final String SIGN_IN_URL = \"https://youtube.com/tv/activate\"; // 18+, no search history\r\n    //private static final String SIGN_IN_URL = \"https://youtube.com/activate\"; // age restricted, supports search history\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static YTSignInPresenter sInstance;\r\n    private final ServiceManager mService;\r\n    private Disposable mSignInAction;\r\n\r\n    private YTSignInPresenter(Context context) {\r\n        super(context);\r\n        mService = YouTubeServiceManager.instance();\r\n    }\r\n\r\n    public static YTSignInPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new YTSignInPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void unhold() {\r\n        RxHelper.disposeActions(mSignInAction);\r\n        sInstance = null;\r\n    }\r\n\r\n    @Override\r\n    public void onViewDestroyed() {\r\n        super.onViewDestroyed();\r\n        unhold();\r\n    }\r\n\r\n    @Override\r\n    public void onViewInitialized() {\r\n        super.onViewInitialized();\r\n        RxHelper.disposeActions(mSignInAction);\r\n        updateUserCode();\r\n    }\r\n\r\n    @Override\r\n    public void onActionClicked() {\r\n        if (getView() != null) {\r\n            getView().close();\r\n        }\r\n    }\r\n\r\n    private void updateUserCode() {\r\n        mSignInAction = mService.getSignInService().signInObserve()\r\n                .subscribe(\r\n                        userCode -> getView().showCode(userCode, SIGN_IN_URL),\r\n                        error -> {\r\n                            Log.e(TAG, \"Sign in error: %s\", error.getMessage());\r\n                            if (getView() != null) {\r\n                                getView().showCode(error.getMessage(), \"\");\r\n                            }\r\n                        },\r\n                        () -> {\r\n                            // Success\r\n                            if (getView() != null) {\r\n                                getView().close();\r\n                            }\r\n\r\n                            AccountSelectionPresenter.instance(getContext()).show(true);\r\n                        }\r\n                 );\r\n    }\r\n\r\n    public void start() {\r\n        super.start();\r\n        RxHelper.disposeActions(mSignInAction);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/base/BasePresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.base;\r\n\r\nimport android.app.Activity;\r\nimport android.content.Context;\r\nimport android.view.View;\r\n\r\nimport androidx.fragment.app.Fragment;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.CommentsService;\r\nimport com.liskovsoft.mediaserviceinterfaces.ContentService;\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.mediaserviceinterfaces.NotificationsService;\r\nimport com.liskovsoft.mediaserviceinterfaces.SignInService;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Playlist;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.interfaces.Presenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.service.SidebarService;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.BrowseView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ChannelUploadsView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ChannelView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.PlaybackView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.SearchView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ViewManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.TickleManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.SearchData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\n\r\nimport java.lang.ref.WeakReference;\r\nimport java.util.Collections;\r\nimport java.util.List;\r\n\r\npublic abstract class BasePresenter<T> implements Presenter<T> {\r\n    private WeakReference<T> mView = new WeakReference<>(null);\r\n    private WeakReference<Activity> mActivity = new WeakReference<>(null);\r\n    private WeakReference<Context> mApplicationContext = new WeakReference<>(null);\r\n\r\n    public BasePresenter(Context context) {\r\n        setContext(context);\r\n    }\r\n\r\n    @Override\r\n    public void setView(T view) {\r\n        if (checkView(view)) {\r\n            mView = new WeakReference<>(view);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public T getView() {\r\n        T view = mView.get();\r\n\r\n        return checkView(view) ? view : null;\r\n    }\r\n\r\n    @Override\r\n    public void setContext(Context context) {\r\n        if (context == null) {\r\n            return;\r\n        }\r\n\r\n        // Localization fix: prefer Activity context\r\n        if (context instanceof Activity && Utils.checkActivity((Activity) context)) {\r\n            mActivity = new WeakReference<>((Activity) context);\r\n        }\r\n\r\n        // In case view was disposed like SplashView does\r\n        mApplicationContext = new WeakReference<>(context.getApplicationContext());\r\n    }\r\n\r\n    @Override\r\n    public Context getContext() {\r\n        Activity activity = null;\r\n\r\n        Activity viewActivity = getViewActivity(mView.get());\r\n\r\n        // Trying to find localized context.\r\n        // First, try the view that belongs to this presenter.\r\n        // Second, try the activity that presenter called (could be destroyed).\r\n        if (viewActivity != null) {\r\n            activity = viewActivity;\r\n        } else if (mActivity.get() != null) {\r\n            activity = mActivity.get();\r\n        }\r\n\r\n        // In case view was disposed like SplashView does\r\n        // Fallback to non-localized ApplicationContext if others fail\r\n        return Utils.checkActivity(activity) ? activity : mApplicationContext.get();\r\n    }\r\n\r\n    @Override\r\n    public void onViewInitialized() {\r\n        enableSync();\r\n    }\r\n\r\n    @Override\r\n    public void onViewDestroyed() {\r\n        // Multiple views with same presenter fix?\r\n        // View stays in RAM after has been destroyed. Is it a bug?\r\n        //mView = new WeakReference<>(null);\r\n        //mActivity = new WeakReference<>(null);\r\n    }\r\n\r\n    @Override\r\n    public void onViewPaused() {\r\n        // NOP\r\n    }\r\n\r\n    @Override\r\n    public void onViewResumed() {\r\n        if (canViewBeSynced()) {\r\n            // NOTE: don't place cleanup in the onViewResumed!!! This could cause errors when view is resumed.\r\n            if (syncItem(Playlist.instance().getChangedItems())) {\r\n                Playlist.instance().onNewSession();\r\n            }\r\n        }\r\n\r\n        //showBootDialogs();\r\n    }\r\n\r\n    @Override\r\n    public void onFinish() {\r\n        if (getSearchData().getTempBackgroundModeClass() == this.getClass() &&\r\n            getPlaybackPresenter().isRunningInBackground()) {\r\n            getViewManager().startView(PlaybackView.class);\r\n        }\r\n    }\r\n\r\n    protected void removeItem(Video item) {\r\n        removeItem(Collections.singletonList(item), VideoGroup.ACTION_REMOVE);\r\n    }\r\n\r\n    protected void removeItemAuthor(Video item) {\r\n        removeItem(Collections.singletonList(item), VideoGroup.ACTION_REMOVE_AUTHOR);\r\n    }\r\n\r\n    private void removeItem(List<Video> items, int action) {\r\n        if (items.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        VideoGroup removedGroup = VideoGroup.from(items);\r\n        removedGroup.setAction(action);\r\n        T view = getView();\r\n\r\n        updateView(removedGroup, view);\r\n    }\r\n\r\n    public boolean syncItem(Video item) {\r\n        return syncItem(Collections.singletonList(item));\r\n    }\r\n\r\n    public boolean syncItem(List<Video> items) {\r\n        if (items.isEmpty()) {\r\n            return false;\r\n        }\r\n\r\n        VideoGroup syncGroup = VideoGroup.from(items);\r\n        syncGroup.setAction(VideoGroup.ACTION_SYNC);\r\n        T view = getView();\r\n\r\n        return updateView(syncGroup, view);\r\n    }\r\n\r\n    private boolean canViewBeSynced() {\r\n        T view = getView();\r\n        return view instanceof BrowseView ||\r\n               view instanceof ChannelView ||\r\n               view instanceof ChannelUploadsView ||\r\n               view instanceof SearchView;\r\n    }\r\n\r\n    private boolean updateView(VideoGroup group, T view) {\r\n        if (view instanceof BrowseView) {\r\n            ((BrowseView) view).updateSection(group);\r\n        } else if (view instanceof ChannelView) {\r\n            ((ChannelView) view).update(group);\r\n        } else if (view instanceof ChannelUploadsView) {\r\n            ((ChannelUploadsView) view).update(group);\r\n        } else if (view instanceof SearchView) {\r\n            ((SearchView) view).updateSearch(group);\r\n        } else if (view instanceof PlaybackView) {\r\n            ((PlaybackView) view).updateSuggestions(group);\r\n        } else {\r\n            return false;\r\n        }\r\n\r\n        return true;\r\n    }\r\n\r\n    private void enableSync() {\r\n        if (this instanceof PlaybackPresenter) {\r\n            Playlist.instance().onNewSession();\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Check that view's activity is alive\r\n     */\r\n    private static <T> boolean checkView(T view) {\r\n        Activity activity = getViewActivity(view);\r\n\r\n        return Utils.checkActivity(activity);\r\n    }\r\n\r\n    @SuppressWarnings(\"deprecation\")\r\n    private static <T> Activity getViewActivity(T view) {\r\n        Activity activity = null;\r\n\r\n        if (view instanceof Fragment) { // regular fragment\r\n            activity = ((Fragment) view).getActivity();\r\n        } else if (view instanceof android.app.Fragment) { // dialog fragment\r\n            activity = ((android.app.Fragment) view).getActivity();\r\n        } else if (view instanceof Activity) { // splash view\r\n            activity = (Activity) view;\r\n        } else if (view instanceof View) {\r\n            Context context = ((View) view).getContext();\r\n            if (context instanceof Activity) {\r\n                activity = (Activity) context;\r\n            }\r\n        }\r\n        return activity;\r\n    }\r\n\r\n    protected MainUIData getMainUIData() {\r\n        return MainUIData.instance(getContext());\r\n    }\r\n\r\n    protected GeneralData getGeneralData() {\r\n        return GeneralData.instance(getContext());\r\n    }\r\n\r\n    protected SearchData getSearchData() {\r\n        return SearchData.instance(getContext());\r\n    }\r\n\r\n    protected SidebarService getSidebarService() {\r\n        return SidebarService.instance(getContext());\r\n    }\r\n\r\n    protected CommentsService getCommentsService() {\r\n        return YouTubeServiceManager.instance().getCommentsService();\r\n    }\r\n\r\n    protected ContentService getContentService() {\r\n        return YouTubeServiceManager.instance().getContentService();\r\n    }\r\n\r\n    protected SignInService getSignInService() {\r\n        return YouTubeServiceManager.instance().getSignInService();\r\n    }\r\n\r\n    protected NotificationsService getNotificationsService() {\r\n        return YouTubeServiceManager.instance().getNotificationsService();\r\n    }\r\n\r\n    protected MediaItemService getMediaItemService() {\r\n        return YouTubeServiceManager.instance().getMediaItemService();\r\n    }\r\n\r\n    protected MediaServiceManager getServiceManager() {\r\n        return MediaServiceManager.instance();\r\n    }\r\n\r\n    protected ViewManager getViewManager() {\r\n        return ViewManager.instance(getContext());\r\n    }\r\n\r\n    protected TickleManager getTickleManager() {\r\n        return TickleManager.instance();\r\n    }\r\n\r\n    protected PlaybackPresenter getPlaybackPresenter() {\r\n        return PlaybackPresenter.instance(getContext());\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/ATVBridgePresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\n\r\npublic class ATVBridgePresenter extends BridgePresenter {\r\n    private static final Integer[] ATV_YOUTUBE_PKG_HASH = {-1}; // always reinstall\r\n    private static final String ATV_YOUTUBE_PKG_NAME = \"com.google.android.youtube.tv\";\r\n    private static final String ATV_BRIDGE_PKG_URL = \"https://github.com/yuliskov/SmartTubeNext/releases/download/latest/ATV_SYTV_Bridge.apk\";\r\n    private static ATVBridgePresenter sInstance;\r\n\r\n    public ATVBridgePresenter(Context context) {\r\n        super(context);\r\n    }\r\n\r\n    public static ATVBridgePresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new ATVBridgePresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void unhold() {\r\n        sInstance = null;\r\n    }\r\n\r\n    @Override\r\n    protected String getPackageName() {\r\n        return ATV_YOUTUBE_PKG_NAME;\r\n    }\r\n\r\n    @Override\r\n    protected String getPackageUrl() {\r\n        return ATV_BRIDGE_PKG_URL;\r\n    }\r\n\r\n    @Override\r\n    protected Integer[] getPackageSignatureHash() {\r\n        return ATV_YOUTUBE_PKG_HASH;\r\n    }\r\n\r\n    @Override\r\n    protected boolean checkLauncher() {\r\n        return Helpers.isAndroidTVLauncher(getContext());\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/AccountSelectionPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.mediaserviceinterfaces.SignInService;\r\nimport com.liskovsoft.mediaserviceinterfaces.oauth.Account;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.AccountSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AccountsData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class AccountSelectionPresenter extends BasePresenter<Void> {\r\n    private static final String TAG = AccountSelectionPresenter.class.getSimpleName();\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static AccountSelectionPresenter sInstance;\r\n    private final SignInService mSignInService;\r\n\r\n    public AccountSelectionPresenter(Context context) {\r\n        super(context);\r\n        ServiceManager service = YouTubeServiceManager.instance();\r\n        mSignInService = service.getSignInService();\r\n    }\r\n\r\n    public static AccountSelectionPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new AccountSelectionPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void show() {\r\n        show(false);\r\n    }\r\n\r\n    public void show(boolean force) {\r\n        if (!AccountsData.instance(getContext()).isSelectAccountOnBootEnabled() && !force) {\r\n            // user don't want to see selection dialog\r\n            return;\r\n        }\r\n\r\n        createAndShowDialog(mSignInService.getAccounts(), force);\r\n    }\r\n\r\n    public void nextAccountOrDialog() {\r\n        MediaServiceManager.instance().loadAccounts(this::nextAccountOrDialog);\r\n    }\r\n\r\n    public void unhold() {\r\n        sInstance = null;\r\n    }\r\n\r\n    private void createAndShowDialog(List<Account> accounts, boolean force) {\r\n        if (accounts.size() <= 1 && !force) {\r\n            return;\r\n        }\r\n\r\n        AppDialogPresenter dialogPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendAccountSelection(accounts, dialogPresenter);\r\n\r\n        dialogPresenter.showDialog(getContext().getString(R.string.settings_accounts), this::unhold);\r\n    }\r\n\r\n    private void appendAccountSelection(List<Account> accounts, AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> optionItems = new ArrayList<>();\r\n\r\n        optionItems.add(UiOptionItem.from(\r\n                getContext().getString(R.string.dialog_account_none), optionItem -> {\r\n                    selectAccount(null);\r\n                    settingsPresenter.closeDialog();\r\n                }, true\r\n        ));\r\n\r\n        for (Account account : accounts) {\r\n            optionItems.add(UiOptionItem.from(\r\n                    formatAccount(account), option -> {\r\n                        selectAccount(account);\r\n                        settingsPresenter.closeDialog();\r\n                    }, account.isSelected()\r\n            ));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.dialog_account_list), optionItems);\r\n    }\r\n\r\n    private void nextAccountOrDialog(List<Account> accounts) {\r\n        if (accounts == null || accounts.isEmpty()) {\r\n            AccountSettingsPresenter.instance(getContext()).show();\r\n            return;\r\n        }\r\n\r\n        Account current = null;\r\n\r\n        for (Account account : accounts) {\r\n            if (account.isSelected()) {\r\n                current = account;\r\n                break;\r\n            }\r\n        }\r\n\r\n        int index = accounts.indexOf(current);\r\n\r\n        int nextIndex = index + 1;\r\n        // null == 'without account'\r\n        selectAccount(nextIndex == accounts.size() ? null : accounts.get(nextIndex));\r\n        //selectAccount(accounts.get(nextIndex == accounts.size() ? 0 : nextIndex));\r\n    }\r\n\r\n    public void selectAccount(Account account) {\r\n        mSignInService.selectAccount(account);\r\n        Utils.updateChannels(getContext());\r\n\r\n        // Account history might be turned off (common issue).\r\n        GeneralData generalData = GeneralData.instance(getContext());\r\n        if (generalData.getHistoryState() != GeneralData.HISTORY_AUTO) {\r\n            MediaServiceManager.instance().enableHistory(generalData.isHistoryEnabled());\r\n        }\r\n    }\r\n\r\n    private String formatAccount(Account account) {\r\n        String format;\r\n\r\n        if (account.getEmail() != null) {\r\n            format = String.format(\"%s (%s)\", account.getName(), account.getEmail());\r\n        } else {\r\n            format = account.getName();\r\n        }\r\n\r\n        return format;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/AmazonBridgePresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\n\r\npublic class AmazonBridgePresenter extends BridgePresenter {\r\n    private static final Integer[] AMAZON_YOUTUBE_PKG_HASH = {-1}; // always reinstall\r\n    private static final String AMAZON_YOUTUBE_PKG_NAME = \"com.amazon.firetv.youtube\";\r\n    private static final String AMAZON_BRIDGE_PKG_URL = \"https://github.com/yuliskov/SmartTubeNext/releases/download/latest/Amazon_SYTV_Bridge.apk\";\r\n    private static AmazonBridgePresenter sInstance;\r\n\r\n    public AmazonBridgePresenter(Context context) {\r\n        super(context);\r\n    }\r\n\r\n    public static AmazonBridgePresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new AmazonBridgePresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void unhold() {\r\n        sInstance = null;\r\n    }\r\n\r\n    @Override\r\n    protected String getPackageName() {\r\n        return AMAZON_YOUTUBE_PKG_NAME;\r\n    }\r\n\r\n    @Override\r\n    protected String getPackageUrl() {\r\n        return AMAZON_BRIDGE_PKG_URL;\r\n    }\r\n\r\n    @Override\r\n    protected Integer[] getPackageSignatureHash() {\r\n        return AMAZON_YOUTUBE_PKG_HASH;\r\n    }\r\n\r\n    @Override\r\n    protected boolean checkLauncher() {\r\n        return Helpers.isAmazonFireTVDevice();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/AppUpdatePresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport com.liskovsoft.appupdatechecker2.AppUpdateChecker;\r\nimport com.liskovsoft.appupdatechecker2.AppUpdateCheckerListener;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.errors.ErrorFragmentData;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.BrowsePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.LoadingManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class AppUpdatePresenter extends BasePresenter<Void> implements AppUpdateCheckerListener {\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static AppUpdatePresenter sInstance;\r\n    private final AppUpdateChecker mUpdateChecker;\r\n    private final AppDialogPresenter mSettingsPresenter;\r\n    private final String[] mUpdateManifestUrls;\r\n    private boolean mIsForceCheck;\r\n\r\n    public AppUpdatePresenter(Context context) {\r\n        super(context);\r\n        mUpdateChecker = new AppUpdateChecker(context, this);\r\n        mSettingsPresenter = AppDialogPresenter.instance(context);\r\n        mUpdateManifestUrls = context.getResources().getStringArray(R.array.update_urls);\r\n    }\r\n\r\n    public static AppUpdatePresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new AppUpdatePresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public static void unhold() {\r\n        sInstance = null;\r\n    }\r\n\r\n    public void start(boolean forceCheck) {\r\n        mIsForceCheck = forceCheck;\r\n\r\n        if (forceCheck) {\r\n            LoadingManager.showLoading(getContext(), true);\r\n            mUpdateChecker.forceCheckForUpdates(mUpdateManifestUrls);\r\n        } else {\r\n            mUpdateChecker.checkForUpdates(mUpdateManifestUrls);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onUpdateFound(String versionName, List<String> changelog, String apkPath) {\r\n        if (mIsForceCheck) {\r\n            LoadingManager.showLoading(getContext(), false);\r\n            showUpdateDialog(versionName, changelog, apkPath);\r\n        } else if (GeneralData.instance(getContext()).isOldUpdateNotificationsEnabled()) {\r\n            showUpdateDialog(versionName, changelog, apkPath);\r\n        } else {\r\n            pinUpdateSection(versionName, changelog, apkPath);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onUpdateError(Exception error) {\r\n        if (mIsForceCheck) {\r\n            LoadingManager.showLoading(getContext(), false);\r\n\r\n            if (AppUpdateCheckerListener.LATEST_VERSION.equals(error.getMessage())) {\r\n                MessageHelpers.showMessage(getContext(), R.string.update_not_found);\r\n            } else {\r\n                MessageHelpers.showMessage(getContext(), String.format(\"%s: %s\", getContext().getString(R.string.update_error),\r\n                        error.getCause() != null ? error.getCause().getMessage() : error.getMessage()));\r\n            }\r\n        }\r\n\r\n        onFinish();\r\n    }\r\n\r\n    private void showUpdateDialog(String versionName, List<String> changelog, String apkPath) {\r\n        // Don't show update dialog if the player opened or the app is collapsed\r\n        if (getContext() == null || getViewManager().isPlayerInForeground() || !Utils.isAppInForegroundFixed()) {\r\n            return;\r\n        }\r\n\r\n        mSettingsPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.install_update), optionItem -> {\r\n                    GeneralData.instance(getContext()).setChangelog(changelog);\r\n                    mUpdateChecker.installUpdate();\r\n                }, false));\r\n        mSettingsPresenter.appendStringsCategory(getContext().getString(R.string.update_changelog), createChangelogOptions(changelog));\r\n        //mSettingsPresenter.appendSingleSwitch(UiOptionItem.from(getContext().getString(R.string.show_again), optionItem -> {\r\n        //    mUpdateChecker.enableUpdateCheck(optionItem.isSelected());\r\n        //}, mUpdateChecker.isUpdateCheckEnabled()));\r\n\r\n        //mSettingsPresenter.setOnFinish(getOnFinish());\r\n        mSettingsPresenter.showDialog(String.format(\"%s %s\", getContext().getString(R.string.app_name), versionName), AppUpdatePresenter::unhold);\r\n    }\r\n\r\n    private void pinUpdateSection(String versionName, List<String> changelog, String apkPath) {\r\n        // Don't show update dialog if the player opened or the app is collapsed\r\n        if (getContext() == null) {\r\n            return;\r\n        }\r\n\r\n        BrowsePresenter.instance(getContext()).pinItem(getContext().getString(R.string.update_found), R.drawable.action_info, new ErrorFragmentData() {\r\n            @Override\r\n            public void onAction() {\r\n                GeneralData.instance(getContext()).setChangelog(changelog);\r\n                mUpdateChecker.installUpdate();\r\n            }\r\n\r\n            @Override\r\n            public String getMessage() {\r\n                return String.format(\"%s %s\", getContext().getString(R.string.app_name), versionName) + \" \" +\r\n                        getContext().getString(R.string.update_changelog) + \":\\n\" +\r\n                        createChangelog(changelog);\r\n            }\r\n\r\n            @Override\r\n            public String getActionText() {\r\n                return getContext().getString(R.string.install_update);\r\n            }\r\n        });\r\n    }\r\n\r\n    private List<OptionItem> createChangelogOptions(List<String> changelog) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (String change : changelog) {\r\n            options.add(UiOptionItem.from(change));\r\n        }\r\n\r\n        return options;\r\n    }\r\n\r\n    private String createChangelog(List<String> changelog) {\r\n        StringBuilder builder = new StringBuilder();\r\n\r\n        int maxLines = 30;\r\n        int lineNum = 0;\r\n\r\n        for (String change : changelog) {\r\n            if (lineNum > maxLines) {\r\n                break;\r\n            }\r\n\r\n            builder.append(\"- \");\r\n            builder.append(change);\r\n            builder.append(\"\\n\");\r\n\r\n            lineNum++;\r\n        }\r\n\r\n        return builder.toString();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/BootDialogPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\n\r\n/**\r\n * Shows boot dialogs one by one.\r\n */\r\npublic class BootDialogPresenter extends BasePresenter<Void> {\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static BootDialogPresenter sInstance;\r\n\r\n    public BootDialogPresenter(Context context) {\r\n        super(context);\r\n    }\r\n\r\n    public static BootDialogPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new BootDialogPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    //public void unhold() {\r\n    //    sInstance = null;\r\n    //}\r\n\r\n    public void start() {\r\n        startUpdatePresenter();\r\n    }\r\n\r\n    private void startUpdatePresenter() {\r\n        AppUpdatePresenter updatePresenter = AppUpdatePresenter.instance(getContext());\r\n        //updatePresenter.setOnDone(this::startBridgePresenter);\r\n        updatePresenter.start(false);\r\n        //updatePresenter.unhold();\r\n    }\r\n\r\n    //private void startBackupPresenter() {\r\n    //    QuickRestorePresenter quickRestorePresenter = QuickRestorePresenter.instance(getContext());\r\n    //    quickRestorePresenter.setOnDone(this::startBridgePresenter);\r\n    //    quickRestorePresenter.start();\r\n    //    quickRestorePresenter.unhold();\r\n    //}\r\n\r\n    //private void startBridgePresenter() {\r\n    //    ATVBridgePresenter atvPresenter = ATVBridgePresenter.instance(getContext());\r\n    //    atvPresenter.start();\r\n    //    atvPresenter.unhold();\r\n    //\r\n    //    AmazonBridgePresenter amazonPresenter = AmazonBridgePresenter.instance(getContext());\r\n    //    amazonPresenter.start();\r\n    //    amazonPresenter.unhold();\r\n    //}\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/BridgePresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs;\r\n\r\nimport android.app.Activity;\r\nimport android.content.Context;\r\nimport android.content.Intent;\r\nimport android.content.pm.PackageInfo;\r\nimport android.content.pm.PackageManager;\r\nimport android.content.pm.PackageManager.NameNotFoundException;\r\nimport android.net.Uri;\r\nimport com.liskovsoft.appupdatechecker2.other.downloadmanager.DownloadManagerTask;\r\nimport com.liskovsoft.appupdatechecker2.other.downloadmanager.DownloadManagerTask.DownloadListener;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MotherActivity;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.LoadingManager;\r\n\r\nabstract class BridgePresenter extends BasePresenter<Void> implements MotherActivity.OnResult {\r\n    private static final String TAG = BridgePresenter.class.getSimpleName();\r\n    private final GeneralData mGeneralData;\r\n    private boolean mRemoveOldApkFirst;\r\n    private String mBridgePath;\r\n    private boolean mForceInstall;\r\n    private final DownloadListener mListener = new DownloadListener() {\r\n        @Override\r\n        public void onDownloadCompleted(Uri uri) {\r\n            mBridgePath = uri.getPath();\r\n\r\n            if (mBridgePath != null) {\r\n                if (mForceInstall) {\r\n                    installBridgeIfNeeded();\r\n                } else {\r\n                    startDialog();\r\n                }\r\n            }\r\n        }\r\n    };\r\n\r\n    public BridgePresenter(Context context) {\r\n        super(context);\r\n\r\n        mGeneralData = GeneralData.instance(context);\r\n    }\r\n\r\n    public void start() {\r\n        if (!mGeneralData.isBridgeCheckEnabled()) {\r\n            onFinish();\r\n            return;\r\n        }\r\n        \r\n        runBridgeInstaller(false);\r\n    }\r\n\r\n    private void startDialog() {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        OptionItem updateCheckOption = UiOptionItem.from(\r\n                getContext().getString(R.string.enable_voice_search),\r\n                option -> installBridgeIfNeeded());\r\n\r\n        settingsPresenter.appendSingleSwitch(\r\n                UiOptionItem.from(getContext().getString(R.string.show_again),\r\n                optionItem -> mGeneralData.setBridgeCheckEnabled(optionItem.isSelected()),\r\n                mGeneralData.isBridgeCheckEnabled())\r\n        );\r\n        settingsPresenter.appendSingleButton(updateCheckOption);\r\n\r\n        //settingsPresenter.setOnFinish(getOnFinish());\r\n        settingsPresenter.showDialog(getContext().getString(R.string.enable_voice_search));\r\n    }\r\n\r\n    public void runBridgeInstaller(boolean force) {\r\n        mForceInstall = force;\r\n\r\n        if (!checkLauncher()) {\r\n            onFinish();\r\n            return;\r\n        }\r\n\r\n        if (mForceInstall) {\r\n            LoadingManager.showLoading(getContext(), true);\r\n        }\r\n\r\n        // Original tube installed\r\n        mRemoveOldApkFirst = isOldApkInstalled();\r\n        // Download apk first and start dialog when download complete\r\n        downloadPackageFromUrl(getContext(), getPackageUrl());\r\n    }\r\n\r\n    private boolean isOldApkInstalled() {\r\n        PackageInfo info = getPackageSignature(getPackageName());\r\n\r\n        return info != null && !Helpers.equalsAny(info.signatures[0].hashCode(), getPackageSignatureHash());\r\n    }\r\n\r\n    private PackageInfo getPackageSignature(String pkgName) {\r\n        PackageManager manager = getContext().getPackageManager();\r\n        PackageInfo packageInfo = null;\r\n\r\n        try {\r\n            packageInfo = manager.getPackageInfo(pkgName, PackageManager.GET_SIGNATURES);\r\n        } catch (NameNotFoundException e) {\r\n            e.printStackTrace();\r\n        }\r\n\r\n        return packageInfo;\r\n    }\r\n\r\n    private void downloadPackageFromUrl(Context context, String pkgUrl) {\r\n        Log.d(TAG, \"Installing bridge apk\");\r\n\r\n        DownloadManagerTask task = new DownloadManagerTask(mListener, context, pkgUrl);\r\n        task.execute();\r\n    }\r\n\r\n    private void installBridgeFromPath(Context context) {\r\n        Helpers.installPackage(context, mBridgePath);\r\n    }\r\n\r\n    @Override\r\n    public void onResult(int requestCode, int resultCode, Intent data) {\r\n        if (requestCode == Helpers.REMOVE_PACKAGE_CODE && !isOldApkInstalled()) {\r\n            installBridgeFromPath(getContext());\r\n        } else {\r\n            MessageHelpers.showMessage(getContext(), \"The package \" + getPackageName() + \" cannot be uninstalled!\");\r\n        }\r\n    }\r\n\r\n    private void installBridgeIfNeeded() {\r\n        LoadingManager.showLoading(getContext(), false);\r\n\r\n        if (mRemoveOldApkFirst) {\r\n            if (getContext() instanceof MotherActivity) {\r\n                ((MotherActivity) getContext()).addOnResult(this);\r\n\r\n                Helpers.removePackageAndGetResult((Activity) getContext(), getPackageName());\r\n            }\r\n        } else {\r\n            installBridgeFromPath(getContext());\r\n        }\r\n    }\r\n\r\n    protected abstract String getPackageName();\r\n    protected abstract String getPackageUrl();\r\n    protected abstract Integer[] getPackageSignatureHash();\r\n    protected abstract boolean checkLauncher();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/QuickRestorePresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.BackupAndRestoreManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\npublic class QuickRestorePresenter extends BasePresenter<Void> {\r\n    private static QuickRestorePresenter sInstance;\r\n\r\n    public QuickRestorePresenter(Context context) {\r\n        super(context);\r\n    }\r\n\r\n    public static QuickRestorePresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new QuickRestorePresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void unhold() {\r\n        sInstance = null;\r\n    }\r\n\r\n    public void start() {\r\n        BackupAndRestoreManager backupManager = new BackupAndRestoreManager(getContext());\r\n\r\n        // NOTE: we don't have storage permission yet\r\n        if (Utils.isFirstRun(getContext()) && backupManager.hasBackup()) {\r\n            AppDialogUtil.showConfirmationDialog(getContext(), getContext().getString(R.string.app_restore), () -> {\r\n                backupManager.checkPermAndRestore();\r\n                MessageHelpers.showMessage(getContext(), R.string.msg_done);\r\n            }, this::onFinish);\r\n        } else {\r\n            onFinish();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/StableRestorePresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs;\r\n\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.DeviceHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.DeviceHelpers.ArchType;\r\n\r\npublic class StableRestorePresenter extends BridgePresenter {\r\n    private static final Integer[] STABLE_PKG_HASH = {-1}; // always re-install\r\n    private static final String STABLE_PKG_NAME = \"com.teamsmart.videomanager.tv\";\r\n    private static final String BETA_PKG_NAME = \"com.liskovsoft.smarttubetv.beta\";\r\n    private static final String STABLE_PKG_URL_ARM = \"https://github.com/yuliskov/SmartTube/releases/download/latest/smarttube_stable.apk\";\r\n    private static final String STABLE_PKG_URL_ARM64 = \"https://github.com/yuliskov/SmartTube/releases/download/latest/smarttube_stable_arm64-v8a.apk\";\r\n    private static final String STABLE_PKG_URL_X86 = \"https://github.com/yuliskov/SmartTube/releases/download/latest/smarttube_stable_x86.apk\";\r\n    private static StableRestorePresenter sInstance;\r\n\r\n    public StableRestorePresenter(Context context) {\r\n        super(context);\r\n    }\r\n\r\n    public static StableRestorePresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new StableRestorePresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void unhold() {\r\n        sInstance = null;\r\n    }\r\n\r\n    @Override\r\n    protected String getPackageName() {\r\n        return STABLE_PKG_NAME;\r\n    }\r\n\r\n    @Override\r\n    protected String getPackageUrl() {\r\n        ArchType archType = DeviceHelpers.getArchType();\r\n\r\n        switch (archType) {\r\n            case ARM_64:\r\n                return STABLE_PKG_URL_ARM64;\r\n            case X86_64:\r\n            case X86:\r\n                return STABLE_PKG_URL_X86;\r\n            default:\r\n                return STABLE_PKG_URL_ARM;\r\n        }\r\n    }\r\n\r\n    @Override\r\n    protected Integer[] getPackageSignatureHash() {\r\n        return STABLE_PKG_HASH;\r\n    }\r\n\r\n    @Override\r\n    protected boolean checkLauncher() {\r\n        return true;\r\n    }\r\n\r\n    public boolean isSupported() {\r\n        return getContext().getPackageName().equals(BETA_PKG_NAME);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/VideoActionPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs;\r\n\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.ChannelUploadsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.SearchPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.LoadingManager;\r\n\r\npublic class VideoActionPresenter extends BasePresenter<Void> {\r\n    private static final String TAG = VideoActionPresenter.class.getSimpleName();\r\n\r\n    private VideoActionPresenter(Context context) {\r\n        super(context);\r\n    }\r\n\r\n    public static VideoActionPresenter instance(Context context) {\r\n        return new VideoActionPresenter(context);\r\n    }\r\n\r\n    public void apply(Video item) {\r\n        if (item == null) {\r\n            return;\r\n        }\r\n\r\n        // Show playlist contents in channel instead of instant playback\r\n        if (item.hasVideo() && !item.isBadgePlaylistInChannel()) {\r\n            PlaybackPresenter.instance(getContext()).openVideo(item);\r\n        } else if (item.hasChannel() || (item.belongsToChannelUploads() && item.hasNestedItems())) {\r\n            MediaServiceManager.chooseChannelPresenter(getContext(), item);\r\n        } else if (item.hasPlaylist() || item.hasNestedItems()) {\r\n            if (item.belongsToMusic()) {\r\n                startFistPlaylistItem(item);\r\n            } else {\r\n                ChannelUploadsPresenter.instance(getContext()).openChannel(item);\r\n            }\r\n        } else if (item.isChapter) {\r\n            PlaybackPresenter.instance(getContext()).setPosition(item.startTimeMs);\r\n        } else if (item.searchQuery != null ) {\r\n            SearchPresenter.instance(getContext()).onSearch(item.searchQuery);\r\n        } else {\r\n            MessageHelpers.showMessage(getContext(), \"Video item doesn't contain needed data!\");\r\n        }\r\n    }\r\n\r\n    private void startFistPlaylistItem(Video item) {\r\n        LoadingManager.showLoading(getContext(), true);\r\n        ChannelUploadsPresenter.instance(getContext()).obtainGroup(item, mediaGroup -> {\r\n            LoadingManager.showLoading(getContext(), false);\r\n            if (!mediaGroup.isEmpty()) {\r\n                PlaybackPresenter.instance(getContext()).openVideo(Video.from(mediaGroup.getMediaItems().get(0)));\r\n            }\r\n        },\r\n        e -> LoadingManager.showLoading(getContext(), false),\r\n        () -> LoadingManager.showLoading(getContext(), false));\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/menu/BaseMenuPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItem;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.PlaylistInfo;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.BrowseSection;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.BrowsePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.AccountSelectionPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.AppUpdatePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter.VideoMenuCallback;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.SimpleEditDialog;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeMediaItemService;\r\nimport io.reactivex.Observable;\r\n\r\nimport java.util.List;\r\n\r\npublic abstract class BaseMenuPresenter extends BasePresenter<Void> {\r\n    private final MediaServiceManager mServiceManager;\r\n    private boolean mIsPinToSidebarEnabled;\r\n    private boolean mIsSavePlaylistEnabled;\r\n    private boolean mIsCreatePlaylistEnabled;\r\n    private boolean mIsAccountSelectionEnabled;\r\n    private boolean mIsAddToNewPlaylistEnabled;\r\n    private boolean mIsToggleHistoryEnabled;\r\n    private boolean mIsClearHistoryEnabled;\r\n    private boolean mIsUpdateCheckEnabled;\r\n    private boolean mIsExcludeFromContentBlockEnabled;\r\n    private boolean mIsRenamePlaylistEnabled;\r\n\r\n    protected BaseMenuPresenter(Context context) {\r\n        super(context);\r\n        mServiceManager = MediaServiceManager.instance();\r\n        updateEnabledMenuItems();\r\n    }\r\n\r\n    protected abstract Video getVideo();\r\n    protected BrowseSection getSection() { return null; }\r\n    protected abstract AppDialogPresenter getDialogPresenter();\r\n    protected abstract VideoMenuCallback getCallback();\r\n\r\n    public void closeDialog() {\r\n        if (getDialogPresenter() != null) {\r\n            getDialogPresenter().closeDialog();\r\n        }\r\n        MessageHelpers.cancelToasts();\r\n    }\r\n\r\n    protected void appendTogglePinVideoToSidebarButton() {\r\n        if (!mIsPinToSidebarEnabled) {\r\n            return;\r\n        }\r\n\r\n        Video original = getVideo();\r\n\r\n        if (original == null || (!original.hasPlaylist() && !original.hasVideo() && !original.hasReloadPageKey() && !original.hasChannel())) {\r\n            return;\r\n        }\r\n\r\n        boolean isPlaylist = original.hasPlaylist() || original.isPlaylistAsChannel() || (original.hasNestedItems() && original.belongsToUserPlaylists());\r\n        getDialogPresenter().appendSingleButton(\r\n                UiOptionItem.from(\r\n                        getContext().getString(isPlaylist ? R.string.pin_playlist : R.string.pin_channel),\r\n                        optionItem -> {\r\n                            if (original.hasPlaylist()) {\r\n                                togglePinToSidebar(createPinnedPlaylist(original));\r\n                            } else if (original.hasVideo()) {\r\n                                MessageHelpers.showMessage(getContext(), R.string.wait_data_loading);\r\n\r\n                                mServiceManager.loadMetadata(\r\n                                        original,\r\n                                        metadata -> {\r\n                                            Video video = Video.from(original);\r\n                                            video.sync(metadata);\r\n                                            togglePinToSidebar(createPinnedChannel(video));\r\n                                        }\r\n                                );\r\n                            } else {\r\n                                togglePinToSidebar(createPinnedChannel(original));\r\n                            }\r\n                        }));\r\n    }\r\n    \r\n    private void togglePinToSidebar(Video section) {\r\n        BrowsePresenter presenter = BrowsePresenter.instance(getContext());\r\n\r\n        // Toggle between pin/unpin while dialog is opened\r\n        boolean isItemPinned = presenter.isItemPinned(section);\r\n\r\n        if (isItemPinned && section.getGroup() == null) { // allow deletion only from the Sidebar\r\n            presenter.unpinItem(section);\r\n            MessageHelpers.showMessage(getContext(), getContext().getString(R.string.unpinned_from_sidebar));\r\n        } else {\r\n            presenter.pinItem(section);\r\n            section.setGroup(null);\r\n            MessageHelpers.showMessage(getContext(), getContext().getString(R.string.pinned_to_sidebar));\r\n        }\r\n    }\r\n\r\n    private static Video createPinnedPlaylist(Video video) {\r\n        if (video == null) {\r\n            return null;\r\n        }\r\n\r\n        Video section = Video.from(video);\r\n        section.videoId = section.channelId = section.reloadPageKey = null; // reset to proper comparison\r\n        section.title = video.createPlaylistTitle();\r\n\r\n        return section;\r\n    }\r\n\r\n    private static Video createPinnedChannel(Video video) {\r\n        if (video == null) {\r\n            return null;\r\n        }\r\n\r\n        Video section = Video.from(video);\r\n        section.videoId = section.playlistId = section.playlistParams = null; // reset to proper comparison\r\n        section.title = video.createChannelTitle();\r\n\r\n        return section;\r\n    }\r\n\r\n    protected void appendUnpinVideoFromSidebarButton() {\r\n        if (!mIsPinToSidebarEnabled) {\r\n            return;\r\n        }\r\n\r\n        Video video = getVideo();\r\n\r\n        if (video == null || (!video.hasPlaylist() && !video.hasReloadPageKey() && !video.hasChannel())) {\r\n            return;\r\n        }\r\n\r\n        getDialogPresenter().appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.unpin_from_sidebar),\r\n                        optionItem -> {\r\n                            togglePinToSidebar(video);\r\n                            closeDialog();\r\n                        }));\r\n    }\r\n\r\n    protected void appendUnpinSectionFromSidebarButton() {\r\n        if (!mIsPinToSidebarEnabled) {\r\n            return;\r\n        }\r\n\r\n        BrowseSection section = getSection();\r\n\r\n        if (section == null || section.getId() == MediaGroup.TYPE_SETTINGS || getVideo() != null) {\r\n            return;\r\n        }\r\n\r\n        getDialogPresenter().appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.unpin_from_sidebar),\r\n                        optionItem -> {\r\n                            BrowsePresenter.instance(getContext()).enableSection(section.getId(), false);\r\n                            closeDialog();\r\n                        }));\r\n    }\r\n\r\n    protected void appendAccountSelectionButton() {\r\n        if (!mIsAccountSelectionEnabled) {\r\n            return;\r\n        }\r\n\r\n        getDialogPresenter().appendSingleButton(\r\n                UiOptionItem.from(\r\n                        getContext().getString(R.string.dialog_account_list),\r\n                        optionItem -> AccountSelectionPresenter.instance(getContext()).show(true)\r\n                ));\r\n    }\r\n\r\n    protected void appendSaveRemovePlaylistButton() {\r\n        if (!mIsSavePlaylistEnabled) {\r\n            return;\r\n        }\r\n\r\n        Video original = getVideo();\r\n\r\n        if (original == null || (!original.hasPlaylist() && !original.isPlaylistAsChannel() && !original.belongsToUserPlaylists())) {\r\n            return;\r\n        }\r\n\r\n        getDialogPresenter().appendSingleButton(\r\n                UiOptionItem.from(\r\n                        getContext().getString(original.belongsToUserPlaylists()? R.string.remove_playlist : R.string.save_playlist),\r\n                        optionItem -> {\r\n                            if (original.hasPlaylist()) {\r\n                                syncToggleSaveRemovePlaylist(original, null);\r\n                            } else if (original.belongsToUserPlaylists()) {\r\n                                // NOTE: Can't get empty playlist id. Empty playlist doesn't contain videos.\r\n                                mServiceManager.loadChannelUploads(\r\n                                        original,\r\n                                        mediaGroup -> {\r\n                                            String playlistId = getFirstPlaylistId(mediaGroup);\r\n\r\n                                            if (playlistId != null) {\r\n                                                syncToggleSaveRemovePlaylist(original, playlistId);\r\n                                            } else {\r\n                                                // Empty playlist fix. Get 'add to playlist' infos\r\n                                                mServiceManager.getPlaylistInfos(playlistInfos -> {\r\n                                                    List<PlaylistInfo> infos = Helpers.filter(playlistInfos, value -> Helpers.equals(\r\n                                                            value.getTitle(), original.getTitle()));\r\n\r\n                                                    String playlistId2 = null;\r\n\r\n                                                    // Multiple playlists may share same name\r\n                                                    if (infos != null && infos.size() == 1) {\r\n                                                        playlistId2 = infos.get(0).getPlaylistId();\r\n                                                    }\r\n\r\n                                                    syncToggleSaveRemovePlaylist(original, playlistId2);\r\n                                                });\r\n                                            }\r\n                                        }\r\n                                );\r\n                            } else {\r\n                                mServiceManager.loadChannelPlaylist(\r\n                                        original,\r\n                                        mediaGroup -> syncToggleSaveRemovePlaylist(original, getFirstPlaylistId(mediaGroup))\r\n                                );\r\n                            }\r\n                        }\r\n                ));\r\n    }\r\n\r\n    private void syncToggleSaveRemovePlaylist(Video original, String playlistId) {\r\n        Video video = Video.from(original);\r\n\r\n        // Need correct playlist title to further comparison (decide whether save or remove)\r\n        if (original.getGroup() != null && original.belongsToSamePlaylistGroup()) {\r\n            video.title = original.getGroup().getTitle();\r\n        }\r\n\r\n        if (!original.hasPlaylist() && playlistId != null) {\r\n            video.playlistId = playlistId;\r\n        }\r\n\r\n        toggleSaveRemovePlaylist(video);\r\n    }\r\n\r\n    private void toggleSaveRemovePlaylist(Video video) {\r\n        mServiceManager.loadPlaylists(video, group -> {\r\n            if (group.getMediaItems() == null)\r\n                return;\r\n\r\n            boolean isSaved = false;\r\n\r\n            for (MediaItem playlist : group.getMediaItems()) {\r\n                if (playlist.getTitle().contains(video.getTitle())) {\r\n                    isSaved = true;\r\n                    break;\r\n                }\r\n            }\r\n\r\n            if (isSaved && video.belongsToUserPlaylists()) { // allow deletion only from the Playlists section\r\n                if (video.playlistId == null) {\r\n                    MessageHelpers.showMessage(getContext(), R.string.cant_delete_empty_playlist);\r\n                } else {\r\n                    AppDialogUtil.showConfirmationDialog(getContext(), getContext().getString(R.string.remove_playlist_fmt, video.title), () -> {\r\n                        removePlaylist(video);\r\n                        closeDialog();\r\n                    });\r\n                }\r\n            } else {\r\n                savePlaylist(video);\r\n            }\r\n        });\r\n    }\r\n\r\n    private void removePlaylist(Video video) {\r\n        MediaItemService manager = YouTubeMediaItemService.instance();\r\n        Observable<Void> action = manager.removePlaylistObserve(video.playlistId);\r\n        RxHelper.execute(action,\r\n                (error) -> MessageHelpers.showMessage(getContext(), error.getLocalizedMessage()),\r\n                () -> {\r\n                    if (getCallback() != null) {\r\n                        getCallback().onItemAction(getVideo(), VideoMenuCallback.ACTION_REMOVE);\r\n                    }\r\n                    GeneralData.instance(getContext()).setPlaylistOrder(video.playlistId, -1);\r\n                    MessageHelpers.showMessage(getContext(), getContext().getString(R.string.removed_from_playlists));\r\n                }\r\n        );\r\n    }\r\n\r\n    private void savePlaylist(Video video) {\r\n        MediaItemService manager = YouTubeMediaItemService.instance();\r\n        Observable<Void> action = video.mediaItem != null && video.mediaItem.getPlaylistId() != null ? manager.savePlaylistObserve(video.mediaItem) : manager.savePlaylistObserve(video.playlistId);\r\n        RxHelper.execute(action,\r\n                (error) -> MessageHelpers.showMessage(getContext(), error.getLocalizedMessage()),\r\n                () -> MessageHelpers.showMessage(getContext(), getContext().getString(R.string.saved_to_playlists))\r\n        );\r\n    }\r\n\r\n    private String getFirstPlaylistId(MediaGroup mediaGroup) {\r\n        if (mediaGroup != null && mediaGroup.getMediaItems() != null) {\r\n            MediaItem first = mediaGroup.getMediaItems().get(0);\r\n            return first.getPlaylistId();\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    protected void appendCreatePlaylistButton() {\r\n        if (!mIsCreatePlaylistEnabled) {\r\n            return;\r\n        }\r\n\r\n        Video original = getVideo() != null ? getVideo() : new Video();\r\n\r\n        BrowsePresenter presenter = BrowsePresenter.instance(getContext());\r\n\r\n        if (original.hasVideo() || !(presenter.isPlaylistsSection() && presenter.inForeground())) {\r\n            return;\r\n        }\r\n\r\n        getDialogPresenter().appendSingleButton(\r\n                UiOptionItem.from(\r\n                        getContext().getString(R.string.create_playlist),\r\n                        optionItem -> showCreatePlaylistDialog(original)\r\n                        ));\r\n    }\r\n\r\n    protected void appendAddToNewPlaylistButton() {\r\n        if (!mIsAddToNewPlaylistEnabled) {\r\n            return;\r\n        }\r\n\r\n        Video original = getVideo() != null ? getVideo() : new Video();\r\n\r\n        if (!original.hasVideo()) {\r\n            return;\r\n        }\r\n\r\n        getDialogPresenter().appendSingleButton(\r\n                UiOptionItem.from(\r\n                        getContext().getString(R.string.add_video_to_new_playlist),\r\n                        optionItem -> showCreatePlaylistDialog(original)\r\n                ));\r\n    }\r\n\r\n    private void showCreatePlaylistDialog(Video video) {\r\n        closeDialog();\r\n        SimpleEditDialog.show(\r\n                getContext(),\r\n                getContext().getString(R.string.create_playlist),\r\n                getContext().getString(R.string.create_playlist_note),\r\n                null,\r\n                newValue -> {\r\n                    MediaItemService manager = YouTubeMediaItemService.instance();\r\n                    Observable<Void> action = video.mediaItem != null ?\r\n                            manager.createPlaylistObserve(newValue, video.hasVideo() ? video.mediaItem : null) :\r\n                            manager.createPlaylistObserve(newValue, video.hasVideo() ? video.videoId : null);\r\n                    RxHelper.execute(\r\n                            action,\r\n                            (error) -> MessageHelpers.showMessage(getContext(), error.getLocalizedMessage()),\r\n                            () -> {\r\n                                if (!video.hasVideo()) { // Playlists section\r\n                                    BrowsePresenter.instance(getContext()).refresh();\r\n                                } else {\r\n                                    MessageHelpers.showMessage(getContext(), getContext().getString(R.string.saved_to_playlists));\r\n                                }\r\n                            }\r\n                    );\r\n                    return true;\r\n                });\r\n    }\r\n\r\n    protected void appendRenamePlaylistButton() {\r\n        if (!mIsRenamePlaylistEnabled) {\r\n            return;\r\n        }\r\n\r\n        Video original = getVideo();\r\n\r\n        BrowsePresenter presenter = BrowsePresenter.instance(getContext());\r\n\r\n        if (original == null || !(presenter.isPlaylistsSection() && presenter.inForeground())) {\r\n            return;\r\n        }\r\n\r\n        getDialogPresenter().appendSingleButton(\r\n                UiOptionItem.from(\r\n                        getContext().getString(R.string.rename_playlist),\r\n                        optionItem -> showRenamePlaylistDialog(original)\r\n                ));\r\n    }\r\n\r\n    private void showRenamePlaylistDialog(Video video) {\r\n        if (video.hasPlaylist()) {\r\n            showRenamePlaylistDialogSimple(video);\r\n        } else {\r\n            showRenamePlaylistDialogUploads(video);\r\n        }\r\n    }\r\n\r\n    private void showRenamePlaylistDialogUploads(Video video) {\r\n        MessageHelpers.showMessage(getContext(), R.string.wait_data_loading);\r\n        mServiceManager.loadChannelUploads(\r\n                video,\r\n                mediaGroup -> {\r\n                    if (mediaGroup.getMediaItems() == null) { // crash fix\r\n                        return;\r\n                    }\r\n\r\n                    MediaItem firstItem = mediaGroup.getMediaItems().get(0);\r\n                    video.playlistId = firstItem.getPlaylistId();\r\n\r\n                    showRenamePlaylistDialogSimple(video);\r\n                }\r\n        );\r\n    }\r\n\r\n    private void showRenamePlaylistDialogSimple(Video video) {\r\n        if (video.getPlaylistId() == null) {\r\n            MessageHelpers.showMessage(getContext(), R.string.cant_rename_empty_playlist);\r\n            return;\r\n        }\r\n\r\n        closeDialog();\r\n\r\n        SimpleEditDialog.show(\r\n                getContext(),\r\n                getContext().getString(R.string.rename_playlist),\r\n                video.getTitle(),\r\n                newValue -> {\r\n                    MediaItemService manager = YouTubeMediaItemService.instance();\r\n                    Observable<Void> action = manager.renamePlaylistObserve(video.getPlaylistId(), newValue);\r\n                    RxHelper.execute(\r\n                            action,\r\n                            (error) -> MessageHelpers.showMessage(getContext(), R.string.owned_playlist_warning),\r\n                            () -> {\r\n                                video.title = newValue;\r\n                                BrowsePresenter.instance(getContext()).syncItem(video);\r\n                            }\r\n                    );\r\n                    return true;\r\n                });\r\n    }\r\n\r\n    protected void appendToggleHistoryButton() {\r\n        if (!mIsToggleHistoryEnabled) {\r\n            return;\r\n        }\r\n\r\n        Video original = getVideo();\r\n\r\n        if (original != null && original.isChapter) {\r\n            return;\r\n        }\r\n\r\n        if (getSection() != null && getSection().getId() != MediaGroup.TYPE_HISTORY) {\r\n            return;\r\n        }\r\n\r\n        GeneralData generalData = GeneralData.instance(getContext());\r\n        boolean enabled = generalData.isHistoryEnabled();\r\n\r\n        getDialogPresenter().appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(enabled ? R.string.pause_history : R.string.resume_history),\r\n                        optionItem -> {\r\n                            mServiceManager.enableHistory(!enabled);\r\n                            generalData.setHistoryEnabled(!enabled);\r\n                            getDialogPresenter().closeDialog();\r\n                        }));\r\n    }\r\n\r\n    protected void appendClearHistoryButton() {\r\n        if (!mIsClearHistoryEnabled) {\r\n            return;\r\n        }\r\n\r\n        BrowsePresenter presenter = BrowsePresenter.instance(getContext());\r\n\r\n        if (!(presenter.isHistorySection() && presenter.inForeground())) {\r\n            return;\r\n        }\r\n\r\n        getDialogPresenter().appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.clear_history),\r\n                        optionItem -> AppDialogUtil.showConfirmationDialog(getContext(),\r\n                                getContext().getString(R.string.clear_history), () -> {\r\n                                    mServiceManager.clearHistory(getContext(), () -> {\r\n                                        getDialogPresenter().closeDialog();\r\n                                        presenter.refresh();\r\n                                    });\r\n                        })));\r\n    }\r\n\r\n    protected void appendUpdateCheckButton() {\r\n        if (!mIsUpdateCheckEnabled) {\r\n            return;\r\n        }\r\n\r\n        getDialogPresenter().appendSingleButton(UiOptionItem.from(\r\n                getContext().getString(R.string.check_for_updates),\r\n                option -> AppUpdatePresenter.instance(getContext()).start(true)));\r\n    }\r\n\r\n    protected void appendToggleExcludeFromContentBlockButton() {\r\n        if (!mIsExcludeFromContentBlockEnabled) {\r\n            return;\r\n        }\r\n\r\n        Video original = getVideo();\r\n\r\n        if (original == null || !(original.hasChannel() || original.hasVideo())) {\r\n            return;\r\n        }\r\n\r\n        getDialogPresenter().appendSingleButton(AppDialogUtil.createExcludeFromContentBlockButton(getContext(), original, mServiceManager, this::closeDialog));\r\n    }\r\n\r\n    protected void updateEnabledMenuItems() {\r\n        MainUIData mainUIData = MainUIData.instance(getContext());\r\n        \r\n        mIsPinToSidebarEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_PIN_TO_SIDEBAR);\r\n        mIsSavePlaylistEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_SAVE_REMOVE_PLAYLIST);\r\n        mIsCreatePlaylistEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_CREATE_PLAYLIST);\r\n        mIsRenamePlaylistEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_RENAME_PLAYLIST);\r\n        mIsAccountSelectionEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_SELECT_ACCOUNT);\r\n        mIsAddToNewPlaylistEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_ADD_TO_NEW_PLAYLIST);\r\n        mIsToggleHistoryEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_TOGGLE_HISTORY);\r\n        mIsClearHistoryEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_CLEAR_HISTORY);\r\n        mIsUpdateCheckEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_UPDATE_CHECK);\r\n        mIsExcludeFromContentBlockEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_EXCLUDE_FROM_CONTENT_BLOCK);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/menu/ChannelUploadsMenuPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItem;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.ChannelPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.ChannelUploadsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter.VideoMenuCallback;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\nimport java.util.List;\r\n\r\npublic class ChannelUploadsMenuPresenter extends BaseMenuPresenter {\r\n    private final MediaItemService mItemManager;\r\n    private final AppDialogPresenter mDialogPresenter;\r\n    private final MediaServiceManager mServiceManager;\r\n    private Disposable mUnsubscribeAction;\r\n    private Video mVideo;\r\n    private VideoMenuCallback mCallback;\r\n\r\n    private ChannelUploadsMenuPresenter(Context context) {\r\n        super(context);\r\n        ServiceManager service = YouTubeServiceManager.instance();\r\n        mItemManager = service.getMediaItemService();\r\n        mDialogPresenter = AppDialogPresenter.instance(context);\r\n        mServiceManager = MediaServiceManager.instance();\r\n    }\r\n\r\n    public static ChannelUploadsMenuPresenter instance(Context context) {\r\n        return new ChannelUploadsMenuPresenter(context);\r\n    }\r\n\r\n    @Override\r\n    protected Video getVideo() {\r\n        return mVideo;\r\n    }\r\n\r\n    @Override\r\n    protected AppDialogPresenter getDialogPresenter() {\r\n        return mDialogPresenter;\r\n    }\r\n\r\n    @Override\r\n    protected VideoMenuCallback getCallback() {\r\n        return mCallback;\r\n    }\r\n\r\n    public void showMenu(Video video, VideoMenuCallback callback) {\r\n        mCallback = callback;\r\n        showMenu(video);\r\n    }\r\n\r\n    public void showMenu(Video video) {\r\n        if (video == null || !video.belongsToChannelUploads()) {\r\n            return;\r\n        }\r\n\r\n        mVideo = video;\r\n\r\n        RxHelper.disposeActions(mUnsubscribeAction);\r\n\r\n        prepareAndShowDialog();\r\n    }\r\n\r\n    private void prepareAndShowDialog() {\r\n        // Doesn't need this since this is the main action.\r\n        //appendOpenChannelUploadsButton();\r\n        appendOpenChannelButton();\r\n        appendUnsubscribeButton();\r\n        appendMarkAsWatched();\r\n        appendTogglePinVideoToSidebarButton();\r\n\r\n        mDialogPresenter.showDialog(mVideo.getTitle());\r\n    }\r\n\r\n    private void appendOpenChannelUploadsButton() {\r\n        if (mVideo == null) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.open_channel_uploads), optionItem -> ChannelUploadsPresenter.instance(getContext()).openChannel(mVideo)));\r\n    }\r\n\r\n    private void appendOpenChannelButton() {\r\n        if (!ChannelPresenter.canOpenChannel(mVideo)) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.open_channel), optionItem -> ChannelPresenter.instance(getContext()).openChannel(mVideo)));\r\n    }\r\n\r\n    private void appendUnsubscribeButton() {\r\n        if (mVideo == null) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.unsubscribe_from_channel), optionItem -> {\r\n                    // Maybe this is subscribed items view\r\n                    ChannelUploadsPresenter.instance(getContext())\r\n                            .obtainGroup(mVideo, group -> {\r\n                                // Some uploads groups doesn't contain channel button.\r\n                                // Use data from first item instead.\r\n                                if (group.getChannelId() == null) {\r\n                                    List<MediaItem> mediaItems = group.getMediaItems();\r\n\r\n                                    // Filter collaborative content\r\n                                    MediaItem first = Helpers.findFirst(mediaItems, mediaItem -> Helpers.startsWith(mediaItem.getAuthor(), mVideo.getAuthor()));\r\n\r\n                                    if (first == null && mediaItems != null && !mediaItems.isEmpty()) {\r\n                                        first = mediaItems.get(0);\r\n                                    }\r\n\r\n                                    if (first != null) {\r\n                                        mServiceManager.loadMetadata(first, metadata -> {\r\n                                            unsubscribe(metadata.getChannelId());\r\n                                            mVideo.channelId = metadata.getChannelId();\r\n                                        });\r\n                                    }\r\n\r\n                                    return;\r\n                                }\r\n\r\n                                unsubscribe(group.getChannelId());\r\n                                mVideo.channelId = group.getChannelId();\r\n                            });\r\n                }));\r\n    }\r\n\r\n    private void appendMarkAsWatched() {\r\n        if (mVideo == null || !mVideo.hasNewContent) {\r\n            return;\r\n        }\r\n\r\n        boolean contentAlreadyLoaded = mVideo.groupPosition == 0;\r\n\r\n        if (contentAlreadyLoaded) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.mark_channel_as_watched), optionItem -> {\r\n                    mServiceManager.loadChannelUploads(mVideo, (group) -> {});\r\n                    MessageHelpers.showMessage(getContext(), R.string.channel_marked_as_watched);\r\n                }));\r\n    }\r\n\r\n    private void unsubscribe(String channelId) {\r\n        if (channelId == null) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mUnsubscribeAction);\r\n        mUnsubscribeAction = RxHelper.execute(mItemManager.unsubscribeObserve(channelId));\r\n\r\n        if (mCallback != null) {\r\n            mDialogPresenter.closeDialog();\r\n            mCallback.onItemAction(mVideo, VideoMenuCallback.ACTION_UNSUBSCRIBE);\r\n        } else {\r\n            MessageHelpers.showMessage(getContext(), R.string.unsubscribed_from_channel);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/menu/SectionMenuPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu;\r\n\r\nimport android.content.Context;\r\n\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItem;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.BrowseSection;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.BrowsePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter.MenuAction;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter.VideoMenuCallback;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.ContextMenuManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.ContextMenuProvider;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.SplashView;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.SimpleEditDialog;\r\n\r\nimport java.util.HashMap;\r\nimport java.util.Iterator;\r\nimport java.util.Map;\r\n\r\npublic class SectionMenuPresenter extends BaseMenuPresenter {\r\n    private static final String TAG = SectionMenuPresenter.class.getSimpleName();\r\n    private final AppDialogPresenter mDialogPresenter;\r\n    private Video mVideo;\r\n    private BrowseSection mSection;\r\n    private boolean mIsReturnToBackgroundVideoEnabled;\r\n    private boolean mIsMarkAllChannelsWatchedEnabled;\r\n    private boolean mIsRefreshEnabled;\r\n    private boolean mIsMoveSectionEnabled;\r\n    private boolean mIsRenameSectionEnabled;\r\n    private final Map<Long, MenuAction> mMenuMapping = new HashMap<>();\r\n\r\n    private SectionMenuPresenter(Context context) {\r\n        super(context);\r\n        mDialogPresenter = AppDialogPresenter.instance(context);\r\n\r\n        initMenuMapping();\r\n    }\r\n\r\n    public static SectionMenuPresenter instance(Context context) {\r\n        return new SectionMenuPresenter(context);\r\n    }\r\n\r\n    @Override\r\n    protected @Nullable Video getVideo() {\r\n        return mVideo;\r\n    }\r\n\r\n    @Override\r\n    protected BrowseSection getSection() {\r\n        return mSection;\r\n    }\r\n\r\n    @Override\r\n    protected AppDialogPresenter getDialogPresenter() {\r\n        return mDialogPresenter;\r\n    }\r\n\r\n    @Override\r\n    protected VideoMenuCallback getCallback() {\r\n        return null;\r\n    }\r\n\r\n    public void showMenu(BrowseSection section) {\r\n        if (section == null) {\r\n            return;\r\n        }\r\n\r\n        disposeActions();\r\n\r\n        mSection = section;\r\n        mVideo = section.getData() instanceof Video ? (Video) section.getData() : null;\r\n\r\n        MediaServiceManager.instance().authCheck(this::obtainPlaylistsAndShowDialogSigned, this::prepareAndShowDialogUnsigned);\r\n    }\r\n\r\n    private void obtainPlaylistsAndShowDialogSigned() {\r\n        prepareAndShowDialogSigned();\r\n    }\r\n\r\n    private void prepareAndShowDialogSigned() {\r\n        if (getContext() == null) {\r\n            return;\r\n        }\r\n\r\n        appendReturnToBackgroundVideoButton();\r\n        appendRefreshButton();\r\n        appendUnpinVideoFromSidebarButton();\r\n        appendUnpinSectionFromSidebarButton();\r\n        appendMarkAllChannelsWatchedButton();\r\n        appendAccountSelectionButton();\r\n        appendMoveSectionButton();\r\n        appendRenameSectionButton();\r\n        appendCreatePlaylistButton();\r\n        appendToggleHistoryButton();\r\n        appendClearHistoryButton();\r\n        appendUpdateCheckButton();\r\n\r\n        for (Long menuItem : MainUIData.instance(getContext()).getMenuItemsOrdered()) {\r\n            MenuAction menuAction = mMenuMapping.get(menuItem);\r\n            if (menuAction != null) {\r\n                menuAction.run();\r\n            }\r\n        }\r\n\r\n        if (!mDialogPresenter.isEmpty()) {\r\n            String title = mSection != null ? mSection.getTitle() : null;\r\n            mDialogPresenter.showDialog(title, this::disposeActions);\r\n        }\r\n    }\r\n\r\n    private void prepareAndShowDialogUnsigned() {\r\n        if (getContext() == null) {\r\n            return;\r\n        }\r\n\r\n        appendReturnToBackgroundVideoButton();\r\n        appendRefreshButton();\r\n        appendUnpinVideoFromSidebarButton();\r\n        appendUnpinSectionFromSidebarButton();\r\n        appendAccountSelectionButton();\r\n        appendMoveSectionButton();\r\n        appendRenameSectionButton();\r\n        appendClearHistoryButton();\r\n\r\n        for (Long menuItem : MainUIData.instance(getContext()).getMenuItemsOrdered()) {\r\n            MenuAction menuAction = mMenuMapping.get(menuItem);\r\n            if (menuAction != null && !menuAction.isAuth()) {\r\n                menuAction.run();\r\n            }\r\n        }\r\n\r\n        if (!mDialogPresenter.isEmpty()) {\r\n            String title = mSection != null ? mSection.getTitle() : null;\r\n            mDialogPresenter.showDialog(title, this::disposeActions);\r\n        }\r\n    }\r\n\r\n    private void appendRefreshButton() {\r\n        if (!mIsRefreshEnabled) {\r\n            return;\r\n        }\r\n\r\n        if (mSection == null || mSection.getId() == MediaGroup.TYPE_SETTINGS) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.refresh_section), optionItem -> {\r\n                    if (BrowsePresenter.instance(getContext()).getView() != null) {\r\n                        BrowsePresenter.instance(getContext()).getView().focusOnContent();\r\n                        BrowsePresenter.instance(getContext()).refresh();\r\n                    }\r\n                    mDialogPresenter.closeDialog();\r\n                }));\r\n    }\r\n\r\n    private void appendMoveSectionButton() {\r\n        if (!mIsMoveSectionEnabled) {\r\n            return;\r\n        }\r\n\r\n        if (mSection == null) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.move_section_up), optionItem -> {\r\n                    //mDialogPresenter.closeDialog();\r\n                    BrowsePresenter.instance(getContext()).moveSectionUp(mSection);\r\n                }));\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.move_section_down), optionItem -> {\r\n                    //mDialogPresenter.closeDialog();\r\n                    BrowsePresenter.instance(getContext()).moveSectionDown(mSection);\r\n                }));\r\n    }\r\n\r\n    private void appendRenameSectionButton() {\r\n        if (!mIsRenameSectionEnabled) {\r\n            return;\r\n        }\r\n\r\n        if (mSection == null || mSection.isDefault() || getVideo() == null ||\r\n                (!getVideo().hasPlaylist() && !getVideo().hasReloadPageKey() && !getVideo().hasChannel())) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.rename_section), optionItem -> {\r\n                    mDialogPresenter.closeDialog();\r\n                    SimpleEditDialog.show(\r\n                            getContext(),\r\n                            getContext().getString(R.string.rename_section),\r\n                            mSection.getTitle(),\r\n                            newValue -> {\r\n                                mSection.setTitle(newValue);\r\n                                BrowsePresenter.instance(getContext()).renameSection(mSection);\r\n                                return true;\r\n                            });\r\n                }));\r\n    }\r\n\r\n    private void appendReturnToBackgroundVideoButton() {\r\n        if (!mIsReturnToBackgroundVideoEnabled || !PlaybackPresenter.instance(getContext()).isRunningInBackground()) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.return_to_background_video),\r\n                        // Assume that the Playback view already blocked and remembered.\r\n                        optionItem -> getViewManager().startView(SplashView.class)\r\n                )\r\n        );\r\n    }\r\n\r\n    private void appendMarkAllChannelsWatchedButton() {\r\n        if (!mIsMarkAllChannelsWatchedEnabled) {\r\n            return;\r\n        }\r\n\r\n        if (mSection == null || mSection.getId() != MediaGroup.TYPE_CHANNEL_UPLOADS) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.mark_all_channels_watched), optionItem -> {\r\n                    mDialogPresenter.closeDialog();\r\n\r\n                    MediaServiceManager serviceManager = MediaServiceManager.instance();\r\n\r\n                    MessageHelpers.showMessage(getContext(), R.string.wait_data_loading);\r\n\r\n                    serviceManager.loadSubscribedChannels(group -> {\r\n                        Iterator<MediaItem> iterator = group.getMediaItems().iterator();\r\n\r\n                        processNextChannel(serviceManager, iterator);\r\n                    });\r\n                }));\r\n    }\r\n\r\n    private void processNextChannel(MediaServiceManager serviceManager, Iterator<MediaItem> iterator) {\r\n        if (iterator.hasNext()) {\r\n            MediaItem next = iterator.next();\r\n\r\n            if (!next.hasNewContent()) {\r\n                processNextChannel(serviceManager, iterator);\r\n                return;\r\n            }\r\n\r\n            MessageHelpers.showMessage(getContext(), next.getTitle());\r\n            serviceManager.loadChannelUploads(next, (groupTmp) -> processNextChannel(serviceManager, iterator));\r\n        } else {\r\n            MessageHelpers.showMessage(getContext(), R.string.msg_done);\r\n        }\r\n    }\r\n\r\n    private void disposeActions() {\r\n        //RxUtils.disposeActions(mPlaylistAction);\r\n    }\r\n\r\n    @Override\r\n    protected void updateEnabledMenuItems() {\r\n        super.updateEnabledMenuItems();\r\n\r\n        mIsReturnToBackgroundVideoEnabled = true;\r\n        mIsRefreshEnabled = true;\r\n        mIsMarkAllChannelsWatchedEnabled = true;\r\n        mIsMoveSectionEnabled = true;\r\n        mIsRenameSectionEnabled = true;\r\n\r\n        MainUIData mainUIData = MainUIData.instance(getContext());\r\n\r\n        mIsMoveSectionEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_MOVE_SECTION_UP);\r\n        mIsMoveSectionEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_MOVE_SECTION_DOWN);\r\n        mIsRenameSectionEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_RENAME_SECTION);\r\n    }\r\n\r\n    private void initMenuMapping() {\r\n        mMenuMapping.clear();\r\n\r\n        for (ContextMenuProvider provider : new ContextMenuManager(getContext()).getProviders()) {\r\n            if (provider.getMenuType() != ContextMenuProvider.MENU_TYPE_SECTION) {\r\n                continue;\r\n            }\r\n            mMenuMapping.put(provider.getId(), new MenuAction(() -> appendContextMenuItem(provider), false));\r\n        }\r\n    }\r\n\r\n    private void appendContextMenuItem(ContextMenuProvider provider) {\r\n        MainUIData mainUIData = MainUIData.instance(getContext());\r\n        if (mainUIData.isMenuItemEnabled(provider.getId()) && provider.isEnabled(getVideo())) {\r\n            mDialogPresenter.appendSingleButton(\r\n                    UiOptionItem.from(getContext().getString(provider.getTitleResId()), optionItem -> provider.onClicked(getVideo(), getCallback()))\r\n            );\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/menu/VideoMenuPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.PlaylistInfo;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Playlist;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.CommentsController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerUI;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService.State;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.BrowsePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.ChannelPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.ChannelUploadsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.ContextMenuManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.ContextMenuProvider;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ChannelUploadsView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.PlaybackView;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.StreamReminderService;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.BlockedChannelData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\nimport io.reactivex.Observable;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\nimport java.lang.ref.WeakReference;\r\nimport java.util.HashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\n\r\npublic class VideoMenuPresenter extends BaseMenuPresenter {\r\n    private static final String TAG = VideoMenuPresenter.class.getSimpleName();\r\n    private final MediaItemService mMediaItemService;\r\n    private final AppDialogPresenter mDialogPresenter;\r\n    private final MediaServiceManager mServiceManager;\r\n    private Disposable mAddToPlaylistAction;\r\n    private Disposable mNotInterestedAction;\r\n    private Disposable mSubscribeAction;\r\n    private Disposable mPlaylistsInfoAction;\r\n    private Video mVideo;\r\n    public static WeakReference<Video> sVideoHolder = new WeakReference<>(null);\r\n    private boolean mIsNotInterestedButtonEnabled;\r\n    private boolean mIsNotRecommendChannelEnabled;\r\n    private boolean mIsBlockChannelEnabled;\r\n    private boolean mIsRemoveFromHistoryButtonEnabled;\r\n    private boolean mIsRemoveFromSubscriptionsButtonEnabled;\r\n    private boolean mIsOpenChannelButtonEnabled;\r\n    private boolean mIsOpenChannelUploadsButtonEnabled;\r\n    private boolean mIsSubscribeButtonEnabled;\r\n    private boolean mIsShareLinkButtonEnabled;\r\n    private boolean mIsShareQRLinkButtonEnabled;\r\n    private boolean mIsShareEmbedLinkButtonEnabled;\r\n    private boolean mIsAddToPlaylistButtonEnabled;\r\n    private boolean mIsAddToRecentPlaylistButtonEnabled;\r\n    private boolean mIsReturnToBackgroundVideoEnabled;\r\n    private boolean mIsOpenPlaylistButtonEnabled;\r\n    private boolean mIsAddToPlaybackQueueButtonEnabled;\r\n    private boolean mIsPlayNextButtonEnabled;\r\n    private boolean mIsShowPlaybackQueueButtonEnabled;\r\n    private boolean mIsOpenDescriptionButtonEnabled;\r\n    private boolean mIsOpenCommentsButtonEnabled;\r\n    private boolean mIsPlayVideoButtonEnabled;\r\n    private boolean mIsPlayVideoIncognitoButtonEnabled;\r\n    private boolean mIsPlayFromStartButtonEnabled;\r\n    private boolean mIsPlaylistOrderButtonEnabled;\r\n    private boolean mIsStreamReminderButtonEnabled;\r\n    private boolean mIsMarkAsWatchedButtonEnabled;\r\n    private VideoMenuCallback mCallback;\r\n    private List<PlaylistInfo> mPlaylistInfos;\r\n    private final Map<Long, MenuAction> mMenuMapping = new HashMap<>();\r\n\r\n    public interface VideoMenuCallback {\r\n        int ACTION_UNDEFINED = 0;\r\n        int ACTION_UNSUBSCRIBE = 1;\r\n        int ACTION_REMOVE = 2;\r\n        int ACTION_REMOVE_FROM_PLAYLIST = 3;\r\n        int ACTION_REMOVE_FROM_QUEUE = 4;\r\n        int ACTION_ADD_TO_QUEUE = 5;\r\n        int ACTION_PLAY_NEXT = 6;\r\n        int ACTION_REMOVE_AUTHOR = 7;\r\n        void onItemAction(Video videoItem, int action);\r\n    }\r\n\r\n    public static class MenuAction {\r\n        private final Runnable mAction;\r\n        private final boolean mIsAuth;\r\n\r\n        public MenuAction(Runnable action, boolean isAuth) {\r\n            this.mAction = action;\r\n            this.mIsAuth = isAuth;\r\n        }\r\n\r\n        public void run() {\r\n            mAction.run();\r\n        }\r\n\r\n        public boolean isAuth() {\r\n            return mIsAuth;\r\n        }\r\n    }\r\n\r\n    private VideoMenuPresenter(Context context) {\r\n        super(context);\r\n        ServiceManager service = YouTubeServiceManager.instance();\r\n        mMediaItemService = service.getMediaItemService();\r\n        mServiceManager = MediaServiceManager.instance();\r\n        mDialogPresenter = AppDialogPresenter.instance(context);\r\n\r\n        initMenuMapping();\r\n    }\r\n\r\n    public static VideoMenuPresenter instance(Context context) {\r\n        return new VideoMenuPresenter(context);\r\n    }\r\n\r\n    @Override\r\n    protected Video getVideo() {\r\n        return mVideo;\r\n    }\r\n\r\n    @Override\r\n    protected AppDialogPresenter getDialogPresenter() {\r\n        return mDialogPresenter;\r\n    }\r\n\r\n    @Override\r\n    protected VideoMenuCallback getCallback() {\r\n        return mCallback;\r\n    }\r\n\r\n    public void showMenu(Video video, VideoMenuCallback callback) {\r\n        mCallback = callback;\r\n        showMenu(video);\r\n    }\r\n\r\n    public void showMenu(Video video) {\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        mVideo = video;\r\n        sVideoHolder = new WeakReference<>(video);\r\n\r\n        MediaServiceManager.instance().authCheck(this::bootstrapPrepareAndShowDialogSigned, this::prepareAndShowDialogUnsigned);\r\n    }\r\n\r\n    private void bootstrapPrepareAndShowDialogSigned() {\r\n        mPlaylistInfos = null;\r\n        RxHelper.disposeActions(mPlaylistsInfoAction);\r\n        if (isAddToRecentPlaylistButtonEnabled()) {\r\n            mPlaylistsInfoAction = mMediaItemService.getPlaylistsInfoObserve(mVideo.videoId)\r\n                    .subscribe(\r\n                            videoPlaylistInfos -> {\r\n                                mPlaylistInfos = videoPlaylistInfos;\r\n                                prepareAndShowDialogSigned();\r\n                            },\r\n                            error -> Log.e(TAG, \"Add to recent playlist error: %s\", error.getMessage())\r\n                    );\r\n        } else {\r\n            prepareAndShowDialogSigned();\r\n        }\r\n    }\r\n\r\n    private void prepareAndShowDialogSigned() {\r\n        if (getContext() == null) {\r\n            return;\r\n        }\r\n\r\n        appendReturnToBackgroundVideoButton();\r\n\r\n        for (Long menuItem : MainUIData.instance(getContext()).getMenuItemsOrdered()) {\r\n            MenuAction menuAction = mMenuMapping.get(menuItem);\r\n            if (menuAction != null) {\r\n                menuAction.run();\r\n            }\r\n        }\r\n\r\n        if (!mDialogPresenter.isEmpty()) {\r\n            String title = mVideo != null ? mVideo.getTitle() : null;\r\n            // No need to add author because: 1) This could be a channel card. 2) This info isn't so important.\r\n            mDialogPresenter.showDialog(title);\r\n        }\r\n    }\r\n\r\n    private void prepareAndShowDialogUnsigned() {\r\n        if (getContext() == null) {\r\n            return;\r\n        }\r\n\r\n        appendReturnToBackgroundVideoButton();\r\n\r\n        for (Long menuItem : MainUIData.instance(getContext()).getMenuItemsOrdered()) {\r\n            MenuAction menuAction = mMenuMapping.get(menuItem);\r\n            if (menuAction != null && !menuAction.isAuth()) {\r\n                menuAction.run();\r\n            }\r\n        }\r\n\r\n        if (!mDialogPresenter.isEmpty()) {\r\n            String title = mVideo != null ? mVideo.getTitle() : null;\r\n            mDialogPresenter.showDialog(title);\r\n        }\r\n    }\r\n\r\n    private void appendAddToPlaylistButton() {\r\n        if (!mIsAddToPlaylistButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        if (mVideo == null || !mVideo.hasVideo() || mVideo.isPlaylistAsChannel()) {\r\n            return;\r\n        }\r\n\r\n        getDialogPresenter().appendSingleButton(\r\n                UiOptionItem.from(\r\n                        getContext().getString(R.string.dialog_add_to_playlist),\r\n                        optionItem -> AppDialogUtil.showAddToPlaylistDialog(getContext(), mVideo, mCallback)\r\n                ));\r\n    }\r\n\r\n    private void appendAddToRecentPlaylistButton() {\r\n        if (!isAddToRecentPlaylistButtonEnabled()) {\r\n            return;\r\n        }\r\n\r\n        String playlistId = GeneralData.instance(getContext()).getLastPlaylistId();\r\n        String playlistTitle = GeneralData.instance(getContext()).getLastPlaylistTitle();\r\n\r\n        if (playlistId == null || playlistTitle == null) {\r\n            return;\r\n        }\r\n\r\n        appendSimpleAddToRecentPlaylistButton(playlistId, playlistTitle);\r\n    }\r\n\r\n    private boolean isAddToRecentPlaylistButtonEnabled() {\r\n        return mIsAddToPlaylistButtonEnabled && mIsAddToRecentPlaylistButtonEnabled && mVideo != null && mVideo.hasVideo();\r\n    }\r\n\r\n    private void appendSimpleAddToRecentPlaylistButton(String playlistId, String playlistTitle) {\r\n        if (mPlaylistInfos == null) {\r\n            return;\r\n        }\r\n\r\n        boolean isSelected = false;\r\n        for (PlaylistInfo playlistInfo : mPlaylistInfos) {\r\n            if (playlistInfo.getPlaylistId().equals(playlistId)) {\r\n                isSelected = playlistInfo.isSelected();\r\n                break;\r\n            }\r\n        }\r\n        boolean add = !isSelected;\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(\r\n                        add ? R.string.dialog_add_to : R.string.dialog_remove_from, playlistTitle),\r\n                        optionItem -> addRemoveFromPlaylist(playlistId, playlistTitle, add)\r\n                )\r\n        );\r\n    }\r\n\r\n    //private void appendReactiveAddToRecentPlaylistButton(String playlistId, String playlistTitle) {\r\n    //    mDialogPresenter.appendSingleButton(\r\n    //            UiOptionItem.from(getContext().getString(\r\n    //                    R.string.dialog_add_remove_from, playlistTitle),\r\n    //                    optionItem -> {\r\n    //                        mPlaylistsInfoAction = mItemManager.getVideoPlaylistsInfoObserve(mVideo.videoId)\r\n    //                                .subscribe(\r\n    //                                        videoPlaylistInfos -> {\r\n    //                                            for (VideoPlaylistInfo playlistInfo : videoPlaylistInfos) {\r\n    //                                                if (playlistInfo.getPlaylistId().equals(playlistId)) {\r\n    //                                                    addRemoveFromPlaylist(playlistInfo.getPlaylistId(), playlistInfo.getTitle(), !playlistInfo.isSelected());\r\n    //                                                    break;\r\n    //                                                }\r\n    //                                            }\r\n    //                                        },\r\n    //                                        error -> {\r\n    //                                            // Fallback to something on error\r\n    //                                            Log.e(TAG, \"Add to recent playlist error: %s\", error.getMessage());\r\n    //                                        }\r\n    //                                );\r\n    //                    }\r\n    //            )\r\n    //    );\r\n    //}\r\n\r\n    private void appendOpenChannelButton() {\r\n        if (!mIsOpenChannelButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        if (!ChannelPresenter.canOpenChannel(mVideo)) {\r\n            return;\r\n        }\r\n\r\n        // Prepare to special type of channels that work as playlist\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(\r\n                        mVideo.isPlaylistAsChannel() ? R.string.open_playlist : R.string.open_channel), optionItem -> {\r\n                    MediaServiceManager.chooseChannelPresenter(getContext(), mVideo);\r\n                    mDialogPresenter.closeDialog();\r\n                }));\r\n    }\r\n\r\n    private void appendOpenPlaylistButton() {\r\n        if (!mIsOpenPlaylistButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        // Check view to allow open playlist in grid\r\n        if (mVideo == null || !mVideo.hasPlaylist() || (getViewManager().getTopView() == ChannelUploadsView.class && mVideo.belongsToSamePlaylistGroup())) {\r\n            return;\r\n        }\r\n\r\n        // Prepare to special type of channels that work as playlist\r\n        if (mVideo.isPlaylistAsChannel() && ChannelPresenter.canOpenChannel(mVideo)) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.open_playlist), optionItem -> ChannelUploadsPresenter.instance(getContext()).openChannel(mVideo)));\r\n    }\r\n\r\n    private void appendOpenChannelUploadsButton() {\r\n        if (!mIsOpenChannelUploadsButtonEnabled || mVideo == null) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.open_channel_uploads), optionItem -> ChannelUploadsPresenter.instance(getContext()).openChannel(mVideo)));\r\n    }\r\n\r\n    private void appendNotInterestedButton() {\r\n        if (mVideo == null || mVideo.mediaItem == null || mVideo.mediaItem.getFeedbackToken() == null) {\r\n            return;\r\n        }\r\n\r\n        if ((!mVideo.belongsToHome() && !mVideo.belongsToShorts()) || !mIsNotInterestedButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mNotInterestedAction);\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.not_interested), optionItem -> {\r\n                    mNotInterestedAction = mMediaItemService.markAsNotInterestedObserve(mVideo.mediaItem.getFeedbackToken())\r\n                            .subscribe(\r\n                                    var -> {},\r\n                                    error -> Log.e(TAG, \"Mark as 'not interested' error: %s\", error.getMessage()),\r\n                                    () -> {\r\n                                        if (mCallback != null) {\r\n                                            mCallback.onItemAction(mVideo, VideoMenuCallback.ACTION_REMOVE);\r\n                                        } else {\r\n                                            MessageHelpers.showMessage(getContext(), R.string.you_wont_see_this_video);\r\n                                        }\r\n                                    }\r\n                            );\r\n                    mDialogPresenter.closeDialog();\r\n                }));\r\n    }\r\n\r\n    private void appendNotRecommendChannelButton() {\r\n        if (mVideo == null || mVideo.mediaItem == null || mVideo.mediaItem.getFeedbackToken2() == null) {\r\n            return;\r\n        }\r\n\r\n        if ((!mVideo.belongsToHome() && !mVideo.belongsToShorts()) || !mIsNotRecommendChannelEnabled) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mNotInterestedAction);\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.not_recommend_channel), optionItem -> {\r\n                    mNotInterestedAction = mMediaItemService.markAsNotInterestedObserve(mVideo.mediaItem.getFeedbackToken2())\r\n                            .subscribe(\r\n                                    var -> {},\r\n                                    error -> Log.e(TAG, \"Mark as 'not interested' error: %s\", error.getMessage()),\r\n                                    () -> {\r\n                                        if (mCallback != null) {\r\n                                            mCallback.onItemAction(mVideo, VideoMenuCallback.ACTION_REMOVE);\r\n                                        } else {\r\n                                            MessageHelpers.showMessage(getContext(), R.string.you_wont_see_this_video);\r\n                                        }\r\n                                    }\r\n                            );\r\n                    mDialogPresenter.closeDialog();\r\n                }));\r\n    }\r\n\r\n    private void appendBlockChannelButton() {\r\n        if (mVideo == null || !mIsBlockChannelEnabled) {\r\n            return;\r\n        }\r\n\r\n        String channelId = mVideo.getChannelIdOrName();\r\n        if (channelId == null || channelId.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        BlockedChannelData blockedChannelData = BlockedChannelData.instance(getContext());\r\n        boolean isBlacklisted = blockedChannelData.containsChannel(channelId);\r\n\r\n        String buttonText = isBlacklisted\r\n                ? getContext().getString(R.string.dialog_unblock_channel)\r\n                : getContext().getString(R.string.dialog_block_channel);\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(buttonText, optionItem -> {\r\n                    if (isBlacklisted) {\r\n                        // Remove from blacklist\r\n                        blockedChannelData.removeChannel(channelId);\r\n                        MessageHelpers.showMessage(getContext(), R.string.channel_unblocked);\r\n                        if (mCallback != null) {\r\n                            mCallback.onItemAction(mVideo, VideoMenuCallback.ACTION_REMOVE);\r\n                        }\r\n                        mDialogPresenter.closeDialog();\r\n                    } else {\r\n                        // Show confirmation dialog before blocking\r\n                        String channelName = mVideo.getAuthor();\r\n                        String confirmMessage = getContext().getString(R.string.confirm_block_channel, channelName);\r\n\r\n                        AppDialogUtil.showConfirmationDialog(\r\n                                getContext(),\r\n                                confirmMessage,\r\n                                () -> {\r\n                                    blockedChannelData.addChannel(channelId, channelName);\r\n                                    MessageHelpers.showMessage(getContext(), R.string.channel_blocked);\r\n\r\n                                    if (mCallback != null) {\r\n                                        mCallback.onItemAction(mVideo, VideoMenuCallback.ACTION_REMOVE);\r\n                                    }\r\n                                    mDialogPresenter.closeDialog();\r\n                                });\r\n                    }\r\n                }));\r\n    }\r\n\r\n    private void appendRemoveFromHistoryButton() {\r\n        if (mVideo == null || !mVideo.belongsToHistory() || !mIsRemoveFromHistoryButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mNotInterestedAction);\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.remove_from_history), optionItem -> {\r\n                    if (mVideo.mediaItem == null || mVideo.mediaItem.getFeedbackToken() == null) {\r\n                        onRemoveFromHistoryDone();\r\n                    } else {\r\n                        mNotInterestedAction = mMediaItemService.markAsNotInterestedObserve(mVideo.mediaItem.getFeedbackToken())\r\n                                .subscribe(\r\n                                        var -> {},\r\n                                        error -> Log.e(TAG, \"Remove from history error: %s\", error.getMessage()),\r\n                                        this::onRemoveFromHistoryDone\r\n                                );\r\n                    }\r\n                    mDialogPresenter.closeDialog();\r\n                }));\r\n    }\r\n\r\n    private void onRemoveFromHistoryDone() {\r\n        if (mCallback != null) {\r\n            mCallback.onItemAction(mVideo, VideoMenuCallback.ACTION_REMOVE);\r\n        } else {\r\n            MessageHelpers.showMessage(getContext(), R.string.removed_from_history);\r\n        }\r\n        VideoStateService stateService = VideoStateService.instance(getContext());\r\n        stateService.removeByVideoId(mVideo.videoId);\r\n    }\r\n\r\n    private void appendRemoveFromSubscriptionsButton() {\r\n        if (mVideo == null || mVideo.mediaItem == null || mVideo.mediaItem.getFeedbackToken() == null) {\r\n            return;\r\n        }\r\n\r\n        if (!mVideo.belongsToSubscriptions() || !mIsRemoveFromSubscriptionsButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mNotInterestedAction);\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.remove_from_subscriptions), optionItem -> {\r\n                    mNotInterestedAction = mMediaItemService.markAsNotInterestedObserve(mVideo.mediaItem.getFeedbackToken())\r\n                            .subscribe(\r\n                                    var -> {},\r\n                                    error -> Log.e(TAG, \"Remove from subscriptions error: %s\", error.getMessage()),\r\n                                    () -> {\r\n                                        if (mCallback != null) {\r\n                                            mCallback.onItemAction(mVideo, VideoMenuCallback.ACTION_REMOVE);\r\n                                        }\r\n                                    }\r\n                            );\r\n                    mDialogPresenter.closeDialog();\r\n                }));\r\n    }\r\n\r\n    private void appendRemoveFromNotificationsButton() {\r\n        if (mVideo == null || mVideo.mediaItem == null) {\r\n            return;\r\n        }\r\n\r\n        if (!mVideo.belongsToNotifications() || !mIsRemoveFromSubscriptionsButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.remove_from_subscriptions), optionItem -> {\r\n                    MediaServiceManager.instance().hideNotification(mVideo);\r\n                    if (mCallback != null) {\r\n                        mCallback.onItemAction(mVideo, VideoMenuCallback.ACTION_REMOVE);\r\n                    }\r\n                    mDialogPresenter.closeDialog();\r\n                }));\r\n    }\r\n\r\n    private void appendMarkAsWatchedButton() {\r\n        if (mVideo == null || !mVideo.hasVideo() || !mIsMarkAsWatchedButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.mark_as_watched), optionItem -> {\r\n                    MediaServiceManager.instance().updateHistory(mVideo, mVideo.getDurationMs());\r\n                    mVideo.markFullyViewed();\r\n                    VideoStateService.instance(getContext()).save(new State(mVideo, mVideo.getDurationMs()));\r\n                    Playlist.instance().sync(mVideo);\r\n                    mDialogPresenter.closeDialog();\r\n                }));\r\n    }\r\n\r\n    private void appendShareLinkButton() {\r\n        if (!mIsShareLinkButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        AppDialogUtil.appendShareLinkDialogItem(getContext(), mDialogPresenter, mVideo);\r\n    }\r\n\r\n    private void appendShareQRLinkButton() {\r\n        if (!mIsShareQRLinkButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        AppDialogUtil.appendShareQRLinkDialogItem(getContext(), mDialogPresenter, mVideo);\r\n    }\r\n\r\n    private void appendShareEmbedLinkButton() {\r\n        if (!mIsShareEmbedLinkButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        AppDialogUtil.appendShareEmbedLinkDialogItem(getContext(), mDialogPresenter, mVideo);\r\n    }\r\n\r\n    private void appendOpenDescriptionButton() {\r\n        if (!mIsOpenDescriptionButtonEnabled || mVideo == null) {\r\n            return;\r\n        }\r\n\r\n        if (mVideo.videoId == null) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.action_video_info),\r\n                        optionItem -> {\r\n                            MessageHelpers.showMessage(getContext(), R.string.wait_data_loading);\r\n                            mServiceManager.loadMetadata(mVideo, metadata -> {\r\n                                String description = metadata.getDescription();\r\n                                if (description != null) {\r\n                                    showLongTextDialog(description);\r\n                                } else {\r\n                                    mServiceManager.loadFormatInfo(mVideo, formatInfo -> {\r\n                                        String newDescription = formatInfo.getDescription();\r\n                                        if (newDescription != null) {\r\n                                            showLongTextDialog(newDescription);\r\n                                        } else {\r\n                                            MessageHelpers.showMessage(getContext(), R.string.description_not_found);\r\n                                        }\r\n                                    });\r\n                                }\r\n                            });\r\n                        }\r\n                ));\r\n    }\r\n\r\n    private void appendOpenCommentsButton() {\r\n        if (!mIsOpenCommentsButtonEnabled || mVideo == null) {\r\n            return;\r\n        }\r\n\r\n        if (mVideo.videoId == null || mVideo.isLive || mVideo.isUpcoming) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.open_comments),\r\n                        optionItem -> {\r\n                            MessageHelpers.showMessage(getContext(), R.string.wait_data_loading);\r\n                            mServiceManager.loadMetadata(mVideo, metadata -> {\r\n                                CommentsController controller = new CommentsController(getContext(), metadata);\r\n                                controller.onButtonClicked(R.id.action_chat, PlayerUI.BUTTON_ON);\r\n                            });\r\n                        }\r\n                ));\r\n    }\r\n\r\n    private void appendPlayVideoButton() {\r\n        if (!mIsPlayVideoButtonEnabled || mVideo == null || mVideo.videoId == null) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.play_video),\r\n                        optionItem -> {\r\n                            PlaybackPresenter.instance(getContext()).openVideo(mVideo);\r\n                            mDialogPresenter.closeDialog();\r\n                        }\r\n                ));\r\n    }\r\n\r\n    private void appendPlayVideoIncognitoButton() {\r\n        if (!mIsPlayVideoIncognitoButtonEnabled || mVideo == null || mVideo.videoId == null) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.play_video_incognito),\r\n                        optionItem -> {\r\n                            mVideo.incognito = true;\r\n                            PlaybackPresenter.instance(getContext()).openVideo(mVideo);\r\n                            mDialogPresenter.closeDialog();\r\n                        }\r\n                ));\r\n    }\r\n\r\n    private void appendPlayFromStartButton() {\r\n        if (!mIsPlayFromStartButtonEnabled || mVideo == null || mVideo.videoId == null) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.play_from_start),\r\n                        optionItem -> {\r\n                            VideoStateService stateService = VideoStateService.instance(getContext());\r\n                            stateService.save(new State(mVideo, 0));\r\n                            PlaybackPresenter.instance(getContext()).openVideo(mVideo);\r\n                            mDialogPresenter.closeDialog();\r\n                        }\r\n                ));\r\n    }\r\n\r\n    private void showLongTextDialog(String description) {\r\n        mDialogPresenter.appendLongTextCategory(mVideo.getTitle(), UiOptionItem.from(description));\r\n        mDialogPresenter.showDialog(mVideo.getTitle());\r\n    }\r\n\r\n    private void appendSubscribeButton() {\r\n        if (!mIsSubscribeButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        if (mVideo == null || mVideo.isPlaylistAsChannel() || (!mVideo.isChannel() && !mVideo.hasVideo())) {\r\n            return;\r\n        }\r\n\r\n        mVideo.isSubscribed = mVideo.isSubscribed || mVideo.belongsToSubscriptions() || mVideo.belongsToChannelUploads();\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(\r\n                        mVideo.isSynced || mVideo.isSubscribed || (!getSignInService().isSigned() && mVideo.channelId != null) ? mVideo.isSubscribed ?\r\n                                R.string.unsubscribe_from_channel : R.string.subscribe_to_channel : R.string.subscribe_unsubscribe_from_channel),\r\n                        optionItem -> toggleSubscribe()));\r\n    }\r\n\r\n    private void appendReturnToBackgroundVideoButton() {\r\n        if (!mIsReturnToBackgroundVideoEnabled || !PlaybackPresenter.instance(getContext()).isRunningInBackground()) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(R.string.return_to_background_video),\r\n                        // Assume that the Playback view already blocked and remembered.\r\n                        optionItem -> getViewManager().startView(PlaybackView.class)\r\n                )\r\n        );\r\n    }\r\n\r\n    private void appendAddToPlaybackQueueButton() {\r\n        if (!mIsAddToPlaybackQueueButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        if (mVideo == null || !mVideo.hasVideo()) {\r\n            return;\r\n        }\r\n\r\n        Playlist playlist = Playlist.instance();\r\n\r\n        // Remain ability to reorder already added video (e.g. move to the top)\r\n        List<Video> all = playlist.getAll();\r\n        if (!all.isEmpty() && mVideo.equals(all.get(all.size() - 1))) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(\r\n                        getContext().getString(R.string.add_to_playback_queue),\r\n                        optionItem -> {\r\n                            mVideo.fromQueue = true;\r\n                            playlist.add(mVideo);\r\n                            if (mCallback != null) {\r\n                                mCallback.onItemAction(mVideo, VideoMenuCallback.ACTION_ADD_TO_QUEUE);\r\n                            }\r\n\r\n                            closeDialog();\r\n                            MessageHelpers.showMessage(getContext(), String.format(\"%s: %s\",\r\n                                    mVideo.getAuthor(),\r\n                                    getContext().getString(R.string.added_to_playback_queue))\r\n                            );\r\n                        }));\r\n    }\r\n\r\n    private void appendRemoveFromPlaybackQueueButton() {\r\n        if (!mIsAddToPlaybackQueueButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        if (mVideo == null || !mVideo.hasVideo()) {\r\n            return;\r\n        }\r\n\r\n        Playlist playlist = Playlist.instance();\r\n\r\n        if (!playlist.contains(mVideo)) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(\r\n                        getContext().getString(R.string.remove_from_playback_queue),\r\n                        optionItem -> {\r\n                            mVideo.fromQueue = false;\r\n                            playlist.remove(mVideo);\r\n                            if (mCallback != null) {\r\n                                mCallback.onItemAction(mVideo, VideoMenuCallback.ACTION_REMOVE_FROM_QUEUE);\r\n                            }\r\n\r\n                            closeDialog();\r\n                            MessageHelpers.showMessage(getContext(), String.format(\"%s: %s\",\r\n                                    mVideo.getAuthor(),\r\n                                    getContext().getString(R.string.removed_from_playback_queue))\r\n                            );\r\n                        }));\r\n    }\r\n\r\n    private void appendPlayNextButton() {\r\n        if (!mIsPlayNextButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        if (mVideo == null || !mVideo.hasVideo()) {\r\n            return;\r\n        }\r\n\r\n        Playlist playlist = Playlist.instance();\r\n\r\n        if (Helpers.equals(playlist.getNext(), mVideo)) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(\r\n                        getContext().getString(R.string.play_next),\r\n                        optionItem -> {\r\n                            mVideo.fromQueue = true;\r\n                            playlist.next(mVideo);\r\n                            if (mCallback != null) {\r\n                                mCallback.onItemAction(mVideo, VideoMenuCallback.ACTION_PLAY_NEXT);\r\n                            }\r\n\r\n                            closeDialog();\r\n                            MessageHelpers.showMessage(getContext(), String.format(\"%s: %s\",\r\n                                    mVideo.getAuthor(),\r\n                                    getContext().getString(R.string.play_next))\r\n                            );\r\n                        }));\r\n    }\r\n\r\n    private void appendShowPlaybackQueueButton() {\r\n        if (!mIsShowPlaybackQueueButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        if (mVideo == null || !mVideo.hasVideo()) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(\r\n                        getContext().getString(R.string.action_playback_queue),\r\n                        optionItem -> AppDialogUtil.showPlaybackQueueDialog(getContext(), video -> PlaybackPresenter.instance(getContext()).openVideo(video))\r\n                )\r\n        );\r\n    }\r\n\r\n    private void appendPlaylistOrderButton() {\r\n        if (!mIsPlaylistOrderButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        BrowsePresenter presenter = BrowsePresenter.instance(getContext());\r\n\r\n        if (mVideo == null || !(presenter.isPlaylistsSection() && presenter.inForeground())) {\r\n            return;\r\n        }\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(\r\n                        R.string.playlist_order),\r\n                        optionItem -> AppDialogUtil.showPlaylistOrderDialog(getContext(), mVideo, mDialogPresenter::closeDialog)\r\n                ));\r\n    }\r\n\r\n    private void appendStreamReminderButton() {\r\n        if (!mIsStreamReminderButtonEnabled) {\r\n            return;\r\n        }\r\n\r\n        if (mVideo == null || !mVideo.isUpcoming) {\r\n            return;\r\n        }\r\n\r\n        StreamReminderService reminderService = StreamReminderService.instance(getContext());\r\n        boolean reminderSet = reminderService.isReminderSet(mVideo);\r\n\r\n        mDialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(getContext().getString(reminderSet ? R.string.unset_stream_reminder : R.string.set_stream_reminder),\r\n                        optionItem -> {\r\n                            reminderService.toggleReminder(mVideo);\r\n                            closeDialog();\r\n                            MessageHelpers.showMessage(getContext(), reminderSet ? R.string.msg_done : R.string.playback_starts_shortly);\r\n                        }\r\n                ));\r\n    }\r\n\r\n    private void addRemoveFromPlaylist(String playlistId, String playlistTitle, boolean add) {\r\n        RxHelper.disposeActions(mAddToPlaylistAction);\r\n        if (add) {\r\n            Observable<Void> editObserve = mVideo.mediaItem != null ?\r\n                    mMediaItemService.addToPlaylistObserve(playlistId, mVideo.mediaItem) : mMediaItemService.addToPlaylistObserve(playlistId, mVideo.videoId);\r\n            // Handle error: Maximum playlist size exceeded (> 5000 items)\r\n            mAddToPlaylistAction = RxHelper.execute(editObserve, error -> MessageHelpers.showLongMessage(getContext(), error.getMessage()));\r\n            mDialogPresenter.closeDialog();\r\n            MessageHelpers.showMessage(getContext(),\r\n                    getContext().getString(R.string.added_to, playlistTitle));\r\n        } else {\r\n            // Check that the current video belongs to the right section\r\n            if (mCallback != null && Helpers.equals(mVideo.playlistId, playlistId)) {\r\n                mCallback.onItemAction(mVideo, VideoMenuCallback.ACTION_REMOVE_FROM_PLAYLIST);\r\n            }\r\n            Observable<Void> editObserve = mMediaItemService.removeFromPlaylistObserve(playlistId, mVideo.videoId);\r\n            mAddToPlaylistAction = RxHelper.execute(editObserve);\r\n            mDialogPresenter.closeDialog();\r\n            MessageHelpers.showMessage(getContext(),\r\n                    getContext().getString(R.string.removed_from, playlistTitle));\r\n        }\r\n    }\r\n\r\n    //private void addRemoveFromPlaylist(String playlistId, String playlistTitle, boolean add) {\r\n    //    if (add) {\r\n    //        Observable<Void> editObserve = mItemManager.addToPlaylistObserve(playlistId, mVideo.videoId);\r\n    //        mAddToPlaylistAction = RxUtils.execute(editObserve);\r\n    //        mDialogPresenter.closeDialog();\r\n    //        MessageHelpers.showMessage(getContext(),\r\n    //                getContext().getString(R.string.added_to, playlistTitle));\r\n    //    } else {\r\n    //        AppDialogUtil.showConfirmationDialog(getContext(), () -> {\r\n    //            // Check that the current video belongs to the right section\r\n    //            if (mCallback != null && Helpers.equals(mVideo.playlistId, playlistId)) {\r\n    //                mCallback.onItemAction(mVideo, VideoMenuCallback.ACTION_REMOVE_FROM_PLAYLIST);\r\n    //            }\r\n    //            Observable<Void> editObserve = mItemManager.removeFromPlaylistObserve(playlistId, mVideo.videoId);\r\n    //            mAddToPlaylistAction = RxUtils.execute(editObserve);\r\n    //            mDialogPresenter.closeDialog();\r\n    //            MessageHelpers.showMessage(getContext(),\r\n    //                    getContext().getString(R.string.removed_from, playlistTitle));\r\n    //        }, getContext().getString(R.string.dialog_remove_from, playlistTitle));\r\n    //    }\r\n    //}\r\n\r\n    private void toggleSubscribe() {\r\n        if (mVideo == null) {\r\n            return;\r\n        }\r\n\r\n        //mVideo.isSynced = true; // default to subscribe\r\n\r\n        // Until synced we won't really know weather we subscribed to a channel.\r\n        // Exclusion: channel item (can't be synced)\r\n        // Note, regular items (from subscribed section etc) aren't contain channel id\r\n        if (mVideo.isSynced || mVideo.isSubscribed || mVideo.isChannel() || (!getSignInService().isSigned() && mVideo.channelId != null)) {\r\n            toggleSubscribe(mVideo);\r\n        } else {\r\n            MessageHelpers.showMessage(getContext(), R.string.wait_data_loading);\r\n\r\n            mServiceManager.loadMetadata(mVideo, metadata -> {\r\n                mVideo.sync(metadata);\r\n                toggleSubscribe(mVideo);\r\n            });\r\n        }\r\n    }\r\n\r\n    private void toggleSubscribe(Video video) {\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mSubscribeAction);\r\n\r\n        Observable<Void> observable = video.isSubscribed ?\r\n                mMediaItemService.unsubscribeObserve(video.channelId) : mMediaItemService.subscribeObserve(video.channelId);\r\n\r\n        mSubscribeAction = RxHelper.execute(observable);\r\n\r\n        video.isSubscribed = !video.isSubscribed;\r\n\r\n        if (!video.isSubscribed && mCallback != null) {\r\n            mCallback.onItemAction(video, VideoMenuCallback.ACTION_UNSUBSCRIBE);\r\n        }\r\n\r\n        MessageHelpers.showMessage(getContext(), getContext().getString(!video.isSubscribed ? R.string.unsubscribed_from_channel : R.string.subscribed_to_channel));\r\n    }\r\n\r\n    @Override\r\n    protected void updateEnabledMenuItems() {\r\n        super.updateEnabledMenuItems();\r\n\r\n        MainUIData mainUIData = MainUIData.instance(getContext());\r\n\r\n        mIsOpenChannelUploadsButtonEnabled = true;\r\n        mIsOpenPlaylistButtonEnabled = true;\r\n        mIsReturnToBackgroundVideoEnabled = true;\r\n        mIsOpenChannelButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_OPEN_CHANNEL);\r\n        mIsAddToRecentPlaylistButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_RECENT_PLAYLIST);\r\n        mIsAddToPlaybackQueueButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_ADD_TO_QUEUE);\r\n        mIsPlayNextButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_PLAY_NEXT);\r\n        mIsAddToPlaylistButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_ADD_TO_PLAYLIST);\r\n        mIsShareLinkButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_SHARE_LINK);\r\n        mIsShareQRLinkButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_SHARE_QR_LINK);\r\n        mIsShareEmbedLinkButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_SHARE_EMBED_LINK);\r\n        mIsNotInterestedButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_NOT_INTERESTED);\r\n        mIsNotRecommendChannelEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_NOT_RECOMMEND_CHANNEL);\r\n        mIsBlockChannelEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_BLOCK_CHANNEL);\r\n        mIsRemoveFromHistoryButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_REMOVE_FROM_HISTORY);\r\n        mIsRemoveFromSubscriptionsButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_REMOVE_FROM_SUBSCRIPTIONS);\r\n        mIsOpenDescriptionButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_OPEN_DESCRIPTION);\r\n        mIsPlayVideoButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_PLAY_VIDEO);\r\n        mIsPlayVideoIncognitoButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_PLAY_VIDEO_INCOGNITO);\r\n        mIsPlayFromStartButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_PLAY_FROM_START);\r\n        mIsSubscribeButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_SUBSCRIBE);\r\n        mIsStreamReminderButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_STREAM_REMINDER);\r\n        mIsShowPlaybackQueueButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_SHOW_QUEUE);\r\n        mIsPlaylistOrderButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_PLAYLIST_ORDER);\r\n        mIsMarkAsWatchedButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_MARK_AS_WATCHED);\r\n        mIsOpenCommentsButtonEnabled = mainUIData.isMenuItemEnabled(MainUIData.MENU_ITEM_OPEN_COMMENTS);\r\n    }\r\n\r\n    private void initMenuMapping() {\r\n        mMenuMapping.clear();\r\n\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_PLAY_VIDEO, new MenuAction(this::appendPlayVideoButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_PLAY_VIDEO_INCOGNITO, new MenuAction(this::appendPlayVideoIncognitoButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_PLAY_FROM_START, new MenuAction(this::appendPlayFromStartButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_REMOVE_FROM_HISTORY, new MenuAction(this::appendRemoveFromHistoryButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_STREAM_REMINDER, new MenuAction(this::appendStreamReminderButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_RECENT_PLAYLIST, new MenuAction(this::appendAddToRecentPlaylistButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_ADD_TO_PLAYLIST, new MenuAction(this::appendAddToPlaylistButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_CREATE_PLAYLIST, new MenuAction(this::appendCreatePlaylistButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_RENAME_PLAYLIST, new MenuAction(this::appendRenamePlaylistButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_ADD_TO_NEW_PLAYLIST, new MenuAction(this::appendAddToNewPlaylistButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_NOT_INTERESTED, new MenuAction(this::appendNotInterestedButton, true));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_NOT_RECOMMEND_CHANNEL, new MenuAction(this::appendNotRecommendChannelButton, true));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_REMOVE_FROM_SUBSCRIPTIONS, new MenuAction(() -> { appendRemoveFromSubscriptionsButton(); appendRemoveFromNotificationsButton(); }, true));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_BLOCK_CHANNEL, new MenuAction(this::appendBlockChannelButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_MARK_AS_WATCHED, new MenuAction(this::appendMarkAsWatchedButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_PLAYLIST_ORDER, new MenuAction(this::appendPlaylistOrderButton, true));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_ADD_TO_QUEUE, new MenuAction(() -> { appendAddToPlaybackQueueButton(); appendRemoveFromPlaybackQueueButton(); }, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_PLAY_NEXT, new MenuAction(this::appendPlayNextButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_SHOW_QUEUE, new MenuAction(this::appendShowPlaybackQueueButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_OPEN_CHANNEL, new MenuAction(this::appendOpenChannelButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_OPEN_PLAYLIST, new MenuAction(this::appendOpenPlaylistButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_SUBSCRIBE, new MenuAction(this::appendSubscribeButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_EXCLUDE_FROM_CONTENT_BLOCK, new MenuAction(this::appendToggleExcludeFromContentBlockButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_PIN_TO_SIDEBAR, new MenuAction(this::appendTogglePinVideoToSidebarButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_SAVE_REMOVE_PLAYLIST, new MenuAction(this::appendSaveRemovePlaylistButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_OPEN_DESCRIPTION, new MenuAction(this::appendOpenDescriptionButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_SHARE_LINK, new MenuAction(this::appendShareLinkButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_SHARE_QR_LINK, new MenuAction(this::appendShareQRLinkButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_SHARE_EMBED_LINK, new MenuAction(this::appendShareEmbedLinkButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_SELECT_ACCOUNT, new MenuAction(this::appendAccountSelectionButton, false));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_TOGGLE_HISTORY, new MenuAction(this::appendToggleHistoryButton, true));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_CLEAR_HISTORY, new MenuAction(this::appendClearHistoryButton, true));\r\n        mMenuMapping.put(MainUIData.MENU_ITEM_OPEN_COMMENTS, new MenuAction(this::appendOpenCommentsButton, false));\r\n\r\n        for (ContextMenuProvider provider : new ContextMenuManager(getContext()).getProviders()) {\r\n            if (provider.getMenuType() != ContextMenuProvider.MENU_TYPE_VIDEO) {\r\n                continue;\r\n            }\r\n            mMenuMapping.put(provider.getId(), new MenuAction(() -> appendContextMenuItem(provider), false));\r\n        }\r\n    }\r\n\r\n    private void appendContextMenuItem(ContextMenuProvider provider) {\r\n        MainUIData mainUIData = MainUIData.instance(getContext());\r\n        if (mainUIData.isMenuItemEnabled(provider.getId()) && provider.isEnabled(getVideo())) {\r\n            mDialogPresenter.appendSingleButton(\r\n                    UiOptionItem.from(getContext().getString(provider.getTitleResId()), optionItem -> provider.onClicked(getVideo(), getCallback()))\r\n            );\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/menu/providers/ContextMenuManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers;\r\n\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.channelgroup.RemoveGroupMenuProvider;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.channelgroup.RenameGroupMenuProvider;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.channelgroup.ChannelGroupMenuProvider;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Collections;\r\nimport java.util.List;\r\n\r\npublic class ContextMenuManager {\r\n    private final ArrayList<ContextMenuProvider> mProviders;\r\n\r\n    public ContextMenuManager(Context context) {\r\n        mProviders = new ArrayList<>();\r\n        // NOTE: don't change idx after release\r\n        mProviders.add(new ChannelGroupMenuProvider(context, 0));\r\n        mProviders.add(new RemoveGroupMenuProvider(context, 1));\r\n        mProviders.add(new RenameGroupMenuProvider(context, 2));\r\n    }\r\n\r\n    public List<ContextMenuProvider> getProviders() {\r\n        return Collections.unmodifiableList(mProviders);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/menu/providers/ContextMenuProvider.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers;\r\n\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter.VideoMenuCallback;\r\n\r\nimport java.lang.ref.WeakReference;\r\n\r\npublic abstract class ContextMenuProvider {\r\n    public static int MENU_TYPE_ANY = 0;\r\n    public static int MENU_TYPE_VIDEO = 1;\r\n    public static int MENU_TYPE_SECTION = 2;\r\n    private final long mId;\r\n    private static final long START_ID = 1L << 50;\r\n    private final WeakReference<Context> mContext;\r\n\r\n    public ContextMenuProvider(Context context, int idx) {\r\n        // NOTE: We can't use the ApplicationContext because providers often use EditDialog internally\r\n        mContext = new WeakReference<>(context);\r\n        mId = START_ID << idx;\r\n    }\r\n\r\n    public abstract int getTitleResId();\r\n    public abstract void onClicked(Video item, VideoMenuCallback callback);\r\n    public abstract boolean isEnabled(Video item);\r\n    public abstract int getMenuType();\r\n\r\n    public final Context getContext() {\r\n        return mContext.get();\r\n    }\r\n\r\n    public final long getId() {\r\n        return mId;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/menu/providers/channelgroup/ChannelGroupMenuProvider.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.channelgroup;\r\n\r\nimport android.content.Context;\r\nimport android.net.Uri;\r\n\r\nimport androidx.annotation.NonNull;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.ItemGroup;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.BrowsePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter.VideoMenuCallback;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.ContextMenuProvider;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.SimpleEditDialog;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Collections;\r\nimport java.util.List;\r\n\r\npublic class ChannelGroupMenuProvider extends ContextMenuProvider {\r\n    private final ChannelGroupServiceWrapper mService;\r\n\r\n    public ChannelGroupMenuProvider(@NonNull Context context, int idx) {\r\n        super(context, idx);\r\n        mService = ChannelGroupServiceWrapper.instance(context);\r\n    }\r\n\r\n    @Override\r\n    public int getTitleResId() {\r\n        return R.string.add_to_subscriptions_group;\r\n    }\r\n\r\n    @Override\r\n    public void onClicked(Video item, VideoMenuCallback callback) {\r\n        showGroupDialogAndFetchChannel(item, callback);\r\n    }\r\n\r\n    @Override\r\n    public boolean isEnabled(Video item) {\r\n        return item != null && (item.channelId != null || item.videoId != null);\r\n    }\r\n\r\n    @Override\r\n    public int getMenuType() {\r\n        return MENU_TYPE_VIDEO;\r\n    }\r\n\r\n    private void showGroupDialogAndFetchChannel(Video item, VideoMenuCallback callback) {\r\n        if (item.hasChannel()) {\r\n            showGroupDialog(item, callback);\r\n        } else {\r\n            MessageHelpers.showMessage(getContext(), R.string.wait_data_loading);\r\n\r\n            MediaServiceManager.instance().loadMetadata(item, metadata -> {\r\n                item.sync(metadata);\r\n                showGroupDialog(item, callback);\r\n            });\r\n        }\r\n    }\r\n\r\n    private void showGroupDialog(Video item, VideoMenuCallback callback) {\r\n        if (item == null || item.channelId == null) {\r\n            return;\r\n        }\r\n\r\n        AppDialogPresenter dialogPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        List<ItemGroup> groups = mService.getChannelGroups();\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        // Create new group or enter url\r\n        String editDialogTitle = getContext().getString(R.string.new_subscriptions_group);\r\n        options.add(UiOptionItem.from(editDialogTitle, optionItem -> {\r\n            dialogPresenter.closeDialog();\r\n            SimpleEditDialog.show(getContext(), editDialogTitle,\r\n                    null,\r\n                    newValue -> {\r\n                        if (mService.findChannelGroupByTitle(newValue) != null) {\r\n                            return false;\r\n                        }\r\n\r\n                        if (Helpers.contains(newValue, \"/\") && !Helpers.startsWith(newValue, \"http\")) {\r\n                            newValue = String.format(\"https://%s\", newValue);\r\n                        }\r\n\r\n                        if (Helpers.startsWith(newValue, \"http\")) {\r\n                            RxHelper.execute(mService.importGroupsObserve(Uri.parse(newValue)), this::pinGroups,\r\n                                    error -> MessageHelpers.showLongMessage(getContext(), error.getMessage()));\r\n                        } else {\r\n                            ItemGroup group = mService.createChannelGroup(newValue, null,\r\n                                    Collections.singletonList(mService.createChannel(item.getAuthor(), item.cardImageUrl, item.channelId)));\r\n                            mService.addChannelGroup(group);\r\n                            BrowsePresenter.instance(getContext()).pinItem(Video.from(group));\r\n                            MessageHelpers.showMessage(getContext(), getContext().getString(R.string.pinned_to_sidebar));\r\n                        }\r\n                        return true;\r\n                    });\r\n        }, false));\r\n\r\n        for (ItemGroup group : groups) {\r\n            options.add(UiOptionItem.from(group.getTitle(), optionItem -> {\r\n                BrowsePresenter presenter = BrowsePresenter.instance(getContext());\r\n\r\n                if (optionItem.isSelected()) {\r\n                    group.add(mService.createChannel(item.getAuthor(), item.cardImageUrl, item.channelId));\r\n                } else {\r\n                    group.remove(item.channelId);\r\n                    Object data = presenter.getCurrentSection() != null ? presenter.getCurrentSection().getData() : null;\r\n                    if (callback != null && (data instanceof Video) && Helpers.equals(((Video) data).channelGroupId, group.getId())) {\r\n                        callback.onItemAction(item, VideoMenuCallback.ACTION_REMOVE_AUTHOR);\r\n                    }\r\n                }\r\n\r\n                if (!group.isEmpty()) {\r\n                    mService.addChannelGroup(group);\r\n                    presenter.pinItem(Video.from(group));\r\n                } else {\r\n                    mService.removeChannelGroup(group);\r\n                    presenter.unpinItem(Video.from(group));\r\n                }\r\n            }, group.contains(item.channelId)));\r\n        }\r\n\r\n        dialogPresenter.appendCheckedCategory(getContext().getString(getTitleResId()), options);\r\n        dialogPresenter.showDialog(getContext().getString(getTitleResId()));\r\n    }\r\n\r\n    private void pinGroups(@NonNull List<ItemGroup> newGroups) {\r\n        if (newGroups.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        for (ItemGroup group : newGroups) {\r\n            BrowsePresenter.instance(getContext()).pinItem(Video.from(group));\r\n        }\r\n        MessageHelpers.showMessage(getContext(), getContext().getString(R.string.pinned_to_sidebar));\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/menu/providers/channelgroup/ChannelGroupServiceWrapper.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.channelgroup;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport android.net.Uri;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.ChannelGroupService;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.ItemGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.ItemGroup.Item;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs.ProfileChangeListener;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\n\r\nimport java.io.File;\r\nimport java.util.List;\r\n\r\nimport io.reactivex.Observable;\r\n\r\npublic class ChannelGroupServiceWrapper implements ProfileChangeListener {\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static ChannelGroupServiceWrapper sInstance;\r\n    private final AppPrefs mPrefs;\r\n    private ChannelGroupService mService;\r\n\r\n    private ChannelGroupServiceWrapper(Context context) {\r\n        mPrefs = AppPrefs.instance(context);\r\n        mPrefs.addListener(this);\r\n        restoreState();\r\n    }\r\n\r\n    public static ChannelGroupServiceWrapper instance(Context context) {\r\n        if (sInstance == null && context != null) {\r\n            sInstance = new ChannelGroupServiceWrapper(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public List<ItemGroup> getChannelGroups() {\r\n        return getService().getChannelGroups();\r\n    }\r\n\r\n    public void addChannelGroup(ItemGroup group) {\r\n        getService().addChannelGroup(group);\r\n    }\r\n\r\n    public void removeChannelGroup(ItemGroup group) {\r\n        getService().removeChannelGroup(group);\r\n    }\r\n\r\n    public String[] findChannelIdsForGroup(String channelGroupId) {\r\n        return getService().findChannelIdsForGroup(channelGroupId);\r\n    }\r\n\r\n    public ItemGroup findChannelGroupById(String channelGroupId) {\r\n        return getService().findChannelGroupById(channelGroupId);\r\n    }\r\n\r\n    public ItemGroup findChannelGroupByTitle(String title) {\r\n        return getService().findChannelGroupByTitle(title);\r\n    }\r\n\r\n    public ItemGroup createChannelGroup(String title, String iconUrl, List<Item> channels) {\r\n        return getService().createChannelGroup(title, iconUrl, channels);\r\n    }\r\n\r\n    public Item createChannel(String title, String iconUrl, String channelId) {\r\n        return getService().createChannel(channelId, title, iconUrl);\r\n    }\r\n\r\n    public void renameChannelGroup(ItemGroup channelGroup, String title) {\r\n        getService().renameChannelGroup(channelGroup, title);\r\n    }\r\n\r\n    public Observable<List<ItemGroup>> importGroupsObserve(Uri uri) {\r\n        return getService().importGroupsObserve(uri);\r\n    }\r\n\r\n    public Observable<List<ItemGroup>> importGroupsObserve(File file) {\r\n        return getService().importGroupsObserve(file);\r\n    }\r\n\r\n    public boolean isEmpty() {\r\n        return getService().isEmpty();\r\n    }\r\n\r\n    private void restoreState() {\r\n        String data = mPrefs.getChannelGroupData();\r\n        if (data != null) {\r\n            mPrefs.setChannelGroupData(null);\r\n            getService().exportData(data);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onProfileChanged() {\r\n        restoreState();\r\n    }\r\n\r\n    private ChannelGroupService getService() {\r\n        if (mService == null) {\r\n            mService = YouTubeServiceManager.instance().getChannelGroupService();\r\n        }\r\n\r\n        return mService;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/menu/providers/channelgroup/RemoveGroupMenuProvider.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.channelgroup;\r\n\r\nimport android.content.Context;\r\n\r\nimport androidx.annotation.NonNull;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.BrowsePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter.VideoMenuCallback;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.ContextMenuProvider;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\n\r\npublic class RemoveGroupMenuProvider extends ContextMenuProvider {\r\n    private final ChannelGroupServiceWrapper mService;\r\n\r\n    public RemoveGroupMenuProvider(@NonNull Context context, int idx) {\r\n        super(context, idx);\r\n        mService = ChannelGroupServiceWrapper.instance(context);\r\n    }\r\n\r\n    @Override\r\n    public int getTitleResId() {\r\n        return R.string.unpin_group_from_sidebar;\r\n    }\r\n\r\n    @Override\r\n    public void onClicked(Video item, VideoMenuCallback callback) {\r\n        AppDialogUtil.showConfirmationDialog(getContext(), getContext().getString(R.string.unpin_group_from_sidebar), () -> {\r\n            mService.removeChannelGroup(\r\n                    mService.findChannelGroupById(item.channelGroupId)\r\n            );\r\n            BrowsePresenter.instance(getContext()).unpinItem(item);\r\n            AppDialogPresenter.instance(getContext()).closeDialog();\r\n        });\r\n    }\r\n\r\n    @Override\r\n    public boolean isEnabled(Video item) {\r\n        return item != null && item.channelGroupId != null;\r\n    }\r\n\r\n    @Override\r\n    public int getMenuType() {\r\n        return MENU_TYPE_SECTION;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/dialogs/menu/providers/channelgroup/RenameGroupMenuProvider.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.channelgroup;\r\n\r\nimport android.content.Context;\r\n\r\nimport androidx.annotation.NonNull;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.ItemGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.BrowsePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter.VideoMenuCallback;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.ContextMenuProvider;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.SimpleEditDialog;\r\n\r\npublic class RenameGroupMenuProvider extends ContextMenuProvider {\r\n    private final ChannelGroupServiceWrapper mService;\r\n\r\n    public RenameGroupMenuProvider(@NonNull Context context, int idx) {\r\n        super(context, idx);\r\n        mService = ChannelGroupServiceWrapper.instance(context);\r\n    }\r\n\r\n    @Override\r\n    public int getTitleResId() {\r\n        return R.string.rename_group;\r\n    }\r\n\r\n    @Override\r\n    public void onClicked(Video item, VideoMenuCallback callback) {\r\n        AppDialogPresenter.instance(getContext()).closeDialog();\r\n        SimpleEditDialog.show(\r\n                getContext(),\r\n                getContext().getString(R.string.rename_group),\r\n                item.title,\r\n                newValue -> {\r\n                    item.title = newValue;\r\n                    BrowsePresenter.instance(getContext()).renameSection(item);\r\n\r\n                    ItemGroup channelGroup = mService.findChannelGroupById(item.channelGroupId);\r\n\r\n                    if (channelGroup != null) {\r\n                        //channelGroup.title = newValue;\r\n                        //mService.addChannelGroup(channelGroup);\r\n                        mService.renameChannelGroup(channelGroup, newValue);\r\n                    }\r\n\r\n                    return true;\r\n                });\r\n    }\r\n\r\n    @Override\r\n    public boolean isEnabled(Video item) {\r\n        return item != null && item.channelGroupId != null;\r\n    }\r\n\r\n    @Override\r\n    public int getMenuType() {\r\n        return MENU_TYPE_SECTION;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/interfaces/Presenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.interfaces;\r\n\r\nimport android.content.Context;\r\n\r\npublic interface Presenter<T> {\r\n    void setView(T view);\r\n    T getView();\r\n    void setContext(Context context);\r\n    Context getContext();\r\n    void onViewInitialized();\r\n    void onViewDestroyed();\r\n    void onViewPaused();\r\n    void onViewResumed();\r\n    void onFinish();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/interfaces/SectionPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.interfaces;\r\n\r\npublic interface SectionPresenter {\r\n    void onSectionFocused(int sectionId);\r\n    void onSectionLongPressed(int sectionId);\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/interfaces/VideoGroupPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.interfaces;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\n\r\npublic interface VideoGroupPresenter {\r\n    void onVideoItemSelected(Video item);\r\n    void onVideoItemClicked(Video item);\r\n    void onVideoItemLongClicked(Video item);\r\n    void onScrollEnd(Video item);\r\n    boolean hasPendingActions();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/service/SidebarService.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.service;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs.ProfileChangeListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\n\r\nimport java.util.Collection;\r\nimport java.util.Collections;\r\nimport java.util.LinkedHashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\n\r\npublic class SidebarService implements ProfileChangeListener {\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static SidebarService sInstance;\r\n    private final Context mContext;\r\n    private List<Video> mPinnedItems;\r\n    private final Map<Integer, Integer> mDefaultSections = new LinkedHashMap<>();\r\n    private final AppPrefs mPrefs;\r\n    private boolean mIsSettingsSectionEnabled;\r\n    private int mBootSectionId;\r\n    private final static int RESERVED_ID = 100;\r\n\r\n    private SidebarService(Context context) {\r\n        mContext = context.getApplicationContext();\r\n        mPrefs = AppPrefs.instance(context);\r\n        mPrefs.addListener(this);\r\n        initSections();\r\n        restoreState();\r\n    }\r\n\r\n    public static SidebarService instance(Context context) {\r\n        if (sInstance == null && context != null) {\r\n            sInstance = new SidebarService(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public Collection<Video> getPinnedItems() {\r\n        return Collections.unmodifiableList(mPinnedItems);\r\n    }\r\n\r\n    public int addPinnedItem(Video item) {\r\n        if (mPinnedItems.contains(item)) {\r\n            return -1;\r\n        }\r\n\r\n        int idx = Helpers.indexOfFirst(mPinnedItems, obj -> obj.getId() > RESERVED_ID);\r\n\r\n        if (idx != -1) {\r\n            mPinnedItems.add(idx, item);\r\n        } else {\r\n            mPinnedItems.add(item);\r\n        }\r\n        persistState();\r\n\r\n        return idx;\r\n    }\r\n\r\n    public void removePinnedItem(Video item) {\r\n        mPinnedItems.remove(item);\r\n        persistState();\r\n    }\r\n\r\n    public void enableSection(int sectionId, boolean enabled) {\r\n        if (enabled) {\r\n            if (sectionId == MediaGroup.TYPE_SETTINGS) {\r\n                mIsSettingsSectionEnabled = true; // prevent Settings lock\r\n            }\r\n\r\n            //Video item = new Video();\r\n            //item.sectionId = sectionId;\r\n            //\r\n            //if (mPinnedItems.contains(item)) { // don't reorder if item already exists\r\n            //    return;\r\n            //}\r\n\r\n            Video section = Helpers.findFirst(mPinnedItems, item -> item != null && item.sectionId == sectionId);\r\n\r\n            if (section != null) { // don't reorder if item already exists\r\n                return;\r\n            }\r\n\r\n            Video item = new Video();\r\n            item.sectionId = sectionId;\r\n\r\n            int index = getDefaultSectionIndex(sectionId);\r\n\r\n            if (index == -1 || index > mPinnedItems.size()) {\r\n                mPinnedItems.add(item);\r\n            } else {\r\n                mPinnedItems.add(index, item);\r\n            }\r\n        } else {\r\n            Helpers.removeIf(mPinnedItems, item -> item == null || item.sectionId == sectionId);\r\n        }\r\n\r\n        persistState();\r\n    }\r\n\r\n    public Map<Integer, Integer> getDefaultSections() {\r\n        return mDefaultSections;\r\n    }\r\n\r\n    private int getDefaultSectionIndex(int sectionId) {\r\n        int index = -1;\r\n\r\n        Collection<Integer> values = mDefaultSections.values();\r\n\r\n        for (int item : values) {\r\n            index++;\r\n            if (item == sectionId) {\r\n                break;\r\n            }\r\n        }\r\n\r\n        return index;\r\n    }\r\n\r\n    /**\r\n     * Contains sections and pinned items!\r\n     */\r\n    public boolean isSectionPinned(int sectionId) {\r\n        Video section = Helpers.findFirst(mPinnedItems, item -> getSectionId(item) == sectionId);\r\n        return section != null; // by default enable all pinned items\r\n    }\r\n\r\n    public int getSectionIndex(int sectionId) {\r\n        // 1) Distinguish section from pinned item\r\n        // 2) Add pinned items after the sections\r\n\r\n        int index = findPinnedItemIndex(sectionId);\r\n\r\n        return index;\r\n    }\r\n\r\n    public void renameSection(int sectionId, String newTitle) {\r\n        int index = findPinnedItemIndex(sectionId);\r\n        if (index != -1) {\r\n            Video video = mPinnedItems.get(index);\r\n            video.title = newTitle;\r\n            persistState();\r\n        }\r\n    }\r\n\r\n    public void moveSectionUp(int sectionId) {\r\n        shiftSection(sectionId, -1);\r\n    }\r\n\r\n    public void moveSectionDown(int sectionId) {\r\n        shiftSection(sectionId, 1);\r\n    }\r\n\r\n    public boolean canMoveSectionUp(int sectionId) {\r\n        return canShiftSection(sectionId, -1);\r\n    }\r\n\r\n    public boolean canMoveSectionDown(int sectionId) {\r\n        return canShiftSection(sectionId, 1);\r\n    }\r\n\r\n    private boolean canShiftSection(int sectionId, int shift) {\r\n        int index = findPinnedItemIndex(sectionId);\r\n\r\n        if (index != -1) {\r\n            return  index + shift >= 0 && index + shift < mPinnedItems.size();\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    private void shiftSection(int sectionId, int shift) {\r\n        if (!canShiftSection(sectionId, shift)) {\r\n            return;\r\n        }\r\n\r\n        int index = findPinnedItemIndex(sectionId);\r\n\r\n        if (index != -1) {\r\n            Video current = mPinnedItems.get(index);\r\n            mPinnedItems.remove(current);\r\n\r\n            mPinnedItems.add(index + shift, current);\r\n            persistState();\r\n        }\r\n    }\r\n\r\n    private int findPinnedItemIndex(int sectionId) {\r\n        int index = -1;\r\n\r\n        for (Video item : mPinnedItems) {\r\n            // Distinguish pinned items by hashCode or extra field (default section)!\r\n            if (getSectionId(item) == sectionId) {\r\n                index = mPinnedItems.indexOf(item);\r\n                break;\r\n            }\r\n        }\r\n\r\n        return index;\r\n    }\r\n\r\n    public void setBootSectionId(int sectionId) {\r\n        mBootSectionId = sectionId;\r\n\r\n        persistState();\r\n    }\r\n\r\n    public int getBootSectionId() {\r\n        return mBootSectionId;\r\n    }\r\n\r\n    public void enableSettingsSection(boolean enabled) {\r\n        mIsSettingsSectionEnabled = enabled;\r\n\r\n        persistState();\r\n    }\r\n\r\n    public boolean isSettingsSectionEnabled() {\r\n        return mIsSettingsSectionEnabled;\r\n    }\r\n\r\n    private int getSectionId(Video item) {\r\n        if (item == null) {\r\n            return -1;\r\n        }\r\n\r\n        return item.sectionId == -1 ? item.getId() : item.sectionId;\r\n    }\r\n\r\n    private void initSections() {\r\n        mDefaultSections.put(R.string.header_notifications, MediaGroup.TYPE_NOTIFICATIONS);\r\n        mDefaultSections.put(R.string.header_home, MediaGroup.TYPE_HOME);\r\n        mDefaultSections.put(R.string.header_shorts, MediaGroup.TYPE_SHORTS);\r\n        mDefaultSections.put(R.string.header_trending, MediaGroup.TYPE_TRENDING);\r\n        mDefaultSections.put(R.string.header_kids_home, MediaGroup.TYPE_KIDS_HOME);\r\n        mDefaultSections.put(R.string.header_sports, MediaGroup.TYPE_SPORTS);\r\n        mDefaultSections.put(R.string.badge_live, MediaGroup.TYPE_LIVE);\r\n        mDefaultSections.put(R.string.header_gaming, MediaGroup.TYPE_GAMING);\r\n        mDefaultSections.put(R.string.header_news, MediaGroup.TYPE_NEWS);\r\n        mDefaultSections.put(R.string.header_music, MediaGroup.TYPE_MUSIC);\r\n        mDefaultSections.put(R.string.header_channels, MediaGroup.TYPE_CHANNEL_UPLOADS);\r\n        mDefaultSections.put(R.string.header_subscriptions, MediaGroup.TYPE_SUBSCRIPTIONS);\r\n        mDefaultSections.put(R.string.header_history, MediaGroup.TYPE_HISTORY);\r\n        mDefaultSections.put(R.string.header_blocked_channels, MediaGroup.TYPE_BLOCKED_CHANNELS);\r\n        mDefaultSections.put(R.string.header_playlists, MediaGroup.TYPE_USER_PLAYLISTS);\r\n        mDefaultSections.put(R.string.my_videos, MediaGroup.TYPE_MY_VIDEOS);\r\n        mDefaultSections.put(R.string.playback_queue_category_title, MediaGroup.TYPE_PLAYBACK_QUEUE);\r\n        mDefaultSections.put(R.string.header_settings, MediaGroup.TYPE_SETTINGS);\r\n    }\r\n\r\n    private void initPinnedItems() {\r\n        for (int sectionId : mDefaultSections.values()) {\r\n            // Broken sections\r\n            enableSection(sectionId, !Helpers.equalsAny(sectionId, new int[]{MediaGroup.TYPE_NOTIFICATIONS, MediaGroup.TYPE_PLAYBACK_QUEUE, MediaGroup.TYPE_TRENDING}));\r\n        }\r\n    }\r\n\r\n    private void cleanupPinnedItems() {\r\n        Helpers.removeDuplicates(mPinnedItems);\r\n\r\n        Helpers.removeIf(mPinnedItems, item -> {\r\n            if (item == null) {\r\n                return true;\r\n            }\r\n\r\n            item.videoId = null;\r\n\r\n            // Fix id collision between pinned and default sections\r\n            if (item.getId() < RESERVED_ID && item.getId() >= -1 && item.sectionId == -1) {\r\n                return true;\r\n            }\r\n\r\n            return !item.hasPlaylist() && item.channelId == null && item.sectionId == -1 && item.channelGroupId == null && !item.hasReloadPageKey();\r\n        });\r\n    }\r\n\r\n    private void restoreState() {\r\n        String data = mPrefs.getSidebarData();\r\n\r\n        String[] split = Helpers.splitData(data);\r\n\r\n        mPinnedItems = Helpers.parseList(split, 0, Video::fromString);\r\n        mBootSectionId = Helpers.parseInt(split, 1, MediaGroup.TYPE_HOME);\r\n        mIsSettingsSectionEnabled = Helpers.parseBoolean(split, 2, true);\r\n\r\n        transferOldPinnedItems();\r\n\r\n        if (mPinnedItems.isEmpty()) {\r\n            initPinnedItems();\r\n        }\r\n\r\n        // Backward compatibility\r\n        enableSection(MediaGroup.TYPE_SETTINGS, true);\r\n\r\n        cleanupPinnedItems();\r\n    }\r\n\r\n    private void transferOldPinnedItems() {\r\n        if (mPinnedItems != null && !mPinnedItems.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        List<Video> oldPinnedItems = GeneralData.instance(mContext).getOldPinnedItems();\r\n\r\n        if (oldPinnedItems != null && !oldPinnedItems.isEmpty()) {\r\n            mPinnedItems = oldPinnedItems;\r\n        }\r\n    }\r\n\r\n    public void persistState() {\r\n        mPrefs.setSidebarData(Helpers.mergeData(mPinnedItems, mBootSectionId, mIsSettingsSectionEnabled));\r\n    }\r\n\r\n    @Override\r\n    public void onProfileChanged() {\r\n        restoreState();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/AboutSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.appupdatechecker2.AppUpdateChecker;\r\nimport com.liskovsoft.sharedutils.helpers.AppInfoHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.locale.LocaleUtility;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.ATVBridgePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.AmazonBridgePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.AppUpdatePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\nimport java.util.Map;\r\nimport java.util.Map.Entry;\r\n\r\npublic class AboutSettingsPresenter extends BasePresenter<Void> {\r\n    private final AppUpdateChecker mUpdateChecker;\r\n\r\n    public AboutSettingsPresenter(Context context) {\r\n        super(context);\r\n\r\n        mUpdateChecker = new AppUpdateChecker(getContext(), null);\r\n    }\r\n\r\n    public static AboutSettingsPresenter instance(Context context) {\r\n        return new AboutSettingsPresenter(context);\r\n    }\r\n\r\n    public void show() {\r\n        String mainTitle = String.format(\"%s %s\",\r\n                getContext().getString(R.string.app_name),\r\n                AppInfoHelpers.getAppVersionName(getContext()));\r\n\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        String country = LocaleUtility.getCurrentLocale(getContext()).getCountry();\r\n\r\n        appendUpdateCheckButton(settingsPresenter);\r\n\r\n        appendAutoUpdateSwitch(settingsPresenter);\r\n\r\n        appendUpdateChangelogButton(settingsPresenter);\r\n\r\n        appendUpdateSource(settingsPresenter);\r\n\r\n        appendInstallBridge(settingsPresenter);\r\n\r\n        if (!Helpers.equalsAny(country, \"RU\", \"UA\")) {\r\n            appendDonation(settingsPresenter);\r\n            appendFeedback(settingsPresenter);\r\n            appendLinks(settingsPresenter);\r\n        }\r\n\r\n        settingsPresenter.showDialog(mainTitle);\r\n    }\r\n\r\n    private void appendAutoUpdateSwitch(AppDialogPresenter settingsPresenter) {\r\n        GeneralData generalData = GeneralData.instance(getContext());\r\n        List<OptionItem> items = new ArrayList<>();\r\n\r\n        items.add(UiOptionItem.from(getContext().getString(R.string.option_disabled),\r\n                optionItem -> mUpdateChecker.setUpdateCheckEnabled(false),\r\n                !mUpdateChecker.isUpdateCheckEnabled()));\r\n\r\n        items.add(UiOptionItem.from(getContext().getString(R.string.sidebar_notification), optionItem -> {\r\n            mUpdateChecker.setUpdateCheckEnabled(true);\r\n            generalData.setOldUpdateNotificationsEnabled(false);\r\n        }, mUpdateChecker.isUpdateCheckEnabled() && !generalData.isOldUpdateNotificationsEnabled()));\r\n        \r\n        items.add(UiOptionItem.from(getContext().getString(R.string.dialog_notification), optionItem -> {\r\n            mUpdateChecker.setUpdateCheckEnabled(true);\r\n            generalData.setOldUpdateNotificationsEnabled(true);\r\n        }, mUpdateChecker.isUpdateCheckEnabled() && generalData.isOldUpdateNotificationsEnabled()));\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.check_updates_auto), items);\r\n    }\r\n\r\n    private void appendUpdateCheckButton(AppDialogPresenter settingsPresenter) {\r\n        OptionItem updateCheckOption = UiOptionItem.from(\r\n                getContext().getString(R.string.check_for_updates),\r\n                option -> AppUpdatePresenter.instance(getContext()).start(true));\r\n\r\n        settingsPresenter.appendSingleButton(updateCheckOption);\r\n    }\r\n\r\n    private void appendUpdateChangelogButton(AppDialogPresenter settingsPresenter) {\r\n        List<String> changes = GeneralData.instance(getContext()).getChangelog();\r\n\r\n        if (changes == null || changes.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        List<OptionItem> changelog = new ArrayList<>();\r\n\r\n        for (String change : changes) {\r\n            changelog.add(UiOptionItem.from(change));\r\n        }\r\n\r\n        String title = String.format(\"%s %s\",\r\n                getContext().getString(R.string.update_changelog),\r\n                AppInfoHelpers.getAppVersionName(getContext()));\r\n\r\n        settingsPresenter.appendStringsCategory(title, changelog);\r\n    }\r\n\r\n    private void appendLinks(AppDialogPresenter settingsPresenter) {\r\n        OptionItem releasesOption = UiOptionItem.from(getContext().getString(R.string.releases),\r\n                option -> Utils.openLink(getContext(), Utils.toQrCodeLink(getContext().getString(R.string.releases_url))));\r\n\r\n        OptionItem sourcesOption = UiOptionItem.from(getContext().getString(R.string.sources),\r\n                option -> Utils.openLink(getContext(), Utils.toQrCodeLink(getContext().getString(R.string.sources_url))));\r\n\r\n        //OptionItem webSiteOption = UiOptionItem.from(getContext().getString(R.string.web_site),\r\n        //        option -> Utils.openLink(getContext(), Utils.toQrCodeLink(getContext().getString(R.string.web_site_url))));\r\n\r\n        settingsPresenter.appendSingleButton(releasesOption);\r\n        settingsPresenter.appendSingleButton(sourcesOption);\r\n        //settingsPresenter.appendSingleButton(webSiteOption);\r\n    }\r\n\r\n    private void appendDonation(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> donateOptions = new ArrayList<>();\r\n\r\n        Map<String, String> donations = Helpers.getMap(getContext(), R.array.donations);\r\n\r\n        for (Entry<String, String> entry : donations.entrySet()) {\r\n            donateOptions.add(UiOptionItem.from(\r\n                    entry.getKey(),\r\n                    option -> Utils.openLink(getContext(), Utils.toQrCodeLink(entry.getValue()))));\r\n        }\r\n\r\n        if (!donateOptions.isEmpty()) {\r\n            settingsPresenter.appendStringsCategory(getContext().getString(R.string.donation), donateOptions);\r\n        }\r\n    }\r\n\r\n    private void appendUpdateSource(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        String[] updateUrls = getContext().getResources().getStringArray(R.array.update_urls);\r\n\r\n        if (updateUrls.length <= 1) {\r\n            return;\r\n        }\r\n\r\n        if (mUpdateChecker.getPreferredHost() == null) {\r\n            mUpdateChecker.setPreferredHost(Helpers.getHost(updateUrls[0]));\r\n        }\r\n\r\n        for (String url : updateUrls) {\r\n            String hostName = Helpers.getHost(url);\r\n            options.add(UiOptionItem.from(hostName,\r\n                    optionItem -> mUpdateChecker.setPreferredHost(hostName),\r\n                    Helpers.equals(hostName, mUpdateChecker.getPreferredHost())));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.preferred_update_source), options);\r\n    }\r\n\r\n    private void appendFeedback(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> feedbackOptions = new ArrayList<>();\r\n\r\n        Map<String, String> feedback = Helpers.getMap(getContext(), R.array.feedback);\r\n\r\n        for (Entry<String, String> entry : feedback.entrySet()) {\r\n            feedbackOptions.add(UiOptionItem.from(\r\n                    entry.getKey(),\r\n                    option -> Utils.openLink(getContext(), Utils.toQrCodeLink(entry.getValue()))));\r\n        }\r\n\r\n        if (!feedbackOptions.isEmpty()) {\r\n            settingsPresenter.appendStringsCategory(getContext().getString(R.string.feedback), feedbackOptions);\r\n        }\r\n    }\r\n\r\n    private void appendInstallBridge(AppDialogPresenter settingsPresenter) {\r\n        OptionItem installBridgeOption = UiOptionItem.from(\r\n                getContext().getString(R.string.install_bridge),\r\n                option -> startBridgePresenter());\r\n\r\n        settingsPresenter.appendSingleButton(installBridgeOption);\r\n    }\r\n\r\n    private void startBridgePresenter() {\r\n        MessageHelpers.showLongMessage(getContext(), R.string.enable_voice_search_desc);\r\n\r\n        ATVBridgePresenter atvPresenter = ATVBridgePresenter.instance(getContext());\r\n        atvPresenter.runBridgeInstaller(true);\r\n        atvPresenter.unhold();\r\n\r\n        AmazonBridgePresenter amazonPresenter = AmazonBridgePresenter.instance(getContext());\r\n        amazonPresenter.runBridgeInstaller(true);\r\n        amazonPresenter.unhold();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/AboutSimpleSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.appupdatechecker2.AppUpdateChecker;\r\nimport com.liskovsoft.sharedutils.helpers.AppInfoHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.ATVBridgePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.AmazonBridgePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.AppUpdatePresenter;\r\n\r\npublic class AboutSimpleSettingsPresenter extends BasePresenter<Void> {\r\n    private final AppUpdateChecker mUpdateChecker;\r\n\r\n    public AboutSimpleSettingsPresenter(Context context) {\r\n        super(context);\r\n\r\n        mUpdateChecker = new AppUpdateChecker(getContext(), null);\r\n    }\r\n\r\n    public static AboutSimpleSettingsPresenter instance(Context context) {\r\n        return new AboutSimpleSettingsPresenter(context);\r\n    }\r\n\r\n    public void show() {\r\n        String mainTitle = String.format(\"%s %s\",\r\n                getContext().getString(R.string.app_name) + \" MOD\",\r\n                AppInfoHelpers.getAppVersionName(getContext()));\r\n\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendAutoUpdateSwitch(settingsPresenter);\r\n\r\n        appendUpdateCheckButton(settingsPresenter);\r\n\r\n        appendInstallBridge(settingsPresenter);\r\n\r\n        settingsPresenter.showDialog(mainTitle);\r\n    }\r\n\r\n    private void appendAutoUpdateSwitch(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleSwitch(UiOptionItem.from(getContext().getString(R.string.check_updates_auto), optionItem -> {\r\n            mUpdateChecker.setUpdateCheckEnabled(optionItem.isSelected());\r\n        }, mUpdateChecker.isUpdateCheckEnabled()));\r\n    }\r\n\r\n    private void appendUpdateCheckButton(AppDialogPresenter settingsPresenter) {\r\n        OptionItem updateCheckOption = UiOptionItem.from(\r\n                getContext().getString(R.string.check_for_updates),\r\n                option -> AppUpdatePresenter.instance(getContext()).start(true));\r\n\r\n        settingsPresenter.appendSingleButton(updateCheckOption);\r\n    }\r\n\r\n    private void appendInstallBridge(AppDialogPresenter settingsPresenter) {\r\n        OptionItem installBridgeOption = UiOptionItem.from(\r\n                getContext().getString(R.string.enable_voice_search),\r\n                option -> startBridgePresenter());\r\n\r\n        settingsPresenter.appendSingleButton(installBridgeOption);\r\n    }\r\n\r\n    private void startBridgePresenter() {\r\n        MessageHelpers.showLongMessage(getContext(), R.string.enable_voice_search_desc);\r\n\r\n        ATVBridgePresenter atvPresenter = ATVBridgePresenter.instance(getContext());\r\n        atvPresenter.runBridgeInstaller(true);\r\n        atvPresenter.unhold();\r\n\r\n        AmazonBridgePresenter amazonPresenter = AmazonBridgePresenter.instance(getContext());\r\n        amazonPresenter.runBridgeInstaller(true);\r\n        amazonPresenter.unhold();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/AccountSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport com.liskovsoft.mediaserviceinterfaces.oauth.Account;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.BrowsePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.YTSignInPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.AccountSelectionPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AccountsData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.SimpleEditDialog;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class AccountSettingsPresenter extends BasePresenter<Void> {\r\n    private static final String TAG = AccountSettingsPresenter.class.getSimpleName();\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static AccountSettingsPresenter sInstance;\r\n    private final MediaServiceManager mMediaServiceManager;\r\n\r\n    public AccountSettingsPresenter(Context context) {\r\n        super(context);\r\n        mMediaServiceManager = MediaServiceManager.instance();\r\n    }\r\n\r\n    public static AccountSettingsPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new AccountSettingsPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void unhold() {\r\n        sInstance = null;\r\n    }\r\n\r\n    public void show() {\r\n        mMediaServiceManager.loadAccounts(this::createAndShowDialog);\r\n    }\r\n\r\n    private void createAndShowDialog(List<Account> accounts) {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendSelectAccountSection(accounts, settingsPresenter);\r\n        appendSignInButton(settingsPresenter);\r\n        appendSignOutSection(accounts, settingsPresenter);\r\n        appendProtectAccountWithPassword(settingsPresenter);\r\n        appendSeparateSettings(settingsPresenter);\r\n        appendSelectAccountOnBoot(settingsPresenter);\r\n\r\n        Account account = getSignInService().getSelectedAccount();\r\n        settingsPresenter.showDialog(account != null ? account.getName() : getContext().getString(R.string.settings_accounts), this::unhold);\r\n    }\r\n\r\n    private void appendSelectAccountSection(List<Account> accounts, AppDialogPresenter settingsPresenter) {\r\n        if (accounts == null || accounts.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        List<OptionItem> optionItems = new ArrayList<>();\r\n\r\n        optionItems.add(UiOptionItem.from(\r\n                getContext().getString(R.string.dialog_account_none), optionItem -> {\r\n                    AccountSelectionPresenter.instance(getContext()).selectAccount(null);\r\n                    settingsPresenter.closeDialog();\r\n                }, true\r\n        ));\r\n\r\n        String accountName = \" (\" + getContext().getString(R.string.dialog_account_none) + \")\";\r\n\r\n        for (Account account : accounts) {\r\n            optionItems.add(UiOptionItem.from(\r\n                    getFullName(account), option -> {\r\n                        AccountSelectionPresenter.instance(getContext()).selectAccount(account);\r\n                        settingsPresenter.closeDialog();\r\n                    }, account.isSelected()\r\n            ));\r\n\r\n            if (account.isSelected()) {\r\n                accountName = \" (\" + getSimpleName(account) + \")\";\r\n            }\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.dialog_account_list) + accountName, optionItems);\r\n    }\r\n\r\n    private void appendSignOutSection(List<Account> accounts, AppDialogPresenter settingsPresenter) {\r\n        if (accounts == null || accounts.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        List<OptionItem> optionItems = new ArrayList<>();\r\n\r\n        for (Account account : accounts) {\r\n            optionItems.add(UiOptionItem.from(\r\n                    getFullName(account), option ->\r\n                        AppDialogUtil.showConfirmationDialog(\r\n                                getContext(), getContext().getString(R.string.dialog_remove_account), () -> {\r\n                                    removeAccount(account);\r\n                                    settingsPresenter.closeDialog();\r\n                                    MessageHelpers.showMessage(getContext(), R.string.msg_done);\r\n                                })\r\n            ));\r\n        }\r\n\r\n        settingsPresenter.appendStringsCategory(getContext().getString(R.string.dialog_remove_account), optionItems);\r\n    }\r\n\r\n    private void appendSignInButton(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleButton(UiOptionItem.from(\r\n                getContext().getString(R.string.dialog_add_account), option -> YTSignInPresenter.instance(getContext()).start()));\r\n    }\r\n\r\n    private void appendSelectAccountOnBoot(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleSwitch(UiOptionItem.from(getContext().getString(R.string.select_account_on_boot), optionItem -> {\r\n            AccountsData.instance(getContext()).selectAccountOnBoot(optionItem.isSelected());\r\n        }, AccountsData.instance(getContext()).isSelectAccountOnBootEnabled()));\r\n    }\r\n\r\n    private void appendProtectAccountWithPassword(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleSwitch(UiOptionItem.from(getContext().getString(R.string.protect_account_with_password), optionItem -> {\r\n            if (optionItem.isSelected()) {\r\n                showAddPasswordDialog(settingsPresenter);\r\n            } else {\r\n                showRemovePasswordDialog(settingsPresenter);\r\n            }\r\n        }, AccountsData.instance(getContext()).getAccountPassword() != null));\r\n    }\r\n\r\n    private void appendSeparateSettings(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleSwitch(UiOptionItem.from(getContext().getString(R.string.multi_profiles),\r\n                option -> {\r\n                    AppPrefs.instance(getContext()).enableMultiProfiles(option.isSelected());\r\n                    BrowsePresenter.instance(getContext()).updateSections();\r\n                },\r\n                AppPrefs.instance(getContext()).isMultiProfilesEnabled()));\r\n    }\r\n\r\n    private String getFullName(Account account) {\r\n        String format;\r\n\r\n        if (account.getEmail() != null) {\r\n            format = String.format(\"%s (%s)\", account.getName(), account.getEmail());\r\n        } else {\r\n            format = account.getName();\r\n        }\r\n\r\n        return format;\r\n    }\r\n\r\n    private String getSimpleName(Account account) {\r\n        return account.getName() != null ? account.getName() : account.getEmail();\r\n    }\r\n\r\n    private void removeAccount(Account account) {\r\n        getSignInService().removeAccount(account);\r\n        BrowsePresenter.instance(getContext()).refresh(false);\r\n    }\r\n\r\n    private void showAddPasswordDialog(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.closeDialog();\r\n        SimpleEditDialog.showPassword(\r\n                getContext(),\r\n                getContext().getString(R.string.enter_account_password),\r\n                null,\r\n                newValue -> {\r\n                    AccountsData.instance(getContext()).setAccountPassword(newValue);\r\n                    BrowsePresenter.instance(getContext()).updateSections();\r\n                    //onSuccess.run();\r\n                    return true;\r\n                });\r\n    }\r\n\r\n    private void showRemovePasswordDialog(AppDialogPresenter settingsPresenter) {\r\n        String password = AccountsData.instance(getContext()).getAccountPassword();\r\n\r\n        if (password == null) {\r\n            return;\r\n        }\r\n\r\n        settingsPresenter.closeDialog();\r\n        SimpleEditDialog.showPassword(\r\n                getContext(),\r\n                getContext().getString(R.string.enter_account_password),\r\n                null,\r\n                newValue -> {\r\n                    if (Utils.passwordMatch(password, newValue)) {\r\n                        AccountsData.instance(getContext()).setAccountPassword(null);\r\n                        BrowsePresenter.instance(getContext()).updateSections();\r\n                        //onSuccess.run();\r\n                        return true;\r\n                    }\r\n                    return false;\r\n                });\r\n    }\r\n\r\n    public void showCheckPasswordDialog() {\r\n        String password = AccountsData.instance(getContext()).getAccountPassword();\r\n\r\n        if (password == null) {\r\n            return;\r\n        }\r\n\r\n        SimpleEditDialog.showPassword(\r\n                getContext(),\r\n                getContext().getString(R.string.enter_account_password),\r\n                null,\r\n                newValue -> {\r\n                    if (Utils.passwordMatch(password, newValue)) {\r\n                        AccountsData.instance(getContext()).setPasswordAccepted(true);\r\n                        BrowsePresenter.instance(getContext()).updateSections();\r\n                        //onSuccess.run();\r\n                        return true;\r\n                    }\r\n                    return false;\r\n                });\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/AutoFrameRateSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.AutoFrameRateController;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionCategory;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\n\r\npublic class AutoFrameRateSettingsPresenter extends BasePresenter<Void> {\r\n    private final PlayerData mPlayerData;\r\n\r\n    public AutoFrameRateSettingsPresenter(Context context) {\r\n        super(context);\r\n        mPlayerData = PlayerData.instance(context);\r\n    }\r\n\r\n    public static AutoFrameRateSettingsPresenter instance(Context context) {\r\n        return new AutoFrameRateSettingsPresenter(context);\r\n    }\r\n\r\n    public void show() {\r\n        show(() -> {});\r\n    }\r\n\r\n    public void show(Runnable onFinish) {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendAutoFrameRateCategory(settingsPresenter);\r\n        appendAutoFrameRatePauseCategory(settingsPresenter);\r\n        appendAutoFrameRateModesCategory(settingsPresenter);\r\n\r\n        settingsPresenter.showDialog(getContext().getString(R.string.auto_frame_rate), onFinish);\r\n    }\r\n\r\n    private void appendAutoFrameRateCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AutoFrameRateController.createAutoFrameRateCategory(getContext(), mPlayerData);\r\n        settingsPresenter.appendCategory(category);\r\n    }\r\n\r\n    private void appendAutoFrameRatePauseCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AutoFrameRateController.createAutoFrameRatePauseCategory(getContext(), mPlayerData);\r\n        settingsPresenter.appendCategory(category);\r\n    }\r\n\r\n    private void appendAutoFrameRateModesCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AutoFrameRateController.createAutoFrameRateModesCategory(getContext());\r\n        settingsPresenter.appendCategory(category);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/BackupSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.GoogleSignInPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.service.SidebarService;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.BackupAndRestoreManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.GDriveBackupManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.GDriveBackupWorker;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.LocalDriveBackupWorker;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class BackupSettingsPresenter extends BasePresenter<Void> {\r\n    private static final String TAG = BackupSettingsPresenter.class.getSimpleName();\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static BackupSettingsPresenter sInstance;\r\n    private final GDriveBackupManager mGDriveBackupManager;\r\n    private final GeneralData mGeneralData;\r\n    private final SidebarService mSidebarService;\r\n\r\n    private BackupSettingsPresenter(Context context) {\r\n        super(context);\r\n        mGDriveBackupManager = GDriveBackupManager.instance(context);\r\n        mGeneralData = GeneralData.instance(context);\r\n        mSidebarService = SidebarService.instance(context);\r\n    }\r\n\r\n    public static BackupSettingsPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new BackupSettingsPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void unhold() {\r\n        sInstance = null;\r\n    }\r\n\r\n    public void show() {\r\n        createAndShowDialog();\r\n    }\r\n\r\n    public void showLocalRestoreDialogApi30() {\r\n        BackupAndRestoreManager backupManager = new BackupAndRestoreManager(getContext(), true);\r\n\r\n        backupManager.getBackupNames(names -> showLocalRestoreDialog(backupManager, names));\r\n    }\r\n\r\n    private void createAndShowDialog() {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendLocalBackupCategory(settingsPresenter);\r\n        appendGDriveBackupCategory(settingsPresenter);\r\n        appendSubscriptionsBackupButton(settingsPresenter);\r\n        settingsPresenter.showDialog(getContext().getString(R.string.app_backup_restore), this::unhold);\r\n    }\r\n\r\n    private void appendGDriveBackupCategory(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleButton(UiOptionItem.from(\"Google Drive\", optionItem -> {\r\n            AppDialogPresenter settingsPresenter2 = AppDialogPresenter.instance(getContext());\r\n            appendGDriveBackupSettings(settingsPresenter2);\r\n            appendGDriveRestoreSettings(settingsPresenter2);\r\n            // NOTE: google account doesn't have a name or email\r\n            appendGDriveAutoBackupButton(settingsPresenter2);\r\n            appendGDriveMiscButtons(settingsPresenter2);\r\n            settingsPresenter2.showDialog(\"Google Drive\");\r\n        }));\r\n    }\r\n\r\n    private void appendGDriveRestoreSettings(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleButton(UiOptionItem.from(getContext().getString(R.string.app_restore), optionItem -> mGDriveBackupManager.restore()));\r\n    }\r\n\r\n    private void appendGDriveBackupSettings(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleButton(UiOptionItem.from(getContext().getString(R.string.app_backup), optionItem -> mGDriveBackupManager.backup()));\r\n    }\r\n\r\n    private void appendLocalAutoBackupOption(List<OptionItem> options) {\r\n        options.add(\r\n                UiOptionItem.from(\r\n                        getContext().getString(R.string.auto_backup_category), option -> {\r\n                            AppDialogPresenter settingsPresenter2 = AppDialogPresenter.instance(getContext());\r\n                            List<OptionItem> options2 = new ArrayList<>();\r\n\r\n                            for (int[] pair : new int[][] {\r\n                                    {R.string.dialog_account_none, -1},\r\n                                    {R.string.once_a_day, 1},\r\n                                    {R.string.once_a_week, 7},\r\n                                    {R.string.once_a_month, 30},\r\n                            }) {\r\n                                options2.add(UiOptionItem.from(getContext().getString(pair[0]),\r\n                                        optionItem -> {\r\n                                            mGeneralData.setLocalDriveBackupFreqDays(pair[1]);\r\n                                            if (pair[1] > 0) {\r\n                                                LocalDriveBackupWorker.forceSchedule(getContext());\r\n                                            } else {\r\n                                                LocalDriveBackupWorker.cancel(getContext());\r\n                                            }\r\n                                        },\r\n                                        mGeneralData.getLocalDriveBackupFreqDays() == pair[1]\r\n                                ));\r\n                            }\r\n\r\n                            settingsPresenter2.appendRadioCategory(getContext().getString(R.string.auto_backup_category), options2);\r\n                            settingsPresenter2.showDialog();\r\n                        })\r\n        );\r\n    }\r\n\r\n    private void appendGDriveAutoBackupButton(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleButton(UiOptionItem.from(\r\n                getContext().getString(R.string.auto_backup_category), option -> {\r\n                    AppDialogPresenter settingsPresenter2 = AppDialogPresenter.instance(getContext());\r\n                    List<OptionItem> options = new ArrayList<>();\r\n\r\n                    for (int[] pair : new int[][] {\r\n                            {R.string.dialog_account_none, -1},\r\n                            {R.string.once_a_day, 1},\r\n                            {R.string.once_a_week, 7},\r\n                            {R.string.once_a_month, 30},\r\n                    }) {\r\n                        options.add(UiOptionItem.from(getContext().getString(pair[0]),\r\n                                optionItem -> {\r\n                                    mGeneralData.setGDriveBackupFreqDays(pair[1]);\r\n                                    if (pair[1] > 0) {\r\n                                        GDriveBackupWorker.forceSchedule(getContext());\r\n                                    } else {\r\n                                        GDriveBackupWorker.cancel(getContext());\r\n                                    }\r\n                                },\r\n                                mGeneralData.getGDriveBackupFreqDays() == pair[1]\r\n                        ));\r\n                    }\r\n\r\n                    settingsPresenter2.appendRadioCategory(getContext().getString(R.string.auto_backup_category), options);\r\n                    settingsPresenter2.showDialog();\r\n                }));\r\n    }\r\n\r\n    private void appendGDriveMiscButtons(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleSwitch(UiOptionItem.from(\r\n                getContext().getString(R.string.device_specific_backup),\r\n                option2 -> mGeneralData.setDeviceSpecificBackupEnabled(option2.isSelected()),\r\n                mGeneralData.isDeviceSpecificBackupEnabled()\r\n        ));\r\n\r\n        settingsPresenter.appendSingleButton(UiOptionItem.from(\r\n                getContext().getString(R.string.dialog_add_account), option2 -> GoogleSignInPresenter.instance(getContext()).start()));\r\n    }\r\n\r\n    private void appendLocalBackupCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        appendLocalBackupRestoreOptions(options);\r\n        appendLocalAutoBackupOption(options);\r\n\r\n        settingsPresenter.appendStringsCategory(getContext().getString(R.string.local_backup), options);\r\n    }\r\n\r\n    private void appendLocalBackupRestoreOptions(List<OptionItem> options) {\r\n        BackupAndRestoreManager backupManager = new BackupAndRestoreManager(getContext());\r\n\r\n        String backupPath = backupManager.getBackupRootPath();\r\n\r\n        options.add(UiOptionItem.from(\r\n                backupPath == null ? getContext().getString(R.string.app_backup) :\r\n                    String.format(\"%s:\\n%s\", getContext().getString(R.string.app_backup), backupPath),\r\n                option -> {\r\n                    AppDialogUtil.showConfirmationDialog(getContext(), getContext().getString(R.string.app_backup), () -> {\r\n                        mSidebarService.enableSection(MediaGroup.TYPE_SETTINGS, true); // prevent Settings lock\r\n                        backupManager.checkPermAndBackup();\r\n                        MessageHelpers.showMessage(getContext(), R.string.msg_done);\r\n                    });\r\n                }));\r\n\r\n        options.add(UiOptionItem.from(\r\n                backupPath == null ? getContext().getString(R.string.app_restore) :\r\n                    String.format(\"%s:\\n%s\", getContext().getString(R.string.app_restore), backupPath),\r\n                option -> {\r\n                    backupManager.getBackupNames(names -> showLocalRestoreDialog(backupManager, names));\r\n                }));\r\n    }\r\n\r\n    private void appendSubscriptionsBackupButton(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleButton(AppDialogUtil.createSubscriptionsBackupButton(getContext()));\r\n    }\r\n\r\n    private void showLocalRestoreDialog(BackupAndRestoreManager backupManager, List<String> backups) {\r\n        if (backups != null && !backups.isEmpty()) {\r\n            showLocalRestoreSelectorDialog(backups, backupManager);\r\n        } else {\r\n            MessageHelpers.showLongMessage(getContext(), R.string.nothing_found);\r\n        }\r\n    }\r\n\r\n    private void showLocalRestoreSelectorDialog(List<String> backups, BackupAndRestoreManager backupManager) {\r\n        AppDialogPresenter dialog = AppDialogPresenter.instance(getContext());\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (String name : backups) {\r\n            options.add(UiOptionItem.from(name, optionItem -> {\r\n                AppDialogUtil.showConfirmationDialog(getContext(), getContext().getString(R.string.app_restore), () -> {\r\n                    backupManager.checkPermAndRestore(name);\r\n                });\r\n            }));\r\n        }\r\n\r\n        dialog.appendStringsCategory(getContext().getString(R.string.app_restore), options);\r\n        dialog.showDialog();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/DeArrowSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.DeArrowData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.ClickbaitRemover;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class DeArrowSettingsPresenter extends BasePresenter<Void> {\r\n    private final MainUIData mMainUIData;\r\n    private final DeArrowData mDeArrowData;\r\n\r\n    private DeArrowSettingsPresenter(Context context) {\r\n        super(context);\r\n        mMainUIData = MainUIData.instance(context);\r\n        mDeArrowData = DeArrowData.instance(context);\r\n    }\r\n\r\n    public static DeArrowSettingsPresenter instance(Context context) {\r\n        return new DeArrowSettingsPresenter(context);\r\n    }\r\n\r\n    public void show(Runnable onFinish) {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n        \r\n        appendSwitches(settingsPresenter);\r\n        appendThumbQuality(settingsPresenter);\r\n        appendLinks(settingsPresenter);\r\n\r\n        settingsPresenter.showDialog(getContext().getString(R.string.dearrow_provider), onFinish);\r\n    }\r\n\r\n    public void show() {\r\n        show(null);\r\n    }\r\n\r\n    private void appendThumbQuality(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.thumb_quality_default, ClickbaitRemover.THUMB_QUALITY_DEFAULT},\r\n                {R.string.thumb_quality_start, ClickbaitRemover.THUMB_QUALITY_START},\r\n                {R.string.thumb_quality_middle, ClickbaitRemover.THUMB_QUALITY_MIDDLE},\r\n                {R.string.thumb_quality_end, ClickbaitRemover.THUMB_QUALITY_END}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]),\r\n                    optionItem -> mMainUIData.setThumbQuality(pair[1]),\r\n                    mMainUIData.getThumbQuality() == pair[1]\r\n            ));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.dearrow_not_submitted_thumbs), options);\r\n    }\r\n\r\n    private void appendSwitches(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.card_unlocalized_titles),\r\n                option -> {\r\n                    mMainUIData.setUnlocalizedTitlesEnabled(option.isSelected());\r\n                    mDeArrowData.setReplaceTitlesEnabled(false);\r\n                },\r\n                mMainUIData.isUnlocalizedTitlesEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.crowdsoursed_titles),\r\n                optionItem -> {\r\n                    mDeArrowData.setReplaceTitlesEnabled(optionItem.isSelected());\r\n                    mMainUIData.setUnlocalizedTitlesEnabled(false);\r\n                },\r\n                mDeArrowData.isReplaceTitlesEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.crowdsourced_thumbnails),\r\n                optionItem -> mDeArrowData.setReplaceThumbnailsEnabled(optionItem.isSelected()),\r\n                mDeArrowData.isReplaceThumbnailsEnabled()));\r\n\r\n        for (OptionItem item : options) {\r\n            settingsPresenter.appendSingleSwitch(item);\r\n        }\r\n    }\r\n\r\n    private void appendLinks(AppDialogPresenter settingsPresenter) {\r\n        OptionItem statsCheckOption = UiOptionItem.from(getContext().getString(R.string.dearrow_status),\r\n                option -> Utils.openLink(getContext(), getContext().getString(R.string.dearrow_status_url)));\r\n\r\n        OptionItem webSiteOption = UiOptionItem.from(getContext().getString(R.string.about_dearrow),\r\n                option -> Utils.openLink(getContext(), getContext().getString(R.string.dearrow_provider_url)));\r\n\r\n        settingsPresenter.appendSingleButton(statsCheckOption);\r\n        settingsPresenter.appendSingleButton(webSiteOption);\r\n    }\r\n\r\n    private void appendDeArrowSwitch(AppDialogPresenter settingsPresenter) {\r\n        String title = String.format(\r\n                \"%s (%s)\",\r\n                getContext().getString(R.string.dearrow_provider),\r\n                getContext().getString(R.string.dearrow_provider_url)\r\n        );\r\n        OptionItem sponsorBlockOption = UiOptionItem.from(title,\r\n                option -> mDeArrowData.setDeArrowEnabled(option.isSelected()),\r\n                mDeArrowData.isDeArrowEnabled()\r\n        );\r\n\r\n        settingsPresenter.appendSingleSwitch(sponsorBlockOption);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/GeneralSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.okhttp.OkHttpManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerConstants;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionCategory;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.BrowsePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.ContextMenuManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.ContextMenuProvider;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.service.SidebarService;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.SearchData;\r\nimport com.liskovsoft.smartyoutubetv2.common.proxy.ProxyManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.proxy.WebProxyDialog;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.SimpleEditDialog;\r\nimport com.liskovsoft.youtubeapi.service.internal.MediaServiceData;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Collection;\r\nimport java.util.HashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\nimport java.util.Map.Entry;\r\n\r\npublic class GeneralSettingsPresenter extends BasePresenter<Void> {\r\n    private final GeneralData mGeneralData;\r\n    private final PlayerData mPlayerData;\r\n    private final PlayerTweaksData mPlayerTweaksData;\r\n    private final MainUIData mMainUIData;\r\n    private final MediaServiceData mMediaServiceData;\r\n    private final SidebarService mSidebarService;\r\n    private boolean mRestartApp;\r\n    private final Runnable mOnFinish = () -> {\r\n        if (mRestartApp) {\r\n            mRestartApp = false;\r\n            MessageHelpers.showLongMessage(getContext(), R.string.msg_restart_app);\r\n        }\r\n    };\r\n\r\n    private GeneralSettingsPresenter(Context context) {\r\n        super(context);\r\n        mGeneralData = GeneralData.instance(context);\r\n        mPlayerData = PlayerData.instance(context);\r\n        mPlayerTweaksData = PlayerTweaksData.instance(context);\r\n        mMainUIData = MainUIData.instance(context);\r\n        mMediaServiceData = MediaServiceData.instance();\r\n        mSidebarService = SidebarService.instance(context);\r\n    }\r\n\r\n    public static GeneralSettingsPresenter instance(Context context) {\r\n        return new GeneralSettingsPresenter(context);\r\n    }\r\n\r\n    public void show() {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendBootToSection(settingsPresenter);\r\n        appendEnabledSections(settingsPresenter);\r\n        appendContextMenuItemsCategory(settingsPresenter);\r\n        appendHideContent(settingsPresenter);\r\n        appendAppExitCategory(settingsPresenter);\r\n        appendBackgroundPlaybackCategory(settingsPresenter);\r\n        appendScreenDimmingCategory(settingsPresenter);\r\n        appendKeyRemappingCategory(settingsPresenter);\r\n        appendInternetCensorship(settingsPresenter);\r\n        appendHistoryCategory(settingsPresenter);\r\n        appendMiscCategory(settingsPresenter);\r\n\r\n        settingsPresenter.showDialog(getContext().getString(R.string.settings_general), mOnFinish);\r\n    }\r\n\r\n    private void appendEnabledSections(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        Map<Integer, Integer> sections = mSidebarService.getDefaultSections();\r\n\r\n        for (Entry<Integer, Integer> section : sections.entrySet()) {\r\n            int sectionResId = section.getKey();\r\n            int sectionId = section.getValue();\r\n\r\n            if (sectionId == MediaGroup.TYPE_SETTINGS) {\r\n                continue;\r\n            }\r\n\r\n            options.add(UiOptionItem.from(getContext().getString(sectionResId), optionItem -> {\r\n                BrowsePresenter.instance(getContext()).enableSection(sectionId, optionItem.isSelected());\r\n            }, mSidebarService.isSectionPinned(sectionId)));\r\n        }\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.side_panel_sections), options);\r\n    }\r\n\r\n    private void appendHideContent(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_shorts_everywhere),\r\n                option -> {\r\n                    mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_SHORTS_ALL, option.isSelected());\r\n                    BrowsePresenter.instance(getContext()).enableSection(MediaGroup.TYPE_SHORTS, !option.isSelected());\r\n                },\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_SHORTS_ALL)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_mixes),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_MIXES, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_MIXES)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_watched_from_watch_later),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_WATCHED_WATCH_LATER, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_WATCHED_WATCH_LATER)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_watched_from_home),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_WATCHED_HOME, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_WATCHED_HOME)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_watched_from_subscriptions),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_WATCHED_SUBSCRIPTIONS, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_WATCHED_SUBSCRIPTIONS)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_watched_from_notifications),\r\n                option -> mGeneralData.setHideWatchedFromNotificationsEnabled(option.isSelected()),\r\n                mGeneralData.isHideWatchedFromNotificationsEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_shorts),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_SHORTS_SUBSCRIPTIONS, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_SHORTS_SUBSCRIPTIONS)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_shorts_from_search),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_SHORTS_SEARCH, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_SHORTS_SEARCH)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_shorts_from_home),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_SHORTS_HOME, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_SHORTS_HOME)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_shorts_channel),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_SHORTS_CHANNEL, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_SHORTS_CHANNEL)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_shorts_from_history),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_SHORTS_HISTORY, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_SHORTS_HISTORY)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_shorts_from_trending),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_SHORTS_TRENDING, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_SHORTS_TRENDING)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_streams),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_STREAMS_SUBSCRIPTIONS, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_STREAMS_SUBSCRIPTIONS)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_upcoming),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_UPCOMING_SUBSCRIPTIONS, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_UPCOMING_SUBSCRIPTIONS)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_upcoming_home),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_UPCOMING_HOME, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_UPCOMING_HOME)));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_upcoming_channel),\r\n                option -> mMediaServiceData.setContentHidden(MediaServiceData.CONTENT_UPCOMING_CHANNEL, option.isSelected()),\r\n                mMediaServiceData.isContentHidden(MediaServiceData.CONTENT_UPCOMING_CHANNEL)));\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.hide_unwanted_content), options);\r\n    }\r\n\r\n    private void appendContextMenuItemsCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        Map<Long, Integer> menuNames = getMenuNames();\r\n\r\n        for (Long menuItem : mMainUIData.getMenuItemsOrdered()) {\r\n            Integer nameResId = menuNames.get(menuItem);\r\n\r\n            if (nameResId == null) {\r\n                continue;\r\n            }\r\n\r\n            options.add(UiOptionItem.from(getContext().getString(nameResId), optionItem -> {\r\n                if (optionItem.isSelected()) {\r\n                    mMainUIData.setMenuItemEnabled(menuItem);\r\n                    showMenuItemOrderDialog(menuItem);\r\n                } else {\r\n                    mMainUIData.setMenuItemDisabled(menuItem);\r\n                }\r\n            }, mMainUIData.isMenuItemEnabled(menuItem)));\r\n        }\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.context_menu), options);\r\n    }\r\n\r\n    private void appendContextMenuSortingCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        Map<Long, Integer> menuNames = getMenuNames();\r\n\r\n        for (Long menuItem : mMainUIData.getMenuItemsOrdered()) {\r\n            Integer nameResId = menuNames.get(menuItem);\r\n\r\n            if (nameResId == null || !mMainUIData.isMenuItemEnabled(menuItem)) {\r\n                continue;\r\n            }\r\n\r\n            options.add(UiOptionItem.from(getContext().getString(nameResId), optionItem ->\r\n                    showMenuItemOrderDialog(menuItem), false));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.context_menu_sorting), options);\r\n    }\r\n\r\n    private void showMenuItemOrderDialog(Long menuItem) {\r\n        AppDialogPresenter dialog = AppDialogPresenter.instance(getContext());\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        Map<Long, Integer> menuNames = getMenuNames();\r\n\r\n        Integer currentNameResId = menuNames.get(menuItem);\r\n\r\n        if (currentNameResId == null) {\r\n            return;\r\n        }\r\n\r\n        List<Long> menuItemsOrdered = mMainUIData.getMenuItemsOrdered();\r\n        int size = menuItemsOrdered.size();\r\n        int currentIndex = mMainUIData.getMenuItemIndex(menuItem);\r\n        int counter = 0;\r\n\r\n        for (int i = 0; i < size; i++) {\r\n            Long item = menuItemsOrdered.get(i);\r\n            Integer nameResId = menuNames.get(item);\r\n\r\n            if (nameResId == null || !mMainUIData.isMenuItemEnabled(item)) {\r\n                continue;\r\n            }\r\n\r\n            final int index = i;\r\n            options.add(UiOptionItem.from((counter + 1) + \" \" + getContext().getString(nameResId), optionItem -> {\r\n                if (optionItem.isSelected()) {\r\n                    mMainUIData.setMenuItemIndex(index, menuItem);\r\n                    dialog.goBack();\r\n                }\r\n            }, currentIndex == i));\r\n            counter++;\r\n        }\r\n\r\n        String itemName = getContext().getString(currentNameResId);\r\n        dialog.appendRadioCategory(getContext().getString(R.string.item_postion) + \" \" + itemName, options);\r\n\r\n        dialog.showDialog();\r\n    }\r\n\r\n    private void appendBootToSection(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        Map<Integer, Integer> sections = mSidebarService.getDefaultSections();\r\n\r\n        for (Entry<Integer, Integer> section : sections.entrySet()) {\r\n            options.add(\r\n                    UiOptionItem.from(\r\n                            getContext().getString(section.getKey()),\r\n                            optionItem -> mSidebarService.setBootSectionId(section.getValue()),\r\n                            section.getValue().equals(mSidebarService.getBootSectionId())\r\n                    )\r\n            );\r\n        }\r\n\r\n        Collection<Video> pinnedItems = mSidebarService.getPinnedItems();\r\n\r\n        for (Video item : pinnedItems) {\r\n            if (item != null && item.getTitle() != null) {\r\n                options.add(\r\n                        UiOptionItem.from(\r\n                                item.getTitle(),\r\n                                optionItem -> mSidebarService.setBootSectionId(item.getId()),\r\n                                item.hashCode() == mSidebarService.getBootSectionId()\r\n                        )\r\n                );\r\n            }\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.boot_to_section), options);\r\n    }\r\n\r\n    private void appendAppExitCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.app_exit_none, GeneralData.EXIT_NONE},\r\n                {R.string.app_double_back_exit, GeneralData.EXIT_DOUBLE_BACK},\r\n                {R.string.app_single_back_exit, GeneralData.EXIT_SINGLE_BACK}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]),\r\n                    optionItem -> mGeneralData.setAppExitShortcut(pair[1]),\r\n                    mGeneralData.getAppExitShortcut() == pair[1]));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.app_exit_shortcut), options);\r\n    }\r\n\r\n    private void appendBackgroundPlaybackCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createBackgroundPlaybackCategory(getContext(), mPlayerData, mGeneralData);\r\n        settingsPresenter.appendRadioCategory(category.title, category.options);\r\n    }\r\n\r\n    private void appendKeyRemappingCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(\"OK -> \" + getContext().getString(R.string.player_toggle_speed),\r\n                option -> mPlayerData.setOKButtonBehavior(option.isSelected() ? PlayerData.OK_TOGGLE_SPEED : PlayerData.OK_ONLY_UI),\r\n                mPlayerData.getOKButtonBehavior() == PlayerData.OK_TOGGLE_SPEED));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_quick_shorts_skip_alt),\r\n                option -> {\r\n                    mPlayerTweaksData.setQuickSkipShortsAltEnabled(option.isSelected());\r\n                    mGeneralData.resetDpadUpDownSettings();\r\n                },\r\n                mPlayerTweaksData.isQuickSkipShortsAltEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_quick_shorts_skip),\r\n                option -> {\r\n                    mPlayerTweaksData.setQuickSkipShortsEnabled(option.isSelected());\r\n                    mGeneralData.resetDpadLeftRightSettings();\r\n                },\r\n                mPlayerTweaksData.isQuickSkipShortsEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_quick_skip_videos_alt),\r\n                option -> {\r\n                    mPlayerTweaksData.setQuickSkipVideosAltEnabled(option.isSelected());\r\n                    mGeneralData.resetDpadUpDownSettings();\r\n                },\r\n                mPlayerTweaksData.isQuickSkipVideosAltEnabled()));\r\n        \r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_quick_skip_videos),\r\n                option -> {\r\n                    mPlayerTweaksData.setQuickSkipVideosEnabled(option.isSelected());\r\n                    mGeneralData.resetDpadLeftRightSettings();\r\n                },\r\n                mPlayerTweaksData.isQuickSkipVideosEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Play/Pause -> OK\",\r\n                option -> mGeneralData.setRemapPlayToOKEnabled(option.isSelected()),\r\n                mGeneralData.isRemapPlayToOKEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"DPAD RIGHT/LEFT -> Volume Up/Down\",\r\n                option -> {\r\n                    mGeneralData.setRemapDpadLeftToVolumeEnabled(option.isSelected());\r\n                    mPlayerTweaksData.resetDpadLeftRightSettings();\r\n                },\r\n                mGeneralData.isRemapDpadLeftToVolumeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"DPAD UP/DOWN -> Volume Up/Down\",\r\n                option -> {\r\n                    mGeneralData.setRemapDpadUpToVolumeEnabled(option.isSelected());\r\n                    mPlayerTweaksData.resetDpadUpDownSettings();\r\n                },\r\n                mGeneralData.isRemapDpadUpToVolumeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"DPAD UP/DOWN -> Speed Up/Down\",\r\n                option -> {\r\n                    mGeneralData.setRemapDpadUpDownToSpeedEnabled(option.isSelected());\r\n                    mPlayerTweaksData.resetDpadUpDownSettings();\r\n                },\r\n                mGeneralData.isRemapDpadUpToSpeedEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Numbers 3/1 -> Speed Up/Down\",\r\n                option -> mGeneralData.setRemapNumbersToSpeedEnabled(option.isSelected()),\r\n                mGeneralData.isRemapNumbersToSpeedEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Next/Previous -> Fast Forward/Rewind\",\r\n                option -> mGeneralData.setRemapNextToFastForwardEnabled(option.isSelected()),\r\n                mGeneralData.isRemapNextToFastForwardEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Next/Previous -> Speed Up/Down\",\r\n                option -> mGeneralData.setRemapNextToSpeedEnabled(option.isSelected()),\r\n                mGeneralData.isRemapNextToSpeedEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Fast Forward/Rewind -> Next/Previous\",\r\n                option -> mGeneralData.setRemapFastForwardToNextEnabled(option.isSelected()),\r\n                mGeneralData.isRemapFastForwardToNextEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Fast Forward/Rewind -> Speed Up/Down\",\r\n                option -> mGeneralData.setRemapFastForwardToSpeedEnabled(option.isSelected()),\r\n                mGeneralData.isRemapFastForwardToSpeedEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Fast Forward/Rewind -> Speed Toggle\",\r\n                option -> mGeneralData.setRemapFastForwardToSpeedToggleEnabled(option.isSelected()),\r\n                mGeneralData.isRemapFastForwardToSpeedToggleEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"S -> Speed Toggle\",\r\n                option -> mGeneralData.setRemapSToSpeedToggleEnabled(option.isSelected()),\r\n                mGeneralData.isRemapSToSpeedToggleEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Page Up/Down -> Next/Previous\",\r\n                option -> mGeneralData.setRemapPageUpToNextEnabled(option.isSelected()),\r\n                mGeneralData.isRemapPageUpToNextEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Page Up/Down -> Like/Dislike\",\r\n                option -> mGeneralData.setRemapPageUpToLikeEnabled(option.isSelected()),\r\n                mGeneralData.isRemapPageUpToLikeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Page Up/Down -> Speed Up/Down\",\r\n                option -> mGeneralData.setRemapPageUpToSpeedEnabled(option.isSelected()),\r\n                mGeneralData.isRemapPageUpToSpeedEnabled()));\r\n        \r\n        options.add(UiOptionItem.from(\"Page Up/Down -> Speed Down/Up\",\r\n                option -> mGeneralData.setRemapPageDownToSpeedEnabled(option.isSelected()),\r\n                mGeneralData.isRemapPageDownToSpeedEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Channel Up/Down -> Volume Up/Down\",\r\n                option -> mGeneralData.setRemapChannelUpToVolumeEnabled(option.isSelected()),\r\n                mGeneralData.isRemapChannelUpToVolumeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Channel Up/Down -> Next/Previous\",\r\n                option -> mGeneralData.setRemapChannelUpToNextEnabled(option.isSelected()),\r\n                mGeneralData.isRemapChannelUpToNextEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Channel Up/Down -> Like/Dislike\",\r\n                option -> mGeneralData.setRemapChannelUpToLikeEnabled(option.isSelected()),\r\n                mGeneralData.isRemapChannelUpToLikeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Channel Up/Down -> Speed Up/Down\",\r\n                option -> mGeneralData.setRemapChannelUpToSpeedEnabled(option.isSelected()),\r\n                mGeneralData.isRemapChannelUpToSpeedEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\"Channel Up/Down -> Search\",\r\n                option -> mGeneralData.setRemapChannelUpToSearchEnabled(option.isSelected()),\r\n                mGeneralData.isRemapChannelUpToSearchEnabled()));\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.key_remapping), options);\r\n    }\r\n\r\n    private void appendScreenDimmingCategory(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleButton(UiOptionItem.from(getContext().getString(R.string.screen_dimming), optionItem -> {\r\n            AppDialogPresenter presenter = AppDialogPresenter.instance(getContext());\r\n            appendScreenDimmingAmountCategory(presenter);\r\n            appendScreenDimmingTimeoutCategory(presenter);\r\n            presenter.showDialog(getContext().getString(R.string.screen_dimming));\r\n        }));\r\n    }\r\n\r\n    private void appendScreenDimmingAmountCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        int activeMode = mGeneralData.getScreensaverDimmingPercents();\r\n\r\n        for (int dimPercents : Helpers.range(10, 100, 10)) {\r\n            options.add(UiOptionItem.from(\r\n                    dimPercents + \"%\",\r\n                    option -> mGeneralData.setScreensaverDimmingPercents(dimPercents),\r\n                    activeMode == dimPercents));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.screen_dimming_amount), options);\r\n    }\r\n\r\n    @SuppressLint(\"StringFormatMatches\")\r\n    private void appendScreenDimmingTimeoutCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        int screensaverTimeoutMs = mGeneralData.getScreensaverTimeoutMs();\r\n\r\n        options.add(UiOptionItem.from(\r\n                getContext().getString(R.string.option_never),\r\n                option -> mGeneralData.setScreensaverTimeoutMs(GeneralData.SCREENSAVER_TIMEOUT_NEVER),\r\n                screensaverTimeoutMs == GeneralData.SCREENSAVER_TIMEOUT_NEVER));\r\n\r\n        for (int timeoutSec : new int[] {5, 15, 30}) {\r\n            int timeoutMs = timeoutSec * 1_000;\r\n            options.add(UiOptionItem.from(\r\n                    getContext().getString(R.string.ui_hide_timeout_sec, timeoutSec),\r\n                    option -> mGeneralData.setScreensaverTimeoutMs(timeoutMs),\r\n                    screensaverTimeoutMs == timeoutMs));\r\n        }\r\n\r\n        for (int i = 1; i <= 15; i++) {\r\n            int timeoutMs = i * 60 * 1_000;\r\n            options.add(UiOptionItem.from(\r\n                    getContext().getString(R.string.screen_dimming_timeout_min, i),\r\n                    option -> mGeneralData.setScreensaverTimeoutMs(timeoutMs),\r\n                    screensaverTimeoutMs == timeoutMs));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.screen_dimming_timeout), options);\r\n    }\r\n\r\n    //private void appendTimeFormatCategory(AppDialogPresenter settingsPresenter) {\r\n    //    List<OptionItem> options = new ArrayList<>();\r\n    //\r\n    //    options.add(UiOptionItem.from(\r\n    //            getContext().getString(R.string.time_format_24),\r\n    //            option -> {\r\n    //                mGeneralData.setTimeFormat(GeneralData.TIME_FORMAT_24);\r\n    //                mRestartApp = true;\r\n    //            },\r\n    //            mGeneralData.getTimeFormat() == GeneralData.TIME_FORMAT_24));\r\n    //\r\n    //    options.add(UiOptionItem.from(\r\n    //            getContext().getString(R.string.time_format_12),\r\n    //            option -> {\r\n    //                mGeneralData.setTimeFormat(GeneralData.TIME_FORMAT_12);\r\n    //                mRestartApp = true;\r\n    //            },\r\n    //            mGeneralData.getTimeFormat() == GeneralData.TIME_FORMAT_12));\r\n    //\r\n    //    settingsPresenter.appendRadioCategory(getContext().getString(R.string.time_format), options);\r\n    //}\r\n\r\n    private void appendHistoryCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.auto_history, GeneralData.HISTORY_AUTO},\r\n                {R.string.enable_history, GeneralData.HISTORY_ENABLED},\r\n                {R.string.disable_history, GeneralData.HISTORY_DISABLED}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]), optionItem -> {\r\n                mGeneralData.setHistoryState(pair[1]);\r\n                MediaServiceManager.instance().enableHistory(pair[1] == GeneralData.HISTORY_AUTO || pair[1] == GeneralData.HISTORY_ENABLED);\r\n            }, mGeneralData.getHistoryState() == pair[1]));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.header_history), options);\r\n    }\r\n\r\n    private void appendMiscCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from( getContext().getString(R.string.player_exit_shortcut) + \": \" + getContext().getString(R.string.app_double_back_exit),\r\n                option -> mGeneralData.setPlayerExitShortcut(option.isSelected() ? GeneralData.EXIT_DOUBLE_BACK : GeneralData.EXIT_SINGLE_BACK),\r\n                mGeneralData.getPlayerExitShortcut() == GeneralData.EXIT_DOUBLE_BACK));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.search_exit_shortcut) + \": \" + getContext().getString(R.string.app_double_back_exit),\r\n                option -> mGeneralData.setSearchExitShortcut(option.isSelected() ? GeneralData.EXIT_DOUBLE_BACK : GeneralData.EXIT_SINGLE_BACK),\r\n                mGeneralData.getSearchExitShortcut() == GeneralData.EXIT_DOUBLE_BACK));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.return_to_launcher),\r\n                option -> mGeneralData.setReturnToLauncherEnabled(option.isSelected()),\r\n                mGeneralData.isReturnToLauncherEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.multi_profiles),\r\n                option -> {\r\n                    AppPrefs.instance(getContext()).enableMultiProfiles(option.isSelected());\r\n                    BrowsePresenter.instance(getContext()).updateSections();\r\n                },\r\n                AppPrefs.instance(getContext()).isMultiProfilesEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.child_mode),\r\n                getContext().getString(R.string.child_mode_desc),\r\n                option -> {\r\n                    if (option.isSelected()) {\r\n                        AppDialogUtil.showConfirmationDialog(getContext(), getContext().getString(R.string.lost_setting_warning),\r\n                                () -> showPasswordDialog(settingsPresenter, () -> enableChildMode(option.isSelected())),\r\n                                settingsPresenter::closeDialog);\r\n                    } else {\r\n                        mGeneralData.setSettingsPassword(null);\r\n                        enableChildMode(option.isSelected());\r\n                        settingsPresenter.closeDialog();\r\n                    }\r\n                },\r\n                mGeneralData.isChildModeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.protect_settings_with_password),\r\n                option -> {\r\n                    if (option.isSelected()) {\r\n                        showPasswordDialog(settingsPresenter, null);\r\n                    } else {\r\n                        mGeneralData.setSettingsPassword(null);\r\n                    }\r\n                },\r\n                mGeneralData.getSettingsPassword() != null));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.enable_master_password),\r\n                option -> {\r\n                    if (option.isSelected()) {\r\n                        showMasterPasswordDialog(settingsPresenter, null);\r\n                    } else {\r\n                        mGeneralData.setMasterPassword(null);\r\n                    }\r\n                },\r\n                mGeneralData.getMasterPassword() != null));\r\n\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.app_corner_clock),\r\n        //        option -> {\r\n        //            mGeneralData.enableGlobalClock(option.isSelected());\r\n        //            mRestartApp = true;\r\n        //        },\r\n        //        mGeneralData.isGlobalClockEnabled()));\r\n        //\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.player_corner_clock),\r\n        //        option -> mPlayerData.enableGlobalClock(option.isSelected()),\r\n        //        mPlayerData.isGlobalClockEnabled()));\r\n        //\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.player_corner_ending_time),\r\n        //        option -> mPlayerData.enableGlobalEndingTime(option.isSelected()),\r\n        //        mPlayerData.isGlobalEndingTimeEnabled()));\r\n        //\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.old_home_look),\r\n        //        option -> {\r\n        //            mGeneralData.enableOldHomeLook(option.isSelected());\r\n        //            mRestartApp = true;\r\n        //        },\r\n        //        mGeneralData.isOldHomeLookEnabled()));\r\n        //\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.old_channel_look),\r\n        //        option -> {\r\n        //            mGeneralData.enableOldChannelLook(option.isSelected());\r\n        //            mMainUIData.enableChannelSearchBar(!option.isSelected());\r\n        //        },\r\n        //        mGeneralData.isOldChannelLookEnabled()));\r\n        //\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.fullscreen_mode),\r\n        //        option -> {\r\n        //            mGeneralData.enableFullscreenMode(option.isSelected());\r\n        //            mRestartApp = true;\r\n        //        },\r\n        //        mGeneralData.isFullscreenModeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.remember_position_subscriptions),\r\n                option -> mGeneralData.setRememberSubscriptionsPositionEnabled(option.isSelected()),\r\n                mGeneralData.isRememberSubscriptionsPositionEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.remember_position_pinned),\r\n                option -> mGeneralData.setRememberPinnedPositionEnabled(option.isSelected()),\r\n                mGeneralData.isRememberPinnedPositionEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.disable_screensaver),\r\n                option -> mGeneralData.setScreensaverDisabled(option.isSelected()),\r\n                mGeneralData.isScreensaverDisabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.select_channel_section),\r\n                option -> mGeneralData.setSelectChannelSectionEnabled(option.isSelected()),\r\n                mGeneralData.isSelectChannelSectionEnabled()));\r\n\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.player_show_tooltips) + \": \" + getContext().getString(R.string.long_press_for_options),\r\n        //        option -> {\r\n        //            mGeneralData.enableFirstUseTooltip(option.isSelected());\r\n        //            mRestartApp = true;\r\n        //        },\r\n        //        mGeneralData.isFirstUseTooltipEnabled()));\r\n\r\n        //// Disable long press on buggy controllers.\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.disable_ok_long_press),\r\n        //        option -> mGeneralData.disableOkButtonLongPress(option.isSelected()),\r\n        //        mGeneralData.isOkButtonLongPressDisabled()));\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.player_other), options);\r\n    }\r\n\r\n    private void appendInternetCensorship(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        appendProxyManager(settingsPresenter, options);\r\n\r\n        //appendOpenVPNManager(settingsPresenter, options);\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.internet_censorship), options);\r\n    }\r\n\r\n    private void appendProxyManager(AppDialogPresenter settingsPresenter, List<OptionItem> options) {\r\n        ProxyManager proxyManager = new ProxyManager(getContext());\r\n\r\n        if (proxyManager.isProxySupported()) {\r\n            options.add(UiOptionItem.from(getContext().getString(R.string.enable_web_proxy),\r\n                    option -> {\r\n                        // Proxy with authentication supported only by OkHttp\r\n                        mPlayerTweaksData.setPlayerDataSource(\r\n                                option.isSelected() ? PlayerTweaksData.PLAYER_DATA_SOURCE_OKHTTP : PlayerTweaksData.PLAYER_DATA_SOURCE_CRONET);\r\n                        mGeneralData.setProxyEnabled(option.isSelected());\r\n                        new WebProxyDialog(getContext()).enable(option.isSelected());\r\n                        if (option.isSelected()) {\r\n                            settingsPresenter.closeDialog();\r\n                        }\r\n                        \r\n                        OkHttpManager.unhold();\r\n                    },\r\n                    mGeneralData.isProxyEnabled()));\r\n        }\r\n    }\r\n\r\n    private void enableChildMode(boolean enable) {\r\n        mGeneralData.setChildModeEnabled(enable);\r\n\r\n        int topButtons = MainUIData.TOP_BUTTON_BROWSE_ACCOUNTS;\r\n        int playerButtons = PlayerTweaksData.PLAYER_BUTTON_PLAY_PAUSE | PlayerTweaksData.PLAYER_BUTTON_NEXT | PlayerTweaksData.PLAYER_BUTTON_PREVIOUS |\r\n                    PlayerTweaksData.PLAYER_BUTTON_DISLIKE | PlayerTweaksData.PLAYER_BUTTON_LIKE | PlayerTweaksData.PLAYER_BUTTON_SCREEN_DIMMING |\r\n                    PlayerTweaksData.PLAYER_BUTTON_SEEK_INTERVAL | PlayerTweaksData.PLAYER_BUTTON_PLAYBACK_QUEUE | PlayerTweaksData.PLAYER_BUTTON_OPEN_CHANNEL |\r\n                    PlayerTweaksData.PLAYER_BUTTON_PIP | PlayerTweaksData.PLAYER_BUTTON_VIDEO_SPEED | PlayerTweaksData.PLAYER_BUTTON_SUBTITLES |\r\n                    PlayerTweaksData.PLAYER_BUTTON_VIDEO_ZOOM | PlayerTweaksData.PLAYER_BUTTON_ADD_TO_PLAYLIST;\r\n        long menuItems = MainUIData.MENU_ITEM_SHOW_QUEUE | MainUIData.MENU_ITEM_ADD_TO_QUEUE | MainUIData.MENU_ITEM_PLAY_NEXT |\r\n                    MainUIData.MENU_ITEM_SELECT_ACCOUNT | MainUIData.MENU_ITEM_STREAM_REMINDER | MainUIData.MENU_ITEM_SAVE_REMOVE_PLAYLIST;\r\n\r\n        PlayerTweaksData tweaksData = PlayerTweaksData.instance(getContext());\r\n        SearchData searchData = SearchData.instance(getContext());\r\n\r\n        // Remove all\r\n        mMainUIData.setTopButtonDisabled(Integer.MAX_VALUE);\r\n        tweaksData.setPlayerButtonDisabled(Integer.MAX_VALUE);\r\n        mMainUIData.setMenuItemDisabled(Integer.MAX_VALUE);\r\n        BrowsePresenter.instance(getContext()).enableAllSections(false);\r\n        searchData.setPopularSearchesDisabled(true);\r\n\r\n        if (enable) {\r\n            // apply child tweaks\r\n            mMainUIData.setTopButtonEnabled(topButtons);\r\n            tweaksData.setPlayerButtonEnabled(playerButtons);\r\n            mMainUIData.setMenuItemEnabled(menuItems);\r\n            mPlayerData.setPlaybackMode(PlayerConstants.PLAYBACK_MODE_LIST);\r\n            BrowsePresenter.instance(getContext()).enableSection(MediaGroup.TYPE_HISTORY, true);\r\n            BrowsePresenter.instance(getContext()).enableSection(MediaGroup.TYPE_USER_PLAYLISTS, true);\r\n            BrowsePresenter.instance(getContext()).enableSection(MediaGroup.TYPE_SUBSCRIPTIONS, true);\r\n            BrowsePresenter.instance(getContext()).enableSection(MediaGroup.TYPE_CHANNEL_UPLOADS, true);\r\n        } else {\r\n            // apply default tweaks\r\n            mMainUIData.setTopButtonEnabled(MainUIData.TOP_BUTTON_DEFAULT);\r\n            tweaksData.setPlayerButtonEnabled(PlayerTweaksData.PLAYER_BUTTON_DEFAULT);\r\n            mMainUIData.setMenuItemEnabled(MainUIData.MENU_ITEM_DEFAULT);\r\n            BrowsePresenter.instance(getContext()).enableAllSections(true);\r\n            tweaksData.setSuggestionsDisabled(false);\r\n            mPlayerData.setPlaybackMode(PlayerConstants.PLAYBACK_MODE_ALL);\r\n            searchData.setPopularSearchesDisabled(false);\r\n        }\r\n    }\r\n\r\n    private void showPasswordDialog(AppDialogPresenter settingsPresenter, Runnable onSuccess) {\r\n        if (mGeneralData.getSettingsPassword() != null) {\r\n            if (onSuccess != null) {\r\n                onSuccess.run();\r\n            }\r\n            return;\r\n        }\r\n\r\n        settingsPresenter.closeDialog();\r\n        SimpleEditDialog.showPassword(\r\n                getContext(),\r\n                getContext().getString(R.string.protect_settings_with_password),\r\n                null,\r\n                newValue -> {\r\n                    mGeneralData.setSettingsPassword(newValue);\r\n                    if (onSuccess != null) {\r\n                        onSuccess.run();\r\n                    }\r\n                    return true;\r\n                });\r\n    }\r\n\r\n    private void showMasterPasswordDialog(AppDialogPresenter settingsPresenter, Runnable onSuccess) {\r\n        if (mGeneralData.getMasterPassword() != null) {\r\n            if (onSuccess != null) {\r\n                onSuccess.run();\r\n            }\r\n            return;\r\n        }\r\n\r\n        settingsPresenter.closeDialog();\r\n        SimpleEditDialog.showPassword(\r\n                getContext(),\r\n                getContext().getString(R.string.enable_master_password),\r\n                null,\r\n                newValue -> {\r\n                    mGeneralData.setMasterPassword(newValue);\r\n                    if (onSuccess != null) {\r\n                        onSuccess.run();\r\n                    }\r\n                    return true;\r\n                });\r\n    }\r\n\r\n    private Map<Long, Integer> getMenuNames() {\r\n        Map<Long, Integer> menuNames = new HashMap<>();\r\n        menuNames.put(MainUIData.MENU_ITEM_EXIT_FROM_PIP, R.string.return_to_background_video);\r\n        menuNames.put(MainUIData.MENU_ITEM_EXCLUDE_FROM_CONTENT_BLOCK, R.string.content_block_exclude_channel);\r\n        menuNames.put(MainUIData.MENU_ITEM_MARK_AS_WATCHED, R.string.mark_as_watched);\r\n        menuNames.put(MainUIData.MENU_ITEM_OPEN_CHANNEL, R.string.open_channel);\r\n        menuNames.put(MainUIData.MENU_ITEM_UPDATE_CHECK, R.string.check_for_updates);\r\n        menuNames.put(MainUIData.MENU_ITEM_CLEAR_HISTORY, R.string.clear_history);\r\n        menuNames.put(MainUIData.MENU_ITEM_TOGGLE_HISTORY, R.string.pause_history);\r\n        menuNames.put(MainUIData.MENU_ITEM_PLAYLIST_ORDER, R.string.playlist_order);\r\n        menuNames.put(MainUIData.MENU_ITEM_PLAY_NEXT, R.string.play_next);\r\n        menuNames.put(MainUIData.MENU_ITEM_ADD_TO_QUEUE, R.string.add_remove_from_playback_queue);\r\n        menuNames.put(MainUIData.MENU_ITEM_SHOW_QUEUE, R.string.action_playback_queue);\r\n        menuNames.put(MainUIData.MENU_ITEM_STREAM_REMINDER, R.string.set_stream_reminder);\r\n        menuNames.put(MainUIData.MENU_ITEM_SUBSCRIBE, R.string.subscribe_unsubscribe_from_channel);\r\n        menuNames.put(MainUIData.MENU_ITEM_SAVE_REMOVE_PLAYLIST, R.string.save_remove_playlist);\r\n        menuNames.put(MainUIData.MENU_ITEM_CREATE_PLAYLIST, R.string.create_playlist);\r\n        menuNames.put(MainUIData.MENU_ITEM_RENAME_PLAYLIST, R.string.rename_playlist);\r\n        menuNames.put(MainUIData.MENU_ITEM_ADD_TO_NEW_PLAYLIST, R.string.add_video_to_new_playlist);\r\n        menuNames.put(MainUIData.MENU_ITEM_ADD_TO_PLAYLIST, R.string.dialog_add_to_playlist);\r\n        menuNames.put(MainUIData.MENU_ITEM_RECENT_PLAYLIST, R.string.add_remove_from_recent_playlist);\r\n        menuNames.put(MainUIData.MENU_ITEM_PLAY_VIDEO, R.string.play_video);\r\n        menuNames.put(MainUIData.MENU_ITEM_PLAY_VIDEO_INCOGNITO, R.string.play_video_incognito);\r\n        menuNames.put(MainUIData.MENU_ITEM_PLAY_FROM_START, R.string.play_from_start);\r\n        menuNames.put(MainUIData.MENU_ITEM_NOT_INTERESTED, R.string.not_interested);\r\n        menuNames.put(MainUIData.MENU_ITEM_NOT_RECOMMEND_CHANNEL, R.string.not_recommend_channel);\r\n        menuNames.put(MainUIData.MENU_ITEM_REMOVE_FROM_HISTORY, R.string.remove_from_history);\r\n        menuNames.put(MainUIData.MENU_ITEM_REMOVE_FROM_SUBSCRIPTIONS, R.string.remove_from_subscriptions);\r\n        menuNames.put(MainUIData.MENU_ITEM_PIN_TO_SIDEBAR, R.string.pin_unpin_from_sidebar);\r\n        menuNames.put(MainUIData.MENU_ITEM_SHARE_LINK, R.string.share_link);\r\n        menuNames.put(MainUIData.MENU_ITEM_SHARE_EMBED_LINK, R.string.share_embed_link);\r\n        menuNames.put(MainUIData.MENU_ITEM_SHARE_QR_LINK, R.string.share_qr_link);\r\n        menuNames.put(MainUIData.MENU_ITEM_SELECT_ACCOUNT, R.string.dialog_account_list);\r\n        menuNames.put(MainUIData.MENU_ITEM_MOVE_SECTION_UP, R.string.move_section_up);\r\n        menuNames.put(MainUIData.MENU_ITEM_MOVE_SECTION_DOWN, R.string.move_section_down);\r\n        menuNames.put(MainUIData.MENU_ITEM_RENAME_SECTION, R.string.rename_section);\r\n        menuNames.put(MainUIData.MENU_ITEM_OPEN_DESCRIPTION, R.string.action_video_info);\r\n        menuNames.put(MainUIData.MENU_ITEM_OPEN_COMMENTS, R.string.open_comments);\r\n        menuNames.put(MainUIData.MENU_ITEM_OPEN_PLAYLIST, R.string.open_playlist);\r\n        menuNames.put(MainUIData.MENU_ITEM_BLOCK_CHANNEL, R.string.dialog_block_channel);\r\n\r\n        for (ContextMenuProvider provider : new ContextMenuManager(getContext()).getProviders()) {\r\n            menuNames.put(provider.getId(), provider.getTitleResId());\r\n        }\r\n\r\n        return menuNames;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/LanguageSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.locale.LocaleUpdater;\r\nimport com.liskovsoft.sharedutils.locale.LocaleUtility;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.LinkedHashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\nimport java.util.Map.Entry;\r\n\r\npublic class LanguageSettingsPresenter extends BasePresenter<Void> {\r\n    private final LocaleUpdater mLangUpdater;\r\n    private boolean mRestartApp;\r\n    private final Runnable mOnFinish = () -> {\r\n        if (mRestartApp) {\r\n            mRestartApp = false;\r\n            MessageHelpers.showLongMessage(getContext(), R.string.msg_restart_app);\r\n        }\r\n    };\r\n\r\n    public LanguageSettingsPresenter(Context context) {\r\n        super(context);\r\n        mLangUpdater = new LocaleUpdater(context);\r\n    }\r\n\r\n    public static LanguageSettingsPresenter instance(Context context) {\r\n        return new LanguageSettingsPresenter(context);\r\n    }\r\n\r\n    public void show() {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendLanguageCategory(settingsPresenter);\r\n        appendCountryCategory(settingsPresenter);\r\n\r\n        settingsPresenter.showDialog(getContext().getString(R.string.settings_language_country), mOnFinish);\r\n    }\r\n\r\n    private void appendLanguageCategory(AppDialogPresenter settingsPresenter) {\r\n        Map<String, String> languages = getSupportedLanguages();\r\n        String language = mLangUpdater.getPreferredLanguage();\r\n        String languageTitle = \"\";\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (Entry<String, String> entry : languages.entrySet()) {\r\n            if (entry.getValue().equals(language)) {\r\n                languageTitle = String.format(\" (%s)\", entry.getKey());\r\n            }\r\n\r\n            options.add(UiOptionItem.from(\r\n                    entry.getKey(),\r\n                    option -> {\r\n                        mLangUpdater.setPreferredLanguage(entry.getValue());\r\n                        mRestartApp = true;\r\n                        settingsPresenter.closeDialog();\r\n                    },\r\n                    entry.getValue().equals(language)));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(\r\n                getContext().getString(R.string.dialog_select_language) + languageTitle, options);\r\n    }\r\n\r\n    private void appendCountryCategory(AppDialogPresenter settingsPresenter) {\r\n        Map<String, String> countries = getSupportedCountries();\r\n        String country = mLangUpdater.getPreferredCountry();\r\n        String countryTitle = \"\";\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (Entry<String, String> entry : countries.entrySet()) {\r\n            if (entry.getValue().equals(country)) {\r\n                countryTitle = String.format(\" (%s)\", entry.getKey());\r\n            }\r\n\r\n            options.add(UiOptionItem.from(\r\n                    entry.getKey(),\r\n                    option -> {\r\n                        mLangUpdater.setPreferredCountry(entry.getValue());\r\n                        mRestartApp = true;\r\n                        settingsPresenter.closeDialog();\r\n                    },\r\n                    entry.getValue().equals(country)));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(\r\n                getContext().getString(R.string.dialog_select_country) + countryTitle, options);\r\n    }\r\n\r\n    /**\r\n     * Gets map of Human readable locale names and their respective lang codes\r\n     * @return locale name/code map\r\n     */\r\n    private Map<String, String> getSupportedLanguages() {\r\n        LinkedHashMap<String, String> map = new LinkedHashMap<>();\r\n        String language = LocaleUtility.getCurrentLocale(getContext()).getDisplayLanguage();\r\n        map.put(getContext().getResources().getString(R.string.default_lang) + \" - \" + language, \"\");\r\n        return Helpers.getMap(Helpers.sortNatural(getContext().getResources().getStringArray(R.array.supported_languages)), \"|\", map);\r\n    }\r\n\r\n    private Map<String, String> getSupportedCountries() {\r\n        LinkedHashMap<String, String> map = new LinkedHashMap<>();\r\n        String country = LocaleUtility.getCurrentLocale(getContext()).getDisplayCountry();\r\n        map.put(getContext().getResources().getString(R.string.default_lang) + \" - \" + country, \"\");\r\n        return Helpers.getMap(Helpers.sortNatural(getContext().getResources().getStringArray(R.array.supported_countries)), \"|\", map);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/MainUISettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.content.Context;\r\nimport android.os.Build;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.BrowsePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.DeArrowData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData.ColorScheme;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.ClickbaitRemover;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class MainUISettingsPresenter extends BasePresenter<Void> {\r\n    private final MainUIData mMainUIData;\r\n    private final GeneralData mGeneralData;\r\n    private final PlayerData mPlayerData;\r\n    private final DeArrowData mDeArrowData;\r\n    private boolean mRestartApp;\r\n    private final Runnable mOnFinish = () -> {\r\n        if (mRestartApp) {\r\n            mRestartApp = false;\r\n            MessageHelpers.showLongMessage(getContext(), R.string.msg_restart_app);\r\n        }\r\n    };\r\n\r\n    private MainUISettingsPresenter(Context context) {\r\n        super(context);\r\n        mMainUIData = MainUIData.instance(context);\r\n        mGeneralData = GeneralData.instance(context);\r\n        mPlayerData = PlayerData.instance(context);\r\n        mDeArrowData = DeArrowData.instance(context);\r\n    }\r\n\r\n    public static MainUISettingsPresenter instance(Context context) {\r\n        return new MainUISettingsPresenter(context);\r\n    }\r\n\r\n    public void show() {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendTopButtonsCategory(settingsPresenter);\r\n        appendColorScheme(settingsPresenter);\r\n        if (Build.VERSION.SDK_INT > 19) {\r\n            appendCardTextScrollSpeed(settingsPresenter);\r\n        }\r\n        appendCardPreviews(settingsPresenter);\r\n        appendCardStyle(settingsPresenter);\r\n        appendThumbSource(settingsPresenter);\r\n        //appendCardTitleLines(settingsPresenter);\r\n        appendChannelSortingCategory(settingsPresenter);\r\n        //appendPlaylistsCategoryStyle(settingsPresenter);\r\n        appendScaleUI(settingsPresenter);\r\n        if (Build.VERSION.SDK_INT > 19) {\r\n            appendVideoGridScale(settingsPresenter);\r\n        }\r\n        //appendTimeFormatCategory(settingsPresenter);\r\n        appendMiscCategory(settingsPresenter);\r\n\r\n        settingsPresenter.showDialog(getContext().getString(R.string.dialog_main_ui), mOnFinish);\r\n    }\r\n\r\n    private void appendTopButtonsCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.settings_search, MainUIData.TOP_BUTTON_SEARCH},\r\n                {R.string.settings_language_country, MainUIData.TOP_BUTTON_CHANGE_LANGUAGE},\r\n                {R.string.settings_accounts, MainUIData.TOP_BUTTON_BROWSE_ACCOUNTS}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]), optionItem -> {\r\n                if (optionItem.isSelected()) {\r\n                    mMainUIData.setTopButtonEnabled(pair[1]);\r\n                } else {\r\n                    mMainUIData.setTopButtonDisabled(pair[1]);\r\n                }\r\n            }, mMainUIData.isTopButtonEnabled(pair[1])));\r\n        }\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.various_buttons), options);\r\n    }\r\n\r\n    private void appendColorScheme(AppDialogPresenter settingsPresenter) {\r\n        List<ColorScheme> colorSchemes = mMainUIData.getColorSchemes();\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.color_scheme), fromColorSchemes(colorSchemes));\r\n    }\r\n\r\n    private List<OptionItem> fromColorSchemes(List<ColorScheme> colorSchemes) {\r\n        List<OptionItem> styleOptions = new ArrayList<>();\r\n\r\n        for (ColorScheme colorScheme : colorSchemes) {\r\n            styleOptions.add(UiOptionItem.from(\r\n                    getContext().getString(colorScheme.nameResId),\r\n                    option -> {\r\n                        mMainUIData.setColorScheme(colorScheme);\r\n                        mRestartApp = true;\r\n                    },\r\n                    colorScheme.equals(mMainUIData.getColorScheme())));\r\n        }\r\n\r\n        return styleOptions;\r\n    }\r\n\r\n    private void appendCardPreviews(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.option_disabled, MainUIData.CARD_PREVIEW_DISABLED},\r\n                {R.string.card_preview_full, MainUIData.CARD_PREVIEW_FULL},\r\n                {R.string.card_preview_muted, MainUIData.CARD_PREVIEW_MUTED}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]), optionItem -> {\r\n                mMainUIData.setCardPreviewType(pair[1]);\r\n            }, mMainUIData.getCardPreviewType() == pair[1]));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.card_preview), options);\r\n    }\r\n\r\n    private void appendCardStyle(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        OptionItem multilineTitle = UiOptionItem.from(getContext().getString(R.string.card_multiline_title),\r\n                option -> mMainUIData.setCardMultilineTitleEnabled(option.isSelected()), mMainUIData.isCardMultilineTitleEnabled());\r\n\r\n        OptionItem multilineSubtitle = UiOptionItem.from(getContext().getString(R.string.card_multiline_subtitle),\r\n                option -> mMainUIData.setCardMultilineSubtitleEnabled(option.isSelected()), mMainUIData.isCardMultilineSubtitleEnabled());\r\n\r\n        OptionItem autoScrolledTitle = UiOptionItem.from(getContext().getString(R.string.card_auto_scrolled_title),\r\n                option -> mMainUIData.setCardTextAutoScrollEnabled(option.isSelected()), mMainUIData.isCardTextAutoScrollEnabled());\r\n\r\n        OptionItem unlocalizedTitle = UiOptionItem.from(getContext().getString(R.string.card_unlocalized_titles),\r\n                option -> mMainUIData.setUnlocalizedTitlesEnabled(option.isSelected()), mMainUIData.isUnlocalizedTitlesEnabled());\r\n        \r\n        options.add(multilineTitle);\r\n        options.add(multilineSubtitle);\r\n        if (Build.VERSION.SDK_INT > 19) {\r\n            options.add(autoScrolledTitle);\r\n        }\r\n        options.add(unlocalizedTitle);\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.cards_style), options);\r\n    }\r\n\r\n    private void appendCardTitleLines(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int linesNum : new int[] {1, 2, 3, 4}) {\r\n            options.add(UiOptionItem.from(String.format(\"%s\", linesNum),\r\n                    optionItem -> mMainUIData.setCartTitleLinesNum(linesNum),\r\n                    linesNum == mMainUIData.getCardTitleLinesNum()));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.card_title_lines_num), options);\r\n    }\r\n\r\n    private void appendThumbSource(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.thumb_quality_default, ClickbaitRemover.THUMB_QUALITY_DEFAULT},\r\n                {R.string.thumb_quality_start, ClickbaitRemover.THUMB_QUALITY_START},\r\n                {R.string.thumb_quality_middle, ClickbaitRemover.THUMB_QUALITY_MIDDLE},\r\n                {R.string.thumb_quality_end, ClickbaitRemover.THUMB_QUALITY_END}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]),\r\n                    optionItem -> mMainUIData.setThumbQuality(pair[1]),\r\n                    mMainUIData.getThumbQuality() == pair[1]\r\n                    ));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.card_content), options);\r\n    }\r\n\r\n    private void appendChannelSortingCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.sorting_last_viewed, MainUIData.CHANNEL_SORTING_LAST_VIEWED},\r\n                {R.string.sorting_alphabetically, MainUIData.CHANNEL_SORTING_NAME},\r\n                {R.string.sorting_by_new_content, MainUIData.CHANNEL_SORTING_NEW_CONTENT}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]), optionItem -> {\r\n                mMainUIData.setChannelCategorySorting(pair[1]);\r\n                BrowsePresenter.instance(getContext()).updateChannelSorting();\r\n            }, mMainUIData.getChannelCategorySorting() == pair[1]));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.channels_section_sorting), options);\r\n    }\r\n\r\n    private void appendPlaylistsCategoryStyle(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.playlists_style_grid, MainUIData.PLAYLISTS_STYLE_GRID},\r\n                {R.string.playlists_style_rows, MainUIData.PLAYLISTS_STYLE_ROWS}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]), optionItem -> {\r\n                mMainUIData.setPlaylistsStyle(pair[1]);\r\n                BrowsePresenter.instance(getContext()).updatePlaylistsStyle();\r\n            }, mMainUIData.getPlaylistsStyle() == pair[1]));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.playlists_style), options);\r\n    }\r\n\r\n    private void appendScaleUI(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (float scale : new float[] {0.4f, 0.5f, 0.6f, 0.65f, 0.7f, 0.75f, 0.8f, 0.85f, 0.9f, 0.95f, 1.0f, 1.05f, 1.1f, 1.15f, 1.2f, 1.25f, 1.3f, 1.35f, 1.4f}) {\r\n            options.add(UiOptionItem.from(String.format(\"%sx\", scale),\r\n                    optionItem -> {\r\n                        mMainUIData.setUIScale(scale);\r\n                        mRestartApp = true;\r\n                    },\r\n                    Helpers.floatEquals(scale, mMainUIData.getUIScale())));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.scale_ui), options);\r\n    }\r\n\r\n    private void appendCardTextScrollSpeed(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (float factor : new float[] {1, 1.5f, 2, 2.5f, 3, 3.5f, 4, 4.5f, 5, 5.5f, 6, 6.5f, 7, 7.5f, 8}) {\r\n            options.add(UiOptionItem.from(String.format(\"%sx\", Helpers.formatFloat(factor)),\r\n                    optionItem -> mMainUIData.setCardTextScrollSpeed(factor),\r\n                    Helpers.floatEquals(factor, mMainUIData.getCardTextScrollSpeed())));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.card_text_scroll_factor), options);\r\n    }\r\n\r\n    private void appendVideoGridScale(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (float scale : new float[] {0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1.0f, 1.1f, 1.2f, 1.25f, 1.3f, 1.35f, 1.4f, 1.5f}) {\r\n            options.add(UiOptionItem.from(String.format(\"%sx\", scale),\r\n                    optionItem -> {\r\n                        mMainUIData.setVideoGridScale(scale);\r\n                        mRestartApp = true;\r\n                    },\r\n                    Helpers.floatEquals(scale, mMainUIData.getVideoGridScale())));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.video_grid_scale), options);\r\n    }\r\n\r\n    private void appendTimeFormatCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(\r\n                getContext().getString(R.string.time_format_24),\r\n                option -> {\r\n                    mGeneralData.set24HourLocaleEnabled(true);\r\n                    mRestartApp = true;\r\n                },\r\n                mGeneralData.is24HourLocaleEnabled()));\r\n\r\n        options.add(UiOptionItem.from(\r\n                getContext().getString(R.string.time_format_12),\r\n                option -> {\r\n                    mGeneralData.set24HourLocaleEnabled(false);\r\n                    mRestartApp = true;\r\n                },\r\n                !mGeneralData.is24HourLocaleEnabled()));\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.time_format), options);\r\n    }\r\n\r\n    private void appendMiscCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.card_unlocalized_titles),\r\n                option -> {\r\n                    mMainUIData.setUnlocalizedTitlesEnabled(option.isSelected());\r\n                    mDeArrowData.setReplaceTitlesEnabled(false);\r\n                },\r\n                mMainUIData.isUnlocalizedTitlesEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.time_format_24) + \" \" + getContext().getString(R.string.time_format),\r\n                option -> {\r\n                    mGeneralData.set24HourLocaleEnabled(option.isSelected());\r\n                    mRestartApp = true;\r\n                },\r\n                mGeneralData.is24HourLocaleEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.app_corner_clock),\r\n                option -> {\r\n                    mGeneralData.setGlobalClockEnabled(option.isSelected());\r\n                    mRestartApp = true;\r\n                },\r\n                mGeneralData.isGlobalClockEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_corner_clock),\r\n                option -> mPlayerData.setGlobalClockEnabled(option.isSelected()),\r\n                mPlayerData.isGlobalClockEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_corner_ending_time),\r\n                option -> mPlayerData.setGlobalEndingTimeEnabled(option.isSelected()),\r\n                mPlayerData.isGlobalEndingTimeEnabled()));\r\n\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.old_home_look),\r\n        //        option -> {\r\n        //            mGeneralData.enableOldHomeLook(option.isSelected());\r\n        //            mRestartApp = true;\r\n        //        },\r\n        //        mGeneralData.isOldHomeLookEnabled()));\r\n\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.old_channel_look),\r\n        //        option -> {\r\n        //            mGeneralData.enableOldChannelLook(option.isSelected());\r\n        //            mMainUIData.enableChannelSearchBar(!option.isSelected());\r\n        //        },\r\n        //        mGeneralData.isOldChannelLookEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.channels_old_look),\r\n                optionItem -> {\r\n                    mMainUIData.setUploadsOldLookEnabled(optionItem.isSelected());\r\n                    mRestartApp = true;\r\n                },\r\n                mMainUIData.isUploadsOldLookEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.fullscreen_mode),\r\n                option -> {\r\n                    mGeneralData.setFullscreenModeEnabled(option.isSelected());\r\n                    mRestartApp = true;\r\n                },\r\n                mGeneralData.isFullscreenModeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.pinned_channel_rows),\r\n                optionItem -> {\r\n                    mMainUIData.setPinnedChannelRowsEnabled(optionItem.isSelected());\r\n                    mRestartApp = true;\r\n                },\r\n                mMainUIData.isPinnedChannelRowsEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.playlists_rows),\r\n                optionItem -> {\r\n                    mMainUIData.setPlaylistsStyle(optionItem.isSelected() ? MainUIData.PLAYLISTS_STYLE_ROWS : MainUIData.PLAYLISTS_STYLE_GRID);\r\n                    BrowsePresenter.instance(getContext()).updatePlaylistsStyle();\r\n                },\r\n                mMainUIData.getPlaylistsStyle() == MainUIData.PLAYLISTS_STYLE_ROWS));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.channels_filter),\r\n                optionItem -> mMainUIData.setChannelsFilterEnabled(optionItem.isSelected()),\r\n                mMainUIData.isChannelsFilterEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.channel_search_bar),\r\n                optionItem -> mMainUIData.setChannelSearchBarEnabled(optionItem.isSelected()),\r\n                mMainUIData.isChannelSearchBarEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.channels_auto_load),\r\n                optionItem -> mMainUIData.setUploadsAutoLoadEnabled(optionItem.isSelected()),\r\n                mMainUIData.isUploadsAutoLoadEnabled()));\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.player_other), options);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/PlayerSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport android.util.Pair;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.prefs.GlobalPreferences;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionCategory;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.service.SidebarService;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.TrackSelectorUtil;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.SearchData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport com.liskovsoft.youtubeapi.service.internal.MediaServiceData;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class PlayerSettingsPresenter extends BasePresenter<Void> {\r\n    private final PlayerData mPlayerData;\r\n    private final PlayerTweaksData mPlayerTweaksData;\r\n    private final SearchData mSearchData;\r\n    private final GeneralData mGeneralData;\r\n    private final SidebarService mSidebarService;\r\n    private final MediaServiceData mMediaServiceData;\r\n    private boolean mRestartApp;\r\n    private final Runnable mOnFinish = () -> {\r\n        if (mRestartApp) {\r\n            mRestartApp = false;\r\n            MessageHelpers.showLongMessage(getContext(), R.string.msg_restart_app);\r\n        }\r\n    };\r\n\r\n    private PlayerSettingsPresenter(Context context) {\r\n        super(context);\r\n        mPlayerData = PlayerData.instance(context);\r\n        mPlayerTweaksData = PlayerTweaksData.instance(context);\r\n        mSearchData = SearchData.instance(context);\r\n        mGeneralData = GeneralData.instance(context);\r\n        mSidebarService = SidebarService.instance(context);\r\n        mMediaServiceData = MediaServiceData.instance();\r\n    }\r\n\r\n    public static PlayerSettingsPresenter instance(Context context) {\r\n        return new PlayerSettingsPresenter(context);\r\n    }\r\n\r\n    public void show() {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendPlaybackModeCategory(settingsPresenter);\r\n        appendVideoPresetsCategory(settingsPresenter);\r\n        appendPlayerButtonsCategory(settingsPresenter);\r\n        appendNetworkEngineCategory(settingsPresenter);\r\n        appendVideoBufferCategory(settingsPresenter);\r\n        appendVideoZoomCategory(settingsPresenter);\r\n        appendVideoSpeedCategory(settingsPresenter);\r\n        appendAudioLanguageCategory(settingsPresenter);\r\n        appendAudioShiftCategory(settingsPresenter);\r\n        appendMasterVolumeCategory(settingsPresenter);\r\n        appendOKButtonCategory(settingsPresenter);\r\n        appendUIAutoHideCategory(settingsPresenter);\r\n        appendSeekTypeCategory(settingsPresenter);\r\n        appendSeekingPreviewCategory(settingsPresenter);\r\n        AppDialogUtil.appendSeekIntervalDialogItems(getContext(), settingsPresenter, mPlayerData, false);\r\n        //appendRememberSpeedCategory(settingsPresenter);\r\n        //appendScreenOffTimeoutCategory(settingsPresenter);\r\n        appendEndingTimeCategory(settingsPresenter);\r\n        appendPixelRatioCategory(settingsPresenter);\r\n        //appendPlayerExitCategory(settingsPresenter);\r\n        appendSleepTimerCategory(settingsPresenter);\r\n        appendMiscCategory(settingsPresenter);\r\n        appendDeveloperCategory(settingsPresenter);\r\n\r\n        settingsPresenter.showDialog(getContext().getString(R.string.settings_player), mOnFinish);\r\n    }\r\n\r\n    private void appendOKButtonCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(\r\n                getContext().getString(R.string.player_only_ui),\r\n                option -> mPlayerData.setOKButtonBehavior(PlayerData.OK_ONLY_UI),\r\n                mPlayerData.getOKButtonBehavior() == PlayerData.OK_ONLY_UI));\r\n\r\n        options.add(UiOptionItem.from(\r\n                getContext().getString(R.string.player_ui_and_pause),\r\n                option -> mPlayerData.setOKButtonBehavior(PlayerData.OK_UI_AND_PAUSE),\r\n                mPlayerData.getOKButtonBehavior() == PlayerData.OK_UI_AND_PAUSE));\r\n\r\n        options.add(UiOptionItem.from(\r\n                getContext().getString(R.string.player_only_pause),\r\n                option -> mPlayerData.setOKButtonBehavior(PlayerData.OK_ONLY_PAUSE),\r\n                mPlayerData.getOKButtonBehavior() == PlayerData.OK_ONLY_PAUSE));\r\n\r\n        options.add(UiOptionItem.from(\r\n                getContext().getString(R.string.player_toggle_speed),\r\n                option -> mPlayerData.setOKButtonBehavior(PlayerData.OK_TOGGLE_SPEED),\r\n                mPlayerData.getOKButtonBehavior() == PlayerData.OK_TOGGLE_SPEED));\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.player_ok_button_behavior), options);\r\n    }\r\n\r\n    @SuppressLint(\"StringFormatMatches\")\r\n    private void appendUIAutoHideCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(\r\n                getContext().getString(R.string.option_never),\r\n                option -> mPlayerData.setUiHideTimeoutSec(PlayerData.AUTO_HIDE_NEVER),\r\n                mPlayerData.getUiHideTimeoutSec() == PlayerData.AUTO_HIDE_NEVER));\r\n\r\n        for (int i = 1; i <= 15; i++) {\r\n            int timeoutSec = i;\r\n            options.add(UiOptionItem.from(\r\n                    getContext().getString(R.string.ui_hide_timeout_sec, i),\r\n                    option -> mPlayerData.setUiHideTimeoutSec(timeoutSec),\r\n                    mPlayerData.getUiHideTimeoutSec() == i));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.player_ui_hide_behavior), options);\r\n    }\r\n\r\n    private void appendVideoBufferCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createVideoBufferCategory(getContext());\r\n        settingsPresenter.appendCategory(category);\r\n    }\r\n\r\n    private void appendVideoPresetsCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createVideoPresetsCategory(getContext());\r\n        settingsPresenter.appendCategory(category);\r\n    }\r\n\r\n    private void appendVideoZoomCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createVideoZoomCategory(getContext());\r\n        settingsPresenter.appendCategory(category);\r\n    }\r\n\r\n    private void appendAudioLanguageCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createAudioLanguageCategory(getContext());\r\n        settingsPresenter.appendCategory(category);\r\n    }\r\n\r\n    private void appendAudioShiftCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createAudioShiftCategory(getContext());\r\n        settingsPresenter.appendCategory(category);\r\n    }\r\n\r\n    private void appendMasterVolumeCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createAudioVolumeCategory(getContext());\r\n        settingsPresenter.appendCategory(category);\r\n    }\r\n\r\n    private void appendSeekingPreviewCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.player_seek_preview_none, PlayerData.SEEK_PREVIEW_NONE},\r\n                {R.string.player_seek_preview_single, PlayerData.SEEK_PREVIEW_SINGLE},\r\n                {R.string.player_seek_preview_carousel_slow, PlayerData.SEEK_PREVIEW_CAROUSEL_SLOW},\r\n                {R.string.player_seek_preview_carousel_fast, PlayerData.SEEK_PREVIEW_CAROUSEL_FAST}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]),\r\n                    optionItem -> mPlayerData.setSeekPreviewMode(pair[1]),\r\n                    mPlayerData.getSeekPreviewMode() == pair[1]));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.player_seek_preview), options);\r\n    }\r\n\r\n    private void appendRememberSpeedCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createRememberSpeedCategory(getContext());\r\n\r\n        settingsPresenter.appendRadioCategory(category.title, category.options);\r\n    }\r\n\r\n    private void appendVideoSpeedCategory(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleButton(UiOptionItem.from(getContext().getString(R.string.video_speed), optionItem -> {\r\n            AppDialogPresenter settingsPresenter2 = AppDialogPresenter.instance(getContext());\r\n            settingsPresenter2.appendCategory(AppDialogUtil.createSpeedListCategory(getContext(), null));\r\n            settingsPresenter2.appendCategory(AppDialogUtil.createRememberSpeedCategory(getContext()));\r\n            settingsPresenter2.appendCategory(AppDialogUtil.createSpeedMiscCategory(getContext()));\r\n            settingsPresenter2.showDialog(getContext().getString(R.string.video_speed));\r\n        }));\r\n    }\r\n\r\n    private void appendScreenOffTimeoutCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createPlayerScreenOffTimeoutCategory(getContext(), null);\r\n\r\n        settingsPresenter.appendRadioCategory(category.title, category.options);\r\n    }\r\n\r\n    private void appendPlayerButtonsCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.auto_frame_rate, PlayerTweaksData.PLAYER_BUTTON_AFR},\r\n                {R.string.action_sound_off, PlayerTweaksData.PLAYER_BUTTON_SOUND_OFF},\r\n                {R.string.video_rotate, PlayerTweaksData.PLAYER_BUTTON_VIDEO_ROTATE},\r\n                {R.string.video_flip, PlayerTweaksData.PLAYER_BUTTON_VIDEO_FLIP},\r\n                {R.string.open_chat, PlayerTweaksData.PLAYER_BUTTON_CHAT},\r\n                {R.string.content_block_provider, PlayerTweaksData.PLAYER_BUTTON_CONTENT_BLOCK},\r\n                {R.string.seek_interval, PlayerTweaksData.PLAYER_BUTTON_SEEK_INTERVAL},\r\n                {R.string.share_link, PlayerTweaksData.PLAYER_BUTTON_SHARE},\r\n                {R.string.action_video_info, PlayerTweaksData.PLAYER_BUTTON_VIDEO_INFO},\r\n                {R.string.action_debug_info, PlayerTweaksData.PLAYER_BUTTON_VIDEO_STATS},\r\n                {R.string.action_playback_queue, PlayerTweaksData.PLAYER_BUTTON_PLAYBACK_QUEUE},\r\n                {R.string.screen_dimming, PlayerTweaksData.PLAYER_BUTTON_SCREEN_DIMMING},\r\n                {R.string.action_video_zoom, PlayerTweaksData.PLAYER_BUTTON_VIDEO_ZOOM},\r\n                {R.string.action_channel, PlayerTweaksData.PLAYER_BUTTON_OPEN_CHANNEL},\r\n                {R.string.action_search, PlayerTweaksData.PLAYER_BUTTON_SEARCH},\r\n                {R.string.run_in_background, PlayerTweaksData.PLAYER_BUTTON_PIP},\r\n                {R.string.action_video_speed, PlayerTweaksData.PLAYER_BUTTON_VIDEO_SPEED},\r\n                {R.string.action_subtitles, PlayerTweaksData.PLAYER_BUTTON_SUBTITLES},\r\n                {R.string.action_subscribe, PlayerTweaksData.PLAYER_BUTTON_SUBSCRIBE},\r\n                {R.string.action_like, PlayerTweaksData.PLAYER_BUTTON_LIKE},\r\n                {R.string.action_dislike, PlayerTweaksData.PLAYER_BUTTON_DISLIKE},\r\n                {R.string.action_playlist_add, PlayerTweaksData.PLAYER_BUTTON_ADD_TO_PLAYLIST},\r\n                {R.string.action_play_pause, PlayerTweaksData.PLAYER_BUTTON_PLAY_PAUSE},\r\n                {R.string.action_repeat_mode, PlayerTweaksData.PLAYER_BUTTON_REPEAT_MODE},\r\n                {R.string.action_next, PlayerTweaksData.PLAYER_BUTTON_NEXT},\r\n                {R.string.action_previous, PlayerTweaksData.PLAYER_BUTTON_PREVIOUS},\r\n                {R.string.playback_settings, PlayerTweaksData.PLAYER_BUTTON_HIGH_QUALITY}\r\n        }) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]), optionItem -> {\r\n                if (optionItem.isSelected()) {\r\n                    mPlayerTweaksData.setPlayerButtonEnabled(pair[1]);\r\n                } else {\r\n                    mPlayerTweaksData.setPlayerButtonDisabled(pair[1]);\r\n                }\r\n            }, mPlayerTweaksData.isPlayerButtonEnabled(pair[1])));\r\n        }\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.player_buttons), options);\r\n    }\r\n\r\n    private void appendSeekTypeCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_seek_regular),\r\n                option -> {\r\n                    mPlayerData.setSeekConfirmPauseEnabled(false);\r\n                    mPlayerData.setSeekConfirmPlayEnabled(false);\r\n                },\r\n                !mPlayerData.isSeekConfirmPauseEnabled() && !mPlayerData.isSeekConfirmPlayEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_seek_confirmation_pause),\r\n                option -> {\r\n                    mPlayerData.setSeekConfirmPauseEnabled(true);\r\n                    mPlayerData.setSeekConfirmPlayEnabled(false);\r\n                },\r\n                mPlayerData.isSeekConfirmPauseEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_seek_confirmation_play),\r\n                option -> {\r\n                    mPlayerData.setSeekConfirmPauseEnabled(false);\r\n                    mPlayerData.setSeekConfirmPlayEnabled(true);\r\n                },\r\n                mPlayerData.isSeekConfirmPlayEnabled()));\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.player_seek_type), options);\r\n    }\r\n\r\n    private void appendEndingTimeCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.option_disabled),\r\n                option -> {\r\n                    mPlayerData.setRemainingTimeEnabled(false);\r\n                    mPlayerData.setEndingTimeEnabled(false);\r\n                },\r\n                !mPlayerData.isRemainingTimeEnabled() && !mPlayerData.isEndingTimeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_show_remaining_time),\r\n                option -> {\r\n                    mPlayerData.setRemainingTimeEnabled(true);\r\n                    mPlayerData.setEndingTimeEnabled(false);\r\n                },\r\n                mPlayerData.isRemainingTimeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_show_ending_time),\r\n                option -> {\r\n                    mPlayerData.setEndingTimeEnabled(true);\r\n                    mPlayerData.setRemainingTimeEnabled(false);\r\n                },\r\n                mPlayerData.isEndingTimeEnabled()));\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.player_show_ending_time), options);\r\n    }\r\n\r\n    private void appendPixelRatioCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        ArrayList<Pair<String, Float>> pairs = new ArrayList<>();\r\n        pairs.add(new Pair<>(\"1:1 (16:9 display)\", 1.0f));\r\n        pairs.add(new Pair<>(\"1.11111:1 (16:10 display)\", 1.11111f));\r\n        pairs.add(new Pair<>(\"1.3333:1 (4:3 display)\", 1.3333f));\r\n        // There is no display with exact 21:9 proportion???\r\n        //pairs.add(new Pair<>(\"0.7619:1 (21:9 display)\", 0.7619f));\r\n        pairs.add(new Pair<>(\"0.75:1 (64:27 display)\", 0.75f));\r\n        pairs.add(new Pair<>(\"0.7442:1 (43:18 display)\", 0.7442f));\r\n        pairs.add(new Pair<>(\"0.7407:1 (12:5 display)\", 0.7407f));\r\n\r\n        for (Pair<String, Float> pair : pairs) {\r\n            options.add(UiOptionItem.from(pair.first,\r\n                    optionItem -> mPlayerTweaksData.setPixelRatio(pair.second),\r\n                    Helpers.floatEquals(mPlayerTweaksData.getPixelRatio(), pair.second)));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.player_pixel_ratio), options);\r\n    }\r\n\r\n    private void appendPlaybackModeCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createPlaybackModeCategory(getContext());\r\n        settingsPresenter.appendCategory(category);\r\n    }\r\n\r\n    private void appendNetworkEngineCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createNetworkEngineCategory(getContext());\r\n        settingsPresenter.appendCategory(category);\r\n    }\r\n\r\n    private void appendPlayerExitCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.app_double_back_exit, GeneralData.EXIT_DOUBLE_BACK},\r\n                {R.string.app_single_back_exit, GeneralData.EXIT_SINGLE_BACK}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]),\r\n                    optionItem -> mGeneralData.setPlayerExitShortcut(pair[1]),\r\n                    mGeneralData.getPlayerExitShortcut() == pair[1]));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.player_exit_shortcut), options);\r\n    }\r\n\r\n    private void appendSleepTimerCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createSleepTimerCategory(getContext());\r\n        settingsPresenter.appendCategory(category);\r\n    }\r\n\r\n    private void appendMiscCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.suggestions_horizontally_scrolled),\r\n                option -> mPlayerTweaksData.setSuggestionsHorizontallyScrolled(option.isSelected()),\r\n                mPlayerTweaksData.isSuggestionsHorizontallyScrolled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.dont_resize_video_to_fit_dialog),\r\n                option -> mPlayerTweaksData.setDontResizeVideoToFitDialogEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isDontResizeVideoToFitDialogEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_audio_focus),\r\n                option -> mPlayerTweaksData.setAudioFocusEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isAudioFocusEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_auto_volume),\r\n                option -> mPlayerTweaksData.setPlayerAutoVolumeEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isPlayerAutoVolumeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_time_stretching),\r\n                getContext().getString(R.string.player_time_stretching_desc),\r\n                option -> mPlayerTweaksData.setAudioTimeStretchingEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isAudioTimeStretchingEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_section_playlist),\r\n                option -> mPlayerTweaksData.setSectionPlaylistEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isSectionPlaylistEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_chapter_notification2),\r\n                option -> mPlayerTweaksData.setChapterNotificationEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isChapterNotificationEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.search_background_playback),\r\n                option -> mSearchData.setTempBackgroundModeEnabled(option.isSelected()),\r\n                mSearchData.isTempBackgroundModeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_exit_shortcut) + \": \" + getContext().getString(R.string.app_double_back_exit),\r\n                option -> mGeneralData.setPlayerExitShortcut(option.isSelected() ? GeneralData.EXIT_DOUBLE_BACK : GeneralData.EXIT_SINGLE_BACK),\r\n                mGeneralData.getPlayerExitShortcut() == GeneralData.EXIT_DOUBLE_BACK));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_loop_shorts),\r\n                option -> mPlayerTweaksData.setLoopShortsEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isLoopShortsEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.place_chat_left),\r\n                option -> mPlayerTweaksData.setChatPlacedLeft(option.isSelected()),\r\n                mPlayerTweaksData.isChatPlacedLeft()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.place_comments_left),\r\n                option -> mPlayerTweaksData.setCommentsPlacedLeft(option.isSelected()),\r\n                mPlayerTweaksData.isCommentsPlacedLeft()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_disable_suggestions),\r\n                option -> mPlayerTweaksData.setSuggestionsDisabled(option.isSelected()),\r\n                mPlayerTweaksData.isSuggestionsDisabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_number_key_seek),\r\n                option -> mPlayerData.setNumberKeySeekEnabled(option.isSelected()),\r\n                mPlayerData.isNumberKeySeekEnabled()));\r\n\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.app_corner_clock),\r\n        //        option -> {\r\n        //            mGeneralData.enableGlobalClock(option.isSelected());\r\n        //            mRestartApp = true;\r\n        //        },\r\n        //        mGeneralData.isGlobalClockEnabled()));\r\n        //\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.player_corner_clock),\r\n        //        option -> mPlayerData.enableGlobalClock(option.isSelected()),\r\n        //        mPlayerData.isGlobalClockEnabled()));\r\n        //\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.player_corner_ending_time),\r\n        //        option -> mPlayerData.enableGlobalEndingTime(option.isSelected()),\r\n        //        mPlayerData.isGlobalEndingTimeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.remember_position_of_live_videos),\r\n                option -> mPlayerTweaksData.setRememberPositionOfLiveVideosEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isRememberPositionOfLiveVideosEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.remember_position_of_short_videos),\r\n                option -> mPlayerTweaksData.setRememberPositionOfShortVideosEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isRememberPositionOfShortVideosEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_speed_button_old_behavior),\r\n                option -> mPlayerTweaksData.setSpeedButtonOldBehaviorEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isSpeedButtonOldBehaviorEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_show_clock),\r\n                option -> mPlayerData.setClockEnabled(option.isSelected()),\r\n                mPlayerData.isClockEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_show_quality_info),\r\n                option -> mPlayerData.setQualityInfoEnabled(option.isSelected()),\r\n                mPlayerData.isQualityInfoEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_show_quality_info_bitrate),\r\n                option -> mPlayerTweaksData.setQualityInfoBitrateEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isQualityInfoBitrateEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_global_focus),\r\n                getContext().getString(R.string.player_global_focus_desc),\r\n                option -> mPlayerTweaksData.setSyncRowButtonIndexEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isSyncRowButtonIndexEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_ui_on_next),\r\n                option -> mPlayerTweaksData.setPlayerUiOnNextEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isPlayerUiOnNextEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_ui_animations),\r\n                option -> mPlayerTweaksData.setUIAnimationsEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isUIAnimationsEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_likes_count),\r\n                option -> mPlayerTweaksData.setLikesCounterEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isLikesCounterEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_show_tooltips),\r\n                option -> mPlayerData.setTooltipsEnabled(option.isSelected()),\r\n                mPlayerData.isTooltipsEnabled()));\r\n\r\n        // See: Utils.updateTooltip\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.player_show_tooltips) + \": \" + getContext().getString(R.string.long_press_for_options),\r\n        //        option -> {\r\n        //            mGeneralData.enableFirstUseTooltip(option.isSelected());\r\n        //            mRestartApp = true;\r\n        //        },\r\n        //        mGeneralData.isFirstUseTooltipEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.player_button_long_click),\r\n                option -> mPlayerTweaksData.setButtonLongClickEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isButtonLongClickEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.real_channel_icon),\r\n                option -> mPlayerTweaksData.setRealChannelIconEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isRealChannelIconEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.queue_respects_playback_mode),\r\n                option -> mPlayerTweaksData.setQueueRespectsPlaybackMode(option.isSelected()),\r\n                mPlayerTweaksData.isQueueRespectsPlaybackMode()));\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.player_other), options);\r\n    }\r\n\r\n    private void appendDeveloperCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.playback_notifications_fix),\r\n                getContext().getString(R.string.playback_notifications_fix_desc),\r\n                option -> mPlayerTweaksData.setPlaybackNotificationsDisabled(option.isSelected()),\r\n                mPlayerTweaksData.isPlaybackNotificationsDisabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.disable_network_error_fixing),\r\n                getContext().getString(R.string.disable_network_error_fixing_desc),\r\n                option -> mPlayerTweaksData.setNetworkErrorFixingDisabled(option.isSelected()),\r\n                mPlayerTweaksData.isNetworkErrorFixingDisabled()));\r\n\r\n        // Oculus Quest fix: back button not closing the activity\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.oculus_quest_fix),\r\n                option -> {\r\n                    mPlayerTweaksData.setOculusQuestFixEnabled(option.isSelected());\r\n                    mRestartApp = true;\r\n                },\r\n                mPlayerTweaksData.isOculusQuestFixEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.prefer_google_dns),\r\n                getContext().getString(R.string.prefer_ipv4_desc),\r\n                option -> {\r\n                    mPlayerTweaksData.setPreferredDnsType(option.isSelected() ? PlayerTweaksData.DNS_TYPE_GOOGLE : PlayerTweaksData.DNS_TYPE_SYSTEM);\r\n                    mRestartApp = true;\r\n                },\r\n                mPlayerTweaksData.getPreferredDnsType() == PlayerTweaksData.DNS_TYPE_GOOGLE));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.prefer_ipv4),\r\n                getContext().getString(R.string.prefer_ipv4_desc),\r\n                option -> {\r\n                    mPlayerTweaksData.setPreferredDnsType(option.isSelected() ? PlayerTweaksData.DNS_TYPE_IPV4 : PlayerTweaksData.DNS_TYPE_SYSTEM);\r\n                    mRestartApp = true;\r\n                },\r\n                mPlayerTweaksData.getPreferredDnsType() == PlayerTweaksData.DNS_TYPE_IPV4));\r\n\r\n        // Disable long press on buggy controllers.\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.disable_ok_long_press),\r\n                getContext().getString(R.string.disable_ok_long_press_desc),\r\n                option -> mGeneralData.setOkButtonLongPressDisabled(option.isSelected()),\r\n                mGeneralData.isOkButtonLongPressDisabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.audio_sync_fix),\r\n                getContext().getString(R.string.audio_sync_fix_desc),\r\n                option -> mPlayerTweaksData.setAudioSyncFixEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isAudioSyncFixEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.ambilight_ratio_fix),\r\n                getContext().getString(R.string.ambilight_ratio_fix_desc),\r\n                option -> {\r\n                    mPlayerTweaksData.setTextureViewEnabled(option.isSelected());\r\n                    if (option.isSelected()) {\r\n                        // Tunneled playback works only with SurfaceView\r\n                        mPlayerTweaksData.setTunneledPlaybackEnabled(false);\r\n                    }\r\n                },\r\n                mPlayerTweaksData.isTextureViewEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.unlock_high_bitrate_formats) + \" \" + TrackSelectorUtil.HIGH_BITRATE_MARK,\r\n                option -> mPlayerTweaksData.setHighBitrateFormatsEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isHighBitrateFormatsEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.unlock_high_bitrate_audio_formats),\r\n                option -> mPlayerTweaksData.setUnsafeAudioFormatsEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isUnsafeAudioFormatsEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.force_legacy_codecs),\r\n                getContext().getString(R.string.force_legacy_codecs_desc),\r\n                option -> mPlayerData.setLegacyCodecsForced(option.isSelected()),\r\n                mPlayerData.isLegacyCodecsForced()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.live_stream_fix),\r\n                getContext().getString(R.string.live_stream_fix_desc),\r\n                option -> {\r\n                    mPlayerTweaksData.setHlsStreamsForced(option.isSelected());\r\n                },\r\n                mPlayerTweaksData.isHlsStreamsForced()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.live_stream_fix_4k),\r\n                getContext().getString(R.string.live_stream_fix_4k_desc),\r\n                option -> {\r\n                    mPlayerTweaksData.setDashUrlStreamsForced(option.isSelected());\r\n                },\r\n                mPlayerTweaksData.isDashUrlStreamsForced()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.disable_stream_buffer),\r\n                getContext().getString(R.string.disable_stream_buffer_desc),\r\n                option -> mPlayerTweaksData.setBufferOnStreamsDisabled(option.isSelected()),\r\n                mPlayerTweaksData.isBufferOnStreamsDisabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.unlock_all_formats),\r\n                getContext().getString(R.string.unlock_all_formats_desc),\r\n                option -> mPlayerTweaksData.setAllFormatsUnlocked(option.isSelected()),\r\n                mPlayerTweaksData.isAllFormatsUnlocked()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.alt_presets_behavior),\r\n                getContext().getString(R.string.alt_presets_behavior_desc),\r\n                option -> mPlayerTweaksData.setNoFpsPresetsEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isNoFpsPresetsEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.prefer_avc_over_vp9),\r\n                getContext().getString(R.string.prefer_avc_over_vp9_desc),\r\n                option -> mPlayerTweaksData.setAvcOverVp9Preferred(option.isSelected()),\r\n                mPlayerTweaksData.isAvcOverVp9Preferred()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.amlogic_fix),\r\n                getContext().getString(R.string.amlogic_fix_desc),\r\n                option -> mPlayerTweaksData.setAmlogicFixEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isAmlogicFixEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.tunneled_video_playback),\r\n                getContext().getString(R.string.tunneled_video_playback_desc),\r\n                option -> {\r\n                    mPlayerTweaksData.setTunneledPlaybackEnabled(option.isSelected());\r\n                    if (option.isSelected()) {\r\n                        // Tunneled playback works only with SurfaceView\r\n                        mPlayerTweaksData.setTextureViewEnabled(false);\r\n                    }\r\n                },\r\n                mPlayerTweaksData.isTunneledPlaybackEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.disable_vsync),\r\n                getContext().getString(R.string.disable_vsync_desc),\r\n                option -> mPlayerTweaksData.setSnappingToVsyncDisabled(option.isSelected()),\r\n                mPlayerTweaksData.isSnappingToVsyncDisabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.skip_codec_profile_check),\r\n                getContext().getString(R.string.skip_codec_profile_check_desc),\r\n                option -> mPlayerTweaksData.setProfileLevelCheckSkipped(option.isSelected()),\r\n                mPlayerTweaksData.isProfileLevelCheckSkipped()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.force_sw_codec),\r\n                getContext().getString(R.string.force_sw_codec_desc),\r\n                option -> mPlayerTweaksData.setSWDecoderForced(option.isSelected()),\r\n                mPlayerTweaksData.isSWDecoderForced()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.sony_frame_drop_fix),\r\n                getContext().getString(R.string.sony_frame_drop_fix_desc),\r\n                option -> mPlayerTweaksData.setSonyFrameDropFixEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isSonyFrameDropFixEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.amazon_frame_drop_fix),\r\n                getContext().getString(R.string.amazon_frame_drop_fix_desc),\r\n                option -> mPlayerTweaksData.setAmazonFrameDropFixEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isAmazonFrameDropFixEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.keep_finished_activities),\r\n                option -> mPlayerTweaksData.setKeepFinishedActivityEnabled(option.isSelected()),\r\n                mPlayerTweaksData.isKeepFinishedActivityEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.disable_channels_service),\r\n                option -> GlobalPreferences.instance(getContext()).setChannelsServiceEnabled(!option.isSelected()),\r\n                !GlobalPreferences.instance(getContext()).isChannelsServiceEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.hide_settings_section),\r\n                option -> {\r\n                    mSidebarService.enableSettingsSection(!option.isSelected());\r\n                    mRestartApp = true;\r\n                },\r\n                !mSidebarService.isSettingsSectionEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.fix_empty_subs_and_channels),\r\n                option -> mMediaServiceData.setLegacyUIEnabled(option.isSelected()),\r\n                mMediaServiceData.isLegacyUIEnabled()));\r\n\r\n        // Disabled inside RetrofitHelper\r\n        //options.add(UiOptionItem.from(\"Prefer IPv4 DNS\",\r\n        //        option -> GlobalPreferences.instance(getContext()).preferIPv4Dns(option.isSelected()),\r\n        //        GlobalPreferences.instance(getContext()).isIPv4DnsPreferred()));\r\n        //\r\n        //options.add(UiOptionItem.from(\"Enable DNS over HTTPS\",\r\n        //        option -> GlobalPreferences.instance(getContext()).enableDnsOverHttps(option.isSelected()),\r\n        //        GlobalPreferences.instance(getContext()).isDnsOverHttpsEnabled()));\r\n\r\n        // Need to be enabled on older version of ExoPlayer (e.g. 2.10.6).\r\n        // It's because there's no tweaks for modern devices.\r\n        //options.add(UiOptionItem.from(\"Enable set output surface workaround\",\r\n        //        option -> mPlayerTweaksData.enableSetOutputSurfaceWorkaround(option.isSelected()),\r\n        //        mPlayerTweaksData.isSetOutputSurfaceWorkaroundEnabled()));\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.player_tweaks), options);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/RemoteControlSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.mediaserviceinterfaces.RemoteControlService;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.PermissionHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AddDevicePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MotherActivity;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.RemoteControlData;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class RemoteControlSettingsPresenter extends BasePresenter<Void> {\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static RemoteControlSettingsPresenter sInstance;\r\n    private final RemoteControlData mRemoteControlData;\r\n    private final RemoteControlService mRemoteManager;\r\n\r\n    public RemoteControlSettingsPresenter(Context context) {\r\n        super(context);\r\n        ServiceManager service = YouTubeServiceManager.instance();\r\n        mRemoteManager = service.getRemoteControlService();\r\n        mRemoteControlData = RemoteControlData.instance(context);\r\n    }\r\n\r\n    public static RemoteControlSettingsPresenter instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new RemoteControlSettingsPresenter(context);\r\n        }\r\n\r\n        sInstance.setContext(context);\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void unhold() {\r\n        sInstance = null;\r\n    }\r\n\r\n    public void show() {\r\n        createAndShowDialog();\r\n    }\r\n\r\n    private void createAndShowDialog() {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendDeviceLinkSwitch(settingsPresenter);\r\n        appendAddDeviceButton(settingsPresenter);\r\n        appendRemoveAllDevicesButton(settingsPresenter);\r\n        appendMiscCategory(settingsPresenter);\r\n\r\n        settingsPresenter.showDialog(getContext().getString(R.string.settings_remote_control), this::unhold);\r\n    }\r\n\r\n    private void appendDeviceLinkSwitch(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleSwitch(UiOptionItem.from(getContext().getString(R.string.settings_remote_control), optionItem -> {\r\n            // Remote link depends on background service\r\n            mRemoteControlData.enableDeviceLink(optionItem.isSelected());\r\n            Utils.updateRemoteControlService(getContext());\r\n\r\n            // Background playback on Android 10 and above\r\n            // Shows overlay dialog if needed (alive activity required)\r\n            if (optionItem.isSelected() && !PermissionHelpers.hasOverlayPermissions(getContext())) {\r\n                AppDialogUtil.showConfirmationDialog(\r\n                        getContext(), getContext().getString(R.string.remote_control_permission),\r\n                        () -> PermissionHelpers.verifyOverlayPermissions(getContext())\r\n                );\r\n            }\r\n        }, mRemoteControlData.isDeviceLinkEnabled()));\r\n    }\r\n\r\n    private void appendAddDeviceButton(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleButton(UiOptionItem.from(\r\n                getContext().getString(R.string.dialog_add_device), option -> {\r\n                    mRemoteControlData.enableDeviceLink(true);\r\n                    Utils.updateRemoteControlService(getContext());\r\n\r\n                    // Background playback on Android 10 and above\r\n                    // Shows overlay dialog if needed (alive activity required)\r\n                    if (!PermissionHelpers.hasOverlayPermissions(getContext())) {\r\n                        AppDialogUtil.showConfirmationDialog(\r\n                                getContext(), getContext().getString(R.string.remote_control_permission), () -> {\r\n                                    PermissionHelpers.verifyOverlayPermissions(getContext());\r\n                                    // Service that prevents the app from destroying\r\n                                    if (getContext() instanceof MotherActivity) {\r\n                                        ((MotherActivity) getContext()).addOnResult(\r\n                                                (request, response, data) -> AddDevicePresenter.instance(getContext()).start()\r\n                                        );\r\n                                    }\r\n                                }, () -> AddDevicePresenter.instance(getContext()).start()\r\n                        );\r\n                    } else {\r\n                        AddDevicePresenter.instance(getContext()).start();\r\n                    }\r\n                }));\r\n    }\r\n\r\n    private void appendRemoveAllDevicesButton(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        OptionItem confirmItem = UiOptionItem.from(\r\n                getContext().getString(R.string.btn_confirm), option -> {\r\n                    RxHelper.execute(mRemoteManager.resetDataObserve());\r\n                    MessageHelpers.showMessage(getContext(), R.string.msg_done);\r\n                    settingsPresenter.closeDialog();\r\n\r\n                    mRemoteControlData.enableDeviceLink(false);\r\n                    Utils.updateRemoteControlService(getContext());\r\n                }\r\n        );\r\n\r\n        options.add(confirmItem);\r\n\r\n        settingsPresenter.appendStringsCategory(getContext().getString(R.string.dialog_remove_all_devices), options);\r\n    }\r\n\r\n    private void appendMiscCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.finish_on_disconnect),\r\n                option -> mRemoteControlData.enableFinishOnDisconnect(option.isSelected()),\r\n                mRemoteControlData.isFinishOnDisconnectEnabled()));\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.show_connect_messages),\r\n        //        option -> mRemoteControlData.enableConnectMessages(option.isSelected()),\r\n        //        mRemoteControlData.isConnectMessagesEnabled()));\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.disable_remote_history),\r\n                option -> mRemoteControlData.disableRemoteHistory(option.isSelected()),\r\n                mRemoteControlData.isRemoteHistoryDisabled()));\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.player_other), options);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/SearchSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.SearchData;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class SearchSettingsPresenter extends BasePresenter<Void> {\r\n    private final SearchData mSearchData;\r\n    private final GeneralData mGeneralData;\r\n\r\n    public SearchSettingsPresenter(Context context) {\r\n        super(context);\r\n        mSearchData = SearchData.instance(context);\r\n        mGeneralData = GeneralData.instance(context);\r\n    }\r\n\r\n    public static SearchSettingsPresenter instance(Context context) {\r\n        return new SearchSettingsPresenter(context);\r\n    }\r\n\r\n    public void show() {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendSpeechRecognizerCategory(settingsPresenter);\r\n        appendMiscCategory(settingsPresenter);\r\n\r\n        settingsPresenter.showDialog(getContext().getString(R.string.dialog_search), () -> {\r\n            if (mSearchData.isSearchHistoryDisabled()) {\r\n                MediaServiceManager.instance().clearSearchHistory();\r\n            }\r\n        });\r\n    }\r\n\r\n    private void appendSpeechRecognizerCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.speech_recognizer_system, SearchData.SPEECH_RECOGNIZER_SYSTEM},\r\n                {R.string.speech_recognizer_external_1, SearchData.SPEECH_RECOGNIZER_INTENT},\r\n                {R.string.speech_recognizer_external_2, SearchData.SPEECH_RECOGNIZER_GOTEV}}) {\r\n            options.add(UiOptionItem.from(getContext().getString(pair[0]),\r\n                    optionItem -> mSearchData.setSpeechRecognizerType(pair[1]),\r\n                    mSearchData.getSpeechRecognizerType() == pair[1]));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.speech_engine), options);\r\n    }\r\n\r\n    private void appendMiscCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        //options.add(UiOptionItem.from(getContext().getString(R.string.disable_popular_searches),\r\n        //        option -> mSearchData.disablePopularSearches(option.isSelected()),\r\n        //        mSearchData.isPopularSearchesDisabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.typing_corrections),\r\n                option -> mSearchData.setTypingCorrectionDisabled(option.isSelected()),\r\n                mSearchData.isTypingCorrectionDisabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.search_exit_shortcut) + \": \" + getContext().getString(R.string.app_double_back_exit),\r\n                option -> mGeneralData.setSearchExitShortcut(option.isSelected() ? GeneralData.EXIT_DOUBLE_BACK : GeneralData.EXIT_SINGLE_BACK),\r\n                mGeneralData.getSearchExitShortcut() == GeneralData.EXIT_DOUBLE_BACK));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.disable_search_history),\r\n                option -> mSearchData.setSearchHistoryDisabled(option.isSelected()),\r\n                mSearchData.isSearchHistoryDisabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.search_background_playback),\r\n                option -> mSearchData.setTempBackgroundModeEnabled(option.isSelected()),\r\n                mSearchData.isTempBackgroundModeEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.instant_voice_search),\r\n                option -> mSearchData.setInstantVoiceSearchEnabled(option.isSelected()),\r\n                mSearchData.isInstantVoiceSearchEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.focus_on_search_results),\r\n                option -> mSearchData.setFocusOnResultsEnabled(option.isSelected()),\r\n                mSearchData.isFocusOnResultsEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.keyboard_auto_show),\r\n                option -> mSearchData.setKeyboardAutoShowEnabled(option.isSelected()),\r\n                mSearchData.isKeyboardAutoShowEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.keyboard_fix),\r\n                option -> mSearchData.setKeyboardFixEnabled(option.isSelected()),\r\n                mSearchData.isKeyboardFixEnabled()));\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.player_other), options);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/SponsorBlockSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.content.Context;\r\nimport android.text.TextUtils;\r\nimport androidx.core.content.ContextCompat;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.SponsorBlockController.SegmentAction;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionCategory;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.PlaybackView;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.SponsorBlockData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\nimport java.util.Set;\r\n\r\npublic class SponsorBlockSettingsPresenter extends BasePresenter<Void> {\r\n    private final SponsorBlockData mContentBlockData;\r\n\r\n    public SponsorBlockSettingsPresenter(Context context) {\r\n        super(context);\r\n        mContentBlockData = SponsorBlockData.instance(context);\r\n    }\r\n\r\n    public static SponsorBlockSettingsPresenter instance(Context context) {\r\n        return new SponsorBlockSettingsPresenter(context);\r\n    }\r\n\r\n    public void show(Runnable onFinish) {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendSponsorBlockSwitch(settingsPresenter);\r\n        appendExcludeChannelButton(settingsPresenter);\r\n        appendActionsCategory(settingsPresenter);\r\n        appendColorMarkersCategory(settingsPresenter);\r\n        appendIgnoreShortSegmentsCategory(settingsPresenter);\r\n        appendMiscCategory(settingsPresenter);\r\n        appendLinks(settingsPresenter);\r\n\r\n        settingsPresenter.showDialog(getContext().getString(R.string.content_block_provider), onFinish);\r\n    }\r\n\r\n    public void show() {\r\n        show(null);\r\n    }\r\n\r\n    private void appendSponsorBlockSwitch(AppDialogPresenter settingsPresenter) {\r\n        Video video = null;\r\n\r\n        if (getViewManager().getTopView() == PlaybackView.class) {\r\n            video = PlaybackPresenter.instance(getContext()).getVideo();\r\n        }\r\n\r\n        final String channelId = video != null ? video.channelId : null;\r\n        boolean isChannelExcluded = SponsorBlockData.instance(getContext()).isChannelExcluded(channelId);\r\n\r\n        OptionItem sponsorBlockOption = UiOptionItem.from(getContext().getString(R.string.enable),\r\n                option -> {\r\n                    mContentBlockData.setSponsorBlockEnabled(option.isSelected());\r\n                    SponsorBlockData.instance(getContext()).stopExcludingChannel(channelId);\r\n                },\r\n                !isChannelExcluded && mContentBlockData.isSponsorBlockEnabled()\r\n        );\r\n\r\n        settingsPresenter.appendSingleSwitch(sponsorBlockOption);\r\n    }\r\n\r\n    private void appendActionsCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        Set<SegmentAction> actions = mContentBlockData.getActions();\r\n\r\n        for (SegmentAction action : actions) {\r\n            options.add(UiOptionItem.from(\r\n                    getColoredString(mContentBlockData.getLocalizedRes(action.segmentCategory), mContentBlockData.getColorRes(action.segmentCategory)),\r\n                    optionItem -> {\r\n                        AppDialogPresenter dialogPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n                        List<OptionItem> nestedOptions = new ArrayList<>();\r\n                        nestedOptions.add(UiOptionItem.from(getContext().getString(R.string.content_block_action_none),\r\n                                optionItem1 -> mContentBlockData.setAction(action.segmentCategory, SponsorBlockData.ACTION_DO_NOTHING),\r\n                                action.actionType == SponsorBlockData.ACTION_DO_NOTHING));\r\n                        nestedOptions.add(UiOptionItem.from(getContext().getString(R.string.content_block_action_only_skip),\r\n                                optionItem1 -> mContentBlockData.setAction(action.segmentCategory, SponsorBlockData.ACTION_SKIP_ONLY),\r\n                                action.actionType == SponsorBlockData.ACTION_SKIP_ONLY));\r\n                        nestedOptions.add(UiOptionItem.from(getContext().getString(R.string.content_block_action_toast),\r\n                                optionItem1 -> mContentBlockData.setAction(action.segmentCategory, SponsorBlockData.ACTION_SKIP_WITH_TOAST),\r\n                                action.actionType == SponsorBlockData.ACTION_SKIP_WITH_TOAST));\r\n                        nestedOptions.add(UiOptionItem.from(getContext().getString(R.string.content_block_action_dialog),\r\n                                optionItem1 -> mContentBlockData.setAction(action.segmentCategory, SponsorBlockData.ACTION_SHOW_DIALOG),\r\n                                action.actionType == SponsorBlockData.ACTION_SHOW_DIALOG));\r\n\r\n                        String title = getContext().getString(mContentBlockData.getLocalizedRes(action.segmentCategory));\r\n\r\n                        dialogPresenter.appendRadioCategory(title, nestedOptions);\r\n                        dialogPresenter.showDialog(title);\r\n                    }));\r\n        }\r\n\r\n        settingsPresenter.appendStringsCategory(getContext().getString(R.string.content_block_action_type), options);\r\n    }\r\n\r\n    private void appendColorMarkersCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (String segmentCategory : mContentBlockData.getAllCategories()) {\r\n            options.add(UiOptionItem.from(getColoredString(mContentBlockData.getLocalizedRes(segmentCategory), mContentBlockData.getColorRes(segmentCategory)),\r\n                    optionItem -> {\r\n                        if (optionItem.isSelected()) {\r\n                            mContentBlockData.enableColorMarker(segmentCategory);\r\n                        } else {\r\n                            mContentBlockData.disableColorMarker(segmentCategory);\r\n                        }\r\n                    },\r\n                    mContentBlockData.isColorMarkerEnabled(segmentCategory)));\r\n        }\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.sponsor_color_markers), options);\r\n    }\r\n\r\n    private void appendIgnoreShortSegmentsCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createIgnoreShortSegmentsCategory(getContext());\r\n        settingsPresenter.appendCategory(category);\r\n    }\r\n\r\n    private void appendLinks(AppDialogPresenter settingsPresenter) {\r\n        OptionItem statsCheckOption = UiOptionItem.from(getContext().getString(R.string.content_block_status),\r\n                option -> Utils.openLink(getContext(), getContext().getString(R.string.content_block_status_url)));\r\n\r\n        OptionItem webSiteOption = UiOptionItem.from(getContext().getString(R.string.about_sponsorblock),\r\n                option -> Utils.openLink(getContext(), getContext().getString(R.string.content_block_provider_url)));\r\n\r\n        settingsPresenter.appendSingleButton(statsCheckOption);\r\n        settingsPresenter.appendSingleButton(webSiteOption);\r\n    }\r\n\r\n    private void appendMiscCategory(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n        \r\n        options.add(UiOptionItem.from(getContext().getString(R.string.paid_content_notification),\r\n                optionItem -> mContentBlockData.setPaidContentNotificationEnabled(optionItem.isSelected()),\r\n                mContentBlockData.isPaidContentNotificationEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.skip_each_segment_once),\r\n                optionItem -> mContentBlockData.setDontSkipSegmentAgainEnabled(optionItem.isSelected()),\r\n                mContentBlockData.isDontSkipSegmentAgainEnabled()));\r\n\r\n        options.add(UiOptionItem.from(getContext().getString(R.string.content_block_alt_server),\r\n                getContext().getString(R.string.content_block_alt_server_desc),\r\n                optionItem -> mContentBlockData.enableAltServer(optionItem.isSelected()),\r\n                mContentBlockData.isAltServerEnabled()));\r\n\r\n        settingsPresenter.appendCheckedCategory(getContext().getString(R.string.player_other), options);\r\n    }\r\n\r\n    private void appendExcludeChannelButton(AppDialogPresenter settingsPresenter) {\r\n        Video video = PlaybackPresenter.instance(getContext()).getVideo();\r\n\r\n        if (video == null || getViewManager().getTopView() != PlaybackView.class) {\r\n            return;\r\n        }\r\n\r\n        settingsPresenter.appendSingleButton(AppDialogUtil.createExcludeFromContentBlockButton(getContext(), video, MediaServiceManager.instance(), settingsPresenter::closeDialog));\r\n    }\r\n\r\n    private CharSequence getColoredString(int strResId, int colorResId) {\r\n        String origin = getContext().getString(strResId);\r\n        CharSequence colorMark = Utils.color(\"●\", ContextCompat.getColor(getContext(), colorResId));\r\n        return TextUtils.concat( colorMark, \" \", origin);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/SubtitleSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionCategory;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport com.liskovsoft.youtubeapi.service.internal.MediaServiceData;\r\n\r\npublic class SubtitleSettingsPresenter extends BasePresenter<Void> {\r\n    private final PlayerData mPlayerData;\r\n\r\n    public SubtitleSettingsPresenter(Context context) {\r\n        super(context);\r\n        mPlayerData = PlayerData.instance(context);\r\n    }\r\n\r\n    public static SubtitleSettingsPresenter instance(Context context) {\r\n        return new SubtitleSettingsPresenter(context);\r\n    }\r\n\r\n    public void show() {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        settingsPresenter.appendSingleSwitch(AppDialogUtil.createSubtitleChannelOption(getContext()));\r\n        // Can't work properly. There is no robust language detection.\r\n        //appendSubtitleLanguageCategory(settingsPresenter);\r\n        //appendMoreSubtitlesSwitch(settingsPresenter);\r\n        appendSubtitleStyleCategory(settingsPresenter);\r\n        appendSubtitleSizeCategory(settingsPresenter);\r\n        appendSubtitlePositionCategory(settingsPresenter);\r\n\r\n        settingsPresenter.showDialog(getContext().getString(R.string.subtitle_category_title));\r\n    }\r\n\r\n    //private void appendSubtitleLanguageCategory(AppSettingsPresenter settingsPresenter) {\r\n    //    String subtitleLanguageTitle = getContext().getString(R.string.subtitle_language);\r\n    //    String subtitlesDisabled = getContext().getString(R.string.subtitles_disabled);\r\n    //\r\n    //    LangUpdater langUpdater = new LangUpdater(getContext());\r\n    //    Map<String, String> locales = langUpdater.getSupportedLocales();\r\n    //    FormatItem currentFormat = mPlayerData.getFormat(FormatItem.TYPE_SUBTITLE);\r\n    //\r\n    //    List<OptionItem> options = new ArrayList<>();\r\n    //\r\n    //    options.add(UiOptionItem.from(\r\n    //            subtitlesDisabled, option -> mPlayerData.setFormat(FormatItem.fromLanguage(null)),\r\n    //            currentFormat == null || currentFormat.equals(FormatItem.fromLanguage(null))));\r\n    //\r\n    //    for (Entry<String, String> entry : locales.entrySet()) {\r\n    //        if (entry.getValue().isEmpty()) {\r\n    //            // Remove default language entry\r\n    //            continue;\r\n    //        }\r\n    //\r\n    //        options.add(UiOptionItem.from(\r\n    //                entry.getKey(), option -> mPlayerData.setFormat(FormatItem.fromLanguage(entry.getValue())),\r\n    //                FormatItem.fromLanguage(entry.getValue()).equals(currentFormat)));\r\n    //    }\r\n    //\r\n    //    settingsPresenter.appendRadioCategory(subtitleLanguageTitle, options);\r\n    //}\r\n\r\n    private void appendSubtitleStyleCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createSubtitleStylesCategory(getContext());\r\n        settingsPresenter.appendRadioCategory(category.title, category.options);\r\n    }\r\n\r\n    private void appendSubtitleSizeCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createSubtitleSizeCategory(getContext());\r\n        settingsPresenter.appendRadioCategory(category.title, category.options);\r\n    }\r\n\r\n    private void appendSubtitlePositionCategory(AppDialogPresenter settingsPresenter) {\r\n        OptionCategory category = AppDialogUtil.createSubtitlePositionCategory(getContext());\r\n        settingsPresenter.appendRadioCategory(category.title, category.options);\r\n    }\r\n\r\n    private void appendMoreSubtitlesSwitch(AppDialogPresenter settingsPresenter) {\r\n        settingsPresenter.appendSingleSwitch(UiOptionItem.from(\"Unlock more subtitles\",\r\n                option -> MediaServiceData.instance().setMoreSubtitlesUnlocked(option.isSelected()),\r\n                MediaServiceData.instance().isMoreSubtitlesUnlocked()));\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/UIScaleSettingsPresenter.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.presenters.settings;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class UIScaleSettingsPresenter extends BasePresenter<Void> {\r\n    private final MainUIData mMainUIData;\r\n    private boolean mRestartApp;\r\n    private final Runnable mOnFinish = () -> {\r\n        if (mRestartApp) {\r\n            mRestartApp = false;\r\n            MessageHelpers.showLongMessage(getContext(), R.string.msg_restart_app);\r\n        }\r\n    };\r\n\r\n    public UIScaleSettingsPresenter(Context context) {\r\n        super(context);\r\n        mMainUIData = MainUIData.instance(context);\r\n    }\r\n\r\n    public static UIScaleSettingsPresenter instance(Context context) {\r\n        return new UIScaleSettingsPresenter(context);\r\n    }\r\n\r\n    public void show() {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());\r\n\r\n        appendScaleUI(settingsPresenter);\r\n        appendVideoGridScale(settingsPresenter);\r\n\r\n        settingsPresenter.showDialog(getContext().getString(R.string.settings_ui_scale), mOnFinish);\r\n    }\r\n\r\n    private void appendVideoGridScale(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (float scale : new float[] {0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1.0f, 1.1f, 1.2f, 1.35f, 1.4f, 1.5f}) {\r\n            options.add(UiOptionItem.from(String.format(\"%sx\", scale),\r\n                    optionItem -> mMainUIData.setVideoGridScale(scale),\r\n                    Helpers.floatEquals(scale, mMainUIData.getVideoGridScale())));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.video_grid_scale), options);\r\n    }\r\n\r\n    private void appendScaleUI(AppDialogPresenter settingsPresenter) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (float scale : new float[] {0.6f, 0.65f, 0.7f, 0.75f, 0.8f, 0.85f, 0.9f, 0.95f, 1.0f, 1.05f, 1.1f, 1.15f, 1.2f}) {\r\n            options.add(UiOptionItem.from(String.format(\"%sx\", scale),\r\n                    optionItem -> {\r\n                        mMainUIData.setUIScale(scale);\r\n                        mRestartApp = true;\r\n                    },\r\n                    Helpers.floatEquals(scale, mMainUIData.getUIScale())));\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(getContext().getString(R.string.scale_ui), options);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/views/AddDeviceView.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.views;\r\n\r\npublic interface AddDeviceView {\r\n    void showCode(String userCode);\r\n    void close();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/views/AppDialogView.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.views;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionCategory;\r\n\r\nimport java.util.List;\r\n\r\npublic interface AppDialogView {\r\n    void show(List<OptionCategory> categories, String title, boolean isExpandable, boolean isTransparent, boolean isOverlay, int id);\r\n    void finish();\r\n    void goBack();\r\n    void clearBackstack();\r\n    boolean isShown();\r\n    boolean isTransparent();\r\n    boolean isOverlay();\r\n    boolean isPaused();\r\n    int getViewId();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/views/AppUpdateView.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.views;\r\n\r\npublic interface AppUpdateView {\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/views/BrowseView.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.views;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.SettingsGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.errors.ErrorFragmentData;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.BrowseSection;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\n\r\npublic interface BrowseView {\r\n    void addSection(int index, BrowseSection section);\r\n    void removeSection(BrowseSection category);\r\n    void removeAllSections();\r\n    void selectSection(int index, boolean focusOnContent);\r\n    void updateSection(VideoGroup group);\r\n    void updateSection(SettingsGroup group);\r\n    void clearSection(BrowseSection section);\r\n    void selectSectionItem(int index);\r\n    void selectSectionItem(Video item);\r\n    void showError(ErrorFragmentData data);\r\n    void showProgressBar(boolean show);\r\n    boolean isProgressBarShowing();\r\n    void focusOnContent();\r\n    boolean isEmpty();\r\n    void updateBadge();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/views/ChannelUploadsView.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.views;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\n\r\npublic interface ChannelUploadsView {\r\n    void update(VideoGroup videoGroup);\r\n    void showProgressBar(boolean show);\r\n    void clear();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/views/ChannelView.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.views;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\n\r\npublic interface ChannelView {\r\n    void update(VideoGroup videoGroup);\r\n    void setPosition(int index);\r\n    void showProgressBar(boolean show);\r\n    void clear();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/views/DetailsView.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.views;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\n\r\npublic interface DetailsView {\r\n    void openVideo(Video video);\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/views/PlaybackView.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.views;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.listener.PlayerEventListener;\r\n\r\npublic interface PlaybackView extends PlayerManager {\r\n    void showProgressBar(boolean show);\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/views/SearchView.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.views;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.search.MediaServiceSearchTagProvider;\r\n\r\npublic interface SearchView {\r\n    void updateSearch(VideoGroup group);\r\n    void clearSearch();\r\n    void clearSearchTags();\r\n    void setTagsProvider(MediaServiceSearchTagProvider provider);\r\n    void showProgressBar(boolean show);\r\n    void startSearch(String searchText);\r\n    String getSearchText();\r\n    void startVoiceRecognition();\r\n    void finishReally();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/views/SignInView.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.views;\r\n\r\npublic interface SignInView {\r\n    void showCode(String userCode, String signInUrl);\r\n    void close();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/views/SplashView.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.views;\r\n\r\nimport android.content.Intent;\r\n\r\npublic interface SplashView {\r\n    Intent getNewIntent();\r\n    void finishView();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/views/ViewManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.views;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.app.Activity;\r\nimport android.app.Fragment;\r\nimport android.content.ActivityNotFoundException;\r\nimport android.content.Context;\r\nimport android.content.Intent;\r\n\r\nimport androidx.annotation.NonNull;\r\nimport androidx.lifecycle.Lifecycle;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.FileHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.locale.LocaleUpdater;\r\nimport com.liskovsoft.sharedutils.misc.WeakHashSet;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.BrowsePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.ChannelPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.ChannelUploadsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.SearchPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.SplashPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.base.BasePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.AppUpdatePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MotherActivity;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\n\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\nimport java.util.Stack;\r\n\r\npublic class ViewManager {\r\n    private static final String TAG = ViewManager.class.getSimpleName();\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static ViewManager sInstance;\r\n    private final Context mContext;\r\n    private final Map<Class<?>, Class<? extends Activity>> mViewMapping;\r\n    private final Map<Class<? extends Activity>, Class<? extends Activity>> mParentMapping;\r\n    private final Stack<Class<? extends Activity>> mActivityStack;\r\n    private Class<? extends Activity> mRootActivity;\r\n    private Class<? extends Activity> mDefaultTop;\r\n    private long mPrevThrottleTimeMS;\r\n    private boolean mIsMoveToBackEnabled;\r\n    private boolean mIsFinished;\r\n    private boolean mIsPlayerOnlyModeEnabled;\r\n    private long mPendingActivityMs;\r\n    private Class<?> mPendingActivityClass;\r\n    private WeakHashSet<Runnable> mOnFinish;\r\n\r\n    private ViewManager(Context context) {\r\n        mContext = context;\r\n        mViewMapping = new HashMap<>();\r\n        mParentMapping = new HashMap<>();\r\n        mActivityStack = new Stack<>();\r\n    }\r\n\r\n    public static ViewManager instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new ViewManager(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void register(Class<?> viewClass, Class<? extends Activity> activityClass) {\r\n        register(viewClass, activityClass, null);\r\n    }\r\n\r\n    public void register(Class<?> viewClass, Class<? extends Activity> activityClass, Class<? extends Activity> parentActivityClass) {\r\n        mViewMapping.put(viewClass, activityClass);\r\n\r\n        if (parentActivityClass != null) {\r\n            mParentMapping.put(activityClass, parentActivityClass);\r\n        }\r\n    }\r\n\r\n    public void unregister(Class<?> viewClass) {\r\n        mViewMapping.remove(viewClass);\r\n    }\r\n\r\n    public Class<? extends Activity> getActivity(Class<?> viewClass) {\r\n        return mViewMapping.get(viewClass);\r\n    }\r\n\r\n    public Class<? extends Activity> getRootActivity() {\r\n        return mRootActivity;\r\n    }\r\n\r\n    /**\r\n     * Use carefully<br/>\r\n     * On running activity, this method invokes standard activity lifecycle: pause, resume etc...\r\n     */\r\n    public void startView(Class<?> viewClass) {\r\n        startView(viewClass, false);\r\n    }\r\n\r\n    /**\r\n     * Use carefully<br/>\r\n     * On running activity, this method invokes standard activity lifecycle: pause, resume etc...\r\n     */\r\n    public void startView(Class<?> viewClass, boolean forceStart) {\r\n        // Skip starting activity twice to get rid of pausing/resuming activity cycle\r\n        if (Utils.isAppInForegroundFixed() && getTopView() == viewClass) {\r\n            return;\r\n        }\r\n\r\n        mIsMoveToBackEnabled = false; // Essential part or new view will be pause immediately\r\n\r\n        //if (!forceStart && doThrottle()) {\r\n        //    Log.d(TAG, \"Too many events. Skipping startView...\");\r\n        //    return;\r\n        //}\r\n\r\n        Class<?> activityClass = mViewMapping.get(viewClass);\r\n\r\n        if (activityClass != null) {\r\n            startActivity(activityClass);\r\n        } else {\r\n            Log.e(TAG, \"Activity not registered for view \" + viewClass.getSimpleName());\r\n        }\r\n    }\r\n\r\n    public void startParentView(Activity activity) {\r\n        if (activity.getIntent() != null) {\r\n            removeTopActivity();\r\n\r\n            Class<?> parentActivity = getTopActivity();\r\n\r\n            if (parentActivity == null && !isPlayerOnlyModeEnabled()) {\r\n                parentActivity = getDefaultParent(activity);\r\n            }\r\n\r\n            if (parentActivity == null || isPlayerOnlyModeEnabled()) {\r\n                Log.d(TAG, \"Parent activity name doesn't stored in registry. Exiting to Home...\");\r\n\r\n                mIsMoveToBackEnabled = true;\r\n\r\n                if (isPlayerOnlyModeEnabled()) {\r\n                    safeMoveTaskToBack(activity);\r\n                }\r\n            } else {\r\n                try {\r\n                    Log.d(TAG, \"Launching parent activity: \" + parentActivity.getSimpleName());\r\n                    Intent intent = new Intent(activity, parentActivity);\r\n\r\n                    safeStartActivity(activity, intent);\r\n                } catch (ActivityNotFoundException e) {\r\n                    e.printStackTrace();\r\n                    Log.e(TAG, \"Parent activity not found.\");\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    public boolean hasParentView(Activity activity) {\r\n        return mActivityStack.size() > 1 || getDefaultParent(activity) != null;\r\n    }\r\n\r\n    public void startDefaultView() {\r\n        mIsMoveToBackEnabled = false;\r\n        mIsPlayerOnlyModeEnabled = false;\r\n\r\n        Class<?> lastActivity;\r\n\r\n        // Check that PIP window isn't closed by the user\r\n        if (mDefaultTop != null && PlaybackPresenter.instance(mContext).isRunningInBackground()) {\r\n            lastActivity = mDefaultTop;\r\n        } else if (!mActivityStack.isEmpty()) {\r\n            lastActivity = mActivityStack.peek();\r\n        } else {\r\n            lastActivity = mRootActivity;\r\n        }\r\n\r\n        Log.d(TAG, \"Launching default activity: \" + lastActivity.getSimpleName());\r\n\r\n        startActivity(lastActivity);\r\n    }\r\n\r\n    private void startActivity(Class<?> activityClass) {\r\n        Log.d(TAG, \"Launching activity: \" + activityClass.getSimpleName());\r\n\r\n        mPendingActivityMs = System.currentTimeMillis();\r\n        mPendingActivityClass = activityClass;\r\n\r\n        Intent intent = new Intent(mContext, activityClass);\r\n\r\n        // Fix: Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag\r\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\r\n\r\n        safeStartActivity(mContext, intent);\r\n    }\r\n\r\n    private boolean doThrottle() {\r\n        long currentTimeMS = System.currentTimeMillis();\r\n        boolean skipEvent = currentTimeMS - mPrevThrottleTimeMS < 1_000;\r\n\r\n        mPrevThrottleTimeMS = currentTimeMS;\r\n\r\n        return skipEvent;\r\n    }\r\n\r\n    public void addTop(Activity activity) {\r\n        if (checkMoveViewsToBack(activity)) {\r\n            // NOTE: Unknown purpose of commented code!\r\n\r\n            // Maybe finish whole app?\r\n            // Move task to back is active.\r\n            // So finishing the activity only.\r\n            //((MotherActivity) activity).finishReally();\r\n            return;\r\n        }\r\n\r\n        Class<? extends Activity> activityClass = activity.getClass();\r\n\r\n        // Open from phone's history fix. Not parent? Make the root then.\r\n        if (mParentMapping.get(activityClass) == null) {\r\n            mActivityStack.clear();\r\n        }\r\n\r\n        // reorder activity\r\n        mActivityStack.remove(activityClass);\r\n        mActivityStack.push(activityClass);\r\n    }\r\n\r\n    private void removeTopActivity() {\r\n        if (!mActivityStack.isEmpty()) {\r\n            mActivityStack.pop();\r\n        }\r\n    }\r\n\r\n    private Class<? extends Activity> getTopActivity() {\r\n        Class<? extends Activity> result = null;\r\n\r\n        if (!mActivityStack.isEmpty()) {\r\n            result = mActivityStack.peek();\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    public void setRoot(@NonNull Class<? extends Activity> rootActivity) {\r\n        mRootActivity = rootActivity;\r\n    }\r\n\r\n    private Class<?> getDefaultParent(Activity activity) {\r\n        Class<?> parentActivity = null;\r\n\r\n        for (Class<?> activityClass : mParentMapping.keySet()) {\r\n            if (activityClass.isInstance(activity)) {\r\n                parentActivity = mParentMapping.get(activityClass);\r\n            }\r\n        }\r\n\r\n        return parentActivity;\r\n    }\r\n\r\n    public void blockTop(Activity activity) {\r\n        mDefaultTop = activity == null ? null : activity.getClass();\r\n    }\r\n\r\n    public Class<? extends Activity> getBlockedTop() {\r\n        return mDefaultTop;\r\n    }\r\n\r\n    public void removeTop(Activity activity) {\r\n        mActivityStack.remove(activity.getClass());\r\n    }\r\n\r\n    private boolean checkMoveViewsToBack(Activity activity) {\r\n        if (mIsMoveToBackEnabled) {\r\n            safeMoveTaskToBack(activity);\r\n\r\n            return true;\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    public void enablePlayerOnlyMode(boolean enable) {\r\n        // Ensure that we're not opening tube link from description dialog\r\n        if (enable && AppDialogPresenter.instance(mContext).isDialogShown()) {\r\n            return;\r\n        }\r\n\r\n        mIsPlayerOnlyModeEnabled = enable;\r\n    }\r\n\r\n    public boolean isPlayerOnlyModeEnabled() {\r\n        //return mIsPlayerOnlyModeEnabled && PlaybackPresenter.instance(mContext).getBackgroundMode() != PlayerEngine.BACKGROUND_MODE_PIP;\r\n        return mIsPlayerOnlyModeEnabled && !PlaybackPresenter.instance(mContext).isInPipMode();\r\n    }\r\n\r\n    public void clearCaches() {\r\n        YouTubeServiceManager.instance().invalidateCache();\r\n        // Note, also deletes cached flags (internal cache)\r\n        // Note, deletes cached apks (external cache)\r\n        FileHelpers.deleteCache(mContext);\r\n        LocaleUpdater.clearCache();\r\n    }\r\n\r\n    /**\r\n     * Finishes the app without killing it (by moves tasks to back).<br/>\r\n     * The app continue to run in the background.\r\n     * @param activity this activity\r\n     */\r\n    public void properlyFinishTheApp(Context activity) {\r\n        if (activity instanceof MotherActivity) {\r\n            Log.d(TAG, \"Trying finish the app...\");\r\n            mIsMoveToBackEnabled = true; // close all activities below current one\r\n            mIsFinished = true;\r\n\r\n            mActivityStack.clear();\r\n\r\n            ((MotherActivity) activity).finishReally();\r\n\r\n            PlaybackPresenter.instance(activity).forceFinish();\r\n\r\n            // NOTE: The device may hung (SecurityException: requires android.permission.BIND_ACCESSIBILITY_SERVICE)\r\n            //exitToHomeScreen(); // fix open another app from the history stack\r\n\r\n            // Fix: can't start finished app activity from history.\r\n            // Do reset state because the app should continue to run in the background.\r\n            // NOTE: Don't rely on MotherActivity.onDestroy() because activity can be killed silently.\r\n            RxHelper.runAsync(() -> {\r\n                clearCaches();\r\n                SplashPresenter.unhold();\r\n                BrowsePresenter.unhold();\r\n                AppUpdatePresenter.unhold();\r\n                MotherActivity.invalidate();\r\n                runOnFinish();\r\n                mIsMoveToBackEnabled = false;\r\n            }, 1_000);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * SecurityException: requires android.permission.BIND_ACCESSIBILITY_SERVICE\r\n     */\r\n    private void exitToHomeScreen() {\r\n        Intent intent = new Intent(Intent.ACTION_MAIN);\r\n        intent.addCategory(Intent.CATEGORY_HOME);\r\n        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\r\n        safeStartActivity(mContext, intent);\r\n    }\r\n\r\n    public Class<?> getTopView() {\r\n        Class<? extends Activity> topActivity = getTopActivity();\r\n\r\n        if (topActivity == null) {\r\n            return null;\r\n        }\r\n\r\n        for (Map.Entry<Class<?>, Class<? extends Activity>> entry: mViewMapping.entrySet()) {\r\n            if (entry.getValue() == topActivity) {\r\n                return entry.getKey();\r\n            }\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    /**\r\n     * Fix: java.lang.NullPointerException Attempt to read from field<br/>\r\n     * 'com.android.server.am.TaskRecord com.android.server.am.ActivityRecord.task'<br/>\r\n     * on a null object reference<br/>\r\n     * <br/>\r\n     * Fix: java.lang.IllegalArgumentException<br/>\r\n     * View=android.widget.TextView not attached to window manager\r\n     */\r\n    private void safeMoveTaskToBack(Activity activity) {\r\n        try {\r\n            activity.moveTaskToBack(true);\r\n        } catch (NullPointerException | IllegalArgumentException | IllegalStateException e) {\r\n            // Pinned stack isn't top stack (IllegalStateException)\r\n            Log.e(TAG, \"Error when moving task to back: %s\", e.getMessage());\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Small delay to fix PIP transition bug (UI become unresponsive)\r\n     */\r\n    private void safeStartActivity(Context context, Intent intent) {\r\n        mIsFinished = false;\r\n\r\n        if (PlaybackPresenter.instance(mContext).isEngineBlocked()) { // Android 14 no-pip fix???\r\n            Utils.postDelayed(() -> safeStartActivityInt(context, intent), 0); // 50 ms old val\r\n        } else {\r\n            safeStartActivityInt(context, intent);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Fix: java.lang.IllegalArgumentException<br/>\r\n     * View=android.widget.TextView not attached to window manager\r\n     */\r\n    private void safeStartActivityInt(Context context, Intent intent) {\r\n        try {\r\n            context.startActivity(intent);\r\n        } catch (IllegalArgumentException | ActivityNotFoundException | IndexOutOfBoundsException | NullPointerException | SecurityException e) {\r\n            // NPE: Attempt to write to field 'boolean com.android.server.am.ActivityStack.mConfigWillChange' on a null object reference\r\n            Log.e(TAG, \"Error when starting activity: %s\", e.getMessage());\r\n            MessageHelpers.showLongMessage(context, e.getLocalizedMessage());\r\n        }\r\n    }\r\n\r\n    public boolean isFinished() {\r\n        return mIsFinished;\r\n    }\r\n\r\n    //public void enableMoveToBack(boolean enable) {\r\n    //    mIsMoveToBackEnabled = enable;\r\n    //}\r\n\r\n    public boolean isNewViewPending() {\r\n        return System.currentTimeMillis() - mPendingActivityMs < 1_000;\r\n    }\r\n\r\n    public boolean isViewPending(Class<?> viewClass) {\r\n        return isNewViewPending() && mViewMapping.get(viewClass) == mPendingActivityClass;\r\n    }\r\n\r\n    public void refreshCurrentView() {\r\n        Class<?> topView = getTopView();\r\n        if (topView == BrowseView.class) {\r\n            BrowsePresenter.instance(mContext).refresh();\r\n        } else if (topView == ChannelUploadsView.class) {\r\n            ChannelUploadsPresenter.instance(mContext).refresh();\r\n        }\r\n    }\r\n\r\n    public boolean isVisible(Object view) {\r\n        if (view instanceof Fragment) {\r\n            return ((Fragment) view).isVisible();\r\n        }\r\n\r\n        if (view instanceof androidx.fragment.app.Fragment) {\r\n            // Proper way to check the visibility\r\n            return ((androidx.fragment.app.Fragment) view).getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED);\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    public boolean isTopViewVisible() {\r\n        BasePresenter<?> presenter = getTopPresenter();\r\n\r\n        return presenter != null && isVisible(presenter.getView());\r\n    }\r\n\r\n    public static MotherActivity getMotherActivity(Object view) {\r\n        MotherActivity motherActivity = null;\r\n\r\n        if (view instanceof Fragment && ((Fragment) view).getActivity() instanceof MotherActivity) {\r\n            motherActivity = ((MotherActivity) ((Fragment) view).getActivity());\r\n        }\r\n\r\n        if (view instanceof androidx.fragment.app.Fragment && ((androidx.fragment.app.Fragment) view).getActivity() instanceof MotherActivity) {\r\n            motherActivity = ((MotherActivity) ((androidx.fragment.app.Fragment) view).getActivity());\r\n        }\r\n\r\n        return motherActivity;\r\n    }\r\n\r\n    public boolean isPlayerInForeground() {\r\n        return Utils.isAppInForegroundFixed() && getTopView() == PlaybackView.class;\r\n    }\r\n\r\n    public void moveAppToForeground() {\r\n        if (!Utils.isAppInForegroundFixed()) {\r\n            startView(SplashView.class);\r\n        }\r\n    }\r\n\r\n    public void movePlayerToForeground() {\r\n        Utils.turnScreenOn(mContext);\r\n\r\n        if (!isPlayerInForeground()) {\r\n            startView(PlaybackView.class);\r\n        }\r\n    }\r\n\r\n    public BasePresenter<?> getTopPresenter() {\r\n        Class<?> topView = getTopView();\r\n\r\n        if (topView == BrowseView.class) {\r\n            return BrowsePresenter.instance(mContext);\r\n        } else if (topView == SearchView.class) {\r\n            return SearchPresenter.instance(mContext);\r\n        } else if (topView == ChannelView.class) {\r\n            return ChannelPresenter.instance(mContext);\r\n        } else if (topView == ChannelUploadsView.class) {\r\n            return ChannelUploadsPresenter.instance(mContext);\r\n        } else if (topView == PlaybackView.class) {\r\n            return PlaybackPresenter.instance(mContext);\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    public void cancelOnFinish() {\r\n        if (mOnFinish != null) {\r\n            mOnFinish.clear();\r\n        }\r\n    }\r\n\r\n    public void addOnFinish(Runnable onFinish) {\r\n        if (mOnFinish == null) {\r\n            mOnFinish = new WeakHashSet<>();\r\n        }\r\n\r\n        mOnFinish.add(onFinish);\r\n    }\r\n\r\n    private void runOnFinish() {\r\n        if (mOnFinish == null) {\r\n            return;\r\n        }\r\n\r\n        mOnFinish.forEach(Runnable::run);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/views/WebBrowserView.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.app.views;\r\n\r\npublic interface WebBrowserView {\r\n    void loadUrl(String url);\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/autoframerate/AutoFrameRateHelper.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.autoframerate;\r\n\r\nimport android.app.Activity;\r\nimport android.content.Context;\r\nimport android.util.Pair;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.autoframerate.internal.DisplayHolder.Mode;\r\nimport com.liskovsoft.smartyoutubetv2.common.autoframerate.internal.DisplaySyncHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.autoframerate.internal.DisplaySyncHelper.AutoFrameRateListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\n\r\nimport java.util.HashMap;\r\n\r\npublic class AutoFrameRateHelper {\r\n    private static final String TAG = AutoFrameRateHelper.class.getSimpleName();\r\n    private static AutoFrameRateHelper sInstance;\r\n    private static final long THROTTLE_INTERVAL_MS = 5_000;\r\n    private final DisplaySyncHelper mSyncHelper;\r\n    private long mPrevCall;\r\n    private HashMap<Float, Float> mFrameRateMapping;\r\n    private boolean mIsFpsCorrectionEnabled;\r\n    private AutoFrameRateListener mListener;\r\n\r\n    private AutoFrameRateHelper(Context context) {\r\n        mSyncHelper = new DisplaySyncHelper(context);\r\n\r\n        initFrameRateMapping();\r\n    }\r\n\r\n    public static AutoFrameRateHelper instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new AutoFrameRateHelper(context);\r\n        }\r\n\r\n        if (context != null) {\r\n            sInstance.setContext(context);\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void apply(Activity activity, FormatItem format) {\r\n        apply(activity, format, false);\r\n    }\r\n\r\n    public boolean isSupported() {\r\n        resetStats();\r\n        return mSyncHelper.supportsDisplayModeChangeComplex();\r\n    }\r\n\r\n    public boolean isResolutionSwitchEnabled() {\r\n        return mSyncHelper.isResolutionSwitchEnabled();\r\n    }\r\n\r\n    public void setResolutionSwitchEnabled(boolean enabled, boolean force) {\r\n        if (force) {\r\n            Mode originalMode = mSyncHelper.getOriginalMode();\r\n            Mode newMode = mSyncHelper.getNewMode();\r\n\r\n            if (originalMode != null && newMode != null) {\r\n                if (enabled) {\r\n                    mSyncHelper.setResolutionSwitchEnabled(true);\r\n                    //syncMode(mCurrentFormat.first, mCurrentFormat.second);\r\n                } else {\r\n                    //syncMode(originalMode.getPhysicalWidth(), newMode.getRefreshRate());\r\n                    mSyncHelper.setResolutionSwitchEnabled(false);\r\n                }\r\n            }\r\n        } else {\r\n            mSyncHelper.setResolutionSwitchEnabled(enabled);\r\n        }\r\n    }\r\n\r\n    public void saveOriginalState(Activity activity) {\r\n        setContext(activity);\r\n\r\n        if (activity == null) {\r\n            Log.e(TAG, \"Activity in null. exiting...\");\r\n            return;\r\n        }\r\n\r\n        if (!isSupported()) {\r\n            return;\r\n        }\r\n\r\n        mSyncHelper.saveOriginalState();\r\n    }\r\n\r\n    private void initFrameRateMapping() {\r\n        mFrameRateMapping = new HashMap<>();\r\n        mFrameRateMapping.put(24f, 23.97f);\r\n        mFrameRateMapping.put(30f, 29.97f);\r\n        mFrameRateMapping.put(60f, 59.94f);\r\n    }\r\n\r\n    public void apply(Activity activity, FormatItem format, boolean force) {\r\n        setContext(activity);\r\n\r\n        if (activity == null) {\r\n            Log.e(TAG, \"Activity in null. exiting...\");\r\n            if (mListener != null) {\r\n                mListener.onModeCancel();\r\n            }\r\n            return;\r\n        }\r\n\r\n        if (format == null) {\r\n            Log.e(TAG, \"Can't apply mode change: format is null\");\r\n            if (mListener != null) {\r\n                mListener.onModeCancel();\r\n            }\r\n            return;\r\n        }\r\n\r\n        if (!isSupported()) {\r\n            Log.e(TAG, \"Autoframerate not supported. Exiting...\");\r\n            if (mListener != null) {\r\n                mListener.onModeCancel();\r\n            }\r\n            return;\r\n        }\r\n\r\n        if (System.currentTimeMillis() - mPrevCall < THROTTLE_INTERVAL_MS) {\r\n            Log.e(TAG, \"Throttling afr calls...\");\r\n            if (mListener != null) {\r\n                mListener.onModeCancel();\r\n            }\r\n            return;\r\n        } else {\r\n            mPrevCall = System.currentTimeMillis();\r\n        }\r\n\r\n        int width = format.getWidth();\r\n        float frameRate = correctFrameRate(format.getFrameRate());\r\n\r\n        Pair<Integer, Float> currentFormat = new Pair<>(width, frameRate);\r\n\r\n        Log.d(TAG, String.format(\"Applying mode change... Video fps: %s, width: %s, height: %s\", frameRate, width, format.getHeight()));\r\n\r\n        syncMode(activity, width, frameRate, force);\r\n    }\r\n\r\n    //private void syncMode(int width, float frameRate) {\r\n    //    syncMode(width, frameRate, false);\r\n    //}\r\n\r\n    private void syncMode(Activity activity, int width, float frameRate, boolean force) {\r\n        if (activity == null) {\r\n            Log.e(TAG, \"Activity in null. exiting...\");\r\n            return;\r\n        }\r\n\r\n        if (!isSupported()) {\r\n            Log.e(TAG, \"Autoframerate not supported. Exiting...\");\r\n            return;\r\n        }\r\n\r\n        mSyncHelper.syncDisplayMode(activity.getWindow(), width, frameRate, force);\r\n    }\r\n\r\n    public void restoreOriginalState(Activity activity) {\r\n        restoreOriginalState(activity, false);\r\n    }\r\n\r\n    private void restoreOriginalState(Activity activity, boolean force) {\r\n        if (activity == null) {\r\n            Log.e(TAG, \"activity == null\");\r\n            return;\r\n        }\r\n\r\n        if (!isSupported()) {\r\n            Log.d(TAG, \"restoreOriginalState: autoframerate not enabled... exiting...\");\r\n            return;\r\n        }\r\n\r\n        Log.d(TAG, \"Restoring original mode...\");\r\n\r\n        boolean result = mSyncHelper.restoreOriginalState(activity.getWindow(), force);\r\n\r\n        Log.d(TAG, \"Restore mode result: \" + result);\r\n    }\r\n\r\n    public void setListener(AutoFrameRateListener listener) {\r\n        mListener = listener;\r\n        mSyncHelper.setListener(listener);\r\n    }\r\n\r\n    public boolean isFpsCorrectionEnabled() {\r\n        return mIsFpsCorrectionEnabled;\r\n    }\r\n\r\n    public void setFpsCorrectionEnabled(boolean enabled) {\r\n        mIsFpsCorrectionEnabled = enabled;\r\n    }\r\n\r\n    public void setDoubleRefreshRateEnabled(boolean enabled) {\r\n        mSyncHelper.setDoubleRefreshRateEnabled(enabled);\r\n    }\r\n\r\n    public void setSkip24RateEnabled(boolean enabled) {\r\n        mSyncHelper.setSkip24RateEnabled(enabled);\r\n    }\r\n\r\n    private void resetStats() {\r\n        mSyncHelper.resetStats();\r\n    }\r\n\r\n    private float correctFrameRate(float frameRate) {\r\n        if (mIsFpsCorrectionEnabled && mFrameRateMapping.containsKey(frameRate)) {\r\n            return mFrameRateMapping.get(frameRate);\r\n        }\r\n\r\n        return frameRate;\r\n    }\r\n\r\n    ///**\r\n    // * UGOOS mode change fix. DEPRECATED!\r\n    // */\r\n    //private void applyModeChangeFix() {\r\n    //    if (!isSupported()) {\r\n    //        return;\r\n    //    }\r\n    //\r\n    //    mSyncHelper.applyModeChangeFix(mActivity.getWindow());\r\n    //}\r\n\r\n    //private void resetState() {\r\n    //    mSyncHelper.resetMode(mActivity.getWindow());\r\n    //}\r\n\r\n    private void setContext(Context activity) {\r\n        mSyncHelper.setContext(activity);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/autoframerate/ModeSyncManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.autoframerate;\r\n\r\nimport android.app.Activity;\r\nimport android.os.Handler;\r\nimport android.os.Looper;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\n\r\npublic class ModeSyncManager {\r\n    private static ModeSyncManager sInstance;\r\n    private FormatItem mFormatItem;\r\n    private AutoFrameRateHelper mFrameRateHelper;\r\n\r\n    private ModeSyncManager() {\r\n        // NOP\r\n    }\r\n\r\n    public static ModeSyncManager instance() {\r\n        if (sInstance == null) {\r\n            sInstance = new ModeSyncManager();\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void save(FormatItem formatItem) {\r\n        mFormatItem = formatItem;\r\n    }\r\n\r\n    public void restore(Activity activity) {\r\n        if (mFrameRateHelper == null) {\r\n            return;\r\n        }\r\n\r\n        new Handler(Looper.myLooper()).postDelayed(() -> applyAfr(activity), 1_000);\r\n    }\r\n\r\n    private void applyAfr(Activity activity) {\r\n        if (mFormatItem != null) {\r\n            mFrameRateHelper.apply(activity, mFormatItem);\r\n        } else {\r\n            //mFrameRateHelper.restoreOriginalState(activity);\r\n        }\r\n    }\r\n\r\n    public void setAfrHelper(AutoFrameRateHelper frameRateHelper) {\r\n        mFrameRateHelper = frameRateHelper;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/autoframerate/internal/DisplayHolder.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.autoframerate.internal;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\n/**\n * Convenience class to hold {@link DisplayHolder} Display object.\n *\n */\npublic class DisplayHolder {\n\n    /**\n     * Stub method to create the ModeInstance. In real world we need not to do it here.\n     * @param modeId Mode Id\n     * @param width Display mode's width\n     * @param height Display mode's height\n     * @param refreshRate Display mode's refresh rate\n     *\n     * @return {@link Mode} instance object.\n     */\n    public Mode getModeInstance(int modeId,int width,int height,float refreshRate){\n        return new Mode(modeId,width,height,refreshRate);\n    }\n\n    /**\n     * Inner class {@link Mode} holds the mode information.\n     */\n    public static class Mode implements Parcelable, Comparable<Mode> {\n\n        private int mModeId;\n        private int mHeight;\n        private int mWidth;\n        private float mRefreshRate;\n\n        public Mode(int mModeId,int mWidth, int mHeight, float refreshRate){\n            this.mModeId = mModeId;\n            this.mWidth = mWidth;\n            this.mHeight = (mHeight);\n            this.mRefreshRate = (refreshRate);\n        }\n        /**\n         * Returns this mode's id.\n         * @return mode id\n         */\n        public int getModeId(){\n            return mModeId;\n        }\n\n        /**\n         * Returns the physical height of the display in pixels when configured in this mode's resolution.\n         * @return display height\n         */\n        public int getPhysicalHeight(){\n            return mHeight;\n        }\n\n        /**\n         * Returns the physical width of the display in pixels when configured in this mode's\n         * resolution.\n         * @return display width\n         */\n        public int getPhysicalWidth() {\n            return mWidth;\n        }\n\n        /**\n         * Returns the refresh rate in frames per second.\n         * @return refresh rate\n         */\n        public float getRefreshRate() {\n            return mRefreshRate;\n\n        }\n\n        @Override\n        public int hashCode() {\n            final int prime = 31;\n            int result = 1;\n            result = prime * result + mModeId;\n            result = prime * result + mHeight;\n            result = prime * result + mWidth;\n            result = prime * result + Float.floatToIntBits(mRefreshRate);\n            return result;\n        }\n\n        @Override\n        public boolean equals(Object obj) {\n            if (this == obj)\n                return true;\n            if (obj == null)\n                return false;\n            if (getClass() != obj.getClass())\n                return false;\n            Mode other = (Mode) obj;\n            if (mModeId != other.mModeId)\n                return false;\n            if (mHeight != other.mHeight)\n                return false;\n            if (mWidth != other.mWidth)\n                return false;\n            if (Float.floatToIntBits(mRefreshRate) != Float.floatToIntBits(other.mRefreshRate))\n                return false;\n            return true;\n        }\n\n        /**\n         * Returns {@code true} if this mode matches the given parameters.\n         * @param width of the given display\n         * @param height of the given display\n         * @param refreshRate refresh rate of the current display\n         * @return {@code true} if width, height and refresh rate matches with the current mode.\n         */\n        public boolean matches(int width, int height, float refreshRate) {\n            return mWidth == width &&\n                    mHeight == height &&\n                    Float.floatToIntBits(mRefreshRate) == Float.floatToIntBits(refreshRate);\n        }\n\n        @Override\n        public String toString() {\n            return \"Mode [mModeId=\" + mModeId + \", mHeight=\" + mHeight + \", mWidth=\" + mWidth\n                    + \", mRefreshRate=\" + mRefreshRate + \"]\";\n        }\n        @Override\n        public int describeContents() {\n            return 0;\n        }\n\n        private Mode(Parcel in) {\n            this(in.readInt(), in.readInt(), in.readInt(), in.readFloat());\n        }\n\n        @Override\n        public void writeToParcel(Parcel out, int parcelableFlags) {\n            out.writeInt(mModeId);\n            out.writeInt(mWidth);\n            out.writeInt(mHeight);\n            out.writeFloat(mRefreshRate);\n        }\n\n        @SuppressWarnings(\"hiding\")\n        public static final Creator<Mode> CREATOR\n                = new Creator<Mode>() {\n            @Override\n            public Mode createFromParcel(Parcel in) {\n                return new Mode(in);\n            }\n\n            @Override\n            public Mode[] newArray(int size) {\n                return new Mode[size];\n            }\n        };\n\n        /**\n         * Sort in descendant order\n         */\n        @Override\n        public int compareTo(Mode o) {\n            if (getPhysicalWidth() == o.getPhysicalWidth()) {\n                return (int) ((o.getRefreshRate() * 1_000) - (getRefreshRate() * 1_000));\n            }\n\n            return o.getPhysicalWidth() - getPhysicalWidth();\n        }\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/autoframerate/internal/DisplaySyncHelper.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.autoframerate.internal;\r\n\r\nimport android.content.Context;\r\nimport android.os.Build;\r\nimport android.os.Build.VERSION;\r\nimport android.view.Window;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.autoframerate.internal.DisplayHolder.Mode;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Arrays;\r\nimport java.util.Collections;\r\nimport java.util.HashMap;\r\nimport java.util.Iterator;\r\nimport java.util.List;\r\n\r\n// Source: https://developer.amazon.com/docs/fire-tv/4k-apis-for-hdmi-mode-switch.html#amazonextension\r\n\r\npublic class DisplaySyncHelper implements UhdHelperListener {\r\n    private static final String TAG = DisplaySyncHelper.class.getSimpleName();\r\n    private static final int STATE_ORIGINAL = 1;\r\n    private static final int HD = 1200;\r\n    private static final int FHD = 1900;\r\n    protected Context mContext;\r\n    private boolean mDisplaySyncInProgress = false;\r\n    private UhdHelper mUhdHelper;\r\n    protected Mode mOriginalMode;\r\n    private Mode mNewMode;\r\n    // switch not only framerate but resolution too\r\n    private boolean mIsResolutionSwitchEnabled;\r\n    private boolean mIsDoubleRefreshRateEnabled = true;\r\n    private boolean mIsSkip24RateEnabled;\r\n    private int mModeLength = -1;\r\n    private AutoFrameRateListener mListener;\r\n\r\n    public interface AutoFrameRateListener {\r\n        void onModeStart(Mode newMode);\r\n        void onModeError(Mode newMode);\r\n        void onModeCancel();\r\n    }\r\n\r\n    public DisplaySyncHelper(Context context) {\r\n        if (context != null) {\r\n            mContext = context.getApplicationContext();\r\n        }\r\n    }\r\n\r\n    public Mode getOriginalMode() {\r\n        return mOriginalMode;\r\n    }\r\n\r\n    public Mode getNewMode() {\r\n        return mNewMode;\r\n    }\r\n\r\n    private List<Mode> filterSameResolutionModes(Mode[] oldModes, Mode currentMode) {\r\n        if (currentMode == null) {\r\n            return Collections.emptyList();\r\n        }\r\n\r\n        ArrayList<Mode> newModes = new ArrayList<>();\r\n        int oldModesLen = oldModes.length;\r\n\r\n        for (int i = 0; i < oldModesLen; ++i) {\r\n            Mode mode = oldModes[i];\r\n            if (mode == null) {\r\n                continue;\r\n            }\r\n\r\n            if (mode.getPhysicalHeight() == currentMode.getPhysicalHeight() && mode.getPhysicalWidth() == currentMode.getPhysicalWidth()) {\r\n                newModes.add(mode);\r\n            }\r\n        }\r\n\r\n        return newModes;\r\n    }\r\n\r\n    /**\r\n     * Filter all modes except one that match by width.\r\n     */\r\n    private ArrayList<Mode> filterModesByWidthOrigin(Mode[] allModes, int videoWidth) {\r\n        ArrayList<Mode> newModes = new ArrayList<>();\r\n\r\n        if (videoWidth == -1) {\r\n            return newModes;\r\n        }\r\n\r\n        for (Mode mode : allModes) {\r\n            int width = mode.getPhysicalWidth();\r\n            if (width >= (videoWidth - 100) && width <= (videoWidth + 100)) {\r\n                newModes.add(mode);\r\n            }\r\n        }\r\n\r\n        if (newModes.isEmpty()) {\r\n            Log.i(TAG, \"MODE CANDIDATES NOT FOUND!! Old modes: \" + Arrays.asList(allModes));\r\n        } else {\r\n            Log.i(TAG, \"FOUND MODE CANDIDATES! New modes: \" + newModes);\r\n        }\r\n\r\n        return newModes;\r\n    }\r\n\r\n    /**\r\n     * Filter out modes that has same width.<br/>\r\n     * Reverse order is important because of later mapping by fps in other method.\r\n     */\r\n    private ArrayList<Mode> filterModesByWidth(Mode[] allModes, int videoWidth) {\r\n        ArrayList<Mode> newModes = new ArrayList<>();\r\n\r\n        if (videoWidth == -1) {\r\n            return newModes;\r\n        }\r\n\r\n        // Reverse order. It's important because of later mapping by fps.\r\n        Arrays.sort(allModes, (mode1, mode2) -> {\r\n            int width1 = mode1.getPhysicalWidth();\r\n            int width2 = mode2.getPhysicalWidth();\r\n\r\n            return width2 - width1;\r\n        });\r\n\r\n        for (Mode mode : allModes) {\r\n            int width = mode.getPhysicalWidth();\r\n\r\n            // Use with caution. Non strict match. E.g. 1440p will match 1440p and up\r\n            if (width >= (videoWidth - 100)) {\r\n                newModes.add(mode);\r\n            }\r\n\r\n            // Strict match\r\n            //if (Math.abs(width - videoWidth) < 100) {\r\n            //    newModes.add(mode);\r\n            //}\r\n        }\r\n\r\n        if (newModes.isEmpty()) {\r\n            Log.i(TAG, \"MODE CANDIDATES NOT FOUND!! Old modes: \" + Arrays.asList(allModes));\r\n        } else {\r\n            Log.i(TAG, \"FOUND MODE CANDIDATES! New modes: \" + newModes);\r\n        }\r\n\r\n        return newModes;\r\n    }\r\n\r\n    private ArrayList<Mode> filterModes(Mode[] oldModes, int minHeight, int maxHeight) {\r\n        ArrayList<Mode> newModes = new ArrayList<>();\r\n\r\n        if (minHeight == -1 || maxHeight == -1) {\r\n            return newModes;\r\n        }\r\n\r\n        int modesNum = oldModes.length;\r\n\r\n        for (int i = 0; i < modesNum; ++i) {\r\n            Mode mode = oldModes[i];\r\n            int height = mode.getPhysicalHeight();\r\n            if (height >= minHeight && height <= maxHeight) {\r\n                newModes.add(mode);\r\n            }\r\n        }\r\n\r\n        if (newModes.isEmpty()) {\r\n            Log.i(TAG, \"MODE CANDIDATES NOT FOUND!! Old modes: \" + Arrays.asList(oldModes));\r\n        } else {\r\n            Log.i(TAG, \"FOUND MODE CANDIDATES! New modes: \" + newModes);\r\n        }\r\n\r\n        return newModes;\r\n    }\r\n\r\n    protected Mode findCloserMode(Mode[] modes, float videoFramerate) {\r\n        if (modes == null) {\r\n            return null;\r\n        }\r\n\r\n        return findCloserMode(Arrays.asList(modes), videoFramerate);\r\n    }\r\n\r\n    private Mode findCloserMode(List<Mode> modes, float videoFramerate) {\r\n        HashMap<Integer, int[]> relatedRates;\r\n\r\n        relatedRates = getRateMapping();\r\n\r\n        int myRate = (int) (videoFramerate * 100.0F);\r\n\r\n        if (myRate >= 2300 && myRate <= 2399) {\r\n            myRate = 2397;\r\n        }\r\n\r\n        if (relatedRates.containsKey(myRate)) {\r\n            HashMap<Integer, Mode> rateAndMode = new HashMap<>();\r\n            Iterator modeIterator = modes.iterator();\r\n\r\n            while (modeIterator.hasNext()) {\r\n                Mode mode = (Mode) modeIterator.next();\r\n                rateAndMode.put((int) (mode.getRefreshRate() * 100.0F), mode);\r\n            }\r\n\r\n            int[] rates = relatedRates.get(myRate);\r\n            int ratesLen = rates.length;\r\n\r\n            for (int i = 0; i < ratesLen; ++i) {\r\n                int newRate = rates[i];\r\n                if (rateAndMode.containsKey(newRate)) {\r\n                    return rateAndMode.get(newRate);\r\n                }\r\n            }\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    private HashMap<Integer, int[]> getRateMapping() {\r\n        HashMap<Integer, int[]> rateMapping = mIsDoubleRefreshRateEnabled ? getDoubleRateMapping() : getSingleRateMapping();\r\n        //return apply24RateSkip(rateMapping);\r\n        return rateMapping;\r\n    }\r\n\r\n    private HashMap<Integer, int[]> getSingleRateMapping() {\r\n        HashMap<Integer, int[]> relatedRates = new HashMap<>();\r\n        relatedRates.put(1500, new int[]{3000, 6000});\r\n        relatedRates.put(2397, new int[]{2397, 2400, 3000, 6000});\r\n        relatedRates.put(2400, new int[]{2400, 3000, 6000});\r\n        relatedRates.put(2500, new int[]{2500, 5000});\r\n        relatedRates.put(2997, new int[]{2997, 3000, 6000});\r\n        relatedRates.put(3000, new int[]{3000, 6000});\r\n        relatedRates.put(5000, new int[]{5000, 2500});\r\n        relatedRates.put(5994, new int[]{5994, 6000, 3000});\r\n        relatedRates.put(6000, new int[]{6000, 3000});\r\n        return relatedRates;\r\n    }\r\n\r\n    /**\r\n     * ExoPlayer reports wrong for 60 and 30 fps formats.<br/>\r\n     * Do workarounds: 60 => 59.94, 30 => 59.94\r\n     */\r\n    private HashMap<Integer, int[]> getDoubleRateMapping() {\r\n        HashMap<Integer, int[]> relatedRates = new HashMap<>();\r\n        relatedRates.put(1500, new int[]{6000, 3000});\r\n        relatedRates.put(2397, new int[]{4794, 4800, 2397, 2400});\r\n        relatedRates.put(2400, new int[]{4800, 2400});\r\n        relatedRates.put(2497, new int[]{4994, 5000, 2497, 2500});\r\n        relatedRates.put(2500, new int[]{5000, 2500});\r\n        relatedRates.put(2997, new int[]{5994, 6000, 2997, 3000});\r\n        relatedRates.put(3000, new int[]{6000, 3000});\r\n        relatedRates.put(5000, new int[]{5000, 2500});\r\n        relatedRates.put(5994, new int[]{5994, 6000, 2997, 3000});\r\n        relatedRates.put(6000, new int[]{6000, 3000});\r\n        return relatedRates;\r\n    }\r\n\r\n    private HashMap<Integer, int[]> apply24RateSkip(HashMap<Integer, int[]> rateMapping) {\r\n        if (mIsSkip24RateEnabled) {\r\n            rateMapping.remove(2397);\r\n            rateMapping.remove(2400);\r\n            rateMapping.remove(2497);\r\n        }\r\n\r\n        return rateMapping;\r\n    }\r\n\r\n    /**\r\n     * Utility method to check if device is Amazon Fire TV device\r\n     * @return {@code true} true if device is Amazon Fire TV device.\r\n     */\r\n    public static boolean isAmazonFireTVDevice(){\r\n        String deviceName = Build.MODEL;\r\n        String manufacturerName = Build.MANUFACTURER;\r\n        return (deviceName.startsWith(\"AFT\")\r\n                && \"Amazon\".equalsIgnoreCase(manufacturerName));\r\n    }\r\n\r\n    public boolean supportsDisplayModeChangeComplex() {\r\n        if (mContext == null) {\r\n            return false;\r\n        }\r\n\r\n        if (mModeLength == -1) {\r\n            Mode[] supportedModes = null;\r\n\r\n            if (VERSION.SDK_INT >= 21) {\r\n                supportedModes = getUhdHelper().getSupportedModes();\r\n            }\r\n\r\n            mModeLength = supportedModes == null ? 0 : supportedModes.length;\r\n        }\r\n\r\n        return mModeLength >= 1 && supportsDisplayModeChange();\r\n    }\r\n\r\n    /**\r\n     * Check whether device supports mode change. Also shows toast if no\r\n     * @return mode change supported\r\n     */\r\n    public static boolean supportsDisplayModeChange() {\r\n        boolean supportedDevice = true;\r\n\r\n        //We fail for following conditions\r\n        if(VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {\r\n            supportedDevice = false;\r\n        } else {\r\n            switch (VERSION.SDK_INT) {\r\n                case Build.VERSION_CODES.LOLLIPOP:\r\n                case Build.VERSION_CODES.LOLLIPOP_MR1:\r\n                    if (!isAmazonFireTVDevice()) {\r\n                        supportedDevice = false;\r\n                    }\r\n                    break;\r\n            }\r\n        }\r\n\r\n        if (!supportedDevice) {\r\n            Log.i(TAG, \"Device doesn't support display mode change\");\r\n        }\r\n\r\n        return supportedDevice;\r\n    }\r\n\r\n    public boolean displayModeSyncInProgress() {\r\n        return mDisplaySyncInProgress;\r\n    }\r\n\r\n    @Override\r\n    public void onModeChanged(Mode mode) {\r\n        mDisplaySyncInProgress = false;\r\n\r\n        if (mNewMode == null) {\r\n            Log.d(TAG, \"Ignore mode change. AFR isn't activated.\");\r\n            return;\r\n        }\r\n\r\n        Mode currentMode = getUhdHelper().getCurrentMode();\r\n\r\n        if (mode == null && currentMode == null) {\r\n            Log.e(TAG, \"Mode change failure. Internal error occurred.\");\r\n        } else {\r\n            if (currentMode.getModeId() != mNewMode.getModeId()) {\r\n                // Once onDisplayChangedListener sends proper callback, the above if condition\r\n                // need to changed to mode.getModeId() != modeId\r\n                Log.e(TAG, \"Mode change failure. Current mode id is %s. Expected mode id is %s\", currentMode.getModeId(), mNewMode.getModeId());\r\n\r\n                if (mListener != null) {\r\n                    mListener.onModeError(mNewMode);\r\n                }\r\n            } else {\r\n                Log.d(TAG, \"Mode changed successfully\");\r\n            }\r\n        }\r\n    }\r\n\r\n    // switch frame rate only\r\n    private boolean getNeedDisplaySync() {\r\n        return true;\r\n    }\r\n\r\n    public boolean syncDisplayMode(Window window, int videoWidth, float videoFramerate) {\r\n        return syncDisplayMode(window, videoWidth, videoFramerate, false);\r\n    }\r\n\r\n    /**\r\n     * Tries to find best suited display params for the video\r\n     * @param window window object\r\n     * @param videoWidth width of the video material\r\n     * @param videoFramerate framerate of the video\r\n     * @return\r\n     */\r\n    public boolean syncDisplayMode(Window window, int videoWidth, float videoFramerate, boolean force) {\r\n        if (supportsDisplayModeChange() && videoWidth >= 10) {\r\n            if (mUhdHelper == null) {\r\n                mUhdHelper = new UhdHelper(mContext);\r\n                mUhdHelper.registerModeChangeListener(this);\r\n            }\r\n\r\n            Mode[] modes = mUhdHelper.getSupportedModes();\r\n\r\n            Log.d(TAG, \"Modes supported by device:\");\r\n            Log.d(TAG, Arrays.asList(modes));\r\n\r\n            boolean needResolutionSwitch = false;\r\n\r\n            List<Mode> resultModes = new ArrayList<>();\r\n\r\n            if (mIsResolutionSwitchEnabled) {\r\n                resultModes = filterModesByWidth(modes, Math.max(videoWidth, HD));\r\n            }\r\n\r\n            if (!resultModes.isEmpty()) {\r\n                needResolutionSwitch = true;\r\n            }\r\n\r\n            Log.i(TAG, \"Need resolution switch: \" + needResolutionSwitch);\r\n\r\n            Mode currentMode = mUhdHelper.getCurrentMode();\r\n\r\n            if (!needResolutionSwitch) {\r\n                resultModes = filterSameResolutionModes(modes, currentMode);\r\n            }\r\n\r\n            // Rate boundaries slightly increased to perfect compare between two floats\r\n            boolean skipFps = mIsSkip24RateEnabled && videoFramerate >= 23.96 && videoFramerate <= 24.98 && currentMode != null;\r\n            Mode closerMode = findCloserMode(resultModes, skipFps ? currentMode.getRefreshRate() : videoFramerate);\r\n\r\n            if (closerMode == null) {\r\n                String msg = \"Could not find closer refresh rate for \" + videoFramerate + \"fps\";\r\n                Log.i(TAG, msg);\r\n                if (modes.length == 1) { // notify tvQuickActions or other related software\r\n                    mListener.onModeError(null);\r\n                } else {\r\n                    mListener.onModeCancel();\r\n                }\r\n                return false;\r\n            }\r\n\r\n            Log.i(TAG, \"Found closer mode: \" + closerMode + \" for fps \" + videoFramerate);\r\n            Log.i(TAG, \"Current mode: \" + currentMode);\r\n\r\n            if (!force && closerMode.equals(currentMode)) {\r\n                Log.i(TAG, \"Do not need to change mode.\");\r\n                mListener.onModeCancel();\r\n                return false;\r\n            }\r\n\r\n            mNewMode = closerMode;\r\n            mUhdHelper.setPreferredDisplayModeId(window, mNewMode.getModeId(), true);\r\n            mDisplaySyncInProgress = true;\r\n\r\n            if (mListener != null) {\r\n                mListener.onModeStart(mNewMode);\r\n            }\r\n\r\n            return true;\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    public void resetMode(Window window) {\r\n        getUhdHelper().setPreferredDisplayModeId(window, 0, true);\r\n    }\r\n\r\n    /**\r\n     * Lazy init of uhd helper.<br/>\r\n     * Convenient when user doesn't use a afr at all.\r\n     * @return helper\r\n     */\r\n    protected UhdHelper getUhdHelper() {\r\n        if (mUhdHelper == null) {\r\n            mUhdHelper = new UhdHelper(mContext);\r\n            mUhdHelper.registerModeChangeListener(this);\r\n        }\r\n\r\n        return mUhdHelper;\r\n    }\r\n\r\n    public void saveOriginalState() {\r\n        saveState(STATE_ORIGINAL);\r\n    }\r\n\r\n    public boolean restoreOriginalState(Window window, boolean force) {\r\n        return restoreState(window, STATE_ORIGINAL, force);\r\n    }\r\n\r\n    public boolean restoreOriginalState(Window window) {\r\n        return restoreOriginalState(window, false);\r\n    }\r\n\r\n    private void saveState(int state) {\r\n        Mode mode = getUhdHelper().getCurrentMode();\r\n\r\n        Log.d(TAG, \"Saving mode: \" + mode);\r\n\r\n        if (mode != null) {\r\n            switch (state) {\r\n                case STATE_ORIGINAL:\r\n                    mOriginalMode = mode;\r\n\r\n                    AppPrefs.instance(mContext).setBootResolution(UhdHelper.toResolution(mode));\r\n                    break;\r\n            }\r\n        }\r\n    }\r\n\r\n    private boolean restoreState(Window window, int state, boolean force) {\r\n        Log.d(TAG, \"Beginning to restore state...\");\r\n\r\n        Mode modeTmp = null;\r\n\r\n        switch (state) {\r\n            case STATE_ORIGINAL:\r\n                modeTmp = mOriginalMode;\r\n                break;\r\n        }\r\n\r\n        if (modeTmp == null) {\r\n            Log.d(TAG, \"Can't restore state. Mode is null.\");\r\n            return false;\r\n        }\r\n\r\n        Mode mode = getUhdHelper().getCurrentMode();\r\n\r\n        if (!force && modeTmp.equals(mode)) {\r\n            Log.d(TAG, \"Do not need to restore mode. Current mode is the same as new.\");\r\n            return false;\r\n        }\r\n\r\n        Log.d(TAG, \"Restoring mode: \" + modeTmp);\r\n        \r\n        getUhdHelper().setPreferredDisplayModeId(\r\n                window,\r\n                modeTmp.getModeId(),\r\n                true);\r\n\r\n        if (mListener != null) {\r\n            mListener.onModeStart(mOriginalMode);\r\n        }\r\n\r\n        return true;\r\n    }\r\n\r\n    public void resetStats() {\r\n        mModeLength = -1;\r\n    }\r\n\r\n    public void setListener(AutoFrameRateListener listener) {\r\n        mListener = listener;\r\n    }\r\n\r\n    /**\r\n     * Set default mode to 1920x1080@50<br/>\r\n     * Because switch not work with some devices running at 60HZ. Like: UGOOS\r\n     */\r\n    public void applyModeChangeFix(Window window) {\r\n        if (mOriginalMode != null) {\r\n            if (mOriginalMode.getRefreshRate() > 55) {\r\n                setDefaultMode(window, mOriginalMode.getPhysicalWidth(), 50);\r\n            } else {\r\n                setDefaultMode(window, mOriginalMode.getPhysicalWidth(), 60);\r\n            }\r\n        } else {\r\n            setDefaultMode(window, 1080, 50);\r\n        }\r\n    }\r\n\r\n    private void setDefaultMode(Window window, int width, float frameRate) {\r\n        syncDisplayMode(window, width, frameRate);\r\n\r\n        if (mNewMode != null) {\r\n            mOriginalMode = mNewMode;\r\n            AppPrefs.instance(mContext).setBootResolution(UhdHelper.toResolution(mOriginalMode));\r\n        }\r\n    }\r\n\r\n    public void setResolutionSwitchEnabled(boolean enabled) {\r\n        mIsResolutionSwitchEnabled = enabled;\r\n    }\r\n\r\n    public boolean isResolutionSwitchEnabled() {\r\n        return mIsResolutionSwitchEnabled;\r\n    }\r\n\r\n    public void setDoubleRefreshRateEnabled(boolean enabled) {\r\n        mIsDoubleRefreshRateEnabled = enabled;\r\n    }\r\n\r\n    public void setSkip24RateEnabled(boolean enabled) {\r\n        mIsSkip24RateEnabled = enabled;\r\n    }\r\n\r\n    public void setContext(Context context) {\r\n        if (context != null) {\r\n            mContext = context.getApplicationContext();\r\n        }\r\n\r\n        mUhdHelper = null; // uhd helper uses context, so do re-init\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/autoframerate/internal/UhdHelper.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.autoframerate.internal;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.graphics.Point;\nimport android.hardware.display.DisplayManager;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\nimport android.view.Display;\nimport android.view.Window;\nimport android.view.WindowManager;\n\nimport androidx.core.content.ContextCompat;\n\nimport com.liskovsoft.sharedutils.mylogger.Log;\nimport com.liskovsoft.smartyoutubetv2.common.autoframerate.internal.DisplayHolder.Mode;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\n// Source: https://developer.amazon.com/docs/fire-tv/4k-apis-for-hdmi-mode-switch.html#amazonextension\n\n/**\n * UhdHelper is a convenience class to provide interfaces to query<br/>\n * 1) Supported Display modes.<br/>\n * 2) Get current display mode<br/>\n * 3) Set preferred mode.\n */\npublic class UhdHelper {\n    Context mContext;\n    private UhdHelperListener mListener;\n    final public static String version = \"v1.1\";\n    private String sDisplayClassName = \"android.view.Display\";\n    private String sSupportedModesMethodName = \"getSupportedModes\";\n    private String sPreferredDisplayModeIdFieldName = \"preferredDisplayModeId\";\n    private String sGetModeMethodName = \"getMode\";\n    private String sGetModeIdMethodName = \"getModeId\";\n    private String sGetPhysicalHeightMethodName = \"getPhysicalHeight\";\n    private String sGetPhysicalWidthMethodName = \"getPhysicalWidth\";\n    private String sGetRefreshRateMethodName = \"getRefreshRate\";\n    private AtomicBoolean mIsSetModeInProgress;\n    private WorkHandler mWorkHandler;\n    private OverlayStateChangeReceiver overlayStateChangeReceiver;\n    boolean isReceiversRegistered;\n    private DisplayHolder mInternalDisplay;\n    private boolean showInterstitial = false;\n    private boolean isInterstitialFadeReceived = false;\n    private Window mTargetWindow;\n    private int currentOverlayStatus;\n    public final static String MODESWITCH_OVERLAY_ENABLE = \"com.amazon.tv.notification.modeswitch_overlay.action.ENABLE\";\n    public final static String MODESWITCH_OVERLAY_DISABLE = \"com.amazon.tv.notification.modeswitch_overlay.action.DISABLE\";\n    public final static String MODESWITCH_OVERLAY_EXTRA_STATE = \"com.amazon.tv.notification.modeswitch_overlay.extra.STATE\";\n    public final static String MODESWITCH_OVERLAY_STATE_CHANGED = \"com.amazon.tv.notification.modeswitch_overlay.action.STATE_CHANGED\";\n    public final static int OVERLAY_STATE_DISMISSED = 0;\n    DisplayManager mDisplayManager;\n    DisplayManager.DisplayListener mDisplayListener;\n    /**\n     * Physical height of UHD in pixels ( {@value} )\n     */\n    public static final int HEIGHT_UHD = 2160;\n    /**\n     * {@value} ms to wait for broadcast before declaring timeout.\n     */\n    public static final int SET_MODE_TIMEOUT_DELAY_MS = 15 * 1000;\n    /**\n     * {@value} ms to wait for Interstitial broadcast before declaring timeout.\n     */\n    public static final int SHOW_INTERSTITIAL_TIMEOUT_DELAY_MS = 2 * 1000;\n\n    private static final String TAG = UhdHelper.class.getSimpleName();\n\n    /**\n     * Construct a UhdHelper object.\n     *\n     * @param context Activity context.\n     */\n    @SuppressLint(\"NewApi\")\n    public UhdHelper(Context context) {\n        mContext = context;\n        mInternalDisplay = new DisplayHolder();\n        mIsSetModeInProgress = new AtomicBoolean(false);\n        mWorkHandler = new WorkHandler(Looper.getMainLooper());\n        overlayStateChangeReceiver = new OverlayStateChangeReceiver();\n        mDisplayManager = (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);\n        isReceiversRegistered = false;\n\n    }\n\n    private static final int MODE_CHANGED_MSG = 1;\n    private static final int MODE_CHANGE_TIMEOUT_MSG = 2;\n    private static final int SEND_CALLBACK_WITH_SUPPLIED_RESULT = 3;\n    private static final int INTERSTITIAL_FADED_BROADCAST_MSG = 4;\n    private static final int INTERSTITIAL_TIMEOUT_MSG = 5;\n\n    /**\n     * Handler that handles the broadcast or timeout\n     * prcoessing and issues callbacks accordingly.\n     */\n    private class WorkHandler extends Handler {\n        private int mRequestedModeId;\n        private UhdHelperListener mCallbackListener;\n\n        public WorkHandler(Looper looper) {\n            super(looper);\n        }\n\n        public void setExpectedMode(int modeId) {\n            mRequestedModeId = modeId;\n        }\n\n        private void setCallbackListener(UhdHelperListener listener) {\n            this.mCallbackListener = listener;\n        }\n\n        @Override\n        public void handleMessage(Message msg) {\n            switch (msg.what) {\n                case MODE_CHANGED_MSG:\n                    Mode mode = getCurrentMode();\n                    if (mode == null) {\n                        Log.w(TAG, \"Mode query returned null after onDisplayChanged callback\");\n                        return;\n                    }\n                    if (mode.getModeId() == mRequestedModeId) {\n                        Log.i(TAG, \"Callback for expected change Id= \" + mRequestedModeId);\n                        maybeDoACallback(mode);\n                        doPostModeSetCleanup();\n                    } else {\n                        Log.w(TAG, \"Callback received but not expected mode. Mode= \" + mode + \" expected= \" + mRequestedModeId);\n                    }\n\n                    break;\n                case MODE_CHANGE_TIMEOUT_MSG:\n                    Log.i(TAG, \"Time out without mode change\");\n                    maybeDoACallback(null);\n                    doPostModeSetCleanup();\n                    break;\n                case SEND_CALLBACK_WITH_SUPPLIED_RESULT:\n                    maybeDoACallback((Mode) msg.obj);\n                    if (msg.arg1 == 1) {\n                        doPostModeSetCleanup();\n                    }\n                    break;\n                case INTERSTITIAL_FADED_BROADCAST_MSG:\n                    if (!isInterstitialFadeReceived) {\n                        Log.i(TAG, \"Broadcast for text fade received, Initializing the mode change.\");\n                        isInterstitialFadeReceived = true;\n                        initModeChange(mRequestedModeId, null);\n                    }\n                    break;\n                case INTERSTITIAL_TIMEOUT_MSG:\n                    if (!isInterstitialFadeReceived) {\n                        Log.w(TAG, \"Didn't received any broadcast for interstitial text fade till time out, starting the mode change.\");\n                        isInterstitialFadeReceived = true; //So we don't do another.\n                        initModeChange(mRequestedModeId, null);\n                    }\n                default:\n                    break;\n            }\n        }\n\n        private void maybeDoACallback(Mode mode) {\n            if (this.mCallbackListener != null) {\n                Log.d(TAG, \"Sending callback to listener\");\n                this.mCallbackListener.onModeChanged(mode);\n            } else {\n                Log.d(TAG, \"Can't issue callback as no listener registered\");\n            }\n        }\n\n        /**\n         * Removal of message and unregistering receiver after mode set is done.\n         */\n        @TargetApi(17)\n        private void doPostModeSetCleanup() {\n            if (currentOverlayStatus != OVERLAY_STATE_DISMISSED) {\n                Log.i(TAG, \"Tearing down the overlay Post mode switch attempt.\");\n                currentOverlayStatus = OVERLAY_STATE_DISMISSED;\n                hideOptimizingOverlay();\n            }\n            synchronized (mIsSetModeInProgress) {\n                // need these to be run in order, tell compiler\n                // not to reorder the instructions.\n                this.removeMessages(MODE_CHANGE_TIMEOUT_MSG);\n                if (isReceiversRegistered) {\n                    mDisplayManager.unregisterDisplayListener(mDisplayListener);\n                    unregisterReceiverSilently(overlayStateChangeReceiver);\n                    isReceiversRegistered = false;\n                }\n                this.removeMessages(MODE_CHANGED_MSG);\n                mCallbackListener = null;\n                mIsSetModeInProgress.set(false);\n            }\n        }\n\n        /**\n         * Simple unregister. If error occurs, then swallow it silently.\n         * @param receiver receiver\n         */\n        private void unregisterReceiverSilently(BroadcastReceiver receiver) {\n            try {\n                mContext.unregisterReceiver(receiver);\n            } catch (IllegalArgumentException e) { // swallow 'Receiver not registered'\n                e.printStackTrace();\n            }\n        }\n    }\n\n    /**\n     * Private class for receiving the\n     * {@link com.amazon.tv.notification.modeswitch_overlay.extra.STATE STATE} events.\n     */\n    @SuppressWarnings(\"JavadocReference\")\n    private class OverlayStateChangeReceiver extends BroadcastReceiver {\n        private final int OVERLAY_FADE_COMPLETE_EXTRA = 3;\n\n        @Override\n        public void onReceive(Context context, Intent intent) {\n            currentOverlayStatus = intent.getIntExtra((MODESWITCH_OVERLAY_EXTRA_STATE), -1);\n            if (currentOverlayStatus == OVERLAY_FADE_COMPLETE_EXTRA && !isInterstitialFadeReceived) {\n                mWorkHandler.removeMessages(INTERSTITIAL_TIMEOUT_MSG);\n                mWorkHandler.sendMessage(mWorkHandler.obtainMessage(INTERSTITIAL_FADED_BROADCAST_MSG));\n                Log.i(TAG, \"Got the Interstitial text fade broadcast, Starting the mode change\");\n            }\n        }\n    }\n\n    /**\n     * Returns the current Display mode.\n     *\n     * @return {@link Mode Mode}\n     * that is currently set on the system or NULL if an error occurred.\n     */\n    public Mode getCurrentMode() {\n        Display currentDisplay = getCurrentDisplay();\n        if (currentDisplay == null) {\n            return null;\n        }\n        try {\n            Class<?> classToInvestigate = Class.forName(sDisplayClassName);\n            Method getModeMethod = classToInvestigate.getDeclaredMethod(sGetModeMethodName);\n            Object currentMode = getModeMethod.invoke(currentDisplay);\n            return convertReturnedModeToInternalMode(currentMode);\n        } catch (Exception e) {\n            Log.e(TAG, \"error getting mode\", e);\n        }\n        Log.e(TAG, \"Current Mode is not present in supported Modes\");\n        return null;\n    }\n\n    /**\n     * Utility function to parse android.view.Display,Mode to\n     * {@link Mode mode}\n     *\n     * @param systemMode mode\n     * @return {@link Mode Mode} object\n     * or NULL if an error occurred.\n     */\n    private Mode convertReturnedModeToInternalMode(Object systemMode) {\n        Mode returnedInstance = null;\n        try {\n            Class<?> modeClass = systemMode.getClass();\n            int modeId = (int) modeClass.getDeclaredMethod(sGetModeIdMethodName).invoke(systemMode);\n            int width = (int) modeClass.getDeclaredMethod(sGetPhysicalWidthMethodName).invoke(systemMode);\n            int height = (int) modeClass.getDeclaredMethod(sGetPhysicalHeightMethodName).invoke(systemMode);\n            float refreshRate = (float) modeClass.getDeclaredMethod(sGetRefreshRateMethodName).invoke(systemMode);\n            returnedInstance = mInternalDisplay.getModeInstance(modeId, width, height, refreshRate);\n        } catch (Exception e) {\n            Log.e(TAG, \"error converting\", e);\n        }\n        return returnedInstance;\n    }\n\n    /**\n     * Returns all the supported modes.\n     *\n     * @return An array of\n     * {@link Mode Mode} objects\n     * or NULL if an error occurred.\n     */\n    public Mode[] getSupportedModes() {\n        Mode[] returnedSupportedModes = {};\n        try {\n            Class<?> classToInvestigate = Class.forName(sDisplayClassName);\n            Method getSupportedMethod = classToInvestigate.getDeclaredMethod(sSupportedModesMethodName);\n            Object[] SupportedModes = (Object[]) getSupportedMethod.invoke(getCurrentDisplay());\n            returnedSupportedModes = new Mode[SupportedModes.length];\n            int i = 0;\n            for (Object mode : SupportedModes) {\n                returnedSupportedModes[i++] = convertReturnedModeToInternalMode(mode);\n            }\n        } catch (Exception e) {\n            Log.e(TAG, \"error getting modes\", e);\n        }\n        return returnedSupportedModes;\n    }\n\n    /**\n     * Returns current {@link Display Display} object.\n     * Assumes that the 1st display is the actual display.\n     *\n     * @return {@link Display Display}\n     */\n    @TargetApi(17)\n    private Display getCurrentDisplay() {\n        if (mContext == null) return null;\n        Display[] displays = mDisplayManager.getDisplays();\n        if (displays == null || displays.length == 0) {\n            Log.e(TAG, \"ERROR on device to get the display\");\n            return null;\n        }\n        //assuming the 1st display is the actual display.\n        return displays[0];\n    }\n\n    /**\n     * Change the display mode to the supplied mode.\n     * <p>\n     * Note that you must register a {@link UhdHelperListener listener} using\n     * {@link UhdHelper#registerModeChangeListener(UhdHelperListener) registerModeChangeListener}\n     * to receive the callback for success or failure.\n     * Also, note that this method need to be called from Main UI thread.\n     * <p>\n     * The method will not attempt a mode switch and fail immediately with callback if\n     * 1) Device SDK is less than Android L\n     * 2) Device is Android L but not Amazon AFT* devices.\n     *\n     * @param targetWindow        {@link Window Window} to use for setting the display\n     *                            and call parameters\n     * @param modeId              The desired mode to switch to. Must be a valid mode supported\n     *                            by the platform.\n     * @param allowOverlayDisplay Flag request to allow display overlay on applicable device.\n     */\n    @TargetApi(17)\n    public void setPreferredDisplayModeId(Window targetWindow, int modeId, boolean allowOverlayDisplay) {\n        if (modeId == 0) { // mode is not set\n            return;\n        }\n\n        /*\n         * The Android M preview adds a preferredDisplayModeId to\n         * WindowManager.LayoutParams.preferredDisplayModeId API. A PreferredDisplayModeId can be\n         * set in the LayoutParams of any Window.\n         */\n        String deviceName = Build.MODEL;\n\n        // Let the handler know what listener to use, we will\n        // send null callback in case of an error.\n        mWorkHandler.setCallbackListener(mListener);\n\n        boolean supportedDevice = DisplaySyncHelper.supportsDisplayModeChange();\n\n        //Some basic failure conditions that need handling\n        if (!supportedDevice) {\n            Log.i(TAG, \"Attempt to set preferred Display mode on an unsupported device: \" + deviceName);\n            //send and cleanup\n            mWorkHandler.sendMessage(mWorkHandler.obtainMessage(SEND_CALLBACK_WITH_SUPPLIED_RESULT, 1, 1, null));\n            return;\n        } else if (!DisplaySyncHelper.isAmazonFireTVDevice()) {\n            //We cannot not show interstitial for Non-Amazon Fire TV devices\n            allowOverlayDisplay = false;\n        }\n        if (mIsSetModeInProgress.get()) {\n            Log.e(TAG, \"setPreferredDisplayModeId is already in progress! \" + \"Cannot set another while it is in progress\");\n            //Send but don't cleanup as further processing is expected.\n            mWorkHandler.sendMessage(mWorkHandler.obtainMessage(SEND_CALLBACK_WITH_SUPPLIED_RESULT, null));\n            return;\n        }\n        Mode currentMode = getCurrentMode();\n        if (currentMode == null || currentMode.getModeId() == modeId) {\n            Log.i(TAG, \"Current mode id same as mode id requested or is Null. Aborting.\");\n            //send and cleanup receivers/callback listeners\n            mWorkHandler.sendMessage(mWorkHandler.obtainMessage(SEND_CALLBACK_WITH_SUPPLIED_RESULT, 1, 1, currentMode));\n            return;\n        }\n        //Check if the modeId given is even supported by the system.\n        Mode[] supportedModes = getSupportedModes();\n        boolean isRequestedModeSupported = false;\n        boolean isRequestedModeUhd = false;\n        for (Mode mode : supportedModes) {\n            if (mode.getModeId() == modeId) {\n                isRequestedModeUhd = (mode.getPhysicalHeight() >= HEIGHT_UHD ? true : false);\n                isRequestedModeSupported = true;\n                break;\n            }\n        }\n        if (!isRequestedModeSupported) {\n            Log.e(TAG, \"Requested mode id not among the supported Mode Id.\");\n            //send and cleanup receivers/callback listeners\n            mWorkHandler.sendMessage(mWorkHandler.obtainMessage(SEND_CALLBACK_WITH_SUPPLIED_RESULT, 1, 1, null));\n            return;\n        }\n\n        //We are now going to do setMode call and will do callback for it.\n        mIsSetModeInProgress.set(true);\n        //Let the handler know what modeId onDisplayChanged callback event to look for\n        mWorkHandler.setExpectedMode(modeId);\n        //mContext.registerReceiver(overlayStateChangeReceiver, new IntentFilter(MODESWITCH_OVERLAY_STATE_CHANGED));\n        ContextCompat.registerReceiver(mContext, overlayStateChangeReceiver, new IntentFilter(MODESWITCH_OVERLAY_STATE_CHANGED), ContextCompat.RECEIVER_NOT_EXPORTED);\n        mDisplayListener = new DisplayManager.DisplayListener() {\n            @Override\n            public void onDisplayAdded(int displayId) {\n            }\n\n            @Override\n            public void onDisplayRemoved(int displayId) {\n            }\n\n            @Override\n            public void onDisplayChanged(int displayId) {\n                Display display = mDisplayManager.getDisplay(displayId);\n                if (display != null) {\n                    Log.i(TAG, \"onDisplayChanged. id= \" + displayId + \" \" + display.toString());\n                }\n                mWorkHandler.obtainMessage(MODE_CHANGED_MSG).sendToTarget();\n            }\n        };\n        mDisplayManager.registerDisplayListener(mDisplayListener, mWorkHandler);\n        isReceiversRegistered = true;\n\n        mTargetWindow = targetWindow;\n        showInterstitial = (allowOverlayDisplay && isRequestedModeUhd);\n\n        //Also check if flag is available, otherwise fail and return\n        WindowManager.LayoutParams mWindowAttributes = mTargetWindow.getAttributes();\n        //Check if the field is available or not. This is for early failure.\n        Class<?> cLayoutParams = mWindowAttributes.getClass();\n        Field attributeFlags;\n        try {\n            attributeFlags = cLayoutParams.getDeclaredField(sPreferredDisplayModeIdFieldName);\n        } catch (Exception e) {\n            Log.e(TAG, \"error getting field\", e);\n            //send and cleanup receivers/callback listeners\n            mWorkHandler.sendMessage(mWorkHandler.obtainMessage(SEND_CALLBACK_WITH_SUPPLIED_RESULT, 1, 1, null));\n            return;\n        }\n\n        if (showInterstitial) {\n            isInterstitialFadeReceived = false;\n            showOptimizingOverlay();\n            mWorkHandler.sendMessageDelayed(mWorkHandler.obtainMessage(INTERSTITIAL_TIMEOUT_MSG), SHOW_INTERSTITIAL_TIMEOUT_DELAY_MS);\n        } else {\n            initModeChange(modeId, attributeFlags);\n        }\n    }\n\n    /**\n     * Start the mode change by setting the preferredDisplayModeId field of {@link WindowManager.LayoutParams}\n     */\n    private void initModeChange(int modeId, Field attributeFlagField) {\n        WindowManager.LayoutParams mWindowAttributes = mTargetWindow.getAttributes();\n        try {\n            if (attributeFlagField == null) {\n                Class<?> cLayoutParams = mWindowAttributes.getClass();\n                attributeFlagField = cLayoutParams.getDeclaredField(sPreferredDisplayModeIdFieldName);\n            }\n\n            // ensure mode is not set\n            int currentModeId = attributeFlagField.getInt(mWindowAttributes);\n            if (currentModeId != modeId) {\n                // attempt mode switch\n                attributeFlagField.setInt(mWindowAttributes, modeId);\n                mTargetWindow.setAttributes(mWindowAttributes);\n            }\n        } catch (Exception e) {\n            Log.e(TAG, \"error getting field\", e);\n            // send and cleanup receivers/callback listeners\n            mWorkHandler.sendMessage(mWorkHandler.obtainMessage(SEND_CALLBACK_WITH_SUPPLIED_RESULT, 1, 1, null));\n            return;\n        }\n        // We assume that the mode change is not instantaneous and will send the onDisplayChanged callback.\n        // Start the clock on the mode change timeout\n        mWorkHandler.sendMessageDelayed(mWorkHandler.obtainMessage(MODE_CHANGE_TIMEOUT_MSG), SET_MODE_TIMEOUT_DELAY_MS);\n    }\n\n    /**\n     * Send the broadcast to show overlay display\n     */\n    private void showOptimizingOverlay() {\n        final Intent overlayIntent = new Intent(MODESWITCH_OVERLAY_ENABLE);\n        mContext.sendBroadcast(overlayIntent);\n        Log.i(TAG, \"Sending the broadcast to display overlay\");\n    }\n\n    /**\n     * Send the broadcast to hide overlay display if showing.\n     */\n    private void hideOptimizingOverlay() {\n        final Intent overlayIntent = new Intent(MODESWITCH_OVERLAY_DISABLE);\n        mContext.sendBroadcast(overlayIntent);\n        Log.i(TAG, \"Sending the broadcast to hide display overlay\");\n\n    }\n\n    /**\n     * Register a {@link UhdHelperListener listener} to be notified of result\n     * of the {@link UhdHelper#setPreferredDisplayModeId(Window, int, boolean) setPreferredDisplayModeId}\n     * call.\n     *\n     * @param listener that will receive the result of the callback.\n     */\n    public void registerModeChangeListener(UhdHelperListener listener) {\n        mListener = listener;\n    }\n\n    /**\n     * Register the {@link UhdHelperListener listener}\n     *\n     * @param listener\n     */\n    public void unregisterDisplayModeChangeListener(UhdHelperListener listener) {\n        mListener = null;\n    }\n\n    public static String toResolution(Mode mode) {\n        if (mode == null) {\n            return null;\n        }\n\n        return String.format(\"%sx%s@%s\", mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());\n    }\n\n    public static Mode getCurrentMode(Context context) {\n        if (Build.VERSION.SDK_INT < 23) {\n            WindowManager wm = (WindowManager) context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE); // the results will be higher than using the activity context object or the getWindowManager() shortcut\n            Display display = wm.getDefaultDisplay();\n\n            if (display == null) {\n                return null;\n            }\n\n            Point size = new Point();\n            display.getSize(size);\n\n            return new Mode(0, size.x, size.y, display.getRefreshRate());\n        } else {\n            Display display = getCurrentDisplay(context);\n\n            if (display == null) {\n                return null;\n            }\n\n            Display.Mode mode = display.getMode();\n\n            return new Mode(mode.getModeId(), mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());\n        }\n    }\n\n    @TargetApi(17)\n    private static Display getCurrentDisplay(Context context) {\n        DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);\n        if (displayManager == null)\n            return null;\n        Display[] displays = displayManager.getDisplays();\n        if (displays == null || displays.length == 0) {\n            return null;\n        }\n        //assuming the 1st display is the actual display.\n        return displays[0];\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/autoframerate/internal/UhdHelperListener.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.autoframerate.internal;\n\nimport android.view.Window;\n\n/**\n * The interface that must be implemented and registered\n * with {@link UhdHelper#registerModeChangeListener(UhdHelperListener) registerListener}\n * to find out the result of requested mode change.\n * <p>\n * Callback will be issued on the Main/UI thread of the application.\n *\n * To unregister the listener, use\n * {@link UhdHelper#unregisterDisplayModeChangeListener(UhdHelperListener) unregisterDisplayModeChangeListener}\n */\npublic interface UhdHelperListener {\n    /**\n     * Callback containing the result of the mode change after\n     * {@link UhdHelper#setPreferredDisplayModeId(Window, int,boolean) setPreferredDisplayModeId}\n     * returns a true.\n     *\n     * @param mode The {@link DisplayHolder.Mode Mode} object containing\n     *             the mode switched to OR NULL if there was a timeout\n     *             or internal error while changing the mode.\n     */\n    void onModeChanged(DisplayHolder.Mode mode);\n\n}\n\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/ExoMediaSourceFactory.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport android.net.Uri;\r\nimport android.text.TextUtils;\r\nimport androidx.annotation.NonNull;\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.ext.cronet.CronetDataSourceFactory;\r\nimport com.google.android.exoplayer2.ext.cronet.CronetEngineWrapper;\r\nimport com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory;\r\nimport com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;\r\nimport com.google.android.exoplayer2.source.ExtractorMediaSource;\r\nimport com.google.android.exoplayer2.source.MediaSource;\r\nimport com.google.android.exoplayer2.source.dash.DashChunkSource;\r\nimport com.google.android.exoplayer2.source.dash.DashMediaSource;\r\nimport com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;\r\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifest;\r\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;\r\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifestParser2;\r\nimport com.google.android.exoplayer2.source.dash.manifest.Period;\r\nimport com.google.android.exoplayer2.source.dash.manifest.ProgramInformation;\r\nimport com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;\r\nimport com.google.android.exoplayer2.source.hls.HlsMediaSource;\r\nimport com.google.android.exoplayer2.source.sabr.DefaultSabrChunkSource;\r\nimport com.google.android.exoplayer2.source.sabr.SabrChunkSource;\r\nimport com.google.android.exoplayer2.source.sabr.SabrMediaSource;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.SabrManifest;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.SabrManifestParser;\r\nimport com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;\r\nimport com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;\r\nimport com.google.android.exoplayer2.upstream.DataSource;\r\nimport com.google.android.exoplayer2.upstream.DataSource.Factory;\r\nimport com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;\r\nimport com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;\r\nimport com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;\r\nimport com.google.android.exoplayer2.upstream.HttpDataSource;\r\nimport com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;\r\nimport com.google.android.exoplayer2.util.Util;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemFormatInfo;\r\nimport com.liskovsoft.sharedutils.cronet.CronetManager;\r\nimport com.liskovsoft.sharedutils.helpers.FileHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.okhttp.OkHttpManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.errors.DashDefaultLoadErrorHandlingPolicy;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.errors.SabrDefaultLoadErrorHandlingPolicy;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.errors.TrackErrorFixer;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.googlecommon.common.helpers.DefaultHeaders;\r\n\r\nimport java.io.IOException;\r\nimport java.io.InputStream;\r\nimport java.util.List;\r\nimport java.util.concurrent.Executors;\r\n\r\npublic class ExoMediaSourceFactory {\r\n    private static final String TAG = ExoMediaSourceFactory.class.getSimpleName();\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    //private static ExoMediaSourceFactory sInstance;\r\n    private static final int MAX_SEGMENTS_PER_LOAD = 1; // default - 1 (1-5)\r\n    private static final String USER_AGENT = DefaultHeaders.APP_USER_AGENT;\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();\r\n    private final Context mContext;\r\n    private static final Uri DASH_MANIFEST_URI = Uri.parse(\"https://example.com/test.mpd\");\r\n    private static final String DASH_MANIFEST_EXTENSION = \"mpd\";\r\n    private static final String HLS_PLAYLIST_EXTENSION = \"m3u8\";\r\n    private static final boolean USE_BANDWIDTH_METER = false;\r\n    private TrackErrorFixer mTrackErrorFixer;\r\n    private Factory mMediaDataSourceFactory;\r\n\r\n    public ExoMediaSourceFactory(Context context) {\r\n        mContext = context;\r\n    }\r\n\r\n    public MediaSource fromSabrFormatInfo(MediaItemFormatInfo formatInfo) {\r\n        return buildSabrMediaSource(formatInfo);\r\n    }\r\n\r\n    public MediaSource fromDashFormatInfo(MediaItemFormatInfo formatInfo) {\r\n        return buildDashMediaSource(formatInfo);\r\n    }\r\n\r\n    public MediaSource fromDashManifest(InputStream dashManifest) {\r\n        return buildMPDMediaSource(DASH_MANIFEST_URI, dashManifest);\r\n    }\r\n\r\n    public MediaSource fromDashManifestUrl(String dashManifestUrl) {\r\n        return buildMediaSource(Uri.parse(dashManifestUrl), DASH_MANIFEST_EXTENSION);\r\n    }\r\n\r\n    public MediaSource fromHlsPlaylist(String hlsPlaylist) {\r\n        return buildMediaSource(Uri.parse(hlsPlaylist), HLS_PLAYLIST_EXTENSION);\r\n    }\r\n\r\n    public MediaSource fromUrlList(List<String> urlList) {\r\n        MediaSource[] mediaSources = new MediaSource[urlList.size()];\r\n\r\n        for (int i = 0; i < urlList.size(); i++) {\r\n            mediaSources[i] = buildMediaSource(Uri.parse(urlList.get(i)), null);\r\n        }\r\n\r\n        //return mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); // or playlist\r\n        return mediaSources[0]; // item with max resolution\r\n    }\r\n\r\n    /**\r\n     * Returns a new DataSource factory.\r\n     *\r\n     * @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new\r\n     *                          DataSource factory.\r\n     * @return A new DataSource factory.\r\n     */\r\n    private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {\r\n        DefaultBandwidthMeter bandwidthMeter = useBandwidthMeter ? BANDWIDTH_METER : null;\r\n        return new DefaultDataSourceFactory(mContext, bandwidthMeter, buildHttpDataSourceFactory(useBandwidthMeter));\r\n    }\r\n\r\n    /**\r\n     * Returns a new HttpDataSource factory.\r\n     *\r\n     * @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new\r\n     *                          DataSource factory.\r\n     * @return A new HttpDataSource factory.\r\n     */\r\n    private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) {\r\n        PlayerTweaksData tweaksData = PlayerTweaksData.instance(mContext);\r\n        int source = tweaksData.getPlayerDataSource();\r\n        DefaultBandwidthMeter bandwidthMeter = useBandwidthMeter ? BANDWIDTH_METER : null;\r\n        return source == PlayerTweaksData.PLAYER_DATA_SOURCE_OKHTTP ? buildOkHttpDataSourceFactory(bandwidthMeter) :\r\n                        source == PlayerTweaksData.PLAYER_DATA_SOURCE_CRONET && CronetManager.getEngine(mContext) != null ? buildCronetDataSourceFactory(bandwidthMeter) :\r\n                                buildDefaultHttpDataSourceFactory(bandwidthMeter);\r\n    }\r\n\r\n    @SuppressWarnings(\"deprecation\")\r\n    private MediaSource buildMediaSource(Uri uri, String overrideExtension) {\r\n        int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType(\".\" + overrideExtension);\r\n        switch (type) {\r\n            case C.TYPE_SS:\r\n                SsMediaSource ssSource =\r\n                        new SsMediaSource.Factory(\r\n                                getSsChunkSourceFactory(),\r\n                                getMediaDataSourceFactory()\r\n                        )\r\n                                .createMediaSource(uri);\r\n                if (mTrackErrorFixer != null) {\r\n                    ssSource.addEventListener(Utils.sHandler, mTrackErrorFixer);\r\n                }\r\n                return ssSource;\r\n            case C.TYPE_DASH:\r\n                DashMediaSource dashSource =\r\n                        new DashMediaSource.Factory(\r\n                                getDashChunkSourceFactory(),\r\n                                getMediaDataSourceFactory()\r\n                        )\r\n                                .setManifestParser(new LiveDashManifestParser()) // Don't make static! Need state reset for each live source.\r\n                                .setLoadErrorHandlingPolicy(new DashDefaultLoadErrorHandlingPolicy())\r\n                                .createMediaSource(uri);\r\n                if (mTrackErrorFixer != null) {\r\n                    dashSource.addEventListener(Utils.sHandler, mTrackErrorFixer);\r\n                }\r\n                return dashSource;\r\n            case C.TYPE_HLS:\r\n                HlsMediaSource hlsSource = new HlsMediaSource.Factory(getMediaDataSourceFactory()).createMediaSource(uri);\r\n                if (mTrackErrorFixer != null) {\r\n                    hlsSource.addEventListener(Utils.sHandler, mTrackErrorFixer);\r\n                }\r\n                return hlsSource;\r\n            case C.TYPE_OTHER:\r\n                ExtractorMediaSource extractorSource = new ExtractorMediaSource.Factory(getMediaDataSourceFactory())\r\n                        .setExtractorsFactory(new DefaultExtractorsFactory())\r\n                        .createMediaSource(uri);\r\n                if (mTrackErrorFixer != null) {\r\n                    extractorSource.addEventListener(Utils.sHandler, mTrackErrorFixer);\r\n                }\r\n                return extractorSource;\r\n            default: {\r\n                throw new IllegalStateException(\"Unsupported type: \" + type);\r\n            }\r\n        }\r\n    }\r\n\r\n    private MediaSource buildSabrMediaSource(MediaItemFormatInfo formatInfo) {\r\n        // Are you using FrameworkSampleSource or ExtractorSampleSource when you build your player?\r\n        SabrMediaSource sabrSource = new SabrMediaSource.Factory(\r\n                getSabrChunkSourceFactory(),\r\n                null\r\n        )\r\n                .setLoadErrorHandlingPolicy(new SabrDefaultLoadErrorHandlingPolicy())\r\n                .createMediaSource(getSabrManifest(formatInfo));\r\n        if (mTrackErrorFixer != null) {\r\n            sabrSource.addEventListener(Utils.sHandler, mTrackErrorFixer);\r\n        }\r\n        return sabrSource;\r\n    }\r\n\r\n    private MediaSource buildDashMediaSource(MediaItemFormatInfo formatInfo) {\r\n        // Are you using FrameworkSampleSource or ExtractorSampleSource when you build your player?\r\n        DashMediaSource dashSource = new DashMediaSource.Factory(\r\n                getDashChunkSourceFactory(),\r\n                null\r\n        )\r\n                .setLoadErrorHandlingPolicy(new DashDefaultLoadErrorHandlingPolicy())\r\n                .createMediaSource(getManifest(formatInfo));\r\n        if (mTrackErrorFixer != null) {\r\n            dashSource.addEventListener(Utils.sHandler, mTrackErrorFixer);\r\n        }\r\n        return dashSource;\r\n    }\r\n\r\n    private MediaSource buildMPDMediaSource(Uri uri, InputStream mpdContent) {\r\n        // Are you using FrameworkSampleSource or ExtractorSampleSource when you build your player?\r\n        DashMediaSource dashSource = new DashMediaSource.Factory(\r\n                getDashChunkSourceFactory(),\r\n                null\r\n        )\r\n                .setLoadErrorHandlingPolicy(new DashDefaultLoadErrorHandlingPolicy())\r\n                .createMediaSource(getManifest(uri, mpdContent));\r\n        if (mTrackErrorFixer != null) {\r\n            dashSource.addEventListener(Utils.sHandler, mTrackErrorFixer);\r\n        }\r\n        return dashSource;\r\n    }\r\n\r\n    private MediaSource buildMPDMediaSource(Uri uri, String mpdContent) {\r\n        if (mpdContent == null || mpdContent.isEmpty()) {\r\n            Log.e(TAG, \"Can't build media source. MpdContent is null or empty. \" + mpdContent);\r\n            return null;\r\n        }\r\n\r\n        // Are you using FrameworkSampleSource or ExtractorSampleSource when you build your player?\r\n        DashMediaSource dashSource = new DashMediaSource.Factory(\r\n                new DefaultDashChunkSource.Factory(getMediaDataSourceFactory()),\r\n                null\r\n        )\r\n                .createMediaSource(getManifest(uri, mpdContent));\r\n        if (mTrackErrorFixer != null) {\r\n            dashSource.addEventListener(Utils.sHandler, mTrackErrorFixer);\r\n        }\r\n        return dashSource;\r\n    }\r\n\r\n    private SabrManifest getSabrManifest(MediaItemFormatInfo formatInfo) {\r\n        SabrManifestParser parser = new SabrManifestParser();\r\n        return parser.parse(formatInfo);\r\n    }\r\n\r\n    private DashManifest getManifest(MediaItemFormatInfo formatInfo) {\r\n        DashManifestParser2 parser = new DashManifestParser2();\r\n        return parser.parse(formatInfo);\r\n    }\r\n\r\n    private DashManifest getManifest(Uri uri, InputStream mpdContent) {\r\n        DashManifestParser parser = new StaticDashManifestParser();\r\n        DashManifest result;\r\n        try {\r\n            result = parser.parse(uri, mpdContent);\r\n        } catch (IOException e) {\r\n            throw new IllegalStateException(\"Malformed mpd file:\\n\" + mpdContent, e);\r\n        }\r\n        return result;\r\n    }\r\n\r\n    private DashManifest getManifest(Uri uri, String mpdContent) {\r\n        DashManifestParser parser = new StaticDashManifestParser();\r\n        DashManifest result;\r\n        try {\r\n            result = parser.parse(uri, FileHelpers.toStream(mpdContent));\r\n        } catch (IOException e) {\r\n            throw new IllegalStateException(\"Malformed mpd file:\\n\" + mpdContent, e);\r\n        }\r\n        return result;\r\n    }\r\n\r\n    /**\r\n     * Use OkHttp for networking\r\n     */\r\n    private HttpDataSource.Factory buildOkHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {\r\n        OkHttpDataSourceFactory dataSourceFactory = new OkHttpDataSourceFactory(OkHttpManager.instance().getClient(), USER_AGENT,\r\n                bandwidthMeter);\r\n        addCommonHeaders(dataSourceFactory);\r\n        return dataSourceFactory;\r\n    }\r\n\r\n    private HttpDataSource.Factory buildCronetDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {\r\n        CronetDataSourceFactory dataSourceFactory =\r\n                new CronetDataSourceFactory(\r\n                        new CronetEngineWrapper(CronetManager.getEngine(mContext)),\r\n                        Executors.newSingleThreadExecutor(),\r\n                        null,\r\n                        bandwidthMeter,\r\n                        (int) OkHttpManager.getConnectTimeoutMs(),\r\n                        (int) OkHttpManager.getReadTimeoutMs(),\r\n                        true,\r\n                        USER_AGENT);\r\n        addCommonHeaders(dataSourceFactory);\r\n        return dataSourceFactory;\r\n    }\r\n\r\n    /**\r\n     * Use built-in component for networking\r\n     */\r\n    private HttpDataSource.Factory buildDefaultHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {\r\n        DefaultHttpDataSourceFactory dataSourceFactory = new DefaultHttpDataSourceFactory(\r\n                USER_AGENT, bandwidthMeter, (int) OkHttpManager.getConnectTimeoutMs(),\r\n                (int) OkHttpManager.getReadTimeoutMs(), true); // allowCrossProtocolRedirects = true\r\n\r\n        addCommonHeaders(dataSourceFactory); // cause troubles for some users\r\n        return dataSourceFactory;\r\n    }\r\n\r\n    private static void addCommonHeaders(BaseFactory dataSourceFactory) {\r\n        // Doesn't work\r\n        // Trying to fix 429 error (too many requests)\r\n        //String authorization = RetrofitOkHttpHelper.getAuthHeaders().get(\"Authorization\");\r\n        //\r\n        //if (authorization != null) {\r\n        //    dataSourceFactory.getDefaultRequestProperties().set(\"Authorization\", authorization);\r\n        //}\r\n\r\n        //HeaderManager headerManager = new HeaderManager(context);\r\n        //HashMap<String, String> headers = headerManager.getHeaders();\r\n\r\n        // NOTE: \"Accept-Encoding\" should not be set manually (gzip is added by default).\r\n\r\n        //for (String header : headers.keySet()) {\r\n        //    if (EXO_HEADERS.contains(header)) {\r\n        //        dataSourceFactory.getDefaultRequestProperties().set(header, headers.get(header));\r\n        //    }\r\n        //}\r\n\r\n        // Emulate browser request\r\n        //dataSourceFactory.getDefaultRequestProperties().set(\"accept\", \"*/*\");\r\n        //dataSourceFactory.getDefaultRequestProperties().set(\"accept-encoding\", \"identity\"); // Next won't work: gzip, deflate, br\r\n        //dataSourceFactory.getDefaultRequestProperties().set(\"accept-language\", \"en-US,en;q=0.9\");\r\n        //dataSourceFactory.getDefaultRequestProperties().set(\"dnt\", \"1\");\r\n        //dataSourceFactory.getDefaultRequestProperties().set(\"origin\", \"https://www.youtube.com\");\r\n        //dataSourceFactory.getDefaultRequestProperties().set(\"referer\", \"https://www.youtube.com/\");\r\n        //dataSourceFactory.getDefaultRequestProperties().set(\"sec-fetch-dest\", \"empty\");\r\n        //dataSourceFactory.getDefaultRequestProperties().set(\"sec-fetch-mode\", \"cors\");\r\n        //dataSourceFactory.getDefaultRequestProperties().set(\"sec-fetch-site\", \"cross-site\");\r\n\r\n        // WARN: Compression won't work with legacy streams.\r\n        // \"Accept-Encoding\" should not be set manually (gzip is added by default).\r\n        // Otherwise you should do decompression yourself.\r\n        // Source: https://stackoverflow.com/questions/18898959/httpurlconnection-not-decompressing-gzip/42346308#42346308\r\n        //dataSourceFactory.getDefaultRequestProperties().set(\"Accept-Encoding\", AppConstants.ACCEPT_ENCODING_DEFAULT);\r\n    }\r\n\r\n    public void setTrackErrorFixer(TrackErrorFixer trackErrorFixer) {\r\n        mTrackErrorFixer = trackErrorFixer;\r\n    }\r\n\r\n    public void release() {\r\n        mMediaDataSourceFactory = null;\r\n    }\r\n\r\n    @NonNull\r\n    private DefaultSsChunkSource.Factory getSsChunkSourceFactory() {\r\n        return new DefaultSsChunkSource.Factory(getMediaDataSourceFactory());\r\n    }\r\n\r\n    @NonNull\r\n    private SabrChunkSource.Factory getSabrChunkSourceFactory() {\r\n        return new DefaultSabrChunkSource.Factory(getMediaDataSourceFactory(), MAX_SEGMENTS_PER_LOAD);\r\n    }\r\n\r\n    @NonNull\r\n    private DashChunkSource.Factory getDashChunkSourceFactory() {\r\n        return new DefaultDashChunkSource.Factory(getMediaDataSourceFactory(), MAX_SEGMENTS_PER_LOAD);\r\n    }\r\n\r\n    private Factory getMediaDataSourceFactory() {\r\n        if (mMediaDataSourceFactory == null) {\r\n            mMediaDataSourceFactory = buildDataSourceFactory(USE_BANDWIDTH_METER);\r\n        }\r\n\r\n        return mMediaDataSourceFactory;\r\n    }\r\n\r\n    // EXO: 2.10 - 2.12\r\n    private static class StaticDashManifestParser extends DashManifestParser {\r\n        @Override\r\n        protected DashManifest buildMediaPresentationDescription(\r\n                long availabilityStartTime,\r\n                long durationMs,\r\n                long minBufferTimeMs,\r\n                boolean dynamic,\r\n                long minUpdateTimeMs,\r\n                long timeShiftBufferDepthMs,\r\n                long suggestedPresentationDelayMs,\r\n                long publishTimeMs,\r\n                ProgramInformation programInformation,\r\n                UtcTimingElement utcTiming,\r\n                Uri location,\r\n                List<Period> periods) {\r\n            return new DashManifest(\r\n                    availabilityStartTime,\r\n                    durationMs,\r\n                    minBufferTimeMs,\r\n                    false,\r\n                    minUpdateTimeMs,\r\n                    timeShiftBufferDepthMs,\r\n                    suggestedPresentationDelayMs,\r\n                    publishTimeMs,\r\n                    programInformation,\r\n                    utcTiming,\r\n                    location,\r\n                    periods);\r\n        }\r\n    }\r\n\r\n    // EXO: 2.13\r\n    //private static class StaticDashManifestParser extends DashManifestParser {\r\n    //    @Override\r\n    //    protected DashManifest buildMediaPresentationDescription(\r\n    //            long availabilityStartTime,\r\n    //            long durationMs,\r\n    //            long minBufferTimeMs,\r\n    //            boolean dynamic,\r\n    //            long minUpdateTimeMs,\r\n    //            long timeShiftBufferDepthMs,\r\n    //            long suggestedPresentationDelayMs,\r\n    //            long publishTimeMs,\r\n    //            @Nullable ProgramInformation programInformation,\r\n    //            @Nullable UtcTimingElement utcTiming,\r\n    //            @Nullable ServiceDescriptionElement serviceDescription,\r\n    //            @Nullable Uri location,\r\n    //            List<Period> periods) {\r\n    //        return new DashManifest(\r\n    //                availabilityStartTime,\r\n    //                durationMs,\r\n    //                minBufferTimeMs,\r\n    //                false,\r\n    //                minUpdateTimeMs,\r\n    //                timeShiftBufferDepthMs,\r\n    //                suggestedPresentationDelayMs,\r\n    //                publishTimeMs,\r\n    //                programInformation,\r\n    //                utcTiming,\r\n    //                serviceDescription,\r\n    //                location,\r\n    //                periods);\r\n    //    }\r\n    //}\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/LiveDashManifestParser.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer;\r\n\r\nimport android.net.Uri;\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.source.dash.DashSegmentIndex;\r\nimport com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;\r\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifest;\r\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;\r\nimport com.google.android.exoplayer2.source.dash.manifest.Descriptor;\r\nimport com.google.android.exoplayer2.source.dash.manifest.Period;\r\nimport com.google.android.exoplayer2.source.dash.manifest.RangedUri;\r\nimport com.google.android.exoplayer2.source.dash.manifest.Representation;\r\nimport com.google.android.exoplayer2.source.dash.manifest.Representation.MultiSegmentRepresentation;\r\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.MultiSegmentBase;\r\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentList;\r\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.querystringparser.UrlQueryString;\r\nimport com.liskovsoft.sharedutils.querystringparser.UrlQueryStringFactory;\r\n\r\nimport java.io.IOException;\r\nimport java.io.InputStream;\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\n/**\r\n * Supported ExoPlayer versions: 2.10.6\r\n */\r\n@SuppressWarnings(\"unchecked\")\r\npublic class LiveDashManifestParser extends DashManifestParser {\r\n    private static final String TAG = LiveDashManifestParser.class.getSimpleName();\r\n    // Should be close to zero but not zero to increase buffer size to 30 sec (Radio Record).\r\n    // Higher values may produce 'url not working' error.\r\n    private static final long MAX_LIVE_STREAM_LENGTH_MS = 30 * 1_000;\r\n    // Usually gaming streams. 10 hrs max.\r\n    private static final long MAX_PAST_STREAM_LENGTH_MS = 12 * 60 * 60 * 1_000;\r\n    private static final long MAX_NEW_STREAM_LENGTH_MS = 30 * 1_000;\r\n    private DashManifest mOldManifest;\r\n    private long mOldSegmentNum;\r\n\r\n    @Override\r\n    public DashManifest parse(Uri uri, InputStream inputStream) throws IOException {\r\n        DashManifest manifest = super.parse(uri, inputStream);\r\n\r\n        //Log.d(TAG, \"Parse start: \" + System.currentTimeMillis());\r\n\r\n        appendManifest(manifest);\r\n\r\n        //Log.d(TAG, \"Parse end: \" + System.currentTimeMillis());\r\n\r\n        return mOldManifest;\r\n    }\r\n\r\n    private void appendManifest(DashManifest newManifest) {\r\n        if (newManifest == null) {\r\n            return;\r\n        }\r\n\r\n        // Optimize ram usage on short streams (< 2 hours)\r\n        if (getFirstSegmentNum(newManifest) == 0) { // Short stream. No need to do something special.\r\n            mOldManifest = newManifest;\r\n            // Below line will be needed later (> 2 hours), when the stream no longer starts from 0 segment\r\n            mOldSegmentNum = getLastSegmentNum(newManifest);\r\n            return;\r\n        }\r\n\r\n        // Even 4+ hours streams could have different length.\r\n        // So, we should take into account last segment num instead of first one.\r\n        long newSegmentNum = getLastSegmentNum(newManifest);\r\n\r\n        if (mOldManifest == null) {\r\n            //recreateMissingSegments(newManifest);\r\n\r\n            //newManifest.availabilityStartTimeMs = -1;\r\n            Period newPeriod = newManifest.getPeriod(0);\r\n            // TODO: modified\r\n            //newPeriod.startMs = 0;\r\n            Helpers.setField(newPeriod, \"startMs\", 0);\r\n            mOldSegmentNum = newSegmentNum;\r\n\r\n            for (int i = 0; i < newPeriod.adaptationSets.size(); i++) {\r\n                for (int j = 0; j < newPeriod.adaptationSets.get(i).representations.size(); j++) {\r\n                    MultiSegmentRepresentation representation = (MultiSegmentRepresentation) newPeriod.adaptationSets.get(i).representations.get(j);\r\n                    //representation.presentationTimeOffsetUs = 0;\r\n\r\n                    // TODO: modified\r\n                    //SegmentList newSegmentList = (SegmentList) representation.segmentBase;\r\n                    SegmentList newSegmentList = (SegmentList) Helpers.getField(representation, \"segmentBase\");\r\n                    // TODO: modified\r\n                    //newSegmentList.presentationTimeOffset = 0;\r\n                    Helpers.setField(newSegmentList, \"presentationTimeOffset\", 0);\r\n                    // TODO: modified\r\n                    //newSegmentList.startNumber = 0;\r\n                    Helpers.setField(newSegmentList, \"startNumber\", 0);\r\n                }\r\n            }\r\n\r\n            mOldManifest = newManifest;\r\n\r\n            return;\r\n        }\r\n\r\n        //long oldSegmentNum = getFirstSegmentNum(mManifest);\r\n\r\n        Period oldPeriod = mOldManifest.getPeriod(0);\r\n        Period newPeriod = newManifest.getPeriod(0);\r\n\r\n        for (int i = 0; i < oldPeriod.adaptationSets.size(); i++) {\r\n            for (int j = 0; j < oldPeriod.adaptationSets.get(i).representations.size(); j++) {\r\n                appendRepresentation(\r\n                        oldPeriod.adaptationSets.get(i).representations.get(j),\r\n                        newPeriod.adaptationSets.get(i).representations.get(j),\r\n                        newSegmentNum - mOldSegmentNum\r\n                );\r\n            }\r\n        }\r\n\r\n        mOldSegmentNum = newSegmentNum;\r\n\r\n        //mManifest.timeShiftBufferDepthMs += (newSegmentNum - oldSegmentNum) * 5_000;\r\n    }\r\n\r\n    private static void appendRepresentation(Representation oldRepresentation, Representation newRepresentation, long segmentNumShift) {\r\n        if (segmentNumShift <= 0) {\r\n            return;\r\n        }\r\n\r\n        MultiSegmentRepresentation oldMultiRepresentation = (MultiSegmentRepresentation) oldRepresentation;\r\n        MultiSegmentRepresentation newMultiRepresentation = (MultiSegmentRepresentation) newRepresentation;\r\n\r\n        // TODO: modified\r\n        //SegmentList oldSegmentList = (SegmentList) oldRepresentation.segmentBase;\r\n        SegmentList oldSegmentList = (SegmentList) Helpers.getField(oldMultiRepresentation, \"segmentBase\");\r\n        // TODO: modified\r\n        //SegmentList newSegmentList = (SegmentList) newRepresentation.segmentBase;\r\n        SegmentList newSegmentList = (SegmentList) Helpers.getField(newMultiRepresentation, \"segmentBase\");\r\n\r\n        // TODO: modified\r\n        //List<RangedUri> oldMediaSegments = oldSegmentList.mediaSegments;\r\n        List<RangedUri> oldMediaSegments = (List<RangedUri>) Helpers.getField(oldSegmentList, \"mediaSegments\");\r\n        // TODO: modified\r\n        //List<RangedUri> newMediaSegments = newSegmentList.mediaSegments;\r\n        List<RangedUri> newMediaSegments = (List<RangedUri>) Helpers.getField(newSegmentList, \"mediaSegments\");\r\n\r\n        oldMediaSegments.addAll(\r\n                newMediaSegments.subList(newMediaSegments.size() - (int) segmentNumShift, newMediaSegments.size()));\r\n\r\n        // TODO: modified\r\n        //List<SegmentTimelineElement> oldSegmentTimeline = oldSegmentList.segmentTimeline;\r\n        List<SegmentTimelineElement> oldSegmentTimeline = (List<SegmentTimelineElement>) Helpers.getField(oldSegmentList, \"segmentTimeline\");\r\n\r\n        // segmentTimeline is the same for all segments\r\n        if (oldMediaSegments.size() != oldSegmentTimeline.size()) {\r\n            SegmentTimelineElement lastTimeline = oldSegmentTimeline.get(oldSegmentTimeline.size() - 1);\r\n            // TODO: modified\r\n            //long lastTimelineDuration = lastTimeline.duration;\r\n            long lastTimelineDuration = (Long) Helpers.getField(lastTimeline, \"duration\");\r\n            // TODO: modified\r\n            //long lastTimelineStartTime = lastTimeline.startTime;\r\n            long lastTimelineStartTime = (Long) Helpers.getField(lastTimeline, \"startTime\");\r\n\r\n            for (int i = 1; i <= segmentNumShift; i++) {\r\n                oldSegmentTimeline.add(new SegmentTimelineElement(lastTimelineStartTime + (lastTimelineDuration * i), lastTimelineDuration));\r\n            }\r\n\r\n            //oldSegmentTimeline.addAll(\r\n            //        newSegmentList.segmentTimeline.subList(newSegmentList.segmentTimeline.size() - (int) segmentNumShift - 1, newSegmentList.segmentTimeline.size()));\r\n        }\r\n    }\r\n\r\n    private static void recreateMissingSegments(DashManifest manifest) {\r\n        if (manifest == null) {\r\n            return;\r\n        }\r\n\r\n        long minUpdatePeriodMs = (long) Helpers.getField(manifest, \"minUpdatePeriodMs\");\r\n        long timeShiftBufferDepthMs = (long) Helpers.getField(manifest, \"timeShiftBufferDepthMs\"); // active live stream\r\n        long durationMs = (long) Helpers.getField(manifest, \"durationMs\"); // past live stream\r\n        long firstSegmentNum = getFirstSegmentNum(manifest);\r\n        long firstSegmentDurationMs = getFirstSegmentDurationMs(manifest);\r\n        long currentSegmentCount = getSegmentCount(manifest);\r\n        if (minUpdatePeriodMs <= 0) { // past live stream\r\n            // May has different length 5_000 (4hrs) or 2_000 (2hrs)\r\n            minUpdatePeriodMs = durationMs / (currentSegmentCount - 1) / 10 * 10; // Round ending digits\r\n        }\r\n\r\n        if (minUpdatePeriodMs != firstSegmentDurationMs) { // variable segment timeline (unpredictable)\r\n            return;\r\n        }\r\n\r\n        boolean isNewStream = firstSegmentNum < 10_000 && currentSegmentCount > 3;\r\n        boolean isPastStream = durationMs > 0 && currentSegmentCount > 3;\r\n        long maxSegmentsCount = (isPastStream ? MAX_PAST_STREAM_LENGTH_MS :\r\n                                    isNewStream ? MAX_NEW_STREAM_LENGTH_MS : MAX_LIVE_STREAM_LENGTH_MS) / minUpdatePeriodMs;\r\n        long recreateSegmentCount = Math.min(firstSegmentNum, maxSegmentsCount - currentSegmentCount);\r\n\r\n        if (recreateSegmentCount <= 0) {\r\n            return;\r\n        }\r\n\r\n        // 2_000 Ms streams has variable limit values in url (that is unpredictable)\r\n        if (minUpdatePeriodMs <= 2_000) {\r\n            return; // url won't work on small (2_000Ms) segments\r\n        }\r\n\r\n        // Skip past streams that are truncated (truncated streams have a problems)\r\n        if ((isNewStream || isPastStream) && firstSegmentNum > recreateSegmentCount) {\r\n            return;\r\n        }\r\n\r\n        if (timeShiftBufferDepthMs > 0) { // active live stream\r\n            Helpers.setField(manifest, \"timeShiftBufferDepthMs\", timeShiftBufferDepthMs + (recreateSegmentCount * minUpdatePeriodMs));\r\n        } else { // past live stream\r\n            Helpers.setField(manifest, \"durationMs\", durationMs + (recreateSegmentCount * minUpdatePeriodMs));\r\n        }\r\n\r\n        Period oldPeriod = manifest.getPeriod(0);\r\n\r\n        for (int i = 0; i < oldPeriod.adaptationSets.size(); i++) {\r\n            AdaptationSet adaptationSet = oldPeriod.adaptationSets.get(i);\r\n            lazyRecreateRepresentations(adaptationSet, recreateSegmentCount, minUpdatePeriodMs);\r\n            //List<Representation> representations = adaptationSet.representations;\r\n            //for (int j = 0; j < representations.size(); j++) {\r\n            //    Representation oldRepresentation = representations.get(j);\r\n            //    recreateRepresentation(oldRepresentation, recreateSegmentCount, minUpdatePeriodMs);\r\n            //}\r\n        }\r\n    }\r\n\r\n    private static void recreateRepresentation(Representation oldRepresentation, long segmentCount, long minUpdatePeriodMs) {\r\n        MultiSegmentRepresentation oldMultiRepresentation = (MultiSegmentRepresentation) oldRepresentation;\r\n\r\n        SegmentList oldSegmentList = (SegmentList) Helpers.getField(oldMultiRepresentation, \"segmentBase\");\r\n\r\n        List<RangedUri> oldMediaSegments = (List<RangedUri>) Helpers.getField(oldSegmentList, \"mediaSegments\");\r\n\r\n        RangedUri firstSegment = oldMediaSegments.get(0);\r\n        RangedUri secondSegment = oldMediaSegments.get(1);\r\n        long start = firstSegment.start;\r\n        long length = firstSegment.length;\r\n        String firstSegmentUri = (String) Helpers.getField(firstSegment, \"referenceUri\");\r\n        String secondSegmentUri = (String) Helpers.getField(secondSegment, \"referenceUri\");\r\n\r\n        UrlQueryString firstSegmentQuery = UrlQueryStringFactory.parse(\"/\" + firstSegmentUri);\r\n        UrlQueryString secondSegmentQuery = UrlQueryStringFactory.parse(\"/\" + secondSegmentUri);\r\n        long firstSegmentNum = Helpers.parseLong(firstSegmentQuery.get(\"sq\"));\r\n        long firstSegmentLimit = Helpers.parseLong(firstSegmentQuery.get(\"lmt\"));\r\n        long secondSegmentLimit = Helpers.parseLong(secondSegmentQuery.get(\"lmt\"));\r\n        long limitDiff = secondSegmentLimit - firstSegmentLimit;\r\n\r\n        // Skip variable segment limit (huge limit diff values)\r\n        if (firstSegmentNum <= 0 || limitDiff > 100) {\r\n            return;\r\n        }\r\n\r\n        long presentationTimeOffsetUs = oldRepresentation.presentationTimeOffsetUs;\r\n        Helpers.setField(oldRepresentation, \"presentationTimeOffsetUs\", presentationTimeOffsetUs - (segmentCount * minUpdatePeriodMs * 1_000));\r\n\r\n        long currentSegmentNum = firstSegmentNum - 1;\r\n        long currentSegmentLimit = firstSegmentLimit - limitDiff;\r\n\r\n        for (int i = 1; i <= segmentCount; i++) {\r\n            oldMediaSegments.add(0, new RangedUri(String.format(\"sq/%s/lmt/%s\", currentSegmentNum, currentSegmentLimit), start, length));\r\n            currentSegmentNum--;\r\n            currentSegmentLimit -= limitDiff;\r\n        }\r\n\r\n        List<SegmentTimelineElement> oldSegmentTimeline = (List<SegmentTimelineElement>) Helpers.getField(oldSegmentList, \"segmentTimeline\");\r\n\r\n        // segmentTimeline is the same for all segments\r\n        if (oldMediaSegments.size() != oldSegmentTimeline.size()) {\r\n            SegmentTimelineElement lastTimeline = oldSegmentTimeline.get(oldSegmentTimeline.size() - 1);\r\n            long lastTimelineDuration = (Long) Helpers.getField(lastTimeline, \"duration\");\r\n            long lastTimelineStartTime = (Long) Helpers.getField(lastTimeline, \"startTime\");\r\n\r\n            for (int i = 1; i <= segmentCount; i++) {\r\n                oldSegmentTimeline.add(new SegmentTimelineElement(lastTimelineStartTime + (lastTimelineDuration * i), lastTimelineDuration));\r\n            }\r\n        }\r\n\r\n        Log.d(TAG, \"Recreate representation: done\");\r\n    }\r\n\r\n    private static void lazyRecreateRepresentations(AdaptationSet adaptationSet, long segmentCount, long minUpdatePeriodMs) {\r\n        List<Representation> representations = adaptationSet.representations;\r\n        List<Representation> newRepresentations = new ArrayList<>();\r\n        for (int j = 0; j < representations.size(); j++) {\r\n            Representation oldRepresentation = representations.get(j);\r\n            newRepresentations.add(new MultiSegmentRepresentationWrapper((MultiSegmentRepresentation) oldRepresentation, segmentCount, minUpdatePeriodMs));\r\n        }\r\n\r\n        Helpers.setField(adaptationSet, \"representations\", newRepresentations);\r\n    }\r\n\r\n    private static long getFirstSegmentNum(DashManifest manifest) {\r\n        DashSegmentIndex dashSegmentIndex = manifest.getPeriod(0).adaptationSets.get(0).representations.get(0).getIndex();\r\n        return dashSegmentIndex.getFirstSegmentNum();\r\n    }\r\n\r\n    private static long getLastSegmentNum(DashManifest manifest) {\r\n        DashSegmentIndex dashSegmentIndex = manifest.getPeriod(0).adaptationSets.get(0).representations.get(0).getIndex();\r\n        return dashSegmentIndex.getFirstSegmentNum() + dashSegmentIndex.getSegmentCount(DashSegmentIndex.INDEX_UNBOUNDED) - 1;\r\n    }\r\n\r\n    private static long getSegmentCount(DashManifest manifest) {\r\n        return manifest.getPeriod(0).adaptationSets.get(0).representations.get(0).getIndex().getSegmentCount(C.TIME_UNSET);\r\n    }\r\n\r\n    private static long getFirstSegmentDurationMs(DashManifest manifest) {\r\n        DashSegmentIndex dashSegmentIndex = manifest.getPeriod(0).adaptationSets.get(0).representations.get(0).getIndex();\r\n        return dashSegmentIndex.getDurationUs(getFirstSegmentNum(manifest), C.TIME_UNSET) / 1_000;\r\n    }\r\n\r\n    private static class MultiSegmentRepresentationWrapper extends MultiSegmentRepresentation {\r\n        private long mSegmentCount;\r\n        private long mMinUpdatePeriodMs;\r\n        private boolean mInitDone;\r\n\r\n        public MultiSegmentRepresentationWrapper(MultiSegmentRepresentation origin, long segmentCount, long minUpdatePeriodMs) {\r\n            this(origin.revisionId, origin.format, origin.baseUrl, (SegmentList) Helpers.getField(origin, \"segmentBase\"), origin.inbandEventStreams);\r\n            mSegmentCount = segmentCount;\r\n            mMinUpdatePeriodMs = minUpdatePeriodMs;\r\n        }\r\n\r\n        public MultiSegmentRepresentationWrapper(\r\n                long revisionId,\r\n                Format format,\r\n                String baseUrl,\r\n                MultiSegmentBase segmentBase,\r\n                List<Descriptor> inbandEventStreams) {\r\n            super(revisionId, format, baseUrl, segmentBase, inbandEventStreams);\r\n        }\r\n\r\n        // DashSegmentIndex implementation.\r\n\r\n        @Override\r\n        public RangedUri getSegmentUrl(long segmentIndex) {\r\n            init();\r\n            return super.getSegmentUrl(segmentIndex);\r\n        }\r\n\r\n        @Override\r\n        public long getSegmentNum(long timeUs, long periodDurationUs) {\r\n            init();\r\n            return super.getSegmentNum(timeUs, periodDurationUs);\r\n        }\r\n\r\n        @Override\r\n        public long getTimeUs(long segmentIndex) {\r\n            init();\r\n            return super.getTimeUs(segmentIndex);\r\n        }\r\n\r\n        @Override\r\n        public long getDurationUs(long segmentIndex, long periodDurationUs) {\r\n            init();\r\n            return super.getDurationUs(segmentIndex, periodDurationUs);\r\n        }\r\n\r\n        @Override\r\n        public long getFirstSegmentNum() {\r\n            init();\r\n            return super.getFirstSegmentNum();\r\n        }\r\n\r\n        @Override\r\n        public int getSegmentCount(long periodDurationUs) {\r\n            init();\r\n            return super.getSegmentCount(periodDurationUs);\r\n        }\r\n\r\n        @Override\r\n        public boolean isExplicit() {\r\n            init();\r\n            return super.isExplicit();\r\n        }\r\n\r\n        private void init() {\r\n            if (mInitDone) {\r\n                return;\r\n            }\r\n\r\n            recreateRepresentation(this, mSegmentCount, mMinUpdatePeriodMs);\r\n\r\n            mInitDone = true;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/controller/ExoPlayerController.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.controller;\r\n\r\nimport android.content.Context;\r\nimport android.os.Build;\r\nimport android.os.Build.VERSION;\r\n\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.ExoPlaybackException;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.PlaybackParameters;\r\nimport com.google.android.exoplayer2.Player;\r\nimport com.google.android.exoplayer2.SimpleExoPlayer;\r\nimport com.google.android.exoplayer2.source.MediaSource;\r\nimport com.google.android.exoplayer2.source.MergingMediaSource;\r\nimport com.google.android.exoplayer2.source.TrackGroupArray;\r\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\r\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\r\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemFormatInfo;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.BuildConfig;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.listener.PlayerEventListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.ExoMediaSourceFactory;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.errors.TrackErrorFixer;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.other.VolumeBooster;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.ExoFormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.TrackInfoFormatter2;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.TrackSelectorManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.TrackSelectorUtil;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.MediaTrack;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.VideoTrack;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.ExoUtils;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\r\n\r\nimport java.io.InputStream;\r\nimport java.lang.ref.WeakReference;\r\nimport java.util.List;\r\n\r\npublic class ExoPlayerController implements Player.EventListener {\r\n    private static final String TAG = ExoPlayerController.class.getSimpleName();\r\n    private final Context mContext;\r\n    private final ExoMediaSourceFactory mMediaSourceFactory;\r\n    private final TrackSelectorManager mTrackSelectorManager;\r\n    private final TrackInfoFormatter2 mTrackFormatter;\r\n    private final TrackErrorFixer mTrackErrorFixer;\r\n    private boolean mOnSourceChanged;\r\n    private WeakReference<Video> mVideo;\r\n    private final PlayerEventListener mEventListener;\r\n    private SimpleExoPlayer mPlayer;\r\n    private PlayerView mPlayerView;\r\n    private VolumeBooster mVolumeBooster;\r\n    private boolean mIsEnded;\r\n    private Runnable mOnVideoLoaded;\r\n\r\n    public ExoPlayerController(Context context, PlayerEventListener eventListener) {\r\n        PlayerTweaksData playerTweaksData = PlayerTweaksData.instance(context);\r\n        mContext = context.getApplicationContext();\r\n        mMediaSourceFactory = new ExoMediaSourceFactory(context);\r\n        mTrackSelectorManager = new TrackSelectorManager(context);\r\n        mTrackFormatter = new TrackInfoFormatter2();\r\n        mTrackFormatter.enableBitrate(PlayerTweaksData.instance(context).isQualityInfoBitrateEnabled());\r\n        mTrackErrorFixer = new TrackErrorFixer(mTrackSelectorManager);\r\n\r\n        mMediaSourceFactory.setTrackErrorFixer(mTrackErrorFixer);\r\n        mEventListener = eventListener;\r\n        \r\n        applyShield720pFix();\r\n        VideoTrack.sIsNoFpsPresetsEnabled = playerTweaksData.isNoFpsPresetsEnabled();\r\n        MediaTrack.preferAvcOverVp9(playerTweaksData.isAvcOverVp9Preferred());\r\n    }\r\n\r\n    private void applyShield720pFix() {\r\n        PlayerData playerData = PlayerData.instance(mContext);\r\n        mTrackSelectorManager.selectTrack(FormatItem.toMediaTrack(playerData.getFormat(FormatItem.TYPE_VIDEO)));\r\n        mTrackSelectorManager.selectTrack(FormatItem.toMediaTrack(playerData.getFormat(FormatItem.TYPE_AUDIO)));\r\n        mTrackSelectorManager.selectTrack(FormatItem.toMediaTrack(playerData.getFormat(FormatItem.TYPE_SUBTITLE)));\r\n    }\r\n\r\n    public void openSabr(MediaItemFormatInfo formatInfo) {\r\n        MediaSource mediaSource = mMediaSourceFactory.fromSabrFormatInfo(formatInfo);\r\n        openMediaSource(mediaSource);\r\n    }\r\n\r\n    public void openDash(MediaItemFormatInfo formatInfo) {\r\n        MediaSource mediaSource = mMediaSourceFactory.fromDashFormatInfo(formatInfo);\r\n        openMediaSource(mediaSource);\r\n    }\r\n\r\n    public void openDash(InputStream dashManifest) {\r\n        MediaSource mediaSource = mMediaSourceFactory.fromDashManifest(dashManifest);\r\n        openMediaSource(mediaSource);\r\n    }\r\n\r\n    public void openDashUrl(String dashManifestUrl) {\r\n        MediaSource mediaSource = mMediaSourceFactory.fromDashManifestUrl(dashManifestUrl);\r\n        openMediaSource(mediaSource);\r\n    }\r\n\r\n    public void openHlsUrl(String hlsPlaylistUrl) {\r\n        MediaSource mediaSource = mMediaSourceFactory.fromHlsPlaylist(hlsPlaylistUrl);\r\n        openMediaSource(mediaSource);\r\n    }\r\n\r\n    public void openUrlList(List<String> urlList) {\r\n        MediaSource mediaSource = mMediaSourceFactory.fromUrlList(urlList);\r\n        openMediaSource(mediaSource);\r\n    }\r\n\r\n    public void openMerged(MediaItemFormatInfo formatInfo, String hlsPlaylistUrl) {\r\n        MediaSource dashMediaSource = mMediaSourceFactory.fromDashFormatInfo(formatInfo);\r\n        MediaSource hlsMediaSource = mMediaSourceFactory.fromHlsPlaylist(hlsPlaylistUrl);\r\n        openMediaSource(new MergingMediaSource(dashMediaSource, hlsMediaSource));\r\n    }\r\n\r\n    public void openMerged(InputStream dashManifest, String hlsPlaylistUrl) {\r\n        MediaSource dashMediaSource = mMediaSourceFactory.fromDashManifest(dashManifest);\r\n        MediaSource hlsMediaSource = mMediaSourceFactory.fromHlsPlaylist(hlsPlaylistUrl);\r\n        openMediaSource(new MergingMediaSource(dashMediaSource, hlsMediaSource));\r\n    }\r\n\r\n    private void openMediaSource(MediaSource mediaSource) {\r\n        resetPlayerState(); // fixes occasional video artifacts and problems with quality switching\r\n        setQualityInfo(\"\");\r\n\r\n        mTrackSelectorManager.setMergedSource(mediaSource instanceof MergingMediaSource);\r\n        mTrackSelectorManager.invalidate();\r\n        mOnSourceChanged = true;\r\n        mEventListener.onSourceChanged(getVideo());\r\n        mPlayer.prepare(mediaSource);\r\n    }\r\n\r\n    public long getPositionMs() {\r\n        if (mPlayer == null) {\r\n            return -1;\r\n        }\r\n\r\n        return mPlayer.getCurrentPosition();\r\n    }\r\n\r\n    /**\r\n     * NOTE: Pos gathered from content block data may slightly exceed video duration\r\n     * (e.g. 302200 when duration is 302000).\r\n     */\r\n    public void setPositionMs(long positionMs) {\r\n        // Url list videos at load stage has undefined (-1) length. So, we need to remove length check.\r\n        if (mPlayer != null && positionMs >= 0 && positionMs <= getDurationMs()) {\r\n            mPlayer.seekTo(positionMs);\r\n        }\r\n    }\r\n\r\n    public long getDurationMs() {\r\n        if (mPlayer == null) {\r\n            return -1;\r\n        }\r\n\r\n        long duration = mPlayer.getDuration();\r\n        return duration != C.TIME_UNSET ? duration : -1;\r\n    }\r\n\r\n    public void setPlayWhenReady(boolean play) {\r\n        if (mPlayer != null) {\r\n            mPlayer.setPlayWhenReady(play);\r\n        }\r\n    }\r\n\r\n    public boolean getPlayWhenReady() {\r\n        if (mPlayer == null) {\r\n            return false;\r\n        }\r\n\r\n        return mPlayer.getPlayWhenReady();\r\n    }\r\n\r\n    public boolean isPlaying() {\r\n        return ExoUtils.isPlaying(mPlayer);\r\n    }\r\n    \r\n    public boolean isLoading() {\r\n        return ExoUtils.isLoading(mPlayer);\r\n    }\r\n    \r\n    public boolean containsMedia() {\r\n        if (mPlayer == null) {\r\n            return false;\r\n        }\r\n\r\n        return mPlayer.getPlaybackState() != Player.STATE_IDLE;\r\n    }\r\n    \r\n    public void release() {\r\n        mTrackSelectorManager.release();\r\n        mMediaSourceFactory.release();\r\n        releasePlayer();\r\n        mPlayerView = null;\r\n        // Don't destroy it (needed inside the bridge)!\r\n        //mEventListener = null;\r\n    }\r\n    \r\n    public void setPlayer(SimpleExoPlayer player) {\r\n        mPlayer = player;\r\n        player.addListener(this);\r\n    }\r\n\r\n    //@Override\r\n    //public void setEventListener(PlayerEventListener eventListener) {\r\n    //    mEventListener = eventListener;\r\n    //}\r\n    \r\n    public void setPlayerView(PlayerView playerView) {\r\n        mPlayerView = playerView;\r\n    }\r\n    \r\n    public void setTrackSelector(DefaultTrackSelector trackSelector) {\r\n        mTrackSelectorManager.setTrackSelector(trackSelector);\r\n\r\n        if (mContext != null && trackSelector != null && PlayerTweaksData.instance(mContext).isTunneledPlaybackEnabled()) {\r\n            // Enable tunneling if supported by the current media and device configuration.\r\n            if (VERSION.SDK_INT >= 21) {\r\n                trackSelector.setParameters(trackSelector.buildUponParameters().setTunnelingAudioSessionId(C.generateAudioSessionIdV21(mContext)));\r\n            }\r\n        }\r\n    }\r\n    \r\n    public void setVideo(Video video) {\r\n        mVideo = new WeakReference<>(video);\r\n    }\r\n    \r\n    public Video getVideo() {\r\n        return mVideo != null ? mVideo.get() : null;\r\n    }\r\n    \r\n    public List<FormatItem> getVideoFormats() {\r\n        return ExoFormatItem.from(mTrackSelectorManager.getVideoTracks());\r\n    }\r\n    \r\n    public List<FormatItem> getAudioFormats() {\r\n        return ExoFormatItem.from(mTrackSelectorManager.getAudioTracks());\r\n    }\r\n    \r\n    public List<FormatItem> getSubtitleFormats() {\r\n        return ExoFormatItem.from(mTrackSelectorManager.getSubtitleTracks());\r\n    }\r\n    \r\n    public void selectFormat(FormatItem formatItem) {\r\n        if (formatItem != null && formatItem.getTrack() != null) {\r\n            FormatItem selectedFormatItem = getSelectedFormat(formatItem.getTrack().rendererIndex);\r\n            if (!formatItem.equals(selectedFormatItem)) {\r\n                mTrackSelectorManager.selectTrack(formatItem.getTrack());\r\n                mEventListener.onTrackSelected(formatItem);\r\n            }\r\n        }\r\n    }\r\n\r\n    public FormatItem getVideoFormat() {\r\n        return getSelectedFormat(TrackSelectorManager.RENDERER_INDEX_VIDEO);\r\n    }\r\n\r\n    public FormatItem getAudioFormat() {\r\n        return getSelectedFormat(TrackSelectorManager.RENDERER_INDEX_AUDIO);\r\n    }\r\n\r\n    public FormatItem getSubtitleFormat() {\r\n        return getSelectedFormat(TrackSelectorManager.RENDERER_INDEX_SUBTITLE);\r\n    }\r\n\r\n    private FormatItem getSelectedFormat(int rendererIndex) {\r\n        return ExoFormatItem.from(mTrackSelectorManager.getSelectedTrack(rendererIndex));\r\n    }\r\n\r\n    @Override\r\n    public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\r\n        Log.d(TAG, \"onTracksChanged: start: groups length: \" + trackGroups.length);\r\n\r\n        if (trackGroups.length == 0) {\r\n            Log.i(TAG, \"onTracksChanged: Hmm. Strange. Received empty groups, no selections. Why is this happens only on next/prev videos?\");\r\n            return;\r\n        }\r\n\r\n        notifyOnVideoLoad();\r\n\r\n        for (TrackSelection selection : trackSelections.getAll()) {\r\n            if (selection != null) {\r\n                // EXO: 2.12.1\r\n                //Format format = selection.getSelectedFormat();\r\n\r\n                // EXO: 2.13.1\r\n                Format format = selection.getFormat(0);\r\n\r\n                mEventListener.onTrackChanged(ExoFormatItem.from(format));\r\n\r\n                mTrackFormatter.setFormat(format);\r\n            }\r\n        }\r\n        \r\n        setQualityInfo(mTrackFormatter.getQualityLabel());\r\n\r\n        // Manage audio focus. E.g. use Spotify when audio is disabled. (NOT NEEDED!!!)\r\n        //MediaTrack audioTrack = mTrackSelectorManager.getAudioTrack();\r\n        //ExoPlayerInitializer.enableAudioFocus(mPlayer, audioTrack != null && !audioTrack.isEmpty());\r\n    }\r\n\r\n    private void notifyOnVideoLoad() {\r\n        if (mOnSourceChanged) {\r\n            mOnSourceChanged = false;\r\n\r\n            mEventListener.onVideoLoaded(getVideo());\r\n\r\n            if (mOnVideoLoaded != null) {\r\n                mOnVideoLoaded.run();\r\n            }\r\n\r\n            // Produce thread sync problems\r\n            // Attempt to read from field 'java.util.TreeMap$Node java.util.TreeMap$Node.left' on a null object reference\r\n            //mTrackSelectorManager.fixTracksSelection();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onPlayerError(ExoPlaybackException error) {\r\n        Log.e(TAG, \"onPlayerError: \" + error);\r\n\r\n        // NOTE: Player is released at this point. So, there is no sense to restore the playback here.\r\n\r\n        Throwable nested = error.getCause() != null ? error.getCause() : error;\r\n\r\n        mEventListener.onEngineError(error.type, error.rendererIndex, nested);\r\n    }\r\n\r\n    @Override\r\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\r\n        if (BuildConfig.DEBUG) {\r\n            Log.d(TAG, \"onPlayerStateChanged: \" + TrackSelectorUtil.stateToString(playbackState));\r\n        }\r\n\r\n        boolean isPlayPressed = Player.STATE_READY == playbackState && playWhenReady;\r\n        boolean isPausePressed = Player.STATE_READY == playbackState && !playWhenReady;\r\n        boolean isPlaybackEnded = Player.STATE_ENDED == playbackState && playWhenReady;\r\n        boolean isBuffering = Player.STATE_BUFFERING == playbackState && playWhenReady;\r\n\r\n        // Fix chapters (seek and play) after playback ends\r\n        if (isPlaybackEnded && mIsEnded) {\r\n            return;\r\n        }\r\n\r\n        if (isPlayPressed) {\r\n            mEventListener.onPlay();\r\n        } else if (isPausePressed) {\r\n            mEventListener.onPause();\r\n        } else if (isPlaybackEnded) {\r\n            mEventListener.onPlayEnd();\r\n            mIsEnded = true;\r\n        } else if (isBuffering) {\r\n            mEventListener.onBuffering();\r\n        }\r\n\r\n        if (getPositionMs() < getDurationMs()) {\r\n            mIsEnded = false;\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onPositionDiscontinuity(int reason) {\r\n        Log.e(TAG, \"onPositionDiscontinuity\");\r\n\r\n        // Fix video loop on 480p with legacy codes enabled\r\n        if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {\r\n            mPlayer.stop();\r\n            mEventListener.onPlayEnd();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onSeekProcessed() {\r\n        mEventListener.onSeekEnd();\r\n    }\r\n\r\n    public float getSpeed() {\r\n        if (mPlayer != null) {\r\n            return mPlayer.getPlaybackParameters().speed;\r\n        } else {\r\n            return -1;\r\n        }\r\n    }\r\n\r\n    public void setSpeed(float speed) {\r\n        if (mPlayer != null && speed > 0) {\r\n            if (PlayerTweaksData.instance(mContext).isAudioTimeStretchingEnabled()) {\r\n                mPlayer.setPlaybackParameters(new PlaybackParameters(speed, mPlayer.getPlaybackParameters().pitch));\r\n            } else {\r\n                mPlayer.setPlaybackParameters(new PlaybackParameters(speed, speed));\r\n            }\r\n\r\n            mTrackFormatter.setSpeed(speed);\r\n            setQualityInfo(mTrackFormatter.getQualityLabel());\r\n            mEventListener.onSpeedChanged(speed);\r\n        }\r\n    }\r\n\r\n    public float getPitch() {\r\n        if (mPlayer != null) {\r\n            return mPlayer.getPlaybackParameters().pitch;\r\n        } else {\r\n            return -1;\r\n        }\r\n    }\r\n\r\n    public void setPitch(float pitch) {\r\n        if (mPlayer != null && pitch > 0) {\r\n            mPlayer.setPlaybackParameters(new PlaybackParameters(mPlayer.getPlaybackParameters().speed, pitch));\r\n        }\r\n    }\r\n\r\n    public void setVolume(float volume) {\r\n        if (mPlayer != null && volume >= 0) {\r\n            mPlayer.setVolume(Math.min(volume, 1f));\r\n\r\n            //applyVolumeBoost(volume);\r\n        }\r\n    }\r\n    \r\n    public float getVolume() {\r\n        if (mPlayer != null) {\r\n            return mPlayer.getVolume();\r\n        } else {\r\n            return 1;\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Fixes video artifacts when switching to the next video.<br/>\r\n     * Also could help with memory leaks(??)<br/>\r\n     * Without this also you'll have problems with track quality switching(??).\r\n     */\r\n    public void resetPlayerState() {\r\n        if (containsMedia()) {\r\n            mPlayer.stop(true);\r\n        }\r\n    }\r\n    \r\n    public void setOnVideoLoaded(Runnable onVideoLoaded) {\r\n        mOnVideoLoaded = onVideoLoaded;\r\n    }\r\n\r\n    private void setQualityInfo(String qualityInfoStr) {\r\n        if (mPlayerView != null && qualityInfoStr != null) {\r\n            mPlayerView.setQualityInfo(qualityInfoStr);\r\n        }\r\n    }\r\n\r\n    private void applyVolumeBoost(float volume) {\r\n        if (mPlayer == null) {\r\n            return;\r\n        }\r\n\r\n        if (mVolumeBooster != null) {\r\n            mPlayer.removeAudioListener(mVolumeBooster);\r\n            mVolumeBooster = null;\r\n        }\r\n\r\n        // 5.1 audio cannot be boosted (format isn't supported error)\r\n        // also, other 2.0 tracks in 5.1 group is already too loud. so cancel them too.\r\n        if (volume > 1f && !contains51Audio() && Build.VERSION.SDK_INT >= 19) {\r\n            mVolumeBooster = new VolumeBooster(true, volume, null);\r\n            mPlayer.addAudioListener(mVolumeBooster);\r\n        }\r\n    }\r\n    \r\n    private boolean contains51Audio() {\r\n        if (mTrackSelectorManager == null || mTrackSelectorManager.getAudioTracks() == null) {\r\n            return false;\r\n        }\r\n\r\n        for (MediaTrack track : mTrackSelectorManager.getAudioTracks()) {\r\n            if (TrackSelectorUtil.is51Audio(track.format)) {\r\n                return true;\r\n            }\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    private void releasePlayer() {\r\n        if (mPlayer == null) {\r\n            return;\r\n        }\r\n\r\n        try {\r\n            mPlayer.removeListener(this);\r\n            mPlayer.stop(true); // Cause input lags due to high cpu load?\r\n            mPlayer.clearVideoSurface();\r\n            mPlayer.release();\r\n            mPlayer = null;\r\n        } catch (ArrayIndexOutOfBoundsException e) { // thrown on stop()\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/controller/PlayerView.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.controller;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\n\r\npublic interface PlayerView {\r\n    void setQualityInfo(String info);\r\n    void setVideo(Video video);\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/errors/DashDefaultLoadErrorHandlingPolicy.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.errors;\r\n\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.ParserException;\r\nimport com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;\r\nimport com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;\r\nimport com.google.android.exoplayer2.upstream.Loader.UnexpectedLoaderException;\r\n\r\nimport java.io.FileNotFoundException;\r\nimport java.io.IOException;\r\n\r\npublic class DashDefaultLoadErrorHandlingPolicy extends DefaultLoadErrorHandlingPolicy {\r\n    /**\r\n     * Copied from the parent class!\r\n     */\r\n    @Override\r\n    public long getBlacklistDurationMsFor(int dataType, long loadDurationMs, IOException exception, int errorCount) {\r\n        if (exception instanceof InvalidResponseCodeException) {\r\n            int responseCode = ((InvalidResponseCodeException) exception).responseCode;\r\n            return responseCode == 404 // HTTP 404 Not Found.\r\n                    || responseCode == 410 // HTTP 410 Gone.\r\n                    ? DEFAULT_TRACK_BLACKLIST_MS\r\n                    : C.TIME_UNSET;\r\n        }\r\n        return C.TIME_UNSET;\r\n    }\r\n\r\n    /**\r\n     * Copied from the parent class!\r\n     */\r\n    @Override\r\n    public long getRetryDelayMsFor(int dataType, long loadDurationMs, IOException exception, int errorCount) {\r\n        return exception instanceof ParserException\r\n                || exception instanceof FileNotFoundException\r\n                || exception instanceof UnexpectedLoaderException\r\n                ? C.TIME_UNSET\r\n                : Math.min((errorCount - 1) * 1000, 5000);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/errors/SabrDefaultLoadErrorHandlingPolicy.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.errors;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\n\r\nimport java.io.IOException;\r\n\r\npublic class SabrDefaultLoadErrorHandlingPolicy extends DashDefaultLoadErrorHandlingPolicy {\r\n    @Override\r\n    public long getBlacklistDurationMsFor(int dataType, long loadDurationMs, IOException exception, int errorCount) {\r\n        return super.getBlacklistDurationMsFor(dataType, loadDurationMs, exception, errorCount);\r\n    }\r\n    \r\n    @Override\r\n    public long getRetryDelayMsFor(int dataType, long loadDurationMs, IOException exception, int errorCount) {\r\n        if (Helpers.contains(exception.getMessage(), \"Wait 5 sec\")) {\r\n            return 5_000;\r\n        }\r\n\r\n        return super.getRetryDelayMsFor(dataType, loadDurationMs, exception, errorCount);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/errors/TrackErrorFixer.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.errors;\r\n\r\nimport androidx.annotation.Nullable;\r\nimport com.google.android.exoplayer2.ExoPlaybackException;\r\nimport com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;\r\nimport com.google.android.exoplayer2.source.DefaultMediaSourceEventListener;\r\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\r\nimport com.google.android.exoplayer2.source.chunk.Chunk;\r\nimport com.google.android.exoplayer2.source.chunk.ContainerMediaChunk;\r\nimport com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;\r\nimport com.google.android.exoplayer2.util.MimeTypes;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.TrackSelectorManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.MediaTrack;\r\n\r\nimport java.io.IOException;\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\nimport java.util.Map.Entry;\r\nimport java.util.Set;\r\n\r\npublic class TrackErrorFixer extends DefaultMediaSourceEventListener {\r\n    private static final int BLACKLIST_CHECK_MS = 1_000;\r\n    private static final int BLACKLIST_CLEAR_MS = 10_000;\r\n    private static final String TAG = TrackErrorFixer.class.getSimpleName();\r\n    private final TrackSelectorManager mTrackSelectorManager;\r\n    private long mSelectionTimeMs;\r\n    private final Map<MediaTrack, Long> mBlacklistedTracks = new HashMap<>();\r\n    private InvalidResponseCodeException mLastEx;\r\n\r\n    public TrackErrorFixer(TrackSelectorManager trackSelectorManager) {\r\n        mTrackSelectorManager = trackSelectorManager;\r\n    }\r\n\r\n    /**\r\n     * 1) Blacklist non-playable audio tracks for live streams.<br/>\r\n     * Last segment of such streams produce 404 error.<br/>\r\n     * See DrLupo streams, for example.<br/.\r\n     * <br/>\r\n     * 2) Blacklist non-playable tracks for regular videos (error 503).<br/>\r\n     */\r\n    public boolean fixError(Exception e) {\r\n        if (!(e instanceof InvalidResponseCodeException)) {\r\n            return false;\r\n        }\r\n\r\n        InvalidResponseCodeException ex = (InvalidResponseCodeException) e;\r\n\r\n        if (ex.responseCode != 404 && ex.responseCode != 503 && ex.responseCode != 500) {\r\n            return false;\r\n        }\r\n\r\n        if (System.currentTimeMillis() - mSelectionTimeMs < BLACKLIST_CHECK_MS) {\r\n            return false;\r\n        }\r\n\r\n        mLastEx = ex;\r\n\r\n        return selectDifferentCodec(isAudio(mLastEx));\r\n    }\r\n\r\n    //public boolean fixError(ExoPlaybackException error) {\r\n    //    if (error != null && error.getCause() instanceof DecoderInitializationException) {\r\n    //        return selectDifferentCodec(MimeTypes.isAudio(((DecoderInitializationException) error.getCause()).mimeType));\r\n    //    }\r\n    //\r\n    //    return false;\r\n    //}\r\n\r\n    private boolean selectDifferentCodec(boolean isAudio) {\r\n        if (System.currentTimeMillis() - mSelectionTimeMs < BLACKLIST_CLEAR_MS) {\r\n            return false;\r\n        }\r\n\r\n        Set<MediaTrack> tracks = isAudio ? mTrackSelectorManager.getAudioTracks() : mTrackSelectorManager.getVideoTracks();\r\n\r\n        if (tracks == null) {\r\n            return false;\r\n        }\r\n\r\n        MediaTrack currentTrack = null;\r\n\r\n        for (MediaTrack track : tracks) {\r\n            if (track.isSelected) {\r\n                currentTrack = track;\r\n                break;\r\n            }\r\n        }\r\n\r\n        if (currentTrack == null || currentTrack.format == null || currentTrack.format.codecs == null) {\r\n            return false;\r\n        }\r\n\r\n        String currentCodec = currentTrack.format.codecs;\r\n        int width = currentTrack.format.width;\r\n\r\n        MediaTrack nextTrack = null;\r\n\r\n        for (MediaTrack track : tracks) {\r\n            if (track.format == null) {\r\n                continue;\r\n            }\r\n\r\n            if (!currentCodec.equals(track.format.codecs) && track.format.width <= width) {\r\n                nextTrack = track;\r\n                break;\r\n            }\r\n        }\r\n\r\n        if (nextTrack != null) {\r\n            mTrackSelectorManager.selectTrack(nextTrack);\r\n            mSelectionTimeMs = System.currentTimeMillis();\r\n            return true;\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    //private boolean selectNextTrack() {\r\n    //    if (mLastEx == null) {\r\n    //        return false;\r\n    //    }\r\n    //\r\n    //    if (System.currentTimeMillis() - mSelectionTimeMs < BLACKLIST_CLEAR_MS) {\r\n    //        return false;\r\n    //    }\r\n    //\r\n    //    Set<MediaTrack> tracks = isAudio(mLastEx) ? mTrackSelectorManager.getAudioTracks() : mTrackSelectorManager.getVideoTracks();\r\n    //\r\n    //    if (tracks == null) {\r\n    //        return false;\r\n    //    }\r\n    //\r\n    //    MediaTrack nextTrack = null;\r\n    //    boolean afterSelected = false;\r\n    //\r\n    //    for (MediaTrack track : tracks) {\r\n    //        if (track.isSelected) {\r\n    //            afterSelected = true;\r\n    //            continue;\r\n    //        }\r\n    //\r\n    //        if (afterSelected) {\r\n    //            nextTrack = track;\r\n    //            break;\r\n    //        }\r\n    //    }\r\n    //\r\n    //    if (nextTrack != null) {\r\n    //        mTrackSelectorManager.selectTrack(nextTrack);\r\n    //        return true;\r\n    //    }\r\n    //\r\n    //    mSelectionTimeMs = System.currentTimeMillis();\r\n    //    return false;\r\n    //}\r\n    //\r\n    //private boolean selectNextTrackOld() {\r\n    //    if (mLastEx == null) {\r\n    //        return false;\r\n    //    }\r\n    //\r\n    //    if (System.currentTimeMillis() - mSelectionTimeMs > BLACKLIST_CLEAR_MS) {\r\n    //        mBlacklistedTracks.clear();\r\n    //    }\r\n    //\r\n    //    Set<MediaTrack> tracks = isAudio(mLastEx) ? mTrackSelectorManager.getAudioTracks() : mTrackSelectorManager.getVideoTracks();\r\n    //    MediaTrack tmpTrack = null;\r\n    //\r\n    //    if (tracks == null) {\r\n    //        return false;\r\n    //    }\r\n    //\r\n    //    for (MediaTrack track : tracks) {\r\n    //        if (track.isSelected) {\r\n    //            addToBlacklist(track);\r\n    //        } else {\r\n    //            tmpTrack = track;\r\n    //            if (!isBlacklisted(track)) {\r\n    //                addToBlacklist(track);\r\n    //                break;\r\n    //            }\r\n    //        }\r\n    //    }\r\n    //\r\n    //    if (tmpTrack != null) {\r\n    //        mTrackSelectorManager.selectTrack(tmpTrack);\r\n    //        mSelectionTimeMs = System.currentTimeMillis();\r\n    //        //Utils.postDelayed(mHandler, mSelectFirstTrack, SELECT_FIRST_TRACK_MS);\r\n    //        return true;\r\n    //    }\r\n    //\r\n    //    return false;\r\n    //}\r\n    //\r\n    //private void selectFirstTrack() {\r\n    //    if (mLastEx == null) {\r\n    //        return;\r\n    //    }\r\n    //\r\n    //    mBlacklistedTracks.clear();\r\n    //\r\n    //    Set<MediaTrack> tracks = isAudio(mLastEx) ? mTrackSelectorManager.getAudioTracks() : mTrackSelectorManager.getVideoTracks();\r\n    //    MediaTrack tmpTrack = null;\r\n    //\r\n    //    for (MediaTrack track : tracks) {\r\n    //        addToBlacklist(track);\r\n    //        tmpTrack = track;\r\n    //        break;\r\n    //    }\r\n    //\r\n    //    if (tmpTrack != null) {\r\n    //        mTrackSelectorManager.selectTrack(tmpTrack);\r\n    //        mSelectionTimeMs = System.currentTimeMillis();\r\n    //    }\r\n    //}\r\n\r\n    private boolean isAudio(InvalidResponseCodeException ex) {\r\n        String url = ex.dataSpec.uri.toString();\r\n\r\n        return url.contains(\"mime/audio\");\r\n    }\r\n\r\n    private boolean isBlacklisted(MediaTrack track) {\r\n        for (Entry<MediaTrack, Long> entry : mBlacklistedTracks.entrySet()) {\r\n            if (track == entry.getKey() && System.currentTimeMillis() - entry.getValue() < BLACKLIST_CLEAR_MS) {\r\n                return true;\r\n            }\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    private void addToBlacklist(MediaTrack track) {\r\n        mBlacklistedTracks.put(track, System.currentTimeMillis());\r\n    }\r\n\r\n    public void fixEmptyChunk(Chunk chunk) {\r\n        // Fix when just started new type live stream ahead of the position\r\n        if (chunk instanceof ContainerMediaChunk) {\r\n            long nextLoadPosition = (Long) Helpers.getField(chunk, \"nextLoadPosition\");\r\n            if (nextLoadPosition == 0) {\r\n                Log.e(TAG, \"Stream position behind the timeline. Waiting for new data...\");\r\n                try {\r\n                    Thread.sleep(10_000);\r\n                } catch (InterruptedException e) {\r\n                    e.printStackTrace();\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onLoadError(int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo,\r\n                            MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) {\r\n        fixError(error);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/other/DebugInfoManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.other;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.net.Uri;\nimport android.os.Build.VERSION;\nimport android.text.TextUtils;\nimport android.util.TypedValue;\nimport android.view.Gravity;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\nimport android.util.Pair;\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.SimpleExoPlayer;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecInfo;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.liskovsoft.sharedutils.helpers.AppInfoHelpers;\nimport com.liskovsoft.sharedutils.helpers.DeviceHelpers;\nimport com.liskovsoft.sharedutils.helpers.FileHelpers;\nimport com.liskovsoft.sharedutils.helpers.Helpers;\nimport com.liskovsoft.sharedutils.querystringparser.UrlQueryStringFactory;\nimport com.liskovsoft.smartyoutubetv2.common.R;\nimport com.liskovsoft.smartyoutubetv2.common.autoframerate.internal.DisplayHolder.Mode;\nimport com.liskovsoft.smartyoutubetv2.common.autoframerate.internal.UhdHelper;\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.ExoUtils;\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs;\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\nimport com.liskovsoft.youtubeapi.app.models.AppInfo;\nimport com.liskovsoft.youtubeapi.common.helpers.AppClient;\nimport com.liskovsoft.youtubeapi.service.internal.MediaServiceData;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\n\n// NOTE: original file taken from\n// https://github.com/google/ExoPlayer/blob/release-v2/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java\n\n/**\n * A helper class for periodically updating a {@link TextView} with debug information obtained from\n * a {@link SimpleExoPlayer}.\n */\npublic final class DebugInfoManager implements Runnable, Player.EventListener {\n    private static final String TAG = DebugInfoManager.class.getSimpleName();\n    private static final int REFRESH_INTERVAL_MS = 1000;\n    private static final String NOT_AVAILABLE = \"none\";\n    private final float mTextSize;\n\n    private final SimpleExoPlayer mPlayer;\n    private final ViewGroup mDebugViewGroup;\n    private final Context mContext;\n\n    private boolean mStarted;\n    private LinearLayout column1;\n    private LinearLayout column2;\n    private UhdHelper mUhdHelper;\n    private final List<Pair<String, String>> mVideoInfo = new ArrayList<>();\n    private final List<Pair<String, String>> mDisplayModeId = new ArrayList<>();\n    private final List<Pair<String, String>> mDisplayInfo = new ArrayList<>();\n    private final String mAppVersion;\n\n    /**\n     * @param debugViewGroup The container that should be updated to display the information.\n     * @param player      The {@link SimpleExoPlayer} from which debug information should be obtained.\n     */\n    public DebugInfoManager(ViewGroup debugViewGroup, SimpleExoPlayer player) {\n        mContext = debugViewGroup.getContext();\n        mDebugViewGroup = debugViewGroup;\n        mPlayer = player;\n        mTextSize = mContext.getResources().getDimension(R.dimen.debug_text_size);\n        mAppVersion = String.format(\"%s version\", mContext.getString(R.string.app_name));\n        inflate();\n    }\n\n    private void inflate() {\n        mDebugViewGroup.removeAllViews();\n        LayoutInflater inflater = LayoutInflater.from(mContext);\n        inflater.inflate(R.layout.debug_view, mDebugViewGroup, true);\n        column1 = mDebugViewGroup.findViewById(R.id.debug_view_column1);\n        column2 = mDebugViewGroup.findViewById(R.id.debug_view_column2);\n    }\n\n    public void show(boolean show) {\n        if (show) {\n            create();\n        } else {\n            destroy();\n        }\n    }\n\n    public boolean isShown() {\n        return mStarted;\n    }\n\n    /**\n     * Starts periodic updates of the {@link TextView}. Must be called from the application's main\n     * thread.\n     */\n    private void create() {\n        if (mStarted) {\n            return;\n        }\n\n        mStarted = true;\n        mDebugViewGroup.setVisibility(View.VISIBLE);\n        mUhdHelper = new UhdHelper(mContext);\n        mPlayer.addListener(this);\n        updateAndPost();\n    }\n\n    /**\n     * Stops periodic updates of the {@link TextView}. Must be called from the application's main\n     * thread.\n     */\n    private void destroy() {\n        if (!mStarted) {\n            return;\n        }\n\n        mStarted = false;\n        mDebugViewGroup.setVisibility(View.GONE);\n        mPlayer.removeListener(this);\n        mDebugViewGroup.removeCallbacks(this);\n        mUhdHelper = null;\n    }\n\n    // Player.EventListener implementation.\n\n    @Override\n    public void onLoadingChanged(boolean isLoading) {\n        // NOP\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n        // NOP\n    }\n\n    @Override\n    public void onRepeatModeChanged(int repeatMode) {\n        // NOP\n    }\n\n    @Override\n    public void onPositionDiscontinuity(int reason) {\n        // NOP\n    }\n\n    @Override\n    public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {\n        // NOP\n    }\n\n    @Override\n    public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {\n        // Do nothing.\n    }\n\n    @Override\n    public void onPlayerError(ExoPlaybackException error) {\n        // Do nothing.\n    }\n\n    @Override\n    public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) {\n        // NOP\n    }\n\n    // Runnable implementation.\n\n    @Override\n    public void run() {\n        updateAndPost();\n    }\n\n    // Private methods.\n\n    @SuppressLint(\"SetTextI18n\")\n    private void updateAndPost() {\n        column1.removeAllViews();\n        column2.removeAllViews();\n\n        updateRareChangedValues();\n\n        appendVideoInfo();\n        appendRuntimeInfo();\n        appendPlayerState();\n        appendDisplayInfo();\n        appendDisplayModeId();\n        //appendPlayerWindowIndex();\n        appendVersion();\n        appendDeviceNameSDKCache();\n        appendMemoryInfo();\n        appendWebViewInfo();\n        appendWebClientInfo();\n        //appendClientType();\n        //appendPlayerVersion();\n        appendAccountInfo();\n\n        // Schedule next update\n        mDebugViewGroup.removeCallbacks(this);\n        mDebugViewGroup.postDelayed(this, REFRESH_INTERVAL_MS);\n    }\n\n    private void updateRareChangedValues() {\n        updateVideoInfo();\n        updateDisplayModeId();\n        updateDisplayInfo();\n    }\n\n    private void appendVideoInfo() {\n        for (Pair<String, String> pair : mVideoInfo) {\n            appendRow(pair.first, pair.second);\n        }\n    }\n\n    private void updateVideoInfo() {\n        mVideoInfo.clear();\n\n        Format video = mPlayer.getVideoFormat();\n        Format audio = mPlayer.getAudioFormat();\n        if (video == null || audio == null) {\n            return;\n        }\n\n        String videoRes = getVideoResolution(video);\n\n        mVideoInfo.add(new Pair<>(\"Video resolution\", videoRes));\n        mVideoInfo.add(new Pair<>(\"Video/Audio codecs\", String.format(\n                \"%s(%s)/%s(%s)\",\n                getFormatMimeType(video),\n                getFormatId(video),\n                getFormatMimeType(audio),\n                getFormatId(audio)\n        )));\n        mVideoInfo.add(new Pair<>(\"Video/Audio bitrate\", String.format(\n                \"%s/%s\",\n                toHumanReadable(video.bitrate),\n                toHumanReadable(audio.bitrate)\n        )));\n        // Aspect info is not valid since we're using custom views\n        //String par = video.pixelWidthHeightRatio == Format.NO_VALUE ||\n        //        video.pixelWidthHeightRatio == 1f ?\n        //        DEFAULT : String.format(Locale.US, \"%.02f\", video.pixelWidthHeightRatio);\n        //mVideoInfo.add(new Pair<>(\"Aspect Ratio\", par));\n        String videoCodecName = getVideoDecoderNameV2();\n        mVideoInfo.add(new Pair<>(\"Video decoder name\", videoCodecName));\n        //mVideoInfo.add(new Pair<>(\"Hardware accelerated\", String.valueOf(DeviceHelpers.isHardwareAccelerated(videoCodecName))));\n        \n        if (video.colorInfo != null) {\n            //String transferFunction = getColorTransferString(video.colorInfo.colorTransfer);\n            //if (!transferFunction.equals(NOT_AVAILABLE)) {\n            //    mVideoInfo.add(new Pair<>(\"Transfer function\", transferFunction));\n            //}\n            //String colorSpace = getColorSpaceString(video.colorInfo.colorSpace);\n            //if (!colorSpace.equals(NOT_AVAILABLE)) {\n            //    mVideoInfo.add(new Pair<>(\"Color space\", colorSpace));\n            //}\n\n            String transferFunction = getColorTransferString(video.colorInfo.colorTransfer);\n            String colorSpace = getColorSpaceString(video.colorInfo.colorSpace);\n\n            mVideoInfo.add(new Pair<>(\"Transfer function/Color space\", transferFunction + \"/\" + colorSpace));\n        }\n    }\n\n    private void appendRuntimeInfo() {\n        DecoderCounters counters = mPlayer.getVideoDecoderCounters();\n        if (counters == null)\n            return;\n\n        counters.ensureUpdated();\n        appendRow(\"Dropped/Rendered frames\", counters.droppedBufferCount + \"/\" + counters.renderedOutputBufferCount);\n        appendRow(\"Buffer size (seconds)\", (int)(mPlayer.getBufferedPosition() - mPlayer.getCurrentPosition()) / 1_000);\n    }\n\n    private void appendPlayerState() {\n        //appendRow(\"Player paused\", !mPlayer.getPlayWhenReady());\n\n        String text;\n        switch (mPlayer.getPlaybackState()) {\n            case Player.STATE_BUFFERING:\n                text = \"buffering\";\n                break;\n            case Player.STATE_ENDED:\n                text = \"ended\";\n                break;\n            case Player.STATE_IDLE:\n                text = \"idle\";\n                break;\n            case Player.STATE_READY:\n                text = \"ready\";\n                break;\n            default:\n                text = \"unknown\";\n                break;\n        }\n        //appendRow(\"Playback state\", text);\n        appendRow(\"Playback state\", String.format(\"paused=%s;state=%s\", !mPlayer.getPlayWhenReady(), text));\n    }\n\n    private void appendDisplayModeId() {\n        for (Pair<String, String> pair : mDisplayModeId) {\n            appendRow(pair.first, pair.second);\n        }\n    }\n\n    private void updateDisplayModeId() {\n        if (mUhdHelper == null) {\n            return;\n        }\n\n        mDisplayModeId.clear();\n\n        Mode currentMode = mUhdHelper.getCurrentMode();\n        Mode[] supportedModes = mUhdHelper.getSupportedModes();\n\n        String bootResolution = AppPrefs.instance(mContext).getBootResolution();\n        String currentResolution = UhdHelper.toResolution(currentMode);\n\n        mDisplayModeId.add(new Pair<>(\"UI resolution\", currentResolution != null ? currentResolution : NOT_AVAILABLE));\n        mDisplayModeId.add(new Pair<>(\"Boot resolution\", bootResolution != null ? bootResolution : NOT_AVAILABLE));\n\n        //mDisplayModeId.add(new Pair<>(\"Display mode ID\", currentMode != null ? String.valueOf(currentMode.getModeId()) : NOT_AVAILABLE));\n        //mDisplayModeId.add(new Pair<>(\"Display modes length\", supportedModes != null ? String.valueOf(supportedModes.length) : NOT_AVAILABLE));\n        String modeId = currentMode != null ? String.valueOf(currentMode.getModeId()) : NOT_AVAILABLE;\n        String modeLength = supportedModes != null ? String.valueOf(supportedModes.length) : NOT_AVAILABLE;\n        mDisplayModeId.add(new Pair<>(\"Display mode ID/length\", modeId + \"/\" + modeLength));\n    }\n\n    private void appendDisplayInfo() {\n        for (Pair<String, String> pair : mDisplayInfo) {\n            appendRow(pair.first, pair.second);\n        }\n    }\n\n    private void updateDisplayInfo() {\n        mDisplayInfo.clear();\n\n        mDisplayInfo.add(new Pair<>(\"Display DPI\", String.valueOf(Helpers.getDeviceDpi(mContext))));\n    }\n\n    private void appendPlayerWindowIndex() {\n        appendRow(\"Window index\", mPlayer.getCurrentWindowIndex());\n    }\n\n    private void appendVersion() {\n        //appendRow(\"ExoPlayer version\", ExoPlayerLibraryInfo.VERSION);\n        appendRow(\"ExoPlayer engine\",\n                PlayerTweaksData.instance(mContext).getPlayerDataSource() == PlayerTweaksData.PLAYER_DATA_SOURCE_OKHTTP ? \"OkHttp\" :\n                        PlayerTweaksData.instance(mContext).getPlayerDataSource() == PlayerTweaksData.PLAYER_DATA_SOURCE_CRONET ? \"Cronet\" :\n                        \"Default\");\n        //appendRow(\"Cronet version\", ApiVersion.getCronetVersion());\n        //appendRow(\"OkHttp version\", Version.userAgent());\n        appendRow(mAppVersion, AppInfoHelpers.getAppVersionName(mContext));\n    }\n\n    private void appendDeviceNameSDKCache() {\n        //appendRow(\"Device name\", Helpers.getDeviceName());\n        //appendRow(\"Android SDK\", VERSION.SDK_INT);\n        appendRow(\"Device name/SDK\", Helpers.getDeviceName() + \"/\" + VERSION.SDK_INT);\n        appendRow(\"Disk cache size (MB)\", String.valueOf(\n                (FileHelpers.getDirSize(FileHelpers.getInternalCacheDir(mContext)) + FileHelpers.getDirSize(FileHelpers.getExternalCacheDir(mContext)))\n                        / 1024 / 1024\n        ));\n    }\n\n    private void appendMemoryInfo() {\n        //appendRow(\"Max heap memory (MB)\", DeviceHelpers.getMaxHeapMemoryMB()); // Growth Limit\n        //appendRow(\"Allocated heap memory (MB)\", DeviceHelpers.getAllocatedHeapMemoryMB());\n        appendRow(\"Allocated heap memory (MB)\", DeviceHelpers.getAllocatedHeapMemoryMB() + \"/\" + DeviceHelpers.getMaxHeapMemoryMB());\n    }\n\n    private void appendWebViewInfo() {\n        appendRow(\"Pot supported\", MediaServiceData.instance().isPotSupported());\n    }\n\n    private void appendClientType() {\n        int videoInfoType = MediaServiceData.instance().getVideoInfoType();\n        String clientType = videoInfoType != -1 && videoInfoType < AppClient.values().length ? AppClient.values()[videoInfoType].name() : \"default\";\n\n        appendRow(\"Client type\", clientType);\n    }\n\n    private void appendPlayerVersion() {\n        AppInfo appInfo = Helpers.firstNonNull(MediaServiceData.instance().getFailedAppInfo(), MediaServiceData.instance().getAppInfo());\n        String playerUrl = appInfo != null ? appInfo.getPlayerUrl() : null;\n        if (playerUrl != null) {\n            String playerVersion = UrlQueryStringFactory.parse(Uri.parse(playerUrl)).get(\"player\");\n            String shortPlayerUrl = playerVersion != null ? playerUrl.split(playerVersion)[1] : null;\n            boolean isFailed = MediaServiceData.instance().getFailedAppInfo() != null;\n            appendRow(\"Player version\", isFailed ? Utils.color(playerVersion, Color.RED) : playerVersion);\n            appendRow(\"Player url\", isFailed ? Utils.color(shortPlayerUrl, Color.RED) : shortPlayerUrl);\n        }\n    }\n\n    private void appendWebClientInfo() {\n        String clientType = getClientType();\n        CharSequence playerVersion = getPlayerVersion();\n\n        appendRow(\"Web client/Web player version\", TextUtils.concat(clientType, \"/\", playerVersion));\n    }\n\n    private void appendAccountInfo() {\n        appendRow(\"Account info\", MediaServiceManager.instance().printAccountDebugInfo());\n    }\n\n    private void appendRow(String name, boolean val) {\n        appendNameColumn(createTextView(name));\n        appendValueColumn(createTextView(val));\n    }\n\n    private void appendRow(String name, CharSequence val) {\n        appendNameColumn(createTextView(name));\n        appendValueColumn(createTextView(val));\n    }\n\n    private void appendRow(String name, int val) {\n        appendNameColumn(createTextView(name));\n        appendValueColumn(createTextView(val));\n    }\n\n    private void appendNameColumn(TextView content) {\n        content.setGravity(Gravity.END);\n        column1.addView(content);\n    }\n\n    private void appendValueColumn(TextView content) {\n        column2.addView(content);\n    }\n\n    private TextView createTextView(CharSequence name) {\n        TextView textView = new TextView(mContext);\n        textView.setText(name);\n        textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);\n        return textView;\n    }\n\n    private TextView createTextView(boolean val) {\n        TextView textView = new TextView(mContext);\n        textView.setText(String.valueOf(val));\n        textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);\n        return textView;\n    }\n\n    private TextView createTextView(int val) {\n        TextView textView = new TextView(mContext);\n        textView.setText(String.valueOf(val));\n        textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);\n        return textView;\n    }\n\n    private String toHumanReadable(int bitrate) {\n        if (bitrate < 0) {\n            return NOT_AVAILABLE;\n        }\n\n        float mbit = ((float) bitrate) / 1_000_000;\n        return String.format(Locale.ENGLISH, \"%.2fMbps\", mbit);\n    }\n\n    private String getFormatId(Format video) {\n        return video.id;\n    }\n\n    private String getFormatMimeType(Format video) {\n        if (video == null || video.sampleMimeType == null) {\n            return null;\n        }\n\n        return video.sampleMimeType.replace(\"video/\", \"\").replace(\"audio/\", \"\");\n    }\n\n    private String getVideoResolution(Format video) {\n        String result = video.width + \"x\" + video.height;\n        if (video.frameRate > 0) {\n            result += \"@\" + ((int) video.frameRate);\n        }\n        return result;\n    }\n\n    // NOTE: Be aware. This info isn't real! It's like caps or something like that. To get real info use method below.\n    private String getVideoDecoderNameV1(Format format) {\n        if (format == null) {\n            return null;\n        }\n\n        MediaCodecInfo info = ExoUtils.getCapsDecoderInfo(format.sampleMimeType);\n\n        return info != null ? info.name : null;\n    }\n\n    private String getVideoDecoderNameV2() {\n        return ExoUtils.getVideoDecoderName();\n    }\n\n    private String getColorTransferString(int colorTransfer) {\n        if (colorTransfer == Format.NO_VALUE) {\n            return NOT_AVAILABLE;\n        }\n\n        switch (colorTransfer) {\n            case C.COLOR_TRANSFER_SDR:\n                return \"Gamma\";\n            case C.COLOR_TRANSFER_ST2084:\n                return \"PQ (ST.2084)\";\n            case C.COLOR_TRANSFER_HLG:\n                return \"HLG\";\n            default:\n                return NOT_AVAILABLE;\n        }\n    }\n\n    private String getColorSpaceString(int colorSpace) {\n        if (colorSpace == Format.NO_VALUE) {\n            return NOT_AVAILABLE;\n        }\n\n        switch (colorSpace) {\n            case C.COLOR_SPACE_BT601:\n                return \"BT.601\";\n            case C.COLOR_SPACE_BT709:\n                return \"BT.709\";\n            case C.COLOR_SPACE_BT2020:\n                return \"BT.2020\";\n            default:\n                return NOT_AVAILABLE;\n        }\n    }\n\n    private String getClientType() {\n        int videoInfoType = MediaServiceData.instance().getVideoInfoType();\n        String clientType = videoInfoType != -1 && videoInfoType < AppClient.values().length ? AppClient.values()[videoInfoType].name() : \"default\";\n\n        return clientType;\n    }\n\n    private CharSequence getPlayerVersion() {\n        CharSequence result = NOT_AVAILABLE;\n        AppInfo appInfo = Helpers.firstNonNull(MediaServiceData.instance().getFailedAppInfo(), MediaServiceData.instance().getAppInfo());\n        String playerUrl = appInfo != null ? appInfo.getPlayerUrl() : null;\n        if (playerUrl != null) {\n            String playerVersion = UrlQueryStringFactory.parse(Uri.parse(playerUrl)).get(\"player\");\n            boolean isFailed = MediaServiceData.instance().getFailedAppInfo() != null;\n            result = isFailed ? Utils.color(playerVersion, Color.RED) : playerVersion;\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/other/ExoPlayerInitializer.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.other;\r\n\r\nimport android.content.Context;\r\nimport android.os.Build;\r\nimport android.os.Handler;\r\nimport androidx.annotation.Nullable;\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.DefaultLoadControl;\r\nimport com.google.android.exoplayer2.DefaultRenderersFactory;\r\nimport com.google.android.exoplayer2.ExoPlayerFactory;\r\nimport com.google.android.exoplayer2.SeekParameters;\r\nimport com.google.android.exoplayer2.SimpleExoPlayer;\r\nimport com.google.android.exoplayer2.audio.AudioAttributes;\r\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionManager;\r\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\r\nimport com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;\r\nimport com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;\r\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\r\nimport com.google.android.exoplayer2.drm.MediaDrmCallback;\r\nimport com.google.android.exoplayer2.drm.UnsupportedDrmException;\r\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\r\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\r\nimport com.google.android.exoplayer2.upstream.TransferListener;\r\nimport com.liskovsoft.sharedutils.helpers.DeviceHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\r\n\r\nimport java.util.UUID;\r\n\r\npublic class ExoPlayerInitializer {\r\n    private final int mMaxBufferBytes;\r\n    private final PlayerData mPlayerData;\r\n    private final PlayerTweaksData mPlayerTweaksData;\r\n    private static AudioAttributes sAudioAttributes;\r\n\r\n    public ExoPlayerInitializer(Context context) {\r\n        mPlayerData = PlayerData.instance(context);\r\n        mPlayerTweaksData = PlayerTweaksData.instance(context);\r\n\r\n        long deviceRam = DeviceHelpers.getDeviceRam(context);\r\n\r\n        // If ram is too big, bigger then max int value DeviceRam will return a negative number...\r\n        // use 196MB as that can only happens if device has more than 17GB of RAM, so 196 is enough and safe\r\n        // https://github.com/yuliskov/SmartYouTubeTV/issues/532\r\n        mMaxBufferBytes = deviceRam <= 0 ? 196_000_000 : (int)(deviceRam / 18);\r\n    }\r\n\r\n    public SimpleExoPlayer createPlayer(Context context, DefaultRenderersFactory renderersFactory, DefaultTrackSelector trackSelector) {\r\n        DefaultLoadControl loadControl = createLoadControl();\r\n\r\n        // HDR fix?\r\n        //trackSelector.setParameters(trackSelector.buildUponParameters().setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)));\r\n\r\n        // Old initializer\r\n        SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector, loadControl);\r\n\r\n        // New initializer\r\n        //SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(\r\n        //        context, renderersFactory, trackSelector, loadControl,\r\n        //        null, new DummyBandwidthMeter(), new AnalyticsCollector.Factory(), Util.getLooper()\r\n        //);\r\n\r\n        //enableAudioFocus(player);\r\n\r\n        // Lead to numbered errors\r\n        //player.setRepeatMode(Player.REPEAT_MODE_ONE);\r\n\r\n        // Fix still image while audio is playing (happens after format change or exit from sleep)\r\n        //player.setPlayWhenReady(true);\r\n\r\n        applyPlaybackFixes(player);\r\n\r\n        setupAudioFocus(player);\r\n\r\n        setupVolumeBoost(player);\r\n\r\n        return player;\r\n    }\r\n\r\n    private static AudioAttributes getAudioAttributes() {\r\n        if (sAudioAttributes == null) {\r\n            sAudioAttributes = new AudioAttributes.Builder()\r\n                    .setUsage(C.USAGE_MEDIA)\r\n                    .setContentType(C.CONTENT_TYPE_MOVIE)\r\n                    .build();\r\n        }\r\n\r\n        return sAudioAttributes;\r\n    }\r\n\r\n    /**\r\n     * Increase player's min/max buffer size to 60 secs\r\n     * @return load control\r\n     */\r\n    private DefaultLoadControl createLoadControl() {\r\n        DefaultLoadControl.Builder baseBuilder = new DefaultLoadControl.Builder();\r\n\r\n        // Default values\r\n        //DefaultLoadControl.DEFAULT_MIN_BUFFER_MS // 15_000\r\n        //DefaultLoadControl.DEFAULT_MAX_BUFFER_MS // 50_000\r\n        //DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS // 2_500\r\n        //DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS // 5_000\r\n\r\n        // Default values\r\n        int minBufferMs = 30_000;\r\n        int maxBufferMs = 30_000;\r\n        int bufferForPlaybackMs = 2_500;\r\n        int bufferForPlaybackAfterRebufferMs = 5_000;\r\n\r\n        switch (mPlayerData.getVideoBufferType()) {\r\n            case PlayerData.BUFFER_HIGHEST:\r\n                minBufferMs = 50_000;\r\n                maxBufferMs = 100_000;\r\n                // Infinite buffer works awfully on live streams. Constant stuttering.\r\n                //maxBufferMs = 36_000_000; // technical infinity, recommended here a very high number, the max will be based on setTargetBufferBytes() value\r\n                baseBuilder\r\n                        .setTargetBufferBytes(mMaxBufferBytes);\r\n                baseBuilder.setBackBuffer(minBufferMs, true);\r\n                break;\r\n            case PlayerData.BUFFER_HIGH:\r\n                minBufferMs = 50_000;\r\n                maxBufferMs = 50_000;\r\n                baseBuilder.setBackBuffer(minBufferMs, true);\r\n                break;\r\n            case PlayerData.BUFFER_MEDIUM:\r\n                //minBufferMs = 30_000;\r\n                //maxBufferMs = 30_000;\r\n                break;\r\n            case PlayerData.BUFFER_LOW:\r\n                minBufferMs = 5_000; // LIVE fix\r\n                maxBufferMs = 5_000; // LIVE fix\r\n                //bufferForPlaybackMs = 1_000;\r\n                //bufferForPlaybackAfterRebufferMs = 1_000;\r\n                break;\r\n        }\r\n\r\n        baseBuilder\r\n                .setBufferDurationsMs(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs);\r\n\r\n        return baseBuilder.createDefaultLoadControl();\r\n    }\r\n\r\n    private void setupVolumeBoost(SimpleExoPlayer player) {\r\n        // 5.1 audio cannot be boosted (format isn't supported error)\r\n        // also, other 2.0 tracks in 5.1 group is already too loud. so cancel them too.\r\n        float volume = mPlayerTweaksData.isPlayerAutoVolumeEnabled() ? 2.0f : mPlayerData.getPlayerVolume();\r\n        if (volume > 1f && Build.VERSION.SDK_INT >= 19) {\r\n            VolumeBooster mVolumeBooster = new VolumeBooster(true, volume, player);\r\n            player.addAudioListener(mVolumeBooster);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Manage audio focus. E.g. use Spotify when audio is disabled.\r\n     */\r\n    private void setupAudioFocus(SimpleExoPlayer player) {\r\n        if (player != null && mPlayerTweaksData.isAudioFocusEnabled()) {\r\n            try {\r\n                player.setAudioAttributes(getAudioAttributes(), true);\r\n            } catch (SecurityException e) { // uid 10390 not allowed to perform TAKE_AUDIO_FOCUS\r\n                e.printStackTrace();\r\n            }\r\n        }\r\n    }\r\n\r\n    private void applyPlaybackFixes(SimpleExoPlayer player) {\r\n        // Try to fix decoder error on Nvidia Shield 2019.\r\n        // Init resources as early as possible.\r\n        //player.setForegroundMode(true);\r\n        // NOTE: Avoid using seekParameters. ContentBlock hangs because of constant skipping to the segment start.\r\n        // ContentBlock hangs on the last segment: https://www.youtube.com/watch?v=pYymRbfjKv8\r\n\r\n        // Fix seeking on TextureView (some devices only)\r\n        if (mPlayerTweaksData.isTextureViewEnabled()) {\r\n            // Also, live stream (dash) seeking fix\r\n            player.setSeekParameters(SeekParameters.CLOSEST_SYNC);\r\n        }\r\n    }\r\n\r\n    private DrmSessionManager<FrameworkMediaCrypto> createDrmManager() {\r\n        try {\r\n            return DefaultDrmSessionManager.newWidevineInstance(new MediaDrmCallback() {\r\n                @Override\r\n                public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) {\r\n                    return new byte[0];\r\n                }\r\n\r\n                @Override\r\n                public byte[] executeKeyRequest(UUID uuid, KeyRequest request) {\r\n                    return new byte[0];\r\n                }\r\n            }, null);\r\n        } catch (UnsupportedDrmException e) {\r\n            e.printStackTrace();\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    private static final class DummyBandwidthMeter implements BandwidthMeter {\r\n        @Override\r\n        public long getBitrateEstimate() {\r\n            return 0;\r\n        }\r\n\r\n        @Nullable\r\n        @Override\r\n        public TransferListener getTransferListener() {\r\n            return null;\r\n        }\r\n\r\n        @Override\r\n        public void addEventListener(Handler eventHandler, EventListener eventListener) {\r\n            // Do nothing.\r\n        }\r\n\r\n        @Override\r\n        public void removeEventListener(EventListener eventListener) {\r\n            // Do nothing.\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/other/SubtitleManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.other;\r\n\r\nimport android.content.Context;\r\nimport android.graphics.Color;\r\nimport android.graphics.Typeface;\r\nimport android.os.Build.VERSION;\r\nimport android.util.TypedValue;\r\nimport android.view.View;\r\nimport android.view.accessibility.CaptioningManager;\r\nimport android.view.accessibility.CaptioningManager.CaptionStyle;\r\n\r\nimport androidx.annotation.RequiresApi;\r\nimport androidx.core.content.ContextCompat;\r\nimport com.google.android.exoplayer2.text.CaptionStyleCompat;\r\nimport com.google.android.exoplayer2.text.Cue;\r\nimport com.google.android.exoplayer2.text.TextOutput;\r\nimport com.google.android.exoplayer2.ui.SubtitleView;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.common.DataChangeBase.OnDataChange;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class SubtitleManager implements TextOutput, OnDataChange {\r\n    private static final String TAG = SubtitleManager.class.getSimpleName();\r\n    private final SubtitleView mSubtitleView;\r\n    private final Context mContext;\r\n    private final List<SubtitleStyle> mSubtitleStyles = new ArrayList<>();\r\n    private final AppPrefs mPrefs;\r\n    private final PlayerData mPlayerData;\r\n    private CharSequence subsBuffer;\r\n\r\n    public static class SubtitleStyle {\r\n        public final int nameResId;\r\n        public final int subsColorResId;\r\n        public final int backgroundColorResId;\r\n        public final int captionStyle;\r\n\r\n        public SubtitleStyle(int nameResId) {\r\n            this(nameResId, -1, -1, -1);\r\n        }\r\n\r\n        public SubtitleStyle(int nameResId, int subsColorResId, int backgroundColorResId, int captionStyle) {\r\n            this.nameResId = nameResId;\r\n            this.subsColorResId = subsColorResId;\r\n            this.backgroundColorResId = backgroundColorResId;\r\n            this.captionStyle = captionStyle;\r\n        }\r\n\r\n        public boolean isSystem() {\r\n            return subsColorResId == -1 && backgroundColorResId == -1 && captionStyle == -1;\r\n        }\r\n    }\r\n\r\n    public SubtitleManager(SubtitleView subtitleView) {\r\n        mContext = subtitleView.getContext();\r\n        mSubtitleView = subtitleView;\r\n        mPrefs = AppPrefs.instance(mContext);\r\n        mPlayerData = PlayerData.instance(mContext);\r\n        mPlayerData.setOnChange(this);\r\n        configureSubtitleView();\r\n    }\r\n\r\n    @Override\r\n    public void onDataChange() {\r\n        configureSubtitleView();\r\n    }\r\n\r\n    @Override\r\n    public void onCues(List<Cue> cues) {\r\n        if (mSubtitleView != null) {\r\n            mSubtitleView.setCues(forceCenterAlignment(cues));\r\n        }\r\n    }\r\n\r\n    public void show(boolean show) {\r\n        if (mSubtitleView != null) {\r\n            mSubtitleView.setVisibility(show ? View.VISIBLE : View.GONE);\r\n        }\r\n    }\r\n\r\n    private List<SubtitleStyle> getSubtitleStyles() {\r\n        return mSubtitleStyles;\r\n    }\r\n\r\n    private SubtitleStyle getSubtitleStyle() {\r\n        return mPlayerData.getSubtitleStyle();\r\n    }\r\n\r\n    private void setSubtitleStyle(SubtitleStyle subtitleStyle) {\r\n        mPlayerData.setSubtitleStyle(subtitleStyle);\r\n        configureSubtitleView();\r\n    }\r\n\r\n    private List<Cue> forceCenterAlignment(List<Cue> cues) {\r\n        List<Cue> result = new ArrayList<>();\r\n\r\n        for (Cue cue : cues) {\r\n            // Autogenerated subs repeated lines fix\r\n            final String textStr = cue.text.toString();\r\n            if (Helpers.endsWithAny(textStr, \"\\n\", \" \")) { // vtt subs format\r\n                subsBuffer = textStr;\r\n            } else if (textStr.contains(\"\\n\")) { // ttml subs format\r\n                //CharSequence text = subsBuffer != null ? textStr.replace(subsBuffer, \"\").replace(\"\\n\", \"\") : textStr;\r\n\r\n                CharSequence text;\r\n\r\n                if (subsBuffer != null && textStr.contains(subsBuffer)) {\r\n                    text = textStr.replace(subsBuffer, \"\").replace(\"\\n\", \"\");\r\n                } else {\r\n                    text = textStr;\r\n                }\r\n\r\n                result.add(new Cue(text)); // sub centered by default\r\n\r\n                String[] split = textStr.split(\"\\n\");\r\n                subsBuffer = split.length == 2 ? split[1] : textStr;\r\n            } else {\r\n                CharSequence text = subsBuffer != null ? textStr.replace(subsBuffer, \"\") : textStr;\r\n                result.add(new Cue(text)); // sub centered by default\r\n                subsBuffer = text;\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    private void configureSubtitleView() {\r\n        if (mSubtitleView != null) {\r\n            // disable default style\r\n            mSubtitleView.setApplyEmbeddedStyles(false);\r\n\r\n            SubtitleStyle subtitleStyle = getSubtitleStyle();\r\n\r\n            if (subtitleStyle.isSystem()) {\r\n                if (VERSION.SDK_INT >= 19) {\r\n                    applySystemStyle();\r\n                }\r\n            } else {\r\n                applyStyle(subtitleStyle);\r\n            }\r\n\r\n            mSubtitleView.setBottomPaddingFraction(mPlayerData.getSubtitlePosition());\r\n        }\r\n    }\r\n\r\n    private void applyStyle(SubtitleStyle subtitleStyle) {\r\n        int textColor = ContextCompat.getColor(mContext, subtitleStyle.subsColorResId);\r\n        int outlineColor = ContextCompat.getColor(mContext, R.color.black);\r\n        int backgroundColor = ContextCompat.getColor(mContext, subtitleStyle.backgroundColorResId);\r\n\r\n        CaptionStyleCompat style =\r\n                new CaptionStyleCompat(textColor,\r\n                        backgroundColor, Color.TRANSPARENT,\r\n                        subtitleStyle.captionStyle,\r\n                        outlineColor, Typeface.DEFAULT_BOLD);\r\n        mSubtitleView.setStyle(style);\r\n\r\n        float textSize = getTextSizePx();\r\n        mSubtitleView.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);\r\n    }\r\n\r\n    @RequiresApi(19)\r\n    private void applySystemStyle() {\r\n        CaptioningManager captioningManager =\r\n                (CaptioningManager) mContext.getSystemService(Context.CAPTIONING_SERVICE);\r\n\r\n        if (captioningManager != null) {\r\n            CaptionStyle userStyle = captioningManager.getUserStyle();\r\n\r\n            CaptionStyleCompat style =\r\n                    new CaptionStyleCompat(userStyle.foregroundColor,\r\n                            userStyle.backgroundColor, VERSION.SDK_INT >= 21 ? userStyle.windowColor : Color.TRANSPARENT,\r\n                            userStyle.edgeType,\r\n                            userStyle.edgeColor, userStyle.getTypeface());\r\n            mSubtitleView.setStyle(style);\r\n\r\n            float textSizePx = getTextSizePx();\r\n            mSubtitleView.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePx * captioningManager.getFontScale());\r\n        }\r\n    }\r\n\r\n    private float getTextSizePx() {\r\n        float textSizePx = mSubtitleView.getContext().getResources().getDimension(R.dimen.subtitle_text_size);\r\n        return textSizePx * mPlayerData.getSubtitleScale();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/other/VideoZoomManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.other;\r\n\r\nimport com.google.android.exoplayer2.ui.AspectRatioFrameLayout;\r\nimport com.google.android.exoplayer2.ui.PlayerView;\r\n\r\n/**\r\n * <a href=\"Zoom to fit video: https://stackoverflow.com/questions/33608746/in-android-using-exoplayer-how-to-fill-surfaceview-with-a-video-that-does-not\">More info</a>\r\n */\r\npublic class VideoZoomManager {\r\n    public static final int MODE_DEFAULT = AspectRatioFrameLayout.RESIZE_MODE_FIT;\r\n    public static final int MODE_FIT_WIDTH = AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH;\r\n    public static final int MODE_FIT_HEIGHT = AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT;\r\n    public static final int MODE_FIT_BOTH = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;\r\n    public static final int MODE_STRETCH = AspectRatioFrameLayout.RESIZE_MODE_FILL;\r\n    private final PlayerView mPlayerView;\r\n\r\n    public VideoZoomManager(PlayerView playerView) {\r\n        mPlayerView = playerView;\r\n    }\r\n\r\n    public int getZoomMode() {\r\n        return mPlayerView.getResizeMode();\r\n    }\r\n\r\n    public void setZoomMode(int mode) {\r\n        mPlayerView.setResizeMode(mode);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/other/VolumeBooster.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.other;\r\n\r\nimport android.media.audiofx.LoudnessEnhancer;\r\nimport android.os.Build.VERSION;\r\n\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.google.android.exoplayer2.SimpleExoPlayer;\r\nimport com.google.android.exoplayer2.audio.AudioListener;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\n\r\npublic class VolumeBooster implements AudioListener {\r\n    private static final String TAG = VolumeBooster.class.getSimpleName();\r\n    private boolean mIsEnabled;\r\n    private final float mVolume;\r\n    private final SimpleExoPlayer mPlayer;\r\n    private LoudnessEnhancer mBooster;\r\n    private boolean mIsSupported;\r\n    private int mCurrentSessionId = -1;\r\n\r\n    public VolumeBooster(boolean enabled, float volume, @Nullable SimpleExoPlayer player) {\r\n        mIsEnabled = enabled;\r\n        mVolume = volume;\r\n        mPlayer = player;\r\n    }\r\n\r\n    @Override\r\n    public void onAudioSessionId(int audioSessionId) {\r\n        if (VERSION.SDK_INT < 19 || mVolume <= 1) {\r\n            return;\r\n        }\r\n\r\n        // NOTE: 5.1 audio cannot be boosted (format isn't supported error)\r\n        if (mPlayer != null && mPlayer.getAudioFormat() != null && mPlayer.getAudioFormat().channelCount > 2) {\r\n            return;\r\n        }\r\n\r\n        Log.d(TAG, \"Audio session id is %s, supported gain %s\", audioSessionId, LoudnessEnhancer.PARAM_TARGET_GAIN_MB);\r\n\r\n        if (audioSessionId == mCurrentSessionId) {\r\n            return; // Already initialized for this session\r\n        }\r\n\r\n        mCurrentSessionId = audioSessionId;\r\n\r\n        if (mBooster != null) {\r\n            mBooster.release();\r\n        }\r\n\r\n        try {\r\n            mBooster = new LoudnessEnhancer(audioSessionId);\r\n            mBooster.setEnabled(mIsEnabled);\r\n\r\n            //double log2 = Math.log(mVolume) / Math.log(2);\r\n            //double gainMb = 10 * log2 * 100;\r\n            //mBooster.setTargetGain((int) gainMb);\r\n\r\n            double gainMb = 20 * Math.log10(mVolume * 3) * 100;\r\n            mBooster.setTargetGain((int) gainMb);\r\n\r\n            //mBooster.setTargetGain((int) (1000 * mVolume));\r\n\r\n            mIsSupported = true;\r\n        } catch (RuntimeException | UnsatisfiedLinkError | NoClassDefFoundError | NoSuchFieldError e) { // Cannot initialize effect engine\r\n            e.printStackTrace();\r\n            mIsSupported = false;\r\n        }\r\n    }\r\n\r\n    public boolean isEnabled() {\r\n        return mIsEnabled;\r\n    }\r\n\r\n    public void setEnabled(boolean enabled) {\r\n        mIsEnabled = enabled;\r\n        if (mBooster != null) {\r\n            mBooster.setEnabled(enabled);\r\n        }\r\n    }\r\n\r\n    public boolean isSupported() {\r\n        return mIsSupported;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/selector/ExoFormatItem.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.selector;\r\n\r\nimport android.text.TextUtils;\r\n\r\nimport androidx.annotation.NonNull;\r\nimport androidx.annotation.Nullable;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.util.MimeTypes;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.MediaTrack;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.SubtitleTrack;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\nimport java.util.Set;\r\n\r\npublic class ExoFormatItem implements FormatItem {\r\n    public static final int RESOLUTION_FHD = 3;\r\n    public static final int RESOLUTION_HD = 2;\r\n    public static final int RESOLUTION_SD = 1;\r\n    public static final int RESOLUTION_LD = 0;\r\n    public static final int FORMAT_AVC = 0;\r\n    public static final int FORMAT_VP9 = 1;\r\n    public static final int FORMAT_MP4A = 2;\r\n    public static final int FPS_30 = 0;\r\n    private int mType;\r\n    private int mId;\r\n    private CharSequence mTitle;\r\n    private MediaTrack mTrack;\r\n    private boolean mIsSelected;\r\n    private boolean mIsDefault;\r\n    private float mFrameRate;\r\n    private int mWidth;\r\n    private int mHeight;\r\n    private String mCodecs;\r\n    private String mLanguage;\r\n    private String mFormatId;\r\n    private boolean mIsPreset;\r\n\r\n    public static List<FormatItem> from(Set<MediaTrack> mediaTracks) {\r\n        if (mediaTracks == null) {\r\n            return null;\r\n        }\r\n\r\n        List<FormatItem> formats = new ArrayList<>();\r\n\r\n        for (MediaTrack track : mediaTracks) {\r\n            formats.add(from(track));\r\n        }\r\n\r\n        return formats;\r\n    }\r\n\r\n    public static ExoFormatItem from(MediaTrack track, boolean isPreset) {\r\n        if (track == null) {\r\n            return null;\r\n        }\r\n\r\n        track.isPreset = isPreset;\r\n        return from(track);\r\n    }\r\n\r\n    public static ExoFormatItem from(MediaTrack track) {\r\n        if (track == null) {\r\n            return null;\r\n        }\r\n\r\n        ExoFormatItem videoFormatItem = from(track.format);\r\n\r\n        videoFormatItem.mType = track.rendererIndex;\r\n        videoFormatItem.mIsSelected = track.isSelected;\r\n        videoFormatItem.mTrack = track;\r\n        videoFormatItem.mIsPreset = track.isPreset;\r\n\r\n        return videoFormatItem;\r\n    }\r\n\r\n    public static ExoFormatItem from(Format format) {\r\n        ExoFormatItem formatItem = new ExoFormatItem();\r\n\r\n        if (format != null) {\r\n            formatItem.mTitle = TrackSelectorUtil.buildTrackNameShort(format);\r\n            // Workaround with LIVE streams. Where no fps info present.\r\n            formatItem.mFrameRate = format.frameRate == -1 ? 30 : format.frameRate;\r\n            formatItem.mWidth = format.width;\r\n            formatItem.mHeight = format.height;\r\n            formatItem.mCodecs = format.codecs;\r\n            formatItem.mLanguage = format.language;\r\n            formatItem.mType = getType(format);\r\n\r\n            if (format.id != null) {\r\n                formatItem.mId = format.id.hashCode();\r\n                formatItem.mFormatId = format.id;\r\n            }\r\n        } else {\r\n            formatItem.mIsDefault = true; // fake auto track\r\n        }\r\n\r\n        return formatItem;\r\n    }\r\n\r\n    private static int getType(Format format) {\r\n        String sampleMimeType = format.sampleMimeType;\r\n\r\n        return MimeTypes.isVideo(sampleMimeType) ? TYPE_VIDEO : MimeTypes.isAudio(sampleMimeType) ? TYPE_AUDIO : TYPE_SUBTITLE;\r\n    }\r\n\r\n    @NonNull\r\n    @Override\r\n    public String toString() {\r\n        int rendererIndex = -1;\r\n        String codecs = \"\";\r\n        int width = -1;\r\n        int height = -1;\r\n        float frameRate = -1;\r\n        String language = \"\";\r\n        String id = \"\";\r\n        boolean isPreset = false;\r\n        int bitrate = -1;\r\n        boolean isDrc = false;\r\n\r\n        if (mTrack != null) {\r\n            rendererIndex = mTrack.rendererIndex;\r\n            isPreset = mTrack.isPreset;\r\n            Format format = mTrack.format;\r\n            if (format != null) {\r\n                id = format.id;\r\n                codecs = format.codecs;\r\n                width = format.width;\r\n                height = format.height;\r\n                frameRate = format.frameRate;\r\n                language = format.language;\r\n                bitrate = format.bitrate;\r\n                isDrc = format.isDrc;\r\n            }\r\n        }\r\n\r\n        return String.format(\"%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\", mType, rendererIndex, id, codecs, width, height, frameRate, language, isPreset, bitrate, isDrc);\r\n    }\r\n\r\n    @Override\r\n    public boolean equals(@Nullable Object obj) {\r\n        if (obj instanceof ExoFormatItem) {\r\n            ExoFormatItem formatItem = (ExoFormatItem) obj;\r\n\r\n            switch (mType) {\r\n                case TYPE_VIDEO:\r\n                case TYPE_AUDIO:\r\n                    // NOTE: Don't compare subs by formatId (it's non-constant)\r\n                    if (mFormatId != null && formatItem.mFormatId != null) {\r\n                        return mType == formatItem.mType &&\r\n                                Helpers.equals(mFormatId, formatItem.mFormatId); // instead of compare by bitrate\r\n                    }\r\n                    return mIsPreset == formatItem.mIsPreset &&\r\n                            mType == formatItem.mType &&\r\n                            mFrameRate == formatItem.mFrameRate &&\r\n                            mWidth == formatItem.mWidth &&\r\n                            mHeight == formatItem.mHeight &&\r\n                            Helpers.equals(mCodecs, formatItem.mCodecs) &&\r\n                            Helpers.contains(SubtitleTrack.trim(mLanguage), SubtitleTrack.trim(formatItem.mLanguage));\r\n                case TYPE_SUBTITLE:\r\n                    return mType == formatItem.mType &&\r\n                            Helpers.contains(SubtitleTrack.trim(mLanguage), SubtitleTrack.trim(formatItem.mLanguage));\r\n            }\r\n        }\r\n\r\n        return super.equals(obj);\r\n    }\r\n\r\n    public static ExoFormatItem from(int type, int rendererIndex, String id, String codecs,\r\n                                     int width, int height, float frameRate, String language,\r\n                                     boolean isPreset, int bitrate, boolean isDrc) {\r\n        MediaTrack mediaTrack = MediaTrack.forRendererIndex(rendererIndex);\r\n\r\n        if (mediaTrack == null) {\r\n            return null;\r\n        }\r\n\r\n        mediaTrack.isPreset = isPreset;\r\n\r\n        // audio/video disabled\r\n        if (TextUtils.isEmpty(id) && TextUtils.isEmpty(codecs)) {\r\n            return from(mediaTrack);\r\n        }\r\n\r\n        // NOTE: Create fake format. It's used in app internal comparison routine.\r\n        switch (type) {\r\n            case TYPE_VIDEO:\r\n                mediaTrack.format = Format.createVideoSampleFormat(\r\n                        id, null, codecs, -1, -1, width, height,\r\n                        frameRate, null, null);\r\n                break;\r\n            case TYPE_AUDIO:\r\n                mediaTrack.format = Format.createAudioSampleFormat(\r\n                        id, null, codecs, bitrate, -1,0, 0,\r\n                        null, null, 0, language, isDrc);\r\n                break;\r\n            case TYPE_SUBTITLE:\r\n                mediaTrack.format = Format.createTextSampleFormat(\r\n                        id, null, -1, language);\r\n                break;\r\n        }\r\n\r\n        return from(mediaTrack);\r\n    }\r\n\r\n    public static ExoFormatItem from(String spec) {\r\n        if (spec == null) {\r\n            return null;\r\n        }\r\n\r\n        String[] split = spec.split(\",\");\r\n\r\n        if (split.length == 9) {\r\n            split = Helpers.appendArray(split, new String[]{\"-1\"});\r\n        } else if (split.length == 10) {\r\n            split = Helpers.appendArray(split, new String[]{\"false\"});\r\n        }\r\n\r\n        if (split.length != 11) {\r\n            return null;\r\n        }\r\n\r\n        int type = Helpers.parseInt(split[0]);\r\n        int rendererIndex = Helpers.parseInt(split[1]);\r\n        String id = Helpers.parseStr(split[2]);\r\n        String codecs = Helpers.parseStr(split[3]);\r\n        int width = Helpers.parseInt(split[4]);\r\n        int height = Helpers.parseInt(split[5]);\r\n        float frameRate = Helpers.parseFloat(split[6]);\r\n        String language = Helpers.parseStr(split[7]);\r\n        boolean isPreset = Helpers.parseBoolean(split[8]);\r\n        int bitrate = Helpers.parseInt(split[9]);\r\n        boolean isDrc = Helpers.parseBoolean(split[10]);\r\n\r\n        return from(type, rendererIndex, id, codecs, width, height, frameRate, language, isPreset, bitrate, isDrc);\r\n    }\r\n\r\n    /**\r\n     * \"2560,1440,30,vp9\"\r\n     */\r\n    public static ExoFormatItem fromVideoSpec(String spec, boolean isPreset) {\r\n        if (spec == null) {\r\n            return null;\r\n        }\r\n\r\n        String[] split = spec.split(\",\");\r\n\r\n        if (split.length != 4) {\r\n            return null;\r\n        }\r\n\r\n        int width = Helpers.parseInt(split[0]);\r\n        int height = Helpers.parseInt(split[1]);\r\n        float frameRate = Helpers.parseFloat(split[2]);\r\n        String codec = split[3];\r\n\r\n        return from(TYPE_VIDEO, TrackSelectorManager.RENDERER_INDEX_VIDEO, null, codec, width, height, frameRate,null, isPreset, -1, false);\r\n    }\r\n\r\n    public static FormatItem fromVideoParams(int resolution, int format, int frameRate) {\r\n        ExoFormatItem formatItem = new ExoFormatItem();\r\n        MediaTrack mediaTrack = MediaTrack.forRendererIndex(TrackSelectorManager.RENDERER_INDEX_VIDEO);\r\n        formatItem.mTrack = mediaTrack;\r\n        formatItem.mType = TYPE_VIDEO;\r\n\r\n        int width = Integer.MAX_VALUE;\r\n        int height = Integer.MAX_VALUE;\r\n        String codec = null;\r\n        int fps = 30;\r\n\r\n        switch (resolution) {\r\n            case RESOLUTION_FHD:\r\n                width = 1920;\r\n                height = 1080;\r\n                break;\r\n            case RESOLUTION_HD:\r\n                width = 1280;\r\n                height = 720;\r\n                break;\r\n            case RESOLUTION_SD:\r\n                width = 640;\r\n                height = 360;\r\n                break;\r\n            case RESOLUTION_LD:\r\n                width = 426;\r\n                height = 240;\r\n                break;\r\n        }\r\n\r\n        switch (format) {\r\n            case FORMAT_AVC:\r\n                codec = \"avc\";\r\n                break;\r\n        }\r\n\r\n        switch (frameRate) {\r\n            case FPS_30:\r\n                fps = 30;\r\n                break;\r\n        }\r\n\r\n        // Fake format. It's used in app internal comparison routine.\r\n        mediaTrack.format = Format.createVideoSampleFormat(\r\n                null, null, codec, -1, -1, width, height, fps, null, null);\r\n\r\n        return formatItem;\r\n    }\r\n\r\n    public static ExoFormatItem fromAudioData(int format) {\r\n        ExoFormatItem formatItem = new ExoFormatItem();\r\n        MediaTrack mediaTrack = MediaTrack.forRendererIndex(TrackSelectorManager.RENDERER_INDEX_AUDIO);\r\n        formatItem.mTrack = mediaTrack;\r\n        formatItem.mType = TYPE_AUDIO;\r\n\r\n        String codec = null;\r\n\r\n        switch (format) {\r\n            case FORMAT_MP4A:\r\n            default:\r\n                codec = \"mp4a\";\r\n                break;\r\n        }\r\n\r\n        // Fake format. It's used in app internal comparison routine.\r\n        mediaTrack.format = Format.createAudioSampleFormat(\r\n                null, null, codec, -1, -1,0, 0, null, null, 0, null);\r\n\r\n        return formatItem;\r\n    }\r\n\r\n    /**\r\n     * Codec and language (lower case) delimited by comma\r\n     * @param spec codec, language\r\n     */\r\n    public static ExoFormatItem fromAudioSpecs(String spec) {\r\n        if (spec == null) {\r\n            return null;\r\n        }\r\n\r\n        String[] split = spec.split(\",\");\r\n\r\n        if (split.length != 2) {\r\n            return null;\r\n        }\r\n\r\n        String codec = Helpers.parseStr(split[0]);\r\n        String language = Helpers.parseStr(split[1]);\r\n\r\n        return from(TYPE_AUDIO, TrackSelectorManager.RENDERER_INDEX_AUDIO, null, codec, 0, 0, 0, language, false, -1, false);\r\n    }\r\n\r\n    public static FormatItem fromSubtitleParams(String langCode) {\r\n        if (langCode != null) {\r\n            // Only first part or lang code is accepted.\r\n            // E.g.: en, ru...\r\n            langCode = langCode.split(\"_\")[0];\r\n        }\r\n\r\n        ExoFormatItem formatItem = new ExoFormatItem();\r\n        MediaTrack mediaTrack = MediaTrack.forRendererIndex(TrackSelectorManager.RENDERER_INDEX_SUBTITLE);\r\n        formatItem.mTrack = mediaTrack;\r\n        formatItem.mType = TYPE_SUBTITLE;\r\n        formatItem.mLanguage = langCode;\r\n        formatItem.mIsDefault = langCode == null;\r\n\r\n        mediaTrack.format = Format.createTextSampleFormat(null, null, -1, langCode);\r\n\r\n        return formatItem;\r\n    }\r\n\r\n    @Override\r\n    public int getId() {\r\n        return mId;\r\n    }\r\n\r\n    @Override\r\n    public String getFormatId() {\r\n        return mFormatId;\r\n    }\r\n\r\n    @Override\r\n    public CharSequence getTitle() {\r\n        return mTitle;\r\n    }\r\n\r\n    @Override\r\n    public boolean isSelected() {\r\n        return mIsSelected;\r\n    }\r\n\r\n    @Override\r\n    public boolean isDefault() {\r\n        return mIsDefault;\r\n    }\r\n\r\n    @Override\r\n    public float getFrameRate() {\r\n        return mFrameRate;\r\n    }\r\n\r\n    @Override\r\n    public String getLanguage() {\r\n        return mLanguage;\r\n    }\r\n\r\n    @Override\r\n    public int getWidth() {\r\n        return mWidth;\r\n    }\r\n\r\n    @Override\r\n    public int getHeight() {\r\n        return mHeight;\r\n    }\r\n\r\n    @Override\r\n    public int getType() {\r\n        return mType;\r\n    }\r\n\r\n    @Override\r\n    public boolean isPreset() {\r\n        return mIsPreset;\r\n    }\r\n\r\n    @Override\r\n    public MediaTrack getTrack() {\r\n        return mTrack;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/selector/FormatItem.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.selector;\r\n\r\nimport androidx.annotation.NonNull;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.MediaTrack;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.VideoTrack;\r\n\r\npublic interface FormatItem {\r\n    FormatItem NO_VIDEO = ExoFormatItem.from(MediaTrack.forRendererIndex(TrackSelectorManager.RENDERER_INDEX_VIDEO));\r\n    FormatItem NO_AUDIO = ExoFormatItem.from(MediaTrack.forRendererIndex(TrackSelectorManager.RENDERER_INDEX_AUDIO));\r\n    FormatItem VIDEO_AUTO = ExoFormatItem.fromVideoParams(-1, -1, -1);\r\n    FormatItem VIDEO_SUB_SD_AVC_30 = ExoFormatItem.fromVideoSpec(\"426,240,30,avc\", false);\r\n    FormatItem VIDEO_SD_AVC_30 = ExoFormatItem.fromVideoSpec(\"640,360,30,avc\", false);\r\n    FormatItem VIDEO_HD_AVC_30 = ExoFormatItem.fromVideoSpec(\"1280,720,30,avc\", false);\r\n    FormatItem VIDEO_FHD_AVC_30 = ExoFormatItem.fromVideoSpec(\"1920,1080,30,avc\", false);\r\n    FormatItem VIDEO_FHD_AVC_60 = ExoFormatItem.fromVideoSpec(\"1920,1080,60,avc\", false);\r\n    FormatItem VIDEO_FHD_VP9_60 = ExoFormatItem.fromVideoSpec(\"1920,1080,60,vp9\", false);\r\n    FormatItem VIDEO_4K_VP9_60 = ExoFormatItem.fromVideoSpec(\"3840,2160,60,vp9\", false);\r\n    FormatItem SUBTITLE_NONE = ExoFormatItem.fromSubtitleParams(null);\r\n    FormatItem AUDIO_HQ_MP4A = ExoFormatItem.fromAudioSpecs(String.format(\"%s,null\", \"mp4a\")); // Note, 5.1 mp4a doesn't work in 5.1 mode\r\n    FormatItem AUDIO_51_EC3 = ExoFormatItem.fromAudioSpecs(String.format(\"%s,null\", \"ec-3\")); // Note, 5.1 mp4a doesn't work in 5.1 mode\r\n    FormatItem AUDIO_51_AC3 = ExoFormatItem.fromAudioSpecs(String.format(\"%s,null\", \"ac-3\")); // Note, 5.1 mp4a doesn't work in 5.1 mode\r\n    int TYPE_VIDEO = 0;\r\n    int TYPE_AUDIO = 1;\r\n    int TYPE_SUBTITLE = 2;\r\n    int getId();\r\n    String getFormatId();\r\n    CharSequence getTitle();\r\n    boolean isDefault();\r\n    boolean isSelected();\r\n    boolean isPreset();\r\n    float getFrameRate();\r\n    String getLanguage();\r\n    int getWidth();\r\n    int getHeight();\r\n    int getType();\r\n    MediaTrack getTrack();\r\n\r\n    static FormatItem checkFormat(FormatItem format, int type) {\r\n        return format != null && format.getType() == type ? format : null;\r\n    }\r\n\r\n    static @NonNull FormatItem fromLanguage(String langCode) {\r\n        return ExoFormatItem.fromSubtitleParams(langCode);\r\n    }\r\n\r\n    static MediaTrack toMediaTrack(FormatItem item) {\r\n        return item != null ? item.getTrack() : null;\r\n    }\r\n\r\n    class VideoPreset {\r\n        public final String name;\r\n        public final FormatItem format;\r\n\r\n        public VideoPreset(String presetName, String presetSpec) {\r\n            this.name = presetName;\r\n            // \"2560,1440,30,vp9\"\r\n            this.format = ExoFormatItem.fromVideoSpec(presetSpec, true);\r\n        }\r\n\r\n        public boolean isVP9Preset() {\r\n            return name != null && name.contains(\"vp9\");\r\n        }\r\n\r\n        public boolean isAV1Preset() {\r\n            return name != null && name.contains(\"av01\");\r\n        }\r\n\r\n        public int getWidth() {\r\n            return format != null ? format.getWidth() : -1;\r\n        }\r\n\r\n        public int getHeight() {\r\n            return format != null ? format.getHeight() : -1;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/selector/TrackInfoFormatter2.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.selector;\r\n\r\nimport com.google.android.exoplayer2.Format;\r\n\r\npublic class TrackInfoFormatter2 {\r\n    private String mResolutionStr;\r\n    private String mFpsStr;\r\n    private String mCodecStr;\r\n    private String mBitrateStr;\r\n    private String mHdrStr;\r\n    private String mSpeedStr;\r\n    private String mChannelsStr;\r\n    private String mHighBitrateStr;\r\n    private boolean mEnableBitrate;\r\n    private String mDrcStr;\r\n\r\n    public void setFormat(Format format) {\r\n        if (TrackSelectorUtil.isVideo(format)) {\r\n            setVideoFormat(format);\r\n        } else if (TrackSelectorUtil.isAudio(format)) {\r\n            setAudioFormat(format);\r\n        }\r\n    }\r\n\r\n    public void setVideoFormat(Format format) {\r\n        if (format == null) {\r\n            return;\r\n        }\r\n\r\n        mResolutionStr = TrackSelectorUtil.getShortResolutionLabel(format);\r\n\r\n        int fpsNum = extractFps(format);\r\n        mFpsStr = fpsNum == 0 ? \"\" : String.valueOf(fpsNum);\r\n\r\n        String codec = TrackSelectorUtil.extractCodec(format);\r\n        mCodecStr = codec != null ? codec.toUpperCase() : \"\";\r\n\r\n        if (mEnableBitrate) {\r\n            String bitrate = TrackSelectorUtil.extractBitrate(format, 0);\r\n            mBitrateStr = bitrate.isEmpty() ? \"\" : bitrate.toUpperCase() + \"Mb\";\r\n        }\r\n\r\n        mHdrStr = TrackSelectorUtil.buildHDRString(format);\r\n\r\n        mHighBitrateStr = TrackSelectorUtil.buildHighBitrateMark(format);\r\n    }\r\n\r\n    public void setAudioFormat(Format format) {\r\n        if (format == null) {\r\n            return;\r\n        }\r\n\r\n        mChannelsStr = TrackSelectorUtil.buildChannels(format);\r\n\r\n        mDrcStr = TrackSelectorUtil.buildDrcMark(format);\r\n    }\r\n\r\n    public void setSpeed(float speed) {\r\n        mSpeedStr = speed != 1.0f ? speed + \"x\" : \"\";\r\n    }\r\n\r\n    public String getQualityLabel() {\r\n        return combine(mResolutionStr, mFpsStr, mCodecStr, mBitrateStr, mHdrStr, mChannelsStr, mSpeedStr, mHighBitrateStr, mDrcStr);\r\n    }\r\n\r\n    private static String combine(String... items) {\r\n        String separator = \"/\";\r\n        StringBuilder result = new StringBuilder();\r\n\r\n        if (items != null && items.length != 0) {\r\n            int index = 0;\r\n\r\n            for (String item : items) {\r\n                if (item == null || item.isEmpty()) {\r\n                    continue;\r\n                }\r\n\r\n                if (index != 0) {\r\n                    result.append(separator);\r\n                }\r\n\r\n                result.append(item);\r\n                index++;\r\n            }\r\n        }\r\n\r\n        return result.toString();\r\n    }\r\n\r\n    private static int extractFps(Format format) {\r\n        return format.frameRate == Format.NO_VALUE ? 0 : Math.round(format.frameRate);\r\n    }\r\n\r\n    public void enableBitrate(boolean enable) {\r\n        mEnableBitrate = enable;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/selector/TrackSelectorManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.selector;\n\nimport android.content.Context;\nimport android.os.Build;\nimport android.text.TextUtils;\nimport android.util.Pair;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\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.TrackSelection.Definition;\nimport com.liskovsoft.sharedutils.helpers.Helpers;\nimport com.liskovsoft.sharedutils.mylogger.Log;\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.AudioTrack;\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.MediaTrack;\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.VideoTrack;\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.selector.RestoreTrackSelector;\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.selector.RestoreTrackSelector.TrackSelectorCallback;\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.SortedSet;\nimport java.util.TreeSet;\n\npublic class TrackSelectorManager implements TrackSelectorCallback {\n    private final Context mContext;\n    public static final int RENDERER_INDEX_UNKNOWN = -1;\n    public static final int RENDERER_INDEX_VIDEO = 0;\n    public static final int RENDERER_INDEX_AUDIO = 1;\n    public static final int RENDERER_INDEX_SUBTITLE = 2;\n    private static final String TAG = TrackSelectorManager.class.getSimpleName();\n    private static final String DEFAULT_LANGUAGE = \"original\"; // original, descriptive, dubbed, secondary\n    private final Map<String, Integer> mBlacklist = new HashMap<>();\n\n    private DefaultTrackSelector mTrackSelector;\n\n    private final Renderer[] mRenderers = new Renderer[3];\n    private final MediaTrack[] mSelectedTracks = new MediaTrack[3];\n    private boolean mIsMergedSource;\n\n    public TrackSelectorManager(Context context) {\n        mContext = context.getApplicationContext();\n    }\n\n    public void invalidate() {\n        Arrays.fill(mRenderers, null);\n    }\n\n    /**\n     * Shows the selection dialog for a given renderer.\n     * @param rendererIndex The index of the renderer. <br/>\n     *                      One of the {@link #RENDERER_INDEX_VIDEO}, {@link #RENDERER_INDEX_AUDIO}, {@link #RENDERER_INDEX_SUBTITLE}\n     */\n    private Set<MediaTrack> getAvailableTracks(int rendererIndex) {\n        initRenderer(rendererIndex);\n\n        if (mRenderers[rendererIndex] == null) {\n            return null;\n        }\n\n        return mRenderers[rendererIndex].sortedTracks;\n    }\n\n    /**\n     * Creates renderer structure.\n     * @param rendererIndex The index of the renderer. <br/>\n     *                      One of the {@link #RENDERER_INDEX_VIDEO}, {@link #RENDERER_INDEX_AUDIO}, {@link #RENDERER_INDEX_SUBTITLE}\n     */\n    private void initRenderer(int rendererIndex) {\n        if (mRenderers[rendererIndex] != null && mRenderers[rendererIndex].mediaTracks != null) {\n            return;\n        }\n\n        if (mTrackSelector == null) {\n            Log.e(TAG, \"Can't init renderer %s. TrackSelector is null!\", rendererIndex);\n            return;\n        }\n\n        initTrackGroups(rendererIndex, mTrackSelector.getCurrentMappedTrackInfo(), mTrackSelector.getParameters());\n        initMediaTracks(rendererIndex);\n    }\n\n    /**\n     * Creates renderer structure.\n     * @param rendererIndex The index of the renderer. <br/>\n     *                      One of the {@link #RENDERER_INDEX_VIDEO}, {@link #RENDERER_INDEX_AUDIO}, {@link #RENDERER_INDEX_SUBTITLE}\n     * @param trackInfo supplied externally from {@link RestoreTrackSelector}\n     * @param parameters supplied externally from {@link RestoreTrackSelector}\n     */\n    private void initRenderer(int rendererIndex, MappedTrackInfo trackInfo, Parameters parameters) {\n        if (mRenderers[rendererIndex] != null && mRenderers[rendererIndex].mediaTracks != null) {\n            return;\n        }\n\n        initTrackGroups(rendererIndex, trackInfo, parameters);\n        initMediaTracks(rendererIndex);\n    }\n\n    /**\n     * Creates renderer structure.\n     * @param rendererIndex The index of the renderer. <br/>\n     *                      One of the {@link #RENDERER_INDEX_VIDEO}, {@link #RENDERER_INDEX_AUDIO}, {@link #RENDERER_INDEX_SUBTITLE}\n     * @param groups supplied externally from {@link RestoreTrackSelector}\n     * @param parameters supplied externally from {@link RestoreTrackSelector}\n     */\n    private void initRenderer(int rendererIndex, TrackGroupArray groups, Parameters parameters) {\n        if (mRenderers[rendererIndex] != null && mRenderers[rendererIndex].mediaTracks != null) {\n            return;\n        }\n\n        initTrackGroups(rendererIndex, groups, parameters);\n        initMediaTracks(rendererIndex);\n    }\n\n    private void initTrackGroups(int rendererIndex, MappedTrackInfo trackInfo, Parameters parameters) {\n        if (trackInfo == null) {\n            Log.e(TAG, \"Can't perform track selection. Mapped track info isn't initialized yet!\");\n            return;\n        }\n\n        if (parameters == null) {\n            Log.e(TAG, \"Can't perform track selection. Track parameters isn't initialized yet!\");\n            return;\n        }\n\n        TrackGroupArray trackGroups = trackInfo.getTrackGroups(rendererIndex);\n\n        initTrackGroups(rendererIndex, trackGroups, parameters);\n    }\n\n    private void initTrackGroups(int rendererIndex, TrackGroupArray trackGroups, Parameters parameters) {\n        Renderer renderer = new Renderer();\n        mRenderers[rendererIndex] = renderer;\n\n        renderer.trackGroups = trackGroups;\n        renderer.isDisabled = parameters.getRendererDisabled(rendererIndex);\n    }\n\n    private void initMediaTracks(int rendererIndex) {\n        if (mRenderers[rendererIndex] == null) {\n            return;\n        }\n\n        Renderer renderer = mRenderers[rendererIndex];\n        renderer.mediaTracks = new MediaTrack[renderer.trackGroups.length][];\n        // Fix for java.util.ConcurrentModificationException inside of:\n        // com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.ExoFormatItem.from (ExoFormatItem.java:44)\n        // Won't help: renderer.sortedTracks = Collections.synchronizedSortedSet(new TreeSet<>(new MediaTrackFormatComparator()));\n        SortedSet<MediaTrack> sortedTracks = new TreeSet<>(new MediaTrackFormatComparator());\n\n        // AUTO OPTION: add disable track option\n        MediaTrack noMediaTrack = MediaTrack.forRendererIndex(rendererIndex);\n        // Temporal selection.\n        // Real selection will be override later on setSelection() routine.\n        noMediaTrack.isSelected = true;\n        sortedTracks.add(noMediaTrack);\n        renderer.selectedTrack = noMediaTrack;\n\n        //if (rendererIndex == RENDERER_INDEX_SUBTITLE) {\n        //    // AUTO OPTION: add disable subs option\n        //    MediaTrack noSubsTrack = MediaTrack.forRendererIndex(rendererIndex);\n        //    // Temporal selection.\n        //    // Real selection will be override later on setSelection() routine.\n        //    noSubsTrack.isSelected = true;\n        //    sortedTracks.add(noSubsTrack);\n        //    renderer.selectedTrack = noSubsTrack;\n        //} else if (rendererIndex == RENDERER_INDEX_AUDIO) {\n        //    MediaTrack noAudioTrack = MediaTrack.forRendererIndex(rendererIndex);\n        //    noAudioTrack.isSelected = true;\n        //    sortedTracks.add(noAudioTrack);\n        //    renderer.selectedTrack = noAudioTrack;\n        //} else if (rendererIndex == RENDERER_INDEX_VIDEO) {\n        //    MediaTrack noVideoTrack = MediaTrack.forRendererIndex(rendererIndex);\n        //    noVideoTrack.isSelected = true;\n        //    sortedTracks.add(noVideoTrack);\n        //    renderer.selectedTrack = noVideoTrack;\n        //}\n\n        for (int groupIndex = 0; groupIndex < renderer.trackGroups.length; groupIndex++) {\n            TrackGroup group = renderer.trackGroups.get(groupIndex);\n            renderer.mediaTracks[groupIndex] = new MediaTrack[group.length];\n\n            for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {\n                Format format = group.getFormat(trackIndex);\n\n                MediaTrack mediaTrack = MediaTrack.forRendererIndex(rendererIndex);\n                mediaTrack.format = format;\n                mediaTrack.groupIndex = groupIndex;\n                mediaTrack.trackIndex = trackIndex;\n\n                if (isErrorInAudio(mediaTrack)) {\n                    continue;\n                }\n\n                if (!isTrackUnique(mediaTrack)) {\n                    continue;\n                }\n\n                if (!PlayerTweaksData.instance(mContext).isAllFormatsUnlocked() && !Utils.isFormatSupported(mediaTrack)) {\n                    continue;\n                }\n\n                // Selected track or not will be decided later in setSelection() routine\n\n                renderer.mediaTracks[groupIndex][trackIndex] = mediaTrack;\n                sortedTracks.add(mediaTrack);\n            }\n        }\n\n        // Late assign to fix ConcurrentModificationException\n        renderer.sortedTracks = sortedTracks;\n\n        mBlacklist.clear();\n    }\n\n    /**\n     * We need to circle through the tracks to remove previously selected marks\n     */\n    private void setSelection(int rendererIndex, int trackGroupIndex, int... trackIndexes) {\n        if (mRenderers[rendererIndex] == null) {\n            return;\n        }\n\n        // Adaptive selection should be disabled in RestoreTrackSelector (e.g trackIndexes.length == 1)\n\n        // We need to circle through the tracks to remove previously selected marks.\n\n        Renderer renderer = mRenderers[rendererIndex];\n        renderer.selectedTrack = null;\n        for (int groupIndex = 0; groupIndex < renderer.mediaTracks.length; groupIndex++) {\n            MediaTrack[] trackGroup = renderer.mediaTracks[groupIndex];\n\n            if (trackGroup == null) {\n                continue;\n            }\n\n            for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {\n                MediaTrack mediaTrack = trackGroup[trackIndex];\n\n                if (mediaTrack == null) {\n                    continue;\n                }\n\n                mediaTrack.isSelected = groupIndex == trackGroupIndex && Helpers.equalsAny(trackIndex, trackIndexes) && !renderer.isDisabled;\n\n                if (mediaTrack.isSelected) {\n                    renderer.selectedTrack = mediaTrack;\n                }\n            }\n        }\n\n        // Special handling for tracks with auto option\n        MediaTrack noMediaTrack = renderer.sortedTracks.first();\n        noMediaTrack.isSelected = renderer.selectedTrack == null;\n        renderer.isDisabled = renderer.selectedTrack == null;\n        renderer.selectedTrack = renderer.selectedTrack == null ? noMediaTrack : renderer.selectedTrack;\n\n        //// Special handling for tracks with auto option\n        //if (rendererIndex == RENDERER_INDEX_SUBTITLE && renderer.selectedTrack == null) { // no subs selected\n        //    MediaTrack noSubsTrack = renderer.sortedTracks.first();\n        //    noSubsTrack.isSelected = true;\n        //    renderer.selectedTrack = noSubsTrack;\n        //} else if (rendererIndex == RENDERER_INDEX_AUDIO && renderer.selectedTrack == null) { // no audio selected\n        //    MediaTrack noAudioTrack = renderer.sortedTracks.first();\n        //    noAudioTrack.isSelected = true;\n        //    renderer.selectedTrack = noAudioTrack;\n        //    renderer.isDisabled = true;\n        //} else if (rendererIndex == RENDERER_INDEX_VIDEO && renderer.selectedTrack == null) { // no video selected\n        //    MediaTrack noVideoTrack = renderer.sortedTracks.first();\n        //    noVideoTrack.isSelected = true;\n        //    renderer.selectedTrack = noVideoTrack;\n        //    renderer.isDisabled = true;\n        //}\n    }\n\n    private void enableAutoSelection(int rendererIndex) {\n        mRenderers[rendererIndex].isDisabled = false;\n        mRenderers[rendererIndex].selectedTrack = null;\n    }\n\n    public Set<MediaTrack> getVideoTracks() {\n        return getAvailableTracks(RENDERER_INDEX_VIDEO);\n    }\n\n    public Set<MediaTrack> getAudioTracks() {\n        return getAvailableTracks(RENDERER_INDEX_AUDIO);\n    }\n\n    public Set<MediaTrack> getSubtitleTracks() {\n        return getAvailableTracks(RENDERER_INDEX_SUBTITLE);\n    }\n\n    private Pair<Definition, MediaTrack> createSelection(TrackGroupArray groups, MediaTrack selectedTrack) {\n        if (selectedTrack == null) {\n            Log.e(TAG, \"Can't create selection. Selected track is null.\");\n            return null;\n        }\n\n        if (mRenderers[selectedTrack.rendererIndex] == null) {\n            Log.e(TAG, \"Can't create selection. Renderer isn't initialized.\");\n            return null;\n        }\n\n        Pair<Definition, MediaTrack> definitionPair = null;\n\n        MediaTrack matchedTrack = findBestMatch(selectedTrack);\n\n        if (matchedTrack.groupIndex != -1) {\n            Definition definition = new Definition(groups.get(matchedTrack.groupIndex), matchedTrack.trackIndex);\n            definitionPair = new Pair<>(definition, matchedTrack);\n            setSelection(matchedTrack.rendererIndex, matchedTrack.groupIndex, matchedTrack.trackIndex);\n        } else {\n            Log.e(TAG, \"Can't create selection. No match for the track %s\", selectedTrack);\n        }\n\n        return definitionPair;\n    }\n\n    private Pair<Definition, MediaTrack> createRendererSelection(int rendererIndex, TrackGroupArray groups, Parameters params) {\n        if (mSelectedTracks[rendererIndex] == null || params.hasSelectionOverride(rendererIndex, groups)) {\n            return null;\n        }\n\n        initRenderer(rendererIndex, groups, params);\n        return createSelection(groups, mSelectedTracks[rendererIndex]);\n    }\n\n    private void updateRendererSelection(int rendererIndex, TrackGroupArray groups, Parameters params, Definition definition) {\n        initRenderer(rendererIndex, groups, params);\n\n        definition = getOverride(rendererIndex, groups, params, definition);\n        \n        setSelection(rendererIndex, groups.indexOf(definition.group), definition.tracks);\n    }\n\n    private Definition getOverride(int rendererIndex, TrackGroupArray rendererTrackGroups, Parameters params, Definition original) {\n        Definition definition = original;\n\n        if (params.hasSelectionOverride(rendererIndex, rendererTrackGroups)) {\n            SelectionOverride override = params.getSelectionOverride(rendererIndex, rendererTrackGroups);\n\n            if (override != null) {\n                definition = new TrackSelection.Definition(\n                                rendererTrackGroups.get(override.groupIndex),\n                                override.tracks,\n                                override.reason,\n                                override.data);\n            }\n        }\n\n        return definition;\n    }\n\n    @Override\n    public Pair<Definition, MediaTrack> onSelectVideoTrack(TrackGroupArray groups, Parameters params) {\n        return createRendererSelection(RENDERER_INDEX_VIDEO, groups, params);\n    }\n\n    @Override\n    public Pair<Definition, MediaTrack> onSelectAudioTrack(TrackGroupArray groups, Parameters params) {\n        return createRendererSelection(RENDERER_INDEX_AUDIO, groups, params);\n    }\n\n    @Override\n    public Pair<Definition, MediaTrack> onSelectSubtitleTrack(TrackGroupArray groups, Parameters params) {\n        return createRendererSelection(RENDERER_INDEX_SUBTITLE, groups, params);\n    }\n\n    @Override\n    public void updateVideoTrackSelection(TrackGroupArray groups, Parameters params, Definition definition) {\n        updateRendererSelection(RENDERER_INDEX_VIDEO, groups, params, definition);\n    }\n\n    @Override\n    public void updateAudioTrackSelection(TrackGroupArray groups, Parameters params, Definition definition) {\n        updateRendererSelection(RENDERER_INDEX_AUDIO, groups, params, definition);\n    }\n\n    @Override\n    public void updateSubtitleTrackSelection(TrackGroupArray groups, Parameters params, Definition definition) {\n        updateRendererSelection(RENDERER_INDEX_SUBTITLE, groups, params, definition);\n    }\n\n    public void selectTrack(MediaTrack track) {\n        if (track == null) {\n            return;\n        }\n\n        int rendererIndex = track.rendererIndex;\n\n        initRenderer(rendererIndex);\n\n        mSelectedTracks[rendererIndex] = track;\n\n        if (mRenderers[rendererIndex] == null || mRenderers[rendererIndex].mediaTracks == null || mRenderers[rendererIndex].sortedTracks == null) {\n            Log.e(TAG, \"Renderer isn't initialized. Waiting for later selection...\");\n            return;\n        }\n\n        // enable renderer\n        mRenderers[rendererIndex].isDisabled = false;\n\n        MediaTrack matchedTrack = findBestMatch(track);\n        \n        setSelection(matchedTrack.rendererIndex, matchedTrack.groupIndex, matchedTrack.trackIndex);\n\n        // save immediately\n        applyOverride(rendererIndex);\n    }\n\n    /**\n     *  Video/audio tracks should be selected at this point.<br/>\n     *  Reselect if not done yet.\n     */\n    public void fixTracksSelection() {\n        for (MediaTrack track : mSelectedTracks) {\n            if (track == null || track.rendererIndex == RENDERER_INDEX_SUBTITLE) {\n                continue;\n            }\n\n            if (!hasSelection(track.rendererIndex)) {\n                Log.e(TAG, \"Oops. Track %s isn't selected before. Fixing...\", track.rendererIndex);\n                selectTrack(track);\n            }\n        }\n    }\n\n    public void setTrackSelector(DefaultTrackSelector selector) {\n        Log.d(TAG, \"Initializing TrackSelector...\");\n        mTrackSelector = selector;\n\n        if (selector instanceof RestoreTrackSelector) {\n            ((RestoreTrackSelector) selector).setOnTrackSelectCallback(this);\n        }\n    }\n\n    public void release() {\n        if (mTrackSelector != null) {\n            Log.d(TAG, \"Destroying TrackSelector...\");\n            if (mTrackSelector instanceof RestoreTrackSelector) {\n                ((RestoreTrackSelector) mTrackSelector).setOnTrackSelectCallback(null);\n            }\n            mTrackSelector = null;\n        }\n\n        invalidate();\n    }\n\n    public void setMergedSource(boolean mergedSource) {\n        mIsMergedSource = mergedSource;\n    }\n\n    private MediaTrack findBestMatch(MediaTrack originTrack) {\n        Log.d(TAG, \"findBestMatch: Starting: \" + originTrack.format);\n\n        Renderer renderer = mRenderers[originTrack.rendererIndex];\n\n        MediaTrack result = createAutoSelection(originTrack.rendererIndex);\n\n        if (originTrack.format != null) { // not auto selection\n            MediaTrack prevResult;\n            MediaTrack tmpResult = null;\n\n            MediaTrack[][] mediaTracks = filterByLanguage(renderer.mediaTracks, originTrack);\n\n            outerloop:\n            for (int groupIndex = 0; groupIndex < mediaTracks.length; groupIndex++) {\n                prevResult = result;\n\n                // Very rare NPE fix\n                MediaTrack[] trackGroup = mediaTracks[groupIndex];\n\n                if (trackGroup == null) {\n                    Log.e(TAG, \"Track selection error. Media track group %s is empty.\", groupIndex);\n                    continue;\n                }\n\n                for (MediaTrack mediaTrack : trackGroup) {\n                    if (mediaTrack == null) {\n                        continue;\n                    }\n\n                    // Fix muted audio on stream with higher bitrate than the target\n                    if (tmpResult == null) {\n                        tmpResult = mediaTrack;\n                    }\n\n                    int bounds = originTrack.inBounds(mediaTrack);\n\n                    // Multiple ru track fix (same id!) Example: https://www.youtube.com/watch?v=l3htINlc2ic\n                    if (bounds == 0 && MediaTrack.bitrateEquals(originTrack, mediaTrack)) {\n                        result = mediaTrack;\n                        break outerloop;\n                    }\n\n                    if (bounds >= 0) {\n                        int compare = mediaTrack.compare(result);\n\n                        //if (compare == 0) {\n                        //    if (MediaTrack.codecEquals(mediaTrack, originTrack)) {\n                        //        result = mediaTrack;\n                        //    }\n                        //} else if (compare > 0) {\n                        //    if (!MediaTrack.preferByCodec(result, mediaTrack)) {\n                        //        result = mediaTrack;\n                        //    }\n                        //}\n\n                        if (compare == 0 && MediaTrack.preferByDrc(originTrack, result, mediaTrack)) {\n                            if (MediaTrack.codecEquals(mediaTrack, originTrack)) {\n                                result = mediaTrack;\n                            } else if (!MediaTrack.codecEquals(result, originTrack) &&\n                                    !MediaTrack.preferByCodec(result, mediaTrack)) {\n                                result = mediaTrack;\n                            }\n                        } else if (compare > 0) {\n                            if (!MediaTrack.preferByCodec(result, mediaTrack)) {\n                                result = mediaTrack;\n                            }\n                        }\n                    }\n                }\n\n                // Don't let change the codec beside needed one.\n                // Handle situation where same codecs in different groups (e.g. subtitles).\n                if (MediaTrack.codecEquals(result, originTrack)) {\n                    if (MediaTrack.codecEquals(prevResult, originTrack) && prevResult.compare(result) > 0) {\n                        result = prevResult;\n                    }\n                } else if (MediaTrack.codecEquals(prevResult, originTrack)) {\n                    result = prevResult;\n                } else if (prevResult.compare(result) == 0) { // Formats are the same except the codecs\n                    if (MediaTrack.preferByCodec(prevResult, result)) {\n                        result = prevResult;\n                    }\n                }\n            }\n\n            // Fix muted audio/video on stream with higher bitrate than the target\n            if ((result instanceof AudioTrack || result instanceof VideoTrack) && result.isEmpty() && tmpResult != null) {\n                result = tmpResult;\n            }\n        }\n\n        Log.d(TAG, \"findBestMatch: Found: \" + result.format);\n\n        return result;\n    }\n\n    private void applyOverride(int rendererIndex) {\n        Renderer renderer = mRenderers[rendererIndex];\n\n        if (renderer == null) {\n            return;\n        }\n\n        mTrackSelector.setParameters(mTrackSelector.buildUponParameters().setRendererDisabled(rendererIndex, renderer.isDisabled));\n\n        MediaTrack selectedTrack = renderer.selectedTrack;\n\n        if (selectedTrack != null && selectedTrack.groupIndex != -1) {\n            Log.d(TAG, \"Setting override for renderer %s, group %s, track %s...\", rendererIndex, selectedTrack.groupIndex, selectedTrack.trackIndex);\n\n            mTrackSelector.setParameters(mTrackSelector.buildUponParameters().setSelectionOverride(\n                    rendererIndex, renderer.trackGroups, new SelectionOverride(selectedTrack.groupIndex, selectedTrack.trackIndex)\n            ));\n        } else {\n            Log.e(TAG, \"Something went wrong. Selected track not found for renderer=%s\", rendererIndex);\n            mTrackSelector.setParameters(mTrackSelector.buildUponParameters().clearSelectionOverrides(rendererIndex)); // Auto quality button selected\n        }\n    }\n\n    private MediaTrack createAutoSelection(int rendererIndex) {\n        return MediaTrack.forRendererIndex(rendererIndex);\n    }\n\n    private boolean hasSelection(int rendererIndex) {\n        return mRenderers[rendererIndex] != null && mRenderers[rendererIndex].selectedTrack != null;\n    }\n\n    /**\n     * Trying to filter languages preferred by the user\n     */\n    private MediaTrack[][] filterByLanguage(MediaTrack[][] trackGroupList, MediaTrack originTrack) {\n        if (!(originTrack instanceof AudioTrack) || trackGroupList.length <= 2) { // non-translated list has max 2 groups\n            return trackGroupList;\n        }\n\n        String audioLanguage = fixLangCode(PlayerData.instance(mContext).getAudioLanguage());\n\n        String resultLanguage = null;\n        String originLanguage = null;\n\n        if (originTrack.format != null && !TextUtils.isEmpty(originTrack.format.language)) {\n            originLanguage = originTrack.format.language;\n        }\n\n        if (originTrack.isSaved) {\n            resultLanguage = TextUtils.isEmpty(audioLanguage) ? null : audioLanguage; // None/Default selection (\"\")\n            originLanguage = null; // fallback to the default language\n        }\n\n        List<MediaTrack[]> resultTracks = null;\n        List<MediaTrack[]> originTracks = null;\n        List<MediaTrack[]> resultTracksFallback = null;\n\n        // Tracks are grouped by the language/formats\n        for (MediaTrack[] trackGroup : trackGroupList) {\n            if (trackGroup != null && trackGroup.length >= 1) {\n                MediaTrack mediaTrack = null;\n\n                // All tracks in the group have same language. Pick up first non empty.\n                for (MediaTrack track : trackGroup) {\n                    if (track != null) {\n                        mediaTrack = track;\n                        break;\n                    }\n                }\n\n                if (mediaTrack != null && mediaTrack.format != null) {\n                    if (resultLanguage != null && Helpers.startsWith(mediaTrack.format.language, resultLanguage)) { // en-us\n                        if (resultTracks == null) {\n                            resultTracks = new ArrayList<>();\n                        }\n\n                        resultTracks.add(trackGroup);\n                    } else if (originLanguage != null && Helpers.startsWith(mediaTrack.format.language, originLanguage)) {\n                        if (originTracks == null) {\n                            originTracks = new ArrayList<>();\n                        }\n\n                        originTracks.add(trackGroup);\n                    } else if (Helpers.contains(mediaTrack.format.language, DEFAULT_LANGUAGE)) { // original, descriptive, dubbed, secondary\n                        if (resultTracksFallback == null) {\n                            resultTracksFallback = new ArrayList<>();\n                        }\n\n                        resultTracksFallback.add(trackGroup);\n                    }\n                }\n            }\n        }\n\n        if (resultTracks != null && !resultTracks.isEmpty()) {\n            return resultTracks.toArray(new MediaTrack[0][]);\n        }\n\n        if (originTracks != null && !originTracks.isEmpty()) {\n            return originTracks.toArray(new MediaTrack[0][]);\n        }\n\n        if (resultTracksFallback != null && !resultTracksFallback.isEmpty()) {\n            return resultTracksFallback.toArray(new MediaTrack[0][]);\n        }\n\n        return trackGroupList;\n    }\n\n    // Track array manipulation.\n    private static int[] getTracksAdding(SelectionOverride override, int addedTrack) {\n        int[] tracks = override.tracks;\n        tracks = Arrays.copyOf(tracks, tracks.length + 1);\n        tracks[tracks.length - 1] = addedTrack;\n        return tracks;\n    }\n\n    private static int[] getTracksRemoving(SelectionOverride override, int removedTrack) {\n        int[] tracks = new int[override.length - 1];\n        int trackCount = 0;\n        for (int i = 0; i < tracks.length + 1; i++) {\n            int track = override.tracks[i];\n            if (track != removedTrack) {\n                tracks[trackCount++] = track;\n            }\n        }\n        return tracks;\n    }\n\n    /**\n     * Get the number of tracks with the same resolution.\n     * <p>I assume that the tracks already have been sorted in descendants order. <br/>\n     * <p>Details: {@code com.liskovsoft.smartyoutubetv.flavors.exoplayer.youtubeinfoparser.mpdbuilder.MyMPDBuilder}\n     * @param group the group\n     * @param trackIndex current track in group\n     * @return\n     */\n    private int getRelatedTrackOffsets(TrackGroup group, int trackIndex) {\n        int prevHeight = 0;\n        int offset = 0;\n        for (int i = trackIndex; i > 0; i--) {\n            Format format = group.getFormat(i);\n            if (prevHeight == 0) {\n                prevHeight = format.height;\n            } else if (prevHeight == format.height) {\n                offset++;\n            } else {\n                break;\n            }\n        }\n        return offset;\n    }\n\n    private static class Renderer {\n        public boolean isDisabled;\n        public TrackGroupArray trackGroups;\n        public MediaTrack[][] mediaTracks;\n        public SortedSet<MediaTrack> sortedTracks;\n        public MediaTrack selectedTrack;\n    }\n\n    private static class MediaTrackFormatComparator implements Comparator<MediaTrack> {\n        @Override\n        public int compare(MediaTrack mediaTrack1, MediaTrack mediaTrack2) {\n            Format format1 = mediaTrack1.format;\n            Format format2 = mediaTrack2.format;\n\n            if (format1 == null) { // assume it's auto option\n                return -1;\n            }\n\n            if (format2 == null) { // assume it's auto option\n                return 1;\n            }\n\n            // sort subtitles/audio tracks by language code\n            if (format1.language != null && format2.language != null) {\n                int result = format1.language.compareTo(format2.language);\n                if (result != 0) {\n                    return result;\n                }\n            }\n\n            int leftVal = format2.width + (int) format2.frameRate + MediaTrack.getCodecWeight(format2.codecs);\n            int rightVal = format1.width + (int) format1.frameRate + MediaTrack.getCodecWeight(format1.codecs);\n\n            int delta = leftVal - rightVal;\n            if (delta == 0) {\n                int delta2 = format2.bitrate - format1.bitrate;\n                return delta2 == 0 ? 1 : delta2; // NOTE: don't return 0 or track will be removed\n            }\n\n            return delta;\n        }\n    }\n\n    private boolean isTrackUnique(MediaTrack mediaTrack) {\n        if (!mIsMergedSource) {\n            return true;\n        }\n\n        Format format = mediaTrack.format;\n\n        // Remove hls un-complete formats altogether\n        if (format.codecs == null || format.codecs.isEmpty() || format.bitrate <= 0) {\n            return false;\n        }\n\n        String hdrTag = TrackSelectorUtil.isHdrFormat(format) ? \"hdr\" : \"\";\n\n        String formatId = format.width + format.height + format.frameRate + format.sampleMimeType + hdrTag + format.language;\n\n        Integer bitrate = mBlacklist.get(formatId);\n\n        //if (bitrate == null || (bitrate < format.bitrate || mediaTrack instanceof AudioTrack)) {\n        //  mBlacklist.put(formatId, format.bitrate + 500_000);\n        //  return true;\n        //}\n\n        if (bitrate == null || bitrate < format.bitrate || (mediaTrack instanceof AudioTrack && Math.abs(bitrate - format.bitrate) > 10_000)) {\n            int diff = mediaTrack instanceof AudioTrack ? 0 : 500_000; // video bitrate min diff\n            mBlacklist.put(formatId, format.bitrate + diff);\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     *  Trying to fix error 'AudioSink.InitializationException: AudioTrack init failed'<br/>\n     *  By removing mp4a tracks with high bitrate.\n     */\n    private boolean isErrorInAudio(MediaTrack mediaTrack) {\n        if (mediaTrack == null || mediaTrack.format == null) {\n            return false;\n        }\n\n        if (!PlayerTweaksData.instance(mContext).isUnsafeAudioFormatsEnabled()) {\n            return isUnsafeFormat(mediaTrack);\n        }\n\n        switch (Build.MODEL) {\n            case \"Smart TV Pro\": // Smart TV Pro (G03_4K_GB) - TCL\n                return isUnsafeFormat(mediaTrack);\n        }\n\n        return false;\n    }\n\n    private boolean isUnsafeFormat(MediaTrack mediaTrack) {\n        return mediaTrack.isMP4ACodec() && mediaTrack.format.bitrate >= 195_000;\n    }\n\n    private String fixLangCode(String langCode) {\n        if (langCode == null) {\n            return null;\n        }\n\n        switch (langCode) {\n            case \"in\": // Wrong Indonesian\n                return \"id\"; // Correct Indonesian\n            default:\n                return langCode;\n        }\n    }\n\n    public MediaTrack getSelectedTrack(int rendererIndex) {\n        initRenderer(rendererIndex);\n\n        Renderer renderer = mRenderers[rendererIndex];\n\n        if (renderer == null) {\n            return null;\n        }\n\n        return renderer.selectedTrack;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/selector/TrackSelectorUtil.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.selector;\r\n\r\nimport android.text.TextUtils;\r\nimport android.util.Pair;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.Player;\r\nimport com.google.android.exoplayer2.util.MimeTypes;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.SubtitleTrack;\r\n\r\nimport java.util.HashMap;\r\n\r\npublic class TrackSelectorUtil {\r\n    private static final String CODEC_PREFIX_AV1 = \"av01\";\r\n    private static final String CODEC_PREFIX_AVC = \"avc\";\r\n    private static final String CODEC_PREFIX_VP9 = \"vp9\";\r\n    private static final String CODEC_PREFIX_VP09 = \"vp09\";\r\n    private static final String CODEC_PREFIX_MP4A = \"mp4a\";\r\n    private static final String CODEC_PREFIX_VORBIS = \"vorbis\";\r\n    private static final String CODEC_PREFIX_VP9_HDR = \"vp9.2\";\r\n    private static final String CODEC_SUFFIX_AV1_HDR = \"10.0.110.09.18.09.0\";\r\n    private static final String CODEC_SUFFIX_AV1_HDR2 = \"10.0.110.09.16.09.0\";\r\n    private static final String CODEC_SHORT_AV1 = \"av1\";\r\n    private static final String HDR_PROFILE_ENDING = \"hdr\";\r\n    private static final String SEPARATOR = \", \";\r\n    private static final HashMap<Integer, Integer> mResolutionMap = new HashMap<>();\r\n    // Unicode chars: https://symbl.cc/en/search/?q=mark\r\n    public static final String HIGH_BITRATE_MARK = \"\\uD83D\\uDC8E\"; // diamond\r\n\r\n    // Try to amplify resolution of aspect ratios that differ from 16:9\r\n    static {\r\n        mResolutionMap.put(256, 144);\r\n        mResolutionMap.put(426, 240);\r\n        mResolutionMap.put(640, 360);\r\n        mResolutionMap.put(854, 480);\r\n        mResolutionMap.put(1280, 720);\r\n        mResolutionMap.put(1920, 1080);\r\n        mResolutionMap.put(2048, 1440); // Tom Zanetti - Didn't Know\r\n        mResolutionMap.put(2560, 1440);\r\n        mResolutionMap.put(3120, 2160); // Мастерская Синдиката - Мы собрали суперкар КУВАЛДОЙ!\r\n        mResolutionMap.put(3840, 2160);\r\n        mResolutionMap.put(7680, 4320);\r\n    }\r\n\r\n    /**\r\n     * Builds a track name for display.\r\n     *\r\n     * @param format {@link Format} of the track.\r\n     * @return a generated name specific to the track.\r\n     */\r\n    public static CharSequence buildTrackNameShort(Format format) {\r\n        String trackName;\r\n        if (MimeTypes.isVideo(format.sampleMimeType)) {\r\n            trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator(buildResolutionShortString(format),\r\n                    buildFPSString(format)), buildBitrateString(format)), extractCodec(format)), buildHDRString(format)), buildHighBitrateMark(format));\r\n        } else if (MimeTypes.isAudio(format.sampleMimeType)) {\r\n            trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format),\r\n                    buildAudioPropertyString(format)), buildBitrateString(format)), extractCodec(format)), buildChannels(format)), buildDrcMark(format));\r\n        } else if (MimeTypes.isText(format.sampleMimeType)) {\r\n            trackName = buildLanguageString(format);\r\n        } else {\r\n            trackName = joinWithSeparator(joinWithSeparator(buildLanguageString(format), buildBitrateString(format)), extractCodec(format));\r\n        }\r\n        return trackName.length() == 0 ? \"unknown\" : trackName;\r\n    }\r\n\r\n    /**\r\n     * Add high bitrate (Premium) mark\r\n     */\r\n    public static String buildHighBitrateMark(Format format) {\r\n        // Unicode chars: https://symbl.cc/en/search/?q=mark\r\n        return isHighBitrateFormat(format) ? HIGH_BITRATE_MARK : \"\";\r\n    }\r\n\r\n    public static boolean isHighBitrateFormat(Format format) {\r\n        return format != null && Helpers.equalsAny(format.id, \"15\");\r\n    }\r\n\r\n    public static boolean isHlsFormat(Format format) {\r\n        return format != null && format.containerMimeType == null && format.height >= 1080;\r\n    }\r\n\r\n    public static String buildHDRString(Format format) {\r\n        if (format == null) {\r\n            return \"\";\r\n        }\r\n\r\n        return isHdrFormat(format) ? \"HDR\" : \"\";\r\n    }\r\n\r\n    private static String buildFPSString(Format format) {\r\n        return format.frameRate == Format.NO_VALUE ? \"\" : Helpers.formatFloat(format.frameRate) + \"fps\";\r\n    }\r\n\r\n    /**\r\n     * Build short resolution: e.g. 720p, 1080p etc<br/>\r\n     * Try to amplify resolution of aspect ratios that differ from 16:9\r\n     */\r\n    private static String buildResolutionShortString(Format format) {\r\n        return getResolutionLabel(format);\r\n    }\r\n\r\n    private static String buildAudioPropertyString(Format format) {\r\n        return format.channelCount == Format.NO_VALUE || format.sampleRate == Format.NO_VALUE ? \"\" :\r\n                format.channelCount + \"ch, \" + format.sampleRate + \"Hz\";\r\n    }\r\n\r\n    private static String buildLanguageString(Format format) {\r\n        return TextUtils.isEmpty(format.language) || \"und\".equals(format.language) ? \"\" : SubtitleTrack.trimIfAuto(format.language);\r\n    }\r\n\r\n    private static String buildBitrateString(Format format) {\r\n        double bitrateMB = Helpers.round(format.bitrate / 1_000_000f, 2);\r\n        return format.bitrate == Format.NO_VALUE || bitrateMB == 0 ? \"\" : String.format(\"%sMbps\", Helpers.formatFloat(bitrateMB));\r\n    }\r\n\r\n    private static String joinWithSeparator(String first, String second) {\r\n        return first.length() == 0 ? second : (second.length() == 0 ? first : first + SEPARATOR + second);\r\n    }\r\n\r\n    /**\r\n     * Add html color tag\r\n     */\r\n    private static String color(String input, String color) {\r\n        return String.format(\"<font color=\\\"%s\\\">%s</font>\", color, input);\r\n    }\r\n\r\n    public static boolean isHdrFormat(Format format) {\r\n        if (format == null) {\r\n            return false;\r\n        }\r\n\r\n        return isHdrFormat(format.id, format.codecs);\r\n    }\r\n\r\n    public static boolean isHdrFormat(String id, String codecs) {\r\n        return id != null ? isHdrFormat(id) : isHdrCodec(codecs);\r\n    }\r\n\r\n    private static boolean isHdrCodec(String codec) {\r\n        if (codec == null) {\r\n            return false;\r\n        }\r\n\r\n        return codec.equals(CODEC_PREFIX_VP9_HDR) || Helpers.endsWithAny(codec, CODEC_SUFFIX_AV1_HDR, CODEC_SUFFIX_AV1_HDR2, HDR_PROFILE_ENDING);\r\n    }\r\n\r\n    private static boolean isHdrFormat(String id) {\r\n        if (id == null) {\r\n            return false;\r\n        }\r\n\r\n        // webm hdr range: 330-337\r\n        // mp4 hdr range: 694-701\r\n\r\n        int parsed = Helpers.parseInt(id);\r\n\r\n        return (parsed >= 330 && parsed <= 337) || (parsed >= 694 && parsed <=701);\r\n    }\r\n\r\n    public static String extractCodec(Format format) {\r\n        if (format.codecs == null) {\r\n            return \"\";\r\n        }\r\n\r\n        return codecNameShort(format.codecs);\r\n    }\r\n\r\n    public static String extractBitrate(Format format, int places) {\r\n        double bitrateMB = Helpers.round(format.bitrate / 1_000_000f, places);\r\n        return format.bitrate == Format.NO_VALUE || bitrateMB == 0 ? \"\" : Helpers.formatFloat(bitrateMB);\r\n    }\r\n\r\n    public static String codecNameShort(String codecNameFull) {\r\n        if (codecNameFull == null) {\r\n            return null;\r\n        }\r\n\r\n        String codec = codecNameFull.toLowerCase();\r\n\r\n        String[] codecNames = {CODEC_PREFIX_AV1, CODEC_PREFIX_AVC, CODEC_PREFIX_VP9, CODEC_PREFIX_VP09, CODEC_PREFIX_MP4A, CODEC_PREFIX_VORBIS};\r\n\r\n        for (String codecName : codecNames) {\r\n            if (codec.contains(codecName)) {\r\n                return fixShortCodecName(codecName);\r\n            }\r\n        }\r\n\r\n        return codec;\r\n    }\r\n\r\n    private static String fixShortCodecName(String shortCodecName) {\r\n        if (shortCodecName == null) {\r\n            return null;\r\n        }\r\n\r\n        switch (shortCodecName) {\r\n            case CODEC_PREFIX_AV1:\r\n                return CODEC_SHORT_AV1;\r\n            case CODEC_PREFIX_VP09:\r\n                return CODEC_PREFIX_VP9;\r\n        }\r\n\r\n        return shortCodecName;\r\n    }\r\n\r\n    public static String buildChannels(Format format) {\r\n        return is51Audio(format) ? \"5.1\" : \"\";\r\n    }\r\n\r\n    public static String buildDrcMark(Format format) {\r\n        return isDrc(format) ? \"DRC\" : \"\";\r\n    }\r\n\r\n    public static boolean isDrc(Format format) {\r\n        return format != null && (Helpers.endsWithAny(format.id, \"drc\") || format.isDrc);\r\n    }\r\n\r\n    public static boolean is51Audio(Format format) {\r\n        if (format == null) {\r\n            return false;\r\n        }\r\n\r\n        return format.bitrate > 300000;\r\n    }\r\n\r\n    public static boolean is48KAudio(Format format) {\r\n        if (format == null) {\r\n            return false;\r\n        }\r\n\r\n        return format.sampleRate >= 48000;\r\n    }\r\n\r\n    public static boolean isVideo(Format format) {\r\n        return MimeTypes.isVideo(format.sampleMimeType);\r\n    }\r\n\r\n    public static boolean isAudio(Format format) {\r\n        return MimeTypes.isAudio(format.sampleMimeType);\r\n    }\r\n\r\n    public static String stateToString(int playbackState) {\r\n        return playbackState == Player.STATE_BUFFERING ? \"STATE_BUFFERING\" :\r\n                playbackState == Player.STATE_READY ? \"STATE_READY\" :\r\n                playbackState == Player.STATE_IDLE ? \"STATE_IDLE\" :\r\n                \"STATE_ENDED\";\r\n    }\r\n\r\n    /**\r\n     * Check widescreen: 16:9, 16:8, 16:7 etc<br/>\r\n     */\r\n    public static boolean isWideScreenOld(Format format) {\r\n        if (format == null) {\r\n            return false;\r\n        }\r\n\r\n        return format.width / (float) format.height >= 1.77;\r\n    }\r\n\r\n    /**\r\n     * MOD: Mimic official behavior (handle low res shorts etc)\r\n     */\r\n    public static boolean isWideScreen(Format format) {\r\n        if (format == null) {\r\n            return false;\r\n        }\r\n\r\n        return format.width / (float) format.height > 1;\r\n    }\r\n\r\n    public static boolean is4to3Screen(Format format) {\r\n        if (format == null) {\r\n            return false;\r\n        }\r\n\r\n        float ratio = format.width / (float) format.height;\r\n        float targetRatio = 4 / (float) 3;\r\n\r\n        return Math.abs(ratio - targetRatio) < 0.2;\r\n    }\r\n\r\n    public static String getResolutionLabel(Format format) {\r\n        Pair<String, String> labels = getResolutionPrefixAndHeight(format);\r\n\r\n        if (labels == null) {\r\n            return null;\r\n        }\r\n\r\n        String prefix = labels.first != null ? \"(\" + labels.first + \") \" : \"\";\r\n\r\n        return prefix + labels.second + \"p\";\r\n    }\r\n\r\n    public static String getShortResolutionLabel(Format format) {\r\n        Pair<String, String> labels = getResolutionPrefixAndHeight(format);\r\n\r\n        if (labels == null) {\r\n            return null;\r\n        }\r\n\r\n        return labels.first != null ? labels.first : labels.second;\r\n    }\r\n\r\n    private static int getOriginHeight(int height) {\r\n        int originHeight = height;\r\n\r\n        // Non-regular examples\r\n        // Мастерская Синдиката - Мы собрали суперкар КУВАЛДОЙ! - 2560x1182\r\n        // [AMATORY] ALL STARS: LIVE IN MOSCOW 2021 - 2560x1088\r\n        // Dream Theater - Night Terror (Official Video) - 2560x1066\r\n\r\n        if (height < 160) { // 256x144\r\n            originHeight = 144;\r\n        } else if (height < 260) { // 426x240\r\n            originHeight = 240;\r\n        } else if (height < 380) { // 640x360\r\n            originHeight = 360;\r\n        } else if (height < 500) { // 854x480\r\n            originHeight = 480;\r\n        } else if (height < 750) { // 1280x720\r\n            originHeight = 720;\r\n        } else if (height < 1085) { // 1920x1080\r\n            originHeight = 1080;\r\n        } else if (height < 1500) { // 2560x1440\r\n            originHeight = 1440;\r\n        } else if (height < 2200) { // 3840x2160\r\n            originHeight = 2160;\r\n        } else if (height < 4400) { // 7680x4320\r\n            originHeight = 4320;\r\n        }\r\n\r\n        return originHeight;\r\n    }\r\n\r\n    private static int getHeightByWidth(int width) {\r\n        int originHeight = -1;\r\n\r\n        if (width < 280) { // 256x144\r\n            originHeight = 144;\r\n        } else if (width < 440) { // 426x240\r\n            originHeight = 240;\r\n        } else if (width < 650) { // 640x360\r\n            originHeight = 360;\r\n        } else if (width < 870) { // 854x480\r\n            originHeight = 480;\r\n        } else if (width < 1300) { // 1280x720\r\n            originHeight = 720;\r\n        } else if (width < 2000) { // 1920x1080\r\n            originHeight = 1080;\r\n        } else if (width < 2600) { // 2560x1440\r\n            originHeight = 1440;\r\n        } else if (width < 3900) { // 3840x2160\r\n            originHeight = 2160;\r\n        } else if (width < 7700) { // 7680x4320\r\n            originHeight = 4320;\r\n        }\r\n\r\n        return originHeight;\r\n    }\r\n\r\n    /**\r\n     * Get the height in terms like it's understandable by the codec.\r\n     */\r\n    public static int getRealHeight(Format format) {\r\n        if (format == null) {\r\n            return -1;\r\n        }\r\n\r\n        int height = format.height;\r\n        int width = format.width;\r\n\r\n        if (width == Format.NO_VALUE || height == Format.NO_VALUE) {\r\n            return -1;\r\n        }\r\n\r\n        // Make resolution calculation of the vertical videos more closer to the official app.\r\n        //boolean isUltraWide = (float) width/height >= 2.1; // maybe 2.3???\r\n        //int originHeight = isUltraWide ? getHeightByWidth(width) : getOriginHeight(Math.min(height, width));\r\n        boolean isUltraWide = (float) width/height >= 2; // maybe 2.1\r\n        int originHeight = isUltraWide ? getHeightByWidth(width) : getOriginHeight(height);\r\n\r\n        return originHeight;\r\n    }\r\n\r\n    private static String getResolutionPrefix(int originHeight) {\r\n        String prefix = null;\r\n\r\n        if (originHeight == 1440) {\r\n            prefix = \"2K\";\r\n        } else if (originHeight == 2160) {\r\n            prefix = \"4K\";\r\n        } else if (originHeight == 4320) {\r\n            prefix = \"8K\";\r\n        }\r\n\r\n        return prefix;\r\n    }\r\n\r\n    private static Pair<String, String> getResolutionPrefixAndHeight(Format format) {\r\n        int originHeight = getRealHeight(format);\r\n\r\n        if (originHeight == -1) {\r\n            return null;\r\n        }\r\n\r\n        String prefix = getResolutionPrefix(originHeight);\r\n\r\n        return new Pair<>(prefix, String.valueOf(originHeight));\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/selector/track/AudioTrack.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track;\r\n\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.TrackSelectorUtil;\r\n\r\npublic class AudioTrack extends MediaTrack {\r\n    public AudioTrack(int rendererIndex) {\r\n        super(rendererIndex);\r\n    }\r\n\r\n    //@Override\r\n    //public int inBounds(MediaTrack track2) {\r\n    //    int result = compare(track2);\r\n    //\r\n    //    // Select at least something.\r\n    //    if (result == -1 && track2 != null && track2.format != null) {\r\n    //        result = 1;\r\n    //    }\r\n    //\r\n    //    return result;\r\n    //}\r\n\r\n    @Override\r\n    public int inBounds(MediaTrack track2) {\r\n        if (format == null) {\r\n            return -1;\r\n        }\r\n\r\n        if (track2 == null || track2.format == null) {\r\n            return 1;\r\n        }\r\n\r\n        int result = -1;\r\n\r\n        String id1 = format.id;\r\n        String id2 = track2.format.id;\r\n        int bitrate1 = format.bitrate;\r\n        int bitrate2 = track2.format.bitrate;\r\n\r\n        // Compare by language isn't robust since language set may not contain target language\r\n        String language1 = format.language;\r\n        String language2 = track2.format.language;\r\n        boolean sameLanguage = sameLanguage(language1, language2);\r\n\r\n        if (Helpers.equals(id1, id2)) {\r\n            result = 0;\r\n        } else if (bitrate1 != -1 && bitrateLessOrEquals(bitrate2, bitrate1)) {\r\n            result = 1;\r\n        } else if (bitrate1 == -1 && (TrackSelectorUtil.is51Audio(format) || !TrackSelectorUtil.is51Audio(track2.format))) {\r\n            result = 1;\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    @Override\r\n    public int compare(MediaTrack track2) {\r\n        if (format == null) {\r\n            return -1;\r\n        }\r\n\r\n        if (track2 == null || track2.format == null) {\r\n            return 1;\r\n        }\r\n\r\n        int result = -1;\r\n\r\n        if (format.id == null && format.language == null && format.bitrate == -1 && codecEquals(this, track2)) {\r\n            result = 0;\r\n        } else if (Helpers.equals(format.id, track2.format.id) && drcEquals(format, track2.format)) {\r\n            result = 1;\r\n        } else if (!codecEquals(this, track2) || !drcEquals(format, track2.format) || bitrateLessOrEquals(track2.format.bitrate, format.bitrate)) {\r\n            result = 0;\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    private boolean sameLanguage(String language1, String language2) {\r\n        return Helpers.equals(language1, language2) || (language1 == null || language2 == null);\r\n    }\r\n\r\n    private static boolean drcEquals(Format format1, Format format2) {\r\n        if (format1 == null || format2 == null) {\r\n            return false;\r\n        }\r\n\r\n        return TrackSelectorUtil.isDrc(format1) == TrackSelectorUtil.isDrc(format2);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/selector/track/MediaTrack.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track;\r\n\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.source.TrackGroupArray;\r\nimport com.google.android.exoplayer2.trackselection.TrackSelection.Definition;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.TrackSelectorManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.TrackSelectorUtil;\r\n\r\npublic abstract class MediaTrack {\r\n    private static final int BITRATE_DIFF_PERCENTS = 7;\r\n    private static final int VP9_WEIGHT = 31;\r\n    private static final int AVC_WEIGHT = 28;\r\n    private static final int AV1_WEIGHT = 14;\r\n    private static int sVP9Weight = VP9_WEIGHT;\r\n    private static int sAVCWeight = AVC_WEIGHT;\r\n    private static int sAV1Weight = AV1_WEIGHT;\r\n    public Format format;\r\n    public int groupIndex = -1;\r\n    public int trackIndex = -1;\r\n    public boolean isSelected;\r\n    public boolean isSaved;\r\n    public boolean isPreset;\r\n    public int rendererIndex;\r\n\r\n    public MediaTrack(int rendererIndex) {\r\n        this.rendererIndex = rendererIndex;\r\n    }\r\n\r\n    public static MediaTrack from(int rendererIndex, TrackGroupArray groups, Definition definition) {\r\n        MediaTrack mediaTrack = forRendererIndex(rendererIndex);\r\n\r\n        if (mediaTrack == null || groups == null || definition == null || definition.tracks == null) {\r\n            return null;\r\n        }\r\n\r\n        mediaTrack.groupIndex = groups.indexOf(definition.group);\r\n        mediaTrack.trackIndex = definition.tracks[0];\r\n\r\n        return mediaTrack;\r\n    }\r\n\r\n    public abstract int compare(MediaTrack track2);\r\n    public abstract int inBounds(MediaTrack track2);\r\n\r\n    public static MediaTrack forRendererIndex(int rendererIndex) {\r\n        switch (rendererIndex) {\r\n            case TrackSelectorManager.RENDERER_INDEX_VIDEO:\r\n                return new VideoTrack(rendererIndex);\r\n            case TrackSelectorManager.RENDERER_INDEX_AUDIO:\r\n                return new AudioTrack(rendererIndex);\r\n            case TrackSelectorManager.RENDERER_INDEX_SUBTITLE:\r\n                return new SubtitleTrack(rendererIndex);\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    protected static boolean bitrateLessOrEquals(int bitrate1, int bitrate2) {\r\n        if (bitrate1 == -1 || bitrate2 == -1) {\r\n            return true;\r\n        }\r\n\r\n        return bitrate1 <= bitrate2 || bitrateAlmostEquals(bitrate1, bitrate2);\r\n    }\r\n\r\n    private static boolean codecEquals(String codecs1, String codecs2) {\r\n        if (codecs1 == null || codecs2 == null) {\r\n            return false;\r\n        }\r\n\r\n        return Helpers.equals(TrackSelectorUtil.codecNameShort(codecs1), TrackSelectorUtil.codecNameShort(codecs2));\r\n    }\r\n\r\n    private static boolean codecEquals(Format format1, Format format2) {\r\n        if (format1 == null || format2 == null) {\r\n            return false;\r\n        }\r\n\r\n        return codecEquals(format1.codecs, format2.codecs);\r\n    }\r\n\r\n    private static boolean bitrateEquals(Format format1, Format format2) {\r\n        if (format1 == null || format2 == null) {\r\n            return false;\r\n        }\r\n\r\n        return format1.bitrate == format2.bitrate;\r\n    }\r\n\r\n    private static boolean bitrateAlmostEquals(int bitrate1, int bitrate2) {\r\n        return bitrate1 == bitrate2 || Math.abs(bitrate2 - bitrate1) < (bitrate2 / 100 * BITRATE_DIFF_PERCENTS);\r\n    }\r\n\r\n    private static boolean preferByBitrate(Format format1, Format format2) {\r\n        if (format1 == null) {\r\n            return false;\r\n        }\r\n\r\n        if (format2 == null) {\r\n            return true;\r\n        }\r\n\r\n        if (!codecEquals(format1, format2)) {\r\n            return true;\r\n        }\r\n\r\n        return format1.bitrate > format2.bitrate;\r\n    }\r\n\r\n    public static boolean codecEquals(MediaTrack track1, MediaTrack track2) {\r\n        if (track1 == null || track2 == null) {\r\n            return false;\r\n        }\r\n\r\n        return codecEquals(track1.format, track2.format);\r\n    }\r\n\r\n    public static boolean bitrateEquals(MediaTrack track1, MediaTrack track2) {\r\n        if (track1 == null || track2 == null) {\r\n            return false;\r\n        }\r\n\r\n        return bitrateEquals(track1.format, track2.format);\r\n    }\r\n\r\n    public static boolean preferByBitrate(MediaTrack track1, MediaTrack track2) {\r\n        if (track1 == null || track2 == null) {\r\n            return false;\r\n        }\r\n\r\n        return preferByBitrate(track1.format, track2.format);\r\n    }\r\n\r\n    public static int getCodecWeight(MediaTrack track) {\r\n        if (track == null || track.format == null) {\r\n            return 0;\r\n        }\r\n\r\n        return getCodecWeight(track.format.codecs);\r\n    }\r\n\r\n    public static int getCodecWeight(String codec) {\r\n        return isVP9Codec(codec) ? sVP9Weight : isAVCCodec(codec) ? sAVCWeight : isAV1Codec(codec) ? sAV1Weight : 0;\r\n    }\r\n\r\n    public static boolean preferByCodec(MediaTrack prevTrack, MediaTrack nextTrack) {\r\n        return getCodecWeight(prevTrack) - getCodecWeight(nextTrack) > 0;\r\n    }\r\n\r\n    public static void preferAvcOverVp9(boolean prefer) {\r\n        sAVCWeight = prefer ? VP9_WEIGHT : AVC_WEIGHT;\r\n        sVP9Weight = prefer ? AVC_WEIGHT : VP9_WEIGHT;\r\n    }\r\n\r\n    public static boolean preferByDrc(MediaTrack origin, MediaTrack prevTrack, MediaTrack nextTrack) {\r\n        if (!(origin instanceof AudioTrack)) {\r\n            return true;\r\n        }\r\n\r\n        boolean isDrcOrigin = TrackSelectorUtil.isDrc(origin.format);\r\n        boolean isDrcPrev = TrackSelectorUtil.isDrc(prevTrack.format);\r\n        boolean isDrcNext = TrackSelectorUtil.isDrc(nextTrack.format);\r\n        boolean preferByDrc = (isDrcOrigin == isDrcNext) || (isDrcPrev == isDrcNext);\r\n        return preferByDrc;\r\n    }\r\n\r\n    public boolean isVP9Codec() {\r\n        return format != null && isVP9Codec(format.codecs);\r\n    }\r\n\r\n    public boolean isAV1Codec() {\r\n        return format != null && isAV1Codec(format.codecs);\r\n    }\r\n\r\n    public boolean isMP4ACodec() {\r\n        return format != null && isMP4ACodec(format.codecs);\r\n    }\r\n\r\n    public boolean isEmpty() {\r\n        return groupIndex == -1 && trackIndex == -1;\r\n    }\r\n\r\n    public int getWidth() {\r\n        return format != null ? format.width : -1;\r\n    }\r\n\r\n    public int getHeight() {\r\n        return format != null ? format.height : -1;\r\n    }\r\n\r\n    private static boolean isVP9Codec(String codec) {\r\n        if (codec == null) {\r\n            return false;\r\n        }\r\n\r\n        codec = codec.toLowerCase();\r\n\r\n        return Helpers.containsAny(codec, \"vp9\", \"vp09\");\r\n    }\r\n\r\n    private static boolean isAVCCodec(String codec) {\r\n        if (codec == null) {\r\n            return false;\r\n        }\r\n\r\n        codec = codec.toLowerCase();\r\n\r\n        return codec.contains(\"avc\");\r\n    }\r\n\r\n    private static boolean isAV1Codec(String codec) {\r\n        if (codec == null) {\r\n            return false;\r\n        }\r\n\r\n        codec = codec.toLowerCase();\r\n\r\n        return codec.contains(\"av01\");\r\n    }\r\n\r\n    private static boolean isMP4ACodec(String codec) {\r\n        if (codec == null) {\r\n            return false;\r\n        }\r\n\r\n        codec = codec.toLowerCase();\r\n\r\n        return codec.contains(\"mp4a\");\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/selector/track/SubtitleTrack.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.youtubeapi.videoinfo.models.TranslatedCaptionTrack;\r\n\r\nimport java.util.regex.Pattern;\r\n\r\npublic class SubtitleTrack extends MediaTrack {\r\n    private static final Pattern AUTO_PATTERN = Pattern.compile(\" \\\\(.*\\\\)$\"); // May have mismatches e.g. 'English (United Kingdom)'\r\n    private static final Pattern TRIM_PATTERN1 = Pattern.compile(\" \\\\(.*\\\\) - .*\");\r\n    private static final Pattern TRIM_PATTERN2 = Pattern.compile(\" - .*\");\r\n    private static final Pattern MARKER_PATTERN = Pattern.compile(\".$\");\r\n\r\n    public SubtitleTrack(int rendererIndex) {\r\n        super(rendererIndex);\r\n    }\r\n\r\n    @Override\r\n    public int inBounds(MediaTrack track2) {\r\n        if (format == null) {\r\n            return -1;\r\n        }\r\n\r\n        if (track2 == null || track2.format == null) {\r\n            return 1;\r\n        }\r\n\r\n        int result = -1;\r\n\r\n        if (Helpers.startsWith(track2.format.language, trim(format.language))) { // partial match\r\n            if (!isAuto(track2.format.language)) {\r\n                // Prefer original subs\r\n                result = 0;\r\n            } else {\r\n                result = 1;\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    @Override\r\n    public int compare(MediaTrack track2) {\r\n        if (format == null) {\r\n            return -1;\r\n        }\r\n\r\n        if (track2 == null || track2.format == null) {\r\n            return 1;\r\n        }\r\n\r\n        int result = -1;\r\n\r\n        if (Helpers.startsWith(track2.format.language, trim(format.language))) { // partial match\r\n            if (!isAuto(format.language)) {\r\n                // Prefer original subs\r\n                result = 1;\r\n            } else if (isAuto(format.language) && isAuto(track2.format.language)) {\r\n                result = 0;\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    /**\r\n     * Autogenerated subs by the user <br/>\r\n     * NOTE: Has an exceptions, like English (United Kingdom)\r\n     */\r\n    private static boolean isAutoUser(String language) {\r\n        if (language == null) {\r\n            return false;\r\n        }\r\n\r\n        return Helpers.matchAll(language, AUTO_PATTERN);\r\n    }\r\n\r\n    /**\r\n     * Remove autogenerated and other stuff\r\n     */\r\n    public static String trim(String language) {\r\n        if (language == null) {\r\n            return null;\r\n        }\r\n\r\n        return trimMarker(Helpers.replace(Helpers.replace(language, TRIM_PATTERN1, \"\"), TRIM_PATTERN2, \"\")); // english - us bla -> english\r\n    }\r\n\r\n    public static String trimIfAuto(String language) {\r\n        return isAuto(language) ? trim(language) : language;\r\n    }\r\n\r\n    /**\r\n     * NOTE: Breaks Portuguese (Portugal) but fixes other similar languages\r\n     */\r\n    private static String trimAuto(String language) {\r\n        if (language == null) {\r\n            return null;\r\n        }\r\n\r\n        return Helpers.replace(trimMarker(language), AUTO_PATTERN, \"\"); // english (us) bla -> english\r\n    }\r\n\r\n    /**\r\n     * Removes auto translate marker\r\n     */\r\n    private static String trimMarker(String language) {\r\n        if (language != null && language.endsWith(TranslatedCaptionTrack.TRANSLATE_MARKER)) {\r\n            return Helpers.replace(language, MARKER_PATTERN, \"\");\r\n        } else {\r\n            return language;\r\n        }\r\n    }\r\n\r\n    public static boolean isAuto(String language) {\r\n        return hasMarker(language);\r\n    }\r\n\r\n    private static boolean hasMarker(String language) {\r\n        return language != null && language.endsWith(TranslatedCaptionTrack.TRANSLATE_MARKER);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/selector/track/VideoTrack.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track;\r\n\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.TrackSelectorUtil;\r\n\r\npublic class VideoTrack extends MediaTrack {\r\n    private static final float LOW_FPS_THRESHOLD = 10;\r\n    private static final int SIZE_EQUITY_THRESHOLD_PERCENT = 5; // was 15 before\r\n    private static final int COMPARE_TYPE_IN_BOUNDS = 0;\r\n    private static final int COMPARE_TYPE_IN_BOUNDS_NO_FPS = 4;\r\n    private static final int COMPARE_TYPE_IN_BOUNDS_PRESET = 1;\r\n    private static final int COMPARE_TYPE_IN_BOUNDS_PRESET_NO_FPS = 3;\r\n    private static final int COMPARE_TYPE_NORMAL = 2;\r\n    public static boolean sIsNoFpsPresetsEnabled;\r\n\r\n    public VideoTrack(int rendererIndex) {\r\n        super(rendererIndex);\r\n    }\r\n\r\n    public static boolean sizeEquals(int size1, int size2) {\r\n        return sizeEquals(size1, size2, SIZE_EQUITY_THRESHOLD_PERCENT);\r\n    }\r\n\r\n    public static boolean sizeEquals(int size1, int size2, int diffPercents) {\r\n        if (size1 == -1 || size2 == -1) {\r\n            return false;\r\n        }\r\n\r\n        int threshold = size1 / 100 * diffPercents;\r\n        boolean diffWithinThreshold = Math.abs(size1 - size2) < threshold;\r\n\r\n        return diffWithinThreshold;\r\n    }\r\n\r\n    private static boolean sizeLessOrEquals(int size1, int size2) {\r\n        if (size1 == -1 || size2 == -1) {\r\n            return false;\r\n        }\r\n\r\n        return size1 <= size2 || sizeEquals(size1, size2);\r\n    }\r\n\r\n    private static boolean sizeLess(int size1, int size2) {\r\n        if (size1 == -1 || size2 == -1) {\r\n            return false;\r\n        }\r\n\r\n        return !sizeEquals(size1, size2) && size1 < size2;\r\n    }\r\n\r\n    private static boolean fpsEquals(float fps1, float fps2) {\r\n        if (fps1 == -1 || fps2 == -1) {\r\n            return true; // probably LIVE translation\r\n        }\r\n\r\n        int threshold = 10;\r\n        boolean diffWithinThreshold = Math.abs(fps1 - fps2) < threshold;\r\n\r\n        return diffWithinThreshold;\r\n    }\r\n\r\n    private static boolean fpsLessOrEquals(float fps1, float fps2) {\r\n        if (fps1 == -1 || fps2 == -1) {\r\n            return true; // probably LIVE translation\r\n        }\r\n\r\n        return fps1 <= fps2 || fpsEquals(fps1, fps2);\r\n    }\r\n\r\n    private static boolean fpsLess(float fps1, float fps2) {\r\n        // NOTE: commented out after no fps fix option\r\n        //if (fps1 == -1 || fps2 == -1) {\r\n        //    return true; // probably LIVE translation\r\n        //}\r\n\r\n        if (fps1 == -1 && fps2 == -1) {\r\n            return false;\r\n        }\r\n        \r\n        return !fpsEquals(fps1, fps2) && fps1 < fps2;\r\n    }\r\n\r\n    private boolean isLive(MediaTrack track) {\r\n        return track.format.frameRate == -1;\r\n    }\r\n\r\n    @Override\r\n    public int inBounds(MediaTrack track2) {\r\n        if (format == null) {\r\n            return -1;\r\n        }\r\n\r\n        // NOTE: MultiFpsFormat: 25/50, 30/60. Currently no more that 720p.\r\n        boolean isMultiFpsFormat = sizeLessOrEquals(format.height, 720);\r\n\r\n        // Detect preset by id presence\r\n        boolean isPreset = format.id == null;\r\n\r\n        if (isPreset) {\r\n            // Overcome non-standard aspect ratio by getting resolution label\r\n            //boolean respectPresetsFps = !sIsNoFpsPresetsEnabled ||\r\n            //        sizeEquals(format.height, TrackSelectorUtil.getOriginHeight(track2.format.height));\r\n            boolean respectPresetsFps = !sIsNoFpsPresetsEnabled ||\r\n                    sizeEquals(TrackSelectorUtil.getRealHeight(format), TrackSelectorUtil.getRealHeight(track2.format));\r\n            return compare(track2, isMultiFpsFormat || respectPresetsFps ? COMPARE_TYPE_IN_BOUNDS_PRESET : COMPARE_TYPE_IN_BOUNDS_PRESET_NO_FPS);\r\n        } else {\r\n            return compare(track2, isMultiFpsFormat ? COMPARE_TYPE_IN_BOUNDS : COMPARE_TYPE_IN_BOUNDS_NO_FPS);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public int compare(MediaTrack track2) {\r\n        return compare(track2, COMPARE_TYPE_NORMAL);\r\n    }\r\n\r\n    private int compare(MediaTrack track2, int type) {\r\n        if (format == null) {\r\n            return -1;\r\n        }\r\n\r\n        if (track2 == null || track2.format == null) {\r\n            return 1;\r\n        }\r\n\r\n        int size1;\r\n        int size2;\r\n\r\n        // MOD: Mimic official behavior (handle low res shorts etc)\r\n        //size1 = TrackSelectorUtil.isWideScreen(format) || exceedHeightLimit(format) ? format.height : format.width;\r\n        //size2 = TrackSelectorUtil.isWideScreen(track2.format) || exceedHeightLimit(track2.format) ? track2.format.height : track2.format.width;\r\n        size1 = TrackSelectorUtil.getRealHeight(format);\r\n        size2 = TrackSelectorUtil.getRealHeight(track2.format);\r\n\r\n        String id1 = format.id;\r\n        String id2 = track2.format.id;\r\n        // Low fps (e.g. 8fps) on original track could break whole comparison\r\n        float frameRate1 = format.frameRate < LOW_FPS_THRESHOLD ? 30 : format.frameRate;\r\n        float frameRate2 = track2.format.frameRate < LOW_FPS_THRESHOLD ? 30 : track2.format.frameRate;\r\n        String codecs1 = format.codecs;\r\n        String codecs2 = track2.format.codecs;\r\n        int bitrate1 = format.bitrate;\r\n        int bitrate2 = track2.format.bitrate;\r\n\r\n        int result;\r\n\r\n        if (type == COMPARE_TYPE_IN_BOUNDS) {\r\n            result = inBounds(id1, id2, size1, size2, frameRate1, frameRate2, codecs1, codecs2, bitrate1, bitrate2);\r\n        } else if (type == COMPARE_TYPE_IN_BOUNDS_NO_FPS) {\r\n            result = inBounds(id1, id2, size1, size2, -1, -1, codecs1, codecs2, bitrate1, bitrate2);\r\n        } else if (type == COMPARE_TYPE_IN_BOUNDS_PRESET) {\r\n            result = inBoundsPreset(id1, id2, size1, size2, frameRate1, frameRate2, codecs1, codecs2);\r\n        } else if (type == COMPARE_TYPE_IN_BOUNDS_PRESET_NO_FPS) {\r\n            result = inBoundsPreset(id1, id2, size1, size2, -1, -1, codecs1, codecs2);\r\n        } else {\r\n            result = compare(id1, id2, size1, size2, frameRate1, frameRate2, codecs1, codecs2, bitrate1, bitrate2);\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    private int inBounds(String id1, String id2, int size1, int size2, float frameRate1, float frameRate2, String codecs1, String codecs2, int bitrate1, int bitrate2) {\r\n        int result = -1;\r\n\r\n        // Fix same id between normal videos and shorts\r\n        if (Helpers.equals(id1, id2) && (size1 == size2)) {\r\n            result = 0;\r\n        //} else if (sizeLessOrEquals(size2, size1) && fpsLessOrEquals(frameRate2, frameRate1) && bitrateLessOrEquals(bitrate2, bitrate1)) {\r\n        } else if (sizeLessOrEquals(size2, size1) && fpsLessOrEquals(frameRate2, frameRate1)) { // NOTE: Removed bitrate check to fix shorts?\r\n            if (TrackSelectorUtil.isHdrFormat(id1, codecs1) == TrackSelectorUtil.isHdrFormat(id2, codecs2)) {\r\n                result = 1;\r\n            } else if (TrackSelectorUtil.isHdrFormat(id1, codecs1)) {\r\n                result = 1;\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    private int inBoundsPreset(String id1, String id2, int size1, int size2, float frameRate1, float frameRate2, String codecs1, String codecs2) {\r\n        // Fix same id between normal videos and shorts\r\n        if (Helpers.equals(id1, id2) && (size1 == size2)) {\r\n            return 0;\r\n        }\r\n\r\n        if (!TrackSelectorUtil.isHdrFormat(id1, codecs1) && TrackSelectorUtil.isHdrFormat(id2, codecs2)) {\r\n            return -1;\r\n        }\r\n\r\n        if (fpsLess(frameRate1, frameRate2)) {\r\n            return -1;\r\n        }\r\n\r\n        if (sizeLess(size1, size2)) {\r\n            return -1;\r\n        }\r\n\r\n        return 1;\r\n    }\r\n\r\n    //private int inBoundsPreset(String id1, String id2, int size1, int size2, float frameRate1, float frameRate2, String codecs1, String codecs2) {\r\n    //    int result = -1;\r\n    //\r\n    //    if (Helpers.equals(id1, id2)) {\r\n    //        result = 0;\r\n    //    } else if (sizeEquals(size1, size2)) {\r\n    //        if (fpsEquals(frameRate2, frameRate1)) {\r\n    //            if (TrackSelectorUtil.isHdrCodec(codecs1) == TrackSelectorUtil.isHdrCodec(codecs2)) {\r\n    //                result = 0;\r\n    //            } else if (TrackSelectorUtil.isHdrCodec(codecs1)) {\r\n    //                result = 1;\r\n    //            }\r\n    //        } else if (fpsLessOrEquals(frameRate2, frameRate1)) {\r\n    //            if (TrackSelectorUtil.isHdrCodec(codecs1) == TrackSelectorUtil.isHdrCodec(codecs2)) {\r\n    //                result = 1;\r\n    //            } else if (TrackSelectorUtil.isHdrCodec(codecs1)) {\r\n    //                result = 1;\r\n    //            }\r\n    //        }\r\n    //    } else if (sizeLessOrEquals(size2, size1) && fpsLessOrEquals(frameRate2, frameRate1)) {\r\n    //        if (TrackSelectorUtil.isHdrCodec(codecs1) == TrackSelectorUtil.isHdrCodec(codecs2)) {\r\n    //            result = 1;\r\n    //        } else if (TrackSelectorUtil.isHdrCodec(codecs1)) {\r\n    //            result = 1;\r\n    //        }\r\n    //    }\r\n    //\r\n    //    return result;\r\n    //}\r\n\r\n    private int compare(String id1, String id2, int size1, int size2, float frameRate1, float frameRate2, String codecs1, String codecs2, int bitrate1, int bitrate2) {\r\n        if (Helpers.equals(id1, id2)) {\r\n            return 0;\r\n        }\r\n\r\n        int leftScore = 0;\r\n        int rightScore = 0;\r\n\r\n        if (TrackSelectorUtil.isHdrFormat(id1, codecs1) && !TrackSelectorUtil.isHdrFormat(id2, codecs2)) {\r\n            leftScore += 3;\r\n        } else if (TrackSelectorUtil.isHdrFormat(id2, codecs2) && !TrackSelectorUtil.isHdrFormat(id1, codecs1)) {\r\n            rightScore += 3;\r\n        }\r\n\r\n        if (fpsLess(frameRate1, frameRate2)) {\r\n            rightScore += 2;\r\n        } else if (fpsLess(frameRate2, frameRate1)) {\r\n            leftScore += 2;\r\n        }\r\n\r\n        if (sizeLess(size1, size2)) {\r\n            rightScore += 1;\r\n        } else if (sizeLess(size2, size1)) {\r\n            leftScore += 1;\r\n        }\r\n\r\n        int result = leftScore - rightScore;\r\n        return result == 0 && TrackSelectorUtil.codecNameShort(codecs1).equals(TrackSelectorUtil.codecNameShort(codecs2)) ? bitrate1 - bitrate2 : result;\r\n    }\r\n\r\n    //private int compare(String id1, String id2, int size1, int size2, float frameRate1, float frameRate2, String codecs1, String codecs2) {\r\n    //    int result = -1;\r\n    //\r\n    //    if (Helpers.equals(id1, id2)) {\r\n    //        result = 0;\r\n    //    } else if (sizeLessOrEquals(size2, size1)) {\r\n    //        if (fpsLessOrEquals(frameRate2, frameRate1)) {\r\n    //            if (TrackSelectorUtil.isHdrCodec(codecs1) == TrackSelectorUtil.isHdrCodec(codecs2)) {\r\n    //                result = 0;\r\n    //            } else if (TrackSelectorUtil.isHdrCodec(codecs2)) {\r\n    //                result = -1;\r\n    //            } else {\r\n    //                result = 1;\r\n    //            }\r\n    //        }\r\n    //    }\r\n    //\r\n    //    return result;\r\n    //}\r\n\r\n    // Shorts fix\r\n    private boolean exceedHeightLimit(Format format) {\r\n        return format.height > 1080;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/versions/ExoUtils.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.versions;\r\n\r\nimport com.google.android.exoplayer2.ExoPlayer;\r\nimport com.google.android.exoplayer2.mediacodec.MediaCodecInfo;\r\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil;\r\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;\r\n\r\npublic class ExoUtils {\r\n    private static String sVideoDecoderName;\r\n\r\n    public static boolean isPlaying(ExoPlayer player) {\r\n        if (player == null) {\r\n            return false;\r\n        }\r\n\r\n        // Exo 2.9\r\n        //return player.getPlayWhenReady() && player.getPlaybackState() == Player.STATE_READY;\r\n\r\n        // Exo 2.10 and up\r\n        return player.isPlaying();\r\n    }\r\n\r\n    public static boolean isLoading(ExoPlayer player) {\r\n        if (player == null) {\r\n            return false;\r\n        }\r\n\r\n        return player.isLoading();\r\n    }\r\n\r\n    public static MediaCodecInfo getCapsDecoderInfo(String mimeType) {\r\n        MediaCodecInfo info = null;\r\n\r\n        try {\r\n            // Exo 2.9\r\n            //info = MediaCodecUtil.getDecoderInfo(mimeType, false);\r\n\r\n            // Exo 2.10 and up\r\n            info = MediaCodecUtil.getDecoderInfo(mimeType, false, false);\r\n        } catch (DecoderQueryException e) {\r\n            e.printStackTrace();\r\n        }\r\n\r\n        return info;\r\n    }\r\n\r\n    public static void updateVideoDecoderInfo(MediaCodecInfo codecInfo) {\r\n        if (codecInfo == null) {\r\n            return;\r\n        }\r\n\r\n        sVideoDecoderName = codecInfo.name;\r\n    }\r\n\r\n    public static String getVideoDecoderName() {\r\n        return sVideoDecoderName;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/versions/renderer/CustomOverridesRenderersFactory.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.renderer;\r\n\r\nimport android.content.Context;\r\nimport android.os.Handler;\r\nimport androidx.annotation.Nullable;\r\nimport com.google.android.exoplayer2.Renderer;\r\nimport com.google.android.exoplayer2.audio.AudioCapabilities;\r\nimport com.google.android.exoplayer2.audio.AudioProcessor;\r\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener;\r\nimport com.google.android.exoplayer2.audio.DefaultAudioSink;\r\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\r\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\r\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\r\nimport com.google.android.exoplayer2.util.AmazonQuirks;\r\nimport com.google.android.exoplayer2.video.VideoRendererEventListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.selector.BlacklistMediaCodecSelector;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\r\n\r\nimport java.util.ArrayList;\r\n\r\n/**\r\n * Main intent: override audio delay\r\n */\r\npublic class CustomOverridesRenderersFactory extends CustomRenderersFactoryBase {\r\n    private static final String TAG = CustomOverridesRenderersFactory.class.getSimpleName();\r\n    private static final String[] FRAME_DROP_FIX_LIST = {\r\n            \"T95ZPLUS (q201_3GB)\",\r\n            \"UGOOS (UGOOS)\",\r\n            \"55UC30G (ctl_iptv_mrvl)\" // Kivi 55uc30g\r\n    };\r\n    private final PlayerData mPlayerData;\r\n    private final PlayerTweaksData mPlayerTweaksData;\r\n    // 2.12, 2.13\r\n    //private int mOperationMode = MediaCodecRenderer.OPERATION_MODE_SYNCHRONOUS;\r\n\r\n    // 2.9, 2.10, 2.11\r\n    public CustomOverridesRenderersFactory(Context activity) {\r\n        super(activity);\r\n\r\n        mPlayerData = PlayerData.instance(activity);\r\n        mPlayerTweaksData = PlayerTweaksData.instance(activity);\r\n\r\n        setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON);\r\n        // setEnableDecoderFallback(true); // Exo 2.10 and up\r\n\r\n        if (mPlayerTweaksData.isSWDecoderForced()) {\r\n            setMediaCodecSelector(new BlacklistMediaCodecSelector());\r\n        }\r\n\r\n        AmazonQuirks.disableSnappingToVsync(mPlayerTweaksData.isSnappingToVsyncDisabled());\r\n        AmazonQuirks.skipProfileLevelCheck(mPlayerTweaksData.isProfileLevelCheckSkipped());\r\n    }\r\n\r\n    // 2.12, 2.13\r\n    //public CustomOverridesRenderersFactory(Context activity) {\r\n    //    super(activity);\r\n    //\r\n    //    mPlayerData = PlayerData.instance(activity);\r\n    //    mPlayerTweaksData = PlayerTweaksData.instance(activity);\r\n    //\r\n    //    // Exo 2.12 (Exclusive experimental tweaks)\r\n    //    //mOperationMode = MediaCodecRenderer.OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_ASYNCHRONOUS_QUEUEING;\r\n    //    //experimentalSetMediaCodecOperationMode(mOperationMode);\r\n    //\r\n    //    setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON);\r\n    //    // setEnableDecoderFallback(true); // Exo 2.10 and up\r\n    //\r\n    //    if (mPlayerTweaksData.isSWDecoderForced()) {\r\n    //        setMediaCodecSelector(new BlacklistMediaCodecSelector());\r\n    //    }\r\n    //\r\n    //    AmazonQuirks.skipProfileLevelCheck(mPlayerTweaksData.isProfileLevelCheckSkipped());\r\n    //}\r\n\r\n    // Exo 2.9\r\n    //@Override\r\n    //protected void buildAudioRenderers(Context context, int extensionRendererMode, MediaCodecSelector mediaCodecSelector,\r\n    //                                   @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,\r\n    //                                   AudioProcessor[] audioProcessors, Handler eventHandler, AudioRendererEventListener eventListener,\r\n    //                                   ArrayList<Renderer> out) {\r\n    //    super.buildAudioRenderers(context, extensionRendererMode, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys,\r\n    //            audioProcessors, eventHandler, eventListener, out);\r\n    //\r\n    //    CustomMediaCodecAudioRenderer audioRenderer = null;\r\n    //\r\n    //    if (mPlayerData.getAudioDelayMs() != 0) {\r\n    //        audioRenderer =\r\n    //                new CustomMediaCodecAudioRenderer(context, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,\r\n    //                        eventListener, new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors));\r\n    //\r\n    //        audioRenderer.setAudioDelayMs(mPlayerData.getAudioDelayMs());\r\n    //    }\r\n    //\r\n    //    replaceAudioRenderer(out, audioRenderer);\r\n    //}\r\n\r\n    // Exo 2.9\r\n    //@Override\r\n    //protected void buildVideoRenderers(Context context, int extensionRendererMode, MediaCodecSelector mediaCodecSelector,\r\n    //                                   @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,\r\n    //                                   Handler eventHandler, VideoRendererEventListener eventListener, long allowedVideoJoiningTimeMs,\r\n    //                                   ArrayList<Renderer> out) {\r\n    //    super.buildVideoRenderers(context, extensionRendererMode, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,\r\n    //            eventListener, allowedVideoJoiningTimeMs, out);\r\n    //\r\n    //    CustomMediaCodecVideoRenderer videoRenderer = null;\r\n    //\r\n    //    if (mPlayerTweaksData.isFrameDropFixEnabled() || mPlayerTweaksData.isAmlogicFixEnabled()) {\r\n    //        videoRenderer = new CustomMediaCodecVideoRenderer(context, mediaCodecSelector, allowedVideoJoiningTimeMs, drmSessionManager,\r\n    //                playClearSamplesWithoutKeys, eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);\r\n    //\r\n    //        videoRenderer.enableFrameDropFix(mPlayerTweaksData.isFrameDropFixEnabled());\r\n    //        videoRenderer.enableAmlogicFix(mPlayerTweaksData.isAmlogicFixEnabled());\r\n    //    }\r\n    //\r\n    //    replaceVideoRenderer(out, videoRenderer);\r\n    //}\r\n\r\n    // 2.10, 2.11\r\n    @Override\r\n    protected void buildAudioRenderers(Context context, @ExtensionRendererMode int extensionRendererMode, MediaCodecSelector mediaCodecSelector,\r\n                                       @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,\r\n                                       boolean enableDecoderFallback, AudioProcessor[] audioProcessors, Handler eventHandler,\r\n                                       AudioRendererEventListener eventListener, ArrayList<Renderer> out) {\r\n        super.buildAudioRenderers(context, extensionRendererMode, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys,\r\n                enableDecoderFallback, audioProcessors, eventHandler, eventListener, out);\r\n\r\n        if (mPlayerData.getAudioDelayMs() == 0 && !mPlayerTweaksData.isAudioSyncFixEnabled()) {\r\n            // Improve performance a bit by eliminating calculations presented in custom renderer.\r\n\r\n            return;\r\n        }\r\n\r\n        DelayMediaCodecAudioRenderer audioRenderer =\r\n                new DelayMediaCodecAudioRenderer(context, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, enableDecoderFallback,\r\n                        eventHandler, eventListener, new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors));\r\n\r\n        audioRenderer.setAudioDelayMs(mPlayerData.getAudioDelayMs());\r\n        audioRenderer.enableAudioSyncFix(mPlayerTweaksData.isAudioSyncFixEnabled());\r\n\r\n        replaceAudioRenderer(out, audioRenderer);\r\n    }\r\n\r\n    // 2.10, 2.11\r\n    @Override\r\n    protected void buildVideoRenderers(Context context, int extensionRendererMode, MediaCodecSelector mediaCodecSelector,\r\n                                       @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,\r\n                                       boolean enableDecoderFallback, Handler eventHandler, VideoRendererEventListener eventListener,\r\n                                       long allowedVideoJoiningTimeMs, ArrayList<Renderer> out) {\r\n        super.buildVideoRenderers(context, extensionRendererMode, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys,\r\n                enableDecoderFallback, eventHandler, eventListener, allowedVideoJoiningTimeMs, out);\r\n        \r\n        if (!mPlayerTweaksData.isAmazonFrameDropFixEnabled() && !mPlayerTweaksData.isSonyFrameDropFixEnabled() && !mPlayerTweaksData.isAmlogicFixEnabled()) {\r\n            // Improve performance a bit by eliminating some if conditions presented in tweaks.\r\n            // But we need to obtain codec real name somehow. So use interceptor below.\r\n\r\n            DebugInfoMediaCodecVideoRenderer videoRenderer =\r\n                    new DebugInfoMediaCodecVideoRenderer(context, mediaCodecSelector, allowedVideoJoiningTimeMs, drmSessionManager,\r\n                        playClearSamplesWithoutKeys, enableDecoderFallback, eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);\r\n\r\n            videoRenderer.enableSetOutputSurfaceWorkaround(true); // Force enable?\r\n\r\n            replaceVideoRenderer(out, videoRenderer);\r\n\r\n            return;\r\n        }\r\n\r\n        TweaksMediaCodecVideoRenderer videoRenderer =\r\n                new TweaksMediaCodecVideoRenderer(context, mediaCodecSelector, allowedVideoJoiningTimeMs, drmSessionManager,\r\n                        playClearSamplesWithoutKeys, enableDecoderFallback, eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);\r\n\r\n        videoRenderer.enableFrameDropFix(mPlayerTweaksData.isAmazonFrameDropFixEnabled());\r\n        videoRenderer.enableFrameDropSonyFix(mPlayerTweaksData.isSonyFrameDropFixEnabled());\r\n        videoRenderer.enableAmlogicFix(mPlayerTweaksData.isAmlogicFixEnabled());\r\n        videoRenderer.enableSetOutputSurfaceWorkaround(true); // Force enable?\r\n\r\n        replaceVideoRenderer(out, videoRenderer);\r\n    }\r\n\r\n    // Exo 2.12, 2.13\r\n    //@Override\r\n    //protected void buildAudioRenderers(Context context,\r\n    //                                   int extensionRendererMode,\r\n    //                                   MediaCodecSelector mediaCodecSelector,\r\n    //                                   boolean enableDecoderFallback,\r\n    //                                   AudioSink audioSink,\r\n    //                                   Handler eventHandler,\r\n    //                                   AudioRendererEventListener eventListener,\r\n    //                                   ArrayList<Renderer> out) {\r\n    //    super.buildAudioRenderers(\r\n    //            context,\r\n    //            extensionRendererMode,\r\n    //            mediaCodecSelector,\r\n    //            enableDecoderFallback,\r\n    //            audioSink,\r\n    //            eventHandler,\r\n    //            eventListener,\r\n    //            out);\r\n    //\r\n    //    if (mPlayerData.getAudioDelayMs() == 0) {\r\n    //        // Improve performance a bit by eliminating calculations presented in custom renderer.\r\n    //\r\n    //        return;\r\n    //    }\r\n    //\r\n    //    DelayMediaCodecAudioRenderer audioRenderer =\r\n    //            new DelayMediaCodecAudioRenderer(context,\r\n    //                    mediaCodecSelector,\r\n    //                    enableDecoderFallback,\r\n    //                    eventHandler,\r\n    //                    eventListener,\r\n    //                    audioSink);\r\n    //\r\n    //    audioRenderer.setAudioDelayMs(mPlayerData.getAudioDelayMs());\r\n    //\r\n    //    // Restore global operation mode (needed for stability)\r\n    //    //audioRenderer.experimentalSetMediaCodecOperationMode(mOperationMode);\r\n    //\r\n    //    replaceAudioRenderer(out, audioRenderer);\r\n    //}\r\n\r\n    // Exo 2.12, 2.13\r\n    //@Override\r\n    //protected void buildVideoRenderers(Context context,\r\n    //                                   int extensionRendererMode,\r\n    //                                   MediaCodecSelector mediaCodecSelector,\r\n    //                                   boolean enableDecoderFallback,\r\n    //                                   Handler eventHandler,\r\n    //                                   VideoRendererEventListener eventListener,\r\n    //                                   long allowedVideoJoiningTimeMs,\r\n    //                                   ArrayList<Renderer> out) {\r\n    //    super.buildVideoRenderers(\r\n    //            context,\r\n    //            extensionRendererMode,\r\n    //            mediaCodecSelector,\r\n    //            enableDecoderFallback,\r\n    //            eventHandler,\r\n    //            eventListener,\r\n    //            allowedVideoJoiningTimeMs,\r\n    //            out);\r\n    //\r\n    //\r\n    //    if (!mPlayerTweaksData.isFrameDropFixEnabled() && !mPlayerTweaksData.isAmlogicFixEnabled()) {\r\n    //        // Improve performance a bit by eliminating some if conditions presented in tweaks.\r\n    //        // But we need to obtain codec real name somehow. So use interceptor below.\r\n    //\r\n    //        DebugInfoMediaCodecVideoRenderer videoRenderer =\r\n    //                new DebugInfoMediaCodecVideoRenderer(context,\r\n    //                        mediaCodecSelector,\r\n    //                        allowedVideoJoiningTimeMs,\r\n    //                        enableDecoderFallback,\r\n    //                        eventHandler,\r\n    //                        eventListener,\r\n    //                        MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);\r\n    //\r\n    //        // Restore global operation mode (needed for stability)\r\n    //        //videoRenderer.experimentalSetMediaCodecOperationMode(mOperationMode);\r\n    //        videoRenderer.enableSetOutputSurfaceWorkaround(mPlayerTweaksData.isSetOutputSurfaceWorkaroundEnabled());\r\n    //\r\n    //        replaceVideoRenderer(out, videoRenderer);\r\n    //\r\n    //        return;\r\n    //    }\r\n    //\r\n    //    TweaksMediaCodecVideoRenderer videoRenderer =\r\n    //            new TweaksMediaCodecVideoRenderer(context,\r\n    //                    mediaCodecSelector,\r\n    //                    allowedVideoJoiningTimeMs,\r\n    //                    enableDecoderFallback,\r\n    //                    eventHandler,\r\n    //                    eventListener,\r\n    //                    MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);\r\n    //\r\n    //    videoRenderer.enableFrameDropFix(mPlayerTweaksData.isFrameDropFixEnabled());\r\n    //    videoRenderer.enableAmlogicFix(mPlayerTweaksData.isAmlogicFixEnabled());\r\n    //    videoRenderer.enableSetOutputSurfaceWorkaround(mPlayerTweaksData.isSetOutputSurfaceWorkaroundEnabled());\r\n    //\r\n    //    // Restore global operation mode (needed for stability)\r\n    //    //videoRenderer.experimentalSetMediaCodecOperationMode(mOperationMode);\r\n    //\r\n    //    replaceVideoRenderer(out, videoRenderer);\r\n    //}\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/versions/renderer/CustomRenderersFactoryBase.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.renderer;\r\n\r\nimport android.content.Context;\r\nimport com.google.android.exoplayer2.DefaultRenderersFactory;\r\nimport com.google.android.exoplayer2.Renderer;\r\nimport com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;\r\nimport com.google.android.exoplayer2.video.MediaCodecVideoRenderer;\r\n\r\nimport java.util.ArrayList;\r\n\r\npublic abstract class CustomRenderersFactoryBase extends DefaultRenderersFactory {\r\n    public CustomRenderersFactoryBase(Context context) {\r\n        super(context);\r\n    }\r\n\r\n    protected void replaceVideoRenderer(ArrayList<Renderer> renderers, MediaCodecVideoRenderer videoRenderer) {\r\n        if (renderers != null && videoRenderer != null) {\r\n            Renderer originMediaCodecVideoRenderer = null;\r\n            int index = 0;\r\n\r\n            for (Renderer renderer : renderers) {\r\n                if (renderer instanceof MediaCodecVideoRenderer) {\r\n                    originMediaCodecVideoRenderer = renderer;\r\n                    break;\r\n                }\r\n                index++;\r\n            }\r\n\r\n            if (originMediaCodecVideoRenderer != null) {\r\n                // replace origin with custom\r\n                renderers.remove(originMediaCodecVideoRenderer);\r\n                renderers.add(index, videoRenderer);\r\n            }\r\n        }\r\n    }\r\n\r\n    protected void replaceAudioRenderer(ArrayList<Renderer> renderers, MediaCodecAudioRenderer audioRenderer) {\r\n        if (renderers != null && audioRenderer != null) {\r\n            Renderer originMediaCodecAudioRenderer = null;\r\n            int index = 0;\r\n\r\n            for (Renderer renderer : renderers) {\r\n                if (renderer instanceof MediaCodecAudioRenderer) {\r\n                    originMediaCodecAudioRenderer = renderer;\r\n                    break;\r\n                }\r\n                index++;\r\n            }\r\n\r\n            if (originMediaCodecAudioRenderer != null) {\r\n                // replace origin with custom\r\n                renderers.remove(originMediaCodecAudioRenderer);\r\n                renderers.add(index, audioRenderer);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/versions/renderer/DebugInfoMediaCodecVideoRenderer.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.renderer;\r\n\r\nimport android.content.Context;\r\nimport android.media.MediaCodec;\r\nimport android.os.Build.VERSION;\r\nimport android.os.Handler;\r\nimport android.view.Surface;\r\nimport androidx.annotation.Nullable;\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.ExoPlaybackException;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\r\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\r\nimport com.google.android.exoplayer2.mediacodec.MediaCodecInfo;\r\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\r\nimport com.google.android.exoplayer2.video.MediaCodecVideoRenderer;\r\nimport com.google.android.exoplayer2.video.VideoRendererEventListener;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.ExoUtils;\r\n\r\npublic class DebugInfoMediaCodecVideoRenderer extends MediaCodecVideoRenderer {\r\n    private static final String TAG = DebugInfoMediaCodecVideoRenderer.class.getSimpleName();\r\n    private int mFrameIndex;\r\n    private boolean mIsSetOutputSurfaceWorkaroundEnabled;\r\n\r\n    // Exo 2.9\r\n    //public DebugInfoMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs,\r\n    //                                     @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,\r\n    //                                     @Nullable Handler eventHandler, @Nullable VideoRendererEventListener eventListener,\r\n    //                                     int maxDroppedFramesToNotify) {\r\n    //    super(context, mediaCodecSelector, allowedJoiningTimeMs, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener,\r\n    //            maxDroppedFramesToNotify);\r\n    //}\r\n\r\n    // Exo 2.10, 2.11\r\n    public DebugInfoMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs,\r\n                                            @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys, boolean enableDecoderFallback, @Nullable Handler eventHandler, @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) {\r\n        super(context, mediaCodecSelector, allowedJoiningTimeMs, drmSessionManager, playClearSamplesWithoutKeys, enableDecoderFallback, eventHandler, eventListener, maxDroppedFramesToNotify);\r\n    }\r\n\r\n    // Exo 2.12, 2.13\r\n    //public DebugInfoMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs,\r\n    //                                     boolean enableDecoderFallback, @Nullable Handler eventHandler,\r\n    //                                     @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) {\r\n    //    super(context, mediaCodecSelector, allowedJoiningTimeMs, enableDecoderFallback, eventHandler, eventListener, maxDroppedFramesToNotify);\r\n    //}\r\n\r\n    @Override\r\n    protected CodecMaxValues getCodecMaxValues(\r\n            MediaCodecInfo codecInfo, Format format, Format[] streamFormats) {\r\n        ExoUtils.updateVideoDecoderInfo(codecInfo);\r\n\r\n        return super.getCodecMaxValues(codecInfo, format, streamFormats);\r\n    }\r\n\r\n    // Measure real fps.\r\n    // Note, that you can't accurate measure frame rate because actual frame rate is the average frame rate for the whole video track!\r\n    // 29.97fps test: https://www.youtube.com/watch?v=LXb3EKWsInQ (Costa Rica)\r\n    // More info: https://github.com/google/ExoPlayer/issues/4088\r\n    //@Override\r\n    //protected void renderOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) {\r\n    //    super.renderOutputBuffer(codec, index, presentationTimeUs);\r\n    //}\r\n    //\r\n    //@Override\r\n    //protected void renderOutputBufferV21(MediaCodec codec, int index, long presentationTimeUs, long releaseTimeNs) {\r\n    //    super.renderOutputBufferV21(codec, index, presentationTimeUs, releaseTimeNs);\r\n    //\r\n    //    mFrameIndex++;\r\n    //\r\n    //    Log.d(TAG, \"Real fps: %s\", 1_000_000f / (presentationTimeUs / mFrameIndex));\r\n    //}\r\n\r\n    @Override\r\n    protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) {\r\n        // Null surface error on Android 9 (VERSION.SDK_INT >= 28) and above (appears on background audio playback)\r\n        // Need to be enabled on older version of ExoPlayer (e.g. 2.10.6).\r\n        // It's because there's no tweaks for modern devices.\r\n        return mIsSetOutputSurfaceWorkaroundEnabled || super.codecNeedsSetOutputSurfaceWorkaround(name);\r\n    }\r\n\r\n    /**\r\n     * Null surface error on Android 9 (VERSION.SDK_INT >= 28) and above (appears on background audio playback)<br/>\r\n     * Need to be enabled on older version of ExoPlayer (e.g. 2.10.6).<br/>\r\n     * It's because there's no tweaks for modern devices.\r\n     */\r\n    public void enableSetOutputSurfaceWorkaround(boolean enable) {\r\n        mIsSetOutputSurfaceWorkaroundEnabled = enable;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/versions/renderer/DelayMediaCodecAudioRenderer.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.renderer;\r\n\r\nimport android.content.Context;\r\nimport android.media.MediaCodec;\r\nimport android.os.Handler;\r\nimport androidx.annotation.Nullable;\r\nimport com.google.android.exoplayer2.ExoPlaybackException;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener;\r\nimport com.google.android.exoplayer2.audio.AudioSink;\r\nimport com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;\r\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\r\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\r\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\n\r\nimport java.nio.ByteBuffer;\r\n\r\npublic class DelayMediaCodecAudioRenderer extends MediaCodecAudioRenderer {\r\n    private static final String TAG = DelayMediaCodecAudioRenderer.class.getSimpleName();\r\n    private int mDelayUs;\r\n    private boolean mIsAudioSyncFixEnabled;\r\n    private boolean mIsAudioSyncFixChanged;\r\n\r\n    // Exo 2.9\r\n    //public CustomMediaCodecAudioRenderer(Context context, MediaCodecSelector mediaCodecSelector,\r\n    //                                           @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\r\n    //                                           boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler,\r\n    //                                           @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) {\r\n    //    super(context, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener, audioSink);\r\n    //}\r\n\r\n    // Exo 2.10, 2.11\r\n    public DelayMediaCodecAudioRenderer(Context context, MediaCodecSelector mediaCodecSelector,\r\n                                        @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\r\n                                        boolean playClearSamplesWithoutKeys, boolean enableDecoderFallback, @Nullable Handler eventHandler,\r\n                                        @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) {\r\n        super(context, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, enableDecoderFallback, eventHandler, eventListener, audioSink);\r\n    }\r\n\r\n    // Exo 2.12, 2.13\r\n    //public DelayMediaCodecAudioRenderer(Context context, MediaCodecSelector mediaCodecSelector,\r\n    //                                        boolean enableDecoderFallback, @Nullable Handler eventHandler,\r\n    //                                        @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) {\r\n    //    super(context, mediaCodecSelector, enableDecoderFallback, eventHandler, eventListener, audioSink);\r\n    //}\r\n\r\n    @Override\r\n    public long getPositionUs() {\r\n        return super.getPositionUs() + mDelayUs;\r\n    }\r\n\r\n    public void setAudioDelayMs(int delayMs) {\r\n        mDelayUs = delayMs * 1_000;\r\n    }\r\n\r\n    public int getAudioDelayMs() {\r\n        return mDelayUs / 1_000;\r\n    }\r\n\r\n    @Override\r\n    protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, ByteBuffer buffer, int bufferIndex,\r\n                                          int bufferFlags, long bufferPresentationTimeUs, boolean isDecodeOnlyBuffer, boolean isLastBuffer, Format format) throws ExoPlaybackException {\r\n        boolean result = super.processOutputBuffer(\r\n                positionUs, elapsedRealtimeUs, codec, buffer, bufferIndex, bufferFlags,\r\n                bufferPresentationTimeUs, isDecodeOnlyBuffer, isLastBuffer, format\r\n        );\r\n\r\n        // Disable the use of AudioTrack.getTimestamp and force ExoPlayer to go through the legacy path of using\r\n        // AudioTrack.getPlaybackHeadPosition instead, which might help if the first one drifts but the second one doesn't.\r\n        if (mIsAudioSyncFixEnabled && mIsAudioSyncFixChanged) {\r\n            Object audioSink = Helpers.getField(this, \"audioSink\");\r\n            if (audioSink != null) {\r\n                Object audioTrackPositionTracker = Helpers.getField(audioSink, \"audioTrackPositionTracker\");\r\n                if (audioTrackPositionTracker != null) {\r\n                    Object audioTimestampPoller = Helpers.getField(audioTrackPositionTracker, \"audioTimestampPoller\");\r\n                    if (audioTimestampPoller != null) {\r\n                        Helpers.setField(audioTimestampPoller, \"audioTimestamp\", null);\r\n                        Helpers.setField(audioTimestampPoller, \"state\", 3);\r\n                        mIsAudioSyncFixChanged = false;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    public void enableAudioSyncFix(boolean enable) {\r\n        if (mIsAudioSyncFixEnabled == enable) {\r\n            return;\r\n        }\r\n\r\n        mIsAudioSyncFixEnabled = enable;\r\n        mIsAudioSyncFixChanged = true;\r\n    }\r\n\r\n    public boolean isAudioSyncFixEnabled() {\r\n        return mIsAudioSyncFixEnabled;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/versions/renderer/TweaksMediaCodecVideoRenderer.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.renderer;\r\n\r\nimport android.annotation.TargetApi;\r\nimport android.content.Context;\r\nimport android.media.MediaCodec;\r\nimport android.os.Handler;\r\nimport androidx.annotation.Nullable;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\r\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\r\nimport com.google.android.exoplayer2.mediacodec.MediaCodecInfo;\r\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\r\nimport com.google.android.exoplayer2.video.VideoRendererEventListener;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\n\r\npublic class TweaksMediaCodecVideoRenderer extends DebugInfoMediaCodecVideoRenderer {\r\n    private static final String TAG = TweaksMediaCodecVideoRenderer.class.getSimpleName();\r\n    private boolean mIsFrameDropFixEnabled;\r\n    private boolean mIsFrameDropSonyFixEnabled;\r\n    private boolean mIsAmlogicFixEnabled;\r\n\r\n    // Exo 2.9\r\n    //public CustomMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs,\r\n    //                                     @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,\r\n    //                                     @Nullable Handler eventHandler, @Nullable VideoRendererEventListener eventListener,\r\n    //                                     int maxDroppedFramesToNotify) {\r\n    //    super(context, mediaCodecSelector, allowedJoiningTimeMs, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener,\r\n    //            maxDroppedFramesToNotify);\r\n    //}\r\n\r\n    // Exo 2.10, 2.11\r\n    public TweaksMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs,\r\n                                         @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys, boolean enableDecoderFallback, @Nullable Handler eventHandler, @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) {\r\n        super(context, mediaCodecSelector, allowedJoiningTimeMs, drmSessionManager, playClearSamplesWithoutKeys, enableDecoderFallback, eventHandler, eventListener, maxDroppedFramesToNotify);\r\n    }\r\n\r\n    // Exo 2.12, 2.13\r\n    //public TweaksMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs,\r\n    //                                     boolean enableDecoderFallback, @Nullable Handler eventHandler,\r\n    //                                     @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) {\r\n    //    super(context, mediaCodecSelector, allowedJoiningTimeMs, enableDecoderFallback, eventHandler, eventListener, maxDroppedFramesToNotify);\r\n    //}\r\n\r\n    // EXO: 2.10, 2.11, 2.12\r\n    @TargetApi(21)\r\n    protected void renderOutputBufferV21(\r\n            MediaCodec codec, int index, long presentationTimeUs, long releaseTimeNs) {\r\n        // Fix frame drops on SurfaceView\r\n        // https://github.com/google/ExoPlayer/issues/6348\r\n        // https://developer.android.com/reference/android/media/MediaCodec#releaseOutputBuffer(int,%20long)\r\n        super.renderOutputBufferV21(codec, index, presentationTimeUs, mIsFrameDropFixEnabled ? 0 : releaseTimeNs);\r\n    }\r\n\r\n    // EXO: 2.13\r\n    //@TargetApi(21)\r\n    //protected void renderOutputBufferV21(\r\n    //        MediaCodecAdapter codec, int index, long presentationTimeUs, long releaseTimeNs) {\r\n    //    // Fix frame drops on SurfaceView\r\n    //    // https://github.com/google/ExoPlayer/issues/6348\r\n    //    // https://developer.android.com/reference/android/media/MediaCodec#releaseOutputBuffer(int,%20long)\r\n    //    super.renderOutputBufferV21(codec, index, presentationTimeUs, 0);\r\n    //}\r\n\r\n    @Override\r\n    protected CodecMaxValues getCodecMaxValues(\r\n            MediaCodecInfo codecInfo, Format format, Format[] streamFormats) {\r\n        CodecMaxValues maxValues =\r\n                super.getCodecMaxValues(codecInfo, format, streamFormats);\r\n\r\n        if (mIsAmlogicFixEnabled) {\r\n            if (maxValues.width < 1920 || maxValues.height < 1089) {\r\n                Log.d(TAG, \"Applying Amlogic fix...\");\r\n                return new CodecMaxValues(\r\n                        Math.max(maxValues.width, 1920),\r\n                        Math.max(maxValues.height, 1089),\r\n                        maxValues.inputSize);\r\n            }\r\n        }\r\n\r\n        return maxValues;\r\n    }\r\n\r\n    /**\r\n     * Frame drop fixes on Sony Bravia<br/>\r\n     * https://github.com/google/ExoPlayer/issues/6348#issuecomment-718986083\r\n     */\r\n    @Override\r\n    protected boolean isBufferLate(long earlyUs) {\r\n        if (mIsFrameDropSonyFixEnabled) {\r\n            return earlyUs < -1000000;\r\n        }\r\n\r\n        return super.isBufferLate(earlyUs);\r\n    }\r\n\r\n    /**\r\n     * Frame drop fixes on Sony Bravia<br/>\r\n     * https://github.com/google/ExoPlayer/issues/6348#issuecomment-718986083\r\n     */\r\n    @Override\r\n    protected boolean isBufferVeryLate(long earlyUs) {\r\n        if (mIsFrameDropSonyFixEnabled) {\r\n            return earlyUs < -1500000;\r\n        }\r\n\r\n        return super.isBufferVeryLate(earlyUs);\r\n    }\r\n\r\n    public void enableFrameDropFix(boolean enabled) {\r\n        mIsFrameDropFixEnabled = enabled;\r\n    }\r\n\r\n    public void enableFrameDropSonyFix(boolean enabled) {\r\n        mIsFrameDropSonyFixEnabled = enabled;\r\n    }\r\n\r\n    public void enableAmlogicFix(boolean enabled) {\r\n        mIsAmlogicFixEnabled = enabled;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/versions/selector/BlacklistMediaCodecSelector.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.selector;\r\n\r\nimport androidx.annotation.Nullable;\r\nimport com.google.android.exoplayer2.mediacodec.MediaCodecInfo;\r\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\r\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.renderer.CustomOverridesRenderersFactory;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\n/**\r\n * Usage {@link CustomOverridesRenderersFactory#setMediaCodecSelector}\r\n */\r\npublic class BlacklistMediaCodecSelector implements MediaCodecSelector {\r\n    private static final String TAG = BlacklistMediaCodecSelector.class.getSimpleName();\r\n\r\n    // list of strings used in blacklisting codecs\r\n    final static String[] ALL_DECODERS = {\r\n            \"OMX.google.h264.decoder\", \"OMX.google.vp9.decoder\", \"OMX.Nvidia.vp9.decoder\",\r\n            \"OMX.MTK.VIDEO.DECODER.VP9\", \"OMX.amlogic.vp9.decoder.awesome\", \"OMX.amlogic.avc.decoder.awesome\",\r\n            \"OMX.qcom.video.decoder.avc\", \"OMX.rk.video_decoder.avc\", \"OMX.allwinner.video.decoder.avc\"\r\n    };\r\n    final static String[] SW_DECODERS = {\"OMX.google\"};\r\n    final static String[] HW_DECODERS = {\"OMX.amlogic\", \"OMX.MTK\", \"OMX.Nvidia\", \"OMX.qcom\", \"OMX.rk\", \"OMX.allwinner\"};\r\n\r\n    // Ver. 2.9.6\r\n    //@Override\r\n    //public List<MediaCodecInfo> getDecoderInfos(String mimeType, boolean requiresSecureDecoder) throws MediaCodecUtil.DecoderQueryException {\r\n    //\r\n    //    List<MediaCodecInfo> codecInfos = MediaCodecUtil.getDecoderInfos(\r\n    //            mimeType, requiresSecureDecoder);\r\n    //    // filter codecs based on blacklist template\r\n    //    List<MediaCodecInfo> filteredCodecInfos = new ArrayList<>();\r\n    //    for (MediaCodecInfo codecInfo: codecInfos) {\r\n    //        Log.d(TAG, \"Checking codec: \" + codecInfo);\r\n    //        boolean blacklisted = false;\r\n    //        for (String blackListedCodec: BLACKLISTEDCODECS) {\r\n    //            if (codecInfo != null && codecInfo.name.toLowerCase().contains(blackListedCodec.toLowerCase())) {\r\n    //                Log.d(TAG, \"Blacklisting codec: \" + blackListedCodec);\r\n    //                blacklisted = true;\r\n    //                break;\r\n    //            }\r\n    //        }\r\n    //        if (!blacklisted) {\r\n    //            filteredCodecInfos.add(codecInfo);\r\n    //        }\r\n    //    }\r\n    //    return filteredCodecInfos;\r\n    //}\r\n\r\n    // Exo 2.10 and up\r\n    @Override\r\n    public List<MediaCodecInfo> getDecoderInfos(String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder) throws MediaCodecUtil.DecoderQueryException {\r\n\r\n        List<MediaCodecInfo> codecInfos = MediaCodecUtil.getDecoderInfos(\r\n                mimeType, requiresSecureDecoder, requiresTunnelingDecoder);\r\n        // filter codecs based on blacklist template\r\n        List<MediaCodecInfo> filteredCodecInfos = new ArrayList<>();\r\n        for (MediaCodecInfo codecInfo: codecInfos) {\r\n            Log.d(TAG, \"Checking codec: \" + codecInfo);\r\n            boolean blacklisted = false;\r\n            for (String blacklistedDecoder: HW_DECODERS) {\r\n                if (codecInfo != null && codecInfo.name.toLowerCase().startsWith(blacklistedDecoder.toLowerCase())) {\r\n                    Log.d(TAG, \"Blacklisting decoder: \" + blacklistedDecoder);\r\n                    blacklisted = true;\r\n                    break;\r\n                }\r\n            }\r\n            if (!blacklisted) {\r\n                filteredCodecInfos.add(codecInfo);\r\n            }\r\n        }\r\n        return filteredCodecInfos;\r\n    }\r\n\r\n    // Exo 2.10\r\n    @Nullable\r\n    @Override\r\n    public MediaCodecInfo getPassthroughDecoderInfo() throws MediaCodecUtil.DecoderQueryException {\r\n        return MediaCodecUtil.getPassthroughDecoderInfo();\r\n    }\r\n}"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/versions/selector/RestoreTrackSelector.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.selector;\r\n\r\nimport android.util.Pair;\r\nimport androidx.annotation.Nullable;\r\nimport com.google.android.exoplayer2.ExoPlaybackException;\r\nimport com.google.android.exoplayer2.RendererCapabilities;\r\nimport com.google.android.exoplayer2.source.TrackGroupArray;\r\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\r\nimport com.google.android.exoplayer2.trackselection.TrackSelection.Definition;\r\nimport com.google.android.exoplayer2.trackselection.TrackSelection.Factory;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.TrackSelectorManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.MediaTrack;\r\n\r\npublic class RestoreTrackSelector extends DefaultTrackSelector {\r\n    private static final String TAG = RestoreTrackSelector.class.getSimpleName();\r\n    private static final int FORMAT_NOT_SUPPORTED = 19;\r\n    private static final int FORMAT_FORCE_SUPPORT = 52;\r\n    private TrackSelectorCallback mCallback;\r\n\r\n    public interface TrackSelectorCallback {\r\n        Pair<Definition, MediaTrack> onSelectVideoTrack(TrackGroupArray groups, Parameters params);\r\n        Pair<Definition, MediaTrack> onSelectAudioTrack(TrackGroupArray groups, Parameters params);\r\n        Pair<Definition, MediaTrack> onSelectSubtitleTrack(TrackGroupArray groups, Parameters params);\r\n        void updateVideoTrackSelection(TrackGroupArray groups, Parameters params, Definition definition);\r\n        void updateAudioTrackSelection(TrackGroupArray groups, Parameters params, Definition definition);\r\n        void updateSubtitleTrackSelection(TrackGroupArray groups, Parameters params, Definition definition);\r\n    }\r\n\r\n    public RestoreTrackSelector(Factory trackSelectionFactory) {\r\n        super(trackSelectionFactory);\r\n        // Could help with Shield resolution bug?\r\n        //setParameters(buildUponParameters().setForceHighestSupportedBitrate(true));\r\n    }\r\n\r\n    public void setOnTrackSelectCallback(TrackSelectorCallback callback) {\r\n        mCallback = callback;\r\n    }\r\n\r\n    // Exo 2.9\r\n    //@Nullable\r\n    //@Override\r\n    //protected TrackSelection selectVideoTrack(TrackGroupArray groups, int[][] formatSupports, int mixedMimeTypeAdaptationSupports,\r\n    //                                          Parameters params, @Nullable Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException {\r\n    //    if (mCallback != null) {\r\n    //        Pair<Definition, MediaTrack> resultPair = mCallback.onSelectVideoTrack(groups, params);\r\n    //\r\n    //        if (resultPair != null) {\r\n    //            Log.d(TAG, \"selectVideoTrack: choose custom video processing\");\r\n    //            return resultPair.first.toSelection();\r\n    //        }\r\n    //    }\r\n    //\r\n    //    Log.d(TAG, \"selectVideoTrack: choose default video processing\");\r\n    //\r\n    //    TrackSelection trackSelection = super.selectVideoTrack(groups, formatSupports, mixedMimeTypeAdaptationSupports, params, adaptiveTrackSelectionFactory);\r\n    //\r\n    //    // Don't invoke if track already has been selected by the app\r\n    //    if (mCallback != null && trackSelection != null && !params.hasSelectionOverride(TrackSelectorManager.RENDERER_INDEX_VIDEO, groups)) {\r\n    //        mCallback.updateVideoTrackSelection(groups, params, Definition.from(trackSelection));\r\n    //    }\r\n    //\r\n    //    return trackSelection;\r\n    //}\r\n\r\n    // Exo 2.9\r\n    //@Nullable\r\n    //@Override\r\n    //protected Pair<TrackSelection, AudioTrackScore> selectAudioTrack(TrackGroupArray groups, int[][] formatSupports,\r\n    //                                                                 int mixedMimeTypeAdaptationSupports, Parameters params,\r\n    //                                                                 @Nullable Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException {\r\n    //    if (mCallback != null) {\r\n    //        Pair<Definition, MediaTrack> resultPair = mCallback.onSelectAudioTrack(groups, params);\r\n    //        if (resultPair != null) {\r\n    //            Log.d(TAG, \"selectVideoTrack: choose custom audio processing\");\r\n    //            return new Pair<>(resultPair.first.toSelection(), new AudioTrackScore(resultPair.second.format, params, RendererCapabilities.FORMAT_HANDLED));\r\n    //        }\r\n    //    }\r\n    //\r\n    //    Log.d(TAG, \"selectAudioTrack: choose default audio processing\");\r\n    //\r\n    //    Pair<TrackSelection, AudioTrackScore> selectionPair =\r\n    //            super.selectAudioTrack(groups, formatSupports, mixedMimeTypeAdaptationSupports, params, adaptiveTrackSelectionFactory);\r\n    //\r\n    //    // Don't invoke if track already has been selected by the app\r\n    //    if (mCallback != null && selectionPair != null && !params.hasSelectionOverride(TrackSelectorManager.RENDERER_INDEX_AUDIO, groups)) {\r\n    //        mCallback.updateAudioTrackSelection(groups, params, Definition.from(selectionPair.first));\r\n    //    }\r\n    //\r\n    //    return selectionPair;\r\n    //}\r\n\r\n    // Exo 2.9\r\n    //@Nullable\r\n    //@Override\r\n    //protected Pair<TrackSelection, Integer> selectTextTrack(TrackGroupArray groups, int[][] formatSupport, Parameters params) throws ExoPlaybackException {\r\n    //    if (mCallback != null) {\r\n    //        Pair<Definition, MediaTrack> resultPair = mCallback.onSelectSubtitleTrack(groups, params);\r\n    //        if (resultPair != null) {\r\n    //            Log.d(TAG, \"selectTextTrack: choose custom text processing\");\r\n    //            return new Pair<>(resultPair.first.toSelection(), 10);\r\n    //        }\r\n    //    }\r\n    //\r\n    //    Log.d(TAG, \"selectTextTrack: choose default text processing\");\r\n    //\r\n    //    Pair<TrackSelection, Integer> selectionPair = super.selectTextTrack(groups, formatSupport, params);\r\n    //\r\n    //    // Don't invoke if track already has been selected by the app\r\n    //    if (mCallback != null && selectionPair != null && !params.hasSelectionOverride(TrackSelectorManager.RENDERER_INDEX_SUBTITLE, groups)) {\r\n    //        mCallback.updateSubtitleTrackSelection(groups, params, Definition.from(selectionPair.first));\r\n    //    }\r\n    //\r\n    //    return selectionPair;\r\n    //}\r\n\r\n    //@Override\r\n    //public void setParameters(Parameters parameters) {\r\n    //    // Fix dropping to 144p by disabling any overrides.\r\n    //    invalidate();\r\n    //}\r\n\r\n    // Exo 2.10 and up\r\n    @Nullable\r\n    @Override\r\n    protected Definition selectVideoTrack(TrackGroupArray groups, int[][] formatSupports, int mixedMimeTypeAdaptationSupports,\r\n                                              Parameters params, boolean enableAdaptiveTrackSelection) throws ExoPlaybackException {\r\n        if (mCallback != null) {\r\n            Pair<Definition, MediaTrack> resultPair = mCallback.onSelectVideoTrack(groups, params);\r\n\r\n            if (resultPair != null) {\r\n                Log.d(TAG, \"selectVideoTrack: choose custom video processing\");\r\n                return resultPair.first;\r\n            } else {\r\n                return null; // video disabled\r\n            }\r\n        }\r\n\r\n        Log.d(TAG, \"selectVideoTrack: choose default video processing\");\r\n\r\n        Definition definition = super.selectVideoTrack(groups, formatSupports, mixedMimeTypeAdaptationSupports, params, false);\r\n\r\n        // Don't invoke if track already has been selected by the app\r\n        if (mCallback != null && definition != null) {\r\n            mCallback.updateVideoTrackSelection(groups, params, definition);\r\n        }\r\n\r\n        return definition;\r\n    }\r\n\r\n    // Exo 2.10 and up\r\n    @Nullable\r\n    @Override\r\n    protected Pair<Definition, AudioTrackScore> selectAudioTrack(TrackGroupArray groups, int[][] formatSupports,\r\n                                                                 int mixedMimeTypeAdaptationSupports, Parameters params, boolean enableAdaptiveTrackSelection) throws ExoPlaybackException {\r\n        if (mCallback != null) {\r\n            Pair<Definition, MediaTrack> resultPair = mCallback.onSelectAudioTrack(groups, params);\r\n            if (resultPair != null) {\r\n                Log.d(TAG, \"selectVideoTrack: choose custom audio processing\");\r\n                return new Pair<>(resultPair.first, new AudioTrackScore(resultPair.second.format, params, RendererCapabilities.FORMAT_HANDLED));\r\n            } else {\r\n                return null; // audio disabled\r\n            }\r\n        }\r\n\r\n        Log.d(TAG, \"selectAudioTrack: choose default audio processing\");\r\n\r\n        Pair<Definition, AudioTrackScore> definitionPair = super.selectAudioTrack(groups, formatSupports,\r\n                mixedMimeTypeAdaptationSupports, params, false);\r\n\r\n        // Don't invoke if track already has been selected by the app\r\n        if (mCallback != null && definitionPair != null) {\r\n            mCallback.updateAudioTrackSelection(groups, params, definitionPair.first);\r\n        }\r\n\r\n        return definitionPair;\r\n    }\r\n\r\n    // Exo 2.10 and up\r\n    @Nullable\r\n    @Override\r\n    protected Pair<Definition, TextTrackScore> selectTextTrack(TrackGroupArray groups, int[][] formatSupport, Parameters params,\r\n                                                               @Nullable String selectedAudioLanguage) throws ExoPlaybackException {\r\n        if (mCallback != null) {\r\n            Pair<Definition, MediaTrack> resultPair = mCallback.onSelectSubtitleTrack(groups, params);\r\n            if (resultPair != null) {\r\n                Log.d(TAG, \"selectTextTrack: choose custom text processing\");\r\n                return new Pair<>(resultPair.first, new TextTrackScore(resultPair.second.format, params, RendererCapabilities.FORMAT_HANDLED, \"\"));\r\n            }\r\n        }\r\n\r\n        Log.d(TAG, \"selectTextTrack: choose default text processing\");\r\n\r\n        Pair<Definition, TextTrackScore> definitionPair = super.selectTextTrack(groups, formatSupport, params, selectedAudioLanguage);\r\n\r\n        // Don't invoke if track already has been selected by the app\r\n        if (mCallback != null && definitionPair != null) {\r\n            mCallback.updateSubtitleTrackSelection(groups, params, definitionPair.first);\r\n        }\r\n\r\n        return definitionPair;\r\n    }\r\n\r\n    private void unlockAllVideoFormats(int[][] formatSupports) {\r\n        final int videoTrackIndex = 0;\r\n\r\n        for (int j = 0; j < formatSupports[videoTrackIndex].length; j++) {\r\n            if (formatSupports[videoTrackIndex][j] == FORMAT_NOT_SUPPORTED) { // video format not supported by system decoders\r\n                formatSupports[videoTrackIndex][j] = FORMAT_FORCE_SUPPORT; // force support of video format\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/exoplayer/versions/selector/backport/Definition.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.exoplayer.versions.selector.backport;\r\n\r\nimport androidx.annotation.Nullable;\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.source.TrackGroup;\r\nimport com.google.android.exoplayer2.trackselection.FixedTrackSelection;\r\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\r\n\r\n// Backport from Exo 2.10 to 2.9\r\n/** Contains of a subset of selected tracks belonging to a {@link TrackGroup}. */\r\npublic final class Definition {\r\n    /** The {@link TrackGroup} which tracks belong to. */\r\n    public final TrackGroup group;\r\n    /** The indices of the selected tracks in {@link #group}. */\r\n    public final int[] tracks;\r\n    /** The track selection reason. One of the {@link C} SELECTION_REASON_ constants. */\r\n    public final int reason;\r\n    /** Optional data associated with this selection of tracks. */\r\n    @Nullable public final Object data;\r\n\r\n    /**\r\n     * @param group The {@link TrackGroup}. Must not be null.\r\n     * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be\r\n     *     null or empty. May be in any order.\r\n     */\r\n    public Definition(TrackGroup group, int... tracks) {\r\n        this(group, tracks, C.SELECTION_REASON_UNKNOWN, /* data= */ null);\r\n    }\r\n\r\n    /**\r\n     * @param group The {@link TrackGroup}. Must not be null.\r\n     * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be\r\n     * @param reason The track selection reason. One of the {@link C} SELECTION_REASON_ constants.\r\n     * @param data Optional data associated with this selection of tracks.\r\n     */\r\n    public Definition(TrackGroup group, int[] tracks, int reason, @Nullable Object data) {\r\n        this.group = group;\r\n        this.tracks = tracks;\r\n        this.reason = reason;\r\n        this.data = data;\r\n    }\r\n\r\n    // Exo 2.10\r\n    //public static Definition from(TrackSelection selection) {\r\n    //    return new Definition(selection.getTrackGroup(), selection.getSelectedIndex());\r\n    //}\r\n\r\n    // Exo 2.10\r\n    //@SuppressWarnings(\"deprecation\")\r\n    //public TrackSelection toSelection() {\r\n    //    return new FixedTrackSelection.Factory().createTrackSelection(group, null, tracks);\r\n    //}\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/AppDataSourceManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.SettingsItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.AboutSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.AboutSimpleSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.AccountSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.AutoFrameRateSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.BackupSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.SponsorBlockSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.DeArrowSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.GeneralSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.LanguageSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.MainUISettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.PlayerSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.RemoteControlSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.SearchSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.SubtitleSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem.VideoPreset;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class AppDataSourceManager {\r\n    private static AppDataSourceManager sInstance;\r\n\r\n    private AppDataSourceManager() {\r\n    }\r\n\r\n    public static AppDataSourceManager instance() {\r\n        if (sInstance == null) {\r\n            sInstance = new AppDataSourceManager();\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public List<SettingsItem> getSettingItems(Context context) {\r\n        List<SettingsItem> settingItems = new ArrayList<>();\r\n\r\n        settingItems.add(new SettingsItem(\r\n                context.getString(R.string.settings_accounts), () -> AccountSettingsPresenter.instance(context).show(), R.drawable.settings_account));\r\n        settingItems.add(new SettingsItem(\r\n                context.getString(R.string.settings_remote_control), () -> RemoteControlSettingsPresenter.instance(context).show(), R.drawable.settings_cast));\r\n        settingItems.add(new SettingsItem(\r\n                context.getString(R.string.settings_language_country), () -> LanguageSettingsPresenter.instance(context).show(), R.drawable.settings_language));\r\n        settingItems.add(new SettingsItem(\r\n                context.getString(R.string.settings_general), () -> GeneralSettingsPresenter.instance(context).show(), R.drawable.settings_app));\r\n        settingItems.add(new SettingsItem(\r\n                context.getString(R.string.settings_main_ui), () -> MainUISettingsPresenter.instance(context).show(), R.drawable.settings_main_ui));\r\n        settingItems.add(new SettingsItem(\r\n                context.getString(R.string.settings_player), () -> PlayerSettingsPresenter.instance(context).show(), R.drawable.settings_player));\r\n        // Don't add afr support check here.\r\n        // Users want even fake afr settings.\r\n        settingItems.add(new SettingsItem(\r\n                context.getString(R.string.auto_frame_rate), () -> AutoFrameRateSettingsPresenter.instance(context).show(), R.drawable.settings_afr));\r\n        settingItems.add(new SettingsItem(\r\n                context.getString(R.string.subtitle_category_title), () -> SubtitleSettingsPresenter.instance(context).show(), R.drawable.settings_subtitles));\r\n        settingItems.add(new SettingsItem(\r\n                context.getString(R.string.settings_search), () -> SearchSettingsPresenter.instance(context).show(), R.drawable.settings_search));\r\n        settingItems.add(new SettingsItem(\r\n                context.getString(R.string.content_block_provider), () -> SponsorBlockSettingsPresenter.instance(context).show(), R.drawable.settings_block));\r\n        settingItems.add(new SettingsItem(\r\n                context.getString(R.string.dearrow_provider), () -> DeArrowSettingsPresenter.instance(context).show(), R.drawable.settings_dearrow));\r\n        settingItems.add(new SettingsItem(\r\n                context.getString(R.string.app_backup_restore), () -> BackupSettingsPresenter.instance(context).show(), R.drawable.settings_backup));\r\n\r\n        if (Helpers.equalsAny(context.getPackageName(), Utils.KNOWN_PACKAGES)) {\r\n            settingItems.add(new SettingsItem(\r\n                    context.getString(R.string.settings_about), () -> AboutSettingsPresenter.instance(context).show(), R.drawable.settings_about));\r\n        } else {\r\n            settingItems.add(new SettingsItem(\r\n                    context.getString(R.string.settings_about), () -> AboutSimpleSettingsPresenter.instance(context).show(), R.drawable.settings_about));\r\n        }\r\n\r\n        return settingItems;\r\n    }\r\n\r\n    public VideoPreset[] getVideoPresets() {\r\n        VideoPreset[] presets = {\r\n                new VideoPreset(\"144p     30fps    avc\", \"256,144,30,avc\"),\r\n                new VideoPreset(\"144p     30fps    vp9\", \"256,144,30,vp9\"),\r\n                new VideoPreset(\"144p     30fps    av01\", \"256,144,30,av01\"),\r\n                new VideoPreset(\"240p     30fps    avc\", \"320,240,30,avc\"),\r\n                new VideoPreset(\"240p     30fps    vp9\", \"320,240,30,vp9\"),\r\n                new VideoPreset(\"240p     30fps    av01\", \"320,240,30,av01\"),\r\n                new VideoPreset(\"360p     30fps    avc\", \"640,360,30,avc\"),\r\n                new VideoPreset(\"360p     30fps    vp9\", \"640,360,30,vp9\"),\r\n                new VideoPreset(\"360p     30fps    av01\", \"640,360,30,av01\"),\r\n                new VideoPreset(\"360p     60fps    avc\", \"640,360,60,avc\"),\r\n                new VideoPreset(\"360p     60fps    vp9\", \"640,360,60,vp9\"),\r\n                new VideoPreset(\"360p     60fps    av01\", \"640,360,60,av01\"),\r\n                new VideoPreset(\"480p     30fps    avc\", \"854,480,30,avc\"),\r\n                new VideoPreset(\"480p     30fps    vp9\", \"854,480,30,vp9\"),\r\n                new VideoPreset(\"480p     30fps    av01\", \"854,480,30,av01\"),\r\n                new VideoPreset(\"480p     60fps    avc\", \"854,480,60,avc\"),\r\n                new VideoPreset(\"480p     60fps    vp9\", \"854,480,60,vp9\"),\r\n                new VideoPreset(\"480p     60fps    av01\", \"854,480,60,av01\"),\r\n                new VideoPreset(\"720p     30fps    avc\", \"1280,720,30,avc\"),\r\n                new VideoPreset(\"720p     60fps    avc\", \"1280,720,60,avc\"),\r\n                new VideoPreset(\"720p     30fps    vp9\", \"1280,720,30,vp9\"),\r\n                new VideoPreset(\"720p     60fps    vp9\", \"1280,720,60,vp9\"),\r\n                new VideoPreset(\"720p     30fps    av01\", \"1280,720,30,av01\"),\r\n                new VideoPreset(\"720p     60fps    av01\", \"1280,720,60,av01\"),\r\n                new VideoPreset(\"720p     30fps    av01+hdr\", \"1280,720,30,av01.hdr\"),\r\n                new VideoPreset(\"720p     60fps    av01+hdr\", \"1280,720,60,av01.hdr\"),\r\n                new VideoPreset(\"1080p    30fps    avc\", \"1920,1080,30,avc\"),\r\n                new VideoPreset(\"1080p    60fps    avc\", \"1920,1080,60,avc\"),\r\n                new VideoPreset(\"1080p    30fps    vp9\", \"1920,1080,30,vp9\"),\r\n                new VideoPreset(\"1080p    60fps    vp9\", \"1920,1080,60,vp9\"),\r\n                new VideoPreset(\"1080p    30fps    vp9+hdr\", \"1920,1080,30,vp9.2\"),\r\n                new VideoPreset(\"1080p    60fps    vp9+hdr\", \"1920,1080,60,vp9.2\"),\r\n                new VideoPreset(\"1080p    30fps    av01\", \"1920,1080,30,av01\"),\r\n                new VideoPreset(\"1080p    60fps    av01\", \"1920,1080,60,av01\"),\r\n                new VideoPreset(\"1080p    30fps    av01+hdr\", \"1920,1080,30,av01.hdr\"),\r\n                new VideoPreset(\"1080p    60fps    av01+hdr\", \"1920,1080,60,av01.hdr\"),\r\n                new VideoPreset(\"(2K) 1440p    30fps    vp9\", \"2560,1440,30,vp9\"),\r\n                new VideoPreset(\"(2K) 1440p    60fps    vp9\", \"2560,1440,60,vp9\"),\r\n                new VideoPreset(\"(2K) 1440p    30fps    vp9+hdr\", \"2560,1440,30,vp9.2\"),\r\n                new VideoPreset(\"(2K) 1440p    60fps    vp9+hdr\", \"2560,1440,60,vp9.2\"),\r\n                new VideoPreset(\"(2K) 1440p    30fps    av01\", \"2560,1440,30,av01\"),\r\n                new VideoPreset(\"(2K) 1440p    60fps    av01\", \"2560,1440,60,av01\"),\r\n                new VideoPreset(\"(2K) 1440p    30fps    av01+hdr\", \"2560,1440,30,av01.hdr\"),\r\n                new VideoPreset(\"(2K) 1440p    60fps    av01+hdr\", \"2560,1440,60,av01.hdr\"),\r\n                new VideoPreset(\"(4K) 2160p    30fps    vp9\", \"3840,2160,30,vp9\"),\r\n                new VideoPreset(\"(4K) 2160p    60fps    vp9\", \"3840,2160,60,vp9\"),\r\n                new VideoPreset(\"(4K) 2160p    30fps    vp9+hdr\", \"3840,2160,30,vp9.2\"),\r\n                new VideoPreset(\"(4K) 2160p    60fps    vp9+hdr\", \"3840,2160,60,vp9.2\"),\r\n                new VideoPreset(\"(4K) 2160p    30fps    av01\", \"3840,2160,30,av01\"),\r\n                new VideoPreset(\"(4K) 2160p    60fps    av01\", \"3840,2160,60,av01\"),\r\n                new VideoPreset(\"(4K) 2160p    30fps    av01+hdr\", \"3840,2160,30,av01.hdr\"),\r\n                new VideoPreset(\"(4K) 2160p    60fps    av01+hdr\", \"3840,2160,60,av01.hdr\"),\r\n                new VideoPreset(\"(8K) 4320p    30fps    vp9\", \"7680,4320,30,vp9\"),\r\n                new VideoPreset(\"(8K) 4320p    60fps    vp9\", \"7680,4320,60,vp9\"),\r\n                new VideoPreset(\"(8K) 4320p    30fps    vp9+hdr\", \"7680,4320,30,vp9.2\"),\r\n                new VideoPreset(\"(8K) 4320p    60fps    vp9+hdr\", \"7680,4320,60,vp9.2\"),\r\n                new VideoPreset(\"(8K) 4320p    30fps    av01\", \"7680,4320,30,av01\"),\r\n                new VideoPreset(\"(8K) 4320p    60fps    av01\", \"7680,4320,60,av01\"),\r\n                new VideoPreset(\"(8K) 4320p    30fps    av01+hdr\", \"7680,4320,30,av01.hdr\"),\r\n                new VideoPreset(\"(8K) 4320p    60fps    av01+hdr\", \"7680,4320,60,av01.hdr\"),\r\n                //new VideoPreset(\"Adaptive\", null)\r\n        };\r\n\r\n        return presets;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackgroundPlaybackService.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.app.Notification;\r\nimport android.app.NotificationChannel;\r\nimport android.app.NotificationManager;\r\nimport android.app.Service;\r\nimport android.content.Context;\r\nimport android.content.Intent;\r\nimport android.os.Build;\r\nimport android.os.IBinder;\r\nimport androidx.annotation.Nullable;\r\nimport androidx.core.app.NotificationCompat;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\n\r\n/**\r\n * This service isn't used at all! It can be safely removed in the future.\r\n */\r\npublic class BackgroundPlaybackService extends Service {\r\n    private static final String TAG = BackgroundPlaybackService.class.getSimpleName();\r\n\r\n    @Override\r\n    public void onCreate() {\r\n        super.onCreate();\r\n\r\n        if (Build.VERSION.SDK_INT >= 26) {\r\n            String CHANNEL_ID = \"my_channel_01\";\r\n            NotificationChannel channel = new NotificationChannel(CHANNEL_ID,\r\n                    \"Channel human readable title\",\r\n                    NotificationManager.IMPORTANCE_DEFAULT);\r\n\r\n            ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);\r\n\r\n            Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)\r\n                    .setContentTitle(\"\")\r\n                    .setContentText(\"\").build();\r\n\r\n            startForeground(1, notification);\r\n        }\r\n    }\r\n\r\n    @Nullable\r\n    @Override\r\n    public IBinder onBind(Intent intent) {\r\n        Log.d(TAG, \"onBind: %s\", Helpers.toString(intent));\r\n\r\n        return null;\r\n    }\r\n\r\n    @Override\r\n    public int onStartCommand(Intent intent, int flags, int startId) {\r\n        Log.d(TAG, \"onStartCommand: %s\", Helpers.toString(intent));\r\n\r\n        return super.onStartCommand(intent, flags, startId);\r\n    }\r\n\r\n    public static void start(Context context) {\r\n        if (Build.VERSION.SDK_INT >= 26) {\r\n            // Fake service to prevent the app from destroying\r\n            Intent serviceIntent = new Intent(context, BackgroundPlaybackService.class);\r\n            context.startForegroundService(serviceIntent);\r\n        }\r\n    }\r\n\r\n    public static void stop(Context context) {\r\n        if (Build.VERSION.SDK_INT >= 26) {\r\n            // Fake service to prevent the app from destroying\r\n            Intent serviceIntent = new Intent(context, BackgroundPlaybackService.class);\r\n            context.stopService(serviceIntent);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreHelper.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.app.Activity;\r\nimport android.content.Context;\r\nimport android.content.Intent;\r\nimport android.content.pm.PackageManager;\r\nimport android.database.Cursor;\r\nimport android.net.Uri;\r\nimport android.os.Build.VERSION;\r\nimport android.provider.OpenableColumns;\r\nimport android.widget.Toast;\r\n\r\nimport androidx.core.content.FileProvider;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.FileHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.BackupSettingsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MotherActivity.OnResult;\r\n\r\nimport java.io.File;\r\nimport java.io.FileOutputStream;\r\nimport java.io.InputStream;\r\nimport java.io.OutputStream;\r\n\r\npublic class BackupAndRestoreHelper implements OnResult {\r\n    private static final int REQ_PICK_FILES = 1001;\r\n    private final Context mContext;\r\n    private Runnable mOnSuccess;\r\n    private final String[] mPreferredFileManagers = {\r\n            \"com.ghisler.android.TotalCommander\",\r\n            \"com.lonelycatgames.Xplore\",\r\n            \"com.alphainventor.filemanager\",\r\n            \"pl.solidexplorer2\"\r\n    };\r\n\r\n    public BackupAndRestoreHelper(Context context) {\r\n        mContext = context;\r\n    }\r\n\r\n    public void exportAppMediaFolder() {\r\n        File mediaDir = FileHelpers.getExternalMediaDirectory(mContext);\r\n        File dataDir = new File(mediaDir, \"data\");\r\n        if (!dataDir.exists() || FileHelpers.isEmpty(dataDir)) return;\r\n\r\n        File zipFile = new File(mediaDir,  \"backup_\" + mContext.getPackageName() + \".zip\");\r\n        ZipHelper2.zipDirectory(dataDir, zipFile);\r\n\r\n        Uri uri = FileProvider.getUriForFile(\r\n                mContext,\r\n                mContext.getPackageName() + \".update_provider\",\r\n                zipFile\r\n        );\r\n\r\n        try {\r\n            openFileManager(uri);\r\n        } catch (Exception e) {\r\n            // Activity launch may fail if called from background (e.g. WorkManager)\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n\r\n    private void openFileManager(Uri uri) {\r\n        Intent intent = new Intent(Intent.ACTION_SEND);\r\n        //intent.setType(\"application/zip\");\r\n        intent.setType(\"*/*\");\r\n        intent.putExtra(Intent.EXTRA_STREAM, uri);\r\n        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);\r\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\r\n\r\n        PackageManager pm = mContext.getPackageManager();\r\n\r\n        for (String pkg : mPreferredFileManagers) {\r\n            Intent targeted = new Intent(intent);\r\n            targeted.setPackage(pkg);\r\n            if (targeted.resolveActivity(pm) != null) {\r\n                mContext.grantUriPermission(pkg, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);\r\n                mContext.startActivity(targeted);\r\n                return;\r\n            }\r\n        }\r\n\r\n        mContext.startActivity(Intent.createChooser(intent, mContext.getString(R.string.app_backup)));\r\n    }\r\n\r\n    /**\r\n     * NOTE: The file picker relies on apps that support the Storage Access Framework (SAF).\r\n     * At the moment, no known third-party file manager properly supports selecting ZIP\r\n     * archives through this API, so the backup file may not appear in the picker.\r\n     */\r\n    public void importAppMediaFolder(Runnable onSuccess) {\r\n        if (VERSION.SDK_INT < 19 || onSuccess == null) {\r\n            return;\r\n        }\r\n\r\n        mOnSuccess = onSuccess;\r\n\r\n        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);\r\n        intent.setType(\"*/*\");\r\n        //intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);\r\n        //intent.addCategory(Intent.CATEGORY_OPENABLE);\r\n        //intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{\r\n        //        \"application/zip\",\r\n        //        \"application/x-zip-compressed\"\r\n        //});\r\n\r\n        ((MotherActivity) mContext).addOnResult(this);\r\n\r\n        ((Activity) mContext).startActivityForResult(intent, REQ_PICK_FILES);\r\n    }\r\n\r\n    @Override\r\n    public void onResult(int requestCode, int resultCode, Intent data) {\r\n        if (requestCode == REQ_PICK_FILES && resultCode == Activity.RESULT_OK) {\r\n\r\n            if (data == null) return;\r\n\r\n            File mediaDir = FileHelpers.getExternalMediaDirectory(mContext);\r\n\r\n            Uri uri = data.getData();\r\n            if (uri == null && data.getClipData() != null) {\r\n                uri = data.getClipData().getItemAt(0).getUri();\r\n            }\r\n            if (uri == null) return;\r\n\r\n            File zipFile = new File(mediaDir, \"restore.zip\");\r\n            copyUriToDir(uri, zipFile);\r\n\r\n            unpackTempZip(zipFile);\r\n\r\n            mOnSuccess.run();\r\n        }\r\n    }\r\n\r\n    public void handleIncomingZip(Intent intent) {\r\n        if (intent == null || !Intent.ACTION_SEND.equals(intent.getAction())) return;\r\n\r\n        Uri zipUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);\r\n        if (zipUri == null) {\r\n            Toast.makeText(mContext, \"No ZIP received\", Toast.LENGTH_SHORT).show();\r\n            return;\r\n        }\r\n\r\n        try {\r\n            File mediaDir = FileHelpers.getExternalMediaDirectory(mContext);\r\n\r\n            // Copy ZIP from URI to temporary file\r\n            File tempZip = new File(mediaDir, \"imported_backup.zip\");\r\n            copyUriToFile(zipUri, tempZip);\r\n\r\n            unpackTempZip(tempZip);\r\n\r\n            BackupSettingsPresenter.instance(mContext).showLocalRestoreDialogApi30();\r\n        } catch (Exception e) {\r\n            e.printStackTrace();\r\n            Toast.makeText(mContext, \"Failed to restore backup\", Toast.LENGTH_SHORT).show();\r\n        }\r\n    }\r\n\r\n    public void unpackTempZip(File tempZip) {\r\n        if (!tempZip.exists()) {\r\n            return;\r\n        }\r\n\r\n        // Target folder: /Android/media/<package>/data\r\n        File mediaDir = FileHelpers.getExternalMediaDirectory(mContext);\r\n        File dataDir = new File(mediaDir, \"data\");\r\n\r\n        // Remove old data\r\n        if (dataDir.exists()) FileHelpers.delete(dataDir);\r\n\r\n        if (ZipHelper2.hasRootDir(tempZip, \"data\")) {\r\n            // Unpack ZIP with data folder\r\n            ZipHelper2.unzip(tempZip, mediaDir);\r\n        } else {\r\n            // Seems we've packed the contents of the data dir not data itself\r\n            ZipHelper2.unzip(tempZip, dataDir);\r\n        }\r\n\r\n        // Delete the temporary ZIP\r\n        tempZip.delete();\r\n    }\r\n\r\n    private void copyUriToDir(Uri uri, File targetDir) {\r\n        try {\r\n            String fileName = getFileName(uri);\r\n            if (fileName == null) fileName = \"imported_\" + System.currentTimeMillis();\r\n\r\n            File outFile = new File(targetDir, fileName);\r\n\r\n            InputStream in = mContext.getContentResolver().openInputStream(uri);\r\n            OutputStream out = new FileOutputStream(outFile);\r\n\r\n            byte[] buffer = new byte[8192];\r\n            int len;\r\n            while ((len = in.read(buffer)) != -1) {\r\n                out.write(buffer, 0, len);\r\n            }\r\n\r\n            in.close();\r\n            out.close();\r\n\r\n        } catch (Exception e) {\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n\r\n    private void copyUriToFile(Uri uri, File outFile) {\r\n        try {\r\n            InputStream in = mContext.getContentResolver().openInputStream(uri);\r\n            OutputStream out = new FileOutputStream(outFile);\r\n\r\n            byte[] buffer = new byte[8192];\r\n            int len;\r\n            while ((len = in.read(buffer)) != -1) {\r\n                out.write(buffer, 0, len);\r\n            }\r\n\r\n            in.close();\r\n            out.close();\r\n\r\n        } catch (Exception e) {\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n\r\n    private String getFileName(Uri uri) {\r\n        Cursor cursor = mContext.getContentResolver().query(uri, null, null, null, null);\r\n        if (cursor != null) {\r\n            int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);\r\n            cursor.moveToFirst();\r\n            String name = cursor.getString(nameIndex);\r\n            cursor.close();\r\n            return name;\r\n        }\r\n        return null;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.content.Context;\r\nimport android.content.pm.PackageManager;\r\nimport android.os.Environment;\r\nimport android.os.Handler;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.AppInfoHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.FileHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.PermissionHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.HiddenPrefs;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.io.File;\r\nimport java.util.ArrayList;\r\nimport java.util.Collection;\r\nimport java.util.List;\r\n\r\nimport io.reactivex.disposables.Disposable;\r\n\r\npublic class BackupAndRestoreManager implements MotherActivity.OnPermissions {\r\n    private static final String TAG = BackupAndRestoreManager.class.getSimpleName();\r\n    private static final String BACKUP_DIR_NAME = \"Backup\";\r\n    private final Context mContext;\r\n    private static final String SHARED_PREFS_SUBDIR = \"shared_prefs\";\r\n    private final File mSharedPrefsDir;\r\n    private final List<File> mBackupDirs;\r\n    private final BackupAndRestoreHelper mHelper;\r\n    private final boolean mForceApi30;\r\n    private Runnable mPendingHandler;\r\n    private Disposable mZipAction;\r\n\r\n    public interface OnBackupNames {\r\n        void onBackupNames(List<String> backupNames);\r\n    }\r\n\r\n    public BackupAndRestoreManager(Context context) {\r\n        this(context, false);\r\n    }\r\n\r\n    public BackupAndRestoreManager(Context context, boolean forceApi30) {\r\n        mContext = context;\r\n        mForceApi30 = forceApi30;\r\n\r\n        mHelper = new BackupAndRestoreHelper(context);\r\n\r\n        mSharedPrefsDir = new File(mContext.getApplicationInfo().dataDir, SHARED_PREFS_SUBDIR);\r\n\r\n        mBackupDirs = new ArrayList<>();\r\n    }\r\n\r\n    private void initBackupDirs() {\r\n        if (!mBackupDirs.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        File externalDir = getExternalStorageDirectory();\r\n        // Main backup dir\r\n        mBackupDirs.add(createBackupDir(new File(externalDir, String.format(\"data/%s\", mContext.getPackageName()))));\r\n\r\n        File dataDir = new File(externalDir, \"data\");\r\n\r\n        if (!dataDir.exists()) {\r\n            dataDir.mkdirs();\r\n        }\r\n\r\n        File[] appDirs = dataDir.listFiles();\r\n\r\n        if (appDirs == null) {\r\n            return;\r\n        }\r\n\r\n        // Fallback dirs: in case multiple app flavors installed\r\n        for (File appDir : appDirs) {\r\n            File backupDir = createBackupDir(appDir);\r\n            if (!mBackupDirs.contains(backupDir)) {\r\n                mBackupDirs.add(backupDir);\r\n            }\r\n        }\r\n    }\r\n\r\n    private File createBackupDir(File appDir) {\r\n        return new File(appDir, BACKUP_DIR_NAME);\r\n    }\r\n\r\n    private File getExternalStorageDirectory() {\r\n        File result;\r\n\r\n        if (hasAccessOnlyToAppFolders()) {\r\n            result = FileHelpers.getExternalMediaDirectory(mContext);\r\n        } else {\r\n            result = Environment.getExternalStorageDirectory();\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    public void checkPermAndRestore() {\r\n        List<String> backupNames = getBackupNames();\r\n\r\n        if (!backupNames.isEmpty()) {\r\n            checkPermAndRestore(backupNames.get(0));\r\n        }\r\n    }\r\n\r\n    public void checkPermAndRestore(String backupName) {\r\n        if (backupName == null) {\r\n            return;\r\n        }\r\n\r\n        if (FileHelpers.isExternalStorageReadable()) {\r\n            if (hasStoragePermissions(mContext)) {\r\n                restoreData(backupName);\r\n            } else {\r\n                mPendingHandler = () -> restoreData(backupName);\r\n                verifyStoragePermissionsAndReturn();\r\n            }\r\n        }\r\n    }\r\n\r\n    public void checkPermAndBackup() {\r\n        if (FileHelpers.isExternalStorageWritable()) {\r\n            if (hasStoragePermissions(mContext)) {\r\n                backupData();\r\n            } else {\r\n                mPendingHandler = this::backupData;\r\n                verifyStoragePermissionsAndReturn();\r\n            }\r\n        }\r\n    }\r\n\r\n    public void backupData() {\r\n        Log.d(TAG, \"App has been updated or installed. Doing data backup...\");\r\n\r\n        File currentBackup = getBackup();\r\n\r\n        if (currentBackup == null) {\r\n            Log.d(TAG, \"Oops. Backup location not writable.\");\r\n            return;\r\n        }\r\n\r\n        if (hasAccessOnlyToAppFolders()) {\r\n            File mediaDir = FileHelpers.getExternalMediaDirectory(mContext);\r\n            File dataDir = new File(mediaDir, \"data\");\r\n            FileHelpers.delete(dataDir);\r\n        } else if (currentBackup.isDirectory()) { // plain sdcard storage\r\n            // remove old backup <app_id>/Backup\r\n            FileHelpers.delete(currentBackup);\r\n        }\r\n\r\n        if (mSharedPrefsDir.isDirectory() && !FileHelpers.isEmpty(mSharedPrefsDir)) {\r\n            File destination = new File(currentBackup, mSharedPrefsDir.getName());\r\n            FileHelpers.copy(mSharedPrefsDir, destination, fileName -> Helpers.endsWithAny(fileName.toString(), Utils.BACKUP_PATTERNS));\r\n\r\n            // Don't store unique id\r\n            FileHelpers.delete(new File(destination, HiddenPrefs.SHARED_PREFERENCES_NAME + \".xml\"));\r\n        }\r\n\r\n        if (hasAccessOnlyToAppFolders()) {\r\n            mHelper.exportAppMediaFolder();\r\n        } else {\r\n            RxHelper.disposeActions(mZipAction);\r\n            mZipAction = RxHelper.runAsync(this::saveDataToZip);\r\n        }\r\n    }\r\n\r\n    private void restoreData(String backupName) {\r\n        Log.d(TAG, \"App just updated. Restoring data...\");\r\n\r\n        File currentBackup = getBackupCheck(backupName);\r\n        File sourceBackupDir = new File(currentBackup, SHARED_PREFS_SUBDIR);\r\n\r\n        if (FileHelpers.isEmpty(sourceBackupDir)) {\r\n            Log.d(TAG, \"Oops. Backup folder is empty.\");\r\n            MessageHelpers.showLongMessage(mContext, \"Oops. Backup folder is empty.\");\r\n            return;\r\n        }\r\n\r\n        if (mSharedPrefsDir.isDirectory()) {\r\n            // remove old data\r\n            FileHelpers.delete(mSharedPrefsDir);\r\n        }\r\n\r\n        FileHelpers.copy(sourceBackupDir, mSharedPrefsDir, fileName -> Helpers.endsWithAny(fileName.toString(), Utils.BACKUP_PATTERNS));\r\n        fixFileNames(mSharedPrefsDir);\r\n\r\n        MessageHelpers.showMessage(mContext, R.string.msg_done);\r\n\r\n        // NOTE: Don't restart the app, just kill. The reboot will broke the files.\r\n        // To apply settings we need to kill the app\r\n        new Handler(mContext.getMainLooper()).postDelayed(() -> Runtime.getRuntime().exit(0), 1_000);\r\n    }\r\n\r\n    /**\r\n     * Fix file names from other app versions\r\n     */\r\n    private void fixFileNames(File dataDir) {\r\n        Collection<File> files = FileHelpers.listFileTree(dataDir);\r\n\r\n        String suffix = \"_preferences.xml\";\r\n        String targetName = mContext.getPackageName() + suffix;\r\n\r\n        for (File file : files) {\r\n            if (file.getName().endsWith(suffix) && !file.getName().endsWith(targetName)) {\r\n                FileHelpers.copy(file, new File(file.getParentFile(), targetName));\r\n                FileHelpers.delete(file);\r\n            }\r\n        }\r\n    }\r\n\r\n    private void verifyStoragePermissionsAndReturn() {\r\n        if (mContext instanceof MotherActivity) {\r\n            ((MotherActivity) mContext).addOnPermissions(this);\r\n\r\n            PermissionHelpers.verifyStoragePermissions(mContext);\r\n        }\r\n    }\r\n\r\n    private File getBackup() {\r\n        File currentBackup = null;\r\n\r\n        for (File backupDir : getBackupDirs()) {\r\n            currentBackup = backupDir;\r\n            break;\r\n        }\r\n\r\n        return currentBackup;\r\n    }\r\n\r\n    private File getBackupCheck(String backupName) {\r\n        File currentBackup = null;\r\n\r\n        for (File backupDir : getBackupDirs()) {\r\n            File parentFile = backupDir.getParentFile(); // backupDir: /data/<app_id>/Backup\r\n\r\n            if (parentFile == null) {\r\n                continue;\r\n            }\r\n\r\n            if (backupDir.exists() && Helpers.equals(parentFile.getName(), backupName)) {\r\n                currentBackup = backupDir;\r\n                break;\r\n            }\r\n        }\r\n\r\n        return currentBackup;\r\n    }\r\n\r\n    private File getBackupCheck() {\r\n        for (File backupDir : getBackupDirs()) {\r\n            if (backupDir.exists()) {\r\n                return backupDir.getParentFile();\r\n            }\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    @Override\r\n    public void onPermissions(int requestCode, String[] permissions, int[] grantResults) {\r\n        if (requestCode == PermissionHelpers.REQUEST_EXTERNAL_STORAGE) {\r\n            if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {\r\n                Log.d(TAG, \"REQUEST_EXTERNAL_STORAGE permission has been granted\");\r\n\r\n                if (mPendingHandler != null) {\r\n                    mPendingHandler.run();\r\n                    mPendingHandler = null;\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    public String getBackupPath() {\r\n        File currentBackup = getBackup();\r\n\r\n        return currentBackup != null ? currentBackup.toString() : null;\r\n    }\r\n\r\n    public String getBackupRootPath() {\r\n        // NOTE: Android 11+ only backup through the file manager (no shared dir)\r\n        return String.format(\"%s/data\", getExternalStorageDirectory());\r\n    }\r\n\r\n    public String getBackupPathCheck() {\r\n        File currentBackup = getBackupCheck();\r\n\r\n        return currentBackup != null ? currentBackup.toString() : null;\r\n    }\r\n\r\n    public void getBackupNames(OnBackupNames callback) {\r\n        if (FileHelpers.isExternalStorageReadable()) {\r\n            if (hasStoragePermissions(mContext)) {\r\n                if (hasAccessOnlyToAppFolders()) {\r\n                    // Try to restore externally copied backup zip (if any)\r\n                    unpackBackupZip();\r\n                }\r\n                callback.onBackupNames(getBackupNames());\r\n            } else {\r\n                mPendingHandler = () -> callback.onBackupNames(getBackupNames());\r\n                verifyStoragePermissionsAndReturn();\r\n            }\r\n        }\r\n    }\r\n\r\n    private List<String> getBackupNames() {\r\n        List<String> names = new ArrayList<>();\r\n\r\n        for (File backupDir : getBackupDirs()) {\r\n            File parentFile = backupDir.getParentFile();\r\n\r\n            if (parentFile == null) {\r\n                continue;\r\n            }\r\n\r\n            if (backupDir.exists()) {\r\n                names.add(parentFile.getName());\r\n            }\r\n        }\r\n\r\n        return names;\r\n    }\r\n\r\n    private List<File> getBackupDirs() {\r\n        initBackupDirs();\r\n\r\n        return mBackupDirs;\r\n    }\r\n\r\n    private boolean hasStoragePermissions(Context context) {\r\n        return hasAccessOnlyToAppFolders() || PermissionHelpers.hasStoragePermissions(context);\r\n    }\r\n\r\n    public boolean hasBackup() {\r\n        return getBackupCheck() != null;\r\n    }\r\n\r\n    // Android 11+: only backup through the file manager (no shared dir)\r\n    private boolean hasAccessOnlyToAppFolders() {\r\n        return AppInfoHelpers.getRealSdkVersion(mContext) > 29 || mForceApi30;\r\n    }\r\n\r\n    private void saveDataToZip() {\r\n        File mediaDir = getExternalStorageDirectory();\r\n        File dataDir = new File(mediaDir, \"data\");\r\n        if (dataDir.exists()) {\r\n            File zipFile = new File(mediaDir,  \"SmartTubeBackup.zip\");\r\n            ZipHelper2.zipDirectory(dataDir, zipFile);\r\n        }\r\n    }\r\n\r\n    private void saveDataToZip(File currentBackup) { // /data/<app_id>/Backup\r\n        if (!FileHelpers.isEmpty(currentBackup)) {\r\n            File source = currentBackup.getParentFile();\r\n            if (source != null) {\r\n                File zipFile = new File(source.getParentFile(), source.getName() + \".zip\");\r\n                ZipHelper2.zipDirectory(source, zipFile);\r\n            }\r\n        }\r\n    }\r\n\r\n    private void unpackBackupZip() {\r\n        File[] files = FileHelpers.getExternalMediaDirectory(mContext).listFiles();\r\n        if (files != null) {\r\n            for (File file : files) {\r\n                if (file.getName().endsWith(\".zip\")) {\r\n                    mHelper.unpackTempZip(file);\r\n                    break;\r\n                }\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupReceiverActivity.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.app.Activity;\r\nimport android.content.Intent;\r\nimport android.content.pm.PackageManager;\r\nimport android.os.Build.VERSION;\r\nimport android.os.Bundle;\r\n\r\nimport androidx.annotation.NonNull;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.AppInfoHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.PermissionHelpers;\r\n\r\npublic class BackupReceiverActivity extends Activity {\r\n    private BackupAndRestoreHelper mRestoreHelper;\r\n    private Runnable mPendingHandler;\r\n\r\n    @Override\r\n    protected void onCreate(Bundle savedInstanceState) {\r\n        super.onCreate(savedInstanceState);\r\n\r\n        mRestoreHelper = new BackupAndRestoreHelper(this);\r\n\r\n        // Android 11+ (API 30+) removed the need for storage permission\r\n        // when accessing files via content:// URIs. Apps are granted temporary\r\n        // access to the URI by the sender, so you can read the file without\r\n        // READ_EXTERNAL_STORAGE.\r\n        if (PermissionHelpers.hasStoragePermissions(this) || AppInfoHelpers.getRealSdkVersion(this) > 29) {\r\n            restoreData();\r\n            finish();\r\n        } else {\r\n            mPendingHandler = this::restoreData;\r\n            PermissionHelpers.verifyStoragePermissions(this);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    protected void onNewIntent(Intent intent) {\r\n        super.onNewIntent(intent);\r\n\r\n        mRestoreHelper.handleIncomingZip(intent);\r\n    }\r\n\r\n    @Override\r\n    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {\r\n        if (requestCode == PermissionHelpers.REQUEST_EXTERNAL_STORAGE) {\r\n            if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {\r\n                if (mPendingHandler != null) {\r\n                    mPendingHandler.run();\r\n                    mPendingHandler = null;\r\n                }\r\n            }\r\n            finish();\r\n        }\r\n    }\r\n\r\n    private void restoreData() {\r\n        mRestoreHelper.handleIncomingZip(getIntent());\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BrowseProcessor.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\n\r\npublic interface BrowseProcessor {\r\n    interface OnItemReady {\r\n        void onItemReady(Video video);\r\n    }\r\n    void process(VideoGroup videoGroup);\r\n    void dispose();\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BrowseProcessorManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\n\r\nimport java.util.ArrayList;\r\n\r\npublic class BrowseProcessorManager implements BrowseProcessor {\r\n    private final ArrayList<BrowseProcessor> mProcessors;\r\n\r\n    public BrowseProcessorManager(Context context, OnItemReady onItemReady) {\r\n        mProcessors = new ArrayList<>();\r\n        mProcessors.add(new DeArrowProcessor(context, onItemReady));\r\n        mProcessors.add(new UnlocalizedTitleProcessor(context, onItemReady));\r\n    }\r\n\r\n    @Override\r\n    public void process(VideoGroup videoGroup) {\r\n        for (BrowseProcessor processor : mProcessors) {\r\n            processor.process(videoGroup);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void dispose() {\r\n        for (BrowseProcessor processor : mProcessors) {\r\n            processor.dispose();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/CrashRestorer.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.content.Context;\r\nimport android.os.Bundle;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService.State;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ViewManager;\r\n\r\npublic class CrashRestorer {\r\n    private static final String SELECTED_HEADER_INDEX = \"SelectedHeaderIndex\";\r\n    private static final String SELECTED_VIDEO = \"SelectedVideo\";\r\n    private static final String IS_PLAYER_IN_FOREGROUND = \"IsPlayerInForeground\";\r\n    private int mSelectedHeaderIndex = -1;\r\n    private Video mSelectedVideo;\r\n    private boolean mIsPlayerInForeground;\r\n    private final Context mContext;\r\n\r\n    public interface OnRestoreHeader {\r\n        void onRestore(int selectedHeaderIndex, Video selectedVideo);\r\n    }\r\n\r\n    public CrashRestorer(Context context, Bundle savedState) {\r\n        mContext = context.getApplicationContext();\r\n        init(savedState);\r\n    }\r\n\r\n    private void init(Bundle savedState) {\r\n        if (savedState == null) {\r\n            return;\r\n        }\r\n\r\n        mSelectedHeaderIndex = savedState.getInt(SELECTED_HEADER_INDEX, -1);\r\n        mSelectedVideo = Video.fromString(savedState.getString(SELECTED_VIDEO));\r\n        mIsPlayerInForeground = savedState.getBoolean(IS_PLAYER_IN_FOREGROUND, false);\r\n    }\r\n\r\n    public void persistHeaderIndex(Bundle outState, int selectedPosition) {\r\n        if (selectedPosition == -1) { // multiple crashes without user interaction\r\n            selectedPosition = mSelectedHeaderIndex;\r\n        }\r\n\r\n        outState.putInt(SELECTED_HEADER_INDEX, selectedPosition);\r\n    }\r\n\r\n    public void persistVideo(Bundle outState, Video currentVideo) {\r\n        if (currentVideo == null) { // multiple crashes without user interaction\r\n            currentVideo = mSelectedVideo;\r\n        }\r\n\r\n        if (currentVideo != null) {\r\n            outState.putString(SELECTED_VIDEO, currentVideo.toString());\r\n        }\r\n        outState.putBoolean(IS_PLAYER_IN_FOREGROUND, ViewManager.instance(mContext).isPlayerInForeground());\r\n    }\r\n\r\n    public void restorePlayback() {\r\n        if (mIsPlayerInForeground && PlaybackPresenter.instance(mContext).getPlayer() == null) {\r\n            VideoStateService stateService = VideoStateService.instance(mContext);\r\n            boolean isVideoStateSynced = mSelectedVideo == null || stateService.getByVideoId(mSelectedVideo.videoId) != null;\r\n            State lastState = stateService.getLastState();\r\n            PlaybackPresenter.instance(mContext).openVideo(lastState != null && isVideoStateSynced ? lastState.video : mSelectedVideo);\r\n        }\r\n\r\n        // Restore can be called only once\r\n        mIsPlayerInForeground = false;\r\n    }\r\n\r\n    public void restoreHeader(OnRestoreHeader onRestoreHeader) {\r\n        if (mSelectedHeaderIndex != -1) {\r\n            onRestoreHeader.onRestore(mSelectedHeaderIndex, mSelectedVideo);\r\n        }\r\n\r\n        // Restore can be called only once\r\n        mSelectedHeaderIndex = -1;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/DeArrowProcessor.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.DeArrowData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.common.DataChangeBase.OnDataChange;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\nimport io.reactivex.disposables.Disposable;\r\n\r\npublic class DeArrowProcessor implements OnDataChange, BrowseProcessor {\r\n    private static final String TAG = DeArrowProcessor.class.getSimpleName();\r\n    private final OnItemReady mOnItemReady;\r\n    private final MediaItemService mItemService;\r\n    private final DeArrowData mDeArrowData;\r\n    private boolean mIsReplaceTitlesEnabled;\r\n    private boolean mIsReplaceThumbnailsEnabled;\r\n    private Disposable mResult;\r\n\r\n    public DeArrowProcessor(Context context, OnItemReady onItemReady) {\r\n        mOnItemReady = onItemReady;\r\n        ServiceManager service = YouTubeServiceManager.instance();\r\n        mItemService = service.getMediaItemService();\r\n        mDeArrowData = DeArrowData.instance(context);\r\n        mDeArrowData.setOnChange(this);\r\n        initData();\r\n    }\r\n\r\n    @Override\r\n    public void onDataChange() {\r\n        initData();\r\n    }\r\n\r\n    private void initData() {\r\n        mIsReplaceTitlesEnabled = mDeArrowData.isReplaceTitlesEnabled();\r\n        mIsReplaceThumbnailsEnabled = mDeArrowData.isReplaceThumbnailsEnabled();\r\n    }\r\n\r\n    @Override\r\n    public void process(VideoGroup videoGroup) {\r\n        if ((!mIsReplaceTitlesEnabled && !mIsReplaceThumbnailsEnabled) || videoGroup == null || videoGroup.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        List<String> videoIds = getVideoIds(videoGroup);\r\n        mResult = mItemService.getDeArrowDataObserve(videoIds)\r\n                .subscribe(deArrowData -> {\r\n                    Video video = videoGroup.findVideoById(deArrowData.getVideoId());\r\n                    if (mIsReplaceTitlesEnabled) {\r\n                        video.deArrowTitle = deArrowData.getTitle();\r\n                    }\r\n                    if (mIsReplaceThumbnailsEnabled) {\r\n                        video.altCardImageUrl = deArrowData.getThumbnailUrl();\r\n                    }\r\n                    mOnItemReady.onItemReady(video);\r\n                },\r\n                error -> {\r\n                    Log.d(TAG, \"DeArrow cannot process the video\");\r\n                });\r\n    }\r\n\r\n    @Override\r\n    public void dispose() {\r\n        RxHelper.disposeActions(mResult);\r\n    }\r\n\r\n    private List<String> getVideoIds(VideoGroup videoGroup) {\r\n        List<String> result = new ArrayList<>();\r\n\r\n        for (Video video : videoGroup.getVideos()) {\r\n            if (video.deArrowProcessed) {\r\n                continue;\r\n            }\r\n            video.deArrowProcessed = true;\r\n            result.add(video.videoId);\r\n        }\r\n\r\n        return result;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/GDriveBackupManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport android.net.Uri;\r\nimport android.os.Build;\r\nimport android.os.Handler;\r\n\r\nimport com.liskovsoft.googleapi.service.DriveService;\r\nimport com.liskovsoft.googleapi.oauth2.impl.GoogleSignInService;\r\nimport com.liskovsoft.sharedutils.helpers.FileHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.GoogleSignInPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.io.File;\r\nimport java.util.ArrayList;\r\nimport java.util.Collection;\r\nimport java.util.List;\r\n\r\nimport io.reactivex.Observable;\r\nimport io.reactivex.android.schedulers.AndroidSchedulers;\r\nimport io.reactivex.disposables.Disposable;\r\nimport io.reactivex.functions.Consumer;\r\nimport io.reactivex.schedulers.Schedulers;\r\n\r\npublic class GDriveBackupManager {\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static GDriveBackupManager sInstance;\r\n    private final Context mContext;\r\n    private static final String SHARED_PREFS_SUBDIR = \"shared_prefs\";\r\n    private static final String BACKUP_NAME = \"backup.zip\";\r\n    private final GoogleSignInService mSignInService;\r\n    private final String mDataDir;\r\n    private final String mBackupDir;\r\n    private final String mRootBackupDir;\r\n    private final GeneralData mGeneralData;\r\n    private Disposable mBackupAction;\r\n    private Disposable mRestoreAction;\r\n    private boolean mIsBlocking;\r\n\r\n    private GDriveBackupManager(Context context) {\r\n        mContext = context;\r\n        mGeneralData = GeneralData.instance(context);\r\n        mDataDir = String.format(\"%s/%s\", mContext.getApplicationInfo().dataDir, SHARED_PREFS_SUBDIR);\r\n        mBackupDir = String.format(\"SmartTubeBackup/%s\", context.getPackageName());\r\n        mRootBackupDir = \"SmartTubeBackup\";\r\n        mSignInService = GoogleSignInService.instance();\r\n    }\r\n\r\n    public static GDriveBackupManager instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new GDriveBackupManager(context);\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public static void unhold() {\r\n        sInstance = null;\r\n    }\r\n\r\n    public void backup() {\r\n        mIsBlocking = false;\r\n        backupInt();\r\n    }\r\n\r\n    public void backupBlocking() {\r\n        mIsBlocking = true;\r\n        backupInt();\r\n    }\r\n\r\n    private void backupInt() {\r\n        if (mIsBlocking && !mSignInService.isSigned()) {\r\n            return;\r\n        }\r\n\r\n        if (RxHelper.isAnyActionRunning(mBackupAction, mRestoreAction)) {\r\n            if (!mIsBlocking)\r\n                MessageHelpers.showMessage(mContext, R.string.wait_data_loading);\r\n            return;\r\n        }\r\n\r\n        if (mSignInService.isSigned()) {\r\n            startBackupConfirm();\r\n        } else {\r\n            logIn(this::startBackupConfirm);\r\n        }\r\n    }\r\n\r\n    public void restore() {\r\n        if (RxHelper.isAnyActionRunning(mBackupAction, mRestoreAction)) {\r\n            MessageHelpers.showMessage(mContext, R.string.wait_data_loading);\r\n            return;\r\n        }\r\n\r\n        if (mSignInService.isSigned()) {\r\n            startRestoreConfirm();\r\n        } else {\r\n            logIn(this::startRestoreConfirm);\r\n        }\r\n    }\r\n\r\n    private void startBackupConfirm() {\r\n        if (!mIsBlocking) {\r\n            AppDialogUtil.showConfirmationDialog(mContext, mContext.getString(R.string.app_backup), this::startBackupWrapper);\r\n        } else {\r\n            startBackupWrapper();\r\n        }\r\n    }\r\n\r\n    private void startBackupWrapper() {\r\n        String backupDir = getBackupDir();\r\n        startBackup(backupDir, mDataDir);\r\n    }\r\n\r\n    private void startBackupOld(String backupDir, String dataDir) {\r\n        Collection<File> files = FileHelpers.listFileTree(new File(dataDir));\r\n\r\n        Consumer<File> backupConsumer = file -> {\r\n            if (file.isFile()) {\r\n                if (checkFileName(file.getName())) {\r\n                    if (!mIsBlocking) MessageHelpers.showLongMessage(mContext, mContext.getString(R.string.app_backup) + \"\\n\" + file.getName());\r\n\r\n                    RxHelper.runBlocking(DriveService.uploadFile(file, Uri.parse(String.format(\"%s%s\", backupDir, file.getAbsolutePath().replace(dataDir, \"\")))));\r\n                }\r\n            }\r\n        };\r\n\r\n        if (mIsBlocking) {\r\n            Observable.fromIterable(files)\r\n                    .blockingSubscribe(backupConsumer);\r\n        } else {\r\n            mBackupAction = Observable.fromIterable(files)\r\n                    .subscribeOn(Schedulers.io())\r\n                    .observeOn(Schedulers.io()) // run subscribe on separate thread\r\n                    .subscribe(backupConsumer, error -> MessageHelpers.showLongMessage(mContext, error.getMessage()));\r\n        }\r\n    }\r\n\r\n    private void startBackup(String backupDir, String dataDir) {\r\n        File source = new File(dataDir);\r\n        File zipFile = new File(mContext.getCacheDir(), BACKUP_NAME);\r\n        ZipHelper.zipFolder(source, zipFile, Utils.BACKUP_PATTERNS);\r\n\r\n        Observable<Void> uploadFile = DriveService.uploadFile(zipFile, Uri.parse(String.format(\"%s/%s\", backupDir, BACKUP_NAME)));\r\n\r\n        if (mIsBlocking) {\r\n            RxHelper.runBlocking(uploadFile);\r\n        } else {\r\n            MessageHelpers.showLongMessage(mContext, mContext.getString(R.string.app_backup));\r\n            mBackupAction = uploadFile\r\n                    .subscribeOn(Schedulers.io())\r\n                    .observeOn(AndroidSchedulers.mainThread())\r\n                    .subscribe(\r\n                            unused -> {},\r\n                            error -> {\r\n                                MessageHelpers.showLongMessage(mContext, error.getMessage());\r\n                                if (Helpers.startsWith(error.getMessage(), \"AuthError\")) {\r\n                                    logIn(this::startBackupConfirm); // auth data outdated (AuthError: invalid_grant)\r\n                                }\r\n                            },\r\n                            () -> MessageHelpers.showMessage(mContext, R.string.msg_done)\r\n                    );\r\n        }\r\n    }\r\n\r\n    private void startRestoreConfirm() {\r\n        //AppDialogUtil.showConfirmationDialog(mContext, mContext.getString(R.string.app_restore), this::startRestoreWrapper);\r\n        showRestoreChooserDialog();\r\n    }\r\n\r\n    private void startRestoreWrapper() {\r\n        startRestore(getBackupDir(), mDataDir,\r\n                () -> startRestore(getAltBackupDir(), mDataDir,\r\n                        () -> startRestoreOld(getBackupDir(), mDataDir,\r\n                                () -> startRestoreOld(getAltBackupDir(), mDataDir, null))));\r\n    }\r\n\r\n    private void startRestoreOld(String backupDir, String dataDir, Runnable onError) {\r\n        mRestoreAction = DriveService.getFileList(Uri.parse(backupDir))\r\n                .subscribeOn(Schedulers.io())\r\n                .observeOn(Schedulers.io()) // run subscribe on separate thread\r\n                .subscribe(names -> {\r\n                    // remove old data\r\n                    FileHelpers.delete(dataDir);\r\n\r\n                    for (String name : names) {\r\n                        if (checkFileName(name)) {\r\n                            MessageHelpers.showLongMessage(mContext, mContext.getString(R.string.app_restore) + \"\\n\" + name);\r\n\r\n                            DriveService.getFile(Uri.parse(String.format(\"%s/%s\", backupDir, name)))\r\n                                    .blockingSubscribe(inputStream -> FileHelpers.copy(inputStream, new File(dataDir, fixAltPackageName(name))));\r\n                        }\r\n                    }\r\n\r\n                    // NOTE: Don't restart the app, just kill. The reboot will broke the files.\r\n                    // To apply settings we need to kill the app\r\n                    new Handler(mContext.getMainLooper()).postDelayed(() -> Runtime.getRuntime().exit(0), 1_000);\r\n                }, error -> {\r\n                    if (onError != null)\r\n                        onError.run();\r\n                    else MessageHelpers.showLongMessage(mContext, error.getMessage());\r\n                });\r\n    }\r\n\r\n    private void startRestore(String backupDir, String dataDir, Runnable onError) {\r\n        MessageHelpers.showLongMessage(mContext, mContext.getString(R.string.app_restore));\r\n        mRestoreAction = DriveService.getFile(Uri.parse(String.format(\"%s/%s\", backupDir, BACKUP_NAME)))\r\n                .subscribeOn(Schedulers.io())\r\n                .observeOn(AndroidSchedulers.mainThread())\r\n                .subscribe(inputStream -> {\r\n                    File zipFile = new File(mContext.getCacheDir(), BACKUP_NAME);\r\n                    FileHelpers.copy(inputStream, zipFile);\r\n\r\n                    File out = new File(dataDir);\r\n                    // remove old data\r\n                    FileHelpers.delete(out);\r\n                    ZipHelper.unzipToFolder(zipFile, out);\r\n                    fixFileNames(out);\r\n\r\n                    // NOTE: Don't restart the app, just kill. The reboot will broke the files.\r\n                    // To apply settings we need to kill the app\r\n                    new Handler(mContext.getMainLooper()).postDelayed(() -> Runtime.getRuntime().exit(0), 1_000);\r\n                }, error -> {\r\n                    if (onError != null)\r\n                        onError.run();\r\n                    else MessageHelpers.showLongMessage(mContext, error.getMessage());\r\n                }, () -> MessageHelpers.showMessage(mContext, R.string.msg_done));\r\n    }\r\n\r\n    private void logIn(Runnable onDone) {\r\n        GoogleSignInPresenter.instance(mContext).start(onDone);\r\n    }\r\n\r\n    private boolean checkFileName(String name) {\r\n        return Helpers.endsWithAny(name, Utils.BACKUP_PATTERNS);\r\n    }\r\n\r\n    private String fixAltPackageName(String name) {\r\n        String altPackageName = getAltPackageName();\r\n        return name.replace(altPackageName, mContext.getPackageName());\r\n    }\r\n\r\n    private String getAltPackageName() {\r\n        String[] altPackages = Utils.KNOWN_PACKAGES;\r\n        // TODO: don't hard code ids. show all existed.\r\n        return mContext.getPackageName().equals(altPackages[0]) ? altPackages[1] : altPackages[0];\r\n    }\r\n\r\n    private String getDeviceSuffix() {\r\n        return mGeneralData.isDeviceSpecificBackupEnabled() ? \"_\" + Build.MODEL.replace(\" \", \"_\") : \"\";\r\n    }\r\n\r\n    private String getAltBackupDir() {\r\n        String backupDir = getBackupDir();\r\n        String altPackageName = getAltPackageName();\r\n        return backupDir.replace(mContext.getPackageName(), altPackageName);\r\n    }\r\n\r\n    public String getBackupDir() {\r\n        return mBackupDir + getDeviceSuffix();\r\n    }\r\n\r\n    /**\r\n     * Fix file names from other app versions\r\n     */\r\n    private void fixFileNames(File dataDir) {\r\n        Collection<File> files = FileHelpers.listFileTree(dataDir);\r\n\r\n        String suffix = \"_preferences.xml\";\r\n        String targetName = mContext.getPackageName() + suffix;\r\n\r\n        for (File file : files) {\r\n            if (file.getName().endsWith(suffix) && !file.getName().endsWith(targetName)) {\r\n                FileHelpers.copy(file, new File(file.getParentFile(), targetName));\r\n                FileHelpers.delete(file);\r\n            }\r\n        }\r\n    }\r\n\r\n    private void showRestoreChooserDialog() {\r\n        mRestoreAction = DriveService.getFolderList(Uri.parse(mRootBackupDir))\r\n                .subscribeOn(Schedulers.io())\r\n                .observeOn(AndroidSchedulers.mainThread()) // run subscribe on separate thread\r\n                .subscribe(\r\n                        this::showLocalRestoreDialog,\r\n                        error -> {\r\n                            MessageHelpers.showLongMessage(mContext, error.getMessage());\r\n                            if (Helpers.startsWith(error.getMessage(), \"AuthError\")) {\r\n                                logIn(this::startRestoreConfirm); // auth data outdated (AuthError: invalid_grant)\r\n                            }\r\n                        }\r\n                );\r\n    }\r\n\r\n    private void showLocalRestoreDialog(List<String> backups) {\r\n        if (backups != null && !backups.isEmpty()) {\r\n            showLocalRestoreSelectorDialog(backups);\r\n        } else {\r\n            MessageHelpers.showLongMessage(mContext, R.string.nothing_found);\r\n        }\r\n    }\r\n\r\n    private void showLocalRestoreSelectorDialog(List<String> backups) {\r\n        AppDialogPresenter dialog = AppDialogPresenter.instance(mContext);\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (String name : backups) {\r\n            options.add(UiOptionItem.from(name, optionItem -> {\r\n                AppDialogUtil.showConfirmationDialog(mContext, mContext.getString(R.string.app_restore), () -> {\r\n                    String backupDir = String.format(\"%s/%s\", mRootBackupDir, name);\r\n                    startRestore(backupDir, mDataDir, () -> startRestoreOld(backupDir, mDataDir, null));\r\n                });\r\n            }));\r\n        }\r\n\r\n        dialog.appendStringsCategory(mContext.getString(R.string.app_restore), options);\r\n        dialog.showDialog();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/GDriveBackupManagerOld.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport android.net.Uri;\r\nimport android.os.Build;\r\n\r\nimport com.liskovsoft.googleapi.oauth2.impl.GoogleSignInService;\r\nimport com.liskovsoft.googleapi.service.DriveService;\r\nimport com.liskovsoft.sharedutils.helpers.FileHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.GoogleSignInPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.io.File;\r\nimport java.util.Collection;\r\n\r\nimport io.reactivex.Observable;\r\nimport io.reactivex.android.schedulers.AndroidSchedulers;\r\nimport io.reactivex.disposables.Disposable;\r\nimport io.reactivex.functions.Consumer;\r\nimport io.reactivex.schedulers.Schedulers;\r\n\r\npublic class GDriveBackupManagerOld {\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static GDriveBackupManagerOld sInstance;\r\n    private final Context mContext;\r\n    private static final String SHARED_PREFS_SUBDIR = \"shared_prefs\";\r\n    private static final String BACKUP_NAME = \"backup.zip\";\r\n    private final GoogleSignInService mSignInService;\r\n    private final String mDataDir;\r\n    private final String mBackupDir;\r\n    private final GeneralData mGeneralData;\r\n    private Disposable mBackupAction;\r\n    private Disposable mRestoreAction;\r\n    private final String[] mBackupNames;\r\n    private boolean mIsBlocking;\r\n\r\n    private GDriveBackupManagerOld(Context context) {\r\n        mContext = context;\r\n        mGeneralData = GeneralData.instance(context);\r\n        mDataDir = String.format(\"%s/%s\", mContext.getApplicationInfo().dataDir, SHARED_PREFS_SUBDIR);\r\n        mBackupDir = String.format(\"SmartTubeBackup/%s\", context.getPackageName());\r\n        mSignInService = GoogleSignInService.instance();\r\n        mBackupNames = new String[] {\r\n                \"yt_service_prefs.xml\",\r\n                \"com.liskovsoft.appupdatechecker2.preferences.xml\",\r\n                \"com.liskovsoft.sharedutils.prefs.GlobalPreferences.xml\",\r\n                \"_preferences.xml\" // before _ should be the app package name\r\n        };\r\n    }\r\n\r\n    public static GDriveBackupManagerOld instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new GDriveBackupManagerOld(context);\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public static void unhold() {\r\n        sInstance = null;\r\n    }\r\n\r\n    public void backup() {\r\n        mIsBlocking = false;\r\n        backupInt();\r\n    }\r\n\r\n    public void backupBlocking() {\r\n        mIsBlocking = true;\r\n        backupInt();\r\n    }\r\n\r\n    private void backupInt() {\r\n        if (mIsBlocking && !mSignInService.isSigned()) {\r\n            return;\r\n        }\r\n\r\n        if (RxHelper.isAnyActionRunning(mBackupAction, mRestoreAction)) {\r\n            if (!mIsBlocking)\r\n                MessageHelpers.showMessage(mContext, R.string.wait_data_loading);\r\n            return;\r\n        }\r\n\r\n        if (mSignInService.isSigned()) {\r\n            startBackupConfirm();\r\n        } else {\r\n            logIn(this::startBackupConfirm);\r\n        }\r\n    }\r\n\r\n    public void restore() {\r\n        if (RxHelper.isAnyActionRunning(mBackupAction, mRestoreAction)) {\r\n            MessageHelpers.showMessage(mContext, R.string.wait_data_loading);\r\n            return;\r\n        }\r\n\r\n        if (mSignInService.isSigned()) {\r\n            startRestoreConfirm();\r\n        } else {\r\n            logIn(this::startRestoreConfirm);\r\n        }\r\n    }\r\n\r\n    private void startBackupConfirm() {\r\n        if (!mIsBlocking) {\r\n            AppDialogUtil.showConfirmationDialog(mContext, mContext.getString(R.string.app_backup), this::startBackup);\r\n        } else {\r\n            startBackup();\r\n        }\r\n    }\r\n\r\n    private void startBackup() {\r\n        String backupDir = getBackupDir();\r\n        startBackup2(backupDir, mDataDir);\r\n    }\r\n\r\n    private void startBackup(String backupDir, String dataDir) {\r\n        Collection<File> files = FileHelpers.listFileTree(new File(dataDir));\r\n\r\n        Consumer<File> backupConsumer = file -> {\r\n            if (file.isFile()) {\r\n                if (checkFileName(file.getName())) {\r\n                    if (!mIsBlocking) MessageHelpers.showLongMessage(mContext, mContext.getString(R.string.app_backup) + \"\\n\" + file.getName());\r\n\r\n                    RxHelper.runBlocking(DriveService.uploadFile(file, Uri.parse(String.format(\"%s%s\", backupDir, file.getAbsolutePath().replace(dataDir, \"\")))));\r\n                }\r\n            }\r\n        };\r\n\r\n        if (mIsBlocking) {\r\n            Observable.fromIterable(files)\r\n                    .blockingSubscribe(backupConsumer);\r\n        } else {\r\n            mBackupAction = Observable.fromIterable(files)\r\n                    .subscribeOn(Schedulers.io())\r\n                    .observeOn(Schedulers.io()) // run subscribe on separate thread\r\n                    .subscribe(backupConsumer, error -> MessageHelpers.showLongMessage(mContext, error.getMessage()));\r\n        }\r\n    }\r\n\r\n    private void startBackup2(String backupDir, String dataDir) {\r\n        File source = new File(dataDir);\r\n        File zipFile = new File(mContext.getCacheDir(), BACKUP_NAME);\r\n        ZipHelper.zipFolder(source, zipFile, mBackupNames);\r\n\r\n        Observable<Void> uploadFile = DriveService.uploadFile(zipFile, Uri.parse(String.format(\"%s/%s\", backupDir, BACKUP_NAME)));\r\n\r\n        if (mIsBlocking) {\r\n            RxHelper.runBlocking(uploadFile);\r\n        } else {\r\n            MessageHelpers.showLongMessage(mContext, mContext.getString(R.string.app_backup));\r\n            mBackupAction = uploadFile\r\n                    .subscribeOn(Schedulers.io())\r\n                    .observeOn(AndroidSchedulers.mainThread())\r\n                    .subscribe(\r\n                            unused -> {},\r\n                            error -> MessageHelpers.showLongMessage(mContext, error.getMessage()),\r\n                            () -> MessageHelpers.showMessage(mContext, R.string.msg_done)\r\n                    );\r\n        }\r\n    }\r\n\r\n    private void startRestoreConfirm() {\r\n        AppDialogUtil.showConfirmationDialog(mContext, mContext.getString(R.string.app_restore), this::startRestore);\r\n    }\r\n\r\n    private void startRestore() {\r\n        startRestore2(getBackupDir(), mDataDir,\r\n                () -> startRestore2(getAltBackupDir(), mDataDir,\r\n                        () -> startRestore(getBackupDir(), mDataDir,\r\n                                () -> startRestore(getAltBackupDir(), mDataDir, null))));\r\n    }\r\n\r\n    private void startRestore(String backupDir, String dataDir, Runnable onError) {\r\n        mRestoreAction = DriveService.getFileList(Uri.parse(backupDir))\r\n                .subscribeOn(Schedulers.io())\r\n                .observeOn(Schedulers.io()) // run subscribe on separate thread\r\n                .subscribe(names -> {\r\n                    // remove old data\r\n                    FileHelpers.delete(dataDir);\r\n\r\n                    for (String name : names) {\r\n                        if (checkFileName(name)) {\r\n                            MessageHelpers.showLongMessage(mContext, mContext.getString(R.string.app_restore) + \"\\n\" + name);\r\n\r\n                            DriveService.getFile(Uri.parse(String.format(\"%s/%s\", backupDir, name)))\r\n                                    .blockingSubscribe(inputStream -> FileHelpers.copy(inputStream, new File(dataDir, fixAltPackageName(name))));\r\n                        }\r\n                    }\r\n                    \r\n                    Utils.restartTheApp(mContext);\r\n                }, error -> {\r\n                    if (onError != null)\r\n                        onError.run();\r\n                    else MessageHelpers.showLongMessage(mContext, R.string.nothing_found);\r\n                });\r\n    }\r\n\r\n    private void startRestore2(String backupDir, String dataDir, Runnable onError) {\r\n        MessageHelpers.showLongMessage(mContext, mContext.getString(R.string.app_restore));\r\n        mRestoreAction = DriveService.getFile(Uri.parse(String.format(\"%s/%s\", backupDir, BACKUP_NAME)))\r\n                .subscribeOn(Schedulers.io())\r\n                .observeOn(AndroidSchedulers.mainThread())\r\n                .subscribe(inputStream -> {\r\n                    File zipFile = new File(mContext.getCacheDir(), BACKUP_NAME);\r\n                    FileHelpers.copy(inputStream, zipFile);\r\n\r\n                    File out = new File(dataDir);\r\n                    // remove old data\r\n                    FileHelpers.delete(out);\r\n                    ZipHelper.unzipToFolder(zipFile, out);\r\n                    fixFileNames(out);\r\n\r\n                    Utils.restartTheApp(mContext);\r\n                }, error -> {\r\n                    if (onError != null)\r\n                        onError.run();\r\n                    else MessageHelpers.showLongMessage(mContext, R.string.nothing_found);\r\n                }, () -> MessageHelpers.showMessage(mContext, R.string.msg_done));\r\n    }\r\n\r\n    private void logIn(Runnable onDone) {\r\n        GoogleSignInPresenter.instance(mContext).start(onDone);\r\n    }\r\n\r\n    private boolean checkFileName(String name) {\r\n        return Helpers.endsWithAny(name, mBackupNames);\r\n    }\r\n\r\n    private String fixAltPackageName(String name) {\r\n        String altPackageName = getAltPackageName();\r\n        return name.replace(altPackageName, mContext.getPackageName());\r\n    }\r\n\r\n    private String getAltPackageName() {\r\n        String[] altPackages = new String[] {\r\n                \"org.smarttube.beta\",\r\n                \"org.smarttube.stable\",\r\n                \"org.smarttube.fdroid\",\r\n                \"com.liskovsoft.smarttubetv.beta\",\r\n                \"com.teamsmart.videomanager.tv\"\r\n        };\r\n        return mContext.getPackageName().equals(altPackages[0]) ? altPackages[1] : altPackages[0];\r\n    }\r\n\r\n    private String getDeviceSuffix() {\r\n        return mGeneralData.isDeviceSpecificBackupEnabled() ? \"_\" + Build.MODEL.replace(\" \", \"_\") : \"\";\r\n    }\r\n\r\n    private String getAltBackupDir() {\r\n        String backupDir = getBackupDir();\r\n        String altPackageName = getAltPackageName();\r\n        return backupDir.replace(mContext.getPackageName(), altPackageName);\r\n    }\r\n\r\n    public String getBackupDir() {\r\n        return mBackupDir + getDeviceSuffix();\r\n    }\r\n\r\n    /**\r\n     * Fix file names from other app versions\r\n     */\r\n    private void fixFileNames(File dataDir) {\r\n        Collection<File> files = FileHelpers.listFileTree(dataDir);\r\n\r\n        String suffix = \"_preferences.xml\";\r\n        String targetName = mContext.getPackageName() + suffix;\r\n\r\n        for (File file : files) {\r\n            if (file.getName().endsWith(suffix) && !file.getName().endsWith(targetName)) {\r\n                FileHelpers.copy(file, new File(file.getParentFile(), targetName));\r\n                FileHelpers.delete(file);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/GDriveBackupWorker.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Build.VERSION;\n\nimport androidx.annotation.NonNull;\nimport androidx.work.ExistingPeriodicWorkPolicy;\nimport androidx.work.PeriodicWorkRequest;\nimport androidx.work.WorkManager;\nimport androidx.work.Worker;\nimport androidx.work.WorkerParameters;\n\nimport com.liskovsoft.googleapi.service.DriveService;\nimport com.liskovsoft.sharedutils.helpers.Helpers;\nimport com.liskovsoft.sharedutils.mylogger.Log;\nimport com.liskovsoft.sharedutils.rx.RxHelper;\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\n\nimport java.util.concurrent.TimeUnit;\n\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\n/**\n * Work to synchronize the TV provider database with the desired list of channels and\n * programs. This sample app runs this once at install time to publish an initial set of channels\n * and programs, however in a real-world setting this might be run at other times to synchronize\n * a server's database with the TV provider database.\n * This code will ensure that the channels from \"SampleClipApi.getDesiredPublishedChannelSet()\"\n * appear in the TV provider database, and that these and all other programs are synchronized with\n * TV provider database.\n */\npublic class GDriveBackupWorker extends Worker {\n    private static final String TAG = GDriveBackupWorker.class.getSimpleName();\n    private static final String WORK_NAME = TAG;\n    private static final String BLOCKED_FILE_NAME = \"blocked\";\n    private static Disposable sAction;\n    private final GDriveBackupManager mTask;\n\n    public GDriveBackupWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {\n        super(context, workerParams);\n\n        mTask = GDriveBackupManager.instance(context);\n    }\n\n    public static void schedule(Context context) {\n        if (VERSION.SDK_INT >= 23 && GeneralData.instance(context).getGDriveBackupFreqDays() > 0) {\n            WorkManager workManager = WorkManager.getInstance(context);\n\n            // https://stackoverflow.com/questions/50943056/avoiding-duplicating-periodicworkrequest-from-workmanager\n            workManager.enqueueUniquePeriodicWork(\n                    WORK_NAME,\n                    ExistingPeriodicWorkPolicy.UPDATE, // fix duplicates (when old worker is running)\n                    new PeriodicWorkRequest.Builder(\n                            GDriveBackupWorker.class,\n                            GeneralData.instance(context).getGDriveBackupFreqDays(),\n                            TimeUnit.DAYS).addTag(WORK_NAME)\n                            .build()\n            );\n        }\n    }\n\n    public static void forceSchedule(Context context) {\n        RxHelper.disposeActions(sAction);\n\n        // get local id\n        String id = Utils.getUniqueId(context);\n\n        // get backup path\n        String backupDir = GDriveBackupManager.instance(context).getBackupDir();\n\n        // then persist id to gdrive\n        sAction = DriveService.uploadFile(id, Uri.parse(String.format(\"%s/%s\", backupDir, BLOCKED_FILE_NAME)))\n                .subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(unused -> {\n                    // NOP\n                }, throwable -> {\n                    // NOP\n                }, () -> {\n                    // then run schedule\n                    schedule(context);\n                });\n    }\n\n    public static void cancel(Context context) {\n        RxHelper.disposeActions(sAction);\n\n        if (VERSION.SDK_INT >= 23 && GeneralData.instance(context).getGDriveBackupFreqDays() > 0) {\n            Log.d(TAG, \"Unregistering worker job...\");\n\n            WorkManager workManager = WorkManager.getInstance(context);\n            workManager.cancelUniqueWork(WORK_NAME);\n        }\n    }\n\n    @NonNull\n    @Override\n    public Result doWork() {\n        Log.d(TAG, \"Starting worker %s...\", this);\n\n        checkedRunBackup();\n\n        return Result.success();\n    }\n\n    private void runBackup() {\n        mTask.backupBlocking();\n        GDriveBackupManager.unhold();\n    }\n\n    private void checkedRunBackup() {\n        // get local id\n        String id = Utils.getUniqueId(getApplicationContext());\n\n        // get backup path\n        String backupDir = GDriveBackupManager.instance(getApplicationContext()).getBackupDir();\n\n        // get id form gdrive\n        DriveService.getFile(Uri.parse(String.format(\"%s/%s\", backupDir, BLOCKED_FILE_NAME)))\n                .blockingSubscribe(inputStream -> {\n                    // if id match run work as usual\n                    String actualId = Helpers.toString(inputStream);\n                    if (Helpers.equals(id, actualId)) {\n                        runBackup();\n                    } else {\n                        // if id not found then disable auto backup in settings\n                        GeneralData.instance(getApplicationContext()).setGDriveBackupFreqDays(-1);\n                    }\n                }, error -> Log.e(TAG, error.getMessage()));\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/GlobalKeyTranslator.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.content.Context;\r\nimport android.view.KeyEvent;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.SearchPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\n\r\nimport java.util.Map;\r\n\r\npublic class GlobalKeyTranslator extends KeyTranslator {\r\n    private final Context mContext;\r\n\r\n    public GlobalKeyTranslator(Context context) {\r\n        mContext = context;\r\n    }\r\n\r\n    @Override\r\n    protected void initKeyMapping() {\r\n        Map<Integer, Integer> globalKeyMapping = getKeyMapping();\r\n\r\n        // Fix rare situations with some remotes. E.g. Shield.\r\n        // NOTE: 'sendKey' won't work with Android 13\r\n        globalKeyMapping.put(KeyEvent.KEYCODE_BUTTON_B, KeyEvent.KEYCODE_BACK);\r\n        // Fix for the unknown usb remote controller: https://smartyoutubetv.github.io/#comment-3742343397\r\n        globalKeyMapping.put(KeyEvent.KEYCODE_ESCAPE, KeyEvent.KEYCODE_BACK);\r\n\r\n        // Could cause serious 'OK not working' bug (where Enter key is used as OK)\r\n        // See: KeyHelpers#fixEnterKey\r\n        //globalKeyMapping.put(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_DPAD_CENTER); // G20s fix: show keyboard on textview click\r\n        //globalKeyMapping.put(KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_DPAD_CENTER); // G20s fix: show keyboard on textview click\r\n\r\n        // May help on buggy firmwares (where Enter key is used as OK)\r\n        if (!getPlaybackPresenter().isInPipMode()) {\r\n            globalKeyMapping.put(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_DPAD_CENTER);\r\n        } else {\r\n            globalKeyMapping.remove(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);\r\n        }\r\n\r\n        // 4pda users have an issues with btn remapping\r\n        //globalKeyMapping.put(KeyEvent.KEYCODE_PAGE_UP, KeyEvent.KEYCODE_DPAD_UP);\r\n        //globalKeyMapping.put(KeyEvent.KEYCODE_PAGE_DOWN, KeyEvent.KEYCODE_DPAD_DOWN);\r\n        //globalKeyMapping.put(KeyEvent.KEYCODE_MEDIA_REWIND, KeyEvent.KEYCODE_DPAD_LEFT);\r\n        //globalKeyMapping.put(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, KeyEvent.KEYCODE_DPAD_RIGHT);\r\n    }\r\n\r\n    @Override\r\n    protected void initActionMapping() {\r\n        addSearchAction();\r\n    }\r\n\r\n    private void addSearchAction() {\r\n        Runnable searchAction = () -> getSearchPresenter().startSearch(null);\r\n\r\n        Map<Integer, Runnable> actionMapping = getActionMapping();\r\n\r\n        actionMapping.put(KeyEvent.KEYCODE_AT, searchAction);\r\n\r\n        if (getGeneralData().isRemapChannelUpToSearchEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_CHANNEL_UP, searchAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_CHANNEL_DOWN, searchAction);\r\n        }\r\n    }\r\n\r\n    private GeneralData getGeneralData() {\r\n        return GeneralData.instance(mContext);\r\n    }\r\n\r\n    private PlaybackPresenter getPlaybackPresenter() {\r\n        return PlaybackPresenter.instance(mContext);\r\n    }\r\n\r\n    private SearchPresenter getSearchPresenter() {\r\n        return SearchPresenter.instance(mContext);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/KeyTranslator.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.view.KeyEvent;\r\n\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\npublic abstract class KeyTranslator {\r\n    private static final String TAG = KeyTranslator.class.getSimpleName();\r\n    private final Map<Integer, Integer> mKeyMapping = new HashMap<>();\r\n    private final Map<Integer, Runnable> mActionMapping = new HashMap<>();\r\n    private boolean mIsChecked;\r\n\r\n    /**\r\n     * NOTE: 'sendKey' won't work with Android 13\r\n     */\r\n    public final boolean translateOld(KeyEvent event) {\r\n        boolean handled = false;\r\n\r\n        Runnable action = mActionMapping.get(event.getKeyCode());\r\n        if (action != null && checkEvent(event)) {\r\n            if (event.getAction() == KeyEvent.ACTION_DOWN) {\r\n                action.run();\r\n            }\r\n            handled = true;\r\n        }\r\n\r\n        if (!handled) {\r\n            Integer newKeyCode = mKeyMapping.get(event.getKeyCode());\r\n            KeyEvent newKeyEvent = translate(event, newKeyCode);\r\n            if (newKeyEvent != event && checkEvent(event)) {\r\n                RxHelper.runAsync(() -> Utils.sendKey(newKeyEvent));\r\n                handled = true;\r\n            }\r\n        }\r\n\r\n        return handled;\r\n    }\r\n\r\n    public final KeyEvent translate(KeyEvent event) {\r\n        Runnable action = mActionMapping.get(event.getKeyCode());\r\n        if (action != null && checkEvent(event)) {\r\n            if (event.getAction() == KeyEvent.ACTION_DOWN) {\r\n                action.run();\r\n            }\r\n            return null; // handled\r\n        }\r\n\r\n        Integer newKeyCode = mKeyMapping.get(event.getKeyCode());\r\n        KeyEvent newKeyEvent = translate(event, newKeyCode);\r\n        if (newKeyEvent != event && checkEvent(event)) {\r\n            return newKeyEvent;\r\n        }\r\n\r\n        return event;\r\n    }\r\n\r\n    private KeyEvent translate(KeyEvent origin, Integer newKeyCode) {\r\n        if (newKeyCode == null) {\r\n            return origin;\r\n        }\r\n\r\n        KeyEvent newKey = new KeyEvent(\r\n                origin.getDownTime(),\r\n                origin.getEventTime(),\r\n                origin.getAction(),\r\n                newKeyCode,\r\n                origin.getRepeatCount(),\r\n                origin.getMetaState(),\r\n                origin.getDeviceId(),\r\n                origin.getScanCode(),\r\n                origin.getFlags(),\r\n                origin.getSource()\r\n        );\r\n\r\n        Log.d(TAG, \"Translating %s to %s\", origin, newKey);\r\n\r\n        return newKey;\r\n    }\r\n\r\n    private boolean checkEvent(KeyEvent event) {\r\n        // Fix Volume binding when UI hide\r\n        if (event.getAction() == KeyEvent.ACTION_UP) {\r\n            return mIsChecked;\r\n        }\r\n\r\n        mIsChecked = (event.getKeyCode() != KeyEvent.KEYCODE_DPAD_UP && event.getKeyCode() != KeyEvent.KEYCODE_DPAD_DOWN &&\r\n                event.getKeyCode() != KeyEvent.KEYCODE_DPAD_LEFT && event.getKeyCode() != KeyEvent.KEYCODE_DPAD_RIGHT) ||\r\n                (PlaybackPresenter.instance(null).isPlaying() &&\r\n                        !PlaybackPresenter.instance(null).isOverlayShown() &&\r\n                        !AppDialogPresenter.instance(null).isDialogShown());\r\n\r\n        return mIsChecked;\r\n    }\r\n\r\n    protected abstract void initKeyMapping();\r\n    protected abstract void initActionMapping();\r\n\r\n    protected Map<Integer, Integer> getKeyMapping() {\r\n        return mKeyMapping;\r\n    }\r\n\r\n    protected Map<Integer, Runnable> getActionMapping() {\r\n        return mActionMapping;\r\n    }\r\n\r\n    public void apply() {\r\n        initKeyMapping();\r\n        initActionMapping();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/LocalDriveBackupWorker.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\n\nimport android.content.Context;\nimport android.os.Build.VERSION;\n\nimport androidx.annotation.NonNull;\nimport androidx.work.ExistingPeriodicWorkPolicy;\nimport androidx.work.PeriodicWorkRequest;\nimport androidx.work.WorkManager;\nimport androidx.work.Worker;\nimport androidx.work.WorkerParameters;\n\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\nimport com.liskovsoft.sharedutils.mylogger.Log;\nimport com.liskovsoft.smartyoutubetv2.common.R;\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\n\nimport java.util.concurrent.TimeUnit;\n\n/**\n * Work to synchronize the TV provider database with the desired list of channels and\n * programs. This sample app runs this once at install time to publish an initial set of channels\n * and programs, however in a real-world setting this might be run at other times to synchronize\n * a server's database with the TV provider database.\n * This code will ensure that the channels from \"SampleClipApi.getDesiredPublishedChannelSet()\"\n * appear in the TV provider database, and that these and all other programs are synchronized with\n * TV provider database.\n */\npublic class LocalDriveBackupWorker extends Worker {\n    private static final String TAG = LocalDriveBackupWorker.class.getSimpleName();\n    private static final String WORK_NAME = TAG;\n\n    public LocalDriveBackupWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {\n        super(context, workerParams);\n    }\n\n    public static void schedule(Context context) {\n        if (Utils.isSharedDirRestricted(context)) {\n            return;\n        }\n\n        if (VERSION.SDK_INT >= 23 && GeneralData.instance(context).getLocalDriveBackupFreqDays() > 0) {\n            WorkManager workManager = WorkManager.getInstance(context);\n\n            // https://stackoverflow.com/questions/50943056/avoiding-duplicating-periodicworkrequest-from-workmanager\n            workManager.enqueueUniquePeriodicWork(\n                    WORK_NAME,\n                    ExistingPeriodicWorkPolicy.UPDATE, // fix duplicates (when old worker is running)\n                    new PeriodicWorkRequest.Builder(\n                            LocalDriveBackupWorker.class,\n                            GeneralData.instance(context).getLocalDriveBackupFreqDays(),\n                            TimeUnit.DAYS).addTag(WORK_NAME)\n                            .build()\n            );\n        }\n    }\n\n    public static void forceSchedule(Context context) {\n        if (Utils.isSharedDirRestricted(context)) {\n            MessageHelpers.showLongMessage(context, R.string.local_backup_not_supported);\n            return;\n        }\n\n        new BackupAndRestoreManager(context).checkPermAndBackup();\n        schedule(context);\n    }\n\n    public static void cancel(Context context) {\n        if (VERSION.SDK_INT >= 23 && GeneralData.instance(context).getLocalDriveBackupFreqDays() > 0) {\n            Log.d(TAG, \"Unregistering worker job...\");\n\n            WorkManager workManager = WorkManager.getInstance(context);\n            workManager.cancelUniqueWork(WORK_NAME);\n        }\n    }\n\n    @NonNull\n    @Override\n    public Result doWork() {\n        Log.d(TAG, \"Starting worker %s...\", this);\n\n        new BackupAndRestoreManager(getApplicationContext()).backupData();\n\n        return Result.success();\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/MediaServiceManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.content.Context;\r\nimport android.util.Pair;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.ContentService;\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.mediaserviceinterfaces.NotificationsService;\r\nimport com.liskovsoft.mediaserviceinterfaces.SignInService;\r\nimport com.liskovsoft.mediaserviceinterfaces.SignInService.OnAccountChange;\r\nimport com.liskovsoft.mediaserviceinterfaces.oauth.Account;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItem;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemFormatInfo;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemMetadata;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.NotificationState;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.PlaylistInfo;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.ChannelPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.ChannelUploadsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AccountsData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.LoadingManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\n\r\nimport io.reactivex.Observable;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\nimport java.util.HashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\nimport java.util.concurrent.CopyOnWriteArrayList;\r\nimport java.util.concurrent.atomic.AtomicInteger;\r\n\r\npublic class MediaServiceManager implements OnAccountChange {\r\n    private static final String TAG = MediaServiceManager.class.getSimpleName();\r\n    private static MediaServiceManager sInstance;\r\n    private final MediaItemService mItemService;\r\n    private final ContentService mContentService;\r\n    private final SignInService mSignInService;\r\n    private final NotificationsService mNotificationsService;\r\n    private Disposable mMetadataAction;\r\n    private Disposable mUploadsAction;\r\n    private Disposable mRowsAction;\r\n    private Disposable mSubscribedChannelsAction;\r\n    private Disposable mFormatInfoAction;\r\n    private Disposable mPlaylistGroupAction;\r\n    private Disposable mPlaylistInfosAction;\r\n    private Disposable mHistoryAction;\r\n    private static final int MIN_GRID_GROUP_SIZE = 13;\r\n    private static final int MIN_ROW_GROUP_SIZE = 5;\r\n    private static final int MIN_SCALED_GRID_GROUP_SIZE = 35;\r\n    private static final int MIN_SCALED_ROW_GROUP_SIZE = 10;\r\n    private final Map<Integer, Pair<Integer, Long>> mContinuations = new HashMap<>();\r\n    private final List<AccountChangeListener> mAccountListeners = new CopyOnWriteArrayList<>();\r\n\r\n    public interface OnMetadata {\r\n        void onMetadata(MediaItemMetadata metadata);\r\n    }\r\n\r\n    public interface OnMediaGroup {\r\n        void onMediaGroup(MediaGroup group);\r\n    }\r\n\r\n    public interface OnMediaGroupList {\r\n        void onMediaGroupList(List<MediaGroup> groupList);\r\n    }\r\n\r\n    public interface OnFormatInfo {\r\n        void onFormatInfo(MediaItemFormatInfo formatInfo);\r\n    }\r\n\r\n    public interface OnAccountList {\r\n        void onAccountList(List<Account> accountList);\r\n    }\r\n\r\n    public interface OnPlaylistInfos {\r\n        void onPlaylistInfos(List<PlaylistInfo> playlistInfos);\r\n    }\r\n\r\n    public interface AccountChangeListener {\r\n        void onAccountChanged(Account account);\r\n    }\r\n\r\n    public interface OnError {\r\n        void onError(Throwable error);\r\n    }\r\n\r\n    public interface OnComplete {\r\n        void onComplete();\r\n    }\r\n\r\n    private MediaServiceManager() {\r\n        ServiceManager service = YouTubeServiceManager.instance();\r\n        mItemService = service.getMediaItemService();\r\n        mContentService = service.getContentService();\r\n        mSignInService = service.getSignInService();\r\n        mNotificationsService = service.getNotificationsService();\r\n\r\n        mSignInService.addOnAccountChange(this);\r\n    }\r\n\r\n    public static MediaServiceManager instance() {\r\n        if (sInstance == null) {\r\n            sInstance = new MediaServiceManager();\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void loadMetadata(MediaItem mediaItem, OnMetadata onMetadata) {\r\n        loadMetadata(mediaItem, onMetadata, null, null);\r\n    }\r\n\r\n    /**\r\n     * NOTE: Load suggestions from MediaItem isn't robust. Because playlistId may be initialized from RemoteControlManager.\r\n     */\r\n    public void loadMetadata(MediaItem mediaItem, OnMetadata onMetadata, OnError onError, OnComplete onComplete) {\r\n        loadMetadata(Video.from(mediaItem), onMetadata, onError, onComplete);\r\n    }\r\n\r\n    public void loadMetadata(Video video, OnMetadata onMetadata) {\r\n        loadMetadata(video, onMetadata, null, null);\r\n    }\r\n\r\n    public void loadMetadata(Video video, OnMetadata onMetadata, OnError onError, OnComplete onComplete) {\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mMetadataAction);\r\n\r\n        Observable<MediaItemMetadata> observable;\r\n\r\n        // NOTE: Load suggestions from mediaItem isn't robust. Because playlistId may be initialized from RemoteControlManager.\r\n        // Video might be loaded from Channels section (has playlistParams)\r\n        if (video.mediaItem != null) {\r\n            // Use additional data like playlist id\r\n            observable = mItemService.getMetadataObserve(video.mediaItem);\r\n        } else {\r\n            // Simply load\r\n            observable = mItemService.getMetadataObserve(video.videoId, video.getPlaylistId(), video.playlistIndex, video.playlistParams);\r\n        }\r\n\r\n        mMetadataAction = observable\r\n                .subscribe(\r\n                        onMetadata::onMetadata,\r\n                        error -> {\r\n                            Log.e(TAG, \"loadMetadata error: %s\", error.getMessage());\r\n                            if (onError != null) {\r\n                                onError.onError(error);\r\n                            }\r\n                        },\r\n                        () -> {\r\n                            if (onComplete != null) {\r\n                                onComplete.onComplete();\r\n                            }\r\n                        }\r\n                );\r\n    }\r\n\r\n    public void loadChannelUploads(Video item, OnMediaGroup onMediaGroup) {\r\n        if (item == null) {\r\n            return;\r\n        }\r\n\r\n        loadChannelUploads(item.mediaItem, onMediaGroup);\r\n    }\r\n\r\n    public void loadChannelUploads(MediaItem item, OnMediaGroup onMediaGroup) {\r\n        if (item == null) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mUploadsAction);\r\n\r\n        Observable<MediaGroup> observable = mContentService.getGroupObserve(item);\r\n\r\n        mUploadsAction = observable\r\n                .subscribe(\r\n                        onMediaGroup::onMediaGroup,\r\n                        error -> {\r\n                            onMediaGroup.onMediaGroup(null);\r\n                            Log.e(TAG, \"loadChannelUploads error: %s\", error.getMessage());\r\n                        }\r\n                );\r\n    }\r\n\r\n    public void loadSubscribedChannels(OnMediaGroup onMediaGroup) {\r\n        RxHelper.disposeActions(mSubscribedChannelsAction);\r\n\r\n        Observable<MediaGroup> observable = mContentService.getSubscribedChannelsByNewContentObserve();\r\n\r\n        mSubscribedChannelsAction = observable\r\n                .subscribe(\r\n                        onMediaGroup::onMediaGroup,\r\n                        error -> Log.e(TAG, \"loadSubscribedChannels error: %s\", error.getMessage())\r\n                );\r\n    }\r\n\r\n    private void loadChannelRows(Video item, OnMediaGroupList onMediaGroupList) {\r\n        loadChannelRows(item, onMediaGroupList, null, null);\r\n    }\r\n\r\n    private void loadChannelRows(Video item, OnMediaGroupList onMediaGroupList, OnError onError, OnComplete onComplete) {\r\n        if (item == null) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mRowsAction);\r\n\r\n        Observable<List<MediaGroup>> observable = item.mediaItem != null ?\r\n                mContentService.getChannelObserve(item.mediaItem) : mContentService.getChannelObserve(item.channelId);\r\n\r\n        mRowsAction = observable\r\n                .subscribe(\r\n                        onMediaGroupList::onMediaGroupList,\r\n                        error -> {\r\n                            Log.e(TAG, \"loadChannelRows error: %s\", error.getMessage());\r\n                            if (onError != null) {\r\n                                onError.onError(error);\r\n                            }\r\n                        },\r\n                        () -> {\r\n                            if (onComplete != null) {\r\n                                onComplete.onComplete();\r\n                            }\r\n                        }\r\n                );\r\n    }\r\n\r\n    public void loadChannelPlaylist(Video item, OnMediaGroup callback) {\r\n        loadChannelRows(\r\n                item,\r\n                mediaGroupList -> callback.onMediaGroup(mediaGroupList.get(0))\r\n        );\r\n    }\r\n\r\n    public void loadFormatInfo(Video item, OnFormatInfo onFormatInfo) {\r\n        if (item == null) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mFormatInfoAction);\r\n\r\n        Observable<MediaItemFormatInfo> observable = mItemService.getFormatInfoObserve(item.videoId);\r\n\r\n        mFormatInfoAction = observable\r\n                .subscribe(\r\n                        onFormatInfo::onFormatInfo,\r\n                        error -> Log.e(TAG, \"loadFormatInfo error: %s\", error.getMessage())\r\n                );\r\n    }\r\n\r\n    public void loadPlaylists(Video item, OnMediaGroup onPlaylistGroup) {\r\n        if (item == null) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mPlaylistGroupAction);\r\n\r\n        Observable<MediaGroup> observable = mContentService.getPlaylistsObserve();\r\n\r\n        mPlaylistGroupAction = observable\r\n                .subscribe(\r\n                        onPlaylistGroup::onMediaGroup,\r\n                        error -> Log.e(TAG, \"loadPlaylists error: %s\", error.getMessage())\r\n                );\r\n    }\r\n\r\n    public void getPlaylistInfos(OnPlaylistInfos onPlaylistInfos) {\r\n        RxHelper.disposeActions(mPlaylistInfosAction);\r\n\r\n        Observable<List<PlaylistInfo>> observable = mItemService.getPlaylistsInfoObserve(null);\r\n\r\n        mPlaylistInfosAction = observable\r\n                .subscribe(\r\n                        onPlaylistInfos::onPlaylistInfos,\r\n                        error -> Log.e(TAG, \"getPlaylistInfos error: %s\", error.getMessage())\r\n                );\r\n    }\r\n\r\n    public void loadAccounts(OnAccountList onAccountList) {\r\n        onAccountList.onAccountList(mSignInService.getAccounts());\r\n    }\r\n\r\n    public void authCheck(Runnable onSuccess, Runnable onError) {\r\n        if (onSuccess == null && onError == null) {\r\n            return;\r\n        }\r\n\r\n        if (mSignInService.isSigned()) {\r\n            if (onSuccess != null) {\r\n                onSuccess.run();\r\n            }\r\n        } else {\r\n            if (onError != null) {\r\n                onError.run();\r\n            }\r\n        }\r\n    }\r\n\r\n    public void disposeActions() {\r\n        RxHelper.disposeActions(mMetadataAction, mUploadsAction, mRowsAction, mSubscribedChannelsAction);\r\n    }\r\n\r\n    /**\r\n     * Most tiny ui has 8 cards in a row or 24 in grid.\r\n     */\r\n    public boolean shouldContinueGridGroup(Context context, VideoGroup group) {\r\n        return shouldContinueTheGroup(context, group, true);\r\n    }\r\n\r\n    public boolean shouldContinueRowGroup(Context context, VideoGroup group) {\r\n        return shouldContinueTheGroup(context, group,false);\r\n    }\r\n\r\n    /**\r\n     * Most tiny ui has 8 cards in a row or 24 in grid.\r\n     */\r\n    public boolean shouldContinueTheGroup(Context context, VideoGroup group, boolean isGrid) {\r\n        if (group == null || group.getMediaGroup() == null) {\r\n            return false;\r\n        }\r\n\r\n        MediaGroup mediaGroup = group.getMediaGroup();\r\n\r\n        Pair<Integer, Long> sizeTimestamp = mContinuations.get(group.getId());\r\n\r\n        long currentTimeMillis = System.currentTimeMillis();\r\n        if (sizeTimestamp != null && currentTimeMillis - sizeTimestamp.second > 3_000) { // seems that section is refreshed\r\n            sizeTimestamp = null;\r\n        }\r\n\r\n        int prevSize = sizeTimestamp != null ? sizeTimestamp.first : 0;\r\n        int newSize = mediaGroup.getMediaItems() != null ? mediaGroup.getMediaItems().size() : 0;\r\n        int totalSize = prevSize + newSize;\r\n\r\n        MainUIData mainUIData = MainUIData.instance(context);\r\n\r\n        boolean isScaledUIEnabled = mainUIData.getUIScale() < 0.8f || mainUIData.getVideoGridScale() < 0.8f;\r\n        int minScaledSize = isGrid ? MIN_SCALED_GRID_GROUP_SIZE : MIN_SCALED_ROW_GROUP_SIZE;\r\n        int minSize = isGrid ? MIN_GRID_GROUP_SIZE : MIN_ROW_GROUP_SIZE;\r\n        boolean groupTooSmall = isScaledUIEnabled ? totalSize < minScaledSize : totalSize < minSize;\r\n\r\n        mContinuations.put(group.getId(), new Pair<>(groupTooSmall ? totalSize : 0, currentTimeMillis));\r\n\r\n        return groupTooSmall;\r\n    }\r\n\r\n    public void enableHistory(boolean enable) {\r\n        if (enable) { // don't disable history for other clients\r\n            RxHelper.runAsyncUser(() -> mContentService.enableHistory(true));\r\n        }\r\n    }\r\n\r\n    public void clearHistory(Context context, Runnable onFinish) {\r\n        RxHelper.runAsyncUser(mContentService::clearHistory, onFinish);\r\n        VideoStateService.instance(context).clear(); // even for the logged users this needed too\r\n    }\r\n\r\n    public void clearSearchHistory() {\r\n        RxHelper.runAsyncUser(mContentService::clearSearchHistory);\r\n    }\r\n\r\n    public void updateHistory(Video video, long positionMs) {\r\n        if (video == null || RxHelper.isAnyActionRunning(mHistoryAction)) {\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mHistoryAction);\r\n\r\n        Observable<Void> historyObservable;\r\n\r\n        if (video.mediaItem != null) {\r\n            historyObservable = mItemService.updateHistoryPositionObserve(video.mediaItem, positionMs / 1_000f);\r\n        } else { // video launched form ATV channels\r\n            historyObservable = mItemService.updateHistoryPositionObserve(video.videoId, positionMs / 1_000f);\r\n        }\r\n\r\n        mHistoryAction = RxHelper.execute(historyObservable, error -> setHistoryBroken(true), () -> setHistoryBroken(false));\r\n    }\r\n\r\n    public void hideNotification(Video item) {\r\n        if (item != null && item.belongsToNotifications()) {\r\n            RxHelper.execute(mNotificationsService.hideNotificationObserve(item.mediaItem));\r\n        }\r\n    }\r\n\r\n    public void setNotificationState(NotificationState state, OnError onError) {\r\n        RxHelper.execute(mNotificationsService.setNotificationStateObserve(state), onError::onError);\r\n    }\r\n\r\n    public void removeFromWatchLaterPlaylist(Video video) {\r\n        removeFromWatchLaterPlaylist(video, null);\r\n    }\r\n\r\n    public void removeFromWatchLaterPlaylist(Video video, Runnable onSuccess) {\r\n        if (video == null || !mSignInService.isSigned()) {\r\n            return;\r\n        }\r\n\r\n        Disposable playlistsInfoAction = mItemService.getPlaylistsInfoObserve(video.videoId)\r\n                .subscribe(\r\n                        videoPlaylistInfos -> {\r\n                            PlaylistInfo watchLater = videoPlaylistInfos.get(0);\r\n\r\n                            if (watchLater.isSelected()) {\r\n                                Observable<Void> editObserve = mItemService.removeFromPlaylistObserve(watchLater.getPlaylistId(), video.videoId);\r\n\r\n                                RxHelper.execute(editObserve, () -> {\r\n                                    if (onSuccess != null) {\r\n                                        onSuccess.run();\r\n                                    }\r\n                                });\r\n                            }\r\n                        },\r\n                        error -> {\r\n                            // Fallback to something on error\r\n                            Log.e(TAG, \"Get playlists error: %s\", error.getMessage());\r\n                        }\r\n                );\r\n    }\r\n\r\n    public void addAccountListener(AccountChangeListener listener) {\r\n        if (!mAccountListeners.contains(listener)) {\r\n            if (listener instanceof AccountsData ||\r\n                listener instanceof AppPrefs) {\r\n                mAccountListeners.add(0, listener); // data classes should be called before regular listeners\r\n            } else {\r\n                mAccountListeners.add(listener);\r\n            }\r\n        }\r\n    }\r\n\r\n    public void removeAccountListener(AccountChangeListener listener) {\r\n        mAccountListeners.remove(listener);\r\n    }\r\n\r\n    public Account getSelectedAccount() {\r\n        return mSignInService.getSelectedAccount();\r\n    }\r\n\r\n    public String printAccountDebugInfo() {\r\n        return mSignInService.printDebugInfo();\r\n    }\r\n\r\n    @Override\r\n    public void onAccountChanged(Account account) {\r\n        for (AccountChangeListener listener : mAccountListeners) {\r\n            listener.onAccountChanged(account);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Selecting right presenter for the channel.<br/>\r\n     * Channels could be of two types: regular (usr channel) and playlist channel (contains single row, try search: 'Mon mix')\r\n     */\r\n    public static void chooseChannelPresenter(Context context, Video item) {\r\n        if (item.hasVideo() || item.hasReloadPageKey()) { // a channel item from Channels section\r\n            ChannelPresenter.instance(context).openChannel(item);\r\n            return;\r\n        }\r\n\r\n        LoadingManager.showLoading(context, true);\r\n\r\n        AtomicInteger atomicIndex = new AtomicInteger(0);\r\n\r\n        MediaServiceManager.instance().loadChannelRows(item, groups -> {\r\n            LoadingManager.showLoading(context, false);\r\n\r\n            if (groups == null || groups.isEmpty()) {\r\n                return;\r\n            }\r\n\r\n            MediaGroup firstGroup = groups.get(0);\r\n            int type = firstGroup.getType();\r\n\r\n            if (type == MediaGroup.TYPE_CHANNEL_UPLOADS) {\r\n                if (atomicIndex.incrementAndGet() == 1) {\r\n                    ChannelUploadsPresenter.instance(context).clear();\r\n                    ChannelUploadsPresenter.instance(context).setChannel(item);\r\n                }\r\n                // NOTE: Crashes RecycleView IndexOutOfBoundsException when doing add immediately after clear\r\n                Utils.postDelayed(() -> ChannelUploadsPresenter.instance(context).update(firstGroup), 100);\r\n            } else if (type == MediaGroup.TYPE_CHANNEL) {\r\n                if (atomicIndex.incrementAndGet() == 1) {\r\n                    ChannelPresenter.instance(context).clear();\r\n                    ChannelPresenter.instance(context).setChannel(item);\r\n                }\r\n                // NOTE: Crashes RecycleView IndexOutOfBoundsException when doing add immediately after clear\r\n                Utils.postDelayed(() -> ChannelPresenter.instance(context).updateRows(groups), 100);\r\n            } else {\r\n                MessageHelpers.showMessage(context, \"Unknown type of channel\");\r\n            }\r\n        }, error -> LoadingManager.showLoading(context, false), () -> LoadingManager.showLoading(context, false));\r\n    }\r\n\r\n    private void setHistoryBroken(boolean isBroken) {\r\n        VideoStateService stateService = VideoStateService.instance(null);\r\n\r\n        if (stateService != null) {\r\n            stateService.setHistoryBroken(isBroken);\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/MotherActivity.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport android.content.Intent;\r\nimport android.content.pm.ActivityInfo;\r\nimport android.content.res.Configuration;\r\nimport android.os.Build.VERSION;\r\nimport android.os.Bundle;\r\nimport android.util.DisplayMetrics;\r\nimport android.view.KeyCharacterMap.UnavailableException;\r\nimport android.view.KeyEvent;\r\nimport android.view.MotionEvent;\r\n\r\nimport androidx.annotation.NonNull;\r\nimport androidx.annotation.Nullable;\r\nimport androidx.fragment.app.FragmentActivity;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.KeyHelpers;\r\nimport com.liskovsoft.sharedutils.locale.LocaleContextWrapper;\r\nimport com.liskovsoft.sharedutils.locale.LocaleUpdater;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ViewManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.internal.MediaServiceData;\r\nimport com.r0adkll.slidr.Slidr;\r\nimport com.r0adkll.slidr.model.SlidrConfig;\r\nimport com.r0adkll.slidr.model.SlidrListener;\r\nimport com.r0adkll.slidr.model.SlidrPosition;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class MotherActivity extends FragmentActivity {\r\n    private static final String TAG = MotherActivity.class.getSimpleName();\r\n    private static final float DEFAULT_DENSITY = 2.0f; // xhdpi\r\n    private static final float DEFAULT_WIDTH = 1920f; // xhdpi\r\n    private static DisplayMetrics sCachedDisplayMetrics;\r\n    protected static boolean sIsInPipMode;\r\n    private ScreensaverManager mScreensaverManager;\r\n    // Make static in case Don't keep activities enabled in Developer settings\r\n    private static List<OnPermissions> mOnPermissions;\r\n    private static List<OnResult> mOnResults;\r\n    private long mLastKeyDownTime;\r\n    private boolean mEnableThrottleKeyDown;\r\n    private boolean mIsOculusQuestFixEnabled;\r\n    private boolean mIsFullscreenModeEnabled;\r\n\r\n    public interface OnPermissions {\r\n        void onPermissions(int requestCode, String[] permissions, int[] grantResults);\r\n    }\r\n\r\n    public interface OnResult {\r\n        void onResult(int requestCode, int resultCode, Intent data);\r\n    }\r\n\r\n    @Override\r\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\r\n        // Fixing: Only fullscreen opaque activities can request orientation (api 26)\r\n        // NOTE: You should remove 'screenOrientation' from the manifest.\r\n        // NOTE: Possible side effect: initDpi() won't work: \"When you setRequestedOrientation() the view may be restarted\"\r\n        //if (VERSION.SDK_INT != 26) {\r\n        //    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);\r\n        //}\r\n        super.onCreate(savedInstanceState);\r\n\r\n        Log.d(TAG, \"Starting %s...\", this.getClass().getSimpleName());\r\n\r\n        mIsOculusQuestFixEnabled = PlayerTweaksData.instance(this).isOculusQuestFixEnabled();\r\n        mIsFullscreenModeEnabled = GeneralData.instance(this).isFullscreenModeEnabled();\r\n\r\n        initDpi();\r\n        initTheme();\r\n\r\n        // Search Fullscreen routine inside onPause() method\r\n        if (!mIsFullscreenModeEnabled) {\r\n            // There's no way to do this programmatically!\r\n            setTheme(R.style.FitSystemWindows);\r\n\r\n            // totally disabling the translucency or any color placed on the status bar and navigation bar\r\n            //if (Build.VERSION.SDK_INT >= 19) {\r\n            //    Window w = getWindow();\r\n            //    w.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);\r\n            //}\r\n        }\r\n\r\n        if (mIsOculusQuestFixEnabled) {\r\n            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);\r\n        }\r\n\r\n        mScreensaverManager = new ScreensaverManager(this); // moved below the theme to fix side effects\r\n\r\n        //Helpers.addFullscreenListener(this);\r\n\r\n        initEdgeSlide();\r\n    }\r\n\r\n    @Override\r\n    public boolean dispatchGenericMotionEvent(MotionEvent event) {\r\n        if (event.getAction() == KeyEvent.ACTION_DOWN) {\r\n            mScreensaverManager.enable();\r\n        }\r\n\r\n        return super.dispatchGenericMotionEvent(event);\r\n    }\r\n\r\n    @Override\r\n    public boolean dispatchTouchEvent(MotionEvent event) {\r\n        if (event.getAction() == KeyEvent.ACTION_DOWN) {\r\n            mScreensaverManager.enable();\r\n        }\r\n\r\n        try {\r\n            return super.dispatchTouchEvent(event);\r\n        } catch (NullPointerException | SecurityException | IllegalStateException | ArrayIndexOutOfBoundsException e) {\r\n            // Attempt to invoke interface method 'boolean android.app.trust.ITrustManager.isDeviceLocked(int)' on a null object reference\r\n            // Permission Denial: starting Intent\r\n            // IllegalStateException: exitFreeformMode: You can only go fullscreen from freeform.\r\n            e.printStackTrace();\r\n            return false;\r\n        }\r\n    }\r\n\r\n    @SuppressLint(\"RestrictedApi\")\r\n    @Override\r\n    public boolean dispatchKeyEvent(KeyEvent event) {\r\n        if (event == null) { // handled\r\n            return true;\r\n        }\r\n\r\n        if (event.getAction() == KeyEvent.ACTION_DOWN) {\r\n            boolean isKeepScreenOff = mScreensaverManager.isScreenOff() && Helpers.equalsAny(event.getKeyCode(),\r\n                    new int[]{KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN});\r\n            if (!isKeepScreenOff) {\r\n                mScreensaverManager.enable();\r\n            }\r\n        }\r\n\r\n        try {\r\n            return super.dispatchKeyEvent(event);\r\n        } catch (NullPointerException | IllegalArgumentException | IllegalStateException | SecurityException | UnavailableException e) {\r\n            // NullPointerException: 'android.view.Window androidx.core.app.ComponentActivity.getWindow()' on a null object reference\r\n            // IllegalArgumentException: View is not a direct child of HorizontalGridView\r\n            // Fatal Exception: java.lang.IllegalStateException\r\n            // android.permission.RECORD_AUDIO required for search (Android 5 mostly)\r\n            // Fatal Exception: java.lang.SecurityException\r\n            // Not allowed to bind to service Intent { act=android.speech.RecognitionService cmp=com.xgimi.duertts/com.baidu.duer.services.tvser\r\n            e.printStackTrace();\r\n            return false;\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\r\n        if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP) { // shortcut for closing PIP\r\n            PlaybackPresenter.instance(this).forceFinish();\r\n            return true;\r\n        }\r\n\r\n        boolean result = super.onKeyDown(keyCode, event);\r\n\r\n        // Fix buggy G20s menu key (focus lost on key press)\r\n        return KeyHelpers.isMenuKey(keyCode) || throttleKeyDown(keyCode) || result;\r\n    }\r\n\r\n    public void finishReally() {\r\n        try {\r\n            if (VERSION.SDK_INT >= 21 && getViewManager().getTopView() != null) { // remain root activity in recents\r\n                super.finishAndRemoveTask();\r\n            } else {\r\n                super.finish();\r\n            }\r\n        } catch (Exception e) {\r\n            // TextView not attached to window manager (IllegalArgumentException)\r\n        }\r\n    }\r\n\r\n    @Override\r\n    protected void attachBaseContext(Context context) {\r\n        Context contextWrapper = null;\r\n\r\n        if (context != null) {\r\n            // NOTE: Use only cached metrics. Because metrics creation involves using WindowManager, which isn't available at this stage.\r\n            contextWrapper = LocaleContextWrapper.wrap(context, LocaleUpdater.getSavedLocale(context), sCachedDisplayMetrics);\r\n        }\r\n\r\n        super.attachBaseContext(contextWrapper);\r\n    }\r\n\r\n    @Override\r\n    protected void onResume() {\r\n        try {\r\n            super.onResume();\r\n        } catch (IllegalArgumentException e) {\r\n            e.printStackTrace();\r\n        }\r\n\r\n        // 4K fix with AFR\r\n        applyCustomConfig();\r\n\r\n        applyFullscreenModeIfNeeded();\r\n\r\n        // Remove screensaver from the previous activity when closing current one.\r\n        // Called on player's next track. Reason unknown.\r\n        mScreensaverManager.enable();\r\n    }\r\n\r\n    @Override\r\n    protected void onPause() {\r\n        super.onPause();\r\n\r\n        // Remove screensaver from the previous activity when closing current one.\r\n        // Called on player's next track. Reason unknown.\r\n        mScreensaverManager.disable();\r\n    }\r\n\r\n    @Override\r\n    public void onConfigurationChanged(Configuration newConfig) {\r\n        super.onConfigurationChanged(newConfig);\r\n\r\n        applyCustomConfig();\r\n    }\r\n\r\n    public ScreensaverManager getScreensaverManager() {\r\n        return mScreensaverManager;\r\n    }\r\n\r\n    protected void initTheme() {\r\n        int rootThemeResId = MainUIData.instance(this).getColorScheme().browseThemeResId;\r\n        if (rootThemeResId > 0) {\r\n            setTheme(rootThemeResId);\r\n        }\r\n    }\r\n\r\n    private void initDpi() {\r\n        getResources().getDisplayMetrics().setTo(getDisplayMetrics(this));\r\n    }\r\n\r\n    private DisplayMetrics getDisplayMetrics(Context context) {\r\n        // BUG: adapt to resolution change (e.g. on AFR)\r\n        // Don't disable caching or you will experience weird sizes on cards in video suggestions (e.g. after exit from PIP)!\r\n        if (sCachedDisplayMetrics == null) {\r\n            // NOTE: Don't replace with getResources().getDisplayMetrics(). Shows wrong metrics here!\r\n            DisplayMetrics displayMetrics = new DisplayMetrics();\r\n            getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);\r\n            float uiScale = MainUIData.instance(context).getUIScale();\r\n            // Take into the account screen orientation (e.g. when running on phone)\r\n            int widthPixels = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels);\r\n            float widthRatio = DEFAULT_WIDTH / widthPixels;\r\n            float density = DEFAULT_DENSITY / widthRatio * uiScale;\r\n            displayMetrics.density = density;\r\n            displayMetrics.scaledDensity = density;\r\n            sCachedDisplayMetrics = displayMetrics;\r\n        }\r\n\r\n        return sCachedDisplayMetrics;\r\n    }\r\n\r\n    private void applyCustomConfig() {\r\n        // NOTE: dpi should come after locale update to prevent resources overriding.\r\n\r\n        // Fix sudden language change.\r\n        // Could happen when screen goes off or after PIP mode.\r\n        LocaleUpdater.applySavedLocale(this);\r\n\r\n        // Fix sudden dpi change.\r\n        // Could happen when screen goes off or after PIP mode.\r\n        initDpi();\r\n    }\r\n\r\n    private void applyFullscreenModeIfNeeded() {\r\n        if (mIsFullscreenModeEnabled) {\r\n            // Most of the fullscreen tweaks could be performed in styles but not all.\r\n            // E.g. Hide bottom navigation bar (couldn't be done in styles).\r\n            Helpers.makeActivityFullscreen2(this);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {\r\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults);\r\n\r\n        if (mOnPermissions != null) {\r\n            for (OnPermissions callback : mOnPermissions) {\r\n                callback.onPermissions(requestCode, permissions, grantResults);\r\n            }\r\n            mOnPermissions.clear();\r\n            mOnPermissions = null;\r\n        }\r\n    }\r\n\r\n    @Override\r\n    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {\r\n        super.onActivityResult(requestCode, resultCode, data);\r\n\r\n        if (mOnResults != null) {\r\n            for (OnResult callback : mOnResults) {\r\n                callback.onResult(requestCode, resultCode, data);\r\n            }\r\n            mOnResults.clear();\r\n            mOnResults = null;\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onBackPressed() {\r\n        super.onBackPressed();\r\n        // Oculus Quest fix: back button not closing the activity\r\n        if (mIsOculusQuestFixEnabled) {\r\n            finish();\r\n        }\r\n    }\r\n\r\n    public void addOnPermissions(OnPermissions onPermissions) {\r\n        if (mOnPermissions == null) {\r\n            mOnPermissions = new ArrayList<>();\r\n        }\r\n\r\n        mOnPermissions.remove(onPermissions);\r\n        mOnPermissions.add(onPermissions);\r\n    }\r\n\r\n    public void addOnResult(OnResult onResult) {\r\n        if (mOnResults == null) {\r\n            mOnResults = new ArrayList<>();\r\n        }\r\n\r\n        mOnResults.remove(onResult);\r\n        mOnResults.add(onResult);\r\n    }\r\n\r\n    /**\r\n     * Use this method only upon exiting from the app.<br/>\r\n     * Big troubles with AFR resolution switch!\r\n     */\r\n    public static void invalidate() {\r\n        sCachedDisplayMetrics = null;\r\n        sIsInPipMode = false;\r\n    }\r\n\r\n    public static DisplayMetrics getCachedDisplayMetrics() {\r\n        return sCachedDisplayMetrics;\r\n    }\r\n\r\n    /**\r\n     * Comments focus fix<br/>\r\n     * https://stackoverflow.com/questions/34277425/recyclerview-items-lose-focus\r\n     */\r\n    private boolean throttleKeyDown(int keyCode) {\r\n        if (mEnableThrottleKeyDown && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {\r\n            long current = System.currentTimeMillis();\r\n            if (current - mLastKeyDownTime < 100) {\r\n                return true;\r\n            }\r\n\r\n            mLastKeyDownTime = current;\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    /**\r\n     * Comments focus fix<br/>\r\n     * https://stackoverflow.com/questions/34277425/recyclerview-items-lose-focus\r\n     */\r\n    public void enableThrottleKeyDown(boolean enable) {\r\n        mEnableThrottleKeyDown = enable;\r\n    }\r\n\r\n    //@Override\r\n    //public void setTheme(int resid) {\r\n    //    super.setTheme(resid);\r\n    //\r\n    //    // No way to do this programmatically!\r\n    //    if (!GeneralData.instance(this).isFullscreenModeEnabled()) {\r\n    //        super.setTheme(R.style.FitSystemWindows);\r\n    //    }\r\n    //}\r\n\r\n    protected ViewManager getViewManager() {\r\n        return ViewManager.instance(this);\r\n    }\r\n\r\n    protected GeneralData getGeneralData() {\r\n        return GeneralData.instance(this);\r\n    }\r\n\r\n    protected PlayerTweaksData getPlayerTweaksData() {\r\n        return PlayerTweaksData.instance(this);\r\n    }\r\n\r\n    protected PlayerData getPlayerData() {\r\n        return PlayerData.instance(this);\r\n    }\r\n\r\n    protected MainUIData getMainUIData() {\r\n        return MainUIData.instance(this);\r\n    }\r\n\r\n    protected MediaServiceData getMediaServiceData() {\r\n        return MediaServiceData.instance();\r\n    }\r\n\r\n    private void initEdgeSlide() {\r\n        if (VERSION.SDK_INT < 21 || !Helpers.isTouchSupported(this) || Utils.isSystemGestureArrowEnabled(this)) {\r\n            return;\r\n        }\r\n\r\n        SlidrConfig config = new SlidrConfig.Builder()\r\n                .position(SlidrPosition.LEFT) // Swipe from the left\r\n                .edge(true)              // Only trigger from the screen edge\r\n                .edgeSize(0.18f)              // Grab 18% of the screen (good for cars)\r\n                .scrimStartAlpha(0f)          // Don't dim the background screen\r\n                .scrimEndAlpha(0f)            // Background clear when finished\r\n                .distanceThreshold(0.1f)      // Set drag distance to minimum\r\n                .partial(true)           // Don't do full slide animation\r\n                .listener(new SlidrListener() {\r\n                    @Override\r\n                    public void onSlideStateChanged(int state) {}\r\n\r\n                    @Override\r\n                    public void onSlideChange(float percent) {}\r\n\r\n                    @Override\r\n                    public void onSlideOpened() {}\r\n\r\n                    @Override\r\n                    public boolean onSlideClosed() {\r\n                        // This replaces the default finish() with your back logic\r\n                        onBackPressed();\r\n                        return true; // Tells the library we handled the close\r\n                    }\r\n                })\r\n                .build();\r\n\r\n        // Attach to this activity\r\n        Slidr.attach(this, config);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/PlayerKeyTranslator.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.content.Context;\r\nimport android.view.KeyEvent;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerUI;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.Arrays;\r\nimport java.util.Map;\r\n\r\npublic class PlayerKeyTranslator extends GlobalKeyTranslator {\r\n    private final GeneralData mGeneralData;\r\n    private final Context mContext;\r\n    private final Runnable likeAction = () -> {\r\n        PlaybackPresenter playbackPresenter = getPlaybackPresenter();\r\n        if (playbackPresenter != null && playbackPresenter.getView() != null) {\r\n            playbackPresenter.onButtonClicked(R.id.action_thumbs_up, PlayerUI.BUTTON_ON);\r\n            playbackPresenter.getView().setButtonState(R.id.action_thumbs_up, PlayerUI.BUTTON_ON);\r\n            playbackPresenter.getView().setButtonState(R.id.action_thumbs_down, PlayerUI.BUTTON_OFF);\r\n            MessageHelpers.showMessage(getContext(), R.string.action_like);\r\n        }\r\n    };\r\n    private final Runnable dislikeAction = () -> {\r\n        PlaybackPresenter playbackPresenter = getPlaybackPresenter();\r\n        if (playbackPresenter != null && playbackPresenter.getView() != null) {\r\n            playbackPresenter.onButtonClicked(R.id.action_thumbs_down, PlayerUI.BUTTON_ON);\r\n            playbackPresenter.getView().setButtonState(R.id.action_thumbs_up, PlayerUI.BUTTON_OFF);\r\n            playbackPresenter.getView().setButtonState(R.id.action_thumbs_down, PlayerUI.BUTTON_ON);\r\n            MessageHelpers.showMessage(getContext(), R.string.action_dislike);\r\n        }\r\n    };\r\n    private final Runnable speedUpAction = () -> speedUp(true);\r\n    private final Runnable speedDownAction = () -> speedUp(false);\r\n    private final Runnable volumeUpAction = () -> volumeUp(true);\r\n    private final Runnable volumeDownAction = () -> volumeUp(false);\r\n    private final Runnable speedToggleAction = () -> {\r\n        PlaybackPresenter playbackPresenter = getPlaybackPresenter();\r\n        if (playbackPresenter != null && playbackPresenter.getView() != null) {\r\n            float currentSpeed = playbackPresenter.getView().getSpeed();\r\n            playbackPresenter.onButtonClicked(R.id.action_video_speed, currentSpeed != 1.0f ? PlayerUI.BUTTON_ON : PlayerUI.BUTTON_OFF);\r\n        }\r\n    };\r\n\r\n    public PlayerKeyTranslator(Context context) {\r\n        super(context);\r\n        mContext = context;\r\n        mGeneralData = GeneralData.instance(context);\r\n    }\r\n\r\n    @Override\r\n    protected void initKeyMapping() {\r\n        super.initKeyMapping();\r\n\r\n        Map<Integer, Integer> globalKeyMapping = getKeyMapping();\r\n\r\n        // Reset global mapping to default\r\n        globalKeyMapping.remove(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);\r\n        globalKeyMapping.remove(KeyEvent.KEYCODE_MEDIA_REWIND);\r\n        globalKeyMapping.remove(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD);\r\n\r\n        if (mGeneralData.isRemapFastForwardToNextEnabled()) {\r\n            globalKeyMapping.put(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, KeyEvent.KEYCODE_MEDIA_NEXT);\r\n            globalKeyMapping.put(KeyEvent.KEYCODE_MEDIA_REWIND, KeyEvent.KEYCODE_MEDIA_PREVIOUS);\r\n        }\r\n\r\n        if (mGeneralData.isRemapNextToFastForwardEnabled()) {\r\n            globalKeyMapping.put(KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD);\r\n            globalKeyMapping.put(KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_MEDIA_REWIND);\r\n        }\r\n\r\n        if (mGeneralData.isRemapPageUpToNextEnabled()) {\r\n            globalKeyMapping.put(KeyEvent.KEYCODE_PAGE_UP, KeyEvent.KEYCODE_MEDIA_NEXT);\r\n            globalKeyMapping.put(KeyEvent.KEYCODE_PAGE_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS);\r\n        }\r\n\r\n        if (mGeneralData.isRemapChannelUpToNextEnabled()) {\r\n            globalKeyMapping.put(KeyEvent.KEYCODE_CHANNEL_UP, KeyEvent.KEYCODE_MEDIA_NEXT);\r\n            globalKeyMapping.put(KeyEvent.KEYCODE_CHANNEL_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS);\r\n        }\r\n        \r\n        if (!PlaybackPresenter.instance(mContext).isInPipMode() && mGeneralData.isRemapPlayToOKEnabled()) {\r\n            globalKeyMapping.put(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_DPAD_CENTER);\r\n        } else {\r\n            globalKeyMapping.remove(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);\r\n        }\r\n\r\n        globalKeyMapping.put(KeyEvent.KEYCODE_DEL, KeyEvent.KEYCODE_0); // reset position of the video (if enabled number key handling in the settings)\r\n\r\n        // Toggle playback on PLAY/PAUSE. NOTE: cause troubles with IoT handlers!!!\r\n        //globalKeyMapping.put(KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);\r\n        //globalKeyMapping.put(KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);\r\n    }\r\n\r\n    @Override\r\n    protected void initActionMapping() {\r\n        super.initActionMapping();\r\n\r\n        Map<Integer, Runnable> actionMapping = getActionMapping();\r\n\r\n        if (mGeneralData.isRemapSToSpeedToggleEnabled()) { // New mapping check\r\n            actionMapping.put(KeyEvent.KEYCODE_S, speedToggleAction);\r\n        }\r\n\r\n        if (mGeneralData.isRemapPageUpToLikeEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_PAGE_UP, likeAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_PAGE_DOWN, dislikeAction);\r\n        }\r\n\r\n        if (mGeneralData.isRemapChannelUpToLikeEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_CHANNEL_UP, likeAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_CHANNEL_DOWN, dislikeAction);\r\n        }\r\n\r\n        if (mGeneralData.isRemapPageUpToSpeedEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_PAGE_UP, speedUpAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_PAGE_DOWN, speedDownAction);\r\n        }\r\n\r\n        if (mGeneralData.isRemapPageDownToSpeedEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_PAGE_UP, speedDownAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_PAGE_DOWN, speedUpAction);\r\n        }\r\n\r\n        if (mGeneralData.isRemapChannelUpToSpeedEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_CHANNEL_UP, speedUpAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_CHANNEL_DOWN, speedDownAction);\r\n        }\r\n\r\n        if (mGeneralData.isRemapFastForwardToSpeedToggleEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, speedToggleAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_MEDIA_REWIND, speedToggleAction);\r\n        } else if (mGeneralData.isRemapFastForwardToSpeedEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, speedUpAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_MEDIA_REWIND, speedDownAction);\r\n        }\r\n\r\n        if (mGeneralData.isRemapNextToSpeedEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_MEDIA_NEXT, speedUpAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_MEDIA_PREVIOUS, speedDownAction);\r\n        }\r\n\r\n        if (mGeneralData.isRemapDpadUpToSpeedEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_DPAD_UP, speedUpAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_DPAD_DOWN, speedDownAction);\r\n        }\r\n\r\n        if (mGeneralData.isRemapNumbersToSpeedEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_3, speedUpAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_1, speedDownAction);\r\n        }\r\n\r\n        if (mGeneralData.isRemapChannelUpToVolumeEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_CHANNEL_UP, volumeUpAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_CHANNEL_DOWN, volumeDownAction);\r\n        }\r\n\r\n        if (mGeneralData.isRemapDpadUpToVolumeEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_DPAD_UP, volumeUpAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_DPAD_DOWN, volumeDownAction);\r\n        }\r\n\r\n        if (mGeneralData.isRemapDpadLeftToVolumeEnabled()) {\r\n            actionMapping.put(KeyEvent.KEYCODE_DPAD_LEFT, volumeDownAction);\r\n            actionMapping.put(KeyEvent.KEYCODE_DPAD_RIGHT, volumeUpAction);\r\n        }\r\n    }\r\n\r\n    private void speedUp(boolean up) {\r\n        PlayerTweaksData data = PlayerTweaksData.instance(mContext);\r\n        float[] speedSteps = data.isLongSpeedListEnabled() ? Utils.SPEED_LIST_LONG :\r\n                data.isExtraLongSpeedListEnabled() ? Utils.SPEED_LIST_EXTRA_LONG : Utils.SPEED_LIST_SHORT;\r\n\r\n        PlaybackPresenter playbackPresenter = getPlaybackPresenter();\r\n\r\n        if (playbackPresenter != null && playbackPresenter.getView() != null) {\r\n            float currentSpeed = playbackPresenter.getView().getSpeed();\r\n            int currentIndex = Arrays.binarySearch(speedSteps, currentSpeed);\r\n\r\n            if (currentIndex < 0) {\r\n                currentIndex = Arrays.binarySearch(speedSteps, 1.0f);\r\n            }\r\n\r\n            int newIndex = up ? currentIndex + 1 : currentIndex - 1;\r\n\r\n            float speed = newIndex >= 0 && newIndex < speedSteps.length ? speedSteps[newIndex] : speedSteps[currentIndex];\r\n\r\n            PlayerData.instance(mContext).setSpeed(speed);\r\n            playbackPresenter.getView().setSpeed(speed);\r\n            MessageHelpers.showMessage(mContext, String.format(\"%sx\", speed));\r\n        }\r\n    }\r\n\r\n    private void volumeUp(boolean up) {\r\n        PlaybackPresenter playbackPresenter = getPlaybackPresenter();\r\n\r\n        if (playbackPresenter != null && playbackPresenter.getView() != null) {\r\n            Utils.volumeUp(mContext, playbackPresenter.getView(), up);\r\n        }\r\n    }\r\n\r\n    private PlaybackPresenter getPlaybackPresenter() {\r\n        return PlaybackPresenter.instance(mContext);\r\n    }\r\n\r\n    private Context getContext() {\r\n        return mContext;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/RemoteControlReceiver.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.BroadcastReceiver;\r\nimport android.content.Context;\r\nimport android.content.Intent;\r\n\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\npublic class RemoteControlReceiver extends BroadcastReceiver {\r\n    private static final String TAG = RemoteControlReceiver.class.getSimpleName();\r\n\r\n    @SuppressLint(\"UnsafeProtectedBroadcastReceiver\")\r\n    @Override\r\n    public void onReceive(Context context, Intent intent) {\r\n        Log.d(TAG, \"Initializing remote control listener...\");\r\n\r\n        // Fix unload from the memory on some devices?\r\n        // NOTE: Starting from Android 12 (api 31) foreground service with type 'connectedDevice' not supported\r\n        // Use 'mediaPlayback' type instead\r\n        try {\r\n            Utils.updateRemoteControlService(context);\r\n        } catch (Exception e) {\r\n            // ForegroundServiceStartNotAllowedException: startForegroundService() not allowed due to mAllowStartForeground false (Android 12)\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/RemoteControlService.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.app.Notification;\r\nimport android.app.Service;\r\nimport android.content.Intent;\r\nimport android.content.pm.ServiceInfo;\r\nimport android.os.Build.VERSION;\r\nimport android.os.IBinder;\r\n\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ViewManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\npublic class RemoteControlService extends Service {\r\n    private static final String TAG = RemoteControlService.class.getSimpleName();\r\n    private static final int NOTIFICATION_ID = RemoteControlService.class.hashCode();\r\n\r\n    @Nullable\r\n    @Override\r\n    public IBinder onBind(Intent intent) {\r\n        Log.d(TAG, \"onBind: %s\", Helpers.toString(intent));\r\n\r\n        return null;\r\n    }\r\n\r\n    @Override\r\n    public void onCreate() {\r\n        super.onCreate();\r\n\r\n        // https://stackoverflow.com/questions/46445265/android-8-0-java-lang-illegalstateexception-not-allowed-to-start-service-inten\r\n        // NOTE: it's impossible to hide notification on Android 9 and above\r\n        // https://stackoverflow.com/questions/10962418/how-to-startforeground-without-showing-notification\r\n        try {\r\n            startForeground(NOTIFICATION_ID, createNotification());\r\n        } catch (Exception e) {\r\n            // NullPointerException: Attempt to read from field 'int com.android.server.am.UidRecord.curProcState' on a null object reference\r\n            // ForegroundServiceStartNotAllowedException: Service.startForeground() not allowed due to mAllowStartForeground false (Android 14)\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public int onStartCommand(Intent intent, int flags, int startId) {\r\n        Log.d(TAG, \"onStartCommand: %s\", Helpers.toString(intent));\r\n\r\n        PlaybackPresenter.instance(getApplicationContext()); // init RemoteControlListener\r\n        StreamReminderService.instance(getApplicationContext()).start(); // init reminder service\r\n\r\n        return START_STICKY;\r\n    }\r\n\r\n    private Notification createNotification() {\r\n        String remoteControl = getString(R.string.settings_remote_control);\r\n        String serviceStarted = getString(R.string.background_service_started);\r\n\r\n        return Utils.createNotification(\r\n                getApplicationContext(),\r\n                getApplicationInfo().icon,\r\n                String.format(\"%s: %s\", remoteControl, serviceStarted),\r\n                ViewManager.instance(getApplicationContext()).getRootActivity());\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/RemoteControlWorker.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.content.Context;\r\nimport androidx.annotation.NonNull;\r\nimport androidx.work.Worker;\r\nimport androidx.work.WorkerParameters;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\n\r\npublic class RemoteControlWorker extends Worker {\r\n    private static final String TAG = RemoteControlWorker.class.getSimpleName();\r\n\r\n    public RemoteControlWorker(\r\n            @NonNull Context context,\r\n            @NonNull WorkerParameters workerParams) {\r\n        super(context, workerParams);\r\n    }\r\n\r\n    @NonNull\r\n    @Override\r\n    public Result doWork() {\r\n        Log.d(TAG, \"Doing some work...\");\r\n\r\n        PlaybackPresenter.instance(getApplicationContext()); // init RemoteControlListener\r\n\r\n        return Result.success();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/ScreensaverManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.app.Activity;\r\nimport android.view.LayoutInflater;\r\nimport android.view.View;\r\nimport android.view.ViewGroup;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AddDevicePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.SignInPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.PlaybackView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ViewManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.sharedutils.misc.WeakHashSet;\r\n\r\nimport java.lang.ref.WeakReference;\r\n\r\npublic class ScreensaverManager {\r\n    private static final String TAG = ScreensaverManager.class.getSimpleName();\r\n    private static final int MODE_SCREENSAVER = 0;\r\n    private static final int MODE_SCREEN_OFF = 1;\r\n    private static final WeakHashSet<ScreensaverManager> sInstances = new WeakHashSet<>();\r\n    private static boolean sLockInstance;\r\n    private final WeakReference<Activity> mActivity;\r\n    private final WeakReference<View> mDimContainer;\r\n    private final Runnable mDimScreen = this::dimScreen;\r\n    private final Runnable mUndimScreen = this::undimScreen;\r\n    private final Runnable mUnlockInstance = () -> sLockInstance = false;\r\n    private int mMode = MODE_SCREENSAVER;\r\n    private boolean mIsScreenOff;\r\n    private boolean mIsBlocked;\r\n    private final Runnable mTimeoutHandler = () -> {\r\n        // Playing the video and dialog overlay isn't shown\r\n        if (getViewManager().getTopView() != PlaybackView.class || !getTweaksData().isScreenOffTimeoutEnabled()) {\r\n            return;\r\n        }\r\n\r\n        if (!getAppDialogPresenter().isDialogShown()) {\r\n            doScreenOff();\r\n        } else {\r\n            // showing dialog... or recheck...\r\n            enableTimeout();\r\n        }\r\n    };\r\n\r\n    public ScreensaverManager(Activity activity) {\r\n        mActivity = new WeakReference<>(activity);\r\n        mDimContainer = new WeakReference<>(createDimContainer(activity));\r\n        enable();\r\n        addToRegistry();\r\n    }\r\n\r\n    private View createDimContainer(Activity activity) {\r\n        View rootView = activity.getWindow().getDecorView().getRootView();\r\n\r\n        View dimContainer = rootView.findViewById(R.id.dim_container);\r\n\r\n        if (dimContainer == null) {\r\n            LayoutInflater layoutInflater = activity.getLayoutInflater();\r\n            dimContainer = layoutInflater.inflate(R.layout.dim_container, null);\r\n            if (rootView instanceof ViewGroup) {\r\n                // NOTE: zoom will be bugged! Frames on top and bottom.\r\n                // Add negative margin to fix un-proper viewport positioning on some devices\r\n                // NOTE: below code is not working!!!\r\n                // NOTE: comment out code below if you don't want this\r\n                //LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(\r\n                //        LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);\r\n                //params.setMargins(-30, -30, -30, -30);\r\n                //((ViewGroup) rootView).addView(dimContainer, params);\r\n\r\n                ((ViewGroup) rootView).addView(dimContainer);\r\n            }\r\n        }\r\n\r\n        return dimContainer;\r\n    }\r\n\r\n    /**\r\n     * Screen off check\r\n     */\r\n    public void enableChecked() {\r\n        // Fix dialog dimming when using the play button on the remote controller.\r\n        // NOTE: only the last activity will show dimming and in our case the last one is PlaybackActivity\r\n        if (mMode == MODE_SCREEN_OFF || getAppDialogPresenter().isDialogShown()) {\r\n            return;\r\n        }\r\n\r\n        enable();\r\n    }\r\n\r\n    /**\r\n     * Screen off check\r\n     */\r\n    public void disableChecked() {\r\n        if (mMode == MODE_SCREEN_OFF) {\r\n            return;\r\n        }\r\n\r\n        disable();\r\n    }\r\n\r\n    public void enable() {\r\n        if (mIsBlocked) {\r\n            Log.d(TAG, \"Screensaver blocked!\");\r\n            return;\r\n        }\r\n\r\n        Log.d(TAG, \"Enable screensaver\");\r\n\r\n        disable();\r\n        int delayMs = getGeneralData().getScreensaverTimeoutMs() == GeneralData.SCREENSAVER_TIMEOUT_NEVER ?\r\n                10_000 :\r\n                getGeneralData().getScreensaverTimeoutMs();\r\n        Utils.postDelayed(mDimScreen, delayMs);\r\n    }\r\n\r\n    public void disable() {\r\n        if (mIsBlocked) {\r\n            Log.d(TAG, \"Screensaver blocked!\");\r\n            return;\r\n        }\r\n\r\n        Log.d(TAG, \"Disable screensaver\");\r\n        mMode = MODE_SCREENSAVER;\r\n        Utils.removeCallbacks(mDimScreen);\r\n        Utils.postDelayed(mUndimScreen, 0);\r\n    }\r\n\r\n    public void doScreenOff() {\r\n        //if (mIsScreenOff) {\r\n        //    return;\r\n        //}\r\n\r\n        disable();\r\n        mMode = MODE_SCREEN_OFF;\r\n        Utils.postDelayed(mDimScreen, 0);\r\n    }\r\n\r\n    public boolean isScreenOff() {\r\n        return mIsScreenOff;\r\n    }\r\n\r\n    public void setBlocked(boolean blocked) {\r\n        mIsBlocked = blocked;\r\n    }\r\n\r\n    private void enableTimeout() {\r\n        // Playing the video and dialog overlay isn't shown\r\n        if (getViewManager().getTopView() != PlaybackView.class || !getTweaksData().isScreenOffTimeoutEnabled()) {\r\n            disableTimeout();\r\n            return;\r\n        }\r\n\r\n        Log.d(TAG, \"Starting auto hide ui timer...\");\r\n        disableTimeout();\r\n        Utils.postDelayed(mTimeoutHandler, getTweaksData().getScreenOffTimeoutSec() * 1_000L);\r\n    }\r\n\r\n    private void disableTimeout() {\r\n        Log.d(TAG, \"Stopping auto hide ui timer...\");\r\n        Utils.removeCallbacks(mTimeoutHandler);\r\n    }\r\n\r\n    private void dimScreen() {\r\n        showHide(true);\r\n    }\r\n\r\n    private void undimScreen() {\r\n        showHide(false);\r\n    }\r\n\r\n    private void showHide(boolean show) {\r\n        showHideScreensaver(show);\r\n        showHideDimming(show);\r\n    }\r\n\r\n    private void showHideDimming(boolean show) {\r\n        Activity activity = mActivity.get();\r\n        View dimContainer = mDimContainer.get();\r\n\r\n        if (activity == null || dimContainer == null) {\r\n            return;\r\n        }\r\n\r\n        if (!show) {\r\n            enableTimeout();\r\n        }\r\n\r\n        // Disable dimming on certain circumstances\r\n        if (show && mMode == MODE_SCREENSAVER &&\r\n                (       isPlaying() ||\r\n                        isSigning() ||\r\n                        getGeneralData().getScreensaverTimeoutMs() == GeneralData.SCREENSAVER_TIMEOUT_NEVER\r\n                )\r\n        ) {\r\n            return;\r\n        }\r\n\r\n        int screenOffColor = Utils.getColor(activity, R.color.black, getTweaksData().getScreenOffDimmingPercents());\r\n        //int screenOffColorResId = getPlayerTweaksData().getScreenOffDimmingPercents() == 50 ? DIM_50 : DIM_100;\r\n        int screensaverColor = Utils.getColor(activity, R.color.black, getGeneralData().getScreensaverDimmingPercents());\r\n        //int screensaverColorResId = getGeneralData().getScreensaverMode() == GeneralData.SCREENSAVER_MODE_NORMAL ? DIM_50 : DIM_100;\r\n\r\n        dimContainer.setBackgroundColor(mMode == MODE_SCREENSAVER ? screensaverColor : screenOffColor);\r\n        //dimContainer.setBackgroundResource(mMode == MODE_SCREENSAVER ? screensaverColorResId : screenOffColorResId);\r\n        dimContainer.setVisibility(show ? View.VISIBLE : View.GONE);\r\n\r\n        mIsScreenOff = mMode == MODE_SCREEN_OFF && getTweaksData().getScreenOffDimmingPercents() == 100 && show;\r\n\r\n        if (mIsScreenOff) {\r\n            hidePlayerOverlay();\r\n        }\r\n\r\n        notifyRegistry();\r\n    }\r\n\r\n    private void showHideScreensaver(boolean show) {\r\n        if (sLockInstance) {\r\n            return;\r\n        }\r\n\r\n        Activity activity = mActivity.get();\r\n\r\n        if (activity == null) {\r\n            return;\r\n        }\r\n\r\n        // Disable screensaver on certain circumstances\r\n        // Fix screen off before the video started\r\n        if (show && (isPlaying() || isSigning() || getGeneralData().isScreensaverDisabled() || (mMode == MODE_SCREEN_OFF && getPosition() == 0))) {\r\n            Helpers.disableScreensaver(activity);\r\n            return;\r\n        }\r\n\r\n        if (show) {\r\n            Helpers.enableScreensaver(activity);\r\n        } else {\r\n            Helpers.disableScreensaver(activity);\r\n        }\r\n    }\r\n\r\n    private boolean isPlaying() {\r\n        Activity activity = mActivity.get();\r\n\r\n        if (activity == null) {\r\n            return false;\r\n        }\r\n\r\n        PlaybackView playbackView = PlaybackPresenter.instance(activity).getView();\r\n        return playbackView != null && playbackView.isPlaying();\r\n    }\r\n\r\n    private long getPosition() {\r\n        Activity activity = mActivity.get();\r\n\r\n        if (activity == null) {\r\n            return 0;\r\n        }\r\n\r\n        PlaybackView playbackView = PlaybackPresenter.instance(activity).getView();\r\n        // Fix screen off before the video started\r\n        return playbackView != null ? playbackView.getPositionMs() : 0;\r\n    }\r\n\r\n    private boolean isSigning() {\r\n        Activity activity = mActivity.get();\r\n\r\n        if (activity == null) {\r\n            return false;\r\n        }\r\n\r\n        return SignInPresenter.instance(activity).getView() != null || AddDevicePresenter.instance(activity).getView() != null;\r\n    }\r\n\r\n    private void hidePlayerOverlay() {\r\n        Activity activity = mActivity.get();\r\n\r\n        if (activity == null) {\r\n            return;\r\n        }\r\n\r\n        PlaybackView playbackView = PlaybackPresenter.instance(activity).getView();\r\n\r\n        if (playbackView != null) {\r\n            playbackView.showOverlay(false);\r\n        }\r\n    }\r\n\r\n    private void addToRegistry() {\r\n        sInstances.add(this);\r\n    }\r\n\r\n    private void notifyRegistry() {\r\n        if (sLockInstance) {\r\n            return;\r\n        }\r\n\r\n        sLockInstance = true;\r\n\r\n        sInstances.forEach(item -> {\r\n            if (item != this) {\r\n                item.disableChecked();\r\n            }\r\n        });\r\n\r\n        Utils.postDelayed(mUnlockInstance, 0);\r\n    }\r\n\r\n    private AppDialogPresenter getAppDialogPresenter() {\r\n        return AppDialogPresenter.instance(mActivity.get());\r\n    }\r\n\r\n    private ViewManager getViewManager() {\r\n        return ViewManager.instance(mActivity.get());\r\n    }\r\n\r\n    private PlayerTweaksData getTweaksData() {\r\n        return PlayerTweaksData.instance(mActivity.get());\r\n    }\r\n\r\n    private GeneralData getGeneralData() {\r\n        return GeneralData.instance(mActivity.get());\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/SharedPreferencesHelper.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.content.Context;\r\nimport android.content.SharedPreferences;\r\n\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\n\r\nimport org.w3c.dom.Document;\r\nimport org.w3c.dom.Element;\r\nimport org.w3c.dom.Node;\r\nimport org.w3c.dom.NodeList;\r\n\r\nimport java.io.File;\r\nimport java.io.FileOutputStream;\r\nimport java.io.InputStream;\r\nimport java.io.ObjectInputStream;\r\nimport java.io.ObjectOutputStream;\r\nimport java.util.Map;\r\n\r\nimport javax.xml.parsers.DocumentBuilder;\r\nimport javax.xml.parsers.DocumentBuilderFactory;\r\n\r\npublic class SharedPreferencesHelper {\r\n    private static final String TAG = SharedPreferencesHelper.class.getSimpleName();\r\n\r\n    // Backup SharedPreferences to a file\r\n    public static void backupSharedPrefs(Context context, File backupFile) {\r\n        try {\r\n            SharedPreferences prefs = context.getSharedPreferences(\"MyPrefs\", Context.MODE_PRIVATE);\r\n            Map<String, ?> allEntries = prefs.getAll();\r\n\r\n            try (FileOutputStream fos = new FileOutputStream(backupFile);\r\n                 ObjectOutputStream oos = new ObjectOutputStream(fos)) {\r\n                oos.writeObject(allEntries);\r\n            }\r\n\r\n            Log.d(TAG, \"Backup completed successfully.\");\r\n        } catch (Exception e) {\r\n            Log.e(TAG, \"Failed to backup SharedPreferences: \" + e.getMessage());\r\n        }\r\n    }\r\n\r\n    // Restore SharedPreferences from a backup file\r\n    @SuppressWarnings(\"unchecked\")\r\n    public static void restoreFromObj(Context context, InputStream backupFile, String preferenceName) {\r\n        if (preferenceName.endsWith(\".xml\")) {\r\n            preferenceName = preferenceName.replace(\".xml\", \"\");\r\n        }\r\n\r\n        try {\r\n            // Read the backup file\r\n            Map<String, ?> restoredData;\r\n            try (ObjectInputStream ois = new ObjectInputStream(backupFile)) {\r\n                //noinspection unchecked\r\n                restoredData = (Map<String, ?>) ois.readObject();\r\n            }\r\n\r\n            // Get SharedPreferences instance (creates a new one if it doesn't exist)\r\n            SharedPreferences prefs = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);\r\n            SharedPreferences.Editor editor = prefs.edit();\r\n\r\n            // Optional: Clear existing preferences before restoring\r\n            editor.clear();\r\n\r\n            // Restore key-value pairs to SharedPreferences\r\n            for (Map.Entry<String, ?> entry : restoredData.entrySet()) {\r\n                String key = entry.getKey();\r\n                Object value = entry.getValue();\r\n\r\n                if (value instanceof String) {\r\n                    editor.putString(key, (String) value);\r\n                } else if (value instanceof Integer) {\r\n                    editor.putInt(key, (Integer) value);\r\n                } else if (value instanceof Boolean) {\r\n                    editor.putBoolean(key, (Boolean) value);\r\n                } else if (value instanceof Float) {\r\n                    editor.putFloat(key, (Float) value);\r\n                } else if (value instanceof Long) {\r\n                    editor.putLong(key, (Long) value);\r\n                } else {\r\n                    Log.e(TAG, \"Unsupported data type: \" + key + \" -> \" + value);\r\n                }\r\n            }\r\n\r\n            // Apply the changes\r\n            editor.apply();\r\n            Log.d(TAG, \"SharedPreferences restored successfully.\");\r\n        } catch (Exception e) {\r\n            Log.e(TAG, \"Failed to restore SharedPreferences: \" + e.getMessage());\r\n        }\r\n    }\r\n\r\n    // Restore SharedPreferences from an XML file\r\n    public static void restoreFromXml(Context context, InputStream backupFile, String preferenceName) {\r\n        if (preferenceName.endsWith(\".xml\")) {\r\n            preferenceName = preferenceName.replace(\".xml\", \"\");\r\n        }\r\n\r\n        try {\r\n            // Parse the XML file\r\n            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();\r\n            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();\r\n            Document doc = dBuilder.parse(backupFile);\r\n            doc.getDocumentElement().normalize();\r\n\r\n            // Get SharedPreferences editor\r\n            SharedPreferences prefs = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);\r\n            SharedPreferences.Editor editor = prefs.edit();\r\n            editor.clear(); // Optional: Clear existing preferences\r\n\r\n            NodeList mapNode = doc.getElementsByTagName(\"map\");\r\n\r\n            if (mapNode.getLength() == 0) {\r\n                throw new IllegalStateException(\"Shared prefs format doesn't contain map item\");\r\n            }\r\n\r\n            if (mapNode.getLength() > 1) {\r\n                throw new IllegalStateException(\"Shared prefs contains more than one map item\");\r\n            }\r\n\r\n            // Iterate over <entry> elements\r\n            NodeList nodeList = mapNode.item(0).getChildNodes();\r\n            for (int i = 0; i < nodeList.getLength(); i++) {\r\n                Node item = nodeList.item(i);\r\n\r\n                if (item.getNodeType() != Node.ELEMENT_NODE) {\r\n                    continue;\r\n                }\r\n\r\n                Element element = (Element) item;\r\n                String key = element.getAttribute(\"name\");\r\n                String type = element.getNodeName();\r\n                String value = \"string\".equals(type) ? element.getTextContent() : element.getAttribute(\"value\");\r\n\r\n                // Restore the value based on its type\r\n                switch (type) {\r\n                    case \"string\":\r\n                        editor.putString(key, value);\r\n                        break;\r\n                    case \"int\":\r\n                        editor.putInt(key, Integer.parseInt(value));\r\n                        break;\r\n                    case \"boolean\":\r\n                        editor.putBoolean(key, Boolean.parseBoolean(value));\r\n                        break;\r\n                    case \"float\":\r\n                        editor.putFloat(key, Float.parseFloat(value));\r\n                        break;\r\n                    case \"long\":\r\n                        editor.putLong(key, Long.parseLong(value));\r\n                        break;\r\n                    default:\r\n                        Log.d(TAG, \"Unsupported data type: \" + type);\r\n                }\r\n            }\r\n\r\n            editor.apply();\r\n            Log.d(TAG, \"SharedPreferences restored successfully.\");\r\n        } catch (Exception e) {\r\n            Log.e(TAG, \"Failed to restore SharedPreferences: \" + e.getMessage());\r\n        }\r\n    }\r\n}\r\n\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/StreamReminderService.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemFormatInfo;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Playlist;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ViewManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.TickleManager.TickleListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\nimport io.reactivex.Observable;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class StreamReminderService implements TickleListener {\r\n    private static final String TAG = StreamReminderService.class.getSimpleName();\r\n    private static StreamReminderService sInstance;\r\n    private final MediaItemService mMediaItemService;\r\n    private final Context mContext;\r\n    private final GeneralData mGeneralData;\r\n    private Disposable mReminderAction;\r\n\r\n    private StreamReminderService(Context context) {\r\n        ServiceManager service = YouTubeServiceManager.instance();\r\n        mMediaItemService = service.getMediaItemService();\r\n        mContext = context.getApplicationContext();\r\n        mGeneralData = GeneralData.instance(context);\r\n    }\r\n\r\n    public static StreamReminderService instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new StreamReminderService(context);\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public boolean isReminderSet(Video video) {\r\n        return mGeneralData.containsPendingStream(video);\r\n    }\r\n\r\n    public void toggleReminder(Video video) {\r\n        if (video.videoId == null || !video.isUpcoming) {\r\n            return;\r\n        }\r\n\r\n        if (mGeneralData.containsPendingStream(video)) {\r\n            mGeneralData.removePendingStream(video);\r\n        } else {\r\n            mGeneralData.addPendingStream(video);\r\n        }\r\n\r\n        start();\r\n    }\r\n\r\n    public void start() {\r\n        if (mGeneralData.getPendingStreams().isEmpty()) {\r\n            TickleManager.instance().removeListener(this);\r\n            sInstance = null;\r\n        } else {\r\n            TickleManager.instance().addListener(this);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onTickle() {\r\n        if (mGeneralData.getPendingStreams().isEmpty()) {\r\n            start();\r\n            return;\r\n        }\r\n\r\n        RxHelper.disposeActions(mReminderAction);\r\n\r\n        List<Observable<MediaItemFormatInfo>> observables = toObservables();\r\n\r\n        mReminderAction = Observable.mergeDelayError(observables)\r\n                .subscribe(\r\n                        this::processMetadata,\r\n                        error -> Log.e(TAG, \"loadMetadata error: %s\", error.getMessage())\r\n                );\r\n    }\r\n\r\n    private void processMetadata(MediaItemFormatInfo formatInfo) {\r\n        String videoId = formatInfo.getVideoId();\r\n        if (formatInfo.containsMedia() && videoId != null) {\r\n            Video video = new Video();\r\n            video.title = formatInfo.getTitle();\r\n            video.videoId = videoId;\r\n            video.isPending = true;\r\n\r\n            Playlist playlist = Playlist.instance();\r\n            Video current = playlist.getCurrent();\r\n\r\n            if (current != null && current.isPending && ViewManager.instance(mContext).isPlayerInForeground()) {\r\n                playlist.add(video);\r\n            } else {\r\n                ViewManager.instance(mContext).movePlayerToForeground();\r\n                PlaybackPresenter.instance(mContext).openVideo(video);\r\n                MessageHelpers.showLongMessage(mContext, R.string.starting_stream);\r\n            }\r\n\r\n            mGeneralData.removePendingStream(video);\r\n            start();\r\n        }\r\n    }\r\n\r\n    /**\r\n     * NOTE: don't use MediaItemMetadata because it has contains isLive and isUpcoming flags\r\n     */\r\n    private List<Observable<MediaItemFormatInfo>> toObservables() {\r\n        List<Observable<MediaItemFormatInfo>> result = new ArrayList<>();\r\n\r\n        for (Video item : mGeneralData.getPendingStreams()) {\r\n            result.add(mMediaItemService.getFormatInfoObserve(item.videoId));\r\n        }\r\n\r\n        return result;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/TickleManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.sharedutils.misc.WeakHashSet;\r\n\r\npublic class TickleManager {\r\n    private static final String TAG = TickleManager.class.getSimpleName();\r\n    private static TickleManager sInstance;\r\n    private final Runnable mUpdateHandler = this::updateTickle;\r\n    // Usually listener is a view. So use weak refs to not hold it forever.\r\n    private final WeakHashSet<TickleListener> mListeners = new WeakHashSet<>();\r\n    private boolean mIsEnabled = true;\r\n\r\n    public interface TickleListener {\r\n        void onTickle();\r\n    }\r\n\r\n    private TickleManager() {\r\n    }\r\n\r\n    public static TickleManager instance() {\r\n        if (sInstance == null) {\r\n            sInstance = new TickleManager();\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void addListener(TickleListener listener) {\r\n        if (mListeners.add(listener)) {\r\n            if (mListeners.size() == 1) { // periodic callback not started yet\r\n                updateTickle();\r\n            } else if (isEnabled()) {\r\n                listener.onTickle(); // first run\r\n            }\r\n        }\r\n    }\r\n\r\n    public void removeListener(TickleListener listener) {\r\n        mListeners.remove(listener);\r\n    }\r\n\r\n    public void setEnabled(boolean enabled) {\r\n        mIsEnabled = enabled;\r\n        updateTickle();\r\n    }\r\n\r\n    public boolean isEnabled() {\r\n        return mIsEnabled;\r\n    }\r\n\r\n    public void clear() {\r\n        mListeners.clear();\r\n        updateTickle();\r\n    }\r\n\r\n    public void runTask(Runnable task, long delayMs) {\r\n        Utils.removeCallbacks(task);\r\n        Utils.postDelayed(task, delayMs);\r\n    }\r\n\r\n    private void updateTickle() {\r\n        Utils.removeCallbacks(mUpdateHandler);\r\n\r\n        if (isEnabled() && !mListeners.isEmpty()) {\r\n            mListeners.forEach(TickleListener::onTickle);\r\n\r\n            // Align tickle by clock minutes\r\n            long timeMillis = System.currentTimeMillis();\r\n            long delayMs = 60_000 - timeMillis % 60_000;\r\n            Log.d(TAG, \"Updating tickle in %s ms...\", delayMs);\r\n            Utils.postDelayed(mUpdateHandler, delayMs);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/UnlocalizedTitleProcessor.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport android.content.Context;\r\nimport android.util.Pair;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.mediaserviceinterfaces.ServiceManager;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.VideoGroup;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.common.DataChangeBase.OnDataChange;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeServiceManager;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\nimport io.reactivex.Observable;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\npublic class UnlocalizedTitleProcessor implements OnDataChange, BrowseProcessor {\r\n    private static final String TAG = UnlocalizedTitleProcessor.class.getSimpleName();\r\n    private final OnItemReady mOnItemReady;\r\n    private final MediaItemService mItemService;\r\n    private final MainUIData mMainUIData;\r\n    private boolean mIsUnlocalizedTitlesEnabled;\r\n    private Disposable mResult;\r\n\r\n    public UnlocalizedTitleProcessor(Context context, OnItemReady onItemReady) {\r\n        mOnItemReady = onItemReady;\r\n        ServiceManager service = YouTubeServiceManager.instance();\r\n        mItemService = service.getMediaItemService();\r\n        mMainUIData = MainUIData.instance(context);\r\n        mMainUIData.setOnChange(this);\r\n        initData();\r\n    }\r\n\r\n    @Override\r\n    public void onDataChange() {\r\n        initData();\r\n    }\r\n\r\n    private void initData() {\r\n        mIsUnlocalizedTitlesEnabled = mMainUIData.isUnlocalizedTitlesEnabled();\r\n    }\r\n\r\n    @Override\r\n    public void process(VideoGroup videoGroup) {\r\n        if (!mIsUnlocalizedTitlesEnabled || videoGroup == null || videoGroup.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        List<String> videoIds = getVideoIds(videoGroup);\r\n        mResult = Observable.fromIterable(videoIds)\r\n                .flatMap(videoId -> mItemService.getUnlocalizedTitleObserve(videoId)\r\n                        .map(newTitle -> new Pair<>(videoId, newTitle)))\r\n                .subscribe(title -> {\r\n                    Video video = videoGroup.findVideoById(title.first);\r\n                    if (video == null || Helpers.equals(video.title, title.second)) {\r\n                        return;\r\n                    }\r\n                    video.deArrowTitle = title.second;\r\n                    mOnItemReady.onItemReady(video);\r\n                },\r\n                error -> {\r\n                    Log.d(TAG, \"Unlocalized title: Cannot process the video\");\r\n                });\r\n    }\r\n\r\n    @Override\r\n    public void dispose() {\r\n        RxHelper.disposeActions(mResult);\r\n    }\r\n\r\n    private List<String> getVideoIds(VideoGroup videoGroup) {\r\n        List<String> result = new ArrayList<>();\r\n\r\n        for (Video video : videoGroup.getVideos()) {\r\n            if (video.deArrowProcessed) {\r\n                continue;\r\n            }\r\n            video.deArrowProcessed = true;\r\n            result.add(video.videoId);\r\n        }\r\n\r\n        return result;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/ZipHelper.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\n\r\nimport java.io.BufferedInputStream;\r\nimport java.io.BufferedOutputStream;\r\nimport java.io.File;\r\nimport java.io.FileInputStream;\r\nimport java.io.FileOutputStream;\r\nimport java.io.IOException;\r\nimport java.util.zip.ZipEntry;\r\nimport java.util.zip.ZipInputStream;\r\nimport java.util.zip.ZipOutputStream;\r\n\r\npublic class ZipHelper {\r\n    public static boolean zipFolder(File sourceFolder, File zipFile, String[] backupPatterns) {\r\n        try (ZipOutputStream zipOut = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFile)))) {\r\n            zipFolderRecursive(sourceFolder, sourceFolder, zipOut, backupPatterns);\r\n            return true;\r\n        } catch (IOException e) {\r\n            e.printStackTrace();\r\n            return false;\r\n        }\r\n    }\r\n\r\n    private static void zipFolderRecursive(File rootFolder, File currentFile, ZipOutputStream zipOut, String[] backupPatterns) throws IOException {\r\n        String entryName = rootFolder.toURI().relativize(currentFile.toURI()).getPath();\r\n\r\n        if (currentFile.isDirectory()) {\r\n            if (!entryName.isEmpty()) {\r\n                zipOut.putNextEntry(new ZipEntry(entryName + \"/\"));\r\n                zipOut.closeEntry();\r\n            }\r\n            File[] children = currentFile.listFiles();\r\n            if (children != null) {\r\n                for (File child : children) {\r\n                    if (Helpers.endsWithAny(child.getName(), backupPatterns))\r\n                        zipFolderRecursive(rootFolder, child, zipOut, backupPatterns);\r\n                }\r\n            }\r\n        } else {\r\n            zipOut.putNextEntry(new ZipEntry(entryName));\r\n            try (FileInputStream input = new FileInputStream(currentFile)) {\r\n                byte[] buffer = new byte[1024];\r\n                int length;\r\n                while ((length = input.read(buffer)) >= 0) {\r\n                    zipOut.write(buffer, 0, length);\r\n                }\r\n            }\r\n            zipOut.closeEntry();\r\n        }\r\n    }\r\n\r\n    public static boolean unzipToFolder(File zipFile, File outputFolder) {\r\n        if (!outputFolder.exists()) {\r\n            outputFolder.mkdirs();\r\n        }\r\n\r\n        try (ZipInputStream zipIn = new ZipInputStream(new BufferedInputStream(new FileInputStream(zipFile)))) {\r\n            String canonicalOutputPath = outputFolder.getCanonicalPath() + File.separator;\r\n            ZipEntry entry;\r\n            while ((entry = zipIn.getNextEntry()) != null) {\r\n                File filePath = new File(outputFolder, entry.getName());\r\n\r\n                String canonicalFilePath = filePath.getCanonicalPath();\r\n\r\n                // Zip Slip protection\r\n                if (!canonicalFilePath.startsWith(canonicalOutputPath)) {\r\n                    throw new IOException(\"Blocked Zip Slip entry: \" + entry.getName());\r\n                }\r\n\r\n                if (entry.isDirectory()) {\r\n                    filePath.mkdirs();\r\n                } else {\r\n                    if (filePath.getParentFile() != null) {\r\n                        filePath.getParentFile().mkdirs();\r\n                    }\r\n                    try (FileOutputStream output = new FileOutputStream(filePath)) {\r\n                        byte[] buffer = new byte[1024];\r\n                        int length;\r\n                        while ((length = zipIn.read(buffer)) > 0) {\r\n                            output.write(buffer, 0, length);\r\n                        }\r\n                    }\r\n                }\r\n                zipIn.closeEntry();\r\n            }\r\n            return true;\r\n        } catch (IOException e) {\r\n            e.printStackTrace();\r\n            return false;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/ZipHelper2.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.misc;\r\n\r\nimport java.io.File;\r\nimport java.io.FileInputStream;\r\nimport java.io.FileOutputStream;\r\nimport java.io.IOException;\r\nimport java.util.zip.ZipEntry;\r\nimport java.util.zip.ZipInputStream;\r\nimport java.util.zip.ZipOutputStream;\r\n\r\npublic class ZipHelper2 {\r\n    public static void zipDirectory(File sourceDir, File zipFile) {\r\n        try {\r\n            ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile));\r\n            zipFileRecursive(zos, sourceDir, sourceDir.getName() + \"/\");\r\n            zos.close();\r\n        } catch (Exception e) { e.printStackTrace(); }\r\n    }\r\n\r\n    private static void zipFileRecursive(ZipOutputStream zos, File file, String base) throws Exception {\r\n        if (file.isDirectory()) {\r\n            File[] children = file.listFiles();\r\n            if (children != null) {\r\n                for (File child : children) {\r\n                    zipFileRecursive(zos, child, base + child.getName() + \"/\");\r\n                }\r\n            }\r\n        } else {\r\n            FileInputStream fis = new FileInputStream(file);\r\n            zos.putNextEntry(new ZipEntry(base.substring(0, base.length() -1))); // strip \"/\" at the end to mark as file\r\n            byte[] buf = new byte[8192];\r\n            int len;\r\n            while ((len = fis.read(buf)) > 0) zos.write(buf, 0, len);\r\n            fis.close();\r\n            zos.closeEntry();\r\n        }\r\n    }\r\n\r\n    public static void unzip(File zipFile, File targetRoot) {\r\n        try {\r\n            ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile));\r\n            String canonicalRoot = targetRoot.getCanonicalPath() + File.separator;\r\n            ZipEntry entry;\r\n            byte[] buffer = new byte[8192];\r\n\r\n            while ((entry = zis.getNextEntry()) != null) {\r\n                File out = new File(targetRoot, entry.getName());\r\n\r\n                String canonicalOut = out.getCanonicalPath();\r\n\r\n                // Zip Slip protection\r\n                if (!canonicalOut.startsWith(canonicalRoot)) {\r\n                    throw new IOException(\"Blocked Zip Slip entry: \" + entry.getName());\r\n                }\r\n\r\n                if (entry.isDirectory()) {\r\n                    out.mkdirs();\r\n                } else {\r\n                    out.getParentFile().mkdirs();\r\n                    FileOutputStream fos = new FileOutputStream(out);\r\n                    int len;\r\n                    while ((len = zis.read(buffer)) > 0) fos.write(buffer, 0, len);\r\n                    fos.close();\r\n                }\r\n                zis.closeEntry();\r\n            }\r\n            zis.close();\r\n        } catch (Exception e) { e.printStackTrace(); }\r\n    }\r\n\r\n    public static boolean hasRootDir(File zipFile, String rootDir) {\r\n        boolean result = false;\r\n\r\n        try {\r\n            ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile));\r\n            ZipEntry entry;\r\n\r\n            String prefix = rootDir + \"/\";\r\n\r\n            while ((entry = zis.getNextEntry()) != null) {\r\n                if (entry.getName().startsWith(prefix)) {\r\n                    zis.closeEntry();\r\n                    result = true;\r\n                    break;\r\n                }\r\n                zis.closeEntry();\r\n            }\r\n            zis.close();\r\n        } catch (Exception e) { e.printStackTrace(); }\r\n\r\n        return result;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/AccountsData.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport androidx.annotation.NonNull;\r\nimport com.liskovsoft.mediaserviceinterfaces.oauth.Account;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager.AccountChangeListener;\r\n\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\npublic class AccountsData implements AccountChangeListener {\r\n    private static final String ACCOUNTS_DATA = \"accounts_data\";\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static AccountsData sInstance;\r\n    private final Context mContext;\r\n    private final AppPrefs mAppPrefs;\r\n    private boolean mIsSelectAccountOnBootEnabled;\r\n    private boolean mIsPasswordAccepted;\r\n    private final Map<String, PasswordItem> mPasswords = new HashMap<>();\r\n\r\n    private static class PasswordItem {\r\n        public String accountName;\r\n        public String password;\r\n\r\n        public PasswordItem(String accountName, String password) {\r\n            this.accountName = accountName;\r\n            this.password = password;\r\n        }\r\n\r\n        public static PasswordItem fromString(String specs) {\r\n            String[] split = Helpers.splitObj(specs);\r\n\r\n            if (split == null || split.length != 2) {\r\n                return new PasswordItem(null, null);\r\n            }\r\n\r\n            return new PasswordItem(Helpers.parseStr(split[0]), Helpers.parseStr(split[1]));\r\n        }\r\n\r\n        @NonNull\r\n        @Override\r\n        public String toString() {\r\n            return Helpers.mergeObj(accountName, password);\r\n        }\r\n    }\r\n\r\n    private AccountsData(Context context) {\r\n        mContext = context;\r\n        mAppPrefs = AppPrefs.instance(mContext);\r\n        MediaServiceManager.instance().addAccountListener(this);\r\n        restoreState();\r\n    }\r\n\r\n    public static AccountsData instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new AccountsData(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void selectAccountOnBoot(boolean select) {\r\n        mIsSelectAccountOnBootEnabled = select;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isSelectAccountOnBootEnabled() {\r\n        return mIsSelectAccountOnBootEnabled;\r\n    }\r\n\r\n    public void setAccountPassword(String password) {\r\n        mPasswords.put(getAccountName(), new PasswordItem(getAccountName(), password));\r\n\r\n        persistState();\r\n    }\r\n\r\n    public String getAccountPassword() {\r\n        if (getAccountName() == null) {\r\n            return null;\r\n        }\r\n\r\n        PasswordItem passwordItem = mPasswords.get(getAccountName());\r\n\r\n        return passwordItem != null ? passwordItem.password : null;\r\n    }\r\n\r\n    public boolean isPasswordAccepted() {\r\n        return mIsPasswordAccepted || getAccountPassword() == null;\r\n    }\r\n\r\n    public void setPasswordAccepted(boolean accepted) {\r\n        mIsPasswordAccepted = accepted;\r\n    }\r\n\r\n    private void restoreState() {\r\n        String data = mAppPrefs.getData(ACCOUNTS_DATA);\r\n\r\n        String[] split = Helpers.splitData(data);\r\n\r\n        mIsSelectAccountOnBootEnabled = Helpers.parseBoolean(split, 0, false);\r\n        // mIsAccountProtectedWithPassword\r\n        // mAccountPassword\r\n        String[] passwords = Helpers.parseArray(split, 3);\r\n\r\n        if (passwords != null) {\r\n            for (String passwordSpec : passwords) {\r\n                PasswordItem item = PasswordItem.fromString(passwordSpec);\r\n                mPasswords.put(item.accountName, item);\r\n            }\r\n        }\r\n    }\r\n\r\n    private void persistState() {\r\n        mAppPrefs.setData(ACCOUNTS_DATA, Helpers.mergeData(\r\n                mIsSelectAccountOnBootEnabled, null, null, Helpers.mergeArray(mPasswords.values().toArray())\r\n        ));\r\n    }\r\n\r\n    private String getAccountName() {\r\n        Account account = MediaServiceManager.instance().getSelectedAccount();\r\n        return account != null ? account.getName() : null;\r\n    }\r\n\r\n    @Override\r\n    public void onAccountChanged(Account account) {\r\n        mIsPasswordAccepted = false;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/AppPrefs.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport android.text.TextUtils;\r\nimport com.liskovsoft.mediaserviceinterfaces.oauth.Account;\r\nimport com.liskovsoft.sharedutils.misc.WeakHashSet;\r\nimport com.liskovsoft.sharedutils.prefs.SharedPreferencesBase;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.service.SidebarService;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager.AccountChangeListener;\r\n\r\nimport java.util.HashMap;\r\nimport java.util.Map;\r\n\r\npublic class AppPrefs extends SharedPreferencesBase implements AccountChangeListener {\r\n    private static final String TAG = AppPrefs.class.getSimpleName();\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static AppPrefs sInstance;\r\n    private static final String ANONYMOUS_PROFILE_NAME = \"anonymous\";\r\n    private static final String MULTI_PROFILES = \"multi_profiles\";\r\n    private static final String STATE_UPDATER_DATA = \"state_updater_data\";\r\n    private static final String CHANNEL_GROUP_DATA = \"channel_group_data\";\r\n    private static final String SIDEBAR_DATA = \"sidebar_data\";\r\n    private static final String VIEW_MANAGER_DATA = \"view_manager_data\";\r\n    private static final String WEB_PROXY_URI = \"web_proxy_uri\";\r\n    private static final String WEB_PROXY_ENABLED = \"web_proxy_enabled\";\r\n    private static final String LAST_PROFILE_NAME = \"last_profile_name\";\r\n    private String mBootResolution;\r\n    private final Map<String, Integer> mDataHashes = new HashMap<>();\r\n    private final WeakHashSet<ProfileChangeListener> mListeners = new WeakHashSet<>();\r\n\r\n    public interface ProfileChangeListener {\r\n        void onProfileChanged();\r\n    }\r\n\r\n    private AppPrefs(Context context) {\r\n        super(context, R.xml.app_prefs);\r\n\r\n        initProfiles();\r\n    }\r\n\r\n    private void initProfiles() {\r\n        MediaServiceManager.instance().addAccountListener(this);\r\n    }\r\n\r\n    @Override\r\n    public void onAccountChanged(Account account) {\r\n        selectProfile(account);\r\n        onProfileChanged();\r\n    }\r\n\r\n    public static AppPrefs instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new AppPrefs(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void enableMultiProfiles(boolean enabled) {\r\n        if (isMultiProfilesEnabled() == enabled) {\r\n            return;\r\n        }\r\n\r\n        putBoolean(MULTI_PROFILES, enabled);\r\n        onProfileChanged();\r\n        //selectAccount(enabled ? MediaServiceManager.instance().getSelectedAccount() : null);\r\n    }\r\n\r\n    public boolean isMultiProfilesEnabled() {\r\n        return getBoolean(MULTI_PROFILES, false);\r\n    }\r\n\r\n    public void setBootResolution(String resolution) {\r\n        mBootResolution = resolution;\r\n    }\r\n\r\n    public String getBootResolution() {\r\n        return mBootResolution;\r\n    }\r\n\r\n    public String getStateUpdaterData() {\r\n        // Always use multiple profiles for the history\r\n        return getData(getProfileKey(STATE_UPDATER_DATA, true));\r\n    }\r\n\r\n    public void setStateUpdaterData(String data) {\r\n        // Always use multiple profiles for the history\r\n        setData(getProfileKey(STATE_UPDATER_DATA, true), data);\r\n    }\r\n\r\n    public String getChannelGroupData() {\r\n        // Always use multiple profiles\r\n        return getData(getProfileKey(CHANNEL_GROUP_DATA, true));\r\n    }\r\n\r\n    public void setChannelGroupData(String data) {\r\n        // Always use multiple profiles\r\n        setData(getProfileKey(CHANNEL_GROUP_DATA, true), data);\r\n    }\r\n\r\n    public String getSidebarData() {\r\n        // Always use multiple profiles\r\n        return getData(getProfileKey(SIDEBAR_DATA, true));\r\n    }\r\n\r\n    public void setSidebarData(String data) {\r\n        // Always use multiple profiles\r\n        setData(getProfileKey(SIDEBAR_DATA, true), data);\r\n    }\r\n\r\n    public void setProfileData(String key, String data) {\r\n        setData(getProfileKey(key, isMultiProfilesEnabled()), data);\r\n    }\r\n\r\n    public String getProfileData(String key) {\r\n        //String data = getData(getProfileKey(key, isMultiProfilesEnabled()));\r\n\r\n        // Fallback to non-profile settings\r\n        //return data != null ? data : getData(key);\r\n\r\n        return getData(getProfileKey(key, isMultiProfilesEnabled()));\r\n    }\r\n\r\n    public void setData(String key, String data) {\r\n        if (checkData(key, data)) {\r\n            putString(key, data);\r\n        }\r\n    }\r\n\r\n    public String getData(String key) {\r\n        // Don't sync hash here. Hashes won't match.\r\n        return getString(key, null);\r\n    }\r\n\r\n    public String getWebProxyUri() {\r\n        return getString(WEB_PROXY_URI, \"\");\r\n    }\r\n\r\n    public void setWebProxyUri(String uri) {\r\n        putString(WEB_PROXY_URI, uri);\r\n    }\r\n\r\n    public boolean isWebProxyEnabled() {\r\n        return getBoolean(WEB_PROXY_ENABLED, false);\r\n    }\r\n\r\n    public void setWebProxyEnabled(boolean enabled) {\r\n        putBoolean(WEB_PROXY_ENABLED, enabled);\r\n    }\r\n\r\n    private void setProfileName(String profileName) {\r\n        putString(LAST_PROFILE_NAME, profileName);\r\n    }\r\n\r\n    private String getProfileName() {\r\n        return getString(LAST_PROFILE_NAME, null);\r\n    }\r\n\r\n    private void selectProfile(Account account) {\r\n        String profileName = account != null && account.getName() != null ? account.getName().replace(\" \", \"_\") : ANONYMOUS_PROFILE_NAME;\r\n\r\n        setProfileName(profileName);\r\n    }\r\n\r\n    private void onProfileChanged() {\r\n        mListeners.forEach(ProfileChangeListener::onProfileChanged);\r\n    }\r\n\r\n    public void addListener(ProfileChangeListener listener) {\r\n        if (!mListeners.contains(listener)) {\r\n            if (listener instanceof GeneralData) {\r\n                mListeners.add(0, listener); // data classes should be called before regular listeners\r\n            } else if (listener instanceof SidebarService) {\r\n                mListeners.add(mListeners.isEmpty() ? 0 : 1, listener); // data classes should be called before regular listeners\r\n            } else {\r\n                mListeners.add(listener);\r\n            }\r\n        }\r\n    }\r\n\r\n    public void removeListener(ProfileChangeListener listener) {\r\n        mListeners.remove(listener);\r\n    }\r\n\r\n    /**\r\n     * Check that the data has been modified.\r\n     */\r\n    private boolean checkData(String key, String data) {\r\n        Integer oldHashCode = mDataHashes.get(key);\r\n        int newHashCode = data != null ? data.hashCode() : -1;\r\n\r\n        if (oldHashCode != null && oldHashCode == newHashCode) {\r\n            return false;\r\n        }\r\n\r\n        mDataHashes.put(key, newHashCode);\r\n\r\n        return true;\r\n    }\r\n\r\n    //private String getProfileKey(String key) {\r\n    //    String profileName = getProfileName();\r\n    //    if (!TextUtils.isEmpty(profileName)) {\r\n    //        key = profileName + \"_\" + key;\r\n    //    }\r\n    //\r\n    //    return key;\r\n    //}\r\n\r\n    private String getProfileKey(String key, boolean isMultiProfilesEnabled) {\r\n        String profileName = getProfileName();\r\n        if (!TextUtils.isEmpty(profileName) && isMultiProfilesEnabled) {\r\n            key = profileName + \"_\" + key;\r\n        }\r\n\r\n        return key;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/BlockedChannelData.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs.ProfileChangeListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Collections;\r\nimport java.util.HashSet;\r\nimport java.util.List;\r\nimport java.util.Map;\r\nimport java.util.Set;\r\n\r\npublic class BlockedChannelData implements ProfileChangeListener {\r\n    private static final String BLOCKED_CHANNEL_DATA = \"blocked_channel_data\";\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static BlockedChannelData sInstance;\r\n    private final AppPrefs mPrefs;\r\n    private Set<String> mChannelIds;\r\n    private Map<String, String> mChannelIdsWithNames;\r\n    private final Runnable mPersistStateInt = this::persistStateInt;\r\n    private final List<BlockedChannelListener> mListeners = new ArrayList<>();\r\n\r\n    public interface BlockedChannelListener {\r\n        void onChanged();\r\n    }\r\n\r\n    private BlockedChannelData(Context context) {\r\n        mPrefs = AppPrefs.instance(context);\r\n        mPrefs.addListener(this);\r\n        restoreState();\r\n    }\r\n\r\n    public static BlockedChannelData instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new BlockedChannelData(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    /**\r\n     * Add a channel to the blacklist\r\n     *\r\n     * @param channelId   The channel ID to blacklist\r\n     * @param channelName The channel name (optional, for display purposes)\r\n     */\r\n    public void addChannel(String channelId, String channelName) {\r\n        if (channelId == null || channelId.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        mChannelIds.add(channelId);\r\n        if (channelName != null && !channelName.isEmpty()) {\r\n            mChannelIdsWithNames.put(channelId, channelName);\r\n        }\r\n        persistState();\r\n        notifyListeners();\r\n    }\r\n\r\n    /**\r\n     * Remove a channel from the blacklist\r\n     *\r\n     * @param channelId The channel ID to remove\r\n     */\r\n    public void removeChannel(String channelId) {\r\n        if (channelId == null || channelId.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        mChannelIds.remove(channelId);\r\n        mChannelIdsWithNames.remove(channelId);\r\n        persistState();\r\n        notifyListeners();\r\n    }\r\n\r\n    /**\r\n     * Check if a channel is blacklisted\r\n     *\r\n     * @param channelId The channel ID to check\r\n     * @return true if the channel is blacklisted\r\n     */\r\n    public boolean containsChannel(String channelId) {\r\n        if (channelId == null || channelId.isEmpty()) {\r\n            return false;\r\n        }\r\n\r\n        return mChannelIds.contains(channelId);\r\n    }\r\n\r\n    /**\r\n     * Get all blacklisted channel IDs\r\n     *\r\n     * @return Unmodifiable set of blacklisted channel IDs\r\n     */\r\n    public Set<String> getChannelIds() {\r\n        return Collections.unmodifiableSet(mChannelIds);\r\n    }\r\n\r\n    /**\r\n     * Get the name of a blacklisted channel\r\n     *\r\n     * @param channelId The channel ID\r\n     * @return The channel name, or null if not available\r\n     */\r\n    public String getChannelName(String channelId) {\r\n        return mChannelIdsWithNames.get(channelId);\r\n    }\r\n\r\n    /**\r\n     * Get all blacklisted channels with their names\r\n     *\r\n     * @return Unmodifiable map of channelId -> channel name\r\n     */\r\n    public Map<String, String> getChannelIdsWithNames() {\r\n        return Collections.unmodifiableMap(mChannelIdsWithNames);\r\n    }\r\n\r\n    /**\r\n     * Get the count of blacklisted channels\r\n     *\r\n     * @return Number of blacklisted channels\r\n     */\r\n    public int getChannelCount() {\r\n        return mChannelIds.size();\r\n    }\r\n\r\n    /**\r\n     * Clear all blacklisted channels\r\n     */\r\n    public void clear() {\r\n        mChannelIds.clear();\r\n        mChannelIdsWithNames.clear();\r\n        persistState();\r\n    }\r\n\r\n    private synchronized void restoreState() {\r\n        String data = mPrefs.getProfileData(BLOCKED_CHANNEL_DATA);\r\n\r\n        String[] split = Helpers.splitData(data);\r\n\r\n        List<String> channelIdList = Helpers.parseStrList(split, 0);\r\n        mChannelIdsWithNames = Helpers.parseMap(split, 1, Helpers::parseStr, Helpers::parseStr);\r\n\r\n        mChannelIds = new HashSet<>(channelIdList);\r\n    }\r\n\r\n    public void persistNow() {\r\n        Utils.post(mPersistStateInt);\r\n    }\r\n\r\n    private void persistState() {\r\n        Utils.postDelayed(mPersistStateInt, 10_000);\r\n    }\r\n\r\n    private void persistStateInt() {\r\n        // Convert Set to List for persistence\r\n        List<String> channelIdList = new ArrayList<>(mChannelIds);\r\n        mPrefs.setProfileData(BLOCKED_CHANNEL_DATA, Helpers.mergeData(\r\n                channelIdList, mChannelIdsWithNames));\r\n    }\r\n\r\n    public void addListener(BlockedChannelListener listener) {\r\n        if (!mListeners.contains(listener)) {\r\n            mListeners.add(listener);\r\n        }\r\n    }\r\n\r\n    public void removeListener(BlockedChannelListener listener) {\r\n        mListeners.remove(listener);\r\n    }\r\n\r\n    private void notifyListeners() {\r\n        for (BlockedChannelListener listener : mListeners) {\r\n            listener.onChanged();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onProfileChanged() {\r\n        restoreState();\r\n    }\r\n}"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/DeArrowData.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs;\r\n\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.common.DataSaverBase;\r\n\r\npublic class DeArrowData extends DataSaverBase {\r\n    private static DeArrowData sInstance;\r\n\r\n    private DeArrowData(Context context) {\r\n        super(context);\r\n    }\r\n\r\n    public static DeArrowData instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new DeArrowData(context);\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public boolean isDeArrowEnabled() {\r\n        return getBoolean(0);\r\n    }\r\n\r\n    public void setDeArrowEnabled(boolean enable) {\r\n        setBoolean(0, enable);\r\n    }\r\n\r\n    public boolean isReplaceTitlesEnabled() {\r\n        return getBoolean(1);\r\n    }\r\n\r\n    public void setReplaceTitlesEnabled(boolean enable) {\r\n        setBoolean(1, enable);\r\n    }\r\n\r\n    public boolean isReplaceThumbnailsEnabled() {\r\n        return getBoolean(2);\r\n    }\r\n\r\n    public void setReplaceThumbnailsEnabled(boolean replace) {\r\n        setBoolean(2, replace);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/GeneralData.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.prefs.GlobalPreferences;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs.ProfileChangeListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.Collections;\r\nimport java.util.List;\r\nimport java.util.Map;\r\n\r\npublic class GeneralData implements ProfileChangeListener {\r\n    public static final int SCREENSAVER_TIMEOUT_NEVER = 0;\r\n    private static final String GENERAL_DATA = \"general_data\";\r\n    public static final int EXIT_NONE = 0;\r\n    public static final int EXIT_DOUBLE_BACK = 1;\r\n    public static final int EXIT_SINGLE_BACK = 2;\r\n    public static final int BACKGROUND_PLAYBACK_SHORTCUT_HOME = 0;\r\n    public static final int BACKGROUND_PLAYBACK_SHORTCUT_HOME_BACK = 1;\r\n    public static final int BACKGROUND_PLAYBACK_SHORTCUT_BACK = 2;\r\n    public static final int HISTORY_AUTO = 0;\r\n    public static final int HISTORY_ENABLED = 1;\r\n    public static final int HISTORY_DISABLED = 2;\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static GeneralData sInstance;\r\n    private final Context mContext;\r\n    private final AppPrefs mPrefs;\r\n    private int mAppExitShortcut;\r\n    private int mPlayerExitShortcut;\r\n    private int mSearchExitShortcut;\r\n    private boolean mIsReturnToLauncherEnabled;\r\n    private int mBackgroundShortcut;\r\n    private boolean mIsHideShortsFromSubscriptionsEnabled;\r\n    private boolean mIsHideUpcomingEnabled;\r\n    private boolean mIsRemapFastForwardToNextEnabled;\r\n    private int mScreensaverTimeoutMs;\r\n    private int mScreensaverDimmingPercents;\r\n    private boolean mIsProxyEnabled;\r\n    private boolean mIsBridgeCheckEnabled;\r\n    private boolean mIsOkButtonLongPressDisabled;\r\n    private String mLastPlaylistId;\r\n    private String mLastPlaylistTitle;\r\n    private boolean mIsRemapPageUpToNextEnabled;\r\n    private boolean mIsRemapPageUpToLikeEnabled;\r\n    private boolean mIsRemapChannelUpToNextEnabled;\r\n    private boolean mIsRemapChannelUpToLikeEnabled;\r\n    private boolean mIsRemapChannelUpToVolumeEnabled;\r\n    private boolean mIsRemapPageUpToSpeedEnabled;\r\n    private boolean mIsRemapPageDownToSpeedEnabled;\r\n    private boolean mIsRemapChannelUpToSpeedEnabled;\r\n    private boolean mIsRemapFastForwardToSpeedEnabled;\r\n    private boolean mIsRemapFastForwardToSpeedToggleEnabled;\r\n    private boolean mIsRemapNextToFastForwardEnabled;\r\n    private boolean mIsRemapNextToSpeedEnabled;\r\n    private boolean mIsRemapNumbersToSpeedEnabled;\r\n    private boolean mIsRemapPlayToOKEnabled;\r\n    private boolean mIsRemapChannelUpToSearchEnabled;\r\n    private boolean mIsHideShortsFromHomeEnabled;\r\n    private boolean mIsHideShortsFromHistoryEnabled;\r\n    private boolean mIsScreensaverDisabled;\r\n    private boolean mIsVPNEnabled;\r\n    private boolean mIsGlobalClockEnabled;\r\n    private String mSettingsPassword;\r\n    private String mMasterPassword;\r\n    private boolean mIsChildModeEnabled;\r\n    private boolean mIsHistoryEnabled;\r\n    private int mHistoryState;\r\n    private boolean mIsAltAppIconEnabled;\r\n    private int mVersionCode;\r\n    private boolean mIsSelectChannelSectionEnabled;\r\n    private boolean mIsOldUpdateNotificationsEnabled;\r\n    private boolean mIsRememberSubscriptionsPositionEnabled;\r\n    private boolean mIsRememberPinnedPositionEnabled;\r\n    private boolean mIsRemapDpadUpToSpeedEnabled;\r\n    private boolean mIsRemapDpadUpToVolumeEnabled;\r\n    private boolean mIsRemapDpadLeftToVolumeEnabled;\r\n    private boolean mIsHideWatchedFromNotificationsEnabled;\r\n    private List<String> mChangelog;\r\n    private Map<String, Integer> mPlaylistOrder;\r\n    private List<Video> mPendingStreams;\r\n    private boolean mIsFullscreenModeEnabled;\r\n    private Map<Integer, Video> mSelectedItems;\r\n    private boolean mIsFirstUseTooltipEnabled;\r\n    private boolean mIsDeviceSpecificBackupEnabled;\r\n    private int mGDriveBackupFreqDays;\r\n    private int mLocalDriveBackupFreqDays;\r\n    private List<Video> mOldPinnedItems;\r\n    private boolean mIsRemapSToSpeedToggleEnabled;\r\n    private final Runnable mPersistStateInt = this::persistStateInt;\r\n\r\n    private GeneralData(Context context) {\r\n        mContext = context;\r\n        mPrefs = AppPrefs.instance(context);\r\n        mPrefs.addListener(this);\r\n        restoreState();\r\n    }\r\n\r\n    public static GeneralData instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new GeneralData(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public int getAppExitShortcut() {\r\n        return mAppExitShortcut;\r\n    }\r\n\r\n    public void setAppExitShortcut(int type) {\r\n        mAppExitShortcut = type;\r\n        persistState();\r\n    }\r\n\r\n    public int getPlayerExitShortcut() {\r\n        return mPlayerExitShortcut;\r\n    }\r\n\r\n    public void setPlayerExitShortcut(int type) {\r\n        mPlayerExitShortcut = type;\r\n        persistState();\r\n    }\r\n\r\n    public int getSearchExitShortcut() {\r\n        return mSearchExitShortcut;\r\n    }\r\n\r\n    public void setSearchExitShortcut(int type) {\r\n        mSearchExitShortcut = type;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isReturnToLauncherEnabled() {\r\n        return mIsReturnToLauncherEnabled;\r\n    }\r\n\r\n    public void setReturnToLauncherEnabled(boolean enable) {\r\n        mIsReturnToLauncherEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public int getBackgroundPlaybackShortcut() {\r\n        return mBackgroundShortcut;\r\n    }\r\n\r\n    public void setBackgroundPlaybackShortcut(int type) {\r\n        mBackgroundShortcut = type;\r\n        persistState();\r\n    }\r\n\r\n    public List<Video> getOldPinnedItems() {\r\n        return mOldPinnedItems;\r\n    }\r\n\r\n    public boolean isRememberSubscriptionsPositionEnabled() {\r\n        return mIsRememberSubscriptionsPositionEnabled;\r\n    }\r\n\r\n    public void setRememberSubscriptionsPositionEnabled(boolean enable) {\r\n        mIsRememberSubscriptionsPositionEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRememberPinnedPositionEnabled() {\r\n        return mIsRememberPinnedPositionEnabled;\r\n    }\r\n\r\n    public void setRememberPinnedPositionEnabled(boolean enable) {\r\n        mIsRememberPinnedPositionEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isHideWatchedFromNotificationsEnabled() {\r\n        return mIsHideWatchedFromNotificationsEnabled;\r\n    }\r\n\r\n    public void setHideWatchedFromNotificationsEnabled(boolean enable) {\r\n        mIsHideWatchedFromNotificationsEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isScreensaverDisabled() {\r\n        return mIsScreensaverDisabled;\r\n    }\r\n\r\n    public void setScreensaverDisabled(boolean disable) {\r\n        mIsScreensaverDisabled = disable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapFastForwardToNextEnabled() {\r\n        return mIsRemapFastForwardToNextEnabled;\r\n    }\r\n\r\n    public void setRemapFastForwardToNextEnabled(boolean enable) {\r\n        resetFastForwardSettings();\r\n        mIsRemapFastForwardToNextEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapFastForwardToSpeedEnabled() {\r\n        return mIsRemapFastForwardToSpeedEnabled;\r\n    }\r\n\r\n    public void setRemapFastForwardToSpeedEnabled(boolean enable) {\r\n        resetFastForwardSettings();\r\n        mIsRemapFastForwardToSpeedEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapFastForwardToSpeedToggleEnabled() {\r\n        return mIsRemapFastForwardToSpeedToggleEnabled;\r\n    }\r\n\r\n    public void setRemapFastForwardToSpeedToggleEnabled(boolean enable) {\r\n        resetFastForwardSettings();\r\n        mIsRemapFastForwardToSpeedToggleEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    private void resetFastForwardSettings() {\r\n        mIsRemapFastForwardToSpeedToggleEnabled = false;\r\n        mIsRemapFastForwardToSpeedEnabled = false;\r\n        mIsRemapFastForwardToNextEnabled = false;\r\n    }\r\n\r\n    public boolean isRemapNextToFastForwardEnabled() {\r\n        return mIsRemapNextToFastForwardEnabled;\r\n    }\r\n\r\n    public void setRemapNextToFastForwardEnabled(boolean enable) {\r\n        resetNextSettings();\r\n        mIsRemapNextToFastForwardEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapNextToSpeedEnabled() {\r\n        return mIsRemapNextToSpeedEnabled;\r\n    }\r\n\r\n    public void setRemapNextToSpeedEnabled(boolean enable) {\r\n        resetNextSettings();\r\n        mIsRemapNextToSpeedEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    private void resetNextSettings() {\r\n        mIsRemapNextToFastForwardEnabled = false;\r\n        mIsRemapNextToSpeedEnabled = false;\r\n    }\r\n\r\n    public boolean isRemapNumbersToSpeedEnabled() {\r\n        return mIsRemapNumbersToSpeedEnabled;\r\n    }\r\n\r\n    public void setRemapNumbersToSpeedEnabled(boolean enable) {\r\n        mIsRemapNumbersToSpeedEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapDpadUpToSpeedEnabled() {\r\n        return mIsRemapDpadUpToSpeedEnabled;\r\n    }\r\n\r\n    public void setRemapDpadUpDownToSpeedEnabled(boolean enable) {\r\n        resetDpadUpDownSettings();\r\n        mIsRemapDpadUpToSpeedEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapDpadUpToVolumeEnabled() {\r\n        return mIsRemapDpadUpToVolumeEnabled;\r\n    }\r\n\r\n    public void setRemapDpadUpToVolumeEnabled(boolean enable) {\r\n        resetDpadUpDownSettings();\r\n        mIsRemapDpadUpToVolumeEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public void resetDpadUpDownSettings() {\r\n        mIsRemapDpadUpToSpeedEnabled = false;\r\n        mIsRemapDpadUpToVolumeEnabled = false;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapDpadLeftToVolumeEnabled() {\r\n        return mIsRemapDpadLeftToVolumeEnabled;\r\n    }\r\n\r\n    public void setRemapDpadLeftToVolumeEnabled(boolean enable) {\r\n        resetDpadLeftRightSettings();\r\n        mIsRemapDpadLeftToVolumeEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public void resetDpadLeftRightSettings() {\r\n        mIsRemapDpadLeftToVolumeEnabled = false;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapPlayToOKEnabled() {\r\n        return mIsRemapPlayToOKEnabled;\r\n    }\r\n\r\n    public void setRemapPlayToOKEnabled(boolean enable) {\r\n        mIsRemapPlayToOKEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapPageUpToNextEnabled() {\r\n        return mIsRemapPageUpToNextEnabled;\r\n    }\r\n\r\n    public void setRemapPageUpToNextEnabled(boolean enable) {\r\n        resetPageUpSettings();\r\n        mIsRemapPageUpToNextEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapPageUpToLikeEnabled() {\r\n        return mIsRemapPageUpToLikeEnabled;\r\n    }\r\n\r\n    public void setRemapPageUpToLikeEnabled(boolean enable) {\r\n        resetPageUpSettings();\r\n        mIsRemapPageUpToLikeEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapPageUpToSpeedEnabled() {\r\n        return mIsRemapPageUpToSpeedEnabled;\r\n    }\r\n\r\n    public void setRemapPageUpToSpeedEnabled(boolean enable) {\r\n        resetPageUpSettings();\r\n        mIsRemapPageUpToSpeedEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapPageDownToSpeedEnabled() {\r\n        return mIsRemapPageDownToSpeedEnabled;\r\n    }\r\n\r\n    public void setRemapPageDownToSpeedEnabled(boolean enable) {\r\n        resetPageUpSettings();\r\n        mIsRemapPageDownToSpeedEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    private void resetPageUpSettings() {\r\n        mIsRemapPageDownToSpeedEnabled = false;\r\n        mIsRemapPageUpToSpeedEnabled = false;\r\n        mIsRemapPageUpToLikeEnabled = false;\r\n        mIsRemapPageUpToNextEnabled = false;\r\n    }\r\n\r\n    public boolean isRemapChannelUpToNextEnabled() {\r\n        return mIsRemapChannelUpToNextEnabled;\r\n    }\r\n\r\n    public void setRemapChannelUpToNextEnabled(boolean enable) {\r\n        resetChannelUpSettings();\r\n        mIsRemapChannelUpToNextEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapChannelUpToVolumeEnabled() {\r\n        return mIsRemapChannelUpToVolumeEnabled;\r\n    }\r\n\r\n    public void setRemapChannelUpToVolumeEnabled(boolean enable) {\r\n        resetChannelUpSettings();\r\n        mIsRemapChannelUpToVolumeEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapChannelUpToLikeEnabled() {\r\n        return mIsRemapChannelUpToLikeEnabled;\r\n    }\r\n\r\n    public void setRemapChannelUpToLikeEnabled(boolean enable) {\r\n        resetChannelUpSettings();\r\n        mIsRemapChannelUpToLikeEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapChannelUpToSpeedEnabled() {\r\n        return mIsRemapChannelUpToSpeedEnabled;\r\n    }\r\n\r\n    public void setRemapChannelUpToSpeedEnabled(boolean enable) {\r\n        resetChannelUpSettings();\r\n        mIsRemapChannelUpToSpeedEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapChannelUpToSearchEnabled() {\r\n        return mIsRemapChannelUpToSearchEnabled;\r\n    }\r\n\r\n    public void setRemapChannelUpToSearchEnabled(boolean enable) {\r\n        resetChannelUpSettings();\r\n        mIsRemapChannelUpToSearchEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    private void resetChannelUpSettings() {\r\n        mIsRemapChannelUpToVolumeEnabled = false;\r\n        mIsRemapChannelUpToNextEnabled = false;\r\n        mIsRemapChannelUpToSearchEnabled = false;\r\n        mIsRemapChannelUpToLikeEnabled = false;\r\n        mIsRemapChannelUpToSpeedEnabled = false;\r\n    }\r\n\r\n    public int getScreensaverTimeoutMs() {\r\n        return mScreensaverTimeoutMs;\r\n    }\r\n\r\n    public void setScreensaverTimeoutMs(int timeoutMs) {\r\n        mScreensaverTimeoutMs = timeoutMs;\r\n        persistState();\r\n    }\r\n\r\n    public int getScreensaverDimmingPercents() {\r\n        return mScreensaverDimmingPercents;\r\n    }\r\n\r\n    public void setScreensaverDimmingPercents(int percents) {\r\n        mScreensaverDimmingPercents = percents;\r\n        persistState();\r\n    }\r\n\r\n    public boolean is24HourLocaleEnabled() {\r\n        return GlobalPreferences.sInstance.is24HourLocaleEnabled();\r\n    }\r\n\r\n    public void set24HourLocaleEnabled(boolean enable) {\r\n        GlobalPreferences.sInstance.set24HourLocaleEnabled(enable);\r\n    }\r\n\r\n    public boolean isProxyEnabled() {\r\n        return mIsProxyEnabled;\r\n    }\r\n\r\n    public void setProxyEnabled(boolean enable) {\r\n        mIsProxyEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isVPNEnabled() {\r\n        return mIsVPNEnabled;\r\n    }\r\n\r\n    public void setVPNEnabled(boolean enable) {\r\n        mIsVPNEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isBridgeCheckEnabled() {\r\n        return mIsBridgeCheckEnabled;\r\n    }\r\n\r\n    public void setBridgeCheckEnabled(boolean enable) {\r\n        mIsBridgeCheckEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isOkButtonLongPressDisabled() {\r\n        return mIsOkButtonLongPressDisabled;\r\n    }\r\n\r\n    public void setOkButtonLongPressDisabled(boolean enable) {\r\n        mIsOkButtonLongPressDisabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public String getLastPlaylistId() {\r\n        return mLastPlaylistId;\r\n    }\r\n\r\n    public void setLastPlaylistId(String playlistId) {\r\n        mLastPlaylistId = playlistId;\r\n        persistState();\r\n    }\r\n\r\n    public String getLastPlaylistTitle() {\r\n        return mLastPlaylistTitle;\r\n    }\r\n\r\n    public void setLastPlaylistTitle(String playlistTitle) {\r\n        mLastPlaylistTitle = playlistTitle;\r\n        persistState();\r\n    }\r\n\r\n    public int getPlaylistOrder(String playlistId) {\r\n        Integer order = mPlaylistOrder.get(playlistId);\r\n        return order != null ? order : -1; // default order unpredictable (depends on site prefs)\r\n    }\r\n\r\n    public void setPlaylistOrder(String playlistId, int playlistOrder) {\r\n        if (playlistOrder == -1) {\r\n            mPlaylistOrder.remove(playlistId);\r\n        } else {\r\n            mPlaylistOrder.put(playlistId, playlistOrder);\r\n        }\r\n        persistState();\r\n    }\r\n\r\n    public List<Video> getPendingStreams() {\r\n        return Collections.unmodifiableList(mPendingStreams);\r\n    }\r\n\r\n    public boolean containsPendingStream(Video video) {\r\n        if (video == null || video.videoId == null) {\r\n            return false;\r\n        }\r\n\r\n        return Helpers.containsIf(mPendingStreams, item -> video.videoId.equals(item.videoId));\r\n    }\r\n\r\n    public void addPendingStream(Video video) {\r\n        if (video == null || video.videoId == null || containsPendingStream(video)) {\r\n            return;\r\n        }\r\n\r\n        mPendingStreams.add(video);\r\n        persistState();\r\n    }\r\n\r\n    public void removePendingStream(Video video) {\r\n        if (video == null || video.videoId == null || !containsPendingStream(video)) {\r\n            return;\r\n        }\r\n\r\n        Helpers.removeIf(mPendingStreams, item -> video.videoId.equals(item.videoId));\r\n        persistState();\r\n    }\r\n\r\n    public boolean isGlobalClockEnabled() {\r\n        return mIsGlobalClockEnabled;\r\n    }\r\n\r\n    public void setGlobalClockEnabled(boolean enable) {\r\n        mIsGlobalClockEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public String getSettingsPassword() {\r\n        return mSettingsPassword;\r\n    }\r\n\r\n    public void setSettingsPassword(String password) {\r\n        mSettingsPassword = password;\r\n\r\n        persistState();\r\n    }\r\n\r\n    public String getMasterPassword() {\r\n        return mMasterPassword;\r\n    }\r\n\r\n    public void setMasterPassword(String password) {\r\n        mMasterPassword = password;\r\n\r\n        persistState();\r\n    }\r\n\r\n    public boolean isChildModeEnabled() {\r\n        return mIsChildModeEnabled;\r\n    }\r\n\r\n    public void setChildModeEnabled(boolean enable) {\r\n        mIsChildModeEnabled = enable;\r\n\r\n        persistState();\r\n    }\r\n\r\n    public boolean isHistoryEnabled() {\r\n        return mHistoryState == HISTORY_ENABLED;\r\n    }\r\n\r\n    public void setHistoryEnabled(boolean enabled) {\r\n        setHistoryState(enabled ? HISTORY_ENABLED : HISTORY_AUTO);\r\n    }\r\n\r\n    public int getHistoryState() {\r\n        return mHistoryState;\r\n    }\r\n\r\n    public void setHistoryState(int historyState) {\r\n        mHistoryState = historyState;\r\n\r\n        persistState();\r\n    }\r\n\r\n    public boolean isAltAppIconEnabled() {\r\n        return mIsAltAppIconEnabled;\r\n    }\r\n\r\n    public void setAltAppIconEnabled(boolean enable) {\r\n        mIsAltAppIconEnabled = enable;\r\n\r\n        persistState();\r\n    }\r\n\r\n    public int getVersionCode() {\r\n        return mVersionCode;\r\n    }\r\n\r\n    public void setVersionCode(int code) {\r\n        mVersionCode = code;\r\n\r\n        persistState();\r\n    }\r\n\r\n    public boolean isSelectChannelSectionEnabled() {\r\n        return mIsSelectChannelSectionEnabled;\r\n    }\r\n\r\n    public void setSelectChannelSectionEnabled(boolean enabled) {\r\n        mIsSelectChannelSectionEnabled = enabled;\r\n\r\n        persistState();\r\n    }\r\n\r\n    public boolean isOldUpdateNotificationsEnabled() {\r\n        return mIsOldUpdateNotificationsEnabled;\r\n    }\r\n\r\n    public void setOldUpdateNotificationsEnabled(boolean enable) {\r\n        mIsOldUpdateNotificationsEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isFullscreenModeEnabled() {\r\n        return mIsFullscreenModeEnabled;\r\n    }\r\n\r\n    public void setFullscreenModeEnabled(boolean enable) {\r\n        mIsFullscreenModeEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public Video getSelectedItem(int sectionId) {\r\n        return mSelectedItems.get(sectionId);\r\n    }\r\n\r\n    public void setSelectedItem(int sectionId, Video item) {\r\n        if (item == null) {\r\n            return;\r\n        }\r\n\r\n        mSelectedItems.put(sectionId, item);\r\n\r\n        persistState();\r\n    }\r\n\r\n    public void removeSelectedItem(int sectionId) {\r\n        mSelectedItems.remove(sectionId);\r\n    }\r\n\r\n    public List<String> getChangelog() {\r\n        return mChangelog;\r\n    }\r\n\r\n    public void setChangelog(List<String> changelog) {\r\n        mChangelog = changelog;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isFirstUseTooltipEnabled() {\r\n        return mIsFirstUseTooltipEnabled;\r\n    }\r\n\r\n    public void setFirstUseTooltipEnabled(boolean enable) {\r\n        mIsFirstUseTooltipEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isDeviceSpecificBackupEnabled() {\r\n        return mIsDeviceSpecificBackupEnabled;\r\n    }\r\n\r\n    public void setDeviceSpecificBackupEnabled(boolean enable) {\r\n        mIsDeviceSpecificBackupEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public int getGDriveBackupFreqDays() {\r\n        return mGDriveBackupFreqDays;\r\n    }\r\n\r\n    public void setGDriveBackupFreqDays(int freqDays) {\r\n        mGDriveBackupFreqDays = freqDays;\r\n        persistState();\r\n    }\r\n\r\n    public int getLocalDriveBackupFreqDays() {\r\n        return mLocalDriveBackupFreqDays;\r\n    }\r\n\r\n    public void setLocalDriveBackupFreqDays(int freqDays) {\r\n        mLocalDriveBackupFreqDays = freqDays;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemapSToSpeedToggleEnabled() {\r\n        return mIsRemapSToSpeedToggleEnabled;\r\n    }\r\n\r\n    public void setRemapSToSpeedToggleEnabled(boolean enable) {\r\n        mIsRemapSToSpeedToggleEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    /**\r\n     * Fixed ConcurrentModificationException after onProfileChanged()<br/>\r\n     * Happened inside cleanupPinnedItems()\r\n     */\r\n    private synchronized void restoreState() {\r\n        String data = mPrefs.getProfileData(GENERAL_DATA);\r\n\r\n        String[] split = Helpers.splitData(data);\r\n\r\n        // Zero index is skipped. Selected sections were there.\r\n        //mBootSectionId = Helpers.parseInt(split, 1, MediaGroup.TYPE_HOME);\r\n        //mIsSettingsSectionEnabled = Helpers.parseBoolean(split, 2, true);\r\n        mAppExitShortcut = Helpers.parseInt(split, 3, EXIT_DOUBLE_BACK);\r\n        mIsReturnToLauncherEnabled = Helpers.parseBoolean(split, 4, false);\r\n        mBackgroundShortcut = Helpers.parseInt(split, 5, BACKGROUND_PLAYBACK_SHORTCUT_HOME_BACK);\r\n        mOldPinnedItems = Helpers.parseList(split, 6, Video::fromString);\r\n        mIsHideShortsFromSubscriptionsEnabled = Helpers.parseBoolean(split, 7, false);\r\n        mIsRemapFastForwardToNextEnabled = Helpers.parseBoolean(split, 8, false);\r\n        //mScreenDimmingTimeoutMs = Helpers.parseInt(split, 9, 1);\r\n        mIsProxyEnabled = Helpers.parseBoolean(split, 10, false);\r\n        mIsBridgeCheckEnabled = Helpers.parseBoolean(split, 11, true);\r\n        mIsOkButtonLongPressDisabled = Helpers.parseBoolean(split, 12, false);\r\n        mLastPlaylistId = Helpers.parseStr(split, 13);\r\n        //String selectedSections = Helpers.parseStr(split, 14);\r\n        mIsHideUpcomingEnabled = Helpers.parseBoolean(split, 15, false);\r\n        mIsRemapPageUpToNextEnabled = Helpers.parseBoolean(split, 16, false);\r\n        mIsRemapPageUpToLikeEnabled = Helpers.parseBoolean(split, 17, false);\r\n        mIsRemapChannelUpToNextEnabled = Helpers.parseBoolean(split, 18, false);\r\n        mIsRemapChannelUpToLikeEnabled = Helpers.parseBoolean(split, 19, false);\r\n        mIsRemapPageUpToSpeedEnabled = Helpers.parseBoolean(split, 20, false);\r\n        mIsRemapChannelUpToSpeedEnabled = Helpers.parseBoolean(split, 21, false);\r\n        mIsRemapFastForwardToSpeedEnabled = Helpers.parseBoolean(split, 22, false);\r\n        mIsRemapChannelUpToSearchEnabled = Helpers.parseBoolean(split, 23, false);\r\n        mIsHideShortsFromHomeEnabled = Helpers.parseBoolean(split, 24, false);\r\n        mIsHideShortsFromHistoryEnabled = Helpers.parseBoolean(split, 25, false);\r\n        mIsScreensaverDisabled = Helpers.parseBoolean(split, 26, true);\r\n        mIsVPNEnabled = Helpers.parseBoolean(split, 27, false);\r\n        mLastPlaylistTitle = Helpers.parseStr(split, 28);\r\n        mPlaylistOrder = Helpers.parseMap(split, 29, Helpers::parseStr, Helpers::parseInt);\r\n        //String pendingStreams = Helpers.parseStr(split, 30);\r\n        mPendingStreams = Helpers.parseList(split, 30, Video::fromString);\r\n        mIsGlobalClockEnabled = Helpers.parseBoolean(split, 31, true);\r\n        //mTimeFormat = Helpers.parseInt(split, 32, -1);\r\n        mSettingsPassword = Helpers.parseStr(split, 33);\r\n        mIsChildModeEnabled = Helpers.parseBoolean(split, 34, false);\r\n        mIsHistoryEnabled = Helpers.parseBoolean(split, 35, true);\r\n        mScreensaverTimeoutMs = Helpers.parseInt(split, 36, 60 * 1_000);\r\n        // ScreensaverMode was here\r\n        mIsAltAppIconEnabled = Helpers.parseBoolean(split, 38, false);\r\n        mVersionCode = Helpers.parseInt(split, 39, -1);\r\n        mIsSelectChannelSectionEnabled = Helpers.parseBoolean(split, 40, true);\r\n        mMasterPassword = Helpers.parseStr(split, 41);\r\n        // StackOverflow on old devices?\r\n        //mIsOldHomeLookEnabled = Helpers.parseBoolean(split, 42, Build.VERSION.SDK_INT <= 19);\r\n        mIsOldUpdateNotificationsEnabled = Helpers.parseBoolean(split, 43, false);\r\n        mScreensaverDimmingPercents = Helpers.parseInt(split, 44, 80);\r\n        mIsRemapNextToSpeedEnabled = Helpers.parseBoolean(split, 45, false);\r\n        mIsRemapPlayToOKEnabled = Helpers.parseBoolean(split, 46, false);\r\n        mHistoryState = Helpers.parseInt(split, 47, HISTORY_AUTO);\r\n        mIsRememberSubscriptionsPositionEnabled = Helpers.parseBoolean(split, 48, false);\r\n        // mSelectedSubscriptionsItem was here\r\n        mIsRemapNumbersToSpeedEnabled = Helpers.parseBoolean(split, 50, false);\r\n        mIsRemapDpadUpToSpeedEnabled = Helpers.parseBoolean(split, 51, false);\r\n        mIsRemapChannelUpToVolumeEnabled = Helpers.parseBoolean(split, 52, false);\r\n        mIsRemapDpadUpToVolumeEnabled = Helpers.parseBoolean(split, 53, false);\r\n        mIsRemapDpadLeftToVolumeEnabled = Helpers.parseBoolean(split, 54, false);\r\n        mIsRemapNextToFastForwardEnabled = Helpers.parseBoolean(split, 55, false);\r\n        mIsHideWatchedFromNotificationsEnabled = Helpers.parseBoolean(split, 56, false);\r\n        mChangelog = Helpers.parseStrList(split, 57);\r\n        mPlayerExitShortcut = Helpers.parseInt(split, 58, EXIT_SINGLE_BACK);\r\n        // StackOverflow on old devices?\r\n        //mIsOldChannelLookEnabled = Helpers.parseBoolean(split, 59, Build.VERSION.SDK_INT <= 19);\r\n        mIsFullscreenModeEnabled = Helpers.parseBoolean(split, 60, true);\r\n        //mIsHideWatchedFromWatchLaterEnabled = Helpers.parseBoolean(split, 61, false);\r\n        mIsRememberPinnedPositionEnabled = Helpers.parseBoolean(split, 62, false);\r\n        mSelectedItems = Helpers.parseMap(split, 63, Helpers::parseInt, Video::fromString);\r\n        mIsFirstUseTooltipEnabled = Helpers.parseBoolean(split, 64, true);\r\n        mIsDeviceSpecificBackupEnabled = Helpers.parseBoolean(split, 65, false);\r\n        //mIsAutoBackupEnabled = Helpers.parseBoolean(split, 66, false);\r\n        mIsRemapPageDownToSpeedEnabled = Helpers.parseBoolean(split, 67, false);\r\n        mSearchExitShortcut = Helpers.parseInt(split, 68, EXIT_SINGLE_BACK);\r\n        mGDriveBackupFreqDays = Helpers.parseInt(split, 69, -1);\r\n        mLocalDriveBackupFreqDays = Helpers.parseInt(split, 70, -1);\r\n        mIsRemapFastForwardToSpeedToggleEnabled = Helpers.parseBoolean(split, 71, false);\r\n        mIsRemapSToSpeedToggleEnabled = Helpers.parseBoolean(split, 72, true);\r\n    }\r\n\r\n    public void persistNow() {\r\n        Utils.post(mPersistStateInt);\r\n    }\r\n\r\n    private void persistState() {\r\n        Utils.postDelayed(mPersistStateInt, 10_000);\r\n    }\r\n\r\n    private void persistStateInt() {\r\n        // Zero index is skipped. Selected sections were there.\r\n        mPrefs.setProfileData(GENERAL_DATA, Helpers.mergeData(null, null, null, mAppExitShortcut, mIsReturnToLauncherEnabled,\r\n                mBackgroundShortcut, mOldPinnedItems, mIsHideShortsFromSubscriptionsEnabled,\r\n                mIsRemapFastForwardToNextEnabled, null, mIsProxyEnabled, mIsBridgeCheckEnabled, mIsOkButtonLongPressDisabled, mLastPlaylistId,\r\n                null, mIsHideUpcomingEnabled, mIsRemapPageUpToNextEnabled, mIsRemapPageUpToLikeEnabled,\r\n                mIsRemapChannelUpToNextEnabled, mIsRemapChannelUpToLikeEnabled, mIsRemapPageUpToSpeedEnabled,\r\n                mIsRemapChannelUpToSpeedEnabled, mIsRemapFastForwardToSpeedEnabled, mIsRemapChannelUpToSearchEnabled,\r\n                mIsHideShortsFromHomeEnabled, mIsHideShortsFromHistoryEnabled, mIsScreensaverDisabled, mIsVPNEnabled, mLastPlaylistTitle,\r\n                mPlaylistOrder, mPendingStreams, mIsGlobalClockEnabled, null, mSettingsPassword, mIsChildModeEnabled, mIsHistoryEnabled,\r\n                mScreensaverTimeoutMs, null, mIsAltAppIconEnabled, mVersionCode, mIsSelectChannelSectionEnabled, mMasterPassword,\r\n                null, mIsOldUpdateNotificationsEnabled, mScreensaverDimmingPercents, mIsRemapNextToSpeedEnabled, mIsRemapPlayToOKEnabled,\r\n                mHistoryState, mIsRememberSubscriptionsPositionEnabled, null, mIsRemapNumbersToSpeedEnabled, mIsRemapDpadUpToSpeedEnabled,\r\n                mIsRemapChannelUpToVolumeEnabled, mIsRemapDpadUpToVolumeEnabled, mIsRemapDpadLeftToVolumeEnabled, mIsRemapNextToFastForwardEnabled,\r\n                mIsHideWatchedFromNotificationsEnabled, mChangelog, mPlayerExitShortcut, null, mIsFullscreenModeEnabled, null,\r\n                mIsRememberPinnedPositionEnabled, mSelectedItems, mIsFirstUseTooltipEnabled, mIsDeviceSpecificBackupEnabled, null,\r\n                mIsRemapPageDownToSpeedEnabled, mSearchExitShortcut, mGDriveBackupFreqDays, mLocalDriveBackupFreqDays, mIsRemapFastForwardToSpeedToggleEnabled, mIsRemapSToSpeedToggleEnabled));\r\n    }\r\n\r\n    @Override\r\n    public void onProfileChanged() {\r\n        Utils.removeCallbacks(mPersistStateInt);\r\n        restoreState();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/HiddenPrefs.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.sharedutils.prefs.SharedPreferencesBase;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\npublic class HiddenPrefs extends SharedPreferencesBase {\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static HiddenPrefs sInstance;\r\n    public static final String SHARED_PREFERENCES_NAME = HiddenPrefs.class.getName();\r\n    private static final String UNIQUE_ID = \"unique_id\";\r\n\r\n    private HiddenPrefs(Context context) {\r\n        super(context, SHARED_PREFERENCES_NAME);\r\n    }\r\n\r\n    public static HiddenPrefs instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new HiddenPrefs(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public String getUniqueId() {\r\n        return getString(UNIQUE_ID, null);\r\n    }\r\n\r\n    public void setUniqueId(String id) {\r\n        putString(UNIQUE_ID, id);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/MainUIData.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport android.os.Build;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.ContextMenuManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.ContextMenuProvider;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs.ProfileChangeListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.common.DataChangeBase;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.ClickbaitRemover;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Collections;\r\nimport java.util.List;\r\n\r\npublic class MainUIData extends DataChangeBase implements ProfileChangeListener {\r\n    private static final String MAIN_UI_DATA = \"main_ui_data2\";\r\n    public static final int CARD_PREVIEW_DISABLED = 0;\r\n    public static final int CARD_PREVIEW_MUTED = 1;\r\n    public static final int CARD_PREVIEW_FULL = 2;\r\n    public static final int CHANNEL_SORTING_NEW_CONTENT = 0;\r\n    public static final int CHANNEL_SORTING_NAME = 1;\r\n    public static final int CHANNEL_SORTING_DEFAULT = 2;\r\n    public static final int CHANNEL_SORTING_LAST_VIEWED = 3;\r\n    public static final int CHANNEL_SORTING_NAME2 = 4;\r\n    public static final int PLAYLISTS_STYLE_GRID = 0;\r\n    public static final int PLAYLISTS_STYLE_ROWS = 1;\r\n    public static final long MENU_ITEM_RECENT_PLAYLIST = 1;\r\n    public static final long MENU_ITEM_ADD_TO_QUEUE = 1 << 1;\r\n    public static final long MENU_ITEM_PIN_TO_SIDEBAR = 1 << 2;\r\n    public static final long MENU_ITEM_SHARE_LINK = 1 << 3;\r\n    public static final long MENU_ITEM_SELECT_ACCOUNT = 1 << 4;\r\n    public static final long MENU_ITEM_NOT_INTERESTED = 1 << 5;\r\n    public static final long MENU_ITEM_REMOVE_FROM_HISTORY = 1 << 6;\r\n    public static final long MENU_ITEM_MOVE_SECTION_UP = 1 << 7;\r\n    public static final long MENU_ITEM_MOVE_SECTION_DOWN = 1 << 8;\r\n    public static final long MENU_ITEM_OPEN_DESCRIPTION = 1 << 9;\r\n    public static final long MENU_ITEM_RENAME_SECTION = 1 << 10;\r\n    public static final long MENU_ITEM_PLAY_VIDEO = 1 << 11;\r\n    public static final long MENU_ITEM_SAVE_REMOVE_PLAYLIST = 1 << 12;\r\n    public static final long MENU_ITEM_ADD_TO_PLAYLIST = 1 << 13;\r\n    public static final long MENU_ITEM_SUBSCRIBE = 1 << 14;\r\n    public static final long MENU_ITEM_CREATE_PLAYLIST = 1 << 15;\r\n    public static final long MENU_ITEM_STREAM_REMINDER = 1 << 16;\r\n    public static final long MENU_ITEM_ADD_TO_NEW_PLAYLIST = 1 << 17;\r\n    public static final long MENU_ITEM_SHARE_EMBED_LINK = 1 << 18;\r\n    public static final long MENU_ITEM_SHOW_QUEUE = 1 << 19;\r\n    public static final long MENU_ITEM_PLAYLIST_ORDER = 1 << 20;\r\n    public static final long MENU_ITEM_TOGGLE_HISTORY = 1 << 21;\r\n    public static final long MENU_ITEM_CLEAR_HISTORY = 1 << 22;\r\n    public static final long MENU_ITEM_UPDATE_CHECK = 1 << 23;\r\n    public static final long MENU_ITEM_OPEN_CHANNEL = 1 << 24;\r\n    public static final long MENU_ITEM_REMOVE_FROM_SUBSCRIPTIONS = 1 << 25;\r\n    public static final long MENU_ITEM_PLAY_VIDEO_INCOGNITO = 1 << 26;\r\n    public static final long MENU_ITEM_MARK_AS_WATCHED = 1 << 27;\r\n    public static final long MENU_ITEM_EXCLUDE_FROM_CONTENT_BLOCK = 1 << 28;\r\n    public static final long MENU_ITEM_OPEN_PLAYLIST = 1 << 29;\r\n    public static final long MENU_ITEM_EXIT_FROM_PIP = 1 << 30;\r\n    public static final long MENU_ITEM_OPEN_COMMENTS = 1L << 31;\r\n    public static final long MENU_ITEM_SHARE_QR_LINK = 1L << 32;\r\n    public static final long MENU_ITEM_PLAY_NEXT = 1L << 33;\r\n    public static final long MENU_ITEM_RENAME_PLAYLIST = 1L << 34;\r\n    public static final long MENU_ITEM_NOT_RECOMMEND_CHANNEL = 1L << 35;\r\n    public static final long MENU_ITEM_PLAY_FROM_START = 1L << 36;\r\n    public static final long MENU_ITEM_BLOCK_CHANNEL = 1L << 37;\r\n    public static final int TOP_BUTTON_BROWSE_ACCOUNTS = 1;\r\n    public static final int TOP_BUTTON_CHANGE_LANGUAGE = 1 << 1;\r\n    public static final int TOP_BUTTON_SEARCH = 1 << 2;\r\n    public static final int TOP_BUTTON_DEFAULT = TOP_BUTTON_SEARCH | TOP_BUTTON_BROWSE_ACCOUNTS;\r\n    public static final long MENU_ITEM_DEFAULT = MENU_ITEM_PIN_TO_SIDEBAR | MENU_ITEM_NOT_INTERESTED | MENU_ITEM_NOT_RECOMMEND_CHANNEL\r\n            | MENU_ITEM_REMOVE_FROM_HISTORY | MENU_ITEM_BLOCK_CHANNEL | MENU_ITEM_MOVE_SECTION_UP | MENU_ITEM_MOVE_SECTION_DOWN\r\n            | MENU_ITEM_RENAME_SECTION | MENU_ITEM_SAVE_REMOVE_PLAYLIST | MENU_ITEM_ADD_TO_PLAYLIST | MENU_ITEM_CREATE_PLAYLIST\r\n            | MENU_ITEM_RENAME_PLAYLIST | MENU_ITEM_ADD_TO_NEW_PLAYLIST | MENU_ITEM_STREAM_REMINDER | MENU_ITEM_PLAYLIST_ORDER\r\n            | MENU_ITEM_OPEN_CHANNEL | MENU_ITEM_REMOVE_FROM_SUBSCRIPTIONS | MENU_ITEM_PLAY_NEXT | MENU_ITEM_OPEN_PLAYLIST\r\n            | MENU_ITEM_SUBSCRIBE | MENU_ITEM_CLEAR_HISTORY;\r\n    private static final Long[] MENU_ITEM_DEFAULT_ORDER = {\r\n            MENU_ITEM_EXIT_FROM_PIP, MENU_ITEM_PLAY_VIDEO, MENU_ITEM_PLAY_VIDEO_INCOGNITO, MENU_ITEM_PLAY_FROM_START, MENU_ITEM_REMOVE_FROM_HISTORY,\r\n            MENU_ITEM_STREAM_REMINDER, MENU_ITEM_RECENT_PLAYLIST, MENU_ITEM_ADD_TO_PLAYLIST, MENU_ITEM_CREATE_PLAYLIST, MENU_ITEM_RENAME_PLAYLIST,\r\n            MENU_ITEM_ADD_TO_NEW_PLAYLIST, MENU_ITEM_NOT_INTERESTED, MENU_ITEM_NOT_RECOMMEND_CHANNEL, MENU_ITEM_BLOCK_CHANNEL,\r\n            MENU_ITEM_REMOVE_FROM_SUBSCRIPTIONS, MENU_ITEM_MARK_AS_WATCHED, MENU_ITEM_PLAYLIST_ORDER, MENU_ITEM_PLAY_NEXT, MENU_ITEM_ADD_TO_QUEUE,\r\n            MENU_ITEM_SHOW_QUEUE, MENU_ITEM_OPEN_CHANNEL, MENU_ITEM_OPEN_PLAYLIST, MENU_ITEM_SUBSCRIBE, MENU_ITEM_EXCLUDE_FROM_CONTENT_BLOCK,\r\n            MENU_ITEM_PIN_TO_SIDEBAR, MENU_ITEM_SAVE_REMOVE_PLAYLIST, MENU_ITEM_OPEN_DESCRIPTION, MENU_ITEM_OPEN_COMMENTS, MENU_ITEM_SHARE_LINK,\r\n            MENU_ITEM_SHARE_EMBED_LINK, MENU_ITEM_SHARE_QR_LINK, MENU_ITEM_SELECT_ACCOUNT, MENU_ITEM_TOGGLE_HISTORY, MENU_ITEM_CLEAR_HISTORY,\r\n            MENU_ITEM_MOVE_SECTION_UP, MENU_ITEM_MOVE_SECTION_DOWN, MENU_ITEM_UPDATE_CHECK\r\n    };\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static MainUIData sInstance;\r\n    private final Context mContext;\r\n    private final AppPrefs mPrefs;\r\n    private boolean mIsCardMultilineTitleEnabled;\r\n    private boolean mIsCardMultilineSubtitleEnabled;\r\n    private boolean mIsCardTextAutoScrollEnabled;\r\n    private int mCardTitleLinesNum;\r\n    private float mUIScale;\r\n    private float mVideoGridScale;\r\n    private final List<ColorScheme> mColorSchemes = new ArrayList<>();\r\n    private int mColorSchemeIndex;\r\n    private int mChannelCategorySorting;\r\n    private int mPlaylistsStyle;\r\n    private boolean mIsUploadsOldLookEnabled;\r\n    private boolean mIsUploadsAutoLoadEnabled;\r\n    private float mCardTextScrollSpeed;\r\n    private long mMenuItems;\r\n    private int mTopButtons;\r\n    private int mThumbQuality;\r\n    private List<Long> mMenuItemsOrdered;\r\n    private boolean mIsChannelsFilterEnabled;\r\n    private boolean mIsChannelSearchBarEnabled;\r\n    private boolean mIsPinnedChannelRowsEnabled;\r\n    private int mCardPreviewType;\r\n    private final Runnable mPersistStateInt = this::persistStateInt;\r\n    private boolean mIsUnlocalizedTitlesEnabled;\r\n\r\n    private MainUIData(Context context) {\r\n        mContext = context;\r\n        mPrefs = AppPrefs.instance(context);\r\n        mPrefs.addListener(this);\r\n        initColorSchemes();\r\n        restoreState();\r\n    }\r\n\r\n    public static MainUIData instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new MainUIData(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public boolean isCardMultilineTitleEnabled() {\r\n        return mIsCardMultilineTitleEnabled;\r\n    }\r\n\r\n    public void setCardMultilineTitleEnabled(boolean enable) {\r\n        mIsCardMultilineTitleEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isCardMultilineSubtitleEnabled() {\r\n        return mIsCardMultilineSubtitleEnabled;\r\n    }\r\n\r\n    public void setCardMultilineSubtitleEnabled(boolean enable) {\r\n        mIsCardMultilineSubtitleEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isCardTextAutoScrollEnabled() {\r\n        return mIsCardTextAutoScrollEnabled;\r\n    }\r\n\r\n    public void setCardTextAutoScrollEnabled(boolean enable) {\r\n        mIsCardTextAutoScrollEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public int getCardTitleLinesNum() {\r\n        return mCardTitleLinesNum;\r\n    }\r\n\r\n    public void setCartTitleLinesNum(int lines) {\r\n        mCardTitleLinesNum = lines;\r\n        persistState();\r\n    }\r\n\r\n    public int getThumbQuality() {\r\n        return mThumbQuality;\r\n    }\r\n\r\n    public void setThumbQuality(int quality) {\r\n        mThumbQuality = quality;\r\n        persistState();\r\n    }\r\n\r\n    public float getVideoGridScale() {\r\n        // Fixing the bug with chaotic cards positioning on Android 4.4 devices.\r\n        return Build.VERSION.SDK_INT <= 19 ? 1.2f : mVideoGridScale;\r\n    }\r\n\r\n    public void setVideoGridScale(float scale) {\r\n        mVideoGridScale = scale;\r\n\r\n        persistState();\r\n    }\r\n\r\n    public float getUIScale() {\r\n        return mUIScale;\r\n    }\r\n\r\n    public void setUIScale(float scale) {\r\n        mUIScale = scale;\r\n\r\n        persistState();\r\n    }\r\n\r\n    public List<ColorScheme> getColorSchemes() {\r\n        return mColorSchemes;\r\n    }\r\n\r\n    public ColorScheme getColorScheme() {\r\n        return mColorSchemes.get(mColorSchemeIndex);\r\n    }\r\n\r\n    public void setColorScheme(ColorScheme scheme) {\r\n        mColorSchemeIndex = mColorSchemes.indexOf(scheme);\r\n        persistState();\r\n    }\r\n\r\n    public int getChannelCategorySorting() {\r\n        return mChannelCategorySorting;\r\n    }\r\n\r\n    public void setChannelCategorySorting(int type) {\r\n        mChannelCategorySorting = type;\r\n        persistState();\r\n    }\r\n\r\n    public int getPlaylistsStyle() {\r\n        return mPlaylistsStyle;\r\n    }\r\n\r\n    public void setPlaylistsStyle(int type) {\r\n        mPlaylistsStyle = type;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isUploadsOldLookEnabled() {\r\n        return mIsUploadsOldLookEnabled;\r\n    }\r\n\r\n    public void setUploadsOldLookEnabled(boolean enable) {\r\n        mIsUploadsOldLookEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isChannelsFilterEnabled() {\r\n        return mIsChannelsFilterEnabled;\r\n    }\r\n\r\n    public void setChannelsFilterEnabled(boolean enable) {\r\n        mIsChannelsFilterEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isPinnedChannelRowsEnabled() {\r\n        return mIsPinnedChannelRowsEnabled;\r\n    }\r\n\r\n    public void setPinnedChannelRowsEnabled(boolean enable) {\r\n        mIsPinnedChannelRowsEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isChannelSearchBarEnabled() {\r\n        return mIsChannelSearchBarEnabled;\r\n    }\r\n\r\n    public void setChannelSearchBarEnabled(boolean enable) {\r\n        mIsChannelSearchBarEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isUploadsAutoLoadEnabled() {\r\n        return mIsUploadsAutoLoadEnabled;\r\n    }\r\n\r\n    public void setUploadsAutoLoadEnabled(boolean enable) {\r\n        mIsUploadsAutoLoadEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public float getCardTextScrollSpeed() {\r\n        return mCardTextScrollSpeed;\r\n    }\r\n\r\n    public void setCardTextScrollSpeed(float factor) {\r\n        mCardTextScrollSpeed = factor;\r\n\r\n        setCardTextAutoScrollEnabled(true);\r\n\r\n        persistState();\r\n    }\r\n\r\n    public boolean isMenuItemEnabled(long menuItems) {\r\n        return (mMenuItems & menuItems) == menuItems;\r\n    }\r\n\r\n    public void setMenuItemEnabled(long menuItems) {\r\n        mMenuItems |= menuItems;\r\n        persistState();\r\n    }\r\n\r\n    public void setMenuItemDisabled(long menuItems) {\r\n        mMenuItems &= ~menuItems;\r\n        persistState();\r\n    }\r\n\r\n    public List<Long> getMenuItemsOrdered() {\r\n        return Collections.unmodifiableList(mMenuItemsOrdered);\r\n    }\r\n\r\n    public int getMenuItemIndex(long menuItem) {\r\n        return mMenuItemsOrdered.indexOf(menuItem);\r\n    }\r\n\r\n    public void setMenuItemIndex(int index, Long menuItem) {\r\n        //int currentIndex = getMenuItemIndex(menuItem);\r\n        //index = currentIndex < index ? index - 1 : index;\r\n\r\n        mMenuItemsOrdered.remove(menuItem);\r\n\r\n        if (index <= mMenuItemsOrdered.size()) {\r\n            mMenuItemsOrdered.add(index, menuItem);\r\n        } else {\r\n            mMenuItemsOrdered.add(menuItem);\r\n        }\r\n\r\n        persistState();\r\n    }\r\n\r\n    public boolean isTopButtonEnabled(int button) {\r\n        return (mTopButtons & button) == button;\r\n    }\r\n\r\n    public void setTopButtonEnabled(int button) {\r\n        mTopButtons |= button;\r\n        persistState();\r\n    }\r\n\r\n    public void setTopButtonDisabled(int button) {\r\n        mTopButtons &= ~button;\r\n        persistState();\r\n    }\r\n\r\n    public int getCardPreviewType() {\r\n        return mCardPreviewType;\r\n    }\r\n\r\n    public void setCardPreviewType(int type) {\r\n        mCardPreviewType = type;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isUnlocalizedTitlesEnabled() {\r\n        return mIsUnlocalizedTitlesEnabled;\r\n    }\r\n\r\n    public void setUnlocalizedTitlesEnabled(boolean enabled) {\r\n        mIsUnlocalizedTitlesEnabled = enabled;\r\n        persistState();\r\n    }\r\n\r\n    private void initColorSchemes() {\r\n        mColorSchemes.add(new ColorScheme(\r\n                R.string.color_scheme_teal,\r\n                null,\r\n                null,\r\n                null,\r\n                mContext));\r\n        mColorSchemes.add(new ColorScheme(\r\n                R.string.color_scheme_dark_grey,\r\n                \"App.Theme.DarkGrey.Player\",\r\n                \"App.Theme.DarkGrey.Browse\",\r\n                \"App.Theme.DarkGrey.Preferences\",\r\n                mContext));\r\n        mColorSchemes.add(new ColorScheme(\r\n                R.string.color_scheme_red,\r\n                \"App.Theme.Red.Player\",\r\n                \"App.Theme.Red.Browse\",\r\n                \"App.Theme.Red.Preferences\",\r\n                mContext));\r\n        mColorSchemes.add(new ColorScheme(\r\n                R.string.color_scheme_dark_grey_oled,\r\n                \"App.Theme.DarkGrey.OLED.Player\",\r\n                \"App.Theme.DarkGrey.OLED.Browse\",\r\n                \"App.Theme.DarkGrey.Preferences\",\r\n                mContext));\r\n        mColorSchemes.add(new ColorScheme(\r\n                R.string.color_scheme_teal_oled,\r\n                \"App.Theme.Leanback.OLED.Player\",\r\n                \"App.Theme.Leanback.OLED.Browse\",\r\n                null,\r\n                mContext));\r\n        mColorSchemes.add(new ColorScheme(\r\n                R.string.color_scheme_dark_grey_monochrome,\r\n                \"App.Theme.DarkGrey2.OLED.Player\",\r\n                \"App.Theme.DarkGrey2.OLED.Browse\",\r\n                \"App.Theme.DarkGrey.Preferences\",\r\n                mContext));\r\n        mColorSchemes.add(new ColorScheme(\r\n                R.string.color_scheme_dark_blue,\r\n                \"App.Theme.Leanback.Blue.Player\",\r\n                \"App.Theme.Leanback.Blue.Browse\",\r\n                \"App.Theme.Leanback.Blue.Preferences\",\r\n                mContext));\r\n    }\r\n\r\n    private void restoreState() {\r\n        String data = mPrefs.getProfileData(MAIN_UI_DATA);\r\n\r\n        String[] split = Helpers.splitData(data);\r\n\r\n        //mIsCardAnimatedPreviewsEnabled = Helpers.parseBoolean(split, 0, true);\r\n        mVideoGridScale = Helpers.parseFloat(split, 1, 1.0f); // 4 cards in a row\r\n        mUIScale = Helpers.parseFloat(split, 2, 1.0f);\r\n        mColorSchemeIndex = Helpers.parseInt(split, 3, 1);\r\n        mIsCardMultilineTitleEnabled = Helpers.parseBoolean(split, 4, true);\r\n        mChannelCategorySorting = Helpers.parseInt(split, 5, CHANNEL_SORTING_LAST_VIEWED);\r\n        mPlaylistsStyle = Helpers.parseInt(split, 6, PLAYLISTS_STYLE_GRID);\r\n        mCardTitleLinesNum = Helpers.parseInt(split, 7, 1);\r\n        mIsCardTextAutoScrollEnabled = Helpers.parseBoolean(split, 8, true);\r\n        mIsUploadsOldLookEnabled = Helpers.parseBoolean(split, 9, false);\r\n        mIsUploadsAutoLoadEnabled = Helpers.parseBoolean(split, 10, true);\r\n        mCardTextScrollSpeed = Helpers.parseFloat(split, 11, 2);\r\n        mMenuItems = Helpers.parseLong(split, 12, MENU_ITEM_DEFAULT);\r\n        mTopButtons = Helpers.parseInt(split, 13, TOP_BUTTON_DEFAULT);\r\n        // 14\r\n        mThumbQuality = Helpers.parseInt(split, 15, ClickbaitRemover.THUMB_QUALITY_DEFAULT);\r\n        mIsCardMultilineSubtitleEnabled = Helpers.parseBoolean(split, 16, true);\r\n        mMenuItemsOrdered = Helpers.parseLongList(split, 17);\r\n        mIsChannelsFilterEnabled = Helpers.parseBoolean(split, 18, true);\r\n        mIsChannelSearchBarEnabled = Helpers.parseBoolean(split, 19, true);\r\n        mIsPinnedChannelRowsEnabled = Helpers.parseBoolean(split, 20, true);\r\n        mCardPreviewType = Helpers.parseInt(split, 21, CARD_PREVIEW_DISABLED);\r\n        mIsUnlocalizedTitlesEnabled = Helpers.parseBoolean(split, 22, false);\r\n\r\n        int idx = -1;\r\n        for (Long menuItem : MENU_ITEM_DEFAULT_ORDER) {\r\n            idx++;\r\n            if (!mMenuItemsOrdered.contains(menuItem)) {\r\n                if (idx < mMenuItemsOrdered.size()) {\r\n                    mMenuItemsOrdered.add(idx, menuItem);\r\n                } else {\r\n                    mMenuItemsOrdered.add(menuItem);\r\n                }\r\n\r\n                boolean isEnabled = (MENU_ITEM_DEFAULT & menuItem) == menuItem;\r\n                if (isEnabled) {\r\n                    mMenuItems |= menuItem;\r\n                }\r\n            }\r\n        }\r\n\r\n        for (ContextMenuProvider provider : new ContextMenuManager(mContext).getProviders()) {\r\n            if (!mMenuItemsOrdered.contains(provider.getId())) {\r\n                mMenuItemsOrdered.add(provider.getId());\r\n            }\r\n        }\r\n        \r\n        updateDefaultValues();\r\n    }\r\n\r\n    public void persistNow() {\r\n        Utils.post(mPersistStateInt);\r\n    }\r\n\r\n    private void persistState() {\r\n        onDataChange();\r\n        Utils.postDelayed(mPersistStateInt, 10_000);\r\n    }\r\n\r\n    private void persistStateInt() {\r\n        mPrefs.setProfileData(MAIN_UI_DATA, Helpers.mergeData(null,\r\n                mVideoGridScale, mUIScale, mColorSchemeIndex, mIsCardMultilineTitleEnabled,\r\n                mChannelCategorySorting, mPlaylistsStyle, mCardTitleLinesNum, mIsCardTextAutoScrollEnabled,\r\n                mIsUploadsOldLookEnabled, mIsUploadsAutoLoadEnabled, mCardTextScrollSpeed, mMenuItems, mTopButtons,\r\n                null, mThumbQuality, mIsCardMultilineSubtitleEnabled, Helpers.mergeList(mMenuItemsOrdered),\r\n                mIsChannelsFilterEnabled, mIsChannelSearchBarEnabled, mIsPinnedChannelRowsEnabled, mCardPreviewType,\r\n                mIsUnlocalizedTitlesEnabled));\r\n    }\r\n\r\n    public static class ColorScheme {\r\n        public final int nameResId;\r\n        public final int playerThemeResId;\r\n        public final int browseThemeResId;\r\n        public final int settingsThemeResId;\r\n\r\n        public ColorScheme(int nameResId,\r\n                           String playerTheme,\r\n                           String browseTheme,\r\n                           String settingsTheme,\r\n                           Context context) {\r\n            this.nameResId = nameResId;\r\n            this.playerThemeResId = Helpers.getResourceId(playerTheme, \"style\", context);\r\n            this.browseThemeResId = Helpers.getResourceId(browseTheme, \"style\", context);\r\n            this.settingsThemeResId = Helpers.getResourceId(settingsTheme, \"style\", context);\r\n        }\r\n    }\r\n\r\n    private void updateDefaultValues() {\r\n        // Enable only certain items (not all, like it was)\r\n        if (mMenuItems >>> 30 == 0b1) { // check leftmost bit (old format)\r\n            int bits = 32 - 27;\r\n            mMenuItems = mMenuItems << bits >>> bits; // remove auto enabled bits\r\n        }\r\n\r\n        if (mChannelCategorySorting == CHANNEL_SORTING_NAME2) {\r\n            mChannelCategorySorting = CHANNEL_SORTING_NAME;\r\n        }\r\n\r\n        if (mChannelCategorySorting == CHANNEL_SORTING_DEFAULT) {\r\n            mChannelCategorySorting = CHANNEL_SORTING_LAST_VIEWED;\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onProfileChanged() {\r\n        restoreState();\r\n        onDataChange();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/PlayerData.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport android.os.Build;\r\nimport android.os.Build.VERSION;\r\n\r\nimport androidx.annotation.NonNull;\r\n\r\nimport com.google.android.exoplayer2.text.CaptionStyleCompat;\r\nimport com.liskovsoft.sharedutils.helpers.DeviceHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.locale.LocaleUtility;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerEngine;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerConstants;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.other.SubtitleManager.SubtitleStyle;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.ExoFormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.MediaTrack;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs.ProfileChangeListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.common.DataChangeBase;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.HashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\n\r\npublic class PlayerData extends DataChangeBase implements PlayerConstants, ProfileChangeListener {\r\n    private static final String VIDEO_PLAYER_DATA = \"video_player_data\";\r\n    public static final int OK_ONLY_UI = 0;\r\n    public static final int OK_UI_AND_PAUSE = 1;\r\n    public static final int OK_ONLY_PAUSE = 2;\r\n    public static final int OK_TOGGLE_SPEED = 3;\r\n    public static final int AUTO_HIDE_NEVER = 0;\r\n    public static final int SEEK_PREVIEW_NONE = 0;\r\n    public static final int SEEK_PREVIEW_SINGLE = 1;\r\n    public static final int SEEK_PREVIEW_CAROUSEL_SLOW = 2;\r\n    public static final int SEEK_PREVIEW_CAROUSEL_FAST = 3;\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static PlayerData sInstance;\r\n    private final AppPrefs mPrefs;\r\n    private int mOKButtonBehavior;\r\n    private int mUiHideTimeoutSec;\r\n    private boolean mIsSeekConfirmPauseEnabled;\r\n    private boolean mIsClockEnabled;\r\n    private boolean mIsGlobalClockEnabled;\r\n    private boolean mIsRemainingTimeEnabled;\r\n    private int mBackgroundMode;\r\n    private FormatItem mVideoFormat;\r\n    private FormatItem mTempVideoFormat;\r\n    private FormatItem mAudioFormat;\r\n    private FormatItem mSubtitleFormat;\r\n    private int mVideoBufferType;\r\n    private final List<SubtitleStyle> mSubtitleStyles = new ArrayList<>();\r\n    private final Map<String, FormatItem> mDefaultVideoFormats = new HashMap<>();\r\n    private int mSubtitleStyleIndex;\r\n    private int mResizeMode;\r\n    private int mZoomPercents;\r\n    private float mAspectRatio;\r\n    private int mRotationAngle;\r\n    private boolean mIsVideoFlipEnabled;\r\n    private int mSeekPreviewMode;\r\n    private float mSpeed;\r\n    private float mLastSpeed;\r\n    private boolean mIsAfrEnabled;\r\n    private boolean mIsAfrFpsCorrectionEnabled;\r\n    private boolean mIsAfrResSwitchEnabled;\r\n    private int mAfrPauseMs;\r\n    private int mAudioDelayMs;\r\n    private String mAudioLanguage;\r\n    private String mSubtitleLanguage;\r\n    private boolean mIsAllSpeedEnabled;\r\n    private int mPlaybackMode;\r\n    private float mSleepTimerHours;\r\n    private boolean mIsQualityInfoEnabled;\r\n    private boolean mIsSpeedPerVideoEnabled;\r\n    private boolean mIsTimeCorrectionEnabled;\r\n    private boolean mIsGlobalEndingTimeEnabled;\r\n    private boolean mIsEndingTimeEnabled;\r\n    private boolean mIsDoubleRefreshRateEnabled;\r\n    private boolean mIsSeekConfirmPlayEnabled;\r\n    private int mSeekIncrementMs;\r\n    private float mSubtitleScale;\r\n    private float mPlayerVolume;\r\n    private boolean mIsTooltipsEnabled;\r\n    private float mSubtitlePosition;\r\n    private boolean mIsNumberKeySeekEnabled;\r\n    private boolean mIsSkip24RateEnabled;\r\n    private boolean mIsSkipShortsEnabled;\r\n    private boolean mIsLiveChatEnabled;\r\n    private List<FormatItem> mLastSubtitleFormats;\r\n    private List<String> mEnabledSubtitlesPerChannel;\r\n    private boolean mIsSubtitlesPerChannelEnabled;\r\n    private boolean mIsSpeedPerChannelEnabled;\r\n    private final Map<String, SpeedItem> mSpeeds = new HashMap<>();\r\n    private float mPitch;\r\n    private long mAfrSwitchTimeMs;\r\n    private List<String> mLastAudioLanguages;\r\n    private final Runnable mPersistStateInt = this::persistStateInt;\r\n    private boolean mIsLegacyCodecsForced;\r\n\r\n    private static class SpeedItem {\r\n        public String channelId;\r\n        public float speed;\r\n\r\n        public SpeedItem(String channelId, float speed) {\r\n            this.channelId = channelId;\r\n            this.speed = speed;\r\n        }\r\n\r\n        public static SpeedItem fromString(String specs) {\r\n            String[] split = Helpers.splitObj(specs);\r\n\r\n            if (split == null || split.length != 2) {\r\n                return new SpeedItem(null, 1);\r\n            }\r\n\r\n            return new SpeedItem(Helpers.parseStr(split[0]), Helpers.parseFloat(split[1]));\r\n        }\r\n\r\n        @NonNull\r\n        @Override\r\n        public String toString() {\r\n            return Helpers.mergeObj(channelId, speed);\r\n        }\r\n    }\r\n\r\n    private PlayerData(Context context) {\r\n        mPrefs = AppPrefs.instance(context);\r\n        mPrefs.addListener(this);\r\n        initSubtitleStyles();\r\n        initDefaultFormats();\r\n        restoreState();\r\n    }\r\n\r\n    public static PlayerData instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new PlayerData(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public int getOKButtonBehavior() {\r\n        return mOKButtonBehavior;\r\n    }\r\n\r\n    public void setOKButtonBehavior(int option) {\r\n        mOKButtonBehavior = option;\r\n        persistState();\r\n    }\r\n\r\n    public int getUiHideTimeoutSec() {\r\n        return mUiHideTimeoutSec;\r\n    }\r\n\r\n    public void setUiHideTimeoutSec(int timeoutSec) {\r\n        mUiHideTimeoutSec = timeoutSec;\r\n        persistState();\r\n    }\r\n\r\n    public int getSeekPreviewMode() {\r\n        return mSeekPreviewMode;\r\n    }\r\n\r\n    public void setSeekPreviewMode(int mode) {\r\n        mSeekPreviewMode = mode;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isSeekConfirmPauseEnabled() {\r\n        return mIsSeekConfirmPauseEnabled;\r\n    }\r\n\r\n    public void setSeekConfirmPauseEnabled(boolean enable) {\r\n        mIsSeekConfirmPauseEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isSeekConfirmPlayEnabled() {\r\n        return mIsSeekConfirmPlayEnabled;\r\n    }\r\n\r\n    public void setSeekConfirmPlayEnabled(boolean enable) {\r\n        mIsSeekConfirmPlayEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isClockEnabled() {\r\n        return mIsClockEnabled;\r\n    }\r\n\r\n    public void setClockEnabled(boolean enable) {\r\n        mIsClockEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isGlobalClockEnabled() {\r\n        return mIsGlobalClockEnabled;\r\n    }\r\n\r\n    public void setGlobalClockEnabled(boolean enable) {\r\n        mIsGlobalClockEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isGlobalEndingTimeEnabled() {\r\n        return mIsGlobalEndingTimeEnabled;\r\n    }\r\n\r\n    public void setGlobalEndingTimeEnabled(boolean enable) {\r\n        mIsGlobalEndingTimeEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemainingTimeEnabled() {\r\n        return mIsRemainingTimeEnabled;\r\n    }\r\n\r\n    public void setRemainingTimeEnabled(boolean enable) {\r\n        mIsRemainingTimeEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isEndingTimeEnabled() {\r\n        return mIsEndingTimeEnabled;\r\n    }\r\n\r\n    public void setEndingTimeEnabled(boolean enable) {\r\n        mIsEndingTimeEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isQualityInfoEnabled() {\r\n        return mIsQualityInfoEnabled;\r\n    }\r\n\r\n    public void setQualityInfoEnabled(boolean enable) {\r\n        mIsQualityInfoEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public void setBackgroundMode(int type) {\r\n        mBackgroundMode = type;\r\n        persistState();\r\n    }\r\n\r\n    public int getBackgroundMode() {\r\n        return mBackgroundMode;\r\n    }\r\n\r\n    public void setPlaybackMode(int mode) {\r\n        mPlaybackMode = mode;\r\n        persistState();\r\n    }\r\n\r\n    public int getPlaybackMode() {\r\n        return mPlaybackMode;\r\n    }\r\n\r\n    public boolean isAllSpeedEnabled() {\r\n        return mIsAllSpeedEnabled;\r\n    }\r\n\r\n    public void setAllSpeedEnabled(boolean enable) {\r\n        mIsAllSpeedEnabled = enable;\r\n        mIsSpeedPerVideoEnabled = false;\r\n        mIsSpeedPerChannelEnabled = false;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isSpeedPerVideoEnabled() {\r\n        return mIsSpeedPerVideoEnabled;\r\n    }\r\n\r\n    public void setSpeedPerVideoEnabled(boolean enable) {\r\n        mIsSpeedPerVideoEnabled = enable;\r\n        mIsAllSpeedEnabled = false;\r\n        mIsSpeedPerChannelEnabled = false;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isLegacyCodecsForced() {\r\n        return mIsLegacyCodecsForced;\r\n    }\r\n\r\n    public void setLegacyCodecsForced(boolean forced) {\r\n        mIsLegacyCodecsForced = forced;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isAfrEnabled() {\r\n        return mIsAfrEnabled;\r\n    }\r\n\r\n    public void setAfrEnabled(boolean enabled) {\r\n        mIsAfrEnabled = enabled;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isAfrFpsCorrectionEnabled() {\r\n        return mIsAfrFpsCorrectionEnabled;\r\n    }\r\n\r\n    public void setAfrFpsCorrectionEnabled(boolean enabled) {\r\n        mIsAfrFpsCorrectionEnabled = enabled;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isAfrResSwitchEnabled() {\r\n        return mIsAfrResSwitchEnabled;\r\n    }\r\n\r\n    public void setAfrResSwitchEnabled(boolean enabled) {\r\n        mIsAfrResSwitchEnabled = enabled;\r\n        persistState();\r\n    }\r\n\r\n    public int getAfrPauseMs() {\r\n        return mAfrPauseMs;\r\n    }\r\n\r\n    public void setAfrPauseMs(int pauseSec) {\r\n        mAfrPauseMs = pauseSec;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isDoubleRefreshRateEnabled() {\r\n        return mIsDoubleRefreshRateEnabled;\r\n    }\r\n\r\n    public void setDoubleRefreshRateEnabled(boolean enabled) {\r\n        mIsDoubleRefreshRateEnabled = enabled;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isTooltipsEnabled() {\r\n        return mIsTooltipsEnabled;\r\n    }\r\n\r\n    public void setTooltipsEnabled(boolean enable) {\r\n        mIsTooltipsEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isNumberKeySeekEnabled() {\r\n        return mIsNumberKeySeekEnabled;\r\n    }\r\n\r\n    public void setNumberKeySeekEnabled(boolean enable) {\r\n        mIsNumberKeySeekEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public FormatItem getFormat(int type) {\r\n        FormatItem format = null;\r\n\r\n        switch (type) {\r\n            case FormatItem.TYPE_VIDEO:\r\n                format = mVideoFormat;\r\n                break;\r\n            case FormatItem.TYPE_AUDIO:\r\n                format = mAudioFormat;\r\n                break;\r\n            case FormatItem.TYPE_SUBTITLE:\r\n                format = mSubtitleFormat;\r\n                break;\r\n        }\r\n\r\n        MediaTrack track = FormatItem.toMediaTrack(format);\r\n        if (track != null) {\r\n            track.isSaved = true;\r\n        }\r\n\r\n        return FormatItem.checkFormat(format, type);\r\n    }\r\n\r\n    public void setFormat(FormatItem format) {\r\n        //if (format == null || Helpers.equalsAny(format, mVideoFormat, mAudioFormat, mSubtitleFormat)) {\r\n        if (format == null) {\r\n            return;\r\n        }\r\n\r\n        switch (format.getType()) {\r\n            case FormatItem.TYPE_VIDEO:\r\n                mVideoFormat = format;\r\n                break;\r\n            case FormatItem.TYPE_AUDIO:\r\n                mAudioFormat = format;\r\n                break;\r\n            case FormatItem.TYPE_SUBTITLE:\r\n                setLastSubtitleFormat(format);\r\n                mSubtitleFormat = format;\r\n                break;\r\n        }\r\n        \r\n        persistState();\r\n    }\r\n\r\n    public void setTempVideoFormat(FormatItem format) {\r\n        mTempVideoFormat = format;\r\n    }\r\n\r\n    public FormatItem getTempVideoFormat() {\r\n        return mTempVideoFormat;\r\n    }\r\n\r\n    public FormatItem getLastSubtitleFormat() {\r\n        return !mLastSubtitleFormats.isEmpty() ? mLastSubtitleFormats.get(0) : FormatItem.SUBTITLE_NONE;\r\n    }\r\n\r\n    public List<FormatItem> getLastSubtitleFormats() {\r\n        return mLastSubtitleFormats;\r\n    }\r\n\r\n    private void setLastSubtitleFormat(FormatItem format) {\r\n        if (format != null && !format.isDefault()) {\r\n            mLastSubtitleFormats.remove(format);\r\n            mLastSubtitleFormats.add(0, format);\r\n        } else if (mSubtitleFormat != null && !mSubtitleFormat.isDefault()) {\r\n            mLastSubtitleFormats.remove(mSubtitleFormat);\r\n            mLastSubtitleFormats.add(0, mSubtitleFormat);\r\n        }\r\n    }\r\n\r\n    public void enableSubtitlesPerChannel(String channelId) {\r\n        mEnabledSubtitlesPerChannel.add(channelId);\r\n        persistState();\r\n    }\r\n\r\n    public void disableSubtitlesPerChannel(String channelId) {\r\n        mEnabledSubtitlesPerChannel.remove(channelId);\r\n        persistState();\r\n    }\r\n\r\n    public boolean isSubtitlesPerChannelEnabled(String channelId) {\r\n        if (channelId == null) {\r\n            return false;\r\n        }\r\n\r\n        return mEnabledSubtitlesPerChannel.contains(channelId);\r\n    }\r\n\r\n    public boolean isSubtitlesPerChannelEnabled() {\r\n        return mIsSubtitlesPerChannelEnabled;\r\n    }\r\n\r\n    public void setSubtitlesPerChannelEnabled(boolean enable) {\r\n        mIsSubtitlesPerChannelEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public void setVideoBufferType(int type) {\r\n        mVideoBufferType = type;\r\n        persistState();\r\n    }\r\n\r\n    public int getVideoBufferType() {\r\n        return mVideoBufferType;\r\n    }\r\n\r\n    public List<SubtitleStyle> getSubtitleStyles() {\r\n        return mSubtitleStyles;\r\n    }\r\n\r\n    public SubtitleStyle getSubtitleStyle() {\r\n        return mSubtitleStyles.get(mSubtitleStyleIndex);\r\n    }\r\n\r\n    public void setSubtitleStyle(SubtitleStyle subtitleStyle) {\r\n        mSubtitleStyleIndex = mSubtitleStyles.indexOf(subtitleStyle);\r\n        persistState();\r\n    }\r\n\r\n    public float getSubtitleScale() {\r\n        return mSubtitleScale;\r\n    }\r\n\r\n    public void setSubtitleScale(float scale) {\r\n        mSubtitleScale = scale;\r\n        persistState();\r\n    }\r\n\r\n    public float getSubtitlePosition() {\r\n        return mSubtitlePosition;\r\n    }\r\n\r\n    public void setSubtitlePosition(float position) {\r\n        mSubtitlePosition = position;\r\n        persistState();\r\n    }\r\n\r\n    public float getPlayerVolume() {\r\n        return mPlayerVolume;\r\n    }\r\n\r\n    public void setPlayerVolume(float scale) {\r\n        mPlayerVolume = scale;\r\n        persistState();\r\n    }\r\n\r\n    public int getResizeMode() {\r\n        return mResizeMode;\r\n    }\r\n\r\n    public void setResizeMode(int mode) {\r\n        mResizeMode = mode;\r\n        persistState();\r\n    }\r\n\r\n    public int getZoomPercents() {\r\n        return mZoomPercents;\r\n    }\r\n\r\n    public void setZoomPercents(int percents) {\r\n        mZoomPercents = percents;\r\n        persistState();\r\n    }\r\n\r\n    public float getAspectRatio() {\r\n        return mAspectRatio;\r\n    }\r\n\r\n    public void setAspectRatio(float ratio) {\r\n        mAspectRatio = ratio;\r\n        persistState();\r\n    }\r\n\r\n    public int getRotationAngle() {\r\n        return mRotationAngle;\r\n    }\r\n\r\n    public void setRotationAngle(int angle) {\r\n        mRotationAngle = angle;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isVideoFlipEnabled() {\r\n        return mIsVideoFlipEnabled;\r\n    }\r\n\r\n    public void setVideoFlipEnabled(boolean enabled) {\r\n        mIsVideoFlipEnabled = enabled;\r\n        persistState();\r\n    }\r\n\r\n    public float getSpeed() {\r\n        return getSpeed(null);\r\n    }\r\n\r\n    public void setSpeed(float speed) {\r\n        setSpeed(null, speed);\r\n    }\r\n\r\n    public float getSpeed(String channelId) {\r\n        SpeedItem speed = null;\r\n\r\n        if (isSpeedPerChannelEnabled() && channelId != null) {\r\n            speed = mSpeeds.get(channelId);\r\n            mSpeed = 1.0f; // reset speed if the channel not found\r\n        }\r\n\r\n        if (speed != null) {\r\n            mSpeed = speed.speed;\r\n        }\r\n\r\n        return mSpeed;\r\n    }\r\n\r\n    public void setSpeed(String channelId, float speed) {\r\n        if (mSpeed == speed && channelId == null) {\r\n            return;\r\n        }\r\n\r\n        if (isSpeedPerChannelEnabled() && channelId != null) {\r\n            if (Helpers.floatEquals(speed, 1.0f)) {\r\n                mSpeeds.remove(channelId);\r\n            } else {\r\n                mSpeeds.put(channelId, new SpeedItem(channelId, speed));\r\n            }\r\n        }\r\n        setLastSpeed(speed);\r\n        mSpeed = speed;\r\n        persistState();\r\n    }\r\n\r\n    public float getLastSpeed() {\r\n        return mLastSpeed;\r\n    }\r\n\r\n    public void setLastSpeed(float speed) {\r\n        if (speed > 0 && !Helpers.floatEquals(speed, 1.0f)) {\r\n            mLastSpeed = speed;\r\n        } else if (mSpeed > 0 && !Helpers.floatEquals(mSpeed, 1.0f)) {\r\n            mLastSpeed = mSpeed;\r\n        }\r\n    }\r\n\r\n    public boolean isSpeedPerChannelEnabled() {\r\n        return mIsSpeedPerChannelEnabled;\r\n    }\r\n\r\n    public void setSpeedPerChannelEnabled(boolean enable) {\r\n        mIsSpeedPerChannelEnabled = enable;\r\n        mIsSpeedPerVideoEnabled = false;\r\n        mIsAllSpeedEnabled = false;\r\n        persistState();\r\n    }\r\n\r\n    public int getAudioDelayMs() {\r\n        return mAudioDelayMs;\r\n    }\r\n\r\n    public void setAudioDelayMs(int delayMs) {\r\n        mAudioDelayMs = delayMs;\r\n        persistState();\r\n    }\r\n\r\n    public float getPitch() {\r\n        return mPitch;\r\n    }\r\n\r\n    public void setPitch(float pitch) {\r\n        mPitch = pitch;\r\n        persistState();\r\n    }\r\n\r\n    public String getAudioLanguage() {\r\n        return mAudioLanguage;\r\n    }\r\n\r\n    public void setAudioLanguage(String language) {\r\n        mAudioLanguage = language;\r\n        setLastAudioLanguage(language);\r\n        persistState();\r\n    }\r\n\r\n    public List<String> getLastAudioLanguages() {\r\n        return mLastAudioLanguages;\r\n    }\r\n\r\n    private void setLastAudioLanguage(String language) {\r\n        mLastAudioLanguages.remove(language);\r\n        mLastAudioLanguages.add(0, language);\r\n    }\r\n\r\n    public String getSubtitleLanguage() {\r\n        return mSubtitleLanguage;\r\n    }\r\n\r\n    public void setSubtitleLanguage(String language) {\r\n        mSubtitleLanguage = language;\r\n        persistState();\r\n    }\r\n\r\n    public float getSleepTimerHours() {\r\n        return mSleepTimerHours;\r\n    }\r\n\r\n    public void setSleepTimerHours(float hours) {\r\n        mSleepTimerHours = hours;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isTimeCorrectionEnabled() {\r\n        return mIsTimeCorrectionEnabled;\r\n    }\r\n\r\n    public void setTimeCorrectionEnabled(boolean enable) {\r\n        mIsTimeCorrectionEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isSkip24RateEnabled() {\r\n        return mIsSkip24RateEnabled;\r\n    }\r\n\r\n    public void setSkip24RateEnabled(boolean enable) {\r\n        mIsSkip24RateEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isSkipShortsEnabled() {\r\n        return mIsSkipShortsEnabled;\r\n    }\r\n\r\n    public void setSkipShortsEnabled(boolean enable) {\r\n        mIsSkipShortsEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isLiveChatEnabled() {\r\n        return mIsLiveChatEnabled;\r\n    }\r\n\r\n    public void setLiveChatEnabled(boolean enable) {\r\n        mIsLiveChatEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public FormatItem getDefaultAudioFormat() {\r\n        // Android 4 (probably some others) doesn't support opus (ac3 will be reverted to opus)\r\n        // Note, 5.1 mp4a doesn't work in 5.1 mode\r\n        // Use opus (ac3 fallback) on modern devices. vp9 and opus should be supported at the same time?\r\n        return DeviceHelpers.isVP9ResolutionSupported(2160) ? FormatItem.AUDIO_51_AC3 : FormatItem.AUDIO_HQ_MP4A;\r\n    }\r\n\r\n    public FormatItem getDefaultVideoFormat() {\r\n        FormatItem formatItem = mDefaultVideoFormats.get(Build.MODEL);\r\n\r\n        if (formatItem == null) {\r\n            if (VERSION.SDK_INT <= 19) { // Android 4 playback crash fix (memory leak?)\r\n                formatItem = FormatItem.VIDEO_SD_AVC_30;\r\n            } else if (VERSION.SDK_INT <= 23 && DeviceHelpers.isVP9ResolutionSupported(1080)) {\r\n                formatItem = FormatItem.VIDEO_FHD_VP9_60;\r\n            } else if (DeviceHelpers.isVP9ResolutionSupported(2160)) {\r\n                formatItem = FormatItem.VIDEO_4K_VP9_60;\r\n            } else if (DeviceHelpers.isVP9ResolutionSupported(1080)) {\r\n                formatItem = FormatItem.VIDEO_FHD_VP9_60;\r\n            }\r\n        }\r\n\r\n        return formatItem != null ? formatItem : FormatItem.VIDEO_HD_AVC_30;\r\n    }\r\n\r\n    public FormatItem getDefaultSubtitleFormat() {\r\n        return FormatItem.SUBTITLE_NONE;\r\n    }\r\n\r\n    public int getSeekIncrementMs() {\r\n        return mSeekIncrementMs;\r\n    }\r\n\r\n    public void setSeekIncrementMs(int seekIncrementMs) {\r\n        mSeekIncrementMs = seekIncrementMs;\r\n        persistState();\r\n    }\r\n\r\n    public void setAfrSwitchTimeMs(long timeMillis) {\r\n        mAfrSwitchTimeMs = timeMillis;\r\n    }\r\n\r\n    public long getAfrSwitchTimeMs() {\r\n        return mAfrSwitchTimeMs;\r\n    }\r\n\r\n    private void initSubtitleStyles() {\r\n        mSubtitleStyles.add(new SubtitleStyle(R.string.subtitle_white_transparent, R.color.light_grey, R.color.transparent, CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW));\r\n        mSubtitleStyles.add(new SubtitleStyle(R.string.subtitle_white_semi_transparent, R.color.light_grey, R.color.semi_transparent, CaptionStyleCompat.EDGE_TYPE_OUTLINE));\r\n        mSubtitleStyles.add(new SubtitleStyle(R.string.subtitle_white_black, R.color.light_grey, R.color.black, CaptionStyleCompat.EDGE_TYPE_OUTLINE));\r\n        mSubtitleStyles.add(new SubtitleStyle(R.string.subtitle_yellow_transparent, R.color.yellow, R.color.transparent, CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW));\r\n        mSubtitleStyles.add(new SubtitleStyle(R.string.subtitle_yellow_semi_transparent, R.color.yellow, R.color.semi_transparent, CaptionStyleCompat.EDGE_TYPE_OUTLINE));\r\n        mSubtitleStyles.add(new SubtitleStyle(R.string.subtitle_yellow_black, R.color.yellow, R.color.black, CaptionStyleCompat.EDGE_TYPE_OUTLINE));\r\n\r\n        if (VERSION.SDK_INT >= 19) {\r\n            mSubtitleStyles.add(new SubtitleStyle(R.string.subtitle_system));\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Overrides for auto detected values\r\n     */\r\n    private void initDefaultFormats() {\r\n        mDefaultVideoFormats.put(\"SHIELD Android TV\", FormatItem.VIDEO_4K_VP9_60);\r\n        mDefaultVideoFormats.put(\"AFTMM\", FormatItem.VIDEO_4K_VP9_60); // Stick 4K 2018\r\n        mDefaultVideoFormats.put(\"AFTKA\", FormatItem.VIDEO_4K_VP9_60); // Stick 4K Max 2021\r\n        mDefaultVideoFormats.put(\"P1\", FormatItem.VIDEO_FHD_AVC_60); // Chinese projector (see annoying emails)\r\n    }\r\n\r\n    private void restoreState() {\r\n        String data = mPrefs.getProfileData(VIDEO_PLAYER_DATA);\r\n\r\n        String[] split = Helpers.splitData(data);\r\n\r\n        mOKButtonBehavior = Helpers.parseInt(split, 0, OK_ONLY_UI);\r\n        mUiHideTimeoutSec = Helpers.parseInt(split, 1, 3);\r\n        // mIsAbsoluteDateEnabled\r\n        mSeekPreviewMode = Helpers.parseInt(split, 3, SEEK_PREVIEW_SINGLE);\r\n        mIsSeekConfirmPauseEnabled = Helpers.parseBoolean(split, 4, false);\r\n        mIsClockEnabled = Helpers.parseBoolean(split, 5, true);\r\n        mIsRemainingTimeEnabled = Helpers.parseBoolean(split, 6, true);\r\n        mBackgroundMode = Helpers.parseInt(split, 7, PlayerEngine.BACKGROUND_MODE_DEFAULT);\r\n        // afrData was there\r\n        mVideoFormat = Helpers.firstNonNull(ExoFormatItem.from(Helpers.parseStr(split, 9)), getDefaultVideoFormat());\r\n        mAudioFormat = Helpers.firstNonNull(ExoFormatItem.from(Helpers.parseStr(split, 10)), getDefaultAudioFormat());\r\n        mSubtitleFormat = Helpers.firstNonNull(ExoFormatItem.from(Helpers.parseStr(split, 11)), getDefaultSubtitleFormat());\r\n        mVideoBufferType = Helpers.parseInt(split, 12, PlayerEngine.BUFFER_MEDIUM);\r\n        mSubtitleStyleIndex = Helpers.parseInt(split, 13, 4); // yellow on semi bg\r\n        mResizeMode = Helpers.parseInt(split, 14, PlayerEngine.RESIZE_MODE_DEFAULT);\r\n        mSpeed = Helpers.parseFloat(split, 15, 1.0f);\r\n        mIsAfrEnabled = Helpers.parseBoolean(split, 16, false);\r\n        mIsAfrFpsCorrectionEnabled = Helpers.parseBoolean(split, 17, true);\r\n        mIsAfrResSwitchEnabled = Helpers.parseBoolean(split, 18, false);\r\n        // old afr delay sec was there\r\n        mAudioDelayMs = Helpers.parseInt(split, 20, 0);\r\n        mIsAllSpeedEnabled = Helpers.parseBoolean(split, 21, false);\r\n        // repeat mode was here\r\n        // didn't remember what was there\r\n        mIsLegacyCodecsForced = Helpers.parseBoolean(split, 24, false);\r\n        mSleepTimerHours = Helpers.parseFloat(split, 25, 0);\r\n        // old player tweaks\r\n        mIsQualityInfoEnabled = Helpers.parseBoolean(split, 28, true);\r\n        mIsSpeedPerVideoEnabled = Helpers.parseBoolean(split, 29, false);\r\n        mAspectRatio = Helpers.parseFloat(split, 30, PlayerEngine.ASPECT_RATIO_DEFAULT);\r\n        mIsGlobalClockEnabled = Helpers.parseBoolean(split, 31, false);\r\n        mIsTimeCorrectionEnabled = Helpers.parseBoolean(split, 32, true);\r\n        mIsGlobalEndingTimeEnabled = Helpers.parseBoolean(split, 33, false);\r\n        mIsEndingTimeEnabled = Helpers.parseBoolean(split, 34, false);\r\n        mIsDoubleRefreshRateEnabled = Helpers.parseBoolean(split, 35, true);\r\n        mIsSeekConfirmPlayEnabled = Helpers.parseBoolean(split, 36, false);\r\n        mSeekIncrementMs = Helpers.parseInt(split, 37, 10_000);\r\n        // old subs size px\r\n        mSubtitleScale = Helpers.parseFloat(split, 39, 1.0f);\r\n        mPlayerVolume = Helpers.parseFloat(split, 40, 1.0f);\r\n        mIsTooltipsEnabled = Helpers.parseBoolean(split, 41, true);\r\n        mSubtitlePosition = Helpers.parseFloat(split, 42, 0.1f);\r\n        mIsNumberKeySeekEnabled = Helpers.parseBoolean(split, 43, true);\r\n        mIsSkip24RateEnabled = Helpers.parseBoolean(split, 44, false);\r\n        mAfrPauseMs = Helpers.parseInt(split, 45, 0);\r\n        mIsLiveChatEnabled = Helpers.parseBoolean(split, 46, false);\r\n        mLastSubtitleFormats = Helpers.parseList(split, 47, ExoFormatItem::from);\r\n        mLastSpeed = Helpers.parseFloat(split, 48, 1.0f);\r\n        mRotationAngle = Helpers.parseInt(split, 49, 0);\r\n        mZoomPercents = Helpers.parseInt(split, 50, -1);\r\n        mPlaybackMode = Helpers.parseInt(split, 51, PlayerConstants.PLAYBACK_MODE_ALL);\r\n        mAudioLanguage = Helpers.parseStr(split, 52, LocaleUtility.getCurrentLanguage(mPrefs.getContext()));\r\n        mSubtitleLanguage = Helpers.parseStr(split, 53, LocaleUtility.getCurrentLanguage(mPrefs.getContext()));\r\n        mEnabledSubtitlesPerChannel = Helpers.parseStrList(split, 54);\r\n        mIsSubtitlesPerChannelEnabled = Helpers.parseBoolean(split, 55, true);\r\n        mIsSpeedPerChannelEnabled = Helpers.parseBoolean(split, 56, true);\r\n        String[] speeds = Helpers.parseArray(split, 57);\r\n        mPitch = Helpers.parseFloat(split, 58, 1.0f);\r\n        mIsSkipShortsEnabled = Helpers.parseBoolean(split, 59, false);\r\n        mLastAudioLanguages = Helpers.parseStrList(split, 60);\r\n        mIsVideoFlipEnabled = Helpers.parseBoolean(split, 61, false);\r\n\r\n        if (speeds != null) {\r\n            for (String speedSpec : speeds) {\r\n                SpeedItem item = SpeedItem.fromString(speedSpec);\r\n                mSpeeds.put(item.channelId, item);\r\n            }\r\n        }\r\n\r\n        if (!mIsAllSpeedEnabled) {\r\n            mSpeed = 1.0f;\r\n        }\r\n    }\r\n\r\n    public void persistNow() {\r\n        Utils.post(mPersistStateInt);\r\n    }\r\n\r\n    private void persistState() {\r\n        onDataChange();\r\n        Utils.postDelayed(mPersistStateInt, 10_000);\r\n    }\r\n\r\n    private void persistStateInt() {\r\n        mPrefs.setProfileData(VIDEO_PLAYER_DATA, Helpers.mergeData(mOKButtonBehavior, mUiHideTimeoutSec, null,\r\n                mSeekPreviewMode, mIsSeekConfirmPauseEnabled, mIsClockEnabled, mIsRemainingTimeEnabled, mBackgroundMode, null,\r\n                mVideoFormat, mAudioFormat, mSubtitleFormat, mVideoBufferType, mSubtitleStyleIndex, mResizeMode, mSpeed,\r\n                mIsAfrEnabled, mIsAfrFpsCorrectionEnabled, mIsAfrResSwitchEnabled, null, mAudioDelayMs, mIsAllSpeedEnabled, null, null,\r\n                mIsLegacyCodecsForced, mSleepTimerHours, null, null, mIsQualityInfoEnabled, mIsSpeedPerVideoEnabled, mAspectRatio,\r\n                mIsGlobalClockEnabled, mIsTimeCorrectionEnabled, mIsGlobalEndingTimeEnabled, mIsEndingTimeEnabled, mIsDoubleRefreshRateEnabled,\r\n                mIsSeekConfirmPlayEnabled, mSeekIncrementMs, null, mSubtitleScale, mPlayerVolume, mIsTooltipsEnabled, mSubtitlePosition,\r\n                mIsNumberKeySeekEnabled, mIsSkip24RateEnabled, mAfrPauseMs, mIsLiveChatEnabled, mLastSubtitleFormats, mLastSpeed, mRotationAngle,\r\n                mZoomPercents, mPlaybackMode, mAudioLanguage, mSubtitleLanguage, mEnabledSubtitlesPerChannel, mIsSubtitlesPerChannelEnabled,\r\n                mIsSpeedPerChannelEnabled, Helpers.mergeArray(mSpeeds.values().toArray()), mPitch, mIsSkipShortsEnabled, mLastAudioLanguages, mIsVideoFlipEnabled\r\n        ));\r\n    }\r\n\r\n    @Override\r\n    public void onProfileChanged() {\r\n        Utils.removeCallbacks(mPersistStateInt);\r\n\r\n        // reset on profile change\r\n        mSpeeds.clear();\r\n\r\n        restoreState();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/PlayerTweaksData.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.prefs.GlobalPreferences;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs.ProfileChangeListener;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\nimport com.liskovsoft.youtubeapi.service.internal.MediaServiceData;\r\n\r\npublic class PlayerTweaksData implements ProfileChangeListener {\r\n    private static final String VIDEO_PLAYER_TWEAKS_DATA = \"video_player_tweaks_data\";\r\n    public static final int PLAYER_DATA_SOURCE_DEFAULT = 0;\r\n    public static final int PLAYER_DATA_SOURCE_OKHTTP = 1;\r\n    public static final int PLAYER_DATA_SOURCE_CRONET = 2;\r\n    public static final int PLAYER_BUTTON_VIDEO_ZOOM = 1;\r\n    public static final int PLAYER_BUTTON_SEARCH = 1 << 1;\r\n    public static final int PLAYER_BUTTON_PIP = 1 << 2;\r\n    //public static final int PLAYER_BUTTON_SCREEN_OFF = 1 << 3;\r\n    public static final int PLAYER_BUTTON_PLAYBACK_QUEUE = 1 << 4;\r\n    public static final int PLAYER_BUTTON_VIDEO_SPEED = 1 << 5;\r\n    public static final int PLAYER_BUTTON_VIDEO_STATS = 1 << 6;\r\n    public static final int PLAYER_BUTTON_OPEN_CHANNEL = 1 << 7;\r\n    public static final int PLAYER_BUTTON_SUBTITLES = 1 << 8;\r\n    public static final int PLAYER_BUTTON_SUBSCRIBE = 1 << 9;\r\n    public static final int PLAYER_BUTTON_LIKE = 1 << 10;\r\n    public static final int PLAYER_BUTTON_DISLIKE = 1 << 11;\r\n    public static final int PLAYER_BUTTON_ADD_TO_PLAYLIST = 1 << 12;\r\n    public static final int PLAYER_BUTTON_PLAY_PAUSE = 1 << 13;\r\n    public static final int PLAYER_BUTTON_REPEAT_MODE = 1 << 14;\r\n    public static final int PLAYER_BUTTON_NEXT = 1 << 15;\r\n    public static final int PLAYER_BUTTON_PREVIOUS = 1 << 16;\r\n    public static final int PLAYER_BUTTON_HIGH_QUALITY = 1 << 17;\r\n    public static final int PLAYER_BUTTON_VIDEO_INFO = 1 << 18;\r\n    public static final int PLAYER_BUTTON_SHARE = 1 << 19;\r\n    public static final int PLAYER_BUTTON_SEEK_INTERVAL = 1 << 20;\r\n    public static final int PLAYER_BUTTON_CONTENT_BLOCK = 1 << 21;\r\n    public static final int PLAYER_BUTTON_CHAT = 1 << 22;\r\n    public static final int PLAYER_BUTTON_VIDEO_ROTATE = 1 << 23;\r\n    public static final int PLAYER_BUTTON_SCREEN_DIMMING = 1 << 24;\r\n    public static final int PLAYER_BUTTON_SOUND_OFF = 1 << 25;\r\n    public static final int PLAYER_BUTTON_AFR = 1 << 26;\r\n    public static final int PLAYER_BUTTON_VIDEO_FLIP = 1 << 27;\r\n    public static final int PLAYER_BUTTON_DEFAULT = PLAYER_BUTTON_SEARCH | PLAYER_BUTTON_PIP | PLAYER_BUTTON_SCREEN_DIMMING | PLAYER_BUTTON_VIDEO_SPEED |\r\n            PLAYER_BUTTON_VIDEO_STATS | PLAYER_BUTTON_OPEN_CHANNEL | PLAYER_BUTTON_SUBTITLES | PLAYER_BUTTON_SUBSCRIBE |\r\n            PLAYER_BUTTON_LIKE | PLAYER_BUTTON_DISLIKE | PLAYER_BUTTON_ADD_TO_PLAYLIST | PLAYER_BUTTON_PLAY_PAUSE |\r\n            PLAYER_BUTTON_REPEAT_MODE | PLAYER_BUTTON_NEXT | PLAYER_BUTTON_PREVIOUS | PLAYER_BUTTON_HIGH_QUALITY |\r\n            PLAYER_BUTTON_VIDEO_INFO | PLAYER_BUTTON_CHAT;\r\n    public static final int DNS_TYPE_SYSTEM = GlobalPreferences.DNS_TYPE_SYSTEM;\r\n    public static final int DNS_TYPE_IPV4 = GlobalPreferences.DNS_TYPE_IPV4;\r\n    public static final int DNS_TYPE_GOOGLE = GlobalPreferences.DNS_TYPE_GOOGLE;\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static PlayerTweaksData sInstance;\r\n    private final AppPrefs mPrefs;\r\n    private boolean mIsAmlogicFixEnabled;\r\n    private boolean mIsAmazonFrameDropFixEnabled;\r\n    private boolean mIsSonyFrameDropFixEnabled;\r\n    private boolean mIsSnapToVsyncDisabled;\r\n    private boolean mIsProfileLevelCheckSkipped;\r\n    private boolean mIsSWDecoderForced;\r\n    private boolean mIsTextureViewEnabled;\r\n    private boolean mIsSetOutputSurfaceWorkaroundEnabled;\r\n    private boolean mIsAudioSyncFixEnabled;\r\n    private boolean mIsKeepFinishedActivityEnabled;\r\n    private boolean mIsHlsStreamsForced;\r\n    private boolean mIsDashUrlStreamsForced;\r\n    private boolean mIsPlaybackNotificationsDisabled;\r\n    private boolean mIsTunneledPlaybackEnabled;\r\n    private int mPlayerButtons;\r\n    private boolean mIsNoFpsPresetsEnabled;\r\n    private boolean mIsRememberPositionOfShortVideosEnabled;\r\n    private boolean mIsRememberPositionOfLiveVideosEnabled;\r\n    private boolean mIsSuggestionsDisabled;\r\n    private boolean mIsAvcOverVp9Preferred;\r\n    private boolean mIsChatPlacedLeft;\r\n    private boolean mIsCommentsPlacedLeft;\r\n    private boolean mIsRealChannelIconEnabled;\r\n    private float mPixelRatio;\r\n    private boolean mIsQualityInfoBitrateEnabled;\r\n    private boolean mIsAudioTimeStretchingEnabled;\r\n    private boolean mIsSpeedButtonOldBehaviorEnabled;\r\n    private boolean mIsButtonLongClickEnabled;\r\n    private boolean mIsLongSpeedListEnabled;\r\n    private boolean mIsExtraLongSpeedListEnabled;\r\n    private int mPlayerDataSource;\r\n    private boolean mUnlockAllFormats;\r\n    private boolean mIsBufferOnStreamsDisabled;\r\n    private boolean mIsSectionPlaylistEnabled;\r\n    private boolean mIsScreenOffTimeoutEnabled;\r\n    private boolean mIsBootScreenOffEnabled;\r\n    private int mScreenOffTimeoutSec;\r\n    private int mScreenOffDimmingPercents;\r\n    private boolean mIsUIAnimationsEnabled;\r\n    private boolean mIsLikesCounterEnabled;\r\n    private boolean mIsChapterNotificationEnabled;\r\n    private boolean mIsPlayerUiOnNextEnabled;\r\n    private boolean mIsPlayerAutoVolumeEnabled;\r\n    private boolean mIsSyncRowButtonIndexEnabled;\r\n    private boolean mIsUnsafeAudioFormatsEnabled;\r\n    private boolean mIsLoopShortsEnabled;\r\n    private boolean mIsQuickSkipShortsEnabled;\r\n    private boolean mIsQuickSkipShortsAltEnabled;\r\n    private boolean mIsQuickSkipVideosEnabled;\r\n    private boolean mIsQuickSkipVideosAltEnabled;\r\n    private boolean mIsOculusQuestFixEnabled;\r\n    private boolean mIsAudioFocusEnabled;\r\n    private boolean mIsNetworkErrorFixingDisabled;\r\n    private boolean mIsDontResizeVideoToFitDialogEnabled;\r\n    private boolean mIsSuggestionsHorizontallyScrolled;\r\n    private boolean mIsQueueRespectsPlaybackMode;\r\n    private final Runnable mPersistDataInt = this::persistDataInt;\r\n\r\n    private PlayerTweaksData(Context context) {\r\n        mPrefs = AppPrefs.instance(context);\r\n        mPrefs.addListener(this);\r\n        restoreData();\r\n    }\r\n\r\n    public static PlayerTweaksData instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new PlayerTweaksData(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public boolean isAmlogicFixEnabled() {\r\n        return mIsAmlogicFixEnabled;\r\n    }\r\n\r\n    public void setAmlogicFixEnabled(boolean enable) {\r\n        mIsAmlogicFixEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isAmazonFrameDropFixEnabled() {\r\n        return mIsAmazonFrameDropFixEnabled;\r\n    }\r\n\r\n    public void setAmazonFrameDropFixEnabled(boolean enable) {\r\n        mIsAmazonFrameDropFixEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isSonyFrameDropFixEnabled() {\r\n        return mIsSonyFrameDropFixEnabled;\r\n    }\r\n\r\n    public void setSonyFrameDropFixEnabled(boolean enable) {\r\n        mIsSonyFrameDropFixEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isSnappingToVsyncDisabled() {\r\n        return mIsSnapToVsyncDisabled;\r\n    }\r\n\r\n    public void setSnappingToVsyncDisabled(boolean disable) {\r\n        mIsSnapToVsyncDisabled = disable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isProfileLevelCheckSkipped() {\r\n        return mIsProfileLevelCheckSkipped;\r\n    }\r\n\r\n    public void setProfileLevelCheckSkipped(boolean enable) {\r\n        mIsProfileLevelCheckSkipped = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isSWDecoderForced() {\r\n        return mIsSWDecoderForced;\r\n    }\r\n\r\n    public void setSWDecoderForced(boolean force) {\r\n        mIsSWDecoderForced = force;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isTextureViewEnabled() {\r\n        return mIsTextureViewEnabled;\r\n    }\r\n\r\n    public void setTextureViewEnabled(boolean enable) {\r\n        mIsTextureViewEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isSetOutputSurfaceWorkaroundEnabled() {\r\n        return mIsSetOutputSurfaceWorkaroundEnabled;\r\n    }\r\n\r\n    /**\r\n     * Need to be enabled on older version of ExoPlayer (e.g. 2.10.6).<br/>\r\n     * It's because there's no tweaks for modern devices.\r\n     */\r\n    public void setSetOutputSurfaceWorkaroundEnabled(boolean enable) {\r\n        mIsSetOutputSurfaceWorkaroundEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isAudioSyncFixEnabled() {\r\n        return mIsAudioSyncFixEnabled;\r\n    }\r\n\r\n    public void setAudioSyncFixEnabled(boolean enable) {\r\n        mIsAudioSyncFixEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    /**\r\n     * Fix crashes on chinese projectors\r\n     */\r\n    public boolean isKeepFinishedActivityEnabled() {\r\n        return mIsKeepFinishedActivityEnabled;\r\n    }\r\n\r\n    /**\r\n     * Fix crashes on chinese projectors\r\n     */\r\n    public void setKeepFinishedActivityEnabled(boolean enable) {\r\n        mIsKeepFinishedActivityEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isHlsStreamsForced() {\r\n        return mIsHlsStreamsForced;\r\n    }\r\n\r\n    public void setHlsStreamsForced(boolean enable) {\r\n        mIsHlsStreamsForced = enable;\r\n        mIsDashUrlStreamsForced = false;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isDashUrlStreamsForced() {\r\n        return mIsDashUrlStreamsForced;\r\n    }\r\n\r\n    public void setDashUrlStreamsForced(boolean enable) {\r\n        mIsDashUrlStreamsForced = enable;\r\n        mIsHlsStreamsForced = false;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isPlaybackNotificationsDisabled() {\r\n        return mIsPlaybackNotificationsDisabled;\r\n    }\r\n\r\n    public void setPlaybackNotificationsDisabled(boolean disable) {\r\n        mIsPlaybackNotificationsDisabled = disable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isTunneledPlaybackEnabled() {\r\n        return mIsTunneledPlaybackEnabled;\r\n    }\r\n\r\n    public void setTunneledPlaybackEnabled(boolean enable) {\r\n        mIsTunneledPlaybackEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isUnsafeAudioFormatsEnabled() {\r\n        return mIsUnsafeAudioFormatsEnabled;\r\n    }\r\n\r\n    public void setUnsafeAudioFormatsEnabled(boolean enable) {\r\n        mIsUnsafeAudioFormatsEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isPlayerButtonEnabled(int menuItems) {\r\n        return (mPlayerButtons & menuItems) == menuItems;\r\n    }\r\n\r\n    public void setPlayerButtonEnabled(int playerButtons) {\r\n        mPlayerButtons |= playerButtons;\r\n        persistData();\r\n    }\r\n\r\n    public void setPlayerButtonDisabled(int playerButtons) {\r\n        mPlayerButtons &= ~playerButtons;\r\n        persistData();\r\n    }\r\n\r\n    public int getPlayerDataSource() {\r\n        return mPlayerDataSource;\r\n    }\r\n\r\n    public void setPlayerDataSource(int dataSource) {\r\n        mPlayerDataSource = dataSource;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isNoFpsPresetsEnabled() {\r\n        return mIsNoFpsPresetsEnabled;\r\n    }\r\n\r\n    public void setNoFpsPresetsEnabled(boolean enable) {\r\n        mIsNoFpsPresetsEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isAvcOverVp9Preferred() {\r\n        return mIsAvcOverVp9Preferred;\r\n    }\r\n\r\n    public void setAvcOverVp9Preferred(boolean prefer) {\r\n        mIsAvcOverVp9Preferred = prefer;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isRememberPositionOfShortVideosEnabled() {\r\n        return mIsRememberPositionOfShortVideosEnabled;\r\n    }\r\n\r\n    public void setRememberPositionOfShortVideosEnabled(boolean enable) {\r\n        mIsRememberPositionOfShortVideosEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isRememberPositionOfLiveVideosEnabled() {\r\n        return mIsRememberPositionOfLiveVideosEnabled;\r\n    }\r\n\r\n    public void setRememberPositionOfLiveVideosEnabled(boolean enable) {\r\n        mIsRememberPositionOfLiveVideosEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isSuggestionsDisabled() {\r\n        return mIsSuggestionsDisabled;\r\n    }\r\n\r\n    public void setSuggestionsDisabled(boolean disable) {\r\n        mIsSuggestionsDisabled = disable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isChatPlacedLeft() {\r\n        return mIsChatPlacedLeft;\r\n    }\r\n\r\n    public void setChatPlacedLeft(boolean left) {\r\n        mIsChatPlacedLeft = left;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isCommentsPlacedLeft() {\r\n        return mIsCommentsPlacedLeft;\r\n    }\r\n\r\n    public void setCommentsPlacedLeft(boolean left) {\r\n        mIsCommentsPlacedLeft = left;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isRealChannelIconEnabled() {\r\n        return mIsRealChannelIconEnabled;\r\n    }\r\n\r\n    public void setRealChannelIconEnabled(boolean enable) {\r\n        mIsRealChannelIconEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public float getPixelRatio() {\r\n        return mPixelRatio;\r\n    }\r\n\r\n    public void setPixelRatio(float pixelRatio) {\r\n        mPixelRatio = pixelRatio;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isQualityInfoBitrateEnabled() {\r\n        return mIsQualityInfoBitrateEnabled;\r\n    }\r\n\r\n    public void setQualityInfoBitrateEnabled(boolean enable) {\r\n        mIsQualityInfoBitrateEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isAudioTimeStretchingEnabled() {\r\n        return mIsAudioTimeStretchingEnabled;\r\n    }\r\n\r\n    public void setAudioTimeStretchingEnabled(boolean enable) {\r\n        mIsAudioTimeStretchingEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isSpeedButtonOldBehaviorEnabled() {\r\n        return mIsSpeedButtonOldBehaviorEnabled;\r\n    }\r\n\r\n    public void setSpeedButtonOldBehaviorEnabled(boolean enable) {\r\n        mIsSpeedButtonOldBehaviorEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isOculusQuestFixEnabled() {\r\n        return mIsOculusQuestFixEnabled;\r\n    }\r\n\r\n    public void setOculusQuestFixEnabled(boolean enable) {\r\n        mIsOculusQuestFixEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isButtonLongClickEnabled() {\r\n        return mIsButtonLongClickEnabled;\r\n    }\r\n\r\n    public void setButtonLongClickEnabled(boolean enable) {\r\n        mIsButtonLongClickEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isLongSpeedListEnabled() {\r\n        return mIsLongSpeedListEnabled;\r\n    }\r\n\r\n    public void setLongSpeedListEnabled(boolean enable) {\r\n        mIsExtraLongSpeedListEnabled = false;\r\n        mIsLongSpeedListEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isExtraLongSpeedListEnabled() {\r\n        return mIsExtraLongSpeedListEnabled;\r\n    }\r\n\r\n    public void setExtraLongSpeedListEnabled(boolean enable) {\r\n        mIsLongSpeedListEnabled = false;\r\n        mIsExtraLongSpeedListEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isAllFormatsUnlocked() {\r\n        return mUnlockAllFormats;\r\n    }\r\n\r\n    public void setAllFormatsUnlocked(boolean unlock) {\r\n        mUnlockAllFormats = unlock;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isBufferOnStreamsDisabled() {\r\n        return mIsBufferOnStreamsDisabled;\r\n    }\r\n\r\n    public void setBufferOnStreamsDisabled(boolean disable) {\r\n        mIsBufferOnStreamsDisabled = disable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isSectionPlaylistEnabled() {\r\n        return mIsSectionPlaylistEnabled;\r\n    }\r\n\r\n    public void setSectionPlaylistEnabled(boolean enable) {\r\n        mIsSectionPlaylistEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isScreenOffTimeoutEnabled() {\r\n        return mIsScreenOffTimeoutEnabled;\r\n    }\r\n\r\n    public void setScreenOffTimeoutEnabled(boolean enable) {\r\n        mIsScreenOffTimeoutEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public int getScreenOffTimeoutSec() {\r\n        return mScreenOffTimeoutSec;\r\n    }\r\n\r\n    public void setScreenOffTimeoutSec(int timeoutSec) {\r\n        mScreenOffTimeoutSec = timeoutSec;\r\n        mIsScreenOffTimeoutEnabled = mIsScreenOffTimeoutEnabled && timeoutSec > 0;\r\n        persistData();\r\n    }\r\n\r\n    public int getScreenOffDimmingPercents() {\r\n        return mScreenOffDimmingPercents;\r\n    }\r\n\r\n    public void setScreenOffDimmingPercents(int percents) {\r\n        mScreenOffDimmingPercents = percents;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isBootScreenOffEnabled() {\r\n        return mIsBootScreenOffEnabled && !isScreenOffTimeoutEnabled();\r\n    }\r\n\r\n    public void setBootScreenOffEnabled(boolean enable) {\r\n        mIsBootScreenOffEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isUIAnimationsEnabled() {\r\n        return mIsUIAnimationsEnabled;\r\n    }\r\n\r\n    public void setUIAnimationsEnabled(boolean enable) {\r\n        mIsUIAnimationsEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isLikesCounterEnabled() {\r\n        return mIsLikesCounterEnabled;\r\n    }\r\n\r\n    public void setLikesCounterEnabled(boolean enable) {\r\n        mIsLikesCounterEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isChapterNotificationEnabled() {\r\n        return mIsChapterNotificationEnabled;\r\n    }\r\n\r\n    public void setChapterNotificationEnabled(boolean enable) {\r\n        mIsChapterNotificationEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isPlayerUiOnNextEnabled() {\r\n        return mIsPlayerUiOnNextEnabled;\r\n    }\r\n\r\n    public void setPlayerUiOnNextEnabled(boolean enable) {\r\n        mIsPlayerUiOnNextEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isPlayerAutoVolumeEnabled() {\r\n        return mIsPlayerAutoVolumeEnabled;\r\n    }\r\n\r\n    public void setPlayerAutoVolumeEnabled(boolean enable) {\r\n        mIsPlayerAutoVolumeEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isAudioFocusEnabled() {\r\n        return mIsAudioFocusEnabled;\r\n    }\r\n\r\n    public void setAudioFocusEnabled(boolean enable) {\r\n        mIsAudioFocusEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isSyncRowButtonIndexEnabled() {\r\n        return mIsSyncRowButtonIndexEnabled;\r\n    }\r\n\r\n    public void setSyncRowButtonIndexEnabled(boolean enable) {\r\n        mIsSyncRowButtonIndexEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isLoopShortsEnabled() {\r\n        return mIsLoopShortsEnabled;\r\n    }\r\n\r\n    public void setLoopShortsEnabled(boolean enable) {\r\n        mIsLoopShortsEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isQuickSkipShortsEnabled() {\r\n        return mIsQuickSkipShortsEnabled;\r\n    }\r\n\r\n    public void setQuickSkipShortsEnabled(boolean enable) {\r\n        resetSkipShortsSettings();\r\n        mIsQuickSkipShortsEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isQuickSkipShortsAltEnabled() {\r\n        return mIsQuickSkipShortsAltEnabled;\r\n    }\r\n\r\n    public void setQuickSkipShortsAltEnabled(boolean enable) {\r\n        resetSkipShortsSettings();\r\n        mIsQuickSkipShortsAltEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    private void resetSkipShortsSettings() {\r\n        mIsQuickSkipShortsEnabled = false;\r\n        mIsQuickSkipShortsAltEnabled = false;\r\n    }\r\n\r\n    public boolean isQuickSkipVideosEnabled() {\r\n        return mIsQuickSkipVideosEnabled;\r\n    }\r\n\r\n    public void setQuickSkipVideosEnabled(boolean enable) {\r\n        resetQuickSkipVideosSettings();\r\n        mIsQuickSkipVideosEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isQuickSkipVideosAltEnabled() {\r\n        return mIsQuickSkipVideosAltEnabled;\r\n    }\r\n\r\n    public void setQuickSkipVideosAltEnabled(boolean enable) {\r\n        resetQuickSkipVideosSettings();\r\n        mIsQuickSkipVideosAltEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    private void resetQuickSkipVideosSettings() {\r\n        mIsQuickSkipVideosEnabled = false;\r\n        mIsQuickSkipVideosAltEnabled = false;\r\n    }\r\n\r\n    public void resetDpadLeftRightSettings() {\r\n        mIsQuickSkipShortsEnabled = false;\r\n        mIsQuickSkipVideosEnabled = false;\r\n        persistData();\r\n    }\r\n\r\n    public void resetDpadUpDownSettings() {\r\n        mIsQuickSkipShortsAltEnabled = false;\r\n        mIsQuickSkipVideosAltEnabled = false;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isHighBitrateFormatsEnabled() {\r\n        return MediaServiceData.instance().isFormatEnabled(MediaServiceData.FORMATS_EXTENDED_HLS);\r\n    }\r\n\r\n    public void setHighBitrateFormatsEnabled(boolean enable) {\r\n        MediaServiceData.instance().setFormatEnabled(MediaServiceData.FORMATS_EXTENDED_HLS, enable);\r\n    }\r\n\r\n    public int getPreferredDnsType() {\r\n        return GlobalPreferences.instance(mPrefs.getContext()).getPreferredDnsType();\r\n    }\r\n\r\n    public void setPreferredDnsType(int dnsType) {\r\n        GlobalPreferences.instance(mPrefs.getContext()).setPreferredDnsType(dnsType);\r\n    }\r\n\r\n    public boolean isNetworkErrorFixingDisabled() {\r\n        return mIsNetworkErrorFixingDisabled;\r\n    }\r\n\r\n    public void setNetworkErrorFixingDisabled(boolean disabled) {\r\n        mIsNetworkErrorFixingDisabled = disabled;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isDontResizeVideoToFitDialogEnabled() {\r\n        return mIsDontResizeVideoToFitDialogEnabled;\r\n    }\r\n\r\n    public void setDontResizeVideoToFitDialogEnabled(boolean enable) {\r\n        mIsDontResizeVideoToFitDialogEnabled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isSuggestionsHorizontallyScrolled() {\r\n        return mIsSuggestionsHorizontallyScrolled;\r\n    }\r\n\r\n    public void setSuggestionsHorizontallyScrolled(boolean enable) {\r\n        mIsSuggestionsHorizontallyScrolled = enable;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isQueueRespectsPlaybackMode() {\r\n        return mIsQueueRespectsPlaybackMode;\r\n    }\r\n\r\n    public void setQueueRespectsPlaybackMode(boolean enable) {\r\n        mIsQueueRespectsPlaybackMode = enable;\r\n        persistData();\r\n    }\r\n\r\n    private void restoreData() {\r\n        String data = mPrefs.getProfileData(VIDEO_PLAYER_TWEAKS_DATA);\r\n\r\n        String[] split = Helpers.splitData(data);\r\n\r\n        mIsAmlogicFixEnabled = Helpers.parseBoolean(split, 0, false);\r\n        mIsAmazonFrameDropFixEnabled = Helpers.parseBoolean(split, 1, false);\r\n        mIsSnapToVsyncDisabled = Helpers.parseBoolean(split, 2, false);\r\n        mIsProfileLevelCheckSkipped = Helpers.parseBoolean(split, 3, false);\r\n        mIsSWDecoderForced = Helpers.parseBoolean(split, 4, false);\r\n        mIsTextureViewEnabled = Helpers.parseBoolean(split, 5, false);\r\n        // Need to be enabled (?) on older version of ExoPlayer (e.g. 2.10.6).\r\n        // It's because there's no tweaks for modern devices.\r\n        mIsSetOutputSurfaceWorkaroundEnabled = Helpers.parseBoolean(split, 7, true);\r\n        mIsAudioSyncFixEnabled = Helpers.parseBoolean(split, 8, false);\r\n        mIsKeepFinishedActivityEnabled = Helpers.parseBoolean(split, 9, false);\r\n        mIsHlsStreamsForced = Helpers.parseBoolean(split, 10, false);\r\n        mIsPlaybackNotificationsDisabled = Helpers.parseBoolean(split, 11, false);\r\n        mIsTunneledPlaybackEnabled = Helpers.parseBoolean(split, 12, false);\r\n        mPlayerButtons = Helpers.parseInt(split, 13, PLAYER_BUTTON_DEFAULT);\r\n        // Buffering fix was there.\r\n        mIsNoFpsPresetsEnabled = Helpers.parseBoolean(split, 15, false);\r\n        mIsRememberPositionOfShortVideosEnabled = Helpers.parseBoolean(split, 16, false);\r\n        mIsSuggestionsDisabled = Helpers.parseBoolean(split, 17, false);\r\n        mIsAvcOverVp9Preferred = Helpers.parseBoolean(split, 18, false);\r\n        mIsChatPlacedLeft = Helpers.parseBoolean(split, 19, false);\r\n        mIsRealChannelIconEnabled = Helpers.parseBoolean(split, 20, true);\r\n        mPixelRatio = Helpers.parseFloat(split, 21, 1.0f);\r\n        mIsQualityInfoBitrateEnabled = Helpers.parseBoolean(split, 22, false);\r\n        mIsSpeedButtonOldBehaviorEnabled = Helpers.parseBoolean(split, 23, false);\r\n        mIsButtonLongClickEnabled = Helpers.parseBoolean(split, 24, true);\r\n        mIsLongSpeedListEnabled = Helpers.parseBoolean(split, 25, true);\r\n        //mPlayerDataSource = Helpers.parseInt(split, 26, Utils.skipCronet() ? PLAYER_DATA_SOURCE_DEFAULT : PLAYER_DATA_SOURCE_CRONET);\r\n        mPlayerDataSource = Helpers.parseInt(split, 26, PLAYER_DATA_SOURCE_DEFAULT);\r\n        mUnlockAllFormats = Helpers.parseBoolean(split, 27, false);\r\n        mIsDashUrlStreamsForced = Helpers.parseBoolean(split, 28, false);\r\n        mIsSonyFrameDropFixEnabled = Helpers.parseBoolean(split, 29, false);\r\n        mIsBufferOnStreamsDisabled = Helpers.parseBoolean(split, 30, false);\r\n        // Cause severe garbage collector stuttering\r\n        mIsSectionPlaylistEnabled = Helpers.parseBoolean(split, 31, Utils.isEnoughRam());\r\n        mIsScreenOffTimeoutEnabled = Helpers.parseBoolean(split, 32, false);\r\n        mScreenOffTimeoutSec = Helpers.parseInt(split, 33, 0);\r\n        mIsUIAnimationsEnabled = Helpers.parseBoolean(split, 34, false);\r\n        mIsLikesCounterEnabled = Helpers.parseBoolean(split, 35, true);\r\n        mIsChapterNotificationEnabled = Helpers.parseBoolean(split, 36, false);\r\n        mScreenOffDimmingPercents = Helpers.parseInt(split, 37, 100);\r\n        mIsBootScreenOffEnabled = Helpers.parseBoolean(split, 38, false);\r\n        mIsPlayerUiOnNextEnabled = Helpers.parseBoolean(split, 39, false);\r\n        mIsPlayerAutoVolumeEnabled = Helpers.parseBoolean(split, 40, true);\r\n        mIsSyncRowButtonIndexEnabled = Helpers.parseBoolean(split, 41, true);\r\n        mIsUnsafeAudioFormatsEnabled = Helpers.parseBoolean(split, 42, true);\r\n        //mIsHighBitrateFormatsEnabled = Helpers.parseBoolean(split, 43, false);\r\n        mIsLoopShortsEnabled = Helpers.parseBoolean(split, 44, true);\r\n        mIsQuickSkipShortsEnabled = Helpers.parseBoolean(split, 45, true);\r\n        mIsRememberPositionOfLiveVideosEnabled = Helpers.parseBoolean(split, 46, true);\r\n        mIsOculusQuestFixEnabled = Helpers.parseBoolean(split, 47, Utils.isOculusQuest());\r\n        // mPlayerDataSource was here\r\n        // Cronet is buffering too, unfortunately, so leave the default as a safest method (e.g. for \"strtarmenia\")\r\n        // mPlayerDataSource = Helpers.parseInt(split, 48, PLAYER_DATA_SOURCE_DEFAULT);\r\n        mIsExtraLongSpeedListEnabled = Helpers.parseBoolean(split, 49, false);\r\n        mIsQuickSkipVideosEnabled = Helpers.parseBoolean(split, 50, false);\r\n        mIsNetworkErrorFixingDisabled = Helpers.parseBoolean(split, 51, false);\r\n        mIsCommentsPlacedLeft = Helpers.parseBoolean(split, 52, false);\r\n        //mIsPersistentAntiBotFixEnabled = Helpers.parseBoolean(split, 53, false);\r\n        mIsAudioFocusEnabled = Helpers.parseBoolean(split, 54, true);\r\n        mIsDontResizeVideoToFitDialogEnabled = Helpers.parseBoolean(split, 55, false);\r\n        mIsSuggestionsHorizontallyScrolled = Helpers.parseBoolean(split, 56, false);\r\n        mIsQuickSkipShortsAltEnabled = Helpers.parseBoolean(split, 57, false);\r\n        mIsQuickSkipVideosAltEnabled = Helpers.parseBoolean(split, 58, false);\r\n        mIsAudioTimeStretchingEnabled = Helpers.parseBoolean(split, 59, true);\r\n        mIsQueueRespectsPlaybackMode = Helpers.parseBoolean(split, 60, false);\r\n\r\n        updateDefaultValues();\r\n    }\r\n\r\n    public void persistNow() {\r\n        Utils.post(mPersistDataInt);\r\n    }\r\n\r\n    private void persistData() {\r\n        Utils.postDelayed(mPersistDataInt, 10_000);\r\n    }\r\n\r\n    private void persistDataInt() {\r\n        mPrefs.setProfileData(VIDEO_PLAYER_TWEAKS_DATA, Helpers.mergeData(\r\n                mIsAmlogicFixEnabled, mIsAmazonFrameDropFixEnabled, mIsSnapToVsyncDisabled,\r\n                mIsProfileLevelCheckSkipped, mIsSWDecoderForced, mIsTextureViewEnabled,\r\n                null, mIsSetOutputSurfaceWorkaroundEnabled, mIsAudioSyncFixEnabled, mIsKeepFinishedActivityEnabled, mIsHlsStreamsForced,\r\n                mIsPlaybackNotificationsDisabled, mIsTunneledPlaybackEnabled, mPlayerButtons,\r\n                null, mIsNoFpsPresetsEnabled, mIsRememberPositionOfShortVideosEnabled, mIsSuggestionsDisabled,\r\n                mIsAvcOverVp9Preferred, mIsChatPlacedLeft, mIsRealChannelIconEnabled, mPixelRatio, mIsQualityInfoBitrateEnabled,\r\n                mIsSpeedButtonOldBehaviorEnabled, mIsButtonLongClickEnabled, mIsLongSpeedListEnabled, mPlayerDataSource, mUnlockAllFormats,\r\n                mIsDashUrlStreamsForced, mIsSonyFrameDropFixEnabled, mIsBufferOnStreamsDisabled, mIsSectionPlaylistEnabled,\r\n                mIsScreenOffTimeoutEnabled, mScreenOffTimeoutSec, mIsUIAnimationsEnabled, mIsLikesCounterEnabled, mIsChapterNotificationEnabled,\r\n                mScreenOffDimmingPercents, mIsBootScreenOffEnabled, mIsPlayerUiOnNextEnabled, mIsPlayerAutoVolumeEnabled, mIsSyncRowButtonIndexEnabled,\r\n                mIsUnsafeAudioFormatsEnabled, null, mIsLoopShortsEnabled, mIsQuickSkipShortsEnabled, mIsRememberPositionOfLiveVideosEnabled,\r\n                mIsOculusQuestFixEnabled, null, mIsExtraLongSpeedListEnabled, mIsQuickSkipVideosEnabled, mIsNetworkErrorFixingDisabled, mIsCommentsPlacedLeft,\r\n                null, mIsAudioFocusEnabled, mIsDontResizeVideoToFitDialogEnabled, mIsSuggestionsHorizontallyScrolled,\r\n                mIsQuickSkipShortsAltEnabled, mIsQuickSkipVideosAltEnabled, mIsAudioTimeStretchingEnabled, mIsQueueRespectsPlaybackMode\r\n                ));\r\n    }\r\n\r\n    private void updateDefaultValues() {\r\n        // Enable only certain buttons (not all, like it was)\r\n        if (mPlayerButtons >>> 30 == 0b1) { // check leftmost bit (old format)\r\n            int bits = 32 - 24;\r\n            mPlayerButtons = mPlayerButtons << bits >>> bits; // remove auto enabled bits\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void onProfileChanged() {\r\n        Utils.removeCallbacks(mPersistDataInt);\r\n        restoreData();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/RemoteControlData.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.common.DataChangeBase;\r\n\r\npublic class RemoteControlData extends DataChangeBase {\r\n    private static final String DEVICE_LINK_DATA = \"device_link_data\";\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static RemoteControlData sInstance;\r\n    private final Context mContext;\r\n    private final AppPrefs mAppPrefs;\r\n    private boolean mIsDeviceLinkEnabled;\r\n    private boolean mIsRunInBackgroundEnabled;\r\n    private boolean mIsFinishOnDisconnectEnabled;\r\n    private boolean mIsConnectMessagesEnabled;\r\n    private boolean mIsRemoteHistoryDisabled;\r\n    private Video mLastVideo;\r\n    private boolean mIsConnectedBefore;\r\n\r\n    private RemoteControlData(Context context) {\r\n        mContext = context;\r\n        mAppPrefs = AppPrefs.instance(mContext);\r\n        restoreState();\r\n    }\r\n\r\n    public static RemoteControlData instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new RemoteControlData(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public void enableDeviceLink(boolean select) {\r\n        mIsDeviceLinkEnabled = select;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isDeviceLinkEnabled() {\r\n        // Merge device link and background service (saves memory)\r\n        return mIsDeviceLinkEnabled;\r\n    }\r\n\r\n    public void enableFinishOnDisconnect(boolean enable) {\r\n        mIsFinishOnDisconnectEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isFinishOnDisconnectEnabled() {\r\n        return mIsFinishOnDisconnectEnabled;\r\n    }\r\n\r\n    public void enableConnectMessages(boolean enable) {\r\n        mIsConnectMessagesEnabled = enable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isConnectMessagesEnabled() {\r\n        return mIsConnectMessagesEnabled;\r\n    }\r\n\r\n    public void disableRemoteHistory(boolean disable) {\r\n        mIsRemoteHistoryDisabled = disable;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isRemoteHistoryDisabled() {\r\n        return mIsRemoteHistoryDisabled;\r\n    }\r\n\r\n    public Video getLastVideo() {\r\n        return mLastVideo;\r\n    }\r\n\r\n    public void setLastVideo(Video video) {\r\n        mLastVideo = video;\r\n        persistState();\r\n    }\r\n\r\n    public void setConnectedBefore(boolean connected) {\r\n        mIsConnectedBefore = connected;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isConnectedBefore() {\r\n        return mIsConnectedBefore;\r\n    }\r\n\r\n    private void restoreState() {\r\n        String data = mAppPrefs.getData(DEVICE_LINK_DATA);\r\n\r\n        String[] split = Helpers.splitData(data);\r\n\r\n        // null\r\n        // null\r\n        mIsDeviceLinkEnabled = Helpers.parseBoolean(split, 2, false);\r\n        mIsFinishOnDisconnectEnabled = Helpers.parseBoolean(split, 3, false);\r\n        mIsConnectMessagesEnabled = Helpers.parseBoolean(split, 4, false);\r\n        mIsRemoteHistoryDisabled = Helpers.parseBoolean(split, 5, false);\r\n        mLastVideo = Helpers.parseItem(split, 6, Video::fromString);\r\n        mIsConnectedBefore = Helpers.parseBoolean(split, 7, false);\r\n    }\r\n\r\n    private void persistState() {\r\n        mAppPrefs.setData(DEVICE_LINK_DATA, Helpers.mergeData(\r\n                null, null, mIsDeviceLinkEnabled, mIsFinishOnDisconnectEnabled, mIsConnectMessagesEnabled,\r\n                mIsRemoteHistoryDisabled, mLastVideo, mIsConnectedBefore\r\n        ));\r\n\r\n        onDataChange();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/SearchData.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\n\r\npublic class SearchData {\r\n    public static final int SPEECH_RECOGNIZER_SYSTEM = 0;\r\n    public static final int SPEECH_RECOGNIZER_INTENT = 1;\r\n    public static final int SPEECH_RECOGNIZER_GOTEV = 2;\r\n    private static final String SEARCH_DATA = \"search_data\";\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static SearchData sInstance;\r\n    private final AppPrefs mAppPrefs;\r\n    private boolean mIsInstantVoiceSearchEnabled;\r\n    private int mSearchOptions;\r\n    private boolean mIsFocusOnResultsEnabled;\r\n    private boolean mIsKeyboardAutoShowEnabled;\r\n    private boolean mIsTempBackgroundModeEnabled;\r\n    private int mSpeechRecognizerType;\r\n    private Class<?> mTempBackgroundModeClass;\r\n    private boolean mIsTrendingSearchesEnabled;\r\n    private boolean mIsSearchHistoryDisabled;\r\n    private boolean mIsPopularSearchesDisabled;\r\n    private boolean mIsKeyboardFixEnabled;\r\n    private boolean mIsTypingCorrectionDisabled;\r\n\r\n    private SearchData(Context context) {\r\n        mAppPrefs = AppPrefs.instance(context);\r\n        restoreData();\r\n    }\r\n\r\n    public static SearchData instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new SearchData(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    public boolean isInstantVoiceSearchEnabled() {\r\n        return mIsInstantVoiceSearchEnabled;\r\n    }\r\n\r\n    public void setInstantVoiceSearchEnabled(boolean enabled) {\r\n        mIsInstantVoiceSearchEnabled = enabled;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isFocusOnResultsEnabled() {\r\n        return mIsFocusOnResultsEnabled;\r\n    }\r\n\r\n    public void setFocusOnResultsEnabled(boolean enabled) {\r\n        mIsFocusOnResultsEnabled = enabled;\r\n        persistData();\r\n    }\r\n\r\n    public int getSearchOptions() {\r\n        return mSearchOptions;\r\n    }\r\n\r\n    public void setSearchOptions(int searchOptions) {\r\n        mSearchOptions = searchOptions;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isKeyboardAutoShowEnabled() {\r\n        return mIsKeyboardAutoShowEnabled;\r\n    }\r\n\r\n    public void setKeyboardAutoShowEnabled(boolean enabled) {\r\n        mIsKeyboardAutoShowEnabled = enabled;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isKeyboardFixEnabled() {\r\n        return mIsKeyboardFixEnabled;\r\n    }\r\n\r\n    public void setKeyboardFixEnabled(boolean enabled) {\r\n        mIsKeyboardFixEnabled = enabled;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isTypingCorrectionDisabled() {\r\n        return mIsTypingCorrectionDisabled;\r\n    }\r\n\r\n    public void setTypingCorrectionDisabled(boolean disabled) {\r\n        mIsTypingCorrectionDisabled = disabled;\r\n        persistData();\r\n    }\r\n\r\n    public void setTrendingSearchesEnabled(boolean enabled) {\r\n        mIsTrendingSearchesEnabled = enabled;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isTrendingSearchesEnabled() {\r\n        return mIsTrendingSearchesEnabled;\r\n    }\r\n\r\n    public boolean isTempBackgroundModeEnabled() {\r\n        return mIsTempBackgroundModeEnabled;\r\n    }\r\n\r\n    public void setTempBackgroundModeEnabled(boolean enabled) {\r\n        mIsTempBackgroundModeEnabled = enabled;\r\n        persistData();\r\n    }\r\n\r\n    public Class<?> getTempBackgroundModeClass() {\r\n        return mTempBackgroundModeClass;\r\n    }\r\n\r\n    public void setTempBackgroundModeClass(Class<?> clazz) {\r\n        mTempBackgroundModeClass = clazz;\r\n    }\r\n\r\n    public int getSpeechRecognizerType() {\r\n        return mSpeechRecognizerType;\r\n    }\r\n\r\n    public void setSpeechRecognizerType(int type) {\r\n        mSpeechRecognizerType = type;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isSearchHistoryDisabled() {\r\n        return mIsSearchHistoryDisabled;\r\n    }\r\n\r\n    public void setSearchHistoryDisabled(boolean disabled) {\r\n        mIsSearchHistoryDisabled = disabled;\r\n        persistData();\r\n    }\r\n\r\n    public boolean isPopularSearchesDisabled() {\r\n        return mIsPopularSearchesDisabled;\r\n    }\r\n\r\n    public void setPopularSearchesDisabled(boolean disabled) {\r\n        mIsPopularSearchesDisabled = disabled;\r\n        persistData();\r\n    }\r\n\r\n    private void restoreData() {\r\n        String data = mAppPrefs.getData(SEARCH_DATA);\r\n\r\n        String[] split = Helpers.splitData(data);\r\n\r\n        // WARN: Don't enable Instant Voice Search\r\n        // Serious bug on Nvidia Shield. Can't type anything with soft keyboard.\r\n        // Other devices probably affected too.\r\n        mIsInstantVoiceSearchEnabled = Helpers.parseBoolean(split, 0, false);\r\n        mSearchOptions = Helpers.parseInt(split, 1, 0);\r\n        mIsFocusOnResultsEnabled = Helpers.parseBoolean(split, 2, true);\r\n        mIsKeyboardAutoShowEnabled = Helpers.parseBoolean(split, 3, false);\r\n        mIsTempBackgroundModeEnabled = Helpers.parseBoolean(split, 4, false);\r\n        //mIsAltSpeechRecognizerEnabled\r\n        mSpeechRecognizerType = Helpers.parseInt(split, 6, SPEECH_RECOGNIZER_SYSTEM);\r\n        mIsTrendingSearchesEnabled = Helpers.parseBoolean(split, 7, true);\r\n        mIsSearchHistoryDisabled = Helpers.parseBoolean(split, 8, false);\r\n        mIsPopularSearchesDisabled = Helpers.parseBoolean(split, 9, false);\r\n        mIsKeyboardFixEnabled = Helpers.parseBoolean(split, 10, false);\r\n        mIsTypingCorrectionDisabled = Helpers.parseBoolean(split, 11, false);\r\n    }\r\n\r\n    private void persistData() {\r\n        mAppPrefs.setData(SEARCH_DATA,\r\n                Helpers.mergeData(mIsInstantVoiceSearchEnabled, mSearchOptions, mIsFocusOnResultsEnabled,\r\n                        mIsKeyboardAutoShowEnabled, mIsTempBackgroundModeEnabled, null, mSpeechRecognizerType,\r\n                        mIsTrendingSearchesEnabled, mIsSearchHistoryDisabled, mIsPopularSearchesDisabled,\r\n                        mIsKeyboardFixEnabled, mIsTypingCorrectionDisabled));\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/SponsorBlockData.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.content.Context;\r\nimport android.os.Build.VERSION;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.SponsorSegment;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.prefs.GlobalPreferences;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.controllers.SponsorBlockController.SegmentAction;\r\n\r\nimport java.util.Arrays;\r\nimport java.util.Collections;\r\nimport java.util.HashMap;\r\nimport java.util.HashSet;\r\nimport java.util.LinkedHashSet;\r\nimport java.util.Map;\r\nimport java.util.Set;\r\n\r\npublic class SponsorBlockData {\r\n    public static final int ACTION_UNDEFINED = -1;\r\n    public static final int ACTION_SKIP_ONLY = 0;\r\n    public static final int ACTION_SKIP_WITH_TOAST = 1;\r\n    public static final int ACTION_SHOW_DIALOG = 2;\r\n    public static final int ACTION_DO_NOTHING = 3;\r\n    private static final String SPONSOR_BLOCK_DATA = \"content_block_data\";\r\n    @SuppressLint(\"StaticFieldLeak\")\r\n    private static SponsorBlockData sInstance;\r\n    private final AppPrefs mAppPrefs;\r\n    private boolean mIsSponsorBlockEnabled;\r\n    private final Set<String> mColorCategories = new LinkedHashSet<>();\r\n    private final Set<SegmentAction> mActions = new LinkedHashSet<>();\r\n    private final Set<String> mExcludedChannels = new LinkedHashSet<>();\r\n    private boolean mIsDontSkipSegmentAgainEnabled;\r\n    private boolean mIsPaidContentNotificationEnabled;\r\n    private long mIgnoredDurationMs;\r\n    private Map<String, Integer> mSegmentLocalizedMapping;\r\n    private Map<String, Integer> mSegmentColorMapping;\r\n    private Set<String> mAllCategories;\r\n\r\n    private SponsorBlockData(Context context) {\r\n        mAppPrefs = AppPrefs.instance(context);\r\n        initLocalizedMapping();\r\n        initColorMapping();\r\n        initAllCategories();\r\n        restoreState();\r\n    }\r\n\r\n    public static SponsorBlockData instance(Context context) {\r\n        if (sInstance == null) {\r\n            sInstance = new SponsorBlockData(context.getApplicationContext());\r\n        }\r\n\r\n        return sInstance;\r\n    }\r\n\r\n    private void initLocalizedMapping() {\r\n        mSegmentLocalizedMapping = new HashMap<>();\r\n        mSegmentLocalizedMapping.put(SponsorSegment.CATEGORY_SPONSOR, R.string.content_block_sponsor);\r\n        mSegmentLocalizedMapping.put(SponsorSegment.CATEGORY_INTRO, R.string.content_block_intro);\r\n        mSegmentLocalizedMapping.put(SponsorSegment.CATEGORY_OUTRO, R.string.content_block_outro);\r\n        mSegmentLocalizedMapping.put(SponsorSegment.CATEGORY_SELF_PROMO, R.string.content_block_self_promo);\r\n        mSegmentLocalizedMapping.put(SponsorSegment.CATEGORY_INTERACTION, R.string.content_block_interaction);\r\n        mSegmentLocalizedMapping.put(SponsorSegment.CATEGORY_MUSIC_OFF_TOPIC, R.string.content_block_music_off_topic);\r\n        mSegmentLocalizedMapping.put(SponsorSegment.CATEGORY_PREVIEW_RECAP, R.string.content_block_preview_recap);\r\n        mSegmentLocalizedMapping.put(SponsorSegment.CATEGORY_POI_HIGHLIGHT, R.string.content_block_highlight);\r\n        mSegmentLocalizedMapping.put(SponsorSegment.CATEGORY_FILLER, R.string.content_block_filler);\r\n    }\r\n\r\n    private void initColorMapping() {\r\n        mSegmentColorMapping = new HashMap<>();\r\n        mSegmentColorMapping.put(SponsorSegment.CATEGORY_SPONSOR, R.color.green);\r\n        mSegmentColorMapping.put(SponsorSegment.CATEGORY_INTRO, R.color.cyan);\r\n        mSegmentColorMapping.put(SponsorSegment.CATEGORY_OUTRO, R.color.blue);\r\n        mSegmentColorMapping.put(SponsorSegment.CATEGORY_SELF_PROMO, R.color.yellow);\r\n        mSegmentColorMapping.put(SponsorSegment.CATEGORY_INTERACTION, R.color.magenta);\r\n        mSegmentColorMapping.put(SponsorSegment.CATEGORY_MUSIC_OFF_TOPIC, R.color.orange_peel);\r\n        mSegmentColorMapping.put(SponsorSegment.CATEGORY_PREVIEW_RECAP, R.color.light_blue);\r\n        mSegmentColorMapping.put(SponsorSegment.CATEGORY_POI_HIGHLIGHT, R.color.light_pink);\r\n        mSegmentColorMapping.put(SponsorSegment.CATEGORY_FILLER, R.color.electric_violet);\r\n    }\r\n\r\n    private void initAllCategories() {\r\n        mAllCategories = new LinkedHashSet<>();\r\n        mAllCategories.add(SponsorSegment.CATEGORY_SPONSOR);\r\n        mAllCategories.add(SponsorSegment.CATEGORY_INTRO);\r\n        mAllCategories.add(SponsorSegment.CATEGORY_OUTRO);\r\n        mAllCategories.add(SponsorSegment.CATEGORY_INTERACTION);\r\n        mAllCategories.add(SponsorSegment.CATEGORY_SELF_PROMO);\r\n        mAllCategories.add(SponsorSegment.CATEGORY_MUSIC_OFF_TOPIC);\r\n        mAllCategories.add(SponsorSegment.CATEGORY_PREVIEW_RECAP);\r\n        mAllCategories.add(SponsorSegment.CATEGORY_POI_HIGHLIGHT);\r\n        mAllCategories.add(SponsorSegment.CATEGORY_FILLER);\r\n    }\r\n\r\n    public Integer getLocalizedRes(String segmentCategory) {\r\n        return mSegmentLocalizedMapping.get(segmentCategory);\r\n    }\r\n\r\n    public Integer getColorRes(String segmentCategory) {\r\n        return mSegmentColorMapping.get(segmentCategory);\r\n    }\r\n\r\n    public Set<String> getAllCategories() {\r\n        return Collections.unmodifiableSet(mAllCategories);\r\n    }\r\n\r\n    public Set<String> getEnabledCategories() {\r\n        Set<String> enabledCategories = new HashSet<>();\r\n\r\n        for (SegmentAction action : mActions) {\r\n            if (action.actionType != ACTION_DO_NOTHING) {\r\n                enabledCategories.add(action.segmentCategory);\r\n            }\r\n        }\r\n\r\n        enabledCategories.addAll(mColorCategories);\r\n\r\n        return Collections.unmodifiableSet(enabledCategories);\r\n    }\r\n\r\n    public void enableColorMarker(String segmentCategory) {\r\n        mColorCategories.add(segmentCategory);\r\n        persistState();\r\n    }\r\n\r\n    public void disableColorMarker(String segmentCategory) {\r\n        mColorCategories.remove(segmentCategory);\r\n        persistState();\r\n    }\r\n\r\n    public boolean isColorMarkerEnabled(String segmentCategory) {\r\n        return mColorCategories.contains(segmentCategory);\r\n    }\r\n\r\n    public boolean isColorMarkersEnabled() {\r\n        return !mColorCategories.isEmpty();\r\n    }\r\n\r\n    public void excludeChannel(String channelId) {\r\n        mExcludedChannels.add(channelId);\r\n        persistState();\r\n    }\r\n\r\n    public void stopExcludingChannel(String channelId) {\r\n        mExcludedChannels.remove(channelId);\r\n        persistState();\r\n    }\r\n\r\n    public boolean isChannelExcluded(String channelId) {\r\n        return mExcludedChannels.contains(channelId);\r\n    }\r\n\r\n    public void toggleExcludeChannel(String channelId) {\r\n        if (channelId == null) {\r\n            return;\r\n        }\r\n\r\n        if (isChannelExcluded(channelId)) {\r\n            stopExcludingChannel(channelId);\r\n        } else {\r\n            excludeChannel(channelId);\r\n        }\r\n    }\r\n\r\n    public Set<SegmentAction> getActions() {\r\n        return Collections.unmodifiableSet(mActions);\r\n    }\r\n\r\n    public int getAction(String segmentCategory) {\r\n        for (SegmentAction action : mActions) {\r\n            if (Helpers.equals(action.segmentCategory, segmentCategory)) {\r\n                return action.actionType;\r\n            }\r\n        }\r\n\r\n        return ACTION_UNDEFINED;\r\n    }\r\n\r\n    public void setAction(String segmentCategory, int actionType) {\r\n        for (SegmentAction action : mActions) {\r\n            if (Helpers.equals(action.segmentCategory, segmentCategory)) {\r\n                action.actionType = actionType;\r\n                break;\r\n            }\r\n        }\r\n\r\n        persistState();\r\n    }\r\n\r\n    public boolean isSponsorBlockEnabled() {\r\n        return mIsSponsorBlockEnabled;\r\n    }\r\n\r\n    public void setSponsorBlockEnabled(boolean enabled) {\r\n        mIsSponsorBlockEnabled = enabled;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isActionsEnabled() {\r\n        for (SegmentAction action : mActions) {\r\n            if (action.actionType != ACTION_DO_NOTHING) {\r\n                return true;\r\n            }\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    public boolean isDontSkipSegmentAgainEnabled() {\r\n        return mIsDontSkipSegmentAgainEnabled;\r\n    }\r\n\r\n    public void setDontSkipSegmentAgainEnabled(boolean enabled) {\r\n        mIsDontSkipSegmentAgainEnabled = enabled;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isPaidContentNotificationEnabled() {\r\n        return mIsPaidContentNotificationEnabled;\r\n    }\r\n\r\n    public void setPaidContentNotificationEnabled(boolean enabled) {\r\n        mIsPaidContentNotificationEnabled = enabled;\r\n        persistState();\r\n    }\r\n\r\n    public long getIgnoredDurationMs() {\r\n        return mIgnoredDurationMs;\r\n    }\r\n\r\n    public void setIgnoredDurationMs(long durationMs) {\r\n        mIgnoredDurationMs = durationMs;\r\n        persistState();\r\n    }\r\n\r\n    public boolean isAltServerEnabled() {\r\n        return GlobalPreferences.instance(mAppPrefs.getContext()).isContentBlockAltServerEnabled();\r\n    }\r\n\r\n    public void enableAltServer(boolean enabled) {\r\n        GlobalPreferences.instance(mAppPrefs.getContext()).setContentBlockAltServerEnabled(enabled);\r\n    }\r\n\r\n    private void restoreState() {\r\n        String data = mAppPrefs.getData(SPONSOR_BLOCK_DATA);\r\n\r\n        String[] split = Helpers.splitData(data);\r\n\r\n        mIsSponsorBlockEnabled = Helpers.parseBoolean(split, 0, VERSION.SDK_INT > 19); // Android 4 may have memory problems\r\n        // categories: index 2\r\n        // don't skip segment\r\n        // colorMarkers: index 4\r\n        String actions = Helpers.parseStr(split, 6);\r\n        String colorCategories = Helpers.parseStr(split, 7);\r\n        mIsDontSkipSegmentAgainEnabled = Helpers.parseBoolean(split, 8, false);\r\n        String excludedChannels = Helpers.parseStr(split, 9);\r\n        mIsPaidContentNotificationEnabled = Helpers.parseBoolean(split, 10, false);\r\n        mIgnoredDurationMs = Helpers.parseLong(split, 11, 5_000);\r\n\r\n        if (colorCategories != null) {\r\n            String[] categoriesArr = Helpers.splitArray(colorCategories);\r\n\r\n            mColorCategories.clear();\r\n\r\n            mColorCategories.addAll(Arrays.asList(categoriesArr));\r\n        } else {\r\n            mColorCategories.clear();\r\n\r\n            mColorCategories.addAll(mAllCategories);\r\n        }\r\n\r\n        if (excludedChannels != null) {\r\n            String[] channelsArr = Helpers.splitArray(excludedChannels);\r\n\r\n            mExcludedChannels.clear();\r\n\r\n            mExcludedChannels.addAll(Arrays.asList(channelsArr));\r\n        } else {\r\n            mExcludedChannels.clear();\r\n        }\r\n\r\n        if (actions != null) {\r\n            String[] actionsArr = Helpers.splitArray(actions);\r\n\r\n            mActions.clear();\r\n\r\n            for (String action : actionsArr) {\r\n                mActions.add(SegmentAction.from(action));\r\n            }\r\n        }\r\n\r\n        // Easy add new segments\r\n        for (String segmentCategory : mAllCategories) {\r\n            if (getAction(segmentCategory) == ACTION_UNDEFINED) {\r\n                // Disable filler category by default\r\n                // This category is very extreme and is recommended to be disabled by default because of that.\r\n                if (SponsorSegment.CATEGORY_FILLER.equals(segmentCategory)) {\r\n                    mActions.add(SegmentAction.from(segmentCategory, ACTION_DO_NOTHING));\r\n                } else {\r\n                    mActions.add(SegmentAction.from(segmentCategory, ACTION_SKIP_WITH_TOAST));\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    private void persistState() {\r\n        String colorCategories = Helpers.mergeArray(mColorCategories.toArray());\r\n        String actions = Helpers.mergeArray(mActions.toArray());\r\n        String excludedChannels = Helpers.mergeArray(mExcludedChannels.toArray());\r\n\r\n        mAppPrefs.setData(SPONSOR_BLOCK_DATA, Helpers.mergeData(\r\n                mIsSponsorBlockEnabled, null, null, null,\r\n                null, null, actions, colorCategories, mIsDontSkipSegmentAgainEnabled,\r\n                excludedChannels, mIsPaidContentNotificationEnabled, mIgnoredDurationMs\r\n        ));\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/common/DataChangeBase.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs.common;\r\n\r\nimport com.liskovsoft.sharedutils.misc.WeakHashSet;\r\n\r\npublic abstract class DataChangeBase {\r\n    public interface OnDataChange {\r\n        void onDataChange();\r\n    }\r\n\r\n    private final WeakHashSet<OnDataChange> mOnChangeList = new WeakHashSet<>();\r\n\r\n    public final void setOnChange(OnDataChange callback) {\r\n        mOnChangeList.add(callback);\r\n    }\r\n\r\n    public final void removeOnChange(OnDataChange callback) {\r\n        mOnChangeList.remove(callback);\r\n    }\r\n\r\n    public final void onDataChange() {\r\n        mOnChangeList.forEach(OnDataChange::onDataChange);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/prefs/common/DataSaverBase.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.prefs.common;\r\n\r\nimport android.content.Context;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs;\r\nimport com.liskovsoft.smartyoutubetv2.common.utils.Utils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Arrays;\r\nimport java.util.List;\r\n\r\npublic abstract class DataSaverBase extends DataChangeBase {\r\n    private final AppPrefs mAppPrefs;\r\n    private final String mDataKey;\r\n    private final List<Object> mValues;\r\n    private final Runnable mPersistStateInt = this::persistStateInt;\r\n\r\n    private interface Converter {\r\n        Object convert(String input);\r\n    }\r\n\r\n    public DataSaverBase(Context context) {\r\n        mAppPrefs = AppPrefs.instance(context.getApplicationContext());\r\n        mDataKey = this.getClass().getSimpleName();\r\n        mValues = new ArrayList<>();\r\n        restoreState();\r\n    }\r\n\r\n    protected boolean getBoolean(int index) {\r\n        return getBoolean(index, false);\r\n    }\r\n\r\n    protected boolean getBoolean(int index, boolean defaultValue) {\r\n        return getValue(index, defaultValue, Helpers::parseBoolean);\r\n    }\r\n\r\n    protected void setBoolean(int index, boolean value) {\r\n        setValue(index, value);\r\n    }\r\n\r\n    protected int getInt(int index) {\r\n        return getInt(index, -1);\r\n    }\r\n\r\n    protected int getInt(int index, int defaultValue) {\r\n        return getValue(index, defaultValue, Helpers::parseInt);\r\n    }\r\n\r\n    protected void setInt(int index, int value) {\r\n        setValue(index, value);\r\n    }\r\n\r\n    @SuppressWarnings(\"unchecked\")\r\n    private <T> T getValue(int index, T defaultValue, Converter converter) {\r\n        if (index >= mValues.size() || mValues.get(index) == null) {\r\n            return defaultValue;\r\n        }\r\n\r\n        Object rawValue = mValues.get(index);\r\n        if (rawValue instanceof String) {\r\n            Object value = converter.convert((String) rawValue);\r\n            mValues.set(index, value);\r\n            return (T) value;\r\n        } else {\r\n            return (T) rawValue;\r\n        }\r\n    }\r\n\r\n    private <T> void setValue(int index, T value) {\r\n        checkCapacity(index);\r\n        mValues.set(index, value);\r\n        persistState();\r\n    }\r\n\r\n    private void checkCapacity(int index) {\r\n        int size = mValues.size();\r\n        if (size <= index) { // fill with nulls\r\n            for (int i = size; i <= index; i++) {\r\n                mValues.add(null);\r\n            }\r\n        }\r\n    }\r\n\r\n    private void restoreState() {\r\n        String data = mAppPrefs.getData(mDataKey);\r\n\r\n        String[] split = Helpers.splitData(data);\r\n\r\n        if (split != null) {\r\n            mValues.addAll(Arrays.asList(split));\r\n        }\r\n    }\r\n\r\n    private void persistState() {\r\n        onDataChange();\r\n        Utils.postDelayed(mPersistStateInt, 10_000);\r\n    }\r\n\r\n    private void persistStateInt() {\r\n        mAppPrefs.setData(mDataKey, Helpers.mergeData(\r\n                mValues.toArray()\r\n        ));\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/proxy/PasswdInetSocketAddress.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.proxy;\r\n\r\nimport androidx.annotation.RequiresApi;\r\n\r\nimport java.net.InetSocketAddress;\r\nimport java.net.SocketAddress;\r\n\r\npublic class PasswdInetSocketAddress extends SocketAddress {\r\n    private final String mUsername;\r\n    private final String mPassword;\r\n    private final InetSocketAddress mInetSocketAddress;\r\n\r\n    private PasswdInetSocketAddress(String hostname, int port, String username, String password) {\r\n        mInetSocketAddress = InetSocketAddress.createUnresolved(hostname, port);\r\n        mUsername = username != null && username.isEmpty() ? null : username;\r\n        mPassword = password != null && password.isEmpty() ? null : password;\r\n    }\r\n\r\n    public static PasswdInetSocketAddress createUnresolved(String host, int port, String username, String password) {\r\n        return new PasswdInetSocketAddress(checkHost(host), checkPort(port), username, password);\r\n    }\r\n\r\n    @RequiresApi(api = 19)\r\n    public String getHostString() {\r\n        return mInetSocketAddress.getHostString();\r\n    }\r\n\r\n    public int getPort() {\r\n        return mInetSocketAddress.getPort();\r\n    }\r\n\r\n    public String getUsername() {\r\n        return mUsername;\r\n    }\r\n\r\n    public String getPassword() {\r\n        return mPassword;\r\n    }\r\n\r\n    private static int checkPort(int port) {\r\n        if (port < 0 || port > 0xFFFF)\r\n            throw new IllegalArgumentException(\"port out of range:\" + port);\r\n        return port;\r\n    }\r\n\r\n    private static String checkHost(String hostname) {\r\n        if (hostname == null)\r\n            throw new IllegalArgumentException(\"hostname can't be null\");\r\n        return hostname;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/proxy/PasswdURI.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.proxy;\r\n\r\nimport java.net.URI;\r\nimport java.net.URISyntaxException;\r\n\r\n/**\r\n * http_proxy=http://USERNAME:PASSWORD@PROXYIP:PROXYPORT\r\n */\r\npublic class PasswdURI {\r\n    private final URI mURI;\r\n\r\n    public PasswdURI(String uriString) throws URISyntaxException {\r\n        mURI = new URI(uriString);\r\n    }\r\n\r\n    public String getScheme() {\r\n        return mURI.getScheme();\r\n    }\r\n\r\n    public String getHost() {\r\n        return mURI.getHost();\r\n    }\r\n\r\n    public int getPort() {\r\n        return mURI.getPort();\r\n    }\r\n\r\n    public String getUsername() {\r\n        String authority = mURI.getAuthority();\r\n        String[] split = authority.split(\"@\");\r\n\r\n        String result = null;\r\n\r\n        if (split.length == 2) {\r\n            String[] split2 = split[0].split(\":\");\r\n\r\n            if (split2.length == 2) {\r\n                result = split2[0];\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    public String getPassword() {\r\n        String authority = mURI.getAuthority();\r\n        String[] split = authority.split(\"@\");\r\n        \r\n        String result = null;\r\n\r\n        if (split.length == 2) {\r\n            String[] split2 = split[0].split(\":\");\r\n\r\n            if (split2.length == 2) {\r\n                result = split2[1];\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/proxy/Proxy.java",
    "content": "/*\n * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved.\n * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.\n *\n * This code is free software; you can redistribute it and/or modify it\n * under the terms of the GNU General Public License version 2 only, as\n * published by the Free Software Foundation.  Oracle designates this\n * particular file as subject to the \"Classpath\" exception as provided\n * by Oracle in the LICENSE file that accompanied this code.\n *\n * This code is distributed in the hope that it will be useful, but WITHOUT\n * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License\n * version 2 for more details (a copy is included in the LICENSE file that\n * accompanied this code).\n *\n * You should have received a copy of the GNU General Public License version\n * 2 along with this work; if not, write to the Free Software Foundation,\n * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.\n *\n * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA\n * or visit www.oracle.com if you need additional information or have any\n * questions.\n */\n\npackage com.liskovsoft.smartyoutubetv2.common.proxy;\n\nimport java.net.InetSocketAddress;\nimport java.net.ProxySelector;\nimport java.net.SocketAddress;\n\n/**\n * NOTE: Added support for PasswdInetSocketAddress<br/>\n * This class represents a proxy setting, typically a type (http, socks) and\n * a socket address.\n * A {@code Proxy} is an immutable object.\n *\n * @see     ProxySelector\n * @author Yingxian Wang\n * @author Jean-Christophe Collet\n * @since   1.5\n */\npublic class Proxy {\n\n    /**\n     * Represents the proxy type.\n     *\n     * @since 1.5\n     */\n    public enum Type {\n        /**\n         * Represents a direct connection, or the absence of a proxy.\n         */\n        DIRECT,\n        /**\n         * Represents proxy for high level protocols such as HTTP or FTP.\n         */\n        HTTP,\n        /**\n         * Represents a SOCKS (V4 or V5) proxy.\n         */\n        SOCKS\n    };\n\n    private Type type;\n    private SocketAddress sa;\n\n    /**\n     * A proxy setting that represents a {@code DIRECT} connection,\n     * basically telling the protocol handler not to use any proxying.\n     * Used, for instance, to create sockets bypassing any other global\n     * proxy settings (like SOCKS):\n     * <P>\n     * {@code Socket s = new Socket(Proxy.NO_PROXY);}\n     *\n     */\n    public final static Proxy NO_PROXY = new Proxy();\n\n    // Creates the proxy that represents a {@code DIRECT} connection.\n    private Proxy() {\n        type = Type.DIRECT;\n        sa = null;\n    }\n\n    /**\n     * Creates an entry representing a PROXY connection.\n     * Certain combinations are illegal. For instance, for types Http, and\n     * Socks, a SocketAddress <b>must</b> be provided.\n     * <P>\n     * Use the {@code Proxy.NO_PROXY} constant\n     * for representing a direct connection.\n     *\n     * @param type the {@code Type} of the proxy\n     * @param sa the {@code SocketAddress} for that proxy\n     * @throws IllegalArgumentException when the type and the address are\n     * incompatible\n     */\n    public Proxy(Type type, SocketAddress sa) {\n        if ((type == Type.DIRECT) || !(sa instanceof PasswdInetSocketAddress))\n            throw new IllegalArgumentException(\"type \" + type + \" is not compatible with address \" + sa);\n        this.type = type;\n        this.sa = sa;\n    }\n\n    /**\n     * Returns the proxy type.\n     *\n     * @return a Type representing the proxy type\n     */\n    public Type type() {\n        return type;\n    }\n\n    /**\n     * Returns the socket address of the proxy, or\n     * {@code null} if its a direct connection.\n     *\n     * @return a {@code SocketAddress} representing the socket end\n     *         point of the proxy\n     */\n    public SocketAddress address() {\n        return sa;\n    }\n\n    /**\n     * Constructs a string representation of this Proxy.\n     * This String is constructed by calling toString() on its type\n     * and concatenating \" @ \" and the toString() result from its address\n     * if its type is not {@code DIRECT}.\n     *\n     * @return  a string representation of this object.\n     */\n    public String toString() {\n        if (type() == Type.DIRECT)\n            return \"DIRECT\";\n        return type() + \" @ \" + address();\n    }\n\n        /**\n     * Compares this object against the specified object.\n     * The result is {@code true} if and only if the argument is\n     * not {@code null} and it represents the same proxy as\n     * this object.\n     * <p>\n     * Two instances of {@code Proxy} represent the same\n     * address if both the SocketAddresses and type are equal.\n     *\n     * @param   obj   the object to compare against.\n     * @return  {@code true} if the objects are the same;\n     *          {@code false} otherwise.\n     * @see InetSocketAddress#equals(Object)\n     */\n    public final boolean equals(Object obj) {\n        if (obj == null || !(obj instanceof Proxy))\n            return false;\n        Proxy p = (Proxy) obj;\n        if (p.type() == type()) {\n            if (address() == null) {\n                return (p.address() == null);\n            } else\n                return address().equals(p.address());\n        }\n        return false;\n    }\n\n    /**\n     * Returns a hashcode for this Proxy.\n     *\n     * @return  a hash code value for this Proxy.\n     */\n    public final int hashCode() {\n        if (address() == null)\n            return type().hashCode();\n        return type().hashCode() + address().hashCode();\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/proxy/ProxyManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.proxy;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.ProxyInfo;\nimport android.os.Build;\nimport android.os.Parcelable;\nimport android.util.ArrayMap;\nimport androidx.annotation.RequiresApi;\nimport com.liskovsoft.sharedutils.mylogger.Log;\nimport com.liskovsoft.smartyoutubetv2.common.prefs.AppPrefs;\n\nimport java.lang.reflect.Constructor;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.net.Authenticator;\nimport java.net.PasswordAuthentication;\nimport java.net.URISyntaxException;\n\nimport com.liskovsoft.smartyoutubetv2.common.BuildConfig;\n\n/**\n * Manages web proxy settings, to enable web view and http client using web proxy.\n * This implementation is based on the discussion here:\n * https://stackoverflow.com/questions/4488338/webview-android-proxy\n *\n * Note that the implementation uses non-public Android API that subject to\n * uninformed change in the future.\n *\n * DONE: Support SOCKS proxy\n * TODO: Support API level 14 ~ 18\n * TODO: Support exclusion list (?)\n * TODO: Support PAC (Proxy Auto-Configuration)\n */\npublic class ProxyManager {\n    public static final String TAG = ProxyManager.class.getSimpleName();\n    private final Context mContext;\n    private final AppPrefs mPrefs;\n    private Proxy mProxy;\n    private boolean mEnabled;\n\n    public ProxyManager(Context context) {\n        mContext = context.getApplicationContext();\n        mPrefs = AppPrefs.instance(mContext);\n        loadProxyInfoFromPrefs();\n    }\n\n\n    /**\n     * Get the string representation of current proxy settings.\n     * @return String representation of current proxy settings.\n     */\n    @RequiresApi(api = Build.VERSION_CODES.KITKAT)\n    public String getProxyUriString() {\n        if (mProxy == null || mProxy.type() == Proxy.Type.DIRECT) {\n            return \"\";\n        } else {\n            PasswdInetSocketAddress proxyAddr = (PasswdInetSocketAddress) mProxy.address();\n            String usernameAndPasswd = \"\";\n            if (proxyAddr.getUsername() != null && proxyAddr.getPassword() != null) {\n                usernameAndPasswd = proxyAddr.getUsername() + \":\" + proxyAddr.getPassword() + \"@\";\n            }\n            return mProxy.type().name().toLowerCase() + \"://\" + usernameAndPasswd + proxyAddr.getHostString()\n                    + \":\" + proxyAddr.getPort();\n        }\n    }\n\n    /**\n     * Check if proxy is enabled in preference settings.\n     * This doesn't reflect the status whether the proxy is in use, use\n     * {@link #isProxyConfigured()} to check if the proxy is effectively configured.\n     * @return True if proxy setting is enabled in preference. Otherwise false.\n     */\n    public boolean isProxyEnabled() {\n        return isProxySupported() && mEnabled;\n    }\n\n    /**\n     * Check if proxy setting is configured and is being used by app.\n     * @return Whether the proxy is being used, i.e. system properties are configured according to proxy settings.\n     */\n    @RequiresApi(api = Build.VERSION_CODES.KITKAT)\n    public boolean isProxyConfigured() {\n        if (mProxy == null || mProxy.type() == Proxy.Type.DIRECT) {\n            return System.getProperty(\"http.proxyHost\") == null\n                    && System.getProperty(\"https.proxyHost\") == null\n                    && System.getProperty(\"socksProxyHost\") == null;\n        }\n        PasswdInetSocketAddress proxyAddr = (PasswdInetSocketAddress) mProxy.address();\n        String proxyHost = proxyAddr.getHostString();\n        String proxyPort = Integer.toString(proxyAddr.getPort());\n        switch (mProxy.type()) {\n            case HTTP:\n                return proxyHost.equals(System.getProperty(\"http.proxyHost\"))\n                        && proxyPort.equals(System.getProperty(\"http.proxyPort\"))\n                        && proxyHost.equals(System.getProperty(\"https.proxyHost\"))\n                        && proxyPort.equals(System.getProperty(\"https.proxyPort\"));\n            case SOCKS:\n                return proxyHost.equals(System.getProperty(\"socksProxyHost\"))\n                        && proxyPort.equals(System.getProperty(\"socksProxyPort\"));\n        }\n        return false;\n    }\n\n    public Proxy getCurrentProxy() {\n        return mProxy;\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.KITKAT)\n    public String getProxyHost() {\n        return mProxy == null ? \"\" : ((PasswdInetSocketAddress) mProxy.address()).getHostString();\n    }\n\n    public int getProxyPort() {\n        return mProxy == null ? 0 : ((PasswdInetSocketAddress) mProxy.address()).getPort();\n    }\n\n    public Proxy.Type getProxyType() {\n        return mProxy == null ? Proxy.Type.DIRECT : mProxy.type();\n    }\n\n    public String getProxyUsername() {\n        return mProxy == null ? \"\" : ((PasswdInetSocketAddress) mProxy.address()).getUsername();\n    }\n\n    public String getProxyPassword() {\n        return mProxy == null ? \"\" : ((PasswdInetSocketAddress) mProxy.address()).getPassword();\n    }\n\n    protected void loadProxyInfoFromPrefs() {\n        try {\n            String proxyUriString = mPrefs.getWebProxyUri();\n\n            Log.d(TAG, \"Web Proxy URI from preferences: \\\"\"\n                    + proxyUriString + \"\\\"; \" + mEnabled);\n            if (proxyUriString.isEmpty() || proxyUriString.equalsIgnoreCase(Proxy.Type.DIRECT.name())) {\n                mProxy = Proxy.NO_PROXY;\n            }\n            else {\n                PasswdURI proxyURI = new PasswdURI(proxyUriString);\n                mProxy = new Proxy(Proxy.Type.valueOf(proxyURI.getScheme().toUpperCase()),\n                        PasswdInetSocketAddress.createUnresolved(proxyURI.getHost(), proxyURI.getPort(), proxyURI.getUsername(), proxyURI.getPassword()));\n            }\n\n            mEnabled = mPrefs.isWebProxyEnabled();\n        } catch (URISyntaxException | IllegalArgumentException e) {\n            Log.e(TAG, e);\n            mProxy = Proxy.NO_PROXY;\n            mEnabled = false; // invalid settings found. disable proxy.\n        }\n    }\n\n    /**\n     * Save proxy settings to preferences.\n     * This method only save the settings, it doesn't actually configure the system to use the proxy.\n     * Use {@link #configureSystemProxy()} to configure system proxy settings.\n     *\n     * @param proxy Specify new proxy settings, if null, current proxy setting will be saved.\n     * @param enable Set proxy enabled/disabled.\n     */\n    @RequiresApi(api = Build.VERSION_CODES.KITKAT)\n    public void saveProxyInfoToPrefs(Proxy proxy, boolean enable) {\n        if (proxy != null)\n            mProxy = new Proxy(proxy.type(), proxy.address());\n        mEnabled = enable;\n        String proxyUriString = getProxyUriString();\n        \n        mPrefs.setWebProxyUri(proxyUriString);\n        mPrefs.setWebProxyEnabled(mEnabled);\n        \n        Log.d(TAG, \"Saved Web Proxy URI to preferences: \"\n                + proxyUriString + \"; Enabled: \" + mEnabled);\n    }\n\n    /**\n     * Create proxy info object required by {@link android.net.Proxy#PROXY_CHANGE_ACTION} intent.\n     * Before API 21, it is an android.net.ProxyProperties object.\n     * Since API 21, it is an {@link ProxyInfo} object.\n     *\n     * Note: this may NOT work in future if Android's internal implementation changes.\n     */\n    @RequiresApi(api = Build.VERSION_CODES.KITKAT)\n    protected Object createProxyChangeInfo(PasswdInetSocketAddress proxyAddr) throws\n            ClassNotFoundException,\n            NoSuchMethodException,\n            IllegalAccessException,\n            InvocationTargetException,\n            InstantiationException\n    {\n        if (Build.VERSION.SDK_INT < 21) {\n            // https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-4.4.4_r2.0.1/core/java/android/net/ProxyProperties.java#65\n            @SuppressLint(\"PrivateApi\") Class<?> proxyPropClazz = Class.forName(\"android.net.ProxyProperties\");\n            Constructor proxyPropCtor = proxyPropClazz.getDeclaredConstructor(String.class, String.class, String[].class);\n            return proxyPropCtor.newInstance(proxyAddr.getHostString(), Integer.toString(proxyAddr.getPort()), null);\n        }\n        else {\n            return ProxyInfo.buildDirectProxy(proxyAddr.getHostString(), proxyAddr.getPort());\n        }\n    }\n\n    /**\n     * Configure web proxy for the app.\n     *\n     * The implementation is based on:\n     * <a href=https://stackoverflow.com/questions/4488338/webview-android-proxy>this StackOverflow discussion</a>.\n     *\n     * Also refer to Chromium source code of\n     * <a href=https://chromium.googlesource.com/chromium/src/net/+/master/android/java/src/org/chromium/net/ProxyChangeListener.java>ProxyChangeListener.java</a>\n     *\n     * @return true if proxy is successfully enabled/disabled, otherwise false.\n     */\n    @RequiresApi(api = Build.VERSION_CODES.KITKAT)\n    private boolean setWebProxyAPI19Plus() throws\n            NoSuchFieldException,\n            IllegalAccessException,\n            NoSuchMethodException,\n            InstantiationException,\n            InvocationTargetException,\n            ClassNotFoundException\n    {\n        if (BuildConfig.DEBUG && Build.VERSION.SDK_INT < 19) {\n            throw new AssertionError(\"API level must >= 19\");\n        }\n        Proxy proxy = isProxyEnabled() && mProxy != null ? mProxy : Proxy.NO_PROXY;\n        Context appContext = mContext.getApplicationContext();\n        PasswdInetSocketAddress proxyAddr = (PasswdInetSocketAddress) proxy.address();\n        String username = proxyAddr != null ? proxyAddr.getUsername() : null;\n        String password = proxyAddr != null ? proxyAddr.getPassword() : null;\n\n        if (username != null && password != null) {\n            Authenticator.setDefault(new Authenticator() {\n                @Override\n                protected PasswordAuthentication getPasswordAuthentication() {\n                    return new PasswordAuthentication(username, password.toCharArray());\n                }\n            });\n        }\n\n        switch (proxy.type()) {\n            case HTTP:\n                System.setProperty(\"http.proxyHost\", proxyAddr.getHostString());\n                System.setProperty(\"http.proxyPort\", proxyAddr.getPort() + \"\");\n                System.setProperty(\"https.proxyHost\", proxyAddr.getHostString());\n                System.setProperty(\"https.proxyPort\", proxyAddr.getPort() + \"\");\n                System.clearProperty(\"socksProxyHost\");\n                System.clearProperty(\"socksProxyPort\");\n\n                if (username != null && password != null) {\n                    System.setProperty(\"http.proxyUser\", username);\n                    System.setProperty(\"http.proxyPassword\", password);\n                    System.setProperty(\"https.proxyUser\", username);\n                    System.setProperty(\"https.proxyPassword\", password);\n                } else {\n                    System.clearProperty(\"http.proxyUser\");\n                    System.clearProperty(\"http.proxyPassword\");\n                    System.clearProperty(\"https.proxyUser\");\n                    System.clearProperty(\"https.proxyPassword\");\n                }\n                System.clearProperty(\"socksProxyUser\");\n                System.clearProperty(\"socksProxyPassword\");\n                break;\n            case SOCKS:\n                System.setProperty(\"socksProxyHost\", proxyAddr.getHostString());\n                System.setProperty(\"socksProxyPort\", proxyAddr.getPort() + \"\");\n                System.clearProperty(\"http.proxyHost\");\n                System.clearProperty(\"http.proxyPort\");\n                System.clearProperty(\"https.proxyHost\");\n                System.clearProperty(\"https.proxyPort\");\n\n                if (username != null && password != null) {\n                    System.setProperty(\"socksProxyUser\", username);\n                    System.setProperty(\"socksProxyPassword\", password);\n                } else {\n                    System.clearProperty(\"socksProxyUser\");\n                    System.clearProperty(\"socksProxyPassword\");\n                }\n                System.clearProperty(\"http.proxyUser\");\n                System.clearProperty(\"http.proxyPassword\");\n                System.clearProperty(\"https.proxyUser\");\n                System.clearProperty(\"https.proxyPassword\");\n                break;\n            case DIRECT:\n                System.clearProperty(\"socksProxyHost\");\n                System.clearProperty(\"socksProxyPort\");\n                System.clearProperty(\"http.proxyHost\");\n                System.clearProperty(\"http.proxyPort\");\n                System.clearProperty(\"https.proxyHost\");\n                System.clearProperty(\"https.proxyPort\");\n\n                System.clearProperty(\"socksProxyUser\");\n                System.clearProperty(\"socksProxyPassword\");\n                System.clearProperty(\"http.proxyUser\");\n                System.clearProperty(\"http.proxyPassword\");\n                System.clearProperty(\"https.proxyUser\");\n                System.clearProperty(\"https.proxyPassword\");\n                break;\n        }\n        Field loadedApkField = appContext.getClass().getField(\"mLoadedApk\");\n        loadedApkField.setAccessible(true);\n        Object loadedApk = loadedApkField.get(appContext);\n        @SuppressLint(\"PrivateApi\") Class<?> loadedApkCls = Class.forName(\"android.app.LoadedApk\");\n        Field receiversField = loadedApkCls.getDeclaredField(\"mReceivers\");\n        receiversField.setAccessible(true);\n        ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);\n        for (Object receiverMap : receivers.values()) {\n            for (Object rec : ((ArrayMap) receiverMap).keySet()) {\n                Class<? extends Object> clazz = rec.getClass();\n                if (clazz.getName().contains(\"ProxyChangeListener\")) {\n                    Method onReceiveMethod = clazz.getDeclaredMethod(\"onReceive\", Context.class, Intent.class);\n                    Intent intent = new Intent(android.net.Proxy.PROXY_CHANGE_ACTION);\n                    Object proxyInfo = createProxyChangeInfo(proxyAddr);\n                    intent.putExtra(\"android.intent.extra.PROXY_INFO\", (Parcelable) proxyInfo);\n                    onReceiveMethod.invoke(rec, appContext, intent);\n                }\n            }\n        }\n        Log.d(TAG, \"Web Proxy set to: \" + getProxyUriString());\n        return true;\n    }\n\n    public boolean configureSystemProxy() {\n        try {\n            if (isProxySupported()) {\n                return setWebProxyAPI19Plus();\n            }\n        } catch (Exception e) {\n            Log.e(TAG, e);\n        }\n\n        return false;\n    }\n\n    public boolean isProxySupported() {\n        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/proxy/WebProxyDialog.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.proxy;\n\nimport android.content.Context;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.EditText;\nimport android.widget.RadioGroup;\nimport android.widget.TextView;\nimport androidx.annotation.RequiresApi;\nimport androidx.appcompat.app.AlertDialog;\nimport com.liskovsoft.sharedutils.helpers.Helpers;\nimport com.liskovsoft.sharedutils.helpers.KeyHelpers;\nimport com.liskovsoft.sharedutils.mylogger.Log;\nimport com.liskovsoft.sharedutils.okhttp.OkHttpManager;\nimport com.liskovsoft.smartyoutubetv2.common.R;\nimport okhttp3.Call;\nimport okhttp3.Callback;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\n\npublic class WebProxyDialog {\n    public static final String TAG = WebProxyDialog.class.getSimpleName();\n    private final Context mContext;\n    private AlertDialog mProxyConfigDialog;\n    private final ProxyManager mProxyManager;\n    private final Handler mProxyTestHandler;\n    private final ArrayList<Call> mUrlTests;\n    private int mNumTests;\n\n    public WebProxyDialog(Context context) {\n        mContext = context;\n        mProxyManager = new ProxyManager(mContext);\n        mProxyTestHandler = new Handler(Looper.myLooper());\n        mUrlTests = new ArrayList<>();\n    }\n\n    public boolean isSupported() {\n        return mProxyManager.isProxySupported();\n    }\n\n    public boolean isEnabled() {\n        return mProxyManager.isProxyEnabled();\n    }\n\n    public void enable(boolean checked) {\n        if (isSupported()) {\n            mProxyManager.saveProxyInfoToPrefs(null, checked);\n            if (checked) {\n                // FIXME: If user hit cancel in dialog, proxy remains enabled.\n                showProxyConfigDialog();\n            } else {\n                mProxyManager.configureSystemProxy();\n            }\n        }\n    }\n\n    protected void appendStatusMessage(String msgFormat, Object ...args) {\n        TextView statusView = mProxyConfigDialog.findViewById(R.id.proxy_config_message);\n        String message = String.format(msgFormat, args);\n        if (statusView.getText().toString().isEmpty())\n            statusView.append(message);\n        else\n            statusView.append(\"\\n\"+message);\n    }\n\n    protected void appendStatusMessage(int resId, Object ...args) {\n        appendStatusMessage(mContext.getString(resId), args);\n    }\n\n    protected Proxy validateProxyConfigFields() {\n        boolean isConfigValid = true;\n        int proxyTypeId = ((RadioGroup) mProxyConfigDialog.findViewById(R.id.proxy_type)).getCheckedRadioButtonId();\n        if (proxyTypeId == -1) {\n            isConfigValid = false;\n            appendStatusMessage(R.string.proxy_type_invalid);\n            mProxyConfigDialog.findViewById(R.id.proxy_type_http).requestFocus();\n        }\n        String proxyHost = ((EditText) mProxyConfigDialog.findViewById(R.id.proxy_host)).getText().toString();\n        if (proxyHost.isEmpty()) {\n            isConfigValid = false;\n            appendStatusMessage(R.string.proxy_host_invalid);\n        }\n        String proxyPortString = ((EditText) mProxyConfigDialog.findViewById(R.id.proxy_port)).getText().toString();\n        int proxyPort = proxyPortString.isEmpty() ? 0 : Helpers.parseInt(proxyPortString);\n        if (proxyPort <= 0) {\n            isConfigValid = false;\n            appendStatusMessage(R.string.proxy_port_invalid);\n        }\n        String proxyUser = ((EditText) mProxyConfigDialog.findViewById(R.id.proxy_username)).getText().toString();\n        String proxyPassword = ((EditText) mProxyConfigDialog.findViewById(R.id.proxy_password)).getText().toString();\n        if (proxyUser.isEmpty() != proxyPassword.isEmpty()) {\n            isConfigValid = false;\n            appendStatusMessage(R.string.proxy_credentials_invalid);\n        }\n        if (!isConfigValid) {\n            return null;\n        }\n        Proxy.Type proxyType = proxyTypeId == R.id.proxy_type_http ? Proxy.Type.HTTP : Proxy.Type.SOCKS;\n        return new Proxy(proxyType, PasswdInetSocketAddress.createUnresolved(proxyHost, proxyPort, proxyUser, proxyPassword));\n    }\n\n    @RequiresApi(19)\n    protected void testProxyConnections() {\n        Proxy proxy;\n        try {\n            proxy = validateProxyConfigFields();\n        } catch (IllegalArgumentException e) {\n            appendStatusMessage(e.getMessage());\n            return;\n        }\n        if (proxy == null) {\n            appendStatusMessage(R.string.proxy_test_aborted);\n            return;\n        }\n\n        mProxyManager.saveProxyInfoToPrefs(proxy, true);\n        mProxyManager.configureSystemProxy();\n\n        String[] testUrls = mContext.getString(R.string.proxy_test_urls).split(\"\\n\");\n        OkHttpClient okHttpClient = OkHttpManager.instance().getClient();\n\n        for (String urlString: testUrls) {\n            int serialNo = ++ mNumTests;\n            Request request = new Request.Builder().url(urlString).build();\n            appendStatusMessage(R.string.proxy_test_start, serialNo, urlString);\n            Call call = okHttpClient.newCall(request);\n            call.enqueue(new Callback() {\n                @Override\n                public void onFailure(Call call, IOException e) {\n                    if (call.isCanceled())\n                        mProxyTestHandler.post(() -> appendStatusMessage(R.string.proxy_test_cancelled, serialNo));\n                    else\n                        mProxyTestHandler.post(() -> appendStatusMessage(R.string.proxy_test_error, serialNo, e));\n                }\n\n                @Override\n                public void onResponse(Call call, Response response) {\n                    String protocol = response.protocol().toString().toUpperCase();\n                    int code = response.code();\n                    String status = response.message();\n                    mProxyTestHandler.post(() -> appendStatusMessage(R.string.proxy_test_status,\n                            serialNo, protocol, code, status.isEmpty() ? \"OK\" : status));\n                }\n            });\n            mUrlTests.add(call);\n        }\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.KITKAT)\n    protected void showProxyConfigDialog() {\n        AlertDialog.Builder builder = new AlertDialog.Builder(mContext, R.style.AppDialog);\n        LayoutInflater inflater = LayoutInflater.from(mContext);\n        View contentView = inflater.inflate(R.layout.web_proxy_dialog, null);\n\n        KeyHelpers.fixShowKeyboard(\n                contentView.findViewById(R.id.proxy_host),\n                contentView.findViewById(R.id.proxy_port),\n                contentView.findViewById(R.id.proxy_username),\n                contentView.findViewById(R.id.proxy_password)\n        );\n\n        if (mProxyManager.getProxyType() == Proxy.Type.DIRECT) {\n            ((EditText) contentView.findViewById(R.id.proxy_host)).setText(\"\");\n            ((EditText) contentView.findViewById(R.id.proxy_port)).setText(\"\");\n            ((RadioGroup) contentView.findViewById(R.id.proxy_type)).clearCheck();\n        } else {\n            ((EditText) contentView.findViewById(R.id.proxy_host)).setText(mProxyManager.getProxyHost());\n            ((EditText) contentView.findViewById(R.id.proxy_port)).setText(String.valueOf(mProxyManager.getProxyPort()));\n            ((EditText) contentView.findViewById(R.id.proxy_username)).setText(mProxyManager.getProxyUsername());\n            ((EditText) contentView.findViewById(R.id.proxy_password)).setText(mProxyManager.getProxyPassword());\n            int proxyTypeId = mProxyManager.getProxyType() == Proxy.Type.HTTP ? R.id.proxy_type_http : R.id.proxy_type_socks;\n            ((RadioGroup) contentView.findViewById(R.id.proxy_type)).check(proxyTypeId);\n        }\n\n        // keep empty, will override below.\n        // https://stackoverflow.com/a/15619098/5379584\n        mProxyConfigDialog = builder\n                .setTitle(R.string.proxy_settings_title)\n                .setView(contentView)\n                .setNeutralButton(R.string.proxy_test_btn, (dialog, which) -> { })\n                .setPositiveButton(android.R.string.ok, (dialog, which) -> { })\n                .setNegativeButton(android.R.string.cancel, (dialog, which) -> { })\n                .create();\n\n        mNumTests = 0;\n        mProxyConfigDialog.show();\n\n        mProxyConfigDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener((view) -> {\n            ((TextView) mProxyConfigDialog.findViewById(R.id.proxy_config_message)).setText(\"\");\n            Proxy proxy = null;\n            try {\n                proxy = validateProxyConfigFields();\n            } catch (IllegalArgumentException e) { // port out of range\n                e.printStackTrace();\n            }\n            if (proxy == null) {\n                appendStatusMessage(R.string.proxy_application_aborted);\n            } else {\n                Log.d(TAG, \"Saving proxy info: \" + proxy);\n                mProxyManager.saveProxyInfoToPrefs(proxy, true);\n                // Proxy applied on dismiss\n                //mProxyManager.configureSystemProxy();\n                for (Call call: mUrlTests) call.cancel();\n                mProxyConfigDialog.dismiss();\n            }\n        });\n\n        mProxyConfigDialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener((view) -> {\n            for (Call call: mUrlTests) call.cancel();\n            mUrlTests.clear();\n            ((TextView) mProxyConfigDialog.findViewById(R.id.proxy_config_message)).setText(\"\");\n            testProxyConnections();\n        });\n\n        mProxyConfigDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener((view) -> {\n            for (Call call: mUrlTests) call.cancel();\n            mProxyConfigDialog.dismiss();\n        });\n\n        mProxyConfigDialog.setOnDismissListener(dialog -> {\n            Proxy proxy = null;\n            try {\n                proxy = validateProxyConfigFields();\n            } catch (IllegalArgumentException e) { // port out of range\n                e.printStackTrace();\n            }\n            if (proxy != null) {\n                Log.d(TAG, \"Saving proxy info: \" + proxy);\n                // Proxy saved on OK button press\n                //mProxyManager.saveProxyInfoToPrefs(proxy, true);\n                mProxyManager.configureSystemProxy();\n                for (Call call: mUrlTests) call.cancel();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/utils/AppDialogUtil.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.utils;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.annotation.TargetApi;\r\nimport android.app.Activity;\r\nimport android.content.Context;\r\nimport android.content.pm.PackageManager;\r\nimport android.os.Build;\r\nimport android.text.TextUtils;\r\n\r\nimport androidx.annotation.NonNull;\r\n\r\nimport com.liskovsoft.mediaserviceinterfaces.MediaItemService;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.ItemGroup;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItem;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.PlaylistInfo;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.PermissionHelpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.rx.RxHelper;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Playlist;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerConstants;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionCategory;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.OptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.ui.UiOptionItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.AppDialogPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.BrowsePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.VideoMenuPresenter.VideoMenuCallback;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.dialogs.menu.providers.channelgroup.ChannelGroupServiceWrapper;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ViewManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.other.SubtitleManager.SubtitleStyle;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem.VideoPreset;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.AppDataSourceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MediaServiceManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MotherActivity;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.SponsorBlockData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\r\nimport com.liskovsoft.youtubeapi.service.YouTubeMediaItemService;\r\nimport com.liskovsoft.youtubeapi.playlist.impl.YouTubePlaylistInfo;\r\n\r\nimport java.io.File;\r\nimport java.util.ArrayList;\r\nimport java.util.Collections;\r\nimport java.util.LinkedHashMap;\r\nimport java.util.List;\r\nimport java.util.Locale;\r\nimport java.util.Map;\r\nimport java.util.Map.Entry;\r\n\r\nimport arte.programar.materialfile.MaterialFilePicker;\r\nimport arte.programar.materialfile.ui.FilePickerActivity;\r\nimport arte.programar.materialfile.utils.FileUtils;\r\nimport io.reactivex.Observable;\r\nimport io.reactivex.disposables.Disposable;\r\n\r\npublic class AppDialogUtil {\r\n    private static final String TAG = AppDialogUtil.class.getSimpleName();\r\n    private static final int VIDEO_BUFFER_ID = 134;\r\n    private static final int BACKGROUND_PLAYBACK_ID = 135;\r\n    private static final int VIDEO_PRESETS_ID = 136;\r\n    private static final int AUDIO_DELAY_ID = 137;\r\n    private static final int AUDIO_LANGUAGE_ID = 138;\r\n    private static final int PLAYER_SCREEN_TIMEOUT_ID = 139;\r\n    private static final int PLAYER_SCREEN_DIMMING_ID = 140;\r\n    private static final int PLAYER_SPEED_LIST_ID = 141;\r\n    private static final int PLAYER_REMEMBER_SPEED_ID = 142;\r\n    private static final int PLAYER_SPEED_MISC_ID = 143;\r\n    private static final int PITCH_EFFECT_ID = 144;\r\n    private static final int AUDIO_VOLUME_ID = 145;\r\n    private static final int PLAYER_REPEAT_ID = 146;\r\n    private static final int PLAYER_ENGINE_ID = 147;\r\n    private static final int IGNORE_DURATION_ID = 148;\r\n    private static final int SLEEP_TIMER_ID = 149;\r\n    private static final int SUBTITLE_STYLES_ID = 45;\r\n    private static final int SUBTITLE_SIZE_ID = 46;\r\n    private static final int SUBTITLE_POSITION_ID = 47;\r\n    private static final int FILE_PICKER_REQUEST_CODE = 205;\r\n\r\n    /**\r\n     * Adds share link items to existing dialog.\r\n     */\r\n    public static void appendShareLinkDialogItem(Context context, AppDialogPresenter dialogPresenter, Video video) {\r\n        appendShareLinkDialogItem(context, dialogPresenter, video, -1);\r\n    }\r\n\r\n    /**\r\n     * Adds share link items to existing dialog.\r\n     */\r\n    public static void appendShareLinkDialogItem(Context context, AppDialogPresenter dialogPresenter, Video video, int positionSec) {\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        if (video.videoId == null && video.channelId == null) {\r\n            return;\r\n        }\r\n\r\n        dialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(context.getString(R.string.share_link), optionItem -> {\r\n                    if (video.videoId != null) {\r\n                        Utils.displayShareVideoDialog(context, video.videoId, positionSec == -1 ? Utils.toSec(video.getPositionMs()) : positionSec);\r\n                    } else if (video.playlistId != null) {\r\n                        Utils.displaySharePlaylistDialog(context, video.playlistId);\r\n                    } else if (video.channelId != null) {\r\n                        Utils.displayShareChannelDialog(context, video.channelId);\r\n                    }\r\n                }));\r\n    }\r\n\r\n    /**\r\n     * Adds share link items to existing dialog.\r\n     */\r\n    public static void appendShareEmbedLinkDialogItem(Context context, AppDialogPresenter dialogPresenter, Video video) {\r\n        appendShareEmbedLinkDialogItem(context, dialogPresenter, video, -1);\r\n    }\r\n\r\n    /**\r\n     * Adds share link items to existing dialog.\r\n     */\r\n    public static void appendShareEmbedLinkDialogItem(Context context, AppDialogPresenter dialogPresenter, Video video, int positionSec) {\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        if (video.videoId == null) {\r\n            return;\r\n        }\r\n\r\n        dialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(context.getString(R.string.share_embed_link), optionItem -> {\r\n                    if (video.videoId != null) {\r\n                        Utils.displayShareEmbedVideoDialog(context, video.videoId, positionSec == -1 ? Utils.toSec(video.getPositionMs()) : positionSec);\r\n                    }\r\n                }));\r\n    }\r\n\r\n    /**\r\n     * Adds QR code item to existing dialog.\r\n     */\r\n    public static void appendShareQRLinkDialogItem(Context context, AppDialogPresenter dialogPresenter, Video video) {\r\n        appendShareQRLinkDialogItem(context, dialogPresenter, video, -1);\r\n    }\r\n\r\n    /**\r\n     * Adds QR code item to existing dialog.\r\n     */\r\n    public static void appendShareQRLinkDialogItem(Context context, AppDialogPresenter dialogPresenter, Video video, int positionSec) {\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        if (video.videoId == null) {\r\n            return;\r\n        }\r\n\r\n        dialogPresenter.appendSingleButton(\r\n                UiOptionItem.from(context.getString(R.string.share_qr_link), optionItem -> {\r\n                    dialogPresenter.closeDialog(); // pause bg video\r\n                    if (video.videoId != null) {\r\n                        Utils.openLink(context, Utils.toQrCodeLink(\r\n                                Utils.convertToFullVideoUrl(video.videoId, positionSec == -1 ? Utils.toSec(video.getPositionMs()) : positionSec).toString()));\r\n                    }\r\n                }));\r\n    }\r\n\r\n    public static OptionCategory createBackgroundPlaybackCategory(Context context, PlayerData playerData, GeneralData generalData) {\r\n        return createBackgroundPlaybackCategory(context, playerData, generalData, () -> {});\r\n    }\r\n\r\n    public static OptionCategory createBackgroundPlaybackCategory(Context context, PlayerData playerData, GeneralData generalData, Runnable onSetCallback) {\r\n        String categoryTitle = context.getString(R.string.category_background_playback);\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n        options.add(UiOptionItem.from(context.getString(R.string.option_background_playback_off),\r\n                optionItem -> {\r\n                    playerData.setBackgroundMode(PlayerData.BACKGROUND_MODE_DEFAULT);\r\n                    generalData.setBackgroundPlaybackShortcut(GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_HOME);\r\n                    onSetCallback.run();\r\n                }, playerData.getBackgroundMode() == PlayerData.BACKGROUND_MODE_DEFAULT));\r\n\r\n        if (Helpers.isPictureInPictureSupported(context)) {\r\n            String pip = context.getString(R.string.option_background_playback_pip);\r\n            options.add(UiOptionItem.from(String.format(\"%s (%s)\", pip, context.getString(R.string.pressing_home)),\r\n                    optionItem -> {\r\n                        playerData.setBackgroundMode(PlayerData.BACKGROUND_MODE_PIP);\r\n                        generalData.setBackgroundPlaybackShortcut(GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_HOME);\r\n                        onSetCallback.run();\r\n                    }, playerData.getBackgroundMode() == PlayerData.BACKGROUND_MODE_PIP &&\r\n                    generalData.getBackgroundPlaybackShortcut() == GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_HOME));\r\n\r\n            options.add(UiOptionItem.from(String.format(\"%s (%s)\", pip, context.getString(R.string.pressing_home_back)),\r\n                    optionItem -> {\r\n                        playerData.setBackgroundMode(PlayerData.BACKGROUND_MODE_PIP);\r\n                        generalData.setBackgroundPlaybackShortcut(GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_HOME_BACK);\r\n                        onSetCallback.run();\r\n                    }, playerData.getBackgroundMode() == PlayerData.BACKGROUND_MODE_PIP &&\r\n                    generalData.getBackgroundPlaybackShortcut() == GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_HOME_BACK));\r\n\r\n            options.add(UiOptionItem.from(String.format(\"%s (%s)\", pip, context.getString(R.string.pressing_back)),\r\n                    optionItem -> {\r\n                        playerData.setBackgroundMode(PlayerData.BACKGROUND_MODE_PIP);\r\n                        generalData.setBackgroundPlaybackShortcut(GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_BACK);\r\n                        onSetCallback.run();\r\n                    }, playerData.getBackgroundMode() == PlayerData.BACKGROUND_MODE_PIP &&\r\n                            generalData.getBackgroundPlaybackShortcut() == GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_BACK));\r\n        }\r\n\r\n        String audio = context.getString(R.string.option_background_playback_only_audio);\r\n        options.add(UiOptionItem.from(String.format(\"%s (%s)\", audio, context.getString(R.string.pressing_home)),\r\n                optionItem -> {\r\n                    playerData.setBackgroundMode(PlayerData.BACKGROUND_MODE_SOUND);\r\n                    generalData.setBackgroundPlaybackShortcut(GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_HOME);\r\n                    onSetCallback.run();\r\n                }, playerData.getBackgroundMode() == PlayerData.BACKGROUND_MODE_SOUND &&\r\n                    generalData.getBackgroundPlaybackShortcut() == GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_HOME));\r\n        options.add(UiOptionItem.from(String.format(\"%s (%s)\", audio, context.getString(R.string.pressing_home_back)),\r\n                optionItem -> {\r\n                    playerData.setBackgroundMode(PlayerData.BACKGROUND_MODE_SOUND);\r\n                    generalData.setBackgroundPlaybackShortcut(GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_HOME_BACK);\r\n                    onSetCallback.run();\r\n                }, playerData.getBackgroundMode() == PlayerData.BACKGROUND_MODE_SOUND &&\r\n                    generalData.getBackgroundPlaybackShortcut() == GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_HOME_BACK));\r\n        options.add(UiOptionItem.from(String.format(\"%s (%s)\", audio, context.getString(R.string.pressing_back)),\r\n                optionItem -> {\r\n                    playerData.setBackgroundMode(PlayerData.BACKGROUND_MODE_SOUND);\r\n                    generalData.setBackgroundPlaybackShortcut(GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_BACK);\r\n                    onSetCallback.run();\r\n                }, playerData.getBackgroundMode() == PlayerData.BACKGROUND_MODE_SOUND &&\r\n                        generalData.getBackgroundPlaybackShortcut() == GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_BACK));\r\n\r\n        if (Helpers.isAndroidTV(context) && Build.VERSION.SDK_INT < 26) { // useful only for pre-Oreo UI\r\n            String behind = context.getString(R.string.option_background_playback_behind);\r\n            options.add(UiOptionItem.from(String.format(\"%s (%s - %s)\", behind, \"Android TV 5,6,7\", context.getString(R.string.pressing_home)),\r\n                    optionItem -> {\r\n                        playerData.setBackgroundMode(PlayerData.BACKGROUND_MODE_PLAY_BEHIND);\r\n                        generalData.setBackgroundPlaybackShortcut(GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_HOME);\r\n                        onSetCallback.run();\r\n                    }, playerData.getBackgroundMode() == PlayerData.BACKGROUND_MODE_PLAY_BEHIND &&\r\n                        generalData.getBackgroundPlaybackShortcut() == GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_HOME));\r\n            options.add(UiOptionItem.from(String.format(\"%s (%s - %s)\", behind, \"Android TV 5,6,7\", context.getString(R.string.pressing_home_back)),\r\n                    optionItem -> {\r\n                        playerData.setBackgroundMode(PlayerData.BACKGROUND_MODE_PLAY_BEHIND);\r\n                        generalData.setBackgroundPlaybackShortcut(GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_HOME_BACK);\r\n                        onSetCallback.run();\r\n                    }, playerData.getBackgroundMode() == PlayerData.BACKGROUND_MODE_PLAY_BEHIND &&\r\n                        generalData.getBackgroundPlaybackShortcut() == GeneralData.BACKGROUND_PLAYBACK_SHORTCUT_HOME_BACK));\r\n        }\r\n\r\n        return OptionCategory.from(BACKGROUND_PLAYBACK_ID, OptionCategory.TYPE_RADIO_LIST, categoryTitle, options);\r\n    }\r\n\r\n    public static OptionCategory createVideoPresetsCategory(Context context) {\r\n        return createVideoPresetsCategory(context, () -> {});\r\n    }\r\n\r\n    public static OptionCategory createVideoPresetsCategory(Context context, Runnable onFormatSelected) {\r\n        return OptionCategory.from(\r\n                VIDEO_PRESETS_ID,\r\n                OptionCategory.TYPE_RADIO_LIST,\r\n                context.getString(R.string.title_video_presets),\r\n                fromPresets(\r\n                        context,\r\n                        AppDataSourceManager.instance().getVideoPresets(),\r\n                        onFormatSelected\r\n                )\r\n        );\r\n    }\r\n\r\n    private static List<OptionItem> fromPresets(Context context, VideoPreset[] presets, Runnable onFormatSelected) {\r\n        List<OptionItem> result = new ArrayList<>();\r\n\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        PlayerTweaksData playerTweaksData = PlayerTweaksData.instance(context);\r\n        FormatItem selectedFormat = playerData.getFormat(FormatItem.TYPE_VIDEO);\r\n        boolean isPresetSelection = selectedFormat != null && selectedFormat.isPreset();\r\n        boolean isAllFormatsUnlocked = playerTweaksData.isAllFormatsUnlocked();\r\n\r\n        for (VideoPreset preset : presets) {\r\n            if (!isAllFormatsUnlocked && !Utils.isPresetSupported(preset)) {\r\n                continue;\r\n            }\r\n\r\n            result.add(0, UiOptionItem.from(preset.name,\r\n                    option -> setFormat(preset.format, playerData, onFormatSelected),\r\n                    isPresetSelection && preset.format.equals(selectedFormat)));\r\n        }\r\n\r\n        //FormatItem noVideo = ExoFormatItem.from(MediaTrack.forRendererIndex(TrackSelectorManager.RENDERER_INDEX_VIDEO), true);\r\n        //result.add(0, UiOptionItem.from(\r\n        //        context.getString(R.string.video_disabled),\r\n        //        optionItem ->\r\n        //                setFormat(noVideo, playerData, onFormatSelected),\r\n        //        isPresetSelection && Helpers.equals(noVideo, selectedFormat)));\r\n\r\n        result.add(0, UiOptionItem.from(\r\n                context.getString(R.string.option_disabled),\r\n                optionItem -> setFormat(playerData.getDefaultVideoFormat(), playerData, onFormatSelected),\r\n                !isPresetSelection));\r\n\r\n        return result;\r\n    }\r\n\r\n    public static OptionCategory createVideoBufferCategory(Context context) {\r\n        return createVideoBufferCategory(context, () -> {});\r\n    }\r\n\r\n    private static void setFormat(FormatItem formatItem, PlayerData playerData, Runnable onFormatSelected) {\r\n        if (playerData.isLegacyCodecsForced()) {\r\n            playerData.setLegacyCodecsForced(false);\r\n        }\r\n        playerData.setFormat(formatItem);\r\n        onFormatSelected.run();\r\n    }\r\n\r\n    public static OptionCategory createVideoBufferCategory(Context context, Runnable onBufferSelected) {\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        String videoBufferTitle = context.getString(R.string.video_buffer);\r\n        List<OptionItem> optionItems = new ArrayList<>();\r\n        optionItems.add(createVideoBufferOption(context, playerData, R.string.video_buffer_size_low, PlayerData.BUFFER_LOW, onBufferSelected));\r\n        optionItems.add(createVideoBufferOption(context, playerData, R.string.video_buffer_size_med, PlayerData.BUFFER_MEDIUM, onBufferSelected));\r\n        optionItems.add(createVideoBufferOption(context, playerData, R.string.video_buffer_size_high, PlayerData.BUFFER_HIGH, onBufferSelected));\r\n        optionItems.add(createVideoBufferOption(context, playerData, R.string.video_buffer_size_highest, PlayerData.BUFFER_HIGHEST, onBufferSelected));\r\n        return OptionCategory.from(VIDEO_BUFFER_ID, OptionCategory.TYPE_RADIO_LIST, videoBufferTitle, optionItems);\r\n    }\r\n\r\n    private static OptionItem createVideoBufferOption(Context context, PlayerData playerData, int titleResId, int type, Runnable onBufferSelected) {\r\n        return UiOptionItem.from(\r\n                context.getString(titleResId),\r\n                optionItem -> {\r\n                    playerData.setVideoBufferType(type);\r\n                    onBufferSelected.run();\r\n                },\r\n                playerData.getVideoBufferType() == type);\r\n    }\r\n\r\n    public static OptionCategory createAudioLanguageCategory(Context context) {\r\n        return createAudioLanguageCategory(context, () -> {});\r\n    }\r\n\r\n    public static OptionCategory createAudioLanguageCategory(Context context, Runnable onSetCallback) {\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        String title = context.getString(R.string.audio_language);\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        List<String> addedCodes = new ArrayList<>();\r\n        List<String> lastLanguages = playerData.getLastAudioLanguages();\r\n\r\n        for (Locale locale : Locale.getAvailableLocales()) {\r\n            String languageCode = locale.getLanguage().toLowerCase();\r\n\r\n            if (addedCodes.contains(languageCode) || lastLanguages.contains(languageCode)) {\r\n                continue;\r\n            }\r\n\r\n            options.add(UiOptionItem.from(locale.getDisplayLanguage(),\r\n                    optionItem -> {\r\n                        playerData.setAudioLanguage(languageCode);\r\n                        onSetCallback.run();\r\n                    },\r\n                    languageCode.equals(playerData.getAudioLanguage())));\r\n            addedCodes.add(languageCode);\r\n        }\r\n\r\n        // NOTE: Comparator.comparing API >= 24\r\n        // Alphabetical order\r\n        Collections.sort(options, (o1, o2) -> ((String) o1.getTitle()).compareTo((String) o2.getTitle()));\r\n\r\n        int idx = 0;\r\n\r\n        for (String languageCode : lastLanguages) {\r\n            if (TextUtils.isEmpty(languageCode)) { // original\r\n                continue;\r\n            }\r\n\r\n            Locale locale = new Locale(languageCode);\r\n\r\n            options.add(idx++, UiOptionItem.from(locale.getDisplayLanguage(),\r\n                    optionItem -> {\r\n                        playerData.setAudioLanguage(languageCode);\r\n                        onSetCallback.run();\r\n                    },\r\n                    languageCode.equals(playerData.getAudioLanguage())));\r\n        }\r\n\r\n        options.add(0, UiOptionItem.from(context.getString(R.string.original_lang),\r\n                optionItem -> {\r\n                    playerData.setAudioLanguage(\"\");\r\n                    onSetCallback.run();\r\n                },\r\n                \"\".equals(playerData.getAudioLanguage())));\r\n\r\n        return OptionCategory.from(AUDIO_LANGUAGE_ID, OptionCategory.TYPE_RADIO_LIST, title, options);\r\n    }\r\n\r\n    public static OptionCategory createAudioShiftCategory(Context context) {\r\n        return createAudioShiftCategory(context, () -> {});\r\n    }\r\n\r\n    public static OptionCategory createAudioShiftCategory(Context context, Runnable onSetCallback) {\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        String title = context.getResources().getQuantityString(R.plurals.seconds, 10, context.getString(R.string.audio_shift));\r\n\r\n        OptionItem optionItem = UiOptionItem.from(title, item -> {\r\n            AppDialogPresenter.instance(context).closeDialog();\r\n            float delaySec = playerData.getAudioDelayMs() / 1_000f;\r\n            SimpleEditDialog.show(\r\n                    context,\r\n                    title,\r\n                    Helpers.toString(delaySec),\r\n                    newValue -> {\r\n                        if (!Helpers.isNumeric(newValue)) {\r\n                            return false;\r\n                        }\r\n\r\n                        float newDelaySec = Helpers.parseFloat(newValue);\r\n                        int newDelayMs = (int) (newDelaySec * 1_000);\r\n\r\n                        if (newDelayMs == 0 && newDelaySec != 0) { // lower than 1ms\r\n                            return false;\r\n                        }\r\n\r\n                        playerData.setAudioDelayMs(newDelayMs);\r\n                        onSetCallback.run();\r\n                        return true;\r\n                    }\r\n            );\r\n        });\r\n\r\n        return OptionCategory.from(AUDIO_DELAY_ID, OptionCategory.TYPE_SINGLE_BUTTON, title, optionItem);\r\n    }\r\n\r\n    public static OptionCategory createAudioVolumeCategory(Context context) {\r\n        return createAudioVolumeCategory(context, () -> {});\r\n    }\r\n\r\n    public static OptionCategory createAudioVolumeCategory(Context context, Runnable onSetCallback) {\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        PlayerTweaksData playerTweaksData = PlayerTweaksData.instance(context);\r\n        String title = context.getString(R.string.player_volume);\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int scalePercent : Helpers.range(0, 300, 5)) {\r\n            float scale = scalePercent / 100f;\r\n            options.add(UiOptionItem.from(String.format(\"%s%%\", scalePercent),\r\n                    optionItem -> {\r\n                        playerData.setPlayerVolume(scale);\r\n                        playerTweaksData.setPlayerAutoVolumeEnabled(scalePercent == 100);\r\n\r\n                        if (scalePercent > 100) {\r\n                            MessageHelpers.showLongMessage(context, R.string.volume_boost_warning);\r\n                        }\r\n\r\n                        onSetCallback.run();\r\n                    },\r\n                    Helpers.floatEquals(scale, playerData.getPlayerVolume())));\r\n        }\r\n\r\n        return OptionCategory.from(AUDIO_VOLUME_ID, OptionCategory.TYPE_RADIO_LIST, title, options);\r\n    }\r\n\r\n    public static OptionCategory createPitchEffectCategory(Context context) {\r\n        String title = context.getString(R.string.pitch_effect);\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        addPitches(context, options, Helpers.range(0.025f, 0.975f, 0.025f));\r\n        addPitches(context, options, new float[]{ 0.985f, 0.990f, 0.995f }); // Custom pitches\r\n        addPitches(context, options, Helpers.range(1f, 2f, 0.025f));\r\n\r\n        return OptionCategory.from(PITCH_EFFECT_ID, OptionCategory.TYPE_RADIO_LIST, title, options);\r\n    }\r\n\r\n    private static void addPitches(Context context, List<OptionItem> options, float[] pitchList) {\r\n        PlayerManager playerManager = PlaybackPresenter.instance(context).getPlayer();\r\n        PlayerData playerData = PlayerData.instance(context);\r\n\r\n        for (float pitch : pitchList) {\r\n            options.add(UiOptionItem.from(Helpers.toString(pitch),\r\n                    optionItem -> {\r\n                        playerManager.setPitch(pitch);\r\n                        playerData.setPitch(pitch);\r\n                    },\r\n                    Helpers.floatEquals(pitch, playerManager.getPitch())));\r\n        }\r\n    }\r\n\r\n    public static OptionCategory createSubtitleStylesCategory(Context context) {\r\n        String subtitleStyleTitle = context.getString(R.string.subtitle_style);\r\n\r\n        return OptionCategory.from(SUBTITLE_STYLES_ID, OptionCategory.TYPE_RADIO_LIST, subtitleStyleTitle, createSubtitleStyles(context));\r\n    }\r\n\r\n    public static OptionItem createSubtitleChannelOption(Context context) {\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        return UiOptionItem.from(context.getString(R.string.subtitle_remember),\r\n                optionItem -> playerData.setSubtitlesPerChannelEnabled(optionItem.isSelected()),\r\n                playerData.isSubtitlesPerChannelEnabled()\r\n        );\r\n    }\r\n\r\n    @TargetApi(19)\r\n    private static List<OptionItem> createSubtitleStyles(Context context) {\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        List<SubtitleStyle> subtitleStyles = playerData.getSubtitleStyles();\r\n        List<OptionItem> styleOptions = new ArrayList<>();\r\n\r\n        for (SubtitleStyle subtitleStyle : subtitleStyles) {\r\n            styleOptions.add(UiOptionItem.from(\r\n                    context.getString(subtitleStyle.nameResId),\r\n                    option -> {\r\n                        playerData.setSubtitleStyle(subtitleStyle);\r\n                        Utils.showPlayerControls(context, false);\r\n                    },\r\n                    subtitleStyle.equals(playerData.getSubtitleStyle())));\r\n        }\r\n\r\n        return styleOptions;\r\n    }\r\n\r\n    public static OptionCategory createSubtitleSizeCategory(Context context) {\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int scalePercent : Helpers.range(10, 200, 10)) {\r\n            float scale = scalePercent / 100f;\r\n            options.add(UiOptionItem.from(String.format(\"%sx\", scale),\r\n                    optionItem -> {\r\n                        playerData.setSubtitleScale(scale);\r\n                        Utils.showPlayerControls(context, false);\r\n                    },\r\n                    Helpers.floatEquals(scale, playerData.getSubtitleScale())));\r\n        }\r\n\r\n        return OptionCategory.from(SUBTITLE_SIZE_ID, OptionCategory.TYPE_RADIO_LIST, context.getString(R.string.subtitle_scale), options);\r\n    }\r\n\r\n    public static OptionCategory createSubtitlePositionCategory(Context context) {\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int positionPercent : Helpers.range(0, 100, 5)) {\r\n            float position = positionPercent / 100f;\r\n            options.add(UiOptionItem.from(String.format(\"%s%%\", positionPercent),\r\n                    optionItem -> {\r\n                        playerData.setSubtitlePosition(position);\r\n                        Utils.showPlayerControls(context, false);\r\n                    },\r\n                    Helpers.floatEquals(position, playerData.getSubtitlePosition())));\r\n        }\r\n\r\n        return OptionCategory.from(SUBTITLE_POSITION_ID, OptionCategory.TYPE_RADIO_LIST, context.getString(R.string.subtitle_position), options);\r\n    }\r\n\r\n    public static OptionCategory createVideoZoomCategory(Context context) {\r\n        return createVideoZoomCategory(context, () -> {});\r\n    }\r\n\r\n    public static OptionCategory createVideoZoomCategory(Context context, Runnable onSelectZoomMode) {\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.video_zoom_default, PlayerData.RESIZE_MODE_DEFAULT},\r\n                {R.string.video_zoom_fit_width, PlayerData.RESIZE_MODE_FIT_WIDTH},\r\n                {R.string.video_zoom_fit_height, PlayerData.RESIZE_MODE_FIT_HEIGHT},\r\n                {R.string.video_zoom_fit_both, PlayerData.RESIZE_MODE_FIT_BOTH},\r\n                {R.string.video_zoom_stretch, PlayerData.RESIZE_MODE_STRETCH}}) {\r\n            options.add(UiOptionItem.from(context.getString(pair[0]),\r\n                    optionItem -> {\r\n                        playerData.setResizeMode(pair[1]);\r\n                        playerData.setZoomPercents(-1);\r\n                        onSelectZoomMode.run();\r\n                    },\r\n                    playerData.getResizeMode() == pair[1] && playerData.getZoomPercents() == -1));\r\n        }\r\n\r\n        // Zoom above 100% has centering problems with 2K-4K videos\r\n        int[][] zoomRanges = {\r\n            Helpers.range(50, 95, 5),  // from 50 to 95 in steps of 5\r\n            Helpers.range(96, 100, 1), // from 96 to 100 in steps of 1\r\n            Helpers.range(105, 300, 5) // from 105 to 300 in steps of 5\r\n        };\r\n\r\n        for (int[] zoomRange : zoomRanges) {\r\n            for (int zoomPercents : zoomRange) {\r\n                options.add(UiOptionItem.from(String.format(\"%s%%\", zoomPercents),\r\n                        optionItem -> {\r\n                            playerData.setZoomPercents(zoomPercents);\r\n                            playerData.setResizeMode(PlayerData.RESIZE_MODE_DEFAULT);\r\n                            onSelectZoomMode.run();\r\n                        },\r\n                        playerData.getZoomPercents() == zoomPercents));\r\n            }\r\n        }\r\n\r\n        String videoZoomTitle = context.getString(R.string.video_zoom);\r\n\r\n        return OptionCategory.from(SUBTITLE_STYLES_ID, OptionCategory.TYPE_RADIO_LIST, videoZoomTitle, options);\r\n    }\r\n\r\n    public static OptionCategory createVideoAspectCategory(Context context, PlayerData playerData, Runnable onSelectAspectMode) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        Map<String, Float> pairs = new LinkedHashMap<>();\r\n        pairs.put(context.getString(R.string.video_zoom_default), PlayerData.ASPECT_RATIO_DEFAULT);\r\n        pairs.put(\"1:1\", PlayerData.ASPECT_RATIO_1_1);\r\n        pairs.put(\"4:3\", PlayerData.ASPECT_RATIO_4_3);\r\n        pairs.put(\"5:4\", PlayerData.ASPECT_RATIO_5_4);\r\n        pairs.put(\"16:9\", PlayerData.ASPECT_RATIO_16_9);\r\n        pairs.put(\"16:10\", PlayerData.ASPECT_RATIO_16_10);\r\n        pairs.put(\"21:9 (2.33:1)\", PlayerData.ASPECT_RATIO_21_9);\r\n        pairs.put(\"64:27 (2.37:1)\", PlayerData.ASPECT_RATIO_64_27);\r\n        pairs.put(\"2.21:1\", PlayerData.ASPECT_RATIO_221_1);\r\n        pairs.put(\"2.35:1\", PlayerData.ASPECT_RATIO_235_1);\r\n        pairs.put(\"2.39:1\", PlayerData.ASPECT_RATIO_239_1);\r\n\r\n        for (Entry<String, Float> entry: pairs.entrySet()) {\r\n            options.add(UiOptionItem.from(entry.getKey(),\r\n                    optionItem -> {\r\n                        playerData.setAspectRatio(entry.getValue());\r\n                        onSelectAspectMode.run();\r\n                    }, Helpers.floatEquals(playerData.getAspectRatio(), entry.getValue())));\r\n        }\r\n\r\n        String videoZoomTitle = context.getString(R.string.video_aspect);\r\n\r\n        return OptionCategory.from(SUBTITLE_STYLES_ID, OptionCategory.TYPE_RADIO_LIST, videoZoomTitle, options);\r\n    }\r\n\r\n    public static OptionCategory createVideoRotateCategory(Context context, PlayerData playerData, Runnable onRotate) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int angle : new int[] {0, 90, 180, 270}) {\r\n            options.add(UiOptionItem.from(String.valueOf(angle),\r\n                    optionItem -> {\r\n                        playerData.setRotationAngle(angle);\r\n                        onRotate.run();\r\n                    }, playerData.getRotationAngle() == angle));\r\n        }\r\n\r\n        String videoRotateTitle = context.getString(R.string.video_rotate);\r\n\r\n        return OptionCategory.from(SUBTITLE_STYLES_ID, OptionCategory.TYPE_RADIO_LIST, videoRotateTitle, options);\r\n    }\r\n\r\n    public static OptionCategory createPlayerScreenOffDimmingCategory(Context context, Runnable onApply) {\r\n        PlayerTweaksData playerTweaksData = PlayerTweaksData.instance(context);\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int dimPercents : Helpers.range(10, 100, 10)) {\r\n            options.add(UiOptionItem.from(dimPercents + \"%\",\r\n                    optionItem -> {\r\n                        playerTweaksData.setScreenOffDimmingPercents(dimPercents);\r\n                        if (onApply != null) {\r\n                            onApply.run();\r\n                        }\r\n                    },\r\n                    playerTweaksData.getScreenOffDimmingPercents() == dimPercents));\r\n        }\r\n\r\n        String title = context.getString(R.string.screen_dimming_amount);\r\n\r\n        return OptionCategory.from(PLAYER_SCREEN_DIMMING_ID, OptionCategory.TYPE_RADIO_LIST, title, options);\r\n    }\r\n\r\n    @SuppressLint(\"StringFormatMatches\")\r\n    public static OptionCategory createPlayerScreenOffTimeoutCategory(Context context, Runnable onApply) {\r\n        PlayerTweaksData playerTweaksData = PlayerTweaksData.instance(context);\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int timeoutSec : Helpers.range(0, 10, 1)) {\r\n            options.add(UiOptionItem.from(timeoutSec == 0 ? context.getString(R.string.option_never) : context.getString(R.string.ui_hide_timeout_sec, timeoutSec),\r\n                    optionItem -> {\r\n                        playerTweaksData.setScreenOffTimeoutSec(timeoutSec);\r\n                        if (onApply != null) {\r\n                            onApply.run();\r\n                        }\r\n                    },\r\n                    playerTweaksData.getScreenOffTimeoutSec() == timeoutSec));\r\n        }\r\n\r\n        for (int min : Helpers.range(30, 180, 30)) {\r\n            int timeoutSec = min * 60;\r\n            options.add(UiOptionItem.from(\r\n                    context.getString(R.string.screen_dimming_timeout_min, min),\r\n                    option -> {\r\n                        playerTweaksData.setScreenOffTimeoutSec(timeoutSec);\r\n                        if (onApply != null) {\r\n                            onApply.run();\r\n                        }\r\n                    },\r\n                    playerTweaksData.getScreenOffTimeoutSec() == timeoutSec));\r\n        }\r\n\r\n        String title = context.getString(R.string.screen_dimming_timeout);\r\n\r\n        return OptionCategory.from(PLAYER_SCREEN_TIMEOUT_ID, OptionCategory.TYPE_RADIO_LIST, title, options);\r\n    }\r\n\r\n    public static OptionItem createExcludeFromContentBlockButton(\r\n            Context context,  Video video, MediaServiceManager serviceManager, Runnable onClose) {\r\n        return UiOptionItem.from(\r\n                context.getString(\r\n                        SponsorBlockData.instance(context).isChannelExcluded(video.channelId) ?\r\n                                R.string.content_block_stop_excluding_channel :\r\n                                R.string.content_block_exclude_channel),\r\n                optionItem -> {\r\n                    if (video.hasChannel()) {\r\n                        SponsorBlockData.instance(context).toggleExcludeChannel(video.channelId);\r\n                        if (onClose != null) {\r\n                            onClose.run();\r\n                        }\r\n                    } else {\r\n                        MessageHelpers.showMessage(context, R.string.wait_data_loading);\r\n\r\n                        serviceManager.loadMetadata(\r\n                                video,\r\n                                metadata -> {\r\n                                    video.sync(metadata);\r\n                                    SponsorBlockData.instance(context).excludeChannel(video.channelId);\r\n                                    if (onClose != null) {\r\n                                        onClose.run();\r\n                                    }\r\n                                }\r\n                        );\r\n                    }\r\n                });\r\n    }\r\n\r\n    public static OptionCategory createSpeedListCategory(Context context, PlayerManager playbackController) {\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        List<OptionItem> items = new ArrayList<>();\r\n\r\n        PlayerTweaksData data = PlayerTweaksData.instance(context);\r\n        for (float speed : data.isLongSpeedListEnabled() ? Utils.SPEED_LIST_LONG :\r\n                data.isExtraLongSpeedListEnabled() ? Utils.SPEED_LIST_EXTRA_LONG : Utils.SPEED_LIST_SHORT) {\r\n            items.add(UiOptionItem.from(\r\n                    String.valueOf(speed),\r\n                    optionItem -> {\r\n                        if (playbackController != null) {\r\n                            //playerData.setSpeed(playbackController.getVideo().channelId, speed);\r\n                            playbackController.setSpeed(speed);\r\n                        } else {\r\n                            playerData.setSpeed(speed);\r\n                        }\r\n                    },\r\n                    (playbackController != null ? playbackController.getSpeed() : playerData.getSpeed()) == speed));\r\n        }\r\n\r\n        return OptionCategory.from(PLAYER_SPEED_LIST_ID, OptionCategory.TYPE_RADIO_LIST, context.getString(R.string.video_speed), items);\r\n    }\r\n\r\n    public static OptionCategory createRememberSpeedCategory(Context context) {\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(context.getString(R.string.player_remember_speed_none),\r\n                optionItem -> {\r\n                    playerData.setAllSpeedEnabled(false);\r\n                    playerData.setSpeedPerVideoEnabled(false);\r\n                    playerData.setSpeedPerChannelEnabled(false);\r\n                },\r\n                !playerData.isAllSpeedEnabled() && !playerData.isSpeedPerVideoEnabled()));\r\n\r\n        options.add(UiOptionItem.from(context.getString(R.string.player_remember_speed_all),\r\n                optionItem -> playerData.setAllSpeedEnabled(true),\r\n                playerData.isAllSpeedEnabled()));\r\n\r\n        options.add(UiOptionItem.from(context.getString(R.string.player_remember_speed_each),\r\n                optionItem -> playerData.setSpeedPerVideoEnabled(true),\r\n                playerData.isSpeedPerVideoEnabled()));\r\n\r\n        options.add(UiOptionItem.from(context.getString(R.string.player_speed_per_channel),\r\n                option -> playerData.setSpeedPerChannelEnabled(option.isSelected()),\r\n                playerData.isSpeedPerChannelEnabled()));\r\n\r\n        String title = context.getString(R.string.player_remember_speed);\r\n\r\n        return OptionCategory.from(PLAYER_REMEMBER_SPEED_ID, OptionCategory.TYPE_RADIO_LIST, title, options);\r\n    }\r\n\r\n    public static OptionCategory createSpeedMiscCategory(Context context) {\r\n        PlayerTweaksData playerTweaksData = PlayerTweaksData.instance(context);\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(context.getString(R.string.player_long_speed_list),\r\n                option -> playerTweaksData.setLongSpeedListEnabled(option.isSelected()),\r\n                playerTweaksData.isLongSpeedListEnabled()));\r\n\r\n        options.add(UiOptionItem.from(context.getString(R.string.player_extra_long_speed_list),\r\n                option -> playerTweaksData.setExtraLongSpeedListEnabled(option.isSelected()),\r\n                playerTweaksData.isExtraLongSpeedListEnabled()));\r\n\r\n        String title = context.getString(R.string.player_other);\r\n\r\n        return OptionCategory.from(PLAYER_SPEED_MISC_ID, OptionCategory.TYPE_CHECKBOX_LIST, title, options);\r\n    }\r\n\r\n    public static OptionCategory createPlaybackModeCategory(Context context) {\r\n        return createPlaybackModeCategory(context, () -> {});\r\n    }\r\n\r\n    public static OptionCategory createPlaybackModeCategory(Context context, Runnable onModeSelected) {\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.repeat_mode_all, PlayerConstants.PLAYBACK_MODE_ALL},\r\n                {R.string.repeat_mode_one, PlayerConstants.PLAYBACK_MODE_ONE},\r\n                {R.string.repeat_mode_shuffle, PlayerConstants.PLAYBACK_MODE_SHUFFLE},\r\n                {R.string.repeat_mode_pause_alt, PlayerConstants.PLAYBACK_MODE_LIST},\r\n                {R.string.repeat_mode_reverse_list, PlayerConstants.PLAYBACK_MODE_REVERSE_LIST},\r\n                {R.string.repeat_mode_pause, PlayerConstants.PLAYBACK_MODE_PAUSE},\r\n                {R.string.repeat_mode_none, PlayerConstants.PLAYBACK_MODE_CLOSE}\r\n        }) {\r\n            options.add(UiOptionItem.from(context.getString(pair[0]),\r\n                    optionItem -> {\r\n                        playerData.setPlaybackMode(pair[1]);\r\n                        onModeSelected.run();\r\n                    },\r\n                    playerData.getPlaybackMode() == pair[1]\r\n            ));\r\n        }\r\n\r\n        return OptionCategory.from(\r\n                PLAYER_REPEAT_ID,\r\n                OptionCategory.TYPE_RADIO_LIST,\r\n                context.getString(R.string.action_repeat_mode),\r\n                options\r\n        );\r\n    }\r\n\r\n    public static OptionCategory createNetworkEngineCategory(Context context) {\r\n        return createNetworkEngineCategory(context, () -> {});\r\n    }\r\n\r\n    public static OptionCategory createNetworkEngineCategory(Context context, Runnable onModeSelected) {\r\n        PlayerTweaksData playerTweaksData = PlayerTweaksData.instance(context);\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(context.getString(R.string.default_lang),\r\n                context.getString(R.string.default_stack_desc),\r\n                option -> {\r\n                    playerTweaksData.setPlayerDataSource(PlayerTweaksData.PLAYER_DATA_SOURCE_DEFAULT);\r\n                    onModeSelected.run();\r\n                },\r\n                playerTweaksData.getPlayerDataSource() == PlayerTweaksData.PLAYER_DATA_SOURCE_DEFAULT));\r\n\r\n        options.add(UiOptionItem.from(\"Cronet\",\r\n                context.getString(R.string.cronet_desc),\r\n                option -> {\r\n                    playerTweaksData.setPlayerDataSource(PlayerTweaksData.PLAYER_DATA_SOURCE_CRONET);\r\n                    onModeSelected.run();\r\n                },\r\n                playerTweaksData.getPlayerDataSource() == PlayerTweaksData.PLAYER_DATA_SOURCE_CRONET));\r\n\r\n        options.add(UiOptionItem.from(\"OkHttp\",\r\n                context.getString(R.string.okhttp_desc),\r\n                option -> {\r\n                    playerTweaksData.setPlayerDataSource(PlayerTweaksData.PLAYER_DATA_SOURCE_OKHTTP);\r\n                    onModeSelected.run();\r\n                },\r\n                playerTweaksData.getPlayerDataSource() == PlayerTweaksData.PLAYER_DATA_SOURCE_OKHTTP));\r\n\r\n        return OptionCategory.from(\r\n                PLAYER_ENGINE_ID,\r\n                OptionCategory.TYPE_RADIO_LIST,\r\n                context.getString(R.string.player_network_stack),\r\n                options\r\n        );\r\n    }\r\n\r\n    public static OptionCategory createIgnoreShortSegmentsCategory(Context context) {\r\n        return createIgnoreShortSegmentsCategory(context, () -> {});\r\n    }\r\n\r\n    public static OptionCategory createIgnoreShortSegmentsCategory(Context context, Runnable onSetCallback) {\r\n        SponsorBlockData sponsorBlockData = SponsorBlockData.instance(context);\r\n        String title = context.getString(R.string.ignore_short_segments);\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int durationMs : Helpers.range(0, 20_000, 500)) {\r\n            float sec = durationMs / 1_000f;\r\n            options.add(UiOptionItem.from(\r\n                    durationMs == 0 ? context.getString(R.string.option_disabled)\r\n                            : context.getResources().getQuantityString(R.plurals.seconds, (int) sec, Helpers.toString(sec)),\r\n                    optionItem -> {\r\n                        sponsorBlockData.setIgnoredDurationMs(durationMs);\r\n                        onSetCallback.run();\r\n                    },\r\n                    durationMs == sponsorBlockData.getIgnoredDurationMs()));\r\n        }\r\n\r\n        return OptionCategory.from(IGNORE_DURATION_ID, OptionCategory.TYPE_RADIO_LIST, title, options);\r\n    }\r\n\r\n    public static OptionCategory createSleepTimerCategory(Context context) {\r\n        PlayerData playerData = PlayerData.instance(context);\r\n        String title = context.getString(R.string.player_sleep_timer);\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (float sleepHours : Helpers.range(0, 10, 0.5f)) {\r\n            options.add(UiOptionItem.from(\r\n                    sleepHours == 0 ? context.getString(R.string.option_disabled)\r\n                            : context.getResources().getQuantityString(R.plurals.hours, (int) sleepHours, Helpers.toString(sleepHours)),\r\n                    option -> playerData.setSleepTimerHours(sleepHours),\r\n                    Helpers.floatEquals(playerData.getSleepTimerHours(), sleepHours)));\r\n        }\r\n\r\n        return OptionCategory.from(SLEEP_TIMER_ID, OptionCategory.TYPE_RADIO_LIST, title, options);\r\n    }\r\n\r\n    public static OptionItem createSubscriptionsBackupButton(Context context) {\r\n        AppDialogPresenter dialogPresenter = AppDialogPresenter.instance(context);\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        // Import from the file\r\n        String filePickerTitle = context.getString(R.string.import_subscriptions_group) + \" (GrayJay/PocketTube/NewPipe)\";\r\n        OptionItem item = UiOptionItem.from(filePickerTitle, optionItem -> {\r\n            dialogPresenter.closeDialog();\r\n\r\n            MotherActivity activity = getMotherActivity(context);\r\n\r\n            if (PermissionHelpers.hasStoragePermissions(activity)) {\r\n                runFilePicker(activity, filePickerTitle);\r\n            } else {\r\n                activity.addOnPermissions((requestCode, permissions, grantResults) -> {\r\n                    if (requestCode == PermissionHelpers.REQUEST_EXTERNAL_STORAGE) {\r\n                        if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {\r\n                            runFilePicker(activity, filePickerTitle);\r\n                        }\r\n                    }\r\n                });\r\n                PermissionHelpers.verifyStoragePermissions(activity);\r\n            }\r\n        });\r\n\r\n        return item;\r\n    }\r\n\r\n    private static void runFilePicker(Activity activity, String title) {\r\n        File filesDir = FileUtils.getFile(activity, null);\r\n\r\n        if (filesDir == null) {\r\n            Log.e(TAG, \"Storage device is unavailable\");\r\n            return;\r\n        }\r\n\r\n        new MaterialFilePicker()\r\n                .withActivity(activity)\r\n                .withTitle(title)\r\n                .withRootPath(filesDir.getAbsolutePath())\r\n                .start(FILE_PICKER_REQUEST_CODE);\r\n    }\r\n\r\n    @NonNull\r\n    private static MotherActivity getMotherActivity(Context context) {\r\n        ChannelGroupServiceWrapper mService = ChannelGroupServiceWrapper.instance(context);\r\n        MotherActivity activity = (MotherActivity) context;\r\n        activity.addOnResult((requestCode, resultCode, data) -> {\r\n            if (FILE_PICKER_REQUEST_CODE == requestCode && resultCode == Activity.RESULT_OK) {\r\n                String filePath = data.getStringExtra(FilePickerActivity.RESULT_FILE_PATH);\r\n                RxHelper.execute(mService.importGroupsObserve(new File(filePath)), result -> pinGroups(context, result),\r\n                        error -> MessageHelpers.showLongMessage(context, error.getMessage()));\r\n            }\r\n        });\r\n        return activity;\r\n    }\r\n\r\n    private static void pinGroups(Context context, @NonNull List<ItemGroup> newGroups) {\r\n        if (newGroups.isEmpty()) {\r\n            // Already added to Subscriptions section\r\n            MessageHelpers.showMessage(context, context.getString(R.string.msg_done));\r\n            return;\r\n        }\r\n\r\n        for (ItemGroup group : newGroups) {\r\n            BrowsePresenter.instance(context).pinItem(Video.from(group));\r\n        }\r\n        MessageHelpers.showMessage(context, context.getString(R.string.pinned_to_sidebar));\r\n    }\r\n\r\n    public static void showConfirmationDialog(Context context, String title, Runnable onConfirm) {\r\n        showConfirmationDialog(context, title, onConfirm, () -> {});\r\n    }\r\n\r\n    public static void showConfirmationDialog(Context context, String title, Runnable onConfirm, Runnable onCancel) {\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(context);\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        options.add(UiOptionItem.from(context.getString(R.string.cancel),\r\n                option -> {\r\n                    settingsPresenter.goBack();\r\n                    onCancel.run();\r\n                }));\r\n\r\n        options.add(UiOptionItem.from(context.getString(R.string.btn_confirm),\r\n                option -> {\r\n                    settingsPresenter.goBack();\r\n                    onConfirm.run();\r\n                }));\r\n\r\n        settingsPresenter.appendStringsCategory(title, options);\r\n\r\n        settingsPresenter.showDialog(title);\r\n    }\r\n\r\n    public static void appendSeekIntervalDialogItems(Context context, AppDialogPresenter dialogPresenter, PlayerData playerData, boolean closeOnSelect) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int intervalMs : new int[] {1_000, 2_000, 3_000, 5_000, 7_000, 10_000, 15_000, 20_000, 30_000, 60_000}) {\r\n            options.add(UiOptionItem.from(context.getString(R.string.seek_interval_sec, Helpers.toString(intervalMs / 1_000f)),\r\n                    optionItem -> {\r\n                        playerData.setSeekIncrementMs(intervalMs);\r\n                        if (playerData.getSeekPreviewMode() == PlayerData.SEEK_PREVIEW_CAROUSEL_SLOW) {\r\n                            Utils.showNotCompatibleMessage(context, R.string.player_seek_preview_carousel_slow);\r\n                        }\r\n                        if (closeOnSelect) {\r\n                            dialogPresenter.closeDialog();\r\n                        }\r\n                    },\r\n                    intervalMs == playerData.getSeekIncrementMs()));\r\n        }\r\n\r\n        dialogPresenter.appendRadioCategory(context.getString(R.string.seek_interval), options);\r\n    }\r\n\r\n    public static void showAddToPlaylistDialog(Context context, Video video, VideoMenuCallback callback) {\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        MediaItemService itemManager = YouTubeMediaItemService.instance();\r\n\r\n        Disposable playlistsInfoAction = itemManager.getPlaylistsInfoObserve(video.videoId)\r\n                .subscribe(\r\n                        videoPlaylistInfos -> showAddToPlaylistDialog(context, video, callback, videoPlaylistInfos, null),\r\n                        error -> {\r\n                            // Fallback to something on error\r\n                            Log.e(TAG, \"Get playlists error: %s\", error.getMessage());\r\n                            MessageHelpers.showMessage(context, R.string.section_is_empty);\r\n                        }\r\n                );\r\n    }\r\n\r\n    public static void showAddToPlaylistDialog(Context context, Video video, VideoMenuCallback callback, List<PlaylistInfo> playlistInfos, Runnable onFinish) {\r\n        if (playlistInfos == null) {\r\n            MessageHelpers.showMessage(context, R.string.msg_signed_users_only);\r\n            return;\r\n        }\r\n\r\n        AppDialogPresenter dialogPresenter = AppDialogPresenter.instance(context);\r\n\r\n        appendPlaylistDialogContent(context, video, callback, dialogPresenter, playlistInfos);\r\n        dialogPresenter.showDialog(context.getString(R.string.dialog_add_to_playlist), onFinish);\r\n    }\r\n\r\n    private static void appendPlaylistDialogContent(\r\n            Context context, Video video, VideoMenuCallback callback, AppDialogPresenter dialogPresenter, List<PlaylistInfo> playlistInfos) {\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (PlaylistInfo playlistInfo : playlistInfos) {\r\n            options.add(UiOptionItem.from(\r\n                    playlistInfo.getTitle(),\r\n                    (item) -> {\r\n                        if (playlistInfo instanceof YouTubePlaylistInfo) {\r\n                            ((YouTubePlaylistInfo) playlistInfo).setSelected(item.isSelected());\r\n                        }\r\n                        addRemoveFromPlaylist(context, video, callback, playlistInfo.getPlaylistId(), item.isSelected());\r\n                        GeneralData.instance(context).setLastPlaylistId(playlistInfo.getPlaylistId());\r\n                        GeneralData.instance(context).setLastPlaylistTitle(playlistInfo.getTitle());\r\n                    },\r\n                    playlistInfo.isSelected()));\r\n        }\r\n\r\n        dialogPresenter.appendCheckedCategory(context.getString(R.string.dialog_add_to_playlist), options);\r\n    }\r\n\r\n    private static void addRemoveFromPlaylist(Context context, Video video, VideoMenuCallback callback, String playlistId, boolean add) {\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        Observable<Void> editObserve;\r\n        MediaItemService itemManager = YouTubeMediaItemService.instance();\r\n\r\n        if (add) {\r\n            editObserve = video.mediaItem != null ?\r\n                    itemManager.addToPlaylistObserve(playlistId, video.mediaItem) : itemManager.addToPlaylistObserve(playlistId, video.videoId);\r\n        } else {\r\n            // Check that the current video belongs to the right section\r\n            if (callback != null && Helpers.equals(video.playlistId, playlistId)) {\r\n                callback.onItemAction(video, VideoMenuCallback.ACTION_REMOVE_FROM_PLAYLIST);\r\n            }\r\n            editObserve = itemManager.removeFromPlaylistObserve(playlistId, video.videoId);\r\n        }\r\n\r\n        // Handle error: Maximum playlist size exceeded (> 5000 items)\r\n        RxHelper.execute(editObserve, error -> MessageHelpers.showLongMessage(context, error.getMessage()));\r\n    }\r\n\r\n    public static void showPlaylistOrderDialog(Context context, Video video, Runnable onClose) {\r\n        if (video == null) {\r\n            return;\r\n        }\r\n\r\n        if (video.hasPlaylist()) {\r\n            showPlaylistOrderDialog(context, video.playlistId, onClose);\r\n        } else if (video.belongsToUserPlaylists()) {\r\n            MediaServiceManager.instance().loadChannelUploads(video, group -> {\r\n                if (group.getMediaItems() == null || group.getMediaItems().isEmpty()) {\r\n                    return;\r\n                }\r\n\r\n                MediaItem first = group.getMediaItems().get(0);\r\n                String playlistId = first.getPlaylistId();\r\n\r\n                showPlaylistOrderDialog(context, playlistId, onClose);\r\n            });\r\n        }\r\n    }\r\n\r\n    public static void showPlaylistOrderDialog(Context context, String playlistId, Runnable onClose) {\r\n        AppDialogPresenter dialogPresenter = AppDialogPresenter.instance(context);\r\n\r\n        GeneralData generalData = GeneralData.instance(context);\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        for (int[] pair : new int[][] {\r\n                {R.string.playlist_order_added_date_newer_first, MediaItemService.PLAYLIST_ORDER_ADDED_DATE_NEWER_FIRST},\r\n                {R.string.playlist_order_added_date_older_first, MediaItemService.PLAYLIST_ORDER_ADDED_DATE_OLDER_FIRST},\r\n                {R.string.playlist_order_popularity, MediaItemService.PLAYLIST_ORDER_POPULARITY},\r\n                {R.string.playlist_order_published_date_newer_first, MediaItemService.PLAYLIST_ORDER_PUBLISHED_DATE_NEWER_FIRST},\r\n                {R.string.playlist_order_published_date_older_first, MediaItemService.PLAYLIST_ORDER_PUBLISHED_DATE_OLDER_FIRST}\r\n        }) {\r\n            options.add(UiOptionItem.from(context.getString(pair[0]), optionItem -> {\r\n                if (optionItem.isSelected()) {\r\n                    RxHelper.execute(\r\n                            YouTubeMediaItemService.instance().setPlaylistOrderObserve(playlistId, pair[1]),\r\n                            (error) -> MessageHelpers.showMessage(context, R.string.owned_playlist_warning),\r\n                            () -> {\r\n                                generalData.setPlaylistOrder(playlistId, pair[1]);\r\n                                ViewManager.instance(context).refreshCurrentView();\r\n                                if (onClose != null) {\r\n                                    dialogPresenter.closeDialog();\r\n                                    onClose.run();\r\n                                }\r\n                                MessageHelpers.showMessage(context, R.string.msg_done);\r\n                            }\r\n                    );\r\n                }\r\n            }, generalData.getPlaylistOrder(playlistId) == pair[1]));\r\n        }\r\n\r\n        dialogPresenter.appendRadioCategory(context.getString(R.string.playlist_order), options);\r\n\r\n        dialogPresenter.showDialog(context.getString(R.string.playlist_order));\r\n    }\r\n\r\n    public interface OnVideoClick {\r\n        void onClick(Video item);\r\n    }\r\n\r\n    public static void showPlaybackQueueDialog(Context context, OnVideoClick onClick) {\r\n        String playbackQueueCategoryTitle = context.getString(R.string.playback_queue_category_title);\r\n\r\n        AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(context);\r\n\r\n        List<OptionItem> options = new ArrayList<>();\r\n\r\n        Playlist playlist = Playlist.instance();\r\n\r\n        for (Video video : playlist.getAll()) {\r\n            String title = video.getTitle();\r\n            String author = video.getAuthor();\r\n            //options.add(0, UiOptionItem.from( // Add to start (recent videos on top)\r\n            options.add(UiOptionItem.from( // Add to end (like on mobile client)\r\n                    String.format(\"%s - %s\", title != null ? title : \"...\", author != null ? author : \"...\"),\r\n                    optionItem -> {\r\n                        video.fromQueue = true;\r\n                        onClick.onClick(video);\r\n                        settingsPresenter.closeDialog();\r\n                    },\r\n                    video == playlist.getCurrent())\r\n            );\r\n        }\r\n\r\n        settingsPresenter.appendRadioCategory(playbackQueueCategoryTitle, options);\r\n\r\n        settingsPresenter.showDialog(playbackQueueCategoryTitle);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/utils/ClickbaitRemover.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.utils;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\n\r\nimport java.util.regex.Pattern;\r\n\r\npublic class ClickbaitRemover {\r\n    public static final int THUMB_QUALITY_DEFAULT = 0;\r\n    public static final int THUMB_QUALITY_START = 1;\r\n    public static final int THUMB_QUALITY_MIDDLE = 2;\r\n    public static final int THUMB_QUALITY_END = 3;\r\n\r\n    private static final Pattern THUMB_QUALITY_PATTERN = Pattern.compile(\"/(hq1|hq2|hq3|hqdefault|mqdefault|sddefault|hq720)\\\\.\");\r\n\r\n    public static String updateThumbnail(String thumbUrl, int thumbQuality) {\r\n        if (thumbUrl == null || thumbQuality == THUMB_QUALITY_DEFAULT) {\r\n            return thumbUrl;\r\n        }\r\n\r\n        String quality = \"hqdefault\";\r\n\r\n        switch (thumbQuality) {\r\n            case THUMB_QUALITY_START:\r\n                quality = \"hq1\";\r\n                break;\r\n            case THUMB_QUALITY_MIDDLE:\r\n                quality = \"hq2\";\r\n                break;\r\n            case THUMB_QUALITY_END:\r\n                quality = \"hq3\";\r\n                break;\r\n        }\r\n\r\n        return Helpers.replace(thumbUrl, THUMB_QUALITY_PATTERN, \"/\" + quality + \".\");\r\n    }\r\n\r\n    public static String updateThumbnail(Video video, int thumbQuality) {\r\n        if (video == null) {\r\n            return null;\r\n        }\r\n\r\n        if (video.isLive || video.isUpcoming || video.altCardImageUrl != null) { // priority to DeArrow\r\n            return video.getCardImageUrl();\r\n        }\r\n\r\n        return updateThumbnail(video.getCardImageUrl(), thumbQuality);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/utils/CopyOnWriteHashList.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.utils;\r\n\r\nimport java.util.concurrent.CopyOnWriteArrayList;\r\n\r\npublic class CopyOnWriteHashList<T> extends CopyOnWriteArrayList<T> {\r\n    @Override\r\n    public boolean add(T item) {\r\n        int index = size() - 1;\r\n        if (item == null || (index >= 0 && indexOf(item) == index)) {\r\n            return false;\r\n        } else if (contains(item)) {\r\n            remove(item);\r\n        }\r\n\r\n        return super.add(item);\r\n    }\r\n\r\n    @Override\r\n    public void add(int index, T item) {\r\n        if (item == null || (index >= 0 && indexOf(item) == index)) {\r\n            return;\r\n        } else if (contains(item)) {\r\n            remove(item);\r\n        }\r\n\r\n        if (index >= 0 && index < size()) {\r\n            super.add(index, item);\r\n        } else {\r\n            super.add(item);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/utils/HttpURLConnectionUtils.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.utils;\r\n\r\nimport javax.net.ssl.HttpsURLConnection;\r\nimport javax.net.ssl.SSLContext;\r\nimport javax.net.ssl.SSLSocket;\r\nimport javax.net.ssl.SSLSocketFactory;\r\nimport java.io.IOException;\r\nimport java.net.InetAddress;\r\nimport java.net.Socket;\r\nimport java.net.UnknownHostException;\r\nimport java.security.KeyManagementException;\r\nimport java.security.NoSuchAlgorithmException;\r\n\r\npublic class HttpURLConnectionUtils {\r\n    /**\r\n     * https://stackoverflow.com/questions/58456778/how-to-enable-support-tls-1-1-1-2-for-exoplayer\r\n     */\r\n    public static void setupSSL() {\r\n        try {\r\n            //// Google Play will install latest OpenSSL\r\n            //ProviderInstaller.installIfNeeded(getApplicationContext());\r\n            SSLContext sslContext;\r\n            sslContext = SSLContext.getInstance(\"TLSv1.2\");\r\n            sslContext.init(null, null, null);\r\n            sslContext.createSSLEngine();\r\n        } catch (NoSuchAlgorithmException | KeyManagementException e) {\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n\r\n    /**\r\n     * https://stackoverflow.com/questions/58456778/how-to-enable-support-tls-1-1-1-2-for-exoplayer\r\n     */\r\n    public static void setupSSL2() {\r\n        try {\r\n            HttpsURLConnection.setDefaultSSLSocketFactory(new MyCustomSSLSocketFactory());\r\n        } catch (NoSuchAlgorithmException | KeyManagementException e) {\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n\r\n    private static class MyCustomSSLSocketFactory extends SSLSocketFactory {\r\n\r\n        private javax.net.ssl.SSLSocketFactory internalSSLSocketFactory;\r\n\r\n        public MyCustomSSLSocketFactory () throws KeyManagementException, NoSuchAlgorithmException {\r\n            SSLContext context = SSLContext.getInstance(\"TLS\");\r\n            context.init(null, null, null);\r\n            internalSSLSocketFactory = context.getSocketFactory();\r\n        }\r\n\r\n        @Override\r\n        public String[] getDefaultCipherSuites() {\r\n            return internalSSLSocketFactory.getDefaultCipherSuites();\r\n        }\r\n\r\n        @Override\r\n        public String[] getSupportedCipherSuites() {\r\n            return internalSSLSocketFactory.getSupportedCipherSuites();\r\n        }\r\n\r\n        @Override\r\n        public Socket createSocket() throws IOException {\r\n            return enableTLSOnSocket(internalSSLSocketFactory.createSocket());\r\n        }\r\n\r\n        @Override\r\n        public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {\r\n            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));\r\n        }\r\n\r\n        @Override\r\n        public Socket createSocket(String host, int port) throws IOException, UnknownHostException {\r\n            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));\r\n        }\r\n\r\n        @Override\r\n        public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {\r\n            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));\r\n        }\r\n\r\n        @Override\r\n        public Socket createSocket(InetAddress host, int port) throws IOException {\r\n            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));\r\n        }\r\n\r\n        @Override\r\n        public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {\r\n            return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));\r\n        }\r\n\r\n        private Socket enableTLSOnSocket(Socket socket) {\r\n            if(socket != null && (socket instanceof SSLSocket)) {\r\n                ((SSLSocket)socket).setEnabledProtocols(new String[] {\"TLSv1.1\", \"TLSv1.2\"});\r\n            }\r\n            return socket;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/utils/IntentExtractor.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.utils;\r\n\r\nimport android.content.Intent;\r\nimport android.net.Uri;\r\n\r\nimport com.liskovsoft.sharedutils.helpers.GlobalConstants;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.querystringparser.UrlQueryString;\r\nimport com.liskovsoft.sharedutils.querystringparser.UrlQueryStringFactory;\r\n\r\nimport java.util.List;\r\nimport java.util.regex.Matcher;\r\nimport java.util.regex.Pattern;\r\n\r\npublic class IntentExtractor {\r\n    public static final String RESTART_INTENT = \"RESTART_INTENT\";\r\n    public static final String INCOGNITO_INTENT = \"INCOGNITO_INTENT\";\r\n    private static final String TAG = IntentExtractor.class.getSimpleName();\r\n    /**\r\n     * Browser: https://www.youtube.com/results?search_query=twice<br/>\r\n     * Amazon: youtube://search?query=linkin+park&isVoice=true\r\n     */\r\n    private static final String[] SEARCH_KEYS = {\"search_query\", \"query\", \"q\"};\r\n    private static final String VIDEO_ID_KEY = \"v\";\r\n    private static final String VIDEO_TIME_KEY = \"t\";\r\n    private static final String VIDEO_ID_LIST_KEY = \"video_ids\";\r\n    /**\r\n     * https://youtube.com/channel/BLABLA/video\r\n     */\r\n    private static final String CHANNEL_KEY = \"channel\";\r\n    private static final String USER_KEY = \"user\";\r\n    private static final String CHANNEL_ALT_KEY = \"c\";\r\n    private static final String USER_URL = \"/user/\";\r\n    private static final String SUBSCRIPTIONS_URL = \"https://www.youtube.com/tv#/zylon-surface?c=FEsubscriptions\"; // last 'resume' param isn't parsed by intent and should be removed\r\n    private static final String HISTORY_URL = \"https://www.youtube.com/tv#/zylon-surface?c=FEmy_youtube\"; // last 'resume' param isn't parsed by intent and should be removed\r\n    private static final String RECOMMENDED_URL = \"https://www.youtube.com/tv#/zylon-surface?c=default\"; // last 'resume' param isn't parsed by intent and should be removed\r\n    private static final String PLAYLIST_KEY = \"list\";\r\n    private static final String VND_SCHEME = \"vnd.youtube\"; // vnd.youtube://8kKDjRmHp0g\r\n    private static final Pattern timePattern = Pattern.compile(\"(\\\\d+)([A-Za-z]{0,2})\");\r\n    private static final Pattern voiceQueryPattern = Pattern.compile(\":\\\\{\\\"query\\\":\\\"([^\\\"]*)\\\"\");\r\n\r\n    public static String extractVideoId(Intent intent) {\r\n        if (isEmptyIntent(intent)) {\r\n            return null;\r\n        }\r\n\r\n        Uri uri = extractUri(intent);\r\n\r\n        if (extractVoiceQuery(uri) != null) {\r\n            return null;\r\n        }\r\n\r\n        // Don't Uri directly or you might get UnsupportedOperationException on some urls.\r\n        UrlQueryString parser = UrlQueryStringFactory.parse(uri);\r\n        String videoId = parser.get(VIDEO_ID_KEY);\r\n\r\n        if (videoId == null) {\r\n            // https://youtube.com/watch_videos?video_ids=xdq_sYfmN6c,xdq_sYfmN6c\r\n            String idList = parser.get(VIDEO_ID_LIST_KEY);\r\n\r\n            if (idList != null) {\r\n                // temp solution: use one video from the list\r\n                videoId = idList.split(\",\")[0];\r\n            } else if (VND_SCHEME.equals(uri.getScheme())) {\r\n                videoId = uri.getHost();\r\n            } else {\r\n                // Suppose that link type is https://youtu.be/lBeMDqcWTG8\r\n                videoId = uri.getLastPathSegment();\r\n            }\r\n        }\r\n\r\n        if (!isValid(videoId)) {\r\n            return null;\r\n        }\r\n\r\n        return videoId;\r\n    }\r\n\r\n    /**\r\n     * Browser: https://www.youtube.com/results?search_query=twice<br/>\r\n     * Amazon: youtube://search?query=linkin+park&isVoice=true\r\n     */\r\n    public static String extractSearchText(Intent intent) {\r\n        if (isEmptyIntent(intent)) {\r\n            return null;\r\n        }\r\n\r\n        String voiceQuery = extractVoiceQuery(extractUri(intent));\r\n\r\n        if (voiceQuery != null) {\r\n            return voiceQuery;\r\n        }\r\n\r\n        // Don't Uri directly or you might get UnsupportedOperationException on some urls.\r\n        UrlQueryString parser;\r\n        try {\r\n            parser = UrlQueryStringFactory.parse(extractUri(intent));\r\n        } catch (IllegalArgumentException e) {\r\n            // URLDecoder: Illegal hex characters in escape (%) pattern : % T\r\n            e.printStackTrace();\r\n            return null;\r\n        }\r\n\r\n        for (String searchKey : SEARCH_KEYS) {\r\n            String searchText = parser.get(searchKey);\r\n\r\n            if (searchText != null) {\r\n                return searchText;\r\n            }\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    /**\r\n     * Data: https://www.youtube.com/channel/UCtDjOV5nk982w35AIdVDuNw\r\n     */\r\n    public static String extractChannelId(Intent intent) {\r\n        if (isEmptyIntent(intent) || isATVChannelUrl(intent)) {\r\n            return null;\r\n        }\r\n\r\n        // https://youtube.com/channel/BLABLA/video\r\n        // Don't Uri directly or you might get UnsupportedOperationException on some urls.\r\n        Uri url = extractUri(intent);\r\n        UrlQueryString parser = UrlQueryStringFactory.parse(url);\r\n\r\n        // https://youtube.com/channel/UCIy_mMwdwbC6GkRSm6gqo6Q\r\n        String channelId = parser.get(CHANNEL_KEY);\r\n\r\n        if (channelId == null) {\r\n            // https://www.youtube.com/c/IngaMezerya\r\n            channelId = parser.get(CHANNEL_ALT_KEY);\r\n\r\n            if (channelId == null) {\r\n                // https://youtube.com/user/RVLucius\r\n                channelId = parser.get(USER_KEY);\r\n            }\r\n\r\n            if (channelId != null) {\r\n                channelId = \"@\" + channelId; // add the prefix to quickly distinguish later\r\n            }\r\n        }\r\n\r\n        if (channelId == null) {\r\n            // https://www.youtube.com/@IngaMezerya or https://youtu.be/builditbasement\r\n            String lastPathSegment = url.getLastPathSegment();\r\n\r\n            // NOTE: can't distinguish a share video link (https://youtu.be/LpNVf8sczqU) from a non-prefix channel link (https://youtu.be/builditbasement)\r\n\r\n            if (Helpers.startsWith(lastPathSegment, \"@\")) {\r\n                channelId = lastPathSegment;\r\n            }\r\n        }\r\n\r\n        return channelId;\r\n    }\r\n\r\n    /**\r\n     * Data: https://www.youtube.com/playlist?list=RDCLAK5uy_mk6AmqcHgCRhyJuYsQz5CCVdCF4SRGivs\r\n     */\r\n    public static String extractPlaylistId(Intent intent) {\r\n        if (isEmptyIntent(intent)) {\r\n            return null;\r\n        }\r\n\r\n        UrlQueryString parser = UrlQueryStringFactory.parse(extractUri(intent));\r\n\r\n        return parser.get(PLAYLIST_KEY);\r\n    }\r\n\r\n    private static boolean isValid(String videoId) {\r\n        return videoId != null && videoId.length() == 11;\r\n    }\r\n\r\n    public static boolean hasData(Intent intent) {\r\n        return intent != null && extractUri(intent) != null;\r\n    }\r\n\r\n    /**\r\n     * ATV: Channel icon url\r\n     */\r\n    public static boolean isATVChannelUrl(Intent intent) {\r\n        return intent != null\r\n                && extractUri(intent) != null\r\n                && Helpers.startsWithAny(extractUri(intent).toString(), SUBSCRIPTIONS_URL, HISTORY_URL, RECOMMENDED_URL);\r\n    }\r\n\r\n    /**\r\n     * ATV: Subscriptions icon url\r\n     */\r\n    public static boolean isSubscriptionsUrl(Intent intent) {\r\n        return intent != null\r\n                && extractUri(intent) != null\r\n                && Helpers.startsWith(extractUri(intent).toString(), SUBSCRIPTIONS_URL);\r\n    }\r\n\r\n    /**\r\n     * ATV: History icon url\r\n     */\r\n    public static boolean isHistoryUrl(Intent intent) {\r\n        return intent != null\r\n                && extractUri(intent) != null\r\n                && Helpers.startsWith(extractUri(intent).toString(), HISTORY_URL);\r\n    }\r\n\r\n    /**\r\n     * ATV: Recommended icon url\r\n     */\r\n    public static boolean isRecommendedUrl(Intent intent) {\r\n        return intent != null\r\n                && extractUri(intent) != null\r\n                && Helpers.startsWith(extractUri(intent).toString(), RECOMMENDED_URL);\r\n    }\r\n\r\n    public static boolean isRootUrl(Intent intent) {\r\n        return intent != null\r\n                && extractUri(intent) != null\r\n                && Helpers.endsWithAny(extractUri(intent).toString(), \".com/\", \".com\", \".com/launch=remote\");\r\n    }\r\n\r\n    public static boolean isStartVoiceCommand(Intent intent) {\r\n        return intent != null && extractUri(intent) != null && extractUri(intent).toString().contains(\"launch=voice\");\r\n    }\r\n\r\n    /**\r\n     * Detect Amazon Alexa play command\r\n     */\r\n    public static boolean isInstantPlayCommand(Intent intent) {\r\n        return intent != null && extractUri(intent) != null && extractUri(intent).toString().contains(\"method=play\");\r\n    }\r\n\r\n    public static boolean hasFinishOnEndedFlag(Intent intent) {\r\n        return intent != null && (intent.getBooleanExtra(\"finish_on_ended\", false)\r\n                || \"true\".equals(intent.getStringExtra(\"finish_on_ended\")));\r\n    }\r\n\r\n    public static long extractVideoTimeMs(Intent intent) {\r\n        if (isEmptyIntent(intent)) {\r\n            return -1;\r\n        }\r\n\r\n        UrlQueryString parser = UrlQueryStringFactory.parse(extractUri(intent));\r\n        String time = parser.get(VIDEO_TIME_KEY);\r\n\r\n        List<String> matches = Helpers.findAll(time, timePattern);\r\n\r\n        if (matches == null) {\r\n            return -1;\r\n        }\r\n\r\n        long result = 0;\r\n\r\n        for (String match : matches) {\r\n            long parsed = parseTimeStr(match);\r\n            if (parsed == -1) {\r\n                return -1;\r\n            }\r\n            result += parsed;\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    public static String extractAccountName(Intent intent) {\r\n        if (intent == null) {\r\n            return null;\r\n        }\r\n\r\n        return intent.getStringExtra(\"account_name\");\r\n    }\r\n\r\n    /**\r\n     * Example: https://www.youtube.com/tv?voice={\"youtubeAssistantRequest\":{\"query\":\"Russian YouTube\",\"queryIntent\":\"CgxTZWFyY2hJbnRlbnQSFAoFcXVlcnkSCxoJCgdSdXNzaWFuEiYKCGRvY190eXBlEhoaGAoWWU9VVFVCRV9ET0NfVFlQRV9WSURFTw==\",\"youtubeAssistantParams\":{\"personalDataParams\":{\"showPersonalData\":false}},\"enablePrefetchLogging\":true},\"updateYoutubeSettings\":{\"enableSafetyMode\":false,\"enablePersonalResults\":false},\"hasEntityBar\":false}&command_id=CWGIYL6nN8Gi3AP_5Y6wAQ&launch=voice&vq=Russian%20YouTube\r\n     */\r\n    private static String extractVoiceQuery(Uri data) {\r\n        return Helpers.runMultiMatcher(data.toString(), voiceQueryPattern);\r\n    }\r\n\r\n    private static boolean isEmptyIntent(Intent intent) {\r\n        return intent == null || (!Intent.ACTION_VIEW.equals(intent.getAction()) && !Intent.ACTION_SEND.equals(intent.getAction())) || extractUri(intent) == null;\r\n    }\r\n\r\n    private static Uri extractUri(Intent intent) {\r\n        if (intent == null) {\r\n            return null;\r\n        }\r\n\r\n        return intent.getData() != null ? intent.getData() :\r\n                intent.getStringExtra(Intent.EXTRA_TEXT) != null ?\r\n                        Uri.parse(intent.getStringExtra(Intent.EXTRA_TEXT)) : null;\r\n    }\r\n\r\n    private static long parseTimeStr(String time) {\r\n        if (time == null) {\r\n            return -1;\r\n        }\r\n\r\n        Matcher matcher = timePattern.matcher(time);\r\n\r\n        if (!matcher.matches()) {\r\n            return -1;\r\n        }\r\n\r\n        String strValue = matcher.group(1);\r\n        String unit = matcher.group(2);\r\n\r\n        long multiplier = 1;\r\n\r\n        if (unit != null && !unit.isEmpty()) {\r\n            switch (unit.toLowerCase()) {\r\n                case \"s\":\r\n                    multiplier = 1000;\r\n                    break;\r\n                case \"m\":\r\n                    multiplier = 60 * 1000;\r\n                    break;\r\n                case \"h\":\r\n                    multiplier = 60 * 60 * 1000;\r\n                    break;\r\n                default:\r\n                    return -1;\r\n            }\r\n        } else {\r\n            // Assume seconds if no unit is present\r\n            multiplier = 1000;\r\n        }\r\n\r\n        try {\r\n            return multiplier * Long.parseLong(strValue);\r\n        } catch (NumberFormatException e) {\r\n            return -1;\r\n        }\r\n    }\r\n\r\n    public static boolean isRestartIntent(Intent intent) {\r\n        return intent != null && intent.getBooleanExtra(RESTART_INTENT, false);\r\n    }\r\n\r\n    public static boolean isIncognitoIntent(Intent intent) {\r\n        return intent != null && intent.getBooleanExtra(INCOGNITO_INTENT, false);\r\n    }\r\n\r\n    public static boolean isATVIntent(Intent intent) {\r\n        return intent != null && intent.getBooleanExtra(GlobalConstants.ATV_INTENT, false);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/utils/LoadingManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.utils;\r\n\r\nimport android.content.Context;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.BrowsePresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.ChannelPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.ChannelUploadsPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.SearchPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.BrowseView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ChannelUploadsView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ChannelView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.PlaybackView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.SearchView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ViewManager;\r\n\r\npublic class LoadingManager {\r\n    public static void showLoading(Context context, boolean show, Class<?> view) {\r\n        Class<?> topView = ViewManager.instance(context).getTopView();\r\n        if (topView == view) {\r\n            showLoading(context, show);\r\n        }\r\n    }\r\n\r\n    public static void showLoading(Context context, boolean show) {\r\n        Class<?> topView = ViewManager.instance(context).getTopView();\r\n\r\n        if (topView == BrowseView.class) {\r\n            BrowseView browseView = BrowsePresenter.instance(context).getView();\r\n            if (browseView != null) {\r\n                browseView.showProgressBar(show);\r\n            }\r\n        } else if (topView == SearchView.class) {\r\n            SearchView searchView = SearchPresenter.instance(context).getView();\r\n            if (searchView != null) {\r\n                searchView.showProgressBar(show);\r\n            }\r\n        } else if (topView == ChannelView.class) {\r\n            ChannelView channelView = ChannelPresenter.instance(context).getView();\r\n            if (channelView != null) {\r\n                channelView.showProgressBar(show);\r\n            }\r\n        } else if (topView == ChannelUploadsView.class) {\r\n            ChannelUploadsView uploadsView = ChannelUploadsPresenter.instance(context).getView();\r\n            if (uploadsView != null) {\r\n                uploadsView.showProgressBar(show);\r\n            }\r\n        } else if (topView == PlaybackView.class) {\r\n            PlaybackView playbackView = PlaybackPresenter.instance(context).getView();\r\n            if (playbackView != null) {\r\n                playbackView.showProgressBar(show);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/utils/SimpleEditDialog.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.utils;\r\n\r\nimport android.content.Context;\r\nimport android.text.InputType;\r\nimport android.view.LayoutInflater;\r\nimport android.view.View;\r\nimport android.view.inputmethod.EditorInfo;\r\nimport android.widget.EditText;\r\nimport androidx.appcompat.app.AlertDialog;\r\nimport com.liskovsoft.sharedutils.helpers.KeyHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\n\r\npublic class SimpleEditDialog {\r\n    public interface OnChange {\r\n        boolean onChange(String newValue);\r\n    }\r\n\r\n    public static void show(Context context, String dialogTitle, String defaultValue, OnChange onChange) {\r\n        show(context, dialogTitle, dialogTitle, defaultValue, onChange, null);\r\n    }\r\n\r\n    public static void show(Context context, String dialogTitle, String dialogHint, String defaultValue, OnChange onChange) {\r\n        show(context, dialogTitle, dialogHint, defaultValue, onChange, null);\r\n    }\r\n\r\n    public static void show(Context context, String dialogTitle, String dialogHint, String defaultValue, OnChange onChange, Runnable onDismiss) {\r\n        show(context, dialogTitle, dialogHint, defaultValue, onChange, onDismiss, false);\r\n    }\r\n\r\n    public static void showPassword(Context context, String dialogTitle, String defaultValue, OnChange onChange) {\r\n        showPassword(context, dialogTitle, defaultValue, onChange, null);\r\n    }\r\n\r\n    public static void showPassword(Context context, String dialogTitle, String defaultValue, OnChange onChange, Runnable onDismiss) {\r\n        show(context, dialogTitle, dialogTitle, defaultValue, onChange, onDismiss, true);\r\n    }\r\n\r\n    private static void show(Context context, String dialogTitle, String dialogHint, String defaultValue, OnChange onChange, Runnable onDismiss, boolean isPassword) {\r\n        AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.AppDialog);\r\n        LayoutInflater inflater = LayoutInflater.from(context);\r\n        View contentView = inflater.inflate(R.layout.simple_edit_dialog, null);\r\n\r\n        EditText editField = contentView.findViewById(R.id.simple_edit_value);\r\n        if (isPassword) {\r\n            editField.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);\r\n        }\r\n        KeyHelpers.fixShowKeyboard(editField);\r\n\r\n        editField.setText(defaultValue);\r\n        editField.setHint(dialogHint);\r\n        editField.setNextFocusDownId(android.R.id.button1); // OK button\r\n\r\n        if (defaultValue != null) { // move cursor to the end\r\n            editField.setSelection(defaultValue.length());\r\n        }\r\n\r\n        // keep empty, will override below.\r\n        // https://stackoverflow.com/a/15619098/5379584\r\n        AlertDialog configDialog = builder\r\n                .setTitle(dialogTitle)\r\n                .setView(contentView)\r\n                .setPositiveButton(android.R.string.ok, (dialog, which) -> { })\r\n                .setNegativeButton(android.R.string.cancel, (dialog, which) -> { })\r\n                .create();\r\n\r\n        if (onDismiss != null) {\r\n            configDialog.setOnDismissListener(dialog -> onDismiss.run());\r\n        }\r\n\r\n        editField.setOnEditorActionListener((v, actionId, event) -> {\r\n            switch (actionId) {\r\n                case EditorInfo.IME_ACTION_NEXT:\r\n                    configDialog.getButton(AlertDialog.BUTTON_POSITIVE).requestFocus();\r\n                    return true;\r\n                case EditorInfo.IME_ACTION_DONE:\r\n                    configDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick();\r\n                    return true;\r\n            }\r\n            return false;\r\n        });\r\n\r\n        try {\r\n            configDialog.show();\r\n        } catch (RuntimeException e) {\r\n            // BadTokenException: Unable to add window -- token null is not for an application\r\n            // RuntimeException: InputChannel is not initialized\r\n            e.printStackTrace();\r\n            MessageHelpers.showMessage(context, e.getMessage());\r\n            return;\r\n        }\r\n\r\n        configDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener((view) -> {\r\n            String newValue = editField.getText().toString();\r\n\r\n            if (newValue.isEmpty()) {\r\n                // Empty fields not allowed\r\n                editField.setHint(R.string.enter_value);\r\n                return;\r\n            }\r\n\r\n            boolean dismiss = onChange.onChange(newValue);\r\n\r\n            if (dismiss) {\r\n                configDialog.dismiss();\r\n            }\r\n        });\r\n\r\n        configDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener((view) -> configDialog.dismiss());\r\n\r\n        //editField.setNextFocusDownId(configDialog.getButton(AlertDialog.BUTTON_POSITIVE).getId()); // OK button\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/utils/TvQuickActions.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.utils;\r\n\r\nimport android.content.Context;\r\nimport android.content.Intent;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem;\r\n\r\npublic class TvQuickActions {\r\n    private final static String PACKAGE = \"dev.vodik7.tvquickactions\";\r\n\r\n    public static void sendStopAFR(Context context) {\r\n        if (context == null) {\r\n            return;\r\n        }\r\n\r\n        Intent intent = new Intent();\r\n        intent.setPackage(PACKAGE);\r\n        intent.setAction(PACKAGE + \".STOP_AFR\");\r\n        context.sendBroadcast(intent);\r\n    }\r\n\r\n    public static void sendStartAFR(Context context, FormatItem videoFormat) {\r\n        if (videoFormat != null) {\r\n            Intent intent = new Intent();\r\n            intent.setPackage(PACKAGE);\r\n            intent.setAction(PACKAGE + \".START_AFR\");\r\n            intent.putExtra(\"fps\", videoFormat.getFrameRate());\r\n            intent.putExtra(\"height\", videoFormat.getHeight());\r\n            context.sendBroadcast(intent);\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/utils/UserAgentManager.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.utils;\r\n\r\nimport java.util.Random;\r\n\r\npublic class UserAgentManager {\r\n    private static final String USER_AGENT_TMPL = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36\";\r\n    private final String[] CHROME_VERSIONS = {\r\n                \"74.0.3729.129\",\r\n                \"76.0.3780.3\",\r\n                \"76.0.3780.2\",\r\n                \"74.0.3729.128\",\r\n                \"76.0.3780.1\",\r\n                \"76.0.3780.0\",\r\n                \"75.0.3770.15\",\r\n                \"74.0.3729.127\",\r\n                \"74.0.3729.126\",\r\n                \"76.0.3779.1\",\r\n                \"76.0.3779.0\",\r\n                \"75.0.3770.14\",\r\n                \"74.0.3729.125\",\r\n                \"76.0.3778.1\",\r\n                \"76.0.3778.0\",\r\n                \"75.0.3770.13\",\r\n                \"74.0.3729.124\",\r\n                \"74.0.3729.123\",\r\n                \"73.0.3683.121\",\r\n                \"76.0.3777.1\",\r\n                \"76.0.3777.0\",\r\n                \"75.0.3770.12\",\r\n                \"74.0.3729.122\",\r\n                \"76.0.3776.4\",\r\n                \"75.0.3770.11\",\r\n                \"74.0.3729.121\",\r\n                \"76.0.3776.3\",\r\n                \"76.0.3776.2\",\r\n                \"73.0.3683.120\",\r\n                \"74.0.3729.120\",\r\n                \"74.0.3729.119\",\r\n                \"74.0.3729.118\",\r\n                \"76.0.3776.1\",\r\n                \"76.0.3776.0\",\r\n                \"76.0.3775.5\",\r\n                \"75.0.3770.10\",\r\n                \"74.0.3729.117\",\r\n                \"76.0.3775.4\",\r\n                \"76.0.3775.3\",\r\n                \"74.0.3729.116\",\r\n                \"75.0.3770.9\",\r\n                \"76.0.3775.2\",\r\n                \"76.0.3775.1\",\r\n                \"76.0.3775.0\",\r\n                \"75.0.3770.8\",\r\n                \"74.0.3729.115\",\r\n                \"74.0.3729.114\",\r\n                \"76.0.3774.1\",\r\n                \"76.0.3774.0\",\r\n                \"75.0.3770.7\",\r\n                \"74.0.3729.113\",\r\n                \"74.0.3729.112\",\r\n                \"74.0.3729.111\",\r\n                \"76.0.3773.1\",\r\n                \"76.0.3773.0\",\r\n                \"75.0.3770.6\",\r\n                \"74.0.3729.110\",\r\n                \"74.0.3729.109\",\r\n                \"76.0.3772.1\",\r\n                \"76.0.3772.0\",\r\n                \"75.0.3770.5\",\r\n                \"74.0.3729.108\",\r\n                \"74.0.3729.107\",\r\n                \"76.0.3771.1\",\r\n                \"76.0.3771.0\",\r\n                \"75.0.3770.4\",\r\n                \"74.0.3729.106\",\r\n                \"74.0.3729.105\",\r\n                \"75.0.3770.3\",\r\n                \"74.0.3729.104\",\r\n                \"74.0.3729.103\",\r\n                \"74.0.3729.102\",\r\n                \"75.0.3770.2\",\r\n                \"74.0.3729.101\",\r\n                \"75.0.3770.1\",\r\n                \"75.0.3770.0\",\r\n                \"74.0.3729.100\",\r\n                \"75.0.3769.5\",\r\n                \"75.0.3769.4\",\r\n                \"74.0.3729.99\",\r\n                \"75.0.3769.3\",\r\n                \"75.0.3769.2\",\r\n                \"75.0.3768.6\",\r\n                \"74.0.3729.98\",\r\n                \"75.0.3769.1\",\r\n                \"75.0.3769.0\",\r\n                \"74.0.3729.97\",\r\n                \"73.0.3683.119\",\r\n                \"73.0.3683.118\",\r\n                \"74.0.3729.96\",\r\n                \"75.0.3768.5\",\r\n                \"75.0.3768.4\",\r\n                \"75.0.3768.3\",\r\n                \"75.0.3768.2\",\r\n                \"74.0.3729.95\",\r\n                \"74.0.3729.94\",\r\n                \"75.0.3768.1\",\r\n                \"75.0.3768.0\",\r\n                \"74.0.3729.93\",\r\n                \"74.0.3729.92\",\r\n                \"73.0.3683.117\",\r\n                \"74.0.3729.91\",\r\n                \"75.0.3766.3\",\r\n                \"74.0.3729.90\",\r\n                \"75.0.3767.2\",\r\n                \"75.0.3767.1\",\r\n                \"75.0.3767.0\",\r\n                \"74.0.3729.89\",\r\n                \"73.0.3683.116\",\r\n                \"75.0.3766.2\",\r\n                \"74.0.3729.88\",\r\n                \"75.0.3766.1\",\r\n                \"75.0.3766.0\",\r\n                \"74.0.3729.87\",\r\n                \"73.0.3683.115\",\r\n                \"74.0.3729.86\",\r\n                \"75.0.3765.1\",\r\n                \"75.0.3765.0\",\r\n                \"74.0.3729.85\",\r\n                \"73.0.3683.114\",\r\n                \"74.0.3729.84\",\r\n                \"75.0.3764.1\",\r\n                \"75.0.3764.0\",\r\n                \"74.0.3729.83\",\r\n                \"73.0.3683.113\",\r\n                \"75.0.3763.2\",\r\n                \"75.0.3761.4\",\r\n                \"74.0.3729.82\",\r\n                \"75.0.3763.1\",\r\n                \"75.0.3763.0\",\r\n                \"74.0.3729.81\",\r\n                \"73.0.3683.112\",\r\n                \"75.0.3762.1\",\r\n                \"75.0.3762.0\",\r\n                \"74.0.3729.80\",\r\n                \"75.0.3761.3\",\r\n                \"74.0.3729.79\",\r\n                \"73.0.3683.111\",\r\n                \"75.0.3761.2\",\r\n                \"74.0.3729.78\",\r\n                \"74.0.3729.77\",\r\n                \"75.0.3761.1\",\r\n                \"75.0.3761.0\",\r\n                \"73.0.3683.110\",\r\n                \"74.0.3729.76\",\r\n                \"74.0.3729.75\",\r\n                \"75.0.3760.0\",\r\n                \"74.0.3729.74\",\r\n                \"75.0.3759.8\",\r\n                \"75.0.3759.7\",\r\n                \"75.0.3759.6\",\r\n                \"74.0.3729.73\",\r\n                \"75.0.3759.5\",\r\n                \"74.0.3729.72\",\r\n                \"73.0.3683.109\",\r\n                \"75.0.3759.4\",\r\n                \"75.0.3759.3\",\r\n                \"74.0.3729.71\",\r\n                \"75.0.3759.2\",\r\n                \"74.0.3729.70\",\r\n                \"73.0.3683.108\",\r\n                \"74.0.3729.69\",\r\n                \"75.0.3759.1\",\r\n                \"75.0.3759.0\",\r\n                \"74.0.3729.68\",\r\n                \"73.0.3683.107\",\r\n                \"74.0.3729.67\",\r\n                \"75.0.3758.1\",\r\n                \"75.0.3758.0\",\r\n                \"74.0.3729.66\",\r\n                \"73.0.3683.106\",\r\n                \"74.0.3729.65\",\r\n                \"75.0.3757.1\",\r\n                \"75.0.3757.0\",\r\n                \"74.0.3729.64\",\r\n                \"73.0.3683.105\",\r\n                \"74.0.3729.63\",\r\n                \"75.0.3756.1\",\r\n                \"75.0.3756.0\",\r\n                \"74.0.3729.62\",\r\n                \"73.0.3683.104\",\r\n                \"75.0.3755.3\",\r\n                \"75.0.3755.2\",\r\n                \"73.0.3683.103\",\r\n                \"75.0.3755.1\",\r\n                \"75.0.3755.0\",\r\n                \"74.0.3729.61\",\r\n                \"73.0.3683.102\",\r\n                \"74.0.3729.60\",\r\n                \"75.0.3754.2\",\r\n                \"74.0.3729.59\",\r\n                \"75.0.3753.4\",\r\n                \"74.0.3729.58\",\r\n                \"75.0.3754.1\",\r\n                \"75.0.3754.0\",\r\n                \"74.0.3729.57\",\r\n                \"73.0.3683.101\",\r\n                \"75.0.3753.3\",\r\n                \"75.0.3752.2\",\r\n                \"75.0.3753.2\",\r\n                \"74.0.3729.56\",\r\n                \"75.0.3753.1\",\r\n                \"75.0.3753.0\",\r\n                \"74.0.3729.55\",\r\n                \"73.0.3683.100\",\r\n                \"74.0.3729.54\",\r\n                \"75.0.3752.1\",\r\n                \"75.0.3752.0\",\r\n                \"74.0.3729.53\",\r\n                \"73.0.3683.99\",\r\n                \"74.0.3729.52\",\r\n                \"75.0.3751.1\",\r\n                \"75.0.3751.0\",\r\n                \"74.0.3729.51\",\r\n                \"73.0.3683.98\",\r\n                \"74.0.3729.50\",\r\n                \"75.0.3750.0\",\r\n                \"74.0.3729.49\",\r\n                \"74.0.3729.48\",\r\n                \"74.0.3729.47\",\r\n                \"75.0.3749.3\",\r\n                \"74.0.3729.46\",\r\n                \"73.0.3683.97\",\r\n                \"75.0.3749.2\",\r\n                \"74.0.3729.45\",\r\n                \"75.0.3749.1\",\r\n                \"75.0.3749.0\",\r\n                \"74.0.3729.44\",\r\n                \"73.0.3683.96\",\r\n                \"74.0.3729.43\",\r\n                \"74.0.3729.42\",\r\n                \"75.0.3748.1\",\r\n                \"75.0.3748.0\",\r\n                \"74.0.3729.41\",\r\n                \"75.0.3747.1\",\r\n                \"73.0.3683.95\",\r\n                \"75.0.3746.4\",\r\n                \"74.0.3729.40\",\r\n                \"74.0.3729.39\",\r\n                \"75.0.3747.0\",\r\n                \"75.0.3746.3\",\r\n                \"75.0.3746.2\",\r\n                \"74.0.3729.38\",\r\n                \"75.0.3746.1\",\r\n                \"75.0.3746.0\",\r\n                \"74.0.3729.37\",\r\n                \"73.0.3683.94\",\r\n                \"75.0.3745.5\",\r\n                \"75.0.3745.4\",\r\n                \"75.0.3745.3\",\r\n                \"75.0.3745.2\",\r\n                \"74.0.3729.36\",\r\n                \"75.0.3745.1\",\r\n                \"75.0.3745.0\",\r\n                \"75.0.3744.2\",\r\n                \"74.0.3729.35\",\r\n                \"73.0.3683.93\",\r\n                \"74.0.3729.34\",\r\n                \"75.0.3744.1\",\r\n                \"75.0.3744.0\",\r\n                \"74.0.3729.33\",\r\n                \"73.0.3683.92\",\r\n                \"74.0.3729.32\",\r\n                \"74.0.3729.31\",\r\n                \"73.0.3683.91\",\r\n                \"75.0.3741.2\",\r\n                \"75.0.3740.5\",\r\n                \"74.0.3729.30\",\r\n                \"75.0.3741.1\",\r\n                \"75.0.3741.0\",\r\n                \"74.0.3729.29\",\r\n                \"75.0.3740.4\",\r\n                \"73.0.3683.90\",\r\n                \"74.0.3729.28\",\r\n                \"75.0.3740.3\",\r\n                \"73.0.3683.89\",\r\n                \"75.0.3740.2\",\r\n                \"74.0.3729.27\",\r\n                \"75.0.3740.1\",\r\n                \"75.0.3740.0\",\r\n                \"74.0.3729.26\",\r\n                \"73.0.3683.88\",\r\n                \"73.0.3683.87\",\r\n                \"74.0.3729.25\",\r\n                \"75.0.3739.1\",\r\n                \"75.0.3739.0\",\r\n                \"73.0.3683.86\",\r\n                \"74.0.3729.24\",\r\n                \"73.0.3683.85\",\r\n                \"75.0.3738.4\",\r\n                \"75.0.3738.3\",\r\n                \"75.0.3738.2\",\r\n                \"75.0.3738.1\",\r\n                \"75.0.3738.0\",\r\n                \"74.0.3729.23\",\r\n                \"73.0.3683.84\",\r\n                \"74.0.3729.22\",\r\n                \"74.0.3729.21\",\r\n                \"75.0.3737.1\",\r\n                \"75.0.3737.0\",\r\n                \"74.0.3729.20\",\r\n                \"73.0.3683.83\",\r\n                \"74.0.3729.19\",\r\n                \"75.0.3736.1\",\r\n                \"75.0.3736.0\",\r\n                \"74.0.3729.18\",\r\n                \"73.0.3683.82\",\r\n                \"74.0.3729.17\",\r\n                \"75.0.3735.1\",\r\n                \"75.0.3735.0\",\r\n                \"74.0.3729.16\",\r\n                \"73.0.3683.81\",\r\n                \"75.0.3734.1\",\r\n                \"75.0.3734.0\",\r\n                \"74.0.3729.15\",\r\n                \"73.0.3683.80\",\r\n                \"74.0.3729.14\",\r\n                \"75.0.3733.1\",\r\n                \"75.0.3733.0\",\r\n                \"75.0.3732.1\",\r\n                \"74.0.3729.13\",\r\n                \"74.0.3729.12\",\r\n                \"73.0.3683.79\",\r\n                \"74.0.3729.11\",\r\n                \"75.0.3732.0\",\r\n                \"74.0.3729.10\",\r\n                \"73.0.3683.78\",\r\n                \"74.0.3729.9\",\r\n                \"74.0.3729.8\",\r\n                \"74.0.3729.7\",\r\n                \"75.0.3731.3\",\r\n                \"75.0.3731.2\",\r\n                \"75.0.3731.0\",\r\n                \"74.0.3729.6\",\r\n                \"73.0.3683.77\",\r\n                \"73.0.3683.76\",\r\n                \"75.0.3730.5\",\r\n                \"75.0.3730.4\",\r\n                \"73.0.3683.75\",\r\n                \"74.0.3729.5\",\r\n                \"73.0.3683.74\",\r\n                \"75.0.3730.3\",\r\n                \"75.0.3730.2\",\r\n                \"74.0.3729.4\",\r\n                \"73.0.3683.73\",\r\n                \"73.0.3683.72\",\r\n                \"75.0.3730.1\",\r\n                \"75.0.3730.0\",\r\n                \"74.0.3729.3\",\r\n                \"73.0.3683.71\",\r\n                \"74.0.3729.2\",\r\n                \"73.0.3683.70\",\r\n                \"74.0.3729.1\",\r\n                \"74.0.3729.0\",\r\n                \"74.0.3726.4\",\r\n                \"73.0.3683.69\",\r\n                \"74.0.3726.3\",\r\n                \"74.0.3728.0\",\r\n                \"74.0.3726.2\",\r\n                \"73.0.3683.68\",\r\n                \"74.0.3726.1\",\r\n                \"74.0.3726.0\",\r\n                \"74.0.3725.4\",\r\n                \"73.0.3683.67\",\r\n                \"73.0.3683.66\",\r\n                \"74.0.3725.3\",\r\n                \"74.0.3725.2\",\r\n                \"74.0.3725.1\",\r\n                \"74.0.3724.8\",\r\n                \"74.0.3725.0\",\r\n                \"73.0.3683.65\",\r\n                \"74.0.3724.7\",\r\n                \"74.0.3724.6\",\r\n                \"74.0.3724.5\",\r\n                \"74.0.3724.4\",\r\n                \"74.0.3724.3\",\r\n                \"74.0.3724.2\",\r\n                \"74.0.3724.1\",\r\n                \"74.0.3724.0\",\r\n                \"73.0.3683.64\",\r\n                \"74.0.3723.1\",\r\n                \"74.0.3723.0\",\r\n                \"73.0.3683.63\",\r\n                \"74.0.3722.1\",\r\n                \"74.0.3722.0\",\r\n                \"73.0.3683.62\",\r\n                \"74.0.3718.9\",\r\n                \"74.0.3702.3\",\r\n                \"74.0.3721.3\",\r\n                \"74.0.3721.2\",\r\n                \"74.0.3721.1\",\r\n                \"74.0.3721.0\",\r\n                \"74.0.3720.6\",\r\n                \"73.0.3683.61\",\r\n                \"72.0.3626.122\",\r\n                \"73.0.3683.60\",\r\n                \"74.0.3720.5\",\r\n                \"72.0.3626.121\",\r\n                \"74.0.3718.8\",\r\n                \"74.0.3720.4\",\r\n                \"74.0.3720.3\",\r\n                \"74.0.3718.7\",\r\n                \"74.0.3720.2\",\r\n                \"74.0.3720.1\",\r\n                \"74.0.3720.0\",\r\n                \"74.0.3718.6\",\r\n                \"74.0.3719.5\",\r\n                \"73.0.3683.59\",\r\n                \"74.0.3718.5\",\r\n                \"74.0.3718.4\",\r\n                \"74.0.3719.4\",\r\n                \"74.0.3719.3\",\r\n                \"74.0.3719.2\",\r\n                \"74.0.3719.1\",\r\n                \"73.0.3683.58\",\r\n                \"74.0.3719.0\",\r\n                \"73.0.3683.57\",\r\n                \"73.0.3683.56\",\r\n                \"74.0.3718.3\",\r\n                \"73.0.3683.55\",\r\n                \"74.0.3718.2\",\r\n                \"74.0.3718.1\",\r\n                \"74.0.3718.0\",\r\n                \"73.0.3683.54\",\r\n                \"74.0.3717.2\",\r\n                \"73.0.3683.53\",\r\n                \"74.0.3717.1\",\r\n                \"74.0.3717.0\",\r\n                \"73.0.3683.52\",\r\n                \"74.0.3716.1\",\r\n                \"74.0.3716.0\",\r\n                \"73.0.3683.51\",\r\n                \"74.0.3715.1\",\r\n                \"74.0.3715.0\",\r\n                \"73.0.3683.50\",\r\n                \"74.0.3711.2\",\r\n                \"74.0.3714.2\",\r\n                \"74.0.3713.3\",\r\n                \"74.0.3714.1\",\r\n                \"74.0.3714.0\",\r\n                \"73.0.3683.49\",\r\n                \"74.0.3713.1\",\r\n                \"74.0.3713.0\",\r\n                \"72.0.3626.120\",\r\n                \"73.0.3683.48\",\r\n                \"74.0.3712.2\",\r\n                \"74.0.3712.1\",\r\n                \"74.0.3712.0\",\r\n                \"73.0.3683.47\",\r\n                \"72.0.3626.119\",\r\n                \"73.0.3683.46\",\r\n                \"74.0.3710.2\",\r\n                \"72.0.3626.118\",\r\n                \"74.0.3711.1\",\r\n                \"74.0.3711.0\",\r\n                \"73.0.3683.45\",\r\n                \"72.0.3626.117\",\r\n                \"74.0.3710.1\",\r\n                \"74.0.3710.0\",\r\n                \"73.0.3683.44\",\r\n                \"72.0.3626.116\",\r\n                \"74.0.3709.1\",\r\n                \"74.0.3709.0\",\r\n                \"74.0.3704.9\",\r\n                \"73.0.3683.43\",\r\n                \"72.0.3626.115\",\r\n                \"74.0.3704.8\",\r\n                \"74.0.3704.7\",\r\n                \"74.0.3708.0\",\r\n                \"74.0.3706.7\",\r\n                \"74.0.3704.6\",\r\n                \"73.0.3683.42\",\r\n                \"72.0.3626.114\",\r\n                \"74.0.3706.6\",\r\n                \"72.0.3626.113\",\r\n                \"74.0.3704.5\",\r\n                \"74.0.3706.5\",\r\n                \"74.0.3706.4\",\r\n                \"74.0.3706.3\",\r\n                \"74.0.3706.2\",\r\n                \"74.0.3706.1\",\r\n                \"74.0.3706.0\",\r\n                \"73.0.3683.41\",\r\n                \"72.0.3626.112\",\r\n                \"74.0.3705.1\",\r\n                \"74.0.3705.0\",\r\n                \"73.0.3683.40\",\r\n                \"72.0.3626.111\",\r\n                \"73.0.3683.39\",\r\n                \"74.0.3704.4\",\r\n                \"73.0.3683.38\",\r\n                \"74.0.3704.3\",\r\n                \"74.0.3704.2\",\r\n                \"74.0.3704.1\",\r\n                \"74.0.3704.0\",\r\n                \"73.0.3683.37\",\r\n                \"72.0.3626.110\",\r\n                \"72.0.3626.109\",\r\n                \"74.0.3703.3\",\r\n                \"74.0.3703.2\",\r\n                \"73.0.3683.36\",\r\n                \"74.0.3703.1\",\r\n                \"74.0.3703.0\",\r\n                \"73.0.3683.35\",\r\n                \"72.0.3626.108\",\r\n                \"74.0.3702.2\",\r\n                \"74.0.3699.3\",\r\n                \"74.0.3702.1\",\r\n                \"74.0.3702.0\",\r\n                \"73.0.3683.34\",\r\n                \"72.0.3626.107\",\r\n                \"73.0.3683.33\",\r\n                \"74.0.3701.1\",\r\n                \"74.0.3701.0\",\r\n                \"73.0.3683.32\",\r\n                \"73.0.3683.31\",\r\n                \"72.0.3626.105\",\r\n                \"74.0.3700.1\",\r\n                \"74.0.3700.0\",\r\n                \"73.0.3683.29\",\r\n                \"72.0.3626.103\",\r\n                \"74.0.3699.2\",\r\n                \"74.0.3699.1\",\r\n                \"74.0.3699.0\",\r\n                \"73.0.3683.28\",\r\n                \"72.0.3626.102\",\r\n                \"73.0.3683.27\",\r\n                \"73.0.3683.26\",\r\n                \"74.0.3698.0\",\r\n                \"74.0.3696.2\",\r\n                \"72.0.3626.101\",\r\n                \"73.0.3683.25\",\r\n                \"74.0.3696.1\",\r\n                \"74.0.3696.0\",\r\n                \"74.0.3694.8\",\r\n                \"72.0.3626.100\",\r\n                \"74.0.3694.7\",\r\n                \"74.0.3694.6\",\r\n                \"74.0.3694.5\",\r\n                \"74.0.3694.4\",\r\n                \"72.0.3626.99\",\r\n                \"72.0.3626.98\",\r\n                \"74.0.3694.3\",\r\n                \"73.0.3683.24\",\r\n                \"72.0.3626.97\",\r\n                \"72.0.3626.96\",\r\n                \"72.0.3626.95\",\r\n                \"73.0.3683.23\",\r\n                \"72.0.3626.94\",\r\n                \"73.0.3683.22\",\r\n                \"73.0.3683.21\",\r\n                \"72.0.3626.93\",\r\n                \"74.0.3694.2\",\r\n                \"72.0.3626.92\",\r\n                \"74.0.3694.1\",\r\n                \"74.0.3694.0\",\r\n                \"74.0.3693.6\",\r\n                \"73.0.3683.20\",\r\n                \"72.0.3626.91\",\r\n                \"74.0.3693.5\",\r\n                \"74.0.3693.4\",\r\n                \"74.0.3693.3\",\r\n                \"74.0.3693.2\",\r\n                \"73.0.3683.19\",\r\n                \"74.0.3693.1\",\r\n                \"74.0.3693.0\",\r\n                \"73.0.3683.18\",\r\n                \"72.0.3626.90\",\r\n                \"74.0.3692.1\",\r\n                \"74.0.3692.0\",\r\n                \"73.0.3683.17\",\r\n                \"72.0.3626.89\",\r\n                \"74.0.3687.3\",\r\n                \"74.0.3691.1\",\r\n                \"74.0.3691.0\",\r\n                \"73.0.3683.16\",\r\n                \"72.0.3626.88\",\r\n                \"72.0.3626.87\",\r\n                \"73.0.3683.15\",\r\n                \"74.0.3690.1\",\r\n                \"74.0.3690.0\",\r\n                \"73.0.3683.14\",\r\n                \"72.0.3626.86\",\r\n                \"73.0.3683.13\",\r\n                \"73.0.3683.12\",\r\n                \"74.0.3689.1\",\r\n                \"74.0.3689.0\",\r\n                \"73.0.3683.11\",\r\n                \"72.0.3626.85\",\r\n                \"73.0.3683.10\",\r\n                \"72.0.3626.84\",\r\n                \"73.0.3683.9\",\r\n                \"74.0.3688.1\",\r\n                \"74.0.3688.0\",\r\n                \"73.0.3683.8\",\r\n                \"72.0.3626.83\",\r\n                \"74.0.3687.2\",\r\n                \"74.0.3687.1\",\r\n                \"74.0.3687.0\",\r\n                \"73.0.3683.7\",\r\n                \"72.0.3626.82\",\r\n                \"74.0.3686.4\",\r\n                \"72.0.3626.81\",\r\n                \"74.0.3686.3\",\r\n                \"74.0.3686.2\",\r\n                \"74.0.3686.1\",\r\n                \"74.0.3686.0\",\r\n                \"73.0.3683.6\",\r\n                \"72.0.3626.80\",\r\n                \"74.0.3685.1\",\r\n                \"74.0.3685.0\",\r\n                \"73.0.3683.5\",\r\n                \"72.0.3626.79\",\r\n                \"74.0.3684.1\",\r\n                \"74.0.3684.0\",\r\n                \"73.0.3683.4\",\r\n                \"72.0.3626.78\",\r\n                \"72.0.3626.77\",\r\n                \"73.0.3683.3\",\r\n                \"73.0.3683.2\",\r\n                \"72.0.3626.76\",\r\n                \"73.0.3683.1\",\r\n                \"73.0.3683.0\",\r\n                \"72.0.3626.75\",\r\n                \"71.0.3578.141\",\r\n                \"73.0.3682.1\",\r\n                \"73.0.3682.0\",\r\n                \"72.0.3626.74\",\r\n                \"71.0.3578.140\",\r\n                \"73.0.3681.4\",\r\n                \"73.0.3681.3\",\r\n                \"73.0.3681.2\",\r\n                \"73.0.3681.1\",\r\n                \"73.0.3681.0\",\r\n                \"72.0.3626.73\",\r\n                \"71.0.3578.139\",\r\n                \"72.0.3626.72\",\r\n                \"72.0.3626.71\",\r\n                \"73.0.3680.1\",\r\n                \"73.0.3680.0\",\r\n                \"72.0.3626.70\",\r\n                \"71.0.3578.138\",\r\n                \"73.0.3678.2\",\r\n                \"73.0.3679.1\",\r\n                \"73.0.3679.0\",\r\n                \"72.0.3626.69\",\r\n                \"71.0.3578.137\",\r\n                \"73.0.3678.1\",\r\n                \"73.0.3678.0\",\r\n                \"71.0.3578.136\",\r\n                \"73.0.3677.1\",\r\n                \"73.0.3677.0\",\r\n                \"72.0.3626.68\",\r\n                \"72.0.3626.67\",\r\n                \"71.0.3578.135\",\r\n                \"73.0.3676.1\",\r\n                \"73.0.3676.0\",\r\n                \"73.0.3674.2\",\r\n                \"72.0.3626.66\",\r\n                \"71.0.3578.134\",\r\n                \"73.0.3674.1\",\r\n                \"73.0.3674.0\",\r\n                \"72.0.3626.65\",\r\n                \"71.0.3578.133\",\r\n                \"73.0.3673.2\",\r\n                \"73.0.3673.1\",\r\n                \"73.0.3673.0\",\r\n                \"72.0.3626.64\",\r\n                \"71.0.3578.132\",\r\n                \"72.0.3626.63\",\r\n                \"72.0.3626.62\",\r\n                \"72.0.3626.61\",\r\n                \"72.0.3626.60\",\r\n                \"73.0.3672.1\",\r\n                \"73.0.3672.0\",\r\n                \"72.0.3626.59\",\r\n                \"71.0.3578.131\",\r\n                \"73.0.3671.3\",\r\n                \"73.0.3671.2\",\r\n                \"73.0.3671.1\",\r\n                \"73.0.3671.0\",\r\n                \"72.0.3626.58\",\r\n                \"71.0.3578.130\",\r\n                \"73.0.3670.1\",\r\n                \"73.0.3670.0\",\r\n                \"72.0.3626.57\",\r\n                \"71.0.3578.129\",\r\n                \"73.0.3669.1\",\r\n                \"73.0.3669.0\",\r\n                \"72.0.3626.56\",\r\n                \"71.0.3578.128\",\r\n                \"73.0.3668.2\",\r\n                \"73.0.3668.1\",\r\n                \"73.0.3668.0\",\r\n                \"72.0.3626.55\",\r\n                \"71.0.3578.127\",\r\n                \"73.0.3667.2\",\r\n                \"73.0.3667.1\",\r\n                \"73.0.3667.0\",\r\n                \"72.0.3626.54\",\r\n                \"71.0.3578.126\",\r\n                \"73.0.3666.1\",\r\n                \"73.0.3666.0\",\r\n                \"72.0.3626.53\",\r\n                \"71.0.3578.125\",\r\n                \"73.0.3665.4\",\r\n                \"73.0.3665.3\",\r\n                \"72.0.3626.52\",\r\n                \"73.0.3665.2\",\r\n                \"73.0.3664.4\",\r\n                \"73.0.3665.1\",\r\n                \"73.0.3665.0\",\r\n                \"72.0.3626.51\",\r\n                \"71.0.3578.124\",\r\n                \"72.0.3626.50\",\r\n                \"73.0.3664.3\",\r\n                \"73.0.3664.2\",\r\n                \"73.0.3664.1\",\r\n                \"73.0.3664.0\",\r\n                \"73.0.3663.2\",\r\n                \"72.0.3626.49\",\r\n                \"71.0.3578.123\",\r\n                \"73.0.3663.1\",\r\n                \"73.0.3663.0\",\r\n                \"72.0.3626.48\",\r\n                \"71.0.3578.122\",\r\n                \"73.0.3662.1\",\r\n                \"73.0.3662.0\",\r\n                \"72.0.3626.47\",\r\n                \"71.0.3578.121\",\r\n                \"73.0.3661.1\",\r\n                \"72.0.3626.46\",\r\n                \"73.0.3661.0\",\r\n                \"72.0.3626.45\",\r\n                \"71.0.3578.120\",\r\n                \"73.0.3660.2\",\r\n                \"73.0.3660.1\",\r\n                \"73.0.3660.0\",\r\n                \"72.0.3626.44\",\r\n                \"71.0.3578.119\",\r\n                \"73.0.3659.1\",\r\n                \"73.0.3659.0\",\r\n                \"72.0.3626.43\",\r\n                \"71.0.3578.118\",\r\n                \"73.0.3658.1\",\r\n                \"73.0.3658.0\",\r\n                \"72.0.3626.42\",\r\n                \"71.0.3578.117\",\r\n                \"73.0.3657.1\",\r\n                \"73.0.3657.0\",\r\n                \"72.0.3626.41\",\r\n                \"71.0.3578.116\",\r\n                \"73.0.3656.1\",\r\n                \"73.0.3656.0\",\r\n                \"72.0.3626.40\",\r\n                \"71.0.3578.115\",\r\n                \"73.0.3655.1\",\r\n                \"73.0.3655.0\",\r\n                \"72.0.3626.39\",\r\n                \"71.0.3578.114\",\r\n                \"73.0.3654.1\",\r\n                \"73.0.3654.0\",\r\n                \"72.0.3626.38\",\r\n                \"71.0.3578.113\",\r\n                \"73.0.3653.1\",\r\n                \"73.0.3653.0\",\r\n                \"72.0.3626.37\",\r\n                \"71.0.3578.112\",\r\n                \"73.0.3652.1\",\r\n                \"73.0.3652.0\",\r\n                \"72.0.3626.36\",\r\n                \"71.0.3578.111\",\r\n                \"73.0.3651.1\",\r\n                \"73.0.3651.0\",\r\n                \"72.0.3626.35\",\r\n                \"71.0.3578.110\",\r\n                \"73.0.3650.1\",\r\n                \"73.0.3650.0\",\r\n                \"72.0.3626.34\",\r\n                \"71.0.3578.109\",\r\n                \"73.0.3649.1\",\r\n                \"73.0.3649.0\",\r\n                \"72.0.3626.33\",\r\n                \"71.0.3578.108\",\r\n                \"73.0.3648.2\",\r\n                \"73.0.3648.1\",\r\n                \"73.0.3648.0\",\r\n                \"72.0.3626.32\",\r\n                \"71.0.3578.107\",\r\n                \"73.0.3647.2\",\r\n                \"73.0.3647.1\",\r\n                \"73.0.3647.0\",\r\n                \"72.0.3626.31\",\r\n                \"71.0.3578.106\",\r\n                \"73.0.3635.3\",\r\n                \"73.0.3646.2\",\r\n                \"73.0.3646.1\",\r\n                \"73.0.3646.0\",\r\n                \"72.0.3626.30\",\r\n                \"71.0.3578.105\",\r\n                \"72.0.3626.29\",\r\n                \"73.0.3645.2\",\r\n                \"73.0.3645.1\",\r\n                \"73.0.3645.0\",\r\n                \"72.0.3626.28\",\r\n                \"71.0.3578.104\",\r\n                \"72.0.3626.27\",\r\n                \"72.0.3626.26\",\r\n                \"72.0.3626.25\",\r\n                \"72.0.3626.24\",\r\n                \"73.0.3644.0\",\r\n                \"73.0.3643.2\",\r\n                \"72.0.3626.23\",\r\n                \"71.0.3578.103\",\r\n                \"73.0.3643.1\",\r\n                \"73.0.3643.0\",\r\n                \"72.0.3626.22\",\r\n                \"71.0.3578.102\",\r\n                \"73.0.3642.1\",\r\n                \"73.0.3642.0\",\r\n                \"72.0.3626.21\",\r\n                \"71.0.3578.101\",\r\n                \"73.0.3641.1\",\r\n                \"73.0.3641.0\",\r\n                \"72.0.3626.20\",\r\n                \"71.0.3578.100\",\r\n                \"72.0.3626.19\",\r\n                \"73.0.3640.1\",\r\n                \"73.0.3640.0\",\r\n                \"72.0.3626.18\",\r\n                \"73.0.3639.1\",\r\n                \"71.0.3578.99\",\r\n                \"73.0.3639.0\",\r\n                \"72.0.3626.17\",\r\n                \"73.0.3638.2\",\r\n                \"72.0.3626.16\",\r\n                \"73.0.3638.1\",\r\n                \"73.0.3638.0\",\r\n                \"72.0.3626.15\",\r\n                \"71.0.3578.98\",\r\n                \"73.0.3635.2\",\r\n                \"71.0.3578.97\",\r\n                \"73.0.3637.1\",\r\n                \"73.0.3637.0\",\r\n                \"72.0.3626.14\",\r\n                \"71.0.3578.96\",\r\n                \"71.0.3578.95\",\r\n                \"72.0.3626.13\",\r\n                \"71.0.3578.94\",\r\n                \"73.0.3636.2\",\r\n                \"71.0.3578.93\",\r\n                \"73.0.3636.1\",\r\n                \"73.0.3636.0\",\r\n                \"72.0.3626.12\",\r\n                \"71.0.3578.92\",\r\n                \"73.0.3635.1\",\r\n                \"73.0.3635.0\",\r\n                \"72.0.3626.11\",\r\n                \"71.0.3578.91\",\r\n                \"73.0.3634.2\",\r\n                \"73.0.3634.1\",\r\n                \"73.0.3634.0\",\r\n                \"72.0.3626.10\",\r\n                \"71.0.3578.90\",\r\n                \"71.0.3578.89\",\r\n                \"73.0.3633.2\",\r\n                \"73.0.3633.1\",\r\n                \"73.0.3633.0\",\r\n                \"72.0.3610.4\",\r\n                \"72.0.3626.9\",\r\n                \"71.0.3578.88\",\r\n                \"73.0.3632.5\",\r\n                \"73.0.3632.4\",\r\n                \"73.0.3632.3\",\r\n                \"73.0.3632.2\",\r\n                \"73.0.3632.1\",\r\n                \"73.0.3632.0\",\r\n                \"72.0.3626.8\",\r\n                \"71.0.3578.87\",\r\n                \"73.0.3631.2\",\r\n                \"73.0.3631.1\",\r\n                \"73.0.3631.0\",\r\n                \"72.0.3626.7\",\r\n                \"71.0.3578.86\",\r\n                \"72.0.3626.6\",\r\n                \"73.0.3630.1\",\r\n                \"73.0.3630.0\",\r\n                \"72.0.3626.5\",\r\n                \"71.0.3578.85\",\r\n                \"72.0.3626.4\",\r\n                \"73.0.3628.3\",\r\n                \"73.0.3628.2\",\r\n                \"73.0.3629.1\",\r\n                \"73.0.3629.0\",\r\n                \"72.0.3626.3\",\r\n                \"71.0.3578.84\",\r\n                \"73.0.3628.1\",\r\n                \"73.0.3628.0\",\r\n                \"71.0.3578.83\",\r\n                \"73.0.3627.1\",\r\n                \"73.0.3627.0\",\r\n                \"72.0.3626.2\",\r\n                \"71.0.3578.82\",\r\n                \"71.0.3578.81\",\r\n                \"71.0.3578.80\",\r\n                \"72.0.3626.1\",\r\n                \"72.0.3626.0\",\r\n                \"71.0.3578.79\",\r\n                \"70.0.3538.124\",\r\n                \"71.0.3578.78\",\r\n                \"72.0.3623.4\",\r\n                \"72.0.3625.2\",\r\n                \"72.0.3625.1\",\r\n                \"72.0.3625.0\",\r\n                \"71.0.3578.77\",\r\n                \"70.0.3538.123\",\r\n                \"72.0.3624.4\",\r\n                \"72.0.3624.3\",\r\n                \"72.0.3624.2\",\r\n                \"71.0.3578.76\",\r\n                \"72.0.3624.1\",\r\n                \"72.0.3624.0\",\r\n                \"72.0.3623.3\",\r\n                \"71.0.3578.75\",\r\n                \"70.0.3538.122\",\r\n                \"71.0.3578.74\",\r\n                \"72.0.3623.2\",\r\n                \"72.0.3610.3\",\r\n                \"72.0.3623.1\",\r\n                \"72.0.3623.0\",\r\n                \"72.0.3622.3\",\r\n                \"72.0.3622.2\",\r\n                \"71.0.3578.73\",\r\n                \"70.0.3538.121\",\r\n                \"72.0.3622.1\",\r\n                \"72.0.3622.0\",\r\n                \"71.0.3578.72\",\r\n                \"70.0.3538.120\",\r\n                \"72.0.3621.1\",\r\n                \"72.0.3621.0\",\r\n                \"71.0.3578.71\",\r\n                \"70.0.3538.119\",\r\n                \"72.0.3620.1\",\r\n                \"72.0.3620.0\",\r\n                \"71.0.3578.70\",\r\n                \"70.0.3538.118\",\r\n                \"71.0.3578.69\",\r\n                \"72.0.3619.1\",\r\n                \"72.0.3619.0\",\r\n                \"71.0.3578.68\",\r\n                \"70.0.3538.117\",\r\n                \"71.0.3578.67\",\r\n                \"72.0.3618.1\",\r\n                \"72.0.3618.0\",\r\n                \"71.0.3578.66\",\r\n                \"70.0.3538.116\",\r\n                \"72.0.3617.1\",\r\n                \"72.0.3617.0\",\r\n                \"71.0.3578.65\",\r\n                \"70.0.3538.115\",\r\n                \"72.0.3602.3\",\r\n                \"71.0.3578.64\",\r\n                \"72.0.3616.1\",\r\n                \"72.0.3616.0\",\r\n                \"71.0.3578.63\",\r\n                \"70.0.3538.114\",\r\n                \"71.0.3578.62\",\r\n                \"72.0.3615.1\",\r\n                \"72.0.3615.0\",\r\n                \"71.0.3578.61\",\r\n                \"70.0.3538.113\",\r\n                \"72.0.3614.1\",\r\n                \"72.0.3614.0\",\r\n                \"71.0.3578.60\",\r\n                \"70.0.3538.112\",\r\n                \"72.0.3613.1\",\r\n                \"72.0.3613.0\",\r\n                \"71.0.3578.59\",\r\n                \"70.0.3538.111\",\r\n                \"72.0.3612.2\",\r\n                \"72.0.3612.1\",\r\n                \"72.0.3612.0\",\r\n                \"70.0.3538.110\",\r\n                \"71.0.3578.58\",\r\n                \"70.0.3538.109\",\r\n                \"72.0.3611.2\",\r\n                \"72.0.3611.1\",\r\n                \"72.0.3611.0\",\r\n                \"71.0.3578.57\",\r\n                \"70.0.3538.108\",\r\n                \"72.0.3610.2\",\r\n                \"71.0.3578.56\",\r\n                \"71.0.3578.55\",\r\n                \"72.0.3610.1\",\r\n                \"72.0.3610.0\",\r\n                \"71.0.3578.54\",\r\n                \"70.0.3538.107\",\r\n                \"71.0.3578.53\",\r\n                \"72.0.3609.3\",\r\n                \"71.0.3578.52\",\r\n                \"72.0.3609.2\",\r\n                \"71.0.3578.51\",\r\n                \"72.0.3608.5\",\r\n                \"72.0.3609.1\",\r\n                \"72.0.3609.0\",\r\n                \"71.0.3578.50\",\r\n                \"70.0.3538.106\",\r\n                \"72.0.3608.4\",\r\n                \"72.0.3608.3\",\r\n                \"72.0.3608.2\",\r\n                \"71.0.3578.49\",\r\n                \"72.0.3608.1\",\r\n                \"72.0.3608.0\",\r\n                \"70.0.3538.105\",\r\n                \"71.0.3578.48\",\r\n                \"72.0.3607.1\",\r\n                \"72.0.3607.0\",\r\n                \"71.0.3578.47\",\r\n                \"70.0.3538.104\",\r\n                \"72.0.3606.2\",\r\n                \"72.0.3606.1\",\r\n                \"72.0.3606.0\",\r\n                \"71.0.3578.46\",\r\n                \"70.0.3538.103\",\r\n                \"70.0.3538.102\",\r\n                \"72.0.3605.3\",\r\n                \"72.0.3605.2\",\r\n                \"72.0.3605.1\",\r\n                \"72.0.3605.0\",\r\n                \"71.0.3578.45\",\r\n                \"70.0.3538.101\",\r\n                \"71.0.3578.44\",\r\n                \"71.0.3578.43\",\r\n                \"70.0.3538.100\",\r\n                \"70.0.3538.99\",\r\n                \"71.0.3578.42\",\r\n                \"72.0.3604.1\",\r\n                \"72.0.3604.0\",\r\n                \"71.0.3578.41\",\r\n                \"70.0.3538.98\",\r\n                \"71.0.3578.40\",\r\n                \"72.0.3603.2\",\r\n                \"72.0.3603.1\",\r\n                \"72.0.3603.0\",\r\n                \"71.0.3578.39\",\r\n                \"70.0.3538.97\",\r\n                \"72.0.3602.2\",\r\n                \"71.0.3578.38\",\r\n                \"71.0.3578.37\",\r\n                \"72.0.3602.1\",\r\n                \"72.0.3602.0\",\r\n                \"71.0.3578.36\",\r\n                \"70.0.3538.96\",\r\n                \"72.0.3601.1\",\r\n                \"72.0.3601.0\",\r\n                \"71.0.3578.35\",\r\n                \"70.0.3538.95\",\r\n                \"72.0.3600.1\",\r\n                \"72.0.3600.0\",\r\n                \"71.0.3578.34\",\r\n                \"70.0.3538.94\",\r\n                \"72.0.3599.3\",\r\n                \"72.0.3599.2\",\r\n                \"72.0.3599.1\",\r\n                \"72.0.3599.0\",\r\n                \"71.0.3578.33\",\r\n                \"70.0.3538.93\",\r\n                \"72.0.3598.1\",\r\n                \"72.0.3598.0\",\r\n                \"71.0.3578.32\",\r\n                \"70.0.3538.87\",\r\n                \"72.0.3597.1\",\r\n                \"72.0.3597.0\",\r\n                \"72.0.3596.2\",\r\n                \"71.0.3578.31\",\r\n                \"70.0.3538.86\",\r\n                \"71.0.3578.30\",\r\n                \"71.0.3578.29\",\r\n                \"72.0.3596.1\",\r\n                \"72.0.3596.0\",\r\n                \"71.0.3578.28\",\r\n                \"70.0.3538.85\",\r\n                \"72.0.3595.2\",\r\n                \"72.0.3591.3\",\r\n                \"72.0.3595.1\",\r\n                \"72.0.3595.0\",\r\n                \"71.0.3578.27\",\r\n                \"70.0.3538.84\",\r\n                \"72.0.3594.1\",\r\n                \"72.0.3594.0\",\r\n                \"71.0.3578.26\",\r\n                \"70.0.3538.83\",\r\n                \"72.0.3593.2\",\r\n                \"72.0.3593.1\",\r\n                \"72.0.3593.0\",\r\n                \"71.0.3578.25\",\r\n                \"70.0.3538.82\",\r\n                \"72.0.3589.3\",\r\n                \"72.0.3592.2\",\r\n                \"72.0.3592.1\",\r\n                \"72.0.3592.0\",\r\n                \"71.0.3578.24\",\r\n                \"72.0.3589.2\",\r\n                \"70.0.3538.81\",\r\n                \"70.0.3538.80\",\r\n                \"72.0.3591.2\",\r\n                \"72.0.3591.1\",\r\n                \"72.0.3591.0\",\r\n                \"71.0.3578.23\",\r\n                \"70.0.3538.79\",\r\n                \"71.0.3578.22\",\r\n                \"72.0.3590.1\",\r\n                \"72.0.3590.0\",\r\n                \"71.0.3578.21\",\r\n                \"70.0.3538.78\",\r\n                \"70.0.3538.77\",\r\n                \"72.0.3589.1\",\r\n                \"72.0.3589.0\",\r\n                \"71.0.3578.20\",\r\n                \"70.0.3538.76\",\r\n                \"71.0.3578.19\",\r\n                \"70.0.3538.75\",\r\n                \"72.0.3588.1\",\r\n                \"72.0.3588.0\",\r\n                \"71.0.3578.18\",\r\n                \"70.0.3538.74\",\r\n                \"72.0.3586.2\",\r\n                \"72.0.3587.0\",\r\n                \"71.0.3578.17\",\r\n                \"70.0.3538.73\",\r\n                \"72.0.3586.1\",\r\n                \"72.0.3586.0\",\r\n                \"71.0.3578.16\",\r\n                \"70.0.3538.72\",\r\n                \"72.0.3585.1\",\r\n                \"72.0.3585.0\",\r\n                \"71.0.3578.15\",\r\n                \"70.0.3538.71\",\r\n                \"71.0.3578.14\",\r\n                \"72.0.3584.1\",\r\n                \"72.0.3584.0\",\r\n                \"71.0.3578.13\",\r\n                \"70.0.3538.70\",\r\n                \"72.0.3583.2\",\r\n                \"71.0.3578.12\",\r\n                \"72.0.3583.1\",\r\n                \"72.0.3583.0\",\r\n                \"71.0.3578.11\",\r\n                \"70.0.3538.69\",\r\n                \"71.0.3578.10\",\r\n                \"72.0.3582.0\",\r\n                \"72.0.3581.4\",\r\n                \"71.0.3578.9\",\r\n                \"70.0.3538.67\",\r\n                \"72.0.3581.3\",\r\n                \"72.0.3581.2\",\r\n                \"72.0.3581.1\",\r\n                \"72.0.3581.0\",\r\n                \"71.0.3578.8\",\r\n                \"70.0.3538.66\",\r\n                \"72.0.3580.1\",\r\n                \"72.0.3580.0\",\r\n                \"71.0.3578.7\",\r\n                \"70.0.3538.65\",\r\n                \"71.0.3578.6\",\r\n                \"72.0.3579.1\",\r\n                \"72.0.3579.0\",\r\n                \"71.0.3578.5\",\r\n                \"70.0.3538.64\",\r\n                \"71.0.3578.4\",\r\n                \"71.0.3578.3\",\r\n                \"71.0.3578.2\",\r\n                \"71.0.3578.1\",\r\n                \"71.0.3578.0\",\r\n                \"70.0.3538.63\",\r\n                \"69.0.3497.128\",\r\n                \"70.0.3538.62\",\r\n                \"70.0.3538.61\",\r\n                \"70.0.3538.60\",\r\n                \"70.0.3538.59\",\r\n                \"71.0.3577.1\",\r\n                \"71.0.3577.0\",\r\n                \"70.0.3538.58\",\r\n                \"69.0.3497.127\",\r\n                \"71.0.3576.2\",\r\n                \"71.0.3576.1\",\r\n                \"71.0.3576.0\",\r\n                \"70.0.3538.57\",\r\n                \"70.0.3538.56\",\r\n                \"71.0.3575.2\",\r\n                \"70.0.3538.55\",\r\n                \"69.0.3497.126\",\r\n                \"70.0.3538.54\",\r\n                \"71.0.3575.1\",\r\n                \"71.0.3575.0\",\r\n                \"71.0.3574.1\",\r\n                \"71.0.3574.0\",\r\n                \"70.0.3538.53\",\r\n                \"69.0.3497.125\",\r\n                \"70.0.3538.52\",\r\n                \"71.0.3573.1\",\r\n                \"71.0.3573.0\",\r\n                \"70.0.3538.51\",\r\n                \"69.0.3497.124\",\r\n                \"71.0.3572.1\",\r\n                \"71.0.3572.0\",\r\n                \"70.0.3538.50\",\r\n                \"69.0.3497.123\",\r\n                \"71.0.3571.2\",\r\n                \"70.0.3538.49\",\r\n                \"69.0.3497.122\",\r\n                \"71.0.3571.1\",\r\n                \"71.0.3571.0\",\r\n                \"70.0.3538.48\",\r\n                \"69.0.3497.121\",\r\n                \"71.0.3570.1\",\r\n                \"71.0.3570.0\",\r\n                \"70.0.3538.47\",\r\n                \"69.0.3497.120\",\r\n                \"71.0.3568.2\",\r\n                \"71.0.3569.1\",\r\n                \"71.0.3569.0\",\r\n                \"70.0.3538.46\",\r\n                \"69.0.3497.119\",\r\n                \"70.0.3538.45\",\r\n                \"71.0.3568.1\",\r\n                \"71.0.3568.0\",\r\n                \"70.0.3538.44\",\r\n                \"69.0.3497.118\",\r\n                \"70.0.3538.43\",\r\n                \"70.0.3538.42\",\r\n                \"71.0.3567.1\",\r\n                \"71.0.3567.0\",\r\n                \"70.0.3538.41\",\r\n                \"69.0.3497.117\",\r\n                \"71.0.3566.1\",\r\n                \"71.0.3566.0\",\r\n                \"70.0.3538.40\",\r\n                \"69.0.3497.116\",\r\n                \"71.0.3565.1\",\r\n                \"71.0.3565.0\",\r\n                \"70.0.3538.39\",\r\n                \"69.0.3497.115\",\r\n                \"71.0.3564.1\",\r\n                \"71.0.3564.0\",\r\n                \"70.0.3538.38\",\r\n                \"69.0.3497.114\",\r\n                \"71.0.3563.0\",\r\n                \"71.0.3562.2\",\r\n                \"70.0.3538.37\",\r\n                \"69.0.3497.113\",\r\n                \"70.0.3538.36\",\r\n                \"70.0.3538.35\",\r\n                \"71.0.3562.1\",\r\n                \"71.0.3562.0\",\r\n                \"70.0.3538.34\",\r\n                \"69.0.3497.112\",\r\n                \"70.0.3538.33\",\r\n                \"71.0.3561.1\",\r\n                \"71.0.3561.0\",\r\n                \"70.0.3538.32\",\r\n                \"69.0.3497.111\",\r\n                \"71.0.3559.6\",\r\n                \"71.0.3560.1\",\r\n                \"71.0.3560.0\",\r\n                \"71.0.3559.5\",\r\n                \"71.0.3559.4\",\r\n                \"70.0.3538.31\",\r\n                \"69.0.3497.110\",\r\n                \"71.0.3559.3\",\r\n                \"70.0.3538.30\",\r\n                \"69.0.3497.109\",\r\n                \"71.0.3559.2\",\r\n                \"71.0.3559.1\",\r\n                \"71.0.3559.0\",\r\n                \"70.0.3538.29\",\r\n                \"69.0.3497.108\",\r\n                \"71.0.3558.2\",\r\n                \"71.0.3558.1\",\r\n                \"71.0.3558.0\",\r\n                \"70.0.3538.28\",\r\n                \"69.0.3497.107\",\r\n                \"71.0.3557.2\",\r\n                \"71.0.3557.1\",\r\n                \"71.0.3557.0\",\r\n                \"70.0.3538.27\",\r\n                \"69.0.3497.106\",\r\n                \"71.0.3554.4\",\r\n                \"70.0.3538.26\",\r\n                \"71.0.3556.1\",\r\n                \"71.0.3556.0\",\r\n                \"70.0.3538.25\",\r\n                \"71.0.3554.3\",\r\n                \"69.0.3497.105\",\r\n                \"71.0.3554.2\",\r\n                \"70.0.3538.24\",\r\n                \"69.0.3497.104\",\r\n                \"71.0.3555.2\",\r\n                \"70.0.3538.23\",\r\n                \"71.0.3555.1\",\r\n                \"71.0.3555.0\",\r\n                \"70.0.3538.22\",\r\n                \"69.0.3497.103\",\r\n                \"71.0.3554.1\",\r\n                \"71.0.3554.0\",\r\n                \"70.0.3538.21\",\r\n                \"69.0.3497.102\",\r\n                \"71.0.3553.3\",\r\n                \"70.0.3538.20\",\r\n                \"69.0.3497.101\",\r\n                \"71.0.3553.2\",\r\n                \"69.0.3497.100\",\r\n                \"71.0.3553.1\",\r\n                \"71.0.3553.0\",\r\n                \"70.0.3538.19\",\r\n                \"69.0.3497.99\",\r\n                \"69.0.3497.98\",\r\n                \"69.0.3497.97\",\r\n                \"71.0.3552.6\",\r\n                \"71.0.3552.5\",\r\n                \"71.0.3552.4\",\r\n                \"71.0.3552.3\",\r\n                \"71.0.3552.2\",\r\n                \"71.0.3552.1\",\r\n                \"71.0.3552.0\",\r\n                \"70.0.3538.18\",\r\n                \"69.0.3497.96\",\r\n                \"71.0.3551.3\",\r\n                \"71.0.3551.2\",\r\n                \"71.0.3551.1\",\r\n                \"71.0.3551.0\",\r\n                \"70.0.3538.17\",\r\n                \"69.0.3497.95\",\r\n                \"71.0.3550.3\",\r\n                \"71.0.3550.2\",\r\n                \"71.0.3550.1\",\r\n                \"71.0.3550.0\",\r\n                \"70.0.3538.16\",\r\n                \"69.0.3497.94\",\r\n                \"71.0.3549.1\",\r\n                \"71.0.3549.0\",\r\n                \"70.0.3538.15\",\r\n                \"69.0.3497.93\",\r\n                \"69.0.3497.92\",\r\n                \"71.0.3548.1\",\r\n                \"71.0.3548.0\",\r\n                \"70.0.3538.14\",\r\n                \"69.0.3497.91\",\r\n                \"71.0.3547.1\",\r\n                \"71.0.3547.0\",\r\n                \"70.0.3538.13\",\r\n                \"69.0.3497.90\",\r\n                \"71.0.3546.2\",\r\n                \"69.0.3497.89\",\r\n                \"71.0.3546.1\",\r\n                \"71.0.3546.0\",\r\n                \"70.0.3538.12\",\r\n                \"69.0.3497.88\",\r\n                \"71.0.3545.4\",\r\n                \"71.0.3545.3\",\r\n                \"71.0.3545.2\",\r\n                \"71.0.3545.1\",\r\n                \"71.0.3545.0\",\r\n                \"70.0.3538.11\",\r\n                \"69.0.3497.87\",\r\n                \"71.0.3544.5\",\r\n                \"71.0.3544.4\",\r\n                \"71.0.3544.3\",\r\n                \"71.0.3544.2\",\r\n                \"71.0.3544.1\",\r\n                \"71.0.3544.0\",\r\n                \"69.0.3497.86\",\r\n                \"70.0.3538.10\",\r\n                \"69.0.3497.85\",\r\n                \"70.0.3538.9\",\r\n                \"69.0.3497.84\",\r\n                \"71.0.3543.4\",\r\n                \"70.0.3538.8\",\r\n                \"71.0.3543.3\",\r\n                \"71.0.3543.2\",\r\n                \"71.0.3543.1\",\r\n                \"71.0.3543.0\",\r\n                \"70.0.3538.7\",\r\n                \"69.0.3497.83\",\r\n                \"71.0.3542.2\",\r\n                \"71.0.3542.1\",\r\n                \"71.0.3542.0\",\r\n                \"70.0.3538.6\",\r\n                \"69.0.3497.82\",\r\n                \"69.0.3497.81\",\r\n                \"71.0.3541.1\",\r\n                \"71.0.3541.0\",\r\n                \"70.0.3538.5\",\r\n                \"69.0.3497.80\",\r\n                \"71.0.3540.1\",\r\n                \"71.0.3540.0\",\r\n                \"70.0.3538.4\",\r\n                \"69.0.3497.79\",\r\n                \"70.0.3538.3\",\r\n                \"71.0.3539.1\",\r\n                \"71.0.3539.0\",\r\n                \"69.0.3497.78\",\r\n                \"68.0.3440.134\",\r\n                \"69.0.3497.77\",\r\n                \"70.0.3538.2\",\r\n                \"70.0.3538.1\",\r\n                \"70.0.3538.0\",\r\n                \"69.0.3497.76\",\r\n                \"68.0.3440.133\",\r\n                \"69.0.3497.75\",\r\n                \"70.0.3537.2\",\r\n                \"70.0.3537.1\",\r\n                \"70.0.3537.0\",\r\n                \"69.0.3497.74\",\r\n                \"68.0.3440.132\",\r\n                \"70.0.3536.0\",\r\n                \"70.0.3535.5\",\r\n                \"70.0.3535.4\",\r\n                \"70.0.3535.3\",\r\n                \"69.0.3497.73\",\r\n                \"68.0.3440.131\",\r\n                \"70.0.3532.8\",\r\n                \"70.0.3532.7\",\r\n                \"69.0.3497.72\",\r\n                \"69.0.3497.71\",\r\n                \"70.0.3535.2\",\r\n                \"70.0.3535.1\",\r\n                \"70.0.3535.0\",\r\n                \"69.0.3497.70\",\r\n                \"68.0.3440.130\",\r\n                \"69.0.3497.69\",\r\n                \"68.0.3440.129\",\r\n                \"70.0.3534.4\",\r\n                \"70.0.3534.3\",\r\n                \"70.0.3534.2\",\r\n                \"70.0.3534.1\",\r\n                \"70.0.3534.0\",\r\n                \"69.0.3497.68\",\r\n                \"68.0.3440.128\",\r\n                \"70.0.3533.2\",\r\n                \"70.0.3533.1\",\r\n                \"70.0.3533.0\",\r\n                \"69.0.3497.67\",\r\n                \"68.0.3440.127\",\r\n                \"70.0.3532.6\",\r\n                \"70.0.3532.5\",\r\n                \"70.0.3532.4\",\r\n                \"69.0.3497.66\",\r\n                \"68.0.3440.126\",\r\n                \"70.0.3532.3\",\r\n                \"70.0.3532.2\",\r\n                \"70.0.3532.1\",\r\n                \"69.0.3497.60\",\r\n                \"69.0.3497.65\",\r\n                \"69.0.3497.64\",\r\n                \"70.0.3532.0\",\r\n                \"70.0.3531.0\",\r\n                \"70.0.3530.4\",\r\n                \"70.0.3530.3\",\r\n                \"70.0.3530.2\",\r\n                \"69.0.3497.58\",\r\n                \"68.0.3440.125\",\r\n                \"69.0.3497.57\",\r\n                \"69.0.3497.56\",\r\n                \"69.0.3497.55\",\r\n                \"69.0.3497.54\",\r\n                \"70.0.3530.1\",\r\n                \"70.0.3530.0\",\r\n                \"69.0.3497.53\",\r\n                \"68.0.3440.124\",\r\n                \"69.0.3497.52\",\r\n                \"70.0.3529.3\",\r\n                \"70.0.3529.2\",\r\n                \"70.0.3529.1\",\r\n                \"70.0.3529.0\",\r\n                \"69.0.3497.51\",\r\n                \"70.0.3528.4\",\r\n                \"68.0.3440.123\",\r\n                \"70.0.3528.3\",\r\n                \"70.0.3528.2\",\r\n                \"70.0.3528.1\",\r\n                \"70.0.3528.0\",\r\n                \"69.0.3497.50\",\r\n                \"68.0.3440.122\",\r\n                \"70.0.3527.1\",\r\n                \"70.0.3527.0\",\r\n                \"69.0.3497.49\",\r\n                \"68.0.3440.121\",\r\n                \"70.0.3526.1\",\r\n                \"70.0.3526.0\",\r\n                \"68.0.3440.120\",\r\n                \"69.0.3497.48\",\r\n                \"69.0.3497.47\",\r\n                \"68.0.3440.119\",\r\n                \"68.0.3440.118\",\r\n                \"70.0.3525.5\",\r\n                \"70.0.3525.4\",\r\n                \"70.0.3525.3\",\r\n                \"68.0.3440.117\",\r\n                \"69.0.3497.46\",\r\n                \"70.0.3525.2\",\r\n                \"70.0.3525.1\",\r\n                \"70.0.3525.0\",\r\n                \"69.0.3497.45\",\r\n                \"68.0.3440.116\",\r\n                \"70.0.3524.4\",\r\n                \"70.0.3524.3\",\r\n                \"69.0.3497.44\",\r\n                \"70.0.3524.2\",\r\n                \"70.0.3524.1\",\r\n                \"70.0.3524.0\",\r\n                \"70.0.3523.2\",\r\n                \"69.0.3497.43\",\r\n                \"68.0.3440.115\",\r\n                \"70.0.3505.9\",\r\n                \"69.0.3497.42\",\r\n                \"70.0.3505.8\",\r\n                \"70.0.3523.1\",\r\n                \"70.0.3523.0\",\r\n                \"69.0.3497.41\",\r\n                \"68.0.3440.114\",\r\n                \"70.0.3505.7\",\r\n                \"69.0.3497.40\",\r\n                \"70.0.3522.1\",\r\n                \"70.0.3522.0\",\r\n                \"70.0.3521.2\",\r\n                \"69.0.3497.39\",\r\n                \"68.0.3440.113\",\r\n                \"70.0.3505.6\",\r\n                \"70.0.3521.1\",\r\n                \"70.0.3521.0\",\r\n                \"69.0.3497.38\",\r\n                \"68.0.3440.112\",\r\n                \"70.0.3520.1\",\r\n                \"70.0.3520.0\",\r\n                \"69.0.3497.37\",\r\n                \"68.0.3440.111\",\r\n                \"70.0.3519.3\",\r\n                \"70.0.3519.2\",\r\n                \"70.0.3519.1\",\r\n                \"70.0.3519.0\",\r\n                \"69.0.3497.36\",\r\n                \"68.0.3440.110\",\r\n                \"70.0.3518.1\",\r\n                \"70.0.3518.0\",\r\n                \"69.0.3497.35\",\r\n                \"69.0.3497.34\",\r\n                \"68.0.3440.109\",\r\n                \"70.0.3517.1\",\r\n                \"70.0.3517.0\",\r\n                \"69.0.3497.33\",\r\n                \"68.0.3440.108\",\r\n                \"69.0.3497.32\",\r\n                \"70.0.3516.3\",\r\n                \"70.0.3516.2\",\r\n                \"70.0.3516.1\",\r\n                \"70.0.3516.0\",\r\n                \"69.0.3497.31\",\r\n                \"68.0.3440.107\",\r\n                \"70.0.3515.4\",\r\n                \"68.0.3440.106\",\r\n                \"70.0.3515.3\",\r\n                \"70.0.3515.2\",\r\n                \"70.0.3515.1\",\r\n                \"70.0.3515.0\",\r\n                \"69.0.3497.30\",\r\n                \"68.0.3440.105\",\r\n                \"68.0.3440.104\",\r\n                \"70.0.3514.2\",\r\n                \"70.0.3514.1\",\r\n                \"70.0.3514.0\",\r\n                \"69.0.3497.29\",\r\n                \"68.0.3440.103\",\r\n                \"70.0.3513.1\",\r\n                \"70.0.3513.0\",\r\n                \"69.0.3497.28\"\r\n    };\r\n\r\n    public String randomUserAgent() {\r\n        int rnd = new Random().nextInt(CHROME_VERSIONS.length);\r\n        return String.format(USER_AGENT_TMPL, CHROME_VERSIONS[rnd]);\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/java/com/liskovsoft/smartyoutubetv2/common/utils/Utils.java",
    "content": "package com.liskovsoft.smartyoutubetv2.common.utils;\r\n\r\nimport android.annotation.SuppressLint;\r\nimport android.app.Activity;\r\nimport android.app.ActivityManager;\r\nimport android.app.ActivityManager.RunningServiceInfo;\r\nimport android.app.AlarmManager;\r\nimport android.app.Instrumentation;\r\nimport android.app.KeyguardManager;\r\nimport android.app.Notification;\r\nimport android.app.NotificationChannel;\r\nimport android.app.NotificationManager;\r\nimport android.app.PendingIntent;\r\nimport android.app.Service;\r\nimport android.content.ActivityNotFoundException;\r\nimport android.content.ComponentName;\r\nimport android.content.Context;\r\nimport android.content.Intent;\r\nimport android.content.ServiceConnection;\r\nimport android.content.pm.PackageManager;\r\nimport android.content.pm.PackageManager.NameNotFoundException;\r\nimport android.content.res.Resources;\r\nimport android.content.res.Resources.NotFoundException;\r\nimport android.database.ContentObserver;\r\nimport android.graphics.Typeface;\r\nimport android.graphics.drawable.Drawable;\r\nimport android.media.AudioManager;\r\nimport android.net.Uri;\r\nimport android.os.Build;\r\nimport android.os.Build.VERSION;\r\nimport android.os.Handler;\r\nimport android.os.IBinder;\r\nimport android.os.Looper;\r\nimport android.os.PowerManager;\r\nimport android.provider.Settings.Secure;\r\nimport android.telephony.TelephonyManager;\r\nimport android.text.Spannable;\r\nimport android.text.SpannableString;\r\nimport android.text.SpannableStringBuilder;\r\nimport android.text.Spanned;\r\nimport android.text.style.AbsoluteSizeSpan;\r\nimport android.text.style.ClickableSpan;\r\nimport android.text.style.ForegroundColorSpan;\r\nimport android.text.style.ImageSpan;\r\nimport android.text.style.StyleSpan;\r\nimport android.text.style.URLSpan;\r\nimport android.view.KeyEvent;\r\nimport android.view.View;\r\nimport android.view.Window;\r\nimport android.view.WindowManager;\r\nimport android.view.inputmethod.BaseInputConnection;\r\n\r\nimport androidx.annotation.NonNull;\r\nimport androidx.browser.customtabs.CustomTabsIntent;\r\nimport androidx.core.app.NotificationCompat;\r\nimport androidx.core.content.ContextCompat;\r\nimport androidx.core.graphics.ColorUtils;\r\nimport androidx.work.ExistingPeriodicWorkPolicy;\r\nimport androidx.work.PeriodicWorkRequest;\r\nimport androidx.work.WorkManager;\r\n\r\nimport com.jakewharton.processphoenix.ProcessPhoenix;\r\nimport com.liskovsoft.sharedutils.helpers.AppInfoHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.DeviceHelpers;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.helpers.MessageHelpers;\r\nimport com.liskovsoft.sharedutils.misc.WeakHashSet;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.smartyoutubetv2.common.BuildConfig;\r\nimport com.liskovsoft.smartyoutubetv2.common.R;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.data.Video;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerConstants;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.manager.PlayerManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.models.playback.service.VideoStateService;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.PlaybackPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.SplashPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.presenters.WebBrowserPresenter;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.PlaybackView;\r\nimport com.liskovsoft.smartyoutubetv2.common.app.views.ViewManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.FormatItem.VideoPreset;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.TrackSelectorUtil;\r\nimport com.liskovsoft.smartyoutubetv2.common.exoplayer.selector.track.MediaTrack;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.MotherActivity;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.RemoteControlService;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.RemoteControlWorker;\r\nimport com.liskovsoft.smartyoutubetv2.common.misc.ScreensaverManager;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.HiddenPrefs;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.MainUIData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.PlayerTweaksData;\r\nimport com.liskovsoft.smartyoutubetv2.common.prefs.RemoteControlData;\r\nimport com.liskovsoft.youtubeapi.service.internal.MediaServiceData;\r\n\r\nimport java.io.UnsupportedEncodingException;\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\nimport java.util.UUID;\r\nimport java.util.concurrent.TimeUnit;\r\n\r\npublic class Utils {\r\n    public static final String[] KNOWN_PACKAGES = {\r\n            \"com.liskovsoft.smarttubetv.beta\",\r\n            \"com.teamsmart.videomanager.tv\",\r\n            \"org.smarttube.beta\",\r\n            \"org.smarttube.stable\",\r\n            \"org.smarttube.fdroid\",\r\n            \"app.smarttube.fdroid\",\r\n    };\r\n    public static final String[] BACKUP_PATTERNS = {\r\n            \"yt_service_prefs.xml\",\r\n            \"com.liskovsoft.appupdatechecker2.preferences.xml\",\r\n            \"com.liskovsoft.sharedutils.prefs.GlobalPreferences.xml\",\r\n            \"_preferences.xml\" // before _ should be the app package name\r\n    };\r\n    private static final String SUPER_PASSWD = \"smarttube\";\r\n    private static final int RANDOM_FAIL_REPEAT_TIMES = 10;\r\n    private static final String REMOTE_CONTROL_RECEIVER_CLASS_NAME = \"com.liskovsoft.smartyoutubetv2.common.misc.RemoteControlReceiver\";\r\n    private static final String UPDATE_CHANNELS_RECEIVER_CLASS_NAME = \"com.liskovsoft.leanbackassistant.channels.UpdateChannelsReceiver\";\r\n    private static final String BOOTSTRAP_ACTIVITY_CLASS_NAME = \"com.liskovsoft.smartyoutubetv2.tv.ui.main.SplashActivity\";\r\n    private static final String TASK_ID = RemoteControlWorker.class.getSimpleName();\r\n    private static final String TAG = Utils.class.getSimpleName();\r\n    private static final String QR_CODE_URL_TEMPLATE = \"https://api.qrserver.com/v1/create-qr-code/?data=%s\";\r\n    private static final int GLOBAL_VOLUME_TYPE = AudioManager.STREAM_MUSIC;\r\n    private static final String GLOBAL_VOLUME_SERVICE = Context.AUDIO_SERVICE;\r\n    public static final Handler sHandler = new Handler(Looper.getMainLooper());\r\n    public static final float[] SPEED_LIST_LONG =\r\n            new float[]{0.25f, 0.5f, 0.75f, 0.80f, 0.85f, 0.90f, 0.95f, 1.0f, 1.05f, 1.1f, 1.15f, 1.2f, 1.25f, 1.3f, 1.4f, 1.5f, 1.75f, 2f, 2.25f, 2.5f, 2.75f, 3.0f, 3.25f, 3.5f, 3.75f, 4.0f};\r\n    public static final float[] SPEED_LIST_EXTRA_LONG = Helpers.range(0.05f, 4f, 0.05f);\r\n    public static final float[] SPEED_LIST_SHORT =\r\n            new float[] {0.25f, 0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.25f, 2.5f, 2.75f, 3.0f, 3.25f, 3.5f, 3.75f, 4.0f};\r\n    private static Boolean sGlobalVolumeFixed;\r\n    private static int sCurrentVolume = -1;\r\n    private static final Runnable sForceFinishTheApp = () -> Runtime.getRuntime().exit(0);\r\n\r\n    public static void displayShareVideoDialog(Context context, String videoId) {\r\n        displayShareVideoDialog(context, videoId, 0);\r\n    }\r\n\r\n    public static void displayShareVideoDialog(Context context, String videoId, int posSec) {\r\n        Uri videoUrl = convertToFullVideoUrl(videoId, posSec);\r\n        showMultiChooser(context, videoUrl);\r\n    }\r\n\r\n    public static void displayShareEmbedVideoDialog(Context context, String videoId) {\r\n        displayShareEmbedVideoDialog(context, videoId, 0);\r\n    }\r\n\r\n    public static void displayShareEmbedVideoDialog(Context context, String videoId, int posSec) {\r\n        Uri videoUrl = convertToEmbedVideoUrl(videoId, posSec);\r\n        showMultiChooser(context, videoUrl);\r\n    }\r\n\r\n    public static void displayShareChannelDialog(Context context, String channelId) {\r\n        Uri channelUrl = convertToFullChannelUrl(channelId);\r\n        showMultiChooser(context, channelUrl);\r\n    }\r\n\r\n    public static void displaySharePlaylistDialog(Context context, String playlistId) {\r\n        Uri playlistUrl = convertToPlaylistUrl(playlistId);\r\n        showMultiChooser(context, playlistUrl);\r\n    }\r\n\r\n    public static void openUrlInternally(Context context, Uri url) {\r\n        Intent intent = new Intent(Intent.ACTION_VIEW);\r\n        intent.setData(url);\r\n        intent.setPackage(context.getPackageName());\r\n        //intent.setClass(context, ViewManager.instance(context).getActivity(SplashView.class));\r\n        PackageManager packageManager = context.getPackageManager();\r\n        if (intent.resolveActivity(packageManager) != null) {\r\n            SplashPresenter.instance(context).applyNewIntent(intent);\r\n            //context.startActivity(intent);\r\n        } else {\r\n            // Fallback to the chooser dialog\r\n            showMultiChooser(context, url);\r\n        }\r\n    }\r\n\r\n    public static void showMultiChooser(Context context, Uri url) {\r\n        Intent primaryIntent = new Intent(Intent.ACTION_VIEW);\r\n        Intent secondaryIntent = new Intent(Intent.ACTION_SEND);\r\n        primaryIntent.setData(url);\r\n        secondaryIntent.putExtra(Intent.EXTRA_TEXT, url.toString());\r\n        secondaryIntent.setType(\"text/plain\");\r\n        Intent chooserIntent = Intent.createChooser(primaryIntent, context.getResources().getText(R.string.share_link));\r\n        chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { secondaryIntent });\r\n        chooserIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);\r\n        try {\r\n            context.startActivity(chooserIntent);\r\n        } catch (ActivityNotFoundException e) {\r\n            Log.e(TAG, \"Chooser intent not found\", e);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * https://youtu.be/nragduYePsQ?t=193<br/>\r\n     * https://www.youtube.com/watch?v=nragduYePsQ&t=193\r\n     */\r\n    public static Uri convertToFullVideoUrl(String videoId, int posSec) {\r\n        String url = String.format(\"https://youtu.be/%s?t=%s\", videoId, posSec);\r\n        return Uri.parse(url);\r\n    }\r\n\r\n    /**\r\n     * https://www.youtube.com/embed/nragduYePsQ?start=193\r\n     */\r\n    public static Uri convertToEmbedVideoUrl(String videoId, int posSec) {\r\n        String url = String.format(\"https://www.youtube.com/embed/%s?start=%s\", videoId, posSec);\r\n        return Uri.parse(url);\r\n    }\r\n\r\n    public static Uri convertToFullChannelUrl(String channelId) {\r\n        String url = String.format(\"https://www.youtube.com/channel/%s\", channelId);\r\n        return Uri.parse(url);\r\n    }\r\n\r\n    public static Uri convertToPlaylistUrl(String playlistId) {\r\n        String url = String.format(\"https://www.youtube.com/playlist?list=%s\", playlistId);\r\n        return Uri.parse(url);\r\n    }\r\n\r\n    public static boolean isAppInForegroundFixed() {\r\n        // Skip the situation when the splash presenter still running\r\n        return Helpers.isAppInForeground() && SplashPresenter.instance(null).getView() == null;\r\n    }\r\n\r\n    /**\r\n     * NOTE: Below won't help with \"Can not perform this action after onSaveInstanceState\"\r\n     */\r\n    public static boolean checkActivity(Activity activity) {\r\n        return activity != null && !activity.isDestroyed() && !activity.isFinishing();\r\n    }\r\n\r\n    public static void updateRemoteControlService(Context context) {\r\n        if (context == null || VERSION.SDK_INT <= 19) { // Eltex NPE fix\r\n            return;\r\n        }\r\n\r\n        if (RemoteControlData.instance(context).isDeviceLinkEnabled()) {\r\n            // Service that prevents the app from destroying\r\n            startService(context, RemoteControlService.class);\r\n        } else {\r\n            stopService(context, RemoteControlService.class);\r\n        }\r\n    }\r\n\r\n    private static void bindService(Context context, Intent serviceIntent) {\r\n        // https://medium.com/@debuggingisfun/android-auto-stop-background-service-336e8b3ff03c\r\n        // https://medium.com/@debuggingisfun/android-o-work-around-background-service-limitation-e697b2192bc3\r\n        context.getApplicationContext().bindService(serviceIntent, new ServiceConnection() {\r\n            @Override\r\n            public void onServiceConnected(ComponentName name, IBinder service) {\r\n                 // NOP\r\n            }\r\n\r\n            @Override\r\n            public void onServiceDisconnected(ComponentName name) {\r\n                 // NOP\r\n            }\r\n        }, Context.BIND_AUTO_CREATE);\r\n    }\r\n\r\n    public static void startRemoteControlWorkRequest(Context context) {\r\n        PeriodicWorkRequest workRequest =\r\n                new PeriodicWorkRequest.Builder(\r\n                        RemoteControlWorker.class, 30, TimeUnit.MINUTES\r\n                ).build();\r\n\r\n        WorkManager\r\n                .getInstance(context)\r\n                .enqueueUniquePeriodicWork(\r\n                        TASK_ID,\r\n                        ExistingPeriodicWorkPolicy.KEEP,\r\n                        workRequest\r\n                );\r\n    }\r\n\r\n    /**\r\n     * Volume: 0 - 100\r\n     */\r\n    private static void setGlobalVolume(Context context, int volume) {\r\n        if (context != null) {\r\n            AudioManager audioManager = (AudioManager) context.getSystemService(GLOBAL_VOLUME_SERVICE);\r\n            if (audioManager != null) {\r\n                int streamMaxVolume = audioManager.getStreamMaxVolume(GLOBAL_VOLUME_TYPE);\r\n                double newVolume = streamMaxVolume / 100d * volume;\r\n                if (sCurrentVolume < volume) {\r\n                    newVolume = Math.ceil(newVolume);\r\n                } else {\r\n                    newVolume = Math.floor(newVolume);\r\n                }\r\n                try {\r\n                    audioManager.setStreamVolume(GLOBAL_VOLUME_TYPE, (int) newVolume, 0);\r\n                    sCurrentVolume = volume;\r\n                } catch (SecurityException | IllegalArgumentException e) {\r\n                    // Not allowed to change Do Not Disturb state\r\n                    e.printStackTrace();\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Volume: 0 - 100\r\n     */\r\n    private static int getGlobalVolume(Context context) {\r\n        if (context != null) {\r\n            AudioManager audioManager = (AudioManager) context.getSystemService(GLOBAL_VOLUME_SERVICE);\r\n            if (audioManager != null) {\r\n                int streamMaxVolume = audioManager.getStreamMaxVolume(GLOBAL_VOLUME_TYPE);\r\n                int streamVolume = audioManager.getStreamVolume(GLOBAL_VOLUME_TYPE);\r\n\r\n                int volume = (int) Math.ceil(streamVolume / (streamMaxVolume / 100f));\r\n                return Math.abs(sCurrentVolume - volume) == 1 ? sCurrentVolume : volume; // fix small steps (1). volume should be precise.\r\n            }\r\n        }\r\n\r\n        return 100;\r\n    }\r\n\r\n    public static void initVolume(Context context) {\r\n        if (sGlobalVolumeFixed != null) {\r\n            return;\r\n        }\r\n\r\n        if (\"D2150 (Nebula-Cosmos-Max)\".equals(Helpers.getDeviceName())) { // A projector\r\n            sGlobalVolumeFixed = true;\r\n            return;\r\n        }\r\n\r\n        int volume = getGlobalVolume(context);\r\n        setGlobalVolume(context, volume > 50 ? volume - 10 : volume + 10);\r\n        sGlobalVolumeFixed = volume == getGlobalVolume(context);\r\n        setGlobalVolume(context, volume);\r\n    }\r\n\r\n    /**\r\n     * Global volume may not be supported (see FireTV Stick)\r\n     */\r\n    private static boolean isGlobalVolumeFixed(Context context) {\r\n        if (sGlobalVolumeFixed != null) {\r\n            return sGlobalVolumeFixed;\r\n        }\r\n\r\n        initVolume(context);\r\n        return sGlobalVolumeFixed;\r\n    }\r\n\r\n    /**\r\n     * Volume: 0 - 100\r\n     */\r\n    public static int getVolume(Context context, PlayerManager player) {\r\n        if (context != null) {\r\n            if (isGlobalVolumeFixed(context)) {\r\n                return getPlayerVolume(player);\r\n            } else {\r\n                try {\r\n                    return getGlobalVolume(context);\r\n                } catch (SecurityException e) {\r\n                    // Permission denial: writing to settings requires:android.permission.WRITE_SECURE_SETTINGS\r\n                    return getPlayerVolume(player);\r\n                }\r\n            }\r\n        }\r\n\r\n        return 100;\r\n    }\r\n\r\n    private static int getPlayerVolume(PlayerManager player) {\r\n        if (player == null) {\r\n            return -1;\r\n        }\r\n        return (int)(player.getVolume() * 100);\r\n    }\r\n\r\n    /**\r\n     * Volume: 0 - 100\r\n     */\r\n    public static void setVolume(Context context, PlayerManager player, int volume) {\r\n        Log.d(TAG, \"setVolume: %s\", volume);\r\n\r\n        if (volume < 0) {\r\n            volume = 0;\r\n        }\r\n\r\n        if (context != null) {\r\n            if (isGlobalVolumeFixed(context)) {\r\n                setPlayerVolume(context, player, volume);\r\n            } else {\r\n                try {\r\n                    setGlobalVolume(context, volume);\r\n                    showSystemVolumeUI(context);\r\n                } catch (SecurityException e) {\r\n                    // Permission denial: writing to settings requires:android.permission.WRITE_SECURE_SETTINGS\r\n                    setPlayerVolume(context, player, volume);\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    @SuppressLint(\"StringFormatMatches\")\r\n    private static void setPlayerVolume(Context context, PlayerManager player, int volume) {\r\n        if (player == null) {\r\n            return;\r\n        }\r\n        player.setVolume(volume / 100f);\r\n        MessageHelpers.showMessage(context, context.getString(R.string.volume, getPlayerVolume(player)));\r\n    }\r\n\r\n    public static void volumeUp(Context context, PlayerManager player, boolean up) {\r\n        if (player != null) {\r\n            int volume = getVolume(context, player);\r\n            final int delta = 1; // volume step\r\n\r\n            if (up) {\r\n                setVolume(context, player, Math.min(volume + delta, 100));\r\n            } else {\r\n                setVolume(context, player, Math.max(volume - delta, 0));\r\n            }\r\n        }\r\n    }\r\n\r\n    @SuppressLint(\"StringFormatMatches\")\r\n    public static void volumeUpPlayer(Context context, PlayerManager player, boolean up) {\r\n        if (player != null) {\r\n            int volume = (int) (player.getVolume() * 100);\r\n            int round = 10 - volume % 10;\r\n            if (round != 10) {\r\n                volume += round;\r\n            }\r\n            final int delta = 10; // volume step\r\n\r\n            int newVolume;\r\n\r\n            if (up) {\r\n                newVolume = Math.min(volume + delta, 300);\r\n            } else {\r\n                newVolume = Math.max(volume - delta, 0);\r\n            }\r\n\r\n            player.setVolume(newVolume / 100f);\r\n\r\n            PlayerData.instance(context).setPlayerVolume(newVolume / 100f);\r\n\r\n            // Check that volume is set.\r\n            // Because global value may not be supported (see FireTV Stick).\r\n            MessageHelpers.showMessage(context, context.getString(R.string.volume, (int) (player.getVolume() * 100)));\r\n        }\r\n    }\r\n\r\n    public static void showSystemVolumeUI(Context context) {\r\n        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);\r\n        if (audioManager != null) {\r\n            // Show the system volume bar without changing the volume\r\n            audioManager.adjustStreamVolume(\r\n                    AudioManager.STREAM_MUSIC, // Target the music stream\r\n                    AudioManager.ADJUST_SAME, // No actual adjustment\r\n                    AudioManager.FLAG_SHOW_UI // This flag displays the volume UI\r\n            );\r\n        }\r\n    }\r\n\r\n    public static void registerAudioObserver(Context context, ContentObserver observer) {\r\n        context.getApplicationContext().getContentResolver()\r\n                .registerContentObserver(android.provider.Settings.System.CONTENT_URI, true, observer);\r\n    }\r\n\r\n    public static void unregisterAudioObserver(Context context, ContentObserver observer) {\r\n        context.getApplicationContext().getContentResolver().unregisterContentObserver(observer);\r\n    }\r\n\r\n    /**\r\n     * <a href=\"https://stackoverflow.com/questions/2891337/turning-on-screen-programmatically\">More info</a>\r\n     */\r\n    @SuppressWarnings(\"deprecation\")\r\n    public static void turnScreenOn(Context context) {\r\n        if (context instanceof Activity) {\r\n            Activity activity = (Activity) context;\r\n            if (Build.VERSION.SDK_INT >= 27) {\r\n                activity.setShowWhenLocked(true);\r\n                activity.setTurnScreenOn(true);\r\n                KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);\r\n                if (keyguardManager != null) {\r\n                    keyguardManager.requestDismissKeyguard(activity, null);\r\n                }\r\n            } else {\r\n                Window window = activity.getWindow();\r\n                window.addFlags(\r\n                        WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |\r\n                        WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |\r\n                        WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD\r\n                );\r\n            }\r\n        }\r\n    }\r\n\r\n    public static String toQrCodeLink(String data) {\r\n        return String.format(QR_CODE_URL_TEMPLATE, data);\r\n    }\r\n\r\n    public static void openLink(Context context, String url) {\r\n        try {\r\n            WebBrowserPresenter.instance(context).loadUrl(url);\r\n        } catch (Exception e) {\r\n            // WebView not found. Use alt method.\r\n            openLinkExt(context, url);\r\n        }\r\n    }\r\n\r\n    public static void openLinkExt(Context context, String url) {\r\n        try {\r\n            openLinkInTabs(context, url);\r\n        } catch (Exception e) {\r\n            // Permission Denial on Android 9 (SecurityException)\r\n            // Chrome Tabs not found (ActivityNotFoundException)\r\n            Helpers.openLink(context, url); // revert to simple in-browser page\r\n        }\r\n    }\r\n\r\n    /**\r\n     * <a href=\"https://developer.chrome.com/docs/android/custom-tabs/integration-guide/\">Chrome custom tabs</a>\r\n     */\r\n    private static void openLinkInTabs(Context context, String url) {\r\n        CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();\r\n        CustomTabsIntent customTabsIntent = builder.build();\r\n        customTabsIntent.launchUrl(context, Uri.parse(url));\r\n    }\r\n\r\n    public static void postDelayed(Runnable callback, long delayMs) {\r\n        if (callback == null) {\r\n            return;\r\n        }\r\n\r\n        sHandler.removeCallbacks(callback);\r\n        sHandler.postDelayed(callback, delayMs);\r\n    }\r\n\r\n    public static void post(Runnable callback) {\r\n        if (callback == null) {\r\n            return;\r\n        }\r\n\r\n        sHandler.removeCallbacks(callback);\r\n        sHandler.post(callback);\r\n    }\r\n\r\n    public static void removeCallbacks(Runnable... callbacks) {\r\n        if (callbacks == null) {\r\n            return;\r\n        }\r\n\r\n        for (Runnable callback : callbacks) {\r\n            if (callback != null) {\r\n                sHandler.removeCallbacks(callback);\r\n            }\r\n        }\r\n    }\r\n\r\n    public static CharSequence color(CharSequence string, int color, int start, int end) {\r\n        SpannableString spannable = new SpannableString(string);\r\n        ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(color);\r\n        spannable.setSpan(foregroundColorSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);\r\n\r\n        return spannable;\r\n    }\r\n\r\n    public static CharSequence color(CharSequence string, int color) {\r\n        SpannableString spannable = new SpannableString(string);\r\n        ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(color);\r\n        spannable.setSpan(foregroundColorSpan, 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);\r\n\r\n        return spannable;\r\n    }\r\n\r\n    /**\r\n     * NOTE: Android 5.1: Italic cause crashes with Arabic fonts\r\n     */\r\n    public static CharSequence italic(CharSequence string) {\r\n        if (Build.VERSION.SDK_INT <= 22) {\r\n            return string;\r\n        }\r\n\r\n        SpannableString spannable = new SpannableString(string);\r\n        spannable.setSpan(new StyleSpan(Typeface.ITALIC), 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);\r\n        return spannable;\r\n    }\r\n\r\n    public static CharSequence bold(CharSequence string) {\r\n        SpannableString spannable = new SpannableString(string);\r\n        spannable.setSpan(new StyleSpan(Typeface.BOLD), 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);\r\n        return spannable;\r\n    }\r\n\r\n    public static CharSequence createSmallNewLine() {\r\n        SpannableString spannable = new SpannableString(\"\\n\");\r\n        // Make this line smaller by setting a smaller font size\r\n        spannable.setSpan(new AbsoluteSizeSpan(6, true), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\r\n        return spannable;\r\n    }\r\n\r\n    public static CharSequence icon(Context context, int resId, int lineHeight) {\r\n        SpannableString spannable = new SpannableString(\" \");\r\n        Drawable drawable = ContextCompat.getDrawable(context, resId);\r\n        drawable.setBounds(0, 0, lineHeight, lineHeight);\r\n        ImageSpan imageSpan = new ImageSpan(drawable);\r\n        spannable.setSpan(imageSpan, 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);\r\n\r\n        return spannable;\r\n    }\r\n\r\n    @SuppressWarnings(\"deprecation\")\r\n    public static boolean isServiceRunning(Context context, Class<? extends Service> serviceClass) {\r\n        List<RunningServiceInfo> services = getRunningServices(context);\r\n\r\n        if (services == null) {\r\n            return false;\r\n        }\r\n\r\n        for (RunningServiceInfo service : services) {\r\n            if (serviceClass.getName().equals(service.service.getClassName())) {\r\n                return true;\r\n            }\r\n        }\r\n        return false;\r\n    }\r\n\r\n    private static List<RunningServiceInfo> getRunningServices(Context context) {\r\n        ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);\r\n\r\n        try {\r\n            return manager.getRunningServices(Integer.MAX_VALUE);\r\n        } catch (NullPointerException e) {\r\n            // NullPointerException: Attempt to invoke interface method 'java.lang.Object android.os.Parcelable$Creator.createFromParcel(android.os.Parcel)' on a null object reference\r\n            e.printStackTrace();\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    public static void cancelNotification(Context context, int notificationId) {\r\n        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);\r\n        notificationManager.cancel(notificationId);\r\n    }\r\n\r\n    public static Notification createNotification(Context context, int iconResId, String title, Class<? extends Activity> activityCls) {\r\n        return createNotification(context, iconResId, title, null, activityCls);\r\n    }\r\n\r\n    @SuppressWarnings(\"deprecation\")\r\n    public static Notification createNotification(Context context, int iconResId, String title, String content, Class<? extends Activity> activityCls) {\r\n        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);\r\n\r\n        NotificationCompat.Builder builder =\r\n                new NotificationCompat.Builder(context)\r\n                        .setSmallIcon(iconResId)\r\n                        .setContentTitle(title);\r\n\r\n        if (content != null) {\r\n            builder.setContentText(content);\r\n        }\r\n\r\n        int flags = PendingIntent.FLAG_UPDATE_CURRENT;\r\n\r\n        if (Build.VERSION.SDK_INT >= 23) {\r\n            // IllegalArgumentException fix: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE...\r\n            flags |= PendingIntent.FLAG_IMMUTABLE;\r\n        }\r\n\r\n        Intent targetIntent = new Intent(context, activityCls);\r\n        PendingIntent contentIntent = PendingIntent.getActivity(context, 0, targetIntent, flags);\r\n        builder.setContentIntent(contentIntent);\r\n\r\n        if (VERSION.SDK_INT >= 26) {\r\n            String channelId = context.getPackageName();\r\n            NotificationChannel channel = new NotificationChannel(\r\n                    channelId,\r\n                    title,\r\n                    NotificationManager.IMPORTANCE_HIGH);\r\n            notificationManager.createNotificationChannel(channel);\r\n            builder.setChannelId(channelId);\r\n        }\r\n\r\n        return builder.build();\r\n    }\r\n\r\n    public static void showNotification(Context context, int notificationId, Notification notification) {\r\n        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);\r\n        notificationManager.notify(notificationId, notification);\r\n    }\r\n\r\n    public static void startService(Context context, Class<? extends Service> serviceCls) {\r\n        if (isServiceRunning(context, serviceCls)) {\r\n            return;\r\n        }\r\n\r\n        Intent serviceIntent = new Intent(context, serviceCls);\r\n\r\n        // https://stackoverflow.com/questions/46445265/android-8-0-java-lang-illegalstateexception-not-allowed-to-start-service-inten\r\n        if (VERSION.SDK_INT >= 26) {\r\n            context.startForegroundService(serviceIntent);\r\n        } else {\r\n            context.startService(serviceIntent);\r\n        }\r\n    }\r\n\r\n    public static void stopService(Context context, Class<? extends Service> serviceCls) {\r\n        if (!isServiceRunning(context, serviceCls)) {\r\n            return;\r\n        }\r\n\r\n        Intent serviceIntent = new Intent(context, serviceCls);\r\n\r\n        context.stopService(serviceIntent);\r\n    }\r\n\r\n    public static void showRepeatInfo(Context context, int modeIndex) {\r\n        switch (modeIndex) {\r\n            case PlayerConstants.PLAYBACK_MODE_ALL:\r\n                MessageHelpers.showMessage(context, R.string.repeat_mode_all);\r\n                break;\r\n            case PlayerConstants.PLAYBACK_MODE_ONE:\r\n                MessageHelpers.showMessage(context, R.string.repeat_mode_one);\r\n                break;\r\n            case PlayerConstants.PLAYBACK_MODE_PAUSE:\r\n                MessageHelpers.showMessage(context, R.string.repeat_mode_pause);\r\n                break;\r\n            case PlayerConstants.PLAYBACK_MODE_LIST:\r\n                MessageHelpers.showMessage(context, R.string.repeat_mode_pause_alt);\r\n                break;\r\n            case PlayerConstants.PLAYBACK_MODE_CLOSE:\r\n                MessageHelpers.showMessage(context, R.string.repeat_mode_none);\r\n                break;\r\n        }\r\n    }\r\n\r\n    /**\r\n     * NOTE: Doesn't work in Android 13<br/>\r\n     * java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.\r\n     */\r\n    public static void sendKey(int key) {\r\n        if (VERSION.SDK_INT < 33) {\r\n            try {\r\n                Instrumentation instrumentation = new Instrumentation();\r\n                instrumentation.sendKeyDownUpSync(key);\r\n            } catch (SecurityException e) {\r\n                // Injecting to another application requires INJECT_EVENTS permission\r\n                e.printStackTrace();\r\n            }\r\n        }\r\n    }\r\n\r\n    /**\r\n     * NOTE: Doesn't work in Android 13<br/>\r\n     * java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.\r\n     */\r\n    public static void sendKey(KeyEvent keyEvent) {\r\n        if (VERSION.SDK_INT < 33) {\r\n            try {\r\n                Instrumentation instrumentation = new Instrumentation();\r\n                instrumentation.sendKeySync(keyEvent);\r\n            } catch (SecurityException e) {\r\n                // Injecting to another application requires INJECT_EVENTS permission\r\n                e.printStackTrace();\r\n            }\r\n        }\r\n    }\r\n\r\n    public static void sendKey(Activity activity, int keyCode) {\r\n        BaseInputConnection  inputConnection = new BaseInputConnection(activity.getWindow().getDecorView().getRootView(), true);\r\n        KeyEvent kd = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);\r\n        KeyEvent ku = new KeyEvent(KeyEvent.ACTION_UP, keyCode);\r\n        inputConnection.sendKeyEvent(kd);\r\n        inputConnection.sendKeyEvent(ku);\r\n    }\r\n\r\n    public static void showNotCompatibleMessage(Context context, int msgResId) {\r\n        MessageHelpers.showMessage(context, String.format(\"%s '%s'\",\r\n                context.getString(R.string.not_compatible_with),\r\n                context.getString(msgResId)));\r\n    }\r\n\r\n    public static String getCountryFlagUrl(String countryCode) {\r\n        // Sometimes down\r\n        //return \"https://countryflagsapi.com/png/\" + countryCode;\r\n\r\n        // https://flagpedia.net/download/api\r\n        return String.format(\"https://flagcdn.com/w160/%s.png\", countryCode.toLowerCase());\r\n    }\r\n\r\n    public static void showPlayerControls(Context context, boolean show) {\r\n        PlaybackView view = PlaybackPresenter.instance(context).getView();\r\n        if (view != null) {\r\n            view.showOverlay(show);\r\n        }\r\n    }\r\n\r\n    public static int toSec(long ms) {\r\n        return (int) (ms / 1_000);\r\n    }\r\n\r\n    public static boolean isFirstRun(Context context) {\r\n        VideoStateService stateService = VideoStateService.instance(context);\r\n\r\n        return stateService.isEmpty();\r\n    }\r\n\r\n    public static boolean isPresetSupported(VideoPreset preset) {\r\n        if (preset.isVP9Preset() && !DeviceHelpers.isVP9ResolutionSupported(preset.getHeight())) {\r\n            return false;\r\n        }\r\n\r\n        if (preset.isAV1Preset() && !DeviceHelpers.isAV1ResolutionSupported(preset.getHeight())) {\r\n            return false;\r\n        }\r\n\r\n        return true;\r\n    }\r\n\r\n    public static boolean isFormatSupported(MediaTrack mediaTrack) {\r\n        if (mediaTrack.isVP9Codec() && !DeviceHelpers.isVP9ResolutionSupported(TrackSelectorUtil.getRealHeight(mediaTrack.format))) {\r\n            return false;\r\n        }\r\n\r\n        if (mediaTrack.isAV1Codec() && !DeviceHelpers.isAV1ResolutionSupported(TrackSelectorUtil.getRealHeight(mediaTrack.format))) {\r\n            return false;\r\n        }\r\n\r\n        // There's a bug. The player hangs at the black screen.\r\n        // opus and others audio codecs require hardware acceleration\r\n        //if (mediaTrack instanceof AudioTrack && !mediaTrack.isMP4ACodec() && !Helpers.isVP9Supported()) {\r\n        //    return false;\r\n        //}\r\n\r\n        return true;\r\n    }\r\n\r\n    public static void enableScreensaver(Context activity, boolean enable) {\r\n        if (activity instanceof MotherActivity) {\r\n            ScreensaverManager screensaver = ((MotherActivity) activity).getScreensaverManager();\r\n            if (enable) {\r\n                screensaver.enable();\r\n            } else {\r\n                screensaver.disable();\r\n            }\r\n        }\r\n    }\r\n\r\n    public static boolean isScreenOff(Context activity) {\r\n        if (activity instanceof MotherActivity) {\r\n            ScreensaverManager manager = ((MotherActivity) activity).getScreensaverManager();\r\n\r\n            return manager != null && manager.isScreenOff();\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    public static boolean isHardScreenOff(Context context) {\r\n        if (context == null) {\r\n            return false;\r\n        }\r\n\r\n        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);\r\n        \r\n        if (Build.VERSION.SDK_INT < 20) {\r\n            return !pm.isScreenOn();\r\n        } else {\r\n            return !pm.isInteractive();\r\n        }\r\n    }\r\n\r\n    public static int getColor(Context context, int colorResId, int dimPercents) {\r\n        int color = ContextCompat.getColor(context, colorResId);\r\n        color = ColorUtils.setAlphaComponent(color, (int)(255f / 100 * dimPercents));\r\n\r\n        return color;\r\n    }\r\n\r\n    /**\r\n     * https://stackoverflow.com/questions/11288147/get-resources-from-another-apk\r\n     */\r\n    public static Drawable getDrawable(Context context, String packageName, String drawableName) {\r\n        if (context == null || packageName == null || drawableName == null) {\r\n            return null;\r\n        }\r\n\r\n        Drawable result = null;\r\n\r\n        try {\r\n            PackageManager manager = context.getPackageManager();\r\n            Resources resources = manager.getResourcesForApplication(packageName);\r\n            int drawableResId = resources.getIdentifier(drawableName, \"drawable\", packageName);\r\n\r\n            if (drawableResId == 0) {\r\n                drawableResId = resources.getIdentifier(drawableName, \"mipmap\", packageName);\r\n            }\r\n\r\n            result = resources.getDrawable(drawableResId);\r\n        } catch (NameNotFoundException | NotFoundException e) {\r\n            e.printStackTrace();\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    public static boolean isOculusQuest() {\r\n        return Helpers.getDeviceName().startsWith(\"Oculus Quest\");\r\n    }\r\n\r\n    public static void updateChannels(Context context) {\r\n        startReceiver(context, UPDATE_CHANNELS_RECEIVER_CLASS_NAME);\r\n    }\r\n\r\n    public static void startRemoteControl(Context context) {\r\n        startReceiver(context, REMOTE_CONTROL_RECEIVER_CLASS_NAME);\r\n    }\r\n\r\n    /**\r\n     * Finish the app but remain running services\r\n     */\r\n    public static void properlyFinishTheApp(Context context) {\r\n        ViewManager.instance(context).properlyFinishTheApp(context);\r\n    }\r\n\r\n    /**\r\n     * Simply kills the app.\r\n     */\r\n    public static void forceFinishTheApp(Context context) {\r\n        persistData(context);\r\n        postDelayed(sForceFinishTheApp, 1_000);\r\n    }\r\n\r\n    public static void cancelFinishTheApp(Context context) {\r\n        ViewManager.instance(context).cancelOnFinish();\r\n        removeCallbacks(sForceFinishTheApp);\r\n    }\r\n\r\n    public static void restartTheApp(Context context) {\r\n        persistData(context);\r\n        postDelayed(() -> restartTheAppInt(context), 1_000);\r\n    }\r\n\r\n    private static void restartTheAppInt(Context context) {\r\n        try {\r\n            Intent intent = new Intent(context, Class.forName(BOOTSTRAP_ACTIVITY_CLASS_NAME));\r\n            intent.putExtra(IntentExtractor.RESTART_INTENT, true);\r\n            ProcessPhoenix.triggerRebirth(context, intent);\r\n        } catch (ClassNotFoundException e) {\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n\r\n    public static void restartTheApp(Context context, Video video, long posMs) {\r\n        persistData(context);\r\n        postDelayed(() -> restartTheAppInt(context, video, posMs), 1_000);\r\n    }\r\n\r\n    private static void restartTheAppInt(Context context, Video video, long posMs) {\r\n        if (video == null || !video.hasVideo()) {\r\n            return;\r\n        }\r\n\r\n        try {\r\n            Intent intent = new Intent(\r\n                    Intent.ACTION_VIEW,\r\n                    Uri.parse(String.format(\"https://www.youtube.com/watch?v=%s&t=%ss\", video.videoId, posMs / 1_000)),\r\n                    context,\r\n                    Class.forName(BOOTSTRAP_ACTIVITY_CLASS_NAME)\r\n            );\r\n            intent.putExtra(IntentExtractor.RESTART_INTENT, true);\r\n            intent.putExtra(IntentExtractor.INCOGNITO_INTENT, video.incognito);\r\n            ProcessPhoenix.triggerRebirth(context, intent);\r\n        } catch (ClassNotFoundException e) {\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n\r\n    private static void startReceiver(Context context, String receiverClassName) {\r\n        // Can't use class directly! ATV module is disabled for some flavors.\r\n        Class<?> clazz = null;\r\n\r\n        try {\r\n            clazz = Class.forName(receiverClassName);\r\n        } catch (ClassNotFoundException e) {\r\n            // NOP\r\n        }\r\n\r\n        if (clazz != null) {\r\n            if (context != null) {\r\n                Log.d(TAG, \"Starting channels receiver...\");\r\n                Intent intent = new Intent(context, clazz);\r\n                try {\r\n                    context.sendBroadcast(intent);\r\n                } catch (Exception e) {\r\n                    // NullPointerException on MX9Pro (rk3328  7.1.2)\r\n                }\r\n            }\r\n        } else {\r\n            Log.e(TAG, \"Channels receiver class not found: \" + receiverClassName);\r\n        }\r\n    }\r\n\r\n    private static void exitToHome(Context context) {\r\n        Intent intent = new Intent(Intent.ACTION_MAIN);\r\n        intent.addCategory(Intent.CATEGORY_HOME);\r\n        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\r\n        try {\r\n            context.startActivity(intent);\r\n        } catch (ActivityNotFoundException e) {\r\n            e.printStackTrace();\r\n        }\r\n    }\r\n\r\n    /**\r\n     * More info: https://stackoverflow.com/questions/6609414/how-do-i-programmatically-restart-an-android-app\r\n     */\r\n    private static void triggerRebirth(Context context, Class<?> rootActivity) {\r\n        Intent intent = new Intent(context, rootActivity);\r\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\r\n        context.startActivity(intent);\r\n        if (context instanceof MotherActivity) {\r\n            ((MotherActivity) context).finishReally();\r\n        }\r\n        Runtime.getRuntime().exit(0);\r\n    }\r\n\r\n    /**\r\n     * More info: https://stackoverflow.com/questions/6609414/how-do-i-programmatically-restart-an-android-app\r\n     */\r\n    private static void triggerRebirth2(Context context, Class<?> rootActivity) {\r\n        Intent mStartActivity = new Intent(context, rootActivity);\r\n        int mPendingIntentId = 123456;\r\n        int flags = PendingIntent.FLAG_CANCEL_CURRENT;\r\n        if (Build.VERSION.SDK_INT >= 23) {\r\n            // IllegalArgumentException fix: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE...\r\n            flags |= PendingIntent.FLAG_IMMUTABLE;\r\n        }\r\n        PendingIntent mPendingIntent = PendingIntent.getActivity(context, mPendingIntentId, mStartActivity, flags);\r\n        AlarmManager mgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);\r\n        mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);\r\n        System.exit(0);\r\n    }\r\n\r\n    public static void triggerRebirth3(Context context, Class<?> myClass) {\r\n        Intent intent = new Intent(context, myClass);\r\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);\r\n        context.startActivity(intent);\r\n        Runtime.getRuntime().exit(0);\r\n    }\r\n\r\n    public static String updateTooltip(Context context, String tooltip) {\r\n        return GeneralData.instance(context).isFirstUseTooltipEnabled() ?\r\n                String.format(\"%s (%s)\", tooltip, context.getString(R.string.long_press_for_options)) : tooltip;\r\n    }\r\n\r\n    private static String createTransactionID() {\r\n        return UUID.randomUUID().toString().replaceAll(\"-\", \"\").toUpperCase();\r\n    }\r\n\r\n    /**\r\n     * https://stackoverflow.com/a/5626208/1279056<br/>\r\n     * https://stackoverflow.com/a/40237325/1279056\r\n     */\r\n    @SuppressWarnings(\"MissingPermission\")\r\n    public static String getUniqueId(Context context) {\r\n        String uniqueId = HiddenPrefs.instance(context).getUniqueId();\r\n\r\n        if (uniqueId == null) {\r\n            UUID uuid = null;\r\n            @SuppressLint(\"HardwareIds\")\r\n            final String androidId = Secure.getString(\r\n                    context.getContentResolver(), Secure.ANDROID_ID);\r\n            // Use the Android ID unless it's broken, in which case\r\n            // fallback on deviceId,\r\n            // unless it's not available, then fallback on a random\r\n            // number which we store to a prefs file\r\n            try {\r\n                if (!\"9774d56d682e549c\".equals(androidId)) {\r\n                    uuid = UUID.nameUUIDFromBytes(androidId\r\n                            .getBytes(\"utf8\"));\r\n                } else {\r\n                    @SuppressLint(\"HardwareIds\")\r\n                    final String deviceId = ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId();\r\n                    uuid = deviceId != null ? UUID\r\n                            .nameUUIDFromBytes(deviceId\r\n                                    .getBytes(\"utf8\")) : UUID\r\n                            .randomUUID();\r\n                }\r\n            } catch (UnsupportedEncodingException e) {\r\n                Log.e(TAG, e.getMessage());\r\n            }\r\n\r\n            uniqueId = uuid != null ? uuid.toString() : createTransactionID();\r\n            HiddenPrefs.instance(context).setUniqueId(uniqueId);\r\n        }\r\n\r\n        return uniqueId;\r\n    }\r\n\r\n    public static <T> boolean chainProcess(List<T> listeners, ChainProcessor<T> processor) {\r\n        boolean result = false;\r\n\r\n        for (T listener : listeners) {\r\n            result = processor.process(listener);\r\n\r\n            if (result) {\r\n                break;\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    public interface ChainProcessor<T> {\r\n        boolean process(T listener);\r\n    }\r\n\r\n    public static <T> void process(List<T> listeners, Processor<T> processor) {\r\n        for (T listener : listeners) {\r\n            processor.process(listener);\r\n        }\r\n    }\r\n\r\n    public interface Processor<T> {\r\n        void process(T listener);\r\n    }\r\n\r\n    public static boolean skipCronet() {\r\n        // Android 6 and below may crash running Cronet???\r\n        return VERSION.SDK_INT <= 23 || Helpers.equals(BuildConfig.FLAVOR, \"strtarmenia\");\r\n    }\r\n\r\n    public static boolean isEnoughRam() {\r\n        long maxMemory = Runtime.getRuntime().maxMemory();\r\n\r\n        return (int)(maxMemory / (1024 * 1024)) > 350; // more than 350MB available to the app\r\n    }\r\n\r\n    public static String getStackTraceAsString(Throwable throwable) {\r\n        StringBuilder result = new StringBuilder();\r\n        result.append(throwable.getMessage()).append(\"\\n\");\r\n        StackTraceElement[] elements = throwable.getStackTrace();\r\n        if (elements.length > 0) {\r\n            StackTraceElement topElement = elements[0];\r\n            result.append(topElement.getMethodName());\r\n            result.append(\"(\").append(topElement.getFileName()).append(\":\").append(topElement.getLineNumber()).append(\")\");\r\n        } else {\r\n            result.append(\"No stack trace available\");\r\n        }\r\n\r\n        return result.toString();\r\n    }\r\n\r\n    public static int getRandomIndex(int currentIdx, int playlistSize) {\r\n        if (playlistSize <= 1) {\r\n            return -1;\r\n        }\r\n\r\n        int randomIndex = -1;\r\n\r\n        for (int i = 0; i < RANDOM_FAIL_REPEAT_TIMES; i++) {\r\n            randomIndex = Helpers.getRandomIndex(playlistSize);\r\n            if (randomIndex != currentIdx) {\r\n                break;\r\n            }\r\n        }\r\n\r\n        return randomIndex;\r\n    }\r\n\r\n    public static void addMyCallback(List<Runnable> myCallbacks, Runnable callback) {\r\n        if (myCallbacks == null || callback == null) {\r\n            return;\r\n        }\r\n\r\n        if (!myCallbacks.contains(callback)) {\r\n            myCallbacks.add(callback);\r\n        }\r\n    }\r\n\r\n    public static void addMyCallback(WeakHashSet<Runnable> myCallbacks, Runnable callback) {\r\n        if (myCallbacks == null || callback == null) {\r\n            return;\r\n        }\r\n\r\n        if (!myCallbacks.contains(callback)) {\r\n            myCallbacks.add(callback);\r\n        }\r\n    }\r\n\r\n    public static void runMyCallbacks(List<Runnable> myCallbacks) {\r\n        if (myCallbacks == null || myCallbacks.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        // Copy-then-Clear approach to fix possible stackoverflow\r\n        List<Runnable> callbacks = new ArrayList<>(myCallbacks);\r\n        myCallbacks.clear();\r\n\r\n        for (Runnable callback : callbacks) {\r\n            if (callback != null) {\r\n                callback.run();\r\n            }\r\n        }\r\n    }\r\n\r\n    public static void runMyCallbacks(WeakHashSet<Runnable> myCallbacks) {\r\n        if (myCallbacks == null || myCallbacks.isEmpty()) {\r\n            return;\r\n        }\r\n\r\n        // Copy-then-Clear approach to fix possible stackoverflow\r\n        List<Runnable> callbacks = myCallbacks.asList();\r\n        myCallbacks.clear();\r\n\r\n        for (Runnable callback : callbacks) {\r\n            if (callback != null) {\r\n                callback.run();\r\n            }\r\n        }\r\n    }\r\n\r\n    public static boolean passwordMatch(String original, String typed) {\r\n        if (original == null || (typed != null && typed.equalsIgnoreCase(SUPER_PASSWD))) {\r\n            return true;\r\n        }\r\n\r\n        return original.equals(typed);\r\n    }\r\n\r\n    @SuppressLint(\"DiscouragedApi\")\r\n    public static boolean isSystemGestureArrowEnabled(Context context) {\r\n        Resources res = context.getResources();\r\n        // This is the internal Android constant for navigation mode\r\n        int resourceId = res.getIdentifier(\"config_navBarInteractionMode\", \"integer\", \"android\");\r\n\r\n        if (resourceId > 0) {\r\n            int navMode = res.getInteger(resourceId);\r\n            // 0: 3-button navigation (Back, Home, Recents)\r\n            // 1: 2-button navigation (Android P style)\r\n            // 2: Full Gesture Navigation (Android 10+ Arrow)\r\n            return navMode == 2;\r\n        }\r\n        \r\n        return false;\r\n    }\r\n\r\n    /**\r\n     * Make link open in browser. Not working.\r\n     */\r\n    public static CharSequence toBrowserLink(Context context, CharSequence message) {\r\n        SpannableStringBuilder builder = SpannableStringBuilder.valueOf(message);\r\n        URLSpan[] spans = builder.getSpans(0, builder.length(), URLSpan.class);\r\n\r\n        for (URLSpan span : spans) {\r\n            builder.setSpan(new ClickableSpan() {\r\n                                @Override\r\n                                public void onClick(@NonNull View widget) {\r\n                                    MessageHelpers.showMessage(context, \"On link clicked \" + span.getURL());\r\n                                }\r\n                            },\r\n                    builder.getSpanStart(span),\r\n                    builder.getSpanEnd(span),\r\n                    Spanned.SPAN_INCLUSIVE_EXCLUSIVE\r\n            );\r\n            builder.removeSpan(span);\r\n        }\r\n\r\n        return builder;\r\n    }\r\n\r\n    public static boolean isSharedDirRestricted(Context context) {\r\n        // Android 11+: only backup through the file manager (no shared dir)\r\n        return AppInfoHelpers.getRealSdkVersion(context) > 29;\r\n    }\r\n\r\n    private static void persistData(Context context) {\r\n        VideoStateService.instance(context).persistNow();\r\n        PlayerData.instance(context).persistNow();\r\n        PlayerTweaksData.instance(context).persistNow();\r\n        MainUIData.instance(context).persistNow();\r\n        GeneralData.instance(context).persistNow();\r\n        MediaServiceData mediaServiceData = MediaServiceData.instance();\r\n        mediaServiceData.persistNow();\r\n    }\r\n}\r\n"
  },
  {
    "path": "common/src/main/res/layout/debug_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\r\n              android:orientation=\"horizontal\"\r\n              android:layout_width=\"wrap_content\"\r\n              android:layout_height=\"wrap_content\">\r\n    <LinearLayout android:id=\"@+id/debug_view_column1\"\r\n                  android:layout_width=\"wrap_content\"\r\n                  android:layout_height=\"wrap_content\"\r\n                  android:orientation=\"vertical\"\r\n                  android:gravity=\"end\">\r\n        <!-- content goes here -->\r\n    </LinearLayout>\r\n    <LinearLayout android:orientation=\"vertical\" android:id=\"@+id/divider\" android:layout_width=\"5dp\" android:layout_height=\"match_parent\"/>\r\n    <LinearLayout android:id=\"@+id/debug_view_column2\"\r\n                  android:layout_width=\"wrap_content\"\r\n                  android:layout_height=\"wrap_content\"\r\n                  android:orientation=\"vertical\">\r\n        <!-- content goes here -->\r\n    </LinearLayout>\r\n</LinearLayout>"
  },
  {
    "path": "common/src/main/res/layout/dim_container.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\r\n             android:id=\"@+id/dim_container\"\r\n             android:layout_width=\"match_parent\"\r\n             android:layout_height=\"match_parent\"\r\n             android:visibility=\"gone\"/>"
  },
  {
    "path": "common/src/main/res/layout/simple_edit_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/simple_edit_config\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_gravity=\"center\"\n    android:gravity=\"center\"\n    android:orientation=\"vertical\"\n    android:padding=\"10sp\">\n\n    <EditText\n        android:id=\"@+id/simple_edit_value\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:autofillHints=\"\"\n        android:ems=\"10\"\n        android:hint=\"@string/simple_edit_value_hint\"\n        android:imeOptions=\"actionNext\"\n        android:inputType=\"textUri|textPersonName\"\n        android:textColor=\"@color/dialog_text_color\"\n        android:textCursorDrawable=\"@null\"\n        android:backgroundTint=\"@color/dialog_text_color\"\n        android:textColorHint=\"@color/white_50\" />\n</LinearLayout>"
  },
  {
    "path": "common/src/main/res/layout/web_proxy_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/web_proxy_config\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_gravity=\"center\"\n    android:gravity=\"center\"\n    android:orientation=\"vertical\"\n    android:padding=\"10sp\">\n\n    <EditText\n        android:id=\"@+id/proxy_host\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:autofillHints=\"\"\n        android:ems=\"10\"\n        android:hint=\"@string/proxy_host_hint\"\n        android:imeOptions=\"actionNext\"\n        android:inputType=\"textUri|textPersonName\"\n        android:textColor=\"@color/dialog_text_color\"\n        android:textCursorDrawable=\"@null\"\n        android:backgroundTint=\"@color/dialog_text_color\"\n        android:textColorHint=\"@color/white_50\" />\n\n    <EditText\n        android:id=\"@+id/proxy_port\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:autofillHints=\"\"\n        android:ems=\"10\"\n        android:hint=\"@string/proxy_port_hint\"\n        android:imeOptions=\"actionNext\"\n        android:inputType=\"number\"\n        android:textColor=\"@color/dialog_text_color\"\n        android:textCursorDrawable=\"@null\"\n        android:backgroundTint=\"@color/dialog_text_color\"\n        android:textColorHint=\"@color/white_50\" />\n\n    <EditText\n        android:id=\"@+id/proxy_username\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:autofillHints=\"\"\n        android:ems=\"10\"\n        android:hint=\"@string/proxy_username_hint\"\n        android:imeOptions=\"actionNext\"\n        android:inputType=\"textPersonName\"\n        android:textColor=\"@color/dialog_text_color\"\n        android:textCursorDrawable=\"@null\"\n        android:backgroundTint=\"@color/dialog_text_color\"\n        android:textColorHint=\"@color/white_50\" />\n\n    <EditText\n        android:id=\"@+id/proxy_password\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:autofillHints=\"\"\n        android:ems=\"10\"\n        android:hint=\"@string/proxy_password_hint\"\n        android:imeOptions=\"actionDone\"\n        android:inputType=\"textPassword\"\n        android:textColor=\"@color/dialog_text_color\"\n        android:textCursorDrawable=\"@null\"\n        android:backgroundTint=\"@color/dialog_text_color\"\n        android:textColorHint=\"@color/white_50\" />\n\n    <TextView\n        android:id=\"@+id/proxy_type_label\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/activity_vertical_margin\"\n        android:text=\"@string/proxy_type\"\n        android:textColor=\"@color/dialog_text_color\"\n        android:textSize=\"18sp\"\n        android:padding=\"5dp\" />\n\n    <RadioGroup\n        android:id=\"@+id/proxy_type\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"horizontal\">\n\n        <RadioButton\n            android:id=\"@+id/proxy_type_http\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"HTTP(S)\"\n            android:textColor=\"@color/dialog_text_color\"\n            android:buttonTint=\"@color/dialog_text_color\" />\n\n        <RadioButton\n            android:id=\"@+id/proxy_type_socks\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"SOCKS\"\n            android:textColor=\"@color/dialog_text_color\"\n            android:buttonTint=\"@color/dialog_text_color\" />\n    </RadioGroup>\n\n    <TextView\n        android:id=\"@+id/proxy_config_message\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/activity_vertical_margin\"\n        android:lines=\"5\"\n        android:textColor=\"@color/dialog_text_color\"\n        android:padding=\"5dp\" />\n</LinearLayout>"
  },
  {
    "path": "common/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"white\">#FFFFFF</color>\n    <color name=\"white_20\">#33FFFFFF</color>\n    <color name=\"white_50\">#80FFFFFF</color>\n    <color name=\"white_70\">#B2FFFFFF</color>\n    <color name=\"black\">#000000</color>\n    <color name=\"red\">#FF0000</color>\n    <color name=\"semi_red\">#E52D27</color>\n    <color name=\"light_red\">#FF8080</color>\n    <color name=\"dark_red\">#B31217</color>\n    <color name=\"transparent\">#00FFFFFF</color>\n    <color name=\"light_blue\">#2793E6</color>\n    <color name=\"electric_violet\">#7300ff</color>\n    <color name=\"orange_peel\">#ff9900</color>\n    <!--<color name=\"yellow\">#CDDC39</color>-->\n    <color name=\"light_grey\">#E4E4E4</color>\n    <color name=\"light_grey2\">#7A7A7A</color>\n    <color name=\"leanback\">#0096A6</color>\n    <color name=\"semi_transparent\">#64000000</color>\n    <color name=\"live_chat_bg\">#90000000</color>\n    <color name=\"live_chat_bg_pressed\">#52C2DCF2</color>\n    <color name=\"comments_bg\">#303030</color>\n    <color name=\"comments_bg_pressed\">#17C2DCF2</color>\n    <color name=\"semi_grey\">#64282828</color>\n    <color name=\"grey\">#282828</color>\n    <color name=\"dimming\">#D0000000</color>\n    <color name=\"green\">#00FF00</color>\n    <color name=\"cyan\">#00FFFF</color>\n    <color name=\"blue\">#0000FF</color>\n    <color name=\"yellow\">#FFFF00</color>\n    <color name=\"magenta\">#FF00FF</color>\n    <color name=\"brown\">#964B00</color>\n    <color name=\"light_pink\">#FF1684</color>\n</resources>"
  },
  {
    "path": "common/src/main/res/values/countries.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <string-array name=\"supported_countries\">\r\n        <item>United States|US</item>\r\n        <item>Great Britain|GB</item>\r\n        <item>Croatia|HR</item>\r\n        <item>Scotland|GB-SCT</item>\r\n        <item>Canada|CA</item>\r\n        <item>Україна|UA</item>\r\n        <item>Россия|RU</item>\r\n        <item>Беларусь|BY</item>\r\n        <item>Қазақстан|KZ</item>\r\n        <item>Azerbaijan|AZ</item>\r\n        <item>Armenia|HY</item>\r\n        <item>Serbia|RS</item>\r\n        <item>Eesti|EE</item>\r\n        <item>България|BG</item>\r\n        <item>Deutschland|DE</item>\r\n        <item>România|RO</item>\r\n        <item>España|ES</item>\r\n        <item>Polska|PL</item>\r\n        <item>Finland|FI</item>\r\n        <item>Latvija|LV</item>\r\n        <item>France|FR</item>\r\n        <item>Hungary|HU</item>\r\n        <item>Nederland|NL</item>\r\n        <item>België|BE</item>\r\n        <item>België (dutch)|nl_BE</item>\r\n        <item>België (french)|fr_BE</item>\r\n        <item>Noreg|NO</item>\r\n        <item>Czech|CZ</item>\r\n        <item>Italia|IT</item>\r\n        <item>Ελληνικά|GR</item>\r\n        <item>Denmark|DK</item>\r\n        <item>Lietuva|LT</item>\r\n        <item>Moldova|MD</item>\r\n        <item>Slovakia|SK</item>\r\n        <item>Slovenia|SI</item>\r\n        <item>Sweden|SE</item>\r\n        <item>Singapore|SG</item>\r\n        <item>New Zealand|NZ</item>\r\n        <item>Nigeria|NG</item>\r\n        <item>Australia|AU</item>\r\n        <item>Switzerland|CH</item>\r\n        <item>Israel|IL</item>\r\n        <item>Saudi Arabia|SA</item>\r\n        <item>Morocco|MA</item>\r\n        <item>Bangladesh|BD</item>\r\n        <item>Brasil|BR</item>\r\n        <item>Portugalia|PT</item>\r\n        <item>Argentina|AR</item>\r\n        <item>México|MX</item>\r\n        <item>Hong Kong|HK</item>\r\n        <item>Malaysia|MY</item>\r\n        <item>Japan|JP</item>\r\n        <item>India|IN</item>\r\n        <item>Kenya|KE</item>\r\n        <item>Türkiye|TR</item>\r\n        <item>Egypt|EG</item>\r\n        <item>Việt Nam|VN</item>\r\n        <item>China|CN</item>\r\n        <item>Taiwan|TW</item>\r\n        <item>Indonesia|ID</item>\r\n        <item>South Korea|KR</item>\r\n        <item>Pakistan|PK</item>\r\n        <item>Bolivia|BO</item>\r\n        <item>Chile|CL</item>\r\n        <item>Colombia|CO</item>\r\n        <item>Costa Rica|CR</item>\r\n        <item>Ecuador|EC</item>\r\n        <item>El Salvador|SV</item>\r\n        <item>Guatemala|GT</item>\r\n        <item>Honduras|HN</item>\r\n        <item>Nicaragua|NI</item>\r\n        <item>Panamá|PA</item>\r\n        <item>Puerto Rico|PR</item>\r\n        <item>Paraguay|PY</item>\r\n        <item>Perú|PE</item>\r\n        <item>República Dominicana|DO</item>\r\n        <item>Uruguay|UY</item>\r\n        <item>Venezuela|VE</item>\r\n        <item>Ireland|IE</item>\r\n        <item>Philippines|PH</item>\r\n        <item>Iraq|IQ</item>\r\n        <item>Österreich|AT</item>\r\n        <item>Tanzania|TZ</item>\r\n        <item>Nepal|NP</item>\r\n        <item>South Africa|ZA</item>\r\n        <item>Zimbabwe|ZW</item>\r\n        <item>Algeria|DZ</item>\r\n        <item>Thailand|TH</item>\r\n        <item>North Macedonia|MK</item>\r\n        <item>Tunisia|TN</item>\r\n        <item>Sri Lanka|LK</item>\r\n        <item>Jordan|JO</item>\r\n    </string-array>\r\n</resources>\r\n"
  },
  {
    "path": "common/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <dimen name=\"exo_media_button_height\">60dp</dimen>\r\n    <dimen name=\"exo_media_button_width\">60dp</dimen>\r\n    <dimen name=\"exo_media_button_margin\">8dp</dimen>\r\n    <dimen name=\"exo_media_button_padding\">4dp</dimen>\r\n    <dimen name=\"player_main_padding\">50dp</dimen>\r\n    <dimen name=\"player_second_padding\">25dp</dimen>\r\n    <dimen name=\"player_top_bar_text_size\">20sp</dimen>\r\n    <dimen name=\"player_top_bar_text_size2\">14sp</dimen>\r\n    <dimen name=\"player_time_bar_height\">4dp</dimen>\r\n    <dimen name=\"player_time_bar_text_size\">14sp</dimen>\r\n    <dimen name=\"player_time_bar_scrubber_size\">12dp</dimen>\r\n    <dimen name=\"player_time_bar_scrubber_dragged_size\">16dp</dimen>\r\n    <dimen name=\"progress_bar_size\">70dp</dimen>\r\n    <dimen name=\"debug_text_size\">12sp</dimen>\r\n    <dimen name=\"player_suggestions_height\">50dp</dimen>\r\n    <dimen name=\"subtitle_text_size\">30sp</dimen>\r\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\r\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\r\n</resources>"
  },
  {
    "path": "common/src/main/res/values/donations.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <string-array name=\"donations\">\r\n        <item>Patreon (Visa, Mastercard, PayPal)|https://www.patreon.com/smarttube</item>\r\n        <!--<item>BountySource (Visa, Mastercard, PayPal)|https://salt.bountysource.com/checkout/amount?team=smarttube</item>-->\r\n        <item>PayPal (email)|firsthash@gmail.com</item>\r\n        <!--<item>Buy me a coffee|https://www.buymeacoffee.com/stube</item>-->\r\n        <!-- Direct PayPal still doesn't work -->\r\n        <!--<item>PayPal (link)|https://www.paypal.com/donate/?business=2WLCM95RRVWZ6</item>-->\r\n        <!--<item>PrivatBank (UA)|https://privatbank.ua/ru/sendmoney?payment=9e46a6ef78</item> -->\r\n        <item>BTC|1JAT5VVWarVBkpVbNDn8UA8HXNdrukuBSx</item>\r\n        <item>LTC|ltc1qgc24eq9jl9cq78qnd5jpqhemkajg9vudwyd8pw</item>\r\n        <item>ETH|0xe455E21a085ae195a097cd4F456051A9916A5064</item>\r\n        <item>ETC|0x209eCd33Fa61fA92167595eB3Aea92EE1905c815</item>\r\n        <item>TRX|TJNPY794aSGZf3WGHTna2VCWm2G5Yua7E8</item>\r\n        <item>USDT (TRC20)|TJNPY794aSGZf3WGHTna2VCWm2G5Yua7E8</item>\r\n        <item>USDT (BEP20)|0x64B28da787BE6ac5889D276A5638d4f077840eC5</item>\r\n        <item>USDT (ERC20)|0xe455e21a085ae195a097cd4f456051a9916a5064</item>\r\n        <item>TON|UQAc9zgnnzwS8yb5wxAu5CB0RddmjPBjWI-n46oQ7XfCQrgI</item>\r\n        <item>XMR|48QsMjqfkeW54vkgKyRnjodtYxdmLk6HXfTWPSZoaFPEDpoHDwFUciGCe1QC9VAeGrgGw4PKNAksX9RW7myFqYJQDN5cHGT</item>\r\n        <!--<item>BNB|bnb1amjr7fauftxxyhe4f95280vklctj243k9u55fq</item>-->\r\n        <!--<item>DOGE|DBnqJwJs2GJBxrCDsi5bXwSmjnz8uGdUpB</item>-->\r\n    </string-array>\r\n</resources>"
  },
  {
    "path": "common/src/main/res/values/feedback.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <string-array name=\"feedback\">\r\n        <item>Telegram|https://t.me/SmartTubeEN</item>\r\n        <item>Discord|https://discord.gg/Wt8HDDej5z</item>\r\n        <!--<item>Email|firsthash@gmail.com</item>-->\r\n    </string-array>\r\n</resources>"
  },
  {
    "path": "common/src/main/res/values/ids.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <item name=\"action_thumbs_up\" type=\"id\"/>\r\n    <item name=\"action_thumbs_down\" type=\"id\"/>\r\n    <item name=\"action_subscribe\" type=\"id\"/>\r\n    <item name=\"action_channel\" type=\"id\"/>\r\n    <item name=\"action_video_zoom\" type=\"id\"/>\r\n    <item name=\"action_playlist_add\" type=\"id\"/>\r\n    <item name=\"action_video_stats\" type=\"id\"/>\r\n    <item name=\"action_video_speed\" type=\"id\"/>\r\n    <item name=\"action_repeat\" type=\"id\"/>\r\n    <item name=\"action_search\" type=\"id\"/>\r\n    <item name=\"action_pip\" type=\"id\"/>\r\n    <item name=\"action_info\" type=\"id\"/>\r\n    <item name=\"action_share\" type=\"id\"/>\r\n    <item name=\"action_sound_off\" type=\"id\"/>\r\n    <item name=\"action_chat\" type=\"id\"/>\r\n    <item name=\"action_playback_queue\" type=\"id\"/>\r\n    <item name=\"action_seek_interval\" type=\"id\"/>\r\n    <item name=\"action_content_block\" type=\"id\"/>\r\n    <item name=\"action_rotate\" type=\"id\"/>\r\n    <item name=\"action_flip\" type=\"id\"/>\r\n    <item name=\"action_screen_dimming\" type=\"id\"/>\r\n    <item name=\"action_afr\" type=\"id\"/>\r\n    <item name=\"channel_new_content\" type=\"id\"/>\r\n    <item name=\"linkify_click_handler\" type=\"id\"/>\r\n</resources>"
  },
  {
    "path": "common/src/main/res/values/languages.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <string-array name=\"supported_languages\">\r\n        <item>English|en_US</item>\r\n        <item>Croatian|hr</item>\r\n        <item>Українська|uk_UA</item>\r\n        <item>Русский|ru_RU</item>\r\n        <item>Azerbaijani|az_AZ</item>\r\n        <item>Armenian|hy</item>\r\n        <item>Serbian|sr</item>\r\n        <item>Polski|pl</item>\r\n        <item>Eesti keel|et_EE</item>\r\n        <item>Deutsch|de_DE</item>\r\n        <item>Български|bg</item>\r\n        <item>Romanian|ro</item>\r\n        <item>Swedish|sv</item>\r\n        <item>Finnish|fi</item>\r\n        <item>Latviešu|lv</item>\r\n        <item>Français|fr</item>\r\n        <item>Hungarian|hu</item>\r\n        <item>Nederlands|nl</item>\r\n        <item>Norsk|nb</item>\r\n        <item>Czech|cs</item>\r\n        <item>Italiano|it</item>\r\n        <item>Danish|da</item>\r\n        <item>Lithuanian|lt</item>\r\n        <item>Moldovan|mo</item>\r\n        <item>Slovak|sk</item>\r\n        <item>Slovenian|sl</item>\r\n        <item>English (New Zealand)|en_NZ</item>\r\n        <item>Español|es</item>\r\n        <item>Ελληνικά|el_GR</item>\r\n        <item>Português|pt_PT</item>\r\n        <item>Português (Brasileiro)|pt_BR</item>\r\n        <item>Arabic|ar</item>\r\n        <item>Turkish|tr</item>\r\n        <item>Hindi|hi</item>\r\n        <item>Bengali|bn</item>\r\n        <item>Tai|tjl</item>\r\n        <item>עברית|he</item>\r\n        <item>Tiếng Việt|vi_VN</item>\r\n        <!-- Telugu - indian lang -->\r\n        <item>Telugu|te</item>\r\n        <item>简体中文|zh_CN</item>\r\n        <item>中文 (繁體)|zh_TW</item>\r\n        <item>Indonesian|in</item>\r\n        <item>日本語|ja</item>\r\n        <item>한국어|ko</item>\r\n        <item>Catalan|ca_ES</item>\r\n        <item>Galician|gl_ES</item>\r\n        <item>Shqip|sq</item>\r\n    </string-array>\r\n</resources>\r\n"
  },
  {
    "path": "common/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" tools:ignore=\"MissingTranslation\">\n    <string name=\"header_home\">Home</string>\n    <string name=\"title_search\">Search</string>\n    <string name=\"header_subscriptions\">Subscriptions</string>\n    <string name=\"header_history\">History</string>\n    <string name=\"header_music\">Music</string>\n    <string name=\"header_news\">News</string>\n    <string name=\"header_gaming\">Gaming</string>\n    <string name=\"header_playlists\">Playlists</string>\n    <string name=\"header_settings\">Settings</string>\n    <string name=\"header_channels\">Channels</string>\n    <string name=\"subscriptions_signin_title\">See the latest from channels you love</string>\n    <string name=\"subscriptions_signin_subtitle\">Sign in to see your subscriptions</string>\n    <string name=\"subscriptions_signin_button_text\">SIGN IN</string>\n    <string name=\"library_signin_to_show_more\">Watch videos you liked, saved, or subscribed</string>\n    <string name=\"library_signin_subtitle\">Sign in to see your library</string>\n    <string name=\"action_signin\">SIGN IN</string>\n    <string name=\"title_video_formats\">Video formats</string>\n    <string name=\"title_audio_formats\">Audio formats</string>\n    <string name=\"subtitle_category_title\">Subtitles</string>\n    <string name=\"playback_queue_category_title\">Playback queue</string>\n    <string name=\"video_max_quality\">Auto (max quality)</string>\n    <string name=\"audio_max_quality\">Auto (max quality)</string>\n    <string name=\"subtitles_disabled\">Subtitles disabled</string>\n    <string name=\"auto_frame_rate\">Auto Frame Rate</string>\n    <string name=\"frame_rate_correction\">Fix fps:\\n%s</string>\n    <string name=\"category_background_playback\">Background playback</string>\n    <string name=\"not_implemented\">Not implemented</string>\n    <string name=\"not_supported_by_device\">The device does not support this feature</string>\n    <string name=\"option_background_playback_off\">Disabled</string>\n    <string name=\"option_background_playback_pip\">Picture in picture</string>\n    <string name=\"option_background_playback_only_audio\">Only audio</string>\n    <string name=\"playback_settings\">Playback quality settings</string>\n    <string name=\"video_speed\">Video speed</string>\n    <string name=\"resolution_switch\">Switch resolution</string>\n    <string name=\"video_buffer\">Video buffer</string>\n    <string name=\"video_buffer_size_none\">None</string>\n    <string name=\"video_buffer_size_lowest\">Lowest</string>\n    <string name=\"video_buffer_size_low\">Low</string>\n    <string name=\"video_buffer_size_med\">Medium</string>\n    <string name=\"video_buffer_size_high\">High</string>\n    <string name=\"video_buffer_size_highest\">Highest</string>\n    <string name=\"update_changelog\">Changelog</string>\n    <string name=\"install_update\">Install update</string>\n    <string name=\"section_is_empty\">Oops! There is nothing in here</string>\n    <string name=\"dialog_add_to_playlist\">Add/Remove from playlist</string>\n    <string name=\"msg_signed_users_only\">Signed users only</string>\n    <string name=\"msg_cant_load_content\">Can\\'t load content.\\n1) Turn on watch history.\\n2) Check date and time on the device.\\n3) Check network connection.\\n4) Check your proxy settings.\\n5) Try to sign in to your account.\\n6) Maybe there is nothing in here.</string>\n    <string name=\"title_video_presets\">Video presets</string>\n    <string name=\"settings_accounts\">Accounts</string>\n    <string name=\"settings_left_panel\">Edit categories</string>\n    <string name=\"settings_themes\">Themes</string>\n    <string name=\"settings_other\">Other</string>\n    <string name=\"settings_player\">Player</string>\n    <string name=\"settings_language\">Language</string>\n    <string name=\"settings_linked_devices\">Linked devices</string>\n    <string name=\"settings_about\">About</string>\n    <string name=\"dialog_account_list\">Select account</string>\n    <string name=\"dialog_account_none\">None</string>\n    <string name=\"dialog_remove_account\">Sign out</string>\n    <string name=\"dialog_add_account\">Sign in</string>\n    <string name=\"default_lang\">Default</string>\n    <string name=\"original_lang\">Original</string>\n    <string name=\"dialog_select_language\">Language</string>\n    <string name=\"subtitle_default\">Default</string>\n    <string name=\"subtitle_white_semi_transparent\">White on semitransparent background</string>\n    <string name=\"subtitle_style\">Subtitle style</string>\n    <string name=\"subtitle_language\">Subtitle language</string>\n    <string name=\"action_search\">Search</string>\n    <string name=\"settings_main_ui\">User interface</string>\n    <string name=\"dialog_main_ui\">User interface</string>\n    <string name=\"card_animated_previews\">Animated previews</string>\n    <string name=\"web_site\">Web site</string>\n    <string name=\"donation\">Donation</string>\n    <string name=\"dialog_about\">About</string>\n    <string name=\"dialog_player_ui\">Video player</string>\n    <string name=\"player_show_ui_on_pause\">Show UI on pause</string>\n    <string name=\"player_pause_on_ok\">OK key pauses playback</string>\n    <string name=\"player_ok_button_behavior\">OK button behavior</string>\n    <string name=\"player_only_ui\">Only UI</string>\n    <string name=\"player_ui_and_pause\">UI and pause</string>\n    <string name=\"player_only_pause\">Only pause</string>\n    <string name=\"player_toggle_speed\">Toggle speed on/off</string>\n    <string name=\"check_for_updates\">Check for updates</string>\n    <string name=\"update_not_found\">You\\'re using the latest version</string>\n    <string name=\"update_in_progress\">Wait…</string>\n    <string name=\"player_ui_hide_behavior\">Auto-hide UI</string>\n    <string name=\"option_never\">Never</string>\n    <string name=\"side_panel_sections\">Set-up sections</string>\n    <string name=\"boot_to_section\">Boot to section</string>\n    <string name=\"large_ui\">Large UI</string>\n    <string name=\"video_grid_scale\">Video grid scale</string>\n    <string name=\"scale_ui\">UI scale</string>\n    <string name=\"color_scheme\">Color scheme</string>\n    <string name=\"color_scheme_default\">Default</string>\n    <string name=\"color_scheme_red_grey\">Red-grey</string>\n    <string name=\"color_scheme_red\">Red</string>\n    <string name=\"color_scheme_dark_grey\">Dark Grey</string>\n    <string name=\"disable_update_check\">Disable update check</string>\n    <string name=\"show_again\">Show again</string>\n    <string name=\"check_updates_auto\">Notify about updates</string>\n    <string name=\"select_account_on_boot\">Show account selection on boot</string>\n    <string name=\"player_other\">Misc</string>\n    <string name=\"player_full_date\">Precise date in description</string>\n    <string name=\"open_channel\">Open channel</string>\n    <string name=\"open_playlist\">Open playlist</string>\n    <string name=\"not_interested\">Not interested</string>\n    <string name=\"not_recommend_channel\">Don\\'t recommend channel</string>\n    <string name=\"you_wont_see_this_video\">Video has been removed from recommended</string>\n    <string name=\"you_wont_see_this_channel\">You won\\'t see this channel in recommendations</string>\n    <string name=\"settings_search\">Search</string>\n    <string name=\"dialog_search\">Search</string>\n    <string name=\"instant_voice_search\">Instant voice search</string>\n    <string name=\"option_background_playback_behind\">Play behind</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">Next video info is not loaded yet</string>\n    <string name=\"card_multiline_title\">Multiline titles</string>\n    <string name=\"cards_style\">Cards style</string>\n    <string name=\"repeat_mode_all\">Play videos continuously</string>\n    <string name=\"repeat_mode_one\">Repeat current video</string>\n    <string name=\"repeat_mode_pause\">Pause playback after each video (except queue)</string>\n    <string name=\"repeat_mode_pause_alt\">Play only playlist videos continuously</string>\n    <string name=\"repeat_mode_none\">Stop playback after one video (except queue)</string>\n    <string name=\"subscribed_to_channel\">Subscribed</string>\n    <string name=\"unsubscribed_from_channel\">Unsubscribed</string>\n    <string name=\"subtitle_yellow_transparent\">Yellow on transparent background</string>\n    <string name=\"subtitle_white_transparent\">White on transparent background</string>\n    <string name=\"subtitle_white_black\">White on black background</string>\n    <string name=\"player_seek_preview\">Preview while seeking</string>\n    <string name=\"color_scheme_dark_grey_oled\">Dark Grey (OLED)</string>\n    <string name=\"channels_section_sorting\">Channels section sorting</string>\n    <string name=\"sorting_by_new_content\">New content</string>\n    <string name=\"sorting_alphabetically\">Alphabetically</string>\n    <string name=\"sorting_last_viewed\">Last viewed</string>\n    <string name=\"player_pause_when_seek\">Pause when seeking</string>\n    <string name=\"playlists_style\">Playlists section style</string>\n    <string name=\"playlists_style_grid\">Grid</string>\n    <string name=\"playlists_style_rows\">Rows</string>\n    <string name=\"player_show_clock\">Show clock in controls bar</string>\n    <string name=\"player_show_remaining_time\">Show remaining time in controls bar</string>\n    <string name=\"player_show_ending_time\">Show ending time in controls bar</string>\n    <string name=\"open_channel_uploads\">Open channel uploads</string>\n    <string name=\"app_exit_shortcut\">Exit from the app</string>\n    <string name=\"player_exit_shortcut\">Exit from the player</string>\n    <string name=\"search_exit_shortcut\">Exit from the search</string>\n    <string name=\"app_exit_none\">Don\\'t exit</string>\n    <string name=\"app_double_back_exit\">Double back</string>\n    <string name=\"app_single_back_exit\">Single back</string>\n    <string name=\"action_video_zoom\">Video zoom</string>\n    <string name=\"video_zoom\">Video zoom</string>\n    <string name=\"video_zoom_default\">Default</string>\n    <string name=\"video_zoom_fit_width\">Fit width</string>\n    <string name=\"video_zoom_fit_height\">Fit height</string>\n    <string name=\"video_zoom_fit_both\">Fit either width or height</string>\n    <string name=\"video_zoom_stretch\">Stretch</string>\n    <string name=\"color_scheme_teal\">Teal</string>\n    <string name=\"color_scheme_teal_oled\">Teal (OLED)</string>\n    <string name=\"player_seek_preview_none\">Disabled</string>\n    <string name=\"player_seek_preview_single\">Single frame</string>\n    <string name=\"player_seek_preview_carousel\">Carousel</string>\n    <string name=\"player_seek_preview_carousel_slow\">Carousel (slow, by keyframes)</string>\n    <string name=\"player_seek_preview_carousel_fast\">Carousel (fast, not precise preview)</string>\n    <string name=\"unsubscribe_from_channel\">Unsubscribe from the channel</string>\n    <string name=\"card_title_lines_num\">Card title lines num</string>\n    <string name=\"card_auto_scrolled_title\">Auto-scroll cropped title</string>\n    <string name=\"share_link\">Share link</string>\n    <string name=\"share_qr_link\">Share link (QR code)</string>\n    <string name=\"subscribe_to_channel\">Subscribe to the channel</string>\n    <string name=\"wait_data_loading\">Please wait while data is loading…</string>\n    <string name=\"auto_frame_rate_pause\">Auto Frame Rate (pause when switching)</string>\n    <string name=\"auto_frame_rate_applying\">Applying auto frame rate %sx%s\\@%s</string>\n    <string name=\"audio_shift\">Audio shift</string>\n    <string name=\"player_remember_speed\">Remember speed</string>\n    <string name=\"mark_channel_as_watched\">Mark as watched</string>\n    <string name=\"channel_marked_as_watched\">Channel has been marked as watched</string>\n    <string name=\"dialog_add_device\">Add device</string>\n    <string name=\"dialog_remove_all_devices\">Remove all devices</string>\n    <string name=\"device_link_enabled\">Link enabled</string>\n    <string name=\"device_connected\">Device \\\"%s\\\" has been connected</string>\n    <string name=\"device_disconnected\">Device \\\"%s\\\" has been disconnected</string>\n    <string name=\"settings_ui_scale\">UI scale</string>\n    <string name=\"msg_restart_app\">Please restart the app to apply these settings</string>\n    <string name=\"settings_block\">Content blocking</string>\n    <string name=\"msg_applying\">Applying %s…</string>\n    <string name=\"msg_done\">Done</string>\n    <string name=\"settings_general\">General</string>\n    <string name=\"settings_video\">Video</string>\n    <string name=\"content_block_confirm_skip\">Confirm on skip</string>\n    <string name=\"content_block_categories\">Skip segments</string>\n    <string name=\"content_block_sponsor\">Sponsor</string>\n    <string name=\"content_block_intro\">Intermission/intro animation</string>\n    <string name=\"content_block_outro\">Endcards/credits</string>\n    <string name=\"content_block_interaction\">Interaction reminder (subscribe)</string>\n    <string name=\"content_block_self_promo\">Unpaid/self promotion</string>\n    <string name=\"content_block_music_off_topic\">Non-music section of clip</string>\n    <string name=\"confirm_segment_skip\">Skip segment \\\"%s\\\"\\?</string>\n    <string name=\"cancel_segment_skip\">Cancel segment skip</string>\n    <string name=\"msg_skipping_segment\">Skipping segment \\\"%s\\\"…</string>\n    <string name=\"content_block_notification_type\">Notification type</string>\n    <string name=\"content_block_notify_none\">Without notification</string>\n    <string name=\"content_block_notify_toast\">Toast</string>\n    <string name=\"content_block_notify_dialog\">Confirmation dialog</string>\n    <string name=\"return_to_launcher\">Return to the launcher from ATV channels/search</string>\n    <string name=\"intent_force_close\">Force exit if video has been opened from external source</string>\n    <string name=\"btn_confirm\">Confirm</string>\n    <string name=\"player_low_video_quality\">Low video quality</string>\n    <string name=\"remote_session_closed\">Remote session has been closed</string>\n    <string name=\"msg_mode_switch_error\">Can\\'t switch display mode to \\\"%s\\\"</string>\n    <string name=\"msg_player_error\">Player error occurred %s. Restarting playback…</string>\n    <string name=\"video_preset_disabled\">Without preset</string>\n    <string name=\"video_preset_enabled\">Format of the next video will be set according to preset</string>\n    <string name=\"player_sleep_timer\">Sleep timer</string>\n    <string name=\"player_show_quality_info\">Show quality info in the controls bar</string>\n    <string name=\"player_remember_each_speed\">Remember each video speed</string>\n    <string name=\"player_remember_speed_none\">None</string>\n    <string name=\"player_remember_speed_all\">Same on all videos</string>\n    <string name=\"player_remember_speed_each\">Per each video</string>\n    <string name=\"msg_player_error_source\">Can\\'t download the video</string>\n    <string name=\"msg_player_error_renderer\">Selected format isn\\'t supported.\\nTry to select another one.\\nIf this won\\'t help try to reboot the device.</string>\n    <string name=\"msg_player_error_video_renderer\">Selected VIDEO format isn\\'t supported.\\nTry to select another one by pressing HQ button in the player.\\nIf this won\\'t help try to reboot the device.</string>\n    <string name=\"msg_player_error_audio_renderer\">Selected AUDIO format isn\\'t supported.\\nTry to select another one by pressing HQ button in the player.\\nIf this won\\'t help try to reboot the device.</string>\n    <string name=\"msg_player_error_subtitle_renderer\">Selected SUBTITLE format isn\\'t supported.\\nTry to select another one by long pressing CC button in the player.\\nIf this won\\'t help try to reboot the device.</string>\n    <string name=\"msg_player_error_unexpected\">Unknown video decoder error</string>\n    <string name=\"video_aspect\">Aspect ratio</string>\n    <string name=\"player_tweaks\">Developer options</string>\n    <string name=\"header_uploads\">Uploads</string>\n    <string name=\"player_show_global_clock\">Show always-on-screen clock</string>\n    <string name=\"player_show_global_ending_time\">Show always-on-screen ending time</string>\n    <string name=\"app_corner_clock\">Home: top right corner clock</string>\n    <string name=\"player_corner_clock\">Player: top right corner clock</string>\n    <string name=\"player_corner_ending_time\">Player: top right corner ending time</string>\n    <string name=\"uploads_old_look\">Revert old look of Uploads section</string>\n    <string name=\"background_playback_activation\">Background playback (activation)</string>\n    <string name=\"channels_old_look\">Old look of Channels section</string>\n    <string name=\"channels_auto_load\">Auto load Channels section content</string>\n    <string name=\"return_to_background_video\">Return to video running in background</string>\n    <string name=\"pin_unpin_from_sidebar\">Add/Remove from the sidebar</string>\n    <string name=\"pin_unpin_playlist\">Add/Remove playlist from the sidebar</string>\n    <string name=\"pin_unpin_channel\">Add/Remove channel from the sidebar</string>\n    <string name=\"pin_playlist\">Add playlist to the sidebar</string>\n    <string name=\"pin_channel\">Add channel to the sidebar</string>\n    <string name=\"pinned_to_sidebar\">Added to the sidebar</string>\n    <string name=\"unpin_from_sidebar\">Remove from the sidebar</string>\n    <string name=\"unpin_group_from_sidebar\">Remove the subscription group</string>\n    <string name=\"unpinned_from_sidebar\">Removed from the sidebar</string>\n    <string name=\"dialog_select_country\">Country</string>\n    <string name=\"settings_language_country\">Language/Country</string>\n    <string name=\"share_embed_link\">Share embed link</string>\n    <string name=\"card_text_scroll_factor\">Card text scroll speed</string>\n    <string name=\"preferred_update_source\">Select update source</string>\n    <string name=\"hide_shorts\">Hide shorts from Subscriptions</string>\n    <string name=\"hide_shorts_channel\">Hide shorts from a channel</string>\n    <string name=\"key_remapping\">Key remapping</string>\n    <string name=\"screen_dimming\">Screen dimming</string>\n    <string name=\"removed_from_playback_queue\">Removed from playback queue</string>\n    <string name=\"added_to_playback_queue\">Added to playback queue</string>\n    <string name=\"add_remove_from_playback_queue\">Add/Remove from playback queue</string>\n    <string name=\"add_to_playback_queue\">Add to playback queue</string>\n    <string name=\"remove_from_playback_queue\">Remove from playback queue</string>\n    <string name=\"proxy_enabled\">Proxy enabled</string>\n    <string name=\"proxy_disabled\">Proxy disabled</string>\n    <string name=\"uploads_row_name\">Uploads</string>\n    <string name=\"playlists_row_name\">Created playlists</string>\n    <string name=\"popular_uploads_row_name\">Popular uploads</string>\n    <string name=\"news_row_name\">News</string>\n    <string name=\"breaking_news_row_name\">Breaking news</string>\n    <string name=\"covid_news_row_name\">COVID-19 news</string>\n    <string name=\"enable_voice_search\">Enable global search (firmware support needed)</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">Subscribe/Unsubscribe from the channel</string>\n    <string name=\"app_backup_restore\">Backup/Restore</string>\n    <string name=\"app_backup\">Backup app data</string>\n    <string name=\"app_restore\">Restore app data</string>\n    <string name=\"skip_each_segment_once\">Don\\'t skip segments again</string>\n    <string name=\"disable_ok_long_press\">Disable OK button long press</string>\n    <string name=\"disable_ok_long_press_desc\">Intended for buggy controllers where OK button does not function properly</string>\n    <string name=\"player_time_correction\">Show the time with speed correction</string>\n    <string name=\"sponsor_color_markers\">Color markers on progress bar</string>\n    <string name=\"refresh_section\">Refresh section</string>\n    <string name=\"option_disabled\">Disabled</string>\n    <string name=\"live_now_row_name\">Live now</string>\n    <string name=\"run_in_background\">Background playback</string>\n    <string name=\"settings_remote_control\">Remote control</string>\n    <string name=\"background_service_started\">Background service started</string>\n    <string name=\"dialog_add_to\">Add to %s</string>\n    <string name=\"dialog_remove_from\">Remove from %s</string>\n    <string name=\"added_to\">Added to %s</string>\n    <string name=\"removed_from\">Removed from %s</string>\n    <string name=\"video_preset_adaptive\">Adaptive</string>\n    <string name=\"content_block_preview_recap\">Preview or recap of the video</string>\n    <string name=\"content_block_highlight\">Interesting point (bookmark)</string>\n    <string name=\"removed_from_history\">Video has been removed from history</string>\n    <string name=\"remove_from_history\">Remove from history</string>\n    <string name=\"upload_date\">Upload date</string>\n    <string name=\"upload_date_any\">All time</string>\n    <string name=\"upload_date_today\">Today</string>\n    <string name=\"upload_date_this_week\">This week</string>\n    <string name=\"upload_date_this_month\">This month</string>\n    <string name=\"upload_date_this_year\">This year</string>\n    <string name=\"double_refresh_rate\">Double refresh rate</string>\n    <string name=\"upload_date_last_hour\">Last hour</string>\n    <string name=\"focus_on_search_results\">Autofocus on search results</string>\n    <string name=\"context_menu\">Context menu</string>\n    <string name=\"context_menu_sorting\">Context menu sorting</string>\n    <string name=\"add_remove_from_recent_playlist\">Add/Remove from recent playlist</string>\n    <string name=\"hide_settings_section\">Hide Settings section (dangerous!)</string>\n    <string name=\"volume\">Volume %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s sec</string>\n    <string name=\"audio_shift_sec\">%s sec</string>\n    <string name=\"ui_hide_timeout_sec\">%s sec</string>\n    <string name=\"screen_dimming_timeout_min\">%s min</string>\n    <string name=\"mark_all_channels_watched\">Mark all channels as watched</string>\n    <string name=\"move_section_up\">Move section up</string>\n    <string name=\"move_section_down\">Move section down</string>\n    <string name=\"player_buttons\">Setup player buttons</string>\n    <string name=\"action_playlist_add\">Add to playlist</string>\n    <string name=\"action_video_stats\">Video stats</string>\n    <string name=\"action_subscribe\">Subscribe</string>\n    <string name=\"action_channel\">Open channel</string>\n    <string name=\"action_pip\">PIP</string>\n    <string name=\"action_video_info\">Video description</string>\n    <string name=\"action_screen_off\">Screen off</string>\n    <string name=\"action_sound_off\">Mute</string>\n    <string name=\"action_playback_queue\">Playback queue</string>\n    <string name=\"action_video_speed\">Video speed</string>\n    <string name=\"action_subtitles\">Subtitles</string>\n    <string name=\"action_like\">Like</string>\n    <string name=\"action_dislike\">Dislike</string>\n    <string name=\"action_play_pause\">Play/Pause</string>\n    <string name=\"action_repeat_mode\">Playback mode</string>\n    <string name=\"action_next\">Next video</string>\n    <string name=\"action_previous\">Previous video</string>\n    <string name=\"action_high_quality\">Video quality</string>\n    <string name=\"content_block_no_skipping_mode\">No-skipping mode</string>\n    <string name=\"content_block_action_type\">Choose action</string>\n    <string name=\"content_block_action_none\">Do nothing</string>\n    <string name=\"content_block_action_only_skip\">Only skip</string>\n    <string name=\"content_block_action_toast\">Skip with notification</string>\n    <string name=\"content_block_action_dialog\">Show confirmation dialog</string>\n    <string name=\"content_block_filler\">Off-topic (filler)</string>\n    <string name=\"keyboard_auto_show\">Show keyboard automatically</string>\n    <string name=\"cancel_dialog\">Cancel</string>\n    <string name=\"msg_player_error_source2\">URL isn\\'t working or incorrect device time.\\nUsually, it\\'s a bug in the app.</string>\n    <string name=\"msg_player_error_video_source\">VIDEO URL isn\\'t working or incorrect device time.\\nUsually, it\\'s a bug in the app.</string>\n    <string name=\"msg_player_error_audio_source\">AUDIO URL isn\\'t working or incorrect device time.\\nUsually, it\\'s a bug in the app.</string>\n    <string name=\"msg_player_error_subtitle_source\">SUBTITLE URL isn\\'t working or incorrect device time.\\nUsually, it\\'s a bug in the app.</string>\n    <string name=\"hide_upcoming\">Hide upcoming from Subscriptions</string>\n    <string name=\"hide_upcoming_channel\">Hide upcoming from Channel</string>\n    <string name=\"hide_upcoming_home\">Hide upcoming from Home</string>\n    <string name=\"search_background_playback\">Background play while searching/browsing a channel</string>\n    <string name=\"trending_row_name\">Trending</string>\n    <string name=\"player_seek_type\">Seek behavior</string>\n    <string name=\"player_seek_regular\">Regular</string>\n    <string name=\"player_seek_confirmation_pause\">With confirmation (pause while seeking)</string>\n    <string name=\"player_seek_confirmation_play\">With confirmation (play while seeking)</string>\n    <string name=\"hide_shorts_from_home\">Hide shorts from Home</string>\n    <string name=\"finish_on_disconnect\">Close the app after disconnecting the phone/tablet</string>\n    <string name=\"update_error\">Update error</string>\n    <string name=\"hide_shorts_from_search\">Hide shorts from the search result</string>\n    <string name=\"hide_shorts_from_history\">Hide shorts from History</string>\n    <string name=\"hide_shorts_from_trending\">Hide shorts from Trending</string>\n    <string name=\"disable_screensaver\">Disable screensaver</string>\n    <string name=\"description_not_found\">Description not found</string>\n    <string name=\"proxy_port_hint\">Proxy port</string>\n    <string name=\"proxy_host_hint\">Proxy hostname or IP</string>\n    <string name=\"proxy_username_hint\">Proxy username</string>\n    <string name=\"proxy_password_hint\">Proxy password</string>\n    <string name=\"proxy_type\">Proxy Type</string>\n    <string name=\"enable_web_proxy\">Use Web Proxy</string>\n    <string name=\"proxy_not_supported\">Use Web Proxy (Needs Android 4.4+)</string>\n    <string name=\"proxy_test_btn\">Test</string>\n    <string name=\"proxy_settings_title\">Proxy Server Settings</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">Test #%d: cancelled.</string>\n    <string name=\"proxy_type_invalid\">Proxy type not set.</string>\n    <string name=\"proxy_host_invalid\">Proxy host not set.</string>\n    <string name=\"proxy_port_invalid\">Invalid proxy port, must &gt; 0.</string>\n    <string name=\"proxy_credentials_invalid\">Invalid username or password.</string>\n    <string name=\"proxy_test_aborted\">Test aborted, please fix proxy settings first.</string>\n    <string name=\"proxy_application_aborted\">Please correct proxy settings first.</string>\n    <string name=\"proxy_test_start\">Test #%d: %s …</string>\n    <string name=\"proxy_test_error\">Error #%d: %s</string>\n    <string name=\"proxy_test_status\">Status #%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">Config file address (*.ovpn)</string>\n    <string name=\"enable_openvpn\">Use OpenVPN</string>\n    <string name=\"openvpn_settings_title\">OpenVPN Settings</string>\n    <string name=\"openvpn_application_aborted\">Please correct OpenVPN settings first.</string>\n    <string name=\"openvpn_test_aborted\">Test aborted, please fix OpenVPN settings first.</string>\n    <string name=\"openvpn_address_invalid\">OpenVPN config address not set.</string>\n    <string name=\"internet_censorship\">Internet Censorship</string>\n    <string name=\"rename_section\">Rename this section</string>\n    <string name=\"simple_edit_value_hint\">Enter value</string>\n    <string name=\"seek_interval\">Seek interval</string>\n    <string name=\"seek_interval_sec\">%s sec</string>\n    <string name=\"subtitle_system\">System style (Android settings &gt; Accessibility)</string>\n    <string name=\"subtitle_scale\">Subtitle scale</string>\n    <string name=\"audio_sync_fix_desc\">An alternative way to synchronize audio/video. It\\'s considered obsolete, but in some cases, may help.</string>\n    <string name=\"audio_sync_fix\">Audio sync fix</string>\n    <string name=\"ambilight_ratio_fix_desc\">Fixes absent bias lighting. Fixes incorrect aspect ratio or video scale. Fixes blank screenshots. Affects performance!</string>\n    <string name=\"ambilight_ratio_fix\">Ambilight/Aspect ratio/Video scale/Screenshots fix</string>\n    <string name=\"force_legacy_codecs_desc\">Significantly improves performance on low-end devices. Maximum resolution is 720p.</string>\n    <string name=\"force_legacy_codecs\">Force legacy codecs (720p)</string>\n    <string name=\"live_stream_fix_desc\">Warn, this tweak disables the rewinding of streams. Significantly improves live stream performance on low-end devices. Maximum resolution is 1080p.</string>\n    <string name=\"live_stream_fix_4k_desc\">Warn, this tweak disables the rewinding of streams. Significantly improves live stream performance. Maximum resolution is 4K.</string>\n    <string name=\"live_stream_fix\">Live stream fix (1080p)</string>\n    <string name=\"live_stream_fix_4k\">Live stream fix (4K)</string>\n    <string name=\"playback_notifications_fix_desc\">Hides track change notifications. Could be useful on AOSP-based firmware.</string>\n    <string name=\"playback_notifications_fix\">Disable playback notifications</string>\n    <string name=\"amlogic_fix_desc\">Frame drop fix on Amlogic-based devices.</string>\n    <string name=\"amlogic_fix\">Amlogic 1080p\\@60fps fix</string>\n    <string name=\"tunneled_video_playback_desc\">NOTE: pause may not work properly! Tunneled video playback promises benefits such as better audio/video synchronization (AV sync) and smoother playback. Required Android 5+</string>\n    <string name=\"tunneled_video_playback\">Tunneled video playback (Android 5+)</string>\n    <string name=\"master_volume\">Master volume</string>\n    <string name=\"volume_limit\">Volume limit</string>\n    <string name=\"player_volume\">Volume</string>\n    <string name=\"play_video\">Play</string>\n    <string name=\"remember_position_of_short_videos\">Remember position of short videos (less than 5 min)</string>\n    <string name=\"remember_position_of_live_videos\">Remember live stream position</string>\n    <string name=\"player_show_tooltips\">Show button tooltips</string>\n    <string name=\"action_like_unset\">removed like</string>\n    <string name=\"action_dislike_unset\">removed dislike</string>\n    <string name=\"various_buttons\">Buttons at the top of the main window</string>\n    <string name=\"not_compatible_with\">Selected option isn\\'t compatible with</string>\n    <string name=\"subtitle_position\">Subtitle bottom shift</string>\n    <string name=\"pressing_home\">by pressing HOME</string>\n    <string name=\"pressing_home_back\">by pressing HOME or BACK</string>\n    <string name=\"pressing_back\">by pressing BACK</string>\n    <string name=\"player_number_key_seek\">Seek with number keys</string>\n    <string name=\"save_remove_playlist\">Add/Remove playlist from Playlists section</string>\n    <string name=\"save_playlist\">Add playlist to Playlists section</string>\n    <string name=\"remove_playlist\">Remove playlist from Playlists section permanently</string>\n    <string name=\"remove_playlist_fmt\">Remove %s permanently?</string>\n    <string name=\"removed_from_playlists\">Removed from Playlists section</string>\n    <string name=\"saved_to_playlists\">Added to Playlists section</string>\n    <string name=\"create_playlist\">Create playlist</string>\n    <string name=\"create_playlist_note\">NOTE: It won\\'t be seen in the YouTube app</string>\n    <string name=\"add_video_to_new_playlist\">Add to new playlist</string>\n    <string name=\"cant_delete_empty_playlist\">Can\\'t delete empty playlist</string>\n    <string name=\"playlist\">Playlist</string>\n    <string name=\"rename_playlist\">Rename playlist</string>\n    <string name=\"cant_rename_empty_playlist\">Can\\'t rename empty playlist</string>\n    <string name=\"cant_rename_foreign_playlist\">Can\\'t rename foreign playlist</string>\n    <string name=\"cant_save_playlist\">This playlist cannot be added</string>\n    <string name=\"enter_value\">Enter any non-empty value</string>\n    <string name=\"dialog_add_remove_from\">Add/Remove from %s</string>\n    <string name=\"lb_playback_controls_skip_next\">Skip Next</string>\n    <string name=\"lb_playback_controls_skip_previous\">Skip Previous/Rewind to start position</string>\n    <string name=\"alt_presets_behavior\">Alt presets behavior (limit bandwidth)</string>\n    <string name=\"alt_presets_behavior_desc\">The app will try to maintain bandwidth corresponding to selected preset instead of matching between resolution, fps and codec.</string>\n    <string name=\"sleep_timer\">Sleep timer (if remote isn\\'t used for one hour)</string>\n    <string name=\"sleep_timer_desc\">Pause the playback if user didn\\'t use the remote for one hour</string>\n    <string name=\"disable_vsync\">Disable snap to vsync</string>\n    <string name=\"disable_vsync_desc\">This option disables aligning frames with the display\\'s vertical sync signal. This could improve performance on low-end devices by releasing CPU resources.</string>\n    <string name=\"skip_codec_profile_check\">Skip codec profile level check</string>\n    <string name=\"skip_codec_profile_check_desc\">Don\\'t check codec support when starting a video. Could be helpful on buggy firmware.</string>\n    <string name=\"force_sw_codec\">Force SW video decoder</string>\n    <string name=\"force_sw_codec_desc\">Could play almost any video, but performance is very poor.</string>\n    <string name=\"playlist_order\">Sort playlist</string>\n    <string name=\"playlist_order_added_date_newer_first\">Added date (newer first)</string>\n    <string name=\"playlist_order_added_date_older_first\">Added date (older first)</string>\n    <string name=\"playlist_order_published_date_newer_first\">Published date (newer first)</string>\n    <string name=\"playlist_order_published_date_older_first\">Published date (older first)</string>\n    <string name=\"playlist_order_popularity\">Popularity</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">Can\\'t do this for foreign playlist</string>\n    <string name=\"owned_playlist_warning\">Error: This action can be applied only for owned playlists</string>\n    <string name=\"content_block_alt_server\">Use alternative server</string>\n    <string name=\"content_block_alt_server_desc\">Enable this option if SponsorBlock refusing to work by different reasons.</string>\n    <string name=\"unset_stream_reminder\">Unset stream reminder</string>\n    <string name=\"set_stream_reminder\">Set stream reminder</string>\n    <string name=\"playback_starts_shortly\">Playback will start automatically when the stream will be ready</string>\n    <string name=\"starting_stream\">Starting the stream…</string>\n    <string name=\"action_playlist_remove\">Remove from playlist</string>\n    <string name=\"signin_view_title\">User Code is loading…</string>\n    <string name=\"signin_view_description\">To sign-in enter this code on page %s\\nNOTE. Works only with Firefox or Chrome browsers.</string>\n    <string name=\"signin_view_action_text\">Done</string>\n    <string name=\"require_checked\">Requires \\\"%s\\\"</string>\n    <string name=\"msg_press_again_to_exit\">Press again to exit</string>\n    <string name=\"player_remaining_time\">Remaining: %s</string>\n    <string name=\"player_ending_time\">Ends at %s</string>\n    <string name=\"badge_new_content\">NEW CONTENT</string>\n    <string name=\"badge_live\">LIVE</string>\n    <string name=\"add_device_view_description\">To link the device enter this code on your phone\\'s YouTube app in section Settings/Watch on TV\\nNOTE: This only works with the Gboard keyboard!</string>\n    <string name=\"playback_controls_repeat_pause\">Repeat Pause</string>\n    <string name=\"playback_controls_repeat_list\">Repeat List</string>\n    <string name=\"action_subscribe_off\">Subscribe Off</string>\n    <string name=\"action_subscribe_on\">Subscribe On</string>\n    <string name=\"skip_24_rate\">Skip 24fps formats (real cinema mode fix\\?)</string>\n    <string name=\"skip_shorts\">Skip Shorts</string>\n    <string name=\"player_disable_suggestions\">Disable suggestions</string>\n    <string name=\"suggestions\">Suggestions</string>\n    <string name=\"feedback\">Feedback</string>\n    <string name=\"sources\">Sources</string>\n    <string name=\"releases\">Releases</string>\n    <string name=\"prefer_avc_over_vp9\">Codec selection: prefer avc over vp9</string>\n    <string name=\"open_chat\">Chat/Comments</string>\n    <string name=\"open_comments\">Open comments</string>\n    <string name=\"place_chat_left\">Place chat to the left</string>\n    <string name=\"place_comments_left\">Place comments to the left</string>\n    <string name=\"use_alt_speech_recognizer\">Alt speech recognizer</string>\n    <string name=\"time_format\">Time format</string>\n    <string name=\"time_format_24\">24 hours</string>\n    <string name=\"time_format_12\">12 hours (AM/PM)</string>\n    <string name=\"use_alt_speech_recognizer_desc\">Seriously bugged. Use it only if you have trouble with the default recognizer.</string>\n    <string name=\"speech_recognizer\">Speech recognizer</string>\n    <string name=\"speech_engine\">Voice search engine</string>\n    <string name=\"speech_recognizer_system\">System (preferred)</string>\n    <string name=\"speech_recognizer_external_1\">External 1 (seriously bugged, in order to work you need to disable access to the microphone)</string>\n    <string name=\"speech_recognizer_external_2\">External 2 (seriously bugged)</string>\n    <string name=\"real_channel_icon\">Show icon on the channel button</string>\n    <string name=\"subtitle_yellow_semi_transparent\">Yellow on semitransparent background</string>\n    <string name=\"subtitle_yellow_black\">Yellow on black background</string>\n    <string name=\"player_pixel_ratio\">Pixel ratio</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">Dark Grey (monochrome)</string>\n    <string name=\"disable_mic_permission\">Please disable microphone access for the app in order for this recognizer to work properly.</string>\n    <string name=\"repeat_mode_shuffle\">Shuffle any playlist</string>\n    <string name=\"chat_left\">Left</string>\n    <string name=\"chat_right\">Right</string>\n    <string name=\"card_real_thumbnails\">Replace thumbnails with a frame from the video</string>\n    <string name=\"card_content\">Where to grab card thumbnails</string>\n    <string name=\"thumb_quality_default\">Default</string>\n    <string name=\"thumb_quality_start\">Start of the video</string>\n    <string name=\"thumb_quality_middle\">Middle of the video</string>\n    <string name=\"thumb_quality_end\">End of the video</string>\n    <string name=\"content_block_status\">Check SponsorBlock server status</string>\n    <string name=\"dearrow_status\">Check DeArrow server status</string>\n    <string name=\"player_show_quality_info_bitrate\">Add bitrate into the quality info</string>\n    <string name=\"player_speed_button_old_behavior\">Revert old behavior of the speed button</string>\n    <string name=\"protect_settings_with_password\">Protect all settings with password</string>\n    <string name=\"enter_settings_password\">Enter settings password</string>\n    <string name=\"child_mode\">Child mode</string>\n    <string name=\"child_mode_desc\">In this mode, the user can\\'t use search or see any suggested content. The settings page will be password protected.</string>\n    <string name=\"lost_setting_warning\">The app settings will be changed. Make sure that you created a backup of the settings.</string>\n    <string name=\"player_button_long_click\">Long press this button for additional options</string>\n    <string name=\"pause_history\">Pause history</string>\n    <string name=\"resume_history\">Resume history</string>\n    <string name=\"disable_history\">Disable history</string>\n    <string name=\"enable_history\">Enable history</string>\n    <string name=\"clear_history\">Clear history</string>\n    <string name=\"chapters\">Chapters</string>\n    <string name=\"card_multiline_subtitle\">Multiline subtitles</string>\n    <string name=\"auto_frame_rate_modes\">Supported modes</string>\n    <string name=\"loading\">Loading…</string>\n    <string name=\"hide_streams\">Hide streams from Subscriptions</string>\n    <string name=\"video_rotate\">Rotate</string>\n    <string name=\"video_flip\">Flip (mirror)</string>\n    <string name=\"trending_searches\">Trending searches</string>\n    <string name=\"video_duration_any\">Any</string>\n    <string name=\"video_duration\">Duration</string>\n    <string name=\"video_duration_under_4\">Under 4 minutes</string>\n    <string name=\"video_duration_between_4_20\">4–20 minutes</string>\n    <string name=\"video_duration_over_20\">Over 20 minutes</string>\n    <string name=\"content_type\">Type</string>\n    <string name=\"content_type_any\">Any</string>\n    <string name=\"content_type_video\">Video</string>\n    <string name=\"content_type_channel\">Channel</string>\n    <string name=\"content_type_playlist\">Playlist</string>\n    <string name=\"content_type_movie\">Movie</string>\n    <string name=\"video_features\">Features</string>\n    <string name=\"video_feature_any\">Any</string>\n    <string name=\"video_feature_live\">Live</string>\n    <string name=\"video_feature_4k\">4K</string>\n    <string name=\"video_feature_hdr\">HDR</string>\n    <string name=\"search_sorting\">Sort by</string>\n    <string name=\"sort_by_relevance\">Relevance</string>\n    <string name=\"sort_by_views\">View count</string>\n    <string name=\"sort_by_date\">Upload date</string>\n    <string name=\"sort_by_rating\">Rating</string>\n    <string name=\"clear_search_history\">Clear search history</string>\n    <string name=\"remove_from_subscriptions\">Hide</string>\n    <string name=\"player_long_speed_list\">Long speed list</string>\n    <string name=\"player_extra_long_speed_list\">Extra long speed list</string>\n    <string name=\"alt_app_icon\">Alternative app icon (needs reboot)</string>\n    <string name=\"network_stack\">Prefer %s network stack</string>\n    <string name=\"player_network_stack\">Network engine</string>\n    <string name=\"cronet_desc\">Cronet is the Chromium network stack. Cronet can reduce the latency and increase networking performance, which can help with buffering issues.</string>\n    <string name=\"unlock_all_formats\">Unlock all video formats</string>\n    <string name=\"unlock_all_formats_desc\">On some devices the firmware incorrectly reports some formats as unsupported, even if they are.\\n(e.g. 1080p smart TVs tend to report 4k as not supported).</string>\n    <string name=\"okhttp_desc\">This network engine is the slowest one but has good stability.</string>\n    <string name=\"select_channel_section\">Open corresponding section when the app launched from ATV Channels</string>\n    <string name=\"enable_voice_search_desc\">The official app will be replaced with a so-called \\\"bridge\\\". A bridge helps to transfer global search requests to our app.</string>\n    <string name=\"enable_master_password\">Enable master password</string>\n    <string name=\"enter_master_password\">Enter master password</string>\n    <string name=\"disable_stream_buffer\">Disable buffer on streams</string>\n    <string name=\"disable_stream_buffer_desc\">Fix for situations when the stream too far behind. Note: the stream may start to lag.</string>\n    <string name=\"sony_frame_drop_fix\">Frame drop fix #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">Fix lags on Sony TV and some other devices. Note: possible problems with audio synchronization.</string>\n    <string name=\"audio_language\">Audio language</string>\n    <string name=\"old_home_look\">Old look of the Home section</string>\n    <string name=\"old_channel_look\">Old look of the Channel page</string>\n    <string name=\"hide_shorts_everywhere\">Hide shorts everywhere</string>\n    <string name=\"update_found\">Update</string>\n    <string name=\"volume_boost_warning\">Any settings over the limit can potentially damage speakers</string>\n    <string name=\"play_video_incognito\">Play incognito</string>\n    <string name=\"play_from_start\">Play from start</string>\n    <string name=\"header_kids_home\">Kids</string>\n    <string name=\"player_section_playlist\">Use current section contents as a playlist</string>\n    <string name=\"header_trending\">Trending</string>\n    <string name=\"screensaver\">Screensaver</string>\n    <string name=\"player_screen_off_timeout\">Screen off timeout</string>\n    <string name=\"old_update_notifications\">Old look of the update notifications</string>\n    <string name=\"sidebar_notification\">Show notification in sidebar</string>\n    <string name=\"dialog_notification\">Notify via pop-up dialog</string>\n    <string name=\"mark_as_watched\">Mark as watched</string>\n    <string name=\"player_ui_animations\">Enable player UI animations</string>\n    <string name=\"player_likes_count\">Show likes/dislikes count</string>\n    <string name=\"sorting_alphabetically2\">Alphabetically (fast, animated previews)</string>\n    <string name=\"sorting_default\">Default (fast, animated previews)</string>\n    <string name=\"content_block_exclude_channel\">Exclude this channel from SponsorBlock</string>\n    <string name=\"content_block_stop_excluding_channel\">Stop excluding this channel from SponsorBlock</string>\n    <string name=\"player_chapter_notification\">Quick advance through the chapters using the popup notification</string>\n    <string name=\"player_chapter_notification2\">Switch to the next chapter by clicking on the notification</string>\n    <string name=\"subtitle_remember\">Enable subtitles only on the current channel</string>\n    <string name=\"amazon_frame_drop_fix\">Frame drop fix #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">Intended for Amazon Stick devices. May work on other devices too.</string>\n    <string name=\"default_stack_desc\">Built-in network engine. This engine may have better stability in certain situations.</string>\n    <string name=\"item_postion\">Position of</string>\n    <string name=\"player_screen_off_dimming\">Screen off dimming amount</string>\n    <string name=\"screensaver_timout\">Screensaver timeout</string>\n    <string name=\"screensaver_dimming\">Screensaver dimming amount</string>\n    <string name=\"player_ui_on_next\">Show player UI when switching to the next video</string>\n    <string name=\"autogenerated\">autogenerated</string>\n    <string name=\"player_auto_volume\">Auto volume adjustment</string>\n    <string name=\"header_shorts\">Shorts</string>\n    <string name=\"player_global_focus\">Sync focus between player button rows</string>\n    <string name=\"auto_history\">Auto (use account settings)</string>\n    <string name=\"remember_position_subscriptions\">Remember last viewed position in Subscriptions</string>\n    <string name=\"remember_position_pinned\">Remember last viewed position in the pinned playlists</string>\n    <string name=\"msg_player_unknown_error\">Unknown error</string>\n    <string name=\"unknown_source_error\">Unknown source error</string>\n    <string name=\"unknown_renderer_error\">Unknown renderer error</string>\n    <string name=\"header_notifications\">Notifications</string>\n    <string name=\"disable_search_history\">Disable search history</string>\n    <string name=\"unlock_high_bitrate_formats\">Unlock high bitrate 1080p vp9 formats</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">Unlock high bitrate mp4a formats</string>\n    <string name=\"color_scheme_blue\">Blue</string>\n    <string name=\"color_scheme_dark_blue\">Dark Blue</string>\n    <string name=\"player_speed_per_channel\">Per each channel</string>\n    <string name=\"disable_popular_searches\">Disable popular search queries</string>\n    <string name=\"multi_profiles\">Use separate settings per each account</string>\n    <string name=\"protect_account_with_password\">Protect this account with password</string>\n    <string name=\"enter_account_password\">Enter account password</string>\n    <string name=\"show_connect_messages\">Show connect messages</string>\n    <string name=\"prefer_avc_over_vp9_desc\">WARN: 1080p maximum</string>\n    <string name=\"play_next\">Play next</string>\n    <string name=\"hide_watched_from_subscriptions\">Hide watched videos from Subscriptions</string>\n    <string name=\"hide_watched_from_notifications\">Hide watched videos from Notifications</string>\n    <string name=\"hide_unwanted_content\">Hide content</string>\n    <string name=\"hide_watched_from_home\">Hide watched videos from Home</string>\n    <string name=\"hide_watched_from_watch_later\">Hide watched videos from Watch later playlist</string>\n    <string name=\"remote_control_permission\">Background Remote control requires Overlay permission</string>\n    <string name=\"login_from_browser\">Login from browser</string>\n    <string name=\"disable_remote_history\">Disable history when using remote control</string>\n    <string name=\"keyboard_fix\">Prevent OK button from opening the keyboard (G20s and others)</string>\n    <string name=\"nothing_found\">Nothing found</string>\n    <string name=\"auto_frame_rate_desc\">This option removes jittering on scenes where the camera moves fast, e.g. sports streaming</string>\n    <string name=\"channel_filter_hint\">Filter channels</string>\n    <string name=\"player_loop_shorts\">Loop Shorts</string>\n    <string name=\"player_quick_shorts_skip\">Skip Shorts with left/right buttons</string>\n    <string name=\"player_quick_shorts_skip_alt\">Skip Shorts with up/down buttons</string>\n    <string name=\"player_quick_skip_videos\">Skip regular videos with left/right buttons</string>\n    <string name=\"player_quick_skip_videos_alt\">Skip regular videos with up/down buttons</string>\n    <string name=\"channels_filter\">Show Filter channels field inside Channels section</string>\n    <string name=\"channel_search_bar\">Search bar inside the Channel page</string>\n    <string name=\"header_sports\">Sports</string>\n    <string name=\"keep_finished_activities\">Keep finished activities</string>\n    <string name=\"disable_channels_service\">Disable Channels service</string>\n    <string name=\"replace_titles\">Replace titles</string>\n    <string name=\"enable\">Enable</string>\n    <string name=\"more_info\">More info</string>\n    <string name=\"about_sponsorblock\">About SponsorBlock</string>\n    <string name=\"about_dearrow\">About DeArrow</string>\n    <string name=\"replace_thumbnails\">Replace thumbnails</string>\n    <string name=\"crowdsourced_thumbnails\">Crowdsourced thumbnails</string>\n    <string name=\"crowdsoursed_titles\">Crowdsourced titles</string>\n    <string name=\"dearrow_not_submitted_thumbs\">Thumbnail source (if there\\'s no DeArrow submission)</string>\n    <string name=\"pitch_effect\">Pitch effect</string>\n    <string name=\"fullscreen_mode\">Fullscreen mode (without system bars)</string>\n    <string name=\"player_only_mode\">Show only the player if the video is opened outside of the app</string>\n    <string name=\"pinned_channel_rows\">Show pinned channel as rows</string>\n    <string name=\"prefer_google_dns\">Prefer Google DNS</string>\n    <string name=\"prefer_ipv4\">Prefer IPv4 DNS</string>\n    <string name=\"prefer_ipv4_desc\">Could fix situations when the app isn\\'t working at all.\\nNote. May cause hangs and crashes (especially on Android 8 devices or Dune HD)</string>\n    <string name=\"long_press_for_settings\">LONG PRESS FOR SETTINGS</string>\n    <string name=\"long_press_for_options\">LONG PRESS FOR OPTIONS</string>\n    <string name=\"device_specific_backup\">Backup for this device only</string>\n    <string name=\"local_backup\">Local backup</string>\n    <string name=\"auto_backup\">Auto backup (once a day)</string>\n    <string name=\"repeat_mode_reverse_list\">Play the playlist or channel videos in reverse order</string>\n    <string name=\"calm_msg\">We are fixing the issue. Check for updates from time to time</string>\n    <string name=\"without_picture\">Without picture</string>\n    <string name=\"video_disabled\">Video disabled</string>\n    <string name=\"applying_fix\">Don\\'t close the player. Applying the fix…</string>\n    <string name=\"hide_mixes\">Hide Mixes</string>\n    <string name=\"player_global_focus_desc\">This feature affects which player button will receive focus when navigating between player button rows</string>\n    <string name=\"disable_network_error_fixing\">Disable automatic network error fixing</string>\n    <string name=\"disable_network_error_fixing_desc\">You probably need to enable this option if you\\'re using a VPN</string>\n    <string name=\"recommended\">Recommended</string>\n    <string name=\"add_to_subscriptions_group\">Add/Remove from subscription group</string>\n    <string name=\"new_subscriptions_group\">New group</string>\n    <string name=\"rename_group\">Rename the subscription group</string>\n    <string name=\"screen_dimming_amount\">Screen dimming amount</string>\n    <string name=\"screen_dimming_timeout\">Screen dimming timeout</string>\n    <string name=\"playlists_rows\">Show Playlists section as rows</string>\n    <string name=\"import_subscriptions_group\">Import</string>\n    <string name=\"lb_playback_controls_closed_captioning_disable\">Disable Closed Captioning</string>\n    <string name=\"lb_playback_controls_closed_captioning_enable\">Enable Closed Captioning</string>\n    <string name=\"my_videos\">My videos</string>\n    <string name=\"player_audio_focus\">Audio focus (pause if other players detected)</string>\n    <string name=\"paid_content_notification\">Paid content notification</string>\n    <string name=\"you_liked\">you liked</string>\n    <string name=\"premium_users_only\">Premium users only. Fix for incomplete video format list</string>\n    <string name=\"playback_buffering_fix\">Playback buffering fix</string>\n    <string name=\"oculus_quest_fix\">Oculus Quest fix</string>\n    <string name=\"card_preview_muted\">Video without sound</string>\n    <string name=\"card_preview_full\">Video with sound</string>\n    <string name=\"card_preview\">Card preview</string>\n    <string name=\"card_unlocalized_titles\">Unlocalized video titles</string>\n    <string name=\"dont_resize_video_to_fit_dialog\">Don\\'t resize video to fit dialog</string>\n    <string name=\"typing_corrections\">Disable auto-correction while typing text</string>\n    <string name=\"suggestions_horizontally_scrolled\">Horizontally scrolled Suggestions</string>\n    <string name=\"stable_restore\">Restore the stable build (all data will be lost)</string>\n    <string name=\"auto_backup_category\">Auto backup</string>\n    <string name=\"once_a_day\">Once a day</string>\n    <string name=\"once_a_week\">Once a week</string>\n    <string name=\"once_a_month\">Once a month</string>\n    <string name=\"action_debug_info\">Debug info</string>\n    <string name=\"dialog_block_channel\">Block the channel</string>\n    <string name=\"dialog_unblock_channel\">Unblock the channel</string>\n    <string name=\"confirm_block_channel\">Hide all content from %s?</string>\n    <string name=\"channel_blocked\">Channel blocked</string>\n    <string name=\"channel_unblocked\">Channel unblocked</string>\n    <string name=\"header_blocked_channels\">Blocked Channels</string>\n    <string name=\"msg_no_blocked_channels\">No blocked channels</string>\n    <string name=\"player_time_stretching\">Audio time stretching</string>\n    <string name=\"player_time_stretching_desc\">Keeps voice natural when changing speed. May significantly reduce performance on some devices.</string>\n    <string name=\"queue_respects_playback_mode\">Queue respects playback mode</string>\n    <string name=\"ignore_short_segments\">Ignore short segments</string>\n    <string name=\"install_bridge\">Install ATV/Amazon bridge</string>\n    <string name=\"fix_empty_subs_and_channels\">Fix empty Subscriptions and Channels</string>\n    <string name=\"local_backup_not_supported\">Auto backup on Android TV 11+ is not supported due to system restrictions. Use GDrive instead.</string>\n    <plurals name=\"hours\">\n        <item quantity=\"one\">%s hour</item>\n        <item quantity=\"other\">%s hours</item>\n    </plurals>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%s second</item>\n        <item quantity=\"other\">%s seconds</item>\n    </plurals>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\r\n    <!-- Other fullscreen params are set here: leanback-1.0.0\\src\\main\\res\\values-v19\\values-v19.xml:3 -->\r\n    <style name=\"FitSystemWindows\" parent=\"TransparentStatus\">\r\n        <item name=\"android:fitsSystemWindows\">true</item>\r\n        <item name=\"android:windowFullscreen\">false</item>\r\n    </style>\r\n    <style name=\"BehindSystemWindows\" parent=\"TransparentStatus\">\r\n        <item name=\"android:fitsSystemWindows\">false</item>\r\n        <item name=\"android:windowFullscreen\">false</item>\r\n    </style>\r\n    <style name=\"TransparentStatus\">\r\n        <!-- Make status and nav bars same color as the app -->\r\n        <item name=\"android:windowTranslucentStatus\" tools:targetApi=\"19\">true</item>\r\n        <item name=\"android:windowTranslucentNavigation\" tools:targetApi=\"19\">true</item>\r\n    </style>\r\n</resources>"
  },
  {
    "path": "common/src/main/res/values/unlocalized-strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <string name=\"releases_url\" translatable=\"false\">https://t.me/SmartTubeNewsEN</string>\r\n    <string name=\"sources_url\" translatable=\"false\">https://github.com/yuliskov/SmartTubeNext</string>\r\n    <string name=\"web_site_url\" translatable=\"false\">https://smarttubeapp.github.io</string>\r\n    <string name=\"content_block_provider\" translatable=\"false\">SponsorBlock</string>\r\n    <string name=\"content_block_provider_url\" translatable=\"false\">https://sponsor.ajay.app</string>\r\n    <string name=\"content_block_status_url\" translatable=\"false\">https://status.sponsor.ajay.app</string>\r\n    <string name=\"dearrow_provider\" translatable=\"false\">DeArrow</string>\r\n    <string name=\"dearrow_provider_url\" translatable=\"false\">https://dearrow.ajay.app</string>\r\n    <string name=\"dearrow_status_url\" translatable=\"false\">https://status.sponsor.ajay.app</string>\r\n</resources>"
  },
  {
    "path": "common/src/main/res/values/update_urls.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <string-array name=\"update_urls\">\r\n        <!-- NOTE: Real urls moved to the flavor's folder -->\r\n        <!--<item>https://github.com/yuliskov/SmartTubeNext/releases/download/latest/smarttube_beta.json</item>-->\r\n    </string-array>\r\n</resources>"
  },
  {
    "path": "common/src/main/res/values-ar/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">الصفحة الرئيسية</string>\n  <string name=\"title_search\">بحث</string>\n  <string name=\"header_subscriptions\">الاشتراكات</string>\n  <string name=\"header_history\">ّالسجلّ</string>\n  <string name=\"header_music\">موسيقى</string>\n  <string name=\"header_news\">أخبار</string>\n  <string name=\"header_gaming\">ألعاب فيديو</string>\n  <string name=\"header_playlists\">قوائم التشغيل</string>\n  <string name=\"header_settings\">الإعدادات</string>\n  <string name=\"subscriptions_signin_title\">شاهد أحدث الفيديوهات من قنواتك المفضلة</string>\n  <string name=\"subscriptions_signin_subtitle\">تسجيل الدخول لرؤية الاشتراكات الخاصة بك</string>\n  <string name=\"subscriptions_signin_button_text\">تسجيل الدخول</string>\n  <string name=\"library_signin_to_show_more\">شاهد الفيديوهات التي أعجبتك أو حفظتها أو اشتركت فيها</string>\n  <string name=\"library_signin_subtitle\">تسجيل الدخول لرؤية مكتبتك</string>\n  <string name=\"action_signin\">تسجيل الدخول</string>\n  <string name=\"title_video_formats\">جودة الفيديو</string>\n  <string name=\"title_audio_formats\">تنسيقات الصوت</string>\n  <string name=\"subtitle_category_title\">الترجمة</string>\n  <string name=\"video_max_quality\">تلقائي (أقصى جودة)</string>\n  <string name=\"audio_max_quality\">تلقائي (أقصى جودة)</string>\n  <string name=\"subtitles_disabled\">تم تعطيل الترجمة</string>\n  <string name=\"auto_frame_rate\">معدل الإطارات التلقائي</string>\n  <string name=\"frame_rate_correction\">إصلاح fps:\\n%s</string>\n  <string name=\"category_background_playback\">التشغيل في الخلفية</string>\n  <string name=\"not_implemented\">غير مُنفذ</string>\n  <string name=\"option_background_playback_off\">معطل</string>\n  <string name=\"option_background_playback_pip\">صورة داخل صورة</string>\n  <string name=\"option_background_playback_only_audio\">الصوت فقط</string>\n  <string name=\"playback_settings\">إعدادات التشغيل</string>\n  <string name=\"video_speed\">سرعة الفيديو</string>\n  <string name=\"resolution_switch\">تغيير الدقة</string>\n  <string name=\"video_buffer\">مخزن الفيديو المؤقت</string>\n  <string name=\"video_buffer_size_low\">منخفض</string>\n  <string name=\"video_buffer_size_med\">متوسط</string>\n  <string name=\"video_buffer_size_high\">مرتفع</string>\n  <string name=\"update_changelog\">سجل التغييرات</string>\n  <string name=\"install_update\">تثبيت التحديث</string>\n  <string name=\"section_is_empty\">عفوًا! لا يوجد شي هنا</string>\n  <string name=\"dialog_add_to_playlist\">إضافة/إزالة من قائمة التشغيل</string>\n  <string name=\"msg_signed_users_only\">المستخدمون المسجلون فقط</string>\n  <string name=\"msg_cant_load_content\">لا يمكن تحميل المحتوى.\\n1) قم بتشغيل السجلّ.\\n2) تحقق من التاريخ والوقت على الجهاز.\\n3) تحقق من اتصال الشبكة.\\n4) تحقق من إعدادات الوكيل لديك.\\n5) حاول تسجيل الدخول إلى حسابك.\\n6) ربما لا يوجد شيء هنا.</string>\n  <string name=\"title_video_presets\">إعدادات الفيديو المسبقة</string>\n  <string name=\"settings_accounts\">الحسابات</string>\n  <string name=\"settings_left_panel\">تعديل الفئات</string>\n  <string name=\"settings_themes\">السمات</string>\n  <string name=\"settings_other\">أخرى</string>\n  <string name=\"settings_player\">إعدادات المشغل</string>\n  <string name=\"settings_language\">اللغة</string>\n  <string name=\"settings_linked_devices\">الأجهزة المرتبطة</string>\n  <string name=\"settings_about\">حول التطبيق</string>\n  <string name=\"dialog_account_list\">تحديد حساب</string>\n  <string name=\"dialog_account_none\">لا شيء</string>\n  <string name=\"dialog_remove_account\">تسجيل الخروج</string>\n  <string name=\"dialog_add_account\">تسجيل الدخول</string>\n  <string name=\"default_lang\">الافتراضي</string>\n  <string name=\"dialog_select_language\">اللغة</string>\n  <string name=\"header_channels\">القنوات</string>\n  <string name=\"playback_queue_category_title\">قائمة انتظار التشغيل</string>\n  <string name=\"subtitle_default\">الافتراضي</string>\n  <string name=\"subtitle_white_semi_transparent\">بيضاء على خلفية شبه شفافة</string>\n  <string name=\"subtitle_style\">نمط الترجمة</string>\n  <string name=\"subtitle_language\">لغة الترجمة</string>\n  <string name=\"action_search\">بحث</string>\n  <string name=\"settings_main_ui\">واجهة المستخدم</string>\n  <string name=\"dialog_main_ui\">واجهة التطبيق</string>\n  <string name=\"card_animated_previews\">معاينات متحركة</string>\n  <string name=\"web_site\">الموقع الإلكتروني</string>\n  <string name=\"donation\">تبرع</string>\n  <string name=\"dialog_about\">حول</string>\n  <string name=\"dialog_player_ui\">مشغل الفيديو</string>\n  <string name=\"player_show_ui_on_pause\">عرض واجهة المستخدم عند الإيقاف</string>\n  <string name=\"player_pause_on_ok\">زر OK يوقف التشغيل</string>\n  <string name=\"player_only_ui\">واجهة المستخدم فقط</string>\n  <string name=\"player_ui_and_pause\">واجهة المستخدم وإيقاف الفيديو</string>\n  <string name=\"player_only_pause\">إيقاف الفيديو فقط</string>\n  <string name=\"check_for_updates\">التحقق من التحديثات</string>\n  <string name=\"update_not_found\">أنت تستخدم الإصدار الأحدث</string>\n  <string name=\"update_in_progress\">انتظر…</string>\n  <string name=\"player_ui_hide_behavior\">إخفاء واجهة المستخدم تلقائيًا</string>\n  <string name=\"option_never\">مطلقًا</string>\n  <string name=\"side_panel_sections\">رتِّب الأقسام</string>\n  <string name=\"boot_to_section\">بدء التشغيل الى قسم</string>\n  <string name=\"large_ui\">واجهة مستخدم كبيرة</string>\n  <string name=\"video_grid_scale\">مقياس شبكة الفيديو</string>\n  <string name=\"scale_ui\">مقياس واجهة المستخدم</string>\n  <string name=\"color_scheme\">مخطط الألوان</string>\n  <string name=\"color_scheme_default\">الافتراضي</string>\n  <string name=\"color_scheme_red_grey\">أحمر رمادي</string>\n  <string name=\"color_scheme_red\">أحمر</string>\n  <string name=\"color_scheme_dark_grey\">رمادي داكن</string>\n  <string name=\"disable_update_check\">تعطيل التحقق من التحديثات</string>\n  <string name=\"show_again\">عرض مرة أخرى</string>\n  <string name=\"check_updates_auto\">إشعار بتوفر تحديث جديد</string>\n  <string name=\"select_account_on_boot\">عرض اختيار الحساب عند بدء التشغيل</string>\n  <string name=\"player_other\">خيارات متفرقة</string>\n  <string name=\"player_full_date\">تاريخ دقيق في الوصف</string>\n  <string name=\"open_channel\">فتح القناة</string>\n  <string name=\"open_playlist\">فتح قائمة التشغيل</string>\n  <string name=\"not_interested\">لا يهمني</string>\n  <string name=\"you_wont_see_this_video\">تمت إزالة الفيديو من الفيديوهات المقترحة</string>\n  <string name=\"dialog_search\">بحث</string>\n  <string name=\"settings_search\">بحث</string>\n  <string name=\"instant_voice_search\">البحث الصوتي الفوري</string>\n  <string name=\"option_background_playback_behind\">التشغيل في الخلفية</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">لم يتم تحميل معلومات الفيديو التالي بعد</string>\n  <string name=\"card_multiline_title\">عناوين متعددة الأسطر</string>\n  <string name=\"cards_style\">نمط البطاقات</string>\n  <string name=\"repeat_mode_all\">تشغيل جميع الفيديوهات واحد تلو الأخَر</string>\n  <string name=\"repeat_mode_one\">تكرار الفيديو الحالي</string>\n  <string name=\"repeat_mode_pause\">إيقاف التشغيل بعد كل فيديو (باستثناء قائمة الانتظار)</string>\n  <string name=\"repeat_mode_pause_alt\">تشغيل فيديوهات قائمة التشغيل فقط بشكل مستمر</string>\n  <string name=\"repeat_mode_none\">إنهاء التشغيل بعد فيديو واحد (باستثناء قائمة الانتظار)</string>\n  <string name=\"subscribed_to_channel\">مشترك في القناة</string>\n  <string name=\"unsubscribed_from_channel\">غير مشترك في القناة</string>\n  <string name=\"subtitle_yellow_transparent\">صفراء على خلفية شفافة</string>\n  <string name=\"subtitle_white_black\">بيضاء على خلفية سوداء</string>\n  <string name=\"player_seek_preview\">معاينة أثناء التنقل في الفيديو</string>\n  <string name=\"color_scheme_dark_grey_oled\">رمادي داكن (OLED)</string>\n  <string name=\"channels_section_sorting\">فرز قسم القنوات</string>\n  <string name=\"sorting_by_new_content\">محتوى جديد</string>\n  <string name=\"sorting_alphabetically\">أبجديًا</string>\n  <string name=\"sorting_last_viewed\">أخر ماتم مشاهدته</string>\n  <string name=\"player_pause_when_seek\">إيقاف أثناء التنقل في الفيديو</string>\n  <string name=\"playlists_style\">نمط قسم قوائم التشغيل</string>\n  <string name=\"playlists_style_grid\">شبكة</string>\n  <string name=\"playlists_style_rows\">صفوف</string>\n  <string name=\"player_show_clock\">عرض الساعة في شريط التحكم</string>\n  <string name=\"player_show_remaining_time\">عرض الوقت المتبقي في شريط التحكم</string>\n  <string name=\"player_show_ending_time\">عرض وقت الإنتهاء في شريط التحكم</string>\n  <string name=\"open_channel_uploads\">عرض فيديوهات القناة</string>\n  <string name=\"app_exit_shortcut\">الخروج من التطبيق</string>\n  <string name=\"app_exit_none\">عدم الخروج</string>\n  <string name=\"app_double_back_exit\">الضغط مرتين على زر الرجوع</string>\n  <string name=\"app_single_back_exit\">الضغط مرة واحدة على زر الرجوع</string>\n  <string name=\"action_video_zoom\">تكبير الفيديو</string>\n  <string name=\"video_zoom\">تكبير الفيديو</string>\n  <string name=\"video_zoom_default\">الافتراضي</string>\n  <string name=\"video_zoom_fit_width\">تناسب العرض</string>\n  <string name=\"video_zoom_fit_height\">الارتفاع الملائم</string>\n  <string name=\"video_zoom_fit_both\">تناسب العرض أو الارتفاع</string>\n  <string name=\"video_zoom_stretch\">تمدد</string>\n  <string name=\"color_scheme_teal\">أزرق مخضر</string>\n  <string name=\"color_scheme_teal_oled\">أزرق مخضر (OLED)</string>\n  <string name=\"player_seek_preview_none\">معطّل</string>\n  <string name=\"player_seek_preview_single\">إطار واحد</string>\n  <string name=\"player_seek_preview_carousel\">إطارات متعددة</string>\n  <string name=\"player_seek_preview_carousel_slow\">إطارات متعددة (بطيء، حسب الإطارات الرئيسية)</string>\n  <string name=\"player_seek_preview_carousel_fast\">إطارات متعددة (سريع، ليس دقيق)</string>\n  <string name=\"unsubscribe_from_channel\">إلغاء الاشتراك من القناة</string>\n  <string name=\"card_title_lines_num\">عدد سطور عنوان البطاقة</string>\n  <string name=\"card_auto_scrolled_title\">تمرير تلقائي للعنوان الطويل</string>\n  <string name=\"share_link\">مشاركة الرابط</string>\n  <string name=\"subscribe_to_channel\">اشتراك في القناة</string>\n  <string name=\"wait_data_loading\">الرجاء الانتظار بينما يتم تحميل البيانات ...</string>\n  <string name=\"auto_frame_rate_pause\">معدل الإطارات التلقائي (إيقاف عند التبديل)</string>\n  <string name=\"auto_frame_rate_applying\">تطبيق معدل الإطارات التلقائي%sx%s\\@%s</string>\n  <string name=\"audio_shift\">تزامن الصوت</string>\n  <string name=\"player_remember_speed\">تذكر السرعة</string>\n  <string name=\"mark_channel_as_watched\">وضع علامة كمشاهد</string>\n  <string name=\"channel_marked_as_watched\">تم وضع علامة على القناة كمشاهدَة</string>\n  <string name=\"dialog_add_device\">إضافة جهاز</string>\n  <string name=\"dialog_remove_all_devices\">إزالة جميع الأجهزة</string>\n  <string name=\"device_link_enabled\">تم تمكين الارتباط</string>\n  <string name=\"device_connected\">تم توصيل الجهاز \\\"%s\\\"</string>\n  <string name=\"device_disconnected\">تم قطع اتصال الجهاز \\\"%s\\\"</string>\n  <string name=\"settings_ui_scale\">مقياس واجهة المستخدم</string>\n  <string name=\"msg_restart_app\">يرجى إعادة تشغيل التطبيق لتطبيق هذه الإعدادات</string>\n  <string name=\"settings_block\">حظر المحتوى</string>\n  <string name=\"msg_applying\">جارٍ تطبيق %s</string>\n  <string name=\"msg_done\">تم</string>\n  <string name=\"settings_general\">عام</string>\n  <string name=\"settings_video\">فيديو</string>\n  <string name=\"content_block_confirm_skip\">التأكيد عند التخطي</string>\n  <string name=\"content_block_categories\">تخطي المقاطع</string>\n  <string name=\"content_block_sponsor\">الراعي</string>\n  <string name=\"content_block_intro\">المقدمة/فاصل</string>\n  <string name=\"content_block_outro\">الخاتمة/تترات النهاية</string>\n  <string name=\"content_block_interaction\">تذكير بالتفاعل (الاشتراك في القناة)</string>\n  <string name=\"content_block_self_promo\">ترويج شخصي/غير مدفوع الأجر</string>\n  <string name=\"content_block_music_off_topic\">قسم غير الموسيقى في المقطع</string>\n  <string name=\"confirm_segment_skip\">هل تريد تخطي المقطع%s\\\"\\?</string>\n  <string name=\"cancel_segment_skip\">إلغاء تخطي المقطع</string>\n  <string name=\"msg_skipping_segment\">تخطي المقطع\\\"%s\\\"…</string>\n  <string name=\"content_block_notification_type\">نوع الإشعار</string>\n  <string name=\"content_block_notify_none\">بدون إشعار</string>\n  <string name=\"content_block_notify_dialog\">مربع تأكيد</string>\n  <string name=\"return_to_launcher\">الانتقال للشاشة الرئيسية من قنوات ATV/البحث</string>\n  <string name=\"btn_confirm\">تأكيد</string>\n  <string name=\"player_low_video_quality\">جودة فيديو منخفضة</string>\n  <string name=\"remote_session_closed\">تم إغلاق الجلسة عن بعد</string>\n  <string name=\"msg_mode_switch_error\">لا يمكن تبديل وضع العرض إلى\\\"%s</string>\n  <string name=\"msg_player_error\">حدث خطأ في المشغل %s. جارٍ إعادة التشغيل...</string>\n  <string name=\"video_preset_disabled\">بدون إعداد مسبق</string>\n  <string name=\"video_preset_enabled\">سيتم ضبط تنسيق الفيديو التالي وفقًا للإعداد المسبق</string>\n  <string name=\"player_sleep_timer\">مؤقت النوم</string>\n  <string name=\"player_show_quality_info\">عرض معلومات الجودة في شريط التحكم</string>\n  <string name=\"player_remember_each_speed\">تذكر سرعة كل فيديو</string>\n  <string name=\"player_remember_speed_none\">بدون</string>\n  <string name=\"player_remember_speed_all\">نفس السرعة في جميع الفيديوهات</string>\n  <string name=\"player_remember_speed_each\">لكل فيديو</string>\n  <string name=\"msg_player_error_source\">لا يمكن تنزيل الفيديو</string>\n  <string name=\"msg_player_error_renderer\">التنسيق المحدد غير مدعوم.\\nجرّب اختيار تنسيق آخر.\\nإذا لم يحلّ ذلك المشكلة، أعد تشغيل الجهاز.</string>\n  <string name=\"msg_player_error_unexpected\">حدث خطأ غير معروف في فك ترميز الفيديو</string>\n  <string name=\"video_aspect\">نسبة العرض إلى الارتفاع</string>\n  <string name=\"player_tweaks\">خيارات المطور</string>\n  <string name=\"header_uploads\">الفيديوهات</string>\n  <string name=\"player_show_global_clock\">عرض الساعة على الشاشة دائمًا</string>\n  <string name=\"player_show_global_ending_time\">عرض وقت الانتهاء على الشاشة دائمًا</string>\n  <string name=\"uploads_old_look\">استعادة المظهر القديم لقسم الفيديوهات</string>\n  <string name=\"background_playback_activation\">تشغيل في الخلفية (تنشيط)</string>\n  <string name=\"channels_old_look\">استعادة المظهر القديم لقسم القنوات</string>\n  <string name=\"channels_auto_load\">تحميل محتوى قسم القنوات تلقائيًا</string>\n  <string name=\"return_to_background_video\">العودة إلى الفيديو الذي يتم تشغيله في الخلفية</string>\n  <string name=\"pin_unpin_from_sidebar\">إضافة/إزالة من الشريط الجانبي</string>\n  <string name=\"pinned_to_sidebar\">مضاف إلى الشريط الجانبي</string>\n  <string name=\"unpin_from_sidebar\">إزالة من الشريط الجانبي</string>\n  <string name=\"unpinned_from_sidebar\">تمت الإزالة من الشريط الجانبي</string>\n  <string name=\"dialog_select_country\">الدولة</string>\n  <string name=\"settings_language_country\">اللغة/الدولة</string>\n  <string name=\"share_embed_link\">مشاركة رابط التضمين</string>\n  <string name=\"card_text_scroll_factor\">سرعة تمرير نص البطاقة</string>\n  <string name=\"preferred_update_source\">اختر مصدر التحديثات</string>\n  <string name=\"hide_shorts\">إخفاء فيديوهات Shorts من الاشتراكات</string>\n  <string name=\"key_remapping\">إعادة ترتيب الأزرار</string>\n  <string name=\"screen_dimming\">تعتيم الشاشة</string>\n  <string name=\"removed_from_playback_queue\">تمت الإزالة من قائمة انتظار التشغيل</string>\n  <string name=\"added_to_playback_queue\">تمت الإضافة إلى قائمة انتظار التشغيل</string>\n  <string name=\"add_remove_from_playback_queue\">إضافة/إزالة من قائمة انتظار التشغيل</string>\n  <string name=\"proxy_enabled\">تم تمكين الخادم الوكيل</string>\n  <string name=\"proxy_disabled\">تم تعطيل الخادم الوكيل</string>\n  <string name=\"uploads_row_name\">الفيديوهات</string>\n  <string name=\"playlists_row_name\">قوائم التشغيل التي تم إنشاؤها</string>\n  <string name=\"popular_uploads_row_name\">الفيديوهات الرائجة</string>\n  <string name=\"breaking_news_row_name\">أخبار عاجلة</string>\n  <string name=\"covid_news_row_name\">أخبار كوفيد-19</string>\n  <string name=\"enable_voice_search\">تمكين البحث العام (يحتاج إلى دعم البرامج الثابتة)</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">اشتراك/إلغاء الاشتراك من القناة</string>\n  <string name=\"app_backup_restore\">النسخ الاحتياطي/الاستعادة</string>\n  <string name=\"app_backup\">نسخ احتياطي للبيانات</string>\n  <string name=\"app_restore\">استعادة البيانات من النسخة الاحتياطية</string>\n  <string name=\"skip_each_segment_once\">لا تتخطى المقاطع مرة أخرى</string>\n  <string name=\"disable_ok_long_press\">تعطيل الضغط المطوّل على زر OK</string>\n  <string name=\"player_time_correction\">عرض الوقت مع مراعاة السرعة</string>\n  <string name=\"sponsor_color_markers\">علامات ملونة على شريط التقدم</string>\n  <string name=\"refresh_section\">تحديث القسم</string>\n  <string name=\"option_disabled\">معطل</string>\n  <string name=\"live_now_row_name\">مباشر الآن</string>\n  <string name=\"run_in_background\">تشغيل في الخلفية</string>\n  <string name=\"settings_remote_control\">التحكم بالهاتف</string>\n  <string name=\"background_service_started\">بدأت خدمة التشغيل بالخلفية</string>\n  <string name=\"dialog_add_to\">إضافة إلى %s</string>\n  <string name=\"dialog_remove_from\">%s إزالة من</string>\n  <string name=\"added_to\">مضاف إلى %s</string>\n  <string name=\"removed_from\">تمت إزالته من %s</string>\n  <string name=\"video_preset_adaptive\">متكيف</string>\n  <string name=\"content_block_preview_recap\">معاينة/موجز</string>\n  <string name=\"content_block_highlight\">نقطة مثيرة للاهتمام (إشارة مرجعية)</string>\n  <string name=\"removed_from_history\">تمت إزالة الفيديو من سجلّ المشاهدة</string>\n  <string name=\"remove_from_history\">إزالة الفيديو من سجلّ المشاهدة</string>\n  <string name=\"upload_date\">تاريخ التحميل</string>\n  <string name=\"upload_date_any\">كل الوقت</string>\n  <string name=\"upload_date_today\">اليوم</string>\n  <string name=\"upload_date_this_week\">هذا الأسبوع</string>\n  <string name=\"upload_date_this_month\">هذا الشهر</string>\n  <string name=\"upload_date_this_year\">هذه السنة</string>\n  <string name=\"double_refresh_rate\">معدل الإطار المزدوج</string>\n  <string name=\"upload_date_last_hour\">الساعة الأخيرة</string>\n  <string name=\"focus_on_search_results\">التركيز التلقائي على نتائج البحث</string>\n  <string name=\"context_menu\">القائمة المنبثقة</string>\n  <string name=\"add_remove_from_recent_playlist\">إضافة/إزالة من قائمة التشغيل الأخيرة</string>\n  <string name=\"hide_settings_section\">إخفاء قسم الإعدادات (خطير!)</string>\n  <string name=\"volume\">الصوت %s%%</string>\n  <string name=\"auto_frame_rate_sec\">\t%s ثانية</string>\n  <string name=\"audio_shift_sec\">\t%s ثانية</string>\n  <string name=\"ui_hide_timeout_sec\">\t%s ثانية</string>\n  <string name=\"screen_dimming_timeout_min\">\t%s دقيقة</string>\n  <string name=\"mark_all_channels_watched\">حدد جميع القنوات على أنها تمت مشاهدتها</string>\n  <string name=\"move_section_up\">حَرِّك القسم لأعلى</string>\n  <string name=\"move_section_down\">حَرِّك القسم لأسفل</string>\n  <string name=\"player_buttons\">إعداد أزرار المشغل</string>\n  <string name=\"action_playlist_add\">إضافة إلى قائمة التشغيل</string>\n  <string name=\"action_video_stats\">إحصائيات الفيديو</string>\n  <string name=\"action_subscribe\">اشتراك</string>\n  <string name=\"action_channel\">فتح القناة</string>\n  <string name=\"action_screen_off\">إغلاق الشاشة</string>\n  <string name=\"action_pip\">صورة داخل صورة</string>\n  <string name=\"action_playback_queue\">قائمة انتظار التشغيل</string>\n  <string name=\"action_video_speed\">سرعة الفيديو</string>\n  <string name=\"action_subtitles\">الترجمة</string>\n  <string name=\"action_like\">أعجبني</string>\n  <string name=\"action_dislike\">لا يعجبني</string>\n  <string name=\"action_play_pause\">تشغيل/إيقاف</string>\n  <string name=\"action_repeat_mode\">وضع التشغيل</string>\n  <string name=\"action_next\">الفيديو التالي</string>\n  <string name=\"action_previous\">الفيديو السابق</string>\n  <string name=\"action_high_quality\">جودة الفيديو</string>\n  <string name=\"content_block_no_skipping_mode\">وضع عدم التخطي</string>\n  <string name=\"content_block_action_type\">اختر الإجراء</string>\n  <string name=\"content_block_action_none\">لا تفعل شيئًا</string>\n  <string name=\"content_block_action_only_skip\">تخطي فقط</string>\n  <string name=\"content_block_action_toast\">تخطي مع الإشعار</string>\n  <string name=\"content_block_action_dialog\">عرض مربع تأكيد التخطي</string>\n  <string name=\"content_block_filler\">خارج الموضوع (حشو)</string>\n  <string name=\"keyboard_auto_show\">عرض لوحة المفاتيح تلقائيًا</string>\n  <string name=\"cancel_dialog\">إلغاء</string>\n  <string name=\"msg_player_error_source2\">عنوان URL لا يعمل أو وقت الجهاز غير صحيح.\\nعادةً ما يكون ذلك خطأً في التطبيق.</string>\n  <string name=\"hide_upcoming\">إخفاء القادم من الاشتراكات</string>\n  <string name=\"search_background_playback\">تشغيل في الخلفية أثناء البحث أو تصفح القناة</string>\n  <string name=\"trending_row_name\">الرائجة</string>\n  <string name=\"player_seek_type\">سلوك التسريع</string>\n  <string name=\"player_seek_regular\">عادي</string>\n  <string name=\"player_seek_confirmation_pause\">مع التأكيد (إيقاف أثناء التسريع)</string>\n  <string name=\"player_seek_confirmation_play\">مع التأكيد (تشغيل أثناء التسريع)</string>\n  <string name=\"hide_shorts_from_home\">إخفاء فيديوهات Shorts من القائمة الرئيسية</string>\n  <string name=\"finish_on_disconnect\">أغلق التطبيق بعد فصل الجوّال/الجهاز اللوحي</string>\n  <string name=\"update_error\">خطأ في التحديث</string>\n  <string name=\"hide_shorts_from_history\">إخفاء فيديوهات Shorts من السجلّ</string>\n  <string name=\"disable_screensaver\">تعطيل شاشة التوقف</string>\n  <string name=\"player_ok_button_behavior\">وظيفة زر OK</string>\n  <string name=\"content_block_notify_toast\">الإشعار</string>\n  <string name=\"intent_force_close\">فرض الخروج إذا تم فتح الفيديو من مصدر خارجي</string>\n  <string name=\"pin_unpin_playlist\">إضافة/إزالة قائمة التشغيل من الشريط الجانبي</string>\n  <string name=\"pin_unpin_channel\">إضافة/إزالة القناة من الشريط الجانبي</string>\n  <string name=\"action_video_info\">وصف الفيديو </string>\n  <string name=\"description_not_found\">الوصف غير موجود</string>\n  <string name=\"subtitle_white_transparent\">بيضاء على خلفية شفافة</string>\n  <string name=\"proxy_port_hint\">منفذ الخادم الوكيل</string>\n  <string name=\"proxy_host_hint\">اسم او IP مضيف الخادم الوكيل</string>\n  <string name=\"proxy_username_hint\">اسم مستخدم الخادم الوكيل</string>\n  <string name=\"proxy_password_hint\">كلمة مرور الخادم الوكيل</string>\n  <string name=\"proxy_type\">نوع الخادم الوكيل</string>\n  <string name=\"enable_web_proxy\">استخدم Web Proxy</string>\n  <string name=\"proxy_not_supported\">استخدام وكيل الويب (يتطلب Android 4.4+)</string>\n  <string name=\"proxy_test_btn\">اختبار</string>\n  <string name=\"proxy_settings_title\">إعدادات الخادم الوكيل</string>\n  <string name=\"proxy_test_cancelled\">اختبار#%d:تم الإلغاء.</string>\n  <string name=\"proxy_type_invalid\">لم يتم تعيين نوع الخادم الوكيل.</string>\n  <string name=\"proxy_host_invalid\">لم يتم تعيين مضيف الخادم الوكيل.</string>\n  <string name=\"proxy_port_invalid\">منفذ الخادم الوكيل غير صالح ، يجب أن يكون&gt; ​​0.</string>\n  <string name=\"proxy_credentials_invalid\">خطأ في اسم المستخدم أو كلمة مرور.</string>\n  <string name=\"proxy_test_aborted\">تم إلغاء الاختبار، يرجى إصلاح إعدادات الخادم الوكيل أولًا.</string>\n  <string name=\"proxy_application_aborted\">من فضلك ، قم بتصحيح إعدادات الخادم الوكيل أولًا.</string>\n  <string name=\"proxy_test_start\">اختبار#%d:%s...</string>\n  <string name=\"proxy_test_error\">خطأ#%d:%s</string>\n  <string name=\"proxy_test_status\">حالة#%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">عنوان ملف التكوين (* .ovpn)</string>\n  <string name=\"enable_openvpn\">استخدام OpenVPN</string>\n  <string name=\"openvpn_settings_title\">إعدادات OpenVPN</string>\n  <string name=\"openvpn_application_aborted\">من فضلك ، قم بتصحيح إعدادات OpenVPN أولًا.</string>\n  <string name=\"openvpn_test_aborted\">تم إلغاء الاختبار ، يرجى إصلاح إعدادات OpenVPN أولًا.</string>\n  <string name=\"openvpn_address_invalid\">لم يتم تعيين عنوان تكوين OpenVPN.</string>\n  <string name=\"internet_censorship\">الرقابة على الإنترنت</string>\n  <string name=\"rename_section\">إعادة تسمية هذا القسم</string>\n  <string name=\"simple_edit_value_hint\">أدخل قيمة</string>\n  <string name=\"seek_interval\">الفاصل الزمني</string>\n  <string name=\"seek_interval_sec\">%s ثانية</string>\n  <string name=\"subtitle_system\">نمط النظام (إعدادات Android&gt; إمكانية الوصول)</string>\n  <string name=\"subtitle_scale\">مقياس الترجمة</string>\n  <string name=\"audio_sync_fix_desc\">طريقة بديلة لمزامنة الصوت والفيديو. تعتبر قديمة، لكنها قد تساعد في بعض الحالات.</string>\n  <string name=\"audio_sync_fix\">إصلاح مزامنة الصوت</string>\n  <string name=\"ambilight_ratio_fix_desc\">يعمل على إصلاح الإضاءة الخلفية المفقودة. يصلح نسبة العرض إلى الارتفاع أو مقياس الصورة غير الصحيح. يصلح لقطات الشاشة الفارغة. قد يؤثر على الأداء!</string>\n  <string name=\"ambilight_ratio_fix\">إصلاح Ambilight / نسبة العرض إلى الارتفاع / مقياس الفيديو / لقطات الشاشة</string>\n  <string name=\"force_legacy_codecs_desc\">يحسن الأداء بشكل كبير على الأجهزة منخفضة المواصفات. الدقة القصوى 720p.</string>\n  <string name=\"force_legacy_codecs\">فرض برامج الترميز القديمة (720p)</string>\n  <string name=\"live_stream_fix_desc\">تحذير: هذا التعديل يعطل إمكانية إعادة تشغيل البث. يحسن أداء البث المباشر بشكل كبير على الأجهزة منخفضة المواصفات. الحد الأقصى للدقة هو 1080p.</string>\n  <string name=\"live_stream_fix\">إصلاح البث المباشر (1080p)</string>\n  <string name=\"playback_notifications_fix_desc\">يخفي إشعارات تغيير المسار. قد يكون مفيدًا في البرامج الثابتة المبنية على AOSP.</string>\n  <string name=\"playback_notifications_fix\">تعطيل إشعارات التشغيل</string>\n  <string name=\"amlogic_fix_desc\">إصلاح هبوط الإطارات على الأجهزة القائمة على Amlogic.</string>\n  <string name=\"amlogic_fix\">إصلاح Amlogic لدقة 1080p\\@60fps.</string>\n  <string name=\"tunneled_video_playback_desc\">انتباه: قد لا يعمل إيقاف الفيديو بشكل صحيح! يوفر Tunneled video playback فوائد مثل تزامن صوت/فيديو أفضل (مزامنة AV) وتشغيل أكثر سلاسة. يتطلب أندرويد 5 أو أعلى.</string>\n  <string name=\"tunneled_video_playback\">Tunneled Video Playback (Android 5+)</string>\n  <string name=\"master_volume\">الصوت الرئيسي</string>\n  <string name=\"volume_limit\">حد الصوت</string>\n  <string name=\"play_video\">تشغيل</string>\n  <string name=\"remember_position_of_short_videos\">تذكر تقدم الفيديوهات القصيرة (أقل من 5 دقائق)</string>\n  <string name=\"player_show_tooltips\">عرض تلميحات الزر</string>\n  <string name=\"action_like_unset\">تم إزالة أعجبني</string>\n  <string name=\"action_dislike_unset\">تم إزالة لا يعجبني</string>\n  <string name=\"various_buttons\">الأزرار المتواجدة في أعلى الصفحة الرئيسية</string>\n  <string name=\"not_compatible_with\">الخيار المحدد غير متوافق مع</string>\n  <string name=\"subtitle_position\">موضع الترجمة من الأسفل</string>\n  <string name=\"pressing_home\">بالضغط على زر HOME</string>\n  <string name=\"pressing_home_back\">بالضغط على زر HOME أو الرجوع</string>\n  <string name=\"player_number_key_seek\">التنقل باستخدام مفاتيح الأرقام</string>\n  <string name=\"save_remove_playlist\">إضافة/إزالة قائمة التشغيل من قسم قوائم التشغيل</string>\n  <string name=\"remove_playlist\">إزالة قائمة التشغيل من قسم قوائم التشغيل بشكل دائم</string>\n  <string name=\"removed_from_playlists\">تمت الإزالة من قسم قوائم التشغيل</string>\n  <string name=\"saved_to_playlists\">تمت الإضافة إلى قسم قوائم التشغيل</string>\n  <string name=\"create_playlist\">إنشاء قائمة التشغيل</string>\n  <string name=\"create_playlist_note\">لن تظهر في تطبيق YouTube</string>\n  <string name=\"add_video_to_new_playlist\">إضافة إلى قائمة تشغيل جديدة</string>\n  <string name=\"cant_delete_empty_playlist\">لا يمكن حذف قائمة التشغيل الفارغة</string>\n  <string name=\"playlist\">قائمة التشغيل</string>\n  <string name=\"rename_playlist\">إعادة تسمية قائمة التشغيل</string>\n  <string name=\"cant_rename_empty_playlist\">لا يمكن إعادة تسمية قائمة التشغيل الفارغة</string>\n  <string name=\"cant_rename_foreign_playlist\">لا يمكن إعادة تسمية قائمة تشغيل لشخص آخر</string>\n  <string name=\"cant_save_playlist\">لا يمكن إضافة قائمة التشغيل هذه</string>\n  <string name=\"enter_value\">أدخل أي قيمة غير فارغة</string>\n  <string name=\"dialog_add_remove_from\">إضافة/إزالة من%s</string>\n  <string name=\"add_to_playback_queue\">إضافة إلى قائمة انتظار التشغيل</string>\n  <string name=\"remove_from_playback_queue\">إزالة من قائمة انتظار التشغيل</string>\n  <string name=\"lb_playback_controls_skip_next\">الانتقال إلى الفيديو التالي</string>\n  <string name=\"lb_playback_controls_skip_previous\">الانتقال إلى الفيديو السابق</string>\n  <string name=\"alt_presets_behavior\">السلوك البديل للإعدادات المسبقة (حد عرض النطاق الترددي)</string>\n  <string name=\"alt_presets_behavior_desc\">سيحاول التطبيق الحفاظ على عرض النطاق الترددي الذي يتوافق مع الإعداد المسبق المحدد بدلًا من المطابقة بين الدقة ومعدل الإطارات والترميز.</string>\n  <string name=\"sleep_timer\">تمكين مؤقت النوم (إذا لم يُستخدم جهاز التحكم عن بُعد لمدة ساعة)</string>\n  <string name=\"sleep_timer_desc\">إيقاف التشغيل مؤقتًا إذا لم يستخدم المستخدم جهاز التحكم عن بُعد خلال ساعة واحدة</string>\n  <string name=\"disable_vsync\">تعطيل محاذاة الإطارات مع المزامنة الرأسية (Vsync)</string>\n  <string name=\"disable_vsync_desc\">يعطل هذا الخيار محاذاة الإطارات مع إشارة المزامنة الرأسية للشاشة. قد يحسن الأداء على الأجهزة منخفضة المواصفات من خلال تحرير موارد وحدة المعالجة المركزية.</string>\n  <string name=\"skip_codec_profile_check\">تخطي فحص مستوى ملف تعريف برنامج الترميز</string>\n  <string name=\"skip_codec_profile_check_desc\">عدم التحقق من دعم برنامج الترميز عند بدء تشغيل الفيديو. قد يكون مفيدًا في البرامج الثابتة التي تحتوي على أخطاء.</string>\n  <string name=\"force_sw_codec\">فرض استخدام فك ترميز الفيديو البرمجي (Software)</string>\n  <string name=\"force_sw_codec_desc\">يمكنه تشغيل معظم أنواع الفيديو، لكن الأداء يكون ضعيفًا جدًا.</string>\n  <string name=\"playlist_order\">فرز قائمة التشغيل</string>\n  <string name=\"playlist_order_added_date_newer_first\">تاريخ الإضافة (الأحدث أولًا)</string>\n  <string name=\"playlist_order_added_date_older_first\">تاريخ الإضافة (الأقدم أولًا)</string>\n  <string name=\"playlist_order_published_date_newer_first\">تاريخ النشر (الأحدث أولًا)</string>\n  <string name=\"playlist_order_published_date_older_first\">تاريخ النشر (الأقدم أولًا)</string>\n  <string name=\"playlist_order_popularity\">شعبية</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">لا يمكن القيام بذلك لقائمة تشغيل خاصة بشخص آخر</string>\n  <string name=\"owned_playlist_warning\">خطأ: لا يمكن تطبيق هذا الإجراء إلا على قوائم التشغيل المملوكة</string>\n  <string name=\"content_block_alt_server\">استخدام خادم بديل</string>\n  <string name=\"content_block_alt_server_desc\">تمكين هذا الخيار في حالة رفض SponsorBlock العمل لأسباب مختلفة.</string>\n  <string name=\"unset_stream_reminder\">إلغاء تعيين تذكير البث المباشر</string>\n  <string name=\"set_stream_reminder\">تعيين تذكير البث المباشر</string>\n  <string name=\"playback_starts_shortly\">سيبدأ التشغيل تلقائيًا عندما يصبح البث جاهزًا</string>\n  <string name=\"starting_stream\">جارٍ بدء البث ...</string>\n  <string name=\"action_playlist_remove\">إزالة من قائمة التشغيل</string>\n  <string name=\"signin_view_title\">يتم تحميل رمز المستخدم ...</string>\n  <string name=\"signin_view_description\">لتسجيل الدخول ، أدخل هذا الرمز في الصفحة%s\\n ملاحظة: يعمل فقط مع متصفح Firefox أو Chrome.</string>\n  <string name=\"signin_view_action_text\">تم</string>\n  <string name=\"require_checked\"> \\\"%sيتطلب</string>\n  <string name=\"msg_press_again_to_exit\">اضغط مرة أخرى للخروج</string>\n  <string name=\"player_remaining_time\">متبقي:\\\"%s</string>\n  <string name=\"player_ending_time\">ينتهي عند \\\"%s</string>\n  <string name=\"badge_new_content\">محتوى جديد</string>\n  <string name=\"badge_live\">مباشر</string>\n  <string name=\"add_device_view_description\">لربط الجهاز، أدخل هذا الرمز في تطبيق YouTube على هاتفك في قسم الإعدادات/المشاهدة على التلفزيون\\nملاحظة: يعمل هذا فقط مع لوحة مفاتيح Gboard!</string>\n  <string name=\"playback_controls_repeat_pause\">إيقاف التكرار</string>\n  <string name=\"playback_controls_repeat_list\">تكرار القائمة</string>\n  <string name=\"action_subscribe_off\">إلغاء الاشتراك</string>\n  <string name=\"action_subscribe_on\">الاشتراك</string>\n  <string name=\"skip_24_rate\">تخطي تنسيقات 24 إطارًا في الثانية (إصلاح وضع السينما الحقيقي؟)</string>\n  <string name=\"player_disable_suggestions\">تعطيل الاقتراحات</string>\n  <string name=\"sources\">المصادر</string>\n  <string name=\"releases\">الإصدارات</string>\n  <string name=\"feedback\">الملاحظات</string>\n  <string name=\"prefer_avc_over_vp9\">تحديد برنامج الترميز: تفضيل AVC على VP9</string>\n  <string name=\"open_chat\">التعليقات/المحادثات المباشرة</string>\n  <string name=\"place_chat_left\">ضع المحادثات المباشرة على اليسار</string>\n  <string name=\"use_alt_speech_recognizer\">أداة التعرف على الكلام البديلة</string>\n  <string name=\"time_format\">تنسيق الوقت</string>\n  <string name=\"time_format_24\">ساعة 24</string>\n  <string name=\"time_format_12\">12 ساعة (ص/م)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">يحتوي على أخطاء كثيرة. استخدمه فقط إذا كانت لديك مشاكل مع أداة التعرف الافتراضية.</string>\n  <string name=\"speech_recognizer\">أداة التعرف على الكلام</string>\n  <string name=\"speech_recognizer_system\">النظام (المفضل)</string>\n  <string name=\"speech_recognizer_external_1\">خارجي 1 (يحتوي على أخطاء كثيرة، لكي يعمل عليك تعطيل الوصول إلى الميكروفون)</string>\n  <string name=\"speech_recognizer_external_2\">خارجي 2 (يحتوي على أخطاء كثيرة)</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"real_channel_icon\">عرض شعار القناة على زر القناة</string>\n  <string name=\"subtitle_yellow_semi_transparent\">صفراء على خلفية شبه شفافة</string>\n  <string name=\"subtitle_yellow_black\">صفراء على خلفية سوداء</string>\n  <string name=\"player_pixel_ratio\">نسبة البكسل</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">رمادي داكن (أحادي اللون)</string>\n  <string name=\"disable_mic_permission\">من فضلك، قم بتعطيل الوصول إلى الميكروفون للتطبيق لكي تعمل أداة التعرف هذه بشكل صحيح.</string>\n  <string name=\"repeat_mode_shuffle\">تشغيل قائمة التشغيل بشكل عشوائي</string>\n  <string name=\"chat_left\">اليسار</string>\n  <string name=\"chat_right\">اليمين</string>\n  <string name=\"card_real_thumbnails\">استبدال الصور المصغرة بإطار من الفيديو</string>\n  <string name=\"card_content\">مكان الحصول على الصور المصغرة للبطاقة</string>\n  <string name=\"thumb_quality_default\">الافتراضي</string>\n  <string name=\"thumb_quality_start\">بداية الفيديو</string>\n  <string name=\"thumb_quality_middle\">منتصف الفيديو</string>\n  <string name=\"thumb_quality_end\">نهاية الفيديو</string>\n  <string name=\"video_buffer_size_none\">لا يوجد</string>\n  <string name=\"content_block_status\">تحقق من حالة SpnsorBlock</string>\n  <string name=\"player_show_quality_info_bitrate\">إضافة معدل البث (Bitrate) إلى معلومات الجودة</string>\n  <string name=\"player_speed_button_old_behavior\">استعادة السلوك القديم لزر السرعة</string>\n  <string name=\"protect_settings_with_password\">حماية جميع الإعدادات بكلمة مرور</string>\n  <string name=\"enter_settings_password\">أدخل كلمة مرور الإعدادات</string>\n  <string name=\"child_mode\">وضع الأطفال</string>\n  <string name=\"child_mode_desc\">في هذا الوضع، لا يستطيع المستخدم استخدام البحث أو رؤية أي محتوى مقترح. صفحة الإعدادات محمية بكلمة مرور.</string>\n  <string name=\"lost_setting_warning\">سيتم تغيير إعدادات التطبيق. تأكد من إنشاء نسخة احتياطية من الإعدادات.</string>\n  <string name=\"pause_history\">إيقاف السجلّ</string>\n  <string name=\"clear_history\">محو سجلّ المشاهدة</string>\n  <string name=\"resume_history\">استئناف السجلّ</string>\n  <string name=\"chapters\">الفصول</string>\n  <string name=\"auto_frame_rate_modes\">الأوضاع المدعومة</string>\n  <string name=\"loading\">جارٍ التحميل…</string>\n  <string name=\"video_rotate\">تدوير</string>\n  <string name=\"hide_streams\">إخفاء البث المباشر من الاشتراكات</string>\n  <string name=\"card_multiline_subtitle\">ترجمة متعددة الأسطر</string>\n  <string name=\"player_button_long_click\">اضغط مطولًا على هذا الزر لخيارات إضافية</string>\n  <string name=\"trending_searches\">البحث الرائج</string>\n  <string name=\"video_duration_any\">أي مدة</string>\n  <string name=\"video_duration\">المدة</string>\n  <string name=\"video_duration_under_4\">أقل من 4 دقائق</string>\n  <string name=\"video_duration_between_4_20\">4-20 دقيقة</string>\n  <string name=\"video_duration_over_20\">أكثر من 20 دقيقة</string>\n  <string name=\"content_type\">النوع</string>\n  <string name=\"content_type_any\">الكل</string>\n  <string name=\"content_type_video\">فيديو</string>\n  <string name=\"content_type_channel\">قناة</string>\n  <string name=\"content_type_playlist\">قائمة تشغيل</string>\n  <string name=\"content_type_movie\">فيلم</string>\n  <string name=\"video_features\">الميزات</string>\n  <string name=\"video_feature_any\">الكل</string>\n  <string name=\"video_feature_live\">مُباشر</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">الترتيب حسب</string>\n  <string name=\"sort_by_relevance\">مدى الصلة</string>\n  <string name=\"sort_by_views\">عدد المشاهدات</string>\n  <string name=\"sort_by_date\">تاريخ التحميل</string>\n  <string name=\"sort_by_rating\">التقييم</string>\n  <string name=\"clear_search_history\">محو سجل البحث</string>\n  <string name=\"remove_from_subscriptions\">إخفاء</string>\n  <string name=\"player_long_speed_list\">قائمة السرعة الطويلة</string>\n  <string name=\"disable_ok_long_press_desc\">مخصص لأجهزة التحكم التي تعاني من مشاكل حيث لا يعمل زر OK بشكل صحيح</string>\n  <string name=\"pressing_back\">بالضغط على زر الرجوع</string>\n  <string name=\"alt_app_icon\">أيقونة بديلة للتطبيق (تتطلب إعادة تشغيل)</string>\n  <string name=\"cronet_desc\">مكدس الشبكة الخاص بمتصفح Chromium. يمكن لـ Cronet تقليل التأخير وزيادة أداء الشبكة، مما يساعد في حل مشاكل التخزين المؤقت.</string>\n  <string name=\"network_stack\">يفضل مكدس الشبكة %s</string>\n  <string name=\"unlock_all_formats\">فتح جميع صيغ الفيديو</string>\n  <string name=\"unlock_all_formats_desc\">في بعض الأجهزة، تقوم البرامج الثابتة بالإبلاغ بشكل غير صحيح عن بعض التنسيقات على أنها غير مدعومة، حتى لو كانت كذلك.\\n(على سبيل المثال، تميل أجهزة التلفزيون الذكية بدقة 1080p إلى الإبلاغ عن عدم دعم دقة 4K).</string>\n  <string name=\"okhttp_desc\">محرك الشبكة هذا هو الأبطأ ولكنه يتميز باستقرار جيد.</string>\n  <string name=\"select_channel_section\">الانتقال للقسم المقابل عند فتح التطبيق من قنوات ATV</string>\n  <string name=\"live_stream_fix_4k_desc\">تحذير: هذا التعديل يعطل إعادة تشغيل البث. يحسن أداء البث المباشر بشكل كبير. أقصى دقة هي 4K.</string>\n  <string name=\"live_stream_fix_4k\">إصلاح البث المباشر (4K)</string>\n  <string name=\"enable_voice_search_desc\">سيتم استبدال التطبيق الرسمي بما يسمى \\\"bridge\\\". يساعد الجسر في نقل طلبات البحث العامة إلى تطبيقنا.</string>\n  <string name=\"not_recommend_channel\">عدم اقتراح القناة</string>\n  <string name=\"you_wont_see_this_channel\">لن ترى هذه القناة في الاقتراحات</string>\n  <string name=\"enable_master_password\">تمكين كلمة المرور الرئيسية</string>\n  <string name=\"enter_master_password\">أدخل كلمة المرور الرئيسية</string>\n  <string name=\"disable_stream_buffer\">تعطيل التخزين المؤقت في البث المباشر</string>\n  <string name=\"disable_stream_buffer_desc\">إصلاح الحالات التي يكون فيها البث متأخرًا جدًا. ملاحظة: قد يبدأ البث في التأخر.</string>\n  <string name=\"sony_frame_drop_fix\">إصلاح فقدان الإطارات #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">إصلاح التأخير في أجهزة تلفاز Sony وبعض الأجهزة الأخرى. ملاحظة: قد تحدث مشكلات في مزامنة الصوت.</string>\n  <string name=\"audio_language\">لغة الصوت</string>\n  <string name=\"old_home_look\">المظهر القديم لقسم الصفحة الرئيسية</string>\n  <string name=\"hide_shorts_everywhere\">إخفاء فيديوهات Shorts في كل مكان</string>\n  <string name=\"update_found\">تحديث</string>\n  <string name=\"volume_boost_warning\">قد يؤدي تجاوز الحد المسموح به إلى إتلاف مكبرات الصوت</string>\n  <string name=\"play_video_incognito\">التشغيل بوضع التصفّح المتخفي</string>\n  <string name=\"header_kids_home\">أطفال</string>\n  <string name=\"player_section_playlist\">استخدم محتويات القسم الحالي كقائمة تشغيل</string>\n  <string name=\"header_trending\">المحتوى الرائج</string>\n  <string name=\"screensaver\">شاشة التوقف</string>\n  <string name=\"player_screen_off_timeout\">مهلة إيقاف تشغيل الشاشة</string>\n  <string name=\"old_update_notifications\">المظهر القديم لإشعارات التحديث</string>\n  <string name=\"mark_as_watched\">وضع علامة كمشاهد</string>\n  <string name=\"player_likes_count\">عرض عدد الإعجابات/عدم الإعجاب</string>\n  <string name=\"sorting_alphabetically2\">أبجديًا (معاينات سريعة ومتحركة)</string>\n  <string name=\"sorting_default\">افتراضيًا (معاينات سريعة ومتحركة)</string>\n  <string name=\"player_ui_animations\">تمكين تأثيرات الحركة في واجهة المشغل</string>\n  <string name=\"content_block_exclude_channel\">استبعاد هذه القناة من SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">إيقاف استبعاد هذه القناة من SponsorBlock</string>\n  <string name=\"player_chapter_notification\">الانتقال السريع بين الفصول باستخدام الإشعار المنبثق</string>\n  <string name=\"subtitle_remember\">تمكين الترجمة فقط على القناة الحالية</string>\n  <string name=\"amazon_frame_drop_fix\">إصلاح فقدان الإطارات #2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">مخصص لأجهزة Amazon Stick. قد تعمل على أجهزة أخرى أيضًا.</string>\n  <string name=\"share_qr_link\">مشاركة الرابط (رمز QR)</string>\n  <string name=\"msg_player_error_video_renderer\">تنسيق الفيديو المحدد غير مدعوم.\\nجرّب اختيار تنسيق آخر بالضغط على زر HQ في المشغل.\\nإذا لم يحلّ ذلك المشكلة، أعد تشغيل الجهاز.</string>\n  <string name=\"msg_player_error_audio_renderer\">تنسيق الصوت المحدد غير مدعوم.\\nجرّب اختيار تنسيق آخر بالضغط على زر HQ في المشغل.\\nإذا لم يحلّ ذلك المشكلة، أعد تشغيل الجهاز.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">تنسيق الترجمة المحدد غير مدعوم.\\nجرّب اختيار تنسيق آخر بالضغط مع الاستمرار على زر CC في المشغل.\\nإذا لم يحلّ ذلك المشكلة، أعد تشغيل الجهاز.</string>\n  <string name=\"msg_player_error_video_source\">عنوان URL للفيديو لا يعمل أو وقت الجهاز غير صحيح.\\nعادةً ما يكون ذلك خطأً في التطبيق.</string>\n  <string name=\"msg_player_error_audio_source\">عنوان URL للصوت لا يعمل أو وقت الجهاز غير صحيح.\\nعادةً ما يكون ذلك خطأً في التطبيق.</string>\n  <string name=\"msg_player_error_subtitle_source\">عنوان URL للترجمة لا يعمل أو وقت الجهاز غير صحيح.\\nعادةً ما يكون ذلك خطأ في التطبيق.</string>\n  <string name=\"player_volume\">مستوى الصوت</string>\n  <string name=\"open_comments\">فتح التعليقات</string>\n  <string name=\"disable_history\">تعطيل السجلّ</string>\n  <string name=\"enable_history\">تمكين السجلّ</string>\n  <string name=\"player_network_stack\">محرك الشبكة</string>\n  <string name=\"dialog_notification\">عرض إشعار بالتحديثات من خلال نافذة منبثقة</string>\n  <string name=\"default_stack_desc\">محرك الشبكة المدمج. قد يكون لهذا المحرك استقرار أفضل في بعض الحالات.</string>\n  <string name=\"item_postion\">موضع</string>\n  <string name=\"screensaver_timout\">مهلة شاشة التوقف</string>\n  <string name=\"screensaver_dimming\">مستوى تعتيم شاشة التوقف</string>\n  <string name=\"player_screen_off_dimming\">مستوى تعتيم إيقاف الشاشة</string>\n  <string name=\"player_ui_on_next\">عرض واجهة المشغل عند الانتقال إلى الفيديو التالي</string>\n  <string name=\"autogenerated\">يتم إنشاؤها تلقائيًا</string>\n  <string name=\"player_auto_volume\">تعديل مستوى الصوت تلقائيًا</string>\n  <string name=\"header_shorts\">Shorts</string>\n  <string name=\"player_global_focus\">مزامنة التركيز بين صفوف أزرار المشغل</string>\n  <string name=\"auto_history\">تلقائي (استخدام إعدادات الحساب)</string>\n  <string name=\"remember_position_subscriptions\">تذكر آخر موضع تم عرضه في الاشتراكات</string>\n  <string name=\"msg_player_unknown_error\">خطأ غير معروف</string>\n  <string name=\"header_notifications\">الإشعارات</string>\n  <string name=\"disable_search_history\">تعطيل سجل البحث</string>\n  <string name=\"unlock_high_bitrate_formats\">تمكين 1080p VP9 بدقة بث عالية</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">تمكين mp4a بدقة بث عالية</string>\n  <string name=\"color_scheme_blue\">أزرق</string>\n  <string name=\"color_scheme_dark_blue\">أزرق داكن</string>\n  <string name=\"player_speed_per_channel\">لكل قناة</string>\n  <string name=\"disable_popular_searches\">تعطيل استعلامات البحث الشائعة</string>\n  <string name=\"multi_profiles\">استخدام إعدادات منفصلة لكل حساب</string>\n  <string name=\"protect_account_with_password\">حماية هذا الحساب بكلمة مرور</string>\n  <string name=\"enter_account_password\">أدخل كلمة مرور الحساب</string>\n  <string name=\"show_connect_messages\">عرض رسائل الاتصال</string>\n  <string name=\"prefer_avc_over_vp9_desc\">تحذير: 1080p كحد أقصى</string>\n  <string name=\"not_supported_by_device\">الجهاز لا يدعم هذه الميزة</string>\n  <string name=\"video_buffer_size_lowest\">الأقل</string>\n  <string name=\"video_buffer_size_highest\">الأعلى</string>\n  <string name=\"player_exit_shortcut\">الخروج من المشغل</string>\n  <string name=\"app_corner_clock\">الصفحة الرئيسية: الساعة في الزاوية اليمنى العليا</string>\n  <string name=\"player_corner_clock\">المشغل: الساعة في الزاوية اليمنى العليا</string>\n  <string name=\"player_corner_ending_time\">المشغل: وقت الانتهاء في الزاوية اليمنى العليا</string>\n  <string name=\"pin_playlist\">إضافة قائمة التشغيل إلى الشريط الجانبي</string>\n  <string name=\"pin_channel\">إضافة القناة إلى الشريط الجانبي</string>\n  <string name=\"unpin_group_from_sidebar\">إزالة مجموعة الاشتراكات</string>\n  <string name=\"hide_shorts_channel\">إخفاء فيديوهات Shorts من القناة</string>\n  <string name=\"news_row_name\">أخبار</string>\n  <string name=\"action_sound_off\">كتم الصوت</string>\n  <string name=\"hide_upcoming_channel\">إخفاء القادم من القناة</string>\n  <string name=\"hide_upcoming_home\">إخفاء القادم من الصفحة الرئيسية</string>\n  <string name=\"hide_shorts_from_trending\">إخفاء فيديوهات Shorts من الأكثر رواجًا</string>\n  <string name=\"remember_position_of_live_videos\">تذكر تقدم البث المباشر</string>\n  <string name=\"save_playlist\">إضافة قائمة التشغيل إلى قسم قوائم التشغيل</string>\n  <string name=\"skip_shorts\">تخطي Shorts</string>\n  <string name=\"suggestions\">الاقتراحات</string>\n  <string name=\"place_comments_left\">ضع التعليقات على اليسار</string>\n  <string name=\"speech_engine\">محرك البحث الصوتي</string>\n  <string name=\"dearrow_status\">تحقق من حالة خادم DeArrow</string>\n  <string name=\"player_extra_long_speed_list\">قائمة السرعة الطويلة جدًا</string>\n  <string name=\"old_channel_look\">المظهر القديم لصفحة القناة</string>\n  <string name=\"remember_position_pinned\">تذكر آخر موضع تم عرضه في قوائم التشغيل المثبتة</string>\n  <string name=\"unknown_source_error\">خطأ المصدر غير معروف</string>\n  <string name=\"unknown_renderer_error\">خطأ غير معروف في برنامج العرض</string>\n  <string name=\"play_next\">تشغيل التالي</string>\n  <string name=\"hide_watched_from_subscriptions\">إخفاء الفيديوهات التي تمت مشاهدتها من الاشتراكات</string>\n  <string name=\"hide_watched_from_notifications\">إخفاء الفيديوهات التي تمت مشاهدتها من الإشعارات</string>\n  <string name=\"hide_unwanted_content\">إخفاء المحتوى غير المرغوب فيه</string>\n  <string name=\"hide_watched_from_home\">إخفاء الفيديوهات التي تمت مشاهدتها من الصفحة الرئيسية</string>\n  <string name=\"hide_watched_from_watch_later\">إخفاء الفيديوهات التي تمت مشاهدتها من قائمة التشغيل \\\"مشاهدة لاحقًا\\\"</string>\n  <string name=\"remote_control_permission\">يتطلب التحكم عن بعد في الخلفية إذن السماح بعرض التراكب على التطبيقات الأخرى</string>\n  <string name=\"login_from_browser\">تسجيل الدخول من المتصفح</string>\n  <string name=\"disable_remote_history\">تعطيل السجلّ عند استخدام التحكم بالهاتف</string>\n  <string name=\"keyboard_fix\">منع زر OK من فتح لوحة المفاتيح (G20s وغيرها)</string>\n  <string name=\"nothing_found\">لم يتم العثور على شيء</string>\n  <string name=\"auto_frame_rate_desc\">يُزيل هذا الخيار الاهتزاز في المشاهد التي تتحرك فيها الكاميرا بسرعة، على سبيل المثال البث الرياضي</string>\n  <string name=\"channel_filter_hint\">فلترة القنوات</string>\n  <string name=\"player_loop_shorts\">تكرار Shorts</string>\n  <string name=\"player_quick_shorts_skip\">تخطي Shorts باستخدام الأزرار اليسرى/اليمنى</string>\n  <string name=\"player_quick_skip_videos\">تخطي الفيديوهات العادية باستخدام الأزرار اليسرى/اليمنى</string>\n  <string name=\"channels_filter\">عرض حقل فلترة القنوات في قسم القنوات</string>\n  <string name=\"channel_search_bar\">شريط البحث في صفحة القناة</string>\n  <string name=\"header_sports\">رياضة</string>\n  <string name=\"keep_finished_activities\">الاحتفاظ بالأنشطة المكتملة</string>\n  <string name=\"disable_channels_service\">تعطيل خدمة القنوات</string>\n  <string name=\"replace_titles\">استبدال العناوين</string>\n  <string name=\"enable\">تمكين</string>\n  <string name=\"more_info\">مزيد من المعلومات</string>\n  <string name=\"about_sponsorblock\">لمحة عن SponsorBlock</string>\n  <string name=\"about_dearrow\">لمحة عن DeArrow</string>\n  <string name=\"replace_thumbnails\">استبدال الصور المصغرة</string>\n  <string name=\"crowdsourced_thumbnails\">الصور المصغرة التي تم جمعها من الجمهور</string>\n  <string name=\"crowdsoursed_titles\">عناوين تم جمعها من الجمهور</string>\n  <string name=\"dearrow_not_submitted_thumbs\">مصدر الصورة المصغرة (إذا لم يكن هناك إرسال DeArrow)</string>\n  <string name=\"pitch_effect\">تأثير الطبقة الصوتية</string>\n  <string name=\"fullscreen_mode\">وضع ملء الشاشة (بدون أشرطة النظام)</string>\n  <string name=\"player_only_mode\">عرض المشغل فقط إذا تم فتح الفيديو خارج التطبيق</string>\n  <string name=\"pinned_channel_rows\">عرض القناة المثبتة في شكل صفوف</string>\n  <string name=\"prefer_ipv4\">تفضيل IPv4 DNS</string>\n  <string name=\"prefer_ipv4_desc\">قد يعمل على إصلاح المواقف التي لا يعمل فيها التطبيق على الإطلاق.\\nملاحظة: قد يتسبب في حدوث تعطلات وأخطاء (خاصة على أجهزة Android 8 أو Dune HD)</string>\n  <string name=\"long_press_for_settings\">اضغط ضغطة طويلة للوصول إلى الإعدادات</string>\n  <string name=\"long_press_for_options\">اضغط ضغطة طويلة للوصول إلى الخيارات</string>\n  <string name=\"device_specific_backup\">النسخ الاحتياطي لهذا الجهاز فقط</string>\n  <string name=\"local_backup\">النسخ الاحتياطي المحلي</string>\n  <string name=\"auto_backup\">النسخ الاحتياطي التلقائي (مرة واحدة يوميًا)</string>\n  <string name=\"repeat_mode_reverse_list\">تشغيل الفيديوهات في قائمة التشغيل أو القناة بترتيب عكسي</string>\n  <string name=\"calm_msg\">نحن نعمل على إصلاح المشكلة. تحقق من التحديثات من وقت لآخر</string>\n  <string name=\"without_picture\">بدون صورة</string>\n  <string name=\"video_disabled\">تم تعطيل الفيديو</string>\n  <string name=\"applying_fix\">لا تغلق المشغل. جارٍ تطبيق الإصلاح...</string>\n  <string name=\"player_global_focus_desc\">تؤثر هذه الميزة على زر المشغل الذي سيحصل على التركيز عند التنقل بين صفوف أزرار المشغل</string>\n  <string name=\"disable_network_error_fixing\">تعطيل إصلاح أخطاء الشبكة تلقائيًا</string>\n  <string name=\"disable_network_error_fixing_desc\">ربما تحتاج إلى تمكين هذا الخيار إذا كنت تستخدم VPN</string>\n  <string name=\"recommended\">الفيديوهات المقترحة</string>\n  <string name=\"add_to_subscriptions_group\">إضافة/إزالة من مجموعة الاشتراك</string>\n  <string name=\"new_subscriptions_group\">مجموعة جديدة</string>\n  <string name=\"rename_group\">إعادة تسمية مجموعة الاشتراك</string>\n  <string name=\"screen_dimming_amount\">مستوى تعتيم الشاشة</string>\n  <string name=\"screen_dimming_timeout\">مهلة تعتيم الشاشة</string>\n  <string name=\"playlists_rows\">عرض قسم قوائم التشغيل كصفوف</string>\n  <string name=\"original_lang\">الأصلية</string>\n  <string name=\"search_exit_shortcut\">الخروج من البحث</string>\n  <string name=\"context_menu_sorting\">فرز القائمة المنبثقة</string>\n  <string name=\"hide_shorts_from_search\">إخفاء فيديوهات Shorts من نتائج البحث</string>\n  <string name=\"remove_playlist_fmt\">هل تريد إزالة %s بشكل دائم؟</string>\n  <string name=\"video_flip\">انعكاس (مرآة)</string>\n  <string name=\"play_from_start\">التشغيل من البداية</string>\n  <string name=\"player_chapter_notification2\">الانتقال إلى الفصل التالي بالنقر على الإشعار</string>\n  <string name=\"hide_mixes\">إخفاء Mixes</string>\n  <string name=\"import_subscriptions_group\">استيراد</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">إيقاف الترجمة والشرح</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">تمكين الترجمة والشرح</string>\n  <string name=\"my_videos\">فيديوهاتي</string>\n  <string name=\"player_audio_focus\">التركيز الصوتي (إيقاف عند اكتشاف مشغلات أخرى)</string>\n  <string name=\"paid_content_notification\">إشعار المحتوى المدفوع</string>\n  <string name=\"you_liked\">أعجبك</string>\n  <string name=\"premium_users_only\">لمستخدمين Premium فقط. إصلاح قائمة صيغ الفيديو غير المكتملة</string>\n  <string name=\"playback_buffering_fix\">إصلاح التخزين المؤقت للتشغيل</string>\n  <string name=\"oculus_quest_fix\">إصلاح Oculus Quest</string>\n  <string name=\"card_preview_muted\">فيديو بدون الصوت</string>\n  <string name=\"card_preview_full\">فيديو مع الصوت</string>\n  <string name=\"card_preview\">معاينة البطاقة</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">عدم تغيير حجم الفيديو ليتناسب مع مربع الحوار</string>\n  <string name=\"typing_corrections\">تعطيل التصحيح التلقائي أثناء كتابة النص</string>\n  <string name=\"card_unlocalized_titles\">عناوين الفيديو غير مترجمة</string>\n  <string name=\"suggestions_horizontally_scrolled\">اقتراحات قابلة للتمرير أفقيًا</string>\n  <string name=\"stable_restore\">استعادة الإصدار المستقر (سيتم فقدان جميع البيانات)</string>\n  <string name=\"auto_backup_category\">النسخ الاحتياطي التلقائي</string>\n  <string name=\"once_a_day\">مرة واحدة يوميًا</string>\n  <string name=\"once_a_week\">مرة واحدة أسبوعيًا</string>\n  <string name=\"once_a_month\">مرة واحدة شهريًا</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-az/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"header_home\">Ev</string>\n    <string name=\"title_search\">Axtarış</string>\n    <string name=\"header_subscriptions\">Abunəliklər</string>\n    <string name=\"header_history\">Tarixçə</string>\n    <string name=\"header_music\">Musiqi</string>\n    <string name=\"header_news\">Xəbərlər</string>\n    <string name=\"header_gaming\">Oyun</string>\n    <string name=\"header_playlists\">Pleylistlər</string>\n    <string name=\"header_settings\">Parametrlər</string>\n    <string name=\"header_channels\">Kanallar</string>\n    <string name=\"subscriptions_signin_title\">Sevdiyiniz kanallardan ən sonlara baxın</string>\n    <string name=\"subscriptions_signin_subtitle\">Abunəliklərinizi görmək üçün daxil olun</string>\n    <string name=\"subscriptions_signin_button_text\">DAXİL OL</string>\n    <string name=\"library_signin_to_show_more\">Bəyəndiyiniz, saxladığınız və ya abunə olduğunuz videolara baxın</string>\n    <string name=\"library_signin_subtitle\">Kitabxananıza baxmaq üçün daxil olun</string>\n    <string name=\"action_signin\">DAXİL OL</string>\n    <string name=\"title_video_formats\">Video formatları</string>\n    <string name=\"title_audio_formats\">Audio formatları</string>\n    <string name=\"subtitle_category_title\">Subtitrlər</string>\n    <string name=\"playback_queue_category_title\">Oxutma növbəsi</string>\n    <string name=\"video_max_quality\">Avto (maks keyfiyyət)</string>\n    <string name=\"audio_max_quality\">Avto (maks keyfiyyət)</string>\n    <string name=\"subtitles_disabled\">Subtitrlər söndürülüb</string>\n    <string name=\"auto_frame_rate\">Auto Frame Rate</string>\n    <string name=\"frame_rate_correction\">Fps düzəltmək:\\n%s</string>\n    <string name=\"category_background_playback\">Arxa fonda oxutma</string>\n    <string name=\"not_implemented\">Tətbiq olunmayıb</string>\n    <string name=\"not_supported_by_device\">Cihaz bu funksiyanı dəstəkləmir</string>\n    <string name=\"option_background_playback_off\">Qeyri-aktivdir</string>\n    <string name=\"option_background_playback_pip\">Şəkil içində şəkil</string>\n    <string name=\"option_background_playback_only_audio\">Yalnız audio</string>\n    <string name=\"playback_settings\">Oxutma keyfiyyəti parametrləri</string>\n    <string name=\"video_speed\">Video sürəti</string>\n    <string name=\"resolution_switch\">Görüntü imkanını dəyiş</string>\n    <string name=\"video_buffer\">Video bufer</string>\n    <string name=\"video_buffer_size_none\">Heç biri</string>\n    <string name=\"video_buffer_size_lowest\">Ən aşağı</string>\n    <string name=\"video_buffer_size_low\">Aşağı</string>\n    <string name=\"video_buffer_size_med\">Orta</string>\n    <string name=\"video_buffer_size_high\">Yüksək</string>\n    <string name=\"video_buffer_size_highest\">Ən yüksək</string>\n    <string name=\"update_changelog\">Dəyişiklik jurnalı</string>\n    <string name=\"install_update\">Yeniləməni quraşdırın</string>\n    <string name=\"section_is_empty\">Ups! Burada heç nə yoxdur</string>\n    <string name=\"dialog_add_to_playlist\">Pleylistdən Əlavə et/Çıxar</string>\n    <string name=\"msg_signed_users_only\">Yalnız daxil olmuş istifadəçilər</string>\n    <string name=\"msg_cant_load_content\">Məzmunu yükləmək mümkün deyil.\\n1) Cihazda tarix və vaxtı yoxlayın.\\n2) Şəbəkə bağlantısını yoxlayın.\\n3) Proksi parametrlərinizi yoxlayın.\\n4) Hesabınıza daxil olmağa çalışın.\\n5) Bəlkə burada heç nə yoxdur.</string>\n    <string name=\"title_video_presets\">Video presetləri</string>\n    <string name=\"settings_accounts\">Hesablar</string>\n    <string name=\"settings_left_panel\">Kateqoriyaları redaktə et</string>\n    <string name=\"settings_themes\">Temalar</string>\n    <string name=\"settings_other\">Başqa</string>\n    <string name=\"settings_player\">Video pleyer</string>\n    <string name=\"settings_language\">Dil</string>\n    <string name=\"settings_linked_devices\">Əlaqəli cihazlar</string>\n    <string name=\"settings_about\">Haqqında</string>\n    <string name=\"dialog_account_list\">Hesab seç</string>\n    <string name=\"dialog_account_none\">Heç biri</string>\n    <string name=\"dialog_remove_account\">Çıxış et</string>\n    <string name=\"dialog_add_account\">Daxil ol</string>\n    <string name=\"default_lang\">Defolt</string>\n    <string name=\"dialog_select_language\">Dil</string>\n    <string name=\"subtitle_default\">Defolt</string>\n    <string name=\"subtitle_white_semi_transparent\">Yarımşəffaf fonda ağ</string>\n    <string name=\"subtitle_style\">Subtitrin üslubu</string>\n    <string name=\"subtitle_language\">Subtitrin dili</string>\n    <string name=\"action_search\">Axtarış</string>\n    <string name=\"settings_main_ui\">İstifadəçi interfeysi</string>\n    <string name=\"dialog_main_ui\">İstifadəçi interfeysi</string>\n    <string name=\"card_animated_previews\">Animasiyalı önbaxışlar</string>\n    <string name=\"web_site\">Web səhifə</string>\n    <string name=\"donation\">İanə</string>\n    <string name=\"dialog_about\">Haqqında</string>\n    <string name=\"dialog_player_ui\">Video pleyer</string>\n    <string name=\"player_show_ui_on_pause\">Fasilədə interfeysi göstərin</string>\n    <string name=\"player_pause_on_ok\">OK düyməsi oxutmanı dayandırır</string>\n    <string name=\"player_ok_button_behavior\">OK düyməsinin davranışı</string>\n    <string name=\"player_only_ui\">Yalnız interfeys</string>\n    <string name=\"player_ui_and_pause\">İnterfeys və fasilə</string>\n    <string name=\"player_only_pause\">Yalnız fasilə</string>\n    <string name=\"check_for_updates\">Yeniləməni yoxlayın</string>\n    <string name=\"update_not_found\">Ən son versiyadan istifadə edirsiniz</string>\n    <string name=\"update_in_progress\">Gözləyin…</string>\n    <string name=\"player_ui_hide_behavior\">İnterfeysi avto-gizlət</string>\n    <string name=\"option_never\">Heç vaxt</string>\n    <string name=\"side_panel_sections\">Quraşdırma bölmələri</string>\n    <string name=\"boot_to_section\">Bölməyə qoşulun</string>\n    <string name=\"large_ui\">Böyük interfeys</string>\n    <string name=\"video_grid_scale\">Video toru miqyası</string>\n    <string name=\"scale_ui\">İstifadəçi interfeysi miqyası</string>\n    <string name=\"color_scheme\">Rəng sxemi</string>\n    <string name=\"color_scheme_default\">Defolt</string>\n    <string name=\"color_scheme_red_grey\">Qırmızı-boz</string>\n    <string name=\"color_scheme_red\">Qırmızı</string>\n    <string name=\"color_scheme_dark_grey\">Qırmızı Boz</string>\n    <string name=\"disable_update_check\">Yeniləmə yoxlamasını söndürün</string>\n    <string name=\"show_again\">Yenidən göstər</string>\n    <string name=\"check_updates_auto\">Yeniləmə haqqında məlumat verin</string>\n    <string name=\"select_account_on_boot\">Qoşulanda hesab seçimini göstərin</string>\n    <string name=\"player_other\">Müxtəlif</string>\n    <string name=\"player_full_date\">Təsvirdə dəqiq tarix</string>\n    <string name=\"open_channel\">Kanalı aç</string>\n    <string name=\"open_playlist\">Pleylisti aç</string>\n    <string name=\"not_interested\">Maraqlı deyil</string>\n    <string name=\"not_recommend_channel\">Kanalı tövsiyə etməyin</string>\n    <string name=\"you_wont_see_this_video\">Video tövsiyə olunanlardan silindi</string>\n    <string name=\"you_wont_see_this_channel\">Bu kanalı tövsiyələrdə görməyəcəksiniz</string>\n    <string name=\"settings_search\">Axtarış</string>\n    <string name=\"dialog_search\">Axtarış</string>\n    <string name=\"instant_voice_search\">Ani səsli axtarış</string>\n    <string name=\"option_background_playback_behind\">Arxada oxudun</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">Növbəti video məlumatı hələ yüklənməyib</string>\n    <string name=\"card_multiline_title\">Çoxsətirli başlıqlar</string>\n    <string name=\"cards_style\">Kartların üslubu</string>\n    <string name=\"repeat_mode_all\">Videoları davamlı olaraq oxudun</string>\n    <string name=\"repeat_mode_one\">Cari videonu təkrarlayın</string>\n    <string name=\"repeat_mode_pause\">Hər videodan sonra oxutmanı dayandırın</string>\n    <string name=\"repeat_mode_pause_alt\">Yalnız pleylist videolarını davamlı olaraq oxudun</string>\n    <string name=\"repeat_mode_none\">Bir videodan sonra oxutmanı dayandırın</string>\n    <string name=\"subscribed_to_channel\">Abunə olundu</string>\n    <string name=\"unsubscribed_from_channel\">Abunəlikdən çıxıldı</string>\n    <string name=\"subtitle_yellow_transparent\">Şəffaf fonda sarı</string>\n    <string name=\"subtitle_white_transparent\">Şəffaf fonda ağ</string>\n    <string name=\"subtitle_white_black\">Qara fonda ağ</string>\n    <string name=\"player_seek_preview\">Axtararkən önbaxış</string>\n    <string name=\"color_scheme_dark_grey_oled\">Tünd Boz(OLED)</string>\n    <string name=\"channels_section_sorting\">Kanallar bölməsinin çeşidlənməsi</string>\n    <string name=\"sorting_by_new_content\">Yeni məzmun</string>\n    <string name=\"sorting_alphabetically\">Əlifba sırası ilə</string>\n    <string name=\"sorting_last_viewed\">Son baxılan</string>\n    <string name=\"player_pause_when_seek\">Axtararkən fasilə verin</string>\n    <string name=\"playlists_style\">Pleylist bölməsinin üslubu</string>\n    <string name=\"playlists_style_grid\">Tor</string>\n    <string name=\"playlists_style_rows\">Sıralar</string>\n    <string name=\"player_show_clock\">İdarəetmə panelində saatı göstərin</string>\n    <string name=\"player_show_remaining_time\">Qalan vaxtı idarəetmə panelində göstərin</string>\n    <string name=\"player_show_ending_time\">İdarəetmə panelində bitmə vaxtını göstərin</string>\n    <string name=\"open_channel_uploads\">Kanal yükləmələrini açın</string>\n    <string name=\"app_exit_shortcut\">Proqramdan çıxın</string>\n    <string name=\"player_exit_shortcut\">Pleyerdən çıx</string>\n    <string name=\"app_exit_none\">Çıxma</string>\n    <string name=\"app_double_back_exit\">İkiqat arxa</string>\n    <string name=\"app_single_back_exit\">Tək arxa</string>\n    <string name=\"action_video_zoom\">Video zoom</string>\n    <string name=\"video_zoom\">Video zoom</string>\n    <string name=\"video_zoom_default\">Defolt</string>\n    <string name=\"video_zoom_fit_width\">Eni uyğunlaşdırın</string>\n    <string name=\"video_zoom_fit_height\">Uzunluğu uyğunlaşdırın</string>\n    <string name=\"video_zoom_fit_both\">Eni və ya uzunluğu uyğunlaşdırın</string>\n    <string name=\"video_zoom_stretch\">Uzatmaq</string>\n    <string name=\"color_scheme_teal\">Mavi</string>\n    <string name=\"color_scheme_teal_oled\">Mavi (OLED)</string>\n    <string name=\"player_seek_preview_none\">Söndürülmüş</string>\n    <string name=\"player_seek_preview_single\">Tək kadr</string>\n    <string name=\"player_seek_preview_carousel\">Karusel</string>\n    <string name=\"player_seek_preview_carousel_slow\">Karusel (yavaş, əsas kadrlar üzrə)</string>\n    <string name=\"player_seek_preview_carousel_fast\">Karusel (sürətli, dəqiq baxış deyil)</string>\n    <string name=\"unsubscribe_from_channel\">Kanala abunə olma</string>\n    <string name=\"card_title_lines_num\">Kartın başlıq sətirlərinin sayı</string>\n    <string name=\"card_auto_scrolled_title\">Kəsilmiş başlığı avtomatik sürüşdürün</string>\n    <string name=\"share_link\">Keçidi paylaş</string>\n    <string name=\"share_qr_link\">Keçidi paylaş (QR kod)</string>\n    <string name=\"subscribe_to_channel\">Kanala abunə ol</string>\n    <string name=\"wait_data_loading\">Xahiş edirik məlumat yüklənənədək gözləyin…</string>\n    <string name=\"auto_frame_rate_pause\">Auto Frame Rate (keçid zamanı fasilə verin)</string>\n    <string name=\"auto_frame_rate_applying\">Auto frame rate tətbiq olunur %sx%s@%s</string>\n    <string name=\"audio_shift\">Audio yerdəyişməsi</string>\n    <string name=\"player_remember_speed\">Sürəti xatırlayın</string>\n    <string name=\"mark_channel_as_watched\">Baxılmış kimi işarələ</string>\n    <string name=\"channel_marked_as_watched\">Kanal baxılmış kimi işarələndi</string>\n    <string name=\"dialog_add_device\">Cihaz əlavə et</string>\n    <string name=\"dialog_remove_all_devices\">Bütün cihazları sil</string>\n    <string name=\"device_link_enabled\">Keçid qoşuldu</string>\n    <string name=\"device_connected\">Cihazla \\\"%s\\\" əlaqə quruldu</string>\n    <string name=\"device_disconnected\">Cihazla \\\"%s\\\" əlaqə kəsildi</string>\n    <string name=\"settings_ui_scale\">İnterfeys miqyası</string>\n    <string name=\"msg_restart_app\">Bu parametrləri tətbiq etmək üçün proqramı yenidən başladın</string>\n    <string name=\"settings_block\">Məzmun bloklanması</string>\n    <string name=\"msg_applying\">Tətbiq edilir %s…</string>\n    <string name=\"msg_done\">Hazırdır</string>\n    <string name=\"settings_general\">Ümumi</string>\n    <string name=\"settings_video\">Video</string>\n    <string name=\"content_block_confirm_skip\">Ötürərkən təsdiqləyin</string>\n    <string name=\"content_block_categories\">Bölmələri ötür</string>\n    <string name=\"content_block_sponsor\">Sponsor</string>\n    <string name=\"content_block_intro\">Fasilə/giriş animasiyası</string>\n    <string name=\"content_block_outro\">Son kartları/kreditlər</string>\n    <string name=\"content_block_interaction\">Qarşılıqlı əlaqə xatırlatması (abunə ol)</string>\n    <string name=\"content_block_self_promo\">Ödənişsiz/özünü tanıtma</string>\n    <string name=\"content_block_music_off_topic\">Klipin musiqi olmayan bölməsi</string>\n    <string name=\"confirm_segment_skip\">Hissəni ötürün \\\"%s\\\"?</string>\n    <string name=\"cancel_segment_skip\">Hissə ötürülməsini ləğv edin</string>\n    <string name=\"msg_skipping_segment\">Hissə ötürülür \\\"%s\\\"…</string>\n    <string name=\"content_block_notification_type\">Bildiriş növü</string>\n    <string name=\"content_block_notify_none\">Bildirişsiz</string>\n    <string name=\"content_block_notify_toast\">Toast</string>\n    <string name=\"content_block_notify_dialog\">Təsdiq dialoqu</string>\n    <string name=\"return_to_launcher\">ATV kanallarından/axtarışdan launcher qayıdın</string>\n    <string name=\"intent_force_close\">Video xarici mənbədən açılıbsa, çıxmağa məcbur edin</string>\n    <string name=\"btn_confirm\">Təsdiqlə</string>\n    <string name=\"player_low_video_quality\">Aşağı video keyfiyyəti</string>\n    <string name=\"remote_session_closed\">Uzaqdan idarəetmə seansı bağlandı</string>\n    <string name=\"msg_mode_switch_error\">Ekran rejimini dəyişmək mümkün deyil \\\"%s\\\"</string>\n    <string name=\"msg_player_error\">Pleyer xətası baş verdi %s. Oxutma yenidən başladılır…</string>\n    <string name=\"video_preset_disabled\">Presetsiz</string>\n    <string name=\"video_preset_enabled\">Növbəti videonun formatı əvvəlcədən təyin edilmiş qaydada qurulacaq</string>\n    <string name=\"player_sleep_timer\">Yuxu taymeri</string>\n    <string name=\"player_show_quality_info\">İdarəetmə panelində keyfiyyət məlumatını göstərin</string>\n    <string name=\"player_remember_each_speed\">Hər bir videonun sürətini xatırlayın</string>\n    <string name=\"player_remember_speed_none\">Heç biri</string>\n    <string name=\"player_remember_speed_all\">Bütün videolar üçün eyni</string>\n    <string name=\"player_remember_speed_each\">Hər video üçün</string>\n    <string name=\"msg_player_error_source\">Videonu endirmək olmur</string>\n    <string name=\"msg_player_error_renderer\">Seçilmiş format dəstəklənmir.\\nBaşqasını seçməyə cəhd edin.\\nƏgər bu kömək etmirsə, cihazı yenidən başlatmağa cəhd edin.</string>\n    <string name=\"msg_player_error_video_renderer\">Seçilmiş VİDEO formatı dəstəklənmir.\\nPleyerdə HQ düyməsini sıxaraq başqasını seçməyə cəhd edin.\\nƏgər bu kömək etməsə, cihazı yenidən işə salmağa cəhd edin.</string>\n    <string name=\"msg_player_error_audio_renderer\">Seçilmiş AUDIO formatı dəstəklənmir.\\nPleyerdə HQ düyməsini sıxmaqla başqasını seçməyə cəhd edin.\\nƏgər bu kömək etməsə, cihazı yenidən işə salmağa cəhd edin.</string>\n    <string name=\"msg_player_error_subtitle_renderer\">Seçilmiş SUBTİTR formatı dəstəklənmir.\\nPleyerdə CC düyməsini uzun basaraq başqasını seçməyə çalışın.\\nƏgər bu kömək etməsə, cihazı yenidən başlatmağa cəhd edin.</string>\n    <string name=\"msg_player_error_unexpected\">Naməlum video dekoder xətası</string>\n    <string name=\"video_aspect\">Aspekt nisbəti</string>\n    <string name=\"player_tweaks\">Tərtibatçılar üçün</string>\n    <string name=\"header_uploads\">Yükləmələr</string>\n    <string name=\"player_show_global_clock\">Həmişə ekranda olan saatı göstərin</string>\n    <string name=\"player_show_global_ending_time\">Həmişə ekranda olan bitmə vaxtını göstərin</string>\n    <string name=\"app_corner_clock\">Ev: yuxarı sağ künc saatı</string>\n    <string name=\"player_corner_clock\">Pleyer: yuxarı sağ küncdə saat</string>\n    <string name=\"player_corner_ending_time\">Pleyer: yuxarı sağ küncdə bitmə vaxtı</string>\n    <string name=\"uploads_old_look\">Yükləmələr bölməsinin köhnə görünüşünü bərpa edin</string>\n    <string name=\"background_playback_activation\">Arxa fon oxutması (aktivləşdirmə)</string>\n    <string name=\"channels_old_look\">Kanallar bölməsinin köhnə görünüşünü bərpa edin</string>\n    <string name=\"channels_auto_load\">Kanallar bölməsinin məzmununu avtomatik yükləyin</string>\n    <string name=\"return_to_background_video\">Arxa fonda işləyən videoya qayıdın</string>\n    <string name=\"pin_unpin_from_sidebar\">Yan paneldən Əlavə et/Çıxar</string>\n    <string name=\"pin_unpin_playlist\">Yan paneldən pleylist Əlavə edin/Çıxarın</string>\n    <string name=\"pin_unpin_channel\">Yan paneldən kanal Əlavə et/Çıxar</string>\n    <string name=\"pin_playlist\">Yan panelə pleylist əlavə edin</string>\n    <string name=\"pin_channel\">Yan panelə kanal əlavə edin</string>\n    <string name=\"pinned_to_sidebar\">Yan panelə əlavə edildi</string>\n    <string name=\"unpin_from_sidebar\">Yan paneldən çıxarın</string>\n    <string name=\"unpin_group_from_sidebar\">Abunəlik qrupunu silin</string>\n    <string name=\"unpinned_from_sidebar\">Yan paneldən silindi</string>\n    <string name=\"dialog_select_country\">Ölkə</string>\n    <string name=\"settings_language_country\">Dil/Ölkə</string>\n    <string name=\"share_embed_link\">Daxil edilə bilən keçidi paylaşın</string>\n    <string name=\"card_text_scroll_factor\">Kart mətninin sürüşmə sürəti</string>\n    <string name=\"preferred_update_source\">Yeniləmə mənbəyini seçin</string>\n    <string name=\"hide_shorts\">Abunəliklərdən shortları gizlət</string>\n    <string name=\"hide_shorts_channel\">Shortları Kanaldan gizlət</string>\n    <string name=\"key_remapping\">Düymənin yenidən təyin edilməsi</string>\n    <string name=\"screen_dimming\">Ekranın qaralması</string>\n    <string name=\"removed_from_playback_queue\">Oxutma növbəsindən silindi</string>\n    <string name=\"added_to_playback_queue\">Oxutma növbəsinə əlavə edildi</string>\n    <string name=\"add_remove_from_playback_queue\">Oxutma növbəsindən Əlavə et/Çıxar</string>\n    <string name=\"add_to_playback_queue\">Oxutma növbəsinə əlavə edin</string>\n    <string name=\"remove_from_playback_queue\">Oxutma növbəsindən silin</string>\n    <string name=\"proxy_enabled\">Proxy qoşuldu</string>\n    <string name=\"proxy_disabled\">Proxy söndürüldü</string>\n    <string name=\"uploads_row_name\">Yükləmələr</string>\n    <string name=\"playlists_row_name\">Pleylistlər</string>\n    <string name=\"popular_uploads_row_name\">Populyar yükləmələr</string>\n    <string name=\"news_row_name\">Xəbərlər</string>\n    <string name=\"breaking_news_row_name\">Qaynar Xəbərlər</string>\n    <string name=\"covid_news_row_name\">COVID-19 xəbərləri</string>\n    <string name=\"enable_voice_search\">Qlobal axtarışı aktivləşdirin (proqram dəstəyi lazımdır)</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">Kanala Abunə ol/Abunə olma</string>\n    <string name=\"app_backup_restore\">Ehtiyat nüsxəsi/Bərpa edin</string>\n    <string name=\"app_backup\">Məlumatların ehtiyyat nüsxəsini çıxarın</string>\n    <string name=\"app_restore\">Ehtiyyat nüsxəsindən bərpa edin</string>\n    <string name=\"skip_each_segment_once\">Seqmentləri yenidən ötürməyin</string>\n    <string name=\"disable_ok_long_press\">OK düyməsini uzun basmağı söndürün</string>\n    <string name=\"disable_ok_long_press_desc\">OK düyməsinin düzgün işləmədiyi pultlar üçün nəzərdə tutulub</string>\n    <string name=\"player_time_correction\">Sürətlə bağlı vaxtı göstərin</string>\n    <string name=\"sponsor_color_markers\">Tərəqqi çubuğunda rəng markerləri</string>\n    <string name=\"refresh_section\">Bölməni yenilə</string>\n    <string name=\"option_disabled\">Söndürülüb</string>\n    <string name=\"live_now_row_name\">İndi efirdə</string>\n    <string name=\"run_in_background\">Arxa fonda oxutmaq</string>\n    <string name=\"settings_remote_control\">Uzaqdan idarə etmə</string>\n    <string name=\"background_service_started\">Arxa fon xidməti başladı</string>\n    <string name=\"dialog_add_to\">Əlavə etmək %s</string>\n    <string name=\"dialog_remove_from\">Çıxarın %s</string>\n    <string name=\"added_to\">Əlavə olundu %s</string>\n    <string name=\"removed_from\">Çıxarıldı %s</string>\n    <string name=\"video_preset_adaptive\">Adaptiv</string>\n    <string name=\"content_block_preview_recap\">Videonun önbaxışı və ya təkrarı</string>\n    <string name=\"content_block_highlight\">Maraqlı məqam (əlfəcin)</string>\n    <string name=\"removed_from_history\">Video tarixçədən silindi</string>\n    <string name=\"remove_from_history\">Tarixçədən sil</string>\n    <string name=\"upload_date\">Yüklənmə vaxtı</string>\n    <string name=\"upload_date_any\">Həmişə</string>\n    <string name=\"upload_date_today\">Bugün</string>\n    <string name=\"upload_date_this_week\">Bu həftə</string>\n    <string name=\"upload_date_this_month\">Bu ay</string>\n    <string name=\"upload_date_this_year\">Bu il</string>\n    <string name=\"double_refresh_rate\">Tezliyi iki dəfə artırın</string>\n    <string name=\"upload_date_last_hour\">Son saat</string>\n    <string name=\"focus_on_search_results\">Axtarış nəticələrinə avtofokus</string>\n    <string name=\"context_menu\">Kontekst menyusu</string>\n    <string name=\"add_remove_from_recent_playlist\">Son pleylistdən əlavə et/çıxar</string>\n    <string name=\"hide_settings_section\">Parametrlər bölməsini gizlədin (təhlükəli!)</string>\n    <string name=\"volume\">Səs həcmi %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s san</string>\n    <string name=\"audio_shift_sec\">%s san</string>\n    <string name=\"ui_hide_timeout_sec\">%s san</string>\n    <string name=\"screen_dimming_timeout_min\">%s dəq</string>\n    <string name=\"mark_all_channels_watched\">Bütün kanalları baxılmış kimi qeyd edin</string>\n    <string name=\"move_section_up\">Bölməni yuxarıya köçürün</string>\n    <string name=\"move_section_down\">Bölməni aşağıya köçürün</string>\n    <string name=\"player_buttons\">Pleyer düymələrini quraşdırın</string>\n    <string name=\"action_playlist_add\">Pleylistə əlavə et</string>\n    <string name=\"action_video_stats\">Video statistikası</string>\n    <string name=\"action_subscribe\">Abunə ol</string>\n    <string name=\"action_channel\">Kanalı aç</string>\n    <string name=\"action_pip\">PIP</string>\n    <string name=\"action_video_info\">Video təsviri</string>\n    <string name=\"action_screen_off\">Ekranı söndürmək</string>\n    <string name=\"action_sound_off\">Səsi söndürmək</string>\n    <string name=\"action_playback_queue\">Oxutma növbəsi</string>\n    <string name=\"action_video_speed\">Videonun sürəti</string>\n    <string name=\"action_subtitles\">Subtitrlər</string>\n    <string name=\"action_like\">Bəyənirəm</string>\n    <string name=\"action_dislike\">Bəyənmirəm</string>\n    <string name=\"action_play_pause\">Oxut/Fasilə</string>\n    <string name=\"action_repeat_mode\">Oxutma rejimi</string>\n    <string name=\"action_next\">Sonrakı video</string>\n    <string name=\"action_previous\">Əvvəlki video</string>\n    <string name=\"action_high_quality\">Video keyfiyyəti</string>\n    <string name=\"content_block_no_skipping_mode\">Ötürməmək rejimi</string>\n    <string name=\"content_block_action_type\">Fəaliyyət seçin</string>\n    <string name=\"content_block_action_none\">Heç nə etmə</string>\n    <string name=\"content_block_action_only_skip\">Yalnız ötür</string>\n    <string name=\"content_block_action_toast\">Bildirişlərlə ötür</string>\n    <string name=\"content_block_action_dialog\">Təsdiqləmə dialoqunu göstər</string>\n    <string name=\"content_block_filler\">Mövzudan kənar (doldurucu)</string>\n    <string name=\"keyboard_auto_show\">Avtomatik olaraq klaviaturanı göstər</string>\n    <string name=\"cancel_dialog\">Ləğv et</string>\n    <string name=\"msg_player_error_source2\">URL işləmir və ya yanlış cihaz vaxtı.\\nAdətən, bu, proqram problemidir.</string>\n    <string name=\"msg_player_error_video_source\">VIDEO URL işləmir və ya yanlış cihaz vaxtı.\\nAdətən, bu, proqram problemidir.</string>\n    <string name=\"msg_player_error_audio_source\">AUDIO URL-i işləmir və ya yanlış cihaz vaxtı.\\nAdətən, bu, proqram problemidir.</string>\n    <string name=\"msg_player_error_subtitle_source\">SUBTİTR URL işləmir və ya yanlış cihaz vaxtı.\\nAdətən, bu, proqram problemidir.</string>\n    <string name=\"hide_upcoming\">Abunəliklərdən gələni gizlət</string>\n    <string name=\"hide_upcoming_channel\">Kanaldan gələni gizlət</string>\n    <string name=\"hide_upcoming_home\">Ev gələni gizlədin</string>\n    <string name=\"search_background_playback\">Axtarış/açıq kanalda PIP daxil edin</string>\n    <string name=\"trending_row_name\">Trendlər</string>\n    <string name=\"player_seek_type\">Davranış axtarın</string>\n    <string name=\"player_seek_regular\">Adi</string>\n    <string name=\"player_seek_confirmation_pause\">Təsdiqlə (axtararkən fasilə verin)</string>\n    <string name=\"player_seek_confirmation_play\">Təsdiqlə (axtararkən oxudun)</string>\n    <string name=\"hide_shorts_from_home\">Shortları Evdən gizlədin</string>\n    <string name=\"finish_on_disconnect\">Telefonu/planşeti ayırdıqdan sonra proqramı bitirin</string>\n    <string name=\"update_error\">Yeniləmə xətası</string>\n    <string name=\"hide_shorts_from_search\">Shortları axtarış nəticəsindən gizlədin</string>\n    <string name=\"hide_shorts_from_history\">Shortları Tarixçədən gizlədin</string>\n    <string name=\"hide_shorts_from_trending\">Shortları Trenddən gizlədin</string>\n    <string name=\"disable_screensaver\">Ekran qoruyucusunu gizlədin</string>\n    <string name=\"description_not_found\">Təsvir tapılmadı</string>\n    <string name=\"proxy_port_hint\">Proxy portu</string>\n    <string name=\"proxy_host_hint\">Proxy host adı və ya IP</string>\n    <string name=\"proxy_username_hint\">Proxy istifadəçi adı</string>\n    <string name=\"proxy_password_hint\">Proxy şifrəsi</string>\n    <string name=\"proxy_type\">Proxy Növü</string>\n    <string name=\"enable_web_proxy\">Web Proxy istifadə et</string>\n    <string name=\"proxy_not_supported\">Web Proxy istifadə et (Android 4.4+ zəruridir)</string>\n    <string name=\"proxy_test_btn\">Test</string>\n    <string name=\"proxy_settings_title\">Proxy Xidmət Parametrləri</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">Test#%d: ləğv edildi.</string>\n    <string name=\"proxy_type_invalid\">Proxy növü seçilməyib.</string>\n    <string name=\"proxy_host_invalid\">Proxy hostu seçilməyib.</string>\n    <string name=\"proxy_port_invalid\">Yanlış proxy portu, olmalıdır &gt; 0.</string>\n    <string name=\"proxy_credentials_invalid\">Yanlış istifadəçi adı və ya şifrə.</string>\n    <string name=\"proxy_test_aborted\">Test dayandırıldı, əvvəlcə proksi parametrlərini düzəldin.</string>\n    <string name=\"proxy_application_aborted\">Zəhmət olmasa, əvvəlcə proksi parametrlərini düzəldin.</string>\n    <string name=\"proxy_test_start\">Test#%d: %s …</string>\n    <string name=\"proxy_test_error\">Xəta#%d: %s</string>\n    <string name=\"proxy_test_status\">Status#%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">Konfiqurasiya faylının ünvanı (*.ovpn)</string>\n    <string name=\"enable_openvpn\">OpenVPN istifadə et</string>\n    <string name=\"openvpn_settings_title\">OpenVPN Parametrləri</string>\n    <string name=\"openvpn_application_aborted\">Zəhmət olmasa, əvvəlcə OpenVPN parametrlərini düzəldin.</string>\n    <string name=\"openvpn_test_aborted\">Test dayandırıldı, xahiş edirik, əvvəlcə OpenVPN parametrlərini düzəldin.</string>\n    <string name=\"openvpn_address_invalid\">OpenVPN konfiqurasiya ünvanı təyin edilməyib.</string>\n    <string name=\"internet_censorship\">İnternet Senzurası</string>\n    <string name=\"rename_section\">Bu bölmənin adını dəyişdirin</string>\n    <string name=\"simple_edit_value_hint\">Qiymət daxil edin</string>\n    <string name=\"seek_interval\">Aralıq axtarın</string>\n    <string name=\"seek_interval_sec\">%s san</string>\n    <string name=\"subtitle_system\">Sistem üslubu (Android parametrləri) > Əlçatanlıq</string>\n    <string name=\"subtitle_scale\">Subtitr miqyası</string>\n    <string name=\"audio_sync_fix_desc\">Audio/video sinxronizasiyasının alternativ yolu. O, köhnəlmiş hesab olunur, lakin bəzi hallarda kömək edə bilər.</string>\n    <string name=\"audio_sync_fix\">Audio sinxronizasiyasını düzəldin</string>\n    <string name=\"ambilight_ratio_fix_desc\">Qeyri-bərabər işıqlandırmanı düzəldir. Yanlış aspekt nisbətini və ya video miqyasını düzəldir. Boş ekran görüntülərini düzəldir. Performansa təsir edir!</string>\n    <string name=\"ambilight_ratio_fix\">Ambilight/Aspekt nisbəti/Video miqyası/Skrinşotların düzəldilməsi</string>\n    <string name=\"force_legacy_codecs_desc\">Aşağı səviyyəli cihazlarda performansı əhəmiyyətli dərəcədə yaxşılaşdırır. Maksimum görüntü imkanı 720p.</string>\n    <string name=\"force_legacy_codecs\">Köhnə kodekləri məcbur edin (720p)</string>\n    <string name=\"live_stream_fix_desc\">Xəbərdarlıq edin, bu çimdik axınların geri çəkilməsini qeyri-aktiv edir. Aşağı səviyyəli cihazlarda canlı yayım performansını əhəmiyyətli dərəcədə yaxşılaşdırır. Maksimum görüntü imkanı 1080p.</string>\n    <string name=\"live_stream_fix_4k_desc\">Xəbərdarlıq edin, bu çimdik axınların geri çəkilməsini qeyri-aktiv edir. Canlı yayım performansını əhəmiyyətli dərəcədə yaxşılaşdırır. Maksimum görüntü imkanı 4K.</string>\n    <string name=\"live_stream_fix\">Canlı yayım düzəlişi (1080p)</string>\n    <string name=\"live_stream_fix_4k\">Canlı yayım düzəlişi (4K)</string>\n    <string name=\"playback_notifications_fix_desc\">İz dəyişikliyi bildirişlərini gizlədir. AOSP əsaslı proqram təminatında faydalı ola bilər.</string>\n    <string name=\"playback_notifications_fix\">Oxutma bildirişlərini deaktiv edin</string>\n    <string name=\"amlogic_fix_desc\">Amlogic əsaslı cihazlarda kadr düşməsinin düzəlişi.</string>\n    <string name=\"amlogic_fix\">Amlogic 1080p@60fps düzəldin</string>\n    <string name=\"tunneled_video_playback_desc\">QEYD: fasilə düzgün işləməyə bilər! Tunelləşdirilmiş video oxutma daha yaxşı audio/video sinxronizasiyası (AV sinxronizasiyası) və daha hamar oxutma kimi üstünlüklər vəd edir. Tələb olunan Android 5+</string>\n    <string name=\"tunneled_video_playback\">Tunelləşdirilmiş video oxutma (Android 5+)</string>\n    <string name=\"master_volume\">Master səs həcmi</string>\n    <string name=\"volume_limit\">Səs həcmi limiti</string>\n    <string name=\"player_volume\">Səs həcmi</string>\n    <string name=\"play_video\">Oxut</string>\n    <string name=\"remember_position_of_short_videos\">Qısa videoların mövqeyini yadda saxla (5 dəqiqədən az)</string>\n    <string name=\"remember_position_of_live_videos\">Canlı yayım mövqeyini yadda saxla</string>\n    <string name=\"player_show_tooltips\">Düymə üçün göstərişləri göstərin</string>\n    <string name=\"action_like_unset\">Bəyənmə təyin olunmayıb</string>\n    <string name=\"action_dislike_unset\">Bəyənməmə təyin olunmayıb</string>\n    <string name=\"various_buttons\">Əsas pəncərənin yuxarı hissəsindəki düymələr</string>\n    <string name=\"not_compatible_with\">Seçilmiş seçim uyğun deyil</string>\n    <string name=\"subtitle_position\">Subtitr aşağı sürüşdürmə</string>\n    <string name=\"pressing_home\">EV düyməsini basaraq</string>\n    <string name=\"pressing_home_back\">EV və ya GERİ düyməsini sıxmaqla</string>\n    <string name=\"pressing_back\">GERİ düyməsini basaraq</string>\n    <string name=\"player_number_key_seek\">Rəqəm düymələri ilə axtarın</string>\n    <string name=\"save_remove_playlist\">Pleylistlər bölməsindən pleylist Əlavə edin/Çıxarın</string>\n    <string name=\"save_playlist\">Pleylistlər bölməsinə pleylist əlavə edin</string>\n    <string name=\"remove_playlist\">Pleylistlər bölməsindən pleylisti həmişəlik silin</string>\n    <string name=\"removed_from_playlists\">Pleylistlər bölməsindən silindi</string>\n    <string name=\"saved_to_playlists\">Pleylistlər bölməsinə əlavə edildi</string>\n    <string name=\"create_playlist\">Pleylist düzəldin</string>\n    <string name=\"add_video_to_new_playlist\">Yeni pleylistə əlavə edin</string>\n    <string name=\"cant_delete_empty_playlist\">Boş pleylisti silmək mümkün deyil</string>\n    <string name=\"playlist\">Pleylist</string>\n    <string name=\"rename_playlist\">Pleylistin adını dəyişdirin</string>\n    <string name=\"cant_rename_empty_playlist\">Boş pleylistin adını dəyişmək mümkün deyil</string>\n    <string name=\"cant_rename_foreign_playlist\">Xarici pleylistin adını dəyişmək mümkün deyil</string>\n    <string name=\"cant_save_playlist\">Bu pleylist əlavə oluna bilməz</string>\n    <string name=\"enter_value\">Boş olmayan istənilən qiyməti daxil edin</string>\n    <string name=\"dialog_add_remove_from\">Əlavə et/Çıxar %s</string>\n    <string name=\"lb_playback_controls_skip_next\">Növbətiyə keç</string>\n    <string name=\"lb_playback_controls_skip_previous\">Əvvəlkinə qayıt/Başlanğıc mövqeyə qaytar</string>\n    <string name=\"alt_presets_behavior\">Alt presetlərin davranışı (bant genişliyini məhdudlaşdırın)</string>\n    <string name=\"alt_presets_behavior_desc\">Tətbiq görüntü imkanı, fps və kodek arasında uyğunluq əvəzinə, seçilmiş əvvəlcədən təyin edilmiş bant genişliyini saxlamağa çalışacaq.</string>\n    <string name=\"sleep_timer\">Yuxu taymeri (əgər pultdan bir saat istifadə olunmasa)</string>\n    <string name=\"sleep_timer_desc\">İstifadəçi bir saat ərzində pultdan istifadə etməyibsə, oxutmanı dayandırın</string>\n    <string name=\"disable_vsync\">Vsync-ə keçidi söndürün</string>\n    <string name=\"disable_vsync_desc\">Bu seçim ekranın şaquli sinxronizasiya siqnalı ilə kadrların uyğunlaşdırılmasını qeyri-aktiv edir. Bu, CPU resurslarını buraxmaqla aşağı səviyyəli cihazlarda performansı yaxşılaşdıra bilər.</string>\n    <string name=\"skip_codec_profile_check\">Kodek profil səviyyəsi yoxlamasını ötürün</string>\n    <string name=\"skip_codec_profile_check_desc\">Videoya başladıqda kodek dəstəyini yoxlamayın. Səhv olan proqram təminatında faydalı ola bilər.</string>\n    <string name=\"force_sw_codec\">SW video dekoderini məcbur edin</string>\n    <string name=\"force_sw_codec_desc\">Demək olar ki, istənilən videonu oxuda bilər, lakin performans çox zəifdir.</string>\n    <string name=\"playlist_order\">Pleylisti çeşidləyin</string>\n    <string name=\"playlist_order_added_date_newer_first\">Əlavə olunma vaxtı (ilk yeni)</string>\n    <string name=\"playlist_order_added_date_older_first\">Əlavə olunma vaxtı (ilk köhnə)</string>\n    <string name=\"playlist_order_published_date_newer_first\">Yüklənmə vaxtı (ilk yeni)</string>\n    <string name=\"playlist_order_published_date_older_first\">Yüklənmə vaxtı (ilk köhnə)</string>\n    <string name=\"playlist_order_popularity\">Populyarlıq</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">Xarici pleylist üçün bunu etmək mümkün deyil</string>\n    <string name=\"owned_playlist_warning\">Xəta. Bu əməliyyat yalnız şəxsi pleylistlər üçün tətbiq oluna bilər</string>\n    <string name=\"content_block_alt_server\">Alternativ serverdən istifadə edin</string>\n    <string name=\"content_block_alt_server_desc\">SponsorBlock müxtəlif səbəblərlə işləməkdən imtina edərsə, bu seçimi aktiv edin.</string>\n    <string name=\"unset_stream_reminder\">Yayım xatırladıcısının parametrini ləğv edin</string>\n    <string name=\"set_stream_reminder\">Yayım xatırladıcısını təyin edin</string>\n    <string name=\"playback_starts_shortly\">Yayım hazır olduqda oxutma avtomatik olaraq başlayacaq</string>\n    <string name=\"starting_stream\">Yayım başlayır…</string>\n    <string name=\"action_playlist_remove\">Pleylistdən silin</string>\n    <!-- BEGIN Moved strings -->\n    <string name=\"signin_view_title\">İstifadəçi kodu yüklənir…</string>\n    <string name=\"signin_view_description\">Daxil olmaq üçün bu kodu %s səhifəsinə daxil edin\\nQEYD. Yalnız Firefox və ya Chrome brauzerləri ilə işləyir.</string>\n    <string name=\"signin_view_action_text\">Hazırdır</string>\n    <string name=\"require_checked\">Tələb edir \\\"%s\\\"</string>\n    <string name=\"msg_press_again_to_exit\">Çıxmaq üçün yenidən basın</string>\n    <string name=\"player_remaining_time\">Qalan: %s</string>\n    <string name=\"player_ending_time\">Bitir %s</string>\n    <string name=\"badge_new_content\">YENİ MƏZMUN</string>\n    <string name=\"badge_live\">CANLI</string>\n    <string name=\"add_device_view_description\">Cihazı əlaqələndirmək üçün bu kodu telefonunuzun YouTube tətbiqində Parametrlər/TV-də izləmə bölməsinə daxil edin\\nQEYD. Yalnız Gboard klaviaturası ilə işləyir.</string>\n    <string name=\"playback_controls_repeat_pause\">Fasiləni təkrarla</string>\n    <string name=\"playback_controls_repeat_list\">Siyahını təkrarla</string>\n    <string name=\"action_subscribe_off\">Abunə Sönlü</string>\n    <string name=\"action_subscribe_on\">Abunə Qoşulu</string>\n    <!-- END Moved strings -->\n    <string name=\"skip_24_rate\">24 kadr formatlarını ötürün (real kino rejimi düzəldildi?)</string>\n    <string name=\"skip_shorts\">Shortları ötürün</string>\n    <string name=\"player_disable_suggestions\">Təklifləri deaktiv edin</string>\n    <string name=\"suggestions\">Təkliflər</string>\n    <string name=\"feedback\">Rəy</string>\n    <string name=\"sources\">Mənbələr</string>\n    <string name=\"releases\">Relizlər</string>\n    <string name=\"prefer_avc_over_vp9\">Kodek seçimi: vp9-dan avc-yə üstünlük verin</string>\n    <string name=\"open_chat\">Çat/Rəylər</string>\n    <string name=\"open_comments\">Rəyləri aç</string>\n    <string name=\"place_chat_left\">Çatı solda yerləşdir</string>\n    <string name=\"place_comments_left\">Şərhləri sola yerləşdirin</string>\n    <string name=\"use_alt_speech_recognizer\">Alternativ nitq tanıyıcı</string>\n    <string name=\"time_format\">Vaxt formatı</string>\n    <string name=\"time_format_24\">24 saat</string>\n    <string name=\"time_format_12\">12 saat (AM/PM)</string>\n    <string name=\"use_alt_speech_recognizer_desc\">Ciddi xarabdır. Onu yalnız standart tanıyıcı ilə probleminiz varsa istifadə edin.</string>\n    <string name=\"speech_recognizer\">Nitq tanıyıcısı</string>\n    <string name=\"speech_engine\">Səsli axtarış hərəkətvericisi</string>\n    <string name=\"speech_recognizer_system\">Sistem (üstünlük verilir)</string>\n    <string name=\"speech_recognizer_external_1\">Xarici 1 (ciddi səhvdir, işləməsi üçün mikrofona girişi söndürməlisiniz)</string>\n    <string name=\"speech_recognizer_external_2\">Xarici 2 (ciddi səhvdir)</string>\n    <string name=\"real_channel_icon\">Kanal düyməsində işarəni göstərin</string>\n    <string name=\"subtitle_yellow_semi_transparent\">Yarımşəffaf fonda sarı</string>\n    <string name=\"subtitle_yellow_black\">Qara fonda sarı</string>\n    <string name=\"player_pixel_ratio\">Piksel nisbəti</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">Tünd Boz (monoxrom)</string>\n    <string name=\"disable_mic_permission\">Xahiş edirik, bu tanıyıcının düzgün işləməsi üçün proqramın mikrofon girişini deaktiv edin.</string>\n    <string name=\"repeat_mode_shuffle\">İstənilən pleylistləri qarışdırın</string>\n    <string name=\"chat_left\">Sol</string>\n    <string name=\"chat_right\">Sağ</string>\n    <string name=\"card_real_thumbnails\">Miniatürləri videodan kadr ilə əvəz edin</string>\n    <string name=\"card_content\">Kartın eskizlərini haradan götürmək olar</string>\n    <string name=\"thumb_quality_default\">Defolt</string>\n    <string name=\"thumb_quality_start\">Videonun başlanğıcı</string>\n    <string name=\"thumb_quality_middle\">Videonun ortası</string>\n    <string name=\"thumb_quality_end\">Videonun sonu</string>\n    <string name=\"content_block_status\">SponsorBlock statusunu yoxlayın</string>\n    <string name=\"dearrow_status\">DeArrow statusunu yoxlayın</string>\n    <string name=\"player_show_quality_info_bitrate\">Keyfiyyət məlumatına bit sürəti əlavə edin</string>\n    <string name=\"player_speed_button_old_behavior\">Sürət düyməsinin köhnə davranışını bərpa edin</string>\n    <string name=\"protect_settings_with_password\">Bütün parametrləri parolla qoruyun</string>\n    <string name=\"enter_settings_password\">Parametrlər parolunu daxil edin</string>\n    <string name=\"child_mode\">Uşaq rejimi</string>\n    <string name=\"child_mode_desc\">Bu rejimdə istifadəçi axtarışdan istifadə edə və ya təklif olunan məzmunu görə bilməz. Parametrlər parolun altında olacaq.</string>\n    <string name=\"lost_setting_warning\">Proqram parametrləri dəyişdiriləcək. Parametrlərin ehtiyat nüsxəsini yaratdığınızdan əmin olun.</string>\n    <string name=\"player_button_long_click\">Sürətli hərəkət etmək üçün düyməyə qısa bir klik və parametrlər üçün uzun bir dialoq</string>\n    <string name=\"pause_history\">Tarixçəni dayandırın</string>\n    <string name=\"resume_history\">Tarixçəni bərpa edin</string>\n    <string name=\"disable_history\">Tarixçəni söndürün</string>\n    <string name=\"enable_history\">Tarixçəni qoşun</string>\n    <string name=\"clear_history\">Tarixçəni təmizləyin</string>\n    <string name=\"chapters\">Fəsillər</string>\n    <string name=\"card_multiline_subtitle\">Çoxsətirli subtitrlər</string>\n    <string name=\"auto_frame_rate_modes\">Dəstəklənən rejimlər</string>\n    <string name=\"loading\">Yuklənir…</string>\n    <string name=\"hide_streams\">Abunəliklərdən canlı yayımları gizlət</string>\n    <string name=\"video_rotate\">Döndər</string>\n    <string name=\"trending_searches\">Trend axtarışları</string>\n    <string name=\"video_duration_any\">Hər hansı</string>\n    <string name=\"video_duration\">Müddət</string>\n    <string name=\"video_duration_under_4\">4 dəqiqədən az</string>\n    <string name=\"video_duration_between_4_20\">4–20 dəqiqə</string>\n    <string name=\"video_duration_over_20\">20 dəqiqədən çox</string>\n    <string name=\"content_type\">Növ</string>\n    <string name=\"content_type_any\">Hər hansı</string>\n    <string name=\"content_type_video\">Video</string>\n    <string name=\"content_type_channel\">Kanal</string>\n    <string name=\"content_type_playlist\">Pleylist</string>\n    <string name=\"content_type_movie\">Film</string>\n    <string name=\"video_features\">Xüsusiyyətlər</string>\n    <string name=\"video_feature_any\">Hər hansı</string>\n    <string name=\"video_feature_live\">Canlı</string>\n    <string name=\"video_feature_4k\">4K</string>\n    <string name=\"video_feature_hdr\">HDR</string>\n    <string name=\"search_sorting\">Sırala</string>\n    <string name=\"sort_by_relevance\">Aktuallıq</string>\n    <string name=\"sort_by_views\">Baxış sayı</string>\n    <string name=\"sort_by_date\">Yükləmə vaxtı</string>\n    <string name=\"sort_by_rating\">Reytinq</string>\n    <string name=\"clear_search_history\">Axtarış tarixçəsini təmizlə</string>\n    <string name=\"remove_from_subscriptions\">Gizlət</string>\n    <string name=\"player_long_speed_list\">Sürət siyahısında daha çox sürət</string>\n    <string name=\"player_extra_long_speed_list\">Əlavə uzun sürət siyahısı</string>\n    <string name=\"alt_app_icon\">Alternativ proqram ikonası (yenidən başlamalıdır)</string>\n    <string name=\"network_stack\">%s şəbəkə yığınına üstünlük verin</string>\n    <string name=\"player_network_stack\">Şəbəkə yığını</string>\n    <string name=\"cronet_desc\">Cronet Chromium şəbəkə yığınıdır. Cronet gecikmə müddətini azalda və şəbəkə performansını artıra bilər ki, bu da buferləşdirmə məsələlərində kömək edə bilər.</string>\n    <string name=\"unlock_all_formats\">Bütün video formatlarını açın</string>\n    <string name=\"unlock_all_formats_desc\">Bəzi cihazlarda proqram təminatı bəzi formatları dəstəklənsə belə, onları yanlış şəkildə bildirir.\\n(məsələn, 1080p smart TV-lər 4k-nın dəstəklənmədiyini bildirir).</string>\n    <string name=\"okhttp_desc\">Bu şəbəkə yığını ən yavaşıdır, lakin yaxşı sabitliyə malikdir.</string>\n    <string name=\"select_channel_section\">Proqram ATV Kanallarından işə salındıqda müvafiq bölməni açın</string>\n    <string name=\"enable_voice_search_desc\">Rəsmi proqram sözdə körpü ilə əvəz olunacaq. Körpü qlobal axtarış sorğularını tətbiqimizə köçürməyə kömək edir.</string>\n    <string name=\"enable_master_password\">Master şifrəni qoşun</string>\n    <string name=\"enter_master_password\">Master şifrəni daxil edin</string>\n    <string name=\"disable_stream_buffer\">Yayımlarda buferi söndürün</string>\n    <string name=\"disable_stream_buffer_desc\">Axının bu qədər geridə qaldığı vəziyyətləri düzəldin. Qeyd: axın gecikməyə başlaya bilər.</string>\n    <string name=\"sony_frame_drop_fix\">Kadr düşməsini düzəldin #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">Sony TV və bəzi digər cihazlarda gecikmələri düzəldin. Qeyd: audio sinxronizasiya ilə bağlı mümkün problemlər.</string>\n    <string name=\"audio_language\">Audio dili</string>\n    <string name=\"old_home_look\">Ev bölməsinin köhnə görünüşü</string>\n    <string name=\"old_channel_look\">Kanal səhifəsinin köhnə görünüşü</string>\n    <string name=\"hide_shorts_everywhere\">Shortları hər yerdə gizlədin</string>\n    <string name=\"update_found\">Yeniləmə</string>\n    <string name=\"volume_boost_warning\">Həddini aşan hər hansı parametrlər dinamikləri zədələyə bilər</string>\n    <string name=\"play_video_incognito\">Gizli oxudun</string>\n    <string name=\"header_kids_home\">Uşaqlar</string>\n    <string name=\"player_section_playlist\">Cari bölmə məzmununu pleylist kimi istifadə edin</string>\n    <string name=\"header_trending\">Trend</string>\n    <string name=\"screensaver\">Ekran qoruyucusu</string>\n    <string name=\"player_screen_off_timeout\">Ekranın söndürülməsi</string>\n    <string name=\"old_update_notifications\">Yeniləmə bildirişlərinin köhnə görünüşü</string>\n    <string name=\"dialog_notification\">Pop-up dialoqu ilə yeniləmə haqqında məlumat verin</string>\n    <string name=\"mark_as_watched\">Baxılmış kimi işarələ</string>\n    <string name=\"player_ui_animations\">İdarəetmə paneli animasiyalarını aktivləşdirin</string>\n    <string name=\"player_likes_count\">Bəyənmə/bəyənməmə sayını göstər</string>\n    <string name=\"sorting_alphabetically2\">Əlifba sırası ilə (sürətli, animasiyalı önbaxışlar)</string>\n    <string name=\"sorting_default\">Defolt (sürətli, animasiyalı önbaxışlar)</string>\n    <string name=\"content_block_exclude_channel\">Bu kanalı SponsorBlock-dan xaric edin</string>\n    <string name=\"content_block_stop_excluding_channel\">Bu kanalı SponsorBlock-dan xaric etməyi dayandırın</string>\n    <string name=\"player_chapter_notification\">Fəsillərdə sürətlə irəliləmək üçün bildiriş göstərin</string>\n    <string name=\"subtitle_remember\">Hər bir kanal üçün aktiv subtitrləri yadda saxla</string>\n    <string name=\"amazon_frame_drop_fix\">Kadr düşməsini düzəldin #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">Amazon Stick cihazları üçün nəzərdə tutulub. Digər cihazlarda da işləyə bilər.</string>\n    <string name=\"default_stack_desc\">Daxili şəbəkə yığını. Bu yığın müəyyən vəziyyətlərdə daha yaxşı sabitliyə malik ola bilər.</string>\n    <string name=\"item_postion\">Mövqeyi</string>\n    <string name=\"player_screen_off_dimming\">Ekranın söndürülməsinin miqdarı</string>\n    <string name=\"screensaver_timout\">Ekran qoruyucu vaxt bitməsi</string>\n    <string name=\"screensaver_dimming\">Ekran qoruyucusu qaralma miqdarı</string>\n    <string name=\"player_ui_on_next\">Növbəti videoya keçərkən pleyer istifadəçi interfeysini göstərin</string>\n    <string name=\"autogenerated\">avtomatik düzəldilmiş</string>\n    <string name=\"player_auto_volume\">Avtomatik səs tənzimlənməsi</string>\n    <string name=\"header_shorts\">Shortlar</string>\n    <string name=\"player_global_focus\">Pleyer düymələri sıraları arasında qüsursuz naviqasiya</string>\n    <string name=\"auto_history\">Avto (hesab parametrlərindən istifadə edin)</string>\n    <string name=\"remember_position_subscriptions\">Abunəliklərdə son baxılan mövqeyi yadda saxla</string>\n    <string name=\"remember_position_pinned\">Saxlanmış pleylistlərdə sonuncu baxılmış mövqeyi yadda saxla</string>\n    <string name=\"msg_player_unknown_error\">Naməlum xəta</string>\n    <string name=\"unknown_source_error\">Naməlum mənbə xətası</string>\n    <string name=\"unknown_renderer_error\">Naməlum göstərmə xətası</string>\n    <string name=\"header_notifications\">Bildirişlər</string>\n    <string name=\"disable_search_history\">Axtarış tarixçəsini söndür</string>\n    <string name=\"unlock_high_bitrate_formats\">Yüksək bit sürətli 1080p vp9 formatlarının kilidini açın</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">Yüksək bit sürətli mp4a formatlarının kilidini açın</string>\n    <string name=\"color_scheme_blue\">Göy</string>\n    <string name=\"color_scheme_dark_blue\">Tünd Göy</string>\n    <string name=\"player_speed_per_channel\">Hər kanala görə</string>\n    <string name=\"disable_popular_searches\">Populyar axtarış sorğularını söndürün</string>\n    <string name=\"multi_profiles\">Hər hesab üçün ayrı parametrlərdən istifadə edin</string>\n    <string name=\"protect_account_with_password\">Bu hesabı şifrə ilə qoruyun</string>\n    <string name=\"enter_account_password\">Hesab şifrəsini daxil edin</string>\n    <string name=\"show_connect_messages\">Qoşulma bildirişlərini göstərin</string>\n    <string name=\"prefer_avc_over_vp9_desc\">XƏBƏRDARLIQ: maksimum 1080p</string>\n    <string name=\"play_next\">Növbətini oxudun</string>\n    <string name=\"hide_watched_from_subscriptions\">Baxılmış videoları Abunəliklərdən gizlət</string>\n    <string name=\"hide_watched_from_notifications\">İzlənmiş videoları Bildirişlərdən gizlədin</string>\n    <string name=\"hide_unwanted_content\">İstənməyən məzmunu gizlət</string>\n    <string name=\"hide_watched_from_home\">Baxılmış videoları Evdən gizlət</string>\n    <string name=\"hide_watched_from_watch_later\">Baxılmış videoları Sonra izlə pleylistindən gizlədin</string>\n    <string name=\"remote_control_permission\">Arxa fon Uzaqdan idarəetməsi Overlay icazəsi tələb edir</string>\n    <string name=\"login_from_browser\">Brauzerdən daxil olun</string>\n    <string name=\"disable_remote_history\">Uzaqdan idarə etmədən istifadə edərkən tarixçəni söndürün</string>\n    <string name=\"keyboard_fix\">OK düyməsini basaraq klaviaturanı göstərilməsini düzəldin (G20s və s.)</string>\n    <string name=\"nothing_found\">Heç nə tapılmadı</string>\n    <string name=\"auto_frame_rate_desc\">Bu seçim kameranın sürətlə hərəkət etdiyi səhnələrdə titrəməni aradan qaldırır, məsələn. idman yayımı</string>\n    <string name=\"channel_filter_hint\">Kanalları filterlə</string>\n    <string name=\"player_loop_shorts\">Shortları təkrarla</string>\n    <string name=\"player_quick_shorts_skip\">Shortları sol/sağ düymələr ilə ötürün</string>\n    <string name=\"player_quick_skip_videos\">Sol/sağ düymələr ilə adi videoları ötürün</string>\n    <string name=\"channels_filter\">Kanallar bölməsindəki kanallar siyahısını filtrləyin</string>\n    <string name=\"channel_search_bar\">Kanal səhifəsindəki axtarış çubuğu</string>\n    <string name=\"header_sports\">İdman</string>\n    <string name=\"keep_finished_activities\">Bitmiş fəaliyyətləri saxlayın</string>\n    <string name=\"disable_channels_service\">Kanallar xidmətini deaktiv edin</string>\n    <string name=\"replace_titles\">Başlıqları dəyişdirin</string>\n    <string name=\"enable\">Aktivləşdirin</string>\n    <string name=\"more_info\">Daha çox məlumat</string>\n    <string name=\"about_sponsorblock\">SponsorBlock haqqında</string>\n    <string name=\"about_dearrow\">DeArrow haqqında</string>\n    <string name=\"replace_thumbnails\">Miniatürləri dəyişdirin</string>\n    <string name=\"crowdsourced_thumbnails\">Crowdsourced miniatürlər</string>\n    <string name=\"crowdsoursed_titles\">Crowdsourced başlıqlar</string>\n    <string name=\"dearrow_not_submitted_thumbs\">Təqdim edilməmiş miniatürləri haradan götürmək olar</string>\n    <string name=\"pitch_effect\">Pitch effekti</string>\n    <string name=\"fullscreen_mode\">Tam ekran rejimi (sistem kənarları olmadan)</string>\n    <string name=\"player_only_mode\">Video proqramdan kənarda açılırsa, yalnız pleyeri göstərin</string>\n    <string name=\"pinned_channel_rows\">Saxlanmış kanalı sətirlər kimi göstərin</string>\n    <string name=\"prefer_ipv4\">IPv4 DNS-ə üstünlük verin</string>\n    <string name=\"prefer_ipv4_desc\">Tətbiqin ümumiyyətlə işləmədiyi vəziyyətləri düzəldə bilər.\\nQeyd. Donmalara və xətalara səbəb ola bilər (xüsusilə Android 8 cihazlarında və ya Dune HD-də)</string>\n    <string name=\"long_press_for_settings\">PARAMETRLƏR ÜÇÜN BASIB SAXLAYIN</string>\n    <string name=\"long_press_for_options\">SEÇİMLƏR ÜÇÜN BASIB SAXLAYIN</string>\n    <string name=\"device_specific_backup\">Yalnız bu cihaz üçün ehtiyyatda saxlayın</string>\n    <string name=\"local_backup\">Yerli ehtiyyat nüsxəsi</string>\n    <string name=\"auto_backup\">Avtomatik ehtiyat nüsxəsi (gündə bir dəfə)</string>\n    <string name=\"repeat_mode_reverse_list\">Pleylist və ya kanal videolarını tərs qaydada oynadın</string>\n    <string name=\"calm_msg\">Problemi həll edirik. Vaxt aşırı yeniləmələri yoxlayın</string>\n    <string name=\"without_picture\">Görüntüsüz</string>\n    <string name=\"video_disabled\">Video deaktiv edilib</string>\n    <string name=\"applying_fix\">Pleyeri bağlamayın. Həll tətbiq olunur...</string>\n    <string name=\"hide_mixes\">Mixləri gizlət</string>\n    <string name=\"player_global_focus_desc\">Bu xüsusiyyət pleyer düymə cərgələri arasında hərəkət edərkən hansı pleyer düyməsinə diqqət yetirilməsinə təsir edir</string>\n    <string name=\"disable_network_error_fixing\">Şəbəkə xətalarının avtomatik həllini deaktiv edin</string>\n    <string name=\"disable_network_error_fixing_desc\">Əgər VPN istifadə edirsinizsə, bu seçimi aktivləşdirin</string>\n    <string name=\"recommended\">Tövsiyyə olunan</string>\n    <string name=\"add_to_subscriptions_group\">Abunəlik qrupuna əlavə et/çıxar</string>\n    <string name=\"new_subscriptions_group\">Yeni qrup</string>\n    <string name=\"rename_group\">Abunəlik qrupunun adını dəyişin</string>\n    <string name=\"screen_dimming_amount\">Ekranın qaralma miqdarı</string>\n    <string name=\"screen_dimming_timeout\">Ekranın qaralma vaxtı</string>\n    <string name=\"playlists_rows\">Pleylistlər bölməsini sətirlər kimi göstərin</string>\n    <string name=\"import_subscriptions_group\">Daxil et</string>\n    <string name=\"lb_playback_controls_closed_captioning_disable\">Qapalı Altyazıları deaktiv edin</string>\n    <string name=\"lb_playback_controls_closed_captioning_enable\">Qapalı Altyazıları aktivləşdirin</string>\n    <string name=\"my_videos\">Videolarım</string>\n    <string name=\"player_audio_focus\">Audio fokus (digər pleyerlər aşkar edildikdə fasilə verin)</string>\n    <string name=\"paid_content_notification\">Ödənişli məzmun bildirişi</string>\n    <string name=\"you_liked\">bəyədiniz</string>\n    <string name=\"premium_users_only\">Premium istifadəçilər üçün. Natamam video format siyahısı üçün düzəliş</string>\n    <string name=\"playback_buffering_fix\">Oxutma buferi üçün düzəliş</string>\n    <string name=\"oculus_quest_fix\">Oculus Quest düzəlişi</string>\n    <string name=\"card_preview_muted\">Səssiz video</string>\n    <string name=\"card_preview_full\">Səsli video</string>\n    <string name=\"card_preview\">Kart önizləməsi</string>\n    <string name=\"card_unlocalized_titles\">Lokallaşdırılmamış video başlıqları</string>\n    <string name=\"dont_resize_video_to_fit_dialog\">Dialoq qutusuna uyğunlaşdırmaq üçün videonun ölçüsünü dəyişməyin</string>\n    <string name=\"typing_corrections\">Mətn yazarkən avtomatik düzəlişi söndürün</string>\n    <string name=\"suggestions_horizontally_scrolled\">Üfüqi sürüşdürülmüş Təkliflər</string>\n    <string name=\"stable_restore\">Stabil quruluşu bərpa edin (bütün məlumatlar itiriləcək)</string>\n    <string name=\"auto_backup_category\">Avtomatik ehtiyat nüsxəsi</string>\n    <string name=\"once_a_day\">Gündə bir dəfə</string>\n    <string name=\"once_a_week\">Həftədə bir dəfə</string>\n    <string name=\"once_a_month\">Ayda bir dəfə</string>\n    <string name=\"action_debug_info\">Debuq məlumatı</string>\n    <string name=\"dialog_block_channel\">Kanalə bloklayın</string>\n    <string name=\"dialog_unblock_channel\">Kanalı blokdan çıxarın</string>\n    <string name=\"confirm_block_channel\">Bütün məzmun %s-dən gizlədilsin?</string>\n    <string name=\"channel_blocked\">Kanal bloklanıb</string>\n    <string name=\"channel_unblocked\">Kanal blokdan çıxarıldı</string>\n    <string name=\"header_blocked_channels\">Bloklanmış Kanallar</string>\n    <string name=\"msg_no_blocked_channels\">Bloklanmış kanal yoxdur</string>\n    <string name=\"player_time_stretching\">Audio vaxtını uzatmaq</string>\n    <string name=\"player_time_stretching_desc\">Sürəti dəyişdirərkən səsi təbii saxlayır. Bəzi cihazlarda məhsuldarlığı əhəmiyyətli dərəcədə azalda bilər.</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-bg/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Начало</string>\n  <string name=\"title_search\">Търсене</string>\n  <string name=\"header_subscriptions\">Абонаменти</string>\n  <string name=\"header_history\">История</string>\n  <string name=\"header_music\">Музика</string>\n  <string name=\"header_news\">Новини</string>\n  <string name=\"header_gaming\">Игри</string>\n  <string name=\"header_playlists\">Плейлист</string>\n  <string name=\"header_settings\">Настройки</string>\n  <string name=\"header_channels\">Канали</string>\n  <string name=\"subscriptions_signin_title\">Вижте най-новото от вашите абонаменти</string>\n  <string name=\"subscriptions_signin_subtitle\">Впишете се за да видите вашите абонаменти</string>\n  <string name=\"subscriptions_signin_button_text\">ВПИШЕТЕ СЕ</string>\n  <string name=\"library_signin_to_show_more\">Гледайте видеоклипове, които сте харесали, запазили или за които сте абонирани</string>\n  <string name=\"library_signin_subtitle\">Впишете се за да видите вашата бибиотека</string>\n  <string name=\"action_signin\">ВПИШЕТЕ СЕ</string>\n  <string name=\"title_video_formats\">Видео формати</string>\n  <string name=\"title_audio_formats\">Аудио формати</string>\n  <string name=\"subtitle_category_title\">Субтитри</string>\n  <string name=\"video_max_quality\">Автоматично (максимално качество)</string>\n  <string name=\"audio_max_quality\">Автоматично (максимално качество)</string>\n  <string name=\"subtitles_disabled\">Субтитрите са деактивирани</string>\n  <string name=\"auto_frame_rate\">Автоматична честота на кадрите</string>\n  <string name=\"frame_rate_correction\">Коригиране на fps:\\n%s</string>\n  <string name=\"category_background_playback\">Възпроизвеждане на заден фон</string>\n  <string name=\"not_implemented\">Не е реализирано</string>\n  <string name=\"option_background_playback_off\">Изключено</string>\n  <string name=\"option_background_playback_pip\">Картина в картината</string>\n  <string name=\"option_background_playback_only_audio\">Само звук</string>\n  <string name=\"playback_settings\">Настройки за възпроизвеждане</string>\n  <string name=\"video_speed\">Скорост на видеото</string>\n  <string name=\"resolution_switch\">Превключване на разделителната способност</string>\n  <string name=\"video_buffer\">Видео буфер</string>\n  <string name=\"video_buffer_size_low\">Ниско</string>\n  <string name=\"video_buffer_size_med\">Средно</string>\n  <string name=\"video_buffer_size_high\">Високо</string>\n  <string name=\"update_changelog\">Промени в актуализацията</string>\n  <string name=\"install_update\">Инсталирайте актуализация</string>\n  <string name=\"section_is_empty\">Ами сега...тук няма нищо.</string>\n  <string name=\"dialog_add_to_playlist\">Добави към плейлист</string>\n  <string name=\"msg_signed_users_only\">Само вписани потребители</string>\n  <string name=\"msg_cant_load_content\">Ами сега...не може да се зареди съдържанието.</string>\n  <string name=\"title_video_presets\">Предварителни настройки на видеото</string>\n  <string name=\"settings_accounts\">Акаунти</string>\n  <string name=\"settings_left_panel\">Редактиране на категориите</string>\n  <string name=\"settings_themes\">Теми</string>\n  <string name=\"settings_other\">Други</string>\n  <string name=\"settings_player\">Видео плейър</string>\n  <string name=\"settings_language\">Език</string>\n  <string name=\"settings_linked_devices\">Свързани устройства</string>\n  <string name=\"settings_about\">Относно</string>\n  <string name=\"dialog_account_list\">Изберете акаунт</string>\n  <string name=\"dialog_account_none\">Нито един</string>\n  <string name=\"dialog_remove_account\">Изтрийте акаунт</string>\n  <string name=\"dialog_add_account\">Добавете акаунт</string>\n  <string name=\"default_lang\">По подразбиране</string>\n  <string name=\"dialog_select_language\">Език на приложението</string>\n  <string name=\"subtitle_default\">По подразбиране</string>\n  <string name=\"subtitle_white_semi_transparent\">Полупрозрачен фон</string>\n  <string name=\"subtitle_style\">Стил на субтитрите</string>\n  <string name=\"subtitle_language\">Език на субтитрите</string>\n  <string name=\"action_search\">Започнете търсене</string>\n  <string name=\"settings_main_ui\">Потребителски интерфейс</string>\n  <string name=\"dialog_main_ui\">Потребителски интерфейс</string>\n  <string name=\"card_animated_previews\">Анимирано визуализиране</string>\n  <string name=\"web_site\">Уеб сайт</string>\n  <string name=\"donation\">Даряване</string>\n  <string name=\"dialog_about\">Относно</string>\n  <string name=\"dialog_player_ui\">Видео плейър</string>\n  <string name=\"player_show_ui_on_pause\">Показване на потребителския интерфейс на пауза</string>\n  <string name=\"player_pause_on_ok\">Клавишът ОК паузира възпроизвеждането</string>\n  <string name=\"player_ok_button_behavior\">Поведение на бутона ОК</string>\n  <string name=\"player_only_ui\">Само потребителски интерфейс</string>\n  <string name=\"player_ui_and_pause\">Потребителски интерфейс и пауза</string>\n  <string name=\"player_only_pause\">Само пауза</string>\n  <string name=\"check_for_updates\">Провери за актуализации</string>\n  <string name=\"update_not_found\">Използвате най-новата версия</string>\n  <string name=\"update_in_progress\">Изчакайте...</string>\n  <string name=\"player_ui_hide_behavior\">Автоматично скриване на потребителския интерфейс</string>\n  <string name=\"option_never\">Никога</string>\n  <string name=\"side_panel_sections\">Настрой разделите</string>\n  <string name=\"boot_to_section\">При стартиране да се визуализира раздел</string>\n  <string name=\"large_ui\">Голям потребителски интерфейс</string>\n  <string name=\"video_grid_scale\">Мащаб на видеорешетката</string>\n  <string name=\"scale_ui\">Мащаб на потребителския интерфейс</string>\n  <string name=\"color_scheme\">Цветова схема</string>\n  <string name=\"color_scheme_default\">По подразбиране</string>\n  <string name=\"color_scheme_red_grey\">Червено-сиво</string>\n  <string name=\"color_scheme_red\">Червено</string>\n  <string name=\"color_scheme_dark_grey\">Тъмно сив</string>\n  <string name=\"disable_update_check\">Деактивирайте проверката за актуализация</string>\n  <string name=\"show_again\">Покажи отново</string>\n  <string name=\"check_updates_auto\">Автоматично актуализиране</string>\n  <string name=\"select_account_on_boot\">Да се избере при зареждане</string>\n  <string name=\"player_other\">Разни</string>\n  <string name=\"player_full_date\">Точна дата в описанието</string>\n  <string name=\"open_channel\">Отваряне на канал</string>\n  <string name=\"not_interested\">Не се интересувам</string>\n  <string name=\"you_wont_see_this_video\">Видеото е премахнато от препоръчаните</string>\n  <string name=\"settings_search\">Търсене</string>\n  <string name=\"dialog_search\">Търсене</string>\n  <string name=\"instant_voice_search\">Незабавно гласово търсене</string>\n  <string name=\"option_background_playback_behind\">Пусни на заден фон</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">Информация за следващото видео все още не е заредена</string>\n  <string name=\"card_multiline_title\">Многоредови заглавия</string>\n  <string name=\"cards_style\">Стил на картите</string>\n  <string name=\"repeat_mode_all\">Пускай всички клипове един по един</string>\n  <string name=\"repeat_mode_one\">Повтори текущия видеоклип</string>\n  <string name=\"repeat_mode_pause\">Паузирай възпроизвеждането след всеки видеоклип</string>\n  <string name=\"repeat_mode_pause_alt\">Паузирай възпроизвеждането след всеки видеоклип, който не е от плейлиста</string>\n  <string name=\"repeat_mode_none\">Спри възпроизвеждането след края на видеото</string>\n  <string name=\"subscribed_to_channel\">Абонирахте се за канала</string>\n  <string name=\"unsubscribed_from_channel\">Отписахте се от канала</string>\n  <string name=\"subtitle_yellow_transparent\">Жълт</string>\n  <string name=\"subtitle_white_black\">Черен фон</string>\n  <string name=\"player_seek_preview\">Визуализиране при търсене</string>\n  <string name=\"color_scheme_dark_grey_oled\">Тъмно сиво (OLED)</string>\n  <string name=\"channels_section_sorting\">Сортиране на видеоклиповете</string>\n  <string name=\"sorting_by_new_content\">Ново съдържание</string>\n  <string name=\"sorting_alphabetically\">Азбучен ред</string>\n  <string name=\"sorting_last_viewed\">Последно гледани</string>\n  <string name=\"player_pause_when_seek\">Пауза при търсене</string>\n  <string name=\"playlists_style\">Стил на плейлиста</string>\n  <string name=\"playlists_style_grid\">Решетка</string>\n  <string name=\"playlists_style_rows\">Редове</string>\n  <string name=\"player_show_clock\">Показване на часовник</string>\n  <string name=\"player_show_remaining_time\">Показване на оставащо време</string>\n  <string name=\"open_channel_uploads\">Отвори канал качени</string>\n  <string name=\"app_exit_shortcut\">Излизане от приложението</string>\n  <string name=\"app_exit_none\">Не излизай</string>\n  <string name=\"app_double_back_exit\">Два пъти назад</string>\n  <string name=\"app_single_back_exit\">Веднъж назад</string>\n  <string name=\"action_video_zoom\">Видео увеличение</string>\n  <string name=\"video_zoom\">Видео увеличение</string>\n  <string name=\"video_zoom_default\">По подразбиране</string>\n  <string name=\"video_zoom_fit_width\">Пасване на ширина</string>\n  <string name=\"video_zoom_fit_height\">Пасване на височина</string>\n  <string name=\"video_zoom_fit_both\">Пасване на ширина и височина</string>\n  <string name=\"video_zoom_stretch\">Разтегляне</string>\n  <string name=\"color_scheme_teal\">Кафяво</string>\n  <string name=\"color_scheme_teal_oled\">Кафяво (OLED)</string>\n  <string name=\"player_seek_preview_none\">Изключено</string>\n  <string name=\"player_seek_preview_single\">Единична рамка</string>\n  <string name=\"player_seek_preview_carousel\">Въртележка</string>\n  <string name=\"player_seek_preview_carousel_slow\">Въртележка (бавно)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Въртележка (бързо)</string>\n  <string name=\"unsubscribe_from_channel\">Отпиши се от канала</string>\n  <string name=\"card_title_lines_num\">Брой редове на заглавието на картата</string>\n  <string name=\"card_auto_scrolled_title\">Автоматично превъртане на изрязаното заглавие</string>\n  <string name=\"share_link\">Сподели линк</string>\n  <string name=\"subscribe_to_channel\">Последвай канал</string>\n  <string name=\"wait_data_loading\">Моля, изчакайте, докато данните се зареждат…</string>\n  <string name=\"auto_frame_rate_pause\">Автоматично паузиране при смяна на честота на кадрите</string>\n  <string name=\"auto_frame_rate_applying\">Прилагане на авто,атична честота на кадрите %sx%s\\@%s</string>\n  <string name=\"audio_shift\">Аудио смяна</string>\n  <string name=\"player_remember_speed\">Запомни скоростта</string>\n  <string name=\"mark_channel_as_watched\">Маркирай, като гледано</string>\n  <string name=\"channel_marked_as_watched\">Каналът е маркиран като гледан</string>\n  <string name=\"dialog_add_device\">Добави устройство</string>\n  <string name=\"dialog_remove_all_devices\">Изтрий всички устройства</string>\n  <string name=\"device_link_enabled\">Връзката е активирана</string>\n  <string name=\"device_connected\">Устройство \\\"%s\\\" беше свързано</string>\n  <string name=\"device_disconnected\">Устройство \\\"%s\\\" беше изключено</string>\n  <string name=\"settings_ui_scale\">Скала на потребителския интерфейс</string>\n  <string name=\"msg_restart_app\">Рестартирайте устройството за да приложите тази настройка</string>\n  <string name=\"settings_block\">Блокиране на съдържание</string>\n  <string name=\"msg_applying\">Прилагане %s…</string>\n  <string name=\"msg_done\">Завършен</string>\n  <string name=\"settings_general\">Общи</string>\n  <string name=\"settings_video\">Видео</string>\n  <string name=\"content_block_confirm_skip\">Потвърди при пропускане</string>\n  <string name=\"content_block_categories\">Категории</string>\n  <string name=\"content_block_sponsor\">Спонсор</string>\n  <string name=\"content_block_intro\">Анимация при антракт / интро</string>\n  <string name=\"content_block_outro\">Endcards/credits</string>\n  <string name=\"content_block_interaction\">Напомняне за взаимодействие (абонирайте се)</string>\n  <string name=\"content_block_self_promo\">Неплатени / саморекламиране</string>\n  <string name=\"content_block_music_off_topic\">Не-музикална секция на видеоклип</string>\n  <string name=\"confirm_segment_skip\">Пропусни сегмент \\\"%s\\\"\\?</string>\n  <string name=\"msg_skipping_segment\">Пропускане на сегмент \\\"%s\\\"…</string>\n  <string name=\"content_block_notification_type\">Тип известие</string>\n  <string name=\"content_block_notify_none\">Без известие</string>\n  <string name=\"content_block_notify_toast\">Тост</string>\n  <string name=\"content_block_notify_dialog\">Диалог за потвърждение</string>\n  <string name=\"return_to_launcher\">Върнете се в лаунчера от ATV канали / търсене</string>\n  <string name=\"intent_force_close\">Принудително излизане, ако видеото е отворено от външен източник</string>\n  <string name=\"btn_confirm\">Потвърди</string>\n  <string name=\"player_low_video_quality\">Ниско качество на видеото</string>\n  <string name=\"remote_session_closed\">Отдалечената сесия е затворена</string>\n  <string name=\"msg_mode_switch_error\">Не може да превключи режима на показване на \\\"%s\\\"</string>\n  <string name=\"msg_player_error\">Възникна грешка в плеъра %s. Рестартиране на възпроизвеждането…</string>\n  <string name=\"video_preset_disabled\">Без предварително зададени</string>\n  <string name=\"video_preset_enabled\">Форматът на следващия видеоклип ще бъде зададен според предварително зададената настройка</string>\n  <string name=\"player_sleep_timer\">Таймер за заспиване</string>\n  <string name=\"player_show_quality_info\">Показване на информация за качеството на видеоклипа</string>\n  <string name=\"player_remember_each_speed\">Запомни скоротта на възпроизвеждане</string>\n  <string name=\"player_remember_speed_none\">Нито един</string>\n  <string name=\"player_remember_speed_all\">Същото за всички видеоклипове</string>\n  <string name=\"player_remember_speed_each\">За всеки видеоклип</string>\n  <string name=\"msg_player_error_source\">Видеоклипът не може да бъде изтеглен</string>\n  <string name=\"msg_player_error_renderer\">Избраният видеоформат не се поддържа</string>\n  <string name=\"msg_player_error_unexpected\">Неизвестна грешка на видеодекодера</string>\n  <string name=\"video_aspect\">Съотношение</string>\n  <string name=\"playback_queue_category_title\">Опашка за възпроизвеждане</string>\n  <string name=\"video_buffer_size_none\">Няма</string>\n  <string name=\"open_playlist\">Отваряне на плейлист</string>\n  <string name=\"not_recommend_channel\">Не препоръчвай канала</string>\n  <string name=\"you_wont_see_this_channel\">Няма да виждате този канал в предложения</string>\n  <string name=\"subtitle_white_transparent\">Бяло на прозрачен фон</string>\n  <string name=\"player_show_ending_time\">Показване на времето на приключване в лентата за управление</string>\n  <string name=\"cancel_segment_skip\">Отмяна на пропускането на сегмента</string>\n  <string name=\"player_speed_per_channel\">Запомнете избраната скорост за всеки канал</string>\n  <string name=\"color_scheme_dark_blue\">Тъмносин</string>\n  <string name=\"color_scheme_blue\">Син</string>\n  <string name=\"msg_player_error_video_renderer\">Избраният VIDEO формат не се поддържа.\\nОпитайте се да изберете друг, като натиснете бутона HQ в плейъра.\\nАко това не помогне, опитайте се да рестартирате устройството.</string>\n  <string name=\"msg_player_error_audio_renderer\">Избраният AUDIO формат не се поддържа.\\nОпитайте се да изберете друг, като натиснете бутона HQ в плейъра.\\nАко това не помогне, опитайте да рестартирате устройството.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">Избраният формат на субтитрите не се поддържа.\\nОпитайте се да изберете друг формат чрез продължително натискане на бутона CC в плейъра.\\nАко това не помогне, опитайте се да рестартирате устройството.</string>\n  <string name=\"player_tweaks\">Опции за разработчици</string>\n  <string name=\"header_uploads\">Качвания</string>\n  <string name=\"player_show_global_clock\">Винаги показвай включен екранен часовник</string>\n  <string name=\"player_show_global_ending_time\">Винаги показвай крайния час на екрана</string>\n  <string name=\"uploads_old_look\">Връщане на стария облик на раздела за качване</string>\n  <string name=\"background_playback_activation\">Фоново възпроизвеждане (активиране)</string>\n  <string name=\"channels_old_look\">Връщане на стария вид на раздел Канали</string>\n  <string name=\"channels_auto_load\">Автоматично зареждане на съдържанието на раздел Канали</string>\n  <string name=\"return_to_background_video\">Връщане към видео, работещо във фонов режим</string>\n  <string name=\"pin_unpin_from_sidebar\">Добавяне/премахване от страничната лента</string>\n  <string name=\"pin_unpin_playlist\">Добавяне/премахване на плейлиста от страничната лента</string>\n  <string name=\"pin_unpin_channel\">Добавяне/премахване на канал от страничната лента</string>\n  <string name=\"pinned_to_sidebar\">Добавено в страничната лента</string>\n  <string name=\"unpin_from_sidebar\">Премахване от страничната лента</string>\n  <string name=\"unpinned_from_sidebar\">Премахнато от страничната лента</string>\n  <string name=\"dialog_select_country\">Държава</string>\n  <string name=\"settings_language_country\">Език/държава</string>\n  <string name=\"share_embed_link\">Споделяне на линк за вграждане</string>\n  <string name=\"card_text_scroll_factor\">Скорост на превъртане на текста на картата</string>\n  <string name=\"preferred_update_source\">Изберете източник на актуализация</string>\n  <string name=\"hide_shorts\">Скриване на кратки клипчета от абонаменти</string>\n  <string name=\"key_remapping\">Пренареждане на клавишите</string>\n  <string name=\"screen_dimming\">Затъмняване на екрана</string>\n  <string name=\"removed_from_playback_queue\">Премахнат от опашката за възпроизвеждане</string>\n  <string name=\"added_to_playback_queue\">Добавено в опашката за възпроизвеждане</string>\n  <string name=\"add_remove_from_playback_queue\">Добави/премахни от опашката за възпроизвеждане</string>\n  <string name=\"add_to_playback_queue\">Добавяне към опашката за възпроизвеждане</string>\n  <string name=\"remove_from_playback_queue\">Премахване от опашката за възпроизвеждане</string>\n  <string name=\"proxy_enabled\">Активиран прокси сървър</string>\n  <string name=\"proxy_disabled\">Деактивиран прокси сървър</string>\n  <string name=\"uploads_row_name\">Качвания</string>\n  <string name=\"playlists_row_name\">Създадени плейлисти</string>\n  <string name=\"popular_uploads_row_name\">Популярни качвания</string>\n  <string name=\"breaking_news_row_name\">Извънредни новини</string>\n  <string name=\"covid_news_row_name\">COVID-19 новини</string>\n  <string name=\"enable_voice_search\">Активиране на глобалното търсене (необходима е поддръжка на фърмуера)</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Абониране/отписване от канала</string>\n  <string name=\"app_backup_restore\">Архивиране/възстановяване</string>\n  <string name=\"app_backup\">Архивиране на данни</string>\n  <string name=\"app_restore\">Възстановяване на данни от резервно копие</string>\n  <string name=\"skip_each_segment_once\">Не прескачайте отново сегменти</string>\n  <string name=\"disable_ok_long_press\">Деактивиране на бутона OK с дълго натискане</string>\n  <string name=\"disable_ok_long_press_desc\">Предназначено за бъгави контролери, при които бутонът OK не функционира правилно</string>\n  <string name=\"player_time_correction\">Показване на времето в зависимост от скоростта</string>\n  <string name=\"sponsor_color_markers\">Цветни маркери в лентата за напредък</string>\n  <string name=\"refresh_section\">Опресняване на раздела</string>\n  <string name=\"option_disabled\">Изключено</string>\n  <string name=\"live_now_row_name\">На живо в момента</string>\n  <string name=\"run_in_background\">Фоново възпроизвеждане</string>\n  <string name=\"settings_remote_control\">Дистанционно управление</string>\n  <string name=\"background_service_started\">Стартирана е фонова услуга</string>\n  <string name=\"dialog_add_to\">Добавяне към %s</string>\n  <string name=\"dialog_remove_from\">Премахване от %s</string>\n  <string name=\"added_to\">Добавено към %s</string>\n  <string name=\"removed_from\">Премахнат от %s</string>\n  <string name=\"video_preset_adaptive\">Адаптивен</string>\n  <string name=\"content_block_preview_recap\">Преглед или резюме на видеоклипа</string>\n  <string name=\"content_block_highlight\">Интересна точка (отметни)</string>\n  <string name=\"removed_from_history\">Видеото е премахнато от историята</string>\n  <string name=\"remove_from_history\">Премахване от историята</string>\n  <string name=\"upload_date\">Дата на качване</string>\n  <string name=\"upload_date_any\">За цялото време</string>\n  <string name=\"upload_date_today\">Днес</string>\n  <string name=\"upload_date_this_week\">Тази седмица</string>\n  <string name=\"upload_date_this_month\">Този месец</string>\n  <string name=\"upload_date_this_year\">Тази година</string>\n  <string name=\"double_refresh_rate\">Двойна честота на опресняване</string>\n  <string name=\"upload_date_last_hour\">Последния час</string>\n  <string name=\"focus_on_search_results\">Автофокус върху резултатите от търсенето</string>\n  <string name=\"context_menu\">Контекстно меню</string>\n  <string name=\"add_remove_from_recent_playlist\">Добавяне/премахване от списъка с последните песни</string>\n  <string name=\"hide_settings_section\">Скриване на раздел \\\"Настройки\\\" (опасно!)</string>\n  <string name=\"volume\">Звук %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s секунди</string>\n  <string name=\"audio_shift_sec\">%s секунди</string>\n  <string name=\"ui_hide_timeout_sec\">%s секунди</string>\n  <string name=\"screen_dimming_timeout_min\">%s минути</string>\n  <string name=\"mark_all_channels_watched\">Маркиране на всички канали като гледани</string>\n  <string name=\"move_section_up\">Преместване на раздела нагоре</string>\n  <string name=\"move_section_down\">Преместване на раздела надолу</string>\n  <string name=\"player_buttons\">Бутони за настройка на плейъра</string>\n  <string name=\"action_playlist_add\">Добавяне към списъка за изпълнение</string>\n  <string name=\"action_video_stats\">Статистика на видеото</string>\n  <string name=\"action_subscribe\">Абонирай се за</string>\n  <string name=\"action_channel\">Отвори канал</string>\n  <string name=\"action_pip\">Плеър в плеъра</string>\n  <string name=\"action_video_info\">Описание на видеото</string>\n  <string name=\"action_screen_off\">Изключване на екрана</string>\n  <string name=\"action_playback_queue\">Опашка за възпроизвеждане</string>\n  <string name=\"action_video_speed\">СКорост на видеото</string>\n  <string name=\"action_subtitles\">Субтитри</string>\n  <string name=\"action_like\">Харесвам</string>\n  <string name=\"action_dislike\">Не харесвам</string>\n  <string name=\"action_play_pause\">Пусни/Спри</string>\n  <string name=\"action_repeat_mode\">Режим на възпроизвеждане</string>\n  <string name=\"action_next\">Следващо видео</string>\n  <string name=\"action_previous\">Предишно видео</string>\n  <string name=\"action_high_quality\">Качество на видеото</string>\n  <string name=\"content_block_no_skipping_mode\">Режим без прескачане</string>\n  <string name=\"content_block_action_type\">Избери действие</string>\n  <string name=\"content_block_action_none\">Не прави нищо</string>\n  <string name=\"content_block_action_only_skip\">Само пропусни</string>\n  <string name=\"content_block_action_toast\">Пропусни с известие</string>\n  <string name=\"content_block_action_dialog\">Показване на диалогов прозорец за потвърждение</string>\n  <string name=\"content_block_filler\">Извън темата (пълнеж)</string>\n  <string name=\"keyboard_auto_show\">Покажи автоматично клавиатурата</string>\n  <string name=\"cancel_dialog\">Отказ</string>\n  <string name=\"msg_player_error_source2\">URL адресът не работи или има неправилно време на устройството.\\nОбикновено това е проблем на приложението.</string>\n  <string name=\"msg_player_error_video_source\">URL VIDEO не работи или времето на устройството е неправилно.Обикновено това е проблем на приложението.</string>\n  <string name=\"msg_player_error_audio_source\">AUDIO URL адресът не работи или има неправилно време на устройството.\\nОбикновено това е проблем на приложението.</string>\n  <string name=\"msg_player_error_subtitle_source\">SUBTITLE URLNне работи или времето на устройството е неправилно.\\nОбикновено това е проблем на приложението.</string>\n  <string name=\"hide_upcoming\">Скриване на предстоящи от абонаменти</string>\n  <string name=\"search_background_playback\">Въвеждане на прозорец в прозореца при търсене/откриване на канал</string>\n  <string name=\"trending_row_name\">Тенденции</string>\n  <string name=\"player_seek_type\">Търсене на поведение</string>\n  <string name=\"player_seek_regular\">Обикновено</string>\n  <string name=\"player_seek_confirmation_pause\">С потвърждение (пауза по време на търсенето)</string>\n  <string name=\"player_seek_confirmation_play\">С потвърждение (възпроизвеждане при търсене)</string>\n  <string name=\"hide_shorts_from_home\">Скриване на кратки видеота от началния екран</string>\n  <string name=\"finish_on_disconnect\">Затваряне на приложението след изключване на телефона/таблета</string>\n  <string name=\"update_error\">Грешка при актуализиране</string>\n  <string name=\"hide_shorts_from_history\">Скриване на кратките видеа от История</string>\n  <string name=\"proxy_test_btn\">Тест</string>\n  <string name=\"disable_screensaver\">Изключване на скрийнсейвъра</string>\n  <string name=\"description_not_found\">Описание не е намерено</string>\n  <string name=\"proxy_port_hint\">Прокси порт</string>\n  <string name=\"proxy_host_hint\">Име на хоста или IP на прокси сървъра</string>\n  <string name=\"proxy_username_hint\">Прокси потребителско име</string>\n  <string name=\"proxy_password_hint\">Прокси парола</string>\n  <string name=\"proxy_type\">Прокси тип</string>\n  <string name=\"enable_web_proxy\">Използване на уеб прокси</string>\n  <string name=\"proxy_not_supported\">Използване на уеб прокси (изисква Android 4.4+)</string>\n  <string name=\"proxy_settings_title\">Настройки на прокси сървъра</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">Test#%d: отменен.</string>\n  <string name=\"proxy_type_invalid\">Типът на проксито не е зададен.</string>\n  <string name=\"proxy_host_invalid\">Прокси хостът не е зададен.</string>\n  <string name=\"proxy_port_invalid\">Невалиден прокси порт, трябва да е &gt; 0.</string>\n  <string name=\"proxy_credentials_invalid\">Невалидно потребителско име или парола.</string>\n  <string name=\"proxy_test_aborted\">Тестът е прекъснат, моля, първо коригирайте настройките на прокси сървъра.</string>\n  <string name=\"proxy_application_aborted\">Моля, първо коригирайте настройките на прокси сървъра.</string>\n  <string name=\"proxy_test_start\">Тест#%d: %s ...</string>\n  <string name=\"proxy_test_error\">Грешка#%d: %s</string>\n  <string name=\"proxy_test_status\">Статус#%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Адрес на файла за конфигуриране (*.ovpn)</string>\n  <string name=\"enable_openvpn\">Използване на OpenVPN</string>\n  <string name=\"openvpn_settings_title\">Настройки на OpenVPN</string>\n  <string name=\"openvpn_application_aborted\">Моля, първо коригирайте настройките на OpenVPN.</string>\n  <string name=\"openvpn_test_aborted\">Тестът е прекъснат, моля, първо коригирайте настройките на OpenVPN.</string>\n  <string name=\"openvpn_address_invalid\">Адресът за конфигуриране на OpenVPN не е зададен.</string>\n  <string name=\"internet_censorship\">Интернет цензура</string>\n  <string name=\"rename_section\">Преименуване на този раздел</string>\n  <string name=\"simple_edit_value_hint\">Въведете стойност</string>\n  <string name=\"seek_interval\">Интервал на търсене</string>\n  <string name=\"seek_interval_sec\">%s секунди</string>\n  <string name=\"subtitle_system\">Стил на системата (Настройки на Android &gt; Достъпност)</string>\n  <string name=\"subtitle_scale\">Мащаб на субтитрите</string>\n  <string name=\"audio_sync_fix_desc\">Алтернативен начин за синхронизиране на аудио/видео. Смята се за остарял, но в някои случаи може да помогне.</string>\n  <string name=\"audio_sync_fix\">Поправка на аудио синхронизацията.</string>\n  <string name=\"ambilight_ratio_fix_desc\">Коригира липсващото пристрастно осветление. Поправя неправилно съотношение на страните или мащаб на видеото. Поправя празни снимки на екрана. Влияе на производителността!</string>\n  <string name=\"force_legacy_codecs_desc\">Значително подобрява производителността на устройства от нисък клас. Максималната разделителна способност е 720p.</string>\n  <string name=\"ambilight_ratio_fix\">Поправка на Ambilight/Aspect ratio/Video scale/Screenshots</string>\n  <string name=\"force_legacy_codecs\">Налагане на наследени кодеци (720p)</string>\n  <string name=\"live_stream_fix_desc\">Предупреждение, че тази настройка деактивира пренавиването на потоците. Значително подобрява производителността на потоците на живо при устройства от нисък клас. Максималната разделителна способност е 1080p.</string>\n  <string name=\"live_stream_fix_4k_desc\">Предупреждение че тази настройка деактивира пренавиването на потоците. Значително подобрява производителността на потоците на живо. Максималната разделителна способност е 4K.</string>\n  <string name=\"live_stream_fix\">Поправка на живото предаване (1080p)</string>\n  <string name=\"live_stream_fix_4k\">Поправка на живото предаване (4К)</string>\n  <string name=\"playback_notifications_fix_desc\">Скрива известията за промяна на пистата. Може да е полезно за AOSP-базирани фърмуери.</string>\n  <string name=\"playback_notifications_fix\">Деактивиране на известията за възпроизвеждане</string>\n  <string name=\"amlogic_fix_desc\">Поправка на падането на кадри при устройства, базирани на Amlogic.</string>\n  <string name=\"amlogic_fix\">Поправка на Amlogic 1080p\\@60fps</string>\n  <string name=\"tunneled_video_playback_desc\">ЗАБЕЛЕЖКА: паузата може да не работи правилно! Тунелното възпроизвеждане на видео обещава предимства като по-добра аудио/видео синхронизация (AV синхронизация) и по-плавно възпроизвеждане. Изисква се Android 5+</string>\n  <string name=\"tunneled_video_playback\">Тунелно възпроизвеждане на видео (Android 5+)</string>\n  <string name=\"master_volume\">Основна сила на звука</string>\n  <string name=\"volume_limit\">Ограничение на звука</string>\n  <string name=\"player_volume\">Звук</string>\n  <string name=\"play_video\">Пусни</string>\n  <string name=\"remember_position_of_short_videos\">Запомнете позицията на кратките видеоклипове (по-малко от 5 минути)</string>\n  <string name=\"player_show_tooltips\">Показване на надписи на бутони</string>\n  <string name=\"action_like_unset\">Харесване не е зададено</string>\n  <string name=\"action_dislike_unset\">Харесване не е зададено</string>\n  <string name=\"various_buttons\">Нехаресване не е зададено</string>\n  <string name=\"not_compatible_with\">Избраната опция не е съвместима с</string>\n  <string name=\"subtitle_position\">Изместване на долната част на субтитрите</string>\n  <string name=\"pressing_home\">с натискане на НАЧАЛО</string>\n  <string name=\"pressing_home_back\">с натискане на НАЧАЛО и НАЗАД </string>\n  <string name=\"pressing_back\">с натискане на НАЗАД </string>\n  <string name=\"player_number_key_seek\">Търсене с цифрови клавиши</string>\n  <string name=\"save_remove_playlist\">Добавяне/премахване на списък за възпроизвеждане от раздел \\\"Списъци за възпроизвеждане</string>\n  <string name=\"remove_playlist\">Премахване на списъка за възпроизвеждане от раздела \\\"Списъци за възпроизвеждане\\\" завинаги</string>\n  <string name=\"removed_from_playlists\">Премахнати от раздел Плейлисти</string>\n  <string name=\"saved_to_playlists\">Добавено към раздел Плейлисти</string>\n  <string name=\"create_playlist\">Създай плейлист</string>\n  <string name=\"add_video_to_new_playlist\">Добави към нов плейлист</string>\n  <string name=\"cant_delete_empty_playlist\">Не може да се изтрие празен плейлист</string>\n  <string name=\"playlist\">Плейлист</string>\n  <string name=\"rename_playlist\">Преименовай плейлист</string>\n  <string name=\"cant_rename_empty_playlist\">Не може да се преименова празен плейлист</string>\n  <string name=\"cant_rename_foreign_playlist\">Не може да се преименова чужд плейлист</string>\n  <string name=\"cant_save_playlist\">Този плейлист не може да бъде добавен</string>\n  <string name=\"enter_value\">Въведете произволна стойност, която не е празна</string>\n  <string name=\"dialog_add_remove_from\">Добавяне/отстраняване от %s</string>\n  <string name=\"lb_playback_controls_skip_next\">Пропусни следващия</string>\n  <string name=\"lb_playback_controls_skip_previous\">Пропускане на предишната/връщане към началната позиция</string>\n  <string name=\"alt_presets_behavior\">Промяна на поведението на предварителните настройки (ограничаване на честотната лента)</string>\n  <string name=\"alt_presets_behavior_desc\">Приложението ще се опита да поддържа широчина на честотната лента, съответстваща на избраната предварителна настройка, вместо да съпоставя разделителна способност, кадри в секунда и кодек.</string>\n  <string name=\"sleep_timer\">Таймер за заспиване (ако дистанционното не се използва един час)</string>\n  <string name=\"sleep_timer_desc\">Спиране на възпроизвеждането, ако потребителят не е използвал дистанционното управление в продължение на един час</string>\n  <string name=\"disable_vsync\">Деактивиране на snap към vsync</string>\n  <string name=\"disable_vsync_desc\">Тази опция деактивира подравняването на кадрите с вертикалния синхронизиращ сигнал на дисплея. Това може да подобри производителността на устройства от нисък клас, като освободи ресурси на процесора.</string>\n  <string name=\"skip_codec_profile_check\">Пропускане на проверката на нивото на профила на кодека</string>\n  <string name=\"skip_codec_profile_check_desc\">Не проверявайте поддръжката на кодеци, когато стартирате видеоклип. Може да е полезно при бъгави фърмуери.</string>\n  <string name=\"force_sw_codec\">Принуди SW видео декодер</string>\n  <string name=\"force_sw_codec_desc\">Може да възпроизвежда почти всяко видео, но изпълнението е много лошо.</string>\n  <string name=\"playlist_order\">Сортиране на списъка за изпълнение</string>\n  <string name=\"playlist_order_added_date_newer_first\">Дата на добавяне (първо по-новата)</string>\n  <string name=\"playlist_order_added_date_older_first\">Дата на добавяне (първо по-старите)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Дата на публикуване (по-новата първа)</string>\n  <string name=\"playlist_order_published_date_older_first\">Дата на публикуване (първо по-старата)</string>\n  <string name=\"playlist_order_popularity\">Популярност</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">Не моге да се направи за чужд плейлист</string>\n  <string name=\"owned_playlist_warning\">Грешка. Това действие може да се прилага само за собствени списъци за възпроизвеждане</string>\n  <string name=\"content_block_alt_server\">Използване на алтернативен сървър</string>\n  <string name=\"content_block_alt_server_desc\">Активирайте тази опция, ако SponsorBlock отказва да работи по различни причини.</string>\n  <string name=\"unset_stream_reminder\">Напомняне за неустановен поток</string>\n  <string name=\"set_stream_reminder\">Задаване на напомняне за поток</string>\n  <string name=\"playback_starts_shortly\">Възпроизвеждането ще започне автоматично, когато потокът е готов</string>\n  <string name=\"starting_stream\">Стартиране на потока...</string>\n  <string name=\"action_playlist_remove\">Премахване от списъка за възпроизвеждане</string>\n  <string name=\"signin_view_title\">Потребителският код се зарежда...</string>\n  <string name=\"signin_view_description\">За да влезете в системата, въведете този код на страница %s\\nЗабележка. Работи само с браузъри Firefox или Chrome.</string>\n  <string name=\"signin_view_action_text\">Завършено</string>\n  <string name=\"require_checked\">Изисква \\\"%s</string>\n  <string name=\"msg_press_again_to_exit\">Натиснете отново, за изход</string>\n  <string name=\"player_remaining_time\">Оставащи: %s</string>\n  <string name=\"player_ending_time\">Завършва на %s</string>\n  <string name=\"badge_new_content\">НОВО СЪДЪРЖАНИЕ</string>\n  <string name=\"badge_live\">НА ЖИВО</string>\n  <string name=\"add_device_view_description\">За да свържете устройството, въведете този код в приложението YouTube на телефона си в раздел Настройки/Вижте на телевизора\\nНота. Работи само с клавиатурата Gboard.</string>\n  <string name=\"playback_controls_repeat_pause\">Повтаряне на паузата</string>\n  <string name=\"playback_controls_repeat_list\">Повтаряне на списъка</string>\n  <string name=\"action_subscribe_off\">Изключи абониране</string>\n  <string name=\"action_subscribe_on\">Включи абониране</string>\n  <string name=\"skip_24_rate\">Пропускане на форматите с 24 кадъра в секунда (поправка на реалния кинорежим\\?)</string>\n  <string name=\"player_disable_suggestions\">Деактивиране на предложенията</string>\n  <string name=\"feedback\">Обратна връзка</string>\n  <string name=\"sources\">Източници</string>\n  <string name=\"releases\">Издания</string>\n  <string name=\"prefer_avc_over_vp9\">Избор на кодек: предпочитате avc пред vp9</string>\n  <string name=\"open_chat\">Чат/коментари</string>\n  <string name=\"open_comments\">Отвори коментари</string>\n  <string name=\"place_chat_left\">Показвай чата вляво</string>\n  <string name=\"use_alt_speech_recognizer\">Разпознаване на алтернативна реч</string>\n  <string name=\"time_format\">Формат на времето</string>\n  <string name=\"time_format_24\">24-часов</string>\n  <string name=\"time_format_12\">12-часов (пр.об./сл.об.)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Сериозен проблем. Използвайте го само ако имате проблеми с разпознавателя по подразбиране.</string>\n  <string name=\"speech_recognizer\">Разпознаване на речта</string>\n  <string name=\"speech_recognizer_system\">Система (за предпочитане)</string>\n  <string name=\"speech_recognizer_external_1\">Външен 1 (сериозно сгрешен, за да работи, трябва да забраните достъпа до микрофона)</string>\n  <string name=\"speech_recognizer_external_2\">Външнен 2 (със сериозни грешки)</string>\n  <string name=\"real_channel_icon\">Показване на икона върху бутона на канала</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Жълто на полупрозрачен фон</string>\n  <string name=\"subtitle_yellow_black\">Жълто на черен фон</string>\n  <string name=\"player_pixel_ratio\">Съотношение на пикселите</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Тъмно сиво (едноцветно)</string>\n  <string name=\"disable_mic_permission\">Моля, деактивирайте достъпа до микрофона за приложението, за да работи разпознавателят правилно.</string>\n  <string name=\"repeat_mode_shuffle\">Разбъркване на всяка плейлиста</string>\n  <string name=\"chat_left\">Ляво</string>\n  <string name=\"chat_right\">Дясно</string>\n  <string name=\"card_real_thumbnails\">Замяна на миниатюрите с кадър от видеоклипа</string>\n  <string name=\"card_content\">Откъде да вземете миниатюрите на картите</string>\n  <string name=\"thumb_quality_default\">По подразбиране</string>\n  <string name=\"thumb_quality_start\">Начало на видеоклипа</string>\n  <string name=\"thumb_quality_middle\">Среда на видеоклипа</string>\n  <string name=\"thumb_quality_end\">Край на видеоклипа</string>\n  <string name=\"content_block_status\">Проверка на състоянието на SponsorBlock</string>\n  <string name=\"player_show_quality_info_bitrate\">Добавяне на битрейт в информацията за качеството</string>\n  <string name=\"player_speed_button_old_behavior\">Връщане на старото поведение на бутона за скорост</string>\n  <string name=\"protect_settings_with_password\">Защита на всички настройки с парола</string>\n  <string name=\"enter_settings_password\">Въвеждане на парола за настройките</string>\n  <string name=\"child_mode\">Детски режим</string>\n  <string name=\"child_mode_desc\">В този режим потребителят не може да използва търсене или да вижда предложено съдържание. Настройките ще се намират под паролата.</string>\n  <string name=\"lost_setting_warning\">Настройките на приложението ще бъдат променени. Уверете се, че сте създали резервно копие на настройките.</string>\n  <string name=\"player_button_long_click\">Кратко щракване върху бутона за бързо действие и дълго за диалогов прозорец с настройки</string>\n  <string name=\"pause_history\">Пауза на историята</string>\n  <string name=\"resume_history\">Активирай история</string>\n  <string name=\"disable_history\">Деактивирай историята</string>\n  <string name=\"enable_history\">Активирай историята</string>\n  <string name=\"clear_history\">Изисти историята</string>\n  <string name=\"chapters\">Глави</string>\n  <string name=\"card_multiline_subtitle\">Многоредови субтитри</string>\n  <string name=\"auto_frame_rate_modes\">Поддържани режими</string>\n  <string name=\"loading\">Зареждане...</string>\n  <string name=\"hide_streams\">Скриване на потоци от абонаменти</string>\n  <string name=\"video_rotate\">Завъртане</string>\n  <string name=\"trending_searches\">Набиращи популярност търсения</string>\n  <string name=\"video_duration_any\">Всички</string>\n  <string name=\"video_duration\">Продължителност</string>\n  <string name=\"video_duration_under_4\">Под 4 минути</string>\n  <string name=\"video_duration_between_4_20\">4-20 минути</string>\n  <string name=\"video_duration_over_20\">Над 20 минути</string>\n  <string name=\"content_type\">Вид</string>\n  <string name=\"content_type_any\">Всички</string>\n  <string name=\"content_type_video\">Видео</string>\n  <string name=\"content_type_channel\">Канал</string>\n  <string name=\"content_type_playlist\">Плейлист</string>\n  <string name=\"content_type_movie\">Филм</string>\n  <string name=\"video_feature_any\">Всики</string>\n  <string name=\"video_feature_live\">На живо</string>\n  <string name=\"video_feature_4k\">4К</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"video_features\">Функции</string>\n  <string name=\"search_sorting\">Сортирай по</string>\n  <string name=\"sort_by_relevance\">Уместност</string>\n  <string name=\"sort_by_views\">Брой преглеждания</string>\n  <string name=\"sort_by_date\">Дата на качване</string>\n  <string name=\"sort_by_rating\">Рейтинг</string>\n  <string name=\"clear_search_history\">Изчистване на историята на търсене</string>\n  <string name=\"remove_from_subscriptions\">Скрий</string>\n  <string name=\"player_long_speed_list\">Повече скорости в списъка със скорости</string>\n  <string name=\"alt_app_icon\">Алтернативна икона на приложение (необходимо е рестартиране)</string>\n  <string name=\"network_stack\">Предпочитане на мрежовия стек %s</string>\n  <string name=\"player_network_stack\">Мрежов стек</string>\n  <string name=\"cronet_desc\">Cronet е мрежовият стек на Chromium. Cronet може да намали латентността и да увеличи производителността на мрежата, което може да помогне при проблеми с буферирането.</string>\n  <string name=\"unlock_all_formats\">Отключване на всички видео формати</string>\n  <string name=\"unlock_all_formats_desc\">При някои устройства фърмуерът некоректно съобщава за някои формати като неподдържани, дори и да са такива.\\n(напр. 1080p смарт телевизорите обикновено съобщават, че 4k не се поддържа).</string>\n  <string name=\"okhttp_desc\">Този мрежов стек е най-бавният, но има добра стабилност.</string>\n  <string name=\"select_channel_section\">Отваряне на съответния раздел при стартиране на приложението от ATV Channels</string>\n  <string name=\"enable_voice_search_desc\">Официалното приложение ще бъде заменено с т.нар. мост. Мостът помага да се прехвърлят глобалните заявки за търсене към нашето приложение.</string>\n  <string name=\"enable_master_password\">Активиране на главната парола</string>\n  <string name=\"enter_master_password\">Въведи главната парола</string>\n  <string name=\"disable_stream_buffer\">Деактивиране на буфера на потоците</string>\n  <string name=\"disable_stream_buffer_desc\">Корекция за ситуации, когато потокът е много назад. Забележка: потокът може да започне да изостава.</string>\n  <string name=\"sony_frame_drop_fix\">Поправка на капка рамка #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">Поправяне на забавянията при Sony TV и някои други устройства. Забележка: възможни са проблеми със синхронизацията на звука.</string>\n  <string name=\"audio_language\">Език на аудиото</string>\n  <string name=\"old_home_look\">Стар вид на раздел Начало</string>\n  <string name=\"hide_shorts_everywhere\">Скрийте кратките видеа навсякъде</string>\n  <string name=\"update_found\">Актуализирай</string>\n  <string name=\"volume_boost_warning\">Всякакви настройки над границата могат да повредят високоговорителите.</string>\n  <string name=\"play_video_incognito\">Възпроизвеждане инкогнито</string>\n  <string name=\"header_kids_home\">Деца</string>\n  <string name=\"player_section_playlist\">Използване на съдържанието на текущия раздел като списък за възпроизвеждане</string>\n  <string name=\"header_trending\">Тенденции</string>\n  <string name=\"screensaver\">Скрийнсейвър</string>\n  <string name=\"player_screen_off_timeout\">Време за изключване на екрана</string>\n  <string name=\"old_update_notifications\">Стар вид на известията за актуализация</string>\n  <string name=\"dialog_notification\">Уведомяване за актуализацията с изскачащ диалогов прозорец</string>\n  <string name=\"mark_as_watched\">Маркирайте като гледан</string>\n  <string name=\"player_ui_animations\">Активиране на анимациите на контролния панел</string>\n  <string name=\"player_likes_count\">Покажете броя на харесванията/нехаресванията</string>\n  <string name=\"sorting_alphabetically2\">По азбучен ред (бързо)</string>\n  <string name=\"sorting_default\">По подразбиране (бързо)</string>\n  <string name=\"content_block_exclude_channel\">Изключване на този канал от SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">Спрете да изключвате този канал от SponsorBlock</string>\n  <string name=\"player_chapter_notification\">Показване на известие за бързо преминаване през главите</string>\n  <string name=\"subtitle_remember\">Запомнете активираните субтитри за всеки канал</string>\n  <string name=\"amazon_frame_drop_fix\">Поправка за падане на рамката #2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">Предназначено за устройства Amazon Stick. Може да работи и на други устройства.</string>\n  <string name=\"default_stack_desc\">Вграден мрежов стек. Този стек може да има по-добра стабилност в определени ситуации.</string>\n  <string name=\"item_postion\">Позиция на</string>\n  <string name=\"player_screen_off_dimming\">Размер на затъмнението при изключен екран</string>\n  <string name=\"screensaver_timout\">Време за прекъсване на скрийнсейвъра</string>\n  <string name=\"screensaver_dimming\">Степен на затъмняване на екрана</string>\n  <string name=\"player_ui_on_next\">Показване на потребителския интерфейс на плейъра при преминаване към следващия видеоклип</string>\n  <string name=\"autogenerated\">автоматично генериран</string>\n  <string name=\"player_auto_volume\">Автоматично регулиране на силата на звука</string>\n  <string name=\"header_shorts\">Кратки видеа</string>\n  <string name=\"player_global_focus\">Безпроблемна навигация между редовете с бутони на играчите</string>\n  <string name=\"auto_history\">Автоматично (използвайте настройките на акаунта)</string>\n  <string name=\"remember_position_subscriptions\">Запомняне на последно разглежданата позиция в Subscriptions</string>\n  <string name=\"msg_player_unknown_error\">Неизвестна грешка</string>\n  <string name=\"header_notifications\">Известия</string>\n  <string name=\"disable_search_history\">Деактивиране на историята на търсенето</string>\n  <string name=\"unlock_high_bitrate_formats\">Отключване на формати с висок битрейт 1080p vp9</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Отключване на формати mp4a с висок битрейт</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-ca/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Inici</string>\n  <string name=\"title_search\">Cerca</string>\n  <string name=\"header_subscriptions\">Subscripcions</string>\n  <string name=\"header_history\">Història</string>\n  <string name=\"header_music\">Música</string>\n  <string name=\"header_news\">Notícies</string>\n  <string name=\"header_gaming\">Jocs</string>\n  <string name=\"header_playlists\">Llistes de reproducció</string>\n  <string name=\"header_settings\">Configuració</string>\n  <string name=\"header_channels\">Canals</string>\n  <string name=\"subscriptions_signin_title\">Consulta les últimes novetats dels canals que t\\'agraden</string>\n  <string name=\"subscriptions_signin_subtitle\">Inicieu la sessió per veure les vostres subscripcions</string>\n  <string name=\"subscriptions_signin_button_text\">REGISTRAR-SE</string>\n  <string name=\"library_signin_to_show_more\">Mira els vídeos que t\\'han agradat, desats o subscrits</string>\n  <string name=\"library_signin_subtitle\">Inicieu la sessió per veure la vostra biblioteca</string>\n  <string name=\"action_signin\">REGISTRAR-SE</string>\n  <string name=\"title_video_formats\">Formats de vídeo</string>\n  <string name=\"title_audio_formats\">Formats d\\'àudio</string>\n  <string name=\"subtitle_category_title\">Subtítols</string>\n  <string name=\"playback_queue_category_title\">Cua de reproducció</string>\n  <string name=\"video_max_quality\">Automàtic (qualitat màxima)</string>\n  <string name=\"audio_max_quality\">Automàtic (qualitat màxima)</string>\n  <string name=\"subtitles_disabled\">Subtítols desactivats</string>\n  <string name=\"auto_frame_rate\">Velocitat de fotogrames automàtica</string>\n  <string name=\"frame_rate_correction\">Corregir fps: \\n %s</string>\n  <string name=\"category_background_playback\">Reproducció de fons</string>\n  <string name=\"not_implemented\">No implementat</string>\n  <string name=\"option_background_playback_off\">Inhabilitat</string>\n  <string name=\"option_background_playback_pip\">Imatge a imatge</string>\n  <string name=\"option_background_playback_only_audio\">Només àudio</string>\n  <string name=\"playback_settings\">Configuració de qualitat de reproducció</string>\n  <string name=\"video_speed\">Velocitat del vídeo</string>\n  <string name=\"resolution_switch\">Canvia la resolució</string>\n  <string name=\"video_buffer\">Buffer de vídeo</string>\n  <string name=\"video_buffer_size_none\">Cap</string>\n  <string name=\"video_buffer_size_low\">baix</string>\n  <string name=\"video_buffer_size_med\">Mitjana</string>\n  <string name=\"video_buffer_size_high\">Alt</string>\n  <string name=\"update_changelog\">Registre de canvis</string>\n  <string name=\"install_update\">Instal·leu l\\'actualització</string>\n  <string name=\"section_is_empty\">Vaja! Aquí no hi ha res.</string>\n  <string name=\"dialog_add_to_playlist\">Afegeix/elimina de la llista de reproducció</string>\n  <string name=\"msg_signed_users_only\">Només usuaris signats</string>\n  <string name=\"msg_cant_load_content\">No es pot carregar el contingut. \\n 1) Comproveu la data i l\\'hora al dispositiu. \\n 2) Comproveu la connexió de xarxa. \\n 3) Comproveu la configuració del servidor intermediari. \\n 4) Intenta iniciar la sessió al teu compte. \\n 5) Potser aquí no hi ha re</string>\n  <string name=\"title_video_presets\">Preajustos de vídeo</string>\n  <string name=\"settings_accounts\">Comptes</string>\n  <string name=\"settings_left_panel\">Edita categories</string>\n  <string name=\"settings_themes\">Temes</string>\n  <string name=\"settings_other\">Altres</string>\n  <string name=\"settings_player\">Reproductor de vídeo</string>\n  <string name=\"settings_language\">Llenguatge</string>\n  <string name=\"settings_linked_devices\">Dispositius enllaçats</string>\n  <string name=\"settings_about\">Sobre</string>\n  <string name=\"dialog_account_list\">Seleccioneu el compte</string>\n  <string name=\"dialog_account_none\">Cap</string>\n  <string name=\"dialog_remove_account\">Tanca sessió</string>\n  <string name=\"dialog_add_account\">Inicia sessió</string>\n  <string name=\"default_lang\">Per defecte</string>\n  <string name=\"dialog_select_language\">Llenguatge</string>\n  <string name=\"subtitle_default\">Per defecte</string>\n  <string name=\"subtitle_white_semi_transparent\">Blanc sobre fons semitransparent</string>\n  <string name=\"subtitle_style\">Estil de subtítols</string>\n  <string name=\"subtitle_language\">Idioma dels subtítols</string>\n  <string name=\"action_search\">Cerca</string>\n  <string name=\"settings_main_ui\">Interfície d\\'usuari</string>\n  <string name=\"dialog_main_ui\">Interfície d\\'usuari</string>\n  <string name=\"card_animated_previews\">Previsualitzacions animades</string>\n  <string name=\"web_site\">Lloc web</string>\n  <string name=\"donation\">Donació</string>\n  <string name=\"dialog_about\">Sobre</string>\n  <string name=\"dialog_player_ui\">Reproductor de vídeo</string>\n  <string name=\"player_show_ui_on_pause\">Mostra la interfície d\\'usuari en pausa</string>\n  <string name=\"player_pause_on_ok\">La tecla D\\'acord posa en pausa la reproducció</string>\n  <string name=\"player_ok_button_behavior\">Comportament del botó D\\'acord</string>\n  <string name=\"player_only_ui\">Només IU</string>\n  <string name=\"player_ui_and_pause\">Interfície d\\'usuari i pausa</string>\n  <string name=\"player_only_pause\">Només una pausa</string>\n  <string name=\"check_for_updates\">Buscar actualitzacions</string>\n  <string name=\"update_not_found\">Estàs utilitzant la darrera versió</string>\n  <string name=\"update_in_progress\">Espera…</string>\n  <string name=\"player_ui_hide_behavior\">Oculta automàticament la IU</string>\n  <string name=\"option_never\">Mai</string>\n  <string name=\"side_panel_sections\">Seccions de muntatge</string>\n  <string name=\"boot_to_section\">Arrencada a la secció</string>\n  <string name=\"large_ui\">Interfície d\\'usuari gran</string>\n  <string name=\"video_grid_scale\">Escala de graella de vídeo</string>\n  <string name=\"scale_ui\">\tescala de la IU</string>\n  <string name=\"color_scheme\">Esquema de colors</string>\n  <string name=\"color_scheme_default\">Per defecte</string>\n  <string name=\"color_scheme_red_grey\">Vermell-gris</string>\n  <string name=\"color_scheme_red\">Vermell</string>\n  <string name=\"color_scheme_dark_grey\">Gris fosc</string>\n  <string name=\"disable_update_check\">Desactiva la comprovació d\\'actualitzacions</string>\n  <string name=\"show_again\">Mostra de nou</string>\n  <string name=\"check_updates_auto\">Notificar sobre la nova versió</string>\n  <string name=\"select_account_on_boot\">Seleccioneu a l\\'arrencada</string>\n  <string name=\"player_other\">Misc</string>\n  <string name=\"player_full_date\">Data precisa a la descripció</string>\n  <string name=\"open_channel\">Obrir Canal</string>\n  <string name=\"open_playlist\">Obre la llista de reproducció</string>\n  <string name=\"not_interested\">No m\\'interesa</string>\n  <string name=\"you_wont_see_this_video\">El vídeo s\\'ha suprimit dels recomanats</string>\n  <string name=\"settings_search\">Cerca</string>\n  <string name=\"dialog_search\">Cerca</string>\n  <string name=\"instant_voice_search\">Cerca per veu instantània</string>\n  <string name=\"option_background_playback_behind\">Reprodueix cap enrere</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">La informació del següent vídeo encara no s\\'ha carregat</string>\n  <string name=\"card_multiline_title\">Títols multilínia</string>\n  <string name=\"cards_style\">Estil de cartes</string>\n  <string name=\"repeat_mode_all\">Reprodueix vídeos contínuament</string>\n  <string name=\"repeat_mode_one\">Repetiu el vídeo actual</string>\n  <string name=\"repeat_mode_pause\">Posa en pausa la reproducció després de cada vídeo</string>\n  <string name=\"repeat_mode_pause_alt\">Reprodueix només vídeos de llistes de reproducció contínuament</string>\n  <string name=\"repeat_mode_none\">Atura la reproducció després d\\'un vídeo</string>\n  <string name=\"subscribed_to_channel\">Canal subscrit</string>\n  <string name=\"unsubscribed_from_channel\">Canal no subscrit</string>\n  <string name=\"subtitle_yellow_transparent\">Groc sobre fons transparent</string>\n  <string name=\"subtitle_white_transparent\">Blanc sobre fons transparent</string>\n  <string name=\"subtitle_white_black\">Blanc sobre fons negre</string>\n  <string name=\"player_seek_preview\">Previsualitza mentre es cerca</string>\n  <string name=\"color_scheme_dark_grey_oled\">Gris fosc (OLED)</string>\n  <string name=\"channels_section_sorting\">Ordenació de la secció de canals</string>\n  <string name=\"sorting_by_new_content\">Nou contingut</string>\n  <string name=\"sorting_alphabetically\">Alfabèticament</string>\n  <string name=\"sorting_last_viewed\">Darrera visita</string>\n  <string name=\"player_pause_when_seek\">\tPausa en cercar</string>\n  <string name=\"playlists_style\">Estil de la secció de llistes de reproducció</string>\n  <string name=\"playlists_style_grid\">Quadrícula</string>\n  <string name=\"playlists_style_rows\">Fileres</string>\n  <string name=\"player_show_clock\">Mostra el rellotge a la barra de controls</string>\n  <string name=\"player_show_remaining_time\">Mostra el temps restant a la barra de controls</string>\n  <string name=\"player_show_ending_time\">Mostra l\\'hora de finalització a la barra de controls</string>\n  <string name=\"open_channel_uploads\">Obre les càrregues del canal</string>\n  <string name=\"app_exit_shortcut\">Sortiu de l\\'aplicació</string>\n  <string name=\"app_exit_none\">No surtis</string>\n  <string name=\"app_double_back_exit\">Reprodueix doble enrere</string>\n  <string name=\"app_single_back_exit\">Reprodueix simple enrere</string>\n  <string name=\"action_video_zoom\">Zoom de vídeo</string>\n  <string name=\"video_zoom\">Zoom de vídeo</string>\n  <string name=\"video_zoom_default\">Per defecte</string>\n  <string name=\"video_zoom_fit_width\">\tAmple d\\'ajust</string>\n  <string name=\"video_zoom_fit_height\">Alçada d\\'ajust</string>\n  <string name=\"video_zoom_fit_both\">S\\'adapta a l\\'amplada o a l\\'alçada</string>\n  <string name=\"video_zoom_stretch\">Estirar</string>\n  <string name=\"color_scheme_teal\">Verd</string>\n  <string name=\"color_scheme_teal_oled\">Verd (OLED)</string>\n  <string name=\"player_seek_preview_none\">Inhabilitat</string>\n  <string name=\"player_seek_preview_single\">Marc úni</string>\n  <string name=\"player_seek_preview_carousel\">Carrusel</string>\n  <string name=\"player_seek_preview_carousel_slow\">\tCarrusel (lent, per fotogrames clau)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Carrusel (visualització prèvia ràpida i no precisa)</string>\n  <string name=\"unsubscribe_from_channel\">Cancel·la la subscripció al canal</string>\n  <string name=\"card_title_lines_num\">Línies de títol de la targeta núm</string>\n  <string name=\"card_auto_scrolled_title\">Títol retallat amb desplaçament automàtic</string>\n  <string name=\"share_link\">Compartir Enllaç</string>\n  <string name=\"subscribe_to_channel\">Subscriu-te al canal</string>\n  <string name=\"wait_data_loading\">Espereu mentre es carreguen les dades...</string>\n  <string name=\"auto_frame_rate_pause\">Velocitat de fotogrames automàtica (pausa en canviar)</string>\n  <string name=\"auto_frame_rate_applying\">S\\'està aplicant la velocitat de fotogrames automàtica %s x %s \\@ %s</string>\n  <string name=\"audio_shift\">Canvi d\\'àudio</string>\n  <string name=\"player_remember_speed\">\tRecordeu la velocitat</string>\n  <string name=\"mark_channel_as_watched\">Marca com a observat</string>\n  <string name=\"channel_marked_as_watched\">El canal s\\'ha marcat com a vist</string>\n  <string name=\"dialog_add_device\">Afegeix un dispositiu</string>\n  <string name=\"dialog_remove_all_devices\">Elimina tots els dispositius</string>\n  <string name=\"device_link_enabled\">Enllaç habilitat</string>\n  <string name=\"device_connected\">El dispositiu \\\" %s \\\" s\\'ha connectat</string>\n  <string name=\"device_disconnected\">El dispositiu \\\" %s \\\" s\\'ha desconnectat</string>\n  <string name=\"settings_ui_scale\">escala de la IU</string>\n  <string name=\"msg_restart_app\">Si us plau, reinicieu l\\'aplicació per aplicar aquesta configuració</string>\n  <string name=\"settings_block\">Bloqueig de contingut</string>\n  <string name=\"msg_applying\">S\\'està aplicant %s</string>\n  <string name=\"msg_done\">Fet</string>\n  <string name=\"settings_general\">General</string>\n  <string name=\"settings_video\">Vídeo</string>\n  <string name=\"content_block_confirm_skip\">Confirmeu en saltar</string>\n  <string name=\"content_block_categories\">Omet segments</string>\n  <string name=\"content_block_sponsor\">Patrocinador</string>\n  <string name=\"content_block_intro\">Animació d\\'entreacte/introducció</string>\n  <string name=\"content_block_outro\">Targetes finals/crèdits</string>\n  <string name=\"content_block_interaction\">Recordatori d\\'interacció (subscriure\\'s)</string>\n  <string name=\"content_block_self_promo\">Promoció no remunerada/autopromoció</string>\n  <string name=\"content_block_music_off_topic\">Secció no musical del clip</string>\n  <string name=\"confirm_segment_skip\">Ometeu el segment \\\" %s \\\"\\?</string>\n  <string name=\"cancel_segment_skip\">Cancel·la l\\'omissió de segment</string>\n  <string name=\"msg_skipping_segment\">S\\'està saltant el segment \\\" %s \\\"...</string>\n  <string name=\"content_block_notification_type\">Tipus de notificació</string>\n  <string name=\"content_block_notify_none\">Sense notificació</string>\n  <string name=\"content_block_notify_toast\">Torrades</string>\n  <string name=\"content_block_notify_dialog\">Diàleg de confirmació</string>\n  <string name=\"return_to_launcher\">Torneu al llançador des dels canals/cerca d\\'ATV</string>\n  <string name=\"intent_force_close\">Força la sortida si el vídeo s\\'ha obert des d\\'una font externa</string>\n  <string name=\"btn_confirm\">Confirmeu</string>\n  <string name=\"player_low_video_quality\">Baixa qualitat de vídeo</string>\n  <string name=\"remote_session_closed\">La sessió remota s\\'ha tancat</string>\n  <string name=\"msg_mode_switch_error\">No es pot canviar el mode de visualització a \\\" %s</string>\n  <string name=\"msg_player_error\">S\\'ha produït un error del jugador %s . S\\'està reiniciant la reproducció...</string>\n  <string name=\"video_preset_disabled\">Sense preajust</string>\n  <string name=\"video_preset_enabled\">El format del següent vídeo s\\'establirà segons el valor predefinit</string>\n  <string name=\"player_sleep_timer\">Temporitzador de son</string>\n  <string name=\"player_show_quality_info\">Mostra informació de qualitat a la barra de controls</string>\n  <string name=\"player_remember_each_speed\">Recordeu cada velocitat de vídeo</string>\n  <string name=\"player_remember_speed_none\">Cap</string>\n  <string name=\"player_remember_speed_all\">El mateix en tots els vídeos</string>\n  <string name=\"player_remember_speed_each\">Per cada vídeo</string>\n  <string name=\"msg_player_error_source\">No es pot descarregar el vídeo</string>\n  <string name=\"msg_player_error_renderer\">El format de vídeo seleccionat no és compatible. \\n Potser és un error del firmware. Intenta reiniciar el dispositiu.</string>\n  <string name=\"msg_player_error_unexpected\">Error desconegut del descodificador de vídeo</string>\n  <string name=\"video_aspect\">Relació d\\'aspecte</string>\n  <string name=\"player_tweaks\">Opcions de desenvolupament</string>\n  <string name=\"header_uploads\">Càrregues</string>\n  <string name=\"player_show_global_clock\">Mostra el rellotge sempre a la pantalla</string>\n  <string name=\"player_show_global_ending_time\">Mostra l\\'hora de finalització sempre a la pantalla</string>\n  <string name=\"uploads_old_look\">Revereix l\\'aspecte antic de la secció de càrregues</string>\n  <string name=\"background_playback_activation\">Reproducció en segon pla (activació)</string>\n  <string name=\"channels_old_look\">Revereix l\\'aspecte antic de la secció Canals</string>\n  <string name=\"channels_auto_load\">Carrega automàticament el contingut de la secció de canals</string>\n  <string name=\"return_to_background_video\">Torna al vídeo en segon pla</string>\n  <string name=\"pin_unpin_from_sidebar\">Fixa/Desenganxa de la barra lateral</string>\n  <string name=\"pin_unpin_playlist\">Fixa/Desenganxa la llista de reproducció de la barra lateral</string>\n  <string name=\"pin_unpin_channel\">Fixa/Desfixa el canal de la barra lateral</string>\n  <string name=\"pinned_to_sidebar\">Fixat a la barra lateral</string>\n  <string name=\"unpin_from_sidebar\">Desenganxa de la barra lateral</string>\n  <string name=\"unpinned_from_sidebar\">S\\'ha desfixat de la barra lateral</string>\n  <string name=\"dialog_select_country\">País</string>\n  <string name=\"settings_language_country\">Idioma/País</string>\n  <string name=\"share_embed_link\">Comparteix l\\'enllaç incrustat</string>\n  <string name=\"card_text_scroll_factor\">Velocitat de desplaçament del text de la targeta</string>\n  <string name=\"preferred_update_source\">Seleccioneu la font d\\'actualització</string>\n  <string name=\"hide_shorts\">Amaga els curts de les subscripcions</string>\n  <string name=\"key_remapping\">Reassignació de claus</string>\n  <string name=\"screen_dimming\">Atenuació de la pantalla</string>\n  <string name=\"removed_from_playback_queue\">S\\'ha eliminat de la cua de reproducció</string>\n  <string name=\"added_to_playback_queue\">S\\'ha afegit a la cua de reproducció</string>\n  <string name=\"add_remove_from_playback_queue\">Afegeix/Elimina de la cua de reproducció</string>\n  <string name=\"add_to_playback_queue\">Afegeix a la cua de reproducció</string>\n  <string name=\"remove_from_playback_queue\">Elimina de la cua de reproducció</string>\n  <string name=\"proxy_enabled\">Proxy habilitat</string>\n  <string name=\"proxy_disabled\">Proxy desactivat</string>\n  <string name=\"uploads_row_name\">Càrregues</string>\n  <string name=\"playlists_row_name\">Llistes de reproducció creades</string>\n  <string name=\"popular_uploads_row_name\">Càrregues populars</string>\n  <string name=\"breaking_news_row_name\">Notícies d\\'última hora</string>\n  <string name=\"covid_news_row_name\">Notícies sobre el covid-19</string>\n  <string name=\"enable_voice_search\">Activa la cerca per veu</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Subscriu/cancel·la la subscripció al canal</string>\n  <string name=\"app_backup_restore\">Restaurar còpia de seguretat</string>\n  <string name=\"app_backup\">Còpia de seguretat de dades</string>\n  <string name=\"app_restore\">Restaura les dades de la còpia de seguretat</string>\n  <string name=\"skip_each_segment_once\">No us torneu a saltar segments</string>\n  <string name=\"disable_ok_long_press\">Desactiva el botó D\\'acord prement llarg</string>\n  <string name=\"player_time_correction\">Mostra el temps respecte a la velocitat</string>\n  <string name=\"sponsor_color_markers\">Marcadors de color a la barra de progrés</string>\n  <string name=\"refresh_section\">Secció d\\'actualització</string>\n  <string name=\"option_disabled\">Inhabilitat</string>\n  <string name=\"live_now_row_name\">Viu ara</string>\n  <string name=\"run_in_background\">Reproducció de fons</string>\n  <string name=\"settings_remote_control\">Control remot</string>\n  <string name=\"background_service_started\">S\\'ha iniciat el servei de fons</string>\n  <string name=\"dialog_add_to\">Afegeix a %s</string>\n  <string name=\"dialog_remove_from\">Elimina de %s</string>\n  <string name=\"added_to\">S\\'ha afegit a %s</string>\n  <string name=\"removed_from\">S\\'ha eliminat de %s</string>\n  <string name=\"video_preset_adaptive\">Adaptatiu</string>\n  <string name=\"content_block_preview_recap\">Previsualització o resum del vídeo</string>\n  <string name=\"content_block_highlight\">Punt d\\'interès (marcador)</string>\n  <string name=\"removed_from_history\">El vídeo s\\'ha eliminat de l\\'historial</string>\n  <string name=\"remove_from_history\">Esborrar de l\\'historial</string>\n  <string name=\"upload_date\">Data de càrrega</string>\n  <string name=\"upload_date_any\">Tot el temps</string>\n  <string name=\"upload_date_today\">Avui</string>\n  <string name=\"upload_date_this_week\">Aquesta setmana</string>\n  <string name=\"upload_date_this_month\">Aquest mes</string>\n  <string name=\"upload_date_this_year\">Aquest any</string>\n  <string name=\"double_refresh_rate\">Doble taxa d\\'actualització</string>\n  <string name=\"upload_date_last_hour\">Última hora</string>\n  <string name=\"focus_on_search_results\">Enfocament automàtic en els resultats de la cerca</string>\n  <string name=\"context_menu\">Menú contextual</string>\n  <string name=\"add_remove_from_recent_playlist\">Afegeix/elimina de la llista de reproducció recent</string>\n  <string name=\"hide_settings_section\">Amaga la secció de configuració (perillós!)</string>\n  <string name=\"volume\">Volum %s %%</string>\n  <string name=\"auto_frame_rate_sec\">%s segons</string>\n  <string name=\"audio_shift_sec\">%s segons</string>\n  <string name=\"ui_hide_timeout_sec\">%s segons</string>\n  <string name=\"screen_dimming_timeout_min\">%s min</string>\n  <string name=\"mark_all_channels_watched\">Marca tots els canals com a vists</string>\n  <string name=\"move_section_up\">Mou la secció cap amunt</string>\n  <string name=\"move_section_down\">Mou la secció cap avall</string>\n  <string name=\"player_buttons\">Configura els botons del reproductor</string>\n  <string name=\"action_playlist_add\">Afegir a la llista de reproducció</string>\n  <string name=\"action_video_stats\">Estadístiques de vídeo</string>\n  <string name=\"action_subscribe\">Subscriu-te</string>\n  <string name=\"action_channel\">Obrir canal</string>\n  <string name=\"action_pip\">PIP</string>\n  <string name=\"action_video_info\">Descripció del vídeo</string>\n  <string name=\"action_screen_off\">Pantalla apagada</string>\n  <string name=\"action_playback_queue\">Cua de reproducció</string>\n  <string name=\"action_video_speed\">Velocitat del vídeo</string>\n  <string name=\"action_subtitles\">Subtítols</string>\n  <string name=\"action_like\">M\\'agrada</string>\n  <string name=\"action_dislike\">No m\\'agrada</string>\n  <string name=\"action_play_pause\">Reproduir/Pausa</string>\n  <string name=\"action_repeat_mode\">Mode de reproducció</string>\n  <string name=\"action_next\">Següent vídeo</string>\n  <string name=\"action_previous\">Vídeo anterior</string>\n  <string name=\"action_high_quality\">Qualitat de vídeo</string>\n  <string name=\"content_block_no_skipping_mode\">Mode sense saltar</string>\n  <string name=\"content_block_action_type\">Trieu l\\'acció</string>\n  <string name=\"content_block_action_none\">No fer res</string>\n  <string name=\"content_block_action_only_skip\">Només saltar</string>\n  <string name=\"content_block_action_toast\">Omet amb notificació</string>\n  <string name=\"content_block_action_dialog\">Mostra el diàleg de confirmació</string>\n  <string name=\"content_block_filler\">Fora de tema (emplenament)</string>\n  <string name=\"keyboard_auto_show\">Mostra el teclat automàticament</string>\n  <string name=\"cancel_dialog\">Cancel · lar</string>\n  <string name=\"msg_player_error_source2\">\tLa font del vídeo no funciona o l\\'hora és incorrecta</string>\n  <string name=\"hide_upcoming\">\tAmaga les properes subscripcions</string>\n  <string name=\"search_background_playback\">Introduïu PIP al canal de cerca/obert</string>\n  <string name=\"trending_row_name\">Tendència</string>\n  <string name=\"player_seek_type\">Busca comportament</string>\n  <string name=\"player_seek_regular\">\tRegular</string>\n  <string name=\"player_seek_confirmation_pause\">Amb confirmació (pausa mentre es cerca)</string>\n  <string name=\"player_seek_confirmation_play\">\tAmb confirmació (juga mentre busques)</string>\n  <string name=\"hide_shorts_from_home\">\tAmaga els pantalons curts de casa</string>\n  <string name=\"finish_on_disconnect\">Finalitzeu l\\'aplicació després de desconnectar el telèfon/tauleta</string>\n  <string name=\"update_error\">\tError d\\'actualització</string>\n  <string name=\"hide_shorts_from_history\">Amaga els curts de la història</string>\n  <string name=\"disable_screensaver\">Desactiva l\\'estalvi de pantalla</string>\n  <string name=\"description_not_found\">No s\\'ha trobat la descripció</string>\n  <string name=\"proxy_port_hint\">Port proxy</string>\n  <string name=\"proxy_host_hint\">Nom d\\'amfitrió o IP del servidor intermediari</string>\n  <string name=\"proxy_username_hint\">Nom d\\'usuari del proxy</string>\n  <string name=\"proxy_password_hint\">Contrasenya de proxy</string>\n  <string name=\"proxy_type\">Tipus de proxy</string>\n  <string name=\"enable_web_proxy\">Utilitzeu el servidor intermediari web</string>\n  <string name=\"proxy_not_supported\">Utilitzeu el servidor intermediari web (necessita Android 4.4+)</string>\n  <string name=\"proxy_test_btn\">Prova</string>\n  <string name=\"proxy_settings_title\">\tConfiguració del servidor intermediari</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com \\n https://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">Prova # %d : cancel·lada.</string>\n  <string name=\"proxy_type_invalid\">No s\\'ha definit el tipus de proxy.</string>\n  <string name=\"proxy_host_invalid\">L\\'amfitrió del servidor intermediari no s\\'ha definit.</string>\n  <string name=\"proxy_port_invalid\">Port proxy no vàlid, ha de ser &gt; 0.</string>\n  <string name=\"proxy_credentials_invalid\">\tNom d\\'usuari o contrasenya incorrectes.</string>\n  <string name=\"proxy_test_aborted\">La prova s\\'ha avortat; primer, arregleu la configuració del servidor intermediari.</string>\n  <string name=\"proxy_application_aborted\">Si us plau, corregiu primer la configuració del servidor intermediari.</string>\n  <string name=\"proxy_test_start\">Prova # %d : %s …</string>\n  <string name=\"proxy_test_error\">Error # %d : %s</string>\n  <string name=\"proxy_test_status\">Estat # %d : %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Adreça del fitxer de configuració (*.ovpn)</string>\n  <string name=\"enable_openvpn\">Utilitzeu OpenVPN</string>\n  <string name=\"openvpn_settings_title\">Configuració d\\'OpenVPN</string>\n  <string name=\"openvpn_application_aborted\">Si us plau, corregiu primer la configuració d\\'OpenVPN</string>\n  <string name=\"openvpn_test_aborted\">La prova s\\'ha avortat, primer arregleu la configuració d\\'OpenVPN.</string>\n  <string name=\"openvpn_address_invalid\">L\\'adreça de configuració d\\'OpenVPN no s\\'ha definit.</string>\n  <string name=\"internet_censorship\">Censura d\\'Internet</string>\n  <string name=\"rename_section\">Canvia el nom d\\'aquesta secció</string>\n  <string name=\"simple_edit_value_hint\">Nou valor</string>\n  <string name=\"seek_interval\">Cerca interval</string>\n  <string name=\"seek_interval_sec\">%s segons</string>\n  <string name=\"subtitle_system\">Estil del sistema (Configuració d\\'Android &gt; Accessibilitat)</string>\n  <string name=\"subtitle_scale\">Escala de subtítols</string>\n  <string name=\"audio_sync_fix_desc\">Una forma alternativa de sincronitzar àudio/vídeo. Es considera obsolet, però, en alguns casos, pot ajudar.</string>\n  <string name=\"audio_sync_fix\">Correcció de sincronització d\\'àudio</string>\n  <string name=\"ambilight_ratio_fix_desc\">Corregeix la il·luminació de biaix absent. Corregeix una relació d\\'aspecte incorrecta. Arregla les captures de pantalla en blanc. Afecta el rendiment!</string>\n  <string name=\"ambilight_ratio_fix\">Ambilight / Relació d\\'aspecte / Captures de pantalla solucionades</string>\n  <string name=\"force_legacy_codecs_desc\">Millora significativament el rendiment en dispositius de gamma baixa. La resolució màxima és de 720p.</string>\n  <string name=\"force_legacy_codecs\">Força còdecs heretats (720p)</string>\n  <string name=\"live_stream_fix_desc\">Millora significativament el rendiment de la reproducció en directe en dispositius de gamma baixa. La resolució màxima és de 1080p.</string>\n  <string name=\"live_stream_fix\">Correcció de la reproducció en directe (1080p)</string>\n  <string name=\"playback_notifications_fix_desc\">Amaga les notificacions de canvi de pista. Podria ser útil en firmwares basats en AOSP.</string>\n  <string name=\"playback_notifications_fix\">Desactiva les notificacions de reproducció</string>\n  <string name=\"amlogic_fix_desc\">\tCorrecció de caiguda de fotogrames en dispositius basats en Amlogic</string>\n  <string name=\"amlogic_fix\">Correcció d\\'Amlogic 1080p\\@60fps</string>\n  <string name=\"tunneled_video_playback_desc\">NOTA: és possible que la pausa no funcioni correctament! La reproducció de vídeo tunelitzada promet avantatges com ara una millor sincronització d\\'àudio/vídeo (sincronització AV) i una reproducció més suau. Es requereix Android 5+</string>\n  <string name=\"tunneled_video_playback\">Reproducció de vídeo tunelitzada (Android 5+)</string>\n  <string name=\"master_volume\">Volum mestre</string>\n  <string name=\"volume_limit\">\tLímit de volum</string>\n  <string name=\"play_video\">Reproduir</string>\n  <string name=\"remember_position_of_short_videos\">Recorda la posició dels vídeos curts (menys de 5 minuts)</string>\n  <string name=\"player_show_tooltips\">Mostra informació sobre eines de botons</string>\n  <string name=\"action_like_unset\">Igual no està configurat</string>\n  <string name=\"action_dislike_unset\">L\\'antipatia no està configurada</string>\n  <string name=\"various_buttons\">Botons a la part superior de la finestra principal</string>\n  <string name=\"not_compatible_with\">\tL\\'opció seleccionada no és compatible amb</string>\n  <string name=\"subtitle_position\">Desplaçament inferior dels subtítols</string>\n  <string name=\"pressing_home\">prement HOME</string>\n  <string name=\"pressing_home_back\">prement HOME o BACK</string>\n  <string name=\"player_number_key_seek\">Cerca amb tecles numèriques</string>\n  <string name=\"save_remove_playlist\">Afegeix/elimina una llista de reproducció de la secció Llistes de reproducció</string>\n  <string name=\"remove_playlist\">Suprimeix la llista de reproducció de la secció Llistes de reproducció permanentment</string>\n  <string name=\"removed_from_playlists\">S\\'ha eliminat de la secció Llistes de reproducció</string>\n  <string name=\"saved_to_playlists\">S\\'ha afegit a la secció Llistes de reproducció</string>\n  <string name=\"create_playlist\">Crea una llista de reproducció</string>\n  <string name=\"add_video_to_new_playlist\">Afegeix a la llista de reproducció nova</string>\n  <string name=\"cant_delete_empty_playlist\">No es pot suprimir la llista de reproducció buida</string>\n  <string name=\"playlist\">Llista de reproducció</string>\n  <string name=\"rename_playlist\">Canvia el nom de la llista de reproducció</string>\n  <string name=\"cant_rename_empty_playlist\">No es pot canviar el nom de la llista de reproducció buida</string>\n  <string name=\"cant_rename_foreign_playlist\">No es pot canviar el nom de la llista de reproducció estrangera</string>\n  <string name=\"cant_save_playlist\">Aquesta llista de reproducció no es pot afegir</string>\n  <string name=\"enter_value\">Introduïu qualsevol valor que no sigui buit</string>\n  <string name=\"dialog_add_remove_from\">Afegeix/Elimina de %s</string>\n  <string name=\"lb_playback_controls_skip_next\">Omet el següent</string>\n  <string name=\"lb_playback_controls_skip_previous\">Omet l\\'anterior/rebobina a la posició inicial</string>\n  <string name=\"alt_presets_behavior\">Comportament predefinits alternatius (ample de banda límit)</string>\n  <string name=\"alt_presets_behavior_desc\">L\\'aplicació intentarà mantenir l\\'amplada de banda corresponent a la configuració predeterminada seleccionada en lloc de fer coincidir la resolució, els fps i el còdec.</string>\n  <string name=\"sleep_timer\">Temporitzador de repòs (si el comandament no s\\'utilitza una hora)</string>\n  <string name=\"sleep_timer_desc\">Posa en pausa la reproducció si l\\'usuari no ha utilitzat el comandament durant una hora</string>\n  <string name=\"disable_vsync\">Desactiva l\\'ajustament a vsync</string>\n  <string name=\"disable_vsync_desc\">Millora una mica el rendiment de reproducció</string>\n  <string name=\"skip_codec_profile_check\">Omet la comprovació del nivell del perfil del còdec</string>\n  <string name=\"skip_codec_profile_check_desc\">No comproveu la compatibilitat amb el còdec quan inicieu un vídeo. Pot ser útil amb firmwares amb errors</string>\n  <string name=\"force_sw_codec\">Força el descodificador de vídeo SW</string>\n  <string name=\"force_sw_codec_desc\">Es pot reproduir gairebé qualsevol vídeo, però el rendiment és molt baix.</string>\n  <string name=\"playlist_order\">Ordena la llista de reproducció</string>\n  <string name=\"playlist_order_added_date_newer_first\">Data afegida (la més nova primer)</string>\n  <string name=\"playlist_order_added_date_older_first\">Data afegida (antiga primer)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Data de publicació (el més nou primer)</string>\n  <string name=\"playlist_order_published_date_older_first\">Data de publicació (antiga primer)</string>\n  <string name=\"playlist_order_popularity\">Popularitat</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">No es pot fer això per a la llista de reproducció estrangera</string>\n  <string name=\"owned_playlist_warning\">Error. Aquesta acció només es pot aplicar a les llistes de reproducció pròpies</string>\n  <string name=\"content_block_alt_server\">Utilitzeu un servidor alternatiu</string>\n  <string name=\"content_block_alt_server_desc\">Activeu aquesta opció si SponsorBlock es nega a treballar per diferents motius.</string>\n  <string name=\"unset_stream_reminder\">Desactiva el recordatori de la reproducció</string>\n  <string name=\"set_stream_reminder\">Estableix un recordatori de reproducció</string>\n  <string name=\"playback_starts_shortly\">La reproducció s\\'iniciarà automàticament quan la reproducció estigui a punt</string>\n  <string name=\"starting_stream\">S\\'està iniciant la reproducció...</string>\n  <string name=\"action_playlist_remove\">Elimina de la llista de reproducció</string>\n  <string name=\"signin_view_title\">S\\'està carregant el codi d\\'usuari...</string>\n  <string name=\"signin_view_description\">Per iniciar la sessió, introduïu aquest codi a la pàgina %s \\n NOTA. Només funciona amb els navegadors Firefox o Chrome.</string>\n  <string name=\"signin_view_action_text\">Fet</string>\n  <string name=\"require_checked\">Requereix \\\" %s</string>\n  <string name=\"msg_press_again_to_exit\">\tPremeu de nou per sortir</string>\n  <string name=\"player_remaining_time\">Restant: %s</string>\n  <string name=\"player_ending_time\">Acaba a les %s</string>\n  <string name=\"badge_new_content\">NOU CONTINGUT</string>\n  <string name=\"badge_live\">EN VIU</string>\n  <string name=\"add_device_view_description\">Per enllaçar el dispositiu, introdueix aquest codi a l\\'aplicació YouTube del teu telèfon a la secció Configuració/Mira a la televisió \\n NOTA. Només funciona amb el teclat Gboard.</string>\n  <string name=\"playback_controls_repeat_pause\">Repetiu la pausa</string>\n  <string name=\"playback_controls_repeat_list\">Llista de repetició</string>\n  <string name=\"action_subscribe_off\">Subscripció desactivada</string>\n  <string name=\"action_subscribe_on\">Subscriu-te activat</string>\n  <string name=\"skip_24_rate\">Omet els formats de 24 fps (correcció del mode cinema real\\?)</string>\n  <string name=\"player_disable_suggestions\">Desactiva els suggeriments</string>\n  <string name=\"feedback\">Comentaris</string>\n  <string name=\"sources\">Fonts</string>\n  <string name=\"releases\">Alliberaments</string>\n  <string name=\"prefer_avc_over_vp9\">Selecció de còdec: preferiu avc sobre vp9</string>\n  <string name=\"open_chat\">Xat/Comentaris</string>\n  <string name=\"place_chat_left\">Col·loca el xat a l\\'esquerra</string>\n  <string name=\"use_alt_speech_recognizer\">Reconeixedor de veu alternatiu</string>\n  <string name=\"time_format\">Format horari</string>\n  <string name=\"time_format_24\">24 hores</string>\n  <string name=\"time_format_12\">12 hores (AM/PM)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Enganxat seriosament. Utilitzeu-lo només si teniu problemes amb el reconeixement predeterminat.\t</string>\n  <string name=\"speech_recognizer\">Reconeixedor de veu</string>\n  <string name=\"speech_recognizer_system\">Sistema (preferit)</string>\n  <string name=\"speech_recognizer_external_1\">Extern 1 (s\\'ha produït un error greu, per tal de funcionar cal desactivar l\\'accés al micròfon)</string>\n  <string name=\"speech_recognizer_external_2\">Extern 2 (error greu)</string>\n  <string name=\"real_channel_icon\">Mostra la icona al botó del canal</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Groc sobre fons semitransparent</string>\n  <string name=\"subtitle_yellow_black\">Groc sobre fons negre</string>\n  <string name=\"player_pixel_ratio\">\tRelació de píxels</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Gris fosc (monocrom)</string>\n  <string name=\"disable_mic_permission\">Si us plau, desactiveu l\\'accés al micròfon per a l\\'aplicació perquè aquest reconeixement funcioni correctament.</string>\n  <string name=\"repeat_mode_shuffle\">Barreja qualsevol llista de reproducció</string>\n  <string name=\"chat_left\">Esquerra</string>\n  <string name=\"chat_right\">Dret</string>\n  <string name=\"card_real_thumbnails\">Substituïu les miniatures per un fotograma del vídeo</string>\n  <string name=\"card_content\">On agafar les miniatures de les targetes</string>\n  <string name=\"thumb_quality_default\">Per defecte</string>\n  <string name=\"thumb_quality_start\">Inici del vídeo</string>\n  <string name=\"thumb_quality_middle\">A la meitat del vídeo</string>\n  <string name=\"thumb_quality_end\">Final del vídeo</string>\n  <string name=\"content_block_status\">Comproveu l\\'estat de SponsorBlock</string>\n  <string name=\"player_show_quality_info_bitrate\">Afegiu la taxa de bits a la informació de qualitat</string>\n  <string name=\"player_speed_button_old_behavior\">Reverteix el comportament antic del botó de velocitat</string>\n  <string name=\"protect_settings_with_password\">Protegiu tots els paràmetres amb una contrasenya</string>\n  <string name=\"enter_settings_password\">Introduïu la contrasenya de configuració</string>\n  <string name=\"child_mode\">Mode infantil</string>\n  <string name=\"child_mode_desc\">En aquest mode, l\\'usuari no pot utilitzar la cerca ni veure cap contingut suggerit. La configuració estarà sota la contrasenya.</string>\n  <string name=\"lost_setting_warning\">La configuració de l\\'aplicació es canviarà. Assegureu-vos que heu creat una còpia de seguretat de la configuració.</string>\n  <string name=\"player_button_long_click\">Un clic breu al botó per a una acció ràpida i un diàleg llarg per a la configuració</string>\n  <string name=\"pause_history\">Posa en pausa l\\'historial</string>\n  <string name=\"resume_history\">Reprendre l\\'historial</string>\n  <string name=\"clear_history\">Història clara</string>\n  <string name=\"chapters\">Capítols</string>\n  <string name=\"card_multiline_subtitle\">Subtítols multilínia</string>\n  <string name=\"auto_frame_rate_modes\">Modes compatibles</string>\n  <string name=\"loading\">S\\'està carregant…</string>\n  <string name=\"open_comments\">Obrir comentaris</string>\n  <string name=\"place_comments_left\">Col·loca el comentaris a l\\'esquerra</string>\n  <string name=\"search_exit_shortcut\">Sortiu de l\\'cerca</string>\n  <string name=\"player_exit_shortcut\">Sortiu de l\\'reproductor</string>\n  <string name=\"pressing_back\">prement BACK</string>\n  <string name=\"dearrow_status\">Comproveu l\\'estat de DeArrow</string>\n  <string name=\"suggestions\">Suggeriments</string>\n  <string name=\"player_volume\">Volum</string>\n  <string name=\"live_stream_fix_4k\">Correcció de la reproducció en directe (4K)</string>\n  <string name=\"header_trending\">Tendència</string>\n  <string name=\"hide_shorts_from_trending\">Amaga els curts de la tendència</string>\n  <string name=\"disable_history\">Desactiva l\\'historial</string>\n  <string name=\"enable_history\">Activeu l\\'historial</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-cs/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <string name=\"header_home\">Domů</string>\r\n    <string name=\"title_search\">Vyhledávání</string>\r\n    <string name=\"header_subscriptions\">Odběry</string>\r\n    <string name=\"header_history\">Historie</string>\r\n    <string name=\"header_music\">Hudba</string>\r\n    <string name=\"header_news\">Zprávy</string>\r\n    <string name=\"header_gaming\">Hraní</string>\r\n    <string name=\"header_playlists\">Playlisty</string>\r\n    <string name=\"header_settings\">Nastavení</string>\r\n    <string name=\"header_channels\">Kanály</string>\r\n    <string name=\"subscriptions_signin_title\">Podívejte se na nejnovější z vašich oblíbených kanálů</string>\r\n    <string name=\"subscriptions_signin_subtitle\">Přihlaste se a podívejte se na své odběry</string>\r\n    <string name=\"subscriptions_signin_button_text\">PŘIHLÁSIT SE</string>\r\n    <string name=\"library_signin_to_show_more\">Sledujte videa, která se vám líbila, uložila nebo odebíráte</string>\r\n    <string name=\"library_signin_subtitle\">Chcete-li zobrazit svou knihovnu, přihlaste se</string>\r\n    <string name=\"action_signin\">PŘIHLÁSIT SE</string>\r\n    <string name=\"title_video_formats\">Video formáty</string>\r\n    <string name=\"title_audio_formats\">Zvukové formáty</string>\r\n    <string name=\"subtitle_category_title\">Titulky</string>\r\n    <string name=\"playback_queue_category_title\">Fronta přehrávání</string>\r\n    <string name=\"video_max_quality\">Auto (maximální kvalita)</string>\r\n    <string name=\"audio_max_quality\">Auto (maximální kvalita)</string>\r\n    <string name=\"subtitles_disabled\">Zakázáno</string>\r\n    <string name=\"auto_frame_rate\">Automatická snímková frekvence</string>\r\n    <string name=\"frame_rate_correction\">Opravit fps: \\n%s</string>\r\n    <string name=\"category_background_playback\">Přehrávání na pozadí</string>\r\n    <string name=\"not_implemented\">Není implementováno</string>\r\n    <string name=\"not_supported_by_device\">Zařízení nepodporuje tuto funkci</string>\r\n    <string name=\"option_background_playback_off\">Zakázáno</string>\r\n    <string name=\"option_background_playback_pip\">Video a zvuk</string>\r\n    <string name=\"option_background_playback_only_audio\">Pouze zvuk</string>\r\n    <string name=\"playback_settings\">Nastavení kvality přehrávání</string>\r\n    <string name=\"video_speed\">Rychlost videa</string>\r\n    <string name=\"resolution_switch\">Přepnout rozlišení</string>\r\n    <string name=\"video_buffer\">Vyrovnávací paměť videa</string>\r\n    <string name=\"video_buffer_size_none\">Žádná</string>\r\n    <string name=\"video_buffer_size_lowest\">Nejnižší</string>\r\n    <string name=\"video_buffer_size_low\">Nízká</string>\r\n    <string name=\"video_buffer_size_med\">Střední</string>\r\n    <string name=\"video_buffer_size_high\">Vysoká</string>\r\n    <string name=\"video_buffer_size_highest\">Nejvyšší</string>\r\n    <string name=\"update_changelog\">Seznam změn</string>\r\n    <string name=\"install_update\">Instalovat aktualizace</string>\r\n    <string name=\"section_is_empty\">Tady nic není</string>\r\n    <string name=\"dialog_add_to_playlist\">Přidat do playlistu</string>\r\n    <string name=\"msg_signed_users_only\">Pouze přihlášení uživatelé</string>\r\n    <string name=\"msg_cant_load_content\">Jejda. Nelze načíst obsah.\\n1) Zapněte historii prohlížení.\\n2) Zkontrolujte čas a datum zařízení.\\n3) Zkontrolujte připojení k síti.\\n4) Zkontrolujte nastavení proxy.\\n5) Zkuste se přihlásit do svého účtu.\\n6) Možná tu nic není.</string>   \r\n    <string name=\"title_video_presets\">Video předvolby</string>\r\n    <string name=\"settings_accounts\">Účty</string>\r\n    <string name=\"settings_left_panel\">Upravit kategorie</string>\r\n    <string name=\"settings_themes\">Motivy</string>\r\n    <string name=\"settings_other\">Jiné</string>\r\n    <string name=\"settings_player\">Přehrávač</string>\r\n    <string name=\"settings_language\">Jazyk</string>\r\n    <string name=\"settings_linked_devices\">Propojená zařízení</string>\r\n    <string name=\"settings_about\">Informace</string>\r\n    <string name=\"dialog_account_list\">Vybrat účet</string>\r\n    <string name=\"dialog_account_none\">Žádný</string>\r\n    <string name=\"dialog_remove_account\">Odstranit účet</string>\r\n    <string name=\"dialog_add_account\">Přidat účet</string>\r\n    <string name=\"default_lang\">Výchozí</string>\r\n    <string name=\"original_lang\">Originál</string>\r\n    <string name=\"dialog_select_language\">Jazyk aplikace</string>\r\n    <string name=\"subtitle_default\">Výchozí</string>\r\n    <string name=\"subtitle_white_semi_transparent\">Poloprůhledné pozadí</string>\r\n    <string name=\"subtitle_style\">Styl titulek</string>\r\n    <string name=\"subtitle_language\">Jazyk titulek</string>\r\n    <string name=\"action_search\">Hledat</string>\r\n    <string name=\"settings_main_ui\">Uživatelské rozhraní</string>\r\n    <string name=\"dialog_main_ui\">Uživatelské rozhraní</string>\r\n    <string name=\"card_animated_previews\">Animované náhledy</string>\r\n    <string name=\"web_site\">Webová stránka</string>\r\n    <string name=\"donation\">Darování</string>\r\n    <string name=\"dialog_about\">Informace</string>\r\n    <string name=\"dialog_player_ui\">Video přehrávač</string>\r\n    <string name=\"player_show_ui_on_pause\">Ukaž ovládání během pauzy</string>\r\n    <string name=\"player_pause_on_ok\">OK tlačítko pozastaví přehrávání</string>\r\n    <string name=\"player_ok_button_behavior\">Chování OK tlačítka</string>\r\n    <string name=\"player_only_ui\">Pouze ovládání</string>\r\n    <string name=\"player_ui_and_pause\">Ovládání a pozastavení</string>\r\n    <string name=\"player_only_pause\">Pouze pozastavení</string>\r\n    <string name=\"player_toggle_speed\">Zapnout/Vypnout rychlost</string>\r\n    <string name=\"check_for_updates\">Zkontrolovat aktualizace</string>\r\n    <string name=\"update_not_found\">Používáte poslední verzi</string>\r\n    <string name=\"update_in_progress\">Čekejte…</string>\r\n    <string name=\"player_ui_hide_behavior\">Automatické skrytí ovládání</string>\r\n    <string name=\"option_never\">Nikdy</string>\r\n    <string name=\"side_panel_sections\">Nastavení sekcí</string>\r\n    <string name=\"boot_to_section\">Spusť se do sekce</string>\r\n    <string name=\"large_ui\">Velké rozhraní</string>\r\n    <string name=\"video_grid_scale\">Měřítko video mřížky</string>\r\n    <string name=\"scale_ui\">Měřítko rozhraní</string>\r\n    <string name=\"color_scheme\">Barevné schéma</string>\r\n    <string name=\"color_scheme_default\">Výchozí</string>\r\n    <string name=\"color_scheme_red_grey\">Červeno-šedé</string>\r\n    <string name=\"color_scheme_red\">Červené</string>\r\n    <string name=\"color_scheme_dark_grey\">Tmavé</string>\r\n    <string name=\"disable_update_check\">Zakázat kontrolu aktualizací</string>\r\n    <string name=\"show_again\">Ukaž znovu</string>\r\n    <string name=\"check_updates_auto\">Upozorňovat na aktualizace</string>\r\n    <string name=\"select_account_on_boot\">Zobrazit dialog pro výběr účtu při spuštění aplikace</string>\r\n    <string name=\"player_other\">Různé</string>\r\n    <string name=\"player_full_date\">Přesné datum v popisku</string>\r\n    <string name=\"open_channel\">Otevřít kanál</string>\r\n    <string name=\"open_playlist\">Otevřít playlist</string>\r\n    <string name=\"not_interested\">Nemám zájem</string>\r\n    <string name=\"not_recommend_channel\">Nedoporučovat kanál</string>\r\n    <string name=\"you_wont_see_this_video\">Video bylo odebráno z doporučení</string>\r\n    <string name=\"you_wont_see_this_channel\">Tento kanál neuvidíte v doporučeních</string>\r\n    <string name=\"settings_search\">Hledat</string>\r\n    <string name=\"dialog_search\">Hledat</string>\r\n    <string name=\"instant_voice_search\">Okamžité hlasové vyhledávání</string>\r\n    <string name=\"option_background_playback_behind\">Přehraj poté</string>\r\n    <string name=\"next_video_info_is_not_loaded_yet\">Informace o dalším videu ještě nejsou načteny</string>\r\n    <string name=\"card_multiline_title\">Víceřádkové názvy</string>\r\n    <string name=\"cards_style\">Styl karet</string>\r\n    <string name=\"repeat_mode_all\">Přehraj videa nepřetržitě</string>\r\n    <string name=\"repeat_mode_one\">Opakovat aktuální video</string>\r\n    <string name=\"repeat_mode_pause\">Pozastavit přehrávání po každém videu (kromě fronty přehrávání)</string>\r\n    <string name=\"repeat_mode_pause_alt\">Hraj videa nepřetržitě pouze pro playlist</string>    \r\n    <string name=\"repeat_mode_none\">Ukončit přehrávání po jednom videu (kromě fronty přehrávání)</string>\r\n    <string name=\"subscribed_to_channel\">Přihlášeno k odběru</string>\r\n    <string name=\"unsubscribed_from_channel\">Odhlášeno z odběru</string>\r\n    <string name=\"subtitle_yellow_transparent\">Žlutá</string>\r\n    <string name=\"subtitle_white_transparent\">Bílá na průhledném pozadí</string>\r\n    <string name=\"subtitle_white_black\">Černé pozadí</string>\r\n    <string name=\"player_seek_preview\">Náhledy při přeskakování</string>\r\n    <string name=\"color_scheme_dark_grey_oled\">Tmavé (OLED)</string>\r\n    <string name=\"channels_section_sorting\">Řazení sekce kanálu</string>\r\n    <string name=\"sorting_by_new_content\">Nový obsah</string>\r\n    <string name=\"sorting_alphabetically\">Abecedně</string>\r\n    <string name=\"sorting_last_viewed\">Poslední zhlédnuté</string>\r\n    <string name=\"player_pause_when_seek\">Pozastav video při přeskakování</string>\r\n    <string name=\"playlists_style\">Styl playlistů</string>\r\n    <string name=\"playlists_style_grid\">Mřížka</string>\r\n    <string name=\"playlists_style_rows\">Řady</string>\r\n    <string name=\"player_show_clock\">Zobraz hodiny na ovládacím panelu</string>\r\n    <string name=\"player_show_remaining_time\">Ukaž zbývající čas na ovládacím panelu</string>\r\n    <string name=\"player_show_ending_time\">Ukaž čas ukončení na ovládacím panelu</string>\r\n    <string name=\"open_channel_uploads\">Otevřít videa kanálu</string>\r\n    <string name=\"app_exit_shortcut\">Ukončit aplikaci</string>\r\n    <string name=\"player_exit_shortcut\">Ukončit z přehrávače</string>\r\n    <string name=\"search_exit_shortcut\">Ukončit z vyhledávání</string>\r\n    <string name=\"app_exit_none\">Neukončovat</string>\r\n    <string name=\"app_double_back_exit\">2x tlačítko Zpět</string>\r\n    <string name=\"app_single_back_exit\">1x tlačítko Zpět</string>\r\n    <string name=\"action_video_zoom\">Video zoom</string>\r\n    <string name=\"video_zoom\">Video zoom</string>\r\n    <string name=\"video_zoom_default\">Výchozí</string>\r\n    <string name=\"video_zoom_fit_width\">Přizpůsobit šířku</string>\r\n    <string name=\"video_zoom_fit_height\">Přizpůsobit výšku</string>\r\n    <string name=\"video_zoom_fit_both\">Přizpůsobit šířku nebo výšku</string>\r\n    <string name=\"video_zoom_stretch\">Roztáhnout</string>\r\n    <string name=\"color_scheme_teal\">Šedozelená</string>\r\n    <string name=\"color_scheme_teal_oled\">Šedozelená (OLED)</string>\r\n    <string name=\"player_seek_preview_none\">Vypnuto</string>\r\n    <string name=\"player_seek_preview_single\">Jeden snímek</string>\r\n    <string name=\"player_seek_preview_carousel\">Několik snímků</string>\r\n    <string name=\"player_seek_preview_carousel_slow\">Několik snímků (pomalu, po snímcích)</string>\r\n    <string name=\"player_seek_preview_carousel_fast\">Několik snímků (rychle, náhled je nepřesný)</string>    \r\n    <string name=\"unsubscribe_from_channel\">Odhlásit z odběru kanálu</string>\r\n    <string name=\"card_title_lines_num\">Počet řádků v názvu videa</string>\r\n    <string name=\"card_auto_scrolled_title\">Auto-posouvání uříznutého názvu videa</string>\r\n    <string name=\"share_link\">Sdílet odkaz</string>\r\n    <string name=\"share_qr_link\">Sdílet odkaz (QR kód)</string>\r\n    <string name=\"subscribe_to_channel\">Přihlásit se k odběru kanálu</string>\r\n    <string name=\"wait_data_loading\">Prosím počkejte, než se načtou data…</string>\r\n    <string name=\"auto_frame_rate_pause\">Automatická snímková frekvence (pozastav při přepnutí)</string>\r\n    <string name=\"auto_frame_rate_applying\">Aplikování automatické snímkové frekvence %sx%s\\@%s</string>\r\n    <string name=\"audio_shift\">Posunutí zvuku</string>\r\n    <string name=\"player_remember_speed\">Pamatovat rychlost videa</string>\r\n    <string name=\"mark_channel_as_watched\">Označit jako zhlédnuté</string>\r\n    <string name=\"channel_marked_as_watched\">Kanál byl označen jako zhlédnutý</string>\r\n    <string name=\"dialog_add_device\">Přidat zařízení</string>\r\n    <string name=\"dialog_remove_all_devices\">Odebrat zařízení</string>\r\n    <string name=\"device_link_enabled\">Spojení povoleno</string>\r\n    <string name=\"device_connected\">Zařízení \\\"%s\\\" bylo připojeno</string>\r\n    <string name=\"device_disconnected\">Zařízení \\\"%s\\\" bylo odpojeno</string>\r\n    <string name=\"settings_ui_scale\">Velikost rozhraní</string>\r\n    <string name=\"msg_restart_app\">Pro aplikování nastavení prosím restartujte aplikaci</string>\r\n    <string name=\"settings_block\">Blokování obsahu</string>\r\n    <string name=\"msg_applying\">Aplikuji %s…</string>\r\n    <string name=\"msg_done\">Hotovo</string>\r\n    <string name=\"settings_general\">Obecné</string>\r\n    <string name=\"settings_video\">Video</string>\r\n    <string name=\"content_block_confirm_skip\">Potvrzovat přeskočení</string>\r\n    <string name=\"content_block_categories\">Kategorie</string>\r\n    <string name=\"content_block_sponsor\">Sponzor</string>\r\n    <string name=\"content_block_intro\">Přestávka/Úvodní animace</string>\r\n    <string name=\"content_block_outro\">Koncové karty/Titulky</string>\r\n    <string name=\"content_block_interaction\">Připomenutí interakce (odběr)</string>\r\n    <string name=\"content_block_self_promo\">Neplacená/vlastní propagace</string>\r\n    <string name=\"content_block_music_off_topic\">Hudba - nehudební sekce klipu</string>\r\n    <string name=\"confirm_segment_skip\">Přeskočit segment \\\"%s\\\"\\?</string>\r\n    <string name=\"cancel_segment_skip\">Zrušit přeskočení segmentu</string>\r\n    <string name=\"msg_skipping_segment\">Přeskakuji segment \\\"%s\\\"…</string>\r\n    <string name=\"content_block_notification_type\">Typ oznámení</string>\r\n    <string name=\"content_block_notify_none\">Bez oznámení</string>\r\n    <string name=\"content_block_notify_toast\">Toast dialog</string>\r\n    <string name=\"content_block_notify_dialog\">Potvrzovací dialog</string>\r\n    <string name=\"return_to_launcher\">Vrátit se do launcheru z ATV kanálu/hledání</string>\r\n    <string name=\"intent_force_close\">Vynutit ukončení, pokud bylo video otevřeno z externího zdroje</string>\r\n    <string name=\"btn_confirm\">Potvrdit</string>\r\n    <string name=\"player_low_video_quality\">Nízká video kvalita</string>\r\n    <string name=\"remote_session_closed\">Vzdálená relace byla uzavřena</string>\r\n    <string name=\"msg_mode_switch_error\">Nelze přepnout režim zobrazení na \\\"%s\\\"</string>\r\n    <string name=\"msg_player_error\">Došlo k chybě přehrávače %s. Restartuji přehrávání…</string>\r\n    <string name=\"video_preset_disabled\">Bez video předvolby</string>\r\n    <string name=\"video_preset_enabled\">Formát dalšího videa bude nastaven podle video předvolby</string>\r\n    <string name=\"player_sleep_timer\">Časovač vypnutí</string>\r\n    <string name=\"player_show_quality_info\">Zobrazit informace o kvalitě na ovládacím panelu</string>\r\n    <string name=\"player_remember_each_speed\">Pamatovat si rychlost každého videa</string>\r\n    <string name=\"player_remember_speed_none\">Nepamatovat si žádné</string>\r\n    <string name=\"player_remember_speed_all\">Stejná rychlost pro všechna videa</string>\r\n    <string name=\"player_remember_speed_each\">Každé video má svou vlastní rychlost</string>\r\n    <string name=\"msg_player_error_source\">Nelze načíst video</string>\r\n    <string name=\"msg_player_error_renderer\">Vybraný formát není podporován.\\nZkuste vybrat jiný.\\nPokud to nepomůže, zkuste restart zařízení.</string>\r\n    <string name=\"msg_player_error_video_renderer\">Vybraný VIDEO formát není podporován.\\nZkuste vybrat jiný kliknutím na tlačítko HQ v přehrávači.\\nPokud to nepomůže, restartujte zařízení.</string>\r\n    <string name=\"msg_player_error_audio_renderer\">Vybraný AUDIO formát není podporován.\\nZkuste vybrat jiný kliknutím na tlačítko HQ v přehrávači.\\nPokud to nepomůže, restartujte zařízení.</string>\r\n    <string name=\"msg_player_error_subtitle_renderer\">Vybraný formát TITULKŮ není podporován.\\nZkuste vybrat jiné kliknutím na tlačítko CC v přehrávači.\\nPokud to nepomůže, restartujte zařízení.</string>\r\n    <string name=\"msg_player_error_unexpected\">Neznámá chyba video dekodéru</string>\r\n    <string name=\"video_aspect\">Poměr stran</string>\r\n    <string name=\"player_tweaks\">Vývojářské možnosti</string>\r\n    <string name=\"header_uploads\">Nahraná videa</string>\r\n    <string name=\"player_show_global_clock\">Vždy zobrazit hodiny v horní části videa</string>\r\n    <string name=\"player_show_global_ending_time\">Vždy zobrazit čas ukončení na obrazovce</string>\r\n    <string name=\"app_corner_clock\">Hlavní stránka: hodiny v pravém horním rohu</string>\r\n    <string name=\"player_corner_clock\">Přehrávač: hodiny v pravém horním rohu</string>\r\n    <string name=\"player_corner_ending_time\">Přehrávač: čas ukončení v pravém horním rohu</string>\r\n    <string name=\"uploads_old_look\">Vrátit zpět starý vzhled sekce Nahraná videa</string>\r\n    <string name=\"background_playback_activation\">Přehrávání na pozadí (aktivace)</string>\r\n    <string name=\"channels_old_look\">Starý vzhled sekce Kanály</string>\r\n    <string name=\"channels_auto_load\">Automaticky načíst obsah sekce Kanály</string>\r\n    <string name=\"return_to_background_video\">Zpět na video běžící na pozadí</string>\r\n    <string name=\"pin_unpin_from_sidebar\">Přidat/Odebrat k postrannímu panelu</string>\r\n    <string name=\"pin_unpin_playlist\">Přidat/Odebrat playlist k postrannímu panelu</string>\r\n    <string name=\"pin_unpin_channel\">Přidat/Odebrat kanál k postrannímu panelu</string>\r\n    <string name=\"pin_playlist\">Přidat playlist na postranní panel</string>\r\n    <string name=\"pin_channel\">Přidat kanál na postranní panel</string>\r\n    <string name=\"pinned_to_sidebar\">Přidáno na postranní panel</string>\r\n    <string name=\"unpin_from_sidebar\">Odebrat z postranního panelu</string>\r\n    <string name=\"unpin_group_from_sidebar\">Odebrat skupinu odběrů</string>\r\n    <string name=\"unpinned_from_sidebar\">Odebráno z postranního panelu</string>\r\n    <string name=\"dialog_select_country\">Země</string>\r\n    <string name=\"settings_language_country\">Jazyk/Země</string>\r\n    <string name=\"share_embed_link\">Sdílet vložený odkaz</string>\r\n    <string name=\"card_text_scroll_factor\">Rychlost posouvání textu karty</string>\r\n    <string name=\"preferred_update_source\">Preferovaný zdroj aktualizace</string>\r\n    <string name=\"hide_shorts\">Skrýt Shorts z Odběrů</string>\r\n    <string name=\"hide_shorts_channel\">Skrýt shorts z kanálu</string>\r\n    <string name=\"key_remapping\">Přenastavení tlačítek</string>\r\n    <string name=\"screen_dimming\">Ztmavení obrazovky</string>\r\n    <string name=\"removed_from_playback_queue\">Smazáno z fronty přehrávání</string>\r\n    <string name=\"added_to_playback_queue\">Přidáno do fronty přehrávání</string>\r\n    <string name=\"add_remove_from_playback_queue\">Přidat do/Smazat z fronty přehrávání</string>\r\n    <string name=\"add_to_playback_queue\">Přidat do fronty přehrávání</string>\r\n    <string name=\"remove_from_playback_queue\">Odebrat z fronty přehrávání</string>\r\n    <string name=\"proxy_enabled\">Povoleno proxy</string>\r\n    <string name=\"proxy_disabled\">Zakázáno proxy</string>\r\n    <string name=\"uploads_row_name\">Nahraná videa</string>\r\n    <string name=\"playlists_row_name\">Vytvořené playlisty</string>\r\n    <string name=\"popular_uploads_row_name\">Populární nahraná videa</string>\r\n    <string name=\"news_row_name\">Zprávy</string>\r\n    <string name=\"breaking_news_row_name\">Nejnovější zprávy</string>\r\n    <string name=\"covid_news_row_name\">COVID-19 zprávy</string>\r\n    <string name=\"enable_voice_search\">Povolit globální vyhledávání (nutná podpora firmwaru)</string>\r\n    <string name=\"subscribe_unsubscribe_from_channel\">Přihlásit/Odhlásit k odběru kanálu</string>\r\n    <string name=\"app_backup_restore\">Záloha/Obnovení</string>\r\n    <string name=\"app_backup\">Zálohovat data aplikace</string>\r\n    <string name=\"app_restore\">Obnovit data aplikace</string>\r\n    <string name=\"skip_each_segment_once\">Znovu nepřeskakovat segmenty</string>\r\n    <string name=\"disable_ok_long_press\">Deaktivovat dlouhé stisknutí tlačítka OK</string>\r\n    <string name=\"disable_ok_long_press_desc\">Určeno pro zabugované ovladače, kde tlačítko OK nefunguje správně</string>\r\n    <string name=\"player_time_correction\">Zobrazit čas s korekcí rychlosti</string>\r\n    <string name=\"sponsor_color_markers\">Barevné značky na ukazateli průběhu</string>\r\n    <string name=\"refresh_section\">Aktualizovat sekci</string>\r\n    <string name=\"option_disabled\">Zakázáno</string>\r\n    <string name=\"live_now_row_name\">Nyní živě</string>\r\n    <string name=\"run_in_background\">Přehrávání na pozadí</string>\r\n    <string name=\"settings_remote_control\">Dálkové ovládání</string>\r\n    <string name=\"background_service_started\">Služba na pozadí spuštěna</string>\r\n    <string name=\"dialog_add_to\">Přidat do %s</string>\r\n    <string name=\"dialog_remove_from\">Odebrat z %s</string>\r\n    <string name=\"added_to\">Přidáno do %s</string>\r\n    <string name=\"removed_from\">Odebráno z %s</string>\r\n    <string name=\"video_preset_adaptive\">Adaptivní</string>\r\n    <string name=\"content_block_preview_recap\">Náhled nebo shrnutí videa</string>\r\n    <string name=\"content_block_highlight\">Zajímavá pointa (záložka)</string>\r\n    <string name=\"removed_from_history\">Video bylo smazáno z historie</string>\r\n    <string name=\"remove_from_history\">Smazat z historie</string>\r\n    <string name=\"upload_date\">Datum nahrání</string>\r\n    <string name=\"upload_date_any\">Za celou dobu</string>\r\n    <string name=\"upload_date_today\">Dnes</string>\r\n    <string name=\"upload_date_this_week\">Tento týden</string>\r\n    <string name=\"upload_date_this_month\">Tento měsíc</string>\r\n    <string name=\"upload_date_this_year\">Tento rok</string>\r\n    <string name=\"double_refresh_rate\">Dvojitá obnovovací frekvence</string>\r\n    <string name=\"upload_date_last_hour\">Za poslední hodinu</string>\r\n    <string name=\"focus_on_search_results\">Automatické zaměření na výsledek vyhledávání</string>\r\n    <string name=\"context_menu\">Kontextová nabídka</string>\r\n    <string name=\"context_menu_sorting\">Řazení kontextové nabídky</string>\r\n    <string name=\"add_remove_from_recent_playlist\">Přidat/odebrat z nedávného playlistu</string>\r\n    <string name=\"hide_settings_section\">Skrýt sekci Nastavení (nebezpečné!)</string>\r\n    <string name=\"volume\">Hlasitost %s%%</string>\r\n    <string name=\"auto_frame_rate_sec\">%s sekund</string>\r\n    <string name=\"audio_shift_sec\">%s sekund</string>\r\n    <string name=\"ui_hide_timeout_sec\">%s sekund</string>\r\n    <string name=\"screen_dimming_timeout_min\">%s minut</string>\r\n    <string name=\"mark_all_channels_watched\">Označit všechny kanály jako zhlédnuté</string>\r\n    <string name=\"move_section_up\">Posuň sekci nahoru</string>\r\n    <string name=\"move_section_down\">Posuň sekci dolů</string>\r\n    <string name=\"player_buttons\">Nastavení tlačítek přehrávače</string>\r\n    <string name=\"action_playlist_add\">Přidat do playlistu</string>\r\n    <string name=\"action_video_stats\">Video statistiky</string>\r\n    <string name=\"action_subscribe\">Odebírat</string>\r\n    <string name=\"action_channel\">Otevřít kanál</string>\r\n    <string name=\"action_pip\">Obraz v obraze</string>\r\n    <string name=\"action_video_info\">Popis videa</string>\r\n    <string name=\"action_screen_off\">Vypnout obrazovku</string>\r\n    <string name=\"action_sound_off\">Ztlumit</string>\r\n    <string name=\"action_playback_queue\">Fronta přehrávání</string>\r\n    <string name=\"action_video_speed\">Rychlost videa</string>\r\n    <string name=\"action_subtitles\">Titulky</string>\r\n    <string name=\"action_like\">Líbí se</string>\r\n    <string name=\"action_dislike\">Nelíbí se</string>\r\n    <string name=\"action_play_pause\">Přehrát/Pozastavit</string>\r\n    <string name=\"action_repeat_mode\">Režim přehrávání</string>\r\n    <string name=\"action_next\">Další video</string>\r\n    <string name=\"action_previous\">Předešlé video</string>\r\n    <string name=\"action_high_quality\">Kvalita videa</string>\r\n    <string name=\"content_block_no_skipping_mode\">Režim bez přeskakování</string>\r\n    <string name=\"content_block_action_type\">Vyberte akci</string>\r\n    <string name=\"content_block_action_none\">Nedělat nic</string>\r\n    <string name=\"content_block_action_only_skip\">Přeskakovat</string>\r\n    <string name=\"content_block_action_toast\">Přeskakovat s upozorněním</string>\r\n    <string name=\"content_block_action_dialog\">Ukaž dialog k potvrzení</string>\r\n    <string name=\"content_block_filler\">Mimo téma (výplň)</string>\r\n    <string name=\"keyboard_auto_show\">Zobraz klávesnici automaticky</string>\r\n    <string name=\"cancel_dialog\">Zrušit</string>\r\n    <string name=\"msg_player_error_source2\">URL nefunguje nebo je na zařízení nesprávný čas.\\nNejspíše je to bug aplikace.</string>\r\n    <string name=\"msg_player_error_video_source\">VIDEO URL nefunguje nebo je na zařízení nesprávný čas.\\nNejspíše je to bug aplikace.</string>\r\n    <string name=\"msg_player_error_audio_source\">AUDIO URL nefunguje nebo je na zařízení nesprávný čas.\\nNejspíše je to bug aplikace.</string>\r\n    <string name=\"msg_player_error_subtitle_source\">SUBTITLE URL nefunguje nebo je na zařízení nesprávný čas.\\nNejspíše je to bug aplikace.</string>\r\n    <string name=\"hide_upcoming\">Skrýt nadcházející videa v Odběrech</string>\r\n    <string name=\"hide_upcoming_channel\">Skrýt nadcházející z Kanálů</string>\r\n    <string name=\"hide_upcoming_home\">Skrýt nadcházející z Domů</string>\r\n    <string name=\"search_background_playback\">Přehrávání na pozadí při vyhledávání/procházení kanálu</string>\r\n    <string name=\"trending_row_name\">Trendy</string>\r\n    <string name=\"player_seek_type\">Typ přetáčení</string>\r\n    <string name=\"player_seek_regular\">Normální</string>\r\n    <string name=\"player_seek_confirmation_pause\">S potvrzením (pozastav při přetáčení)</string>\r\n    <string name=\"player_seek_confirmation_play\">S potvrzením (hraj dál při přetáčení)</string>\r\n    <string name=\"hide_shorts_from_home\">Skrýt Shorts z nabídky Domů</string>\r\n    <string name=\"finish_on_disconnect\">Ukončit aplikaci, když se telefon/tablet odpojí</string>\r\n    <string name=\"update_error\">Chyba aktualizace</string>\r\n    <string name=\"hide_shorts_from_search\">Skrýt Shorts z výsledků vyhledávání</string>\r\n    <string name=\"hide_shorts_from_history\">Skrýt Shorts z nabídky Historie</string>\r\n    <string name=\"hide_shorts_from_trending\">Skrýt Shorts z nabídky Trendy</string>\r\n    <string name=\"disable_screensaver\">Zakaž spořič obrazovky</string>\r\n    <string name=\"description_not_found\">Popis nenalezen</string>\r\n    <string name=\"proxy_port_hint\">Proxy port</string>\r\n    <string name=\"proxy_host_hint\">Název nebo IP adresa proxy serveru</string>\r\n    <string name=\"proxy_username_hint\">Uživatelké jméno proxy</string>\r\n    <string name=\"proxy_password_hint\">Heslo proxy</string>\r\n    <string name=\"proxy_type\">Typ proxy</string>\r\n    <string name=\"enable_web_proxy\">Použij webové proxy</string>\r\n    <string name=\"proxy_not_supported\">Použijte webové proxy (Vyžaduje Android 4.4+)</string>\r\n    <string name=\"proxy_test_btn\">Test</string>\r\n    <string name=\"proxy_settings_title\">Nastavení proxy serveru</string>\r\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\r\n    <string name=\"proxy_test_cancelled\">Test #%d: zrušen.</string>\r\n    <string name=\"proxy_type_invalid\">Typ proxy není nastaven.</string>\r\n    <string name=\"proxy_host_invalid\">Hostitel proxy není nastaven.</string>\r\n    <string name=\"proxy_port_invalid\">Neplatný proxy port, musí být &gt; 0.</string>\r\n    <string name=\"proxy_credentials_invalid\">Neplatné uživ. jméno nebo heslo.</string>\r\n    <string name=\"proxy_test_aborted\">Test přerušen, prosím opravte nastavení proxy.</string>\r\n    <string name=\"proxy_application_aborted\">Nejprve prosím opravte nastavení proxy.</string>\r\n    <string name=\"proxy_test_start\">Test #%d: %s …</string>\r\n    <string name=\"proxy_test_error\">Chyba #%d: %s</string>\r\n    <string name=\"proxy_test_status\">Status #%d: %s %d %s</string>\r\n    <string name=\"openvpn_config_address_hint\">Adresa konfiguračního souboru (*.ovpn)</string>\r\n    <string name=\"enable_openvpn\">Použít OpenVPN</string>\r\n    <string name=\"openvpn_settings_title\">Nastavení OpenVPN</string>\r\n    <string name=\"openvpn_application_aborted\">Nejprve prosím opravte nastavení OpenVPN.</string>\r\n    <string name=\"openvpn_test_aborted\">Test byl přerušen, nejprve opravte nastavení OpenVPN.</string>\r\n    <string name=\"openvpn_address_invalid\">Adresa konfigurace OpenVPN není nastavena.</string>\r\n    <string name=\"internet_censorship\">Cenzura internetu</string>\r\n    <string name=\"rename_section\">Přejmenovat tuto sekci</string>\r\n    <string name=\"simple_edit_value_hint\">Zadejte hodnotu</string>\r\n    <string name=\"seek_interval\">Interval přeskakování</string>\r\n    <string name=\"seek_interval_sec\">%s sekund</string>\r\n    <string name=\"subtitle_system\">Systémový styl (nastavení Androidu &gt; Přístupnost)</string>\r\n    <string name=\"subtitle_scale\">Velikost titulků</string>\r\n    <string name=\"audio_sync_fix_desc\">Jiný způsob synchronizace audia/videa. Pozažuje se za zastaralý, ale v některých případech může pomoci.</string>\r\n    <string name=\"audio_sync_fix\">Oprava synchronizace zvuku</string>\r\n    <string name=\"ambilight_ratio_fix_desc\">Opravuje chybějící bias osvětlení. Opravuje nesprávný poměr stran nebo měřítko videa. Opravuje prázdné screenshoty. Ovlivňuje výkon!</string>\r\n    <string name=\"ambilight_ratio_fix\">Oprava Ambilight/Poměru stran/Měřítka videa/Screenshotů</string>\r\n    <string name=\"force_legacy_codecs_desc\">Výrazně zlepšuje výkon na zařízeních nižší třídy. Maximální rozlišení je 720p.</string>\r\n    <string name=\"force_legacy_codecs\">Vynucení starších kodeků (720p)</string>\r\n    <string name=\"live_stream_fix_desc\">Pozor: Přetáčení vysílání nebude k dispozici. Výrazně zlepšuje výkon živého vysílání na zařízeních nižší třídy. Maximální rozlišení je 1080p.</string>\r\n    <string name=\"live_stream_fix_4k_desc\">Pozor: Přetáčení vysílání nebude k dispozici. Výrazně zlepšuje výkon živého vysílání. Maximální rozlišení je 4K.</string>\r\n    <string name=\"live_stream_fix\">Oprava živého vysílání (1080p)</string>\r\n    <string name=\"live_stream_fix_4k\">Oprava živého vysílání (4K)</string>\r\n    <string name=\"playback_notifications_fix_desc\">Skryje oznámení změny stopy. Mohlo by to být užitečné pro firmware založený na AOSP.</string>\r\n    <string name=\"playback_notifications_fix\">Zakázat oznámení přehrávání</string>\r\n    <string name=\"amlogic_fix_desc\">Oprava poklesu snímků na Amlogic zařízeních.</string>\r\n    <string name=\"amlogic_fix\">Amlogic 1080p\\@60fps oprava</string>\r\n    <string name=\"tunneled_video_playback_desc\">POZNÁMKA: Pozastavení nemusí správně fungovat! Tunelované video přehrávání slibuje výhody jako např. lepší audio/video synchronizace (AV synchronizace) a plynulejší přehrávání. Vyžaduje Android 5+</string>\r\n    <string name=\"tunneled_video_playback\">Tunelované video přehrávání (Android 5+)</string>\r\n    <string name=\"master_volume\">Hlavní hlasitost</string>\r\n    <string name=\"volume_limit\">Omezení hlasitosti</string>\r\n    <string name=\"player_volume\">Hlasitost</string>\r\n    <string name=\"play_video\">Přehrát</string>\r\n    <string name=\"remember_position_of_short_videos\">Pamatovat si pozici krátkých videí (méně než 5 minut)</string>\r\n    <string name=\"remember_position_of_live_videos\">Pamatovat si pozici živých přenosů</string>\r\n    <string name=\"player_show_tooltips\">Zobrazit nápovědu tlačítek</string>\r\n    <string name=\"action_like_unset\">\"Líbí se\" smazáno</string>\r\n    <string name=\"action_dislike_unset\">\"Nelíbí se\" smazáno</string>\r\n    <string name=\"various_buttons\">Tlačítka v horní částí hlavního okna</string>\r\n    <string name=\"not_compatible_with\">Vybraná možnost není kompatibilní s</string>\r\n    <string name=\"subtitle_position\">Spodní posun titulků</string>\r\n    <string name=\"pressing_home\">stisknutím tlačítka DOMŮ</string>\r\n    <string name=\"pressing_home_back\">stisknutím tlačítka DOMŮ nebo ZPĚT</string>\r\n    <string name=\"pressing_back\">stisknutím tlačítka ZPĚT</string>\r\n    <string name=\"player_number_key_seek\">Posuv pomocí čísel</string>\r\n    <string name=\"save_remove_playlist\">Přidat/Odebrat playlist ze sekce Playlisty</string>\r\n    <string name=\"save_playlist\">Přidat playlist do sekce Playlisty</string>\r\n    <string name=\"remove_playlist\">Odstranit playlist ze sekce Playlisty natrvalo</string>\r\n    <string name=\"remove_playlist_fmt\">Trvale smazat %s ?</string>\r\n    <string name=\"removed_from_playlists\">Odstraněno ze sekce Playlisty</string>\r\n    <string name=\"saved_to_playlists\">Přidáno do sekce Playlisty</string>\r\n    <string name=\"create_playlist\">Vytvořit playlist</string>\r\n    <string name=\"create_playlist_note\">POZNÁMKA: Nebude to viditelné v YouTube aplikaci</string>\r\n    <string name=\"add_video_to_new_playlist\">Přidat do nového playlistu</string>\r\n    <string name=\"cant_delete_empty_playlist\">Nelze smazat prázdný playlist</string>\r\n    <string name=\"playlist\">Playlist</string>\r\n    <string name=\"rename_playlist\">Přejmenovat playlist</string>\r\n    <string name=\"cant_rename_empty_playlist\">Nelze přejmenovat prázdný playlist</string>\r\n    <string name=\"cant_rename_foreign_playlist\">Nelze přejmenovat cizí playlist</string>\r\n    <string name=\"cant_save_playlist\">Tento playlist nelze přidat</string>\r\n    <string name=\"enter_value\">Zadejte libovolnou hodnotu</string>\r\n    <string name=\"dialog_add_remove_from\">Přidat do/Smazat z %s</string>\r\n    <string name=\"lb_playback_controls_skip_next\">Přeskočit Další</string>\r\n    <string name=\"lb_playback_controls_skip_previous\">Přeskočit Předchozí/Přetočit do výchozí pozice</string>\r\n    <string name=\"alt_presets_behavior\">Náhradní chování předvoleb (omezit šířku pásma)</string>\r\n    <string name=\"alt_presets_behavior_desc\">Aplikace se pokusí zachovat šířku pásma odpovídající vybrané předvolbě namísto shody mezi rozlišením, fps a kodekem.</string>    \r\n    <string name=\"sleep_timer\">Časovač spánku (pokud není ovladač použit 1 hodinu)</string>\r\n    <string name=\"sleep_timer_desc\">Pozastav přehrávání pokud uživatel nepoužil ovladač během 1 hodiny</string>\r\n    <string name=\"disable_vsync\">Zakázat vertikální synchronizaci</string>\r\n    <string name=\"disable_vsync_desc\">Tato možnost zakáže zarovnání snímků se signálem vertikální synchronizace displeje. Tím se sníží zátěž CPU a zlepší výkon na slabších zařízeních.</string>\r\n    <string name=\"skip_codec_profile_check\">Přeskočit kontrolu kodeku</string>\r\n    <string name=\"skip_codec_profile_check_desc\">Přeskočí kontrolu podpory kodeku při spuštění videa. Může být užitečné pro zabugovaný firmware.</string>\r\n    <string name=\"force_sw_codec\">Vynutit softwarový dekodér videa</string>\r\n    <string name=\"force_sw_codec_desc\">Dokáže přehrát téměř jakékoli video, ale výkon je velmi chabý.</string>\r\n    <string name=\"playlist_order\">Seřadit playlist</string>\r\n    <string name=\"playlist_order_added_date_newer_first\">Podle data přidání (nejprve novejší)</string>\r\n    <string name=\"playlist_order_added_date_older_first\">Podle data přidání (nejprve starší)</string>\r\n    <string name=\"playlist_order_published_date_newer_first\">Podle data vydání (nejprve novejší)</string>\r\n    <string name=\"playlist_order_published_date_older_first\">Podle data vydání (nejprve starší)</string>\r\n    <string name=\"playlist_order_popularity\">Podle popularity</string>\r\n    <string name=\"cant_do_this_for_foreign_playlist\">Nelze provést pro cizí playlist</string>\r\n    <string name=\"owned_playlist_warning\">Chyba: Tuto akci lze provést pouze pro vlastněné playlisty</string>\r\n    <string name=\"content_block_alt_server\">Použít alternativní server</string>\r\n    <string name=\"content_block_alt_server_desc\">Povolte toto nastavení, pokud z nějakého důvodu odmítá SponsorBlock fungovat.</string>\r\n    <string name=\"unset_stream_reminder\">Zrušit připomenutí živého vysílání</string>\r\n    <string name=\"set_stream_reminder\">Nastavit připomenutí živého vysílání</string>\r\n    <string name=\"playback_starts_shortly\">Přehrávání se spustí automaticky, jakmile bude živé vysílání připraveno</string>\r\n    <string name=\"starting_stream\">Živé vysílání začíná…</string>\r\n    <string name=\"action_playlist_remove\">Odstranit z playlistu</string>\r\n    <string name=\"signin_view_title\">Uživatelský kód se načítá…</string>\r\n    <string name=\"signin_view_description\">Pro přihlášení zadejte tento kód na stránce %s\\nPOZNÁMKA. Funguje pouze s Chrome nebo Firefox prohlížečem.</string>\r\n    <string name=\"signin_view_action_text\">Hotovo</string>\r\n    <string name=\"require_checked\">Vyžaduje \\\"%s\\\"</string>\r\n    <string name=\"msg_press_again_to_exit\">Stiskněte znovu pro ukončení</string>\r\n    <string name=\"player_remaining_time\">Zbývá: %s</string>\r\n    <string name=\"player_ending_time\">Končí v %s</string>\r\n    <string name=\"badge_new_content\">NOVÝ OBSAH</string>\r\n    <string name=\"badge_live\">ŽIVĚ</string>\r\n    <string name=\"add_device_view_description\">Ke spárování zařízení zadejte tento kód na vašem telefonu v YouTube aplikaci v sekci Nastavení/Sledovat v televizi\\nPOZNÁMKA: Toto funguje pouze s Gboard klávesnicí!</string>\r\n    <string name=\"playback_controls_repeat_pause\">Opakovat Pozastavení</string>\r\n    <string name=\"playback_controls_repeat_list\">Opakovat Seznam</string>\r\n    <string name=\"action_subscribe_off\">Přestat odebírat</string>\r\n    <string name=\"action_subscribe_on\">Odebírat</string>\r\n    <string name=\"skip_24_rate\">Přeskakovat formáty s 24fps (oprava pro režim Real Cinema\\?)</string>\r\n    <string name=\"skip_shorts\">Přeskočit Shorts</string>\r\n    <string name=\"player_disable_suggestions\">Zakázat návrhy</string>\r\n    <string name=\"suggestions\">Návrhy</string>\r\n    <string name=\"feedback\">Zpětná vazba</string>\r\n    <string name=\"sources\">Zdroje</string>\r\n    <string name=\"releases\">Vydané verze</string>\r\n    <string name=\"prefer_avc_over_vp9\">Výběr kodeku: preferování AVC nad VP9</string>\r\n    <string name=\"open_chat\">Chat/Komentáře</string>\r\n    <string name=\"open_comments\">Otevřít komentáře</string>\r\n    <string name=\"place_chat_left\">Umístit chat na levou stranu</string>\r\n    <string name=\"place_comments_left\">Umístit komentáře na levou stranu</string>\r\n    <string name=\"use_alt_speech_recognizer\">Alternativní rozpoznávač řeči</string>\r\n    <string name=\"time_format\">Časový režim</string>\r\n    <string name=\"time_format_24\">24 hodin</string>\r\n    <string name=\"time_format_12\">12 hodin (AM/PM)</string>\r\n    <string name=\"use_alt_speech_recognizer_desc\">Dost zabugováno. Použijte pouze v případě, když máte problém s výchozím rozpoznávačem.</string>\r\n    <string name=\"speech_recognizer\">Rozpoznávač řeči</string>\r\n    <string name=\"speech_engine\">Hlasový vyhledávač</string>\r\n    <string name=\"speech_recognizer_system\">Systémový (preferovaný)</string>\r\n    <string name=\"speech_recognizer_external_1\">Externí 1 (vážně zabugováno - aby fungovalo, musíte zakázat přístup k mikrofonu)</string>\r\n    <string name=\"speech_recognizer_external_2\">Externí 2 (vážně zabugováno)</string>\r\n    <string name=\"real_channel_icon\">Zobraz ikonu na tlačítku kanálu</string>\r\n    <string name=\"subtitle_yellow_semi_transparent\">Žlutá na poloprůhledném pozadí</string>\r\n    <string name=\"subtitle_yellow_black\">Žlutá na černém pozadí</string>\r\n    <string name=\"player_pixel_ratio\">Poměr pixelů</string>\r\n    <string name=\"color_scheme_dark_grey_monochrome\">Tmavě šedá (černobílá)</string>\r\n    <string name=\"disable_mic_permission\">Zakažte aplikaci přístup k mikrofonu, aby tento rozpoznávač fungoval správně.</string>\r\n    <string name=\"repeat_mode_shuffle\">Promíchat jakýkoliv playlist</string>\r\n    <string name=\"chat_left\">Vlevo</string>\r\n    <string name=\"chat_right\">Vpravo</string>\r\n    <string name=\"card_real_thumbnails\">Nahradit náhledy snímkem z videa</string>\r\n    <string name=\"card_content\">Odkud vzít náhled videa</string>\r\n    <string name=\"thumb_quality_default\">Výchozí</string>\r\n    <string name=\"thumb_quality_start\">Na začátku videa</string>\r\n    <string name=\"thumb_quality_middle\">Uprostřed videa</string>\r\n    <string name=\"thumb_quality_end\">Na konci videa</string>\r\n    <string name=\"content_block_status\">Zkontrolovat stav SponsorBlock serveru</string>\r\n    <string name=\"dearrow_status\">Zkontrolovat stav DeArrow serveru</string>\r\n    <string name=\"player_show_quality_info_bitrate\">Přidej bitrate do informací o kvalitě</string>\r\n    <string name=\"player_speed_button_old_behavior\">Vrátit původní chování tlačítka rychlosti</string>\r\n    <string name=\"protect_settings_with_password\">Chránit všechna nastavení heslem</string>\r\n    <string name=\"enter_settings_password\">Zadejte heslo</string>\r\n    <string name=\"child_mode\">Dětský režim</string>\r\n    <string name=\"child_mode_desc\">V tomto režimu uživatel nemůže použít vyhledávání ani vidět žádný navrhovaný obsah. Stránka nastavení bude chráněna heslem.</string>\r\n    <string name=\"lost_setting_warning\">Nastavení aplikace se změní. Ujistěte se, že jste si vytvořili zálohu nastavení.</string>\r\n    <string name=\"player_button_long_click\">Dlouhým stisknutím tohoto tlačítka zobrazíte další možnosti</string>\r\n    <string name=\"pause_history\">Pozastavit historii</string>\r\n    <string name=\"resume_history\">Obnovit historii</string>\r\n    <string name=\"disable_history\">Zakázat historii</string>\r\n    <string name=\"enable_history\">Povolit historii</string>\r\n    <string name=\"clear_history\">Smazat historii</string>\r\n    <string name=\"chapters\">Kapitoly</string>\r\n    <string name=\"card_multiline_subtitle\">Víceřádkové titulky</string>\r\n    <string name=\"auto_frame_rate_modes\">Podporované režimy</string>\r\n    <string name=\"loading\">Načítání…</string>\r\n    <string name=\"hide_streams\">Skrýt živá vysílání z Odběrů</string>\r\n    <string name=\"video_rotate\">Otočit</string>\r\n    <string name=\"video_flip\">Překlopit (zrcadlově)</string>\r\n    <string name=\"trending_searches\">Vyhledávací trendy</string>\r\n    <string name=\"video_duration_any\">Vše</string>\r\n    <string name=\"video_duration\">Trvání</string>\r\n    <string name=\"video_duration_under_4\">Méně než 4 minuty</string>\r\n    <string name=\"video_duration_between_4_20\">4–20 minut</string>\r\n    <string name=\"video_duration_over_20\">Více než 20 minut</string>\r\n    <string name=\"content_type\">Typ</string>\r\n    <string name=\"content_type_any\">Vše</string>\r\n    <string name=\"content_type_video\">Video</string>\r\n    <string name=\"content_type_channel\">Kanál</string>\r\n    <string name=\"content_type_playlist\">Playlist</string>\r\n    <string name=\"content_type_movie\">Film</string>\r\n    <string name=\"video_features\">Vlastnosti</string>\r\n    <string name=\"video_feature_any\">Vše</string>\r\n    <string name=\"video_feature_live\">Živě</string>\r\n    <string name=\"video_feature_4k\">4K</string>\r\n    <string name=\"video_feature_hdr\">HDR</string>\r\n    <string name=\"search_sorting\">Řadit podle</string>\r\n    <string name=\"sort_by_relevance\">Relevance</string>\r\n    <string name=\"sort_by_views\">Počet zhlédnutí</string>\r\n    <string name=\"sort_by_date\">Datum nahrání</string>\r\n    <string name=\"sort_by_rating\">Hodnocení</string>\r\n    <string name=\"clear_search_history\">Vymazat historii vyhledávání</string>\r\n    <string name=\"remove_from_subscriptions\">Skrýt</string>\r\n    <string name=\"player_long_speed_list\">Dlouhý seznam rychlostí</string>\r\n    <string name=\"player_extra_long_speed_list\">Velmi dlouhý seznam rychlostí</string>\r\n    <string name=\"alt_app_icon\">Alternativní ikona aplikace (vyžaduje restart)</string>\r\n    <string name=\"network_stack\">Preferovat síťový zásobník %s</string>\r\n    <string name=\"player_network_stack\">Síťový vyhledávač</string>\r\n    <string name=\"cronet_desc\">Cronet je Chromium síťový zásobník. Může snížit latenci a zvýšit výkon sítě, což může pomoci s problémy s vyrovnávací pamětí.</string>\r\n    <string name=\"unlock_all_formats\">Odblokovat všechny formáty videa</string>\r\n    <string name=\"unlock_all_formats_desc\">Na některých zařízeních firmware nesprávně hlásí určité formáty jako nepodporované i když podporované jsou.\\n(např. chytré televize s rozlišením 1080p mají tendenci hlásit 4K jako nepodporované)</string>\r\n    <string name=\"okhttp_desc\">Tento síťový vyhledávač je nejpomalejší, ale má dobrou stabilitu.</string>\r\n    <string name=\"select_channel_section\">Otevřít odpovídající sekci při spuštění aplikace z kanálů ATV</string>\r\n    <string name=\"enable_voice_search_desc\">Oficiální aplikace bude nahrazena tzv. \\\"mostem\\\". Most pomáhá přenášet požadavky globálního vyhledávání do této aplikace.</string>\r\n    <string name=\"enable_master_password\">Povolit hlavní heslo</string>\r\n    <string name=\"enter_master_password\">Zadejte hlavní heslo</string>\r\n    <string name=\"disable_stream_buffer\">Zakázat vyrovnávací paměť u vysílání</string>\r\n    <string name=\"disable_stream_buffer_desc\">Oprava pro situace, kdy je vysílání hodně pozadu. Pozor: vysílání se může začít zasekávat</string>\r\n    <string name=\"sony_frame_drop_fix\">Oprava výpadku snímků #1</string>\r\n    <string name=\"sony_frame_drop_fix_desc\">Opravuje záseky na televizích Sony a jiných zařízeních. Pozor: mohou nastat problémy se synchronizací audia</string>\r\n    <string name=\"audio_language\">Jazyk zvuku</string>\r\n    <string name=\"old_home_look\">Původní vzhled sekce Domů</string>\r\n    <string name=\"old_channel_look\">Původní vzhled stránky Kanálu</string>\r\n    <string name=\"hide_shorts_everywhere\">Skrýt Shorts ze všech sekcí</string>\r\n    <string name=\"update_found\">Aktualizace</string>\r\n    <string name=\"volume_boost_warning\">Další nastavení nad limit může poškodit reproduktory</string>\r\n    <string name=\"play_video_incognito\">Přehrát anonymně</string>\r\n    <string name=\"play_from_start\">Přehraj od začátku</string>\r\n    <string name=\"header_kids_home\">Děti</string>\r\n    <string name=\"player_section_playlist\">Použít obsah aktuální sekce jako playlist</string>\r\n    <string name=\"header_trending\">Trendy</string>\r\n    <string name=\"screensaver\">Spořič obrazovky</string>\r\n    <string name=\"player_screen_off_timeout\">Časový limit vypnutí obrazovky</string>\r\n    <string name=\"old_update_notifications\">Původní vzhled upozornění na aktualizace</string>\r\n    <string name=\"dialog_notification\">Upozornit na aktualizaci pomocí vyskakovacího dialogu</string>\r\n    <string name=\"mark_as_watched\">Označit jako zhlédnuté</string>\r\n    <string name=\"player_ui_animations\">Povolit animaci rozhraní přehrávače</string>\r\n    <string name=\"player_likes_count\">Zobrazit počet Líbí/Nelíbí</string>\r\n    <string name=\"sorting_alphabetically2\">Abecedně (rychle, animované náhledy)</string>\r\n    <string name=\"sorting_default\">Výchozí (rychle, animované náhledy)</string>\r\n    <string name=\"content_block_exclude_channel\">Vyloučit tento kanál ze SponsorBlocku</string>\r\n    <string name=\"content_block_stop_excluding_channel\">Přestat vylučovat tento kanál ze SponsorBlocku</string>\r\n    <string name=\"player_chapter_notification\">Rychlý posun mezi kapitolami pomocí vyskakovacího upozornění</string>\r\n    <string name=\"player_chapter_notification2\">Přepnout na další kapitolu kliknutím na upozornění</string>\r\n    <string name=\"subtitle_remember\">Povolit titulky pouze na aktuálním kanálu</string>\r\n    <string name=\"amazon_frame_drop_fix\">Oprava poklesu snímků #2</string>\r\n    <string name=\"amazon_frame_drop_fix_desc\">Určeno pro zařízení Amazon Stick. Může fungovat i na jiných zařízeních.</string>\r\n    <string name=\"default_stack_desc\">Vestavěný síťový vyhledávač. Tento vyhledávač může mít v určitých situacích lepší stabilitu.</string>\r\n    <string name=\"item_postion\">Pozice</string>\r\n    <string name=\"player_screen_off_dimming\">Míra ztmavení obrazovky</string>\r\n    <string name=\"screensaver_timout\">Časový limit spořiče</string>\r\n    <string name=\"screensaver_dimming\">Míra ztmavení spořiče</string>\r\n    <string name=\"player_ui_on_next\">Zobrazit rozhraní přehrávače při přechodu na další video</string>\r\n    <string name=\"autogenerated\">Automaticky generované</string>\r\n    <string name=\"player_auto_volume\">Automatické nastavení hlasitosti</string>\r\n    <string name=\"header_shorts\">Shorts</string>\r\n    <string name=\"player_global_focus\">Sjednotit zaměření mezi řadami tlačítek přehrávače</string>\r\n    <string name=\"auto_history\">Automaticky (použije nastavení účtu)</string>\r\n    <string name=\"remember_position_subscriptions\">Pamatovat si poslední zhlédnutou pozici v Odběrech</string>\r\n    <string name=\"remember_position_pinned\">Pamatovat si poslední zhlédnutou pozici v připnutých playlistech</string>\r\n    <string name=\"msg_player_unknown_error\">Neznámá chyba</string>\r\n    <string name=\"unknown_source_error\">Neznámá chyba zdroje</string>\r\n    <string name=\"unknown_renderer_error\">Neznámá chyba renderu</string>\r\n    <string name=\"header_notifications\">Oznámení</string>\r\n    <string name=\"disable_search_history\">Zakázat historii vyhledávání</string>\r\n    <string name=\"unlock_high_bitrate_formats\">Odemknout vyšší datový tok pro 1080p vp9 formáty</string>\r\n    <string name=\"unlock_high_bitrate_audio_formats\">Odemknout vyšší datový tok pro mp4a formáty</string>\r\n    <string name=\"color_scheme_blue\">Modré</string>\r\n    <string name=\"color_scheme_dark_blue\">Tmavě modré</string>\r\n    <string name=\"player_speed_per_channel\">Pro každý kanál</string>\r\n    <string name=\"disable_popular_searches\">Nezobrazovat oblíbené vyhledávání</string>\r\n    <string name=\"multi_profiles\">Použít samostatná nastavení pro každý účet</string>\r\n    <string name=\"protect_account_with_password\">Chránit tento účet heslem</string>\r\n    <string name=\"enter_account_password\">Zadejte heslo účtu</string>\r\n    <string name=\"show_connect_messages\">Upozornění na připojená zařízení</string>\r\n    <string name=\"prefer_avc_over_vp9_desc\">POZOR: Maximálně 1080p</string>\r\n    <string name=\"play_next\">Přehrát další</string>\r\n    <string name=\"hide_watched_from_subscriptions\">Skrýt zhlédnutá videa v Odběrech</string>\r\n    <string name=\"hide_watched_from_notifications\">Skrýt zhlédnutá videa v Oznámení</string>\r\n    <string name=\"hide_unwanted_content\">Skrýt obsah</string>\r\n    <string name=\"hide_watched_from_home\">Skrýt zhlédnutá videa ze sekce Domů</string>\r\n    <string name=\"hide_watched_from_watch_later\">Skrýt zhlédnutá videa ze seznamu Přehrát později</string>\r\n    <string name=\"remote_control_permission\">Dálkové ovládání na pozadí vyžaduje oprávnění pro překrytí přes okna</string>\r\n    <string name=\"login_from_browser\">Přihlášení přes prohlížeč</string>\r\n    <string name=\"disable_remote_history\">Zazázat historii při používání dálkového ovládání</string>\r\n    <string name=\"keyboard_fix\">Zabránit tlačítku OK v otevření klávesnice (G20s a další)</string>\r\n    <string name=\"nothing_found\">Nic nenalezeno</string>\r\n    <string name=\"auto_frame_rate_desc\">Tato možnost eliminuje chvění ve scénách, kde se kamera rychle pohybuje(např. sportovní přenosy)</string>\r\n    <string name=\"channel_filter_hint\">Filtrovat kanály</string>\r\n    <string name=\"player_loop_shorts\">Opakovat Shorts</string>\r\n    <string name=\"player_quick_shorts_skip\">Přeskakovat Shorts tlačítky vlevo/vpravo</string>\r\n    <string name=\"player_quick_shorts_skip_alt\">Přeskakovat Shorts tlačítky nahoru/dolů</string>\r\n    <string name=\"player_quick_skip_videos\">Přeskakovat běžná videa tlačítky doleva/doprava</string>\r\n    <string name=\"player_quick_skip_videos_alt\">Přeskakovat běžná videa tlačítky nahoru/dolů</string>\r\n    <string name=\"channels_filter\">Zobrazit pole Filtrovat kanály v sekci Kanály</string>\r\n    <string name=\"channel_search_bar\">Vyhledávací panel na stránce kanálu</string>\r\n    <string name=\"header_sports\">Sporty</string>\r\n    <string name=\"keep_finished_activities\">Udržovat dokončené aktivity</string>\r\n    <string name=\"disable_channels_service\">Zakázat službu Kanály</string>\r\n    <string name=\"replace_titles\">Nahradit názvy videí</string>\r\n    <string name=\"enable\">Povolit</string>\r\n    <string name=\"more_info\">Více informací</string>\r\n    <string name=\"about_sponsorblock\">O SponsorBlocku</string>\r\n    <string name=\"about_dearrow\">O DeArrow</string>\r\n    <string name=\"replace_thumbnails\">Nahradit náhledy</string>\r\n    <string name=\"crowdsourced_thumbnails\">Komunitní náhledy</string>\r\n    <string name=\"crowdsoursed_titles\">Komunitní názvy</string>\r\n    <string name=\"dearrow_not_submitted_thumbs\">Zdroj náhledů (pokud neexistuje žádný od DeArrow)</string>\r\n    <string name=\"pitch_effect\">Efekt výšky tónu</string>\r\n    <string name=\"fullscreen_mode\">Režim celé obrazovky (bez systémových lišt)</string>\r\n    <string name=\"player_only_mode\">Zobrazit pouze přehrávač, pokud je video otevřeno mimo aplikaci</string>\r\n    <string name=\"pinned_channel_rows\">Zobrazit připnutý kanál jako řádky</string>\r\n    <string name=\"prefer_google_dns\">Preferovat DNS od Google</string>\r\n    <string name=\"prefer_ipv4\">Preferovat IPv4 DNS</string>\r\n    <string name=\"prefer_ipv4_desc\">Může vyřešit situace, kdy aplikace vůbec nefunguje\\nPozn. Může způsobit zamrznutí a pády (především na zařízeních s Android 8 nebo Dune HD)</string>\r\n    <string name=\"long_press_for_settings\">DLOUZE STISKNĚTE PRO NASTAVENÍ</string>\r\n    <string name=\"long_press_for_options\">DLOUZE STISKNĚTE PRO MOŽNOSTI</string>\r\n    <string name=\"device_specific_backup\">Záloha pouze pro toto zařízení</string>\r\n    <string name=\"local_backup\">Místní záloha</string>\r\n    <string name=\"auto_backup\">Automatická záloha (1x denně)</string>\r\n    <string name=\"repeat_mode_reverse_list\">Přehraj playlist nebo kanálová videa v opačném pořadí</string>\r\n    <string name=\"calm_msg\">Pracujeme na řešení problému. Zkontrolujte čas od času aktualizace.</string>\r\n    <string name=\"without_picture\">Bez obrazu</string>\r\n    <string name=\"video_disabled\">Video zakázáno</string>\r\n    <string name=\"applying_fix\">Nezavírejte přehrávač. Aplikuje se oprava…</string>\r\n    <string name=\"hide_mixes\">Skrýt Mixy</string>\r\n    <string name=\"player_global_focus_desc\">Tato funkce ovlivňuje, které tlačítko přehrávače bude aktivní při procházení mezi řádky tlačítek přehrávače</string>\r\n    <string name=\"disable_network_error_fixing\">Zakázat automatickou opravu chyby sítě</string>\r\n    <string name=\"disable_network_error_fixing_desc\">Pravděpodobně musíte tuto možnost zapnout, pokud používáte VPN</string>\r\n    <string name=\"recommended\">Doporučeno</string>\r\n    <string name=\"add_to_subscriptions_group\">Přidat/Odebrat ze skupiny odběrů</string>\r\n    <string name=\"new_subscriptions_group\">Nová skupina</string>\r\n    <string name=\"rename_group\">Přejmenovat skupinu odběrů</string>\r\n    <string name=\"screen_dimming_amount\">Míra ztmavení obrazovky</string>\r\n    <string name=\"screen_dimming_timeout\">Časový limit ztmavení obrazovky</string>\r\n    <string name=\"playlists_rows\">Zobrazit sekci Playlisty jako řádky</string>\r\n    <string name=\"import_subscriptions_group\">Import</string>\r\n    <string name=\"lb_playback_controls_closed_captioning_disable\">Zakázat titulky</string>\r\n    <string name=\"lb_playback_controls_closed_captioning_enable\">Povolit titulky</string>\r\n    <string name=\"my_videos\">Moje videa</string>\r\n    <string name=\"player_audio_focus\">Audio fokus (pozastavit, pokud jsou detekovány jiné přehrávače)</string>\r\n    <string name=\"paid_content_notification\">Upozornění placeného obsahu</string>\r\n    <string name=\"you_liked\">to se ti líbí</string>\r\n    <string name=\"premium_users_only\">Pouze pro uživatele s Premium. Oprava nekompletního seznamu formátů videa</string>\r\n    <string name=\"playback_buffering_fix\">Oprava načítajícího se přehrávání</string>\r\n    <string name=\"oculus_quest_fix\">Oprava pro Oculus Quest</string>\r\n    <string name=\"card_preview_muted\">Video beze zvuku</string>\r\n    <string name=\"card_preview_full\">Video se zvukem</string>\r\n    <string name=\"card_preview\">Náhled karty</string>\r\n    <string name=\"card_unlocalized_titles\">Nelokalizované názvy videí</string>\r\n    <string name=\"dont_resize_video_to_fit_dialog\">Neměnit velikost videa, aby se přizpůsobilo dialogu</string>\r\n    <string name=\"typing_corrections\">Zakázat automatické opravy při psaní textu</string>\r\n    <string name=\"suggestions_horizontally_scrolled\">Horizontálně posouvané Návrhy</string>\r\n    <string name=\"stable_restore\">Obnovit stabilní verzi (všechna data budou ztracena)</string>\r\n    <string name=\"auto_backup_category\">Automatická záloha</string>\r\n    <string name=\"once_a_day\">Jednou denně</string>\r\n    <string name=\"once_a_week\">Jednou týdně</string>\r\n    <string name=\"once_a_month\">Jednou měsíčně</string>\r\n    <string name=\"action_debug_info\">Debug informace</string>\r\n    <string name=\"dialog_block_channel\">Blokovat kanál</string>\r\n    <string name=\"dialog_unblock_channel\">Odblokovat kanál</string>\r\n    <string name=\"confirm_block_channel\">Skrýt veškerý obsah od %s?</string>\r\n    <string name=\"channel_blocked\">Kanál zablokován</string>\r\n    <string name=\"channel_unblocked\">Kanál odblokován</string>\r\n    <string name=\"header_blocked_channels\">Blokované kanály</string>\r\n    <string name=\"msg_no_blocked_channels\">Žádné blokované kanály</string>\r\n    <string name=\"player_time_stretching\">Protahování času zvuku</string>\r\n    <string name=\"player_time_stretching_desc\">Zachovává přirozený hlas i při změně rychlosti. Na některých zařízeních může výrazně snížit výkon.</string>\r\n    <string name=\"queue_respects_playback_mode\">Fronta respektuje režim přehrávání</string>\r\n    <string name=\"ignore_short_segments\">Ignorovat krátké segmenty</string>\r\n    <string name=\"install_bridge\">Nainstalovat ATV/Amazon most</string>\r\n    <plurals name=\"hours\">\r\n        <item quantity=\"one\">%s hodina</item>\r\n        <item quantity=\"other\">%s hodin</item>\r\n    </plurals>\r\n    <plurals name=\"seconds\">\r\n        <item quantity=\"one\">%s sekunda</item>\r\n        <item quantity=\"other\">%s sekund</item>\r\n    </plurals>\r\n</resources>\r\n\r\n\r\n"
  },
  {
    "path": "common/src/main/res/values-da/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Hjem</string>\n  <string name=\"title_search\">Søg</string>\n  <string name=\"header_subscriptions\">Abonnementer</string>\n  <string name=\"header_history\">Historik</string>\n  <string name=\"header_music\">Musik</string>\n  <string name=\"header_news\">Nyheder</string>\n  <string name=\"header_gaming\">Spil</string>\n  <string name=\"header_playlists\">Playlister</string>\n  <string name=\"header_settings\">Indstillinger</string>\n  <string name=\"header_channels\">Kanaler</string>\n  <string name=\"subscriptions_signin_title\">Se det nyeste fra kanaler, du elsker</string>\n  <string name=\"subscriptions_signin_subtitle\">Log ind for at se dine abonnementer</string>\n  <string name=\"subscriptions_signin_button_text\">LOG IND</string>\n  <string name=\"library_signin_to_show_more\">Se videoer, du syntes om, har gemt eller abonneret på</string>\n  <string name=\"library_signin_subtitle\">Log ind for at se dit bibliotek </string>\n  <string name=\"action_signin\">LOG IND</string>\n  <string name=\"title_video_formats\">Videoformater</string>\n  <string name=\"title_audio_formats\">Lydformater</string>\n  <string name=\"subtitle_category_title\">Undertekster</string>\n  <string name=\"video_max_quality\">Auto (maks. kvalitet)</string>\n  <string name=\"audio_max_quality\">Auto (maks. kvalitet)</string>\n  <string name=\"subtitles_disabled\">Undertekster deaktiveret</string>\n  <string name=\"auto_frame_rate\">Automatisk billedfrekvens</string>\n  <string name=\"frame_rate_correction\">Ret FPS:\\n%s</string>\n  <string name=\"category_background_playback\">Afspil i baggrunden</string>\n  <string name=\"not_implemented\">Ikke implementeret</string>\n  <string name=\"option_background_playback_off\">Deaktiveret</string>\n  <string name=\"option_background_playback_only_audio\">Kun lyd</string>\n  <string name=\"playback_settings\">Indstillinger for afspilningskvalitet</string>\n  <string name=\"video_speed\">Videohastighed</string>\n  <string name=\"resolution_switch\">Skift opløsning</string>\n  <string name=\"video_buffer\">Videobuffer</string>\n  <string name=\"video_buffer_size_low\">Lav</string>\n  <string name=\"video_buffer_size_med\">Mellem</string>\n  <string name=\"video_buffer_size_high\">Høj</string>\n  <string name=\"update_changelog\">Ændringslog</string>\n  <string name=\"install_update\">Installer opdatering</string>\n  <string name=\"section_is_empty\">Ups! Der er intet her.</string>\n  <string name=\"dialog_add_to_playlist\">Tilføj/fjern fra playliste</string>\n  <string name=\"msg_signed_users_only\">Kun registrerede brugere</string>\n  <string name=\"msg_cant_load_content\">Kan ikke indlæse indhold.\\n1) Slå visningshistorik til.\\n2) Tjek enhedens dato og tid.\\n3) Tjek internetforbindelse.\\n4) Tjek dine proxy-indstillinger.\\n5) Prøv at logge ind på din konto.\\n6) Måske er der intet her.</string>\n  <string name=\"title_video_presets\">Videoforudindstillinger</string>\n  <string name=\"settings_accounts\">Konti</string>\n  <string name=\"settings_left_panel\">Rediger kategorier</string>\n  <string name=\"settings_themes\">Temaer</string>\n  <string name=\"settings_other\">Andre</string>\n  <string name=\"settings_player\">Afspiller</string>\n  <string name=\"settings_language\">Sprog</string>\n  <string name=\"settings_linked_devices\">Tilknyttede enheder</string>\n  <string name=\"settings_about\">Om</string>\n  <string name=\"dialog_account_list\">Vælg konto</string>\n  <string name=\"dialog_account_none\">Ingen</string>\n  <string name=\"dialog_remove_account\">Log ud</string>\n  <string name=\"dialog_add_account\">Log ind</string>\n  <string name=\"default_lang\">Standard</string>\n  <string name=\"dialog_select_language\">Sprog</string>\n  <string name=\"subtitle_default\">Standard</string>\n  <string name=\"subtitle_white_semi_transparent\">Hvid på halvgennemsigtig baggrund</string>\n  <string name=\"subtitle_style\">Undertekststil</string>\n  <string name=\"subtitle_language\">Undertekstsprog</string>\n  <string name=\"action_search\">Søg</string>\n  <string name=\"settings_main_ui\">Brugergrænseflade (UI)</string>\n  <string name=\"dialog_main_ui\">Brugergrænseflade (UI)</string>\n  <string name=\"card_animated_previews\">Animerede forhåndsvisninger</string>\n  <string name=\"web_site\">Website</string>\n  <string name=\"donation\">Donation</string>\n  <string name=\"dialog_about\">Om</string>\n  <string name=\"dialog_player_ui\">Videoafspiller</string>\n  <string name=\"player_show_ui_on_pause\">Vis UI, når pauset</string>\n  <string name=\"player_pause_on_ok\">OK-knap pauser afspilningen</string>\n  <string name=\"player_ok_button_behavior\">OK-knappens adfærd</string>\n  <string name=\"player_only_ui\">Kun UI</string>\n  <string name=\"player_ui_and_pause\">UI og pause</string>\n  <string name=\"player_only_pause\">Kun pause</string>\n  <string name=\"check_for_updates\">Søg efter opdateringer</string>\n  <string name=\"update_not_found\">Du bruger den nyeste version</string>\n  <string name=\"update_in_progress\">Vent…</string>\n  <string name=\"player_ui_hide_behavior\">Skjul automatisk UI</string>\n  <string name=\"option_never\">Aldrig</string>\n  <string name=\"side_panel_sections\">Opsæt sektioner</string>\n  <string name=\"boot_to_section\">Opstartssektion</string>\n  <string name=\"large_ui\">Stor UI</string>\n  <string name=\"video_grid_scale\">Skala for videogitter</string>\n  <string name=\"scale_ui\">UI-skala</string>\n  <string name=\"color_scheme\">Farveskema</string>\n  <string name=\"color_scheme_default\">Standard</string>\n  <string name=\"color_scheme_red_grey\">Rødgrå</string>\n  <string name=\"color_scheme_red\">Rød</string>\n  <string name=\"color_scheme_dark_grey\">Mørkegrå</string>\n  <string name=\"disable_update_check\">Deaktiver opdateringstjek</string>\n  <string name=\"show_again\">Vis igen</string>\n  <string name=\"check_updates_auto\">Underret om opdateringer</string>\n  <string name=\"select_account_on_boot\">Vælg konto ved opstart</string>\n  <string name=\"player_other\">Diverse</string>\n  <string name=\"player_full_date\">Nøjagtig dato i beskrivelsen</string>\n  <string name=\"open_channel\">Åbn kanal</string>\n  <string name=\"not_interested\">Ikke interesseret</string>\n  <string name=\"you_wont_see_this_video\">Videoen blev fjernet fra anbefalet</string>\n  <string name=\"settings_search\">Søg</string>\n  <string name=\"dialog_search\">Søg</string>\n  <string name=\"instant_voice_search\">Øjeblikkelig stemmesøgning</string>\n  <string name=\"option_background_playback_behind\">Afspil i baggrunden</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">Info om næste video er ikke indlæst endnu</string>\n  <string name=\"card_multiline_title\">Flerlinjede titler</string>\n  <string name=\"cards_style\">Kortstil</string>\n  <string name=\"repeat_mode_all\">Afspil videoer uafbrudt</string>\n  <string name=\"repeat_mode_one\">Gentag videoen</string>\n  <string name=\"repeat_mode_pause\">Sæt afspilning på pause efter hver video</string>\n  <string name=\"repeat_mode_none\">Stop afspilning efter én video</string>\n  <string name=\"subscribed_to_channel\">Abonneret</string>\n  <string name=\"unsubscribed_from_channel\">Afmeldt</string>\n  <string name=\"subtitle_yellow_transparent\">Gul på gennemsigtig baggrund</string>\n  <string name=\"subtitle_white_black\">Hvid på sort baggrund</string>\n  <string name=\"player_seek_preview\">Forhåndsvis, mens du spoler</string>\n  <string name=\"color_scheme_dark_grey_oled\">Mørkegrå (OLED)</string>\n  <string name=\"channels_section_sorting\">Sortering i kanalsektion</string>\n  <string name=\"sorting_by_new_content\">Nyt indhold</string>\n  <string name=\"sorting_alphabetically\">Alfabetisk</string>\n  <string name=\"sorting_last_viewed\">Sidst set</string>\n  <string name=\"playback_queue_category_title\">Afspilningskø</string>\n  <string name=\"open_playlist\">Åbn playliste</string>\n  <string name=\"repeat_mode_pause_alt\">Afspil kun playlistevideoer uafbrudt</string>\n  <string name=\"player_pause_when_seek\">Pause, når du spoler</string>\n  <string name=\"playlists_style\">Stil for Playlister-sektionen</string>\n  <string name=\"playlists_style_grid\">Gitter</string>\n  <string name=\"playlists_style_rows\">Rækker</string>\n  <string name=\"player_show_clock\">Vis ur i kontrollinjen</string>\n  <string name=\"player_show_remaining_time\">Vis resterende tid i kontrollinjen</string>\n  <string name=\"player_show_ending_time\">Vis sluttid i kontrollinjen</string>\n  <string name=\"open_channel_uploads\">Åbn kanaluploads</string>\n  <string name=\"app_exit_shortcut\">Afslut SmartTube</string>\n  <string name=\"app_exit_none\">Afslut ikke</string>\n  <string name=\"app_double_back_exit\">Dobbelttryk på tilbage</string>\n  <string name=\"app_single_back_exit\">Enkelt tryk på tilbage</string>\n  <string name=\"action_video_zoom\">Videozoom</string>\n  <string name=\"video_zoom\">Videozoom</string>\n  <string name=\"video_zoom_default\">Standard</string>\n  <string name=\"video_zoom_fit_width\">Tilpas bredde</string>\n  <string name=\"video_zoom_fit_height\">Tilpas højde</string>\n  <string name=\"video_zoom_fit_both\">Tilpas enten bredde eller højde</string>\n  <string name=\"video_zoom_stretch\">Stræk</string>\n  <string name=\"color_scheme_teal\">Blågrøn</string>\n  <string name=\"color_scheme_teal_oled\">Blågrøn (OLED)</string>\n  <string name=\"player_seek_preview_none\">Deaktiveret</string>\n  <string name=\"mark_all_channels_watched\">Markér alle kanaler som set</string>\n  <string name=\"screen_dimming_timeout_min\">%s min.</string>\n  <string name=\"ui_hide_timeout_sec\">%s sek.</string>\n  <string name=\"audio_shift_sec\">%s sek.</string>\n  <string name=\"auto_frame_rate_sec\">%s sek.</string>\n  <string name=\"volume\">Lyd %s%%</string>\n  <string name=\"hide_settings_section\">Skjul Indstillinger-sektionen (farligt!)</string>\n  <string name=\"add_remove_from_recent_playlist\">Tilføj/fjern fra seneste playliste</string>\n  <string name=\"context_menu\">Kontekstmenu</string>\n  <string name=\"focus_on_search_results\">Auto-fokuser på søgeresultater</string>\n  <string name=\"upload_date_last_hour\">Sidste time</string>\n  <string name=\"double_refresh_rate\">Dobbelt opdateringshastighed</string>\n  <string name=\"upload_date_this_year\">Dette år</string>\n  <string name=\"upload_date_this_month\">Denne måned</string>\n  <string name=\"upload_date_this_week\">Denne uge</string>\n  <string name=\"upload_date_today\">I dag</string>\n  <string name=\"upload_date_any\">Altid</string>\n  <string name=\"upload_date\">Dato for upload</string>\n  <string name=\"remove_from_history\">Fjern fra historik</string>\n  <string name=\"removed_from_history\">Videoen blev fjernet fra historik</string>\n  <string name=\"content_block_highlight\">Højdepunkt (bogmærke)</string>\n  <string name=\"content_block_preview_recap\">Forhåndsvisning/opsummering</string>\n  <string name=\"video_preset_adaptive\">Adaptiv</string>\n  <string name=\"removed_from\">Fjernet fra %s</string>\n  <string name=\"added_to\">Føjet til %s</string>\n  <string name=\"dialog_remove_from\">Fjern fra %s</string>\n  <string name=\"dialog_add_to\">Føj til %s</string>\n  <string name=\"background_service_started\">Baggrundstjeneste startet</string>\n  <string name=\"settings_remote_control\">Fjernbetjening</string>\n  <string name=\"run_in_background\">Afspil i baggrunden</string>\n  <string name=\"live_now_row_name\">Direkte nu</string>\n  <string name=\"option_disabled\">Deaktiveret</string>\n  <string name=\"refresh_section\">Opdater sektion</string>\n  <string name=\"sponsor_color_markers\">Farveindikatorer på statuslinjen</string>\n  <string name=\"player_time_correction\">Vis tiden med hastighedskorrektion</string>\n  <string name=\"disable_ok_long_press\">Deaktiver langt tryk på OK-knap</string>\n  <string name=\"skip_each_segment_once\">Spring ikke segmenter over igen</string>\n  <string name=\"app_restore\">Gendan appdata</string>\n  <string name=\"app_backup\">Sikkerhedskopiér appdata</string>\n  <string name=\"app_backup_restore\">Sikkerhedskopiér/gendan</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Abonner på/afmeld kanal</string>\n  <string name=\"enable_voice_search\">Aktiver stemmesøgning (kræver firmware-understøttelse)</string>\n  <string name=\"covid_news_row_name\">COVID-19-nyheder</string>\n  <string name=\"breaking_news_row_name\">Seneste nyheder</string>\n  <string name=\"popular_uploads_row_name\">Populære uploads</string>\n  <string name=\"playlists_row_name\">Oprettede playlister</string>\n  <string name=\"uploads_row_name\">Uploads</string>\n  <string name=\"proxy_disabled\">Proxy deaktiveret</string>\n  <string name=\"proxy_enabled\">Proxy aktiveret</string>\n  <string name=\"add_remove_from_playback_queue\">Tilføj/fjern fra afspilningskø</string>\n  <string name=\"added_to_playback_queue\">Føjet til afspilningskø</string>\n  <string name=\"removed_from_playback_queue\">Fjernet fra afspilningskø</string>\n  <string name=\"screen_dimming\">Skærmdæmpning</string>\n  <string name=\"key_remapping\">Omlægning af taster</string>\n  <string name=\"hide_shorts\">Skjul shorts fra Abonnementer</string>\n  <string name=\"preferred_update_source\">Vælg opdateringskilde</string>\n  <string name=\"card_text_scroll_factor\">Rullehastighed for korttekst</string>\n  <string name=\"share_embed_link\">Del indlejringslink</string>\n  <string name=\"settings_language_country\">Sprog/land</string>\n  <string name=\"dialog_select_country\">Land</string>\n  <string name=\"unpinned_from_sidebar\">Fjernet fra sidepanelet</string>\n  <string name=\"unpin_from_sidebar\">Fjern fra sidepanelet</string>\n  <string name=\"pinned_to_sidebar\">Føjet til sidepanelet</string>\n  <string name=\"pin_unpin_from_sidebar\">Tilføj/fjern fra sidepanelet</string>\n  <string name=\"return_to_background_video\">Gå tilbage til video, der kører i baggrunden</string>\n  <string name=\"channels_auto_load\">Auto-indlæs indhold i Kanaler-sektionen</string>\n  <string name=\"channels_old_look\">Gendan det gamle udseende af Kanaler-sektionen</string>\n  <string name=\"background_playback_activation\">Baggrundsafspilning (aktivering)</string>\n  <string name=\"uploads_old_look\">Gendan det gamle udseende af Uploads-sektionen</string>\n  <string name=\"player_show_global_ending_time\">Vis altid videoens sluttid på skærmen</string>\n  <string name=\"player_show_global_clock\">Vis altid ur på skærmen</string>\n  <string name=\"header_uploads\">Uploads</string>\n  <string name=\"player_tweaks\">Udviklerindstillinger</string>\n  <string name=\"video_aspect\">Billedformat</string>\n  <string name=\"msg_player_error_unexpected\">Ukendt videodekoderfejl</string>\n  <string name=\"msg_player_error_renderer\">Det valgte format understøttes ikke.\\nPrøv at vælge et andet.\\nHvis det ikke hjælper, så prøv at genstarte enheden.</string>\n  <string name=\"msg_player_error_source\">Kan ikke hente videoen</string>\n  <string name=\"player_remember_speed_each\">For hver video</string>\n  <string name=\"player_remember_speed_all\">Det samme på alle videoer</string>\n  <string name=\"player_remember_speed_none\">Ingen</string>\n  <string name=\"player_remember_each_speed\">Husk hver videos hastighed</string>\n  <string name=\"player_show_quality_info\">Vis info om videokvalitet i kontrollinjen</string>\n  <string name=\"player_sleep_timer\">Søvntimer</string>\n  <string name=\"video_preset_enabled\">Formatet for den næste video indstilles ifølge forudindstillingen</string>\n  <string name=\"video_preset_disabled\">Uden forudindstilling</string>\n  <string name=\"msg_player_error\">Afpillerfejl opstod %s. Genstarter afspilning…</string>\n  <string name=\"msg_mode_switch_error\">Kan ikke skifte visningstilstand til \\\"%s</string>\n  <string name=\"remote_session_closed\">Fjernsession blev lukket</string>\n  <string name=\"player_low_video_quality\">Lav videokvalitet</string>\n  <string name=\"btn_confirm\">Bekræft</string>\n  <string name=\"intent_force_close\">Tving afslutning, hvis video blev åbnet fra ekstern kilde</string>\n  <string name=\"return_to_launcher\">Gå tilbage til startskærmen fra ATV kanaler/søgning</string>\n  <string name=\"content_block_notify_dialog\">Bekræftelsesdialog</string>\n  <string name=\"content_block_notify_toast\">Toast</string>\n  <string name=\"content_block_notify_none\">Uden notifikation</string>\n  <string name=\"content_block_notification_type\">Notifikationstype</string>\n  <string name=\"msg_skipping_segment\">Springer segmentet \\\"%s\\\" over…</string>\n  <string name=\"cancel_segment_skip\">Annuller overspring af segment</string>\n  <string name=\"confirm_segment_skip\">Spring segmentet \\\"%s\\\" over\\?</string>\n  <string name=\"content_block_music_off_topic\">Afsnit uden musik i klip</string>\n  <string name=\"content_block_self_promo\">Ubetalt/selvpromovering</string>\n  <string name=\"content_block_interaction\">Interaktionspåmindelse (abonner)</string>\n  <string name=\"content_block_outro\">Slutkort/kreditter</string>\n  <string name=\"content_block_intro\">Pause/intro-animation</string>\n  <string name=\"content_block_sponsor\">Sponsor</string>\n  <string name=\"content_block_categories\">Spring segmenter over</string>\n  <string name=\"content_block_confirm_skip\">Bekræft spring over</string>\n  <string name=\"settings_video\">Video</string>\n  <string name=\"settings_general\">Generelt</string>\n  <string name=\"msg_done\">Færdig</string>\n  <string name=\"msg_applying\">Anvender %s</string>\n  <string name=\"settings_block\">Indholdsblokering</string>\n  <string name=\"msg_restart_app\">Genstart venligst SmartTube for at anvende indstillingerne</string>\n  <string name=\"settings_ui_scale\">UI-skala</string>\n  <string name=\"device_disconnected\">Enheden \\\"%s\\\" er blevet afbrudt</string>\n  <string name=\"device_connected\">Enheden \\\"%s\\\" er blevet tilsluttet</string>\n  <string name=\"device_link_enabled\">Link aktiveret</string>\n  <string name=\"dialog_remove_all_devices\">Fjern alle enheder</string>\n  <string name=\"dialog_add_device\">Tilføj enhed</string>\n  <string name=\"channel_marked_as_watched\">Kanal blev markeret som set</string>\n  <string name=\"mark_channel_as_watched\">Markér som set</string>\n  <string name=\"player_remember_speed\">Husk hastighed</string>\n  <string name=\"audio_shift\">Lydforskydning</string>\n  <string name=\"auto_frame_rate_applying\">Anvender auto-billedfrekvens %sx%s\\@%s</string>\n  <string name=\"auto_frame_rate_pause\">Auto-billedfrekvens (pause ved skift)</string>\n  <string name=\"wait_data_loading\">Vent venligst, mens data indlæses…</string>\n  <string name=\"subscribe_to_channel\">Abonner på kanalen</string>\n  <string name=\"share_link\">Del link</string>\n  <string name=\"card_auto_scrolled_title\">Auto-rul beskåret titel</string>\n  <string name=\"card_title_lines_num\">Kortets antal titellinjer</string>\n  <string name=\"unsubscribe_from_channel\">Afmeld kanalen</string>\n  <string name=\"player_seek_preview_carousel_fast\">Karrusel (hurtig; upræcis forhåndsvisning)</string>\n  <string name=\"player_seek_preview_carousel_slow\">Karrusel (langsom; efter keyframes)</string>\n  <string name=\"player_seek_preview_carousel\">Karrusel</string>\n  <string name=\"player_seek_preview_single\">Enkel frame</string>\n  <string name=\"option_background_playback_pip\">Billede-i-billede</string>\n  <string name=\"move_section_up\">Flyt sektion op</string>\n  <string name=\"move_section_down\">Flyt sektion ned</string>\n  <string name=\"player_buttons\">Opsæt afspillerknapper</string>\n  <string name=\"action_playlist_add\">Føj til playliste</string>\n  <string name=\"action_video_stats\">Videostatistik</string>\n  <string name=\"action_subscribe\">Abonner</string>\n  <string name=\"action_channel\">Åbn kanal</string>\n  <string name=\"action_pip\">BiB</string>\n  <string name=\"action_screen_off\">Skærm fra</string>\n  <string name=\"action_playback_queue\">Afspilningskø</string>\n  <string name=\"action_video_speed\">Videohastighed</string>\n  <string name=\"action_subtitles\">Undertekster</string>\n  <string name=\"action_like\">Kan lide</string>\n  <string name=\"action_dislike\">Kan ikke lide</string>\n  <string name=\"action_play_pause\">Afspil/pause</string>\n  <string name=\"action_repeat_mode\">Afspilningstilstand</string>\n  <string name=\"action_next\">Næste video</string>\n  <string name=\"action_previous\">Forrige video</string>\n  <string name=\"action_high_quality\">Videokvalitet</string>\n  <string name=\"content_block_no_skipping_mode\">Ingen overspringning</string>\n  <string name=\"content_block_action_type\">Vælg handling</string>\n  <string name=\"content_block_action_none\">Gør intet</string>\n  <string name=\"content_block_action_only_skip\">Spring kun over</string>\n  <string name=\"content_block_action_toast\">Spring over med notifikation</string>\n  <string name=\"content_block_action_dialog\">Vis bekræftelsesdialog</string>\n  <string name=\"content_block_filler\">Uden for emnet (fyld)</string>\n  <string name=\"cancel_dialog\">Annuller</string>\n  <string name=\"keyboard_auto_show\">Vis tastatur automatisk</string>\n  <string name=\"pin_unpin_playlist\">Tilføj/fjern playliste fra sidepanelet</string>\n  <string name=\"pin_unpin_channel\">Tilføj/fjern kanal fra sidepanelet</string>\n  <string name=\"msg_player_error_source2\">URL virker ikke, ellers er enhedens ur forkert.\\nDette er normalt en fejl i SmartTube.</string>\n  <string name=\"hide_upcoming\">Skjul kommende fra Abonnementer</string>\n  <string name=\"search_background_playback\">Afspil i baggrunden, mens du søger/browser på en kanal</string>\n  <string name=\"trending_row_name\">Populært lige nu</string>\n  <string name=\"player_seek_type\">Spoleadfærd</string>\n  <string name=\"player_seek_regular\">Standard</string>\n  <string name=\"player_seek_confirmation_pause\">Med bekræftelse (pause, mens du spoler)</string>\n  <string name=\"player_seek_confirmation_play\">Med bekræftelse (afspil, mens du spoler)</string>\n  <string name=\"hide_shorts_from_home\">Skjul shorts fra Hjem</string>\n  <string name=\"finish_on_disconnect\">Luk SmartTube efter at have afbrudt telefonen/tabletten</string>\n  <string name=\"update_error\">Opdateringsfejl</string>\n  <string name=\"hide_shorts_from_history\">Skjul shorts fra Historik</string>\n  <string name=\"disable_screensaver\">Deaktiver pauseskærm</string>\n  <string name=\"color_scheme_blue\">Blå</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Lås op for høj bitrate på mp4a-formater</string>\n  <string name=\"unlock_high_bitrate_formats\">Lås op for høj bitrate på 1080p vp9-formater </string>\n  <string name=\"disable_search_history\">Deaktiver søgehistorik</string>\n  <string name=\"header_notifications\">Notifikationer</string>\n  <string name=\"msg_player_unknown_error\">Ukendt fejl</string>\n  <string name=\"remember_position_subscriptions\">Husk sidst sete position i Abonnementer</string>\n  <string name=\"auto_history\">Auto (brug kontoindstillinger)</string>\n  <string name=\"player_global_focus\">Problemfri navigation mellem rækker af afspillerknapper</string>\n  <string name=\"header_shorts\">Shorts</string>\n  <string name=\"player_auto_volume\">Automatisk justering af lydstyrke</string>\n  <string name=\"autogenerated\">autogenereret</string>\n  <string name=\"player_ui_on_next\">Vis afspillerens UI ved skift til næste video</string>\n  <string name=\"screensaver_dimming\">Dæmpningsniveau ved pauseskærm</string>\n  <string name=\"screensaver_timout\">Timeout for pauseskærm</string>\n  <string name=\"player_screen_off_dimming\">Skærmdæmpningsniveau</string>\n  <string name=\"item_postion\">Placering af</string>\n  <string name=\"default_stack_desc\">Indbygget netværksstak. Kan være mere stabil i visse situationer.</string>\n  <string name=\"amazon_frame_drop_fix_desc\">Beregnet til Amazon Stick-enheder. Virker muligvis også på andre enheder.</string>\n  <string name=\"amazon_frame_drop_fix\">Rettelse #2 til tab af frames</string>\n  <string name=\"subtitle_remember\">Husk aktiverede undertekster for hver kanal</string>\n  <string name=\"player_chapter_notification\">Kør hurtigt gennem kapitlerne ved hjælp af pop op-notifikationen</string>\n  <string name=\"content_block_stop_excluding_channel\">Stop med at ekskludere denne kanal fra Sponsorblock</string>\n  <string name=\"content_block_exclude_channel\">Ekskluder denne kanal fra SponsorBlock</string>\n  <string name=\"sorting_default\">Standard (hurtig; animerede forhåndvisninger)</string>\n  <string name=\"sorting_alphabetically2\">Alfabetisk (hurtig; animerede forhåndsvisninger)</string>\n  <string name=\"player_likes_count\">Vis antal likes/dislikes</string>\n  <string name=\"player_ui_animations\">Aktiver animationer i afspillerens UI</string>\n  <string name=\"mark_as_watched\">Markér som set</string>\n  <string name=\"dialog_notification\">Underret om opdateringer med en pop op-dialog</string>\n  <string name=\"old_update_notifications\">Det gamle udseende af opdateringsnotifikationer</string>\n  <string name=\"player_screen_off_timeout\">Timeout for skærmdæmpning</string>\n  <string name=\"screensaver\">Pauseskærm</string>\n  <string name=\"header_trending\">Populært lige nu</string>\n  <string name=\"player_section_playlist\">Brug det aktuelle sektionsindhold som playliste</string>\n  <string name=\"header_kids_home\">Børn</string>\n  <string name=\"play_video_incognito\">Afspil inkognito</string>\n  <string name=\"volume_boost_warning\">Indstillinger over 100% kan skade dine højtalere</string>\n  <string name=\"update_found\">Opdater</string>\n  <string name=\"hide_shorts_everywhere\">Skjul shorts overalt</string>\n  <string name=\"old_home_look\">Det gamle udseende af Hjem-sektionen</string>\n  <string name=\"audio_language\">Lydsprog</string>\n  <string name=\"sony_frame_drop_fix_desc\">Retter lag på Sony TV og visse andre enheder. Bemærk: Mulige problemer med lydsynkronisering.</string>\n  <string name=\"sony_frame_drop_fix\">Rettelse #1 til tab af frames</string>\n  <string name=\"disable_stream_buffer_desc\">Rettelse til situationer, hvor streamen er for langt bagud. Bemærk: Streamen kan begynde at lagge.</string>\n  <string name=\"disable_stream_buffer\">Deaktiver buffer på streams</string>\n  <string name=\"enter_master_password\">Indtast global adgangskode</string>\n  <string name=\"enable_master_password\">Aktivér global adgangskode</string>\n  <string name=\"enable_voice_search_desc\">Den officielle app erstattes med en såkaldt \\\"bro\\\", som videresender globale søgeforespørgsler til SmartTube.</string>\n  <string name=\"select_channel_section\">Åbn tilsvarende sektion, når SmartTube startes fra en sektion i Android TV-launcheren. </string>\n  <string name=\"msg_player_error_video_source\">Video-URL virker ikke, ellers er enhedens ur forkert.\\nDette er normalt en fejl i SmartTube.</string>\n  <string name=\"msg_player_error_audio_source\">Lyd-URL virker ikke, ellers er enhedens ur forkert.\\nDette er normalt en fejl i SmartTube.</string>\n  <string name=\"msg_player_error_subtitle_source\">Undertekst-URL virker ikke, ellers er enhedens ur forkert.\\nDette er normalt en fejl i SmartTube.</string>\n  <string name=\"description_not_found\">Beskrivelse ikke fundet</string>\n  <string name=\"proxy_port_hint\">Proxyport</string>\n  <string name=\"proxy_host_hint\">Proxyvært eller IP</string>\n  <string name=\"proxy_username_hint\">Proxy-brugernavn</string>\n  <string name=\"proxy_password_hint\">Proxy-adgangskode</string>\n  <string name=\"proxy_type\">Proxytype</string>\n  <string name=\"enable_web_proxy\">Brug webproxy</string>\n  <string name=\"proxy_not_supported\">Brug webproxy (kræver Android 4.4+)</string>\n  <string name=\"proxy_test_btn\">Test</string>\n  <string name=\"proxy_settings_title\">Proxyserver-indstillinger</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">Test #%d: annulleret.</string>\n  <string name=\"proxy_type_invalid\">Proxytype ikke angivet.</string>\n  <string name=\"proxy_host_invalid\">Proxyvært ikke angivet.</string>\n  <string name=\"proxy_port_invalid\">Ugyldig proxyport; skal være større end 0.</string>\n  <string name=\"proxy_credentials_invalid\">Ugyldig brugernavn eller adgangskode.</string>\n  <string name=\"proxy_test_aborted\">Test afbrudt. Ret venligst proxyindstillinger først.</string>\n  <string name=\"proxy_application_aborted\">Ret venligst proxyindstillinger først.</string>\n  <string name=\"proxy_test_start\">Test #%d: %s …</string>\n  <string name=\"proxy_test_error\">Fejl #%d: %s</string>\n  <string name=\"proxy_test_status\">Status #%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Konfigurationsfil (*.ovpn)</string>\n  <string name=\"enable_openvpn\">Brug OpenVPN</string>\n  <string name=\"openvpn_settings_title\">OpenVPN-indstillinger</string>\n  <string name=\"openvpn_application_aborted\">Ret venligst OpenVPN-indstillinger først.</string>\n  <string name=\"openvpn_test_aborted\">Test afbrudt. Ret venligst OpenVPN-indstillinger først.</string>\n  <string name=\"openvpn_address_invalid\">OpenVPN-konfigurationsadresse ikke angivet.</string>\n  <string name=\"internet_censorship\">Internetcensur</string>\n  <string name=\"rename_section\">Omdøb denne sektion</string>\n  <string name=\"simple_edit_value_hint\">Indtast værdi</string>\n  <string name=\"seek_interval\">Spoleinterval</string>\n  <string name=\"seek_interval_sec\">%s sek.</string>\n  <string name=\"subtitle_system\">Systemstil (Android-indstillinger &gt; Tilgængelighed)</string>\n  <string name=\"subtitle_scale\">Undertekstskala</string>\n  <string name=\"audio_sync_fix_desc\">Et alternativ til synkronisering af lyd/video. Dette anses som forældet, men kan i nogle tilfælde hjælpe.</string>\n  <string name=\"audio_sync_fix\">Rettelse til lydsynkronisering</string>\n  <string name=\"ambilight_ratio_fix_desc\">Retter manglende bias-belysning, forkert billedformat eller videoskala og tomme skærmbilleder. Påvirker ydeevnen!</string>\n  <string name=\"ambilight_ratio_fix\">Rettelse til ambilight, billedformat, videoskala og skærmbilleder</string>\n  <string name=\"force_legacy_codecs_desc\">Forbedrer ydeevnen betydeligt på low-end-enheder. Maksimal opløsning er 720p.</string>\n  <string name=\"force_legacy_codecs\">Tving forældede codecs (720p)</string>\n  <string name=\"live_stream_fix_desc\">Advarsel: Denne indstilling deaktiverer tilbagespoling af streams. Forbedrer livestream-ydelsen betydeligt på low-end-enheder. Maksimal opløsning er 1080p.</string>\n  <string name=\"live_stream_fix_4k_desc\">Advarsel: Denne indstilling deaktiverer tilbagespoling af streams. Forbedrer livestream-ydelsen betydeligt. Maksimal opløsning er 4K. </string>\n  <string name=\"live_stream_fix\">Livestream-rettelse (1080p)</string>\n  <string name=\"live_stream_fix_4k\">Livestream-rettelse (4K)</string>\n  <string name=\"playback_notifications_fix_desc\">Skjuler notifikationer om sporskift. Kan være nyttig på AOSP-baseret firmware.</string>\n  <string name=\"playback_notifications_fix\">Deaktiver afspilningsnotifikationer</string>\n  <string name=\"amlogic_fix_desc\">Rettelse til fald i billedfrekvens på Amlogic-baserede enheder.</string>\n  <string name=\"amlogic_fix\">Rettelse til Amlogic 1080p\\@60fps</string>\n  <string name=\"tunneled_video_playback_desc\">Afspil videoer direkte til displayet uden, at SmartTube behandler den. Det giver bedre lyd- og billedsynkronisering samt jævnere afspilning. Bemærk: Pause fungerer muligvis ikke korrekt!</string>\n  <string name=\"tunneled_video_playback\">Tunnelet videoafspilning (kræver Android 5+)</string>\n  <string name=\"master_volume\">Global lydstyrke</string>\n  <string name=\"action_video_info\">Videobeskrivelse</string>\n  <string name=\"disable_ok_long_press_desc\">Beregnet til defekte fjernbetjeninger, hvor OK-knappen ikke fungerer ordentligt</string>\n  <string name=\"add_to_playback_queue\">Føj til afspilningskø</string>\n  <string name=\"remove_from_playback_queue\">Fjern fra afspilningskø</string>\n  <string name=\"msg_player_error_video_renderer\">Det valgte videoformat understøttes ikke.\\nPrøv at vælge et andet ved at trykke på HQ-knappen i afspilleren.\\nHvis det ikke hjælper, så prøv at genstarte enheden.</string>\n  <string name=\"msg_player_error_audio_renderer\">Det valgte lydformat understøttes ikke.\\nPrøv at vælge et andet ved at trykke på HQ-knappen i afspilleren.\\nHvis det ikke hjælper, så prøv at genstarte enheden.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">Det valgte undertekstformat understøttes ikke.\\nPrøv at vælge et andet ved at trykke på HQ-knappen i afspilleren.\\nHvis det ikke hjælper, så prøv at genstarte enheden.</string>\n  <string name=\"subtitle_white_transparent\">Hvid på gennemsigtig baggrund</string>\n  <string name=\"you_wont_see_this_channel\">Du ser ikke denne kanal i anbefalinger</string>\n  <string name=\"not_recommend_channel\">Anbefal ikke kanal</string>\n  <string name=\"video_buffer_size_none\">Ingen</string>\n  <string name=\"volume_limit\">Grænse for lydstyrke</string>\n  <string name=\"player_volume\">Lydstyrke</string>\n  <string name=\"play_video\">Afspil</string>\n  <string name=\"remember_position_of_short_videos\">Husk position af short videoer (mindre end 5 min.)</string>\n  <string name=\"player_show_tooltips\">Vis værktøjstip for knapper</string>\n  <string name=\"action_like_unset\">Fjernet \\'Kan lide\\'</string>\n  <string name=\"action_dislike_unset\">Fjernet \\'Kan ikke lide\\'</string>\n  <string name=\"various_buttons\">Knapper i toppen af hovedvinduet</string>\n  <string name=\"not_compatible_with\">Den valgte mulighed er ikke kompatibel med</string>\n  <string name=\"subtitle_position\">Undertekster bundafstand</string>\n  <string name=\"pressing_home\">ved at trykke på HJEM</string>\n  <string name=\"pressing_home_back\">ved at trykke på HJEM eller TILBAGE</string>\n  <string name=\"pressing_back\">ved at trykke på TILBAGE</string>\n  <string name=\"player_number_key_seek\">Spol med numeriske taster</string>\n  <string name=\"save_remove_playlist\">Tilføj/fjern playliste fra Playlister-sektionen</string>\n  <string name=\"remove_playlist\">Fjern playliste fra Playlister-sektionen permanent</string>\n  <string name=\"removed_from_playlists\">Fjernet fra Playlister-sektionen</string>\n  <string name=\"saved_to_playlists\">Føjet til Playlister-sektionen</string>\n  <string name=\"create_playlist\">Opret playliste</string>\n  <string name=\"add_video_to_new_playlist\">Føj til ny playliste</string>\n  <string name=\"cant_delete_empty_playlist\">Kan ikke slette tom playliste</string>\n  <string name=\"playlist\">Playliste</string>\n  <string name=\"rename_playlist\">Omdøb playliste</string>\n  <string name=\"cant_rename_empty_playlist\">Kan ikke omdøbe tom playliste</string>\n  <string name=\"cant_rename_foreign_playlist\">Kan ikke omdøbe fremmed playliste</string>\n  <string name=\"cant_save_playlist\">Denne playliste kan ikke tilføjes</string>\n  <string name=\"enter_value\">Indtast en ikke-tom værdi</string>\n  <string name=\"dialog_add_remove_from\">Tilføj/fjern fra %s</string>\n  <string name=\"lb_playback_controls_skip_next\">Spring næste over</string>\n  <string name=\"lb_playback_controls_skip_previous\">Spring forrige over/spol tilbage til starten</string>\n  <string name=\"alt_presets_behavior\">Alternativ adfærd for forudindstillinger (begræns båndbredde) </string>\n  <string name=\"alt_presets_behavior_desc\">SmartTube vil forsøge at opretholde båndbredden svarende til den valgte forudindstilling i stedet for at matche mellem opløsning, FPS og codec.</string>\n  <string name=\"sleep_timer\">Søvntimer (hvis fjernbetjeningen ikke bruges i én time)</string>\n  <string name=\"sleep_timer_desc\">Sæt afspilning på pause, hvis fjernbetjeningen ikke har været brugt i en time.</string>\n  <string name=\"disable_vsync\">Deaktiver VSync</string>\n  <string name=\"disable_vsync_desc\">Denne indstilling deaktiverer synkronisering af frames med skærmens vertikale signal. Dette kan forbedre ydeevnen på low-end enheder ved at frigøre CPU-ressourcer.</string>\n  <string name=\"skip_codec_profile_check\">Spring codec-tjek over</string>\n  <string name=\"skip_codec_profile_check_desc\">Tjek ikke codec-understøttelse, når du starter en video. Dette kan være nyttigt på fejlagtig firmware.</string>\n  <string name=\"force_sw_codec\">Tving videodekoder i software</string>\n  <string name=\"force_sw_codec_desc\">Kan afspille næsten alle videoer, men ydeevnen er meget dårlig.</string>\n  <string name=\"playlist_order\">Sortér playliste</string>\n  <string name=\"playlist_order_added_date_newer_first\">Tilføjelsesdato (nyeste først)</string>\n  <string name=\"playlist_order_added_date_older_first\">Tilføjelsesdato (ældste først)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Udgivelsesdato (nyeste først)</string>\n  <string name=\"playlist_order_published_date_older_first\">Udgivelsesdato (ældste først)</string>\n  <string name=\"playlist_order_popularity\">Popularitet</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">Kan ikke gøre dette på en fremmed playliste</string>\n  <string name=\"owned_playlist_warning\">Fejl: Denne handling kan kun foretages på egne playlister</string>\n  <string name=\"content_block_alt_server\">Brug alternativ server</string>\n  <string name=\"content_block_alt_server_desc\">Aktiver denne indstilling, hvis SponsorBlock nægter at virke.</string>\n  <string name=\"unset_stream_reminder\">Fjern stream-påmindelse</string>\n  <string name=\"set_stream_reminder\">Indstil stream-påmindelse</string>\n  <string name=\"playback_starts_shortly\">Afspilning starter automatisk, når streamen er klar</string>\n  <string name=\"starting_stream\">Starter stream…</string>\n  <string name=\"action_playlist_remove\">Fjern fra playliste</string>\n  <string name=\"signin_view_title\">Brugerkode indlæser…</string>\n  <string name=\"signin_view_description\">Indtast denne kode for at logge ind på siden %s.\\nBEMÆRK: Virker kun på Firefox- og Chrome-browsere.</string>\n  <string name=\"signin_view_action_text\">Færdig</string>\n  <string name=\"require_checked\">Kræver \\\"%s</string>\n  <string name=\"msg_press_again_to_exit\">Tryk igen for at afslutte</string>\n  <string name=\"player_remaining_time\">Resterende: %s</string>\n  <string name=\"player_ending_time\">Slutter %s</string>\n  <string name=\"badge_new_content\">NYT INDHOLD</string>\n  <string name=\"badge_live\">DIREKTE</string>\n  <string name=\"add_device_view_description\">For at tilknytte enheden skal du indtaste denne kode i din telefons YouTube-app under Indstillinger &gt; Se på tv\\nBEMÆRK: Dette virker kun med Gboard-tastaturet!</string>\n  <string name=\"playback_controls_repeat_pause\">Gentag pause</string>\n  <string name=\"playback_controls_repeat_list\">Gentag liste</string>\n  <string name=\"action_subscribe_off\">Abonner fra</string>\n  <string name=\"action_subscribe_on\">Abonner til</string>\n  <string name=\"skip_24_rate\">Spring 24fps-formater over (rettelse til ægte biograftilstand\\?)</string>\n  <string name=\"player_disable_suggestions\">Deaktiver forslag</string>\n  <string name=\"feedback\">Feedback</string>\n  <string name=\"sources\">Kilder</string>\n  <string name=\"releases\">Udgivelser</string>\n  <string name=\"prefer_avc_over_vp9\">Codec-valg: Foretræk AVC over VP9</string>\n  <string name=\"open_chat\">Chat/kommentarer</string>\n  <string name=\"open_comments\">Åbn kommentarer</string>\n  <string name=\"place_chat_left\">Placer chat til venstre</string>\n  <string name=\"use_alt_speech_recognizer\">Alternativ talegenkendelse</string>\n  <string name=\"time_format\">Tidsformat</string>\n  <string name=\"time_format_24\">24 timer</string>\n  <string name=\"time_format_12\">12 timer (AM/PM)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Seriøst fejlbehæftet. Brug kun, hvis du har problemer med standardgenkendelsen.</string>\n  <string name=\"speech_recognizer\">Talegenkendelse</string>\n  <string name=\"speech_recognizer_system\">System (foretrukket)</string>\n  <string name=\"speech_recognizer_external_1\">Ekstern #1 (seriøst fejlbehæftet. Deaktiver mikrofonadgang for at få den til at virke)</string>\n  <string name=\"speech_recognizer_external_2\">Ekstern #2 (seriøst fejlbehæftet)</string>\n  <string name=\"real_channel_icon\">Vis ikon på kanalknappen</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Gul på halvgennemsigtig baggrund</string>\n  <string name=\"subtitle_yellow_black\">Gul på sort baggrund</string>\n  <string name=\"player_pixel_ratio\">Pixelforhold</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Mørkegrå (monokrom)</string>\n  <string name=\"disable_mic_permission\">Deaktiver venligst mikrofonadgang for SmartTube for, at denne genkender kan fungere korrekt.</string>\n  <string name=\"repeat_mode_shuffle\">Bland enhver playliste</string>\n  <string name=\"chat_left\">Venstre</string>\n  <string name=\"chat_right\">Højre</string>\n  <string name=\"card_real_thumbnails\">Erstat miniaturebillede med et billede fra videoen</string>\n  <string name=\"card_content\">Hvorfra miniaturebilleder tages</string>\n  <string name=\"thumb_quality_default\">Standard</string>\n  <string name=\"thumb_quality_start\">Starten af videoen</string>\n  <string name=\"thumb_quality_middle\">Midten af videoen</string>\n  <string name=\"thumb_quality_end\">Slutningen af videoen</string>\n  <string name=\"content_block_status\">Tjek SponsorBlocks serverstatus</string>\n  <string name=\"player_show_quality_info_bitrate\">Føj bitrate til info om videokvalitet</string>\n  <string name=\"player_speed_button_old_behavior\">Gendan den gamle adfærd for hastighedsknappen</string>\n  <string name=\"protect_settings_with_password\">Beskyt alle indstillinger med en adgangskode</string>\n  <string name=\"enter_settings_password\">Indtast adgangskode til indstillinger</string>\n  <string name=\"child_mode\">Børnetilstand</string>\n  <string name=\"child_mode_desc\">I denne tilstand kan man ikke søge eller se foreslået indhold. Indstillinger er desuden beskyttet med en adgangskode.</string>\n  <string name=\"lost_setting_warning\">SmartTube-indstillingerne bliver ændret. Sørg for, at du har sikkerhedskopieret indstillingerne.</string>\n  <string name=\"player_button_long_click\">Langt tryk på denne knap for flere indstillinger</string>\n  <string name=\"pause_history\">Sæt historik på pause</string>\n  <string name=\"resume_history\">Genoptag historik</string>\n  <string name=\"disable_history\">Deaktiver historik</string>\n  <string name=\"enable_history\">Aktivér historik</string>\n  <string name=\"clear_history\">Ryd historik</string>\n  <string name=\"chapters\">Kapitler</string>\n  <string name=\"card_multiline_subtitle\">Flerlinjede undertekster</string>\n  <string name=\"auto_frame_rate_modes\">Understøttede tilstande</string>\n  <string name=\"loading\">Indlæser…</string>\n  <string name=\"hide_streams\">Skjul streams fra Abonnementer-sektionen</string>\n  <string name=\"video_rotate\">Roter</string>\n  <string name=\"trending_searches\">Populære søgninger</string>\n  <string name=\"video_duration_any\">Enhver</string>\n  <string name=\"video_duration\">Længde</string>\n  <string name=\"video_duration_under_4\">Under 4 minutter</string>\n  <string name=\"video_duration_between_4_20\">4-20 minutter</string>\n  <string name=\"video_duration_over_20\">Over 20 minutter</string>\n  <string name=\"content_type\">Type</string>\n  <string name=\"content_type_any\">Enhver</string>\n  <string name=\"content_type_video\">Video</string>\n  <string name=\"content_type_channel\">Kanal</string>\n  <string name=\"content_type_playlist\">Playliste</string>\n  <string name=\"content_type_movie\">Film</string>\n  <string name=\"video_features\">Funktioner</string>\n  <string name=\"video_feature_any\">Enhver</string>\n  <string name=\"video_feature_live\">Direkte</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">Sortér efter</string>\n  <string name=\"sort_by_relevance\">Relevans</string>\n  <string name=\"sort_by_views\">Visninger</string>\n  <string name=\"sort_by_date\">Dato for upload</string>\n  <string name=\"sort_by_rating\">Bedømmelse</string>\n  <string name=\"clear_search_history\">Ryd søgehistorik</string>\n  <string name=\"remove_from_subscriptions\">Skjul</string>\n  <string name=\"player_long_speed_list\">Lang hastighedsliste</string>\n  <string name=\"alt_app_icon\">Alternativt appikon (kræver genstart)</string>\n  <string name=\"network_stack\">Foretræk %s netværksstak</string>\n  <string name=\"player_network_stack\">Netværksstak</string>\n  <string name=\"cronet_desc\">Cronet er Chromium-netværksstakken. Den kan reducere forsinkelsen og øge netværkets ydeevne, hvilket kan hjælpe med bufferproblemer.</string>\n  <string name=\"unlock_all_formats\">Lås op for alle videoformater</string>\n  <string name=\"unlock_all_formats_desc\">På nogle enheder rapporterer firmwaren forkert nogle formater som ikke-understøttede, selv hvis de er.\\n(F.eks. har 1080p smart-TV\\'er en tendens til at rapportere 4K som ikke-understøttet.)</string>\n  <string name=\"okhttp_desc\">Denne netværksstak er den langsomste, men har god stabilitet.</string>\n  <string name=\"not_supported_by_device\">Enheden understøtter ikke denne funktion</string>\n  <string name=\"video_buffer_size_lowest\">Laveste</string>\n  <string name=\"video_buffer_size_highest\">Højeste</string>\n  <string name=\"player_exit_shortcut\">Afslut afspiller</string>\n  <string name=\"share_qr_link\">Del link (QR-kode)</string>\n  <string name=\"app_corner_clock\">Hjem: ur i øverste højre hjørne</string>\n  <string name=\"player_corner_clock\">Afspiller: ur i øverste højre hjørne</string>\n  <string name=\"player_corner_ending_time\">Afspiller: sluttid i øverste højre hjørne</string>\n  <string name=\"pin_playlist\">Føj playliste til sidepanelet</string>\n  <string name=\"pin_channel\">Føj kanal til sidepanelet</string>\n  <string name=\"unpin_group_from_sidebar\">Fjern abonnementsgruppen</string>\n  <string name=\"hide_shorts_channel\">Skjul shorts fra en kanal</string>\n  <string name=\"news_row_name\">Nyheder</string>\n  <string name=\"context_menu_sorting\">Sortering af kontekstmenu</string>\n  <string name=\"action_sound_off\">Lyd fra</string>\n  <string name=\"hide_upcoming_channel\">Skjul kommende fra Kanaler</string>\n  <string name=\"hide_upcoming_home\">Skjul kommende fra Hjem</string>\n  <string name=\"hide_shorts_from_search\">Skjul shorts fra søgeresultaterne</string>\n  <string name=\"hide_shorts_from_trending\">Skjul shorts fra Populært lige nu</string>\n  <string name=\"remember_position_of_live_videos\">Husk position af livestreams</string>\n  <string name=\"save_playlist\">Føj playliste til Playlister-sektionen</string>\n  <string name=\"skip_shorts\">Spring shorts over</string>\n  <string name=\"suggestions\">Forslag</string>\n  <string name=\"place_comments_left\">Placer kommentarer til venstre</string>\n  <string name=\"speech_engine\">Stemmesøgemaskine</string>\n  <string name=\"dearrow_status\">Tjek DeArrows serverstatus</string>\n  <string name=\"old_channel_look\">Det gamle udseende af Kanaler-sektionen</string>\n  <string name=\"remember_position_pinned\">Husk sidst sete position i de fastgjorte playlister</string>\n  <string name=\"unknown_source_error\">Ukendt kildefejl</string>\n  <string name=\"unknown_renderer_error\">Ukendt renderer-fejl</string>\n  <string name=\"color_scheme_dark_blue\">Mørkeblå</string>\n  <string name=\"player_speed_per_channel\">For hver kanal</string>\n  <string name=\"disable_popular_searches\">Deaktiver populære søgninger</string>\n  <string name=\"multi_profiles\">Brug separate indstillinger for hver konto</string>\n  <string name=\"protect_account_with_password\">Beskyt denne konto med adgangskode</string>\n  <string name=\"enter_account_password\">Indtast kontoens adgangskode</string>\n  <string name=\"show_connect_messages\">Vis forbindelsesbeskeder</string>\n  <string name=\"prefer_avc_over_vp9_desc\">ADVARSEL: maks. 1080p</string>\n  <string name=\"play_next\">Afspil næste</string>\n  <string name=\"hide_watched_from_subscriptions\">Skjul sete videoer fra Abonnementer</string>\n  <string name=\"hide_watched_from_notifications\">Skjul sete videoer fra Notifikationer</string>\n  <string name=\"hide_unwanted_content\">Skjul uønsket indhold</string>\n  <string name=\"hide_watched_from_home\">Skjul sete videoer fra Hjem</string>\n  <string name=\"hide_watched_from_watch_later\">Skjul sete videoer fra playlisten Se senere</string>\n  <string name=\"remote_control_permission\">Baggrundsfjernbetjening kræver Overlay-tilladelse</string>\n  <string name=\"login_from_browser\">Log ind fra browser</string>\n  <string name=\"disable_remote_history\">Deaktiver historik ved brug af fjernbetjening</string>\n  <string name=\"keyboard_fix\">Forhindr OK-knap i at åbne tastaturet (G20 og andre)</string>\n  <string name=\"nothing_found\">Intet fundet</string>\n  <string name=\"auto_frame_rate_desc\">Denne indstilling fjerner rystelser i scener, hvor kameraet bevæger sig hurtigt, f.eks. sportsstreaming.</string>\n  <string name=\"channel_filter_hint\">Filtrér kanaler</string>\n  <string name=\"player_loop_shorts\">Gentag shorts</string>\n  <string name=\"player_quick_shorts_skip\">Spring shorts over med venstre/højre knapperne</string>\n  <string name=\"player_quick_skip_videos\">Spring almindelige videoer over med venstre/højre knapperne</string>\n  <string name=\"channels_filter\">Vis \\\"Filtrér kanaler\\\"-feltet i Kanaler-sektionen</string>\n  <string name=\"channel_search_bar\">Søgefelt inden på Kanalsiden</string>\n  <string name=\"header_sports\">Sport</string>\n  <string name=\"keep_finished_activities\">Behold færdige aktiviteter</string>\n  <string name=\"disable_channels_service\">Deaktiver Kanaler-tjeneste</string>\n  <string name=\"replace_titles\">Erstat titler</string>\n  <string name=\"enable\">Aktivér</string>\n  <string name=\"more_info\">Mere info</string>\n  <string name=\"about_sponsorblock\">Om SponsorBlock</string>\n  <string name=\"about_dearrow\">Om DeArrow</string>\n  <string name=\"replace_thumbnails\">Erstat miniaturebilleder</string>\n  <string name=\"crowdsourced_thumbnails\">Crowdsourcede miniaturebilleder</string>\n  <string name=\"crowdsoursed_titles\">Crowdsourcede titler</string>\n  <string name=\"dearrow_not_submitted_thumbs\">Miniaturebilledkilde (hvis der ikke er nogen DeArrow-indsendelse)</string>\n  <string name=\"pitch_effect\">Pitch-effekt</string>\n  <string name=\"fullscreen_mode\">Fuldskærmstilstand (uden systembjælker)</string>\n  <string name=\"player_only_mode\">Vis kun afspilleren, hvis videoen åbnes uden for SmartTube</string>\n  <string name=\"pinned_channel_rows\">Vis fastgjorte kanaler som rækker</string>\n  <string name=\"prefer_ipv4\">Foretræk IPv4 DNS</string>\n  <string name=\"prefer_ipv4_desc\">Kan løse problemer, når SmartTube slet ikke virker.\\nBemærk: Kan forårsage frysninger og nedbrud (især på Android 8-enheder eller Dune HD).</string>\n  <string name=\"long_press_for_settings\">LANGT TRYK FOR INDSTILLINGER</string>\n  <string name=\"device_specific_backup\">Sikkerhedskopiering kun for denne enhed</string>\n  <string name=\"local_backup\">Lokal sikkerhedskopiering</string>\n  <string name=\"auto_backup\">Auto-sikkerhedskopiering (én gang dagligt)</string>\n  <string name=\"repeat_mode_reverse_list\">Afspil playliste eller kanalvideoer i omvendt rækkefølge</string>\n  <string name=\"calm_msg\">Vi er ved at løse problemet. Søg jævnligt efter opdateringer.</string>\n  <string name=\"without_picture\">Uden billede</string>\n  <string name=\"video_disabled\">Video deaktiveret</string>\n  <string name=\"applying_fix\">Luk ikke afspilleren. Anvender rettelsen…</string>\n  <string name=\"hide_mixes\">Skjul mixes</string>\n  <string name=\"player_global_focus_desc\">Denne funktion påvirker, hvilken knap får fokus, når man navigerer mellem rækker af afspillerknapper.</string>\n  <string name=\"disable_network_error_fixing\">Deaktiver automatisk rettelse af netværksfejl</string>\n  <string name=\"disable_network_error_fixing_desc\">Du skal sikkert aktivere denne indstilling, hvis du bruger en VPN.</string>\n  <string name=\"recommended\">Anbefalet</string>\n  <string name=\"add_to_subscriptions_group\">Tilføj/fjern fra abonnementsgruppe</string>\n  <string name=\"new_subscriptions_group\">Ny gruppe</string>\n  <string name=\"rename_group\">Omdøb abonnementsgruppen</string>\n  <string name=\"screen_dimming_amount\">Skærmdæmpningsniveau</string>\n  <string name=\"screen_dimming_timeout\">Timeout for skærmdæmpning</string>\n  <string name=\"playlists_rows\">Vis Playlister-sektionen som rækker</string>\n  <string name=\"import_subscriptions_group\">Importér</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">Deaktiver undertekster</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">Aktivér undertekster</string>\n  <string name=\"my_videos\">Mine videoer</string>\n  <string name=\"player_extra_long_speed_list\">Ekstra lang hastighedsliste</string>\n  <string name=\"long_press_for_options\">LANGT TRYK FOR VALGMULIGHEDER</string>\n  <string name=\"original_lang\">Original</string>\n  <string name=\"search_exit_shortcut\">Afslut søgning</string>\n  <string name=\"remove_playlist_fmt\">Fjern %s permanent\\?</string>\n  <string name=\"video_flip\">Vend (spejl)</string>\n  <string name=\"player_chapter_notification2\">Skift til næste kapitel ved at klikke på notifikationen</string>\n  <string name=\"player_audio_focus\">Lydfokus (pause, hvis andre afspillere registreres)</string>\n  <string name=\"paid_content_notification\">Notifikation om betalt indhold</string>\n  <string name=\"you_liked\">du kan lide</string>\n  <string name=\"premium_users_only\">Kun premium-brugere. Rettelse af ufuldstændig liste over videoformater</string>\n  <string name=\"playback_buffering_fix\">Rettelse af afspilningsbuffering</string>\n  <string name=\"oculus_quest_fix\">Rettelse af Oculus Quest</string>\n  <string name=\"card_preview_muted\">Video uden lyd</string>\n  <string name=\"card_preview_full\">Video med lyd</string>\n  <string name=\"card_preview\">Kortforhåndsvisning</string>\n  <string name=\"card_unlocalized_titles\">Ikke-lokaliserede videotitler</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">Skaler ikke video til at passe dialog</string>\n  <string name=\"typing_corrections\">Deaktiver autokorrektion under indtastning</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-de/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <string name=\"header_home\">Start</string>\r\n    <string name=\"title_search\">Suche</string>\r\n    <string name=\"header_subscriptions\">Abos</string>\r\n    <string name=\"header_history\">Verlauf</string>\r\n    <string name=\"header_music\">Musik</string>\r\n    <string name=\"header_news\">Nachrichten</string>\r\n    <string name=\"header_gaming\">Spiele</string>\r\n    <string name=\"header_playlists\">Wiedergabelisten</string>\r\n    <string name=\"header_settings\">Einstellungen</string>\r\n    <string name=\"header_channels\">Kanäle</string>\r\n    <string name=\"subscriptions_signin_title\">Abos anzeigen</string>\r\n    <string name=\"subscriptions_signin_subtitle\">Melden Sie sich an, um Ihre Abos anzuzeigen</string>\r\n    <string name=\"subscriptions_signin_button_text\">ANMELDEN</string>\r\n    <string name=\"library_signin_to_show_more\">Melden Sie sich an, um diese Inhalte anzuzeigen</string>\r\n    <string name=\"library_signin_subtitle\">Melden Sie sich an, um Ihre Wiedergabelisten anzuzeigen</string>\r\n    <string name=\"action_signin\">ANMELDEN</string>\r\n    <string name=\"title_video_formats\">Video-Formate</string>\r\n    <string name=\"title_audio_formats\">Audio-Formate</string>\r\n    <string name=\"subtitle_category_title\">Untertitel</string>\r\n    <string name=\"playback_queue_category_title\">Warteschlange</string>\r\n    <string name=\"video_max_quality\">Auto (maximale Qualität)</string>\r\n    <string name=\"audio_max_quality\">Auto (maximale Qualität)</string>\r\n    <string name=\"subtitles_disabled\">Untertitel deaktiviert</string>\r\n    <string name=\"auto_frame_rate\">Automatische Bildrate</string>\r\n    <string name=\"frame_rate_correction\">Bildrate korrigieren:\\n%s</string>\r\n    <string name=\"category_background_playback\">Hintergrund-Wiedergabe</string>\r\n    <string name=\"not_implemented\">Nicht implementiert</string>\r\n    <string name=\"not_supported_by_device\">Das Gerät unterstützt diese Funktion nicht</string>\r\n    <string name=\"option_background_playback_off\">Deaktiviert</string>\r\n    <string name=\"option_background_playback_pip\">Bild in Bild</string>\r\n    <string name=\"option_background_playback_only_audio\">Nur Audio</string>\r\n    <string name=\"playback_settings\">Wiedergabequalität-Einstellungen</string>\r\n    <string name=\"video_speed\">Video-Geschwindigkeit</string>\r\n    <string name=\"resolution_switch\">Auflösung umschalten</string>\r\n    <string name=\"video_buffer\">Video-Puffer</string>\r\n    <string name=\"video_buffer_size_none\">Keiner</string>\r\n    <string name=\"video_buffer_size_lowest\">Sehr niedrig</string>\r\n    <string name=\"video_buffer_size_low\">Niedrig</string>\r\n    <string name=\"video_buffer_size_med\">Mittel</string>\r\n    <string name=\"video_buffer_size_high\">Hoch</string>\r\n    <string name=\"video_buffer_size_highest\">Sehr hoch</string>\r\n    <string name=\"update_changelog\">Änderungsprotokoll</string>\r\n    <string name=\"install_update\">Update installieren</string>\r\n    <string name=\"section_is_empty\">Hier ist nichts drin</string>\r\n    <string name=\"dialog_add_to_playlist\">Hinzufügen/Entfernen aus Wiedergabeliste</string>\r\n    <string name=\"msg_signed_users_only\">Nur angemeldete Nutzer</string>\r\n    <string name=\"msg_cant_load_content\">Inhalt kann nicht geladen werden.\\n1) Aktivieren Sie den Wiedergabeverlauf.\\n2) Überprüfen Sie Datum und Uhrzeit auf dem Gerät.\\n3) Überprüfen Sie die Internetverbindung.\\n4) Überprüfen Sie Ihre Proxy-Einstellungen.\\n5) Versuchen Sie, sich mit Ihrem Konto anzumelden.\\n6) Vielleicht ist hier nichts drin.</string>\r\n    <string name=\"title_video_presets\">Video-Voreinstellungen</string>\r\n    <string name=\"settings_accounts\">Konten</string>\r\n    <string name=\"settings_left_panel\">Kategorien bearbeiten</string>\r\n    <string name=\"settings_themes\">Themen</string>\r\n    <string name=\"settings_other\">Sonstige</string>\r\n    <string name=\"settings_player\">Player</string>\r\n    <string name=\"settings_language\">Sprache</string>\r\n    <string name=\"settings_linked_devices\">Verbundene Geräte</string>\r\n    <string name=\"settings_about\">Über</string>\r\n    <string name=\"dialog_account_list\">Konto auswählen</string>\r\n    <string name=\"dialog_account_none\">Keine</string>\r\n    <string name=\"dialog_remove_account\">Abmelden</string>\r\n    <string name=\"dialog_add_account\">Anmelden</string>\r\n    <string name=\"default_lang\">Standard</string>\r\n    <string name=\"original_lang\">Original</string>\r\n    <string name=\"dialog_select_language\">Sprache</string>\r\n    <string name=\"subtitle_default\">Standard</string>\r\n    <string name=\"subtitle_white_semi_transparent\">Weiß auf halbtransparentem Hintergrund</string>\r\n    <string name=\"subtitle_style\">Untertitel-Stil</string>\r\n    <string name=\"subtitle_language\">Untertitel-Sprache</string>\r\n    <string name=\"action_search\">Suche</string>\r\n    <string name=\"settings_main_ui\">Oberfläche</string>\r\n    <string name=\"dialog_main_ui\">Oberfläche</string>\r\n    <string name=\"card_animated_previews\">Animierte Vorschau</string>\r\n    <string name=\"web_site\">Webseite</string>\r\n    <string name=\"donation\">Spende</string>\r\n    <string name=\"dialog_about\">Über</string>\r\n    <string name=\"dialog_player_ui\">Video-Player</string>\r\n    <string name=\"player_show_ui_on_pause\">Oberfläche bei Pause anzeigen</string>\r\n    <string name=\"player_pause_on_ok\">OK-Taste pausiert die Wiedergabe</string>\r\n    <string name=\"player_ok_button_behavior\">Verhalten der OK-Taste</string>\r\n    <string name=\"player_only_ui\">Nur Oberfläche</string>\r\n    <string name=\"player_ui_and_pause\">Oberfläche und Pause</string>\r\n    <string name=\"player_only_pause\">Nur Pause</string>\r\n    <string name=\"player_toggle_speed\">Geschwindigkeit umschalten</string>\r\n    <string name=\"check_for_updates\">Nach Updates suchen</string>\r\n    <string name=\"update_not_found\">Sie verwenden die neueste Version</string>\r\n    <string name=\"update_in_progress\">Bitte warten…</string>\r\n    <string name=\"player_ui_hide_behavior\">Oberfläche automatisch ausblenden</string>\r\n    <string name=\"option_never\">Niemals</string>\r\n    <string name=\"side_panel_sections\">Kategorien einrichten</string>\r\n    <string name=\"boot_to_section\">Starte in Kategorie</string>\r\n    <string name=\"large_ui\">Große Oberfläche</string>\r\n    <string name=\"video_grid_scale\">Video-Raster skalieren</string>\r\n    <string name=\"scale_ui\">Oberfläche skalieren</string>\r\n    <string name=\"color_scheme\">Farbschema</string>\r\n    <string name=\"color_scheme_default\">Standard</string>\r\n    <string name=\"color_scheme_red_grey\">Rot-Grau</string>\r\n    <string name=\"color_scheme_red\">Rot</string>\r\n    <string name=\"color_scheme_dark_grey\">Dunkel</string>\r\n    <string name=\"disable_update_check\">Update-Prüfung deaktivieren</string>\r\n    <string name=\"show_again\">Erneut anzeigen</string>\r\n    <string name=\"check_updates_auto\">Über Update benachrichtigen</string>\r\n    <string name=\"select_account_on_boot\">Kontoauswahl beim Start anzeigen</string>\r\n    <string name=\"player_other\">Sonstige</string>\r\n    <string name=\"player_full_date\">Genaues Datum in der Beschreibung</string>\r\n    <string name=\"open_channel\">Kanal öffnen</string>\r\n    <string name=\"open_playlist\">Wiedergabeliste öffnen</string>\r\n    <string name=\"not_interested\">Kein Interesse</string>\r\n    <string name=\"not_recommend_channel\">Kanal nicht empfehlen</string>\r\n    <string name=\"you_wont_see_this_video\">Video wurde aus den Empfehlungen entfernt</string>\r\n    <string name=\"you_wont_see_this_channel\">Kanal wird nicht mehr in den Empfehlungen angezeigt</string>\r\n    <string name=\"settings_search\">Suche</string>\r\n    <string name=\"dialog_search\">Suche</string>\r\n    <string name=\"instant_voice_search\">Sprachsuche sofort starten</string>\r\n    <string name=\"option_background_playback_behind\">Im Hintergrund abspielen</string>\r\n    <string name=\"next_video_info_is_not_loaded_yet\">Informationen zum nächsten Video wurden noch nicht geladen</string>\r\n    <string name=\"card_multiline_title\">Mehrzeilige Titel</string>\r\n    <string name=\"cards_style\">Karten-Stil</string>\r\n    <string name=\"repeat_mode_all\">Videos fortlaufend abspielen</string>\r\n    <string name=\"repeat_mode_one\">Aktuelles Video wiederholen</string>\r\n    <string name=\"repeat_mode_pause\">Wiedergabe nach jedem Video pausieren (außer Warteschlange)</string>\r\n    <string name=\"repeat_mode_pause_alt\">Nur Videos der Wiedergabeliste fortlaufend abspielen</string>\r\n    <string name=\"repeat_mode_none\">Wiedergabe nach einem Video stoppen (außer Warteschlange)</string>\r\n    <string name=\"subscribed_to_channel\">Abonniert</string>\r\n    <string name=\"unsubscribed_from_channel\">Nicht abonniert</string>\r\n    <string name=\"subtitle_yellow_transparent\">Gelb auf transparentem Hintergrund</string>\r\n    <string name=\"subtitle_white_transparent\">Weiß auf transparentem Hintergrund</string>\r\n    <string name=\"subtitle_white_black\">Weiß auf schwarzem Hintergrund</string>\r\n    <string name=\"player_seek_preview\">Vorschau beim Spulen anzeigen</string>\r\n    <string name=\"color_scheme_dark_grey_oled\">Dunkel (OLED)</string>\r\n    <string name=\"channels_section_sorting\">Kanäle-Kategorie sortieren</string>\r\n    <string name=\"sorting_by_new_content\">Neue Inhalte</string>\r\n    <string name=\"sorting_alphabetically\">Alphabetisch</string>\r\n    <string name=\"sorting_last_viewed\">Zuletzt angesehen</string>\r\n    <string name=\"player_pause_when_seek\">Pause beim Spulen</string>\r\n    <string name=\"playlists_style\">Wiedergabelisten-Stil</string>\r\n    <string name=\"playlists_style_grid\">Raster</string>\r\n    <string name=\"playlists_style_rows\">Reihe</string>\r\n    <string name=\"player_show_clock\">Uhr im Bedienfeld anzeigen</string>\r\n    <string name=\"player_show_remaining_time\">Verbleibende Zeit im Bedienfeld anzeigen</string>\r\n    <string name=\"player_show_ending_time\">Endzeit im Bedienfeld anzeigen</string>\r\n    <string name=\"open_channel_uploads\">Kanal-Videos öffnen</string>\r\n    <string name=\"app_exit_shortcut\">App beenden</string>\r\n    <string name=\"player_exit_shortcut\">Player beenden</string>\r\n    <string name=\"search_exit_shortcut\">Suche beenden</string>\r\n    <string name=\"app_exit_none\">Nicht beenden</string>\r\n    <string name=\"app_double_back_exit\">2x Zurück-Taste</string>\r\n    <string name=\"app_single_back_exit\">1x Zurück-Taste</string>\r\n    <string name=\"action_video_zoom\">Video-Zoom</string>\r\n    <string name=\"video_zoom\">Video-Zoom</string>\r\n    <string name=\"video_zoom_default\">Standard</string>\r\n    <string name=\"video_zoom_fit_width\">Breite anpassen</string>\r\n    <string name=\"video_zoom_fit_height\">Höhe anpassen</string>\r\n    <string name=\"video_zoom_fit_both\">Breite oder Höhe anpassen</string>\r\n    <string name=\"video_zoom_stretch\">Strecken</string>\r\n    <string name=\"color_scheme_teal\">Blaugrün</string>\r\n    <string name=\"color_scheme_teal_oled\">Blaugrün (OLED)</string>\r\n    <string name=\"player_seek_preview_none\">Keine Vorschau</string>\r\n    <string name=\"player_seek_preview_single\">Einzelbild</string>\r\n    <string name=\"player_seek_preview_carousel\">Mehrere Bilder</string>\r\n    <string name=\"player_seek_preview_carousel_slow\">Mehrere Bilder (langsam, nach Keyframes)</string>\r\n    <string name=\"player_seek_preview_carousel_fast\">Mehrere Bilder (schnell, ungenaue Vorschau)</string>\r\n    <string name=\"unsubscribe_from_channel\">Kanal nicht mehr abonnieren</string>\r\n    <string name=\"card_title_lines_num\">Zeilenanzahl in Titeln</string>\r\n    <string name=\"card_auto_scrolled_title\">Auto-Scrollen in Titeln</string>\r\n    <string name=\"share_link\">Link teilen</string>\r\n    <string name=\"share_qr_link\">Link teilen (QR-Code)</string>\r\n    <string name=\"subscribe_to_channel\">Kanal abonnieren</string>\r\n    <string name=\"wait_data_loading\">Bitte warten, während die Daten geladen werden…</string>\r\n    <string name=\"auto_frame_rate_pause\">Automatische Bildrate (Pause beim Umschalten)</string>\r\n    <string name=\"auto_frame_rate_applying\">Automatische Bildrate wird angewendet %sx%s\\@%s</string>\r\n    <string name=\"audio_shift\">Audio-Verschiebung</string>\r\n    <string name=\"player_remember_speed\">Video-Geschwindigkeit merken</string>\r\n    <string name=\"mark_channel_as_watched\">Als gesehen markieren</string>\r\n    <string name=\"channel_marked_as_watched\">Kanal wurde als gesehen markiert</string>\r\n    <string name=\"dialog_add_device\">Gerät hinzufügen</string>\r\n    <string name=\"dialog_remove_all_devices\">Alle Geräte entfernen</string>\r\n    <string name=\"device_link_enabled\">Benachrichtigungen anzeigen</string>\r\n    <string name=\"device_connected\">Gerät \\\"%s\\\" wurde verbunden</string>\r\n    <string name=\"device_disconnected\">Gerät \\\"%s\\\" wurde getrennt</string>\r\n    <string name=\"settings_ui_scale\">Anzeigegröße</string>\r\n    <string name=\"msg_restart_app\">Bitte starten Sie die App neu, um diese Einstellungen zu übernehmen</string>\r\n    <string name=\"settings_block\">Inhalte blockieren</string>\r\n    <string name=\"msg_applying\">%s aktiv</string>\r\n    <string name=\"msg_done\">Fertig</string>\r\n    <string name=\"settings_general\">Allgemein</string>\r\n    <string name=\"settings_video\">Video</string>\r\n    <string name=\"content_block_confirm_skip\">Überspringen bestätigen</string>\r\n    <string name=\"content_block_categories\">Segmente überspringen</string>\r\n    <string name=\"content_block_sponsor\">Sponsor</string>\r\n    <string name=\"content_block_intro\">Unterbrechung/Intro</string>\r\n    <string name=\"content_block_outro\">Endkarten/Credits</string>\r\n    <string name=\"content_block_interaction\">Interaktions-Erinnerungen</string>\r\n    <string name=\"content_block_self_promo\">Unbezahlt/Eigenwerbung</string>\r\n    <string name=\"content_block_music_off_topic\">Nicht-Musik-Abschnitt</string>\r\n    <string name=\"confirm_segment_skip\">Überspringe Segment \\\"%s\\\"\\?</string>\r\n    <string name=\"cancel_segment_skip\">Segment nicht überspringen</string>\r\n    <string name=\"msg_skipping_segment\">Überspringe Segment \\\"%s\\\"…</string>\r\n    <string name=\"content_block_notification_type\">Benachrichtigungstyp</string>\r\n    <string name=\"content_block_notify_none\">Ohne Benachrichtigung</string>\r\n    <string name=\"content_block_notify_toast\">Toast-Meldung</string>\r\n    <string name=\"content_block_notify_dialog\">Bestätigungs-Dialog</string>\r\n    <string name=\"return_to_launcher\">Rückkehr zum ATV-Launcher/Suche</string>\r\n    <string name=\"intent_force_close\">Beenden erzwingen, wenn Video von externer Quelle geöffnet wurde</string>\r\n    <string name=\"btn_confirm\">Bestätigen</string>\r\n    <string name=\"player_low_video_quality\">Niedrige Videoqualität</string>\r\n    <string name=\"remote_session_closed\">Remote-Sitzung wurde beendet</string>\r\n    <string name=\"msg_mode_switch_error\">Anzeige kann nicht auf \\\"%s\\\" umgeschaltet werden</string>\r\n    <string name=\"msg_player_error\">Wiedergabefehler %s. Neu starten…</string>\r\n    <string name=\"video_preset_disabled\">Keine Voreinstellung</string>\r\n    <string name=\"video_preset_enabled\">Format des nächsten Videos wird gemäß der Video-Voreinstellung festgelegt</string>\r\n    <string name=\"player_sleep_timer\">Sleep-Timer</string>\r\n    <string name=\"player_show_quality_info\">Video-Qualität im Bedienfeld anzeigen</string>\r\n    <string name=\"player_remember_each_speed\">Video-Geschwindigkeit merken (pro Video)</string>\r\n    <string name=\"player_remember_speed_none\">Nicht merken</string>\r\n    <string name=\"player_remember_speed_all\">Gleiche Geschwindigkeit für alle Videos</string>\r\n    <string name=\"player_remember_speed_each\">Spezifische Geschwindigkeit pro Video</string>\r\n    <string name=\"msg_player_error_source\">Video kann nicht geladen werden</string>\r\n    <string name=\"msg_player_error_renderer\">Das ausgewählte Format wird nicht unterstützt.\\nVersuchen Sie, ein anderes Format auszuwählen.\\nWenn dies nicht hilft, versuchen Sie, das Gerät neu zu starten.</string>\r\n    <string name=\"msg_player_error_video_renderer\">Das ausgewählte VIDEO-Format wird nicht unterstützt.\\nVersuchen Sie, ein anderes Format auszuwählen, indem Sie die HQ-Taste im Player drücken.\\nWenn dies nicht hilft, versuchen Sie, das Gerät neu zu starten.</string>\r\n    <string name=\"msg_player_error_audio_renderer\">Das ausgewählte AUDIO-Format wird nicht unterstützt.\\nVersuchen Sie, ein anderes Format auszuwählen, indem Sie die HQ-Taste im Player drücken.\\nWenn dies nicht hilft, versuchen Sie, das Gerät neu zu starten.</string>\r\n    <string name=\"msg_player_error_subtitle_renderer\">Das ausgewählte UNTERTITEL-Format wird nicht unterstützt.\\nVersuchen Sie, ein anderes Format auszuwählen, indem Sie die CC-Taste im Player lange drücken.\\nWenn dies nicht hilft, versuchen Sie, das Gerät neu zu starten.</string>\r\n    <string name=\"msg_player_error_unexpected\">Unbekannter Video-Decoder-Fehler</string>\r\n    <string name=\"video_aspect\">Seitenverhältnis</string>\r\n    <string name=\"player_tweaks\">Entwickleroptionen</string>\r\n    <string name=\"header_uploads\">Uploads</string>\r\n    <string name=\"player_show_global_clock\">Uhr immer auf dem Bildschirm anzeigen</string>\r\n    <string name=\"player_show_global_ending_time\">Endzeit immer auf dem Bildschirm anzeigen</string>\r\n    <string name=\"app_corner_clock\">Start: Uhr in der rechten oberen Ecke</string>\r\n    <string name=\"player_corner_clock\">Player: Uhr in der rechten oberen Ecke</string>\r\n    <string name=\"player_corner_ending_time\">Player: Endzeit in der rechten oberen Ecke</string>\r\n    <string name=\"uploads_old_look\">Altes Aussehen der Uploads-Kategorie</string>\r\n    <string name=\"background_playback_activation\">Hintergrund-Wiedergabe (Taste)</string>\r\n    <string name=\"channels_old_look\">Altes Aussehen der Kanäle-Kategorie</string>\r\n    <string name=\"channels_auto_load\">Inhalt der Kanäle-Kategorie automatisch laden</string>\r\n    <string name=\"return_to_background_video\">Zurück zum Video im Hintergrund</string>\r\n    <string name=\"pin_unpin_from_sidebar\">Hinzufügen/Entfernen aus Seitenleiste</string>\r\n    <string name=\"pin_unpin_playlist\">Wiedergabeliste zur Seitenleiste hinzufügen/entfernen</string>\r\n    <string name=\"pin_unpin_channel\">Kanal zur Seitenleiste hinzufügen/entfernen</string>\r\n    <string name=\"pin_playlist\">Wiedergabeliste zur Seitenleiste hinzufügen</string>\r\n    <string name=\"pin_channel\">Kanal zur Seitenleiste hinzufügen</string>\r\n    <string name=\"pinned_to_sidebar\">Zur Seitenleiste hinzugefügt</string>\r\n    <string name=\"unpin_from_sidebar\">Entfernen aus Seitenleiste</string>\r\n    <string name=\"unpin_group_from_sidebar\">Abo-Gruppe entfernen</string>\r\n    <string name=\"unpinned_from_sidebar\">Aus Seitenleiste entfernt</string>\r\n    <string name=\"dialog_select_country\">Land</string>\r\n    <string name=\"settings_language_country\">Sprache/Land</string>\r\n    <string name=\"share_embed_link\">Link zum Einbetten teilen</string>\r\n    <string name=\"card_text_scroll_factor\">Scrollgeschwindigkeit des Kartentextes</string>\r\n    <string name=\"preferred_update_source\">Update-Quelle auswählen</string>\r\n    <string name=\"hide_shorts\">Shorts in Abos ausblenden</string>\r\n    <string name=\"hide_shorts_channel\">Shorts in Kanäle ausblenden</string>\r\n    <string name=\"key_remapping\">Tastenbelegung anpassen</string>\r\n    <string name=\"screen_dimming\">Bildschirm dimmen</string>\r\n    <string name=\"removed_from_playback_queue\">Aus Warteschlange entfernt</string>\r\n    <string name=\"added_to_playback_queue\">Zur Warteschlange hinzugefügt</string>\r\n    <string name=\"add_remove_from_playback_queue\">Hinzufügen/Entfernen aus Warteschlange</string>\r\n    <string name=\"add_to_playback_queue\">Hinzufügen zur Warteschlange</string>\r\n    <string name=\"remove_from_playback_queue\">Entfernen aus Warteschlange</string>\r\n    <string name=\"proxy_enabled\">Proxy aktiviert</string>\r\n    <string name=\"proxy_disabled\">Proxy deaktiviert</string>\r\n    <string name=\"uploads_row_name\">Uploads</string>\r\n    <string name=\"playlists_row_name\">Erstellte Wiedergabelisten</string>\r\n    <string name=\"popular_uploads_row_name\">Beliebte Uploads</string>\r\n    <string name=\"news_row_name\">Nachrichten</string>\r\n    <string name=\"breaking_news_row_name\">Eilmeldungen</string>\r\n    <string name=\"covid_news_row_name\">Nachrichten zu COVID-19</string>\r\n    <string name=\"enable_voice_search\">Globale Sprachsuche aktivieren (Firmware-Unterstützung erforderlich)</string>\r\n    <string name=\"subscribe_unsubscribe_from_channel\">Kanal abonnieren/nicht mehr abonnieren</string>\r\n    <string name=\"app_backup_restore\">Sichern/Wiederherstellen</string>\r\n    <string name=\"app_backup\">App-Daten sichern</string>\r\n    <string name=\"app_restore\">App-Daten wiederherstellen</string>\r\n    <string name=\"skip_each_segment_once\">Jedes Segment nur einmal überspringen</string>\r\n    <string name=\"disable_ok_long_press\">Langes Drücken der OK-Taste deaktivieren</string>\r\n    <string name=\"disable_ok_long_press_desc\">Vorgesehen für fehlerhafte Fernbedienungen, bei denen die OK-Taste nicht richtig funktioniert</string>\r\n    <string name=\"player_time_correction\">Anzeige der Zeit in Abhängigkeit von der Geschwindigkeit</string>\r\n    <string name=\"sponsor_color_markers\">Farbige Markierungen auf dem Fortschrittsbalken</string>\r\n    <string name=\"refresh_section\">Kategorie aktualisieren</string>\r\n    <string name=\"option_disabled\">Deaktiviert</string>\r\n    <string name=\"live_now_row_name\">Jetzt live</string>\r\n    <string name=\"run_in_background\">Hintergrund-Wiedergabe</string>\r\n    <string name=\"settings_remote_control\">Fernsteuerung</string>\r\n    <string name=\"background_service_started\">Hintergrunddienst gestartet</string>\r\n    <string name=\"dialog_add_to\">Hinzufügen zu %s</string>\r\n    <string name=\"dialog_remove_from\">Entfernen aus %s</string>\r\n    <string name=\"added_to\">Hinzugefügt zu %s</string>\r\n    <string name=\"removed_from\">Entfernt aus %s</string>\r\n    <string name=\"video_preset_adaptive\">Adaptiv</string>\r\n    <string name=\"content_block_preview_recap\">Vorschau/Zusammenfassung</string>\r\n    <string name=\"content_block_highlight\">Highlights</string>\r\n    <string name=\"removed_from_history\">Video wurde aus dem Verlauf entfernt</string>\r\n    <string name=\"remove_from_history\">Aus dem Verlauf entfernen</string>\r\n    <string name=\"upload_date\">Upload-Datum</string>\r\n    <string name=\"upload_date_any\">Beliebig</string>\r\n    <string name=\"upload_date_today\">Heute</string>\r\n    <string name=\"upload_date_this_week\">Diese Woche</string>\r\n    <string name=\"upload_date_this_month\">Dieser Monat</string>\r\n    <string name=\"upload_date_this_year\">Dieses Jahr</string>\r\n    <string name=\"double_refresh_rate\">Doppelte Bildrate</string>\r\n    <string name=\"upload_date_last_hour\">Letzte Stunde</string>\r\n    <string name=\"focus_on_search_results\">Autofokus auf Suchergebnisse</string>\r\n    <string name=\"context_menu\">Kontextmenü</string>\r\n    <string name=\"context_menu_sorting\">Kontextmenü-Sortierung</string>\r\n    <string name=\"add_remove_from_recent_playlist\">Hinzufügen/Entfernen aus letzter Wiedergabeliste</string>\r\n    <string name=\"hide_settings_section\">Einstellungen-Kategorie ausblenden (gefährlich!)</string>\r\n    <string name=\"volume\">Lautstärke %s%%</string>\r\n    <string name=\"auto_frame_rate_sec\">%s sek.</string>\r\n    <string name=\"audio_shift_sec\">%s sek.</string>\r\n    <string name=\"ui_hide_timeout_sec\">%s sek.</string>\r\n    <string name=\"screen_dimming_timeout_min\">%s min.</string>\r\n    <string name=\"mark_all_channels_watched\">Alle Kanäle als gesehen markieren</string>\r\n    <string name=\"move_section_up\">Kategorie nach oben verschieben</string>\r\n    <string name=\"move_section_down\">Kategorie nach unten verschieben</string>\r\n    <string name=\"player_buttons\">Player-Tasten einrichten</string>\r\n    <string name=\"action_playlist_add\">Zur Wiedergabeliste hinzufügen</string>\r\n    <string name=\"action_video_stats\">Video-Statistiken</string>\r\n    <string name=\"action_subscribe\">Kanal abonnieren</string>\r\n    <string name=\"action_channel\">Kanal öffnen</string>\r\n    <string name=\"action_pip\">Bild-in-Bild</string>\r\n    <string name=\"action_video_info\">Video-Beschreibung</string>\r\n    <string name=\"action_screen_off\">Bildschirm aus</string>\r\n    <string name=\"action_sound_off\">Stummschalten</string>\r\n    <string name=\"action_playback_queue\">Warteschlange</string>\r\n    <string name=\"action_video_speed\">Video-Geschwindigkeit</string>\r\n    <string name=\"action_subtitles\">Untertitel anzeigen</string>\r\n    <string name=\"action_like\">Mag ich</string>\r\n    <string name=\"action_dislike\">Mag ich nicht</string>\r\n    <string name=\"action_play_pause\">Wiedergabe/Pause</string>\r\n    <string name=\"action_repeat_mode\">Wiedergabe-Modus</string>\r\n    <string name=\"action_next\">Nächstes Video</string>\r\n    <string name=\"action_previous\">Vorheriges Video</string>\r\n    <string name=\"action_high_quality\">Video-Qualität</string>\r\n    <string name=\"content_block_no_skipping_mode\">Nicht-Überspringen-Modus</string>\r\n    <string name=\"content_block_action_type\">Aktion wählen</string>\r\n    <string name=\"content_block_action_none\">Nichts tun</string>\r\n    <string name=\"content_block_action_only_skip\">Nur Überspringen</string>\r\n    <string name=\"content_block_action_toast\">Überspringen mit Benachrichtigung</string>\r\n    <string name=\"content_block_action_dialog\">Bestätigungsdialog anzeigen</string>\r\n    <string name=\"content_block_filler\">Füller/Witze</string>\r\n    <string name=\"keyboard_auto_show\">Tastatur automatisch anzeigen</string>\r\n    <string name=\"cancel_dialog\">Abbrechen</string>\r\n    <string name=\"msg_player_error_source2\">URL funktioniert nicht oder falsche Geräte-Uhrzeit.\\nNormalerweise ist es ein Fehler in der App.</string>\r\n    <string name=\"msg_player_error_video_source\">VIDEO URL funktioniert nicht oder falsche Geräte-Uhrzeit.\\nNormalerweise ist es ein Fehler in der App.</string>\r\n    <string name=\"msg_player_error_audio_source\">AUDIO URL funktioniert nicht oder falsche Geräte-Uhrzeit.\\nNormalerweise ist es ein Fehler in der App.</string>\r\n    <string name=\"msg_player_error_subtitle_source\">UNTERTITEL URL funktioniert nicht oder falsche Geräte-Uhrzeit.\\nNormalerweise ist es ein Fehler in der App.</string>\r\n    <string name=\"hide_upcoming\">Anstehende Videos in Abos ausblenden</string>\r\n    <string name=\"hide_upcoming_channel\">Anstehende Videos in Kanäle ausblenden</string>\r\n    <string name=\"hide_upcoming_home\">Anstehende Videos in Start ausblenden</string>\r\n    <string name=\"search_background_playback\">Hintergrund-Wiedergabe beim Suchen/Öffnen eines Kanals</string>\r\n    <string name=\"trending_row_name\">Trends</string>\r\n    <string name=\"player_seek_type\">Verhalten beim Spulen</string>\r\n    <string name=\"player_seek_regular\">Normal</string>\r\n    <string name=\"player_seek_confirmation_pause\">Mit Bestätigung (Pause beim Spulen)</string>\r\n    <string name=\"player_seek_confirmation_play\">Mit Bestätigung (Wiedergabe beim Spulen)</string>\r\n    <string name=\"hide_shorts_from_home\">Shorts in Start ausblenden</string>\r\n    <string name=\"finish_on_disconnect\">App nach dem Trennen des Telefons/Tablets beenden</string>\r\n    <string name=\"update_error\">Update-Fehler</string>\r\n    <string name=\"hide_shorts_from_search\">Shorts im Suchergebnis ausblenden</string>\r\n    <string name=\"hide_shorts_from_history\">Shorts im Verlauf ausblenden</string>\r\n    <string name=\"hide_shorts_from_trending\">Shorts in Trends ausblenden</string>\r\n    <string name=\"disable_screensaver\">Bildschirmschoner deaktivieren</string>\r\n    <string name=\"description_not_found\">Beschreibung nicht gefunden</string>\r\n    <string name=\"proxy_port_hint\">Proxy-Port</string>\r\n    <string name=\"proxy_host_hint\">Proxy-Hostname oder IP</string>\r\n    <string name=\"proxy_username_hint\">Proxy-Benutzername</string>\r\n    <string name=\"proxy_password_hint\">Proxy-Passwort</string>\r\n    <string name=\"proxy_type\">Proxy-Typ</string>\r\n    <string name=\"enable_web_proxy\">Web-Proxy verwenden</string>\r\n    <string name=\"proxy_not_supported\">Web-Proxy verwenden (benötigt Android 4.4+)</string>\r\n    <string name=\"proxy_test_btn\">Test</string>\r\n    <string name=\"proxy_settings_title\">Proxy-Server-Einstellungen</string>\r\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\r\n    <string name=\"proxy_test_cancelled\">Test #%d: abgebrochen.</string>\r\n    <string name=\"proxy_type_invalid\">Proxy-Typ nicht eingestellt.</string>\r\n    <string name=\"proxy_host_invalid\">Proxy-Host nicht eingestellt.</string>\r\n    <string name=\"proxy_port_invalid\">Ungültiger Proxy-Port, muss &gt; 0.</string>\r\n    <string name=\"proxy_credentials_invalid\">Ungültiger Benutzername oder Passwort.</string>\r\n    <string name=\"proxy_test_aborted\">Test abgebrochen, bitte korrigieren Sie zuerst die Proxy-Einstellungen.</string>\r\n    <string name=\"proxy_application_aborted\">Bitte korrigieren Sie zuerst die Proxy-Einstellungen.</string>\r\n    <string name=\"proxy_test_start\">Test #%d: %s …</string>\r\n    <string name=\"proxy_test_error\">Fehler #%d: %s</string>\r\n    <string name=\"proxy_test_status\">Status #%d: %s %d %s</string>\r\n    <string name=\"openvpn_config_address_hint\">Adresse der Konfigurationsdatei (*.ovpn)</string>\r\n    <string name=\"enable_openvpn\">OpenVPN verwenden</string>\r\n    <string name=\"openvpn_settings_title\">OpenVPN-Einstellungen</string>\r\n    <string name=\"openvpn_application_aborted\">Bitte korrigieren Sie zuerst die OpenVPN-Einstellungen.</string>\r\n    <string name=\"openvpn_test_aborted\">Der Test wurde abgebrochen, bitte korrigieren Sie zuerst die OpenVPN-Einstellungen.</string>\r\n    <string name=\"openvpn_address_invalid\">OpenVPN-Konfigurationsadresse nicht gesetzt.</string>\r\n    <string name=\"internet_censorship\">Internet-Zensur</string>\r\n    <string name=\"rename_section\">Diese Kategorie umbenennen</string>\r\n    <string name=\"simple_edit_value_hint\">Wert eingeben</string>\r\n    <string name=\"seek_interval\">Spulintervall</string>\r\n    <string name=\"seek_interval_sec\">%s sek.</string>\r\n    <string name=\"subtitle_system\">System-Stil (Android-Einstellungen &gt; Barrierefreiheit)</string>\r\n    <string name=\"subtitle_scale\">Untertitel-Skalierung</string>\r\n    <string name=\"audio_sync_fix_desc\">Eine alternative Methode zur Synchronisierung von Audio/Video. Sie gilt als veraltet, kann aber in manchen Fällen hilfreich sein.</string>\r\n    <string name=\"audio_sync_fix\">Audio-Synchronisation-Fix</string>\r\n    <string name=\"ambilight_ratio_fix_desc\">Behebt fehlende Bias-Beleuchtung. Korrigiert falsches Seitenverhältnis oder Video-Skalierung. Behebt leere Screenshots. Beeinträchtigt die Performance!</string>\r\n    <string name=\"ambilight_ratio_fix\">Ambilight/Seitenverhältnis/Video-Skalierung/Screenshots-Fix</string>\r\n    <string name=\"force_legacy_codecs_desc\">Erhebliche Verbesserung der Leistung auf Low-End-Geräten. Die maximale Auflösung beträgt 720p.</string>\r\n    <string name=\"force_legacy_codecs\">Legacy-Codecs erzwingen (720p)</string>\r\n    <string name=\"live_stream_fix_desc\">Achtung, diese Option deaktiviert das Zurückspulen von Streams. Erhebliche Verbesserung der Live-Stream-Leistung auf Low-End-Geräten. Die maximale Auflösung beträgt 1080p.</string>\r\n    <string name=\"live_stream_fix_4k_desc\">Achtung, diese Option deaktiviert das Zurückspulen von Streams. Erhebliche Verbesserung der Live-Stream-Leistung. Die maximale Auflösung beträgt 4K.</string>\r\n    <string name=\"live_stream_fix\">Live-Stream-Fix (1080p)</string>\r\n    <string name=\"live_stream_fix_4k\">Live-Stream-Fix (4K)</string>\r\n    <string name=\"playback_notifications_fix_desc\">Ausblenden von Benachrichtigungen. Könnte bei AOSP-basierter Firmware nützlich sein.</string>\r\n    <string name=\"playback_notifications_fix\">Wiedergabe-Benachrichtigungen deaktivieren</string>\r\n    <string name=\"amlogic_fix_desc\">Frame-Drop-Fix auf Amlogic-basierten Geräten.</string>\r\n    <string name=\"amlogic_fix\">Amlogic 1080p\\@60fps Fix</string>\r\n    <string name=\"tunneled_video_playback_desc\">HINWEIS: Pause funktioniert möglicherweise nicht richtig! Die getunnelte Video-Wiedergabe verspricht Vorteile wie eine bessere Audio/Video-Synchronisation (AV-Sync) und eine flüssigere Wiedergabe. Erfordert Android 5+</string>\r\n    <string name=\"tunneled_video_playback\">Getunnelte Video-Wiedergabe (Android 5+)</string>\r\n    <string name=\"master_volume\">Gesamtlautstärke</string>\r\n    <string name=\"volume_limit\">Lautstärkebegrenzung</string>\r\n    <string name=\"player_volume\">Lautstärke</string>\r\n    <string name=\"play_video\">Abspielen</string>\r\n    <string name=\"remember_position_of_short_videos\">Position von kurzen Videos merken (weniger als 5 Min.)</string>\r\n    <string name=\"remember_position_of_live_videos\">Position von Live-Streams merken</string>\r\n    <string name=\"player_show_tooltips\">Tasten-Tooltips anzeigen</string>\r\n    <string name=\"action_like_unset\">Mag ich</string>\r\n    <string name=\"action_dislike_unset\">Mag ich nicht</string>\r\n    <string name=\"various_buttons\">Schaltflächen am oberen Rand der Startseite</string>\r\n    <string name=\"not_compatible_with\">Ausgewählte Option ist nicht kompatibel mit</string>\r\n    <string name=\"subtitle_position\">Untertitel-Verschiebung von unten</string>\r\n    <string name=\"pressing_home\">durch Drücken von HOME</string>\r\n    <string name=\"pressing_home_back\">durch Drücken von HOME oder BACK</string>\r\n    <string name=\"pressing_back\">durch Drücken von BACK</string>\r\n    <string name=\"player_number_key_seek\">Spulen mit Zifferntasten</string>\r\n    <string name=\"save_remove_playlist\">Wiedergabeliste zur Wiedergabelisten-Kategorie hinzufügen/entfernen</string>\r\n    <string name=\"save_playlist\">Wiedergabeliste zur Wiedergabelisten-Kategorie hinzufügen</string>\r\n    <string name=\"remove_playlist\">Wiedergabeliste dauerhaft aus Wiedergabelisten-Kategorie entfernen</string>\r\n    <string name=\"remove_playlist_fmt\">%s dauerhaft entfernen?</string>\r\n    <string name=\"removed_from_playlists\">Entfernt aus der Wiedergabelisten-Kategorie</string>\r\n    <string name=\"saved_to_playlists\">Hinzugefügt zur Wiedergabelisten-Kategorie</string>\r\n    <string name=\"create_playlist\">Wiedergabeliste erstellen</string>\r\n    <string name=\"create_playlist_note\">Diese wird nicht in der YouTube-App angezeigt</string>\r\n    <string name=\"add_video_to_new_playlist\">Hinzufügen zu neuer Wiedergabeliste</string>\r\n    <string name=\"cant_delete_empty_playlist\">Leere Wiedergabeliste kann nicht gelöscht werden</string>\r\n    <string name=\"playlist\">Wiedergabeliste</string>\r\n    <string name=\"rename_playlist\">Wiedergabeliste umbenennen</string>\r\n    <string name=\"cant_rename_empty_playlist\">Leere Wiedergabeliste kann nicht umbenannt werden</string>\r\n    <string name=\"cant_rename_foreign_playlist\">Fremde Wiedergabeliste kann nicht umbenannt werden</string>\r\n    <string name=\"cant_save_playlist\">Diese Wiedergabeliste kann nicht hinzugefügt werden</string>\r\n    <string name=\"enter_value\">Geben Sie einen beliebigen (nicht leeren) Wert ein</string>\r\n    <string name=\"dialog_add_remove_from\">Hinzufügen/Entfernen aus %s</string>\r\n    <string name=\"lb_playback_controls_skip_next\">Nächstes Video</string>\r\n    <string name=\"lb_playback_controls_skip_previous\">Vorheriges Video</string>\r\n    <string name=\"alt_presets_behavior\">Alternatives-Voreinstellungen-Verhalten (Bandbreite begrenzen)</string>\r\n    <string name=\"alt_presets_behavior_desc\">Die App wird versuchen, die Bandbreite entsprechend der gewählten Voreinstellung beizubehalten, anstatt Auflösung, Bildrate und Codec aufeinander abzustimmen.</string>\r\n    <string name=\"sleep_timer\">Sleep-Timer (wenn Fernbedienung eine Stunde nicht benutzt)</string>\r\n    <string name=\"sleep_timer_desc\">Pausiert die Wiedergabe, wenn die Fernbedienung eine Stunde lang nicht benutzt wurde.</string>\r\n    <string name=\"disable_vsync\">Snap-to-VSync deaktivieren</string>\r\n    <string name=\"disable_vsync_desc\">Diese Option deaktiviert das Ausrichten von Bildern mit dem vertikalen Synchronisationssignal des Displays. Dies könnte die Leistung auf Low-End-Geräten verbessern, indem CPU-Ressourcen freigegeben werden.</string>\r\n    <string name=\"skip_codec_profile_check\">Überprüfung des Codec-Profil-Level überspringen</string>\r\n    <string name=\"skip_codec_profile_check_desc\">Überprüft nicht die Codec-Unterstützung beim Starten eines Videos. Könnte bei fehlerhafter Firmware hilfreich sein.</string>\r\n    <string name=\"force_sw_codec\">SW-Video-Decoder erzwingen</string>\r\n    <string name=\"force_sw_codec_desc\">Könnte fast jedes Video abspielen, aber die Performance ist sehr schlecht.</string>\r\n    <string name=\"playlist_order\">Wiedergabeliste sortieren</string>\r\n    <string name=\"playlist_order_added_date_newer_first\">Hinzugefügtes Datum (neue zuerst)</string>\r\n    <string name=\"playlist_order_added_date_older_first\">Hinzugefügtes Datum (ältere zuerst)</string>\r\n    <string name=\"playlist_order_published_date_newer_first\">Veröffentlichungsdatum (neue zuerst)</string>\r\n    <string name=\"playlist_order_published_date_older_first\">Veröffentlichungsdatum (ältere zuerst)</string>\r\n    <string name=\"playlist_order_popularity\">Beliebtheit</string>\r\n    <string name=\"cant_do_this_for_foreign_playlist\">Kann dies nicht für fremde Wiedergabelisten tun</string>\r\n    <string name=\"owned_playlist_warning\">Fehler: Diese Aktion kann nur auf eigene Wiedergabelisten angewendet werden</string>\r\n    <string name=\"content_block_alt_server\">Alternativen Server verwenden</string>\r\n    <string name=\"content_block_alt_server_desc\">Aktivieren Sie diese Option, wenn SponsorBlock aus verschiedenen Gründen nicht funktioniert.</string>\r\n    <string name=\"unset_stream_reminder\">Stream-Erinnerung deaktivieren</string>\r\n    <string name=\"set_stream_reminder\">Stream-Erinnerung aktivieren</string>\r\n    <string name=\"playback_starts_shortly\">Die Wiedergabe wird automatisch gestartet, sobald der Stream bereit ist</string>\r\n    <string name=\"starting_stream\">Starten des Streams…</string>\r\n    <string name=\"action_playlist_remove\">Aus Wiedergabeliste entfernen</string>\r\n    <string name=\"signin_view_title\">Benutzercode wird geladen…</string>\r\n    <string name=\"signin_view_description\">Um sich anzumelden, geben Sie diesen Code auf Seite %s ein.\\nHINWEIS: Funktioniert nur mit Firefox- oder Chrome-Browsern.</string>\r\n    <string name=\"signin_view_action_text\">Erledigt</string>\r\n    <string name=\"require_checked\">Benötigt \\\"%s\\\"</string>\r\n    <string name=\"msg_press_again_to_exit\">Zum Beenden erneut drücken</string>\r\n    <string name=\"player_remaining_time\">Verbleibend: %s</string>\r\n    <string name=\"player_ending_time\">Ende: %s</string>\r\n    <string name=\"badge_new_content\">NEU</string>\r\n    <string name=\"badge_live\">LIVE</string>\r\n    <string name=\"add_device_view_description\">Um ein Gerät zu verbinden, geben Sie diesen TV-Code in der YouTube-App auf Ihrem Smartphone/Tablet unter Einstellungen/Auf Fernseher ansehen/TV-Code ein.\\nHINWEIS: Dies funktioniert nur mit der Gboard-Tastatur!</string>\r\n    <string name=\"playback_controls_repeat_pause\">Pause wiederholen</string>\r\n    <string name=\"playback_controls_repeat_list\">Liste wiederholen</string>\r\n    <string name=\"action_subscribe_off\">Abonnieren aus</string>\r\n    <string name=\"action_subscribe_on\">Abonnieren an</string>\r\n    <string name=\"skip_24_rate\">24fps-Formate überspringen (Echter-Kinomodus-Fix\\?)</string>\r\n    <string name=\"skip_shorts\">Shorts überspringen</string>\r\n    <string name=\"player_disable_suggestions\">Vorschläge deaktivieren</string>\r\n    <string name=\"suggestions\">Vorschläge</string>\r\n    <string name=\"feedback\">Feedback</string>\r\n    <string name=\"sources\">Quellen</string>\r\n    <string name=\"releases\">Versionen</string>\r\n    <string name=\"prefer_avc_over_vp9\">Codec-Auswahl: avc statt vp9 bevorzugen</string>\r\n    <string name=\"open_chat\">Chat/Kommentare</string>\r\n    <string name=\"open_comments\">Kommentare öffnen</string>\r\n    <string name=\"place_chat_left\">Chat auf linker Seite platzieren</string>\r\n    <string name=\"place_comments_left\">Kommentare auf linker Seite platzieren</string>\r\n    <string name=\"use_alt_speech_recognizer\">Alternative Spracherkennung</string>\r\n    <string name=\"time_format\">Zeit-Format</string>\r\n    <string name=\"time_format_24\">24 Stunden</string>\r\n    <string name=\"time_format_12\">12 Stunden (AM/PM)</string>\r\n    <string name=\"use_alt_speech_recognizer_desc\">Funktioniert nicht gut. Verwenden Sie es nur, wenn Sie Probleme mit der Standard-Spracherkennung haben.</string>\r\n    <string name=\"speech_recognizer\">Spracherkennung</string>\r\n    <string name=\"speech_engine\">Engine für Sprachsuche</string>\r\n    <string name=\"speech_recognizer_system\">System (bevorzugt)</string>\r\n    <string name=\"speech_recognizer_external_1\">Extern 1 (Funktioniert nicht gut, um es zu benutzen, müssen Sie den Zugriff auf das Mikrofon deaktivieren)</string>\r\n    <string name=\"speech_recognizer_external_2\">Extern 2 (Funktioniert nicht gut)</string>\r\n    <string name=\"real_channel_icon\">Symbol auf der Kanal-Taste anzeigen</string>\r\n    <string name=\"subtitle_yellow_semi_transparent\">Gelb auf halbtransparentem Hintergrund</string>\r\n    <string name=\"subtitle_yellow_black\">Gelb auf schwarzem Hintergrund</string>\r\n    <string name=\"player_pixel_ratio\">Pixel-Verhältnis</string>\r\n    <string name=\"color_scheme_dark_grey_monochrome\">Dunkelgrau (monochrom)</string>\r\n    <string name=\"disable_mic_permission\">Bitte deaktivieren Sie den Mikrofonzugriff für die App, damit diese Spracherkennung richtig funktioniert.</string>\r\n    <string name=\"repeat_mode_shuffle\">Zufällige Wiedergabe einer Wiedergabeliste</string>\r\n    <string name=\"chat_left\">Links</string>\r\n    <string name=\"chat_right\">Rechts</string>\r\n    <string name=\"card_real_thumbnails\">Vorschaubilder durch ein Bild aus dem Video ersetzen</string>\r\n    <string name=\"card_content\">Quelle der Karten-Vorschaubilder</string>\r\n    <string name=\"thumb_quality_default\">Standard</string>\r\n    <string name=\"thumb_quality_start\">Start des Videos</string>\r\n    <string name=\"thumb_quality_middle\">Mitte des Videos</string>\r\n    <string name=\"thumb_quality_end\">Ende des Videos</string>\r\n    <string name=\"content_block_status\">Server-Status von SponsorBlock prüfen</string>\r\n    <string name=\"dearrow_status\">Server-Status von DeArrow prüfen</string>\r\n    <string name=\"player_show_quality_info_bitrate\">Bitrate zu den Qualitätsinfos hinzufügen</string>\r\n    <string name=\"player_speed_button_old_behavior\">Altes Verhalten der Geschwindigkeitstaste wiederherstellen</string>\r\n    <string name=\"protect_settings_with_password\">Alle Einstellungen mit einem Passwort schützen</string>\r\n    <string name=\"enter_settings_password\">Passwort für Einstellungen eingeben</string>\r\n    <string name=\"child_mode\">Kindermodus</string>\r\n    <string name=\"child_mode_desc\">In diesem Modus kann der Benutzer die Suche nicht verwenden und keine vorgeschlagenen Inhalte sehen. Die Einstellungsseite wird durch ein Passwort geschützt.</string>\r\n    <string name=\"lost_setting_warning\">Die Einstellungen der App werden geändert. Stellen Sie sicher, dass Sie eine Sicherung der Einstellungen erstellt haben.</string>\r\n    <string name=\"player_button_long_click\">Diese Taste lange drücken für weitere Optionen</string>\r\n    <string name=\"pause_history\">Verlauf pausieren</string>\r\n    <string name=\"resume_history\">Verlauf fortsetzen</string>\r\n    <string name=\"disable_history\">Verlauf deaktivieren</string>\r\n    <string name=\"enable_history\">Verlauf aktivieren</string>\r\n    <string name=\"clear_history\">Verlauf löschen</string>\r\n    <string name=\"chapters\">Kapitel</string>\r\n    <string name=\"card_multiline_subtitle\">Mehrzeilige Untertitel</string>\r\n    <string name=\"auto_frame_rate_modes\">Unterstützte Modi</string>\r\n    <string name=\"loading\">Wird geladen…</string>\r\n    <string name=\"hide_streams\">Streams in Abos ausblenden</string>\r\n    <string name=\"video_rotate\">Drehen</string>\r\n    <string name=\"video_flip\">Spiegeln</string>\r\n    <string name=\"trending_searches\">Beliebte Suchen</string>\r\n    <string name=\"video_duration_any\">Beliebig</string>\r\n    <string name=\"video_duration\">Dauer</string>\r\n    <string name=\"video_duration_under_4\">Unter 4 Minuten</string>\r\n    <string name=\"video_duration_between_4_20\">4–20 Minuten</string>\r\n    <string name=\"video_duration_over_20\">Über 20 Minuten</string>\r\n    <string name=\"content_type\">Typ</string>\r\n    <string name=\"content_type_any\">Beliebig</string>\r\n    <string name=\"content_type_video\">Video</string>\r\n    <string name=\"content_type_channel\">Kanal</string>\r\n    <string name=\"content_type_playlist\">Wiedergabeliste</string>\r\n    <string name=\"content_type_movie\">Film</string>\r\n    <string name=\"video_features\">Eigenschaften</string>\r\n    <string name=\"video_feature_any\">Beliebig</string>\r\n    <string name=\"video_feature_live\">Live</string>\r\n    <string name=\"video_feature_4k\">4K</string>\r\n    <string name=\"video_feature_hdr\">HDR</string>\r\n    <string name=\"search_sorting\">Sortieren nach</string>\r\n    <string name=\"sort_by_relevance\">Relevanz</string>\r\n    <string name=\"sort_by_views\">Anzahl der Aufrufe</string>\r\n    <string name=\"sort_by_date\">Upload-Datum</string>\r\n    <string name=\"sort_by_rating\">Bewertung</string>\r\n    <string name=\"clear_search_history\">Suchverlauf löschen</string>\r\n    <string name=\"remove_from_subscriptions\">Ausblenden</string>\r\n    <string name=\"player_long_speed_list\">Lange Geschwindigkeitsliste</string>\r\n    <string name=\"player_extra_long_speed_list\">Extra lange Geschwindigkeitsliste</string>\r\n    <string name=\"alt_app_icon\">Alternatives App-Symbol (Neustart erforderlich)</string>\r\n    <string name=\"network_stack\">Netzwerk-Engine %s bevorzugen</string>\r\n    <string name=\"player_network_stack\">Netzwerk-Engine</string>\r\n    <string name=\"cronet_desc\">Cronet ist die Netzwerk-Engine von Chromium. Cronet kann die Latenz verringern und die Netzwerkleistung erhöhen, was bei Pufferproblemen helfen kann.</string>\r\n    <string name=\"unlock_all_formats\">Alle Video-Formate freischalten</string>\r\n    <string name=\"unlock_all_formats_desc\">Bei einigen Geräten meldet die Firmware fälschlicherweise, dass einige Formate nicht unterstützt werden, auch wenn sie es sind.\\n(z.B. melden 1080p-Smart-TVs oft, dass 4k nicht unterstützt wird).</string>\r\n    <string name=\"okhttp_desc\">Diese Netzwerk-Engine ist die langsamste, hat aber eine gute Stabilität.</string>\r\n    <string name=\"select_channel_section\">Die entsprechende Kategorie öffnen, wenn die App über ATV-Kanäle gestartet wird</string>\r\n    <string name=\"enable_voice_search_desc\">Die offizielle App wird durch eine sogenannte \\\"Bridge\\\" ersetzt. Eine Bridge hilft dabei, globale Suchanfragen an unsere App zu übertragen.</string>\r\n    <string name=\"enable_master_password\">Master-Passwort aktivieren</string>\r\n    <string name=\"enter_master_password\">Master-Passwort eingeben</string>\r\n    <string name=\"disable_stream_buffer\">Puffer für Streams deaktivieren</string>\r\n    <string name=\"disable_stream_buffer_desc\">Fix für Situationen, in denen der Stream sehr verzögert ist. Hinweis: Der Stream kann anfangen zu hängen.</string>\r\n    <string name=\"sony_frame_drop_fix\">Frame-Drop-Fix #1</string>\r\n    <string name=\"sony_frame_drop_fix_desc\">Behebt Verzögerungen/Lags auf Sony-TV und einigen anderen Geräten. Hinweis: Mögliche Probleme mit der Audio-Synchronisation.</string>\r\n    <string name=\"audio_language\">Audio-Sprache</string>\r\n    <string name=\"old_home_look\">Altes Aussehen der Startseite</string>\r\n    <string name=\"old_channel_look\">Altes Aussehen der Kanalseite</string>\r\n    <string name=\"hide_shorts_everywhere\">Shorts überall ausblenden</string>\r\n    <string name=\"update_found\">Update</string>\r\n    <string name=\"volume_boost_warning\">Einstellungen über dem Grenzwert können die Lautsprecher möglicherweise beschädigen</string>\r\n    <string name=\"play_video_incognito\">Inkognito abspielen</string>\r\n    <string name=\"play_from_start\">Von Anfang an abspielen</string>\r\n    <string name=\"header_kids_home\">Kinder</string>\r\n    <string name=\"player_section_playlist\">Inhalt der aktuellen Kategorie als Wiedergabeliste verwenden</string>\r\n    <string name=\"header_trending\">Trends</string>\r\n    <string name=\"screensaver\">Bildschirmschoner</string>\r\n    <string name=\"player_screen_off_timeout\">Bildschirm ausschalten nach Inaktivität</string>\r\n    <string name=\"old_update_notifications\">Altes Aussehen der Update-Benachrichtigungen</string>\r\n    <string name=\"dialog_notification\">Benachrichtigung über Update mit Pop-up-Dialog</string>\r\n    <string name=\"mark_as_watched\">Als gesehen markieren</string>\r\n    <string name=\"player_ui_animations\">Animationen der Player-Oberfläche aktivieren</string>\r\n    <string name=\"player_likes_count\">Anzahl der Mag ich/Mag ich nicht anzeigen</string>\r\n    <string name=\"sorting_alphabetically2\">Alphabetisch (schnell, animierte Vorschaubilder)</string>\r\n    <string name=\"sorting_default\">Standard (schnell, animierte Vorschaubilder)</string>\r\n    <string name=\"content_block_exclude_channel\">Diesen Kanal von SponsorBlock ausschließen</string>\r\n    <string name=\"content_block_stop_excluding_channel\">Diesen Kanal nicht mehr von SponsorBlock ausschließen</string>\r\n    <string name=\"player_chapter_notification\">Schnelles Springen durch die Kapitel per Popup-Benachrichtigung</string>\r\n    <string name=\"player_chapter_notification2\">Zum nächsten Kapitel wechseln durch Drücken der Benachrichtigung</string>\r\n    <string name=\"subtitle_remember\">Untertitel nur für den aktuellen Kanal aktivieren</string>\r\n    <string name=\"amazon_frame_drop_fix\">Frame-Drop-Fix #2</string>\r\n    <string name=\"amazon_frame_drop_fix_desc\">Gedacht für Amazon Stick-Geräte. Kann auch auf anderen Geräten funktionieren.</string>\r\n    <string name=\"default_stack_desc\">Eingebaute Netzwerk-Engine. Diese Engine könnte in bestimmten Situationen eine bessere Stabilität haben.</string>\r\n    <string name=\"item_postion\">Position von</string>\r\n    <string name=\"player_screen_off_dimming\">Dimmwert für Bildschirm aus</string>\r\n    <string name=\"screensaver_timout\">Bildschirmschoner-Timeout</string>\r\n    <string name=\"screensaver_dimming\">Dimmwert für Bildschirmschoner</string>\r\n    <string name=\"player_ui_on_next\">Player-Oberfläche beim Wechsel zum nächsten Video anzeigen</string>\r\n    <string name=\"autogenerated\">automatisch erstellt</string>\r\n    <string name=\"player_auto_volume\">Automatische Lautstärkeanpassung</string>\r\n    <string name=\"header_shorts\">Shorts</string>\r\n    <string name=\"player_global_focus\">Fokus zwischen Player-Tastenreihen synchronisieren</string>\r\n    <string name=\"auto_history\">Automatisch (Kontoeinstellungen verwenden)</string>\r\n    <string name=\"remember_position_subscriptions\">Zuletzt angesehene Position in Abos merken</string>\r\n    <string name=\"remember_position_pinned\">Zuletzt angesehene Position in angehefteten Wiedergabelisten merken</string>\r\n    <string name=\"msg_player_unknown_error\">Unbekannter Fehler</string>\r\n    <string name=\"unknown_source_error\">Unbekannter Quellen-Fehler</string>\r\n    <string name=\"unknown_renderer_error\">Unbekannter Renderer-Fehler</string>\r\n    <string name=\"header_notifications\">Benachrichtigungen</string>\r\n    <string name=\"disable_search_history\">Suchverlauf deaktivieren</string>\r\n    <string name=\"unlock_high_bitrate_formats\">1080p vp9-Formate mit hoher Bitrate freischalten</string>\r\n    <string name=\"unlock_high_bitrate_audio_formats\">mp4a-Formate mit hoher Bitrate freischalten</string>\r\n    <string name=\"color_scheme_blue\">Blau</string>\r\n    <string name=\"color_scheme_dark_blue\">Dunkelblau</string>\r\n    <string name=\"player_speed_per_channel\">Für jeden Kanal</string>\r\n    <string name=\"disable_popular_searches\">Beliebte Suchanfragen deaktivieren</string>\r\n    <string name=\"multi_profiles\">Separate Einstellungen für jedes Konto verwenden</string>\r\n    <string name=\"protect_account_with_password\">Dieses Konto mit einem Passwort schützen</string>\r\n    <string name=\"enter_account_password\">Konto-Passwort eingeben</string>\r\n    <string name=\"show_connect_messages\">Verbindungsnachrichten anzeigen</string>\r\n    <string name=\"prefer_avc_over_vp9_desc\">ACHTUNG: Maximal 1080p</string>\r\n    <string name=\"play_next\">Als Nächstes abspielen</string>\r\n    <string name=\"hide_watched_from_subscriptions\">Gesehene Videos in Abos ausblenden</string>\r\n    <string name=\"hide_watched_from_notifications\">Gesehene Videos in Benachrichtigungen ausblenden</string>\r\n    <string name=\"hide_unwanted_content\">Inhalte ausblenden</string>\r\n    <string name=\"hide_watched_from_home\">Gesehene Videos in Start ausblenden</string>\r\n    <string name=\"hide_watched_from_watch_later\">Gesehene Videos in Wiedergabeliste Später ansehen ausblenden</string>\r\n    <string name=\"remote_control_permission\">Hintergrund-Fernsteuerung benötigt Overlay-Berechtigung</string>\r\n    <string name=\"login_from_browser\">Anmeldung über Browser</string>\r\n    <string name=\"disable_remote_history\">Verlauf bei Verwendung der Fernsteuerung deaktivieren</string>\r\n    <string name=\"keyboard_fix\">Verhindern, dass die OK-Taste die Tastatur öffnet (G20s und andere)</string>\r\n    <string name=\"nothing_found\">Nichts gefunden</string>\r\n    <string name=\"auto_frame_rate_desc\">Diese Option verhindert Ruckeln in Szenen, in denen sich die Kamera schnell bewegt, z.B. bei Sportübertragungen</string>\r\n    <string name=\"channel_filter_hint\">Kanäle filtern</string>\r\n    <string name=\"player_loop_shorts\">Shorts wiederholen</string>\r\n    <string name=\"player_quick_shorts_skip\">Shorts überspringen mit Links/Rechts-Tasten</string>\r\n    <string name=\"player_quick_shorts_skip_alt\">Shorts überspringen mit Oben/Unten-Tasten</string>\r\n    <string name=\"player_quick_skip_videos\">Normale Videos überspringen mit Links/Rechts-Tasten</string>\r\n    <string name=\"player_quick_skip_videos_alt\">Normale Videos überspringen mit Oben/Unten-Tasten</string>\r\n    <string name=\"channels_filter\">Feld zum Filtern von Kanälen innerhalb der Kanäle-Kategorie anzeigen</string>\r\n    <string name=\"channel_search_bar\">Suchleiste innerhalb der Kanalseite</string>\r\n    <string name=\"header_sports\">Sport</string>\r\n    <string name=\"keep_finished_activities\">Beendete Aktivitäten behalten</string>\r\n    <string name=\"disable_channels_service\">Kanäle-Dienst deaktivieren</string>\r\n    <string name=\"replace_titles\">Titel ersetzen</string>\r\n    <string name=\"enable\">Aktivieren</string>\r\n    <string name=\"more_info\">Mehr Infos</string>\r\n    <string name=\"about_sponsorblock\">Über SponsorBlock</string>\r\n    <string name=\"about_dearrow\">Über DeArrow</string>\r\n    <string name=\"replace_thumbnails\">Vorschaubilder ersetzen</string>\r\n    <string name=\"crowdsourced_thumbnails\">Community-basierte Vorschaubilder</string>\r\n    <string name=\"crowdsoursed_titles\">Community-basierte Titel</string>\r\n    <string name=\"dearrow_not_submitted_thumbs\">Vorschaubilder-Quelle (wenn es keine DeArrow-Einreichung gibt)</string>\r\n    <string name=\"pitch_effect\">Tonhöhen-Effekt</string>\r\n    <string name=\"fullscreen_mode\">Vollbildmodus (ohne Systemleisten)</string>\r\n    <string name=\"player_only_mode\">Nur den Player anzeigen, wenn das Video außerhalb der App geöffnet wird</string>\r\n    <string name=\"pinned_channel_rows\">Angeheftete Kanäle als Reihen anzeigen</string>\r\n    <string name=\"prefer_google_dns\">Google DNS bevorzugen</string>\r\n    <string name=\"prefer_ipv4\">IPv4 DNS bevorzugen</string>\r\n    <string name=\"prefer_ipv4_desc\">Könnte Situationen beheben, in denen die App überhaupt nicht funktioniert.\\nHinweis: Kann Einfrieren und Abstürze verursachen (besonders auf Android 8 Geräten oder Dune HD)</string>\r\n    <string name=\"long_press_for_settings\">LANGE DRÜCKEN FÜR EINSTELLUNGEN</string>\r\n    <string name=\"long_press_for_options\">LANGE DRÜCKEN FÜR OPTIONEN</string>\r\n    <string name=\"device_specific_backup\">Sicherung nur für dieses Gerät</string>\r\n    <string name=\"local_backup\">Lokale Sicherung</string>\r\n    <string name=\"auto_backup\">Automatische Sicherung (einmal pro Tag)</string>\r\n    <string name=\"repeat_mode_reverse_list\">Wiedergabeliste oder Kanalvideos in umgekehrter Reihenfolge abspielen</string>\r\n    <string name=\"calm_msg\">Wir arbeiten an der Lösung des Problems. Prüfen Sie gelegentlich, ob Updates verfügbar sind</string>\r\n    <string name=\"without_picture\">Ohne Bild</string>\r\n    <string name=\"video_disabled\">Video deaktiviert</string>\r\n    <string name=\"applying_fix\">Player nicht schließen. Fix wird angewendet…</string>\r\n    <string name=\"hide_mixes\">Mix-Wiedergabelisten ausblenden</string>\r\n    <string name=\"player_global_focus_desc\">Diese Funktion beeinflusst, welche Player-Taste den Fokus erhält, wenn Sie zwischen Player-Tastenreihen navigieren</string>\r\n    <string name=\"disable_network_error_fixing\">Automatische Netzwerk-Fehlerbehebung deaktivieren</string>\r\n    <string name=\"disable_network_error_fixing_desc\">Sie müssen diese Option wahrscheinlich aktivieren, wenn Sie einen VPN verwenden.</string>\r\n    <string name=\"recommended\">Empfohlen</string>\r\n    <string name=\"add_to_subscriptions_group\">Hinzufügen/Entfernen aus Abo-Gruppe</string>\r\n    <string name=\"new_subscriptions_group\">Neue Gruppe</string>\r\n    <string name=\"rename_group\">Abo-Gruppe umbenennen</string>\r\n    <string name=\"screen_dimming_amount\">Wert für Bildschirm dimmen</string>\r\n    <string name=\"screen_dimming_timeout\">Zeit für Bildschirm dimmen</string>\r\n    <string name=\"playlists_rows\">Wiedergabelisten-Kategorie als Reihen anzeigen</string>\r\n    <string name=\"import_subscriptions_group\">Importieren</string>\r\n    <string name=\"lb_playback_controls_closed_captioning_disable\">Untertitel deaktivieren</string>\r\n    <string name=\"lb_playback_controls_closed_captioning_enable\">Untertitel aktivieren</string>\r\n    <string name=\"my_videos\">Meine Videos</string>\r\n    <string name=\"player_audio_focus\">Audio-Fokus (Pause, wenn andere Player erkannt werden)</string>\r\n    <string name=\"paid_content_notification\">Benachrichtigung über bezahlte Inhalte</string>\r\n    <string name=\"you_liked\">Mag ich</string>\r\n    <string name=\"premium_users_only\">Nur für Premium-Nutzer. Fix für unvollständige Videoformat-Liste</string>\r\n    <string name=\"playback_buffering_fix\">Wiedergabe-Puffer-Fix</string>\r\n    <string name=\"oculus_quest_fix\">Oculus-Quest-Fix</string>\r\n    <string name=\"card_preview_muted\">Video ohne Ton</string>\r\n    <string name=\"card_preview_full\">Video mit Ton</string>\r\n    <string name=\"card_preview\">Karten-Vorschau</string>\r\n    <string name=\"card_unlocalized_titles\">Videotitel nicht übersetzen</string>\r\n    <string name=\"dont_resize_video_to_fit_dialog\">Video mit Dialogfeld nicht verkleinern</string>\r\n    <string name=\"typing_corrections\">Autokorrektur bei Texteingabe deaktivieren</string>\r\n    <string name=\"suggestions_horizontally_scrolled\">Horizontal scrollbare Vorschläge</string>\r\n    <string name=\"stable_restore\">Stable-Version wiederherstellen (alle Daten werden gelöscht)</string>\r\n    <string name=\"auto_backup_category\">Automatische Sicherung</string>\r\n    <string name=\"once_a_day\">Einmal pro Tag</string>\r\n    <string name=\"once_a_week\">Einmal pro Woche</string>\r\n    <string name=\"once_a_month\">Einmal pro Monat</string>\r\n    <string name=\"action_debug_info\">Debug-Info</string>\r\n    <string name=\"dialog_block_channel\">Kanal blockieren</string>\r\n    <string name=\"dialog_unblock_channel\">Blockierung aufheben</string>\r\n    <string name=\"confirm_block_channel\">Alle Inhalte von %s ausblenden?</string>\r\n    <string name=\"channel_blocked\">Kanal blockiert</string>\r\n    <string name=\"channel_unblocked\">Blockierung aufgehoben</string>\r\n    <string name=\"header_blocked_channels\">Blockierte Kanäle</string>\r\n    <string name=\"msg_no_blocked_channels\">Keine blockierten Kanäle</string>\r\n</resources>\r\n"
  },
  {
    "path": "common/src/main/res/values-el/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Αρχική</string>\n  <string name=\"title_search\">Αναζήτηση</string>\n  <string name=\"header_subscriptions\">Εγγραφές</string>\n  <string name=\"header_history\">Ιστορικό</string>\n  <string name=\"header_music\">Μουσική</string>\n  <string name=\"header_news\">Ειδήσεις</string>\n  <string name=\"header_gaming\">Παιχνίδια</string>\n  <string name=\"header_playlists\">Λίστες αναπαραγωγής</string>\n  <string name=\"header_settings\">Ρυθμίσεις</string>\n  <string name=\"header_channels\">Κανάλια</string>\n  <string name=\"subscriptions_signin_title\">Δείτε τα τελευταία από τα κανάλια που σας αρέσουν</string>\n  <string name=\"subscriptions_signin_subtitle\">Συνδεθείτε για να δείτε τις εγγραφές σας</string>\n  <string name=\"subscriptions_signin_button_text\">ΕΙΣΟΔΟΣ</string>\n  <string name=\"library_signin_to_show_more\">Παρακολουθήστε βίντεο που σας άρεσαν, αποθηκεύσατε ή εγγραφήκατε</string>\n  <string name=\"library_signin_subtitle\">Συνδεθείτε για να δείτε τη βιβλιοθήκη σας</string>\n  <string name=\"action_signin\">ΕΙΣΟΔΟΣ</string>\n  <string name=\"title_video_formats\">Μορφές βίντεο</string>\n  <string name=\"title_audio_formats\">Μορφές ήχου</string>\n  <string name=\"subtitle_category_title\">Υπότιτλοι</string>\n  <string name=\"video_max_quality\">Αυτόματα (μεγ. ποιότητα)</string>\n  <string name=\"audio_max_quality\">Αυτόματα (μεγ. ποιότητα)</string>\n  <string name=\"subtitles_disabled\">Οι υπότιτλοι απενεργοποιήθηκαν</string>\n  <string name=\"auto_frame_rate\">Αυτόματος Ρυθμός Καρέ</string>\n  <string name=\"frame_rate_correction\">Διόρθωση fps:\\n%s</string>\n  <string name=\"category_background_playback\">Αναπαραγωγή στο παρασκήνιο</string>\n  <string name=\"not_implemented\">Δεν έχει υλοποιηθεί</string>\n  <string name=\"option_background_playback_off\">Απενεργοποιημένο</string>\n  <string name=\"option_background_playback_pip\">Εικόνα σε Εικόνα</string>\n  <string name=\"option_background_playback_only_audio\">Μόνο ήχος</string>\n  <string name=\"playback_settings\">Ρυθμίσεις αναπαραγωγής</string>\n  <string name=\"video_speed\">Ταχύτητα βίντεο</string>\n  <string name=\"resolution_switch\">Αλλαγή ανάλυσης</string>\n  <string name=\"video_buffer\">Βίντεο buffer</string>\n  <string name=\"video_buffer_size_low\">Χαμηλό</string>\n  <string name=\"video_buffer_size_med\">Μεσαίο</string>\n  <string name=\"video_buffer_size_high\">Υψηλό</string>\n  <string name=\"update_changelog\">Λίστα Αλλαγών</string>\n  <string name=\"install_update\">Εγκατάσταση ενημέρωσης</string>\n  <string name=\"section_is_empty\">Ωχ! Δεν υπάρχει τίποτα εδώ.</string>\n  <string name=\"dialog_add_to_playlist\">Προσθήκη/Αφαίρεση από λίστα αναπαραγωγής</string>\n  <string name=\"msg_signed_users_only\">Μόνο Εγγεγραμμένοι Χρήστες</string>\n  <string name=\"msg_cant_load_content\">Ωχ! Δεν ήταν δυνατή η φόρτωση περιεχομένου.\\n1) Ελέγξτε την ημερομηνία και ώρα της συσκευής.\\n2) Ελέγξτε τη σύνδεση με το διαδίκτυο..\\n3) Ελέγξτε τις ρυθμίσεις proxy.\\n4) Δοκιμάστε να συνδεθείτε στο λογαριασμό σας.\\n5) Ίσως δεν υπάρχει τίποτα που μπορείτε να κάνετε.</string>\n  <string name=\"title_video_presets\">Προεπιλογές βίντεο</string>\n  <string name=\"settings_accounts\">Λογαριασμοί</string>\n  <string name=\"settings_left_panel\">Επεξεργασία κατηγοριών</string>\n  <string name=\"settings_themes\">Θέματα</string>\n  <string name=\"settings_other\">Άλλο</string>\n  <string name=\"settings_player\">Ρυθμίσεις βίντεο</string>\n  <string name=\"settings_language\">Γλώσσα</string>\n  <string name=\"settings_linked_devices\">Συνδεδεμένες συσκευές</string>\n  <string name=\"settings_about\">Σχετικά</string>\n  <string name=\"dialog_account_list\">Επιλέξτε λογαριασμό</string>\n  <string name=\"dialog_account_none\">Κανένα</string>\n  <string name=\"dialog_remove_account\">Αφαίρεση λογαριασμού</string>\n  <string name=\"dialog_add_account\">Προσθήκη λογαριασμού</string>\n  <string name=\"default_lang\">Προεπιλογή</string>\n  <string name=\"dialog_select_language\">Γλώσσα</string>\n  <string name=\"subtitle_default\">Προεπιλογή</string>\n  <string name=\"subtitle_white_semi_transparent\">Λευκό σε ημιδιαφανές φόντο</string>\n  <string name=\"subtitle_style\">Στυλ υποτίτλων</string>\n  <string name=\"subtitle_language\">Γλώσσα υπότιτλου</string>\n  <string name=\"action_search\">Αναζήτηση</string>\n  <string name=\"settings_main_ui\">Διεπαφή χρήστη</string>\n  <string name=\"dialog_main_ui\">Διεπαφή χρήστη</string>\n  <string name=\"card_animated_previews\">Προεπισκόπηση βίντεο</string>\n  <string name=\"web_site\">Ιστοσελίδα</string>\n  <string name=\"donation\">Δωρεά</string>\n  <string name=\"dialog_about\">Σχετικά</string>\n  <string name=\"dialog_player_ui\">Βίντεο player</string>\n  <string name=\"player_show_ui_on_pause\">Εμφάνιση UI στη παύση</string>\n  <string name=\"player_pause_on_ok\">Το OK κάνει παύση στο βίντεο</string>\n  <string name=\"player_ok_button_behavior\">Συμπεριφορά OK κουμπιού</string>\n  <string name=\"player_only_ui\">Μόνο UI</string>\n  <string name=\"player_ui_and_pause\">UI και παύση</string>\n  <string name=\"player_only_pause\">Μόνο παύση</string>\n  <string name=\"check_for_updates\">Έλεγχος για ενημερώσεις</string>\n  <string name=\"update_not_found\">Χρησιμοποιείτε τη τελευταία έκδοση.</string>\n  <string name=\"update_in_progress\">Περιμένετε…</string>\n  <string name=\"player_ui_hide_behavior\">Αυτόματη απόκρυψη UI</string>\n  <string name=\"option_never\">Ποτέ</string>\n  <string name=\"side_panel_sections\">Ρύθμιση ενοτήτων</string>\n  <string name=\"boot_to_section\">Εκκίνηση στην ενότητα</string>\n  <string name=\"large_ui\">Μεγάλο UI</string>\n  <string name=\"video_grid_scale\">Κλίμακα πλέγματος βίντεο</string>\n  <string name=\"scale_ui\">Κλίμακα UI</string>\n  <string name=\"color_scheme\">Χρωματισμός</string>\n  <string name=\"color_scheme_default\">Προεπιλεγμένο</string>\n  <string name=\"color_scheme_red_grey\">Κόκκινο γκρι</string>\n  <string name=\"color_scheme_red\">Κόκκινο</string>\n  <string name=\"color_scheme_dark_grey\">Μαύρο γκρι</string>\n  <string name=\"disable_update_check\">Απενεργοποίηση ελέγχου ενημερώσεων</string>\n  <string name=\"show_again\">Εμφάνιση ξανά</string>\n  <string name=\"check_updates_auto\">Ειδοποίηση για νέα έκδοση</string>\n  <string name=\"select_account_on_boot\">Επιλογή κατά την εκκίνηση</string>\n  <string name=\"player_other\">Λοιπά</string>\n  <string name=\"player_full_date\">Ακριβής ημερομηνία στην εκκίνηση</string>\n  <string name=\"open_channel\">Άνοιγμα καναλιού</string>\n  <string name=\"not_interested\">Δεν με ενδιαφέρει</string>\n  <string name=\"you_wont_see_this_video\">Το βίντεο καταργήθηκε από τα προτεινόμενα</string>\n  <string name=\"settings_search\">Αναζήτηση</string>\n  <string name=\"dialog_search\">Αναζήτηση</string>\n  <string name=\"instant_voice_search\">Φωνητική Αναζήτηση</string>\n  <string name=\"option_background_playback_behind\">Παίζει στο παρασκήνιο</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">Οι πληροφορίες του επόμενου βίντεο δεν έχουν φορτωθεί ακόμα</string>\n  <string name=\"card_multiline_title\">Τίτλοι πολλαπλών γραμμών</string>\n  <string name=\"cards_style\">Στυλ καρτών</string>\n  <string name=\"repeat_mode_all\">Παίξε όλα τα βίντεο συνεχόμενα</string>\n  <string name=\"repeat_mode_one\">Επανάληψη Βίντεο</string>\n  <string name=\"repeat_mode_pause\">Παύση αναπαραγωγής μετά από κάθε βίντεο (εκτός ουράς)</string>\n  <string name=\"repeat_mode_none\">Διακοπή αναπαραγωγής μετά από ένα βίντεο (εκτός ουράς)</string>\n  <string name=\"subscribed_to_channel\">Εγγραφήκατε</string>\n  <string name=\"unsubscribed_from_channel\">Απεγγραφήκατε</string>\n  <string name=\"subtitle_yellow_transparent\">Κίτρινο σε διαφανές φόντο</string>\n  <string name=\"subtitle_white_black\">Λευκό σε μαύρο φόντο</string>\n  <string name=\"player_seek_preview\">Προεπισκόπηση κατά την αναζήτηση</string>\n  <string name=\"color_scheme_dark_grey_oled\">Σκούρο γκρι (OLED)</string>\n  <string name=\"channels_section_sorting\">Ταξινόμηση ενότητας καναλιού</string>\n  <string name=\"sorting_by_new_content\">Νέο περιεχόμενο</string>\n  <string name=\"sorting_alphabetically\">Αλφαβητικά</string>\n  <string name=\"sorting_last_viewed\">Τελευταίες προβολές</string>\n  <string name=\"player_pause_when_seek\">Παύση κατά την αναζήτηση</string>\n  <string name=\"playlists_style\">Στυλ λίστας αναπαραγωγής</string>\n  <string name=\"playlists_style_grid\">Πλέγμα</string>\n  <string name=\"playlists_style_rows\">Γραμμές</string>\n  <string name=\"player_show_clock\">Εμφάνιση ρολογιού στη μπάρα ελέγχου</string>\n  <string name=\"player_show_remaining_time\">Εμφάνιση υπολοιπόμενου χρόνου στη μπάρα ελέγχου</string>\n  <string name=\"open_channel_uploads\">Άνοιγμα μεταφορτώσεων καναλιού</string>\n  <string name=\"app_exit_shortcut\">Έξοδος από την εφαρμογή</string>\n  <string name=\"app_exit_none\">Μη γίνει έξοδος</string>\n  <string name=\"app_double_back_exit\">2 φορές πίσω</string>\n  <string name=\"app_single_back_exit\">1 φορά πίσω</string>\n  <string name=\"action_video_zoom\">Μεγέθυνση βίντεο</string>\n  <string name=\"video_zoom\">Μεγέθυνση βίντεο</string>\n  <string name=\"video_zoom_default\">Προεπιλεγμένο</string>\n  <string name=\"video_zoom_fit_width\">Προσαρμοσμένο πλάτος</string>\n  <string name=\"video_zoom_fit_height\">Προσαρμοσμένο ύψος</string>\n  <string name=\"video_zoom_fit_both\">Προσαρμοσμένο πλάτος ή ύψος</string>\n  <string name=\"video_zoom_stretch\">Μέγιστη έκταση</string>\n  <string name=\"color_scheme_teal\">Βεραμάν</string>\n  <string name=\"color_scheme_teal_oled\">Βεραμάν (OLED)</string>\n  <string name=\"player_seek_preview_none\">Απενεργοποιημένο</string>\n  <string name=\"player_seek_preview_single\">Μονό frame</string>\n  <string name=\"player_seek_preview_carousel\">Καρουζέλ</string>\n  <string name=\"player_seek_preview_carousel_slow\">Καρουζέλ (αργά)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Καρουζέλ (γρήγορα)</string>\n  <string name=\"unsubscribe_from_channel\">Απεγγραφή από το κανάλι</string>\n  <string name=\"card_title_lines_num\">Αριθμός σειρών τίτλου κάρτας</string>\n  <string name=\"card_auto_scrolled_title\">Αυτόματη κύλιση περικομμένου τίτλου</string>\n  <string name=\"share_link\">Κοινοποίηση συνδέσμου</string>\n  <string name=\"subscribe_to_channel\">Εγγραφή στο κανάλι</string>\n  <string name=\"wait_data_loading\">Παρακαλώ περιμένετε μέχρι να φορτωθούν τα δεδομένα…</string>\n  <string name=\"auto_frame_rate_pause\">Παύση αυτόματου ρυθμού frame</string>\n  <string name=\"auto_frame_rate_applying\">Εφαρμογή αυτόματου ρυθμού frame %sx%s\\@%s</string>\n  <string name=\"audio_shift\">Μετατόπιση ήχου</string>\n  <string name=\"player_remember_speed\">Να θυμάσαι τη ταχύτητα</string>\n  <string name=\"mark_channel_as_watched\">Σήμανση ως προβεβλημένο</string>\n  <string name=\"channel_marked_as_watched\">Το κανάλι έχει επισημανθεί ως προβεβλημένο</string>\n  <string name=\"dialog_add_device\">Προσθήκη συσκευής</string>\n  <string name=\"dialog_remove_all_devices\">Αφαίρεση όλων των συσκευών</string>\n  <string name=\"device_link_enabled\">Ο σύνδεσμος ενεργοποιήθηκε</string>\n  <string name=\"device_connected\">Η συσκευή \\\"%s\\\" έχει συνδεθεί</string>\n  <string name=\"device_disconnected\">Η συσκευή \\\"%s\\\" έχει αποσυνδεθεί</string>\n  <string name=\"settings_ui_scale\">Κλίμακα UI</string>\n  <string name=\"msg_restart_app\">Παρακαλώ, επανεκκινήστε την εφαρμογή για να εφαρμοστούν οι ρυθμίσεις</string>\n  <string name=\"settings_block\">Αποκλεισμός περιεχομένου</string>\n  <string name=\"msg_applying\">Εφαρμογή %s</string>\n  <string name=\"msg_done\">Έτοιμο</string>\n  <string name=\"settings_general\">Γενικά</string>\n  <string name=\"settings_video\">Βίντεο</string>\n  <string name=\"content_block_confirm_skip\">Επιβεβαίωση κατά την παράλειψη</string>\n  <string name=\"content_block_categories\">Παράλειψη τμημάτων</string>\n  <string name=\"content_block_sponsor\">Χορηγός</string>\n  <string name=\"content_block_intro\">Διάλειμμα/εφέ έναρξης</string>\n  <string name=\"content_block_outro\">Τίτλοι τέλους/εύσημα</string>\n  <string name=\"content_block_interaction\">Υπενθύμιση αλληλεπίδρασης (εγγραφή)</string>\n  <string name=\"content_block_self_promo\">Αφιλοκερδώς/αυτοπροώθηση</string>\n  <string name=\"content_block_music_off_topic\">Τμήμα χωρίς μουσική</string>\n  <string name=\"confirm_segment_skip\">Παράλειψη τμήματος \\\"%s\\\";</string>\n  <string name=\"cancel_segment_skip\">Ακύρωση παράλειψης τμήματος</string>\n  <string name=\"msg_skipping_segment\">Παράλειψη τμήματος \\\"%s\\\"...</string>\n  <string name=\"content_block_notification_type\">Τύπος ειδοποίησης</string>\n  <string name=\"content_block_notify_none\">Χωρίς ειδοποίηση</string>\n  <string name=\"content_block_notify_toast\">Toast</string>\n  <string name=\"content_block_notify_dialog\">Ερώτηση για επιβεβαίωση</string>\n  <string name=\"return_to_launcher\">Επιστροφή στον εκκινητή από τα κανάλια ATV/αναζήτηση</string>\n  <string name=\"intent_force_close\">Βίαιη έξοδος αν το βίντεο έχει ανοιχθεί από εξωτερική πηγή</string>\n  <string name=\"btn_confirm\">Επιβεβαίωση</string>\n  <string name=\"player_low_video_quality\">Χαμηλή ποιότητα βίντεο</string>\n  <string name=\"remote_session_closed\">Η απομακρυσμένη συνεδρία έχει κλείσει</string>\n  <string name=\"msg_mode_switch_error\">Δεν είναι δυνατή η εναλλαγή λειτουργίας οθόνης σε \\\"%s\\\"</string>\n  <string name=\"msg_player_error\">Προκλήθηκε σφάλμα αναπαραγωγής %s. Επανεκκίνηση αναπαραγωγής...</string>\n  <string name=\"video_preset_disabled\">Χωρίς προ-ρύθμιση</string>\n  <string name=\"video_preset_enabled\">Η μορφή του βίντεο θα καθοριστεί από την προ-ρύθμιση</string>\n  <string name=\"player_sleep_timer\">Χρονοδιακόπτης ύπνου</string>\n  <string name=\"player_show_quality_info\">Εμφάνιση πληροφοριών ποιότητας στη μπάρα ελέγχου</string>\n  <string name=\"player_remember_each_speed\">Αποθήκευση ταχύτητας για κάθε βίντεο</string>\n  <string name=\"player_remember_speed_none\">Όχι</string>\n  <string name=\"player_remember_speed_all\">Ίδια για όλα τα βίντεο</string>\n  <string name=\"player_remember_speed_each\">Διαφορετική για κάθε βίντεο</string>\n  <string name=\"msg_player_error_source\">Δεν είναι δυνατή η λήψη του βίντεο</string>\n  <string name=\"msg_player_error_renderer\">Η επιλεγμένη μορφή βίντεο δεν υποστηρίζεται.\\nΊσως είναι σφάλματα λογισμικού. Δοκιμάστε να κάνετε επανεκκίνηση της συσκευής.</string>\n  <string name=\"msg_player_error_unexpected\">Άγνωστο σφάλμα αποκωδικοποιητή βίντεο</string>\n  <string name=\"video_aspect\">Αναλογία οθόνης</string>\n  <string name=\"player_tweaks\">Επιλογές προγραμματιστή</string>\n  <string name=\"header_uploads\">Μεταμορφώσεις</string>\n  <string name=\"player_show_global_clock\">Εμφάνιση πάντα-ενεργού ρολογιού στην οθόνη</string>\n  <string name=\"player_show_global_ending_time\">Εμφάνιση πάντα-ενεργής ώρας λήξης  στην οθόνη</string>\n  <string name=\"uploads_old_look\">Επαναφορά στη προηγούμενη εμφάνιση του τμήματος Μεταμορφώσεις</string>\n  <string name=\"background_playback_activation\">Αναπαραγωγή στο παρασκήνιο (ενεργοποίηση)</string>\n  <string name=\"channels_old_look\">Επαναφορά στη προηγούμενη εμφάνιση του τμήματος Κανάλια</string>\n  <string name=\"channels_auto_load\">Αυτόματη φόρτωση του περιεχομένου του τμήματος Κανάλια</string>\n  <string name=\"return_to_background_video\">Επιστροφή στο βίντεο που παίζει στο παρασκήνιο</string>\n  <string name=\"pin_unpin_from_sidebar\">Προσθήκη/Αφαίρεση από τη πλαϊνή στήλη </string>\n  <string name=\"pin_unpin_playlist\">Προσθήκη/Αφαίρεση playlist από τη πλαϊνή στήλη</string>\n  <string name=\"pin_unpin_channel\">Προσθήκη/Αφαίρεση καναλιού από τη πλαϊνή στήλη</string>\n  <string name=\"pinned_to_sidebar\">Προστέθηκε στη πλαϊνή στήλη</string>\n  <string name=\"unpin_from_sidebar\">Αφαίρεση απ\\' τη πλαϊνή στήλη</string>\n  <string name=\"unpinned_from_sidebar\">Ξεκαρφιτσώθηκε απ\\' τη πλαϊνή στήλη</string>\n  <string name=\"dialog_select_country\">Χώρα</string>\n  <string name=\"settings_language_country\">Γλώσσα/Χώρα</string>\n  <string name=\"share_embed_link\">Κοινοποίηση συνδέσμου ενσωμάτωσης</string>\n  <string name=\"card_text_scroll_factor\">Ταχύτητα ροής κειμένου κάρτας</string>\n  <string name=\"preferred_update_source\">Επιλέξτε πηγή ενημέρωσης</string>\n  <string name=\"hide_shorts\">Απόκρυψη shorts από τις Εγγραφές</string>\n  <string name=\"key_remapping\">Επαναπροσδιορισμός κλειδιού</string>\n  <string name=\"screen_dimming\">Συσκότιση οθόνης</string>\n  <string name=\"removed_from_playback_queue\">Αφαιρέθηκε από την ουρά αναπαραγωγής</string>\n  <string name=\"added_to_playback_queue\">Προστέθηκε από την ουρά αναπαραγωγής</string>\n  <string name=\"add_remove_from_playback_queue\">Προσθήκη/Αφαίρεση από την ουρά αναπαραγωγής</string>\n  <string name=\"add_to_playback_queue\">Προσθήκη στην ουρά αναπαραγωγής</string>\n  <string name=\"remove_from_playback_queue\">Αφαίρεση από την ουρά αναπαραγωγής</string>\n  <string name=\"proxy_enabled\">Το proxy ενεργοποιήθηκε</string>\n  <string name=\"proxy_disabled\">Το proxy απενεργοποιήθηκε</string>\n  <string name=\"uploads_row_name\">Μεταφορτώσεις</string>\n  <string name=\"playlists_row_name\">Δημιουργημένες playlist</string>\n  <string name=\"popular_uploads_row_name\">Δημοφιλείς μεταφορτώσεις</string>\n  <string name=\"breaking_news_row_name\">Έκτακτες ειδήσεις</string>\n  <string name=\"covid_news_row_name\">Ειδήσεις για COVID-19 </string>\n  <string name=\"enable_voice_search\">Ενεργοποίηση φωνητικής αναζήτησης</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Εγγραφή/Απεγγραφή από το κανάλι</string>\n  <string name=\"app_backup_restore\">Αντίγραφο ασφαλείας/επαναφορά</string>\n  <string name=\"app_backup\">Αντίγραφο ασφαλείας δεδομένων</string>\n  <string name=\"app_restore\">Επαναφορά δεδομένων από το αντίγραφο ασφαλείας</string>\n  <string name=\"skip_each_segment_once\">Μην παραλείψεις τμήματα ξανά</string>\n  <string name=\"disable_ok_long_press\">Απενεργοποίηση παρατεταμένου πατήματος του κουμπιού ΟΚ</string>\n  <string name=\"player_time_correction\">Εμφάνιση ώρας βάσει ταχύτητας</string>\n  <string name=\"sponsor_color_markers\">Χρωματισμός σημαδιών στη μπάρα προόδου</string>\n  <string name=\"refresh_section\">Ανανέωση τμήματος</string>\n  <string name=\"option_disabled\">Απενεργοποιήθηκε</string>\n  <string name=\"live_now_row_name\">Ζωντανά τώρα</string>\n  <string name=\"run_in_background\">Αναπαραγωγή στο παρασκήνιο</string>\n  <string name=\"settings_remote_control\">Απομακρυσμένος έλεγχος</string>\n  <string name=\"background_service_started\">Η υπηρεσία παρασκηνίου ξεκίνησε</string>\n  <string name=\"dialog_add_to\">Προσθήκη σε %s</string>\n  <string name=\"dialog_remove_from\">Αφαίρεση από %s</string>\n  <string name=\"added_to\">Προστέθηκε στο %s</string>\n  <string name=\"removed_from\">Αφαιρέθηκε από το %s</string>\n  <string name=\"video_preset_adaptive\">Προσαρμοστικό</string>\n  <string name=\"content_block_preview_recap\">Προεπισκόπηση ή περίληψη του βίντεο</string>\n  <string name=\"content_block_highlight\">Ενδιαφέρον σημείο (σελιδοδείκτης)</string>\n  <string name=\"removed_from_history\">Το βίντεο αφαιρέθηκε από το ιστορικό</string>\n  <string name=\"remove_from_history\">Αφαίρεση από το ιστορικό</string>\n  <string name=\"upload_date\">Ημερομηνία μεταφόρτωσης</string>\n  <string name=\"upload_date_any\">Οποιαδήποτε στιγμή</string>\n  <string name=\"upload_date_today\">Σήμερα</string>\n  <string name=\"upload_date_this_week\">Αυτή τη βδομάδα</string>\n  <string name=\"upload_date_this_month\">Αυτόν τον μήνα</string>\n  <string name=\"upload_date_this_year\">Φέτος</string>\n  <string name=\"double_refresh_rate\">Διπλάσιος ρυθμός ανανέωσης</string>\n  <string name=\"upload_date_last_hour\">Τελευταία ώρα</string>\n  <string name=\"focus_on_search_results\">Αυτόματη εστίαση στα αποτελέσματα αναζήτησης</string>\n  <string name=\"context_menu\">Μενού περιεχομένων</string>\n  <string name=\"add_remove_from_recent_playlist\">Προσθήκη/αφαίρεση από πρόσφατη playlist</string>\n  <string name=\"hide_settings_section\">Απόκρυψη τμήματος Ρυθμίσεις (επικίνδυνο!)</string>\n  <string name=\"volume\">Ένταση %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s δλ</string>\n  <string name=\"audio_shift_sec\">%s δλ</string>\n  <string name=\"ui_hide_timeout_sec\">%s δλ</string>\n  <string name=\"screen_dimming_timeout_min\">%s λ</string>\n  <string name=\"mark_all_channels_watched\">Επισήμανση όλων των καναλιών ως προβεβλημένα</string>\n  <string name=\"move_section_up\">Μετακίνηση τμήματος προς τα πάνω</string>\n  <string name=\"move_section_down\">Μετακίνηση τμήματος προς τα κάτω</string>\n  <string name=\"player_buttons\">Ρύθμιση κουμπιών player</string>\n  <string name=\"action_playlist_add\">Προσθήκη στη λίστα αναπαραγωγής</string>\n  <string name=\"action_video_stats\">Στατιστικά βίντεο</string>\n  <string name=\"action_subscribe\">Εγγραφή</string>\n  <string name=\"action_channel\">Άνοιγμα καναλιού</string>\n  <string name=\"action_pip\">PiP</string>\n  <string name=\"action_video_info\">Περιγραφή βίντεο</string>\n  <string name=\"action_screen_off\">Οθόνη κλειστή</string>\n  <string name=\"action_playback_queue\">Ουρά αναπαραγωγής</string>\n  <string name=\"action_video_speed\">Ταχύτητα βίντεο</string>\n  <string name=\"action_subtitles\">Υπότιτλοι</string>\n  <string name=\"action_like\">Μου αρέσει</string>\n  <string name=\"action_dislike\">Δε μου αρέσει</string>\n  <string name=\"action_play_pause\">Αναπαραγωγή/παύση</string>\n  <string name=\"action_repeat_mode\">Λειτουργία αναπαραγωγής</string>\n  <string name=\"action_next\">Επόμενο βίντεο</string>\n  <string name=\"action_previous\">Προηγούμενο βίντεο</string>\n  <string name=\"action_high_quality\">Ποιότητα βίντεο</string>\n  <string name=\"content_block_no_skipping_mode\">Λειτουργία μη-παράλειψης</string>\n  <string name=\"content_block_action_type\">Επιλογή ενέργειας</string>\n  <string name=\"content_block_action_none\">Μην κάνεις τίποτα</string>\n  <string name=\"content_block_action_only_skip\">Μόνο παράλειψη</string>\n  <string name=\"content_block_action_toast\">Παράλειψη με ειδοποίηση</string>\n  <string name=\"content_block_action_dialog\">Εμφάνιση διαλόγου επιβεβαίωσης</string>\n  <string name=\"content_block_filler\">Εκτός θέματος (filler)</string>\n  <string name=\"keyboard_auto_show\">Εμφάνιση πληκτρολογίου αυτόματα</string>\n  <string name=\"cancel_dialog\">Ακύρωση</string>\n  <string name=\"msg_player_error_source2\">Η πηγή του βίντεο δε λειτουργεί ή έχει λάθος ώρα</string>\n  <string name=\"hide_upcoming\">Απόκρυψη επερχόμενων από τις Εγγραφές</string>\n  <string name=\"search_background_playback\">Εκκίνηση PIP κατά την αναζήτηση/άνοιγμα καναλιού</string>\n  <string name=\"trending_row_name\">Τάσεις</string>\n  <string name=\"player_seek_type\">Συμπεριφορά κατά την αναζήτηση</string>\n  <string name=\"player_seek_regular\">Κανονική</string>\n  <string name=\"player_seek_confirmation_pause\">Με επιβεβαίωση (παύση κατά την αναζήτηση)</string>\n  <string name=\"player_seek_confirmation_play\">Με επιβεβαίωση(αναπαραγωγή κατά την αναζήτηση)</string>\n  <string name=\"hide_shorts_from_home\">Απόκρυψη shorts από την Αρχική</string>\n  <string name=\"finish_on_disconnect\">Τερματισμός εφαρμογής μετά την αποσύνδεση τηλεφώνου/τάμπλετ</string>\n  <string name=\"update_error\">Σφάλμα ενημέρωσης</string>\n  <string name=\"hide_shorts_from_history\">Απόκρυψη shorts από το Ιστορικό</string>\n  <string name=\"disable_screensaver\">Απενεργοποίηση screensaver</string>\n  <string name=\"description_not_found\">Δε βρέθηκε περιγραφή</string>\n  <string name=\"proxy_port_hint\">Πύλη proxy</string>\n  <string name=\"proxy_host_hint\">Proxy hostname ή IP</string>\n  <string name=\"proxy_username_hint\">Όνομα χρήστη proxy</string>\n  <string name=\"proxy_password_hint\">Κωδικός proxy</string>\n  <string name=\"proxy_type\">Τύπος proxy</string>\n  <string name=\"enable_web_proxy\">Χρήση web proxy</string>\n  <string name=\"proxy_not_supported\">Χρήση web proxy(απαιτείται Android 4.4+)</string>\n  <string name=\"proxy_test_btn\">Δοκιμή</string>\n  <string name=\"proxy_settings_title\">Ρυθμίσεις εξυπηρετητή server</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">Δοκιμή#%d: ακυρώθηκε.</string>\n  <string name=\"proxy_type_invalid\">Ο τύπος proxy δεν ρυθμίστηκε.</string>\n  <string name=\"proxy_host_invalid\">Ο proxy host δεν ρυθμίστηκε.</string>\n  <string name=\"proxy_port_invalid\">Άκυρη πύλη proxy, πρέπει &gt;0.</string>\n  <string name=\"proxy_credentials_invalid\">Άκυρο όνομα χρήστη ή κωδικός.</string>\n  <string name=\"proxy_test_aborted\">Η δοκιμή ακυρώθηκε, παρακαλώ διορθώσετε τις ρυθμίσεις proxy πρώτα.</string>\n  <string name=\"proxy_application_aborted\">Παρακαλώ, διορθώσετε τις ρυθμίσεις proxy πρώτα.</string>\n  <string name=\"proxy_test_start\">Δοκιμή#%d: %s...</string>\n  <string name=\"proxy_test_error\">Σφάλμα#%d: %s</string>\n  <string name=\"proxy_test_status\">Κατάσταση#%d %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Διαμόρφωση διεύθυνσης αρχείου (*.ovpn)</string>\n  <string name=\"enable_openvpn\">Χρήση OpenVPN</string>\n  <string name=\"openvpn_settings_title\">Άνοιγμα ρυθμίσεων OpenVPN</string>\n  <string name=\"openvpn_application_aborted\">Παρακαλώ, διορθώσετε τις ρυθμίσεις του OpenVPN πρώτα.</string>\n  <string name=\"openvpn_test_aborted\">Η δοκιμή ακυρώθηκε, παρακαλώ διορθώστε τις ρυθμίσεις του OpenVPN πρώτα.</string>\n  <string name=\"openvpn_address_invalid\">Η διαμόρφωση της διεύθυνσης του OpenVPN δεν έχει οριστεί.</string>\n  <string name=\"internet_censorship\">Λογοκρισία διαδικτύου</string>\n  <string name=\"rename_section\">Μετονομασία αυτού του τμήματος</string>\n  <string name=\"simple_edit_value_hint\">Νέα τιμή</string>\n  <string name=\"seek_interval\">Αναζήτηση μεσοδιαστήματος</string>\n  <string name=\"seek_interval_sec\">%s δλ</string>\n  <string name=\"subtitle_system\">Στυλ συστήματος (ρυθμίσεις Android &gt; Προσβασιμότητα)</string>\n  <string name=\"subtitle_scale\">Κλίμακα υπότιτλου</string>\n  <string name=\"audio_sync_fix_desc\">Ένας εναλλακτικός τρόπος συγχρονισμού ήχου/εικόνας. Θεωρείται απαρχαιωμένος, μπορεί σε κάποιες περιπτώσεις να βοηθήσει.</string>\n  <string name=\"audio_sync_fix\">Διόρθωση συγχρονισμού ήχου</string>\n  <string name=\"playback_queue_category_title\">Ουρά αναπαραγωγής</string>\n  <string name=\"open_playlist\">Άνοιγμα λίστας αναπαραγωγής</string>\n  <string name=\"repeat_mode_pause_alt\">Συνεχόμενη αναπαραγωγή μόνο στα βίντεο της λίστας αναπαραγωγής</string>\n  <string name=\"subtitle_white_transparent\">Λευκό σε διαφανές φόντο</string>\n  <string name=\"player_show_ending_time\">Εμφάνιση ώρας λήξης στη μπάρα ελέγχου</string>\n  <string name=\"ambilight_ratio_fix_desc\">Επιδιορθώνει την έλλειψη οπίσθιου φωτισμού. Επιδιορθώνει τη λάθος αναλογία οθόνης. Επιδιορθώνει κενά στιγμιότυπα οθόνης. Επηρεάζει τις επιδόσεις!</string>\n  <string name=\"ambilight_ratio_fix\">Επιδιόρθωση Ambilight/αναλογίας οθόνης/στιγμιότυπων οθόνης</string>\n  <string name=\"force_legacy_codecs_desc\">Βελτιώνει σημαντικά τις επιδόσεις σε αδύναμες συσκευές. Μέγιστη ανάλυση 720p.</string>\n  <string name=\"force_legacy_codecs\">Επιβολή κωδικοποιητές παλαιού τύπου</string>\n  <string name=\"live_stream_fix_desc\">Βελτιώνει σημαντικά τις επιδόσεις στη ζωντανή μετάδοση σε αδύναμες συσκευές. Μέγιστη ανάλυση 1080p.</string>\n  <string name=\"live_stream_fix\">Διόρθωση ζωντανής μετάδοσης (1080p)</string>\n  <string name=\"playback_notifications_fix_desc\">Κρύβει τις ειδοποιήσεις εναλλαγής κομματιών. Ίσως χρήσιμο σε λογισμικά AOSP</string>\n  <string name=\"playback_notifications_fix\">Απενεργοποίηση ειδοποιήσεων αναπαραγωγής</string>\n  <string name=\"amlogic_fix_desc\">Επιδιόρθωση απώλειας καρέ σε συσκευές Amlogic.</string>\n  <string name=\"amlogic_fix\">Επιδιόρθωση Amlogic 1080\\@60fps</string>\n  <string name=\"tunneled_video_playback_desc\">ΣΗΜΕΊΩΣΗ: Η παύση μπορεί να μην λειτουργεί σωστά! Το Tunneled video playback υπόσχεται βελτιώσεις όπως καλύτερο συγχρονισμό ήχου/εικόνας (AV sync) και ομαλότερη αναπαραγωγή. Απαιτείται Android 5+</string>\n  <string name=\"tunneled_video_playback\">Αναπαραγωγή βίντεο μέσω tunneled (Android 5+)</string>\n  <string name=\"master_volume\">Υπέρτατη ένταση</string>\n  <string name=\"volume_limit\">Όριο έντασης</string>\n  <string name=\"play_video\">Αναπαραγωγή</string>\n  <string name=\"remember_position_of_short_videos\">Να θυμάται τη θέση μικρών βίντεο (λιγότερο από 10λ)</string>\n  <string name=\"player_show_tooltips\">Εμφάνιση πληροφοριών κουμπιών</string>\n  <string name=\"action_like_unset\">αφαιρέθηκε \\\"μου αρέσει\\\"</string>\n  <string name=\"action_dislike_unset\">αφαιρέθηκε \\\"δεν μου αρέσει\\\"</string>\n  <string name=\"various_buttons\">Κουμπιά στο πάνω μέρος του κυρίως παραθύρου</string>\n  <string name=\"not_compatible_with\">Η επιλεγμένη ρύθμιση δεν είναι συμβατή με</string>\n  <string name=\"subtitle_position\">Κάτω ολίσθηση υποτίτλων</string>\n  <string name=\"pressing_home\">Πατώντας ΑΡΧΙΚΗ</string>\n  <string name=\"pressing_home_back\">Πατώντας ΑΡΧΙΚΗ ή ΠΊΣΩ</string>\n  <string name=\"player_number_key_seek\">Αναζήτηση με αριθμητικά κουμπιά</string>\n  <string name=\"save_remove_playlist\">Προσθήκη/Διαγραφή λίστας αναπαραγωγής από Λίστες αναπαραγωγής</string>\n  <string name=\"remove_playlist\">Διαγραφή λίστας αναπαραγωγής από Λίστες αναπαραγωγής μόνιμα</string>\n  <string name=\"removed_from_playlists\">Αφαιρέθηκε από Λίστες αναπαραγωγής</string>\n  <string name=\"saved_to_playlists\">Προστέθηκε σε Λίστες αναπαραγωγής</string>\n  <string name=\"create_playlist\">Δημιουργία λίστας αναπαραγωγής</string>\n  <string name=\"add_video_to_new_playlist\">Προσθήκη σε νέα λίστα αναπαραγωγής</string>\n  <string name=\"cant_delete_empty_playlist\">Δεν είναι δυνατή η διαγραφή κενής λίστας αναπαραγωγής</string>\n  <string name=\"playlist\">Λίστα αναπαραγωγής</string>\n  <string name=\"rename_playlist\">Μετονομασία λίστας αναπαραγωγής</string>\n  <string name=\"cant_rename_empty_playlist\">Δεν είναι δυνατή η μετονομασία κενής λίστας αναπαραγωγής</string>\n  <string name=\"cant_rename_foreign_playlist\">Δεν είναι δυνατή η μετονομασία ξένης λίστας αναπαραγωγής</string>\n  <string name=\"cant_save_playlist\">Αυτή η λίστα αναπαραγωγής δεν μπορεί να προστεθεί</string>\n  <string name=\"enter_value\">Εισάγετε μια όχι κενή τιμή</string>\n  <string name=\"dialog_add_remove_from\">Προσθήκη/Αφαίρεση από %s</string>\n  <string name=\"lb_playback_controls_skip_next\">Παράλειψη Επομένου</string>\n  <string name=\"lb_playback_controls_skip_previous\">Παράλειψη προηγουμένου/Επιστροφή στην αρχή</string>\n  <string name=\"alt_presets_behavior\">Συμπεριφορά εναλλακτικών προ-ρυθμίσεων</string>\n  <string name=\"repeat_mode_shuffle\">Τυχαία αναπαραγωγή όλων των λιστών αναπαραγωγής</string>\n  <string name=\"disable_mic_permission\">Παρακαλώ, απενεργοποιήστε την άδεια μικροφώνου για την εφαρμογή ώστε η αναγνώριση να δουλέψει σωστά.</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Σκούρο γκρι (μονόχρωμο)</string>\n  <string name=\"player_pixel_ratio\">Αναλογία εικονοστοιχείων</string>\n  <string name=\"subtitle_yellow_black\">Κίτρινο σε μαύρο φόντο</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Κίτρινο σε ημιδιαφανές φόντο</string>\n  <string name=\"real_channel_icon\">Εμφάνιση πραγματικού εικονιδίου στο κουμπί κανάλι</string>\n  <string name=\"speech_recognizer_external_2\">Εξωτερικός 2 (πολλά σφάλματα)</string>\n  <string name=\"speech_recognizer_external_1\">Εξωτερικός 1 (πολλά σφάλματα, για να δουλέψει απενεργοποιήστε την άδεια μικροφώνου)</string>\n  <string name=\"speech_recognizer_system\">Συστήματος (προτείνεται)</string>\n  <string name=\"speech_recognizer\">Αναγνώριση ομιλίας</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Σοβαρά σφάλματα. Να χρησιμοποιηθεί μόνο αν υπάρχουν προβλήματα με την προεπιλεγμένη αναγνώριση</string>\n  <string name=\"time_format_12\">12ωρο (ΠΜ/ΜΜ)</string>\n  <string name=\"time_format_24\">24ωρο</string>\n  <string name=\"time_format\">Τύπος ώρας</string>\n  <string name=\"use_alt_speech_recognizer\">Εναλλακτική αναγνώριση ομιλίας</string>\n  <string name=\"place_chat_left\">Τοποθέτηση συζήτησης αριστερά</string>\n  <string name=\"open_chat\">Συζήτηση/Σχόλια</string>\n  <string name=\"prefer_avc_over_vp9\">Επιλογή κωδικοποιητή: προτίμηση avc από vp9</string>\n  <string name=\"releases\">Κυκλοφορίες</string>\n  <string name=\"sources\">Πηγές</string>\n  <string name=\"feedback\">Σχόλια</string>\n  <string name=\"player_disable_suggestions\">Απενεργοποίηση προτάσεων</string>\n  <string name=\"skip_24_rate\">Παράλειψη μορφής 24fps (Επιδιόρθωση λειτουργίας κινηματογράφου;)</string>\n  <string name=\"action_subscribe_on\">Εγγραφή ενεργή</string>\n  <string name=\"action_subscribe_off\">Εγγραφή ανενεργή</string>\n  <string name=\"playback_controls_repeat_list\">Επανάληψη λίστας</string>\n  <string name=\"playback_controls_repeat_pause\">επανάληψη παύσης</string>\n  <string name=\"add_device_view_description\">Για να συνδέσετε αυτή τη συσκευή εισάγετε αυτόν τον κωδικό στην εφαρμογή YouTube στο τμήμα Ρυθμίσεις/Παρακολουθήστε σε TV\\nΣΗΜΕΙΩΣΗ. Δουλεύει μόνο με το πληκτρολόγιο Gboard</string>\n  <string name=\"badge_live\">ΖΩΝΤΑΝΑ</string>\n  <string name=\"badge_new_content\">ΝΕΟ ΠΕΡΙΕΧΟΜΕΝΟ</string>\n  <string name=\"player_ending_time\">Τελειώνει στις %s</string>\n  <string name=\"player_remaining_time\">Απομένει: %s</string>\n  <string name=\"msg_press_again_to_exit\">Πατήστε ξανά για έξοδο</string>\n  <string name=\"require_checked\">Απαιτεί \\\"%s</string>\n  <string name=\"signin_view_action_text\">Έγινε</string>\n  <string name=\"signin_view_description\">Για να συνδεθείτε εισάγετε αυτόν τον κωδικό στη σελίδα %s\\nΣΗΜΕΙΩΣΗ: Δουλεύει μόνο με τον Chrome ή τον Firefox</string>\n  <string name=\"signin_view_title\">Φόρτωση Κωδικού Χρήστη...</string>\n  <string name=\"action_playlist_remove\">Αφαίρεση από λίστα αναπαραγωγής</string>\n  <string name=\"starting_stream\">Εκκίνηση της ροής...</string>\n  <string name=\"playback_starts_shortly\">Η αναπαραγωγή θα ξεκινά αυτόματα όταν η ροή είναι έτοιμη</string>\n  <string name=\"set_stream_reminder\">Ορισμός υπενθύμισης ροής</string>\n  <string name=\"unset_stream_reminder\">Διαγραφή υπενθύμισης ροής</string>\n  <string name=\"content_block_alt_server_desc\">Ενεργοποιήστε αυτή την επιλογή αν το SponsorBlock αρνείται να δουλέψει για διάφορους λόγους</string>\n  <string name=\"content_block_alt_server\">Χρήση εναλλακτικού εξυπηρετητή</string>\n  <string name=\"owned_playlist_warning\">Σφάλμα. Αυτή η ενέργεια είναι δυνατή μόνο στις δικές σας λίστες αναπαραγωγής</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">Δεν είναι δυνατό αυτό σε ξένη λίστα αναπαραγωγής</string>\n  <string name=\"playlist_order_popularity\">Δημοτικότητα</string>\n  <string name=\"playlist_order_published_date_older_first\">Ημερομηνία δημοσίευσης (πρώτα τα παλαιότερα)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Ημερομηνία δημοσίευσης (πρώτα τα νεότερα)</string>\n  <string name=\"playlist_order_added_date_older_first\">Ημερομηνία προσθήκης (πρώτα τα παλαιότερα)</string>\n  <string name=\"playlist_order_added_date_newer_first\">Ημερομηνία προσθήκης (πρώτα τα νεότερα)</string>\n  <string name=\"playlist_order\">Ταξινόμηση λίστας αναπαραγωγής</string>\n  <string name=\"force_sw_codec_desc\">Μπορεί να παίξει σχεδόν οποιοδήποτε βίντεο αλλά με χαμηλές επιδόσεις</string>\n  <string name=\"force_sw_codec\">Επιβολή αποκωδικοποιητή βίντεο SW</string>\n  <string name=\"skip_codec_profile_check_desc\">Μην γίνει έλεγχος υποστήριξης κωδικοποιητών όταν ξεκινάει ένα βίντεο. Ίσως φανεί χρήσιμο σε λογισμικά με σφάλματα</string>\n  <string name=\"skip_codec_profile_check\">Παράλειψη ελέγχου επιπέδου προφίλ κωδικοποιητή</string>\n  <string name=\"disable_vsync_desc\">Βελτιώνει λιγάκι τις επιδόσεις στην αναπαραγωγή</string>\n  <string name=\"disable_vsync\">Απενεργοποίηση snap στο vsync</string>\n  <string name=\"sleep_timer_desc\">Παύση αναπαραγωγής αν ο χρήστης δε χρησιμοποίησε το τηλεχειριστήριο για 1 ώρα</string>\n  <string name=\"sleep_timer\">Ενεργοποίηση χρονοδιακόπτη ύπνου (1 ώρα)</string>\n  <string name=\"alt_presets_behavior_desc\">Η εφαρμογή θα προσπαθήσει να διατηρήσει το bandwidth σε σχέση με την επιλεγμένη προεπιλογή αντί να ταιριάζει μεταξύ ανάλυσης, fps και κωδικοποιητή.</string>\n  <string name=\"chat_left\">Αριστερά</string>\n  <string name=\"chat_right\">Δεξιά</string>\n  <string name=\"card_real_thumbnails\">Αντικαταστήστε τις μικρογραφίες με ένα πλαίσιο από το βίντεο</string>\n  <string name=\"thumb_quality_default\">Προεπιλογή</string>\n  <string name=\"thumb_quality_start\">Έναρξη του βίντεο</string>\n  <string name=\"thumb_quality_middle\">Στη μέση του βίντεο</string>\n  <string name=\"thumb_quality_end\">Τέλος του βίντεο</string>\n  <string name=\"card_content\">Από που να πάρει τις μικρογραφίες</string>\n  <string name=\"content_block_status\">Ελέγξτε την κατάσταση του διακομιστή SponsorBlock</string>\n  <string name=\"player_show_quality_info_bitrate\">Προσθέστε ρυθμό μετάδοσης bit στις πληροφορίες ποιότητας</string>\n  <string name=\"player_speed_button_old_behavior\">Επαναφέρετε την παλιά συμπεριφορά του κουμπιού ταχύτητας</string>\n  <string name=\"protect_settings_with_password\">Προστατέψτε όλες τις ρυθμίσεις με κωδικό πρόσβασης</string>\n  <string name=\"enter_settings_password\">Εισαγάγετε τον κωδικό πρόσβασης ρυθμίσεων</string>\n  <string name=\"child_mode\">Παιδική λειτουργία</string>\n  <string name=\"child_mode_desc\">Σε αυτήν τη λειτουργία, ο χρήστης δεν μπορεί να χρησιμοποιήσει την αναζήτηση ή να δει κάποιο προτεινόμενο περιεχόμενο. Οι ρυθμίσεις θα βρίσκονται κάτω από τον κωδικό πρόσβασης.</string>\n  <string name=\"lost_setting_warning\">Οι ρυθμίσεις της εφαρμογής θα αλλάξουν. Βεβαιωθείτε ότι έχετε δημιουργήσει ένα αντίγραφο ασφαλείας των ρυθμίσεων.</string>\n  <string name=\"player_button_long_click\">Ένα σύντομο κλικ στο κουμπί για γρήγορη δράση και ένα παρατεταμένο παράθυρο διαλόγου στις ρυθμίσεις</string>\n  <string name=\"pause_history\">Παύση ιστορικού</string>\n  <string name=\"resume_history\">Συνέχιση ιστορικού</string>\n  <string name=\"clear_history\">Καθαρισμός ιστορικού</string>\n  <string name=\"chapters\">Κεφάλαια</string>\n  <string name=\"card_multiline_subtitle\">Υπότιτλοι πολλαπλών γραμμών</string>\n  <string name=\"auto_frame_rate_modes\">Υποστηριζόμενες λειτουργίες</string>\n  <string name=\"loading\">Φόρτωση…</string>\n  <string name=\"video_buffer_size_none\">Καθόλου</string>\n  <string name=\"pressing_back\">Πατώντας ΠΙΣΩ</string>\n  <string name=\"hide_streams\">Απόκρυψη ροών από τις Συνδρομές</string>\n  <string name=\"video_rotate\">Περιστροφή</string>\n  <string name=\"trending_searches\">Δημοφιλείς αναζητήσεις</string>\n  <string name=\"video_duration_any\">Οσηδήποτε</string>\n  <string name=\"video_duration\">Διάρκεια</string>\n  <string name=\"video_duration_under_4\">Κάτω από 4 λεπτά</string>\n  <string name=\"video_duration_between_4_20\">4-20 λεπτά</string>\n  <string name=\"video_duration_over_20\">Πάνω από 20 λεπτά</string>\n  <string name=\"content_type\">Είδος</string>\n  <string name=\"content_type_any\">Οτιδήποτε</string>\n  <string name=\"content_type_video\">Βίντεο</string>\n  <string name=\"content_type_channel\">Κανάλι</string>\n  <string name=\"content_type_playlist\">Λίστα αναπαραγωγής</string>\n  <string name=\"content_type_movie\">Ταινία</string>\n  <string name=\"video_features\">Λειτουργίες</string>\n  <string name=\"video_feature_any\">Οποιαδήποτε</string>\n  <string name=\"video_feature_live\">Ζωντανά</string>\n  <string name=\"video_feature_4k\">4Κ</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">Ταξινόμηση κατά</string>\n  <string name=\"sort_by_relevance\">Σχετικότητα</string>\n  <string name=\"sort_by_views\">Πλήθος προβολών</string>\n  <string name=\"sort_by_date\">Ημερομηνία μεταμόρφωσης</string>\n  <string name=\"sort_by_rating\">Βαθμολογία </string>\n  <string name=\"clear_search_history\">Διαγραφή ιστορικού αναζήτησης</string>\n  <string name=\"remove_from_subscriptions\">Απόκρυψη</string>\n  <string name=\"player_long_speed_list\">Λίστα υψηλής ταχύτητας</string>\n  <string name=\"alt_app_icon\">Εναλλακτικό εικονίδιο εφαρμογής (απαιτείται επανεκκίνηση)</string>\n  <string name=\"network_stack\">Προτίμηση στοίβας δικτύου %s</string>\n  <string name=\"cronet_desc\">Το Cronet είναι στοίβα δικτύου Chromium. Το Cronet μπορεί να μειώσει την καθυστέρηση και να αυξήσει την απόδοση του δικτύου, που μπορεί να βελτιώσει τα προβλήματα με το buffering.</string>\n  <string name=\"unlock_all_formats\">Ξεκλείδωμα όλων των μορφών βίντεο</string>\n  <string name=\"unlock_all_formats_desc\">Σε κάποιες συσκευές, το λογισμικό αναφέρει εσφαλμένα κάποιες μορφές ως μη υποστηριζόμενες ακόμα και αν είναι.\\n(πχ 1080p smart TVs τείνουν να αναφέρουν το 4Κ ως μη υποστηριζόμενο).</string>\n  <string name=\"okhttp_desc\">Αυτή η στοίβα δικτύου είναι η πιο αργή αλλά, παρέχει σταθερότητα.</string>\n  <string name=\"select_channel_section\">Άνοιγμα σχετικού τμήματος όταν η εφαρμογή ανοίγει από τα Κανάλια ATV</string>\n  <string name=\"enable_voice_search_desc\">Η επίσημη εφαρμογή θα αντικατασταθεί από τη λεγόμενη γέφυρα. Μια γέφυρα βοηθάει στη μεταφορά καθολικών αποτελεσμάτων αναζήτησης στην εφαρμογή μας.</string>\n  <string name=\"enable_master_password\">Ενεργοποίηση κύριου κωδικού πρόσβασης</string>\n  <string name=\"enter_master_password\">Εισαγάγετε τον κύριο κωδικό πρόσβασης</string>\n  <string name=\"disable_stream_buffer\">Απενεργοποίηση buffer στις ροές</string>\n  <string name=\"disable_stream_buffer_desc\">Διόρθωση για καταστάσεις στις οποίες η ροή είναι αρκετά πίσω. Σημείωση: η ροή μπορεί να αρχίσει να καθυστερεί.</string>\n  <string name=\"sony_frame_drop_fix\">Διόρθωση μείωσης καρέ #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">Διορθώστε καθυστερήσεις στην τηλεόραση Sony και σε ορισμένες άλλες συσκευές. Σημείωση: πιθανά προβλήματα με το συγχρονισμό ήχου.</string>\n  <string name=\"audio_language\">Γλώσσα ήχου</string>\n  <string name=\"old_home_look\">Παλιά εμφάνιση της ενότητας Αρχική</string>\n  <string name=\"hide_shorts_everywhere\">Κρύψτε τα shorts από παντού</string>\n  <string name=\"update_found\">Ενημέρωση</string>\n  <string name=\"volume_boost_warning\">Τυχόν ρυθμίσεις πάνω από το όριο μπορεί να βλάψουν τα ηχεία</string>\n  <string name=\"play_video_incognito\">Αναπαραγωγή σε ανώνυμη περιήγηση</string>\n  <string name=\"header_kids_home\">Παιδιά</string>\n  <string name=\"player_section_playlist\">Χρήση περιεχομένου τρέχοντος τμήματος ως λίστα αναπαραγωγής</string>\n  <string name=\"header_trending\">Τάσεις</string>\n  <string name=\"screensaver\">Προφύλαξη οθόνης</string>\n  <string name=\"player_screen_off_timeout\">Λήξη χρονικού ορίου οθόνης</string>\n  <string name=\"old_update_notifications\">Η παλιά εμφάνιση ειδοποίησης ενημέρωσης</string>\n  <string name=\"mark_as_watched\">Επισήμανση ως προβληθέν</string>\n  <string name=\"player_ui_animations\">Ενεργοποίηση εφέ κίνησης πίνακα ελέγχου</string>\n  <string name=\"player_likes_count\">Εμφάνιση αριθμού μου αρέσει/δεν μου αρέσει</string>\n  <string name=\"sorting_alphabetically2\">Αλφαβητικά (γρήγορο)</string>\n  <string name=\"sorting_default\">Προεπιλογή (γρήγορο)</string>\n  <string name=\"content_block_exclude_channel\">Εξαίρεση καναλιού από το SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">Όχι εξαίρεση καναλιού από το SponsorBlock</string>\n  <string name=\"player_chapter_notification\">Εμφάνιση ειδοποίησης για γρήγορη μετακίνηση μεταξύ των κεφαλαίων</string>\n  <string name=\"subtitle_remember\">Απομνημόνευση ενεργών υποτίτλων ανά κανάλι</string>\n  <string name=\"amazon_frame_drop_fix\">Επιδιόρθωση απώλειας καρέ #2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">Προορίζεται για συσκευές Amazon Stick. Ίσως δουλεύει και σε άλλες.</string>\n  <string name=\"default_stack_desc\">Ενσωματωμένη στοίβα δικτύου. Αυτή η στοίβα ίσως είναι πιο σταθερή σε κάποιες περιπτώσεις. </string>\n  <string name=\"item_postion\">Θέση του</string>\n  <string name=\"player_screen_off_dimming\">Ποσότητα συσκότισης οθόνης</string>\n  <string name=\"screensaver_timout\">Λήξη χρονικού ορίου προφύλαξης οθόνης </string>\n  <string name=\"screensaver_dimming\">Ποσότητα σκοτεινιάσματος προφύλαξης οθόνης</string>\n  <string name=\"player_network_stack\">Στοίβα δικτύου</string>\n  <string name=\"header_shorts\">Shorts</string>\n  <string name=\"player_auto_volume\">Αυτόματη ρύθμιση έντασης</string>\n  <string name=\"player_ui_on_next\">Εμφάνιση της διεπαφής χρήστη του προγράμματος αναπαραγωγής κατά τη μετάβαση στο επόμενο βίντεο</string>\n  <string name=\"player_global_focus\">Απρόσκοπτη πλοήγηση μεταξύ σειρών κουμπιών αναπαραγωγής</string>\n  <string name=\"auto_history\">Αυτόματα (χρήση ρυθμίσεων λογαριασμού)</string>\n  <string name=\"remember_position_subscriptions\">Να θυμάται τη θέση της τελευταίας προβολής στις Συνδρομές</string>\n  <string name=\"msg_player_unknown_error\">Άγνωστο σφάλμα</string>\n  <string name=\"header_notifications\">Ειδοποιήσεις</string>\n  <string name=\"disable_search_history\">Απενεργοποίηση ιστορικού αναζήτησης</string>\n  <string name=\"unlock_high_bitrate_formats\">Ξεκλειδώστε μορφές υψηλού bitrate 1080p vp9</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Ξεκλειδώστε μορφές υψηλού bitrate mp4</string>\n  <string name=\"color_scheme_blue\">Μπλε</string>\n  <string name=\"color_scheme_dark_blue\">Σκούρο Μπλε</string>\n  <string name=\"player_speed_per_channel\">Για κάθε κανάλι</string>\n  <string name=\"disable_popular_searches\">Απενεργοποιήστε δημοφιλή ερωτήματα αναζήτησης</string>\n  <string name=\"multi_profiles\">Χρησιμοποιήστε ξεχωριστές ρυθμίσεις για κάθε λογαριασμό</string>\n  <string name=\"protect_account_with_password\">Προστατέψτε αυτόν τον λογαριασμό με κωδικό πρόσβασης</string>\n  <string name=\"enter_account_password\">Εισαγάγετε τον κωδικό πρόσβασης λογαριασμού</string>\n  <string name=\"show_connect_messages\">Εμφάνιση μηνυμάτων σύνδεσης</string>\n  <string name=\"prefer_avc_over_vp9_desc\">ΠΡΟΕΙΔΟΠΟΙΗΣΗ: 1080p μέγιστο</string>\n  <string name=\"play_next\">Αναπαραγωγή επόμενου</string>\n  <string name=\"hide_watched_from_subscriptions\">Απόκρυψη βίντεο που παρακολουθήσατε από τις Συνδρομές</string>\n  <string name=\"hide_unwanted_content\">Απόκρυψη ανεπιθύμητου περιεχομένου</string>\n  <string name=\"hide_watched_from_home\">Απόκρυψη βίντεο από την Αρχική</string>\n  <string name=\"remote_control_permission\">Το τηλεχειριστήριο παρασκηνίου απαιτεί άδεια επικάλυψης</string>\n  <string name=\"login_from_browser\">Είσοδος από το πρόγραμμα περιήγησης</string>\n  <string name=\"disable_remote_history\">Απενεργοποιήστε το ιστορικό όταν χρησιμοποιείτε τηλεχειριστήριο</string>\n  <string name=\"keyboard_fix\">Διορθώστε το πληκτρολόγιο εμφάνισης με το πάτημα του πλήκτρου OK (G20 και άλλα)</string>\n  <string name=\"nothing_found\">Δεν βρέθηκε τίποτα</string>\n  <string name=\"auto_frame_rate_desc\">Αυτή η επιλογή αφαιρεί το τρέμουλο σε σκηνές όπου η κάμερα κινείται γρήγορα, π.χ. αθλητική ροή</string>\n  <string name=\"channel_filter_hint\">Φίλτρο καναλιών</string>\n  <string name=\"player_loop_shorts\">Επαναλαμβανόμενα Shorts</string>\n  <string name=\"player_quick_shorts_skip\">Παράλειψη Shorts με αριστερό/δεξί κουμπί</string>\n  <string name=\"channels_filter\">Εμφάνιση του πεδίου \\\"φιλτράρισμα καναλιών\\\" μέσα στο τμήμα Κανάλια</string>\n  <string name=\"channel_search_bar\">Γραμμή αναζήτησης μέσα στη σελίδα του καναλιού</string>\n  <string name=\"dialog_notification\">Ειδοποίηση για την ενημέρωση με αναδυόμενο παράθυρο διαλόγου</string>\n  <string name=\"disable_history\">Απενεργοποίηση ιστορικού</string>\n  <string name=\"enable_history\">Ενεργοποίηση ιστορικού</string>\n  <string name=\"open_comments\">Ανοίξτε τα σχόλια</string>\n  <string name=\"save_playlist\">Προσθήκη λίστας αναπαραγωγής στην ενότητα Λίστες αναπαραγωγής</string>\n  <string name=\"player_volume\">Ένταση</string>\n  <string name=\"live_stream_fix_4k_desc\">Προειδοποίηση, αυτή η παραμετροποίηση απενεργοποιεί την επαναφορά των ροών. Βελτιώνει σημαντικά την απόδοση της ζωντανής ροής. Η μέγιστη ανάλυση είναι 4K.</string>\n  <string name=\"hide_shorts_from_trending\">Απόκρυψη Shorts από τις Τάσεις</string>\n  <string name=\"hide_upcoming_home\">Απόκρυψη επερχόμενων από την Αρχική σελίδα</string>\n  <string name=\"hide_upcoming_channel\">Απόκρυψη επερχόμενων από το Κανάλι</string>\n  <string name=\"msg_player_error_subtitle_source\">Το URL ΥΠΟΤΙΤΛΟΥ δεν λειτουργεί ή η ώρα της συσκευής είναι εσφαλμένη.\\nΣυνήθως, είναι το πρόβλημα της εφαρμογής.</string>\n  <string name=\"msg_player_error_audio_source\">Το URL ΗΧΟΥ δεν λειτουργεί ή η ώρα της συσκευής είναι εσφαλμένη.\\nΣυνήθως, είναι το πρόβλημα της εφαρμογής.</string>\n  <string name=\"msg_player_error_video_source\">Το URL ΒΙΝΤΕΟ δεν λειτουργεί ή η ώρα της συσκευής είναι εσφαλμένη.\\nΣυνήθως, είναι το πρόβλημα της εφαρμογής.</string>\n  <string name=\"hide_shorts_channel\">Απόκρυψη shorts από το Κανάλι</string>\n  <string name=\"pin_channel\">Προσθήκη καναλιού στην πλαϊνή γραμμή</string>\n  <string name=\"pin_playlist\">Προσθήκη λίστας αναπαραγωγής στην πλαϊνή γραμμή</string>\n  <string name=\"player_corner_ending_time\">Πρόγραμμα Αναπαραγωγής: πάνω δεξιά γωνία ώρα λήξης</string>\n  <string name=\"player_corner_clock\">Πρόγραμμα Αναπαραγωγής: ρολόι πάνω δεξιά γωνία</string>\n  <string name=\"app_corner_clock\">Αρχική: πάνω δεξιά γωνία ώρα λήξης</string>\n  <string name=\"msg_player_error_subtitle_renderer\">Η επιλεγμένη μορφή ΥΠΟΤΙΤΛΟΥ δεν υποστηρίζεται.\\nΔοκιμάστε να επιλέξετε άλλη πατώντας παρατεταμένα το κουμπί CC στη συσκευή αναπαραγωγής.\\nΕάν αυτό δεν σας βοηθήσει, δοκιμάστε να επανεκκινήσετε τη συσκευή.</string>\n  <string name=\"msg_player_error_audio_renderer\">Η επιλεγμένη μορφή ΗΧΟΥ δεν υποστηρίζεται.\\nΔοκιμάστε να επιλέξετε άλλη πατώντας το κουμπί HQ στη συσκευή αναπαραγωγής.\\nΕάν αυτό δεν σας βοηθήσει, δοκιμάστε να επανεκκινήσετε τη συσκευή.</string>\n  <string name=\"msg_player_error_video_renderer\">Η επιλεγμένη μορφή ΒΙΝΤΕΟ δεν υποστηρίζεται.\\nΔοκιμάστε να επιλέξετε άλλη πατώντας το κουμπί HQ στη συσκευή αναπαραγωγής.\\nΕάν αυτό δεν σας βοηθήσει, δοκιμάστε να επανεκκινήσετε τη συσκευή.</string>\n  <string name=\"share_qr_link\">Κοινή χρήση συνδέσμου (κωδικός QR)</string>\n  <string name=\"you_wont_see_this_channel\">Δεν θα βλέπετε αυτό το κανάλι στις προτάσεις</string>\n  <string name=\"not_recommend_channel\">Μη προτείνεται το κανάλι</string>\n  <string name=\"news_row_name\">Ειδήσεις</string>\n  <string name=\"disable_ok_long_press_desc\">Προορίζεται για μη πλήρως λειτουργικά χειριστήρια όπου το κουμπί OK δεν λειτουργεί σωστά</string>\n  <string name=\"live_stream_fix_4k\">Διόρθωση ζωντανής μετάδοσης (4K)</string>\n  <string name=\"autogenerated\">δημιουργήθηκε αυτόματα</string>\n  <string name=\"skip_shorts\">Παράλειψη Shorts</string>\n  <string name=\"player_exit_shortcut\">Έξοδος από το πρόγραμμα αναπαραγωγής</string>\n  <string name=\"video_buffer_size_lowest\">Κατώτατο</string>\n  <string name=\"remember_position_of_live_videos\">Να θυμάται τη θέση των ζωντανών ροών</string>\n  <string name=\"speech_engine\">Μηχανή φωνητικής αναζήτησης</string>\n  <string name=\"dearrow_status\">Ελέγξτε την κατάσταση του διακομιστή DeArrow</string>\n  <string name=\"player_extra_long_speed_list\">Λίστα εξαιρετικά μεγάλων ταχυτήτων</string>\n  <string name=\"old_channel_look\">Παλιά εμφάνιση της σελίδας του Καναλιού</string>\n  <string name=\"remember_position_pinned\">Να θυμάται τη θέση της τελευταίας προβολής στις καρφιτσωμένες λίστες αναπαραγωγής</string>\n  <string name=\"unknown_source_error\">Άγνωστο σφάλμα πηγής</string>\n  <string name=\"unknown_renderer_error\">Άγνωστο σφάλμα απόδοσης</string>\n  <string name=\"hide_watched_from_notifications\">Απόκρυψη βίντεο από τις Ειδοποιήσεις</string>\n  <string name=\"hide_watched_from_watch_later\">Απόκρυψη βίντεο που παρακολουθήσατε από τη λίστα αναπαραγωγής Παρακολούθηση αργότερα</string>\n  <string name=\"player_quick_skip_videos\">Παράλειψη κανονικών βίντεο με αριστερό/δεξί κουμπί</string>\n  <string name=\"header_sports\">Αθλητικά</string>\n  <string name=\"keep_finished_activities\">Διατηρήστε τις ολοκληρωμένες δραστηριότητες</string>\n  <string name=\"disable_channels_service\">Απενεργοποιήστε την υπηρεσία Καναλιών</string>\n  <string name=\"replace_titles\">Αντικατάσταση τίτλων</string>\n  <string name=\"enable\">Ενεργοποίηση</string>\n  <string name=\"more_info\">Περισσότερες πληροφορίες</string>\n  <string name=\"about_sponsorblock\">Σχετικά με το SponsorBlock</string>\n  <string name=\"about_dearrow\">Σχετικά με το DeArrow</string>\n  <string name=\"replace_thumbnails\">Αντικαταστήστε τις μικρογραφίες</string>\n  <string name=\"crowdsourced_thumbnails\">Μικρογραφίες επιλεγμένες από το κοινό</string>\n  <string name=\"crowdsoursed_titles\">Τίτλοι επιλεγμένοι από το κοινό</string>\n  <string name=\"dearrow_not_submitted_thumbs\">Πηγή μικρογραφίας (αν δεν υπάρχει υποβολή DeArrow)</string>\n  <string name=\"pitch_effect\">Εφέ τόνου</string>\n  <string name=\"fullscreen_mode\">Λειτουργία πλήρους οθόνης (χωρίς γραμμές συστήματος)</string>\n  <string name=\"player_only_mode\">Εμφάνιση μόνο του προγράμματος αναπαραγωγής εάν το βίντεο είναι ανοιχτό εκτός της εφαρμογής</string>\n  <string name=\"pinned_channel_rows\">Εμφάνιση καρφιτσωμένων καναλιών ως σειρές</string>\n  <string name=\"prefer_ipv4\">Προτίμηση IPv4 DNS</string>\n  <string name=\"prefer_ipv4_desc\">Θα μπορούσε να διορθώσει καταστάσεις όταν η εφαρμογή δεν λειτουργεί καθόλου.\\nΣημείωση. Μπορεί να προκαλέσει κολλήματα και σφάλματα (ειδικά σε συσκευές Android 8 ή Dune HD)</string>\n  <string name=\"long_press_for_settings\">ΠΑΤΗΣΤΕ ΠΑΡΑΤΕΤΑΜΕΝΑ ΓΙΑ ΡΥΘΜΙΣΕΙΣ</string>\n  <string name=\"long_press_for_options\">ΠΑΤΗΣΤΕ ΠΑΡΑΤΕΤΑΜΕΝΑ ΓΙΑ ΕΠΙΛΟΓΕΣ</string>\n  <string name=\"device_specific_backup\">Δημιουργία αντιγράφων ασφαλείας μόνο για αυτήν τη συσκευή</string>\n  <string name=\"local_backup\">Τοπικό αντίγραφο ασφαλείας</string>\n  <string name=\"auto_backup\">Αυτόματη δημιουργία αντιγράφων ασφαλείας (μία φορά την ημέρα)</string>\n  <string name=\"repeat_mode_reverse_list\">Παίξτε τη λίστα αναπαραγωγής ή τα βίντεο του καναλιού με αντίστροφη σειρά</string>\n  <string name=\"calm_msg\">Διορθώνουμε το πρόβλημα. Ελέγχετε για ενημερώσεις ανά διαστήματα</string>\n  <string name=\"without_picture\">Χωρίς εικόνα</string>\n  <string name=\"video_disabled\">Βίντεο απενεργοποιημένο</string>\n  <string name=\"applying_fix\">Μην κλείνετε το πρόγραμμα αναπαραγωγής. Εφαρμογή της επιδιόρθωσης...</string>\n  <string name=\"hide_mixes\">Απόκρυψη Mixes</string>\n  <string name=\"player_global_focus_desc\">Αυτή η δυνατότητα επηρεάζει το κουμπί του προγράμματος αναπαραγωγής που θα εστιάζει κατά την πλοήγηση μεταξύ σειρών κουμπιών αναπαραγωγής</string>\n  <string name=\"disable_network_error_fixing\">Απενεργοποιήστε την αυτόματη διόρθωση σφαλμάτων δικτύου</string>\n  <string name=\"disable_network_error_fixing_desc\">Μάλλον θα πρέπει να ενεργοποιήσετε αυτήν την επιλογή εάν χρησιμοποιείτε VPN</string>\n  <string name=\"recommended\">Προτεινόμενα</string>\n  <string name=\"action_sound_off\">Ήχος κλειστός</string>\n  <string name=\"not_supported_by_device\">Αυτή η συσκευή δεν υποστηρίζει αύτη τη δυνατότητα</string>\n  <string name=\"video_buffer_size_highest\">Μέγιστο</string>\n  <string name=\"original_lang\">Αρχική</string>\n  <string name=\"player_toggle_speed\">Εναλλαγή ταχύτητας ναι/όχι</string>\n  <string name=\"search_exit_shortcut\">Έξοδος από αναζήτηση</string>\n  <string name=\"unpin_group_from_sidebar\">Αφαίρεση ομάδας συνδρομών</string>\n  <string name=\"context_menu_sorting\">Ταξινόμηση μενού περιεχομένων</string>\n  <string name=\"hide_shorts_from_search\">Απόκρυψη shorts από το αποτέλεσμα αναζήτησης</string>\n  <string name=\"remove_playlist_fmt\">Αφαίρεση %s μόνιμα;</string>\n  <string name=\"create_playlist_note\">δε θα φαίνεται στην εφαρμογή YouTube</string>\n  <string name=\"suggestions\">Προτάσεις</string>\n  <string name=\"place_comments_left\">Τοποθέτηση σχολίων αριστερά</string>\n  <string name=\"video_flip\">Αναστροφή (καθρέπτης)</string>\n  <string name=\"play_from_start\">Αναπαραγωγή από την αρχή</string>\n  <string name=\"player_chapter_notification2\">Αλλαγή στο επόμενο κεφάλαιο με κλικ στην ειδοποίηση</string>\n  <string name=\"player_quick_shorts_skip_alt\">Παράλειψη Shorts με πάνω/κάτω κουμπί</string>\n  <string name=\"player_quick_skip_videos_alt\">Παράλειψη κανονικών βίντεο με πάνω/κάτω κουμπί</string>\n  <string name=\"prefer_google_dns\">Προτίμηση Google DNS</string>\n  <string name=\"add_to_subscriptions_group\">Προσθήκη/Αφαίρεση από ομάδα συνδρομών</string>\n  <string name=\"new_subscriptions_group\">Νέα ομάδα</string>\n  <string name=\"rename_group\">Μετονομασία ομάδας συνδρομών</string>\n  <string name=\"screen_dimming_amount\">Ποσότητα συσκότισης οθόνης</string>\n  <string name=\"screen_dimming_timeout\">Χρονικό όριο συσκότισης οθόνης</string>\n  <string name=\"playlists_rows\">Εμφάνιση τμήματος Λίστες αναπαραγωγής σε γραμμές</string>\n  <string name=\"import_subscriptions_group\">Εισαγωγή</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">Απενεργοποίηση Λεζάντων</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">Ενεργοποίηση Λεζάντων</string>\n  <string name=\"my_videos\">Τα βίντεό μου</string>\n  <string name=\"player_audio_focus\">Εστίαση ήχου (παύση αν ανιχνευθούν άλλα προγράμματα αναπαραγωγής)</string>\n  <string name=\"paid_content_notification\">Ειδοποίηση περιεχομένου επί πληρωμή</string>\n  <string name=\"you_liked\">σας άρεσε</string>\n  <string name=\"auto_backup_category\">Αυτόματο αντίγραφο ασφαλείας</string>\n  <string name=\"once_a_day\">Κάθε μέρα</string>\n  <string name=\"once_a_week\">Κάθε βδομάδα</string>\n  <string name=\"once_a_month\">Κάθε μήνα</string>\n  <string name=\"stable_restore\">Επαναφορά σταθερής δομής (θα χαθούν όλα τα δεδομένα)</string>\n  <string name=\"suggestions_horizontally_scrolled\">Προτάσεις με οριζόντια κύλιση</string>\n  <string name=\"card_preview\">Προεπισκόπηση κάρτας</string>\n  <string name=\"card_preview_full\">Βίντεο με ήχο</string>\n  <string name=\"card_preview_muted\">Βίντεο χωρίς ήχο</string>\n  <string name=\"oculus_quest_fix\">Επιδιόρθωση Oculus Quest</string>\n  <string name=\"playback_buffering_fix\">Επιδιόρθωση buffering αναπαραγωγής</string>\n  <string name=\"premium_users_only\">Μόνο για χρήστες Premium. Επιδιόρθωση μορφής λίστας μη ολοκληρωμένων βίντεο</string>\n  <string name=\"card_unlocalized_titles\">Μη μεταφρασμένοι τίτλοι</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">Διάλογος \\\"μη γίνεται προασαρμογή βίντεο για ταίριασμα\\\"</string>\n  <string name=\"typing_corrections\">Απενεργοποίηση αυτόματης διόρθωσης κατά την πληκτρολόγηση κειμένου</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-es/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Inicio</string>\n  <string name=\"title_search\">Buscar</string>\n  <string name=\"header_subscriptions\">Suscripciones</string>\n  <string name=\"header_history\">Historial</string>\n  <string name=\"header_music\">Música</string>\n  <string name=\"header_news\">Noticias</string>\n  <string name=\"header_gaming\">Videojuegos</string>\n  <string name=\"header_playlists\">Listas de reproducción</string>\n  <string name=\"header_settings\">Configuración</string>\n  <string name=\"header_channels\">Canales</string>\n  <string name=\"subscriptions_signin_title\">Descubre lo último de los canales que te gustan</string>\n  <string name=\"subscriptions_signin_subtitle\">Inicia sesión para ver tus suscripciones</string>\n  <string name=\"subscriptions_signin_button_text\">REGISTRARSE</string>\n  <string name=\"library_signin_to_show_more\">Consulta los vídeos que te han gustado, que has guardado o a los que te has suscrito</string>\n  <string name=\"library_signin_subtitle\">Inicia sesión para ver tu biblioteca</string>\n  <string name=\"action_signin\">REGISTRARSE</string>\n  <string name=\"title_video_formats\">Formatos de vídeo</string>\n  <string name=\"title_audio_formats\">Formatos de audio</string>\n  <string name=\"subtitle_category_title\">Subtítulos</string>\n  <string name=\"playback_queue_category_title\">Cola de reproducción</string>\n  <string name=\"video_max_quality\">Auto (calidad máxima)</string>\n  <string name=\"audio_max_quality\">Auto (calidad máxima)</string>\n  <string name=\"subtitles_disabled\">Subtítulos desactivados</string>\n  <string name=\"auto_frame_rate\">Velocidad de fotogramas automática</string>\n  <string name=\"frame_rate_correction\">Corregir fps: \\n%s</string>\n  <string name=\"category_background_playback\">Reproducción de fondo</string>\n  <string name=\"not_implemented\">No se ha implementado</string>\n  <string name=\"option_background_playback_off\">Desactivado</string>\n  <string name=\"option_background_playback_pip\">Imagen en imagen</string>\n  <string name=\"option_background_playback_only_audio\">Solo audio</string>\n  <string name=\"playback_settings\">Configuración de reproducción</string>\n  <string name=\"video_speed\">Velocidad de vídeo</string>\n  <string name=\"resolution_switch\">Cambiar resolución</string>\n  <string name=\"video_buffer\">Búfer de video</string>\n  <string name=\"video_buffer_size_none\">Nada</string>\n  <string name=\"video_buffer_size_low\">Bajo</string>\n  <string name=\"video_buffer_size_med\">Medio</string>\n  <string name=\"video_buffer_size_high\">Alto</string>\n  <string name=\"update_changelog\">Registro de cambios</string>\n  <string name=\"install_update\">Instalar actualización</string>\n  <string name=\"section_is_empty\">¡Ups! No hay nada aquí.</string>\n  <string name=\"dialog_add_to_playlist\">Añadir/eliminar de la lista de reproducción</string>\n  <string name=\"msg_signed_users_only\">Solo usuarios registrados</string>\n  <string name=\"msg_cant_load_content\">No se puede cargar el contenido.\\n1) Comprueba la fecha y la hora del dispositivo.\\n2) Comprueba la conexión a la red.\\n3) Intenta acceder a tu cuenta.</string>\n  <string name=\"title_video_presets\">Ajustes preestablecidos de vídeo</string>\n  <string name=\"settings_accounts\">Cuentas</string>\n  <string name=\"settings_left_panel\">Editar categorías</string>\n  <string name=\"settings_themes\">Temas</string>\n  <string name=\"settings_other\">Otro</string>\n  <string name=\"settings_player\">Configuración del reproductor</string>\n  <string name=\"settings_language\">Idioma</string>\n  <string name=\"settings_linked_devices\">Dispositivos vinculados</string>\n  <string name=\"settings_about\">Acerca de</string>\n  <string name=\"dialog_account_list\">Seleccionar cuenta</string>\n  <string name=\"dialog_account_none\">Ninguna</string>\n  <string name=\"dialog_remove_account\">Eliminar cuenta</string>\n  <string name=\"dialog_add_account\">Añadir cuenta</string>\n  <string name=\"default_lang\">Por defecto</string>\n  <string name=\"dialog_select_language\">Idioma de la aplicación</string>\n  <string name=\"subtitle_default\">Por defecto</string>\n  <string name=\"subtitle_white_semi_transparent\">Fondo semitransparente</string>\n  <string name=\"subtitle_style\">Estilo subtítulos</string>\n  <string name=\"subtitle_language\">Idioma subtítulos</string>\n  <string name=\"action_search\">Iniciar búsqueda</string>\n  <string name=\"settings_main_ui\">Interfaz de usuario</string>\n  <string name=\"dialog_main_ui\">Interfaz de usuario</string>\n  <string name=\"card_animated_previews\">Previsualizaciones animadas</string>\n  <string name=\"web_site\">Sitio web</string>\n  <string name=\"donation\">Donar</string>\n  <string name=\"dialog_about\">Acerca de</string>\n  <string name=\"dialog_player_ui\">Reproductor de vídeo</string>\n  <string name=\"player_show_ui_on_pause\">Mostrar interfaz en pausa</string>\n  <string name=\"player_pause_on_ok\">Botón OK pausa reproducción</string>\n  <string name=\"player_ok_button_behavior\">Comportamiento botón OK</string>\n  <string name=\"player_only_ui\">Solo interfaz</string>\n  <string name=\"player_ui_and_pause\">Interfaz y pausa</string>\n  <string name=\"player_only_pause\">Solo pausa</string>\n  <string name=\"check_for_updates\">Buscar actualizaciones</string>\n  <string name=\"update_not_found\">Estás usando la última versión</string>\n  <string name=\"update_in_progress\">Espera…</string>\n  <string name=\"player_ui_hide_behavior\">Auto-ocultar interfaz</string>\n  <string name=\"option_never\">Nunca</string>\n  <string name=\"side_panel_sections\">Secciones visibles</string>\n  <string name=\"boot_to_section\">Iniciar en sección</string>\n  <string name=\"large_ui\">Interfaz grande</string>\n  <string name=\"video_grid_scale\">Escala de cuadrícula de video</string>\n  <string name=\"scale_ui\">Escala de interfaz</string>\n  <string name=\"color_scheme\">Esquema de color</string>\n  <string name=\"color_scheme_default\">Por defecto</string>\n  <string name=\"color_scheme_red_grey\">Rojo-gris</string>\n  <string name=\"color_scheme_red\">Rojo</string>\n  <string name=\"color_scheme_dark_grey\">Gris oscuro</string>\n  <string name=\"disable_update_check\">Desactivar comprobación de actualizaciones</string>\n  <string name=\"show_again\">Mostrar de nuevo</string>\n  <string name=\"check_updates_auto\">Notificar sobre la nueva versión</string>\n  <string name=\"select_account_on_boot\">Seleccionar en arranque</string>\n  <string name=\"player_other\">Miscelánea</string>\n  <string name=\"player_full_date\">Fecha exacta en descripción</string>\n  <string name=\"open_channel\">Abrir canal</string>\n  <string name=\"open_playlist\">Abrir lista de reproducción</string>\n  <string name=\"not_interested\">No me interesa</string>\n  <string name=\"not_recommend_channel\">No recomendar canal</string>\n  <string name=\"you_wont_see_this_video\">Vídeo eliminado de recomendaciones</string>\n  <string name=\"you_wont_see_this_channel\">No verás este canal en las recomendaciones.</string>\n  <string name=\"settings_search\">Buscar</string>\n  <string name=\"dialog_search\">Buscar</string>\n  <string name=\"instant_voice_search\">Búsqueda instantánea de voz</string>\n  <string name=\"option_background_playback_behind\">Reproducir de fondo</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">Siguiente vídeo no cargado todavía</string>\n  <string name=\"card_multiline_title\">Títulos multilínea</string>\n  <string name=\"cards_style\">Estilo tarjetas</string>\n  <string name=\"repeat_mode_all\">Reproducir todos uno por uno</string>\n  <string name=\"repeat_mode_one\">Repetir vídeo actual</string>\n  <string name=\"repeat_mode_pause\">Pausar después de cada vídeo</string>\n  <string name=\"repeat_mode_pause_alt\">Pausar después de cada vídeo fuera de lista de reproducción</string>\n  <string name=\"repeat_mode_none\">Parar después de un vídeo</string>\n  <string name=\"subscribed_to_channel\">Suscrito al canal</string>\n  <string name=\"unsubscribed_from_channel\">No estás Suscrito al canal</string>\n  <string name=\"subtitle_yellow_transparent\">Amarillo sobre fondo transparente</string>\n  <string name=\"subtitle_white_transparent\">Blanco sobre fondo transparente</string>\n  <string name=\"subtitle_white_black\">Blanco sobre fondo negro</string>\n  <string name=\"player_seek_preview\">Previsualizar mientras busca</string>\n  <string name=\"color_scheme_dark_grey_oled\">Gris oscuro (OLED)</string>\n  <string name=\"channels_section_sorting\">Orden de la sección canales</string>\n  <string name=\"sorting_by_new_content\">Nuevo contenido</string>\n  <string name=\"sorting_alphabetically\">Alfabéticamente</string>\n  <string name=\"sorting_last_viewed\">Última visualización</string>\n  <string name=\"player_pause_when_seek\">Pausar al buscar</string>\n  <string name=\"playlists_style\">Estilo listas de reproducción</string>\n  <string name=\"playlists_style_grid\">Cuadrícula</string>\n  <string name=\"playlists_style_rows\">Filas</string>\n  <string name=\"player_show_clock\">Mostrar reloj en menu emergente</string>\n  <string name=\"player_show_remaining_time\">Mostrar tiempo restante</string>\n  <string name=\"player_show_ending_time\">Mostrar tiempo finalización</string>\n  <string name=\"open_channel_uploads\">Abrir canal Subidos</string>\n  <string name=\"app_exit_shortcut\">Salir de la aplicación</string>\n  <string name=\"app_exit_none\">No salir</string>\n  <string name=\"app_double_back_exit\">Doble Atrás</string>\n  <string name=\"app_single_back_exit\">Atrás simple</string>\n  <string name=\"action_video_zoom\">Zoom de vídeo</string>\n  <string name=\"video_zoom\">Zoom de vídeo</string>\n  <string name=\"video_zoom_default\">Por defecto</string>\n  <string name=\"video_zoom_fit_width\">Ajustar ancho</string>\n  <string name=\"video_zoom_fit_height\">Ajustar alto</string>\n  <string name=\"video_zoom_fit_both\">Ajustar ancho o alto</string>\n  <string name=\"video_zoom_stretch\">Estirar</string>\n  <string name=\"color_scheme_teal\">Turquesa</string>\n  <string name=\"color_scheme_teal_oled\">Turquesa (OLED)</string>\n  <string name=\"player_seek_preview_none\">Deshabilitado</string>\n  <string name=\"player_seek_preview_single\">Fotograma único</string>\n  <string name=\"player_seek_preview_carousel\">Carrusel</string>\n  <string name=\"player_seek_preview_carousel_slow\">Carrusel (lento)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Carrusel (rápido)</string>\n  <string name=\"unsubscribe_from_channel\">Desuscribirse del canal</string>\n  <string name=\"card_title_lines_num\">Título de tarjeta número de líneas</string>\n  <string name=\"card_auto_scrolled_title\">Título recortado con desplazamiento automático</string>\n  <string name=\"share_link\">Compartir enlace</string>\n  <string name=\"subscribe_to_channel\">Suscribirse al canal</string>\n  <string name=\"wait_data_loading\">Por favor, espera mientras se cargan datos…</string>\n  <string name=\"auto_frame_rate_pause\">Velocidad de fotogramas automática (pausa al cambiar)</string>\n  <string name=\"auto_frame_rate_applying\">Aplicando pausa automática del frame rate %sx%s\\@%s</string>\n  <string name=\"audio_shift\">Desplazamiento del audio</string>\n  <string name=\"player_remember_speed\">Recordar velocidad</string>\n  <string name=\"mark_channel_as_watched\">Marcar como visto</string>\n  <string name=\"channel_marked_as_watched\">El canal ha sido marcado como visto</string>\n  <string name=\"dialog_add_device\">Añadir dispositivo</string>\n  <string name=\"dialog_remove_all_devices\">Eliminar todos los dispositivos</string>\n  <string name=\"device_link_enabled\">Vinculación activada</string>\n  <string name=\"device_connected\">El dispositivo \\\"%s\\\" ha sido conectado</string>\n  <string name=\"device_disconnected\">El dispositivo \\\"%s\\\" ha sido desconectado</string>\n  <string name=\"settings_ui_scale\">Escala de interfaz</string>\n  <string name=\"msg_restart_app\">Por favor, reinicia la app para aplicar esta configuración</string>\n  <string name=\"settings_block\">Bloqueo de contenido</string>\n  <string name=\"msg_applying\">Aplicando %s…</string>\n  <string name=\"msg_done\">Hecho</string>\n  <string name=\"settings_general\">General</string>\n  <string name=\"settings_video\">Vídeo</string>\n  <string name=\"content_block_confirm_skip\">Confirmar al saltar</string>\n  <string name=\"content_block_categories\">Omitir segmentos</string>\n  <string name=\"content_block_sponsor\">Patrocinador</string>\n  <string name=\"content_block_intro\">Animación de intermedio/intro</string>\n  <string name=\"content_block_outro\">Tarjetas/créditos finales</string>\n  <string name=\"content_block_interaction\">Recordatorio de interacción (suscribirse)</string>\n  <string name=\"content_block_self_promo\">Autopromoción</string>\n  <string name=\"content_block_music_off_topic\">Sección no musical del clip</string>\n  <string name=\"confirm_segment_skip\">Saltar segmento \\\"%s\\\"\\?</string>\n  <string name=\"cancel_segment_skip\">Cancelar salto de segmento</string>\n  <string name=\"msg_skipping_segment\">Saltando segmento \\\"%s\\\"…</string>\n  <string name=\"content_block_notification_type\">Tipo de notificación</string>\n  <string name=\"content_block_notify_none\">Sin notificación</string>\n  <string name=\"content_block_notify_toast\">Mensaje emergente</string>\n  <string name=\"content_block_notify_dialog\">Diálogo de confirmación</string>\n  <string name=\"return_to_launcher\">Volver al launcher desde ATV canales/búsqueda</string>\n  <string name=\"intent_force_close\">Forzar la salida si el vídeo se ha abierto desde una fuente externa</string>\n  <string name=\"btn_confirm\">Confirmar</string>\n  <string name=\"player_low_video_quality\">Baja calidad de vídeo</string>\n  <string name=\"remote_session_closed\">Se ha cerrado la sesión remota</string>\n  <string name=\"msg_mode_switch_error\">No se puede cambiar el modo de visualización a \\\"%s\\\"</string>\n  <string name=\"msg_player_error\">Error del reproductor %s. Reiniciando reproducción…</string>\n  <string name=\"video_preset_disabled\">Sin preset</string>\n  <string name=\"video_preset_enabled\">El formato del siguiente vídeo se basará en el preset</string>\n  <string name=\"player_sleep_timer\">Temporizador de reposo</string>\n  <string name=\"player_show_quality_info\">Mostrar info calidad vídeo</string>\n  <string name=\"player_remember_each_speed\">Recordar velocidad de cada vídeo</string>\n  <string name=\"player_remember_speed_none\">Ninguno</string>\n  <string name=\"player_remember_speed_all\">Igual en todos los vídeos</string>\n  <string name=\"player_remember_speed_each\">Por cada vídeo</string>\n  <string name=\"msg_player_error_source\">No se puede descargar el vídeo</string>\n  <string name=\"msg_player_error_renderer\">El formato de vídeo seleccionado no es compatible</string>\n  <string name=\"msg_player_error_unexpected\">Error desconocido decodificando vídeo</string>\n  <string name=\"video_aspect\">Relación de aspecto</string>\n  <string name=\"player_tweaks\">Opciones de desarrollo</string>\n  <string name=\"header_uploads\">Subidas</string>\n  <string name=\"player_show_global_clock\">Mostrar reloj siempre en pantalla</string>\n  <string name=\"player_show_global_ending_time\">Mostrar la hora de finalización en pantalla</string>\n  <string name=\"uploads_old_look\">Revertir antiguo aspecto de sección subidas</string>\n  <string name=\"background_playback_activation\">Reproducción de fondo (activación)</string>\n  <string name=\"channels_old_look\">Revertir antiguo aspecto de sección canales</string>\n  <string name=\"channels_auto_load\">Carga automática del contenido de la sección Canales</string>\n  <string name=\"return_to_background_video\">Volver al vídeo reproduciéndose de fondo</string>\n  <string name=\"pin_unpin_from_sidebar\">Fijar/desfijar de barra lateral</string>\n  <string name=\"pin_unpin_playlist\">Fijar/desfijar lista de reproducción</string>\n  <string name=\"pin_unpin_channel\">Fijar/desfijar canal</string>\n  <string name=\"pinned_to_sidebar\">Fijado a barra lateral</string>\n  <string name=\"unpin_from_sidebar\">Desfijar de barra lateral</string>\n  <string name=\"unpinned_from_sidebar\">Desfijado de barra lateral</string>\n  <string name=\"dialog_select_country\">País</string>\n  <string name=\"settings_language_country\">Idioma/país</string>\n  <string name=\"share_embed_link\">Compartir enlace embebido</string>\n  <string name=\"card_text_scroll_factor\">Velocidad desplazamiento texto tarjeta</string>\n  <string name=\"preferred_update_source\">Seleccionar fuente actualizaciones</string>\n  <string name=\"hide_shorts\">Ocultar Shorts de Suscripciones</string>\n  <string name=\"key_remapping\">Remapeo de teclas</string>\n  <string name=\"screen_dimming\">Atenuación de pantalla</string>\n  <string name=\"removed_from_playback_queue\">Eliminado de cola de reproducción</string>\n  <string name=\"added_to_playback_queue\">Añadido a cola de reproducción</string>\n  <string name=\"add_remove_from_playback_queue\">Añadir/Eliminar de cola de reproducción</string>\n  <string name=\"add_to_playback_queue\">Añadir a cola de reproducción</string>\n  <string name=\"remove_from_playback_queue\">Eliminar de cola de reproducción</string>\n  <string name=\"proxy_enabled\">Proxy activado</string>\n  <string name=\"proxy_disabled\">Proxy desactivado</string>\n  <string name=\"uploads_row_name\">Subidas</string>\n  <string name=\"playlists_row_name\">Listas creadas</string>\n  <string name=\"popular_uploads_row_name\">Subidas populares</string>\n  <string name=\"breaking_news_row_name\">Últimas noticias</string>\n  <string name=\"covid_news_row_name\">Noticias COVID-19</string>\n  <string name=\"enable_voice_search\">Activar búsqueda por voz</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Suscribirse/desuscribirse del canal</string>\n  <string name=\"app_backup_restore\">Copia de seguridad/restaurar</string>\n  <string name=\"app_backup\">Copia de seguridad</string>\n  <string name=\"app_restore\">Restaurar copia de seguridad</string>\n  <string name=\"skip_each_segment_once\">No saltar segmentos de nuevo</string>\n  <string name=\"disable_ok_long_press\">Deshabilitar pulsación larga botón OK</string>\n  <string name=\"disable_ok_long_press_desc\">Destinado a mandos con fallos en los que el botón OK no funciona correctamente</string>\n  <string name=\"player_time_correction\">Mostrar el tiempo respecto a la velocidad</string>\n  <string name=\"sponsor_color_markers\">Marcas de color en la barra de progreso</string>\n  <string name=\"refresh_section\">Actualizar sección</string>\n  <string name=\"option_disabled\">Deshabilitada</string>\n  <string name=\"live_now_row_name\">En vivo ahora</string>\n  <string name=\"run_in_background\">Ejecutar en segundo plano</string>\n  <string name=\"settings_remote_control\">Control remoto</string>\n  <string name=\"background_service_started\">Tarea de fondo iniciada</string>\n  <string name=\"dialog_add_to\">Añadir a %s</string>\n  <string name=\"dialog_remove_from\">Eliminar de %s</string>\n  <string name=\"added_to\">Añadido a %s</string>\n  <string name=\"removed_from\">Eliminado de %s</string>\n  <string name=\"video_preset_adaptive\">Adaptativo</string>\n  <string name=\"content_block_preview_recap\">Previsualización o resumen del vídeo</string>\n  <string name=\"content_block_highlight\">Punto o destacado del vídeo</string>\n  <string name=\"removed_from_history\">El vídeo ha sido eliminado del historial</string>\n  <string name=\"remove_from_history\">Eliminar del historial</string>\n  <string name=\"upload_date\">Fecha de subida</string>\n  <string name=\"upload_date_any\">Cualquiera</string>\n  <string name=\"upload_date_today\">Hoy</string>\n  <string name=\"upload_date_this_week\">Esta semana</string>\n  <string name=\"upload_date_this_month\">Este mes</string>\n  <string name=\"upload_date_this_year\">Este año</string>\n  <string name=\"double_refresh_rate\">Doble tasa de refresco</string>\n  <string name=\"upload_date_last_hour\">Última hora</string>\n  <string name=\"focus_on_search_results\">Enfoque automático en resultados de búsqueda</string>\n  <string name=\"context_menu\">Menú contextual</string>\n  <string name=\"add_remove_from_recent_playlist\">Añadir/Eliminar de lista reciente</string>\n  <string name=\"hide_settings_section\">Ocultar sección configuración (¡peligroso!)</string>\n  <string name=\"volume\">Volumen %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s seg</string>\n  <string name=\"audio_shift_sec\">%s seg</string>\n  <string name=\"ui_hide_timeout_sec\">%s seg</string>\n  <string name=\"screen_dimming_timeout_min\">%s min</string>\n  <string name=\"mark_all_channels_watched\">Marcar todos los canales como vistos</string>\n  <string name=\"move_section_up\">Subir sección</string>\n  <string name=\"move_section_down\">Bajar sección</string>\n  <string name=\"player_buttons\">Configurar botones del reproductor</string>\n  <string name=\"action_playlist_add\">Añadir a la lista</string>\n  <string name=\"action_video_stats\">Estadísticas del vídeo</string>\n  <string name=\"action_subscribe\">Suscribirse</string>\n  <string name=\"action_channel\">Abrir canal</string>\n  <string name=\"action_pip\">PIP</string>\n  <string name=\"action_video_info\">Descripción del vídeo</string>\n  <string name=\"action_screen_off\">Apagar pantalla</string>\n  <string name=\"action_playback_queue\">Cola de reproducción</string>\n  <string name=\"action_video_speed\">Velocidad de vídeo</string>\n  <string name=\"action_subtitles\">Subtítulos</string>\n  <string name=\"action_like\">Me gusta</string>\n  <string name=\"action_dislike\">No me gusta</string>\n  <string name=\"action_play_pause\">Reproducir/Pausa</string>\n  <string name=\"action_repeat_mode\">Modo de reproducción</string>\n  <string name=\"action_next\">Siguiente vídeo</string>\n  <string name=\"action_previous\">Anterior vídeo</string>\n  <string name=\"action_high_quality\">Calidad de vídeo</string>\n  <string name=\"content_block_no_skipping_mode\">Modo sin saltos</string>\n  <string name=\"content_block_action_type\">Elegir acción</string>\n  <string name=\"content_block_action_none\">No hacer nada</string>\n  <string name=\"content_block_action_only_skip\">Solo omitir</string>\n  <string name=\"content_block_action_toast\">Omitir con notificación</string>\n  <string name=\"content_block_action_dialog\">Mostar diálogo de confirmación</string>\n  <string name=\"content_block_filler\">Off-topic (relleno)</string>\n  <string name=\"keyboard_auto_show\">Mostrar teclado automáticamente</string>\n  <string name=\"cancel_dialog\">Cancelar</string>\n  <string name=\"msg_player_error_source2\">La fuente de vídeo no funciona o la hora es incorrecta</string>\n  <string name=\"hide_upcoming\">Ocultar próximos de las Suscripciones</string>\n  <string name=\"search_background_playback\">Introducir PIP al buscar desde el reproductor</string>\n  <string name=\"trending_row_name\">Tendencia</string>\n  <string name=\"player_seek_type\">Comportamiento de búsqueda</string>\n  <string name=\"player_seek_regular\">Regular</string>\n  <string name=\"player_seek_confirmation_pause\">Con confirmación (pause mientras busca)</string>\n  <string name=\"player_seek_confirmation_play\">Con confirmación (play mientras busca)</string>\n  <string name=\"hide_shorts_from_home\">Ocultar Shorts de Inicio</string>\n  <string name=\"finish_on_disconnect\">Finalizar la aplicación después de desconectar el teléfono/tableta</string>\n  <string name=\"update_error\">Error de actualización</string>\n  <string name=\"hide_shorts_from_history\">Ocultar Shorts de Historial</string>\n  <string name=\"disable_screensaver\">Desactivar salvapantallas</string>\n  <string name=\"description_not_found\">Descripción no encontrada</string>\n  <string name=\"proxy_port_hint\">Puerto Proxy</string>\n  <string name=\"proxy_host_hint\">Nombre de host o IP del Proxy</string>\n  <string name=\"proxy_username_hint\">Nombre de usuario del Proxy</string>\n  <string name=\"proxy_password_hint\">Contraseña del Proxy</string>\n  <string name=\"proxy_type\">Tipo de Proxy</string>\n  <string name=\"enable_web_proxy\">Utilizar un Proxy Web</string>\n  <string name=\"proxy_not_supported\">Utilizar un Proxy Web (requiere Android 4.4+)</string>\n  <string name=\"proxy_test_btn\">Probar</string>\n  <string name=\"proxy_settings_title\">Configuración del Servidor Proxy</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com\t</string>\n  <string name=\"proxy_test_cancelled\">Prueba #%d: cancelada.</string>\n  <string name=\"proxy_type_invalid\">Tipo de proxy no configurado.</string>\n  <string name=\"proxy_host_invalid\">Proxy host no configurado.</string>\n  <string name=\"proxy_port_invalid\">Puerto Proxy inválido, debe ser &gt; 0.</string>\n  <string name=\"proxy_credentials_invalid\">Nombre de usuario o contraseña no válidos.</string>\n  <string name=\"proxy_test_aborted\">Prueba abortada, por favor arregle la configuración del proxy primero.</string>\n  <string name=\"proxy_application_aborted\">Por favor, corrija primero la configuración del proxy.</string>\n  <string name=\"proxy_test_start\">Prueba#%d: %s …</string>\n  <string name=\"proxy_test_error\">Error#%d: %s</string>\n  <string name=\"proxy_test_status\">Estado#%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Dirección del archivo de configuración (*.ovpn)</string>\n  <string name=\"enable_openvpn\">Utilizar OpenVPN</string>\n  <string name=\"openvpn_settings_title\">Configuración de OpenVPN</string>\n  <string name=\"openvpn_application_aborted\">Por favor, corrija primero la configuración de OpenVPN.</string>\n  <string name=\"openvpn_test_aborted\">Prueba abortada, por favor arregle la configuración de OpenVPN primero.</string>\n  <string name=\"openvpn_address_invalid\">La dirección de configuración de OpenVPN no está configurada.</string>\n  <string name=\"internet_censorship\">Censura en Internet</string>\n  <string name=\"rename_section\">Renombrar sección</string>\n  <string name=\"simple_edit_value_hint\">Nuevo valor</string>\n  <string name=\"seek_interval\">Intervalo de búsqueda</string>\n  <string name=\"seek_interval_sec\">%s seg</string>\n  <string name=\"subtitle_system\">Estilo del sistema (Ajustes de Android &gt; Accesibilidad)</string>\n  <string name=\"subtitle_scale\">Escala de subtítulos</string>\n  <string name=\"audio_sync_fix_desc\">Una forma alternativa de sincronizar audio/vídeo. Se considera obsoleta, pero, en algunos casos, puede ayudar.</string>\n  <string name=\"audio_sync_fix\">Corrección de la sincronización de audio</string>\n  <string name=\"ambilight_ratio_fix_desc\">Corrige la ausencia de luz de polarización. Corrige la relación de aspecto incorrecta. Corrige las capturas de pantalla en blanco. Afecta al rendimiento.</string>\n  <string name=\"ambilight_ratio_fix\">Corrección de Ambilight/Relación de aspecto/Capturas de pantalla</string>\n  <string name=\"force_legacy_codecs_desc\">Mejora significativamente el rendimiento en dispositivos de gama baja. La resolución máxima es de 720p.</string>\n  <string name=\"force_legacy_codecs\">Forzar códecs legacy (720p)</string>\n  <string name=\"live_stream_fix_desc\">Advertencia, este ajuste desactiva el rebobinado de las transmisiones. Mejora significativamente el rendimiento de las transmisiones en directo. La resolución máxima es de 1080p.</string>\n  <string name=\"live_stream_fix_4k_desc\">Advertencia, este ajuste desactiva el rebobinado de las transmisiones. Mejora significativamente el rendimiento de las transmisiones en directo. La resolución máxima es 4K.</string>\n  <string name=\"live_stream_fix\">Corrección de la transmisión en vivo (1080p)</string>\n  <string name=\"live_stream_fix_4k\">Corrección de la transmisión en vivo (4K)</string>\n  <string name=\"playback_notifications_fix_desc\">Oculta las notificaciones de cambio de pista. Podría ser útil en los firmwares basados en AOSP.</string>\n  <string name=\"playback_notifications_fix\">Desactivar notificaciones de reproducción</string>\n  <string name=\"amlogic_fix_desc\">Corrección de la caída de fotogramas en los dispositivos basados en Amlogic.</string>\n  <string name=\"amlogic_fix\">Corrección Amlogic 1080p\\@60fps</string>\n  <string name=\"tunneled_video_playback_desc\">La reproducción de vídeo en túnel promete ventajas como una mejor sincronización de audio y vídeo (AV sync) y una reproducción más fluida. Requiere Android 5+</string>\n  <string name=\"tunneled_video_playback\">Reproducción de vídeo en túnel (Android 5+)</string>\n  <string name=\"master_volume\">Volumen principal</string>\n  <string name=\"volume_limit\">Límite de volumen</string>\n  <string name=\"play_video\">Reproducir</string>\n  <string name=\"remember_position_of_short_videos\">Recuerde la posición de los vídeos cortos (menos de 10 minutos)</string>\n  <string name=\"player_show_tooltips\">Mostrar información sobre los botones</string>\n  <string name=\"action_like_unset\">Removido \\\"me gusta\\\"</string>\n  <string name=\"action_dislike_unset\">Removido \\\"no me gusta\\\".</string>\n  <string name=\"various_buttons\">Botones en la parte superior de la ventana principal</string>\n  <string name=\"not_compatible_with\">La opción seleccionada no es compatible con</string>\n  <string name=\"subtitle_position\">Desplazamiento inferior de los subtítulos</string>\n  <string name=\"pressing_home\">pulsando HOME</string>\n  <string name=\"pressing_home_back\">pulsando HOME o BACK</string>\n  <string name=\"pressing_back\">pulsando ATRÁS</string>\n  <string name=\"player_number_key_seek\">Búsqueda con teclas numéricas</string>\n  <string name=\"save_remove_playlist\">Añadir/borrar lista de la sección de listas de reproducción</string>\n  <string name=\"remove_playlist\">Eliminar permanentemente la lista de la sección de listas de reproducción</string>\n  <string name=\"removed_from_playlists\">Eliminado de la sección de listas de reproducción</string>\n  <string name=\"saved_to_playlists\">Añadido a la sección de listas de reproducción</string>\n  <string name=\"create_playlist\">Crear lista de reproducción</string>\n  <string name=\"add_video_to_new_playlist\">Añadir a una nueva lista de reproducción</string>\n  <string name=\"cant_delete_empty_playlist\">No se puede eliminar una lista de reproducción vacía</string>\n  <string name=\"playlist\">Lista de reproducción</string>\n  <string name=\"rename_playlist\">Cambiar el nombre de la lista de reproducción</string>\n  <string name=\"cant_rename_empty_playlist\">No se puede cambiar el nombre de una lista de reproducción vacía</string>\n  <string name=\"cant_rename_foreign_playlist\">No se puede cambiar el nombre de la lista de reproducción extranjera</string>\n  <string name=\"cant_save_playlist\">Esta lista de reproducción no se puede añadir</string>\n  <string name=\"enter_value\">Introduzca cualquier valor no vacío</string>\n  <string name=\"dialog_add_remove_from\">Añadir/Eliminar de %s</string>\n  <string name=\"lb_playback_controls_skip_next\">Saltar Siguiente</string>\n  <string name=\"lb_playback_controls_skip_previous\">Saltar anterior/rebobinar a la posición inicial</string>\n  <string name=\"alt_presets_behavior\">Comportamiento alternativo de los preajustes (limitar el ancho de banda)</string>\n  <string name=\"alt_presets_behavior_desc\">La aplicación tratará de mantener el ancho de banda correspondiente a la preselección seleccionada en lugar de hacer coincidir la resolución, los fps y el códec.</string>\n  <string name=\"sleep_timer\">Activar el temporizador de reposo (una hora)</string>\n  <string name=\"sleep_timer_desc\">Poner en pausa la reproducción si el usuario no ha utilizado el mando durante una hora</string>\n  <string name=\"disable_vsync\">Desactivar snap to vsync</string>\n  <string name=\"disable_vsync_desc\">Mejora un poco el rendimiento de la reproducción</string>\n  <string name=\"skip_codec_profile_check\">Omitir la comprobación del nivel del perfil del códec</string>\n  <string name=\"skip_codec_profile_check_desc\">No comprueba la compatibilidad de los códecs al iniciar un vídeo. Podría ser útil en firmwares con errores.</string>\n  <string name=\"force_sw_codec\">Forzar el decodificador de vídeo por software</string>\n  <string name=\"force_sw_codec_desc\">Puede reproducir casi cualquier vídeo pero el rendimiento es muy pobre.</string>\n  <string name=\"playlist_order\">Orden lista</string>\n  <string name=\"playlist_order_added_date_newer_first\">Fecha de añadido (más nuevos primero)</string>\n  <string name=\"playlist_order_added_date_older_first\">Fecha de añadido (más viejos primero)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Fecha de publicación (más nuevos primero)</string>\n  <string name=\"playlist_order_published_date_older_first\">Fecha de publicación (más viejos primero)</string>\n  <string name=\"playlist_order_popularity\">Popularidad</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">No puede hacerse para una lista de reproducción extranjera</string>\n  <string name=\"owned_playlist_warning\">Error. Esta acción solo se puede aplicar a listas propias</string>\n  <string name=\"content_block_alt_server\">Usar servidor alternativo</string>\n  <string name=\"content_block_alt_server_desc\">Habilite esta opción si SponsorBlock se niega a funcionar por diferentes razones.</string>\n  <string name=\"unset_stream_reminder\">Quitar recordatorio de stream</string>\n  <string name=\"set_stream_reminder\">Crear recordatorio de stream</string>\n  <string name=\"playback_starts_shortly\">La reproducción se iniciará automáticamente cuando el stream esté listo</string>\n  <string name=\"starting_stream\">Iniciando el stream…</string>\n  <string name=\"action_playlist_remove\">Eliminar de lista</string>\n  <string name=\"signin_view_title\">El código de usuario se está cargando...</string>\n  <string name=\"signin_view_description\">Para iniciar sesión introduzca este código en la página %s\\nNOTA. Sólo funciona con los navegadores Firefox o Chrome.</string>\n  <string name=\"signin_view_action_text\">Hecho</string>\n  <string name=\"require_checked\">Requiere \\\"%s</string>\n  <string name=\"msg_press_again_to_exit\">Pulse de nuevo para salir</string>\n  <string name=\"player_remaining_time\">Restante: %s</string>\n  <string name=\"player_ending_time\">Termina en %s</string>\n  <string name=\"badge_new_content\">NUEVO CONTENIDO</string>\n  <string name=\"badge_live\">EN VIVO</string>\n  <string name=\"add_device_view_description\">Para vincular el dispositivo introduzca este código en la aplicación de YouTube de su teléfono en la sección Ajustes/Mirar en la TV\\nNOTA. Sólo funciona con el teclado Gboard.</string>\n  <string name=\"playback_controls_repeat_pause\">Repetir Pausa</string>\n  <string name=\"playback_controls_repeat_list\">Repetir Lista</string>\n  <string name=\"action_subscribe_off\">Desuscribirse</string>\n  <string name=\"action_subscribe_on\">Suscribirse</string>\n  <string name=\"skip_24_rate\">Omitir los formatos de 24fps (¿Solución del modo de cine real\\?)</string>\n  <string name=\"player_disable_suggestions\">Deshabilitar sugerencias</string>\n  <string name=\"feedback\">Feedback</string>\n  <string name=\"sources\">Fuentes</string>\n  <string name=\"releases\">Publicaciones</string>\n  <string name=\"prefer_avc_over_vp9\">Selección de códecs: preferir avc sobre vp9</string>\n  <string name=\"open_chat\">Chat/Comentarios</string>\n  <string name=\"place_chat_left\">Colocar chat a la izquierda</string>\n  <string name=\"use_alt_speech_recognizer\">Reconocimiento de voz alternativo</string>\n  <string name=\"time_format\">Modo tiempo</string>\n  <string name=\"time_format_24\">24 horas</string>\n  <string name=\"time_format_12\">12 horas (AM/PM)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Tiene muchos fallos. Úsalo sólo si tienes problemas con el reconocedor por defecto.</string>\n  <string name=\"speech_recognizer\">Reconocimiento de voz</string>\n  <string name=\"speech_recognizer_system\">Sistema (preferido)</string>\n  <string name=\"speech_recognizer_external_1\">Externo 1 (con bugs serios, para usarlo hay que deshabilitar el acceso al micrófono)</string>\n  <string name=\"speech_recognizer_external_2\">Externo 2 (con bugs serios)</string>\n  <string name=\"real_channel_icon\">Mostrar icono real en el botón del canal</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Amarillo en fondo semitransparente</string>\n  <string name=\"subtitle_yellow_black\">Amarillo en fondo negro</string>\n  <string name=\"player_pixel_ratio\">Proporción de píxeles</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Gris oscuro (monocromo)</string>\n  <string name=\"disable_mic_permission\">Por favor, desactiva el acceso al micrófono de la aplicación para que este reconocedor funcione correctamente.</string>\n  <string name=\"repeat_mode_shuffle\">Mezcla cualquier lista</string>\n  <string name=\"chat_left\">Izquierda</string>\n  <string name=\"chat_right\">Derecha</string>\n  <string name=\"card_real_thumbnails\">Reemplazar miniaturas con frame del vídeo</string>\n  <string name=\"card_content\">Dónde obtener miniaturas</string>\n  <string name=\"thumb_quality_default\">Por defecto</string>\n  <string name=\"thumb_quality_start\">Inicio del vídeo</string>\n  <string name=\"thumb_quality_middle\">Mitad del vídeo</string>\n  <string name=\"thumb_quality_end\">Final del vídeo</string>\n  <string name=\"content_block_status\">Comprobar el estado de SponsorBlock</string>\n  <string name=\"player_show_quality_info_bitrate\">Añadir tasa de bits a la información de calidad</string>\n  <string name=\"player_speed_button_old_behavior\">Revertir comportamiento del bóton speed</string>\n  <string name=\"protect_settings_with_password\">Proteger configuración con contraseña</string>\n  <string name=\"enter_settings_password\">Introducir contraseña de configuración</string>\n  <string name=\"child_mode\">Modo infantil</string>\n  <string name=\"child_mode_desc\">En este modo el usuario no puede buscar o ver contenido sugerido. La configuración se hallará bajo contraseña.</string>\n  <string name=\"lost_setting_warning\">La configuración de la aplicación será cambiada. Asegúrese de que ha creado una copia de seguridad de la misma.</string>\n  <string name=\"player_button_long_click\">Un clic corto en el botón para acción rápida y uno largo para el diálogo de configuración</string>\n  <string name=\"pause_history\">Pausar historial</string>\n  <string name=\"resume_history\">Reanudar el historial</string>\n  <string name=\"clear_history\">Borrar el historial</string>\n  <string name=\"chapters\">Capítulos</string>\n  <string name=\"card_multiline_subtitle\">Subtítulos multilínea</string>\n  <string name=\"auto_frame_rate_modes\">Modos soportados</string>\n  <string name=\"loading\">Cargando…</string>\n  <string name=\"hide_streams\">Ocultar streams de Suscripciones</string>\n  <string name=\"video_rotate\">Rotar</string>\n  <string name=\"video_flip\">Voltear (espejo)</string>\n  <string name=\"trending_searches\">Búsquedas de tendencias</string>\n  <string name=\"video_duration_any\">Cualquiera</string>\n  <string name=\"video_duration\">Duración</string>\n  <string name=\"video_duration_under_4\">Menos de 4 minutos</string>\n  <string name=\"video_duration_between_4_20\">4-20 minutos</string>\n  <string name=\"video_duration_over_20\">Más de 20 minutos</string>\n  <string name=\"content_type\">Tipo</string>\n  <string name=\"content_type_any\">Cualquiera</string>\n  <string name=\"content_type_video\">Vídeo</string>\n  <string name=\"content_type_channel\">Canal</string>\n  <string name=\"content_type_playlist\">Lista de reproducción</string>\n  <string name=\"content_type_movie\">Película</string>\n  <string name=\"video_features\">Características</string>\n  <string name=\"video_feature_any\">Cualquiera</string>\n  <string name=\"video_feature_live\">En vivo</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">Ordenar por</string>\n  <string name=\"sort_by_relevance\">Relevancia</string>\n  <string name=\"sort_by_views\">Conteo de visitas</string>\n  <string name=\"sort_by_date\">Fecha de subida</string>\n  <string name=\"sort_by_rating\">Clasificación</string>\n  <string name=\"clear_search_history\">Limpiar historial de búsqueda</string>\n  <string name=\"remove_from_subscriptions\">Ocultar</string>\n  <string name=\"player_long_speed_list\">Lista de velocidad larga</string>\n  <string name=\"alt_app_icon\">Ícono de aplicación alternativa (se necesita reiniciar)</string>\n  <string name=\"network_stack\">Preferir pila de red %s</string>\n  <string name=\"cronet_desc\">Cronet es la pila de red Chromium. Cronet puede reducir la latencia y aumentar el rendimiento de la red, lo que puede ayudar con los problemas de almacenamiento en búfer.</string>\n  <string name=\"unlock_all_formats\">Desbloquear todos los formatos de video</string>\n  <string name=\"unlock_all_formats_desc\">En algunos dispositivos, el firmware informa incorrectamente que algunos formatos no son compatibles, incluso si lo son.\\n(por ejemplo, los televisores inteligentes de 1080p tienden a informar que 4k no es compatible).</string>\n  <string name=\"okhttp_desc\">Esta pila de red es la más lenta pero tiene buena estabilidad.</string>\n  <string name=\"select_channel_section\">Abrir la sección correspondiente cuando la aplicación se inicie desde ATV Channels</string>\n  <string name=\"enable_voice_search_desc\">La aplicación oficial será reemplazada por el llamado puente. Un puente ayuda a transferir solicitudes de búsqueda global a nuestra aplicación.</string>\n  <string name=\"enable_master_password\">Habilitar contraseña maestra</string>\n  <string name=\"enter_master_password\">Ingrese la contraseña maestra</string>\n  <string name=\"disable_stream_buffer\">Deshabilitar el búfer en las transmisiones</string>\n  <string name=\"disable_stream_buffer_desc\">Arreglo para situaciones en las que la transmisión está muy atrasada. Nota: la transmisión puede comenzar a retrasarse.</string>\n  <string name=\"sony_frame_drop_fix\">Corrección de caída de fotogramas #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">Soluciona los retrasos en Sony TV y algunos otros dispositivos. Nota: posibles problemas con la sincronización de audio.</string>\n  <string name=\"audio_language\">Idioma de audio</string>\n  <string name=\"old_home_look\">Aspecto antiguo de la sección Inicio</string>\n  <string name=\"hide_shorts_everywhere\">Ocultar Shorts en todas partes</string>\n  <string name=\"update_found\">Actualizar</string>\n  <string name=\"volume_boost_warning\">Cualquier ajuste por encima del límite puede dañar los altavoces</string>\n  <string name=\"play_video_incognito\">Reproducción de incógnito</string>\n  <string name=\"header_kids_home\">Niños</string>\n  <string name=\"player_screen_off_timeout\">Tiempo de espera de apagado de pantalla</string>\n  <string name=\"screensaver\">Salvapantallas</string>\n  <string name=\"header_trending\">Tendencias</string>\n  <string name=\"player_section_playlist\">Utilizar el contenido de la sección actual como lista de reproducción</string>\n  <string name=\"old_update_notifications\">Antiguo aspecto de las notificaciones de actualización</string>\n  <string name=\"mark_as_watched\">Marcar como visto</string>\n  <string name=\"player_ui_animations\">Activar animaciones del panel de control</string>\n  <string name=\"player_likes_count\">Mostrar contador de me gusta/no me gusta</string>\n  <string name=\"sorting_alphabetically2\">Alfabéticamente (rápido)</string>\n  <string name=\"sorting_default\">Por defecto (rápido)</string>\n  <string name=\"content_block_exclude_channel\">Excluir este canal de SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">Dejar de excluir este canal de SponsorBlock</string>\n  <string name=\"player_chapter_notification\">Mostrar notificación para avanzar rápidamente por los capítulos</string>\n  <string name=\"subtitle_remember\">Recordar subtítulos activados por cada canal</string>\n  <string name=\"amazon_frame_drop_fix_desc\">Diseñado para dispositivos Amazon Stick. También puede funcionar en otros dispositivos.</string>\n  <string name=\"amazon_frame_drop_fix\">Corrección de caída de fotogramas #2</string>\n  <string name=\"default_stack_desc\">Pila de red integrada. Esta pila puede tener mejor estabilidad en determinadas situaciones.</string>\n  <string name=\"item_postion\">Posición</string>\n  <string name=\"player_screen_off_dimming\">Cantidad de atenuación de la pantalla apagada</string>\n  <string name=\"screensaver_timout\">Tiempo de espera del salvapantallas</string>\n  <string name=\"screensaver_dimming\">Cantidad de atenuación del salvapantallas</string>\n  <string name=\"autogenerated\">autogenerado</string>\n  <string name=\"player_ui_on_next\">Mostrar la interfaz de usuario del reproductor al pasar al siguiente vídeo</string>\n  <string name=\"player_auto_volume\">Ajuste automático del volumen</string>\n  <string name=\"header_shorts\">Shorts</string>\n  <string name=\"msg_player_error_video_renderer\">El formato de VIDEO seleccionado no es compatible.\\nIntente seleccionar otro pulsando el botón HQ en el reproductor.\\nSi esto no ayuda intente reiniciar el dispositivo.</string>\n  <string name=\"msg_player_error_audio_renderer\">El formato de AUDIO seleccionado no es compatible.\\nIntente seleccionar otro pulsando el botón HQ en el reproductor.\\nSi esto no ayuda intente reiniciar el dispositivo.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">El formato de SUBTÍTULOS seleccionado no es compatible.\\nIntente seleccionar otro pulsando el botón CC en el reproductor.\\nSi esto no ayuda intente reiniciar el dispositivo.</string>\n  <string name=\"msg_player_error_video_source\">La URL del VIDEO no funciona o la hora del dispositivo es incorrecta.\\nPor lo general, es un problema de la aplicación.</string>\n  <string name=\"msg_player_error_audio_source\">La URL del AUDIO no funciona o la hora del dispositivo es incorrecta.\\nPor lo general, es un problema de la aplicación.</string>\n  <string name=\"msg_player_error_subtitle_source\">La URL de los SUBTÍTULOS no funcionan o la hora del dispositivo es incorrecta.\\nPor lo general, es un problema de la aplicación.</string>\n  <string name=\"open_comments\">Abrir comentarios</string>\n  <string name=\"disable_history\">Desactivar historial</string>\n  <string name=\"enable_history\">Activar historial</string>\n  <string name=\"player_network_stack\">Pila de red</string>\n  <string name=\"dialog_notification\">Notificación de la actualización mediante un cuadro de diálogo emergente</string>\n  <string name=\"auto_history\">Auto (usar configuración de cuenta)</string>\n  <string name=\"remember_position_subscriptions\">Recordar la última posición vista en Suscripciones</string>\n  <string name=\"msg_player_unknown_error\">Error desconocido</string>\n  <string name=\"player_global_focus\">Navegación fluida entre las filas de botones del reproductor</string>\n  <string name=\"player_volume\">Volumen</string>\n  <string name=\"header_notifications\">Notificaciones</string>\n  <string name=\"disable_search_history\">Desactivar historial de búsqueda</string>\n  <string name=\"unlock_high_bitrate_formats\">Desbloquear formatos vp9 1080p de alta tasa de bits</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Desbloquear formatos mp4a de alta tasa de bits</string>\n  <string name=\"color_scheme_blue\">Azul</string>\n  <string name=\"color_scheme_dark_blue\">Azul oscuro</string>\n  <string name=\"player_speed_per_channel\">Por cada canal</string>\n  <string name=\"disable_popular_searches\">Desactivar las búsquedas populares</string>\n  <string name=\"multi_profiles\">Usar distintos ajustes para cada cuenta</string>\n  <string name=\"protect_account_with_password\">Proteger este cuenta con contraseña</string>\n  <string name=\"enter_account_password\">Introduzca la contraseña de la cuenta</string>\n  <string name=\"show_connect_messages\">Mostrar mensajes de conexión</string>\n  <string name=\"prefer_avc_over_vp9_desc\">AVISO: máximo 1080p</string>\n  <string name=\"play_next\">Reproducir a continuación</string>\n  <string name=\"hide_watched_from_subscriptions\">Ocultar vídeos vistos de Suscripciones</string>\n  <string name=\"hide_unwanted_content\">Ocultar contenido no deseado</string>\n  <string name=\"hide_watched_from_home\">Ocultar vídeos vistos de Inicio</string>\n  <string name=\"remote_control_permission\">El control remoto requiere permiso de superposición</string>\n  <string name=\"login_from_browser\">Iniciar sesión desde el navegador</string>\n  <string name=\"disable_remote_history\">Desactivar el historial al utilizar el control remoto</string>\n  <string name=\"keyboard_fix\">Corregir la visualización del teclado pulsando la tecla OK (G20s y otros)</string>\n  <string name=\"nothing_found\">No se ha encontrado nada</string>\n  <string name=\"channel_filter_hint\">Filtrar canales</string>\n  <string name=\"auto_frame_rate_desc\">Esta opción elimina las fluctuaciones en las escenas en las que la cámara se mueve con rapidez, como las retransmisiones deportivas.</string>\n  <string name=\"player_loop_shorts\">Reproducir los Shorts en bucle</string>\n  <string name=\"player_quick_shorts_skip\">Saltar los Shorts con los botones izquierda/derecha</string>\n  <string name=\"channel_search_bar\">Barra de búsqueda dentro de la página Canal</string>\n  <string name=\"channels_filter\">Filtrar la lista de canales dentro de la sección Canales</string>\n  <string name=\"share_qr_link\">Compartir enlace (código QR)</string>\n  <string name=\"app_corner_clock\">Inicio: reloj de la esquina superior derecha</string>\n  <string name=\"player_corner_clock\">Reproductor: reloj de la esquina superior derecha</string>\n  <string name=\"player_corner_ending_time\">Reproductor: reloj de la esquina superior derecha durante el tiempo final</string>\n  <string name=\"pin_playlist\">Añadir lista de reproducción a la barra lateral</string>\n  <string name=\"pin_channel\">Añadir canal a la barra lateral</string>\n  <string name=\"news_row_name\">Noticias</string>\n  <string name=\"hide_shorts_channel\">Ocultar Shorts del Canal</string>\n  <string name=\"hide_upcoming_channel\">Ocultar próximos del Canal</string>\n  <string name=\"hide_upcoming_home\">Ocultar próximos del Inicio</string>\n  <string name=\"hide_shorts_from_trending\">Ocultar Shorts de Tendencias</string>\n  <string name=\"remember_position_of_live_videos\">Recordar posición en el directo</string>\n  <string name=\"save_playlist\">Añadir lista de reproducción a la sección listas de reproducción</string>\n  <string name=\"header_sports\">Deportes</string>\n  <string name=\"enable\">Habilitar</string>\n  <string name=\"more_info\">Más información</string>\n  <string name=\"about_sponsorblock\">Sobre SponsorBlock</string>\n  <string name=\"about_dearrow\">Sobre DeArrow</string>\n  <string name=\"replace_thumbnails\">Reemplazar miniaturas</string>\n  <string name=\"crowdsourced_thumbnails\">Miniaturas de Crowdsourcing</string>\n  <string name=\"crowdsoursed_titles\">Títulos de Crowdsourcing</string>\n  <string name=\"dearrow_not_submitted_thumbs\">De dónde obtener las miniaturas no enviadas</string>\n  <string name=\"replace_titles\">Reemplazar títulos</string>\n  <string name=\"keep_finished_activities\">Guardar las actividades terminadas</string>\n  <string name=\"disable_channels_service\">Desactivar el servicio Canales</string>\n  <string name=\"pitch_effect\">Efecto de tono</string>\n  <string name=\"fullscreen_mode\">Modo pantalla completa (sin barras del sistema)</string>\n  <string name=\"player_exit_shortcut\">Salir del reproductor</string>\n  <string name=\"action_sound_off\">Apagar sonido</string>\n  <string name=\"dearrow_status\">Comprobar el estado de DeArrow </string>\n  <string name=\"old_channel_look\">Aspecto antiguo de la página Canal</string>\n  <string name=\"player_only_mode\">Mostrar sólo el reproductor si el vídeo se abre fuera de la aplicación</string>\n  <string name=\"pinned_channel_rows\">Mostrar el canal fijado como filas</string>\n  <string name=\"prefer_ipv4\">Preferir DNS IPv4</string>\n  <string name=\"prefer_ipv4_desc\">Podría solucionar situaciones en las que la aplicación no funciona en absoluto.\\nNota. Puede causar cuelgues y bloqueos (especialmente en dispositivos Android 8 o Dune HD).</string>\n  <string name=\"hide_watched_from_watch_later\">Ocultar videos vistos de la lista de reproducción Ver más tarde</string>\n  <string name=\"hide_watched_from_notifications\">Ocultar vídeos vistos de Notificaciones</string>\n  <string name=\"player_extra_long_speed_list\">Lista de velocidad extra larga</string>\n  <string name=\"remember_position_pinned\">Recordar la última posición vista en las listas de reproducción fijadas</string>\n  <string name=\"long_press_for_settings\">PULSACIÓN LARGA PARA AJUSTES</string>\n  <string name=\"device_specific_backup\">Copia de seguridad sólo para este dispositivo</string>\n  <string name=\"speech_engine\">Motor de búsqueda por voz</string>\n  <string name=\"long_press_for_options\">PULSACIÓN LARGA PARA OPCIONES</string>\n  <string name=\"local_backup\">Copia de seguridad local</string>\n  <string name=\"auto_backup\">Copia de seguridad automática (una vez al día)</string>\n  <string name=\"repeat_mode_reverse_list\">Reproducir la lista de reproducción o los vídeos del canal en orden inverso</string>\n  <string name=\"unknown_source_error\">Error de fuente desconocida</string>\n  <string name=\"unknown_renderer_error\">Error de renderizado desconocido</string>\n  <string name=\"skip_shorts\">Saltar Shorts</string>\n  <string name=\"player_quick_skip_videos\">Saltar los vídeos normales con los botones izquierda/derecha</string>\n  <string name=\"calm_msg\">Estamos solucionando el problema. Compruebe las actualizaciones de vez en cuando</string>\n  <string name=\"without_picture\">Sin imagen</string>\n  <string name=\"video_disabled\">Vídeo desactivado</string>\n  <string name=\"applying_fix\">No cierres el reproductor. Aplicando la solución...</string>\n  <string name=\"hide_mixes\">Ocultar Mixes</string>\n  <string name=\"disable_network_error_fixing_desc\">Probablemente necesites activar esta opción si utilizas una VPN</string>\n  <string name=\"player_global_focus_desc\">Esta función afecta a qué botón del reproductor recibirá atención al navegar entre las filas de botones del reproductor</string>\n  <string name=\"disable_network_error_fixing\">Desactivar la corrección automática de errores de red</string>\n  <string name=\"recommended\">Recomendados</string>\n  <string name=\"new_subscriptions_group\">Nuevo grupo</string>\n  <string name=\"add_to_subscriptions_group\">Añadir/quitar canal del grupo de suscripción</string>\n  <string name=\"rename_group\">Renombrar el grupo de suscripción</string>\n  <string name=\"screen_dimming_amount\">Cantidad de atenuación de la pantalla</string>\n  <string name=\"screen_dimming_timeout\">Tiempo de atenuación de la pantalla</string>\n  <string name=\"place_comments_left\">Colocar comentarios a la izquierda</string>\n  <string name=\"video_buffer_size_lowest\">Más bajo</string>\n  <string name=\"video_buffer_size_highest\">Más alto</string>\n  <string name=\"unpin_group_from_sidebar\">Eliminar el grupo de suscripción</string>\n  <string name=\"playlists_rows\">Mostrar la sección de listas de reproducción como filas</string>\n  <string name=\"import_subscriptions_group\">Importar</string>\n  <string name=\"hide_shorts_from_search\">Ocultar Shorts del resultado de búsqueda</string>\n  <string name=\"not_supported_by_device\">El dispositivo no es compatible con esta función</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">Desactivar subtítulos</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">Activar subtítulos</string>\n  <string name=\"my_videos\">Mis vídeos</string>\n  <string name=\"player_audio_focus\">Enfoque de audio (se pausa si detecta otros reproductores)</string>\n  <string name=\"paid_content_notification\">Notificación de contenido de pago</string>\n  <string name=\"you_liked\">te gustó</string>\n  <string name=\"premium_users_only\">Sólo usuarios Premium. Corrección para la lista incompleta de formatos de vídeo</string>\n  <string name=\"playback_buffering_fix\">Corrección del buffer de reproducción</string>\n  <string name=\"oculus_quest_fix\">Correción del Oculus Quest</string>\n  <string name=\"card_preview_muted\">Vídeo sin sonido</string>\n  <string name=\"card_preview_full\">Vídeo con sonido</string>\n  <string name=\"card_preview\">Vista previa de la tarjeta</string>\n  <string name=\"card_unlocalized_titles\">Títulos de vídeo sin traducir</string>\n  <string name=\"context_menu_sorting\">Ordenar el menú contextual</string>\n  <string name=\"search_exit_shortcut\">Salir del buscador</string>\n  <string name=\"original_lang\">Original</string>\n  <string name=\"suggestions\">Sugerencias</string>\n  <string name=\"remove_playlist_fmt\">Eliminar %s permanentemente </string>\n  <string name=\"player_chapter_notification2\">Avanzar al siguiente capítulo presionando en la notificación</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">No redimensione el video para ajustar al cuadro de diálogo</string>\n  <string name=\"typing_corrections\">Desactivar auto corrección mientras ingresa texto</string>\n  <string name=\"suggestions_horizontally_scrolled\">Desplazamiento horizontal de Sugerencias </string>\n  <string name=\"dialog_block_channel\">Bloquear canal</string>\n  <string name=\"dialog_unblock_channel\">Desbloquear canal</string>\n  <string name=\"confirm_block_channel\">¿Ocultar todo el contenido de %s?</string>\n  <string name=\"channel_blocked\">Canal bloqueado</string>\n  <string name=\"channel_unblocked\">Canal desbloqueado</string>\n  <string name=\"header_blocked_channels\">Canales bloqueados</string>\n  <string name=\"msg_no_blocked_channels\">No hay canales bloqueados</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-et/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"title_search\">Otsing</string>\n  <string name=\"header_home\">Avaleht</string>\n  <string name=\"header_subscriptions\">Tellimused</string>\n  <string name=\"header_history\">Ajalugu</string>\n  <string name=\"header_music\">Muusika</string>\n  <string name=\"header_news\">Uudised</string>\n  <string name=\"header_gaming\">Mängud</string>\n  <string name=\"header_playlists\">Esitusloendid</string>\n  <string name=\"header_settings\">Seaded</string>\n  <string name=\"subscriptions_signin_title\">Vaata videoid tellitud kanalitelt</string>\n  <string name=\"subscriptions_signin_subtitle\">Alustamiseks sisene oma kontole</string>\n  <string name=\"subscriptions_signin_button_text\">SISENE</string>\n  <string name=\"library_signin_to_show_more\">Vaata meeldinud videoid</string>\n  <string name=\"library_signin_subtitle\">Oma teekide nägemiseks logi sisse</string>\n  <string name=\"action_signin\">SISENE</string>\n  <string name=\"title_video_formats\">Videovormingud</string>\n  <string name=\"title_audio_formats\">Helivormingud</string>\n  <string name=\"subtitle_category_title\">Subtiitrid</string>\n  <string name=\"video_max_quality\">Automaatne (parim kvaliteet)</string>\n  <string name=\"audio_max_quality\">Automaatne (parim kvaliteet)</string>\n  <string name=\"subtitles_disabled\">Väljas</string>\n  <string name=\"auto_frame_rate\">Automaatne kaadrisagedus</string>\n  <string name=\"frame_rate_correction\">Paranda kaadrisagedust:\\n%s</string>\n  <string name=\"category_background_playback\">Taustesituse eelistus</string>\n  <string name=\"not_implemented\">Ei ole toetatud</string>\n  <string name=\"option_background_playback_off\">Ära kasuta</string>\n  <string name=\"option_background_playback_pip\">Pilt-pildis</string>\n  <string name=\"option_background_playback_only_audio\">Ainult heli</string>\n  <string name=\"playback_settings\">Kvaliteet</string>\n  <string name=\"video_speed\">Video kiirus</string>\n  <string name=\"resolution_switch\">Vaheta resolutsiooni</string>\n  <string name=\"video_buffer\">Video vahemälu</string>\n  <string name=\"video_buffer_size_low\">Väike</string>\n  <string name=\"video_buffer_size_med\">Keskmine</string>\n  <string name=\"video_buffer_size_high\">Suur</string>\n  <string name=\"update_changelog\">Äpi muudatused</string>\n  <string name=\"install_update\">Paigalda uuendus</string>\n  <string name=\"section_is_empty\">Ups! Siin ei ole midagi</string>\n  <string name=\"dialog_add_to_playlist\">Lisa esitusloendisse</string>\n  <string name=\"msg_signed_users_only\">Vajalik kontole sisselogimine</string>\n  <string name=\"msg_cant_load_content\">Sisu laadimine ei õnnestunud!\\n1) Kontrolli seadme kuupäeva ja kellaaega\\n2) Kontrolli võrguühendust\\n3) Kontrolli puhverserveri sätteid\\n4) Proovi kasutajasse uuesti sisse logida\\n5) Võibolla on äpp vigane</string>\n  <string name=\"title_video_presets\">Video profiil</string>\n  <string name=\"settings_accounts\">Kontod</string>\n  <string name=\"settings_left_panel\">Redigeeri kategooriaid</string>\n  <string name=\"settings_themes\">Teemad</string>\n  <string name=\"settings_other\">Muu</string>\n  <string name=\"settings_player\">Pleier</string>\n  <string name=\"settings_language\">Keel</string>\n  <string name=\"settings_linked_devices\">Ühendatud seadmed</string>\n  <string name=\"settings_about\">Äpist</string>\n  <string name=\"dialog_account_list\">Vali konto</string>\n  <string name=\"dialog_account_none\">Külaline</string>\n  <string name=\"dialog_remove_account\">Logi välja</string>\n  <string name=\"dialog_add_account\">Logi sisse</string>\n  <string name=\"default_lang\">Vaikimisi</string>\n  <string name=\"dialog_select_language\">Keel</string>\n  <string name=\"header_channels\">Kanalid</string>\n  <string name=\"subtitle_default\">Tavalised</string>\n  <string name=\"subtitle_white_semi_transparent\">Poolläbipaistev taust</string>\n  <string name=\"subtitle_style\">Subtiitrite stiil</string>\n  <string name=\"subtitle_language\">Subtiitrite keel</string>\n  <string name=\"action_search\">Otsi</string>\n  <string name=\"settings_main_ui\">Kasutajaliides</string>\n  <string name=\"dialog_main_ui\">Kasutajaliides</string>\n  <string name=\"card_animated_previews\">Animeeritud eelvaade</string>\n  <string name=\"web_site\">Veebileht</string>\n  <string name=\"donation\">Toeta projekti</string>\n  <string name=\"dialog_about\">Äpist</string>\n  <string name=\"dialog_player_ui\">Pleier</string>\n  <string name=\"player_show_ui_on_pause\">Kuva kasutajaliides peatatud taasesituse ajal</string>\n  <string name=\"player_pause_on_ok\">Klahv OK peatab taasesituse</string>\n  <string name=\"player_ok_button_behavior\">Klahvi OK käitumisviis</string>\n  <string name=\"player_only_ui\">Kuva kasutajaliides</string>\n  <string name=\"player_ui_and_pause\">Kuva kasutajaliides ja peata taasesitus</string>\n  <string name=\"player_only_pause\">Peata taasesitus</string>\n  <string name=\"check_for_updates\">Kontrolli uuendusi</string>\n  <string name=\"update_not_found\">Teie äpp ei vaja uuendamist</string>\n  <string name=\"update_in_progress\">Oodake…</string>\n  <string name=\"player_ui_hide_behavior\">Kasutajaliidese automaatse peitmise aeg</string>\n  <string name=\"option_never\">Ära peida</string>\n  <string name=\"side_panel_sections\">Kuvatavad jaotised</string>\n  <string name=\"boot_to_section\">Käivitamisel kuvatav jaotis</string>\n  <string name=\"large_ui\">Suurendatud kasutajaliides</string>\n  <string name=\"video_grid_scale\">Video ruudustiku skaala</string>\n  <string name=\"scale_ui\">Kasutajaliidese skaala</string>\n  <string name=\"color_scheme\">Värviskeem</string>\n  <string name=\"color_scheme_default\">Vaikimisi</string>\n  <string name=\"color_scheme_red_grey\">Punahall</string>\n  <string name=\"color_scheme_red\">Punane</string>\n  <string name=\"color_scheme_dark_grey\">Tumehall</string>\n  <string name=\"disable_update_check\">Lülita uuenduste otsimine välja</string>\n  <string name=\"show_again\">Näita uuesti</string>\n  <string name=\"check_updates_auto\">Teavita uuendustest</string>\n  <string name=\"select_account_on_boot\">Näita käivitamisel kontovalikut</string>\n  <string name=\"player_other\">Muu</string>\n  <string name=\"player_full_date\">Täpne kuupäev kirjelduses</string>\n  <string name=\"open_channel\">Ava kanal</string>\n  <string name=\"not_interested\">Ära soovita videot</string>\n  <string name=\"you_wont_see_this_video\">Seda videot ei soovitata enam</string>\n  <string name=\"settings_search\">Otsing</string>\n  <string name=\"dialog_search\">Otsing</string>\n  <string name=\"instant_voice_search\">Häälotsing</string>\n  <string name=\"option_background_playback_behind\">Taustesitus</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">Järgmise video andmed ei ole veel saadaval</string>\n  <string name=\"card_multiline_title\">Mitmerealised pealkirjad</string>\n  <string name=\"cards_style\">Eelvaate eelistused</string>\n  <string name=\"repeat_mode_all\">Mängi videoid järjest</string>\n  <string name=\"repeat_mode_one\">Korda videot</string>\n  <string name=\"repeat_mode_pause\">Peata taasesitus iga video järel</string>\n  <string name=\"repeat_mode_pause_alt\">Peata taasesitus iga video järel (väljaarvatud esitusloendid)</string>\n  <string name=\"repeat_mode_none\">Peata taasesitus pärast ühte videot</string>\n  <string name=\"subscribed_to_channel\">Tellitud</string>\n  <string name=\"unsubscribed_from_channel\">Pole tellitud</string>\n  <string name=\"subtitle_yellow_transparent\">Kollane läbipaistval taustal</string>\n  <string name=\"subtitle_white_black\">Valge mustal taustal</string>\n  <string name=\"player_seek_preview\">Eelvaade kerimisel</string>\n  <string name=\"color_scheme_dark_grey_oled\">Tumehall (OLED)</string>\n  <string name=\"channels_section_sorting\">Kanalite jaotise sorteerimine</string>\n  <string name=\"sorting_by_new_content\">Uudsuse järgi</string>\n  <string name=\"sorting_alphabetically\">Tähestiku järgi</string>\n  <string name=\"sorting_last_viewed\">Viimati vaatamise järgi</string>\n  <string name=\"player_pause_when_seek\">Peata taasesitus kerimisel</string>\n  <string name=\"playlists_style\">Esitusloendi stiil</string>\n  <string name=\"playlists_style_grid\">Ruudustik</string>\n  <string name=\"playlists_style_rows\">Read</string>\n  <string name=\"player_show_clock\">Kuva juhtribal kell</string>\n  <string name=\"player_show_remaining_time\">Kuva juhtribal järelejäänud aega</string>\n  <string name=\"open_channel_uploads\">Ava kanali videod</string>\n  <string name=\"app_exit_shortcut\">Äpist väljumise otsetee</string>\n  <string name=\"app_exit_none\">Ära kasuta</string>\n  <string name=\"app_double_back_exit\">Topelt tagasivajutus</string>\n  <string name=\"app_single_back_exit\">Ühekordne tagasivajutus</string>\n  <string name=\"action_video_zoom\">Video suurendus</string>\n  <string name=\"video_zoom_default\">Ära suurenda</string>\n  <string name=\"video_zoom_fit_width\">Sobita laius</string>\n  <string name=\"video_zoom_fit_height\">Sobita kõrgus</string>\n  <string name=\"video_zoom_fit_both\">Sobita laius/kõrgus</string>\n  <string name=\"video_zoom_stretch\">Venita</string>\n  <string name=\"video_zoom\">Video suurendus</string>\n  <string name=\"color_scheme_teal\">Türkiissinine</string>\n  <string name=\"color_scheme_teal_oled\">Türkiissinine (OLED)</string>\n  <string name=\"player_seek_preview_none\">Väljas</string>\n  <string name=\"player_seek_preview_single\">Üks kaader</string>\n  <string name=\"player_seek_preview_carousel\">Karussell</string>\n  <string name=\"player_seek_preview_carousel_slow\">Karussell (aeglane)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Karussell (kiire)</string>\n  <string name=\"unsubscribe_from_channel\">Tühista kanali tellimus</string>\n  <string name=\"card_title_lines_num\">Ridade arv eelvaatel</string>\n  <string name=\"card_auto_scrolled_title\">Pealkirja automaatne kerimine</string>\n  <string name=\"share_link\">Jaga linki</string>\n  <string name=\"subscribe_to_channel\">Telli kanal</string>\n  <string name=\"wait_data_loading\">Andmeid laetakse…</string>\n  <string name=\"auto_frame_rate_pause\">Automaatse kaadrisageduse vahetuse peatamise aeg</string>\n  <string name=\"auto_frame_rate_applying\">Kasutatakse automaatset kaadrisagedust %sx%s\\@%s</string>\n  <string name=\"audio_shift\">Helinihe</string>\n  <string name=\"player_remember_speed\">Kiiruse mäletamine</string>\n  <string name=\"mark_channel_as_watched\">Märgi vaadatuks</string>\n  <string name=\"channel_marked_as_watched\">Kanal on märgitud vaadatuks</string>\n  <string name=\"dialog_add_device\">Lisa uus seade</string>\n  <string name=\"dialog_remove_all_devices\">Eemaldada kõik seadmed</string>\n  <string name=\"device_link_enabled\">Seadmete ühendamine lubatud</string>\n  <string name=\"device_connected\">Seade \\\"%s\\\" ühendatud</string>\n  <string name=\"device_disconnected\">Seade \\\"%s\\\" lahti ühendatud</string>\n  <string name=\"settings_ui_scale\">Kasutajaliidese suurus</string>\n  <string name=\"msg_restart_app\">Sätete rakendamiseks taaskäivita rakendus</string>\n  <string name=\"settings_block\">Sisu blokeerimine</string>\n  <string name=\"msg_applying\">Kasutatakse %s</string>\n  <string name=\"msg_done\">Valmis</string>\n  <string name=\"settings_general\">Üldine</string>\n  <string name=\"settings_video\">Video</string>\n  <string name=\"content_block_confirm_skip\">Vahelejätmise kinnitus</string>\n  <string name=\"content_block_categories\">Vahelejätmise kategooriad</string>\n  <string name=\"content_block_sponsor\">Sponsor</string>\n  <string name=\"content_block_intro\">Sissejuhatus</string>\n  <string name=\"content_block_outro\">Lõputiitrid</string>\n  <string name=\"content_block_interaction\">Tellimise meeldetuletus</string>\n  <string name=\"content_block_self_promo\">Enesereklaam</string>\n  <string name=\"content_block_music_off_topic\">Mittemusikaalne osa</string>\n  <string name=\"confirm_segment_skip\">Jäta %s vahele\\?</string>\n  <string name=\"msg_skipping_segment\">%s vahele jätmine…</string>\n  <string name=\"content_block_notification_type\">Teavituse tüüp</string>\n  <string name=\"content_block_notify_none\">Ilma teavituseta</string>\n  <string name=\"content_block_notify_toast\">Ujuvad teavitused</string>\n  <string name=\"content_block_notify_dialog\">Kinnitusdialoog</string>\n  <string name=\"return_to_launcher\">Saabudes vidina kaudu naase Android TV-sse</string>\n  <string name=\"intent_force_close\">Välisest allikast video avamisel välju automaatselt</string>\n  <string name=\"btn_confirm\">Kinnita</string>\n  <string name=\"player_low_video_quality\">Madal video kvaliteet</string>\n  <string name=\"remote_session_closed\">Kaugühendus on katkestatud</string>\n  <string name=\"msg_mode_switch_error\">Kuvarežiimi \\\"%s\\\" rakendamine ei õnnestunud</string>\n  <string name=\"msg_player_error\">Viga %s. Uuesti laadimine…</string>\n  <string name=\"video_preset_disabled\">Eelistus puudub</string>\n  <string name=\"video_preset_enabled\">Järgmise video vorming määratakse vastavalt profiilile</string>\n  <string name=\"player_sleep_timer\">Unetaimer</string>\n  <string name=\"player_show_quality_info\">Kuva juhtribal video kvaliteedi andmed</string>\n  <string name=\"player_remember_each_speed\">Video kiiruse meeldejätmine</string>\n  <string name=\"player_remember_speed_none\">Ära jäta meelde</string>\n  <string name=\"player_remember_speed_all\">Jäta meelde</string>\n  <string name=\"player_remember_speed_each\">Jäta meelde iga video kohta</string>\n  <string name=\"msg_player_error_source\">Video allalaadimine ebaõnnestus</string>\n  <string name=\"msg_player_error_renderer\">Valitud vormingut ei toetata.\\nProovi mõnda muud vormingut.\\nVajadusel taaskäivita seade.</string>\n  <string name=\"msg_player_error_unexpected\">Tundmatu videodekooder</string>\n  <string name=\"video_aspect\">Kuvasuhe</string>\n  <string name=\"player_tweaks\">Arendaja valikud</string>\n  <string name=\"header_uploads\">Üleslaadimised</string>\n  <string name=\"player_show_global_clock\">Näita alati kella</string>\n  <string name=\"uploads_old_look\">Vana üleslaadimiste jaotise välimus</string>\n  <string name=\"background_playback_activation\">Taustesitus (aktiveerimine)</string>\n  <string name=\"channels_old_look\">Vana kanalite jaotise välimus</string>\n  <string name=\"channels_auto_load\">Lae kanalite jaotise sisu automaatselt</string>\n  <string name=\"return_to_background_video\">Naase taustvideo juurde</string>\n  <string name=\"pinned_to_sidebar\">Kinnitatud külgribale</string>\n  <string name=\"unpinned_from_sidebar\">Eemaldatud külgribalt</string>\n  <string name=\"dialog_select_country\">Riik</string>\n  <string name=\"settings_language_country\">Keel/Riik</string>\n  <string name=\"playback_queue_category_title\">Taasesituse järjekord</string>\n  <string name=\"video_buffer_size_none\">Väljas</string>\n  <string name=\"open_playlist\">Ava esitusloend</string>\n  <string name=\"not_recommend_channel\">Ära soovita kanalit</string>\n  <string name=\"you_wont_see_this_channel\">Seda kanalit ei soovitata enam</string>\n  <string name=\"subtitle_white_transparent\">Valge läbipaistval taustal</string>\n  <string name=\"player_show_ending_time\">Kuva juhtribal lõppemise aega</string>\n  <string name=\"share_qr_link\">Jaga linki (QR kood)</string>\n  <string name=\"cancel_segment_skip\">Tühista lõigu vahelejätmine</string>\n  <string name=\"msg_player_error_video_renderer\">Valitud videovormingut ei toetata.\\nProovi valida mõni muu videovorming, vajutades juhtribal asuvat HQ nuppu.\\nVajadusel taaskäivita seade.</string>\n  <string name=\"msg_player_error_audio_renderer\">Valitud helivormingut ei toetata.\\nProovi valida mõni muu helivorming, vajutades juhtribal asuvat HQ nuppu.\\nVajadusel taaskäivita seade.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">Valitud subtiitrivormingut ei toetata.\\nProovi valida mõni muu subtiitrivorming, hoides all juhtribal asuvat CC nuppu.\\nVajadusel taaskäivita seade.</string>\n  <string name=\"player_show_global_ending_time\">Näita alati video lõppemise aega</string>\n  <string name=\"app_corner_clock\">Kell avalehe paremal ülanurgas</string>\n  <string name=\"player_corner_clock\">Kell pleieri paremal ülanurgas</string>\n  <string name=\"player_corner_ending_time\">Video lõppemise aeg pleieri paremal ülanurgas</string>\n  <string name=\"pin_unpin_from_sidebar\">Lisa/Eemalda külgribalt</string>\n  <string name=\"pin_unpin_playlist\">Lisa/Eemalda esitusloend külgribalt</string>\n  <string name=\"pin_unpin_channel\">Lisa/Eemalda kanal külgribalt</string>\n  <string name=\"unpin_from_sidebar\">Eemalda külgribalt</string>\n  <string name=\"share_embed_link\">Jaga manuse linki</string>\n  <string name=\"card_text_scroll_factor\">Eelvaate teksti kerimise kiirus</string>\n  <string name=\"preferred_update_source\">Vali värskendusallikas</string>\n  <string name=\"hide_shorts\">Peida lühivideod tellimustest</string>\n  <string name=\"hide_shorts_channel\">Peida lühivideod kanalilt</string>\n  <string name=\"key_remapping\">Nuppude funktsioonide eelistused</string>\n  <string name=\"screen_dimming\">Ekraani hämardamine</string>\n  <string name=\"removed_from_playback_queue\">Järjekorrast eemaldatud</string>\n  <string name=\"added_to_playback_queue\">Lisatud järjekorda</string>\n  <string name=\"add_remove_from_playback_queue\">Lisa/Eemalda järjekorrast</string>\n  <string name=\"add_to_playback_queue\">Lisa järjekorda</string>\n  <string name=\"remove_from_playback_queue\">Eemalda järjekorrast</string>\n  <string name=\"proxy_enabled\">Puhverserver sees</string>\n  <string name=\"proxy_disabled\">Puhverserver väljas</string>\n  <string name=\"uploads_row_name\">Üleslaadimised</string>\n  <string name=\"playlists_row_name\">Loodud esitusloendid</string>\n  <string name=\"popular_uploads_row_name\">Populaarsed üleslaadimised</string>\n  <string name=\"news_row_name\">Uudised</string>\n  <string name=\"breaking_news_row_name\">Erakorralised uudised</string>\n  <string name=\"covid_news_row_name\">COVID-19 uudised</string>\n  <string name=\"enable_voice_search\">Luba globaalne häälotsing</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Telli/Tühista kanali tellimus</string>\n  <string name=\"app_backup_restore\">Varundamine/Taastamine</string>\n  <string name=\"app_backup\">Varunda andmed</string>\n  <string name=\"app_restore\">Taasta andmed varukoopiast</string>\n  <string name=\"skip_each_segment_once\">Jäta iga segment vahele ainult ühe korra</string>\n  <string name=\"disable_ok_long_press\">Keela OK nupu pikalt vajutamine</string>\n  <string name=\"disable_ok_long_press_desc\">Mõeldud pultidele, kus OK nupp ei tööta korralikult</string>\n  <string name=\"player_time_correction\">Kuva aeg kiiruse suhtes</string>\n  <string name=\"sponsor_color_markers\">Värvimarkerid edenemisribal</string>\n  <string name=\"refresh_section\">Värskenda jaotist</string>\n  <string name=\"option_disabled\">Keelatud</string>\n  <string name=\"live_now_row_name\">Reaalajas</string>\n  <string name=\"run_in_background\">Taustesitus</string>\n  <string name=\"settings_remote_control\">Kaugjuhtimine</string>\n  <string name=\"background_service_started\">Taustateenus alustatud</string>\n  <string name=\"dialog_add_to\">Lisa %s-sse</string>\n  <string name=\"dialog_remove_from\">Eemalda %s-st</string>\n  <string name=\"added_to\">Lisatud %s-sse</string>\n  <string name=\"removed_from\">Eemaldatud %s-st</string>\n  <string name=\"video_preset_adaptive\">Kohanduv</string>\n  <string name=\"content_block_preview_recap\">Video eelvaade</string>\n  <string name=\"content_block_highlight\">Huvitav koht</string>\n  <string name=\"removed_from_history\">Video on ajaloost eemaldatud</string>\n  <string name=\"remove_from_history\">Eemalda ajaloost</string>\n  <string name=\"upload_date\">Üleslaadimise aeg</string>\n  <string name=\"upload_date_any\">Eelistus puudub</string>\n  <string name=\"upload_date_today\">Täna</string>\n  <string name=\"upload_date_this_week\">See nädal</string>\n  <string name=\"upload_date_this_month\">See kuu</string>\n  <string name=\"upload_date_this_year\">See aasta</string>\n  <string name=\"double_refresh_rate\">Topelt värskendussagedus</string>\n  <string name=\"upload_date_last_hour\">Viimane tund</string>\n  <string name=\"focus_on_search_results\">Fokusseeri automaatselt otsingutulemustele</string>\n  <string name=\"context_menu\">Kontekstimenüüs kuvatavad toimingud</string>\n  <string name=\"add_remove_from_recent_playlist\">Lisa/Eemalda hiljutisest esitusloendist</string>\n  <string name=\"hide_settings_section\">Peida sätete jaotis (ohtlik!)</string>\n  <string name=\"volume\">Helitugevus %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s sek</string>\n  <string name=\"audio_shift_sec\">%s sek</string>\n  <string name=\"ui_hide_timeout_sec\">%s sek</string>\n  <string name=\"screen_dimming_timeout_min\">%s min</string>\n  <string name=\"mark_all_channels_watched\">Märgi kõik kanalid vaadatuks</string>\n  <string name=\"move_section_up\">Liiguta jaotis üles</string>\n  <string name=\"move_section_down\">Liiguta jaotis alla</string>\n  <string name=\"player_buttons\">Kuvatavad pleierinupud</string>\n  <string name=\"action_playlist_add\">Lisa esitusloendisse</string>\n  <string name=\"action_video_stats\">Video statistika</string>\n  <string name=\"action_subscribe\">Telli</string>\n  <string name=\"action_channel\">Ava kanal</string>\n  <string name=\"action_pip\">Pilt-pildis</string>\n  <string name=\"action_video_info\">Ava kirjeldus</string>\n  <string name=\"action_screen_off\">Ekraani väljalülitamine</string>\n  <string name=\"action_playback_queue\">Taasesituse järjekord</string>\n  <string name=\"action_video_speed\">Video kiirus</string>\n  <string name=\"action_subtitles\">Subtiitrid</string>\n  <string name=\"action_like\">Meeldib</string>\n  <string name=\"action_dislike\">Ei meeldi</string>\n  <string name=\"action_play_pause\">Mängi/Peata</string>\n  <string name=\"action_repeat_mode\">Taasesitusviis</string>\n  <string name=\"action_next\">Järgmine video</string>\n  <string name=\"action_previous\">Eelmine video</string>\n  <string name=\"action_high_quality\">Video kvaliteet</string>\n  <string name=\"content_block_no_skipping_mode\">Vahelejätmiseta režiim</string>\n  <string name=\"content_block_action_type\">Vahelejätmise eelistused</string>\n  <string name=\"content_block_action_none\">Ära tee midagi</string>\n  <string name=\"content_block_action_only_skip\">Jäta vahele</string>\n  <string name=\"content_block_action_toast\">Jäta vahele (teavitusega)</string>\n  <string name=\"content_block_action_dialog\">Kuva kinnitusdialoog</string>\n  <string name=\"content_block_filler\">Teemaväline (täide)</string>\n  <string name=\"keyboard_auto_show\">Kuva klaviatuur automaatselt</string>\n  <string name=\"cancel_dialog\">Tühista</string>\n  <string name=\"msg_player_error_source2\">URL ei tööta või seadme kellaaeg on vale.</string>\n  <string name=\"msg_player_error_video_source\">Video URL ei tööta või seadme kellaaeg on vale.</string>\n  <string name=\"msg_player_error_audio_source\">Heli URL ei tööta või seadme kellaaeg on vale.</string>\n  <string name=\"msg_player_error_subtitle_source\">Subtiitri URL ei tööta või seadme kellaaeg on vale.</string>\n  <string name=\"hide_upcoming\">Peida tulevased videod tellimustest</string>\n  <string name=\"hide_upcoming_channel\">Peida tulevased videod kanalilt</string>\n  <string name=\"hide_upcoming_home\">Peida tulevased videod avalehelt</string>\n  <string name=\"search_background_playback\">Kanali avamisel lülitu pilt-pildis režiimi</string>\n  <string name=\"trending_row_name\">Populaarne</string>\n  <string name=\"player_seek_type\">Video kerimise viis</string>\n  <string name=\"player_seek_regular\">Keri ja mängi</string>\n  <string name=\"player_seek_confirmation_pause\">Keri ja kinnita (peatatud)</string>\n  <string name=\"player_seek_confirmation_play\">Keri ja kinnita (mängib)</string>\n  <string name=\"hide_shorts_from_home\">Peida lühivideod avalehelt</string>\n  <string name=\"finish_on_disconnect\">Lülita äpp pärast seadme lahtiühenamist välja</string>\n  <string name=\"update_error\">Viga uuendamisel</string>\n  <string name=\"hide_shorts_from_history\">Peida lühivideod ajaloost</string>\n  <string name=\"hide_shorts_from_trending\">Peida lühivideod populaarsetest videotest</string>\n  <string name=\"disable_screensaver\">Keela ekraanisäästja</string>\n  <string name=\"description_not_found\">Kirjeldust ei leitud</string>\n  <string name=\"proxy_port_hint\">Puhverserveri port</string>\n  <string name=\"proxy_host_hint\">Puhverserveri host või IP</string>\n  <string name=\"proxy_username_hint\">Puhverserveri kasutajatunnus</string>\n  <string name=\"proxy_password_hint\">Puhverserveri parool</string>\n  <string name=\"proxy_type\">Puhverserveri tüüp</string>\n  <string name=\"enable_web_proxy\">Kasuta veebi puhverserverit</string>\n  <string name=\"proxy_not_supported\">Kasuta veebi puhverserverit (Android 4.4+)</string>\n  <string name=\"proxy_test_btn\">Test</string>\n  <string name=\"proxy_settings_title\">Puhverserveri sätted</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">Test#%d: tühistatud.</string>\n  <string name=\"proxy_type_invalid\">Puhverserveri tüüp pole määratud</string>\n  <string name=\"proxy_host_invalid\">Puhverserveri host pole määratud</string>\n  <string name=\"proxy_port_invalid\">Puhverserveri port peab olema suurem kui 0</string>\n  <string name=\"proxy_credentials_invalid\">Vale kasutajanimi või parool.</string>\n  <string name=\"proxy_test_aborted\">Test katkestatud. Vajalik puhverserveri sätete parandamine</string>\n  <string name=\"proxy_application_aborted\">Vajalik puhverserveri sätete parandamine</string>\n  <string name=\"proxy_test_start\">Test#%d: %s …</string>\n  <string name=\"proxy_test_error\">Viga#%d: %s</string>\n  <string name=\"proxy_test_status\">Staatus#%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Konfiguratsiooni faili aadress (*.ovpn)</string>\n  <string name=\"enable_openvpn\">Kasuta OpenVPNi</string>\n  <string name=\"openvpn_settings_title\">OpenVPNi sätted</string>\n  <string name=\"openvpn_application_aborted\">Vajalik OpenVPNi sätete parandamine</string>\n  <string name=\"openvpn_test_aborted\">Test katkestatud. Vajalik OpenVPNi sätete parandamine</string>\n  <string name=\"openvpn_address_invalid\">OpenVPNi konfiguratsiooni aadress pole määratud</string>\n  <string name=\"internet_censorship\">Puhverserveri eelistused</string>\n  <string name=\"rename_section\">Nimeta see jaotis ümber</string>\n  <string name=\"simple_edit_value_hint\">Sisesta väärtus</string>\n  <string name=\"seek_interval\">Kerimise intervall</string>\n  <string name=\"seek_interval_sec\">%s sek</string>\n  <string name=\"subtitle_system\">Süsteem (Androidi seaded &gt; Juurdepääsetavus)</string>\n  <string name=\"subtitle_scale\">Subtiitrite skaala</string>\n  <string name=\"audio_sync_fix_desc\">Alternatiivne viis heli/video sünkroonimiseks.</string>\n  <string name=\"audio_sync_fix\">Heli sünkroonimise parandus</string>\n  <string name=\"ambilight_ratio_fix_desc\">Mõjutab jõudlust!</string>\n  <string name=\"ambilight_ratio_fix\">Ambilighti/Kuvasuhte/Video skaala/Kuvatõmmiste parandus</string>\n  <string name=\"force_legacy_codecs_desc\">Parandab märkimisväärselt jõudlust madalama jõudlusega seadmetes. Maksimaalne eraldusvõime on 720p.</string>\n  <string name=\"force_legacy_codecs\">Kasuta vanu koodekeid (720p)</string>\n  <string name=\"live_stream_fix_desc\">Hoiatus! See seadistus keelab otseülekannete tagasikerimise, kuid parandab märkimisväärselt reaalajas voogesituse jõudlust madalama jõudlusega seadmetes. Maksimaalne eraldusvõime on 1080p.</string>\n  <string name=\"live_stream_fix_4k_desc\">Hoiatus! See seadistus keelab otseülekannete tagasikerimise, kuid parandab märkimisväärselt reaalajas voogesituse jõudlust. Maksimaalne eraldusvõime on 4K.</string>\n  <string name=\"live_stream_fix\">Otseülekande parandus (1080p)</string>\n  <string name=\"live_stream_fix_4k\">Otseülekande parandus (4K)</string>\n  <string name=\"playback_notifications_fix_desc\">Peidab rajavahetuse teavitused. Võib olla kasulik AOSP seadmetes.</string>\n  <string name=\"playback_notifications_fix\">Keela taasesituse märguanded</string>\n  <string name=\"amlogic_fix_desc\">Kaadrikao parandus Amlogic seadmetele</string>\n  <string name=\"amlogic_fix\">Amlogic 1080p\\@60fps parandus</string>\n  <string name=\"tunneled_video_playback_desc\">MÄRKUS: paus ei pruugi korralikult toimida! Tunneldatud video taasesitus lubab selliseid eeliseid nagu parem heli/video sünkroonimine (AV sünkroonimine) ja sujuvam taasesitus. Nõutav Android 5+</string>\n  <string name=\"tunneled_video_playback\">Tunneldatud video taasesitus (Android 5+)</string>\n  <string name=\"master_volume\">Peamine helitugevus</string>\n  <string name=\"volume_limit\">Helitugevuse piirang</string>\n  <string name=\"player_volume\">Helitugevus</string>\n  <string name=\"play_video\">Mängi</string>\n  <string name=\"remember_position_of_short_videos\">Pea meeles lühikeste videote taasesituse kulg (alla 5 min)</string>\n  <string name=\"player_show_tooltips\">Kuva nuppude vihjed</string>\n  <string name=\"action_like_unset\">Meeldib on määramata</string>\n  <string name=\"action_dislike_unset\">Ei meeldi on määramata</string>\n  <string name=\"various_buttons\">Ekraani ülaosas kuvatavad nupud</string>\n  <string name=\"not_compatible_with\">Valitud valik ei ühildu</string>\n  <string name=\"subtitle_position\">Subtiitrite alumine nihe</string>\n  <string name=\"pressing_home\">nupp KODU</string>\n  <string name=\"pressing_home_back\">nupp KODU või TAGASI</string>\n  <string name=\"pressing_back\">nupp TAGASI</string>\n  <string name=\"player_number_key_seek\">Keri numbriklahvidega</string>\n  <string name=\"save_remove_playlist\">Lisa/Eemalda esitusloend esitusloendite jaotisest</string>\n  <string name=\"remove_playlist\">Kustuta esitusloend</string>\n  <string name=\"removed_from_playlists\">Eemaldatud esitusloendite jaotisest</string>\n  <string name=\"saved_to_playlists\">Lisatud esitusloendite jaotisesse</string>\n  <string name=\"create_playlist\">Loo uus esitusloend</string>\n  <string name=\"add_video_to_new_playlist\">Lisa uude esitusloendisse</string>\n  <string name=\"cant_delete_empty_playlist\">Tühja esitusloendit ei saa kustutada</string>\n  <string name=\"playlist\">Esitusloend</string>\n  <string name=\"rename_playlist\">Esitusloendi ümbernimetamine</string>\n  <string name=\"cant_rename_empty_playlist\">Tühja esitusloendit ei saa ümber nimetada</string>\n  <string name=\"cant_rename_foreign_playlist\">Võõrast esitusloendit ei saa ümber nimetada</string>\n  <string name=\"cant_save_playlist\">Seda esitusloendit ei saa lisada</string>\n  <string name=\"enter_value\">Sisesta mis tahes mittetühi väärtus</string>\n  <string name=\"dialog_add_remove_from\">Lisa/Eemalda %s-st</string>\n  <string name=\"lb_playback_controls_skip_next\">Jäta järgmine vahele</string>\n  <string name=\"lb_playback_controls_skip_previous\">Jäta eelmine vahele/Keri tagasi algusesse</string>\n  <string name=\"alt_presets_behavior\">Alternatiivne profiilide käitumisviis (piira ribalaiust)</string>\n  <string name=\"alt_presets_behavior_desc\">Rakendus püüab säilitada ribalaiuse vastavust valitud profiiliga, selle asemel et sobitada eraldusvõime, fps ja koodeki vahel.</string>\n  <string name=\"sleep_timer\">Unetaimer (pult tund aega kasutamata)</string>\n  <string name=\"sleep_timer_desc\">Peata taasesitus, kui pulti ei ole tunni aja jooksul kasutatud</string>\n  <string name=\"disable_vsync\">Keela vsynciga sünkroniseerimine</string>\n  <string name=\"disable_vsync_desc\">See valik keelab kaadrite sünkroniseerimise ekraani vertikaalse sünkroonimissignaaliga (vsync). See võib parandada madala jõudlusega seadmete jõudlust.</string>\n  <string name=\"skip_codec_profile_check\">Jäta koodeki kontroll vahele</string>\n  <string name=\"skip_codec_profile_check_desc\">Video mängimise alustamisel ei kontrollita, kas koodek on toetatud.</string>\n  <string name=\"force_sw_codec\">Kasuta tarkvaralist videodekoodrit</string>\n  <string name=\"force_sw_codec_desc\">Peaaegu kõigi videote taasesitus on toetatud, kuid jõudlus on väga halb.</string>\n  <string name=\"playlist_order\">Sorteeri esitusloendit</string>\n  <string name=\"playlist_order_added_date_newer_first\">Lisamise kuupäev (uuem enne)</string>\n  <string name=\"playlist_order_added_date_older_first\">Lisamise kuupäev (vanem enne)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Avaldamise kuupäev (uuem enne)</string>\n  <string name=\"playlist_order_published_date_older_first\">Avaldamise kuupäev (vanem enne)</string>\n  <string name=\"playlist_order_popularity\">Populaarsus</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">Seda ei saa teha võõraste esitusloenditega</string>\n  <string name=\"owned_playlist_warning\">Viga. Seda toimingut saab teostada ainult esitusloenditega, mis kuuluvad sulle</string>\n  <string name=\"content_block_alt_server\">Kasuta alternatiivset serverit</string>\n  <string name=\"content_block_alt_server_desc\">Luba see valik, kui SponsorBlock keeldub töötamast.</string>\n  <string name=\"unset_stream_reminder\">Tühista otseülekande meeldetuletus</string>\n  <string name=\"set_stream_reminder\">Sea otseülekande meeldetuletus</string>\n  <string name=\"playback_starts_shortly\">Taasesitus algab automaatselt, kui otseülekanne algab</string>\n  <string name=\"starting_stream\">Otseülekande alustamine...</string>\n  <string name=\"action_playlist_remove\">Eemalda esitusloendist</string>\n  <string name=\"signin_view_title\">Kasutaja kood laeb...</string>\n  <string name=\"signin_view_description\">Sisselogimiseks sisesta see kood lehel %s</string>\n  <string name=\"signin_view_action_text\">Valmis</string>\n  <string name=\"require_checked\">Nõuab \\\"%s</string>\n  <string name=\"msg_press_again_to_exit\">Väljumiseks vajuta uuesti</string>\n  <string name=\"player_remaining_time\">Järelejäänud: %s</string>\n  <string name=\"player_ending_time\">Lõpeb %s</string>\n  <string name=\"badge_new_content\">UUS SISU</string>\n  <string name=\"badge_live\">OTSE</string>\n  <string name=\"add_device_view_description\">Seadme linkimiseks sisesta see kood oma telefoni YouTube\\'i rakenduse jaotisesse Seaded/Teleris vaatamine\\nMÄRKUS. Töötab ainult Gboardi klaviatuuriga.</string>\n  <string name=\"playback_controls_repeat_pause\">Korda Peata</string>\n  <string name=\"playback_controls_repeat_list\">Korda List</string>\n  <string name=\"action_subscribe_off\">Tellimused välja</string>\n  <string name=\"action_subscribe_on\">Tellimused sisse</string>\n  <string name=\"skip_24_rate\">Jäta 24fps vormingud vahele</string>\n  <string name=\"player_disable_suggestions\">Keela soovitused</string>\n  <string name=\"feedback\">Tagasiside</string>\n  <string name=\"sources\">Allikad</string>\n  <string name=\"releases\">Väljalasked</string>\n  <string name=\"prefer_avc_over_vp9\">Koodeki valik: eelista avc-d vp9-le</string>\n  <string name=\"open_chat\">Vestlus/Kommentaarid</string>\n  <string name=\"open_comments\">Ava kommentaarid</string>\n  <string name=\"place_chat_left\">Aseta vestlus vasakule</string>\n  <string name=\"use_alt_speech_recognizer\">Alternatiivne kõnetuvastaja</string>\n  <string name=\"time_format\">Ajaformaadi eelistus</string>\n  <string name=\"time_format_24\">24 tundi</string>\n  <string name=\"time_format_12\">12 tundi (AM/PM)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Tõsiselt vigane. Kasutada ainult viimasel juhul.</string>\n  <string name=\"speech_recognizer\">Kõnetuvastus</string>\n  <string name=\"speech_recognizer_system\">Süsteem (eelistatud)</string>\n  <string name=\"speech_recognizer_external_1\">Väline 1 (vigane, vajalik äpil mikrofonile juurdepääsu keelamine)</string>\n  <string name=\"speech_recognizer_external_2\">Väline 2 (vigane)</string>\n  <string name=\"real_channel_icon\">Kuva ikoon kanali nupul</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Kollane poolläbipaistval taustal</string>\n  <string name=\"subtitle_yellow_black\">Kollane mustal taustal</string>\n  <string name=\"player_pixel_ratio\">Kuvasuhe</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Tumehall (ühevärviline)</string>\n  <string name=\"disable_mic_permission\">Kõnetuvastaja korralikult töötamiseks keelake rakenduse jaoks juurdepääs mikrofonile.</string>\n  <string name=\"repeat_mode_shuffle\">Sega esitusloendid</string>\n  <string name=\"chat_left\">Vasak</string>\n  <string name=\"chat_right\">Parem</string>\n  <string name=\"card_real_thumbnails\">Asenda pisipildid kaadriga videost</string>\n  <string name=\"card_content\">Eelvaate pisipildi allikas</string>\n  <string name=\"thumb_quality_default\">Eelistus puudub</string>\n  <string name=\"thumb_quality_start\">Video algus</string>\n  <string name=\"thumb_quality_middle\">Video keskosa</string>\n  <string name=\"thumb_quality_end\">Video lõpp</string>\n  <string name=\"content_block_status\">Kontrolli SponsorBlocki olekut</string>\n  <string name=\"player_show_quality_info_bitrate\">Lisa kvaliteediteabele bitikiirus</string>\n  <string name=\"player_speed_button_old_behavior\">Taasta video kiiruse nupu vana käitumisviis</string>\n  <string name=\"protect_settings_with_password\">Kaitse sätteid parooliga</string>\n  <string name=\"enter_settings_password\">Sisesta sätete parool</string>\n  <string name=\"child_mode\">Lasterežiim</string>\n  <string name=\"child_mode_desc\">Otsing ja soovitatud sisu on keelatud. Väljumiseks on vajalik parooli sisestamine.</string>\n  <string name=\"lost_setting_warning\">Rakenduse sätteid muudetakse. Veenduge, et olete seadistustest varukoopia teinud.</string>\n  <string name=\"player_button_long_click\">Lühike klõps nupul kiirtoiminguks ja pikk klõps sätete dialoogi jaoks</string>\n  <string name=\"pause_history\">Peata ajalugu</string>\n  <string name=\"resume_history\">Jätka ajalugu</string>\n  <string name=\"disable_history\">Keela ajalugu</string>\n  <string name=\"enable_history\">Luba ajalugu</string>\n  <string name=\"clear_history\">Tühjenda ajalugu</string>\n  <string name=\"chapters\">Peatükid</string>\n  <string name=\"card_multiline_subtitle\">Mitmerealised subtiitrid</string>\n  <string name=\"auto_frame_rate_modes\">Toetatud režiimid</string>\n  <string name=\"loading\">Laadimine…</string>\n  <string name=\"hide_streams\">Peida otseülekanded tellimustest</string>\n  <string name=\"video_rotate\">Pööra</string>\n  <string name=\"trending_searches\">Populaarsed otsingud</string>\n  <string name=\"video_duration_any\">Kõik</string>\n  <string name=\"video_duration\">Pikkus</string>\n  <string name=\"video_duration_under_4\">Alla 4 minuti</string>\n  <string name=\"video_duration_between_4_20\">4-20 minutit</string>\n  <string name=\"video_duration_over_20\">Üle 20 minuti</string>\n  <string name=\"content_type\">Tüüp</string>\n  <string name=\"content_type_any\">Kõik</string>\n  <string name=\"content_type_video\">Video</string>\n  <string name=\"content_type_channel\">Kanal</string>\n  <string name=\"content_type_playlist\">Esitusloend</string>\n  <string name=\"content_type_movie\">Film</string>\n  <string name=\"video_features\">Esile tõstetud</string>\n  <string name=\"video_feature_any\">Kõik</string>\n  <string name=\"video_feature_live\">Otse</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">Sorteeri</string>\n  <string name=\"sort_by_relevance\">Asjakohasus</string>\n  <string name=\"sort_by_views\">Vaatamised</string>\n  <string name=\"sort_by_date\">Üleslaadimise aeg</string>\n  <string name=\"sort_by_rating\">Hinnang</string>\n  <string name=\"clear_search_history\">Puhasta otsingu ajalugu</string>\n  <string name=\"remove_from_subscriptions\">Peida video</string>\n  <string name=\"player_long_speed_list\">Suurem kiirustevalik</string>\n  <string name=\"alt_app_icon\">Alternatiivse rakenduse ikoon (vajab taaskäivitamist)</string>\n  <string name=\"network_stack\">Eelista %s võrgutarkvara</string>\n  <string name=\"player_network_stack\">Võrgutarkvara</string>\n  <string name=\"cronet_desc\">Cronet on Chromiumi võrgutarkvara. Võib aidata puhverdusprobleemide korral.</string>\n  <string name=\"unlock_all_formats\">Lase kasutada kõiki videovorminguid</string>\n  <string name=\"unlock_all_formats_desc\">Mõnes seadmes teatab operatsioonisüsteem valesti, et mõni videovorming pole toetatud, kuigi tegelikkuses on.\\n(näiteks 1080p nutitelerid teatavad tihti, et 4K ei toetata).</string>\n  <string name=\"okhttp_desc\">Aeglane kuid stabiilne.</string>\n  <string name=\"select_channel_section\">Saabudes vidina kaudu ava vastav jaotis</string>\n  <string name=\"enable_voice_search_desc\">Ametlik äpp asendatakse nn sillaga. Sild aitab globaalseid otsingupäringuid meie rakendusse üle kanda.</string>\n  <string name=\"enable_master_password\">Kasuta peaparooli</string>\n  <string name=\"enter_master_password\">Sisesta peaparool</string>\n  <string name=\"disable_stream_buffer\">Keela puhver otseülekannetel</string>\n  <string name=\"disable_stream_buffer_desc\">Parandus olukorraks, kus otseülekanne jääb reaalajast maha. Märkus: Taasesitus võib olla ebakorrapärane</string>\n  <string name=\"sony_frame_drop_fix\">Kaadrikao parandus #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">Paranda viivitused Sony teleris ja mõnes muus seadmes. Märkus: võimalikud probleemid heli sünkroonimisega.</string>\n  <string name=\"audio_language\">Heli keel</string>\n  <string name=\"old_home_look\">Kodu jaotise vana välimus</string>\n  <string name=\"hide_shorts_everywhere\">Peida lühivideod kõikjalt</string>\n  <string name=\"update_found\">Uuenda</string>\n  <string name=\"volume_boost_warning\">Kõik seadistused, mis ületavad piiri, võivad kõlareid kahjustada</string>\n  <string name=\"play_video_incognito\">Mängi privaatselt</string>\n  <string name=\"header_kids_home\">Lapsed</string>\n  <string name=\"player_section_playlist\">Kasuta praeguse jaotise sisu esitusloendina</string>\n  <string name=\"header_trending\">Populaarsed</string>\n  <string name=\"screensaver\">Ekraanisäästja</string>\n  <string name=\"player_screen_off_timeout\">Ekraani väljalülitamise aeg</string>\n  <string name=\"old_update_notifications\">Uuendusteatiste vana stiil</string>\n  <string name=\"dialog_notification\">Teavita uuendustest hüpikaknaga</string>\n  <string name=\"mark_as_watched\">Märgi vaadatuks</string>\n  <string name=\"player_ui_animations\">Luba animeeritud kasutajaliides</string>\n  <string name=\"player_likes_count\">Näita meeldimiste/mittemeeldimiste arvu</string>\n  <string name=\"sorting_alphabetically2\">Tähestiku järgi (animeeritud eelvaated)</string>\n  <string name=\"sorting_default\">Eelistus puudub (animeeritud eelvaated)</string>\n  <string name=\"content_block_exclude_channel\">Ära kasuta selle kanali videotel SponsorBlocki</string>\n  <string name=\"content_block_stop_excluding_channel\">Kasuta selle kanali videotel SponsorBlocki</string>\n  <string name=\"player_chapter_notification\">Kiireks liikumiseks peatükkide vahel kuva märguanne</string>\n  <string name=\"subtitle_remember\">Subtiitrite lubamise eelistus kanali kaupa</string>\n  <string name=\"amazon_frame_drop_fix\">Kaadrikao parandus #2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">Mõeldud Amazon Sticki seadmetele. Võib töötada ka teistes seadmetes.</string>\n  <string name=\"default_stack_desc\">Sisseehitatud võrgutarkvara.</string>\n  <string name=\"item_postion\">positsioon</string>\n  <string name=\"player_screen_off_dimming\">Ekraani väljalülitamise hämardamise määr</string>\n  <string name=\"screensaver_timout\">Ekraanisäästja sisselülitamise aeg</string>\n  <string name=\"screensaver_dimming\">Ekraanisäästja hämardamise määr</string>\n  <string name=\"player_ui_on_next\">Kuva kasutajaliides järgmisele videole lülitudes</string>\n  <string name=\"autogenerated\">loodud automaatselt</string>\n  <string name=\"player_auto_volume\">Automaatne helitugevuse reguleerimine</string>\n  <string name=\"header_shorts\">Lühivideod</string>\n  <string name=\"player_global_focus\">Sujuv navigeerimine pleieri nupuridade vahel</string>\n  <string name=\"auto_history\">Automaatne (konto säte)</string>\n  <string name=\"remember_position_subscriptions\">Jäta meelde jaotise Tellimused olek</string>\n  <string name=\"msg_player_unknown_error\">Tundmatu viga</string>\n  <string name=\"header_notifications\">Teavitused</string>\n  <string name=\"disable_search_history\">Keela otsinguajalugu</string>\n  <string name=\"unlock_high_bitrate_formats\">Luba suure bitikiirusega vp9 vormingud</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Luba suure bitikiirusega mp4a vormingud</string>\n  <string name=\"color_scheme_blue\">Sinine</string>\n  <string name=\"color_scheme_dark_blue\">Tumesinine</string>\n  <string name=\"player_speed_per_channel\">Jäta meelde iga kanali kohta</string>\n  <string name=\"disable_popular_searches\">Keela populaarsed otsingupäringud</string>\n  <string name=\"multi_profiles\">Kasuta iga konto jaoks eraldi sätteid</string>\n  <string name=\"protect_account_with_password\">Kaitse kontot parooliga</string>\n  <string name=\"enter_account_password\">Sisesta konto parool</string>\n  <string name=\"show_connect_messages\">Kuva ühendussõnumid</string>\n  <string name=\"prefer_avc_over_vp9_desc\">HOIATUS: maksimaalselt 1080p</string>\n  <string name=\"play_next\">Mängi järgmisena</string>\n  <string name=\"hide_watched_from_subscriptions\">Peida vaadatud videod tellimustest</string>\n  <string name=\"hide_unwanted_content\">Soovimatu sisu eelistused</string>\n  <string name=\"hide_watched_from_home\">Peida vaadatud videod avalehelt</string>\n  <string name=\"remote_control_permission\">Taustesituse ajal kaugjuhtimine kasutamine nõuab Android TV ülekatte luba</string>\n  <string name=\"login_from_browser\">Logi sisse brauserist</string>\n  <string name=\"disable_remote_history\">Keela ajalugu kaugjuhtimise kasutamisel</string>\n  <string name=\"keyboard_fix\">Klaviatuuri kuvamise parandamine klahvivajutusega OK (G20s ja teised)</string>\n  <string name=\"nothing_found\">Midagi ei leitud</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-fa-rIR/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">خانه</string>\n  <string name=\"title_search\">جستجو</string>\n  <string name=\"header_subscriptions\">اشتراک‌ها</string>\n  <string name=\"header_history\">تاریخچه</string>\n  <string name=\"header_music\">موسیقی</string>\n  <string name=\"header_news\">اخبار</string>\n  <string name=\"header_gaming\">بازی‌ها</string>\n  <string name=\"header_playlists\">فهرست‌های پخش</string>\n  <string name=\"header_settings\">تنظیمات</string>\n  <string name=\"subscriptions_signin_title\">تماشای جدیدترین ویدیوها از کانال‌های مورد علاقه شما</string>\n  <string name=\"subscriptions_signin_subtitle\">برای مشاهده اشتراک‌های خود وارد شوید</string>\n  <string name=\"subscriptions_signin_button_text\">ورود</string>\n  <string name=\"library_signin_to_show_more\">ویدیوهایی که پسندیده‌اید، ذخیره کرده‌اید یا در آن‌ها مشترک شده‌اید را مشاهده کنید</string>\n  <string name=\"library_signin_subtitle\">برای مشاهده کتابخانه خود وارد شوید</string>\n  <string name=\"action_signin\">ورود</string>\n  <string name=\"title_video_formats\">فرمت‌های ویدیو</string>\n  <string name=\"title_audio_formats\">فرمت‌های صوتی</string>\n  <string name=\"subtitle_category_title\">زیرنویس</string>\n  <string name=\"video_max_quality\">خودکار (بالاترین کیفیت)</string>\n  <string name=\"audio_max_quality\">خودکار (بالاترین کیفیت)</string>\n  <string name=\"subtitles_disabled\">زیرنویس غیرفعال است</string>\n  <string name=\"auto_frame_rate\">نرخ فریم خودکار</string>\n  <string name=\"frame_rate_correction\">رفع fps:\\n%s</string>\n  <string name=\"category_background_playback\">پخش در پس‌زمینه</string>\n  <string name=\"not_implemented\">پیاده‌سازی نشده</string>\n  <string name=\"option_background_playback_off\">غیرفعال</string>\n  <string name=\"option_background_playback_pip\">پنجره در پنجره</string>\n  <string name=\"option_background_playback_only_audio\">فقط صدا</string>\n  <string name=\"playback_settings\">تنظیمات پخش</string>\n  <string name=\"video_speed\">سرعت ویدیو</string>\n  <string name=\"resolution_switch\">تغییر رزولوشن</string>\n  <string name=\"video_buffer\">بافر ویدیو</string>\n  <string name=\"video_buffer_size_low\">پایین</string>\n  <string name=\"video_buffer_size_med\">متوسط</string>\n  <string name=\"video_buffer_size_high\">بالا</string>\n  <string name=\"update_changelog\">تغییرات نسخه</string>\n  <string name=\"install_update\">نصب بروزرسانی</string>\n  <string name=\"section_is_empty\">چیزی اینجا نیست.</string>\n  <string name=\"dialog_add_to_playlist\">افزودن/حذف از فهرست پخش</string>\n  <string name=\"msg_signed_users_only\">فقط کاربران وارد شده</string>\n  <string name=\"msg_cant_load_content\">بارگیری محتوا ناموفق بود. \\n1) تاریخ و زمان دستگاه را بررسی کنید. \\n2) اتصال شبکه را بررسی کنید. \\n3) سعی کنید وارد حساب خود شوید.</string>\n  <string name=\"title_video_presets\">پیش‌تنظیمات ویدیو</string>\n  <string name=\"settings_accounts\">حساب‌ها</string>\n  <string name=\"settings_left_panel\">ویرایش دسته‌ها</string>\n  <string name=\"settings_themes\">تم‌ها</string>\n  <string name=\"settings_other\">سایر</string>\n  <string name=\"settings_player\">تنظیمات پخش کننده</string>\n  <string name=\"settings_language\">زبان</string>\n  <string name=\"settings_linked_devices\">دستگاه‌های متصل</string>\n  <string name=\"settings_about\">درباره برنامه</string>\n  <string name=\"dialog_account_list\">انتخاب حساب</string>\n  <string name=\"dialog_account_none\">هیچ‌کدام</string>\n  <string name=\"dialog_remove_account\">حذف حساب</string>\n  <string name=\"dialog_add_account\">افزودن حساب</string>\n  <string name=\"default_lang\">پیش‌فرض</string>\n  <string name=\"dialog_select_language\">زبان برنامه</string>\n  <string name=\"header_channels\">کانال‌ها</string>\n  <string name=\"playback_queue_category_title\">فهرست انتظار پخش</string>\n  <string name=\"subtitle_default\">پیش‌فرض</string>\n  <string name=\"subtitle_white_semi_transparent\">سفید روی پس‌زمینه نیمه شفاف</string>\n  <string name=\"subtitle_style\">سبک زیرنویس</string>\n  <string name=\"subtitle_language\">زبان زیرنویس</string>\n  <string name=\"action_search\">جستجو</string>\n  <string name=\"settings_main_ui\">رابط کاربری اصلی</string>\n  <string name=\"dialog_main_ui\">رابط کاربری برنامه</string>\n  <string name=\"card_animated_previews\">پیش‌نمایش متحرک</string>\n  <string name=\"web_site\">وب‌سایت</string>\n  <string name=\"donation\">حمایت از توسعه‌دهنده</string>\n  <string name=\"dialog_about\">درباره برنامه</string>\n  <string name=\"dialog_player_ui\">پخش کننده ویدیو</string>\n  <string name=\"player_show_ui_on_pause\">نمایش رابط کاربری هنگام توقف</string>\n  <string name=\"player_pause_on_ok\">دکمه OK ویدیو را متوقف می‌کند</string>\n  <string name=\"player_only_ui\">فقط رابط کاربری</string>\n  <string name=\"player_ui_and_pause\">رابط کاربری و توقف ویدیو</string>\n  <string name=\"player_only_pause\">فقط توقف ویدیو</string>\n  <string name=\"check_for_updates\">بررسی بروزرسانی‌ها</string>\n  <string name=\"update_not_found\">شما از آخرین نسخه استفاده می‌کنید</string>\n  <string name=\"update_in_progress\">لطفا صبر کنید…</string>\n  <string name=\"player_ui_hide_behavior\">پنهان کردن خودکار رابط کاربری</string>\n  <string name=\"option_never\">هرگز</string>\n  <string name=\"side_panel_sections\">مرتب‌سازی بخش‌ها</string>\n  <string name=\"boot_to_section\">راه‌اندازی به بخش</string>\n  <string name=\"large_ui\">رابط کاربری بزرگ</string>\n  <string name=\"video_grid_scale\">مقیاس شبکه ویدیو</string>\n  <string name=\"scale_ui\">مقیاس رابط کاربری</string>\n  <string name=\"color_scheme\">طرح رنگ</string>\n  <string name=\"color_scheme_default\">پیش‌فرض</string>\n  <string name=\"color_scheme_red_grey\">قرمز خاکستری</string>\n  <string name=\"color_scheme_red\">قرمز</string>\n  <string name=\"color_scheme_dark_grey\">خاکستری تیره</string>\n  <string name=\"disable_update_check\">غیرفعال کردن بررسی بروزرسانی</string>\n  <string name=\"show_again\">نمایش مجدد</string>\n  <string name=\"check_updates_auto\">اعلان هنگام وجود بروزرسانی جدید</string>\n  <string name=\"select_account_on_boot\">نمایش انتخاب حساب هنگام راه‌اندازی</string>\n  <string name=\"player_other\">سایر گزینه‌ها</string>\n  <string name=\"player_full_date\">تاریخ دقیق در توضیحات</string>\n  <string name=\"open_channel\">باز کردن کانال</string>\n  <string name=\"open_playlist\">باز کردن فهرست پخش</string>\n  <string name=\"not_interested\">علاقه‌ای ندارم</string>\n  <string name=\"you_wont_see_this_video\">این ویدیو از پیشنهادها حذف شد</string>\n  <string name=\"dialog_search\">جستجو</string>\n  <string name=\"settings_search\">جستجو</string>\n  <string name=\"instant_voice_search\">جستجوی صوتی</string>\n  <string name=\"option_background_playback_behind\">پخش در پس‌زمینه</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">اطلاعات ویدیوی بعدی هنوز بارگیری نشده است</string>\n  <string name=\"card_multiline_title\">عنوان‌های چند خطی</string>\n  <string name=\"cards_style\">سبک کارت‌ها</string>\n  <string name=\"repeat_mode_all\">پخش تمام ویدیوها به ترتیب</string>\n  <string name=\"repeat_mode_one\">تکرار ویدیوی فعلی</string>\n  <string name=\"repeat_mode_pause\">توقف پس از هر ویدیو</string>\n  <string name=\"repeat_mode_pause_alt\">فقط پخش مداوم فهرست‌های پخش</string>\n  <string name=\"repeat_mode_none\">پایان پخش پس از یک ویدیو</string>\n  <string name=\"subscribed_to_channel\">در کانال مشترک شده‌اید</string>\n  <string name=\"unsubscribed_from_channel\">از کانال لغو اشتراک شده‌اید</string>\n  <string name=\"subtitle_yellow_transparent\">زرد روی پس‌زمینه شفاف</string>\n  <string name=\"subtitle_white_black\">سفید روی پس‌زمینه سیاه</string>\n  <string name=\"player_seek_preview\">پیش‌نمایش هنگام جستجوی ویدیو</string>\n  <string name=\"color_scheme_dark_grey_oled\">خاکستری تیره (OLED)</string>\n  <string name=\"channels_section_sorting\">مرتب‌سازی بخش کانال‌ها</string>\n  <string name=\"sorting_by_new_content\">محتوا جدید</string>\n  <string name=\"sorting_alphabetically\">الفبایی</string>\n  <string name=\"sorting_last_viewed\">آخرین بازدید</string>\n  <string name=\"player_pause_when_seek\">توقف هنگام جستجوی ویدیو</string>\n  <string name=\"playlists_style\">سبک بخش فهرست‌های پخش</string>\n  <string name=\"playlists_style_grid\">شبکه</string>\n  <string name=\"playlists_style_rows\">ردیف‌ها</string>\n  <string name=\"player_show_clock\">نمایش ساعت در پخش کننده ویدیو</string>\n  <string name=\"player_show_remaining_time\">نمایش زمان باقی‌مانده در پخش کننده ویدیو</string>\n  <string name=\"player_show_ending_time\">نمایش زمان پایان در پخش کننده ویدیو</string>\n  <string name=\"open_channel_uploads\">باز کردن ویدیوهای کانال</string>\n  <string name=\"app_exit_shortcut\">خروج از برنامه</string>\n  <string name=\"app_exit_none\">خروج نکن</string>\n  <string name=\"app_double_back_exit\">دوبار فشار دهید</string>\n  <string name=\"app_single_back_exit\">یکبار فشار دهید</string>\n  <string name=\"action_video_zoom\">بزرگنمایی ویدیو</string>\n  <string name=\"video_zoom\">بزرگنمایی ویدیو</string>\n  <string name=\"video_zoom_default\">پیش‌فرض</string>\n  <string name=\"video_zoom_fit_width\">مناسب عرض</string>\n  <string name=\"video_zoom_fit_height\">مناسب ارتفاع</string>\n  <string name=\"video_zoom_fit_both\">مناسب عرض یا ارتفاع</string>\n  <string name=\"video_zoom_stretch\">کشش</string>\n  <string name=\"color_scheme_teal\">فیروزه‌ای</string>\n  <string name=\"color_scheme_teal_oled\">فیروزه‌ای (OLED)</string>\n  <string name=\"player_seek_preview_none\">غیرفعال</string>\n  <string name=\"player_seek_preview_single\">یک فریم</string>\n  <string name=\"player_seek_preview_carousel\">چند فریم</string>\n  <string name=\"player_seek_preview_carousel_slow\">چند فریم (کند، بر اساس فریم‌های کلیدی)</string>\n  <string name=\"player_seek_preview_carousel_fast\">چند فریم (سریع، دقیق نیست)</string>\n  <string name=\"unsubscribe_from_channel\">لغو اشتراک از کانال</string>\n  <string name=\"card_title_lines_num\">تعداد خطوط عنوان کارت</string>\n  <string name=\"card_auto_scrolled_title\">حرکت خودکار عنوان طولانی</string>\n  <string name=\"share_link\">لینک اشتراک‌گذاری</string>\n  <string name=\"subscribe_to_channel\">اشتراک در کانال</string>\n  <string name=\"wait_data_loading\">لطفا منتظر بمانید تا داده‌ها بارگیری شوند...</string>\n  <string name=\"auto_frame_rate_pause\">نرخ فریم خودکار (توقف هنگام تغییر)</string>\n  <string name=\"auto_frame_rate_applying\">اعمال نرخ فریم خودکار%sx%s\\@%s</string>\n  <string name=\"audio_shift\">تنظیم صدا</string>\n  <string name=\"player_remember_speed\">به خاطر سپردن سرعت</string>\n  <string name=\"mark_channel_as_watched\">علامت‌گذاری به عنوان تماشا شده</string>\n  <string name=\"channel_marked_as_watched\">کانال به عنوان تماشا شده علامت‌گذاری شد</string>\n  <string name=\"dialog_add_device\">افزودن دستگاه</string>\n  <string name=\"dialog_remove_all_devices\">حذف همه دستگاه‌ها</string>\n  <string name=\"device_link_enabled\">اتصال فعال شد</string>\n  <string name=\"device_connected\">دستگاه \\\"%s\\\" متصل شد</string>\n  <string name=\"device_disconnected\">دستگاه \\\"%s\\\" قطع شد</string>\n  <string name=\"settings_ui_scale\">مقیاس رابط کاربری</string>\n  <string name=\"msg_restart_app\">لطفا برنامه را مجددا راه‌اندازی کنید تا این تنظیمات اعمال شوند</string>\n  <string name=\"settings_block\">مسدود کردن محتوا</string>\n  <string name=\"msg_applying\">در حال اعمال%s</string>\n  <string name=\"msg_done\">انجام شد</string>\n  <string name=\"settings_general\">عمومی</string>\n  <string name=\"settings_video\">ویدیو</string>\n  <string name=\"content_block_confirm_skip\">تأیید هنگام رد کردن</string>\n  <string name=\"content_block_categories\">رد کردن بخش‌ها</string>\n  <string name=\"content_block_sponsor\">اسپانسر</string>\n  <string name=\"content_block_intro\">مقدمه/فاصله</string>\n  <string name=\"content_block_outro\">خاتمه/تیتراژ پایانی</string>\n  <string name=\"content_block_interaction\">یادآوری تعامل (اشتراک در کانال)</string>\n  <string name=\"content_block_self_promo\">تبلیغ شخصی/غیرپولی</string>\n  <string name=\"content_block_music_off_topic\">بخش غیرموسیقی در ویدیو</string>\n  <string name=\"confirm_segment_skip\">آیا می‌خواهید بخش%s\\\"\\? را رد کنید</string>\n  <string name=\"cancel_segment_skip\">لغو رد کردن بخش</string>\n  <string name=\"msg_skipping_segment\">در حال رد کردن بخش\\\"%s\\\"…</string>\n  <string name=\"content_block_notification_type\">نوع اعلان</string>\n  <string name=\"content_block_notify_none\">بدون اعلان</string>\n  <string name=\"content_block_notify_dialog\">کادر تأیید</string>\n  <string name=\"return_to_launcher\">بازگشت به پخش کننده از کانال‌های ATV / جستجو</string>\n  <string name=\"btn_confirm\">تأیید</string>\n  <string name=\"player_low_video_quality\">کیفیت پایین ویدیو</string>\n  <string name=\"remote_session_closed\">جلسه راه‌دور بسته شد</string>\n  <string name=\"msg_mode_switch_error\">امکان تغییر حالت نمایش به \\\"%s\\\" وجود ندارد</string>\n  <string name=\"msg_player_error\">خطایی در پخش کننده%s رخ داده است. در حال راه‌اندازی مجدد...</string>\n  <string name=\"video_preset_disabled\">بدون پیش‌تنظیم</string>\n  <string name=\"video_preset_enabled\">فرمت ویدیوی بعدی مطابق با پیش‌تنظیم تنظیم خواهد شد</string>\n  <string name=\"player_sleep_timer\">تایمر خواب</string>\n  <string name=\"player_show_quality_info\">نمایش اطلاعات کیفیت ویدیو در پخش کننده</string>\n  <string name=\"player_remember_each_speed\">به خاطر سپردن سرعت هر ویدیو</string>\n  <string name=\"player_remember_speed_none\">هیچ‌کدام</string>\n  <string name=\"player_remember_speed_all\">یکسان برای همه ویدیوها</string>\n  <string name=\"player_remember_speed_each\">برای هر ویدیو</string>\n  <string name=\"msg_player_error_source\">امکان دانلود ویدیو وجود ندارد</string>\n  <string name=\"msg_player_error_renderer\">فرمت ویدیوی انتخاب شده پشتیبانی نمی‌شود.\\nممکن است مشکل از نرم‌افزار دستگاه باشد. لطفا دستگاه را مجددا راه‌اندازی کنید.</string>\n  <string name=\"msg_player_error_unexpected\">خطای ناشناخته در دیکودر ویدیو</string>\n  <string name=\"video_aspect\">نسبت تصویر</string>\n  <string name=\"player_tweaks\">تنظیمات پیشرفته</string>\n  <string name=\"header_uploads\">ویدیوها</string>\n  <string name=\"player_show_global_clock\">نمایش ساعت همیشه روی صفحه</string>\n  <string name=\"player_show_global_ending_time\">نمایش زمان پایان همیشه روی صفحه</string>\n  <string name=\"uploads_old_look\">بازگشت به ظاهر قدیمی بخش ویدیوها</string>\n  <string name=\"background_playback_activation\">پخش در پس‌زمینه (فعال‌سازی)</string>\n  <string name=\"channels_old_look\">بازگشت به ظاهر قدیمی بخش کانال‌ها</string>\n  <string name=\"channels_auto_load\">بارگیری خودکار محتوای بخش کانال‌ها</string>\n  <string name=\"return_to_background_video\">بازگشت به پخش ویدیو در پس‌زمینه</string>\n  <string name=\"pin_unpin_from_sidebar\">ثابت کردن/لغو ثابت از نوار کناری</string>\n  <string name=\"pinned_to_sidebar\">ثابت شده در نوار کناری</string>\n  <string name=\"unpin_from_sidebar\">لغو ثابت از نوار کناری</string>\n  <string name=\"unpinned_from_sidebar\">از نوار کناری لغو ثابت شد</string>\n  <string name=\"dialog_select_country\">کشور</string>\n  <string name=\"settings_language_country\">زبان/کشور</string>\n  <string name=\"share_embed_link\">اشتراک‌گذاری لینک جاسازی</string>\n  <string name=\"card_text_scroll_factor\">سرعت اسکرول متن کارت</string>\n  <string name=\"preferred_update_source\">انتخاب منبع بروزرسانی</string>\n  <string name=\"hide_shorts\">پنهان کردن کوته ویدیوها از اشتراک‌ها</string>\n  <string name=\"key_remapping\">تنظیم مجدد دکمه‌ها</string>\n  <string name=\"screen_dimming\">تاریک کردن صفحه</string>\n  <string name=\"removed_from_playback_queue\">از فهرست انتظار پخش حذف شد</string>\n  <string name=\"added_to_playback_queue\">به فهرست انتظار پخش اضافه شد</string>\n  <string name=\"add_remove_from_playback_queue\">افزودن/حذف از فهرست انتظار پخش</string>\n  <string name=\"proxy_enabled\">پراکسی فعال شد</string>\n  <string name=\"proxy_disabled\">پراکسی غیرفعال شد</string>\n  <string name=\"uploads_row_name\">ویدیوها</string>\n  <string name=\"playlists_row_name\">فهرست‌های پخش ایجاد شده</string>\n  <string name=\"popular_uploads_row_name\">ویدیوهای محبوب</string>\n  <string name=\"breaking_news_row_name\">اخبار فوری</string>\n  <string name=\"covid_news_row_name\">اخبار کووید-۱۹</string>\n  <string name=\"enable_voice_search\">فعال‌سازی جستجوی صوتی (نیاز به پشتیبانی نرم‌افزاری دارد)</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">اشتراک/لغو اشتراک از کانال</string>\n  <string name=\"app_backup_restore\">پشتیبان‌گیری/بازیابی</string>\n  <string name=\"app_backup\">پشتیبان‌گیری از داده‌ها</string>\n  <string name=\"app_restore\">بازیابی داده‌ها از پشتیبان</string>\n  <string name=\"skip_each_segment_once\">بخش‌ها را دوباره رد نکن</string>\n  <string name=\"disable_ok_long_press\">غیرفعال کردن فشار طولانی روی دکمه OK</string>\n  <string name=\"player_time_correction\">نمایش زمان با توجه به سرعت</string>\n  <string name=\"sponsor_color_markers\">نشانگرهای رنگی روی نوار پیشرفت</string>\n  <string name=\"refresh_section\">بروزرسانی بخش</string>\n  <string name=\"option_disabled\">غیرفعال</string>\n  <string name=\"live_now_row_name\">پخش زنده</string>\n  <string name=\"run_in_background\">اجرا در پس‌زمینه</string>\n  <string name=\"settings_remote_control\">کنترل از راه دور</string>\n  <string name=\"background_service_started\">سرویس پس‌زمینه شروع شد</string>\n  <string name=\"dialog_add_to\">افزودن به %s</string>\n  <string name=\"dialog_remove_from\">حذف از %s</string>\n  <string name=\"added_to\">به %s اضافه شد</string>\n  <string name=\"removed_from\">از %s حذف شد</string>\n  <string name=\"video_preset_adaptive\">سازگار</string>\n  <string name=\"content_block_preview_recap\">پیش‌نمایش/خلاصه</string>\n  <string name=\"content_block_highlight\">اشاره یا برجسته‌سازی ویدیو</string>\n  <string name=\"removed_from_history\">از تاریخچه حذف شد</string>\n  <string name=\"remove_from_history\">حذف از تاریخچه</string>\n  <string name=\"upload_date\">تاریخ آپلود</string>\n  <string name=\"upload_date_any\">همه زمان‌ها</string>\n  <string name=\"upload_date_today\">امروز</string>\n  <string name=\"upload_date_this_week\">این هفته</string>\n  <string name=\"upload_date_this_month\">این ماه</string>\n  <string name=\"upload_date_this_year\">امسال</string>\n  <string name=\"double_refresh_rate\">نرخ تازه‌سازی دوبرابر</string>\n  <string name=\"upload_date_last_hour\">آخرین ساعت</string>\n  <string name=\"focus_on_search_results\">تمرکز خودکار روی نتایج جستجو</string>\n  <string name=\"context_menu\">منوی زمینه</string>\n  <string name=\"add_remove_from_recent_playlist\">افزودن/حذف از فهرست پخش اخیر</string>\n  <string name=\"hide_settings_section\">پنهان کردن بخش تنظیمات (خطرناک!)</string>\n  <string name=\"volume\">صدا %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s ثانیه</string>\n  <string name=\"audio_shift_sec\">%s ثانیه</string>\n  <string name=\"ui_hide_timeout_sec\">%s ثانیه</string>\n  <string name=\"screen_dimming_timeout_min\">%s دقیقه</string>\n  <string name=\"mark_all_channels_watched\">علامت‌گذاری همه کانال‌ها به عنوان تماشا شده</string>\n  <string name=\"move_section_up\">انتقال بخش به بالا</string>\n  <string name=\"move_section_down\">انتقال بخش به پایین</string>\n  <string name=\"player_buttons\">تنظیم دکمه‌های پخش کننده</string>\n  <string name=\"action_playlist_add\">افزودن به فهرست پخش</string>\n  <string name=\"action_video_stats\">آمار ویدیو</string>\n  <string name=\"action_subscribe\">اشتراک</string>\n  <string name=\"action_channel\">باز کردن کانال</string>\n  <string name=\"action_screen_off\">خاموش کردن صفحه</string>\n  <string name=\"action_pip\">پنجره در پنجره</string>\n  <string name=\"action_playback_queue\">فهرست انتظار پخش</string>\n  <string name=\"action_video_speed\">سرعت ویدیو</string>\n  <string name=\"action_subtitles\">زیرنویس</string>\n  <string name=\"action_like\">پسندیدن</string>\n  <string name=\"action_dislike\">نپسندیدن</string>\n  <string name=\"action_play_pause\">پخش/توقف</string>\n  <string name=\"action_repeat_mode\">حالت تکرار</string>\n  <string name=\"action_next\">ویدیوی بعدی</string>\n  <string name=\"action_previous\">ویدیوی قبلی</string>\n  <string name=\"action_high_quality\">کیفیت ویدیو</string>\n  <string name=\"content_block_no_skipping_mode\">حالت عدم رد کردن</string>\n  <string name=\"content_block_action_type\">انتخاب عمل</string>\n  <string name=\"content_block_action_none\">هیچ کاری نکن</string>\n  <string name=\"content_block_action_only_skip\">فقط رد کردن</string>\n  <string name=\"content_block_action_toast\">رد کردن با اعلان</string>\n  <string name=\"content_block_action_dialog\">نمایش دکمه رد کردن</string>\n  <string name=\"content_block_filler\">خارج از موضوع (پرکننده)</string>\n  <string name=\"keyboard_auto_show\">نمایش خودکار صفحه کلید</string>\n  <string name=\"cancel_dialog\">لغو</string>\n  <string name=\"msg_player_error_source2\">آدرس URL کار نمی‌کند یا زمان دستگاه نادرست است.\\nمعمولا این یک خطای برنامه است.</string>\n  <string name=\"hide_upcoming\">پنهان کردن آینده از اشتراک‌ها</string>\n  <string name=\"search_background_playback\">فعال‌سازی پنجره در پنجره هنگام جستجو از پخش کننده</string>\n  <string name=\"trending_row_name\">محبوب‌ها</string>\n  <string name=\"player_seek_type\">رفتار جستجو</string>\n  <string name=\"player_seek_regular\">عادی</string>\n  <string name=\"player_seek_confirmation_pause\">با تأیید (توقف هنگام جستجو)</string>\n  <string name=\"player_seek_confirmation_play\">با تأیید (پخش هنگام جستجو)</string>\n  <string name=\"hide_shorts_from_home\">پنهان کردن کوته ویدیوها از صفحه اصلی</string>\n  <string name=\"finish_on_disconnect\">بستن برنامه پس از قطع اتصال موبایل/تبلت</string>\n  <string name=\"update_error\">خطا در بروزرسانی</string>\n  <string name=\"hide_shorts_from_history\">پنهان کردن کوته ویدیوها از تاریخچه</string>\n  <string name=\"disable_screensaver\">غیرفعال کردن محافظ صفحه</string>\n  <string name=\"player_ok_button_behavior\">رفتار دکمه OK</string>\n  <string name=\"content_block_notify_toast\">اعلان</string>\n  <string name=\"intent_force_close\">اجبار به خروج اگر ویدیو از منبع خارجی باز شود</string>\n  <string name=\"pin_unpin_playlist\">ثابت کردن/لغو ثابت فهرست پخش از نوار کناری</string>\n  <string name=\"pin_unpin_channel\">ثابت کردن/لغو ثابت کانال از نوار کناری</string>\n  <string name=\"action_video_info\">توضیحات ویدیو</string>\n  <string name=\"description_not_found\">توضیحات یافت نشد</string>\n  <string name=\"subtitle_white_transparent\">سفید روی پس‌زمینه شفاف</string>\n  <string name=\"proxy_port_hint\">پورت پراکسی</string>\n  <string name=\"proxy_host_hint\">نام یا IP میزبان پراکسی</string>\n  <string name=\"proxy_username_hint\">نام کاربری پراکسی</string>\n  <string name=\"proxy_password_hint\">رمز عبور پراکسی</string>\n  <string name=\"proxy_type\">نوع پراکسی</string>\n  <string name=\"enable_web_proxy\">استفاده از Web Proxy</string>\n  <string name=\"proxy_not_supported\">استفاده از پراکسی وب (نیاز به Android 4.4+ دارد)</string>\n  <string name=\"proxy_test_btn\">تست</string>\n  <string name=\"proxy_settings_title\">تنظیمات پراکسی</string>\n  <string name=\"proxy_test_cancelled\">تست#%d: لغو شد.</string>\n  <string name=\"proxy_type_invalid\">نوع پراکسی تنظیم نشده است.</string>\n  <string name=\"proxy_host_invalid\">میزبان پراکسی تنظیم نشده است.</string>\n  <string name=\"proxy_port_invalid\">پورت پراکسی نامعتبر است، باید بزرگتر از ۰ باشد.</string>\n  <string name=\"proxy_credentials_invalid\">خطا در نام کاربری یا رمز عبور.</string>\n  <string name=\"proxy_test_aborted\">تست لغو شد، لطفا ابتدا تنظیمات پراکسی را اصلاح کنید.</string>\n  <string name=\"proxy_application_aborted\">لطفا ابتدا تنظیمات پراکسی را اصلاح کنید.</string>\n  <string name=\"proxy_test_start\">تست#%d:%s...</string>\n  <string name=\"proxy_test_error\">خطا#%d:%s</string>\n  <string name=\"proxy_test_status\">وضعیت#%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">آدرس فایل پیکربندی (*.ovpn)</string>\n  <string name=\"enable_openvpn\">استفاده از OpenVPN</string>\n  <string name=\"openvpn_settings_title\">تنظیمات OpenVPN</string>\n  <string name=\"openvpn_application_aborted\">لطفا ابتدا تنظیمات OpenVPN را اصلاح کنید.</string>\n  <string name=\"openvpn_test_aborted\">تست لغو شد، لطفا ابتدا تنظیمات OpenVPN را اصلاح کنید.</string>\n  <string name=\"openvpn_address_invalid\">آدرس پیکربندی OpenVPN تنظیم نشده است.</string>\n  <string name=\"internet_censorship\">سانسور اینترنت</string>\n  <string name=\"rename_section\">تغییر نام این بخش</string>\n  <string name=\"simple_edit_value_hint\">مقدار جدید</string>\n  <string name=\"seek_interval\">فاصله جستجو</string>\n  <string name=\"seek_interval_sec\">%s ثانیه</string>\n  <string name=\"subtitle_system\">سبک سیستم (تنظیمات اندروید &gt; دسترسی‌پذیری)</string>\n  <string name=\"subtitle_scale\">مقیاس زیرنویس</string>\n  <string name=\"audio_sync_fix_desc\">روش جایگزین برای همگام‌سازی صدا/ویدیو. قدیمی است، اما در برخی موارد ممکن است کمک کند.</string>\n  <string name=\"audio_sync_fix\">رفع همگام‌سازی صدا</string>\n  <string name=\"ambilight_ratio_fix_desc\">رفع نور پس‌زمینه از دست رفته. نسبت تصویر نادرست را اصلاح می‌کند. صحنه‌های خالی را اصلاح می‌کند. بر عملکرد تأثیر می‌گذارد!</string>\n  <string name=\"ambilight_ratio_fix\">رفع Ambilight / نسبت تصویر / صحنه‌های خالی</string>\n  <string name=\"force_legacy_codecs_desc\">عملکرد را به شدت در دستگاه‌های کم‌قدرت بهبود می‌بخشد. حداکثر رزولوشن ۷۲۰ پیکسل.</string>\n  <string name=\"force_legacy_codecs\">اجبار به استفاده از کدک‌های قدیمی (۷۲۰ پیکسل)</string>\n  <string name=\"live_stream_fix_desc\">عملکرد پخش زنده را در دستگاه‌های کم‌قدرت به شدت بهبود می‌بخشد. حداکثر رزولوشن ۱۰۸۰ پیکسل.</string>\n  <string name=\"live_stream_fix\">رفع پخش زنده (۱۰۸۰ پیکسل)</string>\n  <string name=\"playback_notifications_fix_desc\">اعلان‌های تغییر مسیر را پنهان می‌کند. می‌تواند در نرم‌افزارهای مبتنی بر AOSP مفید باشد.</string>\n  <string name=\"playback_notifications_fix\">غیرفعال کردن اعلان‌های پخش</string>\n  <string name=\"amlogic_fix_desc\">رفع افت فریم در دستگاه‌های مبتنی بر Amlogic.</string>\n  <string name=\"amlogic_fix\">رفع Amlogic 1080p\\@60FPS</string>\n  <string name=\"tunneled_video_playback_desc\">توجه: مشکلات احتمالی با توقف! پخش ویدیو از طریق تونل‌ها با مزایایی مانند همگام‌سازی بهتر صدا/ویدیو (AV sync) و پخش روان‌تر. نیاز به Android 5+ دارد.</string>\n  <string name=\"tunneled_video_playback\">پخش ویدیو از طریق تونل‌ها (اندروید ۵+)</string>\n  <string name=\"master_volume\">صدا اصلی</string>\n  <string name=\"volume_limit\">محدودیت صدا</string>\n  <string name=\"play_video\">پخش</string>\n  <string name=\"remember_position_of_short_videos\">به خاطر سپردن موقعیت ویدیوهای کوتاه (کمتر از ۱۰ دقیقه)</string>\n  <string name=\"player_show_tooltips\">نمایش راهنمای دکمه‌ها</string>\n  <string name=\"action_like_unset\">پسندیدن تنظیم نشده</string>\n  <string name=\"action_dislike_unset\">نپسندیدن تنظیم نشده</string>\n  <string name=\"various_buttons\">دکمه‌های بالای صفحه اصلی</string>\n  <string name=\"not_compatible_with\">ناسازگاری گزینه انتخاب شده با</string>\n  <string name=\"subtitle_position\">موقعیت زیرنویس</string>\n  <string name=\"pressing_home\">با فشار دادن HOME</string>\n  <string name=\"pressing_home_back\">با فشار دادن HOME یا BACK</string>\n  <string name=\"player_number_key_seek\">فاصله جستجو با دکمه‌های عددی</string>\n  <string name=\"save_remove_playlist\">افزودن/حذف فهرست پخش از بخش فهرست‌های پخش</string>\n  <string name=\"remove_playlist\">حذف فهرست پخش از بخش فهرست‌های پخش به طور دائم</string>\n  <string name=\"removed_from_playlists\">از بخش فهرست‌های پخش حذف شد</string>\n  <string name=\"saved_to_playlists\">به بخش فهرست‌های پخش اضافه شد</string>\n  <string name=\"create_playlist\">ایجاد فهرست پخش</string>\n  <string name=\"add_video_to_new_playlist\">افزودن به فهرست پخش جدید</string>\n  <string name=\"cant_delete_empty_playlist\">امکان حذف فهرست پخش خالی وجود ندارد</string>\n  <string name=\"playlist\">فهرست پخش</string>\n  <string name=\"rename_playlist\">تغییر نام فهرست پخش</string>\n  <string name=\"cant_rename_empty_playlist\">امکان تغییر نام فهرست پخش خالی وجود ندارد</string>\n  <string name=\"cant_rename_foreign_playlist\">امکان تغییر نام فهرست پخش دیگران وجود ندارد</string>\n  <string name=\"cant_save_playlist\">امکان افزودن این فهرست پخش وجود ندارد</string>\n  <string name=\"enter_value\">وارد کردن هر مقدار (غیر خالی)</string>\n  <string name=\"dialog_add_remove_from\">افزودن/حذف از %s</string>\n  <string name=\"add_to_playback_queue\">افزودن به فهرست انتظار پخش</string>\n  <string name=\"remove_from_playback_queue\">حذف از فهرست انتظار پخش</string>\n  <string name=\"lb_playback_controls_skip_next\">\\\"رفتن به ویدیوی بعدی\\\"</string>\n  <string name=\"lb_playback_controls_skip_previous\">\\\"رفتن به ویدیوی قبلی\\\"</string>\n  <string name=\"alt_presets_behavior\">رفتار جایگزین پیش‌تنظیمات ویدیو (محدودیت پهنای باند)</string>\n  <string name=\"alt_presets_behavior_desc\">برنامه سعی می‌کند پهنای باندی را حفظ کند که با پیش‌تنظیم انتخاب شده مطابقت دارد، نه تطابق رزولوشن، نرخ فریم و کدک.</string>\n  <string name=\"sleep_timer\">فعال‌سازی تایمر خواب (یک ساعت)</string>\n  <string name=\"sleep_timer_desc\">اگر کاربر از کنترل از راه دور استفاده نکند، پس از یک ساعت پخش متوقف می‌شود</string>\n  <string name=\"disable_vsync\">غیرفعال کردن snap to vsync</string>\n  <string name=\"disable_vsync_desc\">این گزینه همگام‌سازی فریم‌ها با سیگنال عمودی صفحه را غیرفعال می‌کند. این ممکن است عملکرد را در دستگاه‌های کم‌قدرت با آزاد کردن منابع CPU بهبود بخشد.</string>\n  <string name=\"skip_codec_profile_check\">رد بررسی پروفایل کدک</string>\n  <string name=\"skip_codec_profile_check_desc\">هنگام شروع پخش ویدیو، از بررسی پشتیبانی کدک صرف‌نظر می‌کند. می‌تواند در نرم‌افزارهای دارای اشکال مفید باشد.</string>\n  <string name=\"force_sw_codec\">اجبار به دیکد نرم‌افزاری ویدیو</string>\n  <string name=\"force_sw_codec_desc\">تقریبا هر ویدیویی پخش می‌شود، اما عملکرد بسیار ضعیف است.</string>\n  <string name=\"playlist_order\">مرتب‌سازی فهرست پخش</string>\n  <string name=\"playlist_order_added_date_newer_first\">تاریخ افزودن (جدیدترین اول)</string>\n  <string name=\"playlist_order_added_date_older_first\">تاریخ افزودن (قدیمی‌ترین اول)</string>\n  <string name=\"playlist_order_published_date_newer_first\">تاریخ انتشار (جدیدترین اول)</string>\n  <string name=\"playlist_order_published_date_older_first\">تاریخ انتشار (قدیمی‌ترین اول)</string>\n  <string name=\"playlist_order_popularity\">محبوبیت</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">امکان انجام این کار برای فهرست پخش دیگران وجود ندارد</string>\n  <string name=\"owned_playlist_warning\">خطا: این عمل فقط برای فهرست‌های پخش متعلق به شما قابل اعمال است</string>\n  <string name=\"content_block_alt_server\">استفاده از سرور جایگزین</string>\n  <string name=\"content_block_alt_server_desc\">این گزینه را فعال کنید اگر SponsorBlock به دلایل مختلف کار نمی‌کند.</string>\n  <string name=\"unset_stream_reminder\">لغو تنظیم یادآوری پخش زنده</string>\n  <string name=\"set_stream_reminder\">تنظیم یادآوری پخش زنده</string>\n  <string name=\"playback_starts_shortly\">پخش به طور خودکار شروع می‌شود وقتی پخش زنده آماده است</string>\n  <string name=\"starting_stream\">در حال شروع پخش زنده...</string>\n  <string name=\"action_playlist_remove\">حذف از فهرست پخش</string>\n  <string name=\"signin_view_title\">در حال بارگیری کد کاربر...</string>\n  <string name=\"signin_view_description\">برای ورود، این کد را در صفحه%s وارد کنید\\nتوجه: فقط با مرورگر Firefox یا Chrome کار می‌کند.</string>\n  <string name=\"signin_view_action_text\">انجام شد</string>\n  <string name=\"require_checked\">\\\"%s نیاز دارد</string>\n  <string name=\"msg_press_again_to_exit\">برای خروج دوباره فشار دهید</string>\n  <string name=\"player_remaining_time\">باقی‌مانده:\\\"%s</string>\n  <string name=\"player_ending_time\">پایان در \\\"%s</string>\n  <string name=\"badge_new_content\">محتوا جدید</string>\n  <string name=\"badge_live\">پخش زنده</string>\n  <string name=\"add_device_view_description\">برای اتصال دستگاه، این کد را در برنامه YouTube در تلفن خود در بخش تنظیمات/تماشا در تلویزیون وارد کنید\\nتوجه: این فقط با صفحه کلید Gboard کار می‌کند!</string>\n  <string name=\"playback_controls_repeat_pause\">توقف تکرار</string>\n  <string name=\"playback_controls_repeat_list\">تکرار فهرست </string>\n  <string name=\"action_subscribe_off\">اشتراک غیرفعال</string>\n  <string name=\"action_subscribe_on\">اشتراک فعال</string>\n  <string name=\"skip_24_rate\">رد کردن فرمت‌های ۲۴ فریم در ثانیه (رفع حالت سینمای واقعی؟)</string>\n  <string name=\"player_disable_suggestions\">غیرفعال کردن پیشنهادات</string>\n  <string name=\"sources\">منابع</string>\n  <string name=\"releases\">انتشارات</string>\n  <string name=\"feedback\">بازخورد</string>\n  <string name=\"prefer_avc_over_vp9\">انتخاب کدک: ترجیح avc بر vp9</string>\n  <string name=\"open_chat\">نظرات/چت زنده</string>\n  <string name=\"place_chat_left\">قرار دادن چت زنده در سمت چپ</string>\n  <string name=\"use_alt_speech_recognizer\">ابزار تشخیص گفتار جایگزین</string>\n  <string name=\"time_format\">فرمت زمان</string>\n  <string name=\"time_format_24\">۲۴ ساعته</string>\n  <string name=\"time_format_12\">۱۲ ساعته (ص/م)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">به خوبی کار نمی‌کند. فقط در صورت داشتن مشکل با ابزار تشخیص پیش‌فرض استفاده کنید.</string>\n  <string name=\"speech_recognizer\">ابزار تشخیص گفتار</string>\n  <string name=\"speech_recognizer_system\">سیستم (ترجیحی)</string>\n  <string name=\"speech_recognizer_external_1\">خارجی ۱ (به خوبی کار نمی‌کند، برای کار نیاز به غیرفعال کردن دسترسی به میکروفون دارد)</string>\n  <string name=\"speech_recognizer_external_2\">خارجی ۲ (به خوبی کار نمی‌کند)</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"real_channel_icon\">نمایش لوگوی کانال در پخش کننده</string>\n  <string name=\"subtitle_yellow_semi_transparent\">زرد روی پس‌زمینه نیمه شفاف</string>\n  <string name=\"subtitle_yellow_black\">زرد روی پس‌زمینه سیاه</string>\n  <string name=\"player_pixel_ratio\">نسبت پیکسل</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">خاکستری تیره (معکوس)</string>\n  <string name=\"disable_mic_permission\">لطفا دسترسی به میکروفون را برای برنامه غیرفعال کنید تا این ابزار تشخیص به درستی کار کند.</string>\n  <string name=\"repeat_mode_shuffle\">تصادفی کردن هر فهرست پخش</string>\n  <string name=\"chat_left\">چپ</string>\n  <string name=\"chat_right\">راست</string>\n  <string name=\"card_real_thumbnails\">جایگزینی تصاویر کوچک با فریم از ویدیو</string>\n  <string name=\"card_content\">محل گرفتن تصاویر کوچک برای کارت</string>\n  <string name=\"thumb_quality_default\">پیش‌فرض</string>\n  <string name=\"thumb_quality_start\">شروع ویدیو</string>\n  <string name=\"thumb_quality_middle\">وسط ویدیو</string>\n  <string name=\"thumb_quality_end\">پایان ویدیو</string>\n  <string name=\"video_buffer_size_none\">هیچ‌کدام</string>\n  <string name=\"content_block_status\">بررسی وضعیت SponsorBlock</string>\n  <string name=\"player_show_quality_info_bitrate\">افزودن نرخ بیت (Bitrate) به اطلاعات کیفیت</string>\n  <string name=\"player_speed_button_old_behavior\">بازگشت به رفتار قدیمی دکمه سرعت</string>\n  <string name=\"protect_settings_with_password\">محافظت از همه تنظیمات با رمز عبور</string>\n  <string name=\"enter_settings_password\">رمز عبور تنظیمات را وارد کنید</string>\n  <string name=\"child_mode\">حالت کودک</string>\n  <string name=\"child_mode_desc\">در این حالت، کاربر نمی‌تواند از جستجو استفاده کند یا هر محتوای پیشنهادی را مشاهده کند. تنظیمات تحت رمز عبور خواهند بود.</string>\n  <string name=\"lost_setting_warning\">تنظیمات برنامه تغییر خواهد کرد. لطفا از تنظیمات نسخه پشتیبان تهیه کنید.</string>\n  <string name=\"pause_history\">توقف تاریخچه تماشا</string>\n  <string name=\"clear_history\">پاک کردن تاریخچه تماشا</string>\n  <string name=\"resume_history\">از سرگیری تاریخچه تماشا</string>\n  <string name=\"chapters\">فصل‌ها</string>\n  <string name=\"auto_frame_rate_modes\">حالت‌های پشتیبانی شده</string>\n  <string name=\"loading\">در حال بارگیری…</string>\n  <string name=\"video_rotate\">چرخش</string>\n  <string name=\"hide_streams\">پنهان کردن پخش زنده از اشتراک‌ها</string>\n  <string name=\"card_multiline_subtitle\">زیرنویس چند خطی</string>\n  <string name=\"player_button_long_click\">کلیک کوتاه برای عمل سریع و کلیک طولانی برای باز کردن تنظیمات در پنجره بازشو</string>\n  <string name=\"trending_searches\">جستجوهای محبوب</string>\n  <string name=\"video_duration_any\">هر مدت</string>\n  <string name=\"video_duration\">مدت</string>\n  <string name=\"video_duration_under_4\">کمتر از ۴ دقیقه</string>\n  <string name=\"video_duration_between_4_20\">۴-۲۰ دقیقه</string>\n  <string name=\"video_duration_over_20\">بیشتر از ۲۰ دقیقه</string>\n  <string name=\"content_type\">نوع</string>\n  <string name=\"content_type_any\">همه</string>\n  <string name=\"content_type_video\">ویدیو</string>\n  <string name=\"content_type_channel\">کانال</string>\n  <string name=\"content_type_playlist\">فهرست پخش</string>\n  <string name=\"content_type_movie\">فیلم</string>\n  <string name=\"video_features\">ویژگی‌ها</string>\n  <string name=\"video_feature_any\">همه</string>\n  <string name=\"video_feature_live\">پخش زنده</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">مرتب‌سازی بر اساس</string>\n  <string name=\"sort_by_relevance\">مرتبط‌ترین</string>\n  <string name=\"sort_by_views\">تعداد بازدیدها</string>\n  <string name=\"sort_by_date\">تاریخ آپلود</string>\n  <string name=\"sort_by_rating\">امتیاز</string>\n  <string name=\"clear_search_history\">پاک کردن تاریخچه جستجو</string>\n  <string name=\"remove_from_subscriptions\">پنهان کردن</string>\n  <string name=\"player_long_speed_list\">فهرست سرعت طولانی</string>\n  <string name=\"disable_ok_long_press_desc\">مخصوص کنترل‌هایی که دارای اشکال هستند و دکمه OK به درستی کار نمی‌کند</string>\n  <string name=\"pressing_back\">با فشار دادن \\\"BACK\\\"</string>\n  <string name=\"alt_app_icon\">آیکون جایگزین برنامه (نیاز به راه‌اندازی مجدد دارد)</string>\n  <string name=\"cronet_desc\">Cronet یک پشته شبکه Chromium است. معمولا سریع‌تر از دیگران است. می‌تواند در مشکلات کش مفید باشد.</string>\n  <string name=\"network_stack\">ترجیح %s پشته شبکه</string>\n  <string name=\"unlock_all_formats\">باز کردن همه فرمت‌های ویدیو</string>\n  <string name=\"unlock_all_formats_desc\">در برخی دستگاه‌ها، نرم‌افزار به اشتباه برخی فرمت‌ها را به عنوان غیرقابل پشتیبانی گزارش می‌دهد، حتی اگر باشند.\\n(به عنوان مثال، تلویزیون‌های هوشمند ۱۰۸۰p تمایل دارند که 4K را به عنوان غیرقابل پشتیبانی گزارش دهند).</string>\n  <string name=\"okhttp_desc\">این پشته شبکه کندترین است اما ثبات خوبی دارد.</string>\n  <string name=\"select_channel_section\">باز کردن بخش مربوطه هنگام راه‌اندازی برنامه از کانال‌های ATV</string>\n  <string name=\"live_stream_fix_4k_desc\">هشدار، این اصلاح بازپخش پخش زنده را غیرفعال می‌کند. عملکرد پخش زنده را به شدت بهبود می‌بخشد. حداکثر رزولوشن 4K است.</string>\n  <string name=\"live_stream_fix_4k\">رفع پخش زنده (4K)</string>\n  <string name=\"enable_voice_search_desc\">برنامه رسمی با چیزی به نام \\\"پل\\\" جایگزین می‌شود. پل به انتقال درخواست‌های جستجوی جهانی به برنامه ما کمک می‌کند.</string>\n  <string name=\"not_recommend_channel\">پیشنهاد نکردن این کانال</string>\n  <string name=\"you_wont_see_this_channel\">این کانال را در پیشنهادات نخواهید دید</string>\n  <string name=\"enable_master_password\">فعال‌سازی رمز عبور اصلی</string>\n  <string name=\"enter_master_password\">رمز عبور اصلی را وارد کنید</string>\n  <string name=\"disable_stream_buffer\">غیرفعال کردن بافر پخش زنده</string>\n  <string name=\"disable_stream_buffer_desc\">رفع مواردی که پخش خیلی عقب می‌ماند. توجه: ممکن است پخش شروع به عقب ماندن کند.</string>\n  <string name=\"sony_frame_drop_fix\">رفع افت فریم شماره ۱</string>\n  <string name=\"sony_frame_drop_fix_desc\">رفع مشکل تاخیر در تلویزیون‌های سونی و برخی دستگاه‌های دیگر. توجه: ممکن است مشکلات همگام‌سازی صدا وجود داشته باشد.</string>\n  <string name=\"audio_language\">زبان صدا</string>\n  <string name=\"old_home_look\">ظاهر قدیمی بخش صفحه اصلی</string>\n  <string name=\"hide_shorts_everywhere\">پنهان کردن کوته ویدیوها در همه جا</string>\n  <string name=\"update_found\">بروزرسانی</string>\n  <string name=\"volume_boost_warning\">تجاوز از حد مجاز ممکن است به بلندگوها آسیب برساند</string>\n  <string name=\"play_video_incognito\">پخش در حالت ناشناس</string>\n  <string name=\"header_kids_home\">کودکان</string>\n  <string name=\"player_section_playlist\">استفاده از محتوای بخش فعلی به عنوان فهرست پخش</string>\n  <string name=\"header_trending\">محتواهای محبوب</string>\n  <string name=\"screensaver\">محافظ صفحه</string>\n  <string name=\"player_screen_off_timeout\">زمان خاموش شدن صفحه</string>\n  <string name=\"old_update_notifications\">ظاهر قدیمی اعلان‌های بروزرسانی</string>\n  <string name=\"mark_as_watched\">علامت‌گذاری به عنوان تماشا شده</string>\n  <string name=\"player_likes_count\">نمایش تعداد پسندیدن/نپسندیدن</string>\n  <string name=\"sorting_alphabetically2\">الفبایی (پیش‌نمایش‌های سریع و متحرک)</string>\n  <string name=\"sorting_default\">پیش‌فرض (پیش‌نمایش‌های سریع و متحرک)</string>\n  <string name=\"player_ui_animations\">فعال‌سازی انیمیشن‌های پنل کنترل</string>\n  <string name=\"content_block_exclude_channel\">استثنا کردن این کانال از SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">توقف استثنا کردن این کانال از SponsorBlock</string>\n  <string name=\"player_chapter_notification\">پیشرفت سریع از طریق فصل‌ها با استفاده از اعلان بازشو</string>\n  <string name=\"subtitle_remember\">به خاطر سپردن زیرنویس‌های فعال برای هر کانال</string>\n  <string name=\"amazon_frame_drop_fix\">رفع افت فریم شماره ۲</string>\n  <string name=\"amazon_frame_drop_fix_desc\">مخصوص دستگاه‌های Amazon Stick. ممکن است در دستگاه‌های دیگر نیز کار کند.</string>\n  <string name=\"share_qr_link\">لینک اشتراک‌گذاری (کد QR)</string>\n  <string name=\"msg_player_error_video_renderer\">فرمت ویدیوی انتخاب شده پشتیبانی نمی‌شود.\\nسعی کنید فرمت دیگری را با فشار دادن دکمه HQ در پخش کننده انتخاب کنید.\\nاگر کمک نکرد، سعی کنید دستگاه را مجددا راه‌اندازی کنید.</string>\n  <string name=\"msg_player_error_audio_renderer\">فرمت صوتی انتخاب شده پشتیبانی نمی‌شود.\\nسعی کنید فرمت دیگری را با فشار دادن دکمه HQ در پخش کننده انتخاب کنید.\\nاگر کمک نکرد، سعی کنید دستگاه را مجددا راه‌اندازی کنید.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">فرمت زیرنویس انتخاب شده پشتیبانی نمی‌شود.\\nسعی کنید فرمت دیگری را با فشار طولانی روی دکمه CC در پخش کننده انتخاب کنید.\\nاگر کمک نکرد، سعی کنید دستگاه را مجددا راه‌اندازی کنید.</string>\n  <string name=\"msg_player_error_video_source\">آدرس URL ویدیو کار نمی‌کند یا زمان دستگاه نادرست است.\\nمعمولا این یک خطای برنامه است.</string>\n  <string name=\"msg_player_error_audio_source\">آدرس URL صدا کار نمی‌کند یا زمان دستگاه نادرست است.\\nمعمولا این یک خطای برنامه است.</string>\n  <string name=\"msg_player_error_subtitle_source\">آدرس URL زیرنویس کار نمی‌کند یا زمان دستگاه نادرست است.\\nمعمولا این یک خطای برنامه است.</string>\n  <string name=\"player_volume\">سطح صدا</string>\n  <string name=\"open_comments\">باز کردن نظرات</string>\n  <string name=\"disable_history\">غیرفعال کردن تاریخچه</string>\n  <string name=\"enable_history\">فعال‌سازی تاریخچه</string>\n  <string name=\"player_network_stack\">موتور شبکه</string>\n  <string name=\"dialog_notification\">اعلان بروزرسانی‌ها از طریق کادر بازشو</string>\n  <string name=\"default_stack_desc\">موتور شبکه داخلی. این موتور ممکن است در برخی شرایط ثبات بهتری داشته باشد.</string>\n  <string name=\"item_postion\">موقعیت</string>\n  <string name=\"screensaver_timout\">زمان محافظ صفحه</string>\n  <string name=\"screensaver_dimming\">مقدار تاریکی محافظ صفحه</string>\n  <string name=\"player_screen_off_dimming\">مقدار تاریکی خاموش شدن صفحه</string>\n  <string name=\"player_ui_on_next\">نمایش رابط کاربری پخش کننده هنگام تغییر به ویدیوی بعدی</string>\n  <string name=\"autogenerated\">به طور خودکار تولید شده</string>\n  <string name=\"player_auto_volume\">تنظیم خودکار سطح صدا</string>\n  <string name=\"header_shorts\">کوته ویدیوها</string>\n  <string name=\"player_global_focus\">پیمایش روان بین ردیف‌های دکمه‌های پخش کننده</string>\n  <string name=\"auto_history\">خودکار (استفاده از تنظیمات حساب)</string>\n  <string name=\"remember_position_subscriptions\">به خاطر سپردن آخرین موقعیت مشاهده شده در اشتراک‌ها</string>\n  <string name=\"msg_player_unknown_error\">خطای ناشناخته</string>\n  <string name=\"header_notifications\">اعلان‌ها</string>\n  <string name=\"disable_search_history\">غیرفعال کردن تاریخچه جستجو</string>\n  <string name=\"unlock_high_bitrate_formats\">باز کردن فرمت‌های نرخ بیت بالا ۱۰۸۰p vp9</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">باز کردن فرمت‌های mp4a با نرخ بیت بالا</string>\n  <string name=\"color_scheme_blue\">آبی</string>\n  <string name=\"color_scheme_dark_blue\">آبی تیره</string>\n  <string name=\"player_speed_per_channel\">برای هر کانال</string>\n  <string name=\"disable_popular_searches\">غیرفعال کردن جستجوهای محبوب</string>\n  <string name=\"multi_profiles\">استفاده از تنظیمات جداگانه برای هر حساب</string>\n  <string name=\"protect_account_with_password\">محافظت از این حساب با رمز عبور</string>\n  <string name=\"enter_account_password\">رمز عبور حساب را وارد کنید</string>\n  <string name=\"show_connect_messages\">نمایش پیام‌های اتصال</string>\n  <string name=\"prefer_avc_over_vp9_desc\">هشدار: حداکثر ۱۰۸۰p</string>\n  <string name=\"not_supported_by_device\">دستگاه از این ویژگی پشتیبانی نمی‌کند</string>\n  <string name=\"video_buffer_size_lowest\">کمترین</string>\n  <string name=\"video_buffer_size_highest\">بالاترین</string>\n  <string name=\"player_exit_shortcut\">خروج از پخش کننده</string>\n  <string name=\"app_corner_clock\">صفحه اصلی: ساعت در گوشه بالا سمت راست</string>\n  <string name=\"player_corner_clock\">پخش کننده: ساعت در گوشه بالا سمت راست</string>\n  <string name=\"player_corner_ending_time\">پخش کننده: زمان پایان در گوشه بالا سمت راست</string>\n  <string name=\"pin_playlist\">افزودن فهرست پخش به نوار کناری</string>\n  <string name=\"pin_channel\">افزودن کانال به نوار کناری</string>\n  <string name=\"unpin_group_from_sidebar\">حذف گروه اشتراک‌ها</string>\n  <string name=\"hide_shorts_channel\">پنهان کردن کوته ویدیوها از کانال</string>\n  <string name=\"news_row_name\">اخبار</string>\n  <string name=\"action_sound_off\">خاموش کردن صدا</string>\n  <string name=\"hide_upcoming_channel\">پنهان کردن آینده از کانال</string>\n  <string name=\"hide_upcoming_home\">پنهان کردن آینده از صفحه اصلی</string>\n  <string name=\"hide_shorts_from_trending\">پنهان کردن کوته ویدیوها از محبوب‌ها</string>\n  <string name=\"remember_position_of_live_videos\">به خاطر سپردن موقعیت پخش زنده</string>\n  <string name=\"save_playlist\">افزودن فهرست پخش به بخش فهرست‌های پخش</string>\n  <string name=\"skip_shorts\">رد کردن کوته ویدیوها</string>\n  <string name=\"suggestions\">پیشنهادات</string>\n  <string name=\"place_comments_left\">قرار دادن نظرات در سمت چپ</string>\n  <string name=\"speech_engine\">موتور جستجوی صوتی</string>\n  <string name=\"dearrow_status\">بررسی وضعیت سرور DeArrow</string>\n  <string name=\"player_extra_long_speed_list\">فهرست سرعت بسیار طولانی</string>\n  <string name=\"old_channel_look\">ظاهر قدیمی صفحه کانال</string>\n  <string name=\"remember_position_pinned\">به خاطر سپردن آخرین موقعیت مشاهده شده در فهرست‌های پخش ثابت شده</string>\n  <string name=\"unknown_source_error\">خطای منبع ناشناخته</string>\n  <string name=\"unknown_renderer_error\">خطای ناشناخته در رندرر</string>\n  <string name=\"play_next\">پخش بعدی</string>\n  <string name=\"hide_watched_from_subscriptions\">پنهان کردن ویدیوهای تماشا شده از اشتراک‌ها</string>\n  <string name=\"hide_watched_from_notifications\">پنهان کردن ویدیوهای تماشا شده از اعلان‌ها</string>\n  <string name=\"hide_unwanted_content\">پنهان کردن محتوای ناخواسته</string>\n  <string name=\"hide_watched_from_home\">پنهان کردن ویدیوهای تماشا شده از صفحه اصلی</string>\n  <string name=\"hide_watched_from_watch_later\">پنهان کردن ویدیوهای تماشا شده از فهرست \\\"تماشای بعدی\\\"</string>\n  <string name=\"remote_control_permission\">کنترل از راه دور در پس‌زمینه نیاز به مجوز Overlay دارد</string>\n  <string name=\"login_from_browser\">ورود از مرورگر</string>\n  <string name=\"disable_remote_history\">غیرفعال کردن تاریخچه هنگام استفاده از کنترل از راه دور</string>\n  <string name=\"keyboard_fix\">جلوگیری از باز شدن صفحه کلید با دکمه OK (G20s و دیگران)</string>\n  <string name=\"nothing_found\">چیزی یافت نشد</string>\n  <string name=\"auto_frame_rate_desc\">این گزینه لرزش را در صحنه‌هایی که دوربین به سرعت حرکت می‌کند، مانند پخش ورزشی، حذف می‌کند.</string>\n  <string name=\"channel_filter_hint\">فیلتر کانال‌ها</string>\n  <string name=\"player_loop_shorts\">تکرار کوته ویدیوها</string>\n  <string name=\"player_quick_shorts_skip\">رد کردن کوته ویدیوها با دکمه‌های چپ/راست</string>\n  <string name=\"player_quick_skip_videos\">رد کردن ویدیوهای معمولی با دکمه‌های چپ/راست</string>\n  <string name=\"channels_filter\">نمایش فیلد فیلتر کانال‌ها در بخش کانال‌ها</string>\n  <string name=\"channel_search_bar\">نوار جستجو در صفحه کانال</string>\n  <string name=\"header_sports\">ورزش</string>\n  <string name=\"keep_finished_activities\">نگه داشتن فعالیت‌های تکمیل شده</string>\n  <string name=\"disable_channels_service\">غیرفعال کردن سرویس کانال‌ها</string>\n  <string name=\"replace_titles\">جایگزینی عناوین</string>\n  <string name=\"enable\">فعال‌سازی</string>\n  <string name=\"more_info\">اطلاعات بیشتر</string>\n  <string name=\"about_sponsorblock\">درباره مسدودکننده اسپانسرها</string>\n  <string name=\"about_dearrow\">درباره DeArrow</string>\n  <string name=\"replace_thumbnails\">جایگزینی تصاویر کوچک</string>\n  <string name=\"crowdsourced_thumbnails\">تصاویر کوچک جمع‌آوری شده از کاربران</string>\n  <string name=\"crowdsoursed_titles\">عناوین جمع‌آوری شده از کاربران</string>\n  <string name=\"dearrow_not_submitted_thumbs\">منبع تصویر کوچک (اگر ارسال DeArrow وجود نداشته باشد)</string>\n  <string name=\"pitch_effect\">اثر Pitch</string>\n  <string name=\"fullscreen_mode\">حالت تمام صفحه (بدون نوارهای سیستم)</string>\n  <string name=\"player_only_mode\">نمایش فقط پخش کننده اگر ویدیو خارج از برنامه باز شود</string>\n  <string name=\"pinned_channel_rows\">نمایش کانال ثابت شده به عنوان ردیف‌ها</string>\n  <string name=\"prefer_ipv4\">ترجیح DNS IPv4</string>\n  <string name=\"prefer_ipv4_desc\">ممکن است در مواردی که برنامه اصلا کار نمی‌کند، کمک کند.\\nتوجه: ممکن است باعث خرابی و خطا شود (به ویژه در دستگاه‌های Android 8 یا Dune HD).</string>\n  <string name=\"long_press_for_settings\">فشار طولانی برای تنظیمات</string>\n  <string name=\"long_press_for_options\">فشار طولانی برای گزینه‌ها</string>\n  <string name=\"device_specific_backup\">پشتیبان‌گیری فقط برای این دستگاه</string>\n  <string name=\"local_backup\">پشتیبان‌گیری محلی</string>\n  <string name=\"auto_backup\">پشتیبان‌گیری خودکار (یک بار در روز)</string>\n  <string name=\"repeat_mode_reverse_list\">پخش ویدیوها در فهرست پخش یا کانال به ترتیب معکوس</string>\n  <string name=\"calm_msg\">ما در حال رفع مشکل هستیم. هر از گاهی بروزرسانی‌ها را بررسی کنید.</string>\n  <string name=\"without_picture\">بدون تصویر</string>\n  <string name=\"video_disabled\">ویدیو غیرفعال شد</string>\n  <string name=\"applying_fix\">پخش کننده را نبندید. در حال اعمال رفع مشکل...</string>\n  <string name=\"player_global_focus_desc\">این ویژگی بر دکمه پخش کننده که هنگام پیمایش بین ردیف‌های دکمه‌ها فوکوس می‌گیرد، تأثیر می‌گذارد.</string>\n  <string name=\"disable_network_error_fixing\">غیرفعال کردن رفع خودکار خطاهای شبکه</string>\n  <string name=\"disable_network_error_fixing_desc\">ممکن است نیاز باشد این گزینه را فعال کنید اگر از VPN استفاده می‌کنید.</string>\n  <string name=\"recommended\">توصیه شده</string>\n  <string name=\"add_to_subscriptions_group\">افزودن/حذف از گروه اشتراک</string>\n  <string name=\"new_subscriptions_group\">گروه جدید</string>\n  <string name=\"rename_group\">تغییر نام گروه اشتراک</string>\n  <string name=\"screen_dimming_amount\">مقدار تاریکی صفحه</string>\n  <string name=\"screen_dimming_timeout\">زمان تاریکی صفحه</string>\n  <string name=\"playlists_rows\">نمایش بخش فهرست‌های پخش به عنوان ردیف‌ها</string>\n  <string name=\"import_subscriptions_group\">وارد کردن</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">غیرفعال کردن زیرنویس</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">فعال کردن زیرنویس</string>\n  <string name=\"my_videos\">ویدیوهای من</string>\n  <string name=\"hide_mixes\">پنهان کردن میکس‌ها</string>\n  <string name=\"hide_shorts_from_search\">پنهان کردن کوته ویدیوها از نتایج جستجو</string>\n  <string name=\"context_menu_sorting\">مرتب‌سازی منوی زمینه</string>\n  <string name=\"player_audio_focus\">تمرکز صوتی (مکث در صورت شناسایی سایر پخش‌کننده‌ها)</string>\n  <string name=\"paid_content_notification\">اعلان محتوای پولی</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-fi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Koti</string>\n  <string name=\"title_search\">Haku</string>\n  <string name=\"header_subscriptions\">Tilaukset</string>\n  <string name=\"header_history\">Historia</string>\n  <string name=\"header_music\">Musiikki</string>\n  <string name=\"header_news\">Uutiset</string>\n  <string name=\"header_gaming\">Pelit</string>\n  <string name=\"header_playlists\">Soittolista</string>\n  <string name=\"header_settings\">Asetukset</string>\n  <string name=\"header_channels\">Kanavat</string>\n  <string name=\"subscriptions_signin_title\">Katso uusimmat videot suosikkikanaviltasi</string>\n  <string name=\"subscriptions_signin_subtitle\">Kirjaudu sisään nähdäksesi tilauksesi</string>\n  <string name=\"subscriptions_signin_button_text\">Kirjaudu sisään</string>\n  <string name=\"library_signin_to_show_more\">Katso videoita joista pidit, tallensit tai tilasit</string>\n  <string name=\"library_signin_subtitle\">Kirjaudu sisään nähdäksesi tilauksesi</string>\n  <string name=\"action_signin\">Kirjaudu sisään</string>\n  <string name=\"title_video_formats\">Videoformaatti</string>\n  <string name=\"title_audio_formats\">Audioformaatti</string>\n  <string name=\"subtitle_category_title\">Tekstitykset</string>\n  <string name=\"video_max_quality\">Auto (paras kuvanlaatu)</string>\n  <string name=\"audio_max_quality\">Auto (paras äänenlaatu)</string>\n  <string name=\"subtitles_disabled\">Tekstitykset pois</string>\n  <string name=\"auto_frame_rate\">Automaattinen kuvataajuus</string>\n  <string name=\"frame_rate_correction\">Korjaa kuvataajuus:\\n%s</string>\n  <string name=\"category_background_playback\">Taustatoisto</string>\n  <string name=\"not_implemented\">Ei käytössä</string>\n  <string name=\"option_background_playback_off\">Pois</string>\n  <string name=\"option_background_playback_pip\">Kuva kuvassa</string>\n  <string name=\"option_background_playback_only_audio\">Vain ääni</string>\n  <string name=\"playback_settings\">Toiston asetukset</string>\n  <string name=\"video_speed\">Videon nopeus</string>\n  <string name=\"resolution_switch\">Vaihda resoluutiota</string>\n  <string name=\"video_buffer\">Videon puskuri</string>\n  <string name=\"video_buffer_size_low\">Matala</string>\n  <string name=\"video_buffer_size_med\">Keski</string>\n  <string name=\"video_buffer_size_high\">Korkea</string>\n  <string name=\"update_changelog\">Muutokset</string>\n  <string name=\"install_update\">Asenna päivitys</string>\n  <string name=\"section_is_empty\">Oops! Täällä ei ole mitään.</string>\n  <string name=\"dialog_add_to_playlist\">Lisää/poista soittolistasta</string>\n  <string name=\"msg_signed_users_only\">Vain sisäänkirjautuneille</string>\n  <string name=\"msg_cant_load_content\">Oops. Sisältöä ei voida ladata.</string>\n  <string name=\"title_video_presets\">Videon esiasetus</string>\n  <string name=\"settings_accounts\">Tilit</string>\n  <string name=\"settings_left_panel\">Muokkaa osioita</string>\n  <string name=\"settings_themes\">Teemat</string>\n  <string name=\"settings_other\">Muut</string>\n  <string name=\"settings_player\">Videosoitin</string>\n  <string name=\"settings_language\">Kieli</string>\n  <string name=\"settings_linked_devices\">Linkitetyt laitteet</string>\n  <string name=\"settings_about\">Tietoja</string>\n  <string name=\"dialog_account_list\">Valitse tili</string>\n  <string name=\"dialog_account_none\">Ei mitään</string>\n  <string name=\"dialog_remove_account\">Kirjaudu ulos</string>\n  <string name=\"dialog_add_account\">Kirjaudu sisään</string>\n  <string name=\"default_lang\">Oletus</string>\n  <string name=\"dialog_select_language\">Sovelluksen kieli</string>\n  <string name=\"subtitle_default\">Oletus</string>\n  <string name=\"subtitle_white_semi_transparent\">Osittain läpinäkyvä</string>\n  <string name=\"subtitle_style\">Tekstityksen tyyli</string>\n  <string name=\"subtitle_language\">Tekstityksen kieli</string>\n  <string name=\"action_search\">Haku</string>\n  <string name=\"settings_main_ui\">Valikko</string>\n  <string name=\"dialog_main_ui\">Valikko</string>\n  <string name=\"card_animated_previews\">Animoidut esikatselut</string>\n  <string name=\"web_site\">Verkkosivu</string>\n  <string name=\"donation\">Lahjoita projektille</string>\n  <string name=\"dialog_about\">Tietoja</string>\n  <string name=\"dialog_player_ui\">Videosoitin</string>\n  <string name=\"player_show_ui_on_pause\">Näytä valikko kun pysäytetty</string>\n  <string name=\"player_pause_on_ok\">OK näppäin pysäyttää toiston</string>\n  <string name=\"player_ok_button_behavior\">OK näppäimen käyttö</string>\n  <string name=\"player_only_ui\">Vain valikko</string>\n  <string name=\"player_ui_and_pause\">Valikko ja pysäytys</string>\n  <string name=\"player_only_pause\">Vain pysäytys</string>\n  <string name=\"check_for_updates\">Tarkista päivitykset</string>\n  <string name=\"update_not_found\">Uusin versio on jo käytössä.</string>\n  <string name=\"update_in_progress\">Odota</string>\n  <string name=\"player_ui_hide_behavior\">Piilota valikko automaattisesti</string>\n  <string name=\"option_never\">Ei koskaan</string>\n  <string name=\"side_panel_sections\">Järjestä valikot</string>\n  <string name=\"boot_to_section\">Käynnistä valikkoon</string>\n  <string name=\"large_ui\">Suuri valikko</string>\n  <string name=\"video_grid_scale\">Videoruudukon koko</string>\n  <string name=\"scale_ui\">Valikon koko</string>\n  <string name=\"color_scheme\">Väriteema</string>\n  <string name=\"color_scheme_default\">Oletus</string>\n  <string name=\"color_scheme_red_grey\">Puna-harmaa</string>\n  <string name=\"color_scheme_red\">Punainen</string>\n  <string name=\"color_scheme_dark_grey\">Tummanharmaa</string>\n  <string name=\"disable_update_check\">Poista päivitysten tarkistus käytöstä</string>\n  <string name=\"show_again\">Näytä uudelleen</string>\n  <string name=\"check_updates_auto\">Ilmoita uusista päivityksistä</string>\n  <string name=\"select_account_on_boot\">Valitse tili käynnistettäessä</string>\n  <string name=\"player_other\">Muut</string>\n  <string name=\"player_full_date\">Tarkka päivämäärä kuvauksessa</string>\n  <string name=\"open_channel\">Avaa kanava</string>\n  <string name=\"not_interested\">Ei kiinnosta</string>\n  <string name=\"you_wont_see_this_video\">Video on poistettu suosituksista</string>\n  <string name=\"settings_search\">Haku</string>\n  <string name=\"dialog_search\">Haku</string>\n  <string name=\"instant_voice_search\">Äänihaku</string>\n  <string name=\"option_background_playback_behind\">Toista taustalla</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">Seuraavan videon tietoja ei ole vielä ladattu</string>\n  <string name=\"card_multiline_title\">Moniriviset otsikot</string>\n  <string name=\"cards_style\">Korttien tyyli</string>\n  <string name=\"repeat_mode_all\">Toista kaikki videot yksi kerrallaan</string>\n  <string name=\"repeat_mode_one\">Toista tämä video uudelleen</string>\n  <string name=\"repeat_mode_pause\">Pysäytä toisto videon jälkeen</string>\n  <string name=\"repeat_mode_pause_alt\">Toista vain soittolistan videot keskeytyksettä</string>\n  <string name=\"repeat_mode_none\">Pysäytä toisto jokaisen videon jälkeen</string>\n  <string name=\"subscribed_to_channel\">Kanava tilattu</string>\n  <string name=\"unsubscribed_from_channel\">Kanavaa ei ole tilattu</string>\n  <string name=\"subtitle_yellow_transparent\">Keltainen läpinäkyvällä taustalla</string>\n  <string name=\"subtitle_white_black\">Valkoinen mustalla taustalla</string>\n  <string name=\"player_seek_preview\">Esikatselu</string>\n  <string name=\"color_scheme_dark_grey_oled\">Tummanharmaa(OLED)</string>\n  <string name=\"channels_section_sorting\">Kanavien lajittelu</string>\n  <string name=\"sorting_by_new_content\">Uusi sisältö</string>\n  <string name=\"sorting_alphabetically\">Aakkosittain</string>\n  <string name=\"sorting_last_viewed\">Viimeksi katsotut</string>\n  <string name=\"player_pause_when_seek\">Pysäytä selauksen ajaksi</string>\n  <string name=\"playlists_style\">Soittolistan tyyli</string>\n  <string name=\"playlists_style_grid\">Ruudukko</string>\n  <string name=\"playlists_style_rows\">Rivi</string>\n  <string name=\"player_show_clock\">Näytä kello pop-up ikkunassa</string>\n  <string name=\"player_show_remaining_time\">Näytä jäljellä oleva aika</string>\n  <string name=\"open_channel_uploads\">Avaa kanavan lataukset</string>\n  <string name=\"app_exit_shortcut\">Poistu sovelluksesta</string>\n  <string name=\"app_exit_none\">Älä poistu</string>\n  <string name=\"app_double_back_exit\">Tuplanäpäytys taakse</string>\n  <string name=\"app_single_back_exit\">Yksi näpäytys taakse</string>\n  <string name=\"action_video_zoom\">Videon zoomaus</string>\n  <string name=\"video_zoom\">Videon zoomaus (oletusarvo)</string>\n  <string name=\"video_zoom_default\">Oletus</string>\n  <string name=\"video_zoom_fit_width\">Sovita leveys</string>\n  <string name=\"video_zoom_fit_height\">Sovita korkeus</string>\n  <string name=\"video_zoom_fit_both\">Sovita joko levees tai korkeus</string>\n  <string name=\"video_zoom_stretch\">Venytä</string>\n  <string name=\"color_scheme_teal\">Sinivihreä</string>\n  <string name=\"color_scheme_teal_oled\">Sinivihreä (OLED)</string>\n  <string name=\"player_seek_preview_none\">Ei käytössä</string>\n  <string name=\"player_seek_preview_single\">Kuvaruutu kerrallaan</string>\n  <string name=\"player_seek_preview_carousel\">Karuselli</string>\n  <string name=\"player_seek_preview_carousel_slow\">Karuselli (hidas)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Karuselli (nopea)</string>\n  <string name=\"unsubscribe_from_channel\">Poista tilaus</string>\n  <string name=\"card_title_lines_num\">Kortin nimi - rivien määrä</string>\n  <string name=\"card_auto_scrolled_title\">Näytä videon nimi</string>\n  <string name=\"share_link\">Jaa linkki</string>\n  <string name=\"subscribe_to_channel\">Tilaa kanava</string>\n  <string name=\"wait_data_loading\">Ladataan</string>\n  <string name=\"auto_frame_rate_pause\">Kuvataajuuden pysäytys</string>\n  <string name=\"auto_frame_rate_applying\">Säädetään kuvataajuutta %sx%s\\@%s</string>\n  <string name=\"audio_shift\">Äänen siirto</string>\n  <string name=\"player_remember_speed\">Muista nopeus</string>\n  <string name=\"mark_channel_as_watched\">Merkitse katsotuksi</string>\n  <string name=\"channel_marked_as_watched\">Kanava on merkitty katsotuksi</string>\n  <string name=\"dialog_add_device\">Lisää laite</string>\n  <string name=\"dialog_remove_all_devices\">Poista kaikki laitteet</string>\n  <string name=\"device_link_enabled\">Yhteys luotu</string>\n  <string name=\"device_connected\">Laite \\\"%s\\\" on yhdistetty</string>\n  <string name=\"device_disconnected\">Laite \\\"%s\\\" on poistettu</string>\n  <string name=\"settings_ui_scale\">Valikon koko</string>\n  <string name=\"msg_restart_app\">Käynnistä sovellus uudestaan muuttaaksesi asetukset</string>\n  <string name=\"settings_block\">Sisällön esto</string>\n  <string name=\"msg_applying\">Muutetaan %s…</string>\n  <string name=\"msg_done\">Valmis</string>\n  <string name=\"settings_general\">Yleiset</string>\n  <string name=\"settings_video\">Video</string>\n  <string name=\"content_block_confirm_skip\">Varmista skipatessa</string>\n  <string name=\"content_block_categories\">Ohita osiot</string>\n  <string name=\"content_block_sponsor\">Sponsorit</string>\n  <string name=\"content_block_intro\">Intron alkaessa</string>\n  <string name=\"content_block_outro\">Lopputekstit</string>\n  <string name=\"content_block_interaction\">Vuorovaikutuksen muistutus (subscribe)</string>\n  <string name=\"content_block_self_promo\">Kanavan mainostus</string>\n  <string name=\"content_block_music_off_topic\">Musiikittomien kohtien skippaus</string>\n  <string name=\"confirm_segment_skip\">Skippaa kohta \\\"%s\\\"\\?</string>\n  <string name=\"msg_skipping_segment\">Skipataan kohta \\\"%s\\\"…</string>\n  <string name=\"content_block_notification_type\">Ilmoituksen tyyppi</string>\n  <string name=\"content_block_notify_none\">Ei ilmoitusta</string>\n  <string name=\"content_block_notify_toast\">Ponnahdus</string>\n  <string name=\"content_block_notify_dialog\">Ilmoitusikkuna</string>\n  <string name=\"return_to_launcher\">Palaa valikkoon</string>\n  <string name=\"intent_force_close\">Pakota lopetus jos video avattu ulkoisesta lähteestä</string>\n  <string name=\"btn_confirm\">Vahvista</string>\n  <string name=\"player_low_video_quality\">Matala videolaatu</string>\n  <string name=\"remote_session_closed\">Etäyhteys on suljettu</string>\n  <string name=\"msg_mode_switch_error\">Ei voida vaihtaa kuvatilaa \\\"%s\\\"</string>\n  <string name=\"msg_player_error\">Soittimen virhe %s. Uudelleen käynnistetään toistoa…</string>\n  <string name=\"video_preset_disabled\">Ei oletusasetusta</string>\n  <string name=\"video_preset_enabled\">Seuraavan videon formaatti asetetaan oletukseksi</string>\n  <string name=\"player_sleep_timer\">Uniajastin</string>\n  <string name=\"player_show_quality_info\">Näytä videon tiedot</string>\n  <string name=\"player_remember_each_speed\">Muista jokaisen videon nopeus</string>\n  <string name=\"player_remember_speed_none\">Ei mitään</string>\n  <string name=\"player_remember_speed_all\">Sama kaikille videoille</string>\n  <string name=\"player_remember_speed_each\">Jokaiselle videolle erikseen</string>\n  <string name=\"msg_player_error_source\">Videota ei voida ladata</string>\n  <string name=\"msg_player_error_renderer\">Videon formaattia ei tunnisteta</string>\n  <string name=\"msg_player_error_unexpected\">Tuntematon videodekooderin virhe</string>\n  <string name=\"video_aspect\">Kuvasuhde</string>\n  <string name=\"player_tweaks\">Kehittäjäasetukset</string>\n  <string name=\"header_uploads\">Lataukset</string>\n  <string name=\"player_show_global_clock\">Näytä kello videon päällä</string>\n  <string name=\"uploads_old_look\">Vanha näkymä lataukset valikolle</string>\n  <string name=\"background_playback_activation\">Taustatoiston aktivointi</string>\n  <string name=\"channels_old_look\">Vanha näkymä kanavat-valikolle</string>\n  <string name=\"channels_auto_load\">Lataa kanavien sisältö automaattisesti</string>\n  <string name=\"return_to_background_video\">Palaa taustatoistettuun videoon</string>\n  <string name=\"playback_queue_category_title\">Toistojono</string>\n  <string name=\"open_playlist\">Avaa soittolista</string>\n  <string name=\"cancel_segment_skip\">Peruuta osion ohitus</string>\n  <string name=\"pin_unpin_from_sidebar\">Pidä/poista sivuvalikosta</string>\n  <string name=\"pinned_to_sidebar\">Lisätty sivuvalikkoon</string>\n  <string name=\"unpinned_from_sidebar\">Poistettu sivuvalikosta</string>\n  <string name=\"dialog_select_country\">Maa</string>\n  <string name=\"settings_language_country\">Kieli/maa</string>\n  <string name=\"share_embed_link\">Jaa linkki</string>\n  <string name=\"card_text_scroll_factor\">Kortin tekstin nopeus</string>\n  <string name=\"preferred_update_source\">Valitse päivityssivusto</string>\n  <string name=\"hide_shorts\">Piilota Shorts-videot</string>\n  <string name=\"key_remapping\">Näppäimien uudelleenohjelmointi </string>\n  <string name=\"screen_dimming\">Näytön pimennys</string>\n  <string name=\"removed_from_playback_queue\">Poistettu toistojonosta</string>\n  <string name=\"added_to_playback_queue\">Lisätty toistojonoon</string>\n  <string name=\"add_remove_from_playback_queue\">Lisää/poista toistojonosta</string>\n  <string name=\"proxy_enabled\">Proxy käytössä</string>\n  <string name=\"proxy_disabled\">Proxy pois käytöstä</string>\n  <string name=\"uploads_row_name\">Sivustolle lähetetyt</string>\n  <string name=\"playlists_row_name\">Tehdyt toistolistat</string>\n  <string name=\"popular_uploads_row_name\">Suositut sivustolle lähetetyt videot</string>\n  <string name=\"breaking_news_row_name\">Uusimmat uutiset</string>\n  <string name=\"covid_news_row_name\">Covid-19 uutisia</string>\n  <string name=\"enable_voice_search\">Käytä äänihakua</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Tilaa/poista kanavan tilaus</string>\n  <string name=\"app_backup_restore\">Tallenna/palauta asetukset</string>\n  <string name=\"app_backup\">Tallenna asetukset</string>\n  <string name=\"app_restore\">Palauta asetukset</string>\n  <string name=\"skip_each_segment_once\">Älä hyppää osioita uudelleen</string>\n  <string name=\"disable_ok_long_press\">Poista käytöstä pitkä OK painallus</string>\n  <string name=\"sponsor_color_markers\">Värimerkitty aikajana</string>\n  <string name=\"refresh_section\">Päivitä näkymä</string>\n  <string name=\"option_disabled\">Ei käytössä</string>\n  <string name=\"live_now_row_name\">Livenä nyt</string>\n  <string name=\"run_in_background\">Toista taustalla</string>\n  <string name=\"settings_remote_control\">Kaukosäädin</string>\n  <string name=\"background_service_started\">Toista taustalla</string>\n  <string name=\"dialog_add_to\">Lisää %s</string>\n  <string name=\"dialog_remove_from\">Poista %s</string>\n  <string name=\"added_to\">Lisätty %s</string>\n  <string name=\"removed_from\">Poistettu %s</string>\n  <string name=\"video_preset_adaptive\">Mukautuva</string>\n  <string name=\"player_time_correction\">Näytä aika suhteessa nopeuteen </string>\n  <string name=\"player_show_global_ending_time\">Näytä loppuaika koko ajan</string>\n  <string name=\"content_block_preview_recap\">Esikatselu tai tiivistelmä videosta</string>\n  <string name=\"content_block_highlight\">Kohta videosta tai kohokohta</string>\n  <string name=\"removed_from_history\">Video on poistettu katseluhistoriasta</string>\n  <string name=\"remove_from_history\">Poista katseluhistoriasta</string>\n  <string name=\"upload_date\">Latauspäivämäärä</string>\n  <string name=\"upload_date_any\">Kaikkien aikojen</string>\n  <string name=\"upload_date_today\">Tänään</string>\n  <string name=\"upload_date_this_week\">Tällä viikolla</string>\n  <string name=\"upload_date_this_month\">Tässä kuussa</string>\n  <string name=\"upload_date_this_year\">Tänä vuonna</string>\n  <string name=\"double_refresh_rate\">Kaksinkertainen virkistystaajuus</string>\n  <string name=\"upload_date_last_hour\">Viimeisin tunti</string>\n  <string name=\"focus_on_search_results\">Automaattinen tarkennus hakutuloksiin</string>\n  <string name=\"context_menu\">Kontekstivalikko</string>\n  <string name=\"add_remove_from_recent_playlist\">Lisää/poista toistolistasta</string>\n  <string name=\"hide_settings_section\">Piilota asetukset (käytä varoen!)</string>\n  <string name=\"volume\">Äänenvoimakkuus %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s sek.</string>\n  <string name=\"audio_shift_sec\">%s sek.</string>\n  <string name=\"ui_hide_timeout_sec\">%s sek.</string>\n  <string name=\"screen_dimming_timeout_min\">%s min.</string>\n  <string name=\"mark_all_channels_watched\">Merkitse kaikki kanavat katsotuiksi</string>\n  <string name=\"move_section_up\">Siirrä osio ylös</string>\n  <string name=\"move_section_down\">Siirrä osio alas</string>\n  <string name=\"player_buttons\">Aseta soittimen painikkeet</string>\n  <string name=\"action_playlist_add\">Lisää toistolistaan</string>\n  <string name=\"action_video_stats\">Videotilastot</string>\n  <string name=\"action_subscribe\">Tilaa</string>\n  <string name=\"action_channel\">Avaa kanava</string>\n  <string name=\"action_pip\">Kuva kuvassa (PIP)</string>\n  <string name=\"action_screen_off\">Näyttö pois</string>\n  <string name=\"action_playback_queue\">Toistojono</string>\n  <string name=\"action_video_speed\">Videon nopeus</string>\n  <string name=\"action_subtitles\">Tekstitykset</string>\n  <string name=\"action_like\">Tykkää</string>\n  <string name=\"action_dislike\">Peukuta alas</string>\n  <string name=\"action_play_pause\">Toista/paussi</string>\n  <string name=\"action_repeat_mode\">Toistotapa</string>\n  <string name=\"action_next\">Seuraava video</string>\n  <string name=\"action_previous\">Edellinen video</string>\n  <string name=\"action_high_quality\">Videon laatu</string>\n  <string name=\"content_block_no_skipping_mode\">Ei-ohitustila</string>\n  <string name=\"content_block_action_type\">Valitse toiminto</string>\n  <string name=\"content_block_action_none\">Älä tee mitään</string>\n  <string name=\"content_block_action_only_skip\">Ohita</string>\n  <string name=\"content_block_action_toast\">Ohita ilmoituksella</string>\n  <string name=\"content_block_action_dialog\">Näytä vahvistusikkuna</string>\n  <string name=\"content_block_filler\">Aiheen ohi</string>\n  <string name=\"keyboard_auto_show\">Näytä näppäimistö automaattisesti</string>\n  <string name=\"cancel_dialog\">Peruuta</string>\n  <string name=\"msg_player_error_source2\">Videolähde ei toimi tai kellon aika on väärä</string>\n  <string name=\"hide_upcoming\">Piilota tulevat videot tilauksista</string>\n  <string name=\"search_background_playback\">Käytä kuva-kuvassa (PIP) kun haet soittimesta</string>\n  <string name=\"trending_row_name\">Trendaavat</string>\n  <string name=\"player_seek_type\">Hakutapa</string>\n  <string name=\"player_seek_regular\">Tavallinen</string>\n  <string name=\"player_seek_confirmation_pause\">Vahvistuksella (tauko etsimisen aikana)</string>\n  <string name=\"player_seek_confirmation_play\">Vahvistuksella (toista kun etsitään)</string>\n  <string name=\"hide_shorts_from_home\">Piilota Shorts-videot Koti-näkymästä</string>\n  <string name=\"finish_on_disconnect\">Lopeta sovellus kun laite irroitetaan</string>\n  <string name=\"update_error\">Päivitys ei onnistunut</string>\n  <string name=\"unpin_from_sidebar\">Poista sivuvalikosta</string>\n  <string name=\"player_show_ending_time\">Näytä loppuaika</string>\n  <string name=\"hide_shorts_from_history\">Piilota Shorts-videot katseluhistoriasta</string>\n  <string name=\"disable_screensaver\">Älä käytä näytönsäästäjää</string>\n  <string name=\"action_video_info\">Videon kuvaus</string>\n  <string name=\"description_not_found\">Kuvausta ei löydy</string>\n  <string name=\"proxy_host_hint\">Välityspalvelimen isäntänimi tai IP</string>\n  <string name=\"proxy_username_hint\">Välityspalvelimen käyttäjätunnus</string>\n  <string name=\"proxy_port_hint\">Välityspalvelimen portti</string>\n  <string name=\"proxy_password_hint\">Välityspalvelimen salasana</string>\n  <string name=\"proxy_type\">Välityspalvelimen tyyppi</string>\n  <string name=\"enable_web_proxy\">Käytä välityspalvelimena nettiä</string>\n  <string name=\"proxy_not_supported\">Käytä välityspalvelimena nettiä (Android 4.4+)</string>\n  <string name=\"proxy_test_btn\">Testi</string>\n  <string name=\"proxy_settings_title\">Välityspalvelimen asetukset</string>\n  <string name=\"proxy_test_cancelled\">Testi#%d: peruttu</string>\n  <string name=\"proxy_type_invalid\">Välityspalvelimen tyyppiä ei ole asetettu</string>\n  <string name=\"proxy_host_invalid\">Välityspalvelimen isäntää ei ole asetettu</string>\n  <string name=\"proxy_port_invalid\">Välityspalvelimen portti väärä, tulöee olla &gt;0</string>\n  <string name=\"proxy_credentials_invalid\">Väärä käyttäjänimi tai salasana</string>\n  <string name=\"proxy_test_aborted\">Testi keskeytetty, korjaa ensin välityspalvelimen asetukset.</string>\n  <string name=\"enable_openvpn\">Käytä OpenVPN</string>\n  <string name=\"openvpn_settings_title\">OpenVPN asetukset</string>\n  <string name=\"openvpn_application_aborted\">Korjaa OpenVPN asetuksia</string>\n  <string name=\"openvpn_test_aborted\">Testi keskeytetty, korjaa ensin OpenVPN asetukset.</string>\n  <string name=\"openvpn_address_invalid\">OpenVPN-asetukset asettamatta</string>\n  <string name=\"internet_censorship\">Internetsensuuri</string>\n  <string name=\"rename_section\">Nimeä tämä osio uudelleen</string>\n  <string name=\"simple_edit_value_hint\">Uusi arvo</string>\n  <string name=\"seek_interval\">Hakuväli</string>\n  <string name=\"seek_interval_sec\">%s sec</string>\n  <string name=\"subtitle_system\">Järjestelmän tyyli (Android-asetukset -&gt; Esteettömyys)</string>\n  <string name=\"subtitle_scale\">Tekstityksen koko</string>\n  <string name=\"audio_sync_fix_desc\">Vaihtoehtoinen tapa synkronoida ääni/video. Jissain tapauksissa tämä voi auttaa.</string>\n  <string name=\"audio_sync_fix\">Äänen synkronoinnin korjaus</string>\n  <string name=\"ambilight_ratio_fix_desc\">Korjaa puuttuvan vinovalaistuksen. Korjaa virheellisen kuvasuhteen. Korjaa tyhjät kuvakaappaukset. Vaikuttaa suorituskykyyn!</string>\n  <string name=\"ambilight_ratio_fix\">Ambilight/kuvasuhde/kuvakaappaus korjaus</string>\n  <string name=\"force_legacy_codecs_desc\">Parantaa huomattavasti suorituskykyä halvempien laitteiden kanssa. Suurin resoluutio on 720p.</string>\n  <string name=\"force_legacy_codecs\">Pakota vanhat koodekit (720p)</string>\n  <string name=\"live_stream_fix_desc\">Parantaa merkittävästi suoratoiston suorituskykyä halvemmissa laitteissa. Suurin resoluutio on 1080p.</string>\n  <string name=\"live_stream_fix\">Suoratoiston korjaus (1080p)</string>\n  <string name=\"playback_notifications_fix_desc\">Piilottaa radan muutosilmoitukset. Voisi olla hyödyllinen AOSP-pohjaisissa laiteohjelmistoissa.</string>\n  <string name=\"playback_notifications_fix\">Poista toistoilmoitukset käytöstä</string>\n  <string name=\"amlogic_fix_desc\">Frame drop -korjaus Amlogic-pohjaisissa laitteissa.</string>\n  <string name=\"amlogic_fix\">Amlogic 1080p\\@60fps korjaus</string>\n  <string name=\"tunneled_video_playback_desc\">Huom: tauko ei ehkä toimi kunnolla! Tunneloitu videotoisto lupaa parannuksia kuten paremman äänen/videon synkronoinnin (AV-synkronointi) ja sujuvamman toiston. Vaatii Android 5+</string>\n  <string name=\"tunneled_video_playback\">Tunneloitu videotoisto (Android 5+)</string>\n  <string name=\"master_volume\">Äänenvoimakkuus</string>\n  <string name=\"volume_limit\">Äänenvoimakkuuden raja</string>\n  <string name=\"play_video\">Toista</string>\n  <string name=\"remember_position_of_short_videos\">Muista lyhyiden videoiden sijainti (alle 10 min)</string>\n  <string name=\"player_show_tooltips\">Näytä painikkeiden työkaluvihjeet</string>\n  <string name=\"action_like_unset\">Tykkää-toiminto on asettamatta</string>\n  <string name=\"action_dislike_unset\">En tykkää-toiminto on asettamatta</string>\n  <string name=\"various_buttons\">Painikkeet pääikkunan yläosassa</string>\n  <string name=\"not_compatible_with\">Valittu vaihtoehto ei ole yhteensopiva</string>\n  <string name=\"subtitle_position\">Tekstityksen alasiirto</string>\n  <string name=\"pressing_home\">painamalla Koti</string>\n  <string name=\"pressing_home_back\">painamalla Koti tai takaisin</string>\n  <string name=\"player_number_key_seek\">Etsi numeronäppäimillä</string>\n  <string name=\"save_remove_playlist\">Lisää/poista soittolista soittolistat-osiosta</string>\n  <string name=\"remove_playlist\">Poista soittolista</string>\n  <string name=\"removed_from_playlists\">Soittolista poistettu</string>\n  <string name=\"saved_to_playlists\">Lisätty soittolistat-osioon</string>\n  <string name=\"create_playlist\">Luo soittolista</string>\n  <string name=\"add_video_to_new_playlist\">Lisää uuteen soittolistaan</string>\n  <string name=\"cant_delete_empty_playlist\">Tyhjää soittolistaa ei voi poistaa</string>\n  <string name=\"playlist\">Soittolista</string>\n  <string name=\"rename_playlist\">Uudelleennimeä soittolista</string>\n  <string name=\"cant_rename_empty_playlist\">Tyhjää soittolistaa ei voi nimetä uudelleen</string>\n  <string name=\"cant_rename_foreign_playlist\">Ulkomaista soittolistaa ei voi nimetä uudelleen</string>\n  <string name=\"cant_save_playlist\">Tätä soittolistaa ei voi lisätä</string>\n  <string name=\"enter_value\">Anna mikä tahansa ei-tyhjä arvo</string>\n  <string name=\"dialog_add_remove_from\">Lisää/poista %s</string>\n  <string name=\"lb_playback_controls_skip_next\">Ohita/seuraava</string>\n  <string name=\"lb_playback_controls_skip_previous\">Ohita edellinen/kelaa taaksepäin alkuun</string>\n  <string name=\"alt_presets_behavior\">Vaihtoehtoinen esiasetus (rajoittaa kaistanleveyttä)</string>\n  <string name=\"alt_presets_behavior_desc\">Sovellus yrittää säilyttää kaistanleveyden vastaamaan valittua esiasetusta resoluution, fps:n ja koodekin täsmäämisen sijaan.</string>\n  <string name=\"sleep_timer\">Ota uniajastin käyttöön (yksi tunti)</string>\n  <string name=\"sleep_timer_desc\">Keskeytä toisto, jos käyttäjä ei ole käyttänyt kaukosäädintä tunnin aikana</string>\n  <string name=\"disable_vsync\">Poista snap to vsync käytöstä</string>\n  <string name=\"disable_vsync_desc\">Parantaa hieman toiston suorituskykyä</string>\n  <string name=\"skip_codec_profile_check\">Ohita koodekkiprofiilin tason tarkistus</string>\n  <string name=\"skip_codec_profile_check_desc\">Älä tarkista koodekkitukea käynnistäessäsi videota. Voisi olla apua bugisten laiteohjelmistojen kanssa.</string>\n  <string name=\"force_sw_codec\">Pakota ohjelmistopohjainen videodekooderi</string>\n  <string name=\"force_sw_codec_desc\">Voi toistaa melkein minkä tahansa videon mutta suorituskyky on erittäin huono.</string>\n  <string name=\"playlist_order\">Järjestä soittolista</string>\n  <string name=\"playlist_order_added_date_newer_first\">Lisäyspäivä (uusin ensin)</string>\n  <string name=\"playlist_order_added_date_older_first\">Lisäyspäivä (vanhin ensin)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Julkaisupäivä (uusin ensin)</string>\n  <string name=\"playlist_order_published_date_older_first\">Julkaisupäivä (vanhin ensin)</string>\n  <string name=\"playlist_order_popularity\">Katselukertoja</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">Tätä ei voi tehdä ulkomaisille soittolistoille</string>\n  <string name=\"owned_playlist_warning\">Tätä toimintoa voidaan soveltaa vain omistamiisi soittolistoihin.</string>\n  <string name=\"content_block_alt_server\">Käytä vaihtoehtoista palvelinta</string>\n  <string name=\"content_block_alt_server_desc\">Ota tämä vaihtoehto käyttöön jos SponsorBlock ei jostakin syystä toimi.</string>\n  <string name=\"unset_stream_reminder\">Poista striimi-muistutus</string>\n  <string name=\"set_stream_reminder\">Lisää striimi-muistutus</string>\n  <string name=\"playback_starts_shortly\">Toisto alkaa automaattisesti kun striimi alkaa</string>\n  <string name=\"starting_stream\">Videostriimiä aloitetaan</string>\n  <string name=\"action_playlist_remove\">Poista soittolistatsta</string>\n  <string name=\"signin_view_title\">Käyttäjäkoodi latautuu</string>\n  <string name=\"signin_view_description\">Kirjaudu sisään antamalla tämä koodi sivulla %s (Firefox tai Chrome)</string>\n  <string name=\"signin_view_action_text\">Ok</string>\n  <string name=\"require_checked\">Vaatii \\\"%s</string>\n  <string name=\"msg_press_again_to_exit\">Poistu painamalla uudelleen</string>\n  <string name=\"player_remaining_time\">Jäljellä: %s</string>\n  <string name=\"player_ending_time\">Päättyy %s</string>\n  <string name=\"badge_new_content\">Uutta sisältöä</string>\n  <string name=\"badge_live\">Live</string>\n  <string name=\"add_device_view_description\">Linkitä laite kirjoittamalla tämä koodi puhelimesi YouTube-sovelluksen kohtaan Asetukset/katso televisiossa.</string>\n  <string name=\"playback_controls_repeat_pause\">Toista tauko</string>\n  <string name=\"playback_controls_repeat_list\">Toista lista</string>\n  <string name=\"action_subscribe_off\">Tilaus pois päältä</string>\n  <string name=\"action_subscribe_on\">Tilaus päälle</string>\n  <string name=\"skip_24_rate\">Ohita 24 fps -muodot (oikea elokuvatila\\?)</string>\n  <string name=\"player_disable_suggestions\">Poista ehdotukset käytöstä</string>\n  <string name=\"feedback\">Palaute</string>\n  <string name=\"sources\">Lähteet</string>\n  <string name=\"releases\">Julkaisut</string>\n  <string name=\"prefer_avc_over_vp9\">Koodekin valinta: mieluummin avc kuin vp9</string>\n  <string name=\"proxy_application_aborted\">Korjaa ensin välityspalvelimen asetukset</string>\n  <string name=\"proxy_test_start\">Testi#%d: %s</string>\n  <string name=\"proxy_test_error\">Virhe#%d: %s</string>\n  <string name=\"proxy_test_status\">Tila#%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Määritystiedoston osoite (*.ovpn)</string>\n  <string name=\"add_to_playback_queue\">Lisää toistojonoon</string>\n  <string name=\"remove_from_playback_queue\">Poista toistojonosta</string>\n  <string name=\"pin_unpin_playlist\">Kiinnitä/irrota soittolista sivuvalikosta</string>\n  <string name=\"pin_unpin_channel\">Kiinnitä/irrota kanava sivuvalikosta</string>\n  <string name=\"subtitle_white_transparent\">Valkoinen läpinäkyvällä taustalla</string>\n  <string name=\"open_chat\">Chat</string>\n  <string name=\"place_chat_left\">Sijoita chat vasemmalle</string>\n  <string name=\"use_alt_speech_recognizer\">Vaihtoehtoinen puheentunnistus</string>\n  <string name=\"time_format\">Aikaformaatti</string>\n  <string name=\"time_format_24\">24h</string>\n  <string name=\"time_format_12\">12h</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Käytä vain jos kohtaat ongelmia oletuspuheentunnistuksen kanssa.</string>\n  <string name=\"speech_recognizer\">Puheentunnistus</string>\n  <string name=\"speech_recognizer_system\">Oletus (suositus)</string>\n  <string name=\"speech_recognizer_external_1\">Ulkoinen 1 (buginen, jos käytät tätä, sinun tulee ottaa mikrofoni pois käytöstä)</string>\n  <string name=\"speech_recognizer_external_2\">Ulkoinen 1 (myös buginen)</string>\n  <string name=\"real_channel_icon\">Näytä kanavaikoni</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Keltainen puoliläpinäkyvällä taustalla</string>\n  <string name=\"subtitle_yellow_black\">Keltainen mustalla taustalla</string>\n  <string name=\"player_pixel_ratio\">Pikselisuhde</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Tummanharmaa</string>\n  <string name=\"disable_mic_permission\">Poista sovelluksen mikrofoni käytöstä jotta tämä tunnistin toimii oikein.</string>\n  <string name=\"repeat_mode_shuffle\">Sekoita mikä tahansa soittolista</string>\n  <string name=\"chat_left\">Vasen</string>\n  <string name=\"chat_right\">Oikea</string>\n  <string name=\"card_real_thumbnails\">Korvaa videon oletuspikkukuva toisella kuvalla</string>\n  <string name=\"card_content\">Mistä videon pikkukuva otetaan</string>\n  <string name=\"thumb_quality_default\">Oletus</string>\n  <string name=\"thumb_quality_start\">Videon alusta</string>\n  <string name=\"thumb_quality_middle\">Videon keskeltä</string>\n  <string name=\"thumb_quality_end\">Videon lopusta</string>\n  <string name=\"video_buffer_size_none\">Ei valintaa</string>\n  <string name=\"content_block_status\">Tarkista SponsorBlock-palvelimen tilanne</string>\n  <string name=\"player_show_quality_info_bitrate\">Lisää bittinopeus tiedot</string>\n  <string name=\"player_speed_button_old_behavior\">Palauta nopeuspainikkeen vanha toimintatapa</string>\n  <string name=\"protect_settings_with_password\">Suojaa kaikki asetukset salasanalla</string>\n  <string name=\"enter_settings_password\">Syötä asetusten salasana</string>\n  <string name=\"child_mode\">Lapsilukko</string>\n  <string name=\"child_mode_desc\">Tässä tilassa et voi käyttää hakua tai nähdä ehdotettuja sisältöjä. Asetukset on suojattu salasanalla.</string>\n  <string name=\"lost_setting_warning\">Sovelluksen asetuksia muutetaan. Varmista, että olet luonut varmuuskopion asetuksista.</string>\n  <string name=\"player_button_long_click\">Lyhyt painallus nopeaan toimintaan ja pitkä painallus asetusten valintaikkunaan.</string>\n  <string name=\"pause_history\">Keskeytä historiatoiminto</string>\n  <string name=\"resume_history\">Palauta historiatoiminto</string>\n  <string name=\"clear_history\">Tyhjennä historia</string>\n  <string name=\"chapters\">Videon osat</string>\n  <string name=\"card_multiline_subtitle\">Monirivinen tekstitys</string>\n  <string name=\"auto_frame_rate_modes\">Tuetut tilat</string>\n  <string name=\"loading\">Ladataan</string>\n  <string name=\"hide_streams\">Piilota streamit tilauksista</string>\n  <string name=\"video_rotate\">Käännä</string>\n  <string name=\"trending_searches\">Trendaavat haut</string>\n  <string name=\"video_duration_any\">Mikä tahansa</string>\n  <string name=\"video_duration\">Kesto</string>\n  <string name=\"video_duration_under_4\">Alle 4 minuuttia</string>\n  <string name=\"video_duration_between_4_20\">4-20 minuuttia</string>\n  <string name=\"video_duration_over_20\">Yli 20 minuuttia</string>\n  <string name=\"content_type\">Tyyppi</string>\n  <string name=\"content_type_any\">Mikä tahansa</string>\n  <string name=\"content_type_video\">Video</string>\n  <string name=\"content_type_channel\">Kanava</string>\n  <string name=\"content_type_playlist\">Soittolista</string>\n  <string name=\"content_type_movie\">Elokuva</string>\n  <string name=\"video_features\">Ominaisuudet</string>\n  <string name=\"video_feature_any\">Mikä tahansa</string>\n  <string name=\"video_feature_live\">Live</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">Lajittele</string>\n  <string name=\"sort_by_relevance\">Merkityksellisyys</string>\n  <string name=\"sort_by_views\">Katselukerrat</string>\n  <string name=\"sort_by_date\">Julkaisupäivämäärä</string>\n  <string name=\"sort_by_rating\">Luokitus</string>\n  <string name=\"pressing_back\">painamalla takaisin</string>\n  <string name=\"clear_search_history\">Tyhjennä hakuhistoria</string>\n  <string name=\"remove_from_subscriptions\">Piilota</string>\n  <string name=\"player_long_speed_list\">Laaja nopeuslista </string>\n  <string name=\"alt_app_icon\">Vaihtoehtoinen sovelluskuvake (vaatii uudelleenkäynnistyksen)</string>\n  <string name=\"network_stack\">Suosi %s verkkopinoa</string>\n  <string name=\"cronet_desc\">Cronet on Chromiumin verkkopino. Cronet voi vähentää latenssia ja lisätä verkon suorituskykyä, mikä voi auttaa puskurointiongelmissa.</string>\n  <string name=\"unlock_all_formats\">Avaa kaikki videoformaatit</string>\n  <string name=\"unlock_all_formats_desc\">Joissakin laitteissa laiteohjelmisto ilmoittaa virheellisesti, että jotkin formaatit eivät ole tuettuja, vaikka ne ovatkin.\\n  (esim. 1080p-älytelevisiot ilmoittavat yleensä 4k:n olevan tukematon).</string>\n  <string name=\"okhttp_desc\">Tämä verkkopino on hitain, mutta sen vakaus on hyvä.</string>\n  <string name=\"select_channel_section\">Avaa vastaava osio, kun sovellus on käynnistetty Android TV-kanavista</string>\n  <string name=\"enable_voice_search_desc\">Virallinen sovellus korvataan niin sanotulla sillalla. Silta auttaa siirtämään hakupyynnöt sovellukseemme</string>\n  <string name=\"enable_master_password\">Pääsalasanan käyttöönotto</string>\n  <string name=\"enter_master_password\">Syötä pääsalasana</string>\n  <string name=\"disable_stream_buffer\">Puskurin poistaminen käytöstä videoissa</string>\n  <string name=\"disable_stream_buffer_desc\">Korjaus tilanteisiin, joissa livevideo on jäljessä. Huomaa: striimi voi alkaa viivästyä</string>\n  <string name=\"sony_frame_drop_fix\">Frame drop korjaus #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">Korjaa viiveet Sony TV:ssä ja joissakin muissa laitteissa. Huomautus: voi aiheuttaa ongelmia äänen synkronoinnissa</string>\n  <string name=\"audio_language\">Ääniraidan kieli</string>\n  <string name=\"old_home_look\">Päämenun vanha ulkoasu</string>\n  <string name=\"hide_shorts_everywhere\">Piilota Shorts-videot kaikkialta</string>\n  <string name=\"update_found\">Päivitä</string>\n  <string name=\"volume_boost_warning\">Kaikki rajan ylittävät asetukset voivat mahdollisesti vahingoittaa kaiuttimia</string>\n  <string name=\"play_video_incognito\">Toista tuntemattomana</string>\n  <string name=\"header_kids_home\">Lapset</string>\n  <string name=\"live_stream_fix_4k_desc\">Varoitus, tämä poistaa kelaamisen käytöstä. Parantaa merkittävästi suoratoiston suorituskykyä. Maksimiresoluutio on 4K.</string>\n  <string name=\"live_stream_fix_4k\">Suoratoiston korjaus (4K)</string>\n  <string name=\"disable_ok_long_press_desc\">Tarkoitettu bugisille ohjaimille, joissa OK-painike ei toimi kunnolla</string>\n  <string name=\"you_wont_see_this_channel\">Et näe tätä kanavaa suosituksissa</string>\n  <string name=\"not_recommend_channel\">Älä suosittele tätä kanavaa</string>\n  <string name=\"player_section_playlist\">Käytä nykyisen osion sisältöä soittolistana</string>\n  <string name=\"header_trending\">Suosittua nyt</string>\n  <string name=\"screensaver\">Näytönsäästäjä</string>\n  <string name=\"player_screen_off_timeout\">Näytön sammuttamisen aikakatkaisu</string>\n  <string name=\"old_update_notifications\">Päivitysilmoitusten vanha ulkoasu</string>\n  <string name=\"mark_as_watched\">Merkitse katselluksi</string>\n  <string name=\"player_ui_animations\">Ota ohjauspaneelin animaatiot käyttöön</string>\n  <string name=\"player_likes_count\">Näytä tykkäysten/alapeukkujen määrä</string>\n  <string name=\"sorting_alphabetically2\">Aakkosjärjestyksessä (nopea)</string>\n  <string name=\"sorting_default\">Oletusarvo (nopea)</string>\n  <string name=\"content_block_exclude_channel\">Jätä tämä kanava pois SponsorBlockista</string>\n  <string name=\"content_block_stop_excluding_channel\">Lopeta tämän kanavan poissulkeminen SponsorBlockista</string>\n  <string name=\"player_chapter_notification\">Näytä ilmoitus edetäksesi nopeasti videon osioiden läpi</string>\n  <string name=\"player_volume\">Äänenvoimakkuus</string>\n  <string name=\"subtitle_remember\">Muista tekstitysasetus kanavilla</string>\n  <string name=\"amazon_frame_drop_fix_desc\">Tarkoitettu Amazon Stick -laitteille. Saattaa toimia myös muissa laitteissa.</string>\n  <string name=\"default_stack_desc\">Sisäänrakennettu verkkopino. Tämä saattaa olla vakaampi tietyissä tilanteissa.</string>\n  <string name=\"item_postion\">Kohta</string>\n  <string name=\"player_screen_off_dimming\">Näyttö pois päältä - himmennyksen määrä</string>\n  <string name=\"screensaver_timout\">Näytönsäästäjän aikakatkaisu</string>\n  <string name=\"screensaver_dimming\">Näytönsäästäjän himmennyksen määrä</string>\n  <string name=\"player_ui_on_next\">Näytä soittimen käyttöliittymä seuraavaan videoon siirryttäessä</string>\n  <string name=\"autogenerated\">Automaattisesti luotu</string>\n  <string name=\"player_auto_volume\">Automaattinen äänenvoimakkuuden säätö</string>\n  <string name=\"header_shorts\">Short-videot</string>\n  <string name=\"amazon_frame_drop_fix\">Frame drop korjaus #2</string>\n  <string name=\"player_global_focus\">Saumaton navigointi soittimen painikerivien välillä</string>\n  <string name=\"dialog_notification\">Ilmoita päivityksestä ponnahdusikkunassa</string>\n  <string name=\"open_comments\">Näytä kommentit</string>\n  <string name=\"msg_player_error_video_source\">Videon URL-osoite ei toimi tai laitteen aika on väärä.\\nYleensä sovellusongelma.</string>\n  <string name=\"msg_player_error_audio_source\">Ääniraidan URL-osoite ei toimi tai laitteen aika on väärä.\\nYleensä sovellusongelma.</string>\n  <string name=\"msg_player_error_subtitle_source\">Tekstitysten URL-osoite ei toimi tai laitteen aika on väärä.\\nYleensä sovellusongelma.</string>\n  <string name=\"msg_player_error_video_renderer\">Valittua videomuotoa ei tueta.\\nYritä valita toinen painamalla soittimen HQ-painiketta.\\nJos tämä ei auta, käynnistä laite uudelleen.</string>\n  <string name=\"msg_player_error_audio_renderer\">Valittua ääniraitaa ei tueta.\\nYritä valita toinen painamalla soittimen HQ-painiketta.\\nJos tämä ei auta, käynnistä laite uudelleen.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">Valittua tekstitysten muotoa ei tueta.\\nYritä valita toinen painamalla pitkään soittimen CC-painiketta.\\nJos tämä ei auta, käynnistä laite uudelleen.</string>\n  <string name=\"auto_history\">Automaattinen (käytä tiliasetuksia)</string>\n  <string name=\"remember_position_subscriptions\">Muista viimeksi katsottu sijainti tilauksissa</string>\n  <string name=\"msg_player_unknown_error\">Tuntematon virhe</string>\n  <string name=\"header_notifications\">Ilmoitukset</string>\n  <string name=\"disable_search_history\">Poista hakuhistoria käytöstä</string>\n  <string name=\"unlock_high_bitrate_formats\">Avaa korkean bittinopeuden 1080p vp9-formaatit</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Avaa korkean bittinopeuden mp4a-formaatit</string>\n  <string name=\"color_scheme_blue\">Sininen</string>\n  <string name=\"disable_history\">Poista historia käytöstä</string>\n  <string name=\"enable_history\">Ota historia käyttöön</string>\n  <string name=\"color_scheme_dark_blue\">Tummansininen</string>\n  <string name=\"player_speed_per_channel\">Jokaiselle kanavalle</string>\n  <string name=\"disable_popular_searches\">Älä näytä suosittuja hakuja</string>\n  <string name=\"multi_profiles\">Käytä eri asetuksia eri käyttäjille</string>\n  <string name=\"protect_account_with_password\">Suojaa tämä tili salasanalla</string>\n  <string name=\"enter_account_password\">Syötä tilin salasana</string>\n  <string name=\"show_connect_messages\">Näytä yhteysviestit</string>\n  <string name=\"prefer_avc_over_vp9_desc\">Huom. max 1080P</string>\n  <string name=\"news_row_name\">Uutiset</string>\n  <string name=\"play_next\">Toista seuraava</string>\n  <string name=\"hide_watched_from_subscriptions\">Piilota katsotut tilauksista</string>\n  <string name=\"hide_unwanted_content\">Piilota ei-toivottu sisältö</string>\n  <string name=\"hide_watched_from_home\">Piilota katsotut videot pääsivulta</string>\n  <string name=\"remote_control_permission\">Taustakauko-ohjaus vaatii Overlay-luvan</string>\n  <string name=\"login_from_browser\">Sisäänkirjautuminen selaimella</string>\n  <string name=\"disable_remote_history\">Poista historia käytöstä kaukosäädintä käytettäessä</string>\n  <string name=\"keyboard_fix\">Korjaa näppäimistön näyttäminen OK-näppäimen painalluksella (G20s ja muut)</string>\n  <string name=\"nothing_found\">Ei hakutuloksia</string>\n  <string name=\"auto_frame_rate_desc\">Tämä vaihtoehto poistaa värinän kohtauksista, joissa kamera liikkuu nopeasti, esim. urheilulähetykset.</string>\n  <string name=\"channel_filter_hint\">Hae tilauksista</string>\n  <string name=\"player_loop_shorts\">Jatka shorts-videoiden toistamista</string>\n  <string name=\"player_quick_shorts_skip\">Ohita shorts-videot vasemmalle/oikealle -painikkeilla</string>\n  <string name=\"channels_filter\">Suodata kanavaluetteloa Kanavat-osiossa</string>\n  <string name=\"channel_search_bar\">Hakupalkki tilattujen kanavien listauksessa</string>\n  <string name=\"hide_shorts_channel\">Piilota shortsit kanavalta</string>\n  <string name=\"pin_channel\">Lisää kanava sivuvalikkoon</string>\n  <string name=\"pin_playlist\">Lisää soittolista sivuvalikkoon</string>\n  <string name=\"app_corner_clock\">Koti: kello oikeassa yläkulmassa</string>\n  <string name=\"player_corner_clock\">Videosoitin: kello oikeassa yläkulmassa</string>\n  <string name=\"player_corner_ending_time\">Videosoitin: oikeassa yläkulmassa päättymisaika</string>\n  <string name=\"share_qr_link\">Jaa linkki (QR-koodi)</string>\n  <string name=\"hide_upcoming_channel\">Piilota tulevat kanavalta</string>\n  <string name=\"hide_upcoming_home\">Piilota tulevat koti-näkymästä</string>\n  <string name=\"hide_shorts_from_trending\">Piilota Shorts-videot trendaavista</string>\n  <string name=\"player_exit_shortcut\">Poistu soittimesta</string>\n  <string name=\"action_sound_off\">Ääni pois</string>\n  <string name=\"remember_position_of_live_videos\">Muista livestriimin kohta</string>\n  <string name=\"save_playlist\">Lisää soittolista soittolistat-osioon</string>\n  <string name=\"skip_shorts\">Ohita shortit</string>\n  <string name=\"speech_engine\">Äänihaun tekniikka</string>\n  <string name=\"dearrow_status\">Tarkista DeArrow-palvelimen tilanne</string>\n  <string name=\"player_extra_long_speed_list\">Extralaaja nopeuslista </string>\n  <string name=\"player_network_stack\">Verkkotekniikka</string>\n  <string name=\"old_channel_look\">Kanavatilausten vanha ulkoasu</string>\n  <string name=\"remember_position_pinned\">Muista viimeksi katsottu sijainti soittolistoissa</string>\n  <string name=\"unknown_source_error\">Tuntematon lähdevirhe</string>\n  <string name=\"unknown_renderer_error\">Tuntematon renderointivirhe</string>\n  <string name=\"hide_watched_from_notifications\">Piilota katsotut videot ilmoituksista</string>\n  <string name=\"hide_watched_from_watch_later\">Piilota katsotut videot Katsele myöhemmin -soittolistalta</string>\n  <string name=\"player_quick_skip_videos\">Ohita tavallisia videoita vasen/oikea-painikkeilla</string>\n  <string name=\"header_sports\">Urheilu</string>\n  <string name=\"keep_finished_activities\">Säilytä valmiit toiminnot</string>\n  <string name=\"disable_channels_service\">Kanavat-palvelun poistaminen käytöstä</string>\n  <string name=\"replace_titles\">Korvaa otsikot</string>\n  <string name=\"enable\">Ota käyttöön</string>\n  <string name=\"more_info\">Lisätietoa</string>\n  <string name=\"about_sponsorblock\">Tietoa SponsorBlockista</string>\n  <string name=\"about_dearrow\">Tietoa DeArrowista</string>\n  <string name=\"replace_thumbnails\">Korvaa pikkukuvat</string>\n  <string name=\"crowdsourced_thumbnails\">Joukkorahoitteiset pikkukuvat</string>\n  <string name=\"crowdsoursed_titles\">Joukkorahoitteiset otsikot</string>\n  <string name=\"dearrow_not_submitted_thumbs\">Pikkukuvien lähde (jos DeArrow-vaihtoehtoa ei ole)</string>\n  <string name=\"pitch_effect\">Pitch-vaikutus</string>\n  <string name=\"fullscreen_mode\">Koko näytön tila (ilman järjestelmäpalkkeja)</string>\n  <string name=\"player_only_mode\">Näytä vain soitin, jos video avataan sovelluksen ulkopuolella</string>\n  <string name=\"pinned_channel_rows\">Näytä kiinnitetty kanava riveinä</string>\n  <string name=\"prefer_ipv4\">Suosi IPv4 DNS:ää</string>\n  <string name=\"prefer_ipv4_desc\">Voi korjata tilanteet, joissa sovellus ei toimi.\\nHuom. Saattaa aiheuttaa kaatumisia (erityisesti Android 8 -laitteissa tai Dune HD:ssa)</string>\n  <string name=\"long_press_for_settings\">Pitkä painallus -&gt; asetuksiin</string>\n  <string name=\"long_press_for_options\">Pitkä painallus -&gt; vaihtoehtoihin</string>\n  <string name=\"device_specific_backup\">Varmuuskopiointi vain tätä laitetta varten</string>\n  <string name=\"local_backup\">Paikallinen varmuuskopiointi</string>\n  <string name=\"auto_backup\">Automaattinen varmuuskopiointi (kerran päivässä)</string>\n  <string name=\"repeat_mode_reverse_list\">Toista soittolistan tai kanavan videot käänteisessä järjestyksessä</string>\n  <string name=\"calm_msg\">Korjaamme ongelmaa. Tarkista päivitykset aika ajoin</string>\n  <string name=\"without_picture\">Ilman kuvaa</string>\n  <string name=\"video_disabled\">Video poistettu käytöstä</string>\n  <string name=\"applying_fix\">Älä sulje soitinta. Korjausta tehdään juuri</string>\n  <string name=\"player_global_focus_desc\">Mitä nappia korostetaan kun liikutaan nappien välillä</string>\n  <string name=\"hide_mixes\">Piilota sekoitukset</string>\n  <string name=\"disable_network_error_fixing\">Poista automaattinen verkkovirheiden korjaus käytöstä</string>\n  <string name=\"disable_network_error_fixing_desc\">Kannattaa ehkä ottaa tämä vaihtoehto käyttöön, jos käytät VPN:ää.</string>\n  <string name=\"video_buffer_size_lowest\">Matalin</string>\n  <string name=\"recommended\">Suositeltu</string>\n  <string name=\"add_to_subscriptions_group\">Lisää/poista tilausryhmästä</string>\n  <string name=\"new_subscriptions_group\">Uusi ryhmä</string>\n  <string name=\"rename_group\">Nimeä tilausryhmä uudelleen</string>\n  <string name=\"screen_dimming_amount\">Näytön himmennyksen määrä</string>\n  <string name=\"screen_dimming_timeout\">Näytön himmennyksen aikakatkaisu</string>\n  <string name=\"place_comments_left\">Aseta kommentit vasemmalle</string>\n  <string name=\"video_buffer_size_highest\">Korkein</string>\n  <string name=\"unpin_group_from_sidebar\">Poista tilausryhmä</string>\n  <string name=\"playlists_rows\">Näytä soittolistojen osio riveinä</string>\n  <string name=\"suggestions\">Ehdotukset</string>\n  <string name=\"not_supported_by_device\">Laite ei tue tätä toimintoa</string>\n  <string name=\"import_subscriptions_group\">Tuo tilaukset</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">Poista suljetut tekstitykset käytöstä</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">Ota käyttöön suljettu tekstitykset</string>\n  <string name=\"my_videos\">Omat videoni</string>\n  <string name=\"player_audio_focus\">Äänen tarkennus (tauko, jos muita soittimia havaitaan)</string>\n  <string name=\"paid_content_notification\">Maksullista sisältöä koskeva ilmoitus</string>\n  <string name=\"you_liked\">Peukkua ylös</string>\n  <string name=\"card_preview\">Korttien esikatselu</string>\n  <string name=\"card_preview_full\">Videon esikatselu äänellä</string>\n  <string name=\"card_preview_muted\">Videon esikatselu ilman ääntä</string>\n  <string name=\"oculus_quest_fix\">Oculus Questin korjaus</string>\n  <string name=\"playback_buffering_fix\">Toiston puskuroinnin korjaaminen</string>\n  <string name=\"premium_users_only\">Vain Youtube Premium-käyttäjille. Korjaa epätäydellinen videoformaattiluettelo</string>\n  <string name=\"card_unlocalized_titles\">Älä suomenna videoiden otsikoita</string>\n  <string name=\"original_lang\">Alkuperäinen</string>\n  <string name=\"search_exit_shortcut\">Poistu hausta</string>\n  <string name=\"context_menu_sorting\">Kontekstivalikon järjestely</string>\n  <string name=\"video_flip\">Peilaa</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">Älä muuta videon kokoa kommentteja lukiessa</string>\n  <string name=\"hide_shorts_from_search\">Piilota shortsit hakutuloksista</string>\n  <string name=\"remove_playlist_fmt\">Poistetaanko %s pysyvästi\\?</string>\n  <string name=\"typing_corrections\">Poista automaattinen korjaus käytöstä tekstiä kirjoitettaessa</string>\n  <string name=\"suggestions_horizontally_scrolled\">Vaakasuunnassa vieritettävät ehdotukset</string>\n  <string name=\"stable_restore\">Palauta vakaa versio (kaikki SmartTuben tiedot menetetään)</string>\n  <string name=\"auto_backup_category\">Automaattinen asetusten tallentaminen</string>\n  <string name=\"once_a_day\">Kerran päivässä</string>\n  <string name=\"once_a_week\">Kerran viikossa</string>\n  <string name=\"once_a_month\">Kerran kuukaudessa</string>\n  <string name=\"action_debug_info\">Virheenkorjaustiedot</string>\n  <string name=\"dialog_block_channel\">Estä kanava</string>\n  <string name=\"dialog_unblock_channel\">Poista kanavan esto</string>\n  <string name=\"confirm_block_channel\">Piilota kaikki sisältö käyttäjältä %s</string>\n  <string name=\"channel_blocked\">Kanava estetty</string>\n  <string name=\"channel_unblocked\">Kanavan esto poistettu</string>\n  <string name=\"header_blocked_channels\">Estetyt kanavat</string>\n  <string name=\"msg_no_blocked_channels\">Ei estettyjä kanavia</string>\n  <string name=\"player_time_stretching\">Äänen keston venytys</string>\n  <string name=\"player_time_stretching_desc\">Pitää äänen luonnollisena nopeutta vaihdettaessa. Saattaa heikentää suorituskykyä merkittävästi joillakin laitteilla.</string>\n  <string name=\"queue_respects_playback_mode\">Toistojono noudattaa toistotilaa</string>\n  <string name=\"play_from_start\">Toista alusta</string>\n  <string name=\"ignore_short_segments\">Ohita lyhyet video-osiot</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-fr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Accueil</string>\n  <string name=\"title_search\">Rechercher</string>\n  <string name=\"header_subscriptions\">Abonnements</string>\n  <string name=\"header_history\">Historique</string>\n  <string name=\"header_music\">Musique</string>\n  <string name=\"header_news\">Actualités</string>\n  <string name=\"header_gaming\">Jeux vidéo</string>\n  <string name=\"header_playlists\">Playlists</string>\n  <string name=\"header_settings\">Paramètres</string>\n  <string name=\"header_channels\">Chaînes</string>\n  <string name=\"subscriptions_signin_title\">Découvrir les nouveautés de vos chaînes préférées</string>\n  <string name=\"subscriptions_signin_subtitle\">Connectez-vous pour voir vos abonnements</string>\n  <string name=\"subscriptions_signin_button_text\">CONNEXION</string>\n  <string name=\"library_signin_to_show_more\">Connectez-vous pour accéder à vos abonnements et aux vidéos que vous avez aimées ou enregistrées</string>\n  <string name=\"library_signin_subtitle\">Connectez-vous pour accéder à votre bibliothèque</string>\n  <string name=\"action_signin\">CONNEXION</string>\n  <string name=\"title_video_formats\">Formats vidéo</string>\n  <string name=\"title_audio_formats\">Formats audio</string>\n  <string name=\"subtitle_category_title\">Sous-titres</string>\n  <string name=\"playback_queue_category_title\">File d\\'attente</string>\n  <string name=\"video_max_quality\">Auto (qualité maximale)</string>\n  <string name=\"audio_max_quality\">Auto (qualité maximale)</string>\n  <string name=\"subtitles_disabled\">Sous-titres désactivés</string>\n  <string name=\"auto_frame_rate\">Fréquence d\\'images automatique</string>\n  <string name=\"frame_rate_correction\">Correction FPS :\\n%s</string>\n  <string name=\"category_background_playback\">Lecture en arrière-plan</string>\n  <string name=\"not_implemented\">Non implémenté</string>\n  <string name=\"option_background_playback_off\">Désactivée</string>\n  <string name=\"option_background_playback_pip\">Vidéo et audio</string>\n  <string name=\"option_background_playback_only_audio\">Uniquement l\\'audio</string>\n  <string name=\"playback_settings\">Paramètres de lecture</string>\n  <string name=\"video_speed\">Vitesse de lecture</string>\n  <string name=\"resolution_switch\">Changer de résolution</string>\n  <string name=\"video_buffer\">Mémoire tampon vidéo</string>\n  <string name=\"video_buffer_size_none\">Aucune</string>\n  <string name=\"video_buffer_size_low\">Basse</string>\n  <string name=\"video_buffer_size_med\">Moyenne</string>\n  <string name=\"video_buffer_size_high\">Élevée</string>\n  <string name=\"update_changelog\">Journal des modifications</string>\n  <string name=\"install_update\">Installer la mise à jour</string>\n  <string name=\"section_is_empty\">Oups ! Il n\\'y a rien ici.</string>\n  <string name=\"dialog_add_to_playlist\">Ajouter/Retirer d\\'une playlist</string>\n  <string name=\"msg_signed_users_only\">Utilisateurs connectés uniquement</string>\n  <string name=\"msg_cant_load_content\">Impossible de charger le contenu.\\n1) Vérifiez la date et l\\'heure de votre appareil.\\n2) Vérifiez la connexion réseau.\\n3) Essayez de vous connecter à votre compte.\\n4) Peut-être qu\\'il n\\'y a rien ici.</string>\n  <string name=\"title_video_presets\">Préférences de format vidéo</string>\n  <string name=\"settings_accounts\">Comptes</string>\n  <string name=\"settings_left_panel\">Modifier les catégories</string>\n  <string name=\"settings_themes\">Thèmes</string>\n  <string name=\"settings_other\">Autres</string>\n  <string name=\"settings_player\">Lecteur vidéo</string>\n  <string name=\"settings_language\">Langue</string>\n  <string name=\"settings_linked_devices\">Appareils liés</string>\n  <string name=\"settings_about\">À propos</string>\n  <string name=\"dialog_account_list\">Sélectionner un compte</string>\n  <string name=\"dialog_account_none\">Aucun</string>\n  <string name=\"dialog_remove_account\">Supprimer un compte</string>\n  <string name=\"dialog_add_account\">Ajouter un compte</string>\n  <string name=\"default_lang\">Par défaut</string>\n  <string name=\"dialog_select_language\">Langue de l\\'application</string>\n  <string name=\"subtitle_default\">Par défaut</string>\n  <string name=\"subtitle_white_semi_transparent\">Texte blanc sur fond noir semi-transparent</string>\n  <string name=\"subtitle_style\">Style de sous-titre</string>\n  <string name=\"subtitle_language\">Langue des sous-titres</string>\n  <string name=\"action_search\">Nouvelle recherche</string>\n  <string name=\"settings_main_ui\">Interface utilisateur</string>\n  <string name=\"dialog_main_ui\">Interface utilisateur</string>\n  <string name=\"card_animated_previews\">Aperçus animés</string>\n  <string name=\"web_site\">Site Web</string>\n  <string name=\"donation\">Faire un don</string>\n  <string name=\"dialog_about\">À propos</string>\n  <string name=\"dialog_player_ui\">Lecteur vidéo</string>\n  <string name=\"player_show_ui_on_pause\">Afficher l\\'interface en pause</string>\n  <string name=\"player_pause_on_ok\">La touche OK met la lecture en pause</string>\n  <string name=\"player_ok_button_behavior\">Comportement du bouton OK</string>\n  <string name=\"player_only_ui\">Affiche l\\'interface</string>\n  <string name=\"player_ui_and_pause\">Met en pause et affiche l\\'interface</string>\n  <string name=\"player_only_pause\">Met en pause</string>\n  <string name=\"check_for_updates\">Rechercher des mises à jour</string>\n  <string name=\"update_not_found\">Vous utilisez déjà la dernière version</string>\n  <string name=\"update_in_progress\">Chargement…</string>\n  <string name=\"player_ui_hide_behavior\">Masquage automatique de l\\'interface</string>\n  <string name=\"option_never\">Jamais</string>\n  <string name=\"side_panel_sections\">Gestion des sections</string>\n  <string name=\"boot_to_section\">Section au démarrage</string>\n  <string name=\"large_ui\">Grande interface</string>\n  <string name=\"video_grid_scale\">Échelle de la grille vidéo</string>\n  <string name=\"scale_ui\">Échelle de l\\'interface</string>\n  <string name=\"color_scheme\">Couleurs du thème</string>\n  <string name=\"color_scheme_default\">Couleurs par défaut</string>\n  <string name=\"color_scheme_red_grey\">Rouge et gris</string>\n  <string name=\"color_scheme_red\">Rouge</string>\n  <string name=\"color_scheme_dark_grey\">Gris foncé</string>\n  <string name=\"disable_update_check\">Désactiver la vérification des mises à jour</string>\n  <string name=\"show_again\">Afficher à nouveau</string>\n  <string name=\"check_updates_auto\">Mise à jour automatique</string>\n  <string name=\"select_account_on_boot\">Sélection du compte au démarrage</string>\n  <string name=\"player_other\">Divers</string>\n  <string name=\"player_full_date\">Date complète dans la description</string>\n  <string name=\"open_channel\">Accéder à la chaîne</string>\n  <string name=\"open_playlist\">Ouvrir la playlist</string>\n  <string name=\"not_interested\">Pas intéressé</string>\n  <string name=\"not_recommend_channel\">Ne pas recommander cette chaîne</string>\n  <string name=\"you_wont_see_this_video\">La vidéo a été supprimée de vos recommandations</string>\n  <string name=\"you_wont_see_this_channel\">Cette chaîne n\\'apparaîtra plus dans vos recommandations</string>\n  <string name=\"settings_search\">Recherche</string>\n  <string name=\"dialog_search\">Recherche</string>\n  <string name=\"instant_voice_search\">Recherche vocale instantanée</string>\n  <string name=\"option_background_playback_behind\">Lecture en arrière-plan</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">Les informations sur la vidéo suivante ne sont pas encore chargées</string>\n  <string name=\"card_multiline_title\">Titres multilignes</string>\n  <string name=\"cards_style\">Style des tuiles</string>\n  <string name=\"repeat_mode_all\">Lecture automatique de la vidéo suivante</string>\n  <string name=\"repeat_mode_one\">Lecture en boucle</string>\n  <string name=\"repeat_mode_pause\">Mettre en pause la lecture après la vidéo</string>\n  <string name=\"repeat_mode_pause_alt\">Lecture automatique de la vidéo suivante uniquement pour les playlists</string>\n  <string name=\"repeat_mode_none\">Arrêter la lecture après la vidéo</string>\n  <string name=\"subscribed_to_channel\">Abonné(e) à la chaîne</string>\n  <string name=\"unsubscribed_from_channel\">Non abonné(e) à la chaîne</string>\n  <string name=\"subtitle_yellow_transparent\">Texte jaune</string>\n  <string name=\"subtitle_white_transparent\">Texte blanc</string>\n  <string name=\"subtitle_white_black\">Texte blanc sur fond noir</string>\n  <string name=\"player_seek_preview\">Aperçu pendant la navigation vidéo</string>\n  <string name=\"color_scheme_dark_grey_oled\">Gris foncé (OLED)</string>\n  <string name=\"channels_section_sorting\">Tri des sections de chaînes</string>\n  <string name=\"sorting_by_new_content\">Nouveau contenu</string>\n  <string name=\"sorting_alphabetically\">Ordre alphabétique</string>\n  <string name=\"sorting_last_viewed\">Dernier visionnage</string>\n  <string name=\"player_pause_when_seek\">Mettre en pause pendant la navigation vidéo</string>\n  <string name=\"playlists_style\">Style des playlists</string>\n  <string name=\"playlists_style_grid\">Grille</string>\n  <string name=\"playlists_style_rows\">Lignes</string>\n  <string name=\"player_show_clock\">Afficher la date et l\\'heure</string>\n  <string name=\"player_show_remaining_time\">Afficher le temps restant</string>\n  <string name=\"player_show_ending_time\">Afficher l\\'heure de fin</string>\n  <string name=\"open_channel_uploads\">Ouvrir les vidéos de la chaîne</string>\n  <string name=\"app_exit_shortcut\">Contrôles pour sortir de l’application</string>\n  <string name=\"app_exit_none\">Ne pas sortir de l\\'application</string>\n  <string name=\"app_double_back_exit\">2 fois sur retour pour sortir</string>\n  <string name=\"app_single_back_exit\">1 fois sur retour pour sortir</string>\n  <string name=\"action_video_zoom\">Zoom vidéo</string>\n  <string name=\"video_zoom\">Zoom vidéo</string>\n  <string name=\"video_zoom_default\">Par défaut</string>\n  <string name=\"video_zoom_fit_width\">Ajustement de la largeur</string>\n  <string name=\"video_zoom_fit_height\">Ajustement de la hauteur</string>\n  <string name=\"video_zoom_fit_both\">Ajustement de la largeur ou de la hauteur</string>\n  <string name=\"video_zoom_stretch\">Étirer</string>\n  <string name=\"color_scheme_teal\">Bleu sarcelle</string>\n  <string name=\"color_scheme_teal_oled\">Bleu sarcelle (OLED)</string>\n  <string name=\"player_seek_preview_none\">Désactivé</string>\n  <string name=\"player_seek_preview_single\">Une seule image</string>\n  <string name=\"player_seek_preview_carousel\">Carrousel</string>\n  <string name=\"player_seek_preview_carousel_slow\">Carrousel (lent)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Carrousel (rapide)</string>\n  <string name=\"unsubscribe_from_channel\">Se désabonner de la chaîne</string>\n  <string name=\"card_title_lines_num\">Nombre de lignes du titre des tuiles</string>\n  <string name=\"card_auto_scrolled_title\">Défilement automatique des titres trop longs</string>\n  <string name=\"share_link\">Partager le lien</string>\n  <string name=\"subscribe_to_channel\">S\\'abonner à la chaîne</string>\n  <string name=\"wait_data_loading\">Veuillez patienter pendant le chargement des données…</string>\n  <string name=\"auto_frame_rate_pause\">Pause automatique lors d\\'un changement de fréquence d\\'images</string>\n  <string name=\"auto_frame_rate_applying\">Application de la fréquence d\\'images automatique %sx%s\\@%s</string>\n  <string name=\"audio_shift\">Décalage audio</string>\n  <string name=\"player_remember_speed\">Mémoriser la vitesse de lecture</string>\n  <string name=\"mark_channel_as_watched\">Marquer comme vue</string>\n  <string name=\"channel_marked_as_watched\">La chaîne a été marquée comme vue</string>\n  <string name=\"dialog_add_device\">Ajouter un appareil</string>\n  <string name=\"dialog_remove_all_devices\">Supprimer tous les appareils enregistrés</string>\n  <string name=\"device_link_enabled\">Lien activé</string>\n  <string name=\"device_connected\">L\\'appareil \\\"%s\\\" a été connecté</string>\n  <string name=\"device_disconnected\">L\\'appareil \\\"%s\\\" a été déconnecté</string>\n  <string name=\"settings_ui_scale\">Échelle de l\\'interface</string>\n  <string name=\"msg_restart_app\">Veuillez redémarrer l\\'application pour appliquer ces paramètres</string>\n  <string name=\"settings_block\">Blocage de contenu</string>\n  <string name=\"msg_applying\">Application %s…</string>\n  <string name=\"msg_done\">Terminé</string>\n  <string name=\"settings_general\">Général</string>\n  <string name=\"settings_video\">Vidéo</string>\n  <string name=\"content_block_confirm_skip\">Confirmer le saut</string>\n  <string name=\"content_block_categories\">Catégories</string>\n  <string name=\"content_block_sponsor\">Message sponsorisé</string>\n  <string name=\"content_block_intro\">Entracte/Animation d\\'intro</string>\n  <string name=\"content_block_outro\">Générique de fin</string>\n  <string name=\"content_block_interaction\">Rappel d\\'interaction (abonnement)</string>\n  <string name=\"content_block_self_promo\">Non rémunéré/autopromotion</string>\n  <string name=\"content_block_music_off_topic\">Segment non musical du clip</string>\n  <string name=\"confirm_segment_skip\">Sauter le segment \\\"%s\\\" \\?</string>\n  <string name=\"cancel_segment_skip\">Annuler le saut du segment</string>\n  <string name=\"msg_skipping_segment\">Saut du segment \\\"%s\\\"…</string>\n  <string name=\"content_block_notification_type\">Type de notification</string>\n  <string name=\"content_block_notify_none\">Sans notification</string>\n  <string name=\"content_block_notify_toast\">Bulle de notification</string>\n  <string name=\"content_block_notify_dialog\">Dialogue de confirmation</string>\n  <string name=\"return_to_launcher\">Revenir au lanceur à partir des chaînes ATV ou de la recherche</string>\n  <string name=\"intent_force_close\">Fermeture forcée si la vidéo a été ouverte à partir d\\'une source externe</string>\n  <string name=\"btn_confirm\">Confirmer</string>\n  <string name=\"player_low_video_quality\">Faible qualité vidéo</string>\n  <string name=\"remote_session_closed\">La session à distance a été fermée</string>\n  <string name=\"msg_mode_switch_error\">Impossible de basculer le mode d\\'affichage sur \\\"%s\\\"</string>\n  <string name=\"msg_player_error\">Une erreur s\\'est produite durant la lecture %s. Redémarrage du lecteur…</string>\n  <string name=\"video_preset_disabled\">Aucune préférence</string>\n  <string name=\"video_preset_enabled\">Le format de la prochaine vidéo sera défini selon vos préférences</string>\n  <string name=\"player_sleep_timer\">Mise en veille automatique</string>\n  <string name=\"player_show_quality_info\">Afficher la qualité vidéo actuelle dans la barre de contrôles</string>\n  <string name=\"player_remember_each_speed\">Mémoriser la vitesse de lecture pour chaque vidéo</string>\n  <string name=\"player_remember_speed_none\">Jamais</string>\n  <string name=\"player_remember_speed_all\">Identique pour toutes les vidéos</string>\n  <string name=\"player_remember_speed_each\">Unique pour chaque vidéo</string>\n  <string name=\"msg_player_error_source\">Impossible de charger la vidéo</string>\n  <string name=\"msg_player_error_renderer\">Le format sélectionné n\\'est pas pris en charge. Essayez de sélectionner un autre format. Si cela ne suffit pas, essayez de redémarrer l\\'appareil.</string>\n  <string name=\"msg_player_error_video_renderer\">Le format VIDÉO sélectionné n\\'est pas pris en charge. Essayez de sélectionner un autre format en appuyant sur le bouton HQ du lecteur. Si cela ne suffit pas, essayez de redémarrer l\\'appareil.</string>\n  <string name=\"msg_player_error_audio_renderer\">Le format AUDIO sélectionné n\\'est pas pris en charge. Essayez de sélectionner un autre format en appuyant sur le bouton HQ du lecteur. Si cela ne suffit pas, essayez de redémarrer l\\'appareil.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">Le format de SOUS-TITRES sélectionné n\\'est pas pris en charge. Essayez de sélectionner un autre format en réalisant un appui long sur le bouton CC du lecteur. Si cela ne suffit pas, essayez de redémarrer l\\'appareil.</string>\n  <string name=\"msg_player_error_unexpected\">Erreur inconnue du décodeur vidéo</string>\n  <string name=\"video_aspect\">Ratio de l\\'image</string>\n  <string name=\"player_tweaks\">Options développeur</string>\n  <string name=\"header_uploads\">Uploads</string>\n  <string name=\"player_show_global_clock\">Afficher la date et l\\'heure en haut à droite de l\\'interface</string>\n  <string name=\"player_show_global_ending_time\">Afficher en permanence l\\'heure de fin de la vidéo à l\\'écran</string>\n  <string name=\"uploads_old_look\">Utiliser l\\'ancienne apparence pour la section Uploads</string>\n  <string name=\"background_playback_activation\">Activation de la lecture en arrière-plan</string>\n  <string name=\"channels_old_look\">Utiliser l\\'ancienne apparence pour la section Chaînes</string>\n  <string name=\"channels_auto_load\">Charger automatiquement le contenu de la section Chaînes</string>\n  <string name=\"return_to_background_video\">Retour à la vidéo en arrière-plan</string>\n  <string name=\"pin_unpin_from_sidebar\">Épingler/Détacher de la barre latérale</string>\n  <string name=\"pin_unpin_playlist\">Épingler/Détacher la playlist</string>\n  <string name=\"pin_unpin_channel\">Épingler/Détacher la chaîne</string>\n  <string name=\"pinned_to_sidebar\">Épinglé à la barre latérale</string>\n  <string name=\"unpin_from_sidebar\">Détacher de la barre latérale</string>\n  <string name=\"unpinned_from_sidebar\">Détaché de la barre latérale</string>\n  <string name=\"dialog_select_country\">Pays</string>\n  <string name=\"settings_language_country\">Langue/Pays</string>\n  <string name=\"share_embed_link\">Partager le lien d\\'intégration</string>\n  <string name=\"card_text_scroll_factor\">Vitesse de défilement du texte des tuiles</string>\n  <string name=\"preferred_update_source\">Source de mise à jour</string>\n  <string name=\"hide_shorts\">Masquer les shorts des abonnements</string>\n  <string name=\"key_remapping\">Réaffectation des touches</string>\n  <string name=\"screen_dimming\">Réduction de la luminosité de l\\'écran</string>\n  <string name=\"removed_from_playback_queue\">Supprimé de la file d\\'attente</string>\n  <string name=\"added_to_playback_queue\">Ajouté à la file d\\'attente</string>\n  <string name=\"add_remove_from_playback_queue\">Ajouter/Supprimer de la file d\\'attente</string>\n  <string name=\"add_to_playback_queue\">Ajouter à la file d\\'attente</string>\n  <string name=\"remove_from_playback_queue\">Supprimer de la file d\\'attente</string>\n  <string name=\"proxy_enabled\">Proxy activé</string>\n  <string name=\"proxy_disabled\">Proxy désactivé</string>\n  <string name=\"uploads_row_name\">Uploads</string>\n  <string name=\"playlists_row_name\">Playlists créées</string>\n  <string name=\"popular_uploads_row_name\">Uploads populaires</string>\n  <string name=\"breaking_news_row_name\">Dernières nouvelles</string>\n  <string name=\"covid_news_row_name\">Nouvelles du COVID-19</string>\n  <string name=\"enable_voice_search\">Activer la recherche vocale (support du micrologiciel requis)</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">S\\'abonner/se désabonner de la chaîne</string>\n  <string name=\"app_backup_restore\">Sauvegarder/Restaurer</string>\n  <string name=\"app_backup\">Sauvegarder les données</string>\n  <string name=\"app_restore\">Restaurer les données à partir d\\'une sauvegarde</string>\n  <string name=\"skip_each_segment_once\">Sauter les segments uniquement au premier passage</string>\n  <string name=\"disable_ok_long_press\">Désactiver la fonction de pression longue du bouton OK</string>\n  <string name=\"disable_ok_long_press_desc\">Destiné aux télécommandes défectueuses sur lesquelles le bouton OK ne fonctionne pas correctement.</string>\n  <string name=\"player_time_correction\">Affichage de l\\'heure en fonction de la vitesse</string>\n  <string name=\"sponsor_color_markers\">Marqueurs de couleur sur la barre de progression</string>\n  <string name=\"refresh_section\">Actualiser la section</string>\n  <string name=\"option_disabled\">Désactivée</string>\n  <string name=\"live_now_row_name\">Actuellement en direct</string>\n  <string name=\"run_in_background\">Mettre en arrière-plan</string>\n  <string name=\"settings_remote_control\">Télécommander depuis un autre appareil</string>\n  <string name=\"background_service_started\">Démarrage du service d\\'arrière-plan</string>\n  <string name=\"dialog_add_to\">Ajouter à %s</string>\n  <string name=\"dialog_remove_from\">Retirer de %s</string>\n  <string name=\"added_to\">Ajouté à %s</string>\n  <string name=\"removed_from\">Retiré de %s</string>\n  <string name=\"video_preset_adaptive\">Adaptatif</string>\n  <string name=\"content_block_preview_recap\">Aperçu/Résumé</string>\n  <string name=\"content_block_highlight\">Point essentiel (signet)</string>\n  <string name=\"removed_from_history\">La vidéo a été supprimée de l\\'historique</string>\n  <string name=\"remove_from_history\">Supprimer de l\\'historique</string>\n  <string name=\"upload_date\">Date d\\'ajout</string>\n  <string name=\"upload_date_any\">Toutes</string>\n  <string name=\"upload_date_today\">Aujourd\\'hui</string>\n  <string name=\"upload_date_this_week\">Cette semaine</string>\n  <string name=\"upload_date_this_month\">Ce mois-ci</string>\n  <string name=\"upload_date_this_year\">Cette année</string>\n  <string name=\"double_refresh_rate\">Doubler le taux de rafraîchissement</string>\n  <string name=\"upload_date_last_hour\">Dernière heure</string>\n  <string name=\"focus_on_search_results\">Focus automatique sur les résultats de recherche</string>\n  <string name=\"context_menu\">Commandes du menu contextuel</string>\n  <string name=\"add_remove_from_recent_playlist\">Ajouter/Retirer de la playlist la plus récente</string>\n  <string name=\"hide_settings_section\">Masquer la section Paramètres (dangereux !)</string>\n  <string name=\"volume\">Volume %s%%</string>\n  <string name=\"auto_frame_rate_sec\">Pendant %s sec</string>\n  <string name=\"audio_shift_sec\">%s sec</string>\n  <string name=\"ui_hide_timeout_sec\">Après %s sec</string>\n  <string name=\"screen_dimming_timeout_min\">Après %s min</string>\n  <string name=\"mark_all_channels_watched\">Marquer toutes les chaînes comme regardées</string>\n  <string name=\"move_section_up\">Déplacer la section vers le haut</string>\n  <string name=\"move_section_down\">Déplacer la section vers le bas</string>\n  <string name=\"player_buttons\">Gestion des boutons du lecteur</string>\n  <string name=\"action_playlist_add\">Ajouter à la playlist</string>\n  <string name=\"action_video_stats\">Stats de la vidéo</string>\n  <string name=\"action_subscribe\">S\\'abonner</string>\n  <string name=\"action_channel\">Ouvrir la chaîne</string>\n  <string name=\"action_pip\">PIP</string>\n  <string name=\"action_video_info\">Description de la vidéo</string>\n  <string name=\"action_screen_off\">Écran éteint</string>\n  <string name=\"action_playback_queue\">File d\\'attente</string>\n  <string name=\"action_video_speed\">Vitesse de lecture</string>\n  <string name=\"action_subtitles\">Sous-titres</string>\n  <string name=\"action_like\">J\\'aime</string>\n  <string name=\"action_dislike\">Je n\\'aime pas</string>\n  <string name=\"action_play_pause\">Lecture/Pause</string>\n  <string name=\"action_repeat_mode\">Mode de lecture</string>\n  <string name=\"action_next\">Vidéo suivante</string>\n  <string name=\"action_previous\">Vidéo précédente</string>\n  <string name=\"action_high_quality\">Qualité de la vidéo</string>\n  <string name=\"content_block_no_skipping_mode\">Mode sans saut</string>\n  <string name=\"content_block_action_type\">Comportement</string>\n  <string name=\"content_block_action_none\">Ne rien faire</string>\n  <string name=\"content_block_action_only_skip\">Sauter le segment</string>\n  <string name=\"content_block_action_toast\">Sauter le segment avec notification</string>\n  <string name=\"content_block_action_dialog\">Demander confirmation avant de sauter le segment</string>\n  <string name=\"content_block_filler\">Hors sujet (remplissage)</string>\n  <string name=\"keyboard_auto_show\">Afficher le clavier automatiquement</string>\n  <string name=\"cancel_dialog\">Annuler</string>\n  <string name=\"msg_player_error_source2\">La source (URL) ne fonctionne pas ou la date et l\\'heure de votre appareil sont incorrectes. Il s\\'agit généralement d\\'un problème lié à l\\'application.</string>\n  <string name=\"msg_player_error_video_source\">La source (URL) VIDÉO ne fonctionne pas ou la date et l\\'heure de votre appareil sont incorrectes. Il s\\'agit généralement d\\'un problème lié à l\\'application.</string>\n  <string name=\"msg_player_error_audio_source\">La source (URL) AUDIO ne fonctionne pas ou la date et l\\'heure de votre appareil sont incorrectes. Il s\\'agit généralement d\\'un problème lié à l\\'application.</string>\n  <string name=\"msg_player_error_subtitle_source\">La source (URL) des SOUS-TITRES ne fonctionne pas ou la date et l\\'heure de votre appareil sont incorrectes. Il s\\'agit généralement d\\'un problème lié à l\\'application.</string>\n  <string name=\"hide_upcoming\">Masquer les diffusions à venir des abonnements</string>\n  <string name=\"search_background_playback\">Mettre la vidéo en arrière-plan lors d\\'une recherche ou de l\\'ouverture de la chaîne à partir du lecteur</string>\n  <string name=\"trending_row_name\">Tendances</string>\n  <string name=\"player_seek_type\">Comportement de la navigation vidéo</string>\n  <string name=\"player_seek_regular\">Standard</string>\n  <string name=\"player_seek_confirmation_pause\">Avec confirmation (mettre en pause pendant la navigation)</string>\n  <string name=\"player_seek_confirmation_play\">Avec confirmation (continuer la lecture pendant la navigation)</string>\n  <string name=\"hide_shorts_from_home\">Masquer les shorts de l\\'accueil</string>\n  <string name=\"finish_on_disconnect\">Fermer l\\'application lorsque le téléphone est déconnecté</string>\n  <string name=\"update_error\">Erreur de mise à jour</string>\n  <string name=\"hide_shorts_from_history\">Masquer les shorts de l\\'historique</string>\n  <string name=\"disable_screensaver\">Désactiver l\\'économiseur d\\'écran</string>\n  <string name=\"description_not_found\">Description introuvable</string>\n  <string name=\"proxy_port_hint\">Port du proxy</string>\n  <string name=\"proxy_host_hint\">Nom d\\'hôte ou IP du proxy</string>\n  <string name=\"proxy_username_hint\">Nom d\\'utilisateur du proxy</string>\n  <string name=\"proxy_password_hint\">Mot de passe du proxy</string>\n  <string name=\"proxy_type\">Type de proxy</string>\n  <string name=\"enable_web_proxy\">Utiliser un proxy web</string>\n  <string name=\"proxy_not_supported\">Utiliser un proxy web (Nécessite Android 4.4+)</string>\n  <string name=\"proxy_test_btn\">Test</string>\n  <string name=\"proxy_settings_title\">Paramètres du serveur proxy</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">Test#%d : annulé.</string>\n  <string name=\"proxy_type_invalid\">Le type de proxy n\\'est pas défini.</string>\n  <string name=\"proxy_host_invalid\">L\\'hôte du proxy n\\'est pas défini.</string>\n  <string name=\"proxy_port_invalid\">Port du proxy non valide, doit être &gt; 0.</string>\n  <string name=\"proxy_credentials_invalid\">Nom d\\'utilisateur ou mot de passe incorrect.</string>\n  <string name=\"proxy_test_aborted\">Test interrompu, veuillez d\\'abord corriger les paramètres du proxy.</string>\n  <string name=\"proxy_application_aborted\">Veuillez d\\'abord corriger les paramètres du proxy.</string>\n  <string name=\"proxy_test_start\">Test#%d : %s …</string>\n  <string name=\"proxy_test_error\">Erreur#%d : %s</string>\n  <string name=\"proxy_test_status\">Statut#%d : %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Adresse du fichier de configuration (*.ovpn)</string>\n  <string name=\"enable_openvpn\">Utiliser OpenVPN</string>\n  <string name=\"openvpn_settings_title\">Paramètres d\\'OpenVPN</string>\n  <string name=\"openvpn_application_aborted\">Veuillez d\\'abord corrigez les paramètres d\\'OpenVPN.</string>\n  <string name=\"openvpn_test_aborted\">Test interrompu, veuillez d\\'abord corriger les paramètres d\\'OpenVPN.</string>\n  <string name=\"openvpn_address_invalid\">L\\'adresse de configuration d\\'OpenVPN n\\'est pas définie.</string>\n  <string name=\"internet_censorship\">Censure d\\'Internet</string>\n  <string name=\"rename_section\">Renommer la section</string>\n  <string name=\"simple_edit_value_hint\">Saisissez une valeur</string>\n  <string name=\"seek_interval\">Intervalle de navigation vidéo</string>\n  <string name=\"seek_interval_sec\">%s sec</string>\n  <string name=\"subtitle_system\">Style du système (Paramètres Android &gt; Accessibilité)</string>\n  <string name=\"subtitle_scale\">Échelle des sous-titres</string>\n  <string name=\"audio_sync_fix_desc\">Un moyen alternatif de synchroniser l\\'audio/vidéo considéré comme obsolète mais pouvant aider dans certains cas.</string>\n  <string name=\"audio_sync_fix\">Correction de la synchronisation audio</string>\n  <string name=\"ambilight_ratio_fix_desc\">Corrige l\\'absence de l\\'éclairage de biais, les problèmes de ratio d\\'image incorrect et des captures d\\'écran vides. Affecte les performances !</string>\n  <string name=\"ambilight_ratio_fix\">Correction Ambilight/Ratio d\\'image/Captures d\\'écran</string>\n  <string name=\"force_legacy_codecs_desc\">Améliore significativement les performances sur les appareils bas de gamme mais limite la résolution maximale à 720p.</string>\n  <string name=\"force_legacy_codecs\">Forcer les anciens codecs (720p)</string>\n  <string name=\"live_stream_fix_desc\">Attention ! Cette option désactive le retour en arrière pour les diffusions en direct. Améliore considérablement les performances des diffusions en direct sur les appareils bas de gamme mais limite la résolution maximale à 1080p.</string>\n  <string name=\"live_stream_fix_4k_desc\">Attention ! Cette option désactive le retour en arrière pour les diffusions en direct. Améliore considérablement les performances des diffusions en direct sur les appareils bas de gamme mais limite la résolution maximale à 4K.</string>\n  <string name=\"live_stream_fix\">Correction des diffusions en direct (1080p)</string>\n  <string name=\"live_stream_fix_4k\">Correction des diffusions en direct (4K)</string>\n  <string name=\"playback_notifications_fix_desc\">Masque les notifications de changement de piste. Peut être utile sur les micrologiciel basés sur AOSP.</string>\n  <string name=\"playback_notifications_fix\">Désactiver les notifications de lecture</string>\n  <string name=\"amlogic_fix_desc\">Correction de la perte d\\'images sur les appareils basés sur Amlogic.</string>\n  <string name=\"amlogic_fix\">Correction Amlogic 1080p\\@60FPS</string>\n  <string name=\"tunneled_video_playback_desc\">Attention : la fonction pause pourrait ne plus fonctionner. La lecture vidéo par tunnel promet des avantages tels qu\\'une meilleure synchronisation audio/vidéo (AV sync) et une lecture plus fluide (Android 5+ requis).</string>\n  <string name=\"tunneled_video_playback\">Lecture vidéo en tunnel (Android 5+)</string>\n  <string name=\"master_volume\">Volume principal</string>\n  <string name=\"volume_limit\">Limitation du volume</string>\n  <string name=\"player_volume\">Volume</string>\n  <string name=\"play_video\">Lecture</string>\n  <string name=\"remember_position_of_short_videos\">Mémoriser la position des vidéos courtes (moins de 5 min)</string>\n  <string name=\"player_show_tooltips\">Afficher les infobulles des boutons</string>\n  <string name=\"action_like_unset\">\\\"J\\'aime\\\" n\\'est pas défini</string>\n  <string name=\"action_dislike_unset\">\\\"Je n\\'aime pas\\\" n\\'est pas défini</string>\n  <string name=\"various_buttons\">Gestion des boutons du menu supérieur</string>\n  <string name=\"not_compatible_with\">L\\'option sélectionnée n\\'est pas compatible avec</string>\n  <string name=\"subtitle_position\">Décalage des sous-titres depuis le bas de l\\'écran</string>\n  <string name=\"pressing_home\">en appuyant sur ACCUEIL</string>\n  <string name=\"pressing_home_back\">en appuyant sur ACCUEIL ou RETOUR</string>\n  <string name=\"pressing_back\">en appuyant sur RETOUR</string>\n  <string name=\"player_number_key_seek\">Avancer/Reculer la vidéo avec les touches numériques</string>\n  <string name=\"save_remove_playlist\">Ajouter/Supprimer cette playlist de la bibliothèque</string>\n  <string name=\"remove_playlist\">Supprimer cette playlist de la bibliothèque</string>\n  <string name=\"removed_from_playlists\">Playlist supprimée de la bibliothèque</string>\n  <string name=\"saved_to_playlists\">Playlist ajoutée à la bibliothèque</string>\n  <string name=\"create_playlist\">Créer une nouvelle playlist</string>\n  <string name=\"add_video_to_new_playlist\">Ajouter à une nouvelle playlist</string>\n  <string name=\"cant_delete_empty_playlist\">Impossible de supprimer une playlist vide</string>\n  <string name=\"playlist\">Playlist</string>\n  <string name=\"rename_playlist\">Renommer la playlist</string>\n  <string name=\"cant_rename_empty_playlist\">Impossible de renommer une playlist vide</string>\n  <string name=\"cant_rename_foreign_playlist\">Impossible de renommer une playlist étrangère</string>\n  <string name=\"cant_save_playlist\">Impossible d\\'ajouter cette playlist</string>\n  <string name=\"enter_value\">Veuillez saisir une valeur</string>\n  <string name=\"dialog_add_remove_from\">Ajouter/supprimer de %s</string>\n  <string name=\"lb_playback_controls_skip_next\">Passer à la vidéo suivante</string>\n  <string name=\"lb_playback_controls_skip_previous\">Revenir au début de la vidéo ou à la vidéo précédente</string>\n  <string name=\"alt_presets_behavior\">Utiliser un comportement alternatif pour les préférences vidéo afin de limiter la bande passante</string>\n  <string name=\"alt_presets_behavior_desc\">L\\'application essaiera d\\'adapter les préférences vidéo à la bande passante au lieu d\\'appliquer simplement la résolution, les images par seconde et le codec demandés.</string>\n  <string name=\"sleep_timer\">Mise en veille automatique (sans interaction au bout d\\'une heure)</string>\n  <string name=\"sleep_timer_desc\">Met en pause la lecture si l\\'utilisateur n\\'a pas utilisé la télécommande pendant une heure.</string>\n  <string name=\"disable_vsync\">Désactiver la synchronisation verticale (vsync)</string>\n  <string name=\"disable_vsync_desc\">Désactive la synchronisation verticale de l\\'écran. Peut améliorer les performances sur les appareils bas de gamme en libérant des ressources processeur (CPU).</string>\n  <string name=\"skip_codec_profile_check\">Ignorer la vérification des codecs</string>\n  <string name=\"skip_codec_profile_check_desc\">Ne vérifie pas la prise en charge des codecs au démarrage d\\'une vidéo. Peut être utile sur les micrologiciels buggés.</string>\n  <string name=\"force_sw_codec\">Forcer l\\'utilisation du décodeur vidéo logiciel</string>\n  <string name=\"force_sw_codec_desc\">Permet de lire presque toutes les vidéos mais avec de très mauvaises performances.</string>\n  <string name=\"playlist_order\">Trier la playlist</string>\n  <string name=\"playlist_order_added_date_newer_first\">Date d\\'ajout (plus récentes)</string>\n  <string name=\"playlist_order_added_date_older_first\">Date d\\'ajout (plus anciennes)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Date de publication (plus récentes)</string>\n  <string name=\"playlist_order_published_date_older_first\">Date de publication (plus anciennes)</string>\n  <string name=\"playlist_order_popularity\">Les plus populaires</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">Impossible de faire cela pour une playlist étrangère\t</string>\n  <string name=\"owned_playlist_warning\">Erreur : cette action est possible uniquement pour vos propres playlists</string>\n  <string name=\"content_block_alt_server\">Utiliser un serveur alternatif</string>\n  <string name=\"content_block_alt_server_desc\">Activer cette option si SponsorBlock refuse de fonctionner pour différentes raisons.</string>\n  <string name=\"unset_stream_reminder\">Activer un rappel pour cette diffusion</string>\n  <string name=\"set_stream_reminder\">Désactiver le rappel pour cette diffusion</string>\n  <string name=\"playback_starts_shortly\">La lecture démarrera automatiquement lorsque la diffusion commencera</string>\n  <string name=\"starting_stream\">Démarrage de la diffusion…</string>\n  <string name=\"action_playlist_remove\">Supprimer de la playlist</string>\n  <string name=\"signin_view_title\">Le code est en cours de chargement…</string>\n  <string name=\"signin_view_description\">Pour vous connecter, entrez ce code sur la page %s. Fonctionne uniquement sur les navigateurs Firefox ou Chrome.</string>\n  <string name=\"signin_view_action_text\">Terminé</string>\n  <string name=\"require_checked\">Nécessite \\\"%s\\\"</string>\n  <string name=\"msg_press_again_to_exit\">Appuyer à nouveau pour quitter</string>\n  <string name=\"player_remaining_time\">Restant : %s</string>\n  <string name=\"player_ending_time\">Se termine à %s</string>\n  <string name=\"badge_new_content\">NOUVEAU CONTENU</string>\n  <string name=\"badge_live\">EN DIRECT</string>\n  <string name=\"add_device_view_description\">Pour associer l\\'appareil, entrez ce code dans l\\'application YouTube de votre téléphone dans la section Paramètres/Regarder sur un téléviseur.</string>\n  <string name=\"playback_controls_repeat_pause\">Répéter Pause</string>\n  <string name=\"playback_controls_repeat_list\">Répéter la liste</string>\n  <string name=\"action_subscribe_off\">Se désabonner</string>\n  <string name=\"action_subscribe_on\">S\\'abonner</string>\n  <string name=\"skip_24_rate\">Sauter les formats 24 FPS (correction du mode cinéma \\?)</string>\n  <string name=\"player_disable_suggestions\">Désactiver les suggestions</string>\n  <string name=\"feedback\">Envoyer un feedback</string>\n  <string name=\"sources\">Sources</string>\n  <string name=\"releases\">Versions</string>\n  <string name=\"prefer_avc_over_vp9\">Sélection du codec : privilégier AVC à VP9</string>\n  <string name=\"open_chat\">Commentaires/Chat en direct</string>\n  <string name=\"open_comments\">Voir les commentaires</string>\n  <string name=\"place_chat_left\">Positionner le chat en direct à gauche</string>\n  <string name=\"use_alt_speech_recognizer\">Reconnaissance vocale alternative</string>\n  <string name=\"time_format\">Format de l\\'heure</string>\n  <string name=\"time_format_24\">24 heures</string>\n  <string name=\"time_format_12\">12 heures (AM/PM)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Sérieusement buggé. A utiliser uniquement si vous avez des problèmes avec le système de reconnaissance par défaut.</string>\n  <string name=\"speech_recognizer\">Reconnaissance vocale</string>\n  <string name=\"speech_recognizer_system\">Système (préféré)</string>\n  <string name=\"speech_recognizer_external_1\">Externe 1 (sérieusement buggé, il faut désactiver l\\'accès au microphone pour le faire fonctionner)</string>\n  <string name=\"speech_recognizer_external_2\">Externe 2 (sérieusement buggé)</string>\n  <string name=\"real_channel_icon\">Afficher l\\'icône de la chaîne sur le bouton du lecteur</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Texte jaune sur fond noir semi-transparent</string>\n  <string name=\"subtitle_yellow_black\">Texte jaune sur fond noir</string>\n  <string name=\"player_pixel_ratio\">Rapport d\\'aspect des pixels</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Gris foncé (monochrome)</string>\n  <string name=\"disable_mic_permission\">Veuillez désactiver l\\'accès au microphone pour l\\'application afin que ce système de reconnaissance vocale fonctionne correctement.</string>\n  <string name=\"repeat_mode_shuffle\">Lecture aléatoire de n\\'importe quelle playlist</string>\n  <string name=\"chat_left\">Positionnée à gauche</string>\n  <string name=\"chat_right\">Positionnée à droite</string>\n  <string name=\"card_real_thumbnails\">Remplacer les miniatures par une image de la vidéo</string>\n  <string name=\"card_content\">Images à afficher pour les miniatures des vidéos</string>\n  <string name=\"thumb_quality_default\">Miniature d\\'origine (par défaut)</string>\n  <string name=\"thumb_quality_start\">Image en début de vidéo</string>\n  <string name=\"thumb_quality_middle\">Image en milieu de vidéo</string>\n  <string name=\"thumb_quality_end\">Image en fin de vidéo</string>\n  <string name=\"content_block_status\">Vérifier le statut de SponsorBlock</string>\n  <string name=\"player_show_quality_info_bitrate\">Afficher le débit binaire dans les informations de qualité vidéo</string>\n  <string name=\"player_speed_button_old_behavior\">Utiliser l\\'ancien comportement du bouton de vitesse de lecture</string>\n  <string name=\"protect_settings_with_password\">Protéger tous les paramètres avec un mot de passe</string>\n  <string name=\"enter_settings_password\">Veuillez entrer le mot de passe des paramètres</string>\n  <string name=\"child_mode\">Mode enfant</string>\n  <string name=\"child_mode_desc\">Dans ce mode, la recherche ainsi que la suggestion de contenu sont désactivées. Les paramètres seront protégés par un mot de passe.</string>\n  <string name=\"lost_setting_warning\">Les paramètres de l\\'application seront modifiés. Assurez-vous d\\'avoir effectué une sauvegarde des paramètres.</string>\n  <string name=\"player_button_long_click\">Utiliser un appui court sur le bouton principal pour exécuter une action rapide (activer/désactiver les sous-titres par exemple) et un appui long pour ouvrir les paramètres.</string>\n  <string name=\"pause_history\">Suspendre l\\'historique</string>\n  <string name=\"resume_history\">Reprendre l\\'historique</string>\n  <string name=\"disable_history\">Désactiver l\\'historique</string>\n  <string name=\"enable_history\">Activer l\\'historique</string>\n  <string name=\"clear_history\">Effacer l\\'historique</string>\n  <string name=\"chapters\">Chapitres</string>\n  <string name=\"card_multiline_subtitle\">Sous-titres multilignes</string>\n  <string name=\"auto_frame_rate_modes\">Modes supportés</string>\n  <string name=\"loading\">Chargement…</string>\n  <string name=\"hide_streams\">Masquer les diffusions en direct des abonnements</string>\n  <string name=\"video_rotate\">Pivoter</string>\n  <string name=\"trending_searches\">Recherches populaires</string>\n  <string name=\"video_duration_any\">Toutes</string>\n  <string name=\"video_duration\">Durée</string>\n  <string name=\"video_duration_under_4\">Moins de 4 minutes</string>\n  <string name=\"video_duration_between_4_20\">Entre 4 et 20 minutes</string>\n  <string name=\"video_duration_over_20\">Plus de 20 minutes</string>\n  <string name=\"content_type\">Type</string>\n  <string name=\"content_type_any\">Toutes les catégories</string>\n  <string name=\"content_type_video\">Vidéo</string>\n  <string name=\"content_type_channel\">Chaîne</string>\n  <string name=\"content_type_playlist\">Playlist</string>\n  <string name=\"content_type_movie\">Film</string>\n  <string name=\"video_features\">Caractéristiques</string>\n  <string name=\"video_feature_any\">Toutes</string>\n  <string name=\"video_feature_live\">En direct</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">Trier par</string>\n  <string name=\"sort_by_relevance\">Pertinence</string>\n  <string name=\"sort_by_views\">Nombre de vues</string>\n  <string name=\"sort_by_date\">Date de mise en ligne</string>\n  <string name=\"sort_by_rating\">Notes</string>\n  <string name=\"clear_search_history\">Effacer l\\'historique de recherche</string>\n  <string name=\"remove_from_subscriptions\">Masquer</string>\n  <string name=\"player_long_speed_list\">Afficher plus d\\'options intermédiaires de vitesse de lecture</string>\n  <string name=\"alt_app_icon\">Icône d\\'application alternative (nécessite un redémarrage)</string>\n  <string name=\"network_stack\">Privilégier la pile réseau %s</string>\n  <string name=\"player_network_stack\">Pile réseau</string>\n  <string name=\"cronet_desc\">Cronet est la pile réseau Chromium. Elle peut réduire la latence et augmenter les performances réseau, ce qui peut aider à résoudre les problèmes de mise en mémoire tampon.</string>\n  <string name=\"unlock_all_formats\">Déverrouiller tous les formats vidéo</string>\n  <string name=\"unlock_all_formats_desc\">Sur certains appareils, le micrologiciel signale à tort certains formats comme non pris en charge.\\n        (par exemple, les téléviseurs intelligents 1080p ont tendance à signaler le format 4K comme non pris en charge).</string>\n  <string name=\"okhttp_desc\">Cette pile réseau est la plus lente mais apporte une bonne stabilité.</string>\n  <string name=\"select_channel_section\">Ouvrir la section correspondante lorsque l\\'application est lancée depuis les chaînes ATV</string>\n  <string name=\"enable_voice_search_desc\">L\\'application officielle sera remplacée par une \\\"passerelle\\\". Un pont permet de transférer les demandes de recherche globales vers notre application.</string>\n  <string name=\"enable_master_password\">Protéger l\\'application avec un mot de passe</string>\n  <string name=\"enter_master_password\">Veuillez entrer le mot de passe</string>\n  <string name=\"disable_stream_buffer\">Désactiver la mémoire tampon pour les diffusions en direct</string>\n  <string name=\"disable_stream_buffer_desc\">Corrige les situations où la diffusion est en retard. Avertissement : cette option peut provoquer des ralentissements du flux vidéo.</string>\n  <string name=\"sony_frame_drop_fix\">Correction de pertes d\\'images n°1</string>\n  <string name=\"sony_frame_drop_fix_desc\">Corrige les ralentissements sur les téléviseurs Sony et certains autres appareils. Avertissement : cette option peut entraîner des problèmes de synchronisation audio.</string>\n  <string name=\"audio_language\">Langue de la piste audio</string>\n  <string name=\"old_home_look\">Utiliser l\\'ancienne apparence pour la section Accueil</string>\n  <string name=\"hide_shorts_everywhere\">Masquer les shorts partout</string>\n  <string name=\"update_found\">Mise à jour</string>\n  <string name=\"volume_boost_warning\">Les réglages dépassant la limite peuvent potentiellement endommager les haut-parleurs</string>\n  <string name=\"play_video_incognito\">Lire en mode navigation privée</string>\n  <string name=\"header_kids_home\">Enfants</string>\n  <string name=\"player_section_playlist\">Utiliser le contenu de la section actuelle comme playlist</string>\n  <string name=\"header_trending\">Tendances</string>\n  <string name=\"screensaver\">Économiseur d\\'écran</string>\n  <string name=\"player_screen_off_timeout\">Délai avant extinction de l\\'écran</string>\n  <string name=\"old_update_notifications\">Utiliser l\\'ancienne apparence pour les notifications de mise à jour</string>\n  <string name=\"dialog_notification\">Avertir des nouvelles mises à jour à l\\'aide d\\'une boîte de dialogue contextuelle</string>\n  <string name=\"mark_as_watched\">Marquer comme vue</string>\n  <string name=\"player_ui_animations\">Activer l\\'animation de la barre de contrôles</string>\n  <string name=\"player_likes_count\">Afficher le nombre de \\\"J\\'aime\\\" et \\\"Je n\\'aime pas\\\"</string>\n  <string name=\"sorting_alphabetically2\">Ordre alphabétique (rapide)</string>\n  <string name=\"sorting_default\">Par défaut (rapide)</string>\n  <string name=\"content_block_exclude_channel\">Exclure cette chaîne de SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">Ne plus exclure cette chaîne de SponsorBlock</string>\n  <string name=\"player_chapter_notification\">Afficher une notification pour avancer rapidement dans les chapitres</string>\n  <string name=\"subtitle_remember\">Mémoriser l\\'activation des sous-titres pour chaque chaîne</string>\n  <string name=\"amazon_frame_drop_fix\">Correction de pertes d\\'images n°2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">Corrige les ralentissements sur les appareils Amazon Fire TV Stick et certains autres appareils.</string>\n  <string name=\"default_stack_desc\">Pile réseau intégrée. Cette pile peut apporter une meilleure stabilité dans certaines situations.</string>\n  <string name=\"item_postion\">Position de : </string>\n  <string name=\"player_screen_off_dimming\">Réduction de la luminosité de l\\'écran</string>\n  <string name=\"screensaver_timout\">Délai d\\'activation de l\\'économiseur d\\'écran</string>\n  <string name=\"screensaver_dimming\">Réduction de la luminosité de l\\'économiseur d\\'écran</string>\n  <string name=\"player_ui_on_next\">Afficher l\\'interface du lecteur lors du passage à la vidéo suivante</string>\n  <string name=\"autogenerated\">générés automatiquement</string>\n  <string name=\"player_auto_volume\">Ajustement automatique du volume</string>\n  <string name=\"header_shorts\">Shorts</string>\n  <string name=\"player_global_focus\">Navigation continue entre les rangées de boutons du lecteur</string>\n  <string name=\"auto_history\">Auto (utiliser les paramètres du compte)</string>\n  <string name=\"remember_position_subscriptions\">Mémoriser la dernière position consultée dans les abonnements</string>\n  <string name=\"msg_player_unknown_error\">Erreur inconnue</string>\n  <string name=\"header_notifications\">Notifications</string>\n  <string name=\"disable_search_history\">Désactiver l\\'historique de recherche</string>\n  <string name=\"unlock_high_bitrate_formats\">Débloquez les formats vp9 1080p à haut débit</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Débloquez les formats mp4a à haut débit</string>\n  <string name=\"color_scheme_blue\">Bleu</string>\n  <string name=\"color_scheme_dark_blue\">Bleu foncé</string>\n  <string name=\"player_speed_per_channel\">Pour chaque canal</string>\n  <string name=\"disable_popular_searches\">Désactiver les requêtes de recherche populaires</string>\n  <string name=\"multi_profiles\">Utiliser des paramètres distincts pour chaque compte</string>\n  <string name=\"protect_account_with_password\">Protégez ce compte avec un mot de passe</string>\n  <string name=\"enter_account_password\">Entrez le mot de passe du compte</string>\n  <string name=\"show_connect_messages\">Afficher les messages de connexion</string>\n  <string name=\"prefer_avc_over_vp9_desc\">ATT: 1080p maximum</string>\n  <string name=\"share_qr_link\">Lien de partage (QR code)</string>\n  <string name=\"app_corner_clock\">Accueil: horloge en haut à droite</string>\n  <string name=\"player_corner_clock\">Lecteur: horloge en haut à droite</string>\n  <string name=\"player_corner_ending_time\">Lecteur: heure de fin dans le coin supérieur droit</string>\n  <string name=\"hide_shorts_channel\">Masquer les shorts de la chaîne</string>\n  <string name=\"news_row_name\">Nouvelles</string>\n  <string name=\"hide_upcoming_channel\">Masquer les nouvelles de la chaîne</string>\n  <string name=\"hide_upcoming_home\">Masquer les nouvelles depuis la page d’accueil</string>\n  <string name=\"hide_shorts_from_trending\">Masquer les shorts des tendances</string>\n  <string name=\"play_next\">Lire ensuite</string>\n  <string name=\"hide_watched_from_subscriptions\">Masquer les vidéos regardées des abonnements</string>\n  <string name=\"hide_unwanted_content\">Masquer le contenu indésirable</string>\n  <string name=\"hide_watched_from_home\">Masquer les vidéos regardées depuis l\\'accueil</string>\n  <string name=\"remote_control_permission\">Le contrôle à distance en arrière-plan nécessite l’autorisation de superposition</string>\n  <string name=\"login_from_browser\">Se connecter à partir du navigateur</string>\n  <string name=\"disable_remote_history\">Désactiver l’historique lors de l’utilisation de la télécommande</string>\n  <string name=\"keyboard_fix\">Correction de l’affichage du clavier par la pression de la touche OK (G20s et autres)</string>\n  <string name=\"nothing_found\">Rien n’a été trouvé</string>\n  <string name=\"auto_frame_rate_desc\">Cette option supprime les tremblements sur les scènes où la caméra se déplace rapidement, par exemple le streaming sportif</string>\n  <string name=\"channel_filter_hint\">Filtrer les chaînes</string>\n  <string name=\"player_loop_shorts\">Short en boucle</string>\n  <string name=\"hide_watched_from_notifications\">Masquer les vidéos regardées dans les notifications</string>\n  <string name=\"remember_position_of_live_videos\">Mémoriser la position de la diffusion en direct</string>\n  <string name=\"pin_channel\">Ajouter une chaîne à la barre latérale</string>\n  <string name=\"pin_playlist\">Ajouter une playlist à la barre latérale</string>\n  <string name=\"channels_filter\">Filtrer la liste des chaînes dans la section Chaînes</string>\n  <string name=\"player_quick_shorts_skip\">Sauter les Shorts avec les boutons gauche/droite</string>\n  <string name=\"channel_search_bar\">Barre de recherche à l’intérieur de la page Chaîne</string>\n  <string name=\"header_sports\">Sports\t</string>\n  <string name=\"keep_finished_activities\">Conserver les activités terminées</string>\n  <string name=\"disable_channels_service\">Désactiver le service Chaînes</string>\n  <string name=\"replace_titles\">Remplacer les titres</string>\n  <string name=\"enable\">Activer</string>\n  <string name=\"more_info\">Plus d\\'informations</string>\n  <string name=\"about_sponsorblock\">À propos de SponsorBlock</string>\n  <string name=\"about_dearrow\">À propos de DeArrow</string>\n  <string name=\"replace_thumbnails\">Remplacer les vignettes</string>\n  <string name=\"crowdsourced_thumbnails\">Vignettes participatives</string>\n  <string name=\"crowdsoursed_titles\">Titres participatifs</string>\n  <string name=\"dearrow_not_submitted_thumbs\">Où récupérer les vignettes non soumises</string>\n  <string name=\"pitch_effect\">Effet de hauteur</string>\n  <string name=\"action_sound_off\">Son désactivé</string>\n  <string name=\"hide_watched_from_watch_later\">Masquer les vidéos regardées de la playlist À regarder plus tard</string>\n  <string name=\"fullscreen_mode\">Mode plein écran (sans barres système)</string>\n  <string name=\"player_only_mode\">Afficher uniquement le lecteur si la vidéo est ouverte en dehors de l’application</string>\n  <string name=\"pinned_channel_rows\">Afficher la chaîne épinglé sous forme de lignes</string>\n  <string name=\"prefer_ipv4\">Privilégiez le DNS IPv4</string>\n  <string name=\"prefer_ipv4_desc\">Peut résoudre les situations où l’application ne fonctionne pas du tout.\\nRemarque. Peut provoquer des blocages et des plantages (en particulier sur les appareils Android 8 ou Dune HD)</string>\n  <string name=\"long_press_for_settings\">APPUYEZ LONGUEMENT POUR OUVRIR LES PARAMÈTRES</string>\n  <string name=\"remember_position_pinned\">Mémoriser la dernière position consultée dans les playlists épinglées</string>\n  <string name=\"old_channel_look\">Ancienne apparence de la page Chaîne</string>\n  <string name=\"player_extra_long_speed_list\">Liste de vitesse extra longue</string>\n  <string name=\"dearrow_status\">Vérifier l’état du serveur DeArrow</string>\n  <string name=\"skip_shorts\">Sauter les Shorts</string>\n  <string name=\"save_playlist\">Ajouter une playlist à la section Playlists</string>\n  <string name=\"player_exit_shortcut\">Quitter le lecteur</string>\n  <string name=\"device_specific_backup\">Sauvegarde pour cet appareil uniquement</string>\n  <string name=\"local_backup\">Sauvegarde locale</string>\n  <string name=\"long_press_for_options\">APPUYEZ LONGUEMENT POUR OUVRIR LES OPTIONS</string>\n  <string name=\"auto_backup\">Sauvegarde automatique (une fois par jour)</string>\n  <string name=\"repeat_mode_reverse_list\">Lire la playlist ou les vidéos de la chaîne dans l\\'ordre inverse</string>\n  <string name=\"player_quick_skip_videos\">Ignorer les vidéos classiques avec les boutons gauche/droite</string>\n  <string name=\"speech_engine\">Moteur de recherche vocale</string>\n  <string name=\"unknown_source_error\">Erreur de source inconnue</string>\n  <string name=\"unknown_renderer_error\">Erreur de rendu inconnue</string>\n  <string name=\"calm_msg\">Nous sommes en train de résoudre le problème. Vérifiez les mises à jour de temps à autre</string>\n  <string name=\"without_picture\">Sans image</string>\n  <string name=\"video_disabled\">Vidéo désactivée</string>\n  <string name=\"applying_fix\">Ne fermez pas le lecteur. Application du correctif...</string>\n  <string name=\"hide_mixes\">Masquer les mixes</string>\n  <string name=\"player_global_focus_desc\">Cette fonctionnalité affecte le bouton du lecteur qui recevra le focus lors de la navigation entre les rangées de boutons du lecteur</string>\n  <string name=\"disable_network_error_fixing\">Désactiver la correction automatique des erreurs réseau</string>\n  <string name=\"disable_network_error_fixing_desc\">Vous devrez probablement activer cette option si vous utilisez un VPN</string>\n  <string name=\"video_buffer_size_lowest\">Au plus bas</string>\n  <string name=\"recommended\">Recommandé</string>\n  <string name=\"add_to_subscriptions_group\">Ajouter/Supprimer du groupe d\\'abonnement</string>\n  <string name=\"new_subscriptions_group\">Nouveau groupe</string>\n  <string name=\"rename_group\">Renommer le groupe d\\'abonnement</string>\n  <string name=\"unpin_group_from_sidebar\">Supprimer le groupe d\\'abonnement</string>\n  <string name=\"screen_dimming_amount\">Quantité d\\'assombrissement de l\\'écran</string>\n  <string name=\"screen_dimming_timeout\">Délai d\\'assombrissement de l\\'écran</string>\n  <string name=\"video_buffer_size_highest\">Maximum</string>\n  <string name=\"place_comments_left\">Placer les commentaires à gauche</string>\n  <string name=\"playlists_rows\">Afficher la section Playlists sous forme de lignes</string>\n  <string name=\"not_supported_by_device\">L\\'appareil ne prend pas en charge cette fonctionnalité</string>\n  <string name=\"hide_shorts_from_search\">Masquer les shorts dans les résultats de recherche</string>\n  <string name=\"suggestions\">Suggestions</string>\n  <string name=\"import_subscriptions_group\">Importer</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">Désactiver le sous-titrage codé</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">Activer le sous-titrage codé</string>\n  <string name=\"context_menu_sorting\">Tri du menu contextuel</string>\n  <string name=\"video_flip\">Retournement (miroir)</string>\n  <string name=\"my_videos\">Mes vidéos</string>\n  <string name=\"player_audio_focus\">Focus audio (pause si d\\'autres lecteurs sont détectés)</string>\n  <string name=\"paid_content_notification\">Notification de contenu payant</string>\n  <string name=\"you_liked\">vous avez aimé</string>\n  <string name=\"playback_buffering_fix\">Correction de la mise en mémoire tampon</string>\n  <string name=\"premium_users_only\">Réservé aux utilisateurs Premium. Correction de la liste incomplète des formats vidéo.</string>\n  <string name=\"oculus_quest_fix\">Correction pour Oculus Quest</string>\n  <string name=\"card_preview_muted\">Vidéo sans son</string>\n  <string name=\"card_preview_full\">Vidéo avec son</string>\n  <string name=\"card_preview\">Aperçus des tuiles</string>\n  <string name=\"card_unlocalized_titles\">Titres originals des tuiles</string>\n  <string name=\"search_exit_shortcut\">Quitter la recherche</string>\n  <string name=\"original_lang\">Original</string>\n  <string name=\"remove_playlist_fmt\">Supprimer %s définitivement\\?</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">Ne pas ajuster la vidéo à la fenêtre</string>\n  <string name=\"typing_corrections\">Désactiver la correction automatique lors de la saisie de texte</string>\n  <string name=\"play_from_start\">Lire depuis le début</string>\n  <string name=\"player_chapter_notification2\">Passez au chapitre suivant en cliquant sur la notification</string>\n  <string name=\"suggestions_horizontally_scrolled\">Suggestions défilant horizontalement</string>\n  <string name=\"stable_restore\">Restaurer la version stable (toutes les données seront perdues)</string>\n  <string name=\"auto_backup_category\">Sauvegarde automatique</string>\n  <string name=\"once_a_day\">Une fois par jour</string>\n  <string name=\"once_a_week\">Une fois par semaine</string>\n  <string name=\"once_a_month\">Une fois par mois</string>\n  <string name=\"player_toggle_speed\">Basculer la vitesse marche/arrêt</string>\n  <string name=\"create_playlist_note\">cela ne sera pas visible dans l’application YouTube</string>\n  <string name=\"player_quick_shorts_skip_alt\">Naviguer entre les Shorts avec les boutons haut/bas</string>\n  <string name=\"player_quick_skip_videos_alt\">Naviguer entre les vidéos standards avec les boutons haut/bas</string>\n  <string name=\"prefer_google_dns\">Privilégier les DNS Google</string>\n  <string name=\"dialog_block_channel\">Mettre sur liste noire</string>\n  <string name=\"dialog_unblock_channel\">Retirer de la liste noire</string>\n  <string name=\"confirm_block_channel\">Masquer tout le contenu de %s?</string>\n  <string name=\"channel_blocked\">Chaîne mise sur liste noire</string>\n  <string name=\"channel_unblocked\">Retiré de la liste noire</string>\n  <string name=\"header_blocked_channels\">Chaînes sur liste noire</string>\n  <string name=\"msg_no_blocked_channels\">Aucune chaîne sur liste noire</string>\n  <string name=\"action_debug_info\">Informations de débogage</string>\n  <string name=\"player_time_stretching\">Étirement temporel audio</string>\n  <string name=\"player_time_stretching_desc\">Conserve une voix naturelle lors du changement de vitesse. Peut réduire significativement les performances sur certains appareils.</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-gl-rES/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Inicio</string>\n  <string name=\"title_search\">Buscar</string>\n  <string name=\"header_subscriptions\">Subscricións</string>\n  <string name=\"header_history\">Historial</string>\n  <string name=\"header_music\">Música</string>\n  <string name=\"header_news\">Noticias</string>\n  <string name=\"header_gaming\">Xogos</string>\n  <string name=\"header_playlists\">Listaxes de reprodución</string>\n  <string name=\"header_settings\">Axustes</string>\n  <string name=\"header_channels\">Canles</string>\n  <string name=\"subscriptions_signin_title\">Consulta as novidades das canles que che gustan</string>\n  <string name=\"subscriptions_signin_subtitle\">Inicia sesión para ver as túas subscricións</string>\n  <string name=\"subscriptions_signin_button_text\">COMEZAR SESIÓN</string>\n  <string name=\"library_signin_to_show_more\">Mira os vídeos que che gustaron, gardaches ou te subscribiches</string>\n  <string name=\"library_signin_subtitle\">Comeza sesión para ver a túa biblioteca</string>\n  <string name=\"action_signin\">COMEZAR SESIÓN</string>\n  <string name=\"title_video_formats\">Formatos de vídeo</string>\n  <string name=\"title_audio_formats\">Formatos de son</string>\n  <string name=\"subtitle_category_title\">Subtítulos</string>\n  <string name=\"playback_queue_category_title\">Cola de reprodución</string>\n  <string name=\"video_max_quality\">Automático (calidade máxima)</string>\n  <string name=\"audio_max_quality\">Automático (calidade máxima)</string>\n  <string name=\"subtitles_disabled\">Subtítulos desactivados</string>\n  <string name=\"auto_frame_rate\">Velocidade de fotogramas automática</string>\n  <string name=\"frame_rate_correction\">Corrixir fps:\\n%s</string>\n  <string name=\"category_background_playback\">Reprodución en segundo plano</string>\n  <string name=\"not_implemented\">Non implementado</string>\n  <string name=\"option_background_playback_off\">Desactivado</string>\n  <string name=\"option_background_playback_pip\">Imaxe na imaxe</string>\n  <string name=\"option_background_playback_only_audio\">Só son</string>\n  <string name=\"playback_settings\">Axustes de calidade de reprodución</string>\n  <string name=\"video_speed\">Velocidade do vídeo</string>\n  <string name=\"resolution_switch\">Trocar resolución</string>\n  <string name=\"video_buffer\">Memoria intermedia do vídeo</string>\n  <string name=\"video_buffer_size_none\">Ningunha</string>\n  <string name=\"video_buffer_size_low\">Baixa</string>\n  <string name=\"video_buffer_size_med\">Media</string>\n  <string name=\"video_buffer_size_high\">Alta</string>\n  <string name=\"update_changelog\">Rexistro de ´trocos</string>\n  <string name=\"install_update\">Instalar actualización</string>\n  <string name=\"section_is_empty\">Vaia! Non hai nada aquí</string>\n  <string name=\"dialog_add_to_playlist\">Engadir/Eliminar da lista de reprodución</string>\n  <string name=\"msg_signed_users_only\">Só usuarios rexistrados</string>\n  <string name=\"msg_cant_load_content\">Non se pode cargar o contido.\\n1) Activa o historial de visualización.\\n2) Comproba a data e a hora no dispositivo.\\n3) Comproba a conexión de rede.\\n4) Comproba a configuración do teu proxy.\\n5) Tenta iniciar sesión na túa conta .\\n6) Quizais non haxa nada aquí.</string>\n  <string name=\"title_video_presets\">axustes predefinidos de vídeo</string>\n  <string name=\"settings_accounts\">Contas</string>\n  <string name=\"settings_left_panel\">Editar categorías</string>\n  <string name=\"settings_themes\">Temas</string>\n  <string name=\"settings_other\">Outros</string>\n  <string name=\"settings_player\">Reprodutor</string>\n  <string name=\"settings_language\">Linguaxe</string>\n  <string name=\"settings_linked_devices\">Dispositivos vinculados</string>\n  <string name=\"settings_about\">Sobre</string>\n  <string name=\"dialog_account_list\">Escolla conta</string>\n  <string name=\"dialog_account_none\">Ningunha</string>\n  <string name=\"dialog_remove_account\">Pechar sesión</string>\n  <string name=\"dialog_add_account\">comezar sesión</string>\n  <string name=\"default_lang\">Por defecto</string>\n  <string name=\"dialog_select_language\">Linguaxe</string>\n  <string name=\"subtitle_default\">Por defecto</string>\n  <string name=\"subtitle_white_semi_transparent\">Branco sobre fondo semitransparente</string>\n  <string name=\"subtitle_style\">Estilo dos subtítulos</string>\n  <string name=\"subtitle_language\">Lingua dos subtítulos</string>\n  <string name=\"action_search\">Buscar</string>\n  <string name=\"settings_main_ui\">Interface de usuario</string>\n  <string name=\"dialog_main_ui\">Interface de usuario</string>\n  <string name=\"card_animated_previews\">Vistas previas animadas</string>\n  <string name=\"web_site\">Sitio web</string>\n  <string name=\"donation\">Doazón</string>\n  <string name=\"dialog_about\">Sobre</string>\n  <string name=\"dialog_player_ui\">Reproductor de vídeo</string>\n  <string name=\"player_show_ui_on_pause\">Amosar a interface de usuario en pausa</string>\n  <string name=\"player_pause_on_ok\">A tecla Aceptar detén a reprodución</string>\n  <string name=\"player_ok_button_behavior\">Comportamento do botón Aceptar</string>\n  <string name=\"player_only_ui\">Só IU</string>\n  <string name=\"player_ui_and_pause\">IU e pausa</string>\n  <string name=\"player_only_pause\">Só pausa</string>\n  <string name=\"check_for_updates\">Comproba se hai actualizacións</string>\n  <string name=\"update_not_found\">Estás a empregar a derradeira versión</string>\n  <string name=\"update_in_progress\">Agarda…</string>\n  <string name=\"player_ui_hide_behavior\">Ocultar automaticamente a IU</string>\n  <string name=\"option_never\">Xamais</string>\n  <string name=\"side_panel_sections\">seccións de axuste</string>\n  <string name=\"boot_to_section\">Arrancar a sección</string>\n  <string name=\"large_ui\">IU grande</string>\n  <string name=\"video_grid_scale\">Escala de reixa de vídeo</string>\n  <string name=\"scale_ui\">Escala de IU</string>\n  <string name=\"color_scheme\">Esquema de cores</string>\n  <string name=\"color_scheme_default\">Por defecto</string>\n  <string name=\"color_scheme_red_grey\">Vermello-gris</string>\n  <string name=\"color_scheme_red\">Vermello</string>\n  <string name=\"color_scheme_dark_grey\">Gris escuro</string>\n  <string name=\"disable_update_check\">Desactivar a verificación de actualizacións</string>\n  <string name=\"show_again\">Amosar de novo</string>\n  <string name=\"check_updates_auto\">Notificar sobre actualizacións</string>\n  <string name=\"select_account_on_boot\">Amosar a selección de conta no arranque</string>\n  <string name=\"player_other\">Miscelánea</string>\n  <string name=\"player_full_date\">Data precisa na descrición</string>\n  <string name=\"open_channel\">Abrir canle</string>\n  <string name=\"open_playlist\">Abrir lista de reprodución</string>\n  <string name=\"not_interested\">Non interesado</string>\n  <string name=\"not_recommend_channel\">Non recomendar a canle</string>\n  <string name=\"you_wont_see_this_video\">Eliminouse o vídeo dos recomendados</string>\n  <string name=\"you_wont_see_this_channel\">Non verás esta canle nas recomendacións</string>\n  <string name=\"settings_search\">Busca</string>\n  <string name=\"dialog_search\">Busca</string>\n  <string name=\"instant_voice_search\">Busca instantánea por voz</string>\n  <string name=\"option_background_playback_behind\">Reproducir de fondo</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">A información do seguinte vídeo aínda non está cargada</string>\n  <string name=\"card_multiline_title\">Títulos multiliña</string>\n  <string name=\"cards_style\">Estilo das miniaturas</string>\n  <string name=\"repeat_mode_all\">Reproduce vídeos continuamente</string>\n  <string name=\"repeat_mode_one\">Repite o vídeo actual</string>\n  <string name=\"repeat_mode_pause\">Pausa a reprodución despois de cada vídeo</string>\n  <string name=\"repeat_mode_pause_alt\">Reproduce só vídeos da lista de reprodución continuamente</string>\n  <string name=\"repeat_mode_none\">Detén a reprodución despois dun vídeo</string>\n  <string name=\"subscribed_to_channel\">Subscrito</string>\n  <string name=\"unsubscribed_from_channel\">Cancelouse a subscrición</string>\n  <string name=\"subtitle_yellow_transparent\">Amarelo sobre fondo transparente</string>\n  <string name=\"subtitle_white_transparent\">Branco sobre fondo transparente</string>\n  <string name=\"subtitle_white_black\">Branco sobre fondo negro</string>\n  <string name=\"player_seek_preview\">Vista previa mentres busca</string>\n  <string name=\"color_scheme_dark_grey_oled\">Gris escuro (OLED)</string>\n  <string name=\"channels_section_sorting\">ordenación das seccións de canles</string>\n  <string name=\"sorting_by_new_content\">Novo contido</string>\n  <string name=\"sorting_alphabetically\">Alfabéticamente</string>\n  <string name=\"sorting_last_viewed\">Derradeira visualización</string>\n  <string name=\"player_pause_when_seek\">Pausa ao buscar</string>\n  <string name=\"playlists_style\">Estilo de sección de listas de reprodución</string>\n  <string name=\"playlists_style_grid\">Grade</string>\n  <string name=\"playlists_style_rows\">Filas</string>\n  <string name=\"player_show_clock\">Amosa o reloxo na barra de controis</string>\n  <string name=\"player_show_remaining_time\">Amosa o tempo restante na barra de controis</string>\n  <string name=\"player_show_ending_time\">Amosa a hora de finalización na barra de controis</string>\n  <string name=\"open_channel_uploads\">subidas de canles abertas</string>\n  <string name=\"app_exit_shortcut\">Saír da aplicación</string>\n  <string name=\"player_exit_shortcut\">Saír do reprodutor</string>\n  <string name=\"app_exit_none\">Non saír</string>\n  <string name=\"app_double_back_exit\">Dobre atrás</string>\n  <string name=\"app_single_back_exit\">volver ao anterior</string>\n  <string name=\"action_video_zoom\">Ampliación de vídeo</string>\n  <string name=\"video_zoom\">Ampliación de vídeo</string>\n  <string name=\"video_zoom_default\">Por defecto</string>\n  <string name=\"video_zoom_fit_both\">Axustar anchura ou altura</string>\n  <string name=\"video_zoom_fit_height\">Axustar altura</string>\n  <string name=\"video_zoom_fit_width\">Axustar anchura</string>\n  <string name=\"video_zoom_stretch\">Estirar</string>\n  <string name=\"color_scheme_teal\">Verde azulado</string>\n  <string name=\"color_scheme_teal_oled\">Verde azulado (OLED)</string>\n  <string name=\"player_seek_preview_none\">Desactivado</string>\n  <string name=\"player_seek_preview_single\">fotograma único</string>\n  <string name=\"player_seek_preview_carousel\">Carrusel</string>\n  <string name=\"player_seek_preview_carousel_slow\">Carrusel (lento, por fotogramas clave)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Carrusel (rápido, non precisa vista previa)</string>\n  <string name=\"unsubscribe_from_channel\">Cancelar a subscrición da canle</string>\n  <string name=\"card_title_lines_num\">Título da miniatura núm de liñas</string>\n  <string name=\"card_auto_scrolled_title\">Título recortado con desprazamento automático</string>\n  <string name=\"share_link\">Compartir ligazón</string>\n  <string name=\"share_qr_link\">Compartir ligazón (código QR)</string>\n  <string name=\"subscribe_to_channel\">Subscríbete a canle</string>\n  <string name=\"wait_data_loading\">Agarde mentres se cargan os datos...</string>\n  <string name=\"auto_frame_rate_pause\">Velocidade de fotogramas automática (pausa ao trocar)</string>\n  <string name=\"auto_frame_rate_applying\">Aplicando a velocidade de fotogramas automática %sx%s\\@%s</string>\n  <string name=\"audio_shift\">desfase de son</string>\n  <string name=\"player_remember_speed\">Lembra a velocidade</string>\n  <string name=\"mark_channel_as_watched\">Marcar como visto</string>\n  <string name=\"channel_marked_as_watched\">A canle marcouse como vista</string>\n  <string name=\"dialog_add_device\">Engadir dispositivo</string>\n  <string name=\"dialog_remove_all_devices\">Elimina todos os dispositivos</string>\n  <string name=\"device_link_enabled\">Ligazón activada</string>\n  <string name=\"device_connected\">O dispositivo \\\"%s\\\" conectouse</string>\n  <string name=\"device_disconnected\">O dispositivo \\\"%s\\\" foi desconectado</string>\n  <string name=\"settings_ui_scale\">Escala de IU</string>\n  <string name=\"msg_restart_app\">Reinicia a aplicación para aplicar estes axustes</string>\n  <string name=\"settings_block\">Bloqueo de contido</string>\n  <string name=\"msg_applying\">Aplicando %s</string>\n  <string name=\"msg_done\">Feito</string>\n  <string name=\"settings_general\">Xeral</string>\n  <string name=\"settings_video\">Vídeo</string>\n  <string name=\"content_block_confirm_skip\">Confirmar ao saltar</string>\n  <string name=\"content_block_categories\">Saltar segmentos</string>\n  <string name=\"content_block_sponsor\">Patrocinador</string>\n  <string name=\"content_block_intro\">Animación de intermedio/intro</string>\n  <string name=\"content_block_outro\">Tarxetas/créditos</string>\n  <string name=\"content_block_interaction\">Recordatorio de interacción (subscribirse)</string>\n  <string name=\"content_block_self_promo\">Autopromoción non remunerada</string>\n  <string name=\"content_block_music_off_topic\">Sección non musical do clip</string>\n  <string name=\"confirm_segment_skip\">Omitir o segmento \\\"%s\\\"\\?</string>\n  <string name=\"cancel_segment_skip\">Cancelar o salto de segmento</string>\n  <string name=\"msg_skipping_segment\">Saltando o segmento \\\"%s\\\"...</string>\n  <string name=\"content_block_notification_type\">Tipo de notificación</string>\n  <string name=\"content_block_notify_none\">Sen notificación</string>\n  <string name=\"content_block_notify_toast\">Mensaxe emerxente</string>\n  <string name=\"content_block_notify_dialog\">Diálogo de confirmación</string>\n  <string name=\"return_to_launcher\">Voltar ao lanzador desde canles/búsqueda de ATV</string>\n  <string name=\"intent_force_close\">Forzar a saída se o vídeo se abriu desde unha fonte externa</string>\n  <string name=\"btn_confirm\">Confirmar</string>\n  <string name=\"player_low_video_quality\">Baixa calidade de vídeo</string>\n  <string name=\"remote_session_closed\">Pechouse a sesión remota</string>\n  <string name=\"msg_mode_switch_error\">Non se pode trocar o modo de visualización a \\\"%s</string>\n  <string name=\"msg_player_error\">Produciuse un erro do reprodutor %s. Reiniciando a reprodución...</string>\n  <string name=\"video_preset_disabled\">Sen predefinido</string>\n  <string name=\"video_preset_enabled\">O formato do seguinte vídeo establecerase segundo o preestablecido</string>\n  <string name=\"player_sleep_timer\">Temporizador de durmir</string>\n  <string name=\"player_show_quality_info\">Mostra información de calidade na barra de controis</string>\n  <string name=\"player_remember_each_speed\">Lembra cada velocidade de vídeo</string>\n  <string name=\"player_remember_speed_none\">Ningún</string>\n  <string name=\"player_remember_speed_all\">O mesmo en todos os vídeos</string>\n  <string name=\"player_remember_speed_each\">Por cada vídeo</string>\n  <string name=\"msg_player_error_source\">Non se pode descargar o vídeo</string>\n  <string name=\"msg_player_error_renderer\">O formato escollido non é compatible.\\nTenta escoller outro.\\nSe isto non axuda, tenta reiniciar o dispositivo.</string>\n  <string name=\"msg_player_error_video_renderer\">O formato de VIDEO escollido non é compatible.\\nTenta escoller outro premendo o botón HQ do reprodutor.\\nSe isto non axuda, tenta reiniciar o dispositivo.</string>\n  <string name=\"msg_player_error_audio_renderer\">O formato de AUDIO escollido non é compatible.\\nTenta escoller outro premendo o botón HQ do reprodutor.\\nSe isto non axuda, tenta reiniciar o dispositivo.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">O formato de SUBTÍTULO escollido non é compatible.\\nTenta escoller outro premendo durante moito tempo o botón CC do reprodutor.\\nSe isto non axuda, tenta reiniciar o dispositivo.</string>\n  <string name=\"msg_player_error_unexpected\">Erro descoñecido no decodificador de vídeo</string>\n  <string name=\"video_aspect\">Relación de aspecto</string>\n  <string name=\"player_tweaks\">Opcións de programador</string>\n  <string name=\"header_uploads\">Cargas</string>\n  <string name=\"player_show_global_clock\">Amosa o reloxo sempre en pantalla</string>\n  <string name=\"player_show_global_ending_time\">Amosa a hora de finalización sempre en pantalla</string>\n  <string name=\"app_corner_clock\">Inicio: reloxo da esquina superior dereita</string>\n  <string name=\"player_corner_clock\">Reprodutor: reloxo da esquina superior dereita</string>\n  <string name=\"player_corner_ending_time\">Reprodutor: hora de finalización da esquina superior dereita</string>\n  <string name=\"uploads_old_look\">Reverte o aspecto antigo da sección Cargas</string>\n  <string name=\"background_playback_activation\">Reprodución en segundo plano (activación)</string>\n  <string name=\"channels_old_look\">Reverte o aspecto antigo da sección Canles</string>\n  <string name=\"channels_auto_load\">Carga automática de contido da sección Canles</string>\n  <string name=\"return_to_background_video\">Volta ao vídeo en segundo plano</string>\n  <string name=\"pin_unpin_from_sidebar\">Engadir/Eliminar da barra lateral</string>\n  <string name=\"pin_unpin_playlist\">Engadir/eliminar lista de reprodución da barra lateral</string>\n  <string name=\"pin_unpin_channel\">Engadir/eliminar canle da barra lateral</string>\n  <string name=\"pin_playlist\">Engade lista de reprodución á barra lateral</string>\n  <string name=\"pin_channel\">Engade canle á barra lateral</string>\n  <string name=\"pinned_to_sidebar\">Engadida á barra lateral</string>\n  <string name=\"unpin_from_sidebar\">Eliminar da barra lateral</string>\n  <string name=\"unpinned_from_sidebar\">Eliminado da barra lateral</string>\n  <string name=\"dialog_select_country\">País</string>\n  <string name=\"settings_language_country\">Lingua/País</string>\n  <string name=\"share_embed_link\">Compartir ligazón para inserir</string>\n  <string name=\"card_text_scroll_factor\">velocidade de desprazamento do texto da miniatura</string>\n  <string name=\"preferred_update_source\">escolle a fonte de actualización</string>\n  <string name=\"hide_shorts\">Ocultar curtas das subscricións</string>\n  <string name=\"hide_shorts_channel\">Ocultar curtas dunha canle</string>\n  <string name=\"key_remapping\">Reasignación de claves</string>\n  <string name=\"screen_dimming\">Atenuación da pantalla</string>\n  <string name=\"removed_from_playback_queue\">Quitouse da cola de reprodución</string>\n  <string name=\"added_to_playback_queue\">Engadida á cola de reprodución</string>\n  <string name=\"add_remove_from_playback_queue\">Engadir/Eliminar da cola de reprodución</string>\n  <string name=\"add_to_playback_queue\">Engadir á cola de reprodución</string>\n  <string name=\"remove_from_playback_queue\">Eliminar da cola de reprodución</string>\n  <string name=\"proxy_enabled\">Proxy habilitado</string>\n  <string name=\"proxy_disabled\">Proxy desactivado</string>\n  <string name=\"uploads_row_name\">Cargas</string>\n  <string name=\"playlists_row_name\">Listas de reprodución creadas</string>\n  <string name=\"popular_uploads_row_name\">Cargas populares</string>\n  <string name=\"news_row_name\">Noticias</string>\n  <string name=\"breaking_news_row_name\">Noticias de última hora</string>\n  <string name=\"covid_news_row_name\">Noticias sobre a covid-19</string>\n  <string name=\"enable_voice_search\">Activar a busca global (necesita soporte de firmware)</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Subscribirse/Cancelar a subscrición da canle</string>\n  <string name=\"app_backup_restore\">Copia de seguranza/Restauración</string>\n  <string name=\"app_backup\">Copia de seguranza dos datos da aplicación</string>\n  <string name=\"app_restore\">Restaurar datos da aplicación</string>\n  <string name=\"skip_each_segment_once\">Non voltar saltar segmentos</string>\n  <string name=\"disable_ok_long_press\">Desactivar o botón Aceptar mantendo presionado</string>\n  <string name=\"disable_ok_long_press_desc\">Destinado a mandos con erros nos que o botón Aceptar non funciona correctamente</string>\n  <string name=\"player_time_correction\">Amosar o tempo con respecto á velocidade</string>\n  <string name=\"sponsor_color_markers\">Marcadores de cores na barra de progreso</string>\n  <string name=\"refresh_section\">Actualizar sección</string>\n  <string name=\"option_disabled\">Desactivado</string>\n  <string name=\"live_now_row_name\">Ao vivo agora</string>\n  <string name=\"run_in_background\">Reprodución en segundo plano</string>\n  <string name=\"settings_remote_control\">Control remoto</string>\n  <string name=\"background_service_started\">Iniciouse o servizo en segundo plano</string>\n  <string name=\"dialog_add_to\">Engadir a %s</string>\n  <string name=\"dialog_remove_from\">Eliminar de %s</string>\n  <string name=\"added_to\">Engadido a %s</string>\n  <string name=\"removed_from\">Eliminado de %s</string>\n  <string name=\"video_preset_adaptive\">Adaptativo</string>\n  <string name=\"content_block_preview_recap\">Vista previa ou resumo do vídeo</string>\n  <string name=\"content_block_highlight\">Punto de interese (marcador)</string>\n  <string name=\"removed_from_history\">Eliminouse o vídeo do historial</string>\n  <string name=\"remove_from_history\">Eliminar do historial</string>\n  <string name=\"upload_date\">Data de carga</string>\n  <string name=\"upload_date_any\">Todo o tempo</string>\n  <string name=\"upload_date_today\">Hoxe</string>\n  <string name=\"upload_date_this_week\">Esta semana</string>\n  <string name=\"upload_date_this_month\">Este mes</string>\n  <string name=\"upload_date_this_year\">Este ano</string>\n  <string name=\"double_refresh_rate\">Dobre taxa de actualización</string>\n  <string name=\"upload_date_last_hour\">Última hora</string>\n  <string name=\"focus_on_search_results\">Enfoque automático nos resultados da busca</string>\n  <string name=\"context_menu\">Menú contextual</string>\n  <string name=\"add_remove_from_recent_playlist\">Engadir/Eliminar da lista de reprodución recente</string>\n  <string name=\"hide_settings_section\">Ocultar sección de axustes (perigoso!)</string>\n  <string name=\"volume\">Volume %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s seg</string>\n  <string name=\"audio_shift_sec\">%s seg</string>\n  <string name=\"ui_hide_timeout_sec\">%s seg</string>\n  <string name=\"screen_dimming_timeout_min\">%s min</string>\n  <string name=\"mark_all_channels_watched\">Marca todas as canles como vistas</string>\n  <string name=\"move_section_up\">Mover sección cara arriba</string>\n  <string name=\"move_section_down\">Mover sección cara abaixo</string>\n  <string name=\"player_buttons\">Configurar os botóns do reprodutor</string>\n  <string name=\"action_playlist_add\">Engadir a lista de reprodución</string>\n  <string name=\"action_video_stats\">Estadísticas de vídeo</string>\n  <string name=\"action_subscribe\">Subscríbete</string>\n  <string name=\"action_channel\">Abrir canle</string>\n  <string name=\"action_pip\">PIP</string>\n  <string name=\"action_video_info\">Descrición do vídeo</string>\n  <string name=\"action_screen_off\">Pantalla apagada</string>\n  <string name=\"action_sound_off\">Son apagado</string>\n  <string name=\"action_playback_queue\">Cola de reprodución</string>\n  <string name=\"action_video_speed\">Velocidade do vídeo</string>\n  <string name=\"action_subtitles\">Subtítulos</string>\n  <string name=\"action_like\">Gústame</string>\n  <string name=\"action_dislike\">Non me gusta</string>\n  <string name=\"action_play_pause\">Reproducir/Pausa</string>\n  <string name=\"action_repeat_mode\">Modo de reprodución</string>\n  <string name=\"action_next\">Seguinte vídeo</string>\n  <string name=\"action_previous\">Vídeo anterior</string>\n  <string name=\"action_high_quality\">Calidade do vídeo</string>\n  <string name=\"content_block_no_skipping_mode\">Modo sen saltar</string>\n  <string name=\"content_block_action_type\">Escolle acción</string>\n  <string name=\"content_block_action_none\">Non facer nada</string>\n  <string name=\"content_block_action_only_skip\">Só saltar</string>\n  <string name=\"content_block_action_toast\">Saltar con notificación</string>\n  <string name=\"content_block_action_dialog\">Amosar o diálogo de confirmación</string>\n  <string name=\"content_block_filler\">Fóra de tema (recheo)</string>\n  <string name=\"keyboard_auto_show\">Amosa o teclado automaticamente</string>\n  <string name=\"cancel_dialog\">Cancelar</string>\n  <string name=\"msg_player_error_source2\">O URL non funciona ou a hora do dispositivo é incorrecta.\\nNormalmente, é un erro na aplicación.</string>\n  <string name=\"msg_player_error_video_source\">O URL do VÍDEO non funciona ou a hora do dispositivo é incorrecta.\\nNormalmente, é un erro na aplicación.</string>\n  <string name=\"msg_player_error_audio_source\">O URL de AUDIO non funciona ou a hora do dispositivo é incorrecta.\\nNormalmente, é un erro na aplicación.</string>\n  <string name=\"msg_player_error_subtitle_source\">O URL SUBTITLE non funciona ou a hora do dispositivo é incorrecta.\\nNormalmente, é un erro na aplicación.</string>\n  <string name=\"hide_upcoming\">Ocultar próximos desde Subscricións</string>\n  <string name=\"hide_upcoming_channel\">Ocultar próximos desde canle</string>\n  <string name=\"hide_upcoming_home\">Ocultar próximos desde inicio</string>\n  <string name=\"search_background_playback\">Reprodución en segundo plano mentres busca/navega por unha canle</string>\n  <string name=\"trending_row_name\">Tendencias</string>\n  <string name=\"player_seek_type\">Comportamento de busca</string>\n  <string name=\"player_seek_regular\">Regular</string>\n  <string name=\"player_seek_confirmation_pause\">Con confirmación (pausa mentres busca)</string>\n  <string name=\"player_seek_confirmation_play\">Con confirmación (reproducir mentres buscas)</string>\n  <string name=\"hide_shorts_from_home\">Ocultar curtos no inicio</string>\n  <string name=\"finish_on_disconnect\">Pecha a aplicación despois de desconectar o teléfono/tableta</string>\n  <string name=\"update_error\">Erro de actualización</string>\n  <string name=\"hide_shorts_from_history\">Ocultar curtas no Historial</string>\n  <string name=\"hide_shorts_from_trending\">Ocultar curtas nas Tendencias</string>\n  <string name=\"disable_screensaver\">Desactivar o protector de pantalla</string>\n  <string name=\"description_not_found\">Non se atopou a descrición</string>\n  <string name=\"proxy_port_hint\">Porto proxy</string>\n  <string name=\"proxy_host_hint\">Nome de host ou IP do proxy</string>\n  <string name=\"proxy_username_hint\">Nome de usuario do proxy</string>\n  <string name=\"proxy_password_hint\">Contrasinal de proxy</string>\n  <string name=\"proxy_type\">Tipo de proxy</string>\n  <string name=\"enable_web_proxy\">Usa proxy web</string>\n  <string name=\"proxy_not_supported\">Usar proxy web (necesita Android 4.4+)</string>\n  <string name=\"proxy_test_btn\">Proba</string>\n  <string name=\"proxy_settings_title\">Configuración do servidor proxy</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_type_invalid\">Tipo de proxy non definido.</string>\n  <string name=\"proxy_test_cancelled\">Proba #%d: cancelada.</string>\n  <string name=\"proxy_host_invalid\">Host proxy non definido.</string>\n  <string name=\"proxy_port_invalid\">Porto de proxy non válido, debe &gt; 0.</string>\n  <string name=\"proxy_credentials_invalid\">Nome de usuario ou contrasinal non válido.</string>\n  <string name=\"proxy_test_aborted\">Proba abortada, primeiro corrixe a configuración do proxy.</string>\n  <string name=\"proxy_application_aborted\">Corrixe primeiro a configuración do proxy.</string>\n  <string name=\"proxy_test_start\">Proba #%d: %s...</string>\n  <string name=\"proxy_test_error\">Erro #%d: %s</string>\n  <string name=\"proxy_test_status\">Estado #%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Enderezo do ficheiro de configuración (*.ovpn)</string>\n  <string name=\"enable_openvpn\">Use OpenVPN</string>\n  <string name=\"openvpn_settings_title\">Configuración de OpenVPN</string>\n  <string name=\"openvpn_application_aborted\">Corrixe primeiro a configuración de OpenVPN.</string>\n  <string name=\"openvpn_test_aborted\">Proba abortada, primeiro corrixe a configuración de OpenVPN.</string>\n  <string name=\"openvpn_address_invalid\">O enderezo de configuración de OpenVPN non está definido.</string>\n  <string name=\"internet_censorship\">Censura de Internet</string>\n  <string name=\"rename_section\">Cambia o nome desta sección</string>\n  <string name=\"simple_edit_value_hint\">Insira o valor</string>\n  <string name=\"seek_interval\">Busca intervalo</string>\n  <string name=\"seek_interval_sec\">%s seg</string>\n  <string name=\"subtitle_system\">Estilo do sistema (Configuración de Android &gt; Accesibilidade)</string>\n  <string name=\"subtitle_scale\">Escala de subtítulos</string>\n  <string name=\"audio_sync_fix_desc\">Unha forma alternativa de sincronizar son/vídeo. Considérase obsoleto, pero nalgúns casos pode axudar.</string>\n  <string name=\"audio_sync_fix\">Corrección de sincronización de son</string>\n  <string name=\"ambilight_ratio_fix_desc\">Corrixe a iluminación polar ausente. Corrixe a relación de aspecto ou a escala de vídeo incorrectas. Corrixe as capturas de pantalla en branco. Afecta o rendemento!</string>\n  <string name=\"ambilight_ratio_fix\">Ambilight/Relación de aspecto/Escala de vídeo/Corrección de capturas de pantalla</string>\n  <string name=\"force_legacy_codecs_desc\">Mellora significativamente o rendemento en dispositivos de gama baixa. A resolución máxima é de 720p.</string>\n  <string name=\"force_legacy_codecs\">Forzar códecs heredados (720p)</string>\n  <string name=\"live_stream_fix_desc\">Aviso, este axuste desactiva o rebobinado dos fluxos. Mellora significativamente o rendemento da emisión en directo en dispositivos de gama baixa. A resolución máxima é de 1080p.</string>\n  <string name=\"live_stream_fix_4k_desc\">Aviso, este axuste desactiva o rebobinado dos fluxos. Mellora significativamente o rendemento da emisión en directo. A resolución máxima é de 4K.</string>\n  <string name=\"live_stream_fix\">Corrección da emisión en directo (1080p)</string>\n  <string name=\"live_stream_fix_4k\">Corrección da emisión en directo (4K)</string>\n  <string name=\"playback_notifications_fix_desc\">Oculta as notificacións de troco de pista. Podería ser útil no firmware baseado en AOSP.</string>\n  <string name=\"playback_notifications_fix\">Desactiva as notificacións de reprodución</string>\n  <string name=\"amlogic_fix_desc\">Corrección de caída de fotogramas en dispositivos baseados en Amlogic.</string>\n  <string name=\"amlogic_fix\">Corrección de Amlogic 1080p\\@60fps</string>\n  <string name=\"tunneled_video_playback_desc\">NOTA: é posible que a pausa non funcione correctamente! A reprodución de vídeo tunelado promete vantaxes como unha mellor sincronización de son/vídeo (sincronización AV) e unha reprodución máis fluida. Requírese Android 5+</string>\n  <string name=\"tunneled_video_playback\">Reproducción de vídeo tunelizado (Android 5+)</string>\n  <string name=\"master_volume\">Volume mestre</string>\n  <string name=\"volume_limit\">Límite de volume</string>\n  <string name=\"player_volume\">Volume</string>\n  <string name=\"play_video\">Reproducir</string>\n  <string name=\"remember_position_of_short_videos\">Lembrar a posición dos vídeos curtos (menos de 5 minutos)</string>\n  <string name=\"remember_position_of_live_videos\">Lembra a posición da emisión en directo</string>\n  <string name=\"player_show_tooltips\">Amosar información sobre botóns</string>\n  <string name=\"action_like_unset\">eliminado me gusta</string>\n  <string name=\"action_dislike_unset\">eliminado non me gusta</string>\n  <string name=\"various_buttons\">Botóns na parte superior da xanela principal</string>\n  <string name=\"not_compatible_with\">A opción escollida non é compatible con</string>\n  <string name=\"subtitle_position\">desfase inferior dos subtítulos</string>\n  <string name=\"pressing_home\">premendo INICIO</string>\n  <string name=\"pressing_home_back\">Premendo INICIO ou VOLTAR</string>\n  <string name=\"pressing_back\">Premendo VOLTAR</string>\n  <string name=\"player_number_key_seek\">Busca con teclas numéricas</string>\n  <string name=\"save_remove_playlist\">Engadir/eliminar lista de reprodución da sección Listas de reprodución</string>\n  <string name=\"save_playlist\">Engade lista de reprodución á sección Listas de reprodución</string>\n  <string name=\"remove_playlist\">Elimina a lista de reprodución da sección Listas de reprodución permanentemente</string>\n  <string name=\"removed_from_playlists\">Eliminado da sección Listas de reprodución</string>\n  <string name=\"saved_to_playlists\">Engadida á sección Listas de reprodución</string>\n  <string name=\"create_playlist\">Crear lista de reprodución</string>\n  <string name=\"add_video_to_new_playlist\">Engadir a nova lista de reprodución</string>\n  <string name=\"cant_delete_empty_playlist\">Non se pode eliminar a lista de reprodución baleira</string>\n  <string name=\"playlist\">Lista de reprodución</string>\n  <string name=\"rename_playlist\">Cambiar o nome da lista de reprodución</string>\n  <string name=\"cant_rename_empty_playlist\">Non se pode trocar o nome da lista de reprodución baleira</string>\n  <string name=\"cant_rename_foreign_playlist\">Non se pode trocar o nome da lista de reprodución estranxeira</string>\n  <string name=\"cant_save_playlist\">Non se pode engadir esta lista de reprodución</string>\n  <string name=\"enter_value\">Introduza calquera valor que non estea baleiro</string>\n  <string name=\"dialog_add_remove_from\">Engadir/Eliminar de %s</string>\n  <string name=\"lb_playback_controls_skip_next\">Saltar seguinte</string>\n  <string name=\"lb_playback_controls_skip_previous\">Saltar o anterior/rebobinar ata a posición inicial</string>\n  <string name=\"alt_presets_behavior\">Comportamento predefinido alternativo (ancho de banda límite)</string>\n  <string name=\"alt_presets_behavior_desc\">A aplicación intentará manter o ancho de banda correspondente á predefinición seleccionada en vez de axustar entre resolución, fps e códec.</string>\n  <string name=\"sleep_timer\">Temporizador de suspensión (se non se utiliza o control remoto durante unha hora)</string>\n  <string name=\"sleep_timer_desc\">Pausa a reprodución se o usuario non usou o control remoto durante unha hora</string>\n  <string name=\"disable_vsync\">Desactivar a sincronización con vsync</string>\n  <string name=\"disable_vsync_desc\">Esta opción desactiva o aliñamento de fotogramas co sinal de sincronización vertical da pantalla. Isto podería mellorar o rendemento en dispositivos de gama baixa liberando recursos da CPU.</string>\n  <string name=\"skip_codec_profile_check\">Omitir a verificación do nivel de perfil do códec</string>\n  <string name=\"skip_codec_profile_check_desc\">Non comproba a compatibilidade con códecs ao iniciar un vídeo. Podería ser útil no firmware con erros.</string>\n  <string name=\"force_sw_codec\">Forzar o decodificador de vídeo SW</string>\n  <string name=\"force_sw_codec_desc\">Podería reproducir case calquera vídeo, pero o rendemento é moi pobre.</string>\n  <string name=\"playlist_order\">Ordenar lista de reprodución</string>\n  <string name=\"playlist_order_added_date_newer_first\">Data de engadido (a máis nova primeiro)</string>\n  <string name=\"playlist_order_added_date_older_first\">Data de engadido (primeiro máis antiga)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Data de publicación (primeiro máis recente)</string>\n  <string name=\"playlist_order_published_date_older_first\">Data de publicación (antigo primeiro)</string>\n  <string name=\"playlist_order_popularity\">Popularidade</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">Non se pode facer isto para a lista de reprodución estranxeira</string>\n  <string name=\"owned_playlist_warning\">Erro: esta acción só se pode aplicar ás listas de reprodución propias</string>\n  <string name=\"content_block_alt_server\">Usar un servidor alternativo</string>\n  <string name=\"content_block_alt_server_desc\">Activa esta opción se SponsorBlock se nega a funcionar por diferentes motivos.</string>\n  <string name=\"unset_stream_reminder\">Desactivar o recordatorio de emisión</string>\n  <string name=\"set_stream_reminder\">Establecer un recordatorio de emisión</string>\n  <string name=\"playback_starts_shortly\">A reprodución comezará automaticamente cando a emisión estea lista</string>\n  <string name=\"starting_stream\">Comezando a emisión...</string>\n  <string name=\"action_playlist_remove\">Eliminar da lista de reprodución</string>\n  <string name=\"signin_view_title\">O código de usuario estase cargando...</string>\n  <string name=\"signin_view_description\">Para iniciar sesión, introduce este código na páxina %s\\nNOTA. Funciona só cos navegadores Firefox ou Chrome.</string>\n  <string name=\"signin_view_action_text\">Feito</string>\n  <string name=\"require_checked\">Require \\\"%s</string>\n  <string name=\"msg_press_again_to_exit\">Preme de novo para saír</string>\n  <string name=\"player_remaining_time\">Restante: %s</string>\n  <string name=\"player_ending_time\">Remata en %s</string>\n  <string name=\"badge_new_content\">NOVO CONTIDO</string>\n  <string name=\"badge_live\">EN VIVO</string>\n  <string name=\"add_device_view_description\">Para vincular o dispositivo, introduce este código na aplicación YouTube do teu teléfono na sección Configuración/Ver na TV\\nNOTA: Isto só funciona co teclado Gboard.</string>\n  <string name=\"playback_controls_repeat_pause\">Repetir pausa</string>\n  <string name=\"playback_controls_repeat_list\">Repetir Lista</string>\n  <string name=\"action_subscribe_off\">Subscrición desactivada</string>\n  <string name=\"action_subscribe_on\">Subscríción activada</string>\n  <string name=\"skip_24_rate\">Omitir formatos de 24 fps (solución do modo de cine real\\?)</string>\n  <string name=\"skip_shorts\">Saltar Cortos</string>\n  <string name=\"player_disable_suggestions\">Desactivar as suxestións</string>\n  <string name=\"feedback\">Comentarios</string>\n  <string name=\"sources\">Fontes</string>\n  <string name=\"releases\">Lanzamentos</string>\n  <string name=\"prefer_avc_over_vp9\">Selección de códec: prefire avc sobre vp9</string>\n  <string name=\"open_chat\">Chat/Comentarios</string>\n  <string name=\"open_comments\">Abrir comentarios</string>\n  <string name=\"place_chat_left\">Coloca o chat á esquerda</string>\n  <string name=\"use_alt_speech_recognizer\">Recoñecemento de voz alternativo</string>\n  <string name=\"time_format\">Formato horario</string>\n  <string name=\"time_format_24\">24 horas</string>\n  <string name=\"time_format_12\">12 horas (AM/PM)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Con erros graves. Utilízao só se tes problemas co recoñecemento predeterminado.</string>\n  <string name=\"speech_recognizer\">Recoñecemento de voz</string>\n  <string name=\"speech_engine\">Buscador por voz</string>\n  <string name=\"speech_recognizer_system\">Sistema (preferido)</string>\n  <string name=\"speech_recognizer_external_1\">Externo 1 (con erros graves, para que funcione, debes desactivar o acceso ao micrófono)</string>\n  <string name=\"speech_recognizer_external_2\">Externo 2 (con erros graves)</string>\n  <string name=\"real_channel_icon\">Amosar a icona no botón da canle</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Amarelo sobre fondo semitransparente</string>\n  <string name=\"subtitle_yellow_black\">Amarelo sobre fondo negro</string>\n  <string name=\"player_pixel_ratio\">Relación de píxeles</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Gris escuro (monocromo)</string>\n  <string name=\"disable_mic_permission\">Desactiva o acceso ao micrófono para a aplicación para que este recoñecedor funcione correctamente.</string>\n  <string name=\"repeat_mode_shuffle\">Mezcla calquera lista de reprodución</string>\n  <string name=\"chat_left\">Esquerda</string>\n  <string name=\"chat_right\">Dereita</string>\n  <string name=\"card_real_thumbnails\">Substitúe as miniaturas por un fotograma do vídeo</string>\n  <string name=\"card_content\">Onde obter as miniaturas</string>\n  <string name=\"thumb_quality_default\">Por defecto</string>\n  <string name=\"thumb_quality_start\">Inicio do vídeo</string>\n  <string name=\"thumb_quality_middle\">Medio do vídeo</string>\n  <string name=\"thumb_quality_end\">Fin do vídeo</string>\n  <string name=\"content_block_status\">Comprobar o estado do servidor SponsorBlock</string>\n  <string name=\"dearrow_status\">Comprobar o estado do servidor DeArrow</string>\n  <string name=\"player_show_quality_info_bitrate\">Engadir taxa de bits á información de calidade</string>\n  <string name=\"player_speed_button_old_behavior\">Revertir o comportamento antigo do botón de velocidade</string>\n  <string name=\"protect_settings_with_password\">Protexer todas as opcións con contrasinal</string>\n  <string name=\"enter_settings_password\">Introduza o contrasinal de configuración</string>\n  <string name=\"child_mode\">Modo infantil</string>\n  <string name=\"child_mode_desc\">Neste modo, o usuario non pode usar a busca nin ver ningún contido suxerido. A páxina de configuración estará protexida por contrasinal.</string>\n  <string name=\"lost_setting_warning\">Cambiarase a configuración da aplicación. Asegúrese de crear unha copia de seguridade da configuración.</string>\n  <string name=\"player_button_long_click\">Mantén presionado este botón para obter opcións adicionais</string>\n  <string name=\"pause_history\">Pausa o historial</string>\n  <string name=\"resume_history\">Retomar o historial</string>\n  <string name=\"disable_history\">Desactivar o historial</string>\n  <string name=\"enable_history\">Activar o historial</string>\n  <string name=\"clear_history\">Borrar o historial</string>\n  <string name=\"chapters\">Capítulos</string>\n  <string name=\"card_multiline_subtitle\">Subtítulos multiliña</string>\n  <string name=\"auto_frame_rate_modes\">Modos admitidos</string>\n  <string name=\"loading\">Cargando…</string>\n  <string name=\"hide_streams\">Ocultar emisións das subscricións</string>\n  <string name=\"video_rotate\">Xirar</string>\n  <string name=\"trending_searches\">Buscas de tendencia</string>\n  <string name=\"video_duration_any\">Calquera</string>\n  <string name=\"video_duration\">Duración</string>\n  <string name=\"video_duration_under_4\">Menos de 4 minutos</string>\n  <string name=\"video_duration_between_4_20\">4-20 minutos</string>\n  <string name=\"video_duration_over_20\">Máis de 20 minutos</string>\n  <string name=\"content_type\">Tipo</string>\n  <string name=\"content_type_any\">Calquera</string>\n  <string name=\"content_type_video\">Vídeo</string>\n  <string name=\"content_type_channel\">Canle</string>\n  <string name=\"content_type_playlist\">Lista de reprodución</string>\n  <string name=\"content_type_movie\">Película</string>\n  <string name=\"video_features\">Características</string>\n  <string name=\"video_feature_any\">Calquera</string>\n  <string name=\"video_feature_live\">Ao vivo</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">Ordenar por</string>\n  <string name=\"sort_by_relevance\">Relevancia</string>\n  <string name=\"sort_by_views\">Conta de vistas</string>\n  <string name=\"sort_by_date\">Data de carga</string>\n  <string name=\"sort_by_rating\">Valoración</string>\n  <string name=\"clear_search_history\">Borrar o historial de busca</string>\n  <string name=\"remove_from_subscriptions\">Ocultar</string>\n  <string name=\"player_long_speed_list\">Lista de velocidade longa</string>\n  <string name=\"player_extra_long_speed_list\">Lista de velocidades extra longa</string>\n  <string name=\"alt_app_icon\">Icona da aplicación alternativa (necesita reiniciar)</string>\n  <string name=\"network_stack\">Prefire a pila de rede %s</string>\n  <string name=\"player_network_stack\">Motor de rede</string>\n  <string name=\"cronet_desc\">Cronet é Conxunto de protocolos de rede de Chromium. Cronet pode reducir a latencia e aumentar o rendemento da rede, o que pode axudar cos problemas de almacenamento na memoria intermedia.</string>\n  <string name=\"unlock_all_formats\">Desbloquear todos os formatos de vídeo</string>\n  <string name=\"unlock_all_formats_desc\">Nalgúns dispositivos, o firmware indica incorrectamente que algúns formatos non son compatibles, aínda que o sexan.\\n(por exemplo, os televisores intelixentes de 1080p adoitan indicar que 4k non son compatibles).</string>\n  <string name=\"okhttp_desc\">Este motor de rede é o máis lento pero ten unha boa estabilidade.</string>\n  <string name=\"select_channel_section\">Abre a sección correspondente cando se lance a aplicación desde as canles ATV</string>\n  <string name=\"enable_voice_search_desc\">A aplicación oficial será substituída pola chamada \\\"ponte\\\". Unha ponte axuda a transferir solicitudes de busca globais á nosa aplicación.</string>\n  <string name=\"enable_master_password\">Activa o contrasinal mestre</string>\n  <string name=\"enter_master_password\">Introduza o contrasinal mestre</string>\n  <string name=\"disable_stream_buffer\">Desactiva o búfer nas emisións</string>\n  <string name=\"disable_stream_buffer_desc\">Soluciona situacións nas que o fluxo está demasiado atrás. Nota: o fluxo pode comezar a atrasar.</string>\n  <string name=\"sony_frame_drop_fix\">Corrección de caída de fotogramas #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">Corrixir atrasos na TV Sony e nalgúns outros dispositivos. Nota: posibles problemas coa sincronización de son.</string>\n  <string name=\"audio_language\">Linguaxe do son</string>\n  <string name=\"old_home_look\">Aspecto antigo da sección Inicio</string>\n  <string name=\"old_channel_look\">Aspecto antigo da páxina da Canle</string>\n  <string name=\"hide_shorts_everywhere\">Oculta pantalóns curtos en todas partes</string>\n  <string name=\"update_found\">Actualizar</string>\n  <string name=\"volume_boost_warning\">Calquera configuración que supere o límite pode danar os altofalantes</string>\n  <string name=\"play_video_incognito\">Reprodución de incógnito</string>\n  <string name=\"header_kids_home\">Nenos</string>\n  <string name=\"player_section_playlist\">Usa o contido da sección actual como lista de reprodución</string>\n  <string name=\"header_trending\">Tendencias</string>\n  <string name=\"screensaver\">Salvapantallas</string>\n  <string name=\"player_screen_off_timeout\">Tempo de espera de apagado da pantalla</string>\n  <string name=\"old_update_notifications\">Aspecto antigo das notificacións de actualización</string>\n  <string name=\"dialog_notification\">Notificar sobre actualizacións cun diálogo emerxente</string>\n  <string name=\"mark_as_watched\">Marcar como visto</string>\n  <string name=\"player_ui_animations\">Activar as animacións do panel de control</string>\n  <string name=\"player_likes_count\">Mostra o reconto de Gústame/Non me gusta</string>\n  <string name=\"sorting_alphabetically2\">Alfabéticamente (vistas previas rápidas e animadas)</string>\n  <string name=\"sorting_default\">Predeterminado (vistas previas rápidas e animadas)</string>\n  <string name=\"content_block_exclude_channel\">Excluír esta canle de SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">Deixar de excluír esta canle de SponsorBlock</string>\n  <string name=\"player_chapter_notification\">Avance rápido polos capítulos mediante a notificación emerxente</string>\n  <string name=\"subtitle_remember\">Lembrar os subtítulos activados en cada canle</string>\n  <string name=\"amazon_frame_drop_fix\">Corrección de caída de fotogramas #2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">Destinado a dispositivos Amazon Stick. Tamén pode funcionar noutros dispositivos.</string>\n  <string name=\"default_stack_desc\">Motor de rede incorporado. Este motor pode ter unha mellor estabilidade en determinadas situacións.</string>\n  <string name=\"item_postion\">Posición de</string>\n  <string name=\"player_screen_off_dimming\">Cantidade de escurecemento da pantalla</string>\n  <string name=\"screensaver_timout\">Tempo de espera do salvapantallas</string>\n  <string name=\"screensaver_dimming\">Cantidade de atenuación do protector de pantalla</string>\n  <string name=\"player_ui_on_next\">Amosar a interface de usuario do reprodutor ao trocar ao seguinte vídeo</string>\n  <string name=\"autogenerated\">autoxerado</string>\n  <string name=\"player_auto_volume\">Axuste automático de volume</string>\n  <string name=\"header_shorts\">Curtos</string>\n  <string name=\"player_global_focus\">Navegación sen interrupción entre as filas de botóns do reprodutor</string>\n  <string name=\"auto_history\">Automático (usar a configuración da conta)</string>\n  <string name=\"remember_position_subscriptions\">Lembra a última posición vista en Subscricións</string>\n  <string name=\"remember_position_pinned\">Lembra a última posición vista nas listas de reprodución fixadas</string>\n  <string name=\"msg_player_unknown_error\">Erro descoñecido</string>\n  <string name=\"unknown_source_error\">Erro de fonte descoñecido</string>\n  <string name=\"unknown_renderer_error\">Erro de renderizador descoñecido</string>\n  <string name=\"header_notifications\">Notificacións</string>\n  <string name=\"disable_search_history\">Desactivar o historial de busca</string>\n  <string name=\"unlock_high_bitrate_formats\">Desbloquear formatos de alta taxa de bits 1080p vp9</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Desbloquear formatos mp4a de alta taxa de bits</string>\n  <string name=\"color_scheme_blue\">Azul</string>\n  <string name=\"color_scheme_dark_blue\">Azul escuro</string>\n  <string name=\"player_speed_per_channel\">Por cada canle</string>\n  <string name=\"disable_popular_searches\">Desactivar consultas de busca populares</string>\n  <string name=\"multi_profiles\">Usar configuracións separadas para cada conta</string>\n  <string name=\"protect_account_with_password\">Protexa esta conta cun contrasinal</string>\n  <string name=\"enter_account_password\">Inserir o contrasinal da conta</string>\n  <string name=\"show_connect_messages\">Amosar mensaxes de conexión</string>\n  <string name=\"prefer_avc_over_vp9_desc\">AVISO: 1080p máximo</string>\n  <string name=\"play_next\">Reproducir a continuación</string>\n  <string name=\"hide_watched_from_subscriptions\">Ocultar os vídeos vistos das subscricións</string>\n  <string name=\"hide_watched_from_notifications\">Ocultar os vídeos vistos das notificacións</string>\n  <string name=\"hide_unwanted_content\">Ocultar contido non desexado</string>\n  <string name=\"hide_watched_from_home\">Ocultar os vídeos vistos de Inicio</string>\n  <string name=\"hide_watched_from_watch_later\">Ocultar os vídeos vistos da lista de reprodución Ver máis tarde</string>\n  <string name=\"remote_control_permission\">O control remoto en segundo plano require permiso de superposición</string>\n  <string name=\"login_from_browser\">Comezar sesión desde o navegador</string>\n  <string name=\"disable_remote_history\">Desactivar o historial ao usar o control remoto</string>\n  <string name=\"keyboard_fix\">Evitar que o botón Aceptar abra o teclado (G20s e outros)</string>\n  <string name=\"nothing_found\">Non se atopou nada</string>\n  <string name=\"auto_frame_rate_desc\">Esta opción elimina o tremor en escenas onde a cámara se move rapidamente, por exemplo, na transmisión de deportes</string>\n  <string name=\"channel_filter_hint\">Filtrar canles</string>\n  <string name=\"player_loop_shorts\">Repetir Curtos en bucle</string>\n  <string name=\"player_quick_shorts_skip\">Saltar curtos con botóns esquerdo/dereito</string>\n  <string name=\"player_quick_skip_videos\">Saltar vídeos normais cos botóns esquerda/dereita</string>\n  <string name=\"channels_filter\">Amosar o campo de filtro de canles dentro da sección de Canles</string>\n  <string name=\"channel_search_bar\">Barra de busca dentro da páxina da canle</string>\n  <string name=\"header_sports\">Deportes</string>\n  <string name=\"keep_finished_activities\">Manter as actividades rematadas</string>\n  <string name=\"disable_channels_service\">Desactivar o servizo de canles</string>\n  <string name=\"replace_titles\">Substituír títulos</string>\n  <string name=\"enable\">Activar</string>\n  <string name=\"more_info\">Máis información</string>\n  <string name=\"about_sponsorblock\">Acerca de SponsorBlock</string>\n  <string name=\"about_dearrow\">Acerca de DeArrow</string>\n  <string name=\"replace_thumbnails\">Substitúe as miniaturas</string>\n  <string name=\"crowdsourced_thumbnails\">miniaturas colaborativas</string>\n  <string name=\"crowdsoursed_titles\">Títulos colaborativos</string>\n  <string name=\"dearrow_not_submitted_thumbs\">Fonte da miniatura (se non hai unha presentación de DeArrow)</string>\n  <string name=\"pitch_effect\">Efecto de ton</string>\n  <string name=\"fullscreen_mode\">Modo de pantalla chea (sen barras do sistema)</string>\n  <string name=\"player_only_mode\">Amosar só o reprodutor se o vídeo está aberto fóra da aplicación</string>\n  <string name=\"pinned_channel_rows\">Amosar a canle fixada como filas</string>\n  <string name=\"prefer_ipv4\">Prefirir DNS IPv4</string>\n  <string name=\"prefer_ipv4_desc\">Poderíase solucionar situacións nas que a aplicación non funciona.\\nTen en conta. Pode causar bloqueos e fallos (especialmente en dispositivos Android 8 ou Dune HD)</string>\n  <string name=\"long_press_for_settings\">PRESION LONGA PARA AXUSTES</string>\n  <string name=\"long_press_for_options\">PRESION LONGA PARA OPCIÓNS</string>\n  <string name=\"device_specific_backup\">Copia de seguranza só para este dispositivo</string>\n  <string name=\"local_backup\">Copia de seguridade local</string>\n  <string name=\"auto_backup\">Copia de seguranza automática (unha vez ao día)</string>\n  <string name=\"repeat_mode_reverse_list\">Reproduce a lista de reprodución ou os vídeos da canle en orde inversa</string>\n  <string name=\"calm_msg\">Estamos solucionando o problema. Consulta actualizacións de cando en vez</string>\n  <string name=\"without_picture\">Sen imaxe</string>\n  <string name=\"video_disabled\">Vídeo desactivado</string>\n  <string name=\"applying_fix\">Non peche o reprodutor. Aplicando a corrección...</string>\n  <string name=\"hide_mixes\">Ocultar misturas</string>\n  <string name=\"player_global_focus_desc\">Esta función afecta a que botón do reprodutor recibirá o foco ao navegar entre filas de botóns do reprodutor</string>\n  <string name=\"disable_network_error_fixing\">Desactivar a corrección automática de erros de rede</string>\n  <string name=\"disable_network_error_fixing_desc\">Probablemente precises activar esta opción se estás a usar unha VPN</string>\n  <string name=\"recommended\">Recomendado</string>\n  <string name=\"video_buffer_size_lowest\">A máis baixa</string>\n  <string name=\"video_buffer_size_highest\">A máis alta </string>\n  <string name=\"unpin_group_from_sidebar\">Eliminar o grupo de subscrición</string>\n  <string name=\"place_comments_left\">Coloca os comentarios á esquerda</string>\n  <string name=\"add_to_subscriptions_group\">Engadir/Eliminar do grupo de subscrición</string>\n  <string name=\"new_subscriptions_group\">Novo grupo</string>\n  <string name=\"rename_group\">Renomear o grupo de subscrición</string>\n  <string name=\"screen_dimming_amount\">Cantidade de atenuación da pantalla</string>\n  <string name=\"screen_dimming_timeout\">Tempo de espera para a atenuación da pantalla</string>\n  <string name=\"playlists_rows\">Amosar a sección de listas de reprodución como filas</string>\n  <string name=\"not_supported_by_device\">O dispositivo non soporta esta función</string>\n  <string name=\"context_menu_sorting\">Ordenación do menú contextual</string>\n  <string name=\"hide_shorts_from_search\">Ocultar vídeos curtos nos resultados de busca</string>\n  <string name=\"suggestions\">Suxestións</string>\n  <string name=\"import_subscriptions_group\">Importar</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">Desactivar os subtítulos</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">Activar os subtítulos</string>\n  <string name=\"my_videos\">Os meus vídeos</string>\n  <string name=\"player_audio_focus\">Foco de son (pausar se se detectan outros reprodutores)</string>\n  <string name=\"paid_content_notification\">Notificación de contido de pago</string>\n  <string name=\"you_liked\">Gustouche</string>\n  <string name=\"premium_users_only\">Só para usuarios Premium. Corrección para lista de formatos de vídeo incompleta</string>\n  <string name=\"playback_buffering_fix\">Corrección do búfer de reprodución</string>\n  <string name=\"oculus_quest_fix\">Corrección para Oculus Quest</string>\n  <string name=\"card_preview_muted\">Vídeo sen son</string>\n  <string name=\"card_preview_full\">Vídeo con son</string>\n  <string name=\"card_preview\">Previsualización das miniaturas</string>\n  <string name=\"card_unlocalized_titles\">Títulos de vídeo sen traducir</string>\n  <string name=\"search_exit_shortcut\">Saír da busca</string>\n  <string name=\"video_flip\">Volteo (espello)</string>\n  <string name=\"original_lang\">Orixinal</string>\n  <string name=\"remove_playlist_fmt\">Eliminar %s permanentemente\\?</string>\n  <string name=\"player_chapter_notification2\">Cambia ao seguinte capítulo facendo clic na notificación</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">Non redimensionar o vídeo para axustalo ao diálogo</string>\n  <string name=\"typing_corrections\">Desactivar a autocorrección ao escribir texto</string>\n  <string name=\"suggestions_horizontally_scrolled\">Suxestións desprazables horizontalmente</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-he/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\nFor hebrew, in particular, on some devices values-he is recognized. in some others it's values-iw.\nSo, make a copy of values-he and name it values-iw. Keep both in your structure.\n-->\n<resources>\n    <string name=\"header_home\">בית</string>\n    <string name=\"title_search\">חיפוש</string>\n    <string name=\"header_subscriptions\">מינויים</string>\n    <string name=\"header_history\">היסטוריה</string>\n    <string name=\"header_music\">מוזיקה</string>\n    <string name=\"header_news\">חדשות</string>\n    <string name=\"header_gaming\">משחקים</string>\n    <string name=\"header_playlists\">פלייליסטים</string>\n    <string name=\"header_settings\">הגדרות</string>\n    <string name=\"header_channels\">ערוצים</string>\n    <string name=\"subscriptions_signin_title\">ראה את הסרטונים הכי חדשים מהערוצים שאתה אוהב</string>\n    <string name=\"subscriptions_signin_subtitle\">היכנס כדי לראות את המינויים שלך</string>\n    <string name=\"subscriptions_signin_button_text\">היכנס</string>\n    <string name=\"library_signin_to_show_more\">צפה בסרטונים שאהבת, שמרת, או נרשמת כמנוי</string>\n    <string name=\"library_signin_subtitle\">היכנס כדי לראות את הספרייה שלך</string>\n    <string name=\"action_signin\">היכנס</string>\n    <string name=\"title_video_formats\">פורמטי וידאו</string>\n    <string name=\"title_audio_formats\">פורמטי שמע</string>\n    <string name=\"subtitle_category_title\">כתוביות</string>\n    <string name=\"video_max_quality\">אוטומטי (איכות מרבית)</string>\n    <string name=\"audio_max_quality\">אוטומטי (איכות מרבית)</string>\n    <string name=\"subtitles_disabled\">כתוביות מושבתות</string>\n    <string name=\"auto_frame_rate\">קצב פריימים אוט\\'</string>\n    <string name=\"frame_rate_correction\">תקן fps:\\n%s</string>\n    <string name=\"category_background_playback\">השמעת רקע</string>\n    <string name=\"not_implemented\">לא מיושם</string>\n    <string name=\"not_supported_by_device\">המכשיר לא תומך בתכונה זו</string>\n    <string name=\"option_background_playback_off\">מושבתת</string>\n    <string name=\"option_background_playback_pip\">תמונה בתוך תמונה</string>\n    <string name=\"option_background_playback_only_audio\">רק שמע</string>\n    <string name=\"playback_settings\">הגדרות איכות הפעלה</string>\n    <string name=\"video_speed\">מהירות סרטון</string>\n    <string name=\"resolution_switch\">החלף רזולוציה</string>\n    <string name=\"video_buffer_size_lowest\">הכי נמוך</string>\n    <string name=\"video_buffer_size_low\">נמוך</string>\n    <string name=\"video_buffer_size_med\">בינוני</string>\n    <string name=\"video_buffer_size_high\">גבוה</string>\n    <string name=\"video_buffer_size_highest\">הכי גבוה</string>\n    <string name=\"update_changelog\">יומן שינויים</string>\n    <string name=\"install_update\">התקן עדכון</string>\n    <string name=\"section_is_empty\">אופס! אין כאן כלום</string>\n    <string name=\"dialog_add_to_playlist\">הוסף/הסר מפלייליסט</string>\n    <string name=\"msg_signed_users_only\">משתמשים מחוברים בלבד</string>\n    <string name=\"msg_cant_load_content\">לא ניתן לטעון תוכן.\\n 1) הפעל היסטוריית צפייה.\\n 2) בדוק את התאריך והשעה במכשיר.\\n 3) בדוק חיבור רשת.\\n 4) בדוק את הגדרות הפרוקסי שלך.\\n 5) נסה להיכנס לחשבון שלך.\\n 6) אולי אין כאן כלום.</string>\n    <string name=\"title_video_presets\">קביעות וידאו מוגדרות מראש</string>\n    <string name=\"settings_accounts\">חשבונות</string>\n    <string name=\"settings_left_panel\">ערוך קטגוריות</string>\n    <string name=\"settings_themes\">ערכות נושא</string>\n    <string name=\"settings_other\">אחר</string>\n    <string name=\"settings_player\">הגדרות נגן</string>\n    <string name=\"settings_language\">שפה</string>\n    <string name=\"settings_linked_devices\">מכשירים מקושרים</string>\n    <string name=\"settings_about\">אודות</string>\n    <string name=\"dialog_account_list\">בחר חשבון</string>\n    <string name=\"dialog_account_none\">ללא</string>\n    <string name=\"dialog_remove_account\">צא</string>\n    <string name=\"dialog_add_account\">היכנס</string>\n    <string name=\"default_lang\">ברירת מחדל</string>\n    <string name=\"dialog_select_language\">שפה</string>\n    <string name=\"subtitle_default\">ברירת מחדל</string>\n    <string name=\"subtitle_white_semi_transparent\">לבן על רקע שקוף למחצה</string>\n    <string name=\"subtitle_style\">סגנון כתוביות</string>\n    <string name=\"subtitle_language\">שפת כתוביות</string>\n    <string name=\"action_search\">חיפוש</string>\n    <string name=\"settings_main_ui\">ממשק משתמש</string>\n    <string name=\"dialog_main_ui\">ממשק משתמש</string>\n    <string name=\"card_animated_previews\">תצוגות מקדימות מונפשות</string>\n    <string name=\"web_site\">אתר אינטרנט</string>\n    <string name=\"donation\">תרומה</string>\n    <string name=\"dialog_about\">אודות</string>\n    <string name=\"dialog_player_ui\">נגן וידאו</string>\n    <string name=\"player_show_ui_on_pause\">הצג ממשק משתמש בהשהיה</string>\n    <string name=\"player_pause_on_ok\">מקש אישור משהה את ההפעלה</string>\n    <string name=\"player_ok_button_behavior\">התנהגות מקש אישור</string>\n    <string name=\"player_only_ui\">ממשק משתמש בלבד</string>\n    <string name=\"player_ui_and_pause\">ממשק משתמש והשהיה</string>\n    <string name=\"player_only_pause\">השהיה בלבד</string>\n    <string name=\"check_for_updates\">בדוק אם קיימים עדכונים</string>\n    <string name=\"update_not_found\">אתה משתמש בגרסה העדכנית ביותר</string>\n    <string name=\"update_in_progress\">המתן…</string>\n    <string name=\"player_ui_hide_behavior\">הסתר אוטומטית את ממשק המשתמש</string>\n    <string name=\"option_never\">לעולם לא</string>\n    <string name=\"side_panel_sections\">הגדרת מדורים</string>\n    <string name=\"boot_to_section\">אתחול למדור</string>\n    <string name=\"large_ui\">ממשק משתמש גדול</string>\n    <string name=\"video_grid_scale\">קנה מידה של רשת וידאו</string>\n    <string name=\"scale_ui\">קנה מידה של ממשק משתמש</string>\n    <string name=\"color_scheme\">ערכת צבעים</string>\n    <string name=\"color_scheme_default\">ברירת מחדל</string>\n    <string name=\"color_scheme_red_grey\">אדום-אפור</string>\n    <string name=\"color_scheme_red\">אדום</string>\n    <string name=\"color_scheme_dark_grey\">אפור כהה</string>\n    <string name=\"disable_update_check\">השבת בדיקת עדכונים</string>\n    <string name=\"show_again\">הצג שוב</string>\n    <string name=\"check_updates_auto\">הודע על עדכונים</string>\n    <string name=\"select_account_on_boot\">הצג בחירת חשבון באתחול</string>\n    <string name=\"player_other\">שונות</string>\n    <string name=\"player_full_date\">תאריך מדויק בתיאור</string>\n    <string name=\"open_channel\">פתח ערוץ</string>\n    <string name=\"not_interested\">לא מעוניין</string>\n    <string name=\"you_wont_see_this_video\">הסרטון הוסר ממומלצים</string>\n    <string name=\"settings_search\">חיפוש</string>\n    <string name=\"dialog_search\">חיפוש</string>\n    <string name=\"instant_voice_search\">חיפוש קולי מיידי</string>\n    <string name=\"option_background_playback_behind\">הפעל מאחור</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">מידע על הסרטון הבא טרם נטען</string>\n    <string name=\"card_multiline_title\">כותרות מרובות שורות</string>\n    <string name=\"cards_style\">סגנון כרטיסים</string>\n    <string name=\"repeat_mode_all\">הפעל סרטונים ברציפות</string>\n    <string name=\"repeat_mode_one\">חזור על הסרטון הנוכחי</string>\n    <string name=\"repeat_mode_pause\">השהה את ההפעלה לאחר כל סרטון</string>\n    <string name=\"repeat_mode_none\">הפסק הפעלה לאחר סרטון אחד</string>\n    <string name=\"subscribed_to_channel\">מנוי</string>\n    <string name=\"unsubscribed_from_channel\">לא מנוי</string>\n    <string name=\"subtitle_yellow_transparent\">צהוב על רקע שקוף</string>\n    <string name=\"subtitle_white_black\">לבן על רקע שחור</string>\n    <string name=\"player_seek_preview\">תצוגה מקדימה תוך כדי דילוג</string>\n    <string name=\"color_scheme_dark_grey_oled\">אפור כהה (OLED)</string>\n    <string name=\"channels_section_sorting\">מיון מדור ערוצים</string>\n    <string name=\"sorting_by_new_content\">תוכן חדש</string>\n    <string name=\"sorting_alphabetically\">בסדר אלפביתי</string>\n    <string name=\"sorting_last_viewed\">נצפה לאחרונה</string>\n    <string name=\"player_pause_when_seek\">השהה בזמן דילוג</string>\n    <string name=\"playlists_style\">סגנון מדור פלייליסטים</string>\n    <string name=\"playlists_style_grid\">רשת</string>\n    <string name=\"playlists_style_rows\">שורות</string>\n    <string name=\"player_show_clock\">הצג שעון בסרגל הפקדים</string>\n    <string name=\"player_show_remaining_time\">הצג את הזמן הנותר בסרגל הפקדים</string>\n    <string name=\"player_show_ending_time\">הצג את שעת הסיום בסרגל הפקדים</string>\n    <string name=\"repeat_mode_pause_alt\">הפעל רק סרטוני פלייליסט ברציפות</string>\n    <string name=\"subtitle_white_transparent\">לבן על רקע שקוף</string>\n    <string name=\"open_channel_uploads\">פתח העלאות ערוץ</string>\n    <string name=\"app_exit_shortcut\">יציאה מהיישום</string>\n    <string name=\"player_exit_shortcut\">יציאה מהנגן</string>\n    <string name=\"app_exit_none\">אל תצא</string>\n    <string name=\"app_double_back_exit\">חזרה כפולה</string>\n    <string name=\"app_single_back_exit\">חזרה בודדת</string>\n    <string name=\"action_video_zoom\">זום סרטון</string>\n    <string name=\"video_zoom\">זום סרטון</string>\n    <string name=\"video_zoom_default\">ברירת מחדל</string>\n    <string name=\"video_zoom_fit_width\">התאם רוחב</string>\n    <string name=\"video_zoom_fit_height\">התאם גובה</string>\n    <string name=\"video_zoom_fit_both\">התאם רוחב או גובה</string>\n    <string name=\"video_zoom_stretch\">מתיחה</string>\n    <string name=\"color_scheme_teal\">ירוק-כחול</string>\n    <string name=\"color_scheme_teal_oled\">ירוק-כחול (OLED)</string>\n    <string name=\"player_seek_preview_none\">מושבתת</string>\n    <string name=\"player_seek_preview_single\">פריים בודד</string>\n    <string name=\"player_seek_preview_carousel\">קרוסלה</string>\n    <string name=\"player_seek_preview_carousel_slow\">קרוסלה (איטית, לפי תמונות מפתח)</string>\n    <string name=\"player_seek_preview_carousel_fast\">קרוסלה (מהירה, תצוגה מקדימה לא מדויקת)</string>\n    <string name=\"unsubscribe_from_channel\">בטל הרשמה מהערוץ</string>\n    <string name=\"card_title_lines_num\">מספר שורות כותרת הכרטיס</string>\n    <string name=\"card_auto_scrolled_title\">גלילה אוטומטית של כותרת חתוכה</string>\n    <string name=\"share_link\">שתף קישור</string>\n    <string name=\"share_qr_link\">שתף קישור (קוד QR)</string>\n    <string name=\"subscribe_to_channel\">הירשם לערוץ</string>\n    <string name=\"wait_data_loading\">אנא המתן בזמן שהנתונים נטענים…</string>\n    <string name=\"auto_frame_rate_pause\">קצב פריימים אוטומטי (השהיה בעת המעבר)</string>\n    <string name=\"auto_frame_rate_applying\">מחיל קצב פריימים אוט\\' %sx%s\\@%s</string>\n    <string name=\"audio_shift\">הזזת שמע</string>\n    <string name=\"player_remember_speed\">זכור מהירות</string>\n    <string name=\"channel_marked_as_watched\">הערוץ סומן כנצפה</string>\n    <string name=\"mark_channel_as_watched\">סמן כנצפה</string>\n    <string name=\"dialog_add_device\">הוסף מכשיר</string>\n    <string name=\"dialog_remove_all_devices\">הסר את כל המכשירים</string>\n    <string name=\"device_link_enabled\">הקישור מופעל</string>\n    <string name=\"device_connected\">המכשיר \\\"%s\\\" חובר</string>\n    <string name=\"device_disconnected\">המכשיר \\\"%s\\\" נותק</string>\n    <string name=\"settings_ui_scale\">קנה מידה של ממשק משתמש</string>\n    <string name=\"msg_restart_app\">נא להתחיל מחדש את היישום כדי להחיל הגדרות אלה</string>\n    <string name=\"settings_block\">חסימת תוכן</string>\n    <string name=\"msg_applying\">מחיל %s…</string>\n    <string name=\"msg_done\">בוצע</string>\n    <string name=\"settings_general\">כללי</string>\n    <string name=\"settings_video\">וִידֵאוֹ</string>\n    <string name=\"content_block_confirm_skip\">אשר בדילוג</string>\n    <string name=\"content_block_categories\">דלג על מקטעים</string>\n    <string name=\"content_block_sponsor\">נותן חסות</string>\n    <string name=\"content_block_intro\">הנפשת הפסקה/הקדמה</string>\n    <string name=\"content_block_interaction\">תזכורת לאינטראקציה (הרשמה למינוי)</string>\n    <string name=\"content_block_outro\">כרטיסי סיום/קרדיטים</string>\n    <string name=\"content_block_self_promo\">קידום ללא תשלום/עצמי</string>\n    <string name=\"content_block_music_off_topic\">מקטע שאינו מוזיקה בקליפ</string>\n    <string name=\"confirm_segment_skip\">לדלג על מקטע \\\"%s\\\"\\\\?</string>\n    <string name=\"cancel_segment_skip\">בטל דילוג על מקטע</string>\n    <string name=\"msg_skipping_segment\">מדלג על מקטע \\\"%s\\\"…</string>\n    <string name=\"content_block_notification_type\">סוג התראה</string>\n    <string name=\"content_block_notify_none\">ללא התראה</string>\n    <string name=\"content_block_notify_toast\">הודעה קופצת</string>\n    <string name=\"content_block_notify_dialog\">דו-שיח לאישור</string>\n    <string name=\"return_to_launcher\">חזור למשגר מתוך ערוצים/חיפוש של טלוויזית אנדרואיד</string>\n    <string name=\"intent_force_close\">כפה יציאה אם הסרטון נפתח ממקור חיצוני</string>\n    <string name=\"btn_confirm\">אישור</string>\n    <string name=\"player_low_video_quality\">איכות סרטון נמוכה</string>\n    <string name=\"remote_session_closed\">הפעלה מרוחקת נסגרה</string>\n    <string name=\"msg_mode_switch_error\">לא ניתן לעבור למצב תצוגה של \\\"%s\\\"</string>\n    <string name=\"msg_player_error\">אירעה שגיאת נגן %s. מתחיל מחדש את ההפעלה…</string>\n    <string name=\"video_preset_disabled\">ללא קביעה מוגדרת מראש</string>\n    <string name=\"video_preset_enabled\">פורמט הסרטון הבא יוגדר בהתאם לקביעה מוגדרת מראש</string>\n    <string name=\"player_sleep_timer\">טיימר שינה</string>\n    <string name=\"player_show_quality_info\">הצג מידע על איכות בסרגל הפקדים</string>\n    <string name=\"player_remember_each_speed\">זכור מהירות של כל סרטון</string>\n    <string name=\"player_remember_speed_none\">אף סרטון</string>\n    <string name=\"player_remember_speed_all\">זהה בכל הסרטונים</string>\n    <string name=\"player_remember_speed_each\">לכל סרטון</string>\n    <string name=\"msg_player_error_source\">לא ניתן להוריד את הסרטון</string>\n    <string name=\"msg_player_error_renderer\">הפורמט שנבחר אינו נתמך.\\nנסה לבחור אחד אחר.\\nאם זה לא יעזור נסה לאתחל מחדש את המכשיר.</string>\n    <string name=\"msg_player_error_video_renderer\">פורמט הוידאו שנבחר אינו נתמך.\\nנסה לבחור אחד אחר על ידי לחיצה על לחצן HQ בנגן.\\nאם זה לא יעזור נסה לאתחל מחדש את המכשיר.</string>\n    <string name=\"msg_player_error_audio_renderer\">פורמט השמע שנבחר אינו נתמך.\\nנסה לבחור אחד אחר על ידי לחיצה על לחצן HQ בנגן.\\nאם זה לא יעזור נסה לאתחל מחדש את המכשיר.</string>\n    <string name=\"msg_player_error_subtitle_renderer\">פורמט הכתוביות שנבחר אינו נתמך.\\nנסה לבחור אחד אחר על ידי לחיצה ארוכה על לחצן CC בנגן.\\nאם זה לא יעזור נסה לאתחל מחדש את המכשיר.</string>\n    <string name=\"msg_player_error_unexpected\">שגיאת מפענח וידאו לא ידועה</string>\n    <string name=\"video_aspect\">יחס גובה-רוחב</string>\n    <string name=\"player_tweaks\">אפשרויות מפתח</string>\n    <string name=\"header_uploads\">העלאות</string>\n    <string name=\"player_show_global_clock\">הצג שעון תמיד על המסך</string>\n    <string name=\"player_show_global_ending_time\">הצג את שעת הסיום תמיד על המסך</string>\n    <string name=\"app_corner_clock\">בית: שעון בפינה השמאלית העליונה</string>\n    <string name=\"player_corner_clock\">נגן: שעון בפינה הימנית העליונה</string>\n    <string name=\"player_corner_ending_time\">נגן: זמן סיום בפינה הימנית העליונה</string>\n    <string name=\"uploads_old_look\">החזר את המראה הישן של מדור ההעלאות</string>\n    <string name=\"background_playback_activation\">הפעלה ברקע (שפעול)</string>\n    <string name=\"channels_old_look\">החזר את המראה הישן של מדור הערוצים</string>\n    <string name=\"channels_auto_load\">טען אוטומטית את תוכן מדור הערוצים</string>\n    <string name=\"return_to_background_video\">חזור לסרטון שפועל ברקע</string>\n    <string name=\"pin_unpin_from_sidebar\">הוסף/הסר מסרגל הצד</string>\n    <string name=\"pin_unpin_playlist\">הוסף/הסר פלייליסט מסרגל הצד</string>\n    <string name=\"pin_unpin_channel\">הוסף/הסר ערוץ מסרגל הצד</string>\n    <string name=\"pin_playlist\">הוסף פלייליסט לסרגל הצד</string>\n    <string name=\"pin_channel\">הוסף ערוץ לסרגל הצד</string>\n    <string name=\"pinned_to_sidebar\">נוסף לסרגל הצד</string>\n    <string name=\"unpin_from_sidebar\">הסר מסרגל הצד</string>\n    <string name=\"unpin_group_from_sidebar\">הסר מקבוצת המינויים</string>\n    <string name=\"unpinned_from_sidebar\">הוסר מסרגל הצד</string>\n    <string name=\"dialog_select_country\">מדינה</string>\n    <string name=\"settings_language_country\">שפה/מדינה</string>\n    <string name=\"share_embed_link\">שתף קישור להטמעה</string>\n    <string name=\"card_text_scroll_factor\">מהירות גלילת טקסט של כרטיס</string>\n    <string name=\"preferred_update_source\">בחר מקור עדכון</string>\n    <string name=\"hide_shorts\">הסתר Shorts מהמינויים</string>\n    <string name=\"hide_shorts_channel\">הסתר Shorts מערוץ</string>\n    <string name=\"key_remapping\">מיפוי מחדש של מקשים</string>\n    <string name=\"screen_dimming\">עמעום מסך</string>\n    <string name=\"removed_from_playback_queue\">הוסר מתור ההפעלה</string>\n    <string name=\"added_to_playback_queue\">נוסף לתור ההפעלה</string>\n    <string name=\"add_remove_from_playback_queue\">הוסף/הסר מתור ההפעלה</string>\n    <string name=\"add_to_playback_queue\">הוסף לתור ההפעלה</string>\n    <string name=\"remove_from_playback_queue\">הסר מתור ההפעלה</string>\n    <string name=\"proxy_enabled\">פרוקסי מופעל</string>\n    <string name=\"proxy_disabled\">פרוקסי מושבת</string>\n    <string name=\"uploads_row_name\">העלאות</string>\n    <string name=\"playlists_row_name\">פלייליסטים שנוצרו</string>\n    <string name=\"popular_uploads_row_name\">העלאות פופולריות</string>\n    <string name=\"news_row_name\">חדשות</string>\n    <string name=\"breaking_news_row_name\">מבזק חדשות</string>\n    <string name=\"covid_news_row_name\">חדשות קוביד-19</string>\n    <string name=\"enable_voice_search\">אפשר חיפוש גלובלי (דרושה תמיכת קושחה)</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">הירשם/בטל הרשמה לערוץ</string>\n    <string name=\"app_backup_restore\">גיבוי/שחזור</string>\n    <string name=\"app_backup\">גיבוי נתוני יישום</string>\n    <string name=\"app_restore\">שחזור נתוני יישום</string>\n    <string name=\"skip_each_segment_once\">אל תדלג על מקטעים בשנית</string>\n    <string name=\"disable_ok_long_press\">השבת לחיצה ארוכה של לחצן אישור</string>\n    <string name=\"disable_ok_long_press_desc\">מיועד לשלטים תקולים שבהם כפתור האישור אינו מתפקד כראוי</string>\n    <string name=\"player_time_correction\">הצג את השעה עם תיקון מהירות</string>\n    <string name=\"sponsor_color_markers\">סמני צבע בסרגל ההתקדמות</string>\n    <string name=\"refresh_section\">רענן מדור</string>\n    <string name=\"option_disabled\">מושבת</string>\n    <string name=\"live_now_row_name\">בשידור חי עכשיו</string>\n    <string name=\"run_in_background\">הפעלה ברקע</string>\n    <string name=\"settings_remote_control\">שליטה מרחוק</string>\n    <string name=\"background_service_started\">שירות רקע התחיל</string>\n    <string name=\"dialog_add_to\">הוסף ל-%s</string>\n    <string name=\"dialog_remove_from\">הסר מ-%s</string>\n    <string name=\"added_to\">נוסף ל-%s</string>\n    <string name=\"removed_from\">הוסר מ-%s</string>\n    <string name=\"video_preset_adaptive\">אדפטיבית</string>\n    <string name=\"content_block_preview_recap\">תצוגה מקדימה או סיכום של הסרטון</string>\n    <string name=\"content_block_highlight\">נקודה מעניינת (סימניה)</string>\n    <string name=\"removed_from_history\">הסרטון הוסר מההיסטוריה</string>\n    <string name=\"remove_from_history\">מחק מההיסטוריה</string>\n    <string name=\"upload_date\">תאריך העלאה</string>\n    <string name=\"upload_date_any\">כל הזמנים</string>\n    <string name=\"upload_date_today\">היום</string>\n    <string name=\"upload_date_this_week\">השבוע</string>\n    <string name=\"upload_date_this_month\">החודש</string>\n    <string name=\"upload_date_this_year\">השנה</string>\n    <string name=\"double_refresh_rate\">קצב רענון כפול</string>\n    <string name=\"upload_date_last_hour\">שעה אחרונה</string>\n    <string name=\"focus_on_search_results\">התמקד אוטומטית בתוצאות חיפוש</string>\n    <string name=\"context_menu\">תפריט הקשר</string>\n    <string name=\"context_menu_sorting\">מיון תפריט תלוי הקשר</string>\n    <string name=\"add_remove_from_recent_playlist\">הוסף/הסר מפלייליסט אחרון</string>\n    <string name=\"hide_settings_section\">הסתר מדור הגדרות (מסוכן!)</string>\n    <string name=\"volume\">נפח %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s שניות</string>\n    <string name=\"audio_shift_sec\">%s שניות</string>\n    <string name=\"ui_hide_timeout_sec\">%s שניות</string>\n    <string name=\"screen_dimming_timeout_min\">%s דקות</string>\n    <string name=\"mark_all_channels_watched\">סמן את כל הערוצים כנצפו</string>\n    <string name=\"move_section_up\">הזז מדור למעלה</string>\n    <string name=\"move_section_down\">הזז מדור למטה</string>\n    <string name=\"player_buttons\">הגדר לחצני נגן</string>\n    <string name=\"action_playlist_add\">הוסף לפלייליסט</string>\n    <string name=\"action_video_stats\">סטטיסטיקת סרטון</string>\n    <string name=\"action_subscribe\">הרשמה למינוי</string>\n    <string name=\"action_channel\">פתח ערוץ</string>\n    <string name=\"action_pip\">תמונה בתוך תמונה</string>\n    <string name=\"action_video_info\">תיאור סרטון</string>\n    <string name=\"action_screen_off\">מסך כבוי</string>\n    <string name=\"action_sound_off\">שמע כבוי</string>\n    <string name=\"action_playback_queue\">תור הפעלה</string>\n    <string name=\"action_video_speed\">מהירות סרטון</string>\n    <string name=\"action_subtitles\">כתוביות</string>\n    <string name=\"action_like\">אהבתי</string>\n    <string name=\"action_dislike\">דיסלייק</string>\n    <string name=\"action_play_pause\">הפעל/השהה</string>\n    <string name=\"action_repeat_mode\">מצב הפעלה</string>\n    <string name=\"action_next\">הסרטון הבא</string>\n    <string name=\"action_previous\">הסרטון הקודם</string>\n    <string name=\"action_high_quality\">איכות סרטון</string>\n    <string name=\"content_block_no_skipping_mode\">מצב ללא דילוגים</string>\n    <string name=\"content_block_action_type\">בחר פעולה</string>\n    <string name=\"content_block_action_none\">לא לעשות כלום</string>\n    <string name=\"content_block_action_only_skip\">רק לדלג</string>\n    <string name=\"content_block_action_toast\">דלג עם התראה</string>\n    <string name=\"content_block_action_dialog\">הצג דו-שיח לאישור</string>\n    <string name=\"content_block_filler\">מחוץ לנושא (פילר)</string>\n    <string name=\"keyboard_auto_show\">הצג מקלדת באופן אוטומטי</string>\n    <string name=\"cancel_dialog\">ביטול</string>\n    <string name=\"msg_player_error_source2\">כתובת URL לא עובדת או זמן מכשיר שגוי.\\nבדרך כלל, זה באג ביישום.</string>\n    <string name=\"msg_player_error_video_source\">כתובת URL של סרטון לא עובדת או זמן מכשיר שגוי.\\nבדרך כלל, זה באג ביישום.</string>\n    <string name=\"msg_player_error_audio_source\">כתובת URL של שמע לא עובדת או זמן מכשיר שגוי.\\nבדרך כלל, זה באג ביישום.</string>\n    <string name=\"msg_player_error_subtitle_source\">כתובת URL של כתוביות לא עובדת או זמן מכשיר שגוי.\\nבדרך כלל, זה באג ביישום.</string>\n    <string name=\"hide_upcoming\">הסתר בקרוב מהמינויים</string>\n    <string name=\"hide_upcoming_channel\">הסתר בקרוב מהערוצים</string>\n    <string name=\"hide_upcoming_home\">הסתר בקרוב מהבית</string>\n    <string name=\"search_background_playback\">הפעלה ברקע בזמן חיפוש/גלישה בערוץ</string>\n    <string name=\"trending_row_name\">מגמות</string>\n    <string name=\"player_seek_type\">התנהגות דילוג</string>\n    <string name=\"player_seek_regular\">רגילה</string>\n    <string name=\"player_seek_confirmation_pause\">עם אישור (השהה בזמן דילוג)</string>\n    <string name=\"player_seek_confirmation_play\">עם אישור (נגן תוך כדי דילוג)</string>\n    <string name=\"hide_shorts_from_home\">הסתר Shorts מהבית</string>\n    <string name=\"finish_on_disconnect\">סגור את היישום לאחר ניתוק הטלפון/טאבלט</string>\n    <string name=\"update_error\">שגיאת עדכון</string>\n    <string name=\"hide_shorts_from_search\">הסתר Shorts מתוצאות החיפוש</string>\n    <string name=\"hide_shorts_from_history\">הסתר Shorts מההיסטוריה</string>\n    <string name=\"hide_shorts_from_trending\">הסתר Shorts מהמגמות</string>\n    <string name=\"disable_screensaver\">השבת את שומר המסך</string>\n    <string name=\"description_not_found\">התיאור לא נמצא</string>\n    <string name=\"proxy_port_hint\">יציאת פרוקסי</string>\n    <string name=\"proxy_host_hint\">שם מארח פרוקסי או IP</string>\n    <string name=\"proxy_username_hint\">שם משתמש פרוקסי</string>\n    <string name=\"proxy_password_hint\">סיסמת פרוקסי</string>\n    <string name=\"proxy_type\">סוג פרוקסי</string>\n    <string name=\"enable_web_proxy\">השתמש בפרוקסי אינטרנט</string>\n    <string name=\"proxy_not_supported\">השתמש בפרוקסי אינטרנט (צריך אנדרואיד 4.4+)</string>\n    <string name=\"proxy_test_btn\">בדוק</string>\n    <string name=\"proxy_settings_title\">הגדרות שרת פרוקסי</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">בדיקה #%d: בוטלה.</string>\n    <string name=\"proxy_type_invalid\">סוג פרוקסי לא מוגדר.</string>\n    <string name=\"proxy_host_invalid\">מארח פרוקסי לא מוגדר.</string>\n    <string name=\"proxy_port_invalid\">יציאת פרוקסי לא חוקית, חייבת להיות &gt; 0.</string>\n    <string name=\"proxy_credentials_invalid\">שם משתמש או סיסמה לא חוקיים.</string>\n    <string name=\"proxy_test_aborted\">הבדיקה בוטלה, נא לתקן את הגדרות הפרוקסי קודם.</string>\n    <string name=\"proxy_application_aborted\">נא לתקן את הגדרות הפרוקסי קודם.</string>\n    <string name=\"proxy_test_start\">בדיקה #%d: %s …</string>\n    <string name=\"proxy_test_error\">שגיאה #%d: %s</string>\n    <string name=\"proxy_test_status\">מצב #%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">כתובת קובץ התצורה (*.ovpn)</string>\n    <string name=\"enable_openvpn\">השתמש ב-OpenVPN</string>\n    <string name=\"openvpn_settings_title\">OpenVPN הגדרות</string>\n    <string name=\"openvpn_application_aborted\">נא לתקן את הגדרות OpenVPN קודם.</string>\n    <string name=\"openvpn_test_aborted\">הבדיקה בוטלה, נא לתקן את הגדרות OpenVPN קודם.</string>\n    <string name=\"openvpn_address_invalid\">כתובת התצורה של OpenVPN לא מוגדרת.</string>\n    <string name=\"internet_censorship\">צנזורת אינטרנט</string>\n    <string name=\"rename_section\">שנה את שם מדור זה</string>\n    <string name=\"simple_edit_value_hint\">הזן ערך</string>\n    <string name=\"seek_interval\">מרווח דילוג</string>\n    <string name=\"seek_interval_sec\">%s שניות</string>\n    <string name=\"subtitle_system\">סגנון מערכת (הגדרות אנדרואיד &gt; נגישות)</string>\n    <string name=\"subtitle_scale\">קנה מידה של כתוביות</string>\n    <string name=\"audio_sync_fix_desc\">דרך חלופית לסנכרן שמע/וידאו. זו נחשבת למיושנת, אבל במקרים מסוימים, עשויה לעזור.</string>\n    <string name=\"audio_sync_fix\">תיקון סנכרון שמע</string>\n    <string name=\"ambilight_ratio_fix_desc\">מתקן תאורת הטיה נעדרת. מתקן יחס גובה-רוחב או קנה מידה שגויים של וידאו. מתקן צילומי מסך ריקים. משפיע על הביצועים!</string>\n    <string name=\"ambilight_ratio_fix\">תיקון תאורת אווירה/יחס גובה-רוחב/קנה מידה של וידאו/צילומי מסך</string>\n    <string name=\"force_legacy_codecs_desc\">משפר באופן משמעותי את הביצועים במכשירים ברמה נמוכה. הרזולוציה המרבית היא 720p.</string>\n    <string name=\"force_legacy_codecs\">כפה קודקים מדור קודם (720p)</string>\n    <string name=\"live_stream_fix_desc\">אזהרה, תיקון זה משבית את החזרה לאחור של שידורים. משפר באופן משמעותי את הביצועים של שידורים חיים במכשירים ברמה נמוכה. הרזולוציה המרבית היא 1080p.</string>\n    <string name=\"live_stream_fix_4k_desc\">אזהרה, תיקון זה משבית את החזרה לאחור של שידורים. משפר באופן משמעותי את הביצועים של שידורים חיים. הרזולוציה המרבית היא 4K.</string>\n    <string name=\"live_stream_fix\">תיקון שידור חי (1080p)</string>\n    <string name=\"live_stream_fix_4k\">תיקון שידור חי (4K)</string>\n    <string name=\"playback_notifications_fix_desc\">מסתיר התראות על שינוי רצועה. יכול להיות שימושי בקושחה מבוססת AOSP.</string>\n    <string name=\"playback_notifications_fix\">השבת התראות הפעלה</string>\n    <string name=\"amlogic_fix_desc\">תיקון נפילת פריימים במכשירים מבוססי Amlogic.</string>\n    <string name=\"amlogic_fix\">תיקון Amlogic 1080p\\@60fps</string>\n    <string name=\"tunneled_video_playback_desc\">הערה: ייתכן שההשהיה לא תפעל כראוי! הפעלת וידאו במנהרה מבטיחה יתרונות כגון סנכרון שמע/וידאו טוב יותר (סנכרון AV) והפעלה חלקה יותר. נדרש אנדרואיד 5+</string>\n    <string name=\"tunneled_video_playback\">הפעלת וידאו במנהרה (אנדרואיד 5+)</string>\n    <string name=\"master_volume\">עוצמת קול ראשית</string>\n    <string name=\"volume_limit\">מגבלת עוצמת קול</string>\n    <string name=\"player_volume\">עוצמת קול</string>\n    <string name=\"play_video\">הפעל</string>\n    <string name=\"remember_position_of_short_videos\">זכור את המיקום של סרטונים Shorts (פחות מ-5 דקות)</string>\n    <string name=\"remember_position_of_live_videos\">זכור את המיקום של שידורים חיים</string>\n    <string name=\"player_show_tooltips\">הצג תיאורי כלים של לחצנים</string>\n    <string name=\"action_like_unset\">אהבתי הוסר</string>\n    <string name=\"action_dislike_unset\">דיסלייק הוסר</string>\n    <string name=\"various_buttons\">לחצנים בחלק העליון של החלון הראשי</string>\n    <string name=\"not_compatible_with\">האפשרות שנבחרה אינה תואמת את</string>\n    <string name=\"subtitle_position\">הזזת תחתית כתוביות</string>\n    <string name=\"pressing_home\">על ידי לחיצה על HOME</string>\n    <string name=\"pressing_home_back\">על ידי לחיצה על HOME או BACK</string>\n    <string name=\"pressing_back\">על ידי לחיצה על BACK</string>\n    <string name=\"player_number_key_seek\">דלג עם מקשי מספרים</string>\n    <string name=\"save_remove_playlist\">הוסף/הסר פלייליסט ממדור הפלייליסטים</string>\n    <string name=\"save_playlist\">הוסף פלייליסט למדור הפלייליסטים</string>\n    <string name=\"remove_playlist\">הסר פלייליסט ממדור הפלייליסטים לצמיתות</string>\n    <string name=\"removed_from_playlists\">הוסר ממדור הפלייליסטים</string>\n    <string name=\"saved_to_playlists\">נוסף למדור הפלייליסטים</string>\n    <string name=\"create_playlist\">צור פלייליסט</string>\n    <string name=\"add_video_to_new_playlist\">הוסף לפלייליסט חדש</string>\n    <string name=\"cant_delete_empty_playlist\">לא ניתן למחוק פלייליסט ריק</string>\n    <string name=\"playlist\">פלייליסט</string>\n    <string name=\"rename_playlist\">שנה שם פלייליסט</string>\n    <string name=\"cant_rename_empty_playlist\">לא ניתן לשנות שם של פלייליסט ריק</string>\n    <string name=\"cant_rename_foreign_playlist\">לא ניתן לשנות שם של פלייליסט זר</string>\n    <string name=\"cant_save_playlist\">לא ניתן להוסיף פלייליסט זה</string>\n    <string name=\"enter_value\">הזן כל ערך שאינו ריק</string>\n    <string name=\"dialog_add_remove_from\">הוסף/הסר מן %s</string>\n    <string name=\"lb_playback_controls_skip_next\">דלג לבא בתור</string>\n    <string name=\"lb_playback_controls_skip_previous\">דלג לקודם בתור/החזר לאחור למיקום ההתחלה</string>\n    <string name=\"alt_presets_behavior\">התנהגות קביעות מוגדרות מראש חלופיות (הגבל רוחב פס)</string>\n    <string name=\"alt_presets_behavior_desc\">היישום ינסה לשמור על רוחב פס המתאים לקביעה מוגדרת מראש שנבחרה במקום להתאים בין רזולוציה, fps וקודק.</string>\n    <string name=\"sleep_timer\">טיימר שינה (אם לא נעשה שימוש בשלט במשך שעה אחת)</string>\n    <string name=\"sleep_timer_desc\">השהה את ההפעלה אם המשתמש לא השתמש בשלט במשך שעה אחת</string>\n    <string name=\"disable_vsync\">השבת הצמדה אל vsync</string>\n    <string name=\"disable_vsync_desc\">אפשרות זו מבטלת יישור פריימים עם אות הסנכרון האנכי של התצוגה. זה יכול לשפר את הביצועים במכשירים ברמה נמוכה על ידי שחרור משאבי מעבד.</string>\n    <string name=\"skip_codec_profile_check\">דלג על בדיקת רמת פרופיל הקודק</string>\n    <string name=\"skip_codec_profile_check_desc\">אל תבדוק תמיכת קודק כאשר מתחילים סרטון. יכול להיות מועיל על קושחה תקולה.</string>\n    <string name=\"force_sw_codec\">כפה מפענח וידאו של פענוח תוכנה</string>\n    <string name=\"force_sw_codec_desc\">יכול להפעיל כמעט כל סרטון, אבל הביצועים גרועים מאוד.</string>\n    <string name=\"playlist_order\">מיין פלייליסט</string>\n    <string name=\"playlist_order_added_date_newer_first\">תאריך הוספה (חדש יותר תחילה)</string>\n    <string name=\"playlist_order_added_date_older_first\">תאריך הוספה (ישן יותר תחילה)</string>\n    <string name=\"playlist_order_published_date_newer_first\">תאריך פרסום (חדש יותר תחילה)</string>\n    <string name=\"playlist_order_published_date_older_first\">תאריך פרסום (ישן יותר תחילה)</string>\n    <string name=\"playlist_order_popularity\">פופולריות</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">לא ניתן לעשות זאת עבור פלייליסט זר</string>\n    <string name=\"owned_playlist_warning\">שגיאה: ניתן להחיל פעולה זו רק על פלייליסט בבעלות</string>\n    <string name=\"content_block_alt_server\">השתמש בשרת חלופי</string>\n    <string name=\"content_block_alt_server_desc\">אפשר אפשרות זו אם SponsorBlock מסרב לעבוד מסיבות שונות.</string>\n    <string name=\"unset_stream_reminder\">בטל תזכורת שידור</string>\n    <string name=\"set_stream_reminder\">קבע תזכורת שידור</string>\n    <string name=\"playback_starts_shortly\">ההפעלה תתחיל באופן אוטומטי כאשר השידור יהיה מוכן</string>\n    <string name=\"starting_stream\">מתחיל את השידור…</string>\n    <string name=\"action_playlist_remove\">הסר מפלייליסט</string>\n    <string name=\"signin_view_title\">קוד המשתמש נטען…</string>\n    <string name=\"signin_view_description\">כדי להיכנס, הזן את הקוד הזה בעמוד %s\\nהערה: עובד רק עם דפדפני Firefox או Chrome.</string>\n    <string name=\"signin_view_action_text\">בוצע</string>\n    <string name=\"require_checked\">דורש \\\"%s\\\"</string>\n    <string name=\"msg_press_again_to_exit\">לחץ שוב כדי לצאת</string>\n    <string name=\"player_remaining_time\">נותרו: %s</string>\n    <string name=\"player_ending_time\">מסתיים בשעה %s</string>\n    <string name=\"badge_new_content\">תוכן חדש</string>\n    <string name=\"badge_live\">בשידור חי</string>\n    <string name=\"add_device_view_description\">כדי לקשר את המכשיר, הזן את הקוד הזה ביישום YouTube של הטלפון שלך במקטע הגדרות/צפייה בטלוויזיה\\nהערה: זה עובד רק עם המקלדת Gboard!</string>\n    <string name=\"playback_controls_repeat_pause\">השהיית חזרה</string>\n    <string name=\"playback_controls_repeat_list\">חזור על הרשימה</string>\n    <string name=\"action_subscribe_off\">הרשמה למינוי כבויה</string>\n    <string name=\"action_subscribe_on\">הרשמה למינוי פועלת</string>\n    <string name=\"skip_24_rate\">דלג על פורמטי 24fps (תיקון מצב קולנוע אמיתי\\?)</string>\n    <string name=\"skip_shorts\">דלג על Shorts</string>\n    <string name=\"player_disable_suggestions\">השבת הצעות</string>\n    <string name=\"suggestions\">הצעות</string>\n    <string name=\"feedback\">משוב</string>\n    <string name=\"sources\">מקורות</string>\n    <string name=\"releases\">שחרורים</string>\n    <string name=\"prefer_avc_over_vp9\">בחירת קודק: העדף avc על פני vp9</string>\n    <string name=\"open_chat\">צ\\'אט/תגובות</string>\n    <string name=\"open_comments\">פתח תגובות</string>\n    <string name=\"place_chat_left\">מקם את הצ\\'אט משמאל</string>\n    <string name=\"place_comments_left\">מקם תגובות מימין</string>\n    <string name=\"use_alt_speech_recognizer\">מזהה דיבור חלופי</string>\n    <string name=\"time_format\">פורמט זמן</string>\n    <string name=\"time_format_24\">24 שעות</string>\n    <string name=\"time_format_12\">12 שעות (AM/PM)</string>\n    <string name=\"use_alt_speech_recognizer_desc\">בעל באגים רציניים. השתמש בו רק אם יש לך בעיות עם מזהה ברירת המחדל.</string>\n    <string name=\"speech_recognizer\">מזהה דיבור</string>\n    <string name=\"speech_engine\">מנוע חיפוש קולי</string>\n    <string name=\"speech_recognizer_system\">מערכת (מועדפת)</string>\n    <string name=\"speech_recognizer_external_1\">חיצוני 1 (בעל באגים רציניים, כדי שיעבוד אתה צריך להשבית את הגישה למיקרופון)</string>\n    <string name=\"speech_recognizer_external_2\">חיצוני 2 (בעל באגים רציניים)</string>\n    <string name=\"real_channel_icon\">הצג סמל על לחצן הערוץ</string>\n    <string name=\"subtitle_yellow_semi_transparent\">צהוב על רקע שקוף למחצה</string>\n    <string name=\"subtitle_yellow_black\">צהוב על רקע שחור</string>\n    <string name=\"player_pixel_ratio\">יחס פיקסלים</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">אפור כהה (מונוכרום)</string>\n    <string name=\"disable_mic_permission\">נא להשבית גישת מיקרופון עבור היישום על מנת שמזהה זה יעבוד כראוי.</string>\n    <string name=\"repeat_mode_shuffle\">ערבב כל פלייליסט</string>\n    <string name=\"chat_left\">שמאל</string>\n    <string name=\"chat_right\">ימין</string>\n    <string name=\"card_real_thumbnails\">החלף תמונות ממוזערות בפריים מהסרטון</string>\n    <string name=\"card_content\">היכן יש לתפוס תמונות ממוזערות של כרטיסים</string>\n    <string name=\"thumb_quality_default\">ברירת מחדל</string>\n    <string name=\"thumb_quality_start\">תחילת הסרטון</string>\n    <string name=\"thumb_quality_middle\">אמצע הסרטון</string>\n    <string name=\"thumb_quality_end\">סוף הסרטון</string>\n    <string name=\"content_block_status\">בדוק את מצב השרת של SponsorBlock</string>\n    <string name=\"dearrow_status\">בדוק את מצב השרת של DeArrow</string>\n    <string name=\"player_show_quality_info_bitrate\">הוסף קצב סיביות למידע על האיכות</string>\n    <string name=\"player_speed_button_old_behavior\">החזר את ההתנהגות הישנה של לחצן המהירות</string>\n    <string name=\"protect_settings_with_password\">הגן על כל ההגדרות באמצעות סיסמה</string>\n    <string name=\"enter_settings_password\">הזן את סיסמת ההגדרות</string>\n    <string name=\"child_mode\">מצב ילדים</string>\n    <string name=\"child_mode_desc\">במצב זה, המשתמש לא יכול להשתמש בחיפוש או לראות תוכן מוצע כלשהו. עמוד ההגדרות יהיה מוגן סיסמה.</string>\n    <string name=\"lost_setting_warning\">הגדרות היישום ישתנו. ודא שיצרת גיבוי של ההגדרות.</string>\n    <string name=\"player_button_long_click\">לחץ לחיצה ארוכה על לחצן זה לאפשרויות נוספות</string>\n    <string name=\"pause_history\">השהה את ההיסטוריה</string>\n    <string name=\"resume_history\">המשך את ההיסטוריה</string>\n    <string name=\"disable_history\">השבת את ההיסטוריה</string>\n    <string name=\"enable_history\">אפשר היסטוריה</string>\n    <string name=\"clear_history\">נקה היסטוריה</string>\n    <string name=\"chapters\">פרקים</string>\n    <string name=\"card_multiline_subtitle\">כתוביות מרובות שורות</string>\n    <string name=\"auto_frame_rate_modes\">מצבים נתמכים</string>\n    <string name=\"loading\">טוען…</string>\n    <string name=\"hide_streams\">הסתר זרמים מהמינויים</string>\n    <string name=\"video_rotate\">סובב</string>\n    <string name=\"trending_searches\">חיפושים חמים</string>\n    <string name=\"video_duration_any\">כלשהו</string>\n    <string name=\"video_duration\">משך</string>\n    <string name=\"video_duration_under_4\">פחות מ-4 דקות</string>\n    <string name=\"video_duration_between_4_20\">4–20 דקות</string>\n    <string name=\"video_duration_over_20\">מעל 20 דקות</string>\n    <string name=\"content_type\">סוג</string>\n    <string name=\"content_type_any\">כלשהו</string>\n    <string name=\"content_type_video\">סרטון</string>\n    <string name=\"content_type_channel\">ערוץ</string>\n    <string name=\"content_type_playlist\">פלייליסט</string>\n    <string name=\"content_type_movie\">סרט</string>\n    <string name=\"video_features\">תכונות</string>\n    <string name=\"video_feature_any\">כלשהן</string>\n    <string name=\"video_feature_live\">שידור חי</string>\n    <string name=\"video_feature_4k\">4K</string>\n    <string name=\"video_feature_hdr\">HDR</string>\n    <string name=\"search_sorting\">מיין לפי</string>\n    <string name=\"sort_by_relevance\">רלוונטיות</string>\n    <string name=\"sort_by_views\">מספר צפיות</string>\n    <string name=\"sort_by_date\">תאריך העלאה</string>\n    <string name=\"sort_by_rating\">דירוג</string>\n    <string name=\"clear_search_history\">נקה היסטוריית חיפוש</string>\n    <string name=\"remove_from_subscriptions\">הסתר</string>\n    <string name=\"player_long_speed_list\">רשימת מהירויות ארוכה</string>\n    <string name=\"player_extra_long_speed_list\">רשימת מהירויות ארוכה במיוחד</string>\n    <string name=\"alt_app_icon\">סמל יישום חלופי (צריך אתחול מחדש)</string>\n    <string name=\"network_stack\">העדף ערימת רשת מסוג %s</string>\n    <string name=\"player_network_stack\">מנוע רשת</string>\n    <string name=\"cronet_desc\">ערימת הרשת של Chromium היא Cronet. ערימת הרשת Cronet יכולה להפחית את זמן ההמתנה ולהגביר את ביצועי הרשת, מה שיכול לעזור בבעיות טעינה.</string>\n    <string name=\"unlock_all_formats\">בטל את הנעילה של כל פורמטי הוידאו</string>\n    <string name=\"unlock_all_formats_desc\">במכשירים מסוימים הקושחה מדווחת באופן שגוי על פורמטים מסוימים כלא נתמכים, גם אם הם כן.\\n(למשל טלוויזיות 1080p חכמות נוטות לדווח על 4K כלא נתמך).</string>\n    <string name=\"okhttp_desc\">מנוע רשת זה הוא האיטי ביותר אך בעל יציבות טובה.</string>\n    <string name=\"select_channel_section\">פתח את המדור המתאים כאשר היישום שוגר מערוצי טלוויזית אנדרואיד</string>\n    <string name=\"enable_voice_search_desc\">היישום הרשמי יוחלף במה שנקרא \\\"גשר\\\". גשר עוזר להעביר בקשות חיפוש גלובליות ליישום שלנו.</string>\n    <string name=\"enter_master_password\">הזן סיסמה ראשית</string>\n    <string name=\"enable_master_password\">אפשר סיסמה ראשית</string>\n    <string name=\"disable_stream_buffer\">השבת חוצץ בשידורים</string>\n    <string name=\"disable_stream_buffer_desc\">תיקון למצבים שבהם השידור רחוק מדי מאחור. הערה: השידור עשוי להתחיל לפגר.</string>\n    <string name=\"sony_frame_drop_fix\">תיקון נפילת פריימים #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">תקן פיגורים בטלוויזית Sony ובכמה מכשירים אחרים. הערה: ייתכנו בעיות עם סנכרון שמע.</string>\n    <string name=\"audio_language\">שפת שמע</string>\n    <string name=\"old_home_look\">מראה ישן של מדור הבית</string>\n    <string name=\"old_channel_look\">מראה ישן של דף הערוץ</string>\n    <string name=\"hide_shorts_everywhere\">הסתר Shorts בכל מקום</string>\n    <string name=\"update_found\">עדכון</string>\n    <string name=\"volume_boost_warning\">כל הגדרה מעבר למגבלה עלולה באופן פוטנציאלי לגרום נזק לרמקולים</string>\n    <string name=\"play_video_incognito\">הפעל גלישה בסתר</string>\n    <string name=\"header_kids_home\">ילדים</string>\n    <string name=\"player_section_playlist\">השתמש בתוכן המדור הנוכחי כפלייליסט</string>\n    <string name=\"header_trending\">מגמות</string>\n    <string name=\"screensaver\">שומר מסך</string>\n    <string name=\"player_screen_off_timeout\">פסק זמן לכיבוי מסך</string>\n    <string name=\"old_update_notifications\">מראה ישן של הודעות העדכון</string>\n    <string name=\"dialog_notification\">הודע על עדכונים באמצעות תיבת דו-שיח מוקפצת</string>\n    <string name=\"mark_as_watched\">סמן כנצפה</string>\n    <string name=\"player_ui_animations\">אפשר הנפשות ממשק משתמש בנגן</string>\n    <string name=\"player_likes_count\">הצג מספר אהבתי/דיסלייק</string>\n    <string name=\"sorting_alphabetically2\">בסדר אלפביתי (מהיר, תצוגות מקדימות מונפשות)</string>\n    <string name=\"sorting_default\">ברירת מחדל (מהיר, תצוגות מקדימות מונפשות)</string>\n    <string name=\"content_block_exclude_channel\">החרג את הערוץ הזה מ-SponsorBlock</string>\n    <string name=\"content_block_stop_excluding_channel\">הפסק להחריג את הערוץ הזה מ-SponsorBlock</string>\n    <string name=\"player_chapter_notification\">התקדמות מהירה מבעד הפרקים באמצעות ההודעה הקופצת</string>\n    <string name=\"subtitle_remember\">זכור כתוביות מופעלות לכל ערוץ</string>\n    <string name=\"amazon_frame_drop_fix\">תיקון נפילת פריימים #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">מיועד למכשירי Amazon Stick. עשוי לעבוד גם על מכשירים אחרים.</string>\n    <string name=\"default_stack_desc\">מנוע רשת מובנה. מנוע זה עשוי להיות בעל יציבות טובה יותר במצבים מסוימים.</string>\n    <string name=\"item_postion\">מיקום של</string>\n    <string name=\"player_screen_off_dimming\">כמות עמעום מסך כבוי</string>\n    <string name=\"screensaver_timout\">זמן קצוב לשומר מסך</string>\n    <string name=\"screensaver_dimming\">כמות עמעום שומר מסך</string>\n    <string name=\"player_ui_on_next\">הצג את ממשק המשתמש של הנגן בעת מעבר לסרטון הבא</string>\n    <string name=\"autogenerated\">נוצר אוטומטית</string>\n    <string name=\"player_auto_volume\">התאמת עוצמת קול אוטומטית</string>\n    <string name=\"header_shorts\">Shorts</string>\n    <string name=\"player_global_focus\">ניווט חלק בין שורות לחצני הנגן</string>\n    <string name=\"auto_history\">אוטומטי (השתמש בהגדרות חשבון)</string>\n    <string name=\"remember_position_subscriptions\">זכור את המיקום האחרון שנצפה במינויים</string>\n    <string name=\"remember_position_pinned\">זכור את המיקום האחרון שנצפה בפלייליסטים מוצמדים</string>\n    <string name=\"msg_player_unknown_error\">שגיאה לא ידועה</string>\n    <string name=\"header_notifications\">התראות</string>\n    <string name=\"disable_search_history\">השבת את היסטוריית החיפושים</string>\n    <string name=\"unlock_high_bitrate_formats\">בטל את הנעילה של פורמטי 1080p vp9 עם קצב סיביות גבוה</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">בטל את הנעילה של פורמטי mp4a עם קצב סיביות גבוה</string>\n    <string name=\"color_scheme_blue\">כחול</string>\n    <string name=\"color_scheme_dark_blue\">כחול כהה</string>\n    <string name=\"player_speed_per_channel\">לכל ערוץ</string>\n    <string name=\"disable_popular_searches\">השבת שאילתות חיפוש פופולריות</string>\n    <string name=\"multi_profiles\">השתמש בהגדרות נפרדות לכל חשבון</string>\n    <string name=\"protect_account_with_password\">הגן על חשבון זה באמצעות סיסמה</string>\n    <string name=\"enter_account_password\">הזן את סיסמת החשבון</string>\n    <string name=\"show_connect_messages\">הצג הודעות חיבור</string>\n    <string name=\"prefer_avc_over_vp9_desc\">אזהרה: 1080p מקסימום</string>\n    <string name=\"play_next\">הבא בתור</string>\n    <string name=\"hide_watched_from_subscriptions\">הסתר סרטונים שנצפו מהמינויים</string>\n    <string name=\"hide_watched_from_notifications\">הסתר סרטונים שנצפו מההתראות</string>\n    <string name=\"hide_unwanted_content\">הסתר תוכן לא רצוי</string>\n    <string name=\"hide_watched_from_home\">הסתר סרטונים שנצפו מהבית</string>\n    <string name=\"hide_watched_from_watch_later\">הסתר סרטונים שנצפו מהפלייליסט \\'לצפייה בהמשך\\'</string>\n    <string name=\"remote_control_permission\">שליטה מרחוק ברקע דורשת הרשאת שכבת-על</string>\n    <string name=\"login_from_browser\">היכנס מהדפדפן</string>\n    <string name=\"disable_remote_history\">השבת היסטוריה בעת שימוש בשליטה מרחוק</string>\n    <string name=\"keyboard_fix\">מנע מלחצן האישור לפתוח את המקלדת (G20s ואחרות)</string>\n    <string name=\"nothing_found\">שום דבר לא נמצא</string>\n    <string name=\"auto_frame_rate_desc\">אפשרות זו מסירה ריצוד בסצנות שבהן המצלמה נעה מהר, למשל הזרמת ספורט</string>\n    <string name=\"channel_filter_hint\">סינון ערוצים</string>\n    <string name=\"player_loop_shorts\">הפעל Shorts בלופ</string>\n    <string name=\"player_quick_shorts_skip\">דלג על Shorts עם לחצני שמאל/ימין</string>\n    <string name=\"channels_filter\">הצג שדה סינון ערוצים בתוך מדור הערוצים</string>\n    <string name=\"channel_search_bar\">סרגל חיפוש בתוך דף הערוץ</string>\n    <string name=\"header_sports\">ספורט</string>\n    <string name=\"keep_finished_activities\">שמור על פעילויות שהסתיימו</string>\n    <string name=\"disable_channels_service\">השבת את שירות הערוצים</string>\n    <string name=\"replace_titles\">החלף כותרות</string>\n    <string name=\"enable\">אפשר</string>\n    <string name=\"more_info\">עוד מידע</string>\n    <string name=\"about_sponsorblock\">אודות SponsorBlock</string>\n    <string name=\"about_dearrow\">אודות DeArrow</string>\n    <string name=\"replace_thumbnails\">החלף תמונות ממוזערות</string>\n    <string name=\"crowdsourced_thumbnails\">תמונות ממוזערות במיקור המונים</string>\n    <string name=\"crowdsoursed_titles\">כותרות במיקור המונים</string>\n    <string name=\"dearrow_not_submitted_thumbs\">מקור תמונה ממוזערת (אם אין תמונה של DeArrow)</string>\n    <string name=\"pitch_effect\">אפקט גובה הצליל</string>\n    <string name=\"fullscreen_mode\">מצב מסך מלא (ללא פסי מערכת)</string>\n    <string name=\"player_only_mode\">הצג רק את הנגן אם הסרטון נפתח מחוץ ליישום</string>\n    <string name=\"pinned_channel_rows\">הצג ערוצים מוצמדים כשורות</string>\n    <string name=\"prefer_ipv4\">העדף IPv4 DNS</string>\n    <string name=\"prefer_ipv4_desc\">יכול לתקן מצבים שבהם היישום לא עובד בכלל.\\nהערה: עלול לגרום לתקיעות ולקריסות (במיוחד במכשירי אנדרואיד 8 או Dune HD)</string>\n    <string name=\"long_press_for_settings\">לחץ לחיצה ארוכה להגדרות</string>\n    <string name=\"long_press_for_options\">לחץ לחיצה ארוכה לאפשרויות</string>\n    <string name=\"device_specific_backup\">גיבוי למכשיר זה בלבד</string>\n    <string name=\"local_backup\">גיבוי מקומי</string>\n    <string name=\"auto_backup\">גיבוי אוטומטי (פעם ביום)</string>\n    <string name=\"open_playlist\">פתח פלייליסט</string>\n    <string name=\"not_recommend_channel\">אל תמליץ על ערוץ</string>\n    <string name=\"you_wont_see_this_channel\">לא תראה ערוץ זה בהמלצות</string>\n    <string name=\"unknown_source_error\">שגיאת מקור לא ידועה</string>\n    <string name=\"unknown_renderer_error\">שגיאת מעבד תצוגה לא ידועה</string>\n    <string name=\"player_quick_skip_videos\">דלג על סרטונים רגילים עם לחצני שמאל/ימין</string>\n    <string name=\"repeat_mode_reverse_list\">נגן את הפלייליסט או את סרטוני הערוץ בסדר הפוך</string>\n    <string name=\"calm_msg\">אנו מטפלים בבעיה. בדוק אם קיימים עדכונים מעת לעת</string>\n    <string name=\"without_picture\">ללא תמונה</string>\n    <string name=\"video_disabled\">סרטון מושבת</string>\n    <string name=\"playback_queue_category_title\">תור הפעלה</string>\n    <string name=\"video_buffer\">חוצץ סרטון</string>\n    <string name=\"video_buffer_size_none\">ללא</string>\n    <string name=\"applying_fix\">אל תסגור את הנגן. מחיל את התיקון…</string>\n    <string name=\"hide_mixes\">הסתר מיקסים</string>\n    <string name=\"player_global_focus_desc\">תכונה זו משפיעה על לחצן הנגן שיקבל מיקוד כאשר מנווטים בין שורות לחצני הנגן</string>\n    <string name=\"disable_network_error_fixing\">השבת תיקון שגיאת רשת אוטומטי</string>\n    <string name=\"disable_network_error_fixing_desc\">אתה כנראה צריך לאפשר אפשרות זו אם אתה משתמש ב-VPN</string>\n    <string name=\"recommended\">מומלצים</string>\n    <string name=\"add_to_subscriptions_group\">הוסף/הסר מקבוצת מינויים</string>\n    <string name=\"new_subscriptions_group\">קבוצה חדשה</string>\n    <string name=\"rename_group\">שנה את שם קבוצת המינויים</string>\n    <string name=\"screen_dimming_amount\">כמות עמעום מסך</string>\n    <string name=\"screen_dimming_timeout\">פסק זמן לעמעום מסך</string>\n    <string name=\"playlists_rows\">הצג מדור פלייליסטים כשורות</string>\n    <string name=\"import_subscriptions_group\">ייבוא</string>\n    <string name=\"lb_playback_controls_closed_captioning_disable\">השבת כתוביות סמויות</string>\n    <string name=\"lb_playback_controls_closed_captioning_enable\">הפעל כתוביות סמויות</string>\n    <string name=\"my_videos\">הסרטונים שלי</string>\n    <string name=\"player_audio_focus\">מיקוד שמע (השהה אם זוהו נגנים אחרים)</string>\n    <string name=\"paid_content_notification\">התראת תוכן בתשלום</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-hi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">होम</string>\n  <string name=\"title_search\">खोजे</string>\n  <string name=\"header_subscriptions\">सदस्यता</string>\n  <string name=\"header_history\">इतिहास</string>\n  <string name=\"header_music\">संगीत</string>\n  <string name=\"header_news\">समाचार</string>\n  <string name=\"header_gaming\">गेमिंग</string>\n  <string name=\"header_playlists\">प्लेलिस्ट</string>\n  <string name=\"header_settings\">सेटिंग</string>\n  <string name=\"header_channels\">चैनल</string>\n  <string name=\"subscriptions_signin_title\">पसंदीदा चैनलों से नवीनतम देखें</string>\n  <string name=\"subscriptions_signin_subtitle\">अपनी सदस्यताएँ देखने के लिए साइन इन करें</string>\n  <string name=\"subscriptions_signin_button_text\">साइन इन करें</string>\n  <string name=\"library_signin_to_show_more\">आपके द्वारा पसंद,सहेजे,सब्सक्राइब वीडियो देखें</string>\n  <string name=\"library_signin_subtitle\">अपनी लाइब्रेरी देखने के लिए साइन इन करें</string>\n  <string name=\"action_signin\">साइन इन करें</string>\n  <string name=\"title_video_formats\">वीडियो प्रारूप</string>\n  <string name=\"title_audio_formats\">ऑडियो प्रारूप</string>\n  <string name=\"subtitle_category_title\">उपशीर्षक</string>\n  <string name=\"video_max_quality\">स्वत:(अधिकतम गुणवत्ता)</string>\n  <string name=\"audio_max_quality\">स्वत:(अधिकतम गुणवत्ता)</string>\n  <string name=\"subtitles_disabled\">उपशीर्षक चालू</string>\n  <string name=\"auto_frame_rate\">स्वत: फ्रेम दर</string>\n  <string name=\"frame_rate_correction\">निश्चित एफपीएस:\\n%s</string>\n  <string name=\"category_background_playback\">पृष्ठभूमि प्लेबैक</string>\n  <string name=\"not_implemented\">कार्यान्वित नहीं</string>\n  <string name=\"option_background_playback_off\">बंद</string>\n  <string name=\"option_background_playback_pip\">चित्र में चित्र</string>\n  <string name=\"option_background_playback_only_audio\">केवल ध्वनि</string>\n  <string name=\"playback_settings\">प्लेबैक सेटिंग्स</string>\n  <string name=\"video_speed\">वीडियो गति</string>\n  <string name=\"resolution_switch\">गुणवत्ता बदले</string>\n  <string name=\"video_buffer\">वीडियो बफर</string>\n  <string name=\"video_buffer_size_low\">निम्न</string>\n  <string name=\"video_buffer_size_med\">मध्यम</string>\n  <string name=\"video_buffer_size_high\">उच्च</string>\n  <string name=\"update_changelog\">नये बदलाव</string>\n  <string name=\"install_update\">अद्यतन स्थापित करें</string>\n  <string name=\"section_is_empty\">ओह! यहां कुछ नहीं है।</string>\n  <string name=\"dialog_add_to_playlist\">प्लेलिस्ट में जोड़ें</string>\n  <string name=\"msg_signed_users_only\">केवल पंजीकृत उपयोगकर्ता</string>\n  <string name=\"msg_cant_load_content\">ओह! सामग्री लोड नहीं की जा सकती \\n1) नेटवर्क कनेक्शन जांचें \\n2) अपने खाते में साइन इन करने का प्रयास करें</string>\n  <string name=\"title_video_presets\">वीडियो प्रीसेट</string>\n  <string name=\"settings_accounts\">खाता</string>\n  <string name=\"settings_left_panel\">श्रेणियां संपादित करें</string>\n  <string name=\"settings_themes\">विषय</string>\n  <string name=\"settings_other\">अन्य</string>\n  <string name=\"settings_player\">वीडियो प्लेयर</string>\n  <string name=\"settings_language\">भाषा: हिन्दी</string>\n  <string name=\"settings_linked_devices\">जोड़े गए उपकरण</string>\n  <string name=\"settings_about\">के बारे में</string>\n  <string name=\"dialog_account_list\">खाता चुनें</string>\n  <string name=\"dialog_account_none\">कोई नहीं</string>\n  <string name=\"dialog_remove_account\">खाता हटाएं</string>\n  <string name=\"dialog_add_account\">खाता जोड़ें</string>\n  <string name=\"default_lang\">डिफ़ॉल्ट</string>\n  <string name=\"dialog_select_language\">भाषा</string>\n  <string name=\"subtitle_default\">डिफ़ॉल्ट</string>\n  <string name=\"subtitle_white_semi_transparent\">अर्द्धपारदर्शी पृष्ठभूमि</string>\n  <string name=\"subtitle_style\">उपशीर्षक शैली</string>\n  <string name=\"subtitle_language\">उपशीर्षक भाषा</string>\n  <string name=\"action_search\">खोज शुरू करे</string>\n  <string name=\"settings_main_ui\">उपयोगकर्ता इंटरफ़ेस</string>\n  <string name=\"dialog_main_ui\">उपयोगकर्ता इंटरफ़ेस</string>\n  <string name=\"card_animated_previews\">एनिमेटेड पूर्वावलोकन</string>\n  <string name=\"web_site\">वेबसाइट</string>\n  <string name=\"donation\">दान</string>\n  <string name=\"dialog_about\">के बारे में</string>\n  <string name=\"dialog_player_ui\">वीडियो प्लेयर</string>\n  <string name=\"player_show_ui_on_pause\">रोकने पर यूआई दिखाएं</string>\n  <string name=\"player_pause_on_ok\">ओके कुंजी से प्लेबैक रोके </string>\n  <string name=\"player_ok_button_behavior\">ओके बटन व्यवहार</string>\n  <string name=\"player_only_ui\">केवल यूआई</string>\n  <string name=\"player_ui_and_pause\">यूआई और रोके</string>\n  <string name=\"player_only_pause\">केवल रोके</string>\n  <string name=\"check_for_updates\">अद्यतन के लिए जांचे</string>\n  <string name=\"update_not_found\">आप नवीनतम संस्करण का उपयोग कर रहे हैं</string>\n  <string name=\"update_in_progress\">कृपया इंतज़ार करे…</string>\n  <string name=\"player_ui_hide_behavior\">यूआई स्वतः छुपाये</string>\n  <string name=\"option_never\">कभी नहीँ</string>\n  <string name=\"side_panel_sections\">सेट-अप सेक्शन</string>\n  <string name=\"boot_to_section\">अनुभाग में बूट करे</string>\n  <string name=\"large_ui\">बड़ी यूआई</string>\n  <string name=\"video_grid_scale\">वीडियो ग्रिड पैमाना</string>\n  <string name=\"scale_ui\">यूआई पैमाना</string>\n  <string name=\"color_scheme\">रंग प्रणाली</string>\n  <string name=\"color_scheme_default\">डिफ़ॉल्ट</string>\n  <string name=\"color_scheme_red_grey\">लाल-स्लेटी रंग</string>\n  <string name=\"color_scheme_red\">लाल</string>\n  <string name=\"color_scheme_dark_grey\">गहरा स्लेटी</string>\n  <string name=\"disable_update_check\">अपडेट जांच बंद करें</string>\n  <string name=\"show_again\">फिर से दिखाएं</string>\n  <string name=\"check_updates_auto\">स्वत: अपडेट</string>\n  <string name=\"select_account_on_boot\">बूट पर चयन करें</string>\n  <string name=\"player_other\">विविध</string>\n  <string name=\"player_full_date\">विवरण में दिनांक दिखाएं</string>\n  <string name=\"open_channel\">चैनल खोले</string>\n  <string name=\"not_interested\">रुचि नहीं</string>\n  <string name=\"you_wont_see_this_video\">अनुशंसित वीडियो हटा दिया गया है</string>\n  <string name=\"settings_search\">खोज</string>\n  <string name=\"dialog_search\">खोज</string>\n  <string name=\"instant_voice_search\">त्वरित ध्वनि खोज</string>\n  <string name=\"option_background_playback_behind\">पिछला चलाये</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">अगले वीडियो की जानकारी लोड नहीं हुई हैं</string>\n  <string name=\"card_multiline_title\">बहु पंक्ति शीर्षक</string>\n  <string name=\"cards_style\">कार्ड शैली</string>\n  <string name=\"repeat_mode_all\">एक-एक कर सभी वीडियो चलाएं</string>\n  <string name=\"repeat_mode_one\">वर्तमान वीडियो को दोहराएं</string>\n  <string name=\"repeat_mode_pause\">प्रत्येक वीडियो के बाद प्लेबैक रोकें</string>\n  <string name=\"repeat_mode_none\">एक वीडियो के बाद प्लेबैक रोकें</string>\n  <string name=\"subscribed_to_channel\">चैनल के सदस्यता बने </string>\n  <string name=\"unsubscribed_from_channel\">चैनल की सदस्यता समाप्त करें</string>\n  <string name=\"subtitle_yellow_transparent\">पीला</string>\n  <string name=\"subtitle_white_black\">काली पृष्ठभूमि पर सफ़ेद </string>\n  <string name=\"player_seek_preview\">देखते हुए पूर्वावलोकन करें</string>\n  <string name=\"color_scheme_dark_grey_oled\">गहरा स्लेटी(ओएलईडी)</string>\n  <string name=\"channels_section_sorting\">चैनल अनुभाग छँटाई</string>\n  <string name=\"sorting_by_new_content\">नई सामग्री</string>\n  <string name=\"sorting_alphabetically\">वर्णक्रमानुसार</string>\n  <string name=\"sorting_last_viewed\">अंतिम बार देखा गया</string>\n  <string name=\"player_pause_when_seek\">आगे बढ़ाने पर रोकें</string>\n  <string name=\"playlists_style\">प्लेलिस्ट शैली</string>\n  <string name=\"playlists_style_grid\">ग्रिड</string>\n  <string name=\"playlists_style_rows\">पंक्तियाँ</string>\n  <string name=\"player_show_clock\">घड़ी दिखाए</string>\n  <string name=\"player_show_remaining_time\">शेष समय दिखाए</string>\n  <string name=\"open_channel_uploads\">चैनल के अपलोड खोले</string>\n  <string name=\"app_exit_shortcut\">ऐप से बाहर निकलें</string>\n  <string name=\"app_exit_none\">बाहर ना निकलें</string>\n  <string name=\"app_double_back_exit\">दो बार में बाहर निकलें</string>\n  <string name=\"app_single_back_exit\">एक बार में बाहर निकलें</string>\n  <string name=\"action_video_zoom\">वीडियो ज़ूम करें</string>\n  <string name=\"video_zoom\">वीडियो ज़ूम करें</string>\n  <string name=\"video_zoom_default\">डिफ़ॉल्ट</string>\n  <string name=\"video_zoom_fit_width\">चौड़ाई पर फ़िट</string>\n  <string name=\"video_zoom_fit_height\">ऊंचाई पर फ़िट</string>\n  <string name=\"video_zoom_fit_both\">चौड़ाई या ऊँचाई को फिट करें</string>\n  <string name=\"video_zoom_stretch\">खिंचाव</string>\n  <string name=\"color_scheme_teal\">टील</string>\n  <string name=\"color_scheme_teal_oled\">टील (ओएलईडी)</string>\n  <string name=\"player_seek_preview_none\">बंद</string>\n  <string name=\"player_seek_preview_single\">अकेला फ्रेम</string>\n  <string name=\"player_seek_preview_carousel\">Carousel</string>\n  <string name=\"player_seek_preview_carousel_slow\">Carousel (धीमा)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Carousel (तेज)</string>\n  <string name=\"unsubscribe_from_channel\">चैनल से सदस्यता समाप्त करें</string>\n  <string name=\"playback_queue_category_title\">प्लेबैक सूची</string>\n  <string name=\"open_playlist\">प्लेलिस्ट खोलें</string>\n  <string name=\"repeat_mode_pause_alt\">केवल प्लेलिस्ट की वीडियो लगातार चलाये</string>\n  <string name=\"card_auto_scrolled_title\">शीर्षक स्वतः स्क्रॉल करें</string>\n  <string name=\"share_link\">लिंक साझा करे</string>\n  <string name=\"subscribe_to_channel\">चैनल के सदस्य बने</string>\n  <string name=\"wait_data_loading\">जानकारी लोड होने तक इंतजार करें</string>\n  <string name=\"player_remember_speed\">गति याद रखें</string>\n  <string name=\"mark_channel_as_watched\">देखा हुआ चिन्हित करे</string>\n  <string name=\"channel_marked_as_watched\">चैनल देखा हुआ चिन्हित किया गया</string>\n  <string name=\"dialog_add_device\">उपकरण जोड़े</string>\n  <string name=\"dialog_remove_all_devices\">सभी उपकरण निकाले</string>\n  <string name=\"device_link_enabled\">उपकरण जोड़ना चालू</string>\n  <string name=\"device_connected\">\\\"%s\\\" उपकरण जोड़ा गया</string>\n  <string name=\"device_disconnected\">\\\"%s\\\" उपकरण हटाया गया</string>\n  <string name=\"settings_ui_scale\">ui पैमाना</string>\n  <string name=\"msg_restart_app\">सेटिंग्स को सहेजने के लिए ऐप पुनः खोले</string>\n  <string name=\"settings_block\">कंटेंट ब्लॉक करना</string>\n  <string name=\"msg_applying\">%s सहेज रहे है</string>\n  <string name=\"msg_done\">सफल</string>\n  <string name=\"settings_general\">सामान्य</string>\n  <string name=\"settings_video\">वीडियो</string>\n  <string name=\"content_block_categories\">श्रेणी</string>\n  <string name=\"content_block_sponsor\">प्रायोजक</string>\n  <string name=\"content_block_confirm_skip\">पुष्टि करने पर छोड़े</string>\n  <string name=\"content_block_intro\">शुरुआती एनीमेशन</string>\n  <string name=\"content_block_outro\">अंत कार्ड/श्रेय</string>\n  <string name=\"content_block_interaction\">सदस्यता अनुस्मारक</string>\n  <string name=\"content_block_self_promo\">प्रमोशन </string>\n  <string name=\"content_block_music_off_topic\">संगीत का मूक भाग</string>\n  <string name=\"confirm_segment_skip\">\\\"%s\\\" भाग छोड़े\\?</string>\n  <string name=\"msg_skipping_segment\">\\\"%s\\\" भाग छोड़ा गया</string>\n  <string name=\"content_block_notification_type\">अधिसूचना का प्रकार</string>\n  <string name=\"content_block_notify_none\">अधिसूचना के बिना </string>\n  <string name=\"content_block_notify_toast\">सूचना</string>\n  <string name=\"content_block_notify_dialog\">पुष्टीकरण सूचना</string>\n  <string name=\"return_to_launcher\">atv चैनल/खोज से लॉन्चर पर लौटें</string>\n  <string name=\"intent_force_close\">बाहरी स्रोत से वीडियो खोले जाने पर बलपूर्वक बाहर निकलें</string>\n  <string name=\"btn_confirm\">पुष्टि करें</string>\n  <string name=\"player_low_video_quality\">निम्न वीडियो गुणवत्ता</string>\n  <string name=\"remote_session_closed\">रिमोट सत्र बंद</string>\n  <string name=\"msg_mode_switch_error\">प्रदर्शन मोड को \\\"%s पर स्विच नहीं किया जा सकता</string>\n  <string name=\"msg_player_error\">प्लेयर त्रुटि %s हुई,पुनः आरम्भ हो रहा है... </string>\n  <string name=\"video_preset_disabled\">बिना प्रीसेट के</string>\n  <string name=\"video_preset_enabled\">अगले वीडियो का फॉर्मेट प्रीसेट के अनुसार सेट किया जाएगा</string>\n  <string name=\"player_sleep_timer\">स्लीप टाइमर</string>\n  <string name=\"player_show_quality_info\">वीडियो गुणवत्ता की जानकारी दिखाएं</string>\n  <string name=\"player_remember_each_speed\">प्रत्येक वीडियो की गति याद रखें</string>\n  <string name=\"player_remember_speed_none\">कोई नहीं</string>\n  <string name=\"player_remember_speed_all\">सभी वीडियो पर समान</string>\n  <string name=\"player_remember_speed_each\">प्रति वीडियो</string>\n  <string name=\"msg_player_error_source\">वीडियो डाउनलोड नहीं कर सके </string>\n  <string name=\"msg_player_error_renderer\">चयनित वीडियो प्रारूप समर्थित नहीं है</string>\n  <string name=\"msg_player_error_unexpected\">अज्ञात वीडियो डिकोडर त्रुटि</string>\n  <string name=\"video_aspect\">आस्पेक्ट अनुपात</string>\n  <string name=\"player_tweaks\">डेवलपर विकल्प</string>\n  <string name=\"header_uploads\">अपलोड</string>\n  <string name=\"player_show_global_clock\">वीडियो के ऊपरी भाग में घड़ी दिखाएं</string>\n  <string name=\"uploads_old_look\">अपलोड अनुभाग का पुराना स्वरूप वापस लाएं</string>\n  <string name=\"background_playback_activation\">पृष्ठभूमि प्लेबैक (सक्रियण)</string>\n  <string name=\"channels_old_look\">चैनल अनुभाग का पुराना स्वरूप वापस लाएं</string>\n  <string name=\"channels_auto_load\">चैनल अनुभाग सामग्री स्वतः लोड करे</string>\n  <string name=\"return_to_background_video\">पृष्ठभूमि में चल रहे वीडियो पर लौटें</string>\n  <string name=\"pin_unpin_from_sidebar\">साइडबार से पिन/अनपिन करें</string>\n  <string name=\"pinned_to_sidebar\">साइडबार पर पिन किया गया</string>\n  <string name=\"unpinned_from_sidebar\">साइडबार से पिन निकाला गया</string>\n  <string name=\"dialog_select_country\">देश</string>\n  <string name=\"settings_language_country\">भाषा/देश</string>\n  <string name=\"share_embed_link\">एम्बेड लिंक साझा करें</string>\n  <string name=\"card_text_scroll_factor\">कार्ड पाठ स्क्रॉल गति</string>\n  <string name=\"preferred_update_source\">अद्यतन स्रोत का चयन करें</string>\n  <string name=\"hide_shorts\">शॉर्ट्स को छुपाएं</string>\n  <string name=\"key_remapping\">कुंजी बदले</string>\n  <string name=\"screen_dimming\">स्क्रीन मंद करे</string>\n  <string name=\"removed_from_playback_queue\">प्लेबैक कतार से हटाया गया</string>\n  <string name=\"added_to_playback_queue\">प्लेबैक कतार में जोड़ा गया</string>\n  <string name=\"add_remove_from_playback_queue\">प्लेबैक कतार से जोड़ें/निकालें</string>\n  <string name=\"proxy_enabled\">प्रॉक्सी सक्षम</string>\n  <string name=\"proxy_disabled\">प्रॉक्सी अक्षम</string>\n  <string name=\"audio_shift\">ऑडियो शिफ्ट</string>\n  <string name=\"auto_frame_rate_applying\">स्वत: फ्रेम दर %sx%s\\@%s लागू</string>\n  <string name=\"auto_frame_rate_pause\">स्वत: फ्रेम दर बदलने पर रोके </string>\n  <string name=\"card_title_lines_num\">कार्ड शीर्षक की पंक्ति संख्या</string>\n  <string name=\"cancel_segment_skip\">भाग छोड़ना रद्द करे</string>\n  <string name=\"uploads_row_name\">अपलोड</string>\n  <string name=\"playlists_row_name\">प्लेलिस्ट बनाये </string>\n  <string name=\"popular_uploads_row_name\">लोकप्रिय अपलोड </string>\n  <string name=\"breaking_news_row_name\">ताज़ा खबर</string>\n  <string name=\"covid_news_row_name\">कोविड-19 समाचार </string>\n  <string name=\"enable_voice_search\">ध्वनि खोज चालू </string>\n  <string name=\"subscribe_unsubscribe_from_channel\">चैनल से जुड़े/निकले </string>\n  <string name=\"app_backup_restore\">बैकअप/बहाल करे </string>\n  <string name=\"app_backup\">बैकअप डाटा </string>\n  <string name=\"app_restore\">बैकअप से बहाल करे </string>\n  <string name=\"skip_each_segment_once\">भाग दोबारा न छोड़े</string>\n  <string name=\"player_time_correction\">गति अनुसार समय दिखाए</string>\n  <string name=\"disable_ok_long_press\">OK कुंजी का देर तक दबाना बंद करे</string>\n  <string name=\"player_show_ending_time\">समाप्ति समय दिखाए</string>\n  <string name=\"player_show_global_ending_time\">समाप्ति समय हमेशा दिखाए</string>\n  <string name=\"unpin_from_sidebar\">साइडबार से पिन किया गया निकाले</string>\n  <string name=\"option_disabled\">अक्षम</string>\n  <string name=\"refresh_section\">अनुभाग रिफ्रेश</string>\n  <string name=\"sponsor_color_markers\">प्रोग्रेस बार पर रंगीन भाग</string>\n  <string name=\"live_now_row_name\">लाइव</string>\n  <string name=\"run_in_background\">पृष्ठभूमि में चलाएँ</string>\n  <string name=\"settings_remote_control\">रिमोट कंट्रोल</string>\n  <string name=\"background_service_started\">पृष्ठभूमि सेवा शुरू हुई</string>\n  <string name=\"dialog_add_to\">%s में जोड़े</string>\n  <string name=\"dialog_remove_from\">%s से निकाले</string>\n  <string name=\"added_to\">%s में जोड़ा गया</string>\n  <string name=\"removed_from\">%s से निकाला गया</string>\n  <string name=\"video_preset_adaptive\">अनुकूलनीय</string>\n  <string name=\"removed_from_history\">वीडियो को इतिहास से निकाला गया</string>\n  <string name=\"remove_from_history\">इतिहास से निकाले</string>\n  <string name=\"upload_date\">अपलोड दिनाँक</string>\n  <string name=\"upload_date_any\">सभी</string>\n  <string name=\"upload_date_today\">आज</string>\n  <string name=\"upload_date_this_week\">इस सप्ताह</string>\n  <string name=\"upload_date_this_month\">इस माह</string>\n  <string name=\"upload_date_this_year\">इस साल</string>\n  <string name=\"double_refresh_rate\">दुगनी रिफ्रेश दर</string>\n  <string name=\"upload_date_last_hour\">पिछला घंटा</string>\n  <string name=\"volume\">%s%% आवाज</string>\n  <string name=\"auto_frame_rate_sec\">%s सैकंड</string>\n  <string name=\"audio_shift_sec\">%s सैकंड</string>\n  <string name=\"ui_hide_timeout_sec\">%s सैकंड</string>\n  <string name=\"screen_dimming_timeout_min\">%s मिनट</string>\n  <string name=\"mark_all_channels_watched\">सभी चैनल को देखा हुआ चिन्हित करे</string>\n  <string name=\"move_section_up\">भाग ऊपर ले जाए</string>\n  <string name=\"move_section_down\">भाग नीचे ले जाए</string>\n  <string name=\"hide_settings_section\">सेटिंग्स छुपाए</string>\n  <string name=\"add_remove_from_recent_playlist\">हाल की प्लेलिस्ट में जोड़े/निकाले</string>\n  <string name=\"focus_on_search_results\">खोज परिणाम पर केंद्रित करें,</string>\n  <string name=\"context_menu\">संदर्भ विकल्प</string>\n  <string name=\"content_block_preview_recap\">वीडियो का पूर्वावलोकन</string>\n  <string name=\"content_block_highlight\">वीडियो का चिन्हित भाग</string>\n  <string name=\"player_buttons\">प्लेयर बटन सेट करें</string>\n  <string name=\"action_playlist_add\">प्लेलिस्ट में जोड़े</string>\n  <string name=\"action_video_stats\">वीडियो आँकड़े</string>\n  <string name=\"action_subscribe\">सब्सक्राइब</string>\n  <string name=\"action_channel\">चैनल खोले</string>\n  <string name=\"action_pip\">चित्र में चित्र</string>\n  <string name=\"action_screen_off\">स्क्रीन बंद</string>\n  <string name=\"action_playback_queue\">प्लेबैक कतार</string>\n  <string name=\"action_video_speed\">वीडियो गति</string>\n  <string name=\"action_subtitles\">उपशीर्षक</string>\n  <string name=\"action_like\">पसंद</string>\n  <string name=\"action_dislike\">नापसंद</string>\n  <string name=\"action_play_pause\">चलाए/रोके</string>\n  <string name=\"action_repeat_mode\">प्लेबैक मोड</string>\n  <string name=\"action_next\">अगली वीडियो</string>\n  <string name=\"action_previous\">पिछला वीडियो</string>\n  <string name=\"action_high_quality\">वीडियो गुणवत्ता</string>\n  <string name=\"content_block_no_skipping_mode\">बिना रुके चलाए</string>\n  <string name=\"content_block_action_type\">विकल्प चुने</string>\n  <string name=\"content_block_action_none\">कुछ न करे</string>\n  <string name=\"content_block_action_only_skip\">केवल छोड़े</string>\n  <string name=\"content_block_action_toast\">अधिसूचना के साथ छोड़े</string>\n  <string name=\"content_block_action_dialog\">पुष्टिकरण सूचना</string>\n  <string name=\"keyboard_auto_show\">स्वत: कीबोर्ड दिखाए</string>\n  <string name=\"cancel_dialog\">निरस्त</string>\n  <string name=\"msg_player_error_source2\">वीडियो स्त्रोत कार्य नहीं कर रहा है</string>\n  <string name=\"content_block_filler\">असंबंधित (फिल्टर)</string>\n  <string name=\"search_background_playback\">खोजते समय PIP चालू करे </string>\n  <string name=\"disable_screensaver\">स्क्रीन सेवर बंद</string>\n  <string name=\"hide_shorts_from_history\">इतिहास से शॉर्ट्स छुपाए</string>\n  <string name=\"update_error\">अद्यतन त्रुटि</string>\n  <string name=\"finish_on_disconnect\">फोन डिस्कनेक्ट होने पर एप बंद करे</string>\n  <string name=\"hide_shorts_from_home\">होम से शॉर्ट्स छुपाए</string>\n  <string name=\"player_seek_regular\">निरंतर</string>\n  <string name=\"trending_row_name\">ट्रेंडिंग</string>\n  <string name=\"subtitle_white_transparent\">पारदर्शी पृष्ठभूमि पर सफ़ेद</string>\n  <string name=\"pin_unpin_playlist\">साइडबार से प्लेलिस्ट पिन/अनपिन करें</string>\n  <string name=\"pin_unpin_channel\">साइडबार से चैनल पिन/अनपिन करें</string>\n  <string name=\"add_to_playback_queue\">प्लेबैक कतार में जोड़े </string>\n  <string name=\"remove_from_playback_queue\">प्लेबैक कतार से निकले</string>\n  <string name=\"action_video_info\">विडियो विवरण</string>\n  <string name=\"hide_upcoming\">सब्सक्रिप्शन से आगामी विडियो छुपाएं</string>\n  <string name=\"player_seek_type\">सीक व्‍यवहार</string>\n  <string name=\"player_seek_confirmation_pause\">पुष्टिकरण के साथ (seeking के समय रोके)</string>\n  <string name=\"player_seek_confirmation_play\">पुष्टिकरण के साथ (seeking के समय चालू रखे)</string>\n  <string name=\"description_not_found\">विवरण नही मिला </string>\n  <string name=\"proxy_port_hint\">प्रॉक्सी पोर्ट </string>\n  <string name=\"proxy_host_hint\">प्रॉक्सी होस्टनाम या आईपी</string>\n  <string name=\"proxy_username_hint\">प्रॉक्सी उपयोगकर्ता</string>\n  <string name=\"proxy_password_hint\">प्रॉक्सी पासवर्ड</string>\n  <string name=\"proxy_type\">प्रॉक्सी प्रकार</string>\n  <string name=\"enable_web_proxy\">वेब प्रॉक्सी का प्रयोग करे</string>\n  <string name=\"proxy_not_supported\">वेब प्रॉक्सी का प्रयोग करे(एंड्राइड 4.4+)</string>\n  <string name=\"proxy_test_btn\">टेस्ट </string>\n  <string name=\"proxy_settings_title\">प्रॉक्सी सर्वर सेटिंग्स </string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">टेस्ट#%d:रद्द</string>\n  <string name=\"proxy_type_invalid\">प्रॉक्सी प्रकार सेट नहीं है</string>\n  <string name=\"proxy_host_invalid\">प्रॉक्सी होस्ट सेट नहीं है</string>\n  <string name=\"proxy_port_invalid\">अमान्य प्रॉक्सी पोर्ट,0&lt;आवश्यक</string>\n  <string name=\"proxy_credentials_invalid\">अमान्य उपयोगकर्ता नाम या पासवर्ड</string>\n  <string name=\"proxy_test_aborted\">परीक्षण रद्द किया गया, कृपया पहले प्रॉक्सी सेटिंग ठीक करें</string>\n  <string name=\"proxy_application_aborted\">कृपया पहले प्रॉक्सी सेटिंग ठीक करें</string>\n  <string name=\"proxy_test_start\">टेस्ट#%d: %s …</string>\n  <string name=\"proxy_test_error\">त्रुटि#%d: %s</string>\n  <string name=\"proxy_test_status\">स्थिति#%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">कॉन्फ़िग फ़ाइल पता (*.ovpn)</string>\n  <string name=\"enable_openvpn\">OpenVPN का प्रयोग करें</string>\n  <string name=\"openvpn_settings_title\">OpenVPN सेटिंग्स</string>\n  <string name=\"openvpn_application_aborted\">कृपया,पहले OpenVPN सेटिंग ठीक करें</string>\n  <string name=\"openvpn_test_aborted\">परीक्षण रद्द किया गया, कृपया पहले OpenVPN सेटिंग ठीक करें</string>\n  <string name=\"openvpn_address_invalid\">OpenVPN कॉन्फ़िग पता सेट नहीं है.</string>\n  <string name=\"internet_censorship\">इंटरनेट सेंसरशिप</string>\n  <string name=\"rename_section\">इस अनुभाग का नाम बदलें</string>\n  <string name=\"simple_edit_value_hint\">नई वैल्यू</string>\n  <string name=\"seek_interval\">सीक अन्तराल </string>\n  <string name=\"seek_interval_sec\">%s सेकंड</string>\n  <string name=\"subtitle_system\">सिस्टम शैली (एंड्राइड सेटिंग &gt; सुलभता)</string>\n  <string name=\"subtitle_scale\">उपशीर्षक आकर</string>\n  <string name=\"audio_sync_fix_desc\">ऑडियो/वीडियो को सिंक्रनाइज़ करने का एक अप्रचलित तरीका माना जाता है, लेकिन, कुछ मामलों में मदद कर सकता है</string>\n  <string name=\"audio_sync_fix\">ऑडियो सिंक फिक्स</string>\n  <string name=\"ambilight_ratio_fix\">एम्बिलाइट/आस्पेक्ट अनुपात/स्क्रीनशॉट फिक्स</string>\n  <string name=\"force_legacy_codecs_desc\">निम्न स्तर उपकरणों पर प्रदर्शन में सुधार करता है। अधिकतम गुणवत्ता 720P</string>\n  <string name=\"prefer_avc_over_vp9\">कोडेक चयन:vp9 पर avc को प्राथमिकता दें</string>\n  <string name=\"sources\">स्रोत</string>\n  <string name=\"feedback\">प्रतिक्रिया</string>\n  <string name=\"player_disable_suggestions\">सुझावों को बंद करें</string>\n  <string name=\"skip_24_rate\">24fps प्रारूप छोड़ें (असली सिनेमा मोड फिक्स\\?)</string>\n  <string name=\"action_subscribe_on\">सदस्यता चालू</string>\n  <string name=\"action_subscribe_off\">सदस्यता बंद</string>\n  <string name=\"playback_controls_repeat_list\">सूची दोहराएं</string>\n  <string name=\"add_device_view_description\">डिवाइस को लिंक करने के लिए इस कोड को अपने फ़ोन के YouTube ऐप पर  (सेटिंग &gt; टीवी पर देखें) भाग में दर्ज करें</string>\n  <string name=\"badge_live\">लाइव</string>\n  <string name=\"badge_new_content\">नई सामग्री</string>\n  <string name=\"player_ending_time\">%s पर समाप्त</string>\n  <string name=\"player_remaining_time\">शेष: %s</string>\n  <string name=\"msg_press_again_to_exit\">बाहर निकलने के लिए फिर से दबाएं</string>\n  <string name=\"require_checked\">\\\"%s आवश्यक</string>\n  <string name=\"signin_view_action_text\">पूर्ण</string>\n  <string name=\"signin_view_description\">साइन-इन करने के लिए यह कोड दर्ज करें %s</string>\n  <string name=\"signin_view_title\">उपयोगकर्ता कोड लोड हो रहा है…</string>\n  <string name=\"action_playlist_remove\">प्लेलिस्ट से हटाएं</string>\n  <string name=\"starting_stream\">स्ट्रीम शुरू हो रही है…</string>\n  <string name=\"playback_starts_shortly\">स्ट्रीम होने पर प्लेबैक शुरू हो जाएगा</string>\n  <string name=\"set_stream_reminder\">स्ट्रीम अनुस्मारक सेट करें</string>\n  <string name=\"unset_stream_reminder\">स्ट्रीम अनुस्मारक हटाये</string>\n  <string name=\"content_block_alt_server_desc\">इसे चालू करें यदि SponsorBlock काम नहीं करता है।</string>\n  <string name=\"content_block_alt_server\">वैकल्पिक सर्वर का प्रयोग करें</string>\n  <string name=\"owned_playlist_warning\">त्रुटि। यह केवल स्वयं की प्लेलिस्ट पर लागू की जा सकती है</string>\n  <string name=\"force_legacy_codecs\">बलपूर्वक लीगेसी कोडेक्स(720p) करे</string>\n  <string name=\"live_stream_fix_desc\">निम्न स्तर उपकरणों पर लाइव स्ट्रीम प्रदर्शन में सुधार करता है। अधिकतम रिज़ॉल्यूशन 1080p है</string>\n  <string name=\"live_stream_fix\">लाइव स्ट्रीम फिक्स (1080p)</string>\n  <string name=\"playback_notifications_fix_desc\">ट्रैक बदलने की सूचनाओं को छुपाता है। AOSP आधारित फर्मवेयर पर उपयोगी</string>\n  <string name=\"playback_notifications_fix\">प्लेबैक सूचनाएं बंद करें</string>\n  <string name=\"amlogic_fix_desc\">Amlogic आधारित उपकरणों पर फ़्रेम ड्रॉप फिक्स।</string>\n  <string name=\"amlogic_fix\">Amlogic 1080p\\@60fps फिक्स</string>\n  <string name=\"tunneled_video_playback_desc\">नोट: हो सकता है विराम ठीक से काम न करे! टनल वीडियो प्लेबैक बेहतर ऑडियो / वीडियो सिंक्रोनाइज़ेशन (AV सिंक) और स्मूथ प्लेबैक देता है एंड्राइड 5+ आवश्यक</string>\n  <string name=\"tunneled_video_playback\">टनल वीडियो प्लेबैक (एंड्रॉइड 5+)</string>\n  <string name=\"master_volume\">मुख्य ध्वनि</string>\n  <string name=\"volume_limit\">ध्वनि सीमा</string>\n  <string name=\"play_video\">प्ले</string>\n  <string name=\"remember_position_of_short_videos\">लघु वीडियो की स्थिति याद रखें (10 मिनट से कम)</string>\n  <string name=\"player_show_tooltips\">बटन टूलटिप्स दिखाएं</string>\n  <string name=\"action_like_unset\">लाइक बंद है</string>\n  <string name=\"action_dislike_unset\">डिस्लाइक बंद है</string>\n  <string name=\"various_buttons\">शीर्ष पर स्थित बटन</string>\n  <string name=\"not_compatible_with\">चयनित विकल्प संगत नहीं है</string>\n  <string name=\"subtitle_position\">उपशीर्षक नीचे करे</string>\n  <string name=\"pressing_home\">होम दबाने पर</string>\n  <string name=\"pressing_home_back\">होम या बैक दबाने पर</string>\n  <string name=\"player_number_key_seek\">नंबर कुंजियों के साथ आगे/पीछे करे</string>\n  <string name=\"save_remove_playlist\">प्लेलिस्ट अनुभाग से प्लेलिस्ट जोड़ें/हटाएं</string>\n  <string name=\"remove_playlist\">प्लेलिस्ट अनुभाग से प्लेलिस्ट को स्थायी रूप से हटाएं</string>\n  <string name=\"removed_from_playlists\">प्लेलिस्ट अनुभाग से हटाया गया</string>\n  <string name=\"saved_to_playlists\">प्लेलिस्ट अनुभाग में जोड़ा गया</string>\n  <string name=\"create_playlist\">प्लेलिस्ट बनायें</string>\n  <string name=\"add_video_to_new_playlist\">नई प्लेलिस्ट में जोड़ें</string>\n  <string name=\"cant_delete_empty_playlist\">खाली प्लेलिस्ट नहीं हटा सकते</string>\n  <string name=\"playlist\">प्लेलिस्ट</string>\n  <string name=\"rename_playlist\">प्लेलिस्ट का नाम बदले</string>\n  <string name=\"cant_rename_empty_playlist\">खाली प्लेलिस्ट का नाम नहीं बदल सकते</string>\n  <string name=\"cant_rename_foreign_playlist\">अन्य की प्लेलिस्ट का नाम नहीं बदल सकते</string>\n  <string name=\"cant_save_playlist\">इस प्लेलिस्ट को जोड़ा नहीं जा सकता</string>\n  <string name=\"enter_value\">गैर-रिक्त मान दर्ज करें</string>\n  <string name=\"dialog_add_remove_from\">%s से जोड़ें/निकालें</string>\n  <string name=\"lb_playback_controls_skip_next\">अगला छोड़ें</string>\n  <string name=\"lb_playback_controls_skip_previous\">शुरू करने के लिए पिछला/रिवाइंड छोड़ें</string>\n  <string name=\"alt_presets_behavior\">वैकल्पिक प्रीसेट व्यवहार (सीमित बैंडविड्थ)</string>\n  <string name=\"alt_presets_behavior_desc\">ऐप गुणवता,एफपीएस और कोडेक चयनित प्रीसेट के अनुरूप बैंडविड्थ बनाने की कोशिश करेगा।</string>\n  <string name=\"sleep_timer\">स्लीप टाइमर चालू करें (एक घंटा)</string>\n  <string name=\"sleep_timer_desc\">यदि उपयोगकर्ता ने एक घंटे तक रिमोट का उपयोग नहीं किया तो प्लेबैक बंद करे</string>\n  <string name=\"disable_vsync\">vsync के लिए snap बंद करें</string>\n  <string name=\"disable_vsync_desc\">प्लेबैक प्रदर्शन में थोड़ा सुधार करता है</string>\n  <string name=\"skip_codec_profile_check\">कोडेक प्रोफ़ाइल की जाँच छोड़ें</string>\n  <string name=\"skip_codec_profile_check_desc\">वीडियो शुरू करते समय कोडेक की जांच न करें। कुछ फर्मवेयर पर मददगार है।</string>\n  <string name=\"force_sw_codec\">बलपूर्वक SW वीडियो डिकोडर</string>\n  <string name=\"force_sw_codec_desc\">कोई भी वीडियो चला सकता है परंतु प्रदर्शन बहुत खराब है</string>\n  <string name=\"playlist_order\">प्लेलिस्ट क्रमबद्ध</string>\n  <string name=\"playlist_order_added_date_newer_first\">दिनांक अनुसार (नया पहले)</string>\n  <string name=\"playlist_order_added_date_older_first\">दिनांक अनुसार (पुराना पहले)</string>\n  <string name=\"playlist_order_published_date_newer_first\">प्रकाशित दिनांक अनुसार (नया पहले)</string>\n  <string name=\"playlist_order_published_date_older_first\">प्रकाशित दिनांक अनुसार (पुराना पहले)</string>\n  <string name=\"playlist_order_popularity\">लोकप्रिय</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">अन्य की प्लेलिस्ट के लिए नहीं कर सकते</string>\n  <string name=\"playback_controls_repeat_pause\">पॉज़ दोहराएं</string>\n  <string name=\"releases\">ऐप रिलीज़</string>\n  <string name=\"ambilight_ratio_fix_desc\">अस्पष्टत अनुपात को ठीक करता है। रिक्त स्क्रीनशॉट को ठीक करता है। प्रदर्शन को प्रभावित करता है!</string>\n  <string name=\"video_buffer_size_none\">नहीं</string>\n  <string name=\"open_chat\">चैट/टिपण्णी </string>\n  <string name=\"place_chat_left\">चैट को बाईं ओर दिखाए</string>\n  <string name=\"use_alt_speech_recognizer\">वैकल्पिक आवाज़ पहचान</string>\n  <string name=\"time_format\">समय प्रारूप</string>\n  <string name=\"time_format_24\">24 घंटे</string>\n  <string name=\"time_format_12\">12 घंटे (AM/PM)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">गंभीर गड़बड़ी, इसका उपयोग केवल तभी करें जब आपको डिफ़ॉल्ट पहचानकर्ता से परेशानी हो</string>\n  <string name=\"speech_recognizer\">आवाज़ पहचानकर्ता</string>\n  <string name=\"speech_recognizer_system\">सिस्टम (पसंदीदा)</string>\n  <string name=\"speech_recognizer_external_1\">अन्य 1 (गंभीर गड़बड़ी, काम करने के लिए आपको माइक्रोफ़ोन तक पहुंच अक्षम करे )</string>\n  <string name=\"speech_recognizer_external_2\">अन्य 2 (गंभीर गड़बड़ी)</string>\n  <string name=\"real_channel_icon\">चैनल बटन पर आइकन दिखाएं</string>\n  <string name=\"subtitle_yellow_semi_transparent\">अर्धपारदर्शी पृष्ठभूमि पर पीला</string>\n  <string name=\"subtitle_yellow_black\">काली पृष्ठभूमि पर पीला</string>\n  <string name=\"player_pixel_ratio\">पिक्सेल अनुपात</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">डार्क ग्रे (मोनोक्रोम)</string>\n  <string name=\"disable_mic_permission\">कृपया, इस पहचानकर्ता को ठीक से काम करने के लिए ऐप के लिए माइक्रोफ़ोन एक्सेस बंद करें।</string>\n  <string name=\"repeat_mode_shuffle\">किसी भी प्लेलिस्ट को फेरबदल करें</string>\n  <string name=\"chat_left\">बाएं</string>\n  <string name=\"chat_right\">दांए</string>\n  <string name=\"card_real_thumbnails\">थंबनेल को वीडियो के फ्रेम से बदलें</string>\n  <string name=\"card_content\">कार्ड के थंबनेल कहां से चुनें</string>\n  <string name=\"thumb_quality_default\">डिफ़ॉल्ट</string>\n  <string name=\"thumb_quality_start\">वीडियो की शुरुआत से</string>\n  <string name=\"thumb_quality_middle\">वीडियो के मध्य से</string>\n  <string name=\"thumb_quality_end\">वीडियो के अंत से</string>\n  <string name=\"content_block_status\">स्पॉन्सरब्लॉक स्थिति जांचें</string>\n  <string name=\"player_show_quality_info_bitrate\">गुणवत्ता की जानकारी में बिटरेट जोड़ें</string>\n  <string name=\"player_speed_button_old_behavior\">स्पीड बटन के पुराने व्यवहार को वापस लाएं</string>\n  <string name=\"protect_settings_with_password\">पासवर्ड से सभी सेटिंग्स को सुरक्षित रखें</string>\n  <string name=\"enter_settings_password\">सेटिंग्स पासवर्ड दर्ज करें</string>\n  <string name=\"child_mode\">किड्स मोड</string>\n  <string name=\"child_mode_desc\">इस मोड में, उपयोगकर्ता खोज का उपयोग नहीं कर सकता है या कोई सुझाई गई सामग्री नहीं देख सकता है। सेटिंग्स पासवर्ड से सुरक्षित</string>\n  <string name=\"lost_setting_warning\">ऐप सेटिंग बदल दी जाएगी। सुनिश्चित करें कि आपने सेटिंग्स का बैकअप बना लिया है।</string>\n  <string name=\"pause_history\">इतिहास रोकें</string>\n  <string name=\"resume_history\">इतिहास शुरू करें</string>\n  <string name=\"clear_history\">इतिहास मिटाये</string>\n  <string name=\"not_recommend_channel\">चैनल का सुझाव न दे</string>\n  <string name=\"you_wont_see_this_channel\">चैनल सुझावों में दिखाई नहीं देगा</string>\n  <string name=\"msg_player_error_video_renderer\">चयनित वीडियो प्रारूप समर्थित नहीं है</string>\n  <string name=\"msg_player_error_audio_renderer\">चयनित ध्वनि प्रारूप समर्थित नहीं है</string>\n  <string name=\"msg_player_error_subtitle_renderer\">चयनित उपशीर्षक प्रारूप समर्थित नहीं है</string>\n  <string name=\"msg_player_error_video_source\">वीडियो यूआरएल काम नहीं कर रहा है \\n डिवाइस का समय गलत है\\nआमतौर पर,यह ऐप की परेशानी है।</string>\n  <string name=\"msg_player_error_audio_source\">ऑडियो यूआरएल काम नहीं कर रहा है \\n डिवाइस का समय गलत है\\nआमतौर पर,यह ऐप की परेशानी है।</string>\n  <string name=\"msg_player_error_subtitle_source\">उपशीर्षक यूआरएल काम नहीं कर रहा है \\n डिवाइस का समय गलत है\\nआमतौर पर,यह ऐप की परेशानी है।</string>\n  <string name=\"live_stream_fix_4k\">लाइव स्ट्रीम फिक्स (4K)</string>\n  <string name=\"player_volume\">आवाज</string>\n  <string name=\"pressing_back\">बैक दबाने पर</string>\n  <string name=\"open_comments\">टिप्पणी खोले</string>\n  <string name=\"player_button_long_click\">कार्रवाई के लिए बटन पर एक छोटा क्लिक और सेटिंग्स के लिए देर तक दबाये</string>\n  <string name=\"disable_history\">इतिहास बंद</string>\n  <string name=\"enable_history\">इतिहास चालू </string>\n  <string name=\"chapters\">अध्याय</string>\n  <string name=\"card_multiline_subtitle\">एकाधिक पंक्ति वाले उपशीर्षक</string>\n  <string name=\"auto_frame_rate_modes\">समर्थित मोड</string>\n  <string name=\"loading\">लोड हो रहा है…</string>\n  <string name=\"hide_streams\">सब्सक्रिप्शन से स्ट्रीम छुपाएं</string>\n  <string name=\"video_rotate\">घुमाएँ</string>\n  <string name=\"trending_searches\">ट्रेंडिंग खोज</string>\n  <string name=\"video_duration_any\">किसी भी</string>\n  <string name=\"video_duration\">अवधि</string>\n  <string name=\"video_duration_under_4\">4 मिनट से कम</string>\n  <string name=\"video_duration_between_4_20\">4–20 मिनट</string>\n  <string name=\"video_duration_over_20\">20 मिनट से बड़ी </string>\n  <string name=\"content_type\">प्रकार</string>\n  <string name=\"content_type_any\">कोई भी </string>\n  <string name=\"content_type_video\">विडियो</string>\n  <string name=\"content_type_channel\">चैनल </string>\n  <string name=\"content_type_playlist\">प्लेलिस्ट </string>\n  <string name=\"content_type_movie\">मूवी </string>\n  <string name=\"video_features\">विशेषताएँ</string>\n  <string name=\"video_feature_any\">कोई भी </string>\n  <string name=\"video_feature_live\">लाइव </string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">के अनुसार क्रमबद्ध करें</string>\n  <string name=\"sort_by_relevance\">प्रासंगिकता</string>\n  <string name=\"sort_by_views\">देखे जाने की संख्या</string>\n  <string name=\"sort_by_date\">अपलोड दिनांक</string>\n  <string name=\"sort_by_rating\">रेटिंग</string>\n  <string name=\"clear_search_history\">खोज इतिहास मिटाए</string>\n  <string name=\"remove_from_subscriptions\">छुपाये</string>\n  <string name=\"player_long_speed_list\">ज्यादा गति दिखाए</string>\n  <string name=\"alt_app_icon\">वैकल्पिक ऐप आइकन</string>\n  <string name=\"network_stack\">%s नेटवर्क स्टैक को प्राथमिकता दें</string>\n  <string name=\"player_network_stack\">नेटवर्क स्टैक</string>\n  <string name=\"cronet_desc\">क्रोनेट क्रोमियम नेटवर्क स्टैक है। क्रोनेट विलंबता को कम कर सकता है और नेटवर्किंग प्रदर्शन को बढ़ा सकता है, जो बफरिंग समस्याओं में मदद कर सकता है।</string>\n  <string name=\"unlock_all_formats\">सभी वीडियो प्रारूप अनलॉक करें</string>\n  <string name=\"unlock_all_formats_desc\">कुछ डिवाइस पर फ़र्मवेयर कुछ प्रारूपों को असमर्थित के रूप में रिपोर्ट करता है, भले ही वे समर्थित हों।\\n(उदाहरण के लिए, 1080p स्मार्ट टीवी 4k को असमर्थित दर्शाता हैं)।</string>\n  <string name=\"okhttp_desc\">यह नेटवर्क स्टैक सबसे धीमा है लेकिन स्थिर है।</string>\n  <string name=\"select_channel_section\">ATV चैनलों से ऐप लॉन्च होने पर संबंधित अनुभाग खोलें</string>\n  <string name=\"enable_voice_search_desc\">आधिकारिक ऐप को ब्रिज ऐप से बदल दिया जाएगा। वह खोज अनुरोधों को ऐप पर स्थानांतरित करने में मदद करता है।</string>\n  <string name=\"enable_master_password\">मुख्य पासवर्ड चालू करें</string>\n  <string name=\"enter_master_password\">मुख्य पासवर्ड दर्ज करें</string>\n  <string name=\"disable_stream_buffer\">स्ट्रीम पर बफ़र रोके</string>\n  <string name=\"color_scheme_blue\">नीला</string>\n  <string name=\"disable_search_history\">खोज इतिहास बंद करें</string>\n  <string name=\"header_notifications\">सूचनाएं</string>\n  <string name=\"msg_player_unknown_error\">अज्ञात त्रुटि</string>\n  <string name=\"auto_history\">स्वचालित (खाता सेटिंग का उपयोग करें)</string>\n  <string name=\"player_auto_volume\">स्वत: आवाज सुधारे </string>\n  <string name=\"autogenerated\">स्वत: उत्पन्न</string>\n  <string name=\"sorting_default\">डिफ़ॉल्ट (तेज़)</string>\n  <string name=\"sorting_alphabetically2\">वर्णानुक्रमानुसार(तेज़)</string>\n  <string name=\"player_likes_count\">पसंद/नापसंद की संख्या दिखाएं</string>\n  <string name=\"mark_as_watched\">देखे गए के रूप में चिह्नित करें</string>\n  <string name=\"old_update_notifications\">अद्यतन सूचनाओं का पुराना स्वरूप</string>\n  <string name=\"player_screen_off_timeout\">स्क्रीन बंद का समय</string>\n  <string name=\"header_trending\">ट्रेंडिंग</string>\n  <string name=\"player_section_playlist\">प्लेलिस्ट के रूप में वर्तमान अनुभाग का उपयोग करें</string>\n  <string name=\"volume_boost_warning\">सीमा से अधिक की कोई भी सेटिंग संभावित रूप से स्पीकर को नुकसान पहुंचा सकती है</string>\n  <string name=\"update_found\">अद्यतन</string>\n  <string name=\"hide_shorts_everywhere\">शॉर्ट्स को हर जगह छुपाएं</string>\n  <string name=\"old_home_look\">होम अनुभाग का पुराना स्वरुप</string>\n  <string name=\"audio_language\">ऑडियो भाषा</string>\n  <string name=\"content_block_exclude_channel\">इस चैनल को SponsorBlock से बाहर करें</string>\n  <string name=\"content_block_stop_excluding_channel\">इस चैनल को SponsorBlock में जोड़े</string>\n  <string name=\"header_shorts\">शॉर्ट्स</string>\n  <string name=\"unlock_high_bitrate_formats\">उच्च बिटरेट 1080p VP9 प्रारूप अनलॉक करें</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">उच्च बिटरेट mp4a प्रारूप अनलॉक करें</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-hr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"header_home\">Početna</string>\n    <string name=\"title_search\">Pretraga</string>\n    <string name=\"header_subscriptions\">Pretplata</string>\n    <string name=\"header_history\">Povijest</string>\n    <string name=\"header_music\">Glazba</string>\n    <string name=\"header_news\">Novosti</string>\n    <string name=\"header_gaming\">Igre</string>\n    <string name=\"header_playlists\">Zbirka</string>\n    <string name=\"header_settings\">Postavke</string>\n    <string name=\"header_channels\">Kanali</string>\n    <string name=\"subscriptions_signin_title\">Pogledajte najnovije s vaših omiljenih kanala</string>\n    <string name=\"subscriptions_signin_subtitle\">Prijavite se kako bi vidjeli svoje pretplate</string>\n    <string name=\"subscriptions_signin_button_text\">PRIJAVA</string>\n    <string name=\"library_signin_to_show_more\">Gledajte omiljeni video, spremite ga ili se pretplatite</string>\n    <string name=\"library_signin_subtitle\">Prijavite se kako bi vidjeli svoju zbirku</string>\n    <string name=\"action_signin\">PRIJAVA</string>\n    <string name=\"title_video_formats\">Video formati</string>\n    <string name=\"title_audio_formats\">Zvučni formati</string>\n    <string name=\"subtitle_category_title\">Podnaslovi</string>\n    <string name=\"playback_queue_category_title\">Red izvođenja</string>\n    <string name=\"video_max_quality\">Automatski (najveća kvaliteta)</string>\n    <string name=\"audio_max_quality\">Automatski (najveća kvaliteta)</string>\n    <string name=\"subtitles_disabled\">Podnaslovi onemogućeni</string>\n    <string name=\"auto_frame_rate\">Automatsko osvježavanje sličica</string>\n    <string name=\"frame_rate_correction\">Popravi sl/sek:\\n%s</string>\n    <string name=\"category_background_playback\">Pozadinska rapodukcija</string>\n    <string name=\"not_implemented\">Nije implementirano</string>\n    <string name=\"option_background_playback_off\">Onemogućeno</string>\n    <string name=\"option_background_playback_pip\">Slika u slici</string>\n    <string name=\"option_background_playback_only_audio\">Samo zvuk</string>\n    <string name=\"playback_settings\">Postavke kvalitete repodukcije</string>\n    <string name=\"video_speed\">Brzina videozapisa</string>\n    <string name=\"resolution_switch\">Promijeni razlučivost</string>\n    <string name=\"video_buffer\">Video međuspremnik</string>\n    <string name=\"video_buffer_size_none\">Nepoznat</string>\n    <string name=\"video_buffer_size_low\">Nizak</string>\n    <string name=\"video_buffer_size_med\">Srednji</string>\n    <string name=\"video_buffer_size_high\">Velik</string>\n    <string name=\"update_changelog\">Zapis promjena</string>\n    <string name=\"install_update\">Instaliraj ažuriranje</string>\n    <string name=\"section_is_empty\">Ups! Ne postoji ništa ovdje.</string>\n    <string name=\"dialog_add_to_playlist\">Dodaj/Ukloni iz popisa izvođenja</string>\n    <string name=\"msg_signed_users_only\">Samo prijavljeni korisnici</string>\n    <string name=\"msg_cant_load_content\">Nemoguće učitavanje sadržaja.\\n1) Provjerite datum i vrijeme na uređaju.\\n2) Provjerite mrežno povezivanje.\\n3) Provjerite proxy postavke.\\n4) Pokušajte se prijaviti u svoj račun.\\n5) Možda nije ništa problem od ovdje navedenog.</string>\n    <string name=\"title_video_presets\">Video predlošci</string>\n    <string name=\"settings_accounts\">Računi</string>\n    <string name=\"settings_left_panel\">Uredi kategorije</string>\n    <string name=\"settings_themes\">Teme</string>\n    <string name=\"settings_other\">Ostalo</string>\n    <string name=\"settings_player\">Video reproduktor</string>\n    <string name=\"settings_language\">Jezik</string>\n    <string name=\"settings_linked_devices\">Povezani uređaji</string>\n    <string name=\"settings_about\">Informacije</string>\n    <string name=\"dialog_account_list\">Odaberi račun</string>\n    <string name=\"dialog_account_none\">Nedostupno</string>\n    <string name=\"dialog_remove_account\">Odjava</string>\n    <string name=\"dialog_add_account\">Prijava</string>\n    <string name=\"default_lang\">Zadani</string>\n    <string name=\"dialog_select_language\">Jezik</string>\n    <string name=\"subtitle_default\">Zadani</string>\n    <string name=\"subtitle_white_semi_transparent\">Bijeli na poluprozirnioj pozadini</string>\n    <string name=\"subtitle_style\">Izgled podnaslova</string>\n    <string name=\"subtitle_language\">Jezik podnaslova</string>\n    <string name=\"action_search\">Pretraga</string>\n    <string name=\"settings_main_ui\">Korisničko sučelje</string>\n    <string name=\"dialog_main_ui\">Korisničko sučelje</string>\n    <string name=\"card_animated_previews\">Animirani pregledi</string>\n    <string name=\"web_site\">Web stranica</string>\n    <string name=\"donation\">Donacija</string>\n    <string name=\"dialog_about\">Informacije</string>\n    <string name=\"dialog_player_ui\">Video repoduktor</string>\n    <string name=\"player_show_ui_on_pause\">Prikaži korisničko sučelje pri pauzi</string>\n    <string name=\"player_pause_on_ok\">OK tipka pauzira repodukciju</string>\n    <string name=\"player_ok_button_behavior\">Ponašanje OK tipke</string>\n    <string name=\"player_only_ui\">Samo korisničko sučelje</string>\n    <string name=\"player_ui_and_pause\">Korisničko sučelje i pauza</string>\n    <string name=\"player_only_pause\">Samo pauza</string>\n    <string name=\"check_for_updates\">Provjeri ažuriranja</string>\n    <string name=\"update_not_found\">Koristite najnoviju inačicu</string>\n    <string name=\"update_in_progress\">Pričekajte…</string>\n    <string name=\"player_ui_hide_behavior\">Automatski sakrij korisničko sučelje</string>\n    <string name=\"option_never\">Nikada</string>\n    <string name=\"side_panel_sections\">Odabir odjeljaka</string>\n    <string name=\"boot_to_section\">Pokreni u odjeljku</string>\n    <string name=\"large_ui\">Veliko korisničko sučelje</string>\n    <string name=\"video_grid_scale\">Razmjer video mreže</string>\n    <string name=\"scale_ui\">Razmjer korisničkog sučelja</string>\n    <string name=\"color_scheme\">Shema boje</string>\n    <string name=\"color_scheme_default\">Zadana</string>\n    <string name=\"color_scheme_red_grey\">Crveno siva</string>\n    <string name=\"color_scheme_red\">Crvena</string>\n    <string name=\"color_scheme_dark_grey\">Tamno siva</string>\n    <string name=\"disable_update_check\">Onemogući provjeru ažuriranja</string>\n    <string name=\"show_again\">Prikaži ponovno</string>\n    <string name=\"check_updates_auto\">Obavijesti o novim inačicama</string>\n    <string name=\"select_account_on_boot\">Odaberi pri pokretanju</string>\n    <string name=\"player_other\">Ostalo</string>\n    <string name=\"player_full_date\">Precizan datum u opisu</string>\n    <string name=\"open_channel\">Otvori kanal</string>\n    <string name=\"open_playlist\">Otvori popis izvođenja</string>\n    <string name=\"not_interested\">Ne zanima me</string>\n    <string name=\"you_wont_see_this_video\">Video je uklonjen iz preporučenih</string>\n    <string name=\"settings_search\">Pretraga</string>\n    <string name=\"dialog_search\">Pretraga</string>\n    <string name=\"instant_voice_search\">Brza glasovna pretraga</string>\n    <string name=\"option_background_playback_behind\">Reproduciraj u pozadini</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">Informacije sljedećeg videa još nisu učitane</string>\n    <string name=\"card_multiline_title\">Višeredni podanaslovi</string>\n    <string name=\"cards_style\">Izgled kartica</string>\n    <string name=\"repeat_mode_all\">Reproduciraj videozapise neprekidno</string>\n    <string name=\"repeat_mode_one\">Ponovi trenutni video</string>\n    <string name=\"repeat_mode_pause\">Pauziraj repodukciju nakon svakog videa</string>\n    <string name=\"repeat_mode_pause_alt\">Reproduciraj neprekidno samo popise izvođenja</string>\n    <string name=\"repeat_mode_none\">Zaustavi reprodukciju nakon jednog videa</string>\n    <string name=\"subscribed_to_channel\">Kanal je pretplaćen</string>\n    <string name=\"unsubscribed_from_channel\">Kanal nije pretplaćen</string>\n    <string name=\"subtitle_yellow_transparent\">Žuti na prozirnoj pozadini</string>\n    <string name=\"subtitle_white_transparent\">Bijeli na prozirnoj pozadini</string>\n    <string name=\"subtitle_white_black\">Bijeli na crnoj pozadini</string>\n    <string name=\"player_seek_preview\">Pregled pri premotavanju</string>\n    <string name=\"color_scheme_dark_grey_oled\">Tamno siva (OLED)</string>\n    <string name=\"channels_section_sorting\">Razvrstavanje odjeljka kanala</string>\n    <string name=\"sorting_by_new_content\">Novi sadržaj</string>\n    <string name=\"sorting_alphabetically\">Abecedno</string>\n    <string name=\"sorting_last_viewed\">Posljednje gledano</string>\n    <string name=\"player_pause_when_seek\">Pauziraj pri premotavanju</string>\n    <string name=\"playlists_style\">Izgled odjeljka popisa izvođenja</string>\n    <string name=\"playlists_style_grid\">Mreža</string>\n    <string name=\"playlists_style_rows\">Redci</string>\n    <string name=\"player_show_clock\">Prikaži sat u traci upravljanja</string>\n    <string name=\"player_show_remaining_time\">Prikaži preostalo vrijeme u traci upravljanja</string>\n    <string name=\"player_show_ending_time\">Prikaži vrijeme završetka u traci upravljanja</string>\n    <string name=\"open_channel_uploads\">Otvori slanja kanala</string>\n    <string name=\"app_exit_shortcut\">Zatvori aplikaciju</string>\n    <string name=\"app_exit_none\">Ne zatvaraj</string>\n    <string name=\"app_double_back_exit\">Dvostruka tipka Back</string>\n    <string name=\"app_single_back_exit\">Jednostruka tipka Back</string>\n    <string name=\"action_video_zoom\">Video uvećanje</string>\n    <string name=\"video_zoom\">Video uvećanje</string>\n    <string name=\"video_zoom_default\">Zadano</string>\n    <string name=\"video_zoom_fit_width\">Prilagodi širinu</string>\n    <string name=\"video_zoom_fit_height\">Prilagodi visinu</string>\n    <string name=\"video_zoom_fit_both\">Prilagodi ili širinu ili visinu</string>\n    <string name=\"video_zoom_stretch\">Rastegni</string>\n    <string name=\"color_scheme_teal\">Tirkizna</string>\n    <string name=\"color_scheme_teal_oled\">Tirkizna (OLED)</string>\n    <string name=\"player_seek_preview_none\">Onemogućeno</string>\n    <string name=\"player_seek_preview_single\">Jedna sličica</string>\n    <string name=\"player_seek_preview_carousel\">Vrtuljak</string>\n    <string name=\"player_seek_preview_carousel_slow\">Vrtuljak (sporo, ključni kadrovi)</string>\n    <string name=\"player_seek_preview_carousel_fast\">Vrtuljak (brzo, neprecizan pregled)</string>\n    <string name=\"unsubscribe_from_channel\">Prekini pretplatu s kanala</string>\n    <string name=\"card_title_lines_num\">Broj redaka naslova</string>\n    <string name=\"card_auto_scrolled_title\">Automatski pomiči preduge naslove</string>\n    <string name=\"share_link\">Podijeli poveznicu</string>\n    <string name=\"subscribe_to_channel\">Pretplati se na kanal</string>\n    <string name=\"wait_data_loading\">Pričekajte dok se podaci učitavaju…</string>\n    <string name=\"auto_frame_rate_pause\">Automatska brzina kadrova (pauza pri promjeni)</string>\n    <string name=\"auto_frame_rate_applying\">Primjena automatske brzine kadrova %sx%s@%s</string>\n    <string name=\"audio_shift\">Pomak zvuka</string>\n    <string name=\"player_remember_speed\">Pamćenje brzine</string>\n    <string name=\"mark_channel_as_watched\">Označi kao gledano</string>\n    <string name=\"channel_marked_as_watched\">Kanal je označen kao gledan</string>\n    <string name=\"dialog_add_device\">Dodaj uređaj</string>\n    <string name=\"dialog_remove_all_devices\">Ukloni sve uređaje</string>\n    <string name=\"device_link_enabled\">Povezivanje omogućeno</string>\n    <string name=\"device_connected\">Uređaj \\\"%s\\\" je povezan</string>\n    <string name=\"device_disconnected\">Uređaj \\\"%s\\\" nije povezan</string>\n    <string name=\"settings_ui_scale\">Razmjer sučelja</string>\n    <string name=\"msg_restart_app\">Ponovno pokrenite aplikaciju za primjenu postavki</string>\n    <string name=\"settings_block\">Blokiranje sadržaja</string>\n    <string name=\"msg_applying\">Primijenjujem %s…</string>\n    <string name=\"msg_done\">Završeno</string>\n    <string name=\"settings_general\">Općenito</string>\n    <string name=\"settings_video\">Video</string>\n    <string name=\"content_block_confirm_skip\">Potvrda preskoka</string>\n    <string name=\"content_block_categories\">Preskoči odsječke</string>\n    <string name=\"content_block_sponsor\">Sponzore</string>\n    <string name=\"content_block_intro\">Pauza/Uvodna animacija</string>\n    <string name=\"content_block_outro\">Odjavna špica/Zasluge</string>\n    <string name=\"content_block_interaction\">Podsjetnik (o pretplati)</string>\n    <string name=\"content_block_self_promo\">Neplaćena/Samopromocija</string>\n    <string name=\"content_block_music_off_topic\">Ne-glazbeni odsječak videa</string>\n    <string name=\"confirm_segment_skip\">Preskoči odsječak \\\"%s\\\"?</string>\n    <string name=\"cancel_segment_skip\">Prekini preskok odsječka</string>\n    <string name=\"msg_skipping_segment\">Preskakanje odsječka \\\"%s\\\"…</string>\n    <string name=\"content_block_notification_type\">Vrsta obavijesti</string>\n    <string name=\"content_block_notify_none\">Bez obavijesti</string>\n    <string name=\"content_block_notify_toast\">Plivajuće</string>\n    <string name=\"content_block_notify_dialog\">Potvrdni dijalog</string>\n    <string name=\"return_to_launcher\">Vrati se u pokretač iz ATV kanala/pretrage</string>\n    <string name=\"intent_force_close\">Prisili zatvaranje ako je video otvoren iz vanjskog izvora</string>\n    <string name=\"btn_confirm\">Potvrdi</string>\n    <string name=\"player_low_video_quality\">Niska video kvaliteta</string>\n    <string name=\"remote_session_closed\">Udaljena sesija je zatvorena</string>\n    <string name=\"msg_mode_switch_error\">Nemoguće prebacivanje načina prikaza na \\\"%s\\\"</string>\n    <string name=\"msg_player_error\">Greška reproduktora %s. Ponovno pokretanje reprodukcije…</string>\n    <string name=\"video_preset_disabled\">Bez predloška</string>\n    <string name=\"video_preset_enabled\">Format sljedećeg videa biti će postavljen po predlošku</string>\n    <string name=\"player_sleep_timer\">Tajmer spavanja</string>\n    <string name=\"player_show_quality_info\">Prikaži informacije kvalitete u traci upravljanja</string>\n    <string name=\"player_remember_each_speed\">Pamti brzinu svakog videa</string>\n    <string name=\"player_remember_speed_none\">Ne pamti</string>\n    <string name=\"player_remember_speed_all\">Ista za svaki video</string>\n    <string name=\"player_remember_speed_each\">Zasebna za svaki video</string>\n    <string name=\"msg_player_error_source\">Nemoguće preuzimanje videa</string>\n    <string name=\"msg_player_error_renderer\">Odabrani video format nije podržan.\\nMožda je greška firmvera. Pokušajte ponovno pokrenuti uređaj.</string>\n    <string name=\"msg_player_error_unexpected\">Nepoznata greška video dekôdera</string>\n    <string name=\"video_aspect\">Omjer slike</string>\n    <string name=\"player_tweaks\">Razvojne mogućnosti</string>\n    <string name=\"header_uploads\">Slanja</string>\n    <string name=\"player_show_global_clock\">Uvijek prikaži sat na zaslonu</string>\n    <string name=\"player_show_global_ending_time\">Uvijek prikaži vrijeme završetka na zaslonu</string>\n    <string name=\"uploads_old_look\">Vrati stari izgled odjeljka Slanja</string>\n    <string name=\"background_playback_activation\">Pozadinska reprodukcija (aktivacija)</string>\n    <string name=\"channels_old_look\">Vrati stari izgled odjeljka Kanali</string>\n    <string name=\"channels_auto_load\">Automatski učitaj sadržaj odjeljka Kanala</string>\n    <string name=\"return_to_background_video\">Vrati na video pokrenut u pozadini</string>\n    <string name=\"pin_unpin_from_sidebar\">Pričvrsti/Ukloni iz bočne trake</string>\n    <string name=\"pin_unpin_playlist\">Pričvrsti/Ukloni zbirku iz bočne trake</string>\n    <string name=\"pin_unpin_channel\">Pričvrsti/Ukloni kanal iz bočne trake</string>\n    <string name=\"pinned_to_sidebar\">Pričvršćeno na bočnu traku</string>\n    <string name=\"unpin_from_sidebar\">Uklonjeno iz bočne trake</string>\n    <string name=\"unpinned_from_sidebar\">Uklonjeno iz bočne trake</string>\n    <string name=\"dialog_select_country\">Država</string>\n    <string name=\"settings_language_country\">Jezik/Država</string>\n    <string name=\"share_embed_link\">Dijeli ugrađenu poveznicu</string>\n    <string name=\"card_text_scroll_factor\">Brzina pomicanja teksta kartice</string>\n    <string name=\"preferred_update_source\">Odabir izvora ažuriranja</string>\n    <string name=\"hide_shorts\">Sakrij Shorts u pretplatama</string>\n    <string name=\"key_remapping\">Mapiranje tipki</string>\n    <string name=\"screen_dimming\">Zatamnjenje zaslona</string>\n    <string name=\"removed_from_playback_queue\">Ukloni iz reda izvođenja</string>\n    <string name=\"added_to_playback_queue\">Dodano u red izvođenja</string>\n    <string name=\"add_remove_from_playback_queue\">Dodaj/Ukloni iz reda izvođenja</string>\n    <string name=\"add_to_playback_queue\">Dodaj u red izvođenja</string>\n    <string name=\"remove_from_playback_queue\">Ukloni iz reda izvođenja</string>\n    <string name=\"proxy_enabled\">Proxy omogućen</string>\n    <string name=\"proxy_disabled\">Proxy onemogućen</string>\n    <string name=\"uploads_row_name\">Slanja</string>\n    <string name=\"playlists_row_name\">Stvoreni popisi izvođenja</string>\n    <string name=\"popular_uploads_row_name\">Popularna slanja</string>\n    <string name=\"breaking_news_row_name\">Prijelomne vijesti</string>\n    <string name=\"covid_news_row_name\">COVID-19 vijesti</string>\n    <string name=\"enable_voice_search\">Omogući glasovnu pretragu</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">Pretplati se/Ukini pretplatu s kanala</string>\n    <string name=\"app_backup_restore\">Sigurnosno kopiranje/Obnova</string>\n    <string name=\"app_backup\">Sigurnosno kopiraj podatke</string>\n    <string name=\"app_restore\">Obnovi podatke iz sigurnosne kopije</string>\n    <string name=\"skip_each_segment_once\">Ne preskači ponovno odjeljke</string>\n    <string name=\"disable_ok_long_press\">Onemogući dug pritisak OK tipke</string>\n    <string name=\"player_time_correction\">Prikaži vrijeme u skladu s brzinom</string>\n    <string name=\"sponsor_color_markers\">Obojene oznake na traci napretka</string>\n    <string name=\"refresh_section\">Osvježi odjeljak</string>\n    <string name=\"option_disabled\">Onemogućeno</string>\n    <string name=\"live_now_row_name\">Uživo</string>\n    <string name=\"run_in_background\">Pozadinska reprodukcija</string>\n    <string name=\"settings_remote_control\">Udaljeno upravljanje</string>\n    <string name=\"background_service_started\">Pozadinska usluga je pokrenuta</string>\n    <string name=\"dialog_add_to\">Dodaj na %s</string>\n    <string name=\"dialog_remove_from\">Ukloni s %s</string>\n    <string name=\"added_to\">Dodano na %s</string>\n    <string name=\"removed_from\">Uklonjeno s %s</string>\n    <string name=\"video_preset_adaptive\">Prilagođeno</string>\n    <string name=\"content_block_preview_recap\">Pregled ili sažetak videa</string>\n    <string name=\"content_block_highlight\">Točka ili istaknuti dio videa</string>\n    <string name=\"removed_from_history\">Video je uklonjen iz povijesti</string>\n    <string name=\"remove_from_history\">Ukloni iz povijesti</string>\n    <string name=\"upload_date\">Vrijeme slanja</string>\n    <string name=\"upload_date_any\">Svo</string>\n    <string name=\"upload_date_today\">Danas</string>\n    <string name=\"upload_date_this_week\">Ovaj tjedan</string>\n    <string name=\"upload_date_this_month\">Ovaj mjesec</string>\n    <string name=\"upload_date_this_year\">Ove godine</string>\n    <string name=\"double_refresh_rate\">Dvostruko osvježavanje</string>\n    <string name=\"upload_date_last_hour\">Posljednji sat</string>\n    <string name=\"focus_on_search_results\">Prebaci na rezultate pretrage</string>\n    <string name=\"context_menu\">Sadržajni izbornik</string>\n    <string name=\"add_remove_from_recent_playlist\">Dodaj/Ukloni iz nedavnog popisa izvođenja</string>\n    <string name=\"hide_settings_section\">Sakrij odjeljak postavki (opasno!)</string>\n    <string name=\"volume\">Glasnoća %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s sek</string>\n    <string name=\"audio_shift_sec\">%s sek</string>\n    <string name=\"ui_hide_timeout_sec\">%s sek</string>\n    <string name=\"screen_dimming_timeout_min\">%s min</string>\n    <string name=\"mark_all_channels_watched\">Označi sve kanale kao odgledane</string>\n    <string name=\"move_section_up\">Pomakni odjeljak gore</string>\n    <string name=\"move_section_down\">Pomakni odjeljak dolje</string>\n    <string name=\"player_buttons\">Podesi tipke reproduktora</string>\n    <string name=\"action_playlist_add\">Dodaj na popis izvođenja</string>\n    <string name=\"action_video_stats\">Statistika videozapisa</string>\n    <string name=\"action_subscribe\">Pretplati se</string>\n    <string name=\"action_channel\">Otvori kanal</string>\n    <string name=\"action_pip\">Slika u slici</string>\n    <string name=\"action_video_info\">Opis videozapisa</string>\n    <string name=\"action_screen_off\">Isključi zaslon</string>\n    <string name=\"action_playback_queue\">Otvori red izvođenja</string>\n    <string name=\"action_video_speed\">Brzina videozapisa</string>\n    <string name=\"action_subtitles\">Podnaslovi</string>\n    <string name=\"action_like\">Sviđa mi se</string>\n    <string name=\"action_dislike\">Ne sviđa mi se</string>\n    <string name=\"action_play_pause\">Reprodukcija/Pauza</string>\n    <string name=\"action_repeat_mode\">Način reprodukcije</string>\n    <string name=\"action_next\">Sljedeći video</string>\n    <string name=\"action_previous\">Prijašni video</string>\n    <string name=\"action_high_quality\">Video kvaliteta</string>\n    <string name=\"content_block_no_skipping_mode\">Način bez preskoka</string>\n    <string name=\"content_block_action_type\">Odaberi radnju</string>\n    <string name=\"content_block_action_none\">Ništa</string>\n    <string name=\"content_block_action_only_skip\">Samo preskoči</string>\n    <string name=\"content_block_action_toast\">Preskoči uz obavijest</string>\n    <string name=\"content_block_action_dialog\">Prikaži dijalog potvrde</string>\n    <string name=\"content_block_filler\">Izvan teme (dopuna)</string>\n    <string name=\"keyboard_auto_show\">Automatski prikaži tipkovnicu</string>\n    <string name=\"cancel_dialog\">Odustani</string>\n    <string name=\"msg_player_error_source2\">Video izvor ne radi ili je vrijeme netočno</string>\n    <string name=\"hide_upcoming\">Sakrij najavljene iz Pretplata</string>\n    <string name=\"search_background_playback\">Prebaci na sliku u slici pri pretrazi/otvaranju kanala</string>\n    <string name=\"trending_row_name\">U trendu</string>\n    <string name=\"player_seek_type\">Ponašanje premotavanja</string>\n    <string name=\"player_seek_regular\">Uobičajeno</string>\n    <string name=\"player_seek_confirmation_pause\">S potvrdom (pauziraj pri premotavanju)</string>\n    <string name=\"player_seek_confirmation_play\">S potvrdom (reprodukcija pri premotavanju)</string>\n    <string name=\"hide_shorts_from_home\">Sakrij shorts iz Početne</string>\n    <string name=\"finish_on_disconnect\">Zatvori aplikaciju nakon prekida povezivanje s telefonom/tabletom</string>\n    <string name=\"update_error\">Greška ažuriranja</string>\n    <string name=\"hide_shorts_from_history\">Sakrij shorts iz Povijesti</string>\n    <string name=\"disable_screensaver\">Onemogući čuvara zaslona</string>\n    <string name=\"description_not_found\">Opis nije pronađen</string>\n    <string name=\"proxy_port_hint\">Proxy ulaz</string>\n    <string name=\"proxy_host_hint\">Naziv Proxy poslužitelja ili IP</string>\n    <string name=\"proxy_username_hint\">Proxy korisničko ime</string>\n    <string name=\"proxy_password_hint\">Proxy lozinka</string>\n    <string name=\"proxy_type\">Vrsta proxya</string>\n    <string name=\"enable_web_proxy\">Koristi web proxy</string>\n    <string name=\"proxy_not_supported\">Koristi web proxy (Zahtijeva Android 4.4+)</string>\n    <string name=\"proxy_test_btn\">Testiraj</string>\n    <string name=\"proxy_settings_title\">Postavke proxy poslužitelja</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">Test#%d: prekinut.</string>\n    <string name=\"proxy_type_invalid\">Proxy vrsta nije navedena.</string>\n    <string name=\"proxy_host_invalid\">Proxy poslužitelj nije naveden.</string>\n    <string name=\"proxy_port_invalid\">Nevaljan proxy ulaz, mora biti &gt; 0.</string>\n    <string name=\"proxy_credentials_invalid\">Nevaljano korisničko ime ili lozinka.</string>\n    <string name=\"proxy_test_aborted\">Testiranje prekinuto, prvo ispravite proxy postavke.</string>\n    <string name=\"proxy_application_aborted\">Prvo ispravite proxy postavke.</string>\n    <string name=\"proxy_test_start\">Test#%d: %s …</string>\n    <string name=\"proxy_test_error\">Greška#%d: %s</string>\n    <string name=\"proxy_test_status\">Stanje#%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">Adresa datoteke podešavanja (*.ovpn)</string>\n    <string name=\"enable_openvpn\">Koristi OpenVPN</string>\n    <string name=\"openvpn_settings_title\">OpenVPN postavke</string>\n    <string name=\"openvpn_application_aborted\">Prvo ispravite OpenVPN postavke.</string>\n    <string name=\"openvpn_test_aborted\">Testitanje prekinuto, prvo ispravite OpenVPN postavke.</string>\n    <string name=\"openvpn_address_invalid\">OpenVPN adresa podešavanja nije navedena.</string>\n    <string name=\"internet_censorship\">Internet cenzura</string>\n    <string name=\"rename_section\">Preimenuj ovaj odjeljak</string>\n    <string name=\"simple_edit_value_hint\">Nova vrijednost</string>\n    <string name=\"seek_interval\">Razdoblje premotavanja</string>\n    <string name=\"seek_interval_sec\">%s sek</string>\n    <string name=\"subtitle_system\">Sustavski (Android postavke > Pristupačnost)</string>\n    <string name=\"subtitle_scale\">Veličina podnsalova</string>\n    <string name=\"audio_sync_fix_desc\">Alternativan način usklađivanja slike/zvuka. Smatra se zastarjelim, ali u određenim slučajevima može pomoći.</string>\n    <string name=\"audio_sync_fix\">Isprvak usklađivanja zvuka</string>\n    <string name=\"ambilight_ratio_fix_desc\">Ispravlja nedostajuću razinu osvjetljenja. Ispravlja netočan omjer slike. Ispravlja prazne snimke zaslona. Utječe na performanse!</string>\n    <string name=\"ambilight_ratio_fix\">Ispravak ambijetalnog osvjetljenja/Omjera slike/slikanja zaslona</string>\n    <string name=\"force_legacy_codecs_desc\">Značajno poboljšava performanse na slabijim uređajima. Najveća razlučivost je 720p.</string>\n    <string name=\"force_legacy_codecs\">Prisili zastarjele kôdeke (720p)</string>\n    <string name=\"live_stream_fix_desc\">Značajano poboljšava performanse emitiranja uživo na slabijim uređajima. Najveća razlučivost je 1080p.</string>\n    <string name=\"live_stream_fix\">Ispravak emitiranju uživo (1080p)</string>\n    <string name=\"playback_notifications_fix_desc\">Sakriva obavijesti promjene zapisa. Može biti korisno za AOSP-temeljene firmvere.</string>\n    <string name=\"playback_notifications_fix\">Onemgući obavijesti reprodukcije</string>\n    <string name=\"amlogic_fix_desc\">Ispravlja preskakanje sličica na Amlogic-temeljenim uređajima.</string>\n    <string name=\"amlogic_fix\">Ispravi Amlogic 1080p@60 sl/sek</string>\n    <string name=\"tunneled_video_playback_desc\">NAPOMENA: pauza možda neće raditi pravilno! Tunelirana video repordukcija omogućuje poboljšanja poput boljeg usklađivanja slike/zvuka (AV sync) i glatkiju reprodukciju. Zahtijeva Android 5+</string>\n    <string name=\"tunneled_video_playback\">Tunelirana video reprodukcija (Android 5+)</string>\n    <string name=\"master_volume\">Glavna glasnoća</string>\n    <string name=\"volume_limit\">Ograničenje glasnoće</string>\n    <string name=\"play_video\">Reprodukcija</string>\n    <string name=\"remember_position_of_short_videos\">Zapamti položaj kratkih videa (manjih od 5 min)</string>\n    <string name=\"player_show_tooltips\">Prikaži opise tipki</string>\n    <string name=\"action_like_unset\">Sviđa mi se, uklonjeno</string>\n    <string name=\"action_dislike_unset\">Ne sviđa mi se, uklonjeno</string>\n    <string name=\"various_buttons\">Tipke na vrhu glavnog prozora</string>\n    <string name=\"not_compatible_with\">Odabrana mogućnost nije kompatibilna s</string>\n    <string name=\"subtitle_position\">Pomak podnaslova od dna</string>\n    <string name=\"pressing_home\">Pritiskom na HOME</string>\n    <string name=\"pressing_home_back\">Pritiskom na HOME ili BACK</string>\n    <string name=\"player_number_key_seek\">Premotavaj s brojčanim tipkama</string>\n    <string name=\"save_remove_playlist\">Dodaj/Obriši popis izvođenja iz odjeljka Zbirka</string>\n    <string name=\"remove_playlist\">Obriši trajno popis izvođenja iz odjeljka Zbirka</string>\n    <string name=\"removed_from_playlists\">Obrisano iz odjeljka Zbirka</string>\n    <string name=\"saved_to_playlists\">Dodano u odjeljak Zbirka</string>\n    <string name=\"create_playlist\">Stvori popis izvođenja</string>\n    <string name=\"add_video_to_new_playlist\">Dodaj na novi popis izvođenja</string>\n    <string name=\"cant_delete_empty_playlist\">Nemoguće brisanje praznog popisa izvođenja</string>\n    <string name=\"playlist\">Popis izvođenja</string>\n    <string name=\"rename_playlist\">Preimenuj popis izvođenja</string>\n    <string name=\"cant_rename_empty_playlist\">Nemoguće preimenovanje praznog popisa izvođenja</string>\n    <string name=\"cant_rename_foreign_playlist\">Nemoguće preimenovanje tuđeg popisa izvođenja</string>\n    <string name=\"cant_save_playlist\">Ovaj popis izvođenja se ne može dodati</string>\n    <string name=\"enter_value\">Navedite bilo koju vrijednost</string>\n    <string name=\"dialog_add_remove_from\">Dodaj/Ukloni iz %s</string>\n    <string name=\"lb_playback_controls_skip_next\">Skoči na sljedeće</string>\n    <string name=\"lb_playback_controls_skip_previous\">Skoči na prijašnje/Premotaj na početni položaj</string>\n    <string name=\"alt_presets_behavior\">Zamjenski predlošci ponašanja (ograničenje brzine prijenosa)</string>\n    <string name=\"alt_presets_behavior_desc\">Aplikacija će pokušati održavati odgovarajuću brzinu prijenosa prema odabranom predlošku umjesto prema odabranoj razulučivosti, sl/sek i kôdeku.</string>\n    <string name=\"sleep_timer\">Tajmer spavanja (ako se daljinski ne koristi jedan sat)</string>\n    <string name=\"sleep_timer_desc\">Pauziraj reprodukciju ako korisnik nije koristio daljinski upravljač jedan sat</string>\n    <string name=\"disable_vsync\">Onemogući vsync mogućnost</string>\n    <string name=\"disable_vsync_desc\">Malo poboljšava performanse reprodukcije</string>\n    <string name=\"skip_codec_profile_check\">Preskoči provjeru razine profila kôdeka</string>\n    <string name=\"skip_codec_profile_check_desc\">Ne provjeravaj podršku kôdeka pri pokretanju videa. Može biti korisno na problematičnim frimverima.</string>\n    <string name=\"force_sw_codec\">Prisli softverski video dekôder</string>\n    <string name=\"force_sw_codec_desc\">Može reproducirati gotovo svaki video, ali performanse su vrlo slabe.</string>\n    <string name=\"playlist_order\">Poredak popisa izvođenja</string>\n    <string name=\"playlist_order_added_date_newer_first\">Datum dodavanja (prvo noviji)</string>\n    <string name=\"playlist_order_added_date_older_first\">Datum dodavanja (prvo stariji)</string>\n    <string name=\"playlist_order_published_date_newer_first\">Datum objavljivanja (prvo noviji)</string>\n    <string name=\"playlist_order_published_date_older_first\">Datum objavljivanja (prvo stariji)</string>\n    <string name=\"playlist_order_popularity\">Popularnost</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">Ne mogu ovo učiniti na tuđim popisima izvođenja</string>\n    <string name=\"owned_playlist_warning\">Greška. Ovu radnju možete primijeniti samo na svojim popisima izvođenja</string>\n    <string name=\"content_block_alt_server\">Koristi zamjenski poslužitelj</string>\n    <string name=\"content_block_alt_server_desc\">Omogućite ovu mogućnost ako SponsorBlock ne radi iz određenih razloga.</string>\n    <string name=\"unset_stream_reminder\">Isključi podsjetnik za emisiju</string>\n    <string name=\"set_stream_reminder\">Usključi podsjetnik za emisiju</string>\n    <string name=\"playback_starts_shortly\">Reprodukcija će se automatski pokrenuti kada započne emitiranje</string>\n    <string name=\"starting_stream\">Emisija započinje…</string>\n    <string name=\"action_playlist_remove\">Ukloni iz popisa izvođenja</string>\n    <!-- BEGIN Moved strings -->\n    <string name=\"signin_view_title\">Kôd korisnika se učitava…</string>\n    <string name=\"signin_view_description\">Za prijavu, upišite ovaj kôd na stranici %s\\nNAPOMENA. Radi samo s Firefox ili Chrome preglednicima.</string>\n    <string name=\"signin_view_action_text\">Završeno</string>\n    <string name=\"require_checked\">Zahtijeva \\\"%s\\\"</string>\n    <string name=\"msg_press_again_to_exit\">Pritisnite ponovno za zatvaranje</string>\n    <string name=\"player_remaining_time\">Preostalo: %s</string>\n    <string name=\"player_ending_time\">Završava za %s</string>\n    <string name=\"badge_new_content\">NOVI SADRŽAJ</string>\n    <string name=\"badge_live\">UŽIVO</string>\n    <string name=\"add_device_view_description\">Za povezivanje uređaja upišite ovaj kôd u vašu YouTube aplikaciju na vašem telefonu u odjeljku Postavke/Gledajte na TV-u\\nNAPOMENA. Radi samo s Gboard tipkovnicom.</string>\n    <string name=\"playback_controls_repeat_pause\">Ponovi pauzu</string>\n    <string name=\"playback_controls_repeat_list\">Ponovi popis izvođenja</string>\n    <string name=\"action_subscribe_off\">Pretplata isklj.</string>\n    <string name=\"action_subscribe_on\">Pretplata uklj.</string>\n    <!-- END Moved strings -->\n    <string name=\"skip_24_rate\">Preskoči formate s 24sl/sek (real cinema mod popravak?)</string>\n    <string name=\"player_disable_suggestions\">Onemogući prijedloge</string>\n    <string name=\"feedback\">Povratne informacije</string>\n    <string name=\"sources\">Izvori</string>\n    <string name=\"releases\">Izdanja</string>\n    <string name=\"prefer_avc_over_vp9\">Odabir kôdeka: preferiraj avc prije vp9</string>\n    <string name=\"open_chat\">Razgovor</string>\n    <string name=\"place_chat_left\">Smjesti razgovor lijevo</string>\n    <string name=\"use_alt_speech_recognizer\">Alternativno prepoznavanje govora</string>\n    <string name=\"time_format\">Format vremena</string>\n    <string name=\"time_format_24\">24 satni</string>\n    <string name=\"time_format_12\">12 satni (AM/PM)</string>\n    <string name=\"use_alt_speech_recognizer_desc\">Vrlo problematično. Koristite samo ako imate problema sa zadanim prepoznavanjem govora.</string>\n    <string name=\"speech_recognizer\">Prepoznavanje govora</string>\n    <string name=\"speech_recognizer_system\">Sustav (poželjno)</string>\n    <string name=\"speech_recognizer_external_1\">Vanjsko 1 (vrlo problematično, kako bi radilo morate onemogućiti pristup mikrofonu)</string>\n    <string name=\"speech_recognizer_external_2\">Vanjsko 2 (vrlo problematično)</string>\n    <string name=\"real_channel_icon\">Prikaži ikonu na tipki kanala</string>\n    <string name=\"subtitle_yellow_semi_transparent\">Žuti na poluprozirnoj pozadini</string>\n    <string name=\"subtitle_yellow_black\">Žuti na crnoj pozadini</string>\n    <string name=\"player_pixel_ratio\">Omjer piksela</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">Tamno sivo (jednobojno)</string>\n    <string name=\"disable_mic_permission\">Onemogućite aplikaciji pristup mikrofonu kako bi prepoznavanje govora pravilno radilo.</string>\n    <string name=\"repeat_mode_shuffle\">Izmješaj svaki popis izvođenja</string>\n    <string name=\"chat_left\">Lijevo</string>\n    <string name=\"chat_right\">Desno</string>\n    <string name=\"card_real_thumbnails\">Zamijeni umanjeni pregled s kadrom iz videa</string>\n    <string name=\"card_content\">Prikaz umanjenog pregleda</string>\n    <string name=\"thumb_quality_default\">Zadani</string>\n    <string name=\"thumb_quality_start\">Iz početka videa</string>\n    <string name=\"thumb_quality_middle\">Iz sredine videa</string>\n    <string name=\"thumb_quality_end\">Iz kraja videa</string>\n    <string name=\"content_block_status\">Provjeri SponsorBlock stanje</string>\n    <string name=\"player_show_quality_info_bitrate\">Dodaj brzinu prijenosa u informacije kvalitete</string>\n    <string name=\"player_speed_button_old_behavior\">Vrati staro ponašanje tipke brzine</string>\n    <string name=\"protect_settings_with_password\">Zaštiti sve postavke lozinkom</string>\n    <string name=\"enter_settings_password\">Upiši lozinku postavki</string>\n    <string name=\"child_mode\">Dječji način rada</string>\n    <string name=\"child_mode_desc\">U ovom načinu rada, korisnik ne može koristiti pretragu ili vidjeti bilo kakav predloženi sadržaj. Postavke će biti zaštićene lozinkom.</string>\n    <string name=\"lost_setting_warning\">Postavke aplikacije će biti promijenjene. Stvorite sigurnosnu kopiju postavki.</string>\n    <string name=\"player_button_long_click\">Kratak pritisak na tipku za brzu radnju, a dulji pritisak za dijalog postavki</string>\n    <string name=\"pause_history\">Pauziraj povijest</string>\n    <string name=\"resume_history\">Pokreni povijest</string>\n    <string name=\"clear_history\">Ukloni povijest</string>\n    <string name=\"chapters\">Poglavlja</string>\n    <string name=\"card_multiline_subtitle\">Višeredni podnaslovi</string>\n    <string name=\"auto_frame_rate_modes\">Podržani načini</string>\n    <string name=\"loading\">Učitavanje…</string>\n    <string name=\"hide_streams\">Sakrij strujanja iz Pretplata</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-hu/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n  <string name=\"header_home\">Főoldal</string>\r\n  <string name=\"title_search\">Keresés</string>\r\n  <string name=\"header_subscriptions\">Feliratkozások</string>\r\n  <string name=\"header_history\">Előzmények</string>\r\n  <string name=\"header_music\">Zenevideók</string>\r\n  <string name=\"header_news\">Újdonságok</string>\r\n  <string name=\"header_gaming\">Játékvideók</string>\r\n  <string name=\"header_playlists\">Videólisták</string>\r\n  <string name=\"header_settings\">Beállítások</string>\r\n  <string name=\"header_channels\">Csatornák</string>\r\n  <string name=\"subscriptions_signin_title\">Nézze meg az ön által kedvelt csatornák újdonságait</string>\r\n  <string name=\"subscriptions_signin_subtitle\">Jelentkezzen be, hogy megtekinthesse a feliratkozásokat</string>\r\n  <string name=\"subscriptions_signin_button_text\">BEJELENTKEZÉS</string>\r\n  <string name=\"library_signin_to_show_more\">Nézzen meg kedvelt, mentett vagy feliratkozott videókat</string>\r\n  <string name=\"library_signin_subtitle\">Jelentkezzen be videótárának megtekintéséhez</string>\r\n  <string name=\"action_signin\">BEJELENTKEZÉS</string>\r\n  <string name=\"title_video_formats\">Videó formátum</string>\r\n  <string name=\"title_audio_formats\">Audió formátum</string>\r\n  <string name=\"subtitle_category_title\">Feliratok</string>\r\n  <string name=\"video_max_quality\">Auto (max minőség)</string>\r\n  <string name=\"audio_max_quality\">Auto (max minőség)</string>\r\n  <string name=\"subtitles_disabled\">Feliratok letiltva</string>\r\n  <string name=\"auto_frame_rate\">Képkockasebesség</string>\r\n  <string name=\"frame_rate_correction\">FPS javítása:\\n%s</string>\r\n  <string name=\"category_background_playback\">Lejátszás a háttérben</string>\r\n  <string name=\"not_implemented\">Nincs megvalósítva</string>\r\n  <string name=\"option_background_playback_off\">Kikapcsolva</string>\r\n  <string name=\"option_background_playback_pip\">Kép a képben</string>\r\n  <string name=\"option_background_playback_only_audio\">Csak hang</string>\r\n  <string name=\"playback_settings\">Lejátszás beállítása</string>\r\n  <string name=\"video_speed\">Videósebesség</string>\r\n  <string name=\"resolution_switch\">Felbontás váltása</string>\r\n  <string name=\"video_buffer\">Videó-puffer</string>\r\n  <string name=\"video_buffer_size_low\">Alacsony</string>\r\n  <string name=\"video_buffer_size_med\">Közepes</string>\r\n  <string name=\"video_buffer_size_high\">Magas</string>\r\n  <string name=\"update_changelog\">Változások</string>\r\n  <string name=\"install_update\">Új verzió telepítése</string>\r\n  <string name=\"section_is_empty\">Hoppá! Itt nincs semmi.</string>\r\n  <string name=\"dialog_add_to_playlist\">Hozzáadás egy videólistához</string>\r\n  <string name=\"msg_signed_users_only\">Csak bejelentkezett felhasználóknak</string>\r\n  <string name=\"msg_cant_load_content\">Hoppá. Nem lehet betölteni a tartalmat.\\n1) Ellenőrizze a dátumot és az időt a készüléken.\\n2) Ellenőrizze az internetkapcsolatot.\\n3) Ellenőrizze a proxy beállításait.\\n4) Próbáljon bejelentkezni a fiókjába.\\n5) Talán nincs benne semmi.</string>\r\n  <string name=\"title_video_presets\">Videóminőség</string>\r\n  <string name=\"settings_accounts\">Fiókok</string>\r\n  <string name=\"settings_left_panel\">Kategóriák szerkesztése</string>\r\n  <string name=\"settings_themes\">Témák</string>\r\n  <string name=\"settings_other\">Egyebek</string>\r\n  <string name=\"settings_player\">Videólejátszó</string>\r\n  <string name=\"settings_language\">Nyelv</string>\r\n  <string name=\"settings_linked_devices\">Társított eszközök</string>\r\n  <string name=\"settings_about\">Rólunk</string>\r\n  <string name=\"dialog_account_list\">Aktuális fiók</string>\r\n  <string name=\"dialog_account_none\">Fiók nélkül</string>\r\n  <string name=\"dialog_remove_account\">Fiók törlése</string>\r\n  <string name=\"dialog_add_account\">Fiók hozzáadása</string>\r\n  <string name=\"default_lang\">Rendszer</string>\r\n  <string name=\"dialog_select_language\">Alkalmazás nyelve</string>\r\n  <string name=\"subtitle_default\">Alap</string>\r\n  <string name=\"subtitle_white_semi_transparent\">Fehér, félig átlátszó háttérrel</string>\r\n  <string name=\"subtitle_style\">Felirat megjelenése</string>\r\n  <string name=\"subtitle_language\">Felirat nyelve</string>\r\n  <string name=\"action_search\">Keresés indítása</string>\r\n  <string name=\"settings_main_ui\">Kinézet</string>\r\n  <string name=\"dialog_main_ui\">Kinézet</string>\r\n  <string name=\"card_animated_previews\">Animált előnézet</string>\r\n  <string name=\"web_site\">Weboldal (QR)</string>\r\n  <string name=\"donation\">Adományozás</string>\r\n  <string name=\"dialog_about\">Rólunk</string>\r\n  <string name=\"dialog_player_ui\">Videólejátszó</string>\r\n  <string name=\"player_show_ui_on_pause\">A felhasználói felület megjelenítése szünetnél</string>\r\n  <string name=\"player_pause_on_ok\">OK gomb szünet gombként működik</string>\r\n  <string name=\"player_ok_button_behavior\">OK gomb viselkedése</string>\r\n  <string name=\"player_only_ui\">Csak felhasználói felület</string>\r\n  <string name=\"player_ui_and_pause\">Felhasználói felület és szünet</string>\r\n  <string name=\"player_only_pause\">Csak szünet</string>\r\n  <string name=\"check_for_updates\">Frissítés keresése</string>\r\n  <string name=\"update_not_found\">A legújabb verziót használja</string>\r\n  <string name=\"update_in_progress\">Kis türelmet…</string>\r\n  <string name=\"player_ui_hide_behavior\">A felhasználói felület automatikus elrejtése</string>\r\n  <string name=\"option_never\">Soha</string>\r\n  <string name=\"side_panel_sections\">Főmenü testreszabása</string>\r\n  <string name=\"boot_to_section\">Indulás a kiválasztott menüvel</string>\r\n  <string name=\"large_ui\">Nagyobb felhasználói felület</string>\r\n  <string name=\"video_grid_scale\">Videó rácsnézet mérete</string>\r\n  <string name=\"scale_ui\">Felhasználói felület mérete</string>\r\n  <string name=\"color_scheme\">Színséma</string>\r\n  <string name=\"color_scheme_default\">Alapértelmezett</string>\r\n  <string name=\"color_scheme_red_grey\">Vörös-szürke</string>\r\n  <string name=\"color_scheme_red\">Vörös</string>\r\n  <string name=\"color_scheme_dark_grey\">Sötétszürke</string>\r\n  <string name=\"disable_update_check\">Frissítés ellenőrzésének kikapcsolása</string>\r\n  <string name=\"show_again\">Újra</string>\r\n  <string name=\"check_updates_auto\">Új verzió automatikus ellenőrzése</string>\r\n  <string name=\"select_account_on_boot\">Fiókválasztás induláskor</string>\r\n  <string name=\"player_other\">Egyéb beállítások</string>\r\n  <string name=\"player_full_date\">Pontos dátum a leírásban</string>\r\n  <string name=\"open_channel\">Csatorna megnyitása</string>\r\n  <string name=\"not_interested\">Nem érdekel</string>\r\n  <string name=\"you_wont_see_this_video\">A videó eltávolítása került az ajánlottból</string>\r\n  <string name=\"settings_search\">Keresés</string>\r\n  <string name=\"dialog_search\">Keresés</string>\r\n  <string name=\"instant_voice_search\">Hangalapú keresés</string>\r\n  <string name=\"option_background_playback_behind\">Lejátszás a háttérben</string>\r\n  <string name=\"next_video_info_is_not_loaded_yet\">Következő videó még nem töltődött be</string>\r\n  <string name=\"card_multiline_title\">Többsoros címek</string>\r\n  <string name=\"cards_style\">Megjelenés</string>\r\n  <string name=\"repeat_mode_all\">Lejátszás egymás után</string>\r\n  <string name=\"repeat_mode_one\">Aktuális videó ismétlése</string>\r\n  <string name=\"repeat_mode_pause\">Szünet minden videó után</string>\r\n  <string name=\"repeat_mode_pause_alt\">Csak a videólistáról játsszon videókat</string>\r\n  <string name=\"repeat_mode_none\">Lejátszás végén stop</string>\r\n  <string name=\"repeat_mode_shuffle\">Véletlenszerű lejátszás</string>\r\n  <string name=\"subscribed_to_channel\">Feliratkozva</string>\r\n  <string name=\"unsubscribed_from_channel\">Leliratkozva</string>\r\n  <string name=\"subtitle_yellow_transparent\">Sárga, átlátszó háttérrel</string>\r\n  <string name=\"subtitle_white_black\">Fehér, fekete háttérrel</string>\r\n  <string name=\"player_seek_preview\">Előnézet gyorstekeréskor</string>\r\n  <string name=\"color_scheme_dark_grey_oled\">Sötétszürke (OLED)</string>\r\n  <string name=\"channels_section_sorting\">Csatornák rendezése</string>\r\n  <string name=\"sorting_by_new_content\">Új tartalom</string>\r\n  <string name=\"sorting_alphabetically\">Abc szerint</string>\r\n  <string name=\"sorting_last_viewed\">Utoljára nézett</string>\r\n  <string name=\"player_pause_when_seek\">Szünet gyorstekeréskor</string>\r\n  <string name=\"playlists_style\">Videólista nézete</string>\r\n  <string name=\"playlists_style_grid\">Rácsnézet</string>\r\n  <string name=\"playlists_style_rows\">Lista</string>\r\n  <string name=\"player_show_clock\">Óra megjelenítése a kezelőpanelen</string>\r\n  <string name=\"player_show_remaining_time\">Hátralevő idő megjelenítése a kezelőpanelen</string>\r\n  <string name=\"open_channel_uploads\">Csatorna feltöltések megnyitása</string>\r\n  <string name=\"app_exit_shortcut\">Kilépés módjának beállítása</string>\r\n  <string name=\"app_exit_none\">Ne lépjen ki</string>\r\n  <string name=\"app_double_back_exit\">Vissza gomb kétszer nyomva</string>\r\n  <string name=\"app_single_back_exit\">Vissza gomb egyszer nyomva</string>\r\n  <string name=\"action_video_zoom\">Videó zoom</string>\r\n  <string name=\"video_zoom\">Videó zoom</string>\r\n  <string name=\"video_zoom_default\">Alapértelmezett</string>\r\n  <string name=\"video_zoom_fit_width\">Szélességhez igazít</string>\r\n  <string name=\"video_zoom_fit_height\">Magassághoz igazít</string>\r\n  <string name=\"video_zoom_fit_both\">Arányosan nyújtva</string>\r\n  <string name=\"video_zoom_stretch\">Nyújtás</string>\r\n  <string name=\"color_scheme_teal\">Zöldeskék</string>\r\n  <string name=\"color_scheme_teal_oled\">Zöldeskék (OLED)</string>\r\n  <string name=\"player_seek_preview_none\">Tiltva</string>\r\n  <string name=\"player_seek_preview_single\">Egyetlen képkocka</string>\r\n  <string name=\"player_seek_preview_carousel\">Carousel</string>\r\n  <string name=\"player_seek_preview_carousel_slow\">Carousel (lassú)</string>\r\n  <string name=\"player_seek_preview_carousel_fast\">Carousel (gyors)</string>\r\n  <string name=\"unsubscribe_from_channel\">Leiratkozás a csatornáról</string>\r\n  <string name=\"card_title_lines_num\">Sorok száma a címekben</string>\r\n  <string name=\"card_auto_scrolled_title\">A cím automatikus görgetése</string>\r\n  <string name=\"share_link\">Megosztás</string>\r\n  <string name=\"subscribe_to_channel\">Feliratkozás a csatornára</string>\r\n  <string name=\"wait_data_loading\">Várjon, amíg az adatok betöltődnek..</string>\r\n  <string name=\"auto_frame_rate_pause\">Automatikus képkockasebesség (szünet kapcsoláskor)</string>\r\n  <string name=\"auto_frame_rate_applying\">Automatikus képkockasebesség alkalmazása %sx%s\\@%s</string>\r\n  <string name=\"audio_shift\">Hangeltolás</string>\r\n  <string name=\"player_remember_speed\">Lejátszási sebesség</string>\r\n  <string name=\"mark_channel_as_watched\">Megjelölés nézettként</string>\r\n  <string name=\"channel_marked_as_watched\">Csatorna nézettként jelölve</string>\r\n  <string name=\"dialog_add_device\">Eszköz hozzáadása</string>\r\n  <string name=\"dialog_remove_all_devices\">Minden eszköz törlése</string>\r\n  <string name=\"device_link_enabled\">Link engedélyezve</string>\r\n  <string name=\"device_connected\">Eszköz \\\"%s\\\" csatlakoztatva</string>\r\n  <string name=\"device_disconnected\">Eszköz \\\"%s\\\" leválasztva</string>\r\n  <string name=\"settings_ui_scale\">Felületméretezés</string>\r\n  <string name=\"msg_restart_app\">Kérjük, indítsa újra az alkalmazást a beállítások érvényesítéséhez</string>\r\n  <string name=\"settings_block\">Tartalomblokkolás</string>\r\n  <string name=\"msg_applying\">Alkalmazás %s…</string>\r\n  <string name=\"msg_done\">Kész</string>\r\n  <string name=\"settings_general\">Általános</string>\r\n  <string name=\"settings_video\">Videó</string>\r\n  <string name=\"content_block_confirm_skip\">Megerősítés ugráskor</string>\r\n  <string name=\"content_block_categories\">Kategóriák</string>\r\n  <string name=\"content_block_sponsor\">Sponsor</string>\r\n  <string name=\"content_block_intro\">Szünet / intro animáció</string>\r\n  <string name=\"content_block_outro\">Endcards/credits</string>\r\n  <string name=\"content_block_interaction\">Interakció emlékeztető (feliratkozás)</string>\r\n  <string name=\"content_block_self_promo\">Fizetetlen / önreklám</string>\r\n  <string name=\"content_block_music_off_topic\">A klip nem zenei része</string>\r\n  <string name=\"confirm_segment_skip\">Szegmens kihagyása \\\"%s\\\"\\?</string>\r\n  <string name=\"msg_skipping_segment\">Szegmens kihagyva \\\"%s\\\"…</string>\r\n  <string name=\"content_block_notification_type\">Értesítés típusa</string>\r\n  <string name=\"content_block_notify_none\">Értesítés nélkül</string>\r\n  <string name=\"content_block_notify_toast\">Felugró</string>\r\n  <string name=\"content_block_notify_dialog\">Megerősítő párbeszédpanel</string>\r\n  <string name=\"return_to_launcher\">Visszatérés az kezdőlapra a csatornákról/keresésből</string>\r\n  <string name=\"intent_force_close\">Kényszerített kilépés, ha a videót külső forrásból nyitották meg</string>\r\n  <string name=\"btn_confirm\">Megerősítés</string>\r\n  <string name=\"player_low_video_quality\">Alacsony videóminőség</string>\r\n  <string name=\"remote_session_closed\">A távoli munkamenet lezárult</string>\r\n  <string name=\"msg_mode_switch_error\">Nem lehet átállítani a megjelenítési módot \\\"%s\\\"</string>\r\n  <string name=\"msg_player_error\">Lejátszási hiba történt %s. A lejátszás újraindítása..</string>\r\n  <string name=\"open_playlist\">Videólista megnyitása</string>\r\n  <string name=\"playback_queue_category_title\">Videólista</string>\r\n  <string name=\"video_buffer_size_none\">Nincs megadva</string>\r\n  <string name=\"subtitle_white_transparent\">Fehér, átlátszó háttérrel</string>\r\n  <string name=\"player_show_ending_time\">Videó hossza</string>\r\n  <string name=\"cancel_segment_skip\">Szegmens kihagyásának megszakítása</string>\r\n  <string name=\"video_preset_disabled\">Nincs megadva</string>\r\n  <string name=\"video_preset_enabled\">A következő videó formátuma az előre beállított értékek szerint lesz beállítva</string>\r\n  <string name=\"player_sleep_timer\">Ébresztőóra</string>\r\n  <string name=\"player_show_quality_info\">Bitráta megjelenítése a videó információknál</string>\r\n  <string name=\"player_remember_each_speed\">Emlékezzen minden videó sebességére</string>\r\n  <string name=\"player_remember_speed_none\">Alapértelmezett</string>\r\n  <string name=\"player_remember_speed_all\">Minden videón ugyanaz</string>\r\n  <string name=\"player_remember_speed_each\">Videónként eltérő</string>\r\n  <string name=\"msg_player_error_source\">Nem lehet letölteni a videót</string>\r\n  <string name=\"msg_player_error_renderer\">A kiválasztott videó formátum nem támogatott.\\nLehet, hogy ez firmware-hiba. Próbálja meg újraindítani az eszközt.</string>\r\n  <string name=\"msg_player_error_unexpected\">Ismeretlen videó dekódoló hiba</string>\r\n  <string name=\"video_aspect\">Képarány</string>\r\n  <string name=\"player_tweaks\">Fejlesztői beállítások</string>\r\n  <string name=\"header_uploads\">Feltöltések</string>\r\n  <string name=\"player_show_global_clock\">Az aktuális idő mindig látható</string>\r\n  <string name=\"player_show_global_ending_time\">A videó végének ideje mindig látható</string>\r\n  <string name=\"uploads_old_look\">A Feltöltések szakasz régi megjelenésének visszaállítása</string>\r\n  <string name=\"background_playback_activation\">Lejátszás a háttérben (aktiválás)</string>\r\n  <string name=\"channels_old_look\">A csatornák régi megjelenésének visszaállítása</string>\r\n  <string name=\"channels_auto_load\">A csatornák tartalmának automatikus betöltése</string>\r\n  <string name=\"return_to_background_video\">Visszatérés a háttérben futó videóhoz</string>\r\n  <string name=\"pin_unpin_from_sidebar\">Rögzítés/feloldás az oldalsávról</string>\r\n  <string name=\"pin_unpin_playlist\">Videólista rögzítése/feloldása az oldalsávról</string>\r\n  <string name=\"pin_unpin_channel\">Feliratkozások rögzítése/feloldása az oldalsávról</string>\r\n  <string name=\"pinned_to_sidebar\">Az oldalsávhoz rögzítve</string>\r\n  <string name=\"unpin_from_sidebar\">Rögzítés feloldása az oldalsávról</string>\r\n  <string name=\"unpinned_from_sidebar\">Leválasztva az oldalsávról</string>\r\n  <string name=\"dialog_select_country\">Ország</string>\r\n  <string name=\"settings_language_country\">Nyelv/Ország</string>\r\n  <string name=\"share_embed_link\">Beágyazási hivatkozás megosztása</string>\r\n  <string name=\"card_text_scroll_factor\">Indexkép szöveg görgetési sebessége</string>\r\n  <string name=\"preferred_update_source\">Válassza ki a frissítési forrást</string>\r\n  <string name=\"hide_shorts\">Rövid klipek elrejtése a Feliratkozások menüből</string>\r\n  <string name=\"key_remapping\">Billentyű hozzárendelések cseréje</string>\r\n  <string name=\"screen_dimming\">Képernyő elsötétítése</string>\r\n  <string name=\"removed_from_playback_queue\">Videó eltávolítva</string>\r\n  <string name=\"added_to_playback_queue\">Hozzáadva a Megnézendő videókhoz</string>\r\n  <string name=\"add_remove_from_playback_queue\">Videó Hozzáadása/eltávolítása</string>\r\n  <string name=\"add_to_playback_queue\">Hozzáadás a Megnézendő videókhoz</string>\r\n  <string name=\"remove_from_playback_queue\">Videó eltávolítása</string>\r\n  <string name=\"proxy_enabled\">Proxy engedélyezve</string>\r\n  <string name=\"proxy_disabled\">Proxy letiltva</string>\r\n  <string name=\"uploads_row_name\">Feltöltések</string>\r\n  <string name=\"playlists_row_name\">Létrehozott videólisták</string>\r\n  <string name=\"popular_uploads_row_name\">Népszerű feltöltések</string>\r\n  <string name=\"breaking_news_row_name\">Friss hírek</string>\r\n  <string name=\"covid_news_row_name\">COVID-19 hírek</string>\r\n  <string name=\"enable_voice_search\">Hangalapú keresés engedélyezése (firmware támogatás szükséges)</string>\r\n  <string name=\"subscribe_unsubscribe_from_channel\">Feliratkozás/Leiratkozás a csatornáról</string>\r\n  <string name=\"app_backup_restore\">Biztonsági mentés</string>\r\n  <string name=\"app_backup\">Adatok biztonsági mentése</string>\r\n  <string name=\"app_restore\">Adatok visszaállítása biztonsági másolatból</string>\r\n  <string name=\"skip_each_segment_once\">Az egyes szegmenseket csak egyszer hagyja ki</string>\r\n  <string name=\"disable_ok_long_press\">Az OK gomb hosszú lenyomása tiltása</string>\r\n  <string name=\"player_time_correction\">Az idő megjelenítése a sebesség függvényében</string>\r\n  <string name=\"option_disabled\">Letiltva</string>\r\n  <string name=\"sponsor_color_markers\">Színes jelölők a folyamatjelző sávon</string>\r\n  <string name=\"refresh_section\">Videólista frissítése</string>\r\n  <string name=\"live_now_row_name\">Élő adás</string>\r\n  <string name=\"run_in_background\">Lejátszás a háttérben</string>\r\n  <string name=\"settings_remote_control\">Távvezérlés</string>\r\n  <string name=\"background_service_started\">Elindult a háttér szolgáltatás</string>\r\n  <string name=\"dialog_add_to\">Hozzáadás ehhez: %s</string>\r\n  <string name=\"dialog_remove_from\">Eltávolítás innen: %s</string>\r\n  <string name=\"added_to\">Hozzáadva ehhez: %s</string>\r\n  <string name=\"removed_from\">Eltávolítva innen: %s</string>\r\n  <string name=\"video_preset_adaptive\">Adaptív</string>\r\n  <string name=\"content_block_preview_recap\">A videó előnézete vagy összefoglalója</string>\r\n  <string name=\"content_block_highlight\">A videó pontja vagy kiemelése</string>\r\n  <string name=\"removed_from_history\">A videó eltávolítva az előzményekből</string>\r\n  <string name=\"remove_from_history\">Eltávolítás az előzményekből</string>\r\n  <string name=\"upload_date\">Feltöltés dátuma</string>\r\n  <string name=\"upload_date_any\">Mindig</string>\r\n  <string name=\"upload_date_today\">Ma</string>\r\n  <string name=\"upload_date_this_week\">Ezen a héten</string>\r\n  <string name=\"upload_date_this_month\">Ebben a hónapban</string>\r\n  <string name=\"upload_date_this_year\">Ebben az évben</string>\r\n  <string name=\"double_refresh_rate\">Dupla frissítési gyakoriság</string>\r\n  <string name=\"upload_date_last_hour\">Utolsó óra</string>\r\n  <string name=\"focus_on_search_results\">Autofókusz a keresési eredményekre</string>\r\n  <string name=\"context_menu\">Helyi menü</string>\r\n  <string name=\"add_remove_from_recent_playlist\">Hozzáadás/eltávolítás a legutóbbi videólistáról</string>\r\n  <string name=\"hide_settings_section\">Beállítások menü elrejtése (veszélyes!)</string>\r\n  <string name=\"volume\">Hangerő %s%%</string>\r\n  <string name=\"auto_frame_rate_sec\">%s mp</string>\r\n  <string name=\"audio_shift_sec\">%s mp</string>\r\n  <string name=\"ui_hide_timeout_sec\">%s mp</string>\r\n  <string name=\"screen_dimming_timeout_min\">%s perc</string>\r\n  <string name=\"mark_all_channels_watched\">Az összes csatorna megjelölése nézettként</string>\r\n  <string name=\"move_section_up\">Bejegyzés mozgatása felfelé</string>\r\n  <string name=\"move_section_down\">Bejegyzés mozgatása lefelé</string>\r\n  <string name=\"player_buttons\">A lejátszóban látható gombok</string>\r\n  <string name=\"action_playlist_add\">Videólistához adni</string>\r\n  <string name=\"action_video_stats\">Videó statisztika</string>\r\n  <string name=\"action_subscribe\">Feliratkozás</string>\r\n  <string name=\"action_channel\">Csatorna megnyitása</string>\r\n  <string name=\"action_pip\">PIP</string>\r\n  <string name=\"action_video_info\">Videó leírás</string>\r\n  <string name=\"action_screen_off\">Képernyő kikapcsolása</string>\r\n  <string name=\"action_playback_queue\">Megnézendő videók</string>\r\n  <string name=\"action_video_speed\">Videósebesség</string>\r\n  <string name=\"action_subtitles\">Feliratok</string>\r\n  <string name=\"action_like\">Tetszik</string>\r\n  <string name=\"action_dislike\">Nem tetszik</string>\r\n  <string name=\"action_play_pause\">Lejátszás/Szünet</string>\r\n  <string name=\"action_repeat_mode\">Lejátszási mód</string>\r\n  <string name=\"action_next\">Következő videó</string>\r\n  <string name=\"action_previous\">Előző videó</string>\r\n  <string name=\"action_high_quality\">Videó minőség</string>\r\n  <string name=\"content_block_no_skipping_mode\">Kihagyás nélküli mód</string>\r\n  <string name=\"content_block_action_type\">Válasszon egy műveletet</string>\r\n  <string name=\"content_block_action_none\">Ne csinálj semmit</string>\r\n  <string name=\"content_block_action_only_skip\">Kihagyás</string>\r\n  <string name=\"content_block_action_toast\">Kihagyás értesítéssel</string>\r\n  <string name=\"content_block_action_dialog\">Megerősítő párbeszédpanel megjelenítése</string>\r\n  <string name=\"content_block_filler\">Téma nélkül/Kitöltés</string>\r\n  <string name=\"keyboard_auto_show\">A billentyűzet automatikus megjelenítése</string>\r\n  <string name=\"cancel_dialog\">Mégsem</string>\r\n  <string name=\"msg_player_error_source2\">A videoforrás nem működik, vagy helytelen az idő.\\nÁltalában az alkalmazással van a probléma.</string>\r\n  <string name=\"hide_upcoming\">Közelgő premierek elrejtése a Feliratkozások menüből</string>\r\n  <string name=\"search_background_playback\">PIP lejátszás keresés/böngészés közben</string>\r\n  <string name=\"trending_row_name\">Felkapott</string>\r\n  <string name=\"player_seek_type\">Gyorstekerés viselkedése</string>\r\n  <string name=\"player_seek_regular\">Szabályos</string>\r\n  <string name=\"player_seek_confirmation_pause\">Megerősítéssel (szünet tekerés közben)</string>\r\n  <string name=\"player_seek_confirmation_play\">Megerősítéssel (lejátszás tekerés közben)</string>\r\n  <string name=\"hide_shorts_from_home\">Rövid klipek elrejtése a kezdőlapon</string>\r\n  <string name=\"finish_on_disconnect\">Az alkalmazás bezárása a telefon/táblagép leválasztása után</string>\r\n  <string name=\"update_error\">Frissítési hiba!</string>\r\n  <string name=\"hide_shorts_from_history\">Rövid klipek elrejtése az előzményekből</string>\r\n  <string name=\"disable_screensaver\">Képernyővédő tiltása</string>\r\n  <string name=\"description_not_found\">Leírás nem található</string>\r\n  <string name=\"proxy_port_hint\">Proxy port</string>\r\n  <string name=\"proxy_host_hint\">Proxy gépnév vagy IP</string>\r\n  <string name=\"proxy_username_hint\">Proxy felhasználónév</string>\r\n  <string name=\"proxy_password_hint\">Proxy jelszó</string>\r\n  <string name=\"proxy_type\">Proxy típus</string>\r\n  <string name=\"enable_web_proxy\">Web proxy használata</string>\r\n  <string name=\"proxy_not_supported\">Web proxy használata (Android 4.4+)</string>\r\n  <string name=\"proxy_test_btn\">Teszt</string>\r\n  <string name=\"proxy_settings_title\">Proxy szerver beállítások</string>\r\n  <string name=\"proxy_test_cancelled\">Teszt#%d: törölve.</string>\r\n  <string name=\"proxy_type_invalid\">Proxy típus nincs beállítva</string>\r\n  <string name=\"proxy_host_invalid\">Proxy nincs beállítva</string>\r\n  <string name=\"proxy_port_invalid\">Érvénytelen proxy port, &gt;0</string>\r\n  <string name=\"proxy_credentials_invalid\">Érvénytelen felhasználónév vagy jelszó</string>\r\n  <string name=\"proxy_test_aborted\">A teszt megszakadt, először javítsa ki a proxy beállításait.</string>\r\n  <string name=\"proxy_application_aborted\">Kérjük, először javítsa ki a proxy beállításait.</string>\r\n  <string name=\"proxy_test_start\">Teszt#%d: %s …</string>\r\n  <string name=\"proxy_test_error\">Hiba#%d: %s …</string>\r\n  <string name=\"proxy_test_status\">Státusz#%d: %s %d %s</string>\r\n  <string name=\"openvpn_config_address_hint\">Konfigurációs fájl címe (*.ovpn)</string>\r\n  <string name=\"enable_openvpn\">OpenVPN használata</string>\r\n  <string name=\"openvpn_settings_title\">OpenVPN beállítások</string>\r\n  <string name=\"openvpn_application_aborted\">Kérjük, először javítsa ki az OpenVPN beállításait.</string>\r\n  <string name=\"openvpn_test_aborted\">A teszt megszakadt, először javítsa ki az OpenVPN beállításait.</string>\r\n  <string name=\"openvpn_address_invalid\">Az OpenVPN konfigurációs címe nincs beállítva.</string>\r\n  <string name=\"internet_censorship\">Proxy beállítása</string>\r\n  <string name=\"rename_section\">Bejegyzés átnevezése</string>\r\n  <string name=\"simple_edit_value_hint\">Új érték</string>\r\n  <string name=\"seek_interval\">Gyorstekerési intervallum</string>\r\n  <string name=\"seek_interval_sec\">%s mp</string>\r\n  <string name=\"subtitle_system\">Rendszerstílus (Android beállítások &gt; Kisegítő lehetőségek)</string>\r\n  <string name=\"subtitle_scale\">Felirat mérete</string>\r\n  <string name=\"audio_sync_fix_desc\">A hang/videó szinkronizálásának alternatív módja. Elavultnak tekinthető, de bizonyos esetekben segíthet.</string>\r\n  <string name=\"audio_sync_fix\">Hangszinkron javítás</string>\r\n  <string name=\"ambilight_ratio_fix_desc\">Javítja a hiányzó torzítású világítást. Javítja a helytelen képarányt. Javítja az üres képernyőképeket. Befolyásolja a teljesítményt!</string>\r\n  <string name=\"ambilight_ratio_fix\">Ambilight/Képarány/Képernyőképek javítása</string>\r\n  <string name=\"force_legacy_codecs_desc\">Jelentősen javítja a teljesítményt gyenge hardveren. A maximális felbontás 720p.</string>\r\n  <string name=\"force_legacy_codecs\">Legacy kodekek kényszerítése (720p)</string>\r\n  <string name=\"live_stream_fix_desc\">Jelentősen javítja az élő adások teljesítményét gyenge hardveren. Letiltja a visszatekerést. A maximális felbontás 1080p.</string>\r\n  <string name=\"live_stream_fix\">Élő adás javítása (1080p)</string>\r\n  <string name=\"playback_notifications_fix_desc\">Elrejti a track módosítási értesítéseket. Hasznos lehet AOSP-alapú firmware-eknél.</string>\r\n  <string name=\"playback_notifications_fix\">Lejátszási értesítések letiltása</string>\r\n  <string name=\"amlogic_fix_desc\">Képkocka eldobás javítás Amlogic-alapú eszközökön.</string>\r\n  <string name=\"amlogic_fix\">Amlogic 1080p\\@60fps javítás</string>\r\n  <string name=\"tunneled_video_playback_desc\">MEGJEGYZÉS: előfordulhat, hogy a szünet nem működik megfelelően! Az alagútban történő videolejátszás olyan előnyökkel kecsegtet, mint a jobb hang/videó szinkronizálás és simább lejátszás. Android 5+ szükséges</string>\r\n  <string name=\"tunneled_video_playback\">Alagútban történő videólejátszás (Android 5+)</string>\r\n  <string name=\"master_volume\">Fő hangerő</string>\r\n  <string name=\"volume_limit\">Hangerő korlát</string>\r\n  <string name=\"play_video\">Lejátszás</string>\r\n  <string name=\"remember_position_of_short_videos\">Emlékezzen az 5 percnél rövidebb videók pozíciójára</string>\r\n  <string name=\"player_show_tooltips\">Buboréksúgó a gomboknál</string>\r\n  <string name=\"action_like_unset\">A \\\"Tetszik\\\" nincs beállítva</string>\r\n  <string name=\"action_dislike_unset\">A \\\"Nem tetszik\\\" nincs beállítva</string>\r\n  <string name=\"various_buttons\">Gombok a főablak tetején</string>\r\n  <string name=\"not_compatible_with\">A kiválasztott opció nem kompatibilis</string>\r\n  <string name=\"subtitle_position\">Felirat térbeli elhelyezése</string>\r\n  <string name=\"pressing_home\">a HOME gomb megnyomásával</string>\r\n  <string name=\"pressing_home_back\">a HOME vagy a BACK gomb megnyomásával</string>\r\n  <string name=\"player_number_key_seek\">Keresés a számbillentyűkkel</string>\r\n  <string name=\"save_remove_playlist\">Lista mentése/eltávolítása a videólisták részből</string>\r\n  <string name=\"remove_playlist\">Videó eltávolítása</string>\r\n  <string name=\"removed_from_playlists\">Eltávolítva a videólistából</string>\r\n  <string name=\"saved_to_playlists\">Mentés a videólistákhoz</string>\r\n  <string name=\"create_playlist\">Videólista létrehozása</string>\r\n  <string name=\"add_video_to_new_playlist\">Hozzáadás új videólistához</string>\r\n  <string name=\"cant_delete_empty_playlist\">Üres videólista nem törölhető</string>\r\n  <string name=\"playlist\">Videólista</string>\r\n  <string name=\"rename_playlist\">Videólista átnevezése</string>\r\n  <string name=\"cant_rename_empty_playlist\">Üres videólista nem nevezhető át</string>\r\n  <string name=\"cant_rename_foreign_playlist\">Egy idegen videólista nem nevezhető át</string>\r\n  <string name=\"cant_save_playlist\">Ez a videólista nem menthető</string>\r\n  <string name=\"enter_value\">Adjon meg bármilyen nem üres értéket</string>\r\n  <string name=\"dialog_add_remove_from\">Hozzáadás/eltávolítás innen: %s</string>\r\n  <string name=\"lb_playback_controls_skip_next\">Következő</string>\r\n  <string name=\"lb_playback_controls_skip_previous\">Előző/visszatekerés</string>\r\n  <string name=\"alt_presets_behavior\">Az Alt előre beállítja a viselkedést (korlátozza a sávszélességet)</string>\r\n  <string name=\"alt_presets_behavior_desc\">Az alkalmazás megpróbálja fenntartani a sávszélességet a kiválasztott előre beállított értéknek megfelelően a felbontás, az fps és a kodek közötti egyeztetés helyett.</string>\r\n  <string name=\"sleep_timer\">Elalváskapcsoló</string>\r\n  <string name=\"sleep_timer_desc\">Lejátszás szüneteltetése, ha a felhasználó egy órán keresztül nem használja a távirányítót.</string>\r\n  <string name=\"disable_vsync\">vsync letiltása</string>\r\n  <string name=\"disable_vsync_desc\">Kicsit javítja a lejátszási teljesítményt</string>\r\n  <string name=\"skip_codec_profile_check\">A kodekprofilszint-ellenőrzés kihagyása</string>\r\n  <string name=\"skip_codec_profile_check_desc\">Videó indításakor ne ellenőrizze a kodek támogatását. Hasznos lehet a bugos firmware-eknél.</string>\r\n  <string name=\"force_sw_codec\">Szoftveres videó dekóder kényszerítése</string>\r\n  <string name=\"force_sw_codec_desc\">Szinte bármilyen videót le tud játszani, de a teljesítmény nagyon gyenge.</string>\r\n  <string name=\"playlist_order\">Videólista rendezése</string>\r\n  <string name=\"playlist_order_added_date_newer_first\">Hozzáadás dátuma (először az újabbak)</string>\r\n  <string name=\"playlist_order_added_date_older_first\">Hozzáadás dátuma (először a régebbiek)</string>\r\n  <string name=\"playlist_order_published_date_newer_first\">Közzététel dátuma (először az újabbak)</string>\r\n  <string name=\"playlist_order_published_date_older_first\">Közzététel dátuma (először a régebbiek)</string>\r\n  <string name=\"playlist_order_popularity\">Népszerűség szerint</string>\r\n  <string name=\"cant_do_this_for_foreign_playlist\">Idegen videólisták esetén ez nem lehetséges</string>\r\n  <string name=\"owned_playlist_warning\">Hiba. Ez a művelet csak a saját videólistákra alkalmazható</string>\r\n  <string name=\"content_block_alt_server\">Használjon alternatív szervert</string>\r\n  <string name=\"content_block_alt_server_desc\">Engedélyezze ezt az opciót, ha a SponsorBlock különböző okok miatt megtagadja a munkát.</string>\r\n  <string name=\"unset_stream_reminder\">Stream emlékeztető visszavonása</string>\r\n  <string name=\"set_stream_reminder\">Stream emlékeztető beállítása</string>\r\n  <string name=\"playback_starts_shortly\">A lejátszás automatikusan elindul, amikor az adatfolyam készen áll</string>\r\n  <string name=\"starting_stream\">A stream indítása…</string>\r\n  <string name=\"action_playlist_remove\">Eltávolítás a videólistáról</string>\r\n  <string name=\"signin_view_description\">A bejelentkezéshez írja be ezt a kódot a %s oldalon \\n MEGJEGYZÉS. Csak Firefox vagy Chrome böngészőkkel működik.</string>\r\n  <string name=\"signin_view_title\">A felhasználói kód betöltődik…</string>\r\n  <string name=\"signin_view_action_text\">Kész</string>\r\n  <string name=\"require_checked\">Szükséges: \\\" %s</string>\r\n  <string name=\"msg_press_again_to_exit\">Nyomja meg újra a kilépéshez</string>\r\n  <string name=\"player_remaining_time\">Maradt: %s</string>\r\n  <string name=\"player_ending_time\">Vége: %s</string>\r\n  <string name=\"badge_new_content\">ÚJ TARTALOM</string>\r\n  <string name=\"badge_live\">ÉLŐ</string>\r\n  <string name=\"add_device_view_description\">Az eszköz összekapcsolásához írja be ezt a kódot telefonja YouTube-alkalmazásában a Beállítások/Megtekintés tévén szakaszban. \\n MEGJEGYZÉS. Csak Gboard billentyűzettel működik.</string>\r\n  <string name=\"playback_controls_repeat_pause\">Szünet ismétlése</string>\r\n  <string name=\"playback_controls_repeat_list\">Ismétlési lista</string>\r\n  <string name=\"action_subscribe_off\">Feliratkozás kikapcsolva</string>\r\n  <string name=\"action_subscribe_on\">Feliratkozás bekapcsolva</string>\r\n  <string name=\"skip_24_rate\">24 képkocka/mp-es formátumok kihagyása (javítás az igazi mozi módban\\?)</string>\r\n  <string name=\"player_disable_suggestions\">Javaslatok letiltása</string>\r\n  <string name=\"feedback\">Visszajelzés</string>\r\n  <string name=\"sources\">Program forrása (QR)</string>\r\n  <string name=\"releases\">Programverziók (QR)</string>\r\n  <string name=\"prefer_avc_over_vp9\">Kodek kiválasztása: Az avc előnyben részesítése a vp9 helyett</string>\r\n  <string name=\"open_chat\">Hozzászólások</string>\r\n  <string name=\"place_chat_left\">Hozzászólások balra helyezése</string>\r\n  <string name=\"use_alt_speech_recognizer\">Alternatív beszédfelismerő</string>\r\n  <string name=\"time_format\">Idő formátum</string>\r\n  <string name=\"time_format_24\">24 órás</string>\r\n  <string name=\"time_format_12\">12 órás (AM/PM)</string>\r\n  <string name=\"use_alt_speech_recognizer_desc\">Komolyan bugyuta. Csak akkor használja, ha problémái vannak az alapértelmezett felismerővel.</string>\r\n  <string name=\"speech_recognizer\">Beszédfelismerő</string>\r\n  <string name=\"speech_recognizer_system\">Rendszer (javasolt)</string>\r\n  <string name=\"speech_recognizer_external_1\">Külső 1 (komolyan hibás, a működéshez le kell tiltani a mikrofonhoz való hozzáférést)</string>\r\n  <string name=\"speech_recognizer_external_2\">Külső 2 (komolyan hibás)</string>\r\n  <string name=\"real_channel_icon\">Logo megjelenítése a csatorna megnyitása gombon</string>\r\n  <string name=\"subtitle_yellow_semi_transparent\">Sárga, félig átlátszó háttérrel</string>\r\n  <string name=\"subtitle_yellow_black\">Sárga, fekete háttérel</string>\r\n  <string name=\"player_pixel_ratio\">Képernyőfelbontás</string>\r\n  <string name=\"color_scheme_dark_grey_monochrome\">Sötétszürke (monokróm)</string>\r\n  <string name=\"disable_mic_permission\">Kérjük, tiltsa le az alkalmazás mikrofonhoz való hozzáférését, hogy ez a felismerő megfelelően működjön.</string>\r\n  <string name=\"chat_left\">Bal</string>\r\n  <string name=\"chat_right\">Jobb</string>\r\n  <string name=\"card_real_thumbnails\">Indexképek cseréje a videó képére</string>\r\n  <string name=\"card_content\">Indexképek kiválasztása</string>\r\n  <string name=\"thumb_quality_default\">Alapértelmezett</string>\r\n  <string name=\"thumb_quality_start\">A videó elejéről</string>\r\n  <string name=\"thumb_quality_middle\">A videó közepéről</string>\r\n  <string name=\"thumb_quality_end\">A videó végéről</string>\r\n  <string name=\"content_block_status\">SponsorBlock állapotának ellenőrzése</string>\r\n  <string name=\"player_show_quality_info_bitrate\">Bitráta megjelenítése</string>\r\n  <string name=\"player_speed_button_old_behavior\">A sebesség gomb régi viselkedésének visszaállítása</string>\r\n  <string name=\"protect_settings_with_password\">Beállítások védelme jelszóval</string>\r\n  <string name=\"enter_settings_password\">Adja meg a beállítások jelszavát</string>\r\n  <string name=\"child_mode\">Gyermekmód</string>\r\n  <string name=\"child_mode_desc\">Ebben a módban a felhasználó nem használhat keresést, és nem láthat semmilyen javasolt tartalmat. A beállítások a jelszó alatt lesznek.</string>\r\n  <string name=\"lost_setting_warning\">Az alkalmazás beállításai módosulnak. Győződjön meg arról, hogy biztonsági másolatot készített a beállításokról.</string>\r\n  <string name=\"player_button_long_click\">Rövid kattintás a gombra a gyors művelethez, hosszú a beállításokhoz.</string>\r\n  <string name=\"pause_history\">Előzmények szüneteltetése</string>\r\n  <string name=\"resume_history\">Előzmények folytatása</string>\r\n  <string name=\"clear_history\">Előzmények törlése</string>\r\n  <string name=\"chapters\">Fejezetek</string>\r\n  <string name=\"card_multiline_subtitle\">Többsoros feliratok</string>\r\n  <string name=\"auto_frame_rate_modes\">Támogatott módok</string>\r\n  <string name=\"video_rotate\">Forgatás</string>\r\n  <string name=\"loading\">Betöltés...</string>\r\n  <string name=\"hide_streams\">Élő adás elrejtése a Feliratkozásokból</string>\r\n  <string name=\"trending_searches\">Felkapott keresések</string>\r\n  <string name=\"video_duration_any\">Bármi</string>\r\n  <string name=\"video_duration\">Időtartam</string>\r\n  <string name=\"video_duration_under_4\">4 perc alatt</string>\r\n  <string name=\"video_duration_between_4_20\">4-20 perc között</string>\r\n  <string name=\"video_duration_over_20\">Több mint 20 perc</string>\r\n  <string name=\"content_type\">Típus</string>\r\n  <string name=\"content_type_any\">Bármi</string>\r\n  <string name=\"content_type_video\">Videó</string>\r\n  <string name=\"content_type_channel\">Csatorna</string>\r\n  <string name=\"content_type_playlist\">Videólista</string>\r\n  <string name=\"content_type_movie\">Film</string>\r\n  <string name=\"video_features\">Jellemzők</string>\r\n  <string name=\"video_feature_any\">Bármi</string>\r\n  <string name=\"video_feature_live\">Élő</string>\r\n  <string name=\"video_feature_4k\">4K</string>\r\n  <string name=\"video_feature_hdr\">HDR</string>\r\n  <string name=\"pressing_back\">A BACK megnyomásával</string>\r\n  <string name=\"search_sorting\">Rendezési elv</string>\r\n  <string name=\"sort_by_relevance\">Fontosság szerint</string>\r\n  <string name=\"sort_by_views\">Megtekintés száma szerint</string>\r\n  <string name=\"sort_by_date\">Feltöltés dátuma szerint</string>\r\n  <string name=\"sort_by_rating\">Értékelés szerint</string>\r\n  <string name=\"clear_search_history\">Keresési előzmények törlése</string>\r\n  <string name=\"remove_from_subscriptions\">Eltávolítás a feliratkozásokból</string>\r\n  <string name=\"disable_ok_long_press_desc\">Hibás vezérlőkhöz való, ahol az OK gomb nem működik megfelelően</string>\r\n  <string name=\"live_stream_fix_4k_desc\">Figyelem, ez a módosítás letiltja az adatfolyamok visszatekerését. Jelentősen javítja az élő közvetítés teljesítményét. A maximális felbontás 4K.</string>\r\n  <string name=\"live_stream_fix_4k\">Élő közvetítés javítása (4K)</string>\r\n  <string name=\"player_long_speed_list\">Hosszú sebességlista</string>\r\n  <string name=\"alt_app_icon\">Alternatív alkalmazás ikonja (újraindítás szükséges)</string>\r\n  <string name=\"network_stack\">A %s hálózati verem előnyben részesítése</string>\r\n  <string name=\"cronet_desc\">A Cronet a Chromium hálózati verem. A Cronet csökkentheti a várakozási időt és növelheti a hálózati teljesítményt, ami segíthet a pufferelési problémákon.</string>\r\n  <string name=\"unlock_all_formats\">Minden videó formátum feloldása</string>\r\n  <string name=\"unlock_all_formats_desc\">Egyes eszközökön a firmware tévesen azt jelenti, hogy bizonyos formátumok nem támogatottak, még akkor is, ha támogatottak.\\n (pl. az 1080p okostévék általában nem támogatottként jelzik a 4k-t).</string>\r\n  <string name=\"okhttp_desc\">Stabil, de lassú hálózati verem.</string>\r\n  <string name=\"select_channel_section\">A megfelelő kategória megnyitása, ha az alkalmazást az Android TV csatornákon keresztül indítjuk el.</string>\r\n  <string name=\"enable_voice_search_desc\">A hivatalos alkalmazást egy úgynevezett híd váltja fel. A híd segít a globális keresési kérelmek átvitelében az alkalmazásunkba.</string>\r\n  <string name=\"enable_master_password\">Mester jelszó engedélyezése</string>\r\n  <string name=\"enter_master_password\">Adja meg a Mester jelszót</string>\r\n  <string name=\"disable_stream_buffer\">Puffer letiltása az adatfolyamokon</string>\r\n  <string name=\"disable_stream_buffer_desc\">Javítás azokra a helyzetekre, amikor az adatfolyam nagyon késik.</string>\r\n  <string name=\"sony_frame_drop_fix\">Képkocka eldobása #1</string>\r\n  <string name=\"sony_frame_drop_fix_desc\">Javítja a késleltetést / késést a Sony TV-n és néhány más eszközön.</string>\r\n  <string name=\"audio_language\">Hang nyelve</string>\r\n  <string name=\"old_home_look\">A Kezdőlap régi megjelenése</string>\r\n  <string name=\"hide_shorts_everywhere\">Rövid klipek elrejtése mindenhol</string>\r\n  <string name=\"update_found\">Frissítés</string>\r\n  <string name=\"volume_boost_warning\">A határértéket meghaladó beállítások károsíthatják a hangszórókat</string>\r\n  <string name=\"play_video_incognito\">Lejátszás inkognitó módban</string>\r\n  <string name=\"header_kids_home\">Gyermekvideók</string>\r\n  <string name=\"player_section_playlist\">Az aktuális kategória tartalmának használata lejátszási listaként</string>\r\n  <string name=\"not_recommend_channel\">Ne ajánl csatornát</string>\r\n  <string name=\"you_wont_see_this_channel\">Ez a csatorna nem jelenik meg az ajánlások között</string>\r\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\r\n  <string name=\"header_trending\">Felkapott</string>\r\n  <string name=\"screensaver\">Képernyővédő</string>\r\n  <string name=\"player_screen_off_timeout\">Képernyő kikapcsolása</string>\r\n  <string name=\"old_update_notifications\">A frissítési értesítések régi megjelenése</string>\r\n  <string name=\"mark_as_watched\">Megjelölés nézettként</string>\r\n  <string name=\"player_ui_animations\">UI animációk engedélyezése</string>\r\n  <string name=\"player_likes_count\">A Tetszik/Nem tetszik számának megjelenítése</string>\r\n  <string name=\"sorting_alphabetically2\">Rendezés ABC szerint</string>\r\n  <string name=\"sorting_default\">Alapértelmezett rendezés</string>\r\n  <string name=\"content_block_exclude_channel\">Csatorna kizárása a SponsorBlock-ból</string>\r\n  <string name=\"content_block_stop_excluding_channel\">Hagyja abba a csatorna kizárását a SponsorBlock-ból</string>\r\n  <string name=\"player_chapter_notification\">Értesítés megjelenítése a fejezetek ugrásához</string>\r\n  <string name=\"player_volume\">Hangerő</string>\r\n  <string name=\"player_network_stack\">Hálózati verem</string>\r\n  <string name=\"subtitle_remember\">Emlékezzen a felirat beállításokra a felvett csatornákon</string>\r\n  <string name=\"amazon_frame_drop_fix\">Képkocka eldobása #2</string>\r\n  <string name=\"amazon_frame_drop_fix_desc\">Amazon Stick eszközökhöz készült. Működhet más eszközökön is.</string>\r\n  <string name=\"default_stack_desc\">Beépített hálózati verem. Ez a verem bizonyos helyzetekben jobb stabilitású lehet.</string>\r\n  <string name=\"item_postion\">Pozíciója </string>\r\n  <string name=\"player_screen_off_dimming\">Képernyő kikapcsolásának fényereje</string>\r\n  <string name=\"screensaver_timout\">Képernyővédő időtúllépés</string>\r\n  <string name=\"screensaver_dimming\">Képernyővédő elsötétítés mértéke</string>\r\n  <string name=\"player_ui_on_next\">A lejátszó felhasználói felületének megjelenítése, amikor a következő videóra vált</string>\r\n  <string name=\"autogenerated\">automatikusan generált</string>\r\n  <string name=\"player_auto_volume\">Automatikus hangerő beállítás</string>\r\n  <string name=\"header_shorts\">Rövid klipek</string>\r\n  <string name=\"player_global_focus\">Zökkenőmentes navigáció a lejátszó gombsorai között</string>\r\n  <string name=\"dialog_notification\">Értesítés a frissítésről, felugró párbeszédpanelen</string>\r\n  <string name=\"disable_history\">Előzmények tiltása</string>\r\n  <string name=\"enable_history\">Előzmények engedélyezése</string>\r\n  <string name=\"open_comments\">Hozzászólások megnyitása</string>\r\n  <string name=\"msg_player_error_subtitle_source\">A FELIRAT URL-címe nem működik, vagy helytelen az eszközidő.\\nÁltalában az alkalmazással van a probléma.</string>\r\n  <string name=\"msg_player_error_audio_source\">A HANGSÁV URL-címe nem működik, vagy helytelen az eszközidő.\\nÁltalában az alkalmazással van a probléma.</string>\r\n  <string name=\"msg_player_error_video_source\">A VIDEÓ URL-címe nem működik, vagy helytelen az eszközidő.\\nÁltalában az alkalmazással van a probléma.</string>\r\n  <string name=\"msg_player_error_subtitle_renderer\">A kiválasztott FELIRAT formátum nem támogatott.\\nPróbáljon másikat választani a lejátszó CC gombjának hosszan lenyomásával.\\nHa ez nem segít, próbálja meg újraindítani az eszközt.</string>\r\n  <string name=\"msg_player_error_audio_renderer\">A kiválasztott HANGSÁV formátum nem támogatott.\\nPróbáljon másikat választani a lejátszó HQ gombjának lenyomásával.\\nHa ez nem segít, próbálja meg újraindítani az eszközt.</string>\r\n  <string name=\"msg_player_error_video_renderer\">A kiválasztott VIDEÓ formátum nem támogatott.\\nPróbáljon másikat választani a lejátszó HQ gombjának lenyomásával.\\nHa ez nem segít, próbálja meg újraindítani az eszközt.</string>\r\n  <string name=\"share_qr_link\">Megosztás QR kóddal</string>\r\n  <string name=\"auto_history\">Automatikus (a fiókbeállítások használata)</string>\r\n  <string name=\"remember_position_subscriptions\">Emlékezzen az csatornák utoljára megtekintett pozíciójára</string>\r\n  <string name=\"msg_player_unknown_error\">Ismeretlen hiba</string>\r\n  <string name=\"header_notifications\">Értesítések</string>\r\n  <string name=\"disable_search_history\">Keresési előzmények tiltása</string>\r\n  <string name=\"unlock_high_bitrate_formats\">HD 1080p vp9 videó formátumok feloldása</string>\r\n  <string name=\"unlock_high_bitrate_audio_formats\">HD mp4a audió formátumok feloldása</string>\r\n  <string name=\"color_scheme_blue\">Kék</string>\r\n  <string name=\"color_scheme_dark_blue\">Sötétkék</string>\r\n  <string name=\"player_speed_per_channel\">Minden csatornánként</string>\r\n  <string name=\"disable_popular_searches\">Népszerű keresési lekérdezések letiltása</string>\r\n  <string name=\"protect_account_with_password\">Védje ezt a fiókot jelszóval</string>\r\n  <string name=\"multi_profiles\">Használjon külön beállításokat minden fiókhoz</string>\r\n  <string name=\"enter_account_password\">Adja meg a fiók jelszavát</string>\r\n  <string name=\"show_connect_messages\">Csatlakozási üzenetek megjelenítése</string>\r\n  <string name=\"prefer_avc_over_vp9_desc\">FIgyelem!: Maximum 1080p</string>\r\n  <string name=\"news_row_name\">Hírek</string>\r\n  <string name=\"play_next\">Következő lejátszása</string>\r\n  <string name=\"nothing_found\">Nincs találat</string>\r\n  <string name=\"app_corner_clock\">Főoldal: aktuális idő a jobb felső sarokban</string>\r\n  <string name=\"player_corner_clock\">Lejátszó: aktuális idő a jobb felső sarokban</string>\r\n  <string name=\"player_corner_ending_time\">Lejátszó: befejezési idő a jobb felső sarokban</string>\r\n  <string name=\"hide_upcoming_channel\">Közelgő premierek elrejtése a Csatornák menüből</string>\r\n  <string name=\"hide_upcoming_home\">Közelgő premierek elrejtése a Főoldalon</string>\r\n  <string name=\"hide_shorts_from_trending\">Rövid klipek elrejtése a Felkapott menüből</string>\r\n  <string name=\"hide_watched_from_subscriptions\">Megnézett videók elrejtése a Feliratkozások menüből</string>\r\n  <string name=\"hide_unwanted_content\">Kéretlen tartalom elrejtése</string>\r\n  <string name=\"hide_watched_from_home\">Megnézett videók elrejtése a Főoldalról</string>\r\n  <string name=\"login_from_browser\">Bejelentkezés böngészőből</string>\r\n  <string name=\"disable_remote_history\">Előzmények kikapcsolása távvezérlő használatakor</string>\r\n  <string name=\"remote_control_permission\">A háttérben való távvezérléshez átfedés-engedély szükséges</string>\r\n  <string name=\"keyboard_fix\">Megakadályozza a billentyűzet megjelenítését az OK gomb megnyomásával (G20s és egyebek)</string>\r\n  <string name=\"hide_shorts_channel\">Rövid klipek elrejtése a Csatornák menüből</string>\r\n  <string name=\"channel_filter_hint\">Csatornaszűrés</string>\r\n  <string name=\"player_quick_shorts_skip\">Rövid klip lapozás Bal/jobb gomb</string>\r\n  <string name=\"channels_filter\">Csatornaszűrés a Csatornák részben</string>\r\n  <string name=\"channel_search_bar\">Keresősáv a csatornaoldalon belül</string>\r\n  <string name=\"player_loop_shorts\">Rövid klipek újrajátszása</string>\r\n  <string name=\"auto_frame_rate_desc\">Ez az opció eltávolítja a remegést olyan jeleneteknél, ahol a kamera gyorsan mozog, pl. sport streaming</string>\r\n  <string name=\"save_playlist\">Lista hozzáadása a videólisták részhez</string>\r\n  <string name=\"pin_playlist\">Videólista hozzáadása az oldalsávhoz</string>\r\n  <string name=\"pin_channel\">Csatorna hozzáadása az oldalsávhoz</string>\r\n  <string name=\"action_sound_off\">Hang kikapcsolása</string>\r\n  <string name=\"remember_position_of_live_videos\">Emlékezzen az élő adás pozíciójára</string>\r\n  <string name=\"dearrow_status\">DeArrow állapotának ellenőrzése</string>\r\n  <string name=\"hide_watched_from_notifications\">Megnézett videók elrejtése az Értesítésekből</string>\r\n  <string name=\"header_sports\">Sport</string>\r\n  <string name=\"keep_finished_activities\">Befejezett tevékenységek megőrzése</string>\r\n  <string name=\"disable_channels_service\">Csatornák szolgáltatás letiltása</string>\r\n  <string name=\"replace_titles\">Címek lecserélése</string>\r\n  <string name=\"enable\">Engedélyez</string>\r\n  <string name=\"more_info\">További infó</string>\r\n  <string name=\"about_sponsorblock\">A SponsorBlock-ról</string>\r\n  <string name=\"about_dearrow\">A deArrow-ról</string>\r\n  <string name=\"replace_thumbnails\">Bélyegképek lecserélése</string>\r\n  <string name=\"crowdsourced_thumbnails\">Közösségi alapú bélyegképek</string>\r\n  <string name=\"crowdsoursed_titles\">közösségi alapú címek</string>\r\n  <string name=\"dearrow_not_submitted_thumbs\">A nem feliratkozott előnézeti képek forrása</string>\r\n  <string name=\"pitch_effect\">Hangmagasság effekt</string>\r\n  <string name=\"player_exit_shortcut\">Kilépés a lejátszóból</string>\r\n  <string name=\"skip_shorts\">Rövid klipek kihagyása</string>\r\n  <string name=\"speech_engine\">Hang keresőmotor</string>\r\n  <string name=\"player_extra_long_speed_list\">Extra hosszú sebességlista</string>\r\n  <string name=\"old_channel_look\">A csatornalista régi megjelenése</string>\r\n  <string name=\"remember_position_pinned\">Emlékezzen az utoljára megtekintett pozícióra a rögzített lejátszási listákon</string>\r\n  <string name=\"unknown_source_error\">Ismeretlen forrás hiba</string>\r\n  <string name=\"unknown_renderer_error\">Ismeretlen renderelő hiba</string>\r\n  <string name=\"hide_watched_from_watch_later\">A megtekintett videók elrejtése a Megnézendő videók lejátszási listáról</string>\r\n  <string name=\"player_quick_skip_videos\">A normál videók kihagyása a bal/jobb gombokkal</string>\r\n  <string name=\"fullscreen_mode\">Teljes képernyős mód (rendszersávok nélkül)</string>\r\n  <string name=\"player_only_mode\">Csak a lejátszó megjelenítése, ha a videót az alkalmazáson kívül nyitották meg</string>\r\n  <string name=\"pinned_channel_rows\">A rögzített csatorna megjelenítése soronként</string>\r\n  <string name=\"prefer_ipv4\">IPv4 DNS előnyben részesítése</string>\r\n  <string name=\"prefer_ipv4_desc\">Kijavíthatja azokat a helyzeteket, amikor az alkalmazás egyáltalán nem működik.\\nMegjegyzés. Lefagyást és összeomlást okozhat (különösen Android 8 vagy Dune HD eszközökön)</string>\r\n  <string name=\"long_press_for_settings\">HOSSZAN MEGNYOMVA: Beállítások</string>\r\n  <string name=\"long_press_for_options\">HOSSZAN MEGNYOMVA: Opciók</string>\r\n  <string name=\"device_specific_backup\">Biztonsági mentés csak ehhez az eszközhöz</string>\r\n  <string name=\"local_backup\">Helyi biztonsági mentés</string>\r\n  <string name=\"auto_backup\">Automatikus biztonsági mentés (naponta egyszer)</string>\r\n  <string name=\"repeat_mode_reverse_list\">Játssza le a lejátszási listát vagy a csatornavideókat fordított sorrendben</string>\r\n  <string name=\"unpin_group_from_sidebar\">A csoport eltávolítása</string>\r\n  <string name=\"calm_msg\">Rendbe hozzuk a problémát. Időnként ellenőrizze a frissítéseket</string>\r\n  <string name=\"without_picture\">Kép nélkül</string>\r\n  <string name=\"video_disabled\">Videó letiltva</string>\r\n  <string name=\"applying_fix\">Ne lépjen ki. A javítás alkalmazása...</string>\r\n  <string name=\"hide_mixes\">Keverékek elrejtése</string>\r\n  <string name=\"player_global_focus_desc\">Ez a funkció befolyásolja, hogy melyik lejátszó gomb kap fókuszt a lejátszó gombsorai közötti navigálás során</string>\r\n  <string name=\"disable_network_error_fixing\">Az automatikus hálózati hibajavítás letiltása</string>\r\n  <string name=\"disable_network_error_fixing_desc\">Valószínűleg engedélyeznie kell ezt az opciót, ha VPN-t használ</string>\r\n  <string name=\"recommended\">Ajánlott</string>\r\n  <string name=\"add_to_subscriptions_group\">Csatorna hozzáadása/eltávolítása a csoportokhoz</string>\r\n  <string name=\"new_subscriptions_group\">Új csoport</string>\r\n  <string name=\"rename_group\">A csoport átnevezése</string>\r\n  <string name=\"screen_dimming_amount\">Képernyő elsötétítése</string>\r\n  <string name=\"screen_dimming_timeout\">Képernyő elsötétítési ideje</string>\r\n  <string name=\"playlists_rows\">Lejátszási listák szakasz megjelenítése sorokként</string>\r\n  <string name=\"not_supported_by_device\">A készülék nem támogatja ezt a funkciót</string>\r\n  <string name=\"video_buffer_size_lowest\">Legalacsonyabb</string>\r\n  <string name=\"video_buffer_size_highest\">Legmagasabb</string>\r\n  <string name=\"suggestions\">Javaslatok</string>\r\n  <string name=\"place_comments_left\">Helyezze a megjegyzéseket balra</string>\r\n  <string name=\"search_exit_shortcut\">Kilépés a keresésből</string>\r\n  <string name=\"context_menu_sorting\">Helyi menü rendezése</string>\r\n  <string name=\"hide_shorts_from_search\">Rövid klipek elrejtése a keresési eredményekből</string>\r\n  <string name=\"video_flip\">Tükrözés</string>\r\n  <string name=\"import_subscriptions_group\">Importálás</string>\r\n  <string name=\"lb_playback_controls_closed_captioning_disable\">Feliratozás letiltása</string>\r\n  <string name=\"lb_playback_controls_closed_captioning_enable\">Feliratozás engedélyezése</string>\r\n  <string name=\"my_videos\">Saját videók</string>\r\n  <string name=\"player_audio_focus\">Hangfókusz (szünet, ha más lejátszók észlelték)</string>\r\n  <string name=\"paid_content_notification\">Fizetett tartalom értesítés</string>\r\n  <string name=\"you_liked\">Kedvelt</string>\r\n  <string name=\"premium_users_only\">Csak prémium felhasználók. Javítás a hiányos videó formátum listához</string>\r\n  <string name=\"playback_buffering_fix\">Lejátszás pufferelés javítása</string>\r\n  <string name=\"card_preview_muted\">Videó némítva</string>\r\n  <string name=\"card_preview_full\">Videó hanggal</string>\r\n  <string name=\"card_preview\">Előnézet</string>\r\n  <string name=\"card_unlocalized_titles\">Nem lokalizált videócímek</string>\r\n  <string name=\"oculus_quest_fix\">Oculus Quest javítása</string>\r\n  <string name=\"original_lang\">Eredeti</string>\r\n  <string name=\"remove_playlist_fmt\">%s végleges eltávolítása\\?</string>\r\n  <string name=\"play_from_start\">Lejátszás az elejétől</string>\r\n  <string name=\"player_chapter_notification2\">Váltson a következő fejezetre az értesítésre kattintva</string>\r\n  <string name=\"dont_resize_video_to_fit_dialog\">Ne méretezze át a videót </string>\r\n  <string name=\"typing_corrections\">Az automatikus javítás letiltása szöveg beírása közben</string>\r\n  <string name=\"suggestions_horizontally_scrolled\">Vízszintesen görgetett Javaslatok</string>\r\n  <string name=\"stable_restore\">Minden beállítás törlése!</string>\r\n</resources>\r\n"
  },
  {
    "path": "common/src/main/res/values-hy/strings.xml",
    "content": "<resources>\n    <string name=\"header_home\">Գլխավոր էջ</string>\n    <string name=\"title_search\">Որոնում</string>\n    <string name=\"header_subscriptions\">Բաժանորդագրություններ</string>\n    <string name=\"header_history\">Պատմություն</string>\n    <string name=\"header_music\">Երաժշտություն</string>\n    <string name=\"header_news\">Նորություններ</string>\n    <string name=\"header_gaming\">Խաղեր</string>\n    <string name=\"header_playlists\">Երգացանկեր</string>\n    <string name=\"header_settings\">Կարգավորումներ</string>\n    <string name=\"header_channels\">Ալիքներ</string>\n    <string name=\"subscriptions_signin_title\">Դիտեք ձեր բաժանորդագրված ալիքների թարմացումները</string>\n    <string name=\"subscriptions_signin_subtitle\">Մուտք գործեք` ձեր բաժանորդագրությունները տեսնելու համար</string>\n    <string name=\"subscriptions_signin_button_text\">ՄՈՒՏՔ ԳՈՐԾԵԼ</string>\n    <string name=\"library_signin_to_show_more\">Դիտեք ձեզ դուր եկած, պահպանված կամ բաժանորդագրությունների տեսանյութերը</string>\n    <string name=\"library_signin_subtitle\">Մուտք գործեք՝ տեսնելու համար</string>\n    <string name=\"action_signin\">ՄՈՒՏՔ ԳՈՐԾԵԼ</string>\n    <string name=\"title_video_formats\">Վիդեոֆորմատ</string>\n    <string name=\"title_audio_formats\">Աուդիոֆորմատ</string>\n    <string name=\"subtitle_category_title\">Ենթագրեր</string>\n    <string name=\"video_max_quality\">Ավտոմատ (առավելագույն որակ)</string>\n    <string name=\"audio_max_quality\">Ավտոմատ (առավելագույն որակ)</string>\n    <string name=\"subtitles_disabled\">Ենթագրերն անջատված են</string>\n    <string name=\"auto_frame_rate\">Ավտոֆրեյմրեյթ</string>\n    <string name=\"frame_rate_correction\">Ուղղել + fps:\\n%s</string>\n    <string name=\"category_background_playback\">Նվագարկում ետին պլանում</string>\n    <string name=\"not_implemented\">Իրականացված չէ</string>\n    <string name=\"option_background_playback_off\">Անջատված է</string>\n    <string name=\"option_background_playback_pip\">Նկարը նկարում</string>\n    <string name=\"option_background_playback_only_audio\">Միայն աուդիո</string>\n    <string name=\"playback_settings\">Նվագարկման կարգավորումներ</string>\n    <string name=\"video_speed\">Տեսանյութի արագությունը</string>\n    <string name=\"resolution_switch\">Փոխել կետայնությունը</string>\n    <string name=\"video_buffer\">Տեսանյութի բուֆեր</string>\n    <string name=\"video_buffer_size_low\">Ցածր</string>\n    <string name=\"video_buffer_size_med\">Միջին</string>\n    <string name=\"video_buffer_size_high\">Բարձր</string>\n    <string name=\"update_changelog\">Փոփոխությունների ցանկ</string>\n    <string name=\"install_update\">Տեղադրեք թարմացումը</string>\n    <string name=\"section_is_empty\">Այստեղ ոչինչ չկա:</string>\n    <string name=\"dialog_add_to_playlist\">Ավելացնել երգացանկին</string>\n    <string name=\"msg_signed_users_only\">Անհրաժեշտ է մուտք գործել</string>\n    <string name=\"msg_cant_load_content\">Հնարավոր չէ բեռնել բովանդակությունը:</string>\n    <string name=\"title_video_presets\">Տեսանյութի պրոֆիլներ</string>\n    <string name=\"settings_accounts\">Հաշիվներ</string>\n    <string name=\"settings_left_panel\">Խմբագրել կատեգորիաները</string>\n    <string name=\"settings_themes\">Թեմաներ</string>\n    <string name=\"settings_other\">Այլ</string>\n    <string name=\"settings_player\">Նվագարկիչ</string>\n    <string name=\"settings_language\">Լեզու</string>\n    <string name=\"settings_linked_devices\">Կապված սարքեր</string>\n    <string name=\"settings_about\">Ծրագրի մասին</string>\n    <string name=\"dialog_account_list\">Ընտրեք հաշիվը</string>\n    <string name=\"dialog_account_none\">Ընտրված չէ</string>\n    <string name=\"dialog_remove_account\">Հեռացնել հաշիվը</string>\n    <string name=\"dialog_add_account\">Ավելացնել հաշիվ</string>\n    <string name=\"default_lang\">Լռելյայն</string>\n    <string name=\"dialog_select_language\">Ծրագրի լեզուն</string>\n    <string name=\"subtitle_default\">Լռելյայն</string>\n    <string name=\"subtitle_white_semi_transparent\">Կիսաթափանցիկ ֆոն</string>\n    <string name=\"subtitle_style\">Ենթագրերի ոճեր</string>\n    <string name=\"subtitle_language\">Ենթագրերի լեզու</string>\n    <string name=\"action_search\">Սկսեք որոնումը</string>\n    <string name=\"settings_main_ui\">Օգտագործողի ինտերֆեյս</string>\n    <string name=\"dialog_main_ui\">Օգտագործողի ինտերֆեյս</string>\n    <string name=\"card_animated_previews\">Անիմացիոն նախադիտում</string>\n    <string name=\"web_site\">Վեբ կայք</string>\n    <string name=\"donation\">Նվիրատվություն</string>\n    <string name=\"dialog_about\">Ծրագրի մասին</string>\n    <string name=\"dialog_player_ui\">Նվագարկիչ</string>\n    <string name=\"player_show_ui_on_pause\">Ցույց տալ ինտերֆեյս դադարի ժամանակ</string>\n    <string name=\"player_pause_on_ok\">OK ստեղնը դադար է տալիս</string>\n    <string name=\"player_ok_button_behavior\">OK կոճակի պահվածքը</string>\n    <string name=\"player_only_ui\">Միայն ինտերֆեսյ</string>\n    <string name=\"player_ui_and_pause\">Ինտերֆեյս և դադար</string>\n    <string name=\"player_only_pause\">Միայն դադար</string>\n    <string name=\"check_for_updates\">Ստուգել թարմացումները</string>\n    <string name=\"update_not_found\">Դուք օգտագործում եք վերջին տարբերակը</string>\n    <string name=\"update_in_progress\">Սպասեք</string>\n    <string name=\"player_ui_hide_behavior\">Թաքցնել ինտերֆեյսը</string>\n    <string name=\"option_never\">Երբեք</string>\n    <string name=\"side_panel_sections\">Կարգավորել բաժինները</string>\n    <string name=\"boot_to_section\">Բեռնվել նշված բաժնում</string>\n    <string name=\"large_ui\">Խոշոր ինտերֆեյս</string>\n    <string name=\"video_grid_scale\">Տեսանյութերի ցանցի մասշտաբ</string>\n    <string name=\"scale_ui\">Ինտերֆեյսի մասշտաբ</string>\n    <string name=\"color_scheme\">Գունային սխեման</string>\n    <string name=\"color_scheme_default\">Լռելյայն</string>\n    <string name=\"color_scheme_red_grey\">Կարմիր մոխրագույն</string>\n    <string name=\"color_scheme_red\">Կարմիր</string>\n    <string name=\"color_scheme_dark_grey\">Մուգ մոխրագույն</string>\n    <string name=\"disable_update_check\">Անջատել թարմացումների ստուգումը</string>\n    <string name=\"show_again\">Նորից ցուցադրել</string>\n    <string name=\"check_updates_auto\">Ավտոմատ թարմացում</string>\n    <string name=\"select_account_on_boot\">Ընտրեք բեռնման ժամանակ</string>\n    <string name=\"player_other\">Տարբեր</string>\n    <string name=\"player_full_date\">Ճշգրիտ ամսաթիվը նկարագրության մեջ</string>\n    <string name=\"open_channel\">Բացել ալիքը</string>\n    <string name=\"not_interested\">Չի հետաքրքրում</string>\n    <string name=\"you_wont_see_this_video\">Տեսանյութը հանվել է առաջարկվածից</string>\n    <string name=\"settings_search\">Որոնում</string>\n    <string name=\"dialog_search\">Որոնում</string>\n    <string name=\"instant_voice_search\">Ակնթարթային ձայնային որոնում</string>\n    <string name=\"option_background_playback_behind\">Խաղալ ետեւում</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">Հաջորդ տեսանյութի տեղեկությունները դեռ բեռնված չեն</string>\n    <string name=\"card_multiline_title\">Մի քանի տող վերնագրերում</string>\n    <string name=\"cards_style\">Քարտերի ոճը</string>\n    <string name=\"repeat_mode_all\">Նվագել բոլոր տեսանյութերը մեկ առ մեկ</string>\n    <string name=\"repeat_mode_one\">Կրկնել ընթացիկ տեսանյութը</string>\n    <string name=\"repeat_mode_pause\">Դադարեցնել նվագարկումը յուրաքանչյուր տեսանյութից հետո</string>\n    <string name=\"repeat_mode_none\">Դադարեցնել նվագարկումը նվագացանկից դուրս յուրաքանչյուր տեսանյութից հետո</string>\n    <string name=\"subscribed_to_channel\">Բաժանորդագրությունը կատարված է</string>\n    <string name=\"unsubscribed_from_channel\">Բաժանորդագրությունը հեռացված է</string>\n    <string name=\"subtitle_yellow_transparent\">Դեղին</string>\n    <string name=\"subtitle_white_black\">Սև ֆոն</string>\n    <string name=\"player_seek_preview\">Որոնելիս ցույց տալ նախադիտումը</string>\n    <string name=\"color_scheme_dark_grey_oled\">Մուգ մոխրագույն (OLED)</string>\n    <string name=\"channels_section_sorting\">Ալիքի բաժնի դասավորությունը</string>\n    <string name=\"sorting_by_new_content\">Նոր կոնտենտ</string>\n    <string name=\"sorting_alphabetically\">Այբբենական կարգով</string>\n    <string name=\"sorting_last_viewed\">Վերջին դիտվածը</string>\n    <string name=\"player_pause_when_seek\">Դադար տալ որոնելիս</string>\n    <string name=\"playlists_style\">Երգացանկերի ոճը</string>\n    <string name=\"playlists_style_grid\">Ցանց</string>\n    <string name=\"playlists_style_rows\">Շարքեր</string>\n    <string name=\"player_show_clock\">Ցուցադրել ժամը</string>\n    <string name=\"player_show_remaining_time\">Ցույց տալ մնացած ժամանակը</string>\n    <string name=\"open_channel_uploads\">Բացել ալիքի վերբեռնումները</string>\n    <string name=\"app_exit_shortcut\">Ելք հավելվածից</string>\n    <string name=\"app_exit_none\">Դուրս չգա՛լ</string>\n    <string name=\"app_double_back_exit\">Երկու անգամ հետ կոճակը սեղմելով</string>\n    <string name=\"app_single_back_exit\">Մեկ անգամ հետ կոճակը սեղմելով</string>\n    <string name=\"action_video_zoom\">Տեսանյութի մեծացում</string>\n    <string name=\"video_zoom\">Տեսանյութի մեծացում</string>\n    <string name=\"video_zoom_default\">Լռելյայն</string>\n    <string name=\"video_zoom_fit_width\">Տեղավորել լայնությունը</string>\n    <string name=\"video_zoom_fit_height\">Տեղավորել բարձրությունը</string>\n    <string name=\"video_zoom_fit_both\">Տեղավորեք կամ լայնությունը կամ բարձրությունը</string>\n    <string name=\"video_zoom_stretch\">Ձգել</string>\n    <string name=\"color_scheme_teal\">Փիրուզագույն</string>\n    <string name=\"color_scheme_teal_oled\">Փիրուզագույն (OLED)</string>\n    <string name=\"player_seek_preview_none\">Անջատված է</string>\n    <string name=\"player_seek_preview_single\">Մեկ կադր</string>\n    <string name=\"player_seek_preview_carousel\">Կարուսել</string>\n    <string name=\"player_seek_preview_carousel_slow\">Կարուսել (դանդաղ)</string>\n    <string name=\"player_seek_preview_carousel_fast\">Կարուսել (արագ)</string>\n    <string name=\"unsubscribe_from_channel\">Ապաբաժանորդագրվել ալիքից</string>\n</resources>"
  },
  {
    "path": "common/src/main/res/values-in/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <string name=\"header_home\">Beranda</string>\r\n    <string name=\"title_search\">Pencarian</string>\r\n    <string name=\"header_subscriptions\">Langganan</string>\r\n    <string name=\"header_history\">Riwayat</string>\r\n    <string name=\"header_music\">Musik</string>\r\n    <string name=\"header_news\">Berita</string>\r\n    <string name=\"header_gaming\">Permainan</string>\r\n    <string name=\"header_playlists\">Daftar Putar</string>\r\n    <string name=\"header_settings\">Pengaturan</string>\r\n    <string name=\"header_channels\">Channel</string>\r\n    <string name=\"subscriptions_signin_title\">Lihat yang terbaru dari channel yang kamu sukai</string>\r\n    <string name=\"subscriptions_signin_subtitle\">Masuk untuk melihat langganan kamu</string>\r\n    <string name=\"subscriptions_signin_button_text\">MASUK</string>\r\n    <string name=\"library_signin_to_show_more\">Tonton vidio yang kamu suka, simpan, atau jadikan langganan</string>\r\n    <string name=\"library_signin_subtitle\">Masuk untuk melihat perpustakaan kamu</string>\r\n    <string name=\"action_signin\">MASUK</string>\r\n    <string name=\"title_video_formats\">Format vidio</string>\r\n    <string name=\"title_audio_formats\">Format suara</string>\r\n    <string name=\"subtitle_category_title\">Subtitle</string>\r\n    <string name=\"playback_queue_category_title\">Antrian pemutaran</string>\r\n    <string name=\"video_max_quality\">Otomatis (kualitas maks)</string>\r\n    <string name=\"audio_max_quality\">Otomatis (kualitas maks)</string>\r\n    <string name=\"subtitles_disabled\">Subtitel dinonaktifkan</string>\r\n    <string name=\"auto_frame_rate\">Kecepatan bingkai otomatis</string>\r\n    <string name=\"frame_rate_correction\">Perbaiki fps:\\n%s</string>\r\n    <string name=\"category_background_playback\">Pemutaran latar belakang</string>\r\n    <string name=\"not_implemented\">Tidak diimplementasikan</string>\r\n    <string name=\"not_supported_by_device\">Perangkat tidak mendukung fitur ini</string>\r\n    <string name=\"option_background_playback_off\">Dimatikan</string>\r\n    <string name=\"option_background_playback_pip\">Gambar di dalam gambar</string>\r\n    <string name=\"option_background_playback_only_audio\">Hanya suara</string>\r\n    <string name=\"playback_settings\">Pengaturan pemutaran</string>\r\n    <string name=\"video_speed\">Kecepatan vidio</string>\r\n    <string name=\"resolution_switch\">Ganti resolusi</string>\r\n    <string name=\"video_buffer\">Buffer vidio</string>\r\n    <string name=\"video_buffer_size_none\">Tidak ada</string>\r\n    <string name=\"video_buffer_size_lowest\">Paling Rendah</string>\r\n    <string name=\"video_buffer_size_low\">Rendah</string>\r\n    <string name=\"video_buffer_size_med\">Sedang</string>\r\n    <string name=\"video_buffer_size_high\">Tinggi</string>\r\n    <string name=\"video_buffer_size_highest\">Tertinggi</string>\r\n    <string name=\"update_changelog\">Catatan perubahan</string>\r\n    <string name=\"install_update\">Memasang pembaharuan</string>\r\n    <string name=\"section_is_empty\">Ups! Tidak ada apa-apa di sini.</string>\r\n    <string name=\"dialog_add_to_playlist\">Tambahkan ke Daftar Putar</string>\r\n    <string name=\"msg_signed_users_only\">Hanya pengguna yang terdaftar</string>\r\n    <string name=\"msg_cant_load_content\">Tidak dapat memuat konten.\\n1) Aktifkan riwayat tontonan.\\n2) Periksa tanggal dan waktu pada perangkat.\\n3) Periksa koneksi jaringan.\\n4) Periksa pengaturan proxy kamu.\\n5) Coba masuk ke akun kamu.\\n6) Mungkin tidak ada apa pun di sini.</string>\r\n    <string name=\"title_video_presets\">Prasetel vidio</string>\r\n    <string name=\"settings_accounts\">Akun</string>\r\n    <string name=\"settings_left_panel\">Edit kategori</string>\r\n    <string name=\"settings_themes\">Tema</string>\r\n    <string name=\"settings_other\">Lain</string>\r\n    <string name=\"settings_player\">Pemutar</string>\r\n    <string name=\"settings_language\">Bahasa</string>\r\n    <string name=\"settings_linked_devices\">Perangkat yang ditautkan</string>\r\n    <string name=\"settings_about\">Tentang</string>\r\n    <string name=\"dialog_account_list\">Pilih akun</string>\r\n    <string name=\"dialog_account_none\">Tidak ada</string>\r\n    <string name=\"dialog_remove_account\">Menghapus akun</string>\r\n    <string name=\"dialog_add_account\">Menambahkan akun</string>\r\n    <string name=\"default_lang\">Bawaan</string>\r\n    <string name=\"dialog_select_language\">Bahasa aplikasi</string>\r\n    <string name=\"subtitle_default\">Bawaan</string>\r\n    <string name=\"subtitle_white_semi_transparent\">Latar belakang semitransparan</string>\r\n    <string name=\"subtitle_style\">Gaya subtitle</string>\r\n    <string name=\"subtitle_language\">Bahasa subtitle</string>\r\n    <string name=\"action_search\">Mulai mencari</string>\r\n    <string name=\"settings_main_ui\">Antarmuka pengguna</string>\r\n    <string name=\"dialog_main_ui\">Antarmuka pengguna</string>\r\n    <string name=\"card_animated_previews\">Pratinjau animasi</string>\r\n    <string name=\"web_site\">Situs web</string>\r\n    <string name=\"donation\">Donasi</string>\r\n    <string name=\"dialog_about\">Tentang</string>\r\n    <string name=\"dialog_player_ui\">Pemutar vidio</string>\r\n    <string name=\"player_show_ui_on_pause\">Tampilkan UI saat jeda</string>\r\n    <string name=\"player_pause_on_ok\">Tombol OK menjeda pemutaran</string>\r\n    <string name=\"player_ok_button_behavior\">Perilaku tombol OK</string>\r\n    <string name=\"player_only_ui\">Hanya UI</string>\r\n    <string name=\"player_ui_and_pause\">UI dan jeda</string>\r\n    <string name=\"player_only_pause\">Jeda saja</string>\r\n    <string name=\"check_for_updates\">Periksa pembaruan</string>\r\n    <string name=\"update_not_found\">Kamu menggunakan versi terbaru</string>\r\n    <string name=\"update_in_progress\">Harap tunggu sebentar…</string>\r\n    <string name=\"player_ui_hide_behavior\">Sembunyikan UI otomatis</string>\r\n    <string name=\"option_never\">Tidak pernah</string>\r\n    <string name=\"side_panel_sections\">Bagian bilah sisi</string>\r\n    <string name=\"boot_to_section\">Boot ke bagian</string>\r\n    <string name=\"large_ui\">UI besar</string>\r\n    <string name=\"video_grid_scale\">Skala kisi vidio</string>\r\n    <string name=\"scale_ui\">Skala UI</string>\r\n    <string name=\"color_scheme\">Skema warna</string>    \r\n    <string name=\"color_scheme_red_grey\">Merah-Abu2</string>\r\n    <string name=\"color_scheme_red\">Merah</string>\r\n    <string name=\"color_scheme_dark_grey\">Abu-abu gelap</string>\r\n    <string name=\"disable_update_check\">Nonaktifkan pemeriksaan pembaruan</string>\r\n    <string name=\"show_again\">Tunjukkan lagi</string>\r\n    <string name=\"check_updates_auto\">Beritahukan tentang pembaruan</string>\r\n    <string name=\"select_account_on_boot\">Tampilkan pilihan akun saat boot</string>\r\n    <string name=\"player_other\">Lainnya</string>\r\n    <string name=\"player_full_date\">Tanggal yang tepat dalam deskripsi</string>\r\n    <string name=\"open_channel\">Buka channel</string>\r\n    <string name=\"open_playlist\">Buka daftar putar</string>\r\n    <string name=\"not_interested\">Tidak tertarik</string>\r\n    <string name=\"not_recommend_channel\">Jangan rekomendasikan channel</string>\r\n    <string name=\"you_wont_see_this_video\">Vidio telah dihapus dari yang direkomendasikan</string>\r\n    <string name=\"you_wont_see_this_channel\">Kamu tidak akan melihat channel ini dalam rekomendasi</string>\r\n    <string name=\"settings_search\">Pencarian</string>\r\n    <string name=\"dialog_search\">Cari</string>\r\n    <string name=\"instant_voice_search\">Pencarian suara instan</string>\r\n    <string name=\"option_background_playback_behind\">Mainkan di belakang</string>\r\n    <string name=\"next_video_info_is_not_loaded_yet\">Info vidio berikutnya belum dimuat</string>\r\n    <string name=\"card_multiline_title\">Judul multiline</string>\r\n    <string name=\"cards_style\">Gaya kartu</string>\r\n    <string name=\"repeat_mode_all\">Putar semua vidio satu per satu</string>\r\n    <string name=\"repeat_mode_one\">Ulangi vidio saat ini</string>\r\n    <string name=\"repeat_mode_pause\">Jeda pemutaran setelah setiap vidio</string>\r\n    <string name=\"repeat_mode_pause_alt\">Jeda pemutaran setelah setiap vidio yang bukan daftar putar</string>\r\n    <string name=\"repeat_mode_none\">Hentikan pemutaran setelah satu vidio</string>\r\n    <string name=\"subscribed_to_channel\">Berlangganan ke channel</string>\r\n    <string name=\"unsubscribed_from_channel\">Berhenti berlangganan dari channel</string>\r\n    <string name=\"subtitle_yellow_transparent\">Kuning latar belakang transparan</string>\r\n    <string name=\"subtitle_white_transparent\">Putih latar belakang transparan</string>\r\n    <string name=\"subtitle_white_black\">Latar belakang hitam</string>\r\n    <string name=\"player_seek_preview\">Pratinjau sambil mencari</string>\r\n    <string name=\"color_scheme_dark_grey_oled\">Abu-abu Tua (OLED)</string>\r\n    <string name=\"channels_section_sorting\">Penyortiran bagian channel</string>\r\n    <string name=\"sorting_by_new_content\">Konten baru</string>\r\n    <string name=\"sorting_alphabetically\">Menurut abjad</string>\r\n    <string name=\"sorting_last_viewed\">Terakhir dilihat</string>\r\n    <string name=\"player_pause_when_seek\">Jeda saat mencari</string>\r\n    <string name=\"playlists_style\">Gaya playlist</string>\r\n    <string name=\"playlists_style_grid\">Kotak</string>\r\n    <string name=\"playlists_style_rows\">Baris</string>\r\n    <string name=\"player_show_clock\">Tunjukkan jam</string>\r\n    <string name=\"player_show_remaining_time\">Tunjukkan waktu yang tersisa</string>\r\n    <string name=\"player_show_ending_time\">Tampilkan waktu berakhir</string>\r\n    <string name=\"open_channel_uploads\">Buka upload channel</string>\r\n    <string name=\"app_exit_shortcut\">Keluar dari aplikasi</string>\r\n    <string name=\"player_exit_shortcut\">Keluar dari pemutar</string>\r\n    <string name=\"search_exit_shortcut\">Keluar dari pencarian</string>\r\n    <string name=\"app_exit_none\">Jangan keluar</string>\r\n    <string name=\"app_double_back_exit\">Tekan BACK 2X </string>\r\n    <string name=\"app_single_back_exit\">Tekan BACK sekali</string>\r\n    <string name=\"action_video_zoom\">Perbesaran vidio</string>\r\n    <string name=\"video_zoom\">Perbesaran vidio</string>\r\n    <string name=\"video_zoom_default\">Bawaan</string>\r\n    <string name=\"video_zoom_fit_width\">Sesuaikan lebar</string>\r\n    <string name=\"video_zoom_fit_height\">Tinggi pas</string>\r\n    <string name=\"video_zoom_fit_both\">Sesuai dengan lebar atau tinggi</string>\r\n    <string name=\"video_zoom_stretch\">Meregang</string>\r\n    <string name=\"color_scheme_teal\">Hijau Kebiruan</string>\r\n    <string name=\"color_scheme_teal_oled\">Hijau Kebiruan (OLED)</string>\r\n    <string name=\"player_seek_preview_none\">Tanpa pratinjau</string>\r\n    <string name=\"player_seek_preview_single\">Bingkai tunggal</string>\r\n    <string name=\"player_seek_preview_carousel\">Karosel</string>\r\n    <string name=\"player_seek_preview_carousel_slow\">Karosel (lambat)</string>\r\n    <string name=\"player_seek_preview_carousel_fast\">Karosel (cepat)</string>\r\n    <string name=\"unsubscribe_from_channel\">Berhenti berlangganan dari channel</string>\r\n    <string name=\"card_title_lines_num\">Nomor baris judul kartu</string>\r\n    <string name=\"card_auto_scrolled_title\">Gulir otomatis judul yang dipangkas</string>\r\n    <string name=\"share_link\">Membagikan tautan</string>\r\n    <string name=\"share_qr_link\">Membagikan tautan (QR code)</string>\r\n    <string name=\"subscribe_to_channel\">Berlangganan channel</string>\r\n    <string name=\"wait_data_loading\">Harap tunggu sementara data sedang dimuat…</string>\r\n    <string name=\"auto_frame_rate_pause\">Jeda kecepatan bingkai otomatis</string>\r\n    <string name=\"auto_frame_rate_applying\">Menerapkan frekuensi gambar otomatis %sx%s\\@%s</string>\r\n    <string name=\"audio_shift\">Pergeseran suara</string>\r\n    <string name=\"player_remember_speed\">Ingat kecepatan</string>\r\n    <string name=\"mark_channel_as_watched\">Tandai sebagai telah diawasi</string>\r\n    <string name=\"channel_marked_as_watched\">Channel telah ditandai sebagai ditonton</string>\r\n    <string name=\"dialog_add_device\">Tambahkan perangkat</string>\r\n    <string name=\"dialog_remove_all_devices\">Hapus perangkat</string>\r\n    <string name=\"device_link_enabled\">Tautan diaktifkan</string>\r\n    <string name=\"device_connected\">Perangkat \\\"%s\\\" telah terhubung</string>\r\n    <string name=\"device_disconnected\">Perangkat \\\"%s\\\" telah diputuskan</string>\r\n    <string name=\"settings_ui_scale\">Skala UI</string>\r\n    <string name=\"msg_restart_app\">Silakan mulai ulang aplikasi untuk menerapkan pengaturan ini</string>\r\n    <string name=\"settings_block\">Pemblokiran konten</string>\r\n    <string name=\"msg_applying\">Menerapkan %s…</string>\r\n    <string name=\"msg_done\">Selesai</string>\r\n    <string name=\"settings_general\">Umum</string>\r\n    <string name=\"settings_video\">Vidio</string>\r\n    <string name=\"content_block_confirm_skip\">Konfirmasi saat melewati</string>\r\n    <string name=\"content_block_categories\">Kategori</string>    \r\n    <string name=\"content_block_intro\">Animasi jeda/pembuka</string>\r\n    <string name=\"content_block_outro\">Kartu akhir/credits</string>\r\n    <string name=\"content_block_interaction\">Pengingat interaksi (berlangganan)</string>\r\n    <string name=\"content_block_self_promo\">Promosi tidak dibayar/diri sendiri</string>\r\n    <string name=\"content_block_music_off_topic\">Bagian non-musik dari klip</string>\r\n    <string name=\"confirm_segment_skip\">Lewati segmen \\\"%s\\\"\\?</string>\r\n    <string name=\"cancel_segment_skip\">Batalkan melewatkan segmen</string>\r\n    <string name=\"msg_skipping_segment\">Melewati segmen \\\"%s\\\"…</string>\r\n    <string name=\"content_block_notification_type\">Jenis pemberitahuan</string>\r\n    <string name=\"content_block_notify_none\">Tanpa pemberitahuan</string>\r\n    <string name=\"content_block_notify_toast\">Pop Up</string>\r\n    <string name=\"content_block_notify_dialog\">Dialog konfirmasi</string>\r\n    <string name=\"return_to_launcher\">Kembali ke Launcher dari channel/pencarian ATV</string>\r\n    <string name=\"intent_force_close\">Keluar paksa jika vidio telah dibuka dari sumber eksternal </string>\r\n    <string name=\"btn_confirm\">Konfirmasi</string>\r\n    <string name=\"player_low_video_quality\">Kualitas vidio rendah</string>\r\n    <string name=\"remote_session_closed\">Sesi jarak jauh telah ditutup</string>\r\n    <string name=\"msg_mode_switch_error\">Tidak dapat mengganti mode tampilan ke \\\"%s\\\"</string>\r\n    <string name=\"msg_player_error\">Terjadi kesalahan player %s. Memulai ulang pemutaran…</string>\r\n    <string name=\"video_preset_disabled\">Tanpa prasetel</string>\r\n    <string name=\"video_preset_enabled\">Format vidio selanjutnya akan diatur sesuai prasetel</string>\r\n    <string name=\"player_sleep_timer\">Pengatur waktu tidur</string>\r\n    <string name=\"player_show_quality_info\">Tampilkan info kualitas vidio</string>\r\n    <string name=\"player_remember_each_speed\">Ingat setiap kecepatan vidio</string>\r\n    <string name=\"player_remember_speed_none\">Tidak ada</string>\r\n    <string name=\"player_remember_speed_all\">Sama di semua vidio</string>\r\n    <string name=\"player_remember_speed_each\">Per setiap vidio</string>\r\n    <string name=\"msg_player_error_source\">Tidak dapat mengunduh vidio</string>\r\n    <string name=\"msg_player_error_renderer\">Format yang dipilih tidak didukung. Coba pilih format lain. Jika ini tidak membantu, coba nyalakan ulang perangkat.</string>\r\n    <string name=\"msg_player_error_video_renderer\">Format VIDIO yang dipilih tidak didukung.\\nCoba pilih format lain dengan menekan tombol HQ pada pemutar.\\nJika ini tidak membantu, coba hidupkan ulang perangkat.</string>\r\n    <string name=\"msg_player_error_audio_renderer\">Format AUDIO yang dipilih tidak didukung.\\nCoba pilih format lain dengan menekan tombol HQ pada pemutar.\\nJika hal ini tidak membantu, cobalah untuk menghidupkan ulang perangkat.</string>\r\n    <string name=\"msg_player_error_subtitle_renderer\">Format SUBTITLE yang dipilih tidak didukung.\\nCoba pilih format lain dengan menekan lama tombol CC pada pemutar.\\nJika cara ini tidak berhasil, cobalah untuk menghidupkan ulang perangkat.</string>\r\n    <string name=\"msg_player_error_unexpected\">Kesalahan dekoder vidio tidak dikenal</string>\r\n    <string name=\"video_aspect\">Rasio aspek</string>\r\n    <string name=\"player_tweaks\">Opsi pengembang</string>\r\n    <string name=\"header_uploads\">Unggahan</string>\r\n    <string name=\"player_show_global_clock\">Tampilkan jam di layar terus-menerus</string>\r\n    <string name=\"player_show_global_ending_time\">Tampilkan waktu akhir vidio di layar terus-menerus</string>\r\n    <string name=\"app_corner_clock\">Beranda: jam pojok kanan atas</string>\r\n    <string name=\"player_corner_clock\">Pemutar: jam sudut kanan atas</string>\r\n    <string name=\"player_corner_ending_time\">Pemutar: sudut kanan atas waktu berakhir</string>\r\n    <string name=\"uploads_old_look\">Kembalikan tampilan lama bagian Unggahan</string>\r\n    <string name=\"background_playback_activation\">Pemutaran latar belakang (aktivasi)</string>\r\n    <string name=\"channels_old_look\">Kembalikan tampilan lama bagian channel</string>\r\n    <string name=\"channels_auto_load\">Muat otomatis konten bagian channel</string>\r\n    <string name=\"return_to_background_video\">Kembali ke vidio yang berjalan di latar belakang</string>  \r\n    <string name=\"pin_unpin_from_sidebar\">Sematkan/Lepas sematan dari bilah sisi</string>\r\n    <string name=\"pin_unpin_playlist\">Sematkan/Lepas sematan daftar putar</string>\r\n    <string name=\"pin_unpin_channel\">Sematkan/Lepas sematan channel</string>\r\n    <string name=\"pin_playlist\">Menambahkan daftar putar ke bilah sisi</string>\r\n    <string name=\"pin_channel\">Menambahkan channel ke bilah sisi</string>\r\n    <string name=\"pinned_to_sidebar\">Disematkan ke bilah sisi</string>\r\n    <string name=\"unpin_from_sidebar\">Lepaskan pin dari bilah sisi</string>\r\n    <string name=\"unpinned_from_sidebar\">Lepas pin dari bilah sisi</string>\r\n    <string name=\"dialog_select_country\">Negara</string>\r\n    <string name=\"settings_language_country\">Bahasa/Negara</string>\r\n    <string name=\"share_embed_link\">Bagikan tautan semat</string>\r\n    <string name=\"card_text_scroll_factor\">Kecepatan gulir teks kartu</string>\r\n    <string name=\"preferred_update_source\">Pilih sumber pembaruan</string>\r\n    <string name=\"hide_shorts\">Sembunyikan vidio pendek dari Langganan</string>\r\n    <string name=\"hide_shorts_channel\">Sembunyikan vidio pendek dari Channel</string>\r\n    <string name=\"key_remapping\">Pemetaan ulang tombol</string>\r\n    <string name=\"screen_dimming\">Peredupan layar</string>\r\n    <string name=\"removed_from_playback_queue\">Dihapus dari antrian pemutaran</string>\r\n    <string name=\"added_to_playback_queue\">Ditambahkan ke antrian pemutaran</string>\r\n    <string name=\"add_remove_from_playback_queue\">Tambah/Hapus dari antrian pemutaran</string>\r\n    <string name=\"add_to_playback_queue\">Tambahkan ke antrean pemutaran</string>\r\n    <string name=\"remove_from_playback_queue\">Hapus dari antrean pemutaran</string>\r\n    <string name=\"proxy_enabled\">Proxy diaktifkan</string>\r\n    <string name=\"proxy_disabled\">Proxy dinonaktifkan</string>\r\n    <string name=\"uploads_row_name\">Unggahan</string>\r\n    <string name=\"playlists_row_name\">Daftar putar yang dibuat</string>\r\n    <string name=\"popular_uploads_row_name\">Unggahan populer</string>\r\n    <string name=\"news_row_name\">Berita</string>\r\n    <string name=\"breaking_news_row_name\">Berita terbaru</string>\r\n    <string name=\"covid_news_row_name\">Berita COVID-19</string>\r\n    <string name=\"enable_voice_search\">Aktifkan pencarian suara</string>\r\n    <string name=\"subscribe_unsubscribe_from_channel\">Berlangganan/Berhenti berlangganan dari channel</string>\r\n    <string name=\"app_backup_restore\">Cadangkan/Pulihkan</string>\r\n    <string name=\"app_backup\">Cadangkan data aplikasi</string>\r\n    <string name=\"app_restore\">Memulihkan data aplikasi</string>\r\n    <string name=\"skip_each_segment_once\">Jangan lewati segmen lagi</string>\r\n    <string name=\"disable_ok_long_press\">Nonaktifkan tombol OK tekan lama</string>\r\n    <string name=\"disable_ok_long_press_desc\">Ditujukan untuk pengendali yang bug di mana tombol OK tidak berfungsi dengan baik</string>\r\n    <string name=\"player_time_correction\">Menampilkan waktu dengan koreksi kecepatan</string>\r\n    <string name=\"sponsor_color_markers\">Penanda warna pada bilah kemajuan</string>\r\n    <string name=\"refresh_section\">Refresh bagian</string>\r\n    <string name=\"option_disabled\">Dinonaktifkan</string>\r\n    <string name=\"live_now_row_name\">Siaran langsung sekarang</string>\r\n    <string name=\"run_in_background\">Jalankan di latar belakang</string>\r\n    <string name=\"settings_remote_control\">Pengendali jarak jauh</string>\r\n    <string name=\"background_service_started\">Layanan latar belakang dimulai</string>\r\n    <string name=\"dialog_add_to\">Tambahkan ke %s</string>\r\n    <string name=\"dialog_remove_from\">Hapus dari %s</string>\r\n    <string name=\"added_to\">Ditambahkan ke %s</string>\r\n    <string name=\"removed_from\">Dihapus dari %s</string>\r\n    <string name=\"video_preset_adaptive\">Adaptif</string>\r\n    <string name=\"content_block_preview_recap\">Pratinjau atau rekap vidio</string>\r\n    <string name=\"content_block_highlight\">Titik atau sorot vidio</string>\r\n    <string name=\"removed_from_history\">Vidio telah dihapus dari riwayat</string>\r\n    <string name=\"remove_from_history\">Hapus dari riwayat</string>\r\n    <string name=\"upload_date\">Tanggal unggah</string>\r\n    <string name=\"upload_date_any\">Sepanjang waktu</string>\r\n    <string name=\"upload_date_today\">Hari ini</string>\r\n    <string name=\"upload_date_this_week\">Minggu ini</string>\r\n    <string name=\"upload_date_this_month\">Bulan ini</string>\r\n    <string name=\"upload_date_this_year\">Tahun ini</string>\r\n    <string name=\"double_refresh_rate\">Refresh rate ganda</string>\r\n    <string name=\"upload_date_last_hour\">Jam terakhir</string>\r\n    <string name=\"focus_on_search_results\">Fokus otomatis pada hasil pencarian</string>\r\n    <string name=\"context_menu\">Menu konteks</string>\r\n    <string name=\"context_menu_sorting\">Pengurutan menu konteks</string>\r\n    <string name=\"add_remove_from_recent_playlist\">Tambah/Hapus dari daftar putar terbaru</string>\r\n    <string name=\"hide_settings_section\">Sembunyikan bagian Pengaturan (berbahaya!)</string>    \r\n    <string name=\"auto_frame_rate_sec\">%s detik</string>\r\n    <string name=\"audio_shift_sec\">%s detik</string>\r\n    <string name=\"ui_hide_timeout_sec\">%s detik</string>\r\n    <string name=\"screen_dimming_timeout_min\">%s menit</string>\r\n    <string name=\"mark_all_channels_watched\">Tandai semua channel sebagai telah ditonton</string>\r\n    <string name=\"move_section_up\">Pindahkan bagian ke atas</string>\r\n    <string name=\"move_section_down\">Pindahkan bagian ke bawah</string>\r\n    <string name=\"player_buttons\">Atur tombol pemutar</string>\r\n    <string name=\"action_playlist_add\">Tambahkan ke daftar putar</string>\r\n    <string name=\"action_video_stats\">Statistik vidio</string>\r\n    <string name=\"action_subscribe\">Langganan</string>\r\n    <string name=\"action_channel\">Buka channel</string>    \r\n    <string name=\"action_video_info\">Deskripsi Vidio</string>\r\n    <string name=\"action_screen_off\">Layar mati</string>\r\n    <string name=\"action_sound_off\">Suara mati</string>\r\n    <string name=\"action_playback_queue\">Antrian pemutaran</string>\r\n    <string name=\"action_video_speed\">Kecepatan vidio</string>\r\n    <string name=\"action_subtitles\">Subtitle</string>\r\n    <string name=\"action_like\">Suka</string>\r\n    <string name=\"action_dislike\">Tidak suka</string>\r\n    <string name=\"action_play_pause\">Putar/Jeda</string>\r\n    <string name=\"action_repeat_mode\">Mode pemutaran</string>\r\n    <string name=\"action_next\">Vidio berikutnya</string>\r\n    <string name=\"action_previous\">Vidio sebelumnya</string>\r\n    <string name=\"action_high_quality\">Kualitas vidio</string>\r\n    <string name=\"content_block_no_skipping_mode\">Mode tanpa lewati</string>\r\n    <string name=\"content_block_action_type\">Pilih tindakan</string>\r\n    <string name=\"content_block_action_none\">Tidak melakukan apapun</string>\r\n    <string name=\"content_block_action_only_skip\">Lewati saja</string>\r\n    <string name=\"content_block_action_toast\">Lewati dengan pemberitahuan</string>\r\n    <string name=\"content_block_action_dialog\">Tampilkan dialog konfirmasi</string>\r\n    <string name=\"content_block_filler\">Di luar topik (filler)</string>\r\n    <string name=\"keyboard_auto_show\">Tampilkan keyboard secara otomatis</string>\r\n    <string name=\"cancel_dialog\">Batalkan</string>\r\n    <string name=\"msg_player_error_source2\">URL tidak berfungsi atau waktu perangkat yang salah.\\nBiasanya, ini adalah bug dalam aplikasi.</string>\r\n    <string name=\"msg_player_error_video_source\">URL VIDIO tidak berfungsi atau waktu perangkat yang salah.\\nBiasanya, ini adalah bug dalam aplikasi.</string>\r\n    <string name=\"msg_player_error_audio_source\">URL AUDIO tidak berfungsi atau waktu perangkat yang salah.\\nBiasanya, ini adalah bug dalam aplikasi.</string>\r\n    <string name=\"msg_player_error_subtitle_source\">URL SUBTITLE tidak berfungsi atau waktu perangkat yang salah.\\nBiasanya, ini adalah bug dalam aplikasi.</string>\r\n    <string name=\"hide_upcoming\">Sembunyikan yang akan datang dari langganan</string>\r\n    <string name=\"hide_upcoming_channel\">Sembunyikan yang akan datang dari Channel</string>\r\n    <string name=\"hide_upcoming_home\">Sembunyikan yang akan datang dari Beranda</string>\r\n    <string name=\"search_background_playback\">Pemutaran latar belakang saat mencari/menelusuri channel</string>\r\n    <string name=\"trending_row_name\">Sedang tren</string>\r\n    <string name=\"player_seek_type\">Perilaku pencarian</string>\r\n    <string name=\"player_seek_regular\">Reguler</string>\r\n    <string name=\"player_seek_confirmation_pause\">Dengan konfirmasi (jeda saat mencari)</string>\r\n    <string name=\"player_seek_confirmation_play\">Dengan konfirmasi (bermain sambil mencari)</string>\r\n    <string name=\"hide_shorts_from_home\">Sembunyikan vidio pendek dari Beranda</string>\r\n    <string name=\"finish_on_disconnect\">Menutup aplikasi setelah melepaskan ponsel/tablet</string>\r\n    <string name=\"update_error\">Pembaharuan error</string>\r\n    <string name=\"hide_shorts_from_search\">Sembunyikan vidio pendek dari hasil pencarian</string>\r\n    <string name=\"hide_shorts_from_history\">Sembunyikan vidio pendek dari Riwayat</string>\r\n    <string name=\"hide_shorts_from_trending\">Sembunyikan vidio pendek dari Trending</string>\r\n    <string name=\"disable_screensaver\">Nonaktifkan screensaver</string>\r\n    <string name=\"description_not_found\">Deskripsi tidak ditemukan</string>\r\n    <string name=\"proxy_port_hint\">Port proxy</string>\r\n    <string name=\"proxy_host_hint\">Nama host proxy atau IP</string>\r\n    <string name=\"proxy_username_hint\">Nama pengguna proxy</string>\r\n    <string name=\"proxy_password_hint\">Kata sandi proxy</string>\r\n    <string name=\"proxy_type\">Jenis Proksi</string>\r\n    <string name=\"enable_web_proxy\">Gunakan Proksi Web</string>\r\n    <string name=\"proxy_not_supported\">Gunakan Proksi Web (Membutuhkan Android 4.4+)</string>    \r\n    <string name=\"proxy_settings_title\">Pengaturan Server Proksi</string>    \r\n    <string name=\"proxy_test_cancelled\">Tes #%d: dibatalkan.</string>\r\n    <string name=\"proxy_type_invalid\">Jenis proxy tidak disetel.</string>\r\n    <string name=\"proxy_host_invalid\">Host proxy tidak disetel.</string>\r\n    <string name=\"proxy_port_invalid\">Port proxy tidak valid, harus &gt; 0.</string>\r\n    <string name=\"proxy_credentials_invalid\">Username dan password salah.</string>\r\n    <string name=\"proxy_test_aborted\">Tes dibatalkan, harap perbaiki pengaturan proxy terlebih dahulu.</string>\r\n    <string name=\"proxy_application_aborted\">Tolong perbaiki pengaturan proxy terlebih dahulu.</string>\r\n    <string name=\"openvpn_config_address_hint\">Alamat file konfigurasi (*.ovpn)</string>\r\n    <string name=\"enable_openvpn\">Gunakan OpenVPN</string>\r\n    <string name=\"openvpn_settings_title\">Pengaturan OpenVPN</string>\r\n    <string name=\"openvpn_application_aborted\">Tolong perbaiki pengaturan OpenVPN terlebih dahulu.</string>\r\n    <string name=\"openvpn_test_aborted\">Tes dibatalkan, harap perbaiki pengaturan OpenVPN terlebih dahulu.</string>\r\n    <string name=\"openvpn_address_invalid\">Alamat konfigurasi OpenVPN tidak disetel.</string>\r\n    <string name=\"internet_censorship\">Sensor Internet</string>\r\n    <string name=\"rename_section\">Ganti nama bagian ini</string>\r\n    <string name=\"simple_edit_value_hint\">Nilai baru</string>\r\n    <string name=\"seek_interval\">Interval pencarian</string>\r\n    <string name=\"seek_interval_sec\">%s detik</string>\r\n    <string name=\"subtitle_system\">Gaya system (Pengaturan Android &gt; Aksesibilitas)</string>\r\n    <string name=\"subtitle_scale\">Ukuran subtitle</string>\r\n    <string name=\"audio_sync_fix_desc\">Cara alternatif untuk menyinkronkan suara/vidio. Ini dianggap usang, tetapi dalam beberapa kasus, dapat membantu.</string>\r\n    <string name=\"audio_sync_fix\">Perbaikan sinkronisasi suara</string>\r\n    <string name=\"ambilight_ratio_fix_desc\">Memperbaiki pencahayaan bias yang tidak ada. Memperbaiki rasio aspek/ukuran vidio yang salah. Memperbaiki tangkapan layar kosong. Mempengaruhi kinerja!</string>\r\n    <string name=\"ambilight_ratio_fix\">Perbaikan ambilight/rasio aspek/ukuran vidio/tangkapan layar</string>\r\n    <string name=\"force_legacy_codecs_desc\">Secara signifikan meningkatkan kinerja pada perangkat kelas bawah. Resolusi maksimum adalah 720p.</string>\r\n    <string name=\"force_legacy_codecs\">Paksa codec versi lama (720p)</string>\r\n    <string name=\"live_stream_fix_desc\">Peringatan, tweak ini menonaktifkan pemutaran ulang streaming. Secara signifikan meningkatkan performa live streaming pada perangkat kelas bawah. Resolusi maksimum adalah 1080p.</string>\r\n    <string name=\"live_stream_fix_4k_desc\">Peringatan, tweak ini menonaktifkan pemutaran ulang streaming. Secara signifikan meningkatkan performa streaming langsung. Resolusi maksimum adalah 4K.</string>\r\n    <string name=\"live_stream_fix\">Perbaikan live streaming (1080p)</string>\r\n    <string name=\"live_stream_fix_4k\">Perbaikan live stream (4K)</string>\r\n    <string name=\"playback_notifications_fix_desc\">Menyembunyikan pemberitahuan perubahan trek. Bisa berguna pada firmware berbasis AOSP.</string>\r\n    <string name=\"playback_notifications_fix\">Nonaktifkan notifikasi pemutaran</string>\r\n    <string name=\"amlogic_fix_desc\">Perbaikan penurunan frame pada perangkat berbasis Amlogic.</string>\r\n    <string name=\"amlogic_fix\">Perbaikan Amlogic 1080p\\@60fps</string>\r\n    <string name=\"tunneled_video_playback_desc\">CATATAN: jeda mungkin tidak berfungsi dengan baik! Pemutaran tunneled vidio menjanjikan manfaat seperti sinkronisasi suara/vidio yang lebih baik (sinkronisasi AV) dan pemutaran yang lebih lancar. Diperlukan Android 5+</string>\r\n    <string name=\"tunneled_video_playback\">Pemutaran tunneled vidio (Android 5+)</string>\r\n    <string name=\"master_volume\">Volume utama</string>\r\n    <string name=\"volume_limit\">Batas volume</string>\r\n    <string name=\"play_video\">Putar</string>\r\n    <string name=\"remember_position_of_short_videos\">Ingat posisi vidio pendek (kurang dari 5 menit)</string>\r\n    <string name=\"remember_position_of_live_videos\">Ingat posisi streaming langsung</string>\r\n    <string name=\"player_show_tooltips\">Tampilkan tooltips tombol</string>\r\n    <string name=\"action_like_unset\">Like dihapus</string>\r\n    <string name=\"action_dislike_unset\">Dislike dihapus</string>\r\n    <string name=\"various_buttons\">Tombol di bagian atas jendela utama</string>\r\n    <string name=\"not_compatible_with\">Opsi yang dipilih tidak kompatibel dengan</string>\r\n    <string name=\"subtitle_position\">Pergeseran bawah subtitle</string>\r\n    <string name=\"pressing_home\">dengan menekan HOME</string>\r\n    <string name=\"pressing_home_back\">dengan menekan HOME atau BACK</string>\r\n    <string name=\"pressing_back\">dengan menekan BACK</string>\r\n    <string name=\"player_number_key_seek\">Cari dengan tombol angka</string>\r\n    <string name=\"save_remove_playlist\">Simpan/Hapus daftar putar dari bagian Daftar Putar</string>\r\n    <string name=\"save_playlist\">Menambahkan daftar putar ke bagian Daftar Putar</string>\r\n    <string name=\"remove_playlist\">Hapus daftar putar dari bagian Daftar Putar secara permanen</string>\r\n    <string name=\"removed_from_playlists\">Dihapus dari bagian Daftar Putar</string>\r\n    <string name=\"saved_to_playlists\">Ditambahkan ke bagian Daftar Putar</string>\r\n    <string name=\"create_playlist\">Buat daftar putar</string>\r\n    <string name=\"add_video_to_new_playlist\">Tambahkan ke daftar putar baru</string>\r\n    <string name=\"cant_delete_empty_playlist\">Tidak dapat menghapus daftar putar yang kosong</string>\r\n    <string name=\"playlist\">Daftar putar</string>\r\n    <string name=\"rename_playlist\">Ganti nama daftar putar</string>\r\n    <string name=\"cant_rename_empty_playlist\">Tidak dapat mengganti nama daftar putar yang kosong</string>\r\n    <string name=\"cant_rename_foreign_playlist\">Tidak dapat mengganti nama daftar putar asing</string>\r\n    <string name=\"cant_save_playlist\">Daftar putar ini tidak dapat ditambahkan</string>\r\n    <string name=\"enter_value\">Masukkan nilai yang tidak kosong</string>\r\n    <string name=\"dialog_add_remove_from\">Tambah/Hapus dari %s</string>\r\n    <string name=\"lb_playback_controls_skip_next\">Lewati Berikutnya</string>\r\n    <string name=\"lb_playback_controls_skip_previous\">Lewati Sebelumnya/Putar balik untuk memulai posisi</string>\r\n    <string name=\"alt_presets_behavior\">Alt perilaku preset (batas bandwidth)</string>\r\n    <string name=\"alt_presets_behavior_desc\">Aplikasi akan mencoba mempertahankan bandwidth yang sesuai dengan preset yang dipilih, bukan mencocokkan antara resolusi, fps, dan codec.</string>\r\n    <string name=\"sleep_timer\">Pengatur waktu tidur (jika remote tidak digunakan selama satu jam)</string>\r\n    <string name=\"sleep_timer_desc\">Jeda pemutaran jika pengguna tidak menggunakan remote selama satu jam</string>\r\n    <string name=\"disable_vsync\">Nonaktifkan snap ke vsync</string>\r\n    <string name=\"disable_vsync_desc\">Meningkatkan kinerja pemutaran sedikit</string>\r\n    <string name=\"skip_codec_profile_check\">Lewati pemeriksaan level profil codec</string>\r\n    <string name=\"skip_codec_profile_check_desc\">Jangan periksa dukungan codec saat memulai vidio. Bisa membantu pada firmware buggy.</string>\r\n    <string name=\"force_sw_codec\">Paksa dekoder vidio SW</string>\r\n    <string name=\"force_sw_codec_desc\">Dapat memutar hampir semua vidio, tetapi kinerjanya sangat buruk.</string>\r\n    <string name=\"playlist_order\">Susun daftar putar</string>\r\n    <string name=\"playlist_order_added_date_newer_first\">Tanggal ditambahkan (lebih baru dulu)</string>\r\n    <string name=\"playlist_order_added_date_older_first\">Tanggal ditambahkan (lebih lama dulu)</string>\r\n    <string name=\"playlist_order_published_date_newer_first\">Tanggal diterbitkan (lebih baru dulu)</string>\r\n    <string name=\"playlist_order_published_date_older_first\">Tanggal diterbitkan (lebih lama dulu)</string>\r\n    <string name=\"playlist_order_popularity\">Kepopuleran</string>\r\n    <string name=\"cant_do_this_for_foreign_playlist\">Tidak dapat melakukan ini untuk daftar putar asing</string>\r\n    <string name=\"owned_playlist_warning\">Kesalahan: Tindakan ini hanya dapat diterapkan untuk daftar putar yang dimiliki</string>\r\n    <string name=\"content_block_alt_server\">Gunakan server alternatif</string>\r\n    <string name=\"content_block_alt_server_desc\">Aktifkan opsi ini jika SponsorBlock tidak berfungsi dengan alasan yang berbeda.</string>\r\n    <string name=\"unset_stream_reminder\">Pengingat stream yang tidak disetel</string>\r\n    <string name=\"set_stream_reminder\">Setel pengingat stream</string>\r\n    <string name=\"playback_starts_shortly\">Pemutaran akan dimulai secara otomatis saat streaming sudah siap</string>\r\n    <string name=\"starting_stream\">Memulai stream…</string>\r\n    <string name=\"action_playlist_remove\">Hapus dari daftar putar</string>\r\n    <string name=\"signin_view_title\">Kode Pengguna sedang dimuat…</string>\r\n    <string name=\"signin_view_description\">Untuk masuk, masukkan kode ini di halaman %s\\nCatatan: Hanya berfungsi dengan browser Firefox atau Chrome.</string>\r\n    <string name=\"signin_view_action_text\">Selesai</string>\r\n    <string name=\"require_checked\">Memerlukan \\\"%s\\\"</string>\r\n    <string name=\"msg_press_again_to_exit\">Tekan lagi untuk keluar</string>\r\n    <string name=\"player_remaining_time\">Tersisa: %s</string>\r\n    <string name=\"player_ending_time\">Berakhir pada %s</string>\r\n    <string name=\"badge_new_content\">KONTEN BARU</string>    \r\n    <string name=\"add_device_view_description\">Untuk menautkan perangkat, masukkan kode ini di aplikasi YouTube ponsel kamu di bagian Setelan/Tonton di TV\\nNCatatan: Hanya berfungsi dengan keyboard Gboard.</string>\r\n    <string name=\"playback_controls_repeat_pause\">Ulangi Jeda</string>\r\n    <string name=\"playback_controls_repeat_list\">Ulangi daftar</string>\r\n    <string name=\"action_subscribe_off\">Berlangganan Mati</string>\r\n    <string name=\"action_subscribe_on\">Berlangganan Aktif</string>\r\n    <string name=\"skip_24_rate\">Lewati format 24fps (perbaikan mode real cinema\\?)</string>\r\n    <string name=\"skip_shorts\">Lewati vidio pendenk</string>\r\n    <string name=\"player_disable_suggestions\">Nonaktifkan saran</string>\r\n    <string name=\"suggestions\">Saran</string>\r\n    <string name=\"feedback\">Masukan</string>\r\n    <string name=\"sources\">Sumber</string>\r\n    <string name=\"releases\">Rilis</string>\r\n    <string name=\"prefer_avc_over_vp9\">Pilihan codec: lebih utama avc daripada vp9</string>\r\n    <string name=\"open_chat\">Obrolan</string>\r\n    <string name=\"open_comments\">Buka Obrolan</string>\r\n    <string name=\"place_chat_left\">Tempatkan obrolan ke sebelah kiri</string>\r\n    <string name=\"place_comments_left\">Tempatkan komentar ke sebelah kiri</string>\r\n    <string name=\"use_alt_speech_recognizer\">Pengenal ucapan alternatif</string>\r\n    <string name=\"time_format\">Mode waktu</string>\r\n    <string name=\"time_format_24\">24 jam</string>\r\n    <string name=\"time_format_12\">12 jam (AM/PM)</string>\r\n    <string name=\"use_alt_speech_recognizer_desc\">Serius rusak. Gunakan hanya jika kamu memiliki masalah dengan pengenal default.</string>\r\n    <string name=\"speech_recognizer\">Pengenal ucapan</string>\r\n    <string name=\"speech_engine\">Mesin pencari suara</string>\r\n    <string name=\"speech_recognizer_system\">Sistem (lebih utama)</string>\r\n    <string name=\"speech_recognizer_external_1\">Eksternal 1 (serius rusak, agar berfungsi, kamu perlu menonaktifkan akses ke mikrofon)</string>\r\n    <string name=\"speech_recognizer_external_2\">Eksternal 2 (serius rusak)</string>\r\n    <string name=\"real_channel_icon\">Tampilkan ikon asli pada tombol channel</string>\r\n    <string name=\"subtitle_yellow_semi_transparent\">Kuning dengan latar belakang semitransparan</string>\r\n    <string name=\"subtitle_yellow_black\">Kuning dengan latar belakang hitam</string>\r\n    <string name=\"player_pixel_ratio\">Rasio Pixel</string>\r\n    <string name=\"color_scheme_dark_grey_monochrome\">Abu-abu Gelap (monokrom)</string>\r\n    <string name=\"disable_mic_permission\">Harap nonaktifkan akses mikrofon untuk aplikasi agar pengenal ini berfungsi dengan baik.</string>\r\n    <string name=\"repeat_mode_shuffle\">Mengacak daftar putar apa pun</string>\r\n    <string name=\"chat_left\">Kiri</string>\r\n    <string name=\"chat_right\">Kanan</string>\r\n    <string name=\"card_real_thumbnails\">Mengganti thumbnails dengan frame dari vidio</string>\r\n    <string name=\"card_content\">Tempat mengambil kartu thumbnails</string>\r\n    <string name=\"thumb_quality_start\">Awal vidio</string>\r\n    <string name=\"thumb_quality_middle\">Tengah vidio</string>\r\n    <string name=\"thumb_quality_end\">Akhir vidio</string>\r\n    <string name=\"content_block_status\">Cek status server SponsorBlock</string>\r\n    <string name=\"dearrow_status\">Cek status server DeArrow</string>\r\n    <string name=\"player_show_quality_info_bitrate\">Tambahkan bitrate ke dalam informasi kualitas</string>\r\n    <string name=\"player_speed_button_old_behavior\">Mengembalikan perilaku lama tombol kecepatan</string>\r\n    <string name=\"protect_settings_with_password\">Lindungi semua pengaturan dengan sandi</string>\r\n    <string name=\"enter_settings_password\">Masukkan sandi pengaturan</string>\r\n    <string name=\"child_mode\">Mode anak</string>\r\n    <string name=\"child_mode_desc\">Dalam mode ini, pengguna tidak dapat menggunakan pencarian atau melihat konten yang disarankan. Halaman pengaturan akan dilindungi kata sandi.</string>\r\n    <string name=\"lost_setting_warning\">Pengaturan aplikasi akan diubah. Pastikan kamu telah membuat cadangan pengaturan.</string>\r\n    <string name=\"player_button_long_click\">Tekan lama tombol ini untuk opsi tambahan</string>\r\n    <string name=\"pause_history\">Jeda riwayat</string>\r\n    <string name=\"resume_history\">Lanjutkan riwayat</string>\r\n    <string name=\"disable_history\">Nonaktifkan riwayat</string>\r\n    <string name=\"enable_history\">Aktifkan riwayat</string>\r\n    <string name=\"clear_history\">Hapus riwayat</string>    \r\n    <string name=\"card_multiline_subtitle\">Subtitel multiline</string>\r\n    <string name=\"auto_frame_rate_modes\">Mode yang didukung</string>\r\n    <string name=\"hide_streams\">Sembunyikan stream dari Langganan</string>\r\n    <string name=\"video_rotate\">Berputar</string>\r\n    <string name=\"video_flip\">Balikkan (cermin)</string>\r\n    <string name=\"trending_searches\">Penelusuran yang sedang tren</string>\r\n    <string name=\"video_duration_any\">Apa saja</string>\r\n    <string name=\"video_duration\">Durasi</string>\r\n    <string name=\"video_duration_under_4\">Dibawah 4 menit</string>\r\n    <string name=\"video_duration_between_4_20\">4–20 menit</string>\r\n    <string name=\"video_duration_over_20\">Lebih 20 menit</string>\r\n    <string name=\"content_type\">Tipe</string>\r\n    <string name=\"content_type_any\">Apa saja</string>\r\n    <string name=\"content_type_video\">Vidio</string>\r\n    <string name=\"content_type_playlist\">Daftar Putar</string>\r\n    <string name=\"content_type_movie\">Film</string>\r\n    <string name=\"video_features\">Unggulan</string>\r\n    <string name=\"video_feature_any\">Apa saja</string>\r\n    <string name=\"search_sorting\">Urutkan berdasarkan</string>\r\n    <string name=\"sort_by_relevance\">Relevansi</string>\r\n    <string name=\"sort_by_views\">Jumlah tampilan</string>\r\n    <string name=\"sort_by_date\">Tanggal unggahan</string>\r\n    <string name=\"sort_by_rating\">Peringkat</string>\r\n    <string name=\"clear_search_history\">Hapus riwayat pencarian</string>\r\n    <string name=\"remove_from_subscriptions\">Sembunyi</string>\r\n    <string name=\"player_long_speed_list\">Daftar panjang kecepatan</string>\r\n    <string name=\"player_extra_long_speed_list\">Daftar ekstra panjang kecepatan</string>\r\n    <string name=\"alt_app_icon\">Ikon alternatif aplikasi (perlu reboot)</string>\r\n    <string name=\"network_stack\">Lebih suka %s tumpukan jaringan</string>\r\n    <string name=\"player_network_stack\">Mesin jaringan</string>\r\n    <string name=\"cronet_desc\">Cronet adalah tumpukan jaringan Chromium. Cronet dapat mengurangi latensi dan meningkatkan kinerja jaringan, yang dapat membantu mengatasi masalah buffering.</string>\r\n    <string name=\"unlock_all_formats\">Membuka semua format vidio</string>\r\n    <string name=\"unlock_all_formats_desc\">Pada beberapa perangkat, firmware salah melaporkan beberapa format sebagai format yang tidak didukung, meskipun sebenarnya format tersebut didukung.\\n(misalnya, smart TV 1080p cenderung melaporkan 4k sebagai tidak didukung).</string>\r\n    <string name=\"okhttp_desc\">Mesin jaringan ini adalah yang paling lambat tetapi memiliki stabilitas yang baik.</string>\r\n    <string name=\"select_channel_section\">Buka bagian yang sesuai saat aplikasi diluncurkan dari ATV Channels</string>\r\n    <string name=\"enable_voice_search_desc\">Aplikasi resmi akan digantikan dengan apa yang disebut \\\"jembatan\\\". Jembatan membantu mentransfer permintaan pencarian global ke aplikasi kami.</string>\r\n    <string name=\"enable_master_password\">Aktifkan kata sandi utama</string>\r\n    <string name=\"enter_master_password\">Masukkan kata sandi utama</string>\r\n    <string name=\"disable_stream_buffer\">Nonaktifkan buffer pada stream</string>\r\n    <string name=\"disable_stream_buffer_desc\">Memperbaiki situasi ketika streaming tertinggal jauh di belakang. Catatan: streaming mungkin mulai lag.</string>\r\n    <string name=\"sony_frame_drop_fix\">Perbaikan Frame drop #1</string>\r\n    <string name=\"sony_frame_drop_fix_desc\">Memperbaiki lag pada TV Sony dan beberapa perangkat lain. Catatan: kemungkinan masalah dengan sinkronisasi suara.</string>\r\n    <string name=\"audio_language\">Bahasa suara</string>\r\n    <string name=\"old_home_look\">Tampilan lama bagian Beranda</string>\r\n    <string name=\"old_channel_look\">tampilan lama halaman Channel</string>\r\n    <string name=\"hide_shorts_everywhere\">Sembunyikan vidio pendek di mana saja</string>\r\n    <string name=\"update_found\">Pembaharuan</string>\r\n    <string name=\"volume_boost_warning\">Pengaturan apa pun yang melebihi batas dapat berpotensi merusak speaker</string>\r\n    <string name=\"play_video_incognito\">Mainkan di penyamaran</string>\r\n    <string name=\"header_kids_home\">Anak-anak</string>\r\n    <string name=\"player_section_playlist\">Menggunakan konten bagian saat ini sebagai daftar putar</string>\r\n    <string name=\"header_trending\">Sedang tren</string>\r\n    <string name=\"player_screen_off_timeout\">Batas waktu layar mati</string>\r\n    <string name=\"old_update_notifications\">Tampilan lama dari notifikasi pembaruan</string>\r\n    <string name=\"dialog_notification\">Memberitahukan tentang pembaruan dengan dialog pop-up</string>\r\n    <string name=\"mark_as_watched\">Tandai sebagai telah ditonton</string>\r\n    <string name=\"player_ui_animations\">Mengaktifkan animasi UI pemutar</string>\r\n    <string name=\"player_likes_count\">Tampilkan jumlah suka/tidak suka</string>\r\n    <string name=\"sorting_alphabetically2\">Berdasarkan abjad (cepat, pratinjau animasi)</string>\r\n    <string name=\"sorting_default\">Standar (cepat, pratinjau animasi)</string>\r\n    <string name=\"content_block_exclude_channel\">Kecualikan channel ini dari SponsorBlock</string>\r\n    <string name=\"content_block_stop_excluding_channel\">Berhenti mengecualikan channel ini dari SponsorBlock</string>\r\n    <string name=\"player_chapter_notification\">Maju cepat melalui chapters menggunakan notifikasi popup</string>\r\n    <string name=\"subtitle_remember\">Mengaktifkan subtitle hanya pada channel saat ini</string>    \r\n    <string name=\"amazon_frame_drop_fix\">Perbaikan Frame drop #2</string>\r\n    <string name=\"amazon_frame_drop_fix_desc\">Diperuntukkan untuk perangkat Amazon Stick. Dapat berfungsi pada perangkat lain juga.</string>\r\n    <string name=\"default_stack_desc\">Mesin jaringan bawaan. Mesin ini mungkin memiliki stabilitas yang lebih baik dalam situasi tertentu.</string>\r\n    <string name=\"item_postion\">Posisi</string>\r\n    <string name=\"player_screen_off_dimming\">Jumlah peredupan</string>\r\n    <string name=\"screensaver_timout\">Batas waktu screensaver</string>\r\n    <string name=\"screensaver_dimming\">Jumlah peredupan screensaver</string>\r\n    <string name=\"player_ui_on_next\">Menampilkan UI pemutar saat beralih ke vidio berikutnya</string>\r\n    <string name=\"autogenerated\">dibuat otomatis</string>\r\n    <string name=\"player_auto_volume\">Penyesuaian volume otomatis</string>\r\n    <string name=\"header_shorts\">Vidio Pendek</string>\r\n    <string name=\"player_global_focus\">Navigasi yang mulus di antara deretan tombol pemutar</string>\r\n    <string name=\"auto_history\">Otomatis (gunakan pengaturan akun)</string>\r\n    <string name=\"remember_position_subscriptions\">Ingat posisi terakhir yang dilihat di Langganan</string>\r\n    <string name=\"remember_position_pinned\">Mengingat posisi terakhir dilihat di daftar putar yang disematkan</string>\r\n    <string name=\"msg_player_unknown_error\">Kesalahan tidak diketahui</string>\r\n    <string name=\"unknown_source_error\">Kesalahan sumber tidak dikenal</string>\r\n    <string name=\"unknown_renderer_error\">Kesalahan renderer yang tidak dikenal</string>\r\n    <string name=\"header_notifications\">Notifikasi</string>\r\n    <string name=\"disable_search_history\">Nonaktifkan riwayat pencarian</string>\r\n    <string name=\"unlock_high_bitrate_formats\">Buka format 1080p VP9 bitrate tinggi</string>\r\n    <string name=\"unlock_high_bitrate_audio_formats\">Buka format mp4a bitrate tinggi</string>\r\n    <string name=\"color_scheme_blue\">Biru</string>\r\n    <string name=\"color_scheme_dark_blue\">Biru Tua</string>\r\n    <string name=\"player_speed_per_channel\">Per setiap channel</string>\r\n    <string name=\"disable_popular_searches\">Nonaktifkan kueri penelusuran populer</string>\r\n    <string name=\"multi_profiles\">Gunakan pengaturan terpisah untuk tiap akun</string>\r\n    <string name=\"protect_account_with_password\">Lindungi akun ini dengan kata sandi</string>\r\n    <string name=\"enter_account_password\">Masukkan kata sandi akun</string>\r\n    <string name=\"show_connect_messages\">Menampilkan pesan sambungan</string>\r\n    <string name=\"prefer_avc_over_vp9_desc\">PERINGATAN: Maksimum 1080p</string>\r\n    <string name=\"play_next\">Putar berikutnya</string>\r\n    <string name=\"hide_watched_from_subscriptions\">Menyembunyikan vidio yang ditonton dari Langganan</string>\r\n    <string name=\"hide_watched_from_notifications\">Menyembunyikan vidio yang ditonton dari Notifikasi</string>\r\n    <string name=\"hide_unwanted_content\">Sembunyikan konten yang tidak diinginkan</string>\r\n    <string name=\"hide_watched_from_home\">Menyembunyikan vidio yang ditonton dari Beranda</string>\r\n    <string name=\"hide_watched_from_watch_later\">Menyembunyikan vidio yang ditonton dari daftar putar Tonton nanti</string>\r\n    <string name=\"remote_control_permission\">Latar Belakang Remote control memerlukan izin Overlay</string>\r\n    <string name=\"login_from_browser\">Masuk dari browser</string>\r\n    <string name=\"disable_remote_history\">Menonaktifkan riwayat saat menggunakan remote control</string>\r\n    <string name=\"keyboard_fix\">Mencegah tombol OK membuka keyboard (G20 dan lainnya)</string>\r\n    <string name=\"nothing_found\">Tidak ada yang ditemukan</string>\r\n    <string name=\"auto_frame_rate_desc\">Opsi ini menghilangkan jitter pada adegan di mana kamera bergerak cepat, misalnya, streaming olahraga</string>\r\n    <string name=\"channel_filter_hint\">Menyaring channels</string>\r\n    <string name=\"player_loop_shorts\">Ulangi vidio pendek</string>\r\n    <string name=\"player_quick_shorts_skip\">Lewati vidio Pendek dengan tombol kiri/kanan</string>\r\n    <string name=\"player_quick_skip_videos\">Lewati video biasa dengan tombol kiri/kanan</string>\r\n    <string name=\"channels_filter\">Tampilkan bidang Filter channel di dalam chapter Channel</string>\r\n    <string name=\"channel_search_bar\">Bilah pencarian di dalam halaman Channel</string>\r\n    <string name=\"header_sports\">Olahraga</string>\r\n    <string name=\"keep_finished_activities\">Menyimpan aktivitas yang telah selesai</string>\r\n    <string name=\"disable_channels_service\">Nonaktifkan layanan Channels</string>\r\n    <string name=\"replace_titles\">Ganti judul</string>\r\n    <string name=\"enable\">Aktifkan</string>\r\n    <string name=\"more_info\">Info lebih lanjut</string>\r\n    <string name=\"about_sponsorblock\">Tentang SponsorBlock</string>\r\n    <string name=\"about_dearrow\">Tentang DeArrow</string>\r\n    <string name=\"replace_thumbnails\">Ganti thumbnails</string>\r\n    <string name=\"crowdsourced_thumbnails\">Thumbnails dari Crowdsourced</string>\r\n    <string name=\"crowdsoursed_titles\">Judul dari Crowdsourced</string>\r\n    <string name=\"dearrow_not_submitted_thumbs\">Sumber gambar mini (jika tidak ada kiriman DeArrow)</string>\r\n    <string name=\"pitch_effect\">Efek nada</string>\t\t\t\t\t\t\t\t\t\t\t\t \r\n    <string name=\"fullscreen_mode\">Mode layar penuh (tanpa bar sistem)</string>\r\n    <string name=\"player_only_mode\">Hanya menampilkan pemutar jika vidio dibuka di luar aplikasi</string>\r\n    <string name=\"pinned_channel_rows\">Menampilkan channel yang disematkan sebagai baris</string>\r\n    <string name=\"prefer_ipv4\">Lebih memilih DNS IPv4</string>\r\n    <string name=\"prefer_ipv4_desc\">Dapat memperbaiki situasi ketika aplikasi tidak berfungsi sama sekali.\\nCatatan. Dapat menyebabkan hang dan crash (terutama pada perangkat Android 8 atau Dune HD)</string>\r\n    <string name=\"long_press_for_settings\">TEKAN LAMA UNTUK PENGATURAN</string>\r\n    <string name=\"long_press_for_options\">TEKAN LAMA UNTUK OPSI</string>\r\n    <string name=\"device_specific_backup\">Pencadangan hanya untuk perangkat ini</string>\r\n    <string name=\"local_backup\">Pencadangan lokal</string>\r\n    <string name=\"auto_backup\">Pencadangan otomatis (sekali sehari)</string>\r\n    <string name=\"repeat_mode_reverse_list\">Memutar daftar putar atau menyalurkan video dalam urutan terbalik</string>\r\n    <string name=\"calm_msg\">Kami sedang memperbaiki masalah ini. Periksa pembaruan dari waktu ke waktu</string>\r\n    <string name=\"without_picture\">Tanpa gambar</string>\r\n    <string name=\"video_disabled\">Vidio dinonaktifkan</string>\r\n    <string name=\"applying_fix\">Jangan tutup pemutar. Menerapkan perbaikan…</string>\r\n    <string name=\"hide_mixes\">Sembunyikan Campuran</string>\r\n    <string name=\"player_global_focus_desc\">Fitur ini memengaruhi tombol pemutar mana yang akan menerima fokus ketika menavigasi di antara barisan tombol pemutar</string>\r\n    <string name=\"disable_network_error_fixing\">Menonaktifkan perbaikan kesalahan jaringan otomatis</string>\r\n    <string name=\"disable_network_error_fixing_desc\">Kamu mungkin perlu mengaktifkan opsi ini jika kamu menggunakan VPN</string>\r\n    <string name=\"recommended\">Disarankan</string>\r\n    <string name=\"add_to_subscriptions_group\">Menambah/Menghapus dari grup langganan</string>\r\n    <string name=\"new_subscriptions_group\">Grup baru</string>\r\n    <string name=\"rename_group\">Ganti nama grup langganan</string>\r\n    <string name=\"screen_dimming_amount\">Jumlah peredupan layar</string>\r\n    <string name=\"screen_dimming_timeout\">Batas waktu peredupan layar</string>\r\n    <string name=\"playlists_rows\">Tampilkan bagian Daftar Putar sebagai baris</string>\r\n    <string name=\"import_subscriptions_group\">Impor</string>\r\n    <string name=\"lb_playback_controls_closed_captioning_disable\">Nonaktifkan Teks Tertutup</string>\r\n    <string name=\"lb_playback_controls_closed_captioning_enable\">Aktifkan Teks Tertutup</string>\r\n    <string name=\"my_videos\">Vidio saya</string>\r\n    <string name=\"player_audio_focus\">Fokus audio (jeda jika pemutar lain terdeteksi)</string>\r\n    <string name=\"paid_content_notification\">Notifikasi konten berbayar</string>\r\n    <string name=\"you_liked\">Anda menyukai</string>\r\n    <string name=\"premium_users_only\">Hanya untuk pengguna premium. Perbaikan untuk daftar format video yang tidak lengkap</string>\r\n    <string name=\"playback_buffering_fix\">Perbaikan buffering pemutaran</string>\r\n    <string name=\"oculus_quest_fix\">Perbaikan Oculus Quest</string>\r\n    <string name=\"card_preview_muted\">Vidio tanpa suara</string>\r\n    <string name=\"card_preview_full\">Vidio dengan suara</string>\r\n    <string name=\"card_preview\">Pratinjau kartu</string>\r\n</resources>\r\n"
  },
  {
    "path": "common/src/main/res/values-it/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" tools:ignore=\"MissingTranslation\">\n    <string name=\"header_home\">Home</string>\n    <string name=\"title_search\">Ricerca</string>\n    <string name=\"header_subscriptions\">Iscrizioni</string>\n    <string name=\"header_history\">Cronologia</string>\n    <string name=\"header_music\">Musica</string>\n    <string name=\"header_news\">Notizie</string>\n    <string name=\"header_gaming\">Videogiochi</string>\n    <string name=\"header_playlists\">Playlist</string>\n    <string name=\"header_settings\">Impostazioni</string>\n    <string name=\"header_channels\">Canali</string>\n    <string name=\"subscriptions_signin_title\">Guarda le ultime notizie dai canali che ami</string>\n    <string name=\"subscriptions_signin_subtitle\">Accedi per vedere le tue iscrizioni</string>\n    <string name=\"subscriptions_signin_button_text\">ACCEDI</string>\n    <string name=\"library_signin_to_show_more\">Guarda i video che ti sono piaciuti, che hai salvato o a cui ti sei iscritto</string>\n    <string name=\"library_signin_subtitle\">Accedi per vedere la tua libreria</string>\n    <string name=\"action_signin\">ACCEDI</string>\n    <string name=\"title_video_formats\">Formati video</string>\n    <string name=\"title_audio_formats\">Formati audio</string>\n    <string name=\"subtitle_category_title\">Sottotitoli</string>\n    <string name=\"playback_queue_category_title\">Coda di riproduzione</string>\n    <string name=\"video_max_quality\">Automatico (qualità massima)</string>\n    <string name=\"audio_max_quality\">Automatico (qualità massima)</string>\n    <string name=\"subtitles_disabled\">Sottotitoli disabilitati</string>\n    <string name=\"auto_frame_rate\">Frequenza fotogrammi automatica</string>\n    <string name=\"frame_rate_correction\">Correggi fps:\\n%s</string>\n    <string name=\"category_background_playback\">Riproduzione in background</string>\n    <string name=\"not_implemented\">Non implementato</string>\n    <string name=\"not_supported_by_device\">Il dispositivo non supporta questa funzionalità</string>\n    <string name=\"option_background_playback_off\">Disabilitato</string>\n    <string name=\"option_background_playback_pip\">Immagine nell\\'immagine</string>\n    <string name=\"option_background_playback_only_audio\">Solo audio</string>\n    <string name=\"playback_settings\">Impostazioni qualità di riproduzione</string>\n    <string name=\"video_speed\">Velocità video</string>\n    <string name=\"resolution_switch\">Cambia risoluzione</string>\n    <string name=\"video_buffer\">Buffer video</string>\n    <string name=\"video_buffer_size_none\">Nessuno</string>\n    <string name=\"video_buffer_size_lowest\">Più basso</string>\n    <string name=\"video_buffer_size_low\">Basso</string>\n    <string name=\"video_buffer_size_med\">Medio</string>\n    <string name=\"video_buffer_size_high\">Alto</string>\n    <string name=\"video_buffer_size_highest\">Più alto</string>\n    <string name=\"update_changelog\">Registro modifiche</string>\n    <string name=\"install_update\">Installa aggiornamento</string>\n    <string name=\"section_is_empty\">Ops! Non c\\'è niente qui dentro</string>\n    <string name=\"dialog_add_to_playlist\">Aggiungi/Rimuovi dalla playlist</string>\n    <string name=\"msg_signed_users_only\">Solo utenti registrati</string>\n    <string name=\"msg_cant_load_content\">Impossibile caricare i contenuti.\\n1) Attiva la cronologia visualizzazioni.\\n2) Controlla la data e l\\'ora sul dispositivo.\\n3) Controlla la connessione di rete.\\n4) Controlla le impostazioni del proxy.\\n5) Prova ad accedere al tuo account.\\n6) Forse non c\\'è niente qui dentro.</string>\n    <string name=\"title_video_presets\">Preset video</string>\n    <string name=\"settings_accounts\">Account</string>\n    <string name=\"settings_left_panel\">Modifica categorie</string>\n    <string name=\"settings_themes\">Temi</string>\n    <string name=\"settings_other\">Altro</string>\n    <string name=\"settings_player\">Lettore</string>\n    <string name=\"settings_language\">Lingua</string>\n    <string name=\"settings_linked_devices\">Dispositivi collegati</string>\n    <string name=\"settings_about\">Informazioni su</string>\n    <string name=\"dialog_account_list\">Seleziona account</string>\n    <string name=\"dialog_account_none\">Nessuno</string>\n    <string name=\"dialog_remove_account\">Disconnessione</string>\n    <string name=\"dialog_add_account\">Registrazione</string>\n    <string name=\"default_lang\">Predefinita</string>\n    <string name=\"original_lang\">Originale</string>\n    <string name=\"dialog_select_language\">Lingua</string>\n    <string name=\"subtitle_default\">Predefiniti</string>\n    <string name=\"subtitle_white_semi_transparent\">Bianco su sfondo semitrasparente</string>\n    <string name=\"subtitle_style\">Stile sottotitoli</string>\n    <string name=\"subtitle_language\">Lingua sottotitoli</string>\n    <string name=\"action_search\">Cerca</string>\n    <string name=\"settings_main_ui\">Interfaccia utente</string>\n    <string name=\"dialog_main_ui\">Interfaccia utente</string>\n    <string name=\"card_animated_previews\">Anteprime animate</string>\n    <string name=\"web_site\">Sito web</string>\n    <string name=\"donation\">Donazioni</string>\n    <string name=\"dialog_about\">Informazioni su</string>\n    <string name=\"dialog_player_ui\">Lettore video</string>\n    <string name=\"player_show_ui_on_pause\">Mostra interfaccia utente in pausa</string>\n    <string name=\"player_pause_on_ok\">Tasto OK sospende la riproduzione</string>\n    <string name=\"player_ok_button_behavior\">Comportamento del pulsante OK</string>\n    <string name=\"player_only_ui\">Solo interfaccia utente</string>\n    <string name=\"player_ui_and_pause\">Interfaccia utente e pausa</string>\n    <string name=\"player_only_pause\">Fai solo una pausa</string>\n    <string name=\"player_toggle_speed\">Attiva/Disattiva la velocità</string>\n    <string name=\"check_for_updates\">Controlla aggiornamenti</string>\n    <string name=\"update_not_found\">Stai usando l\\'ultima versione</string>\n    <string name=\"update_in_progress\">Attesa…</string>\n    <string name=\"player_ui_hide_behavior\">Nascondi automaticamente l\\'interfaccia utente</string>\n    <string name=\"option_never\">Mai</string>\n    <string name=\"side_panel_sections\">Modifica sezioni</string>\n    <string name=\"boot_to_section\">Accedi alla sezione</string>\n    <string name=\"large_ui\">Interfaccia utente grande</string>\n    <string name=\"video_grid_scale\">Scala griglia video</string>\n    <string name=\"scale_ui\">Scala interfaccia utente</string>\n    <string name=\"color_scheme\">Combinazione di colori</string>\n    <string name=\"color_scheme_default\">Predefinito</string>\n    <string name=\"color_scheme_red_grey\">Rosso-grigio</string>\n    <string name=\"color_scheme_red\">Rosso</string>\n    <string name=\"color_scheme_dark_grey\">Grigio scuro</string>\n    <string name=\"disable_update_check\">Disabilita controllo aggiornamenti</string>\n    <string name=\"show_again\">Mostra di nuovo</string>\n    <string name=\"check_updates_auto\">Notifica aggiornamenti</string>\n    <string name=\"select_account_on_boot\">Mostra la selezione dell\\'account all\\'avvio</string>\n    <string name=\"player_other\">Varie</string>\n    <string name=\"player_full_date\">Data precisa nella descrizione</string>\n    <string name=\"open_channel\">Apri canale</string>\n    <string name=\"open_playlist\">Apri playlist</string>\n    <string name=\"not_interested\">Non interessato</string>\n    <string name=\"not_recommend_channel\">Non consigliare il canale</string>\n    <string name=\"you_wont_see_this_video\">Il video è stato rimosso dai consigliati</string>\n    <string name=\"you_wont_see_this_channel\">Non vedrai questo canale nei consigli</string>\n    <string name=\"settings_search\">Cerca</string>\n    <string name=\"dialog_search\">Cerca</string>\n    <string name=\"instant_voice_search\">Ricerca vocale istantanea</string>\n    <string name=\"option_background_playback_behind\">Riproduci in background</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">Le informazioni sul prossimo video non sono ancora state caricate</string>\n    <string name=\"card_multiline_title\">Titoli multiriga</string>\n    <string name=\"cards_style\">Stile cartoline</string>\n    <string name=\"repeat_mode_all\">Riproduci i video continuamente</string>\n    <string name=\"repeat_mode_one\">Ripeti video attuale</string>\n    <string name=\"repeat_mode_pause\">Metti in pausa la riproduzione dopo ogni video (tranne la coda)</string>\n    <string name=\"repeat_mode_pause_alt\">Riproduci solo i video della playlist continuamente</string>\n    <string name=\"repeat_mode_none\">Interrompi la riproduzione dopo un video (tranne la coda)</string>\n    <string name=\"subscribed_to_channel\">Sottoscritto</string>\n    <string name=\"unsubscribed_from_channel\">Non sottoscritto</string>\n    <string name=\"subtitle_yellow_transparent\">Giallo su sfondo trasparente</string>\n    <string name=\"subtitle_white_transparent\">Bianco su sfondo trasparente</string>\n    <string name=\"subtitle_white_black\">Bianco su sfondo nero</string>\n    <string name=\"player_seek_preview\">Anteprima durante la ricerca</string>\n    <string name=\"color_scheme_dark_grey_oled\">Grigio scuro (OLED)</string>\n    <string name=\"channels_section_sorting\">Ordinamento sezioni dei canali</string>\n    <string name=\"sorting_by_new_content\">Nuovo contenuto</string>\n    <string name=\"sorting_alphabetically\">In ordine alfabetico</string>\n    <string name=\"sorting_last_viewed\">Ultimo visto</string>\n    <string name=\"player_pause_when_seek\">Pausa durante la ricerca</string>\n    <string name=\"playlists_style\">Stile sezione playlist</string>\n    <string name=\"playlists_style_grid\">Griglia</string>\n    <string name=\"playlists_style_rows\">Righe</string>\n    <string name=\"player_show_clock\">Mostra orologio nella barra dei controlli</string>\n    <string name=\"player_show_remaining_time\">Mostra tempo rimanente nella barra dei controlli</string>\n    <string name=\"player_show_ending_time\">Mostra ora di fine nella barra dei controlli</string>\n    <string name=\"open_channel_uploads\">Apri caricamenti dei canali</string>\n    <string name=\"app_exit_shortcut\">Esci dall\\'app</string>\n    <string name=\"player_exit_shortcut\">Esci dal lettore</string>\n    <string name=\"search_exit_shortcut\">Esci dalla ricerca</string>\n    <string name=\"app_exit_none\">Non uscire</string>\n    <string name=\"app_double_back_exit\">Due volte tasto indietro</string>\n    <string name=\"app_single_back_exit\">Una volta tasto indietro</string>\n    <string name=\"action_video_zoom\">Zoom video</string>\n    <string name=\"video_zoom\">Zoom video</string>\n    <string name=\"video_zoom_default\">Predefinito</string>\n    <string name=\"video_zoom_fit_width\">Adatta in larghezza</string>\n    <string name=\"video_zoom_fit_height\">Adatta in altezza</string>\n    <string name=\"video_zoom_fit_both\">Adatta sia in larghezza che in altezza</string>\n    <string name=\"video_zoom_stretch\">Allunga</string>\n    <string name=\"color_scheme_teal\">Verde acqua</string>\n    <string name=\"color_scheme_teal_oled\">Verde acqua (OLED)</string>\n    <string name=\"player_seek_preview_none\">Disabilitato</string>\n    <string name=\"player_seek_preview_single\">Singolo fotogramma</string>\n    <string name=\"player_seek_preview_carousel\">Carosello</string>\n    <string name=\"player_seek_preview_carousel_slow\">Carosello (lento, per fotogrammi chiave)</string>\n    <string name=\"player_seek_preview_carousel_fast\">Carosello (anteprima veloce, non precisa)</string>\n    <string name=\"unsubscribe_from_channel\">Disiscriviti dal canale</string>\n    <string name=\"card_title_lines_num\">Numero righe titoli cartoline</string>\n    <string name=\"card_auto_scrolled_title\">Scorrimento automatico titoli tagliati</string>\n    <string name=\"share_link\">Condividi link</string>\n    <string name=\"share_qr_link\">Condividi link (codice QR)</string>\n    <string name=\"subscribe_to_channel\">Iscriviti al canale</string>\n    <string name=\"wait_data_loading\">Attendi il caricamento dei dati…</string>\n    <string name=\"auto_frame_rate_pause\">Frequenza fotogrammi automatica (pausa durante il passaggio)</string>\n    <string name=\"auto_frame_rate_applying\">Applica frequenza automatica fotogrammi %sx%s\\@%s</string>\n    <string name=\"audio_shift\">Modifica audio</string>\n    <string name=\"player_remember_speed\">Ricorda velocità</string>\n    <string name=\"mark_channel_as_watched\">Contrassegna come già visto</string>\n    <string name=\"channel_marked_as_watched\">Il canale è stato contrassegnato come già visto</string>\n    <string name=\"dialog_add_device\">Aggiungi dispositivo</string>\n    <string name=\"dialog_remove_all_devices\">Rimuovi tutti i dispositivi</string>\n    <string name=\"device_link_enabled\">Collegamento abilitato</string>\n    <string name=\"device_connected\">Il dispositivo \\\"%s\\\" è stato connesso</string>\n    <string name=\"device_disconnected\">Il dispositivo \\\"%s\\\" è stato disconnesso</string>\n    <string name=\"settings_ui_scale\">Scala interfaccia utente</string>\n    <string name=\"msg_restart_app\">Riavvia l\\'app per applicare queste impostazioni</string>\n    <string name=\"settings_block\">Blocco dei contenuti</string>\n    <string name=\"msg_applying\">Applicazione di %s…</string>\n    <string name=\"msg_done\">Fatto</string>\n    <string name=\"settings_general\">Generali</string>\n    <string name=\"settings_video\">Video</string>\n    <string name=\"content_block_confirm_skip\">Conferma su salta</string>\n    <string name=\"content_block_categories\">Salta segmenti</string>\n    <string name=\"content_block_sponsor\">Sponsor</string>\n    <string name=\"content_block_intro\">Introduzione/Intermezzo animato</string>\n    <string name=\"content_block_outro\">Titoli di coda/Ringraziamenti</string>\n    <string name=\"content_block_interaction\">Promemoria di interazione (iscriviti)</string>\n    <string name=\"content_block_self_promo\">Autopromozione non retribuita</string>\n    <string name=\"content_block_music_off_topic\">Sezione non musicale della clip</string>\n    <string name=\"confirm_segment_skip\">Salto il segmento\\n\\\"%s\\\"\\?</string>\n    <string name=\"cancel_segment_skip\">Annulla il salto del segmento</string>\n    <string name=\"msg_skipping_segment\">sto saltando il segmento\\n\\\"%s\\\"…</string>\n    <string name=\"content_block_notification_type\">Tipo di notifica</string>\n    <string name=\"content_block_notify_none\">Senza notifica</string>\n    <string name=\"content_block_notify_toast\">Toast</string>\n    <string name=\"content_block_notify_dialog\">Finestra di dialogo di conferma</string>\n    <string name=\"return_to_launcher\">Ritorna al programma di avvio da canali/ricerca ATV</string>\n    <string name=\"intent_force_close\">Uscita forzata se il video è stato aperto da una fonte esterna</string>\n    <string name=\"btn_confirm\">Conferma</string>\n    <string name=\"player_low_video_quality\">Qualità video bassa</string>\n    <string name=\"remote_session_closed\">La sessione remota è stata chiusa</string>\n    <string name=\"msg_mode_switch_error\">Impossibile passare dalla modalità di visualizzazione a \\\"%s\\\"</string>\n    <string name=\"msg_player_error\">Si è verificato un errore del lettore %s. Riavvio della riproduzione…</string>\n    <string name=\"video_preset_disabled\">Senza preset</string>\n    <string name=\"video_preset_enabled\">Il formato del video successivo verrà impostato in base al preset</string>\n    <string name=\"player_sleep_timer\">Timer spegnimento</string>\n    <string name=\"player_show_quality_info\">Mostra informazioni sulla qualità nella barra dei controlli</string>\n    <string name=\"player_remember_each_speed\">Ricorda velocità di ciascun video</string>\n    <string name=\"player_remember_speed_none\">Nessuna</string>\n    <string name=\"player_remember_speed_all\">Lo stesso su tutti i video</string>\n    <string name=\"player_remember_speed_each\">Per ciascun video</string>\n    <string name=\"msg_player_error_source\">Impossibile scaricare il video</string>\n    <string name=\"msg_player_error_renderer\">Il formato selezionato non è supportato.\\nProva a selezionarne un altro.\\nSe non risolvi, prova a riavviare il dispositivo.</string>\n    <string name=\"msg_player_error_video_renderer\">Il formato VIDEO selezionato non è supportato.\\nProva a selezionarne un altro premendo il pulsante HQ nel lettore.\\nSe il problema persiste, prova a riavviare il dispositivo.</string>\n    <string name=\"msg_player_error_audio_renderer\">Il formato AUDIO selezionato non è supportato.\\nProva a selezionarne un altro premendo il pulsante HQ nel lettore.\\nSe il problema persiste, prova a riavviare il dispositivo.</string>\n    <string name=\"msg_player_error_subtitle_renderer\">Il formato SOTTOTITOLI selezionato non è supportato.\\nProva a selezionarne un altro premendo a lungo il pulsante CC nel lettore.\\nSe il problema persiste, prova a riavviare il dispositivo.</string>\n    <string name=\"msg_player_error_unexpected\">Errore sconosciuto di decodifica video</string>\n    <string name=\"video_aspect\">Proporzioni</string>\n    <string name=\"player_tweaks\">Opzioni per sviluppatori</string>\n    <string name=\"header_uploads\">Caricamenti</string>\n    <string name=\"player_show_global_clock\">Mostra sempre l\\'orologio sullo schermo</string>\n    <string name=\"player_show_global_ending_time\">Mostra sempre l\\'ora di fine sullo schermo</string>\n    <string name=\"app_corner_clock\">Home: orologio nell\\'angolo in alto a destra</string>\n    <string name=\"player_corner_clock\">Lettore: orologio nell\\'angolo in alto a destra</string>\n    <string name=\"player_corner_ending_time\">Lettore: ora di fine nell\\'angolo in alto a destra</string>\n    <string name=\"uploads_old_look\">Ripristina il vecchio aspetto della sezione Caricamenti</string>\n    <string name=\"background_playback_activation\">Riproduzione in background (attivazione)</string>\n    <string name=\"channels_old_look\">Vecchio aspetto della sezione Canali</string>\n    <string name=\"channels_auto_load\">Carica automaticamente il contenuto della sezione Canali</string>\n    <string name=\"return_to_background_video\">Torna al video in esecuzione in background</string>\n    <string name=\"pin_unpin_from_sidebar\">Aggiungi/Rimuovi dalla barra laterale</string>\n    <string name=\"pin_unpin_playlist\">Aggiungi/Rimuovi playlist dalla barra laterale</string>\n    <string name=\"pin_unpin_channel\">Aggiungi/Rimuovi canale dalla barra laterale</string>\n    <string name=\"pin_playlist\">Aggiungi playlist alla barra laterale</string>\n    <string name=\"pin_channel\">Aggiungi canale alla barra laterale</string>\n    <string name=\"pinned_to_sidebar\">Aggiunto alla barra laterale</string>\n    <string name=\"unpin_from_sidebar\">Rimuovi dalla barra laterale</string>\n    <string name=\"unpin_group_from_sidebar\">Rimuovi il gruppo di iscrizioni</string>\n    <string name=\"unpinned_from_sidebar\">Rimosso dalla barra laterale</string>\n    <string name=\"dialog_select_country\">Paese</string>\n    <string name=\"settings_language_country\">Lingua/Paese</string>\n    <string name=\"share_embed_link\">Condividi link incorporato</string>\n    <string name=\"card_text_scroll_factor\">Velocità scorrimento testo titoli cartoline</string>\n    <string name=\"preferred_update_source\">Seleziona fonte aggiornamenti</string>\n    <string name=\"hide_shorts\">Nascondi i video brevi dalle Iscrizioni</string>\n    <string name=\"hide_shorts_channel\">Nascondi i video brevi da un canale</string>\n    <string name=\"key_remapping\">Rimappatura tasti</string>\n    <string name=\"screen_dimming\">Oscuramento schermo</string>\n    <string name=\"removed_from_playback_queue\">Rimosso dalla coda di riproduzione</string>\n    <string name=\"added_to_playback_queue\">Aggiunto alla coda di riproduzione</string>\n    <string name=\"add_remove_from_playback_queue\">Aggiungi/Rimuovi dalla coda di riproduzione</string>\n    <string name=\"add_to_playback_queue\">Aggiungi alla coda di riproduzione</string>\n    <string name=\"remove_from_playback_queue\">Rimuovi dalla coda di riproduzione</string>\n    <string name=\"proxy_enabled\">Proxy abilitato</string>\n    <string name=\"proxy_disabled\">Proxy disabilitato</string>\n    <string name=\"uploads_row_name\">Caricamenti</string>\n    <string name=\"playlists_row_name\">Playlist create</string>\n    <string name=\"popular_uploads_row_name\">Caricamenti popolari</string>\n    <string name=\"news_row_name\">Notizie</string>\n    <string name=\"breaking_news_row_name\">Ultime notizie</string>\n    <string name=\"covid_news_row_name\">Notizie su COVID-19</string>\n    <string name=\"enable_voice_search\">Abilita ricerca vocale (supporto firmware necessario)</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">Iscriviti/Disiscriviti dal canale</string>\n    <string name=\"app_backup_restore\">Backup e ripristino</string>\n    <string name=\"app_backup\">Backup dati app</string>\n    <string name=\"app_restore\">Ripristino dati app</string>\n    <string name=\"skip_each_segment_once\">Non saltare di nuovo i segmenti</string>\n    <string name=\"disable_ok_long_press\">Disabilita pressione prolungata del pulsante OK</string>\n    <string name=\"disable_ok_long_press_desc\">Destinato a controller difettosi in cui il pulsante OK non funziona correttamente</string>\n    <string name=\"player_time_correction\">Mostra la durata con correzione della velocità</string>\n    <string name=\"sponsor_color_markers\">Indicatori colorati sulla barra di avanzamento</string>\n    <string name=\"refresh_section\">Aggiorna sezione</string>\n    <string name=\"option_disabled\">Disabilitato</string>\n    <string name=\"live_now_row_name\">In onda</string>\n    <string name=\"run_in_background\">Riproduzione in background</string>\n    <string name=\"settings_remote_control\">Controllo remoto</string>\n    <string name=\"background_service_started\">Servizio di background avviato</string>\n    <string name=\"dialog_add_to\">Aggiungi a %s</string>\n    <string name=\"dialog_remove_from\">Rimuovi da %s</string>\n    <string name=\"added_to\">Aggiunto a %s</string>\n    <string name=\"removed_from\">Rimosso da %s</string>\n    <string name=\"video_preset_adaptive\">Adattivo</string>\n    <string name=\"content_block_preview_recap\">Anteprima o riepilogo del video</string>\n    <string name=\"content_block_highlight\">Punto interessante (segnalibro)</string>\n    <string name=\"removed_from_history\">Il video è stato rimosso dalla Cronologia</string>\n    <string name=\"remove_from_history\">Rimuovi dalla Cronologia</string>\n    <string name=\"upload_date\">Data di caricamento</string>\n    <string name=\"upload_date_any\">Qualsiasi data</string>\n    <string name=\"upload_date_today\">Oggi</string>\n    <string name=\"upload_date_this_week\">Questa settimana</string>\n    <string name=\"upload_date_this_month\">Questo mese</string>\n    <string name=\"upload_date_this_year\">Quest\\'anno</string>\n    <string name=\"double_refresh_rate\">Doppia frequenza di aggiornamento</string>\n    <string name=\"upload_date_last_hour\">Ultima ora</string>\n    <string name=\"focus_on_search_results\">Messa a fuoco automatica sui risultati della ricerca</string>\n    <string name=\"context_menu\">Menu contestuale</string>\n    <string name=\"context_menu_sorting\">Ordinamento del menu contestuale</string>\n    <string name=\"add_remove_from_recent_playlist\">Aggiungi/Rimuovi dalla playlist recente</string>\n    <string name=\"hide_settings_section\">Nascondi la sezione Impostazioni (pericoloso!)</string>\n    <string name=\"volume\">Volume %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s sec</string>\n    <string name=\"audio_shift_sec\">%s sec</string>\n    <string name=\"ui_hide_timeout_sec\">%s sec</string>\n    <string name=\"screen_dimming_timeout_min\">%s min</string>\n    <string name=\"mark_all_channels_watched\">Contrassegna tutti i canali come già visti</string>\n    <string name=\"move_section_up\">Sposta la sezione in alto</string>\n    <string name=\"move_section_down\">Sposta la sezione in basso</string>\n    <string name=\"player_buttons\">Imposta i pulsanti del lettore</string>\n    <string name=\"action_playlist_add\">Aggiungi alla playlist</string>\n    <string name=\"action_video_stats\">Statistiche video</string>\n    <string name=\"action_subscribe\">Sottoscrivi</string>\n    <string name=\"action_channel\">Apri canale</string>\n    <string name=\"action_pip\">PIP</string>\n    <string name=\"action_video_info\">Descrizione video</string>\n    <string name=\"action_screen_off\">Spegni schermo</string>\n    <string name=\"action_sound_off\">Muto</string>\n    <string name=\"action_playback_queue\">Coda di riproduzione</string>\n    <string name=\"action_video_speed\">Velocità video</string>\n    <string name=\"action_subtitles\">Sottotitoli</string>\n    <string name=\"action_like\">Mi piace</string>\n    <string name=\"action_dislike\">Non mi piace</string>\n    <string name=\"action_play_pause\">Play/Pausa</string>\n    <string name=\"action_repeat_mode\">Modalità di riproduzione</string>\n    <string name=\"action_next\">Video successivo</string>\n    <string name=\"action_previous\">Video precedente</string>\n    <string name=\"action_high_quality\">Qualità video</string>\n    <string name=\"content_block_no_skipping_mode\">Modalità senza salti</string>\n    <string name=\"content_block_action_type\">Scegli azione</string>\n    <string name=\"content_block_action_none\">Non fare nulla</string>\n    <string name=\"content_block_action_only_skip\">Salta direttamente</string>\n    <string name=\"content_block_action_toast\">Salta con notifica</string>\n    <string name=\"content_block_action_dialog\">Mostra finestra di conferma</string>\n    <string name=\"content_block_filler\">Fuori tema (riempitivo)</string>\n    <string name=\"keyboard_auto_show\">Mostra automaticamente la tastiera</string>\n    <string name=\"cancel_dialog\">Annulla</string>\n    <string name=\"msg_player_error_source2\">L\\'URL non funziona o l\\'ora del dispositivo non è corretta.\\nDi solito si tratta di un bug nell\\'app.</string>\n    <string name=\"msg_player_error_video_source\">L\\'URL VIDEO non funziona o l\\'ora del dispositivo non è corretta.\\nDi solito si tratta di un bug nell\\'app.</string>\n    <string name=\"msg_player_error_audio_source\">L\\'URL AUDIO non funziona o l\\'ora del dispositivo non è corretta.\\nDi solito si tratta di un bug nell\\'app.</string>\n    <string name=\"msg_player_error_subtitle_source\">L\\'URL SUBTITLE non funziona o l\\'ora del dispositivo non è corretta.\\nDi solito si tratta di un bug nell\\'app.</string>\n    <string name=\"hide_upcoming\">Nascondi in arrivo dalle Iscrizioni</string>\n    <string name=\"hide_upcoming_channel\">Nascondi in arrivo dal canale</string>\n    <string name=\"hide_upcoming_home\">Nascondi in arrivo dalla Home</string>\n    <string name=\"search_background_playback\">Riproduzione in background durante la ricerca/navigazione di un canale</string>\n    <string name=\"trending_row_name\">Tendenze</string>\n    <string name=\"player_seek_type\">Tipo di ricerca</string>\n    <string name=\"player_seek_regular\">Regolare</string>\n    <string name=\"player_seek_confirmation_pause\">Con conferma (pausa durante la ricerca)</string>\n    <string name=\"player_seek_confirmation_play\">Con conferma (play durante la ricerca)</string>\n    <string name=\"hide_shorts_from_home\">Nascondi i video brevi dalla Home</string>\n    <string name=\"finish_on_disconnect\">Chiudi l\\'app dopo aver scollegato il telefono/tablet</string>\n    <string name=\"update_error\">Errore di aggiornamento</string>\n    <string name=\"hide_shorts_from_search\">Nascondi i video brevi dal risultato della ricerca</string>\n    <string name=\"hide_shorts_from_history\">Nascondi i video brevi dalla Cronologia</string>\n    <string name=\"hide_shorts_from_trending\">Nascondi i video brevi dalle Tendenze</string>\n    <string name=\"disable_screensaver\">Disabilita lo screensaver</string>\n    <string name=\"description_not_found\">Descrizione non trovata</string>\n    <string name=\"proxy_port_hint\">Porta proxy</string>\n    <string name=\"proxy_host_hint\">Proxy hostname o IP</string>\n    <string name=\"proxy_username_hint\">Nome utente proxy</string>\n    <string name=\"proxy_password_hint\">Password proxy</string>\n    <string name=\"proxy_type\">Tipo proxy</string>\n    <string name=\"enable_web_proxy\">Usa proxy web</string>\n    <string name=\"proxy_not_supported\">Usa proxy web (necessita di Android 4.4+)</string>\n    <string name=\"proxy_test_btn\">Test</string>\n    <string name=\"proxy_settings_title\">Impostazioni server proxy</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">Test #%d: annullato.</string>\n    <string name=\"proxy_type_invalid\">Tipo di proxy non impostato.</string>\n    <string name=\"proxy_host_invalid\">Host proxy non impostato.</string>\n    <string name=\"proxy_port_invalid\">Porta proxy non valida, deve essere più grande di 0.</string>\n    <string name=\"proxy_credentials_invalid\">Nome utente o password errati.</string>\n    <string name=\"proxy_test_aborted\">Test interrotto, correggi prima le impostazioni del proxy.</string>\n    <string name=\"proxy_application_aborted\">Correggi prima le impostazioni del proxy.</string>\n    <string name=\"proxy_test_start\">Test #%d: %s …</string>\n    <string name=\"proxy_test_error\">Errore #%d: %s</string>\n    <string name=\"proxy_test_status\">Stato #%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">Indirizzo file di configurazione (*.ovpn)</string>\n    <string name=\"enable_openvpn\">Usa OpenVPN</string>\n    <string name=\"openvpn_settings_title\">Apri Impostazioni VPN</string>\n    <string name=\"openvpn_application_aborted\">Correggi prima le impostazioni di OpenVPN.</string>\n    <string name=\"openvpn_test_aborted\">Test interrotto, correggi prima le impostazioni di OpenVPN.</string>\n    <string name=\"openvpn_address_invalid\">Indirizzo di configurazione OpenVPN non impostato.</string>\n    <string name=\"internet_censorship\">Censura su Internet</string>\n    <string name=\"rename_section\">Rinomina questa sezione</string>\n    <string name=\"simple_edit_value_hint\">Immetti valore</string>\n    <string name=\"seek_interval\">Cerca intervallo</string>\n    <string name=\"seek_interval_sec\">%s sec</string>\n    <string name=\"subtitle_system\">Stile del sistema (Impostazioni Android &gt; Accessibilità)</string>\n    <string name=\"subtitle_scale\">Scala dei sottotitoli</string>\n    <string name=\"audio_sync_fix_desc\">Un modo alternativo per sincronizzare audio/video. È considerato obsoleto, ma, in alcuni casi, può aiutare.</string>\n    <string name=\"audio_sync_fix\">Correggi sincronizzazione audio</string>\n    <string name=\"ambilight_ratio_fix_desc\">Correggi illuminazione bias assente. Correggi proporzioni errate o scala del video. Correggi screenshot vuoti. Influisce sulle prestazioni!</string>\n    <string name=\"ambilight_ratio_fix\">Correggi Ambilight/Proporzioni/Scala video/Screenshot</string>\n    <string name=\"force_legacy_codecs_desc\">Migliora significativamente le prestazioni sui dispositivi di fascia bassa. La risoluzione massima è 720p.</string>\n    <string name=\"force_legacy_codecs\">Forza codec obsoleti (720p)</string>\n    <string name=\"live_stream_fix_desc\">Attenzione, questa modifica disabilita il riavvolgimento dei flussi. Migliora significativamente le prestazioni delle trasmissioni live sui dispositivi di fascia bassa. La risoluzione massima è 1080p.</string>\n    <string name=\"live_stream_fix_4k_desc\">Attenzione, questa modifica disabilita il riavvolgimento dei flussi. Migliora significativamente le prestazioni delle trasmissioni live. La risoluzione massima è 4K.</string>\n    <string name=\"live_stream_fix\">Correggi trasmissione live (1080p)</string>\n    <string name=\"live_stream_fix_4k\">Correggi trasmissione live (4K)</string>\n    <string name=\"playback_notifications_fix_desc\">Nasconde le notifiche di cambio traccia. Potrebbe essere utile su firmware basati su AOSP.</string>\n    <string name=\"playback_notifications_fix\">Disabilita le notifiche di riproduzione</string>\n    <string name=\"amlogic_fix_desc\">Correggi frame drop su dispositivi basati su Amlogic.</string>\n    <string name=\"amlogic_fix\">Correggi Amlogic 1080p\\@60fps</string>\n    <string name=\"tunneled_video_playback_desc\">NOTA: la pausa potrebbe non funzionare correttamente! La riproduzione video tunneled promette vantaggi come una migliore sincronizzazione audio/video (AV sync) e una riproduzione più fluida. Richiesto Android 5+</string>\n    <string name=\"tunneled_video_playback\">Riproduzione video con tunnel (Android 5+)</string>\n    <string name=\"master_volume\">Volume principale</string>\n    <string name=\"volume_limit\">Limite volume</string>\n    <string name=\"player_volume\">Volume</string>\n    <string name=\"play_video\">Riproduci</string>\n    <string name=\"remember_position_of_short_videos\">Ricorda la posizione dei video brevi (meno di 5 minuti)</string>\n    <string name=\"remember_position_of_live_videos\">Ricorda la posizione dei video live</string>\n    <string name=\"player_show_tooltips\">Mostra le descrizioni dei pulsanti</string>\n    <string name=\"action_like_unset\">rimosso Mi piace</string>\n    <string name=\"action_dislike_unset\">rimosso Non mi piace</string>\n    <string name=\"various_buttons\">Pulsanti nella parte superiore della finestra principale</string>\n    <string name=\"not_compatible_with\">L\\'opzione selezionata non è compatibile con</string>\n    <string name=\"subtitle_position\">Spostamento in basso dei sottotitoli</string>\n    <string name=\"pressing_home\">premendo HOME</string>\n    <string name=\"pressing_home_back\">premendo HOME o INDIETRO</string>\n    <string name=\"pressing_back\">premendo INDIETRO</string>\n    <string name=\"player_number_key_seek\">Ricerca con i tasti numerici</string>\n    <string name=\"save_remove_playlist\">Aggiungi/Rimuovi playlist dalla sezione Playlist</string>\n    <string name=\"save_playlist\">Aggiungi la playlist alla sezione Playlist</string>\n    <string name=\"remove_playlist\">Rimuovi definitivamente la playlist dalla sezione Playlist</string>\n    <string name=\"remove_playlist_fmt\">Rimuovere %s definitivamente?</string>\n    <string name=\"removed_from_playlists\">Rimossa dalla sezione Playlist</string>\n    <string name=\"saved_to_playlists\">Aggiunta alla sezione Playlist</string>\n    <string name=\"create_playlist\">Crea playlist</string>\n    <string name=\"create_playlist_note\">NOTA: non verrà visualizzato nell\\'app YouTube</string>\n\t<string name=\"add_video_to_new_playlist\">Aggiungi a nuova playlist</string>\n    <string name=\"cant_delete_empty_playlist\">Impossibile eliminare la playlist vuota</string>\n    <string name=\"playlist\">Playlist</string>\n    <string name=\"rename_playlist\">Rinomina playlist</string>\n    <string name=\"cant_rename_empty_playlist\">Non posso rinominare una playlist vuota</string>\n    <string name=\"cant_rename_foreign_playlist\">Non posso rinominare una playlist esterna</string>\n    <string name=\"cant_save_playlist\">Questa playlist non può essere aggiunta</string>\n    <string name=\"enter_value\">Immetti un valore non vuoto</string>\n    <string name=\"dialog_add_remove_from\">Aggiungi/Rimuovi da %s</string>\n    <string name=\"lb_playback_controls_skip_next\">Salta al prossimo</string>\n    <string name=\"lb_playback_controls_skip_previous\">Salta al precedente o ritorna alla posizione di partenza</string>\n    <string name=\"alt_presets_behavior\">Comportamento preset alternativi (limita la larghezza di banda)</string>\n    <string name=\"alt_presets_behavior_desc\">L\\'app cercherà di mantenere la larghezza di banda corrispondente al preset selezionato invece di corrispondere tra risoluzione, fps e codec.</string>\n    <string name=\"sleep_timer\">Timer di spegnimento (se il telecomando non viene usato per un\\'ora)</string>\n    <string name=\"sleep_timer_desc\">Metti in pausa la riproduzione se l\\'utente non ha usato il telecomando per un\\'ora</string>\n    <string name=\"disable_vsync\">Disabilita lo snap a vsync</string>\n    <string name=\"disable_vsync_desc\">Questa opzione disabilita l\\'allineamento dei fotogrammi con il segnale di sincronizzazione verticale del display. Questo potrebbe migliorare le prestazioni sui dispositivi di fascia bassa rilasciando risorse della CPU.</string>\n    <string name=\"skip_codec_profile_check\">Salta il controllo del livello del profilo del codec</string>\n    <string name=\"skip_codec_profile_check_desc\">Non controllare il supporto del codec quando avvii un video. Potrebbe essere utile su firmware con bug.</string>\n    <string name=\"force_sw_codec\">Forza decodificatore video SW</string>\n    <string name=\"force_sw_codec_desc\">Potrebbe riprodurre quasi tutti i video, ma con prestazioni molto scarse.</string>\n    <string name=\"playlist_order\">Ordina la playlist</string>\n    <string name=\"playlist_order_added_date_newer_first\">Data di aggiunta (prima i più recenti)</string>\n    <string name=\"playlist_order_added_date_older_first\">Data di aggiunta (prima i più vecchi)</string>\n    <string name=\"playlist_order_published_date_newer_first\">Data di pubblicazione (prima i più recenti)</string>\n    <string name=\"playlist_order_published_date_older_first\">Data di pubblicazione (prima i più vecchi)</string>\n    <string name=\"playlist_order_popularity\">Popolarità</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">Non posso fare questo per le playlist esterne</string>\n    <string name=\"owned_playlist_warning\">Errore: questa azione può essere applicata solo alle playlist di tua proprietà.</string>\n    <string name=\"content_block_alt_server\">Usa server alternativo</string>\n    <string name=\"content_block_alt_server_desc\">Abilita questa opzione se SponsorBlock si rifiuta di lavorare per motivi diversi.</string>\n    <string name=\"unset_stream_reminder\">Disabilita l\\'impostazione del promemoria della trasmissione</string>\n    <string name=\"set_stream_reminder\">Imposta promemoria trasmissione</string>\n    <string name=\"playback_starts_shortly\">La riproduzione inizierà automaticamente quando la trasmissione sarà pronto</string>\n    <string name=\"starting_stream\">Sto avviando lo stream…</string>\n    <string name=\"action_playlist_remove\">Rimuovi dalla playlist</string>\n    <string name=\"signin_view_title\">Sto caricando il codice utente…</string>\n    <string name=\"signin_view_description\">Per accedere inserisci questo codice nella pagina %s\\nNOTA. Funziona solo con browser Firefox o Chrome.</string>\n    <string name=\"signin_view_action_text\">Fatto</string>\n    <string name=\"require_checked\">Richiede \\\"%s\\\"</string>\n    <string name=\"msg_press_again_to_exit\">Premi di nuovo per uscire</string>\n    <string name=\"player_remaining_time\">Rimanente: %s</string>\n    <string name=\"player_ending_time\">Termina a %s</string>\n    <string name=\"badge_new_content\">NUOVO CONTENUTO</string>\n    <string name=\"badge_live\">DAL VIVO</string>\n    <string name=\"add_device_view_description\">Per collegare il dispositivo, inserisci questo codice nell\\'app YouTube del tuo telefono nella sezione Impostazioni/Guarda sulla TV\\nNOTA: funziona solo con la tastiera Gboard.</string>\n    <string name=\"playback_controls_repeat_pause\">Ripeti pausa</string>\n    <string name=\"playback_controls_repeat_list\">Ripeti elenco</string>\n    <string name=\"action_subscribe_off\">Iscrizione non attivata</string>\n    <string name=\"action_subscribe_on\">Iscrizione attivata</string>\n    <string name=\"skip_24_rate\">Salta i formati a 24 fps (correzione per la modalità cinema reale\\?)</string>\n    <string name=\"skip_shorts\">Salta i video brevi</string>\n    <string name=\"player_disable_suggestions\">Disabilita suggerimenti</string>\n    <string name=\"suggestions\">Suggerimenti</string>\n    <string name=\"feedback\">Feedback</string>\n    <string name=\"sources\">Sorgenti</string>\n    <string name=\"releases\">Rilasci</string>\n    <string name=\"prefer_avc_over_vp9\">Selezione codec: preferisci avc a vp9</string>\n    <string name=\"open_chat\">Chat/Commenti</string>\n    <string name=\"open_comments\">Apri i commenti</string>\n    <string name=\"place_chat_left\">Posiziona la chat a sinistra</string>\n    <string name=\"place_comments_left\">Posiziona i commenti a sinistra</string>\n    <string name=\"use_alt_speech_recognizer\">Metodo alternativo per riconoscimento vocale</string>\n    <string name=\"time_format\">Formato orario</string>\n    <string name=\"time_format_24\">24 ore</string>\n    <string name=\"time_format_12\">12 ore (AM/PM)</string>\n    <string name=\"use_alt_speech_recognizer_desc\">Con molti bug. Usalo solo se hai un problema con il riconoscimento vocale predefinito.</string>\n    <string name=\"speech_recognizer\">Riconoscimento vocale</string>\n    <string name=\"speech_engine\">Motore di ricerca vocale</string>\n    <string name=\"speech_recognizer_system\">Sistema (preferito)</string>\n    <string name=\"speech_recognizer_external_1\">Esterno 1 (gravemente difettato, per funzionare è necessario disabilitare l\\'accesso al microfono)</string>\n    <string name=\"speech_recognizer_external_2\">Esterno 2 (seriamente difettato)</string>\n    <string name=\"real_channel_icon\">Mostra icona sul pulsante del canale</string>\n    <string name=\"subtitle_yellow_semi_transparent\">Giallo su sfondo semitrasparente</string>\n    <string name=\"subtitle_yellow_black\">Giallo su sfondo nero</string>\n    <string name=\"player_pixel_ratio\">Rapporto pixel</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">Grigio scuro (monocromatico)</string>\n    <string name=\"disable_mic_permission\">Disabilita l\\'accesso al microfono sull\\'app affinché il riconoscimento vocale funzioni correttamente.</string>\n    <string name=\"repeat_mode_shuffle\">Mescola qualsiasi playlist</string>\n    <string name=\"chat_left\">Sinistra</string>\n    <string name=\"chat_right\">Destra</string>\n    <string name=\"card_real_thumbnails\">Sostituisci le miniature con un fotogramma del video</string>\n    <string name=\"card_content\">Dove prendere le miniature delle cartoline</string>\n    <string name=\"thumb_quality_default\">Predefinito</string>\n    <string name=\"thumb_quality_start\">Inizio del video</string>\n    <string name=\"thumb_quality_middle\">Metà del video</string>\n    <string name=\"thumb_quality_end\">Fine del video</string>\n    <string name=\"content_block_status\">Controlla lo stato del server di SponsorBlock</string>\n    <string name=\"dearrow_status\">Controlla lo stato del server di DeArrow</string>\n    <string name=\"player_show_quality_info_bitrate\">Aggiungi bitrate nelle informazioni sulla qualità</string>\n    <string name=\"player_speed_button_old_behavior\">Ripristina il vecchio comportamento del pulsante di velocità</string>\n    <string name=\"protect_settings_with_password\">Proteggi tutte le Impostazioni con una password</string>\n    <string name=\"enter_settings_password\">Immetti password per le Impostazioni</string>\n    <string name=\"child_mode\">Modalità bambino</string>\n    <string name=\"child_mode_desc\">In questa modalità, l\\'utente non può usare la ricerca o visualizzare alcun contenuto suggerito. La pagina delle impostazioni sarà protetta da password.</string>\n    <string name=\"lost_setting_warning\">Le impostazioni dell\\'app verranno modificate. Assicurati di aver creato un backup delle impostazioni.</string>\n    <string name=\"player_button_long_click\">Premi a lungo questo pulsante per ulteriori opzioni</string>\n    <string name=\"pause_history\">Metti in pausa la Cronologia</string>\n    <string name=\"resume_history\">Riprendi la Cronologia</string>\n    <string name=\"disable_history\">Disabilita la Cronologia</string>\n    <string name=\"enable_history\">Abilita la Cronologia</string>\n    <string name=\"clear_history\">Cancella la Cronologia</string>\n    <string name=\"chapters\">Capitoli</string>\n    <string name=\"card_multiline_subtitle\">Sottotitoli multiriga</string>\n    <string name=\"auto_frame_rate_modes\">Modalità supportate</string>\n    <string name=\"loading\">Caricamento in corso…</string>\n    <string name=\"hide_streams\">Nascondi le trasmissioni dalle iscrizioni</string>\n    <string name=\"video_rotate\">Ruota</string>\n    <string name=\"video_flip\">Capovolgi (specchio)</string>\n    <string name=\"trending_searches\">Ricerca per tendenze</string>\n    <string name=\"video_duration_any\">Qualsiasi</string>\n    <string name=\"video_duration\">Durata</string>\n    <string name=\"video_duration_under_4\">Meno di 4 minuti</string>\n    <string name=\"video_duration_between_4_20\">4-20 minuti</string>\n    <string name=\"video_duration_over_20\">Più di 20 minuti</string>\n    <string name=\"content_type\">Tipo</string>\n    <string name=\"content_type_any\">Qualsiasi</string>\n    <string name=\"content_type_video\">Video</string>\n    <string name=\"content_type_channel\">Canale</string>\n    <string name=\"content_type_playlist\">Playlist</string>\n    <string name=\"content_type_movie\">Film</string>\n    <string name=\"video_features\">Caratteristiche</string>\n    <string name=\"video_feature_any\">Qualsiasi</string>\n    <string name=\"video_feature_live\">Live</string>\n    <string name=\"video_feature_4k\">4K</string>\n    <string name=\"video_feature_hdr\">HDR</string>\n    <string name=\"search_sorting\">Ordina per</string>\n    <string name=\"sort_by_relevance\">Rilevanza</string>\n    <string name=\"sort_by_views\">Numero di visualizzazioni</string>\n    <string name=\"sort_by_date\">Data di caricamento</string>\n    <string name=\"sort_by_rating\">Valutazione</string>\n    <string name=\"clear_search_history\">Cancella la Cronologia delle ricerche</string>\n    <string name=\"remove_from_subscriptions\">Nascondi</string>\n    <string name=\"player_long_speed_list\">Più velocità nell\\'elenco delle velocità</string>\n    <string name=\"player_extra_long_speed_list\">Ancora più velocità nell\\'elenco delle velocità</string>\n    <string name=\"alt_app_icon\">Icona alternativa dell\\'app (è necessario riavviare)</string>\n    <string name=\"network_stack\">Preferisci stack di rete %s</string>\n    <string name=\"player_network_stack\">Motore di rete</string>\n    <string name=\"cronet_desc\">Cronet è lo stack di rete di Chromium. Cronet può ridurre la latenza e aumentare le prestazioni di rete, il che può aiutare con problemi di buffering.</string>\n    <string name=\"unlock_all_formats\">Sblocca tutti i formati video</string>\n    <string name=\"unlock_all_formats_desc\">Su alcuni dispositivi il firmware riporta erroneamente alcuni formati come non supportati, anche se lo sono.\\n(ad esempio, le smart TV 1080p tendono a segnalare 4k come non supportato).</string>\n    <string name=\"okhttp_desc\">Questo motore di rete è il più lento ma ha una buona stabilità.</string>\n    <string name=\"select_channel_section\">Apri la sezione corrispondente quando l\\'app è stata avviata dai canali ATV</string>\n    <string name=\"enable_voice_search_desc\">L\\'app ufficiale verrà sostituita da un cosiddetto \\\"bridge\\\". Un bridge aiuta a trasferire le richieste di ricerca globali alla nostra app.</string>\n    <string name=\"enable_master_password\">Abilita password principale.</string>\n    <string name=\"enter_master_password\">Immetti password principale</string>\n    <string name=\"disable_stream_buffer\">Disabilita buffer sui flussi</string>\n    <string name=\"disable_stream_buffer_desc\">Correggi situazioni in cui il flusso è troppo indietro. Nota: il flusso potrebbe iniziare a rallentare.</string>\n    <string name=\"sony_frame_drop_fix\">Correggi salto fotogrammi #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">Correggi ritardi sulle TV Sony e su alcuni altri dispositivi. Nota: possibili problemi con la sincronizzazione audio.</string>\n    <string name=\"audio_language\">Lingua audio</string>\n    <string name=\"old_home_look\">Il vecchio aspetto della sezione Home</string>\n    <string name=\"old_channel_look\">Il vecchio aspetto della pagina del canale</string>\n    <string name=\"hide_shorts_everywhere\">Nascondi i video brevi ovunque</string>\n    <string name=\"update_found\">Aggiornamento</string>\n    <string name=\"volume_boost_warning\">Qualsiasi impostazione oltre il limite può potenzialmente danneggiare gli altoparlanti</string>\n    <string name=\"play_video_incognito\">Riproduci in incognito</string>\n    <string name=\"play_from_start\">Riproduci dall\\'inizio</string>\n    <string name=\"header_kids_home\">Ragazzi</string>\n    <string name=\"player_section_playlist\">Usa i contenuti della sezione attuale come una playlist</string>\n    <string name=\"header_trending\">Tendenze</string>\n    <string name=\"screensaver\">Screensaver</string>\n    <string name=\"player_screen_off_timeout\">Timeout di spegnimento dello schermo</string>\n    <string name=\"old_update_notifications\">Il vecchio aspetto delle notifiche di aggiornamento</string>\n    <string name=\"dialog_notification\">Notifica aggiornamenti con una finestra di dialogo pop-up</string>\n    <string name=\"mark_as_watched\">Contrassegna come già visto</string>\n    <string name=\"player_ui_animations\">Abilita le animazioni dell\\'interfaccia utente del lettore</string>\n    <string name=\"player_likes_count\">Mostra il conteggio dei Mi piace/Non mi piace</string>\n    <string name=\"sorting_alphabetically2\">In ordine alfabetico (anteprime veloci e animate)</string>\n    <string name=\"sorting_default\">Predefinito (anteprime veloci e animate)</string>\n    <string name=\"content_block_exclude_channel\">Escludi questo canale da SponsorBlock</string>\n    <string name=\"content_block_stop_excluding_channel\">Non escludere più questo canale da SponsorBlock</string>\n    <string name=\"player_chapter_notification\">Avanza velocemente tra i capitoli usando la notifica popup</string>\n    <string name=\"player_chapter_notification2\">Passa al capitolo successivo cliccando sulla notifica</string>\n    <string name=\"subtitle_remember\">Abilita i sottotitoli solo sul canale attuale</string>\n    <string name=\"amazon_frame_drop_fix\">Correggi salto fotogrammi #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">Destinato ai dispositivi Amazon Stick. Può funzionare anche su altri dispositivi.</string>\n    <string name=\"default_stack_desc\">Motore di rete integrato. Questo motore può avere una migliore stabilità in determinate situazioni.</string>\n    <string name=\"item_postion\">Posizione di</string>\n    <string name=\"player_screen_off_dimming\">Livello di oscuramento a schermo spento</string>\n    <string name=\"screensaver_timout\">Timeout screensaver</string>\n    <string name=\"screensaver_dimming\">Livello di oscuramento dello screensaver</string>\n    <string name=\"player_ui_on_next\">Mostra l\\'interfaccia utente del lettore quando passi al video successivo</string>\n    <string name=\"autogenerated\">generato automaticamente</string>\n    <string name=\"player_auto_volume\">Regolazione automatica del volume</string>\n    <string name=\"header_shorts\">Video brevi</string>\n    <string name=\"player_global_focus\">Sincronizza lo stato attivo tra le file dei pulsanti del lettore</string>\n    <string name=\"auto_history\">Automatico (usa impostazioni dell\\'account)</string>\n    <string name=\"remember_position_subscriptions\">Ricorda l\\'ultima posizione visualizzata nelle Iscrizioni</string>\n    <string name=\"remember_position_pinned\">Ricorda l\\'ultima posizione visualizzata nelle playlist appuntate</string>\n    <string name=\"msg_player_unknown_error\">Errore sconosciuto</string>\n    <string name=\"unknown_source_error\">Errore sconosciuto della sorgente</string>\n    <string name=\"unknown_renderer_error\">Errore sconosciuto del renderer</string>\n    <string name=\"header_notifications\">Notifiche</string>\n    <string name=\"disable_search_history\">Disabilita la Cronologia delle ricerche</string>\n    <string name=\"unlock_high_bitrate_formats\">Sblocca formati vp9 1080p ad alto bitrate</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">Sblocca formati mp4a 1080p ad alto bitrate</string>\n    <string name=\"color_scheme_blue\">Blu</string>\n    <string name=\"color_scheme_dark_blue\">Blu scuro</string>\n    <string name=\"player_speed_per_channel\">Per ciascun canale</string>\n    <string name=\"disable_popular_searches\">Disabilita le query di ricerca più popolari</string>\n    <string name=\"multi_profiles\">Usa impostazioni separate per ciascun account</string>\n    <string name=\"protect_account_with_password\">Proteggi questo account con una password</string>\n    <string name=\"enter_account_password\">Inserisci la password dell\\'account</string>\n    <string name=\"show_connect_messages\">Mostra messaggi di connessione</string>\n    <string name=\"prefer_avc_over_vp9_desc\">ATTENZIONE: massimo 1080p</string>\n    <string name=\"play_next\">Riproduci successivo</string>\n    <string name=\"hide_watched_from_subscriptions\">Nascondi i video già visti dalle Iscrizioni</string>\n    <string name=\"hide_watched_from_notifications\">Nascondi i video già visti dalle Notifiche</string>\n    <string name=\"hide_unwanted_content\">Nascondi il contenuto</string>\n    <string name=\"hide_watched_from_home\">Nascondi i video già visti dalla Home</string>\n    <string name=\"hide_watched_from_watch_later\">Nascondi i video già visti dalla playlist Guarda più tardi</string>\n    <string name=\"remote_control_permission\">Il controllo remoto in background richiede l\\'autorizzazione alla sovrapposizione</string>\n    <string name=\"login_from_browser\">Accedi dal browser</string>\n    <string name=\"disable_remote_history\">Disabilita la Cronologia quando si usa il controllo remoto</string>\n    <string name=\"keyboard_fix\">Impedisci al pulsante OK di aprire la tastiera (G20 e altri)</string>\n    <string name=\"nothing_found\">Nessun risultato trovato</string>\n    <string name=\"auto_frame_rate_desc\">Questa opzione rimuove il tremolio nelle scene in cui la telecamera si muove velocemente, ad esempio nello sport</string>\n    <string name=\"channel_filter_hint\">Filtra canali</string>\n    <string name=\"player_loop_shorts\">Ripeti continuamente i video brevi</string>\n    <string name=\"player_quick_shorts_skip\">Salta i video brevi con il tasto sinistro/destro</string>\n\t<string name=\"player_quick_shorts_skip_alt\">Salta i video brevi con il tasto su/giù</string>\n    <string name=\"player_quick_skip_videos\">Salta i video normali con il tasto sinistro/destro</string>\n    <string name=\"player_quick_skip_videos_alt\">Salta i video normali con il tasto su/giù</string>\n\t<string name=\"channels_filter\">Mostra il campo Filtra canali nella sezione Canali</string>\n    <string name=\"channel_search_bar\">Barra di ricerca all\\'interno della pagina del Canale</string>\n    <string name=\"header_sports\">Sport</string>\n    <string name=\"keep_finished_activities\">Mantieni le attività terminate</string>\n    <string name=\"disable_channels_service\">Disattiva il servizio Canali</string>\n    <string name=\"replace_titles\">Sostituisci i titoli</string>\n    <string name=\"enable\">Abilita</string>\n    <string name=\"more_info\">Ulteriori informazioni</string>\n    <string name=\"about_sponsorblock\">Informazioni su SponsorBlock</string>\n    <string name=\"about_dearrow\">Informazioni su DeArrow</string>\n    <string name=\"replace_thumbnails\">Sostituisci le miniature</string>\n    <string name=\"crowdsourced_thumbnails\">Miniature in crowdsourcing</string>\n    <string name=\"crowdsoursed_titles\">Titoli in crowdsourcing</string>\n    <string name=\"dearrow_not_submitted_thumbs\">Origine della miniatura (se non è presente alcuna sottomissione a DeArrow)</string>\n    <string name=\"pitch_effect\">Effetto pitch</string>\n    <string name=\"fullscreen_mode\">Modalità a schermo intero (senza barre di sistema)</string>\n    <string name=\"player_only_mode\">Mostra solo il lettore se il video viene aperto all\\'esterno dell\\'app</string>\n    <string name=\"pinned_channel_rows\">Mostra il canale bloccato come righe</string>\n    <string name=\"prefer_google_dns\">Preferisci DNS di Google</string>\n    <string name=\"prefer_ipv4\">Preferisci DNS IPv4</string>\n    <string name=\"prefer_ipv4_desc\">Potrebbe risolvere situazioni in cui l\\'app non funziona affatto.\\nNota: potrebbe causare blocchi e arresti anomali (specialmente su dispositivi Android 8 o Dune HD)</string>\n    <string name=\"long_press_for_settings\">PREMI A LUNGO PER LE IMPOSTAZIONI</string>\n    <string name=\"long_press_for_options\">PREMI A LUNGO PER LE OPZIONI</string>\n    <string name=\"device_specific_backup\">Backup solo per questo dispositivo</string>\n    <string name=\"local_backup\">Backup locale</string>\n    <string name=\"auto_backup\">Backup automatico (una volta al giorno)</string>\n    <string name=\"repeat_mode_reverse_list\">Riproduci la playlist o i video dei canali in ordine inverso</string>\n    <string name=\"calm_msg\">Stiamo lavorando per risolvere il problema. Controlla regolarmente gli aggiornamenti</string>\n    <string name=\"without_picture\">Senza immagine</string>\n    <string name=\"video_disabled\">Video disabilitato</string>\n    <string name=\"applying_fix\">Non chiudere il lettore. Applicazione della correzione…</string>\n    <string name=\"hide_mixes\">Nascondi i mix</string>\n    <string name=\"player_global_focus_desc\">Questa funzione determina quale pulsante del lettore riceverà la messa a fuoco durante la navigazione tra le righe dei pulsanti del lettore</string>\n    <string name=\"disable_network_error_fixing\">Disabilita la correzione automatica degli errori di rete</string>\n    <string name=\"disable_network_error_fixing_desc\">Probabilmente dovrai abilitare questa opzione se stai usando una VPN</string>\n    <string name=\"recommended\">Raccomandato</string>\n    <string name=\"add_to_subscriptions_group\">Aggiungi/Rimuovi dal gruppo di iscrizioni</string>\n    <string name=\"new_subscriptions_group\">Nuovo gruppo</string>\n    <string name=\"rename_group\">Rinomina il gruppo di iscrizioni</string>\n    <string name=\"screen_dimming_amount\">Quantità oscuramento schermo</string>\n    <string name=\"screen_dimming_timeout\">Timeout oscuramento schermo</string>\n    <string name=\"playlists_rows\">Mostra la sezione Playlist come righe</string>\n    <string name=\"import_subscriptions_group\">Importa</string>\n    <string name=\"lb_playback_controls_closed_captioning_disable\">Disabilita i sottotitoli per non udenti</string>\n    <string name=\"lb_playback_controls_closed_captioning_enable\">Abilita i sottotitoli per non udenti</string>\n    <string name=\"my_videos\">I miei video</string>\n    <string name=\"player_audio_focus\">Messa a fuoco audio (pausa se vengono rilevati altri lettori)</string>\n    <string name=\"paid_content_notification\">Notifica di contenuti a pagamento</string>\n    <string name=\"you_liked\">piaciuto</string>\n    <string name=\"premium_users_only\">Solo utenti Premium. Correzione per l\\'elenco incompleto dei formati video</string>\n    <string name=\"playback_buffering_fix\">Correzione del buffering di riproduzione</string>\n    <string name=\"oculus_quest_fix\">Correzione per Oculus Quest</string>\n    <string name=\"card_preview_muted\">Video senza audio</string>\n    <string name=\"card_preview_full\">Video con audio</string>\n    <string name=\"card_preview\">Anteprima delle cartoline</string>\n    <string name=\"card_unlocalized_titles\">Titoli video non localizzati</string>\n    <string name=\"dont_resize_video_to_fit_dialog\">Non ridimensionare il video per adattarlo al dialogo</string>\n    <string name=\"typing_corrections\">Disattiva la correzione automatica durante la digitazione del testo</string>\n    <string name=\"suggestions_horizontally_scrolled\">Suggerimenti a scorrimento orizzontale</string>\n    <string name=\"stable_restore\">Ripristina la versione stabile (tutti i dati verranno eliminati)</string>\n    <string name=\"auto_backup_category\">Backup automatico</string>\n    <string name=\"once_a_day\">Una volta al giorno</string>\n    <string name=\"once_a_week\">Una volta alla settimana</string>\n    <string name=\"once_a_month\">Una volta al mese</string>\n    <string name=\"action_debug_info\">Info di debug</string>\n    <string name=\"dialog_block_channel\">Blocca il canale</string>\n    <string name=\"dialog_unblock_channel\">Sblocca il canale</string>\n    <string name=\"confirm_block_channel\">Nascondere tutto il contenuto da %s?</string>\n    <string name=\"channel_blocked\">Canale bloccato</string>\n    <string name=\"channel_unblocked\">Canale sbloccato</string>\n    <string name=\"header_blocked_channels\">Canali bloccati</string>\n    <string name=\"msg_no_blocked_channels\">Nessun canale bloccato</string>\n    <string name=\"player_time_stretching\">Dilatazione del tempo audio</string>\n    <string name=\"player_time_stretching_desc\">Mantiene la voce naturale quando si cambia velocità. Potrebbe ridurre significativamente le prestazioni su alcuni dispositivi.</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-iw/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\nFor hebrew, in particular, on some devices values-he is recognized. in some others it's values-iw.\nSo, make a copy of values-he and name it values-iw. Keep both in your structure.\n-->\n<resources>\n    <string name=\"header_home\">בית</string>\n    <string name=\"title_search\">חיפוש</string>\n    <string name=\"header_subscriptions\">מינויים</string>\n    <string name=\"header_history\">היסטוריה</string>\n    <string name=\"header_music\">מוזיקה</string>\n    <string name=\"header_news\">חדשות</string>\n    <string name=\"header_gaming\">משחקים</string>\n    <string name=\"header_playlists\">פלייליסטים</string>\n    <string name=\"header_settings\">הגדרות</string>\n    <string name=\"header_channels\">ערוצים</string>\n    <string name=\"subscriptions_signin_title\">ראה את הסרטונים הכי חדשים מהערוצים שאתה אוהב</string>\n    <string name=\"subscriptions_signin_subtitle\">היכנס כדי לראות את המינויים שלך</string>\n    <string name=\"subscriptions_signin_button_text\">היכנס</string>\n    <string name=\"library_signin_to_show_more\">צפה בסרטונים שאהבת, שמרת, או נרשמת כמנוי</string>\n    <string name=\"library_signin_subtitle\">היכנס כדי לראות את הספרייה שלך</string>\n    <string name=\"action_signin\">היכנס</string>\n    <string name=\"title_video_formats\">פורמטי וידאו</string>\n    <string name=\"title_audio_formats\">פורמטי שמע</string>\n    <string name=\"subtitle_category_title\">כתוביות</string>\n    <string name=\"video_max_quality\">אוטומטי (איכות מרבית)</string>\n    <string name=\"audio_max_quality\">אוטומטי (איכות מרבית)</string>\n    <string name=\"subtitles_disabled\">כתוביות מושבתות</string>\n    <string name=\"auto_frame_rate\">קצב פריימים אוט\\'</string>\n    <string name=\"frame_rate_correction\">תקן fps:\\n%s</string>\n    <string name=\"category_background_playback\">השמעת רקע</string>\n    <string name=\"not_implemented\">לא מיושם</string>\n    <string name=\"not_supported_by_device\">המכשיר לא תומך בתכונה זו</string>\n    <string name=\"option_background_playback_off\">מושבתת</string>\n    <string name=\"option_background_playback_pip\">תמונה בתוך תמונה</string>\n    <string name=\"option_background_playback_only_audio\">רק שמע</string>\n    <string name=\"playback_settings\">הגדרות איכות הפעלה</string>\n    <string name=\"video_speed\">מהירות סרטון</string>\n    <string name=\"resolution_switch\">החלף רזולוציה</string>\n    <string name=\"video_buffer_size_lowest\">הכי נמוך</string>\n    <string name=\"video_buffer_size_low\">נמוך</string>\n    <string name=\"video_buffer_size_med\">בינוני</string>\n    <string name=\"video_buffer_size_high\">גבוה</string>\n    <string name=\"video_buffer_size_highest\">הכי גבוה</string>\n    <string name=\"update_changelog\">יומן שינויים</string>\n    <string name=\"install_update\">התקן עדכון</string>\n    <string name=\"section_is_empty\">אופס! אין כאן כלום</string>\n    <string name=\"dialog_add_to_playlist\">הוסף/הסר מפלייליסט</string>\n    <string name=\"msg_signed_users_only\">משתמשים מחוברים בלבד</string>\n    <string name=\"msg_cant_load_content\">לא ניתן לטעון תוכן.\\n 1) הפעל היסטוריית צפייה.\\n 2) בדוק את התאריך והשעה במכשיר.\\n 3) בדוק חיבור רשת.\\n 4) בדוק את הגדרות הפרוקסי שלך.\\n 5) נסה להיכנס לחשבון שלך.\\n 6) אולי אין כאן כלום.</string>\n    <string name=\"title_video_presets\">קביעות וידאו מוגדרות מראש</string>\n    <string name=\"settings_accounts\">חשבונות</string>\n    <string name=\"settings_left_panel\">ערוך קטגוריות</string>\n    <string name=\"settings_themes\">ערכות נושא</string>\n    <string name=\"settings_other\">אחר</string>\n    <string name=\"settings_player\">הגדרות נגן</string>\n    <string name=\"settings_language\">שפה</string>\n    <string name=\"settings_linked_devices\">מכשירים מקושרים</string>\n    <string name=\"settings_about\">אודות</string>\n    <string name=\"dialog_account_list\">בחר חשבון</string>\n    <string name=\"dialog_account_none\">ללא</string>\n    <string name=\"dialog_remove_account\">צא</string>\n    <string name=\"dialog_add_account\">היכנס</string>\n    <string name=\"default_lang\">ברירת מחדל</string>\n    <string name=\"dialog_select_language\">שפה</string>\n    <string name=\"subtitle_default\">ברירת מחדל</string>\n    <string name=\"subtitle_white_semi_transparent\">לבן על רקע שקוף למחצה</string>\n    <string name=\"subtitle_style\">סגנון כתוביות</string>\n    <string name=\"subtitle_language\">שפת כתוביות</string>\n    <string name=\"action_search\">חיפוש</string>\n    <string name=\"settings_main_ui\">ממשק משתמש</string>\n    <string name=\"dialog_main_ui\">ממשק משתמש</string>\n    <string name=\"card_animated_previews\">תצוגות מקדימות מונפשות</string>\n    <string name=\"web_site\">אתר אינטרנט</string>\n    <string name=\"donation\">תרומה</string>\n    <string name=\"dialog_about\">אודות</string>\n    <string name=\"dialog_player_ui\">נגן וידאו</string>\n    <string name=\"player_show_ui_on_pause\">הצג ממשק משתמש בהשהיה</string>\n    <string name=\"player_pause_on_ok\">מקש אישור משהה את ההפעלה</string>\n    <string name=\"player_ok_button_behavior\">התנהגות מקש אישור</string>\n    <string name=\"player_only_ui\">ממשק משתמש בלבד</string>\n    <string name=\"player_ui_and_pause\">ממשק משתמש והשהיה</string>\n    <string name=\"player_only_pause\">השהיה בלבד</string>\n    <string name=\"check_for_updates\">בדוק אם קיימים עדכונים</string>\n    <string name=\"update_not_found\">אתה משתמש בגרסה העדכנית ביותר</string>\n    <string name=\"update_in_progress\">המתן…</string>\n    <string name=\"player_ui_hide_behavior\">הסתר אוטומטית את ממשק המשתמש</string>\n    <string name=\"option_never\">לעולם לא</string>\n    <string name=\"side_panel_sections\">הגדרת מדורים</string>\n    <string name=\"boot_to_section\">אתחול למדור</string>\n    <string name=\"large_ui\">ממשק משתמש גדול</string>\n    <string name=\"video_grid_scale\">קנה מידה של רשת וידאו</string>\n    <string name=\"scale_ui\">קנה מידה של ממשק משתמש</string>\n    <string name=\"color_scheme\">ערכת צבעים</string>\n    <string name=\"color_scheme_default\">ברירת מחדל</string>\n    <string name=\"color_scheme_red_grey\">אדום-אפור</string>\n    <string name=\"color_scheme_red\">אדום</string>\n    <string name=\"color_scheme_dark_grey\">אפור כהה</string>\n    <string name=\"disable_update_check\">השבת בדיקת עדכונים</string>\n    <string name=\"show_again\">הצג שוב</string>\n    <string name=\"check_updates_auto\">הודע על עדכונים</string>\n    <string name=\"select_account_on_boot\">הצג בחירת חשבון באתחול</string>\n    <string name=\"player_other\">שונות</string>\n    <string name=\"player_full_date\">תאריך מדויק בתיאור</string>\n    <string name=\"open_channel\">פתח ערוץ</string>\n    <string name=\"not_interested\">לא מעוניין</string>\n    <string name=\"you_wont_see_this_video\">הסרטון הוסר ממומלצים</string>\n    <string name=\"settings_search\">חיפוש</string>\n    <string name=\"dialog_search\">חיפוש</string>\n    <string name=\"instant_voice_search\">חיפוש קולי מיידי</string>\n    <string name=\"option_background_playback_behind\">הפעל מאחור</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">מידע על הסרטון הבא טרם נטען</string>\n    <string name=\"card_multiline_title\">כותרות מרובות שורות</string>\n    <string name=\"cards_style\">סגנון כרטיסים</string>\n    <string name=\"repeat_mode_all\">הפעל סרטונים ברציפות</string>\n    <string name=\"repeat_mode_one\">חזור על הסרטון הנוכחי</string>\n    <string name=\"repeat_mode_pause\">השהה את ההפעלה לאחר כל סרטון</string>\n    <string name=\"repeat_mode_none\">הפסק הפעלה לאחר סרטון אחד</string>\n    <string name=\"subscribed_to_channel\">מנוי</string>\n    <string name=\"unsubscribed_from_channel\">לא מנוי</string>\n    <string name=\"subtitle_yellow_transparent\">צהוב על רקע שקוף</string>\n    <string name=\"subtitle_white_black\">לבן על רקע שחור</string>\n    <string name=\"player_seek_preview\">תצוגה מקדימה תוך כדי דילוג</string>\n    <string name=\"color_scheme_dark_grey_oled\">אפור כהה (OLED)</string>\n    <string name=\"channels_section_sorting\">מיון מדור ערוצים</string>\n    <string name=\"sorting_by_new_content\">תוכן חדש</string>\n    <string name=\"sorting_alphabetically\">בסדר אלפביתי</string>\n    <string name=\"sorting_last_viewed\">נצפה לאחרונה</string>\n    <string name=\"player_pause_when_seek\">השהה בזמן דילוג</string>\n    <string name=\"playlists_style\">סגנון מדור פלייליסטים</string>\n    <string name=\"playlists_style_grid\">רשת</string>\n    <string name=\"playlists_style_rows\">שורות</string>\n    <string name=\"player_show_clock\">הצג שעון בסרגל הפקדים</string>\n    <string name=\"player_show_remaining_time\">הצג את הזמן הנותר בסרגל הפקדים</string>\n    <string name=\"player_show_ending_time\">הצג את שעת הסיום בסרגל הפקדים</string>\n    <string name=\"repeat_mode_pause_alt\">הפעל רק סרטוני פלייליסט ברציפות</string>\n    <string name=\"subtitle_white_transparent\">לבן על רקע שקוף</string>\n    <string name=\"open_channel_uploads\">פתח העלאות ערוץ</string>\n    <string name=\"app_exit_shortcut\">יציאה מהיישום</string>\n    <string name=\"player_exit_shortcut\">יציאה מהנגן</string>\n    <string name=\"app_exit_none\">אל תצא</string>\n    <string name=\"app_double_back_exit\">חזרה כפולה</string>\n    <string name=\"app_single_back_exit\">חזרה בודדת</string>\n    <string name=\"action_video_zoom\">זום סרטון</string>\n    <string name=\"video_zoom\">זום סרטון</string>\n    <string name=\"video_zoom_default\">ברירת מחדל</string>\n    <string name=\"video_zoom_fit_width\">התאם רוחב</string>\n    <string name=\"video_zoom_fit_height\">התאם גובה</string>\n    <string name=\"video_zoom_fit_both\">התאם רוחב או גובה</string>\n    <string name=\"video_zoom_stretch\">מתיחה</string>\n    <string name=\"color_scheme_teal\">ירוק-כחול</string>\n    <string name=\"color_scheme_teal_oled\">ירוק-כחול (OLED)</string>\n    <string name=\"player_seek_preview_none\">מושבתת</string>\n    <string name=\"player_seek_preview_single\">פריים בודד</string>\n    <string name=\"player_seek_preview_carousel\">קרוסלה</string>\n    <string name=\"player_seek_preview_carousel_slow\">קרוסלה (איטית, לפי תמונות מפתח)</string>\n    <string name=\"player_seek_preview_carousel_fast\">קרוסלה (מהירה, תצוגה מקדימה לא מדויקת)</string>\n    <string name=\"unsubscribe_from_channel\">בטל הרשמה מהערוץ</string>\n    <string name=\"card_title_lines_num\">מספר שורות כותרת הכרטיס</string>\n    <string name=\"card_auto_scrolled_title\">גלילה אוטומטית של כותרת חתוכה</string>\n    <string name=\"share_link\">שתף קישור</string>\n    <string name=\"share_qr_link\">שתף קישור (קוד QR)</string>\n    <string name=\"subscribe_to_channel\">הירשם לערוץ</string>\n    <string name=\"wait_data_loading\">אנא המתן בזמן שהנתונים נטענים…</string>\n    <string name=\"auto_frame_rate_pause\">קצב פריימים אוטומטי (השהיה בעת המעבר)</string>\n    <string name=\"auto_frame_rate_applying\">מחיל קצב פריימים אוט\\' %sx%s\\@%s</string>\n    <string name=\"audio_shift\">הזזת שמע</string>\n    <string name=\"player_remember_speed\">זכור מהירות</string>\n    <string name=\"channel_marked_as_watched\">הערוץ סומן כנצפה</string>\n    <string name=\"mark_channel_as_watched\">סמן כנצפה</string>\n    <string name=\"dialog_add_device\">הוסף מכשיר</string>\n    <string name=\"dialog_remove_all_devices\">הסר את כל המכשירים</string>\n    <string name=\"device_link_enabled\">הקישור מופעל</string>\n    <string name=\"device_connected\">המכשיר \\\"%s\\\" חובר</string>\n    <string name=\"device_disconnected\">המכשיר \\\"%s\\\" נותק</string>\n    <string name=\"settings_ui_scale\">קנה מידה של ממשק משתמש</string>\n    <string name=\"msg_restart_app\">נא להתחיל מחדש את היישום כדי להחיל הגדרות אלה</string>\n    <string name=\"settings_block\">חסימת תוכן</string>\n    <string name=\"msg_applying\">מחיל %s…</string>\n    <string name=\"msg_done\">בוצע</string>\n    <string name=\"settings_general\">כללי</string>\n    <string name=\"settings_video\">וִידֵאוֹ</string>\n    <string name=\"content_block_confirm_skip\">אשר בדילוג</string>\n    <string name=\"content_block_categories\">דלג על מקטעים</string>\n    <string name=\"content_block_sponsor\">נותן חסות</string>\n    <string name=\"content_block_intro\">הנפשת הפסקה/הקדמה</string>\n    <string name=\"content_block_interaction\">תזכורת לאינטראקציה (הרשמה למינוי)</string>\n    <string name=\"content_block_outro\">כרטיסי סיום/קרדיטים</string>\n    <string name=\"content_block_self_promo\">קידום ללא תשלום/עצמי</string>\n    <string name=\"content_block_music_off_topic\">מקטע שאינו מוזיקה בקליפ</string>\n    <string name=\"confirm_segment_skip\">לדלג על מקטע \\\"%s\\\"\\\\?</string>\n    <string name=\"cancel_segment_skip\">בטל דילוג על מקטע</string>\n    <string name=\"msg_skipping_segment\">מדלג על מקטע \\\"%s\\\"…</string>\n    <string name=\"content_block_notification_type\">סוג התראה</string>\n    <string name=\"content_block_notify_none\">ללא התראה</string>\n    <string name=\"content_block_notify_toast\">הודעה קופצת</string>\n    <string name=\"content_block_notify_dialog\">דו-שיח לאישור</string>\n    <string name=\"return_to_launcher\">חזור למשגר מתוך ערוצים/חיפוש של טלוויזית אנדרואיד</string>\n    <string name=\"intent_force_close\">כפה יציאה אם הסרטון נפתח ממקור חיצוני</string>\n    <string name=\"btn_confirm\">אישור</string>\n    <string name=\"player_low_video_quality\">איכות סרטון נמוכה</string>\n    <string name=\"remote_session_closed\">הפעלה מרוחקת נסגרה</string>\n    <string name=\"msg_mode_switch_error\">לא ניתן לעבור למצב תצוגה של \\\"%s\\\"</string>\n    <string name=\"msg_player_error\">אירעה שגיאת נגן %s. מתחיל מחדש את ההפעלה…</string>\n    <string name=\"video_preset_disabled\">ללא קביעה מוגדרת מראש</string>\n    <string name=\"video_preset_enabled\">פורמט הסרטון הבא יוגדר בהתאם לקביעה מוגדרת מראש</string>\n    <string name=\"player_sleep_timer\">טיימר שינה</string>\n    <string name=\"player_show_quality_info\">הצג מידע על איכות בסרגל הפקדים</string>\n    <string name=\"player_remember_each_speed\">זכור מהירות של כל סרטון</string>\n    <string name=\"player_remember_speed_none\">אף סרטון</string>\n    <string name=\"player_remember_speed_all\">זהה בכל הסרטונים</string>\n    <string name=\"player_remember_speed_each\">לכל סרטון</string>\n    <string name=\"msg_player_error_source\">לא ניתן להוריד את הסרטון</string>\n    <string name=\"msg_player_error_renderer\">הפורמט שנבחר אינו נתמך.\\nנסה לבחור אחד אחר.\\nאם זה לא יעזור נסה לאתחל מחדש את המכשיר.</string>\n    <string name=\"msg_player_error_video_renderer\">פורמט הוידאו שנבחר אינו נתמך.\\nנסה לבחור אחד אחר על ידי לחיצה על לחצן HQ בנגן.\\nאם זה לא יעזור נסה לאתחל מחדש את המכשיר.</string>\n    <string name=\"msg_player_error_audio_renderer\">פורמט השמע שנבחר אינו נתמך.\\nנסה לבחור אחד אחר על ידי לחיצה על לחצן HQ בנגן.\\nאם זה לא יעזור נסה לאתחל מחדש את המכשיר.</string>\n    <string name=\"msg_player_error_subtitle_renderer\">פורמט הכתוביות שנבחר אינו נתמך.\\nנסה לבחור אחד אחר על ידי לחיצה ארוכה על לחצן CC בנגן.\\nאם זה לא יעזור נסה לאתחל מחדש את המכשיר.</string>\n    <string name=\"msg_player_error_unexpected\">שגיאת מפענח וידאו לא ידועה</string>\n    <string name=\"video_aspect\">יחס גובה-רוחב</string>\n    <string name=\"player_tweaks\">אפשרויות מפתח</string>\n    <string name=\"header_uploads\">העלאות</string>\n    <string name=\"player_show_global_clock\">הצג שעון תמיד על המסך</string>\n    <string name=\"player_show_global_ending_time\">הצג את שעת הסיום תמיד על המסך</string>\n    <string name=\"app_corner_clock\">בית: שעון בפינה השמאלית העליונה</string>\n    <string name=\"player_corner_clock\">נגן: שעון בפינה הימנית העליונה</string>\n    <string name=\"player_corner_ending_time\">נגן: זמן סיום בפינה הימנית העליונה</string>\n    <string name=\"uploads_old_look\">החזר את המראה הישן של מדור ההעלאות</string>\n    <string name=\"background_playback_activation\">הפעלה ברקע (שפעול)</string>\n    <string name=\"channels_old_look\">החזר את המראה הישן של מדור הערוצים</string>\n    <string name=\"channels_auto_load\">טען אוטומטית את תוכן מדור הערוצים</string>\n    <string name=\"return_to_background_video\">חזור לסרטון שפועל ברקע</string>\n    <string name=\"pin_unpin_from_sidebar\">הוסף/הסר מסרגל הצד</string>\n    <string name=\"pin_unpin_playlist\">הוסף/הסר פלייליסט מסרגל הצד</string>\n    <string name=\"pin_unpin_channel\">הוסף/הסר ערוץ מסרגל הצד</string>\n    <string name=\"pin_playlist\">הוסף פלייליסט לסרגל הצד</string>\n    <string name=\"pin_channel\">הוסף ערוץ לסרגל הצד</string>\n    <string name=\"pinned_to_sidebar\">נוסף לסרגל הצד</string>\n    <string name=\"unpin_from_sidebar\">הסר מסרגל הצד</string>\n    <string name=\"unpin_group_from_sidebar\">הסר מקבוצת המינויים</string>\n    <string name=\"unpinned_from_sidebar\">הוסר מסרגל הצד</string>\n    <string name=\"dialog_select_country\">מדינה</string>\n    <string name=\"settings_language_country\">שפה/מדינה</string>\n    <string name=\"share_embed_link\">שתף קישור להטמעה</string>\n    <string name=\"card_text_scroll_factor\">מהירות גלילת טקסט של כרטיס</string>\n    <string name=\"preferred_update_source\">בחר מקור עדכון</string>\n    <string name=\"hide_shorts\">הסתר Shorts מהמינויים</string>\n    <string name=\"hide_shorts_channel\">הסתר Shorts מערוץ</string>\n    <string name=\"key_remapping\">מיפוי מחדש של מקשים</string>\n    <string name=\"screen_dimming\">עמעום מסך</string>\n    <string name=\"removed_from_playback_queue\">הוסר מתור ההפעלה</string>\n    <string name=\"added_to_playback_queue\">נוסף לתור ההפעלה</string>\n    <string name=\"add_remove_from_playback_queue\">הוסף/הסר מתור ההפעלה</string>\n    <string name=\"add_to_playback_queue\">הוסף לתור ההפעלה</string>\n    <string name=\"remove_from_playback_queue\">הסר מתור ההפעלה</string>\n    <string name=\"proxy_enabled\">פרוקסי מופעל</string>\n    <string name=\"proxy_disabled\">פרוקסי מושבת</string>\n    <string name=\"uploads_row_name\">העלאות</string>\n    <string name=\"playlists_row_name\">פלייליסטים שנוצרו</string>\n    <string name=\"popular_uploads_row_name\">העלאות פופולריות</string>\n    <string name=\"news_row_name\">חדשות</string>\n    <string name=\"breaking_news_row_name\">מבזק חדשות</string>\n    <string name=\"covid_news_row_name\">חדשות קוביד-19</string>\n    <string name=\"enable_voice_search\">אפשר חיפוש גלובלי (דרושה תמיכת קושחה)</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">הירשם/בטל הרשמה לערוץ</string>\n    <string name=\"app_backup_restore\">גיבוי/שחזור</string>\n    <string name=\"app_backup\">גיבוי נתוני יישום</string>\n    <string name=\"app_restore\">שחזור נתוני יישום</string>\n    <string name=\"skip_each_segment_once\">אל תדלג על מקטעים בשנית</string>\n    <string name=\"disable_ok_long_press\">השבת לחיצה ארוכה של לחצן אישור</string>\n    <string name=\"disable_ok_long_press_desc\">מיועד לשלטים תקולים שבהם כפתור האישור אינו מתפקד כראוי</string>\n    <string name=\"player_time_correction\">הצג את השעה עם תיקון מהירות</string>\n    <string name=\"sponsor_color_markers\">סמני צבע בסרגל ההתקדמות</string>\n    <string name=\"refresh_section\">רענן מדור</string>\n    <string name=\"option_disabled\">מושבת</string>\n    <string name=\"live_now_row_name\">בשידור חי עכשיו</string>\n    <string name=\"run_in_background\">הפעלה ברקע</string>\n    <string name=\"settings_remote_control\">שליטה מרחוק</string>\n    <string name=\"background_service_started\">שירות רקע התחיל</string>\n    <string name=\"dialog_add_to\">הוסף ל-%s</string>\n    <string name=\"dialog_remove_from\">הסר מ-%s</string>\n    <string name=\"added_to\">נוסף ל-%s</string>\n    <string name=\"removed_from\">הוסר מ-%s</string>\n    <string name=\"video_preset_adaptive\">אדפטיבית</string>\n    <string name=\"content_block_preview_recap\">תצוגה מקדימה או סיכום של הסרטון</string>\n    <string name=\"content_block_highlight\">נקודה מעניינת (סימניה)</string>\n    <string name=\"removed_from_history\">הסרטון הוסר מההיסטוריה</string>\n    <string name=\"remove_from_history\">מחק מההיסטוריה</string>\n    <string name=\"upload_date\">תאריך העלאה</string>\n    <string name=\"upload_date_any\">כל הזמנים</string>\n    <string name=\"upload_date_today\">היום</string>\n    <string name=\"upload_date_this_week\">השבוע</string>\n    <string name=\"upload_date_this_month\">החודש</string>\n    <string name=\"upload_date_this_year\">השנה</string>\n    <string name=\"double_refresh_rate\">קצב רענון כפול</string>\n    <string name=\"upload_date_last_hour\">שעה אחרונה</string>\n    <string name=\"focus_on_search_results\">התמקד אוטומטית בתוצאות חיפוש</string>\n    <string name=\"context_menu\">תפריט הקשר</string>\n    <string name=\"context_menu_sorting\">מיון תפריט תלוי הקשר</string>\n    <string name=\"add_remove_from_recent_playlist\">הוסף/הסר מפלייליסט אחרון</string>\n    <string name=\"hide_settings_section\">הסתר מדור הגדרות (מסוכן!)</string>\n    <string name=\"volume\">נפח %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s שניות</string>\n    <string name=\"audio_shift_sec\">%s שניות</string>\n    <string name=\"ui_hide_timeout_sec\">%s שניות</string>\n    <string name=\"screen_dimming_timeout_min\">%s דקות</string>\n    <string name=\"mark_all_channels_watched\">סמן את כל הערוצים כנצפו</string>\n    <string name=\"move_section_up\">הזז מדור למעלה</string>\n    <string name=\"move_section_down\">הזז מדור למטה</string>\n    <string name=\"player_buttons\">הגדר לחצני נגן</string>\n    <string name=\"action_playlist_add\">הוסף לפלייליסט</string>\n    <string name=\"action_video_stats\">סטטיסטיקת סרטון</string>\n    <string name=\"action_subscribe\">הרשמה למינוי</string>\n    <string name=\"action_channel\">פתח ערוץ</string>\n    <string name=\"action_pip\">תמונה בתוך תמונה</string>\n    <string name=\"action_video_info\">תיאור סרטון</string>\n    <string name=\"action_screen_off\">מסך כבוי</string>\n    <string name=\"action_sound_off\">שמע כבוי</string>\n    <string name=\"action_playback_queue\">תור הפעלה</string>\n    <string name=\"action_video_speed\">מהירות סרטון</string>\n    <string name=\"action_subtitles\">כתוביות</string>\n    <string name=\"action_like\">אהבתי</string>\n    <string name=\"action_dislike\">דיסלייק</string>\n    <string name=\"action_play_pause\">הפעל/השהה</string>\n    <string name=\"action_repeat_mode\">מצב הפעלה</string>\n    <string name=\"action_next\">הסרטון הבא</string>\n    <string name=\"action_previous\">הסרטון הקודם</string>\n    <string name=\"action_high_quality\">איכות סרטון</string>\n    <string name=\"content_block_no_skipping_mode\">מצב ללא דילוגים</string>\n    <string name=\"content_block_action_type\">בחר פעולה</string>\n    <string name=\"content_block_action_none\">לא לעשות כלום</string>\n    <string name=\"content_block_action_only_skip\">רק לדלג</string>\n    <string name=\"content_block_action_toast\">דלג עם התראה</string>\n    <string name=\"content_block_action_dialog\">הצג דו-שיח לאישור</string>\n    <string name=\"content_block_filler\">מחוץ לנושא (פילר)</string>\n    <string name=\"keyboard_auto_show\">הצג מקלדת באופן אוטומטי</string>\n    <string name=\"cancel_dialog\">ביטול</string>\n    <string name=\"msg_player_error_source2\">כתובת URL לא עובדת או זמן מכשיר שגוי.\\nבדרך כלל, זה באג ביישום.</string>\n    <string name=\"msg_player_error_video_source\">כתובת URL של סרטון לא עובדת או זמן מכשיר שגוי.\\nבדרך כלל, זה באג ביישום.</string>\n    <string name=\"msg_player_error_audio_source\">כתובת URL של שמע לא עובדת או זמן מכשיר שגוי.\\nבדרך כלל, זה באג ביישום.</string>\n    <string name=\"msg_player_error_subtitle_source\">כתובת URL של כתוביות לא עובדת או זמן מכשיר שגוי.\\nבדרך כלל, זה באג ביישום.</string>\n    <string name=\"hide_upcoming\">הסתר בקרוב מהמינויים</string>\n    <string name=\"hide_upcoming_channel\">הסתר בקרוב מהערוצים</string>\n    <string name=\"hide_upcoming_home\">הסתר בקרוב מהבית</string>\n    <string name=\"search_background_playback\">הפעלה ברקע בזמן חיפוש/גלישה בערוץ</string>\n    <string name=\"trending_row_name\">מגמות</string>\n    <string name=\"player_seek_type\">התנהגות דילוג</string>\n    <string name=\"player_seek_regular\">רגילה</string>\n    <string name=\"player_seek_confirmation_pause\">עם אישור (השהה בזמן דילוג)</string>\n    <string name=\"player_seek_confirmation_play\">עם אישור (נגן תוך כדי דילוג)</string>\n    <string name=\"hide_shorts_from_home\">הסתר Shorts מהבית</string>\n    <string name=\"finish_on_disconnect\">סגור את היישום לאחר ניתוק הטלפון/טאבלט</string>\n    <string name=\"update_error\">שגיאת עדכון</string>\n    <string name=\"hide_shorts_from_search\">הסתר Shorts מתוצאות החיפוש</string>\n    <string name=\"hide_shorts_from_history\">הסתר Shorts מההיסטוריה</string>\n    <string name=\"hide_shorts_from_trending\">הסתר Shorts מהמגמות</string>\n    <string name=\"disable_screensaver\">השבת את שומר המסך</string>\n    <string name=\"description_not_found\">התיאור לא נמצא</string>\n    <string name=\"proxy_port_hint\">יציאת פרוקסי</string>\n    <string name=\"proxy_host_hint\">שם מארח פרוקסי או IP</string>\n    <string name=\"proxy_username_hint\">שם משתמש פרוקסי</string>\n    <string name=\"proxy_password_hint\">סיסמת פרוקסי</string>\n    <string name=\"proxy_type\">סוג פרוקסי</string>\n    <string name=\"enable_web_proxy\">השתמש בפרוקסי אינטרנט</string>\n    <string name=\"proxy_not_supported\">השתמש בפרוקסי אינטרנט (צריך אנדרואיד 4.4+)</string>\n    <string name=\"proxy_test_btn\">בדוק</string>\n    <string name=\"proxy_settings_title\">הגדרות שרת פרוקסי</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">בדיקה #%d: בוטלה.</string>\n    <string name=\"proxy_type_invalid\">סוג פרוקסי לא מוגדר.</string>\n    <string name=\"proxy_host_invalid\">מארח פרוקסי לא מוגדר.</string>\n    <string name=\"proxy_port_invalid\">יציאת פרוקסי לא חוקית, חייבת להיות &gt; 0.</string>\n    <string name=\"proxy_credentials_invalid\">שם משתמש או סיסמה לא חוקיים.</string>\n    <string name=\"proxy_test_aborted\">הבדיקה בוטלה, נא לתקן את הגדרות הפרוקסי קודם.</string>\n    <string name=\"proxy_application_aborted\">נא לתקן את הגדרות הפרוקסי קודם.</string>\n    <string name=\"proxy_test_start\">בדיקה #%d: %s …</string>\n    <string name=\"proxy_test_error\">שגיאה #%d: %s</string>\n    <string name=\"proxy_test_status\">מצב #%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">כתובת קובץ התצורה (*.ovpn)</string>\n    <string name=\"enable_openvpn\">השתמש ב-OpenVPN</string>\n    <string name=\"openvpn_settings_title\">OpenVPN הגדרות</string>\n    <string name=\"openvpn_application_aborted\">נא לתקן את הגדרות OpenVPN קודם.</string>\n    <string name=\"openvpn_test_aborted\">הבדיקה בוטלה, נא לתקן את הגדרות OpenVPN קודם.</string>\n    <string name=\"openvpn_address_invalid\">כתובת התצורה של OpenVPN לא מוגדרת.</string>\n    <string name=\"internet_censorship\">צנזורת אינטרנט</string>\n    <string name=\"rename_section\">שנה את שם מדור זה</string>\n    <string name=\"simple_edit_value_hint\">הזן ערך</string>\n    <string name=\"seek_interval\">מרווח דילוג</string>\n    <string name=\"seek_interval_sec\">%s שניות</string>\n    <string name=\"subtitle_system\">סגנון מערכת (הגדרות אנדרואיד &gt; נגישות)</string>\n    <string name=\"subtitle_scale\">קנה מידה של כתוביות</string>\n    <string name=\"audio_sync_fix_desc\">דרך חלופית לסנכרן שמע/וידאו. זו נחשבת למיושנת, אבל במקרים מסוימים, עשויה לעזור.</string>\n    <string name=\"audio_sync_fix\">תיקון סנכרון שמע</string>\n    <string name=\"ambilight_ratio_fix_desc\">מתקן תאורת הטיה נעדרת. מתקן יחס גובה-רוחב או קנה מידה שגויים של וידאו. מתקן צילומי מסך ריקים. משפיע על הביצועים!</string>\n    <string name=\"ambilight_ratio_fix\">תיקון תאורת אווירה/יחס גובה-רוחב/קנה מידה של וידאו/צילומי מסך</string>\n    <string name=\"force_legacy_codecs_desc\">משפר באופן משמעותי את הביצועים במכשירים ברמה נמוכה. הרזולוציה המרבית היא 720p.</string>\n    <string name=\"force_legacy_codecs\">כפה קודקים מדור קודם (720p)</string>\n    <string name=\"live_stream_fix_desc\">אזהרה, תיקון זה משבית את החזרה לאחור של שידורים. משפר באופן משמעותי את הביצועים של שידורים חיים במכשירים ברמה נמוכה. הרזולוציה המרבית היא 1080p.</string>\n    <string name=\"live_stream_fix_4k_desc\">אזהרה, תיקון זה משבית את החזרה לאחור של שידורים. משפר באופן משמעותי את הביצועים של שידורים חיים. הרזולוציה המרבית היא 4K.</string>\n    <string name=\"live_stream_fix\">תיקון שידור חי (1080p)</string>\n    <string name=\"live_stream_fix_4k\">תיקון שידור חי (4K)</string>\n    <string name=\"playback_notifications_fix_desc\">מסתיר התראות על שינוי רצועה. יכול להיות שימושי בקושחה מבוססת AOSP.</string>\n    <string name=\"playback_notifications_fix\">השבת התראות הפעלה</string>\n    <string name=\"amlogic_fix_desc\">תיקון נפילת פריימים במכשירים מבוססי Amlogic.</string>\n    <string name=\"amlogic_fix\">תיקון Amlogic 1080p\\@60fps</string>\n    <string name=\"tunneled_video_playback_desc\">הערה: ייתכן שההשהיה לא תפעל כראוי! הפעלת וידאו במנהרה מבטיחה יתרונות כגון סנכרון שמע/וידאו טוב יותר (סנכרון AV) והפעלה חלקה יותר. נדרש אנדרואיד 5+</string>\n    <string name=\"tunneled_video_playback\">הפעלת וידאו במנהרה (אנדרואיד 5+)</string>\n    <string name=\"master_volume\">עוצמת קול ראשית</string>\n    <string name=\"volume_limit\">מגבלת עוצמת קול</string>\n    <string name=\"player_volume\">עוצמת קול</string>\n    <string name=\"play_video\">הפעל</string>\n    <string name=\"remember_position_of_short_videos\">זכור את המיקום של סרטונים Shorts (פחות מ-5 דקות)</string>\n    <string name=\"remember_position_of_live_videos\">זכור את המיקום של שידורים חיים</string>\n    <string name=\"player_show_tooltips\">הצג תיאורי כלים של לחצנים</string>\n    <string name=\"action_like_unset\">אהבתי הוסר</string>\n    <string name=\"action_dislike_unset\">דיסלייק הוסר</string>\n    <string name=\"various_buttons\">לחצנים בחלק העליון של החלון הראשי</string>\n    <string name=\"not_compatible_with\">האפשרות שנבחרה אינה תואמת את</string>\n    <string name=\"subtitle_position\">הזזת תחתית כתוביות</string>\n    <string name=\"pressing_home\">על ידי לחיצה על HOME</string>\n    <string name=\"pressing_home_back\">על ידי לחיצה על HOME או BACK</string>\n    <string name=\"pressing_back\">על ידי לחיצה על BACK</string>\n    <string name=\"player_number_key_seek\">דלג עם מקשי מספרים</string>\n    <string name=\"save_remove_playlist\">הוסף/הסר פלייליסט ממדור הפלייליסטים</string>\n    <string name=\"save_playlist\">הוסף פלייליסט למדור הפלייליסטים</string>\n    <string name=\"remove_playlist\">הסר פלייליסט ממדור הפלייליסטים לצמיתות</string>\n    <string name=\"removed_from_playlists\">הוסר ממדור הפלייליסטים</string>\n    <string name=\"saved_to_playlists\">נוסף למדור הפלייליסטים</string>\n    <string name=\"create_playlist\">צור פלייליסט</string>\n    <string name=\"add_video_to_new_playlist\">הוסף לפלייליסט חדש</string>\n    <string name=\"cant_delete_empty_playlist\">לא ניתן למחוק פלייליסט ריק</string>\n    <string name=\"playlist\">פלייליסט</string>\n    <string name=\"rename_playlist\">שנה שם פלייליסט</string>\n    <string name=\"cant_rename_empty_playlist\">לא ניתן לשנות שם של פלייליסט ריק</string>\n    <string name=\"cant_rename_foreign_playlist\">לא ניתן לשנות שם של פלייליסט זר</string>\n    <string name=\"cant_save_playlist\">לא ניתן להוסיף פלייליסט זה</string>\n    <string name=\"enter_value\">הזן כל ערך שאינו ריק</string>\n    <string name=\"dialog_add_remove_from\">הוסף/הסר מן %s</string>\n    <string name=\"lb_playback_controls_skip_next\">דלג לבא בתור</string>\n    <string name=\"lb_playback_controls_skip_previous\">דלג לקודם בתור/החזר לאחור למיקום ההתחלה</string>\n    <string name=\"alt_presets_behavior\">התנהגות קביעות מוגדרות מראש חלופיות (הגבל רוחב פס)</string>\n    <string name=\"alt_presets_behavior_desc\">היישום ינסה לשמור על רוחב פס המתאים לקביעה מוגדרת מראש שנבחרה במקום להתאים בין רזולוציה, fps וקודק.</string>\n    <string name=\"sleep_timer\">טיימר שינה (אם לא נעשה שימוש בשלט במשך שעה אחת)</string>\n    <string name=\"sleep_timer_desc\">השהה את ההפעלה אם המשתמש לא השתמש בשלט במשך שעה אחת</string>\n    <string name=\"disable_vsync\">השבת הצמדה אל vsync</string>\n    <string name=\"disable_vsync_desc\">אפשרות זו מבטלת יישור פריימים עם אות הסנכרון האנכי של התצוגה. זה יכול לשפר את הביצועים במכשירים ברמה נמוכה על ידי שחרור משאבי מעבד.</string>\n    <string name=\"skip_codec_profile_check\">דלג על בדיקת רמת פרופיל הקודק</string>\n    <string name=\"skip_codec_profile_check_desc\">אל תבדוק תמיכת קודק כאשר מתחילים סרטון. יכול להיות מועיל על קושחה תקולה.</string>\n    <string name=\"force_sw_codec\">כפה מפענח וידאו של פענוח תוכנה</string>\n    <string name=\"force_sw_codec_desc\">יכול להפעיל כמעט כל סרטון, אבל הביצועים גרועים מאוד.</string>\n    <string name=\"playlist_order\">מיין פלייליסט</string>\n    <string name=\"playlist_order_added_date_newer_first\">תאריך הוספה (חדש יותר תחילה)</string>\n    <string name=\"playlist_order_added_date_older_first\">תאריך הוספה (ישן יותר תחילה)</string>\n    <string name=\"playlist_order_published_date_newer_first\">תאריך פרסום (חדש יותר תחילה)</string>\n    <string name=\"playlist_order_published_date_older_first\">תאריך פרסום (ישן יותר תחילה)</string>\n    <string name=\"playlist_order_popularity\">פופולריות</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">לא ניתן לעשות זאת עבור פלייליסט זר</string>\n    <string name=\"owned_playlist_warning\">שגיאה: ניתן להחיל פעולה זו רק על פלייליסט בבעלות</string>\n    <string name=\"content_block_alt_server\">השתמש בשרת חלופי</string>\n    <string name=\"content_block_alt_server_desc\">אפשר אפשרות זו אם SponsorBlock מסרב לעבוד מסיבות שונות.</string>\n    <string name=\"unset_stream_reminder\">בטל תזכורת שידור</string>\n    <string name=\"set_stream_reminder\">קבע תזכורת שידור</string>\n    <string name=\"playback_starts_shortly\">ההפעלה תתחיל באופן אוטומטי כאשר השידור יהיה מוכן</string>\n    <string name=\"starting_stream\">מתחיל את השידור…</string>\n    <string name=\"action_playlist_remove\">הסר מפלייליסט</string>\n    <string name=\"signin_view_title\">קוד המשתמש נטען…</string>\n    <string name=\"signin_view_description\">כדי להיכנס, הזן את הקוד הזה בעמוד %s\\nהערה: עובד רק עם דפדפני Firefox או Chrome.</string>\n    <string name=\"signin_view_action_text\">בוצע</string>\n    <string name=\"require_checked\">דורש \\\"%s\\\"</string>\n    <string name=\"msg_press_again_to_exit\">לחץ שוב כדי לצאת</string>\n    <string name=\"player_remaining_time\">נותרו: %s</string>\n    <string name=\"player_ending_time\">מסתיים בשעה %s</string>\n    <string name=\"badge_new_content\">תוכן חדש</string>\n    <string name=\"badge_live\">בשידור חי</string>\n    <string name=\"add_device_view_description\">כדי לקשר את המכשיר, הזן את הקוד הזה ביישום YouTube של הטלפון שלך במקטע הגדרות/צפייה בטלוויזיה\\nהערה: זה עובד רק עם המקלדת Gboard!</string>\n    <string name=\"playback_controls_repeat_pause\">השהיית חזרה</string>\n    <string name=\"playback_controls_repeat_list\">חזור על הרשימה</string>\n    <string name=\"action_subscribe_off\">הרשמה למינוי כבויה</string>\n    <string name=\"action_subscribe_on\">הרשמה למינוי פועלת</string>\n    <string name=\"skip_24_rate\">דלג על פורמטי 24fps (תיקון מצב קולנוע אמיתי\\?)</string>\n    <string name=\"skip_shorts\">דלג על Shorts</string>\n    <string name=\"player_disable_suggestions\">השבת הצעות</string>\n    <string name=\"suggestions\">הצעות</string>\n    <string name=\"feedback\">משוב</string>\n    <string name=\"sources\">מקורות</string>\n    <string name=\"releases\">שחרורים</string>\n    <string name=\"prefer_avc_over_vp9\">בחירת קודק: העדף avc על פני vp9</string>\n    <string name=\"open_chat\">צ\\'אט/תגובות</string>\n    <string name=\"open_comments\">פתח תגובות</string>\n    <string name=\"place_chat_left\">מקם את הצ\\'אט משמאל</string>\n    <string name=\"place_comments_left\">מקם תגובות מימין</string>\n    <string name=\"use_alt_speech_recognizer\">מזהה דיבור חלופי</string>\n    <string name=\"time_format\">פורמט זמן</string>\n    <string name=\"time_format_24\">24 שעות</string>\n    <string name=\"time_format_12\">12 שעות (AM/PM)</string>\n    <string name=\"use_alt_speech_recognizer_desc\">בעל באגים רציניים. השתמש בו רק אם יש לך בעיות עם מזהה ברירת המחדל.</string>\n    <string name=\"speech_recognizer\">מזהה דיבור</string>\n    <string name=\"speech_engine\">מנוע חיפוש קולי</string>\n    <string name=\"speech_recognizer_system\">מערכת (מועדפת)</string>\n    <string name=\"speech_recognizer_external_1\">חיצוני 1 (בעל באגים רציניים, כדי שיעבוד אתה צריך להשבית את הגישה למיקרופון)</string>\n    <string name=\"speech_recognizer_external_2\">חיצוני 2 (בעל באגים רציניים)</string>\n    <string name=\"real_channel_icon\">הצג סמל על לחצן הערוץ</string>\n    <string name=\"subtitle_yellow_semi_transparent\">צהוב על רקע שקוף למחצה</string>\n    <string name=\"subtitle_yellow_black\">צהוב על רקע שחור</string>\n    <string name=\"player_pixel_ratio\">יחס פיקסלים</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">אפור כהה (מונוכרום)</string>\n    <string name=\"disable_mic_permission\">נא להשבית גישת מיקרופון עבור היישום על מנת שמזהה זה יעבוד כראוי.</string>\n    <string name=\"repeat_mode_shuffle\">ערבב כל פלייליסט</string>\n    <string name=\"chat_left\">שמאל</string>\n    <string name=\"chat_right\">ימין</string>\n    <string name=\"card_real_thumbnails\">החלף תמונות ממוזערות בפריים מהסרטון</string>\n    <string name=\"card_content\">היכן יש לתפוס תמונות ממוזערות של כרטיסים</string>\n    <string name=\"thumb_quality_default\">ברירת מחדל</string>\n    <string name=\"thumb_quality_start\">תחילת הסרטון</string>\n    <string name=\"thumb_quality_middle\">אמצע הסרטון</string>\n    <string name=\"thumb_quality_end\">סוף הסרטון</string>\n    <string name=\"content_block_status\">בדוק את מצב השרת של SponsorBlock</string>\n    <string name=\"dearrow_status\">בדוק את מצב השרת של DeArrow</string>\n    <string name=\"player_show_quality_info_bitrate\">הוסף קצב סיביות למידע על האיכות</string>\n    <string name=\"player_speed_button_old_behavior\">החזר את ההתנהגות הישנה של לחצן המהירות</string>\n    <string name=\"protect_settings_with_password\">הגן על כל ההגדרות באמצעות סיסמה</string>\n    <string name=\"enter_settings_password\">הזן את סיסמת ההגדרות</string>\n    <string name=\"child_mode\">מצב ילדים</string>\n    <string name=\"child_mode_desc\">במצב זה, המשתמש לא יכול להשתמש בחיפוש או לראות תוכן מוצע כלשהו. עמוד ההגדרות יהיה מוגן סיסמה.</string>\n    <string name=\"lost_setting_warning\">הגדרות היישום ישתנו. ודא שיצרת גיבוי של ההגדרות.</string>\n    <string name=\"player_button_long_click\">לחץ לחיצה ארוכה על לחצן זה לאפשרויות נוספות</string>\n    <string name=\"pause_history\">השהה את ההיסטוריה</string>\n    <string name=\"resume_history\">המשך את ההיסטוריה</string>\n    <string name=\"disable_history\">השבת את ההיסטוריה</string>\n    <string name=\"enable_history\">אפשר היסטוריה</string>\n    <string name=\"clear_history\">נקה היסטוריה</string>\n    <string name=\"chapters\">פרקים</string>\n    <string name=\"card_multiline_subtitle\">כתוביות מרובות שורות</string>\n    <string name=\"auto_frame_rate_modes\">מצבים נתמכים</string>\n    <string name=\"loading\">טוען…</string>\n    <string name=\"hide_streams\">הסתר זרמים מהמינויים</string>\n    <string name=\"video_rotate\">סובב</string>\n    <string name=\"trending_searches\">חיפושים חמים</string>\n    <string name=\"video_duration_any\">כלשהו</string>\n    <string name=\"video_duration\">משך</string>\n    <string name=\"video_duration_under_4\">פחות מ-4 דקות</string>\n    <string name=\"video_duration_between_4_20\">4–20 דקות</string>\n    <string name=\"video_duration_over_20\">מעל 20 דקות</string>\n    <string name=\"content_type\">סוג</string>\n    <string name=\"content_type_any\">כלשהו</string>\n    <string name=\"content_type_video\">סרטון</string>\n    <string name=\"content_type_channel\">ערוץ</string>\n    <string name=\"content_type_playlist\">פלייליסט</string>\n    <string name=\"content_type_movie\">סרט</string>\n    <string name=\"video_features\">תכונות</string>\n    <string name=\"video_feature_any\">כלשהן</string>\n    <string name=\"video_feature_live\">שידור חי</string>\n    <string name=\"video_feature_4k\">4K</string>\n    <string name=\"video_feature_hdr\">HDR</string>\n    <string name=\"search_sorting\">מיין לפי</string>\n    <string name=\"sort_by_relevance\">רלוונטיות</string>\n    <string name=\"sort_by_views\">מספר צפיות</string>\n    <string name=\"sort_by_date\">תאריך העלאה</string>\n    <string name=\"sort_by_rating\">דירוג</string>\n    <string name=\"clear_search_history\">נקה היסטוריית חיפוש</string>\n    <string name=\"remove_from_subscriptions\">הסתר</string>\n    <string name=\"player_long_speed_list\">רשימת מהירויות ארוכה</string>\n    <string name=\"player_extra_long_speed_list\">רשימת מהירויות ארוכה במיוחד</string>\n    <string name=\"alt_app_icon\">סמל יישום חלופי (צריך אתחול מחדש)</string>\n    <string name=\"network_stack\">העדף ערימת רשת מסוג %s</string>\n    <string name=\"player_network_stack\">מנוע רשת</string>\n    <string name=\"cronet_desc\">ערימת הרשת של Chromium היא Cronet. ערימת הרשת Cronet יכולה להפחית את זמן ההמתנה ולהגביר את ביצועי הרשת, מה שיכול לעזור בבעיות טעינה.</string>\n    <string name=\"unlock_all_formats\">בטל את הנעילה של כל פורמטי הוידאו</string>\n    <string name=\"unlock_all_formats_desc\">במכשירים מסוימים הקושחה מדווחת באופן שגוי על פורמטים מסוימים כלא נתמכים, גם אם הם כן.\\n(למשל טלוויזיות 1080p חכמות נוטות לדווח על 4K כלא נתמך).</string>\n    <string name=\"okhttp_desc\">מנוע רשת זה הוא האיטי ביותר אך בעל יציבות טובה.</string>\n    <string name=\"select_channel_section\">פתח את המדור המתאים כאשר היישום שוגר מערוצי טלוויזית אנדרואיד</string>\n    <string name=\"enable_voice_search_desc\">היישום הרשמי יוחלף במה שנקרא \\\"גשר\\\". גשר עוזר להעביר בקשות חיפוש גלובליות ליישום שלנו.</string>\n    <string name=\"enter_master_password\">הזן סיסמה ראשית</string>\n    <string name=\"enable_master_password\">אפשר סיסמה ראשית</string>\n    <string name=\"disable_stream_buffer\">השבת חוצץ בשידורים</string>\n    <string name=\"disable_stream_buffer_desc\">תיקון למצבים שבהם השידור רחוק מדי מאחור. הערה: השידור עשוי להתחיל לפגר.</string>\n    <string name=\"sony_frame_drop_fix\">תיקון נפילת פריימים #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">תקן פיגורים בטלוויזית Sony ובכמה מכשירים אחרים. הערה: ייתכנו בעיות עם סנכרון שמע.</string>\n    <string name=\"audio_language\">שפת שמע</string>\n    <string name=\"old_home_look\">מראה ישן של מדור הבית</string>\n    <string name=\"old_channel_look\">מראה ישן של דף הערוץ</string>\n    <string name=\"hide_shorts_everywhere\">הסתר Shorts בכל מקום</string>\n    <string name=\"update_found\">עדכון</string>\n    <string name=\"volume_boost_warning\">כל הגדרה מעבר למגבלה עלולה באופן פוטנציאלי לגרום נזק לרמקולים</string>\n    <string name=\"play_video_incognito\">הפעל גלישה בסתר</string>\n    <string name=\"header_kids_home\">ילדים</string>\n    <string name=\"player_section_playlist\">השתמש בתוכן המדור הנוכחי כפלייליסט</string>\n    <string name=\"header_trending\">מגמות</string>\n    <string name=\"screensaver\">שומר מסך</string>\n    <string name=\"player_screen_off_timeout\">פסק זמן לכיבוי מסך</string>\n    <string name=\"old_update_notifications\">מראה ישן של הודעות העדכון</string>\n    <string name=\"dialog_notification\">הודע על עדכונים באמצעות תיבת דו-שיח מוקפצת</string>\n    <string name=\"mark_as_watched\">סמן כנצפה</string>\n    <string name=\"player_ui_animations\">אפשר הנפשות ממשק משתמש בנגן</string>\n    <string name=\"player_likes_count\">הצג מספר אהבתי/דיסלייק</string>\n    <string name=\"sorting_alphabetically2\">בסדר אלפביתי (מהיר, תצוגות מקדימות מונפשות)</string>\n    <string name=\"sorting_default\">ברירת מחדל (מהיר, תצוגות מקדימות מונפשות)</string>\n    <string name=\"content_block_exclude_channel\">החרג את הערוץ הזה מ-SponsorBlock</string>\n    <string name=\"content_block_stop_excluding_channel\">הפסק להחריג את הערוץ הזה מ-SponsorBlock</string>\n    <string name=\"player_chapter_notification\">התקדמות מהירה מבעד הפרקים באמצעות ההודעה הקופצת</string>\n    <string name=\"subtitle_remember\">זכור כתוביות מופעלות לכל ערוץ</string>\n    <string name=\"amazon_frame_drop_fix\">תיקון נפילת פריימים #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">מיועד למכשירי Amazon Stick. עשוי לעבוד גם על מכשירים אחרים.</string>\n    <string name=\"default_stack_desc\">מנוע רשת מובנה. מנוע זה עשוי להיות בעל יציבות טובה יותר במצבים מסוימים.</string>\n    <string name=\"item_postion\">מיקום של</string>\n    <string name=\"player_screen_off_dimming\">כמות עמעום מסך כבוי</string>\n    <string name=\"screensaver_timout\">זמן קצוב לשומר מסך</string>\n    <string name=\"screensaver_dimming\">כמות עמעום שומר מסך</string>\n    <string name=\"player_ui_on_next\">הצג את ממשק המשתמש של הנגן בעת מעבר לסרטון הבא</string>\n    <string name=\"autogenerated\">נוצר אוטומטית</string>\n    <string name=\"player_auto_volume\">התאמת עוצמת קול אוטומטית</string>\n    <string name=\"header_shorts\">Shorts</string>\n    <string name=\"player_global_focus\">ניווט חלק בין שורות לחצני הנגן</string>\n    <string name=\"auto_history\">אוטומטי (השתמש בהגדרות חשבון)</string>\n    <string name=\"remember_position_subscriptions\">זכור את המיקום האחרון שנצפה במינויים</string>\n    <string name=\"remember_position_pinned\">זכור את המיקום האחרון שנצפה בפלייליסטים מוצמדים</string>\n    <string name=\"msg_player_unknown_error\">שגיאה לא ידועה</string>\n    <string name=\"header_notifications\">התראות</string>\n    <string name=\"disable_search_history\">השבת את היסטוריית החיפושים</string>\n    <string name=\"unlock_high_bitrate_formats\">בטל את הנעילה של פורמטי 1080p vp9 עם קצב סיביות גבוה</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">בטל את הנעילה של פורמטי mp4a עם קצב סיביות גבוה</string>\n    <string name=\"color_scheme_blue\">כחול</string>\n    <string name=\"color_scheme_dark_blue\">כחול כהה</string>\n    <string name=\"player_speed_per_channel\">לכל ערוץ</string>\n    <string name=\"disable_popular_searches\">השבת שאילתות חיפוש פופולריות</string>\n    <string name=\"multi_profiles\">השתמש בהגדרות נפרדות לכל חשבון</string>\n    <string name=\"protect_account_with_password\">הגן על חשבון זה באמצעות סיסמה</string>\n    <string name=\"enter_account_password\">הזן את סיסמת החשבון</string>\n    <string name=\"show_connect_messages\">הצג הודעות חיבור</string>\n    <string name=\"prefer_avc_over_vp9_desc\">אזהרה: 1080p מקסימום</string>\n    <string name=\"play_next\">הבא בתור</string>\n    <string name=\"hide_watched_from_subscriptions\">הסתר סרטונים שנצפו מהמינויים</string>\n    <string name=\"hide_watched_from_notifications\">הסתר סרטונים שנצפו מההתראות</string>\n    <string name=\"hide_unwanted_content\">הסתר תוכן לא רצוי</string>\n    <string name=\"hide_watched_from_home\">הסתר סרטונים שנצפו מהבית</string>\n    <string name=\"hide_watched_from_watch_later\">הסתר סרטונים שנצפו מהפלייליסט \\'לצפייה בהמשך\\'</string>\n    <string name=\"remote_control_permission\">שליטה מרחוק ברקע דורשת הרשאת שכבת-על</string>\n    <string name=\"login_from_browser\">היכנס מהדפדפן</string>\n    <string name=\"disable_remote_history\">השבת היסטוריה בעת שימוש בשליטה מרחוק</string>\n    <string name=\"keyboard_fix\">מנע מלחצן האישור לפתוח את המקלדת (G20s ואחרות)</string>\n    <string name=\"nothing_found\">שום דבר לא נמצא</string>\n    <string name=\"auto_frame_rate_desc\">אפשרות זו מסירה ריצוד בסצנות שבהן המצלמה נעה מהר, למשל הזרמת ספורט</string>\n    <string name=\"channel_filter_hint\">סינון ערוצים</string>\n    <string name=\"player_loop_shorts\">הפעל Shorts בלופ</string>\n    <string name=\"player_quick_shorts_skip\">דלג על Shorts עם לחצני שמאל/ימין</string>\n    <string name=\"channels_filter\">הצג שדה סינון ערוצים בתוך מדור הערוצים</string>\n    <string name=\"channel_search_bar\">סרגל חיפוש בתוך דף הערוץ</string>\n    <string name=\"header_sports\">ספורט</string>\n    <string name=\"keep_finished_activities\">שמור על פעילויות שהסתיימו</string>\n    <string name=\"disable_channels_service\">השבת את שירות הערוצים</string>\n    <string name=\"replace_titles\">החלף כותרות</string>\n    <string name=\"enable\">אפשר</string>\n    <string name=\"more_info\">עוד מידע</string>\n    <string name=\"about_sponsorblock\">אודות SponsorBlock</string>\n    <string name=\"about_dearrow\">אודות DeArrow</string>\n    <string name=\"replace_thumbnails\">החלף תמונות ממוזערות</string>\n    <string name=\"crowdsourced_thumbnails\">תמונות ממוזערות במיקור המונים</string>\n    <string name=\"crowdsoursed_titles\">כותרות במיקור המונים</string>\n    <string name=\"dearrow_not_submitted_thumbs\">מקור תמונה ממוזערת (אם אין תמונה של DeArrow)</string>\n    <string name=\"pitch_effect\">אפקט גובה הצליל</string>\n    <string name=\"fullscreen_mode\">מצב מסך מלא (ללא פסי מערכת)</string>\n    <string name=\"player_only_mode\">הצג רק את הנגן אם הסרטון נפתח מחוץ ליישום</string>\n    <string name=\"pinned_channel_rows\">הצג ערוצים מוצמדים כשורות</string>\n    <string name=\"prefer_ipv4\">העדף IPv4 DNS</string>\n    <string name=\"prefer_ipv4_desc\">יכול לתקן מצבים שבהם היישום לא עובד בכלל.\\nהערה: עלול לגרום לתקיעות ולקריסות (במיוחד במכשירי אנדרואיד 8 או Dune HD)</string>\n    <string name=\"long_press_for_settings\">לחץ לחיצה ארוכה להגדרות</string>\n    <string name=\"long_press_for_options\">לחץ לחיצה ארוכה לאפשרויות</string>\n    <string name=\"device_specific_backup\">גיבוי למכשיר זה בלבד</string>\n    <string name=\"local_backup\">גיבוי מקומי</string>\n    <string name=\"auto_backup\">גיבוי אוטומטי (פעם ביום)</string>\n    <string name=\"open_playlist\">פתח פלייליסט</string>\n    <string name=\"not_recommend_channel\">אל תמליץ על ערוץ</string>\n    <string name=\"you_wont_see_this_channel\">לא תראה ערוץ זה בהמלצות</string>\n    <string name=\"unknown_source_error\">שגיאת מקור לא ידועה</string>\n    <string name=\"unknown_renderer_error\">שגיאת מעבד תצוגה לא ידועה</string>\n    <string name=\"player_quick_skip_videos\">דלג על סרטונים רגילים עם לחצני שמאל/ימין</string>\n    <string name=\"repeat_mode_reverse_list\">נגן את הפלייליסט או את סרטוני הערוץ בסדר הפוך</string>\n    <string name=\"calm_msg\">אנו מטפלים בבעיה. בדוק אם קיימים עדכונים מעת לעת</string>\n    <string name=\"without_picture\">ללא תמונה</string>\n    <string name=\"video_disabled\">סרטון מושבת</string>\n    <string name=\"playback_queue_category_title\">תור הפעלה</string>\n    <string name=\"video_buffer\">חוצץ סרטון</string>\n    <string name=\"video_buffer_size_none\">ללא</string>\n    <string name=\"applying_fix\">אל תסגור את הנגן. מחיל את התיקון…</string>\n    <string name=\"hide_mixes\">הסתר מיקסים</string>\n    <string name=\"player_global_focus_desc\">תכונה זו משפיעה על לחצן הנגן שיקבל מיקוד כאשר מנווטים בין שורות לחצני הנגן</string>\n    <string name=\"disable_network_error_fixing\">השבת תיקון שגיאת רשת אוטומטי</string>\n    <string name=\"disable_network_error_fixing_desc\">אתה כנראה צריך לאפשר אפשרות זו אם אתה משתמש ב-VPN</string>\n    <string name=\"recommended\">מומלצים</string>\n    <string name=\"add_to_subscriptions_group\">הוסף/הסר מקבוצת מינויים</string>\n    <string name=\"new_subscriptions_group\">קבוצה חדשה</string>\n    <string name=\"rename_group\">שנה את שם קבוצת המינויים</string>\n    <string name=\"screen_dimming_amount\">כמות עמעום מסך</string>\n    <string name=\"screen_dimming_timeout\">פסק זמן לעמעום מסך</string>\n    <string name=\"playlists_rows\">הצג מדור פלייליסטים כשורות</string>\n    <string name=\"import_subscriptions_group\">ייבוא</string>\n    <string name=\"lb_playback_controls_closed_captioning_disable\">השבת כתוביות סמויות</string>\n    <string name=\"lb_playback_controls_closed_captioning_enable\">הפעל כתוביות סמויות</string>\n    <string name=\"my_videos\">הסרטונים שלי</string>\n    <string name=\"player_audio_focus\">מיקוד שמע (השהה אם זוהו נגנים אחרים)</string>\n    <string name=\"paid_content_notification\">התראת תוכן בתשלום</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-ja/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"header_home\">ホーム</string>\n    <string name=\"title_search\">検索</string>\n    <string name=\"header_subscriptions\">登録チャンネル</string>\n    <string name=\"header_history\">履歴</string>\n    <string name=\"header_music\">音楽</string>\n    <string name=\"header_news\">ニュース</string>\n    <string name=\"header_gaming\">ゲーム</string>\n    <string name=\"header_playlists\">再生リスト</string>\n    <string name=\"header_settings\">設定</string>\n    <string name=\"header_channels\">チャンネル</string>\n    <string name=\"subscriptions_signin_title\">お気に入りのチャンネルの最新情報</string>\n    <string name=\"subscriptions_signin_subtitle\">ログインをして登録チャンネルに追加をしましょう</string>\n    <string name=\"subscriptions_signin_button_text\">ログイン</string>\n    <string name=\"library_signin_to_show_more\">高評価、保存、またはチャンネルを登録した動画を視聴しましょう</string>\n    <string name=\"library_signin_subtitle\">ライブラリを見るにはログインしてください</string>\n    <string name=\"action_signin\">ログイン</string>\n    <string name=\"title_video_formats\">動画の形式</string>\n    <string name=\"title_audio_formats\">オーディオの形式</string>\n    <string name=\"subtitle_category_title\">字幕</string>\n    <string name=\"playback_queue_category_title\">再生キュー</string>\n    <string name=\"video_max_quality\">自動 (最高画質)</string>\n    <string name=\"audio_max_quality\">自動 (最高画質)</string>\n    <string name=\"subtitles_disabled\">字幕 OFF</string>\n    <string name=\"auto_frame_rate\">自動フレームレート</string>\n    <string name=\"frame_rate_correction\">固定 FPS:\\n%s</string>\n    <string name=\"category_background_playback\">バックグラウンド再生</string>\n    <string name=\"not_implemented\">未実装</string>\n    <string name=\"not_supported_by_device\">デバイスはこの機能に対応していません</string>\n    <string name=\"option_background_playback_off\">無効</string>\n    <string name=\"option_background_playback_pip\">ピクチャーインピクチャー</string>\n    <string name=\"option_background_playback_only_audio\">オーディオのみ</string>\n    <string name=\"playback_settings\">再生設定</string>\n    <string name=\"video_speed\">再生速度</string>\n    <string name=\"resolution_switch\">解像度を切り替え</string>\n    <string name=\"video_buffer\">動画のバッファ</string>\n    <string name=\"video_buffer_size_none\">なし</string>\n    <string name=\"video_buffer_size_lowest\">最低</string>\n    <string name=\"video_buffer_size_low\">低</string>\n    <string name=\"video_buffer_size_med\">中</string>\n    <string name=\"video_buffer_size_high\">高</string>\n    <string name=\"video_buffer_size_highest\">最高</string>\n    <string name=\"update_changelog\">更新履歴</string>\n    <string name=\"install_update\">更新をインストール</string>\n    <string name=\"section_is_empty\">おっと！ここには何もありません。</string>\n    <string name=\"dialog_add_to_playlist\">再生リストへの追加または削除</string>\n    <string name=\"msg_signed_users_only\">ログインユーザーのみ</string>\n    <string name=\"msg_cant_load_content\">コンテンツを読み込めません。\\n1) 検索履歴を有効にしてください。\\n2) 本体の日時設定を確認してください。\\n3) インターネット接続を確認してください。\\n4) プロキシ設定を確認してください。\\n5) アカウントにログインしてください。\\n6) このページが空白の場合もあります。</string>\n    <string name=\"title_video_presets\">動画のプリセット</string>\n    <string name=\"settings_accounts\">アカウント</string>\n    <string name=\"settings_left_panel\">カテゴリーを編集</string>\n    <string name=\"settings_themes\">テーマ</string>\n    <string name=\"settings_other\">その他</string>\n    <string name=\"settings_player\">プレーヤー</string>\n    <string name=\"settings_language\">言語</string>\n    <string name=\"settings_linked_devices\">リンク済みのデバイス</string>\n    <string name=\"settings_about\">アプリについて</string>\n    <string name=\"dialog_account_list\">アカウントを選択</string>\n    <string name=\"dialog_account_none\">なし</string>\n    <string name=\"dialog_remove_account\">ログアウト</string>\n    <string name=\"dialog_add_account\">ログイン</string>\n    <string name=\"default_lang\">デフォルト</string>\n    <string name=\"original_lang\">オリジナル</string>\n    <string name=\"dialog_select_language\">言語</string>\n    <string name=\"subtitle_default\">デフォルト</string>\n    <string name=\"subtitle_white_semi_transparent\">半透明背景に白字</string>\n    <string name=\"subtitle_style\">字幕のスタイル</string>\n    <string name=\"subtitle_language\">字幕の言語</string>\n    <string name=\"action_search\">検索</string>\n    <string name=\"settings_main_ui\">ユーザーインターフェース</string>\n    <string name=\"dialog_main_ui\">ユーザーインターフェース</string>\n    <string name=\"card_animated_previews\">動画のプレビュー</string>\n    <string name=\"web_site\">ウェブサイト</string>\n    <string name=\"donation\">寄付</string>\n    <string name=\"dialog_about\">アプリについて</string>\n    <string name=\"dialog_player_ui\">動画プレーヤー</string>\n    <string name=\"player_show_ui_on_pause\">一時停止時にユーザーインターフェースを表示</string>\n    <string name=\"player_pause_on_ok\">OK ボタンで再生を一時停止</string>\n    <string name=\"player_ok_button_behavior\">OK ボタンの動作</string>\n    <string name=\"player_only_ui\">ユーザーインターフェースのみ</string>\n    <string name=\"player_ui_and_pause\">ユーザーインターフェースと一時停止</string>\n    <string name=\"player_only_pause\">一時停止のみ</string>\n    <string name=\"check_for_updates\">更新を確認する</string>\n    <string name=\"update_not_found\">最新のバージョンを使用しています</string>\n    <string name=\"update_in_progress\">お待ちください…</string>\n    <string name=\"player_ui_hide_behavior\">ユーザーインターフェースを自動で非表示</string>\n    <string name=\"option_never\">無効</string>\n    <string name=\"side_panel_sections\">表示するセクション</string>\n    <string name=\"boot_to_section\">起動時のセクション</string>\n    <string name=\"large_ui\">大きいユーザーインターフェース</string>\n    <string name=\"video_grid_scale\">サムネイルの大きさ</string>\n    <string name=\"scale_ui\">ユーザーインターフェースの大きさ</string>\n    <string name=\"color_scheme\">カラーテーマ</string>\n    <string name=\"color_scheme_default\">デフォルト</string>\n    <string name=\"color_scheme_red_grey\">レッドとグレー</string>\n    <string name=\"color_scheme_red\">レッド</string>\n    <string name=\"color_scheme_dark_grey\">ダークグレー</string>\n    <string name=\"disable_update_check\">更新の確認を無効化する</string>\n    <string name=\"show_again\">後で表示</string>\n    <string name=\"check_updates_auto\">更新の通知</string>\n    <string name=\"select_account_on_boot\">起動時に選択</string>\n    <string name=\"player_other\">その他</string>\n    <string name=\"player_full_date\">概要欄に正確な日付を表示</string>\n    <string name=\"open_channel\">チャンネルを開く</string>\n    <string name=\"open_playlist\">再生リストを開く</string>\n    <string name=\"not_interested\">興味なし</string>\n    <string name=\"not_recommend_channel\">チャンネルをおすすめに表示しない</string>\n    <string name=\"you_wont_see_this_video\">おすすめから動画が削除されました</string>\n    <string name=\"you_wont_see_this_channel\">今後このチャンネルの動画はおすすめされなくなります</string>\n    <string name=\"settings_search\">検索</string>\n    <string name=\"dialog_search\">検索</string>\n    <string name=\"instant_voice_search\">すぐに音声検索を開始する</string>\n    <string name=\"option_background_playback_behind\">バックグラウンドで再生</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">次の動画情報はまだ読み込まれていません</string>\n    <string name=\"card_multiline_title\">複数行のタイトル</string>\n    <string name=\"cards_style\">カードスタイル</string>\n    <string name=\"repeat_mode_all\">動画を続けて再生</string>\n    <string name=\"repeat_mode_one\">現在の動画をリピート</string>\n    <string name=\"repeat_mode_pause\">各動画の再生後に一時停止 (キューを除く)</string>\n    <string name=\"repeat_mode_pause_alt\">再生リスト内の動画のみ続けて再生</string>\n    <string name=\"repeat_mode_none\">1 件の動画の再生後に一時停止 (キューを除く)</string>\n    <string name=\"subscribed_to_channel\">チャンネルを登録しました</string>\n    <string name=\"unsubscribed_from_channel\">チャンネルの登録を解除しました</string>\n    <string name=\"subtitle_yellow_transparent\">透明背景に黄色字</string>\n    <string name=\"subtitle_white_transparent\">透明背景に白字</string>\n    <string name=\"subtitle_white_black\">黒い背景に白字</string>\n    <string name=\"player_seek_preview\">シーク中にプレビュー</string>\n    <string name=\"color_scheme_dark_grey_oled\">ダークグレー (OLED)</string>\n    <string name=\"channels_section_sorting\">チャンネルセクションの並べ替え</string>\n    <string name=\"sorting_by_new_content\">投稿順</string>\n    <string name=\"sorting_alphabetically\">アルファベット順</string>\n    <string name=\"sorting_last_viewed\">視聴順</string>\n    <string name=\"player_pause_when_seek\">シーク中は一時停止</string>\n    <string name=\"playlists_style\">再生リストセクションのスタイル</string>\n    <string name=\"playlists_style_grid\">グリッド</string>\n    <string name=\"playlists_style_rows\">列</string>\n    <string name=\"player_show_clock\">コントロールバーに時計を表示</string>\n    <string name=\"player_show_remaining_time\">コントロールバーに残り時間を表示</string>\n    <string name=\"player_show_ending_time\">コントロールバーに終了時間を表示</string>\n    <string name=\"open_channel_uploads\">チャンネルの投稿動画を開く</string>\n    <string name=\"app_exit_shortcut\">アプリの終了</string>\n    <string name=\"player_exit_shortcut\">動画プレーヤーの終了</string>\n    <string name=\"search_exit_shortcut\">検索を終了</string>\n    <string name=\"app_exit_none\">終了しない</string>\n    <string name=\"app_double_back_exit\">戻るボタンを 2 回押下</string>\n    <string name=\"app_single_back_exit\">戻るボタンを 1 回押下</string>\n    <string name=\"action_video_zoom\">動画の拡大</string>\n    <string name=\"video_zoom\">動画の拡大</string>\n    <string name=\"video_zoom_default\">デフォルト</string>\n    <string name=\"video_zoom_fit_width\">画面幅に合わせる</string>\n    <string name=\"video_zoom_fit_height\">画面の高さに合わせる</string>\n    <string name=\"video_zoom_fit_both\">画面に合わせる</string>\n    <string name=\"video_zoom_stretch\">自動</string>\n    <string name=\"color_scheme_teal\">青緑</string>\n    <string name=\"color_scheme_teal_oled\">青緑 (OLED)</string>\n    <string name=\"player_seek_preview_none\">無効</string>\n    <string name=\"player_seek_preview_single\">シングルフレーム</string>\n    <string name=\"player_seek_preview_carousel\">カルーセル</string>\n    <string name=\"player_seek_preview_carousel_slow\">カルーセル (低速、フレーム毎)</string>\n    <string name=\"player_seek_preview_carousel_fast\">カルーセル (高速、プレビューは不正確)</string>\n    <string name=\"unsubscribe_from_channel\">チャンネルの登録を解除</string>\n    <string name=\"card_title_lines_num\">カードタイトルの行数</string>\n    <string name=\"card_auto_scrolled_title\">省略されたタイトルを自動スクロール</string>\n    <string name=\"share_link\">リンクを共有</string>\n    <string name=\"share_qr_link\">リンクを共有 (QR コード)</string>\n    <string name=\"subscribe_to_channel\">チャンネルを登録</string>\n    <string name=\"wait_data_loading\">データが読み込まれるまでお待ちください…</string>\n    <string name=\"auto_frame_rate_pause\">自動フレームレート (切り替え中に一時停止)</string>\n    <string name=\"auto_frame_rate_applying\">自動フレームレート %sx%s\\@%s を適用しています</string>\n    <string name=\"audio_shift\">オーディオシフト</string>\n    <string name=\"player_remember_speed\">再生速度を記憶</string>\n    <string name=\"mark_channel_as_watched\">視聴済みとしてマークする</string>\n    <string name=\"channel_marked_as_watched\">チャンネルが視聴済みになりました</string>\n    <string name=\"dialog_add_device\">デバイスを追加</string>\n    <string name=\"dialog_remove_all_devices\">すべてのデバイスを削除</string>\n    <string name=\"device_link_enabled\">リンクは有効です</string>\n    <string name=\"device_connected\">デバイス「%s」が接続されました</string>\n    <string name=\"device_disconnected\">デバイス「%s」が切断されました</string>\n    <string name=\"settings_ui_scale\">ユーザーインターフェースの大きさ</string>\n    <string name=\"msg_restart_app\">設定を適用するにはアプリを再起動してください</string>\n    <string name=\"settings_block\">コンテンツのブロック</string>\n    <string name=\"msg_applying\">「%s」を適用中…</string>\n    <string name=\"msg_done\">完了</string>\n    <string name=\"settings_general\">一般</string>\n    <string name=\"settings_video\">動画</string>\n    <string name=\"content_block_confirm_skip\">スキップを確認する</string>\n    <string name=\"content_block_categories\">セグメントのスキップ</string>\n    <string name=\"content_block_sponsor\">スポンサー</string>\n    <string name=\"content_block_intro\">合間または導入のアニメ</string>\n    <string name=\"content_block_outro\">終了画面またはクレジット</string>\n    <string name=\"content_block_interaction\">行動のお願い (登録または寄付など)</string>\n    <string name=\"content_block_self_promo\">無報酬または自己の宣伝</string>\n    <string name=\"content_block_music_off_topic\">音楽以外の部分</string>\n    <string name=\"confirm_segment_skip\">「%s」のセグメントをスキップしますか?</string>\n    <string name=\"cancel_segment_skip\">セグメントのスキップをキャンセル</string>\n    <string name=\"msg_skipping_segment\">「%s」のセグメントをスキップしました…</string>\n    <string name=\"content_block_notification_type\">通知方法</string>\n    <string name=\"content_block_notify_none\">通知なし</string>\n    <string name=\"content_block_notify_toast\">トースト通知</string>\n    <string name=\"content_block_notify_dialog\">確認のダイアログ</string>\n    <string name=\"return_to_launcher\">ATV チャンネルまたは検索からランチャーに戻る</string>\n    <string name=\"intent_force_close\">外部から動画が開かれた場合に強制で終了する</string>\n    <string name=\"btn_confirm\">確認</string>\n    <string name=\"player_low_video_quality\">低画質</string>\n    <string name=\"remote_session_closed\">キャストのセッションが終了しました</string>\n    <string name=\"msg_mode_switch_error\">表示モードを「%s」に切り替えられません</string>\n    <string name=\"msg_player_error\">プレーヤーエラー「%s」が発生しました。再生を再開します…</string>\n    <string name=\"video_preset_disabled\">プリセットなし</string>\n    <string name=\"video_preset_enabled\">次の動画の形式は、プリセットに従って設定されます</string>\n    <string name=\"player_sleep_timer\">スリープタイマー</string>\n    <string name=\"player_show_quality_info\">画質の情報をコントロールバーに表示する</string>\n    <string name=\"player_remember_each_speed\">各動画の再生速度を記憶する</string>\n    <string name=\"player_remember_speed_none\">なし</string>\n    <string name=\"player_remember_speed_all\">すべての動画で共通</string>\n    <string name=\"player_remember_speed_each\">動画ごと</string>\n    <string name=\"msg_player_error_source\">動画をダウンロードできません</string>\n    <string name=\"msg_player_error_renderer\">選択した形式は未対応です。\\n他の形式を選択してください。\\n改善しなければ、デバイスを再起動してください。</string>\n    <string name=\"msg_player_error_video_renderer\">選択した動画の形式は非対応です。\\nHQ (再生設定) ボタンから他の形式を選択してください。\\n改善しなければ、デバイスを再起動してください。</string>\n    <string name=\"msg_player_error_audio_renderer\">選択したオーディオの形式は非対応です。\\nHQ (再生設定) ボタンから他の形式を選択してください。\\n改善しなければ、デバイスを再起動してください。</string>\n    <string name=\"msg_player_error_subtitle_renderer\">選択した字幕の形式は非対応です。\\nCC (字幕) ボタンを長押しして、他の形式を選択してください。\\n改善しなければ、デバイスを再起動してください。</string>\n    <string name=\"msg_player_error_unexpected\">映像のデコーダーが不明のエラー</string>\n    <string name=\"video_aspect\">アスペクト比</string>\n    <string name=\"player_tweaks\">開発者向けオプション</string>\n    <string name=\"header_uploads\">投稿動画</string>\n    <string name=\"player_show_global_clock\">常に時計を表示</string>\n    <string name=\"player_show_global_ending_time\">常に終了時間を表示</string>\n    <string name=\"app_corner_clock\">ホーム: 上部右端の時計</string>\n    <string name=\"player_corner_clock\">プレーヤー: 上部右端の時計</string>\n    <string name=\"player_corner_ending_time\">プレーヤー: 上部右端の終了時間までの時間</string>\n    <string name=\"uploads_old_look\">投稿動画のセクションを古い外観に戻す</string>\n    <string name=\"background_playback_activation\">バックグラウンド再生 (起動)</string>\n    <string name=\"channels_old_look\">古い外観のチャンネルセクション</string>\n    <string name=\"channels_auto_load\">チャンネルセクションの内容を自動で読み込む</string>\n    <string name=\"return_to_background_video\">バックグラウンド再生中の動画に戻る</string>\n    <string name=\"pin_unpin_from_sidebar\">サイドバーに追加または削除</string>\n    <string name=\"pin_unpin_playlist\">サイドバーに再生リストを追加または削除</string>\n    <string name=\"pin_unpin_channel\">サイドバーにチャンネルを追加または削除</string>\n    <string name=\"pin_playlist\">サイドバーに再生リストを固定</string>\n    <string name=\"pin_channel\">サイドバーにチャンネルを固定</string>\n    <string name=\"pinned_to_sidebar\">サイドバーに固定しました</string>\n    <string name=\"unpin_from_sidebar\">サイドバーから固定を解除</string>\n    <string name=\"unpin_group_from_sidebar\">登録チャンネルのグループを削除</string>\n    <string name=\"unpinned_from_sidebar\">サイドバーから固定を解除しました</string>\n    <string name=\"dialog_select_country\">国</string>\n    <string name=\"settings_language_country\">言語と国</string>\n    <string name=\"share_embed_link\">埋め込みリンクを共有</string>\n    <string name=\"card_text_scroll_factor\">カードテキストのスクロール速度</string>\n    <string name=\"preferred_update_source\">更新元の選択</string>\n    <string name=\"hide_shorts\">登録したチャンネルからショートを非表示にする</string>\n    <string name=\"hide_shorts_channel\">チャンネルからショート動画を非表示にする</string>\n    <string name=\"key_remapping\">キーリマッピング</string>\n    <string name=\"screen_dimming\">画面の減光</string>\n    <string name=\"removed_from_playback_queue\">再生キューから削除されました</string>\n    <string name=\"added_to_playback_queue\">再生キューに追加されました</string>\n    <string name=\"add_remove_from_playback_queue\">再生キューに追加または削除</string>\n    <string name=\"add_to_playback_queue\">再生キューに追加する</string>\n    <string name=\"remove_from_playback_queue\">再生キューから削除する</string>\n    <string name=\"proxy_enabled\">プロキシは有効です</string>\n    <string name=\"proxy_disabled\">プロキシは無効です</string>\n    <string name=\"uploads_row_name\">投稿動画</string>\n    <string name=\"playlists_row_name\">作成した再生リスト</string>\n    <string name=\"popular_uploads_row_name\">人気の投稿動画</string>\n    <string name=\"news_row_name\">ニュース</string>\n    <string name=\"breaking_news_row_name\">ニュース速報</string>\n    <string name=\"covid_news_row_name\">COVID-19 のニュース</string>\n    <string name=\"enable_voice_search\">グローバル音声検索を有効化 (ファームウェアが対応している必要があります)</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">チャンネルを登録または登録を解除</string>\n    <string name=\"app_backup_restore\">バックアップと復元</string>\n    <string name=\"app_backup\">アプリデータをバックアップ</string>\n    <string name=\"app_restore\">アプリデータを復元</string>\n    <string name=\"skip_each_segment_once\">2 度目はセグメントをスキップしない</string>\n    <string name=\"disable_ok_long_press\">OK ボタンの長押しを無効化する</string>\n    <string name=\"disable_ok_long_press_desc\">OK ボタンが正常に機能しないバグがあるコントローラー向けです</string>\n    <string name=\"player_time_correction\">速度補正機能ありで時間を表示</string>\n    <string name=\"sponsor_color_markers\">シークバーのマーカーの色</string>\n    <string name=\"refresh_section\">セクションを更新する</string>\n    <string name=\"option_disabled\">無効</string>\n    <string name=\"live_now_row_name\">ライブ配信中</string>\n    <string name=\"run_in_background\">バックグラウンド再生</string>\n    <string name=\"settings_remote_control\">キャスト</string>\n    <string name=\"background_service_started\">バックグラウンドサービスを開始しました</string>\n    <string name=\"dialog_add_to\">「%s」に追加</string>\n    <string name=\"dialog_remove_from\">「%s」から削除</string>\n    <string name=\"added_to\">「%s」に追加しました</string>\n    <string name=\"removed_from\">「%s」から削除しました</string>\n    <string name=\"video_preset_adaptive\">最適</string>\n    <string name=\"content_block_preview_recap\">予告と前回のあらすじ</string>\n    <string name=\"content_block_highlight\">動画の要点とハイライト</string>\n    <string name=\"removed_from_history\">動画を履歴から削除しました</string>\n    <string name=\"remove_from_history\">履歴から削除</string>\n    <string name=\"upload_date\">アップロード日</string>\n    <string name=\"upload_date_any\">すべて</string>\n    <string name=\"upload_date_today\">今日</string>\n    <string name=\"upload_date_this_week\">今週</string>\n    <string name=\"upload_date_this_month\">今月</string>\n    <string name=\"upload_date_this_year\">今年</string>\n    <string name=\"double_refresh_rate\">リフレッシュレートを倍にする</string>\n    <string name=\"upload_date_last_hour\">1 時間以内</string>\n    <string name=\"focus_on_search_results\">検索結果に自動でカーソルを合わせる</string>\n    <string name=\"context_menu\">コンテキストメニュー</string>\n    <string name=\"context_menu_sorting\">コンテキストメニューの並べ替え</string>\n    <string name=\"add_remove_from_recent_playlist\">直近の再生リストに追加または削除</string>\n    <string name=\"hide_settings_section\">設定セクションを非表示にする (危険です！)</string>\n    <string name=\"volume\">音量 %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s 秒</string>\n    <string name=\"audio_shift_sec\">%s 秒</string>\n    <string name=\"ui_hide_timeout_sec\">%s 秒</string>\n    <string name=\"screen_dimming_timeout_min\">%s 分</string>\n    <string name=\"mark_all_channels_watched\">すべてのチャンネルを視聴済みにする</string>\n    <string name=\"move_section_up\">セクションを上に移動</string>\n    <string name=\"move_section_down\">セクションを下に移動</string>\n    <string name=\"player_buttons\">プレーヤー上のボタン設定</string>\n    <string name=\"action_playlist_add\">再生リストに追加</string>\n    <string name=\"action_video_stats\">動画の詳細情報</string>\n    <string name=\"action_subscribe\">チャンネルを登録</string>\n    <string name=\"action_channel\">チャンネルを開く</string>\n    <string name=\"action_pip\">ピクチャーインピクチャー</string>\n    <string name=\"action_video_info\">概要欄</string>\n    <string name=\"action_screen_off\">画面を OFF にする</string>\n    <string name=\"action_sound_off\">ミュート</string>\n    <string name=\"action_playback_queue\">再生キュー</string>\n    <string name=\"action_video_speed\">再生速度</string>\n    <string name=\"action_subtitles\">字幕</string>\n    <string name=\"action_like\">高評価</string>\n    <string name=\"action_dislike\">低評価</string>\n    <string name=\"action_play_pause\">再生と一時停止</string>\n    <string name=\"action_repeat_mode\">再生モード</string>\n    <string name=\"action_next\">次の動画</string>\n    <string name=\"action_previous\">前の動画</string>\n    <string name=\"action_high_quality\">画質</string>\n    <string name=\"content_block_no_skipping_mode\">セグメントをスキップしない</string>\n    <string name=\"content_block_action_type\">アクションを選択</string>\n    <string name=\"content_block_action_none\">何もしない</string>\n    <string name=\"content_block_action_only_skip\">スキップのみ</string>\n    <string name=\"content_block_action_toast\">スキップの通知を表示する</string>\n    <string name=\"content_block_action_dialog\">確認ダイアログを表示する</string>\n    <string name=\"content_block_filler\">内容からの脱線 (つなぎ)</string>\n    <string name=\"keyboard_auto_show\">自動でキーボードを表示する</string>\n    <string name=\"cancel_dialog\">キャンセル</string>\n    <string name=\"msg_player_error_source2\">URL が動作していないか、デバイスの日時が不正です。\\n通常はアプリ側の不具合です。</string>\n    <string name=\"msg_player_error_video_source\">動画の URL が動作していないか、デバイスの日時が不正です。\\n通常はアプリ側の不具合です。</string>\n    <string name=\"msg_player_error_audio_source\">オーディオの URL が動作していないか、デバイスの日時が不正です。\\n通常はアプリ側の不具合です。</string>\n    <string name=\"msg_player_error_subtitle_source\">字幕の URL が動作していないか、デバイスの日時が不正です。\\n通常はアプリ側の不具合です。</string>\n    <string name=\"hide_upcoming\">登録チャンネルからライブ配信予定を非表示にする</string>\n    <string name=\"hide_upcoming_channel\">チャンネル欄からライブ配信予定を非表示にする</string>\n    <string name=\"hide_upcoming_home\">ホームからライブ配信予定を非表示にする</string>\n    <string name=\"search_background_playback\">チャンネルの検索または閲覧中のバックグラウンド再生</string>\n    <string name=\"trending_row_name\">急上昇</string>\n    <string name=\"player_seek_type\">シーク時の振る舞い</string>\n    <string name=\"player_seek_regular\">通常</string>\n    <string name=\"player_seek_confirmation_pause\">確認あり (シーク時に一時停止)</string>\n    <string name=\"player_seek_confirmation_play\">確認あり (シークをしながら再生)</string>\n    <string name=\"hide_shorts_from_home\">ホームからショート動画を非表示にする</string>\n    <string name=\"finish_on_disconnect\">スマートフォンまたはタブレットの接続を解除後にアプリを終了する</string>\n    <string name=\"update_error\">更新エラー</string>\n    <string name=\"hide_shorts_from_search\">検索結果からショート動画を非表示にする</string>\n    <string name=\"hide_shorts_from_history\">履歴からショート動画を非表示にする</string>\n    <string name=\"hide_shorts_from_trending\">急上昇からショート動画を非表示にする</string>\n    <string name=\"disable_screensaver\">スクリーンセーバーを無効化する</string>\n    <string name=\"description_not_found\">説明はありません</string>\n    <string name=\"proxy_port_hint\">プロキシのポート</string>\n    <string name=\"proxy_host_hint\">プロキシのホスト名または IP</string>\n    <string name=\"proxy_username_hint\">プロキシのユーザー名</string>\n    <string name=\"proxy_password_hint\">プロキシのパスワード</string>\n    <string name=\"proxy_type\">プロキシのタイプ</string>\n    <string name=\"enable_web_proxy\">プロキシを使用する</string>\n    <string name=\"proxy_not_supported\">プロキシを使用する (Android 4.4 以降が必要です)</string>\n    <string name=\"proxy_test_btn\">テスト</string>\n    <string name=\"proxy_settings_title\">プロキシサーバーの設定</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">テスト #%d: キャンセルしました。</string>\n    <string name=\"proxy_type_invalid\">プロキシのタイプが設定されていません。</string>\n    <string name=\"proxy_host_invalid\">プロキシのホストが設定されていません。</string>\n    <string name=\"proxy_port_invalid\">プロキシのポートが不正です。0より上にしてください。</string>\n    <string name=\"proxy_credentials_invalid\">ユーザー名またはパスワードが不正です。</string>\n    <string name=\"proxy_test_aborted\">テストをキャンセルしました。先にプロキシの設定を修正してください。</string>\n    <string name=\"proxy_application_aborted\">先にプロキシの設定を修正してください。</string>\n    <string name=\"proxy_test_start\">テスト #%d: %s …</string>\n    <string name=\"proxy_test_error\">エラー #%d: %s</string>\n    <string name=\"proxy_test_status\">ステータス #%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">設定ファイルのアドレス (*.ovpn)</string>\n    <string name=\"enable_openvpn\">OpenVPN を使用する</string>\n    <string name=\"openvpn_settings_title\">OpenVPN の設定</string>\n    <string name=\"openvpn_application_aborted\">始めに OpenVPN の設定を修正してください。</string>\n    <string name=\"openvpn_test_aborted\">テストをキャンセルしました。先に OpenVPN の設定を修正してください。</string>\n    <string name=\"openvpn_address_invalid\">OpenVPN の設定アドレスが設定されていません。</string>\n    <string name=\"internet_censorship\">インターネット検閲</string>\n    <string name=\"rename_section\">セクションの名前を変更</string>\n    <string name=\"simple_edit_value_hint\">新しい値</string>\n    <string name=\"seek_interval\">シークの間隔</string>\n    <string name=\"seek_interval_sec\">%s 秒</string>\n    <string name=\"subtitle_system\">システムのスタイル (Android 設定→ユーザー補助)</string>\n    <string name=\"subtitle_scale\">字幕の大きさ</string>\n    <string name=\"audio_sync_fix_desc\">オーディオまたは動画を同期させるための代替手段です。古い方法ですが、状況によっては役に立つかもしれません。</string>\n    <string name=\"audio_sync_fix\">オーディオ同期の修正</string>\n    <string name=\"ambilight_ratio_fix_desc\">偏った照明の欠落を修正、誤ったアスペクト比や映像スケール、スクリーンショットが空白になる問題を修正します。性能に影響します!</string>\n    <string name=\"ambilight_ratio_fix\">Ambilight、アスペクト比、映像の大きさ、スクリーンショットの修正</string>\n    <string name=\"force_legacy_codecs_desc\">低性能なデバイスのパフォーマンスを大きく向上させます。最大解像度は720pです。</string>\n    <string name=\"force_legacy_codecs\">従来のコーデックを強制する (720p)</string>\n    <string name=\"live_stream_fix_desc\">警告: ライブ配信の早戻しが無効になります。低性能なデバイスでライブ配信のパフォーマンスを大きく向上させます。最大解像度は 1080p です。</string>\n    <string name=\"live_stream_fix_4k_desc\">警告: ライブ配信の早戻しが無効になります。低性能なデバイスでライブ配信のパフォーマンスを大きく向上させます。最大解像度は 4K です。</string>\n    <string name=\"live_stream_fix\">ライブ配信の修正 (1080p)</string>\n    <string name=\"live_stream_fix_4k\">ライブ配信の修正 (4K)</string>\n    <string name=\"playback_notifications_fix_desc\">トラックの変更通知を非表示にします。AOSPのファームウェアで役立つことがあります。</string>\n    <string name=\"playback_notifications_fix\">再生の通知を無効化する</string>\n    <string name=\"amlogic_fix_desc\">Amlogic のデバイスのフレームドロップを修正します。</string>\n    <string name=\"amlogic_fix\">Amlogic 1080p\\@60fps の修正</string>\n    <string name=\"tunneled_video_playback_desc\">注意: 一時停止が正しく機能しないことがあります！トンネルモードの動画再生は、オーディオと映像の同期 (AVSync) の向上、よりスムーズな再生などにおいて有益です。Android 5以降が必要です。</string>\n    <string name=\"tunneled_video_playback\">トンネルモードの動画再生 (Android 5 以降)</string>\n    <string name=\"master_volume\">マスター音量</string>\n    <string name=\"volume_limit\">音量の上限</string>\n    <string name=\"player_volume\">音量</string>\n    <string name=\"play_video\">再生</string>\n    <string name=\"remember_position_of_short_videos\">短い (5 分未満) 動画の位置を記憶する</string>\n    <string name=\"remember_position_of_live_videos\">ライブ配信の位置を記憶する</string>\n    <string name=\"player_show_tooltips\">ボタンの説明を表示する</string>\n    <string name=\"action_like_unset\">高評価を削除しました</string>\n    <string name=\"action_dislike_unset\">低評価を削除しました</string>\n    <string name=\"various_buttons\">メイン画面上部のボタン</string>\n    <string name=\"not_compatible_with\">選択したオプションは互換性がありません</string>\n    <string name=\"subtitle_position\">字幕の下部シフト</string>\n    <string name=\"pressing_home\">ホームボタンを押下</string>\n    <string name=\"pressing_home_back\">ホームボタンか戻るボタンを押下</string>\n    <string name=\"pressing_back\">戻るボタンを押下</string>\n    <string name=\"player_number_key_seek\">数字ボタンでシーク</string>\n    <string name=\"save_remove_playlist\">再生リストのセクションに追加または削除</string>\n    <string name=\"save_playlist\">再生リストのセクションに追加</string>\n    <string name=\"remove_playlist\">再生リストのセクションから永久的に削除</string>\n    <string name=\"remove_playlist_fmt\">「%s」を完全に消去しますか？</string>\n    <string name=\"removed_from_playlists\">再生リストのセクションから削除しました</string>\n    <string name=\"saved_to_playlists\">再生リストのセクションに追加しました</string>\n    <string name=\"create_playlist\">再生リストを作成</string>\n    <string name=\"add_video_to_new_playlist\">新規の再生リストに追加</string>\n    <string name=\"cant_delete_empty_playlist\">空の再生リストは削除できません</string>\n    <string name=\"playlist\">再生リスト</string>\n    <string name=\"rename_playlist\">再生リストの名前を変更</string>\n    <string name=\"cant_rename_empty_playlist\">空の再生リストの名前は変更できません</string>\n    <string name=\"cant_rename_foreign_playlist\">外部の再生リストの名前は変更できません</string>\n    <string name=\"cant_save_playlist\">この再生リストは追加できません</string>\n    <string name=\"enter_value\">空白でない値を入力してください</string>\n    <string name=\"dialog_add_remove_from\">「%s」に追加または削除</string>\n    <string name=\"lb_playback_controls_skip_next\">次へスキップ</string>\n    <string name=\"lb_playback_controls_skip_previous\">前へスキップまたは開始位置に巻き戻す</string>\n    <string name=\"alt_presets_behavior\">プリセットの代替動作 (帯域制限)</string>\n    <string name=\"alt_presets_behavior_desc\">選択したプリセットに合致する解像度、FPS、コーデックの代わりに、対応する帯域幅の維持を試みます。</string>\n    <string name=\"sleep_timer\">スリープタイマー (1 時間リモコンが無操作の場合)</string>\n    <string name=\"sleep_timer_desc\">1時間リモコンを使用していない場合、再生を一時停止します</string>\n    <string name=\"disable_vsync\">垂直同期信号 (VSync) を同期しない</string>\n    <string name=\"disable_vsync_desc\">画面の垂直同期信号でフレームの同期を無効化します。CPUリソースを解放して、低性能なデバイスの反応性を向上させます。</string>\n    <string name=\"skip_codec_profile_check\">コーデックの確認を省略</string>\n    <string name=\"skip_codec_profile_check_desc\">動画の開始時にコーデックが対応するかを確認しません。バグが多いファームウェアでは役に立つかもしれません。</string>\n    <string name=\"force_sw_codec\">SW 映像デコーダーを強制する</string>\n    <string name=\"force_sw_codec_desc\">ほぼすべての動画を再生できますが、パフォーマンスは非常に悪くなります。</string>\n    <string name=\"playlist_order\">再生リストを並べ替え</string>\n    <string name=\"playlist_order_added_date_newer_first\">追加日時 (新しい順)</string>\n    <string name=\"playlist_order_added_date_older_first\">追加日時 (古い順)</string>\n    <string name=\"playlist_order_published_date_newer_first\">投稿日時 (新しい順)</string>\n    <string name=\"playlist_order_published_date_older_first\">投稿日時 (古い順)</string>\n    <string name=\"playlist_order_popularity\">人気順</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">外部の再生リストでは実行できません</string>\n    <string name=\"owned_playlist_warning\">このアクションは自分の再生リストでのみ実行できます</string>\n    <string name=\"content_block_alt_server\">代替のサーバーを使用する</string>\n    <string name=\"content_block_alt_server_desc\">SponsorBlock が動作しない場合のみ有効化してください。</string>\n    <string name=\"unset_stream_reminder\">ライブ配信のリマインダーを解除</string>\n    <string name=\"set_stream_reminder\">ライブ配信のリマインダーを設定</string>\n    <string name=\"playback_starts_shortly\">ライブ配信の開始時に自動で再生を開始します</string>\n    <string name=\"starting_stream\">ライブ配信が開始しました…</string>\n    <string name=\"action_playlist_remove\">再生リストから削除</string>\n    <string name=\"signin_view_title\">ユーザーコードを読み込んでいます…</string>\n    <string name=\"signin_view_description\">このページにコードを入力してログインしてください: %s\\n注意: Firefox か Chrome でのみ動作します。</string>\n    <string name=\"signin_view_action_text\">完了</string>\n    <string name=\"require_checked\">「%s」が必要です</string>\n    <string name=\"msg_press_again_to_exit\">もう一度押すと終了します</string>\n    <string name=\"player_remaining_time\">残り時間: %s</string>\n    <string name=\"player_ending_time\">終了時間: %s</string>\n    <string name=\"badge_new_content\">新しい動画</string>\n    <string name=\"badge_live\">ライブ</string>\n    <string name=\"add_device_view_description\">デバイスをリンクするには、YouTube アプリ内の「設定」から「テレビで見る」を選択してください\\n注意: Gboard の文字入力のみで動作します。</string>\n    <string name=\"playback_controls_repeat_pause\">一時停止リピート</string>\n    <string name=\"playback_controls_repeat_list\">一覧リピート</string>\n    <string name=\"action_subscribe_off\">チャンネルの登録を OFF</string>\n    <string name=\"action_subscribe_on\">チャンネルの登録を ON</string>\n    <string name=\"skip_24_rate\">24FPS の形式をスキップする (リアルシネマモードの修正\\？)</string>\n    <string name=\"skip_shorts\">ショート動画をスキップ</string>\n    <string name=\"player_disable_suggestions\">おすすめを無効化する</string>\n    <string name=\"suggestions\">提案</string>\n    <string name=\"feedback\">フィードバック</string>\n    <string name=\"sources\">ソース</string>\n    <string name=\"releases\">リリース</string>\n    <string name=\"prefer_avc_over_vp9\">コーデックの選択: VP9 より AVC を優先する</string>\n    <string name=\"open_chat\">チャットとコメント</string>\n    <string name=\"open_comments\">コメント欄を開く</string>\n    <string name=\"place_chat_left\">チャットを左に表示する</string>\n    <string name=\"place_comments_left\">コメントを左側に表示する</string>\n    <string name=\"use_alt_speech_recognizer\">代替の音声認識方法</string>\n    <string name=\"time_format\">時刻の形式</string>\n    <string name=\"time_format_24\">24 時間</string>\n    <string name=\"time_format_12\">12 時間 (午前と午後)</string>\n    <string name=\"use_alt_speech_recognizer_desc\">重大なバグがあります。デフォルトの認識方法で問題がある場合にのみ使用してください。</string>\n    <string name=\"speech_recognizer\">音声認識</string>\n    <string name=\"speech_engine\">音声検索エンジン</string>\n    <string name=\"speech_recognizer_system\">システム (推奨)</string>\n    <string name=\"speech_recognizer_external_1\">外部 1 (深刻なバグがあります。マイクの権限を無効にする必要があります)</string>\n    <string name=\"speech_recognizer_external_2\">外部 2 (深刻なバグがあります)</string>\n    <string name=\"real_channel_icon\">チャンネルボタンにアイコンを表示する</string>\n    <string name=\"subtitle_yellow_semi_transparent\">半透明背景に黄色字</string>\n    <string name=\"subtitle_yellow_black\">黒い背景に黄色字</string>\n    <string name=\"player_pixel_ratio\">ピクセルアスペクト比</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">ダークグレー (モノクロ)</string>\n    <string name=\"disable_mic_permission\">この認識方法を使うには、このアプリのマイクの権限を無効化してください。</string>\n    <string name=\"repeat_mode_shuffle\">再生リストをシャッフルする</string>\n    <string name=\"chat_left\">左</string>\n    <string name=\"chat_right\">右</string>\n    <string name=\"card_real_thumbnails\">サムネイルを動画内の 1 フレームに置き換える</string>\n    <string name=\"card_content\">サムネイルの取得位置</string>\n    <string name=\"thumb_quality_default\">デフォルト</string>\n    <string name=\"thumb_quality_start\">動画の開始</string>\n    <string name=\"thumb_quality_middle\">動画の中盤</string>\n    <string name=\"thumb_quality_end\">動画の終了</string>\n    <string name=\"content_block_status\">SponsorBlock のステータスを確認する</string>\n    <string name=\"dearrow_status\">DeArrow のステータスを確認する</string>\n    <string name=\"player_show_quality_info_bitrate\">画質の情報にビットレートの情報を追加する</string>\n    <string name=\"player_speed_button_old_behavior\">再生速度ボタンを旧仕様に戻す</string>\n    <string name=\"protect_settings_with_password\">すべての設定をパスワードで保護をする</string>\n    <string name=\"enter_settings_password\">設定パスワードを入力してください</string>\n    <string name=\"child_mode\">チャイルドモード</string>\n    <string name=\"child_mode_desc\">このモードでは、検索とおすすめの機能は使用できません。設定はパスワードで保護されます。</string>\n    <string name=\"lost_setting_warning\">設定が変更されます。バックアップを作成しておいてください。</string>\n    <string name=\"player_button_long_click\">追加のオプションを表示するには長押し</string>\n    <string name=\"pause_history\">履歴を一時停止する</string>\n    <string name=\"resume_history\">履歴を再開する</string>\n    <string name=\"disable_history\">履歴を無効化する</string>\n    <string name=\"enable_history\">履歴を有効化する</string>\n    <string name=\"clear_history\">履歴を消去する</string>\n    <string name=\"chapters\">チャプター</string>\n    <string name=\"card_multiline_subtitle\">マルチライン字幕</string>\n    <string name=\"auto_frame_rate_modes\">対応モードの一覧</string>\n    <string name=\"loading\">読み込み中…</string>\n    <string name=\"hide_streams\">登録チャンネルからライブ配信を非表示にする</string>\n    <string name=\"video_rotate\">回転</string>\n    <string name=\"video_flip\">反転 (ミラー)</string>\n    <string name=\"trending_searches\">急上昇の検索</string>\n    <string name=\"video_duration_any\">すべて</string>\n    <string name=\"video_duration\">再生時間</string>\n    <string name=\"video_duration_under_4\">4 分以下</string>\n    <string name=\"video_duration_between_4_20\">4 〜 20 分</string>\n    <string name=\"video_duration_over_20\">20 分以上</string>\n    <string name=\"content_type\">タイプ</string>\n    <string name=\"content_type_any\">すべて</string>\n    <string name=\"content_type_video\">動画</string>\n    <string name=\"content_type_channel\">チャンネル</string>\n    <string name=\"content_type_playlist\">再生リスト</string>\n    <string name=\"content_type_movie\">映画</string>\n    <string name=\"video_features\">特徴</string>\n    <string name=\"video_feature_any\">すべて</string>\n    <string name=\"video_feature_live\">ライブ配信</string>\n    <string name=\"video_feature_4k\">4K</string>\n    <string name=\"video_feature_hdr\">HDR</string>\n    <string name=\"search_sorting\">並べ替え</string>\n    <string name=\"sort_by_relevance\">関連度順</string>\n    <string name=\"sort_by_views\">視聴回数</string>\n    <string name=\"sort_by_date\">アップロード日</string>\n    <string name=\"sort_by_rating\">評価</string>\n    <string name=\"clear_search_history\">検索履歴を消去</string>\n    <string name=\"remove_from_subscriptions\">非表示にする</string>\n    <string name=\"player_long_speed_list\">長い速度リスト</string>\n    <string name=\"player_extra_long_speed_list\">とても長い速度リスト</string>\n    <string name=\"alt_app_icon\">代替のアプリアイコン (要再起動)</string>\n    <string name=\"network_stack\">%s のネットワークスタックを優先する</string>\n    <string name=\"player_network_stack\">ネットワークエンジン</string>\n    <string name=\"cronet_desc\">Cronet は、Chromium のネットワークスタックです。これは遅延を減らし、ネットワーク性能を向上させることでバッファの問題を解決できます。</string>\n    <string name=\"unlock_all_formats\">すべての動画の形式をアンロックする</string>\n    <string name=\"unlock_all_formats_desc\">一部のデバイスで対応している形式であっても、ファームウェアがいくつかの形式に非対応と誤って報告します。\\n(例: 1080p のスマートテレビは、4K に非対応と報告する傾向があります)</string>\n    <string name=\"okhttp_desc\">このネットワークエンジンは低速ですが、良好な安定性があります。</string>\n    <string name=\"select_channel_section\">ATV チャンネルから起動した際に対応したセクションを開く</string>\n    <string name=\"enable_voice_search_desc\">公式アプリを「ブリッジ」に置き換えます。これには、グローバル検索リクエストをこのアプリへ転送する役割があります。</string>\n    <string name=\"enable_master_password\">マスターパスワードを有効化する</string>\n    <string name=\"enter_master_password\">マスターパスワードを入力</string>\n    <string name=\"disable_stream_buffer\">ライブ配信のバッファを無効化する</string>\n    <string name=\"disable_stream_buffer_desc\">ライブ配信が大きく遅延を起こす問題を修正します。注意: ライブ配信でラグが起きる可能性があります。</string>\n    <string name=\"sony_frame_drop_fix\">フレームドロップの修正 #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">Sony 製のテレビやその他デバイスの遅延を修正します。注意: オーディオの同期に問題が発生する可能性があります。</string>\n    <string name=\"audio_language\">オーディオの言語</string>\n    <string name=\"old_home_look\">ホームセクションを古い外観に戻す</string>\n    <string name=\"old_channel_look\">チャンネルページを古い外観に戻す</string>\n    <string name=\"hide_shorts_everywhere\">ショート動画を完全に非表示にする</string>\n    <string name=\"update_found\">更新</string>\n    <string name=\"volume_boost_warning\">上限を超える設定は、スピーカーが破損する可能性があります</string>\n    <string name=\"play_video_incognito\">シークレットモードで再生</string>\n    <string name=\"play_from_start\">最初から再生</string>\n    <string name=\"header_kids_home\">キッズ</string>\n    <string name=\"player_section_playlist\">現在のセクションを再生リストとして扱う</string>\n    <string name=\"header_trending\">急上昇</string>\n    <string name=\"screensaver\">スクリーンセーバー</string>\n    <string name=\"player_screen_off_timeout\">一定時間後に画面を OFF</string>\n    <string name=\"old_update_notifications\">更新の通知を旧仕様に戻す</string>\n    <string name=\"dialog_notification\">更新の通知をポップアップダイアログで表示する</string>\n    <string name=\"mark_as_watched\">視聴済みにする</string>\n    <string name=\"player_ui_animations\">プレーヤー UI のアニメーションを有効化</string>\n    <string name=\"player_likes_count\">高評価と低評価数を表示する</string>\n    <string name=\"sorting_alphabetically2\">アルファベット順 (高速)</string>\n    <string name=\"sorting_default\">デフォルト (高速)</string>\n    <string name=\"content_block_exclude_channel\">チャンネルに SponsorBlock を適用しない</string>\n    <string name=\"content_block_stop_excluding_channel\">チャンネルに SponsorBlock を適用する</string>\n    <string name=\"player_chapter_notification\">ポップアップ通知でチャプターを素早く進める</string>\n    <string name=\"player_chapter_notification2\">通知をクリックで次のチャプターに切り替え</string>\n    <string name=\"subtitle_remember\">現在のチャンネルでのみ字幕を有効化する</string>\n    <string name=\"amazon_frame_drop_fix\">フレームドロップの修正 #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">Amazon Fire TV 向け、他のデバイスでも効果がある場合があります。</string>\n    <string name=\"default_stack_desc\">内蔵のネットワークエンジンです。特定の状況下では優れた安定性があります。</string>\n    <string name=\"item_postion\">位置の設定:</string>\n    <string name=\"player_screen_off_dimming\">画面 OFF 時の減光量</string>\n    <string name=\"screensaver_timout\">一定時間後にスクリーンセーバーを起動する</string>\n    <string name=\"screensaver_dimming\">スクリーンセーバーの減光量</string>\n    <string name=\"player_ui_on_next\">次の動画再生時にプレーヤーユーザーインターフェースを表示する</string>\n    <string name=\"autogenerated\">自動生成</string>\n    <string name=\"player_auto_volume\">音量を自動調整する</string>\n    <string name=\"header_shorts\">ショート動画</string>\n    <string name=\"player_global_focus\">プレーヤーボタンの行間でフォーカスを同期</string>\n    <string name=\"auto_history\">自動 (アカウント設定を使用)</string>\n    <string name=\"remember_position_subscriptions\">登録チャンネルのリストの最終閲覧位置を記憶する</string>\n    <string name=\"remember_position_pinned\">固定した再生リストの最終閲覧位置を記憶する</string>\n    <string name=\"msg_player_unknown_error\">不明なエラー</string>\n    <string name=\"unknown_source_error\">不明なソースのエラー</string>\n    <string name=\"unknown_renderer_error\">不明なレンダラーのエラー</string>\n    <string name=\"header_notifications\">通知</string>\n    <string name=\"disable_search_history\">検索履歴を無効化する</string>\n    <string name=\"unlock_high_bitrate_formats\">高ビットレート 1080p VP9 形式をアンロックする</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">高ビットレート MP4A 形式をアンロックする</string>\n    <string name=\"color_scheme_blue\">ブルー</string>\n    <string name=\"color_scheme_dark_blue\">ダークブルー</string>\n    <string name=\"player_speed_per_channel\">各チャンネル</string>\n    <string name=\"disable_popular_searches\">人気の検索クエリを無効化する</string>\n    <string name=\"multi_profiles\">アカウントごとに個別の設定を使用する</string>\n    <string name=\"protect_account_with_password\">このアカウントをパスワードで保護をする</string>\n    <string name=\"enter_account_password\">アカウントパスワードを入力してください</string>\n    <string name=\"show_connect_messages\">接続メッセージを表示する</string>\n    <string name=\"prefer_avc_over_vp9_desc\">警告: 1080p が最大です</string>\n    <string name=\"play_next\">次を再生</string>\n    <string name=\"hide_watched_from_subscriptions\">登録チャンネルから視聴済みの動画を非表示にする</string>\n    <string name=\"hide_watched_from_notifications\">視聴済みの動画を通知から非表示にする</string>\n    <string name=\"hide_unwanted_content\">動画を隠す</string>\n    <string name=\"hide_watched_from_home\">ホームから視聴済みの動画を非表示にする</string>\n    <string name=\"hide_watched_from_watch_later\">「後で見る」の再生リストから視聴済みの動画を非表示にする</string>\n    <string name=\"remote_control_permission\">バックグラウンドリモコンはオーバーレイの権限の許可が必要です</string>\n    <string name=\"login_from_browser\">ブラウザからログインする</string>\n    <string name=\"disable_remote_history\">リモコン使用時の履歴を無効化する</string>\n    <string name=\"keyboard_fix\">OK ボタンでキーボードを開かないようにする (G20s とその他)</string>\n    <string name=\"nothing_found\">何も見つかりませんでした</string>\n    <string name=\"auto_frame_rate_desc\">このオプションは、スポーツのライブ配信などのカメラの動きが速いシーンでのジッターを除去します</string>\n    <string name=\"channel_filter_hint\">チャンネルを検索</string>\n    <string name=\"player_loop_shorts\">ショート動画をループ再生する</string>\n    <string name=\"player_quick_shorts_skip\">左右のボタンでショート動画をスキップする</string>\n    <string name=\"player_quick_skip_videos\">右か左のボタンで動画をスキップする</string>\n    <string name=\"channels_filter\">チャンネルのセクション内にチャンネルの絞り込みを表示する</string>\n    <string name=\"channel_search_bar\">チャンネルページ内の検索バー</string>\n    <string name=\"header_sports\">スポーツ</string>\n    <string name=\"keep_finished_activities\">完了時のアクティビティを保持する</string>\n    <string name=\"disable_channels_service\">チャンネルサービスを無効化する</string>\n    <string name=\"replace_titles\">タイトルを置き換える</string>\n    <string name=\"enable\">有効化</string>\n    <string name=\"more_info\">詳細情報</string>\n    <string name=\"about_sponsorblock\">SponsorBlock について</string>\n    <string name=\"about_dearrow\">DeArrow について</string>\n    <string name=\"replace_thumbnails\">サムネイルを置き換える</string>\n    <string name=\"crowdsourced_thumbnails\">クラウドソースのサムネイル</string>\n    <string name=\"crowdsoursed_titles\">クラウドソースのタイトル</string>\n    <string name=\"dearrow_not_submitted_thumbs\">サムネイルのソース (DeArrow に存在しない場合)</string>\n    <string name=\"pitch_effect\">ピッチエフェクト</string>\n    <string name=\"fullscreen_mode\">フルスクリーンモード (ナビゲーションバーなし)</string>\n    <string name=\"player_only_mode\">動画がアプリ外で開かれた場合にプレーヤーのみを表示する</string>\n    <string name=\"pinned_channel_rows\">固定されたチャンネルを行として表示</string>\n    <string name=\"prefer_ipv4\">IPv4 DNS を優先する</string>\n    <string name=\"prefer_ipv4_desc\">アプリが全く動作しない状態を修正できる可能性があります。\\n注意: ハングやクラッシュが発生する可能性があります (特に Android 8 のデバイスまたは Dune HD)</string>\n    <string name=\"long_press_for_settings\">長押しで設定を開きます</string>\n    <string name=\"long_press_for_options\">長押しで設定を開く</string>\n    <string name=\"device_specific_backup\">このデバイスのみでバックアップ</string>\n    <string name=\"local_backup\">ローカルにバックアップ</string>\n    <string name=\"auto_backup\">自動バックアップ (1 日 1 回実行)</string>\n    <string name=\"repeat_mode_reverse_list\">再生リストまたはチャンネルの動画を逆順に再生する</string>\n    <string name=\"calm_msg\">問題を修正中です。時々更新を確認してください。</string>\n    <string name=\"without_picture\">画像なし</string>\n    <string name=\"video_disabled\">動画が無効になっています</string>\n    <string name=\"applying_fix\">プレーヤーを閉じないでください。修正を適用中しています…</string>\n    <string name=\"hide_mixes\">ミックスを非表示にする</string>\n    <string name=\"player_global_focus_desc\">この機能はプレーヤーボタンの列間を遷移する際にどのプレーヤーボタンにフォーカスが当たるかに影響します。</string>\n    <string name=\"disable_network_error_fixing\">ネットワークエラー自動修復を無効化</string>\n    <string name=\"disable_network_error_fixing_desc\">VPN を使用している場合は、このオプションを有効化する必要があります。</string>\n    <string name=\"recommended\">おすすめ</string>\n    <string name=\"add_to_subscriptions_group\">登録チャンネルグループの追加または削除</string>\n    <string name=\"new_subscriptions_group\">新規グループ</string>\n    <string name=\"rename_group\">登録チャンネルグループの名前を変更</string>\n    <string name=\"screen_dimming_amount\">画面の減光量</string>\n    <string name=\"screen_dimming_timeout\">画面減光のタイムアウト</string>\n    <string name=\"playlists_rows\">再生リストのセクションを行として表示</string>\n    <string name=\"import_subscriptions_group\">インポート</string>\n    <string name=\"lb_playback_controls_closed_captioning_disable\">クローズドキャプションを無効化</string>\n    <string name=\"lb_playback_controls_closed_captioning_enable\">クローズドキャプションを有効化</string>\n    <string name=\"my_videos\">作成した動画</string>\n    <string name=\"player_audio_focus\">オーディオフォーカス (他のプレーヤーが検出された場合に一時停止)</string>\n    <string name=\"paid_content_notification\">課金コンテンツの通知</string>\n    <string name=\"you_liked\">高評価</string>\n    <string name=\"premium_users_only\">プレミアムユーザー限定、不完全な動画の形式一覧を修正します。</string>\n    <string name=\"playback_buffering_fix\">再生バッファリングの修正</string>\n    <string name=\"oculus_quest_fix\">Oculus Quest の修正</string>\n    <string name=\"card_preview_muted\">サウンドなしの動画</string>\n    <string name=\"card_preview_full\">サウンドありの動画</string>\n    <string name=\"card_preview\">カードプレビュー</string>\n    <string name=\"card_unlocalized_titles\">未翻訳の動画タイトル</string>\n    <string name=\"dont_resize_video_to_fit_dialog\">ダイアログに合わせて動画のサイズを変更しない</string>\n    <string name=\"typing_corrections\">テキストの入力時に自動修正を無効にする</string>\n    <string name=\"suggestions_horizontally_scrolled\">水平スクロールの提案</string>\n    <string name=\"stable_restore\">Stable ビルドに復元 (すべてのデータが失われます)</string>\n    <string name=\"dialog_block_channel\">チャンネルをブロック</string>\n    <string name=\"dialog_unblock_channel\">ブロックを解除</string>\n    <string name=\"confirm_block_channel\">%s のすべてのコンテンツを非表示にしますか？</string>\n    <string name=\"channel_blocked\">チャンネルをブロックしました</string>\n    <string name=\"channel_unblocked\">ブロックを解除しました</string>\n    <string name=\"header_blocked_channels\">ブロックしたチャンネル</string>\n    <string name=\"msg_no_blocked_channels\">ブロックしたチャンネルはありません</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-ko/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">홈</string>\n  <string name=\"title_search\">검색</string>\n  <string name=\"header_subscriptions\">구독</string>\n  <string name=\"header_history\">시청 기록</string>\n  <string name=\"header_music\">음악</string>\n  <string name=\"header_news\">뉴스</string>\n  <string name=\"header_gaming\">게임</string>\n  <string name=\"header_playlists\">재생목록</string>\n  <string name=\"header_settings\">설정</string>\n  <string name=\"header_channels\">채널</string>\n  <string name=\"subscriptions_signin_title\">좋아하는 채널의 최신 정보 보기</string>\n  <string name=\"subscriptions_signin_subtitle\">구독한 채널을 보려면 로그인하세요</string>\n  <string name=\"subscriptions_signin_button_text\">로그인</string>\n  <string name=\"library_signin_to_show_more\">구독하거나 저장 또는 좋아요 표시한 동영상 보기</string>\n  <string name=\"library_signin_subtitle\">로그인하여 라이브러리를 확인하세요</string>\n  <string name=\"action_signin\">로그인</string>\n  <string name=\"title_video_formats\">동영상 형식</string>\n  <string name=\"title_audio_formats\">오디오 형식</string>\n  <string name=\"subtitle_category_title\">자막</string>\n  <string name=\"playback_queue_category_title\">재생 대기열</string>\n  <string name=\"video_max_quality\">자동 (최고 품질)</string>\n  <string name=\"audio_max_quality\">자동 (최고 품질)</string>\n  <string name=\"subtitles_disabled\">자막 끄기</string>\n  <string name=\"auto_frame_rate\">자동 프레임레이트</string>\n  <string name=\"frame_rate_correction\">fps 고정:\\n%s</string>\n  <string name=\"category_background_playback\">백그라운드 재생</string>\n  <string name=\"not_implemented\">구현되지 않음</string>\n  <string name=\"option_background_playback_off\">사용 안 함</string>\n  <string name=\"option_background_playback_pip\">픽처 인 픽처</string>\n  <string name=\"option_background_playback_only_audio\">오디오만 듣기</string>\n  <string name=\"playback_settings\">재생 품질 설정</string>\n  <string name=\"video_speed\">재생 속도</string>\n  <string name=\"resolution_switch\">해상도 변경</string>\n  <string name=\"video_buffer\">동영상 버퍼</string>\n  <string name=\"video_buffer_size_none\">없음</string>\n  <string name=\"video_buffer_size_low\">낮음</string>\n  <string name=\"video_buffer_size_med\">중간</string>\n  <string name=\"video_buffer_size_high\">높음</string>\n  <string name=\"update_changelog\">변경 내역</string>\n  <string name=\"install_update\">업데이트 설치</string>\n  <string name=\"section_is_empty\">이곳에는 아무것도 없어요.</string>\n  <string name=\"dialog_add_to_playlist\">재생 목록에서 추가/제거</string>\n  <string name=\"msg_signed_users_only\">로그인된 사용자 전용</string>\n  <string name=\"msg_cant_load_content\">콘텐츠를 로드할 수 없습니다.\\n1) 기기에서 날짜와 시간을 확인하세요.\\n2) 네트워크 연결을 확인하세요.\\n3) 프록시 설정을 확인하세요.\\n4) 계정에 로그인해 보세요.\\n5) 아마도 여기에는 아무것도 없을 것입니다.</string>\n  <string name=\"title_video_presets\">동영상 사전 설정</string>\n  <string name=\"settings_accounts\">계정</string>\n  <string name=\"settings_left_panel\">카테고리 편집</string>\n  <string name=\"settings_themes\">테마</string>\n  <string name=\"settings_other\">기타</string>\n  <string name=\"settings_player\">동영상 플레이어</string>\n  <string name=\"settings_language\">언어</string>\n  <string name=\"settings_linked_devices\">연결된 장치</string>\n  <string name=\"settings_about\">앱 정보</string>\n  <string name=\"dialog_account_list\">계정 선택</string>\n  <string name=\"dialog_account_none\">없음</string>\n  <string name=\"dialog_remove_account\">로그아웃</string>\n  <string name=\"dialog_add_account\">로그인</string>\n  <string name=\"default_lang\">기본</string>\n  <string name=\"dialog_select_language\">언어</string>\n  <string name=\"subtitle_default\">기본</string>\n  <string name=\"subtitle_white_semi_transparent\">반투명한 배경</string>\n  <string name=\"subtitle_style\">자막 스타일</string>\n  <string name=\"subtitle_language\">자막 언어</string>\n  <string name=\"action_search\">검색</string>\n  <string name=\"settings_main_ui\">사용자 인터페이스</string>\n  <string name=\"dialog_main_ui\">사용자 인터페이스</string>\n  <string name=\"card_animated_previews\">애니메이션 미리보기</string>\n  <string name=\"web_site\">웹 사이트</string>\n  <string name=\"donation\">기부</string>\n  <string name=\"dialog_about\">어플리케이션 정보</string>\n  <string name=\"dialog_player_ui\">동영상 플레이어</string>\n  <string name=\"player_show_ui_on_pause\">일시 정지 시 UI 표시</string>\n  <string name=\"player_pause_on_ok\">확인 버튼으로 재생 일시 정지</string>\n  <string name=\"player_ok_button_behavior\">확인 버튼 설정</string>\n  <string name=\"player_only_ui\">UI 만</string>\n  <string name=\"player_ui_and_pause\">UI 및 일시 정지</string>\n  <string name=\"player_only_pause\">일시 정지만</string>\n  <string name=\"check_for_updates\">업데이트 확인</string>\n  <string name=\"update_not_found\">최신 버전을 사용하고 있습니다.</string>\n  <string name=\"update_in_progress\">잠시만 기다려주세요...</string>\n  <string name=\"player_ui_hide_behavior\">UI 자동 숨김</string>\n  <string name=\"option_never\">안 함</string>\n  <string name=\"side_panel_sections\">섹션 설정</string>\n  <string name=\"boot_to_section\">앱 시작시 섹션</string>\n  <string name=\"large_ui\">큰 UI</string>\n  <string name=\"video_grid_scale\">동영상 격자 크기</string>\n  <string name=\"scale_ui\">UI 크기</string>\n  <string name=\"color_scheme\">색 구성</string>\n  <string name=\"color_scheme_default\">기본</string>\n  <string name=\"color_scheme_red_grey\">붉은 회색</string>\n  <string name=\"color_scheme_red\">빨간색</string>\n  <string name=\"color_scheme_dark_grey\">어두운 회색</string>\n  <string name=\"disable_update_check\">업데이트 확인 비활성화</string>\n  <string name=\"show_again\">다시 표시</string>\n  <string name=\"check_updates_auto\">새 버전 알림</string>\n  <string name=\"select_account_on_boot\">시작 시 선택</string>\n  <string name=\"player_other\">기타</string>\n  <string name=\"player_full_date\">설명에 정확한 날짜 표시</string>\n  <string name=\"open_channel\">채널 열기</string>\n  <string name=\"open_playlist\">재생 목록 열기</string>\n  <string name=\"not_interested\">관심 없음</string>\n  <string name=\"not_recommend_channel\">채널 추천 안 함</string>\n  <string name=\"you_wont_see_this_video\">추천 목록에서 영상이 삭제되었습니다</string>\n  <string name=\"you_wont_see_this_channel\">이 채널은 추천에서 표시되지 않습니다</string>\n  <string name=\"settings_search\">검색</string>\n  <string name=\"dialog_search\">검색</string>\n  <string name=\"instant_voice_search\">실시간 음성 검색</string>\n  <string name=\"option_background_playback_behind\">뒤에서 재생</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">다음 동영상 정보가 아직 로드되지 않았습니다</string>\n  <string name=\"card_multiline_title\">여러 줄 제목</string>\n  <string name=\"cards_style\">카드 스타일</string>\n  <string name=\"repeat_mode_all\">영상 연속 재생</string>\n  <string name=\"repeat_mode_one\">현재 영상 반복</string>\n  <string name=\"repeat_mode_pause\">매 영상 재생후 일시 정지</string>\n  <string name=\"repeat_mode_pause_alt\">재생 목록의 동영상만 연속 재생</string>\n  <string name=\"repeat_mode_none\">매 영상마다 재생 정지</string>\n  <string name=\"subscribed_to_channel\">채널 구독중</string>\n  <string name=\"unsubscribed_from_channel\">채널 구독</string>\n  <string name=\"subtitle_yellow_transparent\">투명 배경에 노란색</string>\n  <string name=\"subtitle_white_transparent\">투명한 배경에 흰색</string>\n  <string name=\"subtitle_white_black\">검정색 배경에 흰색</string>\n  <string name=\"player_seek_preview\">탐색 중 미리보기</string>\n  <string name=\"color_scheme_dark_grey_oled\">어두운 회색 (OLED)</string>\n  <string name=\"channels_section_sorting\">채널 섹션의 정렬</string>\n  <string name=\"sorting_by_new_content\">새 콘텐츠순</string>\n  <string name=\"sorting_alphabetically\">알파벳순</string>\n  <string name=\"sorting_last_viewed\">마지막으로 본 항목순</string>\n  <string name=\"player_pause_when_seek\">탐색바 사용중 일시 정지</string>\n  <string name=\"playlists_style\">재생 목록 섹션 스타일</string>\n  <string name=\"playlists_style_grid\">격자</string>\n  <string name=\"playlists_style_rows\">행</string>\n  <string name=\"player_show_clock\">팝업 메뉴에 시계 표시</string>\n  <string name=\"player_show_remaining_time\">남은 시간 표시</string>\n  <string name=\"player_show_ending_time\">종료 시간 표시</string>\n  <string name=\"open_channel_uploads\">채널 업로드 열기</string>\n  <string name=\"app_exit_shortcut\">앱 종료</string>\n  <string name=\"player_exit_shortcut\">플레이어 나가기</string>\n  <string name=\"app_exit_none\">종료하지 않기</string>\n  <string name=\"app_double_back_exit\">뒤로 버튼 두번</string>\n  <string name=\"app_single_back_exit\">뒤로 버튼 한번</string>\n  <string name=\"action_video_zoom\">동영상 화면 맞춤</string>\n  <string name=\"video_zoom\">동영상 화면 맞춤</string>\n  <string name=\"video_zoom_default\">기본</string>\n  <string name=\"video_zoom_fit_width\">가로 맞춤</string>\n  <string name=\"video_zoom_fit_height\">세로 맞춤</string>\n  <string name=\"video_zoom_fit_both\">가로 또는 세로 맞춤</string>\n  <string name=\"video_zoom_stretch\">전체화면으로 늘이기</string>\n  <string name=\"color_scheme_teal\">청록색</string>\n  <string name=\"color_scheme_teal_oled\">청록색 (OLED)</string>\n  <string name=\"player_seek_preview_none\">사용 안 함</string>\n  <string name=\"player_seek_preview_single\">단일 프레임</string>\n  <string name=\"player_seek_preview_carousel\">구간보기</string>\n  <string name=\"player_seek_preview_carousel_slow\">구간보기 (느림)</string>\n  <string name=\"player_seek_preview_carousel_fast\">구간보기 (빠름)</string>\n  <string name=\"unsubscribe_from_channel\">채널 구독 취소</string>\n  <string name=\"card_title_lines_num\">카드 제목 줄 개수</string>\n  <string name=\"card_auto_scrolled_title\">잘린 제목 자동 스크롤</string>\n  <string name=\"share_link\">공유 링크</string>\n  <string name=\"share_qr_link\">공유 링크 (QR 코드)</string>\n  <string name=\"subscribe_to_channel\">채널 구독</string>\n  <string name=\"wait_data_loading\">데이터 로드 중...</string>\n  <string name=\"auto_frame_rate_pause\">자동 프레임속도 (전환 시 일시정지)</string>\n  <string name=\"auto_frame_rate_applying\">자동 프레임속도 %sx%s\\@%s를 적용하는 중</string>\n  <string name=\"audio_shift\">오디오 싱크</string>\n  <string name=\"player_remember_speed\">재생속도 기억하기</string>\n  <string name=\"mark_channel_as_watched\">시청한 동영상으로 표시</string>\n  <string name=\"channel_marked_as_watched\">시청된 동영상으로 표시되었습니다.</string>\n  <string name=\"dialog_add_device\">장치 추가</string>\n  <string name=\"dialog_remove_all_devices\">모든 장치 제거</string>\n  <string name=\"device_link_enabled\">연결 활성화됨</string>\n  <string name=\"device_connected\">장치 \\\"%s\\\"이(가) 연결되었습니다</string>\n  <string name=\"device_disconnected\">장치 \\\"%s\\\"이(가) 연결이 끊어졌습니다.</string>\n  <string name=\"settings_ui_scale\">UI 크기</string>\n  <string name=\"msg_restart_app\">이 설정을 적용하려면 앱을 다시 시작 해주세요.</string>\n  <string name=\"settings_block\">콘텐츠 차단</string>\n  <string name=\"msg_applying\">%s을(를) 적용하는 중...</string>\n  <string name=\"msg_done\">완료</string>\n  <string name=\"settings_general\">일반</string>\n  <string name=\"settings_video\">동영상</string>\n  <string name=\"content_block_confirm_skip\">건너뛸 때 확인</string>\n  <string name=\"content_block_categories\">구간 건너뛰기</string>\n  <string name=\"content_block_sponsor\">스폰서 광고</string>\n  <string name=\"content_block_intro\">무음 / 인트로 애니메이션</string>\n  <string name=\"content_block_outro\">최종 화면 / 크레딧</string>\n  <string name=\"content_block_interaction\">상호 작용 요청 (구독)</string>\n  <string name=\"content_block_self_promo\">자체 홍보 구간</string>\n  <string name=\"content_block_music_off_topic\">음악이 아닌 구간</string>\n  <string name=\"confirm_segment_skip\">구간 \\\"%s\\\"을(를) 건너뛰겠습니까\\?</string>\n  <string name=\"cancel_segment_skip\">구간 건너뛰기 취소</string>\n  <string name=\"msg_skipping_segment\">구간 \\\"%s\\\"을(를) 건너뛰는 중…</string>\n  <string name=\"content_block_notification_type\">알림 유형</string>\n  <string name=\"content_block_notify_none\">알림 없음</string>\n  <string name=\"content_block_notify_toast\">토스트 팝업</string>\n  <string name=\"content_block_notify_dialog\">확인 대화 상자</string>\n  <string name=\"return_to_launcher\">ATV 채널/검색에서 런처로 돌아가기</string>\n  <string name=\"intent_force_close\">외부 소스에서 동영상을 열었을 경우 강제 종료</string>\n  <string name=\"btn_confirm\">확인</string>\n  <string name=\"player_low_video_quality\">낮은 동영상 품질</string>\n  <string name=\"remote_session_closed\">원격 세션이 종료되었습니다.</string>\n  <string name=\"msg_mode_switch_error\">표시 모드를 \\\"%s\\\"(으)로 전환할 수 없습니다.</string>\n  <string name=\"msg_player_error\">플레이어 오류 %s이(가) 발생했습니다. 다시 재생하는 중...</string>\n  <string name=\"video_preset_disabled\">사전 설정 없음</string>\n  <string name=\"video_preset_enabled\">다음 동영상 포맷은 프리셋에 따라 설정됩니다.</string>\n  <string name=\"player_sleep_timer\">수면 타이머</string>\n  <string name=\"player_show_quality_info\">동영상 화질 정보 표시</string>\n  <string name=\"player_remember_each_speed\">각 동영상의 속도 기억</string>\n  <string name=\"player_remember_speed_none\">없음</string>\n  <string name=\"player_remember_speed_all\">모든 동영상에서 같음</string>\n  <string name=\"player_remember_speed_each\">각 동영상 마다</string>\n  <string name=\"msg_player_error_source\">동영상을 다운로드할 수 없습니다.</string>\n  <string name=\"msg_player_error_renderer\">선택한 동영상 포맷은 지원되지 않습니다.</string>\n  <string name=\"msg_player_error_video_renderer\">선택한 비디오 포맷이 지원되지 않습니다.\\n플레이어에서 HQ 버튼을 눌러 다른 포맷을 선택해 보세요.\\n그래도 문제가 해결되지 않으면 장치를 재부팅해 보세요.</string>\n  <string name=\"msg_player_error_audio_renderer\">선택한 오디오 포맷이 지원되지 않습니다.\\n플레이어에서 HQ 버튼을 눌러 다른 포맷을 선택해 보세요.\\n그래도 문제가 해결되지 않으면 장치를 재부팅해 보세요.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">선택한 자막 포맷이 지원되지 않습니다.\\n플레이어에서 CC 버튼을 길게 눌러 다른 포맷을 선택해 보세요.\\n그래도 문제가 해결되지 않으면 장치를 재부팅해 보세요.</string>\n  <string name=\"msg_player_error_unexpected\">알 수 없는 동영상 디코더 오류</string>\n  <string name=\"video_aspect\">화면 비율</string>\n  <string name=\"player_tweaks\">개발자 옵션</string>\n  <string name=\"header_uploads\">업로드</string>\n  <string name=\"player_show_global_clock\">화면에 시계를 계속 표시</string>\n  <string name=\"player_show_global_ending_time\">화면에 동영상 종료 시간을 계속 표시</string>\n  <string name=\"app_corner_clock\">홈: 오른쪽 상단 모서리 시계</string>\n  <string name=\"player_corner_clock\">플레이어: 오른쪽 상단 모서리 시계</string>\n  <string name=\"player_corner_ending_time\">플레이어: 오른쪽 상단 모서리 종료 시간</string>\n  <string name=\"uploads_old_look\">업로드 섹션 화면을 구버전으로 되돌리기</string>\n  <string name=\"background_playback_activation\">백그라운드 재생 (활성화)</string>\n  <string name=\"channels_old_look\">채널 섹션 화면을 구버전으로 되돌리기</string>\n  <string name=\"channels_auto_load\">채널 섹션 콘텐츠를 자동으로 로드하기</string>\n  <string name=\"return_to_background_video\">백그라운드에서 실행 중인 동영상으로 돌아가기</string>\n  <string name=\"pin_unpin_from_sidebar\">사이드바에 고정/해제</string>\n  <string name=\"pin_unpin_playlist\">사이드바에서 재생목록 고정/고정 해제</string>\n  <string name=\"pin_unpin_channel\">사이드바에서 채널 고정/고정 해제</string>\n  <string name=\"pin_playlist\">사이드바에 재생 목록 추가</string>\n  <string name=\"pin_channel\">사이드바에 채널 추가하기</string>\n  <string name=\"pinned_to_sidebar\">사이드바에 고정됨</string>\n  <string name=\"unpin_from_sidebar\">사이드바에서 고정 해제</string>\n  <string name=\"unpinned_from_sidebar\">사이드바에서 고정 해제됨</string>\n  <string name=\"dialog_select_country\">국가</string>\n  <string name=\"settings_language_country\">언어/국가</string>\n  <string name=\"share_embed_link\">임베드 링크 공유</string>\n  <string name=\"card_text_scroll_factor\">카드 텍스트 스크롤 속도</string>\n  <string name=\"preferred_update_source\">업데이트 소스 선택</string>\n  <string name=\"hide_shorts\">구독한 채널에서 쇼츠 숨기기</string>\n  <string name=\"hide_shorts_channel\">채널에서 쇼츠 숨기기</string>\n  <string name=\"key_remapping\">키 다시 매핑</string>\n  <string name=\"screen_dimming\">화면 디밍</string>\n  <string name=\"removed_from_playback_queue\">재생 대기열에서 제거됨</string>\n  <string name=\"added_to_playback_queue\">재생 대기열에 추가됨</string>\n  <string name=\"add_remove_from_playback_queue\">재생 대기열에서 추가/제거</string>\n  <string name=\"add_to_playback_queue\">재생 대기열에 추가</string>\n  <string name=\"remove_from_playback_queue\">재생 대기열에서 제거</string>\n  <string name=\"proxy_enabled\">프록시 사용</string>\n  <string name=\"proxy_disabled\">프록시 사용 안 함</string>\n  <string name=\"uploads_row_name\">업로드</string>\n  <string name=\"playlists_row_name\">재생 목록 만들기</string>\n  <string name=\"popular_uploads_row_name\">인기 업로드</string>\n  <string name=\"news_row_name\">뉴스</string>\n  <string name=\"breaking_news_row_name\">뉴스 속보</string>\n  <string name=\"covid_news_row_name\">코로나19 뉴스</string>\n  <string name=\"enable_voice_search\">음성 검색 사용</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">채널 구독/구독 취소</string>\n  <string name=\"app_backup_restore\">백업/복원</string>\n  <string name=\"app_backup\">데이터 백업</string>\n  <string name=\"app_restore\">백업에서 데이터 복원</string>\n  <string name=\"skip_each_segment_once\">구간을 다시 건너뛰지 않음</string>\n  <string name=\"disable_ok_long_press\">확인 버튼 길게 누르기 비활성화</string>\n  <string name=\"disable_ok_long_press_desc\">확인 버튼이 제대로 작동하지 않는 버그가 있는 컨트롤러용</string>\n  <string name=\"player_time_correction\">속도에 따라서 시간 표시</string>\n  <string name=\"sponsor_color_markers\">재생바에 색상 마커 표시</string>\n  <string name=\"refresh_section\">섹션 새로 고침</string>\n  <string name=\"option_disabled\">사용 안 함</string>\n  <string name=\"live_now_row_name\">실시간</string>\n  <string name=\"run_in_background\">백그라운드에서 재생</string>\n  <string name=\"settings_remote_control\">원격 제어</string>\n  <string name=\"background_service_started\">백그라운드 서비스가 시작되었습니다.</string>\n  <string name=\"dialog_add_to\">%s에 추가</string>\n  <string name=\"dialog_remove_from\">%s에서 제거</string>\n  <string name=\"added_to\">%s에 추가됨</string>\n  <string name=\"removed_from\">%s에서 제거됨</string>\n  <string name=\"video_preset_adaptive\">자동</string>\n  <string name=\"content_block_preview_recap\">동영상 미리보기 또는 요약</string>\n  <string name=\"content_block_highlight\">동영상의 포인트나 하이라이트</string>\n  <string name=\"removed_from_history\">동영상이 기록에서 삭제</string>\n  <string name=\"remove_from_history\">기록에서 제거</string>\n  <string name=\"upload_date\">업로드 날짜</string>\n  <string name=\"upload_date_any\">전체 시간</string>\n  <string name=\"upload_date_today\">오늘</string>\n  <string name=\"upload_date_this_week\">이번 주</string>\n  <string name=\"upload_date_this_month\">이번 달</string>\n  <string name=\"upload_date_this_year\">올해</string>\n  <string name=\"double_refresh_rate\">이중 주사율</string>\n  <string name=\"upload_date_last_hour\">마지막 시간</string>\n  <string name=\"focus_on_search_results\">검색 결과에 대한 자동 초점</string>\n  <string name=\"context_menu\">상황에 맞는 메뉴</string>\n  <string name=\"add_remove_from_recent_playlist\">최근 재생 목록에서 추가/제거</string>\n  <string name=\"hide_settings_section\">설정 섹션 숨기기 (위험!)</string>\n  <string name=\"volume\">볼륨 %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s 초</string>\n  <string name=\"audio_shift_sec\">%s 초</string>\n  <string name=\"ui_hide_timeout_sec\">%s 초</string>\n  <string name=\"screen_dimming_timeout_min\">%s 분</string>\n  <string name=\"mark_all_channels_watched\">모든 채널을 시청한 것으로 표시</string>\n  <string name=\"move_section_up\">섹션 위로 이동</string>\n  <string name=\"move_section_down\">섹션 아래로 이동</string>\n  <string name=\"player_buttons\">플레이어 버튼 설정</string>\n  <string name=\"action_playlist_add\">재생목록에 추가</string>\n  <string name=\"action_video_stats\">동영상 통계</string>\n  <string name=\"action_subscribe\">구독</string>\n  <string name=\"action_channel\">채널 열기</string>\n  <string name=\"action_pip\">PIP</string>\n  <string name=\"action_video_info\">동영상 설명</string>\n  <string name=\"action_screen_off\">화면 끄기</string>\n  <string name=\"action_sound_off\">소리 끄기</string>\n  <string name=\"action_playback_queue\">재생 대기열</string>\n  <string name=\"action_video_speed\">동영상 속도</string>\n  <string name=\"action_subtitles\">자막</string>\n  <string name=\"action_like\">좋아요</string>\n  <string name=\"action_dislike\">싫어요</string>\n  <string name=\"action_play_pause\">재생/일시정지</string>\n  <string name=\"action_repeat_mode\">재생 모드</string>\n  <string name=\"action_next\">다음 동영상</string>\n  <string name=\"action_previous\">이전 동영상</string>\n  <string name=\"action_high_quality\">동영상 품질</string>\n  <string name=\"content_block_no_skipping_mode\">건너뛰기 비활성화 모드</string>\n  <string name=\"content_block_action_type\">작업 선택</string>\n  <string name=\"content_block_action_none\">아무일 안 함</string>\n  <string name=\"content_block_action_only_skip\">건너뛰기만</string>\n  <string name=\"content_block_action_toast\">알림과 함께 건너뛰기</string>\n  <string name=\"content_block_action_dialog\">확인 대화상자 표시</string>\n  <string name=\"content_block_filler\">주제에서 벗어남 (필터)</string>\n  <string name=\"keyboard_auto_show\">자동으로 키보드 표시</string>\n  <string name=\"cancel_dialog\">취소</string>\n  <string name=\"msg_player_error_source2\">동영상 소스가 작동하지 않거나 잘못된 시간</string>\n  <string name=\"msg_player_error_video_source\">동영상 URL이 작동하지 않거나 장치의 시간이 잘못되었습니다.\\n일반적으로 앱 문제입니다.</string>\n  <string name=\"msg_player_error_audio_source\">오디오 URL이 작동하지 않거나 장치의 시간이 잘못되었습니다.\\n일반적으로 앱 문제입니다.</string>\n  <string name=\"msg_player_error_subtitle_source\">자막 URL이 작동하지 않거나 장치의 시간이 잘못되었습니다.\\n일반적으로 앱 문제입니다.</string>\n  <string name=\"hide_upcoming\">구독에서 예정된 항목 숨기기</string>\n  <string name=\"hide_upcoming_channel\">채널에서 예정된 항목 숨기기</string>\n  <string name=\"hide_upcoming_home\">홈에서 예정된 항목 숨기기</string>\n  <string name=\"search_background_playback\">검색/오픈 채널에 PIP 입력</string>\n  <string name=\"trending_row_name\">트렌드</string>\n  <string name=\"player_seek_type\">탐색 동작</string>\n  <string name=\"player_seek_regular\">정기적</string>\n  <string name=\"player_seek_confirmation_pause\">확인 포함 (찾는 동안 일시정지)</string>\n  <string name=\"player_seek_confirmation_play\">확인 있음 (찾으면서 재생)</string>\n  <string name=\"hide_shorts_from_home\">홈에서 쇼츠를 숨김</string>\n  <string name=\"finish_on_disconnect\">휴대폰/태블릿 연결을 해제한 후 앱 종료</string>\n  <string name=\"update_error\">업데이트 오류</string>\n  <string name=\"hide_shorts_from_history\">기록에서 쇼츠 숨기기</string>\n  <string name=\"hide_shorts_from_trending\">트렌드에서 쇼츠 숨기기</string>\n  <string name=\"disable_screensaver\">화면 보호기 비활성화</string>\n  <string name=\"description_not_found\">설명을 찾을 수 없음</string>\n  <string name=\"proxy_port_hint\">프록시 포트</string>\n  <string name=\"proxy_host_hint\">프록시 호스트이름 또는 IP</string>\n  <string name=\"proxy_username_hint\">프록시 사용자명</string>\n  <string name=\"proxy_password_hint\">프록시 비밀번호</string>\n  <string name=\"proxy_type\">프록시 유형</string>\n  <string name=\"enable_web_proxy\">웹 프록시 사용</string>\n  <string name=\"proxy_not_supported\">웹 프록시 사용 (안드로이드 4.4 이상 필요)</string>\n  <string name=\"proxy_test_btn\">테스트</string>\n  <string name=\"proxy_settings_title\">프록시 서버 설정</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com\t</string>\n  <string name=\"proxy_test_cancelled\">테스트#%d: 취소되었습니다.</string>\n  <string name=\"proxy_type_invalid\">프록시 유형이 설정되지 않았습니다.</string>\n  <string name=\"proxy_host_invalid\">프록시 호스트가 설정되지 않았습니다.</string>\n  <string name=\"proxy_port_invalid\">잘못된 프록시 포트이며, 0보다 커야 합니다.</string>\n  <string name=\"proxy_credentials_invalid\">잘못된 사용자 이름 또는 비밀번호입니다.</string>\n  <string name=\"proxy_test_aborted\">테스트가 중단되었으므로, 먼저 프록시 설정을 수정하세요.</string>\n  <string name=\"proxy_application_aborted\">먼저 프록시 설정을 수정하세요.</string>\n  <string name=\"proxy_test_start\">테스트#%d: %s...</string>\n  <string name=\"proxy_test_error\">오류#%d: %s</string>\n  <string name=\"proxy_test_status\">상태#%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">구성 파일 주소 (*.ovpn)</string>\n  <string name=\"enable_openvpn\">OpenVPN 사용</string>\n  <string name=\"openvpn_settings_title\">OpenVPN 설정</string>\n  <string name=\"openvpn_application_aborted\">먼저 OpenVPN 설정을 수정하세요.</string>\n  <string name=\"openvpn_test_aborted\">테스트가 중단되었습니다. 먼저 OpenVPN 설정을 수정하세요.</string>\n  <string name=\"openvpn_address_invalid\">OpenVPN 구성 주소가 설정되지 않았습니다.</string>\n  <string name=\"internet_censorship\">인터넷 검열</string>\n  <string name=\"rename_section\">이 섹션의 이름 바꾸기</string>\n  <string name=\"simple_edit_value_hint\">새로운 값</string>\n  <string name=\"seek_interval\">탐색 간격</string>\n  <string name=\"seek_interval_sec\">%s 초</string>\n  <string name=\"subtitle_system\">시스템 스타일 (안드로이드 설정 &gt; 접근성)</string>\n  <string name=\"subtitle_scale\">자막 스케일</string>\n  <string name=\"audio_sync_fix_desc\">오디오/동영상을 동기화하는 다른 방법입니다. 구식으로 간주되지만 경우에 따라 도움이 될 수 있습니다.</string>\n  <string name=\"audio_sync_fix\">오디오 동기화 수정</string>\n  <string name=\"ambilight_ratio_fix_desc\">누락된 바이어스 조명을 수정합니다. 잘못된 화면비를 수정합니다. 빈 스크린샷을 수정합니다. 성능에 영향을 미칩니다!</string>\n  <string name=\"ambilight_ratio_fix\">엠비라이트/화면비/스크린샷 수정</string>\n  <string name=\"force_legacy_codecs_desc\">저사양 기기의 성능을 크게 향상시킵니다. 최대 해상도는 720p입니다.</string>\n  <string name=\"force_legacy_codecs\">강제 레거시 코덱 (720p)</string>\n  <string name=\"live_stream_fix_desc\">저사양 장치에서 생방송 스트리밍 성능을 크게 향상시킵니다. 최대 해상도는 1080p입니다.</string>\n  <string name=\"live_stream_fix_4k_desc\">주의, 이 설정은 방송중 뒤로가기 기능을 비활성화합니다. 라이브 스트리밍 성능이 크게 향상됩니다. 최대 해상도는 4K입니다.</string>\n  <string name=\"live_stream_fix\">생방송 스트리밍 수정 (1080p)</string>\n  <string name=\"live_stream_fix_4k\">생방송 스트림 수정 (4K)</string>\n  <string name=\"playback_notifications_fix_desc\">트랙 변경 알림을 숨깁니다. AOSP 기반 펌웨어에서 유용할 수 있습니다.</string>\n  <string name=\"playback_notifications_fix\">재생 알림 비활성화</string>\n  <string name=\"amlogic_fix_desc\">AMLogic 기반 장치의 프레임 드롭 수정</string>\n  <string name=\"amlogic_fix\">AMLogic 1080p\\@60fps 수정</string>\n  <string name=\"tunneled_video_playback_desc\">참고: 일시정지가 제대로 작동하지 않을 수 있습니다! 터널링된 동영상 재생은 더 나은 오디오/동영상 동기화 (AV 동기화) 및 더 부드러운 재생과 같은 이점을 약속합니다. 안드로이드 5 이상이 필요합니다.</string>\n  <string name=\"tunneled_video_playback\">터널링된 동영상 재생 (안드로이드 5 이상)</string>\n  <string name=\"master_volume\">마스터 볼륨</string>\n  <string name=\"volume_limit\">볼륨 제한</string>\n  <string name=\"player_volume\">볼륨</string>\n  <string name=\"play_video\">재생</string>\n  <string name=\"remember_position_of_short_videos\">5분 이내의 짧은 동영상의 재생위치 기억하기</string>\n  <string name=\"remember_position_of_live_videos\">실시간 스트리밍 재생위치 기억하기</string>\n  <string name=\"player_show_tooltips\">버튼 툴팁 표시</string>\n  <string name=\"action_like_unset\">좋아요 설정되지 않음</string>\n  <string name=\"action_dislike_unset\">싫어요 설정되지 않음</string>\n  <string name=\"various_buttons\">기본 창 상단의 버튼</string>\n  <string name=\"not_compatible_with\">선택한 옵션은 다음과 호환되지 않습니다: </string>\n  <string name=\"subtitle_position\">자막 하단 이동</string>\n  <string name=\"pressing_home\">홈을 누름</string>\n  <string name=\"pressing_home_back\">홈 또는 뒤로가기를 누름</string>\n  <string name=\"pressing_back\">뒤로가기를 누름</string>\n  <string name=\"player_number_key_seek\">숫자 키로 찾기</string>\n  <string name=\"save_remove_playlist\">재생목록 섹션에서 재생목록 추가/삭제</string>\n  <string name=\"save_playlist\">재생 목록 섹션에 재생 목록 추가</string>\n  <string name=\"remove_playlist\">재생목록 섹션에서 재생목록을 영구적으로 삭제</string>\n  <string name=\"removed_from_playlists\">재생목록 섹션에서 삭제됨</string>\n  <string name=\"saved_to_playlists\">재생목록 섹션에 추가됨</string>\n  <string name=\"create_playlist\">재생목록 만들기</string>\n  <string name=\"add_video_to_new_playlist\">새로운 재생목록에 추가</string>\n  <string name=\"cant_delete_empty_playlist\">빈 재생목록을 삭제할 수 없습니다.</string>\n  <string name=\"playlist\">재생목록</string>\n  <string name=\"rename_playlist\">재생목록 이름 바꾸기</string>\n  <string name=\"cant_rename_empty_playlist\">빈 재생목록의 이름을 변경할 수 없습니다.</string>\n  <string name=\"cant_rename_foreign_playlist\">외국 재생 목록의 이름을 바꿀 수 없습니다</string>\n  <string name=\"cant_save_playlist\">이 재생목록을 추가할 수 없습니다.</string>\n  <string name=\"enter_value\">비어 있지 않은 값을 입력하세요.</string>\n  <string name=\"dialog_add_remove_from\">%s에서 추가/제거</string>\n  <string name=\"lb_playback_controls_skip_next\">다음으로 건너뛰기</string>\n  <string name=\"lb_playback_controls_skip_previous\">시작 위치로 이전/되감기 건너뛰기</string>\n  <string name=\"alt_presets_behavior\">대체 사전 설정 동작 (대역폭 제한)</string>\n  <string name=\"alt_presets_behavior_desc\">앱은 해상도, fps, 코덱을 일치시키는 대신 선택한 사전 설정에 해당하는 대역폭을 유지하려고 합니다.</string>\n  <string name=\"sleep_timer\">취침 타이머 (리모컨을 1시간 동안 사용하지 않은 경우)</string>\n  <string name=\"sleep_timer_desc\">사용자가 1시간 동안 리모컨을 사용하지 않으면 재생을 일시 정지합니다.</string>\n  <string name=\"disable_vsync\">수직동기화로 스냅 비활성화</string>\n  <string name=\"disable_vsync_desc\">재생 성능을 약간 향상시킵니다.</string>\n  <string name=\"skip_codec_profile_check\">코덱 프로파일 수준 확인 건너뛰기</string>\n  <string name=\"skip_codec_profile_check_desc\">동영상을 시작할 때 코덱 지원을 확인하지 마세요. 버그가 있는 펌웨어에 도움이 될 수 있습니다.</string>\n  <string name=\"force_sw_codec\">강제 SW 동영상 디코더</string>\n  <string name=\"force_sw_codec_desc\">거의 모든 동영상을 재생할 수 있지만 성능이 매우 나쁩니다.</string>\n  <string name=\"playlist_order\">재생목록 정렬</string>\n  <string name=\"playlist_order_added_date_newer_first\">추가된 날짜 (최근순)</string>\n  <string name=\"playlist_order_added_date_older_first\">추가된 날짜 (이전 날짜순)</string>\n  <string name=\"playlist_order_published_date_newer_first\">발매일 (최신순)</string>\n  <string name=\"playlist_order_published_date_older_first\">발매일 (이전순)</string>\n  <string name=\"playlist_order_popularity\">인기도</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">외국 재생목록에 대해 이 작업을 수행할 수 없습니다.</string>\n  <string name=\"owned_playlist_warning\">오류입니다. 이 작업은 소유한 재생목록에만 적용할 수 있습니다.</string>\n  <string name=\"content_block_alt_server\">대체 서버 사용</string>\n  <string name=\"content_block_alt_server_desc\">SponsorBlock이 다른 이유로 작동을 거부하는 경우 이 옵션을 활성화합니다.</string>\n  <string name=\"unset_stream_reminder\">스트리밍 알림 설정 해제</string>\n  <string name=\"set_stream_reminder\">스트리밍 알림 설정</string>\n  <string name=\"playback_starts_shortly\">스트리밍이 준비되면 재생이 자동으로 시작됩니다.</string>\n  <string name=\"starting_stream\">스트리밍 시작 중...</string>\n  <string name=\"action_playlist_remove\">재생목록에서 삭제</string>\n  <string name=\"signin_view_title\">사용자 코드 불러오는 중...</string>\n  <string name=\"signin_view_description\">로그인하려면 %s페이지에 이 코드를 입력하세요.\\n참고. 파이어폭스 또는 크롬 브라우저에서만 작동합니다.</string>\n  <string name=\"signin_view_action_text\">완료</string>\n  <string name=\"require_checked\">\\\"%s\\\" 필요</string>\n  <string name=\"msg_press_again_to_exit\">종료하려면 다시 누르세요.</string>\n  <string name=\"player_remaining_time\">남은 시간: %s</string>\n  <string name=\"player_ending_time\">%s에 종료</string>\n  <string name=\"badge_new_content\">새로운 콘텐츠</string>\n  <string name=\"badge_live\">생방송</string>\n  <string name=\"add_device_view_description\">기기를 연결하려면 휴대전화의 유튜브 앱 설정/TV에서 보기 섹션에 이 코드를 입력하세요. Gboard 키보드에서만 작동합니다.\\n참조. Gboard 키보드에서만 작동합니다.</string>\n  <string name=\"playback_controls_repeat_pause\">일시정지 반복</string>\n  <string name=\"playback_controls_repeat_list\">반복 목록</string>\n  <string name=\"action_subscribe_off\">구독 해제</string>\n  <string name=\"action_subscribe_on\">구독하기</string>\n  <string name=\"skip_24_rate\">24fps 형식 건너뛰기 (실제 시네마 모드 수정\\?)</string>\n  <string name=\"skip_shorts\">쇼츠 스킵</string>\n  <string name=\"player_disable_suggestions\">제안 비활성화</string>\n  <string name=\"feedback\">피드백</string>\n  <string name=\"sources\">소스</string>\n  <string name=\"releases\">출시</string>\n  <string name=\"prefer_avc_over_vp9\">코덱 선택: vp9보다 avc 선호</string>\n  <string name=\"open_chat\">채팅/댓글</string>\n  <string name=\"open_comments\">댓글 열기</string>\n  <string name=\"place_chat_left\">왼쪽에 채팅 배치</string>\n  <string name=\"use_alt_speech_recognizer\">대체 음성 인식기</string>\n  <string name=\"time_format\">시간 형식</string>\n  <string name=\"time_format_24\">24 시간</string>\n  <string name=\"time_format_12\">12 시간 (오전/오후)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">심각한 버그가 발생했습니다. 기본 인식기에 문제가 있는 경우에만 사용하세요.</string>\n  <string name=\"speech_recognizer\">음성 인식기</string>\n  <string name=\"speech_recognizer_system\">시스템 (선호)</string>\n  <string name=\"speech_recognizer_external_1\">외부 1 (심각한 버그 발생, 작동하려면 마이크 접속을 비활성화해야 함)</string>\n  <string name=\"speech_recognizer_external_2\">외부 2 (심각한 버그 발생)</string>\n  <string name=\"real_channel_icon\">채널 버튼에 아이콘 표시</string>\n  <string name=\"subtitle_yellow_semi_transparent\">반투명 배경에 노란색</string>\n  <string name=\"subtitle_yellow_black\">검정색 배경에 노란색</string>\n  <string name=\"player_pixel_ratio\">픽셀 비율</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">어두운 회색 (모노크롬)</string>\n  <string name=\"disable_mic_permission\">이 인식기가 제대로 작동하려면 앱에 대한 마이크 접속을 비활성화하세요.</string>\n  <string name=\"repeat_mode_shuffle\">모든 재생목록 셔플</string>\n  <string name=\"chat_left\">왼쪽</string>\n  <string name=\"chat_right\">오른쪽</string>\n  <string name=\"card_real_thumbnails\">썸네일을 동영상 프레임으로 교체</string>\n  <string name=\"card_content\">카드 썸네일을 가져오는 위치</string>\n  <string name=\"thumb_quality_default\">기본</string>\n  <string name=\"thumb_quality_start\">동영상 시작</string>\n  <string name=\"thumb_quality_middle\">동영상 중간</string>\n  <string name=\"thumb_quality_end\">동영상 끝</string>\n  <string name=\"content_block_status\">SponsorBlock 서버 상태 확인</string>\n  <string name=\"dearrow_status\">DeArrow 서버 상태 확인</string>\n  <string name=\"player_show_quality_info_bitrate\">품질 정보에 비트 전송률 추가</string>\n  <string name=\"player_speed_button_old_behavior\">속도 버튼의 이전 동작 되돌리기</string>\n  <string name=\"protect_settings_with_password\">비밀번호로 모든 설정 보호</string>\n  <string name=\"enter_settings_password\">설정 비밀번호 입력</string>\n  <string name=\"child_mode\">어린이 모드</string>\n  <string name=\"child_mode_desc\">이 모드에서 사용자는 검색을 사용하거나 제안된 콘텐츠를 볼 수 없습니다. 설정은 비밀번호 아래에 있습니다.</string>\n  <string name=\"lost_setting_warning\">앱 설정이 변경됩니다. 설정 백업을 생성했는지 확인하세요.</string>\n  <string name=\"player_button_long_click\">버튼을 짧게 클릭하면 빠른 작업이 가능하고 길게 설정 대화상자가 표시됩니다.</string>\n  <string name=\"pause_history\">일시정지 내역</string>\n  <string name=\"resume_history\">기록 다시 시작</string>\n  <string name=\"disable_history\">기록 비활성화</string>\n  <string name=\"enable_history\">기록 활성화</string>\n  <string name=\"clear_history\">기록 지우기</string>\n  <string name=\"chapters\">챕터</string>\n  <string name=\"card_multiline_subtitle\">여러 줄 자막</string>\n  <string name=\"auto_frame_rate_modes\">지원되는 모드</string>\n  <string name=\"loading\">불러오는 중...</string>\n  <string name=\"hide_streams\">구독에서 스트리밍 숨기기</string>\n  <string name=\"video_rotate\">회전</string>\n  <string name=\"trending_searches\">인기 검색어</string>\n  <string name=\"video_duration_any\">모두</string>\n  <string name=\"video_duration\">기간</string>\n  <string name=\"video_duration_under_4\">4 분 미만</string>\n  <string name=\"video_duration_between_4_20\">4~20 분</string>\n  <string name=\"video_duration_over_20\">20 분 이상</string>\n  <string name=\"content_type\">유형</string>\n  <string name=\"content_type_any\">모두</string>\n  <string name=\"content_type_video\">동영상</string>\n  <string name=\"content_type_channel\">채널</string>\n  <string name=\"content_type_playlist\">재생목록</string>\n  <string name=\"content_type_movie\">영화</string>\n  <string name=\"video_features\">기능</string>\n  <string name=\"video_feature_any\">모두</string>\n  <string name=\"video_feature_live\">생방송</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">정렬 기준</string>\n  <string name=\"sort_by_relevance\">관련성</string>\n  <string name=\"sort_by_views\">조회수 보기</string>\n  <string name=\"sort_by_date\">업로드 날짜</string>\n  <string name=\"sort_by_rating\">평점</string>\n  <string name=\"clear_search_history\">검색 기록 지우기</string>\n  <string name=\"remove_from_subscriptions\">숨기기</string>\n  <string name=\"player_long_speed_list\">긴 속도 목록</string>\n  <string name=\"player_extra_long_speed_list\">매우 긴 속도 목록</string>\n  <string name=\"alt_app_icon\">대체 앱 아이콘 (재부팅 필요)</string>\n  <string name=\"network_stack\">%s 네트워크 스택 선호</string>\n  <string name=\"player_network_stack\">네트워크 스택</string>\n  <string name=\"cronet_desc\">Cronet은 Chromium 네트워크 스택입니다. Cronet은 대기 시간을 줄이고 네트워킹 성능을 향상시켜 버퍼링 문제를 해결할 수 있습니다.</string>\n  <string name=\"unlock_all_formats\">모든 비디오 형식 잠금 해제</string>\n  <string name=\"unlock_all_formats_desc\">일부 장치에서 펌웨어는 일부 형식이 지원되지 않더라도 잘못 보고합니다.\\n(예: 1080p 스마트 TV는 4k를 지원하지 않는 것으로 보고하는 경향이 있습니다).</string>\n  <string name=\"okhttp_desc\">이 네트워크 스택은 가장 느리지만 안정성이 좋습니다.</string>\n  <string name=\"select_channel_section\">앱이 ATV 채널에서 실행될 때 해당 섹션 열기</string>\n  <string name=\"enable_voice_search_desc\">공식 앱은 소위 브리지로 대체됩니다. 브리지는 전역 검색 요청을 앱으로 전송하는 데 도움이 됩니다.</string>\n  <string name=\"enable_master_password\">마스터 비밀번호 활성화</string>\n  <string name=\"enter_master_password\">마스터 비밀번호 입력</string>\n  <string name=\"disable_stream_buffer\">스트림에서 버퍼 비활성화</string>\n  <string name=\"disable_stream_buffer_desc\">스트림이 너무 뒤쳐지는 상황을 수정합니다. 참고: 스트림이 지연되기 시작할 수 있습니다.</string>\n  <string name=\"sony_frame_drop_fix\">프레임 드랍 수정 #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">소니 TV 및 일부 다른 장치의 지연을 수정합니다. 참고: 오디오 동기화에 문제가 있을 수 있습니다.</string>\n  <string name=\"audio_language\">오디오 언어</string>\n  <string name=\"old_home_look\">홈 섹션의 이전 모습</string>\n  <string name=\"old_channel_look\">체널 페이지의 이전 모습</string>\n  <string name=\"hide_shorts_everywhere\">모든 곳에서 쇼츠 숨기기</string>\n  <string name=\"update_found\">업데이트</string>\n  <string name=\"volume_boost_warning\">한도를 초과하여 설정하면 스피커가 손상될 수 있음</string>\n  <string name=\"play_video_incognito\">시크릿 모드로 플레이</string>\n  <string name=\"header_kids_home\">아동</string>\n  <string name=\"player_section_playlist\">현재 섹션 콘텐츠를 재생 목록으로 사용</string>\n  <string name=\"header_trending\">트렌드</string>\n  <string name=\"screensaver\">화면 보호기</string>\n  <string name=\"player_screen_off_timeout\">화면 자동 꺼짐 시간</string>\n  <string name=\"old_update_notifications\">업데이트 알림의 이전 모습</string>\n  <string name=\"dialog_notification\">팝업을 통해 업데이트 알림 받기</string>\n  <string name=\"mark_as_watched\">시청한 것으로 표시</string>\n  <string name=\"player_ui_animations\">제어판 애니메이션 활성화</string>\n  <string name=\"player_likes_count\">좋아요/싫어요 수 표시</string>\n  <string name=\"sorting_alphabetically2\">알파벳순 (빠름)</string>\n  <string name=\"sorting_default\">기본값 (빠름)</string>\n  <string name=\"content_block_exclude_channel\">SponsorBlock에서 이 채널 제외</string>\n  <string name=\"content_block_stop_excluding_channel\">SponsorBlock에서 이 채널 제외 중지</string>\n  <string name=\"player_chapter_notification\">챕터를 빠르게 진행할 수 있는 알림 표시</string>\n  <string name=\"subtitle_remember\">각 채널별로 활성화된 자막 기억</string>\n  <string name=\"amazon_frame_drop_fix\">프레임 드랍 수정 #2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">아마존 스틱 장치용입니다. 다른 장치에서도 작동할 수 있습니다.</string>\n  <string name=\"default_stack_desc\">내장 네트워크 스택입니다. 이 스택은 특정 상황에서 더 나은 안정성을 제공할 수 있습니다.</string>\n  <string name=\"item_postion\">위치</string>\n  <string name=\"player_screen_off_dimming\">화면 꺼짐 어둡기 농도</string>\n  <string name=\"screensaver_timout\">화면 보호기 작동 시작 시간</string>\n  <string name=\"screensaver_dimming\">화면 보호기 어둡기 농도</string>\n  <string name=\"player_ui_on_next\">다음 동영상으로 전환할 때 플레이어 UI 표시</string>\n  <string name=\"autogenerated\">자동 생성</string>\n  <string name=\"player_auto_volume\">자동 볼륨 조절</string>\n  <string name=\"header_shorts\">쇼츠</string>\n  <string name=\"player_global_focus\">플레이어 버튼 행 사이의 원활한 탐색</string>\n  <string name=\"auto_history\">자동 (계정 설정 사용)</string>\n  <string name=\"remember_position_subscriptions\">구독에서 마지막으로 본 위치 기억하기</string>\n  <string name=\"remember_position_pinned\">고정된 재생목록에서 마지막으로 본 위치 기억하기</string>\n  <string name=\"msg_player_unknown_error\">알 수 없는 오류</string>\n  <string name=\"header_notifications\">알림</string>\n  <string name=\"disable_search_history\">검색 기록 비활성화</string>\n  <string name=\"unlock_high_bitrate_formats\">높은 비트 전송율 1080p vp9 포맷 잠금 해제</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">높은 비트 전송율 MP4A 포맷 잠금 해제</string>\n  <string name=\"color_scheme_blue\">파란색</string>\n  <string name=\"color_scheme_dark_blue\">진한 파란색</string>\n  <string name=\"player_speed_per_channel\">각 채널 마다</string>\n  <string name=\"disable_popular_searches\">인기 검색어 비활성화</string>\n  <string name=\"multi_profiles\">계정별로 별도의 설정 사용</string>\n  <string name=\"protect_account_with_password\">이 계정을 비밀번호로 보호</string>\n  <string name=\"enter_account_password\">계정 비밀번호 입력</string>\n  <string name=\"show_connect_messages\">연결 메시지 표시</string>\n  <string name=\"prefer_avc_over_vp9_desc\">경고: 최대 1080p</string>\n  <string name=\"play_next\">다음 재생</string>\n  <string name=\"hide_watched_from_subscriptions\">구독에서 시청한 동영상 숨기기</string>\n  <string name=\"hide_watched_from_notifications\">알림에서 시청한 동영상 숨기기</string>\n  <string name=\"hide_unwanted_content\">원치 않는 콘텐츠 숨기기</string>\n  <string name=\"hide_watched_from_home\">홈에서 시청한 동영상 숨기기</string>\n  <string name=\"hide_watched_from_watch_later\">나중에 볼 동영상 재생 목록에서 시청한 동영상 숨기기</string>\n  <string name=\"remote_control_permission\">백그라운드 원격 제어에는 오버레이 권한이 필요</string>\n  <string name=\"login_from_browser\">브라우저에서 로그인</string>\n  <string name=\"disable_remote_history\">원격 제어 사용 시 기록 비활성화</string>\n  <string name=\"keyboard_fix\">확인 키를 눌러 키보드 표시 수정 (G20 및 기타)</string>\n  <string name=\"nothing_found\">찾을 수 없음</string>\n  <string name=\"auto_frame_rate_desc\">이 옵션은 스포츠 스트리밍과 같이 카메라가 빠르게 움직이는 장면에서 흔들림을 제거합니다.</string>\n  <string name=\"channel_filter_hint\">채널 필터링</string>\n  <string name=\"player_loop_shorts\">쇼츠 반복재생</string>\n  <string name=\"player_quick_shorts_skip\">왼쪽/오른쪽 버튼으로 쇼츠 건너뛰기</string>\n  <string name=\"channels_filter\">채널 섹션에서 채널 필터링 영역을 표시</string>\n  <string name=\"channel_search_bar\">채널 페이지 내의 검색창</string>\n  <string name=\"header_sports\">스포츠</string>\n  <string name=\"keep_finished_activities\">완료된 활동 유지</string>\n  <string name=\"disable_channels_service\">채널 서비스 비활성화</string>\n  <string name=\"replace_titles\">제목 변경</string>\n  <string name=\"enable\">활성화</string>\n  <string name=\"more_info\">더 많은 정보</string>\n  <string name=\"about_sponsorblock\">SponsorBlock 정보</string>\n  <string name=\"about_dearrow\">DeArrow 정보</string>\n  <string name=\"replace_thumbnails\">썸네일 변경</string>\n  <string name=\"crowdsourced_thumbnails\">유저들이 제공한 썸네일</string>\n  <string name=\"crowdsoursed_titles\">유저들이 제공한 제목</string>\n  <string name=\"dearrow_not_submitted_thumbs\">제공된 썸네일이 없을 경우 영상에서 가져올 위치</string>\n  <string name=\"pitch_effect\">피치 효과</string>\n  <string name=\"fullscreen_mode\">전체 화면 모드 (시스템 표시줄 없음)</string>\n  <string name=\"player_only_mode\">앱 외부에서 동영상을 연 경우 플레이어만 표시하기</string>\n  <string name=\"pinned_channel_rows\">고정된 채널을 행으로 표시</string>\n  <string name=\"prefer_ipv4\">IPv4 DNS 선호</string>\n  <string name=\"prefer_ipv4_desc\">앱이 전혀 작동하지 않는 상황을 고칠 수 있습니다.\\n주의. 멈춤 및 충돌이 발생할 수 있습니다 (특히 Android 8 기기 또는 Dune HD에서)</string>\n  <string name=\"long_press_for_settings\">길게 눌러서 설정 열기</string>\n  <string name=\"speech_engine\">음성 검색 엔진</string>\n  <string name=\"unknown_source_error\">알 수 없는 소스 오류</string>\n  <string name=\"unknown_renderer_error\">알 수 없는 렌더러 오류</string>\n  <string name=\"player_quick_skip_videos\">왼쪽/오른쪽 버튼으로 일반 동영상 건너뛰기</string>\n  <string name=\"long_press_for_options\">길게 눌러서 옵션 열기</string>\n  <string name=\"device_specific_backup\">이 기기에 대한 백업만</string>\n  <string name=\"local_backup\">로컬 백업</string>\n  <string name=\"auto_backup\">자동 백업 (하루에 한 번)</string>\n  <string name=\"repeat_mode_reverse_list\">재생목록 또는 채널 비디오를 역순으로 재생</string>\n  <string name=\"calm_msg\">문제를 해결하고 있습니다. 수시로 업데이트 확인</string>\n  <string name=\"without_picture\">사진 없음</string>\n  <string name=\"video_disabled\">비디오 비활성화</string>\n  <string name=\"applying_fix\">플레이어를 닫지 마세요. 수정 사항을 적용하는 중...</string>\n  <string name=\"video_buffer_size_lowest\">매우 낮음</string>\n  <string name=\"hide_mixes\">믹스 숨기기</string>\n  <string name=\"player_global_focus_desc\">이 기능은 플레이어 버튼 행 사이를 이동할 때 어떤 플레이어 버튼이 초점을 받을지 영향을 줌</string>\n  <string name=\"disable_network_error_fixing\">자동 네트워크 오류 수정 비활성화</string>\n  <string name=\"disable_network_error_fixing_desc\">VPN을 사용하는 경우 이 옵션을 활성화해야 할 수도 있음</string>\n  <string name=\"recommended\">추천</string>\n  <string name=\"video_buffer_size_highest\">가장 높음</string>\n  <string name=\"unpin_group_from_sidebar\">구독 그룹 제거</string>\n  <string name=\"place_comments_left\">왼쪽에 댓글 배치</string>\n  <string name=\"add_to_subscriptions_group\">구독 그룹 추가/제거</string>\n  <string name=\"new_subscriptions_group\">새 그룹</string>\n  <string name=\"rename_group\">구독 그룹 이름 바꾸기</string>\n  <string name=\"screen_dimming_amount\">화면 어둡기 양</string>\n  <string name=\"screen_dimming_timeout\">화면 어둡기 시간 초과</string>\n  <string name=\"playlists_rows\">재생목록 섹션을 행으로 표시</string>\n  <string name=\"not_supported_by_device\">해당 장치는 이 기능을 지원하지 않음</string>\n  <string name=\"suggestions\">제안</string>\n  <string name=\"context_menu_sorting\">상황에 맞는 메뉴 정렬</string>\n  <string name=\"hide_shorts_from_search\">검색 결과에서 쇼츠 숨기기</string>\n  <string name=\"import_subscriptions_group\">가져오기</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">폐쇄 자막 넣기 비활성화</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">폐쇄 자막 넣기 활성화</string>\n  <string name=\"search_exit_shortcut\">검색 나가기</string>\n  <string name=\"video_flip\">뒤집기 (미러)</string>\n  <string name=\"my_videos\">내 동영상</string>\n  <string name=\"player_audio_focus\">오디오 포커스 (다른 플레이어가 감지되면 일시 중지)</string>\n  <string name=\"paid_content_notification\">유료 콘텐츠 알림</string>\n  <string name=\"you_liked\">좋아요</string>\n  <string name=\"original_lang\">오리지널</string>\n  <string name=\"remove_playlist_fmt\">%s을(를) 영구적으로 제거할까요\\?</string>\n  <string name=\"premium_users_only\">프리미엄 사용자만 해당합니다. 불완전한 비디오 형식 목록 수정</string>\n  <string name=\"playback_buffering_fix\">재생 버퍼링 수정</string>\n  <string name=\"oculus_quest_fix\">오큘러스 퀘스트 수정</string>\n  <string name=\"card_preview_muted\">소리가 없는 영상</string>\n  <string name=\"card_preview_full\">소리가 있는 영상</string>\n  <string name=\"card_preview\">카드 미리보기</string>\n  <string name=\"card_unlocalized_titles\">현지화되지 않은 비디오 제목</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">대화에 맞게 비디오 크기를 조정하지 않기</string>\n  <string name=\"dialog_block_channel\">채널 차단</string>\n  <string name=\"dialog_unblock_channel\">차단 해제</string>\n  <string name=\"confirm_block_channel\">%s의 모든 콘텐츠를 숨기시겠습니까?</string>\n  <string name=\"channel_blocked\">채널이 차단됨</string>\n  <string name=\"channel_unblocked\">차단 해제됨</string>\n  <string name=\"header_blocked_channels\">차단된 채널</string>\n  <string name=\"msg_no_blocked_channels\">차단된 채널 없음</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-lt/strings.xml",
    "content": "<resources>\r\n    <string name=\"header_home\">Pagrindinis</string>\r\n    <string name=\"title_search\">Paieška</string>\r\n    <string name=\"header_subscriptions\">Prenumeratos</string>\r\n    <string name=\"header_history\">Istorija</string>\r\n    <string name=\"header_music\">Muzika</string>\r\n    <string name=\"header_news\">Naujienos</string>\r\n    <string name=\"header_gaming\">Žaidimai</string>\r\n    <string name=\"header_playlists\">Grojaraščiai</string>\r\n    <string name=\"header_settings\">Nustatymai</string>\r\n    <string name=\"header_channels\">Kanalai</string>\r\n    <string name=\"subscriptions_signin_title\">Peržiūrėkite naujausią informaciją iš mėgstamų kanalų</string>\r\n    <string name=\"subscriptions_signin_subtitle\">Prisijunkite, kad pamatytumėte savo prenumeratas</string>\r\n    <string name=\"subscriptions_signin_button_text\">PRISIJUNGTI</string>\r\n    <string name=\"library_signin_to_show_more\">Žiūrėkite patikusius, išsaugotus ar užsiprenumeruotus vaizdo įrašus</string>\r\n    <string name=\"library_signin_subtitle\">Prisijunkite, kad pamatytumėte savo biblioteką</string>\r\n    <string name=\"action_signin\">PRISIJUNKITE</string>\r\n    <string name=\"title_video_formats\">Vaizdo įrašų formatai</string>\r\n    <string name=\"title_audio_formats\">Garso formatai</string>\r\n    <string name=\"subtitle_category_title\">Subtitrai</string>\r\n    <string name=\"playback_queue_category_title\">Atkūrimo eilė</string>\r\n    <string name=\"video_max_quality\">Automatinis (aukščiausia kokybė)</string>\r\n    <string name=\"audio_max_quality\">Automatinis (aukščiausia kokybė)</string>\r\n    <string name=\"subtitles_disabled\">Subtitrai išjungti</string>\r\n    <string name=\"auto_frame_rate\">Automatinis kadrų dažnis</string>\r\n    <string name=\"frame_rate_correction\">Pataisykite fps:\\n%s</string>\r\n    <string name=\"category_background_playback\">Foninis atkūrimas</string>\r\n    <string name=\"not_implemented\">neįdiegta</string>\r\n    <string name=\"option_background_playback_off\">Išjungta</string>\r\n    <string name=\"option_background_playback_pip\">Paveikslėlis paveikslėlyje</string>\r\n    <string name=\"option_background_playback_only_audio\">Tik garsas</string>\r\n    <string name=\"playback_settings\">Atkūrimo nustatymai</string>\r\n    <string name=\"video_speed\">Vaizdo įrašo greitis</string>\r\n    <string name=\"resolution_switch\">Perjungti skiriamąją gebą</string>\r\n    <string name=\"video_buffer\">Vaizdo įrašo buferis</string>\r\n    <string name=\"video_buffer_size_low\">Mažas</string>\r\n    <string name=\"video_buffer_size_med\">Vidutinis</string>\r\n    <string name=\"video_buffer_size_high\">Didelis</string>\r\n    <string name=\"update_changelog\">Pakeitimai</string>\r\n    <string name=\"install_update\">Įdiegti naujinį</string>\r\n    <string name=\"section_is_empty\">Oi! Čia nieko nėra.</string>\r\n    <string name=\"dialog_add_to_playlist\">Pridėti prie grojaraščio</string>\r\n    <string name=\"msg_signed_users_only\">Tik prisijungusiems naudotojams</string>\r\n    <string name=\"msg_cant_load_content\">Nepavyksta įkelti turinio.\\n1) Patikrinkite įrenginio datą ir laiką.\\n2) Patikrinkite tinklo ryšį.\\n3) Pabandykite prisijungti prie savo paskyros.</string>\r\n    <string name=\"title_video_presets\">Vaizdo įrašų išankstiniai nustatymai</string>\r\n    <string name=\"settings_accounts\">Paskyros</string>\r\n    <string name=\"settings_left_panel\">Redaguoti kategorijas</string>\r\n    <string name=\"settings_themes\">Temos</string>\r\n    <string name=\"settings_other\">Kita</string>\r\n    <string name=\"settings_player\">Vaizdo grotuvas</string>\r\n    <string name=\"settings_language\">Kalba</string>\r\n    <string name=\"settings_linked_devices\">Susieti įrenginiai</string>\r\n    <string name=\"settings_about\">Apie</string>\r\n    <string name=\"dialog_account_list\">Pasirinkite paskyrą</string>\r\n    <string name=\"dialog_account_none\">Nėra</string>\r\n    <string name=\"dialog_remove_account\">Pašalinti paskyrą</string>\r\n    <string name=\"dialog_add_account\">Pridėti paskyrą</string>\r\n    <string name=\"default_lang\">Numatytasis</string>\r\n    <string name=\"dialog_select_language\">Programos kalba</string>\r\n    <string name=\"subtitle_default\">Numatytasis</string>\r\n    <string name=\"subtitle_white_semi_transparent\">Balta pusiau skaidriame fone</string>\r\n    <string name=\"subtitle_style\">Subtitrų stilius</string>\r\n    <string name=\"subtitle_language\">Subtitrų kalba</string>\r\n    <string name=\"action_search\">Pradėti paiešką</string>\r\n    <string name=\"settings_main_ui\">Naudotojo sąsaja</string>\r\n    <string name=\"dialog_main_ui\">Naudotojo sąsaja</string>\r\n    <string name=\"card_animated_previews\">Animuotos peržiūros</string>\r\n    <string name=\"web_site\">Svetainė</string>\r\n    <string name=\"donation\">Aukojimas</string>\r\n    <string name=\"dialog_about\">Apie</string>\r\n    <string name=\"dialog_player_ui\">Vaizdo grotuvas</string>\r\n    <string name=\"player_show_ui_on_pause\">Rodyti naudotojo sąsają pristabdžius</string>\r\n    <string name=\"player_pause_on_ok\">Mygtukas \\„Gerai\\“ sustabdo atkūrimą</string>\r\n    <string name=\"player_ok_button_behavior\">Mygtuko \\„Gerai\\“ veikimas</string>\r\n    <string name=\"player_only_ui\">Tik naudotojo sąsaja</string>\r\n    <string name=\"player_ui_and_pause\">Naudotojo sąsaja ir atkūrimo pristabdymas</string>\r\n    <string name=\"player_only_pause\">Tik atkūrimo pristabdymas</string>\r\n    <string name=\"check_for_updates\">Patikrinti, ar yra naujinių</string>\r\n    <string name=\"update_not_found\">Naudojate naujausią versiją</string>\r\n    <string name=\"update_in_progress\">Palaukite…</string>\r\n    <string name=\"player_ui_hide_behavior\">Automatiškai slėpti naudotojo sąsają</string>\r\n    <string name=\"option_never\">Niekada</string>\r\n    <string name=\"side_panel_sections\">Šoninio meniu skyriai</string>\r\n    <string name=\"boot_to_section\">Pradinis skyrius paleidimo metu</string>\r\n    <string name=\"large_ui\">Didelė naudotojo sąsaja</string>\r\n    <string name=\"video_grid_scale\">Vaizdo tinklelio mastelis</string>\r\n    <string name=\"scale_ui\">Naudotojo sąsajos mastelis</string>\r\n    <string name=\"color_scheme\">Spalvų schema</string>\r\n    <string name=\"color_scheme_default\">Numatytasis</string>\r\n    <string name=\"color_scheme_red_grey\">Raudonai pilka</string>\r\n    <string name=\"color_scheme_red\">Raudona</string>\r\n    <string name=\"color_scheme_dark_grey\">Tamsus</string>\r\n    <string name=\"disable_update_check\">Išjungti automatinį naujinių tikrinimą</string>\r\n    <string name=\"show_again\">Rodyti dar kartą</string>\r\n    <string name=\"check_updates_auto\">Pranešti apie naują versiją</string>\r\n    <string name=\"select_account_on_boot\">Pasirinkti programos paleidimo metu</string>\r\n    <string name=\"player_other\">Įvairūs</string>\r\n    <string name=\"player_full_date\">Tiksli data apraše</string>\r\n    <string name=\"open_channel\">Atidaryti kanalą</string>\r\n    <string name=\"open_playlist\">Atidaryti grojaraštį</string>\r\n    <string name=\"not_interested\">Nedomina</string>\r\n    <string name=\"you_wont_see_this_video\">Vaizdo įrašas pašalintas iš rekomenduojamų</string>\r\n    <string name=\"settings_search\">Paieška</string>\r\n    <string name=\"dialog_search\">Paieška</string>\r\n    <string name=\"instant_voice_search\">Momentinė paieška balsu</string>\r\n    <string name=\"option_background_playback_behind\">Atkurti fone už</string>\r\n    <string name=\"next_video_info_is_not_loaded_yet\">Sekančio vaizdo įrašo informacija dar neįkelta</string>\r\n    <string name=\"card_multiline_title\">Kelių eilučių pavadinimai</string>\r\n    <string name=\"cards_style\">Kortelių stilius</string>\r\n    <string name=\"repeat_mode_all\">Leisti visus vaizdo įrašus vieną po kito</string>\r\n    <string name=\"repeat_mode_one\">Pakartotinai leisti dabartinį vaizdo įrašą</string>\r\n    <string name=\"repeat_mode_pause\">Pristabdyti atkūrimą po kiekvieno vaizdo įrašo</string>\r\n    <string name=\"repeat_mode_pause_alt\">Pristabdyti atkūrimą po kiekvieno į grojaraštį neįtraukto vaizdo įrašo</string>\r\n    <string name=\"repeat_mode_none\">Sustabdyti atkūrimą po vieno vaizdo įrašo</string>\r\n    <string name=\"subscribed_to_channel\">Užsiprenumeravote kanalą</string>\r\n    <string name=\"unsubscribed_from_channel\">Kanalo prenumerata atšaukta</string>\r\n    <string name=\"subtitle_yellow_transparent\">Geltona permatomame fone</string>\r\n    <string name=\"subtitle_white_transparent\">Balta permatomame fone</string>\r\n    <string name=\"subtitle_white_black\">Balta juodame fone</string>\r\n    <string name=\"player_seek_preview\">Peržiūra prasukant vaizdo įrašą</string>\r\n    <string name=\"color_scheme_dark_grey_oled\">Tamsus (OLED)</string>\r\n    <string name=\"channels_section_sorting\">Kanalų skyrių rūšiavimas</string>\r\n    <string name=\"sorting_by_new_content\">Naujas turinys</string>\r\n    <string name=\"sorting_alphabetically\">Abėcėlės tvarka</string>\r\n    <string name=\"sorting_last_viewed\">Paskutinį kartą peržiūrėta</string>\r\n    <string name=\"player_pause_when_seek\">Pristabdyti atkūrimą prasukant vaizdo įrašą</string>\r\n    <string name=\"playlists_style\">Grojaraščių stilius</string>\r\n    <string name=\"playlists_style_grid\">Tinklelis</string>\r\n    <string name=\"playlists_style_rows\">Eilutės</string>\r\n    <string name=\"player_show_clock\">Rodyti laikrodį</string>\r\n    <string name=\"player_show_remaining_time\">Rodyti likusį laiką</string>\r\n    <string name=\"player_show_ending_time\">Rodyti pabaigos laiką</string>\r\n    <string name=\"open_channel_uploads\">Atidaryti kanalo įkėlimus</string>\r\n    <string name=\"app_exit_shortcut\">Programos išjungimas</string>\r\n    <string name=\"app_exit_none\">Neišjungti</string>\r\n    <string name=\"app_double_back_exit\">Paspaudus mygtuką \\„Atgal\\“ du kartus</string>\r\n    <string name=\"app_single_back_exit\">Paspaudus mygtuką \\„Atgal\\“ vieną kartą</string>\r\n    <string name=\"action_video_zoom\">Vaizdo priartinimas</string>\r\n    <string name=\"video_zoom\">Vaizdo priartinimas</string>\r\n    <string name=\"video_zoom_default\">Numatytasis</string>\r\n    <string name=\"video_zoom_fit_width\">Sutalpinti pagal plotį</string>\r\n    <string name=\"video_zoom_fit_height\">Sutalpinti pagal aukštį</string>\r\n    <string name=\"video_zoom_fit_both\">Sutalpinti pagal plotį arba aukštį</string>\r\n    <string name=\"video_zoom_stretch\">Ištempti</string>\r\n    <string name=\"color_scheme_teal\">Žalsvai mėlyna</string>\r\n    <string name=\"color_scheme_teal_oled\">Žalsvai mėlyna (OLED)</string>\r\n    <string name=\"player_seek_preview_none\">Išjungta</string>\r\n    <string name=\"player_seek_preview_single\">Vienas kadras</string>\r\n    <string name=\"player_seek_preview_carousel\">Karuselė</string>\r\n    <string name=\"player_seek_preview_carousel_slow\">Karuselė (lėtai)</string>\r\n    <string name=\"player_seek_preview_carousel_fast\">Karuselė (greitai)</string>\r\n    <string name=\"unsubscribe_from_channel\">Atšaukti kanalo prenumeratą</string>\r\n    <string name=\"card_title_lines_num\">Kortelės pavadinimo eilučių skaičius</string>\r\n    <string name=\"card_auto_scrolled_title\">Automatiškai slinkti nukirptą pavadinimą</string>\r\n    <string name=\"share_link\">Dalintis nuoroda</string>\r\n    <string name=\"subscribe_to_channel\">Prenumeruoti kanalą</string>\r\n    <string name=\"wait_data_loading\">Duomenys įkeliami, palaukite…</string>\r\n    <string name=\"auto_frame_rate_pause\">Automatinis kadrų dažnis (perjungiant pristabdyti)</string>\r\n    <string name=\"auto_frame_rate_applying\">Pritaikomas automatinis kadrų dažnis %sx%s@%s</string>\r\n    <string name=\"audio_shift\">Garso poslinkis</string>\r\n    <string name=\"player_remember_speed\">Įsiminti greitį</string>\r\n    <string name=\"mark_channel_as_watched\">Pažymėti kaip peržiūrėtą</string>\r\n    <string name=\"channel_marked_as_watched\">Kanalas pažymėtas kaip peržiūrėtas</string>\r\n    <string name=\"dialog_add_device\">Pridėti įrenginį</string>\r\n    <string name=\"dialog_remove_all_devices\">Pašalinti visus įrenginius</string>\r\n    <string name=\"device_link_enabled\">Susieta su įrenginiu</string>\r\n    <string name=\"device_connected\">Įrenginys \\„%s\\“ prijungtas</string>\r\n    <string name=\"device_disconnected\">Įrenginys \\„%s\\“ atjungtas</string>\r\n    <string name=\"settings_ui_scale\">Naudotojo sąsajos mastelis</string>\r\n    <string name=\"msg_restart_app\">Šių nustatymų pakeitimui prašome paleisti programą iš naujo</string>\r\n    <string name=\"settings_block\">Turinio blokavimas</string>\r\n    <string name=\"msg_applying\">Pritaikoma %s…</string>\r\n    <string name=\"msg_done\">Atlikta</string>\r\n    <string name=\"settings_general\">Bendra</string>\r\n    <string name=\"settings_video\">Vaizdas</string>\r\n    <string name=\"content_block_confirm_skip\">Patvirtinti praleidžiant</string>\r\n    <string name=\"content_block_categories\">Praleisti segmentus</string>\r\n    <string name=\"content_block_sponsor\">Rėmėjas</string>\r\n    <string name=\"content_block_intro\">Pertrauka/pradžios animacija</string>\r\n    <string name=\"content_block_outro\">Pabaigos kortelės/titrai</string>\r\n    <string name=\"content_block_interaction\">Sąveikos priminimas (\\„prenumeruokite\\“)</string>\r\n    <string name=\"content_block_self_promo\">Savireklama/neapmokama reklama</string>\r\n    <string name=\"content_block_music_off_topic\">Nemuzikinė klipo dalis</string>\r\n    <string name=\"confirm_segment_skip\">Praleisti segmentą \\„%s\\“?</string>\r\n    <string name=\"cancel_segment_skip\">Atšaukti segmento praleidimą</string>\r\n    <string name=\"msg_skipping_segment\">Praleidžiamas segmentas \\„%s\\“…</string>\r\n    <string name=\"content_block_notification_type\">Pranešimo tipas</string>\r\n    <string name=\"content_block_notify_none\">Be pranešimo</string>\r\n    <string name=\"content_block_notify_toast\">Informacinis pranešimas</string>\r\n    <string name=\"content_block_notify_dialog\">Patvirtinimo dialogas</string>\r\n    <string name=\"return_to_launcher\">Grįžti į paleidimo priemonę iš ATV kanalų/paieškos</string>\r\n    <string name=\"intent_force_close\">Priverstinai uždaryti, jei vaizdo įrašas atidarytas iš išorinio šaltinio</string>\r\n    <string name=\"btn_confirm\">Patvirtinti</string>\r\n    <string name=\"player_low_video_quality\">Žema vaizdo kokybė</string>\r\n    <string name=\"remote_session_closed\">Nuotolinė sesija uždaryta</string>\r\n    <string name=\"msg_mode_switch_error\">Nepavyko pakeisti vaizdo režimo į \\„%s\\“\"</string>\r\n    <string name=\"msg_player_error\">Įvyko grotuvo klaida \\„%s\\“. Atkūrimas paleidžiamas iš naujo…</string>\r\n    <string name=\"video_preset_disabled\">Be išankstinio nustatymo</string>\r\n    <string name=\"video_preset_enabled\">Sekančio vaizdo įrašo formatas bus nustatytas pagal išankstinį nustatymą</string>\r\n    <string name=\"player_sleep_timer\">Užmigimo laikmatis</string>\r\n    <string name=\"player_show_quality_info\">Rodyti vaizdo įrašo kokybės informaciją</string>\r\n    <string name=\"player_remember_each_speed\">Įsiminti kiekvieno vaizdo įrašo greitį</string>\r\n    <string name=\"player_remember_speed_none\">Neįsiminti</string>\r\n    <string name=\"player_remember_speed_all\">Pritaikyti visiems vaizdo įrašams</string>\r\n    <string name=\"player_remember_speed_each\">Įsiminti kiekvienam vaizdo įrašui atskirai</string>\r\n    <string name=\"msg_player_error_source\">Nepavyko atsisiųsti vaizdo įrašo</string>\r\n    <string name=\"msg_player_error_renderer\">Pasirinktas vaizdo įrašo formatas nepalaikomas</string>\r\n    <string name=\"msg_player_error_unexpected\">Nežinoma vaizdo įrašų dekoderio klaida</string>\r\n    <string name=\"video_aspect\">Vaizdo kraštinių santykis</string>\r\n    <string name=\"player_tweaks\">Kūrėjo parinktys</string>\r\n    <string name=\"header_uploads\">Įkėlimai</string>\r\n    <string name=\"player_show_global_clock\">Visada rodyti laikrodį</string>\r\n    <string name=\"player_show_global_ending_time\">Visada rodyti pabaigos laiką</string>\r\n    <string name=\"uploads_old_look\">Grįžti prie senesnio įkėlimų skyriaus dizaino</string>\r\n    <string name=\"background_playback_activation\">Foninis atkūrimas (aktyvavimas)</string>\r\n    <string name=\"channels_old_look\">Grįžti prie senesnio kanalų skyriaus dizaino</string>\r\n    <string name=\"channels_auto_load\">Automatiškai įkelti kanalų skyriaus turinį</string>\r\n    <string name=\"return_to_background_video\">Grįžti į fone atkuriamą vaizdo įrašą</string>\r\n    <string name=\"pin_unpin_from_sidebar\">Prisegti/atsegti nuo šoninio meniu</string>\r\n    <string name=\"pin_unpin_playlist\">Prisegti/atsegti šį grojaraštį nuo šoninio meniu</string>\r\n    <string name=\"pin_unpin_channel\">Prisegti/atsegti šį kanalą nuo šoninio meniu</string>\r\n    <string name=\"pinned_to_sidebar\">Prisegta prie šoninio meniu</string>\r\n    <string name=\"unpin_from_sidebar\">Atsegti nuo šoninio meniu</string>\r\n    <string name=\"unpinned_from_sidebar\">Atsegta nuo šoninio meniu</string>\r\n    <string name=\"dialog_select_country\">Šalis</string>\r\n    <string name=\"settings_language_country\">Kalba ir šalis</string>\r\n    <string name=\"share_embed_link\">Dalintis įterpimo nuoroda</string>\r\n    <string name=\"card_text_scroll_factor\">Kortelės teksto slinkimo greitis</string>\r\n    <string name=\"preferred_update_source\">Pasirinkti programos naujinių šaltinį</string>\r\n    <string name=\"hide_shorts\">Paslėpti klipukus prenumeratų skyriuje</string>\r\n    <string name=\"key_remapping\">Mygtukų funkcijų keitimas</string>\r\n    <string name=\"screen_dimming\">Ekrano pritemdymas</string>\r\n    <string name=\"removed_from_playback_queue\">Pašalinta iš grojimo eilės</string>\r\n    <string name=\"added_to_playback_queue\">Pridėta į grojimo eilę</string>\r\n    <string name=\"add_remove_from_playback_queue\">Pridėti/pašalinti iš grojimo eilės</string>\r\n    <string name=\"proxy_enabled\">Tarpinis serveris įjungtas</string>\r\n    <string name=\"proxy_disabled\">Tarpinis serveris išjungtas</string>\r\n    <string name=\"uploads_row_name\">Įkėlimai</string>\r\n    <string name=\"playlists_row_name\">Sukurti grojaraščiai</string>\r\n    <string name=\"popular_uploads_row_name\">Populiarūs įkėlimai</string>\r\n    <string name=\"breaking_news_row_name\">Naujienos</string>\r\n    <string name=\"covid_news_row_name\">COVID-19 naujienos</string>\r\n    <string name=\"enable_voice_search\">Įjungti paiešką balsu</string>\r\n    <string name=\"subscribe_unsubscribe_from_channel\">Prenumeruoti/atšaukti kanalo prenumeratą</string>\r\n    <string name=\"app_backup_restore\">Atsarginės kopijos kūrimas/atstatymas</string>\r\n    <string name=\"app_backup\">Kurti atsarginę duomenų kopiją</string>\r\n    <string name=\"app_restore\">Atkurti duomenis iš atsarginės kopijos</string>\r\n    <string name=\"skip_each_segment_once\">Nepraleisti segmentų pakartotinai</string>\r\n    <string name=\"disable_ok_long_press\">Išjungti mygtuko \\„Gerai\\“ ilgą paspaudimą</string>\r\n    <string name=\"player_time_correction\">Rodyti laiką įvertinant įrašo atkūrimo greitį</string>\r\n    <string name=\"sponsor_color_markers\">Spalvoti žymekliai įrašo atkūrimo eigos juostoje</string>\r\n    <string name=\"refresh_section\">Atnaujinti skyrių</string>\r\n    <string name=\"option_disabled\">Išjungta</string>\r\n    <string name=\"live_now_row_name\">Tiesiogiai</string>\r\n    <string name=\"run_in_background\">Leisti fone</string>\r\n    <string name=\"settings_remote_control\">Nuotolinis valdymas</string>\r\n    <string name=\"background_service_started\">Foninis servisas startavo</string>\r\n    <string name=\"dialog_add_to\">Pridėti prie \\„%s\\“</string>\r\n    <string name=\"dialog_remove_from\">Pašalinti iš \\„%s\\“</string>\r\n    <string name=\"added_to\">Pridėta prie \\„%s\\“</string>\r\n    <string name=\"removed_from\">Pašalinta iš \\„%s\\“</string>\r\n    <string name=\"video_preset_adaptive\">Prisitaikantis</string>\r\n    <string name=\"content_block_preview_recap\">Vaizdo įrašo apžvalga arba santrauka</string>\r\n    <string name=\"content_block_highlight\">Vaizdo įrašo esmė</string>\r\n    <string name=\"removed_from_history\">Pašalinta iš peržiūrų istorijos</string>\r\n    <string name=\"remove_from_history\">Pašalinti iš peržiūrų istorijos</string>\r\n    <string name=\"upload_date\">Įkėlimo data</string>\r\n    <string name=\"upload_date_any\">Bet kada</string>\r\n    <string name=\"upload_date_today\">Šiandien</string>\r\n    <string name=\"upload_date_this_week\">Šią savaitę</string>\r\n    <string name=\"upload_date_this_month\">Šį mėnesį</string>\r\n    <string name=\"upload_date_this_year\">Šiais metais</string>\r\n    <string name=\"double_refresh_rate\">Dvigubas atkūrimo dažnis</string>\r\n    <string name=\"upload_date_last_hour\">Šią valandą</string>\r\n    <string name=\"focus_on_search_results\">Automatiškai pereiti prie paieškos rezultatų</string>\r\n    <string name=\"context_menu\">Kontekstinis meniu</string>\r\n    <string name=\"add_remove_from_recent_playlist\">Įtraukti/pašalinti iš pastarojo grojaraščio</string>\r\n    <string name=\"hide_settings_section\">Paslėpti nustatymų skyrių (pavojinga!)</string>\r\n    <string name=\"volume\">Garsumas %s%%</string>\r\n    <string name=\"auto_frame_rate_sec\">%s sek.</string>\r\n    <string name=\"audio_shift_sec\">%s sek.</string>\r\n    <string name=\"ui_hide_timeout_sec\">%s sek.</string>\r\n    <string name=\"screen_dimming_timeout_min\">%s min.</string>\r\n    <string name=\"mark_all_channels_watched\">Pažymėti visus kanalus kaip peržiūrėtus</string>\r\n    <string name=\"move_section_up\">Kelti skyrių aukštyn</string>\r\n    <string name=\"move_section_down\">Leisti skyrių žemyn</string>\r\n    <string name=\"player_buttons\">Nustatyti grotuvo mygtukus</string>\r\n    <string name=\"action_playlist_add\">Įtraukti į grojaraštį</string>\r\n    <string name=\"action_video_stats\">Vaizdo įrašo statistika</string>\r\n    <string name=\"action_subscribe\">Prenumeruoti</string>\r\n    <string name=\"action_channel\">Eiti į kanalą</string>\r\n    <string name=\"action_pip\">Paveikslėlis paveikslėlyje</string>\r\n    <string name=\"action_video_info\">Vaizdo įrašo aprašymas</string>\r\n    <string name=\"action_screen_off\">Ekrano išjungimas</string>\r\n    <string name=\"action_playback_queue\">Grojimo eilė</string>\r\n    <string name=\"action_video_speed\">Vaizdo įrašo greitis</string>\r\n    <string name=\"action_subtitles\">Subtitrai</string>\r\n    <string name=\"action_like\">Patinka</string>\r\n    <string name=\"action_dislike\">Nepatinka</string>\r\n    <string name=\"action_play_pause\">Leisti/pristabdyti</string>\r\n    <string name=\"action_repeat_mode\">Atkūrimo režimas</string>\r\n    <string name=\"action_next\">Sekantis vaizdo įrašas</string>\r\n    <string name=\"action_previous\">Praeitas vaizdo įrašas</string>\r\n    <string name=\"action_high_quality\">Vaizdo kokybė</string>\r\n    <string name=\"content_block_no_skipping_mode\">Režimas be segmentų praleidimo</string>\r\n    <string name=\"content_block_action_type\">Pasirinkti veiksmą</string>\r\n    <string name=\"content_block_action_none\">Nedaryti nieko</string>\r\n    <string name=\"content_block_action_only_skip\">Tik praleisti segmentą</string>\r\n    <string name=\"content_block_action_toast\">Praleisti segmentą parodant pranešimą</string>\r\n    <string name=\"content_block_action_dialog\">Rodyti patvirtinimo dialogą</string>\r\n    <string name=\"content_block_filler\">Ne į temą (laiko užpildymui)</string>\r\n    <string name=\"keyboard_auto_show\">Automatiškai rodyti klaviatūrą</string>\r\n    <string name=\"cancel_dialog\">Atšaukti</string>\r\n    <string name=\"msg_player_error_source2\">Vaizdo įrašo šaltinis neveikia arba laikas neteisingas</string>\r\n    <string name=\"hide_upcoming\">Slėpti būsimus vaizdo įrašus prenumeratų skyriuje</string>\r\n    <string name=\"search_background_playback\">Pradedant paiešką grotuve, esamą vaizdo įrašą rodyti paveikslėlio paveikslėlyje režimu</string>\r\n    <string name=\"trending_row_name\">Populiaru</string>\r\n    <string name=\"player_seek_type\">Vaizdo prasukimo elgsena</string>\r\n    <string name=\"player_seek_regular\">Įprasta</string>\r\n    <string name=\"player_seek_confirmation_pause\">Su patvirtinimu (pristabdyti prasukant)</string>\r\n    <string name=\"player_seek_confirmation_play\">Su patvirtinimu (tęsti peržiūrą prasukant)</string>\r\n    <string name=\"hide_shorts_from_home\">Paslėpti klipukus pagrindiniame skyriuje</string>\r\n    <string name=\"finish_on_disconnect\">Išjungti programą atjungus telefoną</string>\r\n    <string name=\"update_error\">Klaida vykdant atnaujinimą</string>\r\n    <string name=\"hide_shorts_from_history\">Paslėpti klipukus istorijoje</string>\r\n    <string name=\"disable_screensaver\">Išjungti ekrano užsklandą</string>\r\n    <string name=\"description_not_found\">Aprašymas nerastas</string>\r\n    <string name=\"proxy_port_hint\">Tarpinio serverio prievadas</string>\r\n    <string name=\"proxy_host_hint\">Tarpinio serverio vardas arba IP adresas</string>\r\n    <string name=\"proxy_username_hint\">Tarpinio serverio naudotojo vardas</string>\r\n    <string name=\"proxy_password_hint\">Tarpinio serverio naudotojo slaptažodis</string>\r\n    <string name=\"proxy_type\">Tarpinio serverio tipas</string>\r\n    <string name=\"enable_web_proxy\">Naudoti internetinį tarpinį serverį</string>\r\n    <string name=\"proxy_not_supported\">Naudoti internetinį tarpinį serverį (Android 4.4+)</string>\r\n    <string name=\"proxy_test_btn\">Bandyti</string>\r\n    <string name=\"proxy_settings_title\">Tarpinio serverio nustatymai</string>\r\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\r\n    <string name=\"proxy_test_cancelled\">Bandymas#%d: atšauktas.</string>\r\n    <string name=\"proxy_type_invalid\">Tarpinio serverio tipas nenurodytas.</string>\r\n    <string name=\"proxy_host_invalid\">Tarpinio serverio vardas nenurodytas.</string>\r\n    <string name=\"proxy_port_invalid\">Netinkamas tarpinio serverio prievadas, turi būti &gt; 0.</string>\r\n    <string name=\"proxy_credentials_invalid\">Negaliojantis naudotojo vardas arba slaptažodis.</string>\r\n    <string name=\"proxy_test_aborted\">Bandymas nutrauktas, pirmiau sutvarkykite tarpinio serverio nustatymus.</string>\r\n    <string name=\"proxy_application_aborted\">Pirmiau sutvarkykite tarpinio serverio nustatymus.</string>\r\n    <string name=\"proxy_test_start\">Bandymas#%d: %s …</string>\r\n    <string name=\"proxy_test_error\">Klaida#%d: %s</string>\r\n    <string name=\"proxy_test_status\">Būsena#%d: %s %d %s</string>\r\n    <string name=\"openvpn_config_address_hint\">Konfigūracijos failo adresas (*.ovpn)</string>\r\n    <string name=\"enable_openvpn\">Naudoti OpenVPN</string>\r\n    <string name=\"openvpn_settings_title\">OpenVPN nustatymai</string>\r\n    <string name=\"openvpn_application_aborted\">Pirmiau sutvarkykite OpenVPN nustatymus.</string>\r\n    <string name=\"openvpn_test_aborted\">Bandymas sustabdytas, pirmiau sutvarkykite OpenVPN nustatymus.</string>\r\n    <string name=\"openvpn_address_invalid\">OpenVPN konfigūracijos adresas nenurodytas.</string>\r\n    <string name=\"internet_censorship\">Interneto cenzūra</string>\r\n    <string name=\"rename_section\">Pervardyti skyrių</string>\r\n    <string name=\"simple_edit_value_hint\">Nauja reikšmė</string>\r\n    <string name=\"seek_interval\">Vaizdo prasukimo intervalas</string>\r\n    <string name=\"seek_interval_sec\">%s sek.</string>\r\n    <string name=\"subtitle_system\">Sistemos stilius (Android nustatymai > Pritaikymas neįgaliesiems)</string>\r\n    <string name=\"subtitle_scale\">Subtitrų mastelis</string>\r\n    <string name=\"audio_sync_fix_desc\">Alternatyvus būdas sinchronizuoti garsą su vaizdu. Jis laikytinas nebenaudojamu, bet kai kuriais atvejais gali padėti.</string>\r\n    <string name=\"audio_sync_fix\">Garso sinchronizacijos ištaisymas</string>\r\n    <string name=\"ambilight_ratio_fix_desc\">Ištaiso nesamą foninį apšvietimą. Ištaiso neteisingą kraštinių santykį. Ištaiso tuščias ekrano kopijas. Veikia lėčiau!</string>\r\n    <string name=\"ambilight_ratio_fix\">Ambilight foninio apšvietimo/kraštinių santykio/ekrano kopijų ištaisymas</string>\r\n    <string name=\"force_legacy_codecs_desc\">Ženkliai pagerina veikimą žemesnės klasės įrenginiuose. Didžiausia vaizdo raiška 720p.</string>\r\n    <string name=\"force_legacy_codecs\">Naudoti senus kodekus (720p)</string>\r\n    <string name=\"live_stream_fix_desc\">Ženkliai pagerina tiesioginės transliacijos veikimą žemesnės klasės įrenginiuose. Didžiausia vaizdo raiška 1080p.</string>\r\n    <string name=\"live_stream_fix\">Tiesioginių transliacijų ištaisymas (1080p)</string>\r\n    <string name=\"playback_notifications_fix_desc\">Paslepiami pranešimai apie pradėtus leisti naujus įrašus. Gali būti naudinga, jei naudojama AOSP pagrindu sukurta programinė įranga.</string>\r\n    <string name=\"playback_notifications_fix\">Išjungti vaizdo atkūrimo pranešimus</string>\r\n    <string name=\"amlogic_fix_desc\">Pametamų kadrų ištaisymas Amlogic pagrindu kurtuose įrenginiuose.</string>\r\n    <string name=\"amlogic_fix\">Amlogic 1080p@60fps ištaisymas</string>\r\n    <string name=\"tunneled_video_playback_desc\">Vaizdo įrašų atkūrimas tuneliu leidžia tiksliau sinchronizuoti garsą su vaizdu ir sklandžiau atkurti vaizdo įrašus. Reikalinga Android 5+</string>\r\n    <string name=\"tunneled_video_playback\">Vaizdo įrašų atkūrimas tuneliu (Android 5+)</string>\r\n    <string name=\"master_volume\">Bendras garsumas</string>\r\n    <string name=\"volume_limit\">Garsumo limitas</string>\r\n    <string name=\"play_video\">Leisti</string>\r\n    <string name=\"remember_position_of_short_videos\">Atsiminti trumpų (iki 10 min) vaizdo įrašų atkūrimo poziciją</string>\r\n    <string name=\"player_show_tooltips\">Rodyti mygtukų antraštes</string>\r\n    <string name=\"action_like_unset\">\\„Patinka\\“ paspaudimas atšauktas</string>\r\n    <string name=\"action_dislike_unset\">\\„Nepatinka\\“ paspaudimas atšauktas</string>\r\n    <string name=\"various_buttons\">Įvairūs mygtukai</string>\r\n    <string name=\"not_compatible_with\">Pasirinktas variantas nesuderinamas su</string>\r\n    <string name=\"subtitle_position\">Subtitrų apačios poslinkis</string>\r\n    <string name=\"pressing_home\">spaudžiant mygtuką \\„Namai\\“</string>\r\n    <string name=\"pressing_home_back\">spaudžiant mygtuką \\„Namai\\“ arba \\„Atgal\\“</string>\r\n    <string name=\"player_number_key_seek\">Vaizdą prasukti skaičių klavišais</string>\r\n    <string name=\"save_remove_playlist\">Pridėti/pašalinti grojaraštį iš grojaraščių skyriaus</string>\r\n    <string name=\"remove_playlist\">Pašalinti grojaraštį iš grojaraščių skyriaus</string>\r\n    <string name=\"removed_from_playlists\">Grojaraštis pašalintas iš grojaraščių skyriaus</string>\r\n    <string name=\"saved_to_playlists\">Grojaraštis pridėtas prie grojaraščių skyriaus</string>\r\n    <string name=\"create_playlist\">Sukurti grojaraštį</string>\r\n    <string name=\"add_video_to_new_playlist\">Pridėti šį vaizdo įrašą prie naujo grojaraščio</string>\r\n    <string name=\"cant_delete_empty_playlist\">Tuščio grojarašio pašalinti negalima</string>\r\n    <string name=\"playlist\">Grojaraštis</string>\r\n    <string name=\"rename_playlist\">Pervardyti grojaraštį</string>\r\n    <string name=\"cant_rename_empty_playlist\">Tuščio grojaraščio pervardyti negalima</string>\r\n    <string name=\"cant_rename_foreign_playlist\">Svetimo grojaraščio pervardyti negalima</string>\r\n    <string name=\"cant_save_playlist\">Šio grojaraščio išsaugoti negalima</string>\r\n    <string name=\"enter_value\">Įveskite bet kokią netuščią reikšmę</string>\r\n    <string name=\"dialog_add_remove_from\">Pridėti/pašalinti iš %s</string>\r\n    <string name=\"lb_playback_controls_skip_next\">Leisti sekantį</string>\r\n    <string name=\"lb_playback_controls_skip_previous\">Leisti praeitą/atsukti į pradžią</string>\r\n    <string name=\"alt_presets_behavior\">Alternatyvus veikimas pagal išankstinius nustatymus (pralaidumo ribojimas)</string>\r\n    <string name=\"alt_presets_behavior_desc\">Programa mėgins palaikyti iš anksto nustatytą pralaidumą, o ne adaptuos jį pagal esamą ekrano raišką, kadrų per sek. skaičių ir kodeką.</string>\r\n    <string name=\"sleep_timer\">Įgalinti užmigimo laikmatį (viena valanda)</string>\r\n    <string name=\"sleep_timer_desc\">Pristabdyti vaizdo atkūrimą, jei naudotojas nenaudojo nuotolinio valdymo pultelio vieną valandą</string>\r\n    <string name=\"disable_vsync\">Išjungti vertikalią sinchronizaciją (vsync)</string>\r\n    <string name=\"disable_vsync_desc\">Šiek tiek pagreitina vaizdo įrašų atkūrimą</string>\r\n    <string name=\"skip_codec_profile_check\">Praleisti kodeko profilio lygio patikrinimą</string>\r\n    <string name=\"skip_codec_profile_check_desc\">Pradedant leisti vaizdo įrašą netikrinti, ar kodekas yra palaikomas. Gali padėti, jei programinėje įrangoje yra klaidų.</string>\r\n    <string name=\"force_sw_codec\">Naudoti programinį vaizdo dekoderį</string>\r\n    <string name=\"force_sw_codec_desc\">Gali atkurti beveik bet kurį vaizdo įrašą, bet atkūrimo sparta yra labai maža.</string>\r\n</resources>\r\n"
  },
  {
    "path": "common/src/main/res/values-lv/strings.xml",
    "content": "<resources>\n    <string name=\"header_home\">Sākums</string>\n    <string name=\"title_search\">Meklēt</string>\n    <string name=\"header_subscriptions\">Abonementi</string>\n    <string name=\"header_history\">Vēsture</string>\n    <string name=\"header_music\">Mūzika</string>\n    <string name=\"header_news\">Ziņas</string>\n    <string name=\"header_gaming\">Spēles</string>\n    <string name=\"header_playlists\">Atskaņošanas saraksti</string>\n    <string name=\"header_settings\">Iestatījumi</string>\n    <string name=\"header_channels\">Kanāli</string>\n    <string name=\"subscriptions_signin_title\">Apskati jaunumus no kanāliem kas tev patīk</string>\n    <string name=\"subscriptions_signin_subtitle\">Pierakstieties, lai skatītu savus abonementus</string>\n    <string name=\"subscriptions_signin_button_text\">PIERAKSTIES</string>\n    <string name=\"library_signin_to_show_more\">Skatieties videoklipus, kas jums patika, saglabājat vai abonējat</string>\n    <string name=\"library_signin_subtitle\">Pierakstieties, lai skatītu savu bibliotēku</string>\n    <string name=\"action_signin\">PIERAKSTIES</string>\n    <string name=\"title_video_formats\">Video formāti</string>\n    <string name=\"title_audio_formats\">Audio formāti</string>\n    <string name=\"subtitle_category_title\">Subtitri</string>\n    <string name=\"video_max_quality\">Auto (max kvalitāte)</string>\n    <string name=\"audio_max_quality\">Auto (max kvalitāte)</string>\n    <string name=\"subtitles_disabled\">Subtitri ir atspējoti</string>\n    <string name=\"auto_frame_rate\">Auto frame rate</string>\n    <string name=\"frame_rate_correction\">Fix fps:\\n%s</string>\n    <string name=\"category_background_playback\">Atskaņošana fonā</string>\n    <string name=\"not_implemented\">Nav implementēts</string>\n    <string name=\"option_background_playback_off\">Atspējots</string>\n    <string name=\"option_background_playback_pip\">Attēls attēlā</string>\n    <string name=\"option_background_playback_only_audio\">Tikai audio</string>\n    <string name=\"playback_settings\">Atskaņošanas iestatījumi</string>\n    <string name=\"video_speed\">Video ātrums</string>\n    <string name=\"resolution_switch\">Pārslēgt izšķirtspēju</string>\n    <string name=\"video_buffer\">Video buferis</string>\n    <string name=\"video_buffer_size_low\">Zems</string>\n    <string name=\"video_buffer_size_med\">Vidējs</string>\n    <string name=\"video_buffer_size_high\">Augsts</string>\n    <string name=\"update_changelog\">Izmaiņu žurnāls</string>\n    <string name=\"install_update\">Instalēt atjauninājumu</string>\n    <string name=\"section_is_empty\">Oops! Te nekā nav.</string>\n    <string name=\"dialog_add_to_playlist\">Pievienot atskaņošanas sarakstam</string>\n    <string name=\"msg_signed_users_only\">Tikai parakstījušies lietotāji</string>\n    <string name=\"msg_cant_load_content\">Oops. Nevaru ielādēt šo saturu.</string>\n    <string name=\"title_video_presets\">Video ierobežojumi</string>\n    <string name=\"settings_accounts\">Konti</string>\n    <string name=\"settings_left_panel\">Labot kategorijas</string>\n    <string name=\"settings_themes\">Tēmas</string>\n    <string name=\"settings_other\">Cits</string>\n    <string name=\"settings_player\">Video atskaņotājs</string>\n    <string name=\"settings_language\">Valoda</string>\n    <string name=\"settings_linked_devices\">Saistītās ierīces</string>\n    <string name=\"settings_about\">Par</string>\n    <string name=\"dialog_account_list\">Izvēlies kontu</string>\n    <string name=\"dialog_account_none\">Neviens</string>\n    <string name=\"dialog_remove_account\">Noņemt kontu</string>\n    <string name=\"dialog_add_account\">Pievienot kontu</string>\n    <string name=\"default_lang\">Noklusējums</string>\n    <string name=\"dialog_select_language\">App language</string>\n    <string name=\"subtitle_default\">Noklusējums</string>\n    <string name=\"subtitle_white_semi_transparent\">Puscaurspīdīgs fons</string>\n    <string name=\"subtitle_style\">Subtitru stils</string>\n    <string name=\"subtitle_language\">Subtitru valoda</string>\n    <string name=\"action_search\">Sākt meklēt</string>\n    <string name=\"settings_main_ui\">Lietotāja interfeiss</string>\n    <string name=\"dialog_main_ui\">Lietotāja interfeiss</string>\n    <string name=\"card_animated_previews\">Animēti priekšskatījumi</string>\n    <string name=\"web_site\">Web lapa</string>\n    <string name=\"donation\">Ziedošana (Donation)</string>\n    <string name=\"dialog_about\">Par</string>\n    <string name=\"dialog_player_ui\">Video atskaņotājs</string>\n    <string name=\"player_show_ui_on_pause\">Rādīt UI nopauzējot</string>\n    <string name=\"player_pause_on_ok\">OK poga nopauzē atskaņošanu</string>\n    <string name=\"player_ok_button_behavior\">OK pogas funkcijas</string>\n    <string name=\"player_only_ui\">Tikai UI</string>\n    <string name=\"player_ui_and_pause\">UI un pauze</string>\n    <string name=\"player_only_pause\">Tikai pauze</string>\n    <string name=\"check_for_updates\">Pārbaudīt atjauninājumus</string>\n    <string name=\"update_not_found\">Tev ir jaunākā lietotnes versija</string>\n    <string name=\"update_in_progress\">Gaidi…</string>\n    <string name=\"player_ui_hide_behavior\">Automātiski paslēpt UI</string>\n    <string name=\"option_never\">Nekad</string>\n    <string name=\"side_panel_sections\">Izvēlēties sekcijas</string>\n    <string name=\"boot_to_section\">Atverot lietotni parādīt</string>\n    <string name=\"large_ui\">Liels UI</string>\n    <string name=\"video_grid_scale\">Video režģa skala</string>\n    <string name=\"scale_ui\">UI skala</string>\n    <string name=\"color_scheme\">Krāsu tēma</string>\n    <string name=\"color_scheme_default\">Noklusējums</string>\n    <string name=\"color_scheme_red_grey\">Sarkana-pelēka</string>\n    <string name=\"color_scheme_red\">Sarkana</string>\n    <string name=\"color_scheme_dark_grey\">Tumša</string>\n    <string name=\"disable_update_check\">Atslēgt atjauninājumu pārbaudi</string>\n    <string name=\"show_again\">Parādīt atkal</string>\n    <string name=\"check_updates_auto\">Automātiski atjaunināt</string>\n    <string name=\"select_account_on_boot\">Izvēlēties startējot</string>\n    <string name=\"player_other\">Misc</string>\n    <string name=\"player_full_date\">Precīzs datums aprakstā</string>\n    <string name=\"open_channel\">Atvērt kanālu</string>\n    <string name=\"not_interested\">Neinteresē</string>\n    <string name=\"you_wont_see_this_video\">Video tika noņemts no rekomendācijām</string>\n    <string name=\"settings_search\">Mektēt</string>\n    <string name=\"dialog_search\">Mektēt</string>\n    <string name=\"instant_voice_search\">Tūlītēja meklēšana ar balsi</string>\n    <string name=\"option_background_playback_behind\">Atskaņošana fonā</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">Nākamais video nav vēl ielādējies</string>\n    <string name=\"card_multiline_title\">Daudzrindu nosaukumi</string>\n    <string name=\"cards_style\">Karšu stils</string>\n    <string name=\"repeat_mode_all\">Atskaņot visus video pēc kārtas</string>\n    <string name=\"repeat_mode_one\">Atkārtot pašreizējo videoklipu</string>\n    <string name=\"repeat_mode_pause\">Pārtraukt atskaņošanu pēc katra videoklipa</string>\n    <string name=\"repeat_mode_none\">Pārtraukt atskaņošanu pēc viena videoklipa</string>\n    <string name=\"subscribed_to_channel\">Tu esi pierakstījies</string>\n    <string name=\"unsubscribed_from_channel\">Tu esi atrakstījies</string>\n    <string name=\"subtitle_yellow_transparent\">Dzeltens</string>\n    <string name=\"subtitle_white_black\">Melns fons</string>\n    <string name=\"player_seek_preview\">Priekšskatīt, kamēr meklējat</string>\n    <string name=\"color_scheme_dark_grey_oled\">Tumšs (OLED)</string>\n    <string name=\"channels_section_sorting\">Kanālu sadaļu šķirošana</string>\n    <string name=\"sorting_by_new_content\">Jauns saturs</string>\n    <string name=\"sorting_alphabetically\">Alfabētiski</string>\n    <string name=\"sorting_last_viewed\">Pēdējais skatītais</string>\n</resources>"
  },
  {
    "path": "common/src/main/res/values-mo/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <string name=\"header_home\">Home</string>\r\n    <string name=\"title_search\">Search</string>\r\n    <string name=\"header_subscriptions\">Subscriptions</string>\r\n    <string name=\"header_history\">History</string>\r\n    <string name=\"header_music\">Music</string>\r\n    <string name=\"header_news\">News</string>\r\n    <string name=\"header_gaming\">Gaming</string>\r\n    <string name=\"header_playlists\">Playlists</string>\r\n    <string name=\"header_settings\">Settings</string>\r\n    <string name=\"header_channels\">Channels</string>\r\n    <string name=\"subscriptions_signin_title\">See the latest from channels you love</string>\r\n    <string name=\"subscriptions_signin_subtitle\">Sign in to see your subscriptions</string>\r\n    <string name=\"subscriptions_signin_button_text\">SIGN IN</string>\r\n    <string name=\"library_signin_to_show_more\">Watch videos you liked, saved, or subscribed</string>\r\n    <string name=\"library_signin_subtitle\">Sign in to see your library</string>\r\n    <string name=\"action_signin\">SIGN IN</string>\r\n    <string name=\"title_video_formats\">Video formats</string>\r\n    <string name=\"title_audio_formats\">Audio formats</string>\r\n    <string name=\"subtitle_category_title\">Subtitles</string>\r\n    <string name=\"video_max_quality\">Auto (max quality)</string>\r\n    <string name=\"audio_max_quality\">Auto (max quality)</string>\r\n    <string name=\"subtitles_disabled\">Subtitles disabled</string>\r\n    <string name=\"auto_frame_rate\">Auto frame rate</string>\r\n    <string name=\"frame_rate_correction\">Fix fps:\\n%s</string>\r\n    <string name=\"category_background_playback\">Background playback</string>\r\n    <string name=\"not_implemented\">Not implemented</string>\r\n    <string name=\"option_background_playback_off\">Disabled</string>\r\n    <string name=\"option_background_playback_pip\">Picture in picture</string>\r\n    <string name=\"option_background_playback_only_audio\">Only audio</string>\r\n    <string name=\"playback_settings\">Playback settings</string>\r\n    <string name=\"video_speed\">Video speed</string>\r\n    <string name=\"resolution_switch\">Switch resolution</string>\r\n    <string name=\"video_buffer\">Video buffer</string>\r\n    <string name=\"video_buffer_size_low\">Low</string>\r\n    <string name=\"video_buffer_size_med\">Medium</string>\r\n    <string name=\"video_buffer_size_high\">High</string>\r\n    <string name=\"update_changelog\">Changelog</string>\r\n    <string name=\"install_update\">Install update</string>\r\n    <string name=\"section_is_empty\">Oops! There is nothing in here.</string>\r\n    <string name=\"dialog_add_to_playlist\">Add to playlist</string>\r\n    <string name=\"msg_signed_users_only\">Signed users only</string>\r\n    <string name=\"msg_cant_load_content\">Oops. Can\\'t load content.</string>\r\n    <string name=\"title_video_presets\">Video presets</string>\r\n    <string name=\"settings_accounts\">Accounts</string>\r\n    <string name=\"settings_left_panel\">Edit categories</string>\r\n    <string name=\"settings_themes\">Themes</string>\r\n    <string name=\"settings_other\">Other</string>\r\n    <string name=\"settings_player\">Video player</string>\r\n    <string name=\"settings_language\">Language</string>\r\n    <string name=\"settings_linked_devices\">Linked devices</string>\r\n    <string name=\"settings_about\">About</string>\r\n    <string name=\"dialog_account_list\">Select account</string>\r\n    <string name=\"dialog_account_none\">None</string>\r\n    <string name=\"dialog_remove_account\">Remove account</string>\r\n    <string name=\"dialog_add_account\">Add account</string>\r\n    <string name=\"default_lang\">Default</string>\r\n    <string name=\"dialog_select_language\">App language</string>\r\n    <string name=\"subtitle_default\">Default</string>\r\n    <string name=\"subtitle_white_semi_transparent\">Semitransparent background</string>\r\n    <string name=\"subtitle_style\">Subtitle style</string>\r\n    <string name=\"subtitle_language\">Subtitle language</string>\r\n    <string name=\"action_search\">Start search</string>\r\n    <string name=\"settings_main_ui\">User interface</string>\r\n    <string name=\"dialog_main_ui\">User interface</string>\r\n    <string name=\"card_animated_previews\">Animated previews</string>\r\n    <string name=\"web_site\">Web site</string>\r\n    <string name=\"donation\">Donation</string>\r\n    <string name=\"dialog_about\">About</string>\r\n    <string name=\"dialog_player_ui\">Video player</string>\r\n    <string name=\"player_show_ui_on_pause\">Show UI on pause</string>\r\n    <string name=\"player_pause_on_ok\">OK key pauses playback</string>\r\n    <string name=\"player_ok_button_behavior\">OK button behavior</string>\r\n    <string name=\"player_only_ui\">Only UI</string>\r\n    <string name=\"player_ui_and_pause\">UI and pause</string>\r\n    <string name=\"player_only_pause\">Only pause</string>\r\n    <string name=\"check_for_updates\">Check for updates</string>\r\n    <string name=\"update_not_found\">You\\'re using the latest version</string>\r\n    <string name=\"update_in_progress\">Wait…</string>\r\n    <string name=\"player_ui_hide_behavior\">Auto-hide UI</string>\r\n    <string name=\"option_never\">Never</string>\r\n    <string name=\"side_panel_sections\">Set-up sections</string>\r\n    <string name=\"boot_to_section\">Boot to section</string>\r\n    <string name=\"large_ui\">Large UI</string>\r\n    <string name=\"video_grid_scale\">Video grid scale</string>\r\n    <string name=\"scale_ui\">UI scale</string>\r\n    <string name=\"color_scheme\">Color scheme</string>\r\n    <string name=\"color_scheme_default\">Default</string>\r\n    <string name=\"color_scheme_red_grey\">Red-grey</string>\r\n    <string name=\"color_scheme_red\">Red</string>\r\n    <string name=\"color_scheme_dark_grey\">Dark</string>\r\n    <string name=\"disable_update_check\">Disable update check</string>\r\n    <string name=\"show_again\">Show again</string>\r\n    <string name=\"check_updates_auto\">Auto-update</string>\r\n    <string name=\"select_account_on_boot\">Select on boot</string>\r\n    <string name=\"player_other\">Misc</string>\r\n    <string name=\"player_full_date\">Precise date in description</string>\r\n    <string name=\"open_channel\">Open channel</string>\r\n    <string name=\"not_interested\">Not interested</string>\r\n    <string name=\"you_wont_see_this_video\">Video has been removed from recommended</string>\r\n    <string name=\"settings_search\">Search</string>\r\n    <string name=\"dialog_search\">Search</string>\r\n    <string name=\"instant_voice_search\">Instant voice search</string>\r\n    <string name=\"option_background_playback_behind\">Play behind</string>\r\n    <string name=\"next_video_info_is_not_loaded_yet\">Next video info is not loaded yet</string>\r\n    <string name=\"card_multiline_title\">Multiline titles</string>\r\n    <string name=\"cards_style\">Cards style</string>\r\n    <string name=\"repeat_mode_all\">Play all videos one by one</string>\r\n    <string name=\"repeat_mode_one\">Repeat current video</string>\r\n    <string name=\"repeat_mode_pause\">Pause playback after each video</string>\r\n    <string name=\"repeat_mode_none\">Stop playback after one video</string>\r\n    <string name=\"subscribed_to_channel\">Subscribed to channel</string>\r\n    <string name=\"unsubscribed_from_channel\">Unsubscribed to channel</string>\r\n    <string name=\"subtitle_yellow_transparent\">Yellow</string>\r\n    <string name=\"subtitle_white_black\">Black background</string>\r\n    <string name=\"player_seek_preview\">Preview while seeking</string>\r\n    <string name=\"color_scheme_dark_grey_oled\">Dark (OLED)</string>\r\n    <string name=\"channels_section_sorting\">Channel section sorting</string>\r\n    <string name=\"sorting_by_new_content\">New content</string>\r\n    <string name=\"sorting_alphabetically\">Alphabetically</string>\r\n    <string name=\"sorting_last_viewed\">Last viewed</string>\r\n    <string name=\"player_pause_when_seek\">Pause when seeking</string>\r\n    <string name=\"playlists_style\">Playlists style</string>\r\n    <string name=\"playlists_style_grid\">Grid</string>\r\n    <string name=\"playlists_style_rows\">Rows</string>\r\n    <string name=\"player_show_clock\">Show clock</string>\r\n    <string name=\"player_show_remaining_time\">Show remaining time</string>\r\n    <string name=\"open_channel_uploads\">Open channel videos</string>\r\n</resources>"
  },
  {
    "path": "common/src/main/res/values-nb/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"header_home\">Startside</string>\n    <string name=\"title_search\">Søk</string>\n    <string name=\"header_subscriptions\">Abonnementer</string>\n    <string name=\"header_history\">Historikk</string>\n    <string name=\"header_music\">Musikk</string>\n    <string name=\"header_news\">Nyheter</string>\n    <string name=\"header_gaming\">Spill</string>\n    <string name=\"header_playlists\">Spilleliste</string>\n    <string name=\"header_settings\">Innstillinger</string>\n    <string name=\"header_channels\">Kanaler</string>\n    <string name=\"subscriptions_signin_title\">Se det siste fra kanaler du liker</string>\n    <string name=\"subscriptions_signin_subtitle\">Logg på for å se dine abonnementer</string>\n    <string name=\"subscriptions_signin_button_text\">LOGG PÅ</string>\n    <string name=\"library_signin_to_show_more\">Se videoer du likte, lagret eller abonnerte på</string>\n    <string name=\"library_signin_subtitle\">Logg på for å se biblioteket ditt</string>\n    <string name=\"action_signin\">LOGG PÅ</string>\n    <string name=\"title_video_formats\">Video formater</string>\n    <string name=\"title_audio_formats\">Audio formater</string>\n    <string name=\"subtitle_category_title\">Undertekster</string>\n    <string name=\"video_max_quality\">Auto (høyeste kvalitet)</string>\n    <string name=\"audio_max_quality\">Auto (høyeste kvalitet)</string>\n    <string name=\"subtitles_disabled\">Undertekster deaktivert</string>\n    <string name=\"auto_frame_rate\">Automatisk bildefrekvens</string>\n    <string name=\"frame_rate_correction\">Fiks fps:\\n%s</string>\n    <string name=\"category_background_playback\">Bakgrunnsavspilling</string>\n    <string name=\"not_implemented\">Ikke implementert</string>\n    <string name=\"option_background_playback_off\">Deaktivert</string>\n    <string name=\"option_background_playback_only_audio\">Bare lyd</string>\n    <string name=\"playback_settings\">Avspillingsinnstillinger</string>\n    <string name=\"video_speed\">Videohastighet</string>\n    <string name=\"resolution_switch\">Bytt oppløsning</string>\n    <string name=\"video_buffer\">Videobuffer</string>\n    <string name=\"video_buffer_size_low\">Lite</string>\n    <string name=\"video_buffer_size_med\">Medium</string>\n    <string name=\"video_buffer_size_high\">Stort</string>\n    <string name=\"update_changelog\">Endringslogg</string>\n    <string name=\"install_update\">Installer oppdatering</string>\n    <string name=\"section_is_empty\">Beklager! Det er ingenting her inne.</string>\n    <string name=\"dialog_add_to_playlist\">Legg til i spilleliste</string>\n    <string name=\"msg_signed_users_only\">Bare for innloggede brukere</string>\n    <string name=\"msg_cant_load_content\">Beklager. Kan ikke laste innholdet.</string>\n    <string name=\"title_video_presets\">Forhåndsinnstillinger for video</string>\n    <string name=\"settings_accounts\">Kontoer</string>\n    <string name=\"settings_left_panel\">Endre katogorier</string>\n    <string name=\"settings_themes\">Temaer</string>\n    <string name=\"settings_other\">Andre</string>\n    <string name=\"settings_player\">Video avspiller</string>\n    <string name=\"settings_language\">Språk</string>\n    <string name=\"settings_linked_devices\">Tilkoblede enheter</string>\n    <string name=\"settings_about\">Om</string>\n    <string name=\"dialog_account_list\">Velg konto</string>\n    <string name=\"dialog_account_none\">Ingen</string>\n    <string name=\"dialog_remove_account\">Fjern konto</string>\n    <string name=\"dialog_add_account\">Legg til konto</string>\n    <string name=\"default_lang\">Standard</string>\n    <string name=\"dialog_select_language\">Språk</string>\n    <string name=\"subtitle_default\">Standard</string>\n    <string name=\"subtitle_white_semi_transparent\">Semitransparent bakgrunn</string>\n    <string name=\"subtitle_style\">Undertekststil</string>\n    <string name=\"subtitle_language\">Undertekst språk</string>\n    <string name=\"action_search\">Start søk</string>\n    <string name=\"settings_main_ui\">Brukergrensesnitt</string>\n    <string name=\"dialog_main_ui\">Brukergrensesnitt</string>\n    <string name=\"card_animated_previews\">Animerte forhåndsvisninger</string>\n    <string name=\"web_site\">Nettsted</string>\n    <string name=\"donation\">Donasjon</string>\n    <string name=\"dialog_about\">Om</string>\n    <string name=\"dialog_player_ui\">Video avspiller</string>\n    <string name=\"player_show_ui_on_pause\">Vis brukergrensesnittet ved pause</string>\n    <string name=\"player_pause_on_ok\">OK-tasten stopper avspilling midlertidig</string>\n    <string name=\"player_ok_button_behavior\">OK-tastens oppførsel</string>\n    <string name=\"player_only_ui\">Bare brukergrensesnitt</string>\n    <string name=\"player_ui_and_pause\">Brukergrensesnitt og pause</string>\n    <string name=\"player_only_pause\">Bare pause</string>\n    <string name=\"check_for_updates\">Se etter oppdateringer</string>\n    <string name=\"update_not_found\">Du bruker den nyeste versjonen</string>\n    <string name=\"update_in_progress\">Vent…</string>\n    <string name=\"player_ui_hide_behavior\">Skjul brukergrensesnittet automatisk</string>\n    <string name=\"option_never\">Aldri</string>\n    <string name=\"side_panel_sections\">Sidepanel seksjoner</string>\n    <string name=\"boot_to_section\">Oppstarts seksjon</string>\n    <string name=\"large_ui\">Stort brukergrensesnitt</string>\n    <string name=\"video_grid_scale\">Video rutenett skalering</string>\n    <string name=\"scale_ui\">Brukergrensesnitt skalering</string>\n    <string name=\"color_scheme\">Fargevalg</string>\n    <string name=\"color_scheme_default\">Standard</string>\n    <string name=\"color_scheme_red_grey\">Rød-grå</string>\n    <string name=\"color_scheme_red\">Rød</string>\n    <string name=\"color_scheme_dark_grey\">Mørk</string>\n    <string name=\"disable_update_check\">Deaktiver oppdaterings sjekk</string>\n    <string name=\"show_again\">Vis igjen</string>\n    <string name=\"check_updates_auto\">Automatisk oppdatering</string>\n    <string name=\"select_account_on_boot\">Velg ved oppstart</string>\n    <string name=\"player_other\">Diverse</string>\n    <string name=\"player_full_date\">Nøyaktig dato i beskrivelse</string>\n    <string name=\"open_channel\">Åpne kanal</string>\n    <string name=\"not_interested\">Ikke interessert</string>\n    <string name=\"you_wont_see_this_video\">Videoen er fjernet fra anbefalt</string>\n    <string name=\"settings_search\">Søk</string>\n    <string name=\"dialog_search\">Søk</string>\n    <string name=\"instant_voice_search\">Talesøk først</string>\n    <string name=\"option_background_playback_behind\">Spill Video i bakgrunnen</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">Neste videoinformasjon er ikke lastet inn ennå</string>\n    <string name=\"card_multiline_title\">Flerlinje titler</string>\n    <string name=\"cards_style\">Rutenett stil</string>\n    <string name=\"repeat_mode_all\">Spill av alle videoene en etter en</string>\n    <string name=\"repeat_mode_one\">Gjenta gjeldende video</string>\n    <string name=\"repeat_mode_pause\">Sett avspilling på pause etter hver video</string>\n    <string name=\"repeat_mode_none\">Stopp avspilling etter en video</string>\n    <string name=\"subscribed_to_channel\">Abonnerer på kanalen</string>\n    <string name=\"unsubscribed_from_channel\">Avslutter abonnering på kanalen</string>\n    <string name=\"subtitle_yellow_transparent\">Gul</string>\n    <string name=\"subtitle_white_black\">Svart bakgrunn</string>\n    <string name=\"player_seek_preview\">Forhåndsvisning mens du søker</string>\n    <string name=\"color_scheme_dark_grey_oled\">Mørk (OLED)</string>\n    <string name=\"channels_section_sorting\">Kanalseksjonssortering</string>\n    <string name=\"sorting_by_new_content\">Nytt innhold</string>\n    <string name=\"sorting_alphabetically\">Alfabetisk</string>\n    <string name=\"sorting_last_viewed\">Sist sett</string>\n    <string name=\"settings_general\">Generelt</string>\n    <string name=\"context_menu\">Kontektsmeny</string>\n    <string name=\"various_buttons\">Knapper på toppen av hovedvinduet</string>\n    <string name=\"app_exit_shortcut\">Avslutte appen</string>\n    <string name=\"screen_dimming\">Skjermdimming</string>\n    <string name=\"time_format\">Tidsformat</string>\n    <string name=\"key_remapping\">Remapping av knapper</string>\n    <string name=\"app_backup_restore\">Sikkerhetskopiering</string>\n    <string name=\"internet_censorship\">Internettsensur</string>\n    <string name=\"app_exit_none\">Ikke avslutt</string>\n    <string name=\"app_double_back_exit\">Dobbelt tilbakeklikk</string>\n    <string name=\"app_single_back_exit\">Enkelt tilbakeklikk</string>\n    <string name=\"time_format_24\">24-timers</string>\n    <string name=\"time_format_12\">12-timers (AM/PM)</string>\n    <string name=\"enable_web_proxy\">Bruk proxy</string>\n    <string name=\"app_backup\">Ta sikkerhetskopi</string>\n    <string name=\"app_restore\">Gjenopprett sikkerhetskopi</string>\n    <string name=\"playlist_order\">Sorter spilleliste</string>\n    <string name=\"add_remove_from_playback_queue\">Legg til/Fjern fra spille-kø</string>\n    <string name=\"action_playback_queue\">Spille-kø</string>\n    <string name=\"unset_stream_reminder\">Fjern stream-påminnelse</string>\n    <string name=\"set_stream_reminder\">Sett stream-påminnelse</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">Abonner / Avslutt abonnement</string>\n    <string name=\"save_remove_playlist\">Legg til/Fjern spilleliste fra spilleliste-seksjonen</string>\n    <string name=\"create_playlist\">Opprett spilleliste</string>\n    <string name=\"add_video_to_new_playlist\">Legg til ny spilleliste</string>\n    <string name=\"add_remove_from_recent_playlist\">Legg til/Fjern fra nylig brukt spilleliste</string>\n    <string name=\"play_video\">Spill video</string>\n    <string name=\"remove_from_history\">Fjern fra spille-logg</string>\n    <string name=\"pin_unpin_from_sidebar\">Legg til/Fjern fra sidepanel</string>\n    <string name=\"share_link\">Del lenke</string>\n    <string name=\"share_embed_link\">Del innbygginslenke</string>\n    <string name=\"move_section_up\">Flytt seksjon opp</string>\n    <string name=\"move_section_down\">Flytt seksjon ned</string>\n    <string name=\"rename_section\">Gi nytt navn til seksjon</string>\n    <string name=\"action_video_info\">Videobeskrivelse</string>\n    <string name=\"playback_queue_category_title\">Avspillingskø</string>\n    <string name=\"not_supported_by_device\">Enheten støtter ikke denne funksjonen</string>\n    <string name=\"option_background_playback_pip\">Bilde-i-bilde</string>\n    <string name=\"video_buffer_size_none\">Ingen</string>\n    <string name=\"video_buffer_size_lowest\">Lavest</string>\n    <string name=\"video_buffer_size_highest\">Høyest</string>\n    <string name=\"original_lang\">Original</string>\n    <string name=\"open_playlist\">Åpne spilleliste</string>\n    <string name=\"not_recommend_channel\">Ikke anbefal kanal</string>\n    <string name=\"you_wont_see_this_channel\">Du vil ikke se denne kanalen i anbefalinger</string>\n    <string name=\"repeat_mode_pause_alt\">Spill kun av videoer i spillelisten kontinuerlig</string>\n    <string name=\"subtitle_white_transparent\">Hvit på gjennomsiktig bakgrunn</string>\n    <string name=\"player_pause_when_seek\">Pause ved spoling</string>\n    <string name=\"playlists_style\">Stil for spillelister</string>\n    <string name=\"playlists_style_grid\">Rutenett</string>\n    <string name=\"playlists_style_rows\">Rader</string>\n    <string name=\"player_show_clock\">Vis klokke i kontrollinjen</string>\n    <string name=\"player_show_remaining_time\">Vis gjenværende tid i kontrollinjen</string>\n    <string name=\"player_show_ending_time\">Vis sluttid i kontrollinjen</string>\n    <string name=\"open_channel_uploads\">Åpne kanalopplastinger</string>\n    <string name=\"player_exit_shortcut\">Gå ut av avspilleren</string>\n    <string name=\"search_exit_shortcut\">Gå ut av søket</string>\n    <string name=\"action_video_zoom\">Videozoom</string>\n    <string name=\"video_zoom\">Videozoom</string>\n    <string name=\"video_zoom_default\">Standard</string>\n    <string name=\"video_zoom_fit_width\">Tilpass bredde</string>\n    <string name=\"video_zoom_fit_height\">Tilpass høyde</string>\n    <string name=\"video_zoom_fit_both\">Tilpass enten bredde eller høyde</string>\n    <string name=\"video_zoom_stretch\">Strekk</string>\n    <string name=\"color_scheme_teal\">Blågrønn</string>\n    <string name=\"color_scheme_teal_oled\">Blågrønn (OLED)</string>\n    <string name=\"player_seek_preview_none\">Deaktivert</string>\n    <string name=\"player_seek_preview_single\">Enkeltbilde</string>\n    <string name=\"player_seek_preview_carousel\">Karusell</string>\n    <string name=\"player_seek_preview_carousel_slow\">Karusell (sakte, etter nøkkelbilder)</string>\n    <string name=\"player_seek_preview_carousel_fast\">Karusell (rask, upresis forhåndsvisning)</string>\n    <string name=\"unsubscribe_from_channel\">Avslutt abonnement på kanalen</string>\n    <string name=\"card_title_lines_num\">Antall tittellinjer på kort</string>\n    <string name=\"card_auto_scrolled_title\">Rull beskåret tittel automatisk</string>\n    <string name=\"share_qr_link\">Del lenke (QR-kode)</string>\n    <string name=\"subscribe_to_channel\">Abonner på kanalen</string>\n    <string name=\"wait_data_loading\">Vennligst vent mens data lastes inn …</string>\n    <string name=\"auto_frame_rate_pause\">Automatisk bildefrekvens (pause ved bytte)</string>\n    <string name=\"auto_frame_rate_applying\">Bruker automatisk bildefrekvens %sx%s\\@%s</string>\n    <string name=\"audio_shift\">Lydforskyvning</string>\n    <string name=\"player_remember_speed\">Husk hastighet</string>\n    <string name=\"mark_channel_as_watched\">Merk som sett</string>\n    <string name=\"channel_marked_as_watched\">Kanalen er merket som sett</string>\n    <string name=\"dialog_add_device\">Legg til enhet</string>\n    <string name=\"dialog_remove_all_devices\">Fjern alle enheter</string>\n    <string name=\"device_link_enabled\">Kobling aktivert</string>\n    <string name=\"device_connected\">Enheten «%s» er koblet til</string>\n    <string name=\"device_disconnected\">Enheten «%s» er koblet fra</string>\n    <string name=\"settings_ui_scale\">Skalering av brukergrensesnitt</string>\n    <string name=\"msg_restart_app\">Start appen på nytt for å ta i bruk disse innstillingene</string>\n    <string name=\"settings_block\">Innholdsblokkering</string>\n    <string name=\"msg_applying\">Bruker %s …</string>\n    <string name=\"msg_done\">Ferdig</string>\n    <string name=\"settings_video\">Video</string>\n    <string name=\"content_block_confirm_skip\">Bekreft ved hopping</string>\n    <string name=\"content_block_categories\">Hopp over segmenter</string>\n    <string name=\"content_block_sponsor\">Sponsor</string>\n    <string name=\"content_block_intro\">Pause/intro-animasjon</string>\n    <string name=\"content_block_outro\">Sluttkort/rulletekst</string>\n    <string name=\"content_block_interaction\">Interaksjonspåminnelse (abonner)</string>\n    <string name=\"content_block_self_promo\">Ubetalt/egenpromotering</string>\n    <string name=\"content_block_music_off_topic\">Ikke-musikkdel av klippet</string>\n    <string name=\"confirm_segment_skip\">Hoppe over segmentet «%s»?</string>\n    <string name=\"cancel_segment_skip\">Avbryt hopping over segment</string>\n    <string name=\"msg_skipping_segment\">Hopper over segmentet «%s» …</string>\n    <string name=\"content_block_notification_type\">Varslingstype</string>\n    <string name=\"content_block_notify_none\">Uten varsling</string>\n    <string name=\"content_block_notify_toast\">Toast-varsling</string>\n    <string name=\"content_block_notify_dialog\">Bekreftelsesdialog</string>\n    <string name=\"return_to_launcher\">Gå tilbake til startprogrammet fra ATV-kanaler/søk</string>\n    <string name=\"intent_force_close\">Tving avslutning hvis videoen ble åpnet fra en ekstern kilde</string>\n    <string name=\"btn_confirm\">Bekreft</string>\n    <string name=\"player_low_video_quality\">Lav videokvalitet</string>\n    <string name=\"remote_session_closed\">Fjernøkten er avsluttet</string>\n    <string name=\"msg_mode_switch_error\">Kan ikke bytte visningsmodus til «%s»</string>\n    <string name=\"msg_player_error\">Avspillerfeil %s. Starter avspilling på nytt …</string>\n    <string name=\"video_preset_disabled\">Uten forhåndsinnstilling</string>\n    <string name=\"video_preset_enabled\">Formatet på neste video vil bli satt i henhold til forhåndsinnstillingen</string>\n    <string name=\"player_sleep_timer\">Innsovningstimer</string>\n    <string name=\"player_show_quality_info\">Vis kvalitetsinfo i kontrollinjen</string>\n    <string name=\"player_remember_each_speed\">Husk hver videos hastighet</string>\n    <string name=\"player_remember_speed_none\">Ingen</string>\n    <string name=\"player_remember_speed_all\">Samme på alle videoer</string>\n    <string name=\"player_remember_speed_each\">For hver video</string>\n    <string name=\"msg_player_error_source\">Kan ikke laste ned videoen</string>\n    <string name=\"msg_player_error_renderer\">Valgt format støttes ikke.\\nPrøv å velge et annet.\\nHvis dette ikke hjelper, prøv å starte enheten på nytt.</string>\n    <string name=\"msg_player_error_video_renderer\">Valgt VIDEO-format støttes ikke.\\nPrøv å velge et annet ved å trykke på HQ-knappen i avspilleren.\\nHvis dette ikke hjelper, prøv å starte enheten på nytt.</string>\n    <string name=\"msg_player_error_audio_renderer\">Valgt LYD-format støttes ikke.\\nPrøv å velge et annet ved å trykke på HQ-knappen i avspilleren.\\nHvis dette ikke hjelper, prøv å starte enheten på nytt.</string>\n    <string name=\"msg_player_error_subtitle_renderer\">Valgt UNDERTEKST-format støttes ikke.\\nPrøv å velge et annet ved å holde inne CC-knappen i avspilleren.\\nHvis dette ikke hjelper, prøv å starte enheten på nytt.</string>\n    <string name=\"msg_player_error_unexpected\">Ukjent videodekoderfeil</string>\n    <string name=\"video_aspect\">Sideforhold</string>\n    <string name=\"player_tweaks\">Utvikleralternativer</string>\n    <string name=\"header_uploads\">Opplastinger</string>\n    <string name=\"player_show_global_clock\">Vis klokke alltid på skjermen</string>\n    <string name=\"player_show_global_ending_time\">Vis sluttid alltid på skjermen</string>\n    <string name=\"app_corner_clock\">Hjem: klokke i øvre høyre hjørne</string>\n    <string name=\"player_corner_clock\">Avspiller: klokke i øvre høyre hjørne</string>\n    <string name=\"player_corner_ending_time\">Avspiller: sluttid i øvre høyre hjørne</string>\n    <string name=\"uploads_old_look\">Tilbakestill gammelt utseende for Opplastinger</string>\n    <string name=\"background_playback_activation\">Bakgrunnsavspilling (aktivering)</string>\n    <string name=\"channels_old_look\">Gammelt utseende for Kanaler</string>\n    <string name=\"channels_auto_load\">Last inn innhold i Kanaler-seksjonen automatisk</string>\n    <string name=\"return_to_background_video\">Gå tilbake til video som kjører i bakgrunnen</string>\n    <string name=\"pin_unpin_playlist\">Legg til/fjern spilleliste fra sidefeltet</string>\n    <string name=\"pin_unpin_channel\">Legg til/fjern kanal fra sidefeltet</string>\n    <string name=\"pin_playlist\">Legg til spilleliste i sidefeltet</string>\n    <string name=\"pin_channel\">Legg til kanal i sidefeltet</string>\n    <string name=\"pinned_to_sidebar\">Lagt til i sidefeltet</string>\n    <string name=\"unpin_from_sidebar\">Fjern fra sidefeltet</string>\n    <string name=\"unpin_group_from_sidebar\">Fjern abonnementsgruppen</string>\n    <string name=\"unpinned_from_sidebar\">Fjernet fra sidefeltet</string>\n    <string name=\"dialog_select_country\">Land</string>\n    <string name=\"settings_language_country\">Språk/Land</string>\n    <string name=\"card_text_scroll_factor\">Rullehastighet for korttekst</string>\n    <string name=\"preferred_update_source\">Velg oppdateringskilde</string>\n    <string name=\"hide_shorts\">Skjul Shorts fra Abonnementer</string>\n    <string name=\"hide_shorts_channel\">Skjul Shorts fra en kanal</string>\n    <string name=\"removed_from_playback_queue\">Fjernet fra avspillingskø</string>\n    <string name=\"added_to_playback_queue\">Lagt til i avspillingskø</string>\n    <string name=\"add_to_playback_queue\">Legg til i avspillingskø</string>\n    <string name=\"remove_from_playback_queue\">Fjern fra avspillingskø</string>\n    <string name=\"proxy_enabled\">Proxy aktivert</string>\n    <string name=\"proxy_disabled\">Proxy deaktivert</string>\n    <string name=\"uploads_row_name\">Opplastinger</string>\n    <string name=\"playlists_row_name\">Opprettede spillelister</string>\n    <string name=\"popular_uploads_row_name\">Populære opplastinger</string>\n    <string name=\"news_row_name\">Nyheter</string>\n    <string name=\"breaking_news_row_name\">Siste nytt</string>\n    <string name=\"covid_news_row_name\">COVID-19-nyheter</string>\n    <string name=\"enable_voice_search\">Aktiver globalt søk (krever fastvarestøtte)</string>\n    <string name=\"skip_each_segment_once\">Ikke hopp over segmenter igjen</string>\n    <string name=\"disable_ok_long_press\">Deaktiver langt trykk på OK-knappen</string>\n    <string name=\"disable_ok_long_press_desc\">Beregnet for kontrollere med feil der OK-knappen ikke fungerer som den skal</string>\n    <string name=\"player_time_correction\">Vis tiden med hastighetskorreksjon</string>\n    <string name=\"sponsor_color_markers\">Fargemarkører på fremdriftslinjen</string>\n    <string name=\"refresh_section\">Oppdater seksjon</string>\n    <string name=\"option_disabled\">Deaktivert</string>\n    <string name=\"live_now_row_name\">Direkte nå</string>\n    <string name=\"run_in_background\">Bakgrunnsavspilling</string>\n    <string name=\"settings_remote_control\">Fjernkontroll</string>\n    <string name=\"background_service_started\">Bakgrunnstjeneste startet</string>\n    <string name=\"dialog_add_to\">Legg til i %s</string>\n    <string name=\"dialog_remove_from\">Fjern fra %s</string>\n    <string name=\"added_to\">Lagt til i %s</string>\n    <string name=\"removed_from\">Fjernet fra %s</string>\n    <string name=\"video_preset_adaptive\">Adaptiv</string>\n    <string name=\"content_block_preview_recap\">Forhåndsvisning eller sammendrag av videoen</string>\n    <string name=\"content_block_highlight\">Interessant punkt (bokmerke)</string>\n    <string name=\"removed_from_history\">Videoen er fjernet fra historikken</string>\n    <string name=\"upload_date\">Opplastingsdato</string>\n    <string name=\"upload_date_any\">Alltid</string>\n    <string name=\"upload_date_today\">I dag</string>\n    <string name=\"upload_date_this_week\">Denne uken</string>\n    <string name=\"upload_date_this_month\">Denne måneden</string>\n    <string name=\"upload_date_this_year\">Dette året</string>\n    <string name=\"double_refresh_rate\">Dobbel oppdateringsfrekvens</string>\n    <string name=\"upload_date_last_hour\">Siste time</string>\n    <string name=\"focus_on_search_results\">Autofokus på søkeresultater</string>\n    <string name=\"context_menu_sorting\">Sortering i kontekstmeny</string>\n    <string name=\"hide_settings_section\">Skjul Innstillinger-seksjonen (farlig!)</string>\n    <string name=\"volume\">Volum %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s sek</string>\n    <string name=\"audio_shift_sec\">%s sek</string>\n    <string name=\"ui_hide_timeout_sec\">%s sek</string>\n    <string name=\"screen_dimming_timeout_min\">%s min</string>\n    <string name=\"mark_all_channels_watched\">Merk alle kanaler som sett</string>\n    <string name=\"player_buttons\">Konfigurer avspillerknapper</string>\n    <string name=\"action_playlist_add\">Legg til i spilleliste</string>\n    <string name=\"action_video_stats\">Videostatistikk</string>\n    <string name=\"action_subscribe\">Abonner</string>\n    <string name=\"action_channel\">Åpne kanal</string>\n    <string name=\"action_pip\">PIP</string>\n    <string name=\"action_screen_off\">Skjerm av</string>\n    <string name=\"action_sound_off\">Lyd av</string>\n    <string name=\"action_video_speed\">Videohastighet</string>\n    <string name=\"action_subtitles\">Undertekster</string>\n    <string name=\"action_like\">Liker</string>\n    <string name=\"action_dislike\">Liker ikke</string>\n    <string name=\"action_play_pause\">Spill av/Pause</string>\n    <string name=\"action_repeat_mode\">Avspillingsmodus</string>\n    <string name=\"action_next\">Neste video</string>\n    <string name=\"action_previous\">Forrige video</string>\n    <string name=\"action_high_quality\">Videokvalitet</string>\n    <string name=\"content_block_no_skipping_mode\">Ingen hopping-modus</string>\n    <string name=\"content_block_action_type\">Velg handling</string>\n    <string name=\"content_block_action_none\">Ikke gjør noe</string>\n    <string name=\"content_block_action_only_skip\">Hopp kun over</string>\n    <string name=\"content_block_action_toast\">Hopp over med varsling</string>\n    <string name=\"content_block_action_dialog\">Vis bekreftelsesdialog</string>\n    <string name=\"content_block_filler\">Utenfor tema (fyll)</string>\n    <string name=\"keyboard_auto_show\">Vis tastatur automatisk</string>\n    <string name=\"cancel_dialog\">Avbryt</string>\n    <string name=\"msg_player_error_source2\">URL-en fungerer ikke eller enhetstiden er feil.\\nVanligvis er det en feil i appen.</string>\n    <string name=\"msg_player_error_video_source\">VIDEO-URL-en fungerer ikke eller enhetstiden er feil.\\nVanligvis er det en feil i appen.</string>\n    <string name=\"msg_player_error_audio_source\">LYD-URL-en fungerer ikke eller enhetstiden er feil.\\nVanligvis er det en feil i appen.</string>\n    <string name=\"msg_player_error_subtitle_source\">UNDERTEKST-URL-en fungerer ikke eller enhetstiden er feil.\\nVanligvis er det en feil i appen.</string>\n    <string name=\"hide_upcoming\">Skjul kommende fra Abonnementer</string>\n    <string name=\"hide_upcoming_channel\">Skjul kommende fra Kanal</string>\n    <string name=\"hide_upcoming_home\">Skjul kommende fra Hjem</string>\n    <string name=\"search_background_playback\">Bakgrunnsavspilling mens du søker/blar i en kanal</string>\n    <string name=\"trending_row_name\">Populært</string>\n    <string name=\"player_seek_type\">Spolingsatferd</string>\n    <string name=\"player_seek_regular\">Vanlig</string>\n    <string name=\"player_seek_confirmation_pause\">Med bekreftelse (pause under spoling)</string>\n    <string name=\"player_seek_confirmation_play\">Med bekreftelse (spill av under spoling)</string>\n    <string name=\"hide_shorts_from_home\">Skjul Shorts fra Hjem</string>\n    <string name=\"finish_on_disconnect\">Lukk appen etter å ha koblet fra telefonen/nettbrettet</string>\n    <string name=\"update_error\">Oppdateringsfeil</string>\n    <string name=\"hide_shorts_from_search\">Skjul Shorts fra søkeresultatet</string>\n    <string name=\"hide_shorts_from_history\">Skjul Shorts fra Historikk</string>\n    <string name=\"hide_shorts_from_trending\">Skjul Shorts fra Populært</string>\n    <string name=\"disable_screensaver\">Deaktiver skjermsparer</string>\n    <string name=\"description_not_found\">Beskrivelse ikke funnet</string>\n    <string name=\"proxy_port_hint\">Proxy-port</string>\n    <string name=\"proxy_host_hint\">Proxy-vertsnavn eller IP</string>\n    <string name=\"proxy_username_hint\">Proxy-brukernavn</string>\n    <string name=\"proxy_password_hint\">Proxy-passord</string>\n    <string name=\"proxy_type\">Proxy-type</string>\n    <string name=\"proxy_not_supported\">Bruk Web Proxy (krever Android 4.4+)</string>\n    <string name=\"proxy_test_btn\">Test</string>\n    <string name=\"proxy_settings_title\">Innstillinger for proxy-server</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">Test #%d: avbrutt.</string>\n    <string name=\"proxy_type_invalid\">Proxy-type ikke angitt.</string>\n    <string name=\"proxy_host_invalid\">Proxy-vert ikke angitt.</string>\n    <string name=\"proxy_port_invalid\">Ugyldig proxy-port, må være > 0.</string>\n    <string name=\"proxy_credentials_invalid\">Ugyldig brukernavn eller passord.</string>\n    <string name=\"proxy_test_aborted\">Test avbrutt, vennligst fiks proxy-innstillingene først.</string>\n    <string name=\"proxy_application_aborted\">Vennligst korriger proxy-innstillingene først.</string>\n    <string name=\"proxy_test_start\">Test #%d: %s …</string>\n    <string name=\"proxy_test_error\">Feil #%d: %s</string>\n    <string name=\"proxy_test_status\">Status #%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">Adresse til konfigurasjonsfil (*.ovpn)</string>\n    <string name=\"enable_openvpn\">Bruk OpenVPN</string>\n    <string name=\"openvpn_settings_title\">OpenVPN-innstillinger</string>\n    <string name=\"openvpn_application_aborted\">Vennligst korriger OpenVPN-innstillingene først.</string>\n    <string name=\"openvpn_test_aborted\">Test avbrutt, vennligst fiks OpenVPN-innstillingene først.</string>\n    <string name=\"openvpn_address_invalid\">OpenVPN-konfigurasjonsadresse ikke angitt.</string>\n    <string name=\"simple_edit_value_hint\">Skriv inn verdi</string>\n    <string name=\"seek_interval\">Spolingsintervall</string>\n    <string name=\"seek_interval_sec\">%s sek</string>\n    <string name=\"subtitle_system\">Systemstil (Android-innstillinger > Tilgjengelighet)</string>\n    <string name=\"subtitle_scale\">Skalering av undertekst</string>\n    <string name=\"audio_sync_fix_desc\">En alternativ måte å synkronisere lyd/video på. Den anses som utdatert, men kan i noen tilfeller hjelpe.</string>\n    <string name=\"audio_sync_fix\">Lydsynkroniseringsfiks</string>\n    <string name=\"ambilight_ratio_fix_desc\">Fikser fraværende bakgrunnsbelysning. Fikser feil sideforhold eller videoskalering. Fikser blanke skjermbilder. Påvirker ytelsen!</string>\n    <string name=\"ambilight_ratio_fix\">Ambilight/Sideforhold/Videoskalering/Skjermbilder-fiks</string>\n    <string name=\"force_legacy_codecs_desc\">Forbedrer ytelsen betydelig på enheter med lav ytelse. Maksimal oppløsning er 720p.</string>\n    <string name=\"force_legacy_codecs\">Tving eldre kodeker (720p)</string>\n    <string name=\"live_stream_fix_desc\">Advarsel, denne justeringen deaktiverer tilbakespoling av strømminger. Forbedrer direktestrømmingsytelsen betydelig på enheter med lav ytelse. Maksimal oppløsning er 1080p.</string>\n    <string name=\"live_stream_fix_4k_desc\">Advarsel, denne justeringen deaktiverer tilbakespoling av strømminger. Forbedrer direktestrømmingsytelsen betydelig. Maksimal oppløsning er 4K.</string>\n    <string name=\"live_stream_fix\">Direktestrømmingsfiks (1080p)</string>\n    <string name=\"live_stream_fix_4k\">Direktestrømmingsfiks (4K)</string>\n    <string name=\"playback_notifications_fix_desc\">Skjuler varsler om sporbytte. Kan være nyttig på AOSP-basert fastvare.</string>\n    <string name=\"playback_notifications_fix\">Deaktiver avspillingsvarsler</string>\n    <string name=\"amlogic_fix_desc\">Fiks for tapte bilder på Amlogic-baserte enheter.</string>\n    <string name=\"amlogic_fix\">Amlogic 1080p@60fps-fiks</string>\n    <string name=\"tunneled_video_playback_desc\">MERK: pause fungerer kanskje ikke som den skal! Tunnelert videoavspilling lover fordeler som bedre lyd/video-synkronisering (AV-synk) og jevnere avspilling. Krever Android 5+</string>\n    <string name=\"tunneled_video_playback\">Tunnelert videoavspilling (Android 5+)</string>\n    <string name=\"master_volume\">Hovedvolum</string>\n    <string name=\"volume_limit\">Volumgrense</string>\n    <string name=\"player_volume\">Volum</string>\n    <string name=\"remember_position_of_short_videos\">Husk posisjon i korte videoer (mindre enn 5 min)</string>\n    <string name=\"remember_position_of_live_videos\">Husk posisjon i direktestrømming</string>\n    <string name=\"player_show_tooltips\">Vis verktøytips for knapper</string>\n    <string name=\"action_like_unset\">fjernet «liker»</string>\n    <string name=\"action_dislike_unset\">fjernet «liker ikke»</string>\n    <string name=\"not_compatible_with\">Valgt alternativ er ikke kompatibelt med</string>\n    <string name=\"subtitle_position\">Forskyvning av undertekst nederst</string>\n    <string name=\"pressing_home\">ved å trykke på HJEM</string>\n    <string name=\"pressing_home_back\">ved å trykke på HJEM eller TILBAKE</string>\n    <string name=\"pressing_back\">ved å trykke på TILBAKE</string>\n    <string name=\"player_number_key_seek\">Spol med talltaster</string>\n    <string name=\"save_playlist\">Legg til spilleliste i Spillelister-seksjonen</string>\n    <string name=\"remove_playlist\">Fjern spilleliste permanent fra Spillelister-seksjonen</string>\n    <string name=\"remove_playlist_fmt\">Fjerne %s permanent?</string>\n    <string name=\"removed_from_playlists\">Fjernet fra Spillelister-seksjonen</string>\n    <string name=\"saved_to_playlists\">Lagt til i Spillelister-seksjonen</string>\n    <string name=\"cant_delete_empty_playlist\">Kan ikke slette tom spilleliste</string>\n    <string name=\"playlist\">Spilleliste</string>\n    <string name=\"rename_playlist\">Gi nytt navn til spilleliste</string>\n    <string name=\"cant_rename_empty_playlist\">Kan ikke gi nytt navn til tom spilleliste</string>\n    <string name=\"cant_rename_foreign_playlist\">Kan ikke gi nytt navn til fremmed spilleliste</string>\n    <string name=\"cant_save_playlist\">Denne spillelisten kan ikke legges til</string>\n    <string name=\"enter_value\">Skriv inn en ikke-tom verdi</string>\n    <string name=\"dialog_add_remove_from\">Legg til/Fjern fra %s</string>\n    <string name=\"lb_playback_controls_skip_next\">Hopp til neste</string>\n    <string name=\"lb_playback_controls_skip_previous\">Hopp til forrige/Spol tilbake til start</string>\n    <string name=\"alt_presets_behavior\">Alternativ atferd for forhåndsinnstillinger (begrens båndbredde)</string>\n    <string name=\"alt_presets_behavior_desc\">Appen vil prøve å opprettholde båndbredden som tilsvarer den valgte forhåndsinnstillingen i stedet for å matche mellom oppløsning, fps og kodek.</string>\n    <string name=\"sleep_timer\">Innsovningstimer (hvis fjernkontrollen ikke brukes på en time)</string>\n    <string name=\"sleep_timer_desc\">Pause avspillingen hvis brukeren ikke har brukt fjernkontrollen på en time</string>\n    <string name=\"disable_vsync\">Deaktiver VSync-synkronisering</string>\n    <string name=\"disable_vsync_desc\">Dette alternativet deaktiverer justering av bilderammer med skjermens vertikale synkroniseringssignal. Dette kan forbedre ytelsen på enheter med lav ytelse ved å frigjøre CPU-ressurser.</string>\n    <string name=\"skip_codec_profile_check\">Hopp over sjekk av kodekprofilnivå</string>\n    <string name=\"skip_codec_profile_check_desc\">Ikke sjekk kodekstøtte ved start av en video. Kan være nyttig på fastvare med feil.</string>\n    <string name=\"force_sw_codec\">Tving SW-videodekoder</string>\n    <string name=\"force_sw_codec_desc\">Kan spille av nesten hvilken som helst video, men ytelsen er veldig dårlig.</string>\n    <string name=\"playlist_order_added_date_newer_first\">Lagt til-dato (nyeste først)</string>\n    <string name=\"playlist_order_added_date_older_first\">Lagt til-dato (eldste først)</string>\n    <string name=\"playlist_order_published_date_newer_first\">Publiseringsdato (nyeste først)</string>\n    <string name=\"playlist_order_published_date_older_first\">Publiseringsdato (eldste først)</string>\n    <string name=\"playlist_order_popularity\">Popularitet</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">Kan ikke gjøre dette for en fremmed spilleliste</string>\n    <string name=\"owned_playlist_warning\">Feil: Denne handlingen kan kun brukes på egne spillelister</string>\n    <string name=\"content_block_alt_server\">Bruk alternativ server</string>\n    <string name=\"content_block_alt_server_desc\">Aktiver dette alternativet hvis SponsorBlock nekter å fungere av ulike årsaker.</string>\n    <string name=\"playback_starts_shortly\">Avspillingen starter automatisk når strømmingen er klar</string>\n    <string name=\"starting_stream\">Starter strømming …</string>\n    <string name=\"action_playlist_remove\">Fjern fra spilleliste</string>\n    <string name=\"signin_view_title\">Brukerkode lastes inn …</string>\n    <string name=\"signin_view_description\">For å logge inn, skriv inn denne koden på siden %s\\nMERK. Fungerer kun med Firefox- eller Chrome-nettlesere.</string>\n    <string name=\"signin_view_action_text\">Ferdig</string>\n    <string name=\"require_checked\">Krever «%s»</string>\n    <string name=\"msg_press_again_to_exit\">Trykk igjen for å avslutte</string>\n    <string name=\"player_remaining_time\">Gjenstår: %s</string>\n    <string name=\"player_ending_time\">Slutter %s</string>\n    <string name=\"badge_new_content\">NYTT INNHOLD</string>\n    <string name=\"badge_live\">DIREKTE</string>\n    <string name=\"add_device_view_description\">For å koble til enheten, skriv inn denne koden i YouTube-appen på telefonen din under Innstillinger/Se på TV\\nMERK: Dette fungerer kun med Gboard-tastaturet!</string>\n    <string name=\"playback_controls_repeat_pause\">Gjenta Pause</string>\n    <string name=\"playback_controls_repeat_list\">Gjenta Liste</string>\n    <string name=\"action_subscribe_off\">Avslutt abonnement</string>\n    <string name=\"action_subscribe_on\">Abonner på</string>\n    <string name=\"skip_24_rate\">Hopp over 24fps-formater (ekte kinomodus-fiks?)</string>\n    <string name=\"skip_shorts\">Hopp over Shorts</string>\n    <string name=\"player_disable_suggestions\">Deaktiver forslag</string>\n    <string name=\"suggestions\">Forslag</string>\n    <string name=\"feedback\">Tilbakemelding</string>\n    <string name=\"sources\">Kilder</string>\n    <string name=\"releases\">Utgivelser</string>\n    <string name=\"prefer_avc_over_vp9\">Kodekvalg: foretrekk avc fremfor vp9</string>\n    <string name=\"open_chat\">Chat/Kommentarer</string>\n    <string name=\"open_comments\">Åpne kommentarer</string>\n    <string name=\"place_chat_left\">Plasser chat til venstre</string>\n    <string name=\"place_comments_left\">Plasser kommentarer til venstre</string>\n    <string name=\"use_alt_speech_recognizer\">Alternativ talegjenkjenner</string>\n    <string name=\"use_alt_speech_recognizer_desc\">Alvorlig feil. Bruk den kun hvis du har problemer med standardgjenkjenneren.</string>\n    <string name=\"speech_recognizer\">Talegjenkjenner</string>\n    <string name=\"speech_engine\">Stemmesøkemotor</string>\n    <string name=\"speech_recognizer_system\">System (foretrukket)</string>\n    <string name=\"speech_recognizer_external_1\">Ekstern 1 (alvorlig feil, for at den skal fungere må du deaktivere tilgang til mikrofonen)</string>\n    <string name=\"speech_recognizer_external_2\">Ekstern 2 (alvorlig feil)</string>\n    <string name=\"real_channel_icon\">Vis ikon på kanalknappen</string>\n    <string name=\"subtitle_yellow_semi_transparent\">Gul på halvgjennomsiktig bakgrunn</string>\n    <string name=\"subtitle_yellow_black\">Gul på svart bakgrunn</string>\n    <string name=\"player_pixel_ratio\">Pikselforhold</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">Mørkegrå (monokrom)</string>\n    <string name=\"disable_mic_permission\">Vennligst deaktiver mikrofontilgang for appen for at denne gjenkjenneren skal fungere som den skal.</string>\n    <string name=\"repeat_mode_shuffle\">Bland hvilken som helst spilleliste</string>\n    <string name=\"chat_left\">Venstre</string>\n    <string name=\"chat_right\">Høyre</string>\n    <string name=\"card_real_thumbnails\">Erstatt miniatyrbilder med et bilde fra videoen</string>\n    <string name=\"card_content\">Hvor skal miniatyrbilder for kort hentes fra</string>\n    <string name=\"thumb_quality_default\">Standard</string>\n    <string name=\"thumb_quality_start\">Starten av videoen</string>\n    <string name=\"thumb_quality_middle\">Midten av videoen</string>\n    <string name=\"thumb_quality_end\">Slutten av videoen</string>\n    <string name=\"content_block_status\">Sjekk SponsorBlock-serverstatus</string>\n    <string name=\"dearrow_status\">Sjekk DeArrow-serverstatus</string>\n    <string name=\"player_show_quality_info_bitrate\">Legg til bitrate i kvalitetsinformasjonen</string>\n    <string name=\"player_speed_button_old_behavior\">Tilbakestill gammel atferd for hastighetsknappen</string>\n    <string name=\"protect_settings_with_password\">Beskytt alle innstillinger med passord</string>\n    <string name=\"enter_settings_password\">Skriv inn innstillingspassord</string>\n    <string name=\"child_mode\">Barnemodus</string>\n    <string name=\"child_mode_desc\">I denne modusen kan ikke brukeren søke eller se noe foreslått innhold. Innstillingssiden vil være passordbeskyttet.</string>\n    <string name=\"lost_setting_warning\">Appens innstillinger vil bli endret. Sørg for at du har laget en sikkerhetskopi av innstillingene.</string>\n    <string name=\"player_button_long_click\">Hold inne denne knappen for flere alternativer</string>\n    <string name=\"pause_history\">Pause historikken</string>\n    <string name=\"resume_history\">Gjenoppta historikken</string>\n    <string name=\"disable_history\">Deaktiver historikk</string>\n    <string name=\"enable_history\">Aktiver historikk</string>\n    <string name=\"clear_history\">Tøm historikk</string>\n    <string name=\"chapters\">Kapitler</string>\n    <string name=\"card_multiline_subtitle\">Flerlinjers undertekster</string>\n    <string name=\"auto_frame_rate_modes\">Støttede moduser</string>\n    <string name=\"loading\">Laster inn …</string>\n    <string name=\"hide_streams\">Skjul strømminger fra Abonnementer</string>\n    <string name=\"video_rotate\">Roter</string>\n    <string name=\"video_flip\">Vend (speil)</string>\n    <string name=\"trending_searches\">Populære søk</string>\n    <string name=\"video_duration_any\">Alle</string>\n    <string name=\"video_duration\">Varighet</string>\n    <string name=\"video_duration_under_4\">Under 4 minutter</string>\n    <string name=\"video_duration_between_4_20\">4-20 minutter</string>\n    <string name=\"video_duration_over_20\">Over 20 minutter</string>\n    <string name=\"content_type\">Type</string>\n    <string name=\"content_type_any\">Alle</string>\n    <string name=\"content_type_video\">Video</string>\n    <string name=\"content_type_channel\">Kanal</string>\n    <string name=\"content_type_playlist\">Spilleliste</string>\n    <string name=\"content_type_movie\">Film</string>\n    <string name=\"video_features\">Funksjoner</string>\n    <string name=\"video_feature_any\">Alle</string>\n    <string name=\"video_feature_live\">Direkte</string>\n    <string name=\"video_feature_4k\">4K</string>\n    <string name=\"video_feature_hdr\">HDR</string>\n    <string name=\"search_sorting\">Sorter etter</string>\n    <string name=\"sort_by_relevance\">Relevans</string>\n    <string name=\"sort_by_views\">Antall visninger</string>\n    <string name=\"sort_by_date\">Opplastingsdato</string>\n    <string name=\"sort_by_rating\">Vurdering</string>\n    <string name=\"clear_search_history\">Tøm søkehistorikk</string>\n    <string name=\"remove_from_subscriptions\">Skjul</string>\n    <string name=\"player_long_speed_list\">Lang hastighetsliste</string>\n    <string name=\"player_extra_long_speed_list\">Ekstra lang hastighetsliste</string>\n    <string name=\"alt_app_icon\">Alternativt appikon (krever omstart)</string>\n    <string name=\"network_stack\">Foretrekk %s nettverksstabel</string>\n    <string name=\"player_network_stack\">Nettverksmotor</string>\n    <string name=\"cronet_desc\">Cronet er nettverksstabelen til Chromium. Cronet kan redusere ventetiden og øke nettverksytelsen, noe som kan hjelpe med bufferproblemer.</string>\n    <string name=\"unlock_all_formats\">Lås opp alle videoformater</string>\n    <string name=\"unlock_all_formats_desc\">På noen enheter rapporterer fastvaren feilaktig at noen formater ikke støttes, selv om de er det.\\n(f.eks. har 1080p smart-TVer en tendens til å rapportere 4k som ikke støttet).</string>\n    <string name=\"okhttp_desc\">Denne nettverksmotoren er den tregeste, men har god stabilitet.</string>\n    <string name=\"select_channel_section\">Åpne tilsvarende seksjon når appen startes fra ATV-kanaler</string>\n    <string name=\"enable_voice_search_desc\">Den offisielle appen vil bli erstattet med en såkalt «bro». En bro hjelper med å overføre globale søkeforespørsler til vår app.</string>\n    <string name=\"enable_master_password\">Aktiver hovedpassord</string>\n    <string name=\"enter_master_password\">Skriv inn hovedpassord</string>\n    <string name=\"disable_stream_buffer\">Deaktiver buffer på strømminger</string>\n    <string name=\"disable_stream_buffer_desc\">Fiks for situasjoner der strømmingen er for langt bak. Merk: strømmingen kan begynne å hakke.</string>\n    <string name=\"sony_frame_drop_fix\">Fiks for tapte bilder #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">Fikser hakking på Sony TV og noen andre enheter. Merk: mulige problemer med lydsynkronisering.</string>\n    <string name=\"audio_language\">Lydspråk</string>\n    <string name=\"old_home_look\">Gammelt utseende for Hjem-seksjonen</string>\n    <string name=\"old_channel_look\">Gammelt utseende for Kanalsiden</string>\n    <string name=\"hide_shorts_everywhere\">Skjul Shorts overalt</string>\n    <string name=\"update_found\">Oppdatering</string>\n    <string name=\"volume_boost_warning\">Alle innstillinger over grensen kan potensielt skade høyttalerne</string>\n    <string name=\"play_video_incognito\">Spill av inkognito</string>\n    <string name=\"header_kids_home\">Barn</string>\n    <string name=\"player_section_playlist\">Bruk innholdet i gjeldende seksjon som en spilleliste</string>\n    <string name=\"header_trending\">Populært</string>\n    <string name=\"screensaver\">Skjermsparer</string>\n    <string name=\"player_screen_off_timeout\">Tidsavbrudd for skjerm av</string>\n    <string name=\"old_update_notifications\">Gammelt utseende for oppdateringsvarsler</string>\n    <string name=\"dialog_notification\">Varsle om oppdateringer med en popup-dialog</string>\n    <string name=\"mark_as_watched\">Merk som sett</string>\n    <string name=\"player_ui_animations\">Aktiver animasjoner i avspillerens brukergrensesnitt</string>\n    <string name=\"player_likes_count\">Vis antall «liker»/«liker ikke»</string>\n    <string name=\"sorting_alphabetically2\">Alfabetisk (rask, animerte forhåndsvisninger)</string>\n    <string name=\"sorting_default\">Standard (rask, animerte forhåndsvisninger)</string>\n    <string name=\"content_block_exclude_channel\">Ekskluder denne kanalen fra SponsorBlock</string>\n    <string name=\"content_block_stop_excluding_channel\">Slutt å ekskludere denne kanalen fra SponsorBlock</string>\n    <string name=\"player_chapter_notification\">Naviger raskt gjennom kapitler ved hjelp av popup-varslingen</string>\n    <string name=\"subtitle_remember\">Aktiver undertekster kun på den gjeldende kanalen</string>\n    <string name=\"amazon_frame_drop_fix\">Fiks for tapte bilder #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">Beregnet for Amazon Stick-enheter. Kan også fungere på andre enheter.</string>\n    <string name=\"default_stack_desc\">Innebygd nettverksmotor. Denne motoren kan ha bedre stabilitet i visse situasjoner.</string>\n    <string name=\"item_postion\">Posisjon til</string>\n    <string name=\"player_screen_off_dimming\">Dempingsgrad for skjerm av</string>\n    <string name=\"screensaver_timout\">Tidsavbrudd for skjermsparer</string>\n    <string name=\"screensaver_dimming\">Dempingsgrad for skjermsparer</string>\n    <string name=\"player_ui_on_next\">Vis avspillerens brukergrensesnitt ved bytte til neste video</string>\n    <string name=\"autogenerated\">autogenerert</string>\n    <string name=\"player_auto_volume\">Automatisk volumjustering</string>\n    <string name=\"header_shorts\">Shorts</string>\n    <string name=\"player_global_focus\">Sømløs navigering mellom knapprader i avspilleren</string>\n    <string name=\"auto_history\">Auto (bruk kontoinnstillinger)</string>\n    <string name=\"remember_position_subscriptions\">Husk sist sette posisjon i Abonnementer</string>\n    <string name=\"remember_position_pinned\">Husk sist sette posisjon i festede spillelister</string>\n    <string name=\"msg_player_unknown_error\">Ukjent feil</string>\n    <string name=\"unknown_source_error\">Ukjent kildefeil</string>\n    <string name=\"unknown_renderer_error\">Ukjent gjengivelsesfeil</string>\n    <string name=\"header_notifications\">Varsler</string>\n    <string name=\"disable_search_history\">Deaktiver søkehistorikk</string>\n    <string name=\"unlock_high_bitrate_formats\">Lås opp 1080p vp9-formater med høy bitrate</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">Lås opp mp4a-formater med høy bitrate</string>\n    <string name=\"color_scheme_blue\">Blå</string>\n    <string name=\"color_scheme_dark_blue\">Mørkeblå</string>\n    <string name=\"player_speed_per_channel\">For hver kanal</string>\n    <string name=\"disable_popular_searches\">Deaktiver populære søkeforespørsler</string>\n    <string name=\"multi_profiles\">Bruk separate innstillinger for hver konto</string>\n    <string name=\"protect_account_with_password\">Beskytt denne kontoen med passord</string>\n    <string name=\"enter_account_password\">Skriv inn kontopassord</string>\n    <string name=\"show_connect_messages\">Vis tilkoblingsmeldinger</string>\n    <string name=\"prefer_avc_over_vp9_desc\">ADVARSEL: maksimalt 1080p</string>\n    <string name=\"play_next\">Spill av neste</string>\n    <string name=\"hide_watched_from_subscriptions\">Skjul sette videoer fra Abonnementer</string>\n    <string name=\"hide_watched_from_notifications\">Skjul sette videoer fra Varsler</string>\n    <string name=\"hide_unwanted_content\">Skjul videoer</string>\n    <string name=\"hide_watched_from_home\">Skjul sette videoer fra Hjem</string>\n    <string name=\"hide_watched_from_watch_later\">Skjul sette videoer fra Se senere-spillelisten</string>\n    <string name=\"remote_control_permission\">Fjernkontroll i bakgrunnen krever tillatelse til å vises over andre apper</string>\n    <string name=\"login_from_browser\">Logg inn fra nettleser</string>\n    <string name=\"disable_remote_history\">Deaktiver historikk ved bruk av fjernkontroll</string>\n    <string name=\"keyboard_fix\">Forhindre at OK-knappen åpner tastaturet (G20s og andre)</string>\n    <string name=\"nothing_found\">Ingenting funnet</string>\n    <string name=\"auto_frame_rate_desc\">Dette alternativet fjerner hakking i scener der kameraet beveger seg raskt, f.eks. ved strømming av sport</string>\n    <string name=\"channel_filter_hint\">Filtrer kanaler</string>\n    <string name=\"player_loop_shorts\">Gjenta Shorts</string>\n    <string name=\"player_quick_shorts_skip\">Hopp over Shorts med venstre/høyre-knappene</string>\n    <string name=\"player_quick_skip_videos\">Hopp over vanlige videoer med venstre/høyre-knappene</string>\n    <string name=\"channels_filter\">Vis Filtrer kanaler-feltet inne i Kanaler-seksjonen</string>\n    <string name=\"channel_search_bar\">Søkefelt inne på Kanalsiden</string>\n    <string name=\"header_sports\">Sport</string>\n    <string name=\"keep_finished_activities\">Behold avsluttede aktiviteter</string>\n    <string name=\"disable_channels_service\">Deaktiver Kanaler-tjenesten</string>\n    <string name=\"replace_titles\">Erstatt titler</string>\n    <string name=\"enable\">Aktiver</string>\n    <string name=\"more_info\">Mer info</string>\n    <string name=\"about_sponsorblock\">Om SponsorBlock</string>\n    <string name=\"about_dearrow\">Om DeArrow</string>\n    <string name=\"replace_thumbnails\">Erstatt miniatyrbilder</string>\n    <string name=\"crowdsourced_thumbnails\">Folkefinansierte miniatyrbilder</string>\n    <string name=\"crowdsoursed_titles\">Folkefinansierte titler</string>\n    <string name=\"dearrow_not_submitted_thumbs\">Miniatyrbildekilde (hvis det ikke finnes i DeArrow)</string>\n    <string name=\"pitch_effect\">Tonehøydeeffekt</string>\n    <string name=\"fullscreen_mode\">Fullskjermmodus (uten systemlinjer)</string>\n    <string name=\"player_only_mode\">Vis kun avspilleren hvis videoen åpnes utenfor appen</string>\n    <string name=\"pinned_channel_rows\">Vis festede kanaler som rader</string>\n    <string name=\"prefer_ipv4\">Foretrekk IPv4 DNS</string>\n    <string name=\"prefer_ipv4_desc\">Kan fikse situasjoner der appen ikke fungerer i det hele tatt.\\nMerk. Kan forårsake heng og krasj (spesielt på Android 8-enheter eller Dune HD)</string>\n    <string name=\"long_press_for_settings\">HOLD INNE FOR INNSTILLINGER</string>\n    <string name=\"long_press_for_options\">HOLD INNE FOR ALTERNATIVER</string>\n    <string name=\"device_specific_backup\">Sikkerhetskopi kun for denne enheten</string>\n    <string name=\"local_backup\">Lokal sikkerhetskopi</string>\n    <string name=\"auto_backup\">Automatisk sikkerhetskopi (én gang om dagen)</string>\n    <string name=\"repeat_mode_reverse_list\">Spill av videoer i spillelisten eller kanalen i omvendt rekkefølge</string>\n    <string name=\"calm_msg\">Vi fikser problemet. Sjekk for oppdateringer fra tid til annen</string>\n    <string name=\"without_picture\">Uten bilde</string>\n    <string name=\"video_disabled\">Video deaktivert</string>\n    <string name=\"applying_fix\">Ikke lukk avspilleren. Kjører fiks …</string>\n    <string name=\"hide_mixes\">Skjul Mikser</string>\n    <string name=\"player_global_focus_desc\">Denne funksjonen påvirker hvilken avspillerknapp som får fokus når du navigerer mellom knapprader i avspilleren</string>\n    <string name=\"disable_network_error_fixing\">Deaktiver automatisk retting av nettverksfeil</string>\n    <string name=\"disable_network_error_fixing_desc\">Du må sannsynligvis aktivere dette alternativet hvis du bruker en VPN</string>\n    <string name=\"recommended\">Anbefalt</string>\n    <string name=\"add_to_subscriptions_group\">Legg til/Fjern fra abonnementsgruppe</string>\n    <string name=\"new_subscriptions_group\">Ny gruppe</string>\n    <string name=\"rename_group\">Gi nytt navn til abonnementsgruppen</string>\n    <string name=\"screen_dimming_amount\">Dempingsgrad for skjerm</string>\n    <string name=\"screen_dimming_timeout\">Tidsavbrudd for skjermdemping</string>\n    <string name=\"playlists_rows\">Vis Spillelister-seksjonen som rader</string>\n    <string name=\"import_subscriptions_group\">Importer</string>\n    <string name=\"lb_playback_controls_closed_captioning_disable\">Deaktiver teksting for hørselshemmede</string>\n    <string name=\"lb_playback_controls_closed_captioning_enable\">Aktiver teksting for hørselshemmede</string>\n    <string name=\"my_videos\">Mine videoer</string>\n    <string name=\"player_audio_focus\">Lydfokus (pause hvis andre avspillere oppdages)</string>\n    <string name=\"paid_content_notification\">Varsel om betalt innhold</string>\n    <string name=\"you_liked\">du likte</string>\n    <string name=\"premium_users_only\">Kun for Premium-brukere. Fiks for ufullstendig videoformatliste</string>\n    <string name=\"playback_buffering_fix\">Fiks for avspillingsbuffring</string>\n    <string name=\"oculus_quest_fix\">Oculus Quest-fiks</string>\n    <string name=\"card_preview_muted\">Video uten lyd</string>\n    <string name=\"card_preview_full\">Video med lyd</string>\n    <string name=\"card_preview\">Forhåndsvisning av kort</string>\n    <string name=\"card_unlocalized_titles\">Ikke-lokaliserte videotitler</string>\n    <string name=\"dont_resize_video_to_fit_dialog\">Ikke endre størrelsen på videoen for å passe i dialogboksen</string>\n</resources>"
  },
  {
    "path": "common/src/main/res/values-nl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Home</string>\n  <string name=\"title_search\">Zoeken</string>\n  <string name=\"header_subscriptions\">Abonnementen</string>\n  <string name=\"header_history\">Geschiedenis</string>\n  <string name=\"header_music\">Muziek</string>\n  <string name=\"header_news\">Nieuws</string>\n  <string name=\"header_gaming\">Games</string>\n  <string name=\"header_playlists\">Afspeellijsten</string>\n  <string name=\"header_settings\">Instellingen</string>\n  <string name=\"header_channels\">Kanalen</string>\n  <string name=\"subscriptions_signin_title\">Bekijk je favoriete kanalen</string>\n  <string name=\"subscriptions_signin_subtitle\">Log in om je abonnementen te bekijken</string>\n  <string name=\"subscriptions_signin_button_text\">INLOGGEN</string>\n  <string name=\"library_signin_to_show_more\">Log in om je video\\'s te bekijken</string>\n  <string name=\"library_signin_subtitle\">Log in om je bibliotheek te bekijken</string>\n  <string name=\"action_signin\">INLOGGEN</string>\n  <string name=\"title_video_formats\">Videokwaliteit</string>\n  <string name=\"title_audio_formats\">Audiokwaliteit</string>\n  <string name=\"subtitle_category_title\">Ondertiteling</string>\n  <string name=\"playback_queue_category_title\">Afspeelwachtrij</string>\n  <string name=\"video_max_quality\">Automatisch (max. kwaliteit)</string>\n  <string name=\"audio_max_quality\">Automatisch (max. kwaliteit)</string>\n  <string name=\"subtitles_disabled\">Geen ondertiteling</string>\n  <string name=\"auto_frame_rate\">Automatische framerate</string>\n  <string name=\"frame_rate_correction\">Corrigeer fps:\\n%s</string>\n  <string name=\"category_background_playback\">Afspelen op achtergrond</string>\n  <string name=\"not_implemented\">Niet-geïmplementeerd</string>\n  <string name=\"option_background_playback_off\">Uit</string>\n  <string name=\"option_background_playback_pip\">Scherm-in-scherm</string>\n  <string name=\"option_background_playback_only_audio\">Alleen audio</string>\n  <string name=\"playback_settings\">Afspeelinstellingen</string>\n  <string name=\"video_speed\">Videosnelheid</string>\n  <string name=\"resolution_switch\">Resolutie aanpassen</string>\n  <string name=\"video_buffer\">Video\\'s bufferen</string>\n  <string name=\"video_buffer_size_none\">Niet</string>\n  <string name=\"video_buffer_size_lowest\">Laagste</string>\n  <string name=\"video_buffer_size_low\">Minimaal</string>\n  <string name=\"video_buffer_size_med\">Gemiddeld</string>\n  <string name=\"video_buffer_size_high\">Hoog</string>\n  <string name=\"video_buffer_size_highest\">Hoogste</string>\n  <string name=\"update_changelog\">Wijzigingslog</string>\n  <string name=\"install_update\">Update installeren</string>\n  <string name=\"section_is_empty\">Deze rubriek is leeg.</string>\n  <string name=\"dialog_add_to_playlist\">Op afspeellijst</string>\n  <string name=\"msg_signed_users_only\">Enkel voor ingelogde gebruikers</string>\n  <string name=\"msg_cant_load_content\">De inhoud kan niet worden geladen.\\n1) Controleer je internetverbinding.\\n2) Log in op je account.</string>\n  <string name=\"title_video_presets\">Voorinstelling</string>\n  <string name=\"settings_accounts\">Accounts</string>\n  <string name=\"settings_left_panel\">Rubrieken</string>\n  <string name=\"settings_themes\">Thema</string>\n  <string name=\"settings_other\">Overig</string>\n  <string name=\"settings_player\">Videospeler</string>\n  <string name=\"settings_language\">Taal</string>\n  <string name=\"settings_linked_devices\">Gekoppelde apparaten</string>\n  <string name=\"settings_about\">Informatie</string>\n  <string name=\"dialog_account_list\">Account kiezen</string>\n  <string name=\"dialog_account_none\">Geen account</string>\n  <string name=\"dialog_remove_account\">Account verwijderen</string>\n  <string name=\"dialog_add_account\">Account toevoegen</string>\n  <string name=\"default_lang\">Standaard</string>\n  <string name=\"dialog_select_language\">Taal</string>\n  <string name=\"subtitle_default\">Standaard</string>\n  <string name=\"subtitle_white_semi_transparent\">Doorzichtige achtergrond</string>\n  <string name=\"subtitle_style\">Ondertitel stijl</string>\n  <string name=\"subtitle_language\">Ondertitel taal</string>\n  <string name=\"action_search\">Zoeken</string>\n  <string name=\"settings_main_ui\">Weergave</string>\n  <string name=\"dialog_main_ui\">Weergave</string>\n  <string name=\"card_animated_previews\">Videominiaturen voorvertonen</string>\n  <string name=\"web_site\">Website</string>\n  <string name=\"donation\">App ondersteunen</string>\n  <string name=\"dialog_about\">Informatie</string>\n  <string name=\"dialog_player_ui\">Videospeler</string>\n  <string name=\"player_show_ui_on_pause\">Speler weergeven met oké-toets</string>\n  <string name=\"player_pause_on_ok\">Video pauzeren met oké-toets</string>\n  <string name=\"player_ok_button_behavior\">Oké-toets</string>\n  <string name=\"player_only_ui\">Speler weergeven</string>\n  <string name=\"player_ui_and_pause\">Speler weergeven en pauzeren</string>\n  <string name=\"player_only_pause\">Speler pauzeren</string>\n  <string name=\"check_for_updates\">Controleren op update</string>\n  <string name=\"update_not_found\">Geen update beschikbaar.</string>\n  <string name=\"update_in_progress\">Even geduld…</string>\n  <string name=\"player_ui_hide_behavior\">Speler verbergen</string>\n  <string name=\"option_never\">Nooit</string>\n  <string name=\"side_panel_sections\">Rubrieken</string>\n  <string name=\"boot_to_section\">Opstarten met rubriek</string>\n  <string name=\"large_ui\">Grote weergave</string>\n  <string name=\"video_grid_scale\">Videoformaat</string>\n  <string name=\"scale_ui\">Schermformaat</string>\n  <string name=\"color_scheme\">Thema</string>\n  <string name=\"color_scheme_default\">Standaard</string>\n  <string name=\"color_scheme_red_grey\">Rood-grijs</string>\n  <string name=\"color_scheme_red\">Rood</string>\n  <string name=\"color_scheme_dark_grey\">Donkergrijs</string>\n  <string name=\"disable_update_check\">Niet automatisch updaten</string>\n  <string name=\"show_again\">Altijd weergeven</string>\n  <string name=\"check_updates_auto\">Melding over nieuwe update</string>\n  <string name=\"select_account_on_boot\">Accountkeuze weergeven na opstarten</string>\n  <string name=\"player_other\">Overige instellingen</string>\n  <string name=\"player_full_date\">Publicatiedatum weergeven</string>\n  <string name=\"open_channel\">Kanaal weergeven</string>\n  <string name=\"open_playlist\">Afspeellijst weergeven</string>\n  <string name=\"not_interested\">Geen interesse</string>\n  <string name=\"not_recommend_channel\">Kanaal niet aanraden</string>\n  <string name=\"you_wont_see_this_video\">De video is verwijderd uit je aanbevelingen</string>\n  <string name=\"you_wont_see_this_channel\">Je ziet dit kanaal niet meer in je aanbevelingen</string>\n  <string name=\"settings_search\">Zoeken</string>\n  <string name=\"dialog_search\">Zoeken</string>\n  <string name=\"instant_voice_search\">Spraakgestuurd zoeken</string>\n  <string name=\"option_background_playback_behind\">Afspelen op achtergrond</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">De volgende video is nog niet geladen.</string>\n  <string name=\"card_multiline_title\">Meerdere titelregels weergeven</string>\n  <string name=\"cards_style\">Video-indeling</string>\n  <string name=\"repeat_mode_all\">Alle video\\'s afspelen</string>\n  <string name=\"repeat_mode_one\">Video herhalen</string>\n  <string name=\"repeat_mode_pause\">Pauzeren na elke video</string>\n  <string name=\"repeat_mode_pause_alt\">Pauzeren na elke video (n.v.t. op afspeellijsten)</string>\n  <string name=\"repeat_mode_none\">Stoppen na elke video</string>\n  <string name=\"subscribed_to_channel\">Kanaal geabonneerd</string>\n  <string name=\"unsubscribed_from_channel\">Kanaal niet geabonneerd</string>\n  <string name=\"subtitle_yellow_transparent\">Geel</string>\n  <string name=\"subtitle_white_transparent\">Wit op transparante achtergrond</string>\n  <string name=\"subtitle_white_black\">Zwarte achtergrond</string>\n  <string name=\"player_seek_preview\">Voorvertoning weergeven tijdens snelzoeken</string>\n  <string name=\"color_scheme_dark_grey_oled\">Donkergrijs (OLED)</string>\n  <string name=\"channels_section_sorting\">Kanalen sorteren</string>\n  <string name=\"sorting_by_new_content\">Nieuwe video\\'s</string>\n  <string name=\"sorting_alphabetically\">Alfabetisch</string>\n  <string name=\"sorting_last_viewed\">Laatst bekeken</string>\n  <string name=\"player_pause_when_seek\">Pauzeren tijdens snelzoeken</string>\n  <string name=\"playlists_style\">Afspeellijstindeling</string>\n  <string name=\"playlists_style_grid\">Rooster</string>\n  <string name=\"playlists_style_rows\">Rijen</string>\n  <string name=\"player_show_clock\">Klok weergeven op spelerbediening</string>\n  <string name=\"player_show_remaining_time\">Resterende tijd weergeven</string>\n  <string name=\"player_show_ending_time\">Eindtijd weergeven</string>\n  <string name=\"open_channel_uploads\">Recente uploads</string>\n  <string name=\"app_exit_shortcut\">App afsluiten</string>\n  <string name=\"player_exit_shortcut\">Speler afsluiten</string>\n  <string name=\"app_exit_none\">Niet afsluiten</string>\n  <string name=\"app_double_back_exit\">2x terugtoets</string>\n  <string name=\"app_single_back_exit\">1x terugtoets</string>\n  <string name=\"action_video_zoom\">Beeldverhouding</string>\n  <string name=\"video_zoom\">Beeldverhouding</string>\n  <string name=\"video_zoom_default\">Standaard</string>\n  <string name=\"video_zoom_fit_width\">Breedte aanpassen</string>\n  <string name=\"video_zoom_fit_height\">Hoogte aanpassen</string>\n  <string name=\"video_zoom_fit_both\">Breedte en hoogte aanpassen</string>\n  <string name=\"video_zoom_stretch\">Uitrekken</string>\n  <string name=\"color_scheme_teal\">Blauw-groen</string>\n  <string name=\"color_scheme_teal_oled\">Blauw-groen (OLED)</string>\n  <string name=\"player_seek_preview_none\">Niet voorvertonen</string>\n  <string name=\"player_seek_preview_single\">Eén frame</string>\n  <string name=\"player_seek_preview_carousel\">Meerdere frames</string>\n  <string name=\"player_seek_preview_carousel_slow\">Meerdere frames (traag)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Meerdere frames (snel)</string>\n  <string name=\"unsubscribe_from_channel\">Deabonneren</string>\n  <string name=\"card_title_lines_num\">Aantal rijen</string>\n  <string name=\"card_auto_scrolled_title\">Titel scrollen</string>\n  <string name=\"share_link\">Delen</string>\n  <string name=\"share_qr_link\">Delen (QR code)</string>\n  <string name=\"subscribe_to_channel\">Abonneren</string>\n  <string name=\"wait_data_loading\">Bezig met laden van gegevens…</string>\n  <string name=\"auto_frame_rate_pause\">Automatische framerate (pauze)</string>\n  <string name=\"auto_frame_rate_applying\">Bezig met toepassen van automatische framerate \\‘%sx%s\\@%s\\’</string>\n  <string name=\"audio_shift\">Audiosynchronisatie</string>\n  <string name=\"player_remember_speed\">Videosnelheid onthouden</string>\n  <string name=\"mark_channel_as_watched\">Markeren als bekeken</string>\n  <string name=\"channel_marked_as_watched\">Het kanaal is gemarkeerd als bekeken</string>\n  <string name=\"dialog_add_device\">Apparaat toevoegen</string>\n  <string name=\"dialog_remove_all_devices\">Apparaten verwijderen</string>\n  <string name=\"device_link_enabled\">Meldingen weergeven</string>\n  <string name=\"device_connected\">\\‘%s\\’ is gekoppeld</string>\n  <string name=\"device_disconnected\">\\‘%s\\’ is ontkoppeld</string>\n  <string name=\"settings_ui_scale\">Formaat</string>\n  <string name=\"msg_restart_app\">Start de app opnieuw op om de instellingen toe te passen</string>\n  <string name=\"settings_block\">Inhoudsblokkade</string>\n  <string name=\"msg_applying\">Bezig met toepassen van %s</string>\n  <string name=\"msg_done\">Voltooid</string>\n  <string name=\"settings_general\">Algemeen</string>\n  <string name=\"settings_video\">Video</string>\n  <string name=\"content_block_confirm_skip\">Overslaan bevestigen</string>\n  <string name=\"content_block_categories\">Segmenten</string>\n  <string name=\"content_block_sponsor\">Sponsor/Betaalde promotie</string>\n  <string name=\"content_block_intro\">Introfilmpje/Onderbreking</string>\n  <string name=\"content_block_outro\">Eindscherm/Met dank aan</string>\n  <string name=\"content_block_interaction\">Interactie (abonneren/leuk vinden/volgen)</string>\n  <string name=\"content_block_self_promo\">Zelfpromotie/Onbetaalde promotie</string>\n  <string name=\"content_block_music_off_topic\">Intro-/Outrofilmpje van muziekvideo\\'s</string>\n  <string name=\"confirm_segment_skip\">Wil je\\‘%s\\’ overslaan \\?</string>\n  <string name=\"cancel_segment_skip\">Niet overslaan</string>\n  <string name=\"msg_skipping_segment\">\\‘%s\\’ wordt overgeslagen…</string>\n  <string name=\"content_block_notification_type\">Meldingen</string>\n  <string name=\"content_block_notify_none\">Geen melding weergeven</string>\n  <string name=\"content_block_notify_toast\">Melding weergeven</string>\n  <string name=\"content_block_notify_dialog\">Om bevestiging vragen</string>\n  <string name=\"return_to_launcher\">Terug naar Android TV-Home/zoeken</string>\n  <string name=\"intent_force_close\">Video van externe bron geforceerd afsluiten</string>\n  <string name=\"btn_confirm\">Bevestigen</string>\n  <string name=\"player_low_video_quality\">Lage videokwaliteit</string>\n  <string name=\"remote_session_closed\">De sessie op afstand is beëindigd</string>\n  <string name=\"msg_mode_switch_error\">De verhouding kan niet worden aangepast naar \\‘%s\\’</string>\n  <string name=\"msg_player_error\">\\‘%s\\’ kan niet worden afgespeeld - er wordt opnieuw gestart</string>\n  <string name=\"video_preset_disabled\">Geen voorinstelling</string>\n  <string name=\"video_preset_enabled\">De kwaliteit van de volgende video wordt ingesteld volgens je voorinstelling</string>\n  <string name=\"player_sleep_timer\">Slaapklok</string>\n  <string name=\"player_show_quality_info\">Videokwaliteit weergeven</string>\n  <string name=\"player_remember_each_speed\">Videosnelheid onthouden (per video)</string>\n  <string name=\"player_remember_speed_none\">Niet onthouden</string>\n  <string name=\"player_remember_speed_all\">Dezelfde snelheid gebruiken voor alle video\\'s</string>\n  <string name=\"player_remember_speed_each\">Specifieke snelheid per video</string>\n  <string name=\"msg_player_error_source\">De video kan niet worden geladen</string>\n  <string name=\"msg_player_error_renderer\">De geselecteerde videokwaliteit wordt niet ondersteund</string>\n  <string name=\"msg_player_error_video_renderer\">Geselecteerd VIDEO formaat wordt niet ondersteund.\\n Probeer een andere door op de HQ knop in the player te klikken.\\nAls dit niet werkt herstart het apparaat.</string>\n  <string name=\"msg_player_error_audio_renderer\">Geselecteerd AUDIO formaat wordt niet ondersteund.\\n Probeer een andere door op de HQ knop in the player te klikken.\\nAls dit niet werkt herstart het apparaat.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">Geslecteerd ONDERTITELING formaat wordt niet ondersteund.\\nProbeer een andere, houd lang ingedrukt op CC knop in de speler.\\nnAls dit niet werkt herstart het apparaat.</string>\n  <string name=\"msg_player_error_unexpected\">Onbekende video-decodeerfout</string>\n  <string name=\"video_aspect\">Beeldverhouding</string>\n  <string name=\"player_tweaks\">Ontwikkelaarsopties</string>\n  <string name=\"header_uploads\">Kanalen/Uploads</string>\n  <string name=\"player_show_global_clock\">Klok op videobeeld weergeven</string>\n  <string name=\"player_show_global_ending_time\">Eindtijd altijd op het scherm weergeven</string>\n  <string name=\"app_corner_clock\">Home: klok rechtsboven</string>\n  <string name=\"player_corner_clock\">Speler: klok rechtsboven</string>\n  <string name=\"player_corner_ending_time\">Speler: eindtijd rechtsboven</string>\n  <string name=\"uploads_old_look\">Oude kanaal-/uploadindeling</string>\n  <string name=\"background_playback_activation\">Afspelen op achtergrond (toets)</string>\n  <string name=\"channels_old_look\">Oude kanaalindeling</string>\n  <string name=\"channels_auto_load\">Kanaalinhoud automatisch laden</string>\n  <string name=\"return_to_background_video\">Terug naar achtergrondvideo</string>\n  <string name=\"pin_unpin_from_sidebar\">Vast-/Losmaken aan/van zijbalk</string>\n  <string name=\"pin_unpin_playlist\">Vast-/Losmaken aan/van afspeellijst</string>\n  <string name=\"pin_unpin_channel\">Vast-/Losmaken aan/van kanaal</string>\n  <string name=\"pin_playlist\">Afspeellijst vastmaken aan de zijbalk</string>\n  <string name=\"pin_channel\">Kanaal vastmaken aan de zijbalk</string>\n  <string name=\"pinned_to_sidebar\">Vastgemaakt aan de zijbalk</string>\n  <string name=\"unpin_from_sidebar\">Losmaken van zijbalk</string>\n  <string name=\"unpin_group_from_sidebar\">Verwijder de abonnementsgroep</string>\n  <string name=\"unpinned_from_sidebar\">Losgemaakt van de zijbalk</string>\n  <string name=\"dialog_select_country\">Land</string>\n  <string name=\"settings_language_country\">Taal/Land</string>\n  <string name=\"share_embed_link\">Insluitlink delen</string>\n  <string name=\"card_text_scroll_factor\">Scrollsnelheid van tekst</string>\n  <string name=\"preferred_update_source\">Updatebron kiezen</string>\n  <string name=\"hide_shorts\">Shorts verbergen op abonnementenlijst</string>\n  <string name=\"hide_shorts_channel\">Shorts verbergen op kanaal</string>\n  <string name=\"key_remapping\">Toetstoewijzingen</string>\n  <string name=\"screen_dimming\">Scherm dimmen</string>\n  <string name=\"removed_from_playback_queue\">De video is verwijderd uit de afspeelwachtrij</string>\n  <string name=\"added_to_playback_queue\">De video is toegevoegd aan de afspeelwachtrij</string>\n  <string name=\"add_remove_from_playback_queue\">In afspeelwachtrij</string>\n  <string name=\"add_to_playback_queue\">Toevoegen aan afspeelwachtrij</string>\n  <string name=\"remove_from_playback_queue\">Verwijderen uit afspeelwachtrij</string>\n  <string name=\"proxy_enabled\">De proxy is ingeschakeld</string>\n  <string name=\"proxy_disabled\">De proxy is uitgeschakeld</string>\n  <string name=\"uploads_row_name\">Uploads</string>\n  <string name=\"playlists_row_name\">Gemaakte afspeellijsten</string>\n  <string name=\"popular_uploads_row_name\">Populaire uploads</string>\n  <string name=\"news_row_name\">Nieuws</string>\n  <string name=\"breaking_news_row_name\">Belangrijk nieuws</string>\n  <string name=\"covid_news_row_name\">Coronanieuws</string>\n  <string name=\"enable_voice_search\">Spraakgestuurd zoeken inschakelen</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">(De)abonneren op kanaal</string>\n  <string name=\"app_backup_restore\">Back-up/Herstel</string>\n  <string name=\"app_backup\">Gegevens back-uppen</string>\n  <string name=\"app_restore\">Gegevens herstellen uit back-up</string>\n  <string name=\"skip_each_segment_once\">Segmenten niet meer overslaan</string>\n  <string name=\"disable_ok_long_press\">OK-knop uitschakelen lang indrukken</string>\n  <string name=\"disable_ok_long_press_desc\">Bedoelt voor controllers met bugs waar de OK knop niet goed werkt</string>\n  <string name=\"player_time_correction\">Geef de tijd weer met betrekking tot snelheid</string>\n  <string name=\"sponsor_color_markers\">Kleurmarkeringen op de voortgangsbalk</string>\n  <string name=\"refresh_section\">Sectie vernieuwen</string>\n  <string name=\"option_disabled\">Uitgeschakeld</string>\n  <string name=\"live_now_row_name\">Nu live</string>\n  <string name=\"run_in_background\">Op de achtergrond uitvoeren</string>\n  <string name=\"settings_remote_control\">Afstandsbediening</string>\n  <string name=\"background_service_started\">Achtergrondservice gestart</string>\n  <string name=\"dialog_add_to\">Toevoegen aan %s</string>\n  <string name=\"dialog_remove_from\">Verwijderd uit %s</string>\n  <string name=\"added_to\">Toegevoegd aan %s</string>\n  <string name=\"removed_from\">Verwijderd uit %s</string>\n  <string name=\"video_preset_adaptive\">Adaptief</string>\n  <string name=\"content_block_preview_recap\">Voorbeeld of samenvatting van de video</string>\n  <string name=\"content_block_highlight\">Punt of hoogtepunt van de video</string>\n  <string name=\"removed_from_history\">Video is verwijderd uit geschiedenis</string>\n  <string name=\"remove_from_history\">Verwijderen uit geschiedenis</string>\n  <string name=\"upload_date\">Uploaddatum</string>\n  <string name=\"upload_date_any\">Altijd</string>\n  <string name=\"upload_date_today\">Vandaag</string>\n  <string name=\"upload_date_this_week\">Deze week</string>\n  <string name=\"upload_date_this_month\">Deze maand</string>\n  <string name=\"upload_date_this_year\">Dit jaar</string>\n  <string name=\"double_refresh_rate\">Dubbelde verversingssnelheid</string>\n  <string name=\"upload_date_last_hour\">Laatste uur</string>\n  <string name=\"focus_on_search_results\">Autofocus op zoekresultaten</string>\n  <string name=\"context_menu\">Contextmenu</string>\n  <string name=\"add_remove_from_recent_playlist\">Toevoegen/verwijderen uit recente afspeellijst</string>\n  <string name=\"hide_settings_section\">Sectie Instellingen verbergen (gevaarlijk!)</string>\n  <string name=\"volume\">Volume %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s sec</string>\n  <string name=\"audio_shift_sec\">%s sec</string>\n  <string name=\"ui_hide_timeout_sec\">%s sec</string>\n  <string name=\"screen_dimming_timeout_min\">%s min</string>\n  <string name=\"mark_all_channels_watched\">Markeer alle kanalen als bekeken</string>\n  <string name=\"move_section_up\">Sectie omhoog verplaatsen</string>\n  <string name=\"move_section_down\">Sectie omlaag verplaatsen</string>\n  <string name=\"player_buttons\">Spelerknoppen instellen</string>\n  <string name=\"action_playlist_add\">Toevoegen aan afspeellijst</string>\n  <string name=\"action_video_stats\">Ontwikkelaarsopties</string>\n  <string name=\"action_subscribe\">Abonneren</string>\n  <string name=\"action_channel\">Kanaal openen</string>\n  <string name=\"action_pip\">PIP</string>\n  <string name=\"action_video_info\">Videobeschrijving tonen</string>\n  <string name=\"action_screen_off\">Scherm uit</string>\n  <string name=\"action_sound_off\">Geluid uit</string>\n  <string name=\"action_playback_queue\">Afspeelwachtrij tonen</string>\n  <string name=\"action_video_speed\">Videosnelheid</string>\n  <string name=\"action_subtitles\">Ondertitels</string>\n  <string name=\"action_like\">Vind ik leuk</string>\n  <string name=\"action_dislike\">Vind ik niet leuk</string>\n  <string name=\"action_play_pause\">Speel/Pauzeren</string>\n  <string name=\"action_repeat_mode\">Afspeelmodus</string>\n  <string name=\"action_next\">Volgende video</string>\n  <string name=\"action_previous\">Vorige video</string>\n  <string name=\"action_high_quality\">Video kwaliteit</string>\n  <string name=\"content_block_no_skipping_mode\">Zonder overslaan modus</string>\n  <string name=\"content_block_action_type\">Kies actie</string>\n  <string name=\"content_block_action_none\">Niets doen</string>\n  <string name=\"content_block_action_only_skip\">Alleen overslaan</string>\n  <string name=\"content_block_action_toast\">Overslaan met melding</string>\n  <string name=\"content_block_action_dialog\">Bevestigingsdialoogvenster tonen</string>\n  <string name=\"content_block_filler\">Off-topic (vuller)</string>\n  <string name=\"keyboard_auto_show\">Toetsenbord automatisch weergeven</string>\n  <string name=\"cancel_dialog\">Annuleren</string>\n  <string name=\"msg_player_error_source2\">Videobron werkt niet of verkeerde tijd</string>\n  <string name=\"msg_player_error_video_source\">VIDEO URL werkt niet of incorrect apparaat tijd.\\nVaak een bug in de app.</string>\n  <string name=\"msg_player_error_audio_source\">AUDIO URL werkt niet of incorrect apparaat tijd.\\nVaak een bug in de app.</string>\n  <string name=\"msg_player_error_subtitle_source\">ONDERTITELING URL werkt niet of incorrect apparaat tijd.\\nVaak een bug in de app.</string>\n  <string name=\"hide_upcoming\">Aankomende verbergen van abonnementen</string>\n  <string name=\"hide_upcoming_channel\">Aankomende verbergen van kanaal</string>\n  <string name=\"hide_upcoming_home\">Aankomende verbergen van Home</string>\n  <string name=\"search_background_playback\">PIP tijdens het zoeken/browsen van een kanaal</string>\n  <string name=\"trending_row_name\">Trending</string>\n  <string name=\"player_seek_type\">Zoek gedrag</string>\n  <string name=\"player_seek_regular\">Normaal</string>\n  <string name=\"player_seek_confirmation_pause\">Met bevestiging (pauze tijdens zoeken)</string>\n  <string name=\"player_seek_confirmation_play\">Met bevestiging (spelen tijdens het zoeken)</string>\n  <string name=\"hide_shorts_from_home\">Shorts verbergen op Home</string>\n  <string name=\"finish_on_disconnect\">Voltooi de app nadat de telefoon/tablet is losgekoppeld</string>\n  <string name=\"update_error\">Update fout</string>\n  <string name=\"hide_shorts_from_history\">Shorts verbergen bij geschiedenis</string>\n  <string name=\"hide_shorts_from_trending\">Shorts verbergen bij Trending</string>\n  <string name=\"disable_screensaver\">Schermbeveiliging uitschakelen</string>\n  <string name=\"description_not_found\">Beschrijving niet gevonden</string>\n  <string name=\"proxy_port_hint\">Proxy poort</string>\n  <string name=\"proxy_host_hint\">Proxy-hostnaam of IP</string>\n  <string name=\"proxy_username_hint\">Proxy gebruikersnaam</string>\n  <string name=\"proxy_password_hint\">Proxy wachtwoord</string>\n  <string name=\"proxy_type\">Proxy Type</string>\n  <string name=\"enable_web_proxy\">Gebruik Web Proxy</string>\n  <string name=\"proxy_not_supported\">Gebruik Web Proxy (Heeft Android 4.4+ nodig)</string>\n  <string name=\"proxy_test_btn\">Test</string>\n  <string name=\"proxy_settings_title\">Proxy Server Instellingen</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">Test#%d: geannuleerd.</string>\n  <string name=\"proxy_type_invalid\">Proxy type niet gezet.</string>\n  <string name=\"proxy_host_invalid\">Proxy host niet gezet.</string>\n  <string name=\"proxy_port_invalid\">Ongeldige proxy poort, moet &gt; 0.</string>\n  <string name=\"proxy_credentials_invalid\">ongeldige gebruikersnaam of wachtwoord.</string>\n  <string name=\"proxy_test_aborted\">Test afgebroken, repareer eerst de proxy-instellingen.</string>\n  <string name=\"proxy_application_aborted\">Corrigeer eerst de proxy-instellingen.</string>\n  <string name=\"proxy_test_start\">Test#%d: %s …</string>\n  <string name=\"proxy_test_error\">Error#%d: %s</string>\n  <string name=\"proxy_test_status\">Status#%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Adres configuratiebestand (*.ovpn)</string>\n  <string name=\"enable_openvpn\">Gebruik OpenVPN</string>\n  <string name=\"openvpn_settings_title\">OpenVPN Settings</string>\n  <string name=\"openvpn_application_aborted\">Corrigeer eerst de OpenVPN-instellingen.</string>\n  <string name=\"openvpn_test_aborted\">Test afgebroken, corrigeer eerst de OpenVPN-instellingen.</string>\n  <string name=\"openvpn_address_invalid\">OpenVPN-configuratieadres niet ingesteld.</string>\n  <string name=\"internet_censorship\">Internetcensuur</string>\n  <string name=\"rename_section\">Hernoem sectie</string>\n  <string name=\"simple_edit_value_hint\">Nieuwe waarde</string>\n  <string name=\"seek_interval\">Interval zoeken</string>\n  <string name=\"seek_interval_sec\">%s sec</string>\n  <string name=\"subtitle_system\">Systeem style (Android settings &gt; Toegankelijkheid)</string>\n  <string name=\"subtitle_scale\">Ondertitel schaal</string>\n  <string name=\"audio_sync_fix_desc\">Een alternatieve manier om audio/video te synchroniseren. Het wordt als achterhaald beschouwd, maar kan in sommige gevallen helpen.</string>\n  <string name=\"audio_sync_fix\">Audiosynchronisatie oplossing</string>\n  <string name=\"ambilight_ratio_fix_desc\">Corrigeert afwezige biasverlichting. Corrigeert onjuiste beeldverhouding. Lost lege schermafbeeldingen op. Beïnvloedt de prestaties!</string>\n  <string name=\"ambilight_ratio_fix\">Ambilight/beeldverhouding/screenshots opgelost</string>\n  <string name=\"force_legacy_codecs_desc\">Verbetert de prestaties op low-end apparaten aanzienlijk. Maximale resolutie is 720p.</string>\n  <string name=\"force_legacy_codecs\">Legacy codecs afdwingen (720p)</string>\n  <string name=\"live_stream_fix_desc\">Waarschuwing, deze tweak schakelt het terugspoelen van streams uit. Verbetert de livestreamprestaties op low-end apparaten aanzienlijk. Maximale resolutie is 1080p.</string>\n  <string name=\"live_stream_fix\">Livestream-oplossing (1080p)</string>\n  <string name=\"live_stream_fix_4k\">Livestream-oplossing (4K)</string>\n  <string name=\"playback_notifications_fix_desc\">Verbergt meldingen over trackwijzigingen. Kan handig zijn op op AOSP gebaseerde firmwares.</string>\n  <string name=\"playback_notifications_fix\">Afspeelmeldingen uitschakelen</string>\n  <string name=\"amlogic_fix_desc\">Frame drop fix op Amlogic-gebaseerde apparaten.</string>\n  <string name=\"amlogic_fix\">Amlogic 1080p\\@60fps oplossing</string>\n  <string name=\"tunneled_video_playback_desc\">Getunnelde videoweergave belooft voordelen zoals betere audio/video-synchronisatie (AV-synchronisatie) en soepeler afspelen. Vereist Android 5+</string>\n  <string name=\"tunneled_video_playback\">Getunnelde videoweergave (Android 5+)</string>\n  <string name=\"master_volume\">Hoofd volume</string>\n  <string name=\"volume_limit\">Volume limiet</string>\n  <string name=\"player_volume\">Volume</string>\n  <string name=\"play_video\">Afspelen</string>\n  <string name=\"remember_position_of_short_videos\">Onthoud positie van korte video\\'s (minder dan 5 min)</string>\n  <string name=\"remember_position_of_live_videos\">Onthoud livestream positie</string>\n  <string name=\"player_show_tooltips\">Knopinfo weergeven</string>\n  <string name=\"action_like_unset\">Vind ik leuk is uitgeschakeld</string>\n  <string name=\"action_dislike_unset\">Vind ik niet leuk is uitgeschakeld</string>\n  <string name=\"various_buttons\">Knoppen bovenaan het hoofdvenster</string>\n  <string name=\"not_compatible_with\">Geselecteerde optie is niet compatibel met</string>\n  <string name=\"subtitle_position\">Ondertitel positie</string>\n  <string name=\"pressing_home\">door op HOME te drukken</string>\n  <string name=\"pressing_home_back\">door op HOME of BACK te drukken</string>\n  <string name=\"player_number_key_seek\">Zoeken met cijfertoetsen</string>\n  <string name=\"save_remove_playlist\">Afspeellijst toevoegen/verwijderen uit het gedeelte Afspeellijsten</string>\n  <string name=\"save_playlist\">Afspeellijst toevoegen aan Afspeellijsten gedeelte</string>\n  <string name=\"remove_playlist\">Afspeellijst permanent verwijderen uit het gedeelte Afspeellijsten</string>\n  <string name=\"removed_from_playlists\">Verwijderd uit het gedeelte Afspeellijsten</string>\n  <string name=\"saved_to_playlists\">Toegevoegd aan het gedeelte Afspeellijsten</string>\n  <string name=\"create_playlist\">Maak afspeellijst</string>\n  <string name=\"add_video_to_new_playlist\">Toevoegen aan nieuwe afspeellijst</string>\n  <string name=\"cant_delete_empty_playlist\">Kan lege afspeellijst niet verwijderen</string>\n  <string name=\"playlist\">Afspeellijst</string>\n  <string name=\"rename_playlist\">Afspeellijst hernoemen</string>\n  <string name=\"cant_rename_empty_playlist\">Kan de naam van lege afspeellijst niet wijzigen</string>\n  <string name=\"cant_rename_foreign_playlist\">Kan buitenlandse afspeellijst niet hernoemen</string>\n  <string name=\"cant_save_playlist\">Deze afspeellijst kan niet worden toegevoegd</string>\n  <string name=\"enter_value\">Voer een niet-lege waarde in</string>\n  <string name=\"dialog_add_remove_from\">Toevoegen/verwijderen uit %s</string>\n  <string name=\"lb_playback_controls_skip_next\">Volgende overslaan</string>\n  <string name=\"lb_playback_controls_skip_previous\">Vorige/terugspoelen om positie te starten</string>\n  <string name=\"alt_presets_behavior\">Alt-presets  behavior (bandbreedte beperken)</string>\n  <string name=\"alt_presets_behavior_desc\">De app zal proberen de bandbreedte te behouden die overeenkomt met de geselecteerde preset in plaats van te matchen tussen resolutie, fps en codec.</string>\n  <string name=\"sleep_timer\">Slaaptimer (afstandsbediening uur inactief)</string>\n  <string name=\"sleep_timer_desc\">Pauzeer het afspelen als de gebruiker de afstandsbediening een uur lang niet heeft gebruikt</string>\n  <string name=\"disable_vsync\"></string>\n  <string name=\"disable_vsync_desc\">Met deze optie wordt het uitlijnen van frames met het verticale synchronisatiesignaal van het scherm uitgeschakeld. Dit kan de prestaties op low-end apparaten verbeteren door CPU-bronnen vrij te maken.</string>\n  <string name=\"skip_codec_profile_check\">Controle op codec-profielniveau overslaan</string>\n  <string name=\"skip_codec_profile_check_desc\">Controleer de codec-ondersteuning niet bij het starten van een video.  Zou handig kunnen zijn bij buggy-firmware.</string>\n  <string name=\"force_sw_codec\">Forceer SW-videodecoder</string>\n  <string name=\"force_sw_codec_desc\">Kan bijna elke video afspelen, maar de prestaties zijn erg slecht.</string>\n  <string name=\"playlist_order\">Sorteer afspeellijst</string>\n  <string name=\"playlist_order_added_date_newer_first\">Datum toegevoegd (nieuwere eerst)</string>\n  <string name=\"playlist_order_added_date_older_first\">Datum toegevoegd (ouder eerst)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Publicatiedatum (nieuwere eerst)</string>\n  <string name=\"playlist_order_published_date_older_first\">Publicatiedatum (ouder eerst)</string>\n  <string name=\"playlist_order_popularity\">Populariteit</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">Kan dit niet doen voor buitenlandse afspeellijsten</string>\n  <string name=\"owned_playlist_warning\">Fout. Deze actie kan alleen worden toegepast op afspeellijsten die eigendom zijn van jou</string>\n  <string name=\"content_block_alt_server\">Gebruik alternatieve server</string>\n  <string name=\"content_block_alt_server_desc\">Schakel deze optie in als SponsorBlock om verschillende redenen weigert te werken.</string>\n  <string name=\"unset_stream_reminder\">Streamherinnering uitschakelen</string>\n  <string name=\"set_stream_reminder\">Streamherinnering instellen</string>\n  <string name=\"playback_starts_shortly\">Het afspelen start automatisch wanneer de stream klaar is</string>\n  <string name=\"starting_stream\">De stream starten…</string>\n  <string name=\"action_playlist_remove\">Verwijderen uit afspeellijst</string>\n  <string name=\"signin_view_title\">Gebruikerscode wordt geladen...</string>\n  <string name=\"signin_view_description\">Voer deze code in op pagina %s\\n om in te loggen. Opmerking: Werkt alleen met Firefox- of Chrome-browsers.</string>\n  <string name=\"signin_view_action_text\">Gedaan</string>\n  <string name=\"require_checked\">Vereist \\\"%s</string>\n  <string name=\"msg_press_again_to_exit\">Druk nogmaals om af te sluiten</string>\n  <string name=\"player_remaining_time\">Resterend %s</string>\n  <string name=\"player_ending_time\">Eindigt om %s</string>\n  <string name=\"badge_new_content\">NIEUWE INHOUD</string>\n  <string name=\"badge_live\">LIVE</string>\n  <string name=\"add_device_view_description\">Om het apparaat te koppelen, voert u deze code in op de YouTube-app van uw apparaat in het gedeelte Instellingen/Op tv bekijken. Opmerking: Werkt alleen met Gboard-toetsenbord.</string>\n  <string name=\"playback_controls_repeat_pause\">Herhaal Pauze</string>\n  <string name=\"playback_controls_repeat_list\">Herhaal lijst</string>\n  <string name=\"action_subscribe_off\">Abonneren uit</string>\n  <string name=\"action_subscribe_on\">Abonneren aan</string>\n  <string name=\"skip_24_rate\">Sla 24fps-formaten over (real cinema fix\\?)</string>\n  <string name=\"skip_shorts\">Shorts overslaan</string>\n  <string name=\"player_disable_suggestions\">Suggesties uitschakelen</string>\n  <string name=\"feedback\">Feedback</string>\n  <string name=\"sources\">Bronnen</string>\n  <string name=\"releases\">Vrijgaven</string>\n  <string name=\"prefer_avc_over_vp9\">Codec-selectie: geef de voorkeur aan avc boven vp9</string>\n  <string name=\"open_chat\">Chat/Opmerkingen</string>\n  <string name=\"open_comments\">Open opmerkingen</string>\n  <string name=\"place_chat_left\">Chat aan de linkerkant plaatsen</string>\n  <string name=\"place_comments_left\">Opmerkingen aan de linkerkant plaatsen</string>\n  <string name=\"use_alt_speech_recognizer\">Alt-spraakherkenning</string>\n  <string name=\"time_format\">Tijdformaat</string>\n  <string name=\"time_format_24\">24 uur</string>\n  <string name=\"time_format_12\">12 uur (AM/PM)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Zeer bug gevoelig. Gebruik alleen als je problemen hebt met de standaard spraakherkenner.</string>\n  <string name=\"speech_recognizer\">Spraakherkenner</string>\n  <string name=\"speech_recognizer_system\">Systeem (voorkeur)</string>\n  <string name=\"speech_recognizer_external_1\">Extern 1 (Zeer bug gevoelig, om hier gebruik van te maken moet je de toegang naar de microphone uitzetten)</string>\n  <string name=\"speech_recognizer_external_2\">Extern 2 (Zeer bug gevoelig)</string>\n  <string name=\"real_channel_icon\">Toon pictogram op kanaalknop</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Geel op semitransparante achtergrond</string>\n  <string name=\"subtitle_yellow_black\">Geel op zwarte achtergrond</string>\n  <string name=\"player_pixel_ratio\">Pixelverhouding</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Donkergrijs (monochroom)</string>\n  <string name=\"disable_mic_permission\">Schakel de microfoontoegang voor de app uit zodat deze herkenner correct werkt.</string>\n  <string name=\"repeat_mode_shuffle\">Shuffle een willekeurige afspeellijst</string>\n  <string name=\"chat_left\">Links</string>\n  <string name=\"chat_right\">Rechts</string>\n  <string name=\"card_real_thumbnails\">Vervang miniaturen door een frame uit de video</string>\n  <string name=\"card_content\">Waar miniaturen van gebruiken</string>\n  <string name=\"thumb_quality_default\">Standaard</string>\n  <string name=\"thumb_quality_start\">Begin van de video</string>\n  <string name=\"thumb_quality_middle\">Midden van de video</string>\n  <string name=\"thumb_quality_end\">Einde van de video</string>\n  <string name=\"content_block_status\">Controleer SponsorBlock status</string>\n  <string name=\"player_show_quality_info_bitrate\">Bitrate toevoegen aan de kwaliteit informatie</string>\n  <string name=\"player_speed_button_old_behavior\">Oude gedrag van de snelheidsknop terugzetten</string>\n  <string name=\"protect_settings_with_password\">Bescherm alle instellingen met een wachtwoord</string>\n  <string name=\"enter_settings_password\">Voer het instellingenwachtwoord in</string>\n  <string name=\"child_mode\">Kinder modus</string>\n  <string name=\"child_mode_desc\">In deze modus kan de gebruiker de zoekfunctie niet gebruiken of voorgestelde inhoud bekijken.  De instellingen staan ​​onder het wachtwoord.</string>\n  <string name=\"lost_setting_warning\">De app-instellingen worden gewijzigd.  Zorg ervoor dat u een back-up van de instellingen hebt gemaakt.</string>\n  <string name=\"player_button_long_click\">Een korte klik op de knop naar snelle actie en een lange naar instellingen dialoog</string>\n  <string name=\"pause_history\">Geschiedenis onderbreken</string>\n  <string name=\"resume_history\">Geschiedenis hervatten</string>\n  <string name=\"clear_history\">Geschiedenis wissen</string>\n  <string name=\"chapters\">Hoofdstukken</string>\n  <string name=\"card_multiline_subtitle\">Meerregelige ondertitels</string>\n  <string name=\"auto_frame_rate_modes\">Ondersteunde video-modi</string>\n  <string name=\"loading\">Bezig met laden…</string>\n  <string name=\"hide_streams\">Verberg streams van abonnementen</string>\n  <string name=\"video_rotate\">Het beeld draaien</string>\n  <string name=\"trending_searches\">Populaire zoekopdrachten</string>\n  <string name=\"pressing_back\">door op TERUG te drukken</string>\n  <string name=\"video_duration_over_20\">Over 20 minuten</string>\n  <string name=\"content_type\">Soort</string>\n  <string name=\"content_type_any\">Elk</string>\n  <string name=\"content_type_video\">Video</string>\n  <string name=\"content_type_channel\">Kanaal</string>\n  <string name=\"content_type_playlist\">Afspeellijst</string>\n  <string name=\"content_type_movie\">Film</string>\n  <string name=\"video_features\">Functies</string>\n  <string name=\"video_feature_any\">Elk</string>\n  <string name=\"video_feature_live\">Live</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"video_duration_any\">Elk</string>\n  <string name=\"video_duration\">Duur</string>\n  <string name=\"video_duration_under_4\">Onder de 4 minuten</string>\n  <string name=\"video_duration_between_4_20\">4-20 minuten</string>\n  <string name=\"search_sorting\">Sorteer op</string>\n  <string name=\"sort_by_relevance\">Relevantie</string>\n  <string name=\"sort_by_views\">Aantal weergaven</string>\n  <string name=\"sort_by_date\">Upload datum</string>\n  <string name=\"sort_by_rating\">Beoordeling</string>\n  <string name=\"clear_search_history\">Verwijder zoekgeschiedenis</string>\n  <string name=\"not_supported_by_device\">Het apparaat ondersteunt deze functie niet</string>\n  <string name=\"live_stream_fix_4k_desc\">Waarschuwing, deze tweak schakelt het terugspoelen van streams uit. Verbetert de livestreamprestaties aanzienlijk. Maximale resolutie is 4K.</string>\n  <string name=\"hide_shorts_from_search\">Shorts verbergen in zoekresultaten</string>\n  <string name=\"suggestions\">Suggesties</string>\n  <string name=\"speech_engine\">Stemzoekmachine</string>\n  <string name=\"dearrow_status\">Controleer de DeArrow-serverstatus</string>\n  <string name=\"disable_history\">Geschiedenis uitschakelen</string>\n  <string name=\"enable_history\">Geschiedenis inschakelen</string>\n  <string name=\"remove_from_subscriptions\">Verbergen</string>\n  <string name=\"player_long_speed_list\">Lange snelheidslijst</string>\n  <string name=\"player_extra_long_speed_list\">Extra lange snelheidslijst</string>\n  <string name=\"alt_app_icon\">Alternatief app-pictogram (herstart vereist)</string>\n  <string name=\"cronet_desc\">Cronet is de Chromium-netwerkstack. Cronet kan de latentie verminderen en de netwerkprestaties verbeteren, wat kan helpen bij bufferingsproblemen.</string>\n  <string name=\"unlock_all_formats\">Ontgrendel alle videoformaten</string>\n  <string name=\"okhttp_desc\">Deze netwerkengine is het langzaamst, maar is wel stabiel.</string>\n  <string name=\"select_channel_section\">Open de overeenkomstige sectie wanneer de app wordt gestart vanuit ATV-kanalen</string>\n  <string name=\"enable_voice_search_desc\">De officiële app wordt vervangen door een zogenaamde \\\"bridge\\\". Een bridge helpt om wereldwijde zoekopdrachten over te brengen naar onze app.</string>\n  <string name=\"enable_master_password\">Hoofdwachtwoord inschakelen</string>\n  <string name=\"enter_master_password\">Voer het hoofdwachtwoord in</string>\n  <string name=\"disable_stream_buffer\">Buffer op streams uitschakelen</string>\n  <string name=\"disable_stream_buffer_desc\">Oplossing voor situaties waarin de stream te ver achterloopt. Let op: de stream kan gaan haperen.</string>\n  <string name=\"sony_frame_drop_fix\">Sony frame drop-oplossing #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">Verhelp vertragingen op Sony TV en enkele andere apparaten. Let op: mogelijke problemen met audiosynchronisatie.</string>\n  <string name=\"audio_language\">Audiotaal</string>\n  <string name=\"old_home_look\">Oude look van de Home-sectie</string>\n  <string name=\"old_channel_look\">Oude look van de Kanaalpagina</string>\n  <string name=\"hide_shorts_everywhere\">Shorts overal verbergen</string>\n  <string name=\"update_found\">Update</string>\n  <string name=\"volume_boost_warning\">Elke instelling boven de limiet kan potentieel schade aan de luidsprekers veroorzaken</string>\n  <string name=\"play_video_incognito\">Incognito afspelen</string>\n  <string name=\"header_kids_home\">Kinderen</string>\n  <string name=\"player_section_playlist\">Gebruik de inhoud van de huidige sectie als afspeellijst</string>\n  <string name=\"header_trending\">Trending</string>\n  <string name=\"screensaver\">Schermbeveiliging</string>\n  <string name=\"player_screen_off_timeout\">Scherm uit time-out</string>\n  <string name=\"old_update_notifications\">Oude look van de updatemeldingen</string>\n  <string name=\"dialog_notification\">Meld updates met een pop-updialoog</string>\n  <string name=\"mark_as_watched\">Markeren als bekeken</string>\n  <string name=\"player_ui_animations\">Animaties van het bedieningspaneel inschakelen</string>\n  <string name=\"player_likes_count\">Aantal likes/dislikes weergeven</string>\n  <string name=\"sorting_alphabetically2\">Alfabetisch (snelle, geanimeerde previews)</string>\n  <string name=\"sorting_default\">Standaard (snelle, geanimeerde previews)</string>\n  <string name=\"content_block_exclude_channel\">Sluit dit kanaal uit van SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">Stop met het uitsluiten van dit kanaal van SponsorBlock</string>\n  <string name=\"player_chapter_notification\">Ga snel door de hoofdstukken heen met behulp van de pop-upmelding</string>\n  <string name=\"subtitle_remember\">Onthoud ingeschakelde ondertitels per kanaal</string>\n  <string name=\"amazon_frame_drop_fix\">Amazon frame drop-oplossing #2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">Bedoeld voor Amazon Stick-apparaten. Werkt mogelijk ook op andere apparaten.</string>\n  <string name=\"default_stack_desc\">Ingebouwde netwerkengine. Deze engine kan in bepaalde situaties een betere stabiliteit hebben.</string>\n  <string name=\"item_postion\">Positie van</string>\n  <string name=\"player_screen_off_dimming\">Scherm uit dimmen hoeveelheid</string>\n  <string name=\"screensaver_timout\">Time-out van schermbeveiliging</string>\n  <string name=\"screensaver_dimming\">Hoeveelheid schermbeveiliging dimmen</string>\n  <string name=\"player_ui_on_next\">Speler-UI weergeven bij overschakelen naar de volgende video</string>\n  <string name=\"autogenerated\">automatisch gegenereerd</string>\n  <string name=\"player_auto_volume\">Automatische volumeregeling</string>\n  <string name=\"header_shorts\">Shorts</string>\n  <string name=\"player_global_focus\">Naadloze navigatie tussen spelerknoprijen</string>\n  <string name=\"auto_history\">Automatisch (gebruik accountinstellingen)</string>\n  <string name=\"remember_position_subscriptions\">Onthoud de laatst bekeken positie in Abonnementen</string>\n  <string name=\"remember_position_pinned\">Onthoud de laatst bekeken positie in de vastgezette afspeellijsten</string>\n  <string name=\"msg_player_unknown_error\">Onbekende fout</string>\n  <string name=\"unknown_source_error\">Onbekende bronfout</string>\n  <string name=\"unknown_renderer_error\">Onbekende rendererfout</string>\n  <string name=\"header_notifications\">Meldingen</string>\n  <string name=\"disable_search_history\">Zoekgeschiedenis uitschakelen</string>\n  <string name=\"unlock_high_bitrate_formats\">Ontgrendel 1080p vp9-formaten met hoge bitsnelheid</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Ontgrendel MP4A-formaten met hoge bitsnelheid</string>\n  <string name=\"color_scheme_blue\">Blauw</string>\n  <string name=\"color_scheme_dark_blue\">Donkerblauw</string>\n  <string name=\"player_speed_per_channel\">Per kanaal</string>\n  <string name=\"disable_popular_searches\">Populaire zoekopdrachten uitschakelen</string>\n  <string name=\"multi_profiles\">Gebruik aparte instellingen voor elk account</string>\n  <string name=\"protect_account_with_password\">Bescherm dit account met een wachtwoord</string>\n  <string name=\"enter_account_password\">Voer het wachtwoord van uw account in</string>\n  <string name=\"show_connect_messages\">Toon verbindingsberichten</string>\n  <string name=\"prefer_avc_over_vp9_desc\">WAARSCHUWING: maximaal 1080p</string>\n  <string name=\"play_next\">Volgende afspelen</string>\n  <string name=\"hide_watched_from_subscriptions\">Verberg bekeken video\\'s van Abonnementen</string>\n  <string name=\"hide_watched_from_notifications\">Verberg bekeken video\\'s van Meldingen</string>\n  <string name=\"hide_unwanted_content\">Ongewenste inhoud verbergen</string>\n  <string name=\"hide_watched_from_home\">Verberg bekeken video\\'s van Home</string>\n  <string name=\"hide_watched_from_watch_later\">Verberg bekeken video\\'s van de afspeellijst Later bekijken</string>\n  <string name=\"remote_control_permission\">Achtergrond Afstandsbediening vereist Overlay-machtiging</string>\n  <string name=\"login_from_browser\">Inloggen via browser</string>\n  <string name=\"disable_remote_history\">Geschiedenis uitschakelen bij gebruik van afstandsbediening</string>\n  <string name=\"keyboard_fix\">Voorkom dat de OK-knop het toetsenbord opent (G20\\'s en andere)</string>\n  <string name=\"nothing_found\">Niets gevonden</string>\n  <string name=\"auto_frame_rate_desc\">Met deze optie verwijdert u haperingen in beeld waarin de camera snel beweegt, bijvoorbeeld bij het streamen van sportwedstrijden.</string>\n  <string name=\"channel_filter_hint\">Kanalen filteren</string>\n  <string name=\"player_quick_shorts_skip\">Skip Shorts met linker-/rechterknop</string>\n  <string name=\"player_quick_skip_videos\">Sla normale video\\'s over met de linker-/rechterknoppen</string>\n  <string name=\"channels_filter\">Filterkanalenveld weergeven in het gedeelte Kanalen</string>\n  <string name=\"channel_search_bar\">Zoekbalk op de kanaalpagina</string>\n  <string name=\"header_sports\">Sport</string>\n  <string name=\"keep_finished_activities\">Houd voltooide activiteiten bij</string>\n  <string name=\"disable_channels_service\">Kanalen-service uitschakelen</string>\n  <string name=\"replace_titles\">Titels vervangen</string>\n  <string name=\"enable\">Inschakelen</string>\n  <string name=\"more_info\">Meer info</string>\n  <string name=\"about_sponsorblock\">Over SponsorBlock</string>\n  <string name=\"about_dearrow\">Over DeArrow</string>\n  <string name=\"replace_thumbnails\">Miniaturen vervangen</string>\n  <string name=\"crowdsourced_thumbnails\">Miniaturen van Crowdsourced</string>\n  <string name=\"crowdsoursed_titles\">Crowdsourced titels</string>\n  <string name=\"dearrow_not_submitted_thumbs\">Bron van de miniatuur (als er geen DeArrow-inzending is)</string>\n  <string name=\"pitch_effect\">Toonhoogte-effect</string>\n  <string name=\"fullscreen_mode\">Volledig scherm (zonder systeembalken)</string>\n  <string name=\"player_only_mode\">Alleen de speler weergeven als de video buiten de app wordt geopend</string>\n  <string name=\"pinned_channel_rows\">Toon vastgezette kanalen als rijen</string>\n  <string name=\"prefer_ipv4\">Geef de voorkeur aan IPv4 DNS</string>\n  <string name=\"long_press_for_settings\">LANG INDRUKKEN VOOR INSTELLINGEN</string>\n  <string name=\"long_press_for_options\">LANG DRUKKEN VOOR OPTIES</string>\n  <string name=\"device_specific_backup\">Alleen back-up voor dit apparaat</string>\n  <string name=\"local_backup\">Lokale back-up</string>\n  <string name=\"auto_backup\">Automatische back-up (één keer per dag)</string>\n  <string name=\"repeat_mode_reverse_list\">Speel de afspeellijst of kanaalvideo\\'s in omgekeerde volgorde af</string>\n  <string name=\"calm_msg\">We lossen het probleem op. Controleer van tijd tot tijd op updates</string>\n  <string name=\"without_picture\">Zonder beeld</string>\n  <string name=\"video_disabled\">Video uitgeschakeld</string>\n  <string name=\"applying_fix\">Sluit de speler niet. De oplossing toepassen...</string>\n  <string name=\"hide_mixes\">Verberg mixen</string>\n  <string name=\"player_global_focus_desc\">Deze functie heeft invloed op welke spelerknop de focus krijgt bij het navigeren</string>\n  <string name=\"disable_network_error_fixing\">Automatisch netwerkfoutherstel uitschakelen</string>\n  <string name=\"disable_network_error_fixing_desc\">U moet deze optie waarschijnlijk inschakelen als u een VPN gebruikt</string>\n  <string name=\"recommended\">Aanbevolen</string>\n  <string name=\"add_to_subscriptions_group\">Toevoegen/verwijderen uit abonnementsgroep</string>\n  <string name=\"new_subscriptions_group\">Nieuwe groep</string>\n  <string name=\"rename_group\">De naam van de abonnementsgroep wijzigen</string>\n  <string name=\"screen_dimming_amount\">Hoeveelheid schermverduistering</string>\n  <string name=\"screen_dimming_timeout\">Time-out voor schermdimmen</string>\n  <string name=\"playlists_rows\">Sectie Afspeellijsten weergeven als rijen</string>\n  <string name=\"unlock_all_formats_desc\">Op sommige apparaten meldt de firmware ten onrechte dat bepaalde formaten niet worden ondersteund, ook al zijn ze dat wel.\\n(Bijvoorbeeld: 1080p smart-tv\\'s melden 4k vaak als niet ondersteund.)</string>\n  <string name=\"prefer_ipv4_desc\">Kan situaties oplossen waarin de app helemaal niet werkt.\\nLet op. Kan vastlopen en crashen veroorzaken (vooral op Android 8-apparaten of Dune HD)</string>\n  <string name=\"player_loop_shorts\">Short herhaald afspelen ipv doorlopend</string>\n  <string name=\"import_subscriptions_group\">Importeren</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">Ondertiteling uitschakelen</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">Ondertiteling inschakelen</string>\n  <string name=\"my_videos\">Mijn video\\'s</string>\n  <string name=\"player_audio_focus\">Audiofocus (pauzeren als andere spelers gedetecteerd worden)</string>\n  <string name=\"paid_content_notification\">Melding betaalde content</string>\n  <string name=\"you_liked\">Je vond het leuk</string>\n  <string name=\"premium_users_only\">Alleen voor Premium-gebruikers. Oplossing voor onvolledige lijst met videoformaten.</string>\n  <string name=\"playback_buffering_fix\">Oplossing voor buffering bij afspelen</string>\n  <string name=\"oculus_quest_fix\">Oculus Quest-oplossing</string>\n  <string name=\"card_preview_muted\">Video zonder geluid</string>\n  <string name=\"card_preview_full\">Video met geluid</string>\n  <string name=\"card_preview\">Kaartvoorbeeld</string>\n  <string name=\"card_unlocalized_titles\">Niet-gelokaliseerde videotitels</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">Pas de video niet aan om in de dialoog te passen</string>\n  <string name=\"typing_corrections\">Automatische correctie uitschakelen tijdens het typen van tekst</string>\n  <string name=\"suggestions_horizontally_scrolled\">Horizontaal gescrolde suggesties</string>\n  <string name=\"stable_restore\">Herstel de stable build (alle gegevens gaan verloren)</string>\n  <string name=\"dialog_block_channel\">Blokkeer kanaal</string>\n  <string name=\"dialog_unblock_channel\">Deblokkeer kanaal</string>\n  <string name=\"confirm_block_channel\">Verberg alle inhoud van %s\\?</string>\n  <string name=\"channel_blocked\">Kanaal geblokkeerd</string>\n  <string name=\"channel_unblocked\">Kanaal gedeblokkeerd</string>\n  <string name=\"header_blocked_channels\">Geblokkeerde kanalen</string>\n  <string name=\"msg_no_blocked_channels\">Geen geblokkeerde kanalen</string>\n  <string name=\"auto_backup_category\">Automatische backup</string>\n  <string name=\"once_a_day\">Eenmaal per dag</string>\n  <string name=\"once_a_week\">Eenmaal per week</string>\n  <string name=\"once_a_month\">Eenmaal per maand</string>\n  <string name=\"action_debug_info\">Foutopsporingsinformatie</string>\n  <string name=\"player_time_stretching\">Audio-tijdverlenging</string>\n  <string name=\"player_time_stretching_desc\">Behoudt de natuurlijke stem bij het veranderen van de snelheid. Kan de prestaties op sommige apparaten aanzienlijk verminderen.</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-pl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Główna</string>\n  <string name=\"title_search\">Szukaj</string>\n  <string name=\"header_subscriptions\">Subskrypcje</string>\n  <string name=\"header_history\">Historia</string>\n  <string name=\"header_music\">Muzyka</string>\n  <string name=\"header_news\">Aktualności</string>\n  <string name=\"header_gaming\">Gry</string>\n  <string name=\"header_playlists\">Listy odtwarzania</string>\n  <string name=\"header_settings\">Ustawienia</string>\n  <string name=\"header_channels\">Kanały</string>\n  <string name=\"subscriptions_signin_title\">Zobacz najnowsze z ulubionych Kanałów</string>\n  <string name=\"subscriptions_signin_subtitle\">Zaloguj się, aby zobaczyć swoje Subskrypcje</string>\n  <string name=\"subscriptions_signin_button_text\">ZALOGUJ SIĘ</string>\n  <string name=\"library_signin_to_show_more\">Oglądaj wideo, które polubiłeś, zapisałeś lub subskrybujesz</string>\n  <string name=\"library_signin_subtitle\">Zaloguj się, aby zobaczyć swoją bibliotekę</string>\n  <string name=\"action_signin\">ZALOGUJ SIĘ</string>\n  <string name=\"title_video_formats\">Formaty wideo</string>\n  <string name=\"title_audio_formats\">Formaty dźwięku</string>\n  <string name=\"subtitle_category_title\">Napisy</string>\n  <string name=\"playback_queue_category_title\">Kolejka odtwarzania</string>\n  <string name=\"video_max_quality\">Automatycznie (maks. jakość)</string>\n  <string name=\"audio_max_quality\">Automatycznie (maks. jakość)</string>\n  <string name=\"subtitles_disabled\">Napisy wyłączone</string>\n  <string name=\"auto_frame_rate\">Auto Frame Rate (prędkość klatkowa)</string>\n  <string name=\"frame_rate_correction\">Włącz korekty prędkości klatkowej:\\n%s</string>\n  <string name=\"category_background_playback\">Odtwarzanie w tle</string>\n  <string name=\"not_implemented\">Nie zaimplementowano</string>\n  <string name=\"option_background_playback_off\">Wyłączone</string>\n  <string name=\"option_background_playback_pip\">Obraz w obrazie (PiP)</string>\n  <string name=\"option_background_playback_only_audio\">Tylko dźwięk</string>\n  <string name=\"playback_settings\">Ustawienia jakości odtwarzania</string>\n  <string name=\"video_speed\">Prędkość wideo</string>\n  <string name=\"resolution_switch\">Przełącz rozdzielczość</string>\n  <string name=\"video_buffer\">Bufor wideo</string>\n  <string name=\"video_buffer_size_none\">Brak</string>\n  <string name=\"video_buffer_size_low\">Mały</string>\n  <string name=\"video_buffer_size_med\">Średni</string>\n  <string name=\"video_buffer_size_high\">Duży</string>\n  <string name=\"update_changelog\">Lista zmian</string>\n  <string name=\"install_update\">Zainstaluj aktualizację</string>\n  <string name=\"section_is_empty\">Niestety, nic tu nie ma.</string>\n  <string name=\"dialog_add_to_playlist\">Dodaj/Usuń z listy odtwarzania</string>\n  <string name=\"msg_signed_users_only\">Tylko zalogowani użytkownicy</string>\n  <string name=\"msg_cant_load_content\">Nie można załadować zawartości.\\n1) Włącz historię oglądania.\\n2) Sprawdź datę i godzinę na urządzeniu.\\n3) Sprawdź połączenie sieciowe.\\n4) Sprawdź ustawienia proxy.\\n5) Spróbuj zalogować się na swoje konto.\\n6) Może nic tu nie ma.</string>\n  <string name=\"title_video_presets\">Ustawienia wideo</string>\n  <string name=\"settings_accounts\">Konta</string>\n  <string name=\"settings_left_panel\">Edytuj kategorie</string>\n  <string name=\"settings_themes\">Motywy</string>\n  <string name=\"settings_other\">Inny</string>\n  <string name=\"settings_player\">Odtwarzacz wideo</string>\n  <string name=\"settings_language\">Język</string>\n  <string name=\"settings_linked_devices\">Połączone urządzenia</string>\n  <string name=\"settings_about\">Informacje</string>\n  <string name=\"dialog_account_list\">Wybierz konto</string>\n  <string name=\"dialog_account_none\">Brak</string>\n  <string name=\"dialog_remove_account\">Wyloguj się</string>\n  <string name=\"dialog_add_account\">Zaloguj się</string>\n  <string name=\"default_lang\">Domyślny</string>\n  <string name=\"dialog_select_language\">Język</string>\n  <string name=\"subtitle_default\">Domyślne</string>\n  <string name=\"subtitle_white_semi_transparent\">Białe na półprzezroczystym tle</string>\n  <string name=\"subtitle_style\">Styl napisów</string>\n  <string name=\"subtitle_language\">Język napisów</string>\n  <string name=\"action_search\">Szukaj</string>\n  <string name=\"settings_main_ui\">Interfejs</string>\n  <string name=\"dialog_main_ui\">Interfejs użytkownika</string>\n  <string name=\"card_animated_previews\">Animowany podgląd</string>\n  <string name=\"web_site\">Strona internetowa</string>\n  <string name=\"donation\">Darowizna na dalszy rozwój</string>\n  <string name=\"dialog_about\">O…</string>\n  <string name=\"dialog_player_ui\">Odtwarzacz wideo</string>\n  <string name=\"player_show_ui_on_pause\">Pokaż interfejs podczas pauzy</string>\n  <string name=\"player_pause_on_ok\">Przycisk OK wstrzymuje odtwarzanie</string>\n  <string name=\"player_ok_button_behavior\">Zachowanie przycisku OK</string>\n  <string name=\"player_only_ui\">Tylko interfejs</string>\n  <string name=\"player_ui_and_pause\">Interfejs i pauza</string>\n  <string name=\"player_only_pause\">Tylko pauza</string>\n  <string name=\"check_for_updates\">Sprawdź aktualizacje</string>\n  <string name=\"update_not_found\">Używasz najnowszej wersji</string>\n  <string name=\"update_in_progress\">Zaczekaj…</string>\n  <string name=\"player_ui_hide_behavior\">Automatyczne ukrywanie interfejsu</string>\n  <string name=\"option_never\">Nigdy</string>\n  <string name=\"side_panel_sections\">Widoczne sekcje</string>\n  <string name=\"boot_to_section\">Sekcja startowa</string>\n  <string name=\"large_ui\">Duży interfejs</string>\n  <string name=\"video_grid_scale\">Skalowanie siatki wideo</string>\n  <string name=\"scale_ui\">Skalowanie interfejsu</string>\n  <string name=\"color_scheme\">Schemat kolorów</string>\n  <string name=\"color_scheme_default\">Domyślny</string>\n  <string name=\"color_scheme_red_grey\">Czerwono-szary</string>\n  <string name=\"color_scheme_red\">Czerwony</string>\n  <string name=\"color_scheme_dark_grey\">Ciemnoszary</string>\n  <string name=\"disable_update_check\">Wyłącz sprawdzanie aktualizacji</string>\n  <string name=\"show_again\">Pokaż ponownie</string>\n  <string name=\"check_updates_auto\">Powiadamiaj o nowszej wersji</string>\n  <string name=\"select_account_on_boot\">Wybieraj konto przy starcie</string>\n  <string name=\"player_other\">Pozostałe</string>\n  <string name=\"player_full_date\">Dokładna data w opisie</string>\n  <string name=\"open_channel\">Otwórz Kanał</string>\n  <string name=\"open_playlist\">Otwórz listę odtwarzania</string>\n  <string name=\"not_interested\">Nie interesuje mnie</string>\n  <string name=\"not_recommend_channel\">Nie rekomenduj Kanału</string>\n  <string name=\"you_wont_see_this_video\">Wideo zostało usunięte z rekomendowanych</string>\n  <string name=\"you_wont_see_this_channel\">Nie zobaczysz już treści z tego Kanału w rekomendowanych</string>\n  <string name=\"settings_search\">Wyszukiwanie</string>\n  <string name=\"dialog_search\">Szukaj</string>\n  <string name=\"instant_voice_search\">Szybkie wyszukiwanie głosowe</string>\n  <string name=\"option_background_playback_behind\">Odtwarzaj w tle</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">Informacje o kolejnym wideo nie zostały jeszcze załadowane</string>\n  <string name=\"card_multiline_title\">Tytuły wielowierszowe</string>\n  <string name=\"cards_style\">Styl kart</string>\n  <string name=\"repeat_mode_all\">Odtwarzaj wszystkie wideo jedno po drugim</string>\n  <string name=\"repeat_mode_one\">Powtórz bieżące wideo</string>\n  <string name=\"repeat_mode_pause\">Wstrzymaj odtwarzanie po każdym wideo</string>\n  <string name=\"repeat_mode_pause_alt\">Wstrzymaj odtwarzanie po każdym wideo spoza listy odtwarzania</string>\n  <string name=\"repeat_mode_none\">Wstrzymaj odtwarzanie po bieżącym wideo</string>\n  <string name=\"subscribed_to_channel\">Subskrypcja dodana</string>\n  <string name=\"unsubscribed_from_channel\">Brak Subskrypcji</string>\n  <string name=\"subtitle_yellow_transparent\">Żółte na przezroczystym tle</string>\n  <string name=\"subtitle_white_transparent\">Białe na przezroczystym tle</string>\n  <string name=\"subtitle_white_black\">Białe na czarnym tle</string>\n  <string name=\"player_seek_preview\">Podgląd podczas przewijania</string>\n  <string name=\"color_scheme_dark_grey_oled\">Ciemnoszary (OLED)</string>\n  <string name=\"channels_section_sorting\">Sortowanie sekcji Kanały</string>\n  <string name=\"sorting_by_new_content\">Nowe treści</string>\n  <string name=\"sorting_alphabetically\">Alfabetycznie</string>\n  <string name=\"sorting_last_viewed\">Ostatnio oglądane</string>\n  <string name=\"player_pause_when_seek\">Wstrzymaj podczas przewijania</string>\n  <string name=\"playlists_style\">Styl sekcji list odtwarzania</string>\n  <string name=\"playlists_style_grid\">Siatka</string>\n  <string name=\"playlists_style_rows\">Wiersze</string>\n  <string name=\"player_show_clock\">Pokaż zegar</string>\n  <string name=\"player_show_remaining_time\">Pokaż pozostały czas</string>\n  <string name=\"player_show_ending_time\">Pokaż czas zakończenia</string>\n  <string name=\"open_channel_uploads\">Przesyłanie z otwartego Kanału</string>\n  <string name=\"app_exit_shortcut\">Zamykanie aplikacji</string>\n  <string name=\"player_exit_shortcut\">Zamykanie odtwarzacza</string>\n  <string name=\"app_exit_none\">Nie wychodź</string>\n  <string name=\"app_double_back_exit\">Dwa razy wstecz</string>\n  <string name=\"app_single_back_exit\">Raz wstecz</string>\n  <string name=\"action_video_zoom\">Powiększenie wideo</string>\n  <string name=\"video_zoom\">Powiększenie wideo</string>\n  <string name=\"video_zoom_default\">Domyślnie</string>\n  <string name=\"video_zoom_fit_width\">Dopasuj szerokość</string>\n  <string name=\"video_zoom_fit_height\">Dopasuj wysokość</string>\n  <string name=\"video_zoom_fit_both\">Dopasuj szerokość lub wysokość</string>\n  <string name=\"video_zoom_stretch\">Rozciągnij</string>\n  <string name=\"color_scheme_teal\">Turkusowy</string>\n  <string name=\"color_scheme_teal_oled\">Turkusowy (OLED)</string>\n  <string name=\"player_seek_preview_none\">Wyłączony</string>\n  <string name=\"player_seek_preview_single\">Pojedyncza ramka</string>\n  <string name=\"player_seek_preview_carousel\">Karuzela</string>\n  <string name=\"player_seek_preview_carousel_slow\">Karuzela (wolno)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Karuzela (szybko)</string>\n  <string name=\"unsubscribe_from_channel\">Anuluj subskrypcję Kanału</string>\n  <string name=\"card_title_lines_num\">Liczba wierszy tytułu karty</string>\n  <string name=\"card_auto_scrolled_title\">Automatycznie przewijaj przycięty tytuł</string>\n  <string name=\"share_link\">Udostępnij link</string>\n  <string name=\"share_qr_link\">Udostępnij link (kod QR)</string>\n  <string name=\"subscribe_to_channel\">Subskrybuj Kanał</string>\n  <string name=\"wait_data_loading\">Proszę czekać, trwa ładowanie danych…</string>\n  <string name=\"auto_frame_rate_pause\">Automatyczna liczba klatek na sekundę (wstrzymaj podczas przełączania)</string>\n  <string name=\"auto_frame_rate_applying\">Stosowanie automatycznej liczby klatek na sekundę %sx%s\\@%s</string>\n  <string name=\"audio_shift\">Przesunięcie dźwięku</string>\n  <string name=\"player_remember_speed\">Zapamiętuj prędkość</string>\n  <string name=\"mark_channel_as_watched\">Oznacz jako obejrzane</string>\n  <string name=\"channel_marked_as_watched\">Wideo z tego Kanału zostały oznaczone jako obejrzane</string>\n  <string name=\"dialog_add_device\">Dodaj urządzenie</string>\n  <string name=\"dialog_remove_all_devices\">Usuń wszystkie urządzenia</string>\n  <string name=\"device_link_enabled\">Łącze włączone</string>\n  <string name=\"device_connected\">Urządzenie \\\"%s\\\" zostało podłączone</string>\n  <string name=\"device_disconnected\">Urządzenie \\\"%s\\\" zostało odłączone</string>\n  <string name=\"settings_ui_scale\">Skalowanie interfejsu</string>\n  <string name=\"msg_restart_app\">Uruchom ponownie aplikację, aby zastosować te ustawienia</string>\n  <string name=\"settings_block\">Blokowanie treści</string>\n  <string name=\"msg_applying\">Stosuję %s…</string>\n  <string name=\"msg_done\">Zrobione</string>\n  <string name=\"settings_general\">Ogólne</string>\n  <string name=\"settings_video\">Wideo</string>\n  <string name=\"content_block_confirm_skip\">Potwierdź przy pominięciu</string>\n  <string name=\"content_block_categories\">Pomiń segmenty</string>\n  <string name=\"content_block_sponsor\">Sponsor</string>\n  <string name=\"content_block_intro\">Przerywnik/Animacja wprowadzająca</string>\n  <string name=\"content_block_outro\">Karty końcowe/Napisy końcowe</string>\n  <string name=\"content_block_interaction\">Przypomnienie o interakcji (subskrybuj)</string>\n  <string name=\"content_block_self_promo\">Bezpłatna reklama/Promocja własna</string>\n  <string name=\"content_block_music_off_topic\">Niemuzyczna sekcja klipu</string>\n  <string name=\"confirm_segment_skip\">Pomiń segment \\\"%s\\\"\\?</string>\n  <string name=\"cancel_segment_skip\">Anuluj pomijanie segmentu</string>\n  <string name=\"msg_skipping_segment\">Pomijanie segmentu \\\"%s\\\"…</string>\n  <string name=\"content_block_notification_type\">Typ powiadomienia</string>\n  <string name=\"content_block_notify_none\">Bez powiadomienia</string>\n  <string name=\"content_block_notify_toast\">Powiadomienie typu Toast</string>\n  <string name=\"content_block_notify_dialog\">Pole potwierdzenia</string>\n  <string name=\"return_to_launcher\">Wróć do programu uruchamiającego z trybu ATV (kanałów/wyszukiwania)</string>\n  <string name=\"intent_force_close\">Wymuś wyjście, jeśli wideo zostało otwarte ze źródła zewnętrznego</string>\n  <string name=\"btn_confirm\">Potwierdź</string>\n  <string name=\"player_low_video_quality\">Niska jakość wideo</string>\n  <string name=\"remote_session_closed\">Sesja zdalna została zamknięta</string>\n  <string name=\"msg_mode_switch_error\">Nie można przełączyć trybu wyświetlania na \\\"%s\\\"</string>\n  <string name=\"msg_player_error\">Wystąpił błąd odtwarzacza %s. Wznawiam odtwarzanie…</string>\n  <string name=\"video_preset_disabled\">Bez ustawień wstępnych</string>\n  <string name=\"video_preset_enabled\">Format następnego wideo zostanie ustawiony zgodnie z ustawieniami wstępnymi</string>\n  <string name=\"player_sleep_timer\">Wyłącznik czasowy</string>\n  <string name=\"player_show_quality_info\">Pokaż informacje o jakości wideo</string>\n  <string name=\"player_remember_each_speed\">Zapamiętuj prędkość każdego wideo</string>\n  <string name=\"player_remember_speed_none\">Nie zapamiętuj</string>\n  <string name=\"player_remember_speed_all\">Dla wszystkich wideo taka sama</string>\n  <string name=\"player_remember_speed_each\">Dla każdego wideo oddzielnie</string>\n  <string name=\"msg_player_error_source\">Nie można pobrać wideo</string>\n  <string name=\"msg_player_error_renderer\">Wybrany format nie jest obsługiwany.\\nSpróbuj zresetować urządzenie.</string>\n  <string name=\"msg_player_error_video_renderer\">Wybrany format WIDEO nie jest obsługiwany.\\nSpróbuj wybrać inny poprzez naciśnięcie przycisku HQ na odtwarzaczu.\\nJeśli to nie pomoże, wyłącz urządzenie.</string>\n  <string name=\"msg_player_error_audio_renderer\">Wybrany format AUDIO nie jest obsługiwany.\\nSpróbuj wybrać inny poprzez naciśnięcie przycisku HQ na odtwarzaczu.\\nJeśli to nie pomoże, wyłącz urządzenie.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">Wybrany format NAPISÓW nie jest obsługiwany.\\nSpróbuj wybrać inny poprzez naciśnięcie przycisku CC na odtwarzaczu.\\nJeśli to nie pomoże, wyłącz urządzenie.</string>\n  <string name=\"msg_player_error_unexpected\">Nieznany błąd dekodera wideo</string>\n  <string name=\"video_aspect\">Współczynnik proporcji</string>\n  <string name=\"player_tweaks\">Opcje programistyczne</string>\n  <string name=\"header_uploads\">Przesyłanie</string>\n  <string name=\"player_show_global_clock\">Zawsze pokazuj zegar na ekranie</string>\n  <string name=\"player_show_global_ending_time\">Zawsze pokazuj czas zakończenia na ekranie</string>\n  <string name=\"app_corner_clock\">Strona główna: zegar w górnym prawym rogu</string>\n  <string name=\"player_corner_clock\">Odtwarzacz: zegar w górnym prawym rogu</string>\n  <string name=\"player_corner_ending_time\">Odtwarzacz: czas zakończenia w górnym prawym rogu</string>\n  <string name=\"uploads_old_look\">Przywróć poprzedni wygląd sekcji przesyłania</string>\n  <string name=\"background_playback_activation\">Odtwarzanie w tle (aktywacja)</string>\n  <string name=\"channels_old_look\">Przywróć poprzedni wygląd sekcji Kanały</string>\n  <string name=\"channels_auto_load\">Automatycznie ładuj zawartość sekcji Kanały</string>\n  <string name=\"return_to_background_video\">Wróć do wideo działającego w tle</string>\n  <string name=\"pin_unpin_from_sidebar\">Przypnij/Odepnij od paska bocznego</string>\n  <string name=\"pin_unpin_playlist\">Przypnij/Odepnij listę odtwarzania</string>\n  <string name=\"pin_unpin_channel\">Przypnij/Odepnij Kanał</string>\n  <string name=\"pin_playlist\">Dodaj listę odtwarzania do paska bocznego</string>\n  <string name=\"pin_channel\">Dodaj Kanał do paska bocznego</string>\n  <string name=\"pinned_to_sidebar\">Przypięto do paska bocznego</string>\n  <string name=\"unpin_from_sidebar\">Odepnij od paska bocznego</string>\n  <string name=\"unpinned_from_sidebar\">Odpięto od paska bocznego</string>\n  <string name=\"dialog_select_country\">Kraj</string>\n  <string name=\"settings_language_country\">Język/Kraj</string>\n  <string name=\"share_embed_link\">Udostępnij link do osadzania</string>\n  <string name=\"card_text_scroll_factor\">Szybkość przewijania tekstu karty</string>\n  <string name=\"preferred_update_source\">Wybierz źródło aktualizacji</string>\n  <string name=\"hide_shorts\">Ukryj Shorts w sekcji Subskrypcje</string>\n  <string name=\"hide_shorts_channel\">Ukryj Shorts w sekcji Kanały</string>\n  <string name=\"key_remapping\">Zmiana mapowania przycisków</string>\n  <string name=\"screen_dimming\">Przyciemnianie ekranu</string>\n  <string name=\"removed_from_playback_queue\">Usunięto z kolejki odtwarzania</string>\n  <string name=\"added_to_playback_queue\">Dodano do kolejki odtwarzania</string>\n  <string name=\"add_remove_from_playback_queue\">Dodaj/Usuń z kolejki odtwarzania</string>\n  <string name=\"add_to_playback_queue\">Dodaj do kolejki odtwarzania</string>\n  <string name=\"remove_from_playback_queue\">Usuń z kolejki odtwarzania</string>\n  <string name=\"proxy_enabled\">Proxy włączone</string>\n  <string name=\"proxy_disabled\">Proxy wyłączone</string>\n  <string name=\"uploads_row_name\">Przesyłanie</string>\n  <string name=\"playlists_row_name\">Utworzone listy odtwarzania</string>\n  <string name=\"popular_uploads_row_name\">Popularne wideo</string>\n  <string name=\"news_row_name\">Wiadomości</string>\n  <string name=\"breaking_news_row_name\">Z ostatniej chwili</string>\n  <string name=\"covid_news_row_name\">Wieści dot. COVID-19</string>\n  <string name=\"enable_voice_search\">Włącz wyszukiwanie głosowe (wymagana obsługa przez oprogramowanie wewnętrzne)</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Subskrybuj/Anuluj subskrypcję Kanału</string>\n  <string name=\"app_backup_restore\">Kopia zapasowa/Przywracanie</string>\n  <string name=\"app_backup\">Utwórz kopię zapasową</string>\n  <string name=\"app_restore\">Przywróć dane z kopii zapasowej</string>\n  <string name=\"skip_each_segment_once\">Nie pomijaj ponownie segmentów</string>\n  <string name=\"disable_ok_long_press\">Wyłącz reakcję na długie przyciśnięcie przycisku OK</string>\n  <string name=\"disable_ok_long_press_desc\">Przeznaczone dla kontrolerów z błędami, w których przycisk OK nie działa prawidłowo.</string>\n  <string name=\"player_time_correction\">Wyświetlaj czas uwzględniając prędkość</string>\n  <string name=\"sponsor_color_markers\">Kolorowe znaczniki na pasku postępu odtwarzania</string>\n  <string name=\"refresh_section\">Odśwież sekcję</string>\n  <string name=\"option_disabled\">Wyłączone</string>\n  <string name=\"live_now_row_name\">Teraz na żywo</string>\n  <string name=\"run_in_background\">Uruchom w tle</string>\n  <string name=\"settings_remote_control\">Zdalne sterowanie</string>\n  <string name=\"background_service_started\">Uruchomiono usługę w tle</string>\n  <string name=\"dialog_add_to\">Dodaj do %s</string>\n  <string name=\"dialog_remove_from\">Usuń z %s</string>\n  <string name=\"added_to\">Dodano do %s</string>\n  <string name=\"removed_from\">Usunięto z %s</string>\n  <string name=\"video_preset_adaptive\">Adaptacyjny</string>\n  <string name=\"content_block_preview_recap\">Podgląd lub podsumowanie wideo</string>\n  <string name=\"content_block_highlight\">Szczególnie interesujący fragment (zakładka)</string>\n  <string name=\"removed_from_history\">Wideo zostało usunięte z historii</string>\n  <string name=\"remove_from_history\">Usuń z historii</string>\n  <string name=\"upload_date\">Data przesłania</string>\n  <string name=\"upload_date_any\">Cały czas</string>\n  <string name=\"upload_date_today\">Dziś</string>\n  <string name=\"upload_date_this_week\">Bieżący tydzień</string>\n  <string name=\"upload_date_this_month\">Bieżący miesiąc</string>\n  <string name=\"upload_date_this_year\">Bieżący rok</string>\n  <string name=\"double_refresh_rate\">Podwojenie częstotliwości odświeżania</string>\n  <string name=\"upload_date_last_hour\">Ostatnia godzina</string>\n  <string name=\"focus_on_search_results\">Automatyczne przejście do wyników wyszukiwania</string>\n  <string name=\"context_menu\">Menu kontekstowe</string>\n  <string name=\"add_remove_from_recent_playlist\">Dodaj/Usuń z ostatniej listy odtwarzania</string>\n  <string name=\"hide_settings_section\">Ukryj sekcję Ustawienia (niezalecane!)</string>\n  <string name=\"volume\">Głośność %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s s</string>\n  <string name=\"audio_shift_sec\">%s s</string>\n  <string name=\"ui_hide_timeout_sec\">%s s</string>\n  <string name=\"screen_dimming_timeout_min\">%s min</string>\n  <string name=\"mark_all_channels_watched\">Oznacz wszystkie Kanały jako obejrzane</string>\n  <string name=\"move_section_up\">Przesuń sekcję w górę</string>\n  <string name=\"move_section_down\">Przesuń sekcję w dół</string>\n  <string name=\"player_buttons\">Przyciski odtwarzacza</string>\n  <string name=\"action_playlist_add\">Dodaj do listy odtwarzania</string>\n  <string name=\"action_video_stats\">Statystyki wideo</string>\n  <string name=\"action_subscribe\">Subskrybuj</string>\n  <string name=\"action_channel\">Otwórz Kanał</string>\n  <string name=\"action_pip\">Obraz w obrazie (PiP)</string>\n  <string name=\"action_video_info\">Opis wideo</string>\n  <string name=\"action_screen_off\">Wyłącz ekran</string>\n  <string name=\"action_sound_off\">Wyłącz dźwięk</string>\n  <string name=\"action_playback_queue\">Kolejka odtwarzania</string>\n  <string name=\"action_video_speed\">Prędkość wideo</string>\n  <string name=\"action_subtitles\">Napisy</string>\n  <string name=\"action_like\">Podoba mi się</string>\n  <string name=\"action_dislike\">Nie podoba mi się</string>\n  <string name=\"action_play_pause\">Odtwarzanie/Pauza</string>\n  <string name=\"action_repeat_mode\">Tryb odtwarzania</string>\n  <string name=\"action_next\">Następne wideo</string>\n  <string name=\"action_previous\">Poprzednie wideo</string>\n  <string name=\"action_high_quality\">Jakość wideo</string>\n  <string name=\"content_block_no_skipping_mode\">Tryb bez pomijania</string>\n  <string name=\"content_block_action_type\">Zachowanie dla poszczególnych rodzajów zawartości</string>\n  <string name=\"content_block_action_none\">Nic nie rób</string>\n  <string name=\"content_block_action_only_skip\">Tylko pomiń</string>\n  <string name=\"content_block_action_toast\">Pomiń z powiadomieniem</string>\n  <string name=\"content_block_action_dialog\">Pokaż pole potwierdzenia</string>\n  <string name=\"content_block_filler\">Nie na temat (wypełniacz)</string>\n  <string name=\"keyboard_auto_show\">Pokaż klawiaturę automatycznie</string>\n  <string name=\"cancel_dialog\">Anuluj</string>\n  <string name=\"msg_player_error_source2\">Źródło URL nie działa lub nieprawidłowo ustawiony zegar urządzenia.\\nZazwyczaj jest to wina aplikacji.</string>\n  <string name=\"msg_player_error_video_source\">Źródło WIDEO nie działa lub nieprawidłowo ustawiony zegar urządzenia.\\nZazwyczaj jest to wina aplikacji.</string>\n  <string name=\"msg_player_error_audio_source\">Źródło AUDIO nie działa lub nieprawidłowo ustawiony zegar urządzenia.\\nZazwyczaj jest to wina aplikacji.</string>\n  <string name=\"msg_player_error_subtitle_source\">Źródło NAPISÓW nie działa lub nieprawidłowo ustawiony zegar urządzenia.\\nZazwyczaj jest to wina aplikacji.</string>\n  <string name=\"hide_upcoming\">Ukryj nadchodzące w sekcji Subskrypcje</string>\n  <string name=\"hide_upcoming_channel\">Ukryj nadchodzące w sekcji Kanały</string>\n  <string name=\"hide_upcoming_home\">Ukryj nadchodzące w sekcji Główna</string>\n  <string name=\"search_background_playback\">Przełącz na tryb \\'obraz w obrazie\\', jeśli wyszukiwanie jest uruchamiane z odtwarzacza</string>\n  <string name=\"trending_row_name\">Trendy</string>\n  <string name=\"player_seek_type\">Zachowanie przewijania</string>\n  <string name=\"player_seek_regular\">Regularny</string>\n  <string name=\"player_seek_confirmation_pause\">Z potwierdzeniem (pauza podczas przewijania)</string>\n  <string name=\"player_seek_confirmation_play\">Z potwierdzeniem (odtwarzaj podczas przewijania)</string>\n  <string name=\"hide_shorts_from_home\">Ukryj Shorts w sekcji Główna</string>\n  <string name=\"finish_on_disconnect\">Zamknij aplikację po odłączeniu urządzenia zdalnego sterowania (telefon/tablet)</string>\n  <string name=\"update_error\">Błąd aktualizacji</string>\n  <string name=\"hide_shorts_from_history\">Ukryj Shorts w sekcji Historia</string>\n  <string name=\"hide_shorts_from_trending\">Ukryj Shorts w sekcji Na czasie</string>\n  <string name=\"disable_screensaver\">Wyłącz wygaszacz ekranu</string>\n  <string name=\"description_not_found\">Nie znaleziono opisu</string>\n  <string name=\"proxy_port_hint\">Port proxy</string>\n  <string name=\"proxy_host_hint\">Nazwa hosta lub adres IP serwera proxy</string>\n  <string name=\"proxy_username_hint\">Użytkownik proxy</string>\n  <string name=\"proxy_password_hint\">Hasło proxy</string>\n  <string name=\"proxy_type\">Typ proxy</string>\n  <string name=\"enable_web_proxy\">Używaj serwera proxy</string>\n  <string name=\"proxy_not_supported\">Używaj serwera proxy (wymaga Androida 4.4+)</string>\n  <string name=\"proxy_test_btn\">Test</string>\n  <string name=\"proxy_settings_title\">Ustawienia serwera proxy</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">Test#%d: anulowany.</string>\n  <string name=\"proxy_type_invalid\">Nie ustawiono typu proxy.</string>\n  <string name=\"proxy_host_invalid\">Nie ustawiono hosta proxy.</string>\n  <string name=\"proxy_port_invalid\">Nieprawidłowy port proxy, wymagana wartość liczbowa dodatnia.</string>\n  <string name=\"proxy_credentials_invalid\">Nieprawidłowy użytkownik lub hasło.</string>\n  <string name=\"proxy_test_aborted\">Test został przerwany, wprowadź prawidłowe ustawienia proxy.</string>\n  <string name=\"proxy_application_aborted\">Wprowadź prawidłowe ustawienia proxy.</string>\n  <string name=\"proxy_test_start\">Test #%d: %s …</string>\n  <string name=\"proxy_test_error\">Błąd #%d: %s</string>\n  <string name=\"proxy_test_status\">Status #%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Adres pliku konfiguracyjnego (*.ovpn)</string>\n  <string name=\"enable_openvpn\">Używaj OpenVPN</string>\n  <string name=\"openvpn_settings_title\">Ustawienia OpenVPN</string>\n  <string name=\"openvpn_application_aborted\">Wprowadź prawidłowe ustawienia OpenVPN.</string>\n  <string name=\"openvpn_test_aborted\">Test przerwany, wprowadź prawidłowe ustawienia OpenVPN.</string>\n  <string name=\"openvpn_address_invalid\">Nie ustawiono adresu konfiguracji OpenVPN.</string>\n  <string name=\"internet_censorship\">Cenzura Internetu</string>\n  <string name=\"rename_section\">Zmień nazwę sekcji</string>\n  <string name=\"simple_edit_value_hint\">Nowa wartość</string>\n  <string name=\"seek_interval\">Interwał przewijania</string>\n  <string name=\"seek_interval_sec\">%s s</string>\n  <string name=\"subtitle_system\">Styl systemu (Ustawienia Androida &gt; Ułatwienia dostępu)</string>\n  <string name=\"subtitle_scale\">Skalowanie napisów</string>\n  <string name=\"audio_sync_fix_desc\">Alternatywny sposób synchronizacji audio/wideo. Uważany za przestarzały, ale w niektórych przypadkach może pomóc.</string>\n  <string name=\"audio_sync_fix\">Poprawka synchronizacji dźwięku</string>\n  <string name=\"ambilight_ratio_fix_desc\">Naprawia brakujące oświetlenie bias (np. Ambilight). Naprawia nieprawidłowe proporcje lub skalowanie. Naprawia puste zrzuty ekranu. Wpływa na wydajność!</string>\n  <string name=\"ambilight_ratio_fix\">Poprawka Ambilight/Proporcje/Skalowanie/Zrzuty ekranu</string>\n  <string name=\"force_legacy_codecs_desc\">Znacząco poprawia wydajność na słabszych urządzeniach. Maksymalna rozdzielczość to 720p.</string>\n  <string name=\"force_legacy_codecs\">Wymuś starsze kodeki (720p)</string>\n  <string name=\"live_stream_fix_desc\">UWAGA: ta poprawka wyłącza przewijanie. Znacząco poprawia wydajność transmisji na żywo na słabszych urządzeniach. Maksymalna rozdzielczość to 1080p.</string>\n  <string name=\"live_stream_fix_4k_desc\">UWAGA: ta poprawka wyłącza przewijanie. Znacząco poprawia wydajność transmisji na żywo. Maksymalna rozdzielczość to UHD.</string>\n  <string name=\"live_stream_fix\">Poprawka transmisji na żywo (FHD)</string>\n  <string name=\"live_stream_fix_4k\">Poprawka transmisji na żywo (UHD)</string>\n  <string name=\"playback_notifications_fix_desc\">Ukrywa powiadomienia o zmianach utworu. Może być przydatny w przypadku oprogramowania opartego na AOSP (custom ROM).</string>\n  <string name=\"playback_notifications_fix\">Wyłącz powiadomienia o odtwarzaniu</string>\n  <string name=\"amlogic_fix_desc\">Poprawka dla gubienia klatek na urządzeniach opartych o chip Amlogic</string>\n  <string name=\"amlogic_fix\">Poprawka dla Amlogic 1080p\\@60fps</string>\n  <string name=\"tunneled_video_playback_desc\">UWAGA: pauza może nie działać prawidłowo! Tunelowane odtwarzanie wideo zapewnia korzyści takie jak lepsza synchronizacja audio/wideo (AV sync) i płynniejsze odtwarzanie. Wymagany Android 5+.</string>\n  <string name=\"tunneled_video_playback\">Tunelowane odtwarzanie wideo (Android 5+)</string>\n  <string name=\"master_volume\">Głośność główna</string>\n  <string name=\"volume_limit\">Limit głośności</string>\n  <string name=\"player_volume\">Głośność</string>\n  <string name=\"play_video\">Odtwórz</string>\n  <string name=\"remember_position_of_short_videos\">Zapamiętaj pozycję krótkich wideo (mniej niż 5 min)</string>\n  <string name=\"remember_position_of_live_videos\">Zapamiętaj pozycję transmisji na żywo</string>\n  <string name=\"player_show_tooltips\">Pokaż podpowiedzi przycisków</string>\n  <string name=\"action_like_unset\">Polubienie nie jest ustawione</string>\n  <string name=\"action_dislike_unset\">Niepolubienie nie jest ustawione</string>\n  <string name=\"various_buttons\">Przyciski w górnej części okna głównego</string>\n  <string name=\"not_compatible_with\">Wybrana opcja nie jest zgodna z</string>\n  <string name=\"subtitle_position\">Odległość napisów od dolnej części ekranu</string>\n  <string name=\"pressing_home\">naciskając HOME</string>\n  <string name=\"pressing_home_back\">naciskając HOME lub WSTECZ</string>\n  <string name=\"pressing_back\">naciskając WSTECZ</string>\n  <string name=\"player_number_key_seek\">Przewijaj za pomocą klawiszy numerycznych</string>\n  <string name=\"save_remove_playlist\">Zapisz/Usuń tę listę odtwarzania z sekcji listy odtwarzania</string>\n  <string name=\"save_playlist\">Dodaj listę odtwarzania do sekcji listy odtwarzania</string>\n  <string name=\"remove_playlist\">Skasuj na zawsze listę odtwarzania z sekcji listy odtwarzania</string>\n  <string name=\"removed_from_playlists\">Usunięto z sekcji listy odtwarzania</string>\n  <string name=\"saved_to_playlists\">Dodano do sekcji listy odtwarzania</string>\n  <string name=\"create_playlist\">Stwórz listę odtwarzania</string>\n  <string name=\"add_video_to_new_playlist\">Dodaj bieżące wideo do nowej listy odtwarzania</string>\n  <string name=\"cant_delete_empty_playlist\">Nie można usunąć pustej listy odtwarzania</string>\n  <string name=\"playlist\">Lista odtwarzania</string>\n  <string name=\"rename_playlist\">Zmień nazwę listy odtwarzania</string>\n  <string name=\"cant_rename_empty_playlist\">Nie można zmienić nazwy pustej listy odtwarzania</string>\n  <string name=\"cant_rename_foreign_playlist\">Nie można zmienić nazwy obcej listy odtwarzania</string>\n  <string name=\"cant_save_playlist\">Nie można zapisać tej listy odtwarzania</string>\n  <string name=\"enter_value\">Wpisz dowolną niepustą wartość</string>\n  <string name=\"dialog_add_remove_from\">Dodaj/Usuń z %s</string>\n  <string name=\"lb_playback_controls_skip_next\">Pomiń następny</string>\n  <string name=\"lb_playback_controls_skip_previous\">Pomiń poprzedni/przewiń do pozycji początkowej</string>\n  <string name=\"alt_presets_behavior\">Alternatywna polityka stosowania ustawień wstępnych (ogranicz przepustowość)</string>\n  <string name=\"alt_presets_behavior_desc\">Aplikacja spróbuje utrzymać przepustowość odpowiadającą wybranemu ustawieniu wstępnemu, zamiast dopasowywać rozdzielczość, fps i kodek.</string>\n  <string name=\"sleep_timer\">Włącz wyłącznik czasowy (godzina braku aktywności)</string>\n  <string name=\"sleep_timer_desc\">Wstrzymaj odtwarzanie, jeśli użytkownik nie użył pilota przez godzinę</string>\n  <string name=\"disable_vsync\">Wyłącz VSync</string>\n  <string name=\"disable_vsync_desc\">Wyłącza synchronizację klatek z sygnałem synchronizacji pionowej (vertical sync) wyświetlacza. Dzięki mniejszemu zapotrzebowaniu na zasoby obliczeniowe (CPU) nieco poprawia wydajność odtwarzania na mniej wydajnych urządzeniach.</string>\n  <string name=\"skip_codec_profile_check\">Pomiń sprawdzanie poziomu profilu kodeka</string>\n  <string name=\"skip_codec_profile_check_desc\">Nie sprawdzaj obsługi kodeków podczas uruchamiania wideo. Może być pomocny w przypadku wadliwego oprogramowania układowego.</string>\n  <string name=\"force_sw_codec\">Wymuś programowy (SW) dekoder wideo</string>\n  <string name=\"force_sw_codec_desc\">Umożliwia odtwarzanie niemal każdego wideo, niestety przy bardzo niskiej wydajności.</string>\n  <string name=\"playlist_order\">Sortowanie listy odtwarzania</string>\n  <string name=\"playlist_order_added_date_newer_first\">Data dodania (najpierw nowsza)</string>\n  <string name=\"playlist_order_added_date_older_first\">Data dodania (najpierw starsze)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Data publikacji (najpierw nowsze)</string>\n  <string name=\"playlist_order_published_date_older_first\">Data publikacji (najpierw starsze)</string>\n  <string name=\"playlist_order_popularity\">Popularność</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">Niedostępne dla zewnętrznej listy odtwarzania</string>\n  <string name=\"owned_playlist_warning\">Błąd. Tę czynność można zastosować tylko do własnych list odtwarzania</string>\n  <string name=\"content_block_alt_server\">Użyj alternatywnego serwera</string>\n  <string name=\"content_block_alt_server_desc\">Warto wypróbować, jeśli SponsorBlock odmawia współpracy.</string>\n  <string name=\"unset_stream_reminder\">Usuń przypomnienie o transmisji na żywo</string>\n  <string name=\"set_stream_reminder\">Ustaw przypomnienie o transmisji na żywo</string>\n  <string name=\"playback_starts_shortly\">Odtwarzanie rozpocznie się automatycznie, gdy strumień będzie gotowy</string>\n  <string name=\"starting_stream\">Rozpoczynam transmisję…</string>\n  <string name=\"action_playlist_remove\">Usuń z listy odtwarzania</string>\n  <string name=\"signin_view_title\">Wczytuję kod użytkownika…</string>\n  <string name=\"signin_view_description\">Aby się zalogować, wprowadź podany kod na stronie %s\\nUWAGA: działa tylko z przeglądarkami Firefox lub Chrome.</string>\n  <string name=\"signin_view_action_text\">Gotowe</string>\n  <string name=\"require_checked\">Wymaga \\\"%s\\\"</string>\n  <string name=\"msg_press_again_to_exit\">Naciśnij ponownie, aby wyjść</string>\n  <string name=\"player_remaining_time\">Pozostało: %s</string>\n  <string name=\"player_ending_time\">Kończy się o %s</string>\n  <string name=\"badge_new_content\">NOWA TREŚĆ</string>\n  <string name=\"badge_live\">NA ŻYWO</string>\n  <string name=\"add_device_view_description\">Aby powiązać urządzenie, wpisz na nim podany kod w aplikacji YouTube w sekcji Ustawienia/Oglądaj na TV\\nUWAGA: działa tylko z klawiaturą Gboard.</string>\n  <string name=\"playback_controls_repeat_pause\">Powtarzaj pauzę</string>\n  <string name=\"playback_controls_repeat_list\">Powtórz listę</string>\n  <string name=\"action_subscribe_off\">Subskrypcja wył.</string>\n  <string name=\"action_subscribe_on\">Subskrypcja wł.</string>\n  <string name=\"skip_24_rate\">Pomiń format 24 kl./s (naprawa prawdziwego trybu kinowego\\?)</string>\n  <string name=\"skip_shorts\">Nie stosuj tych ustawień w przypadku Shorts</string>\n  <string name=\"player_disable_suggestions\">Wyłącz sugestie</string>\n  <string name=\"feedback\">Uwagi, sugestie</string>\n  <string name=\"sources\">Kod źródłowy</string>\n  <string name=\"releases\">Wydania aplikacji</string>\n  <string name=\"prefer_avc_over_vp9\">Wybór kodeka: pierwszeństwo AVC przed VP9</string>\n  <string name=\"open_chat\">Czat/Komentarze</string>\n  <string name=\"open_comments\">Otwórz komentarze</string>\n  <string name=\"place_chat_left\">Umieść czat po lewej stronie</string>\n  <string name=\"use_alt_speech_recognizer\">Alternatywne rozpoznawanie mowy</string>\n  <string name=\"time_format\">Format czasu</string>\n  <string name=\"time_format_24\">24-godzinny</string>\n  <string name=\"time_format_12\">12-godzinny (AM/PM)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Zawiera wiele błędów. Wypróbuj tylko wtedy, gdy masz problemy z domyślnym aparatem rozpoznawania.</string>\n  <string name=\"speech_recognizer\">Rozpoznawanie mowy</string>\n  <string name=\"speech_recognizer_system\">Systemowe (preferowane)</string>\n  <string name=\"speech_recognizer_external_1\">Dostawca zewnętrzny 1 (zawiera wiele błędów, wymaga wyłączenia dostępu do mikrofonu)</string>\n  <string name=\"speech_recognizer_external_2\">Dostawca zewnętrzny 2 (zawiera wiele błędów)</string>\n  <string name=\"real_channel_icon\">Pokaż oryginalną ikonę na przycisku Kanału</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Żółty na półprzezroczystym tle</string>\n  <string name=\"subtitle_yellow_black\">Żółty na czarnym tle</string>\n  <string name=\"player_pixel_ratio\">Proporcja pikseli</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Ciemnoszary (monochromatyczny)</string>\n  <string name=\"disable_mic_permission\">Wyłącz dostęp do mikrofonu dla aplikacji, aby bieżący aparat rozpoznawania działał poprawnie.</string>\n  <string name=\"repeat_mode_shuffle\">Losowo w ramach listy odtwarzania</string>\n  <string name=\"chat_left\">Lewo</string>\n  <string name=\"chat_right\">Prawo</string>\n  <string name=\"card_real_thumbnails\">Zastąp miniatury klatką z wideo</string>\n  <string name=\"card_content\">Miejsce pozyskania miniatury karty</string>\n  <string name=\"thumb_quality_default\">Domyślnie</string>\n  <string name=\"thumb_quality_start\">Początek wideo</string>\n  <string name=\"thumb_quality_middle\">Środek wideo</string>\n  <string name=\"thumb_quality_end\">Koniec wideo</string>\n  <string name=\"content_block_status\">Sprawdź stan usługi SponsorBlock</string>\n  <string name=\"dearrow_status\">Sprawdź stan usługi DeArrow</string>\n  <string name=\"player_show_quality_info_bitrate\">Dodaj bitrate do informacji o jakości</string>\n  <string name=\"player_speed_button_old_behavior\">Przywróć poprzednie zachowanie przycisku prędkości</string>\n  <string name=\"protect_settings_with_password\">Chroń wszystkie ustawienia hasłem</string>\n  <string name=\"enter_settings_password\">Wprowadź hasło do ustawień</string>\n  <string name=\"child_mode\">Tryb dziecka</string>\n  <string name=\"child_mode_desc\">W tym trybie użytkownik nie może używać wyszukiwarki ani zobaczyć innej, polecanej zawartości. Ustawienia będą zabezpieczone hasłem.</string>\n  <string name=\"lost_setting_warning\">Ustawienia aplikacji zostaną zmienione. Upewnij się, że utworzyłeś kopię zapasową ustawień.</string>\n  <string name=\"player_button_long_click\">Krótkie naciśnięcie przycisku dla wywołania akcji, długie dla ustawień</string>\n  <string name=\"pause_history\">Wstrzymaj historię odtwarzania</string>\n  <string name=\"resume_history\">Wznów historię odtwarzania</string>\n  <string name=\"disable_history\">Wyłącz historię odtwarzania</string>\n  <string name=\"enable_history\">Włącz historię odtwarzania</string>\n  <string name=\"clear_history\">Wyczyść historię odtwarzania</string>\n  <string name=\"chapters\">Rozdziały</string>\n  <string name=\"card_multiline_subtitle\">Napisy wielowierszowe</string>\n  <string name=\"auto_frame_rate_modes\">Wspierane tryby</string>\n  <string name=\"loading\">Wczytywanie…</string>\n  <string name=\"hide_streams\">Ukryj transmisje w sekcji Subskrypcje</string>\n  <string name=\"video_rotate\">Obróć</string>\n  <string name=\"trending_searches\">Popularne wyszukiwania</string>\n  <string name=\"video_duration_any\">Wszystkie</string>\n  <string name=\"video_duration\">Długość</string>\n  <string name=\"video_duration_under_4\">Poniżej 4 minut</string>\n  <string name=\"video_duration_between_4_20\">4–20 minut</string>\n  <string name=\"video_duration_over_20\">Ponad 20 minut</string>\n  <string name=\"content_type\">Rodzaj</string>\n  <string name=\"content_type_any\">Wszystkie</string>\n  <string name=\"content_type_video\">Wideo</string>\n  <string name=\"content_type_channel\">Kanał</string>\n  <string name=\"content_type_playlist\">Lista odtwarzania</string>\n  <string name=\"content_type_movie\">Film</string>\n  <string name=\"video_features\">Właściwości</string>\n  <string name=\"video_feature_any\">Wszystkie</string>\n  <string name=\"video_feature_live\">Na żywo</string>\n  <string name=\"video_feature_4k\">UHD</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">Sortuj wg</string>\n  <string name=\"sort_by_relevance\">Trafności</string>\n  <string name=\"sort_by_views\">Liczby wyświetleń</string>\n  <string name=\"sort_by_date\">Daty przesłania</string>\n  <string name=\"sort_by_rating\">Oceny</string>\n  <string name=\"clear_search_history\">Wyczyść historię wyszukiwania</string>\n  <string name=\"remove_from_subscriptions\">Ukryj</string>\n  <string name=\"player_long_speed_list\">Więcej pozycji na liście prędkości</string>\n  <string name=\"player_extra_long_speed_list\">Jeszcze więcej pozycji na liście prędkości</string>\n  <string name=\"alt_app_icon\">Alternatywna ikona aplikacji (wymaga ponownego uruchomienia aplikacji)</string>\n  <string name=\"network_stack\">Preferuj bibliotekę sieciową %s</string>\n  <string name=\"player_network_stack\">Biblioteka sieciowa</string>\n  <string name=\"cronet_desc\">Cronet jest biblioteką sieciową projektu Chromium. Cronet potrafi zmniejszyć opóźnienia i zwiększyć wydajność operacji sieciowych, czym może pomóc w przypadku problemów z buforowaniem wideo.</string>\n  <string name=\"unlock_all_formats\">Odblokuj wszystkie formaty wideo</string>\n  <string name=\"unlock_all_formats_desc\">W przypadku niektórych urządzeń ich oprogramowanie wewnętrzne (firmware) nieprawidłowo rozpoznaje pewne formaty jako nieobsługiwane.\\nNa przykład telewizory Smart TV FHD zwykle zgłaszają format UHD jako nieobsługiwany.</string>\n  <string name=\"okhttp_desc\">Biblioteka mało wydajna, ale zapewnia wysoką stabilność działania.</string>\n  <string name=\"select_channel_section\">Otwórz odpowiednią sekcję, kiedy aplikacja jest uruchamiana z ATV Channels</string>\n  <string name=\"enable_voice_search_desc\">Oficjalna aplikacja będzie zastąpiona aplikacją Bridge, to pomoże przenieść wyszukiwane zapytania do naszej aplikacji</string>\n  <string name=\"enable_master_password\">Włącz hasło główne</string>\n  <string name=\"enter_master_password\">Wpisz hasło główne</string>\n  <string name=\"disable_stream_buffer\">Wyłącz buforowanie dla transmisji</string>\n  <string name=\"disable_stream_buffer_desc\">Poprawka problemu opóźniających się transmisji. UWAGA: może powodować przerywanie transmisji.</string>\n  <string name=\"sony_frame_drop_fix\">Poprawka dla gubienia klatek #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">Poprawka opóźnień na telewizorach Sony i niektórych innych urządzeniach. UWAGA: możliwe problemy z synchronizacją audio.</string>\n  <string name=\"audio_language\">Język audio</string>\n  <string name=\"old_home_look\">Poprzedni wygląd sekcji Główna</string>\n  <string name=\"old_channel_look\">Poprzedni wygląd sekcji Kanały</string>\n  <string name=\"hide_shorts_everywhere\">Ukryj Shorts we wszystkich sekcjach</string>\n  <string name=\"update_found\">Aktualizacja jest dostępna</string>\n  <string name=\"volume_boost_warning\">Wszelkie ustawienia powyżej limitu mogą uszkodzić głośniki</string>\n  <string name=\"play_video_incognito\">Odtwarzaj w trybie incognito</string>\n  <string name=\"header_kids_home\">Dzieci</string>\n  <string name=\"player_section_playlist\">Użyj bieżącej zawartości jako listy odtwarzania</string>\n  <string name=\"header_trending\">Na Czasie</string>\n  <string name=\"screensaver\">Wygaszacz ekranu</string>\n  <string name=\"player_screen_off_timeout\">Automatyczne wyłączanie ekranu</string>\n  <string name=\"old_update_notifications\">Poprzedni wygląd powiadomienia o aktualizacji</string>\n  <string name=\"dialog_notification\">Powiadom mnie o aktualizacjach w wyskakującym oknie</string>\n  <string name=\"mark_as_watched\">Oznacz jako obejrzane</string>\n  <string name=\"player_ui_animations\">Uruchom animacje panelu sterowania</string>\n  <string name=\"player_likes_count\">Pokaż licznik polubień</string>\n  <string name=\"sorting_alphabetically2\">Alfabetycznie (szybkie)</string>\n  <string name=\"sorting_default\">Domyślnie (szybkie)</string>\n  <string name=\"content_block_exclude_channel\">Wyklucz bieżący Kanał ze SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">Przestań wykluczać bieżący Kanał ze SponsorBlock</string>\n  <string name=\"player_chapter_notification\">Pokaż powiadomienie, aby szybko przejść do kolejnego rozdziału</string>\n  <string name=\"subtitle_remember\">Zapamiętuj włączenie napisów dla każdego Kanału</string>\n  <string name=\"amazon_frame_drop_fix\">Poprawka dla gubienia klatek #2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">Dla urządzeń Amazon Stick. Może zadziałać też na innych urządzeniach.</string>\n  <string name=\"default_stack_desc\">Używaj wbudowanej biblioteki sieciowej. W określonych okolicznościach może zapewniać lepszą stabilność działania.</string>\n  <string name=\"item_postion\">Pozycja</string>\n  <string name=\"player_screen_off_dimming\">Wartość przyciemnienia ekranu</string>\n  <string name=\"screensaver_timout\">Limit czasu wygaszacza ekranu</string>\n  <string name=\"screensaver_dimming\">Stopień przyciemnienia wygaszacza ekranu</string>\n  <string name=\"player_ui_on_next\">Pokazuj interfejs odtwarzacza podczas przełączania na następne wideo</string>\n  <string name=\"autogenerated\">Automatycznie wygenerowane</string>\n  <string name=\"player_auto_volume\">Automatyczne dostosowanie głośności</string>\n  <string name=\"header_shorts\">Shorts</string>\n  <string name=\"player_global_focus\">Płynna nawigacja między rzędami przycisków odtwarzacza</string>\n  <string name=\"auto_history\">Automatyczne (użyj ustawień konta)</string>\n  <string name=\"remember_position_subscriptions\">Zapamiętaj ostatnio oglądaną pozycję w Subskrypcjach</string>\n  <string name=\"remember_position_pinned\">Zapamiętaj pozycję ostatnio oglądanego wideo w przypiętej liście odtwarzania</string>\n  <string name=\"msg_player_unknown_error\">Nieznany błąd </string>\n  <string name=\"header_notifications\">Powiadomienia</string>\n  <string name=\"disable_search_history\">Wyłącz historię wyszukiwania</string>\n  <string name=\"unlock_high_bitrate_formats\">Odblokuj formaty vp9 z wysokim bitrate 1080p</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Odblokuj formaty mp4a z wysokim bitrate</string>\n  <string name=\"color_scheme_blue\">Niebieski</string>\n  <string name=\"color_scheme_dark_blue\">Granatowy</string>\n  <string name=\"player_speed_per_channel\">Niezależnie dla każdego Kanału</string>\n  <string name=\"disable_popular_searches\">Wyłącz popularne zapytania w wyszukiwaniu</string>\n  <string name=\"multi_profiles\">Używaj oddzielnych ustawień dla każdego konta</string>\n  <string name=\"protect_account_with_password\">Ochrona konta hasłem</string>\n  <string name=\"enter_account_password\">Wprowadź hasło do konta</string>\n  <string name=\"show_connect_messages\">Pokaż komunikaty dot. połączenia</string>\n  <string name=\"prefer_avc_over_vp9_desc\">UWAGA: co najwyżej 1080p</string>\n  <string name=\"play_next\">Odtwórz następnie</string>\n  <string name=\"hide_watched_from_subscriptions\">Ukryj obejrzane wideo w sekcji Subskrypcje</string>\n  <string name=\"hide_watched_from_notifications\">Ukryj obejrzane wideo w sekcji Powiadomienia</string>\n  <string name=\"hide_unwanted_content\">Ukryj niechciane treści</string>\n  <string name=\"hide_watched_from_home\">Ukryj obejrzane wideo w sekcji Główna</string>\n  <string name=\"hide_watched_from_watch_later\">Ukryj obejrzane wideo na liście Do obejrzenia</string>\n  <string name=\"remote_control_permission\">Zdalne sterowanie w tle wymaga uprawnienia Wyświetlanie nad innymi aplikacjami</string>\n  <string name=\"login_from_browser\">Zaloguj przy użyciu przeglądarki</string>\n  <string name=\"disable_remote_history\">Wyłącz historię w trakcie korzystania ze zdalnego sterowania</string>\n  <string name=\"keyboard_fix\">Napraw wyświetlanie klawiatury po naciśnięciu klawisza OK (G20s i inne)</string>\n  <string name=\"nothing_found\">Nic nie znaleziono</string>\n  <string name=\"auto_frame_rate_desc\">Opcja ta ma na celu usunięcie szarpania w scenach, w których kamera szybko się porusza, np. w transmisjach sportowych.</string>\n  <string name=\"channel_filter_hint\">Filtruj Kanały</string>\n  <string name=\"player_loop_shorts\">Zapętl Shorts</string>\n  <string name=\"player_quick_shorts_skip\">Pomijaj Shorts przyciskami w lewo/prawo</string>\n  <string name=\"player_quick_skip_videos\">Pomijaj zwykłe wideo przyciskami w lewo/prawo</string>\n  <string name=\"channels_filter\">Filtrowanie listy kanałów w sekcji Kanały</string>\n  <string name=\"channel_search_bar\">Pasek wyszukiwania w sekcji Kanały</string>\n  <string name=\"header_sports\">Sport</string>\n  <string name=\"keep_finished_activities\">Zachowaj zakończone instancje \\\"playback activity\\\"</string>\n  <string name=\"disable_channels_service\">Wyłącz usługę aktualizacji Kanałów</string>\n  <string name=\"replace_titles\">Podmieniaj tytuły</string>\n  <string name=\"enable\">Włącz</string>\n  <string name=\"more_info\">Więcej informacji</string>\n  <string name=\"about_sponsorblock\">O SponsorBlock</string>\n  <string name=\"about_dearrow\">O DeArrow</string>\n  <string name=\"replace_thumbnails\">Podmień miniatury</string>\n  <string name=\"crowdsourced_thumbnails\">Miniatury współtworzone przez społeczność</string>\n  <string name=\"crowdsoursed_titles\">Tytuły współtworzone przez społeczność</string>\n  <string name=\"dearrow_not_submitted_thumbs\">Zapasowe źródło miniatur</string>\n  <string name=\"pitch_effect\">Wysokość dźwięku</string>\n  <string name=\"fullscreen_mode\">Tryb pełnoekranowy (bez pasków systemowych)</string>\n  <string name=\"player_only_mode\">Pokaż jedynie odtwarzacz, jeśli wideo zostało otwarte spoza aplikacji</string>\n  <string name=\"pinned_channel_rows\">Wyświetlaj przypięte Kanały jako wiersze</string>\n  <string name=\"prefer_ipv4\">Preferuj DNS IPv4</string>\n  <string name=\"prefer_ipv4_desc\">Może pomóc w sytuacji, gdy aplikacja zupełnie nie działa.\\nMoże powodować zawieszanie się lub niespodziewane zakończenie działania (szczególnie na Androidzie 8 lub Dune HD)</string>\n  <string name=\"long_press_for_settings\">przyciśnij długo, by otworzyć ustawienia</string>\n  <string name=\"long_press_for_options\">przyciśnij długo, by otworzyć opcje</string>\n  <string name=\"device_specific_backup\">Kopia zapasowa tylko dla bieżącego urządzenia</string>\n  <string name=\"local_backup\">Lokalna kopia zapasowa</string>\n  <string name=\"auto_backup\">Automatyczna kopia zapasowa (raz dziennie)</string>\n  <string name=\"repeat_mode_reverse_list\">Odtwarzaj listę odtwarzania w odwrotnej kolejności</string>\n  <string name=\"speech_engine\">Silnik wyszukiwania głosowego</string>\n  <string name=\"not_supported_by_device\">Urządzenie nie obsługuje tej funkcji</string>\n  <string name=\"video_buffer_size_lowest\">Najmniejszy</string>\n  <string name=\"video_buffer_size_highest\">Największy</string>\n  <string name=\"unpin_group_from_sidebar\">Usuń grupę Subskrypcji</string>\n  <string name=\"context_menu_sorting\">Sortowanie menu kontekstowego</string>\n  <string name=\"hide_shorts_from_search\">Ukryj Shorts w wynikach wyszukiwania</string>\n  <string name=\"suggestions\">Sugestie</string>\n  <string name=\"place_comments_left\">Umieść komentarze po lewej stronie</string>\n  <string name=\"unknown_source_error\">Nieznany błąd źródła</string>\n  <string name=\"unknown_renderer_error\">Nieznany błąd renderowania</string>\n  <string name=\"calm_msg\">Pracujemy nad poprawką. Regularnie sprawdzaj aktualizacje</string>\n  <string name=\"without_picture\">Bez obrazu</string>\n  <string name=\"video_disabled\">Wideo wyłączone</string>\n  <string name=\"applying_fix\">Nie zamykaj odtwarzacza. Inicjowanie poprawki...</string>\n  <string name=\"hide_mixes\">Ukryj Miksy</string>\n  <string name=\"player_global_focus_desc\">Ustawienie decyduje o tym, który przycisk odtwarzacza otrzyma fokus podczas nawigacji między rzędami przycisków odtwarzacza</string>\n  <string name=\"disable_network_error_fixing\">Wyłącz automatyczne naprawianie błędów sieci</string>\n  <string name=\"disable_network_error_fixing_desc\">Aktywowanie tej opcji jest zwykle konieczne w przypadku korzystania z VPN</string>\n  <string name=\"recommended\">Rekomendowane</string>\n  <string name=\"add_to_subscriptions_group\">Dodaj/Usuń z grupy Subskrypcji</string>\n  <string name=\"new_subscriptions_group\">Nowa grupa</string>\n  <string name=\"rename_group\">Zmiana nazwy grupy Subskrypcji</string>\n  <string name=\"screen_dimming_amount\">Stopień przyciemniania ekranu</string>\n  <string name=\"screen_dimming_timeout\">Opóźnienie przyciemniania ekranu</string>\n  <string name=\"playlists_rows\">Pokaż sekcję list odtwarzania jako wiersze</string>\n  <string name=\"import_subscriptions_group\">Importuj</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">Wyłącz napisy wkodowane</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">Włącz napisy wkodowane</string>\n  <string name=\"my_videos\">Moje wideo</string>\n  <string name=\"player_audio_focus\">Automatyzacja audio (pauza w przypadku wykrycia innych odtwarzaczy)</string>\n  <string name=\"paid_content_notification\">Powiadomienia o płatnych treściach</string>\n  <string name=\"you_liked\">polubionych</string>\n  <string name=\"premium_users_only\">Tylko dla użytkowników Premium. Poprawka niekompletnej listy formatów wideo</string>\n  <string name=\"playback_buffering_fix\">Poprawka bufora odtwarzania</string>\n  <string name=\"oculus_quest_fix\">Poprawka dla Oculus Quest</string>\n  <string name=\"card_preview_muted\">Wideo bez dźwięku</string>\n  <string name=\"card_preview_full\">Wideo z dźwiękiem</string>\n  <string name=\"card_preview\">Podgląd karty</string>\n  <string name=\"search_exit_shortcut\">Zamykanie wyszukiwania</string>\n  <string name=\"card_unlocalized_titles\">Oryginalne tytuły materiałów wideo</string>\n  <string name=\"original_lang\">Oryginalny</string>\n  <string name=\"video_flip\">Odbicie lustrzane</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">Nie zmieniaj rozmiaru wideo, by zmieścić napisy</string>\n  <string name=\"typing_corrections\">Wyłącz autokorektę podczas pisania</string>\n  <string name=\"suggestions_horizontally_scrolled\">Przewijaj sugestie w poziomie</string>\n  <string name=\"stable_restore\">Przywróć stabilną wersję programu (wszystkie dane zostaną utracone)</string>\n  <string name=\"auto_backup_category\">Automatyczna kopia zapasowa</string>\n  <string name=\"once_a_day\">Raz na dzień</string>\n  <string name=\"once_a_week\">Raz w tygodniu</string>\n  <string name=\"once_a_month\">Raz w miesiącu</string>\n  <string name=\"create_playlist_note\">Nie będzie widoczne w aplikacji YouTube</string>\n  <string name=\"remove_playlist_fmt\">Usunąć %s na zawsze\\?</string>\n  <string name=\"play_from_start\">Odtwórz od początku</string>\n  <string name=\"player_chapter_notification2\">Przejdź do następnego rozdziału, klikając powiadomienie</string>\n  <string name=\"player_toggle_speed\">Przełącz prędkość odtwarzania</string>\n  <string name=\"player_quick_shorts_skip_alt\">Pomijaj Shorts przyciskami w góra/dół</string>\n  <string name=\"player_quick_skip_videos_alt\">Pomijaj zwykłe wideo przyciskami w góra/dół</string>\n  <string name=\"prefer_google_dns\">Preferuj Google DNS</string>\n  <string name=\"action_debug_info\">Informacje debugowania</string>\n  <string name=\"dialog_block_channel\">Zablokuj kanał</string>\n  <string name=\"dialog_unblock_channel\">Odblokuj kanał</string>\n  <string name=\"confirm_block_channel\">Ukryć całą zawartość od %s?</string>\n  <string name=\"channel_blocked\">Kanał zablokowany</string>\n  <string name=\"channel_unblocked\">Kanał odblokowany</string>\n  <string name=\"header_blocked_channels\">Zablokowane kanały</string>\n  <string name=\"msg_no_blocked_channels\">Brak zablokowanych kanałów</string>\n  <string name=\"player_time_stretching_desc\">Zapewnia naturalny dźwięk przy zmianie prędkości. Może znacznie obniżyć wydajność niektórych urządzeń.</string>\n  <string name=\"player_time_stretching\">Korekcja czasu w sygnałach audio</string>\n  <string name=\"queue_respects_playback_mode\">Kolejka respektuje tryb odtwarzania</string>\n  <string name=\"ignore_short_segments\">Pomiń krótkie segmenty</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-pt-rBR/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"header_home\">Início</string>\n    <string name=\"title_search\">Pesquisar</string>\n    <string name=\"header_subscriptions\">Inscrições</string>\n    <string name=\"header_history\">Histórico</string>\n    <string name=\"header_music\">Músicas</string>\n    <string name=\"header_news\">Notícias</string>\n    <string name=\"header_gaming\">Jogos</string>\n    <string name=\"header_playlists\">Biblioteca</string>\n    <string name=\"header_settings\">Configurações</string>\n    <string name=\"header_channels\">Canais</string>\n    <string name=\"subscriptions_signin_title\">Veja as novidades dos canais que você adora</string>\n    <string name=\"subscriptions_signin_subtitle\">Faça login para ver suas inscrições</string>\n    <string name=\"subscriptions_signin_button_text\">Fazer login</string>\n    <string name=\"library_signin_to_show_more\">Assistir a vídeos que você gostou, salvou ou se inscreveu</string>\n    <string name=\"library_signin_subtitle\">Faça login para ver sua biblioteca</string>\n    <string name=\"action_signin\">Fazer login</string>\n    <string name=\"title_video_formats\">Formatos/Qualidades de vídeo</string>\n    <string name=\"title_audio_formats\">Formatos de áudio</string>\n    <string name=\"subtitle_category_title\">Legendas</string>\n    <string name=\"playback_queue_category_title\">Fila de reprodução (playback)</string>\n    <string name=\"video_max_quality\">Automático (qualidade máxima)</string>\n    <string name=\"audio_max_quality\">Automático (qualidade máxima)</string>\n    <string name=\"subtitles_disabled\">Legendas desativadas</string>\n    <string name=\"auto_frame_rate\">Taxa de quadros automática</string>\n    <string name=\"frame_rate_correction\">Corrigir FPS:\\n%s</string>\n    <string name=\"category_background_playback\">Reprodução em segundo plano</string>\n    <string name=\"not_implemented\">Não implementado</string>\n    <string name=\"not_supported_by_device\">O dispositivo não suporta esse recurso</string>\n    <string name=\"option_background_playback_off\">Desativado</string>\n    <string name=\"option_background_playback_pip\">Picture in Picture</string>\n    <string name=\"option_background_playback_only_audio\">Somente áudio</string>\n    <string name=\"playback_settings\">Configurações de qualidade de reprodução</string>\n    <string name=\"video_speed\">Velocidade do vídeo</string>\n    <string name=\"resolution_switch\">Alterar resolução</string>\n    <string name=\"video_buffer\">Buffer de vídeo</string>\n    <string name=\"video_buffer_size_none\">Nenhum</string>\n    <string name=\"video_buffer_size_lowest\">Mais baixo</string>\n    <string name=\"video_buffer_size_low\">Baixo</string>\n    <string name=\"video_buffer_size_med\">Médio</string>\n    <string name=\"video_buffer_size_high\">Alto</string>\n    <string name=\"video_buffer_size_highest\">Mais alto</string>\n    <string name=\"update_changelog\">Registro de alterações</string>\n    <string name=\"install_update\">Instalar atualização</string>\n    <string name=\"section_is_empty\">Ops! Não há nada aqui</string>\n    <string name=\"dialog_add_to_playlist\">Adicionar/Remover da lista de reprodução (playlist)</string>\n    <string name=\"msg_signed_users_only\">Apenas usuários logados</string>\n    <string name=\"msg_cant_load_content\">Não foi possível carregar o conteúdo.\\n1) Verifique a data e a hora em seu dispositivo.\\n2) Verifique a conexão de rede.\\n3) Verifique suas configurações de proxy.\\n4) Tente fazer login em sua conta.\\n5) Talvez não haja nada de errado por aqui.</string>\n    <string name=\"title_video_presets\">Predefinições de vídeo</string>\n    <string name=\"settings_accounts\">Contas</string>\n    <string name=\"settings_left_panel\">Editar categorias</string>\n    <string name=\"settings_themes\">Temas</string>\n    <string name=\"settings_other\">Outros</string>\n    <string name=\"settings_player\">Reprodutor (player) de vídeo</string>\n    <string name=\"settings_language\">Idioma</string>\n    <string name=\"settings_linked_devices\">Dispositivos vinculados</string>\n    <string name=\"settings_about\">Sobre</string>\n    <string name=\"dialog_account_list\">Selecionar conta</string>\n    <string name=\"dialog_account_none\">Nenhuma</string>\n    <string name=\"dialog_remove_account\">Sair</string>\n    <string name=\"dialog_add_account\">Fazer login</string>\n    <string name=\"default_lang\">Padrão</string>\n    <string name=\"original_lang\">Original</string>\n    <string name=\"dialog_select_language\">Idioma</string>\n    <string name=\"subtitle_default\">Padrão</string>\n    <string name=\"subtitle_white_semi_transparent\">Branca com fundo semitransparente</string>\n    <string name=\"subtitle_style\">Estilo da legenda</string>\n    <string name=\"subtitle_language\">Idioma da legenda</string>\n    <string name=\"action_search\">Pesquisar</string>\n    <string name=\"settings_main_ui\">Interface de usuário</string>\n    <string name=\"dialog_main_ui\">Interface de usuário</string>\n    <string name=\"card_animated_previews\">Prévias animadas</string>\n    <string name=\"web_site\">Site</string>\n    <string name=\"donation\">Doação</string>\n    <string name=\"dialog_about\">Sobre</string>\n    <string name=\"dialog_player_ui\">Reprodutor (player) de vídeo</string>\n    <string name=\"player_show_ui_on_pause\">Exibir interface ao pausar</string>\n    <string name=\"player_pause_on_ok\">A tecla OK pausa a reprodução</string>\n    <string name=\"player_ok_button_behavior\">Função do botão \\\"OK\\\"</string>\n    <string name=\"player_only_ui\">Somente interface</string>\n    <string name=\"player_ui_and_pause\">Interface e pausar</string>\n    <string name=\"player_only_pause\">Somente pausar</string>\n    <string name=\"player_toggle_speed\">Toggle speed on/off</string>\n    <string name=\"check_for_updates\">Verificar se há atualizações</string>\n    <string name=\"update_not_found\">Você já está utilizando a versão mais recente</string>\n    <string name=\"update_in_progress\">Aguarde…</string>\n    <string name=\"player_ui_hide_behavior\">Ocultar automaticamente a interface</string>\n    <string name=\"option_never\">Nunca</string>\n    <string name=\"side_panel_sections\">Adicionar ou excluir seções</string>\n    <string name=\"boot_to_section\">Iniciar pela seção</string>\n    <string name=\"large_ui\">Interface grande</string>\n    <string name=\"video_grid_scale\">Escala da grade de vídeos</string>\n    <string name=\"scale_ui\">Escala da interface</string>\n    <string name=\"color_scheme\">Esquema de cores</string>\n    <string name=\"color_scheme_default\">Padrão</string>\n    <string name=\"color_scheme_red_grey\">Vermelho e cinza</string>\n    <string name=\"color_scheme_red\">Vermelho</string>\n    <string name=\"color_scheme_dark_grey\">Cinza escuro</string>\n    <string name=\"disable_update_check\">Desativar a verificação de atualizações</string>\n    <string name=\"show_again\">Mostrar novamente</string>\n    <string name=\"check_updates_auto\">Notificar sobre novas versões</string>\n    <string name=\"select_account_on_boot\">Selecionar na inicialização</string>\n    <string name=\"player_other\">Outras configurações</string>\n    <string name=\"player_full_date\">Data precisa na descrição</string>\n    <string name=\"open_channel\">Visualizar canal</string>\n    <string name=\"open_playlist\">Visualizar lista de reprodução (playlist)</string>\n    <string name=\"not_interested\">Não tenho interesse</string>\n    <string name=\"not_recommend_channel\">Não recomendar esse canal</string>\n    <string name=\"you_wont_see_this_video\">O vídeo foi removido dos recomendados</string>\n    <string name=\"you_wont_see_this_channel\">Você não verá esse canal nas recomendações</string>\n    <string name=\"settings_search\">Pesquisa</string>\n    <string name=\"dialog_search\">Pesquisar</string>\n    <string name=\"instant_voice_search\">Pesquisa por voz instantânea</string>\n    <string name=\"option_background_playback_behind\">Reproduzir em segundo plano</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">As informações do próximo vídeo ainda não foram carregadas</string>\n    <string name=\"card_multiline_title\">Títulos em múltiplas linhas</string>\n    <string name=\"cards_style\">Estilo das miniaturas</string>\n    <string name=\"repeat_mode_all\">Reproduzir vídeos ininterruptamente</string>\n    <string name=\"repeat_mode_one\">Repetir o vídeo atual</string>\n    <string name=\"repeat_mode_pause\">Pausar a reprodução após cada vídeo</string>\n    <string name=\"repeat_mode_pause_alt\">Reproduzir apenas vídeos de listas de reprodução (playlists) ininterruptamente</string>\n    <string name=\"repeat_mode_none\">Parar a reprodução após um vídeo</string>\n    <string name=\"subscribed_to_channel\">Inscreveu-se</string>\n    <string name=\"unsubscribed_from_channel\">Desinscreveu-se</string>\n    <string name=\"subtitle_yellow_transparent\">Amarela com fundo transparente</string>\n    <string name=\"subtitle_white_transparent\">Branca com fundo transparente</string>\n    <string name=\"subtitle_white_black\">Branca com fundo preto</string>\n    <string name=\"player_seek_preview\">Pré-visualizar durante avanço/retrocesso</string>\n    <string name=\"color_scheme_dark_grey_oled\">Cinza escuro (OLED)</string>\n    <string name=\"channels_section_sorting\">Ordenação da seção \\\"Canais\\\"</string>\n    <string name=\"sorting_by_new_content\">Novos conteúdos primeiro</string>\n    <string name=\"sorting_alphabetically\">Por ordem alfabética</string>\n    <string name=\"sorting_last_viewed\">Visualizados recentemente primeiro</string>\n    <string name=\"player_pause_when_seek\">Pausar enquanto avança/retrocede</string>\n    <string name=\"playlists_style\">Estilo da seção \\\"Bibliotecas\\\"</string>\n    <string name=\"playlists_style_grid\">Grade</string>\n    <string name=\"playlists_style_rows\">Fileiras</string>\n    <string name=\"player_show_clock\">Mostrar relógio na barra de controles</string>\n    <string name=\"player_show_remaining_time\">Mostrar o tempo restante na barra de controles</string>\n    <string name=\"player_show_ending_time\">Mostrar tempo de término na barra de controles</string>\n    <string name=\"open_channel_uploads\">Abrir os carregamentos (uploads) do canal</string>\n    <string name=\"app_exit_shortcut\">Sair do aplicativo</string>\n    <string name=\"player_exit_shortcut\">Sair do reprodutor (player) de vídeos</string>\n    <string name=\"search_exit_shortcut\">Sair da pesquisa</string>\n    <string name=\"app_exit_none\">Não sair</string>\n    <string name=\"app_double_back_exit\">Toque duplo no botão \\\"Voltar\\\"</string>\n    <string name=\"app_single_back_exit\">Toque único no botão \\\"Voltar\\\"</string>\n    <string name=\"action_video_zoom\">Zoom do vídeo</string>\n    <string name=\"video_zoom\">Zoom do vídeo</string>\n    <string name=\"video_zoom_default\">Padrão</string>\n    <string name=\"video_zoom_fit_width\">Ajustar largura</string>\n    <string name=\"video_zoom_fit_height\">Ajustar altura</string>\n    <string name=\"video_zoom_fit_both\">Ajustar largura ou altura</string>\n    <string name=\"video_zoom_stretch\">Esticar</string>\n    <string name=\"color_scheme_teal\">Azul petróleo</string>\n    <string name=\"color_scheme_teal_oled\">Azul petróleo (OLED)</string>\n    <string name=\"player_seek_preview_none\">Desativado</string>\n    <string name=\"player_seek_preview_single\">Quadro único</string>\n    <string name=\"player_seek_preview_carousel\">Carrossel</string>\n    <string name=\"player_seek_preview_carousel_slow\">Carrossel (lento, visualização por quadros-chaves)</string>\n    <string name=\"player_seek_preview_carousel_fast\">Carrossel (rápido, visualização imprecisa)</string>\n    <string name=\"unsubscribe_from_channel\">Cancelar inscrição do canal</string>\n    <string name=\"card_title_lines_num\">Número de linhas dos títulos das miniaturas</string>\n    <string name=\"card_auto_scrolled_title\">Rolagem automática de títulos</string>\n    <string name=\"share_link\">Compartilhar link</string>\n    <string name=\"share_qr_link\">Compartilhar link (QR code)</string>\n    <string name=\"subscribe_to_channel\">Inscrever-se no canal</string>\n    <string name=\"wait_data_loading\">Por favor, aguarde enquanto os dados são carregados...</string>\n    <string name=\"auto_frame_rate_pause\">Taxa de Quadros Automática (pausar ao trocar)</string>\n    <string name=\"auto_frame_rate_applying\">Aplicando taxa de quadros automática %sx%s\\@%s</string>\n    <string name=\"audio_shift\">Sincronização de áudio</string>\n    <string name=\"player_remember_speed\">Lembrar velocidade</string>\n    <string name=\"mark_channel_as_watched\">Marcar como assistido</string>\n    <string name=\"channel_marked_as_watched\">O canal foi marcado como assistido</string>\n    <string name=\"dialog_add_device\">Adicionar dispositivo</string>\n    <string name=\"dialog_remove_all_devices\">Remover todos os dispositivos</string>\n    <string name=\"device_link_enabled\">Conexão ativada</string>\n    <string name=\"device_connected\">Dispositivo \\\"%s\\\" foi conectado</string>\n    <string name=\"device_disconnected\">Dispositivo \\\"%s\\\" foi desconectado</string>\n    <string name=\"settings_ui_scale\">Escala de interface</string>\n    <string name=\"msg_restart_app\">Por favor, reinicie o aplicativo para aplicar essas configurações</string>\n    <string name=\"settings_block\">Bloqueio de conteúdo</string>\n    <string name=\"msg_applying\">Aplicando %s…</string>\n    <string name=\"msg_done\">Concluído</string>\n    <string name=\"settings_general\">Gerais</string>\n    <string name=\"settings_video\">Vídeo</string>\n    <string name=\"content_block_confirm_skip\">Confirmar ao pular</string>\n    <string name=\"content_block_categories\">Pular segmentos</string>\n    <string name=\"content_block_sponsor\">Patrocinador</string>\n    <string name=\"content_block_intro\">Introdução/animação de intervalo</string>\n    <string name=\"content_block_outro\">Créditos finais/finalização</string>\n    <string name=\"content_block_interaction\">Lembrete de interação (inscrição)</string>\n    <string name=\"content_block_self_promo\">Não-pago/Autopromoção</string>\n    <string name=\"content_block_music_off_topic\">Música: Seção sem música</string>\n    <string name=\"confirm_segment_skip\">Pular segmento \\\"%s\\\"\\?</string>\n    <string name=\"cancel_segment_skip\">Cancelar pulo do segmento</string>\n    <string name=\"msg_skipping_segment\">Pulando segmento \\\"%s\\\"…</string>\n    <string name=\"content_block_notification_type\">Tipo de notificação</string>\n    <string name=\"content_block_notify_none\">Sem notificação</string>\n    <string name=\"content_block_notify_toast\">Notificação toast</string>\n    <string name=\"content_block_notify_dialog\">Diálogo de confirmação</string>\n    <string name=\"return_to_launcher\">Retornar dos canais da Android TV para a tela de início/pesquisa</string>\n    <string name=\"intent_force_close\">Forçar saída se o vídeo tiver sido aberto para uma fonte externa</string>\n    <string name=\"btn_confirm\">Confirmar</string>\n    <string name=\"player_low_video_quality\">Qualidade de vídeo baixa</string>\n    <string name=\"remote_session_closed\">A sessão remota foi encerrada</string>\n    <string name=\"msg_mode_switch_error\">Não é possível alterar o modo de exibição para \\\"%s\\\"</string>\n    <string name=\"msg_player_error\">Ocorreu um erro %s. Reiniciando a reprodução…</string>\n    <string name=\"video_preset_disabled\">Sem predefinição</string>\n    <string name=\"video_preset_enabled\">O formato do próximo vídeo será definido de acordo com a predefinição</string>\n    <string name=\"player_sleep_timer\">Temporizador de sono</string>\n    <string name=\"player_show_quality_info\">Mostrar informação de qualidade na barra de controles</string>\n    <string name=\"player_remember_each_speed\">Lembrar a velocidade de cada vídeo</string>\n    <string name=\"player_remember_speed_none\">Nunca</string>\n    <string name=\"player_remember_speed_all\">A mesma velocidade em todos os vídeos</string>\n    <string name=\"player_remember_speed_each\">Uma velocidade para cada vídeo</string>\n    <string name=\"msg_player_error_source\">Não é possível baixar o vídeo</string>\n    <string name=\"msg_player_error_renderer\">Algo no vídeo selecionado não é suportado.\\nTente selecionar outro.\\nSe isso não ajuda tente reiniciar o seu dispositivo.</string>\n    <string name=\"msg_player_error_video_renderer\">Formato de vídeo não suportado.\\ntente selecionar outro formato de vídeo pressionando o botão \\\"HQ\\\" no reprodutor (player) de vídeos.\\nOu tente reiniciar o dispositivo.</string>\n    <string name=\"msg_player_error_audio_renderer\">Formato de áudio não suportado.\\nTente selecionar outro formato de áudio pressionando o botão \\\"HQ\\\" reprodutor (player) de vídeos.\\nOu tente reiniciar o seu dispositivo.</string>\n    <string name=\"msg_player_error_subtitle_renderer\">Formato de legenda não suportado.\\nTente selecionar outro formato pressionando o botão \\\"CC\\\" no reprodutor (player) de vídeos.\\nOu tente reiniciar o seu dispositivo.</string>\n    <string name=\"msg_player_error_unexpected\">Erro de decodificação de vídeo desconhecido</string>\n    <string name=\"video_aspect\">Proporção da tela</string>\n    <string name=\"player_tweaks\">Opções de desenvolvedor</string>\n    <string name=\"header_uploads\">Carregamentos (Uploads)</string>\n    <string name=\"player_show_global_clock\">Sempre exibir o relógio na tela</string>\n    <string name=\"player_show_global_ending_time\">Sempre exibir o tempo de término na tela</string>\n    <string name=\"app_corner_clock\">Início: relógio no canto superior direito</string>\n    <string name=\"player_corner_clock\">Reprodutor (player): relógio no canto superior direito</string>\n    <string name=\"player_corner_ending_time\">Reprodutor (player): hora de término no canto superior direito</string>\n    <string name=\"uploads_old_look\">Voltar para a aparência antiga da seção \\\"Carregamentos (Uploads)\\\"</string>\n    <string name=\"background_playback_activation\">Reprodução em segundo plano (ativar/desativar)</string>\n    <string name=\"channels_old_look\">Voltar para a aparência antiga da seção \\\"Canais\\\"</string>\n    <string name=\"channels_auto_load\">Carregar conteúdo da seção \\\"Canais\\\" automaticamente</string>\n    <string name=\"return_to_background_video\">Voltar para o vídeo que está em segundo plano</string>\n    <string name=\"pin_unpin_from_sidebar\">Adicionar/Remover da barra lateral</string>\n    <string name=\"pin_unpin_playlist\">Adicionar/Remover lista de reprodução (playlist) da barra lateral</string>\n    <string name=\"pin_unpin_channel\">Adicionar/Remover canal da barra lateral</string>\n    <string name=\"pin_playlist\">Adicionar lista de reprodução (playlist) na barra lateral</string>\n    <string name=\"pin_channel\">Adicionar canal na barra lateral</string>\n    <string name=\"pinned_to_sidebar\">Adicionado à barra lateral</string>\n    <string name=\"unpin_from_sidebar\">Remover da barra lateral</string>\n    <string name=\"unpin_group_from_sidebar\">Remover inscrição do grupo</string>\n    <string name=\"unpinned_from_sidebar\">Removido da barra lateral</string>\n    <string name=\"dialog_select_country\">País</string>\n    <string name=\"settings_language_country\">Idioma/País</string>\n    <string name=\"share_embed_link\">Compartilhar link de incorporação</string>\n    <string name=\"card_text_scroll_factor\">Velocidade de rolagem do texto da miniatura</string>\n    <string name=\"preferred_update_source\">Selecionar a fonte de atualização</string>\n    <string name=\"hide_shorts\">Ocultar os shorts (vídeos curtos) nas suas inscrições</string>\n    <string name=\"hide_shorts_channel\">Ocultar os shorts (vídeos curtos) nos canais</string>\n    <string name=\"key_remapping\">Remapeamento de botões</string>\n    <string name=\"screen_dimming\">Escurecer a tela</string>\n    <string name=\"removed_from_playback_queue\">Removido da fila de reprodução (playback)</string>\n    <string name=\"added_to_playback_queue\">Adicionado à fila de reprodução (playback)</string>\n    <string name=\"add_remove_from_playback_queue\">Adicionar/Remover da fila de reprodução (playback)</string>\n    <string name=\"add_to_playback_queue\">Adicionar à fila de reprodução (playback)</string>\n    <string name=\"remove_from_playback_queue\">Remover da fila de reprodução (playback)</string>\n    <string name=\"proxy_enabled\">Proxy habilitado</string>\n    <string name=\"proxy_disabled\">Proxy desabilitado</string>\n    <string name=\"uploads_row_name\">Carregamentos (Uploads)</string>\n    <string name=\"playlists_row_name\">Listas de reprodução (playlists) criadas</string>\n    <string name=\"popular_uploads_row_name\">Carregamentos (uploads) populares</string>\n    <string name=\"news_row_name\">Noticias</string>\n    <string name=\"breaking_news_row_name\">Últimas notícias</string>\n    <string name=\"covid_news_row_name\">Notícias sobre o COVID-19</string>\n    <string name=\"enable_voice_search\">Ativar pesquisa global (é necessário suporte de firmware)</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">Inscrever-se/Cancelar inscrição do canal</string>\n    <string name=\"app_backup_restore\">Salvar/Restaurar cópia de segurança</string>\n    <string name=\"app_backup\">Salvar cópia das configurações</string>\n    <string name=\"app_restore\">Restaurar cópia das configurações</string>\n    <string name=\"skip_each_segment_once\">Não pular segmentos novamente</string>\n    <string name=\"disable_ok_long_press\">Desativar o botão \\\"OK\\\" ao mantê-lo pressionado</string>\n    <string name=\"disable_ok_long_press_desc\">Voltado para controles em que o botão \\\"OK\\\" não funciona corretamente.</string>\n    <string name=\"player_time_correction\">Exibir o tempo respeitando a velocidade</string>\n    <string name=\"sponsor_color_markers\">Marcadores de cor na barra de progresso</string>\n    <string name=\"refresh_section\">Atualizar seção</string>\n    <string name=\"option_disabled\">Desativado</string>\n    <string name=\"live_now_row_name\">Ao vivo agora</string>\n    <string name=\"run_in_background\">Executar em segundo plano</string>\n    <string name=\"settings_remote_control\">Transmissão</string>\n    <string name=\"background_service_started\">Serviço em segundo plano inicializado</string>\n    <string name=\"dialog_add_to\">Adicionar para %s</string>\n    <string name=\"dialog_remove_from\">Remover de %s</string>\n    <string name=\"added_to\">Adicionado para %s</string>\n    <string name=\"removed_from\">Removido de %s</string>\n    <string name=\"video_preset_adaptive\">Adaptativo</string>\n    <string name=\"content_block_preview_recap\">Prévia/Recapitulação do vídeo</string>\n    <string name=\"content_block_highlight\">Destaques (Às vezes não funciona!)</string>\n    <string name=\"removed_from_history\">O vídeo foi removido do histórico</string>\n    <string name=\"remove_from_history\">Remover do histórico</string>\n    <string name=\"upload_date\">Data de carregamento (upload)</string>\n    <string name=\"upload_date_any\">Todo tempo</string>\n    <string name=\"upload_date_today\">Hoje</string>\n    <string name=\"upload_date_this_week\">Essa semana</string>\n    <string name=\"upload_date_this_month\">Esse mês</string>\n    <string name=\"upload_date_this_year\">Esse ano</string>\n    <string name=\"double_refresh_rate\">Taxa de atualização dupla</string>\n    <string name=\"upload_date_last_hour\">Última hora</string>\n    <string name=\"focus_on_search_results\">Foco automático nos resultados de pesquisa</string>\n    <string name=\"context_menu\">Menu de contexto</string>\n    <string name=\"context_menu_sorting\">Ordenação do menu de contexto</string>\n    <string name=\"add_remove_from_recent_playlist\">Adicionar/Remover da lista de reprodução (playlist) recente</string>\n    <string name=\"hide_settings_section\">Ocultar a seção \\\"Configurações\\\" (perigoso!)</string>\n    <string name=\"volume\">Volume %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s seg</string>\n    <string name=\"audio_shift_sec\">%s seg</string>\n    <string name=\"ui_hide_timeout_sec\">%s seg</string>\n    <string name=\"screen_dimming_timeout_min\">%s min</string>\n    <string name=\"mark_all_channels_watched\">Marcar todos os canais como assistidos</string>\n    <string name=\"move_section_up\">Mover seção para cima</string>\n    <string name=\"move_section_down\">Mover seção para baixo</string>\n    <string name=\"player_buttons\">Definir botões do reprodutor (player)</string>\n    <string name=\"action_playlist_add\">Salvar na lista de reprodução (playlist)</string>\n    <string name=\"action_video_stats\">Estatísticas de vídeo</string>\n    <string name=\"action_subscribe\">Inscrever-se</string>\n    <string name=\"action_channel\">Abrir canal</string>\n    <string name=\"action_pip\">PIP</string>\n    <string name=\"action_video_info\">Descrição do vídeo</string>\n    <string name=\"action_screen_off\">Desligar/Desativar a tela</string>\n    <string name=\"action_sound_off\">Mudo</string>\n    <string name=\"action_playback_queue\">Fila de reprodução (playback)</string>\n    <string name=\"action_video_speed\">Velocidade do vídeo</string>\n    <string name=\"action_subtitles\">Legendas</string>\n    <string name=\"action_like\">Gostei</string>\n    <string name=\"action_dislike\">Não Gostei</string>\n    <string name=\"action_play_pause\">Reproduzir/Pausar</string>\n    <string name=\"action_repeat_mode\">Modo de reprodução</string>\n    <string name=\"action_next\">Próximo vídeo</string>\n    <string name=\"action_previous\">Vídeo anterior</string>\n    <string name=\"action_high_quality\">Qualidade do vídeo</string>\n    <string name=\"content_block_no_skipping_mode\">Modo sem pulos</string>\n    <string name=\"content_block_action_type\">Escolher ação</string>\n    <string name=\"content_block_action_none\">Não fazer nada</string>\n    <string name=\"content_block_action_only_skip\">Somente pular</string>\n    <string name=\"content_block_action_toast\">Pular com notificação</string>\n    <string name=\"content_block_action_dialog\">Mostrar diálogo de confirmação</string>\n    <string name=\"content_block_filler\">Enrolação/Piadas</string>\n    <string name=\"keyboard_auto_show\">Mostrar teclado automaticamente</string>\n    <string name=\"cancel_dialog\">Cancelar</string>\n    <string name=\"msg_player_error_source2\">Este endereço (URL) não está funcionando ou a hora do dispositivo está incorreta.\\nNormalmente, é um problema no aplicativo.</string>\n    <string name=\"msg_player_error_video_source\">Este endereço (URL) do vídeo não está funcionando ou a hora do dispositivo está incorreta.\\nNormalmente, é um problema no aplicativo.</string>\n    <string name=\"msg_player_error_audio_source\">Este endereço (URL) do áudio não está funcionando ou a hora do dispositivo está incorreta.\\nNormalmente, é um problema no aplicativo.</string>\n    <string name=\"msg_player_error_subtitle_source\">Este endereço (URL) da legenda não está funcionando ou a hora do dispositivo está incorreta.\\nNormalmente, é um problema no aplicativo.</string>\n    <string name=\"hide_upcoming\">Ocultar \\\"Em breve\\\" da seção \\\"Inscrições\\\"</string>\n    <string name=\"hide_upcoming_channel\">Ocultar \\\"Em breve\\\" da seção \\\"Canais\\\"</string>\n    <string name=\"hide_upcoming_home\">Ocultar \\\"Em breve\\\" da seção \\\"Início\\\"</string>\n    <string name=\"search_background_playback\">Reprodução em segundo plano durante a pesquisa ou navegação em algum canal</string>\n    <string name=\"trending_row_name\">Em alta</string>\n    <string name=\"player_seek_type\">Comportamento do avanço/retrocesso</string>\n    <string name=\"player_seek_regular\">Normal</string>\n    <string name=\"player_seek_confirmation_pause\">Com confirmação (pausa enquanto avança/retrocede)</string>\n    <string name=\"player_seek_confirmation_play\">Com confirmação (reproduz enquanto avança/retrocede)</string>\n    <string name=\"hide_shorts_from_home\">Ocultar os shorts (vídeos curtos) na seção \\\"Início\\\"</string>\n    <string name=\"finish_on_disconnect\">Fechar o aplicativo após desconectar o telefone/tablet</string>\n    <string name=\"update_error\">Erro de atualização</string>\n    <string name=\"hide_shorts_from_search\">Ocultar os shorts (vídeos curtos) nos resultados da pesquisa</string>\n    <string name=\"hide_shorts_from_history\">Ocultar os shorts (vídeos curtos) na seção \\\"Histórico\\\"</string>\n    <string name=\"hide_shorts_from_trending\">Ocultar os shorts (vídeos curtos) na seção \\\"Em alta\\\"</string>\n    <string name=\"disable_screensaver\">Desativar o protetor de tela</string>\n    <string name=\"description_not_found\">Descrição não encontrada</string>\n    <string name=\"proxy_port_hint\">Porta do proxy</string>\n    <string name=\"proxy_host_hint\">Nome do host ou IP do proxy</string>\n    <string name=\"proxy_username_hint\">Nome de usuário do proxy</string>\n    <string name=\"proxy_password_hint\">Senha do proxy</string>\n    <string name=\"proxy_type\">Tipo do proxy</string>\n    <string name=\"enable_web_proxy\">Utilizar o Web Proxy</string>\n    <string name=\"proxy_not_supported\">Utilizar o Web Proxy (necessário Android 4.4+)</string>\n    <string name=\"proxy_test_btn\">Testar</string>\n    <string name=\"proxy_settings_title\">Configurações do servidor proxy</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">Teste #%d: cancelado.</string>\n    <string name=\"proxy_type_invalid\">Tipo do proxy não definido.</string>\n    <string name=\"proxy_host_invalid\">Host do proxy não definido.</string>\n    <string name=\"proxy_port_invalid\">Porta do proxy inválida, deve ser &gt; 0.</string>\n    <string name=\"proxy_credentials_invalid\">Nome de usuário ou senha inválidos.</string>\n    <string name=\"proxy_test_aborted\">Teste abortado, por favor, corrija as configurações de proxy primeiro.</string>\n    <string name=\"proxy_application_aborted\">Por favor, corrija as configurações de proxy primeiro.</string>\n    <string name=\"proxy_test_start\">Teste #%d: %s...</string>\n    <string name=\"proxy_test_error\">Erro #%d: %s</string>\n    <string name=\"proxy_test_status\">Status #%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">Endereço de arquivo Config (*.ovpn)</string>\n    <string name=\"enable_openvpn\">Utilizar a OpenVPN</string>\n    <string name=\"openvpn_settings_title\">Configurações da OpenVPN</string>\n    <string name=\"openvpn_application_aborted\">Por favor, corrija as configurações da OpenVPN primeiro.</string>\n    <string name=\"openvpn_test_aborted\">Teste abortado, por favor, corrija as configurações do OpenVPN primeiro.</string>\n    <string name=\"openvpn_address_invalid\">Endereço de configuração da OpenVPN não definido.</string>\n    <string name=\"internet_censorship\">Censura da internet</string>\n    <string name=\"rename_section\">Renomear essa seção</string>\n    <string name=\"simple_edit_value_hint\">Escreva algo aqui...</string>\n    <string name=\"seek_interval\">Tempo do intervalo de avanço/retrocesso</string>\n    <string name=\"seek_interval_sec\">%s seg</string>\n    <string name=\"subtitle_system\">Estilo do sistema (configurações do Android &gt; Acessibilidade)</string>\n    <string name=\"subtitle_scale\">Escala das legendas</string>\n    <string name=\"audio_sync_fix_desc\">Uma maneira alternativa de sincronizar áudio/vídeo. Considerada obsoleta, mas, em alguns casos, pode ajudar.</string>\n    <string name=\"audio_sync_fix\">Correção de sincronização de áudio</string>\n    <string name=\"ambilight_ratio_fix_desc\">Corrige a iluminação de polarização ausente. Corrige a proporção incorreta. Corrige capturas de tela em branco. Afeta o desempenho!</string>\n    <string name=\"ambilight_ratio_fix\">Luz ambiente/Proporção/Correção de capturas de tela</string>\n    <string name=\"force_legacy_codecs_desc\">Melhora significativamente o desempenho em dispositivos com baixo poder de processamento. A resolução máxima é de 720p.</string>\n    <string name=\"force_legacy_codecs\">Forçar codecs legados (720p)</string>\n    <string name=\"live_stream_fix_desc\">Aviso, esse ajuste desabilita o retrocesso de transmissões ao vivo. Melhora significativamente o desempenho da transmissão ao vivo em dispositivos com baixo poder de processamento. A resolução máxima é de 1080p.</string>\n    <string name=\"live_stream_fix_4k_desc\">Aviso, esse ajuste desabilita o retrocesso de transmissões ao vivo. Melhora significativamente o desempenho da transmissão ao vivo. A resolução máxima é 4K.</string>\n    <string name=\"live_stream_fix\">Correção de transmissão ao vivo (1080p)</string>\n    <string name=\"live_stream_fix_4k\">Correção de transmissão ao vivo (4K)</string>\n    <string name=\"playback_notifications_fix_desc\">Oculta notificações de alteração de faixa. Pode ser útil em firmwares baseados em AOSP.</string>\n    <string name=\"playback_notifications_fix\">Desativar notificações durante a reprodução</string>\n    <string name=\"amlogic_fix_desc\">Correção de queda de quadros em dispositivos baseados em Amlogic.</string>\n    <string name=\"amlogic_fix\">Correção de 1080p\\@60fps Amlogic</string>\n    <string name=\"tunneled_video_playback_desc\">Observação: o pause talvez não funcione corretamente! A reprodução de vídeo em túnel promete benefícios como melhorar a sincronização de áudio/vídeo (sincronização AV) e reprodução mais suave. Requer Android 5+.</string>\n    <string name=\"tunneled_video_playback\">Reprodução de vídeo em túnel (Android 5+)</string>\n    <string name=\"master_volume\">Volume mestre</string>\n    <string name=\"volume_limit\">Limite de volume</string>\n    <string name=\"player_volume\">Volume do reprodutor (player)</string>\n    <string name=\"play_video\">Reproduzir</string>\n    <string name=\"remember_position_of_short_videos\">Lembrar da posição assistida nos shorts (vídeos curtos de menos de 5 minutos)</string>\n    <string name=\"remember_position_of_live_videos\">Lembrar da posição assistida nas lives (vídeos ao vivo)</string>\n    <string name=\"player_show_tooltips\">Exibir rótulos de dicas nos botões</string>\n    <string name=\"action_like_unset\">Gostei removido</string>\n    <string name=\"action_dislike_unset\">Não Gostei removido</string>\n    <string name=\"various_buttons\">Botões no topo da janela principal</string>\n    <string name=\"not_compatible_with\">A opção selecionada não é compatível</string>\n    <string name=\"subtitle_position\">Posicionamento inferior da legenda</string>\n    <string name=\"pressing_home\">Pressionando o botão \\\"Início\\\"</string>\n    <string name=\"pressing_home_back\">Pressionando o botão \\\"Início\\\" ou \\\"Voltar\\\"</string>\n    <string name=\"pressing_back\">Pressionando o botão \\\"Voltar\\\"</string>\n    <string name=\"player_number_key_seek\">Avanço/Retrocesso com as teclas numéricas</string>\n    <string name=\"save_remove_playlist\">Salvar/Remover essa lista de reprodução (playlist) da seção \\\"Biblioteca\\\"</string>\n    <string name=\"save_playlist\">Adicionar lista de reprodução (playlist) para seção de \\\"Listas de Reprodução (Playlists)\\\"</string>\n    <string name=\"remove_playlist\">Remover essa lista de reprodução (playlist) da seção \\\"Biblioteca\\\" permanentemente</string>\n    <string name=\"remove_playlist_fmt\">Remove %s permanently?</string>\n    <string name=\"removed_from_playlists\">Removida da seção \\\"Biblioteca\\\"</string>\n    <string name=\"saved_to_playlists\">Salva na seção \\\"Biblioteca\\\"</string>\n    <string name=\"create_playlist\">Criar nova lista de reprodução (playlist) </string>\n    <string name=\"create_playlist_note\">it won\\'t be seen in the YouTube app</string>\n    <string name=\"add_video_to_new_playlist\">Adicionar à nova lista de reprodução (playlist)</string>\n    <string name=\"cant_delete_empty_playlist\">Não é possível excluir uma lista de reprodução (playlist) vazia</string>\n    <string name=\"playlist\">Lista de reprodução (playlist)</string>\n    <string name=\"rename_playlist\">Renomear lista de reprodução (playlist)</string>\n    <string name=\"cant_rename_empty_playlist\">Não é possível renomear uma lista de reprodução (playlist) vazia</string>\n    <string name=\"cant_rename_foreign_playlist\">Não é possível renomear uma lista de reprodução (playlist) de terceiros</string>\n    <string name=\"cant_save_playlist\">Essa lista de reprodução (playlist) não pode ser salva</string>\n    <string name=\"enter_value\">Digite qualquer valor que não seja vazio</string>\n    <string name=\"dialog_add_remove_from\">Adicionar/Remover de %s</string>\n    <string name=\"lb_playback_controls_skip_next\">Pular o próximo</string>\n    <string name=\"lb_playback_controls_skip_previous\">Voltar para o anterior/Voltar para a posição inicial</string>\n    <string name=\"alt_presets_behavior\">Comportamento de predefinições (limite da largura de banda)</string>\n    <string name=\"alt_presets_behavior_desc\">O aplicativo tentará manter a largura de banda correspondente à predefinição selecionada ao invés de combinar entre resolução, FPS e codec.</string>\n    <string name=\"sleep_timer\">Ativar o temporizador de sono (se o controle remoto não for utilizado em uma hora)</string>\n    <string name=\"sleep_timer_desc\">Pausar a reprodução se o usuário não utilizar o controle remoto durante uma hora</string>\n    <string name=\"disable_vsync\">Desativar snap para VSync</string>\n    <string name=\"disable_vsync_desc\">Essa opção desativa o alinhamento de quadros com o sinal de sincronização vertical da tela. Isso pode melhorar o desempenho em dispositivos com baixo poder de processamento liberando recursos da CPU.</string>\n    <string name=\"skip_codec_profile_check\">Pular verificação de nível de perfil codec</string>\n    <string name=\"skip_codec_profile_check_desc\">Não verificar o suporte de codec ao iniciar um vídeo. Pode ajudar com firmwares com problemas.</string>\n    <string name=\"force_sw_codec\">Forçar decodificador de vídeo via software</string>\n    <string name=\"force_sw_codec_desc\">Pode reproduzir quase qualquer vídeo, mas o desempenho é muito ruim.</string>\n    <string name=\"playlist_order\">Ordenar lista de reprodução (playlist)</string>\n    <string name=\"playlist_order_added_date_newer_first\">Data de adição (mais novo primeiro)</string>\n    <string name=\"playlist_order_added_date_older_first\">Data de adição (mais velho primeiro)</string>\n    <string name=\"playlist_order_published_date_newer_first\">Data de publicação (mais novo primeiro)</string>\n    <string name=\"playlist_order_published_date_older_first\">Data de publicação (mais velho primeiro)</string>\n    <string name=\"playlist_order_popularity\">Popularidade</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">Não é possível fazer isto para listas de reprodução (playlist) de terceiros</string>\n    <string name=\"owned_playlist_warning\">Erro. Essa ação só pode ser aplicada para listas de reprodução (playlist) próprias</string>\n    <string name=\"content_block_alt_server\">Usar servidor alternativo</string>\n    <string name=\"content_block_alt_server_desc\">Ative essa opção se o SponsorBlock não estiver funcionando por diferentes motivos.</string>\n    <string name=\"unset_stream_reminder\">Remover lembrete para não perder transmissão ao vivo</string>\n    <string name=\"set_stream_reminder\">Adicionar lembrete para não perder a transmissão ao vivo</string>\n    <string name=\"playback_starts_shortly\">A reprodução será iniciada automaticamente quando a transmissão ao vivo estiver pronta</string>\n    <string name=\"starting_stream\">Iniciando a transmissão ao vivo...</string>\n    <string name=\"action_playlist_remove\">Remover da lista de reprodução (playlist)</string>\n    <string name=\"signin_view_title\">O código do usuário está carregando...</string>\n    <string name=\"signin_view_description\">Para fazer o login, digite esse código na página %s\\nObservação: funciona apenas nos navegadores Firefox e Chrome.</string>\n    <string name=\"signin_view_action_text\">Concluído</string>\n    <string name=\"require_checked\">Requer \\\"%s\\\"</string>\n    <string name=\"msg_press_again_to_exit\">Pressione novamente para sair</string>\n    <string name=\"player_remaining_time\">Restante: %s</string>\n    <string name=\"player_ending_time\">Termina em %s</string>\n    <string name=\"badge_new_content\">CONTEÚDO NOVO</string>\n    <string name=\"badge_live\">AO VIVO</string>\n    <string name=\"add_device_view_description\">Para vincular o dispositivo, insira esse código no aplicativo do YouTube do seu telefone (Configurações &gt; Assistir na TV).\\nObservação: funciona apenas com o teclado Gboard.</string>\n    <string name=\"playback_controls_repeat_pause\">Repetir pausa</string>\n    <string name=\"playback_controls_repeat_list\">Repetir lista</string>\n    <string name=\"action_subscribe_off\">Desabilitar inscrições</string>\n    <string name=\"action_subscribe_on\">Habilitar inscrições</string>\n    <string name=\"skip_24_rate\">Pular formatos de 24fps (correção real do modo cinema\\?)</string>\n    <string name=\"skip_shorts\">Ignorar shorts (vídeos curtos)</string>\n    <string name=\"player_disable_suggestions\">Desabilitar sugestões</string>\n    <string name=\"suggestions\">Sugestões</string>\n    <string name=\"feedback\">Feedback</string>\n    <string name=\"sources\">Fontes</string>\n    <string name=\"releases\">Lançamentos</string>\n    <string name=\"prefer_avc_over_vp9\">Seleção de codec: preferir AVC ao VP9</string>\n    <string name=\"open_chat\">Chat/Comentários</string>\n    <string name=\"open_comments\">Abrir comentários</string>\n    <string name=\"place_chat_left\">Mover o chat para a esquerda</string>\n    <string name=\"place_comments_left\">Mover os comentários para a esquerda</string>\n    <string name=\"use_alt_speech_recognizer\">Reconhecimento de voz alternativo</string>\n    <string name=\"time_format\">(formato das horas)</string>\n    <string name=\"time_format_24\">24 horas</string>\n    <string name=\"time_format_12\">12 horas (AM/PM)</string>\n    <string name=\"use_alt_speech_recognizer_desc\">Pode causar sérios problemas. Utilize apenas se estiver tendo problemas com o reconhecimento padrão.</string>\n    <string name=\"speech_recognizer\">Reconhecimento de voz</string>\n    <string name=\"speech_engine\">Motor de pesquisa por voz</string>\n    <string name=\"speech_recognizer_system\">Sistema (recomendado)</string>\n    <string name=\"speech_recognizer_external_1\">Externo #1 (pode causar sérios problemas; para funcionar é necessário desabilitar o acesso ao microfone)</string>\n    <string name=\"speech_recognizer_external_2\">Externo #2 (pode causar sérios problemas)</string>\n    <string name=\"real_channel_icon\">Mostrar ícone no botão do canal</string>\n    <string name=\"subtitle_yellow_semi_transparent\">Amarela com fundo semitransparente</string>\n    <string name=\"subtitle_yellow_black\">Amarela com fundo preto</string>\n    <string name=\"player_pixel_ratio\">Relação de pixels (proporção de tela)</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">Cinza escuro (monocromático)</string>\n    <string name=\"disable_mic_permission\">Por favor, desabilite o acesso ao microfone para o aplicativo para que esse reconhecimento funcione corretamente.</string>\n    <string name=\"repeat_mode_shuffle\">Randomizar qualquer lista de reprodução (playlist)</string>\n    <string name=\"chat_left\">À esquerda</string>\n    <string name=\"chat_right\">À direita</string>\n    <string name=\"card_real_thumbnails\">Substituir as miniaturas por um quadro do vídeo</string>\n    <string name=\"card_content\">Onde obter as imagens das miniaturas</string>\n    <string name=\"thumb_quality_default\">Padrão</string>\n    <string name=\"thumb_quality_start\">No início do vídeo</string>\n    <string name=\"thumb_quality_middle\">No meio do vídeo</string>\n    <string name=\"thumb_quality_end\">No final do vídeo</string>\n    <string name=\"content_block_status\">Verificar o status do SponsorBlock</string>\n    <string name=\"dearrow_status\">Verificar o status do DeArrow</string>\n    <string name=\"player_show_quality_info_bitrate\">Adicionar o bitrate nas informações de qualidade</string>\n    <string name=\"player_speed_button_old_behavior\">Reverter o comportamento antigo do botão de velocidade</string>\n    <string name=\"protect_settings_with_password\">Proteger todas as configurações com senha</string>\n    <string name=\"enter_settings_password\">Insira a senha das configurações</string>\n    <string name=\"child_mode\">Modo infantil</string>\n    <string name=\"child_mode_desc\">Nesse modo, o usuário não pode utilizar a pesquisa nem ver conteúdos sugeridos. As configurações estarão protegidas por senha.</string>\n    <string name=\"lost_setting_warning\">As configurações do aplicativo serão alteradas. \\nTenha certeza de que você criou um backup das configurações.</string>\n    <string name=\"player_button_long_click\">Um toque curto no botão para ação rápida e um longo para o diálogo de configurações</string>\n    <string name=\"pause_history\">Pausar histórico</string>\n    <string name=\"resume_history\">Retomar histórico</string>\n    <string name=\"disable_history\">Desabilitar histórico</string>\n    <string name=\"enable_history\">Habilitar histórico</string>\n    <string name=\"clear_history\">Limpar histórico</string>\n    <string name=\"chapters\">Capítulos</string>\n    <string name=\"card_multiline_subtitle\">Legendas em múltiplas linhas</string>\n    <string name=\"auto_frame_rate_modes\">Modos suportados</string>\n    <string name=\"loading\">Carregando...</string>\n    <string name=\"hide_streams\">Ocultar transmissões ao vivo na seção \\\"Inscrições\\\"</string>\n    <string name=\"video_rotate\">Rotacionar</string>\n    <string name=\"video_flip\">Virar (espelhar)</string>\n    <string name=\"trending_searches\">Pesquisas \\\"Em alta\\\"</string>\n    <string name=\"video_duration_any\">Qualquer</string>\n    <string name=\"video_duration\">Duração</string>\n    <string name=\"video_duration_under_4\">Menor que 4 minutos</string>\n    <string name=\"video_duration_between_4_20\">Entre 4 a 20 minutos</string>\n    <string name=\"video_duration_over_20\">Maior que 20 minutos</string>\n    <string name=\"content_type\">Tipo</string>\n    <string name=\"content_type_any\">Qualquer</string>\n    <string name=\"content_type_video\">Vídeo</string>\n    <string name=\"content_type_channel\">Canal</string>\n    <string name=\"content_type_playlist\">Lista de reprodução (playlist)</string>\n    <string name=\"content_type_movie\">Filme</string>\n    <string name=\"video_features\">Recursos</string>\n    <string name=\"video_feature_any\">Qualquer</string>\n    <string name=\"video_feature_live\">Ao vivo</string>\n    <string name=\"video_feature_4k\">4K</string>\n    <string name=\"video_feature_hdr\">HDR</string>\n    <string name=\"search_sorting\">Ordenar por</string>\n    <string name=\"sort_by_relevance\">Relevância</string>\n    <string name=\"sort_by_views\">Contagem de visualizações</string>\n    <string name=\"sort_by_date\">Data de carregamento (upload)</string>\n    <string name=\"sort_by_rating\">Classificação</string>\n    <string name=\"clear_search_history\">Limpar o histórico de pesquisa</string>\n    <string name=\"remove_from_subscriptions\">Ocultar</string>\n    <string name=\"player_long_speed_list\">Mais velocidades na lista de velocidade</string>\n    <string name=\"player_extra_long_speed_list\">Lista alongada de velocidades de reprodução</string>\n    <string name=\"alt_app_icon\">Ícone de aplicativo alternativo (necessário reiniciar)</string>\n    <string name=\"network_stack\">Preferir Ponte de Conexão %s</string>\n    <string name=\"player_network_stack\">Rotas de conexão</string>\n    <string name=\"cronet_desc\">Cronet é a rota de rede do Chromium. O Cronet pode reduzir a latência e aumentar a desempenho da rede, o que pode ajudar com problemas de buffering.</string>\n    <string name=\"unlock_all_formats\">Desbloquear todos os formatos de vídeo</string>\n    <string name=\"unlock_all_formats_desc\">Em alguns dispositivos o firmware apresenta incorretamente alguns formatos como não suportados, mesmo que eles sejam.\\n (ex. Smart TVs 1080p costumam reportar o 4K como não suportado).</string>\n    <string name=\"okhttp_desc\">Essa rota de rede é a mais lenta, porém possui boa estabilidade.</string>\n    <string name=\"select_channel_section\">Abrir a seção correspondente quando o aplicativo for iniciado por canais da Android TV</string>\n    <string name=\"enable_voice_search_desc\">O aplicativo oficial será substituído por uma ponte. Uma ponte ajuda a transferir resultados de pesquisas globais ao nosso aplicativo.</string>\n    <string name=\"enable_master_password\">Habilitar senha mestra</string>\n    <string name=\"enter_master_password\">Digite a senha mestra</string>\n    <string name=\"disable_stream_buffer\">Desabilitar buffer nas transmissões ao vivo</string>\n    <string name=\"disable_stream_buffer_desc\">Correção para situações em que a transmissão ao vivo fica muito atrasada. Observação: a transmissão ao vivo talvez comece a travar.</string>\n    <string name=\"sony_frame_drop_fix\">Correção de queda de quadros #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">Corrige travamentos em TVs Sony e alguns outros dispositivos. Observação: possíveis problemas com a sincronização de áudio.</string>\n    <string name=\"audio_language\">Linguagem do áudio</string>\n    <string name=\"old_home_look\">Voltar para a aparência antiga da seção \\\"Início\\\"</string>\n    <string name=\"old_channel_look\">Voltar para a aparência antiga da seção \\\"Canal\\\"</string>\n    <string name=\"hide_shorts_everywhere\">Ocultar os shorts (vídeos curtos) em todos os lugares</string>\n    <string name=\"update_found\">Atualização</string>\n    <string name=\"volume_boost_warning\">Qualquer configuração acima do limite pode potencialmente danificar os alto-falantes</string>\n    <string name=\"play_video_incognito\">Reproduzir de forma anônima</string>\n    <string name=\"play_from_start\">Play from start</string>\n    <string name=\"header_kids_home\">Crianças</string>\n    <string name=\"player_section_playlist\">Utilizar os conteúdos da seção atual como lista de reprodução (playlist)</string>\n    <string name=\"header_trending\">Em alta</string>\n    <string name=\"screensaver\">Protetor de tela</string>\n    <string name=\"player_screen_off_timeout\">Tempo de desligamento/desativação da tela</string>\n    <string name=\"old_update_notifications\">Voltar para a aparência antiga das notificações de atualização</string>\n    <string name=\"dialog_notification\">Notificar sobre uma atualização com um diálogo pop-up</string>\n    <string name=\"mark_as_watched\">Marcar como assistido</string>\n    <string name=\"player_ui_animations\">Habilitar animações no painel de controle</string>\n    <string name=\"player_likes_count\">Exibir a quantidade de Gostei/Não Gostei</string>\n    <string name=\"sorting_alphabetically2\">Alfabeticamente (rápido)</string>\n    <string name=\"sorting_default\">Padrão (rápido)</string>\n    <string name=\"content_block_exclude_channel\">Excluir esse canal do SponsorBlock</string>\n    <string name=\"content_block_stop_excluding_channel\">Parar de excluir esse canal do SponsorBlock</string>\n    <string name=\"player_chapter_notification\">Exibir notificações para avançar rapidamente pelos capítulos</string>\n    <string name=\"player_chapter_notification2\">Switch to the next chapter by clicking on the notification</string>\n    <string name=\"subtitle_remember\">Lembrar da ativação das legendas para cada canal</string>\n    <string name=\"amazon_frame_drop_fix\">Correção de queda de quadros #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">Recomendado para aparelhos Amazon Stick. Pode funcionar em outros dispositivos.</string>\n    <string name=\"default_stack_desc\">Fluxo de rede integrado. Este fluxo pode ter melhor estabilidade em determinadas situações.</string>\n    <string name=\"item_postion\">Posição de</string>\n    <string name=\"player_screen_off_dimming\">Porcentagem de escurecimento para tela desligada/desativada</string>\n    <string name=\"screensaver_timout\">Tempo limite para ativar o protetor de tela</string>\n    <string name=\"screensaver_dimming\">Porcentagem de escurecimento do protetor de tela</string>\n    <string name=\"player_ui_on_next\">Mostrar os controles do reprodutor (player) de vídeo quando passar para o próximo vídeo</string>\n    <string name=\"autogenerated\">Auto-gerados</string>\n    <string name=\"player_auto_volume\">Ajuste automático de volume</string>\n    <string name=\"header_shorts\">Shorts</string>\n    <string name=\"player_global_focus\">Navegação sem emendas entre as linhas de botões do reprodutor (player) de vídeo</string>\n    <string name=\"auto_history\">Automático (usar configurações da conta)</string>\n    <string name=\"remember_position_subscriptions\">Lembrar do ponto em que você parou nas suas inscrições</string>\n    <string name=\"remember_position_pinned\">Lembrar do ponto em que você parou nas listas de reprodução (playlists) fixadas</string>\n    <string name=\"msg_player_unknown_error\">Erro desconhecido</string>\n    <string name=\"unknown_source_error\">Erro de fonte desconhecido</string>\n    <string name=\"unknown_renderer_error\">Erro de renderizador desconhecido</string>\n    <string name=\"header_notifications\">Notificações</string>\n    <string name=\"disable_search_history\">Desabilitar histórico de pesquisa</string>\n    <string name=\"unlock_high_bitrate_formats\">Destravar alto bitrate para formatos 1080p/vp9</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">Destravar alto bitrate para formatos mp4a</string>\n    <string name=\"color_scheme_blue\">Azul</string>\n    <string name=\"color_scheme_dark_blue\">Azul Escuro</string>\n    <string name=\"player_speed_per_channel\">Uma velocidade para cada canal</string>\n    <string name=\"disable_popular_searches\">Desabilitar pesquisas populares</string>\n    <string name=\"multi_profiles\">Usar configurações separadas para cada conta</string>\n    <string name=\"protect_account_with_password\">Proteja essa conta com uma senha</string>\n    <string name=\"enter_account_password\">Digite a senha da conta</string>\n    <string name=\"show_connect_messages\">Mostrar mensagens de conexão</string>\n    <string name=\"prefer_avc_over_vp9_desc\">Atenção: Resolução Máxima é 1080p</string>\n    <string name=\"play_next\">Reproduzir próximo</string>\n    <string name=\"hide_watched_from_subscriptions\">Ocultar vídeos assistidos na seção \\\"Inscrições\\\"</string>\n    <string name=\"hide_watched_from_notifications\">Ocultar vídeos assistidos na seção \\\"Notificações\\\"</string>\n    <string name=\"hide_unwanted_content\">Ocultar conteúdo indesejado</string>\n    <string name=\"hide_watched_from_home\">Ocultar vídeos assistidos na seção \\\"Início\\\"</string>\n    <string name=\"hide_watched_from_watch_later\">Ocultar vídeos assistidos da lista de reprodução (playlist) \\\"Assistir mais tarde\\\"</string>\n    <string name=\"remote_control_permission\">O controle remoto em segundo plano requer permissão de sobreposição</string>\n    <string name=\"login_from_browser\">Logar a partir do navegador</string>\n    <string name=\"disable_remote_history\">Desativar o histórico quando usar o controle remoto</string>\n    <string name=\"keyboard_fix\">Impedir que o botão \\\"OK\\\" abra o teclado (G20s e outros)</string>\n    <string name=\"nothing_found\">Nada encontrado</string>\n    <string name=\"auto_frame_rate_desc\">Essa opção remove atrasos nas cenas quando a câmera movimenta muito rápido, Ex: Streaming de esportes</string>\n    <string name=\"channel_filter_hint\">Filtrar canais</string>\n    <string name=\"player_loop_shorts\">Assistir shorts (vídeos curtos) em loop</string>\n    <string name=\"player_quick_shorts_skip\">Passar os shorts (vídeos curtos) com os botões direito/esquerdo</string>\n    <string name=\"player_quick_shorts_skip_alt\">Skip Shorts with up/down buttons</string>\n    <string name=\"player_quick_skip_videos\">Passar os vídeos com botões direito/esquerdo</string>\n    <string name=\"player_quick_skip_videos_alt\">Skip regular videos with up/down buttons</string>\n    <string name=\"channels_filter\">Filtrar lista de canais dentro da seção \\\"Canais\\\"</string>\n    <string name=\"channel_search_bar\">Barra de pesquisa dentro da seção \\\"Canais\\\"</string>\n    <string name=\"header_sports\">Esportes</string>\n    <string name=\"keep_finished_activities\">Manter atividades finalizadas</string>\n    <string name=\"disable_channels_service\">Desabilitar serviços de canais</string>\n    <string name=\"replace_titles\">Substituir títulos</string>\n    <string name=\"enable\">Habilitar</string>\n    <string name=\"more_info\">Mais informações</string>\n    <string name=\"about_sponsorblock\">Sobre Sponsorblock</string>\n    <string name=\"about_dearrow\">Sobre DeArrow</string>\n    <string name=\"replace_thumbnails\">Substituir miniaturas</string>\n    <string name=\"crowdsourced_thumbnails\">Miniaturas selecionadas pela Comunidade de DeArrow</string>\n    <string name=\"crowdsoursed_titles\">Títulos da Comunidade de DeArrow</string>\n    <string name=\"dearrow_not_submitted_thumbs\">De onde obter as miniaturas não enviadas\\?</string>\n    <string name=\"pitch_effect\">Mudar Tom - ∆ Grave / \\\"1\\\" é o valor padrão / ∇ Agudo</string>\n    <string name=\"fullscreen_mode\">Modo tela cheia (sem barras do sistema)</string>\n    <string name=\"player_only_mode\">Mostrar apenas o player se o vídeo for aberto fora do aplicativo</string>\n    <string name=\"pinned_channel_rows\">Mostrar canais fixados como linhas</string>\n    <string name=\"prefer_google_dns\">Prefer Google DNS</string>\n    <string name=\"prefer_ipv4\">Preferir DNS IPv4</string>\n    <string name=\"prefer_ipv4_desc\">Pode corrigir situações em que o aplicativo não está funcionando. \\n Observação. Pode causar Lentidões e travamentos (especialmente em dispositivos Android 8 ou Dune HD)</string>\n    <string name=\"long_press_for_settings\">mantenha pressionado para configurações</string>\n    <string name=\"long_press_for_options\">mantenha pressionado para opções</string>\n    <string name=\"device_specific_backup\">Cópia de segurança somente para neste dispositivo </string>\n    <string name=\"local_backup\">Cópia de segurança Local</string>\n    <string name=\"auto_backup\">Cópia de segurança automática (diária)</string>\n    <string name=\"repeat_mode_reverse_list\">Reproduzir a lista de reprodução (playlist) ou vídeos do canal em ordem reversa</string>\n    <string name=\"calm_msg\">Nós estamos corrigindo as mudanças. Verifique atualizações de tempo em tempo.</string>\n    <string name=\"without_picture\">Sem Imagem:*(</string>\n    <string name=\"video_disabled\">Video Desabilitado</string>\n    <string name=\"applying_fix\">Atenção: Não pressione voltar e/ou não feche o reprodutor (player) de vídeos enquanto são aplicadas as correções! Aplicando as correções...</string>\n    <string name=\"hide_mixes\">Ocultar Mixagens</string>\n    <string name=\"player_global_focus_desc\">Este recurso afeta qual botão do reprodutor (player) de vídeo receberá o foco ao navegar entre as linhas de botões do reprodutor (player) de vídeo</string>\n    <string name=\"disable_network_error_fixing\">Desativar correção automática de erros de rede</string>\n    <string name=\"disable_network_error_fixing_desc\">Talvez essa funcionalidade não funcione se você estiver usando uma VPN</string>\n    <string name=\"recommended\">Recomendados</string>\n    <string name=\"add_to_subscriptions_group\">Adicionar/Remover inscrição do grupo</string>\n    <string name=\"new_subscriptions_group\">Novo grupo</string>\n    <string name=\"rename_group\">Renomear grupo</string>\n    <string name=\"screen_dimming_amount\">Porcentagem de escurecimento de tela</string>\n    <string name=\"screen_dimming_timeout\">Tempo limite para ativar o escurecimento de tela</string>\n    <string name=\"playlists_rows\">Mostrar seção \\\"Listas de Reprodução (Playlists)\\\" como linhas</string>\n    <string name=\"import_subscriptions_group\">Importar arquivo</string>\n    <string name=\"lb_playback_controls_closed_captioning_disable\">Desabilitar legendas</string>\n    <string name=\"lb_playback_controls_closed_captioning_enable\">Habilitar legendas</string>\n    <string name=\"my_videos\">Meus vídeos</string>\n    <string name=\"player_audio_focus\">Foco de áudio do player (pausar ao detectar outros reprodutores)</string>\n    <string name=\"paid_content_notification\">Notificações de conteúdo pago</string>\n    <string name=\"you_liked\">Você gostou</string>\n    <string name=\"premium_users_only\">Apenas para usuários Premium. Correção de lista de formatos de vídeo incompleta</string>\n    <string name=\"playback_buffering_fix\">Correção de buffer de reprodução</string>\n    <string name=\"oculus_quest_fix\">Correção do Oculus Quest</string>\n    <string name=\"card_preview_muted\">Miniatura sem som</string>\n    <string name=\"card_preview_full\">Miniatura com som</string>\n    <string name=\"card_preview\">Miniatura de prévia</string>\n    <string name=\"card_unlocalized_titles\">Não traduzir títulos dos vídeos</string>\n    <string name=\"dont_resize_video_to_fit_dialog\">Don\\'t resize video to fit dialog</string>\n    <string name=\"typing_corrections\">Disable auto-correction while typing text</string>\n    <string name=\"suggestions_horizontally_scrolled\">Horizontally scrolled Suggestions</string>\n    <string name=\"stable_restore\">Restore the stable build (all data will be lost)</string>\n    <string name=\"auto_backup_category\">Auto backup</string>\n    <string name=\"once_a_day\">Once a day</string>\n    <string name=\"once_a_week\">Once a week</string>\n    <string name=\"once_a_month\">Once a month</string>\n    <string name=\"dialog_block_channel\">Bloquear canal</string>\n    <string name=\"dialog_unblock_channel\">Desbloquear canal</string>\n    <string name=\"confirm_block_channel\">Ocultar todo o conteúdo de %s?</string>\n    <string name=\"channel_blocked\">Canal bloqueado</string>\n    <string name=\"channel_unblocked\">Canal desbloqueado</string>\n    <string name=\"header_blocked_channels\">Canais bloqueados</string>\n    <string name=\"msg_no_blocked_channels\">Nenhum canal bloqueado</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-pt-rPT/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"header_home\">Início</string>\n    <string name=\"title_search\">Pesquisar</string>\n    <string name=\"header_subscriptions\">Subscrições</string>\n    <string name=\"header_history\">Histórico</string>\n    <string name=\"header_music\">Músicas</string>\n    <string name=\"header_news\">Notícias</string>\n    <string name=\"header_gaming\">Jogos</string>\n    <string name=\"header_playlists\">Listas de reprodução</string>\n    <string name=\"header_settings\">Definições</string>\n    <string name=\"header_channels\">Canais</string>\n    <string name=\"subscriptions_signin_title\">Consultar novidades dos seus canais preferidos</string>\n    <string name=\"subscriptions_signin_subtitle\">Inicie sessão para ver as suas subscrições</string>\n    <string name=\"subscriptions_signin_button_text\">Iniciar sessão</string>\n    <string name=\"library_signin_to_show_more\">Ver os vídeos de que gostou, guardou ou subscreveu</string>\n    <string name=\"library_signin_subtitle\">Inicie sessão para ver a sua coleção</string>\n    <string name=\"action_signin\">Iniciar sessão</string>\n    <string name=\"title_video_formats\">Formatos de vídeo</string>\n    <string name=\"title_audio_formats\">Formatos de áudio</string>\n    <string name=\"subtitle_category_title\">Legendas</string>\n    <string name=\"playback_queue_category_title\">Fila de reprodução</string>\n    <string name=\"video_max_quality\">Automático (qualidade máxima)</string>\n    <string name=\"audio_max_quality\">Automático (qualidade máxima)</string>\n    <string name=\"subtitles_disabled\">Legendas desativadas</string>\n    <string name=\"auto_frame_rate\">Taxa de fotogramas automática</string>\n    <string name=\"frame_rate_correction\">Corrigir FPS:\\n%s</string>\n    <string name=\"category_background_playback\">Reprodução em segundo plano</string>\n    <string name=\"not_implemented\">Não implementado</string>\n    <string name=\"not_supported_by_device\">O dispositivo não tem suporte a esta funcionalidade</string>\n    <string name=\"option_background_playback_off\">Desativada</string>\n    <string name=\"option_background_playback_pip\">Imagem em imagem (PIP)</string>\n    <string name=\"option_background_playback_only_audio\">Apenas áudio</string>\n    <string name=\"playback_settings\">Definições da qualidade de reprodução</string>\n    <string name=\"video_speed\">Velocidade do vídeo</string>\n    <string name=\"resolution_switch\">Mudar resolução</string>\n    <string name=\"video_buffer\">Buffer do vídeo</string>\n    <string name=\"video_buffer_size_none\">Nada</string>\n    <string name=\"video_buffer_size_lowest\">Mínimo</string>\n    <string name=\"video_buffer_size_low\">Baixo</string>\n    <string name=\"video_buffer_size_med\">Médio</string>\n    <string name=\"video_buffer_size_high\">Alto</string>\n    <string name=\"video_buffer_size_highest\">Máximo</string>\n    <string name=\"update_changelog\">Registo de alterações</string>\n    <string name=\"install_update\">Instalar atualização</string>\n    <string name=\"section_is_empty\">Nada para ver aqui</string>\n    <string name=\"dialog_add_to_playlist\">Adicionar/remover da lista de reprodução</string>\n    <string name=\"msg_signed_users_only\">Apenas utilizadores com sessão iniciada</string>\n    <string name=\"msg_cant_load_content\">Não foi possível carregar o conteúdo\\n1) Verifique a data e a hora do dispositivo\\n2) Verifique a ligação de rede\\n3) Verifique as definições do proxy\\n4) Inicie sessão com outra conta\\n5) Talvez não exista nada aqui</string>\n    <string name=\"title_video_presets\">Pré-ajustes para vídeos</string>\n    <string name=\"settings_accounts\">Contas</string>\n    <string name=\"settings_left_panel\">Editar categorias</string>\n    <string name=\"settings_themes\">Temas</string>\n    <string name=\"settings_other\">Outras</string>\n    <string name=\"settings_player\">Reprodutor de vídeo</string>\n    <string name=\"settings_language\">Idioma</string>\n    <string name=\"settings_linked_devices\">Dispositivos associados</string>\n    <string name=\"settings_about\">Acerca</string>\n    <string name=\"dialog_account_list\">Selecione a conta</string>\n    <string name=\"dialog_account_none\">Nenhuma</string>\n    <string name=\"dialog_remove_account\">Terminar sessão</string>\n    <string name=\"dialog_add_account\">Iniciar sessão</string>\n    <string name=\"default_lang\">Padrão</string>\n    <string name=\"original_lang\">Original</string>\n    <string name=\"dialog_select_language\">Idioma</string>\n    <string name=\"subtitle_default\">Padrão</string>\n    <string name=\"subtitle_white_semi_transparent\">Branco com fundo semitransparente</string>\n    <string name=\"subtitle_style\">Estilo das legendas</string>\n    <string name=\"subtitle_language\">Idioma das legendas</string>\n    <string name=\"action_search\">Pesquisar</string>\n    <string name=\"settings_main_ui\">Interface de utilizador</string>\n    <string name=\"dialog_main_ui\">Interface de utilizador</string>\n    <string name=\"card_animated_previews\">Introduções animadas</string>\n    <string name=\"web_site\">Site</string>\n    <string name=\"donation\">Donativos</string>\n    <string name=\"dialog_about\">Acerca</string>\n    <string name=\"dialog_player_ui\">Reprodutor de vídeo</string>\n    <string name=\"player_show_ui_on_pause\">Mostrar interface se em pausa</string>\n    <string name=\"player_pause_on_ok\">Tecla OK pausa a reprodução</string>\n    <string name=\"player_ok_button_behavior\">Comportamento do botão OK</string>\n    <string name=\"player_only_ui\">Apenas interface</string>\n    <string name=\"player_ui_and_pause\">Interface e pausa</string>\n    <string name=\"player_only_pause\">Apenas pausa</string>\n    <string name=\"player_toggle_speed\">Alternar velocidade ligada/desligada</string>\n    <string name=\"check_for_updates\">Procurar atualizações</string>\n    <string name=\"update_not_found\">A sua versão é a mais recente</string>\n    <string name=\"update_in_progress\">Aguarde…</string>\n    <string name=\"player_ui_hide_behavior\">Ocultar interface automaticamente</string>\n    <string name=\"option_never\">Nunca</string>\n    <string name=\"side_panel_sections\">Configurar secções</string>\n    <string name=\"boot_to_section\">Iniciar na secção</string>\n    <string name=\"large_ui\">Interface grande</string>\n    <string name=\"video_grid_scale\">Escala da grelha de vídeos</string>\n    <string name=\"scale_ui\">Escala da interface</string>\n    <string name=\"color_scheme\">Esquema de cores</string>\n    <string name=\"color_scheme_default\">Padrão</string>\n    <string name=\"color_scheme_red_grey\">Cinzento e vermelho</string>\n    <string name=\"color_scheme_red\">Vermelho</string>\n    <string name=\"color_scheme_dark_grey\">Cinzento escuro</string>\n    <string name=\"disable_update_check\">Desativar procura de atualizações</string>\n    <string name=\"show_again\">Mostrar novamente</string>\n    <string name=\"check_updates_auto\">Notificar acerca de novas versões</string>\n    <string name=\"select_account_on_boot\">Selecionar conta ao iniciar</string>\n    <string name=\"player_other\">Outras</string>\n    <string name=\"player_full_date\">Data precisa na descrição</string>\n    <string name=\"open_channel\">Abrir canal</string>\n    <string name=\"open_playlist\">Abrir lista de reprodução</string>\n    <string name=\"not_interested\">Não interessado</string>\n    <string name=\"not_recommend_channel\">Não recomendar canal</string>\n    <string name=\"you_wont_see_this_video\">O vídeo foi removido das recomendações</string>\n    <string name=\"you_wont_see_this_channel\">Este canal não aparecerá nas recomendações</string>\n    <string name=\"settings_search\">Pesquisar</string>\n    <string name=\"dialog_search\">Pesquisar</string>\n    <string name=\"instant_voice_search\">Pesquisa por voz</string>\n    <string name=\"option_background_playback_behind\">Reproduzir em segundo plano</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">As informações do próximo vídeo ainda não foram carregadas</string>\n    <string name=\"card_multiline_title\">Títulos multilinha</string>\n    <string name=\"cards_style\">Estilo dos cartões</string>\n    <string name=\"repeat_mode_all\">Reproduzir vídeos continuamente</string>\n    <string name=\"repeat_mode_one\">Repetir vídeo atual</string>\n    <string name=\"repeat_mode_pause\">Pausa na reprodução após cada vídeo</string>\n    <string name=\"repeat_mode_pause_alt\">Reprodução contínua apenas para listas de reprodução</string>\n    <string name=\"repeat_mode_none\">Parar reprodução após cada vídeo</string>\n    <string name=\"subscribed_to_channel\">Canal subscrito</string>\n    <string name=\"unsubscribed_from_channel\">Canal não subscrito</string>\n    <string name=\"subtitle_yellow_transparent\">Amarelo com fundo transparente</string>\n    <string name=\"subtitle_white_transparent\">Branco com fundo transparente</string>\n    <string name=\"subtitle_white_black\">Branco com fundo preto</string>\n    <string name=\"player_seek_preview\">Pré-visualizar ao avançar/recuar</string>\n    <string name=\"color_scheme_dark_grey_oled\">Cinzento escuro (OLED)</string>\n    <string name=\"channels_section_sorting\">Ordem dos canais</string>\n    <string name=\"sorting_by_new_content\">Novo conteúdo</string>\n    <string name=\"sorting_alphabetically\">Alfabeticamente</string>\n    <string name=\"sorting_last_viewed\">Últimos visualizados</string>\n    <string name=\"player_pause_when_seek\">Pausa ao avançar/recuar</string>\n    <string name=\"playlists_style\">Estilo das listas de reprodução</string>\n    <string name=\"playlists_style_grid\">Grelha</string>\n    <string name=\"playlists_style_rows\">Linhas</string>\n    <string name=\"player_show_clock\">Mostrar relógio na barra de controlos</string>\n    <string name=\"player_show_remaining_time\">Mostrar tempo restante na barra de controlos</string>\n    <string name=\"player_show_ending_time\">Mostrar tempo até ao fim na na barra de controlos</string>\n    <string name=\"open_channel_uploads\">Abrir carregamentos do canal</string>\n    <string name=\"app_exit_shortcut\">Sair da aplicação</string>\n    <string name=\"player_exit_shortcut\">Sair do reprodutor</string>\n    <string name=\"search_exit_shortcut\">Sair da pesquisa</string>\n    <string name=\"app_exit_none\">Não sair</string>\n    <string name=\"app_double_back_exit\">Dois cliques para sair</string>\n    <string name=\"app_single_back_exit\">Um clique para sair</string>\n    <string name=\"action_video_zoom\">Tamanho do vídeo</string>\n    <string name=\"video_zoom\">Tamanho do vídeo</string>\n    <string name=\"video_zoom_default\">Padrão</string>\n    <string name=\"video_zoom_fit_width\">Ajustar largura</string>\n    <string name=\"video_zoom_fit_height\">Ajustar altura</string>\n    <string name=\"video_zoom_fit_both\">Ajustar largura ou altura</string>\n    <string name=\"video_zoom_stretch\">Esticar</string>\n    <string name=\"color_scheme_teal\">Verde-azulado</string>\n    <string name=\"color_scheme_teal_oled\">Verde-azulado (OLED)</string>\n    <string name=\"player_seek_preview_none\">Desativada</string>\n    <string name=\"player_seek_preview_single\">Um fotograma</string>\n    <string name=\"player_seek_preview_carousel\">Carrossel</string>\n    <string name=\"player_seek_preview_carousel_slow\">Carrossel (lento)</string>\n    <string name=\"player_seek_preview_carousel_fast\">Carrossel (rápido)</string>\n    <string name=\"unsubscribe_from_channel\">Cancelar subscrição do canal</string>\n    <string name=\"card_title_lines_num\">Número de cartões por linha</string>\n    <string name=\"card_auto_scrolled_title\">Visualização automática de títulos</string>\n    <string name=\"share_link\">Partilhar ligação</string>\n    <string name=\"share_qr_link\">Partilhar ligação (Código QR)</string>\n    <string name=\"subscribe_to_channel\">Subscrever canal</string>\n    <string name=\"wait_data_loading\">Os dados estão a ser carregados…</string>\n    <string name=\"auto_frame_rate_pause\">Taxa de fotogramas automática (pausa ao trocar)</string>\n    <string name=\"auto_frame_rate_applying\">A aplicar FPS automáticos %sx%s\\@%s</string>\n    <string name=\"audio_shift\">Sincronizar áudio</string>\n    <string name=\"player_remember_speed\">Memorizar velocidade</string>\n    <string name=\"mark_channel_as_watched\">Marcar como visualizado</string>\n    <string name=\"channel_marked_as_watched\">O canal foi marcado como visualizado</string>\n    <string name=\"dialog_add_device\">Adicionar dispositivo</string>\n    <string name=\"dialog_remove_all_devices\">Remover todos os dispositivos</string>\n    <string name=\"device_link_enabled\">Ligação ativada</string>\n    <string name=\"device_connected\">O dispositivo \\\"%s\\\" foi conectado</string>\n    <string name=\"device_disconnected\">O dispositivo \\\"%s\\\" foi desconectado</string>\n    <string name=\"settings_ui_scale\">Tamanho da interface</string>\n    <string name=\"msg_restart_app\">Reinicie a aplicação para aplicar as alterações</string>\n    <string name=\"settings_block\">Bloqueio de conteúdo</string>\n    <string name=\"msg_applying\">A aplicar %s…</string>\n    <string name=\"msg_done\">Concluído</string>\n    <string name=\"settings_general\">Geral</string>\n    <string name=\"settings_video\">Vídeo</string>\n    <string name=\"content_block_confirm_skip\">Confirmação ao ignorar</string>\n    <string name=\"content_block_categories\">Ignorar segmentos</string>\n    <string name=\"content_block_sponsor\">Patrocinador</string>\n    <string name=\"content_block_intro\">Intromissão/animação inicial</string>\n    <string name=\"content_block_outro\">Cartões finais/créditos</string>\n    <string name=\"content_block_interaction\">Lembrete de interação (Subscrever)</string>\n    <string name=\"content_block_self_promo\">Autopromoção</string>\n    <string name=\"content_block_music_off_topic\">Secção não musical do vídeo</string>\n    <string name=\"confirm_segment_skip\">Ignorar segmento \\\"%s\\\"\\?</string>\n    <string name=\"cancel_segment_skip\">Cancelar Ignorar segmento</string>\n    <string name=\"msg_skipping_segment\">A ignorar segmento \\\"%s\\\"…</string>\n    <string name=\"content_block_notification_type\">Tipo de notificação</string>\n    <string name=\"content_block_notify_none\">Sem notificação</string>\n    <string name=\"content_block_notify_toast\">Notificação</string>\n    <string name=\"content_block_notify_dialog\">Diálogo de confirmação</string>\n    <string name=\"return_to_launcher\">Voltar ao ecrã inicial</string>\n    <string name=\"intent_force_close\">Sair se o vídeo for aberto de uma fonte externa</string>\n    <string name=\"btn_confirm\">Confirmação</string>\n    <string name=\"player_low_video_quality\">Vídeo de baixa qualidade</string>\n    <string name=\"remote_session_closed\">A sessão remota foi encerrada</string>\n    <string name=\"msg_mode_switch_error\">Não foi possível alterar para o modo \\\"%s\\\"</string>\n    <string name=\"msg_player_error\">Erro %s. A reiniciar reprodução…</string>\n    <string name=\"video_preset_disabled\">Sem pré-ajuste</string>\n    <string name=\"video_preset_enabled\">O formato do próximo vídeo estará de acordo com a pré-ajuste</string>\n    <string name=\"player_sleep_timer\">Temporizador</string>\n    <string name=\"player_show_quality_info\">Mostrar qualidade do vídeo na barra de controlos</string>\n    <string name=\"player_remember_each_speed\">Memorizar velocidade de reprodução</string>\n    <string name=\"player_remember_speed_none\">Não</string>\n    <string name=\"player_remember_speed_all\">Igual para todos os vídeos</string>\n    <string name=\"player_remember_speed_each\">Por vídeo</string>\n    <string name=\"msg_player_error_source\">Não foi possível descarregar o vídeo</string>\n    <string name=\"msg_player_error_renderer\">O formato de vídeo selecionado não é suportado.\\nExperimente outro formato.\\nSe, ainda assim, não funcionar, reinicie o dispositivo.</string>\n    <string name=\"msg_player_error_video_renderer\">O formato de vídeo selecionado não é suportado.\\nExperimente outro formato tocando no botão HQ do reprodutor.\\nSe, ainda assim, não funcionar, reinicie o dispositivo.</string>\n    <string name=\"msg_player_error_audio_renderer\">O formato de áudio selecionado não é suportado.\\nExperimente outro formato tocando no botão HQ do reprodutor.\\nSe, ainda assim, não funcionar, reinicie o dispositivo.</string>\n    <string name=\"msg_player_error_subtitle_renderer\">O formato de legenda selecionado não é suportado.\\nExperimente outro formato tocando no botão HQ do reprodutor.\\nSe, ainda assim, não funcionar, reinicie o dispositivo.</string>\n    <string name=\"msg_player_error_unexpected\">Erro ao descodificar o vídeo</string>\n    <string name=\"video_aspect\">Proporção</string>\n    <string name=\"player_tweaks\">Opções de programador</string>\n    <string name=\"header_uploads\">Carregamentos</string>\n    <string name=\"player_show_global_clock\">Mostrar sempre o relógio</string>\n    <string name=\"player_show_global_ending_time\">Mostrar sempre tempo restante</string>\n    <string name=\"app_corner_clock\">Início: relógio no canto superior direito</string>\n    <string name=\"player_corner_clock\">Reprodutor: relógio no canto superior direito</string>\n    <string name=\"player_corner_ending_time\">Reprodutor: tempo restante no canto superior direito</string>\n    <string name=\"uploads_old_look\">Reverter para o anterior aspeto de Carregamentos</string>\n    <string name=\"background_playback_activation\">Reprodução em segundo plano (ativação)</string>\n    <string name=\"channels_old_look\">Reverter para o anterior aspeto de Canais</string>\n    <string name=\"channels_auto_load\">Carregar automaticamente o conteúdo dos Canais</string>\n    <string name=\"return_to_background_video\">Voltar para o vídeo em segundo plano</string>\n    <string name=\"pin_unpin_from_sidebar\">Adicionar/remover da barra lateral</string>\n    <string name=\"pin_unpin_playlist\">Adicionar/remover lista de reprodução da barra lateral</string>\n    <string name=\"pin_unpin_channel\">Adicionar/remover canal da barra lateral</string>\n    <string name=\"pin_playlist\">Adicionar lista de reprodução à barra lateral</string>\n    <string name=\"pin_channel\">Adicionar canal à barra lateral</string>\n    <string name=\"pinned_to_sidebar\">Adicionado à barra lateral</string>\n    <string name=\"unpin_from_sidebar\">Remover da barra lateral</string>\n    <string name=\"unpin_group_from_sidebar\">Remover o grupo de subscrições</string>\n    <string name=\"unpinned_from_sidebar\">Removido da barra lateral</string>\n    <string name=\"dialog_select_country\">País</string>\n    <string name=\"settings_language_country\">Idioma/País</string>\n    <string name=\"share_embed_link\">Partilhar ligação incorporada</string>\n    <string name=\"card_text_scroll_factor\">Velocidade de deslocação do texto do cartão</string>\n    <string name=\"preferred_update_source\">Selecionar fonte de atualização</string>\n    <string name=\"hide_shorts\">Ocultar \\'Shorts\\' nas subscrições</string>\n    <string name=\"hide_shorts_channel\">Ocultar \\'Shorts\\' de um canal</string>\n    <string name=\"key_remapping\">Mapeamento de teclas</string>\n    <string name=\"screen_dimming\">Escurecimento do ecrã</string>\n    <string name=\"removed_from_playback_queue\">Removido da fila de reprodução</string>\n    <string name=\"added_to_playback_queue\">Adicionado à fila de reprodução</string>\n    <string name=\"add_remove_from_playback_queue\">Adicionar/remover da fila de reprodução</string>\n    <string name=\"add_to_playback_queue\">Adicionar à fila de reprodução</string>\n    <string name=\"remove_from_playback_queue\">Remover da fila de reprodução</string>\n    <string name=\"proxy_enabled\">Proxy ativado</string>\n    <string name=\"proxy_disabled\">Proxy desativado</string>\n    <string name=\"uploads_row_name\">Carregamentos</string>\n    <string name=\"playlists_row_name\">Listas de reprodução criadas</string>\n    <string name=\"popular_uploads_row_name\">Carregamentos populares</string>\n    <string name=\"news_row_name\">Notícias</string>\n    <string name=\"breaking_news_row_name\">Notícias em destaque</string>\n    <string name=\"covid_news_row_name\">Notícias COVID-19</string>\n    <string name=\"enable_voice_search\">Ativar pesquisa global (requer suporte de firmware)</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">Subscrever/Cancelar subscrição no canal</string>\n    <string name=\"app_backup_restore\">Backup/Restauro</string>\n    <string name=\"app_backup\">Criar backup</string>\n    <string name=\"app_restore\">Restaurar dados de um backup</string>\n    <string name=\"skip_each_segment_once\">Não ignorar segmentos novamente</string>\n    <string name=\"disable_ok_long_press\">Desativar toque longo no botão OK</string>\n    <string name=\"disable_ok_long_press_desc\">Opção criada para controladores em que o botão OK não funciona corretamente</string>\n    <string name=\"player_time_correction\">Mostrar tempo de acordo com a velocidade</string>\n    <string name=\"sponsor_color_markers\">Marcadores coloridos na barra de progresso</string>\n    <string name=\"refresh_section\">Recarregar secção</string>\n    <string name=\"option_disabled\">Desativada</string>\n    <string name=\"live_now_row_name\">Em direto agora</string>\n    <string name=\"run_in_background\">Reprodução em segundo plano</string>\n    <string name=\"settings_remote_control\">Controlo remoto</string>\n    <string name=\"background_service_started\">Serviço iniciado</string>\n    <string name=\"dialog_add_to\">Adicionar a %s</string>\n    <string name=\"dialog_remove_from\">Remover de %s</string>\n    <string name=\"added_to\">Adicionado a %s</string>\n    <string name=\"removed_from\">Removido de %s</string>\n    <string name=\"video_preset_adaptive\">Adaptativo</string>\n    <string name=\"content_block_preview_recap\">Pré-visualização ou resumo</string>\n    <string name=\"content_block_highlight\">Ponto de interesse (marcador)</string>\n    <string name=\"removed_from_history\">O vídeo foi removido do histórico</string>\n    <string name=\"remove_from_history\">Remover do histórico</string>\n    <string name=\"upload_date\">Data de carregamento</string>\n    <string name=\"upload_date_any\">Qualquer</string>\n    <string name=\"upload_date_today\">Hoje</string>\n    <string name=\"upload_date_this_week\">Esta semana</string>\n    <string name=\"upload_date_this_month\">Este mês</string>\n    <string name=\"upload_date_this_year\">Este ano</string>\n    <string name=\"double_refresh_rate\">Duplicar taxa de atualização</string>\n    <string name=\"upload_date_last_hour\">Última hora</string>\n    <string name=\"focus_on_search_results\">Foco automático nos resultados da pesquisa</string>\n    <string name=\"context_menu\">Menu de contexto</string>\n    <string name=\"context_menu_sorting\">Ordem no menu de contexto</string>\n    <string name=\"add_remove_from_recent_playlist\">Adicionar/remover das listas de reprodução recentes</string>\n    <string name=\"hide_settings_section\">Ocultar definições (PERIGOSO)</string>\n    <string name=\"volume\">Volume %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s seg.</string>\n    <string name=\"audio_shift_sec\">%s seg.</string>\n    <string name=\"ui_hide_timeout_sec\">%s seg.</string>\n    <string name=\"screen_dimming_timeout_min\">%s min.</string>\n    <string name=\"mark_all_channels_watched\">Marcar todos os canais como visualizados</string>\n    <string name=\"move_section_up\">Mover secção para cima</string>\n    <string name=\"move_section_down\">Mover secção para baixo</string>\n    <string name=\"player_buttons\">Configurar botões do reprodutor</string>\n    <string name=\"action_playlist_add\">Adicionar à lista de reprodução</string>\n    <string name=\"action_video_stats\">Estatísticas de vídeos</string>\n    <string name=\"action_subscribe\">Subscrever</string>\n    <string name=\"action_channel\">Abrir canal</string>\n    <string name=\"action_pip\">PIP</string>\n    <string name=\"action_video_info\">Descrição do vídeo</string>\n    <string name=\"action_screen_off\">Desligar ecrã</string>\n    <string name=\"action_sound_off\">Desligar som</string>\n    <string name=\"action_playback_queue\">Fila de reprodução</string>\n    <string name=\"action_video_speed\">Velocidade do vídeo</string>\n    <string name=\"action_subtitles\">Legendas</string>\n    <string name=\"action_like\">Gosto</string>\n    <string name=\"action_dislike\">Não gosto</string>\n    <string name=\"action_play_pause\">Reproduzir/Pausa</string>\n    <string name=\"action_repeat_mode\">Modo de reprodução</string>\n    <string name=\"action_next\">Vídeo seguinte</string>\n    <string name=\"action_previous\">Vídeo anterior</string>\n    <string name=\"action_high_quality\">Qualidade do vídeo</string>\n    <string name=\"content_block_no_skipping_mode\">Modo de Não ignorar</string>\n    <string name=\"content_block_action_type\">Escolha a ação</string>\n    <string name=\"content_block_action_none\">Nada fazer</string>\n    <string name=\"content_block_action_only_skip\">Ignorar</string>\n    <string name=\"content_block_action_toast\">Ignorar com notificação</string>\n    <string name=\"content_block_action_dialog\">Mostrar diálogo</string>\n    <string name=\"content_block_filler\">Off-topic (preenchimento)</string>\n    <string name=\"keyboard_auto_show\">Mostrar teclado automaticamente</string>\n    <string name=\"cancel_dialog\">Cancelar</string>\n    <string name=\"msg_player_error_source2\">O URL não está a funcionar ou a data do dispositivo está errada.\\nNormalmente, é um erro da aplicação.</string>\n    <string name=\"msg_player_error_video_source\">O URL do vídeo não está a funcionar ou a data do dispositivo está errada.\\nNormalmente, é um erro da aplicação.</string>\n    <string name=\"msg_player_error_audio_source\">O URL do áudio não está a funcionar ou a data do dispositivo está errada.\\nNormalmente, é um erro da aplicação.</string>\n    <string name=\"msg_player_error_subtitle_source\">O URL das legendas não está a funcionar ou a data do dispositivo está errada.\\nNormalmente, é um erro da aplicação.</string>\n    <string name=\"hide_upcoming\">Ocultar \\'Seguintes\\' nas subscrições</string>\n    <string name=\"hide_upcoming_channel\">Ocultar \\'Seguintes\\' dos canais</string>\n    <string name=\"hide_upcoming_home\">Ocultar \\'Seguintes\\' de Início</string>\n    <string name=\"search_background_playback\">Reprodução em segundo plano ao pesquisar/explorar o canal</string>\n    <string name=\"trending_row_name\">Tendências</string>\n    <string name=\"player_seek_type\">Comportamento ao avançar/recuar</string>\n    <string name=\"player_seek_regular\">Normal</string>\n    <string name=\"player_seek_confirmation_pause\">Com confirmação (pausa ao avançar/recuar)</string>\n    <string name=\"player_seek_confirmation_play\">Com confirmação (reproduzir ao avançar/recuar)</string>\n    <string name=\"hide_shorts_from_home\">Ocultar \\'Shorts\\' de Início</string>\n    <string name=\"finish_on_disconnect\">Fechar aplicação ao desconectar o telefone/tablet</string>\n    <string name=\"update_error\">Erro ao atualizar</string>\n    <string name=\"hide_shorts_from_search\">Ocultar \\'Shorts\\' dos resultados de pesquisa</string>\n    <string name=\"hide_shorts_from_history\">Ocultar \\'Shorts\\' do histórico</string>\n    <string name=\"hide_shorts_from_trending\">Ocultar \\'Shorts\\' das Tendências </string>\n    <string name=\"disable_screensaver\">Desativar proteção de ecrã</string>\n    <string name=\"description_not_found\">Descrição não encontrada</string>\n    <string name=\"proxy_port_hint\">Porta do proxy</string>\n    <string name=\"proxy_host_hint\">IP ou nome do proxy</string>\n    <string name=\"proxy_username_hint\">Nome de utilizador do proxy</string>\n    <string name=\"proxy_password_hint\">Palavra-passe do proxy</string>\n    <string name=\"proxy_type\">Tipo de proxy</string>\n    <string name=\"enable_web_proxy\">Utilizar proxy na web</string>\n    <string name=\"proxy_not_supported\">Utilizar proxy na web (Android 4.4+)</string>\n    <string name=\"proxy_test_btn\">Testar</string>\n    <string name=\"proxy_settings_title\">Definições do proxy</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">Teste #%d: cancelado</string>\n    <string name=\"proxy_type_invalid\">Tipo de proxy não definido</string>\n    <string name=\"proxy_host_invalid\">IP/nome do proxy não definido</string>\n    <string name=\"proxy_port_invalid\">Porta do proxy inválida. Tem que ser &gt; 0.</string>\n    <string name=\"proxy_credentials_invalid\">Nome de utilizador ou palavra-passe inválida</string>\n    <string name=\"proxy_test_aborted\">Teste cancelado. Corrija as definições do proxy.</string>\n    <string name=\"proxy_application_aborted\">Corrija as definições do proxy</string>\n    <string name=\"proxy_test_start\">Teste #%d: %s…</string>\n    <string name=\"proxy_test_error\">Erro #%d: %s</string>\n    <string name=\"proxy_test_status\">Estado #%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">Endereço do ficheiro de configuração (*.ovpn)</string>\n    <string name=\"enable_openvpn\">Utilizar OpenVPN</string>\n    <string name=\"openvpn_settings_title\">Definições OpenVPN</string>\n    <string name=\"openvpn_application_aborted\">Corrija as definições OpenVPN</string>\n    <string name=\"openvpn_test_aborted\">Teste cancelado. Corrija as definições OpenVPN.</string>\n    <string name=\"openvpn_address_invalid\">Endereço de configuração OpenVPN não definido</string>\n    <string name=\"internet_censorship\">Censura na Internet</string>\n    <string name=\"rename_section\">Mudar nome desta secção</string>\n    <string name=\"simple_edit_value_hint\">Novo valor</string>\n    <string name=\"seek_interval\">Intervalo para avançar/recuar</string>\n    <string name=\"seek_interval_sec\">%s seg.</string>\n    <string name=\"subtitle_system\">Estilo do sistema (Definições Android &gt; Acessibilidade)</string>\n    <string name=\"subtitle_scale\">Escala das legendas</string>\n    <string name=\"audio_sync_fix_desc\">Método alternativo para sincronizar áudio e vídeo. Considerado obsoleto por alguns mas, em certos casos, pode funcionar.</string>\n    <string name=\"audio_sync_fix\">Corrigir sincronização de áudio</string>\n    <string name=\"ambilight_ratio_fix_desc\">Corrige a luz em falta. Corrige a proporção ou escala dos vídeos. Corrige as capturas de ecrã vazias. Afeta o desempenho.</string>\n    <string name=\"ambilight_ratio_fix\">Correção de luz/proporção/escala dos vídeos/capturas de ecrã</string>\n    <string name=\"force_legacy_codecs_desc\">Melhora significativamente o desempenho em dispositivos fracos. A resolução máxima é de 720p.</string>\n    <string name=\"force_legacy_codecs\">Utilizar codificadores legados (720p)</string>\n    <string name=\"live_stream_fix_desc\">Esta opção impede que recue os vídeos, mas melhora o desempenho das emissões em direto nos dispositivos fracos. A resolução máxima é de 1080p.</string>\n    <string name=\"live_stream_fix_4k_desc\">Esta opção impede que recue os vídeos, mas melhora o desempenho das emissões em direto nos dispositivos fracos. A resolução máxima é de 4K.</string>\n    <string name=\"live_stream_fix\">Corrigir emissão de vídeos (1080p)</string>\n    <string name=\"live_stream_fix_4k\">Corrigir emissão de vídeos (4K)</string>\n    <string name=\"playback_notifications_fix_desc\">Oculta a notificação de alteração de vídeos. Pode ser útil em sistemas baseados em AOSP.</string>\n    <string name=\"playback_notifications_fix\">Desativar notificação de reprodução</string>\n    <string name=\"amlogic_fix_desc\">Correção de fotogramas em dispositivos Amlogic</string>\n    <string name=\"amlogic_fix\">Correção para 1080p\\@60fps Amlogic</string>\n    <string name=\"tunneled_video_playback_desc\">NOTA: a função Pausa pode não funcionar corretamente! A reprodução de vídeos em túnel promete melhorias na sincronização de áudio e vídeo (AV sync) e na reprodução. Requer Android 5+.</string>\n    <string name=\"tunneled_video_playback\">Reprodução de vídeos em túnel (Android 5+)</string>\n    <string name=\"master_volume\">Volume principal</string>\n    <string name=\"volume_limit\">Limitar volume</string>\n    <string name=\"player_volume\">Volume</string>\n    <string name=\"play_video\">Reproduzir</string>\n    <string name=\"remember_position_of_short_videos\">Memorizar posição dos \\'Shorts\\'</string>\n    <string name=\"remember_position_of_live_videos\">Memorizar posição das emissões em direto</string>\n    <string name=\"player_show_tooltips\">Mostrar dicas para os botões</string>\n    <string name=\"action_like_unset\">\\'Gosto\\' removido</string>\n    <string name=\"action_dislike_unset\">\\'Não gosto\\' removido</string>\n    <string name=\"various_buttons\">Botões no topo da janela principal</string>\n    <string name=\"not_compatible_with\">A opção selecionada não é compatível com</string>\n    <string name=\"subtitle_position\">Deslocação inferior das legendas</string>\n    <string name=\"pressing_home\">ao tocar em HOME</string>\n    <string name=\"pressing_home_back\">ao tocar em HOME ou BACK</string>\n    <string name=\"pressing_back\">ao tocar em BACK</string>\n    <string name=\"player_number_key_seek\">Avançar/recuar com teclas numéricas</string>\n    <string name=\"save_remove_playlist\">Adicionar/remover esta lista de reprodução da secção Listas de reprodução</string>\n    <string name=\"save_playlist\">Adicionar lista de reprodução à secção Listas de reprodução</string>\n    <string name=\"remove_playlist\">Remover lista de reprodução da secção Listas de reprodução</string>\n    <string name=\"remove_playlist_fmt\">Remover %s permanentemente\\?</string>\n    <string name=\"removed_from_playlists\">Removido da secção Listas de reprodução</string>\n    <string name=\"saved_to_playlists\">Adicionado à secção Listas de reprodução</string>\n    <string name=\"create_playlist\">Criar lista de reprodução</string>\n    <string name=\"create_playlist_note\">não será visto no app do YouTube</string>\n    <string name=\"add_video_to_new_playlist\">Adicionar a uma nova lista de reprodução</string>\n    <string name=\"cant_delete_empty_playlist\">Não pode eliminar uma lista de reprodução vazia</string>\n    <string name=\"playlist\">Lista de reprodução</string>\n    <string name=\"rename_playlist\">Mudar nome da lista de reprodução</string>\n    <string name=\"cant_rename_empty_playlist\">Não pode mudar o nome de uma lista de reprodução vazia</string>\n    <string name=\"cant_rename_foreign_playlist\">Não pode mudar o nome de uma lista de reprodução externa</string>\n    <string name=\"cant_save_playlist\">Não é possível adicionar esta lista de reprodução</string>\n    <string name=\"enter_value\">Introduza qualquer valor não vazio</string>\n    <string name=\"dialog_add_remove_from\">Adicionar/Remover de %s</string>\n    <string name=\"lb_playback_controls_skip_next\">Ignorar seguinte</string>\n    <string name=\"lb_playback_controls_skip_previous\">Ignorar \\\"Seguinte\\\"/Recuar para a posição inicial</string>\n    <string name=\"alt_presets_behavior\">Comportamento alternativo de pré-ajustes</string>\n    <string name=\"alt_presets_behavior_desc\">A aplicação irá tentar manter a largura de banda correspondente ao pré-ajuste selecionado em vez de tentar manter a correspondência entre resolução, fps e codificador.</string>\n    <string name=\"sleep_timer\">Temporizador (para controlo remoto)</string>\n    <string name=\"sleep_timer_desc\">Pausa na reprodução se o controlo remoto não for usado durante uma hora</string>\n    <string name=\"disable_vsync\">Desativar alinhamento com \\'vsync\\'</string>\n    <string name=\"disable_vsync_desc\">Esta opção desativa o alinhamento dos fotogramas com o sinal de sincronização vertical do ecrã. Pode melhorar o desempenho em dipositivos fracos.</string>\n    <string name=\"skip_codec_profile_check\">Ignorar verificação do nível de perfil do codificador</string>\n    <string name=\"skip_codec_profile_check_desc\">Não verificar se existe suporte ao codificador ao iniciar os vídeos. Pode ser útil com alguns \\'firmwares\\'.</string>\n    <string name=\"force_sw_codec\">Impor descodificação de vídeos por software</string>\n    <string name=\"force_sw_codec_desc\">Permite reproduzir todos os vídeos mas o desempenho é muito fraco</string>\n    <string name=\"playlist_order\">Ordenar lista de reprodução</string>\n    <string name=\"playlist_order_added_date_newer_first\">Data de adição (recentes primeiro)</string>\n    <string name=\"playlist_order_added_date_older_first\">Data de adição (antigas primeiro)</string>\n    <string name=\"playlist_order_published_date_newer_first\">Data de publicação (recentes primeiro)</string>\n    <string name=\"playlist_order_published_date_older_first\">Data de publicação (antigas primeiro)</string>\n    <string name=\"playlist_order_popularity\">Popularidade</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">Não pode exercer esta opção em listas de reprodução externas</string>\n    <string name=\"owned_playlist_warning\">ERRO: apenas pode aplicar esta opção em listas de reprodução próprias</string>\n    <string name=\"content_block_alt_server\">Utilizar servidor alternativo</string>\n    <string name=\"content_block_alt_server_desc\">Ative esta opção se SponsorBlock não estiver a funcionar corretamente</string>\n    <string name=\"unset_stream_reminder\">Desativar lembrete de vídeos</string>\n    <string name=\"set_stream_reminder\">Ativar lembrete de vídeos</string>\n    <string name=\"playback_starts_shortly\">A reprodução será iniciada assim que o vídeo estiver pronto</string>\n    <string name=\"starting_stream\">A iniciar vídeo…</string>\n    <string name=\"action_playlist_remove\">Remover da lista de reprodução</string>\n    <string name=\"signin_view_title\">A carregar código do utilizador…</string>\n    <string name=\"signin_view_description\">Para iniciar sessão, introduza este código na página %s\\nNOTA: apenas funciona nos navegadores Firefox e Chrome</string>\n    <string name=\"signin_view_action_text\">Concluído</string>\n    <string name=\"require_checked\">Requer %s</string>\n    <string name=\"msg_press_again_to_exit\">Prima novamente para sair</string>\n    <string name=\"player_remaining_time\">Restante: %s</string>\n    <string name=\"player_ending_time\">Termina em %s</string>\n    <string name=\"badge_new_content\">Novo conteúdo</string>\n    <string name=\"badge_live\">Em direto</string>\n    <string name=\"add_device_view_description\">Para associar este dispositivo, introduza este código em \\\"Definições -&gt; Ver na TV\\\", na aplicação YouTube do seu telefone.\\nNOTA: apenas funciona com o teclado Google.</string>\n    <string name=\"playback_controls_repeat_pause\">Repetir pausa</string>\n    <string name=\"playback_controls_repeat_list\">Repetir lista</string>\n    <string name=\"action_subscribe_off\">Desativar \\'Subscrever\\'</string>\n    <string name=\"action_subscribe_on\">Ativar \\'Subscrever\\'</string>\n    <string name=\"skip_24_rate\">Ignorar formatos 24fps</string>\n    <string name=\"skip_shorts\">Ignorar \\'Shorts\\'</string>\n    <string name=\"player_disable_suggestions\">Desativar sugestões</string>\n    <string name=\"suggestions\">Sugestões</string>\n    <string name=\"feedback\">Opinião</string>\n    <string name=\"sources\">Fontes</string>\n    <string name=\"releases\">Versões</string>\n    <string name=\"prefer_avc_over_vp9\">Seleção de codificador: preferir avc a vp9</string>\n    <string name=\"open_chat\">Chat/Comentários</string>\n    <string name=\"open_comments\">Mostrar comentários</string>\n    <string name=\"place_chat_left\">Colocar \\\"Chat\\\" à esquerda</string>\n    <string name=\"place_comments_left\">Colocar comentários à esquerda</string>\n    <string name=\"use_alt_speech_recognizer\">Reconhecimento de voz alternativo</string>\n    <string name=\"time_format\">Formato de hora</string>\n    <string name=\"time_format_24\">24 horas</string>\n    <string name=\"time_format_12\">12 horas (AM/PM)</string>\n    <string name=\"use_alt_speech_recognizer_desc\">Com MUITOS erros. Utilize apenas se tiver problemas com o reconhecimento padrão.</string>\n    <string name=\"speech_recognizer\">Reconhecimento de voz</string>\n    <string name=\"speech_engine\">Mecanismo de pesquisa por voz</string>\n    <string name=\"speech_recognizer_system\">Sistema (preferencial)</string>\n    <string name=\"speech_recognizer_external_1\">Externo 1 (com MUITOS erros. Para utilizar esta opção, desative o acesso ao microfone)</string>\n    <string name=\"speech_recognizer_external_2\">Externo 2 (com MUITOS MUITOS erros)</string>\n    <string name=\"real_channel_icon\">Mostrar ícone no botão do canal</string>\n    <string name=\"subtitle_yellow_semi_transparent\">Amarelo em fundo semitransparente</string>\n    <string name=\"subtitle_yellow_black\">Amarelo em fundo preto</string>\n    <string name=\"player_pixel_ratio\">Rácio de pixel</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">Cinzento escuro (monocromático)</string>\n    <string name=\"disable_mic_permission\">Desative a permissão de acesso ao microfone para permitir o correto funcionamento do reconhecimento de voz</string>\n    <string name=\"repeat_mode_shuffle\">Baralhar qualquer lista de reprodução</string>\n    <string name=\"chat_left\">Esquerda</string>\n    <string name=\"chat_right\">Direita</string>\n    <string name=\"card_real_thumbnails\">Substituir miniatura por um fotograma do vídeo</string>\n    <string name=\"card_content\">Tipo de miniatura</string>\n    <string name=\"thumb_quality_default\">Padrão</string>\n    <string name=\"thumb_quality_start\">Início do vídeo</string>\n    <string name=\"thumb_quality_middle\">Meio do vídeo</string>\n    <string name=\"thumb_quality_end\">Final do vídeo</string>\n    <string name=\"content_block_status\">Analisar estado SponsorBlock</string>\n    <string name=\"dearrow_status\">Verificar estado do servidor DeArrow</string>\n    <string name=\"player_show_quality_info_bitrate\">Adicionar taxa de dados à informação de qualidade</string>\n    <string name=\"player_speed_button_old_behavior\">Reverter para o anterior comportamento do botão de velocidade</string>\n    <string name=\"protect_settings_with_password\">Proteger definições com palavra-passe</string>\n    <string name=\"enter_settings_password\">Introduza a palavra-passe</string>\n    <string name=\"child_mode\">Modo infantil</string>\n    <string name=\"child_mode_desc\">Neste modo, o utilizador não pode pesquisar nem ver o conteúdo sugerido. As definições estarão protegidas por palavra-passe.</string>\n    <string name=\"lost_setting_warning\">As definições da aplicação irão ser alteradas. Certifique-se de que tem um backup das definições.</string>\n    <string name=\"player_button_long_click\">Um toque rápido no botão para uma ação rápida e um toque longo para abrir as definições</string>\n    <string name=\"pause_history\">Pausa no histórico</string>\n    <string name=\"resume_history\">Retomar histórico</string>\n    <string name=\"disable_history\">Desativar histórico</string>\n    <string name=\"enable_history\">Ativar histórico</string>\n    <string name=\"clear_history\">Limpar histórico</string>\n    <string name=\"chapters\">Capítulos</string>\n    <string name=\"card_multiline_subtitle\">Legendas multilinha</string>\n    <string name=\"auto_frame_rate_modes\">Modos suportados</string>\n    <string name=\"loading\">A carregar...</string>\n    <string name=\"hide_streams\">Ocultar emissões nas subscrições</string>\n    <string name=\"video_rotate\">Rodar</string>\n    <string name=\"video_flip\">Inverter (espelhar)</string>\n    <string name=\"trending_searches\">Pesquisas nas tendências</string>\n    <string name=\"video_duration_any\">Qualquer</string>\n    <string name=\"video_duration\">Duração</string>\n    <string name=\"video_duration_under_4\">Menos do que 4 minutos</string>\n    <string name=\"video_duration_between_4_20\">Entre 4 e 20 minutos</string>\n    <string name=\"video_duration_over_20\">Mais do que 20 minutos</string>\n    <string name=\"content_type\">Tipo</string>\n    <string name=\"content_type_any\">Todos</string>\n    <string name=\"content_type_video\">Vídeo</string>\n    <string name=\"content_type_channel\">Canal</string>\n    <string name=\"content_type_playlist\">Lista de reprodução</string>\n    <string name=\"content_type_movie\">Filmes</string>\n    <string name=\"video_features\">Funcionalidades</string>\n    <string name=\"video_feature_any\">Qualquer</string>\n    <string name=\"video_feature_live\">Em direto</string>\n    <string name=\"video_feature_4k\">4K</string>\n    <string name=\"video_feature_hdr\">HDR</string>\n    <string name=\"search_sorting\">Ordenar por</string>\n    <string name=\"sort_by_relevance\">Relevância</string>\n    <string name=\"sort_by_views\">Número de visualizações</string>\n    <string name=\"sort_by_date\">Data de carregamento</string>\n    <string name=\"sort_by_rating\">Avaliação</string>\n    <string name=\"clear_search_history\">Limpar histórico de pesquisa</string>\n    <string name=\"remove_from_subscriptions\">Ocultar</string>\n    <string name=\"player_long_speed_list\">Mais velocidades na lista de velocidades</string>\n    <string name=\"player_extra_long_speed_list\">Lista de velocidades muito longa</string>\n    <string name=\"alt_app_icon\">Ícone alternativo (tem que reiniciar)</string>\n    <string name=\"network_stack\">Utilizar \\'stack\\' de rede %s</string>\n    <string name=\"player_network_stack\">Mecanismo de rede</string>\n    <string name=\"cronet_desc\">Cronet é a \\'stack\\' de rede usada por Chromium. Pode reduzir a latência e melhorar o desempenho de rede, o que ajuda nos problemas de \\'buffer\\'.</string>\n    <string name=\"unlock_all_formats\">Desbloquear todos os formatos de vídeo</string>\n    <string name=\"unlock_all_formats_desc\">Em alguns dispositivos, o firmware reporta alguns formatos como não suportados, mesmo que o sejam.\\nExemplo: TV inteligentes com resolução 1080p tendem a reportar o formato 4k como não suportado)</string>\n    <string name=\"okhttp_desc\">Este mecanismo de rede é mais lento, mas tem muita estabilidade.</string>\n    <string name=\"select_channel_section\">Abrir secção respetiva ao iniciar a aplicação a partir de canais ATV</string>\n    <string name=\"enable_voice_search_desc\">A aplicação oficial será substituída por uma \\'bridge\\'. A \\'bridge\\' ajuda a transferir os pedidos de pesquisa para a nossa aplicação.</string>\n    <string name=\"enable_master_password\">Ativar palavra-passe global</string>\n    <string name=\"enter_master_password\">Introduza a palavra-passe global</string>\n    <string name=\"disable_stream_buffer\">Desativar \\'buffer\\' nas emissões em direto</string>\n    <string name=\"disable_stream_buffer_desc\">Correção para situações em que a emissão em direto está muito atrasada. Nota: pode provocar paragens.</string>\n    <string name=\"sony_frame_drop_fix\">Correção de fotogramas #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">Corrige a lentidão em dispositivos Sony e outros. Nota: podem ocorrer problemas de sincronização de áudio.</string>\n    <string name=\"audio_language\">Idioma do áudio</string>\n    <string name=\"old_home_look\">Reverter para anterior aspeto de Início</string>\n    <string name=\"old_channel_look\">Reverter para anterior aspeto de Canais</string>\n    <string name=\"hide_shorts_everywhere\">Ocultar \\'Shorts\\' globalmente</string>\n    <string name=\"update_found\">Atualizar</string>\n    <string name=\"volume_boost_warning\">Todas as definições que ultrapassem o limite podem, potencialmente, danificar o sistema de som</string>\n    <string name=\"play_video_incognito\">Ativar modo incógnito</string>\n    <string name=\"play_from_start\">Reproduzir do início</string>\n    <string name=\"header_kids_home\">Crianças</string>\n    <string name=\"player_section_playlist\">Utilizar conteúdo da secção atual como lista de reprodução</string>\n    <string name=\"header_trending\">Tendências</string>\n    <string name=\"screensaver\">Proteção de ecrã</string>\n    <string name=\"player_screen_off_timeout\">Tempo limite para desligar o ecrã</string>\n    <string name=\"old_update_notifications\">Reverter para o anterior aspeto de Notificações</string>\n    <string name=\"dialog_notification\">Notificar atualizações com uma caixa de diálogo</string>\n    <string name=\"mark_as_watched\">Marcar como visualizado</string>\n    <string name=\"player_ui_animations\">Ativar animações do painel de controlo</string>\n    <string name=\"player_likes_count\">Mostrar número de Gosto/Não gosto</string>\n    <string name=\"sorting_alphabetically2\">Alfabeticamente (rápido)</string>\n    <string name=\"sorting_default\">Padrão (rápido)</string>\n    <string name=\"content_block_exclude_channel\">Desativar SponsorBlock neste canal</string>\n    <string name=\"content_block_stop_excluding_channel\">Ativar SponsorBlock neste canal</string>\n    <string name=\"player_chapter_notification\">Mostrar notificação para percorrer os capítulos</string>\n    <string name=\"player_chapter_notification2\">Trocar para o capítulo seguinte ao tocar na notificação</string>\n    <string name=\"subtitle_remember\">Memorizar legendas ativadas (por canal)</string>\n    <string name=\"amazon_frame_drop_fix\">Correção de fotogramas #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">Opção criada para dispositivos Amazon Stick. Também pode funcionar noutros dispositivos.</string>\n    <string name=\"default_stack_desc\">Mecanismo de rede nativo. Pode apresentar mais estabilidade em situações específicas.</string>\n    <string name=\"item_postion\">Posição de</string>\n    <string name=\"player_screen_off_dimming\">Valor de escurecimento ao desligar o ecrã</string>\n    <string name=\"screensaver_timout\">Tempo limite para ativar a proteção de ecrã</string>\n    <string name=\"screensaver_dimming\">Valor de escurecimento ao ativar a proteção de ecrã</string>\n    <string name=\"player_ui_on_next\">Mostrar interface do reprodutor ao trocar de vídeo</string>\n    <string name=\"autogenerated\">gerado automaticamente</string>\n    <string name=\"player_auto_volume\">Ajustar volume automaticamente</string>\n    <string name=\"header_shorts\">Shorts</string>\n    <string name=\"player_global_focus\">Navegação mais suave nas linhas dos botões de reprodução</string>\n    <string name=\"auto_history\">Automático (usar definições da conta)</string>\n    <string name=\"remember_position_subscriptions\">Memorizar posição de visualização nas Subscrições</string>\n    <string name=\"remember_position_pinned\">Memorizar posição de visualização nas Listas de reprodução afixadas</string>\n    <string name=\"msg_player_unknown_error\">Erro desconhecido</string>\n    <string name=\"unknown_source_error\">Erro desconhecido na fonte</string>\n    <string name=\"unknown_renderer_error\">Erro desconhecido de processamento</string>\n    <string name=\"header_notifications\">Notificações</string>\n    <string name=\"disable_search_history\">Desativar histórico de pesquisas</string>\n    <string name=\"unlock_high_bitrate_formats\">Desbloquear taxa de dados mais altas nos formatos 1080p vp9</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">Desbloquear taxa de dados mais altas nos formatos mp4a</string>\n    <string name=\"color_scheme_blue\">Azul</string>\n    <string name=\"color_scheme_dark_blue\">Azul escuro</string>\n    <string name=\"player_speed_per_channel\">Por canal</string>\n    <string name=\"disable_popular_searches\">Desativar pesquisas populares</string>\n    <string name=\"multi_profiles\">Utilizar definições distintas por conta</string>\n    <string name=\"protect_account_with_password\">Proteger esta conta com palavra-passe</string>\n    <string name=\"enter_account_password\">Introduza a palavra-passe</string>\n    <string name=\"show_connect_messages\">Mostrar mensagens de ligação</string>\n    <string name=\"prefer_avc_over_vp9_desc\">AVISO: máximo de 1080p</string>\n    <string name=\"play_next\">Reproduzir seguinte</string>\n    <string name=\"hide_watched_from_subscriptions\">Ocultar, das Subscrições, os vídeos visualizados</string>\n    <string name=\"hide_watched_from_notifications\">Ocultar, das Notificações, os vídeos visualizados</string>\n    <string name=\"hide_unwanted_content\">Ocultar conteúdo indesejado</string>\n    <string name=\"hide_watched_from_home\">Ocultar, de Início, os vídeos visualizados</string>\n    <string name=\"hide_watched_from_watch_later\">Ocultar, de Ver mais tarde, os vídeos visualizados</string>\n    <string name=\"remote_control_permission\">Controlo remoto em segundo plano necessita da permissão \\'Sobreposição\\'</string>\n    <string name=\"login_from_browser\">Iniciar sessão no navegador</string>\n    <string name=\"disable_remote_history\">Desativar histórico ao utilizar um controlo remoto</string>\n    <string name=\"keyboard_fix\">Impedir que um toque em OK mostre o teclado (G20s e outros)</string>\n    <string name=\"nothing_found\">Nada encontrado</string>\n    <string name=\"auto_frame_rate_desc\">Esta opção remove algumas imperfeições das cenas em que a câmara se move rapidamente (exemplo: desporto)</string>\n    <string name=\"channel_filter_hint\">Filtrar canais</string>\n    <string name=\"player_loop_shorts\">Repetir \\'Shorts\\'</string>\n    <string name=\"player_quick_shorts_skip\">Ignorar \\'Shorts\\' com os botões esquerda/direita</string>\n    <string name=\"player_quick_shorts_skip_alt\">Saltar Shorts com os botões cima/baixo</string>\n    <string name=\"player_quick_skip_videos\">Ignorar vídeos com os botões esquerda/direita</string>\n    <string name=\"player_quick_skip_videos_alt\">Saltar vídeos regulares com os botões cima/baixo</string>\n    <string name=\"channels_filter\">Mostrar campo Filtro na secção Canais</string>\n    <string name=\"channel_search_bar\">Mostrar barra de pesquisa na secção Canais</string>\n    <string name=\"header_sports\">Desporto</string>\n    <string name=\"keep_finished_activities\">Manter atividades finalizadas</string>\n    <string name=\"disable_channels_service\">Desativar serviço de Canais</string>\n    <string name=\"replace_titles\">Substituir títulos</string>\n    <string name=\"enable\">Ativar</string>\n    <string name=\"more_info\">Mais informação</string>\n    <string name=\"about_sponsorblock\">Acerca de SponsorBlock</string>\n    <string name=\"about_dearrow\">Acerca de DeArrow</string>\n    <string name=\"replace_thumbnails\">Substituir miniaturas</string>\n    <string name=\"crowdsourced_thumbnails\">Miniaturas da comunidade</string>\n    <string name=\"crowdsoursed_titles\">Títulos da comunidade</string>\n    <string name=\"dearrow_not_submitted_thumbs\">Origem de miniaturas (se não existir DeArrow)</string>\n    <string name=\"pitch_effect\">Efeito de \\'pitch\\'</string>\n    <string name=\"fullscreen_mode\">Modo de ecrã completo (sem barras do sistema)</string>\n    <string name=\"player_only_mode\">Mostrar apenas o reprodutor de o vídeo for aberto fora da aplicação</string>\n    <string name=\"pinned_channel_rows\">Mostrar canais afixados em linhas</string>\n    <string name=\"prefer_google_dns\">Preferir Google DNS</string>\n    <string name=\"prefer_ipv4\">Preferir DNS IPv4</string>\n    <string name=\"prefer_ipv4_desc\">Pode corrigir situações em que a aplicação não está a funcionar.\\nNota: pode causar paragens ou erros (especialmente em dispositivos com Android 8 ou Dune HD)</string>\n    <string name=\"long_press_for_settings\">Toque longo para definições</string>\n    <string name=\"long_press_for_options\">Toque longo para opções</string>\n    <string name=\"device_specific_backup\">Criar backup apenas deste dispositivo</string>\n    <string name=\"local_backup\">Backup local</string>\n    <string name=\"auto_backup\">Backup automático</string>\n    <string name=\"repeat_mode_reverse_list\">Reproduzir listas de reprodução ou vídeos dos canais por ordem inversa</string>\n    <string name=\"calm_msg\">Estamos a trabalhar neste erro. Procure por atualizações de vez em quando.</string>\n    <string name=\"without_picture\">Sem imagem</string>\n    <string name=\"video_disabled\">Vídeo desativado</string>\n    <string name=\"applying_fix\">Estamos a aplicar a correção. Mantenha a aplicação aberta.</string>\n    <string name=\"hide_mixes\">Ocultar misturas</string>\n    <string name=\"player_global_focus_desc\">Esta opção afeta o botão do reprodutor que irá obter o foco ao navegar nas linhas de botão do reprodutor</string>\n    <string name=\"disable_network_error_fixing\">Desativar correção automática de erros de rede</string>\n    <string name=\"disable_network_error_fixing_desc\">Provavelmente, deve utilizar esta opção se estiver a utilizar uma VPN</string>\n    <string name=\"recommended\">Recomendados</string>\n    <string name=\"add_to_subscriptions_group\">Adicionar/remover do grupo de subscrições</string>\n    <string name=\"new_subscriptions_group\">Novo grupo</string>\n    <string name=\"rename_group\">Mudar nome do grupo de subscrições</string>\n    <string name=\"screen_dimming_amount\">Valor </string>\n    <string name=\"screen_dimming_timeout\">Tempo de espera para escurecimento de ecrã</string>\n    <string name=\"playlists_rows\">Mostrar secção Listas de reprodução como linhas</string>\n    <string name=\"import_subscriptions_group\">Importar</string>\n    <string name=\"lb_playback_controls_closed_captioning_disable\">Ativar legendas visíveis</string>\n    <string name=\"lb_playback_controls_closed_captioning_enable\">Desativar legendas visíveis</string>\n    <string name=\"my_videos\">Os meus vídeos</string>\n    <string name=\"player_audio_focus\">Foco de áudio (pausa se forem detetados outros reprodutores)</string>\n    <string name=\"paid_content_notification\">Notificação de conteúdo pago</string>\n    <string name=\"you_liked\">você gostou</string>\n    <string name=\"premium_users_only\">Apenas para utilizadores Premium. Correção para lista incompleta de formatos de vídeo.</string>\n    <string name=\"playback_buffering_fix\">Correção do \\'buffer\\' de reprodução</string>\n    <string name=\"oculus_quest_fix\">Correção para Oculus Quest</string>\n    <string name=\"card_preview_muted\">Vídeo sem som</string>\n    <string name=\"card_preview_full\">Vídeo com som</string>\n    <string name=\"card_preview\">Antevisão de cartões</string>\n    <string name=\"card_unlocalized_titles\">Título de vídeos não traduzidos</string>\n    <string name=\"dont_resize_video_to_fit_dialog\">Diálogo \\\"Não ajustar vídeo para caber\\\"</string>\n    <string name=\"typing_corrections\">Desativar correção automática ao escrever</string>\n    <string name=\"suggestions_horizontally_scrolled\">Deslocação horizontal de sugestões</string>\n    <string name=\"stable_restore\">Restaurar a compilação estável (todos os dados serão perdidos)</string>\n    <string name=\"auto_backup_category\">Backup Automático</string>\n    <string name=\"once_a_day\">Uma vez por dia</string>\n    <string name=\"once_a_week\">Uma vez por semana</string>\n    <string name=\"once_a_month\">Uma vez por mês</string>\n    <string name=\"dialog_block_channel\">Bloquear canal</string>\n    <string name=\"dialog_unblock_channel\">Desbloquear canal</string>\n    <string name=\"confirm_block_channel\">Ocultar todo o conteúdo de %s?</string>\n    <string name=\"channel_blocked\">Canal bloqueado</string>\n    <string name=\"channel_unblocked\">Canal desbloqueado</string>\n    <string name=\"header_blocked_channels\">Canais bloqueados</string>\n    <string name=\"msg_no_blocked_channels\">Nenhum canal bloqueado</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-ro/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Acasă</string>\n  <string name=\"title_search\">Căutare</string>\n  <string name=\"header_subscriptions\">Abonamente</string>\n  <string name=\"header_history\">Istoric</string>\n  <string name=\"header_music\">Muzică</string>\n  <string name=\"header_news\">Știri</string>\n  <string name=\"header_gaming\">Jocuri</string>\n  <string name=\"header_playlists\">Liste de redare</string>\n  <string name=\"header_settings\">Setări</string>\n  <string name=\"header_channels\">Canale</string>\n  <string name=\"subscriptions_signin_title\">Cele mai noi videoclipuri din canalele la care ești abonat</string>\n  <string name=\"subscriptions_signin_subtitle\">Autentifică-te pentru a vedea abonările</string>\n  <string name=\"subscriptions_signin_button_text\">Conectează-te</string>\n  <string name=\"library_signin_to_show_more\">Vezi videoclipurile la care ai dat like, le-ai salvat, sau te-ai abonat</string>\n  <string name=\"library_signin_subtitle\">Conectează-te pentru a-ți vedea biblioteca</string>\n  <string name=\"action_signin\">Conectează-te</string>\n  <string name=\"title_video_formats\">Formate Video</string>\n  <string name=\"title_audio_formats\">Formate Audio</string>\n  <string name=\"subtitle_category_title\">Subtitrări</string>\n  <string name=\"video_max_quality\">Auto (calitate maximă)</string>\n  <string name=\"audio_max_quality\">Auto (calitate maximă)</string>\n  <string name=\"subtitles_disabled\">Subtitrări oprite</string>\n  <string name=\"auto_frame_rate\">Frame rate automat</string>\n  <string name=\"frame_rate_correction\">Repară cps:\\n%s</string>\n  <string name=\"category_background_playback\">Redare în fundal</string>\n  <string name=\"not_implemented\">Neimplementat</string>\n  <string name=\"option_background_playback_off\">Oprit</string>\n  <string name=\"option_background_playback_pip\">Video și audio</string>\n  <string name=\"option_background_playback_only_audio\">Doar audio</string>\n  <string name=\"playback_settings\">Setări redare</string>\n  <string name=\"video_speed\">Viteză de redare</string>\n  <string name=\"resolution_switch\">Schimbă rezoluția</string>\n  <string name=\"video_buffer\">Buffer video</string>\n  <string name=\"video_buffer_size_low\">Mic</string>\n  <string name=\"video_buffer_size_med\">Mediu</string>\n  <string name=\"video_buffer_size_high\">Mare</string>\n  <string name=\"update_changelog\">Ce este nou</string>\n  <string name=\"install_update\">Instalează Actualizare</string>\n  <string name=\"section_is_empty\">Hopa! Nu-i nimic aici.</string>\n  <string name=\"dialog_add_to_playlist\">Adaugă în lista de redare</string>\n  <string name=\"msg_signed_users_only\">Trebuie să fi conectat</string>\n  <string name=\"msg_cant_load_content\">Nu se poate încărca conținutul.\\n1) Activează istoricul vizionărilor.\\n2) Verificați data și ora pe dispozitiv.\\n3) Verificați conexiunea la rețea.\\n4) Verificați setările proxy.\\n5) Încercați să vă conectați la contul dvs.\\n6) Poate nu se află nimic aici</string>\n  <string name=\"title_video_presets\">Presetări video</string>\n  <string name=\"settings_accounts\">Conturi</string>\n  <string name=\"settings_left_panel\">Editează Categoriile</string>\n  <string name=\"settings_themes\">Teme</string>\n  <string name=\"settings_other\">Altele</string>\n  <string name=\"settings_player\">Player video</string>\n  <string name=\"settings_language\">Limbă</string>\n  <string name=\"settings_linked_devices\">Dispozitive conectate</string>\n  <string name=\"settings_about\">Despre</string>\n  <string name=\"dialog_account_list\">Selectează contul</string>\n  <string name=\"dialog_account_none\">Nici unul</string>\n  <string name=\"dialog_remove_account\">Elimină contul</string>\n  <string name=\"dialog_add_account\">Adaugă cont</string>\n  <string name=\"default_lang\">Mod implicit</string>\n  <string name=\"dialog_select_language\">Limbă aplicație</string>\n  <string name=\"subtitle_default\">Mod implicit</string>\n  <string name=\"subtitle_white_semi_transparent\">Alb pe fundal semitransparent</string>\n  <string name=\"subtitle_style\">Stil subtitrare</string>\n  <string name=\"subtitle_language\">Limbă subtitrare</string>\n  <string name=\"action_search\">Pornește Căutarea</string>\n  <string name=\"settings_main_ui\">Interfață Utilizator</string>\n  <string name=\"dialog_main_ui\">Interfață Utilizator</string>\n  <string name=\"card_animated_previews\">Previzualizări animate</string>\n  <string name=\"web_site\">Site web</string>\n  <string name=\"donation\">Donează</string>\n  <string name=\"dialog_about\">Despre</string>\n  <string name=\"dialog_player_ui\">Player video</string>\n  <string name=\"player_show_ui_on_pause\">Afișează interfața în pauză</string>\n  <string name=\"player_pause_on_ok\">Tasta OK pune pauză</string>\n  <string name=\"player_ok_button_behavior\">Opțiuni buton OK</string>\n  <string name=\"player_only_ui\">Doar interfață</string>\n  <string name=\"player_ui_and_pause\">Interfață și pauză</string>\n  <string name=\"player_only_pause\">Doar pauză</string>\n  <string name=\"check_for_updates\">Verifică dacă sunt actualizări</string>\n  <string name=\"update_not_found\">Folosești ultima versiune</string>\n  <string name=\"update_in_progress\">Așteaptă…</string>\n  <string name=\"player_ui_hide_behavior\">Ascundere Interfață automată</string>\n  <string name=\"option_never\">Niciodată</string>\n  <string name=\"side_panel_sections\">Setează secțiunile</string>\n  <string name=\"boot_to_section\">Pornește în secțiunea</string>\n  <string name=\"large_ui\">Interfață mare</string>\n  <string name=\"video_grid_scale\">Dimensiunea grilei video</string>\n  <string name=\"scale_ui\">Scală interfață</string>\n  <string name=\"playback_queue_category_title\">Coadă de redare</string>\n  <string name=\"video_buffer_size_none\">Nimic</string>\n  <string name=\"color_scheme\">Schema de culori</string>\n  <string name=\"color_scheme_default\">Implicit</string>\n  <string name=\"color_scheme_red_grey\">Roșu-gri</string>\n  <string name=\"color_scheme_red\">Roșu</string>\n  <string name=\"color_scheme_dark_grey\">Gri închis</string>\n  <string name=\"disable_update_check\">Dezactivați verificarea actualizărilor</string>\n  <string name=\"show_again\">Arată din nou</string>\n  <string name=\"check_updates_auto\">Notificați despre actualizări</string>\n  <string name=\"select_account_on_boot\">Afișați selecția contului la pornire</string>\n  <string name=\"player_other\">Diverse</string>\n  <string name=\"player_full_date\">Data exactă din descriere</string>\n  <string name=\"open_channel\">Deschideți canalul</string>\n  <string name=\"open_playlist\">Deschideți playlistul</string>\n  <string name=\"not_interested\">Nu mă interesează</string>\n  <string name=\"not_recommend_channel\">Nu recomanda acest canal</string>\n  <string name=\"you_wont_see_this_video\">Videoclipul a fost eliminat din recomandate</string>\n  <string name=\"you_wont_see_this_channel\">Nu vei vedea acest canal în recomandări</string>\n  <string name=\"settings_search\">Căutare</string>\n  <string name=\"dialog_search\">Căutare</string>\n  <string name=\"instant_voice_search\">Căutare vocală instantanee</string>\n  <string name=\"option_background_playback_behind\">Redare înapoi</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">Informațiile videoclipului următor nu sunt încă încărcate</string>\n  <string name=\"card_multiline_title\">Titluri cu mai multe linii</string>\n  <string name=\"cards_style\">Stilul cardurilor</string>\n  <string name=\"repeat_mode_all\">Redați videoclipurile continuu</string>\n  <string name=\"repeat_mode_one\">Repetați videoclipul curent</string>\n  <string name=\"repeat_mode_pause\">Întrerupeți redarea după fiecare videoclip</string>\n  <string name=\"repeat_mode_pause_alt\">Redă continuu numai videoclipuri playlist</string>\n  <string name=\"repeat_mode_none\">Opriți redarea după un videoclip</string>\n  <string name=\"unsubscribed_from_channel\">Dezabonat</string>\n  <string name=\"subscribed_to_channel\">Abonat</string>\n  <string name=\"subtitle_yellow_transparent\">Galben pe fundal transparent</string>\n  <string name=\"subtitle_white_transparent\">Alb pe fundal transparent</string>\n  <string name=\"subtitle_white_black\">Alb pe fundal negru</string>\n  <string name=\"player_seek_preview\">Previzualizați în timp ce căutați</string>\n  <string name=\"color_scheme_dark_grey_oled\">Gri închis (OLED)</string>\n  <string name=\"channels_section_sorting\">Sortarea secțiunii Canale</string>\n  <string name=\"sorting_by_new_content\">Conținut nou</string>\n  <string name=\"sorting_alphabetically\">Alfabetic</string>\n  <string name=\"sorting_last_viewed\">Văzut ultima dată</string>\n  <string name=\"player_pause_when_seek\">Pauză când căutați</string>\n  <string name=\"playlists_style\">Stilul secțiunii Playlisturi</string>\n  <string name=\"playlists_style_grid\">Grilă</string>\n  <string name=\"playlists_style_rows\">Rânduri</string>\n  <string name=\"player_show_clock\">Afișați ceasul în bara de comenzi</string>\n  <string name=\"player_show_remaining_time\">Afișați timpului rămas în bara de controale</string>\n  <string name=\"player_show_ending_time\">Afișați ora de încheiere în bara de controale</string>\n  <string name=\"open_channel_uploads\">Deschideți încărcările canalului</string>\n  <string name=\"app_exit_shortcut\">Ieșiți din aplicație</string>\n  <string name=\"player_exit_shortcut\">Ieșiți din player</string>\n  <string name=\"app_exit_none\">Nu ieșiți</string>\n  <string name=\"app_double_back_exit\">Întoarce-te dublu</string>\n  <string name=\"app_single_back_exit\">Întoarce-te</string>\n  <string name=\"action_video_zoom\">Zoom video</string>\n  <string name=\"video_zoom\">Zoom video</string>\n  <string name=\"video_zoom_default\">Implicit</string>\n  <string name=\"video_zoom_fit_width\">Potrivire lățime</string>\n  <string name=\"video_zoom_fit_height\">Potrivire înălțime</string>\n  <string name=\"video_zoom_fit_both\">Potrivire fie lățime, fie înălțime</string>\n  <string name=\"video_zoom_stretch\">Întinde</string>\n  <string name=\"color_scheme_teal\">Turcoaz</string>\n  <string name=\"color_scheme_teal_oled\">Turcoaz (OLED)</string>\n  <string name=\"player_seek_preview_none\">Dezactivat</string>\n  <string name=\"player_seek_preview_single\">Un singur cadru</string>\n  <string name=\"player_seek_preview_carousel\">Carusel</string>\n  <string name=\"player_seek_preview_carousel_slow\">Carusel (lent, prin cadre cheie)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Carusel (previzualizare rapidă, nu precisă)</string>\n  <string name=\"unsubscribe_from_channel\">Dezabonează-te de la canal</string>\n  <string name=\"card_title_lines_num\">Numărul liniilor de titlu ale cardului</string>\n  <string name=\"card_auto_scrolled_title\">Defilare automată titlu trunchiat</string>\n  <string name=\"share_link\">Distribuie link-ul</string>\n  <string name=\"share_qr_link\">Distribuie link-ul (cod QR)</string>\n  <string name=\"subscribe_to_channel\">Abonează-te la canal</string>\n  <string name=\"wait_data_loading\">Vă rugăm să așteptați până ce se încarcă datele...</string>\n  <string name=\"auto_frame_rate_pause\">Rată Auto de Cadre (pauză la comutare)</string>\n  <string name=\"auto_frame_rate_applying\">Aplicați rata auto de cadre %sx%s\\@%s</string>\n  <string name=\"audio_shift\">Schimbare audio</string>\n  <string name=\"player_remember_speed\">Amintiți-vă viteza</string>\n  <string name=\"mark_channel_as_watched\">Marcați ca vizionat</string>\n  <string name=\"channel_marked_as_watched\">Canalul a fost marcat ca vizionat</string>\n  <string name=\"dialog_add_device\">Adăugați un dispozitiv</string>\n  <string name=\"dialog_remove_all_devices\">Eliminați toate dispozitivele</string>\n  <string name=\"device_link_enabled\">Link activat</string>\n  <string name=\"device_connected\">Dispozitivul \\\"%s\\\" a fost conectat</string>\n  <string name=\"device_disconnected\">Dispozitivul \\\"%s\\\" a fost deconectat</string>\n  <string name=\"settings_ui_scale\">Scalare Interfață</string>\n  <string name=\"msg_restart_app\">Vă rugăm să reporniți aplicația pentru a aplica aceste setări</string>\n  <string name=\"settings_block\">Blocarea conținutului</string>\n  <string name=\"msg_applying\">Se aplică %s</string>\n  <string name=\"msg_done\">Terminat</string>\n  <string name=\"settings_general\">General</string>\n  <string name=\"settings_video\">Video</string>\n  <string name=\"content_block_confirm_skip\">Confirmați la omitere</string>\n  <string name=\"content_block_categories\">Omiteți segmentele</string>\n  <string name=\"content_block_sponsor\">Sponsor</string>\n  <string name=\"content_block_intro\">Animație de pauză/introducere</string>\n  <string name=\"content_block_outro\">Carduri finale/credite</string>\n  <string name=\"content_block_interaction\">Memento de interacțiune (abonare)</string>\n  <string name=\"content_block_self_promo\">Neplătită/autopromovare</string>\n  <string name=\"content_block_music_off_topic\">Secțiune non-muzicală a clipului</string>\n  <string name=\"confirm_segment_skip\">Omiteți segmentul \\\"%s\\\"\\?</string>\n  <string name=\"cancel_segment_skip\">Anulați omiterea segmentului</string>\n  <string name=\"msg_skipping_segment\">Se omite segmentul \\\"%s\\\"...</string>\n  <string name=\"content_block_notification_type\">Tipul notificării</string>\n  <string name=\"content_block_notify_none\">Fără notificare</string>\n  <string name=\"content_block_notify_toast\">Notificare</string>\n  <string name=\"content_block_notify_dialog\">Dialog de confirmare</string>\n  <string name=\"return_to_launcher\">Reveniți la lansator de la canalele ATV/căutare</string>\n  <string name=\"intent_force_close\">Forțați ieșirea dacă videoclipul a fost deschis dintr-o sursă externă</string>\n  <string name=\"btn_confirm\">Confirmă</string>\n  <string name=\"player_low_video_quality\">Calitate video scăzută</string>\n  <string name=\"remote_session_closed\">Sesiunea la distanță a fost închisă</string>\n  <string name=\"msg_mode_switch_error\">Nu se poate comuta modul de afișare la \\\"%s</string>\n  <string name=\"msg_player_error\">A apărut eroarea %s la player. Se repornește redării..</string>\n  <string name=\"video_preset_disabled\">Fără presetare</string>\n  <string name=\"video_preset_enabled\">Formatul următorului videoclip va fi setat conform presetării</string>\n  <string name=\"player_sleep_timer\">Temporizator de oprire</string>\n  <string name=\"player_show_quality_info\">Afișați informații despre calitate în bara de comenzi</string>\n  <string name=\"player_remember_each_speed\">Amintiți-vă fiecare viteză video</string>\n  <string name=\"player_remember_speed_none\">Nimic</string>\n  <string name=\"player_remember_speed_all\">La fel pe toate videoclipurile</string>\n  <string name=\"player_remember_speed_each\">Pentru fiecare videoclip</string>\n  <string name=\"msg_player_error_source\">Nu se poate descărca videoclipul</string>\n  <string name=\"msg_player_error_renderer\">Formatul selectat nu este acceptat.\\nÎncercați să selectați altul.\\nDacă acest lucru nu vă va ajuta, încercați să reporniți dispozitivul.</string>\n  <string name=\"msg_player_error_video_renderer\">Formatul VIDEO selectat nu este acceptat.\\nÎncercați să selectați altul apăsând butonul HQ din player.\\nDacă acest lucru nu vă va ajuta, încercați să reporniți dispozitivul.</string>\n  <string name=\"msg_player_error_audio_renderer\">Formatul AUDIO selectat nu este acceptat.\\nÎncercați să selectați altul apăsând butonul HQ din player.\\nDacă acest lucru nu vă va ajuta, încercați să reporniți dispozitivul.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">Formatul SUBTITRARE selectat nu este acceptat.\\nÎncercați să selectați altul apăsând lung butonul CC din player.\\nDacă acest lucru nu vă va ajuta, încercați să reporniți dispozitivul.</string>\n  <string name=\"msg_player_error_unexpected\">Eroare de decodor video necunoscut</string>\n  <string name=\"video_aspect\">Raport de aspect</string>\n  <string name=\"player_tweaks\">Opțiuni pentru dezvoltatori</string>\n  <string name=\"header_uploads\">Încărcări</string>\n  <string name=\"player_show_global_clock\">Afișarea ceasului mereu pe ecran</string>\n  <string name=\"player_show_global_ending_time\">Afișarea orei de terminare mereu pe ecran</string>\n  <string name=\"app_corner_clock\">Acasă: ceasul din colțul din dreapta sus</string>\n  <string name=\"player_corner_clock\">Player: ceasul din colțul din dreapta sus</string>\n  <string name=\"player_corner_ending_time\">ora de încheiere din colțul din dreapta sus</string>\n  <string name=\"uploads_old_look\">Reveniți la aspectul vechi al secțiunii Încărcări</string>\n  <string name=\"background_playback_activation\">Redare în fundal (activare)</string>\n  <string name=\"channels_old_look\">Reveniți la aspectul vechi al secțiunii Canale</string>\n  <string name=\"channels_auto_load\">Încărcarea automată a conținutului secțiunii Canale</string>\n  <string name=\"return_to_background_video\">Reveniți la rularea videoclipului în fundal</string>\n  <string name=\"pin_unpin_from_sidebar\">Adăugare/Eliminare din bara laterală</string>\n  <string name=\"pin_unpin_playlist\">Adăugare/Eliminare listă de redare din bara laterală</string>\n  <string name=\"pin_unpin_channel\">Adăugare/Eliminare canal din bara laterală</string>\n  <string name=\"pin_playlist\">Adăugare listă de redare în bara laterală</string>\n  <string name=\"pin_channel\">Adăugare canal în bara laterală</string>\n  <string name=\"pinned_to_sidebar\">Adăugat la bara laterală</string>\n  <string name=\"unpin_from_sidebar\">Eliminare din bara laterală</string>\n  <string name=\"unpinned_from_sidebar\">Eliminat din bara laterală</string>\n  <string name=\"dialog_select_country\">Țară</string>\n  <string name=\"settings_language_country\">Limba/Țara</string>\n  <string name=\"share_embed_link\">Partajați linkul de încorporare</string>\n  <string name=\"card_text_scroll_factor\">Viteza de derulare a textului cardului</string>\n  <string name=\"preferred_update_source\">Selectați sursa de actualizare</string>\n  <string name=\"hide_shorts\">Ascundeți shorts din abonamente</string>\n  <string name=\"hide_shorts_channel\">Ascundeți shorts de pe canal</string>\n  <string name=\"key_remapping\">Remaparea cheilor</string>\n  <string name=\"screen_dimming\">Întunecarea ecranului</string>\n  <string name=\"removed_from_playback_queue\">Eliminat din coada de redare</string>\n  <string name=\"added_to_playback_queue\">Adăugat la coada de redare</string>\n  <string name=\"add_remove_from_playback_queue\">Adăugare/Eliminare din coada de redare</string>\n  <string name=\"add_to_playback_queue\">Adăugați din coada de redare</string>\n  <string name=\"remove_from_playback_queue\">Eliminați din coada de redare</string>\n  <string name=\"proxy_enabled\">Proxy activat</string>\n  <string name=\"proxy_disabled\">Proxy dezactivat</string>\n  <string name=\"uploads_row_name\">Încărcări</string>\n  <string name=\"playlists_row_name\">Liste de redare create</string>\n  <string name=\"popular_uploads_row_name\">Încărcări populare</string>\n  <string name=\"news_row_name\">Știri</string>\n  <string name=\"breaking_news_row_name\">Știri de ultimă oră</string>\n  <string name=\"covid_news_row_name\">Știri COVID-19</string>\n  <string name=\"enable_voice_search\">Activați căutarea globală (este necesar suport firmware)</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Abonează-te/Dezabonează-te de la canal</string>\n  <string name=\"app_backup_restore\">Copie de rezervă/Restaurare</string>\n  <string name=\"app_backup\">Faceți copie de rezervă datelor aplicației</string>\n  <string name=\"app_restore\">Restabiliți datele aplicației</string>\n  <string name=\"skip_each_segment_once\">Nu omiteți segmente din nou</string>\n  <string name=\"disable_ok_long_press\">Dezactivați butonul OK apăsați lung</string>\n  <string name=\"disable_ok_long_press_desc\">Destinat controlerelor buggy unde butonul OK nu funcționează corect</string>\n  <string name=\"player_time_correction\">Afișați timpul în raport cu viteza</string>\n  <string name=\"sponsor_color_markers\">Marcatori de culoare pe bara de progres</string>\n  <string name=\"refresh_section\">Secțiunea Reîmprospătare</string>\n  <string name=\"option_disabled\">Dezactivat</string>\n  <string name=\"live_now_row_name\">În Direct acum</string>\n  <string name=\"run_in_background\">Redare în fundal</string>\n  <string name=\"settings_remote_control\">Telecomandă</string>\n  <string name=\"background_service_started\">Serviciul de fundal a început</string>\n  <string name=\"dialog_add_to\">Adăugați la %s</string>\n  <string name=\"dialog_remove_from\">Eliminați din %s</string>\n  <string name=\"added_to\">Adăugat la %s</string>\n  <string name=\"removed_from\">Eliminat din %s</string>\n  <string name=\"video_preset_adaptive\">Adaptabil</string>\n  <string name=\"content_block_preview_recap\">Previzualizarea sau recapitularea videoclipului</string>\n  <string name=\"content_block_highlight\">Punct interesant (marcaj)</string>\n  <string name=\"removed_from_history\">Videoclipul a fost eliminat din istoric</string>\n  <string name=\"remove_from_history\">Eliminați din istoric</string>\n  <string name=\"upload_date\">Data de încărcare</string>\n  <string name=\"upload_date_any\">Tot timpul</string>\n  <string name=\"upload_date_today\">Astăzi</string>\n  <string name=\"upload_date_this_week\">Săptămâna aceasta</string>\n  <string name=\"upload_date_this_month\">Luna aceasta</string>\n  <string name=\"upload_date_this_year\">Anul acesta</string>\n  <string name=\"upload_date_last_hour\">Ultima oră</string>\n  <string name=\"double_refresh_rate\">Rată de reîmprospătare dublă</string>\n  <string name=\"focus_on_search_results\">Focalizare automată pe rezultatele căutării</string>\n  <string name=\"context_menu\">Meniu contextual</string>\n  <string name=\"add_remove_from_recent_playlist\">Adaugă/Elimină dintr-o listă de redare recentă</string>\n  <string name=\"hide_settings_section\">Ascundeți secțiunea Setări (periculos!)</string>\n  <string name=\"volume\">Volum %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s secunde</string>\n  <string name=\"audio_shift_sec\">%s secunde</string>\n  <string name=\"ui_hide_timeout_sec\">%s secunde</string>\n  <string name=\"screen_dimming_timeout_min\">%s minute</string>\n  <string name=\"mark_all_channels_watched\">Marcați toate canalele ca vizionate</string>\n  <string name=\"move_section_up\">Mutați secțiunea în sus</string>\n  <string name=\"move_section_down\">Mutați secțiunea în jos</string>\n  <string name=\"player_buttons\">Configurați butoanele playerului</string>\n  <string name=\"action_playlist_add\">Adaugă în lista de redare</string>\n  <string name=\"action_video_stats\">Statistici video</string>\n  <string name=\"action_subscribe\">Abonați-vă</string>\n  <string name=\"action_channel\">Deschideți canalul</string>\n  <string name=\"action_pip\">PIP</string>\n  <string name=\"action_video_info\">Descrierea videoclipului</string>\n  <string name=\"action_screen_off\">Ecran dezactivat</string>\n  <string name=\"action_sound_off\">Sunet oprit</string>\n  <string name=\"action_playback_queue\">Coadă de redare</string>\n  <string name=\"action_video_speed\">Viteza video</string>\n  <string name=\"action_subtitles\">Subtitrări</string>\n  <string name=\"action_like\">Apreciază</string>\n  <string name=\"action_dislike\">Nu aprecia</string>\n  <string name=\"action_play_pause\">Redare/Pauză</string>\n  <string name=\"action_repeat_mode\">Mod de redare</string>\n  <string name=\"action_next\">Următorul videoclip</string>\n  <string name=\"action_previous\">Videoclipul anterior</string>\n  <string name=\"action_high_quality\">Calitate video</string>\n  <string name=\"content_block_no_skipping_mode\">Mod fără omitere</string>\n  <string name=\"content_block_action_type\">Alegeți acțiunea</string>\n  <string name=\"content_block_action_none\">Nu faceți nimic</string>\n  <string name=\"content_block_action_only_skip\">Doar omiteți</string>\n  <string name=\"content_block_action_toast\">Omiteți cu notificarea</string>\n  <string name=\"content_block_action_dialog\">Afișați dialogul de confirmare</string>\n  <string name=\"content_block_filler\">În afara subiectului</string>\n  <string name=\"keyboard_auto_show\">Afișarea automată a tastaturii</string>\n  <string name=\"cancel_dialog\">Anulați</string>\n  <string name=\"msg_player_error_source2\">URL-ul nu funcționează sau ora dispozitivului este incorectă.\\nDe obicei, este o eroare în aplicație.</string>\n  <string name=\"msg_player_error_video_source\">URL-ul VIDEO nu funcționează sau ora dispozitivului este incorectă.\\nDe obicei, este o eroare în aplicație.</string>\n  <string name=\"msg_player_error_audio_source\">URL-ul AUDIO nu funcționează sau ora dispozitivului este incorectă.\\nDe obicei, este o eroare în aplicație.</string>\n  <string name=\"msg_player_error_subtitle_source\">Adresa URL a subtitrării nu funcționează sau ora dispozitivului este incorectă.\\nDe obicei, este o eroare în aplicație.</string>\n  <string name=\"player_seek_type\">Comportament de căutare</string>\n  <string name=\"player_seek_regular\">Regulat</string>\n  <string name=\"hide_upcoming\">Ascundeți fluxurile în viitor din Abonamente</string>\n  <string name=\"hide_upcoming_channel\">Ascundeți fluxurile în viitor din Canale</string>\n  <string name=\"hide_upcoming_home\">Ascundeți fluxurile în viitor din Acasă</string>\n  <string name=\"search_background_playback\">Redare în fundal în timp ce căutați/navigați pe un canal</string>\n  <string name=\"trending_row_name\">Tendințe</string>\n  <string name=\"player_seek_confirmation_pause\">Cu confirmare (întrerupeți în timp ce căutați)</string>\n  <string name=\"player_seek_confirmation_play\">Cu confirmare (redare în timp ce căutați)</string>\n  <string name=\"hide_shorts_from_home\">Ascundeți shorts din Acasă</string>\n  <string name=\"finish_on_disconnect\">Închideți aplicația după deconectarea telefonului/tabletei</string>\n  <string name=\"update_error\">Eroare de actualizare</string>\n  <string name=\"hide_shorts_from_history\">Ascundeți shorts din Istoric</string>\n  <string name=\"hide_shorts_from_trending\">Ascundeți shorts din Tendințe</string>\n  <string name=\"disable_screensaver\">Dezactivați economizatorul de ecran</string>\n  <string name=\"description_not_found\">Descrierea nu a fost găsită</string>\n  <string name=\"proxy_port_hint\">Port proxy</string>\n  <string name=\"proxy_host_hint\">Nume de gazdă proxy sau IP</string>\n  <string name=\"proxy_username_hint\">Nume utilizator proxy</string>\n  <string name=\"proxy_password_hint\">Parolă proxy</string>\n  <string name=\"proxy_type\">Tip proxy</string>\n  <string name=\"enable_web_proxy\">Utilizați proxy web</string>\n  <string name=\"proxy_not_supported\">Utilizați proxy web (are nevoie de Android 4.4+)</string>\n  <string name=\"proxy_test_btn\">Test</string>\n  <string name=\"proxy_settings_title\">Setări server proxy</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">Testul #%d, s-a anulat.</string>\n  <string name=\"proxy_type_invalid\">Tipul proxy nu este setat.</string>\n  <string name=\"proxy_host_invalid\">Gazda proxy nu este setată.</string>\n  <string name=\"proxy_port_invalid\">Port proxy invalid, trebuie să fie &gt; 0.</string>\n  <string name=\"proxy_credentials_invalid\">Nume de utilizator sau parolă incorectă</string>\n  <string name=\"proxy_test_aborted\">Test abandonat, vă rugăm să remediați mai întâi setările proxy.</string>\n  <string name=\"proxy_application_aborted\">Vă rugăm să corectați mai întâi setările proxy.</string>\n  <string name=\"proxy_test_start\">Test #%d: %s ...</string>\n  <string name=\"proxy_test_error\">Eroare #%d: %s</string>\n  <string name=\"proxy_test_status\">Stare #%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Adresa fișierului Configurații (*.ovpn)</string>\n  <string name=\"enable_openvpn\">Utilizați OpenVPN</string>\n  <string name=\"openvpn_settings_title\">Setări OpenVPN</string>\n  <string name=\"openvpn_application_aborted\">Vă rugăm să corectați mai întâi setările OpenVPN.</string>\n  <string name=\"openvpn_test_aborted\">Test abandonat, vă rugăm să remediați mai întâi setările OpenVPN.</string>\n  <string name=\"openvpn_address_invalid\">Adresa de configurare OpenVPN nu este setată.</string>\n  <string name=\"internet_censorship\">Cenzură Internet</string>\n  <string name=\"rename_section\">Redenumiți această secțiune</string>\n  <string name=\"simple_edit_value_hint\">Introduceți valoarea</string>\n  <string name=\"seek_interval\">Căutați intervalul</string>\n  <string name=\"seek_interval_sec\">%s sec</string>\n  <string name=\"subtitle_system\">Stilul sistemului (Setări Android &gt; Accesibilitate)</string>\n  <string name=\"subtitle_scale\">Scalare subtitrare</string>\n  <string name=\"audio_sync_fix_desc\">O modalitate alternativă de sincronizare audio / video. Este considerat învechit, dar, în unele cazuri, poate ajuta.</string>\n  <string name=\"audio_sync_fix\">Remediere sincronizare audio</string>\n  <string name=\"ambilight_ratio_fix_desc\">Remediază iluminarea de polarizare absentă. Remediază raportul de aspect sau scalarea video incorectă. Remediază capturile de ecran goale. Afectează performanța!</string>\n  <string name=\"ambilight_ratio_fix\">Lumină de ambient/Raport de aspect/Scalare video/Corecție capturi de ecran</string>\n  <string name=\"force_legacy_codecs_desc\">Îmbunătățește semnificativ performanța pe dispozitivele low-end. Rezoluția maximă este de 720p.</string>\n  <string name=\"force_legacy_codecs\">Forțați codecurile moștenite (720p)</string>\n  <string name=\"live_stream_fix_desc\">Avertizare, această modificare dezactivează derularea fluxurilor înapoi. Îmbunătățește semnificativ performanța fluxului în direct pe dispozitivele ieftine. Rezoluția maximă este de 1080p.</string>\n  <string name=\"live_stream_fix_4k_desc\">Avertizare, această modificare dezactivează derularea fluxurilor înapoi. Îmbunătățește semnificativ performanța fluxului în direct. Rezoluția maximă este de 4K</string>\n  <string name=\"live_stream_fix\">Remedierea fluxului în direct (1080p)</string>\n  <string name=\"live_stream_fix_4k\">Remedierea fluxului în direct (4K)</string>\n  <string name=\"playback_notifications_fix_desc\">Ascunde notificările de modificare a pistei. Ar putea fi util pe firmware-ul bazat pe AOSP.</string>\n  <string name=\"playback_notifications_fix\">Dezactivați notificările de redare</string>\n  <string name=\"amlogic_fix_desc\">Remedierea căderii cadrelor pe dispozitivele bazate pe Amlogic</string>\n  <string name=\"amlogic_fix\">Amlogic 1080p\\@60fps reparare</string>\n  <string name=\"tunneled_video_playback_desc\">NOTĂ: este posibil ca pauza să nu funcționeze corect! Redarea video tunelată promite beneficii, cum ar fi o mai bună sincronizare audio/video (sincronizare AV) și redare mai lină. Android 5+ necesar</string>\n  <string name=\"tunneled_video_playback\">Redare video tunelată (Android 5+)</string>\n  <string name=\"master_volume\">Volumul principal</string>\n  <string name=\"volume_limit\">Limita de volum</string>\n  <string name=\"player_volume\">Volum</string>\n  <string name=\"play_video\">Redați</string>\n  <string name=\"remember_position_of_short_videos\">Amintiți-vă poziția videoclipurilor scurte (mai puțin de 5 minute)</string>\n  <string name=\"remember_position_of_live_videos\">Amintiți vă poziția fluxului în direct</string>\n  <string name=\"player_show_tooltips\">Afișare sfaturi ecran buton</string>\n  <string name=\"action_like_unset\">apreciere eliminată</string>\n  <string name=\"action_dislike_unset\">neapreciere eliminată</string>\n  <string name=\"various_buttons\">Butoanele din partea de sus a ferestrei principale</string>\n  <string name=\"not_compatible_with\">Opțiunea selectată nu este compatibilă cu</string>\n  <string name=\"subtitle_position\">Schimbarea inferioară a subtitrării</string>\n  <string name=\"pressing_home\">apăsând HOME</string>\n  <string name=\"pressing_home_back\">apăsând HOME sau BACK</string>\n  <string name=\"pressing_back\">apăsând BACK</string>\n  <string name=\"player_number_key_seek\">Căutați cu tastele numerice</string>\n  <string name=\"save_remove_playlist\">Adaugă/Elimină lista de redare din secțiunea Liste de redare</string>\n  <string name=\"save_playlist\">Adăugați o listă de redare la secțiunea Liste de redare</string>\n  <string name=\"remove_playlist\">Eliminați definitiv lista de redare din secțiunea Liste de redare</string>\n  <string name=\"removed_from_playlists\">Eliminat din secțiunea Liste de redare</string>\n  <string name=\"saved_to_playlists\">Adăugat la secțiunea Liste de redare</string>\n  <string name=\"create_playlist\">Creează o nouă listă de redare</string>\n  <string name=\"add_video_to_new_playlist\">Adaugă într-o listă de redare nouă </string>\n  <string name=\"cant_delete_empty_playlist\">Nu se poate șterge o listă de redare goală</string>\n  <string name=\"playlist\">Liste de redare</string>\n  <string name=\"rename_playlist\">Redenumește lista de redare</string>\n  <string name=\"cant_rename_empty_playlist\">Nu se poate redenumi o listă de redare goală</string>\n  <string name=\"cant_rename_foreign_playlist\">Nu se poate redenumi o listă de redare goală necunoscută</string>\n  <string name=\"cant_save_playlist\">Această listă de redare nu poate fi adăugată</string>\n  <string name=\"enter_value\">Introduceți orice valoare care nu este goală</string>\n  <string name=\"dialog_add_remove_from\">Adăugă/Elimină din %s</string>\n  <string name=\"lb_playback_controls_skip_next\">Omite Următorul</string>\n  <string name=\"lb_playback_controls_skip_previous\">Revino la poziția anterioară/Pornire de la început</string>\n  <string name=\"alt_presets_behavior\">Comportament presetări Alternative (limitați lățimea de bandă)</string>\n  <string name=\"alt_presets_behavior_desc\">Aplicația va încerca să mențină lățimea de bandă corespunzătoare presetării selectate în loc să se potrivească între rezoluție, fps și codec.</string>\n  <string name=\"sleep_timer\">Temporizator de oprire (dacă telecomanda nu este utilizată timp de o oră)</string>\n  <string name=\"sleep_timer_desc\">Întrerupeți redarea dacă utilizatorul nu a utilizat telecomanda timp de o oră</string>\n  <string name=\"disable_vsync\">Dezactivați fixarea vsync</string>\n  <string name=\"disable_vsync_desc\">Această opțiune dezactivează alinierea cadrelor cu semnalul de sincronizare verticală al afișajului. Acest lucru ar putea îmbunătăți performanța pe dispozitivele ieftine prin eliberarea resurselor CPU.</string>\n  <string name=\"skip_codec_profile_check\">Omite verificarea nivelului profilului codecului</string>\n  <string name=\"skip_codec_profile_check_desc\">Nu verificați suportul codecului atunci când porniți un videoclip. Ar putea fi util pe firmware-ul buggy.</string>\n  <string name=\"force_sw_codec\">Forța Decodorului video SW</string>\n  <string name=\"force_sw_codec_desc\">Se poate reda aproape orice videoclip, dar performanta este slaba</string>\n  <string name=\"playlist_order\">Sortați lista de redare</string>\n  <string name=\"playlist_order_added_date_newer_first\">Data adăugării (mai întâi mai nouă)</string>\n  <string name=\"playlist_order_added_date_older_first\">Data adăugării (mai întâi mai veche)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Data publicării (mai întâi mai nouă)</string>\n  <string name=\"playlist_order_published_date_older_first\">Data publicării (mai întâi mai veche)</string>\n  <string name=\"playlist_order_popularity\">Popularitate</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">Nu se poate face acest lucru pentru lista de redare necunoscută</string>\n  <string name=\"owned_playlist_warning\">Eroare: această acțiune poate fi aplicată numai pentru listele de redare deținute</string>\n  <string name=\"content_block_alt_server\">Utilizați un server alternativ</string>\n  <string name=\"content_block_alt_server_desc\">Activați această opțiune dacă SponsorBlock refuză să funcționeze din diferite motive.</string>\n  <string name=\"unset_stream_reminder\">Memento de flux nesetat</string>\n  <string name=\"set_stream_reminder\">Memento de flux setat</string>\n  <string name=\"playback_starts_shortly\">Redarea va începe automat când fluxul va fi gata</string>\n  <string name=\"starting_stream\">Se pornește fluxul...</string>\n  <string name=\"action_playlist_remove\">Elimina din lista de redare</string>\n  <string name=\"signin_view_title\">Codul utilizatorului se încarcă...</string>\n  <string name=\"signin_view_description\">Pentru a vă conecta, introduceți acest cod în pagina %s\\nNOTĂ. Funcționează numai cu browserele Firefox sau Chrome.</string>\n  <string name=\"signin_view_action_text\">Terminat</string>\n  <string name=\"require_checked\">Necesită \\\"%s</string>\n  <string name=\"msg_press_again_to_exit\">Apăsați din nou pentru a ieși</string>\n  <string name=\"player_remaining_time\">Rămas: %s</string>\n  <string name=\"player_ending_time\">Se termină în %s</string>\n  <string name=\"badge_new_content\">CONȚINUT NOU</string>\n  <string name=\"badge_live\">ÎN DIRECT</string>\n  <string name=\"add_device_view_description\">Pentru a vă conecta dispozitivul, introduceți acest cod în aplicația YouTube a telefonului, în secțiunea Setări/Vizionează pe televizor\\nNOTĂ: Aceasta funcționează numai cu tastatura Gboard!</string>\n  <string name=\"playback_controls_repeat_pause\">Repetați Pauza</string>\n  <string name=\"playback_controls_repeat_list\">Repetați Lista</string>\n  <string name=\"action_subscribe_off\">Abonare Oprită</string>\n  <string name=\"action_subscribe_on\">Abonare Ppornită</string>\n  <string name=\"skip_24_rate\">Omiteți formatele de 24 cps (remediere reală a modului cinema\\?)</string>\n  <string name=\"skip_shorts\">Omiteți Short-urile</string>\n  <string name=\"player_disable_suggestions\">Dezactivați sugestiile</string>\n  <string name=\"feedback\">Păreri</string>\n  <string name=\"sources\">Surse</string>\n  <string name=\"releases\">Versiuni</string>\n  <string name=\"prefer_avc_over_vp9\">Selectarea codecului: preferați avc peste vp9</string>\n  <string name=\"open_chat\">Chat/Comentarii</string>\n  <string name=\"open_comments\">Deschideți comentariile</string>\n  <string name=\"place_chat_left\">Plasați chatul la stânga</string>\n  <string name=\"use_alt_speech_recognizer\">Recunoașterea vorbirii alternativă</string>\n  <string name=\"time_format\">Formatul orei</string>\n  <string name=\"time_format_24\">24 de ore</string>\n  <string name=\"time_format_12\">12 ore (AM/PM)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Erori serioase. Utilizați-l numai dacă aveți probleme cu recunoașterea implicită.</string>\n  <string name=\"speech_recognizer\">Recunoașterea vorbirii</string>\n  <string name=\"speech_engine\">Motor de căutare vocală</string>\n  <string name=\"speech_recognizer_system\">Sistem (preferat)</string>\n  <string name=\"speech_recognizer_external_1\">1 extern (serios bugged, pentru a lucra trebuie să dezactivați accesul la microfon)</string>\n  <string name=\"speech_recognizer_external_2\">Extern 2 (erori serioase)</string>\n  <string name=\"real_channel_icon\">Afișați pictograma pe butonul canalului</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Galben pe fundal semitransparent</string>\n  <string name=\"subtitle_yellow_black\">Galben pe fundal negru</string>\n  <string name=\"player_pixel_ratio\">Raportul pixelilor</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Gri închis (monocrom)</string>\n  <string name=\"disable_mic_permission\">Vă rugăm să dezactivați accesul la microfon pentru aplicație pentru ca acest recunoscător să funcționeze corect.</string>\n  <string name=\"repeat_mode_shuffle\">Amestecați orice listă de redare</string>\n  <string name=\"chat_left\">Stânga</string>\n  <string name=\"chat_right\">Dreapta</string>\n  <string name=\"card_real_thumbnails\">Înlocuiți miniaturile cu un cadru din videoclip</string>\n  <string name=\"card_content\">Unde să luați miniaturile cardurilor</string>\n  <string name=\"thumb_quality_default\">Implicit</string>\n  <string name=\"thumb_quality_start\">Începutul videoclipului</string>\n  <string name=\"thumb_quality_middle\">Mijlocul videoclipului</string>\n  <string name=\"thumb_quality_end\">Sfârșitul videoclipului</string>\n  <string name=\"content_block_status\">Verificați starea serverului SponsorBlock</string>\n  <string name=\"dearrow_status\">Verificați starea serverului DeArrow</string>\n  <string name=\"player_show_quality_info_bitrate\">Adăugați rata de biți în informațiile de calitate</string>\n  <string name=\"player_speed_button_old_behavior\">Reveniți la comportamentul vechi al butonului de viteză</string>\n  <string name=\"protect_settings_with_password\">Protejați toate setările cu parolă</string>\n  <string name=\"enter_settings_password\">Introduceți parola setărilor</string>\n  <string name=\"child_mode\">Modul copil</string>\n  <string name=\"child_mode_desc\">În acest mod, utilizatorul nu poate utiliza căutarea sau vedea niciun conținut sugerat. Pagina de setări va fi protejată prin parolă.</string>\n  <string name=\"lost_setting_warning\">Setările aplicației vor fi modificate. Asigurați-vă că ați creat o copie de rezervă a setărilor.</string>\n  <string name=\"player_button_long_click\">Apăsați lung acest buton pentru opțiuni suplimentare</string>\n  <string name=\"pause_history\">Întrerupeți istoricul</string>\n  <string name=\"resume_history\">Reactivați istoricul</string>\n  <string name=\"disable_history\">Dezactivați istoricul</string>\n  <string name=\"enable_history\">Activați istoricul</string>\n  <string name=\"clear_history\">Ștergeți istoricul</string>\n  <string name=\"chapters\">Capitole</string>\n  <string name=\"card_multiline_subtitle\">Subtitrari pe mai multe linii</string>\n  <string name=\"auto_frame_rate_modes\">Moduri acceptate</string>\n  <string name=\"loading\">Se încarcă...</string>\n  <string name=\"hide_streams\">Ascunde fluxurile din abonamente</string>\n  <string name=\"video_rotate\">Rotește</string>\n  <string name=\"trending_searches\">Căutări populare</string>\n  <string name=\"video_duration_any\">Oricare</string>\n  <string name=\"video_duration\">Durată</string>\n  <string name=\"video_duration_under_4\">Sub 4 minute</string>\n  <string name=\"video_duration_between_4_20\">4-20 de minute</string>\n  <string name=\"video_duration_over_20\">Peste 20 de minute</string>\n  <string name=\"content_type\">Tip</string>\n  <string name=\"content_type_any\">Oricare</string>\n  <string name=\"content_type_video\">Video</string>\n  <string name=\"content_type_channel\">Canal</string>\n  <string name=\"content_type_playlist\">Listă de redare</string>\n  <string name=\"content_type_movie\">Film</string>\n  <string name=\"video_features\">Caracteristici</string>\n  <string name=\"video_feature_any\">Oricare</string>\n  <string name=\"video_feature_live\">În direct</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">Sortează după</string>\n  <string name=\"sort_by_relevance\">Relevanță</string>\n  <string name=\"sort_by_views\">Număr de vizualizări</string>\n  <string name=\"sort_by_date\">Data încărcării</string>\n  <string name=\"sort_by_rating\">Evaluare</string>\n  <string name=\"clear_search_history\">Ștergeți istoricul căutărilor</string>\n  <string name=\"remove_from_subscriptions\">Ascunde</string>\n  <string name=\"player_long_speed_list\">Listă lungă de viteze</string>\n  <string name=\"player_extra_long_speed_list\">Listă foarte lungă de viteze</string>\n  <string name=\"alt_app_icon\">Pictogramă alternativă a aplicației (necesită repornire)</string>\n  <string name=\"network_stack\">Preferați %s stivă de rețea</string>\n  <string name=\"player_network_stack\">Motor de rețea</string>\n  <string name=\"cronet_desc\">Cronet este stiva de rețea Chromium. Cronet poate reduce latența și crește performanța rețelei, ceea ce poate ajuta la problemele de redare.</string>\n  <string name=\"unlock_all_formats\">Deblocați toate formatele video</string>\n  <string name=\"unlock_all_formats_desc\">Pe unele dispozitive, firmware-ul raportează incorect unele formate ca neacceptate, chiar dacă sunt.\\n (de exemplu, televizoarele inteligente 1080p tind să raporteze 4k ca nefiind acceptate).</string>\n  <string name=\"okhttp_desc\">Acest motor de rețea este cel mai lent, dar are o stabilitate bună.</string>\n  <string name=\"select_channel_section\">Deschideți secțiunea corespunzătoare atunci când aplicația se lansează de pe Canalele ATV</string>\n  <string name=\"enable_voice_search_desc\">Aplicația oficială va fi înlocuită cu un așa-numit \\\"pod\\\". Un pod ajută la transferul solicitărilor de căutare globală în aplicația noastră.</string>\n  <string name=\"enable_master_password\">Activați parola principală</string>\n  <string name=\"enter_master_password\">Introduceți parola principală</string>\n  <string name=\"disable_stream_buffer\">Dezactivați redarea pe fluxuri</string>\n  <string name=\"disable_stream_buffer_desc\">Remediați situațiile în care fluxul este prea în urmă. Notă: fluxul poate începe să întârzie.</string>\n  <string name=\"sony_frame_drop_fix\">Rezolvați căderea cadrului #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">Remediați întârzierile pe televizoarele Sony și pe alte dispozitive. Notă: posibile probleme cu sincronizarea audio.</string>\n  <string name=\"audio_language\">Limba audio</string>\n  <string name=\"old_home_look\">Aspectul vechi al secțiunii Acasă</string>\n  <string name=\"old_channel_look\">Aspectul vechi a paginii Canal</string>\n  <string name=\"hide_shorts_everywhere\">Ascundeți shorts peste tot</string>\n  <string name=\"update_found\">Actualizați</string>\n  <string name=\"volume_boost_warning\">Orice setări care depășesc limita pot deteriora difuzoarele</string>\n  <string name=\"play_video_incognito\">Redare în incognito</string>\n  <string name=\"header_kids_home\">Copii</string>\n  <string name=\"header_trending\">Tendințe</string>\n  <string name=\"screensaver\">Economizor de ecran</string>\n  <string name=\"player_section_playlist\">Utilizați conținutul secțiunii curente ca listă de redare</string>\n  <string name=\"player_screen_off_timeout\">Expirare oprire ecran</string>\n  <string name=\"old_update_notifications\">Aspectul vechi al notificărilor de actualizare</string>\n  <string name=\"dialog_notification\">Notificați despre actualizări printr-un dialog pop-up</string>\n  <string name=\"mark_as_watched\">Marcați ca vizionat</string>\n  <string name=\"player_ui_animations\">Activați animațiile în panoului de control</string>\n  <string name=\"player_likes_count\">Afișați numărul de aprecieri/neaprecieri</string>\n  <string name=\"sorting_alphabetically2\">În ordine alfabetică (previzualizări rapide, animate)</string>\n  <string name=\"sorting_default\">Implicit (previzualizări rapide, animate)</string>\n  <string name=\"content_block_exclude_channel\">Exclude acest canal din SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">Nu mai exclude acest canal din SponsorBlock</string>\n  <string name=\"player_chapter_notification\">Avansați rapid prin capitole folosind notificarea pop-up</string>\n  <string name=\"subtitle_remember\">Reține subtitrările activate pentru fiecare canal</string>\n  <string name=\"amazon_frame_drop_fix\">Rezolvați căderea cadrului #2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">Destinat dispozitivelor Amazon Stick. Poate funcționa și pe alte dispozitive.</string>\n  <string name=\"default_stack_desc\">Motor de rețea încorporat. Acest motor poate avea o stabilitate mai bună în anumite situații.</string>\n  <string name=\"item_postion\">Poziția lui</string>\n  <string name=\"player_screen_off_dimming\">Cantitate de diminuare a ecranului dezactivat</string>\n  <string name=\"screensaver_timout\">Expirare economizor de ecran</string>\n  <string name=\"screensaver_dimming\">Cantitate de diminuare a economizorului de ecran</string>\n  <string name=\"player_ui_on_next\">Afișați Interfața de Utilizare a playerului atunci când comutați la următorul videoclip</string>\n  <string name=\"autogenerated\">generat automat</string>\n  <string name=\"player_auto_volume\">Reglarea automată a volumului</string>\n  <string name=\"header_shorts\">Shorts</string>\n  <string name=\"player_global_focus\">Navigare perfectă între rândurile butoanelor playerului</string>\n  <string name=\"auto_history\">Auto (utilizați setările contului)</string>\n  <string name=\"remember_position_subscriptions\">Reține ultima poziție vizualizată în Abonamente</string>\n  <string name=\"remember_position_pinned\">Reține ultima poziție vizualizată în listele de redare fixate</string>\n  <string name=\"msg_player_unknown_error\">Eroare necunoscută</string>\n  <string name=\"unknown_source_error\">Eroare sursă necunoscută</string>\n  <string name=\"unknown_renderer_error\">Eroare de randare necunoscută</string>\n  <string name=\"header_notifications\">Notificări</string>\n  <string name=\"color_scheme_blue\">Albastru</string>\n  <string name=\"disable_search_history\">Dezactivați istoricul căutărilor</string>\n  <string name=\"unlock_high_bitrate_formats\">Deblocați formate 1080p vp9 cu rată de biți ridicată</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Deblocați formate mp4a cu rată de biți ridicată</string>\n  <string name=\"color_scheme_dark_blue\">Albastru închis</string>\n  <string name=\"player_speed_per_channel\">Pentru fiecare canal</string>\n  <string name=\"disable_popular_searches\">Dezactivați interogările de căutare populare</string>\n  <string name=\"multi_profiles\">Utilizați setări separate pentru fiecare cont</string>\n  <string name=\"protect_account_with_password\">Protejați acest cont cu parolă</string>\n  <string name=\"enter_account_password\">Introduceți parola contului</string>\n  <string name=\"show_connect_messages\">Afișați mesajele de conectare</string>\n  <string name=\"prefer_avc_over_vp9_desc\">AVERTIZARE: maxim 1080p</string>\n  <string name=\"play_next\">Rada următorul</string>\n  <string name=\"hide_watched_from_subscriptions\">Ascunde videoclipurile vizionate din Abonamente</string>\n  <string name=\"hide_watched_from_notifications\">Ascunde videoclipurile vizionate din Notificări</string>\n  <string name=\"hide_unwanted_content\">Ascundeți conținutul nedorit</string>\n  <string name=\"hide_watched_from_home\">Ascunde videoclipurile vizionate din Acasă</string>\n  <string name=\"hide_watched_from_watch_later\">Ascundeți videoclipurile vizionate din lista de redare Vizionează mai târziu</string>\n  <string name=\"remote_control_permission\">Telecomanda în fundal necesită permisiunea Suprapunere</string>\n  <string name=\"login_from_browser\">Conectează-te din browser</string>\n  <string name=\"disable_remote_history\">Dezactivați istoricul atunci când utilizați telecomanda</string>\n  <string name=\"keyboard_fix\">Împiedicați deschiderea tastaturii de către butonul OK (G20s și altele)</string>\n  <string name=\"nothing_found\">Nu s-a găsit nimic</string>\n  <string name=\"auto_frame_rate_desc\">Această opțiune elimină jittering-ul în scenele în care camera se mișcă rapid, de ex. flux sportiv</string>\n  <string name=\"channel_filter_hint\">Filtrați canalele</string>\n  <string name=\"player_loop_shorts\">Redați Short-urile în buclă</string>\n  <string name=\"player_quick_shorts_skip\">Omiteți videoclipurile scurte cu butoanele stânga/dreapta</string>\n  <string name=\"player_quick_skip_videos\">Omiteți videoclipurile obișnuite cu butoanele stânga/dreapta</string>\n  <string name=\"channels_filter\">Afișați câmpul Filtrare canale în secțiunea Canale</string>\n  <string name=\"channel_search_bar\">Bara de căutare în paginia Canalului</string>\n  <string name=\"header_sports\">Sporturi</string>\n  <string name=\"keep_finished_activities\">Păstrați activitățile terminate</string>\n  <string name=\"disable_channels_service\">Dezactivați serviciul Canale</string>\n  <string name=\"replace_titles\">Înlocuiți titlurile</string>\n  <string name=\"enable\">Activați</string>\n  <string name=\"more_info\">Mai multe informații</string>\n  <string name=\"about_sponsorblock\">Despre SponsorBlock</string>\n  <string name=\"about_dearrow\">Despre DeArrow</string>\n  <string name=\"replace_thumbnails\">Înlocuiți miniaturile</string>\n  <string name=\"crowdsourced_thumbnails\">Miniaturi crowdsourced</string>\n  <string name=\"crowdsoursed_titles\">Titluri crowdsourced</string>\n  <string name=\"dearrow_not_submitted_thumbs\">Sursă miniatură (dacă nu există nicio trimitere DeArrow)</string>\n  <string name=\"pitch_effect\">Efect înalt</string>\n  <string name=\"fullscreen_mode\">Modul ecran complet (fără bare de sistem)</string>\n  <string name=\"player_only_mode\">Afișați numai playerul dacă videoclipul este deschis în afara aplicației</string>\n  <string name=\"pinned_channel_rows\">Afișați canalul fixat ca rânduri</string>\n  <string name=\"prefer_ipv4\">Preferați DNS IPv4</string>\n  <string name=\"prefer_ipv4_desc\">S-ar putea remedia situațiile în care aplicația nu funcționează deloc.\\nNotă. Poate provoca blocări și prăbușiri (în special pe dispozitivele Android 8 sau Dune HD)</string>\n  <string name=\"long_press_for_settings\">APĂSAȚI LUNG PENTRU SETĂRI</string>\n  <string name=\"long_press_for_options\">APĂSAȚI LUNG PENTRU OPȚIUNI</string>\n  <string name=\"device_specific_backup\">Faceți copie de rezervă doar pentru acest dispozitiv</string>\n  <string name=\"local_backup\">Copie de rezervă locală</string>\n  <string name=\"auto_backup\">Copie de rezervă automată (o dată pe zi)</string>\n  <string name=\"repeat_mode_reverse_list\">Redă lista de redare sau videoclipurile canalului în ordine inversă</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-ru/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"title_search\">Поиск</string>\n    <string name=\"header_home\">Главная</string>\n    <string name=\"header_subscriptions\">Подписки</string>\n    <string name=\"header_history\">История</string>\n    <string name=\"header_music\">Музыка</string>\n    <string name=\"header_news\">Новости</string>\n    <string name=\"header_gaming\">Видеоигры</string>\n    <string name=\"header_playlists\">Плейлисты</string>\n    <string name=\"header_settings\">Настройки</string>\n    <string name=\"subscriptions_signin_title\">Смотрите новые видео с каналов, на которые вы подписаны</string>\n    <string name=\"subscriptions_signin_subtitle\">Для начала вам нужно войти в аккаунт.</string>\n    <string name=\"subscriptions_signin_button_text\">ВОЙТИ</string>\n    <string name=\"library_signin_to_show_more\">Здесь находятся понравившиеся, сохраненные и видео из подписок</string>\n    <string name=\"library_signin_subtitle\">Чтобы увидеть их, войдите в аккаунт.</string>\n    <string name=\"action_signin\">ВОЙТИ</string>\n    <string name=\"title_video_formats\">Видеоформат</string>\n    <string name=\"title_audio_formats\">Аудиоформат</string>\n    <string name=\"subtitle_category_title\">Субтитры</string>\n    <string name=\"playback_queue_category_title\">Очередь воспроизведения</string>\n    <string name=\"video_max_quality\">Авто (макс. качество)</string>\n    <string name=\"audio_max_quality\">Авто (макс. качество)</string>\n    <string name=\"subtitles_disabled\">Субтитры отключены</string>\n    <string name=\"auto_frame_rate\">Auto Frame Rate</string>\n    <string name=\"frame_rate_correction\">Исправлять fps:\\n%s</string>\n    <string name=\"category_background_playback\">Фоновое воспроизведение</string>\n    <string name=\"not_implemented\">Пока не готово</string>\n    <string name=\"not_supported_by_device\">Устройство не поддерживает эту функцию</string>\n    <string name=\"option_background_playback_off\">Отключено</string>\n    <string name=\"option_background_playback_pip\">Картинка в картинке</string>\n    <string name=\"option_background_playback_only_audio\">Только аудио</string>\n    <string name=\"playback_settings\">Настройки качества воспроизведения</string>\n    <string name=\"video_speed\">Скорость видео</string>\n    <string name=\"resolution_switch\">Переключать разрешение</string>\n    <string name=\"video_buffer\">Видеобуфер</string>\n    <string name=\"video_buffer_size_none\">Нет</string>\n    <string name=\"video_buffer_size_lowest\">Самый низкий</string>\n    <string name=\"video_buffer_size_low\">Низкий</string>\n    <string name=\"video_buffer_size_med\">Средний</string>\n    <string name=\"video_buffer_size_high\">Высокий</string>\n    <string name=\"video_buffer_size_highest\">Самый высокий</string>\n    <string name=\"update_changelog\">Изменения</string>\n    <string name=\"install_update\">Установить обновление</string>\n    <string name=\"section_is_empty\">Здесь ничего нет</string>\n    <string name=\"dialog_add_to_playlist\">Добавить/Удалить из плейлиста</string>\n    <string name=\"msg_signed_users_only\">Необходимо войти в вккаунт</string>\n    <string name=\"msg_cant_load_content\">Не получилось загрузить содержимое.\\n1) Включите историю просмотров.\\n2) Проверьте дату и время на устройстве.\\n3) Проверьте сетевое подключение.\\n4) Проверьте настройки прокси.\\n5) Попробуйте войти в аккаунт.\\n6) Возможно, здесь ничего нет.</string>\n    <string name=\"title_video_presets\">Профили видео</string>\n    <string name=\"settings_accounts\">Аккаунты</string>\n    <string name=\"settings_left_panel\">Настроить категории</string>\n    <string name=\"settings_themes\">Темы</string>\n    <string name=\"settings_other\">Разное</string>\n    <string name=\"settings_player\">Плеер</string>\n    <string name=\"settings_language\">Язык</string>\n    <string name=\"settings_linked_devices\">Привязанные устройства</string>\n    <string name=\"settings_about\">О приложении</string>\n    <string name=\"dialog_account_list\">Выбрать аккаунт</string>\n    <string name=\"dialog_account_none\">Не выбран</string>\n    <string name=\"dialog_remove_account\">Выйти из аккаунта</string>\n    <string name=\"dialog_add_account\">Войти в аккаунт</string>\n    <string name=\"default_lang\">По умолчанию</string>\n    <string name=\"original_lang\">Оригинальный</string>\n    <string name=\"dialog_select_language\">Язык</string>\n    <string name=\"header_channels\">Каналы</string>\n    <string name=\"subtitle_default\">Обычные</string>\n    <string name=\"subtitle_white_semi_transparent\">Белые на полупрозрачном фоне</string>\n    <string name=\"subtitle_style\">Стиль субтитров</string>\n    <string name=\"subtitle_language\">Язык субтитров</string>\n    <string name=\"action_search\">Поиск</string>\n    <string name=\"settings_main_ui\">Интерфейс</string>\n    <string name=\"dialog_main_ui\">Интерфейс приложения</string>\n    <string name=\"card_animated_previews\">Анимированный предпросмотр</string>\n    <string name=\"web_site\">Веб-сайт</string>\n    <string name=\"donation\">Поддержать проект</string>\n    <string name=\"dialog_about\">О приложении</string>\n    <string name=\"dialog_player_ui\">Видеоплеер</string>\n    <string name=\"player_show_ui_on_pause\">Показать интерфейс на паузе</string>\n    <string name=\"player_pause_on_ok\">Клавиша OK останавливает воспроизведение</string>\n    <string name=\"player_ok_button_behavior\">Поведение кнопки OK</string>\n    <string name=\"player_only_ui\">Только интерфейс</string>\n    <string name=\"player_ui_and_pause\">Интерфейс и пауза</string>\n    <string name=\"player_only_pause\">Только пауза</string>\n    <string name=\"player_toggle_speed\">Включение/выключение скорости</string>\n    <string name=\"check_for_updates\">Проверить обновление</string>\n    <string name=\"update_not_found\">Ваша версия не требует обновления</string>\n    <string name=\"update_in_progress\">Подождите…</string>\n    <string name=\"player_ui_hide_behavior\">Скрывать интерфейс</string>\n    <string name=\"option_never\">Никогда</string>\n    <string name=\"side_panel_sections\">Настроить разделы</string>\n    <string name=\"boot_to_section\">Загружаться в раздел</string>\n    <string name=\"large_ui\">Увеличенный интерфейс</string>\n    <string name=\"video_grid_scale\">Масштаб сетки роликов</string>\n    <string name=\"scale_ui\">Масштаб интерфейса</string>\n    <string name=\"color_scheme\">Цветовая схема</string>\n    <string name=\"color_scheme_default\">По умолчанию</string>\n    <string name=\"color_scheme_red_grey\">Красно-серая</string>\n    <string name=\"color_scheme_red\">Красная</string>\n    <string name=\"color_scheme_dark_grey\">Тёмно-серая</string>\n    <string name=\"disable_update_check\">Не проверять обновления</string>\n    <string name=\"show_again\">Напоминать постоянно</string>\n    <string name=\"check_updates_auto\">Сообщать о наличии обновлений</string>\n    <string name=\"select_account_on_boot\">Показывать диалог выбора аккаунта на старте приложения</string>\n    <string name=\"player_other\">Разное</string>\n    <string name=\"player_full_date\">Точная дата в описании</string>\n    <string name=\"open_channel\">Перейти на канал</string>\n    <string name=\"open_playlist\">Открыть плейлист</string>\n    <string name=\"not_interested\">Не рекомендовать</string>\n    <string name=\"not_recommend_channel\">Не рекомендовать канал</string>\n    <string name=\"you_wont_see_this_video\">Это видео больше не появится в рекомендациях</string>\n    <string name=\"you_wont_see_this_channel\">Этот канал больше не появится в рекомендациях</string>\n    <string name=\"settings_search\">Поиск</string>\n    <string name=\"dialog_search\">Поиск</string>\n    <string name=\"instant_voice_search\">Сразу запускать голосовой поиск</string>\n    <string name=\"option_background_playback_behind\">Играть позади</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">Информация о следующем видео ещё не загрузилась</string>\n    <string name=\"card_multiline_title\">Несколько строк в заголовках</string>\n    <string name=\"cards_style\">Стиль карточек</string>\n    <string name=\"repeat_mode_all\">Воспроизводить видео непрерывно</string>\n    <string name=\"repeat_mode_one\">Повторять текущее видео</string>\n    <string name=\"repeat_mode_pause\">Ставить воспроизведение на паузу после каждого видео (кроме очереди)</string>\n    <string name=\"repeat_mode_pause_alt\">Воспроизводить непрерывно только видео из плейлиста</string>\n    <string name=\"repeat_mode_none\">Завершать воспроизведение после одного видео (кроме очереди)</string>\n    <string name=\"subscribed_to_channel\">Подписка оформлена</string>\n    <string name=\"unsubscribed_from_channel\">Подписка не оформлена</string>\n    <string name=\"subtitle_yellow_transparent\">Жёлтые на прозрачном фоне</string>\n    <string name=\"subtitle_white_transparent\">Белые на прозрачном фоне</string>\n    <string name=\"subtitle_white_black\">Белые на чёрном фоне</string>\n    <string name=\"player_seek_preview\">Предпросмотр при перемотке</string>\n    <string name=\"color_scheme_dark_grey_oled\">Тёмно-серая (OLED)</string>\n    <string name=\"channels_section_sorting\">Сортировка раздела Каналы</string>\n    <string name=\"sorting_by_new_content\">Новый контент</string>\n    <string name=\"sorting_alphabetically\">По алфавиту</string>\n    <string name=\"sorting_last_viewed\">Последние просмотренные</string>\n    <string name=\"player_pause_when_seek\">Пауза при перемотке</string>\n    <string name=\"playlists_style\">Стиль раздела Плейлисты</string>\n    <string name=\"playlists_style_grid\">Сетка</string>\n    <string name=\"playlists_style_rows\">Строки</string>\n    <string name=\"player_show_clock\">Отображать часы в панели управления</string>\n    <string name=\"player_show_remaining_time\">Отображать оставшееся время в панели управления</string>\n    <string name=\"player_show_ending_time\">Отображать время окончания в панели управления</string>\n    <string name=\"open_channel_uploads\">Открыть загрузки</string>\n    <string name=\"app_exit_shortcut\">Выход из приложения</string>\n    <string name=\"player_exit_shortcut\">Выход из плеера</string>\n    <string name=\"search_exit_shortcut\">Выход из поиска</string>\n    <string name=\"app_exit_none\">Не выходить</string>\n    <string name=\"app_double_back_exit\">Двойное назад</string>\n    <string name=\"app_single_back_exit\">Одинарное назад</string>\n    <string name=\"action_video_zoom\">Видео зум</string>\n    <string name=\"video_zoom_default\">По умолчанию</string>\n    <string name=\"video_zoom_fit_width\">По ширине</string>\n    <string name=\"video_zoom_fit_height\">По высоте</string>\n    <string name=\"video_zoom_fit_both\">По ширине или высоте</string>\n    <string name=\"video_zoom_stretch\">Растянуть</string>\n    <string name=\"video_zoom\">Видео зум</string>\n    <string name=\"color_scheme_teal\">Бирюзовая</string>\n    <string name=\"color_scheme_teal_oled\">Бирюзовая (OLED)</string>\n    <string name=\"player_seek_preview_none\">Отключено</string>\n    <string name=\"player_seek_preview_single\">Один кадр</string>\n    <string name=\"player_seek_preview_carousel\">Карусель</string>\n    <string name=\"player_seek_preview_carousel_slow\">Карусель (медленно, по ключевым кадрам)</string>\n    <string name=\"player_seek_preview_carousel_fast\">Карусель (быстро, не точные превью)</string>\n    <string name=\"unsubscribe_from_channel\">Отписаться от канала</string>\n    <string name=\"card_title_lines_num\">Количество строк в карточках</string>\n    <string name=\"card_auto_scrolled_title\">Прокручивать обрезанный заголовок</string>\n    <string name=\"share_link\">Поделиться ссылкой</string>\n    <string name=\"share_qr_link\">Поделиться ссылкой (QR код)</string>\n    <string name=\"subscribe_to_channel\">Подписаться на канал</string>\n    <string name=\"wait_data_loading\">Подождите пока загружаются данные…</string>\n    <string name=\"auto_frame_rate_pause\">Auto Frame Rate (пауза при переключении)</string>\n    <string name=\"auto_frame_rate_applying\">Применяется автофреймрейт %sx%s@%s</string>\n    <string name=\"audio_shift\">Смещение аудио</string>\n    <string name=\"player_remember_speed\">Запоминать скорость</string>\n    <string name=\"mark_channel_as_watched\">Отметить как просмотренное</string>\n    <string name=\"channel_marked_as_watched\">Канал отмечен как просмотренный</string>\n    <string name=\"dialog_add_device\">Добавить устройство</string>\n    <string name=\"dialog_remove_all_devices\">Удалить все устройства</string>\n    <string name=\"device_link_enabled\">Привязка активна</string>\n    <string name=\"device_connected\">Устройство \\\"%s\\\" подключено</string>\n    <string name=\"device_disconnected\">Устройство \\\"%s\\\" отключено</string>\n    <string name=\"settings_ui_scale\">Масштаб интерфейса</string>\n    <string name=\"msg_restart_app\">Пожалуйста, перезапустите приложение, чтобы изменения вступили в силу</string>\n    <string name=\"settings_block\">Блокировка контента</string>\n    <string name=\"msg_applying\">Применяется %s…</string>\n    <string name=\"msg_done\">Готово</string>\n    <string name=\"settings_general\">Общие</string>\n    <string name=\"settings_video\">Видео</string>\n    <string name=\"content_block_confirm_skip\">Подтверждение при пропуске</string>\n    <string name=\"content_block_categories\">Пропускать сегменты</string>\n    <string name=\"content_block_sponsor\">Спонсорский блок</string>\n    <string name=\"content_block_intro\">Вступление</string>\n    <string name=\"content_block_outro\">Заключительный экран/титры</string>\n    <string name=\"content_block_interaction\">Напоминание о подписке</string>\n    <string name=\"content_block_self_promo\">Самореклама</string>\n    <string name=\"content_block_music_off_topic\">Немузыкальная секция клипа</string>\n    <string name=\"confirm_segment_skip\">Пропустить сегмент \\\"%s\\\"?</string>\n    <string name=\"cancel_segment_skip\">Отменить пропуск сегмента</string>\n    <string name=\"msg_skipping_segment\">Пропуск сегмента \\\"%s\\\"…</string>\n    <string name=\"content_block_notification_type\">Тип уведомлений</string>\n    <string name=\"content_block_notify_none\">Без уведомлений</string>\n    <string name=\"content_block_notify_toast\">Всплывающее сообщение</string>\n    <string name=\"content_block_notify_dialog\">Диалог подтверждения</string>\n    <string name=\"return_to_launcher\">Возвращаться в лаунчер из ATV каналов/поиска</string>\n    <string name=\"intent_force_close\">Принудительный выход, если видео открыто из внешнего источника</string>\n    <string name=\"btn_confirm\">Подтвердить</string>\n    <string name=\"player_low_video_quality\">Низкое качество видео</string>\n    <string name=\"remote_session_closed\">Удалённая сессия закрыта</string>\n    <string name=\"msg_mode_switch_error\">Не получилось переключить дисплей в режим \\\"%s\\\"</string>\n    <string name=\"msg_player_error\">В плеере возникла ошибка %s. Перезапускаю воспроизведение…</string>\n    <string name=\"video_preset_disabled\">Без профиля</string>\n    <string name=\"video_preset_enabled\">Формат следующего видео будет установлен в соответствии с профилем</string>\n    <string name=\"player_sleep_timer\">Таймер сна</string>\n    <string name=\"player_show_quality_info\">Отображать информацию о качестве видео в панели управления</string>\n    <string name=\"player_remember_each_speed\">Запоминать скорость каждого видео</string>\n    <string name=\"player_remember_speed_none\">Не запоминать</string>\n    <string name=\"player_remember_speed_all\">Одинаковая скорость на всех видео</string>\n    <string name=\"player_remember_speed_each\">На каждом видео своя скорость</string>\n    <string name=\"msg_player_error_source\">Не получается загрузить видео</string>\n    <string name=\"msg_player_error_renderer\">Выбранный формат не поддерживается.\\nПопробуйте выбрать другой формат.\\nЕсли это не поможет, то перезагрузите устройство.</string>\n    <string name=\"msg_player_error_video_renderer\">Выбранный ВИДЕОФОРМАТ не поддерживается.\\nПопробуйте выбрать другой формат, нажав на кнопку HQ в плеере.\\nЕсли это не поможет, то перезагрузите устройство.</string>\n    <string name=\"msg_player_error_audio_renderer\">Выбранный АУДИОФОРМАТ не поддерживается.\\nПопробуйте выбрать другой формат, нажав на кнопку HQ в плеере.\\nЕсли это не поможет, то перезагрузите устройство.</string>\n    <string name=\"msg_player_error_subtitle_renderer\">Выбранный формат СУБТИТРОВ не поддерживается.\\nПопробуйте выбрать другой формат, удерживая кнопку CC в плеере.\\nЕсли это не поможет, то перезагрузите устройство.</string>\n    <string name=\"msg_player_error_unexpected\">Неизвестная ошибка в видеодекодере</string>\n    <string name=\"video_aspect\">Соотношение сторон</string>\n    <string name=\"player_tweaks\">Для разработчиков</string>\n    <string name=\"header_uploads\">Загрузки</string>\n    <string name=\"player_show_global_clock\">Постоянно отображать на экране часы</string>\n    <string name=\"player_show_global_ending_time\">Постоянно отображать на экране время окончания видео</string>\n    <string name=\"app_corner_clock\">Главная страница: часы в правом верхнем углу</string>\n    <string name=\"player_corner_clock\">Плеер: часы в правом верхнем углу</string>\n    <string name=\"player_corner_ending_time\">Плеер: время окончания в правом верхнем углу</string>\n    <string name=\"uploads_old_look\">Вернуть старый вид раздела Загрузки</string>\n    <string name=\"background_playback_activation\">Фоновое воспроизведение (активация)</string>\n    <string name=\"channels_old_look\">Вернуть старый вид раздела Каналы</string>\n    <string name=\"channels_auto_load\">Автоматически загружать содержимое раздела Каналы</string>\n    <string name=\"return_to_background_video\">Вернуться к фоновому видео</string>\n    <string name=\"pin_unpin_from_sidebar\">Добавить/Удалить из боковой панели</string>\n    <string name=\"pin_unpin_playlist\">Добавить/Удалить плейлист из боковой панели</string>\n    <string name=\"pin_unpin_channel\">Добавить/Удалить канал из боковой панели</string>\n    <string name=\"pin_playlist\">Добавить плейлист на боковую панель</string>\n    <string name=\"pin_channel\">Добавить канал на боковую панель</string>\n    <string name=\"pinned_to_sidebar\">Добавлено на боковую панель</string>\n    <string name=\"unpin_from_sidebar\">Удалить из боковой панели</string>\n    <string name=\"unpin_group_from_sidebar\">Удалить группу подписок</string>\n    <string name=\"unpinned_from_sidebar\">Удалено из боковой панели</string>\n    <string name=\"dialog_select_country\">Страна</string>\n    <string name=\"settings_language_country\">Язык/Страна</string>\n    <string name=\"share_embed_link\">Поделиться встраиваемой ссылкой</string>\n    <string name=\"card_text_scroll_factor\">Скорость прокрутки текста карточки</string>\n    <string name=\"preferred_update_source\">Выбрать источник обновлений</string>\n    <string name=\"hide_shorts\">Скрывать shorts из Подписок</string>\n    <string name=\"hide_shorts_channel\">Скрывать shorts на канале</string>\n    <string name=\"key_remapping\">Переопределение клавиш</string>\n    <string name=\"screen_dimming\">Затемнение экрана</string>\n    <string name=\"removed_from_playback_queue\">Удалено из очереди воспроизведения</string>\n    <string name=\"added_to_playback_queue\">Добавлено в очередь воспроизведения</string>\n    <string name=\"add_remove_from_playback_queue\">Добавить/Удалить из очереди воспроизведения</string>\n    <string name=\"add_to_playback_queue\">Добавить в очередь воспроизведения</string>\n    <string name=\"remove_from_playback_queue\">Удалить из очереди воспроизведения</string>\n    <string name=\"proxy_enabled\">Прокси активирован</string>\n    <string name=\"proxy_disabled\">Прокси деактивирован</string>\n    <string name=\"uploads_row_name\">Все видео</string>\n    <string name=\"playlists_row_name\">Все плейлисты</string>\n    <string name=\"popular_uploads_row_name\">Популярные</string>\n    <string name=\"news_row_name\">Новости</string>\n    <string name=\"breaking_news_row_name\">Срочные новости</string>\n    <string name=\"covid_news_row_name\">Новости о коронавирусе COVID-19</string>\n    <string name=\"enable_voice_search\">Включить глобальный поиск (требуется поддержка прошивки)</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">Подписаться/Отписаться от канала</string>\n    <string name=\"app_backup_restore\">Бэкап и восстановление</string>\n    <string name=\"app_backup\">Резервное копирование данных приложения</string>\n    <string name=\"app_restore\">Восстановить данные приложения</string>\n    <string name=\"skip_each_segment_once\">Не пропускать сегменты повторно</string>\n    <string name=\"disable_ok_long_press\">Не обрабатывать долгое удержание кнопки ОК</string>\n    <string name=\"disable_ok_long_press_desc\">Предназначено для глючных пультов, где кнопка OK не работает должным образом</string>\n    <string name=\"player_time_correction\">Отображать время с учётом скорости воспроизведения</string>\n    <string name=\"sponsor_color_markers\">Цветовые метки на шкале прогресса</string>\n    <string name=\"refresh_section\">Обновить раздел</string>\n    <string name=\"option_disabled\">Отключено</string>\n    <string name=\"live_now_row_name\">Сейчас в эфире</string>\n    <string name=\"run_in_background\">Фоновое воспроизведение</string>\n    <string name=\"settings_remote_control\">Дистанционное управление</string>\n    <string name=\"background_service_started\">Фоновый сервис запущен</string>\n    <string name=\"dialog_add_to\">Добавить в %s</string>\n    <string name=\"dialog_remove_from\">Удалить из %s</string>\n    <string name=\"added_to\">Добавлено в %s</string>\n    <string name=\"removed_from\">Удалено из %s</string>\n    <string name=\"video_preset_adaptive\">Адаптивный</string>\n    <string name=\"content_block_preview_recap\">Содержание предыдущего эпизода</string>\n    <string name=\"content_block_highlight\">Интересный момент (закладка)</string>\n    <string name=\"removed_from_history\">Видео удалено из истории</string>\n    <string name=\"remove_from_history\">Удалить из истории</string>\n    <string name=\"upload_date\">Дата загрузки</string>\n    <string name=\"upload_date_any\">Всё время</string>\n    <string name=\"upload_date_today\">Сегодня</string>\n    <string name=\"upload_date_this_week\">Эта неделя</string>\n    <string name=\"upload_date_this_month\">Этот месяц</string>\n    <string name=\"upload_date_this_year\">Этот год</string>\n    <string name=\"double_refresh_rate\">Удваивать частоту</string>\n    <string name=\"upload_date_last_hour\">Последний час</string>\n    <string name=\"focus_on_search_results\">Автофокусировка на результатах поиска</string>\n    <string name=\"context_menu\">Контекстное меню</string>\n    <string name=\"context_menu_sorting\">Сортировка контекстного меню</string>\n    <string name=\"add_remove_from_recent_playlist\">Добавить/Удалить из недавнего плейлиста</string>\n    <string name=\"hide_settings_section\">Прятать раздел Настройки (опасно!)</string>\n    <string name=\"volume\">Громкость %s%%</string>\n    <string name=\"mark_all_channels_watched\">Отметить все каналы как просмотренные</string>\n    <string name=\"move_section_up\">Переместить раздел выше</string>\n    <string name=\"move_section_down\">Переместить раздел ниже</string>\n    <string name=\"player_buttons\">Настроить кнопки плеера</string>\n    <string name=\"action_playlist_add\">Добавить в плейлист</string>\n    <string name=\"action_video_stats\">Информация для админов</string>\n    <string name=\"action_subscribe\">Подписаться</string>\n    <string name=\"action_channel\">Открыть канал</string>\n    <string name=\"action_pip\">Картинка в картинке</string>\n    <string name=\"action_video_info\">Описание видео</string>\n    <string name=\"action_screen_off\">Выключить экран</string>\n    <string name=\"action_sound_off\">Без звука</string>\n    <string name=\"action_playback_queue\">Очередь воспроизведения</string>\n    <string name=\"action_video_speed\">Скорость видео</string>\n    <string name=\"action_subtitles\">Субтитры</string>\n    <string name=\"action_like\">Нравится</string>\n    <string name=\"action_dislike\">Не нравится</string>\n    <string name=\"action_play_pause\">Воспроизведение/Пауза</string>\n    <string name=\"action_repeat_mode\">Режим воспроизведения</string>\n    <string name=\"action_next\">Следующее видео</string>\n    <string name=\"action_previous\">Предыдущее видео</string>\n    <string name=\"action_high_quality\">Качество видео</string>\n    <string name=\"content_block_no_skipping_mode\">Режим без пропусков</string>\n    <string name=\"content_block_action_type\">Выбрать действие</string>\n    <string name=\"content_block_action_none\">Ничего не делать</string>\n    <string name=\"content_block_action_only_skip\">Только пропуск</string>\n    <string name=\"content_block_action_toast\">Пропуск с уведомлением</string>\n    <string name=\"content_block_action_dialog\">Диалог подтверждения</string>\n    <string name=\"content_block_filler\">Отход от темы (оффтопик)</string>\n    <string name=\"keyboard_auto_show\">Показывать клавиатуру автоматически</string>\n    <string name=\"cancel_dialog\">Отменить</string>\n    <string name=\"msg_player_error_source2\">Ссылка не работает или неправильное время на устройстве.\\nСкорее всего проблема в приложении.</string>\n    <string name=\"msg_player_error_video_source\">Ссылка на ВИДЕОПОТОК не работает или неправильное время на устройстве.\\nСкорее всего проблема в приложении.</string>\n    <string name=\"msg_player_error_audio_source\">Ссылка на АУДИОПОТОК не работает или неправильное время на устройстве.\\nСкорее всего проблема в приложении.</string>\n    <string name=\"msg_player_error_subtitle_source\">Ссылка на СУБТИТРЫ не работает или неправильное время на устройстве.\\nСкорее всего проблема в приложении.</string>\n    <string name=\"hide_upcoming\">Скрывать анонсы из Подписок</string>\n    <string name=\"hide_upcoming_channel\">Скрывать анонсы на Канале</string>\n    <string name=\"hide_upcoming_home\">Скрывать анонсы из Главного раздела</string>\n    <string name=\"search_background_playback\">Фоновое воспроизведение при поиске/просмотре канала</string>\n    <string name=\"trending_row_name\">В тренде</string>\n    <string name=\"player_seek_type\">Поведение перемотки</string>\n    <string name=\"player_seek_regular\">Обычное</string>\n    <string name=\"player_seek_confirmation_pause\">С подтверждением (пауза во время перемотки)</string>\n    <string name=\"player_seek_confirmation_play\">С подтверждением (играть во время перемотки)</string>\n    <string name=\"hide_shorts_from_home\">Скрывать shorts с Главной</string>\n    <string name=\"finish_on_disconnect\">Завершать приложение после отключения телефона/планшета</string>\n    <string name=\"update_error\">Ошибка обновления</string>\n    <string name=\"hide_shorts_from_search\">Скрывать shorts из Поиска</string>\n    <string name=\"hide_shorts_from_history\">Скрывать shorts из Истории</string>\n    <string name=\"hide_shorts_from_trending\">Скрывать shorts из Трендов</string>\n    <string name=\"disable_screensaver\">Отключить скринсейвер</string>\n    <string name=\"description_not_found\">Описание не найдено</string>\n    <string name=\"proxy_port_hint\">Порт прокси</string>\n    <string name=\"proxy_host_hint\">Имя или IP-адрес прокси-сервера</string>\n    <string name=\"proxy_username_hint\">Имя пользователя прокси</string>\n    <string name=\"proxy_password_hint\">Пароль прокси</string>\n    <string name=\"proxy_type\">Тип прокси</string>\n    <string name=\"enable_web_proxy\">Использовать веб-прокси</string>\n    <string name=\"proxy_not_supported\">Использовать веб-прокси (требуется Android 4.4+)</string>\n    <string name=\"proxy_test_btn\">Тест</string>\n    <string name=\"proxy_settings_title\">Настройки прокси-сервера</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">Тест №%d: отменен.</string>\n    <string name=\"proxy_type_invalid\">Тип прокси не задан.</string>\n    <string name=\"proxy_host_invalid\">Хост прокси не задан.</string>\n    <string name=\"proxy_port_invalid\">Недопустимый порт прокси-сервера, должен &gt; 0.</string>\n    <string name=\"proxy_credentials_invalid\">Недопустимое имя пользователя или пароль.</string>\n    <string name=\"proxy_test_aborted\">Тест прерван, сначала исправьте настройки прокси.</string>\n    <string name=\"proxy_application_aborted\">Пожалуйста, сначала исправьте настройки прокси.</string>\n    <string name=\"proxy_test_start\">Тест#%d: %s…</string>\n    <string name=\"proxy_test_error\">Ошибка#%d: %s</string>\n    <string name=\"proxy_test_status\">Статус#%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">Адрес файла конфигурации (*.ovpn)</string>\n    <string name=\"enable_openvpn\">Использовать OpenVPN</string>\n    <string name=\"openvpn_settings_title\">Настройки OpenVPN</string>\n    <string name=\"openvpn_application_aborted\">Пожалуйста, сначала исправьте настройки OpenVPN.</string>\n    <string name=\"openvpn_test_aborted\">Тест прерван, сначала исправьте настройки OpenVPN.</string>\n    <string name=\"openvpn_address_invalid\">Адрес файла конфигурации не задан.</string>\n    <string name=\"internet_censorship\">Интернет-цензура</string>\n    <string name=\"rename_section\">Переименовать этот раздел</string>\n    <string name=\"simple_edit_value_hint\">Введите значение</string>\n    <string name=\"seek_interval\">Интервал перемотки</string>\n    <string name=\"seek_interval_sec\">%s сек</string>\n    <string name=\"subtitle_system\">Системный стиль (Настройки Андроид > Спец. возможности)</string>\n    <string name=\"subtitle_scale\">Масштаб субтитров</string>\n    <string name=\"audio_sync_fix_desc\">Альтернативный способ синхронизации аудио/видео. Считается устаревшим, но, в некоторых случаях, помогает.</string>\n    <string name=\"audio_sync_fix\">Исправление рассинхронизации звука</string>\n    <string name=\"ambilight_ratio_fix_desc\">Исправляет отсутствующую фоновую подсветку. Исправляет неправильное соотношение сторон или масштаб видео. Исправляет пустые скриншоты. Влияет на производительность!</string>\n    <string name=\"ambilight_ratio_fix\">Исправление Ambilight, соотношения сторон, масштаба видео и скриншотов</string>\n    <string name=\"force_legacy_codecs_desc\">Значительно повышает производительность на слабых устройствах. Максимальное разрешение 720p.</string>\n    <string name=\"force_legacy_codecs\">Использовать устаревшие кодеки (720p)</string>\n    <string name=\"live_stream_fix_desc\">Внимание: перемотка трансляций будет недоступна. Значительно повышает производительность трансляций на слабых устройствах. Максимальное разрешение 1080p.</string>\n    <string name=\"live_stream_fix_4k_desc\">Внимание: перемотка трансляций будет недоступна. Значительно повышает производительность трансляций. Максимальное разрешение 4K.</string>\n    <string name=\"live_stream_fix\">Исправление прямых трансляций (1080p)</string>\n    <string name=\"live_stream_fix_4k\">Исправление прямых трансляций (4K)</string>\n    <string name=\"playback_notifications_fix_desc\">Скрывает уведомление о смене воспроизводимого трека. Может быть полезным на прошивках на базе AOSP.</string>\n    <string name=\"playback_notifications_fix\">Отключить уведомления о воспроизведении</string>\n    <string name=\"amlogic_fix_desc\">Исправление потери кадров на устройствах на базе Amlogic.</string>\n    <string name=\"amlogic_fix\">Исправление 1080p@60fps на Amlogic</string>\n    <string name=\"tunneled_video_playback_desc\">Внимание: могут быть проблемы с паузой! Туннельное воспроизведение обещает лучшую синхронизацию аудио/видео и более плавное воспроизведение. Требуется Андроид 5+</string>\n    <string name=\"tunneled_video_playback\">Туннельное воспроизведение (Android 5+)</string>\n    <string name=\"master_volume\">Основная громкость</string>\n    <string name=\"volume_limit\">Ограничение громкости</string>\n    <string name=\"player_volume\">Громкость</string>\n    <string name=\"play_video\">Воспроизвести</string>\n    <string name=\"remember_position_of_short_videos\">Запоминать позицию коротких видео (менее 5 мин)</string>\n    <string name=\"remember_position_of_live_videos\">Запоминать позицию прямых трансляций</string>\n    <string name=\"player_show_tooltips\">Показать подсказки для кнопок</string>\n    <string name=\"action_like_unset\">Нравится не установлено</string>\n    <string name=\"action_dislike_unset\">Не нравится не установлено</string>\n    <string name=\"various_buttons\">Кнопки в верхней части главного окна</string>\n    <string name=\"not_compatible_with\">Выбранная опция не совместима с</string>\n    <string name=\"subtitle_position\">Нижний сдвиг субтитров</string>\n    <string name=\"pressing_home\">по нажатию ДОМОЙ</string>\n    <string name=\"pressing_home_back\">по нажатию ДОМОЙ или НАЗАД</string>\n    <string name=\"pressing_back\">по нажатию НАЗАД</string>\n    <string name=\"player_number_key_seek\">Перематывать цифровыми клавишами</string>\n    <string name=\"save_remove_playlist\">Добавить/Удалить плейлист из раздела Плейлисты</string>\n    <string name=\"save_playlist\">Добавить плейлист в раздел Плейлисты</string>\n    <string name=\"remove_playlist\">Удалить плейлист из раздела Плейлисты навсегда</string>\n    <string name=\"remove_playlist_fmt\">Удалить %s навсегда?</string>\n    <string name=\"removed_from_playlists\">Удалено из раздела Плейлисты</string>\n    <string name=\"saved_to_playlists\">Добавлено в раздел Плейлисты</string>\n    <string name=\"create_playlist\">Создать плейлист</string>\n    <string name=\"create_playlist_note\">ВНИМАНИЕ: Его не будет видно в приложении YouTube</string>\n    <string name=\"add_video_to_new_playlist\">Добавить в новый плейлист</string>\n    <string name=\"cant_delete_empty_playlist\">Невозможно удалить пустой плейлист</string>\n    <string name=\"playlist\">Плейлист</string>\n    <string name=\"rename_playlist\">Переименовать плейлист</string>\n    <string name=\"cant_rename_empty_playlist\">Невозможно переименовать пустой плейлист</string>\n    <string name=\"cant_rename_foreign_playlist\">Невозможно переименовать чужой плейлист</string>\n    <string name=\"cant_save_playlist\">Этот плейлист добавить нельзя</string>\n    <string name=\"enter_value\">Введите любое непустое значение</string>\n    <string name=\"dialog_add_remove_from\">Добавить/Удалить из %s</string>\n    <string name=\"lb_playback_controls_skip_next\">\"Перейти к следующему видео\"</string>\n    <string name=\"lb_playback_controls_skip_previous\">\"Перейти к предыдущему видео/Перемотать в начало\"</string>\n    <string name=\"alt_presets_behavior\">Альтернативное поведение видео пресетов (ограничение пропускной способности)</string>\n    <string name=\"alt_presets_behavior_desc\">Приложение попытается поддерживать пропускную способность близкую к пресету вместо того, чтобы выбрать точное соответсвие между разрешением, частотой кадров и кодеком.</string>\n    <string name=\"sleep_timer\">Таймер сна (если пульт не использовался один час)</string>\n    <string name=\"sleep_timer_desc\">Ставить проигрывание на паузу, если пользователь в течение часа не пользовался пультом.</string>\n    <string name=\"disable_vsync\">Отключить вертикальную синхронизацию</string>\n    <string name=\"disable_vsync_desc\">Этот параметр отключает выравнивание кадров по сигналу вертикальной синхронизации дисплея. Может повысить производительность на слабых устройствах за счет высвобождения ресурсов ЦП.</string>\n    <string name=\"skip_codec_profile_check\">Пропустить проверку кодеков</string>\n    <string name=\"skip_codec_profile_check_desc\">Не проверять поддержку кодеков при запуске видео. Может быть полезно для глючных прошивок.</string>\n    <string name=\"force_sw_codec\">Активировать программный декодер</string>\n    <string name=\"force_sw_codec_desc\">Может воспроизводить любое видео, но производительность очень низкая.</string>\n    <string name=\"playlist_order\">Сортировать плейлист</string>\n    <string name=\"playlist_order_added_date_newer_first\">Дата добавления (сначала более новые)</string>\n    <string name=\"playlist_order_added_date_older_first\">Дата добавления (сначала более старые)</string>\n    <string name=\"playlist_order_published_date_newer_first\">Дата публикации (сначала более новые)</string>\n    <string name=\"playlist_order_published_date_older_first\">Дата публикации (сначала более старые)</string>\n    <string name=\"playlist_order_popularity\">По популярности</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">Невозможно это сделать для чужого плейлиста</string>\n    <string name=\"owned_playlist_warning\">Ошибка. Это действие применимо только для собственных плейлистов.</string>\n    <string name=\"content_block_alt_server\">Использовать альтернативный сервер</string>\n    <string name=\"content_block_alt_server_desc\">Включите эту опцию, если SponsorBlock отказывается работать по разным причинам.</string>\n    <string name=\"unset_stream_reminder\">Отключить напоминание о трансляции</string>\n    <string name=\"set_stream_reminder\">Установить напоминание о трансляции</string>\n    <string name=\"playback_starts_shortly\">Воспроизведение начнется автоматически, когда трансляция будет готова</string>\n    <string name=\"starting_stream\">Трансляция началась…</string>\n    <string name=\"action_playlist_remove\">Удалить из плейлиста</string>\n    <!-- BEGIN Moved strings -->\n    <string name=\"signin_view_title\">Код загружается…</string>\n    <string name=\"signin_view_description\">Чтобы войти в аккаунт, введите этот код на странице %s\\nПРИМЕЧАНИЕ. Работает только с браузерами Firefox или Chrome.</string>\n    <string name=\"signin_view_action_text\">Готово</string>\n    <string name=\"require_checked\">Требуется \\\"%s\\\"</string>\n    <string name=\"msg_press_again_to_exit\">Нажмите ещё раз для выхода</string>\n    <string name=\"player_remaining_time\">Осталось: %s</string>\n    <string name=\"player_ending_time\">Закончится в %s</string>\n    <string name=\"badge_new_content\">НОВЫЙ КОНТЕНТ</string>\n    <string name=\"badge_live\">В ЭФИРЕ</string>\n    <string name=\"add_device_view_description\">Чтобы привязать устройство, введите этот код в приложении YouTube на телефоне в разделе Настройки/Смотреть на ТВ\\nПРИМЕЧАНИЕ. Работает только с клавиатурой Gboard.</string>\n    <!-- END Moved strings -->\n    <string name=\"skip_24_rate\">Пропускать 24fps форматы (исправление для режима real cinema?)</string>\n    <string name=\"skip_shorts\">Пропускать Shorts</string>\n    <string name=\"player_disable_suggestions\">Отключить рекомендации</string>\n    <string name=\"suggestions\">Предложения</string>\n    <string name=\"feedback\">Обратная связь</string>\n    <string name=\"sources\">Исходники</string>\n    <string name=\"releases\">Релизы</string>\n    <string name=\"prefer_avc_over_vp9\">Выбор кодека: отдавать предпочтение avc над vp9</string>\n    <string name=\"open_chat\">Чат/Комментарии</string>\n    <string name=\"open_comments\">Открыть комментарии</string>\n    <string name=\"place_chat_left\">Разместить чат слева</string>\n    <string name=\"place_comments_left\">Разместить комментарии слева</string>\n    <string name=\"use_alt_speech_recognizer\">Альтернативный распознаватель речи</string>\n    <string name=\"time_format\">Формат времени</string>\n    <string name=\"time_format_24\">24 часа</string>\n    <string name=\"time_format_12\">12 часов (AM/PM)</string>\n    <string name=\"use_alt_speech_recognizer_desc\">Серьезно забагован. Используйте его, только если есть проблемы с распознавателем по умолчанию.</string>\n    <string name=\"speech_recognizer\">Распознаватель речи</string>\n    <string name=\"speech_engine\">Движок голосового поиска</string>\n    <string name=\"speech_recognizer_system\">Системный (наилучший)</string>\n    <string name=\"speech_recognizer_external_1\">Внешний 1 (серьезно забагован, для работы нужно отключить доступ к микрофону)</string>\n    <string name=\"speech_recognizer_external_2\">Внешний 2 (серьезно забагован)</string>\n    <string name=\"real_channel_icon\">Показать иконку на кнопке канала</string>\n    <string name=\"subtitle_yellow_semi_transparent\">Жёлтые на полупрозрачном фоне</string>\n    <string name=\"subtitle_yellow_black\">Жёлтые на чёрном фоне</string>\n    <string name=\"player_pixel_ratio\">Соотношение сторон пикселя</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">Тёмно-серая (монохромная)</string>\n    <string name=\"disable_mic_permission\">Пожалуйста, отключите доступ к микрофону для приложения, чтобы этот распознаватель работал правильно.</string>\n    <string name=\"repeat_mode_shuffle\">Перемешать любой плейлист</string>\n    <string name=\"chat_left\">Слева</string>\n    <string name=\"chat_right\">Справа</string>\n    <string name=\"card_real_thumbnails\">Заменить миниатюры кадром из видео</string>\n    <string name=\"card_content\">Откуда брать миниатюры карточек</string>\n    <string name=\"thumb_quality_default\">По умолчанию</string>\n    <string name=\"thumb_quality_start\">Начало видео</string>\n    <string name=\"thumb_quality_middle\">Середина видео</string>\n    <string name=\"thumb_quality_end\">Конец видео</string>\n    <string name=\"content_block_status\">Проверить состояние сервера SponsorBlock</string>\n    <string name=\"dearrow_status\">Проверить состояние сервера DeArrow</string>\n    <string name=\"player_show_quality_info_bitrate\">Добавлять битрейт к информации о качестве видео</string>\n    <string name=\"player_speed_button_old_behavior\">Вернуть старое поведение кнопки скорость</string>\n    <string name=\"protect_settings_with_password\">Защитить все настройки паролем</string>\n    <string name=\"enter_settings_password\">Введите пароль для входа в настройки</string>\n    <string name=\"child_mode\">Детский режим</string>\n    <string name=\"child_mode_desc\">В этом режиме пользователь не может использовать поиск или просматривать предлагаемый контент. Настройки будут под паролем.</string>\n    <string name=\"lost_setting_warning\">Настройки приложения будут изменены. Убедитесь, что вы создали резервную копию настроек.</string>\n    <string name=\"player_button_long_click\">Короткое нажатие на кнопку для быстрого действия и длинное для диалога настроек</string>\n    <string name=\"pause_history\">Поставить историю на паузу</string>\n    <string name=\"resume_history\">Возобновить историю</string>\n    <string name=\"clear_history\">Очистить историю</string>\n    <string name=\"disable_history\">Отключить историю</string>\n    <string name=\"enable_history\">Включить историю</string>\n    <string name=\"chapters\">Эпизоды</string>\n    <string name=\"card_multiline_subtitle\">Несколько строк в подзаголовках</string>\n    <string name=\"auto_frame_rate_modes\">Поддерживаемые режимы</string>\n    <string name=\"loading\">Загрузка…</string>\n    <string name=\"hide_streams\">Скрывать прямые трансляции из Подписок</string>\n    <string name=\"video_rotate\">Вращать</string>\n    <string name=\"video_flip\">Отразить</string>\n    <string name=\"trending_searches\">Популярные запросы</string>\n    <string name=\"video_duration_any\">Любая</string>\n    <string name=\"video_duration\">Длительность</string>\n    <string name=\"video_duration_under_4\">Менее 4 минут</string>\n    <string name=\"video_duration_between_4_20\">4–20 минут</string>\n    <string name=\"video_duration_over_20\">Более 20 минут</string>\n    <string name=\"content_type\">Тип</string>\n    <string name=\"content_type_any\">Любой</string>\n    <string name=\"content_type_video\">Видео</string>\n    <string name=\"content_type_channel\">Канал</string>\n    <string name=\"content_type_playlist\">Плейлист</string>\n    <string name=\"content_type_movie\">Фильм</string>\n    <string name=\"video_features\">Особенности</string>\n    <string name=\"video_feature_any\">Любые</string>\n    <string name=\"video_feature_live\">Прямые трансляции</string>\n    <string name=\"video_feature_4k\">4K</string>\n    <string name=\"video_feature_hdr\">HDR</string>\n    <string name=\"search_sorting\">Сортировать</string>\n    <string name=\"sort_by_relevance\">Релевантность</string>\n    <string name=\"sort_by_views\">Число просмотров</string>\n    <string name=\"sort_by_date\">Дата загрузки</string>\n    <string name=\"sort_by_rating\">Рейтинг</string>\n    <string name=\"clear_search_history\">Очистить историю поиска</string>\n    <string name=\"remove_from_subscriptions\">Скрыть</string>\n    <string name=\"player_long_speed_list\">Длинный список скоростей</string>\n    <string name=\"player_extra_long_speed_list\">Очень длинный список скоростей</string>\n    <string name=\"alt_app_icon\">Альтернативный значок приложения (требуется перезагрузка)</string>\n    <string name=\"network_stack\">Предпочитать сетевой стек %s</string>\n    <string name=\"player_network_stack\">Сетевой движок</string>\n    <string name=\"cronet_desc\">Cronet — это сетевой стек Chromium. Обычно быстрее других. Это может помочь с проблемами буферизации.</string>\n    <string name=\"unlock_all_formats\">Разблокировать все видео форматы</string>\n    <string name=\"unlock_all_formats_desc\">Разблокировать форматы, которые, вероятно, не поддерживаются вашим устройством.</string>\n    <string name=\"okhttp_desc\">Этот сетевой движок является самым медленным, но имеет хорошую стабильность.</string>\n    <string name=\"select_channel_section\">Открывать соответствующий раздел при запуске приложения с каналов ATV.</string>\n    <string name=\"enable_voice_search_desc\">Официальное приложение будет заменено так называемым мостом. Мост помогает передавать глобальные поисковые запросы в наше приложение.</string>\n    <string name=\"enable_master_password\">Включить мастер-пароль</string>\n    <string name=\"enter_master_password\">Введите мастер-пароль</string>\n    <string name=\"disable_stream_buffer\">Отключить буфер на стримах</string>\n    <string name=\"disable_stream_buffer_desc\">Исправляет ситуации, когда стрим сильно отстает. Внимание: стрим может начать тормозить.</string>\n    <string name=\"sony_frame_drop_fix\">Исправление выпадения кадров #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">Исправляет лаги на телевизорах Сони и некоторых других устройствах. Внимание: возможны проблемы с синхронизацией аудио.\n    </string>\n    <string name=\"audio_language\">Язык аудио</string>\n    <string name=\"old_home_look\">Старый вид Главного раздела</string>\n    <string name=\"old_channel_look\">Старый вид страницы Канала</string>\n    <string name=\"hide_shorts_everywhere\">Скрывать shorts повсюду</string>\n    <string name=\"update_found\">Обновление</string>\n    <string name=\"volume_boost_warning\">Настройки привышающие лимит могу повредить динамики</string>\n    <string name=\"play_video_incognito\">Воспроизвести анонимно</string>\n    <string name=\"play_from_start\">Воспроизвести сначала</string>\n    <string name=\"header_kids_home\">Детям</string>\n    <string name=\"player_section_playlist\">Использовать содержимое текущего раздела в качестве плейлиста</string>\n    <string name=\"header_trending\">В тренде</string>\n    <string name=\"screensaver\">Заставка</string>\n    <string name=\"player_screen_off_timeout\">Тайм-аут выключения экрана</string>\n    <string name=\"old_update_notifications\">Старый вид уведомлений о доступном обновлении</string>\n    <string name=\"sidebar_notification\">Показать уведомление на боковой панели</string>\n    <string name=\"dialog_notification\">Уведомлять про обновление всплывающим диалогом</string>\n    <string name=\"mark_as_watched\">Отметить как просмотренное</string>\n    <string name=\"player_ui_animations\">Включить анимацию интерфейса плеера</string>\n    <string name=\"player_likes_count\">Отображать количество лайков/дислайков</string>\n    <string name=\"sorting_alphabetically2\">По алфавиту (быстро, анимированные превью)</string>\n    <string name=\"sorting_default\">По умолчанию (быстро, анимированные превью)</string>\n    <string name=\"content_block_exclude_channel\">Исключить канал из SponsorBlock</string>\n    <string name=\"content_block_stop_excluding_channel\">Перестать исключать канал из SponsorBlock</string>\n    <string name=\"player_chapter_notification\">Быстрое перемещение по эпизодам с помощью всплывающего уведомления</string>\n    <string name=\"player_chapter_notification2\">Перейти к следующему эпизоду, нажав на уведомление</string>\n    <string name=\"subtitle_remember\">Запомнить включенные субтитры для каждого канала</string>\n    <string name=\"amazon_frame_drop_fix\">Исправление выпадения кадров #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">Исправляет лаги на Amazon Stick и некоторых других устройствах.</string>\n    <string name=\"default_stack_desc\">Встроенный сетевой движок. Этот движок может иметь лучшую стабильность в определенных ситуациях.</string>\n    <string name=\"item_postion\">Позиция</string>\n    <string name=\"player_screen_off_dimming\">Величина затемнения экрана</string>\n    <string name=\"screensaver_timout\">Тайм-аут включения заставки</string>\n    <string name=\"screensaver_dimming\">Величина затемнения заставки</string>\n    <string name=\"player_ui_on_next\">Показывать интерфейс при переходе на следующее видео</string>\n    <string name=\"autogenerated\">автоматически созданные</string>\n    <string name=\"player_auto_volume\">Автоматическое управление громкостью</string>\n    <string name=\"header_shorts\">Shorts</string>\n    <string name=\"player_global_focus\">Синхронизировать фокус между рядами кнопок плеера</string>\n    <string name=\"auto_history\">Auto (использовать настройки аккаунта)</string>\n    <string name=\"remember_position_subscriptions\">Запоминать последнюю просмотренную позицию в Подписках</string>\n    <string name=\"remember_position_pinned\">Запоминать последнюю просмотренную позицию в закрепленных плейлистах</string>\n    <string name=\"msg_player_unknown_error\">Неизвестная ошибка</string>\n    <string name=\"unknown_source_error\">Неизвестная ошибка источника</string>\n    <string name=\"unknown_renderer_error\">Неизвестная ошибка рендера</string>\n    <string name=\"header_notifications\">Уведомления</string>\n    <string name=\"disable_search_history\">Отключить историю поиска</string>\n    <string name=\"unlock_high_bitrate_formats\">Разблокировать 1080p vp9 форматы с высоким битрейтом</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">Разблокировать mp4a форматы с высоким битрейтом</string>\n    <string name=\"color_scheme_blue\">Синяя</string>\n    <string name=\"color_scheme_dark_blue\">Тёмно-голубая</string>\n    <string name=\"player_speed_per_channel\">Для каждого канала</string>\n    <string name=\"disable_popular_searches\">Не показывать популярные поисковые запросы</string>\n    <string name=\"multi_profiles\">Использовать раздельные настройки для каждого аккаунта</string>\n    <string name=\"protect_account_with_password\">Защитить этот аккаунт паролем</string>\n    <string name=\"enter_account_password\">Введите пароль от аккаунта</string>\n    <string name=\"show_connect_messages\">Сообщения о подключенных устройствах</string>\n    <string name=\"prefer_avc_over_vp9_desc\">ПРЕДУПРЕЖДЕНИЕ: 1080p максимум</string>\n    <string name=\"play_next\">Воспроизвести следующим</string>\n    <string name=\"hide_watched_from_subscriptions\">Скрывать просмотренные видео из Подписок</string>\n    <string name=\"hide_watched_from_notifications\">Скрывать просмотренные видео из Уведомлений</string>\n    <string name=\"hide_unwanted_content\">Скрывать содержимое</string>\n    <string name=\"hide_watched_from_home\">Скрывать просмотренные видео из Домашнего раздела</string>\n    <string name=\"hide_watched_from_watch_later\">Скрывать просмотренные видео из плейлиста Смотреть позже</string>\n    <string name=\"remote_control_permission\">Фоновое Дистанционное управление требует разрешения Перекрывать другие окна</string>\n    <string name=\"login_from_browser\">Войти через браузер</string>\n    <string name=\"disable_remote_history\">Отключить историю во время дистанционного управления</string>\n    <string name=\"keyboard_fix\">Исправить отображение клавиатуры при нажатии клавиши ОК (G20 и другие)</string>\n    <string name=\"nothing_found\">Ничего не найдено</string>\n    <string name=\"auto_frame_rate_desc\">Эта опция устраняет дрожание в сценах, где камера движется быстро, например. спортивные трансляции</string>\n    <string name=\"channel_filter_hint\">Фильтровать каналы</string>\n    <string name=\"player_loop_shorts\">Зацикливать shorts</string>\n    <string name=\"player_quick_shorts_skip\">Переключать shorts кнопками лево/право</string>\n    <string name=\"player_quick_shorts_skip_alt\">Переключать shorts кнопками вверх/вниз</string>\n    <string name=\"player_quick_skip_videos\">Переключать обычные видео кнопками лево/право</string>\n    <string name=\"player_quick_skip_videos_alt\">Переключать обычные видео кнопками вверх/вниз</string>\n    <string name=\"channels_filter\">Показывать поле Фильтровать каналы в разделе Каналы</string>\n    <string name=\"channel_search_bar\">Панель поиска на странице канала</string>\n    <string name=\"header_sports\">Спорт</string>\n    <string name=\"replace_titles\">Заменять заголовки</string>\n    <string name=\"enable\">Включить</string>\n    <string name=\"more_info\">Больше информации</string>\n    <string name=\"about_sponsorblock\">О SponsorBlock</string>\n    <string name=\"about_dearrow\">О DeArrow</string>\n    <string name=\"replace_thumbnails\">Заменять миниатюры</string>\n    <string name=\"crowdsourced_thumbnails\">Краудсорсинговые миниатюры</string>\n    <string name=\"crowdsoursed_titles\">Краудсорсинговые названия</string>\n    <string name=\"dearrow_not_submitted_thumbs\">Откуда брать не присланные миниатюры</string>\n    <string name=\"pitch_effect\">Эффект высоты тона</string>\n    <string name=\"fullscreen_mode\">Полноэкранный режим (без системных панелей)</string>\n    <string name=\"player_only_mode\">Показывать только плеер, если видео открыто вне приложения</string>\n    <string name=\"pinned_channel_rows\">Отображать закрепленный канал в виде строк</string>\n    <string name=\"prefer_google_dns\">Предпочитать Google DNS</string>\n    <string name=\"prefer_ipv4\">Предпочитать IPv4 DNS</string>\n    <string name=\"prefer_ipv4_desc\">Можеть исправить случаи, когда приложение вообще не работает.\\nВнимание. На некоторых устройствах могут быть вылеты приложения (особенно на Android 8 и приставке Dune HD)</string>\n    <string name=\"long_press_for_settings\">ДЛИТЕЛЬНОЕ НАЖАТИЕ ДЛЯ НАСТРОЕК</string>\n    <string name=\"long_press_for_options\">ДЛИТЕЛЬНОЕ НАЖАТИЕ ДЛЯ ОПЦИЙ</string>\n    <string name=\"device_specific_backup\">Резервное копирование только для этого устройства</string>\n    <string name=\"local_backup\">Локальное резервное копирование</string>\n    <string name=\"auto_backup\">Автоматическое резервное копирование (раз в день)</string>\n    <string name=\"repeat_mode_reverse_list\">Воспроизвести плейлист или видео с канала в обратном порядке</string>\n    <string name=\"calm_msg\">Мы решаем проблему. Время от времени проверяйте обновления</string>\n    <string name=\"without_picture\">Без изображения</string>\n    <string name=\"video_disabled\">Видео отключено</string>\n    <string name=\"applying_fix\">Не закрывайте плеер. Применение исправления…</string>\n    <string name=\"hide_mixes\">Скрывать Миксы</string>\n    <string name=\"player_global_focus_desc\">Эта функция влияет на то, какая кнопка проигрывателя получит фокус при навигации между рядами кнопок проигрывателя</string>\n    <string name=\"disable_network_error_fixing\">Отключить автоматическое исправление сетевых ошибок</string>\n    <string name=\"disable_network_error_fixing_desc\">Возможно, вам нужно включить эту опцию, если вы используете VPN</string>\n    <string name=\"recommended\">Рекомендации</string>\n    <string name=\"add_to_subscriptions_group\">Добавить/удалить из группы подписок</string>\n    <string name=\"new_subscriptions_group\">Новая группа</string>\n    <string name=\"rename_group\">Переименовать группу подписок</string>\n    <string name=\"screen_dimming_amount\">Степень затемнения экрана</string>\n    <string name=\"screen_dimming_timeout\">Тайм-аут затемнения экрана</string>\n    <string name=\"playlists_rows\">Отображать раздел Плейлисты в виде строк</string>\n    <string name=\"import_subscriptions_group\">Импортировать</string>\n    <string name=\"my_videos\">Ваши видео</string>\n    <string name=\"player_audio_focus\">Аудиофокус (пауза, если обнаружены другие плееры)</string>\n    <string name=\"paid_content_notification\">Уведомление о платном содержимом</string>\n    <string name=\"you_liked\">вам понравилось</string>\n    <string name=\"card_preview_muted\">Видео без звука</string>\n    <string name=\"card_preview_full\">Видео со звуком</string>\n    <string name=\"card_preview\">Предпросмотр карточки</string>\n    <string name=\"card_unlocalized_titles\">Не переводить названия видео</string>\n    <string name=\"dont_resize_video_to_fit_dialog\">Не изменять размер видео в соответствии с диалогом</string>\n    <string name=\"oculus_quest_fix\">Исправлять Oculus Quest</string>\n    <string name=\"playback_buffering_fix\">Исправлять воспроизведение буферизации</string>\n    <string name=\"typing_corrections\">Отключить автокоррекцию при наборе текста</string>\n    <string name=\"suggestions_horizontally_scrolled\">Горизонтально прокручиваемые рекомендации</string>\n    <string name=\"stable_restore\">Восстановить стабильную версию (все данные будут удалены)</string>\n    <string name=\"auto_backup_category\">Автоматическое резервное копирование</string>\n    <string name=\"once_a_day\">Один раз в день</string>\n    <string name=\"once_a_week\">Один раз в неделю</string>\n    <string name=\"once_a_month\">Один раз в месяц</string>\n    <string name=\"action_debug_info\">Отладочная информация</string>\n    <string name=\"dialog_block_channel\">Заблокировать канал</string>\n    <string name=\"dialog_unblock_channel\">Разблокировать канал</string>\n    <string name=\"confirm_block_channel\">Скрыть весь контент от %s?</string>\n    <string name=\"channel_blocked\">Канал заблокирован</string>\n    <string name=\"channel_unblocked\">Канал разблокирован</string>\n    <string name=\"header_blocked_channels\">Заблокированные каналы</string>\n    <string name=\"msg_no_blocked_channels\">Нет заблокированных каналов</string>\n    <string name=\"player_time_stretching\">Растяжение аудио во времени</string>\n    <string name=\"player_time_stretching_desc\">Сохраняет естественное звучание голоса при изменении скорости. Может значительно снизить производительность на некоторых устройствах.</string>\n    <string name=\"queue_respects_playback_mode\">Очередь учитывает режим воспроизведения</string>\n    <string name=\"ignore_short_segments\">Игнорировать короткие сегменты</string>\n    <string name=\"local_backup_not_supported\">Автобэкап на Android TV 11+ не поддерживается из-за ограничений системы. Используйте GDrive.</string>\n    <plurals name=\"hours\">\n        <item quantity=\"one\">%s час</item>\n        <item quantity=\"few\">%s часа</item>\n        <item quantity=\"many\">%s часов</item>\n        <item quantity=\"other\">%s часа</item>\n    </plurals>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%s секунда</item>\n        <item quantity=\"few\">%s секунды</item>\n        <item quantity=\"many\">%s секунд</item>\n        <item quantity=\"other\">%s секунды</item>\n    </plurals>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-sk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"header_home\">Domov</string>\n    <string name=\"title_search\">Vyhľadávanie</string>\n    <string name=\"header_subscriptions\">Odbery</string>\n    <string name=\"header_history\">História</string>\n    <string name=\"header_music\">Hudba</string>\n    <string name=\"header_news\">Správy</string>\n    <string name=\"header_gaming\">Hry</string>\n    <string name=\"header_playlists\">Knižnica</string>\n    <string name=\"header_settings\">Nastavenia</string>\n    <string name=\"header_channels\">Kanály</string>\n    <string name=\"subscriptions_signin_title\">Pozrite sa na najnovšie z vašich obľúbených odberov</string>\n    <string name=\"subscriptions_signin_subtitle\">Prihláste sa a pozrite sa na svoje odbery</string>\n    <string name=\"subscriptions_signin_button_text\">PRIHLÁSIŤ SA</string>\n    <string name=\"library_signin_to_show_more\">Sledujte videá, ktoré sa vám páčili, uložili alebo odoberáte</string>\n    <string name=\"library_signin_subtitle\">Ak chcete zobraziť svoju knižnicu, prihláste sa</string>\n    <string name=\"action_signin\">PRIHLÁSIŤ SA</string>\n    <string name=\"title_video_formats\">Video formáty</string>\n    <string name=\"title_audio_formats\">Audio formáty</string>\n    <string name=\"subtitle_category_title\">Titulky</string>\n    <string name=\"playback_queue_category_title\">Poradie prehrávania</string>\n    <string name=\"video_max_quality\">Auto (najvyššia kvalita)</string>\n    <string name=\"audio_max_quality\">Auto (najvyššia kvalita)</string>\n    <string name=\"subtitles_disabled\">Vypnuté</string>\n    <string name=\"auto_frame_rate\">Automatická snímková frekvencia</string>\n    <string name=\"frame_rate_correction\">Opraviť fps: \\n%s</string>\n    <string name=\"category_background_playback\">Prehrávanie na pozadí</string>\n    <string name=\"not_implemented\">Neimplementované</string>\n    <string name=\"not_supported_by_device\">Zariadenie nepodporuje túto funkciu</string>\n    <string name=\"option_background_playback_off\">Vypnuté</string>\n    <string name=\"option_background_playback_pip\">Obraz v obraze</string>\n    <string name=\"option_background_playback_only_audio\">Iba audio</string>\n    <string name=\"playback_settings\">Nastavenie prehrávania</string>\n    <string name=\"video_speed\">Rýchlosť prehrávania</string>\n    <string name=\"resolution_switch\">Prepnúť rozlíšenie</string>\n    <string name=\"video_buffer\">Vyrovnávacia pamäť videa</string>\n    <string name=\"video_buffer_size_none\">Žiadna</string>\n    <string name=\"video_buffer_size_lowest\">Najnižšia</string>\n    <string name=\"video_buffer_size_low\">Nízka</string>\n    <string name=\"video_buffer_size_med\">Stredná</string>\n    <string name=\"video_buffer_size_high\">Vysoká</string>\n    <string name=\"video_buffer_size_highest\">Najvyššia</string>\n    <string name=\"update_changelog\">Zoznam zmien</string>\n    <string name=\"install_update\">Inštalovať aktualizáciu</string>\n    <string name=\"section_is_empty\">Hups! Žiadne videá</string>\n    <string name=\"dialog_add_to_playlist\">Uložiť do zoznamu</string>\n    <string name=\"msg_signed_users_only\">Iba pre prihlásených</string>\n    <string name=\"msg_cant_load_content\">Nemožno načítať obsah.\\n1) Zapnite históriu sledovania.\\n2)  Skontrolujte dátum a čas na zariadení.\\n3) Skontrolujte sieťové pripojenie.\\n4) Skontrolujte nastavenia proxy.\\n5) Skúste sa prihlásiť do vášho účtu.\\n6) Je možné, že tu nie je žiaden obsah.</string>\n    <string name=\"title_video_presets\">Video prednastavenia</string>\n    <string name=\"settings_accounts\">Účty</string>\n    <string name=\"settings_left_panel\">Upraviť kategórie</string>\n    <string name=\"settings_themes\">Témy</string>\n    <string name=\"settings_other\">Iné</string>\n    <string name=\"settings_player\">Prehrávač</string>\n    <string name=\"settings_language\">Jazyk</string>\n    <string name=\"settings_linked_devices\">Pripojené zariadenia</string>\n    <string name=\"settings_about\">Informácie</string>\n    <string name=\"dialog_account_list\">Vybrať účet</string>\n    <string name=\"dialog_account_none\">Žiadny</string>\n    <string name=\"dialog_remove_account\">Odstrániť účet</string>\n    <string name=\"dialog_add_account\">Pridať účet</string>\n    <string name=\"default_lang\">Predvolené</string>\n    <string name=\"original_lang\">Originálny</string>\n    <string name=\"dialog_select_language\">Jazyk</string>\n    <string name=\"subtitle_default\">Predvolené</string>\n    <string name=\"subtitle_white_semi_transparent\">Polopriehľadné pozadie</string>\n    <string name=\"subtitle_style\">Štýl titulkov</string>\n    <string name=\"subtitle_language\">Jazyk titulkov</string>\n    <string name=\"action_search\">Hľadať</string>\n    <string name=\"settings_main_ui\">Používateľské rozhranie</string>\n    <string name=\"dialog_main_ui\">Používateľské rozhranie</string>\n    <string name=\"card_animated_previews\">Animované náhľady</string>\n    <string name=\"web_site\">Webová stránka</string>\n    <string name=\"donation\">Darovanie</string>\n    <string name=\"dialog_about\">Informácie</string>\n    <string name=\"dialog_player_ui\">Video prehrávač</string>\n    <string name=\"player_show_ui_on_pause\">Zobraziť rozhranie počas pauzy</string>\n    <string name=\"player_pause_on_ok\">OK tlačidlo pozastaví prehrávanie</string>\n    <string name=\"player_ok_button_behavior\">Správanie OK tlačidla</string>\n    <string name=\"player_only_ui\">Iba rozhranie</string>\n    <string name=\"player_ui_and_pause\">Rozhranie a pozastavenie</string>\n    <string name=\"player_only_pause\">Iba pozastavenie</string>\n    <string name=\"player_toggle_speed\">Prepnúť rýchlosť zap/vyp</string>\n    <string name=\"check_for_updates\">Skontrolovať aktualizácie</string>\n    <string name=\"update_not_found\">Používate najnovšiu verziu</string>\n    <string name=\"update_in_progress\">Počkajte…</string>\n    <string name=\"player_ui_hide_behavior\">Automatické skrytie rozhrania</string>\n    <string name=\"option_never\">Nikdy</string>\n    <string name=\"side_panel_sections\">Nastavenie sekcií</string>\n    <string name=\"boot_to_section\">Úvodná sekcia</string>\n    <string name=\"large_ui\">Veľké rozhranie</string>\n    <string name=\"video_grid_scale\">Mierka video mriežky</string>\n    <string name=\"scale_ui\">Mierka rozhrania</string>\n    <string name=\"color_scheme\">Farebná schéma</string>\n    <string name=\"color_scheme_default\">Predvolená</string>\n    <string name=\"color_scheme_red_grey\">Červeno-šedá</string>\n    <string name=\"color_scheme_red\">Červená</string>\n    <string name=\"color_scheme_dark_grey\">Tmavo-šedá</string>\n    <string name=\"disable_update_check\">Vypnúť kontrolu aktualizácií</string>\n    <string name=\"show_again\">Zobraziť znova</string>\n    <string name=\"check_updates_auto\">Upozorniť na aktualizáciu</string>\n    <string name=\"select_account_on_boot\">Zobraziť vyberanie účtu pri spustení</string>\n    <string name=\"player_other\">Rôzne</string>\n    <string name=\"player_full_date\">Presný dátum v popisku</string>\n    <string name=\"open_channel\">Otvoriť kanál</string>\n    <string name=\"open_playlist\">Otvoriť knižnicu</string>\n    <string name=\"not_interested\">Nemám záujem</string>\n    <string name=\"not_recommend_channel\">Neodporúčať kanál</string>\n    <string name=\"you_wont_see_this_video\">Video bolo odstránené z odporúčaných</string>\n    <string name=\"you_wont_see_this_channel\">Tento kanál neuvidíte v odporúčaných</string>\n    <string name=\"settings_search\">Hľadať</string>\n    <string name=\"dialog_search\">Hľadať</string>\n    <string name=\"instant_voice_search\">Okamžité hlasové vyhľadávanie</string>\n    <string name=\"option_background_playback_behind\">Pridať do poradia</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">Informácie o ďalšom videu ešte nie sú načítané</string>\n    <string name=\"card_multiline_title\">Viacriadkové názvy</string>\n    <string name=\"cards_style\">Štýl kariet</string>\n    <string name=\"repeat_mode_all\">Prehrať všetko po jednom</string>\n    <string name=\"repeat_mode_one\">Zopakovať aktuálne video</string>\n    <string name=\"repeat_mode_pause\">Pozastaviť prehrávanie po každom videu (okrem poradia)</string>\n    <string name=\"repeat_mode_pause_alt\">Pozastaviť prehrávanie po každom videu, ktoré nie je v poradí</string>\n    <string name=\"repeat_mode_none\">Zastaviť prehrávanie po jednom videu (okrem poradia)</string>\n    <string name=\"subscribed_to_channel\">Odoberané</string>\n    <string name=\"unsubscribed_from_channel\">Odber zrušený</string>\n    <string name=\"subtitle_yellow_transparent\">Žlté na priehľadnom pozadí</string>\n    <string name=\"subtitle_white_transparent\">Biele na priehľadnom pozadí</string>\n    <string name=\"subtitle_white_black\">Biele na čiernom pozadí</string>\n    <string name=\"player_seek_preview\">Náhľady pri preskakovaní</string>\n    <string name=\"color_scheme_dark_grey_oled\">Tmavo-šedá (OLED)</string>\n    <string name=\"channels_section_sorting\">Zoradenie sekcie kanálov</string>\n    <string name=\"sorting_by_new_content\">Nový obsah</string>\n    <string name=\"sorting_alphabetically\">Abecedne</string>\n    <string name=\"sorting_last_viewed\">Posledné videné</string>\n    <string name=\"player_pause_when_seek\">Pozastaviť video pri preskakovaní</string>\n    <string name=\"playlists_style\">Štýl sekcie Knižnica</string>\n    <string name=\"playlists_style_grid\">Mriežka</string>\n    <string name=\"playlists_style_rows\">Riadky</string>\n    <string name=\"player_show_clock\">Zobraziť hodiny vo vyskakovacej ponuke</string>\n    <string name=\"player_show_remaining_time\">Zobraziť zostávajúci čas</string>\n    <string name=\"player_show_ending_time\">Zobraziť čas ukončenia</string>\n    <string name=\"open_channel_uploads\">Otvoriť videá v kanáli</string>\n    <string name=\"app_exit_shortcut\">Vypnúť aplikáciu</string>\n    <string name=\"player_exit_shortcut\">Vypnúť prehrávač</string>\n    <string name=\"search_exit_shortcut\">Vypnúť vyhľadávanie</string>\n    <string name=\"app_exit_none\">Nevypínať</string>\n    <string name=\"app_double_back_exit\">2x tlačidlo Späť</string>\n    <string name=\"app_single_back_exit\">1x tlačidlo Späť</string>\n    <string name=\"action_video_zoom\">Video zoom</string>\n    <string name=\"video_zoom\">Video zoom</string>\n    <string name=\"video_zoom_default\">Predvolené</string>\n    <string name=\"video_zoom_fit_width\">Prispôsobiť šírku</string>\n    <string name=\"video_zoom_fit_height\">Prispôsobiť výšku</string>\n    <string name=\"video_zoom_fit_both\">Prispôsobiť šírku alebo výšku</string>\n    <string name=\"video_zoom_stretch\">Roztiahnuť</string>\n    <string name=\"color_scheme_teal\">Modrozelená</string>\n    <string name=\"color_scheme_teal_oled\">Modrozelená (OLED)</string>\n    <string name=\"player_seek_preview_none\">Vypnuté</string>\n    <string name=\"player_seek_preview_single\">Jeden snímok</string>\n    <string name=\"player_seek_preview_carousel\">Niekoľko snímkov</string>\n    <string name=\"player_seek_preview_carousel_slow\">Niekoľko snímkov (pomaly)</string>\n    <string name=\"player_seek_preview_carousel_fast\">Niekoľko snímkov (rýchlo)</string>\n    <string name=\"unsubscribe_from_channel\">Zrušiť odber kanála</string>\n    <string name=\"card_title_lines_num\">Počet riadkov v názve videa</string>\n    <string name=\"card_auto_scrolled_title\">Auto-posúvanie orezaného názvu videa</string>\n    <string name=\"share_link\">Zdieľať link</string>\n    <string name=\"share_qr_link\">Zdieľať link (QR kód)</string>\n    <string name=\"subscribe_to_channel\">Odoberať</string>\n    <string name=\"wait_data_loading\">Prosím počkajte, kým sa načítajú dáta…</string>\n    <string name=\"auto_frame_rate_pause\">Pozastavenie prepínača automatickej snímkovej frekvencie</string>\n    <string name=\"auto_frame_rate_applying\">Aplikovanie automatickej snímkovej frekvencie %sx%s\\@%s</string>\n    <string name=\"audio_shift\">Posunutie zvuku</string>\n    <string name=\"player_remember_speed\">Pamätať si rýchlosť videa</string>\n    <string name=\"mark_channel_as_watched\">Označiť kanál ako videný</string>\n    <string name=\"channel_marked_as_watched\">Kanál bol označený ako videný</string>\n    <string name=\"dialog_add_device\">Pridať zariadenie</string>\n    <string name=\"dialog_remove_all_devices\">Odstrániť všetky zariadenia</string>\n    <string name=\"device_link_enabled\">Spojenie povolené</string>\n    <string name=\"device_connected\">Zariadenie \\\"%s\\\" bolo pripojené</string>\n    <string name=\"device_disconnected\">Zariadenie \\\"%s\\\" bylo odpojené</string>\n    <string name=\"settings_ui_scale\">Veľkosť rozhrania</string>\n    <string name=\"msg_restart_app\">Pre aplikovanie nastavení prosím reštartujte aplikáciu</string>\n    <string name=\"settings_block\">Blokovanie obsahu</string>\n    <string name=\"msg_applying\">Aplikovanie %s…</string>\n    <string name=\"msg_done\">Hotovo</string>\n    <string name=\"settings_general\">Všeobecné</string>\n    <string name=\"settings_video\">Video</string>\n    <string name=\"content_block_confirm_skip\">Potvrdzovať preskočenie</string>\n    <string name=\"content_block_categories\">Kategórie</string>\n    <string name=\"content_block_sponsor\">Sponzor</string>\n    <string name=\"content_block_intro\">Prestávka/Úvodná animácia</string>\n    <string name=\"content_block_outro\">Koncová animácia/Titulky</string>\n    <string name=\"content_block_interaction\">Pripomenutie interakcie (odber)</string>\n    <string name=\"content_block_self_promo\">Neplatená/vlastná propagácia</string>\n    <string name=\"content_block_music_off_topic\">Tichá časť klipu</string>\n    <string name=\"confirm_segment_skip\">Preskočiť segment \\\"%s\\\"\\?</string>\n    <string name=\"cancel_segment_skip\">Zrušiť preskočenie segmentu</string>\n    <string name=\"msg_skipping_segment\">Preskakujem segment \\\"%s\\\"…</string>\n    <string name=\"content_block_notification_type\">Typ notifikácie</string>\n    <string name=\"content_block_notify_none\">Bez notifikácie</string>\n    <string name=\"content_block_notify_toast\">Toast dialóg</string>\n    <string name=\"content_block_notify_dialog\">Potvrdzovací dialóg</string>\n    <string name=\"return_to_launcher\">Vrátiť sa do launchera z ATV kanála/hľadania</string>\n    <string name=\"intent_force_close\">Vynútiť ukončenie ak bolo video otvorené z externého zdroja</string>\n    <string name=\"btn_confirm\">Potvrdiť</string>\n    <string name=\"player_low_video_quality\">Nízka video kvalita</string>\n    <string name=\"remote_session_closed\">Vzdialená relácia bola ukončená</string>\n    <string name=\"msg_mode_switch_error\">Nemožno prepnúť režim zobrazenia na \\\"%s\\\"</string>\n    <string name=\"msg_player_error\">Nastala chyba prehrávača %s. Reštartujem prehrávanie…</string>\n    <string name=\"video_preset_disabled\">Bez video predvoľby</string>\n    <string name=\"video_preset_enabled\">Formát ďalšieho videa bude nastavený podľa video predvoľby</string>\n    <string name=\"player_sleep_timer\">Časovač vypnutia</string>\n    <string name=\"player_show_quality_info\">Zobraziť info o kvalite videa</string>\n    <string name=\"player_remember_each_speed\">Pamätať si rýchlosť každého videa</string>\n    <string name=\"player_remember_speed_none\">Namapätať si žiadne</string>\n    <string name=\"player_remember_speed_all\">Rovnaká rýchlosť pre všetky videá</string>\n    <string name=\"player_remember_speed_each\">Každé video má svoju vlastnú rýchlosť</string>\n    <string name=\"msg_player_error_source\">Nemožno načítať video</string>\n    <string name=\"msg_player_error_renderer\">Vybraný formát nie je podporovaný.\\nSkúste vybrať iný.\\nAk to nepomôže, skúste reštartovať zariadenie.</string>\n    <string name=\"msg_player_error_video_renderer\">Vybraný VIDEO formát nie je podporovaný.\\nSkúste vybrať iný stlačením tlačidla HQ v prehrávači.\\nAk to nepomôže, skúste reštartovať zariadenie.</string>\n    <string name=\"msg_player_error_audio_renderer\">Vybraný AUDIO formát nie je podporovaný.\\nSkúste vybrať iný stlačením tlačidla HQ v prehrávači.\\nAk to nepomôže, skúste reštartovať zariadenie.</string>\n    <string name=\"msg_player_error_subtitle_renderer\">Vybraný formát TITULKOV nie je podporovaný.\\nSkúste vybrať iný stlačením tlačidla HQ v prehrávači.\\nAk to nepomôže, skúste reštartovať zariadenie.</string>\n    <string name=\"msg_player_error_unexpected\">Neznáma chyba video dekodéra</string>\n    <string name=\"video_aspect\">Pomer strán</string>\n    <string name=\"player_tweaks\">Vývojárske možnosti</string>\n    <string name=\"header_uploads\">Nahraté videá</string>\n    <string name=\"player_show_global_clock\">Neustále zobrazovať hodiny na obrazovke</string>\n    <string name=\"player_show_global_ending_time\">Neustále zobrazovať čas ukončenia videa na obrazovke</string>\n    <string name=\"app_corner_clock\">Domov: hodiny v pravom hornom rohu</string>\n    <string name=\"player_corner_clock\">Prehrávač: hodiny v pravom hornom rohu</string>\n    <string name=\"player_corner_ending_time\">Prehrávač: čas ukončenia v pravom hornom rohu</string>\n    <string name=\"uploads_old_look\">Vrátiť späť starý vzhľad sekcie Nahraté videá</string>\n    <string name=\"background_playback_activation\">Prehrávanie na pozadí (aktivácia)</string>\n    <string name=\"channels_old_look\">Starý vzhľad sekcie Kanály</string>\n    <string name=\"channels_auto_load\">Automaticky načítať obsah sekcie Kanály</string>\n    <string name=\"return_to_background_video\">Späť na video bežiace na pozadí</string>\n    <string name=\"pin_unpin_from_sidebar\">Pridať/Odstrániť na postranný panel</string>\n    <string name=\"pin_unpin_playlist\">Pridať/Odstrániť playlist na postranný panel</string>\n    <string name=\"pin_unpin_channel\">Pridať/Odstrániť kanál na postranný panel</string>\n    <string name=\"pin_playlist\">Pridať playlist na postranný panel</string>\n    <string name=\"pin_channel\">Pridať kanál na postranný panel</string>\n    <string name=\"pinned_to_sidebar\">Pridané na postranný panel</string>\n    <string name=\"unpin_from_sidebar\">Odstrániť z postranného panela</string>\n    <string name=\"unpin_group_from_sidebar\">Odstrániť skupinu z Odberov</string>\n    <string name=\"unpinned_from_sidebar\">Odstránené z postranného panela</string>\n    <string name=\"dialog_select_country\">Krajina</string>\n    <string name=\"settings_language_country\">Jazyk/Krajina</string>\n    <string name=\"share_embed_link\">Zdieľanie</string>\n    <string name=\"card_text_scroll_factor\">Rýchlosť posúvania textu v názve videa</string>\n    <string name=\"preferred_update_source\">Preferovaný zdroj aktualizácie</string>\n    <string name=\"hide_shorts\">Skryť Shorts z Odberov</string>\n    <string name=\"hide_shorts_channel\">Skryť Shorts z kanála</string>\n    <string name=\"key_remapping\">Prednastavenie tlačidiel</string>\n    <string name=\"screen_dimming\">Stmavenie obrazovky</string>\n    <string name=\"removed_from_playback_queue\">Odstránené zo zoznamu</string>\n    <string name=\"added_to_playback_queue\">Pridané do poradia</string>\n    <string name=\"add_remove_from_playback_queue\">Pridať/Odstrániť z poradia</string>\n    <string name=\"add_to_playback_queue\">Pridať do poradia</string>\n    <string name=\"remove_from_playback_queue\">Odstrániť zo zoznamu</string>\n    <string name=\"proxy_enabled\">Zapnuté proxy</string>\n    <string name=\"proxy_disabled\">Vypnuté proxy</string>\n    <string name=\"uploads_row_name\">Nahraté videá</string>\n    <string name=\"playlists_row_name\">Vytvorené zoznamy videí</string>\n    <string name=\"popular_uploads_row_name\">Populárne nahraté videá</string>\n    <string name=\"news_row_name\">Novinky</string>\n    <string name=\"breaking_news_row_name\">Najnovšie správy</string>\n    <string name=\"covid_news_row_name\">COVID-19 správy</string>\n    <string name=\"enable_voice_search\">Zapnúť hlasové vyhľadávanie (nutná podpora firmvéru)</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">Odoberať/Neodoberať kanál</string>\n    <string name=\"app_backup_restore\">Záloha/Obnovenie</string>\n    <string name=\"app_backup\">Zálohovať dáta</string>\n    <string name=\"app_restore\">Obnoviť dáta zo zálohy</string>\n    <string name=\"skip_each_segment_once\">Už viac nepreskakovať segmenty</string>\n    <string name=\"disable_ok_long_press\">Vypnúť dlhé stlačenie tlačidla OK</string>\n    <string name=\"disable_ok_long_press_desc\">Určené pre chybné ovládače, kde tlačidlo OK nefunguje správne</string>\n    <string name=\"player_time_correction\">Zobraziť čas s korekciou rýchlosti</string>\n    <string name=\"sponsor_color_markers\">Farebné značky na ukazovateli priebehu</string>\n    <string name=\"refresh_section\">Aktualizovať sekciu</string>\n    <string name=\"option_disabled\">Vypnuté</string>\n    <string name=\"live_now_row_name\">Naživo</string>\n    <string name=\"run_in_background\">Spustiť na pozadí</string>\n    <string name=\"settings_remote_control\">Dialkové ovládanie</string>\n    <string name=\"background_service_started\">Služba spustená na pozadí</string>\n    <string name=\"dialog_add_to\">Pridať do %s</string>\n    <string name=\"dialog_remove_from\">Odobrať z %s</string>\n    <string name=\"added_to\">Pridané do %s</string>\n    <string name=\"removed_from\">Odobrané z %s</string>\n    <string name=\"video_preset_adaptive\">Adaptívne</string>\n    <string name=\"content_block_preview_recap\">Náhľad alebo zrhnutie videa</string>\n    <string name=\"content_block_highlight\">Zaujímavý bod (záložka)</string>\n    <string name=\"removed_from_history\">Video bolo zmazané z histórie</string>\n    <string name=\"remove_from_history\">Zmazať z histórie</string>\n    <string name=\"upload_date\">Dátum nahrania</string>\n    <string name=\"upload_date_any\">Za celú dobu</string>\n    <string name=\"upload_date_today\">Dnes</string>\n    <string name=\"upload_date_this_week\">Tento týždeň</string>\n    <string name=\"upload_date_this_month\">Tento mesiac</string>\n    <string name=\"upload_date_this_year\">Tento rok</string>\n    <string name=\"double_refresh_rate\">Dvojitá obnovovacia frekvencia</string>\n    <string name=\"upload_date_last_hour\">Za poslednú hodinu</string>\n    <string name=\"focus_on_search_results\">Automatické zameranie na výsledky vyhľadávania</string>\n    <string name=\"context_menu\">Kontextové menu</string>\n    <string name=\"context_menu_sorting\">Radenie kontextového menu</string>\n    <string name=\"add_remove_from_recent_playlist\">Pridať/Odobrať z nedávneho zoznamu</string>\n    <string name=\"hide_settings_section\">Skryť sekciu Nastavenia (nebezpečné!)</string>\n    <string name=\"volume\">Hlasitosť %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s sekúnd</string>\n    <string name=\"audio_shift_sec\">%s sekúnd</string>\n    <string name=\"ui_hide_timeout_sec\">%s sekúnd</string>\n    <string name=\"screen_dimming_timeout_min\">%s min</string>\n    <string name=\"mark_all_channels_watched\">Označiť všetky kanály ako videné</string>\n    <string name=\"move_section_up\">Posunúť sekciu hore</string>\n    <string name=\"move_section_down\">Posunúť sekciu dolu</string>\n    <string name=\"player_buttons\">Nastavenie tlačidiel prehrávača</string>\n    <string name=\"action_playlist_add\">Uložiť</string>\n    <string name=\"action_video_stats\">Video štatistiky</string>\n    <string name=\"action_subscribe\">Odoberať</string>\n    <string name=\"action_channel\">Otvoriť kanál</string>\n    <string name=\"action_pip\">PIP</string>\n    <string name=\"action_video_info\">Popis videa</string>\n    <string name=\"action_screen_off\">Vypnúť obrazovku</string>\n    <string name=\"action_sound_off\">Vypnúť zvuk</string>\n    <string name=\"action_playback_queue\">Poradie prehrávania</string>\n    <string name=\"action_video_speed\">Rýchlosť prehrávania</string>\n    <string name=\"action_subtitles\">Titulky</string>\n    <string name=\"action_like\">Páči sa mi</string>\n    <string name=\"action_dislike\">Nepáči sa mi</string>\n    <string name=\"action_play_pause\">Prahrať/Pozastaviť</string>\n    <string name=\"action_repeat_mode\">Režim prehrávania</string>\n    <string name=\"action_next\">Ďalšie</string>\n    <string name=\"action_previous\">Predchádzajúce</string>\n    <string name=\"action_high_quality\">Kvalita</string>\n    <string name=\"content_block_no_skipping_mode\">Režim bez preskakovania</string>\n    <string name=\"content_block_action_type\">Vybrať akciu</string>\n    <string name=\"content_block_action_none\">Nerobiť nič</string>\n    <string name=\"content_block_action_only_skip\">Preskakovať</string>\n    <string name=\"content_block_action_toast\">Preskakovať s upozornením</string>\n    <string name=\"content_block_action_dialog\">Ukázať potvrdzovací dialóg</string>\n    <string name=\"content_block_filler\">Mimo témy (výplň)</string>\n    <string name=\"keyboard_auto_show\">Zobraziť klávesnicu automaticky</string>\n    <string name=\"cancel_dialog\">Zrušiť</string>\n    <string name=\"msg_player_error_source2\">URL nefunguje alebo je na zariadení nesprávny čas.\\nZvyčajne je to chyba v aplikácii.</string>\n    <string name=\"msg_player_error_video_source\">VIDEO URL nefunguje alebo je na zariadení nesprávny čas.\\nZvyčajne je to chyba v aplikácii.</string>\n    <string name=\"msg_player_error_audio_source\">AUDIO URL nefunguje alebo je na zariadení nesprávny čas.\\nZvyčajne je to chyba v aplikácii.</string>\n    <string name=\"msg_player_error_subtitle_source\">TITULKOVÁ URL nefunguje alebo je na zariadení nesprávny čas.\\nZvyčajne je to chyba v aplikácii.</string>\n    <string name=\"hide_upcoming\">Skryť nadchádzajúce z Odberov</string>\n    <string name=\"hide_upcoming_channel\">Skryť nadchádzajúce z Kanálov</string>\n    <string name=\"hide_upcoming_home\">Skryť nadchádzajúce z Domov</string>\n    <string name=\"search_background_playback\">Prehrávanie na pozadí počas vyhľadávania/prezerania kanála</string>\n    <string name=\"trending_row_name\">Trendy</string>\n    <string name=\"player_seek_type\">Typ pratáčania</string>\n    <string name=\"player_seek_regular\">Bežné</string>\n    <string name=\"player_seek_confirmation_pause\">S potvrdením (pozastavenie pri pratáčaní)</string>\n    <string name=\"player_seek_confirmation_play\">S potvrdením (prehrávanie pri pratáčaní)</string>\n    <string name=\"hide_shorts_from_home\">Skryť Shorts z Domov</string>\n    <string name=\"finish_on_disconnect\">Ukončiť aplikáciu po odpojení telefónu/tabletu</string>\n    <string name=\"update_error\">Chyba aktualizácie</string>\n    <string name=\"hide_shorts_from_search\">Skryť Shorts z výsledkov vyhľadávania</string>\n    <string name=\"hide_shorts_from_history\">Skryť Shorts z Histórie</string>\n    <string name=\"hide_shorts_from_trending\">Skryť Shorts z Trendy</string>\n    <string name=\"disable_screensaver\">Vypnúť šetrič obrazovky</string>\n    <string name=\"description_not_found\">Popis nenájdený</string>\n    <string name=\"proxy_port_hint\">Proxy port</string>\n    <string name=\"proxy_host_hint\">Názov alebo IP adresa proxy servera</string>\n    <string name=\"proxy_username_hint\">Proxy meno používateľa</string>\n    <string name=\"proxy_password_hint\">Proxy heslo používateľa</string>\n    <string name=\"proxy_type\">Typ proxy</string>\n    <string name=\"enable_web_proxy\">Použiť webové proxy</string>\n    <string name=\"proxy_not_supported\">Použiť webové proxy (Podporované od Android 4.4+)</string>\n    <string name=\"proxy_test_btn\">Test</string>\n    <string name=\"proxy_settings_title\">Nastavenia Proxy Servera</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">Test #%d: zrušený.</string>\n    <string name=\"proxy_type_invalid\">Neplatný typ proxy.</string>\n    <string name=\"proxy_host_invalid\">Neplatný názov proxy servera.</string>\n    <string name=\"proxy_port_invalid\">Neplatný proxy port, musí byť &gt; 0.</string>\n    <string name=\"proxy_credentials_invalid\">Neplatné meno alebo heslo používateľa.</string>\n    <string name=\"proxy_test_aborted\">Test prerušený, prosín opravte nastavenia proxy.</string>\n    <string name=\"proxy_application_aborted\">Opravte prosím nastavenia proxy.</string>\n    <string name=\"proxy_test_start\">Test #%d: %s …</string>\n    <string name=\"proxy_test_error\">Chyba #%d: %s</string>\n    <string name=\"proxy_test_status\">Stav #%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">Cesta ku konfiguračnému súboru (*.ovpn)</string>\n    <string name=\"enable_openvpn\">Použiť OpenVPN</string>\n    <string name=\"openvpn_settings_title\">Nastavenia OpenVPN</string>\n    <string name=\"openvpn_application_aborted\">Opravte prosím nastavenia OpenVPN.</string>\n    <string name=\"openvpn_test_aborted\">Test prerušený, prosím opravte nastavenia OpenVPN.</string>\n    <string name=\"openvpn_address_invalid\">Neplatná cesta ku konfiguračnému súboru OpenVPN.</string>\n    <string name=\"internet_censorship\">Internetová Cenzúra</string>\n    <string name=\"rename_section\">Premenovať túto sekciu</string>\n    <string name=\"simple_edit_value_hint\">Zadajte hodnotu</string>\n    <string name=\"seek_interval\">Interval pratáčania</string>\n    <string name=\"seek_interval_sec\">%s sekúnd</string>\n    <string name=\"subtitle_system\">Systémový štýl (Nastavenia Androidu &gt; Dostupnosť)</string>\n    <string name=\"subtitle_scale\">Veľkosť titulkov</string>\n    <string name=\"audio_sync_fix_desc\">Alternatívna Audio/Video synchronizácia. Považované za zastaralé, ale v niektorých prípadoch môže pomôcť.</string>\n    <string name=\"audio_sync_fix\">Oprava Audio synchronizácie</string>\n    <string name=\"ambilight_ratio_fix_desc\">Opravuje chýbajúce podsvietenie. Opravuje nesprávny pomer strán a škálovanie videa. Opravuje prázdne snímky obrazovky. Ovplyvňuje výkon!</string>\n    <string name=\"ambilight_ratio_fix\">Oprava pre Ambilight/Pomeru strán/Škálovanie videa/Snímok obrazovky</string>\n    <string name=\"force_legacy_codecs_desc\">Výrazne zlepšuje výkon na slabších zariadeniach. Maximálne rozlíšenie je 720p.</string>\n    <string name=\"force_legacy_codecs\">Vynútenie zastaralých kodekov (720p)</string>\n    <string name=\"live_stream_fix_desc\">Varovanie: Pretáčanie živého vysielania bude nedostupné. Výrazne zlepšuje živé vysielanie na slabších zariadeniach. Maximálne rozlíšenie je 1080p.</string>\n    <string name=\"live_stream_fix_4k_desc\">Varovanie: Pretáčanie živého vysielania bude nedostupné. Výrazne zlepšuje výkon živého vysielania. Maximálne rozlíšenie je 4K.</string>\n    <string name=\"live_stream_fix\">Oprava živého vysielania (1080p)</string>\n    <string name=\"live_stream_fix_4k\">Oprava živého vysielania (4K)</string>\n    <string name=\"playback_notifications_fix_desc\">Skyje notifikáciu pri zmene skladby. Môže byť uzitočné na AOSP firmvéri.</string>\n    <string name=\"playback_notifications_fix\">Vypnúť notifikácie pri prehrávaní</string>\n    <string name=\"amlogic_fix_desc\">Opravuje poklesu snímok na Amlogic zariadeniach.</string>\n    <string name=\"amlogic_fix\">Oprava Amlogic 1080p\\@60fps</string>\n    <string name=\"tunneled_video_playback_desc\">POZNÁMKA: pozastavenie nemusí fungovať správne! Tunelové video prehrávanie zlepšuje audio/video synchronizáciu (AV sync) a hladšie prehrávanie. Vyžadované na Android 5+</string>\n    <string name=\"tunneled_video_playback\">Tunelové video prehrávanie (Android 5+)</string>\n    <string name=\"master_volume\">Hlavná hlasitosť</string>\n    <string name=\"volume_limit\">Obmedzenie hlasitosti</string>\n    <string name=\"player_volume\">Hlasitosť</string>\n    <string name=\"play_video\">Prehrať</string>\n    <string name=\"remember_position_of_short_videos\">Zapamätať pozíciu v Shorts (menej ako 5 min)</string>\n    <string name=\"remember_position_of_live_videos\">Zapamätať pozíciu živého vysielania</string>\n    <string name=\"player_show_tooltips\">Zobraziť popis tlačítok</string>\n    <string name=\"action_like_unset\">\"Páči sa\" bolo odstránené</string>\n    <string name=\"action_dislike_unset\">\"Nepáči sa\" bolo odstránené</string>\n    <string name=\"various_buttons\">Tlačítka v hornej časti hlavného okna</string>\n    <string name=\"not_compatible_with\">Vybrané nastavenie nie je kompatibilné s</string>\n    <string name=\"subtitle_position\">Spodné posunutie titulkov</string>\n    <string name=\"pressing_home\">stlačením tlačidla DOMOV</string>\n    <string name=\"pressing_home_back\">stlačením tlačidla DOMOV alebo SPÄŤ</string>\n    <string name=\"pressing_back\">stlačením tlačidla SPÄŤ</string>\n    <string name=\"player_number_key_seek\">Pretáčanie s číslami</string>\n    <string name=\"save_remove_playlist\">Uložiť/Zmazať playlist zo sekcie Playlisty</string>\n    <string name=\"save_playlist\">Uložiť playlist do sekcie Playlisty</string>\n    <string name=\"remove_playlist\">Zmazať playlist zo sekcie Playlisty natrvalo</string>\n    <string name=\"remove_playlist_fmt\">Natrvalo zmazať %s?</string>\n    <string name=\"removed_from_playlists\">Zmazané zo sekcie Playlisty</string>\n    <string name=\"saved_to_playlists\">Pridané do sekcie Playlisty</string>\n    <string name=\"create_playlist\">Vytvoriť playlist</string>\n    <string name=\"create_playlist_note\">POZNÁMKA: Nebude viditeľný v aplikácii YouTube</string>\n    <string name=\"add_video_to_new_playlist\">Pridať do nového playlistu</string>\n    <string name=\"cant_delete_empty_playlist\">Nemožno zmazať prázdny playlist</string>\n    <string name=\"playlist\">Playlist</string>\n    <string name=\"rename_playlist\">Premenovať playlist</string>\n    <string name=\"cant_rename_empty_playlist\">Nemožno premenovať prázdny playlist</string>\n    <string name=\"cant_rename_foreign_playlist\">Nemožno premenovať cudzí playlist</string>\n    <string name=\"cant_save_playlist\">Tento playlist nebolo možné pridať</string>\n    <string name=\"enter_value\">Zadajte ľubovoľnú hodnotu</string>\n    <string name=\"dialog_add_remove_from\">Pridať/Odobrať z %s</string>\n    <string name=\"lb_playback_controls_skip_next\">Preskočiť Ďalšie</string>\n    <string name=\"lb_playback_controls_skip_previous\">Preskočiť Predchádzajúce/Pretočiť na začiatok</string>\n    <string name=\"alt_presets_behavior\">Alternatívne správanie predvolieb (obmedzenie šírky pásma)</string>\n    <string name=\"alt_presets_behavior_desc\">Aplikácia sa bude snažiť udržať prednastavenú šírku pásma namiesto zhody medzi rozlíšením, fps a kodekom.</string>\n    <string name=\"sleep_timer\">Časovač spánku (1 hodina neaktivity)</string>\n    <string name=\"sleep_timer_desc\">Pozastaviť prehrávanie ak používateľ nepoužije ovládač počas jednej hodiny</string>\n    <string name=\"disable_vsync\">Vypnúť vertikálnu synchronizáciu</string>\n    <string name=\"disable_vsync_desc\">Táto možnosť zakáže zarovnanie snímkov so signálom vertikálnej synchronizácie displeja. Môže pomôcť znížiť CPU záťaž na slabších zariadeniach.</string>\n    <string name=\"skip_codec_profile_check\">Preskočiť kontrolu kodeku</string>\n    <string name=\"skip_codec_profile_check_desc\">Preskočí kontrolu podpory kodeku pri spúšťaní videa. Môže pomôcť pri chybnom firmvéri.</string>\n    <string name=\"force_sw_codec\">Vynútiť softvérový video dekóder</string>\n    <string name=\"force_sw_codec_desc\">Prehrá takmer každé video, ale výkon je veľmi slabý.</string>\n    <string name=\"playlist_order\">Zoradiť playlist</string>\n    <string name=\"playlist_order_added_date_newer_first\">Dátum pridania (novšie prvé)</string>\n    <string name=\"playlist_order_added_date_older_first\">Dátum pridania (staršie prvé)</string>\n    <string name=\"playlist_order_published_date_newer_first\">Dátum publikácie (novšie prvé)</string>\n    <string name=\"playlist_order_published_date_older_first\">Dátum publikácie (staršie prvé)</string>\n    <string name=\"playlist_order_popularity\">Popularita</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">Nemožno spraviť pre cudzí playlist</string>\n    <string name=\"owned_playlist_warning\">Chyba: Táto akcia je dostupná iba pre vlastné playlisty</string>\n    <string name=\"content_block_alt_server\">Použiť alternatívny server</string>\n    <string name=\"content_block_alt_server_desc\">Povoľte túto možnosť, keď SponsorBlock z nejakého dôvodu odmieta fungovať.</string>\n    <string name=\"unset_stream_reminder\">Zrušiť pripomenutie na živé vysielanie</string>\n    <string name=\"set_stream_reminder\">Nastaviť pripomenutie na živé vysielanie</string>\n    <string name=\"playback_starts_shortly\">Prehrávanie začne automaticky pri začatí živého vysielania</string>\n    <string name=\"starting_stream\">Začína živé vysielanie…</string>\n    <string name=\"action_playlist_remove\">Odobrať z playlistu</string>\n    <string name=\"signin_view_title\">Načítavanie Používateľského Kódu…</string>\n    <string name=\"signin_view_description\">Pre prihlásenie vložte kód zo stránky %s\\nPOZNÁMKA. Funguje len na prehliadačoch Firefox alebo Chrome.</string>\n    <string name=\"signin_view_action_text\">Hotovo</string>\n    <string name=\"require_checked\">Vyžaduje \\\"%s\\\"</string>\n    <string name=\"msg_press_again_to_exit\">Stlačte znovu pre ukončenie</string>\n    <string name=\"player_remaining_time\">Zostáva: %s</string>\n    <string name=\"player_ending_time\">Končí o %s</string>\n    <string name=\"badge_new_content\">NOVÝ OBSAH</string>\n    <string name=\"badge_live\">NAŽIVO</string>\n    <string name=\"add_device_view_description\">Pre spárovanie zariadenia, vložte tento kód vo vašej aplikácii YouTube v sekcii Nastavenia/Prepojenie televíznym kódom\\nPOZNÁMKA. Toto funguje len s klávesnicou Gboard.</string>\n    <string name=\"playback_controls_repeat_pause\">Opakovať Pozastavenie</string>\n    <string name=\"playback_controls_repeat_list\">Opakovať Zoznam</string>\n    <string name=\"action_subscribe_off\">Neodoberať</string>\n    <string name=\"action_subscribe_on\">Odoberať</string>\n    <string name=\"skip_24_rate\">Preskočiť formáty s 24fps (oprava pre režim Real Cinema\\?)</string>\n    <string name=\"skip_shorts\">Preskočiť Shorts</string>\n    <string name=\"player_disable_suggestions\">Vypnúť návrhy</string>\n    <string name=\"suggestions\">Návrhy</string>\n    <string name=\"feedback\">Spätná väzba</string>\n    <string name=\"sources\">Zdroje</string>\n    <string name=\"releases\">Vydané verzie</string>\n    <string name=\"prefer_avc_over_vp9\">Výber kodeku: preferovať AVC pred VP9</string>\n    <string name=\"open_chat\">Chat/Komentáre</string>\n    <string name=\"open_comments\">Otvoriť komentáre</string>\n    <string name=\"place_chat_left\">Zobraziť chat na ľavej strane</string>\n    <string name=\"place_comments_left\">Umiestniť komentáre vľavo</string>\n    <string name=\"use_alt_speech_recognizer\">Alternatívne rozpoznávanie jazyka</string>\n    <string name=\"time_format\">Formát času</string>\n    <string name=\"time_format_24\">24 hodín</string>\n    <string name=\"time_format_12\">12 hodín (AM/PM)</string>\n    <string name=\"use_alt_speech_recognizer_desc\">Veľmi zabugované. Použite ho iba v prípade, že máte problémy s predvoleným rozpoznávačom.</string>\n    <string name=\"speech_recognizer\">Rozpoznávanie hlasu</string>\n    <string name=\"speech_engine\">Hlasový vyhľadávač</string>\n    <string name=\"speech_recognizer_system\">Systém (odporúčané)</string>\n    <string name=\"speech_recognizer_external_1\">Externé 1 (veľmi zabugované, pre funkčnosť musíte zakázať prístup k mikrofónu)</string>\n    <string name=\"speech_recognizer_external_2\">Externé 2 (veľmi zabugované)</string>\n    <string name=\"real_channel_icon\">Zobraziť ikonu na tlačidle kanála</string>\n    <string name=\"subtitle_yellow_semi_transparent\">Žlté na polo-priehľadnom pozadí</string>\n    <string name=\"subtitle_yellow_black\">Žlté na tmavom pozadí</string>\n    <string name=\"player_pixel_ratio\">Pomer pixelov</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">Tmavo-šedá (monochromatická)</string>\n    <string name=\"disable_mic_permission\">Zakážte prosím aplikácii prístup k mikrofónu, aby tento rozpoznávač fungoval správne.</string>\n    <string name=\"repeat_mode_shuffle\">Náhodné prehrávanie playlistov</string>\n    <string name=\"chat_left\">Vľavo</string>\n    <string name=\"chat_right\">Vpravo</string>\n    <string name=\"card_real_thumbnails\">Vyberať náhľadové obrázky z videa</string>\n    <string name=\"card_content\">Výber snímku z videa</string>\n    <string name=\"thumb_quality_default\">Predvolené</string>\n    <string name=\"thumb_quality_start\">Na začiatku videa</string>\n    <string name=\"thumb_quality_middle\">V strede videa</string>\n    <string name=\"thumb_quality_end\">Na konci videa</string>\n    <string name=\"content_block_status\">Skontrolovať stav SponsorBlock servera</string>\n    <string name=\"dearrow_status\">Skontrolovať stav DeArrow servera</string>\n    <string name=\"player_show_quality_info_bitrate\">Pridaj bitrate do informácií o kvalite</string>\n    <string name=\"player_speed_button_old_behavior\">Vrátiť staré správanie tlačidla rýchlosti</string>\n    <string name=\"protect_settings_with_password\">Chrániť všetky nastavenia heslom</string>\n    <string name=\"enter_settings_password\">Zadajte heslo</string>\n    <string name=\"child_mode\">Detský režim</string>\n    <string name=\"child_mode_desc\">V tomto režime používateľ nemôže použiť vyhľadávanie ani vidieť žiadny navrhovaný obsah. Nastavenia budú chránené heslom.</string>\n    <string name=\"lost_setting_warning\">Nastavenia aplikácie sa zmenia. Skontrolujte, či máte vytvorenú zálohu nastavení.</string>\n    <string name=\"player_button_long_click\">Dlhým stlačením tohto tlačidla získate ďalšie možnosti</string>\n    <string name=\"pause_history\">Pozastaviť históriu</string>\n    <string name=\"resume_history\">Obnoviť históriu</string>\n    <string name=\"disable_history\">Vypnúť históriu</string>\n    <string name=\"enable_history\">Povoliť históriu</string>\n    <string name=\"clear_history\">Zmazať históriu</string>\n    <string name=\"chapters\">Kapitoly</string>\n    <string name=\"card_multiline_subtitle\">Viacriadkové titulky</string>\n    <string name=\"auto_frame_rate_modes\">Podporované režimy</string>\n    <string name=\"loading\">Načítavanie…</string>\n    <string name=\"hide_streams\">Skryť živé vysielanie z Odberov</string>\n    <string name=\"video_rotate\">Otočiť</string>\n    <string name=\"video_flip\">Prevrátiť (zrkadliť)</string>\n    <string name=\"trending_searches\">Vyhľadávacie trendy</string>\n    <string name=\"video_duration_any\">Všetky</string>\n    <string name=\"video_duration\">Trvanie</string>\n    <string name=\"video_duration_under_4\">Menej ako 4 minúty</string>\n    <string name=\"video_duration_between_4_20\">4–20 minút</string>\n    <string name=\"video_duration_over_20\">Viac ako 20 minút</string>\n    <string name=\"content_type\">Typ</string>\n    <string name=\"content_type_any\">Všetky</string>\n    <string name=\"content_type_video\">Video</string>\n    <string name=\"content_type_channel\">Kanál</string>\n    <string name=\"content_type_playlist\">Playlist</string>\n    <string name=\"content_type_movie\">Film</string>\n    <string name=\"video_features\">Vlastnosti</string>\n    <string name=\"video_feature_any\">Všetky</string>\n    <string name=\"video_feature_live\">Naživo</string>\n    <string name=\"video_feature_4k\">4K</string>\n    <string name=\"video_feature_hdr\">HDR</string>\n    <string name=\"search_sorting\">Zoradiť podľa</string>\n    <string name=\"sort_by_relevance\">Relevancia</string>\n    <string name=\"sort_by_views\">Počet zhliadnutí</string>\n    <string name=\"sort_by_date\">Dátum nahratia</string>\n    <string name=\"sort_by_rating\">Hodnotenie</string>\n    <string name=\"clear_search_history\">Zmazať históriu vyhľadávania</string>\n    <string name=\"remove_from_subscriptions\">Skryť</string>\n    <string name=\"player_long_speed_list\">Dlhý zoznam rýchlostí videa</string>\n    <string name=\"player_extra_long_speed_list\">Extra dlhý zoznam rýchlostí videa</string>\n    <string name=\"alt_app_icon\">Alternatívna ikonka aplikácie (potrebný reštart)</string>\n    <string name=\"network_stack\">Preferovať sieťový zásobník %s</string>\n    <string name=\"player_network_stack\">Sieťový mechanizmus</string>\n    <string name=\"cronet_desc\">Cronet je Chromium sieťový zásobník. Môže znížiť oneskorenie a zvýšiť výkon siete, čo môže pomôcť pri problémoch s ukladaním do medzipamäte.</string>\n    <string name=\"unlock_all_formats\">Odblokovať všetky video formáty</string>\n    <string name=\"unlock_all_formats_desc\">Na niektorých zariadeniach hlási firmvér niektoré formáty ako nepodporované aj keď podporované sú.\\n(napr. smart televízie s rozlíšením 1080p majú tendenciu hlásiť 4K ako nepodporované).</string>\n    <string name=\"okhttp_desc\">Tento sieťový mechanizmus je najpomalší, ale najstabilnejší.</string>\n    <string name=\"select_channel_section\">Otvoriť zodpovedajúcu sekciu po otvorení aplikácie z kanálov ATV</string>\n    <string name=\"enable_voice_search_desc\">Oficiálna aplikácia bude nahradená tzv. \\\"mostom\\\". Most pomáha prenášať požiadavky globálneho vyhľadávania do tejto aplikácie.</string>\n    <string name=\"enable_master_password\">Povoliť hlavné heslo</string>\n    <string name=\"enter_master_password\">Zadajte hlavné heslo</string>\n    <string name=\"disable_stream_buffer\">Zakázať medzipamäť pri živom vysielaní</string>\n    <string name=\"disable_stream_buffer_desc\">Oprava pre situácie, keď je živé vysielanie príliš pozadu. Poznámka: živé vysielanie môže začať sekať.</string>\n    <string name=\"sony_frame_drop_fix\">Oprava výpadkov prehrávania #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">Opravuje sekanie na Sony TV a iných zariadeniach. Poznámka: môžu nastať problémy so synchronizáciou audia.</string>\n    <string name=\"audio_language\">Jazyk zvuku</string>\n    <string name=\"old_home_look\">Starý vzhľad sekcie Domov</string>\n    <string name=\"old_channel_look\">Starý vzhľad sekcie Kanály</string>\n    <string name=\"hide_shorts_everywhere\">Skryť Shorts všade</string>\n    <string name=\"update_found\">Aktualizácia</string>\n    <string name=\"volume_boost_warning\">Akékoľvek nastavenie nad limit môže poškodiť reproduktory</string>\n    <string name=\"play_video_incognito\">Prehrať anonymne</string>\n    <string name=\"play_from_start\">Prehrať od začiatku</string>\n    <string name=\"header_kids_home\">Deti</string>\n    <string name=\"player_section_playlist\">Použiť aktuálny obsah sekcie ako playlist</string>\n    <string name=\"header_trending\">Trendy</string>\n    <string name=\"screensaver\">Šetrič obrazovky</string>\n    <string name=\"player_screen_off_timeout\">Časový limit vypnutia obrazovky</string>\n    <string name=\"old_update_notifications\">Starý vzhľad notifikácií o aktualizáciách</string>\n    <string name=\"dialog_notification\">Upozorniť na aktualizáciu pomocou vyskakovacieho okna</string>\n    <string name=\"mark_as_watched\">Označiť ako videné</string>\n    <string name=\"player_ui_animations\">Povoliť animácie rozhrania prehrávača</string>\n    <string name=\"player_likes_count\">Zobraziť počet Páči sa/Nepáči sa</string>\n    <string name=\"sorting_alphabetically2\">Abecedne (rýchlo, animované ukážky)</string>\n    <string name=\"sorting_default\">Predvolené (rýchle, animované ukážky)</string>\n    <string name=\"content_block_exclude_channel\">Neaplikovať SponsorBlock na tento kanál</string>\n    <string name=\"content_block_stop_excluding_channel\">Znovu aplikovať SponsorBlock na tento kanál</string>\n    <string name=\"player_chapter_notification\">Rýchlejšie presúvanie cez kapitoly pomocou notifikácií</string>\n    <string name=\"player_chapter_notification2\">Prepnúť na ďalšiu kapitolu kliknutím na notifikáciu</string>\n    <string name=\"subtitle_remember\">Povoliť titulky iba na aktuálnom kanáli</string>\n    <string name=\"amazon_frame_drop_fix\">Oprava výpadkov prehrávania #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">Určené pre zariadenia Amazon Stick. Môže fungovať aj na iných zariadeniach.</string>\n    <string name=\"default_stack_desc\">Predvolený sieťový mechanizmus. Tento mechanizmus môže mať v určitých situáciách lepšiu stabilitu.</string>\n    <string name=\"item_postion\">Pozícia</string>\n    <string name=\"player_screen_off_dimming\">Intenzita stmievania obrazovky</string>\n    <string name=\"screensaver_timout\">Časový limit šetriča obrazovky</string>\n    <string name=\"screensaver_dimming\">Intenzita stmievania šetriča obrazovky</string>\n    <string name=\"player_ui_on_next\">Zobraziť rozhranie prehrávača pri prepnutí na ďalšie video</string>\n    <string name=\"autogenerated\">Automaticky generované</string>\n    <string name=\"player_auto_volume\">Automatické nastavenie hlasitosti</string>\n    <string name=\"header_shorts\">Shorts</string>\n    <string name=\"player_global_focus\">Plynulá navigácia medzi radmi tlačidiel prehrávača</string>\n    <string name=\"auto_history\">Automaticky (použiť nastavenie účtu)</string>\n    <string name=\"remember_position_subscriptions\">Zapamätať poslednú zobrazenú pozíciu v Odberoch</string>\n    <string name=\"remember_position_pinned\">Zapamätať poslednú zobrazenú pozíciu v pripnutých playlistoch</string>\n    <string name=\"msg_player_unknown_error\">Neznáma chyba</string>\n    <string name=\"unknown_source_error\">Neznáma chyba zdroja</string>\n    <string name=\"unknown_renderer_error\">Neznáma chyba vykresľovača</string>\n    <string name=\"header_notifications\">Notifikácie</string>\n    <string name=\"disable_search_history\">Zakázať históriu vyhľadávania</string>\n    <string name=\"unlock_high_bitrate_formats\">Odomknúť formáty 1080p vp9 s vysokým dátovým tokom</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">Odomknúť formáty mp4a s vysokým dátovým tokom</string>\n    <string name=\"color_scheme_blue\">Modrá</string>\n    <string name=\"color_scheme_dark_blue\">Tmavo-modrá</string>\n    <string name=\"player_speed_per_channel\">Pre každý kanál</string>\n    <string name=\"disable_popular_searches\">Nezobrazovať obľúbené vyhľadávanie</string>\n    <string name=\"multi_profiles\">Použiť samostatné nastavenia pre každý účet</string>\n    <string name=\"protect_account_with_password\">Chrániť toto konto heslom</string>\n    <string name=\"enter_account_password\">Zadajte heslo účtu</string>\n    <string name=\"show_connect_messages\">Zobrazovať správy o pripojení</string>\n    <string name=\"prefer_avc_over_vp9_desc\">UPOZORNENIE: Maximálne 1080p</string>\n    <string name=\"play_next\">Prehrať ďalšie</string>\n    <string name=\"hide_watched_from_subscriptions\">Skryť videné videá v Odberoch</string>\n    <string name=\"hide_watched_from_notifications\">Skryť videné videá v Notifikáciách</string>\n    <string name=\"hide_unwanted_content\">Skryť videá</string>\n    <string name=\"hide_watched_from_home\">Skryť videné videá v Domov</string>\n    <string name=\"hide_watched_from_watch_later\">Skryť videné videá zo zoznamu Pozrieť neskôr</string>\n    <string name=\"remote_control_permission\">Diaľkové ovládanie na pozadí vyžaduje povolenie na prekrytie cez okná</string>\n    <string name=\"login_from_browser\">Prihlásenie z prehliadača</string>\n    <string name=\"disable_remote_history\">Zakázanť históriu pri používaní diaľkového ovládania</string>\n    <string name=\"keyboard_fix\">Zabrániť otvoreniu klávesnice tlačidlom OK (G20s a iné)</string>\n    <string name=\"nothing_found\">Nič nebolo nájdené</string>\n    <string name=\"auto_frame_rate_desc\">Táto možnosť odstraňuje chvenie pri scénach, v ktorých sa kamera rýchlo pohybuje, napr. pri športové prenosy</string>\n    <string name=\"channel_filter_hint\">Filtrovať kanály</string>\n    <string name=\"player_loop_shorts\">Shorts v slučke</string>\n    <string name=\"player_quick_shorts_skip\">Preskočiť Shorts s tlačidlami vľavo/vpravo</string>\n    <string name=\"player_quick_shorts_skip_alt\">Preskočiť Shorts s tlačidlami hore/dolu</string>\n    <string name=\"player_quick_skip_videos\">Preskočiť videá s tlačidlami vľavo/vpravo</string>\n    <string name=\"player_quick_skip_videos_alt\">Preskočiť videá s tlačidlami hore/dolu</string>\n    <string name=\"channels_filter\">Zobraziť pole filtrovania kanálov v sekcii Kanály</string>\n    <string name=\"channel_search_bar\">Vyhľadávací panel v sekcii Kanály</string>\n    <string name=\"header_sports\">Šport</string>\n    <string name=\"keep_finished_activities\">Uchovávať dokončené aktivity</string>\n    <string name=\"disable_channels_service\">Zakázať službu Kanály</string>\n    <string name=\"replace_titles\">Nahradiť názvy</string>\n    <string name=\"enable\">Povoliť</string>\n    <string name=\"more_info\">Viac informácií</string>\n    <string name=\"about_sponsorblock\">O SponsorBlocku</string>\n    <string name=\"about_dearrow\">O DeArrow</string>\n    <string name=\"replace_thumbnails\">Nahradiť náhľadové obrázky</string>\n    <string name=\"crowdsourced_thumbnails\">Komunitné náhľadové obrázky</string>\n    <string name=\"crowdsoursed_titles\">Komunitné názvy</string>\n    <string name=\"dearrow_not_submitted_thumbs\">Zdroj náhľadových obrázkov (ak nie je predložený DeArrow)</string>\n    <string name=\"pitch_effect\">Efekt výšky tónu</string>\n    <string name=\"fullscreen_mode\">Režim celej obrazovky (bez systémových líšt)</string>\n    <string name=\"player_only_mode\">Zobraziť iba prehrávač, ak je video otvorené mimo aplikácie</string>\n    <string name=\"pinned_channel_rows\">Zobraziť pripnutý kanál ako riadky</string>\n    <string name=\"prefer_google_dns\">Uprednostniť Google DNS</string>\n    <string name=\"prefer_ipv4\">Uprednostniť IPv4 DNS protokol</string>\n    <string name=\"prefer_ipv4_desc\">Mohlo by to vyriešiť situácie, keď aplikácia vôbec nefunguje.\\nPoznámka. Môže spôsobiť mrznutie a pády (najmä na zariadeniach so systémom Android 8 alebo Dune HD)</string>\n    <string name=\"long_press_for_settings\">STLAČTE DLHO PRE NASTAVENIA</string>\n    <string name=\"long_press_for_options\">STLAČTE DLHO PRE MOŽNOSTI</string>\n    <string name=\"device_specific_backup\">Zálohovať len toto zariadenie</string>\n    <string name=\"local_backup\">Lokálna záloha</string>\n    <string name=\"auto_backup\">Automatická záloha (raz denne)</string>\n    <string name=\"repeat_mode_reverse_list\">Prehrať videá z playlistu alebo kanála v opačnom poradí</string>\n    <string name=\"calm_msg\">Pracujeme na oprave oproblému. Pravidelne kontrolujte aktualizácie.</string>\n    <string name=\"without_picture\">Bez obrazu</string>\n    <string name=\"video_disabled\">Video zakázané</string>\n    <string name=\"applying_fix\">Nezatvárajte prehrávač. Aplikuje sa oprava…</string>\n    <string name=\"hide_mixes\">Skryť mixy</string>\n    <string name=\"player_global_focus_desc\">Táto funkcia ovplyvňuje, ktoré tlačidlo prehrávača bude aktívne pri prechádzaní medzi riadkami tlačidiel prehrávača</string>\n    <string name=\"disable_network_error_fixing\">Zakázať automatickú opravu chýb siete</string>\n    <string name=\"disable_network_error_fixing_desc\">Ak používate sieť VPN, pravdepodobne budete musieť povoliť túto možnosť</string>\n    <string name=\"recommended\">Odporúčané</string>\n    <string name=\"add_to_subscriptions_group\">Pridať/Odstrániť zo skupiny Odberov</string>\n    <string name=\"new_subscriptions_group\">Nová skupina</string>\n    <string name=\"rename_group\">Premenovať skupinu odberov</string>\n    <string name=\"screen_dimming_amount\">Miera stmavenia obrazovky</string>\n    <string name=\"screen_dimming_timeout\">Časový limit stmavenia obrazovky</string>\n    <string name=\"playlists_rows\">Zobraziť sekciu Playlisty ako riadky</string>\n    <string name=\"import_subscriptions_group\">Importovať</string>\n    <string name=\"lb_playback_controls_closed_captioning_disable\">Vypnúť titulky</string>\n    <string name=\"lb_playback_controls_closed_captioning_enable\">Zapnúť titulky</string>\n    <string name=\"my_videos\">Moje videá</string>\n    <string name=\"player_audio_focus\">Zameranie zvuku (pozastaviť ak boli detekované iné prehrávače)</string>\n    <string name=\"paid_content_notification\">Upozornenie na platený obsah</string>\n    <string name=\"you_liked\">páči sa ti</string>\n    <string name=\"premium_users_only\">Iba pre prémium používateľov. Oprava neúplného zoznamu video formátov</string>\n    <string name=\"playback_buffering_fix\">Oprava načítavania videa</string>\n    <string name=\"oculus_quest_fix\">Oprava pre Oculus Quest</string>\n    <string name=\"card_preview_muted\">Video bez zvuku</string>\n    <string name=\"card_preview_full\">Video so zvukom</string>\n    <string name=\"card_preview\">Náhľad karty</string>\n    <string name=\"card_unlocalized_titles\">Nelokalizované názvy videí</string>\n    <string name=\"dont_resize_video_to_fit_dialog\">Nemeniť veľkosť videa na veľkosť dialógu</string>\n    <string name=\"typing_corrections\">Vypnúť automatické opravy pri písaní textu</string>\n    <string name=\"suggestions_horizontally_scrolled\">Horizontálne posúvané návrhy</string>\n    <string name=\"stable_restore\">Obnoviť stabilnú verziu (všetky dáta budú stratené)</string>\n    <string name=\"auto_backup_category\">Automatická záloha</string>\n    <string name=\"once_a_day\">Raz denne</string>\n    <string name=\"once_a_week\">Raz týždenne</string>\n    <string name=\"once_a_month\">Raz mesačne</string>\n    <string name=\"action_debug_info\">Diagnostické informácie</string>\n    <string name=\"dialog_block_channel\">Zablokovať kanál</string>\n    <string name=\"dialog_unblock_channel\">Odblokovať kanál</string>\n    <string name=\"confirm_block_channel\">Skryť všetok obsah od %s?</string>\n    <string name=\"channel_blocked\">Kanál zablokovaný</string>\n    <string name=\"channel_unblocked\">Kanál odblokovaný</string>\n    <string name=\"header_blocked_channels\">Zablokované kanály</string>\n    <string name=\"msg_no_blocked_channels\">Žiadne zablokované kanály</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-sl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Domov</string>\n  <string name=\"title_search\">Išči</string>\n  <string name=\"header_subscriptions\">Naročnine</string>\n  <string name=\"header_history\">Zgodovina</string>\n  <string name=\"header_music\">Glasba</string>\n  <string name=\"header_news\">Novice</string>\n  <string name=\"header_gaming\">Igre</string>\n  <string name=\"header_playlists\">Seznami predvajanja</string>\n  <string name=\"header_settings\">Nastavitve</string>\n  <string name=\"header_channels\">Kanali</string>\n  <string name=\"subscriptions_signin_title\">Prikažite zadnje stvari iz kanalov, ki jih imate radi</string>\n  <string name=\"subscriptions_signin_subtitle\">Prijavi se, da vidiš svoje naročnine</string>\n  <string name=\"subscriptions_signin_button_text\">PRIJAVI SE</string>\n  <string name=\"library_signin_to_show_more\">Glej video, ki ti je bil všeč, si ga shranil ali se nanj naročil</string>\n  <string name=\"library_signin_subtitle\">Prijavi se, da vidiš svojo knjižnico</string>\n  <string name=\"action_signin\">PRIJAVI SE</string>\n  <string name=\"title_video_formats\">Video format</string>\n  <string name=\"title_audio_formats\">Avdio format</string>\n  <string name=\"subtitle_category_title\">Podnapisi</string>\n  <string name=\"video_max_quality\">Avto (maks. kvaliteta)</string>\n  <string name=\"audio_max_quality\">Avto (maks. kvaliteta)</string>\n  <string name=\"subtitles_disabled\">Podnapisi so onemogočeni</string>\n  <string name=\"auto_frame_rate\">Samodejna frekvenca osveževanja</string>\n  <string name=\"frame_rate_correction\">Popravi fps:\\n%s</string>\n  <string name=\"category_background_playback\">Predvajanje v ozadju</string>\n  <string name=\"not_implemented\">Ni implementirano</string>\n  <string name=\"option_background_playback_off\">Onemogočeno</string>\n  <string name=\"option_background_playback_pip\">Slika v sliki</string>\n  <string name=\"option_background_playback_only_audio\">Samo zvok</string>\n  <string name=\"playback_settings\">Nastavitve predvajanja</string>\n  <string name=\"video_speed\">Video hitrost</string>\n  <string name=\"resolution_switch\">Zamenjaj ločljivost</string>\n  <string name=\"video_buffer\">Video pomnilnik</string>\n  <string name=\"video_buffer_size_low\">Nizek</string>\n  <string name=\"video_buffer_size_med\">Srednji</string>\n  <string name=\"video_buffer_size_high\">Visok</string>\n  <string name=\"update_changelog\">Seznam sprememb</string>\n  <string name=\"install_update\">Namesti posodobitve</string>\n  <string name=\"section_is_empty\">Ups! Nič ni tukaj.</string>\n  <string name=\"dialog_add_to_playlist\">Dodaj na seznam predvajanja</string>\n  <string name=\"msg_signed_users_only\">Samo za prijavljene uporabnike</string>\n  <string name=\"msg_cant_load_content\">Ups. Ne morem naložiti vsebine.</string>\n  <string name=\"title_video_presets\">Video predloge</string>\n  <string name=\"settings_accounts\">Računi</string>\n  <string name=\"settings_left_panel\">Uredi kategorije</string>\n  <string name=\"settings_themes\">Teme</string>\n  <string name=\"settings_other\">Ostalo</string>\n  <string name=\"settings_player\">Video predvajalnik</string>\n  <string name=\"settings_language\">Jezik</string>\n  <string name=\"settings_linked_devices\">Povezane naprave</string>\n  <string name=\"settings_about\">O programu</string>\n  <string name=\"dialog_account_list\">Izberi račun</string>\n  <string name=\"dialog_account_none\">Brez</string>\n  <string name=\"dialog_remove_account\">Odstrani račun</string>\n  <string name=\"dialog_add_account\">Dodaj račun</string>\n  <string name=\"default_lang\">Privzeto</string>\n  <string name=\"dialog_select_language\">Jezik aplikacije</string>\n  <string name=\"subtitle_default\">Privzeto</string>\n  <string name=\"subtitle_white_semi_transparent\">Polprosojno ozadje</string>\n  <string name=\"subtitle_style\">Oblika podnapisov</string>\n  <string name=\"subtitle_language\">Jezik podnapisov</string>\n  <string name=\"action_search\">Začni iskanje</string>\n  <string name=\"settings_main_ui\">Uporabniški vmesnik</string>\n  <string name=\"dialog_main_ui\">Uporabniški vmesnik</string>\n  <string name=\"card_animated_previews\">Animirani predogledi</string>\n  <string name=\"web_site\">Spletna stran</string>\n  <string name=\"donation\">Donacija</string>\n  <string name=\"dialog_about\">O programu</string>\n  <string name=\"dialog_player_ui\">Video predvajalnik</string>\n  <string name=\"player_show_ui_on_pause\">Pokaži UI med pavzo</string>\n  <string name=\"player_pause_on_ok\">OK tipka pavzira predvajanje</string>\n  <string name=\"player_ok_button_behavior\">Obnašanje OK tipke</string>\n  <string name=\"player_only_ui\">Samo UI</string>\n  <string name=\"player_ui_and_pause\">UI in pavza</string>\n  <string name=\"player_only_pause\">Samo pavza</string>\n  <string name=\"check_for_updates\">Preveri posodobitve</string>\n  <string name=\"update_not_found\">Uporabljaš zadnjo verzijo</string>\n  <string name=\"update_in_progress\">Počakaj…</string>\n  <string name=\"player_ui_hide_behavior\">Samodejno skrij UI</string>\n  <string name=\"option_never\">Nikoli</string>\n  <string name=\"side_panel_sections\">Nastavi kategorije</string>\n  <string name=\"boot_to_section\">Kategorija ob zagonu</string>\n  <string name=\"large_ui\">Velik UI</string>\n  <string name=\"video_grid_scale\">Razmerje video mreže</string>\n  <string name=\"scale_ui\">UI razmerje</string>\n  <string name=\"color_scheme\">Barvna shema</string>\n  <string name=\"color_scheme_default\">Privzeto</string>\n  <string name=\"color_scheme_red_grey\">Rdeče siva</string>\n  <string name=\"color_scheme_red\">Rdeča</string>\n  <string name=\"color_scheme_dark_grey\">Temno siva</string>\n  <string name=\"disable_update_check\">Onemogoči preverjanje nove verzije</string>\n  <string name=\"show_again\">Pokaži ponovno</string>\n  <string name=\"check_updates_auto\">Samodejno posodobi</string>\n  <string name=\"select_account_on_boot\">Izberi ob zagonu</string>\n  <string name=\"player_other\">Mešano</string>\n  <string name=\"player_full_date\">Točen datum v opisu</string>\n  <string name=\"open_channel\">Odpri kanal</string>\n  <string name=\"not_interested\">Me ne zanima</string>\n  <string name=\"you_wont_see_this_video\">Video je bil odstranjen iz priporočil</string>\n  <string name=\"settings_search\">Išči</string>\n  <string name=\"dialog_search\">Išči</string>\n  <string name=\"instant_voice_search\">Takojšnje glasovno iskanje</string>\n  <string name=\"option_background_playback_behind\">Predvajaj v ozadju</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">Podatki o naslednjem videu še niso naloženi</string>\n  <string name=\"card_multiline_title\">Večvrstični naslovi</string>\n  <string name=\"cards_style\">Stil kartic</string>\n  <string name=\"repeat_mode_all\">Predvajaj vse videe enega za drugim</string>\n  <string name=\"repeat_mode_one\">Ponovi trenutni video</string>\n  <string name=\"repeat_mode_pause\">Pavziraj predvajanje po vsakem videu</string>\n  <string name=\"repeat_mode_none\">Ustavi predvajanje po enem videu</string>\n  <string name=\"subscribed_to_channel\">Naročen na kanal</string>\n  <string name=\"unsubscribed_from_channel\">Odjavljen iz kanala</string>\n  <string name=\"subtitle_yellow_transparent\">Rumeni</string>\n  <string name=\"subtitle_white_black\">Beli s črnim ozadjem</string>\n  <string name=\"player_seek_preview\">Predogled med iskanjem</string>\n  <string name=\"color_scheme_dark_grey_oled\">Temnosiva (OLED)</string>\n  <string name=\"channels_section_sorting\">Sortiranje v kategoriji kanalov</string>\n  <string name=\"sorting_by_new_content\">Nova vsebina</string>\n  <string name=\"sorting_alphabetically\">Po abecedi</string>\n  <string name=\"sorting_last_viewed\">Nazadnje gledano</string>\n  <string name=\"player_pause_when_seek\">Pavziraj med previjanjem</string>\n  <string name=\"playlists_style\">Stil seznama predvajanja</string>\n  <string name=\"playlists_style_grid\">Mreža</string>\n  <string name=\"playlists_style_rows\">Vrstice</string>\n  <string name=\"player_show_clock\">Prikaži uro v nadzorni vrstici</string>\n  <string name=\"player_show_remaining_time\">Prikaži preostali čas v nadzorni vrstici</string>\n  <string name=\"open_channel_uploads\">Odpri nalaganje kanala</string>\n  <string name=\"app_exit_shortcut\">Izhod iz aplikacije</string>\n  <string name=\"app_exit_none\">Brez izhoda</string>\n  <string name=\"app_double_back_exit\">Dvakrat nazaj</string>\n  <string name=\"app_single_back_exit\">Enkrat nazaj</string>\n  <string name=\"action_video_zoom\">Video povečava</string>\n  <string name=\"video_zoom\">Video povečava</string>\n  <string name=\"video_zoom_default\">Privzeto</string>\n  <string name=\"video_zoom_fit_width\">Zapolni širino</string>\n  <string name=\"video_zoom_fit_height\">Zapolni višino</string>\n  <string name=\"video_zoom_fit_both\">Zapolni širino ali višino</string>\n  <string name=\"video_zoom_stretch\">Raztegni</string>\n  <string name=\"color_scheme_teal\">Zeleno modra</string>\n  <string name=\"color_scheme_teal_oled\">Zeleno modra (OLED)</string>\n  <string name=\"player_seek_preview_none\">Onemogočeno</string>\n  <string name=\"player_seek_preview_single\">Enojna slika</string>\n  <string name=\"player_seek_preview_carousel\">Vrtiljak</string>\n  <string name=\"player_seek_preview_carousel_slow\">Vrtiljak (počasno)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Vrtiljak (hitreje)</string>\n  <string name=\"unsubscribe_from_channel\">Odjava iz kanala</string>\n  <string name=\"card_title_lines_num\">Število kartic v naslovu</string>\n  <string name=\"card_auto_scrolled_title\">Samodejno zadrsaj porežene naslove</string>\n  <string name=\"share_link\">Deli povezavo</string>\n  <string name=\"subscribe_to_channel\">Naroči se na kanal</string>\n  <string name=\"wait_data_loading\">Prosim počakaj, da se naložijo podatki…</string>\n  <string name=\"auto_frame_rate_pause\">Pavziraj ob samodejne hitrosti osveževanje</string>\n  <string name=\"auto_frame_rate_applying\">Uveljavi samodejno hitrost osveževanja %sx%s\\@%s</string>\n  <string name=\"audio_shift\">Avdio zamik</string>\n  <string name=\"player_remember_speed\">Zapomni si hitrost</string>\n  <string name=\"mark_channel_as_watched\">Označi kot ogledan</string>\n  <string name=\"channel_marked_as_watched\">Kanal je bil označen kot ogledan</string>\n  <string name=\"dialog_add_device\">Dodaj napravo</string>\n  <string name=\"dialog_remove_all_devices\">Odstrani napravo</string>\n  <string name=\"device_link_enabled\">Povezava omogočena</string>\n  <string name=\"device_connected\">Naprava \\\"%s\\\" je bila povezana</string>\n  <string name=\"device_disconnected\">Naprava \\\"%s\\\" se je odklopila</string>\n  <string name=\"playback_queue_category_title\">Čakalna vrsta za predvajanje</string>\n  <string name=\"open_playlist\">Odpri seznam predvajanja</string>\n  <string name=\"repeat_mode_pause_alt\">Neprekinjeno predvajaj samo videoposnetke s seznama predvajanja</string>\n  <string name=\"subtitle_white_transparent\">Beli</string>\n  <string name=\"player_show_ending_time\">Prikaži čas zaključka v nadzorni vrstici</string>\n  <string name=\"msg_restart_app\">Ponovno zaženite aplikacijo, da uporabite te nastavitve.</string>\n  <string name=\"settings_block\">Blokiranje vsebine</string>\n  <string name=\"settings_ui_scale\"></string>\n  <string name=\"settings_video\">Video</string>\n  <string name=\"settings_general\">Splošno</string>\n  <string name=\"msg_done\">Končano</string>\n  <string name=\"msg_applying\">Uporaba %s</string>\n  <string name=\"content_block_confirm_skip\">Potrdite preskok</string>\n  <string name=\"content_block_sponsor\">Sponzor</string>\n  <string name=\"content_block_categories\">Preskoči odsek</string>\n  <string name=\"content_block_interaction\">Opomnik za interakcijo (naročite se)</string>\n  <string name=\"content_block_self_promo\">Neplačano/samopromocija</string>\n  <string name=\"content_block_music_off_topic\">Ne-glasbeni del posnetka</string>\n  <string name=\"confirm_segment_skip\">Preskoči odsek \\\"%s\\\"\\?</string>\n  <string name=\"cancel_segment_skip\">Prekliči preskok odseka</string>\n  <string name=\"msg_skipping_segment\">Preskakovanje odseka \\\"%s\\\"…</string>\n  <string name=\"content_block_notification_type\">Vrsta obvestil</string>\n  <string name=\"content_block_notify_none\">Brez obvestil</string>\n  <string name=\"intent_force_close\">Prisilno ustavi, če je bil video odprt iz zunanjega vira</string>\n  <string name=\"btn_confirm\">Potrdi</string>\n  <string name=\"player_low_video_quality\">Nizka kakovost videoposnetkov</string>\n  <string name=\"player_remember_speed_none\">Brez</string>\n  <string name=\"player_sleep_timer\">Časovnik za spanje</string>\n  <string name=\"player_show_quality_info\">Prikaži informacije o kakovosti v nadzorni vrstici</string>\n  <string name=\"player_remember_each_speed\">Zapomni si hitrost vsakega videoposnetka</string>\n  <string name=\"player_remember_speed_all\">Enako za vse videoposnetke</string>\n  <string name=\"player_remember_speed_each\">Za vsak videoposnetek posebej</string>\n  <string name=\"msg_player_error_source\">Videoposnetka ni mogoče prenesti</string>\n  <string name=\"msg_player_error_renderer\">Izbrani video format ni podprt.\\nMorda gre za napako v vdelani programski opremi. Poskusite znova zagnati napravo.</string>\n  <string name=\"video_aspect\">Razmerje dimenzij </string>\n  <string name=\"player_tweaks\">Možnosti za razvijalce</string>\n  <string name=\"header_uploads\">Nalaganja</string>\n  <string name=\"background_playback_activation\">Predvajanje v ozadju (aktivacija)</string>\n  <string name=\"pin_unpin_from_sidebar\">Pripni/odpni iz stranske vrstice</string>\n  <string name=\"pin_unpin_playlist\">Pripni/odpni seznam predvajanja iz stranske vrstice</string>\n  <string name=\"pin_unpin_channel\">Pripni/odpni kanal iz stranske vrstice</string>\n  <string name=\"pinned_to_sidebar\">Pripeto v stransko vrstico</string>\n  <string name=\"unpin_from_sidebar\">Odpni iz stranske vrstice</string>\n  <string name=\"unpinned_from_sidebar\">Odpeto iz stranske vrstice</string>\n  <string name=\"dialog_select_country\">Država</string>\n  <string name=\"settings_language_country\">Jezik/Država</string>\n  <string name=\"screen_dimming\">Zatemnitev zaslona</string>\n  <string name=\"removed_from_playback_queue\">Odstranjeno iz čakalne vrste za predvajanje</string>\n  <string name=\"added_to_playback_queue\">Dodano v čakalno vrsto za predvajanje</string>\n  <string name=\"add_remove_from_playback_queue\">Dodaj/odstrani iz čakalne vrste za predvajanje</string>\n  <string name=\"add_to_playback_queue\">Dodaj v čakalno vrsto za predvajanje</string>\n  <string name=\"remove_from_playback_queue\">Odstrani iz čakalne vrste za predvajanje</string>\n  <string name=\"proxy_enabled\">Proxy strežnik omogočen</string>\n  <string name=\"proxy_disabled\">Proxy strežnik onemogočen</string>\n  <string name=\"uploads_row_name\">Nalaganja</string>\n  <string name=\"playlists_row_name\">Ustvarjeni seznami predvajanja</string>\n  <string name=\"popular_uploads_row_name\">Priljubljeni videoposnetki</string>\n  <string name=\"breaking_news_row_name\">Najnovejše novice</string>\n  <string name=\"covid_news_row_name\">COVID-19 novice</string>\n  <string name=\"enable_voice_search\">Omogoči glasovno iskanje</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Naročite/odjavite se od kanala</string>\n  <string name=\"app_backup_restore\">Varnostno kopiranje/obnovitev</string>\n  <string name=\"app_backup\">Varnostno kopirani podatki</string>\n  <string name=\"app_restore\">Obnovitev podatkov iz varnostne kopije</string>\n  <string name=\"skip_each_segment_once\">Ne preskoči odseka ponovno</string>\n  <string name=\"disable_ok_long_press\">Onemogočanje OK gumba z daljšim pritiskom </string>\n  <string name=\"sponsor_color_markers\">Barvne oznake v vrstici napredovanja videoposnetka</string>\n  <string name=\"refresh_section\">Osveži</string>\n  <string name=\"option_disabled\">Onemogočeno</string>\n  <string name=\"live_now_row_name\">V živo</string>\n  <string name=\"dialog_add_to\">Dodaj v %s</string>\n  <string name=\"dialog_remove_from\">Odstrani s %s</string>\n  <string name=\"added_to\">Dodano v %s</string>\n  <string name=\"removed_from\">Odstranjeno s %s</string>\n  <string name=\"video_preset_adaptive\">Prilagodljivo</string>\n  <string name=\"content_block_preview_recap\">Predogled ali povzetek videoposnetka</string>\n  <string name=\"content_block_highlight\">Bistveni del ali vrhunec videoposnetka</string>\n  <string name=\"removed_from_history\">Videoposnetek je bil odstranjen iz zgodovine</string>\n  <string name=\"remove_from_history\">Odstrani iz zgodovine ogledov</string>\n  <string name=\"upload_date\">Datum nalaganja</string>\n  <string name=\"upload_date_today\">Danes</string>\n  <string name=\"upload_date_this_week\">Ta teden</string>\n  <string name=\"upload_date_this_month\">Ta mesec</string>\n  <string name=\"upload_date_this_year\">To leto</string>\n  <string name=\"upload_date_any\">Kadarkoli</string>\n  <string name=\"upload_date_last_hour\">Zadnja ura</string>\n  <string name=\"screen_dimming_timeout_min\">%s min</string>\n  <string name=\"ui_hide_timeout_sec\">%s sek</string>\n  <string name=\"audio_shift_sec\">%s sek</string>\n  <string name=\"auto_frame_rate_sec\">%s sek</string>\n  <string name=\"volume\">Glasnost %s%%</string>\n  <string name=\"mark_all_channels_watched\">Označite vse kanale kot ogledane</string>\n  <string name=\"action_subscribe\">Naroči</string>\n  <string name=\"action_channel\">Prikaži kanal</string>\n  <string name=\"action_video_info\">Opis videoposnetka</string>\n  <string name=\"action_screen_off\">Izklopi zaslon</string>\n  <string name=\"action_playback_queue\">Čakalna vrsta</string>\n  <string name=\"action_video_speed\">Hitrost videoposnetka</string>\n  <string name=\"action_subtitles\">Podnapisi</string>\n  <string name=\"action_like\">Všeč mi je</string>\n  <string name=\"action_dislike\">Ni mi všeč</string>\n  <string name=\"action_play_pause\">Predvajaj/začasno ustavi</string>\n  <string name=\"action_repeat_mode\">Način predvajanja</string>\n  <string name=\"action_next\">Naslednji videoposnetek</string>\n  <string name=\"action_previous\">Prejšnji videoposnetek</string>\n  <string name=\"action_high_quality\">Kakovost videoposnetka</string>\n  <string name=\"content_block_no_skipping_mode\">Način brez preskakovanja</string>\n  <string name=\"content_block_action_type\">Izberi dejanje</string>\n  <string name=\"content_block_action_none\">Ne naredi ničesar</string>\n  <string name=\"content_block_action_only_skip\">Samo preskoči</string>\n  <string name=\"content_block_action_toast\">Preskoči z obvestilom</string>\n  <string name=\"content_block_action_dialog\">Prikaži okno za potrditev</string>\n  <string name=\"cancel_dialog\">Prekliči</string>\n  <string name=\"finish_on_disconnect\">Zaprite aplikacijo, ko odklopite telefon/tablico</string>\n  <string name=\"play_video\">Predvajaj</string>\n  <string name=\"rename_playlist\">Preimenuj seznam predvajanja</string>\n  <string name=\"playlist\">Seznam predvajanja</string>\n  <string name=\"time_format_24\">24 ur</string>\n  <string name=\"time_format_12\">12 ur</string>\n  <string name=\"signin_view_action_text\"></string>\n  <string name=\"action_playlist_remove\">Odstrani iz seznama predvajanja</string>\n  <string name=\"playlist_order_added_date_newer_first\">Datum dodajanja (najnovejši)</string>\n  <string name=\"playlist_order_added_date_older_first\">Datum dodajanja (najstarejši)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Datum objave (najnovejši)</string>\n  <string name=\"playlist_order_published_date_older_first\">Datum objave (najstarejši)</string>\n  <string name=\"playlist_order_popularity\">Najbolj priljubljeni</string>\n  <string name=\"playlist_order\">Razvrsti</string>\n  <string name=\"proxy_credentials_invalid\">Napačno uporabniško ime ali geslo</string>\n  <string name=\"card_text_scroll_factor\">Hitrost pomikanja besedila na karticah</string>\n  <string name=\"content_block_notify_dialog\">Okno za potrditev</string>\n  <string name=\"content_block_intro\">Uvodna špica/vmesna špica</string>\n  <string name=\"content_block_outro\"></string>\n  <string name=\"action_playlist_add\">Dodaj na seznam predvajanja</string>\n  <string name=\"keyboard_auto_show\">Samodejno prikaži tipkovnico</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-sq/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Kryefaqja</string>\n  <string name=\"title_search\">Kërko</string>\n  <string name=\"header_subscriptions\">Abonimet</string>\n  <string name=\"header_history\">Historiku</string>\n  <string name=\"header_music\">Muzikë</string>\n  <string name=\"header_news\">Lajme</string>\n  <string name=\"header_gaming\">Video lojëra</string>\n  <string name=\"header_playlists\">Listat e luajtjes</string>\n  <string name=\"header_settings\">Cilësimet</string>\n  <string name=\"header_channels\">Kanalet</string>\n  <string name=\"subscriptions_signin_title\">Shiko lajmet e fundit nga kanalet që të pëlqejnë</string>\n  <string name=\"subscriptions_signin_subtitle\">Hyr për të parë abonimet e tua</string>\n  <string name=\"subscriptions_signin_button_text\">Identifikohu</string>\n  <string name=\"library_signin_to_show_more\">Shiko videot që ke pëlqyer, ruajtur ose ku je abonuar</string>\n  <string name=\"library_signin_subtitle\">Identifikohu për të parë librarinë tënde</string>\n  <string name=\"action_signin\">Identifikohu</string>\n  <string name=\"title_video_formats\">Formatet e videos</string>\n  <string name=\"title_audio_formats\">Formatet e tingullit</string>\n  <string name=\"subtitle_category_title\">Titra</string>\n  <string name=\"playback_queue_category_title\">Radha e luajtjes</string>\n  <string name=\"video_max_quality\">Automatike (cilësia maksimale)</string>\n  <string name=\"audio_max_quality\">Automatike (cilësia maksimale)</string>\n  <string name=\"subtitles_disabled\">Titrat janë çaktivizuar</string>\n  <string name=\"auto_frame_rate\">Frekuenca pamore automatike</string>\n  <string name=\"frame_rate_correction\">Rregullo fps:\\n%s</string>\n  <string name=\"category_background_playback\">Luajtja në sfond</string>\n  <string name=\"not_implemented\">Nuk është zbatuar</string>\n  <string name=\"option_background_playback_off\">Çaktivizuar</string>\n  <string name=\"option_background_playback_pip\">Figurë në figurë</string>\n  <string name=\"option_background_playback_only_audio\">Vetëm tingulli</string>\n  <string name=\"playback_settings\">Cilësimet e cilësisë së luajtjes</string>\n  <string name=\"video_speed\">Shpejtësia e videos</string>\n  <string name=\"resolution_switch\">Ndrysho rezolucionin</string>\n  <string name=\"video_buffer\">Bufer i videos</string>\n  <string name=\"video_buffer_size_none\">Zero</string>\n  <string name=\"video_buffer_size_low\">I ulët</string>\n  <string name=\"video_buffer_size_med\">I mesëm</string>\n  <string name=\"video_buffer_size_high\">I lartë</string>\n  <string name=\"update_changelog\">Ditari i ndryshimeve</string>\n  <string name=\"install_update\">Instalo përditësimin</string>\n  <string name=\"section_is_empty\">Uf! Nuk ka asgjë këtu</string>\n  <string name=\"dialog_add_to_playlist\">Shto/Hiq nga lista e luajtjes</string>\n  <string name=\"msg_signed_users_only\">Vetëm përdoruesit e regjistruar</string>\n  <string name=\"msg_cant_load_content\">Përmbajtja nuk mund të ngarkohet.\\n1) Aktivizo historikun e shikimit.\\n2) Kontrollo datën dhe orën në pajisjen tënde.\\n3) Kontrollo lidhjen e rrjetit.\\n4) Kontrollo cilësimet e ndërmjetësit.\\n5) Provo të identifikohesh te llogaria jote.\\n6) Ndoshta nuk ka asgjë këtu.</string>\n  <string name=\"title_video_presets\">Paracaktimet e videos</string>\n  <string name=\"settings_accounts\">Llogaritë</string>\n  <string name=\"settings_left_panel\">Redakto kategoritë</string>\n  <string name=\"settings_themes\">Temat</string>\n  <string name=\"settings_other\">Të tjera</string>\n  <string name=\"settings_player\">Luajtësi</string>\n  <string name=\"settings_language\">Gjuha</string>\n  <string name=\"settings_linked_devices\">Pajisjet e lidhura</string>\n  <string name=\"settings_about\">Rreth</string>\n  <string name=\"dialog_account_list\">Zgjidh llogarinë</string>\n  <string name=\"dialog_account_none\">Asnjë</string>\n  <string name=\"dialog_remove_account\">Dil</string>\n  <string name=\"dialog_add_account\">Identifikohu</string>\n  <string name=\"default_lang\">E paracaktuar</string>\n  <string name=\"dialog_select_language\">Gjuha</string>\n  <string name=\"subtitle_default\">Të paracaktuar</string>\n  <string name=\"subtitle_white_semi_transparent\">Të bardha me sfond gjysmë të tejdukshëm</string>\n  <string name=\"subtitle_style\">Stili i titrave</string>\n  <string name=\"subtitle_language\">Gjuha e titrave</string>\n  <string name=\"action_search\">Kërko</string>\n  <string name=\"settings_main_ui\">Ndërfaqja e përdoruesit</string>\n  <string name=\"dialog_main_ui\">Ndërfaqja e përdoruesit</string>\n  <string name=\"card_animated_previews\">Pamje paraprake të animuara</string>\n  <string name=\"web_site\">Faqja e internetit</string>\n  <string name=\"donation\">Donacion</string>\n  <string name=\"dialog_about\">Rreth</string>\n  <string name=\"dialog_player_ui\">Luajtësi i videos</string>\n  <string name=\"player_show_ui_on_pause\">Shfaq ndërfaqen kur pezullohet luajtja</string>\n  <string name=\"player_pause_on_ok\">Butoni OK ndalon luajtjen</string>\n  <string name=\"player_ok_button_behavior\">Sjellja e butonit OK</string>\n  <string name=\"player_only_ui\">Vetëm ndërfaqja</string>\n  <string name=\"player_ui_and_pause\">Ndërfaqja e përdoruesit dhe pezullimi</string>\n  <string name=\"player_only_pause\">Thjesht pezullo</string>\n  <string name=\"check_for_updates\">Kontrollo përditësimet</string>\n  <string name=\"update_not_found\">Po përdor versionin më të fundit</string>\n  <string name=\"update_in_progress\">Prit…</string>\n  <string name=\"player_ui_hide_behavior\">Fshih automatikisht ndërfaqen e përdoruesit</string>\n  <string name=\"option_never\">Kurrë</string>\n  <string name=\"side_panel_sections\">Redakto kategoritë</string>\n  <string name=\"boot_to_section\">Në nisje hyr në kategorinë</string>\n  <string name=\"large_ui\">Ndërfaqe e madhe e përdoruesit</string>\n  <string name=\"video_grid_scale\">Madhësia e rrjetës së videos</string>\n  <string name=\"scale_ui\">Madhësia e ndërfaqes së përdoruesit</string>\n  <string name=\"color_scheme\">Skema e ngjyrave</string>\n  <string name=\"color_scheme_default\">E paracaktuar</string>\n  <string name=\"color_scheme_red_grey\">E kuqe dhe gri</string>\n  <string name=\"color_scheme_red\">E kuqe</string>\n  <string name=\"color_scheme_dark_grey\">Gri e errët</string>\n  <string name=\"disable_update_check\">Çaktivizo kontrollin e përditësimit</string>\n  <string name=\"show_again\">Shfaq përsëri</string>\n  <string name=\"check_updates_auto\">Njofto për përditësimet</string>\n  <string name=\"select_account_on_boot\">Shfaq përzgjedhjen e llogarisë në nisje</string>\n  <string name=\"player_other\">Të ndryshme</string>\n  <string name=\"player_full_date\">Data e saktë në përshkrim</string>\n  <string name=\"open_channel\">Hap kanalin</string>\n  <string name=\"open_playlist\">Hap listën e luajtjes</string>\n  <string name=\"not_interested\">Nuk më intereson</string>\n  <string name=\"not_recommend_channel\">Mos e këshillo kanalin</string>\n  <string name=\"you_wont_see_this_video\">Videoja është hequr nga këshillimet</string>\n  <string name=\"you_wont_see_this_channel\">Nuk do ta shohësh këtë kanal në këshillimet</string>\n  <string name=\"settings_search\">Kërkimi</string>\n  <string name=\"dialog_search\">Kërko</string>\n  <string name=\"instant_voice_search\">Kërkim i menjëhershëm me zë</string>\n  <string name=\"option_background_playback_behind\">Luaj në sfond</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">Informacioni për videon tjetër nuk është ngarkuar ende</string>\n  <string name=\"card_multiline_title\">Tituj me shumë rreshta</string>\n  <string name=\"cards_style\">Stili i skedave</string>\n  <string name=\"repeat_mode_all\">Luaj video vazhdimisht</string>\n  <string name=\"repeat_mode_one\">Përsërit videon aktuale</string>\n  <string name=\"repeat_mode_pause\">Ndalo luajtjen pas çdo videoje</string>\n  <string name=\"repeat_mode_pause_alt\">Luaj vazhdimisht vetëm videot në listën për luajtje</string>\n  <string name=\"repeat_mode_none\">Ndalo luajtjen pas një videoje</string>\n  <string name=\"subscribed_to_channel\">I abonuar</string>\n  <string name=\"unsubscribed_from_channel\">I paabonuar</string>\n  <string name=\"subtitle_yellow_transparent\">Të verdhë në sfond të tejdukshëm</string>\n  <string name=\"subtitle_white_transparent\">Të bardhë në sfond të tejdukshëm</string>\n  <string name=\"subtitle_white_black\">Të bardhë në sfond të zi</string>\n  <string name=\"player_seek_preview\">Shiko pamjet paraprake gjatë kërkimit</string>\n  <string name=\"color_scheme_dark_grey_oled\">Gri e errët (OLED)</string>\n  <string name=\"channels_section_sorting\">Renditja e kategorive të kanaleve</string>\n  <string name=\"sorting_by_new_content\">Përmbajtje e re</string>\n  <string name=\"sorting_alphabetically\">Sipas rendit alfabetik</string>\n  <string name=\"sorting_last_viewed\">Parë për herë të fundit</string>\n  <string name=\"player_pause_when_seek\">Pezullo gjatë kërkimit</string>\n  <string name=\"playlists_style\">Stili i kategorisë të listës së luajtjes</string>\n  <string name=\"playlists_style_grid\">Rrjetë</string>\n  <string name=\"playlists_style_rows\">Rreshtat</string>\n  <string name=\"player_show_clock\">Shfaq orën në shiritin e kontrollit</string>\n  <string name=\"player_show_remaining_time\">Shfaq kohën e mbetur në shiritin e kontrollit</string>\n  <string name=\"player_show_ending_time\">Shfaq orën e përfundimit në shiritin e kontrollit</string>\n  <string name=\"open_channel_uploads\">Hap ngarkimet e kanalit</string>\n  <string name=\"app_exit_shortcut\">Dil nga aplikacioni</string>\n  <string name=\"player_exit_shortcut\">Dil nga luajtësi</string>\n  <string name=\"app_exit_none\">Mos dil</string>\n  <string name=\"app_double_back_exit\">Dy herë butoni prapa</string>\n  <string name=\"app_single_back_exit\">Një herë butoni prapa</string>\n  <string name=\"action_video_zoom\">Zmadho videon</string>\n  <string name=\"video_zoom\">Zmadhimi i videos</string>\n  <string name=\"video_zoom_default\">I paracaktuar</string>\n  <string name=\"video_zoom_fit_width\">Përshtat në gjerësi</string>\n  <string name=\"video_zoom_fit_height\">Përshtat në lartësi</string>\n  <string name=\"video_zoom_fit_both\">Përshtat si për gjerësi ashtu edhe për lartësi</string>\n  <string name=\"video_zoom_stretch\">Shtri</string>\n  <string name=\"color_scheme_teal\">Uji i gjelbër</string>\n  <string name=\"color_scheme_teal_oled\">Uji i gjelbër (OLED)</string>\n  <string name=\"player_seek_preview_none\">Çaktivizuar</string>\n  <string name=\"player_seek_preview_single\">Fotografi e vetme</string>\n  <string name=\"player_seek_preview_carousel\">Shfaqja e pamjes rrëshqitëse</string>\n  <string name=\"player_seek_preview_carousel_slow\">Shfaqja e pamjes rrëshqitëse (e ngadalshme, me fotografi kyçe)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Shfaqja e pamjes rrëshqitëse (e shpejtë, pamjet paraprake jo të sakta)</string>\n  <string name=\"unsubscribe_from_channel\">Hiq abonimin nga kanali</string>\n  <string name=\"card_title_lines_num\">Numri i rreshtave për titullin e skedave</string>\n  <string name=\"card_auto_scrolled_title\">Lëvizje automatike e titullit të shkurtuar</string>\n  <string name=\"share_link\">Shpërndaj lidhjen</string>\n  <string name=\"share_qr_link\">Shpërndaj lidhjen (kod QR)</string>\n  <string name=\"subscribe_to_channel\">Abonohu në kanal</string>\n  <string name=\"wait_data_loading\">Prit që të dhënat të ngarkohen…</string>\n  <string name=\"auto_frame_rate_pause\">Frekuenca pamore automatike (pezullo gjatë ndryshimit)</string>\n  <string name=\"auto_frame_rate_applying\">Apliko frekuencën pamore automatike %sx%s\\@%s</string>\n  <string name=\"audio_shift\">Modifiko tingullin</string>\n  <string name=\"player_remember_speed\">Mbaj mend shpejtësinë</string>\n  <string name=\"mark_channel_as_watched\">Shëno si të shikuar</string>\n  <string name=\"channel_marked_as_watched\">Kanali është shënuar si i shikuar</string>\n  <string name=\"dialog_add_device\">Shto pajisje</string>\n  <string name=\"dialog_remove_all_devices\">Hiq të gjitha pajisjet</string>\n  <string name=\"device_link_enabled\">Lidhja u aktivizua</string>\n  <string name=\"device_connected\">Pajisja \\\"%s\\\" është lidhur</string>\n  <string name=\"device_disconnected\">Pajisja \\\"%s\\\" është shkëputur</string>\n  <string name=\"settings_ui_scale\">Madhësia e ndërfaqes së përdoruesit</string>\n  <string name=\"msg_restart_app\">Të lutem, rinis aplikacionin për të zbatuar këto cilësime</string>\n  <string name=\"settings_block\">Bllokim i përmbajtjes</string>\n  <string name=\"msg_applying\">Po zbatohet %s…</string>\n  <string name=\"msg_done\">U krye</string>\n  <string name=\"settings_general\">Të përgjithshme</string>\n  <string name=\"settings_video\">Video</string>\n  <string name=\"content_block_confirm_skip\">Prano në kapërcim</string>\n  <string name=\"content_block_categories\">Kapërce pjesët</string>\n  <string name=\"content_block_sponsor\">Sponsorët</string>\n  <string name=\"content_block_intro\">Animacion ndërprerës/hyrës</string>\n  <string name=\"content_block_outro\">Mirënjohjet në fund</string>\n  <string name=\"content_block_interaction\">Përkujtesë për ndërveprim (për abonim)</string>\n  <string name=\"content_block_self_promo\">Vetëreklamim i papaguar</string>\n  <string name=\"content_block_music_off_topic\">Pjesë jo muzikore e videos</string>\n  <string name=\"confirm_segment_skip\">Të kapërcehet pjesa \\\"%s\\\"\\?</string>\n  <string name=\"cancel_segment_skip\">Zhbëj kapërcimin e pjesës</string>\n  <string name=\"msg_skipping_segment\">Po kapërcehet pjesa \\\"%s\\\"...</string>\n  <string name=\"content_block_notification_type\">Lloji i njoftimit</string>\n  <string name=\"content_block_notify_none\">Pa njoftim</string>\n  <string name=\"content_block_notify_toast\">Toast</string>\n  <string name=\"content_block_notify_dialog\">Dialog për pranim</string>\n  <string name=\"return_to_launcher\">Kthehu te lëshuesi nga kanalet/kërkimi ATV</string>\n  <string name=\"intent_force_close\">Dil detyrimisht nëse videoja është hapur nga një burim i jashtëm</string>\n  <string name=\"btn_confirm\">Prano</string>\n  <string name=\"player_low_video_quality\">Cilësi e ulët e videos</string>\n  <string name=\"remote_session_closed\">Sesioni në largësi është mbyllur</string>\n  <string name=\"msg_mode_switch_error\">Nuk mund të kalojë në mënyrën e pamjes \\\"%s\\\"</string>\n  <string name=\"msg_player_error\">Ndodhi një gabim i luajtësit %s. Po niset përsëri luajtja…</string>\n  <string name=\"video_preset_disabled\">Pa paracaktim</string>\n  <string name=\"video_preset_enabled\">Formati i videos së ardhshme do të vendoset sipas paracaktimit</string>\n  <string name=\"player_sleep_timer\">Kohëmatës për gjumë</string>\n  <string name=\"player_show_quality_info\">Shfaq informacionin e cilësisë në shiritin e kontrollit</string>\n  <string name=\"player_remember_each_speed\">Mbaj mend shpejtësinë e çdo videoje</string>\n  <string name=\"player_remember_speed_none\">Asnje</string>\n  <string name=\"player_remember_speed_all\">E njëjta në të gjitha videot</string>\n  <string name=\"player_remember_speed_each\">Për çdo video</string>\n  <string name=\"msg_player_error_source\">Videoja nuk mund të shkarkohet</string>\n  <string name=\"msg_player_error_renderer\">Formati i zgjedhur nuk mbështetet nga programi.\\nProvo të zgjidhësh një format tjetër.\\nNëse kjo nuk punon, provo të nisësh përsëri pajisjen.</string>\n  <string name=\"msg_player_error_video_renderer\">Formati i VIDEOS i zgjedhur nuk mbështetet nga programi.\\nProvo të zgjidhësh një tjetër duke shtypur butonin HQ në luajtësin.\\nNëse kjo nuk punon, provo të nisësh përsëri pajisjen.</string>\n  <string name=\"msg_player_error_audio_renderer\">Formati i TINGULLIT i zgjedhur nuk mbështetet nga programi.\\nProvo të zgjidhësh një tjetër duke shtypur butonin HQ në luajtësin.\\nNëse kjo nuk punon, provo të nisësh përsëri pajisjen.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">Formati i zgjedhur i TITRAVE nuk mbështetet nga programi.\\nProvo të zgjidhësh një tjetër duke mbajtur shtypur butonin CC në luajtësin.\\nNëse kjo nuk punon, provo të nisësh përsëri pajisjen.</string>\n  <string name=\"msg_player_error_unexpected\">Gabim i panjohur i deshifruesit të videos</string>\n  <string name=\"video_aspect\">Raporti i videos</string>\n  <string name=\"player_tweaks\">Rregullimet për zhvilluesit</string>\n  <string name=\"header_uploads\">Ngarkimet</string>\n  <string name=\"player_show_global_clock\">Shfaq gjithmonë orën në ekran</string>\n  <string name=\"player_show_global_ending_time\">Shfaq gjithmonë kohën e përfundimit në ekran</string>\n  <string name=\"app_corner_clock\">Kryefaqja: Ora në këndin e sipërm djathtas</string>\n  <string name=\"player_corner_clock\">Luajtësi: Ora në këndin e sipërm djathtas</string>\n  <string name=\"player_corner_ending_time\">Luajtësi: Koha e përfundimit në këndin e sipërm djathtas</string>\n  <string name=\"uploads_old_look\">Rivendos pamjen e vjetër të seksionit Ngarkime</string>\n  <string name=\"background_playback_activation\">Luajtja në sfond (aktivizimi)</string>\n  <string name=\"channels_old_look\">Rivendos pamjen e vjetër të seksionit Kanale</string>\n  <string name=\"channels_auto_load\">Ngarko automatikisht përmbajtjen e seksionit Kanale</string>\n  <string name=\"return_to_background_video\">Kthehu te videoja që po luhet në sfond</string>\n  <string name=\"pin_unpin_from_sidebar\">Shto/Hiq nga shiriti anësor</string>\n  <string name=\"pin_unpin_playlist\">Shto/Hiq listën e luajtjes nga shiriti anësor</string>\n  <string name=\"pin_unpin_channel\">Shto/Hiq kanalin nga shiriti anësor</string>\n  <string name=\"pin_playlist\">Shto listën e luajtjes në shiritin anësor</string>\n  <string name=\"pin_channel\">Shto kanalin në shiritin anësor</string>\n  <string name=\"pinned_to_sidebar\">Shtuar në shiritin anësor</string>\n  <string name=\"unpin_from_sidebar\">Hiq nga shiriti anësor</string>\n  <string name=\"unpinned_from_sidebar\">U hoq nga shiriti anësor</string>\n  <string name=\"dialog_select_country\">Shteti</string>\n  <string name=\"settings_language_country\">Gjuha/Shteti</string>\n  <string name=\"share_embed_link\">Shpërndaj lidhjen e përfshirë</string>\n  <string name=\"card_text_scroll_factor\">Shpejtësia e lëvizjes së tekstit të skedave</string>\n  <string name=\"preferred_update_source\">Zgjidh burimin e përditësimit</string>\n  <string name=\"hide_shorts\">Fshih videot e shkurtra nga Abonimet</string>\n  <string name=\"hide_shorts_channel\">Fshih videot e shkurtra nga kanali</string>\n  <string name=\"key_remapping\">Ndryshimi i butonëve</string>\n  <string name=\"screen_dimming\">Zbehja e ekranit</string>\n  <string name=\"removed_from_playback_queue\">U hoq nga radha e luajtjes</string>\n  <string name=\"added_to_playback_queue\">Shtuar në radhën e luajtjes</string>\n  <string name=\"add_remove_from_playback_queue\">Shto/Hiq nga radha e luajtjes</string>\n  <string name=\"add_to_playback_queue\">Shto në radhën e luajtjes</string>\n  <string name=\"remove_from_playback_queue\">Hiqe nga radha e luajtjes</string>\n  <string name=\"proxy_enabled\">Ndërmjetësi u aktivizua</string>\n  <string name=\"proxy_disabled\">Ndërmjetësi u çaktivizua</string>\n  <string name=\"uploads_row_name\">Ngarkimet</string>\n  <string name=\"playlists_row_name\">Listat e luajtjes të krijuara</string>\n  <string name=\"popular_uploads_row_name\">Ngarkimet më të shikuara</string>\n  <string name=\"news_row_name\">Lajme</string>\n  <string name=\"breaking_news_row_name\">Lajmet e fundit</string>\n  <string name=\"covid_news_row_name\">Lajme rreth COVID-19</string>\n  <string name=\"enable_voice_search\">Aktivizo kërkimin me zë (kërkohet mbështetja e firmuerit)</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Abonohu/Anulo abonimin në kanal</string>\n  <string name=\"app_backup_restore\">Bej/Rikthe një kopje rezerve</string>\n  <string name=\"app_backup\">Bëj një kopje rezervë të të dhënave të aplikacionit</string>\n  <string name=\"app_restore\">Rikthe të dhënat e aplikacionit</string>\n  <string name=\"skip_each_segment_once\">Mos i kapërce përsëri pjesët</string>\n  <string name=\"disable_ok_long_press\">Çaktivizo shtypjen e gjatë të butonit OK</string>\n  <string name=\"disable_ok_long_press_desc\">I menduar për telekomandat me defekt ku butoni OK nuk punon siç duhet</string>\n  <string name=\"player_time_correction\">Shfaq kohëzgjatjen bazuar në shpejtësinë</string>\n  <string name=\"sponsor_color_markers\">Shënjuesit me ngjyra në shiritin e përparimit</string>\n  <string name=\"refresh_section\">Përditëso seksionin</string>\n  <string name=\"option_disabled\">Çaktivizuar</string>\n  <string name=\"live_now_row_name\">Drejtpërdrejtë tani</string>\n  <string name=\"run_in_background\">Luajtja në sfond</string>\n  <string name=\"settings_remote_control\">Kontrolli në largësi</string>\n  <string name=\"background_service_started\">Shërbimi në sfond u nis</string>\n  <string name=\"dialog_add_to\">Shto në %s</string>\n  <string name=\"dialog_remove_from\">Hiq nga %s</string>\n  <string name=\"added_to\">Shtuar në %s</string>\n  <string name=\"removed_from\">U hoq nga %s</string>\n  <string name=\"video_preset_adaptive\">Përshtatshëm</string>\n  <string name=\"content_block_preview_recap\">Pamje paraprake ose përmbledhje e videos</string>\n  <string name=\"content_block_highlight\">Pika interesante (shënjues)</string>\n  <string name=\"removed_from_history\">Videoja u hoq nga Historiku</string>\n  <string name=\"remove_from_history\">Hiqe nga historiku</string>\n  <string name=\"upload_date\">Data e ngarkimit</string>\n  <string name=\"upload_date_any\">Çdo datë</string>\n  <string name=\"upload_date_today\">Sot</string>\n  <string name=\"upload_date_this_week\">Këtë javë</string>\n  <string name=\"upload_date_this_month\">Këtë muaj</string>\n  <string name=\"upload_date_this_year\">Këtë vit</string>\n  <string name=\"double_refresh_rate\">Frekuenca e rifreskimit e dyfishuar</string>\n  <string name=\"upload_date_last_hour\">Orën e fundit</string>\n  <string name=\"focus_on_search_results\">Përqendrohu automatikisht në rezultatet e kërkimit</string>\n  <string name=\"context_menu\">Menyja e kontekstit</string>\n  <string name=\"add_remove_from_recent_playlist\">Shto/Hiq nga lista e fundit e luajtjes</string>\n  <string name=\"hide_settings_section\">Fshih seksionin e cilësimeve (e rrezikshme!)</string>\n  <string name=\"volume\">Vëllimi i zërit %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s sek</string>\n  <string name=\"audio_shift_sec\">%s sek</string>\n  <string name=\"ui_hide_timeout_sec\">%s sek</string>\n  <string name=\"screen_dimming_timeout_min\">%s min</string>\n  <string name=\"mark_all_channels_watched\">Shëno të gjitha kanalet si të shikuara</string>\n  <string name=\"move_section_up\">Zhvendos seksionin lart</string>\n  <string name=\"move_section_down\">Zhvendos seksionin poshtë</string>\n  <string name=\"player_buttons\">Konfiguro butonët e luajtësit</string>\n  <string name=\"action_playlist_add\">Shto në listën e luajtjes</string>\n  <string name=\"action_video_stats\">Statistikat e videos</string>\n  <string name=\"action_subscribe\">Abonohu</string>\n  <string name=\"action_channel\">Hap kanalin</string>\n  <string name=\"action_pip\">FnëF</string>\n  <string name=\"action_video_info\">Përshkrimi i videos</string>\n  <string name=\"action_screen_off\">Fik ekranin</string>\n  <string name=\"action_sound_off\">Hesht zërin</string>\n  <string name=\"action_playback_queue\">Radha e luajtjes</string>\n  <string name=\"action_video_speed\">Shpejtësia e videos</string>\n  <string name=\"action_subtitles\">Titrat</string>\n  <string name=\"action_like\">Më pëlqen</string>\n  <string name=\"action_dislike\">Nuk më pëlqen</string>\n  <string name=\"action_play_pause\">Luaj/Pezullo</string>\n  <string name=\"action_repeat_mode\">Mënyra e luajtjes</string>\n  <string name=\"action_next\">Videoja e radhës</string>\n  <string name=\"action_previous\">Videoja e mëparshme</string>\n  <string name=\"action_high_quality\">Cilësia e videos</string>\n  <string name=\"content_block_no_skipping_mode\">Mënyra pa kapërcim</string>\n  <string name=\"content_block_action_type\">Zgjidh veprim</string>\n  <string name=\"content_block_action_none\">Mos bëj gjë</string>\n  <string name=\"content_block_action_only_skip\">Vetëm kapërce</string>\n  <string name=\"content_block_action_toast\">Kapërce me njoftim</string>\n  <string name=\"content_block_action_dialog\">Shfaq dialog për pranim</string>\n  <string name=\"content_block_filler\">Jashtë temës (mbushës)</string>\n  <string name=\"keyboard_auto_show\">Shfaq automatikisht tastierën</string>\n  <string name=\"cancel_dialog\">Anulo</string>\n  <string name=\"msg_player_error_source2\">URL-ja nuk punon ose koha e pajisjes është e pasaktë.\\nZakonisht, është një defekt i aplikacionit.</string>\n  <string name=\"msg_player_error_video_source\">URL-ja e VIDEOS nuk punon ose koha e pajisjes është e pasaktë.\\nZakonisht, është një defekt i aplikacionit.</string>\n  <string name=\"msg_player_error_audio_source\">URL-ja e TINGULLIT nuk punon ose koha e pajisjes është e pasaktë.\\nZakonisht, është një defekt i aplikacionit.</string>\n  <string name=\"msg_player_error_subtitle_source\">URL-ja e TITRAVE nuk funksionon ose koha e pajisjes është e pasaktë.\\nZakonisht, është një defekt i aplikacionit.</string>\n  <string name=\"hide_upcoming\">Fshih të ardhshmet nga Abonimet</string>\n  <string name=\"hide_upcoming_channel\">Fshih të ardhshmet nga Kanali</string>\n  <string name=\"hide_upcoming_home\">Fshih të ardhshmet nga Kryefaqja</string>\n  <string name=\"search_background_playback\">Luajtja në sfond gjatë kërkimit/shfletimit të një kanali</string>\n  <string name=\"trending_row_name\">Më të shikuarat</string>\n  <string name=\"player_seek_type\">Lloji i kërkimit</string>\n  <string name=\"player_seek_regular\">E zakonshme</string>\n  <string name=\"player_seek_confirmation_pause\">Me pranim (pezullo gjatë kërkimit)</string>\n  <string name=\"player_seek_confirmation_play\">Me pranim (luaj gjatë kërkimit)</string>\n  <string name=\"hide_shorts_from_home\">Fshih videot e shkurtra nga Kryefaqja</string>\n  <string name=\"finish_on_disconnect\">Mbyll aplikacionin pasi të kesh shkëputur telefonin tënd</string>\n  <string name=\"update_error\">Gabim përditësimi</string>\n  <string name=\"hide_shorts_from_history\">Fshih videot e shkurtra nga Historiku</string>\n  <string name=\"hide_shorts_from_trending\">Fshih videot e shkurtra nga Më të shikuarat</string>\n  <string name=\"disable_screensaver\">Çaktivizo mbrojtësin e ekranit</string>\n  <string name=\"description_not_found\">Përshkrimi nuk u gjet</string>\n  <string name=\"proxy_port_hint\">Porta e ndërmjetësit</string>\n  <string name=\"proxy_host_hint\">Emri i pritësit të ndërmjetësit ose adresa IP</string>\n  <string name=\"proxy_username_hint\">Emri i përdoruesit të ndërmjetësit</string>\n  <string name=\"proxy_password_hint\">Fjalëkalimi i ndërmjetësit</string>\n  <string name=\"proxy_type\">Lloji i ndërmjetësit</string>\n  <string name=\"enable_web_proxy\">Përdor Ndërmjetës të Rrjetit</string>\n  <string name=\"proxy_not_supported\">Përdor Ndërmjetës të Rrjetit (kërkohet Android 4.4+)</string>\n  <string name=\"proxy_test_btn\">Provë</string>\n  <string name=\"proxy_settings_title\">Cilësimet e Shërbyesit të Ndërmjetësit</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">Prova #%d: anuluar.</string>\n  <string name=\"proxy_type_invalid\">Lloji i ndërmjetësit nuk është caktuar.</string>\n  <string name=\"proxy_host_invalid\">Pritësi i ndërmjetësit nuk është caktuar.</string>\n  <string name=\"proxy_port_invalid\">Porta e ndërmjetësit e pavlefshme, duhet të jetë më e madhe se 0.</string>\n  <string name=\"proxy_credentials_invalid\">Emri i përdoruesit ose fjalëkalimi i pavlefshëm.</string>\n  <string name=\"proxy_test_aborted\">Prova u ndërpre, të lutem së pari rregullo cilësimet e ndërmjetësit.</string>\n  <string name=\"proxy_application_aborted\">Të lutem, së pari rregullo cilësimet e ndërmjetësit tënd.</string>\n  <string name=\"proxy_test_start\">Prova #%d: %s …</string>\n  <string name=\"proxy_test_error\">Gabimi #%d: %s</string>\n  <string name=\"proxy_test_status\">Gjendja #%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Adresa e skedarit të konfigurimit(*.ovpn)</string>\n  <string name=\"enable_openvpn\">Përdor OpenVPN</string>\n  <string name=\"openvpn_settings_title\">Cilësimet e OpenVPN</string>\n  <string name=\"openvpn_application_aborted\">Të lutem, së pari rregullo cilësimet e OpenVPN.</string>\n  <string name=\"openvpn_test_aborted\">Prova u ndërpre, të lutem së pari rregullo cilësimet e OpenVPN.</string>\n  <string name=\"openvpn_address_invalid\">Adresa e konfigurimit të OpenVPN nuk është caktuar.</string>\n  <string name=\"internet_censorship\">Censura në Internet</string>\n  <string name=\"rename_section\">Riemërto këtë seksion</string>\n  <string name=\"simple_edit_value_hint\">Fut vlerën</string>\n  <string name=\"seek_interval\">Intervali i kërkimi</string>\n  <string name=\"seek_interval_sec\">%s sek</string>\n  <string name=\"subtitle_system\">Stili i sistemit (Cilësimet e Android &gt; Aksesueshmëria)</string>\n  <string name=\"subtitle_scale\">Madhësia e titrave</string>\n  <string name=\"audio_sync_fix_desc\">Një mënyrë tjetër për të sinkronizuar tingullin/videon. Ka dalë nga përdorimi, por, në disa raste, mund të ndihmojë.</string>\n  <string name=\"audio_sync_fix\">Rregullo sinkronizimin e tingullit</string>\n  <string name=\"ambilight_ratio_fix_desc\">Rregullon ndriçimin \\\"absent bias\\\". Rregullon raportin e gabuar të pamjes ose madhësinë e gabuar të videos. Rregullon pamjet bosh të ekranit. Kjo ndikon në performancë!</string>\n  <string name=\"ambilight_ratio_fix\">Rregullo Ndriçimin e Ambientit/Raportin/Madhësinë e videos/Pamjet e ekranit</string>\n  <string name=\"force_legacy_codecs_desc\">Përmirëson ndjeshëm performancën në pajisjet e nivelit të ulët. Rezolucioni maksimal është 720p.</string>\n  <string name=\"force_legacy_codecs\">Detyro përdorimin e kodekëve të vjetër (720p)</string>\n  <string name=\"live_stream_fix_desc\">Kujdes, ky ndryshim çaktivizon kthimin prapa e transmetimeve. Përmirëson ndjeshëm performancën e transmetimit të drejtpërdrejtë në pajisjet e nivelit të ulët. Rezolucioni maksimal është 1080p.</string>\n  <string name=\"live_stream_fix_4k_desc\">Kujdes, ky ndryshim çaktivizon kthimin prapa e transmetimeve. Përmirëson ndjeshëm performancën e transmetimit të drejtpërdrejtë. Rezolucioni maksimal është 4K.</string>\n  <string name=\"live_stream_fix\">Rregullo transmetimin e drejtpërdrejtë (1080p)</string>\n  <string name=\"live_stream_fix_4k\">Rregullo transmetimin e drejtpërdrejtë (4K)</string>\n  <string name=\"playback_notifications_fix_desc\">Fsheh njoftimet për ndryshimin e asaj që po luhet. Mund të jetë i dobishëm në firmuerët që mbështeten mbi AOSP.</string>\n  <string name=\"playback_notifications_fix\">Çaktivizo njoftimet e luajtjes</string>\n  <string name=\"amlogic_fix_desc\">Rregullo uljen e frekuencës në pajisjet që mbështeten mbi Amlogic.</string>\n  <string name=\"amlogic_fix\">Rregullo Amlogic 1080p\\@60fps</string>\n  <string name=\"tunneled_video_playback_desc\">SHËNIM: pezullimi mund të mos punoi siç duhet! Luajtja e videos në tunel premton përfitime si sinkronizimi më i mirë tingull/video (sinkronizimi T/V) dhe luajtje më e qetë. Kërkohet Android 5+</string>\n  <string name=\"tunneled_video_playback\">Luajtja e videos në tunel (Android 5+)</string>\n  <string name=\"master_volume\">Vëllimi kryesor i zërit</string>\n  <string name=\"volume_limit\">Kufiri i vëllimit të zërit</string>\n  <string name=\"player_volume\">Vëllimi i zërit</string>\n  <string name=\"play_video\">Luaj</string>\n  <string name=\"remember_position_of_short_videos\">Mbaj mend pozicionin e luajtjes në videot e shkurtra (më pak se 5 minuta)</string>\n  <string name=\"remember_position_of_live_videos\">Mos harro pozicionin e luajtjes e transmetimit të drejtpërdrejtë</string>\n  <string name=\"player_show_tooltips\">Shfaq përshkrimet e butonëve</string>\n  <string name=\"action_like_unset\">pëlqimi u hoq</string>\n  <string name=\"action_dislike_unset\">mospëlqimi u hoq</string>\n  <string name=\"various_buttons\">Butonët në krye të dritares kryesore</string>\n  <string name=\"not_compatible_with\">Rregullimi i përzgjedhur nuk përputhet me</string>\n  <string name=\"subtitle_position\">Zhvendosja e titrave më poshtë</string>\n  <string name=\"pressing_home\">duke shtypur KRYEFAQE</string>\n  <string name=\"pressing_home_back\">duke shtypur KRYEFAQE ose PRAPA</string>\n  <string name=\"pressing_back\">duke shtypur PRAPA</string>\n  <string name=\"player_number_key_seek\">Kërko me butonët numerikë</string>\n  <string name=\"save_remove_playlist\">Shto/Hiq listën e luajtjes nga seksioni Listat e luajtjes</string>\n  <string name=\"save_playlist\">Shto listën e luajtjes në seksionin e Listat e luajtjes</string>\n  <string name=\"remove_playlist\">Hiq përgjithmonë listën e luajtjes nga seksioni Listat e luajtjes</string>\n  <string name=\"removed_from_playlists\">U hoq nga seksioni Listat e luajtjes</string>\n  <string name=\"saved_to_playlists\">U shtua në seksionin Listat e luajtjes</string>\n  <string name=\"create_playlist\">Krijo listë për luajtje</string>\n  <string name=\"add_video_to_new_playlist\">Shto në listën e re të luajtjes</string>\n  <string name=\"cant_delete_empty_playlist\">Nuk mund të fshihet një listë e luajtjes bosh</string>\n  <string name=\"playlist\">Listë e luajtjes</string>\n  <string name=\"rename_playlist\">Riemërto listën e luajtjes</string>\n  <string name=\"cant_rename_empty_playlist\">Nuk mund të riemëroj një listë e luajtjes bosh</string>\n  <string name=\"cant_rename_foreign_playlist\">Nuk mund të riemërtohet një listë e luajtjes e jashtme</string>\n  <string name=\"cant_save_playlist\">Kjo listë e luajtjes nuk mund të shtohet</string>\n  <string name=\"enter_value\">Fut një vlerë që mos të jetë bosh</string>\n  <string name=\"dialog_add_remove_from\">Shto/Hiq nga %s</string>\n  <string name=\"lb_playback_controls_skip_next\">Kalo te tjetri</string>\n  <string name=\"lb_playback_controls_skip_previous\">Kalo te i mëparshmi/Kthehu në pozicionin fillestar</string>\n  <string name=\"alt_presets_behavior\">Sjellja e paracaktimeve alternative (kufizon gjerësinë e brezit)</string>\n  <string name=\"alt_presets_behavior_desc\">Aplikacioni do të përpiqet të mbajë gjerësinë e brezit të përputhur me paracaktimin e zgjedhur në vend që të përputhet midis rezolucionit, fps dhe kodekut.</string>\n  <string name=\"sleep_timer\">Kohëmatësi për gjumë (nëse telekomanda nuk përdoret për një orë)</string>\n  <string name=\"sleep_timer_desc\">Ndalo luajtjen nëse përdoruesi nuk e ka përdorur telekomandën për një orë</string>\n  <string name=\"disable_vsync\">Çaktivizo ndryshimin në sinkroninë vertikale</string>\n  <string name=\"disable_vsync_desc\">Ky rregullim çaktivizon përafrimin e fotografive me sinjalin e sinkronizimit vertikal të ekranit. Kjo mund të përmirësojë performancën në pajisjet e nivelit të ulët duke liruar burimet e CPU.</string>\n  <string name=\"skip_codec_profile_check\">Kapërce kontrollin e nivelit të profilit të kodekut</string>\n  <string name=\"skip_codec_profile_check_desc\">Mos e kontrollo mbështetjen e kodekëve kur fillon një video. Mund të jetë i dobishëm për firmuerët me defekt.</string>\n  <string name=\"force_sw_codec\">Detyro përdorimin e deshifruesit video SW</string>\n  <string name=\"force_sw_codec_desc\">Mund të luaj pothuajse çdo video, por performanca është shumë e dobët.</string>\n  <string name=\"playlist_order\">Rendit listën e luajtjes</string>\n  <string name=\"playlist_order_added_date_newer_first\">Data e shtimit (më e reja në fillim)</string>\n  <string name=\"playlist_order_added_date_older_first\">Data e shtimit (më e vjetra në fillim)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Data e publikimit (më e reja në fillim)</string>\n  <string name=\"playlist_order_published_date_older_first\">Data e publikimit (më e vjetra në fillim)</string>\n  <string name=\"playlist_order_popularity\">Popullariteti</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">Nuk mund ta bëj këtë për listat e luajtjes të jashtme</string>\n  <string name=\"owned_playlist_warning\">Gabim: Ky veprim mund të zbatohet vetëm për listat e luajtjes që zotëron.</string>\n  <string name=\"content_block_alt_server\">Përdor shërbyes tjetër</string>\n  <string name=\"content_block_alt_server_desc\">Aktivizo këtë rregullim nëse SponsorBlock refuzon të punojë për arsye të ndryshme.</string>\n  <string name=\"unset_stream_reminder\">Çaktivizo cilësimin e njoftuesit për transmetimin</string>\n  <string name=\"set_stream_reminder\">Cakto njoftuesin për transmetimin</string>\n  <string name=\"playback_starts_shortly\">Luajtja do të fillojë automatikisht kur transmetimi të jetë gati</string>\n  <string name=\"starting_stream\">Po fillon transmetimi...</string>\n  <string name=\"action_playlist_remove\">Hiq nga lista e luajtjes</string>\n  <string name=\"signin_view_title\">Po ngarkohet Kodi i Përdoruesit...</string>\n  <string name=\"signin_view_description\">Për të hyrë, futeni këtë kod në faqen %s\\nSHËNIM. Punon vetëm me shfletuesin Firefox ose Chrome.</string>\n  <string name=\"signin_view_action_text\">U krye</string>\n  <string name=\"require_checked\">Kërkon \\\"%s\\\"</string>\n  <string name=\"msg_press_again_to_exit\">Shtyp përsëri për të dalë</string>\n  <string name=\"player_remaining_time\">Mbetur: %s</string>\n  <string name=\"player_ending_time\">Përfundon në %s</string>\n  <string name=\"badge_new_content\">PËRMBAJTJE E RE</string>\n  <string name=\"badge_live\">DREJTPËRDREJT</string>\n  <string name=\"add_device_view_description\">Për të lidhur pajisjen tënde, fute këtë kod në aplikacionin YouTube të telefonit tënd në seksionin Cilësimet/Shiko në TV\\nSHËNIM: Punon vetëm me tastierën Gboard!</string>\n  <string name=\"playback_controls_repeat_pause\">Përsërite Pezullimin</string>\n  <string name=\"playback_controls_repeat_list\">Përsërite Listën</string>\n  <string name=\"action_subscribe_off\">Abonimi nuk është aktivizuar</string>\n  <string name=\"action_subscribe_on\">Abonimi është aktivizuar</string>\n  <string name=\"skip_24_rate\">Kapërce formatet 24 fps (rregullim për mënyrën Kinema e vërtetë\\?)</string>\n  <string name=\"skip_shorts\">Kapërce videot e shkurtra</string>\n  <string name=\"player_disable_suggestions\">Çaktivizo sugjerimet</string>\n  <string name=\"feedback\">Reagim</string>\n  <string name=\"sources\">Burimet</string>\n  <string name=\"releases\">Botimet</string>\n  <string name=\"prefer_avc_over_vp9\">Zgjedhja e kodekut: parapëlqe avc në vend të vp9</string>\n  <string name=\"open_chat\">Biseda/Komente</string>\n  <string name=\"open_comments\">Hap komentet</string>\n  <string name=\"place_chat_left\">Vendos bisedën në të majtë</string>\n  <string name=\"use_alt_speech_recognizer\">Mënyrë tjetër për njohjen e të folurit</string>\n  <string name=\"time_format\">Formati i kohës</string>\n  <string name=\"time_format_24\">24 ore</string>\n  <string name=\"time_format_12\">12 orë (AM/PM)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Ka shumë defekte. Përdore vetëm nëse ke probleme me njohjen e të folurit të paracaktuar.</string>\n  <string name=\"speech_recognizer\">Njohës i të folurit</string>\n  <string name=\"speech_recognizer_system\">I sistemit (i parapëlqyer)</string>\n  <string name=\"speech_recognizer_external_1\">I jashtëm 1 (me defekt të rëndë, që të punoi duhet të çaktivizosh qasjen në mikrofon)</string>\n  <string name=\"speech_recognizer_external_2\">E jashtme 2 (me defekt të rëndë)</string>\n  <string name=\"real_channel_icon\">Shfaq ikonën në butonin e kanalit</string>\n  <string name=\"subtitle_yellow_semi_transparent\">E verdhë në sfond gjysmë të tejdukshëm</string>\n  <string name=\"subtitle_yellow_black\">E verdhë në sfond të zi</string>\n  <string name=\"player_pixel_ratio\">Raporti i pikselëve</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Gri e errët (njëngjyrëshe)</string>\n  <string name=\"disable_mic_permission\">Të lutem çaktivizo qasjen në mikrofon për aplikacionin që njohja e zërit të punoi siç duhet.</string>\n  <string name=\"repeat_mode_shuffle\">Përziej çdo listë luajtjeje</string>\n  <string name=\"chat_left\">Majtas</string>\n  <string name=\"chat_right\">Djathtas</string>\n  <string name=\"card_real_thumbnails\">Zëvendëso miniaturat me një fotografi nga videoja</string>\n  <string name=\"card_content\">Ku të merren miniaturat e skedave</string>\n  <string name=\"thumb_quality_default\">E paracaktuar</string>\n  <string name=\"thumb_quality_start\">Fillimi i videos</string>\n  <string name=\"thumb_quality_middle\">Mesi i videos</string>\n  <string name=\"thumb_quality_end\">Fundi i videos</string>\n  <string name=\"content_block_status\">Kontrollo gjendjen e shërbyesit SponsorBlock</string>\n  <string name=\"dearrow_status\">Kontrollo gjendjen e shërbyesit DeArrow</string>\n  <string name=\"player_show_quality_info_bitrate\">Shto shpejtësinë e biteve në informacionin e cilësisë</string>\n  <string name=\"player_speed_button_old_behavior\">Rivendos sjelljen e vjetër të butonit të shpejtësisë</string>\n  <string name=\"protect_settings_with_password\">Mbro të gjitha cilësimet me fjalëkalim</string>\n  <string name=\"enter_settings_password\">Fut fjalëkalimin për cilësimet</string>\n  <string name=\"child_mode\">Mënyra për fëmijë</string>\n  <string name=\"child_mode_desc\">Në këtë mënyrë, përdoruesi nuk mund të përdorë kërkimin ose të shikojë ndonjë përmbajtje të sugjeruar. Cilësimet do të mbrohen me fjalëkalim.</string>\n  <string name=\"lost_setting_warning\">Cilësimet e aplikacionit do të ndryshohen. Sigurohu që ke krijuar një kopje rezervë të cilësimeve tua.</string>\n  <string name=\"player_button_long_click\">Mbaj shtypur këtë buton për rregullime shtesë</string>\n  <string name=\"pause_history\">Ndalo Historikun</string>\n  <string name=\"resume_history\">Rifillo Historikun</string>\n  <string name=\"disable_history\">Çaktivizo Historikun</string>\n  <string name=\"enable_history\">Aktivizo Historikun</string>\n  <string name=\"clear_history\">Pastro Historikun</string>\n  <string name=\"chapters\">Kapitujt</string>\n  <string name=\"card_multiline_subtitle\">Titra me shumë rreshta</string>\n  <string name=\"auto_frame_rate_modes\">Mënyrat e mbështetura</string>\n  <string name=\"loading\">Po ngarkohet…</string>\n  <string name=\"hide_streams\">Fshih transmetimet nga Abonimet</string>\n  <string name=\"video_rotate\">Rrotullo</string>\n  <string name=\"trending_searches\">Më të kërkuarat</string>\n  <string name=\"video_duration_any\">Çdo kohëzgjatje</string>\n  <string name=\"video_duration\">Kohëzgjatja</string>\n  <string name=\"video_duration_under_4\">Më pak se 4 minuta</string>\n  <string name=\"video_duration_between_4_20\">4-20 minuta</string>\n  <string name=\"video_duration_over_20\">Më shumë se 20 minuta</string>\n  <string name=\"content_type\">Lloj i përmbajtjes</string>\n  <string name=\"content_type_any\">Çdo lloj</string>\n  <string name=\"content_type_video\">Video</string>\n  <string name=\"content_type_channel\">Kanal</string>\n  <string name=\"content_type_playlist\">Lista e luajtjes</string>\n  <string name=\"content_type_movie\">Film</string>\n  <string name=\"video_features\">Veçoritë</string>\n  <string name=\"video_feature_any\">Çdo veçori</string>\n  <string name=\"video_feature_live\">Drejtpërdrejtë</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">Rendit sipas</string>\n  <string name=\"sort_by_relevance\">Rëndësisë</string>\n  <string name=\"sort_by_views\">Numrit të shikimeve</string>\n  <string name=\"sort_by_date\">Datës së ngarkimit</string>\n  <string name=\"sort_by_rating\">Vlerësimit</string>\n  <string name=\"clear_search_history\">Pastro historikun tënd të kërkimit</string>\n  <string name=\"remove_from_subscriptions\">Fshih</string>\n  <string name=\"player_long_speed_list\">Më shumë shpejtësi në listën e shpejtësive</string>\n  <string name=\"player_extra_long_speed_list\">Edhe më shumë shpejtësi në listën e shpejtësive</string>\n  <string name=\"alt_app_icon\">Ikona alternative e aplikacionit (kërkon rinisje)</string>\n  <string name=\"network_stack\">Prefero stivën e rrjetit %s</string>\n  <string name=\"player_network_stack\">Motori i rrjetit</string>\n  <string name=\"cronet_desc\">Cronet është stiva e rrjetit të Chromium. Cronet mund të ul vonesën dhe të rrisë performancën e rrjetit, gjë që mund të ndihmojë në problemet me buferimin.</string>\n  <string name=\"unlock_all_formats\">Zhblloko të gjitha formatet e videos</string>\n  <string name=\"unlock_all_formats_desc\">Në disa pajisje firmueri njofton gabimisht disa formate si të pambështetura, edhe pse janë.\\n(për shembull, televizorët e mençur 1080p priten që të njoftojnë 4k si të pambështetur).</string>\n  <string name=\"okhttp_desc\">Ky motor rrjeti është më i ngadalti, por ka qëndrueshmëri të mirë.</string>\n  <string name=\"select_channel_section\">Hap seksionin përkatës kur aplikacioni të jetë nisur nga Kanalet ATV</string>\n  <string name=\"enable_voice_search_desc\">Aplikacioni zyrtar do të zëvendësohet nga një e ashtuquajtur \\\"urë\\\"\\. Një urë ndihmon në kalimin e kërkesave globale të kërkimit në aplikacionin tonë.</string>\n  <string name=\"enable_master_password\">Aktivizo fjalëkalimin kryesor.</string>\n  <string name=\"enter_master_password\">Fut fjalëkalimin kryesor</string>\n  <string name=\"disable_stream_buffer\">Çaktivizo buferin në transmetime</string>\n  <string name=\"disable_stream_buffer_desc\">Korrigjo situatat ku transmetimi është shumë prapa. Shënim: transmetimi mund të fillojë të ngadalësohet.</string>\n  <string name=\"sony_frame_drop_fix\">Rregullo uljen e frekuencës #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">Rregullo ngadalësimet në televizorët Sony dhe disa pajisje të tjera. Shënim: mund të ketë probleme me sinkronizimin e tingullit.</string>\n  <string name=\"audio_language\">Gjuha e tingullit</string>\n  <string name=\"old_home_look\">Pamja e vjetër e seksionit Kryefaqe</string>\n  <string name=\"old_channel_look\">Pamja e vjetër e faqes Kanali</string>\n  <string name=\"hide_shorts_everywhere\">Fshih video të shkurtra kudo</string>\n  <string name=\"update_found\">Përditësim</string>\n  <string name=\"volume_boost_warning\">Çdo cilësim përtej kufirit mund të dëmtojë lartfolësin</string>\n  <string name=\"play_video_incognito\">Luaj në mënyrë të fshehtë</string>\n  <string name=\"header_kids_home\">Fëmijë</string>\n  <string name=\"player_section_playlist\">Përdor përmbajtjen e seksionit aktual si një listë lujatjeje</string>\n  <string name=\"header_trending\">Më të shikuarat</string>\n  <string name=\"screensaver\">Mbrojtësi i ekranit</string>\n  <string name=\"player_screen_off_timeout\">Koha për fikjen e ekranit</string>\n  <string name=\"old_update_notifications\">Pamja e vjetër e njoftimeve të përditësimit</string>\n  <string name=\"dialog_notification\">Njoftohu për përditësimet me një dialog flluskë</string>\n  <string name=\"mark_as_watched\">Shëno si të shikuar</string>\n  <string name=\"player_ui_animations\">Aktivizo animacionet e ndërfaqes të luajtësit</string>\n  <string name=\"player_likes_count\">Shfaq numrin e pëlqimeve/mospëlqimeve</string>\n  <string name=\"sorting_alphabetically2\">Sipas rendit alfabetik (pamje paraprake të shpejta dhe të animuara)</string>\n  <string name=\"sorting_default\">Paracaktuar (pamje paraprake të shpejta dhe të animuara)</string>\n  <string name=\"content_block_exclude_channel\">Përjashto këtë kanal nga SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">Mos e përjashto më këtë kanal nga SponsorBlock</string>\n  <string name=\"player_chapter_notification\">Kalo shpejt nëpër kapituj duke përdorur njoftimin flluskë</string>\n  <string name=\"subtitle_remember\">Mbaj mend titrat e aktivizuara për çdo kanal</string>\n  <string name=\"amazon_frame_drop_fix\">Rregullo uljen e frekuencës #2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">I menduar për pajisjet Amazon Stick. Mund të punoi edhe në pajisje të tjera.</string>\n  <string name=\"default_stack_desc\">Motor i integruar i rrjetit. Ky motor mund të ketë qëndrueshmëri më të mirë në ca situata.</string>\n  <string name=\"item_postion\">Vendndodhja e</string>\n  <string name=\"player_screen_off_dimming\">Niveli i zbehjes së ekranit të fikur</string>\n  <string name=\"screensaver_timout\">Koha për nisjen e mbrojtësit të ekranit</string>\n  <string name=\"screensaver_dimming\">Niveli i zbehjes i mbrojtësit të ekranit</string>\n  <string name=\"player_ui_on_next\">Shfaq ndërfaqen e përdoruesit të luajtësit kur të kalosh në videon tjetër</string>\n  <string name=\"autogenerated\">prodhuar automatikisht</string>\n  <string name=\"player_auto_volume\">Rregullim automatik i vëllimit të zërit</string>\n  <string name=\"header_shorts\">Video të shkurtra</string>\n  <string name=\"player_global_focus\">Shfletim pa ndërprerje midis rreshtave të butonëve të luajtësit</string>\n  <string name=\"auto_history\">Automatik (përdor cilësimet e llogarisë)</string>\n  <string name=\"remember_position_subscriptions\">Mbaj mend pozicionin e fundit të parë në Abonimet</string>\n  <string name=\"remember_position_pinned\">Mbaj mend pozicionin e fundit të parë në listat e luajtjes të gozhduara</string>\n  <string name=\"msg_player_unknown_error\">Gabim i panjohur</string>\n  <string name=\"header_notifications\">Njoftimet</string>\n  <string name=\"disable_search_history\">Çaktivizo historikun e kërkimit</string>\n  <string name=\"unlock_high_bitrate_formats\">Zhblloko formatet me shpejtësi të lartë të biteve 1080p vp9</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Zhblloko formatet me shpejtësi të lartë të biteve mp4a</string>\n  <string name=\"color_scheme_blue\">Blu</string>\n  <string name=\"color_scheme_dark_blue\">Blu e Errët</string>\n  <string name=\"player_speed_per_channel\">Për çdo kanal</string>\n  <string name=\"disable_popular_searches\">Çaktivizo pyetjet për kërkimet popullore</string>\n  <string name=\"multi_profiles\">Përdor cilësime të veçanta për secilën llogari</string>\n  <string name=\"protect_account_with_password\">Mbro këtë llogari me një fjalëkalim</string>\n  <string name=\"enter_account_password\">Fut fjalëkalimin e llogarisë</string>\n  <string name=\"show_connect_messages\">Shfaq mesazhet e lidhjes</string>\n  <string name=\"prefer_avc_over_vp9_desc\">KUJDES: maksimumi 1080p</string>\n  <string name=\"play_next\">Luaj më pas</string>\n  <string name=\"hide_watched_from_subscriptions\">Fshih videot e shikuara nga Abonimet</string>\n  <string name=\"hide_watched_from_notifications\">Fshih videot e shikuara nga Njoftimet</string>\n  <string name=\"hide_unwanted_content\">Fshih përmbajtjen e padëshiruar</string>\n  <string name=\"hide_watched_from_home\">Fshih videot e shikuara nga Kryefaqja</string>\n  <string name=\"hide_watched_from_watch_later\">Fshih videot e shikuara nga lista e luajtjes \\\"Shiko më vonë\\\".</string>\n  <string name=\"remote_control_permission\">Kontrolli në Largësi në sfond kërkon leje për Mbivendosjeje</string>\n  <string name=\"login_from_browser\">Hyr nga shfletuesi</string>\n  <string name=\"disable_remote_history\">Çaktivizo Historikun kur përdor telekomandën</string>\n  <string name=\"keyboard_fix\">Parandalo hapjen e tastierës me butonin OK (G20 dhe të tjerë)</string>\n  <string name=\"nothing_found\">Nuk u gjet asgjë</string>\n  <string name=\"auto_frame_rate_desc\">Ky rregullim heq dridhjet në skenat ku kamera lëviz shpejt, si p.sh. në transmetimin e sporteve</string>\n  <string name=\"channel_filter_hint\">Filtro kanalet</string>\n  <string name=\"player_loop_shorts\">Përsërit Videot e Shkurtra vazhdimisht</string>\n  <string name=\"player_quick_shorts_skip\">Kapërce Videot e Shkurtra me butonat majtas/djathtas</string>\n  <string name=\"channels_filter\">Shfaq fushën e Filtrimit të kanaleve në seksionin Kanale</string>\n  <string name=\"channel_search_bar\">Shiriti i kërkimit brenda faqes së Kanalit</string>\n  <string name=\"header_sports\">Sporti</string>\n  <string name=\"keep_finished_activities\">Mbaj detyrat e përfunduara</string>\n  <string name=\"disable_channels_service\">Çaktivizo shërbimin e Kanaleve</string>\n  <string name=\"replace_titles\">Zëvendëso titujt</string>\n  <string name=\"enable\">Aktivizo</string>\n  <string name=\"more_info\">Informacione të mëtejshme</string>\n  <string name=\"about_sponsorblock\">Rreth SponsorBlock</string>\n  <string name=\"about_dearrow\">Rreth DeArrow</string>\n  <string name=\"replace_thumbnails\">Zëvendëso miniaturat</string>\n  <string name=\"crowdsourced_thumbnails\">Miniaturat të mbledhura në grup</string>\n  <string name=\"crowdsoursed_titles\">Tituj të mbledhur në grup</string>\n  <string name=\"dearrow_not_submitted_thumbs\">Burimi i miniaturave (nëse nuk ka paraqitje në DeArrow)</string>\n  <string name=\"pitch_effect\">Efekti i tonit</string>\n  <string name=\"fullscreen_mode\">Mënyra e ekranit të plotë (pa shirita të sistemit)</string>\n  <string name=\"player_only_mode\">Shfaq vetëm luajtësin nëse videoja hapet jashtë aplikacionit</string>\n  <string name=\"pinned_channel_rows\">Paraqit kanalet e gozhduara si rreshta</string>\n  <string name=\"prefer_ipv4\">Parapëlqe DNS IPv4</string>\n  <string name=\"prefer_ipv4_desc\">Mund të zgjidhë situatat kur aplikacioni nuk punon fare.\\nShënim: Mund të shkaktojë ngrirje dhe rënie (veçanërisht në pajisjet Android 8 ose Dune HD)</string>\n  <string name=\"long_press_for_settings\">MBAJ SHTYPUR PËR CILËSIMET</string>\n  <string name=\"device_specific_backup\">Kopja rezervë vetëm për këtë pajisje</string>\n  <string name=\"local_backup\">Kopje rezervë lokale</string>\n  <string name=\"long_press_for_options\">MBAJ SHTYPUR PËR RREGULLIMET</string>\n  <string name=\"speech_engine\">Motori i kërkimit me zë</string>\n  <string name=\"auto_backup\">Kopje rezervë automatike (një herë në ditë)</string>\n  <string name=\"repeat_mode_reverse_list\">Luaj listën e luajtjes ose videot e kanalit në rend të kundërt</string>\n  <string name=\"unknown_source_error\">Gabim i panjohur i burimit</string>\n  <string name=\"unknown_renderer_error\">Gabim i panjohur i paraqitësit</string>\n  <string name=\"player_quick_skip_videos\">Kapërce videot e zakonshme me butonat majtas/djathtas</string>\n  <string name=\"calm_msg\">Po e rregullojmë problemin. Kontrollo për përditësime herë pas here</string>\n  <string name=\"without_picture\">Pa figurë</string>\n  <string name=\"video_disabled\">Video e çaktivizuar</string>\n  <string name=\"applying_fix\">Mos mbyll luajtësin. Duke zbatuar rregullimin...</string>\n  <string name=\"hide_mixes\">Fshih Përzjerjet</string>\n  <string name=\"player_global_focus_desc\">Kjo veçori ndikon në se cilin buton të luajtësit do të marrë përqëndrimin kur lundron midis rreshtave të butonave të luajtësit</string>\n  <string name=\"video_buffer_size_lowest\">Më i ulëti</string>\n  <string name=\"disable_network_error_fixing\">Çaktivizo rregullimin automatik të gabimeve të rrjetit</string>\n  <string name=\"disable_network_error_fixing_desc\">Ndoshta duhet ta aktivizosh këtë rregullim nëse je duke përdorur një VPN</string>\n  <string name=\"recommended\">Të këshilluara</string>\n  <string name=\"new_subscriptions_group\">Grup i ri</string>\n  <string name=\"add_to_subscriptions_group\">Shto/Hiq nga grupi i abonimit</string>\n  <string name=\"unpin_group_from_sidebar\">Hiq grupin e abonimit</string>\n  <string name=\"rename_group\">Riemërto grupin e abonimit</string>\n  <string name=\"video_buffer_size_highest\">Më i larti</string>\n  <string name=\"screen_dimming_amount\">Niveli i zbehjes së ekranit</string>\n  <string name=\"screen_dimming_timeout\">Koha për nisjen e zbehjes së ekranit</string>\n  <string name=\"place_comments_left\">Vendos komentet në të majtë</string>\n  <string name=\"suggestions\">Sugjerime</string>\n  <string name=\"playlists_rows\">Shfaq Listat e Luajtjes si rreshta</string>\n  <string name=\"not_supported_by_device\">Pajisja nuk mbështet këtë veçori</string>\n  <string name=\"hide_shorts_from_search\">Fshih videot e shkurtra nga rezultatet e kërkimit</string>\n  <string name=\"import_subscriptions_group\">Importo</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">Çaktivizo Titrat</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">Aktivizo Titrat</string>\n  <string name=\"context_menu_sorting\">Renditja e menysë së kontekstit</string>\n  <string name=\"my_videos\">Videot e mia</string>\n  <string name=\"paid_content_notification\">Njoftim për përmbajtje me pagesë</string>\n  <string name=\"player_audio_focus\">Përqendrimi i zërit (pezullo nëse zbulohen luajtës të tjerë)</string>\n  <string name=\"you_liked\">E pëlqeve</string>\n  <string name=\"video_flip\">Pasqyro</string>\n  <string name=\"search_exit_shortcut\">Dil nga kërkimi</string>\n  <string name=\"premium_users_only\">Vetëm për përdorues Premium. Rregullim për listën e formateve të videove që ishte e paplotë</string>\n  <string name=\"playback_buffering_fix\">Rregullim për ngecjet gjatë luajtjes</string>\n  <string name=\"oculus_quest_fix\">Rregullim për Oculus Quest</string>\n  <string name=\"card_preview_muted\">Video pa zë</string>\n  <string name=\"card_preview_full\">Video me zë</string>\n  <string name=\"card_preview\">Pamje paraprake të skedave</string>\n  <string name=\"card_unlocalized_titles\">Tituj video të palokalizuar</string>\n  <string name=\"original_lang\">Origjinale</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">Mos ndrysho madhësinë e videos për t\\'u përshtatur me dritaren e dialogut</string>\n  <string name=\"typing_corrections\">Çaktivizo vetë-korrigjimin gjatë shtypjes së tekstit</string>\n  <string name=\"remove_playlist_fmt\">Do ta fshish %s përgjithmonë?</string>\n  <string name=\"player_chapter_notification2\">Kalo te kapitulli tjetër duke klikuar mbi njoftimin</string>\n  <string name=\"suggestions_horizontally_scrolled\">Sugjerime të lëvizura horizontalisht</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-sr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<resources>\n    <string name=\"header_home\">Почетна</string>\n    <string name=\"title_search\">Тражи</string>\n    <string name=\"header_subscriptions\">Претплате</string>\n    <string name=\"header_history\">Историја</string>\n    <string name=\"header_music\">Музика</string>\n    <string name=\"header_news\">Новости</string>\n    <string name=\"header_gaming\">Играње</string>\n    <string name=\"header_playlists\">Листе</string>\n    <string name=\"header_settings\">Поставке</string>\n    <string name=\"header_channels\">Канали</string>\n    <string name=\"subscriptions_signin_title\">Најновије са канала које волите</string>\n    <string name=\"subscriptions_signin_subtitle\">Пријавите се да видите своје претплате</string>\n    <string name=\"subscriptions_signin_button_text\">ПРИЈАВА</string>\n    <string name=\"library_signin_to_show_more\">Гледајте омиљене, сачуване или претплаћене видео записе</string>\n    <string name=\"library_signin_subtitle\">Пријавите се да видите своју збирку</string>\n    <string name=\"action_signin\">ПРИЈАВА</string>\n    <string name=\"title_video_formats\">Видео формати</string>\n    <string name=\"title_audio_formats\">Аудио формати</string>\n    <string name=\"subtitle_category_title\">Титлови</string>\n    <string name=\"playback_queue_category_title\">Ред за пуштање</string>\n    <string name=\"video_max_quality\">Ауто (макс. квалитет)</string>\n    <string name=\"audio_max_quality\">Ауто (макс. квалитет)</string>\n    <string name=\"subtitles_disabled\">Титлови искључени</string>\n    <string name=\"auto_frame_rate\">Аутоматска брзина кадрова</string>\n    <string name=\"frame_rate_correction\">Фиксна:\\n%s</string>\n    <string name=\"category_background_playback\">Пуштање у позадини</string>\n    <string name=\"not_implemented\">Није подржано</string>\n    <string name=\"not_supported_by_device\">Уређај не подржава ову функцију</string>\n    <string name=\"option_background_playback_off\">Искључено</string>\n    <string name=\"option_background_playback_pip\">Слика у слици</string>\n    <string name=\"option_background_playback_only_audio\">Само звук</string>\n    <string name=\"playback_settings\">Поставке квалитета пуштања</string>\n    <string name=\"video_speed\">Брзина видеа</string>\n    <string name=\"resolution_switch\">Пребаци резолуцију</string>\n    <string name=\"video_buffer\">Видео бафер</string>\n    <string name=\"video_buffer_size_none\">ништа</string>\n    <string name=\"video_buffer_size_lowest\">најмањи</string>\n    <string name=\"video_buffer_size_low\">мали</string>\n    <string name=\"video_buffer_size_med\">средњи</string>\n    <string name=\"video_buffer_size_high\">велики</string>\n    <string name=\"video_buffer_size_highest\">највећи</string>\n    <string name=\"update_changelog\">Дневник измена</string>\n    <string name=\"install_update\">Инсталирај ажурирање</string>\n    <string name=\"section_is_empty\">Нема ничега овде.</string>\n    <string name=\"dialog_add_to_playlist\">Додај/уклони са листе</string>\n    <string name=\"msg_signed_users_only\">Само пријављени корисници</string>\n    <string name=\"msg_cant_load_content\">Не могу да учитам садржај.\\n1) Проверите датум и време на свом уређају.\\n2) Проверите повезаност на интернет.\\n3) Проверите поставке проксија.\\n4) Пробајте да се пријавите на свој налог.\\n5) Можда овде ни нема ничега.</string>\n    <string name=\"title_video_presets\">Шаблони за видео</string>\n    <string name=\"settings_accounts\">Налози</string>\n    <string name=\"settings_left_panel\">Уреди категорије</string>\n    <string name=\"settings_themes\">Теме</string>\n    <string name=\"settings_other\">Остало</string>\n    <string name=\"settings_player\">Видео плејер</string>\n    <string name=\"settings_language\">Језик</string>\n    <string name=\"settings_linked_devices\">Повезани уређаји</string>\n    <string name=\"settings_about\">О програму</string>\n    <string name=\"dialog_account_list\">Изаберите налог</string>\n    <string name=\"dialog_account_none\">ништа</string>\n    <string name=\"dialog_remove_account\">Одјави се</string>\n    <string name=\"dialog_add_account\">Пријави се</string>\n    <string name=\"default_lang\">Подразумевано</string>\n    <string name=\"original_lang\">Изворан</string>\n    <string name=\"dialog_select_language\">Језик</string>\n    <string name=\"subtitle_default\">Подразумевано</string>\n    <string name=\"subtitle_white_semi_transparent\">бело на полупровидној позадини</string>\n    <string name=\"subtitle_style\">Стил титла</string>\n    <string name=\"subtitle_language\">Језик титла</string>\n    <string name=\"action_search\">Тражи</string>\n    <string name=\"settings_main_ui\">Корисничко сучеље</string>\n    <string name=\"dialog_main_ui\">Корисничко сучеље</string>\n    <string name=\"card_animated_previews\">Анимирани прегледи</string>\n    <string name=\"web_site\">Веб сајт</string>\n    <string name=\"donation\">Донација</string>\n    <string name=\"dialog_about\">О програму</string>\n    <string name=\"dialog_player_ui\">Видео плејер</string>\n    <string name=\"player_show_ui_on_pause\">Приказ сучеља при паузи</string>\n    <string name=\"player_pause_on_ok\">ОК дугме паузира пуштање</string>\n    <string name=\"player_ok_button_behavior\">Понашање ОК дугмета</string>\n    <string name=\"player_only_ui\">Само сучеље</string>\n    <string name=\"player_ui_and_pause\">Сучеље и пауза</string>\n    <string name=\"player_only_pause\">Само пауза</string>\n    <string name=\"check_for_updates\">Провери ажурирања</string>\n    <string name=\"update_not_found\">Користите последњу верзију</string>\n    <string name=\"update_in_progress\">Сачекајте…</string>\n    <string name=\"player_ui_hide_behavior\">Скривање сучеља</string>\n    <string name=\"option_never\">никад</string>\n    <string name=\"side_panel_sections\">Избор одељака</string>\n    <string name=\"boot_to_section\">Почни у одељку</string>\n    <string name=\"large_ui\">Велико сучеље</string>\n    <string name=\"video_grid_scale\">Размера видео мреже</string>\n    <string name=\"scale_ui\">Размера сучеља</string>\n    <string name=\"color_scheme\">Шема боја</string>\n    <string name=\"color_scheme_default\">подразумевана</string>\n    <string name=\"color_scheme_red_grey\">црвено-сива</string>\n    <string name=\"color_scheme_red\">црвена</string>\n    <string name=\"color_scheme_dark_grey\">тамно сива</string>\n    <string name=\"disable_update_check\">Искључи проверу ажурирања</string>\n    <string name=\"show_again\">Прикажи поново</string>\n    <string name=\"check_updates_auto\">Обавести о новим верзијама</string>\n    <string name=\"select_account_on_boot\">Изабери на покретању</string>\n    <string name=\"player_other\">Разно</string>\n    <string name=\"player_full_date\">Прецизан датум у опису</string>\n    <string name=\"open_channel\">Отвори канал</string>\n    <string name=\"open_playlist\">Отвори листу пуштања</string>\n    <string name=\"not_interested\">Не интересује ме</string>\n    <string name=\"not_recommend_channel\">Не препоручуј канал</string>\n    <string name=\"you_wont_see_this_video\">Видео је уклоњен из препоручених</string>\n    <string name=\"you_wont_see_this_channel\">Канал је уклоњен из препоручених</string>\n    <string name=\"settings_search\">Претрага</string>\n    <string name=\"dialog_search\">Тражи</string>\n    <string name=\"instant_voice_search\">Брза гласовна претрага</string>\n    <string name=\"option_background_playback_behind\">Пусти иза</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">Подаци о следећем видеу још нису учитани</string>\n    <string name=\"card_multiline_title\">Наслов у више линија</string>\n    <string name=\"repeat_mode_all\">Пуштај видео без престанка</string>\n    <string name=\"repeat_mode_one\">Понови тренутни видео</string>\n    <string name=\"repeat_mode_pause\">Паузирај након сваког видеа (осим оних у реду)</string>\n    <string name=\"repeat_mode_pause_alt\">Пуштај без престанка само листу</string>\n    <string name=\"repeat_mode_none\">Заустави након једног видеа (осим оних у реду)</string>\n    <string name=\"subscribed_to_channel\">Претплаћен канал</string>\n    <string name=\"unsubscribed_from_channel\">Без претплате</string>\n    <string name=\"subtitle_yellow_transparent\">жуто на провидној позадини</string>\n    <string name=\"subtitle_white_transparent\">бело на провидној позадини</string>\n    <string name=\"subtitle_white_black\">бело на црној позадини</string>\n    <string name=\"player_seek_preview\">Преглед при премотавању</string>\n    <string name=\"color_scheme_dark_grey_oled\">тамно сиво (ОЛЕД)</string>\n    <string name=\"channels_section_sorting\">Ређање у одељку канала</string>\n    <string name=\"sorting_by_new_content\">Нови садржај</string>\n    <string name=\"sorting_alphabetically\">Абецедно</string>\n    <string name=\"sorting_last_viewed\">Последње гледано</string>\n    <string name=\"player_pause_when_seek\">Пауза при премотавању</string>\n    <string name=\"playlists_style\">Стил одељка листе пуштања</string>\n    <string name=\"playlists_style_grid\">Мрежа</string>\n    <string name=\"playlists_style_rows\">Редови</string>\n    <string name=\"player_show_clock\">Прикажи сат у контролној траци</string>\n    <string name=\"player_show_remaining_time\">Прикажи преостало време у контролној траци</string>\n    <string name=\"player_show_ending_time\">Прикажи време завршетка у контролној траци</string>\n    <string name=\"open_channel_uploads\">Отвори слања канала</string>\n    <string name=\"app_exit_shortcut\">Изађи из апликације</string>\n    <string name=\"player_exit_shortcut\">Изађи из плејера</string>\n    <string name=\"search_exit_shortcut\">Изађи из претраге</string>\n    <string name=\"app_exit_none\">Не излази</string>\n    <string name=\"app_double_back_exit\">Двоструко назад</string>\n    <string name=\"app_single_back_exit\">Једном назад</string>\n    <string name=\"action_video_zoom\">Увећање видеа</string>\n    <string name=\"video_zoom\">Увећање видеа</string>\n    <string name=\"video_zoom_default\">подразумевано</string>\n    <string name=\"video_zoom_fit_width\">попуни ширину</string>\n    <string name=\"video_zoom_fit_height\">попуни висину</string>\n    <string name=\"video_zoom_fit_both\">попуни ширину или висину</string>\n    <string name=\"video_zoom_stretch\">развуци</string>\n    <string name=\"color_scheme_teal\">тиркизна</string>\n    <string name=\"color_scheme_teal_oled\">тиркизна (ОЛЕД)</string>\n    <string name=\"player_seek_preview_none\">искључено</string>\n    <string name=\"player_seek_preview_single\">један кадар</string>\n    <string name=\"player_seek_preview_carousel\">вртешка</string>\n    <string name=\"player_seek_preview_carousel_slow\">вртешка (споро, кључни кадрови)</string>\n    <string name=\"player_seek_preview_carousel_fast\">вртешка (брзо, непрецизан преглед)</string>\n    <string name=\"unsubscribe_from_channel\">Одјави се са канала</string>\n    <string name=\"card_auto_scrolled_title\">Клизај дугачке наслове</string>\n    <string name=\"card_title_lines_num\">Број линија за наслов</string>\n    <string name=\"share_link\">Подели везу</string>\n    <string name=\"share_qr_link\">Подели везу (Кју-ар код)</string>\n    <string name=\"subscribe_to_channel\">Пријави се на канал</string>\n    <string name=\"wait_data_loading\">Сачекајте док се подаци учитавају…</string>\n    <string name=\"auto_frame_rate_pause\">Аутоматска брзина кадрова (пауза при промени)</string>\n    <string name=\"audio_shift\">Померај звука</string>\n    <string name=\"player_remember_speed\">Памти брзину</string>\n    <string name=\"mark_channel_as_watched\">Означи као гледано</string>\n    <string name=\"channel_marked_as_watched\">Канал је означен као одгледан</string>\n    <string name=\"dialog_add_device\">Додај уређај</string>\n    <string name=\"dialog_remove_all_devices\">Уклони све уређаје</string>\n    <string name=\"device_link_enabled\">Веза укључена</string>\n    <string name=\"device_connected\">Уређај „%s“ је повезан</string>\n    <string name=\"device_disconnected\">Уређај „%s“ је искључен</string>\n    <string name=\"settings_ui_scale\">Размера сучеља</string>\n    <string name=\"msg_restart_app\">Поново покрените ради примене ове поставке</string>\n    <string name=\"settings_block\">Блокирање садржаја</string>\n    <string name=\"msg_applying\">Примењујем „%s“</string>\n    <string name=\"msg_done\">Завршено</string>\n    <string name=\"settings_general\">Опште</string>\n    <string name=\"settings_video\">Видео</string>\n    <string name=\"content_block_confirm_skip\">Потврда прескока</string>\n    <string name=\"content_block_categories\">Прескочи делове</string>\n    <string name=\"content_block_sponsor\">спонзоре</string>\n    <string name=\"content_block_intro\">пауза/уводна анимација</string>\n    <string name=\"content_block_outro\">одјавна шпица</string>\n    <string name=\"content_block_interaction\">подсећања (о претплати)</string>\n    <string name=\"content_block_self_promo\">неплаћено/само-рекламирање</string>\n    <string name=\"content_block_music_off_topic\">не-музички део</string>\n    <string name=\"confirm_segment_skip\">Прескочити део „%s“\\?</string>\n    <string name=\"cancel_segment_skip\">Откажи прескакање дела</string>\n    <string name=\"msg_skipping_segment\">Прескачем део „%s“…</string>\n    <string name=\"content_block_notification_type\">Врста обавештења</string>\n    <string name=\"content_block_notify_none\">без</string>\n    <string name=\"content_block_notify_toast\">пливајућа</string>\n    <string name=\"content_block_notify_dialog\">Потврдни дијалог</string>\n    <string name=\"return_to_launcher\">Врати се на покретач са АТВ канала/претраге</string>\n    <string name=\"intent_force_close\">Принудни излаз ако је видео отворен из спољашњег извора</string>\n    <string name=\"btn_confirm\">Потврди</string>\n    <string name=\"player_low_video_quality\">Низак квалитет видеа</string>\n    <string name=\"remote_session_closed\">Удаљена сесија је затворена</string>\n    <string name=\"msg_mode_switch_error\">Не могу да пребацим режим приказа на „%s“</string>\n    <string name=\"msg_player_error\">Дошло је до грешке %s. Поново покрећем …</string>\n    <string name=\"video_preset_disabled\">Без преподешења</string>\n    <string name=\"video_preset_enabled\">Формат следећег видеа биће подешен у складу са преподешењем</string>\n    <string name=\"player_sleep_timer\">Тајмер спавања</string>\n    <string name=\"player_show_quality_info\">Подаци о квалитету у контролној траци</string>\n    <string name=\"player_remember_each_speed\">Памти брзину за сваки видео</string>\n    <string name=\"player_remember_speed_none\">не</string>\n    <string name=\"player_remember_speed_all\">иста за сваки видео</string>\n    <string name=\"player_remember_speed_each\">за сваки посебно</string>\n    <string name=\"msg_player_error_source\">Не могу да преузмем видео</string>\n    <string name=\"video_aspect\">Размера</string>\n    <string name=\"player_tweaks\">Програмерске опције</string>\n    <string name=\"header_uploads\">Слања</string>\n    <string name=\"player_show_global_clock\">Сат увек приказан</string>\n    <string name=\"player_show_global_ending_time\">Време завршетка увек приказано</string>\n    <string name=\"app_corner_clock\">Почетна: сат у горњем десном углу</string>\n    <string name=\"player_corner_clock\">Плејер: сат у горњем десном углу</string>\n    <string name=\"player_corner_ending_time\">Плејер: време завршетка у горњем десном углу</string>\n    <string name=\"uploads_old_look\">Врати стари изглед одељка Слања</string>\n    <string name=\"background_playback_activation\">Пуштање у позадини (активација)</string>\n    <string name=\"dialog_select_country\">Држава</string>\n    <string name=\"settings_language_country\">Језик/Држава</string>\n    <string name=\"screen_dimming\">Затамњивање екрана</string>\n    <string name=\"removed_from_playback_queue\">Уклоњен из реда за пуштање</string>\n    <string name=\"added_to_playback_queue\">Додат у ред за пуштање</string>\n    <string name=\"add_to_playback_queue\">Додај у ред за пуштање</string>\n    <string name=\"remove_from_playback_queue\">Уклони из реда за пуштање</string>\n    <string name=\"proxy_enabled\">Прокси укључен</string>\n    <string name=\"proxy_disabled\">Прокси искључен</string>\n    <string name=\"uploads_row_name\">Слања</string>\n    <string name=\"playlists_row_name\">Направљене листе</string>\n    <string name=\"popular_uploads_row_name\">Популарна слања</string>\n    <string name=\"news_row_name\">Вести</string>\n    <string name=\"breaking_news_row_name\">Ударне вести</string>\n    <string name=\"covid_news_row_name\">КОВИД-19 дезинформације</string>\n    <string name=\"enable_voice_search\">Укључи гласовну претрагу</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">Пријави/Одјави се са канала</string>\n    <string name=\"skip_each_segment_once\">Не прескачи делове поново</string>\n    <string name=\"disable_ok_long_press\">Искључи дуг притисак за ОК дугме</string>\n    <string name=\"disable_ok_long_press_desc\">Намењено за контролере са грешкама где ОК дугме не ради исправно</string>\n    <string name=\"player_time_correction\">Прикажи време у складу са брзином</string>\n    <string name=\"sponsor_color_markers\">Обојени маркери у траци напретка</string>\n    <string name=\"refresh_section\">Освежи одељак</string>\n    <string name=\"option_disabled\">Искључено</string>\n    <string name=\"live_now_row_name\">Уживо сада</string>\n    <string name=\"run_in_background\">Пуштање у позадини</string>\n    <string name=\"settings_remote_control\">Даљински управљач</string>\n    <string name=\"removed_from_history\">Видео је уклоњен из историјата</string>\n    <string name=\"remove_from_history\">Уклони из историјата</string>\n    <string name=\"upload_date\">Време слања</string>\n    <string name=\"upload_date_today\">данас</string>\n    <string name=\"upload_date_this_week\">ове недеље</string>\n    <string name=\"upload_date_this_month\">овог месеца</string>\n    <string name=\"upload_date_this_year\">ове године</string>\n    <string name=\"upload_date_last_hour\">у последњем сату</string>\n    <string name=\"context_menu\">Контекстни мени</string>\n    <string name=\"context_menu_sorting\">Сотрирање контекстног менија</string>\n    <string name=\"volume\">Јачина %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s сек.</string>\n    <string name=\"audio_shift_sec\">%s сек.</string>\n    <string name=\"ui_hide_timeout_sec\">%s сек.</string>\n    <string name=\"screen_dimming_timeout_min\">%s мин.</string>\n    <string name=\"mark_all_channels_watched\">Означи све канале као одгледане</string>\n    <string name=\"move_section_up\">Помери одељак горе</string>\n    <string name=\"move_section_down\">Помери одељак доле</string>\n    <string name=\"player_buttons\">Подеси дугмад плејера</string>\n    <string name=\"action_playlist_add\">Додај на листу</string>\n    <string name=\"action_video_stats\">Видео статистика</string>\n    <string name=\"action_subscribe\">Претплати се</string>\n    <string name=\"action_channel\">Отвори канал</string>\n    <string name=\"action_pip\">СуС</string>\n    <string name=\"action_video_info\">Опис видеа</string>\n    <string name=\"action_screen_off\">Екран искључен</string>\n    <string name=\"action_sound_off\">Звук искључен</string>\n    <string name=\"action_playback_queue\">Пусти редослед</string>\n    <string name=\"action_video_speed\">Брзина видеа</string>\n    <string name=\"action_subtitles\">Титлови</string>\n    <string name=\"action_like\">Свиђање</string>\n    <string name=\"action_dislike\">Несвиђање</string>\n    <string name=\"action_play_pause\">Пусти/Паузирај</string>\n    <string name=\"action_repeat_mode\">Режим пуштања</string>\n    <string name=\"action_next\">Следећи видео</string>\n    <string name=\"action_previous\">Претходни видео</string>\n    <string name=\"action_high_quality\">Квалитет видеа</string>\n    <string name=\"content_block_no_skipping_mode\">Режим без прескока</string>\n    <string name=\"content_block_action_type\">Изаберите радњу</string>\n    <string name=\"content_block_action_none\">ништа</string>\n    <string name=\"content_block_action_only_skip\">само прескочи</string>\n    <string name=\"content_block_action_toast\">прескок уз обавештење</string>\n    <string name=\"content_block_action_dialog\">приказ дијалога за потврду</string>\n    <string name=\"cancel_dialog\">Откажи</string>\n    <string name=\"trending_row_name\">У тренду</string>\n    <string name=\"player_seek_type\">Понашање премотавања</string>\n    <string name=\"player_seek_regular\">Обично</string>\n    <string name=\"player_seek_confirmation_pause\">са потврдом (пауза при премотавању)</string>\n    <string name=\"player_seek_confirmation_play\">са потврдом (пушта при премотавању)</string>\n    <string name=\"hide_shorts_from_home\">Сакриј кратке са Почетне</string>\n    <string name=\"finish_on_disconnect\">Затвори апликацију при искључењу телефона/таблета</string>\n    <string name=\"update_error\">Грешка ажурирања</string>\n    <string name=\"hide_shorts_from_search\">Сакриј кратке са резултата претраге</string>\n    <string name=\"hide_shorts_from_history\">Сакриј кратке са Историје</string>\n    <string name=\"hide_shorts_from_trending\">Сакриј кратке са Трендинга</string>\n    <string name=\"disable_screensaver\">Искључи чувара екрана</string>\n    <string name=\"description_not_found\">Опис није нађен</string>\n    <string name=\"proxy_port_hint\">Порт проксија</string>\n    <string name=\"proxy_host_hint\">Име или ИП проксија</string>\n    <string name=\"proxy_username_hint\">Корисничко име за прокси</string>\n    <string name=\"proxy_password_hint\">Лозинка за прокси</string>\n    <string name=\"proxy_test_btn\">Тестирај</string>\n    <string name=\"internet_censorship\">Интернет цензура</string>\n    <string name=\"rename_section\">Преименуј овај одељак</string>\n    <string name=\"simple_edit_value_hint\">Нова вредност</string>\n    <string name=\"seek_interval\">Размак премотавања</string>\n    <string name=\"seek_interval_sec\">%s сек.</string>\n    <string name=\"subtitle_scale\">Размера титла</string>\n    <string name=\"master_volume\">Главна јачина</string>\n    <string name=\"volume_limit\">Ограничење јачине</string>\n    <string name=\"player_volume\">Јачина</string>\n    <string name=\"play_video\">Пусти</string>\n    <string name=\"various_buttons\">Дугмад на врху главног прозора</string>\n    <string name=\"pressing_home\">притиском на ПОЧЕТНА</string>\n    <string name=\"pressing_home_back\">притиском на ПОЧЕТНА или НАЗАД</string>\n    <string name=\"pressing_back\">пристиском на НАЗАД</string>\n    <string name=\"player_number_key_seek\">Премотај са тастерима бројева</string>\n    <string name=\"save_remove_playlist\">Додај/Обриши листу из одељка Листе</string>\n    <string name=\"remove_playlist_fmt\">Обриши %s трајно?</string>\n    <string name=\"save_playlist\">Сачувај листу на одељку Листе</string>\n    <string name=\"remove_playlist\">Обриши листу трајно из одељка Листе</string>\n    <string name=\"removed_from_playlists\">Обрисана из одељка Листе</string>\n    <string name=\"saved_to_playlists\">Додата у одељак Листе</string>\n    <string name=\"create_playlist\">Направи листу пуштања</string>\n    <string name=\"add_video_to_new_playlist\">Додај на нову листу</string>\n    <string name=\"cant_delete_empty_playlist\">Не могу да обришем празну листу</string>\n    <string name=\"playlist\">Листа пуштања</string>\n    <string name=\"rename_playlist\">Преименуј листу</string>\n    <string name=\"cant_rename_empty_playlist\">Не могу да преименујем празну листу</string>\n    <string name=\"cant_rename_foreign_playlist\">Не могу да преименујем туђу листу</string>\n    <string name=\"cant_save_playlist\">Ова листа се не може додати</string>\n    <string name=\"playlist_order\">Сложи листу</string>\n    <string name=\"playlist_order_added_date_newer_first\">Датум додавања (новије прво)</string>\n    <string name=\"playlist_order_added_date_older_first\">Датум додавања (старије прво)</string>\n    <string name=\"playlist_order_published_date_newer_first\">Датум објављивања (новије прво)</string>\n    <string name=\"playlist_order_published_date_older_first\">Датум објављивања (старије прво)</string>\n    <string name=\"playlist_order_popularity\">Популарност</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">Не могу ово да радим туђим листама</string>\n    <string name=\"action_playlist_remove\">Уклони са листе</string>\n    <string name=\"signin_view_action_text\">Завршено</string>\n    <string name=\"msg_press_again_to_exit\">Притисните још једном за излаз</string>\n    <string name=\"player_remaining_time\">Преостало: %s</string>\n    <string name=\"player_ending_time\">Завршава у %s</string>\n    <string name=\"badge_new_content\">НОВИ САДРЖАЈ</string>\n    <string name=\"badge_live\">УЖИВО</string>\n    <string name=\"player_disable_suggestions\">Искључи предлоге</string>\n    <string name=\"suggestions\">Предлози</string>\n    <string name=\"feedback\">Повратне информације</string>\n    <string name=\"sources\">Извори</string>\n    <string name=\"releases\">Издања</string>\n    <string name=\"open_chat\">Ћаскање</string>\n    <string name=\"open_comments\">Отвори коментаре</string>\n    <string name=\"place_chat_left\">Постави ћаскање лево</string>\n    <string name=\"place_comments_left\">Постави коментаре лево</string>\n    <string name=\"time_format\">Приказ времена</string>\n    <string name=\"time_format_24\">24 сата</string>\n    <string name=\"time_format_12\">12 сати</string>\n    <string name=\"speech_engine\">Гласовна претрага</string>\n    <string name=\"speech_recognizer_system\">Системски</string>\n    <string name=\"real_channel_icon\">Прикажи стварну икону на дугмету канала</string>\n    <string name=\"subtitle_yellow_semi_transparent\">жуто на полупровидној позадини</string>\n    <string name=\"subtitle_yellow_black\">жуто на црној позадини</string>\n    <string name=\"player_pixel_ratio\">Размера тачке</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">тамно сиво (једнобојно)</string>\n    <string name=\"chat_left\">Лево</string>\n    <string name=\"chat_right\">Десно</string>\n    <string name=\"cards_style\">Стил картица</string>\n    <string name=\"auto_frame_rate_applying\">Примењујем аутоматску брзину кадрова %sx%s\\@%s</string>\n    <string name=\"msg_player_error_renderer\">Изабрани видео формат није подржан.\\nМожда је до фирмвера. Покушајте са гашењем и паљењем уређаја.</string>\n    <string name=\"msg_player_error_video_renderer\">Изабрани ВИДЕО формат није подржан.\\nПокушај да изабереш други притиском на HQ дугме у плејеру.\\nАко ни то не помогне, покушај са гашењем и паљењем уређаја.</string>\n    <string name=\"msg_player_error_audio_renderer\">Изабрани АУДИО формат није подржан.\\nПокушај да изабереш други притиском на HQ дугме у плејеру.\\nАко ни то не помогне, покушај са гашењем и паљењем уређаја.</string>\n    <string name=\"msg_player_error_subtitle_renderer\">Изабрани формат ТИТЛОВА није подржан.\\nПокушај да изабереш други притиском на HQ дугме у плејеру.\\nАко ни то не помогне, покушај са гашењем и паљењем уређаја.</string>\n    <string name=\"msg_player_error_unexpected\">Непозната грешка видео декодера</string>\n    <string name=\"channels_old_look\">Врати стари изглед одељка Канали</string>\n    <string name=\"channels_auto_load\">Аутоматски учитај садржај одељка Канали</string>\n    <string name=\"return_to_background_video\">Врати се на видео који ради у позадини</string>\n    <string name=\"pin_unpin_from_sidebar\">Закачи/откачи са бочне траке</string>\n    <string name=\"unpin_group_from_sidebar\">Уклони групу претплата</string>\n    <string name=\"pin_unpin_playlist\">Закачи/откачи листу са бочне траке</string>\n    <string name=\"pin_unpin_channel\">Закачи/откачи канал са бочне траке</string>\n    <string name=\"pin_playlist\">Закачи листу на бочну траку</string>\n    <string name=\"pin_channel\">Закачи канал на бочну траку</string>\n    <string name=\"pinned_to_sidebar\">Закачено на бочну траку</string>\n    <string name=\"unpin_from_sidebar\">Откачи са бочне траке</string>\n    <string name=\"unpinned_from_sidebar\">Откачено са бочне траке</string>\n    <string name=\"subtitle_system\">системски (Андроид поставке &gt; Приступачност)</string>\n    <string name=\"subtitle_position\">Померај титла од дна</string>\n    <string name=\"share_embed_link\">Дели уграђену везу</string>\n    <string name=\"card_text_scroll_factor\">Брзина клизања текста картице</string>\n    <string name=\"preferred_update_source\">Избор извора ажурирања</string>\n    <string name=\"hide_shorts\">Сакриј кратке у Претплатама</string>\n    <string name=\"hide_shorts_channel\">Сакриј кратке са канала</string>\n    <string name=\"key_remapping\">Мапирање тастера</string>\n    <string name=\"add_remove_from_playback_queue\">Додај/Уклони из реда за пуштање</string>\n    <string name=\"app_backup_restore\">Резервна копија/Враћање</string>\n    <string name=\"app_backup\">Сачувај податке</string>\n    <string name=\"app_restore\">Врати податке из архиве</string>\n    <string name=\"background_service_started\">Позадински сервис покренут</string>\n    <string name=\"dialog_add_to\">Додај у %s</string>\n    <string name=\"dialog_remove_from\">Уклони из %s</string>\n    <string name=\"added_to\">Додато у %s</string>\n    <string name=\"removed_from\">Уклоњено из %s</string>\n    <string name=\"video_preset_adaptive\">Прилагођено</string>\n    <string name=\"content_block_preview_recap\">Преглед или сажетак видеа</string>\n    <string name=\"content_block_highlight\">Поента или врхунац видеа</string>\n    <string name=\"upload_date_any\">све</string>\n    <string name=\"double_refresh_rate\">Двоструко освежавање</string>\n    <string name=\"focus_on_search_results\">Пређи на резултате претраге</string>\n    <string name=\"add_remove_from_recent_playlist\">Додај уклони са недавне листе</string>\n    <string name=\"hide_settings_section\">Сакриј одељак Поставке (опасно!)</string>\n    <string name=\"content_block_filler\">Ван теме (попуна)</string>\n    <string name=\"keyboard_auto_show\">Аутоматски прикажи тастатуру</string>\n    <string name=\"msg_player_error_source2\">Видео извор не ради или време није тачно</string>\n    <string name=\"msg_player_error_video_source\">Извор за видео не ради или време није тачно.\\nУглавном је грешка у апликацији.</string>\n    <string name=\"msg_player_error_audio_source\">Извор за звук не ради или време није тачно.\\nУглавном је грешка у апликацији.</string>\n    <string name=\"msg_player_error_subtitle_source\">Извор за титлове не ради или време није тачно.\\nУглавном је грешка у апликацији.</string>\n    <string name=\"hide_upcoming\">Сакриј најављене из Претплата</string>\n    <string name=\"hide_upcoming_channel\">Сакриј предстојеће са Канала</string>\n    <string name=\"hide_upcoming_home\">Сакриј предстојеће са Почетне</string>\n    <string name=\"search_background_playback\">Слика у слици при претрази/отварању канала</string>\n    <string name=\"proxy_type\">Тип проксија</string>\n    <string name=\"enable_web_proxy\">Користи веб прокси</string>\n    <string name=\"proxy_not_supported\">Користи веб прокси (захтева Андроид 4.4+)</string>\n    <string name=\"proxy_settings_title\">Поставке прокси сервера</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">Тест#%d: отказан.</string>\n    <string name=\"proxy_type_invalid\">Тип проксија није подешен</string>\n    <string name=\"proxy_host_invalid\">Име проксија није подешено</string>\n    <string name=\"proxy_port_invalid\">Неисправан порт проксија, мора &gt; 0.</string>\n    <string name=\"proxy_credentials_invalid\">Неисправно корисничко име или лозинка.</string>\n    <string name=\"proxy_test_aborted\">Тест прекинут. Прво поправите поставке проксија.</string>\n    <string name=\"proxy_application_aborted\">Исправите поставке проксија.</string>\n    <string name=\"proxy_test_start\">Тест#%d: %s …</string>\n    <string name=\"proxy_test_error\">Грешка#%d: %s</string>\n    <string name=\"proxy_test_status\">Статус#%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">Адреса за фајл подешавања (*.ovpn)</string>\n    <string name=\"enable_openvpn\">Користи ОпенВПН</string>\n    <string name=\"openvpn_settings_title\">Поставке за ОпенВПН</string>\n    <string name=\"openvpn_application_aborted\">Прво поправите поставке за ОпенВПН</string>\n    <string name=\"openvpn_test_aborted\">Тест прекинут. Поправите позтавке за ОпенВПН</string>\n    <string name=\"openvpn_address_invalid\">Адреса подешавања за ОпенВПН није постављена</string>\n    <string name=\"audio_sync_fix_desc\">Алтернативни начин синхронизације. Сматра се застарелим али некада може да помогне</string>\n    <string name=\"audio_sync_fix\">Сређивање аудио синх.</string>\n    <string name=\"ambilight_ratio_fix_desc\">Сређује недостајући ниво осветљења, размеру, празне снимке екрана. Утиче на перформансе!</string>\n    <string name=\"ambilight_ratio_fix\">Среди амби-светло/размеру/снимке екрана</string>\n    <string name=\"force_legacy_codecs_desc\">Значајно побољшава рад на слабијим уређајима. Највећа резолуција је 720п</string>\n    <string name=\"force_legacy_codecs\">За старије кодеке (720п)</string>\n    <string name=\"live_stream_fix_desc\">Значајно побољшава живо емитовање на слабијим уређајима. Највећа резолуција је 1080п</string>\n    <string name=\"live_stream_fix_4k_desc\">Значајно побољшава живо емитовање. Највећа резолуција је 4К</string>\n    <string name=\"live_stream_fix\">Среди живо емитовање (1080п)</string>\n    <string name=\"live_stream_fix_4k\">Среди живо емитовање (4К)</string>\n    <string name=\"playback_notifications_fix_desc\">Сакрива обавештење о промени нумере. Може користити код АОСП базираних фирмвера</string>\n    <string name=\"playback_notifications_fix\">Искључи обавештења о пуштању</string>\n    <string name=\"amlogic_fix_desc\">Сређује прескоке на „Amlogic“ базираним уређајима</string>\n    <string name=\"amlogic_fix\">Среди „Amlogic“ 1080\\@60</string>\n    <string name=\"tunneled_video_playback\">Тунелована видео репродукција (Андроид 5+)</string>\n    <string name=\"tunneled_video_playback_desc\">НАПОМЕНА: пауза можда неће радити! Тунелована репродукција обећава бољу синхронизацију и глаткију репродукцију. Захтева Андроид 5+</string>\n    <string name=\"remember_position_of_short_videos\">Памти позицију кратких видеа (&lt; 5 мин)</string>\n    <string name=\"remember_position_of_live_videos\">Памти позицију живог емитовања</string>\n    <string name=\"player_show_tooltips\">Прикажи описе дугмића</string>\n    <string name=\"action_like_unset\">Свиђање искључено</string>\n    <string name=\"action_dislike_unset\">Несвиђање искључено</string>\n    <string name=\"not_compatible_with\">Изабрана опција не иде са</string>\n    <string name=\"enter_value\">Унесите неку вредност</string>\n    <string name=\"dialog_add_remove_from\">Додај/уклони из %s</string>\n    <string name=\"lb_playback_controls_skip_next\">Скочи на следеће</string>\n    <string name=\"lb_playback_controls_skip_previous\">Скочи на претходно/премотај на почетак</string>\n    <string name=\"alt_presets_behavior\">Алтернатива преподешењу (ограничење протока)</string>\n    <string name=\"alt_presets_behavior_desc\">Апликација ће покушати да одржи прописани проток уместо да мења резолуцију, кадрове и кодек.</string>\n    <string name=\"sleep_timer\">Тајмер спавања (један сат)</string>\n    <string name=\"sleep_timer_desc\">Паузирај пуштање ако корисник сат времена није користио даљински</string>\n    <string name=\"disable_vsync\">Искључи „vsync“ праћење</string>\n    <string name=\"disable_vsync_desc\">Мало побољшава перформансе</string>\n    <string name=\"skip_codec_profile_check\">Прескочи проверу кодека</string>\n    <string name=\"skip_codec_profile_check_desc\">Не проверава подршку за кодек при покретању видеа. Корисно за проблематичним фирмверима.</string>\n    <string name=\"force_sw_codec\">Форсирај софтверски декодер</string>\n    <string name=\"force_sw_codec_desc\">Пуштаће скоро сваки видео али веома споро.</string>\n    <string name=\"owned_playlist_warning\">Грешка. Ово се може радити само на својим листама</string>\n    <string name=\"content_block_alt_server\">Користи алтернативни сервер</string>\n    <string name=\"content_block_alt_server_desc\">Укључите ако блоккирање спонзора не ради из неког разлога.</string>\n    <string name=\"unset_stream_reminder\">Искључи подсетник за емисију</string>\n    <string name=\"set_stream_reminder\">Укључи подсетник за емисију</string>\n    <string name=\"playback_starts_shortly\">Репродукција ће почети чим почне емитовање</string>\n    <string name=\"starting_stream\">Покрећем емисију…</string>\n    <string name=\"signin_view_title\">Кориснички код се учитава…</string>\n    <string name=\"signin_view_description\">Да се пријавите, унесите овај код на страни %s\\nПАЖЊА Ради само са Фајерфокс или Хром прегледачима.</string>\n    <string name=\"require_checked\">Захтева %s</string>\n    <string name=\"add_device_view_description\">Да бисте повезали уређај, унесите овај код у Јутуб апликацију на свом телефону у одељку Поставке/Гледај на ТВ\\nПАЖЊА Ради само са Гугл тастатуром.</string>\n    <string name=\"playback_controls_repeat_pause\">Не понављај</string>\n    <string name=\"playback_controls_repeat_list\">Понови листу</string>\n    <string name=\"action_subscribe_off\">Претплата искљ.</string>\n    <string name=\"action_subscribe_on\">Претплата укљ.</string>\n    <string name=\"skip_24_rate\">Прескочи формате са 24 кадра</string>\n    <string name=\"skip_shorts\">Прескочи Кратке</string>\n    <string name=\"prefer_avc_over_vp9\">Избор кодека: „avc“ пре „vp9“</string>\n    <string name=\"use_alt_speech_recognizer\">Алтернативно препознавање гласа</string>\n    <string name=\"use_alt_speech_recognizer_desc\">Озбиљно проблематично. Користите само ако имате проблем са подразумеваним.</string>\n    <string name=\"speech_recognizer\">Препознавање гласа</string>\n    <string name=\"speech_recognizer_external_1\">спољни 1 (озбиљно проблематичан, да би радио, морате искључити приступ микрофону)</string>\n    <string name=\"speech_recognizer_external_2\">спољни 2 (озбиљно проблематичан)</string>\n    <string name=\"disable_mic_permission\">Искључите апликацији приступ микрофону да би препознавање радило исправно.</string>\n    <string name=\"repeat_mode_shuffle\">Мешај сваку листу</string>\n    <string name=\"card_real_thumbnails\">Замени умањени приказ са кадром из видеа</string>\n    <string name=\"card_content\">Одакле узети умањени приказ</string>\n    <string name=\"thumb_quality_default\">подразумевано</string>\n    <string name=\"thumb_quality_start\">са почетка видеа</string>\n    <string name=\"thumb_quality_middle\">из средине</string>\n    <string name=\"thumb_quality_end\">са краја видеа</string>\n    <string name=\"content_block_status\">Провери статус сервера за блокирање спонзора</string>\n    <string name=\"dearrow_status\">Провери статус ДеАрров сервера</string>\n    <string name=\"player_show_quality_info_bitrate\">Додај проток у информације о квалитету</string>\n    <string name=\"player_speed_button_old_behavior\">Врати старо понашање дугмета за брзину</string>\n    <string name=\"protect_settings_with_password\">Заштити све поставке лозинком</string>\n    <string name=\"enter_settings_password\">Унесите лозинку за поставке</string>\n    <string name=\"child_mode\">Дечији режим</string>\n    <string name=\"child_mode_desc\">У овом режиму, корисник не може користити претрагу нити видети препоручени садржај. Страница са поставкама ће бити заштићена лозинком.</string>\n    <string name=\"lost_setting_warning\">Поставке апликације биће промењене. Будите сигурни да сте направили резервну копију поставки.</string>\n    <string name=\"player_button_long_click\">Задржите ово дугме за додатне поставке</string>\n    <string name=\"pause_history\">Паузирај историју</string>\n    <string name=\"resume_history\">Настави историју</string>\n    <string name=\"disable_history\">Искључи историју</string>\n    <string name=\"enable_history\">Укључи историју</string>\n    <string name=\"clear_history\">Обриши историју</string>\n    <string name=\"chapters\">Поглавља</string>\n    <string name=\"card_multiline_subtitle\">Вишелинијски титлови</string>\n    <string name=\"auto_frame_rate_modes\">Подржани режими</string>\n    <string name=\"loading\">Учитавам…</string>\n    <string name=\"hide_streams\">Сакриј жива емитовања са Претплата</string>\n    <string name=\"video_rotate\">Ротирај</string>\n    <string name=\"video_flip\">Обрни (огледало)</string>\n    <string name=\"trending_searches\">Претраге у тренду</string>\n    <string name=\"video_duration_any\">Све</string>\n    <string name=\"video_duration\">Трајање</string>\n    <string name=\"video_duration_under_4\">Испод 4 минута</string>\n    <string name=\"video_duration_between_4_20\">4–20 minutа</string>\n    <string name=\"video_duration_over_20\">Преко 20 минута</string>\n    <string name=\"content_type\">Тип</string>\n    <string name=\"content_type_any\">Све</string>\n    <string name=\"content_type_video\">Видео</string>\n    <string name=\"content_type_channel\">Канал</string>\n    <string name=\"content_type_playlist\">Листа</string>\n    <string name=\"content_type_movie\">Филм</string>\n    <string name=\"video_features\">Функције</string>\n    <string name=\"video_feature_any\">Све</string>\n    <string name=\"video_feature_live\">Живо емитовање</string>\n    <string name=\"video_feature_4k\">4К</string>\n    <string name=\"video_feature_hdr\">ХДР</string>\n    <string name=\"search_sorting\">Поређај према</string>\n    <string name=\"sort_by_relevance\">Релевантност</string>\n    <string name=\"sort_by_views\">Броју прегледа</string>\n    <string name=\"sort_by_date\">Датуму отпремања</string>\n    <string name=\"sort_by_rating\">Оцена</string>\n    <string name=\"clear_search_history\">Обриши историју гледања</string>\n    <string name=\"remove_from_subscriptions\">Сакриј</string>\n    <string name=\"player_long_speed_list\">Дуга листа брзина</string>\n    <string name=\"player_extra_long_speed_list\">Екстра дуга листа брзина</string>\n    <string name=\"alt_app_icon\">Алтернативна икона апликације (потребно је поновно покретање)</string>\n    <string name=\"network_stack\">Дај предност %s мрежном стеку</string>\n    <string name=\"player_network_stack\">Мрежни мотор</string>\n    <string name=\"cronet_desc\">Кронет је Хромиум мрежни стек. Кронет може смањити кашњење и побољшати перформансе мреже, што може помоћи са проблемима баферовања.</string>\n    <string name=\"unlock_all_formats\">Откључај све видео формате</string>\n    <string name=\"unlock_all_formats_desc\">На неким уређајима фирмвер нетачно пријављује да одређени формати нису подржани, иако јесу.\\n(нпр. 1080п паметни телевизори често пријављују да 4к није подржан).</string>\n    <string name=\"okhttp_desc\">Овај мрежни мотор је најспорији али има добру стабилност.</string>\n    <string name=\"select_channel_section\">Отвори одговарајући одељак када се апликација покрене са ATV канала</string>\n    <string name=\"enable_voice_search_desc\">Званична апликација биће замењена такозваним \\\"мостом\\\". Мост помаже да се глобални захтеви претраге проследе нашој апликацији.</string>\n    <string name=\"enable_master_password\">Укључи главну лозинку</string>\n    <string name=\"enter_master_password\">Унесите главну лозинку</string>\n    <string name=\"disable_stream_buffer\">Искључи бафер на живим емитовањима</string>\n    <string name=\"disable_stream_buffer_desc\">Сређује ситуације када живо емитовање превише заостаје. Напомена: живо емитовање може почети да сецка.</string>\n    <string name=\"sony_frame_drop_fix\">Сређује пад кадрова #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">Сређује сецкање на Сони телевизорима и неким другим уређајима. Напомена: могући проблеми са синхронизацијом звука.</string>\n    <string name=\"audio_language\">Језик звука</string>\n    <string name=\"old_home_look\">Стари излед одељка Почетна</string>\n    <string name=\"old_channel_look\">Стари изглед странице Канал</string>\n    <string name=\"hide_shorts_everywhere\">Сакриј кратке свугде</string>\n    <string name=\"update_found\">Ажурирање</string>\n    <string name=\"volume_boost_warning\">Свака поставка изнад границе може наштетити звучнике</string>\n    <string name=\"play_video_incognito\">Пусти тајно</string>\n    <string name=\"play_from_start\">Пусти од почетка</string>\n    <string name=\"header_kids_home\">Деца</string>\n    <string name=\"player_section_playlist\">Користи садржај тренутног одељка као листу</string>\n    <string name=\"header_trending\">У тренду</string>\n    <string name=\"screensaver\">Чувар екрана</string>\n    <string name=\"player_screen_off_timeout\">Време до искључења екрана</string>\n    <string name=\"old_update_notifications\">Стари изглед обавештења ажурирања</string>\n    <string name=\"dialog_notification\">Обавести о ажурирањима помоћу искачућег дијалога</string>\n    <string name=\"mark_as_watched\">Означи као одгледано</string>\n    <string name=\"player_ui_animations\">Укључи анимације сучеља плејера</string>\n    <string name=\"player_likes_count\">Прикажи број свиђања/несвиђања</string>\n    <string name=\"sorting_alphabetically2\">Абецедно (брзо, са анимираним прегледима)</string>\n    <string name=\"sorting_default\">Подразумевано (брзо, са анимираним прегледима)</string>\n    <string name=\"content_block_exclude_channel\">Искључи овај канал са СпонзорБлока-а</string>\n    <string name=\"content_block_stop_excluding_channel\">Прекини искључивање овог канала са СпонзорБлок-а</string>\n    <string name=\"player_chapter_notification\">Брзо прелазите кроз поглавља помођу искачућег обавештења</string>\n    <string name=\"player_chapter_notification2\">Пређите на следеће поглавље кликом на обавештење</string>\n    <string name=\"subtitle_remember\">Укључи титлове само на тренутном каналу</string>\n    <string name=\"amazon_frame_drop_fix\">Сређује пад кадрова #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">Намењено за Амазон Стик уређаје. Можда може радити и на другим уређајима.</string>\n    <string name=\"default_stack_desc\">Уграђени мрежни мотор. Овај мотор може имати бољу стабилност у одређеним ситуацијама.</string>\n    <string name=\"item_postion\">Позиција</string>\n    <string name=\"player_screen_off_dimming\">Степен затамњења при искључењу екрана</string>\n    <string name=\"screensaver_timout\">Време до укључења чувара екрана</string>\n    <string name=\"screensaver_dimming\">Степен затамњења чувара екрана</string>\n    <string name=\"player_ui_on_next\">Прикажи сучеље плејера при преласку на следећи видео</string>\n    <string name=\"autogenerated\">ауто-генерисано</string>\n    <string name=\"player_auto_volume\">Аутоматско подешавање јачине звука</string>\n    <string name=\"header_shorts\">Кратке</string>\n    <string name=\"player_global_focus\">Несметана навигација кроз редове дугмади плејера</string>\n    <string name=\"auto_history\">Аутоматски (користи поставке налога)</string>\n    <string name=\"remember_position_subscriptions\">Памти последњу гледану позицију у Претплатама</string>\n    <string name=\"remember_position_pinned\">Памти последњу гледану позицију у закаченим листама</string>\n    <string name=\"msg_player_unknown_error\">Непозната грешка</string>\n    <string name=\"unknown_source_error\">Непозната грешка извора</string>\n    <string name=\"unknown_renderer_error\">Непозната грешка рендерера</string>\n    <string name=\"header_notifications\">Обавештења</string>\n    <string name=\"disable_search_history\">Искључи историју претраге</string>\n    <string name=\"unlock_high_bitrate_formats\">Откључај 1080п вп9 формате високог протока</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">Откључај мп4а формате високог протока</string>\n    <string name=\"color_scheme_blue\">Плава</string>\n    <string name=\"color_scheme_dark_blue\">Тамно плава</string>\n    <string name=\"player_speed_per_channel\">За сваки канал посебно</string>\n    <string name=\"disable_popular_searches\">Искључи популарне претраге</string>\n    <string name=\"multi_profiles\">Користи посебне поставке за сваки налог</string>\n    <string name=\"protect_account_with_password\">Заштити овај налог лозинком</string>\n    <string name=\"enter_account_password\">Унесите лозинку за налог</string>\n    <string name=\"show_connect_messages\">Прикажи поруке о повезивању</string>\n    <string name=\"prefer_avc_over_vp9_desc\">УПОЗОРЕЊЕ: максимално 1080п</string>\n    <string name=\"play_next\">Пусти следеће</string>\n    <string name=\"hide_watched_from_subscriptions\">Сакриј одгледане видее са Претплата</string>\n    <string name=\"hide_watched_from_notifications\">Сакриј одгледане видее са Обавештења</string>\n    <string name=\"hide_unwanted_content\">Сакриј видее</string>\n    <string name=\"hide_watched_from_home\">Сакриј одгледане видее са Почетне</string>\n    <string name=\"hide_watched_from_watch_later\">Сакриј одгледане видее са листе Гледај касније</string>\n    <string name=\"remote_control_permission\">Даљинско управљање у позадини захтева дозволу за преклапање</string>\n    <string name=\"login_from_browser\">Пријави се преко прегледача</string>\n    <string name=\"disable_remote_history\">Искључи историју при коришћењу даљинског управљача</string>\n    <string name=\"keyboard_fix\">Спречи ОК дугме да отвара тастатуру (Г20с и други)</string>\n    <string name=\"nothing_found\">Ништа није нађено</string>\n    <string name=\"auto_frame_rate_desc\">Ова поставка уклања трзање у сценама са брзим кретањем камере, нпр. спортска жива емитовања</string>\n    <string name=\"channel_filter_hint\">Филтрирај канале</string>\n    <string name=\"player_loop_shorts\">Понављај Кратке</string>\n    <string name=\"player_quick_shorts_skip\">Прекочи Кратке левим/десним дугметима</string>\n    <string name=\"player_quick_skip_videos\">Прекочи обичне видее левим/десним дугметима</string>\n    <string name=\"channels_filter\">Прикажи поље Филтрирај канале унутар одељка Канала</string>\n    <string name=\"channel_search_bar\">Трака за претрагу унутар странице Канала</string>\n    <string name=\"header_sports\">Спортови</string>\n    <string name=\"keep_finished_activities\">Задржи завршене активности</string>\n    <string name=\"disable_channels_service\">Искључи сервис Канала</string>\n    <string name=\"replace_titles\">Замени наслове</string>\n    <string name=\"enable\">Укључи</string>\n    <string name=\"more_info\">Више информација</string>\n    <string name=\"about_sponsorblock\">О СпонзорБлок-у</string>\n    <string name=\"about_dearrow\">О ДеАрров-у</string>\n    <string name=\"replace_thumbnails\">Замени umaњене приказе</string>\n    <string name=\"crowdsourced_thumbnails\">Умањени прикази из заједнице</string>\n    <string name=\"crowdsoursed_titles\">Титлови из заједнице</string>\n    <string name=\"dearrow_not_submitted_thumbs\">Извор умањених приказа (ако нема ДеАрров података)</string>\n    <string name=\"pitch_effect\">Ефекат висине тона</string>\n    <string name=\"fullscreen_mode\">Режим целог екрана (без системских трака)</string>\n    <string name=\"player_only_mode\">Прикажи само плејер ако је видео отворен изван апликације</string>\n    <string name=\"pinned_channel_rows\">Прикажи закачене канале као редове</string>\n    <string name=\"prefer_ipv4\">Дај предност ИПв4 ДНС-у</string>\n    <string name=\"prefer_ipv4_desc\">Може средити ситуације када апликација уопште не ради.\\nНапомена: Може изазвати заглављивања и падове (посебно на Андроид 8 уређајима или Дуне ХД)</string>\n    <string name=\"long_press_for_settings\">ЗАДРЖИ ЗА ПОСТАВКЕ</string>\n    <string name=\"long_press_for_options\">ЗАДРЖИ ЗА ОПЦИЈЕ</string>\n    <string name=\"device_specific_backup\">Резервна копија само за овај уређај</string>\n    <string name=\"local_backup\">Локална резервна копија</string>\n    <string name=\"auto_backup\">Аутоматска резервна копија (једном дневно)</string>\n    <string name=\"repeat_mode_reverse_list\">Пусти видее са листа или канала обрнутим редоследом</string>\n    <string name=\"calm_msg\">Сређујемо проблем. Проверавај ажурирања с времена на време</string>\n    <string name=\"without_picture\">Без слике</string>\n    <string name=\"video_disabled\">Видео је искључен</string>\n    <string name=\"applying_fix\">Не затврај плејер. Примењујем поправку…</string>\n    <string name=\"hide_mixes\">Сакриј Миксове</string>\n    <string name=\"player_global_focus_desc\">Ова функција утиче на то које дугме плејера ће добити фокус при кретању између редова дугмади плејера</string>\n    <string name=\"disable_network_error_fixing\">Искључи аутоматско сређивање мрежног проблема</string>\n    <string name=\"disable_network_error_fixing_desc\">Вероватно мораш укључити ову опцију ако користиш ВПН</string>\n    <string name=\"recommended\">Препоручено</string>\n    <string name=\"add_to_subscriptions_group\">Додај/Уклони са групе претплата</string>\n    <string name=\"new_subscriptions_group\">Нова група</string>\n    <string name=\"rename_group\">Преименуј групу претплата</string>\n    <string name=\"screen_dimming_amount\">Степен затамњења екрана</string>\n    <string name=\"screen_dimming_timeout\">Време до затамњења екрана</string>\n    <string name=\"playlists_rows\">Прикажи одељак Листе као редове</string>\n    <string name=\"import_subscriptions_group\">Увези</string>\n    <string name=\"lb_playback_controls_closed_captioning_disable\">Искључи титлове</string>\n    <string name=\"lb_playback_controls_closed_captioning_enable\">Укључи титлове</string>\n    <string name=\"my_videos\">Моји видеи</string>\n    <string name=\"player_audio_focus\">Фокус звука (паузирај ако се открију други плејери)</string>\n    <string name=\"paid_content_notification\">Обавештење о плаћеном садржају</string>\n    <string name=\"you_liked\">свиђа вам се</string>\n    <string name=\"premium_users_only\">Само за премијум кориснике. Сређивање за непотпуну листу видео формата</string>\n    <string name=\"playback_buffering_fix\">Сређивање баферовања при пуштању</string>\n    <string name=\"oculus_quest_fix\">Окулус Квест сређивање</string>\n    <string name=\"card_preview_muted\">Видео без звука</string>\n    <string name=\"card_preview_full\">Видео са звуком</string>\n    <string name=\"card_preview\">Преглед картице</string>\n    <string name=\"card_unlocalized_titles\">Наслови видеа без превода</string>\n    <string name=\"dont_resize_video_to_fit_dialog\">Не мењај величину видеа да би се уклопио у дијалог</string>\n    <string name=\"typing_corrections\">Онемогући аутоматско исправљање приликом куцања текста</string>\n    <string name=\"suggestions_horizontally_scrolled\">Хоризонтално померани предлози</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-sv/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Hem</string>\n  <string name=\"title_search\">Sök</string>\n  <string name=\"header_subscriptions\">Prenumerationer</string>\n  <string name=\"header_history\">Historik</string>\n  <string name=\"header_music\">Musik</string>\n  <string name=\"header_news\">Nyheter</string>\n  <string name=\"header_gaming\">Gaming</string>\n  <string name=\"header_playlists\">Spellistor</string>\n  <string name=\"header_settings\">Inställningar</string>\n  <string name=\"header_channels\">Kanaler</string>\n  <string name=\"subscriptions_signin_title\">Se det senaste från kanaler du älskar</string>\n  <string name=\"subscriptions_signin_subtitle\">Logga in för att se dina prenumerationer</string>\n  <string name=\"subscriptions_signin_button_text\">LOGGA IN</string>\n  <string name=\"library_signin_to_show_more\">Titta på videor du har gillat, sparat eller som du prenumererar på</string>\n  <string name=\"library_signin_subtitle\">Logga in för att se ditt bibliotek</string>\n  <string name=\"action_signin\">LOGGA IN</string>\n  <string name=\"title_video_formats\">Videoformat</string>\n  <string name=\"title_audio_formats\">Ljudformat</string>\n  <string name=\"subtitle_category_title\">Undertexter</string>\n  <string name=\"playback_queue_category_title\">Uppspelningskö</string>\n  <string name=\"video_max_quality\">Automatisk (maximal kvalitet)</string>\n  <string name=\"audio_max_quality\">Automatisk (maximal kvalitet)</string>\n  <string name=\"subtitles_disabled\">Undertexter inaktiverade</string>\n  <string name=\"auto_frame_rate\">Automatisk bildfrekvens</string>\n  <string name=\"frame_rate_correction\">Korrigera bildfrekvens:\\n%s</string>\n  <string name=\"category_background_playback\">Bakgrundsuppspelning</string>\n  <string name=\"not_implemented\">Inte implementerad</string>\n  <string name=\"not_supported_by_device\">Enheten har inte stöd för denna funktion</string>\n  <string name=\"option_background_playback_off\">Inaktiverad</string>\n  <string name=\"option_background_playback_pip\">Bild-i-bild</string>\n  <string name=\"option_background_playback_only_audio\">Endast ljud</string>\n  <string name=\"playback_settings\">Inställningar för uppspelningskvalitet</string>\n  <string name=\"video_speed\">Videohastighet</string>\n  <string name=\"resolution_switch\">Byt upplösning</string>\n  <string name=\"video_buffer\">Videobuffert</string>\n  <string name=\"video_buffer_size_none\">Ingen</string>\n  <string name=\"video_buffer_size_lowest\">Lägst</string>\n  <string name=\"video_buffer_size_low\">Låg</string>\n  <string name=\"video_buffer_size_med\">Medelhög</string>\n  <string name=\"video_buffer_size_high\">Hög</string>\n  <string name=\"video_buffer_size_highest\">Högst</string>\n  <string name=\"update_changelog\">Ändringslogg</string>\n  <string name=\"install_update\">Installera uppdatering</string>\n  <string name=\"section_is_empty\">Oj! Det finns inget här</string>\n  <string name=\"dialog_add_to_playlist\">Lägg till/ta bort från spellista</string>\n  <string name=\"msg_signed_users_only\">Endast inloggade användare</string>\n  <string name=\"msg_cant_load_content\">Det går inte att läsa in innehållet.\\n1) Aktivera visningshistoriken.\\n2) Kontrollera datum och klockslag på enheten.\\n3) Kontrollera nätverksanslutningen.\\n4) Kontrollera dina proxyinställningar.\\n5) Testa att logga in på ditt konto.\\n6) Det kanske inte finns något här.</string>\n  <string name=\"title_video_presets\">Videoförinställningar</string>\n  <string name=\"settings_accounts\">Konton</string>\n  <string name=\"settings_left_panel\">Redigera kategorier</string>\n  <string name=\"settings_themes\">Teman</string>\n  <string name=\"settings_other\">Övrigt</string>\n  <string name=\"settings_player\">Spelare</string>\n  <string name=\"settings_language\">Språk</string>\n  <string name=\"settings_linked_devices\">Länkade enheter</string>\n  <string name=\"settings_about\">Om</string>\n  <string name=\"dialog_account_list\">Välj konto</string>\n  <string name=\"dialog_account_none\">Inget</string>\n  <string name=\"dialog_remove_account\">Logga ut</string>\n  <string name=\"dialog_add_account\">Logga in</string>\n  <string name=\"default_lang\">Standard</string>\n  <string name=\"original_lang\">Ursprungligt</string>\n  <string name=\"dialog_select_language\">Språk</string>\n  <string name=\"subtitle_default\">Standard</string>\n  <string name=\"subtitle_white_semi_transparent\">Vitt på halvgenomskinlig bakgrund</string>\n  <string name=\"subtitle_style\">Undertextstil</string>\n  <string name=\"subtitle_language\">Språk för undertexter</string>\n  <string name=\"action_search\">Sök</string>\n  <string name=\"settings_main_ui\">Användargränssnitt</string>\n  <string name=\"dialog_main_ui\">Användargränssnitt</string>\n  <string name=\"card_animated_previews\">Animerade förhandsvisningar</string>\n  <string name=\"web_site\">Webbplats</string>\n  <string name=\"donation\">Donation</string>\n  <string name=\"dialog_about\">Om</string>\n  <string name=\"dialog_player_ui\">Videospelare</string>\n  <string name=\"player_show_ui_on_pause\">Visa användargränssnittet vid paus</string>\n  <string name=\"player_pause_on_ok\">OK-knappen pausar uppspelningen</string>\n  <string name=\"player_ok_button_behavior\">OK-knappens beteende</string>\n  <string name=\"player_only_ui\">Endast användargränssnittet</string>\n  <string name=\"player_ui_and_pause\">Användargränssnittet och pausa</string>\n  <string name=\"player_only_pause\">Endast pausa</string>\n  <string name=\"player_toggle_speed\">Växla hastighet mellan på och av</string>\n  <string name=\"check_for_updates\">Sök efter uppdateringar</string>\n  <string name=\"update_not_found\">Du använder den senaste versionen</string>\n  <string name=\"update_in_progress\">Vänta …</string>\n  <string name=\"player_ui_hide_behavior\">Dölj användargränssnittet automatiskt</string>\n  <string name=\"option_never\">Aldrig</string>\n  <string name=\"side_panel_sections\">Visa/dölj avsnitt</string>\n  <string name=\"boot_to_section\">Starta i avsnitt</string>\n  <string name=\"large_ui\">Stort användargränssnitt</string>\n  <string name=\"video_grid_scale\">Skala för videorutnät</string>\n  <string name=\"scale_ui\">Skala för användargränssnitt</string>\n  <string name=\"color_scheme\">Färgschema</string>\n  <string name=\"color_scheme_default\">Standard</string>\n  <string name=\"color_scheme_red_grey\">Rödgrått</string>\n  <string name=\"color_scheme_red\">Rött</string>\n  <string name=\"color_scheme_dark_grey\">Mörkgrått</string>\n  <string name=\"disable_update_check\">Inaktivera sökning efter uppdateringar</string>\n  <string name=\"show_again\">Visa igen</string>\n  <string name=\"check_updates_auto\">Avisera om uppdateringar</string>\n  <string name=\"select_account_on_boot\">Välj konto vid start</string>\n  <string name=\"player_other\">Övrigt</string>\n  <string name=\"player_full_date\">Exakt datum i beskrivningen</string>\n  <string name=\"open_channel\">Öppna kanalen</string>\n  <string name=\"open_playlist\">Öppna spellistan</string>\n  <string name=\"not_interested\">Inte intresserad</string>\n  <string name=\"not_recommend_channel\">Rekommendera inte kanalen</string>\n  <string name=\"you_wont_see_this_video\">Videon har tagits bort från rekommendationerna</string>\n  <string name=\"you_wont_see_this_channel\">Du kommer inte att se den här kanalen i rekommendationerna</string>\n  <string name=\"settings_search\">Sök</string>\n  <string name=\"dialog_search\">Sök</string>\n  <string name=\"instant_voice_search\">Direkt röstsökning</string>\n  <string name=\"option_background_playback_behind\">Spela upp i bakgrunden</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">Info om nästa video har inte lästs in än</string>\n  <string name=\"card_multiline_title\">Titlar med flera rader</string>\n  <string name=\"cards_style\">Kortstil</string>\n  <string name=\"repeat_mode_all\">Spela upp videor kontinuerligt</string>\n  <string name=\"repeat_mode_one\">Upprepa den aktuella videon</string>\n  <string name=\"repeat_mode_pause\">Pausa uppspelningen efter varje video (utom i kön)</string>\n  <string name=\"repeat_mode_pause_alt\">Spela upp endast videor i spellistor kontinuerligt</string>\n  <string name=\"repeat_mode_none\">Stoppa uppspelningen efter en video (utom i kön)</string>\n  <string name=\"subscribed_to_channel\">Prenumererar</string>\n  <string name=\"unsubscribed_from_channel\">Prenumererar inte</string>\n  <string name=\"subtitle_yellow_transparent\">Gult på genomskinlig bakgrund</string>\n  <string name=\"subtitle_white_transparent\">Vitt på genomskinlig bakgrund</string>\n  <string name=\"subtitle_white_black\">Vitt på svart bakgrund</string>\n  <string name=\"player_seek_preview\">Förhandsgranskning under sökning</string>\n  <string name=\"color_scheme_dark_grey_oled\">Mörkgrått (OLED)</string>\n  <string name=\"channels_section_sorting\">Sortera avsnittet Kanaler</string>\n  <string name=\"sorting_by_new_content\">Nytt innehåll</string>\n  <string name=\"sorting_alphabetically\">Alfabetiskt</string>\n  <string name=\"sorting_last_viewed\">Senast visat</string>\n  <string name=\"player_pause_when_seek\">Pausa under sökning</string>\n  <string name=\"playlists_style\">Stil för spellistor</string>\n  <string name=\"playlists_style_grid\">Rutnät</string>\n  <string name=\"playlists_style_rows\">Rader</string>\n  <string name=\"player_show_clock\">Visa klocka i fältet för uppspelningsknappar</string>\n  <string name=\"player_show_remaining_time\">Visa den återstående tiden i fältet för uppspelningsknappar</string>\n  <string name=\"player_show_ending_time\">Visa när videon slutar i fältet för uppspelningsknappar</string>\n  <string name=\"open_channel_uploads\">Öppna kanalens uppladdningar</string>\n  <string name=\"app_exit_shortcut\">Avsluta appen</string>\n  <string name=\"player_exit_shortcut\">Avsluta spelaren</string>\n  <string name=\"search_exit_shortcut\">Avsluta sökning</string>\n  <string name=\"app_exit_none\">Avsluta inte</string>\n  <string name=\"app_double_back_exit\">Tryck två gånger på bakåtknappen</string>\n  <string name=\"app_single_back_exit\">Tryck en gång på bakåtknappen</string>\n  <string name=\"action_video_zoom\">Videozoomning</string>\n  <string name=\"video_zoom\">Videozoomning</string>\n  <string name=\"video_zoom_default\">Standard</string>\n  <string name=\"video_zoom_fit_width\">Anpassa till bredd</string>\n  <string name=\"video_zoom_fit_height\">Anpassa till höjd</string>\n  <string name=\"video_zoom_fit_both\">Anpassa både till bredd och höjd</string>\n  <string name=\"video_zoom_stretch\">Dra ut</string>\n  <string name=\"color_scheme_teal\">Turkost</string>\n  <string name=\"color_scheme_teal_oled\">Turkost (OLED)</string>\n  <string name=\"player_seek_preview_none\">Inaktiverad</string>\n  <string name=\"player_seek_preview_single\">En bild</string>\n  <string name=\"player_seek_preview_carousel\">Snurra</string>\n  <string name=\"player_seek_preview_carousel_slow\">Snurra (långsam, baserad på nyckelbildrutor)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Snurra (snabb, inte exaxt förhandsgranskning)</string>\n  <string name=\"unsubscribe_from_channel\">Avsluta prenumeration på kanalen</string>\n  <string name=\"card_title_lines_num\">Antal rader för titlar i kort</string>\n  <string name=\"card_auto_scrolled_title\">Rulla beskuren titel automatiskt</string>\n  <string name=\"share_link\">Dela länk</string>\n  <string name=\"share_qr_link\">Dela länk (QR-kod)</string>\n  <string name=\"subscribe_to_channel\">Prenumerera på kanalen</string>\n  <string name=\"wait_data_loading\">Vänta medan data läses in …</string>\n  <string name=\"auto_frame_rate_pause\">Automatisk bildfrekvens (paus vid växling)</string>\n  <string name=\"auto_frame_rate_applying\">Tillämpar automatisk bildfrekvens %sx%s\\@%s</string>\n  <string name=\"audio_shift\">Flytta ljud</string>\n  <string name=\"player_remember_speed\">Kom ihåg hastighet</string>\n  <string name=\"mark_channel_as_watched\">Markera som visad</string>\n  <string name=\"channel_marked_as_watched\">Kanalen har markerats som visad</string>\n  <string name=\"dialog_add_device\">Lägg till en enhet</string>\n  <string name=\"dialog_remove_all_devices\">Ta bort alla enheter</string>\n  <string name=\"device_link_enabled\">Länk aktiverad</string>\n  <string name=\"device_connected\">Enheten \\\"%s\\\" har anslutits</string>\n  <string name=\"device_disconnected\">Enheten \\\"%s\\\" har kopplats från</string>\n  <string name=\"settings_ui_scale\">Skala för användargränssnitt</string>\n  <string name=\"msg_restart_app\">Starta om appen för att tillämpa inställningarna</string>\n  <string name=\"settings_block\">Innehållsblockering</string>\n  <string name=\"msg_applying\">Tillämpar %s</string>\n  <string name=\"msg_done\">Klart</string>\n  <string name=\"settings_general\">Allmänt</string>\n  <string name=\"settings_video\">Video</string>\n  <string name=\"content_block_confirm_skip\">Bekräfta vid överhoppning</string>\n  <string name=\"content_block_categories\">Hoppa över segment</string>\n  <string name=\"content_block_sponsor\">Sponsor</string>\n  <string name=\"content_block_intro\">Uppehåll/introanimation</string>\n  <string name=\"content_block_outro\">Slutkort/eftertexter</string>\n  <string name=\"content_block_interaction\">Interaktionspåminnelse (prenumerera)</string>\n  <string name=\"content_block_self_promo\">Obetald reklam/egenreklam</string>\n  <string name=\"content_block_music_off_topic\">Icke-musikavsnitt av klipp</string>\n  <string name=\"confirm_segment_skip\">Hoppa över segmentet \\\"%s\\\"\\?</string>\n  <string name=\"cancel_segment_skip\">Avbryt överhoppning av segment</string>\n  <string name=\"msg_skipping_segment\">Hoppar över segmentet \\\"%s\\\" …</string>\n  <string name=\"content_block_notification_type\">Aviseringstyp</string>\n  <string name=\"content_block_notify_none\">Utan avisering</string>\n  <string name=\"content_block_notify_toast\">Popup-meddelande</string>\n  <string name=\"content_block_notify_dialog\">Dialogruta för att bekräfta</string>\n  <string name=\"return_to_launcher\">Återgå till appstartaren från ATV-kanaler/sökning</string>\n  <string name=\"intent_force_close\">Tvångsavsluta om videon har öppnats från extern källa</string>\n  <string name=\"btn_confirm\">Bekräfta</string>\n  <string name=\"player_low_video_quality\">Låg videokvalitet</string>\n  <string name=\"remote_session_closed\">Fjärrsessionen har stängts</string>\n  <string name=\"msg_mode_switch_error\">Det går inte att byta visningsläge till \\\"%s\\\"</string>\n  <string name=\"msg_player_error\">Det uppstod ett fel med spelaren %s. Uppspelningen startas om …</string>\n  <string name=\"video_preset_disabled\">Utan förinställning</string>\n  <string name=\"video_preset_enabled\">Formatet för nästa video ställs in enligt förinställningen</string>\n  <string name=\"player_sleep_timer\">Sovtimer</string>\n  <string name=\"player_show_quality_info\">Visa kvalitetsinfo i fältet för uppspelningsknappar</string>\n  <string name=\"player_remember_each_speed\">Kom ihåg hastighet för varje video</string>\n  <string name=\"player_remember_speed_none\">Ingen</string>\n  <string name=\"player_remember_speed_all\">Samma för alla videor</string>\n  <string name=\"player_remember_speed_each\">För varje video</string>\n  <string name=\"msg_player_error_source\">Det går inte att ladda ned videon</string>\n  <string name=\"msg_player_error_renderer\">Det valda formatet stöds inte.\\nTesta att välja ett annat.\\Om det inte hjälper kan du testa att starta om enheten.</string>\n  <string name=\"msg_player_error_video_renderer\">Det valda formatet för VIDEO stöds inte.\\nTesta att välja ett annat genom att trycka på HQ-knappen i spelaren.\\nOm det inte hjälper kan du testa att starta om enheten.</string>\n  <string name=\"msg_player_error_audio_renderer\">Det valda formatet för LJUD stöds inte.\\nTesta att välja ett annat genom att trycka på HQ-knappen i spelaren.\\nOm det inte hjälper kan du testa att starta om enheten.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">Det valda formatet för UNDERTEXTER stöds inte.\\nTesta att välja ett annat genom att trycka på CC-knappen i spelaren.\\nOm det inte hjälper kan du testa att starta om enheten.</string>\n  <string name=\"msg_player_error_unexpected\">Okänt fel med videoavkodaren</string>\n  <string name=\"video_aspect\">Bildformat</string>\n  <string name=\"player_tweaks\">Utvecklaralternativ</string>\n  <string name=\"header_uploads\">Uppladdningar</string>\n  <string name=\"player_show_global_clock\">Visa klockan hela tiden</string>\n  <string name=\"player_show_global_ending_time\">Visa sluttiden för videon hela tiden</string>\n  <string name=\"app_corner_clock\">Hem: klocka högst upp till höger</string>\n  <string name=\"player_corner_clock\">Spelare: klocka högst upp till höger</string>\n  <string name=\"player_corner_ending_time\">Spelare: sluttid högst upp till höger</string>\n  <string name=\"uploads_old_look\">Återställ det gamla utseendet för avsnittet Uppladdningar</string>\n  <string name=\"background_playback_activation\">Bakgrundsuppspelning (aktiverad)</string>\n  <string name=\"channels_old_look\">Återställ det gamla utseendet för avsnittet Kanaler</string>\n  <string name=\"channels_auto_load\">Läs in innehållet automatiskt i avsnittet Kanaler</string>\n  <string name=\"return_to_background_video\">Återgå till videon som spelas upp i bakgrunden</string>\n  <string name=\"pin_unpin_from_sidebar\">Lägg till/ta bort från sidofältet</string>\n  <string name=\"pin_unpin_playlist\">Lägg till/ta bort spellista från sidofältet</string>\n  <string name=\"pin_unpin_channel\">Lägg till/ta bort kanal från sidofältet</string>\n  <string name=\"pin_playlist\">Lägg till spellistan i sidofältet</string>\n  <string name=\"pin_channel\">Lägg till kanalen i sidofältet</string>\n  <string name=\"pinned_to_sidebar\">Tillagd i sidofältet</string>\n  <string name=\"unpin_from_sidebar\">Ta bort från sidofältet</string>\n  <string name=\"unpin_group_from_sidebar\">Ta bort prenumerationsgruppen</string>\n  <string name=\"unpinned_from_sidebar\">Borttagen från sidofältet</string>\n  <string name=\"dialog_select_country\">Land</string>\n  <string name=\"settings_language_country\">Språk/land</string>\n  <string name=\"share_embed_link\">Dela inbäddningslänk</string>\n  <string name=\"card_text_scroll_factor\">Rullningshastighet för korttext</string>\n  <string name=\"preferred_update_source\">Välj källa för uppdateringar</string>\n  <string name=\"hide_shorts\">Dölj Shorts-videor i Prenumerationer</string>\n  <string name=\"hide_shorts_channel\">Dölj Shorts-videor i kanaler</string>\n  <string name=\"key_remapping\">Konfigurera knappar</string>\n  <string name=\"screen_dimming\">Skärmnedtoning</string>\n  <string name=\"removed_from_playback_queue\">Borttagen från uppspelningskön</string>\n  <string name=\"added_to_playback_queue\">Tillagd i uppspelningskön</string>\n  <string name=\"add_remove_from_playback_queue\">Lägg till/ta bort från uppspelningskön</string>\n  <string name=\"add_to_playback_queue\">Lägg till i uppspelningskön</string>\n  <string name=\"remove_from_playback_queue\">Ta bort från uppspelningskön</string>\n  <string name=\"proxy_enabled\">Proxy aktiverad</string>\n  <string name=\"proxy_disabled\">Proxy inaktiverad</string>\n  <string name=\"uploads_row_name\">Uppladdningar</string>\n  <string name=\"playlists_row_name\">Skapade spellistor</string>\n  <string name=\"popular_uploads_row_name\">Populära uppladdningar</string>\n  <string name=\"news_row_name\">Nyheter</string>\n  <string name=\"breaking_news_row_name\">Senaste nytt</string>\n  <string name=\"covid_news_row_name\">Nyheter om COVID-19</string>\n  <string name=\"enable_voice_search\">Aktivera global sökning (kräver att den fasta programvaran har stöd för det)</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Prenumerera/avsluta prenumeration på kanalen</string>\n  <string name=\"app_backup_restore\">Säkerhetskopiera/återställ</string>\n  <string name=\"app_backup\">Säkerhetskopiera appdata</string>\n  <string name=\"app_restore\">Återställ appdata</string>\n  <string name=\"skip_each_segment_once\">Hoppa inte över segment igen</string>\n  <string name=\"disable_ok_long_press\">Inaktivera långt tryck på OK-knappen</string>\n  <string name=\"disable_ok_long_press_desc\">Avsedd för trasiga kontroller där OK-knappen inte fungerar som den ska</string>\n  <string name=\"player_time_correction\">Visa tiden med hastighetskorrigering</string>\n  <string name=\"sponsor_color_markers\">Färgmarkörer på förloppsindikatorn</string>\n  <string name=\"refresh_section\">Uppdatera avsnittet</string>\n  <string name=\"option_disabled\">Inaktiverat</string>\n  <string name=\"live_now_row_name\">Live nu</string>\n  <string name=\"run_in_background\">Bakgrundsuppspelning</string>\n  <string name=\"settings_remote_control\">Fjärrstyrning</string>\n  <string name=\"background_service_started\">Bakgrundstjänst har startat</string>\n  <string name=\"dialog_add_to\">Lägg till i %s</string>\n  <string name=\"dialog_remove_from\">Ta bort från %s</string>\n  <string name=\"added_to\">Tillagd i %s</string>\n  <string name=\"removed_from\">Borttagen från %s</string>\n  <string name=\"video_preset_adaptive\">Adaptiv</string>\n  <string name=\"content_block_preview_recap\">Förhandstitt eller sammanfattning av videon</string>\n  <string name=\"content_block_highlight\">Intressant punkt (bokmärke)</string>\n  <string name=\"removed_from_history\">Videon har tagits bort från historiken</string>\n  <string name=\"remove_from_history\">Ta bort från historiken</string>\n  <string name=\"upload_date\">Uppladdningsdatum</string>\n  <string name=\"upload_date_any\">När som helst</string>\n  <string name=\"upload_date_today\">I dag</string>\n  <string name=\"upload_date_this_week\">Den här veckan</string>\n  <string name=\"upload_date_this_month\">Den här månaden</string>\n  <string name=\"upload_date_this_year\">I år</string>\n  <string name=\"double_refresh_rate\">Dubblera bildfrekvens</string>\n  <string name=\"upload_date_last_hour\">Senaste timmen</string>\n  <string name=\"focus_on_search_results\">Fokusera automatiskt på sökresultaten</string>\n  <string name=\"context_menu\">Snabbmeny</string>\n  <string name=\"context_menu_sorting\">Sortera snabbmenyn</string>\n  <string name=\"add_remove_from_recent_playlist\">Lägg till/ta bort från nylig spellista</string>\n  <string name=\"hide_settings_section\">Dölj avsnittet Inställnigar (farligt!)</string>\n  <string name=\"volume\">Volym %s %%</string>\n  <string name=\"auto_frame_rate_sec\">%s sek.</string>\n  <string name=\"audio_shift_sec\">%s sek.</string>\n  <string name=\"ui_hide_timeout_sec\">%s sek.</string>\n  <string name=\"screen_dimming_timeout_min\">%s min.</string>\n  <string name=\"mark_all_channels_watched\">Markera alla kanaler som visade</string>\n  <string name=\"move_section_up\">Flytta avsnittet uppåt</string>\n  <string name=\"move_section_down\">Flytta avsnittet nedåt</string>\n  <string name=\"player_buttons\">Ställ in knappar i spelaren</string>\n  <string name=\"action_playlist_add\">Lägg till i spellista</string>\n  <string name=\"action_video_stats\">Videostatistik</string>\n  <string name=\"action_subscribe\">Prenumerera</string>\n  <string name=\"action_channel\">Öppna kanalen</string>\n  <string name=\"action_pip\">BIB</string>\n  <string name=\"action_video_info\">Videobeskrivning</string>\n  <string name=\"action_screen_off\">Skärm av</string>\n  <string name=\"action_sound_off\">Ljud av</string>\n  <string name=\"action_playback_queue\">Uppspelningskö</string>\n  <string name=\"action_video_speed\">Videohastighet</string>\n  <string name=\"action_subtitles\">Undertexter</string>\n  <string name=\"action_like\">Gilla</string>\n  <string name=\"action_dislike\">Ogilla</string>\n  <string name=\"action_play_pause\">Spela upp/pausa</string>\n  <string name=\"action_repeat_mode\">Uppspelningsläge</string>\n  <string name=\"action_next\">Nästa video</string>\n  <string name=\"action_previous\">Föregående video</string>\n  <string name=\"action_high_quality\">Videokvalitet</string>\n  <string name=\"content_block_no_skipping_mode\">Läget för inga överhoppningar</string>\n  <string name=\"content_block_action_type\">Välj åtgärd</string>\n  <string name=\"content_block_action_none\">Gör ingenting</string>\n  <string name=\"content_block_action_only_skip\">Endast hoppa över</string>\n  <string name=\"content_block_action_toast\">Hoppa över med avisering</string>\n  <string name=\"content_block_action_dialog\">Visa dialogruta för att bekräfta</string>\n  <string name=\"content_block_filler\">Ämnesavvikelse (utfyllnad)</string>\n  <string name=\"keyboard_auto_show\">Visa tangentbordet automatiskt</string>\n  <string name=\"cancel_dialog\">Avbryt</string>\n  <string name=\"msg_player_error_source2\">Webbadress fungerar inte eller fel tid på enheten.\\nVanligtvis är det ett fel i appen.</string>\n  <string name=\"msg_player_error_video_source\">Webbadress för VIDEO fungerar inte eller fel tid på enheten.\\nVanligtvis är det ett fel i appen.</string>\n  <string name=\"msg_player_error_audio_source\">Webbadress för LJUD fungerar inte eller fel tid på enheten.\\nVanligtvis är det ett fel i appen.</string>\n  <string name=\"msg_player_error_subtitle_source\">Webbadress för UNDERTEXTER fungerar inte eller fel tid på enheten.\\nVanligtvis är det ett fel i appen.</string>\n  <string name=\"hide_upcoming\">Dölj kommande i Prenumerationer</string>\n  <string name=\"hide_upcoming_channel\">Dölj kommande i kanaler</string>\n  <string name=\"hide_upcoming_home\">Dölj kommande i Hem</string>\n  <string name=\"search_background_playback\">Uppspelning i bakgrunden medan du söker/bläddrar i en kanal</string>\n  <string name=\"trending_row_name\">Populärt</string>\n  <string name=\"player_seek_type\">Sökningsbeteende</string>\n  <string name=\"player_seek_regular\">Vanligt</string>\n  <string name=\"player_seek_confirmation_pause\">Med bekräftelse (pausa medan du söker)</string>\n  <string name=\"player_seek_confirmation_play\">Med bekräftelse (spela medan du söker)</string>\n  <string name=\"hide_shorts_from_home\">Dölj Shorts-videor i Hem</string>\n  <string name=\"finish_on_disconnect\">Stäng appen efter att telefonen/surfplattan har frånkopplats</string>\n  <string name=\"update_error\">Uppdateringsfel</string>\n  <string name=\"hide_shorts_from_search\">Dölj Shorts-videor i sökresultat</string>\n  <string name=\"hide_shorts_from_history\">Dölj Shorts-videor i Historik</string>\n  <string name=\"hide_shorts_from_trending\">Dölj Shorts-videor i Populärt</string>\n  <string name=\"disable_screensaver\">Inaktivera skärmsläckare</string>\n  <string name=\"description_not_found\">Beskrivning hittades inte</string>\n  <string name=\"proxy_port_hint\">Proxyport</string>\n  <string name=\"proxy_host_hint\">Värdnamn eller IP för proxy</string>\n  <string name=\"proxy_username_hint\">Proxyanvändarnamn</string>\n  <string name=\"proxy_password_hint\">Proxylösenord</string>\n  <string name=\"proxy_type\">Proxytyp</string>\n  <string name=\"enable_web_proxy\">Använd en webbproxy</string>\n  <string name=\"proxy_not_supported\">Använd webbproxy (Kräver Android 4.4+)</string>\n  <string name=\"proxy_test_btn\">Test</string>\n  <string name=\"proxy_settings_title\">Proxyserverinställningar</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">Test nr %d: avbrutet.</string>\n  <string name=\"proxy_type_invalid\">Proxytyp har inte angetts.</string>\n  <string name=\"proxy_host_invalid\">Proxyvärd har inte angetts.</string>\n  <string name=\"proxy_port_invalid\">Ogiltig proxyport, måste vara &gt; 0.</string>\n  <string name=\"proxy_credentials_invalid\">Ogiltigt användarnamn eller lösenord.</string>\n  <string name=\"proxy_test_aborted\">Testet avbröts. Korrigera proxyinställningarna först.</string>\n  <string name=\"proxy_application_aborted\">Korrigera proxyinställningarna först.</string>\n  <string name=\"proxy_test_start\">Test nr %d: %s …</string>\n  <string name=\"proxy_test_error\">Fel nr %d: %s</string>\n  <string name=\"proxy_test_status\">Status nr %d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">Konfigurationsfilens adress (*.ovpn)</string>\n  <string name=\"enable_openvpn\">Använd OpenVPN</string>\n  <string name=\"openvpn_settings_title\">Inställningar för OpenVPN</string>\n  <string name=\"openvpn_application_aborted\">Korrigera inställningarna för OpenVPN först.</string>\n  <string name=\"openvpn_test_aborted\">Testet avbröts. Korrigera inställningarna för OpenVPN först.</string>\n  <string name=\"openvpn_address_invalid\">Adress för OpenVPN-konfigurationsfil har inte angetts.</string>\n  <string name=\"internet_censorship\">Internetcensur</string>\n  <string name=\"rename_section\">Byt namn på avsnittet</string>\n  <string name=\"simple_edit_value_hint\">Ange värde</string>\n  <string name=\"seek_interval\">Sökningsintervall</string>\n  <string name=\"seek_interval_sec\">%s sek.</string>\n  <string name=\"subtitle_system\">Systemstil (Android-inställningar &gt; Tillgänglighet)</string>\n  <string name=\"subtitle_scale\">Undertextskala</string>\n  <string name=\"audio_sync_fix_desc\">Ett alternativt sätt att synkronisera ljud och video. Det anses vara föråldrat, men kan i vissa fall vara till hjälp.</string>\n  <string name=\"audio_sync_fix\">Lösning för ljudsynkronisering</string>\n  <string name=\"ambilight_ratio_fix_desc\">Åtgärdar bakgrundsbelysning som saknas. Åtgärdar felaktigt bildformat eller felaktig videoskala. Åtgärdar tomma skärmdumpar. Påverkar prestandan!</string>\n  <string name=\"ambilight_ratio_fix\">Lösning för Ambilight-ljus/bildformat/videoskala/skärmdumpar</string>\n  <string name=\"force_legacy_codecs_desc\">Förbättrar prestandan på bristande enheter avsevärt. Maximal upplösning är 720p.</string>\n  <string name=\"force_legacy_codecs\">Tvinga gamla kodekar (720p)</string>\n  <string name=\"live_stream_fix_desc\">OBS! Detta inaktiverar förmågan att spola tillbaka streamar. Förbättrar livestreamprestandan på bristande enheter avsevärt. Maximal upplösning är 1080p.</string>\n  <string name=\"live_stream_fix_4k_desc\">OBS! Detta inaktiverar förmågan att spola tillbaka streamar. Förbättrar livestreamprestandan avsevärt. Maximal upplösning är 4K.</string>\n  <string name=\"live_stream_fix\">Lösning för livestreamar (1080p)</string>\n  <string name=\"live_stream_fix_4k\">Lösning för livestreamar (4K)</string>\n  <string name=\"playback_notifications_fix_desc\">Döljer aviseringar om spårbyte. Kan vara användbart på fast programvara baserad på AOSP</string>\n  <string name=\"playback_notifications_fix\">Inaktivera uppspelningsaviseringar</string>\n  <string name=\"amlogic_fix_desc\">Åtgärdar tappade bildrutor på Amlogic-baserade enheter.</string>\n  <string name=\"amlogic_fix\">Lösning för Amlogic 1080p\\@60fps</string>\n  <string name=\"tunneled_video_playback_desc\">OBS! Att pausa kanske inte fungerar som det ska! Tunnlad videouppspelning utlovar fördelar som bättre ljud- och videosynkronisering (A/V-synkronisering) och smidigare uppspelning. Kräver Android 5+</string>\n  <string name=\"tunneled_video_playback\">Tunnlad videouppspelning (Android 5+)</string>\n  <string name=\"master_volume\">Huvudvolym</string>\n  <string name=\"volume_limit\">Volymgräns</string>\n  <string name=\"player_volume\">Volym</string>\n  <string name=\"play_video\">Spela upp</string>\n  <string name=\"remember_position_of_short_videos\">Kom ihåg plats för korta videor (kortare än 5 min.)</string>\n  <string name=\"remember_position_of_live_videos\">Kom ihåg plats för livestreamar</string>\n  <string name=\"player_show_tooltips\">Visa beskrivning av knappar</string>\n  <string name=\"action_like_unset\">tog bort gilla-markering</string>\n  <string name=\"action_dislike_unset\">tog bort ogilla-markering</string>\n  <string name=\"various_buttons\">Knapparna högst upp på huvudfönstret</string>\n  <string name=\"not_compatible_with\">Det valda alternativet är inte kompatibelt med</string>\n  <string name=\"subtitle_position\">Flytta undertexter nedifrån</string>\n  <string name=\"pressing_home\">genom att trycka på hemknappen</string>\n  <string name=\"pressing_home_back\">genom att trycka på hem- eller bakåtknappen</string>\n  <string name=\"pressing_back\">genom att trycka på bakåtknappen</string>\n  <string name=\"player_number_key_seek\">Sök med sifferknapparna</string>\n  <string name=\"save_remove_playlist\">Lägg till/ta bort spellistan från avsnittet Spellistor</string>\n  <string name=\"save_playlist\">Lägg till spellistan i avsnittet Spellistor</string>\n  <string name=\"remove_playlist\">Ta bort spellistan från avsnittet Spellistor permanent</string>\n  <string name=\"remove_playlist_fmt\">Ta bort %s permanent\\?</string>\n  <string name=\"removed_from_playlists\">Borttagen från avsnittet Spellistor</string>\n  <string name=\"saved_to_playlists\">Tillagd i avsnittet Spellistor</string>\n  <string name=\"create_playlist\">Skapa en spellista</string>\n  <string name=\"create_playlist_note\">den kommer inte att synas i YouTube-appen</string>\n  <string name=\"add_video_to_new_playlist\">Lägg till i ny spellista</string>\n  <string name=\"cant_delete_empty_playlist\">Det går inte att ta bort en tom spellista</string>\n  <string name=\"playlist\">Spellista</string>\n  <string name=\"rename_playlist\">Byt namn på spellistan</string>\n  <string name=\"cant_rename_empty_playlist\">Det går inte att byta namn på en tom spellista</string>\n  <string name=\"cant_rename_foreign_playlist\">Det går inte att byta namn på en främmande spellista</string>\n  <string name=\"cant_save_playlist\">Den här spellistan kan inte läggas till</string>\n  <string name=\"enter_value\">Ange valfritt icke-tomt värde</string>\n  <string name=\"dialog_add_remove_from\">Lägg till/ta bort från %s</string>\n  <string name=\"lb_playback_controls_skip_next\">Hoppa till nästa</string>\n  <string name=\"lb_playback_controls_skip_previous\">Hoppa till föregående/spola tillbaka till startpositionen</string>\n  <string name=\"alt_presets_behavior\">Alternativt beteende för förinställningar (begränsa bandbredden)</string>\n  <string name=\"alt_presets_behavior_desc\">Appen försöker hålla en bandbredd motsvarande den valda förinställningen i stället för att matcha upplösning, bildfrekvens och kodek.</string>\n  <string name=\"sleep_timer\">Sovtimer (i fall fjärrkontrollen inte har använts i en timme)</string>\n  <string name=\"sleep_timer_desc\">Pausa uppspelningen om användaren inte har använt fjärrkontrollen i en timme</string>\n  <string name=\"disable_vsync\">Inaktivera vsync-justering</string>\n  <string name=\"disable_vsync_desc\">Detta alternativ inaktiverar att bildrutor justeras efter bildskärmens vertikala synkroniseringssignal. Detta kan förbättra prestandan på bristande enheter genom att frigöra CPU-resurser.</string>\n  <string name=\"skip_codec_profile_check\">Hoppa över kontroll av kodekprofilnivå</string>\n  <string name=\"skip_codec_profile_check_desc\">Kontrollera inte stöd för kodek när du startar en video. Kan hjälpa på felaktig fast programvara.</string>\n  <string name=\"force_sw_codec\">Tvinga programvarubaserad avkodare för video</string>\n  <string name=\"force_sw_codec_desc\">Kan spela upp nästan alla videor, men prestandan är mycket dålig.</string>\n  <string name=\"playlist_order\">Sortera spellistan</string>\n  <string name=\"playlist_order_added_date_newer_first\">Tilläggningsdatum (nyare först)</string>\n  <string name=\"playlist_order_added_date_older_first\">Tilläggningsdatum (äldre först)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Publiceringsdatum (nyare först)</string>\n  <string name=\"playlist_order_published_date_older_first\">Publiceringsdatum (äldre först)</string>\n  <string name=\"playlist_order_popularity\">Populäritet</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">Det går inte att göra detta för en främmande spellista</string>\n  <string name=\"owned_playlist_warning\">Fel: Den här åtgärden kan endast tillämpas för spellistor som du äger</string>\n  <string name=\"content_block_alt_server\">Använd en alternativ server</string>\n  <string name=\"content_block_alt_server_desc\">Aktivera det här alternativet om SponsorBlock vägrar att fungera av olika skäl.</string>\n  <string name=\"unset_stream_reminder\">Ta bort påminnelse om streamen</string>\n  <string name=\"set_stream_reminder\">Ställ in påminnelse om streamen</string>\n  <string name=\"playback_starts_shortly\">Uppspelningen startar automatiskt när streamen är redo</string>\n  <string name=\"starting_stream\">Startar streamen …</string>\n  <string name=\"action_playlist_remove\">Ta bort från spellistan</string>\n  <string name=\"signin_view_title\">Användarkod läses in …</string>\n  <string name=\"signin_view_description\">Om du vill logga in, ange den här koden på sidan %s\\nOBS! Fungerar endast med webbläsarna Firefox och Chrome.</string>\n  <string name=\"signin_view_action_text\">Klart</string>\n  <string name=\"require_checked\">Kräver \\\"%s</string>\n  <string name=\"msg_press_again_to_exit\">Tryck igen för att avsluta</string>\n  <string name=\"player_remaining_time\">Tid kvar: %s</string>\n  <string name=\"player_ending_time\">Slutar %s</string>\n  <string name=\"badge_new_content\">NYTT INNEHÅLL</string>\n  <string name=\"badge_live\">LIVE</string>\n  <string name=\"add_device_view_description\">Om du vill länka en enhet, ange den här koden i din telefons YouTube-app i avsnittet Inställningar/Titta på tv:n\\nOBS: Detta fungerar endast med Gboard-tangentbordet!</string>\n  <string name=\"playback_controls_repeat_pause\">Upprepa paus</string>\n  <string name=\"playback_controls_repeat_list\">Upprepa lista</string>\n  <string name=\"action_subscribe_off\">Prenumerera på</string>\n  <string name=\"action_subscribe_on\">Prenumerera av</string>\n  <string name=\"skip_24_rate\">Hoppa över 24 bildrutor i sekunden-format (riktig lösning för bioläge\\?).</string>\n  <string name=\"skip_shorts\">Hoppa över Shorts-videor</string>\n  <string name=\"player_disable_suggestions\">Inaktivera förslag</string>\n  <string name=\"suggestions\">Förslag</string>\n  <string name=\"feedback\">Feedback</string>\n  <string name=\"sources\">Källor</string>\n  <string name=\"releases\">Versioner</string>\n  <string name=\"prefer_avc_over_vp9\">Val av kodek: föredra avc framför vp9</string>\n  <string name=\"open_chat\">Chatt/kommetarer</string>\n  <string name=\"open_comments\">Öppna kommentarer</string>\n  <string name=\"place_chat_left\">Placera chatt till vänster</string>\n  <string name=\"place_comments_left\">Placera kommentarer till vänster</string>\n  <string name=\"use_alt_speech_recognizer\">Alternativ taligenkännare</string>\n  <string name=\"time_format\">tidsformat</string>\n  <string name=\"time_format_24\">24-timmars</string>\n  <string name=\"time_format_12\">12-timmars (fm/em)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Allvarligt bristfällig. Använd endast om du har problem med standardigenkännaren.</string>\n  <string name=\"speech_recognizer\">Taligenkännare</string>\n  <string name=\"speech_engine\">Röstsökningsmotor</string>\n  <string name=\"speech_recognizer_system\">System (föredras)</string>\n  <string name=\"speech_recognizer_external_1\">Extern 1 (allvarligt bristfällig – för att den ska fungera måste åtkomst till mikrofonen inaktiveras)</string>\n  <string name=\"speech_recognizer_external_2\">Extern 2 (allvarligt bristfällig)</string>\n  <string name=\"real_channel_icon\">Visa ikon på kanalknappen</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Gult på halvgenomskinlig bakgrund</string>\n  <string name=\"subtitle_yellow_black\">Gult på svart bakgrund</string>\n  <string name=\"player_pixel_ratio\">Pixelformat</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Mörkgrått (monokromt)</string>\n  <string name=\"disable_mic_permission\">Inaktivera mikrofonåtkomst för appen för att den här röstigenkännaren ska fungera som den ska.</string>\n  <string name=\"repeat_mode_shuffle\">Shuffla alla spellistor</string>\n  <string name=\"chat_left\">Vänster</string>\n  <string name=\"chat_right\">Höger</string>\n  <string name=\"card_real_thumbnails\">Byt ut miniatyrer mot en bildruta från videon</string>\n  <string name=\"card_content\">Varifrån kortminiatyrer ska hämtas</string>\n  <string name=\"thumb_quality_default\">Standard</string>\n  <string name=\"thumb_quality_start\">Början av videon</string>\n  <string name=\"thumb_quality_middle\">Mitten av videon</string>\n  <string name=\"thumb_quality_end\">Slutet av videon</string>\n  <string name=\"content_block_status\">Kontrollera serverstatus för SponsorBlock</string>\n  <string name=\"dearrow_status\">Kontrollera serverstatus för DeArrow</string>\n  <string name=\"player_show_quality_info_bitrate\">Lägg till bithastighet i kvalitetsinfon</string>\n  <string name=\"player_speed_button_old_behavior\">Återställ det gamla beteendet för hastighetsknappen</string>\n  <string name=\"protect_settings_with_password\">Skydda alla inställningar med lösenord</string>\n  <string name=\"enter_settings_password\">Ange inställningslösenord</string>\n  <string name=\"child_mode\">Barnläge</string>\n  <string name=\"child_mode_desc\">I det här läget kan användaren inte använda sökfunktionen eller se något föreslaget innehåll. Inställningssidan skyddas med lösenord.</string>\n  <string name=\"lost_setting_warning\">Appens inställningar kommer att ändras. Se till att du har skapat en säkerhetskopia av inställningarna.</string>\n  <string name=\"player_button_long_click\">Tryck länge på den här knappen för ytterligare alternativ</string>\n  <string name=\"pause_history\">Pausa historik</string>\n  <string name=\"resume_history\">Återuppta historik</string>\n  <string name=\"disable_history\">Inaktivera historik</string>\n  <string name=\"enable_history\">Aktivera historik</string>\n  <string name=\"clear_history\">Rensa historik</string>\n  <string name=\"chapters\">Kapitel</string>\n  <string name=\"card_multiline_subtitle\">Undertexter med flera rader</string>\n  <string name=\"auto_frame_rate_modes\">Lägen som stöds</string>\n  <string name=\"loading\">Läser in …</string>\n  <string name=\"hide_streams\">Dölj streamar i Prenumerationer</string>\n  <string name=\"video_rotate\">Rotera</string>\n  <string name=\"video_flip\">Spegelvänd</string>\n  <string name=\"trending_searches\">Populära sökningar</string>\n  <string name=\"video_duration_any\">Alla</string>\n  <string name=\"video_duration\">Längd</string>\n  <string name=\"video_duration_under_4\">Under 4 minuter</string>\n  <string name=\"video_duration_between_4_20\">4–20 minuter</string>\n  <string name=\"video_duration_over_20\">Mer än 20 minuter</string>\n  <string name=\"content_type\">Typ</string>\n  <string name=\"content_type_any\">Alla</string>\n  <string name=\"content_type_video\">Video</string>\n  <string name=\"content_type_channel\">Kanal</string>\n  <string name=\"content_type_playlist\">Spellista</string>\n  <string name=\"content_type_movie\">Film</string>\n  <string name=\"video_features\">Egenskaper</string>\n  <string name=\"video_feature_any\">Alla</string>\n  <string name=\"video_feature_live\">Live</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">Sortera efter</string>\n  <string name=\"sort_by_relevance\">Relevans</string>\n  <string name=\"sort_by_views\">Antal visningar</string>\n  <string name=\"sort_by_date\">Uppladdningsdatum</string>\n  <string name=\"sort_by_rating\">Betyg</string>\n  <string name=\"clear_search_history\">Rensa sökhistorik</string>\n  <string name=\"remove_from_subscriptions\">Dölj</string>\n  <string name=\"player_long_speed_list\">Lång hastighetslista</string>\n  <string name=\"player_extra_long_speed_list\">Extra lång hastighetslista</string>\n  <string name=\"alt_app_icon\">Alternativ appikon (kräver omstart)</string>\n  <string name=\"network_stack\">Föredra nätverksstacken %s</string>\n  <string name=\"player_network_stack\">Nätverksmotor</string>\n  <string name=\"cronet_desc\">Cronet är Chromiums nätverksstack. Cronet kan minska latensen och öka nätverksprestandan, vilket kan hjälpa med buffringsproblem.</string>\n  <string name=\"unlock_all_formats\">Lås upp alla videoformat</string>\n  <string name=\"unlock_all_formats_desc\">På vissa enheter rapporterar den fasta programvaran felaktigt att vissa format inte stöds, även om de stöds.\\n(t.ex. smart-tv:ar med 1080p-upplösning brukar rapportera att 4k inte stöds).</string>\n  <string name=\"okhttp_desc\">Den här nätverksmotorn är den långsammaste men har bra stabilitet.</string>\n  <string name=\"select_channel_section\">Öppna motsvarande avsnitt när appen startas från ATV-kanaler</string>\n  <string name=\"enable_voice_search_desc\">Den officiella appen byts ut mot en så kallad \\\"bro\\\". En bro hjälper att överföra globala sökförfrågningar till vår app.</string>\n  <string name=\"enable_master_password\">Aktivera huvudlösenord</string>\n  <string name=\"enter_master_password\">Ange huvudlösenord</string>\n  <string name=\"disable_stream_buffer\">Inaktivera buffert för streamar</string>\n  <string name=\"disable_stream_buffer_desc\">Lösning för situationer när streamen ligger för långt efter. OBS! Streamen kan bli hackig.</string>\n  <string name=\"sony_frame_drop_fix\">Lösning för tappade bildrutor 1</string>\n  <string name=\"sony_frame_drop_fix_desc\">Åtgärdar hackig uppspelning på Sony-TV:ar och vissa andra enheter. OBS! Eventuella problem med ljudsynkronisering.</string>\n  <string name=\"audio_language\">Ljudspråk</string>\n  <string name=\"old_home_look\">Det gamla utseendet för avsnittet Hem</string>\n  <string name=\"old_channel_look\">Det gamla utseendet för kanalsidan</string>\n  <string name=\"hide_shorts_everywhere\">Dölj Shorts-videor överallt</string>\n  <string name=\"update_found\">Uppdatering</string>\n  <string name=\"volume_boost_warning\">Inställningar över gränsen kan eventuellt skada högtalare</string>\n  <string name=\"play_video_incognito\">Spela upp i inkognitoläge</string>\n  <string name=\"play_from_start\">Spela upp från början</string>\n  <string name=\"header_kids_home\">Barn</string>\n  <string name=\"player_section_playlist\">Andvänd det aktuella avsnittets innehåll som spellista</string>\n  <string name=\"header_trending\">Populärt</string>\n  <string name=\"screensaver\">Skärmsläckare</string>\n  <string name=\"player_screen_off_timeout\">Tidsgräns för att stänga av skärmen</string>\n  <string name=\"old_update_notifications\">Det gamla utseendet för aviseringarna om uppdatering</string>\n  <string name=\"dialog_notification\">Avisera om uppdateringar med en popup-dialogruta</string>\n  <string name=\"mark_as_watched\">Markera som sett</string>\n  <string name=\"player_ui_animations\">Aktivera animeringar för spelarens användargränssnitt</string>\n  <string name=\"player_likes_count\">Visa antalet gilla- och ogilla-markeringar</string>\n  <string name=\"sorting_alphabetically2\">Alfabetiskt (snabbt, animerade förhandsgranskningar)</string>\n  <string name=\"sorting_default\">Standard (snabbt, animerade förhandsgranskningar)</string>\n  <string name=\"content_block_exclude_channel\">Undanta den här kanalen från SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">Sluta undanta den här kanalen från SponsorBlock</string>\n  <string name=\"player_chapter_notification\">Bläddra snabbt igenom kapitlen med popup-aviseringen</string>\n  <string name=\"player_chapter_notification2\">Byt till nästa kapitel genom att trycka på aviseringen</string>\n  <string name=\"subtitle_remember\">Aktivera undertexter endast på den aktuella kanalen</string>\n  <string name=\"amazon_frame_drop_fix\">Lösning för tappade bildrutor 2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">Avsedd för Amazon Stick-enheter. Kan fungera även på andra enheter.</string>\n  <string name=\"default_stack_desc\">Inbyggd nätverksmotor. Den här motorn kan ha bättre stabilitet i vissa sammanhang.</string>\n  <string name=\"item_postion\">Plats för</string>\n  <string name=\"player_screen_off_dimming\">Nedtoningsgrad för avstängd skärm</string>\n  <string name=\"screensaver_timout\">Tidsgräns för skärmsläckare</string>\n  <string name=\"screensaver_dimming\">Nedtoningsgrad för skärmsläckare</string>\n  <string name=\"player_ui_on_next\">Visa spelarens användargränssnitt när du byter till nästa video</string>\n  <string name=\"autogenerated\">Autogenererat</string>\n  <string name=\"player_auto_volume\">Automatisk volymjustering</string>\n  <string name=\"header_shorts\">Shorts</string>\n  <string name=\"player_global_focus\">Synkronisera fokus mellan spelarens knapprader</string>\n  <string name=\"auto_history\">Automatiskt (använd kontoinställningarna)</string>\n  <string name=\"remember_position_subscriptions\">Kom ihåg den senast visade platsen i Prenumerationer</string>\n  <string name=\"remember_position_pinned\">Kom ihåg den senast visade platsen i fästa spellistor</string>\n  <string name=\"msg_player_unknown_error\">Okänt fel</string>\n  <string name=\"unknown_source_error\">Okänt fel med källan</string>\n  <string name=\"unknown_renderer_error\">Okänt fel med renderaren</string>\n  <string name=\"header_notifications\">Aviseringar</string>\n  <string name=\"disable_search_history\">Inaktivera sökhistorik</string>\n  <string name=\"unlock_high_bitrate_formats\">Lås upp vp9-format i 1080p med hög bithastighet</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Lås upp mp4a-format med hög bithastighet</string>\n  <string name=\"color_scheme_blue\">Blått</string>\n  <string name=\"color_scheme_dark_blue\">Mörkblått</string>\n  <string name=\"player_speed_per_channel\">För varje kanal</string>\n  <string name=\"disable_popular_searches\">Inaktivera populära sökfrågor</string>\n  <string name=\"multi_profiles\">Använd egna inställningar för varje konto</string>\n  <string name=\"protect_account_with_password\">Skydda det här kontot med ett lösenord</string>\n  <string name=\"enter_account_password\">Ange kontolösenord</string>\n  <string name=\"show_connect_messages\">Visa anslutningsmeddelanden</string>\n  <string name=\"prefer_avc_over_vp9_desc\">OBS! Maximal upplösning är 1080p</string>\n  <string name=\"play_next\">Spela upp som nästa</string>\n  <string name=\"hide_watched_from_subscriptions\">Dölj sedda videor i Prenumerationer</string>\n  <string name=\"hide_watched_from_notifications\">Dölj sedda videor i Aviseringar</string>\n  <string name=\"hide_unwanted_content\">Dölj videor</string>\n  <string name=\"hide_watched_from_home\">Dölj sedda videor i Hem</string>\n  <string name=\"hide_watched_from_watch_later\">Dölj sedda videor i spellistan Titta senare</string>\n  <string name=\"remote_control_permission\">Fjärrstyrning i bakgrunden kräver behörighet för överlägg</string>\n  <string name=\"login_from_browser\">Logga in från webbläsare</string>\n  <string name=\"disable_remote_history\">Inaktivera historik när du använder fjärrstyrning</string>\n  <string name=\"keyboard_fix\">Förhindra OK-knappen från att öppna tangentbordet (G20s och andra)</string>\n  <string name=\"nothing_found\">Hittade inget</string>\n  <string name=\"auto_frame_rate_desc\">Det här alternativet tar bort bildstörningar från scener där kameran rör sig snabbt som i t.ex. sportsändningar</string>\n  <string name=\"channel_filter_hint\">Filtrera kanaler</string>\n  <string name=\"player_loop_shorts\">Spela upp Shorts-videor i en slinga</string>\n  <string name=\"player_quick_shorts_skip\">Hoppa över Shorts-videor med vänster- och högerknapparna</string>\n  <string name=\"player_quick_shorts_skip_alt\">Hoppa över Shorts-videor med uppåt- och nedåtknapparna</string>\n  <string name=\"player_quick_skip_videos\">Hoppa över vanliga videor med vänster- och högerknapparna</string>\n  <string name=\"player_quick_skip_videos_alt\">Hoppa över vanliga videor med uppåt- och nedåtknapparna</string>\n  <string name=\"channels_filter\">Visa fältet Filtrera kanaler i avsnittet Kanaler</string>\n  <string name=\"channel_search_bar\">Sökfält på kanalsidan</string>\n  <string name=\"header_sports\">Sport</string>\n  <string name=\"keep_finished_activities\">Behåll slutförda aktiviteter</string>\n  <string name=\"disable_channels_service\">Inaktivera kanaler-tjänsten</string>\n  <string name=\"replace_titles\">Ersätt titlar</string>\n  <string name=\"enable\">Aktivera</string>\n  <string name=\"more_info\">Mer info</string>\n  <string name=\"about_sponsorblock\">Om SponsorBlock</string>\n  <string name=\"about_dearrow\">Om DeArrow</string>\n  <string name=\"replace_thumbnails\">Ersätt miniatyrer</string>\n  <string name=\"crowdsourced_thumbnails\">Crowdsourcing-baserade miniatyrer</string>\n  <string name=\"crowdsoursed_titles\">Crowdsourcing-baserade titlar</string>\n  <string name=\"dearrow_not_submitted_thumbs\">Källa för miniatyrer (om det inte finns något DeArrow-bidrag)</string>\n  <string name=\"pitch_effect\">Tonhöjdseffekt</string>\n  <string name=\"fullscreen_mode\">Helskärmsläge (utan systembalkar)</string>\n  <string name=\"player_only_mode\">Visa endast spelaren om videon öppnas utanför appen</string>\n  <string name=\"pinned_channel_rows\">Visa fäst kanal som rader</string>\n  <string name=\"prefer_google_dns\">Föredra Google-DNS</string>\n  <string name=\"prefer_ipv4\">Föredra IPv4-DNS</string>\n  <string name=\"prefer_ipv4_desc\">Kan åtgärda situationer där appen inte fungerar alls.\\nOBS! Kan leda till att appen låser sig och krashar (särskilt på Android 8-enheter och Dune HD)</string>\n  <string name=\"long_press_for_settings\">TRYCK LÄNGE FÖR INSTÄLLNINGAR</string>\n  <string name=\"long_press_for_options\">TRYCK LÄNGE FÖR ALTERNATIV</string>\n  <string name=\"device_specific_backup\">Säkerhetskopia för endast den här enheten</string>\n  <string name=\"local_backup\">Säkerhetskopiera lokalt</string>\n  <string name=\"auto_backup\">Säkerhetskopiera automatiskt (en gång om dagen)</string>\n  <string name=\"repeat_mode_reverse_list\">Spela upp spellistan eller kanalvideorna i omvänd ordning</string>\n  <string name=\"calm_msg\">Vi håller på att åtgärda problemet. Kontrollera om det finns uppdateringar då och då</string>\n  <string name=\"without_picture\">Utan bild</string>\n  <string name=\"video_disabled\">Video inaktiverad</string>\n  <string name=\"applying_fix\">Stäng inte spelaren. Åtgärden tillämpas …</string>\n  <string name=\"hide_mixes\">Dölj mixar</string>\n  <string name=\"player_global_focus_desc\">Denna funktion påverkar vilken knapp i spelaren som får fokus när du navigerar mellan spelarens knapprader</string>\n  <string name=\"disable_network_error_fixing\">Inaktivera att nätverksfel åtgärdas automatiskt</string>\n  <string name=\"disable_network_error_fixing_desc\">Du behöver troligen aktivera det här alternativet om du använder VPN</string>\n  <string name=\"recommended\">Rekommenderat</string>\n  <string name=\"add_to_subscriptions_group\">Lägg till/ta bort från prenumerationsgrupp</string>\n  <string name=\"new_subscriptions_group\">Ny grupp</string>\n  <string name=\"rename_group\">Byt namn på prenumerationsgruppen</string>\n  <string name=\"screen_dimming_amount\">Nedtoningsgrad för skärm</string>\n  <string name=\"screen_dimming_timeout\">Tidsgräns för skärmnedtoning</string>\n  <string name=\"playlists_rows\">Visa avsnittet Spellistor som rader</string>\n  <string name=\"import_subscriptions_group\">Importera</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">Inaktivera textning</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">Aktivera textning</string>\n  <string name=\"my_videos\">Mina videor</string>\n  <string name=\"player_audio_focus\">Fokusera ljud (pausa om andra spelare upptäcks)</string>\n  <string name=\"paid_content_notification\">Avisering om betalt innehåll</string>\n  <string name=\"you_liked\">du gillade</string>\n  <string name=\"premium_users_only\">Endast för Premium-användare. Lösning för ofullständig lista över videoformat</string>\n  <string name=\"playback_buffering_fix\">Lösning för uppspelningsbuffring</string>\n  <string name=\"oculus_quest_fix\">Lösning för Oculus Quest</string>\n  <string name=\"card_preview_muted\">Video utan ljud</string>\n  <string name=\"card_preview_full\">Video med ljud</string>\n  <string name=\"card_preview\">Förhandsgranskning i kort</string>\n  <string name=\"card_unlocalized_titles\">Icke-lokaliserade videotitlar</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">Ändra inte storlek på videon för att passa dialogruta</string>\n  <string name=\"typing_corrections\">Inaktivera autokorrigering medan du skriver text</string>\n  <string name=\"suggestions_horizontally_scrolled\">Horisontellt rullande förslag</string>\n  <string name=\"stable_restore\">Återgå till den stabila versionen (alla data kommer att gå förlorade)</string>\n  <string name=\"auto_backup_category\">Automatisk säkerhetskopiering</string>\n  <string name=\"once_a_day\">En gång om dagen</string>\n  <string name=\"once_a_week\">En gång i veckan</string>\n  <string name=\"once_a_month\">En gång i månaden</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-te/strings.xml",
    "content": "<resources>\n    <!-- Generated by Automatic String Resource Translation -->\n<!--          https://asrt.gluege.boerde.de             -->\n<string name=\"header_home\">హోమ్</string>\n    <string name=\"title_search\">వెతకండి</string>\n    <string name=\"header_subscriptions\">చందాలు</string>\n    <string name=\"header_history\">చరిత్ర</string>\n    <string name=\"header_music\">సంగీతం</string>\n    <string name=\"header_news\">వార్తలు</string>\n    <string name=\"header_gaming\">గేమింగ్</string>\n    <string name=\"header_playlists\">ప్లేజాబితాలు</string>\n    <string name=\"header_settings\">సెట్టింగులు</string>\n    <string name=\"header_channels\">ఛానెల్‌లు</string>\n    <string name=\"subscriptions_signin_title\">మీరు ఇష్టపడే ఛానెల్‌ల నుండి క్రొత్తదాన్ని చూడండి</string>\n    <string name=\"subscriptions_signin_subtitle\">మీ సభ్యత్వాలను చూడటానికి సైన్ ఇన్ చేయండి</string>\n    <string name=\"subscriptions_signin_button_text\">సైన్ ఇన్ చేయండి</string>\n    <string name=\"library_signin_to_show_more\">మీరు ఇష్టపడిన, సేవ్ చేసిన లేదా సభ్యత్వం పొందిన వీడియోలను చూడండి</string>\n    <string name=\"library_signin_subtitle\">మీ లైబ్రరీని చూడటానికి సైన్ ఇన్ చేయండి</string>\n    <string name=\"action_signin\">సైన్ ఇన్ చేయండి</string>\n    <string name=\"title_video_formats\">వీడియో ఆకృతులు</string>\n    <string name=\"title_audio_formats\">ఆడియో ఆకృతులు</string>\n    <string name=\"subtitle_category_title\">ఉపశీర్షికలు</string>\n    <string name=\"video_max_quality\">ఆటో (గరిష్ట నాణ్యత)</string>\n    <string name=\"audio_max_quality\">ఆటో (గరిష్ట నాణ్యత)</string>\n    <string name=\"subtitles_disabled\">ఉపశీర్షికలు నిలిపివేయబడ్డాయి</string>\n    <string name=\"auto_frame_rate\">ఆటో ఫ్రేమ్ రేటు</string>\n    <string name=\"frame_rate_correction\">Fps ని పరిష్కరించండి:%s</string>\n    <string name=\"category_background_playback\">నేపథ్య ప్లేబ్యాక్</string>\n    <string name=\"not_implemented\">అమలు చేయలేదు</string>\n    <string name=\"option_background_playback_off\">నిలిపివేయబడింది</string>\n    <string name=\"option_background_playback_pip\">చిత్రంలో చిత్రం</string>\n    <string name=\"option_background_playback_only_audio\">ఆడియో మాత్రమే</string>\n    <string name=\"playback_settings\">ప్లేబ్యాక్ సెట్టింగ్‌లు</string>\n    <string name=\"video_speed\">వీడియో వేగం</string>\n    <string name=\"resolution_switch\">రిజల్యూషన్ మారండి</string>\n    <string name=\"video_buffer\">వీడియో బఫర్</string>\n    <string name=\"video_buffer_size_low\">తక్కువ</string>\n    <string name=\"video_buffer_size_med\">మధ్యస్థం</string>\n    <string name=\"video_buffer_size_high\">అధిక</string>\n    <string name=\"update_changelog\">చేంజ్లాగ్</string>\n    <string name=\"install_update\">నవీకరణను ఇన్‌స్టాల్ చేయండి</string>\n    <string name=\"section_is_empty\">అయ్యో! ఇక్కడ ఏమీ లేదు.</string>\n    <string name=\"dialog_add_to_playlist\">ప్లేజాబితా నుండి జోడించండి /తీసివేయండి</string>\n    <string name=\"msg_signed_users_only\">సంతకం చేసిన వినియోగదారులు మాత్రమే</string>\n    <string name=\"msg_cant_load_content\">అయ్యో. కంటెంట్‌ను లోడ్ చేయలేరు.</string>\n    <string name=\"title_video_presets\">వీడియో ప్రీసెట్లు</string>\n    <string name=\"settings_accounts\">ఖాతాలు</string>\n    <string name=\"settings_left_panel\">వర్గాలను సవరించండి</string>\n    <string name=\"settings_themes\">థీమ్స్</string>\n    <string name=\"settings_other\">ఇతర</string>\n    <string name=\"settings_player\">వీడియో ప్లేయర్</string>\n    <string name=\"settings_language\">భాష</string>\n    <string name=\"settings_linked_devices\">లింక్ చేసిన పరికరాలు</string>\n    <string name=\"settings_about\">గురించి</string>\n    <string name=\"dialog_account_list\">ఖాతాను ఎంచుకోండి</string>\n    <string name=\"dialog_account_none\">ఏదీ లేదు</string>\n    <string name=\"dialog_remove_account\">ఖాతాను తొలగించండి</string>\n    <string name=\"dialog_add_account\">ఖాతా జోడించండి</string>\n    <string name=\"default_lang\">డిఫాల్ట్</string>\n    <string name=\"dialog_select_language\">భాష</string>\n    <string name=\"subtitle_default\">డిఫాల్ట్</string>\n    <string name=\"subtitle_white_semi_transparent\">సెమిట్రాన్స్పరెంట్ నేపథ్యం</string>\n    <string name=\"subtitle_style\">ఉపశీర్షిక శైలి</string>\n    <string name=\"subtitle_language\">ఉపశీర్షిక భాష</string>\n    <string name=\"action_search\">శోధనను ప్రారంభించండి</string>\n    <string name=\"settings_main_ui\">వినియోగ మార్గము</string>\n    <string name=\"dialog_main_ui\">వినియోగ మార్గము</string>\n    <string name=\"card_animated_previews\">యానిమేటెడ్ ప్రివ్యూలు</string>\n    <string name=\"web_site\">వెబ్‌సైట్</string>\n    <string name=\"donation\">విరాళం</string>\n    <string name=\"dialog_about\">గురించి</string>\n    <string name=\"dialog_player_ui\">వీడియో ప్లేయర్</string>\n    <string name=\"player_show_ui_on_pause\">విరామంలో UI ని చూపించు</string>\n    <string name=\"player_pause_on_ok\">సరే కీ ప్లేబ్యాక్‌ను పాజ్ చేస్తుంది</string>\n    <string name=\"player_ok_button_behavior\">సరే బటన్ ప్రవర్తన</string>\n    <string name=\"player_only_ui\">UI మాత్రమే</string>\n    <string name=\"player_ui_and_pause\">UI మరియు పాజ్</string>\n    <string name=\"player_only_pause\">విరామం మాత్రమే</string>\n    <string name=\"check_for_updates\">తాజాకరణలకోసం ప్రయత్నించండి</string>\n    <string name=\"update_not_found\">మీరు తాజా సంస్కరణను ఉపయోగిస్తున్నారు</string>\n    <string name=\"update_in_progress\">వేచి ఉండండి…</string>\n    <string name=\"player_ui_hide_behavior\">ఆటో-దాచు UI</string>\n    <string name=\"option_never\">ఎప్పుడూ</string>\n    <string name=\"side_panel_sections\">విభాగాలను సెటప్ చేయండి</string>\n    <string name=\"boot_to_section\">విభాగానికి బూట్ చేయండి</string>\n    <string name=\"large_ui\">పెద్ద UI</string>\n    <string name=\"video_grid_scale\">వీడియో గ్రిడ్ స్కేల్</string>\n    <string name=\"scale_ui\">UI స్కేల్</string>\n    <string name=\"color_scheme\">రంగు పథకం</string>\n    <string name=\"color_scheme_default\">డిఫాల్ట్</string>\n    <string name=\"color_scheme_red_grey\">ఎరుపు-బూడిద</string>\n    <string name=\"color_scheme_red\">ఎరుపు</string>\n    <string name=\"color_scheme_dark_grey\">ముదురు బూడిద రంగు</string>\n    <string name=\"disable_update_check\">నవీకరణ తనిఖీని నిలిపివేయండి</string>\n    <string name=\"show_again\">మళ్ళీ చూపించు</string>\n    <string name=\"check_updates_auto\">స్వీయ-నవీకరణ</string>\n    <string name=\"select_account_on_boot\">బూట్లో ఎంచుకోండి</string>\n    <string name=\"player_other\">ఇతర</string>\n    <string name=\"player_full_date\">వివరణలో ఖచ్చితమైన తేదీ</string>\n    <string name=\"open_channel\">ఛానెల్ తెరవండి</string>\n    <string name=\"not_interested\">ఆసక్తి లేదు</string>\n    <string name=\"you_wont_see_this_video\">సిఫార్సు నుండి వీడియో తీసివేయబడింది</string>\n    <string name=\"settings_search\">వెతకండి</string>\n    <string name=\"dialog_search\">వెతకండి</string>\n    <string name=\"instant_voice_search\">తక్షణ వాయిస్ శోధన</string>\n    <string name=\"option_background_playback_behind\">వెనుక ఆడండి</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">తదుపరి వీడియో సమాచారం ఇంకా లోడ్ కాలేదు</string>\n    <string name=\"card_multiline_title\">బహుళ శీర్షికలు</string>\n    <string name=\"cards_style\">కార్డుల శైలి</string>\n    <string name=\"repeat_mode_all\">అన్ని వీడియోలను ఒక్కొక్కటిగా ప్లే చేయండి</string>\n    <string name=\"repeat_mode_one\">ప్రస్తుత వీడియోను పునరావృతం చేయండి</string>\n    <string name=\"repeat_mode_pause\">ప్రతి వీడియో తర్వాత ప్లేబ్యాక్‌ను పాజ్ చేయండి</string>\n    <string name=\"repeat_mode_pause_alt\">ప్రతి ప్లేజాబితా కాని వీడియో తర్వాత ప్లేబ్యాక్‌ను పాజ్ చేయండి</string>\n    <string name=\"repeat_mode_none\">ఒక వీడియో తర్వాత ప్లేబ్యాక్ ఆపు</string>\n    <string name=\"subscribed_to_channel\">ఛానెల్‌కు సభ్యత్వాన్ని పొందారు</string>\n    <string name=\"unsubscribed_from_channel\">ఛానెల్ నుండి చందాను తొలగించారు</string>\n    <string name=\"subtitle_yellow_transparent\">పసుపు</string>\n    <string name=\"subtitle_white_black\">నలుపు నేపథ్యం</string>\n    <string name=\"player_seek_preview\">కోరుతున్నప్పుడు ప్రివ్యూ చేయండి</string>\n    <string name=\"color_scheme_dark_grey_oled\">డార్క్ గ్రే (OLED)</string>\n    <string name=\"channels_section_sorting\">ఛానెల్స్ విభాగం సార్టింగ్</string>\n    <string name=\"sorting_by_new_content\">క్రొత్త కంటెంట్</string>\n    <string name=\"sorting_alphabetically\">అక్షరక్రమంలో</string>\n    <string name=\"sorting_last_viewed\">చివరిగా వీక్షించారు</string>\n    <string name=\"player_pause_when_seek\">కోరినప్పుడు పాజ్ చేయండి</string>\n    <string name=\"playlists_style\">ప్లేజాబితాల శైలి</string>\n    <string name=\"playlists_style_grid\">గ్రిడ్</string>\n    <string name=\"playlists_style_rows\">వరుసలు</string>\n    <string name=\"player_show_clock\">పాప్-అప్ మెనులో గడియారాన్ని చూపించు</string>\n    <string name=\"player_show_remaining_time\">మిగిలిన సమయాన్ని చూపించు</string>\n    <string name=\"open_channel_uploads\">ఛానెల్ అప్‌లోడ్‌లను తెరవండి</string>\n    <string name=\"app_exit_shortcut\">అనువర్తనం నుండి నిష్క్రమించండి</string>\n    <string name=\"app_exit_none\">నిష్క్రమించవద్దు</string>\n    <string name=\"app_double_back_exit\">డబుల్ బ్యాక్</string>\n    <string name=\"app_single_back_exit\">సింగిల్ బ్యాక్</string>\n    <string name=\"action_video_zoom\">వీడియో జూమ్</string>\n    <string name=\"video_zoom\">వీడియో జూమ్</string>\n    <string name=\"video_zoom_default\">డిఫాల్ట్</string>\n    <string name=\"video_zoom_fit_width\">వెడల్పు సరిపోతుంది</string>\n    <string name=\"video_zoom_fit_height\">ఎత్తు సరిపోతుంది</string>\n    <string name=\"video_zoom_fit_both\">వెడల్పు లేదా ఎత్తుకు సరిపోతుంది</string>\n    <string name=\"video_zoom_stretch\">సాగదీయండి</string>\n    <string name=\"color_scheme_teal\">టీల్</string>\n    <string name=\"color_scheme_teal_oled\">టీల్ (OLED)</string>\n    <string name=\"player_seek_preview_none\">నిలిపివేయబడింది</string>\n    <string name=\"player_seek_preview_single\">ఒకే ఫ్రేమ్</string>\n    <string name=\"player_seek_preview_carousel\">రంగులరాట్నం</string>\n    <string name=\"player_seek_preview_carousel_slow\">రంగులరాట్నం (నెమ్మదిగా)</string>\n    <string name=\"player_seek_preview_carousel_fast\">రంగులరాట్నం (వేగంగా)</string>\n    <string name=\"unsubscribe_from_channel\">ఛానెల్ నుండి చందాను తొలగించండి</string>\n    <string name=\"card_title_lines_num\">కార్డ్ శీర్షిక పంక్తులు సంఖ్య</string>\n    <string name=\"card_auto_scrolled_title\">ఆటో-స్క్రోల్ కత్తిరించిన శీర్షిక</string>\n    <string name=\"share_link\">భాగస్వామ్యం లింక్</string>\n    <string name=\"subscribe_to_channel\">ఛానెల్‌కు సభ్యత్వాన్ని పొందండి</string>\n    <string name=\"wait_data_loading\">డేటా లోడ్ అవుతున్నప్పుడు వేచి ఉండండి…</string>\n    <string name=\"auto_frame_rate_pause\">ఆటో ఫ్రేమ్ రేటు విరామం</string>\n    <string name=\"auto_frame_rate_applying\">ఆటో ఫ్రేమ్ రేటు %sx %s @ %s ను వర్తింపజేస్తోంది</string>\n    <string name=\"audio_shift\">ఆడియో షిఫ్ట్</string>\n    <string name=\"player_remember_speed\">వేగం గుర్తుంచుకో</string>\n    <string name=\"mark_channel_as_watched\">చూసినట్లుగా గుర్తించండి</string>\n    <string name=\"channel_marked_as_watched\">ఛానెల్ చూసినట్లుగా గుర్తించబడింది</string>\n    <string name=\"dialog_add_device\">పరికరాన్ని జోడించండి</string>\n    <string name=\"dialog_remove_all_devices\">అన్ని పరికరాలను తొలగించండి</string>\n    <string name=\"device_link_enabled\">లింక్ ప్రారంభించబడింది</string>\n    <string name=\"settings_ui_scale\">UI స్కేల్</string>\n    <string name=\"msg_restart_app\">దయచేసి, ఈ సెట్టింగ్‌లను వర్తింపజేయడానికి అనువర్తనాన్ని పున art ప్రారంభించండి</string>\n    <string name=\"settings_block\">కంటెంట్ నిరోధించడం</string>\n    <string name=\"msg_done\">పూర్తి</string>\n    <string name=\"settings_general\">జనరల్</string>\n    <string name=\"settings_video\">వీడియో</string>\n    <string name=\"content_block_confirm_skip\">దాటవేసినట్లు నిర్ధారించండి</string>\n    <string name=\"content_block_categories\">కేటగిరీలు</string>\n    <string name=\"content_block_sponsor\">స్పాన్సర్</string>\n    <string name=\"content_block_intro\">అంతరాయం /పరిచయ యానిమేషన్</string>\n    <string name=\"content_block_outro\">ఎండ్‌కార్డులు /క్రెడిట్‌లు</string>\n    <string name=\"content_block_interaction\">ఇంటరాక్షన్ రిమైండర్ (సభ్యత్వాన్ని పొందండి)</string>\n    <string name=\"content_block_self_promo\">చెల్లించని /స్వీయ ప్రమోషన్</string>\n    <string name=\"content_block_music_off_topic\">క్లిప్ యొక్క నాన్-మ్యూజిక్ విభాగం</string>\n    <string name=\"content_block_notification_type\">నోటిఫికేషన్ రకం</string>\n    <string name=\"content_block_notify_none\">నోటిఫికేషన్ లేకుండా</string>\n    <string name=\"content_block_notify_toast\">అభినందించి త్రాగుట</string>\n    <string name=\"content_block_notify_dialog\">నిర్ధారణ డైలాగ్</string>\n    <string name=\"return_to_launcher\">ATV ఛానెల్స్ /శోధన నుండి లాంచర్‌కు తిరిగి వెళ్ళు</string>\n    <string name=\"intent_force_close\">బాహ్య మూలం నుండి వీడియో తెరిచినట్లయితే బలవంతంగా నిష్క్రమించండి</string>\n    <string name=\"btn_confirm\">నిర్ధారించండి</string>\n    <string name=\"player_low_video_quality\">తక్కువ వీడియో నాణ్యత</string>\n    <string name=\"remote_session_closed\">రిమోట్ సెషన్ మూసివేయబడింది</string>\n    <string name=\"video_preset_disabled\">ప్రీసెట్ లేకుండా</string>\n    <string name=\"video_preset_enabled\">ప్రీసెట్ ప్రకారం తదుపరి వీడియో యొక్క ఫార్మాట్ సెట్ చేయబడుతుంది</string>\n    <string name=\"player_sleep_timer\">స్లీప్ టైమర్</string>\n    <string name=\"player_show_quality_info\">వీడియో నాణ్యత సమాచారాన్ని చూపించు</string>\n    <string name=\"player_remember_each_speed\">ప్రతి వీడియో వేగాన్ని గుర్తుంచుకోండి</string>\n    <string name=\"player_remember_speed_none\">ఏదీ లేదు</string>\n    <string name=\"player_remember_speed_all\">అన్ని వీడియోలలో అదే</string>\n    <string name=\"player_remember_speed_each\">ప్రతి వీడియోకు</string>\n    <string name=\"msg_player_error_source\">వీడియోను డౌన్‌లోడ్ చేయలేరు</string>\n    <string name=\"msg_player_error_renderer\">ఎంచుకున్న వీడియో ఆకృతికి మద్దతు లేదు</string>\n    <string name=\"msg_player_error_unexpected\">తెలియని వీడియో డీకోడర్ లోపం</string>\n    <string name=\"video_aspect\">కారక నిష్పత్తి</string>\n    <string name=\"player_tweaks\">డెవలపర్ ఎంపికలు</string>\n    <string name=\"header_uploads\">అప్‌లోడ్‌లు</string>\n    <string name=\"player_show_global_clock\">వీడియో పైన గడియారాన్ని చూపించు</string>\n    <string name=\"uploads_old_look\">అప్‌లోడ్‌ల విభాగం యొక్క పాత రూపాన్ని తిరిగి మార్చండి</string>\n    <string name=\"background_playback_activation\">నేపథ్య ప్లేబ్యాక్ (క్రియాశీలత)</string>\n    <string name=\"channels_old_look\">ఛానెల్స్ విభాగం యొక్క పాత రూపాన్ని తిరిగి మార్చండి</string>\n    <string name=\"channels_auto_load\">ఆటో లోడ్ ఛానెల్స్ విభాగం కంటెంట్</string>\n    <string name=\"return_to_background_video\">నేపథ్యంలో నడుస్తున్న వీడియోకు తిరిగి వెళ్ళు</string>\n    <string name=\"pinned_to_sidebar\">సైడ్‌బార్‌కు పిన్ చేయండి</string>\n    <string name=\"unpinned_from_sidebar\">సైడ్‌బార్ నుండి అన్‌పిన్ చేయండి</string>\n    <string name=\"dialog_select_country\">దేశం</string>\n    <string name=\"settings_language_country\">భాష /దేశం</string>\n    <string name=\"share_embed_link\">పొందుపరచండి లింక్‌ను భాగస్వామ్యం చేయండి</string>\n    <string name=\"card_text_scroll_factor\">కార్డ్ టెక్స్ట్ స్క్రోల్ వేగం</string>\n    <string name=\"preferred_update_source\">ఇష్టపడే నవీకరణ మూలం</string>\n<!-- Generated by Automatic String Resource Translation -->\n<!--          https://asrt.gluege.boerde.de             -->\n</resources>"
  },
  {
    "path": "common/src/main/res/values-th/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"header_home\">หน้าแรก</string>\n    <string name=\"title_search\">ค้นหา</string>\n    <string name=\"header_subscriptions\">การติดตาม</string>\n    <string name=\"header_history\">ประวัติการเข้าชม</string>\n    <string name=\"header_music\">เพลง</string>\n    <string name=\"header_news\">ข่าวสาร</string>\n    <string name=\"header_gaming\">เกม</string>\n    <string name=\"header_playlists\">เพลย์ลิสต์</string>\n    <string name=\"header_settings\">การตั้งค่า</string>\n    <string name=\"header_channels\">รายชื่อช่อง</string>\n    <string name=\"subscriptions_signin_title\">ดูวิดีโอล่าสุดจากช่องที่คุณชื่นชอบ</string>\n    <string name=\"subscriptions_signin_subtitle\">ลงชื่อเข้าใช้เพื่อดูช่องที่คุณติดตาม</string>\n    <string name=\"subscriptions_signin_button_text\">ลงชื่อเข้าใช้</string>\n    <string name=\"library_signin_to_show_more\">ดูวิดีโอที่คุณกดชอบ บันทึก หรือซื้อแล้ว</string>\n    <string name=\"library_signin_subtitle\">ลงชื่อเข้าใช้เพื่อดูคลังวิดีโอ</string>\n    <string name=\"action_signin\">ลงชื่อเข้าใช้</string>\n    <string name=\"title_video_formats\">รูปแบบไฟล์วิดีโอ</string>\n    <string name=\"title_audio_formats\">รูปแบบไฟล์เสียง</string>\n    <string name=\"subtitle_category_title\">คำบรรยาย</string>\n    <string name=\"playback_queue_category_title\">คิวการเล่น</string>\n    <string name=\"video_max_quality\">อัตโนมัติ (คุณภาพวิดีโอสูงสุด)</string>\n    <string name=\"audio_max_quality\">อัตโนมัติ (คุณภาพเสียงสูงสุด)</string>\n    <string name=\"subtitles_disabled\">ปิด</string>\n    <string name=\"auto_frame_rate\">อัตราเฟรมอัตโนมัติ</string>\n    <string name=\"frame_rate_correction\">กำหนดอัตราเฟรมต่อวินาที:\\n%s</string>\n    <string name=\"category_background_playback\">การเล่นอยู่เบื้องหลัง</string>\n    <string name=\"not_implemented\">ไม่ได้ดำเนินการ</string>\n    <string name=\"option_background_playback_off\">ปิด</string>\n    <string name=\"option_background_playback_pip\">การแสดงภาพซ้อนภาพ</string>\n    <string name=\"option_background_playback_only_audio\">เสียงเท่านั้น</string>\n    <string name=\"playback_settings\">การตั้งค่าการเล่น</string>\n    <string name=\"video_speed\">ความเร็ววิดีโอ</string>\n    <string name=\"resolution_switch\">กำหนดคุณภาพวิดีโอ</string>\n    <string name=\"video_buffer\">วิดีโอบัฟเฟอร์</string>\n    <string name=\"video_buffer_size_low\">ต่ำ</string>\n    <string name=\"video_buffer_size_med\">กลาง</string>\n    <string name=\"video_buffer_size_high\">สูง</string>\n    <string name=\"update_changelog\">บันทึกการเปลี่ยนแปลง</string>\n    <string name=\"install_update\">ติดตั้งการอัปเดต</string>\n    <string name=\"section_is_empty\">อ๊ะ! ไม่เจออะไรที่นี่</string>\n    <string name=\"dialog_add_to_playlist\">บันทึกไปยังเพลย์ลิสต์</string>\n    <string name=\"msg_signed_users_only\">ลงชื่อเข้าใช้แล้วเท่านั้น</string>\n    <string name=\"msg_cant_load_content\">ไม่สามารถเล่นเนื้อหาได้\\n1) ตรวจสอบวันที่และเวลาของอุปกรณ์\\n2) ตรวจสอบการเชื่อมต่ออินเทอร์เน็ต\\n3) ลองลงชื่อเข้าใช้ด้วยบัญชีของคุณ</string>\n    <string name=\"title_video_presets\">พรีเซ็ตวิดีโอ</string>\n    <string name=\"settings_accounts\">บัญชี</string>\n    <string name=\"settings_left_panel\">แก้ไขหมวดหมู่</string>\n    <string name=\"settings_themes\">ธีม</string>\n    <string name=\"settings_other\">อื่นๆ</string>\n    <string name=\"settings_player\">เครื่องเล่นวิดีโอ</string>\n    <string name=\"settings_language\">ภาษา</string>\n    <string name=\"settings_linked_devices\">อุปกรณ์ที่เชื่อมโยง</string>\n    <string name=\"settings_about\">เกี่ยวกับ</string>\n    <string name=\"dialog_account_list\">เลือกบัญชี</string>\n    <string name=\"dialog_account_none\">ไม่ได้เลือก</string>\n    <string name=\"dialog_remove_account\">ออกจากระบบ</string>\n    <string name=\"dialog_add_account\">ลงชื่อเข้าใช้</string>\n    <string name=\"default_lang\">ค่าเริ่มต้น</string>\n    <string name=\"dialog_select_language\">ภาษา</string>\n    <string name=\"subtitle_default\">ค่าเริ่มต้น</string>\n    <string name=\"subtitle_white_semi_transparent\">สีขาวบนพื้นกึ่งโปร่งใส</string>\n    <string name=\"subtitle_style\">รูปแบบคำบรรยาย</string>\n    <string name=\"subtitle_language\">คำบรรยาย</string>\n    <string name=\"action_search\">เริ่มการค้นหา</string>\n    <string name=\"settings_main_ui\">การแสดงผล</string>\n    <string name=\"dialog_main_ui\">การแสดงผล</string>\n    <string name=\"card_animated_previews\">Animated previews</string>\n    <string name=\"web_site\">เว็บไซต์</string>\n    <string name=\"donation\">การบริจาค</string>\n    <string name=\"dialog_about\">เกี่ยวกับ</string>\n    <string name=\"dialog_player_ui\">เครื่องเล่นวีดีโอ</string>\n    <string name=\"player_show_ui_on_pause\">แสดง UI เมื่อหยุดชั่วคราว</string>\n    <string name=\"player_pause_on_ok\">หยุดชั่วคราวด้วยปุ่ม OK</string>\n    <string name=\"player_ok_button_behavior\">ค่ากำหนดของปุ่ม OK</string>\n    <string name=\"player_only_ui\">แสดง UI เท่านั้น</string>\n    <string name=\"player_ui_and_pause\">แสดง UI และหยุดชั่วคราว</string>\n    <string name=\"player_only_pause\">หยุดชั่วคราวเท่านั้น</string>\n    <string name=\"check_for_updates\">ตรวจสอบการอัปเดต</string>\n    <string name=\"update_not_found\">คุณใช้เวอร์ชั่นล่าสุด</string>\n    <string name=\"update_in_progress\">รอ…</string>\n    <string name=\"player_ui_hide_behavior\">ซ่อน UI อัตโนมัติ</string>\n    <string name=\"option_never\">ไม่เลย</string>\n    <string name=\"side_panel_sections\">ตั้งค่าเมนูหลัก</string>\n    <string name=\"boot_to_section\">หน้าจอเริ่มต้น</string>\n    <string name=\"large_ui\">UI ขนาดใหญ่</string>\n    <string name=\"video_grid_scale\">ขนาดตารางวิดีโอ</string>\n    <string name=\"scale_ui\">ขนาดของ UI</string>\n    <string name=\"color_scheme\">ธีมสี</string>\n    <string name=\"color_scheme_default\">ค่าเริ่มต้น</string>\n    <string name=\"color_scheme_red_grey\">แดง-เทา</string>\n    <string name=\"color_scheme_red\">แดง</string>\n    <string name=\"color_scheme_dark_grey\">เทาเข้ม</string>\n    <string name=\"disable_update_check\">ปิดตรวจสอบการอัปเดต</string>\n    <string name=\"show_again\">แสดงอีกครั้ง</string>\n    <string name=\"check_updates_auto\">แจ้งเตือนเมื่อมีเวอร์ชั่นใหม่</string>\n    <string name=\"select_account_on_boot\">เริ่มต้นด้วยการเลือกบัญชี</string>\n    <string name=\"player_other\">อื่นๆ</string>\n    <string name=\"player_full_date\">วันที่และเวลาในคำอธิบายวิดีโอ</string>\n    <string name=\"open_channel\">ไปที่ช่อง</string>\n    <string name=\"open_playlist\">ไปที่เพลย์ลิสต์</string>\n    <string name=\"not_interested\">ไม่สนใจ</string>\n    <string name=\"you_wont_see_this_video\">วิดีโอถูกลบออกจากการแนะนำ</string>\n    <string name=\"settings_search\">ค้นหา</string>\n    <string name=\"dialog_search\">ค้นหา</string>\n    <string name=\"instant_voice_search\">พูดเพื่อค้นหา</string>\n    <string name=\"option_background_playback_behind\">เล่นบนพื้นหลัง</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">ไม่สามารถเล่นเนื้อหาถัดไปได้</string>\n    <string name=\"card_multiline_title\">Multiline titles</string>\n    <string name=\"cards_style\">รูปแบบการ์ด</string>\n    <string name=\"repeat_mode_all\">เล่นวิดีโอทั้งหมด</string>\n    <string name=\"repeat_mode_one\">เล่นซ้ำวิดีโอปัจจุบัน</string>\n    <string name=\"repeat_mode_pause\">หยุดชั่วคราวเมื่อวิดีโอสิ้นสุด</string>\n    <string name=\"repeat_mode_pause_alt\">หยุดชั่วคราวเมื่อเพลย์ลิสต์สิ้นสุด</string>\n    <string name=\"repeat_mode_none\">ปิดแอปเมื่อวิดีโอสิ้นสุด</string>\n    <string name=\"subscribed_to_channel\">ติดตามแล้ว</string>\n    <string name=\"unsubscribed_from_channel\">ยกเลิกการติดตามแล้ว</string>\n    <string name=\"subtitle_yellow_transparent\">สีเหลืองบนพื้นโปร่งใส</string>\n    <string name=\"subtitle_white_black\">สีขาวบนพื้นสีดำ</string>\n    <string name=\"player_seek_preview\">แสดงตัวอย่างของเนื้อหาเมื่อกรอวิดีโอ</string>\n    <string name=\"color_scheme_dark_grey_oled\">เทาเข้ม (OLED)</string>\n    <string name=\"channels_section_sorting\">การเรียงลำดับรายชื่อช่อง</string>\n    <string name=\"sorting_by_new_content\">เนื้อหาใหม่</string>\n    <string name=\"sorting_alphabetically\">ตามตัวอักษร</string>\n    <string name=\"sorting_last_viewed\">ดูล่าสุด</string>\n    <string name=\"player_pause_when_seek\">หยุดชั่วคราวเมื่อกรอวิดีโอ</string>\n    <string name=\"playlists_style\">รูปแบบของส่วนเพลย์ลิสต์</string>\n    <string name=\"playlists_style_grid\">ตาราง</string>\n    <string name=\"playlists_style_rows\">แถว</string>\n    <string name=\"player_show_clock\">แสดงนาฬิกา</string>\n    <string name=\"player_show_remaining_time\">แสดงเวลาที่เหลือ</string>\n    <string name=\"player_show_ending_time\">แสดงเวลาสิ้นสุด</string>\n    <string name=\"open_channel_uploads\">ไปที่อัปโหลดของช่อง</string>\n    <string name=\"app_exit_shortcut\">ค่ากำหนดการออกจากแอป</string>\n    <string name=\"app_exit_none\">ไม่เลย</string>\n    <string name=\"app_double_back_exit\">กดปุ่มกลับสองครั้ง</string>\n    <string name=\"app_single_back_exit\">กดปุ่มกลับหนึ่งครั้ง</string>\n    <string name=\"action_video_zoom\">ขยายวิดีโอ</string>\n    <string name=\"video_zoom\">ขยายวิดีโอ</string>\n    <string name=\"video_zoom_default\">ค่าเริ่มต้น</string>\n    <string name=\"video_zoom_fit_width\">ความกว้างพอดี</string>\n    <string name=\"video_zoom_fit_height\">ความสูงพอดี</string>\n    <string name=\"video_zoom_fit_both\">พอดีทั้งความกว้างและความสูง</string>\n    <string name=\"video_zoom_stretch\">ยืด</string>\n    <string name=\"color_scheme_teal\">น้ำเงิน-เขียว</string>\n    <string name=\"color_scheme_teal_oled\">น้ำเงิน-เขียว (OLED)</string>\n    <string name=\"player_seek_preview_none\">ปิด</string>\n    <string name=\"player_seek_preview_single\">Single frame</string>\n    <string name=\"player_seek_preview_carousel\">Carousel</string>\n    <string name=\"player_seek_preview_carousel_slow\">Carousel (ช้า)</string>\n    <string name=\"player_seek_preview_carousel_fast\">Carousel (เร็ว)</string>\n    <string name=\"unsubscribe_from_channel\">ยกเลิกการติดตาม</string>\n    <string name=\"card_title_lines_num\">Card title lines num</string>\n    <string name=\"card_auto_scrolled_title\">Auto-scroll cropped title</string>\n    <string name=\"share_link\">แชร์ลิงก์</string>\n    <string name=\"subscribe_to_channel\">ติดตามช่อง</string>\n    <string name=\"wait_data_loading\">โปรดรอในขณะที่โหลดข้อมูล...</string>\n    <string name=\"auto_frame_rate_pause\">อัตราเฟรมอัตโนมัติ (หยุดชั่วคราวเมื่อเปลี่ยน)</string>\n    <string name=\"auto_frame_rate_applying\">ใช้อัตราเฟรมอัตโนมัติ %sx%s@%s</string>\n    <string name=\"audio_shift\">การหน่วงเวลาเสียง</string>\n    <string name=\"player_remember_speed\">บันทึกความเร็วในการเล่น</string>\n    <string name=\"mark_channel_as_watched\">ทำเครื่องหมาย</string>\n    <string name=\"channel_marked_as_watched\">ทำเครื่องหมายแล้ว</string>\n    <string name=\"dialog_add_device\">เพิ่มอุปกรณ์</string>\n    <string name=\"dialog_remove_all_devices\">ลบอุปกรณ์ทั้งหมด</string>\n    <string name=\"device_link_enabled\">เชื่อมต่ออุปกรณ์แล้ว</string>\n    <string name=\"device_connected\">อุปกรณ์ \\\"%s\\\" ได้รับการเชื่อมโยงแล้ว</string>\n    <string name=\"device_disconnected\">อุปกรณ์ \\\"%s\\\" ได้ยกเลิกการเชื่อมโยงแล้ว</string>\n    <string name=\"settings_ui_scale\">ขนาดของ UI</string>\n    <string name=\"msg_restart_app\">โปรดรีสตาร์ทแอปเพื่อใช้การตั้งค่านี้</string>\n    <string name=\"settings_block\">การปิดกั้นเนื้อหา</string>\n    <string name=\"msg_applying\">Applying %s…</string>\n    <string name=\"msg_done\">เสร็จแล้ว</string>\n    <string name=\"settings_general\">ทั่วไป</string>\n    <string name=\"settings_video\">วิดีโอ</string>\n    <string name=\"content_block_confirm_skip\">Confirm on skip</string>\n    <string name=\"content_block_categories\">Skip segments</string>\n    <string name=\"content_block_sponsor\">Sponsor</string>\n    <string name=\"content_block_intro\">Intermission/intro animation</string>\n    <string name=\"content_block_outro\">Endcards/credits</string>\n    <string name=\"content_block_interaction\">Interaction reminder (subscribe)</string>\n    <string name=\"content_block_self_promo\">Unpaid/self promotion</string>\n    <string name=\"content_block_music_off_topic\">Non-music section of clip</string>\n    <string name=\"confirm_segment_skip\">Skip segment \\\"%s\\\"?</string>\n    <string name=\"cancel_segment_skip\">Cancel segment skip</string>\n    <string name=\"msg_skipping_segment\">Skipping segment \\\"%s\\\"…</string>\n    <string name=\"content_block_notification_type\">Notification type</string>\n    <string name=\"content_block_notify_none\">Without notification</string>\n    <string name=\"content_block_notify_toast\">Toast</string>\n    <string name=\"content_block_notify_dialog\">Confirmation dialog</string>\n    <string name=\"return_to_launcher\">Return to the launcher from ATV channels/search</string>\n    <string name=\"intent_force_close\">Force exit if video has been opened from external source</string>\n    <string name=\"btn_confirm\">Confirm</string>\n    <string name=\"player_low_video_quality\">คุณภาพวิดีโอต่ำ</string>\n    <string name=\"remote_session_closed\">Remote session has been closed</string>\n    <string name=\"msg_mode_switch_error\">Can\\'t switch display mode to \\\"%s\\\"</string>\n    <string name=\"msg_player_error\">Player error occurred %s. Restarting playback…</string>\n    <string name=\"video_preset_disabled\">ไม่ได้ตั้งค่า</string>\n    <string name=\"video_preset_enabled\">รูปแบบของวิดีโอจะถูกตั้งค่าตามที่ตั้งไว้</string>\n    <string name=\"player_sleep_timer\">เวลานอน</string>\n    <string name=\"player_show_quality_info\">แสดงข้อมูลคุณภาพวิดีโอ</string>\n    <string name=\"player_remember_each_speed\">บันทึกความเร็วในการเล่น</string>\n    <string name=\"player_remember_speed_none\">ไม่ได้เลือก</string>\n    <string name=\"player_remember_speed_all\">การเล่นทั้งหมด</string>\n    <string name=\"player_remember_speed_each\">การเล่นแต่ละครั้งเท่านั้น</string>\n    <string name=\"msg_player_error_source\">Can\\'t download the video</string>\n    <string name=\"msg_player_error_renderer\">Selected video format isn\\'t supported</string>\n    <string name=\"msg_player_error_unexpected\">Unknown video decoder error</string>\n    <string name=\"video_aspect\">อัตราส่วนภาพ</string>\n    <string name=\"player_tweaks\">ตัวเลือกนักพัฒนา</string>\n    <string name=\"header_uploads\">อัปโหลด</string>\n    <string name=\"player_show_global_clock\">แสดงนาฬิกาบนหน้าจอเสมอ</string>\n    <string name=\"player_show_global_ending_time\">แสดงเวลาสิ้นสุดบนหน้าจอเสมอ</string>\n    <string name=\"uploads_old_look\">Revert old look of Uploads section</string>\n    <string name=\"background_playback_activation\">การเล่นบนพื้นหลัง (เปิดใช้งาน)</string>\n    <string name=\"channels_old_look\">Revert old look of Channels section</string>\n    <string name=\"channels_auto_load\">Auto load Channels section content</string>\n    <string name=\"return_to_background_video\">กลับไปที่วิดีโอที่เล่นบนพื้นหลัง</string>\n    <string name=\"pin_unpin_from_sidebar\">ปักหมุด/ถอนหมุด จากแถบด้านข้าง</string>\n    <string name=\"pin_unpin_playlist\">ปักหมุด/ถอนหมุด เพลย์ลิสต์</string>\n    <string name=\"pin_unpin_channel\">ปักหมุด/ถอนหมุด เพลย์ลิสต์</string>\n    <string name=\"pinned_to_sidebar\">ปักหมุดไปที่แถบด้านข้างแล้ว</string>\n    <string name=\"unpin_from_sidebar\">ถอนหมุดจากแถบด้านข้าง</string>\n    <string name=\"unpinned_from_sidebar\">ถอนหมุดจากแถบด้านข้างแล้ว</string>\n    <string name=\"dialog_select_country\">ประเทศ</string>\n    <string name=\"settings_language_country\">ภาษา/ประเทศ</string>\n    <string name=\"share_embed_link\">Share embed link</string>\n    <string name=\"card_text_scroll_factor\">Card text scroll speed</string>\n    <string name=\"preferred_update_source\">Select update source</string>\n    <string name=\"hide_shorts\">Hide shorts from Subscriptions</string>\n    <string name=\"key_remapping\">Key remapping</string>\n    <string name=\"screen_dimming\">การลดแสงหน้าจอ</string>\n    <string name=\"removed_from_playback_queue\">ลบออกจากคิวการเล่น</string>\n    <string name=\"added_to_playback_queue\">เพิ่มไปยังคิวการเล่น</string>\n    <string name=\"add_remove_from_playback_queue\">เพิ่ม/ลบ จากคิวการเล่น</string>\n    <string name=\"proxy_enabled\">Proxy enabled</string>\n    <string name=\"proxy_disabled\">Proxy disabled</string>\n    <string name=\"uploads_row_name\">อัปโหลด</string>\n    <string name=\"playlists_row_name\">เพลย์ลิสต์ที่สร้างขึ้น</string>\n    <string name=\"popular_uploads_row_name\">อัปโหลดยอดนิยม</string>\n    <string name=\"breaking_news_row_name\">ข่าวด่วน</string>\n    <string name=\"covid_news_row_name\">ข่าว COVID-19</string>\n    <string name=\"enable_voice_search\">เปิดใช้งานการค้นหาด้วยเสียง</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">ติดตาม/ยกเลิกการติดตาม จากช่องนี้</string>\n    <string name=\"app_backup_restore\">การสำรอง/คืนค่า</string>\n    <string name=\"app_backup\">สำรองข้อมูล</string>\n    <string name=\"app_restore\">คืนค่าข้อมูล</string>\n    <string name=\"skip_each_segment_once\">Don\\'t skip segments again</string>\n    <string name=\"disable_ok_long_press\">Disable OK button long press</string>\n    <string name=\"player_time_correction\">Display the time with respect of speed</string>\n    <string name=\"sponsor_color_markers\">Color markers on progress bar</string>\n    <string name=\"refresh_section\">Refresh section</string>\n    <string name=\"option_disabled\">ปิดการใช้งาน</string>\n    <string name=\"live_now_row_name\">ไลฟ์สดอยู่</string>\n    <string name=\"run_in_background\">ใช้งานบนพื้นหลัง</string>\n    <string name=\"settings_remote_control\">การควบคุมระยะไกล</string>\n    <string name=\"background_service_started\">Background service started</string>\n    <string name=\"dialog_add_to\">เพิ่มไปที่ %s</string>\n    <string name=\"dialog_remove_from\">ลบจาก %s</string>\n    <string name=\"added_to\">เพิ่มไปที่ %s แล้ว</string>\n    <string name=\"removed_from\">ลบจาก %s แล้ว</string>\n    <string name=\"video_preset_adaptive\">Adaptive</string>\n    <string name=\"content_block_preview_recap\">Preview or recap of the video</string>\n    <string name=\"content_block_highlight\">Point or highlight of the video</string>\n    <string name=\"removed_from_history\">วิดีโอถูกลบออกจากประวัติการเข้าชม</string>\n    <string name=\"remove_from_history\">ลบจากประวัติการเข้าชม</string>\n    <string name=\"upload_date\">วันที่อัปโหลด</string>\n    <string name=\"upload_date_any\">ทุกเวลา</string>\n    <string name=\"upload_date_today\">วันนี้</string>\n    <string name=\"upload_date_this_week\">สัปดาห์นี้</string>\n    <string name=\"upload_date_this_month\">เดือนนี้</string>\n    <string name=\"upload_date_this_year\">ปีนี้</string>\n    <string name=\"double_refresh_rate\">อัตราการรีเฟรชสองเท่า</string>\n    <string name=\"upload_date_last_hour\">ชั่วโมงที่ผ่านมา</string>\n    <string name=\"focus_on_search_results\">Autofocus on search results</string>\n    <string name=\"context_menu\">Context menu</string>\n    <string name=\"add_remove_from_recent_playlist\">เพิ่ม/ลบ จากเพลลิสต์ล่าสุด</string>\n    <string name=\"hide_settings_section\">ซ่อนส่วนการตั้งค่า (อันตราย!)</string>\n    <string name=\"volume\">ระดับเสียง %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s วินาที</string>\n    <string name=\"audio_shift_sec\">%s วินาที</string>\n    <string name=\"ui_hide_timeout_sec\">%s วินาที</string>\n    <string name=\"screen_dimming_timeout_min\">%s นาที</string>\n    <string name=\"mark_all_channels_watched\">ทำเครื่องหมายทั้งหมด</string>\n    <string name=\"move_section_up\">ขยับขึ้นไป</string>\n    <string name=\"move_section_down\">ขยับลงไป</string>\n    <string name=\"player_buttons\">ตั้งค่าปุ่มเครื่องเล่นวิดีโอ</string>\n    <string name=\"action_playlist_add\">เพิ่มไปยังเพลย์ลิสต์</string>\n    <string name=\"action_video_stats\">สถานะวิดีโอ</string>\n    <string name=\"action_subscribe\">ติดตาม</string>\n    <string name=\"action_channel\">ไปที่ช่อง</string>\n    <string name=\"action_pip\">PIP</string>\n    <string name=\"action_screen_off\">ปิดหน้าจอ</string>\n    <string name=\"action_playback_queue\">คิวการเล่น</string>\n    <string name=\"action_video_speed\">ความเร็ววิดีโอ</string>\n    <string name=\"action_subtitles\">คำบรรยาย</string>\n    <string name=\"action_like\">ชอบ</string>\n    <string name=\"action_dislike\">ไม่ชอบ</string>\n    <string name=\"action_play_pause\">เล่น/หยุดชั่วคราว</string>\n    <string name=\"action_repeat_mode\">โหมดการเล่น</string>\n    <string name=\"action_next\">วิดีโอถัดไป</string>\n    <string name=\"action_previous\">วิดีโอก่อนหน้า</string>\n    <string name=\"action_high_quality\">คุณภาพวีดีโอ</string>\n    <string name=\"content_block_no_skipping_mode\">No-skipping mode</string>\n    <string name=\"content_block_action_type\">Choose action</string>\n    <string name=\"content_block_action_none\">Do nothing</string>\n    <string name=\"content_block_action_only_skip\">Only skip</string>\n    <string name=\"content_block_action_toast\">Skip with notification</string>\n    <string name=\"content_block_action_dialog\">Show confirmation dialog</string>\n    <string name=\"content_block_filler\">Off-topic (filler)</string>\n    <string name=\"keyboard_auto_show\">แสดงแป้นพิมพ์โดยอัตโนมัติ</string>\n    <string name=\"cancel_dialog\">ยกเลิก</string>\n    <string name=\"msg_player_error_source2\">Video source isn\\'t working or incorrect time</string>\n    <string name=\"hide_upcoming\">Hide upcoming from Subscriptions</string>\n    <string name=\"search_background_playback\">แสดงภาพซ้อนภาพเมื่อค้นหาจากเครื่องเล่นวิดีโอ</string>\n    <string name=\"trending_row_name\">ที่ได้รับความนิยม</string>\n    <string name=\"player_seek_type\">ค่ากำหนดการกรอวิดีโอ</string>\n    <string name=\"player_seek_regular\">ปกติ</string>\n    <string name=\"player_seek_confirmation_pause\">With confirmation (หยุดชั่วคราวเมื่อกรอวิดีโอ)</string>\n    <string name=\"player_seek_confirmation_play\">With confirmation (เล่นขณะกรอวิดีโอ)</string>\n    <string name=\"hide_shorts_from_home\">ซ่อน shorts จากหน้าแรก</string>\n    <string name=\"finish_on_disconnect\">ปิดแอปเมื่อยกเลิกการเชื่อมโยงกับโทรศัพท์</string>\n    <string name=\"update_error\">อัปเดตข้อผิดพลาด</string>\n    <string name=\"hide_shorts_from_history\">ซ่อน shorts จากประวัติการเข้าชม</string>\n    <string name=\"disable_screensaver\">Disable screensaver</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-tr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"header_home\">Ana Sayfa</string>\n    <string name=\"title_search\">Arama</string>\n    <string name=\"header_subscriptions\">Abonelikler</string>\n    <string name=\"header_history\">Geçmiş</string>\n    <string name=\"header_music\">Müzik</string>\n    <string name=\"header_news\">Haberler</string>\n    <string name=\"header_gaming\">Oyun</string>\n    <string name=\"header_playlists\">Oynatma Listeleri</string>\n    <string name=\"header_settings\">Ayarlar</string>\n    <string name=\"header_channels\">Kanallar</string>\n    <string name=\"subscriptions_signin_title\">Sevdiğiniz kanallardan en son haberleri görün</string>\n    <string name=\"subscriptions_signin_subtitle\">Aboneliklerinizi görmek için oturum açın</string>\n    <string name=\"subscriptions_signin_button_text\">OTURUM AÇ</string>\n    <string name=\"library_signin_to_show_more\">Beğendiğiniz, kaydettiğiniz ya da abone olduğunuz videoları izleyin</string>\n    <string name=\"library_signin_subtitle\">Kitaplığınızı görmek için oturum açın</string>\n    <string name=\"action_signin\">OTURUM AÇ</string>\n    <string name=\"title_video_formats\">Video biçimleri</string>\n    <string name=\"title_audio_formats\">Ses biçimleri</string>\n    <string name=\"subtitle_category_title\">Altyazılar</string>\n    <string name=\"playback_queue_category_title\">Oynatma sırası</string>\n    <string name=\"video_max_quality\">Otomatik (en iyi kalite)</string>\n    <string name=\"audio_max_quality\">Otomatik (en iyi kalite)</string>\n    <string name=\"subtitles_disabled\">Kapalı</string>\n    <string name=\"auto_frame_rate\">Otomatik kare hızı</string>\n    <string name=\"frame_rate_correction\">Fps düzeltme:\\n%s</string>\n    <string name=\"category_background_playback\">Arka planda oynatma</string>\n    <string name=\"not_implemented\">Uygulanmadı</string>\n    <string name=\"not_supported_by_device\">Cihaz bu özelliği desteklemiyor</string>\n    <string name=\"option_background_playback_off\">Devre dışı</string>\n    <string name=\"option_background_playback_pip\">Resim içinde resim</string>\n    <string name=\"option_background_playback_only_audio\">Sadece ses</string>\n    <string name=\"playback_settings\">Oynatma kalitesi ayarları</string>\n    <string name=\"video_speed\">Video hızı</string>\n    <string name=\"resolution_switch\">Çözünürlüğü değiştir</string>\n    <string name=\"video_buffer\">Video arabelleği</string>\n    <string name=\"video_buffer_size_none\">Hiçbiri</string>\n    <string name=\"video_buffer_size_lowest\">En düşük</string>\n    <string name=\"video_buffer_size_low\">Düşük</string>\n    <string name=\"video_buffer_size_med\">Orta</string>\n    <string name=\"video_buffer_size_high\">Yüksek</string>\n    <string name=\"video_buffer_size_highest\">En yüksek</string>\n    <string name=\"update_changelog\">Değişim günlüğü</string>\n    <string name=\"install_update\">Güncellemeyi yükle</string>\n    <string name=\"section_is_empty\">Hata! Burada hiçbir şey yok.</string>\n    <string name=\"dialog_add_to_playlist\">Oynatma listesine Ekle/Kaldır</string>\n    <string name=\"msg_signed_users_only\">Sadece oturum açmış kullanıcılar</string>\n    <string name=\"msg_cant_load_content\">İçerik yüklenemiyor.\\n1) İzleme geçmişini açın.\\n2) Cihazdaki tarih ve saati kontrol edin.\\n3) Ağ bağlantısını kontrol edin.\\n4) Vekil Sunucu ayarlarınızı kontrol edin.\\n5) Hesabınızda oturum açmayı deneyin.\\n6) Belki burada hiçbir şey yoktur.</string>\n    <string name=\"title_video_presets\">Video ön ayarları</string>\n    <string name=\"settings_accounts\">Hesaplar</string>\n    <string name=\"settings_left_panel\">Kategorileri düzenle</string>\n    <string name=\"settings_themes\">Temalar</string>\n    <string name=\"settings_other\">Diğer</string>\n    <string name=\"settings_player\">Oynatıcı</string>\n    <string name=\"settings_language\">Diller</string>\n    <string name=\"settings_linked_devices\">Bağlı cihazlar</string>\n    <string name=\"settings_about\">Hakkında</string>\n    <string name=\"dialog_account_list\">Hesap seç</string>\n    <string name=\"dialog_account_none\">Hiçbiri</string>\n    <string name=\"dialog_remove_account\">Oturumu kapat</string>\n    <string name=\"dialog_add_account\">Oturum aç</string>\n    <string name=\"default_lang\">Varsayılan</string>\n    <string name=\"original_lang\">Orijinal</string>\n    <string name=\"dialog_select_language\">Uygulama dili</string>\n    <string name=\"subtitle_default\">Varsayılan</string>\n    <string name=\"subtitle_white_semi_transparent\">Beyaz (Yarı saydam arka plan)</string>\n    <string name=\"subtitle_style\">Altyazı stili</string>\n    <string name=\"subtitle_language\">Altyazı dili</string>\n    <string name=\"action_search\">Ara</string>\n    <string name=\"settings_main_ui\">Kullanıcı arayüzü</string>\n    <string name=\"dialog_main_ui\">Kullanıcı arayüzü</string>\n    <string name=\"card_animated_previews\">Hareketli ön izlemeler</string>\n    <string name=\"web_site\">Web sitesi</string>\n    <string name=\"donation\">Bağış</string>\n    <string name=\"dialog_about\">Hakkında</string>\n    <string name=\"dialog_player_ui\">Video oynatıcı</string>\n    <string name=\"player_show_ui_on_pause\">Duraklatıldığında kullanıcı arayüzünü göster</string>\n    <string name=\"player_pause_on_ok\">TAMAM tuşu oynatmayı duraklatır</string>\n    <string name=\"player_ok_button_behavior\">TAMAM düğmesi davranışı</string>\n    <string name=\"player_only_ui\">Sadece arayüz</string>\n    <string name=\"player_ui_and_pause\">Arayüz ve duraklat</string>\n    <string name=\"player_only_pause\">Sadece duraklat</string>\n    <string name=\"player_toggle_speed\">Hızı açma/kapama</string>\n    <string name=\"check_for_updates\">Güncellemeyi kontrol et</string>\n    <string name=\"update_not_found\">En son sürümü kullanıyorsunuz</string>\n    <string name=\"update_in_progress\">Bekleyin…</string>\n    <string name=\"player_ui_hide_behavior\">Kullanıcı arayüzünü otomatik gizle</string>\n    <string name=\"option_never\">Asla</string>\n    <string name=\"side_panel_sections\">Bölümleri ayarla</string>\n    <string name=\"boot_to_section\">Açılışta başlanacak bölüm</string>\n    <string name=\"large_ui\">Büyük kullanıcı arayüzü</string>\n    <string name=\"video_grid_scale\">Video ızgara ölçeği</string>\n    <string name=\"scale_ui\">Kullanıcı arayüz ölçeği</string>\n    <string name=\"color_scheme\">Renk düzeni</string>\n    <string name=\"color_scheme_default\">Varsayılan</string>\n    <string name=\"color_scheme_red_grey\">Kırmızı-Gri</string>\n    <string name=\"color_scheme_red\">Kırmızı</string>\n    <string name=\"color_scheme_dark_grey\">Koyu Gri</string>\n    <string name=\"disable_update_check\">Güncelleme kontrolünü devre dışı bırak</string>\n    <string name=\"show_again\">Tekrar göster</string>\n    <string name=\"check_updates_auto\">Güncelleme hakkında bilgi ver</string>\n    <string name=\"select_account_on_boot\">Açılışta hesap seçimini göster</string>\n    <string name=\"player_other\">Çeşitli</string>\n    <string name=\"player_full_date\">Açıklamada tam tarih</string>\n    <string name=\"open_channel\">Kanalı aç</string>\n    <string name=\"open_playlist\">Oynatma listesini aç</string>\n    <string name=\"not_interested\">İlgilenmiyorum</string>\n    <string name=\"not_recommend_channel\">Kanalı önerme</string>\n    <string name=\"you_wont_see_this_video\">Video önerilenlerden kaldırıldı</string>\n    <string name=\"you_wont_see_this_channel\">Bu kanalı önerilenlerde görmeyeceksiniz</string>\n    <string name=\"settings_search\">Arama</string>\n    <string name=\"dialog_search\">Arama</string>\n    <string name=\"instant_voice_search\">Sesli arama</string>\n    <string name=\"option_background_playback_behind\">Arka planda oynat</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">Sonraki video bilgisi henüz yüklenmedi</string>\n    <string name=\"card_multiline_title\">Çok satırlı başlıklar</string>\n    <string name=\"cards_style\">Kart stili</string>\n    <string name=\"repeat_mode_all\">Videoları sürekli oynat</string>\n    <string name=\"repeat_mode_one\">Videoyu tekrarla</string>\n    <string name=\"repeat_mode_pause\">Her videodan sonra oynatmayı duraklat (kuyruk hariç)</string>\n    <string name=\"repeat_mode_pause_alt\">Sadece oynatma listesi videolarını sürekli oynat</string>\n    <string name=\"repeat_mode_none\">Videodan sonra oynatmayı durdur (kuyruk hariç)</string>\n    <string name=\"subscribed_to_channel\">Abone olundu</string>\n    <string name=\"unsubscribed_from_channel\">Abonelikten çıkıldı</string>\n    <string name=\"subtitle_yellow_transparent\">Sarı (Saydam arka plan)</string>\n    <string name=\"subtitle_white_transparent\">Beyaz (Saydam arka plan)</string>\n    <string name=\"subtitle_white_black\">Beyaz (Siyah arka plan)</string>\n    <string name=\"player_seek_preview\">Ararken ön izleme</string>\n    <string name=\"color_scheme_dark_grey_oled\">Koyu Gri (OLED)</string>\n    <string name=\"channels_section_sorting\">Kanallar bölümü sıralaması</string>\n    <string name=\"sorting_by_new_content\">Yeni içerik</string>\n    <string name=\"sorting_alphabetically\">Alfabetik</string>\n    <string name=\"sorting_last_viewed\">Son görüntülenen</string>\n    <string name=\"player_pause_when_seek\">Ararken duraklat</string>\n    <string name=\"playlists_style\">Oynatma Listeleri bölümü stili</string>\n    <string name=\"playlists_style_grid\">Izgara</string>\n    <string name=\"playlists_style_rows\">Satırlar</string>\n    <string name=\"player_show_clock\">Saati göster</string>\n    <string name=\"player_show_remaining_time\">Kalan süreyi göster</string>\n    <string name=\"player_show_ending_time\">Bitiş saatini göster</string>\n    <string name=\"open_channel_uploads\">Kanal yüklemelerini aç</string>\n    <string name=\"app_exit_shortcut\">Uygulamadan çıkış</string>\n    <string name=\"player_exit_shortcut\">Oynatıcıdan çıkış</string>\n    <string name=\"search_exit_shortcut\">Aramadan çıkış</string>\n    <string name=\"app_exit_none\">Çıkış yapma</string>\n    <string name=\"app_double_back_exit\">Çift geri</string>\n    <string name=\"app_single_back_exit\">Tek geri</string>\n    <string name=\"action_video_zoom\">Video yakınlaştırma</string>\n    <string name=\"video_zoom\">Video yakınlaştırma</string>\n    <string name=\"video_zoom_default\">Varsayılan</string>\n    <string name=\"video_zoom_fit_width\">Genişliğe sığdır</string>\n    <string name=\"video_zoom_fit_height\">Yüksekliğe sığdır</string>\n    <string name=\"video_zoom_fit_both\">Genişliğe ya da yüksekliğe sığdır</string>\n    <string name=\"video_zoom_stretch\">Uzat</string>\n    <string name=\"color_scheme_teal\">Turkuaz</string>\n    <string name=\"color_scheme_teal_oled\">Turkuaz (OLED)</string>\n    <string name=\"player_seek_preview_none\">Devre dışı</string>\n    <string name=\"player_seek_preview_single\">Tek kare</string>\n    <string name=\"player_seek_preview_carousel\">Atlıkarınca</string>\n    <string name=\"player_seek_preview_carousel_slow\">Atlıkarınca (yavaş, ana karelerle)</string>\n    <string name=\"player_seek_preview_carousel_fast\">Atlıkarınca (hızlı, kesin olmayan ön izleme)</string>\n    <string name=\"unsubscribe_from_channel\">Kanal aboneliğini iptal et</string>\n    <string name=\"card_title_lines_num\">Kart başlık satır sayısı</string>\n    <string name=\"card_auto_scrolled_title\">Otomatik kaydırılan kırpılmış başlık</string>\n    <string name=\"share_link\">Bağlantıyı paylaş</string>\n    <string name=\"share_qr_link\">Bağlantıyı paylaş (QR kod)</string>\n    <string name=\"subscribe_to_channel\">Kanala abone ol</string>\n    <string name=\"wait_data_loading\">Lütfen veriler yüklenirken bekleyin…</string>\n    <string name=\"auto_frame_rate_pause\">Otomatik kare hızı değişim duraklaması</string>\n    <string name=\"auto_frame_rate_applying\">Otomatik kare hızı uygulanıyor: %sx%s\\@%s</string>\n    <string name=\"audio_shift\">Ses kaydırma</string>\n    <string name=\"player_remember_speed\">Hızı hatırla</string>\n    <string name=\"mark_channel_as_watched\">İzlendi olarak işaretle</string>\n    <string name=\"channel_marked_as_watched\">Kanal izlendi olarak işaretlendi</string>\n    <string name=\"dialog_add_device\">Cihaz ekle</string>\n    <string name=\"dialog_remove_all_devices\">Tüm cihazları kaldır</string>\n    <string name=\"device_link_enabled\">Bağlantı etkinleştirildi</string>\n    <string name=\"device_connected\">\\\"%s\\\" cihazı bağlandı</string>\n    <string name=\"device_disconnected\">\\\"%s\\\" cihazının baglantısı kesildi</string>\n    <string name=\"settings_ui_scale\">Arayüz ölçegi</string>\n    <string name=\"msg_restart_app\">Lütfen bu ayarları uygulamak için uygulamayı yeniden başlatın</string>\n    <string name=\"settings_block\">İçerik engelleme</string>\n    <string name=\"msg_applying\">Uygulanıyor: %s…</string>\n    <string name=\"msg_done\">Tamamlandı</string>\n    <string name=\"settings_general\">Genel ayarlar</string>\n    <string name=\"settings_video\">Video</string>\n    <string name=\"content_block_confirm_skip\">Atlamayı onayla</string>\n    <string name=\"content_block_categories\">Atlanacak bölümler</string>\n    <string name=\"content_block_sponsor\">Sponsor</string>\n    <string name=\"content_block_intro\">Ara/Giriş animasyonu</string>\n    <string name=\"content_block_outro\">Bitiş kartları/Jenerik</string>\n    <string name=\"content_block_interaction\">Etkileşim hatırlatıcısı (abone olun)</string>\n    <string name=\"content_block_self_promo\">Tanıtımlar</string>\n    <string name=\"content_block_music_off_topic\">Klibin müzik dışı yeri</string>\n    <string name=\"confirm_segment_skip\">\\\"%s\\\" bölümü atlansın mı\\?</string>\n    <string name=\"cancel_segment_skip\">Bölüm atlamayı iptal et</string>\n    <string name=\"msg_skipping_segment\">\\\"%s\\\" bölümü atlanıyor…</string>\n    <string name=\"content_block_notification_type\">Bildirim türü</string>\n    <string name=\"content_block_notify_none\">Bildirim olmadan</string>\n    <string name=\"content_block_notify_toast\">Ekranda göster</string>\n    <string name=\"content_block_notify_dialog\">Onay mesajı</string>\n    <string name=\"return_to_launcher\">ATV Kanallarından/aramadan başlatıcıya dön</string>\n    <string name=\"intent_force_close\">Video harici kaynaktan açıldıysa çıkışı zorla</string>\n    <string name=\"btn_confirm\">Onayla</string>\n    <string name=\"player_low_video_quality\">Düşük video kalitesi</string>\n    <string name=\"remote_session_closed\">Uzak oturum kapatıldı</string>\n    <string name=\"msg_mode_switch_error\">Görüntü modu değiştirilemiyor: \\\"%s\\\"</string>\n    <string name=\"msg_player_error\">Oynatıcı hatası oluştu %s. Oynatma yeniden başlatılıyor…</string>\n    <string name=\"video_preset_disabled\">Ön ayar olmadan</string>\n    <string name=\"video_preset_enabled\">Bir sonraki videonun biçimi ön ayara göre ayarlanacaktır.</string>\n    <string name=\"player_sleep_timer\">Uyku zamanlayıcısı</string>\n    <string name=\"player_show_quality_info\">Video kalite bilgisini göster</string>\n    <string name=\"player_remember_each_speed\">Her video hızını hatırla</string>\n    <string name=\"player_remember_speed_none\">Asla</string>\n    <string name=\"player_remember_speed_all\">Tüm videolarda aynı</string>\n    <string name=\"player_remember_speed_each\">Her video için</string>\n    <string name=\"msg_player_error_source\">Video indirilemiyor</string>\n    <string name=\"msg_player_error_renderer\">Seçilen biçim desteklenmiyor.\\nBaşka bir biçim seçmeyi deneyin.\\nBu işe yaramazsa cihazı yeniden başlatmayı deneyin.</string>\n    <string name=\"msg_player_error_video_renderer\">Seçilen VİDEO biçimi desteklenmiyor.\\nOynatıcıdaki HQ düğmesine basarak başka bir tane seçmeyi deneyin.\\nBu işe yaramazsa cihazı yeniden başlatmayı deneyin.</string>\n    <string name=\"msg_player_error_audio_renderer\">Seçilen SES biçimi desteklenmiyor.\\nOynatıcıdaki HQ düğmesine basarak başka bir tane seçmeyi deneyin.\\nBu işe yaramazsa cihazı yeniden başlatmayı deneyin.</string>\n    <string name=\"msg_player_error_subtitle_renderer\">Seçilen ALTYAZI biçimi desteklenmiyor.\\nOynatıcıdaki CC düğmesine uzun basarak başka bir tane seçmeyi deneyin.\\nBu işe yaramazsa cihazı yeniden başlatmayı deneyin.</string>\n    <string name=\"msg_player_error_unexpected\">Bilinmeyen video çözücü hatası</string>\n    <string name=\"video_aspect\">En boy oranı</string>\n    <string name=\"player_tweaks\">Geliştirici seçenekleri</string>\n    <string name=\"header_uploads\">Yüklemeler</string>\n    <string name=\"player_show_global_clock\">Saati ekranda sürekli göster</string>\n    <string name=\"player_show_global_ending_time\">Video bitiş saatini ekranda sürekli göster</string>\n    <string name=\"app_corner_clock\">Ana Sayfa: Sağ üst köşede saat</string>\n    <string name=\"player_corner_clock\">Oynatıcı: Sağ üst köşede saat</string>\n    <string name=\"player_corner_ending_time\">Oynatıcı: Sağ üst köşede bitiş saati</string>\n    <string name=\"uploads_old_look\">Yüklemeler bölümünün eski görünümününe geri dön</string>\n    <string name=\"background_playback_activation\">Arka planda oynatma (etkinleştirme)</string>\n    <string name=\"channels_old_look\">Kanallar bölümünün eski görünümününe geri dön</string>\n    <string name=\"channels_auto_load\">Kanallar bölümü içeriğini otomatik yükle</string>\n    <string name=\"return_to_background_video\">Arka planda oynatılan videoya dön</string>\n    <string name=\"pin_unpin_from_sidebar\">Kenar çubuğuna Ekle/Kaldır</string>\n    <string name=\"pin_unpin_playlist\">Oynatma listesini kenar çubuğuna Ekle/Kaldır</string>\n    <string name=\"pin_unpin_channel\">Kanalı kenar çubuğuna Ekle/Kaldır</string>\n    <string name=\"pin_playlist\">Onatma listesini kenar çubuğuna ekle</string>\n    <string name=\"pin_channel\">Kanalı kenar çubuğuna ekle</string>\n    <string name=\"pinned_to_sidebar\">Kenar çubuğuna eklendi</string>\n    <string name=\"unpin_from_sidebar\">Kenar çubuğundan kaldır</string>\n    <string name=\"unpin_group_from_sidebar\">Abonelik grubunun kaldır</string>\n    <string name=\"unpinned_from_sidebar\">Kenar çubuğundan kaldırıldı</string>\n    <string name=\"dialog_select_country\">Ülke</string>\n    <string name=\"settings_language_country\">Dil/Ülke</string>\n    <string name=\"share_embed_link\">Yerleştirme video bağlantısını paylaş</string>\n    <string name=\"card_text_scroll_factor\">Kart metin kaydırma hızı</string>\n    <string name=\"preferred_update_source\">Güncelleme kaynağını seç</string>\n    <string name=\"hide_shorts\">Shortsları Abonelikler\\'de gizle</string>\n    <string name=\"hide_shorts_channel\">Shortsları Kanalda gizle</string>\n    <string name=\"key_remapping\">Tuş yeniden atama</string>\n    <string name=\"screen_dimming\">Ekran karartma</string>\n    <string name=\"removed_from_playback_queue\">Oynatma sırasından kaldırıldı</string>\n    <string name=\"added_to_playback_queue\">Oynatma sırasına eklendi</string>\n    <string name=\"add_remove_from_playback_queue\">Oynatma sırasına Ekle/Kaldır</string>\n    <string name=\"add_to_playback_queue\">Oynatma sırasına ekle</string>\n    <string name=\"remove_from_playback_queue\">Oynatma sırasından kaldır</string>\n    <string name=\"proxy_enabled\">Vekil Sunucu etkin</string>\n    <string name=\"proxy_disabled\">Vekil Sunucu devre dışı</string>\n    <string name=\"uploads_row_name\">Yüklemeler</string>\n    <string name=\"playlists_row_name\">Oluşturulan oynatma listeleri</string>\n    <string name=\"popular_uploads_row_name\">Popüler yüklemeler</string>\n    <string name=\"news_row_name\">Haberler</string>\n    <string name=\"breaking_news_row_name\">Son dakika haberleri</string>\n    <string name=\"covid_news_row_name\">COVID-19 haberleri</string>\n    <string name=\"enable_voice_search\">Genel aramayı etkinleştir (aygıt yazılımı desteği gerekli)</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">Kanala Abone ol/Aboneliği iptal et</string>\n    <string name=\"app_backup_restore\">Yedekle/Geri al</string>\n    <string name=\"app_backup\">Uygulama verilerini yedekle</string>\n    <string name=\"app_restore\">Uygulama verilerini geri yükle</string>\n    <string name=\"skip_each_segment_once\">Bölümleri tekrar atlama</string>\n    <string name=\"disable_ok_long_press\">TAMAM düğmesine uzun basmayı devre dışı bırak</string>\n    <string name=\"disable_ok_long_press_desc\">TAMAM düğmesinin düzgün çalışmadığı hatalı denetleyiciler için tasarlanmıştır</string>\n    <string name=\"player_time_correction\">Hıza göre zamanı göster</string>\n    <string name=\"sponsor_color_markers\">İlerleme çubuğunda renkli işaretçiler</string>\n    <string name=\"refresh_section\">Bölümü yenile</string>\n    <string name=\"option_disabled\">Devre dışı</string>\n    <string name=\"live_now_row_name\">Canlı</string>\n    <string name=\"run_in_background\">Arka planda oynat</string>\n    <string name=\"settings_remote_control\">Uzaktan kontrol</string>\n    <string name=\"background_service_started\">Arka plan hizmeti başlatıldı</string>\n    <string name=\"dialog_add_to\">Ekle: %s</string>\n    <string name=\"dialog_remove_from\">Kaldır: %s</string>\n    <string name=\"added_to\">Eklendi: %s</string>\n    <string name=\"removed_from\">Kaldırıldı: %s</string>\n    <string name=\"video_preset_adaptive\">Uyarlanır</string>\n    <string name=\"content_block_preview_recap\">Videonun ön izlemesi ya da özeti</string>\n    <string name=\"content_block_highlight\">Videonun ana fikiri ya da öne çıkanları</string>\n    <string name=\"removed_from_history\">Video geçmişten kaldırıldı</string>\n    <string name=\"remove_from_history\">Geçmişten kaldır</string>\n    <string name=\"upload_date\">Yükleme tarihi</string>\n    <string name=\"upload_date_any\">Tüm zamanlar</string>\n    <string name=\"upload_date_today\">Bugün</string>\n    <string name=\"upload_date_this_week\">Bu hafta</string>\n    <string name=\"upload_date_this_month\">Bu ay</string>\n    <string name=\"upload_date_this_year\">Bu yıl</string>\n    <string name=\"double_refresh_rate\">Çift yenileme hızı</string>\n    <string name=\"upload_date_last_hour\">Son bir saat</string>\n    <string name=\"focus_on_search_results\">Arama sonuçlarına otomatik odaklan</string>\n    <string name=\"context_menu\">İçerik menüsü</string>\n    <string name=\"context_menu_sorting\">İçerik menüsü sıralama</string>\n    <string name=\"add_remove_from_recent_playlist\">Son oynatma listesine Ekle/Kaldır</string>\n    <string name=\"hide_settings_section\">Ayarlar bölümünü gizle (tehlikeli!)</string>\n    <string name=\"volume\">Ses %s%%</string>\n    <string name=\"auto_frame_rate_sec\">%s sn.</string>\n    <string name=\"audio_shift_sec\">%s sn.</string>\n    <string name=\"ui_hide_timeout_sec\">%s sn.</string>\n    <string name=\"screen_dimming_timeout_min\">%s dk.</string>\n    <string name=\"mark_all_channels_watched\">Tüm kanalları izlendi olarak işaretle</string>\n    <string name=\"move_section_up\">Bölümü yukarı taşı</string>\n    <string name=\"move_section_down\">Bölümü aşağı taşı</string>\n    <string name=\"player_buttons\">Oynatıcı düğmelerini ayarla</string>\n    <string name=\"action_playlist_add\">Oynatma listesine ekle</string>\n    <string name=\"action_video_stats\">Video istatistikleri</string>\n    <string name=\"action_subscribe\">Abone ol</string>\n    <string name=\"action_channel\">Kanalı aç</string>\n    <string name=\"action_pip\">Resim içinde resim</string>\n    <string name=\"action_video_info\">Video açıklaması</string>\n    <string name=\"action_screen_off\">Ekranı kapat</string>\n    <string name=\"action_sound_off\">Sesi kapat</string>\n    <string name=\"action_playback_queue\">Oynatma sırası</string>\n    <string name=\"action_video_speed\">Video hızı</string>\n    <string name=\"action_subtitles\">Altyazılar</string>\n    <string name=\"action_like\">Beğen</string>\n    <string name=\"action_dislike\">Beğenme</string>\n    <string name=\"action_play_pause\">Oynat/Duraklat</string>\n    <string name=\"action_repeat_mode\">Oynatma modu</string>\n    <string name=\"action_next\">Sonraki video</string>\n    <string name=\"action_previous\">Önceki video</string>\n    <string name=\"action_high_quality\">Video kalitesi</string>\n    <string name=\"content_block_no_skipping_mode\">Atlamasız mod</string>\n    <string name=\"content_block_action_type\">İşlemi seç</string>\n    <string name=\"content_block_action_none\">Hiçbir şey yapma</string>\n    <string name=\"content_block_action_only_skip\">Sadece atla</string>\n    <string name=\"content_block_action_toast\">Bildirimle atla</string>\n    <string name=\"content_block_action_dialog\">Onay mesajı göster</string>\n    <string name=\"content_block_filler\">Konu dışı (boşluk doldurma)</string>\n    <string name=\"keyboard_auto_show\">Klavyeyi otomatik olarak göster</string>\n    <string name=\"cancel_dialog\">İptal et</string>\n    <string name=\"msg_player_error_source2\">URL çalışmıyor ya da yanlış cihaz saati.\\nGenellikle bu, uygulama sorunudur.</string>\n    <string name=\"msg_player_error_video_source\">VİDEO URL\\'si çalışmıyor ya da yanlış cihaz saati.\\nGenellikle bu, uygulama sorunudur.</string>\n    <string name=\"msg_player_error_audio_source\">SES URL\\'si çalışmıyor ya da yanlış cihaz saati.\\nGenellikle bu, uygulama sorunudur.</string>\n    <string name=\"msg_player_error_subtitle_source\">ALTYAZI URL\\'si çalışmıyor ya da yanlış cihaz saati.\\nGenellikle bu, uygulama sorunudur.</string>\n    <string name=\"hide_upcoming\">Yaklaşan yayınları Abonelikler\\'de gizle</string>\n    <string name=\"hide_upcoming_channel\">Yaklaşan yayınları Kanalda gizle</string>\n    <string name=\"hide_upcoming_home\">Yaklaşan yayınları Ana Sayfa\\'da gizle</string>\n    <string name=\"search_background_playback\">Arama yaparken/Kanalda gezinirken arka planda oynat</string>\n    <string name=\"trending_row_name\">Trendler</string>\n    <string name=\"player_seek_type\">Arama davranışı</string>\n    <string name=\"player_seek_regular\">Normal</string>\n    <string name=\"player_seek_confirmation_pause\">Onay ile (ararken duraklat)</string>\n    <string name=\"player_seek_confirmation_play\">Onay ile (ararken oynat)</string>\n    <string name=\"hide_shorts_from_home\">Shortsları Ana Sayfa\\'da gizle</string>\n    <string name=\"finish_on_disconnect\">Telefon bağlantısı kesildiğinde uygulamayı sonlandır</string>\n    <string name=\"update_error\">Güncelleme hatası</string>\n    <string name=\"hide_shorts_from_search\">Shortsları Arama\\'te gizle</string>\n    <string name=\"hide_shorts_from_history\">Shortsları Geçmiş\\'te gizle</string>\n    <string name=\"hide_shorts_from_trending\">Shortsları Trendler\\'de gizle</string>\n    <string name=\"disable_screensaver\">Ekran koruyucuyu devre dışı bırak</string>\n    <string name=\"description_not_found\">Açıklama bulunamadı</string>\n    <string name=\"proxy_port_hint\">Vekil Sunucu port</string>\n    <string name=\"proxy_host_hint\">Vekil Sunucu adı ya da IP</string>\n    <string name=\"proxy_username_hint\">Vekil Sunucu kullanıcı adı</string>\n    <string name=\"proxy_password_hint\">Vekil Sunucu şifre</string>\n    <string name=\"proxy_type\">Vekil Sunucu Türü</string>\n    <string name=\"enable_web_proxy\">Web Vekil Sunucu kullan</string>\n    <string name=\"proxy_not_supported\">Web Vekil Sunucu kullan (Android 4.4+ gerekli)</string>\n    <string name=\"proxy_test_btn\">Test</string>\n    <string name=\"proxy_settings_title\">Vekil Sunucu Ayarları</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">Test#%d: iptal edildi.</string>\n    <string name=\"proxy_type_invalid\">Vekil Sunucu türü ayarlı değil.</string>\n    <string name=\"proxy_host_invalid\">Vekil Sunucu adı ayarlı değil</string>\n    <string name=\"proxy_port_invalid\">Hatalı vekil sunucu portu, &gt; 0 olmalı.</string>\n    <string name=\"proxy_credentials_invalid\">Hatalı kullanıcı adı ya da şifre.</string>\n    <string name=\"proxy_test_aborted\">Test durduruldu, lütfen önce vekil sunucu ayarlarını düzeltin.</string>\n    <string name=\"proxy_application_aborted\">Lütfen önce vekil sunucu ayarlarını düzeltin.</string>\n    <string name=\"proxy_test_start\">Test#%d: %s …</string>\n    <string name=\"proxy_test_error\">Hata#%d: %s</string>\n    <string name=\"proxy_test_status\">Durum#%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">Yapılandırma dosya adresi (*.ovpn)</string>\n    <string name=\"enable_openvpn\">OpenVPN kullan</string>\n    <string name=\"openvpn_settings_title\">OpenVPN Ayarları</string>\n    <string name=\"openvpn_application_aborted\">Lütfen önce OpenVPN ayarlarını düzeltin.</string>\n    <string name=\"openvpn_test_aborted\">Test durduruldu, lütfen önce OpenVPN ayarlarını düzeltin.</string>\n    <string name=\"openvpn_address_invalid\">OpenVPN yapılandırma adresi ayarlanmadı.</string>\n    <string name=\"internet_censorship\">İnternet Sansürü</string>\n    <string name=\"rename_section\">Bölümü yeniden adlandır</string>\n    <string name=\"simple_edit_value_hint\">Değer girin</string>\n    <string name=\"seek_interval\">Arama aralığı</string>\n    <string name=\"seek_interval_sec\">%s sn.</string>\n    <string name=\"subtitle_system\">Sistem stili (Android ayarları &gt; Erişilebilirlik)</string>\n    <string name=\"subtitle_scale\">Altyazı ölçeği</string>\n    <string name=\"audio_sync_fix_desc\">Ses/video senkronize etmenin alternatif bir yolu. Eskimiş bir yöntem sayılır ama bazı durumlarda işe yarayabilir.</string>\n    <string name=\"audio_sync_fix\">Ses senk. düzeltmesi</string>\n    <string name=\"ambilight_ratio_fix_desc\">Olmayan ön gerilim aydınlatmasını düzeltir. Yanlış en boy oranını ya da video ölçeğini düzeltir. Boş ekran görüntülerini düzeltir. Performansı etkiler!</string>\n    <string name=\"ambilight_ratio_fix\">Ambilight/En boy oranı/Video ölçeği/Ekran görüntüleri düzeltmesi</string>\n    <string name=\"force_legacy_codecs_desc\">Alt sınıf cihazlarda performansı önemli ölçüde artırır. Maksimum çözünürlük 720p\\'dir.</string>\n    <string name=\"force_legacy_codecs\">Eski kod çözücüleri zorla (720p)</string>\n    <string name=\"live_stream_fix_desc\">Uyarı, bu ince ayar canlı yayınların geri sarılmasını devre dışı bırakır. Alt sınıf cihazlarda canlı yayın performansını önemli ölçüde artırır. Maksimum çözünürlük 1080p\\'dir.</string>\n    <string name=\"live_stream_fix_4k_desc\">Uyarı, bu ince ayar canlı yayınların geri sarılmasını devre dışı bırakır. Canlı yayın performansını önemli ölçüde artırır. Maksimum çözünürlük 4K\\'dır.</string>\n    <string name=\"live_stream_fix\">Canlı yayın düzeltmesi (1080p)</string>\n    <string name=\"live_stream_fix_4k\">Canlı yayın düzeltmesi (4K)</string>\n    <string name=\"playback_notifications_fix_desc\">Parça değişikliği bildirimlerini gizler. AOSP tabanlı aygıt yazılımlarında faydalı olabilir.</string>\n    <string name=\"playback_notifications_fix\">Oynatma bildirimlerini devre dışı bırak</string>\n    <string name=\"amlogic_fix_desc\">Amlogic tabanlı cihazlarda kare atlaması düzeltmesi.</string>\n    <string name=\"amlogic_fix\">Amlogic 1080p\\@60fps düzeltmesi</string>\n    <string name=\"tunneled_video_playback_desc\">Tünelli video oynatma, daha iyi ses/video senkronizasyonu (SV senk.) ve daha sorunsuz oynatma gibi avantajlar vaat ediyor. Android 5+ gerekli. NOT: Duraklatma düzgün çalışmayabilir!</string>\n    <string name=\"tunneled_video_playback\">Tünelli video oynatma (Android 5+)</string>\n    <string name=\"master_volume\">Ana ses</string>\n    <string name=\"volume_limit\">Ses sınırı</string>\n    <string name=\"player_volume\">Ses</string>\n    <string name=\"play_video\">Oynat</string>\n    <string name=\"remember_position_of_short_videos\">Kısa videoların konumunu hatırla (5 dakikadan az)</string>\n    <string name=\"remember_position_of_live_videos\">Canlı yayın konumunu hatırla</string>\n    <string name=\"player_show_tooltips\">Düğme ipuçlarını göster</string>\n    <string name=\"action_like_unset\">Beğen</string>\n    <string name=\"action_dislike_unset\">Beğenme</string>\n    <string name=\"various_buttons\">Ana pencerenin üst kısmındaki düğmeler</string>\n    <string name=\"not_compatible_with\">Seçilen seçenek şunlarla uyumlu değil:</string>\n    <string name=\"subtitle_position\">Altyazı alttan kaydırma</string>\n    <string name=\"pressing_home\">ANA EKRAN\\'a basarak</string>\n    <string name=\"pressing_home_back\">ANA EKRAN\\'a ya da GERİ\\'ye basarak</string>\n    <string name=\"pressing_back\">GERİ\\'ye basarak</string>\n    <string name=\"player_number_key_seek\">Sayı tuşlarıyla arama</string>\n    <string name=\"save_remove_playlist\">Oynatma listesini Oynatma Listeleri bölümüne Ekle/Kaldır</string>\n    <string name=\"save_playlist\">Oynatma listesini Oynatma Listeleri bölümüne Ekle</string>\n    <string name=\"remove_playlist\">Oynatma listesini Oynatma Listeleri bölümünden kalıcı olarak kaldır</string>\n    <string name=\"remove_playlist_fmt\">%s\\'yi kalıcı olarak kaldır?</string>\n    <string name=\"removed_from_playlists\">Oynatma Listeleri bölümünden kaldırıldı</string>\n    <string name=\"saved_to_playlists\">Oynatma Listeleri bölümüne eklendi</string>\n    <string name=\"create_playlist\">Oynatma listesi oluştur</string>\n    <string name=\"create_playlist_note\">NOT: YouTube uygulamasında görünmeyecek</string>\n    <string name=\"add_video_to_new_playlist\">Videoyu yeni oynatma listesine ekle</string>\n    <string name=\"cant_delete_empty_playlist\">Boş oynatma listesi silinemiyor</string>\n    <string name=\"playlist\">Oynatma listesi</string>\n    <string name=\"rename_playlist\">Oynatma listesini yeniden adlandır</string>\n    <string name=\"cant_rename_empty_playlist\">Boş oynatma listesi yeniden adlandırılamıyor</string>\n    <string name=\"cant_rename_foreign_playlist\">Yabancı oynatma listesi yeniden adlandırılamıyor</string>\n    <string name=\"cant_save_playlist\">Bu oynatma listesi eklenemiyor</string>\n    <string name=\"enter_value\">Boş olmayan bir değer girin</string>\n    <string name=\"dialog_add_remove_from\">Ekle/Kaldır: %s</string>\n    <string name=\"lb_playback_controls_skip_next\">Sonrakine atla</string>\n    <string name=\"lb_playback_controls_skip_previous\">Öncekine atla/Başlangıç konumuna geri sar</string>\n    <string name=\"alt_presets_behavior\">Alt. ön ayar davranışı (bant genişliğini sınırla)</string>\n    <string name=\"alt_presets_behavior_desc\">Uygulama çözünürlük, fps ve kod çözücü arasında eşleştirme yapmak yerine bant genişliğinin seçilen ön ayara karşılık gelmesini sağlamaya çalışacaktır.</string>\n    <string name=\"sleep_timer\">Uyku zamanlayıcısı (uzaktan kumanda bir saat kullanılmadıysa)</string>\n    <string name=\"sleep_timer_desc\">Kullanıcı bir saat boyunca uzaktan kumandayı kullanmadıysa oynatmayı duraklatır.</string>\n    <string name=\"disable_vsync\">Dikey eşzamanlamaya uymayı devre dışı bırak</string>\n    <string name=\"disable_vsync_desc\">Bu seçenek, karelerin ekranın dikey senkronizasyon sinyaliyle hizalanmasını devre dışı bırakır. Bu, işlemci kaynaklarını serbest bırakarak düşük kaliteli cihazlarda performansı artırabilir.</string>\n    <string name=\"skip_codec_profile_check\">Kod çözücü profil düzey kontrolünü atla</string>\n    <string name=\"skip_codec_profile_check_desc\">Bir videoyu başlatırken kod çözücü desteğini kontrol etmez. Sorunlu aygıt yazılımlarında yardımcı olabilir.</string>\n    <string name=\"force_sw_codec\">Yazılımsal video kod çözücüyü zorla</string>\n    <string name=\"force_sw_codec_desc\">Hemen hemen her videoyu oynatabilir ama performansı çok kötüdür.</string>\n    <string name=\"playlist_order\">Oynatma listesini sırala</string>\n    <string name=\"playlist_order_added_date_newer_first\">Eklenme tarihi (önce daha yenisi)</string>\n    <string name=\"playlist_order_added_date_older_first\">Eklenme tarihi (önce eski)</string>\n    <string name=\"playlist_order_published_date_newer_first\">Yayımlanma tarihi (önce daha yenisi)</string>\n    <string name=\"playlist_order_published_date_older_first\">Yayımlanma tarihi (önce eski)</string>\n    <string name=\"playlist_order_popularity\">Popülerlik</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">Bunu yabancı oynatma listesi için yapamazsınız</string>\n    <string name=\"owned_playlist_warning\">Hata. Bu işlem sedece sahip olunan oynatma listeleri için uygulanabilir</string>\n    <string name=\"content_block_alt_server\">Alternatif sunucu kullan</string>\n    <string name=\"content_block_alt_server_desc\">SponsorBlock farklı nedenlerle çalışmayı reddediyorsa bu seçeneği etkinleştirebilirsiniz.</string>\n    <string name=\"unset_stream_reminder\">Canlı yayın hatırlatıcısını iptal et</string>\n    <string name=\"set_stream_reminder\">Canlı yayın hatırlatıcısını ayarla</string>\n    <string name=\"playback_starts_shortly\">Canlı yayın başladığında otomatik olarak oynatılacaktır.</string>\n    <string name=\"starting_stream\">Canlı yayın başlatılıyor…</string>\n    <string name=\"action_playlist_remove\">Oynatma listesinden kaldır</string>\n    <string name=\"signin_view_title\">Kullanıcı Kodu yükleniyor…</string>\n    <string name=\"signin_view_description\">Oturum açmak için bu kodu sayfaya girin %s\\nNOT: Yalnızca Firefox ya da Chrome tarayıcılarla çalışır.</string>\n    <string name=\"signin_view_action_text\">Tamam</string>\n    <string name=\"require_checked\">\\\"%s\\\" gerektirir</string>\n    <string name=\"msg_press_again_to_exit\">Çıkmak için tekrar basın</string>\n    <string name=\"player_remaining_time\">Kalan: %s</string>\n    <string name=\"player_ending_time\">Bitiş saati: %s</string>\n    <string name=\"badge_new_content\">YENİ İÇERİK</string>\n    <string name=\"badge_live\">CANLI</string>\n    <string name=\"add_device_view_description\">Cihazı bağlamak için bu kodu telefonunuzun YouTube uygulamasında Ayarlar/TV\\'de İzle bölümüne girin.\\nNot: Yalnızca Gboard klavye ile çalışır.</string>\n    <string name=\"playback_controls_repeat_pause\">Tekrarla Duraklat</string>\n    <string name=\"playback_controls_repeat_list\">Tekrarla Liste</string>\n    <string name=\"action_subscribe_off\">Abone ol Kapalı</string>\n    <string name=\"action_subscribe_on\">Abone ol Açık</string>\n    <string name=\"skip_24_rate\">24 fps biçimlerini atla (gerçek sinema modu düzeltmesi\\?)</string>\n    <string name=\"skip_shorts\">Shortsları atla</string>\n    <string name=\"player_disable_suggestions\">Önerileri devre dışı bırak</string>\n    <string name=\"suggestions\">Öneriler</string>\n    <string name=\"feedback\">Geri bildirim</string>\n    <string name=\"sources\">Kaynaklar</string>\n    <string name=\"releases\">Sürümler</string>\n    <string name=\"prefer_avc_over_vp9\">Kod çözücü seçimi: avc\\'yi vp9\\'a tercih et</string>\n    <string name=\"open_chat\">Sohbet/Yorumlar</string>\n    <string name=\"open_comments\">Yorumları aç</string>\n    <string name=\"place_chat_left\">Sohbeti sola yerleştir</string>\n    <string name=\"place_comments_left\">Yorumları sola yerleştir</string>\n    <string name=\"use_alt_speech_recognizer\">Alternatif konuşma tanıyıcı</string>\n    <string name=\"time_format\">Saat modu</string>\n    <string name=\"time_format_24\">24 saat</string>\n    <string name=\"time_format_12\">12 saat (ÖÖ/ÖS)</string>\n    <string name=\"use_alt_speech_recognizer_desc\">Ciddi hataları var. Yalnızca varsayılan tanıyıcıyla ilgili sorunlarınız varsa kullanın.</string>\n    <string name=\"speech_recognizer\">Konuşma tanıyıcı</string>\n    <string name=\"speech_engine\">Sesli arama motoru</string>\n    <string name=\"speech_recognizer_system\">Sistem (tercih edilmiş)</string>\n    <string name=\"speech_recognizer_external_1\">Harici 1 (Ciddi hataları var, çalışması için mikrofona erişimi devre dışı bırakmanız gerekir)</string>\n    <string name=\"speech_recognizer_external_2\">Harici 2 (Ciddi hataları var)</string>\n    <string name=\"real_channel_icon\">Kanal düğmesinde simgeyi göster</string>\n    <string name=\"subtitle_yellow_semi_transparent\">Sarı (Yarı saydam arka plan)</string>\n    <string name=\"subtitle_yellow_black\">Sarı (Siyah arka plan)</string>\n    <string name=\"player_pixel_ratio\">Görüntü en boy oranı</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">Koyu Gri (tek renkli)</string>\n    <string name=\"disable_mic_permission\">Bu tanıyıcının düzgün çalışması için lütfen uygulamanın mikrofon erişimini devre dışı bırakın.</string>\n    <string name=\"repeat_mode_shuffle\">Herhangi bir oynatma listesini karıştır</string>\n    <string name=\"chat_left\">Sol</string>\n    <string name=\"chat_right\">Sağ</string>\n    <string name=\"card_real_thumbnails\">Küçük resimleri videodan bir kareyle değiştir</string>\n    <string name=\"card_content\">Küçük resimler nereden alınsın\\?</string>\n    <string name=\"thumb_quality_default\">Varsayılan</string>\n    <string name=\"thumb_quality_start\">Videonun başlangıcından</string>\n    <string name=\"thumb_quality_middle\">Videonun ortasından</string>\n    <string name=\"thumb_quality_end\">Videonun sonundan</string>\n    <string name=\"content_block_status\">SponsorBlock sunucu durumunu kontrol et</string>\n    <string name=\"dearrow_status\">DeArrow sunucu durumunu kontrol et</string>\n    <string name=\"player_show_quality_info_bitrate\">Kalite bilgisine bit hızı ekle</string>\n    <string name=\"player_speed_button_old_behavior\">Hız düğmesinin eski davranışına geri dön</string>\n    <string name=\"protect_settings_with_password\">Tüm ayarları şifre ile koru</string>\n    <string name=\"enter_settings_password\">Ayarlar şifresini gir</string>\n    <string name=\"child_mode\">Çocuk modu</string>\n    <string name=\"child_mode_desc\">Bu modda, kullanıcı aramayı kullanamaz ya da önerilen içeriği göremez. Ayarlar şifrenin altında olacaktır.</string>\n    <string name=\"lost_setting_warning\">Uygulama ayarları değiştirilecektir. Ayarların yedeğini oluşturduğunuzdan emin olun.</string>\n    <string name=\"player_button_long_click\">Hızlı işlem için düğmeye kısa tıkla, ayarlarına gitmek için uzun tıkla</string>\n    <string name=\"pause_history\">Geçmişi duraklat</string>\n    <string name=\"resume_history\">Geçmişi devam ettir</string>\n    <string name=\"disable_history\">Geçmişi devre dışı bırak</string>\n    <string name=\"enable_history\">Geçmişi etkinleştir</string>\n    <string name=\"clear_history\">Geçmişi temizle</string>\n    <string name=\"chapters\">Bölümler</string>\n    <string name=\"card_multiline_subtitle\">Çok satırlı altyazılar</string>\n    <string name=\"auto_frame_rate_modes\">Desteklenen modlar</string>\n    <string name=\"loading\">Yükleniyor…</string>\n    <string name=\"hide_streams\">Canlı yayınları Abonelikler\\'de gizle</string>\n    <string name=\"video_rotate\">Döndür</string>\n    <string name=\"video_flip\">Ayna</string>\n    <string name=\"trending_searches\">Trend olan aramalar</string>\n    <string name=\"video_duration_any\">Herhangi biri</string>\n    <string name=\"video_duration\">Süre</string>\n    <string name=\"video_duration_under_4\">4 dakikadan kısa</string>\n    <string name=\"video_duration_between_4_20\">4–20 dakika</string>\n    <string name=\"video_duration_over_20\">20 dakikadan fazla</string>\n    <string name=\"content_type\">Tür</string>\n    <string name=\"content_type_any\">Herhangi biri</string>\n    <string name=\"content_type_video\">Video</string>\n    <string name=\"content_type_channel\">Kanal</string>\n    <string name=\"content_type_playlist\">Oynatma Listesi</string>\n    <string name=\"content_type_movie\">Film</string>\n    <string name=\"video_features\">Özellikler</string>\n    <string name=\"video_feature_any\">Herhangi biri</string>\n    <string name=\"video_feature_live\">Canlı</string>\n    <string name=\"video_feature_4k\">4K</string>\n    <string name=\"video_feature_hdr\">HDR</string>\n    <string name=\"search_sorting\">Sıralama Ölçütü</string>\n    <string name=\"sort_by_relevance\">Alaka düzeyi</string>\n    <string name=\"sort_by_views\">Görüntüleme sayısı</string>\n    <string name=\"sort_by_date\">Yüklenme tarihi</string>\n    <string name=\"sort_by_rating\">Derecelendirme</string>\n    <string name=\"clear_search_history\">Arama geçmişini temizle</string>\n    <string name=\"remove_from_subscriptions\">Gizle</string>\n    <string name=\"player_long_speed_list\">Uzun oynatma hızı listesi</string>\n    <string name=\"player_extra_long_speed_list\">Ekstra uzun oynatma hızı listesi</string>\n    <string name=\"alt_app_icon\">Alternatif uygulama simgesi (yeniden başlatma gerekli)</string>\n    <string name=\"network_stack\">%s ağ yığınını tercih et</string>\n    <string name=\"player_network_stack\">Ağ motoru</string>\n    <string name=\"cronet_desc\">Cronet, Chromium ağ motoru. Cronet, ara belleğe alma sorunlarına yardımcı olabilecek gecikmeyi azaltabilir ve ağ performansını artırabilir.</string>\n    <string name=\"unlock_all_formats\">Tüm video biçimlerinin kilidini aç</string>\n    <string name=\"unlock_all_formats_desc\">Bazı cihazlarda aygıt yazılımı, desteklense bile bazı biçimlerin desteklenmediğini hatalı bir şekilde bildirir.\\n(ör. 1080p akıllı TV\\'ler genellikle 4k\\'yı desteklenmiyor olarak bildirir).</string>\n    <string name=\"okhttp_desc\">Bu ağ motoru en yavaş olanıdır ancak iyi bir kararlılığa sahiptir.</string>\n    <string name=\"select_channel_section\">Uygulama ATV Kanallarından açıldığında ilgili bölümü aç</string>\n    <string name=\"enable_voice_search_desc\">Resmi uygulama sözde bir köprü ile değiştirilecek. Bir köprü, genel arama isteklerini uygulamamıza aktarmaya yardımcı olur.</string>\n    <string name=\"enable_master_password\">Ana şifreyi etkinleştir</string>\n    <string name=\"enter_master_password\">Ana şifreyi gir</string>\n    <string name=\"disable_stream_buffer\">Canlı yayınlarda ara belleği devre dışı bırak</string>\n    <string name=\"disable_stream_buffer_desc\">Canlı yayının çok geride olduğu durumlar için düzeltme. Not: Canlı yayın gecikmeye başlayabilir.</string>\n    <string name=\"sony_frame_drop_fix\">Kare bırakma düzeltmesi #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">Sony TV ve diğer bazı cihazlardaki gecikmeleri düzeltir. Not: Ses senkronizasyonu ile ilgili olası sorunlar olabilir.</string>\n    <string name=\"audio_language\">Ses dili</string>\n    <string name=\"old_home_look\">Ana Sayfa bölümünün eski görünümü</string>\n    <string name=\"old_channel_look\">Kanal sayfasının eski görünümü</string>\n    <string name=\"hide_shorts_everywhere\">Shortsları her yerde gizle</string>\n    <string name=\"update_found\">Güncelleme</string>\n    <string name=\"volume_boost_warning\">Sınırın üzerindeki herhangi bir ayar hoparlörlere zarar verebilir</string>\n    <string name=\"play_video_incognito\">Gizli oynat</string>\n    <string name=\"play_from_start\">Baştan oyna</string>\n    <string name=\"header_kids_home\">Kids</string>\n    <string name=\"player_section_playlist\">Geçerli bölüm içeriklerini oynatma listesi olarak kullan</string>\n    <string name=\"header_trending\">Trendler</string>\n    <string name=\"screensaver\">Ekran koruyucu</string>\n    <string name=\"player_screen_off_timeout\">Ekran kapatma zaman aşımı</string>\n    <string name=\"old_update_notifications\">Güncelleme bildirimlerinin eski görünümü</string>\n    <string name=\"sidebar_notification\">Bildirimi kenar çubuğunda göster</string>\n    <string name=\"dialog_notification\">Açılır iletişim kutusuyla güncelleme hakkında bilgi ver</string>\n    <string name=\"mark_as_watched\">İzlendi olarak işaretle</string>\n    <string name=\"player_ui_animations\">Kontrol paneli animasyonlarını etkinleştir</string>\n    <string name=\"player_likes_count\">Beğenme/Beğenmeme sayısını göster</string>\n    <string name=\"sorting_alphabetically2\">Alfabetik (hızlı, hareketli ön izlemeler)</string>\n    <string name=\"sorting_default\">Varsayılan (hızlı, hareketli ön izlemeler)</string>\n    <string name=\"content_block_exclude_channel\">Bu kanalı SponsorBlock\\'tan hariç tut</string>\n    <string name=\"content_block_stop_excluding_channel\">Bu kanalı SponsorBlock\\'tan hariç tutmayı durdur</string>\n    <string name=\"player_chapter_notification\">Açılır bildirimi kullanarak bölümler arasında hızlı ilerle</string>\n    <string name=\"player_chapter_notification2\">Bildirime tıklayarak bir sonraki bölüme geç</string>\n    <string name=\"subtitle_remember\">Her kanal için etkinleştirilmiş altyazıları hatırla</string>\n    <string name=\"amazon_frame_drop_fix\">Kare bırakma düzeltmesi #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">Amazon Stick cihazları için tasarlanmıştır. Diğer cihazlarda da çalışabilir.</string>\n    <string name=\"default_stack_desc\">Dahili ağ motoru. Bu motoru belirli durumlarda daha iyi kararlılığa sahip olabilir.</string>\n    <string name=\"item_postion\">Konum:</string>\n    <string name=\"player_screen_off_dimming\">Ekran kapatma karartma miktarı</string>\n    <string name=\"screensaver_timout\">Ekran koruyucu zaman aşımı</string>\n    <string name=\"screensaver_dimming\">Ekran koruyucu karartma miktarı</string>\n    <string name=\"player_ui_on_next\">Bir sonraki videoya geçerken oynatıcı arayüzünü göster</string>\n    <string name=\"autogenerated\">otomatik oluşturuldu</string>\n    <string name=\"player_auto_volume\">Otomatik ses ayarı</string>\n    <string name=\"header_shorts\">Shorts</string>\n    <string name=\"player_global_focus\">Oynatıcı düğme satırları arasında odağı senkronize et</string>\n    <string name=\"auto_history\">Otomatik (hesap ayarlarını kullan)</string>\n    <string name=\"remember_position_subscriptions\">Abonelikler\\'de son görüntülenen konumu hatırla</string>\n    <string name=\"remember_position_pinned\">Sabitlenmiş oynatma listelerinde son görüntülenen konumu hatırla</string>\n    <string name=\"msg_player_unknown_error\">Bilinmeyen hata</string>\n    <string name=\"unknown_source_error\">Bilinmeyen kaynak hatası</string>\n    <string name=\"unknown_renderer_error\">Bilinmeyen çizici hatası</string>\n    <string name=\"header_notifications\">Bildirimler</string>\n    <string name=\"disable_search_history\">Arama geçmişini devre dışı bırak</string>\n    <string name=\"unlock_high_bitrate_formats\">Yüksek bit hızına sahip 1080p vp9 biçimlerinin kilidini aç</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">Yüksek bit hızına sahip mp4a biçimlerinin kilidini aç</string>\n    <string name=\"color_scheme_blue\">Mavi</string>\n    <string name=\"color_scheme_dark_blue\">Koyu Mavi</string>\n    <string name=\"player_speed_per_channel\">Her kanal için</string>\n    <string name=\"disable_popular_searches\">Popüler arama sorgularını devre dışı bırak</string>\n    <string name=\"multi_profiles\">Her hesap için ayrı ayarlar kullan</string>\n    <string name=\"protect_account_with_password\">Bu hesabı şifre ile koru</string>\n    <string name=\"enter_account_password\">Hesap şifresini girin</string>\n    <string name=\"show_connect_messages\">Bağlantı mesajlarını göster</string>\n    <string name=\"prefer_avc_over_vp9_desc\">UYARI: Maksimum 1080p</string>\n    <string name=\"play_next\">Sonrakini oynat</string>\n    <string name=\"hide_watched_from_subscriptions\">İzlenen videoları Abonelikler\\'de gizle</string>\n    <string name=\"hide_watched_from_notifications\">İzlenen videoları Bildirimler\\'de gizle</string>\n    <string name=\"hide_unwanted_content\">İstenmeyen içerik gizleme</string>\n    <string name=\"hide_watched_from_home\">İzlenen videoları Ana Sayfa\\'da gizle</string>\n    <string name=\"hide_watched_from_watch_later\">İzlenen videoları Daha sonra izle oynatma listesinde gizle</string>\n    <string name=\"remote_control_permission\">Arka plan Uzaktan kontrol, Yer Paylaşımı izni gerektirir</string>\n    <string name=\"login_from_browser\">Tarayıcıdan giriş yap</string>\n    <string name=\"disable_remote_history\">Uzaktan kontrol kullanırken geçmişi devre dışı bırak</string>\n    <string name=\"keyboard_fix\">TAMAM tuşuna basarak klavyeyi göstermeyi düzelt (G20\\'ler ve diğerleri)</string>\n    <string name=\"nothing_found\">Hiçbir şey bulunamadı</string>\n    <string name=\"auto_frame_rate_desc\">Bu seçenek, spor yayınları gibi kameranın hızlı hareket ettiği sahnelerdeki titremeyi ortadan kaldırır</string>\n    <string name=\"channel_filter_hint\">Kanalları filtrele</string>\n    <string name=\"player_loop_shorts\">Shortsları tekrarla</string>\n    <string name=\"player_quick_shorts_skip\">Shortsları sol/sağ düğmeyle atla</string>\n    <string name=\"player_quick_shorts_skip_alt\">Shortsları yukarı/aşağı düğmeyle atla</string>\n    <string name=\"player_quick_skip_videos\">Atla Normal videolar sol/sağ düğmeyle</string>\n    <string name=\"player_quick_skip_videos_alt\">Atla Normal videolar yukarı/aşağı düğmeyle</string>\n    <string name=\"channels_filter\">Kanallar bölümü içindeki Kanalları filtrele alanını göster</string>\n    <string name=\"channel_search_bar\">Kanal sayfasında arama çubuğu</string>\n    <string name=\"header_sports\">Spor</string>\n    <string name=\"keep_finished_activities\">Bitmiş etkinlikleri sakla</string>\n    <string name=\"disable_channels_service\">Kanallar hizmetini devre dışı bırak</string>\n    <string name=\"replace_titles\">Başlıkları değiştir</string>\n    <string name=\"enable\">Etkinleştir</string>\n    <string name=\"more_info\">Daha fazla bilgi</string>\n    <string name=\"about_sponsorblock\">SponsorBlock Hakkında</string>\n    <string name=\"about_dearrow\">DeArrow Hakkında</string>\n    <string name=\"replace_thumbnails\">Küçük resimleri değiştir</string>\n    <string name=\"crowdsourced_thumbnails\">Kitle kaynaklı küçük resimler</string>\n    <string name=\"crowdsoursed_titles\">Kitle kaynaklı başlıklar</string>\n    <string name=\"dearrow_not_submitted_thumbs\">Gönderilmeyen küçük resimler nereden alınsın\\?</string>\n    <string name=\"pitch_effect\">Pitch efekti</string>\n    <string name=\"fullscreen_mode\">Tam ekran modu (sistem çubukları olmadan)</string>\n    <string name=\"player_only_mode\">Video uygulama dışında açılırsa yalnızca oynatıcıyı göster</string>\n    <string name=\"pinned_channel_rows\">Sabitlenmiş kanalı satır olarak göster</string>\n    <string name=\"prefer_google_dns\">Google DNS tercih et</string>\n    <string name=\"prefer_ipv4\">IPv4 DNS tercih et</string>\n    <string name=\"prefer_ipv4_desc\">Uygulamanın hiç çalışmadığı durumları düzeltebilir.\\nNot: Takılmalara ve çökmelere neden olabilir (özellikle Android 8 cihazlarda ya da Dune HD\\'de)</string>\n    <string name=\"long_press_for_settings\">AYARLAR İÇİN UZUN BASIN</string>\n    <string name=\"long_press_for_options\">SEÇENEKLER IÇIN UZUN BASIN</string>\n    <string name=\"device_specific_backup\">Yalnızca bu cihaz için yedekleme</string>\n    <string name=\"local_backup\">Yerel yedekleme</string>\n    <string name=\"auto_backup\">Otomatik yedekleme (günde bir kez)</string>\n    <string name=\"repeat_mode_reverse_list\">Çalma listesini ters sırayla çal</string>\n    <string name=\"calm_msg\">Sorunu düzeltiyoruz. Zaman zaman güncellemeleri kontrol edin</string>\n    <string name=\"without_picture\">Resimsiz</string>\n    <string name=\"video_disabled\">Video devre dışı</string>\n    <string name=\"applying_fix\">Oynatıcıyı kapatmayın. Düzeltme uygulanıyor...</string>\n    <string name=\"hide_mixes\">Karışımları gizle</string>\n    <string name=\"player_global_focus_desc\">Bu özellik, oynatıcı düğmeleri sıraları arasında gezinirken hangi oynatıcı düğmesinin odaklanacağını etkiler</string>\n    <string name=\"disable_network_error_fixing\">Otomatik ağ hatası düzeltmeyi devre dışı bırak</string>\n    <string name=\"disable_network_error_fixing_desc\">Bir VPN kullanıyorsanız muhtemelen bu seçeneği etkinleştirmeniz gerekir</string>\n    <string name=\"recommended\">Önerilen Videolar</string>\n    <string name=\"add_to_subscriptions_group\">Abonelik grubuna kanal ekle/kaldır</string>\n    <string name=\"new_subscriptions_group\">Yeni grup</string>\n    <string name=\"rename_group\">Abonelik grubunun yeniden adlandır</string>\n    <string name=\"screen_dimming_amount\">Ekran karartma miktarı</string>\n    <string name=\"screen_dimming_timeout\">Ekran karartma zaman aşımı</string>\n    <string name=\"playlists_rows\">Oynatma listelerini satır olarak göster</string>\n    <string name=\"import_subscriptions_group\">Içe aktar</string>\n    <string name=\"lb_playback_controls_closed_captioning_disable\">Altyazıyı Devre Dışı Bırak</string>\n    <string name=\"lb_playback_controls_closed_captioning_enable\">Altyazıyı Aktifleştir</string>\n    <string name=\"my_videos\">Videolarım</string>\n    <string name=\"player_audio_focus\">Ses odaklama (diğer video oynatıcılar algılanırsa duraklatma)</string>\n    <string name=\"paid_content_notification\">Ücretli içerik bildirimleri</string>\n    <string name=\"you_liked\">sen beğendin</string>\n    <string name=\"premium_users_only\">Premium users only. Fix for incomplete video format list</string>\n    <string name=\"playback_buffering_fix\">Playback buffering fix</string>\n    <string name=\"oculus_quest_fix\">Oculus Quest fix</string>\n    <string name=\"card_preview_muted\">Sessiz video</string>\n    <string name=\"card_preview_full\">Sesli video</string>\n    <string name=\"card_preview\">Kart önizlemesi</string>\n    <string name=\"card_unlocalized_titles\">Yerelleştirilmemiş video başlıkları</string>\n    <string name=\"dont_resize_video_to_fit_dialog\">Videoyu diyaloğa uyacak şekilde yeniden boyutlandırmayın</string>\n    <string name=\"typing_corrections\">Metin yazarken otomatik düzeltmeyi devre dışı bırak</string>\n    <string name=\"suggestions_horizontally_scrolled\">Yatay Kaydırılan Öneriler</string>\n    <string name=\"stable_restore\">Kararlı sürümü geri yükle (tüm veriler silinecek)</string>\n    <string name=\"auto_backup_category\">Otomatik yedekleme</string>\n    <string name=\"once_a_day\">Günde bir kez</string>\n    <string name=\"once_a_week\">Haftada bir kez</string>\n    <string name=\"once_a_month\">Ayda bir kez</string>\n    <string name=\"action_debug_info\">Hata ayıklama bilgileri</string>\n    <string name=\"dialog_block_channel\">Kanalı engelle</string>\n    <string name=\"dialog_unblock_channel\">Kanalın engelini kaldır</string>\n    <string name=\"confirm_block_channel\">%s adlı kanaldaki tüm içerik gizlensin mi?</string>\n    <string name=\"channel_blocked\">Kanal engellendi</string>\n    <string name=\"channel_unblocked\">Kanalın engeli kaldırıldı</string>\n    <string name=\"header_blocked_channels\">Engellenen Kanallar</string>\n    <string name=\"msg_no_blocked_channels\">Engellenmiş kanal yok</string>\n    <string name=\"player_time_stretching\">Ses zaman esnetme</string>\n    <string name=\"player_time_stretching_desc\">Hız değiştirildiğinde sesi doğal tutar. Bazı cihazlarda performansı önemli ölçüde düşürebilir.</string>\n    <string name=\"queue_respects_playback_mode\">Kuyruk oynatma modunu dikkate alır</string>\n    <string name=\"ignore_short_segments\">Kısa segmentleri yok say</string>\n    <string name=\"local_backup_not_supported\">Android TV 11+’da otomatik yedekleme sistem kısıtlamaları nedeniyle desteklenmez. Bunun yerine GDrive kullanın.</string>\n    <plurals name=\"hours\">\n        <item quantity=\"one\">%s saat</item>\n        <item quantity=\"other\">%s saat</item>\n    </plurals>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%s saniye</item>\n        <item quantity=\"other\">%s saniye</item>\n    </plurals>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-uk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"header_home\">Головна</string>\n    <string name=\"title_search\">Пошук</string>\n    <string name=\"header_subscriptions\">Підписки</string>\n    <string name=\"header_history\">Історія</string>\n    <string name=\"header_music\">Музика</string>\n    <string name=\"header_news\">Новини</string>\n    <string name=\"header_gaming\">Ігри</string>\n    <string name=\"header_playlists\">Плейлисти</string>\n    <string name=\"header_channels\">Канали</string>\n    <string name=\"subscriptions_signin_title\">Переглядайте найновіші відео з улюблених каналів</string>\n    <string name=\"subscriptions_signin_subtitle\">Увійдіть, щоб побачити свої підписки</string>\n    <string name=\"subscriptions_signin_button_text\">УВІЙТИ</string>\n    <string name=\"library_signin_to_show_more\">Переглядайте відео, які ви вподобали, зберегли або підписалися</string>\n    <string name=\"library_signin_subtitle\">Увійдіть, щоб переглянути бібліотеку</string>\n    <string name=\"action_signin\">УВІЙТИ</string>\n    <string name=\"header_settings\">Налаштування</string>\n    <string name=\"title_video_formats\">Відеоформат</string>\n    <string name=\"title_audio_formats\">Аудіоформат</string>\n    <string name=\"subtitle_category_title\">Субтитри</string>\n    <string name=\"playback_queue_category_title\">Черга відтворення</string>\n    <string name=\"video_max_quality\">Авто (макс. якість)</string>\n    <string name=\"audio_max_quality\">Авто (макс. якість)</string>\n    <string name=\"subtitles_disabled\">Субтитри відключені</string>\n    <string name=\"auto_frame_rate\">Auto Frame Rate</string>\n    <string name=\"frame_rate_correction\">Виправляти fps:\\n%s</string>\n    <string name=\"category_background_playback\">Фонове відтворення</string>\n    <string name=\"not_implemented\">Поки не зроблено</string>\n    <string name=\"not_supported_by_device\">Пристрій не підтримує цю функцію</string>\n    <string name=\"option_background_playback_off\">Відключено</string>\n    <string name=\"option_background_playback_pip\">Картинка в картинці</string>\n    <string name=\"option_background_playback_only_audio\">Тільки аудіо</string>\n    <string name=\"playback_settings\">Налаштування якості відтворення</string>\n    <string name=\"video_speed\">Швидкість відео</string>\n    <string name=\"resolution_switch\">Перемикати роздільну здатність</string>\n    <string name=\"video_buffer\">Відеобуфер</string>\n    <string name=\"video_buffer_size_none\">Немає</string>\n    <string name=\"video_buffer_size_lowest\">Найнижчий</string>\n    <string name=\"video_buffer_size_low\">Низький</string>\n    <string name=\"video_buffer_size_med\">Середній</string>\n    <string name=\"video_buffer_size_high\">Високий</string>\n    <string name=\"video_buffer_size_highest\">Найвищий</string>\n    <string name=\"update_changelog\">Зміни</string>\n    <string name=\"install_update\">Встановити оновлення</string>\n    <string name=\"section_is_empty\">Тут нічого немає</string>\n    <string name=\"dialog_add_to_playlist\">Додати/Видалити із плейлиста</string>\n    <string name=\"msg_signed_users_only\">Необхідно увійти в обліковий запис</string>\n    <string name=\"msg_cant_load_content\">Не вдалося завантажити вміст.\\n1) Увімкніть історію переглядів.\\n2) Перевірте дату та час на пристрої.\\n3) Перевірте мережеве з\\`єднання.\\n4) Перевірте налаштування проксі.\\n5) Спробуйте увійти в обліковий запис.\\n6) Можливо, тут нічого не має.</string>\n    <string name=\"title_video_presets\">Відео пресети</string>\n    <string name=\"settings_accounts\">Облікові записи</string>\n    <string name=\"settings_left_panel\">Налаштувати категорії</string>\n    <string name=\"settings_themes\">Теми</string>\n    <string name=\"settings_other\">Різне</string>\n    <string name=\"settings_player\">Плеєр</string>\n    <string name=\"settings_language\">Мова</string>\n    <string name=\"settings_linked_devices\">Прив`язані пристрої</string>\n    <string name=\"settings_about\">Про застосунок</string>\n    <string name=\"dialog_account_list\">Обрати обліковий запис</string>\n    <string name=\"dialog_account_none\">Не обрано</string>\n    <string name=\"dialog_remove_account\">Вийти з облікового запису</string>\n    <string name=\"dialog_add_account\">Увійти в обліковий запис</string>\n    <string name=\"default_lang\">За замовчуванням</string>\n    <string name=\"original_lang\">Оригінальний</string>\n    <string name=\"dialog_select_language\">Мова</string>\n    <string name=\"subtitle_default\">Звичайні</string>\n    <string name=\"subtitle_white_semi_transparent\">Білі на напівпрозорому фоні</string>\n    <string name=\"subtitle_style\">Стиль субтитрів</string>\n    <string name=\"subtitle_language\">Мова субтитрів</string>\n    <string name=\"action_search\">Пошук</string>\n    <string name=\"settings_main_ui\">Інтерфейс</string>\n    <string name=\"dialog_main_ui\">Інтерфейс застосунку</string>\n    <string name=\"card_animated_previews\">Анімований попередній перегляд</string>\n    <string name=\"web_site\">Вебсайт</string>\n    <string name=\"donation\">Підтримати проект</string>\n    <string name=\"dialog_about\">Про застосунок</string>\n    <string name=\"dialog_player_ui\">Відеоплеєр</string>\n    <string name=\"player_show_ui_on_pause\">Відображати інтерфейс на паузі</string>\n    <string name=\"player_pause_on_ok\">Клавіша OK зупиняє відтворення</string>\n    <string name=\"player_ok_button_behavior\">Поводження кнопки OK</string>\n    <string name=\"player_only_ui\">Тільки інтерфейс</string>\n    <string name=\"player_ui_and_pause\">Інтерфейс та пауза</string>\n    <string name=\"player_only_pause\">Тільки пауза</string>\n    <string name=\"player_toggle_speed\">Увімкнення/вимкнення швидкості</string>\n    <string name=\"check_for_updates\">Перевірити оновлення</string>\n    <string name=\"update_not_found\">Ваша версія не потребує оновлення</string>\n    <string name=\"update_in_progress\">Хвилинку…</string>\n    <string name=\"player_ui_hide_behavior\">Приховувати інтерфейс</string>\n    <string name=\"option_never\">Ніколи</string>\n    <string name=\"side_panel_sections\">Налаштувати розділи</string>\n    <string name=\"boot_to_section\">Завантажуватися у розділ</string>\n    <string name=\"large_ui\">Збільшений інтерфейс</string>\n    <string name=\"video_grid_scale\">Масштаб сітки відео</string>\n    <string name=\"scale_ui\">Масштаб інтерфейсу</string>\n    <string name=\"color_scheme\">Схема кольорів</string>\n    <string name=\"color_scheme_default\">За замовчуванням</string>\n    <string name=\"color_scheme_red_grey\">Червоно-сіра</string>\n    <string name=\"color_scheme_red\">Червона</string>\n    <string name=\"color_scheme_dark_grey\">Темно-сіра</string>\n    <string name=\"disable_update_check\">Не перевіряти оновлення</string>\n    <string name=\"show_again\">Нагадувати постійно</string>\n    <string name=\"check_updates_auto\">Повідомляти про оновлення</string>\n    <string name=\"select_account_on_boot\">Показувати діалог вибору облікового запису на старті застосунку</string>\n    <string name=\"player_other\">Різне</string>\n    <string name=\"player_full_date\">Точна дата в описі</string>\n    <string name=\"open_channel\">Перейти до каналу</string>\n    <string name=\"open_playlist\">Відкрити плейлист</string>\n    <string name=\"not_interested\">Більше не радити</string>\n    <string name=\"not_recommend_channel\">Більше не радити канал</string>\n    <string name=\"you_wont_see_this_video\">Це відео більше не з\\`явиться серед рекомендацій</string>\n    <string name=\"you_wont_see_this_channel\">Цей канал більше не з\\`явиться серед рекомендацій</string>\n    <string name=\"settings_search\">Пошук</string>\n    <string name=\"dialog_search\">Пошук</string>\n    <string name=\"instant_voice_search\">Відразу запускати голосовий пошук</string>\n    <string name=\"option_background_playback_behind\">Грати позаду</string>\n    <string name=\"next_video_info_is_not_loaded_yet\">Інформація про наступне відео ще не завантажена</string>\n    <string name=\"card_multiline_title\">Декілька рядків у заголовках</string>\n    <string name=\"cards_style\">Стиль карток</string>\n    <string name=\"repeat_mode_all\">Відтворювати відео безперервно</string>\n    <string name=\"repeat_mode_one\">Повторювати поточне відео</string>\n    <string name=\"repeat_mode_pause\">Ставити відтворення на паузу після кожного відео (крім черги)</string>\n    <string name=\"repeat_mode_pause_alt\">Відтворювати безперервно тільки відео з плейлиста</string>\n    <string name=\"repeat_mode_none\">Завершувати відтворення після одного відео (крім черги)</string>\n    <string name=\"subscribed_to_channel\">Підписка оформлена</string>\n    <string name=\"unsubscribed_from_channel\">Підписка не оформлена</string>\n    <string name=\"subtitle_yellow_transparent\">Жовті на прозорому фоні</string>\n    <string name=\"subtitle_white_transparent\">Білі на прозорому фоні</string>\n    <string name=\"subtitle_white_black\">Білі на чорному фоні</string>\n    <string name=\"player_seek_preview\">Попередній перегляд при перемотуванні</string>\n    <string name=\"color_scheme_dark_grey_oled\">Темно-сіра (OLED)</string>\n    <string name=\"channels_section_sorting\">Сортування розділу Канали</string>\n    <string name=\"sorting_by_new_content\">Новий контент</string>\n    <string name=\"sorting_alphabetically\">За абеткою</string>\n    <string name=\"sorting_last_viewed\">Останні переглянуті</string>\n    <string name=\"player_pause_when_seek\">Пауза при перемотуванні</string>\n    <string name=\"playlists_style\">Стиль розділу Плейлисти</string>\n    <string name=\"playlists_style_grid\">Сітка</string>\n    <string name=\"playlists_style_rows\">Рядки</string>\n    <string name=\"player_show_clock\">Відображати годинник у панелі керування</string>\n    <string name=\"player_show_remaining_time\">Відображати час, що залишився у панелі керування</string>\n    <string name=\"player_show_ending_time\">Відображати час закінчення у панелі керування</string>\n    <string name=\"open_channel_uploads\">Відкрити завантаження</string>\n    <string name=\"app_exit_shortcut\">Вихід з застосунку</string>\n    <string name=\"player_exit_shortcut\">Вихід з плеєру</string>\n    <string name=\"search_exit_shortcut\">Вихід з пошуку</string>\n    <string name=\"app_exit_none\">Не виходити</string>\n    <string name=\"app_double_back_exit\">Подвійне назад</string>\n    <string name=\"app_single_back_exit\">Одиночне назад</string>\n    <string name=\"action_video_zoom\">Відео зум</string>\n    <string name=\"video_zoom\">Відео зум</string>\n    <string name=\"video_zoom_default\">За замовчуванням</string>\n    <string name=\"video_zoom_fit_width\">За шириною</string>\n    <string name=\"video_zoom_fit_height\">За висотою</string>\n    <string name=\"video_zoom_fit_both\">За шириною або висотою</string>\n    <string name=\"video_zoom_stretch\">Розтягнути</string>\n    <string name=\"color_scheme_teal\">Бірюзова</string>\n    <string name=\"color_scheme_teal_oled\">Бірюзова (OLED)</string>\n    <string name=\"player_seek_preview_none\">Відключено</string>\n    <string name=\"player_seek_preview_single\">Один кадр</string>\n    <string name=\"player_seek_preview_carousel\">Карусель</string>\n    <string name=\"player_seek_preview_carousel_slow\">Карусель (повільно, за ключовими кадрами)</string>\n    <string name=\"player_seek_preview_carousel_fast\">Карусель (швидко, не точні прев\\'ю)</string>\n    <string name=\"unsubscribe_from_channel\">Відписатися від каналу</string>\n    <string name=\"card_title_lines_num\">Кількість рядків у картках</string>\n    <string name=\"card_auto_scrolled_title\">Прокручувати обрізаний заголовок</string>\n    <string name=\"share_link\">Поділитися посиланням</string>\n    <string name=\"share_qr_link\">Поділитися посиланням (QR код)</string>\n    <string name=\"subscribe_to_channel\">Підписатися на канал</string>\n    <string name=\"wait_data_loading\">Зачекайте поки дані завантажуються…</string>\n    <string name=\"auto_frame_rate_pause\">Auto Frame Rate (пауза при перемиканні)</string>\n    <string name=\"auto_frame_rate_applying\">Застосовується автофреймрейт %sx%s@%s</string>\n    <string name=\"audio_shift\">Зміщення аудіо</string>\n    <string name=\"player_remember_speed\">Запам\\'ятовувати швидкість</string>\n    <string name=\"mark_channel_as_watched\">Позначити як переглянуте</string>\n    <string name=\"channel_marked_as_watched\">Канал позначен як переглянутий</string>\n    <string name=\"dialog_add_device\">Додати пристрій</string>\n    <string name=\"dialog_remove_all_devices\">Видалити усі пристрої</string>\n    <string name=\"device_link_enabled\">Прив`язка активована</string>\n    <string name=\"device_connected\">Пристрій \\\"%s\\\" підключений</string>\n    <string name=\"device_disconnected\">Пристрій \\\"%s\\\" відключений</string>\n    <string name=\"settings_ui_scale\">Масштаб інтерфейсу</string>\n    <string name=\"msg_restart_app\">Будь ласка, перезапустіть застосунок, щоб зміни набули чинності</string>\n    <string name=\"settings_block\">Блокування контенту</string>\n    <string name=\"msg_applying\">Застосовується %s…</string>\n    <string name=\"msg_done\">Готово</string>\n    <string name=\"settings_general\">Загальні</string>\n    <string name=\"settings_video\">Відео</string>\n    <string name=\"content_block_confirm_skip\">Підтвердження при пропуску</string>\n    <string name=\"content_block_categories\">Пропускати сегменти</string>\n    <string name=\"content_block_sponsor\">Спонсорський блок</string>\n    <string name=\"content_block_intro\">Вступ</string>\n    <string name=\"content_block_outro\">Заключний екран/титри</string>\n    <string name=\"content_block_interaction\">Нагадування про підписку</string>\n    <string name=\"content_block_self_promo\">Самореклама</string>\n    <string name=\"content_block_music_off_topic\">Немузична секція кліпу</string>\n    <string name=\"confirm_segment_skip\">Пропустити сегмент \\\"%s\\\"?</string>\n    <string name=\"cancel_segment_skip\">Скасувати пропуск сегменту</string>\n    <string name=\"msg_skipping_segment\">Пропуск сегменту \\\"%s\\\"…</string>\n    <string name=\"content_block_notification_type\">Тип повідомлень</string>\n    <string name=\"content_block_notify_none\">Без повідомлень</string>\n    <string name=\"content_block_notify_toast\">Спливаюче повідомлення</string>\n    <string name=\"content_block_notify_dialog\">Діалог підтвердження</string>\n    <string name=\"return_to_launcher\">Повертатися у лаунчер із ATV каналів/пошуку</string>\n    <string name=\"intent_force_close\">Примусовий вихід, якщо відео відкрито з зовнішнього джерела</string>\n    <string name=\"btn_confirm\">Підтвердити</string>\n    <string name=\"player_low_video_quality\">Погана якість зображення</string>\n    <string name=\"remote_session_closed\">Віддалена сесія закрита</string>\n    <string name=\"msg_mode_switch_error\">Неможливо перемкнути дисплей у режим \\\"%s\\\"</string>\n    <string name=\"msg_player_error\">У плеєрі виникла помилка %s. Перезапускаю відтворювання…</string>\n    <string name=\"video_preset_disabled\">Без пресета</string>\n    <string name=\"video_preset_enabled\">Формат слідуючого відео буде встановлений у відповідності до пресету</string>\n    <string name=\"player_sleep_timer\">Таймер сну</string>\n    <string name=\"player_show_quality_info\">Відображати інформацію про якість відео у панелі керування</string>\n    <string name=\"player_remember_each_speed\">Запам\\'ятовувати швидкість кожного відео</string>\n    <string name=\"player_remember_speed_none\">Не запам\\'ятовувати</string>\n    <string name=\"player_remember_speed_all\">Однакова швидкість на усіх відео</string>\n    <string name=\"player_remember_speed_each\">На кожному відео своя швидкість</string>\n    <string name=\"msg_player_error_source\">Не вдається завантажити відео</string>\n    <string name=\"msg_player_error_renderer\">Обраний формат не підтримуєтся.\\nСпробуйте обрати інший формат.\\nЯкщо це не допоможе, то спробуйте перезавантажити пристрій.</string>\n    <string name=\"msg_player_error_video_renderer\">Обраний ВІДЕОФОРМАТ не підтримуєтся.\\nСпробуйте обрати інший формат, натиснувши кнопку HQ у плеєрі.\\nЯкщо це не допоможе, то спробуйте перезавантажити пристрій.</string>\n    <string name=\"msg_player_error_audio_renderer\">Обраний АУДІОФОРМАТ не підтримуєтся.\\nСпробуйте обрати інший формат, натиснувши кнопку HQ у плеєрі.\\nЯкщо це не допоможе, то спробуйте перезавантажити пристрій.</string>\n    <string name=\"msg_player_error_subtitle_renderer\">Обраний формат СУБТИТРІВ не підтримуєтся.\\nСпробуйте обрати інший формат, утримуючи кнопку CC у плеєрі.\\nЯкщо це не допоможе, то спробуйте перезавантажити пристрій.</string>\n    <string name=\"msg_player_error_unexpected\">Невідома помилка у відеодекодері</string>\n    <string name=\"video_aspect\">Співвідношення сторін</string>\n    <string name=\"player_tweaks\">Для розробників</string>\n    <string name=\"header_uploads\">Завантаження</string>\n    <string name=\"player_show_global_clock\">Постійно відображати на екрані годинник</string>\n    <string name=\"player_show_global_ending_time\">Постійно відображати на екрані час закінчення відео</string>\n    <string name=\"app_corner_clock\">Головна: годинник у верхньому правому куті</string>\n    <string name=\"player_corner_clock\">Плеєр: годинник у верхньому правому куті</string>\n    <string name=\"player_corner_ending_time\">Плеєр: час закінчення у верхньому правому куті</string>\n    <string name=\"uploads_old_look\">Повернути старий вигляд розділу Завантаження</string>\n    <string name=\"background_playback_activation\">Фонове відтворення (активація)</string>\n    <string name=\"channels_old_look\">Повернути старий вигляд розділу Канали</string>\n    <string name=\"channels_auto_load\">Автоматично завантажувати вміст розділу Канали</string>\n    <string name=\"return_to_background_video\">Повернутися до фонового відео</string>\n    <string name=\"pin_unpin_from_sidebar\">Додати/Видалити з бічної панелі</string>\n    <string name=\"pin_unpin_playlist\">Додати/Видалити плейлист з бічної панелі</string>\n    <string name=\"pin_unpin_channel\">Додати/Видалити канал з бічної панелі</string>\n    <string name=\"pin_playlist\">Додати плейлист на бічну панель</string>\n    <string name=\"pin_channel\">Додати канал на бічну панель</string>\n    <string name=\"pinned_to_sidebar\">Додано на бічну панель</string>\n    <string name=\"unpin_from_sidebar\">Видалити з бічної панелі</string>\n    <string name=\"unpin_group_from_sidebar\">Видалити групу підписок</string>\n    <string name=\"unpinned_from_sidebar\">Видалено з бічної панелі</string>\n    <string name=\"dialog_select_country\">Країна</string>\n    <string name=\"settings_language_country\">Мова/Країна</string>\n    <string name=\"share_embed_link\">Поділитися вбудованим посиланням</string>\n    <string name=\"card_text_scroll_factor\">Швидкість прокрутки тексту картки</string>\n    <string name=\"preferred_update_source\">Вибрати джерело оновлень</string>\n    <string name=\"hide_shorts\">Приховувати короткі відео з Підписок</string>\n    <string name=\"hide_shorts_channel\">Приховувати короткі відео на каналі</string>\n    <string name=\"key_remapping\">Перевизначення клавіш</string>\n    <string name=\"screen_dimming\">Затемнення екрану</string>\n    <string name=\"removed_from_playback_queue\">Видалено з черги відтворення</string>\n    <string name=\"added_to_playback_queue\">Додано до черги відтворення</string>\n    <string name=\"add_remove_from_playback_queue\">Додати/Видалити з черги відтворення</string>\n    <string name=\"add_to_playback_queue\">Додати до черги відтворення</string>\n    <string name=\"remove_from_playback_queue\">Видалити з черги відтворення</string>\n    <string name=\"proxy_enabled\">Проксі активовано</string>\n    <string name=\"proxy_disabled\">Проксі деактивовано</string>\n    <string name=\"uploads_row_name\">Завантаження</string>\n    <string name=\"playlists_row_name\">Створені списки відтворення</string>\n    <string name=\"popular_uploads_row_name\">Популярні завантаження</string>\n    <string name=\"news_row_name\">Новини</string>\n    <string name=\"breaking_news_row_name\">Термінові новини</string>\n    <string name=\"covid_news_row_name\">Новини про коронавірус COVID-19</string>\n    <string name=\"enable_voice_search\">Увімкнути глобальний пошук (потрібна підтримка прошивки)</string>\n    <string name=\"subscribe_unsubscribe_from_channel\">Підписатися/Відписатися від каналу</string>\n    <string name=\"app_backup_restore\">Бекап та відновлення</string>\n    <string name=\"app_backup\">Резервне копіювання даних застосунку</string>\n    <string name=\"app_restore\">Відновити дані застосунку</string>\n    <string name=\"skip_each_segment_once\">Не пропускати сегменти повторно</string>\n    <string name=\"disable_ok_long_press\">Не обробляти довге утримання кнопки ОК</string>\n    <string name=\"disable_ok_long_press_desc\">Призначено для глючних пультів, де кнопка OK не працює належним чином</string>\n    <string name=\"player_time_correction\">Відображати час з урахуванням швидкості відтворення</string>\n    <string name=\"sponsor_color_markers\">Кольорові помітки на шкалі прогресу</string>\n    <string name=\"refresh_section\">Оновити розділ</string>\n    <string name=\"option_disabled\">Відключено</string>\n    <string name=\"live_now_row_name\">У прямому ефірі зараз</string>\n    <string name=\"run_in_background\">Фонове відтворення</string>\n    <string name=\"settings_remote_control\">Дистанційне керування</string>\n    <string name=\"background_service_started\">Фоновий сервіс запущений</string>\n    <string name=\"dialog_add_to\">Додати у %s</string>\n    <string name=\"dialog_remove_from\">Видалити із %s</string>\n    <string name=\"added_to\">Додано у %s</string>\n    <string name=\"removed_from\">Видалено із %s</string>\n    <string name=\"video_preset_adaptive\">Адаптивний</string>\n    <string name=\"content_block_preview_recap\">Зміст попереднього епізоду</string>\n    <string name=\"content_block_highlight\">Цікавинка (закладка)</string>\n    <string name=\"removed_from_history\">Відео видалено із історії</string>\n    <string name=\"remove_from_history\">Видалити із історії</string>\n    <string name=\"upload_date\">Дата завантаження</string>\n    <string name=\"upload_date_any\">Увесь час</string>\n    <string name=\"upload_date_today\">Сьогодні</string>\n    <string name=\"upload_date_this_week\">Цей тиждень</string>\n    <string name=\"upload_date_this_month\">Цей місяць</string>\n    <string name=\"upload_date_this_year\">Цей рік</string>\n    <string name=\"double_refresh_rate\">Подвоювати частоту</string>\n    <string name=\"upload_date_last_hour\">Остання година</string>\n    <string name=\"focus_on_search_results\">Автофокусування на результатах пошуку</string>\n    <string name=\"context_menu\">Контекстне меню</string>\n    <string name=\"context_menu_sorting\">Сортування контекстного меню</string>\n    <string name=\"add_remove_from_recent_playlist\">Додати/Видалити з недавнього плейлиста</string>\n    <string name=\"hide_settings_section\">Приховувати розділ Налаштування (небезпечно!)</string>\n    <string name=\"volume\">Гучність %s%%</string>\n    <string name=\"mark_all_channels_watched\">Відмітити усі канали як переглядені</string>\n    <string name=\"move_section_up\">Перемістити розділ вище</string>\n    <string name=\"move_section_down\">Перемістити розділ нижче</string>\n    <string name=\"player_buttons\">Налаштувати кнопки плеєра</string>\n    <string name=\"action_playlist_add\">Додати до плейлиста</string>\n    <string name=\"action_video_stats\">Інформація для адмінів</string>\n    <string name=\"action_subscribe\">Підписатися на канал</string>\n    <string name=\"action_channel\">Відкрити канал</string>\n    <string name=\"action_pip\">Картинка в картинці</string>\n    <string name=\"action_video_info\">Опис відео</string>\n    <string name=\"action_screen_off\">Вимкнути екран</string>\n    <string name=\"action_sound_off\">Без звуку</string>\n    <string name=\"action_playback_queue\">Черга відтворення</string>\n    <string name=\"action_video_speed\">Швидкість відео</string>\n    <string name=\"action_subtitles\">Субтитри</string>\n    <string name=\"action_like\">Подобається</string>\n    <string name=\"action_dislike\">Не подобається</string>\n    <string name=\"action_play_pause\">Відтворення/Пауза</string>\n    <string name=\"action_repeat_mode\">Режим відтворення</string>\n    <string name=\"action_next\">Наступне відео</string>\n    <string name=\"action_previous\">Попереднє відео</string>\n    <string name=\"action_high_quality\">Якість відео</string>\n    <string name=\"content_block_no_skipping_mode\">Режим без пропусків</string>\n    <string name=\"content_block_action_type\">Вибрати дію</string>\n    <string name=\"content_block_action_none\">Нічого не робити</string>\n    <string name=\"content_block_action_only_skip\">Тільки пропуск</string>\n    <string name=\"content_block_action_toast\">Пропуск з повідомленням</string>\n    <string name=\"content_block_action_dialog\">Діалог підтвердження</string>\n    <string name=\"content_block_filler\">Відхід від теми (офтопік)</string>\n    <string name=\"keyboard_auto_show\">Показувати клавіатуру автоматично</string>\n    <string name=\"cancel_dialog\">Скасувати</string>\n    <string name=\"msg_player_error_source2\">Посилання не працює або невірний час на пристрої.\\nСкоріш за все проблема у застосунку.</string>\n    <string name=\"msg_player_error_video_source\">Посилання на ВІДЕОПОТІК не працює або невірний час на пристрої.\\nСкоріш за все проблема у застосунку.</string>\n    <string name=\"msg_player_error_audio_source\">Посилання на АУДІОПОТІК не працює або невірний час на пристрої.\\nСкоріш за все проблема у застосунку.</string>\n    <string name=\"msg_player_error_subtitle_source\">Посилання на СУБТИТРИ не працює або невірний час на пристрої.\\nСкоріш за все проблема у застосунку.</string>\n    <string name=\"hide_upcoming\">Приховувати анонси з Підписок</string>\n    <string name=\"hide_upcoming_channel\">Приховувати анонси на Каналі</string>\n    <string name=\"hide_upcoming_home\">Приховувати анонси з Головної</string>\n    <string name=\"search_background_playback\">Фонове відтворення під час пошуку/перегляду каналу</string>\n    <string name=\"trending_row_name\">Популярне</string>\n    <string name=\"player_seek_type\">Поведінка перемотування</string>\n    <string name=\"player_seek_regular\">Звичайна</string>\n    <string name=\"player_seek_confirmation_pause\">З підтвердженням (пауза під час перемотування)</string>\n    <string name=\"player_seek_confirmation_play\">З підтвердженням (грати під час перемотування)</string>\n    <string name=\"hide_shorts_from_home\">Приховувати короткі відео з Головної</string>\n    <string name=\"finish_on_disconnect\">Завершувати застосунок після відключення телефону/планшету</string>\n    <string name=\"update_error\">Помилка оновлення</string>\n    <string name=\"hide_shorts_from_search\">Приховувати короткі відео з Пошуку</string>\n    <string name=\"hide_shorts_from_history\">Приховувати короткі відео з Історії</string>\n    <string name=\"hide_shorts_from_trending\">Приховувати короткі відео з Трендів</string>\n    <string name=\"disable_screensaver\">Вимкнути скрінсейвер</string>\n    <string name=\"description_not_found\">Опис не знайдено</string>\n    <string name=\"proxy_port_hint\">Порт проксі-сервера</string>\n    <string name=\"proxy_host_hint\">Ім\\'я або IP-адреса проксі-сервера</string>\n    <string name=\"proxy_username_hint\">Ім\\'я користувача проксі</string>\n    <string name=\"proxy_password_hint\">Пароль проксі-сервера</string>\n    <string name=\"proxy_type\">Тип проксі-сервера</string>\n    <string name=\"enable_web_proxy\">Використовувати веб-проксі</string>\n    <string name=\"proxy_not_supported\">Використовувати веб-проксі (потрібен Android 4.4+)</string>\n    <string name=\"proxy_test_btn\">Тестувати</string>\n    <string name=\"proxy_settings_title\">Налаштування проксі-сервера</string>\n    <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n    <string name=\"proxy_test_cancelled\">Тест №%d: скасовано.</string>\n    <string name=\"proxy_type_invalid\">Тип проксі не встановлено.</string>\n    <string name=\"proxy_host_invalid\">Хост проксі не встановлено.</string>\n    <string name=\"proxy_port_invalid\">Недійсний проксі-порт, має &gt; 0.</string>\n    <string name=\"proxy_credentials_invalid\">Неприпустиме ім\\'я користувача або пароль.</string>\n    <string name=\"proxy_test_aborted\">Тест скасовано, спершу виправте налаштування проксі.</string>\n    <string name=\"proxy_application_aborted\">Будь ласка, спершу виправте налаштування проксі.</string>\n    <string name=\"proxy_test_start\">Тест#%d: %s …</string>\n    <string name=\"proxy_test_error\">Помилка #%d: %s</string>\n    <string name=\"proxy_test_status\">Статус#%d: %s %d %s</string>\n    <string name=\"openvpn_config_address_hint\">Адреса файла конфігурації (*.ovpn)</string>\n    <string name=\"enable_openvpn\">Використовувати OpenVPN</string>\n    <string name=\"openvpn_settings_title\">Налаштування OpenVPN</string>\n    <string name=\"openvpn_application_aborted\">Будь ласка, спершу виправте налаштування OpenVPN.</string>\n    <string name=\"openvpn_test_aborted\">Тест скасовано, спершу виправте налаштування OpenVPN.</string>\n    <string name=\"openvpn_address_invalid\">Адреса файла конфігурації не встановлена.</string>\n    <string name=\"internet_censorship\">Цензура в Інтернеті</string>\n    <string name=\"rename_section\">Перейменувати цей розділ</string>\n    <string name=\"simple_edit_value_hint\">Введіть значення</string>\n    <string name=\"seek_interval\">Інтервал перемотування</string>\n    <string name=\"seek_interval_sec\">%s сек</string>\n    <string name=\"subtitle_system\">Системний стиль (Налаштування Андроід > Спец. можливості)</string>\n    <string name=\"subtitle_scale\">Масштаб субтитрів</string>\n    <string name=\"audio_sync_fix_desc\">Альтернативний спосіб синхронізації аудіо/відео. Вважається застарілим, але, в деяких випадках, допомагає.</string>\n    <string name=\"audio_sync_fix\">Виправлення розсинхронізації звуку</string>\n    <string name=\"ambilight_ratio_fix_desc\">Виправляє відсутнє фонове підсвічування. Виправляє невірне співвідношення сторін або масштаб відео. Виправляє порожні скріншоти. Впливає на продуктивність пристрою!</string>\n    <string name=\"ambilight_ratio_fix\">Виправлення Ambilight, співвідношення сторін, масштабу відео та скріншотів</string>\n    <string name=\"force_legacy_codecs_desc\">Значно покращує продуктивність на слабких пристроях. Максимальна роздільна здатність 720p.</string>\n    <string name=\"force_legacy_codecs\">Використовувати застарілі кодеки (720p)</string>\n    <string name=\"live_stream_fix_desc\">Увага: перемотування прямих трансляцій буде недоступним. Значно покращує продуктивність трансляцій на слабких пристроях. Максимальна роздільна здатність 1080p.</string>\n    <string name=\"live_stream_fix_4k_desc\">Увага: перемотування прямих трансляцій буде недоступним. Значно покращує продуктивність трансляцій. Максимальна роздільна здатність 4K.</string>\n    <string name=\"live_stream_fix\">Виправлення прямих трансляцій (1080p)</string>\n    <string name=\"live_stream_fix_4k\">Виправлення прямих трансляцій (4K)</string>\n    <string name=\"playback_notifications_fix_desc\">Приховує сповіщення про зміну відтворюваного треку. Може бути корисним на прошивках на базі AOSP.</string>\n    <string name=\"playback_notifications_fix\">Вимкнути сповіщення про відтворення</string>\n    <string name=\"amlogic_fix_desc\">Виправлення втрати кадрів на пристроях на базі Amlogic.</string>\n    <string name=\"amlogic_fix\">Виправлення 1080p@60fps на Amlogic</string>\n    <string name=\"tunneled_video_playback_desc\">Увага: можливі проблеми з паузой! Тунельне відтворення обіцяє кращу синхронізацію аудіо/відео та більш плавне відтворення. Потрібен Андроїд 5+</string>\n    <string name=\"tunneled_video_playback\">Тунельне відтворення (Android 5+)</string>\n    <string name=\"master_volume\">Основна гучність</string>\n    <string name=\"volume_limit\">Обмеження гучності</string>\n    <string name=\"player_volume\">Гучність</string>\n    <string name=\"play_video\">Відтворити</string>\n    <string name=\"remember_position_of_short_videos\">Запам\\'ятовувати позицію коротких відео (меньше 5 хв)</string>\n    <string name=\"remember_position_of_live_videos\">Запам\\'ятовувати позицію прямих трансляцій</string>\n    <string name=\"player_show_tooltips\">Показати підказки для кнопок</string>\n    <string name=\"action_like_unset\">Подобається не встановлено</string>\n    <string name=\"action_dislike_unset\">Не подобається не встановлено</string>\n    <string name=\"various_buttons\">Кнопки у верхній частині головного вікна</string>\n    <string name=\"not_compatible_with\">Обрана опція не зумісна з</string>\n    <string name=\"subtitle_position\">Зсув субтитрів внизу</string>\n    <string name=\"pressing_home\">натиснувши ДОДОМУ</string>\n    <string name=\"pressing_home_back\">натиснувши ДОДОМУ або ПОВЕРНУТИСЯ</string>\n    <string name=\"pressing_back\">натиснувши ПОВЕРНУТИСЯ</string>\n    <string name=\"player_number_key_seek\">Перемотувати цифровими клавішами</string>\n    <string name=\"save_remove_playlist\">Додати/Видалити плейлист з розділу Плейлисти</string>\n    <string name=\"save_playlist\">Додати плейлист до розділу Плейлисти</string>\n    <string name=\"remove_playlist\">Видалити плейлист з розділу Плейлисти назавжди</string>\n    <string name=\"remove_playlist_fmt\">Видалити %s назавжди?</string>\n    <string name=\"removed_from_playlists\">Видалено з розділу Плейлисти</string>\n    <string name=\"saved_to_playlists\">Додано у розділ Плейлисти</string>\n    <string name=\"create_playlist\">Створити плейлист</string>\n    <string name=\"create_playlist_note\">УВАГА: Його не буде видно в застосунку YouTube</string>\n    <string name=\"add_video_to_new_playlist\">Додати у новий плейлист</string>\n    <string name=\"cant_delete_empty_playlist\">Неможливо видалити порожній плейлист</string>\n    <string name=\"playlist\">Плейлист</string>\n    <string name=\"rename_playlist\">Перейменувати плейлист</string>\n    <string name=\"cant_rename_empty_playlist\">Неможливо перейменувати порожній плейлист</string>\n    <string name=\"cant_rename_foreign_playlist\">Неможливо перейменувати чужий плейлист</string>\n    <string name=\"cant_save_playlist\">Цей плейлист додати неможливо</string>\n    <string name=\"enter_value\">Введіть будь-яке непусте значення</string>\n    <string name=\"dialog_add_remove_from\">Додати/Видалити з %s</string>\n    <string name=\"lb_playback_controls_skip_next\">\"Перейти до наступного відео\"</string>\n    <string name=\"lb_playback_controls_skip_previous\">\"Перейти до попереднього відео/Перемотати на початок\"</string>\n    <string name=\"alt_presets_behavior\">Альтернативна поведінка відео пресетів (обмеження пропускної спроможності)</string>\n    <string name=\"alt_presets_behavior_desc\">Додаток спробує підтримувати пропускну здатність близьку до пресету замість того, щоб вибрати точну відповідність між роздільною здатністю, частотою кадрів та кодеком.</string>\n    <string name=\"sleep_timer\">Таймер сна (якщо пульт не використовувался одну годину)</string>\n    <string name=\"sleep_timer_desc\">Ставити відтворення на паузу, якщо користувач не використовував пульт дистанційного керування протягом однієї години</string>\n    <string name=\"disable_vsync\">Вимкнути вертикальну синхронізацію</string>\n    <string name=\"disable_vsync_desc\">Цей параметр вимикає вирівнювання кадрів із сигналом вертикальної синхронізації дисплея. Може підвищити продуктивність на слабких пристроях, звільнивши ресурси ЦП.</string>\n    <string name=\"skip_codec_profile_check\">Пропустити перевірку кодеків</string>\n    <string name=\"skip_codec_profile_check_desc\">Не перевіряти підтримку кодеків під час запуску відео. Може бути корисним для глючних прошивок.</string>\n    <string name=\"force_sw_codec\">Активувати програмний декодер</string>\n    <string name=\"force_sw_codec_desc\">Може відтворювати будь-яке відео, але продуктивність дуже низька.</string>\n    <string name=\"playlist_order\">Сортувати плейлист</string>\n    <string name=\"playlist_order_added_date_newer_first\">Дата додавання (спочатку новіші)</string>\n    <string name=\"playlist_order_added_date_older_first\">Дата додавання (спочатку старіші)</string>\n    <string name=\"playlist_order_published_date_newer_first\">Дата публікації (спочатку новіші)</string>\n    <string name=\"playlist_order_published_date_older_first\">Дата публікації (спочатку старіші)</string>\n    <string name=\"playlist_order_popularity\">За популярністю</string>\n    <string name=\"cant_do_this_for_foreign_playlist\">Неможливо це зробити для чужого плейлиста</string>\n    <string name=\"owned_playlist_warning\">Помилка. Ця дія застосовується лише для своїх плейлистів.</string>\n    <string name=\"content_block_alt_server\">Використовувати альтернативний сервер</string>\n    <string name=\"content_block_alt_server_desc\">Увімкніть цю опцію, якщо SponsorBlock відмовляється працювати з різних причин.</string>\n    <string name=\"unset_stream_reminder\">Скасувати нагадування про трансляцію</string>\n    <string name=\"set_stream_reminder\">Встановити нагадування про трансляцію</string>\n    <string name=\"playback_starts_shortly\">Відтворення почнеться автоматично, коли трансляція буде готова</string>\n    <string name=\"starting_stream\">Трансляція почалася…</string>\n    <string name=\"action_playlist_remove\">Видалити з плейлиста</string>\n    <!-- BEGIN Moved strings -->\n    <string name=\"signin_view_title\">Код завантажується…</string>\n    <string name=\"signin_view_description\">Шоб увійти в обліковий запис, введіть цей код на сторінці %s\\nПРИМІТКА. Працює лише з браузерами Firefox або Chrome.</string>\n    <string name=\"signin_view_action_text\">Готово</string>\n    <string name=\"require_checked\">Потрібно \\\"%s\\\"</string>\n    <string name=\"msg_press_again_to_exit\">Натисніть ще раз для виходу</string>\n    <string name=\"player_remaining_time\">Залишилось: %s</string>\n    <string name=\"player_ending_time\">Закінчиться в %s</string>\n    <string name=\"badge_new_content\">НОВИЙ КОНТЕНТ</string>\n    <string name=\"badge_live\">НАЖИВО</string>\n    <string name=\"add_device_view_description\">Щоб прив`язати пристрій, введіть цей код у застосунку YouTube на телефоні у розділі Налаштування/Дивитися на ТВ\\nПРИМІТКА. Працює лише з клавіатурою Gboard.</string>\n    <!-- END Moved strings -->\n    <string name=\"skip_24_rate\">Пропускати 24fps формати (виправлення режиму real cinema?)</string>\n    <string name=\"skip_shorts\">Пропускати Shorts</string>\n    <string name=\"player_disable_suggestions\">Вимкнути рекомендації</string>\n    <string name=\"suggestions\">Пропозиції</string>\n    <string name=\"feedback\">Зворотній зв`язок</string>\n    <string name=\"sources\">Вихідний код</string>\n    <string name=\"releases\">Релізи</string>\n    <string name=\"prefer_avc_over_vp9\">Вибір кодека: віддавати перевагу avc над vp9</string>\n    <string name=\"open_chat\">Чат/Коментарі</string>\n    <string name=\"open_comments\">Відкрити коментарі</string>\n    <string name=\"place_chat_left\">Розмістити чат ліворуч</string>\n    <string name=\"place_comments_left\">Розмістити коментарі ліворуч</string>\n    <string name=\"use_alt_speech_recognizer\">Альтернативний розпізнавач мови</string>\n    <string name=\"time_format\">Формат часу</string>\n    <string name=\"time_format_24\">24 години</string>\n    <string name=\"time_format_12\">12 годин (AM/PM)</string>\n    <string name=\"use_alt_speech_recognizer_desc\">Серйозно забагований. Використовуйте його, лише якщо є проблеми із розпізнавачем за замовчуванням.</string>\n    <string name=\"speech_recognizer\">Розпізнавач мови</string>\n    <string name=\"speech_engine\">Рушій голосового пошуку</string>\n    <string name=\"speech_recognizer_system\">Системний (найкращій)</string>\n    <string name=\"speech_recognizer_external_1\">Зовнішній 1 (серйозно забагований, для роботи потрібно заборонити доступ до мікрофону)</string>\n    <string name=\"speech_recognizer_external_2\">Зовнішній 2 (серйозно забагований)</string>\n    <string name=\"real_channel_icon\">Показувати іконку на кнопці каналу</string>\n    <string name=\"subtitle_yellow_semi_transparent\">Жовті на напівпрозорому фоні</string>\n    <string name=\"subtitle_yellow_black\">Жовті на чорному фоні</string>\n    <string name=\"player_pixel_ratio\">Співвідношення сторін пікселя</string>\n    <string name=\"color_scheme_dark_grey_monochrome\">Темно-сіра (монохромна)</string>\n    <string name=\"disable_mic_permission\">Будь ласка, вимкніть доступ до мікрофона для програми, щоб цей розпізнавач працював належним чином.</string>\n    <string name=\"repeat_mode_shuffle\">Перемішати будь-який плейлист</string>\n    <string name=\"chat_left\">Ліворуч</string>\n    <string name=\"chat_right\">Праворуч</string>\n    <string name=\"card_real_thumbnails\">Замінити мініатюри кадром із відео</string>\n    <string name=\"card_content\">Звідки брати мініатюри карток</string>\n    <string name=\"thumb_quality_default\">За замовчуванням</string>\n    <string name=\"thumb_quality_start\">Початок відео</string>\n    <string name=\"thumb_quality_middle\">Середина відео</string>\n    <string name=\"thumb_quality_end\">Кінець відео</string>\n    <string name=\"content_block_status\">Перевірити стан серверу SponsorBlock</string>\n    <string name=\"dearrow_status\">Перевірити стан серверу DeArrow</string>\n    <string name=\"player_show_quality_info_bitrate\">Додавати бітрейт до інформації о якості відео</string>\n    <string name=\"player_speed_button_old_behavior\">Повернути стару поведінку кнопки швидкість</string>\n    <string name=\"protect_settings_with_password\">Захистити усі налаштування паролем</string>\n    <string name=\"enter_settings_password\">Введіть пароль для входу в налаштування</string>\n    <string name=\"child_mode\">Дитячий режим</string>\n    <string name=\"child_mode_desc\">У цьому режимі користувач не може використовувати пошук або переглядати запропонований контент. Налаштування будуть під паролем.</string>\n    <string name=\"lost_setting_warning\">Налаштування застосунку буде змінено. Переконайтеся, що ви створили резервну копію налаштувань.</string>\n    <string name=\"player_button_long_click\">Коротке натискання кнопки для швидкої дії та довге для діалогового вікна налаштувань</string>\n    <string name=\"pause_history\">Поставити історію на паузу</string>\n    <string name=\"resume_history\">Відновити історію</string>\n    <string name=\"clear_history\">Очистити історію</string>\n    <string name=\"disable_history\">Вимкнути історію</string>\n    <string name=\"enable_history\">Увімкнути історію</string>\n    <string name=\"chapters\">Епізоди</string>\n    <string name=\"card_multiline_subtitle\">Декілька рядків у підзаголовках</string>\n    <string name=\"auto_frame_rate_modes\">Підтримувані режими</string>\n    <string name=\"loading\">Завантаження…</string>\n    <string name=\"hide_streams\">Приховувати прямі трансляції з Підписок</string>\n    <string name=\"video_rotate\">Обертати</string>\n    <string name=\"video_flip\">Віддзеркалити</string>\n    <string name=\"trending_searches\">Популярні пошукові запити</string>\n    <string name=\"video_duration_any\">Будь-яка</string>\n    <string name=\"video_duration\">Тривалість</string>\n    <string name=\"video_duration_under_4\">Меньше 4 хвилин</string>\n    <string name=\"video_duration_between_4_20\">4–20 хвилин</string>\n    <string name=\"video_duration_over_20\">Більше 20 хвилин</string>\n    <string name=\"content_type\">Тип</string>\n    <string name=\"content_type_any\">Будь-який</string>\n    <string name=\"content_type_video\">Відео</string>\n    <string name=\"content_type_channel\">Канал</string>\n    <string name=\"content_type_playlist\">Плейлист</string>\n    <string name=\"content_type_movie\">Фільм</string>\n    <string name=\"video_features\">Характеристики</string>\n    <string name=\"video_feature_any\">Будь-які</string>\n    <string name=\"video_feature_live\">Прямі трансляції</string>\n    <string name=\"video_feature_4k\">4K</string>\n    <string name=\"video_feature_hdr\">HDR</string>\n    <string name=\"search_sorting\">Сортувати</string>\n    <string name=\"sort_by_relevance\">Відповідність</string>\n    <string name=\"sort_by_views\">Кількість переглядів</string>\n    <string name=\"sort_by_date\">Дата завантаження</string>\n    <string name=\"sort_by_rating\">Оцінка</string>\n    <string name=\"clear_search_history\">Видалити історію пошуку</string>\n    <string name=\"remove_from_subscriptions\">Приховати</string>\n    <string name=\"player_long_speed_list\">Довгий список швидкостей</string>\n    <string name=\"player_extra_long_speed_list\">Дуже довгий список швидкостей</string>\n    <string name=\"alt_app_icon\">Альтернативна іконка застосунку (потрібне перезавантаження)</string>\n    <string name=\"network_stack\">Надавати перевагу мережевому стеку %s</string>\n    <string name=\"player_network_stack\">Мережевий рушій</string>\n    <string name=\"cronet_desc\">Cronet — мережевий рушій Chromium. Зазвичай швидше за інших. Це може допомогти у проблемах буферизації.</string>\n    <string name=\"unlock_all_formats\">Розблокувати усі відео формати</string>\n    <string name=\"unlock_all_formats_desc\">Розблокувати формати, які, ймовірно, не підтримуються вашим пристроєм.</string>\n    <string name=\"okhttp_desc\">Цей мережевий рушій є найповільнішим, але має добру стабільність.</string>\n    <string name=\"select_channel_section\">Відкривати відповідний розділ, коли застосунок запущений з каналів ATV</string>\n    <string name=\"enable_voice_search_desc\">Офіційний застосунок буде замінений на так званий міст. Міст допомагає передавати запити глобального пошуку в наш застосунок.</string>\n    <string name=\"enable_master_password\">Увімкнути головний пароль</string>\n    <string name=\"enter_master_password\">Введіть головний пароль</string>\n    <string name=\"disable_stream_buffer\">Вимкнути буфер на стрімах</string>\n    <string name=\"disable_stream_buffer_desc\">Виправляє ситуації, коли стрім запізнюється. Увага: стрім може почати гальмувати.</string>\n    <string name=\"sony_frame_drop_fix\">Виправлення випадіння кадрів #1</string>\n    <string name=\"sony_frame_drop_fix_desc\">Виправляє лагання на тв Соні та деяких інших пристроях. Увага: можливі проблеми с синхронизацією аудіо.\n    </string>\n    <string name=\"audio_language\">Мова аудіо</string>\n    <string name=\"old_home_look\">Старий вигляд розділу Головна</string>\n    <string name=\"old_channel_look\">Старий вигляд сторінки Каналу</string>\n    <string name=\"hide_shorts_everywhere\">Приховувати короткі відео з усіх розділів</string>\n    <string name=\"update_found\">Оновлення</string>\n    <string name=\"volume_boost_warning\">Налаштування, які перевищують ліміт можуть пошкодити динаміки</string>\n    <string name=\"play_video_incognito\">Відтворити анонімно</string>\n    <string name=\"play_from_start\">Відтворити спочатку</string>\n    <string name=\"header_kids_home\">Дітям</string>\n    <string name=\"player_section_playlist\">Використовувати вміст поточного розділу як список відтворення</string>\n    <string name=\"header_trending\">Популярне</string>\n    <string name=\"screensaver\">Заставка</string>\n    <string name=\"player_screen_off_timeout\">Тайм-аут вимкнення екрана</string>\n    <string name=\"old_update_notifications\">Старий вигляд сповіщень про оновлення</string>\n    <string name=\"sidebar_notification\">Показувати сповіщення на бічній панелі</string>\n    <string name=\"dialog_notification\">Повідомляти про оновлення вспливаючим діалогом</string>\n    <string name=\"mark_as_watched\">Відмітити як переглянуте</string>\n    <string name=\"player_ui_animations\">Увімкнути анімацію інтерфейсу плеєра</string>\n    <string name=\"player_likes_count\">Відображати кількість лайків/діслайків</string>\n    <string name=\"sorting_alphabetically2\">За абеткою (швидко, анімовані прев\\'ю)</string>\n    <string name=\"sorting_default\">За замовчуванням (швидко, анімовані прев\\'ю)</string>\n    <string name=\"content_block_exclude_channel\">Виключити канал із SponsorBlock</string>\n    <string name=\"content_block_stop_excluding_channel\">Припинити виключати канал із SponsorBlock</string>\n    <string name=\"player_chapter_notification\">Швидке просування по епізодах за допомогою спливаючого сповіщення</string>\n    <string name=\"player_chapter_notification2\">Перейти до наступного епізоду, натиснувши на сповіщення</string>\n    <string name=\"subtitle_remember\">Запам\\'ятати ввімкнені субтитри для кожного каналу</string>\n    <string name=\"amazon_frame_drop_fix\">Виправлення випадіння кадрів #2</string>\n    <string name=\"amazon_frame_drop_fix_desc\">Виправляє лагання на Amazon Stick та деяких інших пристроях.</string>\n    <string name=\"default_stack_desc\">Вбудований мережевий рушій. Цей рушій може мати кращу стабільність у певних ситуаціях.</string>\n    <string name=\"item_postion\">Позиція</string>\n    <string name=\"player_screen_off_dimming\">Величина затемнення екрану</string>\n    <string name=\"screensaver_timout\">Тайм-аут увімкнення заставки</string>\n    <string name=\"screensaver_dimming\">Величина затемнення заставки</string>\n    <string name=\"player_ui_on_next\">Показувати інтерфейс під час переходу на наступне відео</string>\n    <string name=\"autogenerated\">автоматично створені</string>\n    <string name=\"player_auto_volume\">Автоматичне керування гучністю</string>\n    <string name=\"header_shorts\">Shorts</string>\n    <string name=\"player_global_focus\">Синхронізувати фокус між рядами кнопок плеєра</string>\n    <string name=\"auto_history\">Auto (використовувати налаштування облікового запису)</string>\n    <string name=\"remember_position_subscriptions\">Запам\\'ятовувати останню переглянуту позицію у Підписках</string>\n    <string name=\"remember_position_pinned\">Запам\\'ятовувати останню переглянуту позицію у закріплених плейлистах</string>\n    <string name=\"msg_player_unknown_error\">Невідома помилка</string>\n    <string name=\"unknown_source_error\">Невідома помилка джерела</string>\n    <string name=\"unknown_renderer_error\">Невідома помилка рендера</string>\n    <string name=\"header_notifications\">Повідомлення</string>\n    <string name=\"disable_search_history\">Вимкнути історію пошуку</string>\n    <string name=\"unlock_high_bitrate_formats\">Розблокувати 1080p vp9 формати з високим бітрейтом</string>\n    <string name=\"unlock_high_bitrate_audio_formats\">Розблокувати mp4a формати з високим бітрейтом</string>\n    <string name=\"color_scheme_blue\">Блакитна</string>\n    <string name=\"color_scheme_dark_blue\">Темно-блакитна</string>\n    <string name=\"player_speed_per_channel\">Для кожного каналу</string>\n    <string name=\"disable_popular_searches\">Не показувати популяні пошукові запити</string>\n    <string name=\"multi_profiles\">Використовувати окремі налаштування для кожного облікового запису</string>\n    <string name=\"protect_account_with_password\">Захистити цей обліковий запис паролем</string>\n    <string name=\"enter_account_password\">Введіть пароль від облікового запису</string>\n    <string name=\"show_connect_messages\">Сповіщення про підключені пристрої</string>\n    <string name=\"prefer_avc_over_vp9_desc\">ПОПЕРЕДЖЕННЯ: 1080p максімум</string>\n    <string name=\"play_next\">Відтворити наступним</string>\n    <string name=\"hide_watched_from_subscriptions\">Приховати переглянуті відео з Підписок</string>\n    <string name=\"hide_watched_from_notifications\">Приховати переглянуті відео з Повідомлень</string>\n    <string name=\"hide_unwanted_content\">Приховати вміст</string>\n    <string name=\"hide_watched_from_home\">Приховати переглянуті відео з Головного розділу</string>\n    <string name=\"hide_watched_from_watch_later\">Приховати переглянуті відео зі списку Переглянути пізніше</string>\n    <string name=\"remote_control_permission\">Фонове Дистанційне керування вимагає дозволу на Перекривання інших вікон</string>\n    <string name=\"login_from_browser\">Увійти з браузера</string>\n    <string name=\"disable_remote_history\">Вимкнути історію під час дистанційного керування</string>\n    <string name=\"keyboard_fix\">Виправити показ клавіатури натисканням клавіші OK (G20s та інші)</string>\n    <string name=\"nothing_found\">Нічого не знайдено</string>\n    <string name=\"auto_frame_rate_desc\">Ця опція усуває тремтіння на сценах, де камера рухається швидко, напр. спортивна трансляція</string>\n    <string name=\"channel_filter_hint\">Фільтрувати канали</string>\n    <string name=\"player_loop_shorts\">Зациклювати короткі відео</string>\n    <string name=\"player_quick_shorts_skip\">Перемикати короткі відео кнопками ліво/право</string>\n    <string name=\"player_quick_shorts_skip_alt\">Перемикати короткі відео кнопками вгору/вниз</string>\n    <string name=\"player_quick_skip_videos\">Перемикати звичайні відео кнопками ліво/право</string>\n    <string name=\"player_quick_skip_videos_alt\">Перемикати звичайні відео кнопками вгору/вниз</string>\n    <string name=\"channels_filter\">Показати поле Фільтрувати канали в розділі Канали</string>\n    <string name=\"channel_search_bar\">Панель пошуку на сторінці каналу</string>\n    <string name=\"header_sports\">Спорт</string>\n    <string name=\"replace_titles\">Замінити назви</string>\n    <string name=\"enable\">Увімкнути</string>\n    <string name=\"more_info\">Більше інформації</string>\n    <string name=\"about_sponsorblock\">Про SponsorBlock</string>\n    <string name=\"about_dearrow\">Про DeArrow</string>\n    <string name=\"replace_thumbnails\">Замінити мініатюри</string>\n    <string name=\"crowdsourced_thumbnails\">Краудсорсингові мініатюри</string>\n    <string name=\"crowdsoursed_titles\">Краудсорсингові назви</string>\n    <string name=\"dearrow_not_submitted_thumbs\">Звідки брати не надіслані мініатюри</string>\n    <string name=\"pitch_effect\">Ефект висоти тона</string>\n    <string name=\"fullscreen_mode\">Повноекранний режим (без системних панелей)</string>\n    <string name=\"player_only_mode\">Показувати лише плеєр, якщо відео відкрито поза застосунком</string>\n    <string name=\"pinned_channel_rows\">Відображати закріплений канал у вигляді рядків</string>\n    <string name=\"prefer_google_dns\">Надавати перевагу Google DNS</string>\n    <string name=\"prefer_ipv4\">Надавати перевагу IPv4 DNS</string>\n    <string name=\"prefer_ipv4_desc\">Може виправити випадки, коли застосунок взагалі не працює.\\nУвага. На деяких пристроях можливі падіння застосунку (особливо на Android 8 та пристроях Dune HD)</string>\n    <string name=\"long_press_for_settings\">ТРИВАЛЕ НАТИСКАННЯ ДЛЯ НАЛАШТУВАНЬ</string>\n    <string name=\"long_press_for_options\">ТРИВАЛЕ НАТИСКАННЯ ДЛЯ ОПЦІЙ</string>\n    <string name=\"device_specific_backup\">Резервне копіювання лише для цього пристрою</string>\n    <string name=\"local_backup\">Локальне резервне копіювання</string>\n    <string name=\"auto_backup\">Автоматичне резервне копіювання (один раз на день)</string>\n    <string name=\"repeat_mode_reverse_list\">Відтворювати плейлист або відео з каналу у зворотньому порядку</string>\n    <string name=\"calm_msg\">Ми вирішуємо проблему. Час від часу перевіряйте наявність оновлень</string>\n    <string name=\"without_picture\">Без зображення</string>\n    <string name=\"video_disabled\">Відео вимкнено</string>\n    <string name=\"applying_fix\">Не закривайте плеєр. Застосування виправлення…</string>\n    <string name=\"hide_mixes\">Приховувати Мікси</string>\n    <string name=\"player_global_focus_desc\">Ця функція впливає на те, яка кнопка плеєру отримає фокус під час навігації між рядками його кнопок</string>\n    <string name=\"disable_network_error_fixing\">Вимкнути автоматичне виправлення мережевих помилок</string>\n    <string name=\"disable_network_error_fixing_desc\">Можливо, вам потрібно увімкнути цю опцію, якщо ви використовуєте VPN</string>\n    <string name=\"recommended\">Рекомендації</string>\n    <string name=\"add_to_subscriptions_group\">Додати/видалити із групи підписок</string>\n    <string name=\"new_subscriptions_group\">Нова група</string>\n    <string name=\"rename_group\">Перейменувати групу підписок</string>\n    <string name=\"screen_dimming_amount\">Величина затемнення екрана</string>\n    <string name=\"screen_dimming_timeout\">Тайм-аут затемнення екрана</string>\n    <string name=\"playlists_rows\">Відображати розділ Плейлисти у вигляді рядків</string>\n    <string name=\"import_subscriptions_group\">Імпорт</string>\n    <string name=\"my_videos\">Мої відео</string>\n    <string name=\"player_audio_focus\">Аудіо фокус (пауза, якщо виявлено інші плеєри)</string>\n    <string name=\"paid_content_notification\">Повідомлення про платний вміст</string>\n    <string name=\"you_liked\">вам сподобалося</string>\n    <string name=\"card_preview_muted\">Відео без звуку</string>\n    <string name=\"card_preview_full\">Відео зі звуком</string>\n    <string name=\"card_preview\">Попередній перегляд картки</string>\n    <string name=\"card_unlocalized_titles\">Не перекладати назви відео</string>\n    <string name=\"dont_resize_video_to_fit_dialog\">Не змінювати розмір відео відповідно до розміру діалогу</string>\n    <string name=\"typing_corrections\">Вимкнути автокорекцію під час введення тексту</string>\n    <string name=\"suggestions_horizontally_scrolled\">Рекомендації з горизонтальним прокручуванням</string>\n    <string name=\"stable_restore\">Відновити стабільну збірку (всі дані будуть втрачені)</string>\n    <string name=\"auto_backup_category\">Автоматичне резервне копіювання</string>\n    <string name=\"once_a_day\">Один раз на день</string>\n    <string name=\"once_a_week\">Один раз на тиждень</string>\n    <string name=\"once_a_month\">Один раз на місяць</string>\n    <string name=\"action_debug_info\">Налагоджувальна інформація</string>\n    <string name=\"dialog_block_channel\">Заблокувати канал</string>\n    <string name=\"dialog_unblock_channel\">Розблокувати канал</string>\n    <string name=\"confirm_block_channel\">Приховати весь контент від %s?</string>\n    <string name=\"channel_blocked\">Канал заблоковано</string>\n    <string name=\"channel_unblocked\">Канал розблоковано</string>\n    <string name=\"header_blocked_channels\">Заблоковані канали</string>\n    <string name=\"msg_no_blocked_channels\">Немає заблокованих каналів</string>\n    <string name=\"player_time_stretching\">Розтягування аудіо в часі</string>\n    <string name=\"player_time_stretching_desc\">Зберігає природний голос при зміні швидкості. Може суттєво знизити продуктивність на деяких пристроях.</string>\n    <string name=\"queue_respects_playback_mode\">Черга враховує режим відтворення</string>\n    <string name=\"ignore_short_segments\">Ігнорувати короткі сегменти</string>\n    <string name=\"local_backup_not_supported\">Автобекап на Android TV 11+ не підтримується через обмеження системи. Використовуйте GDrive.</string>\n    <plurals name=\"hours\">\n        <item quantity=\"one\">%s година</item>\n        <item quantity=\"few\">%s години</item>\n        <item quantity=\"many\">%s годин</item>\n        <item quantity=\"other\">%s години</item>\n    </plurals>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%s секунда</item>\n        <item quantity=\"few\">%s секунди</item>\n        <item quantity=\"many\">%s секунд</item>\n        <item quantity=\"other\">%s секунди</item>\n    </plurals>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-vi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">Trang chủ</string>\n  <string name=\"title_search\">Tìm kiếm</string>\n  <string name=\"header_subscriptions\">Kênh đăng ký</string>\n  <string name=\"header_history\">Lịch sử</string>\n  <string name=\"header_music\">Âm nhạc</string>\n  <string name=\"header_news\">Tin tức</string>\n  <string name=\"header_gaming\">Trò chơi</string>\n  <string name=\"header_playlists\">Danh sách phát</string>\n  <string name=\"header_settings\">Cài đặt</string>\n  <string name=\"header_channels\">Kênh</string>\n  <string name=\"subscriptions_signin_title\">Xem video mới nhất từ các kênh bạn yêu thích</string>\n  <string name=\"subscriptions_signin_subtitle\">Đăng nhập để xem các kênh đăng ký của bạn</string>\n  <string name=\"subscriptions_signin_button_text\">ĐĂNG NHẬP</string>\n  <string name=\"library_signin_to_show_more\">Xem video bạn đã thích, đã lưu hoặc đã đăng ký</string>\n  <string name=\"library_signin_subtitle\">Đăng nhập để xem thư viện của bạn</string>\n  <string name=\"action_signin\">ĐĂNG NHẬP</string>\n  <string name=\"title_video_formats\">Định dạng video</string>\n  <string name=\"title_audio_formats\">Định dạng âm thanh</string>\n  <string name=\"subtitle_category_title\">Phụ đề</string>\n  <string name=\"video_max_quality\">Tự động (chất lượng cao nhất)</string>\n  <string name=\"audio_max_quality\">Tự động (chất lượng cao nhất)</string>\n  <string name=\"subtitles_disabled\">Tắt phụ đề</string>\n  <string name=\"auto_frame_rate\">Tốc độ khung hình tự động</string>\n  <string name=\"frame_rate_correction\">Cố định fps:\\n%s</string>\n  <string name=\"category_background_playback\">Phát lại trong nền</string>\n  <string name=\"not_implemented\">Không thực hiện được</string>\n  <string name=\"option_background_playback_off\">Tắt</string>\n  <string name=\"option_background_playback_pip\">Hình trong hình</string>\n  <string name=\"option_background_playback_only_audio\">Chỉ âm thanh</string>\n  <string name=\"playback_settings\">Cài đặt trình phát</string>\n  <string name=\"video_speed\">Tốc độ video</string>\n  <string name=\"resolution_switch\">Chuyển đổi độ phân giải</string>\n  <string name=\"video_buffer\">Bộ đệm video</string>\n  <string name=\"video_buffer_size_low\">Thấp</string>\n  <string name=\"video_buffer_size_med\">Trung bình</string>\n  <string name=\"video_buffer_size_high\">Cao</string>\n  <string name=\"update_changelog\">Nhật ký thay đổi</string>\n  <string name=\"install_update\">Cài đặt bản cập nhật</string>\n  <string name=\"section_is_empty\">Xin lỗi! Không có gì ở đây cả.</string>\n  <string name=\"dialog_add_to_playlist\">Thêm vào/Xóa khỏi danh sách phát</string>\n  <string name=\"msg_signed_users_only\">Chỉ người dùng đã đăng nhập</string>\n  <string name=\"msg_cant_load_content\">Không thể tải nội dung.\\n1) Bật tuỳ chọn lưu lịch sử xem.\\n2) Kiểm tra ngày giờ trên thiết bị của bạn.\\n3) Kiểm tra kết nối mạng.\\n4) Kiểm tra cài đặt proxy.\\n5) Thử đăng nhập vào tài khoản của bạn.\\n6) Có lẽ là không có gì ở đây cả.</string>\n  <string name=\"title_video_presets\">Thiết lập trước cho video</string>\n  <string name=\"settings_accounts\">Tài khoản</string>\n  <string name=\"settings_left_panel\">Chỉnh sửa danh mục</string>\n  <string name=\"settings_themes\">Chủ đề</string>\n  <string name=\"settings_other\">Khác</string>\n  <string name=\"settings_player\">Trình phát video</string>\n  <string name=\"settings_language\">Ngôn ngữ</string>\n  <string name=\"settings_linked_devices\">Các thiết bị được liên kết</string>\n  <string name=\"settings_about\">Giới thiệu</string>\n  <string name=\"dialog_account_list\">Chọn tài khoản</string>\n  <string name=\"dialog_account_none\">Không có</string>\n  <string name=\"dialog_remove_account\">Đăng xuất</string>\n  <string name=\"dialog_add_account\">Đăng nhập</string>\n  <string name=\"default_lang\">Mặc định</string>\n  <string name=\"dialog_select_language\">Ngôn ngữ</string>\n  <string name=\"subtitle_default\">Mặc định</string>\n  <string name=\"subtitle_white_semi_transparent\">Chữ trắng trên nền bán trong suốt</string>\n  <string name=\"subtitle_style\">Kiểu phụ đề</string>\n  <string name=\"subtitle_language\">Ngôn ngữ phụ đề</string>\n  <string name=\"action_search\">Bắt đầu tìm kiếm</string>\n  <string name=\"settings_main_ui\">Giao diện người dùng</string>\n  <string name=\"dialog_main_ui\">Giao diện người dùng</string>\n  <string name=\"card_animated_previews\">Hình động xem trước</string>\n  <string name=\"web_site\">Trang web</string>\n  <string name=\"donation\">Ủng hộ</string>\n  <string name=\"dialog_about\">Giới thiệu</string>\n  <string name=\"dialog_player_ui\">Trình phát video</string>\n  <string name=\"player_show_ui_on_pause\">Hiện giao diện khi tạm dừng</string>\n  <string name=\"player_pause_on_ok\">Nhấn OK để tạm dừng phát lại</string>\n  <string name=\"player_ok_button_behavior\">Chức năng phím OK</string>\n  <string name=\"player_only_ui\">Chỉ mở giao diện</string>\n  <string name=\"player_ui_and_pause\">Mở giao diện và tạm dừng</string>\n  <string name=\"player_only_pause\">Chỉ tạm dừng</string>\n  <string name=\"check_for_updates\">Kiểm tra bản cập nhật</string>\n  <string name=\"update_not_found\">Bạn đang sử dụng phiên bản mới nhất</string>\n  <string name=\"update_in_progress\">Vui lòng đợi...</string>\n  <string name=\"player_ui_hide_behavior\">Tự động ẩn giao diện</string>\n  <string name=\"option_never\">Không bao giờ</string>\n  <string name=\"side_panel_sections\">Cài đặt bảng điều khiển cạnh</string>\n  <string name=\"boot_to_section\">Mặc định khi khởi động</string>\n  <string name=\"large_ui\">Giao diện lớn</string>\n  <string name=\"video_grid_scale\">Tỷ lệ lưới video</string>\n  <string name=\"scale_ui\">Tỷ lệ giao diện</string>\n  <string name=\"color_scheme\">Màu giao diện</string>\n  <string name=\"color_scheme_default\">Mặc định</string>\n  <string name=\"color_scheme_red_grey\">Đỏ xám</string>\n  <string name=\"color_scheme_red\">Đỏ</string>\n  <string name=\"color_scheme_dark_grey\">Đen tối</string>\n  <string name=\"disable_update_check\">Vô hiệu hóa kiểm tra cập nhật</string>\n  <string name=\"show_again\">Hiển thị lại</string>\n  <string name=\"check_updates_auto\">Thông báo về phiên bản mới</string>\n  <string name=\"select_account_on_boot\">Chọn tài khoản khi khởi động</string>\n  <string name=\"player_other\">Cài đặt khác</string>\n  <string name=\"player_full_date\">Ngày chính xác trong mô tả</string>\n  <string name=\"open_channel\">Mở kênh</string>\n  <string name=\"not_interested\">Không quan tâm</string>\n  <string name=\"you_wont_see_this_video\">Đã xoá video khỏi mục đề xuất</string>\n  <string name=\"settings_search\">Tìm kiếm</string>\n  <string name=\"dialog_search\">Tìm kiếm</string>\n  <string name=\"instant_voice_search\">Tìm kiếm bằng giọng nói tức thì</string>\n  <string name=\"option_background_playback_behind\">Chạy nền phía sau</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">Thông tin video tiếp theo chưa được tải</string>\n  <string name=\"card_multiline_title\">Tiêu đề nhiều dòng</string>\n  <string name=\"cards_style\">Kiểu thẻ</string>\n  <string name=\"repeat_mode_all\">Phát các video liên tục</string>\n  <string name=\"repeat_mode_one\">Lặp lại video đang phát</string>\n  <string name=\"repeat_mode_pause\">Tạm dừng phát lại sau mỗi video</string>\n  <string name=\"repeat_mode_pause_alt\">Chỉ phát liên tục các video trong danh sách phát</string>\n  <string name=\"repeat_mode_none\">Dừng phát lại sau một video</string>\n  <string name=\"subscribed_to_channel\">Huỷ đăng ký kênh</string>\n  <string name=\"unsubscribed_from_channel\">Đăng ký kênh</string>\n  <string name=\"subtitle_yellow_transparent\">Chữ vàng trên nền trong suốt</string>\n  <string name=\"subtitle_white_black\">Chữ trắng trên nền đen</string>\n  <string name=\"player_seek_preview\">Xem trước trong khi tua</string>\n  <string name=\"color_scheme_dark_grey_oled\">Xám tối (OLED)</string>\n  <string name=\"channels_section_sorting\">Sắp xếp kênh</string>\n  <string name=\"sorting_by_new_content\">Nội dung mới</string>\n  <string name=\"sorting_alphabetically\">Theo bảng chữ cái</string>\n  <string name=\"sorting_last_viewed\">Xem lần cuối</string>\n  <string name=\"player_pause_when_seek\">Tạm dừng khi tua</string>\n  <string name=\"playlists_style\">Kiểu danh sách phát</string>\n  <string name=\"playlists_style_grid\">Lưới</string>\n  <string name=\"playlists_style_rows\">Hàng</string>\n  <string name=\"player_show_clock\">Hiển thị đồng hồ ở thanh điều khiển</string>\n  <string name=\"player_show_remaining_time\">Hiển thị thời gian còn lại ở thanh điều khiển</string>\n  <string name=\"open_channel_uploads\">Mở video đã tải lên của kênh</string>\n  <string name=\"app_exit_shortcut\">Thoát khỏi ứng dụng</string>\n  <string name=\"app_exit_none\">Không thoát</string>\n  <string name=\"app_double_back_exit\">Nhấn BACK 2 lần để</string>\n  <string name=\"app_single_back_exit\">Nhấn BACK 1 lần để</string>\n  <string name=\"action_video_zoom\">Thu phóng video</string>\n  <string name=\"video_zoom\">Thu phóng video</string>\n  <string name=\"video_zoom_default\">Bình thường</string>\n  <string name=\"video_zoom_fit_width\">Vừa chiều rộng</string>\n  <string name=\"video_zoom_fit_height\">Vừa chiều cao</string>\n  <string name=\"video_zoom_fit_both\">Vừa với chiều rộng hoặc chiều cao</string>\n  <string name=\"video_zoom_stretch\">Kéo dãn ra</string>\n  <string name=\"color_scheme_teal\">Mòng két</string>\n  <string name=\"color_scheme_teal_oled\">Mòng két (OLED)</string>\n  <string name=\"player_seek_preview_none\">Vô hiệu hóa</string>\n  <string name=\"player_seek_preview_single\">Khung đơn</string>\n  <string name=\"player_seek_preview_carousel\">Băng chuyền</string>\n  <string name=\"player_seek_preview_carousel_slow\">Băng chuyền (chậm, chính xác đến từng khung hình)</string>\n  <string name=\"player_seek_preview_carousel_fast\">Băng chuyền (nhanh, nhưng không quá chính xác)</string>\n  <string name=\"unsubscribe_from_channel\">Hủy đăng ký kênh</string>\n  <string name=\"card_title_lines_num\">Số dòng tiêu đề thẻ</string>\n  <string name=\"card_auto_scrolled_title\">Tự động cuộn tiêu đề đã cắt</string>\n  <string name=\"share_link\">Chia sẻ liên kết</string>\n  <string name=\"subscribe_to_channel\">Đăng ký kênh</string>\n  <string name=\"wait_data_loading\">Vui lòng đợi trong khi dữ liệu đang tải…</string>\n  <string name=\"auto_frame_rate_pause\">Tốc độ khung hình tự động (tạm dừng khi chuyển đổi)</string>\n  <string name=\"auto_frame_rate_applying\">Áp dụng tốc độ khung hình tự động %sx%s\\@%s</string>\n  <string name=\"audio_shift\">Dịch chuyển âm thanh</string>\n  <string name=\"player_remember_speed\">Ghi nhớ tốc độ</string>\n  <string name=\"mark_channel_as_watched\">Đánh dấu là đã xem</string>\n  <string name=\"channel_marked_as_watched\">Kênh được đánh dấu là đã xem</string>\n  <string name=\"dialog_add_device\">Thêm thiết bị</string>\n  <string name=\"dialog_remove_all_devices\">Gỡ bỏ thiết bị</string>\n  <string name=\"device_link_enabled\">Liên kết đã được kích hoạt</string>\n  <string name=\"device_connected\">Thiết bị \\\"%s\\\" đã được kết nối</string>\n  <string name=\"device_disconnected\">Thiết bị \\\"%s\\\" đã ngắt kết nối</string>\n  <string name=\"settings_ui_scale\">Tỷ lệ giao diện</string>\n  <string name=\"msg_restart_app\">Hãy khởi động lại ứng dụng để thiết lập cài đặt</string>\n  <string name=\"settings_block\">Chặn nội dung</string>\n  <string name=\"msg_applying\">Đang thiết lập %s</string>\n  <string name=\"msg_done\">Hoàn thành</string>\n  <string name=\"settings_general\">Chung</string>\n  <string name=\"settings_video\">Video</string>\n  <string name=\"content_block_confirm_skip\">Xác nhận bỏ qua</string>\n  <string name=\"content_block_categories\">Bỏ qua các phân đoạn</string>\n  <string name=\"content_block_sponsor\">Nhà tài trợ</string>\n  <string name=\"content_block_intro\">Hoạt ảnh giới thiệu/ngắt quãng</string>\n  <string name=\"content_block_outro\">Đoạn cuối/Đoạn credit</string>\n  <string name=\"content_block_interaction\">Lời nhắc tương tác (đăng ký)</string>\n  <string name=\"content_block_self_promo\">Quảng cáo bản thân/không được trả tiền</string>\n  <string name=\"content_block_music_off_topic\">Phần không phải âm nhạc của clip</string>\n  <string name=\"confirm_segment_skip\">Bỏ qua phân đoạn \\\"%s\\\"\\?</string>\n  <string name=\"msg_skipping_segment\">Đang bỏ qua phân đoạn \\\"%s\\\"…</string>\n  <string name=\"content_block_notification_type\">Loại thông báo</string>\n  <string name=\"content_block_notify_none\">Không có thông báo</string>\n  <string name=\"content_block_notify_toast\">Thông báo nhanh</string>\n  <string name=\"content_block_notify_dialog\">Hộp thoại xác nhận</string>\n  <string name=\"return_to_launcher\">Trở lại launcher từ kênh ATV/tìm kiếm</string>\n  <string name=\"intent_force_close\">Buộc thoát nếu video đã được mở từ nguồn bên ngoài</string>\n  <string name=\"btn_confirm\">Xác nhận</string>\n  <string name=\"player_low_video_quality\">Chất lượng video thấp</string>\n  <string name=\"remote_session_closed\">Phiên từ xa đã bị đóng</string>\n  <string name=\"msg_mode_switch_error\">Không thể chuyển chế độ hiển thị sang \\\"%s\\\"</string>\n  <string name=\"msg_player_error\">Đã xảy ra lỗi trình phát %s. Bắt đầu phát lại…</string>\n  <string name=\"video_preset_disabled\">Không có cài đặt trước</string>\n  <string name=\"video_preset_enabled\">Định dạng của video tiếp theo sẽ được thiết lập theo cài đặt trước</string>\n  <string name=\"player_sleep_timer\">Hẹn giờ ngủ</string>\n  <string name=\"player_show_quality_info\">Hiển thị thông tin chất lượng video ở thanh điều khiển</string>\n  <string name=\"player_remember_each_speed\">Ghi nhớ tốc độ từng video</string>\n  <string name=\"player_remember_speed_none\">Không</string>\n  <string name=\"player_remember_speed_all\">Tương tự trên tất cả các video</string>\n  <string name=\"player_remember_speed_each\">Trên mỗi video</string>\n  <string name=\"msg_player_error_source\">Không thể tải xuống video</string>\n  <string name=\"msg_player_error_renderer\">Định dạng video đã chọn không được hỗ trợ.\\nHãy thử chọn một định dạng khác.\\nNếu vẫn không được thì hãy thử khởi động lại thiết bị của bạn.</string>\n  <string name=\"msg_player_error_unexpected\">Lỗi không xác định từ bộ giải mã video</string>\n  <string name=\"video_aspect\">Tỷ lệ khung hình</string>\n  <string name=\"player_tweaks\">Tùy chọn nhà phát triển</string>\n  <string name=\"header_uploads\">Tải lên</string>\n  <string name=\"player_show_global_clock\">Luôn hiển thị đồng hồ trên màn hình</string>\n  <string name=\"uploads_old_look\">Khôi phục giao diện cũ của mục Tải lên</string>\n  <string name=\"background_playback_activation\">Phát lại trong nền (kích hoạt)</string>\n  <string name=\"channels_old_look\">Khôi phục giao diện cũ của mục Kênh</string>\n  <string name=\"channels_auto_load\">Tự động tải nội dung từ mục Kênh</string>\n  <string name=\"return_to_background_video\">Quay lại video đang chạy trong nền</string>\n  <string name=\"pinned_to_sidebar\">Đã ghim vào thanh bên</string>\n  <string name=\"unpinned_from_sidebar\">Đã bỏ ghim khỏi thanh bên</string>\n  <string name=\"player_show_global_ending_time\">Luôn hiển thị thời gian kết thúc trên màn hình</string>\n  <string name=\"pin_unpin_from_sidebar\">Ghim/Bỏ ghim khỏi thanh bên</string>\n  <string name=\"unpin_from_sidebar\">Bỏ ghim khỏi thanh bên</string>\n  <string name=\"dialog_select_country\">Quốc gia</string>\n  <string name=\"settings_language_country\">Ngôn ngữ/Quốc gia</string>\n  <string name=\"share_embed_link\">Chia sẻ liên kết nhúng</string>\n  <string name=\"card_text_scroll_factor\">Tốc độ cuộn thẻ</string>\n  <string name=\"preferred_update_source\">Chọn nguồn cập nhật</string>\n  <string name=\"hide_shorts\">Ẩn Shorts khỏi Kênh đăng ký</string>\n  <string name=\"key_remapping\">Thay đổi nút</string>\n  <string name=\"screen_dimming\">Làm mờ màn hình</string>\n  <string name=\"removed_from_playback_queue\">Đã xóa khỏi hàng chờ phát lại</string>\n  <string name=\"added_to_playback_queue\">Đã thêm vào hàng chờ phát lại</string>\n  <string name=\"add_remove_from_playback_queue\">Thêm vào/Xóa khỏi hàng chờ phát lại</string>\n  <string name=\"proxy_enabled\">Đã bật proxy</string>\n  <string name=\"proxy_disabled\">Đã tắt proxy</string>\n  <string name=\"uploads_row_name\">Tải lên</string>\n  <string name=\"playlists_row_name\">Danh sách phát đã tạo</string>\n  <string name=\"popular_uploads_row_name\">Tải lên phổ biến</string>\n  <string name=\"breaking_news_row_name\">Tin nóng</string>\n  <string name=\"covid_news_row_name\">Tin tức về COVID-19</string>\n  <string name=\"enable_voice_search\">Bật tìm kiếm bằng giọng nói (phần cứng và hệ điều hành của thiết bị cần hỗ trợ tính năng này)</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">Đăng ký/Hủy đăng ký kênh</string>\n  <string name=\"app_backup_restore\">Sao lưu/Phục hồi</string>\n  <string name=\"app_backup\">Sao lưu dữ liệu</string>\n  <string name=\"app_restore\">Phục hồi từ dữ liệu sao lưu</string>\n  <string name=\"skip_each_segment_once\">Không bỏ qua các phân đoạn</string>\n  <string name=\"disable_ok_long_press\">Vô hiệu hoá nhấn giữ phím OK</string>\n  <string name=\"player_time_correction\">Hiển thị thời gian theo tốc độ</string>\n  <string name=\"sponsor_color_markers\">Dánh dấu màu trên thanh tiến trình</string>\n  <string name=\"refresh_section\">Làm mới mục này</string>\n  <string name=\"option_disabled\">Vô hiệu hoá</string>\n  <string name=\"playback_queue_category_title\">Hàng chờ phát lại</string>\n  <string name=\"open_playlist\">Mở danh sách phát</string>\n  <string name=\"player_show_ending_time\">Hiển thị thời gian kết thúc ở thanh điều khiển</string>\n  <string name=\"live_now_row_name\">Trực tiếp bây giờ</string>\n  <string name=\"run_in_background\">Chạy trong nền</string>\n  <string name=\"settings_remote_control\">Điều khiển từ xa</string>\n  <string name=\"background_service_started\">Dịch vụ nền đã được bắt đầu</string>\n  <string name=\"dialog_remove_from\">Xóa khỏi %s</string>\n  <string name=\"dialog_add_to\">Thêm vào %s</string>\n  <string name=\"added_to\">Đã thêm vào %s</string>\n  <string name=\"removed_from\">Đã xóa khỏi %s</string>\n  <string name=\"dialog_add_remove_from\">Thêm vào/Xóa khỏi %s</string>\n  <string name=\"enter_value\">Nhập một giá trị bất kỳ (không được để trống)</string>\n  <string name=\"cant_save_playlist\">Không thể lưu danh sách phát này</string>\n  <string name=\"cant_rename_foreign_playlist\">Không thể đổi tên danh sách phát của người khác</string>\n  <string name=\"cant_rename_empty_playlist\">Không thể đổi tên danh sách phát rỗng</string>\n  <string name=\"rename_playlist\">Đổi tên danh sách phát</string>\n  <string name=\"playlist\">Danh sách phát</string>\n  <string name=\"cant_delete_empty_playlist\">Không thể xóa danh sách phát rỗng</string>\n  <string name=\"add_video_to_new_playlist\">Thêm video này vào danh sách phát mới</string>\n  <string name=\"create_playlist\">Tạo danh sách phát</string>\n  <string name=\"saved_to_playlists\">Đã lưu vào mục Danh sách phát</string>\n  <string name=\"removed_from_playlists\">Đã xóa từ mục Danh sách phát</string>\n  <string name=\"pressing_home\">bằng cách nhấn nút HOME</string>\n  <string name=\"pressing_home_back\">bằng cách nhấn nút HOME hoặc nút BACK</string>\n  <string name=\"player_number_key_seek\">Tua video bằng các phím số</string>\n  <string name=\"save_remove_playlist\">Lưu/xóa danh sách phát này khỏi mục Danh sách phát</string>\n  <string name=\"remove_playlist\">Xóa danh sách phát này khỏi mục Danh sách phát</string>\n  <string name=\"subtitle_position\">Phụ đề di chuyển dưới cùng</string>\n  <string name=\"not_compatible_with\">Tùy chọn đã chọn không tương thích với</string>\n  <string name=\"master_volume\">Âm lượng tổng thể</string>\n  <string name=\"volume_limit\">Giới hạn âm lượng</string>\n  <string name=\"play_video\">Phát</string>\n  <string name=\"remember_position_of_short_videos\">Nhớ vị trí của các video Shorts (dưới 5 phút)</string>\n  <string name=\"player_show_tooltips\">Hiển thị chú thích của các nút</string>\n  <string name=\"action_like_unset\">Đã xoá lượt thích</string>\n  <string name=\"amlogic_fix_desc\">Sửa lỗi giảm khung hình trên các thiết bị có vi xử lí Amlogic.</string>\n  <string name=\"amlogic_fix\">Sửa lỗi 1080p\\@60fps trên thiết bị Amlogic</string>\n  <string name=\"playback_notifications_fix\">Tắt thông báo phát lại</string>\n  <string name=\"playback_notifications_fix_desc\">Ẩn thông báo thay đổi bộ phát. Có thể hữu ích cho các phiên bản hệ điều hành dựa trên AOSP.</string>\n  <string name=\"live_stream_fix\">Sửa lỗi phát video trực tiếp (1080p)</string>\n  <string name=\"audio_sync_fix\">Sửa đồng bộ âm thanh</string>\n  <string name=\"force_legacy_codecs_desc\">Cải thiện đáng kể hiệu suất trên các thiết bị cấu hình thấp. Độ phân giải tối đa là 720p.</string>\n  <string name=\"live_stream_fix_desc\">Vô hiệu hoá việc tua lùi video phát trực tiếp. Cải thiện đáng kể hiệu suất trên các thiết bị cấu hình thấp. Độ phân giải tối đa là 1080p.</string>\n  <string name=\"simple_edit_value_hint\">Giá trị mới</string>\n  <string name=\"rename_section\">Đổi tên mục</string>\n  <string name=\"internet_censorship\">Kiểm Duyệt Mạng</string>\n  <string name=\"openvpn_address_invalid\">Địa chỉ cấu hình OpenVPN chưa được thiết lập.</string>\n  <string name=\"action_dislike_unset\">Đã xoá lượt không thích</string>\n  <string name=\"cancel_dialog\">Hủy bỏ</string>\n  <string name=\"ambilight_ratio_fix_desc\">Sửa lỗi mất độ nghiêng ánh sáng. Sửa tỷ lệ khung hình không chính xác. Sửa ảnh chụp màn hình trống. Nhưng nó sẽ ảnh hưởng đến hiệu suất!</string>\n  <string name=\"ambilight_ratio_fix\">Sửa hệ thống chiếu sáng/tỉ lệ khung hình/ảnh chụp màn hình</string>\n  <string name=\"force_legacy_codecs\">Buộc dùng bộ giải mã cũ (720p)</string>\n  <string name=\"openvpn_test_aborted\">Kiểm tra bị huỷ bỏ, vui lòng sửa lại cài đặt OpenVPN trước.</string>\n  <string name=\"openvpn_application_aborted\">Vui lòng sửa lại cài đặt OpenVPN trước.</string>\n  <string name=\"openvpn_settings_title\">Cài đặt OpenVPN</string>\n  <string name=\"enable_openvpn\">Sử dụng OpenVPN</string>\n  <string name=\"openvpn_config_address_hint\">Địa chỉ tệp cấu hình (*.ovpn)</string>\n  <string name=\"proxy_test_start\">Kiểm tra #%d: %s …</string>\n  <string name=\"proxy_test_error\">Lỗi #%d: %s</string>\n  <string name=\"proxy_test_status\">Trạng thái #%d: %s %d %s</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">Kiểm tra#%d: đã bị hủy bỏ.</string>\n  <string name=\"keyboard_auto_show\">Hiển thị bàn phím tự động</string>\n  <string name=\"msg_player_error_source2\">Liên kết không hoạt động hoặc thời gian của thiết bị không chính xác.\\nThông thường thì đây là lỗi từ ứng dụng.</string>\n  <string name=\"hide_upcoming\">Ẩn mục \\\"sắp tới\\\" khỏi Kênh đăng ký</string>\n  <string name=\"trending_row_name\">Thịnh hành</string>\n  <string name=\"player_seek_regular\">Bình thường</string>\n  <string name=\"hide_shorts_from_home\">Ẩn Shorts từ Trang chủ</string>\n  <string name=\"update_error\">Lỗi cập nhật</string>\n  <string name=\"hide_shorts_from_history\">Ẩn Shorts từ Lịch sử</string>\n  <string name=\"disable_screensaver\">Tắt màn hình chờ</string>\n  <string name=\"description_not_found\">Không tìm thấy mô tả</string>\n  <string name=\"finish_on_disconnect\">Đóng ứng dụng sau khi ngắt kết nối điện thoại/máy tính bảng</string>\n  <string name=\"search_background_playback\">Phát trong nền khi đang tìm kiếm/xem qua một kênh</string>\n  <string name=\"action_pip\">Hình trong Hình</string>\n  <string name=\"action_screen_off\">Tắt màn hình</string>\n  <string name=\"action_playback_queue\">Hàng chờ phát lại</string>\n  <string name=\"action_video_speed\">Tốc độ video</string>\n  <string name=\"action_subtitles\">Phụ đề</string>\n  <string name=\"action_like\">Thích</string>\n  <string name=\"action_dislike\">Không thích</string>\n  <string name=\"action_play_pause\">Phát/Tạm dừng</string>\n  <string name=\"action_repeat_mode\">Chế độ phát lại</string>\n  <string name=\"action_next\">Video tiếp theo</string>\n  <string name=\"action_previous\">Video trước</string>\n  <string name=\"action_high_quality\">Chất lượng video</string>\n  <string name=\"content_block_no_skipping_mode\">Chế độ không bỏ qua</string>\n  <string name=\"action_video_info\">Mô tả video</string>\n  <string name=\"action_channel\">Mở kênh</string>\n  <string name=\"action_subscribe\">Đăng ký</string>\n  <string name=\"action_video_stats\">Thống kê video</string>\n  <string name=\"action_playlist_add\">Thêm vào danh sách phát</string>\n  <string name=\"player_buttons\">Thiết lập các nút cho trình phát</string>\n  <string name=\"auto_frame_rate_sec\">%s giây</string>\n  <string name=\"audio_shift_sec\">%s giây</string>\n  <string name=\"ui_hide_timeout_sec\">%s giây</string>\n  <string name=\"screen_dimming_timeout_min\">%s phút</string>\n  <string name=\"volume\">Âm lượng %s%%</string>\n  <string name=\"hide_settings_section\">Ẩn phần Cài đặt (Nguy hiểm!)</string>\n  <string name=\"context_menu\">Danh mục</string>\n  <string name=\"add_remove_from_recent_playlist\">Thêm vào/xóa khỏi danh sách phát gần đây</string>\n  <string name=\"move_section_up\">Di chuyển mục lên trên</string>\n  <string name=\"move_section_down\">Di chuyển mục xuống dưới</string>\n  <string name=\"mark_all_channels_watched\">Đánh dấu tất cả các kênh là đã xem</string>\n  <string name=\"video_preset_adaptive\">Linh hoạt</string>\n  <string name=\"content_block_preview_recap\">Xem trước hoặc tóm tắt lại video</string>\n  <string name=\"content_block_highlight\">(Đánh dấu) điểm nổi bật của video</string>\n  <string name=\"removed_from_history\">Video đã bị xóa khỏi lịch sử</string>\n  <string name=\"remove_from_history\">Xóa khỏi lịch sử</string>\n  <string name=\"upload_date\">Ngày tải lên</string>\n  <string name=\"upload_date_any\">Toàn thời gian</string>\n  <string name=\"upload_date_today\">Hôm nay</string>\n  <string name=\"upload_date_this_week\">Tuần này</string>\n  <string name=\"upload_date_this_month\">Tháng này</string>\n  <string name=\"upload_date_this_year\">Năm này</string>\n  <string name=\"double_refresh_rate\">Tốc độ làm mới gấp đôi</string>\n  <string name=\"upload_date_last_hour\">Gần đây</string>\n  <string name=\"focus_on_search_results\">Tự động chọn vào kết quả tìm kiếm</string>\n  <string name=\"content_block_action_none\">Không làm gì</string>\n  <string name=\"content_block_action_only_skip\">Chỉ bỏ qua</string>\n  <string name=\"content_block_action_toast\">Bỏ qua với thông báo</string>\n  <string name=\"content_block_action_dialog\">Hiển thị hộp thoại xác nhận</string>\n  <string name=\"subtitle_white_transparent\">Chữ trắng trên nền trong suốt</string>\n  <string name=\"various_buttons\">Các nút ở phía trên của cửa sổ chính</string>\n  <string name=\"content_block_action_type\">Chọn hành động</string>\n  <string name=\"player_seek_confirmation_pause\">Xác nhận (tạm dừng khi đang tua)</string>\n  <string name=\"player_seek_confirmation_play\">Xác nhận (phát video khi đang tua)</string>\n  <string name=\"force_sw_codec_desc\">Có thể phát hầu hết mọi video nhưng hiệu suất rất kém.</string>\n  <string name=\"skip_codec_profile_check_desc\">Không kiểm tra việc hỗ trợ giải mã khi bắt đầu video. Có thể hữu ích trên các phiên bản hệ điều hành không ổn định.</string>\n  <string name=\"force_sw_codec\">Buộc dùng bộ giải mã video bằng phần mềm</string>\n  <string name=\"lb_playback_controls_skip_next\">Chuyển sang phần tiếp theo</string>\n  <string name=\"seek_interval_sec\">%s giây</string>\n  <string name=\"subtitle_scale\">Tỷ lệ phụ đề</string>\n  <string name=\"alt_presets_behavior\">Chức năng cài đặt thay thế (giới hạn băng thông)</string>\n  <string name=\"alt_presets_behavior_desc\">Ứng dụng sẽ cố gắng duy trì băng thông tương ứng với cài đặt có sẵn đã chọn thay vì so khớp giữa độ phân giải, tốc độ khung hình và trình giải mã.</string>\n  <string name=\"disable_vsync_desc\">Tùy chọn này vô hiệu hóa việc đồng bộ khung hình với tín hiệu dọc của màn hình. Điều này có thể cải thiện hiệu suất trên các thiết bị cấu hình thấp bằng cách giải phóng tài nguyên CPU.</string>\n  <string name=\"sleep_timer_desc\">Tạm dừng phát lại nếu người dùng không sử dụng điều khiển từ xa trong một giờ</string>\n  <string name=\"sleep_timer\">Bật hẹn giờ ngủ (nếu điều khiển không được sử dụng trong một giờ)</string>\n  <string name=\"audio_sync_fix_desc\">Một cách khác để đồng bộ hóa âm thanh/video. Nó đã lỗi thời nhưng vẫn có thể hữu ích trong một số trường hợp.</string>\n  <string name=\"seek_interval\">Khoảng thời gian tua</string>\n  <string name=\"cancel_segment_skip\">Hủy bỏ qua phân đoạn</string>\n  <string name=\"pin_unpin_playlist\">Ghim/Bỏ ghim danh sách phát này từ thanh bên</string>\n  <string name=\"pin_unpin_channel\">Ghim/Bỏ ghim kênh này từ thanh bên</string>\n  <string name=\"player_seek_type\">Chức năng khi tua</string>\n  <string name=\"proxy_port_hint\">Cổng proxy</string>\n  <string name=\"proxy_username_hint\">Tên người dùng proxy</string>\n  <string name=\"proxy_password_hint\">Mật khẩu proxy</string>\n  <string name=\"proxy_type\">Loại proxy</string>\n  <string name=\"proxy_test_btn\">Kiểm tra</string>\n  <string name=\"enable_web_proxy\">Sử dụng web proxy</string>\n  <string name=\"proxy_not_supported\">Sử dụng web proxy (Cần Android 4.4+)</string>\n  <string name=\"proxy_settings_title\">Cài đặt máy chủ proxy</string>\n  <string name=\"proxy_host_hint\">Tên máy chủ proxy hoặc địa chỉ IP</string>\n  <string name=\"proxy_port_invalid\">Cổng proxy không hợp lệ, phải &gt; 0.</string>\n  <string name=\"proxy_credentials_invalid\">Tên đăng nhập hoặc mật khẩu không hợp lệ.</string>\n  <string name=\"proxy_test_aborted\">Kiểm tra bị hủy bỏ, vui lòng sửa lại cài đặt proxy trước.</string>\n  <string name=\"proxy_type_invalid\">Kiểu proxy không được thiết lập.</string>\n  <string name=\"proxy_host_invalid\">Máy chủ proxy không được thiết lập.</string>\n  <string name=\"skip_codec_profile_check\">Bỏ kiểm tra cấp cấu hình của mã hóa</string>\n  <string name=\"tunneled_video_playback\">Phát video theo kiểu đường hầm (Android 5+)</string>\n  <string name=\"tunneled_video_playback_desc\">LƯU Ý: Tính năng tạm dừng video có thể không hoạt động đúng cách! Kiểu phát video đường hầm sẽ có một số lợi ích như đồng bộ hoá âm thanh/hình ảnh tốt hơn, và phát video mượt mà hơn. Yêu cầu Android 5+</string>\n  <string name=\"subtitle_system\">Kiểu hệ thống (Cài đặt Android &gt; Trợ năng)</string>\n  <string name=\"lb_playback_controls_skip_previous\">Chuyển sang phần trước/Tua lại về vị trí bắt đầu</string>\n  <string name=\"proxy_application_aborted\">Vui lòng sửa lại cài đặt proxy trước.</string>\n  <string name=\"content_block_filler\">Chủ đề ngoài lề</string>\n  <string name=\"disable_vsync\">Tắt V-Sync</string>\n  <string name=\"add_to_playback_queue\">Thêm vào hàng chờ phát lại</string>\n  <string name=\"remove_from_playback_queue\">Xoá khỏi hàng chờ phát lại</string>\n  <string name=\"playlist_order\">Sắp xếp danh sách phát</string>\n  <string name=\"playlist_order_added_date_newer_first\">Ngày thêm danh sách phát (Mới nhất trước)</string>\n  <string name=\"playlist_order_added_date_older_first\">Ngày thêm danh sách phát (Cũ nhất trước)</string>\n  <string name=\"playlist_order_published_date_newer_first\">Ngày đã xuất bản (Mới nhất trước)</string>\n  <string name=\"playlist_order_published_date_older_first\">Ngày đã xuất bản (Cũ nhất trước)</string>\n  <string name=\"playlist_order_popularity\">Độ phổ biến</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">Không thể làm điều này cho danh sách phát của người khác</string>\n  <string name=\"owned_playlist_warning\">Lỗi: Tác vụ này chỉ có thể được áp dụng cho chủ sở hữu danh sách phát</string>\n  <string name=\"content_block_alt_server\">Sử dụng máy chủ thay thế</string>\n  <string name=\"content_block_alt_server_desc\">Bật tùy chọn này nếu SponsorBlock không hoạt động vì nhiều lý do khác nhau.</string>\n  <string name=\"playback_starts_shortly\">Sẽ tự động bắt đầu phát khi buổi phát trực tiếp sẵn sàng</string>\n  <string name=\"unset_stream_reminder\">Bỏ đặt lịch phát trực tiếp</string>\n  <string name=\"set_stream_reminder\">Đặt lịch phát trực tiếp</string>\n  <string name=\"starting_stream\">Đang bắt đầu buổi phát trực tiếp...</string>\n  <string name=\"action_playlist_remove\">Loại bỏ khỏi danh sách phát</string>\n  <string name=\"signin_view_title\">Đang tải mã đăng nhập...</string>\n  <string name=\"signin_view_description\">Để đăng nhập, nhập mã được hiển thị trên màn hình vào trang web %s\\nLưu ý: Chỉ hoạt động với trình duyệt Chrome và Firefox.</string>\n  <string name=\"signin_view_action_text\">Đã xong</string>\n  <string name=\"require_checked\">Yêu cầu \\\"%s</string>\n  <string name=\"msg_press_again_to_exit\">Nhấn lần nữa để thoát</string>\n  <string name=\"player_remaining_time\">Còn lại: %s</string>\n  <string name=\"player_ending_time\">Kết thúc vào %s</string>\n  <string name=\"badge_new_content\">NỘI DUNG MỚI</string>\n  <string name=\"badge_live\">TRỰC TIẾP</string>\n  <string name=\"add_device_view_description\">Để liên kết thiết bị, nhập mã này trong ứng dụng YouTube trên điện thoại của bạn tại mục Cài đặt/Xem trên TV\\nLưu ý: Chỉ hoạt động với bàn phím Gboard!</string>\n  <string name=\"playback_controls_repeat_pause\">Dừng lặp lại</string>\n  <string name=\"playback_controls_repeat_list\">Danh sách video lặp lại</string>\n  <string name=\"action_subscribe_off\">Đăng ký Tắt</string>\n  <string name=\"action_subscribe_on\">Đăng ký Bật</string>\n  <string name=\"skip_24_rate\">Bỏ qua định dạng 24fps (có thể sửa lỗi ở chế độ điện ảnh\\?)</string>\n  <string name=\"player_disable_suggestions\">Vô hiệu hoá gợi ý trong trình phát</string>\n  <string name=\"feedback\">Phản hồi</string>\n  <string name=\"sources\">Mã nguồn</string>\n  <string name=\"releases\">Phiên bản</string>\n  <string name=\"open_chat\">Trò chuyện</string>\n  <string name=\"prefer_avc_over_vp9\">Lựa chọn bộ giải mã: ưu tiên sử dụng avc thay cho vp9</string>\n  <string name=\"place_chat_left\">Đặt trò chuyện ở bên trái</string>\n  <string name=\"use_alt_speech_recognizer\">Trình nhận dạng giọng nói thay thế</string>\n  <string name=\"time_format\">định dạng thời gian</string>\n  <string name=\"time_format_24\">24 giờ</string>\n  <string name=\"time_format_12\">12 giờ (AM/PM)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">Cực kỳ không ổn định. Chỉ sử dụng nó nếu bạn gặp sự cố với trình nhận dạng mặc định.</string>\n  <string name=\"speech_recognizer\">Trình nhận dạng giọng nói</string>\n  <string name=\"speech_recognizer_system\">Theo hệ thống (Ưu tiên)</string>\n  <string name=\"speech_recognizer_external_1\">Bên ngoài 1 (cực kỳ không ổn định, để hoạt động, bạn cần tắt quyền truy cập vào micrô)</string>\n  <string name=\"speech_recognizer_external_2\">Bên ngoài 2 (cực kỳ không ổn định)</string>\n  <string name=\"real_channel_icon\">Hiển thị ảnh đại diện của kênh trong trình phát</string>\n  <string name=\"video_buffer_size_none\">Không có</string>\n  <string name=\"not_recommend_channel\">Không đề xuất kênh này</string>\n  <string name=\"you_wont_see_this_channel\">Chúng tôi sẽ không đề xuất kênh này cho bạn nữa</string>\n  <string name=\"share_qr_link\">Chia sẻ liên kết (Mã QR)</string>\n  <string name=\"msg_player_error_video_renderer\">Định dạng VIDEO không được hỗ trợ.\\nHãy thử chọn một định dạng khác bằng cách bấm nút HQ trong trình phát.\\nNếu vẫn không được thì hãy thử khởi động lại thiết bị của bạn.</string>\n  <string name=\"msg_player_error_audio_renderer\">Định dạng ÂM THANH không được hỗ trợ.\\nHãy thử chọn một định dạng khác bằng cách bấm nút HQ trong trình phát.\\nNếu vẫn không được thì hãy thử khởi động lại thiết bị của bạn.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">Định dạng PHỤ ĐỀ không được hỗ trợ.\\nHãy thử chọn một định dạng khác bằng cách bấm nút CC trong trình phát.\\nNếu vẫn không được thì hãy thử khởi động lại thiết bị của bạn.</string>\n  <string name=\"disable_ok_long_press_desc\">Dành cho các điều khiển có lỗi nút OK không hoạt động bình thường</string>\n  <string name=\"msg_player_error_video_source\">URL video không hoạt động hoặc thời gian của thiết bị không chính xác.\\nThông thường thì đây là lỗi từ ứng dụng.</string>\n  <string name=\"msg_player_error_audio_source\">URL âm thanh không hoạt động hoặc thời gian của thiết bị không chính xác.\\nThông thường thì đây là lỗi từ ứng dụng.</string>\n  <string name=\"msg_player_error_subtitle_source\">URL phụ đề không hoạt động hoặc thời gian của thiết bị không chính xác.\\nThông thường thì đây là lỗi từ ứng dụng.</string>\n  <string name=\"player_volume\">Âm lượng</string>\n  <string name=\"pressing_back\">bằng cách nhấn nút BACK</string>\n  <string name=\"open_comments\">Bình luận</string>\n  <string name=\"subtitle_yellow_semi_transparent\">Chữ vàng trên nền bán trong suốt</string>\n  <string name=\"subtitle_yellow_black\">Chữ vàng trên nền đen</string>\n  <string name=\"player_pixel_ratio\">Tỉ lệ pixel</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">Xám tối (Đơn sắc)</string>\n  <string name=\"disable_mic_permission\">Vui lòng tắt quyền truy cập micrô của ứng dụng để trình nhận dạng hoạt động bình thường.</string>\n  <string name=\"repeat_mode_shuffle\">Trộn danh sách phát</string>\n  <string name=\"chat_left\">Trái</string>\n  <string name=\"chat_right\">Phải</string>\n  <string name=\"card_real_thumbnails\">Thay thế hình xem trước bằng một khung ảnh từ video</string>\n  <string name=\"card_content\">Nơi lấy hình xem trước của thẻ</string>\n  <string name=\"content_block_status\">Kiểm tra trạng thái máy chủ của SponsorBlock</string>\n  <string name=\"thumb_quality_end\">Cuối video</string>\n  <string name=\"thumb_quality_middle\">Giữa video</string>\n  <string name=\"thumb_quality_start\">Đầu video</string>\n  <string name=\"thumb_quality_default\">Mặc định</string>\n  <string name=\"player_show_quality_info_bitrate\">Thêm bitrate vào thông tin chất lượng</string>\n  <string name=\"pause_history\">Tạm dừng lưu lịch sử</string>\n  <string name=\"resume_history\">Tiếp tục lưu lịch sử</string>\n  <string name=\"disable_history\">Tắt lưu lịch sử</string>\n  <string name=\"enable_history\">Bật lưu lịch sử</string>\n  <string name=\"clear_history\">Xoá lịch sử</string>\n  <string name=\"chapters\">Chương</string>\n  <string name=\"card_multiline_subtitle\">Phụ đề nhiều dòng</string>\n  <string name=\"auto_frame_rate_modes\">Chế độ được hỗ trợ</string>\n  <string name=\"loading\">Đang tải...</string>\n  <string name=\"video_duration_any\">Bất kì</string>\n  <string name=\"video_duration\">Độ dài</string>\n  <string name=\"video_duration_under_4\">Dưới 4 phút</string>\n  <string name=\"video_duration_between_4_20\">Từ 4-20 phút</string>\n  <string name=\"video_duration_over_20\">Hơn 20 phút</string>\n  <string name=\"content_type\">Thể loại</string>\n  <string name=\"content_type_any\">Bất kì</string>\n  <string name=\"content_type_video\">Video</string>\n  <string name=\"content_type_channel\">Kênh</string>\n  <string name=\"content_type_playlist\">Danh sách phát</string>\n  <string name=\"content_type_movie\">Phim</string>\n  <string name=\"video_features\">Nổi bật</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"search_sorting\">Sắp xếp theo</string>\n  <string name=\"sort_by_relevance\">Độ liên quan</string>\n  <string name=\"sort_by_views\">Lượt xem</string>\n  <string name=\"sort_by_date\">Ngày tải lên</string>\n  <string name=\"sort_by_rating\">Đánh giá</string>\n  <string name=\"clear_search_history\">Xoá lịch sử tìm kiếm</string>\n  <string name=\"remove_from_subscriptions\">Ẩn</string>\n  <string name=\"unlock_all_formats\">Mở khoá tất cả định dạng video</string>\n  <string name=\"update_found\">Cập nhật</string>\n  <string name=\"header_kids_home\">Trẻ em</string>\n  <string name=\"header_trending\">Thịnh hành</string>\n  <string name=\"player_likes_count\">Hiển thị lượt thích/không thích</string>\n  <string name=\"mark_as_watched\">Đánh dấu là đã xem</string>\n  <string name=\"content_block_exclude_channel\">Loại trừ kênh này khỏi SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">Ngừng loại trừ kênh này khỏi SponsorBlock</string>\n  <string name=\"player_chapter_notification\">Hiển thị thông báo để nhanh chóng chuyển qua các phân cảnh</string>\n  <string name=\"player_auto_volume\">Tự động điều chỉnh âm lượng</string>\n  <string name=\"player_ui_on_next\">Hiển thị trình phát khi chuyển qua video tiếp theo</string>\n  <string name=\"msg_player_unknown_error\">Lỗi không xác định</string>\n  <string name=\"header_notifications\">Thông báo</string>\n  <string name=\"disable_search_history\">Tắt lịch sử tìm kiếm</string>\n  <string name=\"color_scheme_blue\">Xanh biển</string>\n  <string name=\"color_scheme_dark_blue\">Xanh biển tối</string>\n  <string name=\"enter_account_password\">Nhập mật khẩu của tài khoản</string>\n  <string name=\"show_connect_messages\">Hiển thị tin nhắn kết nối</string>\n  <string name=\"prefer_avc_over_vp9_desc\">Cảnh báo: Tối đa là 1080p</string>\n  <string name=\"hide_shorts_everywhere\">Ẩn Shorts ở mọi nơi</string>\n  <string name=\"volume_boost_warning\">Cài đặt quá mức có thể sẽ dẫn đến hư hỏng loa</string>\n  <string name=\"play_video_incognito\">Phát ở chế độ ẩn danh</string>\n  <string name=\"dialog_notification\">Thông báo về bản cập nhật với hộp thoại</string>\n  <string name=\"audio_language\">Ngôn ngữ âm thanh</string>\n  <string name=\"player_ui_animations\">Bật hoạt ảnh của bảng điều khiển</string>\n  <string name=\"trending_searches\">Tìm kiếm nổi bật</string>\n  <string name=\"video_feature_any\">Bất kì</string>\n  <string name=\"video_feature_live\">Trực tiếp</string>\n  <string name=\"player_speed_button_old_behavior\">Khôi phục chức năng cũ của nút chỉnh tốc độ</string>\n  <string name=\"protect_settings_with_password\">Bảo vệ cài đặt bằng mật khẩu</string>\n  <string name=\"enter_settings_password\">Nhập mật khẩu cài đặt</string>\n  <string name=\"child_mode\">Chế độ trẻ em</string>\n  <string name=\"child_mode_desc\">Trong chế độ này, người dùng không thể tìm kiếm hoặc xem nội dung đề xuất. Trang cài đặt sẽ được bảo vệ bằng mật khẩu.</string>\n  <string name=\"player_button_long_click\">Giữ nút này để mở thêm tuỳ chọn</string>\n  <string name=\"video_rotate\">Xoay</string>\n  <string name=\"live_stream_fix_4k_desc\">Vô hiệu hoá việc tua lùi video phát trực tiếp. Cải thiện đáng kể hiệu suất. Độ phân giải tối đa là 4K.</string>\n  <string name=\"live_stream_fix_4k\">Sửa lỗi phát video trực tiếp (4K)</string>\n  <string name=\"player_speed_per_channel\">Cho mỗi kênh</string>\n  <string name=\"disable_popular_searches\">Tắt các từ khoá tìm kiếm phổ biến</string>\n  <string name=\"video_buffer_size_lowest\">Thấp nhất</string>\n  <string name=\"video_buffer_size_highest\">Cao nhất</string>\n  <string name=\"player_exit_shortcut\">Thoát khỏi trình phát</string>\n  <string name=\"app_corner_clock\">Trang chủ: đồng hồ ở góc trên bên phải</string>\n  <string name=\"player_corner_clock\">Trình phát: đồng hồ ở góc trên bên phải</string>\n  <string name=\"player_corner_ending_time\">Trình phát: thời gian kết thúc ở góc trên bên phải</string>\n  <string name=\"pin_playlist\">Thêm danh sách phát vào thanh bên</string>\n  <string name=\"pin_channel\">Thêm kênh vào thanh bên</string>\n  <string name=\"unpin_group_from_sidebar\">Bỏ nhóm đăng ký khỏi thanh bên</string>\n  <string name=\"hide_shorts_channel\">Ẩn Shorts khỏi kênh</string>\n  <string name=\"news_row_name\">Tin tức</string>\n  <string name=\"action_sound_off\">Tắt âm thanh</string>\n  <string name=\"hide_upcoming_channel\">Ẩn mục \\\"sắp tới\\\" khỏi Kênh</string>\n  <string name=\"hide_upcoming_home\">Ẩn mục \\\"sắp tới\\\" khỏi Trang chủ</string>\n  <string name=\"hide_shorts_from_trending\">Ẩn Shorts từ Thịnh hành</string>\n  <string name=\"save_playlist\">Thêm danh sách phát này vào vào mục Danh sách phát</string>\n  <string name=\"skip_shorts\">Bỏ qua Shorts</string>\n  <string name=\"speech_engine\">Công cụ tìm kiếm bằng giọng nói</string>\n  <string name=\"dearrow_status\">Kiểm tra trạng thái máy chủ của DeArrow</string>\n  <string name=\"lost_setting_warning\">Cài đặt ứng dụng sẽ được thay đổi. Hãy đảm bảo rằng bạn đã sao lưu các cài đặt.</string>\n  <string name=\"hide_streams\">Ẩn video phát trực tiếp khỏi mục Kênh đăng ký</string>\n  <string name=\"player_long_speed_list\">Danh sách tốc độ dài</string>\n  <string name=\"player_extra_long_speed_list\">Danh sách tốc độ cực dài</string>\n  <string name=\"alt_app_icon\">Biểu tượng ứng dụng thay thế (cần khởi động lại)</string>\n  <string name=\"network_stack\">Ưu tiên lớp giao thức mạng %s</string>\n  <string name=\"player_network_stack\">Trình xử lý mạng</string>\n  <string name=\"cronet_desc\">Cronet là lớp giao thức mạng cho Chromium. Cronet có thể giảm độ trễ và tăng hiệu suất mạng, và có thể giúp giải quyết các vấn đề về bộ đệm.</string>\n  <string name=\"okhttp_desc\">Trình xử lý mạng này là trình xử lý chậm nhất nhưng có độ ổn định tốt.</string>\n  <string name=\"select_channel_section\">Mở phần tương ứng khi ứng dụng được khởi chạy từ kênh ATV</string>\n  <string name=\"enable_master_password\">Bật mật khẩu chính</string>\n  <string name=\"enter_master_password\">Nhập mật khẩu chính</string>\n  <string name=\"disable_stream_buffer\">Vô hiệu hóa bộ đệm trên video phát trực tiếp</string>\n  <string name=\"disable_stream_buffer_desc\">Sửa lỗi cho các trường hợp khi video phát trực tiếp bị chậm quá nhiều so với thực tế. Lưu ý: video phát trực tiếp đó có thể sẽ bắt đầu bị giật.</string>\n  <string name=\"old_home_look\">Giao diện cũ của mục Trang chủ</string>\n  <string name=\"old_channel_look\">Giao diện cũ của mục Kênh</string>\n  <string name=\"player_section_playlist\">Sử dụng nội dung phần hiện tại làm danh sách phát</string>\n  <string name=\"screensaver\">Màn hình chờ</string>\n  <string name=\"player_screen_off_timeout\">Thời gian chờ đến khi tắt màn hình</string>\n  <string name=\"old_update_notifications\">Giao diện cũ của thông báo cập nhật</string>\n  <string name=\"sorting_alphabetically2\">Theo thứ tự bảng chữ cái (nhanh, có hoạt ảnh xem trước)</string>\n  <string name=\"sorting_default\">Mặc định (nhanh, có hoạt ảnh xem trước)</string>\n  <string name=\"subtitle_remember\">Nhớ bật phụ đề cho từng kênh</string>\n  <string name=\"screensaver_timout\">Thời gian chờ đến màn hình chờ</string>\n  <string name=\"screensaver_dimming\">Độ làm mờ màn hình chờ</string>\n  <string name=\"autogenerated\">Được tạo tự động</string>\n  <string name=\"player_global_focus\">Điều hướng mượt mà giữa các hàng nút của trình phát</string>\n  <string name=\"auto_history\">Tự động (sử dụng cài đặt của tài khoản)</string>\n  <string name=\"remember_position_subscriptions\">Ghi nhớ vị trí xem cuối cùng trong mục Kênh đăng ký</string>\n  <string name=\"remember_position_pinned\">Ghi nhớ vị trí xem cuối cùng trong danh sách phát được ghim</string>\n  <string name=\"unknown_source_error\">Lỗi nguồn không xác định</string>\n  <string name=\"unknown_renderer_error\">Lỗi kết xuất không xác định</string>\n  <string name=\"unlock_high_bitrate_formats\">Mở khóa các định dạng 1080p vp9 bitrate cao</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">Mở khóa các định dạng mp4a bitrate cao</string>\n  <string name=\"multi_profiles\">Sử dụng các thiết lập riêng biệt cho từng tài khoản</string>\n  <string name=\"protect_account_with_password\">Bảo vệ tài khoản bằng mật khẩu</string>\n  <string name=\"play_next\">Phát video tiếp theo</string>\n  <string name=\"hide_watched_from_subscriptions\">Ẩn video đã xem khỏi mục Kênh đăng ký</string>\n  <string name=\"hide_watched_from_notifications\">Ẩn video đã xem khỏi mục Thông báo</string>\n  <string name=\"hide_unwanted_content\">Ẩn nội dung không mong muốn</string>\n  <string name=\"hide_watched_from_home\">Ẩn video đã xem khỏi Trang chủ</string>\n  <string name=\"hide_watched_from_watch_later\">Ẩn video đã xem khỏi danh sách phát Xem sau</string>\n  <string name=\"player_loop_shorts\">Lặp Shorts</string>\n  <string name=\"disable_channels_service\">Vô hiệu hóa dịch vụ Kênh</string>\n  <string name=\"enable\">Bật</string>\n  <string name=\"about_sponsorblock\">Về SponsorBlock</string>\n  <string name=\"about_dearrow\">Về DeArrow</string>\n  <string name=\"replace_titles\">Thay thế tiêu đề</string>\n  <string name=\"more_info\">Thông tin thêm</string>\n  <string name=\"replace_thumbnails\">Thay thế hình thu nhỏ</string>\n  <string name=\"long_press_for_settings\">NHẤN GIỮ ĐỂ MỞ CÀI ĐẶT</string>\n  <string name=\"long_press_for_options\">NHẤN GIỮ ĐỂ MỞ CÁC TÙY CHỌN</string>\n  <string name=\"device_specific_backup\">Chỉ sao lưu cho thiết bị này</string>\n  <string name=\"local_backup\">Sao lưu cục bộ</string>\n  <string name=\"video_disabled\">Video đã bị vô hiệu hoá</string>\n  <string name=\"hide_mixes\">Ẩn danh sách phát kết hợp</string>\n  <string name=\"recommended\">Đề xuất</string>\n  <string name=\"new_subscriptions_group\">Thêm nhóm</string>\n  <string name=\"rename_group\">Đổi tên nhóm đăng ký</string>\n  <string name=\"add_to_subscriptions_group\">Thêm/Xóa khỏi nhóm đăng ký</string>\n  <string name=\"repeat_mode_reverse_list\">Phát danh sách phát hoặc video của kênh theo thứ tự ngược lại</string>\n  <string name=\"pinned_channel_rows\">Hiển thị kênh được ghim dưới dạng hàng</string>\n  <string name=\"pitch_effect\">Hiệu ứng cao độ</string>\n  <string name=\"header_sports\">Thể thao</string>\n  <string name=\"crowdsoursed_titles\">Nguồn tiêu đề từ người xem</string>\n  <string name=\"crowdsourced_thumbnails\">Nguồn hình thu nhỏ từ người xem</string>\n  <string name=\"dearrow_not_submitted_thumbs\">Nguồn hình thu nhỏ (nếu không có hình ảnh được gửi qua DeArrow)</string>\n  <string name=\"player_quick_shorts_skip\">Bỏ qua Shorts với nút trái/phải</string>\n  <string name=\"player_quick_skip_videos\">Bỏ qua video bình thường với nút trái/phải</string>\n  <string name=\"channels_filter\">Hiển thị phần \\\"Lọc kênh\\\" ở mục Kênh</string>\n  <string name=\"channel_search_bar\">Thanh tìm kiếm ở mục Kênh</string>\n  <string name=\"keep_finished_activities\">Giữ lại các hoạt động đã hoàn thành</string>\n  <string name=\"login_from_browser\">Đăng nhập từ trình duyệt</string>\n  <string name=\"fullscreen_mode\">Chế độ toàn màn hình (không có thanh hệ thống)</string>\n  <string name=\"without_picture\">Không có hình ảnh</string>\n  <string name=\"default_stack_desc\">Trình xử lý mạng tích hợp. Trình xử lý này có thể có độ ổn định tốt hơn trong một số tình huống nhất định.</string>\n  <string name=\"player_screen_off_dimming\">Độ làm mờ màn hình</string>\n  <string name=\"auto_backup\">Sao lưu tự động (một lần một ngày)</string>\n  <string name=\"disable_remote_history\">Vô hiệu hóa lịch sử khi sử dụng điều khiển từ xa</string>\n  <string name=\"nothing_found\">Không tìm thấy gì</string>\n  <string name=\"channel_filter_hint\">Lọc kênh</string>\n  <string name=\"place_comments_left\">Đặt bình luận ở bên trái</string>\n  <string name=\"prefer_ipv4\">Ưu tiên DNS IPv4</string>\n  <string name=\"disable_network_error_fixing\">Vô hiệu hóa chức năng tự động sửa lỗi mạng</string>\n  <string name=\"disable_network_error_fixing_desc\">Bạn có thể cần phải bật tùy chọn này nếu bạn đang sử dụng VPN</string>\n  <string name=\"playlists_rows\">Hiển thị mục Danh sách phát dưới dạng hàng</string>\n  <string name=\"remember_position_of_live_videos\">Nhớ vị trí của các video phát trực tiếp</string>\n  <string name=\"not_supported_by_device\">Thiết bị của bạn không hỗ trợ tính năng này</string>\n  <string name=\"hide_shorts_from_search\">Ẩn Shorts từ kết quả tìm kiếm</string>\n  <string name=\"suggestions\">Gợi ý</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"unlock_all_formats_desc\">Ở một số thiết bị thì hệ điều hành sẽ báo sai một số định dạng video là \\\"không được hỗ trợ\\\", mặc dù thực tế là có.\\n(ví dụ như các TV thông minh có độ phân giải 1080p thường sẽ báo không hỗ trợ độ phân giải 4K).</string>\n  <string name=\"enable_voice_search_desc\">Ứng dụng chính thức sẽ được thay thế với một thứ gọi là \\\"cây cầu\\\". Một \\\"cây cầu\\\" sẽ giúp đỡ trong việc chuyển đổi yêu cầu tìm kiếm tổng quát sang ứng dụng của chúng tôi.</string>\n  <string name=\"sony_frame_drop_fix\">Sửa lỗi giảm khung hình #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">Sửa lỗi giật lag ở các TV của Sony và một số thiết bị khác. Lưu ý: có thể sẽ có vấn đề với việc đồng bộ hoá âm thanh.</string>\n  <string name=\"amazon_frame_drop_fix\">Sửa lỗi giảm khung hình #2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">Dành cho các thiết bị Amazon Stick. Cũng có thể hoạt động ở một số thiết bị khác.</string>\n  <string name=\"item_postion\">Vị trí của</string>\n  <string name=\"header_shorts\">Shorts</string>\n  <string name=\"remote_control_permission\">Điều khiển trong nền yêu cầu quyền Vẽ trên ứng dụng khác</string>\n  <string name=\"keyboard_fix\">Ngăn việc nút OK vô tình mở bàn phím (G20s và các thiết bị khác)</string>\n  <string name=\"auto_frame_rate_desc\">Tùy chọn này loại bỏ hiện tượng giật hình trong các cảnh có chuyển động nhanh, ví dụ như ở các video phát trực tiếp về thể thao</string>\n  <string name=\"player_only_mode\">Chỉ hiển thị trình phát nếu video được mở bên ngoài ứng dụng</string>\n  <string name=\"prefer_ipv4_desc\">Có thể sửa các trường hợp khi ứng dụng hoàn toàn không hoạt động.\\nLưu ý: Có thể gây ra tình trạng đơ hoặc thoát đột ngột ứng dụng (đặc biệt là ở các thiết bị Android 8 hoặc Dune HD)</string>\n  <string name=\"calm_msg\">Chúng tôi đang sửa lỗi này. Hãy kiểm tra cập nhật thường xuyên</string>\n  <string name=\"applying_fix\">Đang sửa lỗi... Vui lòng không đóng trình phát.</string>\n  <string name=\"player_global_focus_desc\">Tính năng này ảnh hưởng đến việc nút nào sẽ được tập trung khi điều hướng giữa các hàng nút của trình phát</string>\n  <string name=\"screen_dimming_amount\">Độ làm mờ màn hình</string>\n  <string name=\"screen_dimming_timeout\">Thời gian chờ đến khi làm mờ màn hình</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">Bật phụ đề</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">Tắt phụ đề</string>\n  <string name=\"import_subscriptions_group\">Nhập</string>\n  <string name=\"my_videos\">Video của bạn</string>\n  <string name=\"paid_content_notification\">Thông báo nội dung trả tiền</string>\n  <string name=\"context_menu_sorting\">Sắp xếp danh mục</string>\n  <string name=\"you_liked\">đã thích</string>\n  <string name=\"player_audio_focus\">Lấy nét âm thanh (tạm dừng nếu trình phát khác đã phát hiện)</string>\n  <string name=\"oculus_quest_fix\">Sửa Oculus Quest</string>\n  <string name=\"playback_buffering_fix\">Sửa bộ đệm phát lại</string>\n  <string name=\"search_exit_shortcut\">Thoát khỏi tìm kiếm</string>\n  <string name=\"card_preview_muted\">Video không có âm thanh</string>\n  <string name=\"card_preview_full\">Video có âm thanh</string>\n  <string name=\"card_preview\">Xem trước thẻ</string>\n  <string name=\"original_lang\">Nguyên bản</string>\n  <string name=\"remove_playlist_fmt\">Xóa %s vĩnh viễn\\?</string>\n  <string name=\"auto_backup_category\">Tự động sao lưu</string>\n  <string name=\"once_a_day\">Một lần một ngày</string>\n  <string name=\"once_a_week\">Một lần một tuần</string>\n  <string name=\"once_a_month\">Một lần một tháng</string>\n  <string name=\"play_from_start\">Phát từ đầu</string>\n  <string name=\"typing_corrections\">Vô hiệu hóa tự động sửa khi gõ văn bản</string>\n  <string name=\"player_toggle_speed\">Chuyển đổi tốc độ (bật/tắt)</string>\n  <string name=\"player_quick_shorts_skip_alt\">Bỏ qua Shorts với nút lên/xuống</string>\n  <string name=\"player_quick_skip_videos_alt\">Bỏ qua video bình thường với nút lên/xuống</string>\n  <string name=\"prefer_google_dns\">Ưu tiên DNS Google</string>\n  <string name=\"dialog_block_channel\">Chặn kênh</string>\n  <string name=\"dialog_unblock_channel\">Bỏ chặn kênh</string>\n  <string name=\"confirm_block_channel\">Ẩn tất cả nội dung từ %s?</string>\n  <string name=\"channel_blocked\">Đã chặn kênh</string>\n  <string name=\"channel_unblocked\">Đã bỏ chặn kênh</string>\n  <string name=\"header_blocked_channels\">Kênh đã chặn</string>\n  <string name=\"msg_no_blocked_channels\">Không có kênh nào bị chặn</string>\n  <string name=\"action_debug_info\">Thông tin gỡ lỗi</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-zh/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">首页</string>\n  <string name=\"title_search\">搜索</string>\n  <string name=\"header_subscriptions\">订阅</string>\n  <string name=\"header_history\">历史记录</string>\n  <string name=\"header_music\">音乐</string>\n  <string name=\"header_news\">新闻</string>\n  <string name=\"header_gaming\">游戏</string>\n  <string name=\"header_playlists\">播放列表</string>\n  <string name=\"header_settings\">设置</string>\n  <string name=\"header_channels\">频道</string>\n  <string name=\"subscriptions_signin_title\">查看你喜爱的频道的最新动态</string>\n  <string name=\"subscriptions_signin_subtitle\">登录以查看你的订阅</string>\n  <string name=\"subscriptions_signin_button_text\">登录</string>\n  <string name=\"library_signin_to_show_more\">观看你点赞、保存或订阅的视频</string>\n  <string name=\"library_signin_subtitle\">登录以查看你的收藏夹</string>\n  <string name=\"action_signin\">登录</string>\n  <string name=\"title_video_formats\">视频格式</string>\n  <string name=\"title_audio_formats\">音频格式</string>\n  <string name=\"subtitle_category_title\">字幕</string>\n  <string name=\"playback_queue_category_title\">播放队列</string>\n  <string name=\"video_max_quality\">自动（最高画质）</string>\n  <string name=\"audio_max_quality\">自动（最高音质）</string>\n  <string name=\"subtitles_disabled\">关闭字幕</string>\n  <string name=\"auto_frame_rate\">自动帧率</string>\n  <string name=\"frame_rate_correction\">修复 fps:\\n%s</string>\n  <string name=\"category_background_playback\">后台播放</string>\n  <string name=\"not_implemented\">尚不可用</string>\n  <string name=\"option_background_playback_off\">关闭</string>\n  <string name=\"option_background_playback_pip\">画中画</string>\n  <string name=\"option_background_playback_only_audio\">仅音频</string>\n  <string name=\"playback_settings\">播放质量设置</string>\n  <string name=\"video_speed\">播放速度</string>\n  <string name=\"resolution_switch\">切换分辨率</string>\n  <string name=\"video_buffer\">视频缓冲</string>\n  <string name=\"video_buffer_size_none\">无</string>\n  <string name=\"video_buffer_size_low\">低</string>\n  <string name=\"video_buffer_size_med\">中</string>\n  <string name=\"video_buffer_size_high\">高</string>\n  <string name=\"update_changelog\">更新日志</string>\n  <string name=\"install_update\">安装更新</string>\n  <string name=\"section_is_empty\">哎呀！这里什么都没有</string>\n  <string name=\"dialog_add_to_playlist\">添加至播放列表或从中移除</string>\n  <string name=\"msg_signed_users_only\">仅限已登录用户</string>\n  <string name=\"msg_cant_load_content\">无法加载内容。\\n1）开启观看历史。\\n2）检查设备的日期和时间设置。\\n3）检查网络连接。\\n4）检查代理设置。\\n5）尝试登录你的帐户。\\n6）这里可能什么都没有。</string>\n  <string name=\"title_video_presets\">视频预设</string>\n  <string name=\"settings_accounts\">帐户</string>\n  <string name=\"settings_left_panel\">编辑分类</string>\n  <string name=\"settings_themes\">主题</string>\n  <string name=\"settings_other\">其他</string>\n  <string name=\"settings_player\">视频播放器</string>\n  <string name=\"settings_language\">语言</string>\n  <string name=\"settings_linked_devices\">已关联的设备</string>\n  <string name=\"settings_about\">关于</string>\n  <string name=\"dialog_account_list\">选择账号</string>\n  <string name=\"dialog_account_none\">无</string>\n  <string name=\"dialog_remove_account\">注销</string>\n  <string name=\"dialog_add_account\">登录</string>\n  <string name=\"default_lang\">默认</string>\n  <string name=\"dialog_select_language\">语言</string>\n  <string name=\"subtitle_default\">默认</string>\n  <string name=\"subtitle_white_semi_transparent\">白字半透明背景</string>\n  <string name=\"subtitle_style\">字幕样式</string>\n  <string name=\"subtitle_language\">字幕语言</string>\n  <string name=\"action_search\">搜索</string>\n  <string name=\"settings_main_ui\">用户界面</string>\n  <string name=\"dialog_main_ui\">用户界面</string>\n  <string name=\"card_animated_previews\">动画预览</string>\n  <string name=\"web_site\">网站</string>\n  <string name=\"donation\">捐赠</string>\n  <string name=\"dialog_about\">关于</string>\n  <string name=\"dialog_player_ui\">视频播放器</string>\n  <string name=\"player_show_ui_on_pause\">暂停时显示用户界面</string>\n  <string name=\"player_pause_on_ok\">按确认键暂停播放</string>\n  <string name=\"player_ok_button_behavior\">确认键操作</string>\n  <string name=\"player_only_ui\">仅用户界面</string>\n  <string name=\"player_ui_and_pause\">用户界面和暂停</string>\n  <string name=\"player_only_pause\">仅暂停</string>\n  <string name=\"check_for_updates\">检查更新</string>\n  <string name=\"update_not_found\">已是最新版本</string>\n  <string name=\"update_in_progress\">请稍等…</string>\n  <string name=\"option_never\">从不</string>\n  <string name=\"side_panel_sections\">设置分区</string>\n  <string name=\"boot_to_section\">启动时进入的分区</string>\n  <string name=\"large_ui\">大尺寸用户界面</string>\n  <string name=\"video_grid_scale\">视频网格比例</string>\n  <string name=\"scale_ui\">用户界面比例</string>\n  <string name=\"color_scheme\">配色方案</string>\n  <string name=\"color_scheme_default\">默认</string>\n  <string name=\"color_scheme_red_grey\">红-灰</string>\n  <string name=\"color_scheme_red\">红色</string>\n  <string name=\"color_scheme_dark_grey\">深灰</string>\n  <string name=\"disable_update_check\">禁用检查更新</string>\n  <string name=\"show_again\">再次显示</string>\n  <string name=\"check_updates_auto\">通知更新</string>\n  <string name=\"select_account_on_boot\">启动时显示账号选择</string>\n  <string name=\"player_ui_hide_behavior\">用户界面自动隐藏</string>\n  <string name=\"player_other\">杂项</string>\n  <string name=\"player_full_date\">视频描述中的确切日期</string>\n  <string name=\"open_channel\">打开频道</string>\n  <string name=\"open_playlist\">打开播放列表</string>\n  <string name=\"not_interested\">不感兴趣</string>\n  <string name=\"not_recommend_channel\">不推荐此频道</string>\n  <string name=\"you_wont_see_this_video\">该视频已从推荐中移除</string>\n  <string name=\"you_wont_see_this_channel\">你将不再看到此频道的推荐</string>\n  <string name=\"settings_search\">搜索</string>\n  <string name=\"dialog_search\">搜索</string>\n  <string name=\"instant_voice_search\">即时语音搜索</string>\n  <string name=\"option_background_playback_behind\">后台播放</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">下一个视频信息尚未加载</string>\n  <string name=\"card_multiline_title\">多行标题</string>\n  <string name=\"cards_style\">卡片样式</string>\n  <string name=\"repeat_mode_all\">连续播放视频</string>\n  <string name=\"repeat_mode_one\">重复当前视频</string>\n  <string name=\"repeat_mode_pause\">每个视频结束后暂停播放（队列除外）</string>\n  <string name=\"repeat_mode_pause_alt\">仅连续播放播放列表中的视频</string>\n  <string name=\"repeat_mode_none\">每个视频结束后停止播放（队列除外）</string>\n  <string name=\"subscribed_to_channel\">已订阅</string>\n  <string name=\"unsubscribed_from_channel\">已退订</string>\n  <string name=\"subtitle_yellow_transparent\">黄字透明背景</string>\n  <string name=\"subtitle_white_transparent\">白字透明背景</string>\n  <string name=\"subtitle_white_black\">白字黑色背景</string>\n  <string name=\"player_seek_preview\">快进预览</string>\n  <string name=\"color_scheme_dark_grey_oled\">深灰（OLED）</string>\n  <string name=\"channels_section_sorting\">频道分区排序</string>\n  <string name=\"sorting_by_new_content\">最新内容</string>\n  <string name=\"sorting_alphabetically\">字母顺序</string>\n  <string name=\"sorting_last_viewed\">最近观看</string>\n  <string name=\"player_pause_when_seek\">快进时暂停</string>\n  <string name=\"playlists_style\">播放列表分区样式</string>\n  <string name=\"playlists_style_grid\">网格</string>\n  <string name=\"playlists_style_rows\">列表</string>\n  <string name=\"player_show_clock\">控制栏显示时钟</string>\n  <string name=\"player_show_remaining_time\">控制栏显示剩余时间</string>\n  <string name=\"player_show_ending_time\">控制栏显示结束时间</string>\n  <string name=\"open_channel_uploads\">开放频道上传</string>\n  <string name=\"app_exit_shortcut\">退出应用</string>\n  <string name=\"player_exit_shortcut\">退出播放器</string>\n  <string name=\"app_exit_none\">不退出</string>\n  <string name=\"app_double_back_exit\">双击返回键退出</string>\n  <string name=\"app_single_back_exit\">单击返回键退出</string>\n  <string name=\"action_video_zoom\">视频缩放</string>\n  <string name=\"video_zoom\">视频缩放</string>\n  <string name=\"video_zoom_default\">默认</string>\n  <string name=\"video_zoom_fit_width\">自适应宽度</string>\n  <string name=\"video_zoom_fit_height\">自适应高度</string>\n  <string name=\"video_zoom_fit_both\">自适应宽高</string>\n  <string name=\"video_zoom_stretch\">全屏拉伸</string>\n  <string name=\"color_scheme_teal\">蓝绿</string>\n  <string name=\"color_scheme_teal_oled\">蓝绿（OLED）</string>\n  <string name=\"player_seek_preview_none\">关闭</string>\n  <string name=\"player_seek_preview_single\">单帧</string>\n  <string name=\"player_seek_preview_carousel\">漫游</string>\n  <string name=\"player_seek_preview_carousel_slow\">慢速漫游</string>\n  <string name=\"player_seek_preview_carousel_fast\">快速漫游</string>\n  <string name=\"unsubscribe_from_channel\">退订频道</string>\n  <string name=\"card_title_lines_num\">卡片标题行数</string>\n  <string name=\"card_auto_scrolled_title\">自动滚动标题</string>\n  <string name=\"share_link\">分享链接</string>\n  <string name=\"share_qr_link\">分享二维码</string>\n  <string name=\"subscribe_to_channel\">订阅频道</string>\n  <string name=\"wait_data_loading\">正在加载，请稍后…</string>\n  <string name=\"auto_frame_rate_pause\">自动帧率（切换时暂停）</string>\n  <string name=\"auto_frame_rate_applying\">应用自动帧率 %sx%s\\@%s</string>\n  <string name=\"audio_shift\">音频偏移</string>\n  <string name=\"player_remember_speed\">记忆播放速度</string>\n  <string name=\"mark_channel_as_watched\">标记为已观看</string>\n  <string name=\"channel_marked_as_watched\">频道已标记为已观看</string>\n  <string name=\"dialog_add_device\">添加设备</string>\n  <string name=\"dialog_remove_all_devices\">移除所有设备</string>\n  <string name=\"device_link_enabled\">连接已启用</string>\n  <string name=\"device_connected\">设备 \\\"%s\\\" 已连接</string>\n  <string name=\"device_disconnected\">设备 \\\"%s\\\" 已断开</string>\n  <string name=\"settings_ui_scale\">用户界面缩放</string>\n  <string name=\"msg_restart_app\">请重启应用以使设置生效</string>\n  <string name=\"settings_block\">内容屏蔽</string>\n  <string name=\"msg_applying\">正在应用 %s</string>\n  <string name=\"msg_done\">完成</string>\n  <string name=\"settings_general\">通用</string>\n  <string name=\"settings_video\">视频</string>\n  <string name=\"content_block_confirm_skip\">确认跳过</string>\n  <string name=\"content_block_categories\">跳过片段</string>\n  <string name=\"content_block_sponsor\">赞助商</string>\n  <string name=\"content_block_intro\">中场/片头动画</string>\n  <string name=\"content_block_outro\">片尾/演职员表</string>\n  <string name=\"content_block_interaction\">互动提醒（订阅）</string>\n  <string name=\"content_block_self_promo\">无偿/自我推广</string>\n  <string name=\"content_block_music_off_topic\">无配乐片段</string>\n  <string name=\"confirm_segment_skip\">跳过片段 \\\"%s\\\" ？</string>\n  <string name=\"cancel_segment_skip\">取消跳过片段</string>\n  <string name=\"msg_skipping_segment\">正在跳过片段 \\\"%s\\\" …</string>\n  <string name=\"content_block_notification_type\">通知类型</string>\n  <string name=\"content_block_notify_none\">无通知</string>\n  <string name=\"content_block_notify_toast\">消息框</string>\n  <string name=\"content_block_notify_dialog\">确认对话框</string>\n  <string name=\"return_to_launcher\">从ATV频道/搜索返回启动器</string>\n  <string name=\"intent_force_close\">如果视频是从外部来源打开的，则强制退出</string>\n  <string name=\"btn_confirm\">确认</string>\n  <string name=\"player_low_video_quality\">低画质</string>\n  <string name=\"remote_session_closed\">远程会话已关闭</string>\n  <string name=\"msg_mode_switch_error\">无法切换至 %s 显示模式</string>\n  <string name=\"msg_player_error\">播放器错误 %s。重启播放…</string>\n  <string name=\"video_preset_disabled\">无预设</string>\n  <string name=\"video_preset_enabled\">下一个视频的格式将根据预设值设置</string>\n  <string name=\"player_sleep_timer\">睡眠定时器</string>\n  <string name=\"player_show_quality_info\">控制栏显示画质信息</string>\n  <string name=\"player_remember_each_speed\">记住每个视频的播放速度</string>\n  <string name=\"player_remember_speed_none\">无</string>\n  <string name=\"player_remember_speed_all\">所有视频均相同</string>\n  <string name=\"player_remember_speed_each\">每个视频单独</string>\n  <string name=\"msg_player_error_source\">无法下载视频</string>\n  <string name=\"msg_player_error_renderer\">选择的格式不支持。\\n请尝试其他格式。\\n如若不行,请重启设备。</string>\n  <string name=\"msg_player_error_video_renderer\">选择的视频格式不支持。\\n点击播放器的画质按钮选择其他格式。\\n如若不行,请重启设备。</string>\n  <string name=\"msg_player_error_audio_renderer\">选择的音频格式不支持。\\n点击播放器的画质按钮选择其他格式。\\n如若不行,请重启设备。</string>\n  <string name=\"msg_player_error_subtitle_renderer\">选择的字幕格式不支持。\\n长按播放器中的CC字幕按钮选择其他格式。\\n如若不行,请重启设备。</string>\n  <string name=\"msg_player_error_unexpected\">未知视频解码错误</string>\n  <string name=\"video_aspect\">画面比例</string>\n  <string name=\"player_tweaks\">开发者选项</string>\n  <string name=\"header_uploads\">上传</string>\n  <string name=\"player_show_global_clock\">总是显示时钟</string>\n  <string name=\"player_show_global_ending_time\">总是显示结束时间</string>\n  <string name=\"app_corner_clock\">首页右上角时钟</string>\n  <string name=\"player_corner_clock\">播放器右上角时钟</string>\n  <string name=\"player_corner_ending_time\">播放器右上角结束时间</string>\n  <string name=\"uploads_old_look\">恢复上传分区的旧样式</string>\n  <string name=\"background_playback_activation\">后台播放（激活）</string>\n  <string name=\"channels_old_look\">恢复频道分区旧样式</string>\n  <string name=\"channels_auto_load\">自动加载频道分区内容</string>\n  <string name=\"return_to_background_video\">返回后台播放视频</string>\n  <string name=\"pin_unpin_from_sidebar\">添加至侧边栏或从中移除</string>\n  <string name=\"pin_unpin_playlist\">添加至播放列表或从中移除</string>\n  <string name=\"pin_unpin_channel\">添加至频道或从中移除</string>\n  <string name=\"pin_playlist\">添加播放列表至侧边栏</string>\n  <string name=\"pin_channel\">添加频道至侧边栏</string>\n  <string name=\"pinned_to_sidebar\">已添加至侧边栏</string>\n  <string name=\"unpin_from_sidebar\">从侧边栏移除</string>\n  <string name=\"unpinned_from_sidebar\">已从侧边栏移除</string>\n  <string name=\"dialog_select_country\">国家/地区</string>\n  <string name=\"settings_language_country\">语言/国家</string>\n  <string name=\"share_embed_link\">分享嵌入链接</string>\n  <string name=\"card_text_scroll_factor\">卡片文本滚动速度</string>\n  <string name=\"preferred_update_source\">选择更新源</string>\n  <string name=\"hide_shorts\">隐藏订阅分区的短视频</string>\n  <string name=\"hide_shorts_channel\">隐藏频道分区的短视频</string>\n  <string name=\"key_remapping\">按键重映射</string>\n  <string name=\"screen_dimming\">屏幕调光</string>\n  <string name=\"removed_from_playback_queue\">已从播放队列中移除</string>\n  <string name=\"added_to_playback_queue\">已添加至播放队列</string>\n  <string name=\"add_remove_from_playback_queue\">添加至播放队列或从中移除</string>\n  <string name=\"add_to_playback_queue\">添加至播放队列</string>\n  <string name=\"remove_from_playback_queue\">从播放队列中移除</string>\n  <string name=\"proxy_enabled\">代理已启用</string>\n  <string name=\"proxy_disabled\">代理已禁用</string>\n  <string name=\"uploads_row_name\">上传</string>\n  <string name=\"playlists_row_name\">创建播放列表</string>\n  <string name=\"popular_uploads_row_name\">热门上传</string>\n  <string name=\"news_row_name\">新闻</string>\n  <string name=\"breaking_news_row_name\">突发新闻</string>\n  <string name=\"covid_news_row_name\">COVID-19新闻</string>\n  <string name=\"enable_voice_search\">开启全局搜索（需要固件支持）</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">订阅/退订频道</string>\n  <string name=\"app_backup_restore\">备份/恢复</string>\n  <string name=\"app_backup\">备份数据</string>\n  <string name=\"app_restore\">恢复数据</string>\n  <string name=\"skip_each_segment_once\">不再跳过任何片段</string>\n  <string name=\"disable_ok_long_press\">禁用确认键长按</string>\n  <string name=\"disable_ok_long_press_desc\">适用于确认键无法正常工作的故障控制器</string>\n  <string name=\"player_time_correction\">显示带播放速度校正的时间</string>\n  <string name=\"sponsor_color_markers\">进度条上显示彩色标记</string>\n  <string name=\"refresh_section\">刷新分区</string>\n  <string name=\"option_disabled\">禁用</string>\n  <string name=\"live_now_row_name\">直播中</string>\n  <string name=\"run_in_background\">后台播放</string>\n  <string name=\"settings_remote_control\">远程控制</string>\n  <string name=\"background_service_started\">后台服务已启动</string>\n  <string name=\"dialog_add_to\">添加至 %s</string>\n  <string name=\"dialog_remove_from\">从 %s 移除</string>\n  <string name=\"added_to\">已添加至 %s</string>\n  <string name=\"removed_from\">已从 %s 中移除</string>\n  <string name=\"video_preset_adaptive\">自适应</string>\n  <string name=\"content_block_preview_recap\">视频预览或回顾</string>\n  <string name=\"content_block_highlight\">精彩片段（书签）</string>\n  <string name=\"removed_from_history\">视频已从历史记录中删除</string>\n  <string name=\"remove_from_history\">从历史记录中删除</string>\n  <string name=\"upload_date\">上传日期</string>\n  <string name=\"upload_date_any\">所有时间</string>\n  <string name=\"upload_date_today\">今天</string>\n  <string name=\"upload_date_this_week\">本周</string>\n  <string name=\"upload_date_this_month\">本月</string>\n  <string name=\"upload_date_this_year\">今年</string>\n  <string name=\"double_refresh_rate\">双倍刷新率</string>\n  <string name=\"upload_date_last_hour\">最近1小时</string>\n  <string name=\"focus_on_search_results\">自动定位搜索结果</string>\n  <string name=\"context_menu\">快捷菜单</string>\n  <string name=\"add_remove_from_recent_playlist\">添加至最近的播放列表或从中移除</string>\n  <string name=\"hide_settings_section\">隐藏设置分区（危险！）</string>\n  <string name=\"volume\">音量 %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s 秒</string>\n  <string name=\"audio_shift_sec\">%s 秒</string>\n  <string name=\"ui_hide_timeout_sec\">%s 秒</string>\n  <string name=\"screen_dimming_timeout_min\">%s 分钟</string>\n  <string name=\"mark_all_channels_watched\">标记所有频道为已观看</string>\n  <string name=\"move_section_up\">上移分区</string>\n  <string name=\"move_section_down\">下移分区</string>\n  <string name=\"player_buttons\">设置播放器按钮</string>\n  <string name=\"action_playlist_add\">添加至播放列表</string>\n  <string name=\"action_video_stats\">视频统计</string>\n  <string name=\"action_subscribe\">订阅</string>\n  <string name=\"action_channel\">打开频道</string>\n  <string name=\"action_pip\">画中画</string>\n  <string name=\"action_video_info\">视频描述</string>\n  <string name=\"action_screen_off\">息屏</string>\n  <string name=\"action_sound_off\">静音</string>\n  <string name=\"action_playback_queue\">播放队列</string>\n  <string name=\"action_video_speed\">播放速度</string>\n  <string name=\"action_subtitles\">字幕</string>\n  <string name=\"action_like\">点赞</string>\n  <string name=\"action_dislike\">点踩</string>\n  <string name=\"action_play_pause\">播放/暂停</string>\n  <string name=\"action_repeat_mode\">播放模式</string>\n  <string name=\"action_next\">下一个视频</string>\n  <string name=\"action_previous\">上一个视频</string>\n  <string name=\"action_high_quality\">视频画质</string>\n  <string name=\"content_block_no_skipping_mode\">无跳过模式</string>\n  <string name=\"content_block_action_type\">选择操作</string>\n  <string name=\"content_block_action_none\">无操作</string>\n  <string name=\"content_block_action_only_skip\">仅跳过</string>\n  <string name=\"content_block_action_toast\">带通知的跳过</string>\n  <string name=\"content_block_action_dialog\">显示确认对话框</string>\n  <string name=\"content_block_filler\">不相干内容（凑时长）</string>\n  <string name=\"keyboard_auto_show\">自动显示键盘</string>\n  <string name=\"cancel_dialog\">取消</string>\n  <string name=\"msg_player_error_source2\">URL无法访问或设备​​时间不正确。\\n通常是应用BUG。</string>\n  <string name=\"msg_player_error_video_source\">视频URL无法访问或设备时间不正确。\\n通常是应用BUG。</string>\n  <string name=\"msg_player_error_audio_source\">音频URL无法访问或设备时间不正确。\\n通常是应用BUG。</string>\n  <string name=\"msg_player_error_subtitle_source\">字幕URL无法访问或设备时间不正确。\\n通常是应用BUG。</string>\n  <string name=\"hide_upcoming\">隐藏订阅分区的即将上线</string>\n  <string name=\"hide_upcoming_channel\">隐藏频道分区的即将上线</string>\n  <string name=\"hide_upcoming_home\">隐藏首页的即将上线</string>\n  <string name=\"search_background_playback\">搜索/浏览频道时后台播放</string>\n  <string name=\"trending_row_name\">热门</string>\n  <string name=\"player_seek_type\">快进方式</string>\n  <string name=\"player_seek_regular\">常规</string>\n  <string name=\"player_seek_confirmation_pause\">带确认（快进时暂停）</string>\n  <string name=\"player_seek_confirmation_play\">带确认（快进时播放）</string>\n  <string name=\"hide_shorts_from_home\">隐藏首页的短视频</string>\n  <string name=\"finish_on_disconnect\">手机/平板断开连接后关闭应用</string>\n  <string name=\"update_error\">更新错误</string>\n  <string name=\"hide_shorts_from_history\">历史记录隐藏短视频</string>\n  <string name=\"hide_shorts_from_trending\">热门分区隐藏短视频</string>\n  <string name=\"disable_screensaver\">禁用屏保</string>\n  <string name=\"description_not_found\">未找到视频描述</string>\n  <string name=\"proxy_port_hint\">代理端口</string>\n  <string name=\"proxy_host_hint\">代理主机或IP</string>\n  <string name=\"proxy_username_hint\">代理用户名</string>\n  <string name=\"proxy_password_hint\">代理密码</string>\n  <string name=\"proxy_type\">代理类型</string>\n  <string name=\"enable_web_proxy\">使用网络代理</string>\n  <string name=\"proxy_not_supported\">使用网络代理（需要Android 4.4及以上版本）</string>\n  <string name=\"proxy_test_btn\">测试</string>\n  <string name=\"proxy_settings_title\">代理服务器设置</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">第 #%d 次测试：已取消。</string>\n  <string name=\"proxy_type_invalid\">未设置代理类型。</string>\n  <string name=\"proxy_host_invalid\">未设置代理主机。</string>\n  <string name=\"proxy_port_invalid\">代理端口无效，必须大于0。</string>\n  <string name=\"proxy_credentials_invalid\">用户名或密码无效。</string>\n  <string name=\"proxy_test_aborted\">测试已中止，请先修复代理设置。</string>\n  <string name=\"proxy_application_aborted\">请先更正代理设置。</string>\n  <string name=\"proxy_test_start\">第 #%d 次测试：%s …</string>\n  <string name=\"proxy_test_error\">第 #%d 次错误：%s</string>\n  <string name=\"proxy_test_status\">第 #%d 次状态：%s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">配置文件地址（*.ovpn）</string>\n  <string name=\"enable_openvpn\">使用OpenVPN</string>\n  <string name=\"openvpn_settings_title\">OpenVPN设置</string>\n  <string name=\"openvpn_application_aborted\">请先正确设置OpenVPN。</string>\n  <string name=\"openvpn_test_aborted\">测试已中止，请先修复OpenVPN设置。</string>\n  <string name=\"openvpn_address_invalid\">未设置OpenVPN配置地址。</string>\n  <string name=\"internet_censorship\">网络代理</string>\n  <string name=\"rename_section\">重命名此分区</string>\n  <string name=\"simple_edit_value_hint\">输入值</string>\n  <string name=\"seek_interval\">快进间隔</string>\n  <string name=\"seek_interval_sec\">%s 秒</string>\n  <string name=\"subtitle_system\">系统样式（系统设置&gt;无障碍）</string>\n  <string name=\"subtitle_scale\">字幕缩放</string>\n  <string name=\"audio_sync_fix_desc\">另一种同步音频/视频的方法。虽然已被视为过时，但在某些情况下可能有所帮助。</string>\n  <string name=\"audio_sync_fix\">音频同步修复</string>\n  <string name=\"ambilight_ratio_fix_desc\">修复背光失效。修复视频比例或缩放错误。修复截图为空白。将影响性能!</string>\n  <string name=\"ambilight_ratio_fix\">背光/比例/缩放/截图修复</string>\n  <string name=\"force_legacy_codecs_desc\">显著提升低端设备的性能。最高分辨率为720p。</string>\n  <string name=\"force_legacy_codecs\">强制使用传统编解码器（720P）</string>\n  <string name=\"live_stream_fix_desc\">警告：此调整会禁用直播回放功能。显著提升低端设备的直播性能。最高分辨率为1080p。</string>\n  <string name=\"live_stream_fix_4k_desc\">警告：此调整会禁用直播回放功能。显著提升直播性能。最高分辨率为4K。</string>\n  <string name=\"live_stream_fix\">直播流修复（1080P）</string>\n  <string name=\"live_stream_fix_4k\">直播流修复（4K）</string>\n  <string name=\"playback_notifications_fix_desc\">隐藏修订通知。在基于AOSP的固件上可能很有用。</string>\n  <string name=\"playback_notifications_fix\">禁用播放通知</string>\n  <string name=\"amlogic_fix_desc\">修复Amlogic设备的丢帧问题。</string>\n  <string name=\"amlogic_fix\">修复Amlogic设备播放1080p\\@60fps视频的问题。</string>\n  <string name=\"tunneled_video_playback_desc\">注意：暂停功能可能无法正常工作！隧道式视频播放可带来更好的音频/视频同步（AV同步）和更流畅的播放等好处。需要Android 5及以上版本。</string>\n  <string name=\"tunneled_video_playback\">隧道视频播放（Android 5及以上版本）</string>\n  <string name=\"master_volume\">总音量</string>\n  <string name=\"volume_limit\">音量限制</string>\n  <string name=\"player_volume\">音量</string>\n  <string name=\"play_video\">播放</string>\n  <string name=\"remember_position_of_short_videos\">记住短视频位置（小于5分钟）</string>\n  <string name=\"remember_position_of_live_videos\">记住直播位置</string>\n  <string name=\"player_show_tooltips\">显示按钮提示</string>\n  <string name=\"action_like_unset\">点赞已清除</string>\n  <string name=\"action_dislike_unset\">点踩已清除</string>\n  <string name=\"various_buttons\">主窗口顶部的按钮</string>\n  <string name=\"not_compatible_with\">与所选项不兼容</string>\n  <string name=\"subtitle_position\">字幕偏移</string>\n  <string name=\"pressing_home\">按主页键</string>\n  <string name=\"pressing_home_back\">按主页或返回键</string>\n  <string name=\"pressing_back\">按返回键</string>\n  <string name=\"player_number_key_seek\">使用数字键快进</string>\n  <string name=\"save_remove_playlist\">添加播放列表至播放列表分区或从中移除</string>\n  <string name=\"save_playlist\">添加播放列表至播放列表分区</string>\n  <string name=\"remove_playlist\">从播放列表分区中永久移除播放列表</string>\n  <string name=\"removed_from_playlists\">已从播放列表分区移除</string>\n  <string name=\"saved_to_playlists\">已添加至播放列表分区</string>\n  <string name=\"create_playlist\">新建播放列表</string>\n  <string name=\"add_video_to_new_playlist\">添加至新播放列表</string>\n  <string name=\"cant_delete_empty_playlist\">无法删除空播放列表</string>\n  <string name=\"playlist\">播放列表</string>\n  <string name=\"rename_playlist\">重命名播放列表</string>\n  <string name=\"cant_rename_empty_playlist\">无法重命名空播放列表</string>\n  <string name=\"cant_rename_foreign_playlist\">无法重命名外部播放列表</string>\n  <string name=\"cant_save_playlist\">无法添加此播放列表</string>\n  <string name=\"enter_value\">请输入任意非空字符</string>\n  <string name=\"dialog_add_remove_from\">添加至 %s 或从中移除</string>\n  <string name=\"lb_playback_controls_skip_next\">跳转至下一个视频</string>\n  <string name=\"lb_playback_controls_skip_previous\">跳过上集回顾/返回起始位置</string>\n  <string name=\"alt_presets_behavior\">备用预设操作（低带宽）</string>\n  <string name=\"alt_presets_behavior_desc\">应用将尽量匹配预设带宽而不是分辨率、帧率和编解码器。</string>\n  <string name=\"sleep_timer\">启用睡眠倒计时（一小时内无操作）</string>\n  <string name=\"sleep_timer_desc\">一小时内无操作后自动暂停播放</string>\n  <string name=\"disable_vsync\">禁用切换至垂直同步</string>\n  <string name=\"disable_vsync_desc\">禁用与显示器同步的垂直同步信号调整，通过释放CPU资源来提高低端设备性能。</string>\n  <string name=\"skip_codec_profile_check\">跳过编解码器的画质检查</string>\n  <string name=\"skip_codec_profile_check_desc\">开始播放视频时不检查编码格式支持情况。对于有缺陷的固件可能有帮助。</string>\n  <string name=\"force_sw_codec\">强制软件解码</string>\n  <string name=\"force_sw_codec_desc\">可播放几乎所有视频，但性能极差。</string>\n  <string name=\"playlist_order\">播放列表排序</string>\n  <string name=\"playlist_order_added_date_newer_first\">添加日期（新入先显示）</string>\n  <string name=\"playlist_order_added_date_older_first\">添加日期（旧入先显示）</string>\n  <string name=\"playlist_order_published_date_newer_first\">发布日期（新近先显示）</string>\n  <string name=\"playlist_order_published_date_older_first\">发布日期（久远先显示）</string>\n  <string name=\"playlist_order_popularity\">人气</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">无法对外部播放列表执行此操作</string>\n  <string name=\"owned_playlist_warning\">错误：此操作仅适用于自有播放列表</string>\n  <string name=\"content_block_alt_server\">使用备用服务器</string>\n  <string name=\"content_block_alt_server_desc\">如果SponsorBlock由于各种原因无法正常工作，请启用此选项。</string>\n  <string name=\"unset_stream_reminder\">取消直播提醒</string>\n  <string name=\"set_stream_reminder\">设置直播提醒</string>\n  <string name=\"playback_starts_shortly\">当直播视频准备好后将自动开始播放</string>\n  <string name=\"starting_stream\">开始直播…</string>\n  <string name=\"action_playlist_remove\">从播放列表中移除</string>\n  <string name=\"signin_view_title\">正在加载用户代码…</string>\n  <string name=\"signin_view_description\">请在网页 %s 输入以上代码完成登录。\\n注意：仅限Firefox或Chrome浏览器。</string>\n  <string name=\"signin_view_action_text\">完成</string>\n  <string name=\"require_checked\">需要 %s</string>\n  <string name=\"msg_press_again_to_exit\">再按一次退出</string>\n  <string name=\"player_remaining_time\">剩余： %s</string>\n  <string name=\"player_ending_time\">结束于 %s</string>\n  <string name=\"badge_new_content\">新内容</string>\n  <string name=\"badge_live\">直播</string>\n  <string name=\"add_device_view_description\">要连接设备，请在手机YouTube应用的“设置&gt;在电视上观看”页面输入以上代码 \\n注意：此功能仅适用于Gboard键盘！</string>\n  <string name=\"playback_controls_repeat_pause\">暂停循环播放</string>\n  <string name=\"playback_controls_repeat_list\">循环播放列表</string>\n  <string name=\"action_subscribe_off\">关闭订阅</string>\n  <string name=\"action_subscribe_on\">开启订阅</string>\n  <string name=\"skip_24_rate\">跳过24帧格式（修复影院模式）</string>\n  <string name=\"player_disable_suggestions\">禁用建议</string>\n  <string name=\"feedback\">反馈</string>\n  <string name=\"sources\">来源</string>\n  <string name=\"releases\">版本</string>\n  <string name=\"prefer_avc_over_vp9\">解码器选择优先级：AVC高于VP9</string>\n  <string name=\"open_chat\">聊天/评论</string>\n  <string name=\"open_comments\">打开评论</string>\n  <string name=\"place_chat_left\">将聊天放左侧</string>\n  <string name=\"use_alt_speech_recognizer\">备用语音识别</string>\n  <string name=\"time_format\">时间格式</string>\n  <string name=\"time_format_24\">24小时制</string>\n  <string name=\"time_format_12\">12小时制（上午/下午）</string>\n  <string name=\"use_alt_speech_recognizer_desc\">存在严重缺陷。仅在默认识别器出现问题时才使用。</string>\n  <string name=\"speech_recognizer\">语音识别</string>\n  <string name=\"speech_recognizer_system\">系统（首选）</string>\n  <string name=\"speech_recognizer_external_1\">例外1（存在严重缺陷，需要禁用麦克风访问权限才能使用）</string>\n  <string name=\"speech_recognizer_external_2\">例外2（存在严重缺陷）</string>\n  <string name=\"real_channel_icon\">在频道按钮上显示图标</string>\n  <string name=\"subtitle_yellow_semi_transparent\">黄字半透明背景</string>\n  <string name=\"subtitle_yellow_black\">黄字黑色背景</string>\n  <string name=\"player_pixel_ratio\">像素比例</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">深灰（单色）</string>\n  <string name=\"disable_mic_permission\">请禁用本应用的麦克风访问权限以便此识别器正常工作。</string>\n  <string name=\"repeat_mode_shuffle\">随机播放任意播放列表</string>\n  <string name=\"chat_left\">左侧</string>\n  <string name=\"chat_right\">右侧</string>\n  <string name=\"card_real_thumbnails\">用视频中的单帧替换缩略图</string>\n  <string name=\"card_content\">获取卡片缩略图</string>\n  <string name=\"thumb_quality_default\">默认</string>\n  <string name=\"thumb_quality_start\">视频开头</string>\n  <string name=\"thumb_quality_middle\">视频中间</string>\n  <string name=\"thumb_quality_end\">视频结尾</string>\n  <string name=\"content_block_status\">检查SponsorBlock服务器状态</string>\n  <string name=\"dearrow_status\">检查DeArrow服务器状态</string>\n  <string name=\"player_show_quality_info_bitrate\">在画质信息中显示码率</string>\n  <string name=\"player_speed_button_old_behavior\">恢复播放速度按钮的旧操作</string>\n  <string name=\"protect_settings_with_password\">使用密码保护所有设置</string>\n  <string name=\"enter_settings_password\">输入设置密码</string>\n  <string name=\"child_mode\">儿童模式</string>\n  <string name=\"child_mode_desc\">此模式下用户无法使用搜索功能，也无法查看任何推荐内容。设置分区将启用密码保护。</string>\n  <string name=\"lost_setting_warning\">应用设置将会更改。请务必提前备份设置。</string>\n  <string name=\"player_button_long_click\">长按此按钮查看更多选项</string>\n  <string name=\"pause_history\">暂停记录历史</string>\n  <string name=\"resume_history\">继续记录历史</string>\n  <string name=\"disable_history\">禁用历史记录</string>\n  <string name=\"enable_history\">启用历史记录</string>\n  <string name=\"clear_history\">清空历史记录</string>\n  <string name=\"chapters\">章节</string>\n  <string name=\"card_multiline_subtitle\">多行字幕</string>\n  <string name=\"auto_frame_rate_modes\">支持模式</string>\n  <string name=\"loading\">加载中…</string>\n  <string name=\"hide_streams\">订阅分区隐藏直播流</string>\n  <string name=\"video_rotate\">旋转</string>\n  <string name=\"trending_searches\">热门搜索</string>\n  <string name=\"video_duration_any\">任意</string>\n  <string name=\"video_duration\">时长</string>\n  <string name=\"video_duration_under_4\">4分钟内</string>\n  <string name=\"video_duration_between_4_20\">4-20分钟</string>\n  <string name=\"video_duration_over_20\">20分钟以上</string>\n  <string name=\"content_type\">类型</string>\n  <string name=\"content_type_any\">任意</string>\n  <string name=\"content_type_video\">视频</string>\n  <string name=\"content_type_channel\">频道</string>\n  <string name=\"content_type_playlist\">播放列表</string>\n  <string name=\"content_type_movie\">电影</string>\n  <string name=\"video_features\">特性</string>\n  <string name=\"video_feature_any\">任意</string>\n  <string name=\"video_feature_live\">直播</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">排序</string>\n  <string name=\"sort_by_relevance\">相关度</string>\n  <string name=\"sort_by_views\">观看次数</string>\n  <string name=\"sort_by_date\">上传日期</string>\n  <string name=\"sort_by_rating\">评分</string>\n  <string name=\"clear_search_history\">清除搜索历史</string>\n  <string name=\"remove_from_subscriptions\">隐藏</string>\n  <string name=\"player_long_speed_list\">长速度列表</string>\n  <string name=\"alt_app_icon\">备用应用图标（需重启）</string>\n  <string name=\"network_stack\">首选 %s 网络协议栈</string>\n  <string name=\"player_network_stack\">网络引擎</string>\n  <string name=\"cronet_desc\">Cronet是Chromium的网络协议栈。可以降低延迟并提升网络性能，有助于解决缓冲问题。</string>\n  <string name=\"unlock_all_formats\">解锁所有视频格式</string>\n  <string name=\"unlock_all_formats_desc\">某些设备的固件会错误地将某些格式报告为不受支持，即使这些格式实际上是支持的。\\n（例如，1080p智能电视往往会将4k视频报告为不受支持）。</string>\n  <string name=\"okhttp_desc\">这个网络引擎速度最慢，但稳定性极佳。</string>\n  <string name=\"select_channel_section\">从ATV频道启动应用时打开对应分区</string>\n  <string name=\"enable_voice_search_desc\">官方应用将被所谓的“桥接器”取代。桥接器有助于将全局搜索请求转发到我们的应用。</string>\n  <string name=\"enable_master_password\">启用主密码</string>\n  <string name=\"enter_master_password\">输入主密码</string>\n  <string name=\"disable_stream_buffer\">禁用直播流缓冲</string>\n  <string name=\"disable_stream_buffer_desc\">修复视频流延迟过大的问题。注意：视频流可能会出现卡顿。</string>\n  <string name=\"sony_frame_drop_fix\">丢帧修复 #1</string>\n  <string name=\"sony_frame_drop_fix_desc\">修复索尼电视和一些其他设备上的延迟问题。注意：可能存在音频同步问题。</string>\n  <string name=\"audio_language\">音频语言</string>\n  <string name=\"old_home_look\">恢复首页旧样式</string>\n  <string name=\"old_channel_look\">恢复频道分区旧样式</string>\n  <string name=\"hide_shorts_everywhere\">隐藏所有分区的短视频</string>\n  <string name=\"update_found\">发现更新</string>\n  <string name=\"volume_boost_warning\">任何超出限制的设置都可能损坏扬声器</string>\n  <string name=\"play_video_incognito\">隐私模式播放</string>\n  <string name=\"header_kids_home\">儿童</string>\n  <string name=\"player_section_playlist\">将当前分区内容作为播放列表</string>\n  <string name=\"header_trending\">热门</string>\n  <string name=\"screensaver\">屏保</string>\n  <string name=\"player_screen_off_timeout\">息屏超时</string>\n  <string name=\"old_update_notifications\">旧版更新通知</string>\n  <string name=\"dialog_notification\">使用弹窗通知更新</string>\n  <string name=\"mark_as_watched\">标记为已观看</string>\n  <string name=\"player_ui_animations\">启用播放器用户界面动画</string>\n  <string name=\"player_likes_count\">显示点赞/点踩数</string>\n  <string name=\"sorting_alphabetically2\">字母排序（快速，动画预览）</string>\n  <string name=\"sorting_default\">默认（快速，动画预览）</string>\n  <string name=\"content_block_exclude_channel\">从SponsorBlock中排除此频道</string>\n  <string name=\"content_block_stop_excluding_channel\">停止从SponsorBlock中排除此频道</string>\n  <string name=\"player_chapter_notification\">使用弹出通知快速浏览章节</string>\n  <string name=\"subtitle_remember\">仅在当前频道启用字幕</string>\n  <string name=\"amazon_frame_drop_fix\">丢帧修复 #2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">专为Amazon电视棒设计。也可能适用于其他设备。</string>\n  <string name=\"default_stack_desc\">内置网络引擎。在某些情况下稳定性更好。</string>\n  <string name=\"item_postion\">位于</string>\n  <string name=\"player_screen_off_dimming\">息屏亮度</string>\n  <string name=\"screensaver_timout\">屏保超时</string>\n  <string name=\"screensaver_dimming\">屏保亮度</string>\n  <string name=\"player_ui_on_next\">切换至下一个视频时显示播放器界面</string>\n  <string name=\"autogenerated\">自动生成</string>\n  <string name=\"player_auto_volume\">自动音量调节</string>\n  <string name=\"header_shorts\">短视频</string>\n  <string name=\"player_global_focus\">在播放器按钮行间无缝导航</string>\n  <string name=\"remember_position_subscriptions\">记住上次在订阅中浏览的位置</string>\n  <string name=\"msg_player_unknown_error\">未知错误</string>\n  <string name=\"header_notifications\">通知</string>\n  <string name=\"disable_search_history\">禁用搜索历史</string>\n  <string name=\"unlock_high_bitrate_formats\">解锁1080p高码率vp9格式</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">解锁高码率mp4a格式</string>\n  <string name=\"color_scheme_blue\">蓝色</string>\n  <string name=\"color_scheme_dark_blue\">深蓝</string>\n  <string name=\"player_speed_per_channel\">每个频道</string>\n  <string name=\"disable_popular_searches\">禁用热门搜索查询</string>\n  <string name=\"multi_profiles\">每个账号使用单独的设置</string>\n  <string name=\"protect_account_with_password\">使用密码保护此账号</string>\n  <string name=\"enter_account_password\">输入账号密码</string>\n  <string name=\"show_connect_messages\">显示连接信息</string>\n  <string name=\"prefer_avc_over_vp9_desc\">警告：最高支持1080P</string>\n  <string name=\"play_next\">播放下一集</string>\n  <string name=\"hide_watched_from_subscriptions\">隐藏订阅中已观看的视频</string>\n  <string name=\"hide_watched_from_notifications\">隐藏通知中已观看的视频</string>\n  <string name=\"hide_unwanted_content\">隐藏内容</string>\n  <string name=\"hide_watched_from_home\">隐藏首页中已观看的视频</string>\n  <string name=\"remote_control_permission\">后台远程控制需要悬浮窗权限</string>\n  <string name=\"login_from_browser\">从浏览器登录</string>\n  <string name=\"disable_remote_history\">使用远程控制时禁用历史记录</string>\n  <string name=\"keyboard_fix\">防止确认键弹出键盘（G20s及其他型号）</string>\n  <string name=\"nothing_found\">未找到任何结果</string>\n  <string name=\"auto_frame_rate_desc\">此项可以消除镜头快速移动场景（例如体育赛事直播）产生的抖动。</string>\n  <string name=\"channel_filter_hint\">频道筛选</string>\n  <string name=\"player_loop_shorts\">短视频循环播放</string>\n  <string name=\"player_quick_shorts_skip\">使用左/右键跳过短视频</string>\n  <string name=\"channels_filter\">频道内筛选器</string>\n  <string name=\"channel_search_bar\">频道内搜索栏</string>\n  <string name=\"header_sports\">体育</string>\n  <string name=\"keep_finished_activities\">保留结束的活动</string>\n  <string name=\"disable_channels_service\">禁用频道服务</string>\n  <string name=\"replace_titles\">替换标题</string>\n  <string name=\"enable\">启用</string>\n  <string name=\"more_info\">更多信息</string>\n  <string name=\"about_sponsorblock\">关于SponsorBlock</string>\n  <string name=\"about_dearrow\">关于DeArrow</string>\n  <string name=\"replace_thumbnails\">替换缩略图</string>\n  <string name=\"crowdsourced_thumbnails\">众包缩略图</string>\n  <string name=\"crowdsoursed_titles\">众包标题</string>\n  <string name=\"dearrow_not_submitted_thumbs\">缩略图来源（如果没有DeArrow的投稿）</string>\n  <string name=\"pitch_effect\">音调效果</string>\n  <string name=\"fullscreen_mode\">全屏模式（无系统栏）</string>\n  <string name=\"player_only_mode\">如果视频在应用外打开则仅显示播放器</string>\n  <string name=\"pinned_channel_rows\">将置顶频道显示为一行</string>\n  <string name=\"prefer_ipv4\">首选IPv4 DNS</string>\n  <string name=\"prefer_ipv4_desc\">可以修复应用完全无法运行的情况。\\n注意：可能会导致应用卡顿和崩溃（尤其是在Android 8设备或Dune HD上）。</string>\n  <string name=\"video_buffer_size_lowest\">最低</string>\n  <string name=\"video_buffer_size_highest\">最高</string>\n  <string name=\"recommended\">推荐</string>\n  <string name=\"import_subscriptions_group\">导入</string>\n  <string name=\"you_liked\">你点赞过的</string>\n  <string name=\"oculus_quest_fix\">修复Oculus Quest</string>\n  <string name=\"unknown_source_error\">未知来源错误</string>\n  <string name=\"unknown_renderer_error\">未知渲染器错误</string>\n  <string name=\"without_picture\">无图</string>\n  <string name=\"playback_buffering_fix\">播放缓冲修复</string>\n  <string name=\"auto_history\">自动（使用账号设定）</string>\n  <string name=\"not_supported_by_device\">该设备不支持此功能</string>\n  <string name=\"original_lang\">初始</string>\n  <string name=\"speech_engine\">语音搜索引擎</string>\n  <string name=\"remember_position_pinned\">记住上次在置顶播放列表中浏览的位置</string>\n  <string name=\"hide_watched_from_watch_later\">隐藏稍后观看列表中已观看的视频</string>\n  <string name=\"player_quick_skip_videos\">使用左/右键跳过常规视频</string>\n  <string name=\"video_flip\">翻转（镜像）</string>\n  <string name=\"player_extra_long_speed_list\">超长速度列表</string>\n  <string name=\"long_press_for_settings\">长按进入设置</string>\n  <string name=\"long_press_for_options\">长按查看选项</string>\n  <string name=\"video_disabled\">视频已被禁止</string>\n  <string name=\"hide_mixes\">隐藏音乐合辑</string>\n  <string name=\"card_preview_muted\">无声视频</string>\n  <string name=\"card_preview_full\">有声视频</string>\n  <string name=\"card_preview\">卡片预览</string>\n  <string name=\"paid_content_notification\">付费内容通知</string>\n  <string name=\"local_backup\">本地备份</string>\n  <string name=\"search_exit_shortcut\">退出搜索</string>\n  <string name=\"new_subscriptions_group\">新群组</string>\n  <string name=\"rename_group\">重命名订阅群组</string>\n  <string name=\"screen_dimming_amount\">屏幕调光量</string>\n  <string name=\"screen_dimming_timeout\">屏幕调光超时</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">禁用CC字幕</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">启用CC字幕</string>\n  <string name=\"my_videos\">我的视频</string>\n  <string name=\"player_audio_focus\">音频聚焦（检测到其他播放器时暂停）</string>\n  <string name=\"card_unlocalized_titles\">未本地化的视频标题</string>\n  <string name=\"disable_network_error_fixing_desc\">如果你正在使用VPN，可能需要启用此选项。</string>\n  <string name=\"applying_fix\">请勿关闭播放器。正在应用修复程序…</string>\n  <string name=\"auto_backup\">自动备份（每天）</string>\n  <string name=\"device_specific_backup\">仅备份此设备</string>\n  <string name=\"suggestions\">建议</string>\n  <string name=\"skip_shorts\">跳过短视频</string>\n  <string name=\"place_comments_left\">将评论放左侧</string>\n  <string name=\"premium_users_only\">仅限会员用户。修复视频格式列表不完整的问题。</string>\n  <string name=\"playlists_rows\">将播放列表分区显示为行</string>\n  <string name=\"add_to_subscriptions_group\">添加至订阅群组或从中移除</string>\n  <string name=\"disable_network_error_fixing\">禁用自动网络错误修复</string>\n  <string name=\"player_global_focus_desc\">此功能的影响：在播放器按钮行之间导航时，哪个按钮会获得焦点。</string>\n  <string name=\"repeat_mode_reverse_list\">倒序播放播放列表或频道视频</string>\n  <string name=\"calm_msg\">我们正在修复此问题。请不时检查更新。</string>\n  <string name=\"dialog_block_channel\">屏蔽频道</string>\n  <string name=\"dialog_unblock_channel\">解除屏蔽频道</string>\n  <string name=\"confirm_block_channel\">隐藏 %s 的所有内容？</string>\n  <string name=\"channel_blocked\">频道已屏蔽</string>\n  <string name=\"channel_unblocked\">频道已解除屏蔽</string>\n  <string name=\"header_blocked_channels\">被屏蔽频道</string>\n  <string name=\"msg_no_blocked_channels\">未屏蔽频道</string>\n  <string name=\"player_toggle_speed\">切换播放速度开/关</string>\n  <string name=\"unpin_group_from_sidebar\">移除订阅群组</string>\n  <string name=\"context_menu_sorting\">快捷菜单排序</string>\n  <string name=\"hide_shorts_from_search\">搜索结果隐藏短视频</string>\n  <string name=\"remove_playlist_fmt\">永久移除 %s ？</string>\n  <string name=\"create_playlist_note\">注意：此项不会显示在 YouTube 应用中</string>\n  <string name=\"play_from_start\">从头开始播放</string>\n  <string name=\"auto_backup_category\">自动备份</string>\n  <string name=\"once_a_day\">每天</string>\n  <string name=\"once_a_week\">每周</string>\n  <string name=\"once_a_month\">每月</string>\n  <string name=\"action_debug_info\">调试信息</string>\n  <string name=\"player_chapter_notification2\">点击通知即可切换到下一章节</string>\n  <string name=\"player_quick_shorts_skip_alt\">使用上/下键跳过短视频</string>\n  <string name=\"player_quick_skip_videos_alt\">使用上/下键跳过常规视频</string>\n  <string name=\"prefer_google_dns\">首选Google DNS</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">不要为了适应对话框而调整视频大小</string>\n  <string name=\"typing_corrections\">输入文本时禁用自动纠错</string>\n  <string name=\"suggestions_horizontally_scrolled\">横向滚动建议</string>\n  <string name=\"stable_restore\">回滚至稳定版本（所有数据将丢失）</string>\n  <string name=\"player_time_stretching\">音频时间伸展</string>\n  <string name=\"player_time_stretching_desc\">变速时保持语音自然。可能会显著降低某些设备的性能。</string>\n  <string name=\"queue_respects_playback_mode\">队列遵循播放模式</string>\n  <string name=\"ignore_short_segments\">忽略短暂片段</string>\n  <string name=\"install_bridge\">安装ATV/Amazon桥接器</string>\n  <string name=\"fix_empty_subs_and_channels\">修复空白订阅和频道</string>\n  <string name=\"local_backup_not_supported\">由于系统限制，Android TV 11及以上版本不支持自动备份。请改用Google云端硬盘。</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/values-zh-rTW/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">首頁</string>\n  <string name=\"title_search\">搜尋</string>\n  <string name=\"header_subscriptions\">訂閱內容</string>\n  <string name=\"header_history\">觀看紀錄</string>\n  <string name=\"header_music\">音樂</string>\n  <string name=\"header_news\">新聞</string>\n  <string name=\"header_gaming\">遊戲</string>\n  <string name=\"header_playlists\">播放清單</string>\n  <string name=\"header_settings\">設定</string>\n  <string name=\"header_channels\">訂閱頻道清單</string>\n  <string name=\"subscriptions_signin_title\">查看您喜歡的頻道最新資訊</string>\n  <string name=\"subscriptions_signin_subtitle\">登入檢視您的訂閱</string>\n  <string name=\"subscriptions_signin_button_text\">登入</string>\n  <string name=\"library_signin_to_show_more\">觀看您喜歡、儲存或訂閱的影片</string>\n  <string name=\"library_signin_subtitle\">登入以檢視您的影片</string>\n  <string name=\"action_signin\">登入</string>\n  <string name=\"title_video_formats\">影片格式</string>\n  <string name=\"title_audio_formats\">音訊格式</string>\n  <string name=\"subtitle_category_title\">字幕</string>\n  <string name=\"playback_queue_category_title\">播放佇列</string>\n  <string name=\"video_max_quality\">自動（最高畫質）</string>\n  <string name=\"audio_max_quality\">自動（最高音質）</string>\n  <string name=\"subtitles_disabled\">字幕關閉</string>\n  <string name=\"auto_frame_rate\">自動畫面更新率</string>\n  <string name=\"frame_rate_correction\">修正 fps：\\n%s</string>\n  <string name=\"category_background_playback\">背景播放</string>\n  <string name=\"not_implemented\">未實作</string>\n  <string name=\"option_background_playback_off\">關閉</string>\n  <string name=\"option_background_playback_pip\">子母畫面</string>\n  <string name=\"option_background_playback_only_audio\">只有聲音</string>\n  <string name=\"playback_settings\">播放品質設定</string>\n  <string name=\"video_speed\">播放速度</string>\n  <string name=\"resolution_switch\">畫質</string>\n  <string name=\"video_buffer\">影片緩衝</string>\n  <string name=\"video_buffer_size_none\">無</string>\n  <string name=\"video_buffer_size_low\">低</string>\n  <string name=\"video_buffer_size_med\">中</string>\n  <string name=\"video_buffer_size_high\">高</string>\n  <string name=\"update_changelog\">更新日誌</string>\n  <string name=\"install_update\">安裝更新</string>\n  <string name=\"section_is_empty\">糟糕！這裡什麼都沒有。</string>\n  <string name=\"dialog_add_to_playlist\">新增/刪除播放清單</string>\n  <string name=\"msg_signed_users_only\">僅限已登入的使用者</string>\n  <string name=\"msg_cant_load_content\">無法載入內容。\\n1）請開啟觀看紀錄。\\n2）檢查裝置的日期和時間。\\n3）檢查網路連線。\\n4）檢查您的代理伺服器設定。\\n5）嘗試登入您的帳戶。\\n6）也許這裡真的什麼都沒有。</string>\n  <string name=\"title_video_presets\">影片預設</string>\n  <string name=\"settings_accounts\">帳戶</string>\n  <string name=\"settings_left_panel\">編輯類別</string>\n  <string name=\"settings_themes\">主題</string>\n  <string name=\"settings_other\">其它</string>\n  <string name=\"settings_player\">播放器設定</string>\n  <string name=\"settings_language\">語言</string>\n  <string name=\"settings_linked_devices\">連結裝置</string>\n  <string name=\"settings_about\">關於</string>\n  <string name=\"dialog_account_list\">選擇帳戶</string>\n  <string name=\"dialog_account_none\">無</string>\n  <string name=\"dialog_remove_account\">登出</string>\n  <string name=\"dialog_add_account\">登入</string>\n  <string name=\"default_lang\">預設</string>\n  <string name=\"dialog_select_language\">語言</string>\n  <string name=\"subtitle_default\">預設</string>\n  <string name=\"subtitle_white_semi_transparent\">半透明背景</string>\n  <string name=\"subtitle_style\">字幕樣式</string>\n  <string name=\"subtitle_language\">字幕語言</string>\n  <string name=\"action_search\">開始搜尋</string>\n  <string name=\"settings_main_ui\">使用者介面</string>\n  <string name=\"dialog_main_ui\">使用者介面</string>\n  <string name=\"card_animated_previews\">動畫預覽</string>\n  <string name=\"web_site\">網站</string>\n  <string name=\"donation\">捐贈</string>\n  <string name=\"dialog_about\">關於</string>\n  <string name=\"dialog_player_ui\">影片播放器</string>\n  <string name=\"player_show_ui_on_pause\">暫停時顯示使用者介面</string>\n  <string name=\"player_pause_on_ok\">OK 鍵暫停重播</string>\n  <string name=\"player_ok_button_behavior\">OK 按鍵行為</string>\n  <string name=\"player_only_ui\">只有使用者介面</string>\n  <string name=\"player_ui_and_pause\">使用者介面和暫停</string>\n  <string name=\"player_only_pause\">只有暫停</string>\n  <string name=\"check_for_updates\">檢查更新</string>\n  <string name=\"update_not_found\">已經是最新版本</string>\n  <string name=\"update_in_progress\">請稍候…</string>\n  <string name=\"player_ui_hide_behavior\">自動隱藏使用者介面</string>\n  <string name=\"option_never\">從不</string>\n  <string name=\"side_panel_sections\">設定側邊欄區塊</string>\n  <string name=\"boot_to_section\">預設開啟區塊</string>\n  <string name=\"large_ui\">大型使用者介面</string>\n  <string name=\"video_grid_scale\">影片方格比例</string>\n  <string name=\"scale_ui\">使用者介面比例</string>\n  <string name=\"color_scheme\">色彩設定</string>\n  <string name=\"color_scheme_default\">預設</string>\n  <string name=\"color_scheme_red_grey\">紅灰色</string>\n  <string name=\"color_scheme_red\">紅色</string>\n  <string name=\"color_scheme_dark_grey\">深灰色</string>\n  <string name=\"disable_update_check\">停用更新檢查</string>\n  <string name=\"show_again\">再次顯示</string>\n  <string name=\"check_updates_auto\">有新版本時通知我</string>\n  <string name=\"select_account_on_boot\">開機時自動選擇</string>\n  <string name=\"player_other\">其他</string>\n  <string name=\"player_full_date\">於描述框顯示完整日期</string>\n  <string name=\"open_channel\">開啟頻道</string>\n  <string name=\"open_playlist\">開啟播放清單</string>\n  <string name=\"not_interested\">沒興趣</string>\n  <string name=\"you_wont_see_this_video\">影片已經從推薦中刪除</string>\n  <string name=\"settings_search\">搜尋</string>\n  <string name=\"dialog_search\">搜尋</string>\n  <string name=\"instant_voice_search\">即時語音搜尋</string>\n  <string name=\"option_background_playback_behind\">稍後觀看</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">下一部影片資訊尚未載入</string>\n  <string name=\"card_multiline_title\">多行標題</string>\n  <string name=\"cards_style\">卡片樣式</string>\n  <string name=\"repeat_mode_all\">持續播放所有影片</string>\n  <string name=\"repeat_mode_one\">重複播放同一影片</string>\n  <string name=\"repeat_mode_pause\">影片播放結束後暫停（播放佇列除外）</string>\n  <string name=\"repeat_mode_pause_alt\">持續播放相同播放清單的影片</string>\n  <string name=\"repeat_mode_none\">播放一部影片後就停止（播放佇列除外）</string>\n  <string name=\"subscribed_to_channel\">已訂閱頻道</string>\n  <string name=\"unsubscribed_from_channel\">已取消訂閱</string>\n  <string name=\"subtitle_yellow_transparent\">黃色</string>\n  <string name=\"subtitle_white_transparent\">白色透明</string>\n  <string name=\"subtitle_white_black\">背景播放</string>\n  <string name=\"player_seek_preview\">跳轉時預覽</string>\n  <string name=\"color_scheme_dark_grey_oled\">深灰色（OLED）</string>\n  <string name=\"channels_section_sorting\">頻道區塊排序</string>\n  <string name=\"sorting_by_new_content\">新內容</string>\n  <string name=\"sorting_alphabetically\">字母排序</string>\n  <string name=\"sorting_last_viewed\">上次觀看</string>\n  <string name=\"player_pause_when_seek\">跳轉時暫停</string>\n  <string name=\"playlists_style\">播放清單區塊樣式</string>\n  <string name=\"playlists_style_grid\">網格</string>\n  <string name=\"playlists_style_rows\">列</string>\n  <string name=\"player_show_clock\">在彈出選單中顯示時間</string>\n  <string name=\"player_show_remaining_time\">顯示剩餘時間</string>\n  <string name=\"player_show_ending_time\">顯示結束時間</string>\n  <string name=\"open_channel_uploads\">開啟頻道上傳</string>\n  <string name=\"app_exit_shortcut\">關閉應用程式</string>\n  <string name=\"app_exit_none\">不要關閉</string>\n  <string name=\"app_double_back_exit\">按兩下返回鍵</string>\n  <string name=\"app_single_back_exit\">按一下返回鍵</string>\n  <string name=\"action_video_zoom\">影片縮放</string>\n  <string name=\"video_zoom\">影片縮放</string>\n  <string name=\"video_zoom_default\">預設</string>\n  <string name=\"video_zoom_fit_width\">符合寬度</string>\n  <string name=\"video_zoom_fit_height\">符合高度</string>\n  <string name=\"video_zoom_fit_both\">符合螢幕長或寬</string>\n  <string name=\"video_zoom_stretch\">延展</string>\n  <string name=\"color_scheme_teal\">藍綠色</string>\n  <string name=\"color_scheme_teal_oled\">藍綠色（OLED）</string>\n  <string name=\"player_seek_preview_none\">關閉</string>\n  <string name=\"player_seek_preview_single\">單一影格</string>\n  <string name=\"player_seek_preview_carousel\">循環</string>\n  <string name=\"player_seek_preview_carousel_slow\">循環（慢，用關鍵影格跳轉）</string>\n  <string name=\"player_seek_preview_carousel_fast\">循環（快，預覽畫面不精準）</string>\n  <string name=\"unsubscribe_from_channel\">取消訂閱</string>\n  <string name=\"card_title_lines_num\">卡片標題行數</string>\n  <string name=\"card_auto_scrolled_title\">自動捲動</string>\n  <string name=\"share_link\">分享連結</string>\n  <string name=\"subscribe_to_channel\">訂閱頻道</string>\n  <string name=\"wait_data_loading\">資料正在讀取中，請稍候…</string>\n  <string name=\"auto_frame_rate_pause\">暫停自動畫面更新率切換</string>\n  <string name=\"auto_frame_rate_applying\">套用自動畫面更新率 %sx%s\\@%s</string>\n  <string name=\"audio_shift\">音訊轉換</string>\n  <string name=\"player_remember_speed\">記住速度</string>\n  <string name=\"mark_channel_as_watched\">標記為已觀看</string>\n  <string name=\"channel_marked_as_watched\">頻道標記為已觀看</string>\n  <string name=\"dialog_add_device\">新增裝置</string>\n  <string name=\"dialog_remove_all_devices\">移除所有裝置</string>\n  <string name=\"device_link_enabled\">連結已啟用</string>\n  <string name=\"device_connected\">已連線至裝置「%s」</string>\n  <string name=\"device_disconnected\">已與裝置「%s」中斷連線</string>\n  <string name=\"settings_ui_scale\">介面縮放</string>\n  <string name=\"msg_restart_app\">請重新啟動應用程式來套用此設定</string>\n  <string name=\"settings_block\">內容封鎖</string>\n  <string name=\"msg_applying\">套用 %s 中…</string>\n  <string name=\"msg_done\">結束</string>\n  <string name=\"settings_general\">一般</string>\n  <string name=\"settings_video\">影片</string>\n  <string name=\"content_block_confirm_skip\">確認跳過</string>\n  <string name=\"content_block_categories\">跳過片段</string>\n  <string name=\"content_block_sponsor\">贊助商</string>\n  <string name=\"content_block_intro\">開場 / 中場動畫</string>\n  <string name=\"content_block_outro\">片尾</string>\n  <string name=\"content_block_interaction\">互動提醒（訂閱）</string>\n  <string name=\"content_block_self_promo\">無償 / 自我推廣</string>\n  <string name=\"content_block_music_off_topic\">非音樂片段</string>\n  <string name=\"confirm_segment_skip\">要跳過片段「%s」嗎？</string>\n  <string name=\"cancel_segment_skip\">取消跳過片段</string>\n  <string name=\"msg_skipping_segment\">正在跳過片段「%s」…</string>\n  <string name=\"content_block_notification_type\">通知類型</string>\n  <string name=\"content_block_notify_none\">沒有通知</string>\n  <string name=\"content_block_notify_toast\">Toast</string>\n  <string name=\"content_block_notify_dialog\">確認對話方塊</string>\n  <string name=\"return_to_launcher\">從 ATV 頻道 / 搜尋返回啟動器</string>\n  <string name=\"intent_force_close\">如果影片已經從外部開啟，則強制退出</string>\n  <string name=\"btn_confirm\">確認</string>\n  <string name=\"player_low_video_quality\">較低的影片畫質</string>\n  <string name=\"remote_session_closed\">遠端已經關閉</string>\n  <string name=\"msg_mode_switch_error\">無法切換顯示模式到「%s」</string>\n  <string name=\"msg_player_error\">播放器發生錯誤 %s。正在重新啟動播放…</string>\n  <string name=\"video_preset_disabled\">不使用預設規則</string>\n  <string name=\"video_preset_enabled\">下一段影片的格式將根據預設規則播放</string>\n  <string name=\"player_sleep_timer\">睡眠定時</string>\n  <string name=\"player_show_quality_info\">於控制列顯示畫質資訊</string>\n  <string name=\"player_remember_each_speed\">記住每部影片的速度</string>\n  <string name=\"player_remember_speed_none\">無</string>\n  <string name=\"player_remember_speed_all\">所有影片都相同</string>\n  <string name=\"player_remember_speed_each\">每部影片</string>\n  <string name=\"msg_player_error_source\">無法下載這部影片</string>\n  <string name=\"msg_player_error_renderer\">不支援選擇的影片格式。\\n請嘗試選擇其他格式。若沒有改善，請嘗試重新啟動裝置。</string>\n  <string name=\"msg_player_error_unexpected\">未知影片解碼器錯誤</string>\n  <string name=\"video_aspect\">影片長寬比</string>\n  <string name=\"player_tweaks\">開發人員選項</string>\n  <string name=\"header_uploads\">上傳</string>\n  <string name=\"player_show_global_clock\">固定在螢幕上顯示時鐘</string>\n  <string name=\"player_show_global_ending_time\">在螢幕上固定顯示影片結束時間</string>\n  <string name=\"uploads_old_look\">上傳區塊使用舊版外觀</string>\n  <string name=\"background_playback_activation\">背景播放（啟用）</string>\n  <string name=\"channels_old_look\">頻道區塊使用舊版外觀</string>\n  <string name=\"channels_auto_load\">自動載入頻道區塊內容</string>\n  <string name=\"return_to_background_video\">返回背景播放的影片</string>\n  <string name=\"pin_unpin_from_sidebar\">釘選 / 取消釘選到側邊欄</string>\n  <string name=\"pin_unpin_playlist\">釘選 / 取消釘選播放清單到側邊欄</string>\n  <string name=\"pin_unpin_channel\">釘選 / 取消釘選頻道到側邊欄</string>\n  <string name=\"pinned_to_sidebar\">已加入到側邊欄</string>\n  <string name=\"unpin_from_sidebar\">從側邊欄移除</string>\n  <string name=\"unpinned_from_sidebar\">已從側邊欄移除</string>\n  <string name=\"dialog_select_country\">國家</string>\n  <string name=\"settings_language_country\">語言/國家</string>\n  <string name=\"share_embed_link\">分享嵌入連結</string>\n  <string name=\"card_text_scroll_factor\">卡片文字捲動速度</string>\n  <string name=\"preferred_update_source\">選擇更新來源</string>\n  <string name=\"hide_shorts\">從訂閱頻道中隱藏 Shorts</string>\n  <string name=\"key_remapping\">按鍵重新對應</string>\n  <string name=\"screen_dimming\">螢幕調光</string>\n  <string name=\"removed_from_playback_queue\">已從播放佇列中刪除</string>\n  <string name=\"added_to_playback_queue\">已加入播放佇列</string>\n  <string name=\"add_remove_from_playback_queue\">從播放佇列中新增 / 刪除</string>\n  <string name=\"add_to_playback_queue\">加入播放佇列</string>\n  <string name=\"remove_from_playback_queue\">從播放佇列中刪除</string>\n  <string name=\"proxy_enabled\">已啟用 Proxy</string>\n  <string name=\"proxy_disabled\">已關閉 Proxy</string>\n  <string name=\"uploads_row_name\">上傳</string>\n  <string name=\"playlists_row_name\">建立的播放清單</string>\n  <string name=\"popular_uploads_row_name\">熱門上傳</string>\n  <string name=\"breaking_news_row_name\">即時新聞</string>\n  <string name=\"covid_news_row_name\">COVID-19 新聞</string>\n  <string name=\"enable_voice_search\">啟用全域搜尋（需要韌體支援）</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">訂閱 / 取消訂閱頻道</string>\n  <string name=\"app_backup_restore\">備份 / 還原</string>\n  <string name=\"skip_each_segment_once\">不要再跳過片段</string>\n  <string name=\"disable_ok_long_press\">停用長按 OK 按鈕</string>\n  <string name=\"disable_ok_long_press_desc\">此功能是針對控制器故障，OK 按鈕無法正常運作時使用。</string>\n  <string name=\"player_time_correction\">顯示與速度校正有關的時間</string>\n  <string name=\"sponsor_color_markers\">進度條上的彩色標記</string>\n  <string name=\"refresh_section\">重新整理區塊內容</string>\n  <string name=\"option_disabled\">停用</string>\n  <string name=\"live_now_row_name\">正在直播</string>\n  <string name=\"run_in_background\">背景播放</string>\n  <string name=\"settings_remote_control\">遙控器</string>\n  <string name=\"background_service_started\">背景服務已經啟動</string>\n  <string name=\"dialog_add_to\">新增到 %s</string>\n  <string name=\"dialog_remove_from\">從 %s 移除</string>\n  <string name=\"added_to\">已新增到 %s</string>\n  <string name=\"removed_from\">已從 %s 移除</string>\n  <string name=\"video_preset_adaptive\">自動調節</string>\n  <string name=\"content_block_preview_recap\">影片預覽或回顧</string>\n  <string name=\"content_block_highlight\">影片精華片段（書籤點）</string>\n  <string name=\"removed_from_history\">已從觀看紀錄移除影片</string>\n  <string name=\"remove_from_history\">從觀看紀錄中移除</string>\n  <string name=\"upload_date\">上傳日期</string>\n  <string name=\"upload_date_any\">不限時間</string>\n  <string name=\"upload_date_today\">今天</string>\n  <string name=\"upload_date_this_week\">本週</string>\n  <string name=\"upload_date_this_month\">本月</string>\n  <string name=\"upload_date_this_year\">今年</string>\n  <string name=\"double_refresh_rate\">加倍更新率</string>\n  <string name=\"upload_date_last_hour\">最近一小時</string>\n  <string name=\"focus_on_search_results\">自動切換至搜尋結果</string>\n  <string name=\"context_menu\">內容選單</string>\n  <string name=\"add_remove_from_recent_playlist\">從最近播放清單中增加/移除</string>\n  <string name=\"hide_settings_section\">隱藏設定區塊（危險！）</string>\n  <string name=\"volume\">音量 %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s 秒</string>\n  <string name=\"audio_shift_sec\">%s 秒</string>\n  <string name=\"ui_hide_timeout_sec\">%s 秒</string>\n  <string name=\"screen_dimming_timeout_min\">%s 分</string>\n  <string name=\"mark_all_channels_watched\">將所有頻道標記為已觀看</string>\n  <string name=\"move_section_up\">上移區塊</string>\n  <string name=\"move_section_down\">下移區塊</string>\n  <string name=\"player_buttons\">設定播放器按鈕</string>\n  <string name=\"action_playlist_add\">加入播放清單</string>\n  <string name=\"action_video_stats\">影片統計資料</string>\n  <string name=\"action_subscribe\">訂閱</string>\n  <string name=\"action_channel\">開啟頻道</string>\n  <string name=\"action_pip\">子母畫面</string>\n  <string name=\"action_video_info\">影片描述</string>\n  <string name=\"action_screen_off\">關閉螢幕</string>\n  <string name=\"action_playback_queue\">播放佇列</string>\n  <string name=\"action_video_speed\">影片速度</string>\n  <string name=\"action_subtitles\">字幕</string>\n  <string name=\"action_like\">喜歡</string>\n  <string name=\"action_dislike\">不喜歡</string>\n  <string name=\"action_play_pause\">播放 / 暫停</string>\n  <string name=\"action_repeat_mode\">播放模式</string>\n  <string name=\"action_next\">下一部影片</string>\n  <string name=\"action_previous\">前一部影片</string>\n  <string name=\"action_high_quality\">影片畫質</string>\n  <string name=\"content_block_no_skipping_mode\">不跳過模式</string>\n  <string name=\"content_block_action_type\">選擇動作</string>\n  <string name=\"content_block_action_none\">不作任何事</string>\n  <string name=\"content_block_action_only_skip\">直接跳過片段</string>\n  <string name=\"content_block_action_toast\">跳過並通知</string>\n  <string name=\"content_block_action_dialog\">顯示確認對話框</string>\n  <string name=\"content_block_filler\">離題（充數內容）</string>\n  <string name=\"keyboard_auto_show\">自動顯示鍵盤</string>\n  <string name=\"cancel_dialog\">取消</string>\n  <string name=\"msg_player_error_source2\">網址錯誤或裝置時間不正確。\\n通常是應用程式的問題。</string>\n  <string name=\"hide_upcoming\">隱藏訂閱頻道中的即將播出內容</string>\n  <string name=\"search_background_playback\">搜尋/瀏覽頻道時繼續於背景播放</string>\n  <string name=\"trending_row_name\">熱門影片</string>\n  <string name=\"player_seek_type\">跳轉行為</string>\n  <string name=\"player_seek_regular\">一般</string>\n  <string name=\"player_seek_confirmation_pause\">確認後再跳轉（快轉時暫停播放）</string>\n  <string name=\"player_seek_confirmation_play\">確認後再跳轉（快轉時維持播放）</string>\n  <string name=\"hide_shorts_from_home\">在首頁隱藏 Shorts</string>\n  <string name=\"finish_on_disconnect\">手機 / 平板電腦中斷連線後，關閉應用程式</string>\n  <string name=\"update_error\">更新錯誤</string>\n  <string name=\"hide_shorts_from_history\">在觀看紀錄中隱藏 Shorts</string>\n  <string name=\"disable_screensaver\">關閉螢幕保護程式</string>\n  <string name=\"description_not_found\">沒有找到影片描述</string>\n  <string name=\"proxy_port_hint\">Proxy 連接埠</string>\n  <string name=\"proxy_host_hint\">Proxy 主機名稱或 IP</string>\n  <string name=\"proxy_username_hint\">Proxy 使用者名稱</string>\n  <string name=\"proxy_password_hint\">Proxy 密碼</string>\n  <string name=\"proxy_type\">Proxy 類型</string>\n  <string name=\"enable_web_proxy\">使用 Web Proxy</string>\n  <string name=\"proxy_not_supported\">使用 Web Proxy（需要 Android 4.4 以上版本）</string>\n  <string name=\"proxy_test_btn\">測試</string>\n  <string name=\"proxy_settings_title\">Proxy 伺服器設定</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"proxy_test_cancelled\">測試 #%d：已取消。</string>\n  <string name=\"proxy_type_invalid\">Proxy 類型未設定。</string>\n  <string name=\"proxy_host_invalid\">Proxy 主機未設定。</string>\n  <string name=\"proxy_port_invalid\">Proxy 連接埠無效，必須 &gt; 0。</string>\n  <string name=\"proxy_credentials_invalid\">使用者名稱或密碼無效。</string>\n  <string name=\"proxy_test_aborted\">已中止測試，請先修正 Proxy 設定。</string>\n  <string name=\"proxy_application_aborted\">請先更正 Proxy 設定。</string>\n  <string name=\"proxy_test_start\">測試 #%d：%s…</string>\n  <string name=\"proxy_test_error\">錯誤 #%d：%s</string>\n  <string name=\"proxy_test_status\">結果 #%d：%s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">設定檔位置（*.ovpn）</string>\n  <string name=\"enable_openvpn\">使用 OpenVPN</string>\n  <string name=\"openvpn_settings_title\">OpenVPN 設定</string>\n  <string name=\"openvpn_application_aborted\">請先修正 OpenVPN 設定。</string>\n  <string name=\"openvpn_test_aborted\">已中止測試，請先修正 OpenVPN 設定。</string>\n  <string name=\"openvpn_address_invalid\">未設定 OpenVPN 設定檔位置。</string>\n  <string name=\"internet_censorship\">網路審查</string>\n  <string name=\"rename_section\">重新命名此區塊</string>\n  <string name=\"simple_edit_value_hint\">輸入值</string>\n  <string name=\"seek_interval\">跳轉間隔</string>\n  <string name=\"seek_interval_sec\">%s 秒</string>\n  <string name=\"subtitle_system\">系統樣式（Android 設定 &gt; 輔助功能）</string>\n  <string name=\"subtitle_scale\">字幕比例</string>\n  <string name=\"force_legacy_codecs_desc\">大幅改善低階裝置上的效能，最高解析度僅支援 720p。</string>\n  <string name=\"force_legacy_codecs\">強制使用舊版編解碼器（720p）</string>\n  <string name=\"live_stream_fix_desc\">大幅改善低階裝置上的直播串流播放效能，最高解析度僅支援 1080p。警告：開啟此選項後，串流將無法向前倒帶。</string>\n  <string name=\"live_stream_fix\">直播串流功能修正（1080p）</string>\n  <string name=\"playback_notifications_fix_desc\">隱藏軌道變更通知。對使用 AOSP 韌體的系統可能有用。</string>\n  <string name=\"playback_notifications_fix\">停用播放通知</string>\n  <string name=\"tunneled_video_playback_desc\">注意：可能無法正常暫停影片！啟用『隧道式播放』可提升影音同步與播放流暢度。（需 Android 5 以上版本）</string>\n  <string name=\"tunneled_video_playback\">隧道式播放（Android 5+）</string>\n  <string name=\"master_volume\">主音量</string>\n  <string name=\"volume_limit\">音量限制</string>\n  <string name=\"play_video\">播放</string>\n  <string name=\"remember_position_of_short_videos\">記住短片（少於 5 分鐘）的播放位置</string>\n  <string name=\"player_show_tooltips\">顯示按鈕工具提示</string>\n  <string name=\"various_buttons\">主視窗頂部的各種按鈕</string>\n  <string name=\"not_compatible_with\">所選選項不相容</string>\n  <string name=\"subtitle_position\">字幕下移</string>\n  <string name=\"pressing_home\">按首頁鍵</string>\n  <string name=\"pressing_home_back\">按首頁鍵或返回鍵</string>\n  <string name=\"pressing_back\">按返回鍵</string>\n  <string name=\"player_number_key_seek\">用數字鍵跳轉</string>\n  <string name=\"save_remove_playlist\">從播放清單區塊新增 / 刪除播放清單</string>\n  <string name=\"remove_playlist\">從播放清單區塊永久刪除播放清單</string>\n  <string name=\"removed_from_playlists\">已從播放清單區塊刪除</string>\n  <string name=\"saved_to_playlists\">已新增到播放清單區塊</string>\n  <string name=\"create_playlist\">建立播放清單</string>\n  <string name=\"add_video_to_new_playlist\">新增到新的播放清單</string>\n  <string name=\"cant_delete_empty_playlist\">無法刪除空的播放清單</string>\n  <string name=\"playlist\">播放清單</string>\n  <string name=\"rename_playlist\">重新命名播放清單</string>\n  <string name=\"cant_rename_empty_playlist\">無法重新命名空的播放清單</string>\n  <string name=\"cant_rename_foreign_playlist\">無法重新命名外部播放清單</string>\n  <string name=\"cant_save_playlist\">無法新增此播放清單</string>\n  <string name=\"enter_value\">輸入任何不為空的值</string>\n  <string name=\"dialog_add_remove_from\">從 %s 新增 / 刪除</string>\n  <string name=\"lb_playback_controls_skip_next\">跳過下一個</string>\n  <string name=\"lb_playback_controls_skip_previous\">跳過上一個 / 倒回開始位置</string>\n  <string name=\"sleep_timer\">啟用睡眠定時器（一小時）</string>\n  <string name=\"sleep_timer_desc\">如果使用者在一小時內沒有使用遙控器，則暫停播放</string>\n  <string name=\"disable_vsync\">停用垂直同步對齊</string>\n  <string name=\"disable_vsync_desc\">此選項會停止將影格與顯示器的垂直同步訊號對齊。這樣可釋放 CPU 資源來提高低階裝置的效能。</string>\n  <string name=\"skip_codec_profile_check\">跳過編解碼器規格等級檢查</string>\n  <string name=\"skip_codec_profile_check_desc\">播放影片時不檢查解碼支援度。若裝置韌體有缺陷，開啟此選項可能有幫助。</string>\n  <string name=\"force_sw_codec\">強制使用軟體解碼器</string>\n  <string name=\"force_sw_codec_desc\">幾乎可以播放任何影片，但效能很差。</string>\n  <string name=\"playlist_order\">排序播放清單</string>\n  <string name=\"playlist_order_added_date_newer_first\">新增日期（新的優先）</string>\n  <string name=\"playlist_order_added_date_older_first\">新增日期（舊的優先）</string>\n  <string name=\"playlist_order_published_date_newer_first\">發布日期（新的優先）</string>\n  <string name=\"playlist_order_published_date_older_first\">發布日期（舊的優先）</string>\n  <string name=\"playlist_order_popularity\">觀看次數</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">無法對外部的播放清單執行此操作</string>\n  <string name=\"owned_playlist_warning\">錯誤：此操作僅適用於自有的播放清單</string>\n  <string name=\"content_block_alt_server\">使用備用伺服器</string>\n  <string name=\"content_block_alt_server_desc\">如果 SponsorBlock 因其他原因拒絕工作，請啟用此選項。</string>\n  <string name=\"unset_stream_reminder\">取消直播提醒</string>\n  <string name=\"set_stream_reminder\">設定直播提醒</string>\n  <string name=\"playback_starts_shortly\">當直播準備好時會自動開始播放</string>\n  <string name=\"starting_stream\">即將開始直播…</string>\n  <string name=\"action_playlist_remove\">從播放清單中刪除</string>\n  <string name=\"signin_view_title\">正在載入使用者代碼…</string>\n  <string name=\"signin_view_description\">若要登入，請在頁面 %s 輸入此代碼\\n註：僅對 Firefox 或 Chrome 瀏覽器有效。</string>\n  <string name=\"signin_view_action_text\">完成</string>\n  <string name=\"require_checked\">需要「%s」</string>\n  <string name=\"msg_press_again_to_exit\">再按一次即可結束本程式</string>\n  <string name=\"player_remaining_time\">剩餘：%s</string>\n  <string name=\"player_ending_time\">結束於 %s</string>\n  <string name=\"badge_new_content\">新內容</string>\n  <string name=\"badge_live\">直播</string>\n  <string name=\"playback_controls_repeat_pause\">重播暫停</string>\n  <string name=\"playback_controls_repeat_list\">重播清單</string>\n  <string name=\"action_subscribe_off\">取消訂閱</string>\n  <string name=\"action_subscribe_on\">訂閱</string>\n  <string name=\"skip_24_rate\">略過 24fps 格式（真實戲院模式修復？）</string>\n  <string name=\"player_disable_suggestions\">停用建議</string>\n  <string name=\"feedback\">意見反應</string>\n  <string name=\"sources\">來源</string>\n  <string name=\"releases\">發佈</string>\n  <string name=\"prefer_avc_over_vp9\">解碼器選擇：偏好 avc 大過 vp9</string>\n  <string name=\"open_chat\">聊天/評論</string>\n  <string name=\"place_chat_left\">把聊天室放在左邊</string>\n  <string name=\"use_alt_speech_recognizer\">替代語音辨識器</string>\n  <string name=\"time_format\">時間格式</string>\n  <string name=\"time_format_24\">24 小時制</string>\n  <string name=\"time_format_12\">12 小時制（AM/PM）</string>\n  <string name=\"use_alt_speech_recognizer_desc\">有嚴重的缺陷。只在您的預設辨識器有問題時再使用。</string>\n  <string name=\"speech_recognizer\">語音辨識器</string>\n  <string name=\"speech_recognizer_system\">系統（偏好）</string>\n  <string name=\"speech_recognizer_external_1\">外部引擎 1（極不穩定，需關閉麥克風權限才能使用）</string>\n  <string name=\"speech_recognizer_external_2\">外部引擎 2（極不穩定）</string>\n  <string name=\"real_channel_icon\">在頻道按鈕上顯示圖示</string>\n  <string name=\"subtitle_yellow_semi_transparent\">黃色帶半透明背景</string>\n  <string name=\"subtitle_yellow_black\">黃色帶黑色背景</string>\n  <string name=\"player_pixel_ratio\">像素比例</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">深灰色（單色）</string>\n  <string name=\"disable_mic_permission\">請關閉應用程式的麥克風權限，這個辨識器才能正常使用。</string>\n  <string name=\"repeat_mode_shuffle\">隨機播放任何播放清單</string>\n  <string name=\"chat_left\">左邊</string>\n  <string name=\"chat_right\">右邊</string>\n  <string name=\"card_real_thumbnails\">使用影片中的一格畫面來取代縮圖</string>\n  <string name=\"card_content\">取得卡片縮圖來源</string>\n  <string name=\"thumb_quality_default\">預設</string>\n  <string name=\"thumb_quality_start\">影片的起始畫面</string>\n  <string name=\"thumb_quality_middle\">影片的中間畫面</string>\n  <string name=\"thumb_quality_end\">影片的結束畫面</string>\n  <string name=\"content_block_status\">檢查 SponsorBlock 狀態</string>\n  <string name=\"player_show_quality_info_bitrate\">在影片品質資訊中顯示位元率</string>\n  <string name=\"player_speed_button_old_behavior\">將速度按鈕還原成舊的行為</string>\n  <string name=\"protect_settings_with_password\">變更任何設定時都需要輸入密碼</string>\n  <string name=\"enter_settings_password\">請輸入設定畫面的密碼</string>\n  <string name=\"child_mode\">兒童模式</string>\n  <string name=\"child_mode_desc\">在這種模式下，使用者無法使用搜尋或看到任何推薦內容。設定頁面也會受到密碼保護。</string>\n  <string name=\"lost_setting_warning\">應用程式的設定將被改變。請確保您已經建立備份。</string>\n  <string name=\"player_button_long_click\">長按此按鈕即可設定更多選項。</string>\n  <string name=\"pause_history\">暫停觀看紀錄</string>\n  <string name=\"resume_history\">恢復觀看紀錄</string>\n  <string name=\"clear_history\">清除觀看紀錄</string>\n  <string name=\"chapters\">章節</string>\n  <string name=\"card_multiline_subtitle\">多行字幕</string>\n  <string name=\"auto_frame_rate_modes\">支援的模式</string>\n  <string name=\"loading\">正在載入…</string>\n  <string name=\"hide_streams\">從訂閱中隱藏串流</string>\n  <string name=\"video_rotate\">旋轉</string>\n  <string name=\"trending_searches\">熱門搜尋</string>\n  <string name=\"video_duration_any\">任何</string>\n  <string name=\"video_duration\">長度</string>\n  <string name=\"video_duration_under_4\">小於 4 分鐘</string>\n  <string name=\"video_duration_between_4_20\">4-20 分鐘</string>\n  <string name=\"video_duration_over_20\">超過 20 分鐘</string>\n  <string name=\"content_type\">類型</string>\n  <string name=\"content_type_any\">任何</string>\n  <string name=\"content_type_video\">影片</string>\n  <string name=\"content_type_channel\">頻道</string>\n  <string name=\"content_type_playlist\">播放清單</string>\n  <string name=\"content_type_movie\">電影</string>\n  <string name=\"video_features\">影片規格</string>\n  <string name=\"video_feature_any\">任何</string>\n  <string name=\"video_feature_live\">直播</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">排序方式</string>\n  <string name=\"sort_by_relevance\">相關性</string>\n  <string name=\"sort_by_views\">觀看次數</string>\n  <string name=\"sort_by_date\">上傳日期</string>\n  <string name=\"sort_by_rating\">評分</string>\n  <string name=\"clear_search_history\">清除搜尋紀錄</string>\n  <string name=\"remove_from_subscriptions\">隱藏</string>\n  <string name=\"player_long_speed_list\">較長的速度列表</string>\n  <string name=\"alt_app_icon\">替代應用程式圖示（需要重新啟動）</string>\n  <string name=\"not_recommend_channel\">不要推薦此頻道</string>\n  <string name=\"you_wont_see_this_channel\">您不會在推薦中再看到這個頻道</string>\n  <string name=\"live_stream_fix_4k_desc\">大幅改善直播串流播放效能，最高支援 4K 解析度。警告：開啟此選項後，串流將無法倒帶。</string>\n  <string name=\"live_stream_fix_4k\">直播串流功能修正（4K）</string>\n  <string name=\"network_stack\">偏好 %s 網路堆疊</string>\n  <string name=\"player_network_stack\">網路引擎</string>\n  <string name=\"cronet_desc\">Cronet 是 Chromium 的網路堆疊。Cronet 可以降低延遲並提升網路效能，有助於緩衝問題。</string>\n  <string name=\"unlock_all_formats\">解鎖所有影片格式</string>\n  <string name=\"unlock_all_formats_desc\">在某些裝置上，韌體就算有支援，還是會錯誤地回報不支援某些格式。（例如 1080p 解析度的智慧電視傾向於回報不支援 4K 影片）。</string>\n  <string name=\"okhttp_desc\">這個網路引擎是最慢的，但穩定性較好。</string>\n  <string name=\"select_channel_section\">從 ATV Channels 啟動應用程式時，開啟對應的區塊</string>\n  <string name=\"enable_voice_search_desc\">官方應用程式將被「橋接程式」所取代。橋接程式可將全域搜尋請求轉移到我們的程式進行搜尋。</string>\n  <string name=\"enable_master_password\">啟用主控密碼</string>\n  <string name=\"enter_master_password\">輸入主控密碼</string>\n  <string name=\"disable_stream_buffer\">對串流停用緩衝</string>\n  <string name=\"disable_stream_buffer_desc\">修正串流落後太多的情況。注意：開啟後串流可能會 Lag。</string>\n  <string name=\"sony_frame_drop_fix\">掉幀問題修復 #2</string>\n  <string name=\"sony_frame_drop_fix_desc\">修復 Sony 電視和其他一些設備上的延遲問題。注意：可能會出現音訊同步問題。</string>\n  <string name=\"audio_language\">音訊語言</string>\n  <string name=\"old_home_look\">舊版首頁區塊外觀</string>\n  <string name=\"hide_shorts_everywhere\">在所有地方隱藏 Shorts</string>\n  <string name=\"update_found\">有更新</string>\n  <string name=\"volume_boost_warning\">超出限制的任何設定可能會損壞喇叭</string>\n  <string name=\"play_video_incognito\">無痕播放</string>\n  <string name=\"header_kids_home\">兒童</string>\n  <string name=\"player_section_playlist\">將目前區塊內容用作播放清單</string>\n  <string name=\"header_trending\">熱門</string>\n  <string name=\"screensaver\">螢幕保護程式</string>\n  <string name=\"player_screen_off_timeout\">螢幕關閉逾時</string>\n  <string name=\"old_update_notifications\">舊版更新通知外觀</string>\n  <string name=\"mark_as_watched\">標記為已觀看</string>\n  <string name=\"player_ui_animations\">啟用播放器介面動畫</string>\n  <string name=\"player_likes_count\">顯示喜歡/不喜歡數量</string>\n  <string name=\"sorting_alphabetically2\">按字母順序排列（快速、預覽圖會播放）</string>\n  <string name=\"sorting_default\">預設排序（快速、預覽圖會播放）</string>\n  <string name=\"content_block_exclude_channel\">對此頻道排除 SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">停止將此頻道排除 SponsorBlock</string>\n  <string name=\"player_chapter_notification\">顯示通知以快速跳過章節</string>\n  <string name=\"subtitle_remember\">只對目前頻道開啟字幕</string>\n  <string name=\"amazon_frame_drop_fix\">掉幀問題修復 #2</string>\n  <string name=\"amazon_frame_drop_fix_desc\">專為 Amazon Stick 裝置設計，也可能適用於其他裝置。</string>\n  <string name=\"default_stack_desc\">內建的網路引擎。這個引擎在某些情況下可能較穩定。</string>\n  <string name=\"item_postion\">位置</string>\n  <string name=\"player_screen_off_dimming\">螢幕關閉暗化程度</string>\n  <string name=\"screensaver_timout\">螢幕保護程式逾時</string>\n  <string name=\"screensaver_dimming\">螢幕保護程式暗化程度</string>\n  <string name=\"player_ui_on_next\">切換至下一部影片時顯示播放器介面</string>\n  <string name=\"autogenerated\">自動產生</string>\n  <string name=\"player_auto_volume\">自動音量調節</string>\n  <string name=\"msg_player_error_video_renderer\">不支援選擇的影片格式。\\n請按播放器的 HQ 按鈕嘗試選擇其他格式。\\n若沒有改善，請嘗試重新啟動裝置。</string>\n  <string name=\"msg_player_error_audio_renderer\">不支援選擇的音訊格式。\\n請按播放器的 HQ 按鈕嘗試選擇其他格式。\\n若沒有改善，請嘗試重新啟動裝置。</string>\n  <string name=\"msg_player_error_subtitle_renderer\">不支援選擇的字幕格式。\\n請長按播放器的 CC 按鈕嘗試選擇其他格式。\\n若沒有改善，請嘗試重新啟動裝置。</string>\n  <string name=\"msg_player_error_video_source\">影片網址錯誤或裝置時間不正確。\\n通常是應用程式的問題。</string>\n  <string name=\"msg_player_error_audio_source\">音訊網址錯誤或裝置時間不正確。\\n通常是應用程式的問題。</string>\n  <string name=\"msg_player_error_subtitle_source\">字幕網址網址錯誤或裝置時間不正確。\\n通常是應用程式的問題。</string>\n  <string name=\"player_volume\">音量</string>\n  <string name=\"open_comments\">開啟評論</string>\n  <string name=\"disable_history\">停用歷史紀錄</string>\n  <string name=\"enable_history\">啟用歷史紀錄</string>\n  <string name=\"dialog_notification\">彈出對話框通知更新</string>\n  <string name=\"header_shorts\">Shorts</string>\n  <string name=\"player_global_focus\">在播放器按鈕行之間無縫切換</string>\n  <string name=\"auto_history\">自動（使用帳戶設定）</string>\n  <string name=\"remember_position_subscriptions\">於訂閱頻道中記住上次觀看位置</string>\n  <string name=\"msg_player_unknown_error\">未知錯誤</string>\n  <string name=\"header_notifications\">通知</string>\n  <string name=\"disable_search_history\">停用搜尋紀錄功能</string>\n  <string name=\"unlock_high_bitrate_formats\">開放 1080p vp9 高位元率格式</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">開放 mp4a 高位元率格式</string>\n  <string name=\"color_scheme_blue\">藍色</string>\n  <string name=\"color_scheme_dark_blue\">深藍色</string>\n  <string name=\"player_speed_per_channel\">針對單一頻道</string>\n  <string name=\"disable_popular_searches\">停用熱門搜尋建議</string>\n  <string name=\"multi_profiles\">分帳號獨立設定</string>\n  <string name=\"protect_account_with_password\">使用密碼保護本帳號</string>\n  <string name=\"enter_account_password\">請輸入您的帳號密碼</string>\n  <string name=\"share_qr_link\">分享連結（QR Code）</string>\n  <string name=\"app_corner_clock\">首頁：右上角時鐘</string>\n  <string name=\"player_corner_clock\">播放器：右上角時鐘</string>\n  <string name=\"player_corner_ending_time\">播放器：右上角結束時間</string>\n  <string name=\"hide_shorts_channel\">隱藏頻道中的 Shorts</string>\n  <string name=\"news_row_name\">新聞</string>\n  <string name=\"hide_upcoming_channel\">隱藏頻道中的即將播出內容</string>\n  <string name=\"hide_upcoming_home\">隱藏首頁中的即將播出內容</string>\n  <string name=\"hide_shorts_from_trending\">在熱門影片中隱藏 Shorts</string>\n  <string name=\"show_connect_messages\">顯示連線訊息</string>\n  <string name=\"prefer_avc_over_vp9_desc\">警告：最高支援 1080p</string>\n  <string name=\"play_next\">播放下一個</string>\n  <string name=\"hide_watched_from_subscriptions\">從訂閱頻道中隱藏已觀看的影片</string>\n  <string name=\"hide_unwanted_content\">隱藏內容</string>\n  <string name=\"hide_watched_from_home\">從首頁隱藏已觀看的影片</string>\n  <string name=\"remote_control_permission\">背景遙控功能需要「顯示在其他應用程式上層」的權限。</string>\n  <string name=\"login_from_browser\">從瀏覽器登入</string>\n  <string name=\"disable_remote_history\">使用遙控器時停用觀看紀錄</string>\n  <string name=\"keyboard_fix\">防止 OK 按鈕開啟鍵盤（適用於 G20s 等）</string>\n  <string name=\"nothing_found\">未找到任何內容</string>\n  <string name=\"channel_filter_hint\">篩選頻道</string>\n  <string name=\"player_exit_shortcut\">離開播放器</string>\n  <string name=\"pin_playlist\">將播放清單加入到側邊欄</string>\n  <string name=\"pin_channel\">將頻道加入到側邊欄</string>\n  <string name=\"action_sound_off\">靜音</string>\n  <string name=\"remember_position_of_live_videos\">記住直播串流的播放位置</string>\n  <string name=\"save_playlist\">將播放清單加入到播放清單區塊</string>\n  <string name=\"dearrow_status\">檢查 DeArrow 伺服器狀態</string>\n  <string name=\"old_channel_look\">舊版頻道頁面外觀</string>\n  <string name=\"hide_watched_from_notifications\">從通知隱藏已觀看的影片</string>\n  <string name=\"about_sponsorblock\">關於 SponsorBlock</string>\n  <string name=\"about_dearrow\">關於 DeArrow</string>\n  <string name=\"pinned_channel_rows\">將釘選頻道顯示為列</string>\n  <string name=\"skip_shorts\">跳過 Shorts</string>\n  <string name=\"player_extra_long_speed_list\">超長速度列表</string>\n  <string name=\"remember_position_pinned\">於釘選的播放清單中記住上次觀看位置</string>\n  <string name=\"speech_engine\">語音搜尋引擎</string>\n  <string name=\"unknown_source_error\">未知來源錯誤</string>\n  <string name=\"unknown_renderer_error\">未知渲染器錯誤</string>\n  <string name=\"action_dislike_unset\">已移除不喜歡</string>\n  <string name=\"action_like_unset\">已移除喜歡</string>\n  <string name=\"add_device_view_description\">若要連結裝置，請在手機的 YouTube 應用程式中「設定/在電視上觀看」區塊輸入此代碼\\n注意：這僅適用於 Gboard 鍵盤！</string>\n  <string name=\"add_to_subscriptions_group\">新增/從訂閱群組中移除</string>\n  <string name=\"alt_presets_behavior\">替代預設行為（限制頻寬）</string>\n  <string name=\"alt_presets_behavior_desc\">應用程式將嘗試維持對應於所選預設的頻寬，而不是在解析度、fps 和編解碼器之間進行比對。</string>\n  <string name=\"ambilight_ratio_fix\">Ambilight/長寬比例/影片縮放/螢幕擷圖修正</string>\n  <string name=\"ambilight_ratio_fix_desc\">修正情境背光失效、畫面長寬比或影片縮放異常，以及擷圖黑畫面的問題。會影響效能！</string>\n  <string name=\"amlogic_fix\">Amlogic 1080p\\@60fps 修正</string>\n  <string name=\"amlogic_fix_desc\">修正 Amlogic 裝置上的影格遺失問題。</string>\n  <string name=\"app_backup\">備份應用程式資料</string>\n  <string name=\"app_restore\">還原應用程式資料</string>\n  <string name=\"applying_fix\">正在套用修正，請勿關閉播放器…</string>\n  <string name=\"audio_sync_fix\">音訊同步修正</string>\n  <string name=\"audio_sync_fix_desc\">用替代方式同步影音內容。通常已經不使用本方式，但某些情況下可能會有幫助。</string>\n  <string name=\"auto_backup\">自動備份（每日一次）</string>\n  <string name=\"auto_frame_rate_desc\">此選項可消除相機快速移動場景（例如體育串流）中的抖動</string>\n  <string name=\"calm_msg\">我們正在修正問題，請不時檢查更新</string>\n  <string name=\"card_preview\">卡片預覽</string>\n  <string name=\"card_preview_full\">有聲影片</string>\n  <string name=\"card_preview_muted\">無聲影片</string>\n  <string name=\"card_unlocalized_titles\">未本地化的影片標題</string>\n  <string name=\"channel_search_bar\">頻道頁面內的搜尋列</string>\n  <string name=\"channels_filter\">在「頻道」區段中顯示「篩選頻道」欄位</string>\n  <string name=\"context_menu_sorting\">內容選單排序</string>\n  <string name=\"crowdsourced_thumbnails\">社群貢獻縮圖</string>\n  <string name=\"crowdsoursed_titles\">社群貢獻標題</string>\n  <string name=\"dearrow_not_submitted_thumbs\">縮圖來源（如果沒有 DeArrow 的縮圖）</string>\n  <string name=\"device_specific_backup\">僅備份此裝置</string>\n  <string name=\"disable_channels_service\">停用頻道服務</string>\n  <string name=\"disable_network_error_fixing\">停用自動網路錯誤修正</string>\n  <string name=\"disable_network_error_fixing_desc\">如果您使用 VPN，可能需要開啟此選項</string>\n  <string name=\"enable\">啟用</string>\n  <string name=\"fullscreen_mode\">全螢幕模式（無系統列）</string>\n  <string name=\"header_sports\">體育</string>\n  <string name=\"hide_mixes\">隱藏合輯</string>\n  <string name=\"hide_watched_from_watch_later\">從「稍後再看」播放清單隱藏已觀看的影片</string>\n  <string name=\"import_subscriptions_group\">匯入</string>\n  <string name=\"keep_finished_activities\">保留已完成的畫面</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">停用隱藏式字幕</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">啟用隱藏式字幕</string>\n  <string name=\"local_backup\">本機備份</string>\n  <string name=\"long_press_for_options\">長按以進行設定</string>\n  <string name=\"long_press_for_settings\">長按以進行設定</string>\n  <string name=\"more_info\">更多資訊</string>\n  <string name=\"my_videos\">我的影片</string>\n  <string name=\"new_subscriptions_group\">新群組</string>\n  <string name=\"not_supported_by_device\">裝置不支援此功能</string>\n  <string name=\"oculus_quest_fix\">Oculus Quest 修正</string>\n  <string name=\"original_lang\">原始</string>\n  <string name=\"paid_content_notification\">付費內容通知</string>\n  <string name=\"pitch_effect\">音高效果</string>\n  <string name=\"playback_buffering_fix\">播放緩衝修正</string>\n  <string name=\"player_audio_focus\">音訊焦點（如果偵測到其他播放器則暫停）</string>\n  <string name=\"player_global_focus_desc\">此功能會影響在播放器按鈕列之間切換時，哪個按鈕會取得焦點</string>\n  <string name=\"player_loop_shorts\">循環播放 Shorts</string>\n  <string name=\"player_only_mode\">如果影片是從應用程式外部開啟，則僅顯示播放器</string>\n  <string name=\"player_quick_skip_videos\">使用向左 / 向右按鈕跳過一般影片</string>\n  <string name=\"player_quick_shorts_skip\">使用向左 / 向右按鈕跳過 Shorts</string>\n  <string name=\"playlists_rows\">將「播放清單」區塊顯示為列</string>\n  <string name=\"prefer_ipv4\">偏好使用 IPv4 DNS</string>\n  <string name=\"prefer_ipv4_desc\">可能可以修正應用程式完全無法運作的情況。\\n注意：可能會導致當機或發生錯誤（尤其是在 Android 8 裝置或 Dune HD 上）</string>\n  <string name=\"premium_users_only\">僅限 Premium 會員。修正不完整的影片格式清單</string>\n  <string name=\"recommended\">推薦</string>\n  <string name=\"rename_group\">重新命名訂閱群組</string>\n  <string name=\"repeat_mode_reverse_list\">反序播放播放清單或頻道影片</string>\n  <string name=\"replace_thumbnails\">取代縮圖</string>\n  <string name=\"replace_titles\">取代標題</string>\n  <string name=\"screen_dimming_amount\">螢幕調暗量</string>\n  <string name=\"screen_dimming_timeout\">螢幕調暗逾時</string>\n  <string name=\"search_exit_shortcut\">從搜尋中退出</string>\n  <string name=\"video_buffer_size_highest\">最高</string>\n  <string name=\"video_disabled\">影片已停用</string>\n  <string name=\"video_flip\">翻轉（鏡像）</string>\n  <string name=\"without_picture\">無畫面</string>\n  <string name=\"you_liked\">您已按下喜歡</string>\n  <string name=\"video_buffer_size_lowest\">最低</string>\n  <string name=\"suggestions\">建議</string>\n  <string name=\"place_comments_left\">把評論區放在左邊</string>\n  <string name=\"unpin_group_from_sidebar\">移除訂閱群組</string>\n  <string name=\"hide_shorts_from_search\">在搜尋結果中隱藏 Shorts</string>\n  <string name=\"remove_playlist_fmt\">要永久移除 %s 嗎？</string>\n  <string name=\"dont_resize_video_to_fit_dialog\">不調整影片大小來配合對話框</string>\n  <string name=\"play_from_start\">從頭播放</string>\n  <string name=\"player_chapter_notification2\">點選通知即可切換至下一章節</string>\n  <string name=\"typing_corrections\">輸入文字時停用自動更正</string>\n  <string name=\"suggestions_horizontally_scrolled\">水平捲動建議內容</string>\n  <string name=\"player_toggle_speed\">切換速度開關</string>\n  <string name=\"create_playlist_note\">不會在 YouTube 應用程式裡出現</string>\n  <string name=\"player_quick_shorts_skip_alt\">使用向上 / 向下按鈕跳過 Shorts</string>\n  <string name=\"player_quick_skip_videos_alt\">使用向上 / 向下按鈕跳過一般影片</string>\n  <string name=\"prefer_google_dns\">偏好使用 Google DNS</string>\n  <string name=\"auto_backup_category\">自動備份</string>\n  <string name=\"once_a_day\">一天一次</string>\n  <string name=\"once_a_week\">一週一次</string>\n  <string name=\"once_a_month\">一個月一次</string>\n  <string name=\"action_debug_info\">除錯資訊</string>\n  <string name=\"stable_restore\">還原使用穩定版（將失去現有資料）</string>\n  <string name=\"dialog_block_channel\">封鎖頻道</string>\n  <string name=\"dialog_unblock_channel\">解除封鎖</string>\n  <string name=\"confirm_block_channel\">隱藏 %s 的所有內容？</string>\n  <string name=\"channel_blocked\">頻道已封鎖</string>\n  <string name=\"channel_unblocked\">已解除封鎖</string>\n  <string name=\"header_blocked_channels\">封鎖的頻道</string>\n  <string name=\"msg_no_blocked_channels\">沒有封鎖的頻道</string>\n</resources>"
  },
  {
    "path": "common/src/main/res/volume-fa/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"header_home\">خانه</string>\n  <string name=\"title_search\">جستجو</string>\n  <string name=\"header_subscriptions\">اشتراک‌ها</string>\n  <string name=\"header_history\">تاریخچه</string>\n  <string name=\"header_music\">موسیقی</string>\n  <string name=\"header_news\">اخبار</string>\n  <string name=\"header_gaming\">بازی‌ها</string>\n  <string name=\"header_playlists\">لیست‌های پخش</string>\n  <string name=\"header_settings\">تنظیمات</string>\n  <string name=\"subscriptions_signin_title\">تماشای جدیدترین ویدیوها از کانال‌های مورد علاقه شما</string>\n  <string name=\"subscriptions_signin_subtitle\">برای مشاهده اشتراک‌های خود وارد شوید</string>\n  <string name=\"subscriptions_signin_button_text\">ورود</string>\n  <string name=\"library_signin_to_show_more\">ویدیوهایی که پسندیده‌اید، ذخیره کرده‌اید یا در آن‌ها مشترک شده‌اید را مشاهده کنید</string>\n  <string name=\"library_signin_subtitle\">برای مشاهده کتابخانه خود وارد شوید</string>\n  <string name=\"action_signin\">ورود</string>\n  <string name=\"title_video_formats\">فرمت‌های ویدیو</string>\n  <string name=\"title_audio_formats\">فرمت‌های صوتی</string>\n  <string name=\"subtitle_category_title\">زیرنویس</string>\n  <string name=\"video_max_quality\">خودکار (بالاترین کیفیت)</string>\n  <string name=\"audio_max_quality\">خودکار (بالاترین کیفیت)</string>\n  <string name=\"subtitles_disabled\">زیرنویس غیرفعال است</string>\n  <string name=\"auto_frame_rate\">نرخ فریم خودکار</string>\n  <string name=\"frame_rate_correction\">رفع fps:\\n%s</string>\n  <string name=\"category_background_playback\">پخش در پس‌زمینه</string>\n  <string name=\"not_implemented\">پیاده‌سازی نشده</string>\n  <string name=\"option_background_playback_off\">غیرفعال</string>\n  <string name=\"option_background_playback_pip\">پنجره در پنجره</string>\n  <string name=\"option_background_playback_only_audio\">فقط صدا</string>\n  <string name=\"playback_settings\">تنظیمات پخش</string>\n  <string name=\"video_speed\">سرعت ویدیو</string>\n  <string name=\"resolution_switch\">تغییر رزولوشن</string>\n  <string name=\"video_buffer\">بافر ویدیو</string>\n  <string name=\"video_buffer_size_low\">پایین</string>\n  <string name=\"video_buffer_size_med\">متوسط</string>\n  <string name=\"video_buffer_size_high\">بالا</string>\n  <string name=\"update_changelog\">تغییرات نسخه</string>\n  <string name=\"install_update\">نصب بروزرسانی</string>\n  <string name=\"section_is_empty\">چیزی اینجا نیست.</string>\n  <string name=\"dialog_add_to_playlist\">افزودن/حذف از لیست پخش</string>\n  <string name=\"msg_signed_users_only\">فقط کاربران وارد شده</string>\n  <string name=\"msg_cant_load_content\">بارگیری محتوا ناموفق بود. \\n1) تاریخ و زمان دستگاه را بررسی کنید. \\n2) اتصال شبکه را بررسی کنید. \\n3) سعی کنید وارد حساب خود شوید.</string>\n  <string name=\"title_video_presets\">پیش‌تنظیمات ویدیو</string>\n  <string name=\"settings_accounts\">حساب‌ها</string>\n  <string name=\"settings_left_panel\">ویرایش دسته‌ها</string>\n  <string name=\"settings_themes\">تم‌ها</string>\n  <string name=\"settings_other\">سایر</string>\n  <string name=\"settings_player\">تنظیمات پخش کننده</string>\n  <string name=\"settings_language\">زبان</string>\n  <string name=\"settings_linked_devices\">دستگاه‌های متصل</string>\n  <string name=\"settings_about\">درباره برنامه</string>\n  <string name=\"dialog_account_list\">انتخاب حساب</string>\n  <string name=\"dialog_account_none\">هیچ‌کدام</string>\n  <string name=\"dialog_remove_account\">حذف حساب</string>\n  <string name=\"dialog_add_account\">افزودن حساب</string>\n  <string name=\"default_lang\">پیش‌فرض</string>\n  <string name=\"dialog_select_language\">زبان برنامه</string>\n  <string name=\"header_channels\">کانال‌ها</string>\n  <string name=\"playback_queue_category_title\">لیست انتظار پخش</string>\n  <string name=\"subtitle_default\">پیش‌فرض</string>\n  <string name=\"subtitle_white_semi_transparent\">سفید روی پس‌زمینه نیمه شفاف</string>\n  <string name=\"subtitle_style\">سبک زیرنویس</string>\n  <string name=\"subtitle_language\">زبان زیرنویس</string>\n  <string name=\"action_search\">جستجو</string>\n  <string name=\"settings_main_ui\">رابط کاربری اصلی</string>\n  <string name=\"dialog_main_ui\">رابط کاربری برنامه</string>\n  <string name=\"card_animated_previews\">پیش‌نمایش متحرک</string>\n  <string name=\"web_site\">وب‌سایت</string>\n  <string name=\"donation\">حمایت از توسعه‌دهنده</string>\n  <string name=\"dialog_about\">درباره برنامه</string>\n  <string name=\"dialog_player_ui\">پخش کننده ویدیو</string>\n  <string name=\"player_show_ui_on_pause\">نمایش رابط کاربری هنگام توقف</string>\n  <string name=\"player_pause_on_ok\">دکمه OK ویدیو را متوقف می‌کند</string>\n  <string name=\"player_only_ui\">فقط رابط کاربری</string>\n  <string name=\"player_ui_and_pause\">رابط کاربری و توقف ویدیو</string>\n  <string name=\"player_only_pause\">فقط توقف ویدیو</string>\n  <string name=\"check_for_updates\">بررسی بروزرسانی‌ها</string>\n  <string name=\"update_not_found\">شما از آخرین نسخه استفاده می‌کنید</string>\n  <string name=\"update_in_progress\">لطفا صبر کنید…</string>\n  <string name=\"player_ui_hide_behavior\">پنهان کردن خودکار رابط کاربری</string>\n  <string name=\"option_never\">هرگز</string>\n  <string name=\"side_panel_sections\">مرتب‌سازی بخش‌ها</string>\n  <string name=\"boot_to_section\">راه‌اندازی به بخش</string>\n  <string name=\"large_ui\">رابط کاربری بزرگ</string>\n  <string name=\"video_grid_scale\">مقیاس شبکه ویدیو</string>\n  <string name=\"scale_ui\">مقیاس رابط کاربری</string>\n  <string name=\"color_scheme\">طرح رنگ</string>\n  <string name=\"color_scheme_default\">پیش‌فرض</string>\n  <string name=\"color_scheme_red_grey\">قرمز خاکستری</string>\n  <string name=\"color_scheme_red\">قرمز</string>\n  <string name=\"color_scheme_dark_grey\">خاکستری تیره</string>\n  <string name=\"disable_update_check\">غیرفعال کردن بررسی بروزرسانی</string>\n  <string name=\"show_again\">نمایش مجدد</string>\n  <string name=\"check_updates_auto\">اعلان هنگام وجود بروزرسانی جدید</string>\n  <string name=\"select_account_on_boot\">نمایش انتخاب حساب هنگام راه‌اندازی</string>\n  <string name=\"player_other\">گزینه‌های متفرقه</string>\n  <string name=\"player_full_date\">تاریخ دقیق در توضیحات</string>\n  <string name=\"open_channel\">باز کردن کانال</string>\n  <string name=\"open_playlist\">باز کردن لیست پخش</string>\n  <string name=\"not_interested\">علاقه‌ای ندارم</string>\n  <string name=\"you_wont_see_this_video\">این ویدیو از پیشنهادها حذف شد</string>\n  <string name=\"dialog_search\">جستجو</string>\n  <string name=\"settings_search\">جستجو</string>\n  <string name=\"instant_voice_search\">جستجوی صوتی</string>\n  <string name=\"option_background_playback_behind\">پخش در پس‌زمینه</string>\n  <string name=\"next_video_info_is_not_loaded_yet\">اطلاعات ویدیوی بعدی هنوز بارگیری نشده است</string>\n  <string name=\"card_multiline_title\">عنوان‌های چند خطی</string>\n  <string name=\"cards_style\">سبک کارت‌ها</string>\n  <string name=\"repeat_mode_all\">پخش تمام ویدیوها به ترتیب</string>\n  <string name=\"repeat_mode_one\">تکرار ویدیوی فعلی</string>\n  <string name=\"repeat_mode_pause\">توقف پس از هر ویدیو</string>\n  <string name=\"repeat_mode_pause_alt\">فقط پخش مداوم لیست‌های پخش</string>\n  <string name=\"repeat_mode_none\">پایان پخش پس از یک ویدیو</string>\n  <string name=\"subscribed_to_channel\">در کانال مشترک شده‌اید</string>\n  <string name=\"unsubscribed_from_channel\">از کانال لغو اشتراک شده‌اید</string>\n  <string name=\"subtitle_yellow_transparent\">زرد روی پس‌زمینه شفاف</string>\n  <string name=\"subtitle_white_black\">سفید روی پس‌زمینه سیاه</string>\n  <string name=\"player_seek_preview\">پیش‌نمایش هنگام جستجوی ویدیو</string>\n  <string name=\"color_scheme_dark_grey_oled\">خاکستری تیره (OLED)</string>\n  <string name=\"channels_section_sorting\">مرتب‌سازی بخش کانال‌ها</string>\n  <string name=\"sorting_by_new_content\">محتوا جدید</string>\n  <string name=\"sorting_alphabetically\">الفبایی</string>\n  <string name=\"sorting_last_viewed\">آخرین بازدید</string>\n  <string name=\"player_pause_when_seek\">توقف هنگام جستجوی ویدیو</string>\n  <string name=\"playlists_style\">سبک بخش لیست‌های پخش</string>\n  <string name=\"playlists_style_grid\">شبکه</string>\n  <string name=\"playlists_style_rows\">ردیف‌ها</string>\n  <string name=\"player_show_clock\">نمایش ساعت در پخش کننده ویدیو</string>\n  <string name=\"player_show_remaining_time\">نمایش زمان باقی‌مانده در پخش کننده ویدیو</string>\n  <string name=\"player_show_ending_time\">نمایش زمان پایان در پخش کننده ویدیو</string>\n  <string name=\"open_channel_uploads\">باز کردن ویدیوهای کانال</string>\n  <string name=\"app_exit_shortcut\">خروج از برنامه</string>\n  <string name=\"app_exit_none\">خروج نکن</string>\n  <string name=\"app_double_back_exit\">دوبار فشار دهید</string>\n  <string name=\"app_single_back_exit\">یک بار فشار دهید</string>\n  <string name=\"action_video_zoom\">بزرگنمایی ویدیو</string>\n  <string name=\"video_zoom\">بزرگنمایی ویدیو</string>\n  <string name=\"video_zoom_default\">پیش‌فرض</string>\n  <string name=\"video_zoom_fit_width\">مناسب عرض</string>\n  <string name=\"video_zoom_fit_height\">مناسب ارتفاع</string>\n  <string name=\"video_zoom_fit_both\">مناسب عرض یا ارتفاع</string>\n  <string name=\"video_zoom_stretch\">کشش</string>\n  <string name=\"color_scheme_teal\">فیروزه‌ای</string>\n  <string name=\"color_scheme_teal_oled\">فیروزه‌ای (OLED)</string>\n  <string name=\"player_seek_preview_none\">غیرفعال</string>\n  <string name=\"player_seek_preview_single\">یک فریم</string>\n  <string name=\"player_seek_preview_carousel\">چند فریم</string>\n  <string name=\"player_seek_preview_carousel_slow\">چند فریم (کند، بر اساس فریم‌های کلیدی)</string>\n  <string name=\"player_seek_preview_carousel_fast\">چند فریم (سریع، دقیق نیست)</string>\n  <string name=\"unsubscribe_from_channel\">لغو اشتراک از کانال</string>\n  <string name=\"card_title_lines_num\">تعداد خطوط عنوان کارت</string>\n  <string name=\"card_auto_scrolled_title\">حرکت خودکار عنوان طولانی</string>\n  <string name=\"share_link\">لینک اشتراک‌گذاری</string>\n  <string name=\"subscribe_to_channel\">اشتراک در کانال</string>\n  <string name=\"wait_data_loading\">لطفا منتظر بمانید تا داده‌ها بارگیری شوند...</string>\n  <string name=\"auto_frame_rate_pause\">نرخ فریم خودکار (توقف هنگام تغییر)</string>\n  <string name=\"auto_frame_rate_applying\">اعمال نرخ فریم خودکار%sx%s\\@%s</string>\n  <string name=\"audio_shift\">تنظیم صدا</string>\n  <string name=\"player_remember_speed\">به خاطر سپردن سرعت</string>\n  <string name=\"mark_channel_as_watched\">علامت‌گذاری به عنوان تماشا شده</string>\n  <string name=\"channel_marked_as_watched\">کانال به عنوان تماشا شده علامت‌گذاری شد</string>\n  <string name=\"dialog_add_device\">افزودن دستگاه</string>\n  <string name=\"dialog_remove_all_devices\">حذف همه دستگاه‌ها</string>\n  <string name=\"device_link_enabled\">اتصال فعال شد</string>\n  <string name=\"device_connected\">دستگاه \\\"%s\\\" متصل شد</string>\n  <string name=\"device_disconnected\">دستگاه \\\"%s\\\" قطع شد</string>\n  <string name=\"settings_ui_scale\">مقیاس رابط کاربری</string>\n  <string name=\"msg_restart_app\">لطفا برنامه را مجددا راه‌اندازی کنید تا این تنظیمات اعمال شوند</string>\n  <string name=\"settings_block\">مسدود کردن محتوا</string>\n  <string name=\"msg_applying\">در حال اعمال%s</string>\n  <string name=\"msg_done\">انجام شد</string>\n  <string name=\"settings_general\">عمومی</string>\n  <string name=\"settings_video\">ویدیو</string>\n  <string name=\"content_block_confirm_skip\">تأیید هنگام رد کردن</string>\n  <string name=\"content_block_categories\">رد کردن بخش‌ها</string>\n  <string name=\"content_block_sponsor\">اسپانسر</string>\n  <string name=\"content_block_intro\">مقدمه/فاصله</string>\n  <string name=\"content_block_outro\">خاتمه/تیتراژ پایانی</string>\n  <string name=\"content_block_interaction\">یادآوری تعامل (اشتراک در کانال)</string>\n  <string name=\"content_block_self_promo\">تبلیغ شخصی/غیرپولی</string>\n  <string name=\"content_block_music_off_topic\">بخش غیرموسیقی در ویدیو</string>\n  <string name=\"confirm_segment_skip\">آیا می‌خواهید بخش%s\\\"\\? را رد کنید</string>\n  <string name=\"cancel_segment_skip\">لغو رد کردن بخش</string>\n  <string name=\"msg_skipping_segment\">در حال رد کردن بخش\\\"%s\\\"…</string>\n  <string name=\"content_block_notification_type\">نوع اعلان</string>\n  <string name=\"content_block_notify_none\">بدون اعلان</string>\n  <string name=\"content_block_notify_dialog\">کادر تأیید</string>\n  <string name=\"return_to_launcher\">بازگشت به پخش کننده از کانال‌های ATV / جستجو</string>\n  <string name=\"btn_confirm\">تأیید</string>\n  <string name=\"player_low_video_quality\">کیفیت پایین ویدیو</string>\n  <string name=\"remote_session_closed\">جلسه راه‌دور بسته شد</string>\n  <string name=\"msg_mode_switch_error\">امکان تغییر حالت نمایش به \\\"%s\\\" وجود ندارد</string>\n  <string name=\"msg_player_error\">خطایی در پخش کننده%s رخ داده است. در حال راه‌اندازی مجدد...</string>\n  <string name=\"video_preset_disabled\">بدون پیش‌تنظیم</string>\n  <string name=\"video_preset_enabled\">فرمت ویدیوی بعدی مطابق با پیش‌تنظیم تنظیم خواهد شد</string>\n  <string name=\"player_sleep_timer\">تایمر خواب</string>\n  <string name=\"player_show_quality_info\">نمایش اطلاعات کیفیت ویدیو در پخش کننده</string>\n  <string name=\"player_remember_each_speed\">به خاطر سپردن سرعت هر ویدیو</string>\n  <string name=\"player_remember_speed_none\">هیچ‌کدام</string>\n  <string name=\"player_remember_speed_all\">یکسان برای همه ویدیوها</string>\n  <string name=\"player_remember_speed_each\">برای هر ویدیو</string>\n  <string name=\"msg_player_error_source\">امکان دانلود ویدیو وجود ندارد</string>\n  <string name=\"msg_player_error_renderer\">فرمت ویدیوی انتخاب شده پشتیبانی نمی‌شود.\\nممکن است مشکل از نرم‌افزار دستگاه باشد. لطفا دستگاه را مجددا راه‌اندازی کنید.</string>\n  <string name=\"msg_player_error_unexpected\">خطای ناشناخته در دیکودر ویدیو</string>\n  <string name=\"video_aspect\">نسبت تصویر</string>\n  <string name=\"player_tweaks\">تنظیمات پیشرفته</string>\n  <string name=\"header_uploads\">ویدیوها</string>\n  <string name=\"player_show_global_clock\">نمایش ساعت همیشه روی صفحه</string>\n  <string name=\"player_show_global_ending_time\">نمایش زمان پایان همیشه روی صفحه</string>\n  <string name=\"uploads_old_look\">بازگشت به ظاهر قدیمی بخش ویدیوها</string>\n  <string name=\"background_playback_activation\">پخش در پس‌زمینه (فعال‌سازی)</string>\n  <string name=\"channels_old_look\">بازگشت به ظاهر قدیمی بخش کانال‌ها</string>\n  <string name=\"channels_auto_load\">بارگیری خودکار محتوای بخش کانال‌ها</string>\n  <string name=\"return_to_background_video\">بازگشت به پخش ویدیو در پس‌زمینه</string>\n  <string name=\"pin_unpin_from_sidebar\">ثابت کردن/لغو ثابت از نوار کناری</string>\n  <string name=\"pinned_to_sidebar\">ثابت شده در نوار کناری</string>\n  <string name=\"unpin_from_sidebar\">لغو ثابت از نوار کناری</string>\n  <string name=\"unpinned_from_sidebar\">از نوار کناری لغو ثابت شد</string>\n  <string name=\"dialog_select_country\">کشور</string>\n  <string name=\"settings_language_country\">زبان/کشور</string>\n  <string name=\"share_embed_link\">اشتراک‌گذاری لینک جاسازی</string>\n  <string name=\"card_text_scroll_factor\">سرعت اسکرول متن کارت</string>\n  <string name=\"preferred_update_source\">انتخاب منبع بروزرسانی</string>\n  <string name=\"hide_shorts\">پنهان کردن کوته ویدیوها از اشتراک‌ها</string>\n  <string name=\"key_remapping\">تنظیم مجدد دکمه‌ها</string>\n  <string name=\"screen_dimming\">تاریک کردن صفحه</string>\n  <string name=\"removed_from_playback_queue\">از لیست انتظار پخش حذف شد</string>\n  <string name=\"added_to_playback_queue\">به لیست انتظار پخش اضافه شد</string>\n  <string name=\"add_remove_from_playback_queue\">افزودن/حذف از لیست انتظار پخش</string>\n  <string name=\"proxy_enabled\">پراکسی فعال شد</string>\n  <string name=\"proxy_disabled\">پراکسی غیرفعال شد</string>\n  <string name=\"uploads_row_name\">ویدیوها</string>\n  <string name=\"playlists_row_name\">لیست‌های پخش ایجاد شده</string>\n  <string name=\"popular_uploads_row_name\">ویدیوهای محبوب</string>\n  <string name=\"breaking_news_row_name\">اخبار فوری</string>\n  <string name=\"covid_news_row_name\">اخبار کووید-۱۹</string>\n  <string name=\"enable_voice_search\">فعال‌سازی جستجوی صوتی (نیاز به پشتیبانی نرم‌افزاری دارد)</string>\n  <string name=\"subscribe_unsubscribe_from_channel\">اشتراک/لغو اشتراک از کانال</string>\n  <string name=\"app_backup_restore\">پشتیبان‌گیری/بازیابی</string>\n  <string name=\"app_backup\">پشتیبان‌گیری از داده‌ها</string>\n  <string name=\"app_restore\">بازیابی داده‌ها از پشتیبان</string>\n  <string name=\"skip_each_segment_once\">بخش‌ها را دوباره رد نکن</string>\n  <string name=\"disable_ok_long_press\">غیرفعال کردن فشار طولانی روی دکمه OK</string>\n  <string name=\"player_time_correction\">نمایش زمان با توجه به سرعت</string>\n  <string name=\"sponsor_color_markers\">نشانگرهای رنگی روی نوار پیشرفت</string>\n  <string name=\"refresh_section\">بروزرسانی بخش</string>\n  <string name=\"option_disabled\">غیرفعال</string>\n  <string name=\"live_now_row_name\">پخش زنده</string>\n  <string name=\"run_in_background\">اجرا در پس‌زمینه</string>\n  <string name=\"settings_remote_control\">کنترل از راه دور</string>\n  <string name=\"background_service_started\">سرویس پس‌زمینه شروع شد</string>\n  <string name=\"dialog_add_to\">افزودن به %s</string>\n  <string name=\"dialog_remove_from\">حذف از %s</string>\n  <string name=\"added_to\">به %s اضافه شد</string>\n  <string name=\"removed_from\">از %s حذف شد</string>\n  <string name=\"video_preset_adaptive\">سازگار</string>\n  <string name=\"content_block_preview_recap\">پیش‌نمایش/خلاصه</string>\n  <string name=\"content_block_highlight\">اشاره یا برجسته‌سازی ویدیو</string>\n  <string name=\"removed_from_history\">از تاریخچه حذف شد</string>\n  <string name=\"remove_from_history\">حذف از تاریخچه</string>\n  <string name=\"upload_date\">تاریخ آپلود</string>\n  <string name=\"upload_date_any\">همه زمان‌ها</string>\n  <string name=\"upload_date_today\">امروز</string>\n  <string name=\"upload_date_this_week\">این هفته</string>\n  <string name=\"upload_date_this_month\">این ماه</string>\n  <string name=\"upload_date_this_year\">امسال</string>\n  <string name=\"double_refresh_rate\">نرخ تازه‌سازی دوبرابر</string>\n  <string name=\"upload_date_last_hour\">آخرین ساعت</string>\n  <string name=\"focus_on_search_results\">تمرکز خودکار روی نتایج جستجو</string>\n  <string name=\"context_menu\">منوی زمینه</string>\n  <string name=\"add_remove_from_recent_playlist\">افزودن/حذف از لیست پخش اخیر</string>\n  <string name=\"hide_settings_section\">پنهان کردن بخش تنظیمات (خطرناک!)</string>\n  <string name=\"volume\">صدا %s%%</string>\n  <string name=\"auto_frame_rate_sec\">%s ثانیه</string>\n  <string name=\"audio_shift_sec\">%s ثانیه</string>\n  <string name=\"ui_hide_timeout_sec\">%s ثانیه</string>\n  <string name=\"screen_dimming_timeout_min\">%s دقیقه</string>\n  <string name=\"mark_all_channels_watched\">علامت‌گذاری همه کانال‌ها به عنوان تماشا شده</string>\n  <string name=\"move_section_up\">انتقال بخش به بالا</string>\n  <string name=\"move_section_down\">انتقال بخش به پایین</string>\n  <string name=\"player_buttons\">تنظیم دکمه‌های پخش کننده</string>\n  <string name=\"action_playlist_add\">افزودن به لیست پخش</string>\n  <string name=\"action_video_stats\">آمار ویدیو</string>\n  <string name=\"action_subscribe\">اشتراک</string>\n  <string name=\"action_channel\">باز کردن کانال</string>\n  <string name=\"action_screen_off\">خاموش کردن صفحه</string>\n  <string name=\"action_pip\">پنجره در پنجره</string>\n  <string name=\"action_playback_queue\">لیست انتظار پخش</string>\n  <string name=\"action_video_speed\">سرعت ویدیو</string>\n  <string name=\"action_subtitles\">زیرنویس</string>\n  <string name=\"action_like\">پسندیدن</string>\n  <string name=\"action_dislike\">نپسندیدن</string>\n  <string name=\"action_play_pause\">پخش/توقف</string>\n  <string name=\"action_repeat_mode\">حالت تکرار</string>\n  <string name=\"action_next\">ویدیوی بعدی</string>\n  <string name=\"action_previous\">ویدیوی قبلی</string>\n  <string name=\"action_high_quality\">کیفیت ویدیو</string>\n  <string name=\"content_block_no_skipping_mode\">حالت عدم رد کردن</string>\n  <string name=\"content_block_action_type\">انتخاب عمل</string>\n  <string name=\"content_block_action_none\">هیچ کاری نکن</string>\n  <string name=\"content_block_action_only_skip\">فقط رد کردن</string>\n  <string name=\"content_block_action_toast\">رد کردن با اعلان</string>\n  <string name=\"content_block_action_dialog\">نمایش دکمه رد کردن</string>\n  <string name=\"content_block_filler\">خارج از موضوع (پرکننده)</string>\n  <string name=\"keyboard_auto_show\">نمایش خودکار صفحه کلید</string>\n  <string name=\"cancel_dialog\">لغو</string>\n  <string name=\"msg_player_error_source2\">آدرس URL کار نمی‌کند یا زمان دستگاه نادرست است.\\nمعمولا این یک خطای برنامه است.</string>\n  <string name=\"hide_upcoming\">پنهان کردن آینده از اشتراک‌ها</string>\n  <string name=\"search_background_playback\">فعال‌سازی پنجره در پنجره هنگام جستجو از پخش کننده</string>\n  <string name=\"trending_row_name\">محبوب‌ها</string>\n  <string name=\"player_seek_type\">رفتار جستجو</string>\n  <string name=\"player_seek_regular\">عادی</string>\n  <string name=\"player_seek_confirmation_pause\">با تأیید (توقف هنگام جستجو)</string>\n  <string name=\"player_seek_confirmation_play\">با تأیید (پخش هنگام جستجو)</string>\n  <string name=\"hide_shorts_from_home\">پنهان کردن کوته ویدیوها از صفحه اصلی</string>\n  <string name=\"finish_on_disconnect\">بستن برنامه پس از قطع اتصال موبایل/تبلت</string>\n  <string name=\"update_error\">خطا در بروزرسانی</string>\n  <string name=\"hide_shorts_from_history\">پنهان کردن کوته ویدیوها از تاریخچه</string>\n  <string name=\"disable_screensaver\">غیرفعال کردن محافظ صفحه</string>\n  <string name=\"player_ok_button_behavior\">رفتار دکمه OK</string>\n  <string name=\"content_block_notify_toast\">اعلان</string>\n  <string name=\"intent_force_close\">اجبار به خروج اگر ویدیو از منبع خارجی باز شود</string>\n  <string name=\"pin_unpin_playlist\">ثابت کردن/لغو ثابت لیست پخش از نوار کناری</string>\n  <string name=\"pin_unpin_channel\">ثابت کردن/لغو ثابت کانال از نوار کناری</string>\n  <string name=\"action_video_info\">توضیحات ویدیو</string>\n  <string name=\"description_not_found\">توضیحات یافت نشد</string>\n  <string name=\"subtitle_white_transparent\">سفید روی پس‌زمینه شفاف</string>\n  <string name=\"proxy_port_hint\">پورت پراکسی</string>\n  <string name=\"proxy_host_hint\">نام یا IP میزبان پراکسی</string>\n  <string name=\"proxy_username_hint\">نام کاربری پراکسی</string>\n  <string name=\"proxy_password_hint\">رمز عبور پراکسی</string>\n  <string name=\"proxy_type\">نوع پراکسی</string>\n  <string name=\"enable_web_proxy\">استفاده از Web Proxy</string>\n  <string name=\"proxy_not_supported\">استفاده از پراکسی وب (نیاز به Android 4.4+ دارد)</string>\n  <string name=\"proxy_test_btn\">تست</string>\n  <string name=\"proxy_settings_title\">تنظیمات پراکسی</string>\n  <string name=\"proxy_test_cancelled\">تست#%d: لغو شد.</string>\n  <string name=\"proxy_type_invalid\">نوع پراکسی تنظیم نشده است.</string>\n  <string name=\"proxy_host_invalid\">میزبان پراکسی تنظیم نشده است.</string>\n  <string name=\"proxy_port_invalid\">پورت پراکسی نامعتبر است، باید بزرگتر از ۰ باشد.</string>\n  <string name=\"proxy_credentials_invalid\">خطا در نام کاربری یا رمز عبور.</string>\n  <string name=\"proxy_test_aborted\">تست لغو شد، لطفا ابتدا تنظیمات پراکسی را اصلاح کنید.</string>\n  <string name=\"proxy_application_aborted\">لطفا ابتدا تنظیمات پراکسی را اصلاح کنید.</string>\n  <string name=\"proxy_test_start\">تست#%d:%s...</string>\n  <string name=\"proxy_test_error\">خطا#%d:%s</string>\n  <string name=\"proxy_test_status\">وضعیت#%d: %s %d %s</string>\n  <string name=\"openvpn_config_address_hint\">آدرس فایل پیکربندی (*.ovpn)</string>\n  <string name=\"enable_openvpn\">استفاده از OpenVPN</string>\n  <string name=\"openvpn_settings_title\">تنظیمات OpenVPN</string>\n  <string name=\"openvpn_application_aborted\">لطفا ابتدا تنظیمات OpenVPN را اصلاح کنید.</string>\n  <string name=\"openvpn_test_aborted\">تست لغو شد، لطفا ابتدا تنظیمات OpenVPN را اصلاح کنید.</string>\n  <string name=\"openvpn_address_invalid\">آدرس پیکربندی OpenVPN تنظیم نشده است.</string>\n  <string name=\"internet_censorship\">سانسور اینترنت</string>\n  <string name=\"rename_section\">تغییر نام این بخش</string>\n  <string name=\"simple_edit_value_hint\">مقدار جدید</string>\n  <string name=\"seek_interval\">فاصله جستجو</string>\n  <string name=\"seek_interval_sec\">%s ثانیه</string>\n  <string name=\"subtitle_system\">سبک سیستم (تنظیمات اندروید &gt; دسترسی‌پذیری)</string>\n  <string name=\"subtitle_scale\">مقیاس زیرنویس</string>\n  <string name=\"audio_sync_fix_desc\">روش جایگزین برای همگام‌سازی صدا/ویدیو. قدیمی است، اما در برخی موارد ممکن است کمک کند.</string>\n  <string name=\"audio_sync_fix\">رفع همگام‌سازی صدا</string>\n  <string name=\"ambilight_ratio_fix_desc\">رفع نور پس‌زمینه از دست رفته. نسبت تصویر نادرست را اصلاح می‌کند. صحنه‌های خالی را اصلاح می‌کند. بر عملکرد تأثیر می‌گذارد!</string>\n  <string name=\"ambilight_ratio_fix\">رفع Ambilight / نسبت تصویر / صحنه‌های خالی</string>\n  <string name=\"force_legacy_codecs_desc\">عملکرد را به شدت در دستگاه‌های کم‌قدرت بهبود می‌بخشد. حداکثر رزولوشن ۷۲۰ پیکسل.</string>\n  <string name=\"force_legacy_codecs\">اجبار به استفاده از کدک‌های قدیمی (۷۲۰ پیکسل)</string>\n  <string name=\"live_stream_fix_desc\">عملکرد پخش زنده را در دستگاه‌های کم‌قدرت به شدت بهبود می‌بخشد. حداکثر رزولوشن ۱۰۸۰ پیکسل.</string>\n  <string name=\"live_stream_fix\">رفع پخش زنده (۱۰۸۰ پیکسل)</string>\n  <string name=\"playback_notifications_fix_desc\">اعلان‌های تغییر مسیر را پنهان می‌کند. می‌تواند در نرم‌افزارهای مبتنی بر AOSP مفید باشد.</string>\n  <string name=\"playback_notifications_fix\">غیرفعال کردن اعلان‌های پخش</string>\n  <string name=\"amlogic_fix_desc\">رفع افت فریم در دستگاه‌های مبتنی بر Amlogic.</string>\n  <string name=\"amlogic_fix\">رفع Amlogic 1080p\\@60FPS</string>\n  <string name=\"tunneled_video_playback_desc\">توجه: مشکلات احتمالی با توقف! پخش ویدیو از طریق تونل‌ها با مزایایی مانند همگام‌سازی بهتر صدا/ویدیو (AV sync) و پخش روان‌تر. نیاز به Android 5+ دارد.</string>\n  <string name=\"tunneled_video_playback\">پخش ویدیو از طریق تونل‌ها (اندروید ۵+)</string>\n  <string name=\"master_volume\">صدا اصلی</string>\n  <string name=\"volume_limit\">محدودیت صدا</string>\n  <string name=\"play_video\">پخش</string>\n  <string name=\"remember_position_of_short_videos\">به خاطر سپردن موقعیت ویدیوهای کوتاه (کمتر از ۱۰ دقیقه)</string>\n  <string name=\"player_show_tooltips\">نمایش راهنمای دکمه‌ها</string>\n  <string name=\"action_like_unset\">پسندیدن تنظیم نشده</string>\n  <string name=\"action_dislike_unset\">نپسندیدن تنظیم نشده</string>\n  <string name=\"various_buttons\">دکمه‌های بالای صفحه اصلی</string>\n  <string name=\"not_compatible_with\">گزینه انتخاب شده با %s سازگار نیست</string>\n  <string name=\"subtitle_position\">موقعیت زیرنویس</string>\n  <string name=\"pressing_home\">با فشار دادن HOME</string>\n  <string name=\"pressing_home_back\">با فشار دادن HOME یا BACK</string>\n  <string name=\"player_number_key_seek\">فاصله جستجو با دکمه‌های عددی</string>\n  <string name=\"save_remove_playlist\">افزودن/حذف لیست پخش از بخش لیست‌های پخش</string>\n  <string name=\"remove_playlist\">حذف لیست پخش از بخش لیست‌های پخش به طور دائم</string>\n  <string name=\"removed_from_playlists\">از بخش لیست‌های پخش حذف شد</string>\n  <string name=\"saved_to_playlists\">به بخش لیست‌های پخش اضافه شد</string>\n  <string name=\"create_playlist\">ایجاد لیست پخش</string>\n  <string name=\"add_video_to_new_playlist\">افزودن به لیست پخش جدید</string>\n  <string name=\"cant_delete_empty_playlist\">امکان حذف لیست پخش خالی وجود ندارد</string>\n  <string name=\"playlist\">لیست پخش</string>\n  <string name=\"rename_playlist\">تغییر نام لیست پخش</string>\n  <string name=\"cant_rename_empty_playlist\">امکان تغییر نام لیست پخش خالی وجود ندارد</string>\n  <string name=\"cant_rename_foreign_playlist\">امکان تغییر نام لیست پخش دیگران وجود ندارد</string>\n  <string name=\"cant_save_playlist\">امکان افزودن این لیست پخش وجود ندارد</string>\n  <string name=\"enter_value\">وارد کردن هر مقدار (غیر خالی)</string>\n  <string name=\"dialog_add_remove_from\">افزودن/حذف از %s</string>\n  <string name=\"add_to_playback_queue\">افزودن به لیست انتظار پخش</string>\n  <string name=\"remove_from_playback_queue\">حذف از لیست انتظار پخش</string>\n  <string name=\"lb_playback_controls_skip_next\">\\\"رفتن به ویدیوی بعدی\\\"</string>\n  <string name=\"lb_playback_controls_skip_previous\">\\\"رفتن به ویدیوی قبلی\\\"</string>\n  <string name=\"alt_presets_behavior\">رفتار جایگزین پیش‌تنظیمات ویدیو (محدودیت پهنای باند)</string>\n  <string name=\"alt_presets_behavior_desc\">برنامه سعی می‌کند پهنای باندی را حفظ کند که با پیش‌تنظیم انتخاب شده مطابقت دارد، نه تطابق رزولوشن، نرخ فریم و کدک.</string>\n  <string name=\"sleep_timer\">فعال‌سازی تایمر خواب (یک ساعت)</string>\n  <string name=\"sleep_timer_desc\">اگر کاربر از کنترل از راه دور استفاده نکند، پس از یک ساعت پخش متوقف می‌شود</string>\n  <string name=\"disable_vsync\">غیرفعال کردن snap to vsync</string>\n  <string name=\"disable_vsync_desc\">این گزینه همگام‌سازی فریم‌ها با سیگنال عمودی صفحه را غیرفعال می‌کند. این ممکن است عملکرد را در دستگاه‌های کم‌قدرت با آزاد کردن منابع CPU بهبود بخشد.</string>\n  <string name=\"skip_codec_profile_check\">رد بررسی پروفایل کدک</string>\n  <string name=\"skip_codec_profile_check_desc\">هنگام شروع پخش ویدیو، از بررسی پشتیبانی کدک صرف‌نظر می‌کند. می‌تواند در نرم‌افزارهای دارای اشکال مفید باشد.</string>\n  <string name=\"force_sw_codec\">اجبار به دیکد نرم‌افزاری ویدیو</string>\n  <string name=\"force_sw_codec_desc\">تقریبا هر ویدیویی پخش می‌شود، اما عملکرد بسیار ضعیف است.</string>\n  <string name=\"playlist_order\">مرتب‌سازی لیست پخش</string>\n  <string name=\"playlist_order_added_date_newer_first\">تاریخ افزودن (جدیدترین اول)</string>\n  <string name=\"playlist_order_added_date_older_first\">تاریخ افزودن (قدیمی‌ترین اول)</string>\n  <string name=\"playlist_order_published_date_newer_first\">تاریخ انتشار (جدیدترین اول)</string>\n  <string name=\"playlist_order_published_date_older_first\">تاریخ انتشار (قدیمی‌ترین اول)</string>\n  <string name=\"playlist_order_popularity\">محبوبیت</string>\n  <string name=\"cant_do_this_for_foreign_playlist\">امکان انجام این کار برای لیست پخش دیگران وجود ندارد</string>\n  <string name=\"owned_playlist_warning\">خطا: این عمل فقط برای لیست‌های پخش متعلق به شما قابل اعمال است</string>\n  <string name=\"content_block_alt_server\">استفاده از سرور جایگزین</string>\n  <string name=\"content_block_alt_server_desc\">این گزینه را فعال کنید اگر SponsorBlock به دلایل مختلف کار نمی‌کند.</string>\n  <string name=\"unset_stream_reminder\">لغو تنظیم یادآوری پخش زنده</string>\n  <string name=\"set_stream_reminder\">تنظیم یادآوری پخش زنده</string>\n  <string name=\"playback_starts_shortly\">پخش به طور خودکار شروع می‌شود وقتی پخش زنده آماده است</string>\n  <string name=\"starting_stream\">در حال شروع پخش زنده...</string>\n  <string name=\"action_playlist_remove\">حذف از لیست پخش</string>\n  <string name=\"signin_view_title\">در حال بارگیری کد کاربر...</string>\n  <string name=\"signin_view_description\">برای ورود، این کد را در صفحه%s وارد کنید\\nتوجه: فقط با مرورگر Firefox یا Chrome کار می‌کند.</string>\n  <string name=\"signin_view_action_text\">انجام شد</string>\n  <string name=\"require_checked\">\\\"%s نیاز دارد</string>\n  <string name=\"msg_press_again_to_exit\">برای خروج دوباره فشار دهید</string>\n  <string name=\"player_remaining_time\">باقی‌مانده:\\\"%s</string>\n  <string name=\"player_ending_time\">پایان در \\\"%s</string>\n  <string name=\"badge_new_content\">محتوا جدید</string>\n  <string name=\"badge_live\">پخش زنده</string>\n  <string name=\"add_device_view_description\">برای اتصال دستگاه، این کد را در برنامه YouTube در تلفن خود در بخش تنظیمات/تماشا در تلویزیون وارد کنید\\nتوجه: این فقط با صفحه کلید Gboard کار می‌کند!</string>\n  <string name=\"playback_controls_repeat_pause\">توقف تکرار</string>\n  <string name=\"playback_controls_repeat_list\">تکرار لیست</string>\n  <string name=\"action_subscribe_off\">اشتراک غیرفعال</string>\n  <string name=\"action_subscribe_on\">اشتراک فعال</string>\n  <string name=\"skip_24_rate\">رد کردن فرمت‌های ۲۴ فریم در ثانیه (رفع حالت سینمای واقعی؟)</string>\n  <string name=\"player_disable_suggestions\">غیرفعال کردن پیشنهادات</string>\n  <string name=\"sources\">منابع</string>\n  <string name=\"releases\">انتشارات</string>\n  <string name=\"feedback\">بازخورد</string>\n  <string name=\"prefer_avc_over_vp9\">انتخاب کدک: ترجیح avc بر vp9</string>\n  <string name=\"open_chat\">نظرات/چت زنده</string>\n  <string name=\"place_chat_left\">قرار دادن چت زنده در سمت چپ</string>\n  <string name=\"use_alt_speech_recognizer\">ابزار تشخیص گفتار جایگزین</string>\n  <string name=\"time_format\">فرمت زمان</string>\n  <string name=\"time_format_24\">۲۴ ساعته</string>\n  <string name=\"time_format_12\">۱۲ ساعته (ص/م)</string>\n  <string name=\"use_alt_speech_recognizer_desc\">به خوبی کار نمی‌کند. فقط در صورت داشتن مشکل با ابزار تشخیص پیش‌فرض استفاده کنید.</string>\n  <string name=\"speech_recognizer\">ابزار تشخیص گفتار</string>\n  <string name=\"speech_recognizer_system\">سیستم (ترجیحی)</string>\n  <string name=\"speech_recognizer_external_1\">خارجی ۱ (به خوبی کار نمی‌کند، برای کار نیاز به غیرفعال کردن دسترسی به میکروفون دارد)</string>\n  <string name=\"speech_recognizer_external_2\">خارجی ۲ (به خوبی کار نمی‌کند)</string>\n  <string name=\"proxy_test_urls\">https://www.youtube.com\\nhttps://www.google.com</string>\n  <string name=\"real_channel_icon\">نمایش لوگوی کانال در پخش کننده</string>\n  <string name=\"subtitle_yellow_semi_transparent\">زرد روی پس‌زمینه نیمه شفاف</string>\n  <string name=\"subtitle_yellow_black\">زرد روی پس‌زمینه سیاه</string>\n  <string name=\"player_pixel_ratio\">نسبت پیکسل</string>\n  <string name=\"color_scheme_dark_grey_monochrome\">خاکستری تیره (معکوس)</string>\n  <string name=\"disable_mic_permission\">لطفا دسترسی به میکروفون را برای برنامه غیرفعال کنید تا این ابزار تشخیص به درستی کار کند.</string>\n  <string name=\"repeat_mode_shuffle\">تصادفی کردن هر لیست پخش</string>\n  <string name=\"chat_left\">چپ</string>\n  <string name=\"chat_right\">راست</string>\n  <string name=\"card_real_thumbnails\">جایگزینی تصاویر کوچک با فریم از ویدیو</string>\n  <string name=\"card_content\">محل گرفتن تصاویر کوچک برای کارت</string>\n  <string name=\"thumb_quality_default\">پیش‌فرض</string>\n  <string name=\"thumb_quality_start\">شروع ویدیو</string>\n  <string name=\"thumb_quality_middle\">وسط ویدیو</string>\n  <string name=\"thumb_quality_end\">پایان ویدیو</string>\n  <string name=\"video_buffer_size_none\">هیچ‌کدام</string>\n  <string name=\"content_block_status\">بررسی وضعیت SponsorBlock</string>\n  <string name=\"player_show_quality_info_bitrate\">افزودن نرخ بیت (Bitrate) به اطلاعات کیفیت</string>\n  <string name=\"player_speed_button_old_behavior\">بازگشت به رفتار قدیمی دکمه سرعت</string>\n  <string name=\"protect_settings_with_password\">محافظت از همه تنظیمات با رمز عبور</string>\n  <string name=\"enter_settings_password\">رمز عبور تنظیمات را وارد کنید</string>\n  <string name=\"child_mode\">حالت کودک</string>\n  <string name=\"child_mode_desc\">در این حالت، کاربر نمی‌تواند از جستجو استفاده کند یا هر محتوای پیشنهادی را مشاهده کند. تنظیمات تحت رمز عبور خواهند بود.</string>\n  <string name=\"lost_setting_warning\">تنظیمات برنامه تغییر خواهد کرد. لطفا از تنظیمات نسخه پشتیبان تهیه کنید.</string>\n  <string name=\"pause_history\">توقف تاریخچه تماشا</string>\n  <string name=\"clear_history\">پاک کردن تاریخچه تماشا</string>\n  <string name=\"resume_history\">از سرگیری تاریخچه تماشا</string>\n  <string name=\"chapters\">فصل‌ها</string>\n  <string name=\"auto_frame_rate_modes\">حالت‌های پشتیبانی شده</string>\n  <string name=\"loading\">در حال بارگیری…</string>\n  <string name=\"video_rotate\">چرخش</string>\n  <string name=\"hide_streams\">پنهان کردن پخش زنده از اشتراک‌ها</string>\n  <string name=\"card_multiline_subtitle\">زیرنویس چند خطی</string>\n  <string name=\"player_button_long_click\">کلیک کوتاه برای عمل سریع و کلیک طولانی برای باز کردن تنظیمات در پنجره بازشو</string>\n  <string name=\"trending_searches\">جستجوهای محبوب</string>\n  <string name=\"video_duration_any\">هر مدت</string>\n  <string name=\"video_duration\">مدت</string>\n  <string name=\"video_duration_under_4\">کمتر از ۴ دقیقه</string>\n  <string name=\"video_duration_between_4_20\">۴-۲۰ دقیقه</string>\n  <string name=\"video_duration_over_20\">بیشتر از ۲۰ دقیقه</string>\n  <string name=\"content_type\">نوع</string>\n  <string name=\"content_type_any\">همه</string>\n  <string name=\"content_type_video\">ویدیو</string>\n  <string name=\"content_type_channel\">کانال</string>\n  <string name=\"content_type_playlist\">لیست پخش</string>\n  <string name=\"content_type_movie\">فیلم</string>\n  <string name=\"video_features\">ویژگی‌ها</string>\n  <string name=\"video_feature_any\">همه</string>\n  <string name=\"video_feature_live\">پخش زنده</string>\n  <string name=\"video_feature_4k\">4K</string>\n  <string name=\"video_feature_hdr\">HDR</string>\n  <string name=\"search_sorting\">مرتب‌سازی بر اساس</string>\n  <string name=\"sort_by_relevance\">مرتبط‌ترین</string>\n  <string name=\"sort_by_views\">تعداد بازدیدها</string>\n  <string name=\"sort_by_date\">تاریخ آپلود</string>\n  <string name=\"sort_by_rating\">امتیاز</string>\n  <string name=\"clear_search_history\">پاک کردن تاریخچه جستجو</string>\n  <string name=\"remove_from_subscriptions\">پنهان کردن</string>\n  <string name=\"player_long_speed_list\">لیست سرعت طولانی</string>\n  <string name=\"disable_ok_long_press_desc\">مخصوص کنترل‌هایی که دارای اشکال هستند و دکمه OK به درستی کار نمی‌کند</string>\n  <string name=\"pressing_back\">با فشار دادن \\\"BACK\\\"</string>\n  <string name=\"alt_app_icon\">آیکون جایگزین برنامه (نیاز به راه‌اندازی مجدد دارد)</string>\n  <string name=\"cronet_desc\">Cronet یک پشته شبکه Chromium است. معمولا سریع‌تر از دیگران است. می‌تواند در مشکلات کش مفید باشد.</string>\n  <string name=\"network_stack\">ترجیح %s پشته شبکه</string>\n  <string name=\"unlock_all_formats\">باز کردن همه فرمت‌های ویدیو</string>\n  <string name=\"unlock_all_formats_desc\">در برخی دستگاه‌ها، نرم‌افزار به اشتباه برخی فرمت‌ها را به عنوان غیرقابل پشتیبانی گزارش می‌دهد، حتی اگر باشند.\\n(به عنوان مثال، تلویزیون‌های هوشمند ۱۰۸۰p تمایل دارند که 4K را به عنوان غیرقابل پشتیبانی گزارش دهند).</string>\n  <string name=\"okhttp_desc\">این پشته شبکه کندترین است اما ثبات خوبی دارد.</string>\n  <string name=\"select_channel_section\">باز کردن بخش مربوطه هنگام راه‌اندازی برنامه از کانال‌های ATV</string>\n  <string name=\"live_stream_fix_4k_desc\">هشدار، این اصلاح بازپخش پخش زنده را غیرفعال می‌کند. عملکرد پخش زنده را به شدت بهبود می‌بخشد. حداکثر رزولوشن 4K است.</string>\n  <string name=\"live_stream_fix_4k\">رفع پخش زنده (4K)</string>\n  <string name=\"enable_voice_search_desc\">برنامه رسمی با چیزی به نام \\\"پل\\\" جایگزین می‌شود. پل به انتقال درخواست‌های جستجوی جهانی به برنامه ما کمک می‌کند.</string>\n  <string name=\"not_recommend_channel\">پیشنهاد نکردن این کانال</string>\n  <string name=\"you_wont_see_this_channel\">این کانال را در پیشنهادات نخواهید دید</string>\n  <string name=\"enable_master_password\">فعال‌سازی رمز عبور اصلی</string>\n  <string name=\"enter_master_password\">رمز عبور اصلی را وارد کنید</string>\n  <string name=\"disable_stream_buffer\">غیرفعال کردن بافر پخش زنده</string>\n  <string name=\"disable_stream_buffer_desc\">رفع مواردی که پخش خیلی عقب می‌ماند. توجه: ممکن است پخش شروع به عقب ماندن کند.</string>\n  <string name=\"sony_frame_drop_fix\">رفع افت فریم شماره ۱</string>\n  <string name=\"sony_frame_drop_fix_desc\">رفع مشکل تاخیر در تلویزیون‌های سونی و برخی دستگاه‌های دیگر. توجه: ممکن است مشکلات همگام‌سازی صدا وجود داشته باشد.</string>\n  <string name=\"audio_language\">زبان صدا</string>\n  <string name=\"old_home_look\">ظاهر قدیمی بخش صفحه اصلی</string>\n  <string name=\"hide_shorts_everywhere\">پنهان کردن کوته ویدیوها در همه جا</string>\n  <string name=\"update_found\">بروزرسانی</string>\n  <string name=\"volume_boost_warning\">تجاوز از حد مجاز ممکن است به بلندگوها آسیب برساند</string>\n  <string name=\"play_video_incognito\">پخش در حالت ناشناس</string>\n  <string name=\"header_kids_home\">کودکان</string>\n  <string name=\"player_section_playlist\">استفاده از محتوای بخش فعلی به عنوان لیست پخش</string>\n  <string name=\"header_trending\">محتواهای محبوب</string>\n  <string name=\"screensaver\">محافظ صفحه</string>\n  <string name=\"player_screen_off_timeout\">زمان خاموش شدن صفحه</string>\n  <string name=\"old_update_notifications\">ظاهر قدیمی اعلان‌های بروزرسانی</string>\n  <string name=\"mark_as_watched\">علامت‌گذاری به عنوان تماشا شده</string>\n  <string name=\"player_likes_count\">نمایش تعداد پسندیدن/نپسندیدن</string>\n  <string name=\"sorting_alphabetically2\">الفبایی (پیش‌نمایش‌های سریع و متحرک)</string>\n  <string name=\"sorting_default\">پیش‌فرض (پیش‌نمایش‌های سریع و متحرک)</string>\n  <string name=\"player_ui_animations\">فعال‌سازی انیمیشن‌های پنل کنترل</string>\n  <string name=\"content_block_exclude_channel\">استثنا کردن این کانال از SponsorBlock</string>\n  <string name=\"content_block_stop_excluding_channel\">توقف استثنا کردن این کانال از SponsorBlock</string>\n  <string name=\"player_chapter_notification\">پیشرفت سریع از طریق فصل‌ها با استفاده از اعلان بازشو</string>\n  <string name=\"subtitle_remember\">به خاطر سپردن زیرنویس‌های فعال برای هر کانال</string>\n  <string name=\"amazon_frame_drop_fix\">رفع افت فریم شماره ۲</string>\n  <string name=\"amazon_frame_drop_fix_desc\">مخصوص دستگاه‌های Amazon Stick. ممکن است در دستگاه‌های دیگر نیز کار کند.</string>\n  <string name=\"share_qr_link\">لینک اشتراک‌گذاری (کد QR)</string>\n  <string name=\"msg_player_error_video_renderer\">فرمت ویدیوی انتخاب شده پشتیبانی نمی‌شود.\\nسعی کنید فرمت دیگری را با فشار دادن دکمه HQ در پخش کننده انتخاب کنید.\\nاگر کمک نکرد، سعی کنید دستگاه را مجددا راه‌اندازی کنید.</string>\n  <string name=\"msg_player_error_audio_renderer\">فرمت صوتی انتخاب شده پشتیبانی نمی‌شود.\\nسعی کنید فرمت دیگری را با فشار دادن دکمه HQ در پخش کننده انتخاب کنید.\\nاگر کمک نکرد، سعی کنید دستگاه را مجددا راه‌اندازی کنید.</string>\n  <string name=\"msg_player_error_subtitle_renderer\">فرمت زیرنویس انتخاب شده پشتیبانی نمی‌شود.\\nسعی کنید فرمت دیگری را با فشار طولانی روی دکمه CC در پخش کننده انتخاب کنید.\\nاگر کمک نکرد، سعی کنید دستگاه را مجددا راه‌اندازی کنید.</string>\n  <string name=\"msg_player_error_video_source\">آدرس URL ویدیو کار نمی‌کند یا زمان دستگاه نادرست است.\\nمعمولا این یک خطای برنامه است.</string>\n  <string name=\"msg_player_error_audio_source\">آدرس URL صدا کار نمی‌کند یا زمان دستگاه نادرست است.\\nمعمولا این یک خطای برنامه است.</string>\n  <string name=\"msg_player_error_subtitle_source\">آدرس URL زیرنویس کار نمی‌کند یا زمان دستگاه نادرست است.\\nمعمولا این یک خطای برنامه است.</string>\n  <string name=\"player_volume\">سطح صدا</string>\n  <string name=\"open_comments\">باز کردن نظرات</string>\n  <string name=\"disable_history\">غیرفعال کردن تاریخچه</string>\n  <string name=\"enable_history\">فعال‌سازی تاریخچه</string>\n  <string name=\"player_network_stack\">موتور شبکه</string>\n  <string name=\"dialog_notification\">اعلان بروزرسانی‌ها از طریق کادر بازشو</string>\n  <string name=\"default_stack_desc\">موتور شبکه داخلی. این موتور ممکن است در برخی شرایط ثبات بهتری داشته باشد.</string>\n  <string name=\"item_postion\">موقعیت</string>\n  <string name=\"screensaver_timout\">زمان محافظ صفحه</string>\n  <string name=\"screensaver_dimming\">مقدار تاریکی محافظ صفحه</string>\n  <string name=\"player_screen_off_dimming\">مقدار تاریکی خاموش شدن صفحه</string>\n  <string name=\"player_ui_on_next\">نمایش رابط کاربری پخش کننده هنگام تغییر به ویدیوی بعدی</string>\n  <string name=\"autogenerated\">به طور خودکار تولید شده</string>\n  <string name=\"player_auto_volume\">تنظیم خودکار سطح صدا</string>\n  <string name=\"header_shorts\">کوته ویدیوها</string>\n  <string name=\"player_global_focus\">پیمایش روان بین ردیف‌های دکمه‌های پخش کننده</string>\n  <string name=\"auto_history\">خودکار (استفاده از تنظیمات حساب)</string>\n  <string name=\"remember_position_subscriptions\">به خاطر سپردن آخرین موقعیت مشاهده شده در اشتراک‌ها</string>\n  <string name=\"msg_player_unknown_error\">خطای ناشناخته</string>\n  <string name=\"header_notifications\">اعلان‌ها</string>\n  <string name=\"disable_search_history\">غیرفعال کردن تاریخچه جستجو</string>\n  <string name=\"unlock_high_bitrate_formats\">باز کردن فرمت‌های نرخ بیت بالا ۱۰۸۰p vp9</string>\n  <string name=\"unlock_high_bitrate_audio_formats\">باز کردن فرمت‌های mp4a با نرخ بیت بالا</string>\n  <string name=\"color_scheme_blue\">آبی</string>\n  <string name=\"color_scheme_dark_blue\">آبی تیره</string>\n  <string name=\"player_speed_per_channel\">برای هر کانال</string>\n  <string name=\"disable_popular_searches\">غیرفعال کردن جستجوهای محبوب</string>\n  <string name=\"multi_profiles\">استفاده از تنظیمات جداگانه برای هر حساب</string>\n  <string name=\"protect_account_with_password\">محافظت از این حساب با رمز عبور</string>\n  <string name=\"enter_account_password\">رمز عبور حساب را وارد کنید</string>\n  <string name=\"show_connect_messages\">نمایش پیام‌های اتصال</string>\n  <string name=\"prefer_avc_over_vp9_desc\">هشدار: حداکثر ۱۰۸۰p</string>\n  <string name=\"not_supported_by_device\">دستگاه از این ویژگی پشتیبانی نمی‌کند</string>\n  <string name=\"video_buffer_size_lowest\">کمترین</string>\n  <string name=\"video_buffer_size_highest\">بالاترین</string>\n  <string name=\"player_exit_shortcut\">خروج از پخش کننده</string>\n  <string name=\"app_corner_clock\">صفحه اصلی: ساعت در گوشه بالا سمت راست</string>\n  <string name=\"player_corner_clock\">پخش کننده: ساعت در گوشه بالا سمت راست</string>\n  <string name=\"player_corner_ending_time\">پخش کننده: زمان پایان در گوشه بالا سمت راست</string>\n  <string name=\"pin_playlist\">افزودن لیست پخش به نوار کناری</string>\n  <string name=\"pin_channel\">افزودن کانال به نوار کناری</string>\n  <string name=\"unpin_group_from_sidebar\">حذف گروه اشتراک‌ها</string>\n  <string name=\"hide_shorts_channel\">پنهان کردن کوته ویدیوها از کانال</string>\n  <string name=\"news_row_name\">اخبار</string>\n  <string name=\"action_sound_off\">خاموش کردن صدا</string>\n  <string name=\"hide_upcoming_channel\">پنهان کردن آینده از کانال</string>\n  <string name=\"hide_upcoming_home\">پنهان کردن آینده از صفحه اصلی</string>\n  <string name=\"hide_shorts_from_trending\">پنهان کردن کوته ویدیوها از محبوب‌ها</string>\n  <string name=\"remember_position_of_live_videos\">به خاطر سپردن موقعیت پخش زنده</string>\n  <string name=\"save_playlist\">افزودن لیست پخش به بخش لیست‌های پخش</string>\n  <string name=\"skip_shorts\">رد کردن کوته ویدیوها</string>\n  <string name=\"suggestions\">پیشنهادات</string>\n  <string name=\"place_comments_left\">قرار دادن نظرات در سمت چپ</string>\n  <string name=\"speech_engine\">موتور جستجوی صوتی</string>\n  <string name=\"dearrow_status\">بررسی وضعیت سرور DeArrow</string>\n  <string name=\"player_extra_long_speed_list\">لیست سرعت بسیار طولانی</string>\n  <string name=\"old_channel_look\">ظاهر قدیمی صفحه کانال</string>\n  <string name=\"remember_position_pinned\">به خاطر سپردن آخرین موقعیت مشاهده شده در لیست‌های پخش ثابت شده</string>\n  <string name=\"unknown_source_error\">خطای منبع ناشناخته</string>\n  <string name=\"unknown_renderer_error\">خطای ناشناخته در رندرر</string>\n  <string name=\"play_next\">پخش بعدی</string>\n  <string name=\"hide_watched_from_subscriptions\">پنهان کردن ویدیوهای تماشا شده از اشتراک‌ها</string>\n  <string name=\"hide_watched_from_notifications\">پنهان کردن ویدیوهای تماشا شده از اعلان‌ها</string>\n  <string name=\"hide_unwanted_content\">پنهان کردن محتوای ناخواسته</string>\n  <string name=\"hide_watched_from_home\">پنهان کردن ویدیوهای تماشا شده از صفحه اصلی</string>\n  <string name=\"hide_watched_from_watch_later\">پنهان کردن ویدیوهای تماشا شده از لیست \\\"تماشای بعدی\\\"</string>\n  <string name=\"remote_control_permission\">کنترل از راه دور در پس‌زمینه نیاز به مجوز Overlay دارد</string>\n  <string name=\"login_from_browser\">ورود از مرورگر</string>\n  <string name=\"disable_remote_history\">غیرفعال کردن تاریخچه هنگام استفاده از کنترل از راه دور</string>\n  <string name=\"keyboard_fix\">جلوگیری از باز شدن صفحه کلید با دکمه OK (G20s و دیگران)</string>\n  <string name=\"nothing_found\">چیزی یافت نشد</string>\n  <string name=\"auto_frame_rate_desc\">این گزینه لرزش را در صحنه‌هایی که دوربین به سرعت حرکت می‌کند، مانند پخش ورزشی، حذف می‌کند.</string>\n  <string name=\"channel_filter_hint\">فیلتر کانال‌ها</string>\n  <string name=\"player_loop_shorts\">تکرار کوته ویدیوها</string>\n  <string name=\"player_quick_shorts_skip\">رد کردن کوته ویدیوها با دکمه‌های چپ/راست</string>\n  <string name=\"player_quick_skip_videos\">رد کردن ویدیوهای معمولی با دکمه‌های چپ/راست</string>\n  <string name=\"channels_filter\">نمایش فیلد فیلتر کانال‌ها در بخش کانال‌ها</string>\n  <string name=\"channel_search_bar\">نوار جستجو در صفحه کانال</string>\n  <string name=\"header_sports\">ورزش</string>\n  <string name=\"keep_finished_activities\">نگه داشتن فعالیت‌های تکمیل شده</string>\n  <string name=\"disable_channels_service\">غیرفعال کردن سرویس کانال‌ها</string>\n  <string name=\"replace_titles\">جایگزینی عناوین</string>\n  <string name=\"enable\">فعال‌سازی</string>\n  <string name=\"more_info\">اطلاعات بیشتر</string>\n  <string name=\"about_sponsorblock\">درباره SponsorBlock</string>\n  <string name=\"about_dearrow\">درباره DeArrow</string>\n  <string name=\"replace_thumbnails\">جایگزینی تصاویر کوچک</string>\n  <string name=\"crowdsourced_thumbnails\">تصاویر کوچک جمع‌آوری شده از کاربران</string>\n  <string name=\"crowdsoursed_titles\">عناوین جمع‌آوری شده از کاربران</string>\n  <string name=\"dearrow_not_submitted_thumbs\">منبع تصویر کوچک (اگر ارسال DeArrow وجود نداشته باشد)</string>\n  <string name=\"pitch_effect\">اثر Pitch</string>\n  <string name=\"fullscreen_mode\">حالت تمام صفحه (بدون نوارهای سیستم)</string>\n  <string name=\"player_only_mode\">نمایش فقط پخش کننده اگر ویدیو خارج از برنامه باز شود</string>\n  <string name=\"pinned_channel_rows\">نمایش کانال ثابت شده به عنوان ردیف‌ها</string>\n  <string name=\"prefer_ipv4\">ترجیح DNS IPv4</string>\n  <string name=\"prefer_ipv4_desc\">ممکن است در مواردی که برنامه اصلا کار نمی‌کند، کمک کند.\\nتوجه: ممکن است باعث خرابی و خطا شود (به ویژه در دستگاه‌های Android 8 یا Dune HD).</string>\n  <string name=\"long_press_for_settings\">فشار طولانی برای تنظیمات</string>\n  <string name=\"long_press_for_options\">فشار طولانی برای گزینه‌ها</string>\n  <string name=\"device_specific_backup\">پشتیبان‌گیری فقط برای این دستگاه</string>\n  <string name=\"local_backup\">پشتیبان‌گیری محلی</string>\n  <string name=\"auto_backup\">پشتیبان‌گیری خودکار (یک بار در روز)</string>\n  <string name=\"repeat_mode_reverse_list\">پخش ویدیوها در لیست پخش یا کانال به ترتیب معکوس</string>\n  <string name=\"calm_msg\">ما در حال رفع مشکل هستیم. هر از گاهی بروزرسانی‌ها را بررسی کنید.</string>\n  <string name=\"without_picture\">بدون تصویر</string>\n  <string name=\"video_disabled\">ویدیو غیرفعال شد</string>\n  <string name=\"applying_fix\">پخش کننده را نبندید. در حال اعمال رفع مشکل...</string>\n  <string name=\"player_global_focus_desc\">این ویژگی بر دکمه پخش کننده که هنگام پیمایش بین ردیف‌های دکمه‌ها فوکوس می‌گیرد، تأثیر می‌گذارد.</string>\n  <string name=\"disable_network_error_fixing\">غیرفعال کردن رفع خودکار خطاهای شبکه</string>\n  <string name=\"disable_network_error_fixing_desc\">ممکن است نیاز باشد این گزینه را فعال کنید اگر از VPN استفاده می‌کنید.</string>\n  <string name=\"recommended\">توصیه شده</string>\n  <string name=\"add_to_subscriptions_group\">افزودن/حذف از گروه اشتراک</string>\n  <string name=\"new_subscriptions_group\">گروه جدید</string>\n  <string name=\"rename_group\">تغییر نام گروه اشتراک</string>\n  <string name=\"screen_dimming_amount\">مقدار تاریکی صفحه</string>\n  <string name=\"screen_dimming_timeout\">زمان تاریکی صفحه</string>\n  <string name=\"playlists_rows\">نمایش بخش لیست‌های پخش به عنوان ردیف‌ها</string>\n  <string name=\"import_subscriptions_group\">وارد کردن</string>\n  <string name=\"lb_playback_controls_closed_captioning_disable\">غیرفعال کردن زیرنویس</string>\n  <string name=\"lb_playback_controls_closed_captioning_enable\">فعال کردن زیرنویس</string>\n  <string name=\"my_videos\">ویدیوهای من</string>\n  <string name=\"hide_mixes\">پنهان کردن میکس‌ها</string>\n  <string name=\"hide_shorts_from_search\">پنهان کردن کوته ویدیوها از نتایج جستجو</string>\n  <string name=\"context_menu_sorting\">مرتب‌سازی منوی زمینه</string>\n</resources>\n"
  },
  {
    "path": "common/src/main/res/xml/app_prefs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\">\n</PreferenceScreen>\n"
  },
  {
    "path": "common/src/stbeta/res/values/update_urls.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <string-array name=\"update_urls\">\r\n        <item>https://github.com/yuliskov/SmartTubeNext/releases/download/latest/smarttube_beta2.json</item>\r\n    </string-array>\r\n</resources>"
  },
  {
    "path": "common/src/ststable/res/values/update_urls.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <string-array name=\"update_urls\">\r\n        <item>https://github.com/yuliskov/SmartTubeNext/releases/download/latest/smarttube_stable2.json</item>\r\n    </string-array>\r\n</resources>"
  },
  {
    "path": "crowdin.yml",
    "content": "files:\r\n  - source: /common/src/main/res/values/strings.xml\r\n    translation: /common/src/main/res/values-%android_code%/strings.xml"
  },
  {
    "path": "doubletapplayerview/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "doubletapplayerview/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Viktor Krez\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "doubletapplayerview/README.md",
    "content": "# DoubleTapPlayerView\n\nA simple library to include double tap behavior to ExoPlayer's PlayerView. \nCreated to handle fast forward/rewind behavior like YouTube.\n\n![teamwork-flip](github/preview_gif.gif)\n\n# Sample app\n\nIf you would like to test the YouTube overlay, then you can either download the demo app,\nwhich can be found under Assets of the release or build it yourself from code.\nIt provides all main modifications available.\n\nThe sample videos own by *Blender Foundation* and a full list can be found [here][videolist].\n\n# Download\n\nThe Gradle dependency is available via [jitpack.io][jitpack].\nTo be able to load this library, you have to add the repository to your project's gradle file:\n\n```gradle\nallprojects {\n  repositories {\n    ...\n    maven { url 'https://jitpack.io' }\n  }\n}\n```\n\nThen, in your app's directory, you can include it the same way like other libraries:\n\n```gradle\nandroid {\n  ...\n  // If you face problems during building you should try including the below lines if you\n  // haven't already\n  \n  // compileOptions {\n  //   sourceCompatibility JavaVersion.VERSION_1_8\n  //   targetCompatibility JavaVersion.VERSION_1_8\n  // }\n}\n\ndependencies {\n  implementation 'com.github.vkay94:DoubleTapPlayerView:1.0.4'\n}\n```\n\nThe minimum API level supported by this library is API 16 as ExoPlayer does, but I can't \nverify versions below API level 21 (Lollipop) myself. So feedback is welcomed.\n\n# Getting started\n\nIn order to start using the YouTube overlay, the easiest way is to include it directly \ninto your XML layout, e.g. on top of `DoubleTapPlayerView` or inside ExoPlayer's controller:\n\n```xml\n<FrameLayout\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\" >\n    \n    <com.github.vkay94.dtpv.DoubleTapPlayerView\n        android:id=\"@+id/playerView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        \n        app:dtpv_controller=\"@+id/youtube_overlay\" />\n\n    <com.github.vkay94.dtpv.youtube.YouTubeOverlay\n        android:id=\"@+id/youtube_overlay\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:visibility=\"invisible\"\n        \n        app:yt_playerView=\"@+id/playerView\" />\n</FrameLayout>\n```\n\nThen, inside your `Activity` or `Fragment`, you can specify which preparations should be done\nbefore and after the animation, but at least, you have got to toggle the visibility of the \noverlay and reference the (Simple)ExoPlayer to it:\n\n```kotlin\nyoutube_overlay\n    .performListener(object : YouTubeOverlay.PerformListener {\n        override fun onAnimationStart() {\n            // Do UI changes when circle scaling animation starts (e.g. hide controller views)\n            youtube_overlay.visibility = View.VISIBLE\n        }\n\n        override fun onAnimationEnd() {\n            // Do UI changes when circle scaling animation starts (e.g. show controller views)\n            youtube_overlay.visibility = View.GONE\n        }\n    })\n    // Uncomment this line if you haven't set yt_playerView in XML\n    // .playerView(playerView)\n\n// Uncomment this line if you haven't set dtpv_controller in XML \n// playerView.controller(youtube_overlay)\n\n// Call this method whenever the player is released and recreated\nyoutube_overlay.player(simpleExoPlayer)\n```\n\nThis way, you have more control about the appearance, for example you could apply a fading \nanimation to it. For a full initialization you can refer to the demo application's MainActivity \nand layout files.\n\n---\n\n# API documentation\n\nThe following sections provide detailed documentation for the components of the library.\n\n## DoubleTapPlayerView\n\n`DoubleTapPlayerView` is the core of this library. It recognizes specific gestures \nwhich provides more control for the double tapping gesture.\nAn overview about the added methods can be found in the [PlayerDoubleTapListener][PlayerDoubleTapListener] \ninterface.\n\nYou can adjust how long the double tap mode remains after the last action,\nthe default value is 650 milliseconds.\n\n## YouTubeOverlay\n\n`YouTubeOverlay` is the reason for this library. It provides nearly the\nsame experience like the fast forward/rewind feature which is used by YouTube's\nAndroid app. It is highly modifiable.\n\n### XML attributes\n\nIf you add the view to your XML layout you can set some custom attributes \nto customize the view's look and behavior. \nEvery attributes value can also be get and set programmatically.\n\n| Attribute name | Description | Type |\n| ------------- | ------------| ------|\n| `yt_seekSeconds` | Fast forward/rewind seconds skip per tap. The text *xx seconds* will be changed where xx is `value`. | `int` |\n| `yt_animationDuration` |  Speed of the circle scaling / time in millis to expand completely. When this time has passed, YouTubeOverlay's `PerformListener.onAnimationEnd()` will be called. | `int` |\n| `yt_arcSize` | Arc of the background circle. The higher the value the more roundish the shape becomes. This attribute should be set dynamically depending on screen size and orientation. | `dimen` | \n| `yt_tapCircleColor` | Color of the scaling circle after tap. | `color` |\n| `yt_backgroundCircleColor` | Color of the background shape. | `color` |\n| `yt_iconAnimationDuration` | Time in millis to run through an full fade cycle. | `int` |\n| `yt_icon` | One of the three forward icons. Will be multiplied by three and mirrored for rewind. | `drawable` |\n| `yt_textAppearance` | Text appearance for the *xx seconds* text. | `style` |\n\nI'd recommend the sample app to try out the different values for them.\n\n### YouTubeOverlay.PerformListener\n\nThis interface listens to the *lifecycle* of the overlay.\n\n```kotlin\n// Obligatory: Called when the overlay is not visible and the first valid double tap event occurred.\n// Visibility of the overlay should be set to VISIBLE within this interface method.\nfun onAnimationStart()\n\n// Obligatory: Called when the circle animation is finished.\n// Visibility of the overlay should be set to GONE or INVISIBLE within this interface method.\nfun onAnimationEnd()\n\n// Optional: Determines whether the player should forward (true), rewind (false) or ignore (null) taps.\nfun shouldForward(player: Player, playerView: DoubleTapPlayerView, posX: Float): Boolean?\n```\n\n### SeekListener\n\nThis interface reacts to the events during rewinding/forwarding.\n\n```kotlin\n// Called when the start of the video is reached\nfun onVideoStartReached()\n\n// Called when the end of the video is reached\nfun onVideoEndReached()\n```\n\n[videolist]: https://gist.github.com/jsturgis/3b19447b304616f18657\n[jitpack]: https://jitpack.io/#vkay94/DoubleTapPlayerView\n[PlayerDoubleTapListener]: https://github.com/vkay94/DoubleTapPlayerView/blob/master/doubletapplayerview/src/main/java/com/github/vkay94/dtpv/PlayerDoubleTapListener.java\n"
  },
  {
    "path": "doubletapplayerview/build.gradle",
    "content": "apply from: gradle.ext.sharedModulesConstants\napply plugin: 'kotlin-android'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.properties.compileSdkVersion\n    buildToolsVersion project.properties.buildToolsVersion\n\n    defaultConfig {\n        minSdkVersion project.properties.minSdkVersion\n        targetSdkVersion project.properties.targetSdkVersion\n        versionCode 40001\n        versionName '1.0.4'\n\n        vectorDrawables.useSupportLibrary = true\n\n        consumerProguardFiles 'consumer-rules.pro'\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    // kotlinOptions {\n    //     jvmTarget = JavaVersion.VERSION_1_8\n    // }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n\n    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + kotlinVersion\n    implementation 'androidx.appcompat:appcompat:' + appCompatXVersion\n    implementation 'androidx.core:core-ktx:' + kotlinCoreXVersion\n    implementation 'androidx.constraintlayout:constraintlayout:' + constraintLayoutXVersion\n\n    implementation project(':exoplayer-library-core')\n    implementation project(':exoplayer-library-ui')\n}\n"
  },
  {
    "path": "doubletapplayerview/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "doubletapplayerview/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": "doubletapplayerview/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.github.vkay94.dtpv\" />\n"
  },
  {
    "path": "doubletapplayerview/src/main/java/com/github/vkay94/dtpv/DoubleTapPlayerAdapter.kt",
    "content": "package com.github.vkay94.dtpv\n\nimport android.os.Handler\nimport android.os.Looper\nimport android.util.Log\nimport android.view.GestureDetector\nimport android.view.MotionEvent\nimport android.view.View\nimport androidx.core.view.GestureDetectorCompat\nimport com.google.android.exoplayer2.ui.PlayerView\n\n/**\n * Custom player class for Double-Tapping listening\n */\nclass DoubleTapPlayerAdapter(private val playerView: View): DoubleTapPlayerView {\n    private val gestureDetector: GestureDetectorCompat\n    private val gestureListener: DoubleTapGestureListener = DoubleTapGestureListener(playerView)\n\n    private var controller: PlayerDoubleTapListener? = null\n        get() = gestureListener.controls\n        set(value) {\n            gestureListener.controls = value\n            field = value\n        }\n\n    interface OnSingleTap {\n        fun onSingleTap(event: MotionEvent)\n    }\n\n    init {\n        gestureDetector = GestureDetectorCompat(playerView.context, gestureListener)\n    }\n\n    override val playerWidth: Int\n        get() = playerView.width\n\n    /**\n     * If this field is set to `true` this view will handle double tapping, otherwise it will\n     * handle touches the same way as the original [PlayerView][com.google.android.exoplayer2.ui.PlayerView] does\n     */\n    override var isDoubleTapEnabled = true\n\n    /**\n     * Time window a double tap is active, so a followed tap is calling a gesture detector\n     * method instead of normal tap (see [PlayerView.onTouchEvent])\n     */\n    override var doubleTapDelay: Long = 700\n        get() = gestureListener.doubleTapDelay\n        set(value) {\n            gestureListener.doubleTapDelay = value\n            field = value\n        }\n\n    /**\n     * Sets the [PlayerDoubleTapListener] which handles the gesture callbacks.\n     *\n     * Primarily used for [YouTubeOverlay][com.github.vkay94.dtpv.youtube.YouTubeOverlay]\n     */\n    override fun controller(controller: PlayerDoubleTapListener?) = apply { this.controller = controller }\n\n    /**\n     * Returns the current state of double tapping.\n     */\n    override fun isInDoubleTapMode(): Boolean = gestureListener.isDoubleTapping\n\n    /**\n     * Resets the timeout to keep in double tap mode.\n     *\n     * Called once in [PlayerDoubleTapListener.onDoubleTapStarted]. Needs to be called\n     * from outside if the double tap is customized / overridden to detect ongoing taps\n     */\n    override fun keepInDoubleTapMode() {\n        gestureListener.keepInDoubleTapMode()\n    }\n\n    /**\n     * Cancels double tap mode instantly by calling [PlayerDoubleTapListener.onDoubleTapFinished]\n     */\n    override fun cancelInDoubleTapMode() {\n        gestureListener.cancelInDoubleTapMode()\n    }\n\n    fun onTouchEvent(ev: MotionEvent): Boolean {\n        if (isDoubleTapEnabled) {\n            gestureDetector.onTouchEvent(ev)\n\n            // Do not trigger original behavior when double tapping\n            // otherwise the controller would show/hide - it would flack\n            return true\n        }\n        return false\n    }\n\n    fun onSingleTap(singleTap: OnSingleTap?) = apply { gestureListener.onSingleTap = singleTap }\n\n    /**\n     * Gesture Listener for double tapping\n     *\n     * For more information which methods are called in certain situations look for\n     * [GestureDetector.onTouchEvent][android.view.GestureDetector.onTouchEvent],\n     * especially for ACTION_DOWN and ACTION_UP\n     */\n    private class DoubleTapGestureListener(private val rootView: View) : GestureDetector.SimpleOnGestureListener() {\n\n        private val mHandler = Handler(Looper.getMainLooper())\n        private val mRunnable = Runnable {\n            if (DEBUG) Log.d(TAG, \"Runnable called\")\n            isDoubleTapping = false\n            controls?.onDoubleTapFinished()\n        }\n\n        var controls: PlayerDoubleTapListener? = null\n        var isDoubleTapping = false\n        var doubleTapDelay: Long = 650\n        var onSingleTap: OnSingleTap? = null\n\n        /**\n         * Resets the timeout to keep in double tap mode.\n         *\n         * Called once in [PlayerDoubleTapListener.onDoubleTapStarted]. Needs to be called\n         * from outside if the double tap is customized / overridden to detect ongoing taps\n         */\n        fun keepInDoubleTapMode() {\n            isDoubleTapping = true\n            mHandler.removeCallbacks(mRunnable)\n            mHandler.postDelayed(mRunnable, doubleTapDelay)\n        }\n\n        /**\n         * Cancels double tap mode instantly by calling [PlayerDoubleTapListener.onDoubleTapFinished]\n         */\n        fun cancelInDoubleTapMode() {\n            mHandler.removeCallbacks(mRunnable)\n            isDoubleTapping = false\n            controls?.onDoubleTapFinished()\n        }\n\n        override fun onDown(e: MotionEvent): Boolean {\n            // Used to override the other methods\n            if (isDoubleTapping) {\n                controls?.onDoubleTapProgressDown(e.x, e.y)\n                return true\n            }\n            return super.onDown(e)\n        }\n\n        override fun onSingleTapUp(e: MotionEvent): Boolean {\n            if (isDoubleTapping) {\n                if (DEBUG) Log.d(TAG, \"onSingleTapUp: isDoubleTapping = true\")\n                controls?.onDoubleTapProgressUp(e.x, e.y)\n                return true\n            }\n            return super.onSingleTapUp(e)\n        }\n\n        override fun onSingleTapConfirmed(e: MotionEvent): Boolean {\n            // Ignore this event if double tapping is still active\n            // Return true needed because this method is also called if you tap e.g. three times\n            // in a row, therefore the controller would appear since the original behavior is\n            // to hide and show on single tap\n            if (isDoubleTapping) return true\n            if (DEBUG) Log.d(TAG, \"onSingleTapConfirmed: isDoubleTap = false\")\n            onSingleTap?.onSingleTap(e)\n            return rootView.performClick()\n        }\n\n        override fun onDoubleTap(e: MotionEvent): Boolean {\n            // First tap (ACTION_DOWN) of both taps\n            if (DEBUG) Log.d(TAG, \"onDoubleTap\")\n            if (!isDoubleTapping) {\n                isDoubleTapping = true\n                keepInDoubleTapMode()\n                controls?.onDoubleTapStarted(e.x, e.y)\n            }\n            return true\n        }\n\n        override fun onDoubleTapEvent(e: MotionEvent): Boolean {\n            // Second tap (ACTION_UP) of both taps\n            if (e.actionMasked == MotionEvent.ACTION_UP && isDoubleTapping) {\n                if (DEBUG) Log.d(\n                    TAG,\n                    \"onDoubleTapEvent, ACTION_UP\"\n                )\n                controls?.onDoubleTapProgressUp(e.x, e.y)\n                return true\n            }\n            return super.onDoubleTapEvent(e)\n        }\n\n        companion object {\n            private const val TAG = \".DTGListener\"\n            private var DEBUG = true\n        }\n    }\n}"
  },
  {
    "path": "doubletapplayerview/src/main/java/com/github/vkay94/dtpv/DoubleTapPlayerView.kt",
    "content": "package com.github.vkay94.dtpv\r\n\r\nimport com.google.android.exoplayer2.ui.PlayerView\r\n\r\ninterface DoubleTapPlayerView {\r\n    /**\r\n     * Returns the View width\r\n     */\r\n    val playerWidth: Int\r\n    /**\r\n     * If this field is set to `true` this view will handle double tapping, otherwise it will\r\n     * handle touches the same way as the original [PlayerView][com.google.android.exoplayer2.ui.PlayerView] does\r\n     */\r\n    var isDoubleTapEnabled: Boolean\r\n    /**\r\n     * Time window a double tap is active, so a followed tap is calling a gesture detector\r\n     * method instead of normal tap (see [PlayerView.onTouchEvent])\r\n     */\r\n    var doubleTapDelay: Long\r\n    /**\r\n     * Sets the [PlayerDoubleTapListener] which handles the gesture callbacks.\r\n     *\r\n     * Primarily used for [YouTubeOverlay][com.github.vkay94.dtpv.youtube.YouTubeOverlay]\r\n     */\r\n    fun controller(controller: PlayerDoubleTapListener?): DoubleTapPlayerView\r\n    /**\r\n     * Returns the current state of double tapping.\r\n     */\r\n    fun isInDoubleTapMode(): Boolean\r\n    /**\r\n     * Resets the timeout to keep in double tap mode.\r\n     *\r\n     * Called once in [PlayerDoubleTapListener.onDoubleTapStarted]. Needs to be called\r\n     * from outside if the double tap is customized / overridden to detect ongoing taps\r\n     */\r\n    fun keepInDoubleTapMode()\r\n    /**\r\n     * Cancels double tap mode instantly by calling [PlayerDoubleTapListener.onDoubleTapFinished]\r\n     */\r\n    fun cancelInDoubleTapMode()\r\n}"
  },
  {
    "path": "doubletapplayerview/src/main/java/com/github/vkay94/dtpv/DoubleTapPlayerViewImpl.kt",
    "content": "package com.github.vkay94.dtpv\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.os.Handler\nimport android.os.Looper\nimport android.util.AttributeSet\nimport android.util.Log\nimport android.view.GestureDetector\nimport android.view.MotionEvent\nimport android.view.View\nimport androidx.core.view.GestureDetectorCompat\nimport com.google.android.exoplayer2.ui.PlayerView\nimport androidx.core.content.withStyledAttributes\n\n/**\n * Custom player class for Double-Tapping listening\n */\nopen class DoubleTapPlayerViewImpl @JvmOverloads constructor(\n    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0\n) : PlayerView(context, attrs, defStyleAttr), DoubleTapPlayerView {\n\n    private val gestureDetector: GestureDetectorCompat\n    private val gestureListener: DoubleTapGestureListener = DoubleTapGestureListener(rootView)\n\n    private var controller: PlayerDoubleTapListener? = null\n        get() = gestureListener.controls\n        set(value) {\n            gestureListener.controls = value\n            field = value\n        }\n\n    private var controllerRef: Int = -1\n\n    init {\n        gestureDetector = GestureDetectorCompat(context, gestureListener)\n\n        // Check whether controller is set through XML\n        attrs?.let {\n            context.withStyledAttributes(attrs, R.styleable.DoubleTapPlayerView, 0, 0) {\n                controllerRef = getResourceId(R.styleable.DoubleTapPlayerView_dtpv_controller, -1)\n            }\n        }\n    }\n\n    override val playerWidth: Int\n        get() = width\n\n    /**\n     * If this field is set to `true` this view will handle double tapping, otherwise it will\n     * handle touches the same way as the original [PlayerView][com.google.android.exoplayer2.ui.PlayerView] does\n     */\n    override var isDoubleTapEnabled = true\n\n    /**\n     * Time window a double tap is active, so a followed tap is calling a gesture detector\n     * method instead of normal tap (see [PlayerView.onTouchEvent])\n     */\n    override var doubleTapDelay: Long = 700\n        get() = gestureListener.doubleTapDelay\n        set(value) {\n            gestureListener.doubleTapDelay = value\n            field = value\n        }\n\n    /**\n     * Sets the [PlayerDoubleTapListener] which handles the gesture callbacks.\n     *\n     * Primarily used for [YouTubeOverlay][com.github.vkay94.dtpv.youtube.YouTubeOverlay]\n     */\n    override fun controller(controller: PlayerDoubleTapListener?) = apply { this.controller = controller }\n\n    /**\n     * Returns the current state of double tapping.\n     */\n    override fun isInDoubleTapMode(): Boolean = gestureListener.isDoubleTapping\n\n    /**\n     * Resets the timeout to keep in double tap mode.\n     *\n     * Called once in [PlayerDoubleTapListener.onDoubleTapStarted]. Needs to be called\n     * from outside if the double tap is customized / overridden to detect ongoing taps\n     */\n    override fun keepInDoubleTapMode() {\n        gestureListener.keepInDoubleTapMode()\n    }\n\n    /**\n     * Cancels double tap mode instantly by calling [PlayerDoubleTapListener.onDoubleTapFinished]\n     */\n    override fun cancelInDoubleTapMode() {\n        gestureListener.cancelInDoubleTapMode()\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    override fun onTouchEvent(ev: MotionEvent): Boolean {\n        if (isDoubleTapEnabled) {\n            gestureDetector.onTouchEvent(ev)\n\n            // Do not trigger original behavior when double tapping\n            // otherwise the controller would show/hide - it would flack\n            return true\n        }\n        return super.onTouchEvent(ev)\n    }\n\n    override fun onAttachedToWindow() {\n        super.onAttachedToWindow()\n\n        // If the PlayerView is set by XML then call the corresponding setter method\n        if (controllerRef != -1) {\n            try {\n                val view = (this.parent as View).findViewById(controllerRef) as View\n                if (view is PlayerDoubleTapListener) {\n                    controller(view)\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n                Log.e(\"DoubleTapPlayerView\",\n                    \"controllerRef is either invalid or not PlayerDoubleTapListener: ${e.message}\")\n            }\n        }\n\n    }\n\n    /**\n     * Gesture Listener for double tapping\n     *\n     * For more information which methods are called in certain situations look for\n     * [GestureDetector.onTouchEvent][android.view.GestureDetector.onTouchEvent],\n     * especially for ACTION_DOWN and ACTION_UP\n     */\n    private class DoubleTapGestureListener(private val rootView: View) : GestureDetector.SimpleOnGestureListener() {\n\n        private val mHandler = Handler(Looper.getMainLooper())\n        private val mRunnable = Runnable {\n            if (DEBUG) Log.d(TAG, \"Runnable called\")\n            isDoubleTapping = false\n            controls?.onDoubleTapFinished()\n        }\n\n        var controls: PlayerDoubleTapListener? = null\n        var isDoubleTapping = false\n        var doubleTapDelay: Long = 650\n\n        /**\n         * Resets the timeout to keep in double tap mode.\n         *\n         * Called once in [PlayerDoubleTapListener.onDoubleTapStarted]. Needs to be called\n         * from outside if the double tap is customized / overridden to detect ongoing taps\n         */\n        fun keepInDoubleTapMode() {\n            isDoubleTapping = true\n            mHandler.removeCallbacks(mRunnable)\n            mHandler.postDelayed(mRunnable, doubleTapDelay)\n        }\n\n        /**\n         * Cancels double tap mode instantly by calling [PlayerDoubleTapListener.onDoubleTapFinished]\n         */\n        fun cancelInDoubleTapMode() {\n            mHandler.removeCallbacks(mRunnable)\n            isDoubleTapping = false\n            controls?.onDoubleTapFinished()\n        }\n\n        override fun onDown(e: MotionEvent): Boolean {\n            // Used to override the other methods\n            if (isDoubleTapping) {\n                controls?.onDoubleTapProgressDown(e.x, e.y)\n                return true\n            }\n            return super.onDown(e)\n        }\n\n        override fun onSingleTapUp(e: MotionEvent): Boolean {\n            if (isDoubleTapping) {\n                if (DEBUG) Log.d(TAG, \"onSingleTapUp: isDoubleTapping = true\")\n                controls?.onDoubleTapProgressUp(e.x, e.y)\n                return true\n            }\n            return super.onSingleTapUp(e)\n        }\n\n        override fun onSingleTapConfirmed(e: MotionEvent): Boolean {\n            // Ignore this event if double tapping is still active\n            // Return true needed because this method is also called if you tap e.g. three times\n            // in a row, therefore the controller would appear since the original behavior is\n            // to hide and show on single tap\n            if (isDoubleTapping) return true\n            if (DEBUG) Log.d(TAG, \"onSingleTapConfirmed: isDoubleTap = false\")\n            return rootView.performClick()\n        }\n\n        override fun onDoubleTap(e: MotionEvent): Boolean {\n            // First tap (ACTION_DOWN) of both taps\n            if (DEBUG) Log.d(TAG, \"onDoubleTap\")\n            if (!isDoubleTapping) {\n                isDoubleTapping = true\n                keepInDoubleTapMode()\n                controls?.onDoubleTapStarted(e.x, e.y)\n            }\n            return true\n        }\n\n        override fun onDoubleTapEvent(e: MotionEvent): Boolean {\n            // Second tap (ACTION_UP) of both taps\n            if (e.actionMasked == MotionEvent.ACTION_UP && isDoubleTapping) {\n                if (DEBUG) Log.d(\n                    TAG,\n                    \"onDoubleTapEvent, ACTION_UP\"\n                )\n                controls?.onDoubleTapProgressUp(e.x, e.y)\n                return true\n            }\n            return super.onDoubleTapEvent(e)\n        }\n\n        companion object {\n            private const val TAG = \".DTGListener\"\n            private var DEBUG = true\n        }\n    }\n}"
  },
  {
    "path": "doubletapplayerview/src/main/java/com/github/vkay94/dtpv/PlayerDoubleTapListener.java",
    "content": "package com.github.vkay94.dtpv;\n\npublic interface PlayerDoubleTapListener {\n\n    /**\n     * Called when double tapping starts, after double tap gesture\n     *\n     * @param posX x tap position on the root view\n     * @param posY y tap position on the root view\n     */\n    default void onDoubleTapStarted(float posX, float posY) { }\n\n    /**\n     * Called for each ongoing tap (also single tap) (MotionEvent#ACTION_DOWN)\n     * when double tap started and still in double tap mode defined\n     * by {@link DoubleTapPlayerView#getDoubleTapDelay()}\n     *\n     * @param posX x tap position on the root view\n     * @param posY y tap position on the root view\n     */\n    default void onDoubleTapProgressDown(float posX, float posY) { }\n\n    /**\n     * Called for each ongoing tap (also single tap) (MotionEvent#ACTION_UP}\n     * when double tap started and still in double tap mode defined\n     * by {@link DoubleTapPlayerView#getDoubleTapDelay()}\n     *\n     * @param posX x tap position on the root view\n     * @param posY y tap position on the root view\n     */\n    default void onDoubleTapProgressUp(float posX, float posY) { }\n\n    /**\n     * Called when {@link DoubleTapPlayerView#getDoubleTapDelay()} is over\n     */\n    default void onDoubleTapFinished() { }\n}"
  },
  {
    "path": "doubletapplayerview/src/main/java/com/github/vkay94/dtpv/SeekListener.kt",
    "content": "package com.github.vkay94.dtpv\n\ninterface SeekListener {\n    /**\n     * Called when video start reached during rewinding\n     */\n    fun onVideoStartReached() {}\n\n    /**\n     * Called when video end reached during forwarding\n     */\n    fun onVideoEndReached() {}\n}"
  },
  {
    "path": "doubletapplayerview/src/main/java/com/github/vkay94/dtpv/youtube/YouTubeOverlay.kt",
    "content": "package com.github.vkay94.dtpv.youtube\n\nimport android.content.Context\nimport android.media.session.PlaybackState\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.widget.TextView\nimport androidx.annotation.*\nimport androidx.constraintlayout.widget.ConstraintLayout\nimport androidx.constraintlayout.widget.ConstraintSet\nimport androidx.core.content.ContextCompat\nimport androidx.core.widget.TextViewCompat\nimport com.github.vkay94.dtpv.DoubleTapPlayerView\nimport com.github.vkay94.dtpv.PlayerDoubleTapListener\nimport com.github.vkay94.dtpv.R\nimport com.github.vkay94.dtpv.SeekListener\nimport com.github.vkay94.dtpv.youtube.views.CircleClipTapView\nimport com.github.vkay94.dtpv.youtube.views.SecondsView\nimport com.google.android.exoplayer2.Player\nimport com.google.android.exoplayer2.ui.PlayerView\nimport androidx.core.content.withStyledAttributes\n\n\n/**\n * Overlay for [DoubleTapPlayerView] to create a similar UI/UX experience like the official\n * YouTube Android app.\n *\n * The overlay has the typical YouTube scaling circle animation and provides some configurations\n * which can't be accomplished with the regular Android Ripple (I didn't find any options in the\n * documentation ...).\n */\nclass YouTubeOverlay(context: Context, private val attrs: AttributeSet?) :\n    ConstraintLayout(context, attrs), PlayerDoubleTapListener {\n\n    private var rootLayout: ConstraintLayout\n    private var secondsView: SecondsView\n    private var circleClipTapView: CircleClipTapView\n\n    constructor(context: Context) : this(context, null) {\n        // Hide overlay initially when added programmatically\n        this.visibility = View.INVISIBLE\n    }\n\n    private var playerViewRef: Int = -1\n\n    // Player behaviors\n    private var playerView: DoubleTapPlayerView? = null\n    private var player: Player? = null\n\n    init {\n        LayoutInflater.from(context).inflate(R.layout.yt_overlay, this, true)\n\n        rootLayout = findViewById(R.id.root_constraint_layout)\n        secondsView = findViewById(R.id.seconds_view)\n        circleClipTapView = findViewById(R.id.circle_clip_tap_view)\n\n        // Initialize UI components\n        initializeAttributes()\n        secondsView.isForward = true\n        changeConstraints(true)\n\n        // This code snippet is executed when the circle scale animation is finished\n        circleClipTapView.performAtEnd = {\n            performListener?.onAnimationEnd()\n\n            secondsView.visibility = View.INVISIBLE\n            secondsView.seconds = 0\n            secondsView.stop()\n        }\n    }\n\n    /**\n     * Sets all optional XML attributes and defaults\n     */\n    private fun initializeAttributes() {\n        if (attrs != null) {\n            context.withStyledAttributes(\n                attrs,\n                R.styleable.YouTubeOverlay, 0, 0\n            ) {\n\n                // PlayerView => see onAttachToWindow\n                playerViewRef = getResourceId(R.styleable.YouTubeOverlay_yt_playerView, -1)\n\n                // Durations\n                animationDuration = getInt(\n                    R.styleable.YouTubeOverlay_yt_animationDuration, 650\n                ).toLong()\n\n                seekSeconds = getInt(\n                    R.styleable.YouTubeOverlay_yt_seekSeconds, 10\n                )\n\n                iconAnimationDuration = getInt(\n                    R.styleable.YouTubeOverlay_yt_iconAnimationDuration, 750\n                ).toLong()\n\n                // Arc size\n                arcSize = getDimensionPixelSize(\n                    R.styleable.YouTubeOverlay_yt_arcSize,\n                    context.resources.getDimensionPixelSize(R.dimen.dtpv_yt_arc_size)\n                ).toFloat()\n\n                // Colors\n                tapCircleColor = getColor(\n                    R.styleable.YouTubeOverlay_yt_tapCircleColor,\n                    ContextCompat.getColor(context, R.color.dtpv_yt_tap_circle_color)\n                )\n\n                circleBackgroundColor = getColor(\n                    R.styleable.YouTubeOverlay_yt_backgroundCircleColor,\n                    ContextCompat.getColor(context, R.color.dtpv_yt_background_circle_color)\n                )\n\n                // Seconds TextAppearance\n                textAppearance = getResourceId(\n                    R.styleable.YouTubeOverlay_yt_textAppearance,\n                    R.style.YTOSecondsTextAppearance\n                )\n\n                // Seconds icon\n                icon = getResourceId(\n                    R.styleable.YouTubeOverlay_yt_icon,\n                    R.drawable.ic_play_triangle\n                )\n\n            }\n\n        } else {\n            // Set defaults\n            arcSize = context.resources.getDimensionPixelSize(R.dimen.dtpv_yt_arc_size).toFloat()\n            tapCircleColor = ContextCompat.getColor(context, R.color.dtpv_yt_tap_circle_color)\n            circleBackgroundColor = ContextCompat.getColor(context, R.color.dtpv_yt_background_circle_color)\n            animationDuration = 650\n            iconAnimationDuration = 750\n            seekSeconds = 10\n            textAppearance = R.style.YTOSecondsTextAppearance\n        }\n    }\n\n    override fun onAttachedToWindow() {\n        super.onAttachedToWindow()\n\n        // If the PlayerView is set by XML then call the corresponding setter method\n        if (playerViewRef != -1)\n            playerView((this.parent as View).findViewById(playerViewRef) as DoubleTapPlayerView)\n    }\n\n    /**\n     * Obligatory call if playerView is not set via XML!\n     *\n     * Links the DoubleTapPlayerView to this view for recognizing the tapped position.\n     *\n     * @param playerView PlayerView which triggers the event\n     */\n    fun playerView(playerView: DoubleTapPlayerView?) = apply {\n        this.playerView = playerView\n    }\n\n    /**\n     * Obligatory call! Needs to be called whenever the Player changes.\n     *\n     * Performs seekTo-calls on the ExoPlayer's Player instance.\n     *\n     * @param player PlayerView which triggers the event\n     */\n    fun player(player: Player?) = apply {\n        this.player = player\n    }\n\n    /*\n        Properties\n     */\n\n    private var seekListener: SeekListener? = null\n\n    /**\n     * Optional: Sets a listener to observe whether double tap reached the start / end of the video\n     */\n    fun seekListener(listener: SeekListener?) = apply {\n        seekListener = listener\n    }\n\n    private var performListener: PerformListener? = null\n\n    /**\n     * Sets a listener to execute some code before and after the animation\n     * (for example UI changes (hide and show views etc.))\n     */\n    fun performListener(listener: PerformListener?) = apply {\n        performListener = listener\n    }\n\n    /**\n     * Forward / rewind duration on a tap in seconds.\n     */\n    var seekSeconds: Int = 0\n        private set\n\n    fun seekSeconds(seconds: Int) = apply {\n        seekSeconds = seconds\n    }\n\n    /**\n     * Color of the scaling circle on touch feedback.\n     */\n    var tapCircleColor: Int\n        get() = circleClipTapView.circleColor\n        private set(value) {\n            circleClipTapView.circleColor = value\n        }\n\n    fun tapCircleColorRes(@ColorRes resId: Int) = apply {\n        tapCircleColor = ContextCompat.getColor(context, resId)\n    }\n\n    fun tapCircleColorInt(@ColorInt color: Int) = apply {\n        tapCircleColor = color\n    }\n\n    /**\n     * Color of the clipped background circle\n     */\n    var circleBackgroundColor: Int\n        get() = circleClipTapView.circleBackgroundColor\n        private set(value) {\n            circleClipTapView.circleBackgroundColor = value\n        }\n\n    fun circleBackgroundColorRes(@ColorRes resId: Int) = apply {\n        circleBackgroundColor = ContextCompat.getColor(context, resId)\n    }\n\n    fun circleBackgroundColorInt(@ColorInt color: Int) = apply {\n        circleBackgroundColor = color\n    }\n\n    /**\n     * Duration of the circle scaling animation / speed in milliseconds.\n     * The overlay keeps visible until the animation finishes.\n     */\n    var animationDuration: Long\n        get() = circleClipTapView.animationDuration\n        private set(value) {\n            circleClipTapView.animationDuration = value\n        }\n\n    fun animationDuration(duration: Long) = apply {\n        animationDuration = duration\n    }\n\n    /**\n     * Size of the arc which will be clipped from the background circle.\n     * The greater the value the more roundish the shape becomes\n     */\n    var arcSize: Float\n        get() = circleClipTapView.arcSize\n        internal set(value) {\n            circleClipTapView.arcSize = value\n        }\n\n    fun arcSize(@DimenRes resId: Int) = apply {\n        arcSize = context.resources.getDimension(resId)\n    }\n\n    fun arcSize(px: Float) = apply {\n        arcSize = px\n    }\n\n    /**\n     * Duration the icon animation (fade in + fade out) for a full cycle in milliseconds.\n     */\n    var iconAnimationDuration: Long = 750\n        get() = secondsView.cycleDuration\n        private set(value) {\n            secondsView.cycleDuration = value\n            field = value\n        }\n\n    fun iconAnimationDuration(duration: Long) = apply {\n        iconAnimationDuration = duration\n    }\n\n    /**\n     * One of the three forward icons which will be animated above the seconds indicator.\n     * The rewind icon will be the 180° mirrored version.\n     *\n     * Keep in mind that padding on the left and right of the drawable will be rendered which\n     * could result in additional space between the three icons.\n     */\n    @DrawableRes\n    var icon: Int = 0\n        get() = secondsView.icon\n        private set(value) {\n            secondsView.stop()\n            secondsView.icon = value\n            field = value\n        }\n\n    fun icon(@DrawableRes resId: Int) = apply {\n        icon = resId\n    }\n\n    /**\n     * Text appearance of the *xx seconds* text.\n     */\n    @StyleRes\n    var textAppearance: Int = 0\n        private set(value) {\n            TextViewCompat.setTextAppearance(secondsView.textView, value)\n            field = value\n        }\n\n    fun textAppearance(@StyleRes resId: Int) = apply {\n        textAppearance = resId\n    }\n\n    /**\n     * TextView view for *xx seconds*.\n     *\n     * In case of you'd like to change some specific attributes of the TextView in runtime.\n     */\n    val secondsTextView: TextView\n        get() = secondsView.textView\n\n    override fun onDoubleTapStarted(posX: Float, posY: Float) {\n        if (player == null || playerView == null)\n            return\n\n        if (performListener?.shouldForward(player!!, playerView!!, posX) == null)\n            return\n    }\n\n    override fun onDoubleTapProgressUp(posX: Float, posY: Float) {\n\n        // Check first whether forwarding/rewinding is \"valid\"\n        if (player == null || playerView == null) return\n\n        val shouldForward = performListener?.shouldForward(player!!, playerView!!, posX)\n\n        // YouTube behavior: show overlay on MOTION_UP\n        // But check whether the first double tap is in invalid area\n        if (this.visibility != View.VISIBLE) {\n            if (shouldForward != null) {\n                performListener?.onAnimationStart()\n                secondsView.visibility = View.VISIBLE\n                secondsView.start()\n            } else\n                return\n        }\n\n        when (shouldForward) {\n            false -> {\n\n                // First time tap or switched\n                if (secondsView.isForward) {\n                    changeConstraints(false)\n                    secondsView.apply {\n                        isForward = false\n                        seconds = 0\n                    }\n                }\n\n                // Cancel ripple and start new without triggering overlay disappearance\n                // (resetting instead of ending)\n                circleClipTapView.resetAnimation {\n                    circleClipTapView.updatePosition(posX, posY)\n                }\n                rewinding()\n            }\n            true -> {\n\n                // First time tap or switched\n                if (!secondsView.isForward) {\n                    changeConstraints(true)\n                    secondsView.apply {\n                        isForward = true\n                        seconds = 0\n                    }\n                }\n\n                // Cancel ripple and start new without triggering overlay disappearance\n                // (resetting instead of ending)\n                circleClipTapView.resetAnimation {\n                    circleClipTapView.updatePosition(posX, posY)\n                }\n                forwarding()\n            }\n            else -> {\n                // Middle area tapped: do nothing\n                //\n                // playerView?.cancelInDoubleTapMode()\n                // circle_clip_tap_view.endAnimation()\n                // triangle_seconds_view.stop()\n            }\n        }\n    }\n\n    /**\n     * Seeks the video to desired position.\n     * Calls interface functions when start reached ([SeekListener.onVideoStartReached])\n     * or when end reached ([SeekListener.onVideoEndReached])\n     *\n     * @param newPosition desired position\n     */\n    private fun seekToPosition(newPosition: Long?) {\n        if (newPosition == null) return\n\n        // Start of the video reached\n        if (newPosition <= 0) {\n            player?.seekTo(0)\n\n            seekListener?.onVideoStartReached()\n            return\n        }\n\n        // End of the video reached\n        player?.duration?.let { total ->\n            if (newPosition >= total) {\n                player?.seekTo(total)\n\n                seekListener?.onVideoEndReached()\n                return\n            }\n        }\n\n        // Otherwise\n        playerView?.keepInDoubleTapMode()\n        player?.seekTo(newPosition)\n    }\n\n    private fun forwarding() {\n        secondsView.seconds += seekSeconds\n        seekToPosition(player?.currentPosition?.plus(seekSeconds * 1000))\n    }\n\n    private fun rewinding() {\n        secondsView.seconds += seekSeconds\n        seekToPosition(player?.currentPosition?.minus(seekSeconds * 1000))\n    }\n\n    private fun changeConstraints(forward: Boolean) {\n        val constraintSet = ConstraintSet()\n        with(constraintSet) {\n            clone(rootLayout)\n            if (forward) {\n                clear(secondsView.id, ConstraintSet.START)\n                connect(secondsView.id, ConstraintSet.END,\n                    ConstraintSet.PARENT_ID, ConstraintSet.END)\n            } else {\n                clear(secondsView.id, ConstraintSet.END)\n                connect(secondsView.id, ConstraintSet.START,\n                    ConstraintSet.PARENT_ID, ConstraintSet.START)\n            }\n            secondsView.start()\n            applyTo(rootLayout)\n        }\n    }\n\n    interface PerformListener {\n        /**\n         * Called when the overlay is not visible and onDoubleTapProgressUp event occurred.\n         * Visibility of the overlay should be set to VISIBLE within this interface method.\n         */\n        fun onAnimationStart()\n\n        /**\n         * Called when the circle animation is finished.\n         * Visibility of the overlay should be set to GONE within this interface method.\n         */\n        fun onAnimationEnd()\n\n        /**\n         * Determines whether the player should forward, rewind or skip this tap by doing\n         * nothing / ignoring. Is called for each tap.\n         *\n         * By overriding this method you can check for self-defined conditions whether showing the\n         * overlay and rewinding/forwarding (e.g. if the media source valid) or skip it.\n         *\n         * In the following you see the default conditions for each action (if there is no media\n         * to play ([PlaybackState.STATE_NONE]), an error occurred ([PlaybackState.STATE_ERROR])\n         * or the media is stopped ([PlaybackState.STATE_STOPPED]) the tap will be ignored in any\n         * case):\n         *\n         *\n         *      | Action  | Current position          | Screen width portion |\n         *      |---------|---------------------------|----------------------|\n         *      | rewind  | greater than 500 ms       | 0% to 35%            |\n         *      | forward | less than total duration  | 65% to 100%          |\n         *      | ignore  |       ------------        | between 35% and 65%  |\n         *\n         * @param player Current [Player]\n         * @param playerView [PlayerView] which accepts the taps\n         * @param posX Position of the tap on the x-axis\n         *\n         * @return `true` to forward, `false` to rewind or `null` to ignore.\n         */\n        fun shouldForward(player: Player, playerView: DoubleTapPlayerView, posX: Float): Boolean? {\n\n            if (player.playbackState == PlaybackState.STATE_ERROR ||\n                player.playbackState == PlaybackState.STATE_NONE ||\n                player.playbackState == PlaybackState.STATE_STOPPED) {\n\n                playerView.cancelInDoubleTapMode()\n                return null\n            }\n\n            if (player.currentPosition > 500 && posX < playerView.playerWidth * 0.35)\n                return false\n\n            if (player.currentPosition < player.duration && posX > playerView.playerWidth * 0.65)\n                return true\n\n            return null\n        }\n    }\n}\n"
  },
  {
    "path": "doubletapplayerview/src/main/java/com/github/vkay94/dtpv/youtube/views/CircleClipTapView.kt",
    "content": "package com.github.vkay94.dtpv.youtube.views\n\nimport android.animation.Animator\nimport android.animation.ValueAnimator\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.Paint\nimport android.graphics.Path\nimport android.util.AttributeSet\nimport android.view.View\nimport androidx.core.content.ContextCompat\nimport com.github.vkay94.dtpv.R\n\n/**\n * View class\n *\n * Draws a arc shape and provides a circle scaling animation.\n * Used by [YouTubeOverlay][com.github.vkay94.dtpv.youtube.YouTubeOverlay].\n */\ninternal class CircleClipTapView(context: Context?, attrs: AttributeSet) :\n    View(context, attrs) {\n\n    private var backgroundPaint = Paint()\n    private var circlePaint = Paint()\n\n    private var widthPx = 0\n    private var heightPx = 0\n\n    // Background\n\n    private var shapePath = Path()\n    private var isLeft = true\n\n    // Circle\n\n    private var cX = 0f\n    private var cY = 0f\n\n    private var currentRadius = 0f\n    private var minRadius: Int = 0\n    private var maxRadius: Int = 0\n\n    // Animation\n\n    private var valueAnimator: ValueAnimator? = null\n    private var forceReset = false\n\n    init {\n        requireNotNull(context) { \"Context is null.\" }\n\n        backgroundPaint.apply {\n            style = Paint.Style.FILL\n            isAntiAlias = true\n            color = ContextCompat.getColor(context, R.color.dtpv_yt_background_circle_color)\n        }\n\n        circlePaint.apply {\n            style = Paint.Style.FILL\n            isAntiAlias = true\n            color = ContextCompat.getColor(context, R.color.dtpv_yt_tap_circle_color)\n        }\n\n        // Pre-configuations depending on device display metrics\n        val dm = context.resources.displayMetrics\n\n        widthPx = dm.widthPixels\n        heightPx = dm.heightPixels\n\n        minRadius = (30f * dm.density).toInt()\n        maxRadius = (400f * dm.density).toInt()\n\n        updatePathShape()\n\n        valueAnimator = getCircleAnimator()\n    }\n\n    var performAtEnd: () -> Unit = { }\n\n    /*\n        Getter and setter\n     */\n\n    var arcSize: Float = 80f\n        set(value) {\n            field = value\n            updatePathShape()\n        }\n\n    var circleBackgroundColor: Int\n        get() = backgroundPaint.color\n        set(value) {\n            backgroundPaint.color = value\n        }\n\n    var circleColor: Int\n        get() = circlePaint.color\n        set(value) {\n            circlePaint.color = value\n        }\n\n    var animationDuration: Long\n        get() = valueAnimator?.duration ?: 650\n        set(value) {\n            getCircleAnimator().duration = value\n        }\n\n    /*\n       Methods\n    */\n\n    /*\n        Circle\n     */\n\n    fun updatePosition(x: Float, y: Float) {\n        cX = x\n        cY = y\n\n        val newIsLeft = x <= resources.displayMetrics.widthPixels / 2\n        if (isLeft != newIsLeft) {\n            isLeft = newIsLeft\n            updatePathShape()\n        }\n    }\n\n    private fun invalidateWithCurrentRadius(factor: Float) {\n        currentRadius = minRadius + ((maxRadius - minRadius) * factor)\n        invalidate()\n    }\n\n    /*\n        Background\n     */\n\n    private fun updatePathShape() {\n        val halfWidth = widthPx * 0.5f\n\n        shapePath.reset()\n//        shapePath.fillType = Path.FillType.WINDING\n\n        val w = if (isLeft) 0f else widthPx.toFloat()\n        val f = if (isLeft) 1 else -1\n\n        shapePath.moveTo(w, 0f)\n        shapePath.lineTo(f * (halfWidth - arcSize) + w, 0f)\n        shapePath.quadTo(\n            f * (halfWidth + arcSize) + w,\n            heightPx.toFloat() / 2,\n            f * (halfWidth - arcSize) + w,\n            heightPx.toFloat()\n        )\n        shapePath.lineTo(w, heightPx.toFloat())\n\n        shapePath.close()\n        invalidate()\n    }\n\n    /*\n        Animation\n     */\n\n    private fun getCircleAnimator(): ValueAnimator {\n        if (valueAnimator == null) {\n            valueAnimator = ValueAnimator.ofFloat(0f, 1f).apply {\n                duration = animationDuration\n//                interpolator = LinearInterpolator()\n                addUpdateListener {\n                    invalidateWithCurrentRadius(it.animatedValue as Float)\n                }\n\n                addListener(object : Animator.AnimatorListener {\n                    override fun onAnimationStart(animation: Animator) {\n                        visibility = VISIBLE\n                    }\n\n                    override fun onAnimationEnd(animation: Animator) {\n                        if (!forceReset) performAtEnd()\n                    }\n\n                    override fun onAnimationRepeat(animation: Animator) {}\n                    override fun onAnimationCancel(animation: Animator) {}\n                })\n            }\n        }\n        return valueAnimator!!\n    }\n\n    fun resetAnimation(body: () -> Unit) {\n        forceReset = true\n        getCircleAnimator().end()\n        body()\n        forceReset = false\n        getCircleAnimator().start()\n    }\n\n    fun endAnimation() {\n        getCircleAnimator().end()\n    }\n\n    /*\n        Others: Drawing and Measurements\n     */\n\n    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {\n        super.onSizeChanged(w, h, oldw, oldh)\n        widthPx = w\n        heightPx = h\n        updatePathShape()\n    }\n\n    override fun onDraw(canvas: Canvas) {\n        super.onDraw(canvas)\n\n        // Background\n        canvas.clipPath(shapePath)\n        canvas.drawPath(shapePath, backgroundPaint)\n\n        // Circle\n        canvas.drawCircle(cX, cY, currentRadius, circlePaint)\n    }\n}"
  },
  {
    "path": "doubletapplayerview/src/main/java/com/github/vkay94/dtpv/youtube/views/YouTubeSecondsView.kt",
    "content": "package com.github.vkay94.dtpv.youtube.views\n\nimport android.animation.ValueAnimator\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.widget.ImageView\nimport android.widget.LinearLayout\nimport android.widget.TextView\nimport androidx.annotation.DrawableRes\nimport androidx.constraintlayout.widget.ConstraintLayout\nimport androidx.core.animation.doOnEnd\nimport androidx.core.animation.doOnStart\nimport com.github.vkay94.dtpv.R\n\n/**\n * Layout group which handles the icon animation while forwarding and rewinding.\n *\n * Since it's based on view's alpha the fading effect is more fluid (more YouTube-like) than\n * using static drawables, especially when [cycleDuration] is low.\n *\n * Used by [YouTubeOverlay][com.github.vkay94.dtpv.youtube.YouTubeOverlay].\n */\nclass SecondsView(context: Context, attrs: AttributeSet?) :\n    ConstraintLayout(context, attrs) {\n\n    private var trianglesContainer: LinearLayout\n    private var secondsTextView: TextView\n    private var icon1: ImageView\n    private var icon2: ImageView\n    private var icon3: ImageView\n\n    init {\n        LayoutInflater.from(context).inflate(R.layout.yt_seconds_view, this, true)\n\n        trianglesContainer = findViewById(R.id.triangle_container)\n        secondsTextView = findViewById(R.id.tv_seconds)\n        icon1 = findViewById(R.id.icon_1)\n        icon2 = findViewById(R.id.icon_2)\n        icon3 = findViewById(R.id.icon_3)\n    }\n\n    /**\n     * Defines the duration for a full cycle of the triangle animation.\n     * Each animation step takes 20% of it.\n     */\n    var cycleDuration: Long = 750L\n        set(value) {\n            firstAnimator.duration = value / 5\n            secondAnimator.duration = value / 5\n            thirdAnimator.duration = value / 5\n            fourthAnimator.duration = value / 5\n            fifthAnimator.duration = value / 5\n            field = value\n        }\n\n    /**\n     * Sets the `TextView`'s seconds text according to the device`s language.\n     */\n    var seconds: Int = 0\n        set(value) {\n            secondsTextView.text = context.resources.getQuantityString(\n                R.plurals.quick_seek_x_second, value, value\n            )\n            field = value\n        }\n\n    /**\n     * Mirrors the triangles depending on what kind of type should be used (forward/rewind).\n     */\n    var isForward: Boolean = true\n        set(value) {\n            trianglesContainer.rotation = if (value) 0f else 180f\n            field = value\n        }\n\n    val textView: TextView\n        get() = secondsTextView\n\n    @DrawableRes\n    var icon: Int = R.drawable.ic_play_triangle\n        set(value) {\n            if (value > 0) {\n                icon1.setImageResource(value)\n                icon2.setImageResource(value)\n                icon3.setImageResource(value)\n            }\n            field = value\n        }\n\n    /**\n     * Starts the triangle animation\n     */\n    fun start() {\n        stop()\n        firstAnimator.start()\n    }\n\n    /**\n     * Stops the triangle animation\n     */\n    fun stop() {\n        firstAnimator.cancel()\n        secondAnimator.cancel()\n        thirdAnimator.cancel()\n        fourthAnimator.cancel()\n        fifthAnimator.cancel()\n        reset()\n    }\n\n    private fun reset() {\n        icon1.alpha = 0f\n        icon2.alpha = 0f\n        icon3.alpha = 0f\n    }\n\n    private val firstAnimator: ValueAnimator by lazy {\n        ValueAnimator.ofFloat(0f, 1f).setDuration(cycleDuration / 5).apply {\n            doOnStart {\n                icon1.alpha = 0f\n                icon2.alpha = 0f\n                icon3.alpha = 0f\n            }\n            addUpdateListener {\n                icon1.alpha = (it.animatedValue as Float)\n            }\n\n            doOnEnd {\n                secondAnimator.start()\n            }\n        }\n    }\n\n    private val secondAnimator: ValueAnimator by lazy {\n        ValueAnimator.ofFloat(0f, 1f).setDuration(cycleDuration / 5).apply {\n            doOnStart {\n                icon1.alpha = 1f\n                icon2.alpha = 0f\n                icon3.alpha = 0f\n            }\n            addUpdateListener {\n                icon2.alpha = (it.animatedValue as Float)\n            }\n            doOnEnd {\n                thirdAnimator.start()\n            }\n        }\n    }\n\n    private val thirdAnimator: ValueAnimator by lazy {\n        ValueAnimator.ofFloat(0f, 1f).setDuration(cycleDuration / 5).apply {\n            doOnStart {\n                icon1.alpha = 1f\n                icon2.alpha = 1f\n                icon3.alpha = 0f\n            }\n            addUpdateListener {\n                icon1.alpha =\n                    1f - icon3.alpha // or 1f - it (t3.alpha => all three stay a little longer together)\n                icon3.alpha = (it.animatedValue as Float)\n            }\n            doOnEnd {\n                fourthAnimator.start()\n            }\n        }\n\n    }\n\n    private val fourthAnimator: ValueAnimator by lazy {\n        ValueAnimator.ofFloat(0f, 1f).setDuration(cycleDuration / 5).apply {\n            doOnStart {\n                icon1.alpha = 0f\n                icon2.alpha = 1f\n                icon3.alpha = 1f\n            }\n            addUpdateListener {\n                icon2.alpha = 1f - (it.animatedValue as Float)\n            }\n            doOnEnd {\n                fifthAnimator.start()\n            }\n\n        }\n    }\n\n    private val fifthAnimator: ValueAnimator by lazy {\n        ValueAnimator.ofFloat(0f, 1f).setDuration(cycleDuration / 5).apply {\n            doOnStart {\n                icon1.alpha = 0f\n                icon2.alpha = 0f\n                icon3.alpha = 1f\n            }\n            addUpdateListener {\n                icon3.alpha = 1f - (it.animatedValue as Float)\n            }\n            doOnEnd {\n                firstAnimator.start()\n            }\n        }\n    }\n}"
  },
  {
    "path": "doubletapplayerview/src/main/res/drawable/ic_play_triangle.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"16dp\"\n        android:height=\"20dp\"\n        android:viewportWidth=\"24\"\n        android:viewportHeight=\"24\">\n\n    <path\n            android:fillColor=\"#FFFFFF\"\n            android:pathData=\"M3,2 L22,12 L3,22 Z\" />\n\n</vector>\n"
  },
  {
    "path": "doubletapplayerview/src/main/res/layout/yt_overlay.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:id=\"@+id/root_constraint_layout\"\n    android:layout_height=\"match_parent\">\n\n    <com.github.vkay94.dtpv.youtube.views.CircleClipTapView\n        android:id=\"@+id/circle_clip_tap_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clickable=\"false\"\n        android:focusable=\"false\" />\n\n    <com.github.vkay94.dtpv.youtube.views.SecondsView\n        android:id=\"@+id/seconds_view\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:clickable=\"false\"\n        android:focusable=\"false\"\n        android:visibility=\"invisible\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintWidth_default=\"percent\"\n        app:layout_constraintWidth_percent=\"0.5\"\n        tools:visibility=\"visible\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "doubletapplayerview/src/main/res/layout/yt_seconds_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\"\n    tools:ignore=\"ContentDescription\">\n\n    <LinearLayout\n        android:id=\"@+id/triangle_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center_horizontal\"\n        android:orientation=\"horizontal\">\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/icon_1\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/ic_play_triangle\"\n            tools:alpha=\"0.18\" />\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/icon_2\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/ic_play_triangle\"\n            tools:alpha=\"0.5\" />\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/icon_3\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/ic_play_triangle\"\n            tools:alpha=\"1\" />\n\n    </LinearLayout>\n\n    <androidx.appcompat.widget.AppCompatTextView\n        android:id=\"@+id/tv_seconds\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center\"\n        android:padding=\"4dp\"\n        tools:text=\"20 Sekunden\" />\n\n</LinearLayout>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values/dtpv.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <declare-styleable name=\"DoubleTapPlayerView\">\n        <attr name=\"dtpv_controller\" format=\"reference\" />\n    </declare-styleable>\n\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d seconds</item>\n        <item quantity=\"one\">%d second</item>\n        <item quantity=\"many\">%d seconds</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values/public.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <public />\n\n    <public name=\"YouTubeOverlay_yt_playerView\" type=\"attr\" />\n    <public name=\"YouTubeOverlay_yt_seekSeconds\" type=\"attr\" />\n    <public name=\"YouTubeOverlay_yt_animationDuration\" type=\"attr\" />\n    <public name=\"YouTubeOverlay_yt_arcSize\" type=\"attr\" />\n    <public name=\"YouTubeOverlay_yt_tapCircleColor\" type=\"attr\" />\n    <public name=\"YouTubeOverlay_yt_backgroundCircleColor\" type=\"attr\" />\n    <public name=\"YouTubeOverlay_yt_textAppearance\" type=\"attr\" />\n    <public name=\"YouTubeOverlay_yt_iconAnimationDuration\" type=\"attr\" />\n    <public name=\"YouTubeOverlay_yt_icon\" type=\"attr\" />\n\n    <public name=\"DoubleTapPlayerView_dtpv_controller\" type=\"attr\" />\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values/yt_overlay.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <declare-styleable name=\"YouTubeOverlay\">\n        <attr name=\"yt_playerView\" format=\"reference\" />\n        <attr name=\"yt_seekSeconds\" format=\"integer\" />\n        <attr name=\"yt_animationDuration\" format=\"integer\" />\n        <attr name=\"yt_iconAnimationDuration\" format=\"integer\" />\n        <attr name=\"yt_arcSize\" format=\"dimension\" />\n        <attr name=\"yt_tapCircleColor\" format=\"color\" />\n        <attr name=\"yt_backgroundCircleColor\" format=\"color\" />\n\n        <attr name=\"yt_textAppearance\" format=\"reference\" />\n        <attr name=\"yt_icon\" format=\"reference\" />\n    </declare-styleable>\n\n    <color name=\"dtpv_yt_tap_circle_color\">#18FFFFFF</color>\n    <color name=\"dtpv_yt_background_circle_color\">#20EEEEEE</color>\n    <dimen name=\"dtpv_yt_arc_size\">40dp</dimen>\n\n    <style name=\"YTOSecondsTextAppearance\" parent=\"TextAppearance.AppCompat\">\n        <item name=\"android:textSize\">12sp</item>\n        <item name=\"android:textColor\">@android:color/white</item>\n    </style>\n\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-af/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekondes</item>\n        <item quantity=\"one\">%d sekonde</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-am/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d ሰከንዶች</item>\n        <item quantity=\"one\">%d ሰከንድ</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-ar/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d ثانية</item>\n        <item quantity=\"zero\">%d ثانية</item>\n        <item quantity=\"one\">ثانية واحدة (%d)</item>\n        <item quantity=\"two\">ثانيتان (%d)</item>\n        <item quantity=\"few\">%d ثوانٍ</item>\n        <item quantity=\"many\">%d ثانية</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-az/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d saniyə</item>\n        <item quantity=\"one\">%d saniyə</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-b+sr+Latn/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekundi</item>\n        <item quantity=\"one\">%d sekunda</item>\n        <item quantity=\"few\">%d sekunde</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-be/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d секунды</item>\n        <item quantity=\"one\">%d секунда</item>\n        <item quantity=\"few\">%d секунды</item>\n        <item quantity=\"many\">%d секунд</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-bg/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d секунди</item>\n        <item quantity=\"one\">%d секунда</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-bn/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d সেকেন্ড</item>\n        <item quantity=\"one\">%d সেকেন্ড</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-bs/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d s</item>\n        <item quantity=\"one\">%d s</item>\n        <item quantity=\"few\">%d s</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-ca/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d segons</item>\n        <item quantity=\"one\">%d segon</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-cs/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekund</item>\n        <item quantity=\"one\">%d sekunda</item>\n        <item quantity=\"few\">%d sekundy</item>\n        <item quantity=\"many\">%d sekundy</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-da/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekunder</item>\n        <item quantity=\"one\">%d sekund</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-de/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d Sekunden</item>\n        <item quantity=\"one\">%d Sekunde</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-el/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d δευτερόλεπτα</item>\n        <item quantity=\"one\">%d δευτερόλεπτο</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-en-rGB/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d seconds</item>\n        <item quantity=\"one\">%d second</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-en-rIN/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d seconds</item>\n        <item quantity=\"one\">%d second</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-es/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d segundos</item>\n        <item quantity=\"one\">%d segundo</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-es-rUS/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d segundos</item>\n        <item quantity=\"one\">%d segundo</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-et/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekundit</item>\n        <item quantity=\"one\">%d sekund</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-eu/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d segundo</item>\n        <item quantity=\"one\">%d segundo</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-fa/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d ثانیه</item>\n        <item quantity=\"one\">%d ثانیه</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-fi/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekuntia</item>\n        <item quantity=\"one\">%d sekunti</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-fr/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d secondes</item>\n        <item quantity=\"one\">%d seconde</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-fr-rCA/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d secondes</item>\n        <item quantity=\"one\">%d seconde</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-gl/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d segundos</item>\n        <item quantity=\"one\">%d segundo</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-gu/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d સેકન્ડ</item>\n        <item quantity=\"one\">%d સેકન્ડ</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-hi/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d सेकंड</item>\n        <item quantity=\"one\">%d सेकंड</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-hr/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekundi</item>\n        <item quantity=\"one\">%d sekunda</item>\n        <item quantity=\"few\">%d sekunde</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-hu/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d másodperc</item>\n        <item quantity=\"one\">%d másodperc</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-hy/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d վայրկյան</item>\n        <item quantity=\"one\">%d վայրկյան</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-in/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d detik</item>\n        <item quantity=\"one\">%d detik</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-is/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekúndur</item>\n        <item quantity=\"one\">%d sekúnda</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-it/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d secondi</item>\n        <item quantity=\"one\">%d secondo</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-iw/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d שניות</item>\n        <item quantity=\"one\">שנייה אחת (%d)</item>\n        <item quantity=\"two\">%d שניות</item>\n        <item quantity=\"many\">%d שניות</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-ja/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d秒</item>\n        <item quantity=\"one\">%d秒</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-ka/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d წამი</item>\n        <item quantity=\"one\">%d წამი</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-kk/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d секунд</item>\n        <item quantity=\"one\">%d секунд</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-km/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d វិនាទី</item>\n        <item quantity=\"one\">%d វិនាទី</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-kn/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d ಸೆಕೆಂಡ್‌ಗಳು</item>\n        <item quantity=\"one\">%d ಸೆಕೆಂಡ್‌ಗಳು</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-ko/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d초</item>\n        <item quantity=\"one\">%d초</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-ky/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d секунд</item>\n        <item quantity=\"one\">%d секунд</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-lo/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d ວິນາທີ</item>\n        <item quantity=\"one\">%d ວິນາທີ</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-lt/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekundžių</item>\n        <item quantity=\"one\">%d sekundė</item>\n        <item quantity=\"few\">%d sekundės</item>\n        <item quantity=\"many\">%d sekundės</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-lv/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekundes</item>\n        <item quantity=\"zero\">%d sekundes</item>\n        <item quantity=\"one\">%d sekunde</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-mk/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d секунди</item>\n        <item quantity=\"one\">%d секунда</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-ml/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d സെക്കൻഡ്</item>\n        <item quantity=\"one\">%d സെക്കൻഡ്</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-mn/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d секунд</item>\n        <item quantity=\"one\">%d секунд</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-mr/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d सेकंद</item>\n        <item quantity=\"one\">%d सेकंद</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-ms/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d saat</item>\n        <item quantity=\"one\">%d saat</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-my/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d စက္ကန့်</item>\n        <item quantity=\"one\">%d စက္ကန့်</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-nb/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekunder</item>\n        <item quantity=\"one\">%d sekund</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-ne/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d सेकेन्ड</item>\n        <item quantity=\"one\">%d सेकेन्ड</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-nl/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d seconden</item>\n        <item quantity=\"one\">%d seconde</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-pa/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d ਸਕਿੰਟ</item>\n        <item quantity=\"one\">%d ਸਕਿੰਟ</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-pl/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekundy</item>\n        <item quantity=\"one\">%d sekunda</item>\n        <item quantity=\"few\">%d sekundy</item>\n        <item quantity=\"many\">%d sekund</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-pt-rBR/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d segundos</item>\n        <item quantity=\"one\">%d segundo</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-pt-rPT/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d segundos</item>\n        <item quantity=\"one\">%d segundo</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-ro/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d de secunde</item>\n        <item quantity=\"one\">%d secundă</item>\n        <item quantity=\"few\">%d secunde</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-ru/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d секунды</item>\n        <item quantity=\"one\">%d секунда</item>\n        <item quantity=\"few\">%d секунды</item>\n        <item quantity=\"many\">%d секунд</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-si/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">තත්පර %dක්</item>\n        <item quantity=\"one\">තත්පර %dක්</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-sk/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekúnd</item>\n        <item quantity=\"one\">%d sekunda</item>\n        <item quantity=\"few\">%d sekundy</item>\n        <item quantity=\"many\">%d sekundy</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-sl/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekund</item>\n        <item quantity=\"one\">%d sekunda</item>\n        <item quantity=\"two\">%d sekundi</item>\n        <item quantity=\"few\">%d sekunde</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-sq/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekonda</item>\n        <item quantity=\"one\">%d sekondë</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-sr/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d секунди</item>\n        <item quantity=\"one\">%d секунда</item>\n        <item quantity=\"few\">%d секунде</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-sv/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d sekunder</item>\n        <item quantity=\"one\">%d sekund</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-sw/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">Sekunde %d</item>\n        <item quantity=\"one\">Sekunde %d</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-ta/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d விநாடிகள்</item>\n        <item quantity=\"one\">%d விநாடி</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-te/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d సెకన్లు</item>\n        <item quantity=\"one\">%d సెకను</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-th/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d วินาที</item>\n        <item quantity=\"one\">%d วินาที</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-tl/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d na segundo</item>\n        <item quantity=\"one\">%d segundo</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-tr/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d saniye</item>\n        <item quantity=\"one\">%d saniye</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-uk/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d секунди</item>\n        <item quantity=\"one\">%d секунда</item>\n        <item quantity=\"few\">%d секунди</item>\n        <item quantity=\"many\">%d секунд</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-ur/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d سیکنڈز</item>\n        <item quantity=\"one\">%d سیکنڈ</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-uz/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d soniya</item>\n        <item quantity=\"one\">%d soniya</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-vi/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d giây</item>\n        <item quantity=\"one\">%d giây</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-zh-rCN/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d 秒</item>\n        <item quantity=\"one\">%d 秒</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-zh-rHK/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d 秒</item>\n        <item quantity=\"one\">%d 秒</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-zh-rTW/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d 秒</item>\n        <item quantity=\"one\">%d 秒</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "doubletapplayerview/src/main/res/values-zu/plurals.xml",
    "content": "<resources>\n    <plurals name=\"quick_seek_x_second\">\n        <item quantity=\"other\">%d amasekhondi</item>\n        <item quantity=\"one\">%d amasekhondi</item>\n    </plurals>\n</resources>"
  },
  {
    "path": "exoplayer-amzn-2.10.6/.gitignore",
    "content": "# Android generated\nbin\ngen\nlibs\nobj\nlint.xml\n\n# IntelliJ IDEA\n.idea\n*.iml\n*.ipr\n*.iws\nclasses\ngen-external-apklibs\n\n# Eclipse\n.project\n.classpath\n.settings\n.checkstyle\n.cproject\n\n# Gradle\n.gradle\nbuild\nbuildout\nout\n\n# Maven\ntarget\nrelease.properties\npom.xml.*\n\n# Ant\nant.properties\nlocal.properties\nproguard.cfg\nproguard-project.txt\n\n# Bazel\nbazel-bin\nbazel-genfiles\nbazel-out\nbazel-testlogs\n\n# Other\n.DS_Store\ncmake-build-debug\ndist\ntmp\n\n# External native builds\n.externalNativeBuild\n\n# VP9 extension\nextensions/vp9/src/main/jni/libvpx\nextensions/vp9/src/main/jni/libvpx_android_configs\nextensions/vp9/src/main/jni/libyuv\n\n# Opus extension\nextensions/opus/src/main/jni/libopus\n\n# FLAC extension\nextensions/flac/src/main/jni/flac\n\n# FFmpeg extension\nextensions/ffmpeg/src/main/jni/ffmpeg\n\n# Cronet extension\nextensions/cronet/jniLibs/*\n!extensions/cronet/jniLibs/README.md\nextensions/cronet/libs/*\n!extensions/cronet/libs/README.md\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/.hgignore",
    "content": "# Mercurial's .hgignore files can only be used in the root directory.\n# You can still apply these rules by adding\n# include:path/to/this/directory/.hgignore to the top-level .hgignore file.\n\n# Ensure same syntax as in .gitignore can be used\nsyntax:glob\n\n# Android generated\nbin\ngen\nlibs\nobj\nlint.xml\n\n# IntelliJ IDEA & Android Studio\n.idea\n*.iml\n*.ipr\n*.iws\nclasses\ngen-external-apklibs\n*.li\n\n# Eclipse\n.project\n.classpath\n.settings\n.checkstyle\n.cproject\n\n# Gradle\n.gradle\nbuild\nbuildout\nout\n\n# Maven\ntarget\nrelease.properties\npom.xml.*\n\n# Ant\nant.properties\nlocal.properties\nproguard.cfg\nproguard-project.txt\n\n# Bazel\nbazel-bin\nbazel-genfiles\nbazel-out\nbazel-testlogs\n\n# Other\n.DS_Store\ncmake-build-debug\ndist\ntmp\n\n# VP9 extension\nextensions/vp9/src/main/jni/libvpx\nextensions/vp9/src/main/jni/libvpx_android_configs\nextensions/vp9/src/main/jni/libyuv\n\n# Opus extension\nextensions/opus/src/main/jni/libopus\n\n# FLAC extension\nextensions/flac/src/main/jni/flac\n\n# FFmpeg extension\nextensions/ffmpeg/src/main/jni/ffmpeg\n\n# Cronet extension\nextensions/cronet/jniLibs/*\n!extensions/cronet/jniLibs/README.md\nextensions/cronet/libs/*\n!extensions/cronet/libs/README.md\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/CONTRIBUTING.md",
    "content": "# How to contribute #\n\n## Reporting issues ##\n\nWe use the [GitHub issue tracker](https://github.com/google/ExoPlayer/issues)\nto track bugs, feature requests and questions.\n\nBefore filing a new issue, please search the tracker to check if it's already\ncovered by an existing report. Avoiding duplicates helps us maximize the time we\ncan spend fixing bugs and adding new features.\n\nWhen filing an issue, be sure to provide enough information for us to\nefficiently diagnose and reproduce the problem. In particular, please include\nall of the information requested in the issue template.\n\n## Pull requests ##\n\nWe will also consider high quality pull requests. These should normally merge\ninto the `dev-v2` branch. Before a pull request can be accepted you must submit\na Contributor License Agreement, as described below.\n\n[dev]: https://github.com/google/ExoPlayer/tree/dev\n\n## Contributor license agreement ##\n\nContributions to any Google project must be accompanied by a Contributor\nLicense Agreement. This is not a copyright **assignment**, it simply gives\nGoogle permission to use and redistribute your contributions as part of the\nproject.\n\n  * If you are an individual writing original source code and you're sure you\n    own the intellectual property, then you'll need to sign an [individual\n    CLA][].\n\n  * If you work for a company that wants to allow you to contribute your work,\n    then you'll need to sign a [corporate CLA][].\n\nYou generally only need to submit a CLA once, so if you've already submitted\none (even if it was for a different project), you probably don't need to do it\nagain.\n\n[individual CLA]: https://developers.google.com/open-source/cla/individual\n[corporate CLA]: https://developers.google.com/open-source/cla/corporate\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/README-ORIGINAL.md",
    "content": "# ExoPlayer #\n\nExoPlayer is an application level media player for Android. It provides an\nalternative to Android’s MediaPlayer API for playing audio and video both\nlocally and over the Internet. ExoPlayer supports features not currently\nsupported by Android’s MediaPlayer API, including DASH and SmoothStreaming\nadaptive playbacks. Unlike the MediaPlayer API, ExoPlayer is easy to customize\nand extend, and can be updated through Play Store application updates.\n\n## Documentation ##\n\n* The [developer guide][] provides a wealth of information.\n* The [class reference][] documents ExoPlayer classes.\n* The [release notes][] document the major changes in each release.\n* Follow our [developer blog][] to keep up to date with the latest ExoPlayer\n  developments!\n\n[developer guide]: https://exoplayer.dev/guide.html\n[class reference]: https://exoplayer.dev/doc/reference\n[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md\n[developer blog]: https://medium.com/google-exoplayer\n\n## Using ExoPlayer ##\n\nExoPlayer modules can be obtained from JCenter. It's also possible to clone the\nrepository and depend on the modules locally.\n\n### From JCenter ###\n\n#### 1. Add repositories ####\n\nThe easiest way to get started using ExoPlayer is to add it as a gradle\ndependency. You need to make sure you have the Google and JCenter repositories\nincluded in the `build.gradle` file in the root of your project:\n\n```gradle\nrepositories {\n    google()\n    jcenter()\n}\n```\n\n#### 2. Add ExoPlayer module dependencies ####\n\nNext add a dependency in the `build.gradle` file of your app module. The\nfollowing will add a dependency to the full library:\n\n```gradle\nimplementation 'com.google.android.exoplayer:exoplayer:2.X.X'\n```\n\nwhere `2.X.X` is your preferred version.\n\nAs an alternative to the full library, you can depend on only the library\nmodules that you actually need. For example the following will add dependencies\non the Core, DASH and UI library modules, as might be required for an app that\nplays DASH content:\n\n```gradle\nimplementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'\nimplementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X'\nimplementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'\n```\n\nThe available library modules are listed below. Adding a dependency to the full\nlibrary is equivalent to adding dependencies on all of the library modules\nindividually.\n\n* `exoplayer-core`: Core functionality (required).\n* `exoplayer-dash`: Support for DASH content.\n* `exoplayer-hls`: Support for HLS content.\n* `exoplayer-smoothstreaming`: Support for SmoothStreaming content.\n* `exoplayer-ui`: UI components and resources for use with ExoPlayer.\n\nIn addition to library modules, ExoPlayer has multiple extension modules that\ndepend on external libraries to provide additional functionality. Some\nextensions are available from JCenter, whereas others must be built manually.\nBrowse the [extensions directory][] and their individual READMEs for details.\n\nMore information on the library and extension modules that are available from\nJCenter can be found on [Bintray][].\n\n[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/\n[Bintray]: https://bintray.com/google/exoplayer\n\n#### 3. Turn on Java 8 support ####\n\nIf not enabled already, you also need to turn on Java 8 support in all\n`build.gradle` files depending on ExoPlayer, by adding the following to the\n`android` section:\n\n```gradle\ncompileOptions {\n  targetCompatibility JavaVersion.VERSION_1_8\n}\n```\n\n### Locally ###\n\nCloning the repository and depending on the modules locally is required when\nusing some ExoPlayer extension modules. It's also a suitable approach if you\nwant to make local changes to ExoPlayer, or if you want to use a development\nbranch.\n\nFirst, clone the repository into a local directory and checkout the desired\nbranch:\n\n```sh\ngit clone https://github.com/google/ExoPlayer.git\ngit checkout release-v2\n```\n\nNext, add the following to your project's `settings.gradle` file, replacing\n`path/to/exoplayer` with the path to your local copy:\n\n```gradle\ngradle.ext.exoplayerRoot = 'path/to/exoplayer'\ngradle.ext.exoplayerModulePrefix = 'exoplayer-'\napply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle')\n```\n\nYou should now see the ExoPlayer modules appear as part of your project. You can\ndepend on them as you would on any other local module, for example:\n\n```gradle\nimplementation project(':exoplayer-library-core')\nimplementation project(':exoplayer-library-dash')\nimplementation project(':exoplayer-library-ui')\n```\n\n## Developing ExoPlayer ##\n\n#### Project branches ####\n\n* Development work happens on the `dev-v2` branch. Pull requests should\n  normally be made to this branch.\n* The `release-v2` branch holds the most recent release.\n\n#### Using Android Studio ####\n\nTo develop ExoPlayer using Android Studio, simply open the ExoPlayer project in\nthe root directory of the repository.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/README.md",
    "content": "Amazon ExoPlayer Port - https://github.com/amzn/exoplayer-amazon-port\nThis repository is a port of the ExoPlayer project for Amazon devices.\nSee https://github.com/google/ExoPlayer for the original project.\nSee \"README-ORIGINAL.md\" for the original ExoPlayer README.\n\nAlso see https://developer.amazon.com/docs/fire-tv/media-players.html#exoplayer\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/RELEASENOTES.md",
    "content": "# Release notes #\n\n### 2.10.6 (2019-10-18) ###\n\n* Downloads: Merge downloads in `SegmentDownloader` to improve overall download\n  speed ([#5978](https://github.com/google/ExoPlayer/issues/5978))\n* Add `Player.onPlaybackSuppressionReasonChanged` to allow listeners to\n  detect playbacks suppressions (e.g. transient audio focus loss) directly\n  ([#6203](https://github.com/google/ExoPlayer/issues/6203)).\n* DASH:\n  * Support `Label` elements\n    ([#6297](https://github.com/google/ExoPlayer/issues/6297)).\n  * Support legacy audio channel configuration\n    ([#6523](https://github.com/google/ExoPlayer/issues/6523)).\n* HLS: Add support for ID3 in EMSG when using FMP4 streams\n  ([spec](https://aomediacodec.github.io/av1-id3/)).\n* MP3: Add workaround to avoid prematurely ending playback of some SHOUTcast\n  live streams ([#6537](https://github.com/google/ExoPlayer/issues/6537),\n  [#6315](https://github.com/google/ExoPlayer/issues/6315) and\n  [#5658](https://github.com/google/ExoPlayer/issues/5658)).\n* Metadata: Expose the raw ICY metadata through `IcyInfo`\n  ([#6476](https://github.com/google/ExoPlayer/issues/6476)).\n* UI:\n  * Setting `app:played_color` on `PlayerView` and `PlayerControlView` no longer\n    adjusts the colors of the scrubber handle , buffered and unplayed parts of\n    the time bar. These can be set separately using `app:scrubber_color`,\n    `app:buffered_color` and `app_unplayed_color` respectively.\n  * Setting `app:ad_marker_color` on `PlayerView` and `PlayerControlView` no\n    longer adjusts the color of played ad markers. The color of played ad\n    markers can be set separately using `app:played_ad_marker_color`.\n\n### 2.10.5 (2019-09-20) ###\n\n* Add `Player.isPlaying` and `EventListener.onIsPlayingChanged` to check whether\n  the playback position is advancing. This helps to determine if playback is\n  suppressed due to audio focus loss. Also add\n  `Player.getPlaybackSuppressedReason` to determine the reason of the\n  suppression ([#6203](https://github.com/google/ExoPlayer/issues/6203)).\n* Track selection\n  * Add `allowAudioMixedChannelCountAdaptiveness` parameter to\n    `DefaultTrackSelector` to allow adaptive selections of audio tracks with\n    different channel counts.\n  * Improve text selection logic to always prefer the better language matches\n    over other selection parameters.\n  * Fix audio selection issue where languages are compared by bitrate\n    ([#6335](https://github.com/google/ExoPlayer/issues/6335)).\n* Performance\n  * Increase maximum video buffer size from 13MB to 32MB. The previous default\n    was too small for high quality streams.\n  * Reset `DefaultBandwidthMeter` to initial values on network change.\n  * Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor is\n    provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)).\n* Metadata\n  * Support EMSG V1 boxes in FMP4.\n  * Support unwrapping of nested metadata (e.g. ID3 and SCTE-35 in EMSG).\n* Add `HttpDataSource.getResponseCode` to provide the status code associated\n  with the most recent HTTP response.\n* Fix transitions between packed audio and non-packed audio segments in HLS\n  ([#6444](https://github.com/google/ExoPlayer/issues/6444)).\n* Fix issue where a request would be retried after encountering an error, even\n  though the `LoadErrorHandlingPolicy` classified the error as fatal.\n* Fix initialization data handling for FLAC in MP4\n  ([#6396](https://github.com/google/ExoPlayer/issues/6396),\n  [#6397](https://github.com/google/ExoPlayer/issues/6397)).\n* Fix decoder selection for E-AC3 JOC streams\n  ([#6398](https://github.com/google/ExoPlayer/issues/6398)).\n* Fix `PlayerNotificationManager` to show play icon rather than pause icon when\n  playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)).\n* RTMP extension: Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues\n  ([#4200](https://github.com/google/ExoPlayer/issues/4200),\n  [#4249](https://github.com/google/ExoPlayer/issues/4249),\n  [#4319](https://github.com/google/ExoPlayer/issues/4319),\n  [#4337](https://github.com/google/ExoPlayer/issues/4337)).\n* IMA extension: Fix crash in `ImaAdsLoader.onTimelineChanged`\n  ([#5831](https://github.com/google/ExoPlayer/issues/5831)).\n\n### 2.10.4 (2019-07-26) ###\n\n* Offline: Add `Scheduler` implementation that uses `WorkManager`.\n* Add ability to specify a description when creating notification channels via\n  ExoPlayer library classes.\n* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language\n  tags instead of 3-letter ISO 639-2 language tags.\n* Ensure the `SilenceMediaSource` position is in range\n  ([#6229](https://github.com/google/ExoPlayer/issues/6229)).\n* WAV: Calculate correct duration for clipped streams\n  ([#6241](https://github.com/google/ExoPlayer/issues/6241)).\n* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change\n  from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)).\n* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata\n  ([#5527](https://github.com/google/ExoPlayer/issues/5527)).\n* Fix issue where initial seek positions get ignored when playing a preroll ad\n  ([#6201](https://github.com/google/ExoPlayer/issues/6201)).\n* Fix issue where invalid language tags were normalized to \"und\" instead of\n  keeping the original\n  ([#6153](https://github.com/google/ExoPlayer/issues/6153)).\n* Fix `DataSchemeDataSource` re-opening and range requests\n  ([#6192](https://github.com/google/ExoPlayer/issues/6192)).\n* Fix Flac and ALAC playback on some LG devices\n  ([#5938](https://github.com/google/ExoPlayer/issues/5938)).\n* Fix issue when calling `performClick` on `PlayerView` without\n  `PlayerControlView`\n  ([#6260](https://github.com/google/ExoPlayer/issues/6260)).\n* Fix issue where playback speeds are not used in adaptive track selections\n  after manual selection changes for other renderers\n  ([#6256](https://github.com/google/ExoPlayer/issues/6256)).\n\n### 2.10.3 (2019-07-09) ###\n\n* Display last frame when seeking to end of stream\n  ([#2568](https://github.com/google/ExoPlayer/issues/2568)).\n* Audio:\n  * Fix an issue where not all audio was played out when the configuration\n    for the underlying track was changing (e.g., at some period transitions).\n  * Fix an issue where playback speed was applied inaccurately in playlists\n    ([#6117](https://github.com/google/ExoPlayer/issues/6117)).\n* UI: Fix `PlayerView` incorrectly consuming touch events if no controller is\n  attached ([#6109](https://github.com/google/ExoPlayer/issues/6109)).\n* CEA608: Fix repetition of special North American characters\n  ([#6133](https://github.com/google/ExoPlayer/issues/6133)).\n* FLV: Fix bug that caused playback of some live streams to not start\n  ([#6111](https://github.com/google/ExoPlayer/issues/6111)).\n* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`.\n* MediaSession extension: Fix `MediaSessionConnector.play()` not resuming\n  playback ([#6093](https://github.com/google/ExoPlayer/issues/6093)).\n\n### 2.10.2 (2019-06-03) ###\n\n* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s\n  ([#5779](https://github.com/google/ExoPlayer/issues/5779)).\n* Add `SilenceMediaSource` that can be used to play silence of a given\n  duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)).\n* Offline:\n  * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after\n    preparation of a `DownloadHelper` fails\n    ([#5915](https://github.com/google/ExoPlayer/issues/5915)).\n  * Fix `CacheUtil.cache()` downloading too much data\n    ([#5927](https://github.com/google/ExoPlayer/issues/5927)).\n  * Fix misreporting cached bytes when caching is paused\n    ([#5573](https://github.com/google/ExoPlayer/issues/5573)).\n* UI:\n  * Allow setting `DefaultTimeBar` attributes on `PlayerView` and\n    `PlayerControlView`.\n  * Change playback controls toggle from touch down to touch up events\n    ([#5784](https://github.com/google/ExoPlayer/issues/5784)).\n  * Fix issue where playback controls were not kept visible on key presses\n    ([#5963](https://github.com/google/ExoPlayer/issues/5963)).\n* Subtitles:\n  * CEA-608: Handle XDS and TEXT modes\n    ([#5807](https://github.com/google/ExoPlayer/pull/5807)).\n  * TTML: Fix bitmap rendering\n    ([#5633](https://github.com/google/ExoPlayer/pull/5633)).\n* IMA: Fix ad pod index offset calculation without preroll\n  ([#5928](https://github.com/google/ExoPlayer/issues/5928)).\n* Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods\n  to indicate whether a controller sent a play or only a prepare command. This\n  allows to take advantage of decoder reuse with the MediaSessionConnector\n  ([#5891](https://github.com/google/ExoPlayer/issues/5891)).\n* Add `ProgressUpdateListener` to `PlayerControlView`\n  ([#5834](https://github.com/google/ExoPlayer/issues/5834)).\n* Add support for auto-detecting UDP streams in `DefaultDataSource`\n  ([#6036](https://github.com/google/ExoPlayer/pull/6036)).\n* Allow enabling decoder fallback with `DefaultRenderersFactory`\n  ([#5942](https://github.com/google/ExoPlayer/issues/5942)).\n* Gracefully handle revoked `ACCESS_NETWORK_STATE` permission\n  ([#6019](https://github.com/google/ExoPlayer/issues/6019)).\n* Fix decoding problems when seeking back after seeking beyond a mid-roll ad\n  ([#6009](https://github.com/google/ExoPlayer/issues/6009)).\n* Fix application of `maxAudioBitrate` for adaptive audio track groups\n  ([#6006](https://github.com/google/ExoPlayer/issues/6006)).\n* Fix bug caused by parallel adaptive track selection using `Format`s without\n  bitrate information\n  ([#5971](https://github.com/google/ExoPlayer/issues/5971)).\n* Fix bug in `CastPlayer.getCurrentWindowIndex()`\n  ([#5955](https://github.com/google/ExoPlayer/issues/5955)).\n\n### 2.10.1 (2019-05-16) ###\n\n* Offline: Add option to remove all downloads.\n* HLS: Fix `NullPointerException` when using HLS chunkless preparation\n  ([#5868](https://github.com/google/ExoPlayer/issues/5868)).\n* Fix handling of empty values and line terminators in SHOUTcast ICY metadata\n  ([#5876](https://github.com/google/ExoPlayer/issues/5876)).\n* Fix DVB subtitles for SDK 28\n  ([#5862](https://github.com/google/ExoPlayer/issues/5862)).\n* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing\n  48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)).\n\n### 2.10.0 (2019-04-15) ###\n\n* Core library:\n  * Improve decoder re-use between playbacks\n    ([#2826](https://github.com/google/ExoPlayer/issues/2826)). Read\n    [this blog post](https://medium.com/google-exoplayer/improved-decoder-reuse-in-exoplayer-ef4c6d99591d)\n    for more details.\n  * Rename `ExtractorMediaSource` to `ProgressiveMediaSource`.\n  * Fix issue where using `ProgressiveMediaSource.Factory` would mean that\n    `DefaultExtractorsFactory` would be kept by proguard. Custom\n    `ExtractorsFactory` instances must now be passed via the\n    `ProgressiveMediaSource.Factory` constructor, and `setExtractorsFactory` is\n    deprecated.\n  * Make the default minimum buffer size equal the maximum buffer size for video\n    playbacks ([#2083](https://github.com/google/ExoPlayer/issues/2083)).\n  * Move `PriorityTaskManager` from `DefaultLoadControl` to `SimpleExoPlayer`.\n  * Add new `ExoPlaybackException` types for remote exceptions and out-of-memory\n    errors.\n  * Use full BCP 47 language tags in `Format`.\n  * Do not retry failed loads whose error is `FileNotFoundException`.\n  * Fix issue where not resetting the position for a new `MediaSource` in calls\n    to `ExoPlayer.prepare` causes an `IndexOutOfBoundsException`\n    ([#5520](https://github.com/google/ExoPlayer/issues/5520)).\n* Offline:\n  * Improve offline support. `DownloadManager` now tracks all offline content,\n    not just tasks in progress. Read\n    [this page](https://exoplayer.dev/downloading-media.html) for more details.\n* Caching:\n  * Improve performance of `SimpleCache`\n    ([#4253](https://github.com/google/ExoPlayer/issues/4253)).\n  * Cache data with unknown length by default. The previous flag to opt in to\n    this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been\n    replaced with an opt out flag\n    (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`).\n* Extractors:\n  * MP4/FMP4: Add support for Dolby Vision.\n  * MP4: Fix issue handling meta atoms in some streams\n    ([#5698](https://github.com/google/ExoPlayer/issues/5698),\n    [#5694](https://github.com/google/ExoPlayer/issues/5694)).\n  * MP3: Add support for SHOUTcast ICY metadata\n    ([#3735](https://github.com/google/ExoPlayer/issues/3735)).\n  * MP3: Fix ID3 frame unsychronization\n    ([#5673](https://github.com/google/ExoPlayer/issues/5673)).\n  * MP3: Fix playback of badly clipped files\n    ([#5772](https://github.com/google/ExoPlayer/issues/5772)).\n  * MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default\n    (i.e. if the flag is not set), the 0x82 elementary stream type is now\n    treated as an SCTE subtitle track\n    ([#5330](https://github.com/google/ExoPlayer/issues/5330)).\n* Track selection:\n  * Add options for controlling audio track selections to `DefaultTrackSelector`\n    ([#3314](https://github.com/google/ExoPlayer/issues/3314)).\n  * Update `TrackSelection.Factory` interface to support creating all track\n    selections together.\n  * Allow to specify a selection reason for a `SelectionOverride`.\n  * When no text language preference matches, only select forced text tracks\n    whose language matches the selected audio language.\n* UI:\n  * Update `DefaultTimeBar` based on duration of media and add parameter to set\n    the minimum update interval to control the smoothness of the updates\n    ([#5040](https://github.com/google/ExoPlayer/issues/5040)).\n  * Move creation of dialogs for `TrackSelectionView`s to\n    `TrackSelectionDialogBuilder` and add option to select multiple overrides.\n  * Change signature of `PlayerNotificationManager.NotificationListener` to\n    better fit service requirements.\n  * Add option to include navigation actions in the compact mode of\n    notifications created using `PlayerNotificationManager`.\n  * Fix issues with flickering notifications on KitKat when using\n    `PlayerNotificationManager` and `DownloadNotificationUtil`. For the latter,\n    applications should switch to using `DownloadNotificationHelper`.\n  * Fix accuracy of D-pad seeking in `DefaultTimeBar`\n    ([#5767](https://github.com/google/ExoPlayer/issues/5767)).\n* Audio:\n  * Allow `AudioProcessor`s to be drained of pending output after they are\n    reconfigured.\n  * Fix an issue that caused audio to be truncated at the end of a period\n    when switching to a new period where gapless playback information was newly\n    present or newly absent.\n  * Add support for reading AC-4 streams\n    ([#5303](https://github.com/google/ExoPlayer/pull/5303)).\n* Video:\n  * Remove `MediaCodecSelector.DEFAULT_WITH_FALLBACK`. Apps should instead\n    signal that fallback should be used by passing `true` as the\n    `enableDecoderFallback` parameter when instantiating the video renderer.\n  * Support video tunneling when the decoder is not listed first for the MIME\n    type ([#3100](https://github.com/google/ExoPlayer/issues/3100)).\n  * Query `MediaCodecList.ALL_CODECS` when selecting a tunneling decoder\n    ([#5547](https://github.com/google/ExoPlayer/issues/5547)).\n* DRM:\n  * Fix black flicker when keys rotate in DRM protected content\n    ([#3561](https://github.com/google/ExoPlayer/issues/3561)).\n  * Work around lack of LA_URL attribute in PlayReady key request init data.\n* CEA-608: Improved conformance to the specification\n  ([#3860](https://github.com/google/ExoPlayer/issues/3860)).\n* DASH:\n  * Parse role and accessibility descriptors into `Format.roleFlags`.\n  * Support multiple CEA-608 channels muxed into FMP4 representations\n    ([#5656](https://github.com/google/ExoPlayer/issues/5656)).\n* HLS:\n  * Prevent unnecessary reloads of initialization segments.\n  * Form an adaptive track group out of audio renditions with matching name.\n  * Support encrypted initialization segments\n    ([#5441](https://github.com/google/ExoPlayer/issues/5441)).\n  * Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`.\n  * Add metadata entry for HLS tracks to expose master playlist information.\n  * Prevent `IndexOutOfBoundsException` in some live HLS scenarios\n    ([#5816](https://github.com/google/ExoPlayer/issues/5816)).\n* Support for playing spherical videos on Daydream.\n* Cast extension: Work around Cast framework returning a limited-size queue\n  items list ([#4964](https://github.com/google/ExoPlayer/issues/4964)).\n* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to\n  surface YUV output as the default. Remove constructor parameters `scaleToFit`\n  and `useSurfaceYuvOutput`.\n* MediaSession extension:\n  * Let apps intercept media button events\n    ([#5179](https://github.com/google/ExoPlayer/issues/5179)).\n  * Fix issue with `TimelineQueueNavigator` not publishing the queue in shuffled\n    order when in shuffle mode.\n  * Allow handling of custom commands via `registerCustomCommandReceiver`.\n  * Add ability to include an extras `Bundle` when reporting a custom error.\n* Log warnings when extension native libraries can't be used, to help with\n  diagnosing playback failures\n  ([#5788](https://github.com/google/ExoPlayer/issues/5788)).\n\n### 2.9.6 (2019-02-19) ###\n\n* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.\n* IMA extension:\n  * Require setting the `Player` on `AdsLoader` instances before\n    playback.\n  * Remove deprecated `ImaAdsMediaSource`. Create `AdsMediaSource` with an\n    `ImaAdsLoader` instead.\n  * Remove deprecated `AdsMediaSource` constructors. Listen for media source\n    events using `AdsMediaSource.addEventListener`, and ad interaction events by\n    adding a listener when building `ImaAdsLoader`.\n  * Allow apps to register playback-related obstructing views that are on top of\n    their ad display containers via `AdsLoader.AdViewProvider`. `PlayerView`\n    implements this interface and will register its control view. This makes it\n    possible for ad loading SDKs to calculate ad viewability accurately.\n* DASH: Fix issue handling large `EventStream` presentation timestamps\n  ([#5490](https://github.com/google/ExoPlayer/issues/5490)).\n* HLS: Fix transition to STATE_ENDED when playing fragmented mp4 in chunkless\n  preparation ([#5524](https://github.com/google/ExoPlayer/issues/5524)).\n* Revert workaround for video quality problems with Amlogic decoders, as this\n  may cause problems for some devices and/or non-interlaced content\n  ([#5003](https://github.com/google/ExoPlayer/issues/5003)).\n\n### 2.9.5 (2019-01-31) ###\n\n* HLS: Parse `CHANNELS` attribute from `EXT-X-MEDIA` tag.\n* ConcatenatingMediaSource:\n  * Add `Handler` parameter to methods that take a callback `Runnable`.\n  * Fix issue with dropped messages when releasing the source\n    ([#5464](https://github.com/google/ExoPlayer/issues/5464)).\n* ExtractorMediaSource: Fix issue that could cause the player to get stuck\n  buffering at the end of the media.\n* PlayerView: Fix issue preventing `OnClickListener` from receiving events\n  ([#5433](https://github.com/google/ExoPlayer/issues/5433)).\n* IMA extension: Upgrade IMA dependency to 3.10.6.\n* Cronet extension: Upgrade Cronet dependency to 71.3578.98.\n* OkHttp extension: Upgrade OkHttp dependency to 3.12.1.\n* MP3: Wider fix for issue where streams would play twice on some Samsung\n  devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)).\n\n### 2.9.4 (2019-01-15) ###\n\n* IMA extension: Clear ads loader listeners on release\n  ([#4114](https://github.com/google/ExoPlayer/issues/4114)).\n* SmoothStreaming: Fix support for subtitles in DRM protected streams\n  ([#5378](https://github.com/google/ExoPlayer/issues/5378)).\n* FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior\n  of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)).\n* GVR extension: upgrade GVR SDK dependency to 1.190.0.\n* Associate fatal player errors of type SOURCE with the loading source in\n  `AnalyticsListener.EventTime`\n  ([#5407](https://github.com/google/ExoPlayer/issues/5407)).\n* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where\n  using lazy preparation in `ConcatenatingMediaSource` with an\n  `ExtractorMediaSource` overrides initial seek positions\n  ([#5350](https://github.com/google/ExoPlayer/issues/5350)).\n* Add subtext to the `MediaDescriptionAdapter` of the\n  `PlayerNotificationManager`.\n* Add workaround for video quality problems with Amlogic decoders\n  ([#5003](https://github.com/google/ExoPlayer/issues/5003)).\n* Fix issue where sending callbacks for playlist changes may cause problems\n  because of parallel player access\n  ([#5240](https://github.com/google/ExoPlayer/issues/5240)).\n* Fix issue with reusing a `ClippingMediaSource` with an inner\n  `ExtractorMediaSource` and a non-zero start position\n  ([#5351](https://github.com/google/ExoPlayer/issues/5351)).\n* Fix issue where uneven track durations in MP4 streams can cause OOM problems\n  ([#3670](https://github.com/google/ExoPlayer/issues/3670)).\n\n### 2.9.3 (2018-12-20) ###\n\n* Captions: Support PNG subtitles in SMPTE-TT\n  ([#1583](https://github.com/google/ExoPlayer/issues/1583)).\n* MPEG-TS: Use random access indicators to minimize the need for\n  `FLAG_ALLOW_NON_IDR_KEYFRAMES`.\n* Downloading: Reduce time taken to remove downloads\n  ([#5136](https://github.com/google/ExoPlayer/issues/5136)).\n* MP3:\n  * Use the true bitrate for constant-bitrate MP3 seeking.\n  * Fix issue where streams would play twice on some Samsung devices\n    ([#4519](https://github.com/google/ExoPlayer/issues/4519)).\n* Fix regression where some audio formats were incorrectly marked as being\n  unplayable due to under-reporting of platform decoder capabilities\n  ([#5145](https://github.com/google/ExoPlayer/issues/5145)).\n* Fix decode-only frame skipping on Nvidia Shield TV devices.\n* Workaround for MiTV (dangal) issue when swapping output surface\n  ([#5169](https://github.com/google/ExoPlayer/issues/5169)).\n\n### 2.9.2 (2018-11-28) ###\n\n* HLS:\n  * Fix issue causing unnecessary media playlist requests when playing live\n    streams ([#5059](https://github.com/google/ExoPlayer/issues/5059)).\n  * Fix decoder re-instantiation issue for packed audio streams\n    ([#5063](https://github.com/google/ExoPlayer/issues/5063)).\n* MP4: Support Opus and FLAC in the MP4 container, and in DASH\n  ([#4883](https://github.com/google/ExoPlayer/issues/4883)).\n* DASH: Fix detecting the end of live events\n  ([#4780](https://github.com/google/ExoPlayer/issues/4780)).\n* Spherical video: Fall back to `TYPE_ROTATION_VECTOR` if\n  `TYPE_GAME_ROTATION_VECTOR` is unavailable\n  ([#5119](https://github.com/google/ExoPlayer/issues/5119)).\n* Support seeking for a wider range of MPEG-TS streams\n  ([#5097](https://github.com/google/ExoPlayer/issues/5097)).\n* Include channel count in audio capabilities check\n  ([#4690](https://github.com/google/ExoPlayer/issues/4690)).\n* Fix issue with applying the `show_buffering` attribute in `PlayerView`\n  ([#5139](https://github.com/google/ExoPlayer/issues/5139)).\n* Fix issue where null `Metadata` was output when it failed to decode\n  ([#5149](https://github.com/google/ExoPlayer/issues/5149)).\n* Fix playback of some invalid but playable MP4 streams by replacing assertions\n  with logged warnings in sample table parsing code\n  ([#5162](https://github.com/google/ExoPlayer/issues/5162)).\n* Fix UUID passed to `MediaCrypto` when using `C.CLEARKEY_UUID` before API 27.\n\n### 2.9.1 (2018-11-01) ###\n\n* Add convenience methods `Player.next`, `Player.previous`, `Player.hasNext`\n  and `Player.hasPrevious`\n  ([#4863](https://github.com/google/ExoPlayer/issues/4863)).\n* Improve initial bandwidth meter estimates using the current country and\n  network type.\n* IMA extension:\n  * For preroll to live stream transitions, project forward the loading position\n    to avoid being behind the live window.\n  * Let apps specify whether to focus the skip button on ATV\n    ([#5019](https://github.com/google/ExoPlayer/issues/5019)).\n* MP3:\n  * Support seeking based on MLLT metadata\n    ([#3241](https://github.com/google/ExoPlayer/issues/3241)).\n  * Fix handling of streams with appended data\n    ([#4954](https://github.com/google/ExoPlayer/issues/4954)).\n* DASH: Parse ProgramInformation element if present in the manifest.\n* HLS:\n  * Add constructor to `DefaultHlsExtractorFactory` for adding TS payload\n    reader factory flags\n    ([#4861](https://github.com/google/ExoPlayer/issues/4861)).\n  * Fix bug in segment sniffing\n    ([#5039](https://github.com/google/ExoPlayer/issues/5039)).\n* SubRip: Add support for alignment tags, and remove tags from the displayed\n  captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).\n* Fix issue with blind seeking to windows with non-zero offset in a\n  `ConcatenatingMediaSource`\n  ([#4873](https://github.com/google/ExoPlayer/issues/4873)).\n* Fix logic for enabling next and previous actions in `TimelineQueueNavigator`\n  ([#5065](https://github.com/google/ExoPlayer/issues/5065)).\n* Fix issue where audio focus handling could not be disabled after enabling it\n  ([#5055](https://github.com/google/ExoPlayer/issues/5055)).\n* Fix issue where subtitles were positioned incorrectly if `SubtitleView` had a\n  non-zero position offset to its parent\n  ([#4788](https://github.com/google/ExoPlayer/issues/4788)).\n* Fix issue where the buffered position was not updated correctly when\n  transitioning between periods\n  ([#4899](https://github.com/google/ExoPlayer/issues/4899)).\n* Fix issue where a `NullPointerException` is thrown when removing an unprepared\n  media source from a `ConcatenatingMediaSource` with the `useLazyPreparation`\n  option enabled ([#4986](https://github.com/google/ExoPlayer/issues/4986)).\n* Work around an issue where a non-empty end-of-stream audio buffer would be\n  output with timestamp zero, causing the player position to jump backwards\n  ([#5045](https://github.com/google/ExoPlayer/issues/5045)).\n* Suppress a spurious assertion failure on some Samsung devices\n  ([#4532](https://github.com/google/ExoPlayer/issues/4532)).\n* Suppress spurious \"references unknown class member\" shrinking warning\n  ([#4890](https://github.com/google/ExoPlayer/issues/4890)).\n* Swap recommended order for google() and jcenter() in gradle config\n  ([#4997](https://github.com/google/ExoPlayer/issues/4997)).\n\n### 2.9.0 (2018-09-06) ###\n\n* Turn on Java 8 compiler support for the ExoPlayer library. Apps may need to\n  add `compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their\n  gradle settings to ensure bytecode compatibility.\n* Set `compileSdkVersion` and `targetSdkVersion` to 28.\n* Support for automatic audio focus handling via\n  `SimpleExoPlayer.setAudioAttributes`.\n* Add `ExoPlayer.retry` convenience method.\n* Add `AudioListener` for listening to changes in audio configuration during\n  playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)).\n* Add `LoadErrorHandlingPolicy` to allow configuration of load error handling\n  across `MediaSource` implementations\n  ([#3370](https://github.com/google/ExoPlayer/issues/3370)).\n* Allow passing a `Looper`, which specifies the thread that must be used to\n  access the player, when instantiating player instances using\n  `ExoPlayerFactory` ([#4278](https://github.com/google/ExoPlayer/issues/4278)).\n* Allow setting log level for ExoPlayer logcat output\n  ([#4665](https://github.com/google/ExoPlayer/issues/4665)).\n* Simplify `BandwidthMeter` injection: The `BandwidthMeter` should now be\n  passed directly to `ExoPlayerFactory`, instead of to `TrackSelection.Factory`\n  and `DataSource.Factory`. The `BandwidthMeter` is passed to the components\n  that need it internally. The `BandwidthMeter` may also be omitted, in which\n  case a default instance will be used.\n* Spherical video:\n  * Support for spherical video by setting `surface_type=\"spherical_view\"` on\n    `PlayerView`.\n  * Support for\n    [VR180](https://github.com/google/spatial-media/blob/master/docs/vr180.md).\n* HLS:\n  * Support PlayReady.\n  * Add container format sniffing\n    ([#2025](https://github.com/google/ExoPlayer/issues/2025)).\n  * Support alternative `EXT-X-KEY` tags.\n  * Support `EXT-X-INDEPENDENT-SEGMENTS` in the master playlist.\n  * Support variable substitution\n    ([#4422](https://github.com/google/ExoPlayer/issues/4422)).\n  * Fix the bitrate being unset on primary track sample formats\n    ([#3297](https://github.com/google/ExoPlayer/issues/3297)).\n  * Make `HlsMediaSource.Factory` take a factory of trackers instead of a\n    tracker instance ([#4814](https://github.com/google/ExoPlayer/issues/4814)).\n* DASH:\n  * Support `messageData` attribute for in-manifest event streams.\n  * Clip periods to their specified durations\n    ([#4185](https://github.com/google/ExoPlayer/issues/4185)).\n* Improve seeking support for progressive streams:\n  * Support seeking in MPEG-TS\n    ([#966](https://github.com/google/ExoPlayer/issues/966)).\n  * Support seeking in MPEG-PS\n    ([#4476](https://github.com/google/ExoPlayer/issues/4476)).\n  * Support approximate seeking in ADTS using a constant bitrate assumption\n    ([#4548](https://github.com/google/ExoPlayer/issues/4548)). The\n    `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the extractor to\n    enable this functionality.\n  * Support approximate seeking in AMR using a constant bitrate assumption.\n    The `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the extractor\n    to enable this functionality.\n  * Add `DefaultExtractorsFactory.setConstantBitrateSeekingEnabled` to enable\n    approximate seeking using a constant bitrate assumption on all extractors\n    that support it.\n* Video:\n  * Add callback to `VideoListener` to notify of surface size changes.\n  * Improve performance when playing high frame-rate content, and when playing\n    at greater than 1x speed\n    ([#2777](https://github.com/google/ExoPlayer/issues/2777)).\n  * Scale up the initial video decoder maximum input size so playlist\n    transitions with small increases in maximum sample size do not require\n    reinitialization ([#4510](https://github.com/google/ExoPlayer/issues/4510)).\n  * Fix a bug where the player would not transition to the ended state when\n    playing video in tunneled mode.\n* Audio:\n  * Support attaching auxiliary audio effects to the `AudioTrack` via\n    `Player.setAuxEffectInfo` and `Player.clearAuxEffectInfo`.\n  * Support seamless adaptation while playing xHE-AAC streams.\n    ([#4360](https://github.com/google/ExoPlayer/issues/4360)).\n  * Increase `AudioTrack` buffer sizes to the theoretical maximum required for\n    each encoding for passthrough playbacks\n    ([#3803](https://github.com/google/ExoPlayer/issues/3803)).\n  * WAV: Fix issue where white noise would be output at the end of playback\n    ([#4724](https://github.com/google/ExoPlayer/issues/4724)).\n  * MP3: Fix issue where streams would play twice on the SM-T530\n    ([#4519](https://github.com/google/ExoPlayer/issues/4519)).\n* Analytics:\n  * Add callbacks to `DefaultDrmSessionEventListener` and `AnalyticsListener` to\n    be notified of acquired and released DRM sessions.\n  * Add uri field to `LoadEventInfo` in `MediaSourceEventListener` and\n    `AnalyticsListener` callbacks. This uri is the redirected uri if redirection\n    occurred ([#2054](https://github.com/google/ExoPlayer/issues/2054)).\n  * Add response headers field to `LoadEventInfo` in `MediaSourceEventListener`\n    and `AnalyticsListener` callbacks\n    ([#4361](https://github.com/google/ExoPlayer/issues/4361) and\n    [#4615](https://github.com/google/ExoPlayer/issues/4615)).\n* UI components:\n  * Add option to `PlayerView` to show buffering view when playWhenReady is\n    false ([#4304](https://github.com/google/ExoPlayer/issues/4304)).\n  * Allow any `Drawable` to be used as `PlayerView` default artwork.\n* ConcatenatingMediaSource:\n  * Support lazy preparation of playlist media sources\n    ([#3972](https://github.com/google/ExoPlayer/issues/3972)).\n  * Support range removal with `removeMediaSourceRange` methods\n    ([#4542](https://github.com/google/ExoPlayer/issues/4542)).\n  * Support setting a new shuffle order with `setShuffleOrder`\n    ([#4791](https://github.com/google/ExoPlayer/issues/4791)).\n* MPEG-TS: Support CEA-608/708 in H262\n  ([#2565](https://github.com/google/ExoPlayer/issues/2565)).\n* Allow configuration of the back buffer in `DefaultLoadControl.Builder`\n  ([#4857](https://github.com/google/ExoPlayer/issues/4857)).\n* Allow apps to pass a `CacheKeyFactory` for setting custom cache keys when\n  creating a `CacheDataSource`.\n* Provide additional information for adaptive track selection.\n  `TrackSelection.updateSelectedTrack` has two new parameters for the current\n  queue of media chunks and iterators for information about upcoming chunks.\n* Allow `MediaCodecSelector`s to return multiple compatible decoders for\n  `MediaCodecRenderer`, and provide an (optional) `MediaCodecSelector` that\n  falls back to less preferred decoders like `MediaCodec.createDecoderByType`\n  ([#273](https://github.com/google/ExoPlayer/issues/273)).\n* Enable gzip for requests made by `SingleSampleMediaSource`\n  ([#4771](https://github.com/google/ExoPlayer/issues/4771)).\n* Fix bug reporting buffered position for multi-period windows, and add\n  convenience methods `Player.getTotalBufferedDuration` and\n  `Player.getContentBufferedDuration`\n  ([#4023](https://github.com/google/ExoPlayer/issues/4023)).\n* Fix bug where transitions to clipped media sources would happen too early\n  ([#4583](https://github.com/google/ExoPlayer/issues/4583)).\n* Fix bugs reporting events for multi-period media sources\n  ([#4492](https://github.com/google/ExoPlayer/issues/4492) and\n  [#4634](https://github.com/google/ExoPlayer/issues/4634)).\n* Fix issue where removing looping media from a playlist throws an exception\n  ([#4871](https://github.com/google/ExoPlayer/issues/4871).\n* Fix issue where the preferred audio or text track would not be selected if\n  mapped onto a secondary renderer of the corresponding type\n  ([#4711](http://github.com/google/ExoPlayer/issues/4711)).\n* Fix issue where errors of upcoming playlist items are thrown too early\n  ([#4661](https://github.com/google/ExoPlayer/issues/4661)).\n* Allow edit lists which do not start with a sync sample.\n  ([#4774](https://github.com/google/ExoPlayer/issues/4774)).\n* Fix issue with audio discontinuities at period transitions, e.g. when\n  looping ([#3829](https://github.com/google/ExoPlayer/issues/3829)).\n* Fix issue where `player.getCurrentTag()` throws an `IndexOutOfBoundsException`\n  ([#4822](https://github.com/google/ExoPlayer/issues/4822)).\n* Fix bug preventing use of multiple key session support (`multiSession=true`)\n  for non-Widevine `DefaultDrmSessionManager` instances\n  ([#4834](https://github.com/google/ExoPlayer/issues/4834)).\n* Fix issue where audio and video would desynchronize when playing\n  concatenations of gapless content\n  ([#4559](https://github.com/google/ExoPlayer/issues/4559)).\n* IMA extension:\n  * Refine the previous fix for empty ad groups to avoid discarding ad breaks\n    unnecessarily ([#4030](https://github.com/google/ExoPlayer/issues/4030) and\n    [#4280](https://github.com/google/ExoPlayer/issues/4280)).\n  * Fix handling of empty postrolls\n    ([#4681](https://github.com/google/ExoPlayer/issues/4681)).\n  * Fix handling of postrolls with multiple ads\n    ([#4710](https://github.com/google/ExoPlayer/issues/4710)).\n* MediaSession extension:\n  * Add `MediaSessionConnector.setCustomErrorMessage` to support setting custom\n    error messages.\n  * Add `MediaMetadataProvider` to support setting custom metadata\n    ([#3497](https://github.com/google/ExoPlayer/issues/3497)).\n* Cronet extension: Now distributed via jCenter.\n* FFmpeg extension: Support mu-law and A-law PCM.\n\n### 2.8.4 (2018-08-17) ###\n\n* IMA extension: Improve handling of consecutive empty ad groups\n  ([#4030](https://github.com/google/ExoPlayer/issues/4030)),\n  ([#4280](https://github.com/google/ExoPlayer/issues/4280)).\n\n### 2.8.3 (2018-07-23) ###\n\n* IMA extension:\n  * Fix behavior when creating/releasing the player then releasing\n    `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)).\n  * Add support for setting slots for companion ads.\n* Captions:\n  * TTML: Fix an issue with TTML using font size as % of cell resolution that\n    makes `SubtitleView.setApplyEmbeddedFontSizes()` not work correctly.\n    ([#4491](https://github.com/google/ExoPlayer/issues/4491)).\n  * CEA-608: Improve handling of embedded styles\n    ([#4321](https://github.com/google/ExoPlayer/issues/4321)).\n* DASH:\n  * Exclude text streams from duration calculations\n    ([#4029](https://github.com/google/ExoPlayer/issues/4029)).\n  * Fix freezing when playing multi-period manifests with `EventStream`s\n    ([#4492](https://github.com/google/ExoPlayer/issues/4492)).\n* DRM: Allow DrmInitData to carry a license server URL\n  ([#3393](https://github.com/google/ExoPlayer/issues/3393)).\n* MPEG-TS: Fix bug preventing SCTE-35 cues from being output\n  ([#4573](https://github.com/google/ExoPlayer/issues/4573)).\n* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using\n  CommentFrame to InternalFrame for frames with gapless metadata in MP4.\n* Add `PlayerView.isControllerVisible`\n  ([#4385](https://github.com/google/ExoPlayer/issues/4385)).\n* Fix issue playing DRM protected streams on Asus Zenfone 2\n  ([#4403](https://github.com/google/ExoPlayer/issues/4413)).\n* Add support for multiple audio and video tracks in MPEG-PS streams\n  ([#4406](https://github.com/google/ExoPlayer/issues/4406)).\n* Add workaround for track index mismatches between trex and tkhd boxes in\n  fragmented MP4 files\n  ([#4477](https://github.com/google/ExoPlayer/issues/4477)).\n* Add workaround for track index mismatches between tfhd and tkhd boxes in\n  fragmented MP4 files\n  ([#4083](https://github.com/google/ExoPlayer/issues/4083)).\n* Ignore all MP4 edit lists if one edit list couldn't be handled\n  ([#4348](https://github.com/google/ExoPlayer/issues/4348)).\n* Fix issue when switching track selection from an embedded track to a primary\n  track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)).\n* Fix accessibility class name for `DefaultTimeBar`\n  ([#4611](https://github.com/google/ExoPlayer/issues/4611)).\n* Improved compatibility with FireOS devices.\n\n### 2.8.2 (2018-06-06) ###\n\n* IMA extension: Don't advertise support for video/mpeg ad media, as we don't\n  have an extractor for this\n  ([#4297](https://github.com/google/ExoPlayer/issues/4297)).\n* DASH: Fix playback getting stuck when playing representations that have both\n  sidx atoms and non-zero presentationTimeOffset values.\n* HLS:\n  * Allow injection of custom playlist trackers.\n  * Fix adaptation in live playlists with EXT-X-PROGRAM-DATE-TIME tags.\n* Mitigate memory leaks when `MediaSource` loads are slow to cancel\n  ([#4249](https://github.com/google/ExoPlayer/issues/4249)).\n* Fix inconsistent `Player.EventListener` invocations for recursive player state\n  changes ([#4276](https://github.com/google/ExoPlayer/issues/4276)).\n* Fix `MediaCodec.native_setSurface` crash on Moto C\n  ([#4315](https://github.com/google/ExoPlayer/issues/4315)).\n* Fix missing whitespace in CEA-608\n  ([#3906](https://github.com/google/ExoPlayer/issues/3906)).\n* Fix crash downloading HLS media playlists\n  ([#4396](https://github.com/google/ExoPlayer/issues/4396)).\n* Fix a bug where download cancellation was ignored\n  ([#4403](https://github.com/google/ExoPlayer/issues/4403)).\n* Set `METADATA_KEY_TITLE` on media descriptions\n  ([#4292](https://github.com/google/ExoPlayer/issues/4292)).\n* Allow apps to register custom MIME types\n  ([#4264](https://github.com/google/ExoPlayer/issues/4264)).\n\n### 2.8.1 (2018-05-22) ###\n\n* HLS:\n  * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags\n    ([#4239](https://github.com/google/ExoPlayer/issues/4239)).\n  * Fix playback of clipped streams starting from non-keyframe positions\n    ([#4241](https://github.com/google/ExoPlayer/issues/4241)).\n* OkHttp extension: Fix to correctly include response headers in thrown\n  `InvalidResponseCodeException`s.\n* Add possibility to cancel `PlayerMessage`s.\n* UI components:\n  * Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed\n    video frame or media artwork visible when the player is reset\n    ([#2843](https://github.com/google/ExoPlayer/issues/2843)).\n* Fix crash when switching surface on Moto E(4)\n  ([#4134](https://github.com/google/ExoPlayer/issues/4134)).\n* Fix a bug that could cause event listeners to be called with inconsistent\n  information if an event listener interacted with the player\n  ([#4262](https://github.com/google/ExoPlayer/issues/4262)).\n* Audio:\n  * Fix extraction of PCM in MP4/MOV\n    ([#4228](https://github.com/google/ExoPlayer/issues/4228)).\n  * FLAC: Supports seeking for FLAC files without SEEKTABLE\n    ([#1808](https://github.com/google/ExoPlayer/issues/1808)).\n* Captions:\n  * TTML:\n    * Fix a styling issue when there are multiple regions displayed at the same\n      time that can make text size of each region much smaller than defined.\n    * Fix an issue when the caption line has no text (empty line or only line\n      break), and the line's background is still displayed.\n    * Support TTML font size using % correctly (as percentage of document cell\n      resolution).\n\n### 2.8.0 (2018-05-03) ###\n\n* Downloading:\n  * Add `DownloadService`, `DownloadManager` and related classes\n    ([#2643](https://github.com/google/ExoPlayer/issues/2643)). Information on\n    using these components to download progressive formats can be found\n    [here](https://medium.com/google-exoplayer/downloading-streams-6d259eec7f95).\n    To see how to download DASH, HLS and SmoothStreaming media, take a look at\n    the app.\n  * Updated main demo app to support downloading DASH, HLS, SmoothStreaming and\n    progressive media.\n* MediaSources:\n  * Allow reusing media sources after they have been released and\n    also in parallel to allow adding them multiple times to a concatenation.\n    ([#3498](https://github.com/google/ExoPlayer/issues/3498)).\n  * Merged `DynamicConcatenatingMediaSource` into `ConcatenatingMediaSource` and\n    deprecated `DynamicConcatenatingMediaSource`.\n  * Allow clipping of child media sources where the period and window have a\n    non-zero offset with `ClippingMediaSource`.\n  * Allow adding and removing `MediaSourceEventListener`s to MediaSources after\n    they have been created. Listening to events is now supported for all\n    media sources including composite sources.\n  * Added callbacks to `MediaSourceEventListener` to get notified when media\n    periods are created, released and being read from.\n  * Support live stream clipping with `ClippingMediaSource`.\n  * Allow setting tags for all media sources in their factories. The tag of the\n    current window can be retrieved with `Player.getCurrentTag`.\n* UI components:\n  * Add support for displaying error messages and a buffering spinner in\n    `PlayerView`.\n  * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update\n    ([#3736](https://github.com/google/ExoPlayer/issues/3736)).\n  * Add `PlayerNotificationManager` for displaying notifications reflecting the\n    player state.\n  * Add `TrackSelectionView` for selecting tracks with `DefaultTrackSelector`.\n  * Add `TrackNameProvider` for converting track `Format`s to textual\n    descriptions, and `DefaultTrackNameProvider` as a default implementation.\n* Track selection:\n  * Reworked `MappingTrackSelector` and `DefaultTrackSelector`.\n  * `DefaultTrackSelector.Parameters` now implements `Parcelable`.\n  * Added UI components for track selection (see above).\n* Audio:\n  * Support extracting data from AMR container formats, including both narrow\n    and wide band ([#2527](https://github.com/google/ExoPlayer/issues/2527)).\n  * FLAC:\n    * Sniff FLAC files correctly if they have ID3 headers\n      ([#4055](https://github.com/google/ExoPlayer/issues/4055)).\n    * Supports FLAC files with high sample rate (176400 and 192000)\n      ([#3769](https://github.com/google/ExoPlayer/issues/3769)).\n  * Factor out `AudioTrack` position tracking from `DefaultAudioSink`.\n  * Fix an issue where the playback position would pause just after playback\n    begins, and poll the audio timestamp less frequently once it starts\n    advancing ([#3841](https://github.com/google/ExoPlayer/issues/3841)).\n  * Add an option to skip silent audio in `PlaybackParameters`\n    ([#2635](https://github.com/google/ExoPlayer/issues/2635)).\n  * Fix an issue where playback of TrueHD streams would get stuck after seeking\n    due to not finding a syncframe\n    ([#3845](https://github.com/google/ExoPlayer/issues/3845)).\n  * Fix an issue with eac3-joc playback where a codec would fail to configure\n    ([#4165](https://github.com/google/ExoPlayer/issues/4165)).\n  * Handle non-empty end-of-stream buffers, to fix gapless playback of streams\n    with encoder padding when the decoder returns a non-empty final buffer.\n  * Allow trimming more than one sample when applying an elst audio edit via\n    gapless playback info.\n  * Allow overriding skipping/scaling with custom `AudioProcessor`s\n    ([#3142](https://github.com/google/ExoPlayer/issues/3142)).\n* Caching:\n  * Add release method to the `Cache` interface, and prevent multiple instances\n    of `SimpleCache` using the same folder at the same time.\n  * Cache redirect URLs\n    ([#2360](https://github.com/google/ExoPlayer/issues/2360)).\n* DRM:\n  * Allow multiple listeners for `DefaultDrmSessionManager`.\n  * Pass `DrmSessionManager` to `ExoPlayerFactory` instead of `RendererFactory`.\n  * Change minimum API requirement for CBC and pattern encryption from 24 to 25\n    ([#4022](https://github.com/google/ExoPlayer/issues/4022)).\n  * Fix handling of 307/308 redirects when making license requests\n    ([#4108](https://github.com/google/ExoPlayer/issues/4108)).\n* HLS:\n  * Fix playlist loading error propagation when the current selection does\n    not include all of the playlist's variants.\n  * Fix SAMPLE-AES-CENC and SAMPLE-AES-CTR EXT-X-KEY methods\n    ([#4145](https://github.com/google/ExoPlayer/issues/4145)).\n  * Preeptively declare an ID3 track in chunkless preparation\n    ([#4016](https://github.com/google/ExoPlayer/issues/4016)).\n  * Add support for multiple #EXT-X-MAP tags in a media playlist\n    ([#4164](https://github.com/google/ExoPlayer/issues/4182)).\n  * Fix seeking in live streams\n    ([#4187](https://github.com/google/ExoPlayer/issues/4187)).\n* IMA extension:\n  * Allow setting the ad media load timeout\n    ([#3691](https://github.com/google/ExoPlayer/issues/3691)).\n  * Expose ad load errors via `MediaSourceEventListener` on `AdsMediaSource`,\n    and allow setting an ad event listener on `ImaAdsLoader`. Deprecate the\n    `AdsMediaSource.EventListener`.\n* Add `AnalyticsListener` interface which can be registered in\n  `SimpleExoPlayer` to receive detailed metadata for each ExoPlayer event.\n* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample within\n  a fragment. This benefits standalone FMP4 playbacks, DASH and SmoothStreaming.\n* Updated default max buffer length in `DefaultLoadControl`.\n* Fix ClearKey decryption error if the key contains a forward slash\n  ([#4075](https://github.com/google/ExoPlayer/issues/4075)).\n* Fix crash when switching surface on Huawei P9 Lite\n  ([#4084](https://github.com/google/ExoPlayer/issues/4084)), and Philips QM163E\n  ([#4104](https://github.com/google/ExoPlayer/issues/4104)).\n* Support ZLIB compressed PGS subtitles.\n* Added `getPlaybackError` to `Player` interface.\n* Moved initial bitrate estimate from `AdaptiveTrackSelection` to\n  `DefaultBandwidthMeter`.\n* Removed default renderer time offset of 60000000 from internal player. The\n  actual renderer timestamp offset can be obtained by listening to\n  `BaseRenderer.onStreamChanged`.\n* Added dependencies on checkerframework annotations for static code analysis.\n\n### 2.7.3 (2018-04-04) ###\n\n* Fix ProGuard configuration for Cast, IMA and OkHttp extensions.\n* Update OkHttp extension to depend on OkHttp 3.10.0.\n\n### 2.7.2 (2018-03-29) ###\n\n* Gradle: Upgrade Gradle version from 4.1 to 4.4 so it can work with Android\n  Studio 3.1 ([#3708](https://github.com/google/ExoPlayer/issues/3708)).\n* Match codecs starting with \"mp4a\" to different Audio MimeTypes\n  ([#3779](https://github.com/google/ExoPlayer/issues/3779)).\n* Fix ANR issue on Redmi 4X and Redmi Note 4\n  ([#4006](https://github.com/google/ExoPlayer/issues/4006)).\n* Fix handling of zero padded strings when parsing Matroska streams\n  ([#4010](https://github.com/google/ExoPlayer/issues/4010)).\n* Fix \"Decoder input buffer too small\" error when playing some FLAC streams.\n* MediaSession extension: Omit fast forward and rewind actions when media is not\n  seekable ([#4001](https://github.com/google/ExoPlayer/issues/4001)).\n\n### 2.7.1 (2018-03-09) ###\n\n* Gradle: Replaced 'compile' (deprecated) with 'implementation' and\n  'api'. This may lead to build breakage for applications upgrading from\n  previous version that rely on indirect dependencies of certain modules. In\n  such cases, application developers need to add the missing dependency to\n  their gradle file. You can read more about the new dependency configurations\n  [here](https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#new_configurations).\n* HlsMediaSource: Make HLS periods start at zero instead of the epoch.\n  Applications that rely on HLS timelines having a period starting at\n  the epoch will need to update their handling of HLS timelines. The program\n  date time is still available via the informational\n  `Timeline.Window.windowStartTimeMs` field\n  ([#3865](https://github.com/google/ExoPlayer/issues/3865),\n  [#3888](https://github.com/google/ExoPlayer/issues/3888)).\n* Enable seeking in MP4 streams where duration is set incorrectly in the track\n  header ([#3926](https://github.com/google/ExoPlayer/issues/3926)).\n* Video: Force rendering a frame periodically in `MediaCodecVideoRenderer` and\n  `LibvpxVideoRenderer`, even if it is late.\n\n### 2.7.0 (2018-02-19) ###\n\n* Player interface:\n  * Add optional parameter to `stop` to reset the player when stopping.\n  * Add a reason to `EventListener.onTimelineChanged` to distinguish between\n    initial preparation, reset and dynamic updates.\n  * Add `Player.DISCONTINUITY_REASON_AD_INSERTION` to the possible reasons\n    reported in `Eventlistener.onPositionDiscontinuity` to distinguish\n    transitions to and from ads within one period from transitions between\n    periods.\n  * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow\n    more customization of the message. Now supports setting a message delivery\n    playback position and/or a delivery handler\n    ([#2189](https://github.com/google/ExoPlayer/issues/2189)).\n  * Add `Player.VideoComponent`, `Player.TextComponent` and\n    `Player.MetadataComponent` interfaces that define optional video, text and\n    metadata output functionality. New `getVideoComponent`, `getTextComponent`\n    and `getMetadataComponent` methods provide access to this functionality.\n* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are\n  performed. The `SeekParameters` class contains defaults for exact seeking and\n  seeking to the closest sync points before, either side or after specified seek\n  positions. `SeekParameters` are not currently supported when playing HLS\n  streams.\n* DefaultTrackSelector:\n  * Replace `DefaultTrackSelector.Parameters` copy methods with a builder.\n  * Support disabling of individual text track selection flags.\n* Buffering:\n  * Allow a back-buffer of media to be retained behind the current playback\n    position, for fast backward seeking. The back-buffer can be configured by\n    custom `LoadControl` implementations.\n  * Add ability for `SequenceableLoader` to re-evaluate its buffer and discard\n    buffered media so that it can be re-buffered in a different quality.\n  * Allow more flexible loading strategy when playing media containing multiple\n    sub-streams, by allowing injection of custom `CompositeSequenceableLoader`\n    factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`,\n    `SsMediaSource.Factory`, and `MergingMediaSource`.\n  * Play out existing buffer before retrying for progressive live streams\n    ([#1606](https://github.com/google/ExoPlayer/issues/1606)).\n* UI components:\n  * Generalized player and control views to allow them to bind with any\n    `Player`, and renamed them to `PlayerView` and `PlayerControlView`\n    respectively.\n  * Made `PlayerView` automatically apply video rotation when configured to use\n    `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)).\n  * Made `PlayerView` play button behave correctly when the player is ended\n    ([#3689](https://github.com/google/ExoPlayer/issues/3689)), and call a\n    `PlaybackPreparer` when the player is idle.\n* DRM: Optimistically attempt playback of DRM protected content that does not\n  declare scheme specific init data in the manifest. If playback of clear\n  samples without keys is allowed, delay DRM session error propagation until\n  keys are actually needed\n  ([#3630](https://github.com/google/ExoPlayer/issues/3630)).\n* DASH:\n  * Support in-band Emsg events targeting the player with scheme id\n    `urn:mpeg:dash:event:2012` and scheme values \"1\", \"2\" and \"3\".\n  * Support EventStream elements in DASH manifests.\n* HLS:\n    * Add opt-in support for chunkless preparation in HLS. This allows an\n      HLS source to finish preparation without downloading any chunks, which can\n      significantly reduce initial buffering time\n      ([#3149](https://github.com/google/ExoPlayer/issues/3149)). More details\n      can be found\n      [here](https://medium.com/google-exoplayer/faster-hls-preparation-f6611aa15ea6).\n    * Fail if unable to sync with the Transport Stream, rather than entering\n      stuck in an indefinite buffering state.\n    * Fix mime type propagation\n      ([#3653](https://github.com/google/ExoPlayer/issues/3653)).\n    * Fix ID3 context reuse across segment format changes\n      ([#3622](https://github.com/google/ExoPlayer/issues/3622)).\n    * Use long for media sequence numbers\n      ([#3747](https://github.com/google/ExoPlayer/issues/3747))\n    * Add initial support for the EXT-X-GAP tag.\n* Audio:\n  * Support TrueHD passthrough for rechunked samples in Matroska files\n    ([#2147](https://github.com/google/ExoPlayer/issues/2147)).\n  * Support resampling 24-bit and 32-bit integer to 32-bit float for high\n    resolution output in `DefaultAudioSink`\n    ([#3635](https://github.com/google/ExoPlayer/pull/3635)).\n* Captions:\n  * Basic support for PGS subtitles\n    ([#3008](https://github.com/google/ExoPlayer/issues/3008)).\n  * Fix handling of CEA-608 captions where multiple buffers have the same\n    presentation timestamp\n    ([#3782](https://github.com/google/ExoPlayer/issues/3782)).\n* Caching:\n  * Fix cache corruption issue\n    ([#3762](https://github.com/google/ExoPlayer/issues/3762)).\n  * Implement periodic check in `CacheDataSource` to see whether it's possible\n    to switch to reading/writing the cache having initially bypassed it.\n* IMA extension:\n    * Fix the player getting stuck when an ad group fails to load\n      ([#3584](https://github.com/google/ExoPlayer/issues/3584)).\n    * Work around loadAd not being called beore the LOADED AdEvent arrives\n      ([#3552](https://github.com/google/ExoPlayer/issues/3552)).\n    * Handle asset mismatch errors\n      ([#3801](https://github.com/google/ExoPlayer/issues/3801)).\n    * Add support for playing non-Extractor content MediaSources in\n      the IMA demo app\n      ([#3676](https://github.com/google/ExoPlayer/issues/3676)).\n    * Fix handling of ad tags where ad groups are out of order\n      ([#3716](https://github.com/google/ExoPlayer/issues/3716)).\n    * Fix handling of ad tags with only preroll/postroll ad groups\n      ([#3715](https://github.com/google/ExoPlayer/issues/3715)).\n    * Propagate ad media preparation errors to IMA so that the ads can be\n      skipped.\n    * Handle exceptions in IMA callbacks so that can be logged less verbosely.\n* New Cast extension. Simplifies toggling between local and Cast playbacks.\n* `EventLogger` moved from the demo app into the core library.\n* Fix ANR issue on the Huawei P8 Lite, Huawei Y6II, Moto C+, Meizu M5C,\n  Lenovo K4 Note and Sony Xperia E5.\n  ([#3724](https://github.com/google/ExoPlayer/issues/3724),\n  [#3835](https://github.com/google/ExoPlayer/issues/3835)).\n* Fix potential NPE when removing media sources from a\n  DynamicConcatenatingMediaSource\n  ([#3796](https://github.com/google/ExoPlayer/issues/3796)).\n* Check `sys.display-size` on Philips ATVs\n  ([#3807](https://github.com/google/ExoPlayer/issues/3807)).\n* Release `Extractor`s on the loading thread to avoid potentially leaking\n  resources when the playback thread has quit by the time the loading task has\n  completed.\n* ID3: Better handle malformed ID3 data\n  ([#3792](https://github.com/google/ExoPlayer/issues/3792).\n* Support 14-bit mode and little endianness in DTS PES packets\n  ([#3340](https://github.com/google/ExoPlayer/issues/3340)).\n* Demo app: Add ability to download not DRM protected content.\n\n### 2.6.1 (2017-12-15) ###\n\n* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`,\n  `DashMediaSource` and `SingleSampleMediaSource`.\n* Use the same listener `MediaSourceEventListener` for all MediaSource\n  implementations.\n* IMA extension:\n  * Support non-ExtractorMediaSource ads\n    ([#3302](https://github.com/google/ExoPlayer/issues/3302)).\n  * Skip ads before the ad preceding the player's initial seek position\n    ([#3527](https://github.com/google/ExoPlayer/issues/3527)).\n  * Fix ad loading when there is no preroll.\n  * Add an option to turn off hiding controls during ad playback\n    ([#3532](https://github.com/google/ExoPlayer/issues/3532)).\n  * Support specifying an ads response instead of an ad tag\n    ([#3548](https://github.com/google/ExoPlayer/issues/3548)).\n  * Support overriding the ad load timeout\n    ([#3556](https://github.com/google/ExoPlayer/issues/3556)).\n* DASH: Support time zone designators in ISO8601 UTCTiming elements\n  ([#3524](https://github.com/google/ExoPlayer/issues/3524)).\n* Audio:\n  * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option\n    to use this with `FfmpegAudioRenderer`.\n  * Add support for extracting 32-bit WAVE files\n    ([#3379](https://github.com/google/ExoPlayer/issues/3379)).\n  * Support extraction and decoding of Dolby Atmos\n    ([#2465](https://github.com/google/ExoPlayer/issues/2465)).\n  * Fix handling of playback parameter changes while paused when followed by a\n    seek.\n* SimpleExoPlayer: Allow multiple audio and video debug listeners.\n* DefaultTrackSelector: Support undefined language text track selection when the\n  preferred language is not available\n  ([#2980](https://github.com/google/ExoPlayer/issues/2980)).\n* Add options to `DefaultLoadControl` to set maximum buffer size in bytes and\n  to choose whether size or time constraints are prioritized.\n* Use surfaceless context for secure `DummySurface`, if available\n  ([#3558](https://github.com/google/ExoPlayer/issues/3558)).\n* FLV: Fix playback of live streams that do not contain an audio track\n  ([#3188](https://github.com/google/ExoPlayer/issues/3188)).\n* CEA-608: Fix handling of row count changes in roll-up mode\n  ([#3513](https://github.com/google/ExoPlayer/issues/3513)).\n* Prevent period transitions when seeking to the end of a period when paused\n  ([#2439](https://github.com/google/ExoPlayer/issues/2439)).\n\n### 2.6.0 (2017-11-03) ###\n\n* Removed \"r\" prefix from versions. This release is \"2.6.0\", not \"r2.6.0\".\n* New `Player.DefaultEventListener` abstract class can be extended to avoid\n  having to implement all methods defined by `Player.EventListener`.\n* Added a reason to `EventListener.onPositionDiscontinuity`\n  ([#3252](https://github.com/google/ExoPlayer/issues/3252)).\n* New `setShuffleModeEnabled` method for enabling shuffled playback.\n* SimpleExoPlayer: Support for multiple video, text and metadata outputs.\n* Support for `Renderer`s that don't consume any media\n  ([#3212](https://github.com/google/ExoPlayer/issues/3212)).\n* Fix reporting of internal position discontinuities via\n  `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is\n  added to disambiguate position adjustments during seeks from other types of\n  internal position discontinuity.\n* Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration`\n  ([#3362](https://github.com/google/ExoPlayer/issues/3362)).\n* Fix playbacks involving looping, concatenation and ads getting stuck when\n  media contains tracks with uneven durations\n  ([#1874](https://github.com/google/ExoPlayer/issues/1874)).\n* Fix issue with `ContentDataSource` when reading from certain `ContentProvider`\n  implementations ([#3426](https://github.com/google/ExoPlayer/issues/3426)).\n* Better playback experience when the video decoder cannot keep up, by skipping\n  to key-frames. This is particularly relevant for variable speed playbacks.\n* Allow `SingleSampleMediaSource` to suppress load errors\n  ([#3140](https://github.com/google/ExoPlayer/issues/3140)).\n* `DynamicConcatenatingMediaSource`: Allow specifying a callback to be invoked\n  after a dynamic playlist modification has been applied\n  ([#3407](https://github.com/google/ExoPlayer/issues/3407)).\n* Audio: New `AudioSink` interface allows customization of audio output path.\n* Offline: Added `Downloader` implementations for DASH, HLS, SmoothStreaming\n  and progressive streams.\n* Track selection:\n  * Fixed adaptive track selection logic for live playbacks\n    ([#3017](https://github.com/google/ExoPlayer/issues/3017)).\n  * Added ability to select the lowest bitrate tracks.\n* DASH:\n  * Don't crash when a malformed or unexpected manifest update occurs\n    ([#2795](https://github.com/google/ExoPlayer/issues/2795)).\n* HLS:\n  * Support for Widevine protected FMP4 variants.\n  * Support CEA-608 in FMP4 variants.\n  * Support extractor injection\n    ([#2748](https://github.com/google/ExoPlayer/issues/2748)).\n* DRM:\n  * Improved compatibility with ClearKey content\n    ([#3138](https://github.com/google/ExoPlayer/issues/3138)).\n  * Support multiple PSSH boxes of the same type.\n  * Retry initial provisioning and key requests if they fail\n  * Fix incorrect parsing of non-CENC sinf boxes.\n* IMA extension:\n  * Expose `AdsLoader` via getter\n    ([#3322](https://github.com/google/ExoPlayer/issues/3322)).\n  * Handle `setPlayWhenReady` calls during ad playbacks\n    ([#3303](https://github.com/google/ExoPlayer/issues/3303)).\n  * Ignore seeks if an ad is playing\n    ([#3309](https://github.com/google/ExoPlayer/issues/3309)).\n  * Improve robustness of `ImaAdsLoader` in case content is not paused between\n    content to ad transitions\n    ([#3430](https://github.com/google/ExoPlayer/issues/3430)).\n* UI:\n  * Allow specifying a `Drawable` for the `TimeBar` scrubber\n    ([#3337](https://github.com/google/ExoPlayer/issues/3337)).\n  * Allow multiple listeners on `TimeBar`\n    ([#3406](https://github.com/google/ExoPlayer/issues/3406)).\n* New Leanback extension: Simplifies binding Exoplayer to Leanback UI\n  components.\n* Unit tests moved to Robolectric.\n* Misc bugfixes.\n\n### r2.5.4 (2017-10-19) ###\n\n* Remove unnecessary media playlist fetches during playback of live HLS streams.\n* Add the ability to inject a HLS playlist parser through `HlsMediaSource`.\n* Fix potential `IndexOutOfBoundsException` when using `ImaMediaSource`\n  ([#3334](https://github.com/google/ExoPlayer/issues/3334)).\n* Fix an issue parsing MP4 content containing non-CENC sinf boxes.\n* Fix memory leak when seeking with repeated periods.\n* Fix playback position when `ExoPlayer.prepare` is called with `resetPosition`\n  set to false.\n* Ignore MP4 edit lists that seem invalid\n  ([#3351](https://github.com/google/ExoPlayer/issues/3351)).\n* Add extractor flag for ignoring all MP4 edit lists\n  ([#3358](https://github.com/google/ExoPlayer/issues/3358)).\n* Improve extensibility by exposing public constructors for\n  `FrameworkMediaCrypto` and by making `DefaultDashChunkSource.getNextChunk`\n  non-final.\n\n### r2.5.3 (2017-09-20) ###\n\n* IMA extension: Support skipping of skippable ads on AndroidTV and other\n  non-touch devices ([#3258](https://github.com/google/ExoPlayer/issues/3258)).\n* HLS: Fix broken WebVTT captions when PTS wraps around\n  ([#2928](https://github.com/google/ExoPlayer/issues/2928)).\n* Captions: Fix issues rendering CEA-608 captions\n  ([#3250](https://github.com/google/ExoPlayer/issues/3250)).\n* Workaround broken AAC decoders on Galaxy S6\n  ([#3249](https://github.com/google/ExoPlayer/issues/3249)).\n* Caching: Fix infinite loop when cache eviction fails\n  ([#3260](https://github.com/google/ExoPlayer/issues/3260)).\n* Caching: Force use of BouncyCastle on JellyBean to fix decryption issue\n  ([#2755](https://github.com/google/ExoPlayer/issues/2755)).\n\n### r2.5.2 (2017-09-11) ###\n\n* IMA extension: Fix issue where ad playback could end prematurely for some\n  content types ([#3180](https://github.com/google/ExoPlayer/issues/3180)).\n* RTMP extension: Fix SIGABRT on fast RTMP stream restart\n  ([#3156](https://github.com/google/ExoPlayer/issues/3156)).\n* UI: Allow app to manually specify ad markers\n  ([#3184](https://github.com/google/ExoPlayer/issues/3184)).\n* DASH: Expose segment indices to subclasses of DefaultDashChunkSource\n  ([#3037](https://github.com/google/ExoPlayer/issues/3037)).\n* Captions: Added robustness against malformed WebVTT captions\n  ([#3228](https://github.com/google/ExoPlayer/issues/3228)).\n* DRM: Support forcing a specific license URL.\n* Fix playback error when seeking in media loaded through content:// URIs\n  ([#3216](https://github.com/google/ExoPlayer/issues/3216)).\n* Fix issue playing MP4s in which the last atom specifies a size of zero\n  ([#3191](https://github.com/google/ExoPlayer/issues/3191)).\n* Workaround playback failures on some Xiaomi devices\n  ([#3171](https://github.com/google/ExoPlayer/issues/3171)).\n* Workaround SIGSEGV issue on some devices when setting and swapping surface for\n  secure playbacks ([#3215](https://github.com/google/ExoPlayer/issues/3215)).\n* Workaround for Nexus 7 issue when swapping output surface\n  ([#3236](https://github.com/google/ExoPlayer/issues/3236)).\n* Workaround for SimpleExoPlayerView's surface not being hidden properly\n  ([#3160](https://github.com/google/ExoPlayer/issues/3160)).\n\n### r2.5.1 (2017-08-08) ###\n\n* Fix an issue that could cause the reported playback position to stop advancing\n  in some cases.\n* Fix an issue where a Surface could be released whilst still in use by the\n  player.\n\n### r2.5.0 (2017-08-07) ###\n\n* IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an\n  easy and seamless way of incorporating display ads into ExoPlayer playbacks.\n  You can read more about the IMA extension\n  [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea).\n* MediaSession extension: Provides an easy way to connect ExoPlayer with\n  MediaSessionCompat in the Android Support Library.\n* RTMP extension: An extension for playing streams over RTMP.\n* Build: Made it easier for application developers to depend on a local checkout\n  of ExoPlayer. You can learn how to do this\n  [here](https://medium.com/google-exoplayer/howto-2-depend-on-a-local-checkout-of-exoplayer-bcd7f8531720).\n* Core playback improvements:\n  * Eliminated re-buffering when changing audio and text track selections during\n    playback of progressive streams\n    ([#2926](https://github.com/google/ExoPlayer/issues/2926)).\n  * New DynamicConcatenatingMediaSource class to support playback of dynamic\n    playlists.\n  * New ExoPlayer.setRepeatMode method for dynamic toggling of repeat mode\n    during playback. Use of setRepeatMode should be preferred to\n    LoopingMediaSource for most looping use cases. You can read more about\n    setRepeatMode\n    [here](https://medium.com/google-exoplayer/repeat-modes-in-exoplayer-19dd85f036d3).\n  * Eliminated jank when switching video playback from one Surface to another on\n    API level 23+ for unencrypted content, and on devices that support the\n    EGL_EXT_protected_content OpenGL extension for protected content\n    ([#677](https://github.com/google/ExoPlayer/issues/677)).\n  * Enabled ExoPlayer instantiation on background threads without Loopers.\n    Events from such players are delivered on the application's main thread.\n* HLS improvements:\n  * Optimized adaptive switches for playlists that specify the\n    EXT-X-INDEPENDENT-SEGMENTS tag.\n  * Optimized in-buffer seeking\n    ([#551](https://github.com/google/ExoPlayer/issues/551)).\n  * Eliminated re-buffering when changing audio and text track selections during\n    playback, provided the new selection does not require switching to different\n    renditions ([#2718](https://github.com/google/ExoPlayer/issues/2718)).\n  * Exposed all media playlist tags in ExoPlayer's MediaPlaylist object.\n* DASH: Support for seamless switching across streams in different AdaptationSet\n  elements ([#2431](https://github.com/google/ExoPlayer/issues/2431)).\n* DRM: Support for additional crypto schemes (cbc1, cbcs and cens) on\n  API level 24+ ([#1989](https://github.com/google/ExoPlayer/issues/1989)).\n* Captions: Initial support for SSA/ASS subtitles\n  ([#889](https://github.com/google/ExoPlayer/issues/889)).\n* AndroidTV: Fixed issue where tunneled video playback would not start on some\n  devices ([#2985](https://github.com/google/ExoPlayer/issues/2985)).\n* MPEG-TS: Fixed segmentation issue when parsing H262\n  ([#2891](https://github.com/google/ExoPlayer/issues/2891)).\n* Cronet extension: Support for a user-defined fallback if Cronet library is not\n  present.\n* Fix buffer too small IllegalStateException issue affecting some composite\n  media playbacks ([#2900](https://github.com/google/ExoPlayer/issues/2900)).\n* Misc bugfixes.\n\n### r2.4.4 (2017-07-19) ###\n\n* HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance\n  ([#3040](https://github.com/google/ExoPlayer/issues/3040)).\n* HLS: Fix propagation of format identifier for CEA-608\n  ([#3033](https://github.com/google/ExoPlayer/issues/3033)).\n* HLS: Detect playlist stuck and reset conditions\n  ([#2872](https://github.com/google/ExoPlayer/issues/2872)).\n* Video: Fix video dimension reporting on some devices\n  ([#3007](https://github.com/google/ExoPlayer/issues/3007)).\n\n### r2.4.3 (2017-06-30) ###\n\n* Audio: Workaround custom audio decoders misreporting their maximum supported\n  channel counts ([#2940](https://github.com/google/ExoPlayer/issues/2940)).\n* Audio: Workaround for broken MediaTek raw decoder on some devices\n  ([#2873](https://github.com/google/ExoPlayer/issues/2873)).\n* Captions: Fix TTML captions appearing at the top of the screen\n  ([#2953](https://github.com/google/ExoPlayer/issues/2953)).\n* Captions: Fix handling of some DVB subtitles\n  ([#2957](https://github.com/google/ExoPlayer/issues/2957)).\n* Track selection: Fix setSelectionOverride(index, tracks, null)\n  ([#2988](https://github.com/google/ExoPlayer/issues/2988)).\n* GVR extension: Add support for mono input\n  ([#2710](https://github.com/google/ExoPlayer/issues/2710)).\n* FLAC extension: Fix failing build\n  ([#2977](https://github.com/google/ExoPlayer/pull/2977)).\n* Misc bugfixes.\n\n### r2.4.2 (2017-06-06) ###\n\n* Stability: Work around Nexus 10 reboot when playing certain content\n  ([#2806](https://github.com/google/ExoPlayer/issues/2806)).\n* MP3: Correctly treat MP3s with INFO headers as constant bitrate\n  ([#2895](https://github.com/google/ExoPlayer/issues/2895)).\n* HLS: Use average rather than peak bandwidth when available\n  ([#2863](https://github.com/google/ExoPlayer/issues/2863)).\n* SmoothStreaming: Fix timeline for live streams\n  ([#2760](https://github.com/google/ExoPlayer/issues/2760)).\n* UI: Fix DefaultTimeBar invalidation\n  ([#2871](https://github.com/google/ExoPlayer/issues/2871)).\n* Misc bugfixes.\n\n### r2.4.1 (2017-05-23) ###\n\n* Stability: Avoid OutOfMemoryError in extractors when parsing malformed media\n  ([#2780](https://github.com/google/ExoPlayer/issues/2780)).\n* Stability: Avoid native crash on Galaxy Nexus. Avoid unnecessarily large codec\n  input buffer allocations on all devices\n  ([#2607](https://github.com/google/ExoPlayer/issues/2607)).\n* Variable speed playback: Fix interpolation for rate/pitch adjustment\n  ([#2774](https://github.com/google/ExoPlayer/issues/2774)).\n* HLS: Include EXT-X-DATERANGE tags in HlsMediaPlaylist.\n* HLS: Don't expose CEA-608 track if CLOSED-CAPTIONS=NONE\n  ([#2743](https://github.com/google/ExoPlayer/issues/2743)).\n* HLS: Correctly propagate errors loading the media playlist\n  ([#2623](https://github.com/google/ExoPlayer/issues/2623)).\n* UI: DefaultTimeBar enhancements and bug fixes\n  ([#2740](https://github.com/google/ExoPlayer/issues/2740)).\n* Ogg: Fix failure to play some Ogg files\n  ([#2782](https://github.com/google/ExoPlayer/issues/2782)).\n* Captions: Don't select text tack with no language by default.\n* Captions: TTML positioning fixes\n  ([#2824](https://github.com/google/ExoPlayer/issues/2824)).\n* Misc bugfixes.\n\n### r2.4.0 (2017-04-25) ###\n\n* New modular library structure. You can read more about depending on individual\n  library modules\n  [here](https://medium.com/google-exoplayer/exoplayers-new-modular-structure-a916c0874907).\n* Variable speed playback support on API level 16+. You can read more about\n  changing the playback speed\n  [here](https://medium.com/google-exoplayer/variable-speed-playback-with-exoplayer-e6e6a71e0343)\n  ([#26](https://github.com/google/ExoPlayer/issues/26)).\n* New time bar view, including support for displaying ad break markers.\n* Support DVB subtitles in MPEG-TS and MKV.\n* Support adaptive playback for audio only DASH, HLS and SmoothStreaming\n  ([#1975](https://github.com/google/ExoPlayer/issues/1975)).\n* Support for setting extractor flags on DefaultExtractorsFactory\n  ([#2657](https://github.com/google/ExoPlayer/issues/2657)).\n* Support injecting custom renderers into SimpleExoPlayer using a new\n  RenderersFactory interface.\n* Correctly set ExoPlayer's internal thread priority to `THREAD_PRIORITY_AUDIO`.\n* TX3G: Support styling and positioning.\n* FLV:\n  * Support MP3 in FLV.\n  * Skip unhandled metadata rather than failing\n    ([#2634](https://github.com/google/ExoPlayer/issues/2634)).\n  * Fix potential OutOfMemory errors.\n* ID3: Better handle malformed ID3 data\n  ([#2604](https://github.com/google/ExoPlayer/issues/2604),\n  [#2663](https://github.com/google/ExoPlayer/issues/2663)).\n* FFmpeg extension: Fixed build instructions\n  ([#2561](https://github.com/google/ExoPlayer/issues/2561)).\n* VP9 extension: Reduced binary size.\n* FLAC extension: Enabled 64 bit targets.\n* Misc bugfixes.\n\n### r2.3.1 (2017-03-23) ###\n\n* Fix NPE enabling WebVTT subtitles in DASH streams\n  ([#2596](https://github.com/google/ExoPlayer/issues/2596)).\n* Fix skipping to keyframes when MediaCodecVideoRenderer is enabled but without\n  a Surface ([#2575](https://github.com/google/ExoPlayer/issues/2575)).\n* Minor fix for CEA-708 decoder\n  ([#2595](https://github.com/google/ExoPlayer/issues/2595)).\n\n### r2.3.0 (2017-03-16) ###\n\n* GVR extension: Wraps the Google VR Audio SDK to provide spatial audio\n  rendering. You can read more about the GVR extension\n  [here](https://medium.com/google-exoplayer/spatial-audio-with-exoplayer-and-gvr-cecb00e9da5f#.xdjebjd7g).\n* DASH improvements:\n  * Support embedded CEA-608 closed captions\n    ([#2362](https://github.com/google/ExoPlayer/issues/2362)).\n  * Support embedded EMSG events\n    ([#2176](https://github.com/google/ExoPlayer/issues/2176)).\n  * Support mspr:pro manifest element\n    ([#2386](https://github.com/google/ExoPlayer/issues/2386)).\n  * Correct handling of empty segment indices at the start of live events\n    ([#1865](https://github.com/google/ExoPlayer/issues/1865)).\n* HLS improvements:\n  * Respect initial track selection\n    ([#2353](https://github.com/google/ExoPlayer/issues/2353)).\n  * Reduced frequency of media playlist requests when playback position is close\n    to the live edge ([#2548](https://github.com/google/ExoPlayer/issues/2548)).\n  * Exposed the master playlist through ExoPlayer.getCurrentManifest()\n    ([#2537](https://github.com/google/ExoPlayer/issues/2537)).\n  * Support CLOSED-CAPTIONS #EXT-X-MEDIA type\n    ([#341](https://github.com/google/ExoPlayer/issues/341)).\n  * Fixed handling of negative values in #EXT-X-SUPPORT\n    ([#2495](https://github.com/google/ExoPlayer/issues/2495)).\n  * Fixed potential endless buffering state for streams with WebVTT subtitles\n    ([#2424](https://github.com/google/ExoPlayer/issues/2424)).\n* MPEG-TS improvements:\n  * Support for multiple programs.\n  * Support for multiple closed captions and caption service descriptors\n   ([#2161](https://github.com/google/ExoPlayer/issues/2161)).\n* MP3: Add `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` extractor option to enable\n  constant bitrate seeking in MP3 files that would otherwise be unseekable\n  ([#2445](https://github.com/google/ExoPlayer/issues/2445)).\n* ID3: Better handle malformed ID3 data\n  ([#2486](https://github.com/google/ExoPlayer/issues/2486)).\n* Track selection: Added maxVideoBitrate parameter to DefaultTrackSelector.\n* DRM: Add support for CENC ClearKey on API level 21+\n  ([#2361](https://github.com/google/ExoPlayer/issues/2361)).\n* DRM: Support dynamic setting of key request headers\n  ([#1924](https://github.com/google/ExoPlayer/issues/1924)).\n* SmoothStreaming: Fixed handling of start_time placeholder\n  ([#2447](https://github.com/google/ExoPlayer/issues/2447)).\n* FLAC extension: Fix proguard configuration\n  ([#2427](https://github.com/google/ExoPlayer/issues/2427)).\n* Misc bugfixes.\n\n### r2.2.0 (2017-01-30) ###\n\n* Demo app: Automatic recovery from BehindLiveWindowException, plus improved\n  handling of pausing and resuming live streams\n  ([#2344](https://github.com/google/ExoPlayer/issues/2344)).\n* AndroidTV: Added Support for tunneled video playback\n  ([#1688](https://github.com/google/ExoPlayer/issues/1688)).\n* DRM: Renamed StreamingDrmSessionManager to DefaultDrmSessionManager and\n  added support for using offline licenses\n  ([#876](https://github.com/google/ExoPlayer/issues/876)).\n* DRM: Introduce OfflineLicenseHelper to help with offline license acquisition,\n  renewal and release.\n* UI: Updated player control assets. Added vector drawables for use on API level\n  21 and above.\n* UI: Made player control seek bar work correctly with key events if focusable\n  ([#2278](https://github.com/google/ExoPlayer/issues/2278)).\n* HLS: Improved support for streams that use EXT-X-DISCONTINUITY without\n  EXT-X-DISCONTINUITY-SEQUENCE\n  ([#1789](https://github.com/google/ExoPlayer/issues/1789)).\n* HLS: Support for EXT-X-START tag\n  ([#1544](https://github.com/google/ExoPlayer/issues/1544)).\n* HLS: Check #EXTM3U header is present when parsing the playlist. Fail\n  gracefully if not ([#2301](https://github.com/google/ExoPlayer/issues/2301)).\n* HLS: Fix memory leak\n  ([#2319](https://github.com/google/ExoPlayer/issues/2319)).\n* HLS: Fix non-seamless first adaptation where master playlist omits resolution\n  tags ([#2096](https://github.com/google/ExoPlayer/issues/2096)).\n* HLS: Fix handling of WebVTT subtitle renditions with non-standard segment file\n  extensions ([#2025](https://github.com/google/ExoPlayer/issues/2025) and\n  [#2355](https://github.com/google/ExoPlayer/issues/2355)).\n* HLS: Better handle inconsistent HLS playlist update\n  ([#2249](https://github.com/google/ExoPlayer/issues/2249)).\n* DASH: Don't overflow when dealing with large segment numbers\n  ([#2311](https://github.com/google/ExoPlayer/issues/2311)).\n* DASH: Fix propagation of language from the manifest\n  ([#2335](https://github.com/google/ExoPlayer/issues/2335)).\n* SmoothStreaming: Work around \"Offset to sample data was negative\" failures\n  ([#2292](https://github.com/google/ExoPlayer/issues/2292),\n  [#2101](https://github.com/google/ExoPlayer/issues/2101) and\n  [#1152](https://github.com/google/ExoPlayer/issues/1152)).\n* MP3/ID3: Added support for parsing Chapter and URL link frames\n  ([#2316](https://github.com/google/ExoPlayer/issues/2316)).\n* MP3/ID3: Handle ID3 frames that end with empty text field\n  ([#2309](https://github.com/google/ExoPlayer/issues/2309)).\n* Added ClippingMediaSource for playing clipped portions of media\n  ([#1988](https://github.com/google/ExoPlayer/issues/1988)).\n* Added convenience methods to query whether the current window is dynamic and\n  seekable ([#2320](https://github.com/google/ExoPlayer/issues/2320)).\n* Support setting of default headers on HttpDataSource.Factory implementations\n  ([#2166](https://github.com/google/ExoPlayer/issues/2166)).\n* Fixed cache failures when using an encrypted cache content index.\n* Fix visual artifacts when switching output surface\n  ([#2093](https://github.com/google/ExoPlayer/issues/2093)).\n* Fix gradle + proguard configurations.\n* Fix player position when replacing the MediaSource\n  ([#2369](https://github.com/google/ExoPlayer/issues/2369)).\n* Misc bug fixes, including\n  [#2330](https://github.com/google/ExoPlayer/issues/2330),\n  [#2269](https://github.com/google/ExoPlayer/issues/2269),\n  [#2252](https://github.com/google/ExoPlayer/issues/2252),\n  [#2264](https://github.com/google/ExoPlayer/issues/2264) and\n  [#2290](https://github.com/google/ExoPlayer/issues/2290).\n\n### r2.1.1 (2016-12-20) ###\n\n* Fix some subtitle types (e.g. WebVTT) being displayed out of sync\n  ([#2208](https://github.com/google/ExoPlayer/issues/2208)).\n* Fix incorrect position reporting for on-demand HLS media that includes\n  EXT-X-PROGRAM-DATE-TIME tags\n  ([#2224](https://github.com/google/ExoPlayer/issues/2224)).\n* Fix issue where playbacks could get stuck in the initial buffering state if\n  over 1MB of data needs to be read to initialize the playback.\n\n### r2.1.0 (2016-12-14) ###\n\n* HLS: Support for seeking in live streams\n  ([#87](https://github.com/google/ExoPlayer/issues/87)).\n* HLS: Improved support:\n  * Support for EXT-X-PROGRAM-DATE-TIME\n    ([#747](https://github.com/google/ExoPlayer/issues/747)).\n  * Improved handling of sample timestamps and their alignment across variants\n    and renditions.\n  * Fix issue that could cause playbacks to get stuck in an endless initial\n    buffering state.\n  * Correctly propagate BehindLiveWindowException instead of\n    IndexOutOfBoundsException exception\n    ([#1695](https://github.com/google/ExoPlayer/issues/1695)).\n* MP3/MP4: Support for ID3 metadata, including embedded album art\n  ([#979](https://github.com/google/ExoPlayer/issues/979)).\n* Improved customization of UI components. You can read about customization of\n  ExoPlayer's UI components\n  [here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi).\n* Robustness improvements when handling MediaSource timeline changes and\n  MediaPeriod transitions.\n* CEA-608: Support for caption styling and positioning.\n* MPEG-TS: Improved support:\n  * Support injection of custom TS payload readers.\n  * Support injection of custom section payload readers.\n  * Support SCTE-35 splice information messages.\n  * Support multiple table sections in a single PSI section.\n  * Fix NullPointerException when an unsupported stream type is encountered\n    ([#2149](https://github.com/google/ExoPlayer/issues/2149)).\n  * Avoid failure when expected ID3 header not found\n    ([#1966](https://github.com/google/ExoPlayer/issues/1966)).\n* Improvements to the upstream cache package.\n  * Support caching of media segments for DASH, HLS and SmoothStreaming. Note\n    that caching of manifest and playlist files is still not supported in the\n    (normal) case where the corresponding responses are compressed.\n  * Support caching for ExtractorMediaSource based playbacks.\n* Improved flexibility of SimpleExoPlayer\n  ([#2102](https://github.com/google/ExoPlayer/issues/2102)).\n* Fix issue where only the audio of a video would play due to capability\n  detection issues ([#2007](https://github.com/google/ExoPlayer/issues/2007),\n  [#2034](https://github.com/google/ExoPlayer/issues/2034) and\n  [#2157](https://github.com/google/ExoPlayer/issues/2157)).\n* Fix issues that could cause ExtractorMediaSource based playbacks to get stuck\n  buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)).\n* Correctly set SimpleExoPlayerView surface aspect ratio when an active player\n  is attached ([#2077](https://github.com/google/ExoPlayer/issues/2077)).\n* OGG: Fix playback of short OGG files\n  ([#1976](https://github.com/google/ExoPlayer/issues/1976)).\n* MP4: Support `.mp3` tracks\n  ([#2066](https://github.com/google/ExoPlayer/issues/2066)).\n* SubRip: Don't fail playbacks if SubRip file contains negative timestamps\n  ([#2145](https://github.com/google/ExoPlayer/issues/2145)).\n* Misc bugfixes.\n\n### r2.0.4 (2016-10-20) ###\n\n* Fix crash on Jellybean devices when using playback controls\n  ([#1965](https://github.com/google/ExoPlayer/issues/1965)).\n\n### r2.0.3 (2016-10-17) ###\n\n* Fixed NullPointerException in ExtractorMediaSource\n  ([#1914](https://github.com/google/ExoPlayer/issues/1914)).\n* Fixed NullPointerException in HlsMediaPeriod\n  ([#1907](https://github.com/google/ExoPlayer/issues/1907)).\n* Fixed memory leak in PlaybackControlView\n  ([#1908](https://github.com/google/ExoPlayer/issues/1908)).\n* Fixed strict mode violation when using\n  SimpleExoPlayer.setVideoPlayerTextureView().\n* Fixed L3 Widevine provisioning\n  ([#1925](https://github.com/google/ExoPlayer/issues/1925)).\n* Fixed hiding of controls with use_controller=\"false\"\n  ([#1919](https://github.com/google/ExoPlayer/issues/1919)).\n* Improvements to Cronet network stack extension.\n* Misc bug fixes.\n\n### r2.0.2 (2016-10-06) ###\n\n* Fixes for MergingMediaSource and sideloaded subtitles.\n  ([#1882](https://github.com/google/ExoPlayer/issues/1882),\n  [#1854](https://github.com/google/ExoPlayer/issues/1854),\n  [#1900](https://github.com/google/ExoPlayer/issues/1900)).\n* Reduced effect of application code leaking player references\n  ([#1855](https://github.com/google/ExoPlayer/issues/1855)).\n* Initial support for fragmented MP4 in HLS.\n* Misc bug fixes and minor features.\n\n### r2.0.1 (2016-09-30) ###\n\n* Fix playback of short duration content\n  ([#1837](https://github.com/google/ExoPlayer/issues/1837)).\n* Fix MergingMediaSource preparation issue\n  ([#1853](https://github.com/google/ExoPlayer/issues/1853)).\n* Fix live stream buffering (out of memory) issue\n  ([#1825](https://github.com/google/ExoPlayer/issues/1825)).\n\n### r2.0.0 (2016-09-14) ###\n\nExoPlayer 2.x is a major iteration of the library. It includes significant API\nand architectural changes, new features and many bug fixes. You can read about\nsome of the motivations behind ExoPlayer 2.x\n[here](https://medium.com/google-exoplayer/exoplayer-2-x-why-what-and-when-74fd9cb139#.am7h8nytm).\n\n* Root package name changed to `com.google.android.exoplayer2`. The library\n  structure and class names have also been sanitized. Read more\n  [here](https://medium.com/google-exoplayer/exoplayer-2-x-new-package-and-class-names-ef8e1d9ba96f#.lv8sd4nez).\n* Key architectural changes:\n  * Late binding between rendering and media source components. Allows the same\n    rendering components to be re-used from one playback to another. Enables\n    features such as gapless playback through playlists and DASH multi-period\n    support.\n  * Improved track selection design. More details can be found\n    [here](https://medium.com/google-exoplayer/exoplayer-2-x-track-selection-2b62ff712cc9#.n00zo76b6).\n  * LoadControl now used to control buffering and loading across all playback\n    types.\n  * Media source components given additional structure. A new MediaSource class\n    has been introduced. MediaSources expose Timelines that describe the media\n    they expose, and can consist of multiple MediaPeriods. This enables features\n    such as seeking in live playbacks and DASH multi-period support.\n  * Responsibility for loading the initial DASH/SmoothStreaming/HLS manifest is\n    promoted to the corresponding MediaSource components and is no longer the\n    application's responsibility.\n  * Higher level abstractions such as SimpleExoPlayer have been added to the\n    library. These make the library easier to use for common use cases. The demo\n    app is halved in size as a result, whilst at the same time gaining more\n    functionality. Read more\n    [here](https://medium.com/google-exoplayer/exoplayer-2-x-improved-demo-app-d97171aaaaa1).\n  * Enhanced library support for implementing audio extensions. Read more\n    [here](https://medium.com/google-exoplayer/exoplayer-2-x-new-audio-features-cfb26c2883a#.ua75vu4s3).\n  * Format and MediaFormat are replaced by a single Format class.\n* Key new features:\n  * Playlist support. Includes support for gapless playback between playlist\n    items and consistent application of LoadControl and TrackSelector policies\n    when transitioning between items\n    ([#1270](https://github.com/google/ExoPlayer/issues/1270)).\n  * Seeking in live playbacks for DASH and SmoothStreaming\n    ([#291](https://github.com/google/ExoPlayer/issues/291)).\n  * DASH multi-period support\n    ([#557](https://github.com/google/ExoPlayer/issues/557)).\n  * MediaSource composition allows MediaSources to be concatenated into a\n    playlist, merged and looped. Read more\n    [here](https://medium.com/google-exoplayer/exoplayer-2-x-mediasource-composition-6c285fcbca1f#.zfha8qupz).\n  * Looping support (see above)\n    ([#490](https://github.com/google/ExoPlayer/issues/490)).\n  * Ability to query information about all tracks in a piece of media (including\n    those not supported by the device)\n    ([#1121](https://github.com/google/ExoPlayer/issues/1121)).\n  * Improved player controls.\n  * Support for PSSH in fMP4 moof atoms\n    ([#1143](https://github.com/google/ExoPlayer/issues/1143)).\n  * Support for Opus in Ogg\n    ([#1447](https://github.com/google/ExoPlayer/issues/1447)).\n  * CacheDataSource support for standalone media file playbacks (mp3, mp4 etc).\n  * FFMPEG extension (for audio only).\n* Key bug fixes:\n  * Removed unnecessary secondary requests when playing standalone media files\n    ([#1041](https://github.com/google/ExoPlayer/issues/1041)).\n  * Fixed playback of video only (i.e. no audio) live streams\n    ([#758](https://github.com/google/ExoPlayer/issues/758)).\n  * Fixed silent failure when media buffer is too small\n    ([#583](https://github.com/google/ExoPlayer/issues/583)).\n  * Suppressed \"Sending message to a Handler on a dead thread\" warnings\n    ([#426](https://github.com/google/ExoPlayer/issues/426)).\n\n# Legacy release notes #\n\nNote: Since ExoPlayer V1 is still being maintained alongside V2, there is some\noverlap between these notes and the notes above. r2.0.0 followed from r1.5.11,\nand hence it can be assumed that all changes in r1.5.11 and earlier are included\nin all V2 releases. This cannot be assumed for changes in r1.5.12 and later,\nhowever it can be assumed that all such changes are included in the most recent\nV2 release.\n\n### r1.5.16 ###\n\n* VP9 extension: Reduced binary size.\n* FLAC extension: Enabled 64 bit targets and fixed proguard config.\n* Misc bugfixes.\n\n### r1.5.15 ###\n\n* SmoothStreaming: Fixed handling of start_time placeholder\n  ([#2447](https://github.com/google/ExoPlayer/issues/2447)).\n* Misc bugfixes.\n\n### r1.5.14 ###\n\n* Fixed cache failures when using an encrypted cache content index.\n* SmoothStreaming: Work around \"Offset to sample data was negative\" failures\n  ([#2292](https://github.com/google/ExoPlayer/issues/2292),\n  [#2101](https://github.com/google/ExoPlayer/issues/2101) and\n  [#1152](https://github.com/google/ExoPlayer/issues/1152)).\n\n### r1.5.13 ###\n\n* Improvements to the upstream cache package.\n* MP4: Support `.mp3` tracks\n  ([#2066](https://github.com/google/ExoPlayer/issues/2066)).\n* SubRip: Don't fail playbacks if SubRip file contains negative timestamps\n  ([#2145](https://github.com/google/ExoPlayer/issues/2145)).\n* MPEG-TS: Avoid failure when expected ID3 header not found\n  ([#1966](https://github.com/google/ExoPlayer/issues/1966)).\n* Misc bugfixes.\n\n### r1.5.12 ###\n\n* Improvements to Cronet network stack extension.\n* Fix bug in demo app introduced in r1.5.11 that caused L3 Widevine\n  provisioning requests to fail.\n* Misc bugfixes.\n\n### r1.5.11 ###\n\n* Cronet network stack extension.\n* HLS: Fix propagation of language for alternative audio renditions\n  ([#1784](https://github.com/google/ExoPlayer/issues/1784)).\n* WebM: Support for subsample encryption.\n* ID3: Fix EOS detection for 2-byte encodings\n  ([#1774](https://github.com/google/ExoPlayer/issues/1774)).\n* MPEG-TS: Support multiple tracks of the same type.\n* MPEG-TS: Work toward robust handling of stream corruption.\n* Fix ContentDataSource failures triggered by garbage collector\n  ([#1759](https://github.com/google/ExoPlayer/issues/1759)).\n\n### r1.5.10 ###\n\n* HLS: Stability fixes.\n* MP4: Support for stz2 Atoms.\n* Enable 4K format selection on Sony AndroidTV + nVidia SHIELD.\n* TX3G caption fixes.\n\n### r1.5.9 ###\n\n* MP4: Fixed incorrect sniffing in some cases (#1523).\n* MP4: Improved file compatibility (#1567).\n* ID3: Support for TIT2 and APIC frames.\n* Fixed querying of platform decoders on some devices.\n* Misc bug fixes.\n\n### r1.5.8 ###\n\n* HLS: Fix handling of HTTP redirects.\n* Audio: Minor adjustment to improve A/V sync.\n* OGG: Support FLAC in OGG.\n* TTML: Support regions.\n* WAV/PCM: Support 8, 24 and 32-bit WAV and PCM audio.\n* Misc bug fixes and performance optimizations.\n\n### r1.5.7 ###\n\n* OGG: Support added for OGG.\n* FLAC: Support for FLAC extraction and playback (via an extension).\n* HLS: Multiple audio track support (via Renditions).\n* FMP4: Support multiple tracks in fragmented MP4 (not applicable to\n  DASH/SmoothStreaming).\n* WAV: Support for 16-bit WAV files.\n* MKV: Support non-square pixel formats.\n* Misc bug fixes.\n\n### r1.5.6 ###\n\n* MP3: Fix mono streams playing at 2x speed on some MediaTek based devices\n  (#801).\n* MP3: Fix playback of some streams when stream length is unknown.\n* ID3: Support multiple frames of the same type in a single tag.\n* CEA-608: Correctly handle repeated control characters, fixing an issue in\n  which captions would immediately disappear.\n* AVC3: Fix decoder failures on some MediaTek devices in the case where the\n  first buffer fed to the decoder does not start with SPS/PPS NAL units.\n* Misc bug fixes.\n\n### r1.5.5 ###\n\n* DASH: Enable MP4 embedded WebVTT playback (#1185)\n* HLS: Fix handling of extended ID3 tags in MPEG-TS (#1181)\n* MP3: Fix incorrect position calculation in VBRI header (#1197)\n* Fix issue seeking backward using SingleSampleSource (#1193)\n\n### r1.5.4 ###\n\n* HLS: Support for variant selection and WebVtt subtitles.\n* MP4: Support for embedded WebVtt.\n* Improved device compatibility.\n* Fix for resource leak (Issue #1066).\n* Misc bug fixes + minor features.\n\n### r1.5.3 ###\n\n* Support for FLV (without seeking).\n* MP4: Fix for playback of media containing basic edit lists.\n* QuickTime: Fix parsing of QuickTime style audio sample entry.\n* HLS: Add H262 support for devices that have an H262 decoder.\n* Allow AudioTrack PlaybackParams (e.g. speed/pitch) on API level 23+.\n* Correctly detect 4K displays on API level 23+.\n* Misc bug fixes.\n\n### r1.5.2 ###\n\n* MPEG-TS/HLS: Fix frame drops playing H265 video.\n* SmoothStreaming: Fix parsing of ProtectionHeader.\n\n### r1.5.1 ###\n\n* Enable smooth frame release by default.\n* Added OkHttpDataSource extension.\n* AndroidTV: Correctly detect 4K display size on Bravia devices.\n* FMP4: Handle non-sample data in mdat boxes.\n* TTML: Fix parsing of some colors on Jellybean.\n* SmoothStreaming: Ignore tfdt boxes.\n* Misc bug fixes.\n\n### r1.5.0 ###\n\n* Multi-track support.\n* DASH: Limited support for multi-period manifests.\n* HLS: Smoother format adaptation.\n* HLS: Support for MP3 media segments.\n* TTML: Support for most embedded TTML styling.\n* WebVTT: Enhanced positioning support.\n* Initial playback tests.\n* Misc bug fixes.\n\n### r1.4.2 ###\n\n* Implemented automatic format detection for regular container formats.\n* Added UdpDataSource for connecting to multicast streams.\n* Improved robustness for MP4 playbacks.\n* Misc bug fixes.\n\n### r1.4.1 ###\n\n* HLS: Fix premature playback failures that could occur in some cases.\n\n### r1.4.0 ###\n\n* Support for extracting Matroska streams (implemented by WebmExtractor).\n* Support for tx3g captions in MP4 streams.\n* Support for H.265 in MPEG-TS streams on supported devices.\n* HLS: Added support for MPEG audio (e.g. MP3) in TS media segments.\n* HLS: Improved robustness against missing chunks and variants.\n* MP4: Added support for embedded MPEG audio (e.g. MP3).\n* TTML: Improved handling of whitespace.\n* DASH: Support Mpd.Location element.\n* Add option to TsExtractor to allow non-IDR keyframes.\n* Added MulticastDataSource for connecting to multicast streams.\n* (WorkInProgress) - First steps to supporting seeking in DASH DVR window.\n* (WorkInProgress) - First steps to supporting styled + positioned subtitles.\n* Misc bug fixes.\n\n### r1.3.3 ###\n\n* HLS: Fix failure when playing HLS AAC streams.\n* Misc bug fixes.\n\n### r1.3.2 ###\n\n* DataSource improvements: `DefaultUriDataSource` now handles http://, https://,\n  file://, asset:// and content:// URIs automatically. It also handles\n  file:///android_asset/* URIs, and file paths like /path/to/media.mp4 where the\n  scheme is omitted.\n* HLS: Fix for some ID3 events being dropped.\n* HLS: Correctly handle 0x0 and floating point RESOLUTION tags.\n* Mp3Extractor: robustness improvements.\n\n### r1.3.1 ###\n\n* No notes provided.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/build.gradle",
    "content": "// Copyright (C) 2016 The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\nbuildscript {\n    repositories {\n        google()\n        jcenter()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:3.5.1'\n        classpath 'com.novoda:bintray-release:0.9.1'\n        classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0'\n    }\n}\nallprojects {\n    repositories {\n        google()\n        jcenter()\n    }\n    project.ext {\n        exoplayerPublishEnabled = false\n    }\n    if (it.hasProperty('externalBuildDir')) {\n        if (!new File(externalBuildDir).isAbsolute()) {\n            externalBuildDir = new File(rootDir, externalBuildDir)\n        }\n        buildDir = \"${externalBuildDir}/${project.name}\"\n    }\n    group = 'com.google.android.exoplayer'\n}\n\napply from: 'javadoc_combined.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/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.10.6'\n    releaseVersionCode = 2010006\n    // MOD: fix unmatched minSdk between exo and sharedutils\n    // minSdkVersion = 16\n    // targetSdkVersion = 28\n    // compileSdkVersion = 28\n    minSdkVersion = project.properties.minSdkVersion\n    targetSdkVersion = project.properties.targetSdkVersion\n    compileSdkVersion = project.properties.compileSdkVersion\n    // dexmakerVersion = '2.21.0' // absent in mavenCentral\n    dexmakerVersion = '2.28.3'\n    mockitoVersion = '2.25.0'\n    // MOD: use versions from sharedutils\n    // robolectricVersion = '4.2'\n    robolectricVersion = project.properties.robolectricVersion\n    annotationXVersion = project.properties.annotationXVersion\n    autoValueVersion = '1.6'\n    checkerframeworkVersion = '2.5.0'\n    // MOD: use versions from sharedutils\n    // androidXTestVersion = '1.1.0'\n    //protobufVersion = '4.32.1'\n    protobufVersion = '3.17.3' // the latest for Android 4\n    androidXTestVersion = project.properties.testXSupportVersion\n    modulePrefix = ':'\n    if (gradle.ext.has('exoplayerModulePrefix')) {\n        modulePrefix += gradle.ext.exoplayerModulePrefix\n    }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/core_settings.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.\ndef rootDir = gradle.ext.exoplayerRoot\ndef modulePrefix = ':'\nif (gradle.ext.has('exoplayerModulePrefix')) {\n    modulePrefix += gradle.ext.exoplayerModulePrefix\n}\n\ninclude modulePrefix + 'library'\ninclude modulePrefix + 'library-core'\ninclude modulePrefix + 'library-dash'\ninclude modulePrefix + 'library-sabr'\ninclude modulePrefix + 'library-hls'\ninclude modulePrefix + 'library-smoothstreaming'\ninclude modulePrefix + 'library-ui'\ninclude modulePrefix + 'testutils'\ninclude modulePrefix + 'testutils-robolectric'\ninclude modulePrefix + 'extension-ffmpeg'\ninclude modulePrefix + 'extension-flac'\n// include modulePrefix + 'extension-gvr' // absent in mavenCentral\n// include modulePrefix + 'extension-ima' // absent in mavenCentral\ninclude modulePrefix + 'extension-cast'\ninclude modulePrefix + 'extension-cronet'\ninclude modulePrefix + 'extension-mediasession'\ninclude modulePrefix + 'extension-okhttp'\ninclude modulePrefix + 'extension-opus'\ninclude modulePrefix + 'extension-vp9'\n// include modulePrefix + 'extension-rtmp' // absent in mavenCentral\ninclude modulePrefix + 'extension-leanback'\n// include modulePrefix + 'extension-jobdispatcher' // absent in mavenCentral\ninclude modulePrefix + 'extension-workmanager'\n\nproject(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')\nproject(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')\nproject(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash')\nproject(modulePrefix + 'library-sabr').projectDir = new File(rootDir, 'library/sabr')\nproject(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')\nproject(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')\nproject(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')\nproject(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')\nproject(modulePrefix + 'testutils-robolectric').projectDir = new File(rootDir, 'testutils_robolectric')\nproject(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')\nproject(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')\n// project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') // absent in mavenCentral\n// project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima') // absent in mavenCentral\nproject(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')\nproject(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')\nproject(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')\nproject(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')\nproject(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')\nproject(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')\n// project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')\nproject(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')\n// project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher') // absent in mavenCentral\nproject(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager')\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/README.md",
    "content": "# ExoPlayer demos #\n\nThis directory contains applications that demonstrate how to use ExoPlayer.\nBrowse the individual demos and their READMEs to learn more.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/README.md",
    "content": "# Cast demo application #\n\nThis folder contains a demo application that showcases ExoPlayer integration\nwith Google Cast.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/build.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.\napply from: '../../constants.gradle'\napply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        versionName project.ext.releaseVersion\n        versionCode project.ext.releaseVersionCode\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    buildTypes {\n        release {\n            shrinkResources true\n            minifyEnabled true\n            proguardFiles = [\n                \"proguard-rules.txt\",\n                getDefaultProguardFile('proguard-android.txt')\n            ]\n        }\n        debug {\n            jniDebuggable = true\n        }\n    }\n\n    lintOptions {\n        // The demo app isn't indexed and doesn't have translations.\n        disable 'GoogleAppIndexingWarning','MissingTranslation'\n    }\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    implementation project(modulePrefix + 'library-dash')\n    implementation project(modulePrefix + 'library-hls')\n    implementation project(modulePrefix + 'library-smoothstreaming')\n    implementation project(modulePrefix + 'library-ui')\n    implementation project(modulePrefix + 'extension-cast')\n    implementation 'com.google.android.material:material:1.0.0'\n    implementation 'androidx.legacy:legacy-support-v4:1.0.0'\n    implementation 'androidx.appcompat:appcompat:1.0.2'\n    implementation 'androidx.recyclerview:recyclerview:1.0.0'\n}\n\napply plugin: 'com.google.android.gms.strict-version-matcher-plugin'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/proguard-rules.txt",
    "content": "# Proguard rules specific to the Cast demo app.\n\n# Accessed via menu.xml\n-keep class androidx.mediarouter.app.MediaRouteActionProvider {\n  *;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.google.android.exoplayer2.castdemo\">\n\n  <uses-permission android:name=\"android.permission.INTERNET\"/>\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n\n  <uses-sdk/>\n\n  <application android:label=\"@string/application_name\" android:icon=\"@mipmap/ic_launcher\"\n      android:largeHeap=\"true\" android:allowBackup=\"false\">\n\n    <meta-data android:name=\"com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME\"\n        android:value=\"com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider\"/>\n\n    <activity android:name=\"com.google.android.exoplayer2.castdemo.MainActivity\"\n        android:configChanges=\"keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode\"\n        android:launchMode=\"singleTop\" android:label=\"@string/application_name\"\n        android:theme=\"@style/Theme.AppCompat\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\"/>\n        <category android:name=\"android.intent.category.LAUNCHER\"/>\n      </intent-filter>\n    </activity>\n\n  </application>\n\n</manifest>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.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.castdemo;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport android.view.KeyEvent;\nimport android.view.View;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.DefaultRenderersFactory;\nimport com.google.android.exoplayer2.ExoPlayerFactory;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Player.DiscontinuityReason;\nimport com.google.android.exoplayer2.Player.EventListener;\nimport com.google.android.exoplayer2.Player.TimelineChangeReason;\nimport com.google.android.exoplayer2.RenderersFactory;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.Timeline.Period;\nimport com.google.android.exoplayer2.ext.cast.CastPlayer;\nimport com.google.android.exoplayer2.ext.cast.MediaItem;\nimport com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;\nimport com.google.android.exoplayer2.source.ConcatenatingMediaSource;\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.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.ui.PlayerControlView;\nimport com.google.android.exoplayer2.ui.PlayerView;\nimport com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;\nimport com.google.android.gms.cast.MediaInfo;\nimport com.google.android.gms.cast.MediaMetadata;\nimport com.google.android.gms.cast.MediaQueueItem;\nimport com.google.android.gms.cast.framework.CastContext;\nimport java.util.ArrayList;\n\n/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */\n/* package */ class DefaultReceiverPlayerManager\n    implements PlayerManager, EventListener, SessionAvailabilityListener {\n\n  private static final String USER_AGENT = \"ExoCastDemoPlayer\";\n  private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =\n      new DefaultHttpDataSourceFactory(USER_AGENT);\n\n  private final PlayerView localPlayerView;\n  private final PlayerControlView castControlView;\n  private final SimpleExoPlayer exoPlayer;\n  private final CastPlayer castPlayer;\n  private final ArrayList<MediaItem> mediaQueue;\n  private final Listener listener;\n  private final ConcatenatingMediaSource concatenatingMediaSource;\n\n  private int currentItemIndex;\n  private Player currentPlayer;\n\n  /**\n   * Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}.\n   *\n   * @param listener A {@link Listener} for queue position changes.\n   * @param localPlayerView The {@link PlayerView} for local playback.\n   * @param castControlView The {@link PlayerControlView} to control remote playback.\n   * @param context A {@link Context}.\n   * @param castContext The {@link CastContext}.\n   */\n  public DefaultReceiverPlayerManager(\n      Listener listener,\n      PlayerView localPlayerView,\n      PlayerControlView castControlView,\n      Context context,\n      CastContext castContext) {\n    this.listener = listener;\n    this.localPlayerView = localPlayerView;\n    this.castControlView = castControlView;\n    mediaQueue = new ArrayList<>();\n    currentItemIndex = C.INDEX_UNSET;\n    concatenatingMediaSource = new ConcatenatingMediaSource();\n\n    DefaultTrackSelector trackSelector = new DefaultTrackSelector();\n    RenderersFactory renderersFactory = new DefaultRenderersFactory(context);\n    exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);\n    exoPlayer.addListener(this);\n    localPlayerView.setPlayer(exoPlayer);\n\n    castPlayer = new CastPlayer(castContext);\n    castPlayer.addListener(this);\n    castPlayer.setSessionAvailabilityListener(this);\n    castControlView.setPlayer(castPlayer);\n\n    setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);\n  }\n\n  // Queue manipulation methods.\n\n  /**\n   * Plays a specified queue item in the current player.\n   *\n   * @param itemIndex The index of the item to play.\n   */\n  @Override\n  public void selectQueueItem(int itemIndex) {\n    setCurrentItem(itemIndex, C.TIME_UNSET, true);\n  }\n\n  /** Returns the index of the currently played item. */\n  @Override\n  public int getCurrentItemIndex() {\n    return currentItemIndex;\n  }\n\n  /**\n   * Appends {@code item} to the media queue.\n   *\n   * @param item The {@link MediaItem} to append.\n   */\n  @Override\n  public void addItem(MediaItem item) {\n    mediaQueue.add(item);\n    concatenatingMediaSource.addMediaSource(buildMediaSource(item));\n    if (currentPlayer == castPlayer) {\n      castPlayer.addItems(buildMediaQueueItem(item));\n    }\n  }\n\n  /** Returns the size of the media queue. */\n  @Override\n  public int getMediaQueueSize() {\n    return mediaQueue.size();\n  }\n\n  /**\n   * Returns the item at the given index in the media queue.\n   *\n   * @param position The index of the item.\n   * @return The item at the given index in the media queue.\n   */\n  @Override\n  public MediaItem getItem(int position) {\n    return mediaQueue.get(position);\n  }\n\n  /**\n   * Removes the item at the given index from the media queue.\n   *\n   * @param item The item to remove.\n   * @return Whether the removal was successful.\n   */\n  @Override\n  public boolean removeItem(MediaItem item) {\n    int itemIndex = mediaQueue.indexOf(item);\n    if (itemIndex == -1) {\n      return false;\n    }\n    concatenatingMediaSource.removeMediaSource(itemIndex);\n    if (currentPlayer == castPlayer) {\n      if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {\n        Timeline castTimeline = castPlayer.getCurrentTimeline();\n        if (castTimeline.getPeriodCount() <= itemIndex) {\n          return false;\n        }\n        castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);\n      }\n    }\n    mediaQueue.remove(itemIndex);\n    if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {\n      maybeSetCurrentItemAndNotify(C.INDEX_UNSET);\n    } else if (itemIndex < currentItemIndex) {\n      maybeSetCurrentItemAndNotify(currentItemIndex - 1);\n    }\n    return true;\n  }\n\n  /**\n   * Moves an item within the queue.\n   *\n   * @param item The item to move.\n   * @param toIndex The target index of the item in the queue.\n   * @return Whether the item move was successful.\n   */\n  @Override\n  public boolean moveItem(MediaItem item, int toIndex) {\n    int fromIndex = mediaQueue.indexOf(item);\n    if (fromIndex == -1) {\n      return false;\n    }\n    // Player update.\n    concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);\n    if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {\n      Timeline castTimeline = castPlayer.getCurrentTimeline();\n      int periodCount = castTimeline.getPeriodCount();\n      if (periodCount <= fromIndex || periodCount <= toIndex) {\n        return false;\n      }\n      int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;\n      castPlayer.moveItem(elementId, toIndex);\n    }\n\n    mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));\n\n    // Index update.\n    if (fromIndex == currentItemIndex) {\n      maybeSetCurrentItemAndNotify(toIndex);\n    } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {\n      maybeSetCurrentItemAndNotify(currentItemIndex - 1);\n    } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {\n      maybeSetCurrentItemAndNotify(currentItemIndex + 1);\n    }\n\n    return true;\n  }\n\n  /**\n   * Dispatches a given {@link KeyEvent} to the corresponding view of the current player.\n   *\n   * @param event The {@link KeyEvent}.\n   * @return Whether the event was handled by the target view.\n   */\n  @Override\n  public boolean dispatchKeyEvent(KeyEvent event) {\n    if (currentPlayer == exoPlayer) {\n      return localPlayerView.dispatchKeyEvent(event);\n    } else /* currentPlayer == castPlayer */ {\n      return castControlView.dispatchKeyEvent(event);\n    }\n  }\n\n  /** Releases the manager and the players that it holds. */\n  @Override\n  public void release() {\n    currentItemIndex = C.INDEX_UNSET;\n    mediaQueue.clear();\n    concatenatingMediaSource.clear();\n    castPlayer.setSessionAvailabilityListener(null);\n    castPlayer.release();\n    localPlayerView.setPlayer(null);\n    exoPlayer.release();\n  }\n\n  // Player.EventListener implementation.\n\n  @Override\n  public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n    updateCurrentItemIndex();\n  }\n\n  @Override\n  public void onPositionDiscontinuity(@DiscontinuityReason int reason) {\n    updateCurrentItemIndex();\n  }\n\n  @Override\n  public void onTimelineChanged(\n      Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {\n    updateCurrentItemIndex();\n  }\n\n  // CastPlayer.SessionAvailabilityListener implementation.\n\n  @Override\n  public void onCastSessionAvailable() {\n    setCurrentPlayer(castPlayer);\n  }\n\n  @Override\n  public void onCastSessionUnavailable() {\n    setCurrentPlayer(exoPlayer);\n  }\n\n  // Internal methods.\n\n  private void updateCurrentItemIndex() {\n    int playbackState = currentPlayer.getPlaybackState();\n    maybeSetCurrentItemAndNotify(\n        playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED\n            ? currentPlayer.getCurrentWindowIndex()\n            : C.INDEX_UNSET);\n  }\n\n  private void setCurrentPlayer(Player currentPlayer) {\n    if (this.currentPlayer == currentPlayer) {\n      return;\n    }\n\n    // View management.\n    if (currentPlayer == exoPlayer) {\n      localPlayerView.setVisibility(View.VISIBLE);\n      castControlView.hide();\n    } else /* currentPlayer == castPlayer */ {\n      localPlayerView.setVisibility(View.GONE);\n      castControlView.show();\n    }\n\n    // Player state management.\n    long playbackPositionMs = C.TIME_UNSET;\n    int windowIndex = C.INDEX_UNSET;\n    boolean playWhenReady = false;\n    if (this.currentPlayer != null) {\n      int playbackState = this.currentPlayer.getPlaybackState();\n      if (playbackState != Player.STATE_ENDED) {\n        playbackPositionMs = this.currentPlayer.getCurrentPosition();\n        playWhenReady = this.currentPlayer.getPlayWhenReady();\n        windowIndex = this.currentPlayer.getCurrentWindowIndex();\n        if (windowIndex != currentItemIndex) {\n          playbackPositionMs = C.TIME_UNSET;\n          windowIndex = currentItemIndex;\n        }\n      }\n      this.currentPlayer.stop(true);\n    } else {\n      // This is the initial setup. No need to save any state.\n    }\n\n    this.currentPlayer = currentPlayer;\n\n    // Media queue management.\n    if (currentPlayer == exoPlayer) {\n      exoPlayer.prepare(concatenatingMediaSource);\n    }\n\n    // Playback transition.\n    if (windowIndex != C.INDEX_UNSET) {\n      setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);\n    }\n  }\n\n  /**\n   * Starts playback of the item at the given position.\n   *\n   * @param itemIndex The index of the item to play.\n   * @param positionMs The position at which playback should start.\n   * @param playWhenReady Whether the player should proceed when ready to do so.\n   */\n  private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {\n    maybeSetCurrentItemAndNotify(itemIndex);\n    if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {\n      MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];\n      for (int i = 0; i < items.length; i++) {\n        items[i] = buildMediaQueueItem(mediaQueue.get(i));\n      }\n      castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);\n    } else {\n      currentPlayer.seekTo(itemIndex, positionMs);\n      currentPlayer.setPlayWhenReady(playWhenReady);\n    }\n  }\n\n  private void maybeSetCurrentItemAndNotify(int currentItemIndex) {\n    if (this.currentItemIndex != currentItemIndex) {\n      int oldIndex = this.currentItemIndex;\n      this.currentItemIndex = currentItemIndex;\n      listener.onQueuePositionChanged(oldIndex, currentItemIndex);\n    }\n  }\n\n  private static MediaSource buildMediaSource(MediaItem item) {\n    Uri uri = item.media.uri;\n    switch (item.mimeType) {\n      case DemoUtil.MIME_TYPE_SS:\n        return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);\n      case DemoUtil.MIME_TYPE_DASH:\n        return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);\n      case DemoUtil.MIME_TYPE_HLS:\n        return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);\n      case DemoUtil.MIME_TYPE_VIDEO_MP4:\n        return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);\n      default:\n        {\n          throw new IllegalStateException(\"Unsupported type: \" + item.mimeType);\n        }\n    }\n  }\n\n  private static MediaQueueItem buildMediaQueueItem(MediaItem item) {\n    MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);\n    movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title);\n    MediaInfo mediaInfo =\n        new MediaInfo.Builder(item.media.uri.toString())\n            .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)\n            .setContentType(item.mimeType)\n            .setMetadata(movieMetadata)\n            .build();\n    return new MediaQueueItem.Builder(mediaInfo).build();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.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.castdemo;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.UUID;\n\n/** Utility methods and constants for the Cast demo application. */\n/* package */ final class DemoUtil {\n\n  /** Represents a media sample. */\n  public static final class Sample {\n\n    /** The uri of the media content. */\n    public final String uri;\n    /** The name of the sample. */\n    public final String name;\n    /** The mime type of the sample media content. */\n    public final String mimeType;\n    /**\n     * The {@link UUID} of the DRM scheme that protects the content, or null if the content is not\n     * DRM-protected.\n     */\n    @Nullable public final UUID drmSchemeUuid;\n    /**\n     * The url from which players should obtain DRM licenses, or null if the content is not\n     * DRM-protected.\n     */\n    @Nullable public final Uri licenseServerUri;\n\n    /**\n     * @param uri See {@link #uri}.\n     * @param name See {@link #name}.\n     * @param mimeType See {@link #mimeType}.\n     */\n    public Sample(String uri, String name, String mimeType) {\n      this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null);\n    }\n\n    public Sample(\n        String uri,\n        String name,\n        String mimeType,\n        @Nullable UUID drmSchemeUuid,\n        @Nullable String licenseServerUriString) {\n      this.uri = uri;\n      this.name = name;\n      this.mimeType = mimeType;\n      this.drmSchemeUuid = drmSchemeUuid;\n      this.licenseServerUri =\n          licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null;\n    }\n\n    @Override\n    public String toString() {\n      return name;\n    }\n  }\n\n  public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;\n  public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;\n  public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;\n  public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;\n\n  /** The list of samples available in the cast demo app. */\n  public static final List<Sample> SAMPLES;\n\n  static {\n    // App samples.\n    ArrayList<Sample> samples = new ArrayList<>();\n\n    // Clear content.\n    samples.add(\n        new Sample(\n            \"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd\",\n            \"Clear DASH: Tears\",\n            MIME_TYPE_DASH));\n    samples.add(\n        new Sample(\n            \"https://html5demos.com/assets/dizzy.mp4\", \"Clear MP4: Dizzy\", MIME_TYPE_VIDEO_MP4));\n\n    SAMPLES = Collections.unmodifiableList(samples);\n  }\n\n  private DemoUtil() {}\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.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.castdemo;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport androidx.core.graphics.ColorUtils;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.RecyclerView.ViewHolder;\nimport androidx.recyclerview.widget.ItemTouchHelper;\nimport android.view.KeyEvent;\nimport android.view.LayoutInflater;\nimport android.view.Menu;\nimport android.view.View;\nimport android.view.View.OnClickListener;\nimport android.view.ViewGroup;\nimport android.widget.ArrayAdapter;\nimport android.widget.ListView;\nimport android.widget.TextView;\nimport android.widget.Toast;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.ext.cast.MediaItem;\nimport com.google.android.exoplayer2.ui.PlayerControlView;\nimport com.google.android.exoplayer2.ui.PlayerView;\nimport com.google.android.gms.cast.CastMediaControlIntent;\nimport com.google.android.gms.cast.framework.CastButtonFactory;\nimport com.google.android.gms.cast.framework.CastContext;\nimport com.google.android.gms.dynamite.DynamiteModule;\nimport java.util.Collections;\n\n/**\n * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's\n * Cast extension.\n */\npublic class MainActivity extends AppCompatActivity\n    implements OnClickListener, PlayerManager.Listener {\n\n  private final MediaItem.Builder mediaItemBuilder;\n\n  private PlayerView localPlayerView;\n  private PlayerControlView castControlView;\n  private PlayerManager playerManager;\n  private RecyclerView mediaQueueList;\n  private MediaQueueListAdapter mediaQueueListAdapter;\n  private CastContext castContext;\n\n  public MainActivity() {\n    mediaItemBuilder = new MediaItem.Builder();\n  }\n\n  // Activity lifecycle methods.\n\n  @Override\n  public void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    // Getting the cast context later than onStart can cause device discovery not to take place.\n    try {\n      castContext = CastContext.getSharedInstance(this);\n    } catch (RuntimeException e) {\n      Throwable cause = e.getCause();\n      while (cause != null) {\n        if (cause instanceof DynamiteModule.LoadingException) {\n          setContentView(R.layout.cast_context_error);\n          return;\n        }\n        cause = cause.getCause();\n      }\n      // Unknown error. We propagate it.\n      throw e;\n    }\n\n    setContentView(R.layout.main_activity);\n\n    localPlayerView = findViewById(R.id.local_player_view);\n    localPlayerView.requestFocus();\n\n    castControlView = findViewById(R.id.cast_control_view);\n\n    mediaQueueList = findViewById(R.id.sample_list);\n    ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback());\n    helper.attachToRecyclerView(mediaQueueList);\n    mediaQueueList.setLayoutManager(new LinearLayoutManager(this));\n    mediaQueueList.setHasFixedSize(true);\n    mediaQueueListAdapter = new MediaQueueListAdapter();\n\n    findViewById(R.id.add_sample_button).setOnClickListener(this);\n  }\n\n  @Override\n  public boolean onCreateOptionsMenu(Menu menu) {\n    super.onCreateOptionsMenu(menu);\n    getMenuInflater().inflate(R.menu.menu, menu);\n    CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);\n    return true;\n  }\n\n  @Override\n  public void onResume() {\n    super.onResume();\n    if (castContext == null) {\n      // There is no Cast context to work with. Do nothing.\n      return;\n    }\n    String applicationId = castContext.getCastOptions().getReceiverApplicationId();\n    switch (applicationId) {\n      case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:\n        playerManager =\n            new DefaultReceiverPlayerManager(\n                /* listener= */ this,\n                localPlayerView,\n                castControlView,\n                /* context= */ this,\n                castContext);\n        break;\n      default:\n        throw new IllegalStateException(\"Illegal receiver app id: \" + applicationId);\n    }\n    mediaQueueList.setAdapter(mediaQueueListAdapter);\n  }\n\n  @Override\n  public void onPause() {\n    super.onPause();\n    if (castContext == null) {\n      // Nothing to release.\n      return;\n    }\n    mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());\n    mediaQueueList.setAdapter(null);\n    playerManager.release();\n    playerManager = null;\n  }\n\n  // Activity input.\n\n  @Override\n  public boolean dispatchKeyEvent(KeyEvent event) {\n    // If the event was not handled then see if the player view can handle it.\n    return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event);\n  }\n\n  @Override\n  public void onClick(View view) {\n    new AlertDialog.Builder(this)\n        .setTitle(R.string.add_samples)\n        .setView(buildSampleListView())\n        .setPositiveButton(android.R.string.ok, null)\n        .create()\n        .show();\n  }\n\n  // PlayerManager.Listener implementation.\n\n  @Override\n  public void onQueuePositionChanged(int previousIndex, int newIndex) {\n    if (previousIndex != C.INDEX_UNSET) {\n      mediaQueueListAdapter.notifyItemChanged(previousIndex);\n    }\n    if (newIndex != C.INDEX_UNSET) {\n      mediaQueueListAdapter.notifyItemChanged(newIndex);\n    }\n  }\n\n  @Override\n  public void onQueueContentsExternallyChanged() {\n    mediaQueueListAdapter.notifyDataSetChanged();\n  }\n\n  @Override\n  public void onPlayerError() {\n    Toast.makeText(getApplicationContext(), R.string.player_error_msg, Toast.LENGTH_LONG).show();\n  }\n\n  // Internal methods.\n\n  private View buildSampleListView() {\n    View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null);\n    ListView sampleList = dialogList.findViewById(R.id.sample_list);\n    sampleList.setAdapter(new SampleListAdapter(this));\n    sampleList.setOnItemClickListener(\n        (parent, view, position, id) -> {\n          DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position);\n          mediaItemBuilder\n              .clear()\n              .setMedia(sample.uri)\n              .setTitle(sample.name)\n              .setMimeType(sample.mimeType);\n          if (sample.drmSchemeUuid != null) {\n            mediaItemBuilder.setDrmSchemes(\n                Collections.singletonList(\n                    new MediaItem.DrmScheme(\n                        sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri))));\n          }\n          playerManager.addItem(mediaItemBuilder.build());\n          mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);\n        });\n    return dialogList;\n  }\n\n  // Internal classes.\n\n  private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {\n\n    @Override\n    public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {\n      TextView v = (TextView) LayoutInflater.from(parent.getContext())\n          .inflate(android.R.layout.simple_list_item_1, parent, false);\n      return new QueueItemViewHolder(v);\n    }\n\n    @Override\n    public void onBindViewHolder(QueueItemViewHolder holder, int position) {\n      holder.item = playerManager.getItem(position);\n      TextView view = holder.textView;\n      view.setText(holder.item.title);\n      // TODO: Solve coloring using the theme's ColorStateList.\n      view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(),\n           position == playerManager.getCurrentItemIndex() ? 255 : 100));\n    }\n\n    @Override\n    public int getItemCount() {\n      return playerManager.getMediaQueueSize();\n    }\n\n  }\n\n  private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback {\n\n    private int draggingFromPosition;\n    private int draggingToPosition;\n\n    public RecyclerViewCallback() {\n      super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END);\n      draggingFromPosition = C.INDEX_UNSET;\n      draggingToPosition = C.INDEX_UNSET;\n    }\n\n    @Override\n    public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin,\n        RecyclerView.ViewHolder target) {\n      int fromPosition = origin.getAdapterPosition();\n      int toPosition = target.getAdapterPosition();\n      if (draggingFromPosition == C.INDEX_UNSET) {\n        // A drag has started, but changes to the media queue will be reflected in clearView().\n        draggingFromPosition = fromPosition;\n      }\n      draggingToPosition = toPosition;\n      mediaQueueListAdapter.notifyItemMoved(fromPosition, toPosition);\n      return true;\n    }\n\n    @Override\n    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {\n      int position = viewHolder.getAdapterPosition();\n      QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;\n      if (playerManager.removeItem(queueItemHolder.item)) {\n        mediaQueueListAdapter.notifyItemRemoved(position);\n        // Update whichever item took its place, in case it became the new selected item.\n        mediaQueueListAdapter.notifyItemChanged(position);\n      }\n    }\n\n    @Override\n    public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {\n      super.clearView(recyclerView, viewHolder);\n      if (draggingFromPosition != C.INDEX_UNSET) {\n        QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;\n        // A drag has ended. We reflect the media queue change in the player.\n        if (!playerManager.moveItem(queueItemHolder.item, draggingToPosition)) {\n          // The move failed. The entire sequence of onMove calls since the drag started needs to be\n          // invalidated.\n          mediaQueueListAdapter.notifyDataSetChanged();\n        }\n      }\n      draggingFromPosition = C.INDEX_UNSET;\n      draggingToPosition = C.INDEX_UNSET;\n    }\n  }\n\n  private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener {\n\n    public final TextView textView;\n    public MediaItem item;\n\n    public QueueItemViewHolder(TextView textView) {\n      super(textView);\n      this.textView = textView;\n      textView.setOnClickListener(this);\n    }\n\n    @Override\n    public void onClick(View v) {\n      playerManager.selectQueueItem(getAdapterPosition());\n    }\n  }\n\n  private static final class SampleListAdapter extends ArrayAdapter<DemoUtil.Sample> {\n\n    public SampleListAdapter(Context context) {\n      super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.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.castdemo;\n\nimport android.view.KeyEvent;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ext.cast.MediaItem;\n\n/** Manages the players in the Cast demo app. */\n/* package */ interface PlayerManager {\n\n  /** Listener for events. */\n  interface Listener {\n\n    /** Called when the currently played item of the media queue changes. */\n    void onQueuePositionChanged(int previousIndex, int newIndex);\n\n    /** Called when the media queue changes due to modifications not caused by this manager. */\n    void onQueueContentsExternallyChanged();\n\n    /** Called when an error occurs in the current player. */\n    void onPlayerError();\n  }\n\n  /** Redirects the given {@code keyEvent} to the active player. */\n  boolean dispatchKeyEvent(KeyEvent keyEvent);\n\n  /** Appends the given {@link MediaItem} to the media queue. */\n  void addItem(MediaItem mediaItem);\n\n  /** Returns the number of items in the media queue. */\n  int getMediaQueueSize();\n\n  /** Selects the item at the given position for playback. */\n  void selectQueueItem(int position);\n\n  /**\n   * Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is\n   * being played.\n   */\n  int getCurrentItemIndex();\n\n  /** Returns the {@link MediaItem} at the given {@code position}. */\n  MediaItem getItem(int position);\n\n  /** Moves the item at position {@code from} to position {@code to}. */\n  boolean moveItem(MediaItem item, int to);\n\n  /** Removes the item at position {@code index}. */\n  boolean removeItem(MediaItem item);\n\n  /** Releases any acquired resources. */\n  void release();\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/src/main/res/drawable/ic_plus.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24.0dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\"\n    android:width=\"24.0dp\" >\n  <path\n      android:fillColor=\"#FFFFFFFF\"\n      android:pathData=\"M18,13h-5v5c0,0.55 -0.45,1 -1,1h0c-0.55,0 -1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1v0c0,-0.55 0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1h0c0.55,0 1,0.45 1,1v5h5c0.55,0 1,0.45 1,1v0C19,12.55 18.55,13 18,13z\"/>\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/src/main/res/layout/cast_context_error.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/textView\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:gravity=\"center\"\n    android:textSize=\"20sp\"\n    android:text=\"@string/cast_context_error\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/src/main/res/layout/main_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:keepScreenOn=\"true\">\n\n  <com.google.android.exoplayer2.ui.PlayerView android:id=\"@+id/local_player_view\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"0dp\"\n      android:layout_weight=\"1\"\n      android:background=\"@android:color/black\"\n      app:repeat_toggle_modes=\"all|one\"/>\n\n  <RelativeLayout android:layout_width=\"match_parent\"\n      android:layout_height=\"0dp\"\n      android:layout_weight=\"1\">\n\n    <androidx.recyclerview.widget.RecyclerView android:id=\"@+id/sample_list\"\n        android:choiceMode=\"singleChoice\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:scrollbars=\"vertical\"\n        android:fadeScrollbars=\"false\"/>\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton android:id=\"@+id/add_sample_button\"\n        android:src=\"@drawable/ic_plus\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentEnd=\"true\"\n        android:layout_alignParentRight=\"true\"\n        android:layout_alignParentBottom=\"true\"\n        android:layout_margin=\"16dp\"\n        android:contentDescription=\"@string/add_samples\"/>\n\n  </RelativeLayout>\n\n  <com.google.android.exoplayer2.ui.PlayerControlView android:id=\"@+id/cast_control_view\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:visibility=\"gone\"\n      app:repeat_toggle_modes=\"all|one\"\n      app:show_timeout=\"-1\"/>\n\n</LinearLayout>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/src/main/res/layout/sample_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\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  <ListView android:id=\"@+id/sample_list\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"250dp\"\n      android:fadeScrollbars=\"false\"/>\n\n</LinearLayout>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/src/main/res/menu/menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n      xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n  <item\n      android:id=\"@+id/media_route_menu_item\"\n      android:title=\"@string/media_route_menu_title\"\n      app:actionProviderClass=\"androidx.mediarouter.app.MediaRouteActionProvider\"\n      app:showAsAction=\"always\" />\n\n</menu>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/cast/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n\n<resources>\n\n  <string name=\"application_name\">Exo Cast Demo</string>\n\n  <string name=\"media_route_menu_title\">Cast</string>\n\n  <string name=\"add_samples\">Add samples</string>\n\n  <string name=\"cast_context_error\">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>\n\n  <string name=\"player_error_msg\">Player error encountered. Select a queue item to reprepare. Check the logcat and receiver app\\'s console for more info.</string>\n\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/ima/README.md",
    "content": "# IMA demo application #\n\nThis folder contains a demo application that showcases ExoPlayer integration\nwith the IMA SDK.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/ima/build.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.\napply from: '../../constants.gradle'\napply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        versionName project.ext.releaseVersion\n        versionCode project.ext.releaseVersionCode\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    buildTypes {\n        release {\n            shrinkResources true\n            minifyEnabled true\n            proguardFiles getDefaultProguardFile('proguard-android.txt')\n        }\n        debug {\n            jniDebuggable = true\n        }\n    }\n\n    lintOptions {\n        // The demo app isn't indexed and doesn't have translations.\n        disable 'GoogleAppIndexingWarning','MissingTranslation'\n    }\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    implementation project(modulePrefix + 'library-ui')\n    implementation project(modulePrefix + 'library-dash')\n    implementation project(modulePrefix + 'library-hls')\n    implementation project(modulePrefix + 'library-smoothstreaming')\n    implementation project(modulePrefix + 'extension-ima')\n    implementation 'androidx.annotation:annotation:1.1.0'\n}\n\napply plugin: 'com.google.android.gms.strict-version-matcher-plugin'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/ima/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.google.android.exoplayer2.imademo\">\n\n  <uses-permission android:name=\"android.permission.INTERNET\"/>\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n  <uses-sdk/>\n\n  <application android:label=\"@string/application_name\" android:icon=\"@mipmap/ic_launcher\"\n      android:largeHeap=\"true\" android:allowBackup=\"false\">\n\n    <activity android:name=\"com.google.android.exoplayer2.imademo.MainActivity\"\n        android:configChanges=\"keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode\"\n        android:label=\"@string/application_name\"\n        android:theme=\"@style/PlayerTheme\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\"/>\n        <category android:name=\"android.intent.category.LAUNCHER\"/>\n      </intent-filter>\n    </activity>\n\n  </application>\n\n</manifest>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.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.imademo;\n\nimport android.app.Activity;\nimport android.os.Bundle;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.ui.PlayerView;\n\n/**\n * Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by\n * {@link PlayerManager}, which this class instantiates.\n */\npublic final class MainActivity extends Activity {\n\n  private PlayerView playerView;\n  private PlayerManager player;\n\n  @Override\n  protected void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.main_activity);\n    playerView = findViewById(R.id.player_view);\n    player = new PlayerManager(this);\n  }\n\n  @Override\n  public void onResume() {\n    super.onResume();\n    player.init(this, playerView);\n  }\n\n  @Override\n  public void onPause() {\n    super.onPause();\n    player.reset();\n  }\n\n  @Override\n  public void onDestroy() {\n    player.release();\n    super.onDestroy();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.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.imademo;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.C.ContentType;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.ExoPlayerFactory;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.ext.ima.ImaAdsLoader;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.ProgressiveMediaSource;\nimport com.google.android.exoplayer2.source.ads.AdsMediaSource;\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.ui.PlayerView;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;\nimport com.google.android.exoplayer2.util.Util;\n\n/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */\n/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory {\n\n  private final ImaAdsLoader adsLoader;\n  private final DataSource.Factory dataSourceFactory;\n\n  private SimpleExoPlayer player;\n  private long contentPosition;\n\n  public PlayerManager(Context context) {\n    String adTag = context.getString(R.string.ad_tag_url);\n    adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));\n    dataSourceFactory =\n        new DefaultDataSourceFactory(\n            context, Util.getUserAgent(context, context.getString(R.string.application_name)));\n  }\n\n  public void init(Context context, PlayerView playerView) {\n    // Create a player instance.\n    player = ExoPlayerFactory.newSimpleInstance(context);\n    adsLoader.setPlayer(player);\n    playerView.setPlayer(player);\n\n    // This is the MediaSource representing the content media (i.e. not the ad).\n    String contentUrl = context.getString(R.string.content_url);\n    MediaSource contentMediaSource = buildMediaSource(Uri.parse(contentUrl));\n\n    // Compose the content media source into a new AdsMediaSource with both ads and content.\n    MediaSource mediaSourceWithAds =\n        new AdsMediaSource(\n            contentMediaSource, /* adMediaSourceFactory= */ this, adsLoader, playerView);\n\n    // Prepare the player with the source.\n    player.seekTo(contentPosition);\n    player.prepare(mediaSourceWithAds);\n    player.setPlayWhenReady(true);\n  }\n\n  public void reset() {\n    if (player != null) {\n      contentPosition = player.getContentPosition();\n      player.release();\n      player = null;\n      adsLoader.setPlayer(null);\n    }\n  }\n\n  public void release() {\n    if (player != null) {\n      player.release();\n      player = null;\n    }\n    adsLoader.release();\n  }\n\n  // AdsMediaSource.MediaSourceFactory implementation.\n\n  @Override\n  public MediaSource createMediaSource(Uri uri) {\n    return buildMediaSource(uri);\n  }\n\n  @Override\n  public int[] getSupportedTypes() {\n    // IMA does not support Smooth Streaming ads.\n    return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER};\n  }\n\n  // Internal methods.\n\n  private MediaSource buildMediaSource(Uri uri) {\n    @ContentType int type = Util.inferContentType(uri);\n    switch (type) {\n      case C.TYPE_DASH:\n        return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);\n      case C.TYPE_SS:\n        return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);\n      case C.TYPE_HLS:\n        return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);\n      case C.TYPE_OTHER:\n        return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);\n      default:\n        throw new IllegalStateException(\"Unsupported type: \" + type);\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/ima/src/main/res/layout/main_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n<com.google.android.exoplayer2.ui.PlayerView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/player_view\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:keepScreenOn=\"true\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/ima/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n<resources>\n\n  <string name=\"application_name\">Exo IMA Demo</string>\n\n  <string name=\"content_url\"><![CDATA[https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv]]></string>\n\n  <string name=\"ad_tag_url\"><![CDATA[https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=]]></string>\n\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/ima/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n<resources xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n  <style name=\"PlayerTheme\" parent=\"android:Theme.Holo\">\n    <item name=\"android:windowNoTitle\">true</item>\n    <item name=\"android:windowBackground\">@android:color/black</item>\n  </style>\n\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/README.md",
    "content": "# ExoPlayer main demo #\n\nThis is the main ExoPlayer demo application. It uses ExoPlayer to play a number\nof test streams. It can be used as a starting point or reference project when\ndeveloping other applications that make use of the ExoPlayer library.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/build.gradle",
    "content": "// Copyright (C) 2016 The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\napply from: '../../constants.gradle'\napply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        versionName project.ext.releaseVersion\n        versionCode project.ext.releaseVersionCode\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    buildTypes {\n        release {\n            shrinkResources true\n            minifyEnabled true\n            proguardFiles = [\n                \"proguard-rules.txt\",\n                getDefaultProguardFile('proguard-android.txt')\n            ]\n        }\n        debug {\n            jniDebuggable = true\n        }\n    }\n\n    lintOptions {\n        // The demo app isn't indexed, doesn't have translations, and has a\n        // banner for AndroidTV that's only in xhdpi density.\n        disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'\n    }\n\n    flavorDimensions \"extensions\"\n\n    productFlavors {\n        noExtensions {\n            dimension \"extensions\"\n        }\n        withExtensions {\n            dimension \"extensions\"\n        }\n    }\n}\n\ndependencies {\n    implementation 'androidx.annotation:annotation:1.1.0'\n    implementation 'androidx.legacy:legacy-support-core-ui:1.0.0'\n    implementation 'androidx.fragment:fragment:1.0.0'\n    implementation 'com.google.android.material:material:1.0.0'\n    implementation project(modulePrefix + 'library-core')\n    implementation project(modulePrefix + 'library-dash')\n    implementation project(modulePrefix + 'library-hls')\n    implementation project(modulePrefix + 'library-smoothstreaming')\n    implementation project(modulePrefix + 'library-ui')\n    withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')\n    withExtensionsImplementation project(path: modulePrefix + 'extension-flac')\n    withExtensionsImplementation project(path: modulePrefix + 'extension-ima')\n    withExtensionsImplementation project(path: modulePrefix + 'extension-opus')\n    withExtensionsImplementation project(path: modulePrefix + 'extension-vp9')\n    withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp')\n}\n\napply plugin: 'com.google.android.gms.strict-version-matcher-plugin'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/proguard-rules.txt",
    "content": "# Proguard rules specific to the main demo app.\n\n# Constructor accessed via reflection in PlayerActivity\n-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader\n-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader {\n  <init>(android.content.Context, android.net.Uri);\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"com.google.android.exoplayer2.demo\">\n\n  <uses-permission android:name=\"android.permission.INTERNET\"/>\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n  <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>\n  <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>\n  <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\"/>\n\n  <uses-feature android:name=\"android.software.leanback\" android:required=\"false\"/>\n  <uses-feature android:name=\"android.hardware.touchscreen\" android:required=\"false\"/>\n  <uses-sdk/>\n\n  <application\n      android:label=\"@string/application_name\"\n      android:icon=\"@mipmap/ic_launcher\"\n      android:banner=\"@drawable/ic_banner\"\n      android:largeHeap=\"true\"\n      android:allowBackup=\"false\"\n      android:name=\"com.google.android.exoplayer2.demo.DemoApplication\"\n      tools:ignore=\"UnusedAttribute\">\n\n    <activity android:name=\"com.google.android.exoplayer2.demo.SampleChooserActivity\"\n        android:configChanges=\"keyboardHidden\"\n        android:label=\"@string/application_name\"\n        android:theme=\"@style/Theme.AppCompat\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\"/>\n        <category android:name=\"android.intent.category.LAUNCHER\"/>\n        <category android:name=\"android.intent.category.LEANBACK_LAUNCHER\"/>\n      </intent-filter>\n      <intent-filter>\n        <action android:name=\"android.intent.action.VIEW\"/>\n        <category android:name=\"android.intent.category.DEFAULT\"/>\n        <category android:name=\"android.intent.category.BROWSABLE\"/>\n        <data android:scheme=\"http\"/>\n        <data android:scheme=\"https\"/>\n        <data android:scheme=\"content\"/>\n        <data android:scheme=\"asset\"/>\n        <data android:scheme=\"file\"/>\n        <data android:host=\"*\"/>\n        <data android:pathPattern=\".*\\\\.exolist\\\\.json\"/>\n      </intent-filter>\n    </activity>\n\n    <activity android:name=\"com.google.android.exoplayer2.demo.PlayerActivity\"\n        android:configChanges=\"keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode\"\n        android:launchMode=\"singleTop\"\n        android:label=\"@string/application_name\"\n        android:theme=\"@style/PlayerTheme\">\n      <intent-filter>\n        <action android:name=\"com.google.android.exoplayer.demo.action.VIEW\"/>\n        <category android:name=\"android.intent.category.DEFAULT\"/>\n        <data android:scheme=\"http\"/>\n        <data android:scheme=\"https\"/>\n        <data android:scheme=\"content\"/>\n        <data android:scheme=\"asset\"/>\n        <data android:scheme=\"file\"/>\n      </intent-filter>\n      <intent-filter>\n        <action android:name=\"com.google.android.exoplayer.demo.action.VIEW_LIST\"/>\n        <category android:name=\"android.intent.category.DEFAULT\"/>\n      </intent-filter>\n    </activity>\n\n    <service android:name=\"com.google.android.exoplayer2.demo.DemoDownloadService\"\n        android:exported=\"false\">\n      <intent-filter>\n        <action android:name=\"com.google.android.exoplayer.downloadService.action.RESTART\"/>\n        <category android:name=\"android.intent.category.DEFAULT\"/>\n      </intent-filter>\n    </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\n  </application>\n\n</manifest>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/assets/media.exolist.json",
    "content": "[\n  {\n    \"name\": \"YouTube DASH\",\n    \"samples\": [\n      {\n        \"name\": \"Google Glass (MP4,H264)\",\n        \"uri\": \"https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0\",\n        \"extension\": \"mpd\"\n      },\n      {\n        \"name\": \"Google Play (MP4,H264)\",\n        \"uri\": \"https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0\",\n        \"extension\": \"mpd\"\n      },\n      {\n        \"name\": \"Google Glass (WebM,VP9)\",\n        \"uri\": \"https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0\",\n        \"extension\": \"mpd\"\n      },\n      {\n        \"name\": \"Google Play (WebM,VP9)\",\n        \"uri\": \"https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0\",\n        \"extension\": \"mpd\"\n      }\n    ]\n  },\n  {\n    \"name\": \"Widevine DASH Policy Tests (GTS)\",\n    \"samples\": [\n      {\n        \"name\": \"WV: HDCP not specified\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1c&provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: HDCP not required\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=48fcc369939ac96c&provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: HDCP required\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=e06c39f1151da3df&provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure video path required (MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure video path required (WebM,VP9)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure video path required (MP4,H265)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: HDCP + secure video path required\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=efd045b1eb61888a&provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: 30s license duration (fails at ~30s)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=f9a34cab7b05881a&provider=widevine_test\"\n      }\n    ]\n  },\n  {\n    \"name\": \"Widevine HDCP Capabilities Tests\",\n    \"samples\": [\n      {\n        \"name\": \"WV: HDCP: None (not required)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=HDCP_None&provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: HDCP: 1.0 required\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=HDCP_V1&provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: HDCP: 2.0 required\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2&provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: HDCP: 2.1 required\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_1&provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: HDCP: 2.2 required\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_2&provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: HDCP: No digital output\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?video_id=HDCP_NO_DIGTAL_OUTPUT&provider=widevine_test\"\n      }\n    ]\n  },\n  {\n    \"name\": \"Widevine DASH: MP4,H264\",\n    \"samples\": [\n      {\n        \"name\": \"WV: Clear SD & HD (MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd\"\n      },\n      {\n        \"name\": \"WV: Clear SD (MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_sd.mpd\"\n      },\n      {\n        \"name\": \"WV: Clear HD (MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd\"\n      },\n      {\n        \"name\": \"WV: Clear UHD (MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd\"\n      },\n      {\n        \"name\": \"WV: Secure SD & HD (cenc,MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure SD (cenc,MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure HD (cenc,MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure UHD (cenc,MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure SD & HD (cbc1,MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure SD (cbc1,MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_sd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure HD (cbc1,MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_hd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure UHD (cbc1,MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure SD & HD (cbcs,MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure SD (cbcs,MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_sd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure HD (cbcs,MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_hd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure UHD (cbcs,MP4,H264)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      }\n    ]\n  },\n  {\n    \"name\": \"Widevine DASH: WebM,VP9\",\n    \"samples\": [\n      {\n        \"name\": \"WV: Clear SD & HD (WebM,VP9)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd\"\n      },\n      {\n        \"name\": \"WV: Clear SD (WebM,VP9)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_sd.mpd\"\n      },\n      {\n        \"name\": \"WV: Clear HD (WebM,VP9)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_hd.mpd\"\n      },\n      {\n        \"name\": \"WV: Clear UHD (WebM,VP9)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd\"\n      },\n      {\n        \"name\": \"WV: Secure Fullsample SD & HD (WebM,VP9)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure Fullsample SD (WebM,VP9)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure Fullsample HD (WebM,VP9)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure Fullsample UHD (WebM,VP9)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure Subsample SD & HD (WebM,VP9)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure Subsample SD (WebM,VP9)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure Subsample HD (WebM,VP9)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure Subsample UHD (WebM,VP9)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      }\n    ]\n  },\n  {\n    \"name\": \"Widevine DASH: MP4,H265\",\n    \"samples\": [\n      {\n        \"name\": \"WV: Clear SD & HD (MP4,H265)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd\"\n      },\n      {\n        \"name\": \"WV: Clear SD (MP4,H265)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_sd.mpd\"\n      },\n      {\n        \"name\": \"WV: Clear HD (MP4,H265)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_hd.mpd\"\n      },\n      {\n        \"name\": \"WV: Clear UHD (MP4,H265)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd\"\n      },\n      {\n        \"name\": \"WV: Secure SD & HD (MP4,H265)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure SD (MP4,H265)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_sd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure HD (MP4,H265)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_hd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      },\n      {\n        \"name\": \"WV: Secure UHD (MP4,H265)\",\n        \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\"\n      }\n    ]\n  },\n  {\n    \"name\": \"SmoothStreaming\",\n    \"samples\": [\n      {\n        \"name\": \"Super speed\",\n        \"uri\": \"https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest\"\n      },\n      {\n        \"name\": \"Super speed (PlayReady)\",\n        \"uri\": \"https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest\",\n        \"drm_scheme\": \"playready\"\n      }\n    ]\n  },\n  {\n    \"name\": \"HLS\",\n    \"samples\": [\n      {\n        \"name\": \"Apple 4x3 basic stream\",\n        \"uri\": \"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8\"\n      },\n      {\n        \"name\": \"Apple 16x9 basic stream\",\n        \"uri\": \"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8\"\n      },\n      {\n        \"name\": \"Apple master playlist advanced (TS)\",\n        \"uri\": \"https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8\"\n      },\n      {\n        \"name\": \"Apple master playlist advanced (fMP4)\",\n        \"uri\": \"https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8\"\n      },\n      {\n        \"name\": \"Apple TS media playlist\",\n        \"uri\": \"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8\"\n      },\n      {\n        \"name\": \"Apple AAC media playlist\",\n        \"uri\": \"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8\"\n      }\n    ]\n  },\n  {\n    \"name\": \"Misc\",\n    \"samples\": [\n      {\n        \"name\": \"Dizzy (MP4)\",\n        \"uri\": \"https://html5demos.com/assets/dizzy.mp4\"\n      },\n      {\n        \"name\": \"Apple AAC 10s\",\n        \"uri\": \"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac\"\n      },\n      {\n        \"name\": \"Apple TS 10s\",\n        \"uri\": \"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts\"\n      },\n      {\n        \"name\": \"Android screens (Matroska)\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\"\n      },\n      {\n        \"name\": \"Screens 360P (WebM,VP9,No Audio)\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm\"\n      },\n      {\n        \"name\": \"Screens 480p (FMP4,H264,No Audio)\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4\"\n      },\n      {\n        \"name\": \"Screens 1080p (FMP4,H264, No Audio)\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4\"\n      },\n      {\n        \"name\": \"Screens (FMP4,AAC Audio)\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4\"\n      },\n      {\n        \"name\": \"Google Play (MP3 Audio)\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-0/play.mp3\"\n      },\n      {\n        \"name\": \"Google Play (Ogg/Vorbis Audio)\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg\"\n      },\n      {\n        \"name\": \"Big Buck Bunny (FLV Video)\",\n        \"uri\": \"https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0\"\n      }\n    ]\n  },\n  {\n    \"name\": \"Playlists\",\n    \"samples\": [\n      {\n        \"name\": \"Cats -> Dogs\",\n        \"playlist\": [\n          {\n            \"uri\": \"https://html5demos.com/assets/dizzy.mp4\"\n          },\n          {\n            \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\"\n          }\n        ]\n      },\n      {\n        \"name\": \"Audio -> Video -> Audio\",\n        \"playlist\": [\n          {\n            \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4\"\n          },\n          {\n            \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\"\n          },\n          {\n            \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4\"\n          }\n        ]\n      },\n      {\n        \"name\": \"Clear -> Enc -> Clear -> Enc -> Enc\",\n        \"drm_scheme\": \"widevine\",\n        \"drm_license_url\": \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\",\n        \"playlist\": [\n          {\n            \"uri\": \"https://html5demos.com/assets/dizzy.mp4\"\n          },\n          {\n            \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd\"\n          },\n          {\n            \"uri\": \"https://html5demos.com/assets/dizzy.mp4\"\n          },\n          {\n            \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd\"\n          },\n          {\n            \"uri\": \"https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd\"\n          }\n        ]\n      }\n    ]\n  },\n  {\n    \"name\": \"IMA sample ad tags\",\n    \"samples\": [\n      {\n        \"name\": \"Single inline linear\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=\"\n      },\n      {\n        \"name\": \"Single skippable inline\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator=\"\n      },\n      {\n        \"name\": \"Single redirect linear\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirectlinear&correlator=\"\n      },\n      {\n        \"name\": \"Single redirect error\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&nofb=1&correlator=\"\n      },\n      {\n        \"name\": \"Single redirect broken (fallback)\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&correlator=\"\n      },\n      {\n        \"name\": \"VMAP pre-roll\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator=\"\n      },\n      {\n        \"name\": \"VMAP pre-roll + bumper\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonlybumper&cmsid=496&vid=short_onecue&correlator=\"\n      },\n      {\n        \"name\": \"VMAP post-roll\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonly&cmsid=496&vid=short_onecue&correlator=\"\n      },\n      {\n        \"name\": \"VMAP post-roll + bumper\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonlybumper&cmsid=496&vid=short_onecue&correlator=\"\n      },\n      {\n        \"name\": \"VMAP pre-, mid- and post-rolls, single ads\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=\"\n      },\n      {\n        \"name\": \"VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator=\"\n      },\n      {\n        \"name\": \"VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpod&cmsid=496&vid=short_onecue&correlator=\"\n      },\n      {\n        \"name\": \"VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad (bumpers around all ad breaks)\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpodbumper&cmsid=496&vid=short_onecue&correlator=\"\n      },\n      {\n        \"name\": \"VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad (bumpers around all ad breaks)\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator=\"\n      },\n      {\n        \"name\": \"VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator=\"\n      },\n      {\n        \"name\": \"VMAP empty midroll\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://vastsynthesizer.appspot.com/empty-midroll\"\n      },\n      {\n        \"name\": \"VMAP full, empty, full midrolls\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv\",\n        \"ad_tag_uri\": \"https://vastsynthesizer.appspot.com/empty-midroll-2\"\n      }\n    ]\n  },\n  {\n    \"name\": \"360\",\n    \"samples\": [\n      {\n        \"name\": \"Congo (360 top-bottom stereo)\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4\",\n        \"spherical_stereo_mode\": \"top_bottom\"\n      },\n      {\n        \"name\": \"Sphericalv2 (180 top-bottom stereo)\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4\",\n        \"spherical_stereo_mode\": \"top_bottom\"\n      },\n      {\n        \"name\": \"Iceland (360 top-bottom stereo ts)\",\n        \"uri\": \"https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts\",\n        \"spherical_stereo_mode\": \"top_bottom\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.demo;\n\nimport android.app.Application;\nimport com.google.android.exoplayer2.DefaultRenderersFactory;\nimport com.google.android.exoplayer2.RenderersFactory;\nimport com.google.android.exoplayer2.database.DatabaseProvider;\nimport com.google.android.exoplayer2.database.ExoDatabaseProvider;\nimport com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;\nimport com.google.android.exoplayer2.offline.DefaultDownloadIndex;\nimport com.google.android.exoplayer2.offline.DefaultDownloaderFactory;\nimport com.google.android.exoplayer2.offline.DownloadManager;\nimport com.google.android.exoplayer2.offline.DownloaderConstructorHelper;\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.upstream.FileDataSourceFactory;\nimport com.google.android.exoplayer2.upstream.HttpDataSource;\nimport com.google.android.exoplayer2.upstream.cache.Cache;\nimport com.google.android.exoplayer2.upstream.cache.CacheDataSource;\nimport com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;\nimport com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;\nimport com.google.android.exoplayer2.upstream.cache.SimpleCache;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.io.IOException;\n\n/**\n * Placeholder application to facilitate overriding Application methods for debugging and testing.\n */\npublic class DemoApplication extends Application {\n\n  private static final String TAG = \"DemoApplication\";\n  private static final String DOWNLOAD_ACTION_FILE = \"actions\";\n  private static final String DOWNLOAD_TRACKER_ACTION_FILE = \"tracked_actions\";\n  private static final String DOWNLOAD_CONTENT_DIRECTORY = \"downloads\";\n\n  protected String userAgent;\n\n  private DatabaseProvider databaseProvider;\n  private File downloadDirectory;\n  private Cache downloadCache;\n  private DownloadManager downloadManager;\n  private DownloadTracker downloadTracker;\n\n  @Override\n  public void onCreate() {\n    super.onCreate();\n    userAgent = Util.getUserAgent(this, \"ExoPlayerDemo\");\n  }\n\n  /** Returns a {@link DataSource.Factory}. */\n  public DataSource.Factory buildDataSourceFactory() {\n    DefaultDataSourceFactory upstreamFactory =\n        new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());\n    return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());\n  }\n\n  /** Returns a {@link HttpDataSource.Factory}. */\n  public HttpDataSource.Factory buildHttpDataSourceFactory() {\n    return new DefaultHttpDataSourceFactory(userAgent);\n  }\n\n  /** Returns whether extension renderers should be used. */\n  public boolean useExtensionRenderers() {\n    return \"withExtensions\".equals(BuildConfig.FLAVOR);\n  }\n\n  public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {\n    @DefaultRenderersFactory.ExtensionRendererMode\n    int extensionRendererMode =\n        useExtensionRenderers()\n            ? (preferExtensionRenderer\n                ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER\n                : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)\n            : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;\n    return new DefaultRenderersFactory(/* context= */ this)\n        .setExtensionRendererMode(extensionRendererMode);\n  }\n\n  public DownloadManager getDownloadManager() {\n    initDownloadManager();\n    return downloadManager;\n  }\n\n  public DownloadTracker getDownloadTracker() {\n    initDownloadManager();\n    return downloadTracker;\n  }\n\n  protected synchronized Cache getDownloadCache() {\n    if (downloadCache == null) {\n      File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);\n      downloadCache =\n          new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider());\n    }\n    return downloadCache;\n  }\n\n  private synchronized void initDownloadManager() {\n    if (downloadManager == null) {\n      DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider());\n      upgradeActionFile(\n          DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);\n      upgradeActionFile(\n          DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);\n      DownloaderConstructorHelper downloaderConstructorHelper =\n          new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());\n      downloadManager =\n          new DownloadManager(\n              this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));\n      downloadTracker =\n          new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);\n    }\n  }\n\n  private void upgradeActionFile(\n      String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) {\n    try {\n      ActionFileUpgradeUtil.upgradeAndDelete(\n          new File(getDownloadDirectory(), fileName),\n          /* downloadIdProvider= */ null,\n          downloadIndex,\n          /* deleteOnFailure= */ true,\n          addNewDownloadsAsCompleted);\n    } catch (IOException e) {\n      Log.e(TAG, \"Failed to upgrade action file: \" + fileName, e);\n    }\n  }\n\n  private DatabaseProvider getDatabaseProvider() {\n    if (databaseProvider == null) {\n      databaseProvider = new ExoDatabaseProvider(this);\n    }\n    return databaseProvider;\n  }\n\n  private File getDownloadDirectory() {\n    if (downloadDirectory == null) {\n      downloadDirectory = getExternalFilesDir(null);\n      if (downloadDirectory == null) {\n        downloadDirectory = getFilesDir();\n      }\n    }\n    return downloadDirectory;\n  }\n\n  protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(\n      DataSource.Factory upstreamFactory, Cache cache) {\n    return new CacheDataSourceFactory(\n        cache,\n        upstreamFactory,\n        new FileDataSourceFactory(),\n        /* cacheWriteDataSinkFactory= */ null,\n        CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,\n        /* eventListener= */ null);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.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.demo;\n\nimport android.app.Notification;\nimport com.google.android.exoplayer2.offline.Download;\nimport com.google.android.exoplayer2.offline.DownloadManager;\nimport com.google.android.exoplayer2.offline.DownloadService;\nimport com.google.android.exoplayer2.scheduler.PlatformScheduler;\nimport com.google.android.exoplayer2.ui.DownloadNotificationHelper;\nimport com.google.android.exoplayer2.util.NotificationUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.List;\n\n/** A service for downloading media. */\npublic class DemoDownloadService extends DownloadService {\n\n  private static final String CHANNEL_ID = \"download_channel\";\n  private static final int JOB_ID = 1;\n  private static final int FOREGROUND_NOTIFICATION_ID = 1;\n\n  private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;\n\n  private DownloadNotificationHelper notificationHelper;\n\n  public DemoDownloadService() {\n    super(\n        FOREGROUND_NOTIFICATION_ID,\n        DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,\n        CHANNEL_ID,\n        R.string.exo_download_notification_channel_name,\n        /* channelDescriptionResourceId= */ 0);\n    nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;\n  }\n\n  @Override\n  public void onCreate() {\n    super.onCreate();\n    notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID);\n  }\n\n  @Override\n  protected DownloadManager getDownloadManager() {\n    return ((DemoApplication) getApplication()).getDownloadManager();\n  }\n\n  @Override\n  protected PlatformScheduler getScheduler() {\n    return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null;\n  }\n\n  @Override\n  protected Notification getForegroundNotification(List<Download> downloads) {\n    return notificationHelper.buildProgressNotification(\n        R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);\n  }\n\n  @Override\n  protected void onDownloadChanged(Download download) {\n    Notification notification;\n    if (download.state == Download.STATE_COMPLETED) {\n      notification =\n          notificationHelper.buildDownloadCompletedNotification(\n              R.drawable.ic_download_done,\n              /* contentIntent= */ null,\n              Util.fromUtf8Bytes(download.request.data));\n    } else if (download.state == Download.STATE_FAILED) {\n      notification =\n          notificationHelper.buildDownloadFailedNotification(\n              R.drawable.ic_download_done,\n              /* contentIntent= */ null,\n              Util.fromUtf8Bytes(download.request.data));\n    } else {\n      return;\n    }\n    NotificationUtil.setNotification(this, nextNotificationId++, notification);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.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.demo;\n\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.FragmentManager;\nimport android.widget.Toast;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.RenderersFactory;\nimport com.google.android.exoplayer2.offline.Download;\nimport com.google.android.exoplayer2.offline.DownloadCursor;\nimport com.google.android.exoplayer2.offline.DownloadHelper;\nimport com.google.android.exoplayer2.offline.DownloadIndex;\nimport com.google.android.exoplayer2.offline.DownloadManager;\nimport com.google.android.exoplayer2.offline.DownloadRequest;\nimport com.google.android.exoplayer2.offline.DownloadService;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\n/** Tracks media that has been downloaded. */\npublic class DownloadTracker {\n\n  /** Listens for changes in the tracked downloads. */\n  public interface Listener {\n\n    /** Called when the tracked downloads changed. */\n    void onDownloadsChanged();\n  }\n\n  private static final String TAG = \"DownloadTracker\";\n\n  private final Context context;\n  private final DataSource.Factory dataSourceFactory;\n  private final CopyOnWriteArraySet<Listener> listeners;\n  private final HashMap<Uri, Download> downloads;\n  private final DownloadIndex downloadIndex;\n\n  @Nullable private StartDownloadDialogHelper startDownloadDialogHelper;\n\n  public DownloadTracker(\n      Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {\n    this.context = context.getApplicationContext();\n    this.dataSourceFactory = dataSourceFactory;\n    listeners = new CopyOnWriteArraySet<>();\n    downloads = new HashMap<>();\n    downloadIndex = downloadManager.getDownloadIndex();\n    downloadManager.addListener(new DownloadManagerListener());\n    loadDownloads();\n  }\n\n  public void addListener(Listener listener) {\n    listeners.add(listener);\n  }\n\n  public void removeListener(Listener listener) {\n    listeners.remove(listener);\n  }\n\n  public boolean isDownloaded(Uri uri) {\n    Download download = downloads.get(uri);\n    return download != null && download.state != Download.STATE_FAILED;\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  public DownloadRequest getDownloadRequest(Uri uri) {\n    Download download = downloads.get(uri);\n    return download != null && download.state != Download.STATE_FAILED ? download.request : null;\n  }\n\n  public void toggleDownload(\n      FragmentManager fragmentManager,\n      String name,\n      Uri uri,\n      String extension,\n      RenderersFactory renderersFactory) {\n    Download download = downloads.get(uri);\n    if (download != null) {\n      DownloadService.sendRemoveDownload(\n          context, DemoDownloadService.class, download.request.id, /* foreground= */ false);\n    } else {\n      if (startDownloadDialogHelper != null) {\n        startDownloadDialogHelper.release();\n      }\n      startDownloadDialogHelper =\n          new StartDownloadDialogHelper(\n              fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name);\n    }\n  }\n\n  private void loadDownloads() {\n    try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) {\n      while (loadedDownloads.moveToNext()) {\n        Download download = loadedDownloads.getDownload();\n        downloads.put(download.request.uri, download);\n      }\n    } catch (IOException e) {\n      Log.w(TAG, \"Failed to query downloads\", e);\n    }\n  }\n\n  private DownloadHelper getDownloadHelper(\n      Uri uri, String extension, RenderersFactory renderersFactory) {\n    int type = Util.inferContentType(uri, extension);\n    switch (type) {\n      case C.TYPE_DASH:\n        return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory);\n      case C.TYPE_SS:\n        return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory);\n      case C.TYPE_HLS:\n        return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory);\n      case C.TYPE_OTHER:\n        return DownloadHelper.forProgressive(uri);\n      default:\n        throw new IllegalStateException(\"Unsupported type: \" + type);\n    }\n  }\n\n  private class DownloadManagerListener implements DownloadManager.Listener {\n\n    @Override\n    public void onDownloadChanged(DownloadManager downloadManager, Download download) {\n      downloads.put(download.request.uri, download);\n      for (Listener listener : listeners) {\n        listener.onDownloadsChanged();\n      }\n    }\n\n    @Override\n    public void onDownloadRemoved(DownloadManager downloadManager, Download download) {\n      downloads.remove(download.request.uri);\n      for (Listener listener : listeners) {\n        listener.onDownloadsChanged();\n      }\n    }\n  }\n\n  private final class StartDownloadDialogHelper\n      implements DownloadHelper.Callback,\n          DialogInterface.OnClickListener,\n          DialogInterface.OnDismissListener {\n\n    private final FragmentManager fragmentManager;\n    private final DownloadHelper downloadHelper;\n    private final String name;\n\n    private TrackSelectionDialog trackSelectionDialog;\n    private MappedTrackInfo mappedTrackInfo;\n\n    public StartDownloadDialogHelper(\n        FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) {\n      this.fragmentManager = fragmentManager;\n      this.downloadHelper = downloadHelper;\n      this.name = name;\n      downloadHelper.prepare(this);\n    }\n\n    public void release() {\n      downloadHelper.release();\n      if (trackSelectionDialog != null) {\n        trackSelectionDialog.dismiss();\n      }\n    }\n\n    // DownloadHelper.Callback implementation.\n\n    @Override\n    public void onPrepared(DownloadHelper helper) {\n      if (helper.getPeriodCount() == 0) {\n        Log.d(TAG, \"No periods found. Downloading entire stream.\");\n        startDownload();\n        downloadHelper.release();\n        return;\n      }\n      mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);\n      if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {\n        Log.d(TAG, \"No dialog content. Downloading entire stream.\");\n        startDownload();\n        downloadHelper.release();\n        return;\n      }\n      trackSelectionDialog =\n          TrackSelectionDialog.createForMappedTrackInfoAndParameters(\n              /* titleId= */ R.string.exo_download_description,\n              mappedTrackInfo,\n              /* initialParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,\n              /* allowAdaptiveSelections =*/ false,\n              /* allowMultipleOverrides= */ true,\n              /* onClickListener= */ this,\n              /* onDismissListener= */ this);\n      trackSelectionDialog.show(fragmentManager, /* tag= */ null);\n    }\n\n    @Override\n    public void onPrepareError(DownloadHelper helper, IOException e) {\n      Toast.makeText(\n              context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)\n          .show();\n      Log.e(TAG, \"Failed to start download\", e);\n    }\n\n    // DialogInterface.OnClickListener implementation.\n\n    @Override\n    public void onClick(DialogInterface dialog, int which) {\n      for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) {\n        downloadHelper.clearTrackSelections(periodIndex);\n        for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {\n          if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) {\n            downloadHelper.addTrackSelectionForSingleRenderer(\n                periodIndex,\n                /* rendererIndex= */ i,\n                DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,\n                trackSelectionDialog.getOverrides(/* rendererIndex= */ i));\n          }\n        }\n      }\n      DownloadRequest downloadRequest = buildDownloadRequest();\n      if (downloadRequest.streamKeys.isEmpty()) {\n        // All tracks were deselected in the dialog. Don't start the download.\n        return;\n      }\n      startDownload(downloadRequest);\n    }\n\n    // DialogInterface.OnDismissListener implementation.\n\n    @Override\n    public void onDismiss(DialogInterface dialogInterface) {\n      trackSelectionDialog = null;\n      downloadHelper.release();\n    }\n\n    // Internal methods.\n\n    private void startDownload() {\n      startDownload(buildDownloadRequest());\n    }\n\n    private void startDownload(DownloadRequest downloadRequest) {\n      DownloadService.sendAddDownload(\n          context, DemoDownloadService.class, downloadRequest, /* foreground= */ false);\n    }\n\n    private DownloadRequest buildDownloadRequest() {\n      return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name));\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.demo;\n\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Looper;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\nimport android.util.Pair;\nimport android.util.Log;\nimport android.view.KeyEvent;\nimport android.view.View;\nimport android.view.View.OnClickListener;\nimport android.widget.Button;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\nimport android.widget.Toast;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.C.ContentType;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayerFactory;\nimport com.google.android.exoplayer2.PlaybackPreparer;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.RenderersFactory;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.audio.AudioCapabilities;\nimport com.google.android.exoplayer2.audio.AudioCapabilitiesReceiver;\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.drm.FrameworkMediaDrm;\nimport com.google.android.exoplayer2.drm.HttpMediaDrmCallback;\nimport com.google.android.exoplayer2.drm.UnsupportedDrmException;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;\nimport com.google.android.exoplayer2.offline.DownloadHelper;\nimport com.google.android.exoplayer2.offline.DownloadRequest;\nimport com.google.android.exoplayer2.source.BehindLiveWindowException;\nimport com.google.android.exoplayer2.source.ConcatenatingMediaSource;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.ProgressiveMediaSource;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.source.ads.AdsLoader;\nimport com.google.android.exoplayer2.source.ads.AdsMediaSource;\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.trackselection.AdaptiveTrackSelection;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;\nimport com.google.android.exoplayer2.trackselection.RandomTrackSelection;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.google.android.exoplayer2.ui.DebugTextViewHelper;\nimport com.google.android.exoplayer2.ui.PlayerControlView;\nimport com.google.android.exoplayer2.ui.PlayerView;\nimport com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.HttpDataSource;\nimport com.google.android.exoplayer2.util.ErrorMessageProvider;\nimport com.google.android.exoplayer2.util.EventLogger;\nimport com.google.android.exoplayer2.util.Util;\nimport java.lang.reflect.Constructor;\nimport java.net.CookieHandler;\nimport java.net.CookieManager;\nimport java.net.CookiePolicy;\nimport java.util.UUID;\n\n/** An activity that plays media using {@link SimpleExoPlayer}. */\npublic class PlayerActivity extends AppCompatActivity\n    implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener, AudioCapabilitiesReceiver.Listener  {\n\n  public static final String TAG = PlayerActivity.class.getSimpleName();\n  public static final String DRM_SCHEME_EXTRA = \"drm_scheme\";\n  public static final String DRM_LICENSE_URL_EXTRA = \"drm_license_url\";\n  public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = \"drm_key_request_properties\";\n  public static final String DRM_MULTI_SESSION_EXTRA = \"drm_multi_session\";\n  public static final String PREFER_EXTENSION_DECODERS_EXTRA = \"prefer_extension_decoders\";\n\n  public static final String ACTION_VIEW = \"com.google.android.exoplayer.demo.action.VIEW\";\n  public static final String EXTENSION_EXTRA = \"extension\";\n\n  public static final String ACTION_VIEW_LIST =\n      \"com.google.android.exoplayer.demo.action.VIEW_LIST\";\n  public static final String URI_LIST_EXTRA = \"uri_list\";\n  public static final String EXTENSION_LIST_EXTRA = \"extension_list\";\n\n  public static final String AD_TAG_URI_EXTRA = \"ad_tag_uri\";\n\n  public static final String ABR_ALGORITHM_EXTRA = \"abr_algorithm\";\n  public static final String ABR_ALGORITHM_DEFAULT = \"default\";\n  public static final String ABR_ALGORITHM_RANDOM = \"random\";\n\n  public static final String SPHERICAL_STEREO_MODE_EXTRA = \"spherical_stereo_mode\";\n  public static final String SPHERICAL_STEREO_MODE_MONO = \"mono\";\n  public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = \"top_bottom\";\n  public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = \"left_right\";\n\n  // For backwards compatibility only.\n  private static final String DRM_SCHEME_UUID_EXTRA = \"drm_scheme_uuid\";\n\n  // Saved instance state keys.\n  private static final String KEY_TRACK_SELECTOR_PARAMETERS = \"track_selector_parameters\";\n  private static final String KEY_WINDOW = \"window\";\n  private static final String KEY_POSITION = \"position\";\n  private static final String KEY_AUTO_PLAY = \"auto_play\";\n\n  private static final CookieManager DEFAULT_COOKIE_MANAGER;\n  static {\n    DEFAULT_COOKIE_MANAGER = new CookieManager();\n    DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);\n  }\n\n  private PlayerView playerView;\n  private LinearLayout debugRootView;\n  private Button selectTracksButton;\n  private TextView debugTextView;\n  private boolean isShowingTrackSelectionDialog;\n\n  private DataSource.Factory dataSourceFactory;\n  private SimpleExoPlayer player;\n  private FrameworkMediaDrm mediaDrm;\n  private MediaSource mediaSource;\n  private DefaultTrackSelector trackSelector;\n  private DefaultTrackSelector.Parameters trackSelectorParameters;\n  private DebugTextViewHelper debugViewHelper;\n  private TrackGroupArray lastSeenTrackGroupArray;\n\n  private boolean startAutoPlay;\n  private int startWindow;\n  private long startPosition;\n\n  // Fields used only for ad playback. The ads loader is loaded via reflection.\n\n  private AdsLoader adsLoader;\n  private Uri loadedAdTagUri;\n\n  private AudioCapabilitiesReceiver audioCapabilitiesReceiver;\n  // Activity lifecycle\n\n  @Override\n  public void onCreate(Bundle savedInstanceState) {\n    String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);\n    if (sphericalStereoMode != null) {\n      setTheme(R.style.PlayerTheme_Spherical);\n    }\n    super.onCreate(savedInstanceState);\n    dataSourceFactory = buildDataSourceFactory();\n    if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {\n      CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);\n    }\n\n    setContentView(R.layout.player_activity);\n    debugRootView = findViewById(R.id.controls_root);\n    debugTextView = findViewById(R.id.debug_text_view);\n    selectTracksButton = findViewById(R.id.select_tracks_button);\n    selectTracksButton.setOnClickListener(this);\n\n    playerView = findViewById(R.id.player_view);\n    playerView.setControllerVisibilityListener(this);\n    playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());\n    playerView.requestFocus();\n    if (sphericalStereoMode != null) {\n      int stereoMode;\n      if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {\n        stereoMode = C.STEREO_MODE_MONO;\n      } else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {\n        stereoMode = C.STEREO_MODE_TOP_BOTTOM;\n      } else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {\n        stereoMode = C.STEREO_MODE_LEFT_RIGHT;\n      } else {\n        showToast(R.string.error_unrecognized_stereo_mode);\n        finish();\n        return;\n      }\n      ((SphericalSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode);\n    }\n\n    if (savedInstanceState != null) {\n      trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS);\n      startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY);\n      startWindow = savedInstanceState.getInt(KEY_WINDOW);\n      startPosition = savedInstanceState.getLong(KEY_POSITION);\n    } else {\n      trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build();\n      clearStartPosition();\n    }\n    audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, this);\n    audioCapabilitiesReceiver.register();\n  }\n\n  @Override\n  public void onNewIntent(Intent intent) {\n    super.onNewIntent(intent);\n    releasePlayer();\n    releaseAdsLoader();\n    clearStartPosition();\n    setIntent(intent);\n  }\n\n  @Override\n  public void onStart() {\n    super.onStart();\n    if (Util.SDK_INT > 23) {\n      initializePlayer();\n      if (playerView != null) {\n        playerView.onResume();\n      }\n    }\n  }\n\n  @Override\n  public void onResume() {\n    super.onResume();\n    if (Util.SDK_INT <= 23 || player == null) {\n      initializePlayer();\n      if (playerView != null) {\n        playerView.onResume();\n      }\n    }\n  }\n\n  @Override\n  public void onPause() {\n    super.onPause();\n    if (Util.SDK_INT <= 23) {\n      if (playerView != null) {\n        playerView.onPause();\n      }\n      releasePlayer();\n    }\n  }\n\n  @Override\n  public void onStop() {\n    super.onStop();\n    if (Util.SDK_INT > 23) {\n      if (playerView != null) {\n        playerView.onPause();\n      }\n      releasePlayer();\n    }\n  }\n\n  @Override\n  public void onDestroy() {\n    super.onDestroy();\n    releaseAdsLoader();\n  }\n\n  @Override\n  public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,\n      @NonNull int[] grantResults) {\n    if (grantResults.length == 0) {\n      // Empty results are triggered if a permission is requested while another request was already\n      // pending and can be safely ignored in this case.\n      return;\n    }\n    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {\n      initializePlayer();\n    } else {\n      showToast(R.string.storage_permission_denied);\n      finish();\n    }\n  }\n\n  @Override\n  public void onSaveInstanceState(Bundle outState) {\n    super.onSaveInstanceState(outState);\n    updateTrackSelectorParameters();\n    updateStartPosition();\n    outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters);\n    outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay);\n    outState.putInt(KEY_WINDOW, startWindow);\n    outState.putLong(KEY_POSITION, startPosition);\n  }\n\n  // Activity input\n\n  @Override\n  public boolean dispatchKeyEvent(KeyEvent event) {\n    // See whether the player view wants to handle media or DPAD keys events.\n    return playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);\n  }\n\n  // OnClickListener methods\n\n  @Override\n  public void onClick(View view) {\n    if (view == selectTracksButton\n        && !isShowingTrackSelectionDialog\n        && TrackSelectionDialog.willHaveContent(trackSelector)) {\n      isShowingTrackSelectionDialog = true;\n      TrackSelectionDialog trackSelectionDialog =\n          TrackSelectionDialog.createForTrackSelector(\n              trackSelector,\n              /* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false);\n      trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null);\n    }\n  }\n\n  // PlaybackControlView.PlaybackPreparer implementation\n\n  @Override\n  public void preparePlayback() {\n    player.retry();\n  }\n\n    // AudioCapabilitiesReceiver.Listener methods\n\n  @Override\n  public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {\n    if (player == null) {\n      return;\n    }\n    //boolean backgrounded = player.getBackgrounded();\n    Log.d(TAG, \"onAudioCapabilitiesChanged(), rebuild pipeline. Caps = \" + audioCapabilities);\n\n    releasePlayer();\n\n    Handler handler = new Handler(Looper.getMainLooper());\n    handler.post(new Runnable() {\n        @Override\n        public void run() {\n           initializePlayer();\n        }\n    });\n  }\n\n  // PlaybackControlView.VisibilityListener implementation\n\n  @Override\n  public void onVisibilityChange(int visibility) {\n    debugRootView.setVisibility(visibility);\n  }\n\n  // Internal methods\n\n  private void initializePlayer() {\n    if (player == null) {\n      Intent intent = getIntent();\n      String action = intent.getAction();\n      Uri[] uris;\n      String[] extensions;\n      if (ACTION_VIEW.equals(action)) {\n        uris = new Uri[] {intent.getData()};\n        extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)};\n      } else if (ACTION_VIEW_LIST.equals(action)) {\n        String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);\n        uris = new Uri[uriStrings.length];\n        for (int i = 0; i < uriStrings.length; i++) {\n          uris[i] = Uri.parse(uriStrings[i]);\n        }\n        extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA);\n        if (extensions == null) {\n          extensions = new String[uriStrings.length];\n        }\n      } else {\n        showToast(getString(R.string.unexpected_intent_action, action));\n        finish();\n        return;\n      }\n      if (!Util.checkCleartextTrafficPermitted(uris)) {\n        showToast(R.string.error_cleartext_not_permitted);\n        return;\n      }\n      if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, uris)) {\n        // The player will be reinitialized if the permission is granted.\n        return;\n      }\n\n      DefaultDrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;\n      if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) {\n        String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA);\n        String[] keyRequestPropertiesArray =\n            intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA);\n        boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA, false);\n        int errorStringId = R.string.error_drm_unknown;\n        if (Util.SDK_INT < 18) {\n          errorStringId = R.string.error_drm_not_supported;\n        } else {\n          try {\n            String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA\n                : DRM_SCHEME_UUID_EXTRA;\n            UUID drmSchemeUuid = Util.getDrmUuid(intent.getStringExtra(drmSchemeExtra));\n            if (drmSchemeUuid == null) {\n              errorStringId = R.string.error_drm_unsupported_scheme;\n            } else {\n              drmSessionManager =\n                  buildDrmSessionManagerV18(\n                      drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession);\n            }\n          } catch (UnsupportedDrmException e) {\n            errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME\n                ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown;\n          }\n        }\n        if (drmSessionManager == null) {\n          showToast(errorStringId);\n          finish();\n          return;\n        }\n      }\n\n      TrackSelection.Factory trackSelectionFactory;\n      String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);\n      if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {\n        trackSelectionFactory = new AdaptiveTrackSelection.Factory();\n      } else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {\n        trackSelectionFactory = new RandomTrackSelection.Factory();\n      } else {\n        showToast(R.string.error_unrecognized_abr_algorithm);\n        finish();\n        return;\n      }\n\n      boolean preferExtensionDecoders =\n          intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);\n      RenderersFactory renderersFactory =\n          ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);\n\n      trackSelector = new DefaultTrackSelector(trackSelectionFactory);\n      trackSelector.setParameters(trackSelectorParameters);\n      lastSeenTrackGroupArray = null;\n\n      player =\n          ExoPlayerFactory.newSimpleInstance(\n              /* context= */ this, renderersFactory, trackSelector, drmSessionManager);\n      player.addListener(new PlayerEventListener());\n      player.setPlayWhenReady(startAutoPlay);\n      player.addAnalyticsListener(new EventLogger(trackSelector));\n      playerView.setPlayer(player);\n      playerView.setPlaybackPreparer(this);\n      debugViewHelper = new DebugTextViewHelper(player, debugTextView);\n      debugViewHelper.start();\n\n      MediaSource[] mediaSources = new MediaSource[uris.length];\n      for (int i = 0; i < uris.length; i++) {\n        mediaSources[i] = buildMediaSource(uris[i], extensions[i]);\n      }\n      mediaSource =\n          mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);\n      String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);\n      if (adTagUriString != null) {\n        Uri adTagUri = Uri.parse(adTagUriString);\n        if (!adTagUri.equals(loadedAdTagUri)) {\n          releaseAdsLoader();\n          loadedAdTagUri = adTagUri;\n        }\n        MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString));\n        if (adsMediaSource != null) {\n          mediaSource = adsMediaSource;\n        } else {\n          showToast(R.string.ima_not_loaded);\n        }\n      } else {\n        releaseAdsLoader();\n      }\n    }\n    boolean haveStartPosition = startWindow != C.INDEX_UNSET;\n    if (haveStartPosition) {\n      player.seekTo(startWindow, startPosition);\n    }\n    player.prepare(mediaSource, !haveStartPosition, false);\n    updateButtonVisibility();\n  }\n\n  private MediaSource buildMediaSource(Uri uri) {\n    return buildMediaSource(uri, null);\n  }\n\n  private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {\n    DownloadRequest downloadRequest =\n        ((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri);\n    if (downloadRequest != null) {\n      return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);\n    }\n    @ContentType int type = Util.inferContentType(uri, overrideExtension);\n    switch (type) {\n      case C.TYPE_DASH:\n        return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);\n      case C.TYPE_SS:\n        return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);\n      case C.TYPE_HLS:\n        return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);\n      case C.TYPE_OTHER:\n        return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);\n      default:\n        throw new IllegalStateException(\"Unsupported type: \" + type);\n    }\n  }\n\n  private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(\n      UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)\n      throws UnsupportedDrmException {\n    HttpDataSource.Factory licenseDataSourceFactory =\n        ((DemoApplication) getApplication()).buildHttpDataSourceFactory();\n    HttpMediaDrmCallback drmCallback =\n        new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory);\n    if (keyRequestPropertiesArray != null) {\n      for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {\n        drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],\n            keyRequestPropertiesArray[i + 1]);\n      }\n    }\n    releaseMediaDrm();\n    mediaDrm = FrameworkMediaDrm.newInstance(uuid);\n    return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession);\n  }\n\n  private void releasePlayer() {\n    if (player != null) {\n      updateTrackSelectorParameters();\n      updateStartPosition();\n      debugViewHelper.stop();\n      debugViewHelper = null;\n      player.release();\n      player = null;\n      mediaSource = null;\n      trackSelector = null;\n    }\n    if (adsLoader != null) {\n      adsLoader.setPlayer(null);\n    }\n    releaseMediaDrm();\n  }\n\n  private void releaseMediaDrm() {\n    if (mediaDrm != null) {\n      mediaDrm.release();\n      mediaDrm = null;\n    }\n  }\n\n  private void releaseAdsLoader() {\n    if (adsLoader != null) {\n      adsLoader.release();\n      adsLoader = null;\n      loadedAdTagUri = null;\n      playerView.getOverlayFrameLayout().removeAllViews();\n    }\n  }\n\n  private void updateTrackSelectorParameters() {\n    if (trackSelector != null) {\n      trackSelectorParameters = trackSelector.getParameters();\n    }\n  }\n\n  private void updateStartPosition() {\n    if (player != null) {\n      startAutoPlay = player.getPlayWhenReady();\n      startWindow = player.getCurrentWindowIndex();\n      startPosition = Math.max(0, player.getContentPosition());\n    }\n  }\n\n  private void clearStartPosition() {\n    startAutoPlay = true;\n    startWindow = C.INDEX_UNSET;\n    startPosition = C.TIME_UNSET;\n  }\n\n  /** Returns a new DataSource factory. */\n  private DataSource.Factory buildDataSourceFactory() {\n    return ((DemoApplication) getApplication()).buildDataSourceFactory();\n  }\n\n  /** Returns an ads media source, reusing the ads loader if one exists. */\n  private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {\n    // Load the extension source using reflection so the demo app doesn't have to depend on it.\n    // The ads loader is reused for multiple playbacks, so that ad playback can resume.\n    try {\n      Class<?> loaderClass = Class.forName(\"com.google.android.exoplayer2.ext.ima.ImaAdsLoader\");\n      if (adsLoader == null) {\n        // Full class names used so the LINT.IfChange rule triggers should any of the classes move.\n        // LINT.IfChange\n        Constructor<? extends AdsLoader> loaderConstructor =\n            loaderClass\n                .asSubclass(AdsLoader.class)\n                .getConstructor(android.content.Context.class, android.net.Uri.class);\n        // LINT.ThenChange(../../../../../../../../proguard-rules.txt)\n        adsLoader = loaderConstructor.newInstance(this, adTagUri);\n      }\n      adsLoader.setPlayer(player);\n      AdsMediaSource.MediaSourceFactory adMediaSourceFactory =\n          new AdsMediaSource.MediaSourceFactory() {\n            @Override\n            public MediaSource createMediaSource(Uri uri) {\n              return PlayerActivity.this.buildMediaSource(uri);\n            }\n\n            @Override\n            public int[] getSupportedTypes() {\n              return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};\n            }\n          };\n      return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, playerView);\n    } catch (ClassNotFoundException e) {\n      // IMA extension not loaded.\n      return null;\n    } catch (Exception e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  // User controls\n\n  private void updateButtonVisibility() {\n    selectTracksButton.setEnabled(\n        player != null && TrackSelectionDialog.willHaveContent(trackSelector));\n  }\n\n  private void showControls() {\n    debugRootView.setVisibility(View.VISIBLE);\n  }\n\n  private void showToast(int messageId) {\n    showToast(getString(messageId));\n  }\n\n  private void showToast(String message) {\n    Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();\n  }\n\n  private static boolean isBehindLiveWindow(ExoPlaybackException e) {\n    if (e.type != ExoPlaybackException.TYPE_SOURCE) {\n      return false;\n    }\n    Throwable cause = e.getSourceException();\n    while (cause != null) {\n      if (cause instanceof BehindLiveWindowException) {\n        return true;\n      }\n      cause = cause.getCause();\n    }\n    return false;\n  }\n\n  private class PlayerEventListener implements Player.EventListener {\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n      if (playbackState == Player.STATE_ENDED) {\n        showControls();\n      }\n      updateButtonVisibility();\n    }\n\n    @Override\n    public void onPlayerError(ExoPlaybackException e) {\n      if (isBehindLiveWindow(e)) {\n        clearStartPosition();\n        initializePlayer();\n      } else {\n        updateButtonVisibility();\n        showControls();\n      }\n    }\n\n    @Override\n    @SuppressWarnings(\"ReferenceEquality\")\n    public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n      updateButtonVisibility();\n      if (trackGroups != lastSeenTrackGroupArray) {\n        MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();\n        if (mappedTrackInfo != null) {\n          if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)\n              == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {\n            showToast(R.string.error_unsupported_video);\n          }\n          if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)\n              == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {\n            showToast(R.string.error_unsupported_audio);\n          }\n        }\n        lastSeenTrackGroupArray = trackGroups;\n      }\n    }\n  }\n\n  private class PlayerErrorMessageProvider implements ErrorMessageProvider<ExoPlaybackException> {\n\n    @Override\n    public Pair<Integer, String> getErrorMessage(ExoPlaybackException e) {\n      String errorString = getString(R.string.error_generic);\n      if (e.type == ExoPlaybackException.TYPE_RENDERER) {\n        Exception cause = e.getRendererException();\n        if (cause instanceof DecoderInitializationException) {\n          // Special case for decoder initialization failures.\n          DecoderInitializationException decoderInitializationException =\n              (DecoderInitializationException) cause;\n          if (decoderInitializationException.decoderName == null) {\n            if (decoderInitializationException.getCause() instanceof DecoderQueryException) {\n              errorString = getString(R.string.error_querying_decoders);\n            } else if (decoderInitializationException.secureDecoderRequired) {\n              errorString =\n                  getString(\n                      R.string.error_no_secure_decoder, decoderInitializationException.mimeType);\n            } else {\n              errorString =\n                  getString(R.string.error_no_decoder, decoderInitializationException.mimeType);\n            }\n          } else {\n            errorString =\n                getString(\n                    R.string.error_instantiating_decoder,\n                    decoderInitializationException.decoderName);\n          }\n        }\n      }\n      return Pair.create(0, errorString);\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.demo;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.res.AssetManager;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.os.Bundle;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\nimport android.util.JsonReader;\nimport android.view.Menu;\nimport android.view.MenuInflater;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.View.OnClickListener;\nimport android.view.ViewGroup;\nimport android.widget.BaseExpandableListAdapter;\nimport android.widget.ExpandableListView;\nimport android.widget.ExpandableListView.OnChildClickListener;\nimport android.widget.ImageButton;\nimport android.widget.TextView;\nimport android.widget.Toast;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.RenderersFactory;\nimport com.google.android.exoplayer2.offline.DownloadService;\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.DefaultDataSource;\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.InputStream;\nimport java.io.InputStreamReader;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\n/** An activity for selecting from a list of media samples. */\npublic class SampleChooserActivity extends AppCompatActivity\n    implements DownloadTracker.Listener, OnChildClickListener {\n\n  private static final String TAG = \"SampleChooserActivity\";\n\n  private boolean useExtensionRenderers;\n  private DownloadTracker downloadTracker;\n  private SampleAdapter sampleAdapter;\n  private MenuItem preferExtensionDecodersMenuItem;\n  private MenuItem randomAbrMenuItem;\n\n  @Override\n  public void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setContentView(R.layout.sample_chooser_activity);\n    sampleAdapter = new SampleAdapter();\n    ExpandableListView sampleListView = findViewById(R.id.sample_list);\n    sampleListView.setAdapter(sampleAdapter);\n    sampleListView.setOnChildClickListener(this);\n\n    Intent intent = getIntent();\n    String dataUri = intent.getDataString();\n    String[] uris;\n    if (dataUri != null) {\n      uris = new String[] {dataUri};\n    } else {\n      ArrayList<String> uriList = new ArrayList<>();\n      AssetManager assetManager = getAssets();\n      try {\n        for (String asset : assetManager.list(\"\")) {\n          if (asset.endsWith(\".exolist.json\")) {\n            uriList.add(\"asset:///\" + asset);\n          }\n        }\n      } catch (IOException e) {\n        Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)\n            .show();\n      }\n      uris = new String[uriList.size()];\n      uriList.toArray(uris);\n      Arrays.sort(uris);\n    }\n\n    DemoApplication application = (DemoApplication) getApplication();\n    useExtensionRenderers = application.useExtensionRenderers();\n    downloadTracker = application.getDownloadTracker();\n    SampleListLoader loaderTask = new SampleListLoader();\n    loaderTask.execute(uris);\n\n    // Start the download service if it should be running but it's not currently.\n    // Starting the service in the foreground causes notification flicker if there is no scheduled\n    // action. Starting it in the background throws an exception if the app is in the background too\n    // (e.g. if device screen is locked).\n    try {\n      DownloadService.start(this, DemoDownloadService.class);\n    } catch (IllegalStateException e) {\n      DownloadService.startForeground(this, DemoDownloadService.class);\n    }\n  }\n\n  @Override\n  public boolean onCreateOptionsMenu(Menu menu) {\n    MenuInflater inflater = getMenuInflater();\n    inflater.inflate(R.menu.sample_chooser_menu, menu);\n    preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders);\n    preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers);\n    randomAbrMenuItem = menu.findItem(R.id.random_abr);\n    return true;\n  }\n\n  @Override\n  public boolean onOptionsItemSelected(MenuItem item) {\n    item.setChecked(!item.isChecked());\n    return true;\n  }\n\n  @Override\n  public void onStart() {\n    super.onStart();\n    downloadTracker.addListener(this);\n    sampleAdapter.notifyDataSetChanged();\n  }\n\n  @Override\n  public void onStop() {\n    downloadTracker.removeListener(this);\n    super.onStop();\n  }\n\n  @Override\n  public void onDownloadsChanged() {\n    sampleAdapter.notifyDataSetChanged();\n  }\n\n  private void onSampleGroups(final List<SampleGroup> groups, boolean sawError) {\n    if (sawError) {\n      Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)\n          .show();\n    }\n    sampleAdapter.setSampleGroups(groups);\n  }\n\n  @Override\n  public boolean onChildClick(\n      ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {\n    Sample sample = (Sample) view.getTag();\n    startActivity(\n        sample.buildIntent(\n            /* context= */ this,\n            isNonNullAndChecked(preferExtensionDecodersMenuItem),\n            isNonNullAndChecked(randomAbrMenuItem)\n                ? PlayerActivity.ABR_ALGORITHM_RANDOM\n                : PlayerActivity.ABR_ALGORITHM_DEFAULT));\n    return true;\n  }\n\n  private void onSampleDownloadButtonClicked(Sample sample) {\n    int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample);\n    if (downloadUnsupportedStringId != 0) {\n      Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)\n          .show();\n    } else {\n      UriSample uriSample = (UriSample) sample;\n      RenderersFactory renderersFactory =\n          ((DemoApplication) getApplication())\n              .buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));\n      downloadTracker.toggleDownload(\n          getSupportFragmentManager(),\n          sample.name,\n          uriSample.uri,\n          uriSample.extension,\n          renderersFactory);\n    }\n  }\n\n  private int getDownloadUnsupportedStringId(Sample sample) {\n    if (sample instanceof PlaylistSample) {\n      return R.string.download_playlist_unsupported;\n    }\n    UriSample uriSample = (UriSample) sample;\n    if (uriSample.drmInfo != null) {\n      return R.string.download_drm_unsupported;\n    }\n    if (uriSample.adTagUri != null) {\n      return R.string.download_ads_unsupported;\n    }\n    String scheme = uriSample.uri.getScheme();\n    if (!(\"http\".equals(scheme) || \"https\".equals(scheme))) {\n      return R.string.download_scheme_unsupported;\n    }\n    return 0;\n  }\n\n  private static boolean isNonNullAndChecked(@Nullable MenuItem menuItem) {\n    // Temporary workaround for layouts that do not inflate the options menu.\n    return menuItem != null && menuItem.isChecked();\n  }\n\n  private final class SampleListLoader extends AsyncTask<String, Void, List<SampleGroup>> {\n\n    private boolean sawError;\n\n    @Override\n    protected List<SampleGroup> doInBackground(String... uris) {\n      List<SampleGroup> result = new ArrayList<>();\n      Context context = getApplicationContext();\n      String userAgent = Util.getUserAgent(context, \"ExoPlayerDemo\");\n      DataSource dataSource =\n          new DefaultDataSource(context, userAgent, /* allowCrossProtocolRedirects= */ false);\n      for (String uri : uris) {\n        DataSpec dataSpec = new DataSpec(Uri.parse(uri));\n        InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);\n        try {\n          readSampleGroups(new JsonReader(new InputStreamReader(inputStream, \"UTF-8\")), result);\n        } catch (Exception e) {\n          Log.e(TAG, \"Error loading sample list: \" + uri, e);\n          sawError = true;\n        } finally {\n          Util.closeQuietly(dataSource);\n        }\n      }\n      return result;\n    }\n\n    @Override\n    protected void onPostExecute(List<SampleGroup> result) {\n      onSampleGroups(result, sawError);\n    }\n\n    private void readSampleGroups(JsonReader reader, List<SampleGroup> groups) throws IOException {\n      reader.beginArray();\n      while (reader.hasNext()) {\n        readSampleGroup(reader, groups);\n      }\n      reader.endArray();\n    }\n\n    private void readSampleGroup(JsonReader reader, List<SampleGroup> groups) throws IOException {\n      String groupName = \"\";\n      ArrayList<Sample> samples = new ArrayList<>();\n\n      reader.beginObject();\n      while (reader.hasNext()) {\n        String name = reader.nextName();\n        switch (name) {\n          case \"name\":\n            groupName = reader.nextString();\n            break;\n          case \"samples\":\n            reader.beginArray();\n            while (reader.hasNext()) {\n              samples.add(readEntry(reader, false));\n            }\n            reader.endArray();\n            break;\n          case \"_comment\":\n            reader.nextString(); // Ignore.\n            break;\n          default:\n            throw new ParserException(\"Unsupported name: \" + name);\n        }\n      }\n      reader.endObject();\n\n      SampleGroup group = getGroup(groupName, groups);\n      group.samples.addAll(samples);\n    }\n\n    private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {\n      String sampleName = null;\n      Uri uri = null;\n      String extension = null;\n      String drmScheme = null;\n      String drmLicenseUrl = null;\n      String[] drmKeyRequestProperties = null;\n      boolean drmMultiSession = false;\n      ArrayList<UriSample> playlistSamples = null;\n      String adTagUri = null;\n      String sphericalStereoMode = null;\n\n      reader.beginObject();\n      while (reader.hasNext()) {\n        String name = reader.nextName();\n        switch (name) {\n          case \"name\":\n            sampleName = reader.nextString();\n            break;\n          case \"uri\":\n            uri = Uri.parse(reader.nextString());\n            break;\n          case \"extension\":\n            extension = reader.nextString();\n            break;\n          case \"drm_scheme\":\n            Assertions.checkState(!insidePlaylist, \"Invalid attribute on nested item: drm_scheme\");\n            drmScheme = reader.nextString();\n            break;\n          case \"drm_license_url\":\n            Assertions.checkState(!insidePlaylist,\n                \"Invalid attribute on nested item: drm_license_url\");\n            drmLicenseUrl = reader.nextString();\n            break;\n          case \"drm_key_request_properties\":\n            Assertions.checkState(!insidePlaylist,\n                \"Invalid attribute on nested item: drm_key_request_properties\");\n            ArrayList<String> drmKeyRequestPropertiesList = new ArrayList<>();\n            reader.beginObject();\n            while (reader.hasNext()) {\n              drmKeyRequestPropertiesList.add(reader.nextName());\n              drmKeyRequestPropertiesList.add(reader.nextString());\n            }\n            reader.endObject();\n            drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]);\n            break;\n          case \"drm_multi_session\":\n            drmMultiSession = reader.nextBoolean();\n            break;\n          case \"playlist\":\n            Assertions.checkState(!insidePlaylist, \"Invalid nesting of playlists\");\n            playlistSamples = new ArrayList<>();\n            reader.beginArray();\n            while (reader.hasNext()) {\n              playlistSamples.add((UriSample) readEntry(reader, true));\n            }\n            reader.endArray();\n            break;\n          case \"ad_tag_uri\":\n            adTagUri = reader.nextString();\n            break;\n          case \"spherical_stereo_mode\":\n            Assertions.checkState(\n                !insidePlaylist, \"Invalid attribute on nested item: spherical_stereo_mode\");\n            sphericalStereoMode = reader.nextString();\n            break;\n          default:\n            throw new ParserException(\"Unsupported attribute name: \" + name);\n        }\n      }\n      reader.endObject();\n      DrmInfo drmInfo =\n          drmScheme == null\n              ? null\n              : new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession);\n      if (playlistSamples != null) {\n        UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);\n        return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray);\n      } else {\n        return new UriSample(\n            sampleName,\n            drmInfo,\n            uri,\n            extension,\n            adTagUri,\n            sphericalStereoMode);\n      }\n    }\n\n    private SampleGroup getGroup(String groupName, List<SampleGroup> groups) {\n      for (int i = 0; i < groups.size(); i++) {\n        if (Util.areEqual(groupName, groups.get(i).title)) {\n          return groups.get(i);\n        }\n      }\n      SampleGroup group = new SampleGroup(groupName);\n      groups.add(group);\n      return group;\n    }\n\n  }\n\n  private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener {\n\n    private List<SampleGroup> sampleGroups;\n\n    public SampleAdapter() {\n      sampleGroups = Collections.emptyList();\n    }\n\n    public void setSampleGroups(List<SampleGroup> sampleGroups) {\n      this.sampleGroups = sampleGroups;\n      notifyDataSetChanged();\n    }\n\n    @Override\n    public Sample getChild(int groupPosition, int childPosition) {\n      return getGroup(groupPosition).samples.get(childPosition);\n    }\n\n    @Override\n    public long getChildId(int groupPosition, int childPosition) {\n      return childPosition;\n    }\n\n    @Override\n    public View getChildView(int groupPosition, int childPosition, boolean isLastChild,\n        View convertView, ViewGroup parent) {\n      View view = convertView;\n      if (view == null) {\n        view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false);\n        View downloadButton = view.findViewById(R.id.download_button);\n        downloadButton.setOnClickListener(this);\n        downloadButton.setFocusable(false);\n      }\n      initializeChildView(view, getChild(groupPosition, childPosition));\n      return view;\n    }\n\n    @Override\n    public int getChildrenCount(int groupPosition) {\n      return getGroup(groupPosition).samples.size();\n    }\n\n    @Override\n    public SampleGroup getGroup(int groupPosition) {\n      return sampleGroups.get(groupPosition);\n    }\n\n    @Override\n    public long getGroupId(int groupPosition) {\n      return groupPosition;\n    }\n\n    @Override\n    public View getGroupView(int groupPosition, boolean isExpanded, View convertView,\n        ViewGroup parent) {\n      View view = convertView;\n      if (view == null) {\n        view =\n            getLayoutInflater()\n                .inflate(android.R.layout.simple_expandable_list_item_1, parent, false);\n      }\n      ((TextView) view).setText(getGroup(groupPosition).title);\n      return view;\n    }\n\n    @Override\n    public int getGroupCount() {\n      return sampleGroups.size();\n    }\n\n    @Override\n    public boolean hasStableIds() {\n      return false;\n    }\n\n    @Override\n    public boolean isChildSelectable(int groupPosition, int childPosition) {\n      return true;\n    }\n\n    @Override\n    public void onClick(View view) {\n      onSampleDownloadButtonClicked((Sample) view.getTag());\n    }\n\n    private void initializeChildView(View view, Sample sample) {\n      view.setTag(sample);\n      TextView sampleTitle = view.findViewById(R.id.sample_title);\n      sampleTitle.setText(sample.name);\n\n      boolean canDownload = getDownloadUnsupportedStringId(sample) == 0;\n      boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri);\n      ImageButton downloadButton = view.findViewById(R.id.download_button);\n      downloadButton.setTag(sample);\n      downloadButton.setColorFilter(\n          canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFFEEEEEE);\n      downloadButton.setImageResource(\n          isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download);\n    }\n  }\n\n  private static final class SampleGroup {\n\n    public final String title;\n    public final List<Sample> samples;\n\n    public SampleGroup(String title) {\n      this.title = title;\n      this.samples = new ArrayList<>();\n    }\n\n  }\n\n  private static final class DrmInfo {\n    public final String drmScheme;\n    public final String drmLicenseUrl;\n    public final String[] drmKeyRequestProperties;\n    public final boolean drmMultiSession;\n\n    public DrmInfo(\n        String drmScheme,\n        String drmLicenseUrl,\n        String[] drmKeyRequestProperties,\n        boolean drmMultiSession) {\n      this.drmScheme = drmScheme;\n      this.drmLicenseUrl = drmLicenseUrl;\n      this.drmKeyRequestProperties = drmKeyRequestProperties;\n      this.drmMultiSession = drmMultiSession;\n    }\n\n    public void updateIntent(Intent intent) {\n      Assertions.checkNotNull(intent);\n      intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmScheme);\n      intent.putExtra(PlayerActivity.DRM_LICENSE_URL_EXTRA, drmLicenseUrl);\n      intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA, drmKeyRequestProperties);\n      intent.putExtra(PlayerActivity.DRM_MULTI_SESSION_EXTRA, drmMultiSession);\n    }\n  }\n\n  private abstract static class Sample {\n    public final String name;\n    public final DrmInfo drmInfo;\n\n    public Sample(String name, DrmInfo drmInfo) {\n      this.name = name;\n      this.drmInfo = drmInfo;\n    }\n\n    public Intent buildIntent(\n        Context context, boolean preferExtensionDecoders, String abrAlgorithm) {\n      Intent intent = new Intent(context, PlayerActivity.class);\n      intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders);\n      intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);\n      if (drmInfo != null) {\n        drmInfo.updateIntent(intent);\n      }\n      return intent;\n    }\n\n  }\n\n  private static final class UriSample extends Sample {\n\n    public final Uri uri;\n    public final String extension;\n    public final String adTagUri;\n    public final String sphericalStereoMode;\n\n    public UriSample(\n        String name,\n        DrmInfo drmInfo,\n        Uri uri,\n        String extension,\n        String adTagUri,\n        String sphericalStereoMode) {\n      super(name, drmInfo);\n      this.uri = uri;\n      this.extension = extension;\n      this.adTagUri = adTagUri;\n      this.sphericalStereoMode = sphericalStereoMode;\n    }\n\n    @Override\n    public Intent buildIntent(\n        Context context, boolean preferExtensionDecoders, String abrAlgorithm) {\n      return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)\n          .setData(uri)\n          .putExtra(PlayerActivity.EXTENSION_EXTRA, extension)\n          .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)\n          .putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode)\n          .setAction(PlayerActivity.ACTION_VIEW);\n    }\n\n  }\n\n  private static final class PlaylistSample extends Sample {\n\n    public final UriSample[] children;\n\n    public PlaylistSample(\n        String name,\n        DrmInfo drmInfo,\n        UriSample... children) {\n      super(name, drmInfo);\n      this.children = children;\n    }\n\n    @Override\n    public Intent buildIntent(\n        Context context, boolean preferExtensionDecoders, String abrAlgorithm) {\n      String[] uris = new String[children.length];\n      String[] extensions = new String[children.length];\n      for (int i = 0; i < children.length; i++) {\n        uris[i] = children[i].uri.toString();\n        extensions[i] = children[i].extension;\n      }\n      return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)\n          .putExtra(PlayerActivity.URI_LIST_EXTRA, uris)\n          .putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions)\n          .setAction(PlayerActivity.ACTION_VIEW_LIST);\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.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.demo;\n\nimport android.app.Dialog;\nimport android.content.DialogInterface;\nimport android.content.res.Resources;\nimport android.os.Bundle;\nimport androidx.annotation.Nullable;\nimport com.google.android.material.tabs.TabLayout;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.fragment.app.Fragment;\nimport androidx.fragment.app.FragmentManager;\nimport androidx.fragment.app.FragmentPagerAdapter;\nimport androidx.viewpager.widget.ViewPager;\nimport androidx.appcompat.app.AppCompatDialog;\nimport android.util.SparseArray;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.Button;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;\nimport com.google.android.exoplayer2.ui.TrackSelectionView;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/** Dialog to select tracks. */\npublic final class TrackSelectionDialog extends DialogFragment {\n\n  private final SparseArray<TrackSelectionViewFragment> tabFragments;\n  private final ArrayList<Integer> tabTrackTypes;\n\n  private int titleId;\n  private DialogInterface.OnClickListener onClickListener;\n  private DialogInterface.OnDismissListener onDismissListener;\n\n  /**\n   * Returns whether a track selection dialog will have content to display if initialized with the\n   * specified {@link DefaultTrackSelector} in its current state.\n   */\n  public static boolean willHaveContent(DefaultTrackSelector trackSelector) {\n    MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();\n    return mappedTrackInfo != null && willHaveContent(mappedTrackInfo);\n  }\n\n  /**\n   * Returns whether a track selection dialog will have content to display if initialized with the\n   * specified {@link MappedTrackInfo}.\n   */\n  public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) {\n    for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {\n      if (showTabForRenderer(mappedTrackInfo, i)) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Creates a dialog for a given {@link DefaultTrackSelector}, whose parameters will be\n   * automatically updated when tracks are selected.\n   *\n   * @param trackSelector The {@link DefaultTrackSelector}.\n   * @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is\n   *     dismissed.\n   */\n  public static TrackSelectionDialog createForTrackSelector(\n      DefaultTrackSelector trackSelector, DialogInterface.OnDismissListener onDismissListener) {\n    MappedTrackInfo mappedTrackInfo =\n        Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());\n    TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog();\n    DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();\n    trackSelectionDialog.init(\n        /* titleId= */ R.string.track_selection_title,\n        mappedTrackInfo,\n        /* initialParameters = */ parameters,\n        /* allowAdaptiveSelections =*/ true,\n        /* allowMultipleOverrides= */ false,\n        /* onClickListener= */ (dialog, which) -> {\n          DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon();\n          for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {\n            builder\n                .clearSelectionOverrides(/* rendererIndex= */ i)\n                .setRendererDisabled(\n                    /* rendererIndex= */ i,\n                    trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i));\n            List<SelectionOverride> overrides =\n                trackSelectionDialog.getOverrides(/* rendererIndex= */ i);\n            if (!overrides.isEmpty()) {\n              builder.setSelectionOverride(\n                  /* rendererIndex= */ i,\n                  mappedTrackInfo.getTrackGroups(/* rendererIndex= */ i),\n                  overrides.get(0));\n            }\n          }\n          trackSelector.setParameters(builder);\n        },\n        onDismissListener);\n    return trackSelectionDialog;\n  }\n\n  /**\n   * Creates a dialog for given {@link MappedTrackInfo} and {@link DefaultTrackSelector.Parameters}.\n   *\n   * @param titleId The resource id of the dialog title.\n   * @param mappedTrackInfo The {@link MappedTrackInfo} to display.\n   * @param initialParameters The {@link DefaultTrackSelector.Parameters} describing the initial\n   *     track selection.\n   * @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track)\n   *     can be made.\n   * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected.\n   * @param onClickListener {@link DialogInterface.OnClickListener} called when tracks are selected.\n   * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is\n   *     dismissed.\n   */\n  public static TrackSelectionDialog createForMappedTrackInfoAndParameters(\n      int titleId,\n      MappedTrackInfo mappedTrackInfo,\n      DefaultTrackSelector.Parameters initialParameters,\n      boolean allowAdaptiveSelections,\n      boolean allowMultipleOverrides,\n      DialogInterface.OnClickListener onClickListener,\n      DialogInterface.OnDismissListener onDismissListener) {\n    TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog();\n    trackSelectionDialog.init(\n        titleId,\n        mappedTrackInfo,\n        initialParameters,\n        allowAdaptiveSelections,\n        allowMultipleOverrides,\n        onClickListener,\n        onDismissListener);\n    return trackSelectionDialog;\n  }\n\n  public TrackSelectionDialog() {\n    tabFragments = new SparseArray<>();\n    tabTrackTypes = new ArrayList<>();\n    // Retain instance across activity re-creation to prevent losing access to init data.\n    setRetainInstance(true);\n  }\n\n  private void init(\n      int titleId,\n      MappedTrackInfo mappedTrackInfo,\n      DefaultTrackSelector.Parameters initialParameters,\n      boolean allowAdaptiveSelections,\n      boolean allowMultipleOverrides,\n      DialogInterface.OnClickListener onClickListener,\n      DialogInterface.OnDismissListener onDismissListener) {\n    this.titleId = titleId;\n    this.onClickListener = onClickListener;\n    this.onDismissListener = onDismissListener;\n    for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {\n      if (showTabForRenderer(mappedTrackInfo, i)) {\n        int trackType = mappedTrackInfo.getRendererType(/* rendererIndex= */ i);\n        TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i);\n        TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment();\n        tabFragment.init(\n            mappedTrackInfo,\n            /* rendererIndex= */ i,\n            initialParameters.getRendererDisabled(/* rendererIndex= */ i),\n            initialParameters.getSelectionOverride(/* rendererIndex= */ i, trackGroupArray),\n            allowAdaptiveSelections,\n            allowMultipleOverrides);\n        tabFragments.put(i, tabFragment);\n        tabTrackTypes.add(trackType);\n      }\n    }\n  }\n\n  /**\n   * Returns whether a renderer is disabled.\n   *\n   * @param rendererIndex Renderer index.\n   * @return Whether the renderer is disabled.\n   */\n  public boolean getIsDisabled(int rendererIndex) {\n    TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex);\n    return rendererView != null && rendererView.isDisabled;\n  }\n\n  /**\n   * Returns the list of selected track selection overrides for the specified renderer. There will\n   * be at most one override for each track group.\n   *\n   * @param rendererIndex Renderer index.\n   * @return The list of track selection overrides for this renderer.\n   */\n  public List<SelectionOverride> getOverrides(int rendererIndex) {\n    TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex);\n    return rendererView == null ? Collections.emptyList() : rendererView.overrides;\n  }\n\n  @Override\n  public Dialog onCreateDialog(Bundle savedInstanceState) {\n    // We need to own the view to let tab layout work correctly on all API levels. We can't use\n    // AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using\n    // the AlertDialog theme overlay with force-enabled title.\n    AppCompatDialog dialog =\n        new AppCompatDialog(getActivity(), R.style.TrackSelectionDialogThemeOverlay);\n    dialog.setTitle(titleId);\n    return dialog;\n  }\n\n  @Override\n  public void onDismiss(DialogInterface dialog) {\n    super.onDismiss(dialog);\n    onDismissListener.onDismiss(dialog);\n  }\n\n  @Nullable\n  @Override\n  public View onCreateView(\n      LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n\n    View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false);\n    TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout);\n    ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager);\n    Button cancelButton = dialogView.findViewById(R.id.track_selection_dialog_cancel_button);\n    Button okButton = dialogView.findViewById(R.id.track_selection_dialog_ok_button);\n    viewPager.setAdapter(new FragmentAdapter(getChildFragmentManager()));\n    tabLayout.setupWithViewPager(viewPager);\n    tabLayout.setVisibility(tabFragments.size() > 1 ? View.VISIBLE : View.GONE);\n    cancelButton.setOnClickListener(view -> dismiss());\n    okButton.setOnClickListener(\n        view -> {\n          onClickListener.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);\n          dismiss();\n        });\n    return dialogView;\n  }\n\n  private static boolean showTabForRenderer(MappedTrackInfo mappedTrackInfo, int rendererIndex) {\n    TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex);\n    if (trackGroupArray.length == 0) {\n      return false;\n    }\n    int trackType = mappedTrackInfo.getRendererType(rendererIndex);\n    return isSupportedTrackType(trackType);\n  }\n\n  private static boolean isSupportedTrackType(int trackType) {\n    switch (trackType) {\n      case C.TRACK_TYPE_VIDEO:\n      case C.TRACK_TYPE_AUDIO:\n      case C.TRACK_TYPE_TEXT:\n        return true;\n      default:\n        return false;\n    }\n  }\n\n  private static String getTrackTypeString(Resources resources, int trackType) {\n    switch (trackType) {\n      case C.TRACK_TYPE_VIDEO:\n        return resources.getString(R.string.exo_track_selection_title_video);\n      case C.TRACK_TYPE_AUDIO:\n        return resources.getString(R.string.exo_track_selection_title_audio);\n      case C.TRACK_TYPE_TEXT:\n        return resources.getString(R.string.exo_track_selection_title_text);\n      default:\n        throw new IllegalArgumentException();\n    }\n  }\n\n  private final class FragmentAdapter extends FragmentPagerAdapter {\n\n    public FragmentAdapter(FragmentManager fragmentManager) {\n      super(fragmentManager);\n    }\n\n    @Override\n    public Fragment getItem(int position) {\n      return tabFragments.valueAt(position);\n    }\n\n    @Override\n    public int getCount() {\n      return tabFragments.size();\n    }\n\n    @Nullable\n    @Override\n    public CharSequence getPageTitle(int position) {\n      return getTrackTypeString(getResources(), tabTrackTypes.get(position));\n    }\n  }\n\n  /** Fragment to show a track selection in tab of the track selection dialog. */\n  public static final class TrackSelectionViewFragment extends Fragment\n      implements TrackSelectionView.TrackSelectionListener {\n\n    private MappedTrackInfo mappedTrackInfo;\n    private int rendererIndex;\n    private boolean allowAdaptiveSelections;\n    private boolean allowMultipleOverrides;\n\n    /* package */ boolean isDisabled;\n    /* package */ List<SelectionOverride> overrides;\n\n    public TrackSelectionViewFragment() {\n      // Retain instance across activity re-creation to prevent losing access to init data.\n      setRetainInstance(true);\n    }\n\n    public void init(\n        MappedTrackInfo mappedTrackInfo,\n        int rendererIndex,\n        boolean initialIsDisabled,\n        @Nullable SelectionOverride initialOverride,\n        boolean allowAdaptiveSelections,\n        boolean allowMultipleOverrides) {\n      this.mappedTrackInfo = mappedTrackInfo;\n      this.rendererIndex = rendererIndex;\n      this.isDisabled = initialIsDisabled;\n      this.overrides =\n          initialOverride == null\n              ? Collections.emptyList()\n              : Collections.singletonList(initialOverride);\n      this.allowAdaptiveSelections = allowAdaptiveSelections;\n      this.allowMultipleOverrides = allowMultipleOverrides;\n    }\n\n    @Nullable\n    @Override\n    public View onCreateView(\n        LayoutInflater inflater,\n        @Nullable ViewGroup container,\n        @Nullable Bundle savedInstanceState) {\n      View rootView =\n          inflater.inflate(\n              R.layout.exo_track_selection_dialog, container, /* attachToRoot= */ false);\n      TrackSelectionView trackSelectionView = rootView.findViewById(R.id.exo_track_selection_view);\n      trackSelectionView.setShowDisableOption(true);\n      trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides);\n      trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections);\n      trackSelectionView.init(\n          mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this);\n      return rootView;\n    }\n\n    @Override\n    public void onTrackSelectionChanged(boolean isDisabled, List<SelectionOverride> overrides) {\n      this.isDisabled = isDisabled;\n      this.overrides = overrides;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/res/layout/player_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/root\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:keepScreenOn=\"true\">\n\n  <com.google.android.exoplayer2.ui.PlayerView android:id=\"@+id/player_view\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"/>\n\n  <LinearLayout\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:background=\"#88000000\"\n      android:orientation=\"vertical\">\n\n    <TextView android:id=\"@+id/debug_text_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:paddingLeft=\"4dp\"\n        android:paddingRight=\"4dp\"\n        android:textSize=\"10sp\"\n        tools:ignore=\"SmallSp\"/>\n\n    <LinearLayout android:id=\"@+id/controls_root\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\"\n        android:visibility=\"gone\">\n\n      <Button android:id=\"@+id/select_tracks_button\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/track_selection_title\"\n          android:enabled=\"false\"/>\n\n    </LinearLayout>\n\n  </LinearLayout>\n\n</FrameLayout>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/res/layout/sample_chooser_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n  <ExpandableListView android:id=\"@+id/sample_list\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"/>\n\n</LinearLayout>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/res/layout/sample_list_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingStart=\"12dp\"\n    android:paddingEnd=\"12dp\"\n    android:gravity=\"center_vertical\"\n    android:orientation=\"horizontal\">\n\n  <TextView android:id=\"@+id/sample_title\"\n      android:layout_width=\"0dp\"\n      android:layout_height=\"wrap_content\"\n      android:layout_weight=\"1\"\n      android:gravity=\"center_vertical\"\n      android:minHeight=\"?android:attr/listPreferredItemHeightSmall\"\n      android:textAppearance=\"?android:attr/textAppearanceListItemSmall\"/>\n\n  <ImageButton android:id=\"@+id/download_button\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:contentDescription=\"@string/exo_download_description\"\n      android:background=\"@android:color/transparent\"/>\n\n</LinearLayout>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/res/layout/track_selection_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n  <androidx.viewpager.widget.ViewPager\n      android:id=\"@+id/track_selection_dialog_view_pager\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"0dp\"\n      android:layout_weight=\"1\">\n\n    <com.google.android.material.tabs.TabLayout\n        android:id=\"@+id/track_selection_dialog_tab_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:tabGravity=\"fill\"\n        app:tabMode=\"fixed\"/>\n\n  </androidx.viewpager.widget.ViewPager>\n\n  <LinearLayout\n      android:orientation=\"horizontal\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:gravity=\"end\">\n\n    <Button\n        android:id=\"@+id/track_selection_dialog_cancel_button\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@android:string/cancel\"\n        style=\"?android:attr/borderlessButtonStyle\"/>\n\n    <Button\n        android:id=\"@+id/track_selection_dialog_ok_button\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@android:string/ok\"\n        style=\"?android:attr/borderlessButtonStyle\"/>\n\n  </LinearLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/res/menu/sample_chooser_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n  <item android:id=\"@+id/prefer_extension_decoders\"\n      android:title=\"@string/prefer_extension_decoders\"\n      android:checkable=\"true\"\n      app:showAsAction=\"never\"/>\n  <item android:id=\"@+id/random_abr\"\n      android:title=\"@string/random_abr\"\n      android:checkable=\"true\"\n      app:showAsAction=\"never\"/>\n</menu>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n\n  <string name=\"application_name\">ExoPlayer</string>\n\n  <string name=\"track_selection_title\">Select tracks</string>\n\n  <string name=\"unexpected_intent_action\">Unexpected intent action: <xliff:g id=\"action\">%1$s</xliff:g></string>\n\n  <string name=\"error_cleartext_not_permitted\">Cleartext traffic not permitted</string>\n\n  <string name=\"error_generic\">Playback failed</string>\n\n  <string name=\"error_unrecognized_abr_algorithm\">Unrecognized ABR algorithm</string>\n\n  <string name=\"error_unrecognized_stereo_mode\">Unrecognized stereo mode</string>\n\n  <string name=\"error_drm_not_supported\">Protected content not supported on API levels below 18</string>\n\n  <string name=\"error_drm_unsupported_scheme\">This device does not support the required DRM scheme</string>\n\n  <string name=\"error_drm_unknown\">An unknown DRM error occurred</string>\n\n  <string name=\"error_no_decoder\">This device does not provide a decoder for <xliff:g id=\"mime_type\">%1$s</xliff:g></string>\n\n  <string name=\"error_no_secure_decoder\">This device does not provide a secure decoder for <xliff:g id=\"mime_type\">%1$s</xliff:g></string>\n\n  <string name=\"error_querying_decoders\">Unable to query device decoders</string>\n\n  <string name=\"error_instantiating_decoder\">Unable to instantiate decoder <xliff:g id=\"decoder_name\">%1$s</xliff:g></string>\n\n  <string name=\"error_unsupported_video\">Media includes video tracks, but none are playable by this device</string>\n\n  <string name=\"error_unsupported_audio\">Media includes audio tracks, but none are playable by this device</string>\n\n  <string name=\"storage_permission_denied\">Permission to access storage was denied</string>\n\n  <string name=\"sample_list_load_error\">One or more sample lists failed to load</string>\n\n  <string name=\"ima_not_loaded\">Playing sample without ads, as the IMA extension was not loaded</string>\n\n  <string name=\"download_start_error\">Failed to start download</string>\n\n  <string name=\"download_playlist_unsupported\">This demo app does not support downloading playlists</string>\n\n  <string name=\"download_drm_unsupported\">This demo app does not support downloading protected content</string>\n\n  <string name=\"download_scheme_unsupported\">This demo app only supports downloading http streams</string>\n\n  <string name=\"download_ads_unsupported\">IMA does not support offline ads</string>\n\n  <string name=\"prefer_extension_decoders\">Prefer extension decoders</string>\n\n  <string name=\"random_abr\">Enable random ABR</string>\n\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/demos/main/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<resources xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n  <style name=\"TrackSelectionDialogThemeOverlay\" parent=\"ThemeOverlay.AppCompat.Dialog.Alert\">\n    <item name=\"windowNoTitle\">false</item>\n  </style>\n\n  <style name=\"PlayerTheme\" parent=\"Theme.AppCompat.NoActionBar\">\n    <item name=\"android:windowBackground\">@android:color/black</item>\n  </style>\n\n  <style name=\"PlayerTheme.Spherical\">\n    <item name=\"surface_type\">spherical_view</item>\n  </style>\n\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/README.md",
    "content": "# ExoPlayer extensions #\n\nExoPlayer extensions are modules that depend on external libraries to provide\nadditional functionality. Browse the individual extensions and their READMEs to\nlearn more.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/README.md",
    "content": "# ExoPlayer Cast extension #\n\n## Description ##\n\nThe cast extension is a [Player][] implementation that controls playback on a\nCast receiver app.\n\n[Player]: https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/Player.html\n\n## Getting the extension ##\n\nThe easiest way to use the extension is to add it as a gradle dependency:\n\n```gradle\nimplementation 'com.google.android.exoplayer:extension-cast:2.X.X'\n```\n\nwhere `2.X.X` is the version, which must match the version of the ExoPlayer\nlibrary being used.\n\nAlternatively, you can clone the ExoPlayer repository and depend on the module\nlocally. Instructions for doing this can be found in ExoPlayer's\n[top level README][].\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n\n## Using the extension ##\n\nCreate a `CastPlayer` and use it to integrate Cast into your app using\nExoPlayer's common `Player` interface.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/build.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.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    api 'com.google.android.gms:play-services-cast-framework:17.0.0'\n    implementation 'androidx.annotation:annotation:1.1.0'\n    implementation project(modulePrefix + 'library-core')\n    implementation project(modulePrefix + 'library-ui')\n    compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion\n    compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n}\n\next {\n    javadocTitle = 'Cast extension'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'extension-cast'\n    releaseDescription = 'Cast extension for ExoPlayer.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n<manifest package=\"com.google.android.exoplayer2.ext.cast\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.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.ext.cast;\n\nimport android.os.Looper;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.BasePlayer;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.FixedTrackSelection;\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.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.gms.cast.CastStatusCodes;\nimport com.google.android.gms.cast.MediaInfo;\nimport com.google.android.gms.cast.MediaQueueItem;\nimport com.google.android.gms.cast.MediaStatus;\nimport com.google.android.gms.cast.MediaTrack;\nimport com.google.android.gms.cast.framework.CastContext;\nimport com.google.android.gms.cast.framework.CastSession;\nimport com.google.android.gms.cast.framework.SessionManager;\nimport com.google.android.gms.cast.framework.SessionManagerListener;\nimport com.google.android.gms.cast.framework.media.RemoteMediaClient;\nimport com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;\nimport com.google.android.gms.common.api.PendingResult;\nimport com.google.android.gms.common.api.ResultCallback;\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\n/**\n * {@link Player} implementation that communicates with a Cast receiver app.\n *\n * <p>The behavior of this class depends on the underlying Cast session, which is obtained from the\n * Cast context passed to {@link #CastPlayer}. To keep track of the session, {@link\n * #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be\n * implemented and attached to the player.\n *\n * <p>If no session is available, the player state will remain unchanged and calls to methods that\n * alter it will be ignored. Querying the player state is possible even when no session is\n * available, in which case, the last observed receiver app state is reported.\n *\n * <p>Methods should be called on the application's main thread.\n */\npublic final class CastPlayer extends BasePlayer {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.cast\");\n  }\n\n  private static final String TAG = \"CastPlayer\";\n\n  private static final int RENDERER_COUNT = 3;\n  private static final int RENDERER_INDEX_VIDEO = 0;\n  private static final int RENDERER_INDEX_AUDIO = 1;\n  private static final int RENDERER_INDEX_TEXT = 2;\n  private static final long PROGRESS_REPORT_PERIOD_MS = 1000;\n  private static final TrackSelectionArray EMPTY_TRACK_SELECTION_ARRAY =\n      new TrackSelectionArray(null, null, null);\n  private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];\n\n  private final CastContext castContext;\n  // TODO: Allow custom implementations of CastTimelineTracker.\n  private final CastTimelineTracker timelineTracker;\n  private final Timeline.Period period;\n\n  // Result callbacks.\n  private final StatusListener statusListener;\n  private final SeekResultCallback seekResultCallback;\n\n  // Listeners and notification.\n  private final CopyOnWriteArrayList<ListenerHolder> listeners;\n  private final ArrayList<ListenerNotificationTask> notificationsBatch;\n  private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;\n  @Nullable private SessionAvailabilityListener sessionAvailabilityListener;\n\n  // Internal state.\n  @Nullable private RemoteMediaClient remoteMediaClient;\n  private CastTimeline currentTimeline;\n  private TrackGroupArray currentTrackGroups;\n  private TrackSelectionArray currentTrackSelection;\n  private int playbackState;\n  private int repeatMode;\n  private int currentWindowIndex;\n  private boolean playWhenReady;\n  private long lastReportedPositionMs;\n  private int pendingSeekCount;\n  private int pendingSeekWindowIndex;\n  private long pendingSeekPositionMs;\n  private boolean waitingForInitialTimeline;\n\n  /**\n   * @param castContext The context from which the cast session is obtained.\n   */\n  public CastPlayer(CastContext castContext) {\n    this.castContext = castContext;\n    timelineTracker = new CastTimelineTracker();\n    period = new Timeline.Period();\n    statusListener = new StatusListener();\n    seekResultCallback = new SeekResultCallback();\n    listeners = new CopyOnWriteArrayList<>();\n    notificationsBatch = new ArrayList<>();\n    ongoingNotificationsTasks = new ArrayDeque<>();\n\n    SessionManager sessionManager = castContext.getSessionManager();\n    sessionManager.addSessionManagerListener(statusListener, CastSession.class);\n    CastSession session = sessionManager.getCurrentCastSession();\n    remoteMediaClient = session != null ? session.getRemoteMediaClient() : null;\n\n    playbackState = STATE_IDLE;\n    repeatMode = REPEAT_MODE_OFF;\n    currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;\n    currentTrackGroups = TrackGroupArray.EMPTY;\n    currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;\n    pendingSeekWindowIndex = C.INDEX_UNSET;\n    pendingSeekPositionMs = C.TIME_UNSET;\n    updateInternalState();\n  }\n\n  // Media Queue manipulation methods.\n\n  /**\n   * Loads a single item media queue. If no session is available, does nothing.\n   *\n   * @param item The item to load.\n   * @param positionMs The position at which the playback should start in milliseconds relative to\n   *     the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback\n   *     starts at position 0.\n   * @return The Cast {@code PendingResult}, or null if no session is available.\n   */\n  @Nullable\n  public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {\n    return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);\n  }\n\n  /**\n   * Loads a media queue. If no session is available, does nothing.\n   *\n   * @param items The items to load.\n   * @param startIndex The index of the item at which playback should start.\n   * @param positionMs The position at which the playback should start in milliseconds relative to\n   *     the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback\n   *     starts at position 0.\n   * @param repeatMode The repeat mode for the created media queue.\n   * @return The Cast {@code PendingResult}, or null if no session is available.\n   */\n  @Nullable\n  public PendingResult<MediaChannelResult> loadItems(\n      MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {\n    if (remoteMediaClient != null) {\n      positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;\n      waitingForInitialTimeline = true;\n      return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode),\n          positionMs, null);\n    }\n    return null;\n  }\n\n  /**\n   * Appends a sequence of items to the media queue. If no media queue exists, does nothing.\n   *\n   * @param items The items to append.\n   * @return The Cast {@code PendingResult}, or null if no media queue exists.\n   */\n  @Nullable\n  public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {\n    return addItems(MediaQueueItem.INVALID_ITEM_ID, items);\n  }\n\n  /**\n   * Inserts a sequence of items into the media queue. If no media queue or period with id {@code\n   * periodId} exist, does nothing.\n   *\n   * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item\n   *     that will follow immediately after the inserted items.\n   * @param items The items to insert.\n   * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code\n   *     periodId} exist.\n   */\n  @Nullable\n  public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {\n    if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID\n        || currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {\n      return remoteMediaClient.queueInsertItems(items, periodId, null);\n    }\n    return null;\n  }\n\n  /**\n   * Removes an item from the media queue. If no media queue or period with id {@code periodId}\n   * exist, does nothing.\n   *\n   * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item\n   *     to remove.\n   * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code\n   *     periodId} exist.\n   */\n  @Nullable\n  public PendingResult<MediaChannelResult> removeItem(int periodId) {\n    if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {\n      return remoteMediaClient.queueRemoveItem(periodId, null);\n    }\n    return null;\n  }\n\n  /**\n   * Moves an existing item within the media queue. If no media queue or period with id {@code\n   * periodId} exist, does nothing.\n   *\n   * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item\n   *     to move.\n   * @param newIndex The target index of the item in the media queue. Must be in the range 0 &lt;=\n   *     index &lt; {@link Timeline#getPeriodCount()}, as provided by {@link #getCurrentTimeline()}.\n   * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code\n   *     periodId} exist.\n   */\n  @Nullable\n  public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {\n    Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());\n    if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {\n      return remoteMediaClient.queueMoveItemToNewIndex(periodId, newIndex, null);\n    }\n    return null;\n  }\n\n  /**\n   * Returns the item that corresponds to the period with the given id, or null if no media queue or\n   * period with id {@code periodId} exist.\n   *\n   * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item\n   *     to get.\n   * @return The item that corresponds to the period with the given id, or null if no media queue or\n   *     period with id {@code periodId} exist.\n   */\n  @Nullable\n  public MediaQueueItem getItem(int periodId) {\n    MediaStatus mediaStatus = getMediaStatus();\n    return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET\n        ? mediaStatus.getItemById(periodId) : null;\n  }\n\n  // CastSession methods.\n\n  /**\n   * Returns whether a cast session is available.\n   */\n  public boolean isCastSessionAvailable() {\n    return remoteMediaClient != null;\n  }\n\n  /**\n   * Sets a listener for updates on the cast session availability.\n   *\n   * @param listener The {@link SessionAvailabilityListener}, or null to clear the listener.\n   */\n  public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {\n    sessionAvailabilityListener = listener;\n  }\n\n  // Player implementation.\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 getApplicationLooper() {\n    return Looper.getMainLooper();\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  public int getPlaybackState() {\n    return playbackState;\n  }\n\n  @Override\n  @PlaybackSuppressionReason\n  public int getPlaybackSuppressionReason() {\n    return Player.PLAYBACK_SUPPRESSION_REASON_NONE;\n  }\n\n  @Override\n  @Nullable\n  public ExoPlaybackException getPlaybackError() {\n    return null;\n  }\n\n  @Override\n  public void setPlayWhenReady(boolean playWhenReady) {\n    if (remoteMediaClient == null) {\n      return;\n    }\n    if (playWhenReady) {\n      remoteMediaClient.play();\n    } else {\n      remoteMediaClient.pause();\n    }\n  }\n\n  @Override\n  public boolean getPlayWhenReady() {\n    return playWhenReady;\n  }\n\n  @Override\n  public void seekTo(int windowIndex, long positionMs) {\n    MediaStatus mediaStatus = getMediaStatus();\n    // We assume the default position is 0. There is no support for seeking to the default position\n    // in RemoteMediaClient.\n    positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;\n    if (mediaStatus != null) {\n      if (getCurrentWindowIndex() != windowIndex) {\n        remoteMediaClient.queueJumpToItem((int) currentTimeline.getPeriod(windowIndex, period).uid,\n            positionMs, null).setResultCallback(seekResultCallback);\n      } else {\n        remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback);\n      }\n      pendingSeekCount++;\n      pendingSeekWindowIndex = windowIndex;\n      pendingSeekPositionMs = positionMs;\n      notificationsBatch.add(\n          new ListenerNotificationTask(\n              listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));\n    } else if (pendingSeekCount == 0) {\n      notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));\n    }\n    flushNotifications();\n  }\n\n  @Override\n  public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {\n    // Unsupported by the RemoteMediaClient API. Do nothing.\n  }\n\n  @Override\n  public PlaybackParameters getPlaybackParameters() {\n    return PlaybackParameters.DEFAULT;\n  }\n\n  @Override\n  public void stop(boolean reset) {\n    playbackState = STATE_IDLE;\n    if (remoteMediaClient != null) {\n      // TODO(b/69792021): Support or emulate stop without position reset.\n      remoteMediaClient.stop();\n    }\n  }\n\n  @Override\n  public void release() {\n    SessionManager sessionManager = castContext.getSessionManager();\n    sessionManager.removeSessionManagerListener(statusListener, CastSession.class);\n    sessionManager.endCurrentSession(false);\n  }\n\n  @Override\n  public int getRendererCount() {\n    // We assume there are three renderers: video, audio, and text.\n    return RENDERER_COUNT;\n  }\n\n  @Override\n  public int getRendererType(int index) {\n    switch (index) {\n      case RENDERER_INDEX_VIDEO:\n        return C.TRACK_TYPE_VIDEO;\n      case RENDERER_INDEX_AUDIO:\n        return C.TRACK_TYPE_AUDIO;\n      case RENDERER_INDEX_TEXT:\n        return C.TRACK_TYPE_TEXT;\n      default:\n        throw new IndexOutOfBoundsException();\n    }\n  }\n\n  @Override\n  public void setRepeatMode(@RepeatMode int repeatMode) {\n    if (remoteMediaClient != null) {\n      remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), null);\n    }\n  }\n\n  @Override\n  @RepeatMode public int getRepeatMode() {\n    return repeatMode;\n  }\n\n  @Override\n  public void setShuffleModeEnabled(boolean shuffleModeEnabled) {\n    // TODO: Support shuffle mode.\n  }\n\n  @Override\n  public boolean getShuffleModeEnabled() {\n    // TODO: Support shuffle mode.\n    return false;\n  }\n\n  @Override\n  public TrackSelectionArray getCurrentTrackSelections() {\n    return currentTrackSelection;\n  }\n\n  @Override\n  public TrackGroupArray getCurrentTrackGroups() {\n    return currentTrackGroups;\n  }\n\n  @Override\n  public Timeline getCurrentTimeline() {\n    return currentTimeline;\n  }\n\n  @Override\n  @Nullable public Object getCurrentManifest() {\n    return null;\n  }\n\n  @Override\n  public int getCurrentPeriodIndex() {\n    return getCurrentWindowIndex();\n  }\n\n  @Override\n  public int getCurrentWindowIndex() {\n    return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex;\n  }\n\n  // TODO: Fill the cast timeline information with ProgressListener's duration updates.\n  // See [Internal: b/65152553].\n  @Override\n  public long getDuration() {\n    return getContentDuration();\n  }\n\n  @Override\n  public long getCurrentPosition() {\n    return pendingSeekPositionMs != C.TIME_UNSET\n        ? pendingSeekPositionMs\n        : remoteMediaClient != null\n            ? remoteMediaClient.getApproximateStreamPosition()\n            : lastReportedPositionMs;\n  }\n\n  @Override\n  public long getBufferedPosition() {\n    return getCurrentPosition();\n  }\n\n  @Override\n  public long getTotalBufferedDuration() {\n    long bufferedPosition = getBufferedPosition();\n    long currentPosition = getCurrentPosition();\n    return bufferedPosition == C.TIME_UNSET || currentPosition == C.TIME_UNSET\n        ? 0\n        : bufferedPosition - currentPosition;\n  }\n\n  @Override\n  public boolean isPlayingAd() {\n    return false;\n  }\n\n  @Override\n  public int getCurrentAdGroupIndex() {\n    return C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getCurrentAdIndexInAdGroup() {\n    return C.INDEX_UNSET;\n  }\n\n  @Override\n  public boolean isLoading() {\n    return false;\n  }\n\n  @Override\n  public long getContentPosition() {\n    return getCurrentPosition();\n  }\n\n  @Override\n  public long getContentBufferedPosition() {\n    return getBufferedPosition();\n  }\n\n  // Internal methods.\n\n  private void updateInternalState() {\n    if (remoteMediaClient == null) {\n      // There is no session. We leave the state of the player as it is now.\n      return;\n    }\n\n    boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady;\n    int playbackState = fetchPlaybackState(remoteMediaClient);\n    boolean playWhenReady = !remoteMediaClient.isPaused();\n    if (this.playbackState != playbackState\n        || this.playWhenReady != playWhenReady) {\n      this.playbackState = playbackState;\n      this.playWhenReady = playWhenReady;\n      notificationsBatch.add(\n          new ListenerNotificationTask(\n              listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState)));\n    }\n    boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady;\n    if (wasPlaying != isPlaying) {\n      notificationsBatch.add(\n          new ListenerNotificationTask(listener -> listener.onIsPlayingChanged(isPlaying)));\n    }\n    @RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);\n    if (this.repeatMode != repeatMode) {\n      this.repeatMode = repeatMode;\n      notificationsBatch.add(\n          new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode)));\n    }\n    maybeUpdateTimelineAndNotify();\n\n    int currentWindowIndex = C.INDEX_UNSET;\n    MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();\n    if (currentItem != null) {\n      currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId());\n    }\n    if (currentWindowIndex == C.INDEX_UNSET) {\n      // The timeline is empty. Fall back to index 0, which is what ExoPlayer would do.\n      currentWindowIndex = 0;\n    }\n    if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {\n      this.currentWindowIndex = currentWindowIndex;\n      notificationsBatch.add(\n          new ListenerNotificationTask(\n              listener ->\n                  listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));\n    }\n    if (updateTracksAndSelections()) {\n      notificationsBatch.add(\n          new ListenerNotificationTask(\n              listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));\n    }\n    flushNotifications();\n  }\n\n  private void maybeUpdateTimelineAndNotify() {\n    if (updateTimeline()) {\n      @Player.TimelineChangeReason int reason = waitingForInitialTimeline\n          ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;\n      waitingForInitialTimeline = false;\n      notificationsBatch.add(\n          new ListenerNotificationTask(\n              listener ->\n                  listener.onTimelineChanged(currentTimeline, /* manifest= */ null, reason)));\n    }\n  }\n\n  /**\n   * Updates the current timeline and returns whether it has changed.\n   */\n  private boolean updateTimeline() {\n    CastTimeline oldTimeline = currentTimeline;\n    MediaStatus status = getMediaStatus();\n    currentTimeline =\n        status != null\n            ? timelineTracker.getCastTimeline(remoteMediaClient)\n            : CastTimeline.EMPTY_CAST_TIMELINE;\n    return !oldTimeline.equals(currentTimeline);\n  }\n\n  /**\n   * Updates the internal tracks and selection and returns whether they have changed.\n   */\n  private boolean updateTracksAndSelections() {\n    if (remoteMediaClient == null) {\n      // There is no session. We leave the state of the player as it is now.\n      return false;\n    }\n\n    MediaStatus mediaStatus = getMediaStatus();\n    MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null;\n    List<MediaTrack> castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null;\n    if (castMediaTracks == null || castMediaTracks.isEmpty()) {\n      boolean hasChanged = !currentTrackGroups.isEmpty();\n      currentTrackGroups = TrackGroupArray.EMPTY;\n      currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;\n      return hasChanged;\n    }\n    long[] activeTrackIds = mediaStatus.getActiveTrackIds();\n    if (activeTrackIds == null) {\n      activeTrackIds = EMPTY_TRACK_ID_ARRAY;\n    }\n\n    TrackGroup[] trackGroups = new TrackGroup[castMediaTracks.size()];\n    TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT];\n    for (int i = 0; i < castMediaTracks.size(); i++) {\n      MediaTrack mediaTrack = castMediaTracks.get(i);\n      trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack));\n\n      long id = mediaTrack.getId();\n      int trackType = MimeTypes.getTrackType(mediaTrack.getContentType());\n      int rendererIndex = getRendererIndexForTrackType(trackType);\n      if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET\n          && trackSelections[rendererIndex] == null) {\n        trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0);\n      }\n    }\n    TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups);\n    TrackSelectionArray newTrackSelections = new TrackSelectionArray(trackSelections);\n\n    if (!newTrackGroups.equals(currentTrackGroups)\n        || !newTrackSelections.equals(currentTrackSelection)) {\n      currentTrackSelection = new TrackSelectionArray(trackSelections);\n      currentTrackGroups = new TrackGroupArray(trackGroups);\n      return true;\n    }\n    return false;\n  }\n\n  private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) {\n    if (this.remoteMediaClient == remoteMediaClient) {\n      // Do nothing.\n      return;\n    }\n    if (this.remoteMediaClient != null) {\n      this.remoteMediaClient.removeListener(statusListener);\n      this.remoteMediaClient.removeProgressListener(statusListener);\n    }\n    this.remoteMediaClient = remoteMediaClient;\n    if (remoteMediaClient != null) {\n      if (sessionAvailabilityListener != null) {\n        sessionAvailabilityListener.onCastSessionAvailable();\n      }\n      remoteMediaClient.addListener(statusListener);\n      remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);\n      updateInternalState();\n    } else {\n      if (sessionAvailabilityListener != null) {\n        sessionAvailabilityListener.onCastSessionUnavailable();\n      }\n    }\n  }\n\n  @Nullable\n  private MediaStatus getMediaStatus() {\n    return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;\n  }\n\n  /**\n   * Retrieves the playback state from {@code remoteMediaClient} and maps it into a {@link Player}\n   * state\n   */\n  private static int fetchPlaybackState(RemoteMediaClient remoteMediaClient) {\n    int receiverAppStatus = remoteMediaClient.getPlayerState();\n    switch (receiverAppStatus) {\n      case MediaStatus.PLAYER_STATE_BUFFERING:\n        return STATE_BUFFERING;\n      case MediaStatus.PLAYER_STATE_PLAYING:\n      case MediaStatus.PLAYER_STATE_PAUSED:\n        return STATE_READY;\n      case MediaStatus.PLAYER_STATE_IDLE:\n      case MediaStatus.PLAYER_STATE_UNKNOWN:\n      default:\n        return STATE_IDLE;\n    }\n  }\n\n  /**\n   * Retrieves the repeat mode from {@code remoteMediaClient} and maps it into a\n   * {@link Player.RepeatMode}.\n   */\n  @RepeatMode\n  private static int fetchRepeatMode(RemoteMediaClient remoteMediaClient) {\n    MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();\n    if (mediaStatus == null) {\n      // No media session active, yet.\n      return REPEAT_MODE_OFF;\n    }\n    int castRepeatMode = mediaStatus.getQueueRepeatMode();\n    switch (castRepeatMode) {\n      case MediaStatus.REPEAT_MODE_REPEAT_SINGLE:\n        return REPEAT_MODE_ONE;\n      case MediaStatus.REPEAT_MODE_REPEAT_ALL:\n      case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE:\n        return REPEAT_MODE_ALL;\n      case MediaStatus.REPEAT_MODE_REPEAT_OFF:\n        return REPEAT_MODE_OFF;\n      default:\n        throw new IllegalStateException();\n    }\n  }\n\n  private static boolean isTrackActive(long id, long[] activeTrackIds) {\n    for (long activeTrackId : activeTrackIds) {\n      if (activeTrackId == id) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  private static int getRendererIndexForTrackType(int trackType) {\n    return trackType == C.TRACK_TYPE_VIDEO\n        ? RENDERER_INDEX_VIDEO\n        : trackType == C.TRACK_TYPE_AUDIO\n            ? RENDERER_INDEX_AUDIO\n            : trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT : C.INDEX_UNSET;\n  }\n\n  private static int getCastRepeatMode(@RepeatMode int repeatMode) {\n    switch (repeatMode) {\n      case REPEAT_MODE_ONE:\n        return MediaStatus.REPEAT_MODE_REPEAT_SINGLE;\n      case REPEAT_MODE_ALL:\n        return MediaStatus.REPEAT_MODE_REPEAT_ALL;\n      case REPEAT_MODE_OFF:\n        return MediaStatus.REPEAT_MODE_REPEAT_OFF;\n      default:\n        throw new IllegalArgumentException();\n    }\n  }\n\n  private final class StatusListener implements RemoteMediaClient.Listener,\n      SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {\n\n    // RemoteMediaClient.ProgressListener implementation.\n\n    @Override\n    public void onProgressUpdated(long progressMs, long unusedDurationMs) {\n      lastReportedPositionMs = progressMs;\n    }\n\n    // RemoteMediaClient.Listener implementation.\n\n    @Override\n    public void onStatusUpdated() {\n      updateInternalState();\n    }\n\n    @Override\n    public void onMetadataUpdated() {}\n\n    @Override\n    public void onQueueStatusUpdated() {\n      maybeUpdateTimelineAndNotify();\n    }\n\n    @Override\n    public void onPreloadStatusUpdated() {}\n\n    @Override\n    public void onSendingRemoteMediaRequest() {}\n\n    @Override\n    public void onAdBreakStatusUpdated() {}\n\n    // SessionManagerListener implementation.\n\n    @Override\n    public void onSessionStarted(CastSession castSession, String s) {\n      setRemoteMediaClient(castSession.getRemoteMediaClient());\n    }\n\n    @Override\n    public void onSessionResumed(CastSession castSession, boolean b) {\n      setRemoteMediaClient(castSession.getRemoteMediaClient());\n    }\n\n    @Override\n    public void onSessionEnded(CastSession castSession, int i) {\n      setRemoteMediaClient(null);\n    }\n\n    @Override\n    public void onSessionSuspended(CastSession castSession, int i) {\n      setRemoteMediaClient(null);\n    }\n\n    @Override\n    public void onSessionResumeFailed(CastSession castSession, int statusCode) {\n      Log.e(TAG, \"Session resume failed. Error code \" + statusCode + \": \"\n          + CastUtils.getLogString(statusCode));\n    }\n\n    @Override\n    public void onSessionStarting(CastSession castSession) {\n      // Do nothing.\n    }\n\n    @Override\n    public void onSessionStartFailed(CastSession castSession, int statusCode) {\n      Log.e(TAG, \"Session start failed. Error code \" + statusCode + \": \"\n          + CastUtils.getLogString(statusCode));\n    }\n\n    @Override\n    public void onSessionEnding(CastSession castSession) {\n      // Do nothing.\n    }\n\n    @Override\n    public void onSessionResuming(CastSession castSession, String s) {\n      // Do nothing.\n    }\n\n  }\n\n  // Internal methods.\n\n  private void flushNotifications() {\n    boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();\n    ongoingNotificationsTasks.addAll(notificationsBatch);\n    notificationsBatch.clear();\n    if (recursiveNotification) {\n      // This will be handled once the current notification task is finished.\n      return;\n    }\n    while (!ongoingNotificationsTasks.isEmpty()) {\n      ongoingNotificationsTasks.peekFirst().execute();\n      ongoingNotificationsTasks.removeFirst();\n    }\n  }\n\n  // Internal classes.\n\n  private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {\n\n    @Override\n    public void onResult(@NonNull MediaChannelResult result) {\n      int statusCode = result.getStatus().getStatusCode();\n      if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {\n        Log.e(TAG, \"Seek failed. Error code \" + statusCode + \": \"\n            + CastUtils.getLogString(statusCode));\n      }\n      if (--pendingSeekCount == 0) {\n        pendingSeekWindowIndex = C.INDEX_UNSET;\n        pendingSeekPositionMs = C.TIME_UNSET;\n        notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));\n        flushNotifications();\n      }\n    }\n  }\n\n  private final class ListenerNotificationTask {\n\n    private final Iterator<ListenerHolder> listenersSnapshot;\n    private final ListenerInvocation listenerInvocation;\n\n    private ListenerNotificationTask(ListenerInvocation listenerInvocation) {\n      this.listenersSnapshot = listeners.iterator();\n      this.listenerInvocation = listenerInvocation;\n    }\n\n    public void execute() {\n      while (listenersSnapshot.hasNext()) {\n        listenersSnapshot.next().invoke(listenerInvocation);\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.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.ext.cast;\n\nimport androidx.annotation.Nullable;\nimport android.util.SparseArray;\nimport android.util.SparseIntArray;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Timeline;\nimport java.util.Arrays;\n\n/**\n * A {@link Timeline} for Cast media queues.\n */\n/* package */ final class CastTimeline extends Timeline {\n\n  /** Holds {@link Timeline} related data for a Cast media item. */\n  public static final class ItemData {\n\n    /** Holds no media information. */\n    public static final ItemData EMPTY = new ItemData();\n\n    /** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */\n    public final long durationUs;\n    /**\n     * The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown.\n     */\n    public final long defaultPositionUs;\n\n    private ItemData() {\n      this(/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */ C.TIME_UNSET);\n    }\n\n    /**\n     * Creates an instance.\n     *\n     * @param durationUs See {@link #durationsUs}.\n     * @param defaultPositionUs See {@link #defaultPositionUs}.\n     */\n    public ItemData(long durationUs, long defaultPositionUs) {\n      this.durationUs = durationUs;\n      this.defaultPositionUs = defaultPositionUs;\n    }\n\n    /** Returns an instance with the given {@link #durationsUs}. */\n    public ItemData copyWithDurationUs(long durationUs) {\n      if (durationUs == this.durationUs) {\n        return this;\n      }\n      return new ItemData(durationUs, defaultPositionUs);\n    }\n\n    /** Returns an instance with the given {@link #defaultPositionsUs}. */\n    public ItemData copyWithDefaultPositionUs(long defaultPositionUs) {\n      if (defaultPositionUs == this.defaultPositionUs) {\n        return this;\n      }\n      return new ItemData(durationUs, defaultPositionUs);\n    }\n  }\n\n  /** {@link Timeline} for a cast queue that has no items. */\n  public static final CastTimeline EMPTY_CAST_TIMELINE =\n      new CastTimeline(new int[0], new SparseArray<>());\n\n  private final SparseIntArray idsToIndex;\n  private final int[] ids;\n  private final long[] durationsUs;\n  private final long[] defaultPositionsUs;\n\n  /**\n   * Creates a Cast timeline from the given data.\n   *\n   * @param itemIds The ids of the items in the timeline.\n   * @param itemIdToData Maps item ids to {@link ItemData}.\n   */\n  public CastTimeline(int[] itemIds, SparseArray<ItemData> itemIdToData) {\n    int itemCount = itemIds.length;\n    idsToIndex = new SparseIntArray(itemCount);\n    ids = Arrays.copyOf(itemIds, itemCount);\n    durationsUs = new long[itemCount];\n    defaultPositionsUs = new long[itemCount];\n    for (int i = 0; i < ids.length; i++) {\n      int id = ids[i];\n      idsToIndex.put(id, i);\n      ItemData data = itemIdToData.get(id, ItemData.EMPTY);\n      durationsUs[i] = data.durationUs;\n      defaultPositionsUs[i] = data.defaultPositionUs;\n    }\n  }\n\n  // Timeline implementation.\n\n  @Override\n  public int getWindowCount() {\n    return ids.length;\n  }\n\n  @Override\n  public Window getWindow(\n      int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {\n    long durationUs = durationsUs[windowIndex];\n    boolean isDynamic = durationUs == C.TIME_UNSET;\n    Object tag = setTag ? ids[windowIndex] : null;\n    return window.set(\n        tag,\n        /* presentationStartTimeMs= */ C.TIME_UNSET,\n        /* windowStartTimeMs= */ C.TIME_UNSET,\n        /* isSeekable= */ !isDynamic,\n        isDynamic,\n        defaultPositionsUs[windowIndex],\n        durationUs,\n        /* firstPeriodIndex= */ windowIndex,\n        /* lastPeriodIndex= */ windowIndex,\n        /* positionInFirstPeriodUs= */ 0);\n  }\n\n  @Override\n  public int getPeriodCount() {\n    return ids.length;\n  }\n\n  @Override\n  public Period getPeriod(int periodIndex, Period period, boolean setIds) {\n    int id = ids[periodIndex];\n    return period.set(id, id, periodIndex, durationsUs[periodIndex], 0);\n  }\n\n  @Override\n  public int getIndexOfPeriod(Object uid) {\n    return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET;\n  }\n\n  @Override\n  public Integer getUidOfPeriod(int periodIndex) {\n    return ids[periodIndex];\n  }\n\n  // equals and hashCode implementations.\n\n  @Override\n  public boolean equals(@Nullable Object other) {\n    if (this == other) {\n      return true;\n    } else if (!(other instanceof CastTimeline)) {\n      return false;\n    }\n    CastTimeline that = (CastTimeline) other;\n    return Arrays.equals(ids, that.ids)\n        && Arrays.equals(durationsUs, that.durationsUs)\n        && Arrays.equals(defaultPositionsUs, that.defaultPositionsUs);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = Arrays.hashCode(ids);\n    result = 31 * result + Arrays.hashCode(durationsUs);\n    result = 31 * result + Arrays.hashCode(defaultPositionsUs);\n    return result;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.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.ext.cast;\n\nimport android.util.SparseArray;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.gms.cast.MediaQueueItem;\nimport com.google.android.gms.cast.MediaStatus;\nimport com.google.android.gms.cast.framework.media.RemoteMediaClient;\nimport java.util.HashSet;\n\n/**\n * Creates {@link CastTimeline CastTimelines} from cast receiver app status updates.\n *\n * <p>This class keeps track of the duration reported by the current item to fill any missing\n * durations in the media queue items [See internal: b/65152553].\n */\n/* package */ final class CastTimelineTracker {\n\n  private final SparseArray<CastTimeline.ItemData> itemIdToData;\n\n  public CastTimelineTracker() {\n    itemIdToData = new SparseArray<>();\n  }\n\n  /**\n   * Returns a {@link CastTimeline} that represents the state of the given {@code\n   * remoteMediaClient}.\n   *\n   * <p>Returned timelines may contain values obtained from {@code remoteMediaClient} in previous\n   * invocations of this method.\n   *\n   * @param remoteMediaClient The Cast media client.\n   * @return A {@link CastTimeline} that represents the given {@code remoteMediaClient} status.\n   */\n  public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) {\n    int[] itemIds = remoteMediaClient.getMediaQueue().getItemIds();\n    if (itemIds.length > 0) {\n      // Only remove unused items when there is something in the queue to avoid removing all entries\n      // if the remote media client clears the queue temporarily. See [Internal ref: b/128825216].\n      removeUnusedItemDataEntries(itemIds);\n    }\n\n    // TODO: Reset state when the app instance changes [Internal ref: b/129672468].\n    MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();\n    if (mediaStatus == null) {\n      return CastTimeline.EMPTY_CAST_TIMELINE;\n    }\n\n    int currentItemId = mediaStatus.getCurrentItemId();\n    long durationUs = CastUtils.getStreamDurationUs(mediaStatus.getMediaInfo());\n    itemIdToData.put(\n        currentItemId,\n        itemIdToData\n            .get(currentItemId, CastTimeline.ItemData.EMPTY)\n            .copyWithDurationUs(durationUs));\n\n    for (MediaQueueItem item : mediaStatus.getQueueItems()) {\n      int itemId = item.getItemId();\n      itemIdToData.put(\n          itemId,\n          itemIdToData\n              .get(itemId, CastTimeline.ItemData.EMPTY)\n              .copyWithDefaultPositionUs((long) (item.getStartTime() * C.MICROS_PER_SECOND)));\n    }\n\n    return new CastTimeline(itemIds, itemIdToData);\n  }\n\n  private void removeUnusedItemDataEntries(int[] itemIds) {\n    HashSet<Integer> scratchItemIds = new HashSet<>(/* initialCapacity= */ itemIds.length * 2);\n    for (int id : itemIds) {\n      scratchItemIds.add(id);\n    }\n\n    int index = 0;\n    while (index < itemIdToData.size()) {\n      if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {\n        itemIdToData.removeAt(index);\n      } else {\n        index++;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.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.ext.cast;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.gms.cast.CastStatusCodes;\nimport com.google.android.gms.cast.MediaInfo;\nimport com.google.android.gms.cast.MediaTrack;\n\n/**\n * Utility methods for ExoPlayer/Cast integration.\n */\n/* package */ final class CastUtils {\n\n  /**\n   * Returns the duration in microseconds advertised by a media info, or {@link C#TIME_UNSET} if\n   * unknown or not applicable.\n   *\n   * @param mediaInfo The media info to get the duration from.\n   * @return The duration in microseconds, or {@link C#TIME_UNSET} if unknown or not applicable.\n   */\n  public static long getStreamDurationUs(MediaInfo mediaInfo) {\n    if (mediaInfo == null) {\n      return C.TIME_UNSET;\n    }\n    long durationMs = mediaInfo.getStreamDuration();\n    return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;\n  }\n\n  /**\n   * Returns a descriptive log string for the given {@code statusCode}, or \"Unknown.\" if not one of\n   * {@link CastStatusCodes}.\n   *\n   * @param statusCode A Cast API status code.\n   * @return A descriptive log string for the given {@code statusCode}, or \"Unknown.\" if not one of\n   *     {@link CastStatusCodes}.\n   */\n  public static String getLogString(int statusCode) {\n    switch (statusCode) {\n      case CastStatusCodes.APPLICATION_NOT_FOUND:\n        return \"A requested application could not be found.\";\n      case CastStatusCodes.APPLICATION_NOT_RUNNING:\n        return \"A requested application is not currently running.\";\n      case CastStatusCodes.AUTHENTICATION_FAILED:\n        return \"Authentication failure.\";\n      case CastStatusCodes.CANCELED:\n        return \"An in-progress request has been canceled, most likely because another action has \"\n            + \"preempted it.\";\n      case CastStatusCodes.ERROR_SERVICE_CREATION_FAILED:\n        return \"The Cast Remote Display service could not be created.\";\n      case CastStatusCodes.ERROR_SERVICE_DISCONNECTED:\n        return \"The Cast Remote Display service was disconnected.\";\n      case CastStatusCodes.FAILED:\n        return \"The in-progress request failed.\";\n      case CastStatusCodes.INTERNAL_ERROR:\n        return \"An internal error has occurred.\";\n      case CastStatusCodes.INTERRUPTED:\n        return \"A blocking call was interrupted while waiting and did not run to completion.\";\n      case CastStatusCodes.INVALID_REQUEST:\n        return \"An invalid request was made.\";\n      case CastStatusCodes.MESSAGE_SEND_BUFFER_TOO_FULL:\n        return \"A message could not be sent because there is not enough room in the send buffer at \"\n            + \"this time.\";\n      case CastStatusCodes.MESSAGE_TOO_LARGE:\n        return \"A message could not be sent because it is too large.\";\n      case CastStatusCodes.NETWORK_ERROR:\n        return \"Network I/O error.\";\n      case CastStatusCodes.NOT_ALLOWED:\n        return \"The request was disallowed and could not be completed.\";\n      case CastStatusCodes.REPLACED:\n        return \"The request's progress is no longer being tracked because another request of the \"\n            + \"same type has been made before the first request completed.\";\n      case CastStatusCodes.SUCCESS:\n        return \"Success.\";\n      case CastStatusCodes.TIMEOUT:\n        return \"An operation has timed out.\";\n      case CastStatusCodes.UNKNOWN_ERROR:\n        return \"An unknown, unexpected error has occurred.\";\n      default:\n        return CastStatusCodes.getStatusCodeString(statusCode);\n    }\n  }\n\n  /**\n   * Creates a {@link Format} instance containing all information contained in the given\n   * {@link MediaTrack} object.\n   *\n   * @param mediaTrack The {@link MediaTrack}.\n   * @return The equivalent {@link Format}.\n   */\n  public static Format mediaTrackToFormat(MediaTrack mediaTrack) {\n    return Format.createContainerFormat(\n        mediaTrack.getContentId(),\n        /* label= */ null,\n        mediaTrack.getContentType(),\n        /* sampleMimeType= */ null,\n        /* codecs= */ null,\n        /* bitrate= */ Format.NO_VALUE,\n        /* selectionFlags= */ 0,\n        /* roleFlags= */ 0,\n        mediaTrack.getLanguage());\n  }\n\n  private CastUtils() {}\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.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.ext.cast;\n\nimport android.content.Context;\nimport com.google.android.gms.cast.CastMediaControlIntent;\nimport com.google.android.gms.cast.framework.CastOptions;\nimport com.google.android.gms.cast.framework.OptionsProvider;\nimport com.google.android.gms.cast.framework.SessionProvider;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * A convenience {@link OptionsProvider} to target the default cast receiver app.\n */\npublic final class DefaultCastOptionsProvider implements OptionsProvider {\n\n  @Override\n  public CastOptions getCastOptions(Context context) {\n    return new CastOptions.Builder()\n        .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)\n        .setStopReceiverApplicationWhenEndingSession(true).build();\n  }\n\n  @Override\n  public List<SessionProvider> getAdditionalSessionProviders(Context context) {\n    return Collections.emptyList();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.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.ext.cast;\n\nimport android.net.Uri;\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.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport org.checkerframework.checker.initialization.qual.UnknownInitialization;\nimport org.checkerframework.checker.nullness.qual.EnsuresNonNull;\n\n/** Representation of an item that can be played by a media player. */\npublic final class MediaItem {\n\n  /** A builder for {@link MediaItem} instances. */\n  public static final class Builder {\n\n    @Nullable private UUID uuid;\n    private String title;\n    private String description;\n    private MediaItem.UriBundle media;\n    @Nullable private Object attachment;\n    private List<MediaItem.DrmScheme> drmSchemes;\n    private long startPositionUs;\n    private long endPositionUs;\n    private String mimeType;\n\n    /** Creates an builder with default field values. */\n    public Builder() {\n      clearInternal();\n    }\n\n    /** See {@link MediaItem#uuid}. */\n    public Builder setUuid(UUID uuid) {\n      this.uuid = uuid;\n      return this;\n    }\n\n    /** See {@link MediaItem#title}. */\n    public Builder setTitle(String title) {\n      this.title = title;\n      return this;\n    }\n\n    /** See {@link MediaItem#description}. */\n    public Builder setDescription(String description) {\n      this.description = description;\n      return this;\n    }\n\n    /** Equivalent to {@link #setMedia(UriBundle) setMedia(new UriBundle(Uri.parse(uri)))}. */\n    public Builder setMedia(String uri) {\n      return setMedia(new UriBundle(Uri.parse(uri)));\n    }\n\n    /** See {@link MediaItem#media}. */\n    public Builder setMedia(UriBundle media) {\n      this.media = media;\n      return this;\n    }\n\n    /** See {@link MediaItem#attachment}. */\n    public Builder setAttachment(Object attachment) {\n      this.attachment = attachment;\n      return this;\n    }\n\n    /** See {@link MediaItem#drmSchemes}. */\n    public Builder setDrmSchemes(List<MediaItem.DrmScheme> drmSchemes) {\n      this.drmSchemes = Collections.unmodifiableList(new ArrayList<>(drmSchemes));\n      return this;\n    }\n\n    /** See {@link MediaItem#startPositionUs}. */\n    public Builder setStartPositionUs(long startPositionUs) {\n      this.startPositionUs = startPositionUs;\n      return this;\n    }\n\n    /** See {@link MediaItem#endPositionUs}. */\n    public Builder setEndPositionUs(long endPositionUs) {\n      Assertions.checkArgument(endPositionUs != C.TIME_END_OF_SOURCE);\n      this.endPositionUs = endPositionUs;\n      return this;\n    }\n\n    /** See {@link MediaItem#mimeType}. */\n    public Builder setMimeType(String mimeType) {\n      this.mimeType = mimeType;\n      return this;\n    }\n\n    /**\n     * Equivalent to {@link #build()}, except it also calls {@link #clear()} after creating the\n     * {@link MediaItem}.\n     */\n    public MediaItem buildAndClear() {\n      MediaItem item = build();\n      clearInternal();\n      return item;\n    }\n\n    /** Returns the builder to default values. */\n    public Builder clear() {\n      clearInternal();\n      return this;\n    }\n\n    /**\n     * Returns a new {@link MediaItem} instance with the current builder values. This method also\n     * clears any values passed to {@link #setUuid(UUID)}.\n     */\n    public MediaItem build() {\n      UUID uuid = this.uuid;\n      this.uuid = null;\n      return new MediaItem(\n          uuid != null ? uuid : UUID.randomUUID(),\n          title,\n          description,\n          media,\n          attachment,\n          drmSchemes,\n          startPositionUs,\n          endPositionUs,\n          mimeType);\n    }\n\n    @EnsuresNonNull({\"title\", \"description\", \"media\", \"drmSchemes\", \"mimeType\"})\n    private void clearInternal(@UnknownInitialization Builder this) {\n      uuid = null;\n      title = \"\";\n      description = \"\";\n      media = UriBundle.EMPTY;\n      attachment = null;\n      drmSchemes = Collections.emptyList();\n      startPositionUs = C.TIME_UNSET;\n      endPositionUs = C.TIME_UNSET;\n      mimeType = \"\";\n    }\n  }\n\n  /** Bundles a resource's URI with headers to attach to any request to that URI. */\n  public static final class UriBundle {\n\n    /** An empty {@link UriBundle}. */\n    public static final UriBundle EMPTY = new UriBundle(Uri.EMPTY);\n\n    /** A URI. */\n    public final Uri uri;\n\n    /** The headers to attach to any request for the given URI. */\n    public final Map<String, String> requestHeaders;\n\n    /**\n     * Creates an instance with no request headers.\n     *\n     * @param uri See {@link #uri}.\n     */\n    public UriBundle(Uri uri) {\n      this(uri, Collections.emptyMap());\n    }\n\n    /**\n     * Creates an instance with the given URI and request headers.\n     *\n     * @param uri See {@link #uri}.\n     * @param requestHeaders See {@link #requestHeaders}.\n     */\n    public UriBundle(Uri uri, Map<String, String> requestHeaders) {\n      this.uri = uri;\n      this.requestHeaders = Collections.unmodifiableMap(new HashMap<>(requestHeaders));\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      UriBundle uriBundle = (UriBundle) other;\n      return uri.equals(uriBundle.uri) && requestHeaders.equals(uriBundle.requestHeaders);\n    }\n\n    @Override\n    public int hashCode() {\n      int result = uri.hashCode();\n      result = 31 * result + requestHeaders.hashCode();\n      return result;\n    }\n  }\n\n  /**\n   * Represents a DRM protection scheme, and optionally provides information about how to acquire\n   * the license for the media.\n   */\n  public static final class DrmScheme {\n\n    /** The UUID of the protection scheme. */\n    public final UUID uuid;\n\n    /**\n     * Optional {@link UriBundle} for the license server. If no license server is provided, the\n     * server must be provided by the media.\n     */\n    @Nullable public final UriBundle licenseServer;\n\n    /**\n     * Creates an instance.\n     *\n     * @param uuid See {@link #uuid}.\n     * @param licenseServer See {@link #licenseServer}.\n     */\n    public DrmScheme(UUID uuid, @Nullable UriBundle licenseServer) {\n      this.uuid = uuid;\n      this.licenseServer = licenseServer;\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      DrmScheme drmScheme = (DrmScheme) other;\n      return uuid.equals(drmScheme.uuid) && Util.areEqual(licenseServer, drmScheme.licenseServer);\n    }\n\n    @Override\n    public int hashCode() {\n      int result = uuid.hashCode();\n      result = 31 * result + (licenseServer != null ? licenseServer.hashCode() : 0);\n      return result;\n    }\n  }\n\n  /**\n   * A UUID that identifies this item, potentially across different devices. The default value is\n   * obtained by calling {@link UUID#randomUUID()}.\n   */\n  public final UUID uuid;\n\n  /** The title of the item. The default value is an empty string. */\n  public final String title;\n\n  /** A description for the item. The default value is an empty string. */\n  public final String description;\n\n  /**\n   * A {@link UriBundle} to fetch the media content. The default value is {@link UriBundle#EMPTY}.\n   */\n  public final UriBundle media;\n\n  /**\n   * An optional opaque object to attach to the media item. Handling of this attachment is\n   * implementation specific. The default value is null.\n   */\n  @Nullable public final Object attachment;\n\n  /**\n   * Immutable list of {@link DrmScheme} instances sorted in decreasing order of preference. The\n   * default value is an empty list.\n   */\n  public final List<DrmScheme> drmSchemes;\n\n  /**\n   * The position in microseconds at which playback of this media item should start. {@link\n   * C#TIME_UNSET} if playback should start at the default position. The default value is {@link\n   * C#TIME_UNSET}.\n   */\n  public final long startPositionUs;\n\n  /**\n   * The position in microseconds at which playback of this media item should end. {@link\n   * C#TIME_UNSET} if playback should end at the end of the media. The default value is {@link\n   * C#TIME_UNSET}.\n   */\n  public final long endPositionUs;\n\n  /**\n   * The mime type of this media item. The default value is an empty string.\n   *\n   * <p>The usage of this mime type is optional and player implementation specific.\n   */\n  public final String mimeType;\n\n  // TODO: Add support for sideloaded tracks, artwork, icon, and subtitle.\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    MediaItem mediaItem = (MediaItem) other;\n    return startPositionUs == mediaItem.startPositionUs\n        && endPositionUs == mediaItem.endPositionUs\n        && uuid.equals(mediaItem.uuid)\n        && title.equals(mediaItem.title)\n        && description.equals(mediaItem.description)\n        && media.equals(mediaItem.media)\n        && Util.areEqual(attachment, mediaItem.attachment)\n        && drmSchemes.equals(mediaItem.drmSchemes)\n        && mimeType.equals(mediaItem.mimeType);\n  }\n\n  @Override\n  public int hashCode() {\n    int result = uuid.hashCode();\n    result = 31 * result + title.hashCode();\n    result = 31 * result + description.hashCode();\n    result = 31 * result + media.hashCode();\n    result = 31 * result + (attachment != null ? attachment.hashCode() : 0);\n    result = 31 * result + drmSchemes.hashCode();\n    result = 31 * result + (int) (startPositionUs ^ (startPositionUs >>> 32));\n    result = 31 * result + (int) (endPositionUs ^ (endPositionUs >>> 32));\n    result = 31 * result + mimeType.hashCode();\n    return result;\n  }\n\n  private MediaItem(\n      UUID uuid,\n      String title,\n      String description,\n      UriBundle media,\n      @Nullable Object attachment,\n      List<DrmScheme> drmSchemes,\n      long startPositionUs,\n      long endPositionUs,\n      String mimeType) {\n    this.uuid = uuid;\n    this.title = title;\n    this.description = description;\n    this.media = media;\n    this.attachment = attachment;\n    this.drmSchemes = drmSchemes;\n    this.startPositionUs = startPositionUs;\n    this.endPositionUs = endPositionUs;\n    this.mimeType = mimeType;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.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.ext.cast;\n\n/** Represents a sequence of {@link MediaItem MediaItems}. */\npublic interface MediaItemQueue {\n\n  /**\n   * Returns the item at the given index.\n   *\n   * @param index The index of the item to retrieve.\n   * @return The item at the given index.\n   * @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.\n   */\n  MediaItem get(int index);\n\n  /** Returns the number of items in this queue. */\n  int getSize();\n\n  /**\n   * Appends the given sequence of items to the queue.\n   *\n   * @param items The sequence of items to append.\n   */\n  void add(MediaItem... items);\n\n  /**\n   * Adds the given sequence of items to the queue at the given position, so that the first of\n   * {@code items} is placed at the given index.\n   *\n   * @param index The index at which {@code items} will be inserted.\n   * @param items The sequence of items to append.\n   * @throws IndexOutOfBoundsException If {@code index < 0 || index > getSize()}.\n   */\n  void add(int index, MediaItem... items);\n\n  /**\n   * Moves an existing item within the playlist.\n   *\n   * <p>Calling this method is equivalent to removing the item at position {@code indexFrom} and\n   * immediately inserting it at position {@code indexTo}. If the moved item is being played at the\n   * moment of the invocation, playback will stick with the moved item.\n   *\n   * @param indexFrom The index of the item to move.\n   * @param indexTo The index at which the item will be placed after this operation.\n   * @throws IndexOutOfBoundsException If for either index, {@code index < 0 || index >= getSize()}.\n   */\n  void move(int indexFrom, int indexTo);\n\n  /**\n   * Removes an item from the queue.\n   *\n   * @param index The index of the item to remove from the queue.\n   * @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.\n   */\n  void remove(int index);\n\n  /**\n   * Removes a range of items from the queue.\n   *\n   * <p>Does nothing if an empty range ({@code from == exclusiveTo}) is passed.\n   *\n   * @param from The inclusive index at which the range to remove starts.\n   * @param exclusiveTo The exclusive index at which the range to remove ends.\n   * @throws IndexOutOfBoundsException If {@code from < 0 || exclusiveTo > getSize() || from >\n   *     exclusiveTo}.\n   */\n  void removeRange(int from, int exclusiveTo);\n\n  /** Removes all items in the queue. */\n  void clear();\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/SessionAvailabilityListener.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.ext.cast;\n\n/** Listener of changes in the cast session availability. */\npublic interface SessionAvailabilityListener {\n\n  /** Called when a cast session becomes available to the player. */\n  void onCastSessionAvailable();\n\n  /** Called when the cast session becomes unavailable. */\n  void onCastSessionUnavailable();\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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\n<manifest package=\"com.google.android.exoplayer2.ext.cast.test\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.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.ext.cast;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.TimelineAsserts;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.gms.cast.MediaInfo;\nimport com.google.android.gms.cast.MediaStatus;\nimport com.google.android.gms.cast.framework.media.MediaQueue;\nimport com.google.android.gms.cast.framework.media.RemoteMediaClient;\nimport java.util.Collections;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mockito;\n\n/** Tests for {@link CastTimelineTracker}. */\n@RunWith(AndroidJUnit4.class)\npublic class CastTimelineTrackerTest {\n\n  private static final long DURATION_2_MS = 2000;\n  private static final long DURATION_3_MS = 3000;\n  private static final long DURATION_4_MS = 4000;\n  private static final long DURATION_5_MS = 5000;\n\n  /** Tests that duration of the current media info is correctly propagated to the timeline. */\n  @Test\n  public void testGetCastTimelinePersistsDuration() {\n    CastTimelineTracker tracker = new CastTimelineTracker();\n\n    RemoteMediaClient remoteMediaClient =\n        mockRemoteMediaClient(\n            /* itemIds= */ new int[] {1, 2, 3, 4, 5},\n            /* currentItemId= */ 2,\n            /* currentDurationMs= */ DURATION_2_MS);\n    TimelineAsserts.assertPeriodDurations(\n        tracker.getCastTimeline(remoteMediaClient),\n        C.TIME_UNSET,\n        C.msToUs(DURATION_2_MS),\n        C.TIME_UNSET,\n        C.TIME_UNSET,\n        C.TIME_UNSET);\n\n    remoteMediaClient =\n        mockRemoteMediaClient(\n            /* itemIds= */ new int[] {1, 2, 3},\n            /* currentItemId= */ 3,\n            /* currentDurationMs= */ DURATION_3_MS);\n    TimelineAsserts.assertPeriodDurations(\n        tracker.getCastTimeline(remoteMediaClient),\n        C.TIME_UNSET,\n        C.msToUs(DURATION_2_MS),\n        C.msToUs(DURATION_3_MS));\n\n    remoteMediaClient =\n        mockRemoteMediaClient(\n            /* itemIds= */ new int[] {1, 3},\n            /* currentItemId= */ 3,\n            /* currentDurationMs= */ DURATION_3_MS);\n    TimelineAsserts.assertPeriodDurations(\n        tracker.getCastTimeline(remoteMediaClient), C.TIME_UNSET, C.msToUs(DURATION_3_MS));\n\n    remoteMediaClient =\n        mockRemoteMediaClient(\n            /* itemIds= */ new int[] {1, 2, 3, 4, 5},\n            /* currentItemId= */ 4,\n            /* currentDurationMs= */ DURATION_4_MS);\n    TimelineAsserts.assertPeriodDurations(\n        tracker.getCastTimeline(remoteMediaClient),\n        C.TIME_UNSET,\n        C.TIME_UNSET,\n        C.msToUs(DURATION_3_MS),\n        C.msToUs(DURATION_4_MS),\n        C.TIME_UNSET);\n\n    remoteMediaClient =\n        mockRemoteMediaClient(\n            /* itemIds= */ new int[] {1, 2, 3, 4, 5},\n            /* currentItemId= */ 5,\n            /* currentDurationMs= */ DURATION_5_MS);\n    TimelineAsserts.assertPeriodDurations(\n        tracker.getCastTimeline(remoteMediaClient),\n        C.TIME_UNSET,\n        C.TIME_UNSET,\n        C.msToUs(DURATION_3_MS),\n        C.msToUs(DURATION_4_MS),\n        C.msToUs(DURATION_5_MS));\n  }\n\n  private static RemoteMediaClient mockRemoteMediaClient(\n      int[] itemIds, int currentItemId, long currentDurationMs) {\n    RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);\n    MediaStatus status = Mockito.mock(MediaStatus.class);\n    Mockito.when(status.getQueueItems()).thenReturn(Collections.emptyList());\n    Mockito.when(remoteMediaClient.getMediaStatus()).thenReturn(status);\n    Mockito.when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));\n    Mockito.when(status.getCurrentItemId()).thenReturn(currentItemId);\n    MediaQueue mediaQueue = mockMediaQueue(itemIds);\n    Mockito.when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue);\n    return remoteMediaClient;\n  }\n\n  private static MediaQueue mockMediaQueue(int[] itemIds) {\n    MediaQueue mediaQueue = Mockito.mock(MediaQueue.class);\n    Mockito.when(mediaQueue.getItemIds()).thenReturn(itemIds);\n    return mediaQueue;\n  }\n\n  private static MediaInfo getMediaInfo(long durationMs) {\n    return new MediaInfo.Builder(/*contentId= */ \"\")\n        .setStreamDuration(durationMs)\n        .setContentType(MimeTypes.APPLICATION_MP4)\n        .setStreamType(MediaInfo.STREAM_TYPE_NONE)\n        .build();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.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.ext.cast;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.UUID;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link MediaItem}. */\n@RunWith(AndroidJUnit4.class)\npublic class MediaItemTest {\n\n  @Test\n  public void buildMediaItem_resetsUuid() {\n    MediaItem.Builder builder = new MediaItem.Builder();\n    UUID uuid = new UUID(1, 1);\n    MediaItem item1 = builder.setUuid(uuid).build();\n    MediaItem item2 = builder.build();\n    MediaItem item3 = builder.build();\n    assertThat(item1.uuid).isEqualTo(uuid);\n    assertThat(item2.uuid).isNotEqualTo(uuid);\n    assertThat(item3.uuid).isNotEqualTo(item2.uuid);\n    assertThat(item3.uuid).isNotEqualTo(uuid);\n  }\n\n  @Test\n  public void buildMediaItem_doesNotChangeState() {\n    MediaItem.Builder builder = new MediaItem.Builder();\n    MediaItem item1 =\n        builder\n            .setUuid(new UUID(0, 1))\n            .setMedia(\"http://example.com\")\n            .setTitle(\"title\")\n            .setMimeType(MimeTypes.AUDIO_MP4)\n            .setStartPositionUs(3)\n            .setEndPositionUs(4)\n            .build();\n    MediaItem item2 = builder.setUuid(new UUID(0, 1)).build();\n    assertThat(item1).isEqualTo(item2);\n  }\n\n  @Test\n  public void buildMediaItem_assertDefaultValues() {\n    assertDefaultValues(new MediaItem.Builder().build());\n  }\n\n  @Test\n  public void buildAndClear_assertDefaultValues() {\n    MediaItem.Builder builder = new MediaItem.Builder();\n    builder\n        .setMedia(\"http://example.com\")\n        .setTitle(\"title\")\n        .setMimeType(MimeTypes.AUDIO_MP4)\n        .setStartPositionUs(3)\n        .setEndPositionUs(4)\n        .buildAndClear();\n    assertDefaultValues(builder.build());\n  }\n\n  @Test\n  public void equals_withEqualDrmSchemes_returnsTrue() {\n    MediaItem.Builder builder = new MediaItem.Builder();\n    MediaItem mediaItem1 =\n        builder\n            .setUuid(new UUID(0, 1))\n            .setMedia(\"www.google.com\")\n            .setDrmSchemes(createDummyDrmSchemes(1))\n            .buildAndClear();\n    MediaItem mediaItem2 =\n        builder\n            .setUuid(new UUID(0, 1))\n            .setMedia(\"www.google.com\")\n            .setDrmSchemes(createDummyDrmSchemes(1))\n            .buildAndClear();\n    assertThat(mediaItem1).isEqualTo(mediaItem2);\n  }\n\n  @Test\n  public void equals_withDifferentDrmRequestHeaders_returnsFalse() {\n    MediaItem.Builder builder = new MediaItem.Builder();\n    MediaItem mediaItem1 =\n        builder\n            .setUuid(new UUID(0, 1))\n            .setMedia(\"www.google.com\")\n            .setDrmSchemes(createDummyDrmSchemes(1))\n            .buildAndClear();\n    MediaItem mediaItem2 =\n        builder\n            .setUuid(new UUID(0, 1))\n            .setMedia(\"www.google.com\")\n            .setDrmSchemes(createDummyDrmSchemes(2))\n            .buildAndClear();\n    assertThat(mediaItem1).isNotEqualTo(mediaItem2);\n  }\n\n  private static void assertDefaultValues(MediaItem item) {\n    assertThat(item.title).isEmpty();\n    assertThat(item.description).isEmpty();\n    assertThat(item.media.uri).isEqualTo(Uri.EMPTY);\n    assertThat(item.attachment).isNull();\n    assertThat(item.drmSchemes).isEmpty();\n    assertThat(item.startPositionUs).isEqualTo(C.TIME_UNSET);\n    assertThat(item.endPositionUs).isEqualTo(C.TIME_UNSET);\n    assertThat(item.mimeType).isEmpty();\n  }\n\n  private static List<MediaItem.DrmScheme> createDummyDrmSchemes(int seed) {\n    HashMap<String, String> requestHeaders1 = new HashMap<>();\n    requestHeaders1.put(\"key1\", \"value1\");\n    requestHeaders1.put(\"key2\", \"value1\");\n    MediaItem.UriBundle uriBundle1 =\n        new MediaItem.UriBundle(Uri.parse(\"www.uri1.com\"), requestHeaders1);\n    MediaItem.DrmScheme drmScheme1 = new MediaItem.DrmScheme(C.WIDEVINE_UUID, uriBundle1);\n    HashMap<String, String> requestHeaders2 = new HashMap<>();\n    requestHeaders2.put(\"key3\", \"value3\");\n    requestHeaders2.put(\"key4\", \"valueWithSeed\" + seed);\n    MediaItem.UriBundle uriBundle2 =\n        new MediaItem.UriBundle(Uri.parse(\"www.uri2.com\"), requestHeaders2);\n    MediaItem.DrmScheme drmScheme2 = new MediaItem.DrmScheme(C.PLAYREADY_UUID, uriBundle2);\n    return Arrays.asList(drmScheme1, drmScheme2);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cronet/README.md",
    "content": "# ExoPlayer Cronet extension #\n\nThe Cronet extension is an [HttpDataSource][] implementation using [Cronet][].\n\n[HttpDataSource]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html\n[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F\n\n## Getting the extension ##\n\nThe easiest way to use the extension is to add it as a gradle dependency:\n\n```gradle\nimplementation 'com.google.android.exoplayer:extension-cronet:2.X.X'\n```\n\nwhere `2.X.X` is the version, which must match the version of the ExoPlayer\nlibrary being used.\n\nAlternatively, you can clone the ExoPlayer repository and depend on the module\nlocally. Instructions for doing this can be found in ExoPlayer's\n[top level README][].\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n\n## Using the extension ##\n\nExoPlayer requests data through `DataSource` instances. These instances are\neither instantiated and injected from application code, or obtained from\ninstances of `DataSource.Factory` that are instantiated and injected from\napplication code.\n\nIf your application only needs to play http(s) content, using the Cronet\nextension is as simple as updating any `DataSource`s and `DataSource.Factory`\ninstantiations in your application code to use `CronetDataSource` and\n`CronetDataSourceFactory` respectively. If your application also needs to play\nnon-http(s) content such as local files, use\n```\nnew DefaultDataSource(\n    ...\n    new CronetDataSource(...) /* baseDataSource argument */);\n```\nand\n```\nnew DefaultDataSourceFactory(\n    ...\n    new CronetDataSourceFactory(...) /* baseDataSourceFactory argument */);\n```\nrespectively.\n\n## Links ##\n\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*`\n  belong to this module.\n\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cronet/build.gradle",
    "content": "// 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.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n\n    // gradle 4.6 migration: disable dimensions mechanism\n    // more: https://proandroiddev.com/advanced-android-flavors-part-4-a-new-version-fc2ad80c01bb\n    flavorDimensions \"default\"\n\n    productFlavors {\n        stbeta {}\n        ststable {}\n        stfdroid {}\n    }\n}\n\ndependencies {\n    api 'org.chromium.net:cronet-embedded:75.3770.101'\n    implementation project(modulePrefix + 'library-core')\n    implementation 'androidx.annotation:annotation:1.1.0'\n    testImplementation project(modulePrefix + 'library')\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n}\n\next {\n    javadocTitle = 'Cronet extension'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'extension-cronet'\n    releaseDescription = 'Cronet extension for ExoPlayer.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cronet/src/main/AndroidManifest.xml",
    "content": "<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.google.android.exoplayer2.ext.cronet\">\n\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n\n</manifest>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProvider.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.cronet;\n\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport org.chromium.net.UploadDataProvider;\nimport org.chromium.net.UploadDataSink;\n\n/**\n * A {@link UploadDataProvider} implementation that provides data from a {@code byte[]}.\n */\n/* package */ final class ByteArrayUploadDataProvider extends UploadDataProvider {\n\n  private final byte[] data;\n\n  private int position;\n\n  public ByteArrayUploadDataProvider(byte[] data) {\n    this.data = data;\n  }\n\n  @Override\n  public long getLength() {\n    return data.length;\n  }\n\n  @Override\n  public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) throws IOException {\n    int readLength = Math.min(byteBuffer.remaining(), data.length - position);\n    byteBuffer.put(data, position, readLength);\n    position += readLength;\n    uploadDataSink.onReadSucceeded(false);\n  }\n\n  @Override\n  public void rewind(UploadDataSink uploadDataSink) throws IOException {\n    position = 0;\n    uploadDataSink.onRewindSucceeded();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.cronet;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport android.text.TextUtils;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.metadata.icy.IcyHeaders;\nimport com.google.android.exoplayer2.upstream.BaseDataSource;\nimport com.google.android.exoplayer2.upstream.DataSourceException;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.HttpDataSource;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.util.ConditionVariable;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Predicate;\nimport java.io.IOException;\nimport java.net.SocketTimeoutException;\nimport java.net.UnknownHostException;\nimport java.nio.ByteBuffer;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.concurrent.Executor;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport org.chromium.net.CronetEngine;\nimport org.chromium.net.CronetException;\nimport org.chromium.net.NetworkException;\nimport org.chromium.net.UrlRequest;\nimport org.chromium.net.UrlRequest.Status;\nimport org.chromium.net.UrlResponseInfo;\n\n/**\n * DataSource without intermediate buffer based on Cronet API set using UrlRequest.\n *\n * <p>This class's methods are organized in the sequence of expected calls.\n */\npublic class CronetDataSource extends BaseDataSource implements HttpDataSource {\n\n  /**\n   * Thrown when an error is encountered when trying to open a {@link CronetDataSource}.\n   */\n  public static final class OpenException extends HttpDataSourceException {\n\n    /**\n     * Returns the status of the connection establishment at the moment when the error occurred, as\n     * defined by {@link UrlRequest.Status}.\n     */\n    public final int cronetConnectionStatus;\n\n    public OpenException(IOException cause, DataSpec dataSpec, int cronetConnectionStatus) {\n      super(cause, dataSpec, TYPE_OPEN);\n      this.cronetConnectionStatus = cronetConnectionStatus;\n    }\n\n    public OpenException(String errorMessage, DataSpec dataSpec, int cronetConnectionStatus) {\n      super(errorMessage, dataSpec, TYPE_OPEN);\n      this.cronetConnectionStatus = cronetConnectionStatus;\n    }\n\n  }\n\n  /** Thrown on catching an InterruptedException. */\n  public static final class InterruptedIOException extends IOException {\n\n    public InterruptedIOException(InterruptedException e) {\n      super(e);\n    }\n  }\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.cronet\");\n  }\n\n  /**\n   * The default connection timeout, in milliseconds.\n   */\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  /* package */ final UrlRequest.Callback urlRequestCallback;\n\n  private static final String TAG = \"CronetDataSource\";\n  private static final String CONTENT_TYPE = \"Content-Type\";\n  private static final String SET_COOKIE = \"Set-Cookie\";\n  private static final String COOKIE = \"Cookie\";\n\n  private static final Pattern CONTENT_RANGE_HEADER_PATTERN =\n      Pattern.compile(\"^bytes (\\\\d+)-(\\\\d+)/(\\\\d+)$\");\n  // The size of read buffer passed to cronet UrlRequest.read().\n  private static final int READ_BUFFER_SIZE_BYTES = 32 * 1024;\n\n  private final CronetEngine cronetEngine;\n  private final Executor executor;\n  @Nullable private final Predicate<String> contentTypePredicate;\n  private final int connectTimeoutMs;\n  private final int readTimeoutMs;\n  private final boolean resetTimeoutOnRedirects;\n  private final boolean handleSetCookieRequests;\n  private final RequestProperties defaultRequestProperties;\n  private final RequestProperties requestProperties;\n  private final ConditionVariable operation;\n  private final Clock clock;\n\n  // Accessed by the calling thread only.\n  private boolean opened;\n  private long bytesToSkip;\n  private long bytesRemaining;\n\n  // Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible\n  // to reads made by the Cronet thread.\n  private UrlRequest currentUrlRequest;\n  private DataSpec currentDataSpec;\n\n  // Reference written and read by calling thread only. Passed to Cronet thread as a local variable.\n  // operation.open() calls ensure writes into the buffer are visible to reads made by the calling\n  // thread.\n  private ByteBuffer readBuffer;\n\n  // Written from the Cronet thread only. operation.open() calls ensure writes are visible to reads\n  // made by the calling thread.\n  private UrlResponseInfo responseInfo;\n  private IOException exception;\n  private boolean finished;\n\n  private volatile long currentConnectTimeoutMs;\n\n  /**\n   * @param cronetEngine A CronetEngine.\n   * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may\n   *     be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread\n   *     hop from Cronet's internal network thread to the response handling thread. However, to\n   *     avoid slowing down overall network performance, care must be taken to make sure response\n   *     handling is a fast operation when using a direct executor.\n   */\n  public CronetDataSource(CronetEngine cronetEngine, Executor executor) {\n    this(cronetEngine, executor, /* contentTypePredicate= */ null);\n  }\n\n  /**\n   * @param cronetEngine A CronetEngine.\n   * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may\n   *     be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread\n   *     hop from Cronet's internal network thread to the response handling thread. However, to\n   *     avoid slowing down overall network performance, care must be taken to make sure response\n   *     handling is a fast operation when using a direct executor.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then an {@link InvalidContentTypeException} is thrown from {@link\n   *     #open(DataSpec)}.\n   */\n  public CronetDataSource(\n      CronetEngine cronetEngine,\n      Executor executor,\n      @Nullable Predicate<String> contentTypePredicate) {\n    this(\n        cronetEngine,\n        executor,\n        contentTypePredicate,\n        DEFAULT_CONNECT_TIMEOUT_MILLIS,\n        DEFAULT_READ_TIMEOUT_MILLIS,\n        false,\n        null,\n        false);\n  }\n\n  /**\n   * @param cronetEngine A CronetEngine.\n   * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may\n   *     be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread\n   *     hop from Cronet's internal network thread to the response handling thread. However, to\n   *     avoid slowing down overall network performance, care must be taken to make sure response\n   *     handling is a fast operation when using a direct executor.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then an {@link InvalidContentTypeException} is thrown from {@link\n   *     #open(DataSpec)}.\n   * @param connectTimeoutMs The connection timeout, in milliseconds.\n   * @param readTimeoutMs The read timeout, in milliseconds.\n   * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.\n   * @param defaultRequestProperties The default request properties to be used.\n   */\n  public CronetDataSource(\n      CronetEngine cronetEngine,\n      Executor executor,\n      @Nullable Predicate<String> contentTypePredicate,\n      int connectTimeoutMs,\n      int readTimeoutMs,\n      boolean resetTimeoutOnRedirects,\n      RequestProperties defaultRequestProperties) {\n    this(\n        cronetEngine,\n        executor,\n        contentTypePredicate,\n        connectTimeoutMs,\n        readTimeoutMs,\n        resetTimeoutOnRedirects,\n        Clock.DEFAULT,\n        defaultRequestProperties,\n        false);\n  }\n\n  /**\n   * @param cronetEngine A CronetEngine.\n   * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may\n   *     be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread\n   *     hop from Cronet's internal network thread to the response handling thread. However, to\n   *     avoid slowing down overall network performance, care must be taken to make sure response\n   *     handling is a fast operation when using a direct executor.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then an {@link InvalidContentTypeException} is thrown from {@link\n   *     #open(DataSpec)}.\n   * @param connectTimeoutMs The connection timeout, in milliseconds.\n   * @param readTimeoutMs The read timeout, in milliseconds.\n   * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.\n   * @param defaultRequestProperties The default request properties to be used.\n   * @param handleSetCookieRequests Whether \"Set-Cookie\" requests on redirect should be forwarded to\n   *     the redirect url in the \"Cookie\" header.\n   */\n  public CronetDataSource(\n      CronetEngine cronetEngine,\n      Executor executor,\n      @Nullable Predicate<String> contentTypePredicate,\n      int connectTimeoutMs,\n      int readTimeoutMs,\n      boolean resetTimeoutOnRedirects,\n      RequestProperties defaultRequestProperties,\n      boolean handleSetCookieRequests) {\n    this(\n        cronetEngine,\n        executor,\n        contentTypePredicate,\n        connectTimeoutMs,\n        readTimeoutMs,\n        resetTimeoutOnRedirects,\n        Clock.DEFAULT,\n        defaultRequestProperties,\n        handleSetCookieRequests);\n  }\n\n  /* package */ CronetDataSource(\n      CronetEngine cronetEngine,\n      Executor executor,\n      @Nullable Predicate<String> contentTypePredicate,\n      int connectTimeoutMs,\n      int readTimeoutMs,\n      boolean resetTimeoutOnRedirects,\n      Clock clock,\n      RequestProperties defaultRequestProperties,\n      boolean handleSetCookieRequests) {\n    super(/* isNetwork= */ true);\n    this.urlRequestCallback = new UrlRequestCallback();\n    this.cronetEngine = Assertions.checkNotNull(cronetEngine);\n    this.executor = Assertions.checkNotNull(executor);\n    this.contentTypePredicate = contentTypePredicate;\n    this.connectTimeoutMs = connectTimeoutMs;\n    this.readTimeoutMs = readTimeoutMs;\n    this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;\n    this.clock = Assertions.checkNotNull(clock);\n    this.defaultRequestProperties = defaultRequestProperties;\n    this.handleSetCookieRequests = handleSetCookieRequests;\n    requestProperties = new RequestProperties();\n    operation = new ConditionVariable();\n  }\n\n  // HttpDataSource implementation.\n\n  @Override\n  public void setRequestProperty(String name, String value) {\n    requestProperties.set(name, value);\n  }\n\n  @Override\n  public void clearRequestProperty(String name) {\n    requestProperties.remove(name);\n  }\n\n  @Override\n  public void clearAllRequestProperties() {\n    requestProperties.clear();\n  }\n\n  @Override\n  public int getResponseCode() {\n    return responseInfo == null || responseInfo.getHttpStatusCode() <= 0\n        ? -1\n        : responseInfo.getHttpStatusCode();\n  }\n\n  @Override\n  public Map<String, List<String>> getResponseHeaders() {\n    return responseInfo == null ? Collections.emptyMap() : responseInfo.getAllHeaders();\n  }\n\n  @Override\n  public Uri getUri() {\n    return responseInfo == null ? null : Uri.parse(responseInfo.getUrl());\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws HttpDataSourceException {\n    Assertions.checkNotNull(dataSpec);\n    Assertions.checkState(!opened);\n\n    operation.close();\n    resetConnectTimeout();\n    currentDataSpec = dataSpec;\n    try {\n      currentUrlRequest = buildRequestBuilder(dataSpec).build();\n    } catch (IOException e) {\n      throw new OpenException(e, currentDataSpec, Status.IDLE);\n    }\n    currentUrlRequest.start();\n\n    transferInitializing(dataSpec);\n    try {\n      boolean connectionOpened = blockUntilConnectTimeout();\n      if (exception != null) {\n        throw new OpenException(exception, currentDataSpec, getStatus(currentUrlRequest));\n      } else if (!connectionOpened) {\n        // The timeout was reached before the connection was opened.\n        throw new OpenException(\n            new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));\n      }\n    } catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new OpenException(new InterruptedIOException(e), dataSpec, Status.INVALID);\n    }\n\n    // Check for a valid response code.\n    int responseCode = responseInfo.getHttpStatusCode();\n    if (responseCode < 200 || responseCode > 299) {\n      InvalidResponseCodeException exception =\n          new InvalidResponseCodeException(\n              responseCode,\n              responseInfo.getHttpStatusText(),\n              responseInfo.getAllHeaders(),\n              currentDataSpec);\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    if (contentTypePredicate != null) {\n      List<String> contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE);\n      String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);\n      if (!contentTypePredicate.evaluate(contentType)) {\n        throw new InvalidContentTypeException(contentType, currentDataSpec);\n      }\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    // Calculate the content length.\n    if (!getIsCompressed(responseInfo)) {\n      if (dataSpec.length != C.LENGTH_UNSET) {\n        bytesRemaining = dataSpec.length;\n      } else {\n        bytesRemaining = getContentLength(responseInfo);\n      }\n    } else {\n      // If the response is compressed then the content length will be that of the compressed data\n      // which isn't what we want. Always use the dataSpec length in this case.\n      bytesRemaining = currentDataSpec.length;\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 HttpDataSourceException {\n    Assertions.checkState(opened);\n\n    if (readLength == 0) {\n      return 0;\n    } else if (bytesRemaining == 0) {\n      return C.RESULT_END_OF_INPUT;\n    }\n\n    if (readBuffer == null) {\n      readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);\n      readBuffer.limit(0);\n    }\n    while (!readBuffer.hasRemaining()) {\n      // Fill readBuffer with more data from Cronet.\n      operation.close();\n      readBuffer.clear();\n      currentUrlRequest.read(readBuffer);\n      try {\n        if (!operation.block(readTimeoutMs)) {\n          throw new SocketTimeoutException();\n        }\n      } catch (InterruptedException e) {\n        // The operation is ongoing so replace readBuffer to avoid it being written to by this\n        // operation during a subsequent request.\n        readBuffer = null;\n        Thread.currentThread().interrupt();\n        throw new HttpDataSourceException(\n            new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ);\n      } catch (SocketTimeoutException e) {\n        // The operation is ongoing so replace readBuffer to avoid it being written to by this\n        // operation during a subsequent request.\n        readBuffer = null;\n        throw new HttpDataSourceException(e, currentDataSpec, HttpDataSourceException.TYPE_READ);\n      }\n\n      if (exception != null) {\n        throw new HttpDataSourceException(exception, currentDataSpec,\n            HttpDataSourceException.TYPE_READ);\n      } else if (finished) {\n        bytesRemaining = 0;\n        return C.RESULT_END_OF_INPUT;\n      } else {\n        // The operation didn't time out, fail or finish, and therefore data must have been read.\n        readBuffer.flip();\n        Assertions.checkState(readBuffer.hasRemaining());\n        if (bytesToSkip > 0) {\n          int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);\n          readBuffer.position(readBuffer.position() + bytesSkipped);\n          bytesToSkip -= bytesSkipped;\n        }\n      }\n    }\n\n    int bytesRead = Math.min(readBuffer.remaining(), readLength);\n    readBuffer.get(buffer, offset, bytesRead);\n\n    if (bytesRemaining != C.LENGTH_UNSET) {\n      bytesRemaining -= bytesRead;\n    }\n    bytesTransferred(bytesRead);\n    return bytesRead;\n  }\n\n  @Override\n  public synchronized void close() {\n    if (currentUrlRequest != null) {\n      currentUrlRequest.cancel();\n      currentUrlRequest = null;\n    }\n    if (readBuffer != null) {\n      readBuffer.limit(0);\n    }\n    currentDataSpec = null;\n    responseInfo = null;\n    exception = null;\n    finished = false;\n    if (opened) {\n      opened = false;\n      transferEnded();\n    }\n  }\n\n  /** Returns current {@link UrlRequest}. May be null if the data source is not opened. */\n  @Nullable\n  protected UrlRequest getCurrentUrlRequest() {\n    return currentUrlRequest;\n  }\n\n  /** Returns current {@link UrlResponseInfo}. May be null if the data source is not opened. */\n  @Nullable\n  protected UrlResponseInfo getCurrentUrlResponseInfo() {\n    return responseInfo;\n  }\n\n  // Internal methods.\n\n  private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {\n    UrlRequest.Builder requestBuilder =\n        cronetEngine\n            .newUrlRequestBuilder(dataSpec.uri.toString(), urlRequestCallback, executor)\n            .allowDirectExecutor();\n    // Set the headers.\n    boolean isContentTypeHeaderSet = false;\n    if (defaultRequestProperties != null) {\n      for (Entry<String, String> headerEntry : defaultRequestProperties.getSnapshot().entrySet()) {\n        String key = headerEntry.getKey();\n        isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);\n        requestBuilder.addHeader(key, headerEntry.getValue());\n      }\n    }\n    Map<String, String> requestPropertiesSnapshot = requestProperties.getSnapshot();\n    for (Entry<String, String> headerEntry : requestPropertiesSnapshot.entrySet()) {\n      String key = headerEntry.getKey();\n      isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);\n      requestBuilder.addHeader(key, headerEntry.getValue());\n    }\n\n    // MOD: apply headers from DataSpec\n    for (Entry<String, String> headerEntry : dataSpec.httpRequestHeaders.entrySet()) {\n      String key = headerEntry.getKey();\n      isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);\n      requestBuilder.addHeader(key, headerEntry.getValue());\n    }\n\n    if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {\n      throw new IOException(\"HTTP request with non-empty body must set Content-Type\");\n    }\n    if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {\n      requestBuilder.addHeader(\n          IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,\n          IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);\n    }\n    // Set the Range header.\n    if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {\n      StringBuilder rangeValue = new StringBuilder();\n      rangeValue.append(\"bytes=\");\n      rangeValue.append(dataSpec.position);\n      rangeValue.append(\"-\");\n      if (dataSpec.length != C.LENGTH_UNSET) {\n        rangeValue.append(dataSpec.position + dataSpec.length - 1);\n      }\n      requestBuilder.addHeader(\"Range\", rangeValue.toString());\n    }\n    // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=767025 is fixed\n    // (adjusting the code as necessary).\n    // Force identity encoding unless gzip is allowed.\n    // if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {\n    //   requestBuilder.addHeader(\"Accept-Encoding\", \"identity\");\n    // }\n    // Set the method and (if non-empty) the body.\n    requestBuilder.setHttpMethod(dataSpec.getHttpMethodString());\n    if (dataSpec.httpBody != null) {\n      requestBuilder.setUploadDataProvider(\n          new ByteArrayUploadDataProvider(dataSpec.httpBody), executor);\n    }\n    return requestBuilder;\n  }\n\n  private boolean blockUntilConnectTimeout() throws InterruptedException {\n    long now = clock.elapsedRealtime();\n    boolean opened = false;\n    while (!opened && now < currentConnectTimeoutMs) {\n      opened = operation.block(currentConnectTimeoutMs - now + 5 /* fudge factor */);\n      now = clock.elapsedRealtime();\n    }\n    return opened;\n  }\n\n  private void resetConnectTimeout() {\n    currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;\n  }\n\n  private static boolean getIsCompressed(UrlResponseInfo info) {\n    for (Map.Entry<String, String> entry : info.getAllHeadersAsList()) {\n      if (entry.getKey().equalsIgnoreCase(\"Content-Encoding\")) {\n        return !entry.getValue().equalsIgnoreCase(\"identity\");\n      }\n    }\n    return false;\n  }\n\n  private static long getContentLength(UrlResponseInfo info) {\n    long contentLength = C.LENGTH_UNSET;\n    Map<String, List<String>> headers = info.getAllHeaders();\n    List<String> contentLengthHeaders = headers.get(\"Content-Length\");\n    String contentLengthHeader = null;\n    if (!isEmpty(contentLengthHeaders)) {\n      contentLengthHeader = contentLengthHeaders.get(0);\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    }\n    List<String> contentRangeHeaders = headers.get(\"Content-Range\");\n    if (!isEmpty(contentRangeHeaders)) {\n      String contentRangeHeader = contentRangeHeaders.get(0);\n      Matcher matcher = CONTENT_RANGE_HEADER_PATTERN.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\n            // would 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  private static String parseCookies(List<String> setCookieHeaders) {\n    return TextUtils.join(\";\", setCookieHeaders);\n  }\n\n  private static void attachCookies(UrlRequest.Builder requestBuilder, String cookies) {\n    if (TextUtils.isEmpty(cookies)) {\n      return;\n    }\n    requestBuilder.addHeader(COOKIE, cookies);\n  }\n\n  private static int getStatus(UrlRequest request) throws InterruptedException {\n    final ConditionVariable conditionVariable = new ConditionVariable();\n    final int[] statusHolder = new int[1];\n    request.getStatus(new UrlRequest.StatusListener() {\n      @Override\n      public void onStatus(int status) {\n        statusHolder[0] = status;\n        conditionVariable.open();\n      }\n    });\n    conditionVariable.block();\n    return statusHolder[0];\n  }\n\n  private static boolean isEmpty(List<?> list) {\n    return list == null || list.isEmpty();\n  }\n\n  private final class UrlRequestCallback extends UrlRequest.Callback {\n\n    @Override\n    public synchronized void onRedirectReceived(\n        UrlRequest request, UrlResponseInfo info, String newLocationUrl) {\n      if (request != currentUrlRequest) {\n        return;\n      }\n      if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {\n        int responseCode = info.getHttpStatusCode();\n        // The industry standard is to disregard POST redirects when the status code is 307 or 308.\n        if (responseCode == 307 || responseCode == 308) {\n          exception =\n              new InvalidResponseCodeException(\n                  responseCode, info.getHttpStatusText(), info.getAllHeaders(), currentDataSpec);\n          operation.open();\n          return;\n        }\n      }\n      if (resetTimeoutOnRedirects) {\n        resetConnectTimeout();\n      }\n\n      Map<String, List<String>> headers = info.getAllHeaders();\n      if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) {\n        request.followRedirect();\n      } else {\n        currentUrlRequest.cancel();\n        DataSpec redirectUrlDataSpec;\n        if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {\n          // For POST redirects that aren't 307 or 308, the redirect is followed but request is\n          // transformed into a GET.\n          redirectUrlDataSpec =\n              new DataSpec(\n                  Uri.parse(newLocationUrl),\n                  DataSpec.HTTP_METHOD_GET,\n                  /* httpBody= */ null,\n                  currentDataSpec.absoluteStreamPosition,\n                  currentDataSpec.position,\n                  currentDataSpec.length,\n                  currentDataSpec.key,\n                  currentDataSpec.flags);\n        } else {\n          redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl));\n        }\n        UrlRequest.Builder requestBuilder;\n        try {\n          requestBuilder = buildRequestBuilder(redirectUrlDataSpec);\n        } catch (IOException e) {\n          exception = e;\n          return;\n        }\n        String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE));\n        attachCookies(requestBuilder, cookieHeadersValue);\n        currentUrlRequest = requestBuilder.build();\n        currentUrlRequest.start();\n      }\n    }\n\n    @Override\n    public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {\n      if (request != currentUrlRequest) {\n        return;\n      }\n      responseInfo = info;\n      operation.open();\n    }\n\n    @Override\n    public synchronized void onReadCompleted(\n        UrlRequest request, UrlResponseInfo info, ByteBuffer buffer) {\n      if (request != currentUrlRequest) {\n        return;\n      }\n      operation.open();\n    }\n\n    @Override\n    public synchronized void onSucceeded(UrlRequest request, UrlResponseInfo info) {\n      if (request != currentUrlRequest) {\n        return;\n      }\n      finished = true;\n      operation.open();\n    }\n\n    @Override\n    public synchronized void onFailed(\n        UrlRequest request, UrlResponseInfo info, CronetException error) {\n      if (request != currentUrlRequest) {\n        return;\n      }\n      if (error instanceof NetworkException\n          && ((NetworkException) error).getErrorCode()\n              == NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) {\n        exception = new UnknownHostException();\n      } else {\n        exception = error;\n      }\n      operation.open();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.cronet;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;\nimport com.google.android.exoplayer2.upstream.HttpDataSource;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.Factory;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Predicate;\nimport java.util.concurrent.Executor;\nimport org.chromium.net.CronetEngine;\n\n/**\n * A {@link Factory} that produces {@link CronetDataSource}.\n */\npublic final class CronetDataSourceFactory extends BaseFactory {\n\n  /**\n   * The default connection timeout, in milliseconds.\n   */\n  public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS =\n      CronetDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS;\n\n  /**\n   * The default read timeout, in milliseconds.\n   */\n  public static final int DEFAULT_READ_TIMEOUT_MILLIS =\n      CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS;\n\n  private final CronetEngineWrapper cronetEngineWrapper;\n  private final Executor executor;\n  private final Predicate<String> contentTypePredicate;\n  private final @Nullable TransferListener transferListener;\n  private final int connectTimeoutMs;\n  private final int readTimeoutMs;\n  private final boolean resetTimeoutOnRedirects;\n  private final HttpDataSource.Factory fallbackFactory;\n\n  /**\n   * Constructs a CronetDataSourceFactory.\n   *\n   * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided\n   * fallback {@link HttpDataSource.Factory} will be used instead.\n   *\n   * <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,\n   * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables\n   * cross-protocol redirects.\n   *\n   * @param cronetEngineWrapper A {@link CronetEngineWrapper}.\n   * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then an {@link InvalidContentTypeException} is thrown from {@link\n   *     CronetDataSource#open}.\n   * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no\n   *     suitable CronetEngine can be build.\n   */\n  public CronetDataSourceFactory(\n      CronetEngineWrapper cronetEngineWrapper,\n      Executor executor,\n      Predicate<String> contentTypePredicate,\n      HttpDataSource.Factory fallbackFactory) {\n    this(\n        cronetEngineWrapper,\n        executor,\n        contentTypePredicate,\n        /* transferListener= */ null,\n        DEFAULT_CONNECT_TIMEOUT_MILLIS,\n        DEFAULT_READ_TIMEOUT_MILLIS,\n        false,\n        fallbackFactory);\n  }\n\n  /**\n   * Constructs a CronetDataSourceFactory.\n   *\n   * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link\n   * DefaultHttpDataSourceFactory} will be used instead.\n   *\n   * <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,\n   * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables\n   * cross-protocol redirects.\n   *\n   * @param cronetEngineWrapper A {@link CronetEngineWrapper}.\n   * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then an {@link InvalidContentTypeException} is thrown from {@link\n   *     CronetDataSource#open}.\n   * @param userAgent A user agent used to create a fallback HttpDataSource if needed.\n   */\n  public CronetDataSourceFactory(\n      CronetEngineWrapper cronetEngineWrapper,\n      Executor executor,\n      Predicate<String> contentTypePredicate,\n      String userAgent) {\n    this(\n        cronetEngineWrapper,\n        executor,\n        contentTypePredicate,\n        /* transferListener= */ null,\n        DEFAULT_CONNECT_TIMEOUT_MILLIS,\n        DEFAULT_READ_TIMEOUT_MILLIS,\n        false,\n        new DefaultHttpDataSourceFactory(\n            userAgent,\n            /* listener= */ null,\n            DEFAULT_CONNECT_TIMEOUT_MILLIS,\n            DEFAULT_READ_TIMEOUT_MILLIS,\n            false));\n  }\n\n  /**\n   * Constructs a CronetDataSourceFactory.\n   *\n   * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link\n   * DefaultHttpDataSourceFactory} will be used instead.\n   *\n   * @param cronetEngineWrapper A {@link CronetEngineWrapper}.\n   * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then an {@link InvalidContentTypeException} is thrown from {@link\n   *     CronetDataSource#open}.\n   * @param connectTimeoutMs The connection timeout, in milliseconds.\n   * @param readTimeoutMs The read timeout, in milliseconds.\n   * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.\n   * @param userAgent A user agent used to create a fallback HttpDataSource if needed.\n   */\n  public CronetDataSourceFactory(\n      CronetEngineWrapper cronetEngineWrapper,\n      Executor executor,\n      Predicate<String> contentTypePredicate,\n      int connectTimeoutMs,\n      int readTimeoutMs,\n      boolean resetTimeoutOnRedirects,\n      String userAgent) {\n    this(\n        cronetEngineWrapper,\n        executor,\n        contentTypePredicate,\n        /* transferListener= */ null,\n        DEFAULT_CONNECT_TIMEOUT_MILLIS,\n        DEFAULT_READ_TIMEOUT_MILLIS,\n        resetTimeoutOnRedirects,\n        new DefaultHttpDataSourceFactory(\n            userAgent,\n            /* listener= */ null,\n            connectTimeoutMs,\n            readTimeoutMs,\n            resetTimeoutOnRedirects));\n  }\n\n  /**\n   * Constructs a CronetDataSourceFactory.\n   *\n   * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided\n   * fallback {@link HttpDataSource.Factory} will be used instead.\n   *\n   * @param cronetEngineWrapper A {@link CronetEngineWrapper}.\n   * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then an {@link InvalidContentTypeException} is thrown from {@link\n   *     CronetDataSource#open}.\n   * @param connectTimeoutMs The connection timeout, in milliseconds.\n   * @param readTimeoutMs The read timeout, in milliseconds.\n   * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.\n   * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no\n   *     suitable CronetEngine can be build.\n   */\n  public CronetDataSourceFactory(\n      CronetEngineWrapper cronetEngineWrapper,\n      Executor executor,\n      Predicate<String> contentTypePredicate,\n      int connectTimeoutMs,\n      int readTimeoutMs,\n      boolean resetTimeoutOnRedirects,\n      HttpDataSource.Factory fallbackFactory) {\n    this(\n        cronetEngineWrapper,\n        executor,\n        contentTypePredicate,\n        /* transferListener= */ null,\n        connectTimeoutMs,\n        readTimeoutMs,\n        resetTimeoutOnRedirects,\n        fallbackFactory);\n  }\n\n  /**\n   * Constructs a CronetDataSourceFactory.\n   *\n   * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided\n   * fallback {@link HttpDataSource.Factory} will be used instead.\n   *\n   * <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,\n   * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables\n   * cross-protocol redirects.\n   *\n   * @param cronetEngineWrapper A {@link CronetEngineWrapper}.\n   * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then an {@link InvalidContentTypeException} is thrown from {@link\n   *     CronetDataSource#open}.\n   * @param transferListener An optional listener.\n   * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no\n   *     suitable CronetEngine can be build.\n   */\n  public CronetDataSourceFactory(\n      CronetEngineWrapper cronetEngineWrapper,\n      Executor executor,\n      Predicate<String> contentTypePredicate,\n      @Nullable TransferListener transferListener,\n      HttpDataSource.Factory fallbackFactory) {\n    this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,\n        DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);\n  }\n\n  /**\n   * Constructs a CronetDataSourceFactory.\n   *\n   * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link\n   * DefaultHttpDataSourceFactory} will be used instead.\n   *\n   * <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,\n   * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables\n   * cross-protocol redirects.\n   *\n   * @param cronetEngineWrapper A {@link CronetEngineWrapper}.\n   * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then an {@link InvalidContentTypeException} is thrown from {@link\n   *     CronetDataSource#open}.\n   * @param transferListener An optional listener.\n   * @param userAgent A user agent used to create a fallback HttpDataSource if needed.\n   */\n  public CronetDataSourceFactory(\n      CronetEngineWrapper cronetEngineWrapper,\n      Executor executor,\n      Predicate<String> contentTypePredicate,\n      @Nullable TransferListener transferListener,\n      String userAgent) {\n    this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,\n        DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,\n        new DefaultHttpDataSourceFactory(userAgent, transferListener,\n            DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false));\n  }\n\n  /**\n   * Constructs a CronetDataSourceFactory.\n   *\n   * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link\n   * DefaultHttpDataSourceFactory} will be used instead.\n   *\n   * @param cronetEngineWrapper A {@link CronetEngineWrapper}.\n   * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then an {@link InvalidContentTypeException} is thrown from {@link\n   *     CronetDataSource#open}.\n   * @param transferListener An optional listener.\n   * @param connectTimeoutMs The connection timeout, in milliseconds.\n   * @param readTimeoutMs The read timeout, in milliseconds.\n   * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.\n   * @param userAgent A user agent used to create a fallback HttpDataSource if needed.\n   */\n  public CronetDataSourceFactory(\n      CronetEngineWrapper cronetEngineWrapper,\n      Executor executor,\n      Predicate<String> contentTypePredicate,\n      @Nullable TransferListener transferListener,\n      int connectTimeoutMs,\n      int readTimeoutMs,\n      boolean resetTimeoutOnRedirects,\n      String userAgent) {\n    this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,\n        DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,\n        new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,\n            readTimeoutMs, resetTimeoutOnRedirects));\n  }\n\n  /**\n   * Constructs a CronetDataSourceFactory.\n   *\n   * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided\n   * fallback {@link HttpDataSource.Factory} will be used instead.\n   *\n   * @param cronetEngineWrapper A {@link CronetEngineWrapper}.\n   * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.\n   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the\n   *     predicate then an {@link InvalidContentTypeException} is thrown from {@link\n   *     CronetDataSource#open}.\n   * @param transferListener An optional listener.\n   * @param connectTimeoutMs The connection timeout, in milliseconds.\n   * @param readTimeoutMs The read timeout, in milliseconds.\n   * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.\n   * @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no\n   *     suitable CronetEngine can be build.\n   */\n  public CronetDataSourceFactory(\n      CronetEngineWrapper cronetEngineWrapper,\n      Executor executor,\n      Predicate<String> contentTypePredicate,\n      @Nullable TransferListener transferListener,\n      int connectTimeoutMs,\n      int readTimeoutMs,\n      boolean resetTimeoutOnRedirects,\n      HttpDataSource.Factory fallbackFactory) {\n    this.cronetEngineWrapper = cronetEngineWrapper;\n    this.executor = executor;\n    this.contentTypePredicate = contentTypePredicate;\n    this.transferListener = transferListener;\n    this.connectTimeoutMs = connectTimeoutMs;\n    this.readTimeoutMs = readTimeoutMs;\n    this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;\n    this.fallbackFactory = fallbackFactory;\n  }\n\n  @Override\n  protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperties\n      defaultRequestProperties) {\n    CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine();\n    if (cronetEngine == null) {\n      return fallbackFactory.createDataSource();\n    }\n    CronetDataSource dataSource =\n        new CronetDataSource(\n            cronetEngine,\n            executor,\n            contentTypePredicate,\n            connectTimeoutMs,\n            readTimeoutMs,\n            resetTimeoutOnRedirects,\n            defaultRequestProperties);\n    if (transferListener != null) {\n      dataSource.addTransferListener(transferListener);\n    }\n    return dataSource;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.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.ext.cronet;\n\nimport android.content.Context;\nimport androidx.annotation.IntDef;\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.lang.reflect.Field;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport org.chromium.net.CronetEngine;\nimport org.chromium.net.CronetProvider;\n\n/**\n * A wrapper class for a {@link CronetEngine}.\n */\npublic final class CronetEngineWrapper {\n\n  private static final String TAG = \"CronetEngineWrapper\";\n\n  private final CronetEngine cronetEngine;\n  private final @CronetEngineSource int cronetEngineSource;\n\n  /**\n   * Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link\n   * #SOURCE_UNKNOWN}, {@link #SOURCE_USER_PROVIDED} or {@link #SOURCE_UNAVAILABLE}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({SOURCE_NATIVE, SOURCE_GMS, SOURCE_UNKNOWN, SOURCE_USER_PROVIDED, SOURCE_UNAVAILABLE})\n  public @interface CronetEngineSource {}\n  /**\n   * Natively bundled Cronet implementation.\n   */\n  public static final int SOURCE_NATIVE = 0;\n  /**\n   * Cronet implementation from GMSCore.\n   */\n  public static final int SOURCE_GMS = 1;\n  /**\n   * Other (unknown) Cronet implementation.\n   */\n  public static final int SOURCE_UNKNOWN = 2;\n  /**\n   * User-provided Cronet engine.\n   */\n  public static final int SOURCE_USER_PROVIDED = 3;\n  /**\n   * No Cronet implementation available. Fallback Http provider is used if possible.\n   */\n  public static final int SOURCE_UNAVAILABLE = 4;\n\n  /**\n   * Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable\n   * {@link CronetProvider}. Sets wrapper to prefer natively bundled Cronet over GMSCore Cronet\n   * if both are available.\n   *\n   * @param context A context.\n   */\n  public CronetEngineWrapper(Context context) {\n    this(context, false);\n  }\n\n  /**\n   * Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable\n   * {@link CronetProvider} based on user preference.\n   *\n   * @param context A context.\n   * @param preferGMSCoreCronet Whether Cronet from GMSCore should be preferred over natively\n   *     bundled Cronet if both are available.\n   */\n  public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {\n    CronetEngine cronetEngine = null;\n    @CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;\n    List<CronetProvider> cronetProviders = new ArrayList<>(CronetProvider.getAllProviders(context));\n    // Remove disabled and fallback Cronet providers from list\n    for (int i = cronetProviders.size() - 1; i >= 0; i--) {\n      if (!cronetProviders.get(i).isEnabled()\n          || CronetProvider.PROVIDER_NAME_FALLBACK.equals(cronetProviders.get(i).getName())) {\n        cronetProviders.remove(i);\n      }\n    }\n    // Sort remaining providers by type and version.\n    CronetProviderComparator providerComparator = new CronetProviderComparator(preferGMSCoreCronet);\n    Collections.sort(cronetProviders, providerComparator);\n    for (int i = 0; i < cronetProviders.size() && cronetEngine == null; i++) {\n      String providerName = cronetProviders.get(i).getName();\n      try {\n        // MODIFIED: enable Quic\n        //cronetEngine = cronetProviders.get(i).createBuilder().build();\n        cronetEngine = cronetProviders.get(i).createBuilder()\n                .enableQuic(true)\n                .enableHttp2(true)\n                .enableBrotli(true)\n                .build();\n        if (providerComparator.isNativeProvider(providerName)) {\n          cronetEngineSource = SOURCE_NATIVE;\n        } else if (providerComparator.isGMSCoreProvider(providerName)) {\n          cronetEngineSource = SOURCE_GMS;\n        } else {\n          cronetEngineSource = SOURCE_UNKNOWN;\n        }\n        Log.d(TAG, \"CronetEngine built using \" + providerName);\n      } catch (SecurityException e) {\n        Log.w(TAG, \"Failed to build CronetEngine. Please check if current process has \"\n            + \"android.permission.ACCESS_NETWORK_STATE.\");\n      } catch (UnsatisfiedLinkError e) {\n        Log.w(TAG, \"Failed to link Cronet binaries. Please check if native Cronet binaries are \"\n            + \"bundled into your app.\");\n      }\n    }\n    if (cronetEngine == null) {\n      Log.w(TAG, \"Cronet not available. Using fallback provider.\");\n    }\n    this.cronetEngine = cronetEngine;\n    this.cronetEngineSource = cronetEngineSource;\n  }\n\n  /**\n   * Creates a wrapper for an existing CronetEngine.\n   *\n   * @param cronetEngine An existing CronetEngine.\n   */\n  public CronetEngineWrapper(CronetEngine cronetEngine) {\n    this.cronetEngine = cronetEngine;\n    this.cronetEngineSource = SOURCE_USER_PROVIDED;\n  }\n\n  /**\n   * Returns the source of the wrapped {@link CronetEngine}.\n   *\n   * @return A {@link CronetEngineSource} value.\n   */\n  public @CronetEngineSource int getCronetEngineSource() {\n    return cronetEngineSource;\n  }\n\n  /**\n   * Returns the wrapped {@link CronetEngine}.\n   *\n   * @return The CronetEngine, or null if no CronetEngine is available.\n   */\n  /* package */ CronetEngine getCronetEngine() {\n    return cronetEngine;\n  }\n\n  private static class CronetProviderComparator implements Comparator<CronetProvider> {\n\n    private final String gmsCoreCronetName;\n    private final boolean preferGMSCoreCronet;\n\n    // Multi-catch can only be used for API 19+ in this case.\n    @SuppressWarnings(\"UseMultiCatch\")\n    public CronetProviderComparator(boolean preferGMSCoreCronet) {\n      // GMSCore CronetProvider classes are only available in some configurations.\n      // Thus, we use reflection to copy static name.\n      String gmsCoreVersionString = null;\n      try {\n        Class<?> cronetProviderInstallerClass =\n            Class.forName(\"com.google.android.gms.net.CronetProviderInstaller\");\n        Field providerNameField = cronetProviderInstallerClass.getDeclaredField(\"PROVIDER_NAME\");\n        gmsCoreVersionString = (String) providerNameField.get(null);\n      } catch (ClassNotFoundException e) {\n        // GMSCore CronetProvider not available.\n      } catch (NoSuchFieldException e) {\n        // GMSCore CronetProvider not available.\n      } catch (IllegalAccessException e) {\n        // GMSCore CronetProvider not available.\n      }\n      gmsCoreCronetName = gmsCoreVersionString;\n      this.preferGMSCoreCronet = preferGMSCoreCronet;\n    }\n\n    @Override\n    public int compare(CronetProvider providerLeft, CronetProvider providerRight) {\n      int typePreferenceLeft = evaluateCronetProviderType(providerLeft.getName());\n      int typePreferenceRight = evaluateCronetProviderType(providerRight.getName());\n      if (typePreferenceLeft != typePreferenceRight) {\n        return typePreferenceLeft - typePreferenceRight;\n      }\n      return -compareVersionStrings(providerLeft.getVersion(), providerRight.getVersion());\n    }\n\n    public boolean isNativeProvider(String providerName) {\n      return CronetProvider.PROVIDER_NAME_APP_PACKAGED.equals(providerName);\n    }\n\n    public boolean isGMSCoreProvider(String providerName) {\n      return gmsCoreCronetName != null && gmsCoreCronetName.equals(providerName);\n    }\n\n    /**\n     * Convert Cronet provider name into a sortable preference value.\n     * Smaller values are preferred.\n     */\n    private int evaluateCronetProviderType(String providerName) {\n      if (isNativeProvider(providerName)) {\n        return 1;\n      }\n      if (isGMSCoreProvider(providerName)) {\n        return preferGMSCoreCronet ? 0 : 2;\n      }\n      // Unknown provider type.\n      return -1;\n    }\n\n    /**\n     * Compares version strings of format \"12.123.35.23\".\n     */\n    private static int compareVersionStrings(String versionLeft, String versionRight) {\n      if (versionLeft == null || versionRight == null) {\n        return 0;\n      }\n      String[] versionStringsLeft = Util.split(versionLeft, \"\\\\.\");\n      String[] versionStringsRight = Util.split(versionRight, \"\\\\.\");\n      int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length);\n      for (int i = 0; i < minLength; i++) {\n        if (!versionStringsLeft[i].equals(versionStringsRight[i])) {\n          try {\n            int versionIntLeft = Integer.parseInt(versionStringsLeft[i]);\n            int versionIntRight = Integer.parseInt(versionStringsRight[i]);\n            return versionIntLeft - versionIntRight;\n          } catch (NumberFormatException e) {\n            return 0;\n          }\n        }\n      }\n      return 0;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cronet/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest package=\"com.google.android.exoplayer2.ext.cronet\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.cronet;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.util.Arrays;\nimport org.chromium.net.UploadDataSink;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.MockitoAnnotations;\n\n/** Tests for {@link ByteArrayUploadDataProvider}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ByteArrayUploadDataProviderTest {\n\n  private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};\n\n  @Mock private UploadDataSink mockUploadDataSink;\n  private ByteBuffer byteBuffer;\n  private ByteArrayUploadDataProvider byteArrayUploadDataProvider;\n\n  @Before\n  public void setUp() {\n    MockitoAnnotations.initMocks(this);\n    byteBuffer = ByteBuffer.allocate(TEST_DATA.length);\n    byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA);\n  }\n\n  @Test\n  public void testGetLength() {\n    assertThat(byteArrayUploadDataProvider.getLength()).isEqualTo(TEST_DATA.length);\n  }\n\n  @Test\n  public void testReadFullBuffer() throws IOException {\n    byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);\n    assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);\n  }\n\n  @Test\n  public void testReadPartialBuffer() throws IOException {\n    byte[] firstHalf = Arrays.copyOf(TEST_DATA, TEST_DATA.length / 2);\n    byte[] secondHalf = Arrays.copyOfRange(TEST_DATA, TEST_DATA.length / 2, TEST_DATA.length);\n    byteBuffer = ByteBuffer.allocate(TEST_DATA.length / 2);\n    // Read half of the data.\n    byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);\n    assertThat(byteBuffer.array()).isEqualTo(firstHalf);\n\n    // Read the second half of the data.\n    byteBuffer.rewind();\n    byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);\n    assertThat(byteBuffer.array()).isEqualTo(secondHalf);\n    verify(mockUploadDataSink, times(2)).onReadSucceeded(false);\n  }\n\n  @Test\n  public void testRewind() throws IOException {\n    // Read all the data.\n    byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);\n    assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);\n\n    // Rewind and make sure it can be read again.\n    byteBuffer.clear();\n    byteArrayUploadDataProvider.rewind(mockUploadDataSink);\n    byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);\n    assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);\n    verify(mockUploadDataSink).onRewindSucceeded();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.cronet;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Matchers.anyString;\nimport static org.mockito.Matchers.eq;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.doThrow;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport android.net.Uri;\nimport android.os.ConditionVariable;\nimport android.os.SystemClock;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.HttpDataSource;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.util.Predicate;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.net.SocketTimeoutException;\nimport java.net.UnknownHostException;\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicReference;\nimport org.chromium.net.CronetEngine;\nimport org.chromium.net.NetworkException;\nimport org.chromium.net.UrlRequest;\nimport org.chromium.net.UrlResponseInfo;\nimport org.chromium.net.impl.UrlResponseInfoImpl;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.MockitoAnnotations;\n\n/** Tests for {@link CronetDataSource}. */\n@RunWith(AndroidJUnit4.class)\npublic final class CronetDataSourceTest {\n\n  private static final int TEST_CONNECT_TIMEOUT_MS = 100;\n  private static final int TEST_READ_TIMEOUT_MS = 100;\n  private static final String TEST_URL = \"http://google.com\";\n  private static final String TEST_CONTENT_TYPE = \"test/test\";\n  private static final byte[] TEST_POST_BODY = Util.getUtf8Bytes(\"test post body\");\n  private static final long TEST_CONTENT_LENGTH = 16000L;\n  private static final int TEST_CONNECTION_STATUS = 5;\n  private static final int TEST_INVALID_CONNECTION_STATUS = -1;\n\n  private DataSpec testDataSpec;\n  private DataSpec testPostDataSpec;\n  private DataSpec testHeadDataSpec;\n  private Map<String, String> testResponseHeader;\n  private UrlResponseInfo testUrlResponseInfo;\n\n  @Mock private UrlRequest.Builder mockUrlRequestBuilder;\n  @Mock private UrlRequest mockUrlRequest;\n  @Mock private Predicate<String> mockContentTypePredicate;\n  @Mock private TransferListener mockTransferListener;\n  @Mock private Executor mockExecutor;\n  @Mock private NetworkException mockNetworkException;\n  @Mock private CronetEngine mockCronetEngine;\n\n  private CronetDataSource dataSourceUnderTest;\n  private boolean redirectCalled;\n\n  @Before\n  public void setUp() throws Exception {\n    MockitoAnnotations.initMocks(this);\n    dataSourceUnderTest =\n        new CronetDataSource(\n            mockCronetEngine,\n            mockExecutor,\n            mockContentTypePredicate,\n            TEST_CONNECT_TIMEOUT_MS,\n            TEST_READ_TIMEOUT_MS,\n            true, // resetTimeoutOnRedirects\n            Clock.DEFAULT,\n            null,\n            false);\n    dataSourceUnderTest.addTransferListener(mockTransferListener);\n    when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);\n    when(mockCronetEngine.newUrlRequestBuilder(\n            anyString(), any(UrlRequest.Callback.class), any(Executor.class)))\n        .thenReturn(mockUrlRequestBuilder);\n    when(mockUrlRequestBuilder.allowDirectExecutor()).thenReturn(mockUrlRequestBuilder);\n    when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);\n    mockStatusResponse();\n\n    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null);\n    testPostDataSpec =\n        new DataSpec(Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0);\n    testHeadDataSpec =\n        new DataSpec(\n            Uri.parse(TEST_URL), DataSpec.HTTP_METHOD_HEAD, null, 0, 0, C.LENGTH_UNSET, null, 0);\n    testResponseHeader = new HashMap<>();\n    testResponseHeader.put(\"Content-Type\", TEST_CONTENT_TYPE);\n    // This value can be anything since the DataSpec is unset.\n    testResponseHeader.put(\"Content-Length\", Long.toString(TEST_CONTENT_LENGTH));\n    testUrlResponseInfo = createUrlResponseInfo(200); // statusCode\n  }\n\n  private UrlResponseInfo createUrlResponseInfo(int statusCode) {\n    return createUrlResponseInfoWithUrl(TEST_URL, statusCode);\n  }\n\n  private UrlResponseInfo createUrlResponseInfoWithUrl(String url, int statusCode) {\n    ArrayList<Map.Entry<String, String>> responseHeaderList = new ArrayList<>();\n    responseHeaderList.addAll(testResponseHeader.entrySet());\n    return new UrlResponseInfoImpl(\n        Collections.singletonList(url),\n        statusCode,\n        null, // httpStatusText\n        responseHeaderList,\n        false, // wasCached\n        null, // negotiatedProtocol\n        null); // proxyServer\n  }\n\n  @Test\n  public void testOpeningTwiceThrows() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n    dataSourceUnderTest.open(testDataSpec);\n    try {\n      dataSourceUnderTest.open(testDataSpec);\n      fail(\"Expected IllegalStateException.\");\n    } catch (IllegalStateException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testCallbackFromPreviousRequest() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n\n    dataSourceUnderTest.open(testDataSpec);\n    dataSourceUnderTest.close();\n    // Prepare a mock UrlRequest to be used in the second open() call.\n    final UrlRequest mockUrlRequest2 = mock(UrlRequest.class);\n    when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest2);\n    doAnswer(\n            invocation -> {\n              // Invoke the callback for the previous request.\n              dataSourceUnderTest.urlRequestCallback.onFailed(\n                  mockUrlRequest, testUrlResponseInfo, mockNetworkException);\n              dataSourceUnderTest.urlRequestCallback.onResponseStarted(\n                  mockUrlRequest2, testUrlResponseInfo);\n              return null;\n            })\n        .when(mockUrlRequest2)\n        .start();\n    dataSourceUnderTest.open(testDataSpec);\n  }\n\n  @Test\n  public void testRequestStartCalled() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n\n    dataSourceUnderTest.open(testDataSpec);\n    verify(mockCronetEngine)\n        .newUrlRequestBuilder(eq(TEST_URL), any(UrlRequest.Callback.class), any(Executor.class));\n    verify(mockUrlRequest).start();\n  }\n\n  @Test\n  public void testRequestHeadersSet() throws HttpDataSourceException {\n    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);\n    mockResponseStartSuccess();\n\n    dataSourceUnderTest.setRequestProperty(\"firstHeader\", \"firstValue\");\n    dataSourceUnderTest.setRequestProperty(\"secondHeader\", \"secondValue\");\n\n    dataSourceUnderTest.open(testDataSpec);\n    // The header value to add is current position to current position + length - 1.\n    verify(mockUrlRequestBuilder).addHeader(\"Range\", \"bytes=1000-5999\");\n    verify(mockUrlRequestBuilder).addHeader(\"firstHeader\", \"firstValue\");\n    verify(mockUrlRequestBuilder).addHeader(\"secondHeader\", \"secondValue\");\n    verify(mockUrlRequest).start();\n  }\n\n  @Test\n  public void testRequestOpen() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n    assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);\n    verify(mockTransferListener)\n        .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);\n  }\n\n  @Test\n  public void testRequestOpenGzippedCompressedReturnsDataSpecLength()\n      throws HttpDataSourceException {\n    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 5000, null);\n    testResponseHeader.put(\"Content-Encoding\", \"gzip\");\n    testResponseHeader.put(\"Content-Length\", Long.toString(50L));\n    mockResponseStartSuccess();\n\n    assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(5000 /* contentLength */);\n    verify(mockTransferListener)\n        .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);\n  }\n\n  @Test\n  public void testRequestOpenFail() {\n    mockResponseStartFailure();\n\n    try {\n      dataSourceUnderTest.open(testDataSpec);\n      fail(\"HttpDataSource.HttpDataSourceException expected\");\n    } catch (HttpDataSourceException e) {\n      // Check for connection not automatically closed.\n      assertThat(e.getCause() instanceof UnknownHostException).isFalse();\n      verify(mockUrlRequest, never()).cancel();\n      verify(mockTransferListener, never())\n          .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);\n    }\n  }\n\n  @Test\n  public void testRequestOpenFailDueToDnsFailure() {\n    mockResponseStartFailure();\n    when(mockNetworkException.getErrorCode())\n        .thenReturn(NetworkException.ERROR_HOSTNAME_NOT_RESOLVED);\n\n    try {\n      dataSourceUnderTest.open(testDataSpec);\n      fail(\"HttpDataSource.HttpDataSourceException expected\");\n    } catch (HttpDataSourceException e) {\n      // Check for connection not automatically closed.\n      assertThat(e.getCause() instanceof UnknownHostException).isTrue();\n      verify(mockUrlRequest, never()).cancel();\n      verify(mockTransferListener, never())\n          .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);\n    }\n  }\n\n  @Test\n  public void testRequestOpenValidatesStatusCode() {\n    mockResponseStartSuccess();\n    testUrlResponseInfo = createUrlResponseInfo(500); // statusCode\n\n    try {\n      dataSourceUnderTest.open(testDataSpec);\n      fail(\"HttpDataSource.HttpDataSourceException expected\");\n    } catch (HttpDataSourceException e) {\n      assertThat(e instanceof HttpDataSource.InvalidResponseCodeException).isTrue();\n      // Check for connection not automatically closed.\n      verify(mockUrlRequest, never()).cancel();\n      verify(mockTransferListener, never())\n          .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);\n    }\n  }\n\n  @Test\n  public void testRequestOpenValidatesContentTypePredicate() {\n    mockResponseStartSuccess();\n    when(mockContentTypePredicate.evaluate(anyString())).thenReturn(false);\n\n    try {\n      dataSourceUnderTest.open(testDataSpec);\n      fail(\"HttpDataSource.HttpDataSourceException expected\");\n    } catch (HttpDataSourceException e) {\n      assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue();\n      // Check for connection not automatically closed.\n      verify(mockUrlRequest, never()).cancel();\n      verify(mockContentTypePredicate).evaluate(TEST_CONTENT_TYPE);\n    }\n  }\n\n  @Test\n  public void testPostRequestOpen() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n\n    dataSourceUnderTest.setRequestProperty(\"Content-Type\", TEST_CONTENT_TYPE);\n    assertThat(dataSourceUnderTest.open(testPostDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);\n    verify(mockTransferListener)\n        .onTransferStart(dataSourceUnderTest, testPostDataSpec, /* isNetwork= */ true);\n  }\n\n  @Test\n  public void testPostRequestOpenValidatesContentType() {\n    mockResponseStartSuccess();\n\n    try {\n      dataSourceUnderTest.open(testPostDataSpec);\n      fail(\"HttpDataSource.HttpDataSourceException expected\");\n    } catch (HttpDataSourceException e) {\n      verify(mockUrlRequest, never()).start();\n    }\n  }\n\n  @Test\n  public void testPostRequestOpenRejects307Redirects() {\n    mockResponseStartSuccess();\n    mockResponseStartRedirect();\n\n    try {\n      dataSourceUnderTest.setRequestProperty(\"Content-Type\", TEST_CONTENT_TYPE);\n      dataSourceUnderTest.open(testPostDataSpec);\n      fail(\"HttpDataSource.HttpDataSourceException expected\");\n    } catch (HttpDataSourceException e) {\n      verify(mockUrlRequest, never()).followRedirect();\n    }\n  }\n\n  @Test\n  public void testHeadRequestOpen() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n    dataSourceUnderTest.open(testHeadDataSpec);\n    verify(mockTransferListener)\n        .onTransferStart(dataSourceUnderTest, testHeadDataSpec, /* isNetwork= */ true);\n    dataSourceUnderTest.close();\n  }\n\n  @Test\n  public void testRequestReadTwice() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n    mockReadSuccess(0, 16);\n\n    dataSourceUnderTest.open(testDataSpec);\n\n    byte[] returnedBuffer = new byte[8];\n    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);\n    assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));\n    assertThat(bytesRead).isEqualTo(8);\n\n    returnedBuffer = new byte[8];\n    bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);\n    assertThat(returnedBuffer).isEqualTo(buildTestDataArray(8, 8));\n    assertThat(bytesRead).isEqualTo(8);\n\n    // Should have only called read on cronet once.\n    verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));\n    verify(mockTransferListener, times(2))\n        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);\n  }\n\n  @Test\n  public void testSecondRequestNoContentLength() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n    testResponseHeader.put(\"Content-Length\", Long.toString(1L));\n    mockReadSuccess(0, 16);\n\n    // First request.\n    dataSourceUnderTest.open(testDataSpec);\n    byte[] returnedBuffer = new byte[8];\n    dataSourceUnderTest.read(returnedBuffer, 0, 1);\n    dataSourceUnderTest.close();\n\n    testResponseHeader.remove(\"Content-Length\");\n    mockReadSuccess(0, 16);\n\n    // Second request.\n    dataSourceUnderTest.open(testDataSpec);\n    returnedBuffer = new byte[16];\n    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);\n    assertThat(bytesRead).isEqualTo(10);\n    bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);\n    assertThat(bytesRead).isEqualTo(6);\n    bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);\n    assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT);\n  }\n\n  @Test\n  public void testReadWithOffset() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n    mockReadSuccess(0, 16);\n\n    dataSourceUnderTest.open(testDataSpec);\n\n    byte[] returnedBuffer = new byte[16];\n    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);\n    assertThat(bytesRead).isEqualTo(8);\n    assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));\n    verify(mockTransferListener)\n        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);\n  }\n\n  @Test\n  public void testRangeRequestWith206Response() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n    mockReadSuccess(1000, 5000);\n    testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.\n    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);\n\n    dataSourceUnderTest.open(testDataSpec);\n\n    byte[] returnedBuffer = new byte[16];\n    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);\n    assertThat(bytesRead).isEqualTo(16);\n    assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));\n    verify(mockTransferListener)\n        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);\n  }\n\n  @Test\n  public void testRangeRequestWith200Response() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n    mockReadSuccess(0, 7000);\n    testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.\n    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);\n\n    dataSourceUnderTest.open(testDataSpec);\n\n    byte[] returnedBuffer = new byte[16];\n    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);\n    assertThat(bytesRead).isEqualTo(16);\n    assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));\n    verify(mockTransferListener)\n        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);\n  }\n\n  @Test\n  public void testReadWithUnsetLength() throws HttpDataSourceException {\n    testResponseHeader.remove(\"Content-Length\");\n    mockResponseStartSuccess();\n    mockReadSuccess(0, 16);\n\n    dataSourceUnderTest.open(testDataSpec);\n\n    byte[] returnedBuffer = new byte[16];\n    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);\n    assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));\n    assertThat(bytesRead).isEqualTo(8);\n    verify(mockTransferListener)\n        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);\n  }\n\n  @Test\n  public void testReadReturnsWhatItCan() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n    mockReadSuccess(0, 16);\n\n    dataSourceUnderTest.open(testDataSpec);\n\n    byte[] returnedBuffer = new byte[24];\n    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 24);\n    assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(0, 16), 24));\n    assertThat(bytesRead).isEqualTo(16);\n    verify(mockTransferListener)\n        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);\n  }\n\n  @Test\n  public void testClosedMeansClosed() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n    mockReadSuccess(0, 16);\n\n    int bytesRead = 0;\n    dataSourceUnderTest.open(testDataSpec);\n\n    byte[] returnedBuffer = new byte[8];\n    bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);\n    assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));\n    assertThat(bytesRead).isEqualTo(8);\n\n    dataSourceUnderTest.close();\n    verify(mockTransferListener)\n        .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);\n\n    try {\n      bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);\n      fail();\n    } catch (IllegalStateException e) {\n      // Expected.\n    }\n\n    // 16 bytes were attempted but only 8 should have been successfully read.\n    assertThat(bytesRead).isEqualTo(8);\n  }\n\n  @Test\n  public void testOverread() throws HttpDataSourceException {\n    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null);\n    testResponseHeader.put(\"Content-Length\", Long.toString(16L));\n    mockResponseStartSuccess();\n    mockReadSuccess(0, 16);\n\n    dataSourceUnderTest.open(testDataSpec);\n\n    byte[] returnedBuffer = new byte[8];\n    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);\n    assertThat(bytesRead).isEqualTo(8);\n    assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));\n\n    // The current buffer is kept if not completely consumed by DataSource reader.\n    returnedBuffer = new byte[8];\n    bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 6);\n    assertThat(bytesRead).isEqualTo(14);\n    assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(8, 6), 8));\n\n    // 2 bytes left at this point.\n    returnedBuffer = new byte[8];\n    bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);\n    assertThat(bytesRead).isEqualTo(16);\n    assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(14, 2), 8));\n\n    // Should have only called read on cronet once.\n    verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));\n    verify(mockTransferListener, times(1))\n        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);\n    verify(mockTransferListener, times(1))\n        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);\n    verify(mockTransferListener, times(1))\n        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2);\n\n    // Now we already returned the 16 bytes initially asked.\n    // Try to read again even though all requested 16 bytes are already returned.\n    // Return C.RESULT_END_OF_INPUT\n    returnedBuffer = new byte[16];\n    int bytesOverRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);\n    assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);\n    assertThat(returnedBuffer).isEqualTo(new byte[16]);\n    // C.RESULT_END_OF_INPUT should not be reported though the TransferListener.\n    verify(mockTransferListener, never())\n        .onBytesTransferred(\n            dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT);\n    // There should still be only one call to read on cronet.\n    verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));\n    // Check for connection not automatically closed.\n    verify(mockUrlRequest, never()).cancel();\n    assertThat(bytesRead).isEqualTo(16);\n  }\n\n  @Test\n  public void testConnectTimeout() throws InterruptedException {\n    long startTimeMs = SystemClock.elapsedRealtime();\n    final ConditionVariable startCondition = buildUrlRequestStartedCondition();\n    final CountDownLatch timedOutLatch = new CountDownLatch(1);\n\n    new Thread() {\n      @Override\n      public void run() {\n        try {\n          dataSourceUnderTest.open(testDataSpec);\n          fail();\n        } catch (HttpDataSourceException e) {\n          // Expected.\n          assertThat(e instanceof CronetDataSource.OpenException).isTrue();\n          assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();\n          assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)\n              .isEqualTo(TEST_CONNECTION_STATUS);\n          timedOutLatch.countDown();\n        }\n      }\n    }.start();\n    startCondition.block();\n\n    // We should still be trying to open.\n    assertNotCountedDown(timedOutLatch);\n    // We should still be trying to open as we approach the timeout.\n    SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);\n    assertNotCountedDown(timedOutLatch);\n    // Now we timeout.\n    SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10);\n    timedOutLatch.await();\n\n    verify(mockTransferListener, never())\n        .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);\n  }\n\n  @Test\n  public void testConnectInterrupted() throws InterruptedException {\n    long startTimeMs = SystemClock.elapsedRealtime();\n    final ConditionVariable startCondition = buildUrlRequestStartedCondition();\n    final CountDownLatch timedOutLatch = new CountDownLatch(1);\n\n    Thread thread =\n        new Thread() {\n          @Override\n          public void run() {\n            try {\n              dataSourceUnderTest.open(testDataSpec);\n              fail();\n            } catch (HttpDataSourceException e) {\n              // Expected.\n              assertThat(e instanceof CronetDataSource.OpenException).isTrue();\n              assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();\n              assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)\n                  .isEqualTo(TEST_INVALID_CONNECTION_STATUS);\n              timedOutLatch.countDown();\n            }\n          }\n        };\n    thread.start();\n    startCondition.block();\n\n    // We should still be trying to open.\n    assertNotCountedDown(timedOutLatch);\n    // We should still be trying to open as we approach the timeout.\n    SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);\n    assertNotCountedDown(timedOutLatch);\n    // Now we interrupt.\n    thread.interrupt();\n    timedOutLatch.await();\n\n    verify(mockTransferListener, never())\n        .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);\n  }\n\n  @Test\n  public void testConnectResponseBeforeTimeout() throws Exception {\n    long startTimeMs = SystemClock.elapsedRealtime();\n    final ConditionVariable startCondition = buildUrlRequestStartedCondition();\n    final CountDownLatch openLatch = new CountDownLatch(1);\n\n    AtomicReference<Exception> exceptionOnTestThread = new AtomicReference<>();\n    new Thread() {\n      @Override\n      public void run() {\n        try {\n          dataSourceUnderTest.open(testDataSpec);\n        } catch (HttpDataSourceException e) {\n          exceptionOnTestThread.set(e);\n        } finally {\n          openLatch.countDown();\n        }\n      }\n    }.start();\n    startCondition.block();\n\n    // We should still be trying to open.\n    assertNotCountedDown(openLatch);\n    // We should still be trying to open as we approach the timeout.\n    SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);\n    assertNotCountedDown(openLatch);\n    // The response arrives just in time.\n    dataSourceUnderTest.urlRequestCallback.onResponseStarted(mockUrlRequest, testUrlResponseInfo);\n    openLatch.await();\n    assertThat(exceptionOnTestThread.get()).isNull();\n  }\n\n  @Test\n  public void testRedirectIncreasesConnectionTimeout() throws Exception {\n    long startTimeMs = SystemClock.elapsedRealtime();\n    final ConditionVariable startCondition = buildUrlRequestStartedCondition();\n    final CountDownLatch timedOutLatch = new CountDownLatch(1);\n    final AtomicInteger openExceptions = new AtomicInteger(0);\n\n    new Thread() {\n      @Override\n      public void run() {\n        try {\n          dataSourceUnderTest.open(testDataSpec);\n          fail();\n        } catch (HttpDataSourceException e) {\n          // Expected.\n          assertThat(e instanceof CronetDataSource.OpenException).isTrue();\n          assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();\n          openExceptions.getAndIncrement();\n          timedOutLatch.countDown();\n        }\n      }\n    }.start();\n    startCondition.block();\n\n    // We should still be trying to open.\n    assertNotCountedDown(timedOutLatch);\n    // We should still be trying to open as we approach the timeout.\n    SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);\n    assertNotCountedDown(timedOutLatch);\n    // A redirect arrives just in time.\n    dataSourceUnderTest.urlRequestCallback.onRedirectReceived(\n        mockUrlRequest, testUrlResponseInfo, \"RandomRedirectedUrl1\");\n\n    long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;\n    SystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1);\n    // We should still be trying to open as we approach the new timeout.\n    assertNotCountedDown(timedOutLatch);\n    // A redirect arrives just in time.\n    dataSourceUnderTest.urlRequestCallback.onRedirectReceived(\n        mockUrlRequest, testUrlResponseInfo, \"RandomRedirectedUrl2\");\n\n    newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;\n    SystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1);\n    // We should still be trying to open as we approach the new timeout.\n    assertNotCountedDown(timedOutLatch);\n    // Now we timeout.\n    SystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10);\n    timedOutLatch.await();\n\n    verify(mockTransferListener, never())\n        .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);\n    assertThat(openExceptions.get()).isEqualTo(1);\n  }\n\n  @Test\n  public void testRedirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_followsRedirect()\n      throws HttpDataSourceException {\n    mockSingleRedirectSuccess();\n    mockFollowRedirectSuccess();\n\n    testResponseHeader.put(\"Set-Cookie\", \"testcookie=testcookie; Path=/video\");\n\n    dataSourceUnderTest.open(testDataSpec);\n    verify(mockUrlRequestBuilder, never()).addHeader(eq(\"Cookie\"), any(String.class));\n    verify(mockUrlRequest).followRedirect();\n  }\n\n  @Test\n  public void\n      testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()\n          throws HttpDataSourceException {\n    dataSourceUnderTest =\n        new CronetDataSource(\n            mockCronetEngine,\n            mockExecutor,\n            mockContentTypePredicate,\n            TEST_CONNECT_TIMEOUT_MS,\n            TEST_READ_TIMEOUT_MS,\n            true, // resetTimeoutOnRedirects\n            Clock.DEFAULT,\n            null,\n            true);\n    dataSourceUnderTest.addTransferListener(mockTransferListener);\n    dataSourceUnderTest.setRequestProperty(\"Content-Type\", TEST_CONTENT_TYPE);\n\n    mockSingleRedirectSuccess();\n\n    testResponseHeader.put(\"Set-Cookie\", \"testcookie=testcookie; Path=/video\");\n\n    dataSourceUnderTest.open(testDataSpec);\n    verify(mockUrlRequestBuilder).addHeader(eq(\"Cookie\"), any(String.class));\n    verify(mockUrlRequestBuilder, never()).addHeader(eq(\"Range\"), any(String.class));\n    verify(mockUrlRequestBuilder, times(2)).addHeader(\"Content-Type\", TEST_CONTENT_TYPE);\n    verify(mockUrlRequest, never()).followRedirect();\n    verify(mockUrlRequest, times(2)).start();\n  }\n\n  @Test\n  public void\n      testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader()\n          throws HttpDataSourceException {\n    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);\n    dataSourceUnderTest =\n        new CronetDataSource(\n            mockCronetEngine,\n            mockExecutor,\n            mockContentTypePredicate,\n            TEST_CONNECT_TIMEOUT_MS,\n            TEST_READ_TIMEOUT_MS,\n            true, // resetTimeoutOnRedirects\n            Clock.DEFAULT,\n            null,\n            true);\n    dataSourceUnderTest.addTransferListener(mockTransferListener);\n    dataSourceUnderTest.setRequestProperty(\"Content-Type\", TEST_CONTENT_TYPE);\n\n    mockSingleRedirectSuccess();\n\n    testResponseHeader.put(\"Set-Cookie\", \"testcookie=testcookie; Path=/video\");\n\n    dataSourceUnderTest.open(testDataSpec);\n    verify(mockUrlRequestBuilder).addHeader(eq(\"Cookie\"), any(String.class));\n    verify(mockUrlRequestBuilder, times(2)).addHeader(\"Range\", \"bytes=1000-5999\");\n    verify(mockUrlRequestBuilder, times(2)).addHeader(\"Content-Type\", TEST_CONTENT_TYPE);\n    verify(mockUrlRequest, never()).followRedirect();\n    verify(mockUrlRequest, times(2)).start();\n  }\n\n  @Test\n  public void testRedirectNoSetCookieFollowsRedirect() throws HttpDataSourceException {\n    mockSingleRedirectSuccess();\n    mockFollowRedirectSuccess();\n\n    dataSourceUnderTest.open(testDataSpec);\n    verify(mockUrlRequestBuilder, never()).addHeader(eq(\"Cookie\"), any(String.class));\n    verify(mockUrlRequest).followRedirect();\n  }\n\n  @Test\n  public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()\n      throws HttpDataSourceException {\n    dataSourceUnderTest =\n        new CronetDataSource(\n            mockCronetEngine,\n            mockExecutor,\n            mockContentTypePredicate,\n            TEST_CONNECT_TIMEOUT_MS,\n            TEST_READ_TIMEOUT_MS,\n            true, // resetTimeoutOnRedirects\n            Clock.DEFAULT,\n            null,\n            true);\n    dataSourceUnderTest.addTransferListener(mockTransferListener);\n    mockSingleRedirectSuccess();\n    mockFollowRedirectSuccess();\n\n    dataSourceUnderTest.open(testDataSpec);\n    verify(mockUrlRequestBuilder, never()).addHeader(eq(\"Cookie\"), any(String.class));\n    verify(mockUrlRequest).followRedirect();\n  }\n\n  @Test\n  public void testExceptionFromTransferListener() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n\n    // Make mockTransferListener throw an exception in CronetDataSource.close(). Ensure that\n    // the subsequent open() call succeeds.\n    doThrow(new NullPointerException())\n        .when(mockTransferListener)\n        .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);\n    dataSourceUnderTest.open(testDataSpec);\n    try {\n      dataSourceUnderTest.close();\n      fail(\"NullPointerException expected\");\n    } catch (NullPointerException e) {\n      // Expected.\n    }\n    // Open should return successfully.\n    dataSourceUnderTest.open(testDataSpec);\n  }\n\n  @Test\n  public void testReadFailure() throws HttpDataSourceException {\n    mockResponseStartSuccess();\n    mockReadFailure();\n\n    dataSourceUnderTest.open(testDataSpec);\n    byte[] returnedBuffer = new byte[8];\n    try {\n      dataSourceUnderTest.read(returnedBuffer, 0, 8);\n      fail(\"dataSourceUnderTest.read() returned, but IOException expected\");\n    } catch (IOException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testReadInterrupted() throws HttpDataSourceException, InterruptedException {\n    mockResponseStartSuccess();\n    dataSourceUnderTest.open(testDataSpec);\n\n    final ConditionVariable startCondition = buildReadStartedCondition();\n    final CountDownLatch timedOutLatch = new CountDownLatch(1);\n    byte[] returnedBuffer = new byte[8];\n    Thread thread =\n        new Thread() {\n          @Override\n          public void run() {\n            try {\n              dataSourceUnderTest.read(returnedBuffer, 0, 8);\n              fail();\n            } catch (HttpDataSourceException e) {\n              // Expected.\n              assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();\n              timedOutLatch.countDown();\n            }\n          }\n        };\n    thread.start();\n    startCondition.block();\n\n    assertNotCountedDown(timedOutLatch);\n    // Now we interrupt.\n    thread.interrupt();\n    timedOutLatch.await();\n  }\n\n  @Test\n  public void testAllowDirectExecutor() throws HttpDataSourceException {\n    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);\n    mockResponseStartSuccess();\n\n    dataSourceUnderTest.open(testDataSpec);\n    verify(mockUrlRequestBuilder).allowDirectExecutor();\n  }\n\n  // Helper methods.\n\n  private void mockStatusResponse() {\n    doAnswer(\n            invocation -> {\n              UrlRequest.StatusListener statusListener =\n                  (UrlRequest.StatusListener) invocation.getArguments()[0];\n              statusListener.onStatus(TEST_CONNECTION_STATUS);\n              return null;\n            })\n        .when(mockUrlRequest)\n        .getStatus(any(UrlRequest.StatusListener.class));\n  }\n\n  private void mockResponseStartSuccess() {\n    doAnswer(\n            invocation -> {\n              dataSourceUnderTest.urlRequestCallback.onResponseStarted(\n                  mockUrlRequest, testUrlResponseInfo);\n              return null;\n            })\n        .when(mockUrlRequest)\n        .start();\n  }\n\n  private void mockResponseStartRedirect() {\n    doAnswer(\n            invocation -> {\n              dataSourceUnderTest.urlRequestCallback.onRedirectReceived(\n                  mockUrlRequest,\n                  createUrlResponseInfo(307), // statusCode\n                  \"http://redirect.location.com\");\n              return null;\n            })\n        .when(mockUrlRequest)\n        .start();\n  }\n\n  private void mockSingleRedirectSuccess() {\n    doAnswer(\n            invocation -> {\n              if (!redirectCalled) {\n                redirectCalled = true;\n                dataSourceUnderTest.urlRequestCallback.onRedirectReceived(\n                    mockUrlRequest,\n                    createUrlResponseInfoWithUrl(\"http://example.com/video\", 300),\n                    \"http://example.com/video/redirect\");\n              } else {\n                dataSourceUnderTest.urlRequestCallback.onResponseStarted(\n                    mockUrlRequest, testUrlResponseInfo);\n              }\n              return null;\n            })\n        .when(mockUrlRequest)\n        .start();\n  }\n\n  private void mockFollowRedirectSuccess() {\n    doAnswer(\n            invocation -> {\n              dataSourceUnderTest.urlRequestCallback.onResponseStarted(\n                  mockUrlRequest, testUrlResponseInfo);\n              return null;\n            })\n        .when(mockUrlRequest)\n        .followRedirect();\n  }\n\n  private void mockResponseStartFailure() {\n    doAnswer(\n            invocation -> {\n              dataSourceUnderTest.urlRequestCallback.onFailed(\n                  mockUrlRequest,\n                  createUrlResponseInfo(500), // statusCode\n                  mockNetworkException);\n              return null;\n            })\n        .when(mockUrlRequest)\n        .start();\n  }\n\n  private void mockReadSuccess(int position, int length) {\n    final int[] positionAndRemaining = new int[] {position, length};\n    doAnswer(\n            invocation -> {\n              if (positionAndRemaining[1] == 0) {\n                dataSourceUnderTest.urlRequestCallback.onSucceeded(\n                    mockUrlRequest, testUrlResponseInfo);\n              } else {\n                ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];\n                int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());\n                inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));\n                positionAndRemaining[0] += readLength;\n                positionAndRemaining[1] -= readLength;\n                dataSourceUnderTest.urlRequestCallback.onReadCompleted(\n                    mockUrlRequest, testUrlResponseInfo, inputBuffer);\n              }\n              return null;\n            })\n        .when(mockUrlRequest)\n        .read(any(ByteBuffer.class));\n  }\n\n  private void mockReadFailure() {\n    doAnswer(\n            invocation -> {\n              dataSourceUnderTest.urlRequestCallback.onFailed(\n                  mockUrlRequest,\n                  createUrlResponseInfo(500), // statusCode\n                  mockNetworkException);\n              return null;\n            })\n        .when(mockUrlRequest)\n        .read(any(ByteBuffer.class));\n  }\n\n  private ConditionVariable buildReadStartedCondition() {\n    final ConditionVariable startedCondition = new ConditionVariable();\n    doAnswer(\n            invocation -> {\n              startedCondition.open();\n              return null;\n            })\n        .when(mockUrlRequest)\n        .read(any(ByteBuffer.class));\n    return startedCondition;\n  }\n\n  private ConditionVariable buildUrlRequestStartedCondition() {\n    final ConditionVariable startedCondition = new ConditionVariable();\n    doAnswer(\n            invocation -> {\n              startedCondition.open();\n              return null;\n            })\n        .when(mockUrlRequest)\n        .start();\n    return startedCondition;\n  }\n\n  private void assertNotCountedDown(CountDownLatch countDownLatch) throws InterruptedException {\n    // We are asserting that another thread does not count down the latch. We therefore sleep some\n    // time to give the other thread the chance to fail this test.\n    Thread.sleep(50);\n    assertThat(countDownLatch.getCount()).isGreaterThan(0L);\n  }\n\n  private static byte[] buildTestDataArray(int position, int length) {\n    return buildTestDataBuffer(position, length).array();\n  }\n\n  public static byte[] prefixZeros(byte[] data, int requiredLength) {\n    byte[] prefixedData = new byte[requiredLength];\n    System.arraycopy(data, 0, prefixedData, requiredLength - data.length, data.length);\n    return prefixedData;\n  }\n\n  public static byte[] suffixZeros(byte[] data, int requiredLength) {\n    return Arrays.copyOf(data, requiredLength);\n  }\n\n  private static ByteBuffer buildTestDataBuffer(int position, int length) {\n    ByteBuffer testBuffer = ByteBuffer.allocate(length);\n    for (int i = 0; i < length; i++) {\n      testBuffer.put((byte) (position + i));\n    }\n    testBuffer.flip();\n    return testBuffer;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ffmpeg/README.md",
    "content": "# ExoPlayer FFmpeg extension #\n\nThe FFmpeg extension provides `FfmpegAudioRenderer`, which uses FFmpeg for\ndecoding and can render audio encoded in a variety of formats.\n\n## License note ##\n\nPlease note that whilst the code in this repository is licensed under\n[Apache 2.0][], using this extension also requires building and including one or\nmore external libraries as described below. These are licensed separately.\n\n[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE\n\n## Build instructions ##\n\nTo use this extension you need to clone the ExoPlayer repository and depend on\nits modules locally. Instructions for doing this can be found in ExoPlayer's\n[top level README][]. The extension is not provided via JCenter (see [#2781][]\nfor more information).\n\nIn addition, it's necessary to build the extension's native components as\nfollows:\n\n* Set the following environment variables:\n\n```\ncd \"<path to exoplayer checkout>\"\nFFMPEG_EXT_PATH=\"$(pwd)/extensions/ffmpeg/src/main/jni\"\n```\n\n* Download the [Android NDK][] and set its location in an environment variable.\n  Only versions up to NDK 15c are supported currently.\n\n```\nNDK_PATH=\"<path to Android NDK>\"\n```\n\n* Set up host platform (\"darwin-x86_64\" for Mac OS X):\n\n```\nHOST_PLATFORM=\"linux-x86_64\"\n```\n\n* Fetch and build FFmpeg. The configuration flags determine which formats will\n  be supported. See the [Supported formats][] page for more details of the\n  available flags.\n\nFor example, to fetch and build FFmpeg release 4.0 for armeabi-v7a,\n  arm64-v8a and x86 on Linux x86_64:\n\n```\nCOMMON_OPTIONS=\"\\\n    --target-os=android \\\n    --disable-static \\\n    --enable-shared \\\n    --disable-doc \\\n    --disable-programs \\\n    --disable-everything \\\n    --disable-avdevice \\\n    --disable-avformat \\\n    --disable-swscale \\\n    --disable-postproc \\\n    --disable-avfilter \\\n    --disable-symver \\\n    --disable-swresample \\\n    --enable-avresample \\\n    --enable-decoder=vorbis \\\n    --enable-decoder=opus \\\n    --enable-decoder=flac \\\n    \" && \\\ncd \"${FFMPEG_EXT_PATH}\" && \\\n(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \\\ncd ffmpeg && git checkout release/4.0 && \\\n./configure \\\n    --libdir=android-libs/armeabi-v7a \\\n    --arch=arm \\\n    --cpu=armv7-a \\\n    --cross-prefix=\"${NDK_PATH}/toolchains/arm-linux-androideabi-4.9/prebuilt/${HOST_PLATFORM}/bin/arm-linux-androideabi-\" \\\n    --sysroot=\"${NDK_PATH}/platforms/android-9/arch-arm/\" \\\n    --extra-cflags=\"-march=armv7-a -mfloat-abi=softfp\" \\\n    --extra-ldflags=\"-Wl,--fix-cortex-a8\" \\\n    --extra-ldexeflags=-pie \\\n    ${COMMON_OPTIONS} \\\n    && \\\nmake -j4 && make install-libs && \\\nmake clean && ./configure \\\n    --libdir=android-libs/arm64-v8a \\\n    --arch=aarch64 \\\n    --cpu=armv8-a \\\n    --cross-prefix=\"${NDK_PATH}/toolchains/aarch64-linux-android-4.9/prebuilt/${HOST_PLATFORM}/bin/aarch64-linux-android-\" \\\n    --sysroot=\"${NDK_PATH}/platforms/android-21/arch-arm64/\" \\\n    --extra-ldexeflags=-pie \\\n    ${COMMON_OPTIONS} \\\n    && \\\nmake -j4 && make install-libs && \\\nmake clean && ./configure \\\n    --libdir=android-libs/x86 \\\n    --arch=x86 \\\n    --cpu=i686 \\\n    --cross-prefix=\"${NDK_PATH}/toolchains/x86-4.9/prebuilt/${HOST_PLATFORM}/bin/i686-linux-android-\" \\\n    --sysroot=\"${NDK_PATH}/platforms/android-9/arch-x86/\" \\\n    --extra-ldexeflags=-pie \\\n    --disable-asm \\\n    ${COMMON_OPTIONS} \\\n    && \\\nmake -j4 && make install-libs && \\\nmake clean\n```\n\n* Build the JNI native libraries, setting `APP_ABI` to include the architectures\n  built in the previous step. For example:\n\n```\ncd \"${FFMPEG_EXT_PATH}\" && \\\n${NDK_PATH}/ndk-build APP_ABI=\"armeabi-v7a arm64-v8a x86\" -j4\n```\n\n## Using the extension ##\n\nOnce you've followed the instructions above to check out, build and depend on\nthe extension, the next step is to tell ExoPlayer to use `FfmpegAudioRenderer`.\nHow you do this depends on which player API you're using:\n\n* If you're passing a `DefaultRenderersFactory` to\n  `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by\n  setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory`\n  constructor to `EXTENSION_RENDERER_MODE_ON`. This will use\n  `FfmpegAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't\n  support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give\n  `FfmpegAudioRenderer` priority over `MediaCodecAudioRenderer`.\n* If you've subclassed `DefaultRenderersFactory`, add an `FfmpegAudioRenderer`\n  to the output list in `buildAudioRenderers`. ExoPlayer will use the first\n  `Renderer` in the list that supports the input media format.\n* If you've implemented your own `RenderersFactory`, return an\n  `FfmpegAudioRenderer` instance from `createRenderers`. ExoPlayer will use the\n  first `Renderer` in the returned array that supports the input media format.\n* If you're using `ExoPlayerFactory.newInstance`, pass an `FfmpegAudioRenderer`\n  in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the\n  list that supports the input media format.\n\nNote: These instructions assume you're using `DefaultTrackSelector`. If you have\na custom track selector the choice of `Renderer` is up to your implementation,\nso you need to make sure you are passing an `FfmpegAudioRenderer` to the player,\nthen implement your own logic to use the renderer for a given track.\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html\n[#2781]: https://github.com/google/ExoPlayer/issues/2781\n[Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension\n\n## Links ##\n\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`\n  belong to this module.\n\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ffmpeg/build.gradle",
    "content": "// Copyright (C) 2016 The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n        consumerProguardFiles 'proguard-rules.txt'\n    }\n\n    sourceSets.main {\n        jniLibs.srcDir 'src/main/libs'\n        jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    implementation 'androidx.annotation:annotation:1.1.0'\n    compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n}\n\next {\n    javadocTitle = 'FFmpeg extension'\n}\napply from: '../../javadoc_library.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ffmpeg/proguard-rules.txt",
    "content": "# Proguard rules specific to the FFmpeg extension.\n\n# This prevents the names of native methods from being obfuscated.\n-keepclasseswithmembernames class * {\n    native <methods>;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ffmpeg/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest package=\"com.google.android.exoplayer2.ext.ffmpeg\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.ffmpeg;\n\nimport android.os.Handler;\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.audio.AudioProcessor;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener;\nimport com.google.android.exoplayer2.audio.AudioSink;\nimport com.google.android.exoplayer2.audio.DefaultAudioSink;\nimport com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;\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.MimeTypes;\nimport java.util.Collections;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/**\n * Decodes and renders audio using FFmpeg.\n */\npublic final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {\n\n  /** The number of input and output buffers. */\n  private static final int NUM_BUFFERS = 16;\n  /** The default input buffer size. */\n  private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;\n\n  private final boolean enableFloatOutput;\n\n  private @MonotonicNonNull FfmpegDecoder decoder;\n\n  public FfmpegAudioRenderer() {\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 FfmpegAudioRenderer(\n      @Nullable Handler eventHandler,\n      @Nullable AudioRendererEventListener eventListener,\n      AudioProcessor... audioProcessors) {\n    this(\n        eventHandler,\n        eventListener,\n        new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors),\n        /* enableFloatOutput= */ 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 audioSink The sink to which audio will be output.\n   * @param enableFloatOutput Whether to enable 32-bit float audio format, if supported on the\n   *     device/build and if the input format may have bit depth higher than 16-bit. When using\n   *     32-bit float output, any audio processing will be disabled, including playback speed/pitch\n   *     adjustment.\n   */\n  public FfmpegAudioRenderer(\n      @Nullable Handler eventHandler,\n      @Nullable AudioRendererEventListener eventListener,\n      AudioSink audioSink,\n      boolean enableFloatOutput) {\n    super(\n        eventHandler,\n        eventListener,\n        /* drmSessionManager= */ null,\n        /* playClearSamplesWithoutKeys= */ false,\n        audioSink);\n    this.enableFloatOutput = enableFloatOutput;\n  }\n\n  @Override\n  protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,\n      Format format) {\n    Assertions.checkNotNull(format.sampleMimeType);\n    if (!FfmpegLibrary.isAvailable()) {\n      return FORMAT_UNSUPPORTED_TYPE;\n    } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding)\n        || !isOutputSupported(format)) {\n      return FORMAT_UNSUPPORTED_SUBTYPE;\n    } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {\n      return FORMAT_UNSUPPORTED_DRM;\n    } else {\n      return FORMAT_HANDLED;\n    }\n  }\n\n  @Override\n  public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {\n    return ADAPTIVE_NOT_SEAMLESS;\n  }\n\n  @Override\n  protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)\n      throws FfmpegDecoderException {\n    int initialInputBufferSize =\n        format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;\n    decoder =\n        new FfmpegDecoder(\n            NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format, shouldUseFloatOutput(format));\n    return decoder;\n  }\n\n  @Override\n  public Format getOutputFormat() {\n    Assertions.checkNotNull(decoder);\n    int channelCount = decoder.getChannelCount();\n    int sampleRate = decoder.getSampleRate();\n    @C.PcmEncoding int encoding = decoder.getEncoding();\n    return Format.createAudioSampleFormat(\n        /* id= */ null,\n        MimeTypes.AUDIO_RAW,\n        /* codecs= */ null,\n        Format.NO_VALUE,\n        Format.NO_VALUE,\n        channelCount,\n        sampleRate,\n        encoding,\n        Collections.emptyList(),\n        /* drmInitData= */ null,\n        /* selectionFlags= */ 0,\n        /* language= */ null);\n  }\n\n  private boolean isOutputSupported(Format inputFormat) {\n    return shouldUseFloatOutput(inputFormat)\n        || supportsOutput(inputFormat.channelCount, C.ENCODING_PCM_16BIT);\n  }\n\n  private boolean shouldUseFloatOutput(Format inputFormat) {\n    Assertions.checkNotNull(inputFormat.sampleMimeType);\n    if (!enableFloatOutput || !supportsOutput(inputFormat.channelCount, C.ENCODING_PCM_FLOAT)) {\n      return false;\n    }\n    switch (inputFormat.sampleMimeType) {\n      case MimeTypes.AUDIO_RAW:\n        // For raw audio, output in 32-bit float encoding if the bit depth is > 16-bit.\n        return inputFormat.pcmEncoding == C.ENCODING_PCM_24BIT\n            || inputFormat.pcmEncoding == C.ENCODING_PCM_32BIT\n            || inputFormat.pcmEncoding == C.ENCODING_PCM_FLOAT;\n      case MimeTypes.AUDIO_AC3:\n        // AC-3 is always 16-bit, so there is no point outputting in 32-bit float encoding.\n        return false;\n      default:\n        // For all other formats, assume that it's worth using 32-bit float encoding.\n        return true;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.ffmpeg;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\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.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.nio.ByteBuffer;\nimport java.util.List;\n\n/**\n * FFmpeg audio decoder.\n */\n/* package */ final class FfmpegDecoder extends\n    SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {\n\n  // Output buffer sizes when decoding PCM mu-law streams, which is the maximum FFmpeg outputs.\n  private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;\n  private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;\n\n  // Error codes matching ffmpeg_jni.cc.\n  private static final int DECODER_ERROR_INVALID_DATA = -1;\n  private static final int DECODER_ERROR_OTHER = -2;\n\n  private final String codecName;\n  private final @Nullable byte[] extraData;\n  private final @C.Encoding int encoding;\n  private final int outputBufferSize;\n\n  private long nativeContext; // May be reassigned on resetting the codec.\n  private boolean hasOutputFormat;\n  private volatile int channelCount;\n  private volatile int sampleRate;\n\n  public FfmpegDecoder(\n      int numInputBuffers,\n      int numOutputBuffers,\n      int initialInputBufferSize,\n      Format format,\n      boolean outputFloat)\n      throws FfmpegDecoderException {\n    super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);\n    if (!FfmpegLibrary.isAvailable()) {\n      throw new FfmpegDecoderException(\"Failed to load decoder native libraries.\");\n    }\n    Assertions.checkNotNull(format.sampleMimeType);\n    codecName =\n        Assertions.checkNotNull(\n            FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding));\n    extraData = getExtraData(format.sampleMimeType, format.initializationData);\n    encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;\n    outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;\n    nativeContext =\n        ffmpegInitialize(codecName, extraData, outputFloat, format.sampleRate, format.channelCount);\n    if (nativeContext == 0) {\n      throw new FfmpegDecoderException(\"Initialization failed.\");\n    }\n    setInitialInputBufferSize(initialInputBufferSize);\n  }\n\n  @Override\n  public String getName() {\n    return \"ffmpeg\" + FfmpegLibrary.getVersion() + \"-\" + codecName;\n  }\n\n  @Override\n  protected DecoderInputBuffer createInputBuffer() {\n    return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);\n  }\n\n  @Override\n  protected SimpleOutputBuffer createOutputBuffer() {\n    return new SimpleOutputBuffer(this);\n  }\n\n  @Override\n  protected FfmpegDecoderException createUnexpectedDecodeException(Throwable error) {\n    return new FfmpegDecoderException(\"Unexpected decode error\", error);\n  }\n\n  @Override\n  protected @Nullable FfmpegDecoderException decode(\n      DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {\n    if (reset) {\n      nativeContext = ffmpegReset(nativeContext, extraData);\n      if (nativeContext == 0) {\n        return new FfmpegDecoderException(\"Error resetting (see logcat).\");\n      }\n    }\n    ByteBuffer inputData = inputBuffer.data;\n    int inputSize = inputData.limit();\n    ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);\n    int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);\n    if (result == DECODER_ERROR_INVALID_DATA) {\n      // Treat invalid data errors as non-fatal to match the behavior of MediaCodec. No output will\n      // be produced for this buffer, so mark it as decode-only to ensure that the audio sink's\n      // position is reset when more audio is produced.\n      outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);\n      return null;\n    } else if (result == DECODER_ERROR_OTHER) {\n      return new FfmpegDecoderException(\"Error decoding (see logcat).\");\n    }\n    if (!hasOutputFormat) {\n      channelCount = ffmpegGetChannelCount(nativeContext);\n      sampleRate = ffmpegGetSampleRate(nativeContext);\n      if (sampleRate == 0 && \"alac\".equals(codecName)) {\n        Assertions.checkNotNull(extraData);\n        // ALAC decoder did not set the sample rate in earlier versions of FFMPEG.\n        // See https://trac.ffmpeg.org/ticket/6096\n        ParsableByteArray parsableExtraData = new ParsableByteArray(extraData);\n        parsableExtraData.setPosition(extraData.length - 4);\n        sampleRate = parsableExtraData.readUnsignedIntToInt();\n      }\n      hasOutputFormat = true;\n    }\n    outputBuffer.data.position(0);\n    outputBuffer.data.limit(result);\n    return null;\n  }\n\n  @Override\n  public void release() {\n    super.release();\n    ffmpegRelease(nativeContext);\n    nativeContext = 0;\n  }\n\n  /**\n   * Returns the channel count of output audio. May only be called after {@link #decode}.\n   */\n  public int getChannelCount() {\n    return channelCount;\n  }\n\n  /**\n   * Returns the sample rate of output audio. May only be called after {@link #decode}.\n   */\n  public int getSampleRate() {\n    return sampleRate;\n  }\n\n  /**\n   * Returns the encoding of output audio.\n   */\n  public @C.Encoding int getEncoding() {\n    return encoding;\n  }\n\n  /**\n   * Returns FFmpeg-compatible codec-specific initialization data (\"extra data\"), or {@code null} if\n   * not required.\n   */\n  private static @Nullable byte[] getExtraData(String mimeType, List<byte[]> initializationData) {\n    switch (mimeType) {\n      case MimeTypes.AUDIO_AAC:\n      case MimeTypes.AUDIO_OPUS:\n        return initializationData.get(0);\n      case MimeTypes.AUDIO_ALAC:\n        return getAlacExtraData(initializationData);\n      case MimeTypes.AUDIO_VORBIS:\n        return getVorbisExtraData(initializationData);\n      default:\n        // Other codecs do not require extra data.\n        return null;\n    }\n  }\n\n  private static byte[] getAlacExtraData(List<byte[]> initializationData) {\n    // FFmpeg's ALAC decoder expects an ALAC atom, which contains the ALAC \"magic cookie\", as extra\n    // data. initializationData[0] contains only the magic cookie, and so we need to package it into\n    // an ALAC atom. See:\n    // https://ffmpeg.org/doxygen/0.6/alac_8c.html\n    // https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt\n    byte[] magicCookie = initializationData.get(0);\n    int alacAtomLength = 12 + magicCookie.length;\n    ByteBuffer alacAtom = ByteBuffer.allocate(alacAtomLength);\n    alacAtom.putInt(alacAtomLength);\n    alacAtom.putInt(0x616c6163); // type=alac\n    alacAtom.putInt(0); // version=0, flags=0\n    alacAtom.put(magicCookie, /* offset= */ 0, magicCookie.length);\n    return alacAtom.array();\n  }\n\n  private static byte[] getVorbisExtraData(List<byte[]> initializationData) {\n    byte[] header0 = initializationData.get(0);\n    byte[] header1 = initializationData.get(1);\n    byte[] extraData = new byte[header0.length + header1.length + 6];\n    extraData[0] = (byte) (header0.length >> 8);\n    extraData[1] = (byte) (header0.length & 0xFF);\n    System.arraycopy(header0, 0, extraData, 2, header0.length);\n    extraData[header0.length + 2] = 0;\n    extraData[header0.length + 3] = 0;\n    extraData[header0.length + 4] = (byte) (header1.length >> 8);\n    extraData[header0.length + 5] = (byte) (header1.length & 0xFF);\n    System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);\n    return extraData;\n  }\n\n  private native long ffmpegInitialize(\n      String codecName,\n      @Nullable byte[] extraData,\n      boolean outputFloat,\n      int rawSampleRate,\n      int rawChannelCount);\n\n  private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize,\n      ByteBuffer outputData, int outputSize);\n  private native int ffmpegGetChannelCount(long context);\n  private native int ffmpegGetSampleRate(long context);\n\n  private native long ffmpegReset(long context, @Nullable byte[] extraData);\n\n  private native void ffmpegRelease(long context);\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.ffmpeg;\n\nimport com.google.android.exoplayer2.audio.AudioDecoderException;\n\n/**\n * Thrown when an FFmpeg decoder error occurs.\n */\npublic final class FfmpegDecoderException extends AudioDecoderException {\n\n  /* package */ FfmpegDecoderException(String message) {\n    super(message);\n  }\n\n  /* package */ FfmpegDecoderException(String message, Throwable cause) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.ffmpeg;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.util.LibraryLoader;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\n\n/**\n * Configures and queries the underlying native library.\n */\npublic final class FfmpegLibrary {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.ffmpeg\");\n  }\n\n  private static final String TAG = \"FfmpegLibrary\";\n\n  private static final LibraryLoader LOADER =\n      new LibraryLoader(\"avutil\", \"avresample\", \"avcodec\", \"ffmpeg\");\n\n  private FfmpegLibrary() {}\n\n  /**\n   * Override the names of the FFmpeg native libraries. If an application wishes to call this\n   * method, it must do so before calling any other method defined by this class, and before\n   * instantiating a {@link FfmpegAudioRenderer} instance.\n   *\n   * @param libraries The names of the FFmpeg native libraries.\n   */\n  public static void setLibraries(String... libraries) {\n    LOADER.setLibraries(libraries);\n  }\n\n  /**\n   * Returns whether the underlying library is available, loading it if necessary.\n   */\n  public static boolean isAvailable() {\n    return LOADER.isAvailable();\n  }\n\n  /** Returns the version of the underlying library if available, or null otherwise. */\n  public static @Nullable String getVersion() {\n    return isAvailable() ? ffmpegGetVersion() : null;\n  }\n\n  /**\n   * Returns whether the underlying library supports the specified MIME type.\n   *\n   * @param mimeType The MIME type to check.\n   * @param encoding The PCM encoding for raw audio.\n   */\n  public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) {\n    if (!isAvailable()) {\n      return false;\n    }\n    String codecName = getCodecName(mimeType, encoding);\n    if (codecName == null) {\n      return false;\n    }\n    if (!ffmpegHasDecoder(codecName)) {\n      Log.w(TAG, \"No \" + codecName + \" decoder available. Check the FFmpeg build configuration.\");\n      return false;\n    }\n    return true;\n  }\n\n  /**\n   * Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null}\n   * if it's unsupported.\n   */\n  /* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) {\n    switch (mimeType) {\n      case MimeTypes.AUDIO_AAC:\n        return \"aac\";\n      case MimeTypes.AUDIO_MPEG:\n      case MimeTypes.AUDIO_MPEG_L1:\n      case MimeTypes.AUDIO_MPEG_L2:\n        return \"mp3\";\n      case MimeTypes.AUDIO_AC3:\n        return \"ac3\";\n      case MimeTypes.AUDIO_E_AC3:\n      case MimeTypes.AUDIO_E_AC3_JOC:\n        return \"eac3\";\n      case MimeTypes.AUDIO_TRUEHD:\n        return \"truehd\";\n      case MimeTypes.AUDIO_DTS:\n      case MimeTypes.AUDIO_DTS_HD:\n        return \"dca\";\n      case MimeTypes.AUDIO_VORBIS:\n        return \"vorbis\";\n      case MimeTypes.AUDIO_OPUS:\n        return \"opus\";\n      case MimeTypes.AUDIO_AMR_NB:\n        return \"amrnb\";\n      case MimeTypes.AUDIO_AMR_WB:\n        return \"amrwb\";\n      case MimeTypes.AUDIO_FLAC:\n        return \"flac\";\n      case MimeTypes.AUDIO_ALAC:\n        return \"alac\";\n      case MimeTypes.AUDIO_RAW:\n        if (encoding == C.ENCODING_PCM_MU_LAW) {\n          return \"pcm_mulaw\";\n        } else if (encoding == C.ENCODING_PCM_A_LAW) {\n          return \"pcm_alaw\";\n        } else {\n          return null;\n        }\n      default:\n        return null;\n    }\n  }\n\n  private static native String ffmpegGetVersion();\n  private static native boolean ffmpegHasDecoder(String codecName);\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ffmpeg/src/main/jni/Android.mk",
    "content": "#\n# Copyright (C) 2016 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS 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\nLOCAL_PATH := $(call my-dir)\n\ninclude $(CLEAR_VARS)\nLOCAL_MODULE    := libavcodec\nLOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so\ninclude $(PREBUILT_SHARED_LIBRARY)\n\ninclude $(CLEAR_VARS)\nLOCAL_MODULE    := libavutil\nLOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so\ninclude $(PREBUILT_SHARED_LIBRARY)\n\ninclude $(CLEAR_VARS)\nLOCAL_MODULE    := libavresample\nLOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so\ninclude $(PREBUILT_SHARED_LIBRARY)\n\ninclude $(CLEAR_VARS)\nLOCAL_MODULE := ffmpeg\nLOCAL_SRC_FILES := ffmpeg_jni.cc\nLOCAL_C_INCLUDES := ffmpeg\nLOCAL_SHARED_LIBRARIES := libavcodec libavresample libavutil\nLOCAL_LDLIBS := -Lffmpeg/android-libs/$(TARGET_ARCH_ABI) -llog\ninclude $(BUILD_SHARED_LIBRARY)\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ffmpeg/src/main/jni/Application.mk",
    "content": "#\n# Copyright (C) 2016 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS 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\nAPP_OPTIM := release\nAPP_STL := gnustl_static\nAPP_CPPFLAGS := -frtti\nAPP_PLATFORM := android-9\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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#include <jni.h>\n#include <stdlib.h>\n#include <android/log.h>\n\nextern \"C\" {\n#ifdef __cplusplus\n#define __STDC_CONSTANT_MACROS\n#ifdef _STDINT_H\n#undef _STDINT_H\n#endif\n#include <stdint.h>\n#endif\n#include <libavcodec/avcodec.h>\n#include <libavresample/avresample.h>\n#include <libavutil/channel_layout.h>\n#include <libavutil/error.h>\n#include <libavutil/opt.h>\n}\n\n#define LOG_TAG \"ffmpeg_jni\"\n#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, \\\n                   __VA_ARGS__))\n\n#define DECODER_FUNC(RETURN_TYPE, NAME, ...) \\\n  extern \"C\" { \\\n  JNIEXPORT RETURN_TYPE \\\n    Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegDecoder_ ## NAME \\\n      (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\\\n  } \\\n  JNIEXPORT RETURN_TYPE \\\n    Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegDecoder_ ## NAME \\\n      (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\\\n\n#define LIBRARY_FUNC(RETURN_TYPE, NAME, ...) \\\n  extern \"C\" { \\\n  JNIEXPORT RETURN_TYPE \\\n    Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_ ## NAME \\\n      (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\\\n  } \\\n  JNIEXPORT RETURN_TYPE \\\n    Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_ ## NAME \\\n      (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\\\n\n#define ERROR_STRING_BUFFER_LENGTH 256\n\n// Output format corresponding to AudioFormat.ENCODING_PCM_16BIT.\nstatic const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16;\n// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.\nstatic const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;\n\n// Error codes matching FfmpegDecoder.java.\nstatic const int DECODER_ERROR_INVALID_DATA = -1;\nstatic const int DECODER_ERROR_OTHER = -2;\n\n/**\n * Returns the AVCodec with the specified name, or NULL if it is not available.\n */\nAVCodec *getCodecByName(JNIEnv* env, jstring codecName);\n\n/**\n * Allocates and opens a new AVCodecContext for the specified codec, passing the\n * provided extraData as initialization data for the decoder if it is non-NULL.\n * Returns the created context.\n */\nAVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,\n                              jboolean outputFloat, jint rawSampleRate,\n                              jint rawChannelCount);\n\n/**\n * Decodes the packet into the output buffer, returning the number of bytes\n * written, or a negative DECODER_ERROR constant value in the case of an error.\n */\nint decodePacket(AVCodecContext *context, AVPacket *packet,\n                 uint8_t *outputBuffer, int outputSize);\n\n/**\n * Outputs a log message describing the avcodec error number.\n */\nvoid logError(const char *functionName, int errorNumber);\n\n/**\n * Releases the specified context.\n */\nvoid releaseContext(AVCodecContext *context);\n\njint JNI_OnLoad(JavaVM *vm, void *reserved) {\n  JNIEnv *env;\n  if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {\n    return -1;\n  }\n  avcodec_register_all();\n  return JNI_VERSION_1_6;\n}\n\nLIBRARY_FUNC(jstring, ffmpegGetVersion) {\n  return env->NewStringUTF(LIBAVCODEC_IDENT);\n}\n\nLIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) {\n  return getCodecByName(env, codecName) != NULL;\n}\n\nDECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData,\n             jboolean outputFloat, jint rawSampleRate, jint rawChannelCount) {\n  AVCodec *codec = getCodecByName(env, codecName);\n  if (!codec) {\n    LOGE(\"Codec not found.\");\n    return 0L;\n  }\n  return (jlong)createContext(env, codec, extraData, outputFloat, rawSampleRate,\n                              rawChannelCount);\n}\n\nDECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,\n    jint inputSize, jobject outputData, jint outputSize) {\n  if (!context) {\n    LOGE(\"Context must be non-NULL.\");\n    return -1;\n  }\n  if (!inputData || !outputData) {\n    LOGE(\"Input and output buffers must be non-NULL.\");\n    return -1;\n  }\n  if (inputSize < 0) {\n    LOGE(\"Invalid input buffer size: %d.\", inputSize);\n    return -1;\n  }\n  if (outputSize < 0) {\n    LOGE(\"Invalid output buffer length: %d\", outputSize);\n    return -1;\n  }\n  uint8_t *inputBuffer = (uint8_t *) env->GetDirectBufferAddress(inputData);\n  uint8_t *outputBuffer = (uint8_t *) env->GetDirectBufferAddress(outputData);\n  AVPacket packet;\n  av_init_packet(&packet);\n  packet.data = inputBuffer;\n  packet.size = inputSize;\n  return decodePacket((AVCodecContext *) context, &packet, outputBuffer,\n                      outputSize);\n}\n\nDECODER_FUNC(jint, ffmpegGetChannelCount, jlong context) {\n  if (!context) {\n    LOGE(\"Context must be non-NULL.\");\n    return -1;\n  }\n  return ((AVCodecContext *) context)->channels;\n}\n\nDECODER_FUNC(jint, ffmpegGetSampleRate, jlong context) {\n  if (!context) {\n    LOGE(\"Context must be non-NULL.\");\n    return -1;\n  }\n  return ((AVCodecContext *) context)->sample_rate;\n}\n\nDECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) {\n  AVCodecContext *context = (AVCodecContext *) jContext;\n  if (!context) {\n    LOGE(\"Tried to reset without a context.\");\n    return 0L;\n  }\n\n  AVCodecID codecId = context->codec_id;\n  if (codecId == AV_CODEC_ID_TRUEHD) {\n    // Release and recreate the context if the codec is TrueHD.\n    // TODO: Figure out why flushing doesn't work for this codec.\n    releaseContext(context);\n    AVCodec *codec = avcodec_find_decoder(codecId);\n    if (!codec) {\n      LOGE(\"Unexpected error finding codec %d.\", codecId);\n      return 0L;\n    }\n    jboolean outputFloat =\n        (jboolean)(context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT);\n    return (jlong)createContext(env, codec, extraData, outputFloat,\n                                /* rawSampleRate= */ -1,\n                                /* rawChannelCount= */ -1);\n  }\n\n  avcodec_flush_buffers(context);\n  return (jlong) context;\n}\n\nDECODER_FUNC(void, ffmpegRelease, jlong context) {\n  if (context) {\n    releaseContext((AVCodecContext *) context);\n  }\n}\n\nAVCodec *getCodecByName(JNIEnv* env, jstring codecName) {\n  if (!codecName) {\n    return NULL;\n  }\n  const char *codecNameChars = env->GetStringUTFChars(codecName, NULL);\n  AVCodec *codec = avcodec_find_decoder_by_name(codecNameChars);\n  env->ReleaseStringUTFChars(codecName, codecNameChars);\n  return codec;\n}\n\nAVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,\n                              jboolean outputFloat, jint rawSampleRate,\n                              jint rawChannelCount) {\n  AVCodecContext *context = avcodec_alloc_context3(codec);\n  if (!context) {\n    LOGE(\"Failed to allocate context.\");\n    return NULL;\n  }\n  context->request_sample_fmt =\n      outputFloat ? OUTPUT_FORMAT_PCM_FLOAT : OUTPUT_FORMAT_PCM_16BIT;\n  if (extraData) {\n    jsize size = env->GetArrayLength(extraData);\n    context->extradata_size = size;\n    context->extradata =\n        (uint8_t *) av_malloc(size + AV_INPUT_BUFFER_PADDING_SIZE);\n    if (!context->extradata) {\n      LOGE(\"Failed to allocate extradata.\");\n      releaseContext(context);\n      return NULL;\n    }\n    env->GetByteArrayRegion(extraData, 0, size, (jbyte *) context->extradata);\n  }\n  if (context->codec_id == AV_CODEC_ID_PCM_MULAW ||\n      context->codec_id == AV_CODEC_ID_PCM_ALAW) {\n    context->sample_rate = rawSampleRate;\n    context->channels = rawChannelCount;\n    context->channel_layout = av_get_default_channel_layout(rawChannelCount);\n  }\n  context->err_recognition = AV_EF_IGNORE_ERR;\n  int result = avcodec_open2(context, codec, NULL);\n  if (result < 0) {\n    logError(\"avcodec_open2\", result);\n    releaseContext(context);\n    return NULL;\n  }\n  return context;\n}\n\nint decodePacket(AVCodecContext *context, AVPacket *packet,\n                 uint8_t *outputBuffer, int outputSize) {\n  int result = 0;\n  // Queue input data.\n  result = avcodec_send_packet(context, packet);\n  if (result) {\n    logError(\"avcodec_send_packet\", result);\n    return result == AVERROR_INVALIDDATA ? DECODER_ERROR_INVALID_DATA\n                                         : DECODER_ERROR_OTHER;\n  }\n\n  // Dequeue output data until it runs out.\n  int outSize = 0;\n  while (true) {\n    AVFrame *frame = av_frame_alloc();\n    if (!frame) {\n      LOGE(\"Failed to allocate output frame.\");\n      return -1;\n    }\n    result = avcodec_receive_frame(context, frame);\n    if (result) {\n      av_frame_free(&frame);\n      if (result == AVERROR(EAGAIN)) {\n        break;\n      }\n      logError(\"avcodec_receive_frame\", result);\n      return result;\n    }\n\n    // Resample output.\n    AVSampleFormat sampleFormat = context->sample_fmt;\n    int channelCount = context->channels;\n    int channelLayout = context->channel_layout;\n    int sampleRate = context->sample_rate;\n    int sampleCount = frame->nb_samples;\n    int dataSize = av_samples_get_buffer_size(NULL, channelCount, sampleCount,\n                                              sampleFormat, 1);\n    AVAudioResampleContext *resampleContext;\n    if (context->opaque) {\n      resampleContext = (AVAudioResampleContext *) context->opaque;\n    } else {\n      resampleContext = avresample_alloc_context();\n      av_opt_set_int(resampleContext, \"in_channel_layout\",  channelLayout, 0);\n      av_opt_set_int(resampleContext, \"out_channel_layout\", channelLayout, 0);\n      av_opt_set_int(resampleContext, \"in_sample_rate\", sampleRate, 0);\n      av_opt_set_int(resampleContext, \"out_sample_rate\", sampleRate, 0);\n      av_opt_set_int(resampleContext, \"in_sample_fmt\", sampleFormat, 0);\n      // The output format is always the requested format.\n      av_opt_set_int(resampleContext, \"out_sample_fmt\",\n          context->request_sample_fmt, 0);\n      result = avresample_open(resampleContext);\n      if (result < 0) {\n        logError(\"avresample_open\", result);\n        av_frame_free(&frame);\n        return -1;\n      }\n      context->opaque = resampleContext;\n    }\n    int inSampleSize = av_get_bytes_per_sample(sampleFormat);\n    int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt);\n    int outSamples = avresample_get_out_samples(resampleContext, sampleCount);\n    int bufferOutSize = outSampleSize * channelCount * outSamples;\n    if (outSize + bufferOutSize > outputSize) {\n      LOGE(\"Output buffer size (%d) too small for output data (%d).\",\n           outputSize, outSize + bufferOutSize);\n      av_frame_free(&frame);\n      return -1;\n    }\n    result = avresample_convert(resampleContext, &outputBuffer, bufferOutSize,\n                                outSamples, frame->data, frame->linesize[0],\n                                sampleCount);\n    av_frame_free(&frame);\n    if (result < 0) {\n      logError(\"avresample_convert\", result);\n      return result;\n    }\n    int available = avresample_available(resampleContext);\n    if (available != 0) {\n      LOGE(\"Expected no samples remaining after resampling, but found %d.\",\n           available);\n      return -1;\n    }\n    outputBuffer += bufferOutSize;\n    outSize += bufferOutSize;\n  }\n  return outSize;\n}\n\nvoid logError(const char *functionName, int errorNumber) {\n  char *buffer = (char *) malloc(ERROR_STRING_BUFFER_LENGTH * sizeof(char));\n  av_strerror(errorNumber, buffer, ERROR_STRING_BUFFER_LENGTH);\n  LOGE(\"Error in %s: %s\", functionName, buffer);\n  free(buffer);\n}\n\nvoid releaseContext(AVCodecContext *context) {\n  if (!context) {\n    return;\n  }\n  AVAudioResampleContext *resampleContext;\n  if ((resampleContext = (AVAudioResampleContext *) context->opaque)) {\n    avresample_free(&resampleContext);\n    context->opaque = NULL;\n  }\n  avcodec_free_context(&context);\n}\n\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ffmpeg/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<manifest package=\"com.google.android.exoplayer2.ext.ffmpeg\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ffmpeg/src/test/java/com/google/android/exoplayer2/ext/ffmpeg/DefaultRenderersFactoryTest.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.ext.ffmpeg;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DefaultRenderersFactoryTest {\n\n  @Test\n  public void createRenderers_instantiatesVpxRenderer() {\n    DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(\n        FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/README.md",
    "content": "# ExoPlayer Flac extension #\n\nThe Flac extension provides `FlacExtractor` and `LibflacAudioRenderer`, which\nuse libFLAC (the Flac decoding library) to extract and decode FLAC audio.\n\n## License note ##\n\nPlease note that whilst the code in this repository is licensed under\n[Apache 2.0][], using this extension also requires building and including one or\nmore external libraries as described below. These are licensed separately.\n\n[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE\n\n## Build instructions ##\n\nTo use this extension you need to clone the ExoPlayer repository and depend on\nits modules locally. Instructions for doing this can be found in ExoPlayer's\n[top level README][].\n\nIn addition, it's necessary to build the extension's native components as\nfollows:\n\n* Set the following environment variables:\n\n```\ncd \"<path to exoplayer checkout>\"\nEXOPLAYER_ROOT=\"$(pwd)\"\nFLAC_EXT_PATH=\"${EXOPLAYER_ROOT}/extensions/flac/src/main\"\n```\n\n* Download the [Android NDK][] (version <= 17c) and set its location in an\n  environment variable:\n\n```\nNDK_PATH=\"<path to Android NDK>\"\n```\n\n* Download and extract flac-1.3.2 as \"${FLAC_EXT_PATH}/jni/flac\" folder:\n\n```\ncd \"${FLAC_EXT_PATH}/jni\" && \\\ncurl https://ftp.osuosl.org/pub/xiph/releases/flac/flac-1.3.2.tar.xz | tar xJ && \\\nmv flac-1.3.2 flac\n```\n\n* Build the JNI native libraries from the command line:\n\n```\ncd \"${FLAC_EXT_PATH}\"/jni && \\\n${NDK_PATH}/ndk-build APP_ABI=all -j4\n```\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html\n\n## Using the extension ##\n\nOnce you've followed the instructions above to check out, build and depend on\nthe extension, the next step is to tell ExoPlayer to use the extractor and/or\nrenderer.\n\n### Using `FlacExtractor` ###\n\n`FlacExtractor` is used via `ExtractorMediaSource`. If you're using\n`DefaultExtractorsFactory`, `FlacExtractor` will automatically be used to read\n`.flac` files. If you're not using `DefaultExtractorsFactory`, return a\n`FlacExtractor` from your `ExtractorsFactory.createExtractors` implementation.\n\n### Using `LibflacAudioRenderer` ###\n\n* If you're passing a `DefaultRenderersFactory` to\n  `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by\n  setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory`\n  constructor to `EXTENSION_RENDERER_MODE_ON`. This will use\n  `LibflacAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't\n  support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give\n  `LibflacAudioRenderer` priority over `MediaCodecAudioRenderer`.\n* If you've subclassed `DefaultRenderersFactory`, add a `LibflacAudioRenderer`\n  to the output list in `buildAudioRenderers`. ExoPlayer will use the first\n  `Renderer` in the list that supports the input media format.\n* If you've implemented your own `RenderersFactory`, return a\n  `LibflacAudioRenderer` instance from `createRenderers`. ExoPlayer will use the\n  first `Renderer` in the returned array that supports the input media format.\n* If you're using `ExoPlayerFactory.newInstance`, pass a `LibflacAudioRenderer`\n  in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the\n  list that supports the input media format.\n\nNote: These instructions assume you're using `DefaultTrackSelector`. If you have\na custom track selector the choice of `Renderer` is up to your implementation,\nso you need to make sure you are passing an `LibflacAudioRenderer` to the\nplayer, then implement your own logic to use the renderer for a given track.\n\n## Links ##\n\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*`\n  belong to this module.\n\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/build.gradle",
    "content": "// Copyright (C) 2016 The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n        consumerProguardFiles 'proguard-rules.txt'\n        testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'\n    }\n\n    sourceSets.main {\n        jniLibs.srcDir 'src/main/libs'\n        jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    implementation 'androidx.annotation:annotation:1.1.0'\n    compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion\n    androidTestImplementation project(modulePrefix + 'testutils')\n    androidTestImplementation 'androidx.test:runner:' + androidXTestVersion\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n}\n\next {\n    javadocTitle = 'FLAC extension'\n}\napply from: '../../javadoc_library.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/proguard-rules.txt",
    "content": "# Proguard rules specific to the Flac extension.\n\n# This prevents the names of native methods from being obfuscated.\n-keepclasseswithmembernames class * {\n    native <methods>;\n}\n\n# Some members of these classes are being accessed from native methods. Keep them unobfuscated.\n-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {\n    *;\n}\n-keep class com.google.android.exoplayer2.util.FlacStreamMetadata {\n    *;\n}\n-keep class com.google.android.exoplayer2.metadata.flac.PictureFrame {\n    *;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/androidTest/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"com.google.android.exoplayer2.ext.flac.test\">\n\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n  <uses-sdk/>\n\n  <application\n      android:allowBackup=\"false\"\n      tools:ignore=\"MissingApplicationIcon,HardcodedDebugMode\">\n    <uses-library android:name=\"android.test.runner\"/>\n  </application>\n\n  <instrumentation\n      android:targetPackage=\"com.google.android.exoplayer2.ext.flac.test\"\n      android:name=\"androidx.test.runner.AndroidJUnitRunner\"/>\n\n</manifest>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/androidTest/assets/bear.flac.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=8880]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 768000\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/raw\n    maxInputSize = 16384\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = 2\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 526272\n  sample count = 33\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 16384, hash 61D2C5C2\n  sample 1:\n    time = 85333\n    flags = 1\n    data = length 16384, hash E6D7F214\n  sample 2:\n    time = 170666\n    flags = 1\n    data = length 16384, hash 59BF0D5D\n  sample 3:\n    time = 256000\n    flags = 1\n    data = length 16384, hash 3625F468\n  sample 4:\n    time = 341333\n    flags = 1\n    data = length 16384, hash F66A323\n  sample 5:\n    time = 426666\n    flags = 1\n    data = length 16384, hash CDBAE629\n  sample 6:\n    time = 512000\n    flags = 1\n    data = length 16384, hash 536F3A91\n  sample 7:\n    time = 597333\n    flags = 1\n    data = length 16384, hash D4F35C9C\n  sample 8:\n    time = 682666\n    flags = 1\n    data = length 16384, hash EE04CEBF\n  sample 9:\n    time = 768000\n    flags = 1\n    data = length 16384, hash 647E2A67\n  sample 10:\n    time = 853333\n    flags = 1\n    data = length 16384, hash 31583F2C\n  sample 11:\n    time = 938666\n    flags = 1\n    data = length 16384, hash E433A93D\n  sample 12:\n    time = 1024000\n    flags = 1\n    data = length 16384, hash 5E1C7051\n  sample 13:\n    time = 1109333\n    flags = 1\n    data = length 16384, hash 43E6E358\n  sample 14:\n    time = 1194666\n    flags = 1\n    data = length 16384, hash 5DC1B256\n  sample 15:\n    time = 1280000\n    flags = 1\n    data = length 16384, hash 3D9D95CF\n  sample 16:\n    time = 1365333\n    flags = 1\n    data = length 16384, hash 2A5BD2C0\n  sample 17:\n    time = 1450666\n    flags = 1\n    data = length 16384, hash 93E25061\n  sample 18:\n    time = 1536000\n    flags = 1\n    data = length 16384, hash B81793D8\n  sample 19:\n    time = 1621333\n    flags = 1\n    data = length 16384, hash 1A3BD49F\n  sample 20:\n    time = 1706666\n    flags = 1\n    data = length 16384, hash FB672FF1\n  sample 21:\n    time = 1792000\n    flags = 1\n    data = length 16384, hash 48AB8B45\n  sample 22:\n    time = 1877333\n    flags = 1\n    data = length 16384, hash 13C9640A\n  sample 23:\n    time = 1962666\n    flags = 1\n    data = length 16384, hash 499E4A0B\n  sample 24:\n    time = 2048000\n    flags = 1\n    data = length 16384, hash F9A783E6\n  sample 25:\n    time = 2133333\n    flags = 1\n    data = length 16384, hash D2B77598\n  sample 26:\n    time = 2218666\n    flags = 1\n    data = length 16384, hash CE5B826C\n  sample 27:\n    time = 2304000\n    flags = 1\n    data = length 16384, hash E99EE956\n  sample 28:\n    time = 2389333\n    flags = 1\n    data = length 16384, hash F2DB1486\n  sample 29:\n    time = 2474666\n    flags = 1\n    data = length 16384, hash 1636EAB\n  sample 30:\n    time = 2560000\n    flags = 1\n    data = length 16384, hash 23457C08\n  sample 31:\n    time = 2645333\n    flags = 1\n    data = length 16384, hash 30EB8381\n  sample 32:\n    time = 2730666\n    flags = 1\n    data = length 1984, hash 59CFDE1B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/androidTest/assets/bear.flac.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=8880]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 768000\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/raw\n    maxInputSize = 16384\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = 2\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 362432\n  sample count = 23\n  sample 0:\n    time = 853333\n    flags = 1\n    data = length 16384, hash 31583F2C\n  sample 1:\n    time = 938666\n    flags = 1\n    data = length 16384, hash E433A93D\n  sample 2:\n    time = 1024000\n    flags = 1\n    data = length 16384, hash 5E1C7051\n  sample 3:\n    time = 1109333\n    flags = 1\n    data = length 16384, hash 43E6E358\n  sample 4:\n    time = 1194666\n    flags = 1\n    data = length 16384, hash 5DC1B256\n  sample 5:\n    time = 1280000\n    flags = 1\n    data = length 16384, hash 3D9D95CF\n  sample 6:\n    time = 1365333\n    flags = 1\n    data = length 16384, hash 2A5BD2C0\n  sample 7:\n    time = 1450666\n    flags = 1\n    data = length 16384, hash 93E25061\n  sample 8:\n    time = 1536000\n    flags = 1\n    data = length 16384, hash B81793D8\n  sample 9:\n    time = 1621333\n    flags = 1\n    data = length 16384, hash 1A3BD49F\n  sample 10:\n    time = 1706666\n    flags = 1\n    data = length 16384, hash FB672FF1\n  sample 11:\n    time = 1792000\n    flags = 1\n    data = length 16384, hash 48AB8B45\n  sample 12:\n    time = 1877333\n    flags = 1\n    data = length 16384, hash 13C9640A\n  sample 13:\n    time = 1962666\n    flags = 1\n    data = length 16384, hash 499E4A0B\n  sample 14:\n    time = 2048000\n    flags = 1\n    data = length 16384, hash F9A783E6\n  sample 15:\n    time = 2133333\n    flags = 1\n    data = length 16384, hash D2B77598\n  sample 16:\n    time = 2218666\n    flags = 1\n    data = length 16384, hash CE5B826C\n  sample 17:\n    time = 2304000\n    flags = 1\n    data = length 16384, hash E99EE956\n  sample 18:\n    time = 2389333\n    flags = 1\n    data = length 16384, hash F2DB1486\n  sample 19:\n    time = 2474666\n    flags = 1\n    data = length 16384, hash 1636EAB\n  sample 20:\n    time = 2560000\n    flags = 1\n    data = length 16384, hash 23457C08\n  sample 21:\n    time = 2645333\n    flags = 1\n    data = length 16384, hash 30EB8381\n  sample 22:\n    time = 2730666\n    flags = 1\n    data = length 1984, hash 59CFDE1B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/androidTest/assets/bear.flac.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=8880]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 768000\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/raw\n    maxInputSize = 16384\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = 2\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 182208\n  sample count = 12\n  sample 0:\n    time = 1792000\n    flags = 1\n    data = length 16384, hash 48AB8B45\n  sample 1:\n    time = 1877333\n    flags = 1\n    data = length 16384, hash 13C9640A\n  sample 2:\n    time = 1962666\n    flags = 1\n    data = length 16384, hash 499E4A0B\n  sample 3:\n    time = 2048000\n    flags = 1\n    data = length 16384, hash F9A783E6\n  sample 4:\n    time = 2133333\n    flags = 1\n    data = length 16384, hash D2B77598\n  sample 5:\n    time = 2218666\n    flags = 1\n    data = length 16384, hash CE5B826C\n  sample 6:\n    time = 2304000\n    flags = 1\n    data = length 16384, hash E99EE956\n  sample 7:\n    time = 2389333\n    flags = 1\n    data = length 16384, hash F2DB1486\n  sample 8:\n    time = 2474666\n    flags = 1\n    data = length 16384, hash 1636EAB\n  sample 9:\n    time = 2560000\n    flags = 1\n    data = length 16384, hash 23457C08\n  sample 10:\n    time = 2645333\n    flags = 1\n    data = length 16384, hash 30EB8381\n  sample 11:\n    time = 2730666\n    flags = 1\n    data = length 1984, hash 59CFDE1B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/androidTest/assets/bear.flac.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=8880]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 768000\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/raw\n    maxInputSize = 16384\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = 2\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 18368\n  sample count = 2\n  sample 0:\n    time = 2645333\n    flags = 1\n    data = length 16384, hash 30EB8381\n  sample 1:\n    time = 2730666\n    flags = 1\n    data = length 1984, hash 59CFDE1B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/androidTest/assets/bear_with_id3.flac.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=55284]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 768000\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/raw\n    maxInputSize = 16384\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = 2\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 526272\n  sample count = 33\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 16384, hash 61D2C5C2\n  sample 1:\n    time = 85333\n    flags = 1\n    data = length 16384, hash E6D7F214\n  sample 2:\n    time = 170666\n    flags = 1\n    data = length 16384, hash 59BF0D5D\n  sample 3:\n    time = 256000\n    flags = 1\n    data = length 16384, hash 3625F468\n  sample 4:\n    time = 341333\n    flags = 1\n    data = length 16384, hash F66A323\n  sample 5:\n    time = 426666\n    flags = 1\n    data = length 16384, hash CDBAE629\n  sample 6:\n    time = 512000\n    flags = 1\n    data = length 16384, hash 536F3A91\n  sample 7:\n    time = 597333\n    flags = 1\n    data = length 16384, hash D4F35C9C\n  sample 8:\n    time = 682666\n    flags = 1\n    data = length 16384, hash EE04CEBF\n  sample 9:\n    time = 768000\n    flags = 1\n    data = length 16384, hash 647E2A67\n  sample 10:\n    time = 853333\n    flags = 1\n    data = length 16384, hash 31583F2C\n  sample 11:\n    time = 938666\n    flags = 1\n    data = length 16384, hash E433A93D\n  sample 12:\n    time = 1024000\n    flags = 1\n    data = length 16384, hash 5E1C7051\n  sample 13:\n    time = 1109333\n    flags = 1\n    data = length 16384, hash 43E6E358\n  sample 14:\n    time = 1194666\n    flags = 1\n    data = length 16384, hash 5DC1B256\n  sample 15:\n    time = 1280000\n    flags = 1\n    data = length 16384, hash 3D9D95CF\n  sample 16:\n    time = 1365333\n    flags = 1\n    data = length 16384, hash 2A5BD2C0\n  sample 17:\n    time = 1450666\n    flags = 1\n    data = length 16384, hash 93E25061\n  sample 18:\n    time = 1536000\n    flags = 1\n    data = length 16384, hash B81793D8\n  sample 19:\n    time = 1621333\n    flags = 1\n    data = length 16384, hash 1A3BD49F\n  sample 20:\n    time = 1706666\n    flags = 1\n    data = length 16384, hash FB672FF1\n  sample 21:\n    time = 1792000\n    flags = 1\n    data = length 16384, hash 48AB8B45\n  sample 22:\n    time = 1877333\n    flags = 1\n    data = length 16384, hash 13C9640A\n  sample 23:\n    time = 1962666\n    flags = 1\n    data = length 16384, hash 499E4A0B\n  sample 24:\n    time = 2048000\n    flags = 1\n    data = length 16384, hash F9A783E6\n  sample 25:\n    time = 2133333\n    flags = 1\n    data = length 16384, hash D2B77598\n  sample 26:\n    time = 2218666\n    flags = 1\n    data = length 16384, hash CE5B826C\n  sample 27:\n    time = 2304000\n    flags = 1\n    data = length 16384, hash E99EE956\n  sample 28:\n    time = 2389333\n    flags = 1\n    data = length 16384, hash F2DB1486\n  sample 29:\n    time = 2474666\n    flags = 1\n    data = length 16384, hash 1636EAB\n  sample 30:\n    time = 2560000\n    flags = 1\n    data = length 16384, hash 23457C08\n  sample 31:\n    time = 2645333\n    flags = 1\n    data = length 16384, hash 30EB8381\n  sample 32:\n    time = 2730666\n    flags = 1\n    data = length 1984, hash 59CFDE1B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/androidTest/assets/bear_with_id3.flac.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=55284]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 768000\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/raw\n    maxInputSize = 16384\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = 2\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 362432\n  sample count = 23\n  sample 0:\n    time = 853333\n    flags = 1\n    data = length 16384, hash 31583F2C\n  sample 1:\n    time = 938666\n    flags = 1\n    data = length 16384, hash E433A93D\n  sample 2:\n    time = 1024000\n    flags = 1\n    data = length 16384, hash 5E1C7051\n  sample 3:\n    time = 1109333\n    flags = 1\n    data = length 16384, hash 43E6E358\n  sample 4:\n    time = 1194666\n    flags = 1\n    data = length 16384, hash 5DC1B256\n  sample 5:\n    time = 1280000\n    flags = 1\n    data = length 16384, hash 3D9D95CF\n  sample 6:\n    time = 1365333\n    flags = 1\n    data = length 16384, hash 2A5BD2C0\n  sample 7:\n    time = 1450666\n    flags = 1\n    data = length 16384, hash 93E25061\n  sample 8:\n    time = 1536000\n    flags = 1\n    data = length 16384, hash B81793D8\n  sample 9:\n    time = 1621333\n    flags = 1\n    data = length 16384, hash 1A3BD49F\n  sample 10:\n    time = 1706666\n    flags = 1\n    data = length 16384, hash FB672FF1\n  sample 11:\n    time = 1792000\n    flags = 1\n    data = length 16384, hash 48AB8B45\n  sample 12:\n    time = 1877333\n    flags = 1\n    data = length 16384, hash 13C9640A\n  sample 13:\n    time = 1962666\n    flags = 1\n    data = length 16384, hash 499E4A0B\n  sample 14:\n    time = 2048000\n    flags = 1\n    data = length 16384, hash F9A783E6\n  sample 15:\n    time = 2133333\n    flags = 1\n    data = length 16384, hash D2B77598\n  sample 16:\n    time = 2218666\n    flags = 1\n    data = length 16384, hash CE5B826C\n  sample 17:\n    time = 2304000\n    flags = 1\n    data = length 16384, hash E99EE956\n  sample 18:\n    time = 2389333\n    flags = 1\n    data = length 16384, hash F2DB1486\n  sample 19:\n    time = 2474666\n    flags = 1\n    data = length 16384, hash 1636EAB\n  sample 20:\n    time = 2560000\n    flags = 1\n    data = length 16384, hash 23457C08\n  sample 21:\n    time = 2645333\n    flags = 1\n    data = length 16384, hash 30EB8381\n  sample 22:\n    time = 2730666\n    flags = 1\n    data = length 1984, hash 59CFDE1B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/androidTest/assets/bear_with_id3.flac.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=55284]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 768000\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/raw\n    maxInputSize = 16384\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = 2\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 182208\n  sample count = 12\n  sample 0:\n    time = 1792000\n    flags = 1\n    data = length 16384, hash 48AB8B45\n  sample 1:\n    time = 1877333\n    flags = 1\n    data = length 16384, hash 13C9640A\n  sample 2:\n    time = 1962666\n    flags = 1\n    data = length 16384, hash 499E4A0B\n  sample 3:\n    time = 2048000\n    flags = 1\n    data = length 16384, hash F9A783E6\n  sample 4:\n    time = 2133333\n    flags = 1\n    data = length 16384, hash D2B77598\n  sample 5:\n    time = 2218666\n    flags = 1\n    data = length 16384, hash CE5B826C\n  sample 6:\n    time = 2304000\n    flags = 1\n    data = length 16384, hash E99EE956\n  sample 7:\n    time = 2389333\n    flags = 1\n    data = length 16384, hash F2DB1486\n  sample 8:\n    time = 2474666\n    flags = 1\n    data = length 16384, hash 1636EAB\n  sample 9:\n    time = 2560000\n    flags = 1\n    data = length 16384, hash 23457C08\n  sample 10:\n    time = 2645333\n    flags = 1\n    data = length 16384, hash 30EB8381\n  sample 11:\n    time = 2730666\n    flags = 1\n    data = length 1984, hash 59CFDE1B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/androidTest/assets/bear_with_id3.flac.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=55284]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 768000\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/raw\n    maxInputSize = 16384\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = 2\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 18368\n  sample count = 2\n  sample 0:\n    time = 2645333\n    flags = 1\n    data = length 16384, hash 30EB8381\n  sample 1:\n    time = 2730666\n    flags = 1\n    data = length 1984, hash 59CFDE1B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.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.ext.flac;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.io.IOException;\nimport org.junit.Before;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link FlacBinarySearchSeeker}. */\n@RunWith(AndroidJUnit4.class)\npublic final class FlacBinarySearchSeekerTest {\n\n  private static final String NOSEEKTABLE_FLAC = \"bear_no_seek.flac\";\n  private static final int DURATION_US = 2_741_000;\n\n  @Before\n  public void setUp() {\n    if (!FlacLibrary.isAvailable()) {\n      fail(\"Flac library not available.\");\n    }\n  }\n\n  public void testGetSeekMap_returnsSeekMapWithCorrectDuration()\n      throws IOException, FlacDecoderException, InterruptedException {\n    byte[] data =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);\n\n    FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();\n    FlacDecoderJni decoderJni = new FlacDecoderJni();\n    decoderJni.setData(input);\n\n    FlacBinarySearchSeeker seeker =\n        new FlacBinarySearchSeeker(\n            decoderJni.decodeStreamMetadata(),\n            /* firstFramePosition= */ 0,\n            data.length,\n            decoderJni);\n\n    SeekMap seekMap = seeker.getSeekMap();\n    assertThat(seekMap).isNotNull();\n    assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);\n    assertThat(seekMap.isSeekable()).isTrue();\n  }\n\n  public void testSetSeekTargetUs_returnsSeekPending()\n      throws IOException, FlacDecoderException, InterruptedException {\n    byte[] data =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);\n\n    FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();\n    FlacDecoderJni decoderJni = new FlacDecoderJni();\n    decoderJni.setData(input);\n    FlacBinarySearchSeeker seeker =\n        new FlacBinarySearchSeeker(\n            decoderJni.decodeStreamMetadata(),\n            /* firstFramePosition= */ 0,\n            data.length,\n            decoderJni);\n\n    seeker.setSeekTargetUs(/* timeUs= */ 1000);\n    assertThat(seeker.isSeeking()).isTrue();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.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.ext.flac;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\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.extractor.SeekMap;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.FakeExtractorOutput;\nimport com.google.android.exoplayer2.testutil.FakeTrackOutput;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.DefaultDataSource;\nimport com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Random;\nimport org.junit.Before;\nimport org.junit.runner.RunWith;\n\n/** Seeking tests for {@link FlacExtractor} when the FLAC stream does not have a SEEKTABLE. */\n@RunWith(AndroidJUnit4.class)\npublic final class FlacExtractorSeekTest {\n\n  private static final String NO_SEEKTABLE_FLAC = \"bear_no_seek.flac\";\n  private static final int DURATION_US = 2_741_000;\n  private static final Uri FILE_URI = Uri.parse(\"file:///android_asset/\" + NO_SEEKTABLE_FLAC);\n  private static final Random RANDOM = new Random(1234L);\n\n  private FakeExtractorOutput expectedOutput;\n  private FakeTrackOutput expectedTrackOutput;\n\n  private DefaultDataSource dataSource;\n  private PositionHolder positionHolder;\n  private long totalInputLength;\n\n  @Before\n  public void setUp() throws Exception {\n    if (!FlacLibrary.isAvailable()) {\n      fail(\"Flac library not available.\");\n    }\n    expectedOutput = new FakeExtractorOutput();\n    extractAllSamplesFromFileToExpectedOutput(\n        ApplicationProvider.getApplicationContext(), NO_SEEKTABLE_FLAC);\n    expectedTrackOutput = expectedOutput.trackOutputs.get(0);\n\n    dataSource =\n        new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), \"UserAgent\")\n            .createDataSource();\n    totalInputLength = readInputLength();\n    positionHolder = new PositionHolder();\n  }\n\n  public void testFlacExtractorReads_nonSeekTableFile_returnSeekableSeekMap()\n      throws IOException, InterruptedException {\n    FlacExtractor extractor = new FlacExtractor();\n\n    SeekMap seekMap = extractSeekMap(extractor, new FakeExtractorOutput());\n\n    assertThat(seekMap).isNotNull();\n    assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);\n    assertThat(seekMap.isSeekable()).isTrue();\n  }\n\n  public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    FlacExtractor extractor = new FlacExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = extractSeekMap(extractor, extractorOutput);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long targetSeekTimeUs = 987_000;\n    int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame()\n      throws IOException, InterruptedException {\n    FlacExtractor extractor = new FlacExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = extractSeekMap(extractor, extractorOutput);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long targetSeekTimeUs = seekMap.getDurationUs();\n\n    int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    FlacExtractor extractor = new FlacExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = extractSeekMap(extractor, extractorOutput);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long firstSeekTimeUs = 987_000;\n    seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput);\n\n    long targetSeekTimeUs = 0;\n    int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    FlacExtractor extractor = new FlacExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = extractSeekMap(extractor, extractorOutput);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long firstSeekTimeUs = 987_000;\n    seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput);\n\n    long targetSeekTimeUs = 1_234_000;\n    int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    FlacExtractor extractor = new FlacExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = extractSeekMap(extractor, extractorOutput);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long numSeek = 100;\n    for (long i = 0; i < numSeek; i++) {\n      long targetSeekTimeUs = RANDOM.nextInt(DURATION_US + 1);\n      int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);\n\n      assertThat(extractedFrameIndex).isNotEqualTo(-1);\n      assertFirstFrameAfterSeekContainTargetSeekTime(\n          trackOutput, targetSeekTimeUs, extractedFrameIndex);\n    }\n  }\n\n  // Internal methods\n\n  private long readInputLength() throws IOException {\n    DataSpec dataSpec = new DataSpec(FILE_URI, 0, C.LENGTH_UNSET, null);\n    long totalInputLength = dataSource.open(dataSpec);\n    Util.closeQuietly(dataSource);\n    return totalInputLength;\n  }\n\n  /**\n   * Seeks to the given seek time and keeps reading from input until we can extract at least one\n   * frame from the seek position, or until end-of-input is reached.\n   *\n   * @return The index of the first extracted frame written to the given {@code trackOutput} after\n   *     the seek is completed, or -1 if the seek is completed without any extracted frame.\n   */\n  private int seekToTimeUs(\n      FlacExtractor flacExtractor, SeekMap seekMap, long seekTimeUs, FakeTrackOutput trackOutput)\n      throws IOException, InterruptedException {\n    int numSampleBeforeSeek = trackOutput.getSampleCount();\n    SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs);\n\n    long initialSeekLoadPosition = seekPoints.first.position;\n    flacExtractor.seek(initialSeekLoadPosition, seekTimeUs);\n\n    positionHolder.position = C.POSITION_UNSET;\n    ExtractorInput extractorInput = getExtractorInputFromPosition(initialSeekLoadPosition);\n    int extractorReadResult = Extractor.RESULT_CONTINUE;\n    while (true) {\n      try {\n        // Keep reading until we can read at least one frame after seek\n        while (extractorReadResult == Extractor.RESULT_CONTINUE\n            && trackOutput.getSampleCount() == numSampleBeforeSeek) {\n          extractorReadResult = flacExtractor.read(extractorInput, positionHolder);\n        }\n      } finally {\n        Util.closeQuietly(dataSource);\n      }\n\n      if (extractorReadResult == Extractor.RESULT_SEEK) {\n        extractorInput = getExtractorInputFromPosition(positionHolder.position);\n        extractorReadResult = Extractor.RESULT_CONTINUE;\n      } else if (extractorReadResult == Extractor.RESULT_END_OF_INPUT) {\n        return -1;\n      } else if (trackOutput.getSampleCount() > numSampleBeforeSeek) {\n        // First index after seek = num sample before seek.\n        return numSampleBeforeSeek;\n      }\n    }\n  }\n\n  private @Nullable SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)\n      throws IOException, InterruptedException {\n    try {\n      ExtractorInput input = getExtractorInputFromPosition(0);\n      extractor.init(output);\n      while (output.seekMap == null) {\n        extractor.read(input, positionHolder);\n      }\n    } finally {\n      Util.closeQuietly(dataSource);\n    }\n    return output.seekMap;\n  }\n\n  private void assertFirstFrameAfterSeekContainTargetSeekTime(\n      FakeTrackOutput trackOutput, long seekTimeUs, int firstFrameIndexAfterSeek) {\n    int expectedSampleIndex = findTargetFrameInExpectedOutput(seekTimeUs);\n    // Assert that after seeking, the first sample frame written to output contains the sample\n    // at seek time.\n    trackOutput.assertSample(\n        firstFrameIndexAfterSeek,\n        expectedTrackOutput.getSampleData(expectedSampleIndex),\n        expectedTrackOutput.getSampleTimeUs(expectedSampleIndex),\n        expectedTrackOutput.getSampleFlags(expectedSampleIndex),\n        expectedTrackOutput.getSampleCryptoData(expectedSampleIndex));\n  }\n\n  private int findTargetFrameInExpectedOutput(long seekTimeUs) {\n    List<Long> sampleTimes = expectedTrackOutput.getSampleTimesUs();\n    for (int i = 0; i < sampleTimes.size() - 1; i++) {\n      long currentSampleTime = sampleTimes.get(i);\n      long nextSampleTime = sampleTimes.get(i + 1);\n      if (currentSampleTime <= seekTimeUs && nextSampleTime > seekTimeUs) {\n        return i;\n      }\n    }\n    return sampleTimes.size() - 1;\n  }\n\n  private ExtractorInput getExtractorInputFromPosition(long position) throws IOException {\n    DataSpec dataSpec = new DataSpec(FILE_URI, position, totalInputLength, /* key= */ null);\n    dataSource.open(dataSpec);\n    return new DefaultExtractorInput(dataSource, position, totalInputLength);\n  }\n\n  private void extractAllSamplesFromFileToExpectedOutput(Context context, String fileName)\n      throws IOException, InterruptedException {\n    byte[] data = TestUtil.getByteArray(context, fileName);\n\n    FlacExtractor extractor = new FlacExtractor();\n    extractor.init(expectedOutput);\n    FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();\n\n    while (extractor.read(input, new PositionHolder()) != Extractor.RESULT_END_OF_INPUT) {}\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.flac;\n\nimport static org.junit.Assert.fail;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport org.junit.Before;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link FlacExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic class FlacExtractorTest {\n\n  @Before\n  public void setUp() {\n    if (!FlacLibrary.isAvailable()) {\n      fail(\"Flac library not available.\");\n    }\n  }\n\n  public void testExtractFlacSample() throws Exception {\n    ExtractorAsserts.assertBehavior(\n        FlacExtractor::new, \"bear.flac\", ApplicationProvider.getApplicationContext());\n  }\n\n  public void testExtractFlacSampleWithId3Header() throws Exception {\n    ExtractorAsserts.assertBehavior(\n        FlacExtractor::new, \"bear_with_id3.flac\", ApplicationProvider.getApplicationContext());\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.flac;\n\nimport static org.junit.Assert.fail;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Looper;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.ExoPlayerFactory;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.ProgressiveMediaSource;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Playback tests using {@link LibflacAudioRenderer}. */\n@RunWith(AndroidJUnit4.class)\npublic class FlacPlaybackTest {\n\n  private static final String BEAR_FLAC_URI = \"asset:///bear-flac.mka\";\n\n  @Before\n  public void setUp() {\n    if (!FlacLibrary.isAvailable()) {\n      fail(\"Flac library not available.\");\n    }\n  }\n\n  @Test\n  public void testBasicPlayback() throws Exception {\n    playUri(BEAR_FLAC_URI);\n  }\n\n  private void playUri(String uri) throws Exception {\n    TestPlaybackRunnable testPlaybackRunnable =\n        new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());\n    Thread thread = new Thread(testPlaybackRunnable);\n    thread.start();\n    thread.join();\n    if (testPlaybackRunnable.playbackException != null) {\n      throw testPlaybackRunnable.playbackException;\n    }\n  }\n\n  private static class TestPlaybackRunnable implements Player.EventListener, Runnable {\n\n    private final Context context;\n    private final Uri uri;\n\n    private ExoPlayer player;\n    private ExoPlaybackException playbackException;\n\n    public TestPlaybackRunnable(Uri uri, Context context) {\n      this.uri = uri;\n      this.context = context;\n    }\n\n    @Override\n    public void run() {\n      Looper.prepare();\n      LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();\n      DefaultTrackSelector trackSelector = new DefaultTrackSelector();\n      player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);\n      player.addListener(this);\n      MediaSource mediaSource =\n          new ProgressiveMediaSource.Factory(\n                  new DefaultDataSourceFactory(context, \"ExoPlayerExtFlacTest\"),\n                  MatroskaExtractor.FACTORY)\n              .createMediaSource(uri);\n      player.prepare(mediaSource);\n      player.setPlayWhenReady(true);\n      Looper.loop();\n    }\n\n    @Override\n    public void onPlayerError(ExoPlaybackException error) {\n      playbackException = error;\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n      if (playbackState == Player.STATE_ENDED\n          || (playbackState == Player.STATE_IDLE && playbackException != null)) {\n        player.release();\n        Looper.myLooper().quit();\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest package=\"com.google.android.exoplayer2.ext.flac\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.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.ext.flac;\n\nimport com.google.android.exoplayer2.extractor.BinarySearchSeeker;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.FlacStreamMetadata;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\n\n/**\n * A {@link SeekMap} implementation for FLAC stream using binary search.\n *\n * <p>This seeker performs seeking by using binary search within the stream, until it finds the\n * frame that contains the target sample.\n */\n/* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker {\n\n  private final FlacDecoderJni decoderJni;\n\n  public FlacBinarySearchSeeker(\n      FlacStreamMetadata streamMetadata,\n      long firstFramePosition,\n      long inputLength,\n      FlacDecoderJni decoderJni) {\n    super(\n        new FlacSeekTimestampConverter(streamMetadata),\n        new FlacTimestampSeeker(decoderJni),\n        streamMetadata.durationUs(),\n        /* floorTimePosition= */ 0,\n        /* ceilingTimePosition= */ streamMetadata.totalSamples,\n        /* floorBytePosition= */ firstFramePosition,\n        /* ceilingBytePosition= */ inputLength,\n        /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),\n        /* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize));\n    this.decoderJni = Assertions.checkNotNull(decoderJni);\n  }\n\n  @Override\n  protected void onSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {\n    if (!foundTargetFrame) {\n      // If we can't find the target frame (sample), we need to reset the decoder jni so that\n      // it can continue from the result position.\n      decoderJni.reset(resultPosition);\n    }\n  }\n\n  private static final class FlacTimestampSeeker implements TimestampSeeker {\n\n    private final FlacDecoderJni decoderJni;\n\n    private FlacTimestampSeeker(FlacDecoderJni decoderJni) {\n      this.decoderJni = decoderJni;\n    }\n\n    @Override\n    public TimestampSearchResult searchForTimestamp(\n        ExtractorInput input, long targetSampleIndex, OutputFrameHolder outputFrameHolder)\n        throws IOException, InterruptedException {\n      ByteBuffer outputBuffer = outputFrameHolder.byteBuffer;\n      long searchPosition = input.getPosition();\n      decoderJni.reset(searchPosition);\n      try {\n        decoderJni.decodeSampleWithBacktrackPosition(\n            outputBuffer, /* retryPosition= */ searchPosition);\n      } catch (FlacDecoderJni.FlacFrameDecodeException e) {\n        // For some reasons, the extractor can't find a frame mid-stream.\n        // Stop the seeking and let it re-try playing at the last search position.\n        return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;\n      }\n      if (outputBuffer.limit() == 0) {\n        return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;\n      }\n\n      long lastFrameSampleIndex = decoderJni.getLastFrameFirstSampleIndex();\n      long nextFrameSampleIndex = decoderJni.getNextFrameFirstSampleIndex();\n      long nextFrameSamplePosition = decoderJni.getDecodePosition();\n\n      boolean targetSampleInLastFrame =\n          lastFrameSampleIndex <= targetSampleIndex && nextFrameSampleIndex > targetSampleIndex;\n\n      if (targetSampleInLastFrame) {\n        // We are holding the target frame in outputFrameHolder. Set its presentation time now.\n        outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp();\n        return TimestampSearchResult.targetFoundResult(input.getPosition());\n      } else if (nextFrameSampleIndex <= targetSampleIndex) {\n        return TimestampSearchResult.underestimatedResult(\n            nextFrameSampleIndex, nextFrameSamplePosition);\n      } else {\n        return TimestampSearchResult.overestimatedResult(lastFrameSampleIndex, searchPosition);\n      }\n    }\n  }\n\n  /**\n   * A {@link SeekTimestampConverter} implementation that returns the frame index (sample index) as\n   * the timestamp for a stream seek time position.\n   */\n  private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {\n    private final FlacStreamMetadata streamMetadata;\n\n    public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) {\n      this.streamMetadata = streamMetadata;\n    }\n\n    @Override\n    public long timeUsToTargetTime(long timeUs) {\n      return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.flac;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\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.util.FlacStreamMetadata;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.util.List;\n\n/**\n * Flac decoder.\n */\n/* package */ final class FlacDecoder extends\n    SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FlacDecoderException> {\n\n  private final int maxOutputBufferSize;\n  private final FlacDecoderJni decoderJni;\n\n  /**\n   * Creates a Flac decoder.\n   *\n   * @param numInputBuffers The number of input buffers.\n   * @param numOutputBuffers The number of output buffers.\n   * @param maxInputBufferSize The maximum required input buffer size if known, or {@link\n   *     Format#NO_VALUE} otherwise.\n   * @param initializationData Codec-specific initialization data. It should contain only one entry\n   *     which is the flac file header.\n   * @throws FlacDecoderException Thrown if an exception occurs when initializing the decoder.\n   */\n  public FlacDecoder(\n      int numInputBuffers,\n      int numOutputBuffers,\n      int maxInputBufferSize,\n      List<byte[]> initializationData)\n      throws FlacDecoderException {\n    super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);\n    if (initializationData.size() != 1) {\n      throw new FlacDecoderException(\"Initialization data must be of length 1\");\n    }\n    decoderJni = new FlacDecoderJni();\n    decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));\n    FlacStreamMetadata streamMetadata;\n    try {\n      streamMetadata = decoderJni.decodeStreamMetadata();\n    } catch (ParserException e) {\n      throw new FlacDecoderException(\"Failed to decode StreamInfo\", e);\n    } catch (IOException | InterruptedException e) {\n      // Never happens.\n      throw new IllegalStateException(e);\n    }\n\n    int initialInputBufferSize =\n        maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;\n    setInitialInputBufferSize(initialInputBufferSize);\n    maxOutputBufferSize = streamMetadata.maxDecodedFrameSize();\n  }\n\n  @Override\n  public String getName() {\n    return \"libflac\";\n  }\n\n  @Override\n  protected DecoderInputBuffer createInputBuffer() {\n    return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);\n  }\n\n  @Override\n  protected SimpleOutputBuffer createOutputBuffer() {\n    return new SimpleOutputBuffer(this);\n  }\n\n  @Override\n  protected FlacDecoderException createUnexpectedDecodeException(Throwable error) {\n    return new FlacDecoderException(\"Unexpected decode error\", error);\n  }\n\n  @Override\n  @Nullable\n  protected FlacDecoderException decode(\n      DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {\n    if (reset) {\n      decoderJni.flush();\n    }\n    decoderJni.setData(inputBuffer.data);\n    ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize);\n    try {\n      decoderJni.decodeSample(outputData);\n    } catch (FlacDecoderJni.FlacFrameDecodeException e) {\n      return new FlacDecoderException(\"Frame decoding failed\", e);\n    } catch (IOException | InterruptedException e) {\n      // Never happens.\n      throw new IllegalStateException(e);\n    }\n    return null;\n  }\n\n  @Override\n  public void release() {\n    super.release();\n    decoderJni.release();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.flac;\n\nimport com.google.android.exoplayer2.audio.AudioDecoderException;\n\n/**\n * Thrown when an Flac decoder error occurs.\n */\npublic final class FlacDecoderException extends AudioDecoderException {\n\n  /* package */ FlacDecoderException(String message) {\n    super(message);\n  }\n\n  /* package */ FlacDecoderException(String message, Throwable cause) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.flac;\n\nimport androidx.annotation.Nullable;\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.FlacStreamMetadata;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\n\n/**\n * JNI wrapper for the libflac Flac decoder.\n */\n/* package */ final class FlacDecoderJni {\n\n  /** Exception to be thrown if {@link #decodeSample(ByteBuffer)} fails to decode a frame. */\n  public static final class FlacFrameDecodeException extends Exception {\n\n    public final int errorCode;\n\n    public FlacFrameDecodeException(String message, int errorCode) {\n      super(message);\n      this.errorCode = errorCode;\n    }\n  }\n\n  private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size as libflac.\n\n  private final long nativeDecoderContext;\n\n  @Nullable private ByteBuffer byteBufferData;\n  @Nullable private ExtractorInput extractorInput;\n  @Nullable private byte[] tempBuffer;\n  private boolean endOfExtractorInput;\n\n  public FlacDecoderJni() throws FlacDecoderException {\n    if (!FlacLibrary.isAvailable()) {\n      throw new FlacDecoderException(\"Failed to load decoder native libraries.\");\n    }\n    nativeDecoderContext = flacInit();\n    if (nativeDecoderContext == 0) {\n      throw new FlacDecoderException(\"Failed to initialize decoder\");\n    }\n  }\n\n  /**\n   * Sets the data to be parsed.\n   *\n   * @param byteBufferData Source {@link ByteBuffer}.\n   */\n  public void setData(ByteBuffer byteBufferData) {\n    this.byteBufferData = byteBufferData;\n    this.extractorInput = null;\n  }\n\n  /**\n   * Sets the data to be parsed.\n   *\n   * @param extractorInput Source {@link ExtractorInput}.\n   */\n  public void setData(ExtractorInput extractorInput) {\n    this.byteBufferData = null;\n    this.extractorInput = extractorInput;\n    endOfExtractorInput = false;\n    if (tempBuffer == null) {\n      tempBuffer = new byte[TEMP_BUFFER_SIZE];\n    }\n  }\n\n  /**\n   * Returns whether the end of the data to be parsed has been reached, or true if no data was set.\n   */\n  public boolean isEndOfData() {\n    if (byteBufferData != null) {\n      return byteBufferData.remaining() == 0;\n    } else if (extractorInput != null) {\n      return endOfExtractorInput;\n    } else {\n      return true;\n    }\n  }\n\n  /** Clears the data to be parsed. */\n  public void clearData() {\n    byteBufferData = null;\n    extractorInput = null;\n  }\n\n  /**\n   * Reads up to {@code length} bytes from the data source.\n   *\n   * <p>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 {@link ByteBuffer} into which data should be written.\n   * @return Returns the number of bytes read, or -1 on failure. If all of the data has already been\n   *     read from the source, then 0 is returned.\n   */\n  @SuppressWarnings(\"unused\") // Called from native code.\n  public int read(ByteBuffer target) throws IOException, InterruptedException {\n    int byteCount = target.remaining();\n    if (byteBufferData != null) {\n      byteCount = Math.min(byteCount, byteBufferData.remaining());\n      int originalLimit = byteBufferData.limit();\n      byteBufferData.limit(byteBufferData.position() + byteCount);\n      target.put(byteBufferData);\n      byteBufferData.limit(originalLimit);\n    } else if (extractorInput != null) {\n      ExtractorInput extractorInput = this.extractorInput;\n      byte[] tempBuffer = Util.castNonNull(this.tempBuffer);\n      byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE);\n      int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount);\n      if (read < 4) {\n        // Reading less than 4 bytes, most of the time, happens because of getting the bytes left in\n        // the buffer of the input. Do another read to reduce the number of calls to this method\n        // from the native code.\n        read +=\n            readFromExtractorInput(\n                extractorInput, tempBuffer, read, /* length= */ byteCount - read);\n      }\n      byteCount = read;\n      target.put(tempBuffer, 0, byteCount);\n    } else {\n      return -1;\n    }\n    return byteCount;\n  }\n\n  /** Decodes and consumes the metadata from the FLAC stream. */\n  public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException {\n    FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext);\n    if (streamMetadata == null) {\n      throw new ParserException(\"Failed to decode stream metadata\");\n    }\n    return streamMetadata;\n  }\n\n  /**\n   * Decodes and consumes the next frame from the FLAC stream into the given byte buffer. If any IO\n   * error occurs, resets the stream and input to the given {@code retryPosition}.\n   *\n   * @param output The byte buffer to hold the decoded frame.\n   * @param retryPosition If any error happens, the input will be rewound to {@code retryPosition}.\n   */\n  public void decodeSampleWithBacktrackPosition(ByteBuffer output, long retryPosition)\n      throws InterruptedException, IOException, FlacFrameDecodeException {\n    try {\n      decodeSample(output);\n    } catch (IOException e) {\n      if (retryPosition >= 0) {\n        reset(retryPosition);\n        if (extractorInput != null) {\n          extractorInput.setRetryPosition(retryPosition, e);\n        }\n      }\n      throw e;\n    }\n  }\n\n  /** Decodes and consumes the next sample from the FLAC stream into the given byte buffer. */\n  @SuppressWarnings(\"ByteBufferBackingArray\")\n  public void decodeSample(ByteBuffer output)\n      throws IOException, InterruptedException, FlacFrameDecodeException {\n    output.clear();\n    int frameSize =\n        output.isDirect()\n            ? flacDecodeToBuffer(nativeDecoderContext, output)\n            : flacDecodeToArray(nativeDecoderContext, output.array());\n    if (frameSize < 0) {\n      if (!isDecoderAtEndOfInput()) {\n        throw new FlacFrameDecodeException(\"Cannot decode FLAC frame\", frameSize);\n      }\n      // The decoder has read to EOI. Return a 0-size frame to indicate the EOI.\n      output.limit(0);\n    } else {\n      output.limit(frameSize);\n    }\n  }\n\n  /**\n   * Returns the position of the next data to be decoded, or -1 in case of error.\n   */\n  public long getDecodePosition() {\n    return flacGetDecodePosition(nativeDecoderContext);\n  }\n\n  /** Returns the timestamp for the first sample in the last decoded frame. */\n  public long getLastFrameTimestamp() {\n    return flacGetLastFrameTimestamp(nativeDecoderContext);\n  }\n\n  /** Returns the first sample index of the last extracted frame. */\n  public long getLastFrameFirstSampleIndex() {\n    return flacGetLastFrameFirstSampleIndex(nativeDecoderContext);\n  }\n\n  /** Returns the first sample index of the frame to be extracted next. */\n  public long getNextFrameFirstSampleIndex() {\n    return flacGetNextFrameFirstSampleIndex(nativeDecoderContext);\n  }\n\n  /**\n   * Maps a seek position in microseconds to the corresponding {@link SeekMap.SeekPoints} in the\n   * stream.\n   *\n   * @param timeUs A seek position in microseconds.\n   * @return The corresponding {@link SeekMap.SeekPoints} obtained from the seek table, or {@code\n   *     null} if the stream doesn't have a seek table.\n   */\n  @Nullable\n  public SeekMap.SeekPoints getSeekPoints(long timeUs) {\n    long[] seekPoints = new long[4];\n    if (!flacGetSeekPoints(nativeDecoderContext, timeUs, seekPoints)) {\n      return null;\n    }\n    SeekPoint firstSeekPoint = new SeekPoint(seekPoints[0], seekPoints[1]);\n    SeekPoint secondSeekPoint =\n        seekPoints[2] == seekPoints[0]\n            ? firstSeekPoint\n            : new SeekPoint(seekPoints[2], seekPoints[3]);\n    return new SeekMap.SeekPoints(firstSeekPoint, secondSeekPoint);\n  }\n\n  public String getStateString() {\n    return flacGetStateString(nativeDecoderContext);\n  }\n\n  /** Returns whether the decoder has read to the end of the input. */\n  public boolean isDecoderAtEndOfInput() {\n    return flacIsDecoderAtEndOfStream(nativeDecoderContext);\n  }\n\n  public void flush() {\n    flacFlush(nativeDecoderContext);\n  }\n\n  /**\n   * Resets internal state of the decoder and sets the stream position.\n   *\n   * @param newPosition Stream's new position.\n   */\n  public void reset(long newPosition) {\n    flacReset(nativeDecoderContext, newPosition);\n  }\n\n  public void release() {\n    flacRelease(nativeDecoderContext);\n  }\n\n  private int readFromExtractorInput(\n      ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length)\n      throws IOException, InterruptedException {\n    int read = extractorInput.read(tempBuffer, offset, length);\n    if (read == C.RESULT_END_OF_INPUT) {\n      endOfExtractorInput = true;\n      read = 0;\n    }\n    return read;\n  }\n\n  private native long flacInit();\n\n  private native FlacStreamMetadata flacDecodeMetadata(long context)\n      throws IOException, InterruptedException;\n\n  private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)\n      throws IOException, InterruptedException;\n\n  private native int flacDecodeToArray(long context, byte[] outputArray)\n      throws IOException, InterruptedException;\n\n  private native long flacGetDecodePosition(long context);\n\n  private native long flacGetLastFrameTimestamp(long context);\n\n  private native long flacGetLastFrameFirstSampleIndex(long context);\n\n  private native long flacGetNextFrameFirstSampleIndex(long context);\n\n  private native boolean flacGetSeekPoints(long context, long timeUs, long[] outSeekPoints);\n\n  private native String flacGetStateString(long context);\n\n  private native boolean flacIsDecoderAtEndOfStream(long context);\n\n  private native void flacFlush(long context);\n\n  private native void flacReset(long context, long newPosition);\n\n  private native void flacRelease(long context);\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.flac;\n\nimport static com.google.android.exoplayer2.util.Util.getPcmEncoding;\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.extractor.BinarySearchSeeker.OutputFrameHolder;\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.Id3Peeker;\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.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.id3.Id3Decoder;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.FlacStreamMetadata;\nimport com.google.android.exoplayer2.util.MimeTypes;\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.nio.ByteBuffer;\nimport java.util.Arrays;\nimport org.checkerframework.checker.nullness.qual.EnsuresNonNull;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.RequiresNonNull;\n\n/**\n * Facilitates the extraction of data from the FLAC container format.\n */\npublic final class FlacExtractor implements Extractor {\n\n  /** Factory that returns one extractor which is a {@link FlacExtractor}. */\n  public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()};\n\n  /**\n   * Flags controlling the behavior of the extractor. Possible flag value is {@link\n   * #FLAG_DISABLE_ID3_METADATA}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {FLAG_DISABLE_ID3_METADATA})\n  public @interface Flags {}\n\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 = 1;\n\n  /**\n   * FLAC signature: first 4 is the signature word, second 4 is the sizeof STREAMINFO. 0x22 is the\n   * mandatory STREAMINFO.\n   */\n  private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};\n\n  private final ParsableByteArray outputBuffer;\n  private final Id3Peeker id3Peeker;\n  private final boolean id3MetadataDisabled;\n\n  @Nullable private FlacDecoderJni decoderJni;\n  private @MonotonicNonNull ExtractorOutput extractorOutput;\n  private @MonotonicNonNull TrackOutput trackOutput;\n\n  private boolean streamMetadataDecoded;\n  private @MonotonicNonNull FlacStreamMetadata streamMetadata;\n  private @MonotonicNonNull OutputFrameHolder outputFrameHolder;\n\n  @Nullable private Metadata id3Metadata;\n  @Nullable private FlacBinarySearchSeeker binarySearchSeeker;\n\n  /** Constructs an instance with flags = 0. */\n  public FlacExtractor() {\n    this(/* flags= */ 0);\n  }\n\n  /**\n   * Constructs an instance.\n   *\n   * @param flags Flags that control the extractor's behavior.\n   */\n  public FlacExtractor(int flags) {\n    outputBuffer = new ParsableByteArray();\n    id3Peeker = new Id3Peeker();\n    id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;\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    try {\n      decoderJni = new FlacDecoderJni();\n    } catch (FlacDecoderException e) {\n      throw new RuntimeException(e);\n    }\n  }\n\n  @Override\n  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {\n    if (input.getPosition() == 0) {\n      id3Metadata = peekId3Data(input);\n    }\n    return peekFlacSignature(input);\n  }\n\n  @Override\n  public int read(final ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {\n      id3Metadata = peekId3Data(input);\n    }\n\n    FlacDecoderJni decoderJni = initDecoderJni(input);\n    try {\n      decodeStreamMetadata(input);\n\n      if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {\n        return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput);\n      }\n\n      ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;\n      long lastDecodePosition = decoderJni.getDecodePosition();\n      try {\n        decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);\n      } catch (FlacDecoderJni.FlacFrameDecodeException e) {\n        throw new IOException(\"Cannot read frame at position \" + lastDecodePosition, e);\n      }\n      int outputSize = outputByteBuffer.limit();\n      if (outputSize == 0) {\n        return RESULT_END_OF_INPUT;\n      }\n\n      outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput);\n      return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;\n    } finally {\n      decoderJni.clearData();\n    }\n  }\n\n  @Override\n  public void seek(long position, long timeUs) {\n    if (position == 0) {\n      streamMetadataDecoded = false;\n    }\n    if (decoderJni != null) {\n      decoderJni.reset(position);\n    }\n    if (binarySearchSeeker != null) {\n      binarySearchSeeker.setSeekTargetUs(timeUs);\n    }\n  }\n\n  @Override\n  public void release() {\n    binarySearchSeeker = null;\n    if (decoderJni != null) {\n      decoderJni.release();\n      decoderJni = null;\n    }\n  }\n\n  /**\n   * Peeks ID3 tag data at the beginning of the input.\n   *\n   * @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input.\n   */\n  @Nullable\n  private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {\n    input.resetPeekPosition();\n    Id3Decoder.FramePredicate id3FramePredicate =\n        id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;\n    return id3Peeker.peekId3Data(input, id3FramePredicate);\n  }\n\n  @EnsuresNonNull({\"decoderJni\", \"extractorOutput\", \"trackOutput\"}) // Ensures initialized.\n  @SuppressWarnings({\"contracts.postcondition.not.satisfied\"})\n  private FlacDecoderJni initDecoderJni(ExtractorInput input) {\n    FlacDecoderJni decoderJni = Assertions.checkNotNull(this.decoderJni);\n    decoderJni.setData(input);\n    return decoderJni;\n  }\n\n  @RequiresNonNull({\"decoderJni\", \"extractorOutput\", \"trackOutput\"}) // Requires initialized.\n  @EnsuresNonNull({\"streamMetadata\", \"outputFrameHolder\"}) // Ensures stream metadata decoded.\n  @SuppressWarnings({\"contracts.postcondition.not.satisfied\"})\n  private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException {\n    if (streamMetadataDecoded) {\n      return;\n    }\n\n    FlacStreamMetadata streamMetadata;\n    try {\n      streamMetadata = decoderJni.decodeStreamMetadata();\n    } catch (IOException e) {\n      decoderJni.reset(/* newPosition= */ 0);\n      input.setRetryPosition(/* position= */ 0, e);\n      throw e;\n    }\n\n    streamMetadataDecoded = true;\n    if (this.streamMetadata == null) {\n      this.streamMetadata = streamMetadata;\n      binarySearchSeeker =\n          outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput);\n      Metadata metadata = id3MetadataDisabled ? null : id3Metadata;\n      if (streamMetadata.metadata != null) {\n        metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata);\n      }\n      outputFormat(streamMetadata, metadata, trackOutput);\n      outputBuffer.reset(streamMetadata.maxDecodedFrameSize());\n      outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));\n    }\n  }\n\n  @RequiresNonNull(\"binarySearchSeeker\")\n  private int handlePendingSeek(\n      ExtractorInput input,\n      PositionHolder seekPosition,\n      ParsableByteArray outputBuffer,\n      OutputFrameHolder outputFrameHolder,\n      TrackOutput trackOutput)\n      throws InterruptedException, IOException {\n    int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);\n    ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;\n    if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {\n      outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput);\n    }\n    return seekResult;\n  }\n\n  /**\n   * Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present.\n   *\n   * @return Whether the input begins with {@link #FLAC_SIGNATURE}.\n   */\n  private static boolean peekFlacSignature(ExtractorInput input)\n      throws IOException, InterruptedException {\n    byte[] header = new byte[FLAC_SIGNATURE.length];\n    input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length);\n    return Arrays.equals(header, FLAC_SIGNATURE);\n  }\n\n  /**\n   * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to\n   * handle seeks.\n   */\n  @Nullable\n  private static FlacBinarySearchSeeker outputSeekMap(\n      FlacDecoderJni decoderJni,\n      FlacStreamMetadata streamMetadata,\n      long streamLength,\n      ExtractorOutput output) {\n    boolean haveSeekTable = decoderJni.getSeekPoints(/* timeUs= */ 0) != null;\n    FlacBinarySearchSeeker binarySearchSeeker = null;\n    SeekMap seekMap;\n    if (haveSeekTable) {\n      seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni);\n    } else if (streamLength != C.LENGTH_UNSET) {\n      long firstFramePosition = decoderJni.getDecodePosition();\n      binarySearchSeeker =\n          new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni);\n      seekMap = binarySearchSeeker.getSeekMap();\n    } else {\n      seekMap = new SeekMap.Unseekable(streamMetadata.durationUs());\n    }\n    output.seekMap(seekMap);\n    return binarySearchSeeker;\n  }\n\n  private static void outputFormat(\n      FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {\n    Format mediaFormat =\n        Format.createAudioSampleFormat(\n            /* id= */ null,\n            MimeTypes.AUDIO_RAW,\n            /* codecs= */ null,\n            streamMetadata.bitRate(),\n            streamMetadata.maxDecodedFrameSize(),\n            streamMetadata.channels,\n            streamMetadata.sampleRate,\n            getPcmEncoding(streamMetadata.bitsPerSample),\n            /* encoderDelay= */ 0,\n            /* encoderPadding= */ 0,\n            /* initializationData= */ null,\n            /* drmInitData= */ null,\n            /* selectionFlags= */ 0,\n            /* language= */ null,\n            metadata);\n    output.format(mediaFormat);\n  }\n\n  private static void outputSample(\n      ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) {\n    sampleData.setPosition(0);\n    output.sampleData(sampleData, size);\n    output.sampleMetadata(\n        timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null);\n  }\n\n  /** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */\n  private static final class FlacSeekMap implements SeekMap {\n\n    private final long durationUs;\n    private final FlacDecoderJni decoderJni;\n\n    public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni) {\n      this.durationUs = durationUs;\n      this.decoderJni = decoderJni;\n    }\n\n    @Override\n    public boolean isSeekable() {\n      return true;\n    }\n\n    @Override\n    public SeekPoints getSeekPoints(long timeUs) {\n      @Nullable SeekPoints seekPoints = decoderJni.getSeekPoints(timeUs);\n      return seekPoints == null ? new SeekPoints(SeekPoint.START) : seekPoints;\n    }\n\n    @Override\n    public long getDurationUs() {\n      return durationUs;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.flac;\n\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.util.LibraryLoader;\n\n/**\n * Configures and queries the underlying native library.\n */\npublic final class FlacLibrary {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.flac\");\n  }\n\n  private static final LibraryLoader LOADER = new LibraryLoader(\"flacJNI\");\n\n  private FlacLibrary() {}\n\n  /**\n   * Override the names of the Flac native libraries. If an application wishes to call this method,\n   * it must do so before calling any other method defined by this class, and before instantiating\n   * any {@link LibflacAudioRenderer} and {@link FlacExtractor} instances.\n   *\n   * @param libraries The names of the Flac native libraries.\n   */\n  public static void setLibraries(String... libraries) {\n    LOADER.setLibraries(libraries);\n  }\n\n  /**\n   * Returns whether the underlying library is available, loading it if necessary.\n   */\n  public static boolean isAvailable() {\n    return LOADER.isAvailable();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.flac;\n\nimport android.os.Handler;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.audio.AudioProcessor;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener;\nimport com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.ExoMediaCrypto;\nimport com.google.android.exoplayer2.util.MimeTypes;\n\n/**\n * Decodes and renders audio using the native Flac decoder.\n */\npublic class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {\n\n  private static final int NUM_BUFFERS = 16;\n\n  public LibflacAudioRenderer() {\n    this(null, 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 LibflacAudioRenderer(\n      Handler eventHandler,\n      AudioRendererEventListener eventListener,\n      AudioProcessor... audioProcessors) {\n    super(eventHandler, eventListener, audioProcessors);\n  }\n\n  @Override\n  protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,\n      Format format) {\n    if (!FlacLibrary.isAvailable()\n        || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {\n      return FORMAT_UNSUPPORTED_TYPE;\n    } else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) {\n      return FORMAT_UNSUPPORTED_SUBTYPE;\n    } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {\n      return FORMAT_UNSUPPORTED_DRM;\n    } else {\n      return FORMAT_HANDLED;\n    }\n  }\n\n  @Override\n  protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)\n      throws FlacDecoderException {\n    return new FlacDecoder(\n        NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/jni/Android.mk",
    "content": "#\n# Copyright (C) 2016 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS 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\nWORKING_DIR := $(call my-dir)\n\n# build libflacJNI.so\ninclude $(CLEAR_VARS)\ninclude $(WORKING_DIR)/flac_sources.mk\n\nLOCAL_PATH := $(WORKING_DIR)\nLOCAL_MODULE := libflacJNI\nLOCAL_ARM_MODE := arm\nLOCAL_CPP_EXTENSION := .cc\n\nLOCAL_C_INCLUDES := \\\n    $(LOCAL_PATH)/flac/include \\\n    $(LOCAL_PATH)/flac/src/libFLAC/include\nLOCAL_SRC_FILES := $(FLAC_SOURCES)\n\nLOCAL_CFLAGS += '-DPACKAGE_VERSION=\"1.3.2\"' -DFLAC__NO_MD5 -DFLAC__INTEGER_ONLY_LIBRARY\nLOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC -DHAVE_SYS_PARAM_H\nLOCAL_CFLAGS += -O3 -funroll-loops -finline-functions -DFLAC__NO_ASM '-DFLAC__HAS_OGG=0'\n\nLOCAL_LDLIBS := -llog -lz -lm\ninclude $(BUILD_SHARED_LIBRARY)\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/jni/Application.mk",
    "content": "#\n# Copyright (C) 2016 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS 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\nAPP_OPTIM := release\nAPP_STL := gnustl_static\nAPP_CPPFLAGS := -frtti\nAPP_PLATFORM := android-14\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/jni/flac_jni.cc",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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#include <android/log.h>\n#include <jni.h>\n\n#include <array>\n#include <cstdlib>\n#include <cstring>\n\n#include \"include/flac_parser.h\"\n\n#define LOG_TAG \"flac_jni\"\n#define ALOGE(...) \\\n  ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))\n#define ALOGV(...) \\\n  ((void)__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__))\n\n#define DECODER_FUNC(RETURN_TYPE, NAME, ...)                               \\\n  extern \"C\" {                                                             \\\n  JNIEXPORT RETURN_TYPE                                                    \\\n      Java_com_google_android_exoplayer2_ext_flac_FlacDecoderJni_##NAME( \\\n          JNIEnv *env, jobject thiz, ##__VA_ARGS__);                       \\\n  }                                                                        \\\n  JNIEXPORT RETURN_TYPE                                                    \\\n      Java_com_google_android_exoplayer2_ext_flac_FlacDecoderJni_##NAME( \\\n          JNIEnv *env, jobject thiz, ##__VA_ARGS__)\n\nclass JavaDataSource : public DataSource {\n public:\n  void setFlacDecoderJni(JNIEnv *env, jobject flacDecoderJni) {\n    this->env = env;\n    this->flacDecoderJni = flacDecoderJni;\n    if (mid == NULL) {\n      jclass cls = env->GetObjectClass(flacDecoderJni);\n      mid = env->GetMethodID(cls, \"read\", \"(Ljava/nio/ByteBuffer;)I\");\n    }\n  }\n\n  ssize_t readAt(off64_t offset, void *const data, size_t size) {\n    jobject byteBuffer = env->NewDirectByteBuffer(data, size);\n    int result = env->CallIntMethod(flacDecoderJni, mid, byteBuffer);\n    if (env->ExceptionCheck()) {\n      // Exception is thrown in Java when returning from the native call.\n      result = -1;\n    }\n    return result;\n  }\n\n private:\n  JNIEnv *env;\n  jobject flacDecoderJni;\n  jmethodID mid;\n};\n\nstruct Context {\n  JavaDataSource *source;\n  FLACParser *parser;\n\n  Context() {\n    source = new JavaDataSource();\n    parser = new FLACParser(source);\n  }\n\n  ~Context() {\n    delete parser;\n    delete source;\n  }\n};\n\nDECODER_FUNC(jlong, flacInit) {\n  Context *context = new Context;\n  if (!context->parser->init()) {\n    delete context;\n    return 0;\n  }\n  return reinterpret_cast<intptr_t>(context);\n}\n\nDECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {\n  Context *context = reinterpret_cast<Context *>(jContext);\n  context->source->setFlacDecoderJni(env, thiz);\n  if (!context->parser->decodeMetadata()) {\n    return NULL;\n  }\n\n  jclass arrayListClass = env->FindClass(\"java/util/ArrayList\");\n  jmethodID arrayListConstructor =\n      env->GetMethodID(arrayListClass, \"<init>\", \"()V\");\n  jobject commentList = env->NewObject(arrayListClass, arrayListConstructor);\n  jmethodID arrayListAddMethod =\n      env->GetMethodID(arrayListClass, \"add\", \"(Ljava/lang/Object;)Z\");\n\n  if (context->parser->areVorbisCommentsValid()) {\n    std::vector<std::string> vorbisComments =\n        context->parser->getVorbisComments();\n    for (std::vector<std::string>::const_iterator vorbisComment =\n             vorbisComments.begin();\n         vorbisComment != vorbisComments.end(); ++vorbisComment) {\n      jstring commentString = env->NewStringUTF((*vorbisComment).c_str());\n      env->CallBooleanMethod(commentList, arrayListAddMethod, commentString);\n      env->DeleteLocalRef(commentString);\n    }\n  }\n\n  jobject pictureFrames = env->NewObject(arrayListClass, arrayListConstructor);\n  bool picturesValid = context->parser->arePicturesValid();\n  if (picturesValid) {\n    std::vector<FlacPicture> pictures = context->parser->getPictures();\n    jclass pictureFrameClass = env->FindClass(\n        \"com/google/android/exoplayer2/metadata/flac/PictureFrame\");\n    jmethodID pictureFrameConstructor =\n        env->GetMethodID(pictureFrameClass, \"<init>\",\n                         \"(ILjava/lang/String;Ljava/lang/String;IIII[B)V\");\n    for (std::vector<FlacPicture>::const_iterator picture = pictures.begin();\n         picture != pictures.end(); ++picture) {\n      jstring mimeType = env->NewStringUTF(picture->mimeType.c_str());\n      jstring description = env->NewStringUTF(picture->description.c_str());\n      jbyteArray pictureData = env->NewByteArray(picture->data.size());\n      env->SetByteArrayRegion(pictureData, 0, picture->data.size(),\n                              (signed char *)&picture->data[0]);\n      jobject pictureFrame = env->NewObject(\n          pictureFrameClass, pictureFrameConstructor, picture->type, mimeType,\n          description, picture->width, picture->height, picture->depth,\n          picture->colors, pictureData);\n      env->CallBooleanMethod(pictureFrames, arrayListAddMethod, pictureFrame);\n      env->DeleteLocalRef(mimeType);\n      env->DeleteLocalRef(description);\n      env->DeleteLocalRef(pictureData);\n    }\n  }\n\n  const FLAC__StreamMetadata_StreamInfo &streamInfo =\n      context->parser->getStreamInfo();\n\n  jclass flacStreamMetadataClass = env->FindClass(\n      \"com/google/android/exoplayer2/util/\"\n      \"FlacStreamMetadata\");\n  jmethodID flacStreamMetadataConstructor =\n      env->GetMethodID(flacStreamMetadataClass, \"<init>\",\n                       \"(IIIIIIIJLjava/util/List;Ljava/util/List;)V\");\n\n  return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,\n                        streamInfo.min_blocksize, streamInfo.max_blocksize,\n                        streamInfo.min_framesize, streamInfo.max_framesize,\n                        streamInfo.sample_rate, streamInfo.channels,\n                        streamInfo.bits_per_sample, streamInfo.total_samples,\n                        commentList, pictureFrames);\n}\n\nDECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {\n  Context *context = reinterpret_cast<Context *>(jContext);\n  context->source->setFlacDecoderJni(env, thiz);\n  void *outputBuffer = env->GetDirectBufferAddress(jOutputBuffer);\n  jint outputSize = env->GetDirectBufferCapacity(jOutputBuffer);\n  return context->parser->readBuffer(outputBuffer, outputSize);\n}\n\nDECODER_FUNC(jint, flacDecodeToArray, jlong jContext, jbyteArray jOutputArray) {\n  Context *context = reinterpret_cast<Context *>(jContext);\n  context->source->setFlacDecoderJni(env, thiz);\n  jbyte *outputBuffer = env->GetByteArrayElements(jOutputArray, NULL);\n  jint outputSize = env->GetArrayLength(jOutputArray);\n  int count = context->parser->readBuffer(outputBuffer, outputSize);\n  env->ReleaseByteArrayElements(jOutputArray, outputBuffer, 0);\n  return count;\n}\n\nDECODER_FUNC(jlong, flacGetDecodePosition, jlong jContext) {\n  Context *context = reinterpret_cast<Context *>(jContext);\n  return context->parser->getDecodePosition();\n}\n\nDECODER_FUNC(jlong, flacGetLastFrameTimestamp, jlong jContext) {\n  Context *context = reinterpret_cast<Context *>(jContext);\n  return context->parser->getLastFrameTimestamp();\n}\n\nDECODER_FUNC(jlong, flacGetLastFrameFirstSampleIndex, jlong jContext) {\n  Context *context = reinterpret_cast<Context *>(jContext);\n  return context->parser->getLastFrameFirstSampleIndex();\n}\n\nDECODER_FUNC(jlong, flacGetNextFrameFirstSampleIndex, jlong jContext) {\n  Context *context = reinterpret_cast<Context *>(jContext);\n  return context->parser->getNextFrameFirstSampleIndex();\n}\n\nDECODER_FUNC(jboolean, flacGetSeekPoints, jlong jContext, jlong timeUs,\n             jlongArray outSeekPoints) {\n  Context *context = reinterpret_cast<Context *>(jContext);\n  std::array<int64_t, 4> result;\n  bool success = context->parser->getSeekPositions(timeUs, result);\n  if (success) {\n    env->SetLongArrayRegion(outSeekPoints, 0, result.size(), result.data());\n  }\n  return success;\n}\n\nDECODER_FUNC(jstring, flacGetStateString, jlong jContext) {\n  Context *context = reinterpret_cast<Context *>(jContext);\n  const char *str = context->parser->getDecoderStateString();\n  return env->NewStringUTF(str);\n}\n\nDECODER_FUNC(jboolean, flacIsDecoderAtEndOfStream, jlong jContext) {\n  Context *context = reinterpret_cast<Context *>(jContext);\n  return context->parser->isDecoderAtEndOfStream();\n}\n\nDECODER_FUNC(void, flacFlush, jlong jContext) {\n  Context *context = reinterpret_cast<Context *>(jContext);\n  context->parser->flush();\n}\n\nDECODER_FUNC(void, flacReset, jlong jContext, jlong newPosition) {\n  Context *context = reinterpret_cast<Context *>(jContext);\n  context->parser->reset(newPosition);\n}\n\nDECODER_FUNC(void, flacRelease, jlong jContext) {\n  Context *context = reinterpret_cast<Context *>(jContext);\n  delete context;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/jni/flac_parser.cc",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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#include \"include/flac_parser.h\"\n\n#include <jni.h>\n\n#include <android/log.h>\n\n#include <cassert>\n#include <cstdlib>\n#include <cstring>\n\n#define LOG_TAG \"FLACParser\"\n#define ALOGE(...) \\\n  ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))\n#define ALOGV(...) \\\n  ((void)__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__))\n\n#define LOG_ALWAYS_FATAL(...) \\\n  (__android_log_assert(NULL, LOG_TAG, ##__VA_ARGS__))\n\n#define LITERAL_TO_STRING_INTERNAL(x) #x\n#define LITERAL_TO_STRING(x) LITERAL_TO_STRING_INTERNAL(x)\n\n#define TRESPASS()          \\\n  LOG_ALWAYS_FATAL(__FILE__ \\\n                   \":\" LITERAL_TO_STRING(__LINE__) \" Should not be here.\");\n#define CHECK(x) \\\n  if (!(x)) ALOGE(\"Check failed: %s \", #x)\n\nconst int endian = 1;\n#define isBigEndian() (*(reinterpret_cast<const char *>(&endian)) == 0)\n\n// The FLAC parser calls our C++ static callbacks using C calling conventions,\n// inside FLAC__stream_decoder_process_until_end_of_metadata\n// and FLAC__stream_decoder_process_single.\n// We immediately then call our corresponding C++ instance methods\n// with the same parameter list, but discard redundant information.\n\nFLAC__StreamDecoderReadStatus FLACParser::read_callback(\n    const FLAC__StreamDecoder * /* decoder */, FLAC__byte buffer[],\n    size_t *bytes, void *client_data) {\n  return reinterpret_cast<FLACParser *>(client_data)\n      ->readCallback(buffer, bytes);\n}\n\nFLAC__StreamDecoderSeekStatus FLACParser::seek_callback(\n    const FLAC__StreamDecoder * /* decoder */,\n    FLAC__uint64 absolute_byte_offset, void *client_data) {\n  return reinterpret_cast<FLACParser *>(client_data)\n      ->seekCallback(absolute_byte_offset);\n}\n\nFLAC__StreamDecoderTellStatus FLACParser::tell_callback(\n    const FLAC__StreamDecoder * /* decoder */,\n    FLAC__uint64 *absolute_byte_offset, void *client_data) {\n  return reinterpret_cast<FLACParser *>(client_data)\n      ->tellCallback(absolute_byte_offset);\n}\n\nFLAC__StreamDecoderLengthStatus FLACParser::length_callback(\n    const FLAC__StreamDecoder * /* decoder */, FLAC__uint64 *stream_length,\n    void *client_data) {\n  return reinterpret_cast<FLACParser *>(client_data)\n      ->lengthCallback(stream_length);\n}\n\nFLAC__bool FLACParser::eof_callback(const FLAC__StreamDecoder * /* decoder */,\n                                    void *client_data) {\n  return reinterpret_cast<FLACParser *>(client_data)->eofCallback();\n}\n\nFLAC__StreamDecoderWriteStatus FLACParser::write_callback(\n    const FLAC__StreamDecoder * /* decoder */, const FLAC__Frame *frame,\n    const FLAC__int32 *const buffer[], void *client_data) {\n  return reinterpret_cast<FLACParser *>(client_data)\n      ->writeCallback(frame, buffer);\n}\n\nvoid FLACParser::metadata_callback(const FLAC__StreamDecoder * /* decoder */,\n                                   const FLAC__StreamMetadata *metadata,\n                                   void *client_data) {\n  reinterpret_cast<FLACParser *>(client_data)->metadataCallback(metadata);\n}\n\nvoid FLACParser::error_callback(const FLAC__StreamDecoder * /* decoder */,\n                                FLAC__StreamDecoderErrorStatus status,\n                                void *client_data) {\n  reinterpret_cast<FLACParser *>(client_data)->errorCallback(status);\n}\n\n// These are the corresponding callbacks with C++ calling conventions\n\nFLAC__StreamDecoderReadStatus FLACParser::readCallback(FLAC__byte buffer[],\n                                                       size_t *bytes) {\n  size_t requested = *bytes;\n  ssize_t actual = mDataSource->readAt(mCurrentPos, buffer, requested);\n  if (0 > actual) {\n    *bytes = 0;\n    return FLAC__STREAM_DECODER_READ_STATUS_ABORT;\n  } else if (0 == actual) {\n    *bytes = 0;\n    mEOF = true;\n    return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM;\n  } else {\n    assert(actual <= requested);\n    *bytes = actual;\n    mCurrentPos += actual;\n    return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;\n  }\n}\n\nFLAC__StreamDecoderSeekStatus FLACParser::seekCallback(\n    FLAC__uint64 absolute_byte_offset) {\n  mCurrentPos = absolute_byte_offset;\n  mEOF = false;\n  return FLAC__STREAM_DECODER_SEEK_STATUS_OK;\n}\n\nFLAC__StreamDecoderTellStatus FLACParser::tellCallback(\n    FLAC__uint64 *absolute_byte_offset) {\n  *absolute_byte_offset = mCurrentPos;\n  return FLAC__STREAM_DECODER_TELL_STATUS_OK;\n}\n\nFLAC__StreamDecoderLengthStatus FLACParser::lengthCallback(\n    FLAC__uint64 *stream_length) {\n  return FLAC__STREAM_DECODER_LENGTH_STATUS_UNSUPPORTED;\n}\n\nFLAC__bool FLACParser::eofCallback() { return mEOF; }\n\nFLAC__StreamDecoderWriteStatus FLACParser::writeCallback(\n    const FLAC__Frame *frame, const FLAC__int32 *const buffer[]) {\n  if (mWriteRequested) {\n    mWriteRequested = false;\n    // FLAC parser doesn't free or realloc buffer until next frame or finish\n    mWriteHeader = frame->header;\n    mWriteBuffer = buffer;\n    mWriteCompleted = true;\n    return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;\n  } else {\n    ALOGE(\"FLACParser::writeCallback unexpected\");\n    return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;\n  }\n}\n\nvoid FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) {\n  switch (metadata->type) {\n    case FLAC__METADATA_TYPE_STREAMINFO:\n      if (!mStreamInfoValid) {\n        mStreamInfo = metadata->data.stream_info;\n        mStreamInfoValid = true;\n      } else {\n        ALOGE(\"FLACParser::metadataCallback unexpected STREAMINFO\");\n      }\n      break;\n    case FLAC__METADATA_TYPE_SEEKTABLE:\n      mSeekTable = &metadata->data.seek_table;\n      break;\n    case FLAC__METADATA_TYPE_VORBIS_COMMENT:\n      if (!mVorbisCommentsValid) {\n        FLAC__StreamMetadata_VorbisComment vorbisComment =\n            metadata->data.vorbis_comment;\n        for (FLAC__uint32 i = 0; i < vorbisComment.num_comments; ++i) {\n          FLAC__StreamMetadata_VorbisComment_Entry vorbisCommentEntry =\n              vorbisComment.comments[i];\n          if (vorbisCommentEntry.entry != NULL) {\n            std::string comment(\n                reinterpret_cast<char *>(vorbisCommentEntry.entry),\n                vorbisCommentEntry.length);\n            mVorbisComments.push_back(comment);\n          }\n        }\n        mVorbisCommentsValid = true;\n      } else {\n        ALOGE(\"FLACParser::metadataCallback unexpected VORBISCOMMENT\");\n      }\n      break;\n    case FLAC__METADATA_TYPE_PICTURE: {\n      const FLAC__StreamMetadata_Picture *parsedPicture =\n          &metadata->data.picture;\n      FlacPicture picture;\n      picture.mimeType.assign(std::string(parsedPicture->mime_type));\n      picture.description.assign(\n          std::string((char *)parsedPicture->description));\n      picture.data.assign(parsedPicture->data,\n                          parsedPicture->data + parsedPicture->data_length);\n      picture.width = parsedPicture->width;\n      picture.height = parsedPicture->height;\n      picture.depth = parsedPicture->depth;\n      picture.colors = parsedPicture->colors;\n      picture.type = parsedPicture->type;\n      mPictures.push_back(picture);\n      mPicturesValid = true;\n      break;\n    }\n    default:\n      ALOGE(\"FLACParser::metadataCallback unexpected type %u\", metadata->type);\n      break;\n  }\n}\n\nvoid FLACParser::errorCallback(FLAC__StreamDecoderErrorStatus status) {\n  ALOGE(\"FLACParser::errorCallback status=%d\", status);\n  mErrorStatus = status;\n}\n\n// Copy samples from FLAC native 32-bit non-interleaved to\n// correct bit-depth (non-zero padded), interleaved.\n// These are candidates for optimization if needed.\nstatic void copyToByteArrayBigEndian(int8_t *dst, const int *const *src,\n                                     unsigned bytesPerSample, unsigned nSamples,\n                                     unsigned nChannels) {\n  for (unsigned i = 0; i < nSamples; ++i) {\n    for (unsigned c = 0; c < nChannels; ++c) {\n      // point to the first byte of the source address\n      // and then skip the first few bytes (most significant bytes)\n      // depending on the bit depth\n      const int8_t *byteSrc =\n          reinterpret_cast<const int8_t *>(&src[c][i]) + 4 - bytesPerSample;\n      memcpy(dst, byteSrc, bytesPerSample);\n      dst = dst + bytesPerSample;\n    }\n  }\n}\n\nstatic void copyToByteArrayLittleEndian(int8_t *dst, const int *const *src,\n                                        unsigned bytesPerSample,\n                                        unsigned nSamples, unsigned nChannels) {\n  for (unsigned i = 0; i < nSamples; ++i) {\n    for (unsigned c = 0; c < nChannels; ++c) {\n      // with little endian, the most significant bytes will be at the end\n      // copy the bytes in little endian will remove the most significant byte\n      // so we are good here.\n      memcpy(dst, &(src[c][i]), bytesPerSample);\n      dst = dst + bytesPerSample;\n    }\n  }\n}\n\nstatic void copyTrespass(int8_t * /* dst */, const int *const * /* src */,\n                         unsigned /* bytesPerSample */, unsigned /* nSamples */,\n                         unsigned /* nChannels */) {\n  TRESPASS();\n}\n\n// FLACParser\n\nFLACParser::FLACParser(DataSource *source)\n    : mDataSource(source),\n      mCopy(copyTrespass),\n      mDecoder(NULL),\n      mSeekTable(NULL),\n      firstFrameOffset(0LL),\n      mCurrentPos(0LL),\n      mEOF(false),\n      mStreamInfoValid(false),\n      mVorbisCommentsValid(false),\n      mPicturesValid(false),\n      mWriteRequested(false),\n      mWriteCompleted(false),\n      mWriteBuffer(NULL),\n      mErrorStatus((FLAC__StreamDecoderErrorStatus)-1) {\n  ALOGV(\"FLACParser::FLACParser\");\n  memset(&mStreamInfo, 0, sizeof(mStreamInfo));\n  memset(&mWriteHeader, 0, sizeof(mWriteHeader));\n}\n\nFLACParser::~FLACParser() {\n  ALOGV(\"FLACParser::~FLACParser\");\n  if (mDecoder != NULL) {\n    FLAC__stream_decoder_delete(mDecoder);\n    mDecoder = NULL;\n  }\n}\n\nbool FLACParser::init() {\n  // setup libFLAC parser\n  mDecoder = FLAC__stream_decoder_new();\n  if (mDecoder == NULL) {\n    // The new should succeed, since probably all it does is a malloc\n    // that always succeeds in Android.  But to avoid dependence on the\n    // libFLAC internals, we check and log here.\n    ALOGE(\"new failed\");\n    return false;\n  }\n  FLAC__stream_decoder_set_md5_checking(mDecoder, false);\n  FLAC__stream_decoder_set_metadata_ignore_all(mDecoder);\n  FLAC__stream_decoder_set_metadata_respond(mDecoder,\n                                            FLAC__METADATA_TYPE_STREAMINFO);\n  FLAC__stream_decoder_set_metadata_respond(mDecoder,\n                                            FLAC__METADATA_TYPE_SEEKTABLE);\n  FLAC__stream_decoder_set_metadata_respond(mDecoder,\n                                            FLAC__METADATA_TYPE_VORBIS_COMMENT);\n  FLAC__stream_decoder_set_metadata_respond(mDecoder,\n                                            FLAC__METADATA_TYPE_PICTURE);\n  FLAC__StreamDecoderInitStatus initStatus;\n  initStatus = FLAC__stream_decoder_init_stream(\n      mDecoder, read_callback, seek_callback, tell_callback, length_callback,\n      eof_callback, write_callback, metadata_callback, error_callback,\n      reinterpret_cast<void *>(this));\n  if (initStatus != FLAC__STREAM_DECODER_INIT_STATUS_OK) {\n    // A failure here probably indicates a programming error and so is\n    // unlikely to happen. But we check and log here similarly to above.\n    ALOGE(\"init_stream failed %d\", initStatus);\n    return false;\n  }\n  return true;\n}\n\nbool FLACParser::decodeMetadata() {\n  // parse all metadata\n  if (!FLAC__stream_decoder_process_until_end_of_metadata(mDecoder)) {\n    ALOGE(\"metadata decoding failed\");\n    return false;\n  }\n  // store first frame offset\n  FLAC__stream_decoder_get_decode_position(mDecoder, &firstFrameOffset);\n\n  if (mStreamInfoValid) {\n    // check channel count\n    if (getChannels() == 0 || getChannels() > 8) {\n      ALOGE(\"unsupported channel count %u\", getChannels());\n      return false;\n    }\n    // check bit depth\n    switch (getBitsPerSample()) {\n      case 8:\n      case 16:\n      case 24:\n      case 32:\n        break;\n      default:\n        ALOGE(\"unsupported bits per sample %u\", getBitsPerSample());\n        return false;\n    }\n    // check sample rate\n    switch (getSampleRate()) {\n      case 8000:\n      case 11025:\n      case 12000:\n      case 16000:\n      case 22050:\n      case 24000:\n      case 32000:\n      case 44100:\n      case 48000:\n      case 88200:\n      case 96000:\n      case 176400:\n      case 192000:\n        break;\n      default:\n        ALOGE(\"unsupported sample rate %u\", getSampleRate());\n        return false;\n    }\n    // configure the appropriate copy function based on device endianness.\n    if (isBigEndian()) {\n      mCopy = copyToByteArrayBigEndian;\n    } else {\n      mCopy = copyToByteArrayLittleEndian;\n    }\n  } else {\n    ALOGE(\"missing STREAMINFO\");\n    return false;\n  }\n  return true;\n}\n\nsize_t FLACParser::readBuffer(void *output, size_t output_size) {\n  mWriteRequested = true;\n  mWriteCompleted = false;\n\n  if (!FLAC__stream_decoder_process_single(mDecoder)) {\n    ALOGE(\"FLACParser::readBuffer process_single failed. Status: %s\",\n          getDecoderStateString());\n    return -1;\n  }\n  if (!mWriteCompleted) {\n    if (FLAC__stream_decoder_get_state(mDecoder) !=\n        FLAC__STREAM_DECODER_END_OF_STREAM) {\n      ALOGE(\"FLACParser::readBuffer write did not complete. Status: %s\",\n            getDecoderStateString());\n    }\n    return -1;\n  }\n\n  // verify that block header keeps the promises made by STREAMINFO\n  unsigned blocksize = mWriteHeader.blocksize;\n  if (blocksize == 0 || blocksize > getMaxBlockSize()) {\n    ALOGE(\"FLACParser::readBuffer write invalid blocksize %u\", blocksize);\n    return -1;\n  }\n  if (mWriteHeader.sample_rate != getSampleRate() ||\n      mWriteHeader.channels != getChannels() ||\n      mWriteHeader.bits_per_sample != getBitsPerSample()) {\n    ALOGE(\n        \"FLACParser::readBuffer write changed parameters mid-stream: %d/%d/%d \"\n        \"-> %d/%d/%d\",\n        getSampleRate(), getChannels(), getBitsPerSample(),\n        mWriteHeader.sample_rate, mWriteHeader.channels,\n        mWriteHeader.bits_per_sample);\n    return -1;\n  }\n\n  unsigned bytesPerSample = getBitsPerSample() >> 3;\n  size_t bufferSize = blocksize * getChannels() * bytesPerSample;\n  if (bufferSize > output_size) {\n    ALOGE(\n        \"FLACParser::readBuffer not enough space in output buffer \"\n        \"%zu < %zu\",\n        output_size, bufferSize);\n    return -1;\n  }\n\n  // copy PCM from FLAC write buffer to our media buffer, with interleaving.\n  (*mCopy)(reinterpret_cast<int8_t *>(output), mWriteBuffer, bytesPerSample,\n           blocksize, getChannels());\n\n  // fill in buffer metadata\n  CHECK(mWriteHeader.number_type == FLAC__FRAME_NUMBER_TYPE_SAMPLE_NUMBER);\n\n  return bufferSize;\n}\n\nbool FLACParser::getSeekPositions(int64_t timeUs,\n                                  std::array<int64_t, 4> &result) {\n  if (!mSeekTable) {\n    return false;\n  }\n\n  unsigned sampleRate = getSampleRate();\n  int64_t totalSamples = getTotalSamples();\n  int64_t targetSampleNumber = (timeUs * sampleRate) / 1000000LL;\n  if (targetSampleNumber >= totalSamples) {\n    targetSampleNumber = totalSamples - 1;\n  }\n\n  FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points;\n  unsigned length = mSeekTable->num_points;\n\n  for (unsigned i = length; i != 0; i--) {\n    int64_t sampleNumber = points[i - 1].sample_number;\n    if (sampleNumber <= targetSampleNumber) {\n      result[0] = (sampleNumber * 1000000LL) / sampleRate;\n      result[1] = firstFrameOffset + points[i - 1].stream_offset;\n      if (sampleNumber == targetSampleNumber || i >= length) {\n        // exact seek, or no following seek point.\n        result[2] = result[0];\n        result[3] = result[1];\n      } else {\n        result[2] = (points[i].sample_number * 1000000LL) / sampleRate;\n        result[3] = firstFrameOffset + points[i].stream_offset;\n      }\n      return true;\n    }\n  }\n  result[0] = 0;\n  result[1] = firstFrameOffset;\n  result[2] = 0;\n  result[3] = firstFrameOffset;\n  return true;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/jni/flac_sources.mk",
    "content": "#\n# Copyright (C) 2016 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS 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\nFLAC_SOURCES = \\\n  flac_jni.cc                                    \\\n  flac_parser.cc                                 \\\n  flac/src/libFLAC/bitmath.c                     \\\n  flac/src/libFLAC/bitreader.c                   \\\n  flac/src/libFLAC/bitwriter.c                   \\\n  flac/src/libFLAC/cpu.c                         \\\n  flac/src/libFLAC/crc.c                         \\\n  flac/src/libFLAC/fixed.c                       \\\n  flac/src/libFLAC/fixed_intrin_sse2.c           \\\n  flac/src/libFLAC/fixed_intrin_ssse3.c          \\\n  flac/src/libFLAC/float.c                       \\\n  flac/src/libFLAC/format.c                      \\\n  flac/src/libFLAC/lpc.c                         \\\n  flac/src/libFLAC/lpc_intrin_avx2.c             \\\n  flac/src/libFLAC/lpc_intrin_sse2.c             \\\n  flac/src/libFLAC/lpc_intrin_sse41.c            \\\n  flac/src/libFLAC/lpc_intrin_sse.c              \\\n  flac/src/libFLAC/md5.c                         \\\n  flac/src/libFLAC/memory.c                      \\\n  flac/src/libFLAC/metadata_iterators.c          \\\n  flac/src/libFLAC/metadata_object.c             \\\n  flac/src/libFLAC/stream_decoder.c              \\\n  flac/src/libFLAC/stream_encoder.c              \\\n  flac/src/libFLAC/stream_encoder_framing.c      \\\n  flac/src/libFLAC/stream_encoder_intrin_avx2.c  \\\n  flac/src/libFLAC/stream_encoder_intrin_sse2.c  \\\n  flac/src/libFLAC/stream_encoder_intrin_ssse3.c \\\n  flac/src/libFLAC/window.c\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/jni/include/data_source.h",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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#ifndef INCLUDE_DATA_SOURCE_H_\n#define INCLUDE_DATA_SOURCE_H_\n\n#include <jni.h>\n#include <sys/types.h>\n\nclass DataSource {\n public:\n  virtual ~DataSource() {}\n  // Returns the number of bytes read, or -1 on failure. It's not an error if\n  // this returns zero; it just means the given offset is equal to, or\n  // beyond, the end of the source.\n  virtual ssize_t readAt(off64_t offset, void* const data, size_t size) = 0;\n};\n\n#endif  // INCLUDE_DATA_SOURCE_H_\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/main/jni/include/flac_parser.h",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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#ifndef FLAC_PARSER_H_\n#define FLAC_PARSER_H_\n\n#include <stdint.h>\n\n#include <array>\n#include <cstdlib>\n#include <string>\n#include <vector>\n\n// libFLAC parser\n#include \"FLAC/stream_decoder.h\"\n\n#include \"include/data_source.h\"\n\ntypedef int status_t;\n\nstruct FlacPicture {\n  int type;\n  std::string mimeType;\n  std::string description;\n  FLAC__uint32 width;\n  FLAC__uint32 height;\n  FLAC__uint32 depth;\n  FLAC__uint32 colors;\n  std::vector<char> data;\n};\n\nclass FLACParser {\n public:\n  FLACParser(DataSource *source);\n  ~FLACParser();\n\n  bool init();\n\n  // stream properties\n  unsigned getMaxBlockSize() const { return mStreamInfo.max_blocksize; }\n  unsigned getSampleRate() const { return mStreamInfo.sample_rate; }\n  unsigned getChannels() const { return mStreamInfo.channels; }\n  unsigned getBitsPerSample() const { return mStreamInfo.bits_per_sample; }\n  FLAC__uint64 getTotalSamples() const { return mStreamInfo.total_samples; }\n\n  const FLAC__StreamMetadata_StreamInfo& getStreamInfo() const {\n    return mStreamInfo;\n  }\n\n  bool areVorbisCommentsValid() const { return mVorbisCommentsValid; }\n\n  std::vector<std::string> getVorbisComments() { return mVorbisComments; }\n\n  bool arePicturesValid() const { return mPicturesValid; }\n\n  const std::vector<FlacPicture> &getPictures() const { return mPictures; }\n\n  int64_t getLastFrameTimestamp() const {\n    return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();\n  }\n\n  int64_t getLastFrameFirstSampleIndex() const {\n    return mWriteHeader.number.sample_number;\n  }\n\n  int64_t getNextFrameFirstSampleIndex() const {\n    return mWriteHeader.number.sample_number + mWriteHeader.blocksize;\n  }\n\n  bool decodeMetadata();\n  size_t readBuffer(void *output, size_t output_size);\n\n  bool getSeekPositions(int64_t timeUs, std::array<int64_t, 4> &result);\n\n  void flush() {\n    reset(mCurrentPos);\n  }\n\n  void reset(int64_t newPosition) {\n    if (mDecoder != NULL) {\n      mCurrentPos = newPosition;\n      mEOF = false;\n      if (newPosition == 0) {\n        mStreamInfoValid = false;\n        mVorbisCommentsValid = false;\n        mPicturesValid = false;\n        mVorbisComments.clear();\n        mPictures.clear();\n        FLAC__stream_decoder_reset(mDecoder);\n      } else {\n        FLAC__stream_decoder_flush(mDecoder);\n      }\n    }\n  }\n\n  int64_t getDecodePosition() {\n    uint64_t position;\n    if (mDecoder != NULL\n        && FLAC__stream_decoder_get_decode_position(mDecoder, &position)) {\n      return position;\n    }\n    return -1;\n  }\n\n  const char *getDecoderStateString() {\n    return FLAC__stream_decoder_get_resolved_state_string(mDecoder);\n  }\n\n  bool isDecoderAtEndOfStream() const {\n    return FLAC__stream_decoder_get_state(mDecoder) ==\n           FLAC__STREAM_DECODER_END_OF_STREAM;\n  }\n\n private:\n  DataSource *mDataSource;\n\n  void (*mCopy)(int8_t *dst, const int *const *src, unsigned bytesPerSample,\n                unsigned nSamples, unsigned nChannels);\n\n  // handle to underlying libFLAC parser\n  FLAC__StreamDecoder *mDecoder;\n\n  // current position within the data source\n  off64_t mCurrentPos;\n  bool mEOF;\n\n  // cached when the STREAMINFO metadata is parsed by libFLAC\n  FLAC__StreamMetadata_StreamInfo mStreamInfo;\n  bool mStreamInfoValid;\n\n  const FLAC__StreamMetadata_SeekTable *mSeekTable;\n  uint64_t firstFrameOffset;\n\n  // cached when the VORBIS_COMMENT metadata is parsed by libFLAC\n  std::vector<std::string> mVorbisComments;\n  bool mVorbisCommentsValid;\n\n  // cached when the PICTURE metadata is parsed by libFLAC\n  std::vector<FlacPicture> mPictures;\n  bool mPicturesValid;\n\n  // cached when a decoded PCM block is \"written\" by libFLAC parser\n  bool mWriteRequested;\n  bool mWriteCompleted;\n  FLAC__FrameHeader mWriteHeader;\n  const FLAC__int32 *const *mWriteBuffer;\n\n  // most recent error reported by libFLAC parser\n  FLAC__StreamDecoderErrorStatus mErrorStatus;\n\n  // no copy constructor or assignment\n  FLACParser(const FLACParser &);\n  FLACParser &operator=(const FLACParser &);\n\n  // FLAC parser callbacks as C++ instance methods\n  FLAC__StreamDecoderReadStatus readCallback(FLAC__byte buffer[],\n                                             size_t *bytes);\n  FLAC__StreamDecoderSeekStatus seekCallback(FLAC__uint64 absolute_byte_offset);\n  FLAC__StreamDecoderTellStatus tellCallback(\n      FLAC__uint64 *absolute_byte_offset);\n  FLAC__StreamDecoderLengthStatus lengthCallback(FLAC__uint64 *stream_length);\n  FLAC__bool eofCallback();\n  FLAC__StreamDecoderWriteStatus writeCallback(\n      const FLAC__Frame *frame, const FLAC__int32 *const buffer[]);\n  void metadataCallback(const FLAC__StreamMetadata *metadata);\n  void errorCallback(FLAC__StreamDecoderErrorStatus status);\n\n  // FLAC parser callbacks as C-callable functions\n  static FLAC__StreamDecoderReadStatus read_callback(\n      const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes,\n      void *client_data);\n  static FLAC__StreamDecoderSeekStatus seek_callback(\n      const FLAC__StreamDecoder *decoder, FLAC__uint64 absolute_byte_offset,\n      void *client_data);\n  static FLAC__StreamDecoderTellStatus tell_callback(\n      const FLAC__StreamDecoder *decoder, FLAC__uint64 *absolute_byte_offset,\n      void *client_data);\n  static FLAC__StreamDecoderLengthStatus length_callback(\n      const FLAC__StreamDecoder *decoder, FLAC__uint64 *stream_length,\n      void *client_data);\n  static FLAC__bool eof_callback(const FLAC__StreamDecoder *decoder,\n                                 void *client_data);\n  static FLAC__StreamDecoderWriteStatus write_callback(\n      const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame,\n      const FLAC__int32 *const buffer[], void *client_data);\n  static void metadata_callback(const FLAC__StreamDecoder *decoder,\n                                const FLAC__StreamMetadata *metadata,\n                                void *client_data);\n  static void error_callback(const FLAC__StreamDecoder *decoder,\n                             FLAC__StreamDecoderErrorStatus status,\n                             void *client_data);\n};\n\n#endif  // FLAC_PARSER_H_\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest package=\"com.google.android.exoplayer2.ext.flac\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.flac;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.Extractor;\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.PsExtractor;\nimport com.google.android.exoplayer2.extractor.ts.TsExtractor;\nimport com.google.android.exoplayer2.extractor.wav.WavExtractor;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link DefaultExtractorsFactory}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DefaultExtractorsFactoryTest {\n\n  @Test\n  public void testCreateExtractors_returnExpectedClasses() {\n    DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();\n\n    Extractor[] extractors = defaultExtractorsFactory.createExtractors();\n    List<Class<?>> listCreatedExtractorClasses = new ArrayList<>();\n    for (Extractor extractor : extractors) {\n      listCreatedExtractorClasses.add(extractor.getClass());\n    }\n\n    Class<?>[] expectedExtractorClassses =\n        new Class<?>[] {\n          MatroskaExtractor.class,\n          FragmentedMp4Extractor.class,\n          Mp4Extractor.class,\n          Mp3Extractor.class,\n          AdtsExtractor.class,\n          Ac3Extractor.class,\n          Ac4Extractor.class,\n          TsExtractor.class,\n          FlvExtractor.class,\n          OggExtractor.class,\n          PsExtractor.class,\n          WavExtractor.class,\n          AmrExtractor.class,\n          FlacExtractor.class\n        };\n\n    assertThat(listCreatedExtractorClasses).containsNoDuplicates();\n    assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultRenderersFactoryTest.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.ext.flac;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link DefaultRenderersFactoryTest} with {@link LibflacAudioRenderer}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DefaultRenderersFactoryTest {\n\n  @Test\n  public void createRenderers_instantiatesVpxRenderer() {\n    DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(\n        LibflacAudioRenderer.class, C.TRACK_TYPE_AUDIO);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/gvr/README.md",
    "content": "# ExoPlayer GVR extension #\n\nThe GVR extension wraps the [Google VR SDK for Android][]. It provides a\nGvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering\nof surround sound and ambisonic soundfields.\n\n[Google VR SDK for Android]: https://developers.google.com/vr/android/\n[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround\n\n## Getting the extension ##\n\nThe easiest way to use the extension is to add it as a gradle dependency:\n\n```gradle\nimplementation 'com.google.android.exoplayer:extension-gvr:2.X.X'\n```\n\nwhere `2.X.X` is the version, which must match the version of the ExoPlayer\nlibrary being used.\n\nAlternatively, you can clone the ExoPlayer repository and depend on the module\nlocally. Instructions for doing this can be found in ExoPlayer's\n[top level README][].\n\n## Using the extension ##\n\n* If using `DefaultRenderersFactory`, override\n  `DefaultRenderersFactory.buildAudioProcessors` to return a\n  `GvrAudioProcessor`.\n* If constructing renderers directly, pass a `GvrAudioProcessor` to\n  `MediaCodecAudioRenderer`'s constructor.\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n\n## Links ##\n\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.gvr.*`\n  belong to this module.\n\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/gvr/build.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.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion 19\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    implementation project(modulePrefix + 'library-ui')\n    implementation 'androidx.annotation:annotation:1.1.0'\n    api 'com.google.vr:sdk-base:1.190.0'\n    compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion\n}\n\next {\n    javadocTitle = 'GVR extension'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'extension-gvr'\n    releaseDescription = 'Google VR extension for ExoPlayer.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/gvr/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n<manifest package=\"com.google.android.exoplayer2.ext.gvr\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.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.ext.gvr;\n\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.audio.AudioProcessor;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.vr.sdk.audio.GvrAudioSurround;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\n\n/**\n * An {@link AudioProcessor} that uses {@code GvrAudioSurround} to provide binaural rendering of\n * surround sound and ambisonic soundfields.\n */\npublic final class GvrAudioProcessor implements AudioProcessor {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.gvr\");\n  }\n\n  private static final int FRAMES_PER_OUTPUT_BUFFER = 1024;\n  private static final int OUTPUT_CHANNEL_COUNT = 2;\n  private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output.\n  private static final int NO_SURROUND_FORMAT = GvrAudioSurround.SurroundFormat.INVALID;\n\n  private int sampleRateHz;\n  private int channelCount;\n  private int pendingGvrAudioSurroundFormat;\n  @Nullable private GvrAudioSurround gvrAudioSurround;\n  private ByteBuffer buffer;\n  private boolean inputEnded;\n\n  private float w;\n  private float x;\n  private float y;\n  private float z;\n\n  /** Creates a new GVR audio processor. */\n  public GvrAudioProcessor() {\n    // Use the identity for the initial orientation.\n    w = 1f;\n    sampleRateHz = Format.NO_VALUE;\n    channelCount = Format.NO_VALUE;\n    buffer = EMPTY_BUFFER;\n    pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;\n  }\n\n  /**\n   * Updates the listener head orientation. May be called on any thread. See\n   * {@code GvrAudioSurround.updateNativeOrientation}.\n   *\n   * @param w The w component of the quaternion.\n   * @param x The x component of the quaternion.\n   * @param y The y component of the quaternion.\n   * @param z The z component of the quaternion.\n   */\n  public synchronized void updateOrientation(float w, float x, float y, float z) {\n    this.w = w;\n    this.x = x;\n    this.y = y;\n    this.z = z;\n    if (gvrAudioSurround != null) {\n      gvrAudioSurround.updateNativeOrientation(w, x, y, z);\n    }\n  }\n\n  @SuppressWarnings(\"ReferenceEquality\")\n  @Override\n  public synchronized boolean configure(\n      int sampleRateHz, int channelCount, @C.Encoding int encoding)\n      throws UnhandledFormatException {\n    if (encoding != C.ENCODING_PCM_16BIT) {\n      maybeReleaseGvrAudioSurround();\n      throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);\n    }\n    if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) {\n      return false;\n    }\n    this.sampleRateHz = sampleRateHz;\n    this.channelCount = channelCount;\n    switch (channelCount) {\n      case 1:\n        pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;\n        break;\n      case 2:\n        pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;\n        break;\n      case 4:\n        pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS;\n        break;\n      case 6:\n        pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE;\n        break;\n      case 9:\n        pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS;\n        break;\n      case 16:\n        pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;\n        break;\n      default:\n        throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);\n    }\n    if (buffer == EMPTY_BUFFER) {\n      buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)\n          .order(ByteOrder.nativeOrder());\n    }\n    return true;\n  }\n\n  @Override\n  public boolean isActive() {\n    return pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT || gvrAudioSurround != null;\n  }\n\n  @Override\n  public int getOutputChannelCount() {\n    return OUTPUT_CHANNEL_COUNT;\n  }\n\n  @Override\n  public int getOutputEncoding() {\n    return C.ENCODING_PCM_16BIT;\n  }\n\n  @Override\n  public int getOutputSampleRateHz() {\n    return sampleRateHz;\n  }\n\n  @Override\n  public void queueInput(ByteBuffer input) {\n    int position = input.position();\n    Assertions.checkNotNull(gvrAudioSurround);\n    int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position);\n    input.position(position + readBytes);\n  }\n\n  @Override\n  public void queueEndOfStream() {\n    if (gvrAudioSurround != null) {\n      gvrAudioSurround.triggerProcessing();\n    }\n    inputEnded = true;\n  }\n\n  @Override\n  public ByteBuffer getOutput() {\n    if (gvrAudioSurround == null) {\n      return EMPTY_BUFFER;\n    }\n    int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());\n    buffer.position(0).limit(writtenBytes);\n    return buffer;\n  }\n\n  @Override\n  public boolean isEnded() {\n    return inputEnded\n        && (gvrAudioSurround == null || gvrAudioSurround.getAvailableOutputSize() == 0);\n  }\n\n  @Override\n  public void flush() {\n    if (pendingGvrAudioSurroundFormat != NO_SURROUND_FORMAT) {\n      maybeReleaseGvrAudioSurround();\n      gvrAudioSurround =\n          new GvrAudioSurround(\n              pendingGvrAudioSurroundFormat, sampleRateHz, channelCount, FRAMES_PER_OUTPUT_BUFFER);\n      gvrAudioSurround.updateNativeOrientation(w, x, y, z);\n      pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;\n    } else if (gvrAudioSurround != null) {\n      gvrAudioSurround.flush();\n    }\n    inputEnded = false;\n  }\n\n  @Override\n  public synchronized void reset() {\n    maybeReleaseGvrAudioSurround();\n    updateOrientation(/* w= */ 1f, /* x= */ 0f, /* y= */ 0f, /* z= */ 0f);\n    inputEnded = false;\n    sampleRateHz = Format.NO_VALUE;\n    channelCount = Format.NO_VALUE;\n    buffer = EMPTY_BUFFER;\n    pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;\n  }\n\n  private void maybeReleaseGvrAudioSurround() {\n    if (gvrAudioSurround != null) {\n      gvrAudioSurround.release();\n      gvrAudioSurround = null;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.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.ext.gvr;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.SurfaceTexture;\nimport android.opengl.Matrix;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Looper;\nimport androidx.annotation.BinderThread;\nimport androidx.annotation.CallSuper;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.UiThread;\nimport android.view.ContextThemeWrapper;\nimport android.view.MotionEvent;\nimport android.view.Surface;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.ui.PlayerControlView;\nimport com.google.android.exoplayer2.ui.spherical.GlViewGroup;\nimport com.google.android.exoplayer2.ui.spherical.PointerRenderer;\nimport com.google.android.exoplayer2.ui.spherical.SceneRenderer;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.vr.ndk.base.DaydreamApi;\nimport com.google.vr.sdk.base.AndroidCompat;\nimport com.google.vr.sdk.base.Eye;\nimport com.google.vr.sdk.base.GvrActivity;\nimport com.google.vr.sdk.base.GvrView;\nimport com.google.vr.sdk.base.HeadTransform;\nimport com.google.vr.sdk.base.Viewport;\nimport com.google.vr.sdk.controller.Controller;\nimport com.google.vr.sdk.controller.ControllerManager;\nimport javax.microedition.khronos.egl.EGLConfig;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/** Base activity for VR 360 video playback. */\npublic abstract class GvrPlayerActivity extends GvrActivity {\n\n  private static final int EXIT_FROM_VR_REQUEST_CODE = 42;\n\n  private final Handler mainHandler;\n\n  @Nullable private Player player;\n  @MonotonicNonNull private GlViewGroup glView;\n  @MonotonicNonNull private ControllerManager controllerManager;\n  @MonotonicNonNull private SurfaceTexture surfaceTexture;\n  @MonotonicNonNull private Surface surface;\n  @MonotonicNonNull private SceneRenderer scene;\n  @MonotonicNonNull private PlayerControlView playerControl;\n\n  public GvrPlayerActivity() {\n    mainHandler = new Handler(Looper.getMainLooper());\n  }\n\n  @Override\n  protected void onCreate(@Nullable Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    setScreenAlwaysOn(true);\n\n    GvrView gvrView = new GvrView(this);\n    // Since videos typically have fewer pixels per degree than the phones, reducing the render\n    // target scaling factor reduces the work required to render the scene.\n    gvrView.setRenderTargetScale(.5f);\n\n    // If a custom theme isn't specified, the Context's theme is used. For VR Activities, this is\n    // the old Android default theme rather than a modern theme. Override this with a custom theme.\n    Context theme = new ContextThemeWrapper(this, R.style.VrTheme);\n    glView = new GlViewGroup(theme, R.layout.vr_ui);\n\n    playerControl = Assertions.checkNotNull(glView.findViewById(R.id.controller));\n    playerControl.setShowVrButton(true);\n    playerControl.setVrButtonListener(v -> exit());\n\n    PointerRenderer pointerRenderer = new PointerRenderer();\n    scene = new SceneRenderer();\n    Renderer renderer = new Renderer(scene, glView, pointerRenderer);\n\n    // Attach glView to gvrView in order to properly handle UI events.\n    gvrView.addView(glView, 0);\n\n    // Standard GvrView configuration\n    gvrView.setEGLConfigChooser(\n        8, 8, 8, 8, // RGBA bits.\n        16, // Depth bits.\n        0); // Stencil bits.\n    gvrView.setRenderer(renderer);\n    setContentView(gvrView);\n\n    // Most Daydream phones can render a 4k video at 60fps in sustained performance mode. These\n    // options can be tweaked along with the render target scale.\n    if (gvrView.setAsyncReprojectionEnabled(true)) {\n      AndroidCompat.setSustainedPerformanceMode(this, true);\n    }\n\n    // Handle the user clicking on the 'X' in the top left corner. Since this is done when the user\n    // has taken the headset out of VR, it should launch the app's exit flow directly rather than\n    // using the transition flow.\n    gvrView.setOnCloseButtonListener(this::finish);\n\n    ControllerManager.EventListener listener =\n        new ControllerManager.EventListener() {\n          @Override\n          public void onApiStatusChanged(int status) {\n            // Do nothing.\n          }\n\n          @Override\n          public void onRecentered() {\n            // TODO if in cardboard mode call gvrView.recenterHeadTracker();\n            glView.post(() -> Util.castNonNull(playerControl).show());\n          }\n        };\n    controllerManager = new ControllerManager(this, listener);\n\n    Controller controller = controllerManager.getController();\n    ControllerEventListener controllerEventListener =\n        new ControllerEventListener(controller, pointerRenderer, glView);\n    controller.setEventListener(controllerEventListener);\n  }\n\n  /**\n   * Sets the {@link Player} to use.\n   *\n   * @param newPlayer The {@link Player} to use, or {@code null} to detach the current player.\n   */\n  protected void setPlayer(@Nullable Player newPlayer) {\n    Assertions.checkNotNull(scene);\n    if (player == newPlayer) {\n      return;\n    }\n    if (player != null) {\n      Player.VideoComponent videoComponent = player.getVideoComponent();\n      if (videoComponent != null) {\n        if (surface != null) {\n          videoComponent.clearVideoSurface(surface);\n        }\n        videoComponent.clearVideoFrameMetadataListener(scene);\n        videoComponent.clearCameraMotionListener(scene);\n      }\n    }\n    player = newPlayer;\n    if (player != null) {\n      Player.VideoComponent videoComponent = player.getVideoComponent();\n      if (videoComponent != null) {\n        videoComponent.setVideoFrameMetadataListener(scene);\n        videoComponent.setCameraMotionListener(scene);\n        videoComponent.setVideoSurface(surface);\n      }\n    }\n    Assertions.checkNotNull(playerControl).setPlayer(player);\n  }\n\n  /**\n   * Sets the default stereo mode. If the played video doesn't contain a stereo mode the default one\n   * is used.\n   *\n   * @param stereoMode A {@link C.StereoMode} value.\n   */\n  protected void setDefaultStereoMode(@C.StereoMode int stereoMode) {\n    Assertions.checkNotNull(scene).setDefaultStereoMode(stereoMode);\n  }\n\n  @CallSuper\n  @Override\n  protected void onActivityResult(int requestCode, int resultCode, Intent unused) {\n    if (requestCode == EXIT_FROM_VR_REQUEST_CODE && resultCode == RESULT_OK) {\n      finish();\n    }\n  }\n\n  @Override\n  protected void onResume() {\n    super.onResume();\n    Util.castNonNull(controllerManager).start();\n  }\n\n  @Override\n  protected void onPause() {\n    Util.castNonNull(controllerManager).stop();\n    super.onPause();\n  }\n\n  @Override\n  protected void onDestroy() {\n    setPlayer(null);\n    releaseSurface(surfaceTexture, surface);\n    super.onDestroy();\n  }\n\n  /** Tries to exit gracefully from VR using a VR transition dialog. */\n  @SuppressWarnings(\"nullness:argument.type.incompatible\")\n  protected void exit() {\n    // This needs to use GVR's exit transition to avoid disorienting the user.\n    DaydreamApi api = DaydreamApi.create(this);\n    if (api != null) {\n      api.exitFromVr(this, EXIT_FROM_VR_REQUEST_CODE, null);\n      // Eventually, the Activity's onActivityResult will be called.\n      api.close();\n    } else {\n      finish();\n    }\n  }\n\n  /** Toggles PlayerControl visibility. */\n  @UiThread\n  protected void togglePlayerControlVisibility() {\n    if (Assertions.checkNotNull(playerControl).isVisible()) {\n      playerControl.hide();\n    } else {\n      playerControl.show();\n    }\n  }\n\n  // Called on GL thread.\n  private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {\n    mainHandler.post(\n        () -> {\n          SurfaceTexture oldSurfaceTexture = this.surfaceTexture;\n          Surface oldSurface = this.surface;\n          this.surfaceTexture = surfaceTexture;\n          this.surface = new Surface(surfaceTexture);\n          if (player != null) {\n            Player.VideoComponent videoComponent = player.getVideoComponent();\n            if (videoComponent != null) {\n              videoComponent.setVideoSurface(surface);\n            }\n          }\n          releaseSurface(oldSurfaceTexture, oldSurface);\n        });\n  }\n\n  private static void releaseSurface(\n      @Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) {\n    if (oldSurfaceTexture != null) {\n      oldSurfaceTexture.release();\n    }\n    if (oldSurface != null) {\n      oldSurface.release();\n    }\n  }\n\n  private class Renderer implements GvrView.StereoRenderer {\n    private static final float Z_NEAR = .1f;\n    private static final float Z_FAR = 100;\n\n    private final float[] viewProjectionMatrix = new float[16];\n    private final SceneRenderer scene;\n    private final GlViewGroup glView;\n    private final PointerRenderer pointerRenderer;\n\n    public Renderer(SceneRenderer scene, GlViewGroup glView, PointerRenderer pointerRenderer) {\n      this.scene = scene;\n      this.glView = glView;\n      this.pointerRenderer = pointerRenderer;\n    }\n\n    @Override\n    public void onNewFrame(HeadTransform headTransform) {}\n\n    @Override\n    public void onDrawEye(Eye eye) {\n      Matrix.multiplyMM(\n          viewProjectionMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, eye.getEyeView(), 0);\n      scene.drawFrame(viewProjectionMatrix, eye.getType() == Eye.Type.RIGHT);\n      if (glView.isVisible()) {\n        glView.getRenderer().draw(viewProjectionMatrix);\n        pointerRenderer.draw(viewProjectionMatrix);\n      }\n    }\n\n    @Override\n    public void onFinishFrame(Viewport viewport) {}\n\n    @Override\n    public void onSurfaceCreated(EGLConfig config) {\n      onSurfaceTextureAvailable(scene.init());\n      glView.getRenderer().init();\n      pointerRenderer.init();\n    }\n\n    @Override\n    public void onSurfaceChanged(int width, int height) {}\n\n    @Override\n    public void onRendererShutdown() {\n      glView.getRenderer().shutdown();\n      pointerRenderer.shutdown();\n      scene.shutdown();\n    }\n  }\n\n  private class ControllerEventListener extends Controller.EventListener {\n\n    private final Controller controller;\n    private final PointerRenderer pointerRenderer;\n    private final GlViewGroup glView;\n    private final float[] controllerOrientationMatrix;\n    private boolean clickButtonDown;\n    private boolean appButtonDown;\n\n    public ControllerEventListener(\n        Controller controller, PointerRenderer pointerRenderer, GlViewGroup glView) {\n      this.controller = controller;\n      this.pointerRenderer = pointerRenderer;\n      this.glView = glView;\n      controllerOrientationMatrix = new float[16];\n    }\n\n    @Override\n    @BinderThread\n    public void onUpdate() {\n      controller.update();\n      controller.orientation.toRotationMatrix(controllerOrientationMatrix);\n      pointerRenderer.setControllerOrientation(controllerOrientationMatrix);\n\n      if (clickButtonDown || controller.clickButtonState) {\n        int action;\n        if (clickButtonDown != controller.clickButtonState) {\n          clickButtonDown = controller.clickButtonState;\n          action = clickButtonDown ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP;\n        } else {\n          action = MotionEvent.ACTION_MOVE;\n        }\n        glView.post(\n            () -> {\n              float[] angles = controller.orientation.toYawPitchRollRadians(new float[3]);\n              boolean clickedOnView = glView.simulateClick(action, angles[0], angles[1]);\n              if (action == MotionEvent.ACTION_DOWN && !clickedOnView) {\n                togglePlayerControlVisibility();\n              }\n            });\n      } else if (!appButtonDown && controller.appButtonState) {\n        glView.post(GvrPlayerActivity.this::togglePlayerControlVisibility);\n      }\n      appButtonDown = controller.appButtonState;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/gvr/src/main/res/layout/vr_ui.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:id=\"@+id/video_ui_view\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"wrap_content\"\n  android:background=\"@android:color/black\"\n  android:orientation=\"horizontal\"\n  tools:ignore=\"Overdraw\">\n  <com.google.android.exoplayer2.ui.PlayerControlView\n    android:id=\"@+id/controller\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"/>\n</merge>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/gvr/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources>\n  <style name=\"VrTheme\" parent=\"android:Theme.Holo\"/>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/gvr/src/main/res/values-v21/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources>\n  <style name=\"VrTheme\" parent=\"android:Theme.Material\"/>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ima/README.md",
    "content": "# ExoPlayer IMA extension #\n\nThe IMA extension is an [AdsLoader][] implementation wrapping the\n[Interactive Media Ads SDK for Android][IMA]. You can use it to insert ads\nalongside content.\n\n[IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/\n[AdsLoader]: https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/source/ads/AdsLoader.html\n\n## Getting the extension ##\n\nThe easiest way to use the extension is to add it as a gradle dependency:\n\n```gradle\nimplementation 'com.google.android.exoplayer:extension-ima:2.X.X'\n```\n\nwhere `2.X.X` is the version, which must match the version of the ExoPlayer\nlibrary being used.\n\nAlternatively, you can clone the ExoPlayer repository and depend on the module\nlocally. Instructions for doing this can be found in ExoPlayer's\n[top level README][].\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n\n## Using the extension ##\n\nTo play ads alongside a single-window content `MediaSource`, prepare the player\nwith an `AdsMediaSource` constructed using an `ImaAdsLoader`, the content\n`MediaSource` and an overlay `ViewGroup` on top of the player. Pass an ad tag\nURI from your ad campaign when creating the `ImaAdsLoader`. The IMA\ndocumentation includes some [sample ad tags][] for testing. Note that the IMA\nextension only supports players which are accessed on the application's main\nthread.\n\nResuming the player after entering the background requires some special handling\nwhen playing ads. The player and its media source are released on entering the\nbackground, and are recreated when the player returns to the foreground. When\nplaying ads it is necessary to persist ad playback state while in the background\nby keeping a reference to the `ImaAdsLoader`. Reuse it when resuming playback of\nthe same content/ads by passing it in when constructing the new\n`AdsMediaSource`. It is also important to persist the player position when\nentering the background by storing the value of `player.getContentPosition()`.\nOn returning to the foreground, seek to that position before preparing the new\nplayer instance. Finally, it is important to call `ImaAdsLoader.release()` when\nplayback of the content/ads has finished and will not be resumed.\n\nYou can try the IMA extension in the ExoPlayer demo app. To do this you must\nselect and build one of the `withExtensions` build variants of the demo app in\nAndroid Studio. You can find IMA test content in the \"IMA sample ad tags\"\nsection of the app. The demo app's `PlayerActivity` also shows how to persist\nthe `ImaAdsLoader` instance and the player position when backgrounded during ad\nplayback.\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n[sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags\n\n## Links ##\n\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*`\n  belong to this module.\n\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ima/build.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.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n        consumerProguardFiles 'proguard-rules.txt'\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3'\n    implementation project(modulePrefix + 'library-core')\n    implementation 'androidx.annotation:annotation:1.1.0'\n    implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n}\n\next {\n    javadocTitle = 'IMA extension'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'extension-ima'\n    releaseDescription = 'Interactive Media Ads extension for ExoPlayer.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ima/proguard-rules.txt",
    "content": "# Proguard rules specific to the IMA extension.\n\n-keep class com.google.ads.interactivemedia.** { *; }\n-keep interface com.google.ads.interactivemedia.** { *; }\n-keep class com.google.obf.** { *; }\n-keep interface com.google.obf.** { *; }\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ima/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.google.android.exoplayer2.ext.ima\">\n  <application>\n    <meta-data android:name=\"com.google.android.gms.ads.AD_MANAGER_APP\"\n        android:value=\"true\"/>\n    <meta-data android:name=\"com.google.android.gms.version\"\n        android:value=\"@integer/google_play_services_version\"/>\n  </application>\n</manifest>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.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.ext.ima;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Looper;\nimport android.os.SystemClock;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.VisibleForTesting;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport com.google.ads.interactivemedia.v3.api.Ad;\nimport com.google.ads.interactivemedia.v3.api.AdDisplayContainer;\nimport com.google.ads.interactivemedia.v3.api.AdError;\nimport com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode;\nimport com.google.ads.interactivemedia.v3.api.AdErrorEvent;\nimport com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;\nimport com.google.ads.interactivemedia.v3.api.AdEvent;\nimport com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener;\nimport com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType;\nimport com.google.ads.interactivemedia.v3.api.AdPodInfo;\nimport com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener;\nimport com.google.ads.interactivemedia.v3.api.AdsManager;\nimport com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;\nimport com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;\nimport com.google.ads.interactivemedia.v3.api.AdsRequest;\nimport com.google.ads.interactivemedia.v3.api.CompanionAdSlot;\nimport com.google.ads.interactivemedia.v3.api.ImaSdkFactory;\nimport com.google.ads.interactivemedia.v3.api.ImaSdkSettings;\nimport com.google.ads.interactivemedia.v3.api.UiElement;\nimport com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;\nimport com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;\nimport com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.ads.AdPlaybackState;\nimport com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState;\nimport com.google.android.exoplayer2.source.ads.AdsLoader;\nimport com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.google.android.exoplayer2.upstream.DataSpec;\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;\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.Collection;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * {@link AdsLoader} using the IMA SDK. All methods must be called on the main thread.\n *\n * <p>The player instance that will play the loaded ads must be set before playback using {@link\n * #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling\n * {@link #release()}.\n *\n * <p>The IMA SDK can take into account video control overlay views when calculating ad viewability.\n * For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} and {@link\n * AdViewProvider#getAdOverlayViews()}.\n */\npublic final class ImaAdsLoader\n    implements Player.EventListener,\n        AdsLoader,\n        VideoAdPlayer,\n        ContentProgressProvider,\n        AdErrorListener,\n        AdsLoadedListener,\n        AdEventListener {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.ima\");\n  }\n\n  /** Builder for {@link ImaAdsLoader}. */\n  public static final class Builder {\n\n    private final Context context;\n\n    @Nullable private ImaSdkSettings imaSdkSettings;\n    @Nullable private AdEventListener adEventListener;\n    @Nullable private Set<UiElement> adUiElements;\n    private int vastLoadTimeoutMs;\n    private int mediaLoadTimeoutMs;\n    private int mediaBitrate;\n    private boolean focusSkipButtonWhenAvailable;\n    private ImaFactory imaFactory;\n\n    /**\n     * Creates a new builder for {@link ImaAdsLoader}.\n     *\n     * @param context The context;\n     */\n    public Builder(Context context) {\n      this.context = Assertions.checkNotNull(context);\n      vastLoadTimeoutMs = TIMEOUT_UNSET;\n      mediaLoadTimeoutMs = TIMEOUT_UNSET;\n      mediaBitrate = BITRATE_UNSET;\n      focusSkipButtonWhenAvailable = true;\n      imaFactory = new DefaultImaFactory();\n    }\n\n    /**\n     * Sets the IMA SDK settings. The provided settings instance's player type and version fields\n     * may be overwritten.\n     *\n     * <p>If this method is not called the default settings will be used.\n     *\n     * @param imaSdkSettings The {@link ImaSdkSettings}.\n     * @return This builder, for convenience.\n     */\n    public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) {\n      this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings);\n      return this;\n    }\n\n    /**\n     * Sets a listener for ad events that will be passed to {@link\n     * AdsManager#addAdEventListener(AdEventListener)}.\n     *\n     * @param adEventListener The ad event listener.\n     * @return This builder, for convenience.\n     */\n    public Builder setAdEventListener(AdEventListener adEventListener) {\n      this.adEventListener = Assertions.checkNotNull(adEventListener);\n      return this;\n    }\n\n    /**\n     * Sets the ad UI elements to be rendered by the IMA SDK.\n     *\n     * @param adUiElements The ad UI elements to be rendered by the IMA SDK.\n     * @return This builder, for convenience.\n     * @see AdsRenderingSettings#setUiElements(Set)\n     */\n    public Builder setAdUiElements(Set<UiElement> adUiElements) {\n      this.adUiElements = new HashSet<>(Assertions.checkNotNull(adUiElements));\n      return this;\n    }\n\n    /**\n     * Sets the VAST load timeout, in milliseconds.\n     *\n     * @param vastLoadTimeoutMs The VAST load timeout, in milliseconds.\n     * @return This builder, for convenience.\n     * @see AdsRequest#setVastLoadTimeout(float)\n     */\n    public Builder setVastLoadTimeoutMs(int vastLoadTimeoutMs) {\n      Assertions.checkArgument(vastLoadTimeoutMs > 0);\n      this.vastLoadTimeoutMs = vastLoadTimeoutMs;\n      return this;\n    }\n\n    /**\n     * Sets the ad media load timeout, in milliseconds.\n     *\n     * @param mediaLoadTimeoutMs The ad media load timeout, in milliseconds.\n     * @return This builder, for convenience.\n     * @see AdsRenderingSettings#setLoadVideoTimeout(int)\n     */\n    public Builder setMediaLoadTimeoutMs(int mediaLoadTimeoutMs) {\n      Assertions.checkArgument(mediaLoadTimeoutMs > 0);\n      this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;\n      return this;\n    }\n\n    /**\n     * Sets the media maximum recommended bitrate for ads, in bps.\n     *\n     * @param bitrate The media maximum recommended bitrate for ads, in bps.\n     * @return This builder, for convenience.\n     * @see AdsRenderingSettings#setBitrateKbps(int)\n     */\n    public Builder setMaxMediaBitrate(int bitrate) {\n      Assertions.checkArgument(bitrate > 0);\n      this.mediaBitrate = bitrate;\n      return this;\n    }\n\n    /**\n     * Sets whether to focus the skip button (when available) on Android TV devices. The default\n     * setting is {@code true}.\n     *\n     * @param focusSkipButtonWhenAvailable Whether to focus the skip button (when available) on\n     *     Android TV devices.\n     * @return This builder, for convenience.\n     * @see AdsRenderingSettings#setFocusSkipButtonWhenAvailable(boolean)\n     */\n    public Builder setFocusSkipButtonWhenAvailable(boolean focusSkipButtonWhenAvailable) {\n      this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable;\n      return this;\n    }\n\n    @VisibleForTesting\n    /* package */ Builder setImaFactory(ImaFactory imaFactory) {\n      this.imaFactory = Assertions.checkNotNull(imaFactory);\n      return this;\n    }\n\n    /**\n     * Returns a new {@link ImaAdsLoader} for the specified ad tag.\n     *\n     * @param adTagUri The URI of a compatible ad tag to load. See\n     *     https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for\n     *     information on compatible ad tags.\n     * @return The new {@link ImaAdsLoader}.\n     */\n    public ImaAdsLoader buildForAdTag(Uri adTagUri) {\n      return new ImaAdsLoader(\n          context,\n          adTagUri,\n          imaSdkSettings,\n          null,\n          vastLoadTimeoutMs,\n          mediaLoadTimeoutMs,\n          mediaBitrate,\n          focusSkipButtonWhenAvailable,\n          adUiElements,\n          adEventListener,\n          imaFactory);\n    }\n\n    /**\n     * Returns a new {@link ImaAdsLoader} with the specified sideloaded ads response.\n     *\n     * @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of\n     *     making a request via an ad tag URL.\n     * @return The new {@link ImaAdsLoader}.\n     */\n    public ImaAdsLoader buildForAdsResponse(String adsResponse) {\n      return new ImaAdsLoader(\n          context,\n          null,\n          imaSdkSettings,\n          adsResponse,\n          vastLoadTimeoutMs,\n          mediaLoadTimeoutMs,\n          mediaBitrate,\n          focusSkipButtonWhenAvailable,\n          adUiElements,\n          adEventListener,\n          imaFactory);\n    }\n  }\n\n  private static final boolean DEBUG = false;\n  private static final String TAG = \"ImaAdsLoader\";\n\n  /**\n   * Whether to enable preloading of ads in {@link AdsRenderingSettings}.\n   */\n  private static final boolean ENABLE_PRELOADING = true;\n\n  private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = \"google/exo.ext.ima\";\n  private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION;\n\n  /** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */\n  private static final long IMA_DURATION_UNSET = -1L;\n\n  /**\n   * Threshold before the end of content at which IMA is notified that content is complete if the\n   * player buffers, in milliseconds.\n   */\n  private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000;\n\n  /** The maximum duration before an ad break that IMA may start preloading the next ad. */\n  private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000;\n\n  private static final int TIMEOUT_UNSET = -1;\n  private static final int BITRATE_UNSET = -1;\n\n  /** The state of ad playback. */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED})\n  private @interface ImaAdState {}\n  /**\n   * The ad playback state when IMA is not playing an ad.\n   */\n  private static final int IMA_AD_STATE_NONE = 0;\n  /**\n   * The ad playback state when IMA has called {@link #playAd()} and not {@link #pauseAd()}.\n   */\n  private static final int IMA_AD_STATE_PLAYING = 1;\n  /**\n   * The ad playback state when IMA has called {@link #pauseAd()} while playing an ad.\n   */\n  private static final int IMA_AD_STATE_PAUSED = 2;\n\n  private final @Nullable Uri adTagUri;\n  private final @Nullable String adsResponse;\n  private final int vastLoadTimeoutMs;\n  private final int mediaLoadTimeoutMs;\n  private final boolean focusSkipButtonWhenAvailable;\n  private final int mediaBitrate;\n  private final @Nullable Set<UiElement> adUiElements;\n  private final @Nullable AdEventListener adEventListener;\n  private final ImaFactory imaFactory;\n  private final Timeline.Period period;\n  private final List<VideoAdPlayerCallback> adCallbacks;\n  private final AdDisplayContainer adDisplayContainer;\n  private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;\n\n  private boolean wasSetPlayerCalled;\n  @Nullable private Player nextPlayer;\n  private Object pendingAdRequestContext;\n  private List<String> supportedMimeTypes;\n  @Nullable private EventListener eventListener;\n  @Nullable private Player player;\n  private VideoProgressUpdate lastContentProgress;\n  private VideoProgressUpdate lastAdProgress;\n  private int lastVolumePercentage;\n\n  private AdsManager adsManager;\n  private boolean initializedAdsManager;\n  private AdLoadException pendingAdLoadError;\n  private Timeline timeline;\n  private long contentDurationMs;\n  private int podIndexOffset;\n  private AdPlaybackState adPlaybackState;\n\n  // Fields tracking IMA's state.\n\n  /** The expected ad group index that IMA should load next. */\n  private int expectedAdGroupIndex;\n  /** The index of the current ad group that IMA is loading. */\n  private int adGroupIndex;\n  /** Whether IMA has sent an ad event to pause content since the last resume content event. */\n  private boolean imaPausedContent;\n  /** The current ad playback state. */\n  private @ImaAdState int imaAdState;\n  /**\n   * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been\n   * called since starting ad playback.\n   */\n  private boolean sentContentComplete;\n\n  // Fields tracking the player/loader state.\n\n  /** Whether the player is playing an ad. */\n  private boolean playingAd;\n  /**\n   * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET}\n   * otherwise.\n   */\n  private int playingAdIndexInAdGroup;\n  /**\n   * Whether there's a pending ad preparation error which IMA needs to be notified of when it\n   * transitions from playing content to playing the ad.\n   */\n  private boolean shouldNotifyAdPrepareError;\n  /**\n   * If a content period has finished but IMA has not yet called {@link #playAd()}, stores the value\n   * of {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to\n   * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise.\n   */\n  private long fakeContentProgressElapsedRealtimeMs;\n  /**\n   * If {@link #fakeContentProgressElapsedRealtimeMs} is set, stores the offset from which the\n   * content progress should increase. {@link C#TIME_UNSET} otherwise.\n   */\n  private long fakeContentProgressOffsetMs;\n  /** Stores the pending content position when a seek operation was intercepted to play an ad. */\n  private long pendingContentPositionMs;\n  /** Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */\n  private boolean sentPendingContentPositionMs;\n\n  /**\n   * Creates a new IMA ads loader.\n   *\n   * <p>If you need to customize the ad request, use {@link ImaAdsLoader.Builder} instead.\n   *\n   * @param context The context.\n   * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See\n   *     https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for\n   *     more information.\n   */\n  public ImaAdsLoader(Context context, Uri adTagUri) {\n    this(\n        context,\n        adTagUri,\n        /* imaSdkSettings= */ null,\n        /* adsResponse= */ null,\n        /* vastLoadTimeoutMs= */ TIMEOUT_UNSET,\n        /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,\n        /* mediaBitrate= */ BITRATE_UNSET,\n        /* focusSkipButtonWhenAvailable= */ true,\n        /* adUiElements= */ null,\n        /* adEventListener= */ null,\n        /* imaFactory= */ new DefaultImaFactory());\n  }\n\n  /**\n   * Creates a new IMA ads loader.\n   *\n   * @param context The context.\n   * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See\n   *     https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for\n   *     more information.\n   * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to\n   *     use the default settings. If set, the player type and version fields may be overwritten.\n   * @deprecated Use {@link ImaAdsLoader.Builder}.\n   */\n  @Deprecated\n  public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) {\n    this(\n        context,\n        adTagUri,\n        imaSdkSettings,\n        /* adsResponse= */ null,\n        /* vastLoadTimeoutMs= */ TIMEOUT_UNSET,\n        /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,\n        /* mediaBitrate= */ BITRATE_UNSET,\n        /* focusSkipButtonWhenAvailable= */ true,\n        /* adUiElements= */ null,\n        /* adEventListener= */ null,\n        /* imaFactory= */ new DefaultImaFactory());\n  }\n\n  private ImaAdsLoader(\n      Context context,\n      @Nullable Uri adTagUri,\n      @Nullable ImaSdkSettings imaSdkSettings,\n      @Nullable String adsResponse,\n      int vastLoadTimeoutMs,\n      int mediaLoadTimeoutMs,\n      int mediaBitrate,\n      boolean focusSkipButtonWhenAvailable,\n      @Nullable Set<UiElement> adUiElements,\n      @Nullable AdEventListener adEventListener,\n      ImaFactory imaFactory) {\n    Assertions.checkArgument(adTagUri != null || adsResponse != null);\n    this.adTagUri = adTagUri;\n    this.adsResponse = adsResponse;\n    this.vastLoadTimeoutMs = vastLoadTimeoutMs;\n    this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;\n    this.mediaBitrate = mediaBitrate;\n    this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable;\n    this.adUiElements = adUiElements;\n    this.adEventListener = adEventListener;\n    this.imaFactory = imaFactory;\n    if (imaSdkSettings == null) {\n      imaSdkSettings = imaFactory.createImaSdkSettings();\n      if (DEBUG) {\n        imaSdkSettings.setDebugMode(true);\n      }\n    }\n    imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);\n    imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);\n    period = new Timeline.Period();\n    adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);\n    adDisplayContainer = imaFactory.createAdDisplayContainer();\n    adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);\n    adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);\n    adsLoader.addAdErrorListener(/* adErrorListener= */ this);\n    adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this);\n    fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;\n    fakeContentProgressOffsetMs = C.TIME_UNSET;\n    pendingContentPositionMs = C.TIME_UNSET;\n    adGroupIndex = C.INDEX_UNSET;\n    contentDurationMs = C.TIME_UNSET;\n    timeline = Timeline.EMPTY;\n  }\n\n  /**\n   * Returns the underlying {@code com.google.ads.interactivemedia.v3.api.AdsLoader} wrapped by\n   * this instance.\n   */\n  public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() {\n    return adsLoader;\n  }\n\n  /**\n   * Returns the {@link AdDisplayContainer} used by this loader.\n   *\n   * <p>Note: any video controls overlays registered via {@link\n   * AdDisplayContainer#registerVideoControlsOverlay(View)} will be unregistered automatically when\n   * the media source detaches from this instance. It is therefore necessary to re-register views\n   * each time the ads loader is reused. Alternatively, provide overlay views via the {@link\n   * AdsLoader.AdViewProvider} when creating the media source to benefit from automatic\n   * registration.\n   */\n  public AdDisplayContainer getAdDisplayContainer() {\n    return adDisplayContainer;\n  }\n\n  /**\n   * Sets the slots for displaying companion ads. Individual slots can be created using {@link\n   * ImaSdkFactory#createCompanionAdSlot()}.\n   *\n   * @param companionSlots Slots for displaying companion ads.\n   * @see AdDisplayContainer#setCompanionSlots(Collection)\n   * @deprecated Use {@code getAdDisplayContainer().setCompanionSlots(...)}.\n   */\n  @Deprecated\n  public void setCompanionSlots(Collection<CompanionAdSlot> companionSlots) {\n    adDisplayContainer.setCompanionSlots(companionSlots);\n  }\n\n  /**\n   * Requests ads, if they have not already been requested. Must be called on the main thread.\n   *\n   * <p>Ads will be requested automatically when the player is prepared if this method has not been\n   * called, so it is only necessary to call this method if you want to request ads before preparing\n   * the player.\n   *\n   * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.\n   */\n  public void requestAds(ViewGroup adViewGroup) {\n    if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) {\n      // Ads have already been requested.\n      return;\n    }\n    adDisplayContainer.setAdContainer(adViewGroup);\n    pendingAdRequestContext = new Object();\n    AdsRequest request = imaFactory.createAdsRequest();\n    if (adTagUri != null) {\n      request.setAdTagUrl(adTagUri.toString());\n    } else /* adsResponse != null */ {\n      request.setAdsResponse(adsResponse);\n    }\n    if (vastLoadTimeoutMs != TIMEOUT_UNSET) {\n      request.setVastLoadTimeout(vastLoadTimeoutMs);\n    }\n    request.setContentProgressProvider(this);\n    request.setUserRequestContext(pendingAdRequestContext);\n    adsLoader.requestAds(request);\n  }\n\n  // AdsLoader implementation.\n\n  @Override\n  public void setPlayer(@Nullable Player player) {\n    Assertions.checkState(Looper.getMainLooper() == Looper.myLooper());\n    Assertions.checkState(\n        player == null || player.getApplicationLooper() == Looper.getMainLooper());\n    nextPlayer = player;\n    wasSetPlayerCalled = true;\n  }\n\n  @Override\n  public void setSupportedContentTypes(@C.ContentType int... contentTypes) {\n    List<String> supportedMimeTypes = new ArrayList<>();\n    for (@C.ContentType int contentType : contentTypes) {\n      if (contentType == C.TYPE_DASH) {\n        supportedMimeTypes.add(MimeTypes.APPLICATION_MPD);\n      } else if (contentType == C.TYPE_HLS) {\n        supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8);\n      } else if (contentType == C.TYPE_OTHER) {\n        supportedMimeTypes.addAll(\n            Arrays.asList(\n                MimeTypes.VIDEO_MP4,\n                MimeTypes.VIDEO_WEBM,\n                MimeTypes.VIDEO_H263,\n                MimeTypes.AUDIO_MP4,\n                MimeTypes.AUDIO_MPEG));\n      } else if (contentType == C.TYPE_SS) {\n        // IMA does not support Smooth Streaming ad media.\n      }\n    }\n    this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes);\n  }\n\n  @Override\n  public void start(EventListener eventListener, AdViewProvider adViewProvider) {\n    Assertions.checkState(\n        wasSetPlayerCalled, \"Set player using adsLoader.setPlayer before preparing the player.\");\n    player = nextPlayer;\n    if (player == null) {\n      return;\n    }\n    this.eventListener = eventListener;\n    lastVolumePercentage = 0;\n    lastAdProgress = null;\n    lastContentProgress = null;\n    ViewGroup adViewGroup = adViewProvider.getAdViewGroup();\n    adDisplayContainer.setAdContainer(adViewGroup);\n    View[] adOverlayViews = adViewProvider.getAdOverlayViews();\n    for (View view : adOverlayViews) {\n      adDisplayContainer.registerVideoControlsOverlay(view);\n    }\n    player.addListener(this);\n    maybeNotifyPendingAdLoadError();\n    if (adPlaybackState != null) {\n      // Pass the ad playback state to the player, and resume ads if necessary.\n      eventListener.onAdPlaybackState(adPlaybackState);\n      if (imaPausedContent && player.getPlayWhenReady()) {\n        adsManager.resume();\n      }\n    } else if (adsManager != null) {\n      adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints()));\n      updateAdPlaybackState();\n    } else {\n      // Ads haven't loaded yet, so request them.\n      requestAds(adViewGroup);\n    }\n  }\n\n  @Override\n  public void stop() {\n    if (player == null) {\n      return;\n    }\n    if (adsManager != null && imaPausedContent) {\n      adPlaybackState =\n          adPlaybackState.withAdResumePositionUs(\n              playingAd ? C.msToUs(player.getCurrentPosition()) : 0);\n      adsManager.pause();\n    }\n    lastVolumePercentage = getVolume();\n    lastAdProgress = getAdProgress();\n    lastContentProgress = getContentProgress();\n    adDisplayContainer.unregisterAllVideoControlsOverlays();\n    player.removeListener(this);\n    player = null;\n    eventListener = null;\n  }\n\n  @Override\n  public void release() {\n    pendingAdRequestContext = null;\n    if (adsManager != null) {\n      adsManager.destroy();\n      adsManager = null;\n    }\n    adsLoader.removeAdsLoadedListener(/* adsLoadedListener= */ this);\n    adsLoader.removeAdErrorListener(/* adErrorListener= */ this);\n    imaPausedContent = false;\n    imaAdState = IMA_AD_STATE_NONE;\n    pendingAdLoadError = null;\n    adPlaybackState = AdPlaybackState.NONE;\n    updateAdPlaybackState();\n  }\n\n  @Override\n  public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) {\n    if (player == null) {\n      return;\n    }\n    try {\n      handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception);\n    } catch (Exception e) {\n      maybeNotifyInternalError(\"handlePrepareError\", e);\n    }\n  }\n\n  // com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation.\n\n  @Override\n  public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {\n    AdsManager adsManager = adsManagerLoadedEvent.getAdsManager();\n    if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) {\n      adsManager.destroy();\n      return;\n    }\n    pendingAdRequestContext = null;\n    this.adsManager = adsManager;\n    adsManager.addAdErrorListener(this);\n    adsManager.addAdEventListener(this);\n    if (adEventListener != null) {\n      adsManager.addAdEventListener(adEventListener);\n    }\n    if (player != null) {\n      // If a player is attached already, start playback immediately.\n      try {\n        adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints()));\n        updateAdPlaybackState();\n      } catch (Exception e) {\n        maybeNotifyInternalError(\"onAdsManagerLoaded\", e);\n      }\n    }\n  }\n\n  // AdEvent.AdEventListener implementation.\n\n  @Override\n  public void onAdEvent(AdEvent adEvent) {\n    AdEventType adEventType = adEvent.getType();\n    if (DEBUG) {\n      Log.d(TAG, \"onAdEvent: \" + adEventType);\n    }\n    if (adsManager == null) {\n      Log.w(TAG, \"Ignoring AdEvent after release: \" + adEvent);\n      return;\n    }\n    try {\n      handleAdEvent(adEvent);\n    } catch (Exception e) {\n      maybeNotifyInternalError(\"onAdEvent\", e);\n    }\n  }\n\n  // AdErrorEvent.AdErrorListener implementation.\n\n  @Override\n  public void onAdError(AdErrorEvent adErrorEvent) {\n    AdError error = adErrorEvent.getError();\n    if (DEBUG) {\n      Log.d(TAG, \"onAdError\", error);\n    }\n    if (adsManager == null) {\n      // No ads were loaded, so allow playback to start without any ads.\n      pendingAdRequestContext = null;\n      adPlaybackState = new AdPlaybackState();\n      updateAdPlaybackState();\n    } else if (isAdGroupLoadError(error)) {\n      try {\n        handleAdGroupLoadError(error);\n      } catch (Exception e) {\n        maybeNotifyInternalError(\"onAdError\", e);\n      }\n    }\n    if (pendingAdLoadError == null) {\n      pendingAdLoadError = AdLoadException.createForAllAds(error);\n    }\n    maybeNotifyPendingAdLoadError();\n  }\n\n  // ContentProgressProvider implementation.\n\n  @Override\n  public VideoProgressUpdate getContentProgress() {\n    if (player == null) {\n      return lastContentProgress;\n    }\n    boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;\n    long contentPositionMs;\n    if (pendingContentPositionMs != C.TIME_UNSET) {\n      sentPendingContentPositionMs = true;\n      contentPositionMs = pendingContentPositionMs;\n      expectedAdGroupIndex =\n          adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));\n    } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {\n      long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;\n      contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;\n      expectedAdGroupIndex =\n          adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));\n    } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {\n      contentPositionMs = player.getCurrentPosition();\n      // Update the expected ad group index for the current content position. The update is delayed\n      // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered\n      // just after an ad group isn't incorrectly attributed to the next ad group.\n      int nextAdGroupIndex =\n          adPlaybackState.getAdGroupIndexAfterPositionUs(\n              C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));\n      if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) {\n        long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]);\n        if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) {\n          nextAdGroupTimeMs = contentDurationMs;\n        }\n        if (nextAdGroupTimeMs - contentPositionMs < MAXIMUM_PRELOAD_DURATION_MS) {\n          expectedAdGroupIndex = nextAdGroupIndex;\n        }\n      }\n    } else {\n      return VideoProgressUpdate.VIDEO_TIME_NOT_READY;\n    }\n    long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET;\n    return new VideoProgressUpdate(contentPositionMs, contentDurationMs);\n  }\n\n  // VideoAdPlayer implementation.\n\n  @Override\n  public VideoProgressUpdate getAdProgress() {\n    if (player == null) {\n      return lastAdProgress;\n    } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) {\n      long adDuration = player.getDuration();\n      return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY\n          : new VideoProgressUpdate(player.getCurrentPosition(), adDuration);\n    } else {\n      return VideoProgressUpdate.VIDEO_TIME_NOT_READY;\n    }\n  }\n\n  @Override\n  public int getVolume() {\n    if (player == null) {\n      return lastVolumePercentage;\n    }\n\n    Player.AudioComponent audioComponent = player.getAudioComponent();\n    if (audioComponent != null) {\n      return (int) (audioComponent.getVolume() * 100);\n    }\n\n    // Check for a selected track using an audio renderer.\n    TrackSelectionArray trackSelections = player.getCurrentTrackSelections();\n    for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) {\n      if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) {\n        return 100;\n      }\n    }\n    return 0;\n  }\n\n  @Override\n  public void loadAd(String adUriString) {\n    try {\n      if (DEBUG) {\n        Log.d(TAG, \"loadAd in ad group \" + adGroupIndex);\n      }\n      if (adsManager == null) {\n        Log.w(TAG, \"Ignoring loadAd after release\");\n        return;\n      }\n      if (adGroupIndex == C.INDEX_UNSET) {\n        Log.w(\n            TAG,\n            \"Unexpected loadAd without LOADED event; assuming ad group index is actually \"\n                + expectedAdGroupIndex);\n        adGroupIndex = expectedAdGroupIndex;\n        adsManager.start();\n      }\n      int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);\n      if (adIndexInAdGroup == C.INDEX_UNSET) {\n        Log.w(TAG, \"Unexpected loadAd in an ad group with no remaining unavailable ads\");\n        return;\n      }\n      adPlaybackState =\n          adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString));\n      updateAdPlaybackState();\n    } catch (Exception e) {\n      maybeNotifyInternalError(\"loadAd\", e);\n    }\n  }\n\n  @Override\n  public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) {\n    adCallbacks.add(videoAdPlayerCallback);\n  }\n\n  @Override\n  public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) {\n    adCallbacks.remove(videoAdPlayerCallback);\n  }\n\n  @Override\n  public void playAd() {\n    if (DEBUG) {\n      Log.d(TAG, \"playAd\");\n    }\n    if (adsManager == null) {\n      Log.w(TAG, \"Ignoring playAd after release\");\n      return;\n    }\n    switch (imaAdState) {\n      case IMA_AD_STATE_PLAYING:\n        // IMA does not always call stopAd before resuming content.\n        // See [Internal: b/38354028, b/63320878].\n        Log.w(TAG, \"Unexpected playAd without stopAd\");\n        break;\n      case IMA_AD_STATE_NONE:\n        // IMA is requesting to play the ad, so stop faking the content position.\n        fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;\n        fakeContentProgressOffsetMs = C.TIME_UNSET;\n        imaAdState = IMA_AD_STATE_PLAYING;\n        for (int i = 0; i < adCallbacks.size(); i++) {\n          adCallbacks.get(i).onPlay();\n        }\n        if (shouldNotifyAdPrepareError) {\n          shouldNotifyAdPrepareError = false;\n          for (int i = 0; i < adCallbacks.size(); i++) {\n            adCallbacks.get(i).onError();\n          }\n        }\n        break;\n      case IMA_AD_STATE_PAUSED:\n        imaAdState = IMA_AD_STATE_PLAYING;\n        for (int i = 0; i < adCallbacks.size(); i++) {\n          adCallbacks.get(i).onResume();\n        }\n        break;\n      default:\n        throw new IllegalStateException();\n    }\n    if (player == null) {\n      // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].\n      Log.w(TAG, \"Unexpected playAd while detached\");\n    } else if (!player.getPlayWhenReady()) {\n      adsManager.pause();\n    }\n  }\n\n  @Override\n  public void stopAd() {\n    if (DEBUG) {\n      Log.d(TAG, \"stopAd\");\n    }\n    if (adsManager == null) {\n      Log.w(TAG, \"Ignoring stopAd after release\");\n      return;\n    }\n    if (player == null) {\n      // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].\n      Log.w(TAG, \"Unexpected stopAd while detached\");\n    }\n    if (imaAdState == IMA_AD_STATE_NONE) {\n      Log.w(TAG, \"Unexpected stopAd\");\n      return;\n    }\n    try {\n      stopAdInternal();\n    } catch (Exception e) {\n      maybeNotifyInternalError(\"stopAd\", e);\n    }\n  }\n\n  @Override\n  public void pauseAd() {\n    if (DEBUG) {\n      Log.d(TAG, \"pauseAd\");\n    }\n    if (imaAdState == IMA_AD_STATE_NONE) {\n      // This method is called after content is resumed.\n      return;\n    }\n    imaAdState = IMA_AD_STATE_PAUSED;\n    for (int i = 0; i < adCallbacks.size(); i++) {\n      adCallbacks.get(i).onPause();\n    }\n  }\n\n  @Override\n  public void resumeAd() {\n    // This method is never called. See [Internal: b/18931719].\n    maybeNotifyInternalError(\"resumeAd\", new IllegalStateException(\"Unexpected call to resumeAd\"));\n  }\n\n  // Player.EventListener implementation.\n\n  @Override\n  public void onTimelineChanged(\n      Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {\n    if (timeline.isEmpty()) {\n      // The player is being reset or contains no media.\n      return;\n    }\n    Assertions.checkArgument(timeline.getPeriodCount() == 1);\n    this.timeline = timeline;\n    long contentDurationUs = timeline.getPeriod(0, period).durationUs;\n    contentDurationMs = C.usToMs(contentDurationUs);\n    if (contentDurationUs != C.TIME_UNSET) {\n      adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs);\n    }\n    if (!initializedAdsManager && adsManager != null) {\n      initializedAdsManager = true;\n      initializeAdsManager();\n    }\n    onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);\n  }\n\n  @Override\n  public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n    if (adsManager == null) {\n      return;\n    }\n\n    if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) {\n      adsManager.pause();\n      return;\n    }\n\n    if (imaAdState == IMA_AD_STATE_PAUSED && playWhenReady) {\n      adsManager.resume();\n      return;\n    }\n\n    if (imaAdState == IMA_AD_STATE_NONE && playbackState == Player.STATE_BUFFERING\n        && playWhenReady) {\n      checkForContentComplete();\n    } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) {\n      for (int i = 0; i < adCallbacks.size(); i++) {\n        adCallbacks.get(i).onEnded();\n      }\n      if (DEBUG) {\n        Log.d(TAG, \"VideoAdPlayerCallback.onEnded in onPlayerStateChanged\");\n      }\n    }\n  }\n\n  @Override\n  public void onPlayerError(ExoPlaybackException error) {\n    if (imaAdState != IMA_AD_STATE_NONE) {\n      for (int i = 0; i < adCallbacks.size(); i++) {\n        adCallbacks.get(i).onError();\n      }\n    }\n  }\n\n  @Override\n  public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {\n    if (adsManager == null) {\n      return;\n    }\n    if (!playingAd && !player.isPlayingAd()) {\n      checkForContentComplete();\n      if (sentContentComplete) {\n        for (int i = 0; i < adPlaybackState.adGroupCount; i++) {\n          if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) {\n            adPlaybackState = adPlaybackState.withSkippedAdGroup(i);\n          }\n        }\n        updateAdPlaybackState();\n      } else if (!timeline.isEmpty()) {\n        long positionMs = player.getCurrentPosition();\n        timeline.getPeriod(0, period);\n        int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs));\n        if (newAdGroupIndex != C.INDEX_UNSET) {\n          sentPendingContentPositionMs = false;\n          pendingContentPositionMs = positionMs;\n          if (newAdGroupIndex != adGroupIndex) {\n            shouldNotifyAdPrepareError = false;\n          }\n        }\n      }\n    }\n    updateImaStateForPlayerState();\n  }\n\n  // Internal methods.\n\n  private void initializeAdsManager() {\n    AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings();\n    adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING);\n    adsRenderingSettings.setMimeTypes(supportedMimeTypes);\n    if (mediaLoadTimeoutMs != TIMEOUT_UNSET) {\n      adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs);\n    }\n    if (mediaBitrate != BITRATE_UNSET) {\n      adsRenderingSettings.setBitrateKbps(mediaBitrate / 1000);\n    }\n    adsRenderingSettings.setFocusSkipButtonWhenAvailable(focusSkipButtonWhenAvailable);\n    if (adUiElements != null) {\n      adsRenderingSettings.setUiElements(adUiElements);\n    }\n\n    // Skip ads based on the start position as required.\n    long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());\n    long contentPositionMs = player.getContentPosition();\n    int adGroupIndexForPosition =\n        adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));\n    if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) {\n      // Skip any ad groups before the one at or immediately before the playback position.\n      for (int i = 0; i < adGroupIndexForPosition; i++) {\n        adPlaybackState = adPlaybackState.withSkippedAdGroup(i);\n      }\n      // Play ads after the midpoint between the ad to play and the one before it, to avoid issues\n      // with rounding one of the two ad times.\n      long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition];\n      long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1];\n      double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d;\n      adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);\n    }\n\n    // IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0.\n    // Store an index offset as we want to index all ads (including skipped ones) from 0.\n    if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) {\n      // We are playing a preroll.\n      podIndexOffset = 0;\n    } else if (adGroupIndexForPosition == C.INDEX_UNSET) {\n      // There's no ad to play which means there's no preroll.\n      podIndexOffset = -1;\n    } else {\n      // We are playing a midroll and any ads before it were skipped.\n      podIndexOffset = adGroupIndexForPosition - 1;\n    }\n\n    if (adGroupIndexForPosition != C.INDEX_UNSET && hasMidrollAdGroups(adGroupTimesUs)) {\n      // Provide the player's initial position to trigger loading and playing the ad.\n      pendingContentPositionMs = contentPositionMs;\n    }\n\n    adsManager.init(adsRenderingSettings);\n    updateAdPlaybackState();\n    if (DEBUG) {\n      Log.d(TAG, \"Initialized with ads rendering settings: \" + adsRenderingSettings);\n    }\n  }\n\n  private void handleAdEvent(AdEvent adEvent) {\n    Ad ad = adEvent.getAd();\n    switch (adEvent.getType()) {\n      case LOADED:\n        // The ad position is not always accurate when using preloading. See [Internal: b/62613240].\n        AdPodInfo adPodInfo = ad.getAdPodInfo();\n        int podIndex = adPodInfo.getPodIndex();\n        adGroupIndex =\n            podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset);\n        int adPosition = adPodInfo.getAdPosition();\n        int adCount = adPodInfo.getTotalAds();\n        adsManager.start();\n        if (DEBUG) {\n          Log.d(TAG, \"Loaded ad \" + adPosition + \" of \" + adCount + \" in group \" + adGroupIndex);\n        }\n        int oldAdCount = adPlaybackState.adGroups[adGroupIndex].count;\n        if (adCount != oldAdCount) {\n          if (oldAdCount == C.LENGTH_UNSET) {\n            adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount);\n            updateAdPlaybackState();\n          } else {\n            // IMA sometimes unexpectedly decreases the ad count in an ad group.\n            Log.w(TAG, \"Unexpected ad count in LOADED, \" + adCount + \", expected \" + oldAdCount);\n          }\n        }\n        if (adGroupIndex != expectedAdGroupIndex) {\n          Log.w(\n              TAG,\n              \"Expected ad group index \"\n                  + expectedAdGroupIndex\n                  + \", actual ad group index \"\n                  + adGroupIndex);\n          expectedAdGroupIndex = adGroupIndex;\n        }\n        break;\n      case CONTENT_PAUSE_REQUESTED:\n        // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads\n        // before sending CONTENT_RESUME_REQUESTED.\n        imaPausedContent = true;\n        pauseContentInternal();\n        break;\n      case TAPPED:\n        if (eventListener != null) {\n          eventListener.onAdTapped();\n        }\n        break;\n      case CLICKED:\n        if (eventListener != null) {\n          eventListener.onAdClicked();\n        }\n        break;\n      case CONTENT_RESUME_REQUESTED:\n        imaPausedContent = false;\n        resumeContentInternal();\n        break;\n      case LOG:\n        Map<String, String> adData = adEvent.getAdData();\n        String message = \"AdEvent: \" + adData;\n        Log.i(TAG, message);\n        if (\"adLoadError\".equals(adData.get(\"type\"))) {\n          handleAdGroupLoadError(new IOException(message));\n        }\n        break;\n      case STARTED:\n      case ALL_ADS_COMPLETED:\n      default:\n        break;\n    }\n  }\n\n  private void updateImaStateForPlayerState() {\n    boolean wasPlayingAd = playingAd;\n    int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup;\n    playingAd = player.isPlayingAd();\n    playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET;\n    boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup;\n    if (adFinished) {\n      // IMA is waiting for the ad playback to finish so invoke the callback now.\n      // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.\n      for (int i = 0; i < adCallbacks.size(); i++) {\n        adCallbacks.get(i).onEnded();\n      }\n      if (DEBUG) {\n        Log.d(TAG, \"VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity\");\n      }\n    }\n    if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) {\n      int adGroupIndex = player.getCurrentAdGroupIndex();\n      // IMA hasn't called playAd yet, so fake the content position.\n      fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();\n      fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);\n      if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {\n        fakeContentProgressOffsetMs = contentDurationMs;\n      }\n    }\n  }\n\n  private void resumeContentInternal() {\n    if (imaAdState != IMA_AD_STATE_NONE) {\n      imaAdState = IMA_AD_STATE_NONE;\n      if (DEBUG) {\n        Log.d(TAG, \"Unexpected CONTENT_RESUME_REQUESTED without stopAd\");\n      }\n    }\n    if (adGroupIndex != C.INDEX_UNSET) {\n      adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex);\n      adGroupIndex = C.INDEX_UNSET;\n      updateAdPlaybackState();\n    }\n  }\n\n  private void pauseContentInternal() {\n    imaAdState = IMA_AD_STATE_NONE;\n    if (sentPendingContentPositionMs) {\n      pendingContentPositionMs = C.TIME_UNSET;\n      sentPendingContentPositionMs = false;\n    }\n  }\n\n  private void stopAdInternal() {\n    imaAdState = IMA_AD_STATE_NONE;\n    int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay();\n    // TODO: Handle the skipped event so the ad can be marked as skipped rather than played.\n    adPlaybackState =\n        adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0);\n    updateAdPlaybackState();\n    if (!playingAd) {\n      adGroupIndex = C.INDEX_UNSET;\n    }\n  }\n\n  private void handleAdGroupLoadError(Exception error) {\n    int adGroupIndex =\n        this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex;\n    if (adGroupIndex == C.INDEX_UNSET) {\n      // Drop the error, as we don't know which ad group it relates to.\n      return;\n    }\n    AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];\n    if (adGroup.count == C.LENGTH_UNSET) {\n      adPlaybackState =\n          adPlaybackState.withAdCount(adGroupIndex, Math.max(1, adGroup.states.length));\n      adGroup = adPlaybackState.adGroups[adGroupIndex];\n    }\n    for (int i = 0; i < adGroup.count; i++) {\n      if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {\n        if (DEBUG) {\n          Log.d(TAG, \"Removing ad \" + i + \" in ad group \" + adGroupIndex);\n        }\n        adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i);\n      }\n    }\n    updateAdPlaybackState();\n    if (pendingAdLoadError == null) {\n      pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);\n    }\n    pendingContentPositionMs = C.TIME_UNSET;\n    fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;\n  }\n\n  private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {\n    if (DEBUG) {\n      Log.d(\n          TAG, \"Prepare error for ad \" + adIndexInAdGroup + \" in group \" + adGroupIndex, exception);\n    }\n    if (adsManager == null) {\n      Log.w(TAG, \"Ignoring ad prepare error after release\");\n      return;\n    }\n    if (imaAdState == IMA_AD_STATE_NONE) {\n      // Send IMA a content position at the ad group so that it will try to play it, at which point\n      // we can notify that it failed to load.\n      fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();\n      fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);\n      if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {\n        fakeContentProgressOffsetMs = contentDurationMs;\n      }\n      shouldNotifyAdPrepareError = true;\n    } else {\n      // We're already playing an ad.\n      if (adIndexInAdGroup > playingAdIndexInAdGroup) {\n        // Mark the playing ad as ended so we can notify the error on the next ad and remove it,\n        // which means that the ad after will load (if any).\n        for (int i = 0; i < adCallbacks.size(); i++) {\n          adCallbacks.get(i).onEnded();\n        }\n      }\n      playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay();\n      for (int i = 0; i < adCallbacks.size(); i++) {\n        adCallbacks.get(i).onError();\n      }\n    }\n    adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup);\n    updateAdPlaybackState();\n  }\n\n  private void checkForContentComplete() {\n    if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET\n        && player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs\n        && !sentContentComplete) {\n      adsLoader.contentComplete();\n      if (DEBUG) {\n        Log.d(TAG, \"adsLoader.contentComplete\");\n      }\n      sentContentComplete = true;\n      // After sending content complete IMA will not poll the content position, so set the expected\n      // ad group index.\n      expectedAdGroupIndex =\n          adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentDurationMs));\n    }\n  }\n\n  private void updateAdPlaybackState() {\n    // Ignore updates while detached. When a player is attached it will receive the latest state.\n    if (eventListener != null) {\n      eventListener.onAdPlaybackState(adPlaybackState);\n    }\n  }\n\n  /**\n   * Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all\n   * ads in the ad group have loaded.\n   */\n  private int getAdIndexInAdGroupToLoad(int adGroupIndex) {\n    @AdState int[] states = adPlaybackState.adGroups[adGroupIndex].states;\n    int adIndexInAdGroup = 0;\n    // IMA loads ads in order.\n    while (adIndexInAdGroup < states.length\n        && states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE) {\n      adIndexInAdGroup++;\n    }\n    return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup;\n  }\n\n  private void maybeNotifyPendingAdLoadError() {\n    if (pendingAdLoadError != null && eventListener != null) {\n      eventListener.onAdLoadError(pendingAdLoadError, new DataSpec(adTagUri));\n      pendingAdLoadError = null;\n    }\n  }\n\n  private void maybeNotifyInternalError(String name, Exception cause) {\n    String message = \"Internal error in \" + name;\n    Log.e(TAG, message, cause);\n    // We can't recover from an unexpected error in general, so skip all remaining ads.\n    if (adPlaybackState == null) {\n      adPlaybackState = AdPlaybackState.NONE;\n    } else {\n      for (int i = 0; i < adPlaybackState.adGroupCount; i++) {\n        adPlaybackState = adPlaybackState.withSkippedAdGroup(i);\n      }\n    }\n    updateAdPlaybackState();\n    if (eventListener != null) {\n      eventListener.onAdLoadError(\n          AdLoadException.createForUnexpected(new RuntimeException(message, cause)),\n          new DataSpec(adTagUri));\n    }\n  }\n\n  private static long[] getAdGroupTimesUs(List<Float> cuePoints) {\n    if (cuePoints.isEmpty()) {\n      // If no cue points are specified, there is a preroll ad.\n      return new long[] {0};\n    }\n\n    int count = cuePoints.size();\n    long[] adGroupTimesUs = new long[count];\n    int adGroupIndex = 0;\n    for (int i = 0; i < count; i++) {\n      double cuePoint = cuePoints.get(i);\n      if (cuePoint == -1.0) {\n        adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE;\n      } else {\n        adGroupTimesUs[adGroupIndex++] = (long) (C.MICROS_PER_SECOND * cuePoint);\n      }\n    }\n    // Cue points may be out of order, so sort them.\n    Arrays.sort(adGroupTimesUs, 0, adGroupIndex);\n    return adGroupTimesUs;\n  }\n\n  private static boolean isAdGroupLoadError(AdError adError) {\n    // TODO: Find out what other errors need to be handled (if any), and whether each one relates to\n    // a single ad, ad group or the whole timeline.\n    return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH\n        || adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR;\n  }\n\n  private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) {\n    int count = adGroupTimesUs.length;\n    if (count == 1) {\n      return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE;\n    } else if (count == 2) {\n      return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE;\n    } else {\n      // There's at least one midroll ad group, as adGroupTimesUs is never empty.\n      return true;\n    }\n  }\n\n  /** Factory for objects provided by the IMA SDK. */\n  @VisibleForTesting\n  /* package */ interface ImaFactory {\n    /** @see ImaSdkSettings */\n    ImaSdkSettings createImaSdkSettings();\n    /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRenderingSettings() */\n    AdsRenderingSettings createAdsRenderingSettings();\n    /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdDisplayContainer() */\n    AdDisplayContainer createAdDisplayContainer();\n    /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */\n    AdsRequest createAdsRequest();\n    /** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) */\n    com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(\n        Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer);\n  }\n\n  /** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */\n  private static final class DefaultImaFactory implements ImaFactory {\n    @Override\n    public ImaSdkSettings createImaSdkSettings() {\n      return ImaSdkFactory.getInstance().createImaSdkSettings();\n    }\n\n    @Override\n    public AdsRenderingSettings createAdsRenderingSettings() {\n      return ImaSdkFactory.getInstance().createAdsRenderingSettings();\n    }\n\n    @Override\n    public AdDisplayContainer createAdDisplayContainer() {\n      return ImaSdkFactory.getInstance().createAdDisplayContainer();\n    }\n\n    @Override\n    public AdsRequest createAdsRequest() {\n      return ImaSdkFactory.getInstance().createAdsRequest();\n    }\n\n    @Override\n    public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(\n        Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {\n      return ImaSdkFactory.getInstance()\n          .createAdsLoader(context, imaSdkSettings, adDisplayContainer);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ima/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<manifest package=\"com.google.android.exoplayer2.ext.ima.test\" />\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.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.ext.ima;\n\nimport com.google.ads.interactivemedia.v3.api.Ad;\nimport com.google.ads.interactivemedia.v3.api.AdPodInfo;\nimport com.google.ads.interactivemedia.v3.api.CompanionAd;\nimport com.google.ads.interactivemedia.v3.api.UiElement;\nimport java.util.List;\nimport java.util.Set;\n\n/** A fake ad for testing. */\n/* package */ final class FakeAd implements Ad {\n\n  private final boolean skippable;\n  private final AdPodInfo adPodInfo;\n\n  public FakeAd(boolean skippable, int podIndex, int totalAds, int adPosition) {\n    this.skippable = skippable;\n    adPodInfo =\n        new AdPodInfo() {\n          @Override\n          public int getTotalAds() {\n            return totalAds;\n          }\n\n          @Override\n          public int getAdPosition() {\n            return adPosition;\n          }\n\n          @Override\n          public int getPodIndex() {\n            return podIndex;\n          }\n\n          @Override\n          public boolean isBumper() {\n            throw new UnsupportedOperationException();\n          }\n\n          @Override\n          public double getMaxDuration() {\n            throw new UnsupportedOperationException();\n          }\n\n          @Override\n          public double getTimeOffset() {\n            throw new UnsupportedOperationException();\n          }\n        };\n  }\n\n  @Override\n  public int getVastMediaWidth() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public int getVastMediaHeight() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public int getVastMediaBitrate() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public boolean isSkippable() {\n    return skippable;\n  }\n\n  @Override\n  public AdPodInfo getAdPodInfo() {\n    return adPodInfo;\n  }\n\n  @Override\n  public String getAdId() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String getCreativeId() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String getCreativeAdId() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String getUniversalAdIdValue() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String getUniversalAdIdRegistry() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String getAdSystem() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String[] getAdWrapperIds() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String[] getAdWrapperSystems() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String[] getAdWrapperCreativeIds() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public boolean isLinear() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public double getSkipTimeOffset() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public boolean isUiDisabled() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String getDescription() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String getTitle() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String getContentType() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String getAdvertiserName() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String getSurveyUrl() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String getDealId() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public int getWidth() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public int getHeight() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String getTraffickingParameters() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public double getDuration() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public Set<UiElement> getUiElements() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public List<CompanionAd> getCompanionAds() {\n    throw new UnsupportedOperationException();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.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.ext.ima;\n\nimport com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;\nimport com.google.ads.interactivemedia.v3.api.AdsManager;\nimport com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;\nimport com.google.ads.interactivemedia.v3.api.AdsRequest;\nimport com.google.ads.interactivemedia.v3.api.ImaSdkSettings;\nimport com.google.ads.interactivemedia.v3.api.StreamManager;\nimport com.google.ads.interactivemedia.v3.api.StreamRequest;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.ArrayList;\n\n/** Fake {@link com.google.ads.interactivemedia.v3.api.AdsLoader} implementation for tests. */\npublic final class FakeAdsLoader implements com.google.ads.interactivemedia.v3.api.AdsLoader {\n\n  private final ImaSdkSettings imaSdkSettings;\n  private final AdsManager adsManager;\n  private final ArrayList<AdsLoadedListener> adsLoadedListeners;\n  private final ArrayList<AdErrorListener> adErrorListeners;\n\n  public FakeAdsLoader(ImaSdkSettings imaSdkSettings, AdsManager adsManager) {\n    this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings);\n    this.adsManager = Assertions.checkNotNull(adsManager);\n    adsLoadedListeners = new ArrayList<>();\n    adErrorListeners = new ArrayList<>();\n  }\n\n  @Override\n  public void contentComplete() {\n    // Do nothing.\n  }\n\n  @Override\n  public ImaSdkSettings getSettings() {\n    return imaSdkSettings;\n  }\n\n  @Override\n  public void requestAds(AdsRequest adsRequest) {\n    for (AdsLoadedListener listener : adsLoadedListeners) {\n      listener.onAdsManagerLoaded(\n          new AdsManagerLoadedEvent() {\n            @Override\n            public AdsManager getAdsManager() {\n              return adsManager;\n            }\n\n            @Override\n            public StreamManager getStreamManager() {\n              throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public Object getUserRequestContext() {\n              return adsRequest.getUserRequestContext();\n            }\n          });\n    }\n  }\n\n  @Override\n  public String requestStream(StreamRequest streamRequest) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void addAdsLoadedListener(AdsLoadedListener adsLoadedListener) {\n    adsLoadedListeners.add(adsLoadedListener);\n  }\n\n  @Override\n  public void removeAdsLoadedListener(AdsLoadedListener adsLoadedListener) {\n    adsLoadedListeners.remove(adsLoadedListener);\n  }\n\n  @Override\n  public void addAdErrorListener(AdErrorListener adErrorListener) {\n    adErrorListeners.add(adErrorListener);\n  }\n\n  @Override\n  public void removeAdErrorListener(AdErrorListener adErrorListener) {\n    adErrorListeners.remove(adErrorListener);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.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.ext.ima;\n\nimport com.google.ads.interactivemedia.v3.api.AdDisplayContainer;\nimport com.google.ads.interactivemedia.v3.api.AdsRequest;\nimport com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;\nimport java.util.List;\nimport java.util.Map;\n\n/** Fake {@link AdsRequest} implementation for tests. */\npublic final class FakeAdsRequest implements AdsRequest {\n\n  private String adTagUrl;\n  private String adsResponse;\n  private Object userRequestContext;\n  private AdDisplayContainer adDisplayContainer;\n  private ContentProgressProvider contentProgressProvider;\n\n  @Override\n  public void setAdTagUrl(String adTagUrl) {\n    this.adTagUrl = adTagUrl;\n  }\n\n  @Override\n  public String getAdTagUrl() {\n    return adTagUrl;\n  }\n\n  @Override\n  public void setExtraParameter(String s, String s1) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public String getExtraParameter(String s) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public Map<String, String> getExtraParameters() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void setUserRequestContext(Object userRequestContext) {\n    this.userRequestContext = userRequestContext;\n  }\n\n  @Override\n  public Object getUserRequestContext() {\n    return userRequestContext;\n  }\n\n  @Override\n  public AdDisplayContainer getAdDisplayContainer() {\n    return adDisplayContainer;\n  }\n\n  @Override\n  public void setAdDisplayContainer(AdDisplayContainer adDisplayContainer) {\n    this.adDisplayContainer = adDisplayContainer;\n  }\n\n  @Override\n  public ContentProgressProvider getContentProgressProvider() {\n    return contentProgressProvider;\n  }\n\n  @Override\n  public void setContentProgressProvider(ContentProgressProvider contentProgressProvider) {\n    this.contentProgressProvider = contentProgressProvider;\n  }\n\n  @Override\n  public String getAdsResponse() {\n    return adsResponse;\n  }\n\n  @Override\n  public void setAdsResponse(String adsResponse) {\n    this.adsResponse = adsResponse;\n  }\n\n  @Override\n  public void setAdWillAutoPlay(boolean b) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void setAdWillPlayMuted(boolean b) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void setContentDuration(float v) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void setContentKeywords(List<String> list) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void setContentTitle(String s) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void setVastLoadTimeout(float v) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void setLiveStreamPrefetchSeconds(float v) {\n    throw new UnsupportedOperationException();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.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.ext.ima;\n\nimport android.os.Looper;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.testutil.StubExoPlayer;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport java.util.ArrayList;\n\n/** A fake player for testing content/ad playback. */\n/* package */ final class FakePlayer extends StubExoPlayer {\n\n  private final ArrayList<Player.EventListener> listeners;\n  private final Timeline.Period period;\n  private final Timeline timeline;\n\n  private boolean prepared;\n  private int state;\n  private boolean playWhenReady;\n  private long position;\n  private long contentPosition;\n  private boolean isPlayingAd;\n  private int adGroupIndex;\n  private int adIndexInAdGroup;\n\n  public FakePlayer() {\n    listeners = new ArrayList<>();\n    period = new Timeline.Period();\n    state = Player.STATE_IDLE;\n    playWhenReady = true;\n    timeline = Timeline.EMPTY;\n  }\n\n  /** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */\n  public void updateTimeline(Timeline timeline) {\n    for (Player.EventListener listener : listeners) {\n      listener.onTimelineChanged(\n          timeline,\n          null,\n          prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);\n    }\n    prepared = true;\n  }\n\n  /**\n   * Sets the state of this player as if it were playing content at the given {@code position}. If\n   * an ad is currently playing, this will trigger a position discontinuity.\n   */\n  public void setPlayingContentPosition(long position) {\n    boolean notify = isPlayingAd;\n    isPlayingAd = false;\n    adGroupIndex = C.INDEX_UNSET;\n    adIndexInAdGroup = C.INDEX_UNSET;\n    this.position = position;\n    contentPosition = position;\n    if (notify) {\n      for (Player.EventListener listener : listeners) {\n        listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION);\n      }\n    }\n  }\n\n  /**\n   * Sets the state of this player as if it were playing an ad with the given indices at the given\n   * {@code position}. If the player is playing a different ad or content, this will trigger a\n   * position discontinuity.\n   */\n  public void setPlayingAdPosition(\n      int adGroupIndex, int adIndexInAdGroup, long position, long contentPosition) {\n    boolean notify = !isPlayingAd || this.adIndexInAdGroup != adIndexInAdGroup;\n    isPlayingAd = true;\n    this.adGroupIndex = adGroupIndex;\n    this.adIndexInAdGroup = adIndexInAdGroup;\n    this.position = position;\n    this.contentPosition = contentPosition;\n    if (notify) {\n      for (Player.EventListener listener : listeners) {\n        listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION);\n      }\n    }\n  }\n\n  /** Sets the state of this player with the given {@code STATE} constant. */\n  public void setState(int state, boolean playWhenReady) {\n    boolean notify = this.state != state || this.playWhenReady != playWhenReady;\n    this.state = state;\n    this.playWhenReady = playWhenReady;\n    if (notify) {\n      for (Player.EventListener listener : listeners) {\n        listener.onPlayerStateChanged(playWhenReady, state);\n      }\n    }\n  }\n\n  // ExoPlayer methods. Other methods are unsupported.\n\n  @Override\n  public AudioComponent getAudioComponent() {\n    return null;\n  }\n\n  @Override\n  public Looper getApplicationLooper() {\n    return Looper.getMainLooper();\n  }\n\n  @Override\n  public void addListener(Player.EventListener listener) {\n    listeners.add(listener);\n  }\n\n  @Override\n  public void removeListener(Player.EventListener listener) {\n    listeners.remove(listener);\n  }\n\n  @Override\n  public int getPlaybackState() {\n    return state;\n  }\n\n  @Override\n  public boolean getPlayWhenReady() {\n    return playWhenReady;\n  }\n\n  @Override\n  public int getRendererCount() {\n    return 0;\n  }\n\n  @Override\n  public TrackSelectionArray getCurrentTrackSelections() {\n    return new TrackSelectionArray();\n  }\n\n  @Override\n  public Timeline getCurrentTimeline() {\n    return timeline;\n  }\n\n  @Override\n  public int getCurrentPeriodIndex() {\n    return 0;\n  }\n\n  @Override\n  public int getCurrentWindowIndex() {\n    return 0;\n  }\n\n  @Override\n  public long getDuration() {\n    if (timeline.isEmpty()) {\n      return C.INDEX_UNSET;\n    }\n    if (isPlayingAd()) {\n      long adDurationUs =\n          timeline.getPeriod(0, period).getAdDurationUs(adGroupIndex, adIndexInAdGroup);\n      return C.usToMs(adDurationUs);\n    } else {\n      return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();\n    }\n  }\n\n  @Override\n  public long getCurrentPosition() {\n    return position;\n  }\n\n  @Override\n  public boolean isPlayingAd() {\n    return isPlayingAd;\n  }\n\n  @Override\n  public int getCurrentAdGroupIndex() {\n    return adGroupIndex;\n  }\n\n  @Override\n  public int getCurrentAdIndexInAdGroup() {\n    return adIndexInAdGroup;\n  }\n\n  @Override\n  public long getContentPosition() {\n    return contentPosition;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.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.ext.ima;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.ads.interactivemedia.v3.api.Ad;\nimport com.google.ads.interactivemedia.v3.api.AdDisplayContainer;\nimport com.google.ads.interactivemedia.v3.api.AdEvent;\nimport com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType;\nimport com.google.ads.interactivemedia.v3.api.AdsManager;\nimport com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;\nimport com.google.ads.interactivemedia.v3.api.ImaSdkSettings;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.SinglePeriodTimeline;\nimport com.google.android.exoplayer2.source.ads.AdPlaybackState;\nimport com.google.android.exoplayer2.source.ads.AdsLoader;\nimport com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;\nimport com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Map;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.InOrder;\nimport org.mockito.Mock;\nimport org.mockito.MockitoAnnotations;\n\n/** Test for {@link ImaAdsLoader}. */\n@RunWith(AndroidJUnit4.class)\npublic class ImaAdsLoaderTest {\n\n  private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND;\n  private static final Timeline CONTENT_TIMELINE =\n      new SinglePeriodTimeline(CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false);\n  private static final Uri TEST_URI = Uri.EMPTY;\n  private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND;\n  private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}};\n  private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f};\n  private static final FakeAd UNSKIPPABLE_AD =\n      new FakeAd(/* skippable= */ false, /* podIndex= */ 0, /* totalAds= */ 1, /* adPosition= */ 1);\n\n  private @Mock ImaSdkSettings imaSdkSettings;\n  private @Mock AdsRenderingSettings adsRenderingSettings;\n  private @Mock AdDisplayContainer adDisplayContainer;\n  private @Mock AdsManager adsManager;\n  private SingletonImaFactory testImaFactory;\n  private ViewGroup adViewGroup;\n  private View adOverlayView;\n  private AdsLoader.AdViewProvider adViewProvider;\n  private TestAdsLoaderListener adsLoaderListener;\n  private FakePlayer fakeExoPlayer;\n  private ImaAdsLoader imaAdsLoader;\n\n  @Before\n  public void setUp() {\n    MockitoAnnotations.initMocks(this);\n    FakeAdsRequest fakeAdsRequest = new FakeAdsRequest();\n    FakeAdsLoader fakeAdsLoader = new FakeAdsLoader(imaSdkSettings, adsManager);\n    testImaFactory =\n        new SingletonImaFactory(\n            imaSdkSettings,\n            adsRenderingSettings,\n            adDisplayContainer,\n            fakeAdsRequest,\n            fakeAdsLoader);\n    adViewGroup = new FrameLayout(ApplicationProvider.getApplicationContext());\n    adOverlayView = new View(ApplicationProvider.getApplicationContext());\n    adViewProvider =\n        new AdsLoader.AdViewProvider() {\n          @Override\n          public ViewGroup getAdViewGroup() {\n            return adViewGroup;\n          }\n\n          @Override\n          public View[] getAdOverlayViews() {\n            return new View[] {adOverlayView};\n          }\n        };\n  }\n\n  @After\n  public void teardown() {\n    if (imaAdsLoader != null) {\n      imaAdsLoader.release();\n    }\n  }\n\n  @Test\n  public void testBuilder_overridesPlayerType() {\n    when(imaSdkSettings.getPlayerType()).thenReturn(\"test player type\");\n    setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);\n\n    verify(imaSdkSettings).setPlayerType(\"google/exo.ext.ima\");\n  }\n\n  @Test\n  public void testStart_setsAdUiViewGroup() {\n    setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);\n    imaAdsLoader.start(adsLoaderListener, adViewProvider);\n\n    verify(adDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup);\n    verify(adDisplayContainer, atLeastOnce()).registerVideoControlsOverlay(adOverlayView);\n  }\n\n  @Test\n  public void testStart_updatesAdPlaybackState() {\n    setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);\n    imaAdsLoader.start(adsLoaderListener, adViewProvider);\n\n    assertThat(adsLoaderListener.adPlaybackState)\n        .isEqualTo(\n            new AdPlaybackState(/* adGroupTimesUs= */ 0)\n                .withAdDurationsUs(PREROLL_ADS_DURATIONS_US)\n                .withContentDurationUs(CONTENT_DURATION_US));\n  }\n\n  @Test\n  public void testStartAfterRelease() {\n    setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);\n    imaAdsLoader.release();\n    imaAdsLoader.start(adsLoaderListener, adViewProvider);\n  }\n\n  @Test\n  public void testStartAndCallbacksAfterRelease() {\n    setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);\n    imaAdsLoader.release();\n    imaAdsLoader.start(adsLoaderListener, adViewProvider);\n    fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);\n    fakeExoPlayer.setState(Player.STATE_READY, true);\n\n    // If callbacks are invoked there is no crash.\n    // Note: we can't currently call getContentProgress/getAdProgress as a VerifyError is thrown\n    // when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA\n    // SDK being proguarded.\n    imaAdsLoader.requestAds(adViewGroup);\n    imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));\n    imaAdsLoader.loadAd(TEST_URI.toString());\n    imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));\n    imaAdsLoader.playAd();\n    imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, UNSKIPPABLE_AD));\n    imaAdsLoader.pauseAd();\n    imaAdsLoader.stopAd();\n    imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException()));\n    imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);\n    imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));\n    imaAdsLoader.handlePrepareError(\n        /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException());\n  }\n\n  @Test\n  public void testPlayback_withPrerollAd_marksAdAsPlayed() {\n    setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);\n\n    // Load the preroll ad.\n    imaAdsLoader.start(adsLoaderListener, adViewProvider);\n    imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));\n    imaAdsLoader.loadAd(TEST_URI.toString());\n    imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));\n\n    // Play the preroll ad.\n    imaAdsLoader.playAd();\n    fakeExoPlayer.setPlayingAdPosition(\n        /* adGroupIndex= */ 0,\n        /* adIndexInAdGroup= */ 0,\n        /* position= */ 0,\n        /* contentPosition= */ 0);\n    fakeExoPlayer.setState(Player.STATE_READY, true);\n    imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, UNSKIPPABLE_AD));\n    imaAdsLoader.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, UNSKIPPABLE_AD));\n    imaAdsLoader.onAdEvent(getAdEvent(AdEventType.MIDPOINT, UNSKIPPABLE_AD));\n    imaAdsLoader.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, UNSKIPPABLE_AD));\n\n    // Play the content.\n    fakeExoPlayer.setPlayingContentPosition(0);\n    imaAdsLoader.stopAd();\n    imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));\n\n    // Verify that the preroll ad has been marked as played.\n    assertThat(adsLoaderListener.adPlaybackState)\n        .isEqualTo(\n            new AdPlaybackState(/* adGroupTimesUs= */ 0)\n                .withContentDurationUs(CONTENT_DURATION_US)\n                .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)\n                .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI)\n                .withAdDurationsUs(PREROLL_ADS_DURATIONS_US)\n                .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)\n                .withAdResumePositionUs(/* adResumePositionUs= */ 0));\n  }\n\n  @Test\n  public void testStop_unregistersAllVideoControlOverlays() {\n    setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);\n    imaAdsLoader.start(adsLoaderListener, adViewProvider);\n    imaAdsLoader.requestAds(adViewGroup);\n    imaAdsLoader.stop();\n\n    InOrder inOrder = inOrder(adDisplayContainer);\n    inOrder.verify(adDisplayContainer).registerVideoControlsOverlay(adOverlayView);\n    inOrder.verify(adDisplayContainer).unregisterAllVideoControlsOverlays();\n  }\n\n  private void setupPlayback(Timeline contentTimeline, long[][] adDurationsUs, Float[] cuePoints) {\n    fakeExoPlayer = new FakePlayer();\n    adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs);\n    when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints));\n    imaAdsLoader =\n        new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())\n            .setImaFactory(testImaFactory)\n            .setImaSdkSettings(imaSdkSettings)\n            .buildForAdTag(TEST_URI);\n    imaAdsLoader.setPlayer(fakeExoPlayer);\n  }\n\n  private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) {\n    return new AdEvent() {\n      @Override\n      public AdEventType getType() {\n        return adEventType;\n      }\n\n      @Override\n      public @Nullable Ad getAd() {\n        return ad;\n      }\n\n      @Override\n      public Map<String, String> getAdData() {\n        return Collections.emptyMap();\n      }\n    };\n  }\n\n  /** Ad loader event listener that forwards ad playback state to a fake player. */\n  private static final class TestAdsLoaderListener implements AdsLoader.EventListener {\n\n    private final FakePlayer fakeExoPlayer;\n    private final Timeline contentTimeline;\n    private final long[][] adDurationsUs;\n\n    public AdPlaybackState adPlaybackState;\n\n    public TestAdsLoaderListener(\n        FakePlayer fakeExoPlayer, Timeline contentTimeline, long[][] adDurationsUs) {\n      this.fakeExoPlayer = fakeExoPlayer;\n      this.contentTimeline = contentTimeline;\n      this.adDurationsUs = adDurationsUs;\n    }\n\n    @Override\n    public void onAdPlaybackState(AdPlaybackState adPlaybackState) {\n      adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);\n      this.adPlaybackState = adPlaybackState;\n      fakeExoPlayer.updateTimeline(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState));\n    }\n\n    @Override\n    public void onAdLoadError(AdLoadException error, DataSpec dataSpec) {\n      assertThat(error.type).isNotEqualTo(AdLoadException.TYPE_UNEXPECTED);\n    }\n\n    @Override\n    public void onAdClicked() {\n      // Do nothing.\n    }\n\n    @Override\n    public void onAdTapped() {\n      // Do nothing.\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.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.ext.ima;\n\nimport android.content.Context;\nimport com.google.ads.interactivemedia.v3.api.AdDisplayContainer;\nimport com.google.ads.interactivemedia.v3.api.AdsLoader;\nimport com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;\nimport com.google.ads.interactivemedia.v3.api.AdsRequest;\nimport com.google.ads.interactivemedia.v3.api.ImaSdkSettings;\n\n/** {@link ImaAdsLoader.ImaFactory} that returns provided instances from each getter, for tests. */\nfinal class SingletonImaFactory implements ImaAdsLoader.ImaFactory {\n\n  private final ImaSdkSettings imaSdkSettings;\n  private final AdsRenderingSettings adsRenderingSettings;\n  private final AdDisplayContainer adDisplayContainer;\n  private final AdsRequest adsRequest;\n  private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;\n\n  public SingletonImaFactory(\n      ImaSdkSettings imaSdkSettings,\n      AdsRenderingSettings adsRenderingSettings,\n      AdDisplayContainer adDisplayContainer,\n      AdsRequest adsRequest,\n      com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader) {\n    this.imaSdkSettings = imaSdkSettings;\n    this.adsRenderingSettings = adsRenderingSettings;\n    this.adDisplayContainer = adDisplayContainer;\n    this.adsRequest = adsRequest;\n    this.adsLoader = adsLoader;\n  }\n\n  @Override\n  public ImaSdkSettings createImaSdkSettings() {\n    return imaSdkSettings;\n  }\n\n  @Override\n  public AdsRenderingSettings createAdsRenderingSettings() {\n    return adsRenderingSettings;\n  }\n\n  @Override\n  public AdDisplayContainer createAdDisplayContainer() {\n    return adDisplayContainer;\n  }\n\n  @Override\n  public AdsRequest createAdsRequest() {\n    return adsRequest;\n  }\n\n  @Override\n  public AdsLoader createAdsLoader(\n      Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {\n    return adsLoader;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/jobdispatcher/README.md",
    "content": "# ExoPlayer Firebase JobDispatcher extension #\n\n**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] instead.**\n\nThis extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].\n\n[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md\n[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java\n[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android\n\n## Getting the extension ##\n\nThe easiest way to use the extension is to add it as a gradle dependency:\n\n```gradle\nimplementation 'com.google.android.exoplayer:extension-jobdispatcher:2.X.X'\n```\n\nwhere `2.X.X` is the version, which must match the version of the ExoPlayer\nlibrary being used.\n\nAlternatively, you can clone the ExoPlayer repository and depend on the module\nlocally. Instructions for doing this can be found in ExoPlayer's\n[top level README][].\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/jobdispatcher/build.gradle",
    "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 */\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    implementation 'com.firebase:firebase-jobdispatcher:0.8.5'\n}\n\next {\n    javadocTitle = 'Firebase JobDispatcher extension'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'extension-jobdispatcher'\n    releaseDescription = 'Firebase JobDispatcher extension for ExoPlayer.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/jobdispatcher/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\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\n<manifest package=\"com.google.android.exoplayer2.ext.jobdispatcher\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.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.ext.jobdispatcher;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport com.firebase.jobdispatcher.Constraint;\nimport com.firebase.jobdispatcher.FirebaseJobDispatcher;\nimport com.firebase.jobdispatcher.GooglePlayDriver;\nimport com.firebase.jobdispatcher.Job;\nimport com.firebase.jobdispatcher.JobParameters;\nimport com.firebase.jobdispatcher.JobService;\nimport com.firebase.jobdispatcher.Lifetime;\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.Util;\n\n/**\n * A {@link Scheduler} that uses {@link FirebaseJobDispatcher}. To use this scheduler, you must add\n * {@link JobDispatcherSchedulerService} 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\n *     android:name=\"com.google.android.exoplayer2.ext.jobdispatcher.JobDispatcherScheduler$JobDispatcherSchedulerService\"\n *     android:exported=\"false\">\n *   <intent-filter>\n *     <action android:name=\"com.firebase.jobdispatcher.ACTION_EXECUTE\"/>\n *   </intent-filter>\n * </service>\n * }</pre>\n *\n * <p>This Scheduler uses Google Play services but does not do any availability checks. Any uses\n * should be guarded with a call to {@code\n * GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)}\n *\n * @see <a\n *     href=\"https://developers.google.com/android/reference/com/google/android/gms/common/GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)\">GoogleApiAvailability</a>\n * @deprecated Use com.google.android.exoplayer2.ext.workmanager.WorkManagerScheduler or {@link\n *     com.google.android.exoplayer2.scheduler.PlatformScheduler}.\n */\n@Deprecated\npublic final class JobDispatcherScheduler implements Scheduler {\n\n  private static final boolean DEBUG = false;\n  private static final String TAG = \"JobDispatcherScheduler\";\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 String jobTag;\n  private final FirebaseJobDispatcher jobDispatcher;\n\n  /**\n   * @param context A context.\n   * @param jobTag A tag for jobs scheduled by this instance. If the same tag was used by a previous\n   *     instance, anything scheduled by the previous instance will be canceled by this instance if\n   *     {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called.\n   */\n  public JobDispatcherScheduler(Context context, String jobTag) {\n    this.jobDispatcher =\n        new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext()));\n    this.jobTag = jobTag;\n  }\n\n  @Override\n  public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {\n    Job job = buildJob(jobDispatcher, requirements, jobTag, servicePackage, serviceAction);\n    int result = jobDispatcher.schedule(job);\n    logd(\"Scheduling job: \" + jobTag + \" result: \" + result);\n    return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;\n  }\n\n  @Override\n  public boolean cancel() {\n    int result = jobDispatcher.cancel(jobTag);\n    logd(\"Canceling job: \" + jobTag + \" result: \" + result);\n    return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;\n  }\n\n  private static Job buildJob(\n      FirebaseJobDispatcher dispatcher,\n      Requirements requirements,\n      String tag,\n      String servicePackage,\n      String serviceAction) {\n    Job.Builder builder =\n        dispatcher\n            .newJobBuilder()\n            .setService(JobDispatcherSchedulerService.class) // the JobService that will be called\n            .setTag(tag);\n\n    if (requirements.isUnmeteredNetworkRequired()) {\n      builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);\n    } else if (requirements.isNetworkRequired()) {\n      builder.addConstraint(Constraint.ON_ANY_NETWORK);\n    }\n\n    if (requirements.isIdleRequired()) {\n      builder.addConstraint(Constraint.DEVICE_IDLE);\n    }\n    if (requirements.isChargingRequired()) {\n      builder.addConstraint(Constraint.DEVICE_CHARGING);\n    }\n    builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true);\n\n    Bundle extras = new Bundle();\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 JobDispatcherSchedulerService extends JobService {\n    @Override\n    public boolean onStartJob(JobParameters params) {\n      logd(\"JobDispatcherSchedulerService is started\");\n      Bundle extras = params.getExtras();\n      Assertions.checkNotNull(extras, \"Service started without extras.\");\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        Assertions.checkNotNull(serviceAction, \"Service action missing.\");\n        Assertions.checkNotNull(servicePackage, \"Service package missing.\");\n        Intent intent = new Intent(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-amzn-2.10.6/extensions/leanback/README.md",
    "content": "# ExoPlayer Leanback extension #\n\nThis [Leanback][] Extension provides a [PlayerAdapter][] implementation for\nExoPlayer.\n\n[PlayerAdapter]: https://developer.android.com/reference/android/support/v17/leanback/media/PlayerAdapter.html\n[Leanback]: https://developer.android.com/reference/android/support/v17/leanback/package-summary.html\n\n## Getting the extension ##\n\nThe easiest way to use the extension is to add it as a gradle dependency:\n\n```gradle\nimplementation 'com.google.android.exoplayer:extension-leanback:2.X.X'\n```\n\nwhere `2.X.X` is the version, which must match the version of the ExoPlayer\nlibrary being used.\n\nAlternatively, you can clone the ExoPlayer repository and depend on the module\nlocally. Instructions for doing this can be found in ExoPlayer's\n[top level README][].\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n\n## Links ##\n\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.leanback.*`\n  belong to this module.\n\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/leanback/build.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.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion 17\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    // MOD: add custom lib\n    implementation project(':leanback-1.0.0')\n    implementation project(modulePrefix + 'library-core')\n    implementation 'androidx.annotation:annotation:1.1.0'\n    implementation 'androidx.leanback:leanback:1.0.0'\n}\n\next {\n    javadocTitle = 'Leanback extension'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'extension-leanback'\n    releaseDescription = 'Leanback extension for ExoPlayer.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/leanback/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n\n<manifest package=\"com.google.android.exoplayer2.ext.leanback\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.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.ext.leanback;\n\nimport android.content.Context;\nimport android.os.Handler;\nimport androidx.annotation.Nullable;\nimport androidx.leanback.R;\nimport androidx.leanback.media.PlaybackGlueHost;\nimport androidx.leanback.media.PlayerAdapter;\nimport androidx.leanback.media.SurfaceHolderGlueHost;\nimport android.util.Pair;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ControlDispatcher;\nimport com.google.android.exoplayer2.DefaultControlDispatcher;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.PlaybackPreparer;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Player.DiscontinuityReason;\nimport com.google.android.exoplayer2.Player.TimelineChangeReason;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.util.ErrorMessageProvider;\nimport com.google.android.exoplayer2.video.VideoListener;\n\n/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */\npublic final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnable {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.leanback\");\n  }\n\n  private final Context context;\n  private final Player player;\n  private final Handler handler;\n  private final ComponentListener componentListener;\n  private final int updatePeriodMs;\n\n  private @Nullable PlaybackPreparer playbackPreparer;\n  private ControlDispatcher controlDispatcher;\n  private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;\n  private @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost;\n  private boolean hasSurface;\n  private boolean lastNotifiedPreparedState;\n\n  /**\n   * Builds an instance. Note that the {@code PlayerAdapter} does not manage the lifecycle of the\n   * {@link Player} instance. The caller remains responsible for releasing the player when it's no\n   * longer required.\n   *\n   * @param context The current context (activity).\n   * @param player Instance of your exoplayer that needs to be configured.\n   * @param updatePeriodMs The delay between player control updates, in milliseconds.\n   */\n  public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) {\n    this.context = context;\n    this.player = player;\n    this.updatePeriodMs = updatePeriodMs;\n    handler = new Handler();\n    componentListener = new ComponentListener();\n    controlDispatcher = new DefaultControlDispatcher();\n  }\n\n  /**\n   * Sets the {@link PlaybackPreparer}.\n   *\n   * @param playbackPreparer The {@link PlaybackPreparer}.\n   */\n  public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {\n    this.playbackPreparer = playbackPreparer;\n  }\n\n  /**\n   * Sets the {@link ControlDispatcher}.\n   *\n   * @param controlDispatcher The {@link ControlDispatcher}, or null to use\n   *     {@link DefaultControlDispatcher}.\n   */\n  public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) {\n    this.controlDispatcher = controlDispatcher == null ? new DefaultControlDispatcher()\n        : controlDispatcher;\n  }\n\n  /**\n   * Sets the optional {@link ErrorMessageProvider}.\n   *\n   * @param errorMessageProvider The {@link ErrorMessageProvider}.\n   */\n  public void setErrorMessageProvider(\n      @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {\n    this.errorMessageProvider = errorMessageProvider;\n  }\n\n  // PlayerAdapter implementation.\n\n  @Override\n  public void onAttachedToHost(PlaybackGlueHost host) {\n    if (host instanceof SurfaceHolderGlueHost) {\n      surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host);\n      surfaceHolderGlueHost.setSurfaceHolderCallback(componentListener);\n    }\n    notifyStateChanged();\n    player.addListener(componentListener);\n    Player.VideoComponent videoComponent = player.getVideoComponent();\n    if (videoComponent != null) {\n      videoComponent.addVideoListener(componentListener);\n    }\n  }\n\n  @Override\n  public void onDetachedFromHost() {\n    player.removeListener(componentListener);\n    Player.VideoComponent videoComponent = player.getVideoComponent();\n    if (videoComponent != null) {\n      videoComponent.removeVideoListener(componentListener);\n    }\n    if (surfaceHolderGlueHost != null) {\n      removeSurfaceHolderCallback(surfaceHolderGlueHost);\n      surfaceHolderGlueHost = null;\n    }\n    hasSurface = false;\n    Callback callback = getCallback();\n    callback.onBufferingStateChanged(this, false);\n    callback.onPlayStateChanged(this);\n    maybeNotifyPreparedStateChanged(callback);\n  }\n\n  @Override\n  public void setProgressUpdatingEnabled(boolean enabled) {\n    handler.removeCallbacks(this);\n    if (enabled) {\n      handler.post(this);\n    }\n  }\n\n  @Override\n  public boolean isPlaying() {\n    int playbackState = player.getPlaybackState();\n    return playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED\n        && player.getPlayWhenReady();\n  }\n\n  @Override\n  public long getDuration() {\n    long durationMs = player.getDuration();\n    return durationMs == C.TIME_UNSET ? -1 : durationMs;\n  }\n\n  @Override\n  public long getCurrentPosition() {\n    return player.getPlaybackState() == Player.STATE_IDLE ? -1 : player.getCurrentPosition();\n  }\n\n  @Override\n  public void play() {\n    if (player.getPlaybackState() == Player.STATE_IDLE) {\n      if (playbackPreparer != null) {\n        playbackPreparer.preparePlayback();\n      }\n    } else if (player.getPlaybackState() == Player.STATE_ENDED) {\n      controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);\n    }\n    if (controlDispatcher.dispatchSetPlayWhenReady(player, true)) {\n      getCallback().onPlayStateChanged(this);\n    }\n  }\n\n  @Override\n  public void pause() {\n    if (controlDispatcher.dispatchSetPlayWhenReady(player, false)) {\n      getCallback().onPlayStateChanged(this);\n    }\n  }\n\n  @Override\n  public void seekTo(long positionMs) {\n    controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), positionMs);\n  }\n\n  @Override\n  public long getBufferedPosition() {\n    return player.getBufferedPosition();\n  }\n\n  @Override\n  public boolean isPrepared() {\n    return player.getPlaybackState() != Player.STATE_IDLE\n        && (surfaceHolderGlueHost == null || hasSurface);\n  }\n\n  // Runnable implementation.\n\n  @Override\n  public void run() {\n    Callback callback = getCallback();\n    callback.onCurrentPositionChanged(this);\n    callback.onBufferedPositionChanged(this);\n    handler.postDelayed(this, updatePeriodMs);\n  }\n\n  // Internal methods.\n\n  /* package */ void setVideoSurface(@Nullable Surface surface) {\n    hasSurface = surface != null;\n    Player.VideoComponent videoComponent = player.getVideoComponent();\n    if (videoComponent != null) {\n      videoComponent.setVideoSurface(surface);\n    }\n    maybeNotifyPreparedStateChanged(getCallback());\n  }\n\n  /* package */ void notifyStateChanged() {\n    int playbackState = player.getPlaybackState();\n    Callback callback = getCallback();\n    maybeNotifyPreparedStateChanged(callback);\n    callback.onPlayStateChanged(this);\n    callback.onBufferingStateChanged(this, playbackState == Player.STATE_BUFFERING);\n    if (playbackState == Player.STATE_ENDED) {\n      callback.onPlayCompleted(this);\n    }\n  }\n\n  private void maybeNotifyPreparedStateChanged(Callback callback) {\n    boolean isPrepared = isPrepared();\n    if (lastNotifiedPreparedState != isPrepared) {\n      lastNotifiedPreparedState = isPrepared;\n      callback.onPreparedStateChanged(this);\n    }\n  }\n\n  @SuppressWarnings(\"nullness:argument.type.incompatible\")\n  private static void removeSurfaceHolderCallback(SurfaceHolderGlueHost surfaceHolderGlueHost) {\n    surfaceHolderGlueHost.setSurfaceHolderCallback(null);\n  }\n\n  private final class ComponentListener\n      implements Player.EventListener, SurfaceHolder.Callback, VideoListener {\n\n    // SurfaceHolder.Callback implementation.\n\n    @Override\n    public void surfaceCreated(SurfaceHolder surfaceHolder) {\n      setVideoSurface(surfaceHolder.getSurface());\n    }\n\n    @Override\n    public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {\n      // Do nothing.\n    }\n\n    @Override\n    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {\n      setVideoSurface(null);\n    }\n\n    // Player.EventListener implementation.\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n      notifyStateChanged();\n    }\n\n    @Override\n    public void onPlayerError(ExoPlaybackException exception) {\n      Callback callback = getCallback();\n      if (errorMessageProvider != null) {\n        Pair<Integer, String> errorMessage = errorMessageProvider.getErrorMessage(exception);\n        callback.onError(LeanbackPlayerAdapter.this, errorMessage.first, errorMessage.second);\n      } else {\n        callback.onError(LeanbackPlayerAdapter.this, exception.type, context.getString(\n            R.string.lb_media_player_error, exception.type, exception.rendererIndex));\n      }\n    }\n\n    @Override\n    public void onTimelineChanged(\n        Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {\n      Callback callback = getCallback();\n      callback.onDurationChanged(LeanbackPlayerAdapter.this);\n      callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);\n      callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);\n    }\n\n    @Override\n    public void onPositionDiscontinuity(@DiscontinuityReason int reason) {\n      Callback callback = getCallback();\n      callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);\n      callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);\n    }\n\n    // VideoListener implementation.\n\n    @Override\n    public void onVideoSizeChanged(\n        int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {\n      getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height);\n    }\n\n    @Override\n    public void onRenderedFirstFrame() {\n      // Do nothing.\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/README.md",
    "content": "# ExoPlayer MediaSession extension #\n\nThe MediaSession extension mediates between a Player (or ExoPlayer) instance\nand a [MediaSession][]. It automatically retrieves and implements playback\nactions and syncs the player state with the state of the media session. The\nbehaviour can be extended to support other playback and custom actions.\n\n[MediaSession]: https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat.html\n\n## Getting the extension ##\n\nThe easiest way to use the extension is to add it as a gradle dependency:\n\n```gradle\nimplementation 'com.google.android.exoplayer:extension-mediasession:2.X.X'\n```\n\nwhere `2.X.X` is the version, which must match the version of the ExoPlayer\nlibrary being used.\n\nAlternatively, you can clone the ExoPlayer repository and depend on the module\nlocally. Instructions for doing this can be found in ExoPlayer's\n[top level README][].\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n\n## Links ##\n\n* [Javadoc][]: Classes matching\n  `com.google.android.exoplayer2.ext.mediasession.*` belong to this module.\n\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/build.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.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    api 'androidx.media:media:1.0.1'\n}\n\next {\n    javadocTitle = 'Media session extension'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'extension-mediasession'\n    releaseDescription = 'Media session extension for ExoPlayer.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n\n<manifest package=\"com.google.android.exoplayer2.ext.mediasession\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.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.ext.mediasession;\n\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.ResultReceiver;\nimport android.os.SystemClock;\nimport androidx.annotation.LongDef;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport android.support.v4.media.MediaDescriptionCompat;\nimport android.support.v4.media.MediaMetadataCompat;\nimport android.support.v4.media.RatingCompat;\nimport android.support.v4.media.session.MediaControllerCompat;\nimport android.support.v4.media.session.MediaSessionCompat;\nimport android.support.v4.media.session.PlaybackStateCompat;\nimport android.util.Pair;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ControlDispatcher;\nimport com.google.android.exoplayer2.DefaultControlDispatcher;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.ErrorMessageProvider;\nimport com.google.android.exoplayer2.util.RepeatModeUtil;\nimport com.google.android.exoplayer2.util.Util;\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;\n\n/**\n * Connects a {@link MediaSessionCompat} to a {@link Player}.\n *\n * <p>This connector does <em>not</em> call {@link MediaSessionCompat#setActive(boolean)}, and so\n * application code is responsible for making the session active when desired. A session must be\n * active for transport controls to be displayed (e.g. on the lock screen) and for it to receive\n * media button events.\n *\n * <p>The connector listens for actions sent by the media session's controller and implements these\n * actions by calling appropriate player methods. The playback state of the media session is\n * automatically synced with the player. The connector can also be optionally extended by providing\n * various collaborators:\n *\n * <ul>\n *   <li>Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and {@code\n *       PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed to\n *       {@link #setPlaybackPreparer(PlaybackPreparer)}.\n *   <li>Custom actions can be handled by passing one or more {@link CustomActionProvider}s to\n *       {@link #setCustomActionProviders(CustomActionProvider...)}.\n *   <li>To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by\n *       calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator}\n *       is recommended for most use cases.\n *   <li>To enable editing of the media queue, you can set a {@link QueueEditor} by calling {@link\n *       #setQueueEditor(QueueEditor)}.\n *   <li>A {@link MediaButtonEventHandler} can be set by calling {@link\n *       #setMediaButtonEventHandler(MediaButtonEventHandler)}. By default media button events are\n *       handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.\n *   <li>An {@link ErrorMessageProvider} for providing human readable error messages and\n *       corresponding error codes can be set by calling {@link\n *       #setErrorMessageProvider(ErrorMessageProvider)}.\n *   <li>A {@link MediaMetadataProvider} can be set by calling {@link\n *       #setMediaMetadataProvider(MediaMetadataProvider)}. By default the {@link\n *       DefaultMediaMetadataProvider} is used.\n * </ul>\n */\npublic final class MediaSessionConnector {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.mediasession\");\n  }\n\n  /** Playback actions supported by the connector. */\n  @LongDef(\n      flag = true,\n      value = {\n        PlaybackStateCompat.ACTION_PLAY_PAUSE,\n        PlaybackStateCompat.ACTION_PLAY,\n        PlaybackStateCompat.ACTION_PAUSE,\n        PlaybackStateCompat.ACTION_SEEK_TO,\n        PlaybackStateCompat.ACTION_FAST_FORWARD,\n        PlaybackStateCompat.ACTION_REWIND,\n        PlaybackStateCompat.ACTION_STOP,\n        PlaybackStateCompat.ACTION_SET_REPEAT_MODE,\n        PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE\n      })\n  @Retention(RetentionPolicy.SOURCE)\n  public @interface PlaybackActions {}\n\n  @PlaybackActions\n  public static final long ALL_PLAYBACK_ACTIONS =\n      PlaybackStateCompat.ACTION_PLAY_PAUSE\n          | PlaybackStateCompat.ACTION_PLAY\n          | PlaybackStateCompat.ACTION_PAUSE\n          | PlaybackStateCompat.ACTION_SEEK_TO\n          | PlaybackStateCompat.ACTION_FAST_FORWARD\n          | PlaybackStateCompat.ACTION_REWIND\n          | PlaybackStateCompat.ACTION_STOP\n          | PlaybackStateCompat.ACTION_SET_REPEAT_MODE\n          | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;\n\n  /** The default playback actions. */\n  @PlaybackActions public static final long DEFAULT_PLAYBACK_ACTIONS = ALL_PLAYBACK_ACTIONS;\n\n  /** The default fast forward increment, in milliseconds. */\n  public static final int DEFAULT_FAST_FORWARD_MS = 15000;\n  /** The default rewind increment, in milliseconds. */\n  public static final int DEFAULT_REWIND_MS = 5000;\n\n  /**\n   * The name of the {@link PlaybackStateCompat} float extra with the value of {@link\n   * PlaybackParameters#speed}.\n   */\n  public static final String EXTRAS_SPEED = \"EXO_SPEED\";\n  /**\n   * The name of the {@link PlaybackStateCompat} float extra with the value of {@link\n   * PlaybackParameters#pitch}.\n   */\n  public static final String EXTRAS_PITCH = \"EXO_PITCH\";\n\n  private static final long BASE_PLAYBACK_ACTIONS =\n      PlaybackStateCompat.ACTION_PLAY_PAUSE\n          | PlaybackStateCompat.ACTION_PLAY\n          | PlaybackStateCompat.ACTION_PAUSE\n          | PlaybackStateCompat.ACTION_STOP\n          | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE\n          | PlaybackStateCompat.ACTION_SET_REPEAT_MODE;\n  private static final int BASE_MEDIA_SESSION_FLAGS =\n      MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS\n          | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS;\n  private static final int EDITOR_MEDIA_SESSION_FLAGS =\n      BASE_MEDIA_SESSION_FLAGS | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;\n\n  private static final MediaMetadataCompat METADATA_EMPTY =\n      new MediaMetadataCompat.Builder().build();\n\n  /** Receiver of media commands sent by a media controller. */\n  public interface CommandReceiver {\n    /**\n     * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. The\n     * receiver may handle the command, but is not required to do so. Changes to the player should\n     * be made via the {@link ControlDispatcher}.\n     *\n     * @param player The player connected to the media session.\n     * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching\n     *     changes to the player.\n     * @param command The command name.\n     * @param extras Optional parameters for the command, may be null.\n     * @param cb A result receiver to which a result may be sent by the command, may be null.\n     * @return Whether the receiver handled the command.\n     */\n    boolean onCommand(\n        Player player,\n        ControlDispatcher controlDispatcher,\n        String command,\n        Bundle extras,\n        ResultReceiver cb);\n  }\n\n  /** Interface to which playback preparation and play actions are delegated. */\n  public interface PlaybackPreparer extends CommandReceiver {\n\n    long ACTIONS =\n        PlaybackStateCompat.ACTION_PREPARE\n            | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID\n            | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH\n            | PlaybackStateCompat.ACTION_PREPARE_FROM_URI\n            | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID\n            | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH\n            | PlaybackStateCompat.ACTION_PLAY_FROM_URI;\n\n    /**\n     * Returns the actions which are supported by the preparer. The supported actions must be a\n     * bitmask combined out of {@link PlaybackStateCompat#ACTION_PREPARE}, {@link\n     * PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}, {@link\n     * PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}, {@link\n     * PlaybackStateCompat#ACTION_PREPARE_FROM_URI}, {@link\n     * PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID}, {@link\n     * PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH} and {@link\n     * PlaybackStateCompat#ACTION_PLAY_FROM_URI}.\n     *\n     * @return The bitmask of the supported media actions.\n     */\n    long getSupportedPrepareActions();\n    /**\n     * See {@link MediaSessionCompat.Callback#onPrepare()}.\n     *\n     * @param playWhenReady Whether playback should be started after preparation.\n     */\n    void onPrepare(boolean playWhenReady);\n    /**\n     * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.\n     *\n     * @param mediaId The media id of the media item to be prepared.\n     * @param playWhenReady Whether playback should be started after preparation.\n     * @param extras A {@link Bundle} of extras passed by the media controller.\n     */\n    void onPrepareFromMediaId(String mediaId, boolean playWhenReady, Bundle extras);\n    /**\n     * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.\n     *\n     * @param query The search query.\n     * @param playWhenReady Whether playback should be started after preparation.\n     * @param extras A {@link Bundle} of extras passed by the media controller.\n     */\n    void onPrepareFromSearch(String query, boolean playWhenReady, Bundle extras);\n    /**\n     * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.\n     *\n     * @param uri The {@link Uri} of the media item to be prepared.\n     * @param playWhenReady Whether playback should be started after preparation.\n     * @param extras A {@link Bundle} of extras passed by the media controller.\n     */\n    void onPrepareFromUri(Uri uri, boolean playWhenReady, Bundle extras);\n  }\n\n  /**\n   * Handles queue navigation actions, and updates the media session queue by calling {@code\n   * MediaSessionCompat.setQueue()}.\n   */\n  public interface QueueNavigator extends CommandReceiver {\n\n    long ACTIONS =\n        PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM\n            | PlaybackStateCompat.ACTION_SKIP_TO_NEXT\n            | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;\n\n    /**\n     * Returns the actions which are supported by the navigator. The supported actions must be a\n     * bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}, {@link\n     * PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, {@link\n     * PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}.\n     *\n     * @param player The player connected to the media session.\n     * @return The bitmask of the supported media actions.\n     */\n    long getSupportedQueueNavigatorActions(Player player);\n    /**\n     * Called when the timeline of the player has changed.\n     *\n     * @param player The player connected to the media session.\n     */\n    void onTimelineChanged(Player player);\n    /**\n     * Called when the current window index changed.\n     *\n     * @param player The player connected to the media session.\n     */\n    void onCurrentWindowIndexChanged(Player player);\n    /**\n     * Gets the id of the currently active queue item, or {@link\n     * MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown.\n     *\n     * <p>To let the connector publish metadata for the active queue item, the queue item with the\n     * returned id must be available in the list of items returned by {@link\n     * MediaControllerCompat#getQueue()}.\n     *\n     * @param player The player connected to the media session.\n     * @return The id of the active queue item.\n     */\n    long getActiveQueueItemId(@Nullable Player player);\n    /**\n     * See {@link MediaSessionCompat.Callback#onSkipToPrevious()}.\n     *\n     * @param player The player connected to the media session.\n     * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching\n     *     changes to the player.\n     */\n    void onSkipToPrevious(Player player, ControlDispatcher controlDispatcher);\n    /**\n     * See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}.\n     *\n     * @param player The player connected to the media session.\n     * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching\n     *     changes to the player.\n     */\n    void onSkipToQueueItem(Player player, ControlDispatcher controlDispatcher, long id);\n    /**\n     * See {@link MediaSessionCompat.Callback#onSkipToNext()}.\n     *\n     * @param player The player connected to the media session.\n     * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching\n     *     changes to the player.\n     */\n    void onSkipToNext(Player player, ControlDispatcher controlDispatcher);\n  }\n\n  /** Handles media session queue edits. */\n  public interface QueueEditor extends CommandReceiver {\n\n    /**\n     * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description)}.\n     */\n    void onAddQueueItem(Player player, MediaDescriptionCompat description);\n    /**\n     * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description, int\n     * index)}.\n     */\n    void onAddQueueItem(Player player, MediaDescriptionCompat description, int index);\n    /**\n     * See {@link MediaSessionCompat.Callback#onRemoveQueueItem(MediaDescriptionCompat\n     * description)}.\n     */\n    void onRemoveQueueItem(Player player, MediaDescriptionCompat description);\n  }\n\n  /** Callback receiving a user rating for the active media item. */\n  public interface RatingCallback extends CommandReceiver {\n\n    /** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat)}. */\n    void onSetRating(Player player, RatingCompat rating);\n\n    /** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat, Bundle)}. */\n    void onSetRating(Player player, RatingCompat rating, Bundle extras);\n  }\n\n  /** Handles a media button event. */\n  public interface MediaButtonEventHandler {\n    /**\n     * See {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.\n     *\n     * @param player The {@link Player}.\n     * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching\n     *     changes to the player.\n     * @param mediaButtonEvent The {@link Intent}.\n     * @return True if the event was handled, false otherwise.\n     */\n    boolean onMediaButtonEvent(\n        Player player, ControlDispatcher controlDispatcher, Intent mediaButtonEvent);\n  }\n\n  /**\n   * Provides a {@link PlaybackStateCompat.CustomAction} to be published and handles the action when\n   * sent by a media controller.\n   */\n  public interface CustomActionProvider {\n    /**\n     * Called when a custom action provided by this provider is sent to the media session.\n     *\n     * @param player The player connected to the media session.\n     * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching\n     *     changes to the player.\n     * @param action The name of the action which was sent by a media controller.\n     * @param extras Optional extras sent by a media controller.\n     */\n    void onCustomAction(\n        Player player, ControlDispatcher controlDispatcher, String action, Bundle extras);\n\n    /**\n     * Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media\n     * session by the connector or {@code null} if this action should not be published at the given\n     * player state.\n     *\n     * @param player The player connected to the media session.\n     * @return The custom action to be included in the session playback state or {@code null}.\n     */\n    PlaybackStateCompat.CustomAction getCustomAction(Player player);\n  }\n\n  /** Provides a {@link MediaMetadataCompat} for a given player state. */\n  public interface MediaMetadataProvider {\n    /**\n     * Gets the {@link MediaMetadataCompat} to be published to the session.\n     *\n     * <p>An app may need to load metadata resources like artwork bitmaps asynchronously. In such a\n     * case the app should return a {@link MediaMetadataCompat} object that does not contain these\n     * resources as a placeholder. The app should start an asynchronous operation to download the\n     * bitmap and put it into a cache. Finally, the app should call {@link\n     * #invalidateMediaSessionMetadata()}. This causes this callback to be called again and the app\n     * can now return a {@link MediaMetadataCompat} object with all the resources included.\n     *\n     * @param player The player connected to the media session.\n     * @return The {@link MediaMetadataCompat} to be published to the session.\n     */\n    MediaMetadataCompat getMetadata(Player player);\n  }\n\n  /** The wrapped {@link MediaSessionCompat}. */\n  public final MediaSessionCompat mediaSession;\n\n  private final Looper looper;\n  private final ComponentListener componentListener;\n  private final ArrayList<CommandReceiver> commandReceivers;\n  private final ArrayList<CommandReceiver> customCommandReceivers;\n\n  private ControlDispatcher controlDispatcher;\n  private CustomActionProvider[] customActionProviders;\n  private Map<String, CustomActionProvider> customActionMap;\n  @Nullable private MediaMetadataProvider mediaMetadataProvider;\n  @Nullable private Player player;\n  @Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;\n  @Nullable private Pair<Integer, CharSequence> customError;\n  @Nullable private Bundle customErrorExtras;\n  @Nullable private PlaybackPreparer playbackPreparer;\n  @Nullable private QueueNavigator queueNavigator;\n  @Nullable private QueueEditor queueEditor;\n  @Nullable private RatingCallback ratingCallback;\n  @Nullable private MediaButtonEventHandler mediaButtonEventHandler;\n\n  private long enabledPlaybackActions;\n  private int rewindMs;\n  private int fastForwardMs;\n\n  /**\n   * Creates an instance.\n   *\n   * @param mediaSession The {@link MediaSessionCompat} to connect to.\n   */\n  public MediaSessionConnector(MediaSessionCompat mediaSession) {\n    this.mediaSession = mediaSession;\n    looper = Util.getLooper();\n    componentListener = new ComponentListener();\n    commandReceivers = new ArrayList<>();\n    customCommandReceivers = new ArrayList<>();\n    controlDispatcher = new DefaultControlDispatcher();\n    customActionProviders = new CustomActionProvider[0];\n    customActionMap = Collections.emptyMap();\n    mediaMetadataProvider =\n        new DefaultMediaMetadataProvider(\n            mediaSession.getController(), /* metadataExtrasPrefix= */ null);\n    enabledPlaybackActions = DEFAULT_PLAYBACK_ACTIONS;\n    rewindMs = DEFAULT_REWIND_MS;\n    fastForwardMs = DEFAULT_FAST_FORWARD_MS;\n    mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);\n    mediaSession.setCallback(componentListener, new Handler(looper));\n  }\n\n  /**\n   * Sets the player to be connected to the media session. Must be called on the same thread that is\n   * used to access the player.\n   *\n   * @param player The player to be connected to the {@code MediaSession}, or {@code null} to\n   *     disconnect the current player.\n   */\n  public void setPlayer(@Nullable Player player) {\n    Assertions.checkArgument(player == null || player.getApplicationLooper() == looper);\n    if (this.player != null) {\n      this.player.removeListener(componentListener);\n    }\n    this.player = player;\n    if (player != null) {\n      player.addListener(componentListener);\n    }\n    invalidateMediaSessionPlaybackState();\n    invalidateMediaSessionMetadata();\n  }\n\n  /**\n   * Sets the {@link PlaybackPreparer}.\n   *\n   * @param playbackPreparer The {@link PlaybackPreparer}.\n   */\n  public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {\n    if (this.playbackPreparer != playbackPreparer) {\n      unregisterCommandReceiver(this.playbackPreparer);\n      this.playbackPreparer = playbackPreparer;\n      registerCommandReceiver(playbackPreparer);\n      invalidateMediaSessionPlaybackState();\n    }\n  }\n\n  /**\n   * Sets the {@link ControlDispatcher}.\n   *\n   * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link\n   *     DefaultControlDispatcher}.\n   */\n  public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) {\n    if (this.controlDispatcher != controlDispatcher) {\n      this.controlDispatcher =\n          controlDispatcher == null ? new DefaultControlDispatcher() : controlDispatcher;\n    }\n  }\n\n  /**\n   * Sets the {@link MediaButtonEventHandler}. Pass {@code null} if the media button event should be\n   * handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.\n   *\n   * @param mediaButtonEventHandler The {@link MediaButtonEventHandler}, or null to let the event be\n   *     handled by {@link MediaSessionCompat.Callback#onMediaButtonEvent(Intent)}.\n   */\n  public void setMediaButtonEventHandler(\n      @Nullable MediaButtonEventHandler mediaButtonEventHandler) {\n    this.mediaButtonEventHandler = mediaButtonEventHandler;\n  }\n\n  /**\n   * Sets the enabled playback actions.\n   *\n   * @param enabledPlaybackActions The enabled playback actions.\n   */\n  public void setEnabledPlaybackActions(@PlaybackActions long enabledPlaybackActions) {\n    enabledPlaybackActions &= ALL_PLAYBACK_ACTIONS;\n    if (this.enabledPlaybackActions != enabledPlaybackActions) {\n      this.enabledPlaybackActions = enabledPlaybackActions;\n      invalidateMediaSessionPlaybackState();\n    }\n  }\n\n  /**\n   * Sets the rewind increment in milliseconds.\n   *\n   * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the\n   *     rewind button to be disabled.\n   */\n  public void setRewindIncrementMs(int rewindMs) {\n    if (this.rewindMs != rewindMs) {\n      this.rewindMs = rewindMs;\n      invalidateMediaSessionPlaybackState();\n    }\n  }\n\n  /**\n   * Sets the fast forward increment in milliseconds.\n   *\n   * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will\n   *     cause the fast forward button to be disabled.\n   */\n  public void setFastForwardIncrementMs(int fastForwardMs) {\n    if (this.fastForwardMs != fastForwardMs) {\n      this.fastForwardMs = fastForwardMs;\n      invalidateMediaSessionPlaybackState();\n    }\n  }\n\n  /**\n   * Sets the optional {@link ErrorMessageProvider}.\n   *\n   * @param errorMessageProvider The error message provider.\n   */\n  public void setErrorMessageProvider(\n      @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {\n    if (this.errorMessageProvider != errorMessageProvider) {\n      this.errorMessageProvider = errorMessageProvider;\n      invalidateMediaSessionPlaybackState();\n    }\n  }\n\n  /**\n   * Sets the {@link QueueNavigator} to handle queue navigation actions {@code ACTION_SKIP_TO_NEXT},\n   * {@code ACTION_SKIP_TO_PREVIOUS} and {@code ACTION_SKIP_TO_QUEUE_ITEM}.\n   *\n   * @param queueNavigator The queue navigator.\n   */\n  public void setQueueNavigator(QueueNavigator queueNavigator) {\n    if (this.queueNavigator != queueNavigator) {\n      unregisterCommandReceiver(this.queueNavigator);\n      this.queueNavigator = queueNavigator;\n      registerCommandReceiver(queueNavigator);\n    }\n  }\n\n  /**\n   * Sets the {@link QueueEditor} to handle queue edits sent by the media controller.\n   *\n   * @param queueEditor The queue editor.\n   */\n  public void setQueueEditor(QueueEditor queueEditor) {\n    if (this.queueEditor != queueEditor) {\n      unregisterCommandReceiver(this.queueEditor);\n      this.queueEditor = queueEditor;\n      registerCommandReceiver(queueEditor);\n      mediaSession.setFlags(\n          queueEditor == null ? BASE_MEDIA_SESSION_FLAGS : EDITOR_MEDIA_SESSION_FLAGS);\n    }\n  }\n\n  /**\n   * Sets the {@link RatingCallback} to handle user ratings.\n   *\n   * @param ratingCallback The rating callback.\n   */\n  public void setRatingCallback(RatingCallback ratingCallback) {\n    if (this.ratingCallback != ratingCallback) {\n      unregisterCommandReceiver(this.ratingCallback);\n      this.ratingCallback = ratingCallback;\n      registerCommandReceiver(this.ratingCallback);\n    }\n  }\n\n  /**\n   * Sets a custom error on the session.\n   *\n   * <p>This sets the error code via {@link PlaybackStateCompat.Builder#setErrorMessage(int,\n   * CharSequence)}. By default, the error code will be set to {@link\n   * PlaybackStateCompat#ERROR_CODE_APP_ERROR}.\n   *\n   * @param message The error string to report or {@code null} to clear the error.\n   */\n  public void setCustomErrorMessage(@Nullable CharSequence message) {\n    int code = (message == null) ? 0 : PlaybackStateCompat.ERROR_CODE_APP_ERROR;\n    setCustomErrorMessage(message, code);\n  }\n\n  /**\n   * Sets a custom error on the session.\n   *\n   * @param message The error string to report or {@code null} to clear the error.\n   * @param code The error code to report. Ignored when {@code message} is {@code null}.\n   */\n  public void setCustomErrorMessage(@Nullable CharSequence message, int code) {\n    setCustomErrorMessage(message, code, /* extras= */ null);\n  }\n\n  /**\n   * Sets a custom error on the session.\n   *\n   * @param message The error string to report or {@code null} to clear the error.\n   * @param code The error code to report. Ignored when {@code message} is {@code null}.\n   * @param extras Extras to include in reported {@link PlaybackStateCompat}.\n   */\n  public void setCustomErrorMessage(\n      @Nullable CharSequence message, int code, @Nullable Bundle extras) {\n    customError = (message == null) ? null : new Pair<>(code, message);\n    customErrorExtras = (message == null) ? null : extras;\n    invalidateMediaSessionPlaybackState();\n  }\n\n  /**\n   * Sets custom action providers. The order of the {@link CustomActionProvider}s determines the\n   * order in which the actions are published.\n   *\n   * @param customActionProviders The custom action providers, or null to remove all existing custom\n   *     action providers.\n   */\n  public void setCustomActionProviders(@Nullable CustomActionProvider... customActionProviders) {\n    this.customActionProviders =\n        customActionProviders == null ? new CustomActionProvider[0] : customActionProviders;\n    invalidateMediaSessionPlaybackState();\n  }\n\n  /**\n   * Sets a provider of metadata to be published to the media session. Pass {@code null} if no\n   * metadata should be published.\n   *\n   * @param mediaMetadataProvider The provider of metadata to publish, or {@code null} if no\n   *     metadata should be published.\n   */\n  public void setMediaMetadataProvider(@Nullable MediaMetadataProvider mediaMetadataProvider) {\n    if (this.mediaMetadataProvider != mediaMetadataProvider) {\n      this.mediaMetadataProvider = mediaMetadataProvider;\n      invalidateMediaSessionMetadata();\n    }\n  }\n\n  /**\n   * Updates the metadata of the media session.\n   *\n   * <p>Apps normally only need to call this method when the backing data for a given media item has\n   * changed and the metadata should be updated immediately.\n   *\n   * <p>The {@link MediaMetadataCompat} which is published to the session is obtained by calling\n   * {@link MediaMetadataProvider#getMetadata(Player)}.\n   */\n  public final void invalidateMediaSessionMetadata() {\n    // MODIFIED: disable notifications if no provider is set\n    if (mediaMetadataProvider == null) {\n      return;\n    }\n\n    MediaMetadataCompat metadata =\n        mediaMetadataProvider != null && player != null\n            ? mediaMetadataProvider.getMetadata(player)\n            : METADATA_EMPTY;\n    // MOD: fix NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference\n    try {\n      mediaSession.setMetadata(metadata != null ? metadata : METADATA_EMPTY);\n    } catch (NullPointerException e) {\n      e.printStackTrace();\n    }\n  }\n\n  /**\n   * Updates the playback state of the media session.\n   *\n   * <p>Apps normally only need to call this method when the custom actions provided by a {@link\n   * CustomActionProvider} changed and the playback state needs to be updated immediately.\n   */\n  public final void invalidateMediaSessionPlaybackState() {\n    PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();\n    if (player == null) {\n      builder\n          .setActions(buildPrepareActions())\n          .setState(\n              PlaybackStateCompat.STATE_NONE,\n              /* position= */ 0,\n              /* playbackSpeed= */ 0,\n              /* updateTime= */ SystemClock.elapsedRealtime());\n      mediaSession.setPlaybackState(builder.build());\n      return;\n    }\n\n    Map<String, CustomActionProvider> currentActions = new HashMap<>();\n    for (CustomActionProvider customActionProvider : customActionProviders) {\n      PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction(player);\n      if (customAction != null) {\n        currentActions.put(customAction.getAction(), customActionProvider);\n        builder.addCustomAction(customAction);\n      }\n    }\n    customActionMap = Collections.unmodifiableMap(currentActions);\n\n    Bundle extras = new Bundle();\n    @Nullable ExoPlaybackException playbackError = player.getPlaybackError();\n    boolean reportError = playbackError != null || customError != null;\n    int sessionPlaybackState =\n        reportError\n            ? PlaybackStateCompat.STATE_ERROR\n            : getMediaSessionPlaybackState(player.getPlaybackState(), player.getPlayWhenReady());\n    if (customError != null) {\n      builder.setErrorMessage(customError.first, customError.second);\n      if (customErrorExtras != null) {\n        extras.putAll(customErrorExtras);\n      }\n    } else if (playbackError != null && errorMessageProvider != null) {\n      Pair<Integer, String> message = errorMessageProvider.getErrorMessage(playbackError);\n      builder.setErrorMessage(message.first, message.second);\n    }\n    long activeQueueItemId =\n        queueNavigator != null\n            ? queueNavigator.getActiveQueueItemId(player)\n            : MediaSessionCompat.QueueItem.UNKNOWN_ID;\n    PlaybackParameters playbackParameters = player.getPlaybackParameters();\n    extras.putFloat(EXTRAS_SPEED, playbackParameters.speed);\n    extras.putFloat(EXTRAS_PITCH, playbackParameters.pitch);\n    float sessionPlaybackSpeed = player.isPlaying() ? playbackParameters.speed : 0f;\n    builder\n        .setActions(buildPrepareActions() | buildPlaybackActions(player))\n        .setActiveQueueItemId(activeQueueItemId)\n        .setBufferedPosition(player.getBufferedPosition())\n        .setState(\n            sessionPlaybackState,\n            player.getCurrentPosition(),\n            sessionPlaybackSpeed,\n            /* updateTime= */ SystemClock.elapsedRealtime())\n        .setExtras(extras);\n    mediaSession.setPlaybackState(builder.build());\n  }\n\n  /**\n   * Updates the queue of the media session by calling {@link\n   * QueueNavigator#onTimelineChanged(Player)}.\n   *\n   * <p>Apps normally only need to call this method when the backing data for a given queue item has\n   * changed and the queue should be updated immediately.\n   */\n  public final void invalidateMediaSessionQueue() {\n    if (queueNavigator != null && player != null) {\n      queueNavigator.onTimelineChanged(player);\n    }\n  }\n\n  /**\n   * Registers a custom command receiver for responding to commands delivered via {@link\n   * MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}.\n   *\n   * <p>Commands are only dispatched to this receiver when a player is connected.\n   *\n   * @param commandReceiver The command receiver to register.\n   */\n  public void registerCustomCommandReceiver(CommandReceiver commandReceiver) {\n    if (!customCommandReceivers.contains(commandReceiver)) {\n      customCommandReceivers.add(commandReceiver);\n    }\n  }\n\n  /**\n   * Unregisters a previously registered custom command receiver.\n   *\n   * @param commandReceiver The command receiver to unregister.\n   */\n  public void unregisterCustomCommandReceiver(CommandReceiver commandReceiver) {\n    customCommandReceivers.remove(commandReceiver);\n  }\n\n  private void registerCommandReceiver(CommandReceiver commandReceiver) {\n    if (!commandReceivers.contains(commandReceiver)) {\n      commandReceivers.add(commandReceiver);\n    }\n  }\n\n  private void unregisterCommandReceiver(CommandReceiver commandReceiver) {\n    commandReceivers.remove(commandReceiver);\n  }\n\n  private long buildPrepareActions() {\n    return playbackPreparer == null\n        ? 0\n        : (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());\n  }\n\n  private long buildPlaybackActions(Player player) {\n    boolean enableSeeking = false;\n    boolean enableRewind = false;\n    boolean enableFastForward = false;\n    boolean enableSetRating = false;\n    Timeline timeline = player.getCurrentTimeline();\n    if (!timeline.isEmpty() && !player.isPlayingAd()) {\n      enableSeeking = player.isCurrentWindowSeekable();\n      enableRewind = enableSeeking && rewindMs > 0;\n      enableFastForward = enableSeeking && fastForwardMs > 0;\n      enableSetRating = true;\n    }\n\n    long playbackActions = BASE_PLAYBACK_ACTIONS;\n    if (enableSeeking) {\n      playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;\n    }\n    if (enableFastForward) {\n      playbackActions |= PlaybackStateCompat.ACTION_FAST_FORWARD;\n    }\n    if (enableRewind) {\n      playbackActions |= PlaybackStateCompat.ACTION_REWIND;\n    }\n    playbackActions &= enabledPlaybackActions;\n\n    long actions = playbackActions;\n    if (queueNavigator != null) {\n      actions |=\n          (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));\n    }\n    if (ratingCallback != null && enableSetRating) {\n      actions |= PlaybackStateCompat.ACTION_SET_RATING;\n    }\n    return actions;\n  }\n\n  private boolean canDispatchPlaybackAction(long action) {\n    return player != null && (enabledPlaybackActions & action) != 0;\n  }\n\n  private boolean canDispatchToPlaybackPreparer(long action) {\n    return playbackPreparer != null\n        && (playbackPreparer.getSupportedPrepareActions() & action) != 0;\n  }\n\n  private boolean canDispatchToQueueNavigator(long action) {\n    return player != null\n        && queueNavigator != null\n        && (queueNavigator.getSupportedQueueNavigatorActions(player) & action) != 0;\n  }\n\n  private boolean canDispatchSetRating() {\n    return player != null && ratingCallback != null;\n  }\n\n  private boolean canDispatchQueueEdit() {\n    return player != null && queueEditor != null;\n  }\n\n  private boolean canDispatchMediaButtonEvent() {\n    return player != null && mediaButtonEventHandler != null;\n  }\n\n  private void rewind(Player player) {\n    if (player.isCurrentWindowSeekable() && rewindMs > 0) {\n      seekToOffset(player, /* offsetMs= */ -rewindMs);\n    }\n  }\n\n  private void fastForward(Player player) {\n    if (player.isCurrentWindowSeekable() && fastForwardMs > 0) {\n      seekToOffset(player, /* offsetMs= */ fastForwardMs);\n    }\n  }\n\n  private void seekToOffset(Player player, long offsetMs) {\n    long positionMs = player.getCurrentPosition() + offsetMs;\n    long durationMs = player.getDuration();\n    if (durationMs != C.TIME_UNSET) {\n      positionMs = Math.min(positionMs, durationMs);\n    }\n    positionMs = Math.max(positionMs, 0);\n    seekTo(player, player.getCurrentWindowIndex(), positionMs);\n  }\n\n  private void seekTo(Player player, int windowIndex, long positionMs) {\n    controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);\n  }\n\n  private static int getMediaSessionPlaybackState(\n      int exoPlayerPlaybackState, boolean playWhenReady) {\n    switch (exoPlayerPlaybackState) {\n      case Player.STATE_BUFFERING:\n        return PlaybackStateCompat.STATE_BUFFERING;\n      case Player.STATE_READY:\n        return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;\n      case Player.STATE_ENDED:\n        return PlaybackStateCompat.STATE_STOPPED;\n      default:\n        return PlaybackStateCompat.STATE_NONE;\n    }\n  }\n\n  /**\n   * Provides a default {@link MediaMetadataCompat} with properties and extras taken from the {@link\n   * MediaDescriptionCompat} of the {@link MediaSessionCompat.QueueItem} of the active queue item.\n   */\n  public static final class DefaultMediaMetadataProvider implements MediaMetadataProvider {\n\n    private final MediaControllerCompat mediaController;\n    private final String metadataExtrasPrefix;\n\n    /**\n     * Creates a new instance.\n     *\n     * @param mediaController The {@link MediaControllerCompat}.\n     * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the\n     *     active queue item to the session metadata.\n     */\n    public DefaultMediaMetadataProvider(\n        MediaControllerCompat mediaController, @Nullable String metadataExtrasPrefix) {\n      this.mediaController = mediaController;\n      this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : \"\";\n    }\n\n    @Override\n    public MediaMetadataCompat getMetadata(Player player) {\n      if (player.getCurrentTimeline().isEmpty()) {\n        return METADATA_EMPTY;\n      }\n      MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();\n      if (player.isPlayingAd()) {\n        builder.putLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT, 1);\n      }\n      builder.putLong(\n          MediaMetadataCompat.METADATA_KEY_DURATION,\n          player.isCurrentWindowDynamic() || player.getDuration() == C.TIME_UNSET\n              ? -1\n              : player.getDuration());\n      long activeQueueItemId = mediaController.getPlaybackState().getActiveQueueItemId();\n      if (activeQueueItemId != MediaSessionCompat.QueueItem.UNKNOWN_ID) {\n        List<MediaSessionCompat.QueueItem> queue = mediaController.getQueue();\n        for (int i = 0; queue != null && i < queue.size(); i++) {\n          MediaSessionCompat.QueueItem queueItem = queue.get(i);\n          if (queueItem.getQueueId() == activeQueueItemId) {\n            MediaDescriptionCompat description = queueItem.getDescription();\n            Bundle extras = description.getExtras();\n            if (extras != null) {\n              for (String key : extras.keySet()) {\n                Object value = extras.get(key);\n                if (value instanceof String) {\n                  builder.putString(metadataExtrasPrefix + key, (String) value);\n                } else if (value instanceof CharSequence) {\n                  builder.putText(metadataExtrasPrefix + key, (CharSequence) value);\n                } else if (value instanceof Long) {\n                  builder.putLong(metadataExtrasPrefix + key, (Long) value);\n                } else if (value instanceof Integer) {\n                  builder.putLong(metadataExtrasPrefix + key, (Integer) value);\n                } else if (value instanceof Bitmap) {\n                  builder.putBitmap(metadataExtrasPrefix + key, (Bitmap) value);\n                } else if (value instanceof RatingCompat) {\n                  builder.putRating(metadataExtrasPrefix + key, (RatingCompat) value);\n                }\n              }\n            }\n            if (description.getTitle() != null) {\n              String title = String.valueOf(description.getTitle());\n              builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);\n              builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title);\n            }\n            if (description.getSubtitle() != null) {\n              builder.putString(\n                  MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,\n                  String.valueOf(description.getSubtitle()));\n            }\n            if (description.getDescription() != null) {\n              builder.putString(\n                  MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,\n                  String.valueOf(description.getDescription()));\n            }\n            if (description.getIconBitmap() != null) {\n              builder.putBitmap(\n                  MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, description.getIconBitmap());\n            }\n            if (description.getIconUri() != null) {\n              builder.putString(\n                  MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,\n                  String.valueOf(description.getIconUri()));\n            }\n            if (description.getMediaId() != null) {\n              builder.putString(\n                  MediaMetadataCompat.METADATA_KEY_MEDIA_ID,\n                  String.valueOf(description.getMediaId()));\n            }\n            if (description.getMediaUri() != null) {\n              builder.putString(\n                  MediaMetadataCompat.METADATA_KEY_MEDIA_URI,\n                  String.valueOf(description.getMediaUri()));\n            }\n            break;\n          }\n        }\n      }\n      return builder.build();\n    }\n  }\n\n  private class ComponentListener extends MediaSessionCompat.Callback\n      implements Player.EventListener {\n\n    private int currentWindowIndex;\n    private int currentWindowCount;\n\n    // Player.EventListener implementation.\n\n    @Override\n    public void onTimelineChanged(\n        Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {\n      int windowCount = player.getCurrentTimeline().getWindowCount();\n      int windowIndex = player.getCurrentWindowIndex();\n      if (queueNavigator != null) {\n        queueNavigator.onTimelineChanged(player);\n        invalidateMediaSessionPlaybackState();\n      } else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) {\n        // active queue item and queue navigation actions may need to be updated\n        invalidateMediaSessionPlaybackState();\n      }\n      currentWindowCount = windowCount;\n      currentWindowIndex = windowIndex;\n      invalidateMediaSessionMetadata();\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n      invalidateMediaSessionPlaybackState();\n    }\n\n    @Override\n    public void onIsPlayingChanged(boolean isPlaying) {\n      invalidateMediaSessionPlaybackState();\n    }\n\n    @Override\n    public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {\n      mediaSession.setRepeatMode(\n          repeatMode == Player.REPEAT_MODE_ONE\n              ? PlaybackStateCompat.REPEAT_MODE_ONE\n              : repeatMode == Player.REPEAT_MODE_ALL\n                  ? PlaybackStateCompat.REPEAT_MODE_ALL\n                  : PlaybackStateCompat.REPEAT_MODE_NONE);\n      invalidateMediaSessionPlaybackState();\n    }\n\n    @Override\n    public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {\n      mediaSession.setShuffleMode(\n          shuffleModeEnabled\n              ? PlaybackStateCompat.SHUFFLE_MODE_ALL\n              : PlaybackStateCompat.SHUFFLE_MODE_NONE);\n      invalidateMediaSessionPlaybackState();\n      invalidateMediaSessionQueue();\n    }\n\n    @Override\n    public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {\n      if (currentWindowIndex != player.getCurrentWindowIndex()) {\n        if (queueNavigator != null) {\n          queueNavigator.onCurrentWindowIndexChanged(player);\n        }\n        currentWindowIndex = player.getCurrentWindowIndex();\n        // Update playback state after queueNavigator.onCurrentWindowIndexChanged has been called\n        // and before updating metadata.\n        invalidateMediaSessionPlaybackState();\n        invalidateMediaSessionMetadata();\n        return;\n      }\n      invalidateMediaSessionPlaybackState();\n    }\n\n    @Override\n    public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {\n      invalidateMediaSessionPlaybackState();\n    }\n\n    // MediaSessionCompat.Callback implementation.\n\n    @Override\n    public void onPlay() {\n      if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {\n        if (player.getPlaybackState() == Player.STATE_IDLE) {\n          if (playbackPreparer != null) {\n            playbackPreparer.onPrepare(/* playWhenReady= */ true);\n          }\n        } else if (player.getPlaybackState() == Player.STATE_ENDED) {\n          seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);\n        }\n        controlDispatcher.dispatchSetPlayWhenReady(\n            Assertions.checkNotNull(player), /* playWhenReady= */ true);\n      }\n    }\n\n    @Override\n    public void onPause() {\n      if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) {\n        controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false);\n      }\n    }\n\n    @Override\n    public void onSeekTo(long positionMs) {\n      if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SEEK_TO)) {\n        seekTo(player, player.getCurrentWindowIndex(), positionMs);\n      }\n    }\n\n    @Override\n    public void onFastForward() {\n      if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_FAST_FORWARD)) {\n        fastForward(player);\n      }\n    }\n\n    @Override\n    public void onRewind() {\n      if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_REWIND)) {\n        rewind(player);\n      }\n    }\n\n    @Override\n    public void onStop() {\n      if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_STOP)) {\n        controlDispatcher.dispatchStop(player, /* reset= */ true);\n      }\n    }\n\n    @Override\n    public void onSetShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {\n      if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE)) {\n        boolean shuffleModeEnabled;\n        switch (shuffleMode) {\n          case PlaybackStateCompat.SHUFFLE_MODE_ALL:\n          case PlaybackStateCompat.SHUFFLE_MODE_GROUP:\n            shuffleModeEnabled = true;\n            break;\n          case PlaybackStateCompat.SHUFFLE_MODE_NONE:\n          case PlaybackStateCompat.SHUFFLE_MODE_INVALID:\n          default:\n            shuffleModeEnabled = false;\n            break;\n        }\n        controlDispatcher.dispatchSetShuffleModeEnabled(player, shuffleModeEnabled);\n      }\n    }\n\n    @Override\n    public void onSetRepeatMode(@PlaybackStateCompat.RepeatMode int mediaSessionRepeatMode) {\n      if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) {\n        @RepeatModeUtil.RepeatToggleModes int repeatMode;\n        switch (mediaSessionRepeatMode) {\n          case PlaybackStateCompat.REPEAT_MODE_ALL:\n          case PlaybackStateCompat.REPEAT_MODE_GROUP:\n            repeatMode = Player.REPEAT_MODE_ALL;\n            break;\n          case PlaybackStateCompat.REPEAT_MODE_ONE:\n            repeatMode = Player.REPEAT_MODE_ONE;\n            break;\n          case PlaybackStateCompat.REPEAT_MODE_NONE:\n          case PlaybackStateCompat.REPEAT_MODE_INVALID:\n          default:\n            repeatMode = Player.REPEAT_MODE_OFF;\n            break;\n        }\n        controlDispatcher.dispatchSetRepeatMode(player, repeatMode);\n      }\n    }\n\n    @Override\n    public void onSkipToNext() {\n      if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) {\n        queueNavigator.onSkipToNext(player, controlDispatcher);\n      }\n    }\n\n    @Override\n    public void onSkipToPrevious() {\n      if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) {\n        queueNavigator.onSkipToPrevious(player, controlDispatcher);\n      }\n    }\n\n    @Override\n    public void onSkipToQueueItem(long id) {\n      if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM)) {\n        queueNavigator.onSkipToQueueItem(player, controlDispatcher, id);\n      }\n    }\n\n    @Override\n    public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {\n      if (player != null && customActionMap.containsKey(action)) {\n        customActionMap.get(action).onCustomAction(player, controlDispatcher, action, extras);\n        invalidateMediaSessionPlaybackState();\n      }\n    }\n\n    @Override\n    public void onCommand(String command, Bundle extras, ResultReceiver cb) {\n      if (player != null) {\n        for (int i = 0; i < commandReceivers.size(); i++) {\n          if (commandReceivers.get(i).onCommand(player, controlDispatcher, command, extras, cb)) {\n            return;\n          }\n        }\n        for (int i = 0; i < customCommandReceivers.size(); i++) {\n          if (customCommandReceivers\n              .get(i)\n              .onCommand(player, controlDispatcher, command, extras, cb)) {\n            return;\n          }\n        }\n      }\n    }\n\n    @Override\n    public void onPrepare() {\n      if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {\n        playbackPreparer.onPrepare(/* playWhenReady= */ false);\n      }\n    }\n\n    @Override\n    public void onPrepareFromMediaId(String mediaId, Bundle extras) {\n      if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {\n        playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras);\n      }\n    }\n\n    @Override\n    public void onPrepareFromSearch(String query, Bundle extras) {\n      if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {\n        playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras);\n      }\n    }\n\n    @Override\n    public void onPrepareFromUri(Uri uri, Bundle extras) {\n      if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {\n        playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras);\n      }\n    }\n\n    @Override\n    public void onPlayFromMediaId(String mediaId, Bundle extras) {\n      if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {\n        playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras);\n      }\n    }\n\n    @Override\n    public void onPlayFromSearch(String query, Bundle extras) {\n      if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {\n        playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras);\n      }\n    }\n\n    @Override\n    public void onPlayFromUri(Uri uri, Bundle extras) {\n      if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {\n        playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras);\n      }\n    }\n\n    @Override\n    public void onSetRating(RatingCompat rating) {\n      if (canDispatchSetRating()) {\n        ratingCallback.onSetRating(player, rating);\n      }\n    }\n\n    @Override\n    public void onSetRating(RatingCompat rating, Bundle extras) {\n      if (canDispatchSetRating()) {\n        ratingCallback.onSetRating(player, rating, extras);\n      }\n    }\n\n    @Override\n    public void onAddQueueItem(MediaDescriptionCompat description) {\n      if (canDispatchQueueEdit()) {\n        queueEditor.onAddQueueItem(player, description);\n      }\n    }\n\n    @Override\n    public void onAddQueueItem(MediaDescriptionCompat description, int index) {\n      if (canDispatchQueueEdit()) {\n        queueEditor.onAddQueueItem(player, description, index);\n      }\n    }\n\n    @Override\n    public void onRemoveQueueItem(MediaDescriptionCompat description) {\n      if (canDispatchQueueEdit()) {\n        queueEditor.onRemoveQueueItem(player, description);\n      }\n    }\n\n    @Override\n    public boolean onMediaButtonEvent(Intent mediaButtonEvent) {\n      boolean isHandled =\n          canDispatchMediaButtonEvent()\n              && mediaButtonEventHandler.onMediaButtonEvent(\n                  player, controlDispatcher, mediaButtonEvent);\n      return isHandled || super.onMediaButtonEvent(mediaButtonEvent);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.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.ext.mediasession;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.support.v4.media.session.PlaybackStateCompat;\nimport com.google.android.exoplayer2.ControlDispatcher;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.util.RepeatModeUtil;\n\n/** Provides a custom action for toggling repeat modes. */\npublic final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider {\n\n  /** The default repeat toggle modes. */\n  @RepeatModeUtil.RepeatToggleModes\n  public static final int DEFAULT_REPEAT_TOGGLE_MODES =\n      RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;\n\n  private static final String ACTION_REPEAT_MODE = \"ACTION_EXO_REPEAT_MODE\";\n\n  @RepeatModeUtil.RepeatToggleModes\n  private final int repeatToggleModes;\n  private final CharSequence repeatAllDescription;\n  private final CharSequence repeatOneDescription;\n  private final CharSequence repeatOffDescription;\n\n  /**\n   * Creates a new instance.\n   *\n   * <p>Equivalent to {@code RepeatModeActionProvider(context, DEFAULT_REPEAT_TOGGLE_MODES)}.\n   *\n   * @param context The context.\n   */\n  public RepeatModeActionProvider(Context context) {\n    this(context, DEFAULT_REPEAT_TOGGLE_MODES);\n  }\n\n  /**\n   * Creates a new instance enabling the given repeat toggle modes.\n   *\n   * @param context The context.\n   * @param repeatToggleModes The toggle modes to enable.\n   */\n  public RepeatModeActionProvider(\n      Context context, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {\n    this.repeatToggleModes = repeatToggleModes;\n    repeatAllDescription = context.getString(R.string.exo_media_action_repeat_all_description);\n    repeatOneDescription = context.getString(R.string.exo_media_action_repeat_one_description);\n    repeatOffDescription = context.getString(R.string.exo_media_action_repeat_off_description);\n  }\n\n  @Override\n  public void onCustomAction(\n      Player player, ControlDispatcher controlDispatcher, String action, Bundle extras) {\n    int mode = player.getRepeatMode();\n    int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);\n    if (mode != proposedMode) {\n      controlDispatcher.dispatchSetRepeatMode(player, proposedMode);\n    }\n  }\n\n  @Override\n  public PlaybackStateCompat.CustomAction getCustomAction(Player player) {\n    CharSequence actionLabel;\n    int iconResourceId;\n    switch (player.getRepeatMode()) {\n      case Player.REPEAT_MODE_ONE:\n        actionLabel = repeatOneDescription;\n        iconResourceId = R.drawable.exo_media_action_repeat_one;\n        break;\n      case Player.REPEAT_MODE_ALL:\n        actionLabel = repeatAllDescription;\n        iconResourceId = R.drawable.exo_media_action_repeat_all;\n        break;\n      case Player.REPEAT_MODE_OFF:\n      default:\n        actionLabel = repeatOffDescription;\n        iconResourceId = R.drawable.exo_media_action_repeat_off;\n        break;\n    }\n    PlaybackStateCompat.CustomAction.Builder repeatBuilder = new PlaybackStateCompat.CustomAction\n        .Builder(ACTION_REPEAT_MODE, actionLabel, iconResourceId);\n    return repeatBuilder.build();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.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.ext.mediasession;\n\nimport android.os.Bundle;\nimport android.os.ResultReceiver;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport android.support.v4.media.MediaDescriptionCompat;\nimport android.support.v4.media.session.MediaControllerCompat;\nimport android.support.v4.media.session.MediaSessionCompat;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ControlDispatcher;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.source.ConcatenatingMediaSource;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.List;\n\n/**\n * A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link\n * ConcatenatingMediaSource}.\n *\n * <p>This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles\n * the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it.\n * This allows to move the currently playing window without interrupting playback.\n */\npublic final class TimelineQueueEditor\n    implements MediaSessionConnector.QueueEditor, MediaSessionConnector.CommandReceiver {\n\n  public static final String COMMAND_MOVE_QUEUE_ITEM = \"exo_move_window\";\n  public static final String EXTRA_FROM_INDEX = \"from_index\";\n  public static final String EXTRA_TO_INDEX = \"to_index\";\n\n  /**\n   * Factory to create {@link MediaSource}s.\n   */\n  public interface MediaSourceFactory {\n    /**\n     * Creates a {@link MediaSource} for the given {@link MediaDescriptionCompat}.\n     *\n     * @param description The {@link MediaDescriptionCompat} to create a media source for.\n     * @return A {@link MediaSource} or {@code null} if no source can be created for the given\n     *     description.\n     */\n    @Nullable MediaSource createMediaSource(MediaDescriptionCompat description);\n  }\n\n  /**\n   * Adapter to get {@link MediaDescriptionCompat} of items in the queue and to notify the\n   * application about changes in the queue to sync the data structure backing the\n   * {@link MediaSessionConnector}.\n   */\n  public interface QueueDataAdapter {\n    /**\n     * Adds a {@link MediaDescriptionCompat} at the given {@code position}.\n     *\n     * @param position The position at which to add.\n     * @param description The {@link MediaDescriptionCompat} to be added.\n     */\n    void add(int position, MediaDescriptionCompat description);\n    /**\n     * Removes the item at the given {@code position}.\n     *\n     * @param position The position at which to remove the item.\n     */\n    void remove(int position);\n    /**\n     * Moves a queue item from position {@code from} to position {@code to}.\n     *\n     * @param from The position from which to remove the item.\n     * @param to The target position to which to move the item.\n     */\n    void move(int from, int to);\n  }\n\n  /**\n   * Used to evaluate whether two {@link MediaDescriptionCompat} are considered equal.\n   */\n  interface MediaDescriptionEqualityChecker {\n    /**\n     * Returns {@code true} whether the descriptions are considered equal.\n     *\n     * @param d1 The first {@link MediaDescriptionCompat}.\n     * @param d2 The second {@link MediaDescriptionCompat}.\n     * @return {@code true} if considered equal.\n     */\n    boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2);\n  }\n\n  /**\n   * Media description comparator comparing the media IDs. Media IDs are considered equals if both\n   * are {@code null}.\n   */\n  public static final class MediaIdEqualityChecker implements MediaDescriptionEqualityChecker {\n\n    @Override\n    public boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2) {\n      return Util.areEqual(d1.getMediaId(), d2.getMediaId());\n    }\n\n  }\n\n  private final MediaControllerCompat mediaController;\n  private final QueueDataAdapter queueDataAdapter;\n  private final MediaSourceFactory sourceFactory;\n  private final MediaDescriptionEqualityChecker equalityChecker;\n  private final ConcatenatingMediaSource queueMediaSource;\n\n  /**\n   * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.\n   *\n   * @param mediaController A {@link MediaControllerCompat} to read the current queue.\n   * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.\n   * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.\n   * @param sourceFactory The {@link MediaSourceFactory} to build media sources.\n   */\n  public TimelineQueueEditor(\n      @NonNull MediaControllerCompat mediaController,\n      @NonNull ConcatenatingMediaSource queueMediaSource,\n      @NonNull QueueDataAdapter queueDataAdapter,\n      @NonNull MediaSourceFactory sourceFactory) {\n    this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory,\n        new MediaIdEqualityChecker());\n  }\n\n  /**\n   * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.\n   *\n   * @param mediaController A {@link MediaControllerCompat} to read the current queue.\n   * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.\n   * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.\n   * @param sourceFactory The {@link MediaSourceFactory} to build media sources.\n   * @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.\n   */\n  public TimelineQueueEditor(\n      @NonNull MediaControllerCompat mediaController,\n      @NonNull ConcatenatingMediaSource queueMediaSource,\n      @NonNull QueueDataAdapter queueDataAdapter,\n      @NonNull MediaSourceFactory sourceFactory,\n      @NonNull MediaDescriptionEqualityChecker equalityChecker) {\n    this.mediaController = mediaController;\n    this.queueMediaSource = queueMediaSource;\n    this.queueDataAdapter = queueDataAdapter;\n    this.sourceFactory = sourceFactory;\n    this.equalityChecker = equalityChecker;\n  }\n\n  @Override\n  public void onAddQueueItem(Player player, MediaDescriptionCompat description) {\n    onAddQueueItem(player, description, player.getCurrentTimeline().getWindowCount());\n  }\n\n  @Override\n  public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) {\n    MediaSource mediaSource = sourceFactory.createMediaSource(description);\n    if (mediaSource != null) {\n      queueDataAdapter.add(index, description);\n      queueMediaSource.addMediaSource(index, mediaSource);\n    }\n  }\n\n  @Override\n  public void onRemoveQueueItem(Player player, MediaDescriptionCompat description) {\n    List<MediaSessionCompat.QueueItem> queue = mediaController.getQueue();\n    for (int i = 0; i < queue.size(); i++) {\n      if (equalityChecker.equals(queue.get(i).getDescription(), description)) {\n        queueDataAdapter.remove(i);\n        queueMediaSource.removeMediaSource(i);\n        return;\n      }\n    }\n  }\n\n  // CommandReceiver implementation.\n\n  @Override\n  public boolean onCommand(\n      Player player,\n      ControlDispatcher controlDispatcher,\n      String command,\n      Bundle extras,\n      ResultReceiver cb) {\n    if (!COMMAND_MOVE_QUEUE_ITEM.equals(command)) {\n      return false;\n    }\n    int from = extras.getInt(EXTRA_FROM_INDEX, C.INDEX_UNSET);\n    int to = extras.getInt(EXTRA_TO_INDEX, C.INDEX_UNSET);\n    if (from != C.INDEX_UNSET && to != C.INDEX_UNSET) {\n      queueDataAdapter.move(from, to);\n      queueMediaSource.moveMediaSource(from, to);\n    }\n    return true;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.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.ext.mediasession;\n\nimport android.os.Bundle;\nimport android.os.ResultReceiver;\nimport androidx.annotation.Nullable;\nimport android.support.v4.media.MediaDescriptionCompat;\nimport android.support.v4.media.session.MediaSessionCompat;\nimport android.support.v4.media.session.PlaybackStateCompat;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ControlDispatcher;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.Collections;\n\n/**\n * An abstract implementation of the {@link MediaSessionConnector.QueueNavigator} that maps the\n * windows of a {@link Player}'s {@link Timeline} to the media session queue.\n */\npublic abstract class TimelineQueueNavigator implements MediaSessionConnector.QueueNavigator {\n\n  public static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000;\n  public static final int DEFAULT_MAX_QUEUE_SIZE = 10;\n\n  private final MediaSessionCompat mediaSession;\n  private final Timeline.Window window;\n  private final int maxQueueSize;\n\n  private long activeQueueItemId;\n\n  /**\n   * Creates an instance for a given {@link MediaSessionCompat}.\n   * <p>\n   * Equivalent to {@code TimelineQueueNavigator(mediaSession, DEFAULT_MAX_QUEUE_SIZE)}.\n   *\n   * @param mediaSession The {@link MediaSessionCompat}.\n   */\n  public TimelineQueueNavigator(MediaSessionCompat mediaSession) {\n    this(mediaSession, DEFAULT_MAX_QUEUE_SIZE);\n  }\n\n  /**\n   * Creates an instance for a given {@link MediaSessionCompat} and maximum queue size.\n   * <p>\n   * If the number of windows in the {@link Player}'s {@link Timeline} exceeds {@code maxQueueSize},\n   * the media session queue will correspond to {@code maxQueueSize} windows centered on the one\n   * currently being played.\n   *\n   * @param mediaSession The {@link MediaSessionCompat}.\n   * @param maxQueueSize The maximum queue size.\n   */\n  public TimelineQueueNavigator(MediaSessionCompat mediaSession, int maxQueueSize) {\n    Assertions.checkState(maxQueueSize > 0);\n    this.mediaSession = mediaSession;\n    this.maxQueueSize = maxQueueSize;\n    activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;\n    window = new Timeline.Window();\n  }\n\n  /**\n   * Gets the {@link MediaDescriptionCompat} for a given timeline window index.\n   *\n   * <p>Often artworks and icons need to be loaded asynchronously. In such a case, return a {@link\n   * MediaDescriptionCompat} without the images, load your images asynchronously off the main thread\n   * and then call {@link MediaSessionConnector#invalidateMediaSessionQueue()} to make the connector\n   * update the queue by calling {@link #getMediaDescription(Player, int)} again.\n   *\n   * @param player The current player.\n   * @param windowIndex The timeline window index for which to provide a description.\n   * @return A {@link MediaDescriptionCompat}.\n   */\n  public abstract MediaDescriptionCompat getMediaDescription(Player player, int windowIndex);\n\n  @Override\n  public long getSupportedQueueNavigatorActions(Player player) {\n    boolean enableSkipTo = false;\n    boolean enablePrevious = false;\n    boolean enableNext = false;\n    Timeline timeline = player.getCurrentTimeline();\n    if (!timeline.isEmpty() && !player.isPlayingAd()) {\n      timeline.getWindow(player.getCurrentWindowIndex(), window);\n      enableSkipTo = timeline.getWindowCount() > 1;\n      enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious();\n      enableNext = window.isDynamic || player.hasNext();\n    }\n\n    long actions = 0;\n    if (enableSkipTo) {\n      actions |= PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;\n    }\n    if (enablePrevious) {\n      actions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;\n    }\n    if (enableNext) {\n      actions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT;\n    }\n    return actions;\n  }\n\n  @Override\n  public final void onTimelineChanged(Player player) {\n    publishFloatingQueueWindow(player);\n  }\n\n  @Override\n  public final void onCurrentWindowIndexChanged(Player player) {\n    if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID\n        || player.getCurrentTimeline().getWindowCount() > maxQueueSize) {\n      publishFloatingQueueWindow(player);\n    } else if (!player.getCurrentTimeline().isEmpty()) {\n      activeQueueItemId = player.getCurrentWindowIndex();\n    }\n  }\n\n  @Override\n  public final long getActiveQueueItemId(@Nullable Player player) {\n    return activeQueueItemId;\n  }\n\n  @Override\n  public void onSkipToPrevious(Player player, ControlDispatcher controlDispatcher) {\n    Timeline timeline = player.getCurrentTimeline();\n    if (timeline.isEmpty() || player.isPlayingAd()) {\n      return;\n    }\n    int windowIndex = player.getCurrentWindowIndex();\n    timeline.getWindow(windowIndex, window);\n    int previousWindowIndex = player.getPreviousWindowIndex();\n    if (previousWindowIndex != C.INDEX_UNSET\n        && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS\n            || (window.isDynamic && !window.isSeekable))) {\n      controlDispatcher.dispatchSeekTo(player, previousWindowIndex, C.TIME_UNSET);\n    } else {\n      controlDispatcher.dispatchSeekTo(player, windowIndex, 0);\n    }\n  }\n\n  @Override\n  public void onSkipToQueueItem(Player player, ControlDispatcher controlDispatcher, long id) {\n    Timeline timeline = player.getCurrentTimeline();\n    if (timeline.isEmpty() || player.isPlayingAd()) {\n      return;\n    }\n    int windowIndex = (int) id;\n    if (0 <= windowIndex && windowIndex < timeline.getWindowCount()) {\n      controlDispatcher.dispatchSeekTo(player, windowIndex, C.TIME_UNSET);\n    }\n  }\n\n  @Override\n  public void onSkipToNext(Player player, ControlDispatcher controlDispatcher) {\n    Timeline timeline = player.getCurrentTimeline();\n    if (timeline.isEmpty() || player.isPlayingAd()) {\n      return;\n    }\n    int windowIndex = player.getCurrentWindowIndex();\n    int nextWindowIndex = player.getNextWindowIndex();\n    if (nextWindowIndex != C.INDEX_UNSET) {\n      controlDispatcher.dispatchSeekTo(player, nextWindowIndex, C.TIME_UNSET);\n    } else if (timeline.getWindow(windowIndex, window).isDynamic) {\n      controlDispatcher.dispatchSeekTo(player, windowIndex, C.TIME_UNSET);\n    }\n  }\n\n  // CommandReceiver implementation.\n\n  @Override\n  public boolean onCommand(\n      Player player,\n      ControlDispatcher controlDispatcher,\n      String command,\n      Bundle extras,\n      ResultReceiver cb) {\n    return false;\n  }\n\n  // Helper methods.\n\n  private void publishFloatingQueueWindow(Player player) {\n    Timeline timeline = player.getCurrentTimeline();\n    if (timeline.isEmpty()) {\n      mediaSession.setQueue(Collections.emptyList());\n      activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;\n      return;\n    }\n    ArrayDeque<MediaSessionCompat.QueueItem> queue = new ArrayDeque<>();\n    int queueSize = Math.min(maxQueueSize, timeline.getWindowCount());\n\n    // Add the active queue item.\n    int currentWindowIndex = player.getCurrentWindowIndex();\n    queue.add(\n        new MediaSessionCompat.QueueItem(\n            getMediaDescription(player, currentWindowIndex), currentWindowIndex));\n\n    // Fill queue alternating with next and/or previous queue items.\n    int firstWindowIndex = currentWindowIndex;\n    int lastWindowIndex = currentWindowIndex;\n    boolean shuffleModeEnabled = player.getShuffleModeEnabled();\n    while ((firstWindowIndex != C.INDEX_UNSET || lastWindowIndex != C.INDEX_UNSET)\n        && queue.size() < queueSize) {\n      // Begin with next to have a longer tail than head if an even sized queue needs to be trimmed.\n      if (lastWindowIndex != C.INDEX_UNSET) {\n        lastWindowIndex =\n            timeline.getNextWindowIndex(\n                lastWindowIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled);\n        if (lastWindowIndex != C.INDEX_UNSET) {\n          queue.add(\n              new MediaSessionCompat.QueueItem(\n                  getMediaDescription(player, lastWindowIndex), lastWindowIndex));\n        }\n      }\n      if (firstWindowIndex != C.INDEX_UNSET && queue.size() < queueSize) {\n        firstWindowIndex =\n            timeline.getPreviousWindowIndex(\n                firstWindowIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled);\n        if (firstWindowIndex != C.INDEX_UNSET) {\n          queue.addFirst(\n              new MediaSessionCompat.QueueItem(\n                  getMediaDescription(player, firstWindowIndex), firstWindowIndex));\n        }\n      }\n    }\n    mediaSession.setQueue(new ArrayList<>(queue));\n    activeQueueItemId = currentWindowIndex;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_all.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"32dp\"\n        android:height=\"32dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z\"/>\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_off.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"32dp\"\n        android:height=\"32dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#4EFFFFFF\"\n        android:pathData=\"M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z\"/>\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_one.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:height=\"32dp\"\n        android:width=\"32dp\"\n        android:viewportHeight=\"24.0\"\n        android:viewportWidth=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4zM13,15L13,9h-1l-2,1v1h1.5v4L13,15z\"/>\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<resources>\n  <!-- Description for a button that controls the repeat mode of a media playback. In this mode media is not repeated. [CHAR LIMIT=30] -->\n  <string name=\"exo_media_action_repeat_off_description\">Repeat none</string>\n  <!-- Description for a button that controls the repeat mode of a media playback. In this mode the current piece of media is repeated. [CHAR LIMIT=30] -->\n  <string name=\"exo_media_action_repeat_one_description\">Repeat one</string>\n  <!-- Description for a button that controls the repeat mode of a media playback. In this mode the entire playlist is repeated. [CHAR LIMIT=30] -->\n  <string name=\"exo_media_action_repeat_all_description\">Repeat all</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-af/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Herhaal niks</string>\n  <string name=\"exo_media_action_repeat_one_description\">Herhaal een</string>\n  <string name=\"exo_media_action_repeat_all_description\">Herhaal alles</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-am/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">ምንም አትድገም</string>\n  <string name=\"exo_media_action_repeat_one_description\">አንድ ድገም</string>\n  <string name=\"exo_media_action_repeat_all_description\">ሁሉንም ድገም</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-ar/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">عدم التكرار</string>\n  <string name=\"exo_media_action_repeat_one_description\">تكرار مقطع صوتي واحد</string>\n  <string name=\"exo_media_action_repeat_all_description\">تكرار الكل</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-az/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Heç biri təkrarlanmasın</string>\n  <string name=\"exo_media_action_repeat_one_description\">Biri təkrarlansın</string>\n  <string name=\"exo_media_action_repeat_all_description\">Hamısı təkrarlansın</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Ne ponavljaj nijednu</string>\n  <string name=\"exo_media_action_repeat_one_description\">Ponovi jednu</string>\n  <string name=\"exo_media_action_repeat_all_description\">Ponovi sve</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-be/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Не паўтараць нічога</string>\n  <string name=\"exo_media_action_repeat_one_description\">Паўтарыць адзін элемент</string>\n  <string name=\"exo_media_action_repeat_all_description\">Паўтарыць усе</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-bg/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Без повтаряне</string>\n  <string name=\"exo_media_action_repeat_one_description\">Повтаряне на един елемент</string>\n  <string name=\"exo_media_action_repeat_all_description\">Повтаряне на всички</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-bn/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">কোনও আইটেম আবার চালাবেন না</string>\n  <string name=\"exo_media_action_repeat_one_description\">একটি আইটেম আবার চালান</string>\n  <string name=\"exo_media_action_repeat_all_description\">সবগুলি আইটেম আবার চালান</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-bs/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Ne ponavljaj</string>\n  <string name=\"exo_media_action_repeat_one_description\">Ponovi jedno</string>\n  <string name=\"exo_media_action_repeat_all_description\">Ponovi sve</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-ca/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">No en repeteixis cap</string>\n  <string name=\"exo_media_action_repeat_one_description\">Repeteix una</string>\n  <string name=\"exo_media_action_repeat_all_description\">Repeteix tot</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-cs/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Neopakovat</string>\n  <string name=\"exo_media_action_repeat_one_description\">Opakovat jednu</string>\n  <string name=\"exo_media_action_repeat_all_description\">Opakovat vše</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-da/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Gentag ingen</string>\n  <string name=\"exo_media_action_repeat_one_description\">Gentag én</string>\n  <string name=\"exo_media_action_repeat_all_description\">Gentag alle</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-de/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Keinen wiederholen</string>\n  <string name=\"exo_media_action_repeat_one_description\">Einen wiederholen</string>\n  <string name=\"exo_media_action_repeat_all_description\">Alle wiederholen</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-el/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Καμία επανάληψη</string>\n  <string name=\"exo_media_action_repeat_one_description\">Επανάληψη ενός κομματιού</string>\n  <string name=\"exo_media_action_repeat_all_description\">Επανάληψη όλων</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-en-rAU/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Repeat none</string>\n  <string name=\"exo_media_action_repeat_one_description\">Repeat one</string>\n  <string name=\"exo_media_action_repeat_all_description\">Repeat all</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-en-rGB/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Repeat none</string>\n  <string name=\"exo_media_action_repeat_one_description\">Repeat one</string>\n  <string name=\"exo_media_action_repeat_all_description\">Repeat all</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-en-rIN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Repeat none</string>\n  <string name=\"exo_media_action_repeat_one_description\">Repeat one</string>\n  <string name=\"exo_media_action_repeat_all_description\">Repeat all</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-es/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">No repetir</string>\n  <string name=\"exo_media_action_repeat_one_description\">Repetir uno</string>\n  <string name=\"exo_media_action_repeat_all_description\">Repetir todo</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-es-rUS/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">No repetir</string>\n  <string name=\"exo_media_action_repeat_one_description\">Repetir uno</string>\n  <string name=\"exo_media_action_repeat_all_description\">Repetir todo</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-et/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Ära korda ühtegi</string>\n  <string name=\"exo_media_action_repeat_one_description\">Korda ühte</string>\n  <string name=\"exo_media_action_repeat_all_description\">Korda kõiki</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-eu/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Ez errepikatu</string>\n  <string name=\"exo_media_action_repeat_one_description\">Errepikatu bat</string>\n  <string name=\"exo_media_action_repeat_all_description\">Errepikatu guztiak</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-fa/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">تکرار هیچ‌کدام</string>\n  <string name=\"exo_media_action_repeat_one_description\">یکبار تکرار</string>\n  <string name=\"exo_media_action_repeat_all_description\">تکرار همه</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-fi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Ei uudelleentoistoa</string>\n  <string name=\"exo_media_action_repeat_one_description\">Toista yksi uudelleen</string>\n  <string name=\"exo_media_action_repeat_all_description\">Toista kaikki uudelleen</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-fr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Ne rien lire en boucle</string>\n  <string name=\"exo_media_action_repeat_one_description\">Lire un titre en boucle</string>\n  <string name=\"exo_media_action_repeat_all_description\">Tout lire en boucle</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Ne rien lire en boucle</string>\n  <string name=\"exo_media_action_repeat_one_description\">Lire une chanson en boucle</string>\n  <string name=\"exo_media_action_repeat_all_description\">Tout lire en boucle</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-gl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Non repetir</string>\n  <string name=\"exo_media_action_repeat_one_description\">Repetir unha pista</string>\n  <string name=\"exo_media_action_repeat_all_description\">Repetir todas as pistas</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-gu/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">કોઈ રિપીટ કરતા નહીં</string>\n  <string name=\"exo_media_action_repeat_one_description\">એક રિપીટ કરો</string>\n  <string name=\"exo_media_action_repeat_all_description\">બધાને રિપીટ કરો</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-hi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">किसी को न दोहराएं</string>\n  <string name=\"exo_media_action_repeat_one_description\">एक को दोहराएं</string>\n  <string name=\"exo_media_action_repeat_all_description\">सभी को दोहराएं</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-hr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Bez ponavljanja</string>\n  <string name=\"exo_media_action_repeat_one_description\">Ponovi jedno</string>\n  <string name=\"exo_media_action_repeat_all_description\">Ponovi sve</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-hu/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Nincs ismétlés</string>\n  <string name=\"exo_media_action_repeat_one_description\">Egy szám ismétlése</string>\n  <string name=\"exo_media_action_repeat_all_description\">Összes szám ismétlése</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-hy/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Չկրկնել</string>\n  <string name=\"exo_media_action_repeat_one_description\">Կրկնել մեկը</string>\n  <string name=\"exo_media_action_repeat_all_description\">Կրկնել բոլորը</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-in/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Jangan ulangi</string>\n  <string name=\"exo_media_action_repeat_one_description\">Ulangi 1</string>\n  <string name=\"exo_media_action_repeat_all_description\">Ulangi semua</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-is/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Endurtaka ekkert</string>\n  <string name=\"exo_media_action_repeat_one_description\">Endurtaka eitt</string>\n  <string name=\"exo_media_action_repeat_all_description\">Endurtaka allt</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-it/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Non ripetere nulla</string>\n  <string name=\"exo_media_action_repeat_one_description\">Ripeti uno</string>\n  <string name=\"exo_media_action_repeat_all_description\">Ripeti tutto</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-iw/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">אל תחזור על אף פריט</string>\n  <string name=\"exo_media_action_repeat_one_description\">חזור על פריט אחד</string>\n  <string name=\"exo_media_action_repeat_all_description\">חזור על הכול</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-ja/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">リピートなし</string>\n  <string name=\"exo_media_action_repeat_one_description\">1 曲をリピート</string>\n  <string name=\"exo_media_action_repeat_all_description\">全曲をリピート</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-ka/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">არცერთის გამეორება</string>\n  <string name=\"exo_media_action_repeat_one_description\">ერთის გამეორება</string>\n  <string name=\"exo_media_action_repeat_all_description\">ყველას გამეორება</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-kk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Ешқайсысын қайталамау</string>\n  <string name=\"exo_media_action_repeat_one_description\">Біреуін қайталау</string>\n  <string name=\"exo_media_action_repeat_all_description\">Барлығын қайталау</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-km/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">មិន​លេង​ឡើងវិញ</string>\n  <string name=\"exo_media_action_repeat_one_description\">លេង​ឡើង​វិញ​ម្ដង</string>\n  <string name=\"exo_media_action_repeat_all_description\">លេង​ឡើងវិញ​ទាំងអស់</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-kn/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ</string>\n  <string name=\"exo_media_action_repeat_one_description\">ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ</string>\n  <string name=\"exo_media_action_repeat_all_description\">ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-ko/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">반복 안함</string>\n  <string name=\"exo_media_action_repeat_one_description\">현재 미디어 반복</string>\n  <string name=\"exo_media_action_repeat_all_description\">모두 반복</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-ky/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Кайталанбасын</string>\n  <string name=\"exo_media_action_repeat_one_description\">Бирөөнү кайталоо</string>\n  <string name=\"exo_media_action_repeat_all_description\">Баарын кайталоо</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-lo/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">ບໍ່ຫຼິ້ນຊ້ຳ</string>\n  <string name=\"exo_media_action_repeat_one_description\">ຫຼິ້ນຊໍ້າ</string>\n  <string name=\"exo_media_action_repeat_all_description\">ຫຼິ້ນຊ້ຳທັງໝົດ</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-lt/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Nekartoti nieko</string>\n  <string name=\"exo_media_action_repeat_one_description\">Kartoti vieną</string>\n  <string name=\"exo_media_action_repeat_all_description\">Kartoti viską</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-lv/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Neatkārtot nevienu</string>\n  <string name=\"exo_media_action_repeat_one_description\">Atkārtot vienu</string>\n  <string name=\"exo_media_action_repeat_all_description\">Atkārtot visu</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-mk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Не повторувај ниту една</string>\n  <string name=\"exo_media_action_repeat_one_description\">Повтори една</string>\n  <string name=\"exo_media_action_repeat_all_description\">Повтори ги сите</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-ml/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">ഒന്നും ആവർത്തിക്കരുത്</string>\n  <string name=\"exo_media_action_repeat_one_description\">ഒരെണ്ണം ആവർത്തിക്കുക</string>\n  <string name=\"exo_media_action_repeat_all_description\">എല്ലാം ആവർത്തിക്കുക</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-mn/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Алийг нь ч дахин тоглуулахгүй</string>\n  <string name=\"exo_media_action_repeat_one_description\">Одоогийн тоглуулж буй медиаг дахин тоглуулах</string>\n  <string name=\"exo_media_action_repeat_all_description\">Бүгдийг нь дахин тоглуулах</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-mr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">रीपीट करू नका</string>\n  <string name=\"exo_media_action_repeat_one_description\">एक रीपीट करा</string>\n  <string name=\"exo_media_action_repeat_all_description\">सर्व रीपीट करा</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-ms/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Jangan ulang</string>\n  <string name=\"exo_media_action_repeat_one_description\">Ulang satu</string>\n  <string name=\"exo_media_action_repeat_all_description\">Ulang semua</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-my/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">မည်သည်ကိုမျှ ပြန်မကျော့ရန်</string>\n  <string name=\"exo_media_action_repeat_one_description\">တစ်ခုကို ပြန်ကျော့ရန်</string>\n  <string name=\"exo_media_action_repeat_all_description\">အားလုံး ပြန်ကျော့ရန်</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-nb/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Ikke gjenta noen</string>\n  <string name=\"exo_media_action_repeat_one_description\">Gjenta én</string>\n  <string name=\"exo_media_action_repeat_all_description\">Gjenta alle</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-ne/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">कुनै पनि नदोहोर्‍याउनुहोस्</string>\n  <string name=\"exo_media_action_repeat_one_description\">एउटा दोहोर्‍याउनुहोस्</string>\n  <string name=\"exo_media_action_repeat_all_description\">सबै दोहोर्‍याउनुहोस्</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-nl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Niets herhalen</string>\n  <string name=\"exo_media_action_repeat_one_description\">Eén herhalen</string>\n  <string name=\"exo_media_action_repeat_all_description\">Alles herhalen</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-pa/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">ਕਿਸੇ ਨੂੰ ਨਾ ਦੁਹਰਾਓ</string>\n  <string name=\"exo_media_action_repeat_one_description\">ਇੱਕ ਵਾਰ ਦੁਹਰਾਓ</string>\n  <string name=\"exo_media_action_repeat_all_description\">ਸਾਰਿਆਂ ਨੂੰ ਦੁਹਰਾਓ</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-pl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Nie powtarzaj</string>\n  <string name=\"exo_media_action_repeat_one_description\">Powtórz jeden</string>\n  <string name=\"exo_media_action_repeat_all_description\">Powtórz wszystkie</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-pt/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Não repetir</string>\n  <string name=\"exo_media_action_repeat_one_description\">Repetir uma</string>\n  <string name=\"exo_media_action_repeat_all_description\">Repetir tudo</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Não repetir nenhum</string>\n  <string name=\"exo_media_action_repeat_one_description\">Repetir um</string>\n  <string name=\"exo_media_action_repeat_all_description\">Repetir tudo</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-ro/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Nu repetați niciunul</string>\n  <string name=\"exo_media_action_repeat_one_description\">Repetați unul</string>\n  <string name=\"exo_media_action_repeat_all_description\">Repetați-le pe toate</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-ru/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Не повторять</string>\n  <string name=\"exo_media_action_repeat_one_description\">Повторять трек</string>\n  <string name=\"exo_media_action_repeat_all_description\">Повторять все</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-si/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">කිසිවක් පුනරාවර්තනය නොකරන්න</string>\n  <string name=\"exo_media_action_repeat_one_description\">එකක් පුනරාවර්තනය කරන්න</string>\n  <string name=\"exo_media_action_repeat_all_description\">සියල්ල නැවත කරන්න</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-sk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Neopakovať</string>\n  <string name=\"exo_media_action_repeat_one_description\">Opakovať jednu</string>\n  <string name=\"exo_media_action_repeat_all_description\">Opakovať všetko</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-sl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Brez ponavljanja</string>\n  <string name=\"exo_media_action_repeat_one_description\">Ponavljanje ene</string>\n  <string name=\"exo_media_action_repeat_all_description\">Ponavljanje vseh</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-sq/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Mos përsërit asnjë</string>\n  <string name=\"exo_media_action_repeat_one_description\">Përsërit një</string>\n  <string name=\"exo_media_action_repeat_all_description\">Përsërit të gjitha</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-sr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Не понављај ниједну</string>\n  <string name=\"exo_media_action_repeat_one_description\">Понови једну</string>\n  <string name=\"exo_media_action_repeat_all_description\">Понови све</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-sv/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Upprepa inga</string>\n  <string name=\"exo_media_action_repeat_one_description\">Upprepa en</string>\n  <string name=\"exo_media_action_repeat_all_description\">Upprepa alla</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-sw/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Usirudie yoyote</string>\n  <string name=\"exo_media_action_repeat_one_description\">Rudia moja</string>\n  <string name=\"exo_media_action_repeat_all_description\">Rudia zote</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-ta/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">எதையும் மீண்டும் இயக்காதே</string>\n  <string name=\"exo_media_action_repeat_one_description\">இதை மட்டும் மீண்டும் இயக்கு</string>\n  <string name=\"exo_media_action_repeat_all_description\">அனைத்தையும் மீண்டும் இயக்கு</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-te/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">దేన్నీ పునరావృతం చేయకండి</string>\n  <string name=\"exo_media_action_repeat_one_description\">ఒకదాన్ని పునరావృతం చేయండి</string>\n  <string name=\"exo_media_action_repeat_all_description\">అన్నింటినీ పునరావృతం చేయండి</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-th/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">ไม่เล่นซ้ำ</string>\n  <string name=\"exo_media_action_repeat_one_description\">เล่นซ้ำเพลงเดียว</string>\n  <string name=\"exo_media_action_repeat_all_description\">เล่นซ้ำทั้งหมด</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-tl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Walang uulitin</string>\n  <string name=\"exo_media_action_repeat_one_description\">Mag-ulit ng isa</string>\n  <string name=\"exo_media_action_repeat_all_description\">Ulitin lahat</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-tr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Hiçbirini tekrarlama</string>\n  <string name=\"exo_media_action_repeat_one_description\">Bir şarkıyı tekrarla</string>\n  <string name=\"exo_media_action_repeat_all_description\">Tümünü tekrarla</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-uk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Не повторювати</string>\n  <string name=\"exo_media_action_repeat_one_description\">Повторити 1</string>\n  <string name=\"exo_media_action_repeat_all_description\">Повторити всі</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-ur/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">کسی کو نہ دہرائیں</string>\n  <string name=\"exo_media_action_repeat_one_description\">ایک کو دہرائیں</string>\n  <string name=\"exo_media_action_repeat_all_description\">سبھی کو دہرائیں</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-uz/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Takrorlanmasin</string>\n  <string name=\"exo_media_action_repeat_one_description\">Bittasini takrorlash</string>\n  <string name=\"exo_media_action_repeat_all_description\">Hammasini takrorlash</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-vi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Không lặp lại</string>\n  <string name=\"exo_media_action_repeat_one_description\">Lặp lại một</string>\n  <string name=\"exo_media_action_repeat_all_description\">Lặp lại tất cả</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">不重复播放</string>\n  <string name=\"exo_media_action_repeat_one_description\">重复播放一项</string>\n  <string name=\"exo_media_action_repeat_all_description\">全部重复播放</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">不重複播放</string>\n  <string name=\"exo_media_action_repeat_one_description\">重複播放一個</string>\n  <string name=\"exo_media_action_repeat_all_description\">全部重複播放</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">不重複播放</string>\n  <string name=\"exo_media_action_repeat_one_description\">重複播放單一項目</string>\n  <string name=\"exo_media_action_repeat_all_description\">重複播放所有項目</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/mediasession/src/main/res/values-zu/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <string name=\"exo_media_action_repeat_off_description\">Phinda okungekho</string>\n  <string name=\"exo_media_action_repeat_one_description\">Phinda okukodwa</string>\n  <string name=\"exo_media_action_repeat_all_description\">Phinda konke</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/okhttp/README.md",
    "content": "# ExoPlayer OkHttp extension #\n\nThe OkHttp extension is an [HttpDataSource][] implementation using Square's\n[OkHttp][].\n\n[HttpDataSource]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html\n[OkHttp]: https://square.github.io/okhttp/\n\n## License note ##\n\nPlease note that whilst the code in this repository is licensed under\n[Apache 2.0][], using this extension requires depending on OkHttp, which is\nlicensed separately.\n\n[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE\n\n## Getting the extension ##\n\nThe easiest way to use the extension is to add it as a gradle dependency:\n\n```gradle\nimplementation 'com.google.android.exoplayer:extension-okhttp:2.X.X'\n```\n\nwhere `2.X.X` is the version, which must match the version of the ExoPlayer\nlibrary being used.\n\nAlternatively, you can clone the ExoPlayer repository and depend on the module\nlocally. Instructions for doing this can be found in ExoPlayer's\n[top level README][].\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n\n## Using the extension ##\n\nExoPlayer requests data through `DataSource` instances. These instances are\neither instantiated and injected from application code, or obtained from\ninstances of `DataSource.Factory` that are instantiated and injected from\napplication code.\n\nIf your application only needs to play http(s) content, using the OkHttp\nextension is as simple as updating any `DataSource`s and `DataSource.Factory`\ninstantiations in your application code to use `OkHttpDataSource` and\n`OkHttpDataSourceFactory` respectively. If your application also needs to play\nnon-http(s) content such as local files, use\n```\nnew DefaultDataSource(\n    ...\n    new OkHttpDataSource(...) /* baseDataSource argument */);\n```\nand\n```\nnew DefaultDataSourceFactory(\n    ...\n    new OkHttpDataSourceFactory(...) /* baseDataSourceFactory argument */);\n```\nrespectively.\n\n## Links ##\n\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.okhttp.*`\n  belong to this module.\n\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/okhttp/build.gradle",
    "content": "// Copyright (C) 2016 The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n        consumerProguardFiles 'proguard-rules.txt'\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    implementation 'androidx.annotation:annotation:1.1.0'\n    compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion\n    api 'com.squareup.okhttp3:okhttp:3.12.5'\n}\n\next {\n    javadocTitle = 'OkHttp extension'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'extension-okhttp'\n    releaseDescription = 'OkHttp extension for ExoPlayer.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/okhttp/proguard-rules.txt",
    "content": "# Proguard rules specific to the OkHttp extension.\n\n# Options specified by https://github.com/square/okhttp/blob/master/README.md\n-dontwarn okhttp3.**\n-dontwarn okio.**\n-dontwarn javax.annotation.**\n-dontwarn org.conscrypt.**\n-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/okhttp/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest package=\"com.google.android.exoplayer2.ext.okhttp\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.okhttp;\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.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.metadata.icy.IcyHeaders;\nimport com.google.android.exoplayer2.upstream.BaseDataSource;\nimport com.google.android.exoplayer2.upstream.DataSourceException;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.HttpDataSource;\nimport com.google.android.exoplayer2.util.Assertions;\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.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport okhttp3.CacheControl;\nimport okhttp3.Call;\nimport okhttp3.HttpUrl;\nimport okhttp3.MediaType;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\nimport okhttp3.ResponseBody;\n\n/** An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}. */\npublic class OkHttpDataSource extends BaseDataSource implements HttpDataSource {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.okhttp\");\n  }\n\n  private static final byte[] SKIP_BUFFER = new byte[4096];\n\n  private final Call.Factory callFactory;\n  private final RequestProperties requestProperties;\n\n  private final @Nullable String userAgent;\n  private final @Nullable Predicate<String> contentTypePredicate;\n  private final @Nullable CacheControl cacheControl;\n  private final @Nullable RequestProperties defaultRequestProperties;\n\n  private @Nullable DataSpec dataSpec;\n  private @Nullable Response response;\n  private @Nullable InputStream responseByteStream;\n  private boolean opened;\n\n  private long bytesToSkip;\n  private long bytesToRead;\n\n  private long bytesSkipped;\n  private long bytesRead;\n\n  /**\n   * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use\n   *     by the source.\n   * @param userAgent An optional User-Agent string.\n   */\n  public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) {\n    this(callFactory, userAgent, /* contentTypePredicate= */ null);\n  }\n\n  /**\n   * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use\n   *     by the source.\n   * @param userAgent An optional User-Agent string.\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   */\n  public OkHttpDataSource(\n      Call.Factory callFactory,\n      @Nullable String userAgent,\n      @Nullable Predicate<String> contentTypePredicate) {\n    this(\n        callFactory,\n        userAgent,\n        contentTypePredicate,\n        /* cacheControl= */ null,\n        /* defaultRequestProperties= */ null);\n  }\n\n  /**\n   * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use\n   *     by the source.\n   * @param userAgent An optional User-Agent string.\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 cacheControl An optional {@link CacheControl} for setting the Cache-Control header.\n   * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to\n   *     the server as HTTP headers on every request.\n   */\n  public OkHttpDataSource(\n      Call.Factory callFactory,\n      @Nullable String userAgent,\n      @Nullable Predicate<String> contentTypePredicate,\n      @Nullable CacheControl cacheControl,\n      @Nullable RequestProperties defaultRequestProperties) {\n    super(/* isNetwork= */ true);\n    this.callFactory = Assertions.checkNotNull(callFactory);\n    this.userAgent = userAgent;\n    this.contentTypePredicate = contentTypePredicate;\n    this.cacheControl = cacheControl;\n    this.defaultRequestProperties = defaultRequestProperties;\n    this.requestProperties = new RequestProperties();\n  }\n\n  @Override\n  public @Nullable Uri getUri() {\n    return response == null ? null : Uri.parse(response.request().url().toString());\n  }\n\n  @Override\n  public int getResponseCode() {\n    return response == null ? -1 : response.code();\n  }\n\n  @Override\n  public Map<String, List<String>> getResponseHeaders() {\n    return response == null ? Collections.emptyMap() : response.headers().toMultimap();\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  @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\n    Request request = makeRequest(dataSpec);\n    Response response;\n    ResponseBody responseBody;\n    try {\n      this.response = callFactory.newCall(request).execute();\n      response = this.response;\n      responseBody = Assertions.checkNotNull(response.body());\n      responseByteStream = responseBody.byteStream();\n    } catch (IOException e) {\n      throw new HttpDataSourceException(\n          \"Unable to connect to \" + dataSpec.uri, e, dataSpec, HttpDataSourceException.TYPE_OPEN);\n    }\n\n    int responseCode = response.code();\n\n    // Check for a valid response code.\n    if (!response.isSuccessful()) {\n      Map<String, List<String>> headers = response.headers().toMultimap();\n      closeConnectionQuietly();\n      InvalidResponseCodeException exception =\n          new InvalidResponseCodeException(responseCode, response.message(), 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    MediaType mediaType = responseBody.contentType();\n    String contentType = mediaType != null ? mediaType.toString() : \"\";\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    if (dataSpec.length != C.LENGTH_UNSET) {\n      bytesToRead = dataSpec.length;\n    } else {\n      long contentLength = responseBody.contentLength();\n      bytesToRead = contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;\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(\n          e, Assertions.checkNotNull(dataSpec), HttpDataSourceException.TYPE_READ);\n    }\n  }\n\n  @Override\n  public void close() throws HttpDataSourceException {\n    if (opened) {\n      opened = false;\n      transferEnded();\n      closeConnectionQuietly();\n    }\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  /** Establishes a connection. */\n  private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException {\n    long position = dataSpec.position;\n    long length = dataSpec.length;\n\n    HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());\n    if (url == null) {\n      throw new HttpDataSourceException(\n          \"Malformed URL\", dataSpec, HttpDataSourceException.TYPE_OPEN);\n    }\n\n    Request.Builder builder = new Request.Builder().url(url);\n    if (cacheControl != null) {\n      builder.cacheControl(cacheControl);\n    }\n    if (defaultRequestProperties != null) {\n      for (Map.Entry<String, String> property : defaultRequestProperties.getSnapshot().entrySet()) {\n        builder.header(property.getKey(), property.getValue());\n      }\n    }\n    for (Map.Entry<String, String> property : requestProperties.getSnapshot().entrySet()) {\n      builder.header(property.getKey(), property.getValue());\n    }\n\n    // MOD: apply headers from DataSpec\n    for (Map.Entry<String, String> headerEntry : dataSpec.httpRequestHeaders.entrySet()) {\n      builder.header(headerEntry.getKey(), headerEntry.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      builder.addHeader(\"Range\", rangeRequest);\n    }\n    if (userAgent != null) {\n      builder.addHeader(\"User-Agent\", userAgent);\n    }\n    if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {\n      builder.addHeader(\"Accept-Encoding\", \"identity\");\n    }\n    if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {\n      builder.addHeader(\n          IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,\n          IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);\n    }\n    RequestBody requestBody = null;\n    if (dataSpec.httpBody != null) {\n      requestBody = RequestBody.create(null, dataSpec.httpBody);\n    } else if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {\n      // OkHttp requires a non-null body for POST requests.\n      requestBody = RequestBody.create(null, Util.EMPTY_BYTE_ARRAY);\n    }\n    builder.method(dataSpec.getHttpMethodString(), requestBody);\n    return builder.build();\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    while (bytesSkipped != bytesToSkip) {\n      int readLength = (int) Math.min(bytesToSkip - bytesSkipped, SKIP_BUFFER.length);\n      int read = castNonNull(responseByteStream).read(SKIP_BUFFER, 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\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 = castNonNull(responseByteStream).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   * Closes the current connection quietly, if there is one.\n   */\n  private void closeConnectionQuietly() {\n    if (response != null) {\n      Assertions.checkNotNull(response.body()).close();\n      response = null;\n    }\n    responseByteStream = null;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.okhttp;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.upstream.HttpDataSource;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.Factory;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport okhttp3.CacheControl;\nimport okhttp3.Call;\n\n/**\n * A {@link Factory} that produces {@link OkHttpDataSource}.\n */\npublic final class OkHttpDataSourceFactory extends BaseFactory {\n\n  private final Call.Factory callFactory;\n  private final @Nullable String userAgent;\n  private final @Nullable TransferListener listener;\n  private final @Nullable CacheControl cacheControl;\n\n  /**\n   * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use\n   *     by the sources created by the factory.\n   * @param userAgent An optional User-Agent string.\n   */\n  public OkHttpDataSourceFactory(Call.Factory callFactory, @Nullable String userAgent) {\n    this(callFactory, userAgent, /* listener= */ null, /* cacheControl= */ null);\n  }\n\n  /**\n   * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use\n   *     by the sources created by the factory.\n   * @param userAgent An optional User-Agent string.\n   * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.\n   */\n  public OkHttpDataSourceFactory(\n      Call.Factory callFactory, @Nullable String userAgent, @Nullable CacheControl cacheControl) {\n    this(callFactory, userAgent, /* listener= */ null, cacheControl);\n  }\n\n  /**\n   * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use\n   *     by the sources created by the factory.\n   * @param userAgent An optional User-Agent string.\n   * @param listener An optional listener.\n   */\n  public OkHttpDataSourceFactory(\n      Call.Factory callFactory, @Nullable String userAgent, @Nullable TransferListener listener) {\n    this(callFactory, userAgent, listener, /* cacheControl= */ null);\n  }\n\n  /**\n   * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use\n   *     by the sources created by the factory.\n   * @param userAgent An optional User-Agent string.\n   * @param listener An optional listener.\n   * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.\n   */\n  public OkHttpDataSourceFactory(\n      Call.Factory callFactory,\n      @Nullable String userAgent,\n      @Nullable TransferListener listener,\n      @Nullable CacheControl cacheControl) {\n    this.callFactory = callFactory;\n    this.userAgent = userAgent;\n    this.listener = listener;\n    this.cacheControl = cacheControl;\n  }\n\n  @Override\n  protected OkHttpDataSource createDataSourceInternal(\n      HttpDataSource.RequestProperties defaultRequestProperties) {\n    OkHttpDataSource dataSource =\n        new OkHttpDataSource(\n            callFactory,\n            userAgent,\n            /* contentTypePredicate= */ null,\n            cacheControl,\n            defaultRequestProperties);\n    if (listener != null) {\n      dataSource.addTransferListener(listener);\n    }\n    return dataSource;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/README.md",
    "content": "# ExoPlayer Opus extension #\n\nThe Opus extension provides `LibopusAudioRenderer`, which uses libopus (the Opus\ndecoding library) to decode Opus audio.\n\n## License note ##\n\nPlease note that whilst the code in this repository is licensed under\n[Apache 2.0][], using this extension also requires building and including one or\nmore external libraries as described below. These are licensed separately.\n\n[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE\n\n## Build instructions ##\n\nTo use this extension you need to clone the ExoPlayer repository and depend on\nits modules locally. Instructions for doing this can be found in ExoPlayer's\n[top level README][].\n\nIn addition, it's necessary to build the extension's native components as\nfollows:\n\n* Set the following environment variables:\n\n```\ncd \"<path to exoplayer checkout>\"\nEXOPLAYER_ROOT=\"$(pwd)\"\nOPUS_EXT_PATH=\"${EXOPLAYER_ROOT}/extensions/opus/src/main\"\n```\n\n* Download the [Android NDK][] and set its location in an environment variable:\n\n```\nNDK_PATH=\"<path to Android NDK>\"\n```\n\n* Fetch libopus:\n\n```\ncd \"${OPUS_EXT_PATH}/jni\" && \\\ngit clone https://git.xiph.org/opus.git libopus\n```\n\n* Run the script to convert arm assembly to NDK compatible format:\n\n```\ncd ${OPUS_EXT_PATH}/jni && ./convert_android_asm.sh\n```\n\n* Build the JNI native libraries from the command line:\n\n```\ncd \"${OPUS_EXT_PATH}\"/jni && \\\n${NDK_PATH}/ndk-build APP_ABI=all -j4\n```\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html\n\n## Notes ##\n\n* Every time there is a change to the libopus checkout:\n  * Arm assembly should be converted by running `convert_android_asm.sh`\n  * Clean and re-build the project.\n* If you want to use your own version of libopus, place it in\n  `${OPUS_EXT_PATH}/jni/libopus`.\n\n## Using the extension ##\n\nOnce you've followed the instructions above to check out, build and depend on\nthe extension, the next step is to tell ExoPlayer to use `LibopusAudioRenderer`.\nHow you do this depends on which player API you're using:\n\n* If you're passing a `DefaultRenderersFactory` to\n  `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by\n  setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory`\n  constructor to `EXTENSION_RENDERER_MODE_ON`. This will use\n  `LibopusAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't\n  support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give\n  `LibopusAudioRenderer` priority over `MediaCodecAudioRenderer`.\n* If you've subclassed `DefaultRenderersFactory`, add a `LibopusAudioRenderer`\n  to the output list in `buildAudioRenderers`. ExoPlayer will use the first\n  `Renderer` in the list that supports the input media format.\n* If you've implemented your own `RenderersFactory`, return a\n  `LibopusAudioRenderer` instance from `createRenderers`. ExoPlayer will use the\n  first `Renderer` in the returned array that supports the input media format.\n* If you're using `ExoPlayerFactory.newInstance`, pass a `LibopusAudioRenderer`\n  in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the\n  list that supports the input media format.\n\nNote: These instructions assume you're using `DefaultTrackSelector`. If you have\na custom track selector the choice of `Renderer` is up to your implementation,\nso you need to make sure you are passing an `LibopusAudioRenderer` to the\nplayer, then implement your own logic to use the renderer for a given track.\n\n## Links ##\n\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.opus.*`\n  belong to this module.\n\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/build.gradle",
    "content": "// Copyright (C) 2016 The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n        consumerProguardFiles 'proguard-rules.txt'\n        testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'\n    }\n\n    sourceSets.main {\n        jniLibs.srcDir 'src/main/libs'\n        jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    implementation 'androidx.annotation:annotation:1.1.0'\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n    androidTestImplementation 'androidx.test:runner:' + androidXTestVersion\n    androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion\n}\n\next {\n    javadocTitle = 'Opus extension'\n}\napply from: '../../javadoc_library.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/proguard-rules.txt",
    "content": "# Proguard rules specific to the Opus extension.\n\n# This prevents the names of native methods from being obfuscated.\n-keepclasseswithmembernames class * {\n    native <methods>;\n}\n\n# Some members of this class are being accessed from native methods. Keep them unobfuscated.\n-keep class com.google.android.exoplayer2.decoder.SimpleOutputBuffer {\n    *;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/androidTest/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"com.google.android.exoplayer2.ext.opus.test\">\n\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n  <uses-sdk/>\n\n  <application\n      android:allowBackup=\"false\"\n      tools:ignore=\"MissingApplicationIcon,HardcodedDebugMode\">\n    <uses-library android:name=\"android.test.runner\"/>\n  </application>\n\n  <instrumentation\n      android:targetPackage=\"com.google.android.exoplayer2.ext.opus.test\"\n    android:name=\"androidx.test.runner.AndroidJUnitRunner\"/>\n\n</manifest>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.opus;\n\nimport static org.junit.Assert.fail;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Looper;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.ExoPlayerFactory;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.ProgressiveMediaSource;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Playback tests using {@link LibopusAudioRenderer}. */\n@RunWith(AndroidJUnit4.class)\npublic class OpusPlaybackTest {\n\n  private static final String BEAR_OPUS_URI = \"asset:///bear-opus.webm\";\n\n  @Before\n  public void setUp() {\n    if (!OpusLibrary.isAvailable()) {\n      fail(\"Opus library not available.\");\n    }\n  }\n\n  @Test\n  public void testBasicPlayback() throws Exception {\n    playUri(BEAR_OPUS_URI);\n  }\n\n  private void playUri(String uri) throws Exception {\n    TestPlaybackRunnable testPlaybackRunnable =\n        new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());\n    Thread thread = new Thread(testPlaybackRunnable);\n    thread.start();\n    thread.join();\n    if (testPlaybackRunnable.playbackException != null) {\n      throw testPlaybackRunnable.playbackException;\n    }\n  }\n\n  private static class TestPlaybackRunnable implements Player.EventListener, Runnable {\n\n    private final Context context;\n    private final Uri uri;\n\n    private ExoPlayer player;\n    private ExoPlaybackException playbackException;\n\n    public TestPlaybackRunnable(Uri uri, Context context) {\n      this.uri = uri;\n      this.context = context;\n    }\n\n    @Override\n    public void run() {\n      Looper.prepare();\n      LibopusAudioRenderer audioRenderer = new LibopusAudioRenderer();\n      DefaultTrackSelector trackSelector = new DefaultTrackSelector();\n      player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);\n      player.addListener(this);\n      MediaSource mediaSource =\n          new ProgressiveMediaSource.Factory(\n                  new DefaultDataSourceFactory(context, \"ExoPlayerExtOpusTest\"),\n                  MatroskaExtractor.FACTORY)\n              .createMediaSource(uri);\n      player.prepare(mediaSource);\n      player.setPlayWhenReady(true);\n      Looper.loop();\n    }\n\n    @Override\n    public void onPlayerError(ExoPlaybackException error) {\n      playbackException = error;\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n      if (playbackState == Player.STATE_ENDED\n          || (playbackState == Player.STATE_IDLE && playbackException != null)) {\n        player.release();\n        Looper.myLooper().quit();\n      }\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest package=\"com.google.android.exoplayer2.ext.opus\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.opus;\n\nimport android.os.Handler;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.audio.AudioProcessor;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener;\nimport com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.ExoMediaCrypto;\nimport com.google.android.exoplayer2.util.MimeTypes;\n\n/**\n * Decodes and renders audio using the native Opus decoder.\n */\npublic final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {\n\n  /** The number of input and output buffers. */\n  private static final int NUM_BUFFERS = 16;\n  /** The default input buffer size. */\n  private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;\n\n  private OpusDecoder decoder;\n\n  public LibopusAudioRenderer() {\n    this(null, 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 LibopusAudioRenderer(\n      Handler eventHandler,\n      AudioRendererEventListener eventListener,\n      AudioProcessor... audioProcessors) {\n    super(eventHandler, eventListener, 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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output.\n   */\n  public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,\n      DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,\n      AudioProcessor... audioProcessors) {\n    super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys,\n        audioProcessors);\n  }\n\n  @Override\n  protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,\n      Format format) {\n    if (!OpusLibrary.isAvailable()\n        || !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) {\n      return FORMAT_UNSUPPORTED_TYPE;\n    } else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) {\n      return FORMAT_UNSUPPORTED_SUBTYPE;\n    } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {\n      return FORMAT_UNSUPPORTED_DRM;\n    } else {\n      return FORMAT_HANDLED;\n    }\n  }\n\n  @Override\n  protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)\n      throws OpusDecoderException {\n    int initialInputBufferSize =\n        format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;\n    decoder =\n        new OpusDecoder(\n            NUM_BUFFERS,\n            NUM_BUFFERS,\n            initialInputBufferSize,\n            format.initializationData,\n            mediaCrypto);\n    return decoder;\n  }\n\n  @Override\n  protected Format getOutputFormat() {\n    return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,\n        Format.NO_VALUE, decoder.getChannelCount(), decoder.getSampleRate(), C.ENCODING_PCM_16BIT,\n        null, null, 0, null);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.opus;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.decoder.CryptoInfo;\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.DecryptionException;\nimport com.google.android.exoplayer2.drm.ExoMediaCrypto;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.List;\n\n/**\n * Opus decoder.\n */\n/* package */ final class OpusDecoder extends\n    SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, OpusDecoderException> {\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 NO_ERROR = 0;\n  private static final int DECODE_ERROR = -1;\n  private static final int DRM_ERROR = -2;\n\n  private final ExoMediaCrypto exoMediaCrypto;\n\n  private final int channelCount;\n  private final int headerSkipSamples;\n  private final int headerSeekPreRollSamples;\n  private final long nativeDecoderContext;\n\n  private int skipSamples;\n\n  /**\n   * Creates an Opus decoder.\n   *\n   * @param numInputBuffers The number of input buffers.\n   * @param numOutputBuffers The number of output buffers.\n   * @param initialInputBufferSize The initial size of each input buffer.\n   * @param initializationData Codec-specific initialization data. The first element must contain an\n   *     opus header. Optionally, the list may contain two additional buffers, which must contain\n   *     the encoder delay and seek pre roll values in nanoseconds, encoded as longs.\n   * @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted\n   *     content. Maybe null and can be ignored if decoder does not handle encrypted content.\n   * @throws OpusDecoderException Thrown if an exception occurs when initializing the decoder.\n   */\n  public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,\n      List<byte[]> initializationData, ExoMediaCrypto exoMediaCrypto) throws OpusDecoderException {\n    super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);\n    if (!OpusLibrary.isAvailable()) {\n      throw new OpusDecoderException(\"Failed to load decoder native libraries.\");\n    }\n    this.exoMediaCrypto = exoMediaCrypto;\n    if (exoMediaCrypto != null && !OpusLibrary.opusIsSecureDecodeSupported()) {\n      throw new OpusDecoderException(\"Opus decoder does not support secure decode.\");\n    }\n    byte[] headerBytes = initializationData.get(0);\n    if (headerBytes.length < 19) {\n      throw new OpusDecoderException(\"Header size is too small.\");\n    }\n    channelCount = headerBytes[9] & 0xFF;\n    if (channelCount > 8) {\n      throw new OpusDecoderException(\"Invalid channel count: \" + channelCount);\n    }\n    int preskip = readLittleEndian16(headerBytes, 10);\n    int gain = readLittleEndian16(headerBytes, 16);\n\n    byte[] streamMap = new byte[8];\n    int numStreams;\n    int numCoupled;\n    if (headerBytes[18] == 0) { // Channel mapping\n      // If there is no channel mapping, use the defaults.\n      if (channelCount > 2) { // Maximum channel count with default layout.\n        throw new OpusDecoderException(\"Invalid Header, missing stream map.\");\n      }\n      numStreams = 1;\n      numCoupled = (channelCount == 2) ? 1 : 0;\n      streamMap[0] = 0;\n      streamMap[1] = 1;\n    } else {\n      if (headerBytes.length < 21 + channelCount) {\n        throw new OpusDecoderException(\"Header size is too small.\");\n      }\n      // Read the channel mapping.\n      numStreams = headerBytes[19] & 0xFF;\n      numCoupled = headerBytes[20] & 0xFF;\n      System.arraycopy(headerBytes, 21, streamMap, 0, channelCount);\n    }\n    if (initializationData.size() == 3) {\n      if (initializationData.get(1).length != 8 || initializationData.get(2).length != 8) {\n        throw new OpusDecoderException(\"Invalid Codec Delay or Seek Preroll\");\n      }\n      long codecDelayNs =\n          ByteBuffer.wrap(initializationData.get(1)).order(ByteOrder.nativeOrder()).getLong();\n      long seekPreRollNs =\n          ByteBuffer.wrap(initializationData.get(2)).order(ByteOrder.nativeOrder()).getLong();\n      headerSkipSamples = nsToSamples(codecDelayNs);\n      headerSeekPreRollSamples = nsToSamples(seekPreRollNs);\n    } else {\n      headerSkipSamples = preskip;\n      headerSeekPreRollSamples = DEFAULT_SEEK_PRE_ROLL_SAMPLES;\n    }\n    nativeDecoderContext = opusInit(SAMPLE_RATE, channelCount, numStreams, numCoupled, gain,\n        streamMap);\n    if (nativeDecoderContext == 0) {\n      throw new OpusDecoderException(\"Failed to initialize decoder\");\n    }\n    setInitialInputBufferSize(initialInputBufferSize);\n  }\n\n  @Override\n  public String getName() {\n    return \"libopus\" + OpusLibrary.getVersion();\n  }\n\n  @Override\n  protected DecoderInputBuffer createInputBuffer() {\n    return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);\n  }\n\n  @Override\n  protected SimpleOutputBuffer createOutputBuffer() {\n    return new SimpleOutputBuffer(this);\n  }\n\n  @Override\n  protected OpusDecoderException createUnexpectedDecodeException(Throwable error) {\n    return new OpusDecoderException(\"Unexpected decode error\", error);\n  }\n\n  @Override\n  @Nullable\n  protected OpusDecoderException decode(\n      DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {\n    if (reset) {\n      opusReset(nativeDecoderContext);\n      // When seeking to 0, skip number of samples as specified in opus header. When seeking to\n      // any other time, skip number of samples as specified by seek preroll.\n      skipSamples = (inputBuffer.timeUs == 0) ? headerSkipSamples : headerSeekPreRollSamples;\n    }\n    ByteBuffer inputData = inputBuffer.data;\n    CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;\n    int result = inputBuffer.isEncrypted()\n        ? opusSecureDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(),\n            outputBuffer, SAMPLE_RATE, exoMediaCrypto, cryptoInfo.mode,\n            cryptoInfo.key, cryptoInfo.iv, cryptoInfo.numSubSamples,\n            cryptoInfo.numBytesOfClearData, cryptoInfo.numBytesOfEncryptedData)\n        : opusDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(),\n            outputBuffer);\n    if (result < 0) {\n      if (result == DRM_ERROR) {\n        String message = \"Drm error: \" + opusGetErrorMessage(nativeDecoderContext);\n        DecryptionException cause = new DecryptionException(\n            opusGetErrorCode(nativeDecoderContext), message);\n        return new OpusDecoderException(message, cause);\n      } else {\n        return new OpusDecoderException(\"Decode error: \" + opusGetErrorMessage(result));\n      }\n    }\n\n    ByteBuffer outputData = outputBuffer.data;\n    outputData.position(0);\n    outputData.limit(result);\n    if (skipSamples > 0) {\n      int bytesPerSample = channelCount * 2;\n      int skipBytes = skipSamples * bytesPerSample;\n      if (result <= skipBytes) {\n        skipSamples -= result / bytesPerSample;\n        outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);\n        outputData.position(result);\n      } else {\n        skipSamples = 0;\n        outputData.position(skipBytes);\n      }\n    }\n    return null;\n  }\n\n  @Override\n  public void release() {\n    super.release();\n    opusClose(nativeDecoderContext);\n  }\n\n  /**\n   * Returns the channel count of output audio.\n   */\n  public int getChannelCount() {\n    return channelCount;\n  }\n\n  /**\n   * Returns the sample rate of output audio.\n   */\n  public int getSampleRate() {\n    return SAMPLE_RATE;\n  }\n\n  private static int nsToSamples(long ns) {\n    return (int) (ns * SAMPLE_RATE / 1000000000);\n  }\n\n  private static int readLittleEndian16(byte[] input, int offset) {\n    int value = input[offset] & 0xFF;\n    value |= (input[offset + 1] & 0xFF) << 8;\n    return value;\n  }\n\n  private native long opusInit(int sampleRate, int channelCount, int numStreams, int numCoupled,\n      int gain, byte[] streamMap);\n  private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize,\n      SimpleOutputBuffer outputBuffer);\n  private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer,\n      int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate,\n      ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,\n      int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);\n  private native void opusClose(long decoder);\n  private native void opusReset(long decoder);\n  private native int opusGetErrorCode(long decoder);\n  private native String opusGetErrorMessage(long decoder);\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoderException.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.opus;\n\nimport com.google.android.exoplayer2.audio.AudioDecoderException;\n\n/**\n * Thrown when an Opus decoder error occurs.\n */\npublic final class OpusDecoderException extends AudioDecoderException {\n\n  /* package */ OpusDecoderException(String message) {\n    super(message);\n  }\n\n  /* package */ OpusDecoderException(String message, Throwable cause) {\n    super(message, cause);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.opus;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.util.LibraryLoader;\n\n/**\n * Configures and queries the underlying native library.\n */\npublic final class OpusLibrary {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.opus\");\n  }\n\n  private static final LibraryLoader LOADER = new LibraryLoader(\"opusV2JNI\");\n\n  private OpusLibrary() {}\n\n  /**\n   * Override the names of the Opus native libraries. If an application wishes to call this method,\n   * it must do so before calling any other method defined by this class, and before instantiating a\n   * {@link LibopusAudioRenderer} instance.\n   *\n   * @param libraries The names of the Opus native libraries.\n   */\n  public static void setLibraries(String... libraries) {\n    LOADER.setLibraries(libraries);\n  }\n\n  /**\n   * Returns whether the underlying library is available, loading it if necessary.\n   */\n  public static boolean isAvailable() {\n    return LOADER.isAvailable();\n  }\n\n  /** Returns the version of the underlying library if available, or null otherwise. */\n  @Nullable\n  public static String getVersion() {\n    return isAvailable() ? opusGetVersion() : null;\n  }\n\n  public static native String opusGetVersion();\n  public static native boolean opusIsSecureDecodeSupported();\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/main/jni/Android.mk",
    "content": "#\n# Copyright (C) 2016 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS 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\nWORKING_DIR := $(call my-dir)\ninclude $(CLEAR_VARS)\n\n# build libopus.a\nLOCAL_PATH := $(WORKING_DIR)\ninclude libopus.mk\n\n# build libopusV2JNI.so\ninclude $(CLEAR_VARS)\nLOCAL_PATH := $(WORKING_DIR)\nLOCAL_MODULE := libopusV2JNI\nLOCAL_ARM_MODE := arm\nLOCAL_CPP_EXTENSION := .cc\nLOCAL_SRC_FILES := opus_jni.cc\nLOCAL_LDLIBS := -llog -lz -lm\nLOCAL_STATIC_LIBRARIES := libopus\ninclude $(BUILD_SHARED_LIBRARY)\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/main/jni/Application.mk",
    "content": "#\n# Copyright (C) 2016 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS 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\nAPP_OPTIM := release\nAPP_STL := gnustl_static\nAPP_CPPFLAGS := -frtti\nAPP_PLATFORM := android-9\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/main/jni/convert_android_asm.sh",
    "content": "#!/bin/bash\n#\n# Copyright (C) 2016 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS 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\nset -e\nASM_CONVERTER=\"./libopus/celt/arm/arm2gnu.pl\"\n\nif [[ ! -x \"${ASM_CONVERTER}\" ]]; then\n  echo \"Please make sure you have checked out libopus.\"\n  exit\nfi\n\nwhile read file; do\n  # This check is required because the ASM conversion script doesn't seem to be\n  # idempotent.\n  if [[ ! \"${file}\" =~ .*_gnu\\.s$ ]]; then\n    gnu_file=\"${file%.s}_gnu.s\"\n    ${ASM_CONVERTER} \"${file}\" > \"${gnu_file}\"\n    # The ASM conversion script replaces includes with *_gnu.S. So, replace\n    # occurences of \"*-gnu.S\" with \"*_gnu.s\".\n    perl -pi -e \"s/-gnu\\.S/_gnu\\.s/g\" \"${gnu_file}\"\n    rm -f \"${file}\"\n  fi\ndone < <(find . -iname '*.s')\n\n# Generate armopts.s from armopts.s.in\nsed \\\n  -e \"s/@OPUS_ARM_MAY_HAVE_EDSP@/1/g\" \\\n  -e \"s/@OPUS_ARM_MAY_HAVE_MEDIA@/1/g\" \\\n  -e \"s/@OPUS_ARM_MAY_HAVE_NEON@/1/g\" \\\n  libopus/celt/arm/armopts.s.in > libopus/celt/arm/armopts.s.temp\n${ASM_CONVERTER} \"libopus/celt/arm/armopts.s.temp\" > \"libopus/celt/arm/armopts_gnu.s\"\nrm \"libopus/celt/arm/armopts.s.temp\"\necho \"Converted all ASM files and generated armopts.s successfully.\"\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/main/jni/libopus.mk",
    "content": "#\n# Copyright (C) 2016 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS 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\nLOCAL_PATH := $(call my-dir)/libopus\n\ninclude $(CLEAR_VARS)\n\ninclude $(LOCAL_PATH)/celt_headers.mk\ninclude $(LOCAL_PATH)/celt_sources.mk\ninclude $(LOCAL_PATH)/opus_headers.mk\ninclude $(LOCAL_PATH)/opus_sources.mk\ninclude $(LOCAL_PATH)/silk_headers.mk\ninclude $(LOCAL_PATH)/silk_sources.mk\n\nLOCAL_MODULE := libopus\nLOCAL_ARM_MODE := arm\nLOCAL_CFLAGS := -DOPUS_BUILD -DFIXED_POINT -DUSE_ALLOCA -DHAVE_LRINT \\\n                -DHAVE_LRINTF\nLOCAL_C_INCLUDES := $(LOCAL_PATH)/include $(LOCAL_PATH)/src \\\n                    $(LOCAL_PATH)/silk $(LOCAL_PATH)/celt \\\n                    $(LOCAL_PATH)/silk/fixed\nLOCAL_SRC_FILES := $(CELT_SOURCES) $(OPUS_SOURCES) $(OPUS_SOURCES_FLOAT) \\\n                   $(SILK_SOURCES) $(SILK_SOURCES_FIXED)\n\nifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),)\nLOCAL_SRC_FILES += $(CELT_SOURCES_ARM)\nLOCAL_SRC_FILES += celt/arm/armopts_gnu.s.neon\nLOCAL_SRC_FILES += $(subst .s,_gnu.s.neon,$(CELT_SOURCES_ARM_ASM))\nLOCAL_CFLAGS += -DOPUS_ARM_ASM -DOPUS_ARM_INLINE_ASM -DOPUS_ARM_INLINE_EDSP \\\n                -DOPUS_ARM_INLINE_MEDIA -DOPUS_ARM_INLINE_NEON \\\n                -DOPUS_ARM_MAY_HAVE_NEON -DOPUS_ARM_MAY_HAVE_MEDIA \\\n                -DOPUS_ARM_MAY_HAVE_EDSP\nendif\n\nLOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include\n\ninclude $(BUILD_STATIC_LIBRARY)\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/main/jni/opus_jni.cc",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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#include <jni.h>\n\n#include <android/log.h>\n\n#include <cstdlib>\n\n#include \"opus.h\"  // NOLINT\n#include \"opus_multistream.h\"  // NOLINT\n\n#define LOG_TAG \"opus_jni\"\n#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, \\\n                                             __VA_ARGS__))\n\n#define DECODER_FUNC(RETURN_TYPE, NAME, ...) \\\n  extern \"C\" { \\\n  JNIEXPORT RETURN_TYPE \\\n    Java_com_google_android_exoplayer2_ext_opus_OpusDecoder_ ## NAME \\\n      (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\\\n  } \\\n  JNIEXPORT RETURN_TYPE \\\n    Java_com_google_android_exoplayer2_ext_opus_OpusDecoder_ ## NAME \\\n      (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\\\n\n#define LIBRARY_FUNC(RETURN_TYPE, NAME, ...) \\\n  extern \"C\" { \\\n  JNIEXPORT RETURN_TYPE \\\n    Java_com_google_android_exoplayer2_ext_opus_OpusLibrary_ ## NAME \\\n      (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\\\n  } \\\n  JNIEXPORT RETURN_TYPE \\\n    Java_com_google_android_exoplayer2_ext_opus_OpusLibrary_ ## NAME \\\n      (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\\\n\n// JNI references for SimpleOutputBuffer class.\nstatic jmethodID outputBufferInit;\n\njint JNI_OnLoad(JavaVM* vm, void* reserved) {\n  JNIEnv* env;\n  if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {\n    return -1;\n  }\n  return JNI_VERSION_1_6;\n}\n\nstatic const int kBytesPerSample = 2;  // opus fixed point uses 16 bit samples.\nstatic const int kMaxOpusOutputPacketSizeSamples = 960 * 6;\nstatic int channelCount;\nstatic int errorCode;\n\nDECODER_FUNC(jlong, opusInit, jint sampleRate, jint channelCount,\n     jint numStreams, jint numCoupled, jint gain, jbyteArray jStreamMap) {\n  int status = OPUS_INVALID_STATE;\n  ::channelCount = channelCount;\n  errorCode = 0;\n  jbyte* streamMapBytes = env->GetByteArrayElements(jStreamMap, 0);\n  uint8_t* streamMap = reinterpret_cast<uint8_t*>(streamMapBytes);\n  OpusMSDecoder* decoder = opus_multistream_decoder_create(\n      sampleRate, channelCount, numStreams, numCoupled, streamMap, &status);\n  env->ReleaseByteArrayElements(jStreamMap, streamMapBytes, 0);\n  if (!decoder || status != OPUS_OK) {\n    LOGE(\"Failed to create Opus Decoder; status=%s\", opus_strerror(status));\n    return 0;\n  }\n  status = opus_multistream_decoder_ctl(decoder, OPUS_SET_GAIN(gain));\n  if (status != OPUS_OK) {\n    LOGE(\"Failed to set Opus header gain; status=%s\", opus_strerror(status));\n    return 0;\n  }\n\n  // Populate JNI References.\n  const jclass outputBufferClass = env->FindClass(\n      \"com/google/android/exoplayer2/decoder/SimpleOutputBuffer\");\n  outputBufferInit = env->GetMethodID(outputBufferClass, \"init\",\n      \"(JI)Ljava/nio/ByteBuffer;\");\n\n  return reinterpret_cast<intptr_t>(decoder);\n}\n\nDECODER_FUNC(jint, opusDecode, jlong jDecoder, jlong jTimeUs,\n     jobject jInputBuffer, jint inputSize, jobject jOutputBuffer) {\n  OpusMSDecoder* decoder = reinterpret_cast<OpusMSDecoder*>(jDecoder);\n  const uint8_t* inputBuffer =\n      reinterpret_cast<const uint8_t*>(\n          env->GetDirectBufferAddress(jInputBuffer));\n\n  const jint outputSize =\n      kMaxOpusOutputPacketSizeSamples * kBytesPerSample * channelCount;\n\n  env->CallObjectMethod(jOutputBuffer, outputBufferInit, jTimeUs, outputSize);\n  if (env->ExceptionCheck()) {\n    // Exception is thrown in Java when returning from the native call.\n    return -1;\n  }\n  const jobject jOutputBufferData = env->CallObjectMethod(jOutputBuffer,\n      outputBufferInit, jTimeUs, outputSize);\n  if (env->ExceptionCheck()) {\n    // Exception is thrown in Java when returning from the native call.\n    return -1;\n  }\n\n  int16_t* outputBufferData = reinterpret_cast<int16_t*>(\n      env->GetDirectBufferAddress(jOutputBufferData));\n  int sampleCount = opus_multistream_decode(decoder, inputBuffer, inputSize,\n      outputBufferData, kMaxOpusOutputPacketSizeSamples, 0);\n  // record error code\n  errorCode = (sampleCount < 0) ? sampleCount : 0;\n  return (sampleCount < 0) ? sampleCount\n      : sampleCount * kBytesPerSample * channelCount;\n}\n\nDECODER_FUNC(jint, opusSecureDecode, jlong jDecoder, jlong jTimeUs,\n     jobject jInputBuffer, jint inputSize, jobject jOutputBuffer,\n     jint sampleRate, jobject mediaCrypto, jint inputMode, jbyteArray key,\n     jbyteArray javaIv, jint inputNumSubSamples, jintArray numBytesOfClearData,\n     jintArray numBytesOfEncryptedData) {\n  // Doesn't support\n  // Java client should have checked vpxSupportSecureDecode\n  // and avoid calling this\n  // return -2 (DRM Error)\n  return -2;\n}\n\nDECODER_FUNC(void, opusClose, jlong jDecoder) {\n  OpusMSDecoder* decoder = reinterpret_cast<OpusMSDecoder*>(jDecoder);\n  opus_multistream_decoder_destroy(decoder);\n}\n\nDECODER_FUNC(void, opusReset, jlong jDecoder) {\n  OpusMSDecoder* decoder = reinterpret_cast<OpusMSDecoder*>(jDecoder);\n  opus_multistream_decoder_ctl(decoder, OPUS_RESET_STATE);\n}\n\nDECODER_FUNC(jstring, opusGetErrorMessage, jlong jContext) {\n  return env->NewStringUTF(opus_strerror(errorCode));\n}\n\nDECODER_FUNC(jint, opusGetErrorCode, jlong jContext) {\n  return errorCode;\n}\n\nLIBRARY_FUNC(jstring, opusIsSecureDecodeSupported) {\n  // Doesn't support\n  return 0;\n}\n\nLIBRARY_FUNC(jstring, opusGetVersion) {\n  return env->NewStringUTF(opus_get_version_string());\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<manifest package=\"com.google.android.exoplayer2.ext.opus\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/opus/src/test/java/com/google/android/exoplayer2/ext/opus/DefaultRenderersFactoryTest.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.ext.opus;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link DefaultRenderersFactoryTest} with {@link LibopusAudioRenderer}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DefaultRenderersFactoryTest {\n\n  @Test\n  public void createRenderers_instantiatesVpxRenderer() {\n    DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(\n        LibopusAudioRenderer.class, C.TRACK_TYPE_AUDIO);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/rtmp/README.md",
    "content": "# ExoPlayer RTMP extension #\n\nThe RTMP extension is a [DataSource][] implementation for playing [RTMP][]\nstreams using [LibRtmp Client for Android][].\n\n[DataSource]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/DataSource.html\n[RTMP]: https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol\n[LibRtmp Client for Android]: https://github.com/ant-media/LibRtmp-Client-for-Android\n\n## License note ##\n\nPlease note that whilst the code in this repository is licensed under\n[Apache 2.0][], using this extension requires depending on LibRtmp Client for\nAndroid, which is licensed separately.\n\n[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE\n\n## Getting the extension ##\n\nThe easiest way to use the extension is to add it as a gradle dependency:\n\n```gradle\nimplementation 'com.google.android.exoplayer:extension-rtmp:2.X.X'\n```\n\nwhere `2.X.X` is the version, which must match the version of the ExoPlayer\nlibrary being used.\n\nAlternatively, you can clone the ExoPlayer repository and depend on the module\nlocally. Instructions for doing this can be found in ExoPlayer's\n[top level README][].\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n\n## Using the extension ##\n\nExoPlayer requests data through `DataSource` instances. These instances are\neither instantiated and injected from application code, or obtained from\ninstances of `DataSource.Factory` that are instantiated and injected from\napplication code.\n\n`DefaultDataSource` will automatically use the RTMP extension whenever it's\navailable. Hence if your application is using `DefaultDataSource` or\n`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as\nadding a dependency to the RTMP extension as described above. No changes to your\napplication code are required. Alternatively, if you know that your application\ndoesn't need to handle any other protocols, you can update any `DataSource`s and\n`DataSource.Factory` instantiations in your application code to use\n`RtmpDataSource` and `RtmpDataSourceFactory` directly.\n\n## Links ##\n\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.rtmp.*`\n  belong to this module.\n\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/rtmp/build.gradle",
    "content": "// Copyright (C) 2016 The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    implementation 'net.butterflytv.utils:rtmp-client:3.1.0'\n    implementation 'androidx.annotation:annotation:1.1.0'\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n}\n\next {\n    javadocTitle = 'RTMP extension'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'extension-rtmp'\n    releaseDescription = 'RTMP extension for ExoPlayer.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/rtmp/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n\n<manifest package=\"com.google.android.exoplayer2.ext.rtmp\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.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.ext.rtmp;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.upstream.BaseDataSource;\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 net.butterflytv.rtmp_client.RtmpClient;\nimport net.butterflytv.rtmp_client.RtmpClient.RtmpIOException;\n\n/** A Real-Time Messaging Protocol (RTMP) {@link DataSource}. */\npublic final class RtmpDataSource extends BaseDataSource {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.rtmp\");\n  }\n\n  private RtmpClient rtmpClient;\n  private Uri uri;\n\n  public RtmpDataSource() {\n    super(/* isNetwork= */ true);\n  }\n\n  /**\n   * @param listener An optional listener.\n   * @deprecated Use {@link #RtmpDataSource()} and {@link #addTransferListener(TransferListener)}.\n   */\n  @Deprecated\n  public RtmpDataSource(@Nullable TransferListener listener) {\n    this();\n    if (listener != null) {\n      addTransferListener(listener);\n    }\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws RtmpIOException {\n    transferInitializing(dataSpec);\n    rtmpClient = new RtmpClient();\n    rtmpClient.open(dataSpec.uri.toString(), false);\n\n    this.uri = dataSpec.uri;\n    transferStarted(dataSpec);\n    return C.LENGTH_UNSET;\n  }\n\n  @Override\n  public int read(byte[] buffer, int offset, int readLength) throws IOException {\n    int bytesRead = rtmpClient.read(buffer, offset, readLength);\n    if (bytesRead == -1) {\n      return C.RESULT_END_OF_INPUT;\n    }\n    bytesTransferred(bytesRead);\n    return bytesRead;\n  }\n\n  @Override\n  public void close() {\n    if (uri != null) {\n      uri = null;\n      transferEnded();\n    }\n    if (rtmpClient != null) {\n      rtmpClient.close();\n      rtmpClient = null;\n    }\n  }\n\n  @Override\n  public Uri getUri() {\n    return uri;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.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.ext.rtmp;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.Factory;\nimport com.google.android.exoplayer2.upstream.TransferListener;\n\n/**\n * A {@link Factory} that produces {@link RtmpDataSource}.\n */\npublic final class RtmpDataSourceFactory implements DataSource.Factory {\n\n  private final @Nullable TransferListener listener;\n\n  public RtmpDataSourceFactory() {\n    this(null);\n  }\n\n  /** @param listener An optional listener. */\n  public RtmpDataSourceFactory(@Nullable TransferListener listener) {\n    this.listener = listener;\n  }\n\n  @Override\n  public RtmpDataSource createDataSource() {\n    RtmpDataSource dataSource = new RtmpDataSource();\n    if (listener != null) {\n      dataSource.addTransferListener(listener);\n    }\n    return dataSource;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/rtmp/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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\n<manifest package=\"com.google.android.exoplayer2.ext.rtmp\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/rtmp/src/test/java/com/google/android/exoplayer2/ext/rtmp/DefaultDataSourceTest.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.ext.rtmp;\n\nimport android.net.Uri;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.DefaultDataSource;\nimport java.io.IOException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link DefaultDataSource} with RTMP URIs. */\n@RunWith(AndroidJUnit4.class)\npublic final class DefaultDataSourceTest {\n\n  @Test\n  public void openRtmpDataSpec_instantiatesRtmpDataSourceViaReflection() throws IOException {\n    DefaultDataSource dataSource =\n        new DefaultDataSource(\n            ApplicationProvider.getApplicationContext(),\n            \"userAgent\",\n            /* allowCrossProtocolRedirects= */ false);\n    DataSpec dataSpec = new DataSpec(Uri.parse(\"rtmp://test.com/stream\"));\n    try {\n      dataSource.open(dataSpec);\n    } catch (UnsatisfiedLinkError e) {\n      // RtmpDataSource was successfully instantiated (test run using Gradle).\n    } catch (UnsupportedOperationException e) {\n      // RtmpDataSource was successfully instantiated (test run using Blaze).\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/README.md",
    "content": "# ExoPlayer VP9 extension #\n\nThe VP9 extension provides `LibvpxVideoRenderer`, which uses libvpx (the VPx\ndecoding library) to decode VP9 video.\n\n## License note ##\n\nPlease note that whilst the code in this repository is licensed under\n[Apache 2.0][], using this extension also requires building and including one or\nmore external libraries as described below. These are licensed separately.\n\n[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE\n\n## Build instructions ##\n\nTo use this extension you need to clone the ExoPlayer repository and depend on\nits modules locally. Instructions for doing this can be found in ExoPlayer's\n[top level README][].\n\nIn addition, it's necessary to build the extension's native components as\nfollows:\n\n* Set the following environment variables:\n\n```\ncd \"<path to exoplayer checkout>\"\nEXOPLAYER_ROOT=\"$(pwd)\"\nVP9_EXT_PATH=\"${EXOPLAYER_ROOT}/extensions/vp9/src/main\"\n```\n\n* Download the [Android NDK][] and set its location in an environment variable.\n  The build configuration has been tested with Android NDK r19c.\n\n```\nNDK_PATH=\"<path to Android NDK>\"\n```\n\n* Fetch libvpx:\n\n```\ncd \"${VP9_EXT_PATH}/jni\" && \\\ngit clone https://chromium.googlesource.com/webm/libvpx libvpx\n```\n\n* Checkout the appropriate branch of libvpx (the scripts and makefiles bundled\n  in this repo are known to work only at specific versions of the library - we\n  will update this periodically as newer versions of libvpx are released):\n\n```\ncd \"${VP9_EXT_PATH}/jni/libvpx\" && \\\ngit checkout tags/v1.8.0 -b v1.8.0\n```\n\n* Run a script that generates necessary configuration files for libvpx:\n\n```\ncd ${VP9_EXT_PATH}/jni && \\\n./generate_libvpx_android_configs.sh\n```\n\n* Build the JNI native libraries from the command line:\n\n```\ncd \"${VP9_EXT_PATH}\"/jni && \\\n${NDK_PATH}/ndk-build APP_ABI=all -j4\n```\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html\n\n## Notes ##\n\n* Every time there is a change to the libvpx checkout:\n  * Android config scripts should be re-generated by running\n    `generate_libvpx_android_configs.sh`\n  * Clean and re-build the project.\n* If you want to use your own version of libvpx, place it in\n  `${VP9_EXT_PATH}/jni/libvpx`. Please note that\n  `generate_libvpx_android_configs.sh` and the makefiles may need to be modified\n  to work with arbitrary versions of libvpx.\n\n## Using the extension ##\n\nOnce you've followed the instructions above to check out, build and depend on\nthe extension, the next step is to tell ExoPlayer to use `LibvpxVideoRenderer`.\nHow you do this depends on which player API you're using:\n\n* If you're passing a `DefaultRenderersFactory` to\n  `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by\n  setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory`\n  constructor to `EXTENSION_RENDERER_MODE_ON`. This will use\n  `LibvpxVideoRenderer` for playback if `MediaCodecVideoRenderer` doesn't\n  support decoding the input VP9 stream. Pass `EXTENSION_RENDERER_MODE_PREFER`\n  to give `LibvpxVideoRenderer` priority over `MediaCodecVideoRenderer`.\n* If you've subclassed `DefaultRenderersFactory`, add a `LibvpxVideoRenderer`\n  to the output list in `buildVideoRenderers`. ExoPlayer will use the first\n  `Renderer` in the list that supports the input media format.\n* If you've implemented your own `RenderersFactory`, return a\n  `LibvpxVideoRenderer` instance from `createRenderers`. ExoPlayer will use the\n  first `Renderer` in the returned array that supports the input media format.\n* If you're using `ExoPlayerFactory.newInstance`, pass a `LibvpxVideoRenderer`\n  in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the\n  list that supports the input media format.\n\nNote: These instructions assume you're using `DefaultTrackSelector`. If you have\na custom track selector the choice of `Renderer` is up to your implementation,\nso you need to make sure you are passing an `LibvpxVideoRenderer` to the\nplayer, then implement your own logic to use the renderer for a given track.\n\n`LibvpxVideoRenderer` can optionally output to a `VpxVideoSurfaceView` when not\nbeing used via `SimpleExoPlayer`, in which case color space conversion will be\nperformed using a GL shader. To enable this mode, send the renderer a message of\ntype `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` with the\n`VpxVideoSurfaceView` as its object, instead of sending `MSG_SET_SURFACE` with a\n`Surface`.\n\n## Links ##\n\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.vp9.*`\n  belong to this module.\n\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/build.gradle",
    "content": "// Copyright (C) 2016 The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n        consumerProguardFiles 'proguard-rules.txt'\n        testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'\n    }\n\n    sourceSets.main {\n        jniLibs.srcDir 'src/main/libs'\n        jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    implementation 'androidx.annotation:annotation:1.1.0'\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n    androidTestImplementation 'androidx.test:runner:' + androidXTestVersion\n    androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion\n    androidTestImplementation 'androidx.test.ext:truth:' + androidXTestVersion\n}\n\next {\n    javadocTitle = 'VP9 extension'\n}\napply from: '../../javadoc_library.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/proguard-rules.txt",
    "content": "# Proguard rules specific to the VP9 extension.\n\n# This prevents the names of native methods from being obfuscated.\n-keepclasseswithmembernames class * {\n    native <methods>;\n}\n\n# Some members of this class are being accessed from native methods. Keep them unobfuscated.\n-keep class com.google.android.exoplayer2.ext.vp9.VpxOutputBuffer {\n    *;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/androidTest/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"com.google.android.exoplayer2.ext.vp9.test\">\n\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n  <uses-sdk/>\n\n  <application\n      android:allowBackup=\"false\"\n      tools:ignore=\"MissingApplicationIcon,HardcodedDebugMode\">\n    <uses-library android:name=\"android.test.runner\"/>\n  </application>\n\n  <instrumentation\n      android:targetPackage=\"com.google.android.exoplayer2.ext.vp9.test\"\n      android:name=\"androidx.test.runner.AndroidJUnitRunner\"/>\n\n</manifest>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.vp9;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Looper;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.ExoPlayerFactory;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.ProgressiveMediaSource;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;\nimport com.google.android.exoplayer2.util.Log;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Playback tests using {@link LibvpxVideoRenderer}. */\n@RunWith(AndroidJUnit4.class)\npublic class VpxPlaybackTest {\n\n  private static final String BEAR_URI = \"asset:///bear-vp9.webm\";\n  private static final String BEAR_ODD_DIMENSIONS_URI = \"asset:///bear-vp9-odd-dimensions.webm\";\n  private static final String ROADTRIP_10BIT_URI = \"asset:///roadtrip-vp92-10bit.webm\";\n  private static final String INVALID_BITSTREAM_URI = \"asset:///invalid-bitstream.webm\";\n\n  private static final String TAG = \"VpxPlaybackTest\";\n\n  @Before\n  public void setUp() {\n    if (!VpxLibrary.isAvailable()) {\n      fail(\"Vpx library not available.\");\n    }\n  }\n\n  @Test\n  public void testBasicPlayback() throws Exception {\n    playUri(BEAR_URI);\n  }\n\n  @Test\n  public void testOddDimensionsPlayback() throws Exception {\n    playUri(BEAR_ODD_DIMENSIONS_URI);\n  }\n\n  @Test\n  public void test10BitProfile2Playback() throws Exception {\n    if (VpxLibrary.isHighBitDepthSupported()) {\n      Log.d(TAG, \"High Bit Depth supported.\");\n      playUri(ROADTRIP_10BIT_URI);\n      return;\n    }\n    Log.d(TAG, \"High Bit Depth not supported.\");\n  }\n\n  @Test\n  public void testInvalidBitstream() {\n    try {\n      playUri(INVALID_BITSTREAM_URI);\n      fail();\n    } catch (Exception e) {\n      assertThat(e.getCause()).isNotNull();\n      assertThat(e.getCause()).isInstanceOf(VpxDecoderException.class);\n    }\n  }\n\n  private void playUri(String uri) throws Exception {\n    TestPlaybackRunnable testPlaybackRunnable =\n        new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());\n    Thread thread = new Thread(testPlaybackRunnable);\n    thread.start();\n    thread.join();\n    if (testPlaybackRunnable.playbackException != null) {\n      throw testPlaybackRunnable.playbackException;\n    }\n  }\n\n  private static class TestPlaybackRunnable implements Player.EventListener, Runnable {\n\n    private final Context context;\n    private final Uri uri;\n\n    private ExoPlayer player;\n    private ExoPlaybackException playbackException;\n\n    public TestPlaybackRunnable(Uri uri, Context context) {\n      this.uri = uri;\n      this.context = context;\n    }\n\n    @Override\n    public void run() {\n      Looper.prepare();\n      LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(0);\n      DefaultTrackSelector trackSelector = new DefaultTrackSelector();\n      player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector);\n      player.addListener(this);\n      MediaSource mediaSource =\n          new ProgressiveMediaSource.Factory(\n                  new DefaultDataSourceFactory(context, \"ExoPlayerExtVp9Test\"),\n                  MatroskaExtractor.FACTORY)\n              .createMediaSource(uri);\n      player\n          .createMessage(videoRenderer)\n          .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER)\n          .setPayload(new VpxVideoSurfaceView(context))\n          .send();\n      player.prepare(mediaSource);\n      player.setPlayWhenReady(true);\n      Looper.loop();\n    }\n\n    @Override\n    public void onPlayerError(ExoPlaybackException error) {\n      playbackException = error;\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n      if (playbackState == Player.STATE_ENDED\n          || (playbackState == Player.STATE_IDLE && playbackException != null)) {\n        player.release();\n        Looper.myLooper().quit();\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.google.android.exoplayer2.ext.vp9\">\n\n  <uses-feature android:glEsVersion=\"0x00020000\"/>\n\n</manifest>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.vp9;\n\nimport static java.lang.Runtime.getRuntime;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.SystemClock;\nimport androidx.annotation.CallSuper;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport android.view.Surface;\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.PlayerMessage.Target;\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.ExoMediaCrypto;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.TimedValueQueue;\nimport com.google.android.exoplayer2.util.TraceUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.VideoFrameMetadataListener;\nimport com.google.android.exoplayer2.video.VideoRendererEventListener;\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/**\n * Decodes and renders video using the native VP9 decoder.\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 #MSG_SET_OUTPUT_BUFFER_RENDERER} to set the output buffer\n *       renderer. The message payload should be the target {@link VpxOutputBufferRenderer}, or\n *       null.\n * </ul>\n */\npublic class LibvpxVideoRenderer extends BaseRenderer {\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  /**\n   * The type of a message that can be passed to an instance of this class via {@link\n   * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link\n   * VpxOutputBufferRenderer}, or null.\n   */\n  public static final int MSG_SET_OUTPUT_BUFFER_RENDERER = C.MSG_CUSTOM_BASE;\n\n  /** The number of input buffers. */\n  private final int numInputBuffers;\n  /**\n   * The number of output buffers. The renderer may limit the minimum possible value due to\n   * requiring multiple output buffers to be dequeued at a time for it to make progress.\n   */\n  private final int numOutputBuffers;\n  /** The default input buffer size. */\n  private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.\n\n  private final boolean enableRowMultiThreadMode;\n  private final boolean disableLoopFilter;\n  private final long allowedJoiningTimeMs;\n  private final int maxDroppedFramesToNotify;\n  private final boolean playClearSamplesWithoutKeys;\n  private final EventDispatcher eventDispatcher;\n  private final FormatHolder formatHolder;\n  private final TimedValueQueue<Format> formatQueue;\n  private final DecoderInputBuffer flagsOnlyBuffer;\n  private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;\n  private final int threads;\n\n  private Format format;\n  private Format pendingFormat;\n  private Format outputFormat;\n  private VpxDecoder decoder;\n  private VpxInputBuffer inputBuffer;\n  private VpxOutputBuffer outputBuffer;\n  @Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;\n  @Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;\n\n  private @ReinitializationState int decoderReinitializationState;\n  private boolean decoderReceivedBuffers;\n\n  private boolean renderedFirstFrame;\n  private long initialPositionUs;\n  private long joiningDeadlineMs;\n  private Surface surface;\n  private VpxOutputBufferRenderer outputBufferRenderer;\n  private int outputMode;\n  private boolean waitingForKeys;\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  private VideoFrameMetadataListener frameMetadataListener;\n\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   */\n  public LibvpxVideoRenderer(long allowedJoiningTimeMs) {\n    this(allowedJoiningTimeMs, null, null, 0);\n  }\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   */\n  public LibvpxVideoRenderer(\n      long allowedJoiningTimeMs,\n      Handler eventHandler,\n      VideoRendererEventListener eventListener,\n      int maxDroppedFramesToNotify) {\n    this(\n        allowedJoiningTimeMs,\n        eventHandler,\n        eventListener,\n        maxDroppedFramesToNotify,\n        /* drmSessionManager= */ null,\n        /* playClearSamplesWithoutKeys= */ false,\n        /* disableLoopFilter= */ false);\n  }\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   * @param disableLoopFilter Disable the libvpx in-loop smoothing filter.\n   */\n  public LibvpxVideoRenderer(\n      long allowedJoiningTimeMs,\n      Handler eventHandler,\n      VideoRendererEventListener eventListener,\n      int maxDroppedFramesToNotify,\n      DrmSessionManager<ExoMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys,\n      boolean disableLoopFilter) {\n    this(\n        allowedJoiningTimeMs,\n        eventHandler,\n        eventListener,\n        maxDroppedFramesToNotify,\n        drmSessionManager,\n        playClearSamplesWithoutKeys,\n        disableLoopFilter,\n        /* enableRowMultiThreadMode= */ false,\n        getRuntime().availableProcessors(),\n        /* numInputBuffers= */ 4,\n        /* numOutputBuffers= */ 4);\n  }\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   * @param disableLoopFilter Disable the libvpx in-loop smoothing filter.\n   * @param enableRowMultiThreadMode Whether row multi threading decoding is enabled.\n   * @param threads Number of threads libvpx will use to decode.\n   * @param numInputBuffers Number of input buffers.\n   * @param numOutputBuffers Number of output buffers.\n   */\n  public LibvpxVideoRenderer(\n      long allowedJoiningTimeMs,\n      Handler eventHandler,\n      VideoRendererEventListener eventListener,\n      int maxDroppedFramesToNotify,\n      DrmSessionManager<ExoMediaCrypto> drmSessionManager,\n      boolean playClearSamplesWithoutKeys,\n      boolean disableLoopFilter,\n      boolean enableRowMultiThreadMode,\n      int threads,\n      int numInputBuffers,\n      int numOutputBuffers) {\n    super(C.TRACK_TYPE_VIDEO);\n    this.disableLoopFilter = disableLoopFilter;\n    this.allowedJoiningTimeMs = allowedJoiningTimeMs;\n    this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;\n    this.drmSessionManager = drmSessionManager;\n    this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;\n    this.enableRowMultiThreadMode = enableRowMultiThreadMode;\n    this.threads = threads;\n    this.numInputBuffers = numInputBuffers;\n    this.numOutputBuffers = numOutputBuffers;\n    joiningDeadlineMs = C.TIME_UNSET;\n    clearReportedVideoSize();\n    formatHolder = new FormatHolder();\n    formatQueue = new TimedValueQueue<>();\n    flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();\n    eventDispatcher = new EventDispatcher(eventHandler, eventListener);\n    outputMode = VpxDecoder.OUTPUT_MODE_NONE;\n    decoderReinitializationState = REINITIALIZATION_STATE_NONE;\n  }\n\n  // BaseRenderer implementation.\n\n  @Override\n  public int supportsFormat(Format format) {\n    if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) {\n      return FORMAT_UNSUPPORTED_TYPE;\n    } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {\n      return FORMAT_UNSUPPORTED_DRM;\n    }\n    return FORMAT_HANDLED | ADAPTIVE_SEAMLESS;\n  }\n\n  @Override\n  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {\n    if (outputStreamEnded) {\n      return;\n    }\n\n    if (format == null) {\n      // We don't have a format yet, so try and read one.\n      flagsOnlyBuffer.clear();\n      int result = readSource(formatHolder, flagsOnlyBuffer, true);\n      if (result == C.RESULT_FORMAT_READ) {\n        onInputFormatChanged(formatHolder.format);\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 (VpxDecoderException e) {\n        throw ExoPlaybackException.createForRenderer(e, getIndex());\n      }\n      decoderCounters.ensureUpdated();\n    }\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 (format != null && (isSourceReady() || outputBuffer != null)\n        && (renderedFirstFrame || outputMode == VpxDecoder.OUTPUT_MODE_NONE)) {\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 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    format = 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  private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {\n    DrmSession<ExoMediaCrypto> previous = sourceDrmSession;\n    sourceDrmSession = session;\n    releaseDrmSessionIfUnused(previous);\n  }\n\n  private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {\n    DrmSession<ExoMediaCrypto> previous = decoderDrmSession;\n    decoderDrmSession = session;\n    releaseDrmSessionIfUnused(previous);\n  }\n\n  private void releaseDrmSessionIfUnused(@Nullable DrmSession<ExoMediaCrypto> session) {\n    if (session != null && session != decoderDrmSession && session != sourceDrmSession) {\n      drmSessionManager.releaseSession(session);\n    }\n  }\n\n  /**\n   * Called when a new format is read from the upstream source.\n   *\n   * @param newFormat The new format.\n   * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder.\n   */\n  @CallSuper\n  protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {\n    Format oldFormat = format;\n    format = newFormat;\n    pendingFormat = newFormat;\n\n    boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null\n        : oldFormat.drmInitData);\n    if (drmInitDataChanged) {\n      if (format.drmInitData != null) {\n        if (drmSessionManager == null) {\n          throw ExoPlaybackException.createForRenderer(\n              new IllegalStateException(\"Media requires a DrmSessionManager\"), getIndex());\n        }\n        DrmSession<ExoMediaCrypto> session =\n            drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);\n        if (session == decoderDrmSession || session == sourceDrmSession) {\n          // We already had this session. The manager must be reference counting, so release it once\n          // to get the count attributed to this renderer back down to 1.\n          drmSessionManager.releaseSession(session);\n        }\n        setSourceDrmSession(session);\n      } else {\n        setSourceDrmSession(null);\n      }\n    }\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(format);\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(VpxInputBuffer 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 android.os.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 android.os.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(VpxOutputBuffer 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(VpxOutputBuffer outputBuffer) {\n    updateDroppedBufferCounters(1);\n    outputBuffer.release();\n  }\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 VpxOutputBuffer#release()} either immediately or in the future.\n   *\n   * @param outputBuffer The buffer to render.\n   */\n  protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException {\n    int bufferMode = outputBuffer.mode;\n    boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null;\n    boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;\n    lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;\n    if (!renderYuv && !renderSurface) {\n      dropOutputBuffer(outputBuffer);\n    } else {\n      maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);\n      if (renderYuv) {\n        outputBufferRenderer.setOutputBuffer(outputBuffer);\n        // The renderer will release the buffer.\n      } else { // renderSurface\n        decoder.renderToSurface(outputBuffer, surface);\n        outputBuffer.release();\n      }\n      consecutiveDroppedFrameCount = 0;\n      decoderCounters.renderedOutputBufferCount++;\n      maybeNotifyRenderedFirstFrame();\n    }\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  // PlayerMessage.Target implementation.\n\n  @Override\n  public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {\n    if (messageType == C.MSG_SET_SURFACE) {\n      setOutput((Surface) message, null);\n    } else if (messageType == MSG_SET_OUTPUT_BUFFER_RENDERER) {\n      setOutput(null, (VpxOutputBufferRenderer) message);\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  // Internal methods.\n\n  private void setOutput(\n      @Nullable Surface surface, @Nullable VpxOutputBufferRenderer outputBufferRenderer) {\n    // At most one output may be non-null. Both may be null if the output is being cleared.\n    Assertions.checkState(surface == null || outputBufferRenderer == null);\n    if (this.surface != surface || this.outputBufferRenderer != outputBufferRenderer) {\n      // The output has changed.\n      this.surface = surface;\n      this.outputBufferRenderer = outputBufferRenderer;\n      if (surface != null) {\n        outputMode = VpxDecoder.OUTPUT_MODE_SURFACE_YUV;\n      } else {\n        outputMode =\n            outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE;\n      }\n      if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) {\n        if (decoder != null) {\n          decoder.setOutputMode(outputMode);\n        }\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      } 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        clearReportedVideoSize();\n        clearRenderedFirstFrame();\n      }\n    } else if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) {\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\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      TraceUtil.beginSection(\"createVpxDecoder\");\n      int initialInputBufferSize =\n          format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;\n      decoder =\n          new VpxDecoder(\n              numInputBuffers,\n              numOutputBuffers,\n              initialInputBufferSize,\n              mediaCrypto,\n              disableLoopFilter,\n              enableRowMultiThreadMode,\n              threads);\n      decoder.setOutputMode(outputMode);\n      TraceUtil.endSection();\n      long decoderInitializedTimestamp = SystemClock.elapsedRealtime();\n      onDecoderInitialized(\n          decoder.getName(),\n          decoderInitializedTimestamp,\n          decoderInitializedTimestamp - decoderInitializingTimestamp);\n      decoderCounters.decoderInitCount++;\n    } catch (VpxDecoderException e) {\n      throw ExoPlaybackException.createForRenderer(e, getIndex());\n    }\n  }\n\n  private boolean feedInputBuffer() throws VpxDecoderException, 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    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.format);\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 (pendingFormat != null) {\n      formatQueue.add(inputBuffer.timeUs, pendingFormat);\n      pendingFormat = null;\n    }\n    inputBuffer.flip();\n    inputBuffer.colorInfo = format.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 android.os.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, VpxDecoderException {\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 android.os.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, VpxDecoderException {\n    if (initialPositionUs == C.TIME_UNSET) {\n      initialPositionUs = positionUs;\n    }\n\n    long earlyUs = outputBuffer.timeUs - positionUs;\n    if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {\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      if (frameMetadataListener != null) {\n        frameMetadataListener.onVideoFrameAboutToBeRendered(\n            presentationTimeUs, System.nanoTime(), outputFormat);\n      }\n      renderOutputBuffer(outputBuffer);\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      if (frameMetadataListener != null) {\n        frameMetadataListener.onVideoFrameAboutToBeRendered(\n            presentationTimeUs, System.nanoTime(), outputFormat);\n      }\n      renderOutputBuffer(outputBuffer);\n      return true;\n    }\n\n    return false;\n  }\n\n  private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {\n    if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {\n      return false;\n    }\n    @DrmSession.State int drmSessionState = decoderDrmSession.getState();\n    if (drmSessionState == DrmSession.STATE_ERROR) {\n      throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());\n    }\n    return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;\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  }\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(width, height, 0, 1);\n    }\n  }\n\n  private void maybeRenotifyVideoSizeChanged() {\n    if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) {\n      eventDispatcher.videoSizeChanged(reportedWidth, reportedHeight, 0, 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}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.vp9;\n\nimport androidx.annotation.Nullable;\nimport android.view.Surface;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.decoder.CryptoInfo;\nimport com.google.android.exoplayer2.decoder.SimpleDecoder;\nimport com.google.android.exoplayer2.drm.DecryptionException;\nimport com.google.android.exoplayer2.drm.ExoMediaCrypto;\nimport java.nio.ByteBuffer;\n\n/**\n * Vpx decoder.\n */\n/* package */ final class VpxDecoder extends\n    SimpleDecoder<VpxInputBuffer, VpxOutputBuffer, VpxDecoderException> {\n\n  public static final int OUTPUT_MODE_NONE = -1;\n  public static final int OUTPUT_MODE_YUV = 0;\n  public static final int OUTPUT_MODE_SURFACE_YUV = 1;\n\n  private static final int NO_ERROR = 0;\n  private static final int DECODE_ERROR = 1;\n  private static final int DRM_ERROR = 2;\n\n  private final ExoMediaCrypto exoMediaCrypto;\n  private final long vpxDecContext;\n\n  private volatile int outputMode;\n\n  /**\n   * Creates a VP9 decoder.\n   *\n   * @param numInputBuffers The number of input buffers.\n   * @param numOutputBuffers The number of output buffers.\n   * @param initialInputBufferSize The initial size of each input buffer.\n   * @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted\n   *     content. Maybe null and can be ignored if decoder does not handle encrypted content.\n   * @param disableLoopFilter Disable the libvpx in-loop smoothing filter.\n   * @param enableRowMultiThreadMode Whether row multi threading decoding is enabled.\n   * @param threads Number of threads libvpx will use to decode.\n   * @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.\n   */\n  public VpxDecoder(\n      int numInputBuffers,\n      int numOutputBuffers,\n      int initialInputBufferSize,\n      ExoMediaCrypto exoMediaCrypto,\n      boolean disableLoopFilter,\n      boolean enableRowMultiThreadMode,\n      int threads)\n      throws VpxDecoderException {\n    super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);\n    if (!VpxLibrary.isAvailable()) {\n      throw new VpxDecoderException(\"Failed to load decoder native libraries.\");\n    }\n    this.exoMediaCrypto = exoMediaCrypto;\n    if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {\n      throw new VpxDecoderException(\"Vpx decoder does not support secure decode.\");\n    }\n    vpxDecContext = vpxInit(disableLoopFilter, enableRowMultiThreadMode, threads);\n    if (vpxDecContext == 0) {\n      throw new VpxDecoderException(\"Failed to initialize decoder\");\n    }\n    setInitialInputBufferSize(initialInputBufferSize);\n  }\n\n  @Override\n  public String getName() {\n    return \"libvpx\" + VpxLibrary.getVersion();\n  }\n\n  /**\n   * Sets the output mode for frames rendered by the decoder.\n   *\n   * @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE} and {@link\n   *     #OUTPUT_MODE_YUV}.\n   */\n  public void setOutputMode(int outputMode) {\n    this.outputMode = outputMode;\n  }\n\n  @Override\n  protected VpxInputBuffer createInputBuffer() {\n    return new VpxInputBuffer();\n  }\n\n  @Override\n  protected VpxOutputBuffer createOutputBuffer() {\n    return new VpxOutputBuffer(this);\n  }\n\n  @Override\n  protected void releaseOutputBuffer(VpxOutputBuffer buffer) {\n    // Decode only frames do not acquire a reference on the internal decoder buffer and thus do not\n    // require a call to vpxReleaseFrame.\n    if (outputMode == OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {\n      vpxReleaseFrame(vpxDecContext, buffer);\n    }\n    super.releaseOutputBuffer(buffer);\n  }\n\n  @Override\n  protected VpxDecoderException createUnexpectedDecodeException(Throwable error) {\n    return new VpxDecoderException(\"Unexpected decode error\", error);\n  }\n\n  @Override\n  @Nullable\n  protected VpxDecoderException decode(\n      VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) {\n    ByteBuffer inputData = inputBuffer.data;\n    int inputSize = inputData.limit();\n    CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;\n    final long result = inputBuffer.isEncrypted()\n        ? vpxSecureDecode(vpxDecContext, inputData, inputSize, exoMediaCrypto,\n        cryptoInfo.mode, cryptoInfo.key, cryptoInfo.iv, cryptoInfo.numSubSamples,\n        cryptoInfo.numBytesOfClearData, cryptoInfo.numBytesOfEncryptedData)\n        : vpxDecode(vpxDecContext, inputData, inputSize);\n    if (result != NO_ERROR) {\n      if (result == DRM_ERROR) {\n        String message = \"Drm error: \" + vpxGetErrorMessage(vpxDecContext);\n        DecryptionException cause = new DecryptionException(\n            vpxGetErrorCode(vpxDecContext), message);\n        return new VpxDecoderException(message, cause);\n      } else {\n        return new VpxDecoderException(\"Decode error: \" + vpxGetErrorMessage(vpxDecContext));\n      }\n    }\n\n    if (!inputBuffer.isDecodeOnly()) {\n      outputBuffer.init(inputBuffer.timeUs, outputMode);\n      int getFrameResult = vpxGetFrame(vpxDecContext, outputBuffer);\n      if (getFrameResult == 1) {\n        outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);\n      } else if (getFrameResult == -1) {\n        return new VpxDecoderException(\"Buffer initialization failed.\");\n      }\n      outputBuffer.colorInfo = inputBuffer.colorInfo;\n    }\n    return null;\n  }\n\n  @Override\n  public void release() {\n    super.release();\n    vpxClose(vpxDecContext);\n  }\n\n  /** Renders the outputBuffer to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. */\n  public void renderToSurface(VpxOutputBuffer outputBuffer, Surface surface)\n      throws VpxDecoderException {\n    int getFrameResult = vpxRenderFrame(vpxDecContext, surface, outputBuffer);\n    if (getFrameResult == -1) {\n      throw new VpxDecoderException(\"Buffer render failed.\");\n    }\n  }\n\n  private native long vpxInit(\n      boolean disableLoopFilter, boolean enableRowMultiThreadMode, int threads);\n\n  private native long vpxClose(long context);\n  private native long vpxDecode(long context, ByteBuffer encoded, int length);\n  private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,\n      ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,\n      int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);\n  private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);\n\n  /**\n   * Renders the frame to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called\n   * if {@link #vpxInit} was called with {@code enableBufferManager = true}.\n   */\n  private native int vpxRenderFrame(long context, Surface surface, VpxOutputBuffer outputBuffer);\n\n  /**\n   * Releases the frame. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called if {@link\n   * #vpxInit} was called with {@code enableBufferManager = true}.\n   */\n  private native int vpxReleaseFrame(long context, VpxOutputBuffer outputBuffer);\n\n  private native int vpxGetErrorCode(long context);\n  private native String vpxGetErrorMessage(long context);\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.vp9;\n\n/** Thrown when a libvpx decoder error occurs. */\npublic final class VpxDecoderException extends Exception {\n\n  /* package */ VpxDecoderException(String message) {\n    super(message);\n  }\n\n  /* package */ VpxDecoderException(String message, Throwable cause) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.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.ext.vp9;\n\nimport com.google.android.exoplayer2.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.video.ColorInfo;\n\n/**\n * Input buffer to a {@link VpxDecoder}.\n */\n/* package */ final class VpxInputBuffer extends DecoderInputBuffer {\n\n  public ColorInfo colorInfo;\n\n  public VpxInputBuffer() {\n    super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.vp9;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.util.LibraryLoader;\n\n/**\n * Configures and queries the underlying native library.\n */\npublic final class VpxLibrary {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.vpx\");\n  }\n\n  private static final LibraryLoader LOADER = new LibraryLoader(\"vpx\", \"vpxV2JNI\");\n\n  private VpxLibrary() {}\n\n  /**\n   * Override the names of the Vpx native libraries. If an application wishes to call this method,\n   * it must do so before calling any other method defined by this class, and before instantiating a\n   * {@link LibvpxVideoRenderer} instance.\n   *\n   * @param libraries The names of the Vpx native libraries.\n   */\n  public static void setLibraries(String... libraries) {\n    LOADER.setLibraries(libraries);\n  }\n\n  /**\n   * Returns whether the underlying library is available, loading it if necessary.\n   */\n  public static boolean isAvailable() {\n    return LOADER.isAvailable();\n  }\n\n  /** Returns the version of the underlying library if available, or null otherwise. */\n  @Nullable\n  public static String getVersion() {\n    return isAvailable() ? vpxGetVersion() : null;\n  }\n\n  /**\n   * Returns the configuration string with which the underlying library was built if available, or\n   * null otherwise.\n   */\n  @Nullable\n  public static String getBuildConfig() {\n    return isAvailable() ? vpxGetBuildConfig() : null;\n  }\n\n  /**\n   * Returns true if the underlying libvpx library supports high bit depth.\n   */\n  public static boolean isHighBitDepthSupported() {\n    String config = getBuildConfig();\n    int indexHbd = config != null\n        ? config.indexOf(\"--enable-vp9-highbitdepth\") : -1;\n    return indexHbd >= 0;\n  }\n\n  private static native String vpxGetVersion();\n  private static native String vpxGetBuildConfig();\n  public static native boolean vpxIsSecureDecodeSupported();\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.vp9;\n\nimport com.google.android.exoplayer2.decoder.OutputBuffer;\nimport com.google.android.exoplayer2.video.ColorInfo;\nimport java.nio.ByteBuffer;\n\n/** Output buffer containing video frame data, populated by {@link VpxDecoder}. */\npublic final class VpxOutputBuffer extends OutputBuffer {\n\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\n  private final VpxDecoder owner;\n  /** Decoder private data. */\n  public int decoderPrivate;\n\n  public int mode;\n  /**\n   * RGB buffer for RGB mode.\n   */\n  public ByteBuffer data;\n  public int width;\n  public int height;\n  public ColorInfo colorInfo;\n\n  /**\n   * YUV planes for YUV mode.\n   */\n  public ByteBuffer[] yuvPlanes;\n  public int[] yuvStrides;\n  public int colorspace;\n\n  public VpxOutputBuffer(VpxDecoder 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 VpxDecoder#OUTPUT_MODE_NONE}, {@link\n   *     VpxDecoder#OUTPUT_MODE_YUV} and {@link VpxDecoder#OUTPUT_MODE_SURFACE_YUV}.\n   */\n  public void init(long timeUs, int mode) {\n    this.timeUs = timeUs;\n    this.mode = mode;\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    initData(minimumYuvSize);\n\n    if (yuvPlanes == null) {\n      yuvPlanes = new ByteBuffer[3];\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  private void initData(int size) {\n    if (data == null || data.capacity() < size) {\n      data = ByteBuffer.allocateDirect(size);\n    } else {\n      data.position(0);\n      data.limit(size);\n    }\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 boolean isSafeToMultiply(int a, int b) {\n    return a >= 0 && b >= 0 && !(b > 0 && a >= Integer.MAX_VALUE / b);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.vp9;\n\n/**\n * Renders the {@link VpxOutputBuffer}.\n */\npublic interface VpxOutputBufferRenderer {\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(VpxOutputBuffer outputBuffer);\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxRenderer.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.vp9;\n\nimport android.opengl.GLES20;\nimport android.opengl.GLSurfaceView;\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 libvpx after\n * decoding. It does the YUV to RGB color conversion in the Fragment Shader.\n */\n/* package */ class VpxRenderer implements GLSurfaceView.Renderer {\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;\\n\"\n      + \"attribute vec4 in_pos;\\n\"\n      + \"attribute vec2 in_tc;\\n\"\n      + \"void main() {\\n\"\n      + \"  gl_Position = in_pos;\\n\"\n      + \"  interp_tc = in_tc;\\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;\\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).r - 0.0625;\\n\"\n      + \"  yuv.y = texture2D(u_tex, interp_tc).r - 0.5;\\n\"\n      + \"  yuv.z = texture2D(v_tex, interp_tc).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 int[] yuvTextures = new int[3];\n  private final AtomicReference<VpxOutputBuffer> pendingOutputBufferReference;\n\n  // Kept in a field rather than a local variable so that it doesn't get garbage collected before\n  // glDrawArrays uses it.\n  @SuppressWarnings(\"FieldCanBeLocal\")\n  private FloatBuffer textureCoords;\n  private int program;\n  private int texLocation;\n  private int colorMatrixLocation;\n  private int previousWidth;\n  private int previousStride;\n\n  private VpxOutputBuffer renderedOutputBuffer; // Accessed only from the GL thread.\n\n  public VpxRenderer() {\n    previousWidth = -1;\n    previousStride = -1;\n    pendingOutputBufferReference = new AtomicReference<>();\n  }\n\n  /**\n   * Set a frame to be rendered. This should be followed by a call to\n   * VpxVideoSurfaceView.requestRender() to actually render the frame.\n   *\n   * @param outputBuffer OutputBuffer containing the YUV Frame to be rendered\n   */\n  public void setFrame(VpxOutputBuffer outputBuffer) {\n    VpxOutputBuffer oldPendingOutputBuffer = 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  }\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(\n        posLocation, 2, GLES20.GL_FLOAT, false, 0, TEXTURE_VERTICES);\n    texLocation = GLES20.glGetAttribLocation(program, \"in_tc\");\n    GLES20.glEnableVertexAttribArray(texLocation);\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    VpxOutputBuffer 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    VpxOutputBuffer outputBuffer = renderedOutputBuffer;\n    // Set color matrix. Assume BT709 if the color space is unknown.\n    float[] colorConversion = kColorConversion709;\n    switch (outputBuffer.colorspace) {\n      case VpxOutputBuffer.COLORSPACE_BT601:\n        colorConversion = kColorConversion601;\n        break;\n      case VpxOutputBuffer.COLORSPACE_BT2020:\n        colorConversion = kColorConversion2020;\n        break;\n      case VpxOutputBuffer.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(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,\n          outputBuffer.yuvStrides[i], h, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE,\n          outputBuffer.yuvPlanes[i]);\n    }\n    // Set cropping of stride if either width or stride has changed.\n    if (previousWidth != outputBuffer.width || previousStride != outputBuffer.yuvStrides[0]) {\n      float crop = (float) outputBuffer.width / outputBuffer.yuvStrides[0];\n      // This buffer is consumed during each call to glDrawArrays. It needs to be a member variable\n      // rather than a local variable to ensure that it doesn't get garbage collected.\n      textureCoords =\n          GlUtil.createBuffer(new float[] {0.0f, 0.0f, 0.0f, 1.0f, crop, 0.0f, crop, 1.0f});\n      GLES20.glVertexAttribPointer(\n          texLocation, 2, GLES20.GL_FLOAT, false, 0, textureCoords);\n      previousWidth = outputBuffer.width;\n      previousStride = outputBuffer.yuvStrides[0];\n    }\n    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);\n    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);\n    GlUtil.checkGlError();\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,\n          GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);\n      GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,\n          GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);\n      GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,\n          GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);\n      GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,\n          GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);\n    }\n    GlUtil.checkGlError();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ext.vp9;\n\nimport android.content.Context;\nimport android.opengl.GLSurfaceView;\nimport android.util.AttributeSet;\n\n/**\n * A GLSurfaceView extension that scales itself to the given aspect ratio.\n */\npublic class VpxVideoSurfaceView extends GLSurfaceView implements VpxOutputBufferRenderer {\n\n  private final VpxRenderer renderer;\n\n  public VpxVideoSurfaceView(Context context) {\n    this(context, null);\n  }\n\n  public VpxVideoSurfaceView(Context context, AttributeSet attrs) {\n    super(context, attrs);\n    renderer = new VpxRenderer();\n    setPreserveEGLContextOnPause(true);\n    setEGLContextClientVersion(2);\n    setRenderer(renderer);\n    setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);\n  }\n\n  @Override\n  public void setOutputBuffer(VpxOutputBuffer outputBuffer) {\n    renderer.setFrame(outputBuffer);\n    requestRender();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/jni/Android.mk",
    "content": "#\n# Copyright (C) 2016 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS 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\nWORKING_DIR := $(call my-dir)\ninclude $(CLEAR_VARS)\nLIBVPX_ROOT := $(WORKING_DIR)/libvpx\n\n# build libvpx.so\nLOCAL_PATH := $(WORKING_DIR)\ninclude libvpx.mk\n\n# build libvpxV2JNI.so\ninclude $(CLEAR_VARS)\nLOCAL_PATH := $(WORKING_DIR)\nLOCAL_MODULE := libvpxV2JNI\nLOCAL_ARM_MODE := arm\nLOCAL_CPP_EXTENSION := .cc\nLOCAL_SRC_FILES := vpx_jni.cc\nLOCAL_LDLIBS := -llog -lz -lm -landroid\nLOCAL_SHARED_LIBRARIES := libvpx\nLOCAL_STATIC_LIBRARIES := cpufeatures\ninclude $(BUILD_SHARED_LIBRARY)\n\n$(call import-module,android/cpufeatures)\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/jni/Application.mk",
    "content": "#\n# Copyright (C) 2016 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS 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\nAPP_OPTIM := release\nAPP_STL := c++_static\nAPP_CPPFLAGS := -frtti\nAPP_PLATFORM := android-16\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh",
    "content": "#!/bin/bash\n#\n# Copyright (C) 2016 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS 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# a bash script that generates the necessary config files for libvpx android ndk\n# builds.\n\nset -e\n\nif [ $# -ne 0 ]; then\n  echo \"Usage: ${0}\"\n  exit\nfi\n\n# configuration parameters common to all architectures\ncommon_params=\"--disable-examples --disable-docs --enable-realtime-only\"\ncommon_params+=\" --disable-vp8 --disable-vp9-encoder --disable-webm-io\"\ncommon_params+=\" --disable-libyuv --disable-runtime-cpu-detect\"\ncommon_params+=\" --enable-external-build\"\n\n# configuration parameters for various architectures\narch[0]=\"armeabi-v7a\"\nconfig[0]=\"--target=armv7-android-gcc --enable-neon --enable-neon-asm\"\n\narch[1]=\"x86\"\nconfig[1]=\"--force-target=x86-android-gcc --disable-sse2\"\nconfig[1]+=\" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx\"\nconfig[1]+=\" --disable-avx2 --enable-pic\"\n\narch[2]=\"arm64-v8a\"\nconfig[2]=\"--force-target=armv8-android-gcc --enable-neon\"\n\narch[3]=\"x86_64\"\nconfig[3]=\"--force-target=x86_64-android-gcc --disable-sse2\"\nconfig[3]+=\" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx\"\nconfig[3]+=\" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm\"\n\nlimit=$((${#arch[@]} - 1))\n\n# list of files allowed after running configure in each arch directory.\n# everything else will be removed.\nallowed_files=\"libvpx_srcs.txt vpx_config.c vpx_config.h vpx_scale_rtcd.h\"\nallowed_files+=\" vp8_rtcd.h vp9_rtcd.h vpx_version.h vpx_config.asm\"\nallowed_files+=\" vpx_dsp_rtcd.h libvpx.ver\"\n\nremove_trailing_whitespace() {\n  perl -pi -e 's/\\s\\+$//' \"$@\"\n}\n\nconvert_asm() {\n  for i in $(seq 0 ${limit}); do\n    while read file; do\n      case \"${file}\" in\n        *.asm.[sS])\n          # Some files may already have been processed (there are duplicated\n          # .asm.s files for vp8 in the armeabi/armeabi-v7a configurations).\n          file=\"libvpx/${file}\"\n          if [[ ! -e \"${file}\" ]]; then\n            asm_file=\"${file%.[sS]}\"\n            cat \"${asm_file}\" | libvpx/build/make/ads2gas.pl > \"${file}\"\n            remove_trailing_whitespace \"${file}\"\n            rm \"${asm_file}\"\n          fi\n          ;;\n      esac\n    done < libvpx_android_configs/${arch[${i}]}/libvpx_srcs.txt\n  done\n}\n\nextglob_status=\"$(shopt extglob | cut -f2)\"\nshopt -s extglob\nfor i in $(seq 0 ${limit}); do\n  mkdir -p \"libvpx_android_configs/${arch[${i}]}\"\n  pushd \"libvpx_android_configs/${arch[${i}]}\"\n\n  # configure and make\n  echo \"build_android_configs: \"\n  echo \"configure ${config[${i}]} ${common_params}\"\n  ../../libvpx/configure ${config[${i}]} ${common_params}\n  rm -f libvpx_srcs.txt\n  for f in ${allowed_files}; do\n    # the build system supports multiple different configurations. avoid\n    # failing out when, for example, vp8_rtcd.h is not part of a configuration\n    make \"${f}\" || true\n  done\n\n  # remove files that aren't needed\n  rm -rf !(${allowed_files// /|})\n  remove_trailing_whitespace *\n\n  popd\ndone\n\n# restore extglob status as it was before\nif [[ \"${extglob_status}\" == \"off\" ]]; then\n  shopt -u extglob\nfi\n\nconvert_asm\n\necho \"Generated android config files.\"\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/jni/libvpx.mk",
    "content": "#\n# Copyright (C) 2016 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS 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\nLOCAL_PATH := $(call my-dir)\ninclude $(CLEAR_VARS)\nCONFIG_DIR := $(LOCAL_PATH)/libvpx_android_configs/$(TARGET_ARCH_ABI)\nlibvpx_source_dir := $(LOCAL_PATH)/libvpx\n\nLOCAL_MODULE := libvpx\nLOCAL_MODULE_CLASS := STATIC_LIBRARIES\nLOCAL_CFLAGS := -DHAVE_CONFIG_H=vpx_config.h\nLOCAL_ARM_MODE := arm\nLOCAL_CFLAGS += -O3\n\n# config specific include should go first to pick up the config specific rtcd.\nLOCAL_C_INCLUDES := $(CONFIG_DIR) $(libvpx_source_dir)\n\n# generate source file list\nlibvpx_codec_srcs := $(sort $(shell cat $(CONFIG_DIR)/libvpx_srcs.txt))\nLOCAL_SRC_FILES := libvpx_android_configs/$(TARGET_ARCH_ABI)/vpx_config.c\nLOCAL_SRC_FILES += $(addprefix libvpx/, $(filter-out vpx_config.c, \\\n                     $(filter %.c, $(libvpx_codec_srcs))))\n\n# include assembly files if they exist\n# \"%.asm.[sS]\" covers neon assembly and \"%.asm\" covers x86 assembly\nLOCAL_SRC_FILES += $(addprefix libvpx/, \\\n                     $(filter %.asm.s %.asm.S %.asm, $(libvpx_codec_srcs)))\n\nifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),)\n# append .neon to *_neon.c and *.[sS]\nLOCAL_SRC_FILES := $(subst _neon.c,_neon.c.neon,$(LOCAL_SRC_FILES))\nLOCAL_SRC_FILES := $(subst .s,.s.neon,$(LOCAL_SRC_FILES))\nLOCAL_SRC_FILES := $(subst .S,.S.neon,$(LOCAL_SRC_FILES))\nendif\n\nLOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libvpx \\\n                           $(LOCAL_PATH)/libvpx/vpx\n\nLOCAL_LDFLAGS := -Wl,--version-script=$(CONFIG_DIR)/libvpx.ver\ninclude $(BUILD_SHARED_LIBRARY)\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/main/jni/vpx_jni.cc",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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#include <cpu-features.h>\n#ifdef __ARM_NEON__\n#include <arm_neon.h>\n#endif\n#include <jni.h>\n\n#include <android/log.h>\n#include <android/native_window.h>\n#include <android/native_window_jni.h>\n#include <pthread.h>\n#include <algorithm>\n#include <cstdio>\n#include <cstdlib>\n#include <cstring>\n#include <new>\n\n#define VPX_CODEC_DISABLE_COMPAT 1\n#include \"vpx/vpx_decoder.h\"\n#include \"vpx/vp8dx.h\"\n\n#define LOG_TAG \"vpx_jni\"\n#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, \\\n                                             __VA_ARGS__))\n\n#define DECODER_FUNC(RETURN_TYPE, NAME, ...) \\\n  extern \"C\" { \\\n  JNIEXPORT RETURN_TYPE \\\n    Java_com_google_android_exoplayer2_ext_vp9_VpxDecoder_ ## NAME \\\n      (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\\\n  } \\\n  JNIEXPORT RETURN_TYPE \\\n    Java_com_google_android_exoplayer2_ext_vp9_VpxDecoder_ ## NAME \\\n      (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\\\n\n#define LIBRARY_FUNC(RETURN_TYPE, NAME, ...) \\\n  extern \"C\" { \\\n  JNIEXPORT RETURN_TYPE \\\n    Java_com_google_android_exoplayer2_ext_vp9_VpxLibrary_ ## NAME \\\n      (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\\\n  } \\\n  JNIEXPORT RETURN_TYPE \\\n    Java_com_google_android_exoplayer2_ext_vp9_VpxLibrary_ ## NAME \\\n      (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\\\n\n// JNI references for VpxOutputBuffer class.\nstatic jmethodID initForYuvFrame;\nstatic jmethodID initForPrivateFrame;\nstatic jfieldID dataField;\nstatic jfieldID outputModeField;\nstatic jfieldID decoderPrivateField;\n\n// android.graphics.ImageFormat.YV12.\nstatic const int kHalPixelFormatYV12 = 0x32315659;\nstatic const int kDecoderPrivateBase = 0x100;\nstatic int errorCode;\n\njint JNI_OnLoad(JavaVM* vm, void* reserved) {\n  JNIEnv* env;\n  if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {\n    return -1;\n  }\n  return JNI_VERSION_1_6;\n}\n\n#ifdef __ARM_NEON__\nstatic int convert_16_to_8_neon(const vpx_image_t* const img, jbyte* const data,\n                                const int32_t uvHeight, const int32_t yLength,\n                                const int32_t uvLength) {\n  if (!(android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON)) return 0;\n  uint32x2_t lcg_val = vdup_n_u32(random());\n  lcg_val = vset_lane_u32(random(), lcg_val, 1);\n  // LCG values recommended in good ol' \"Numerical Recipes\"\n  const uint32x2_t LCG_MULT = vdup_n_u32(1664525);\n  const uint32x2_t LCG_INCR = vdup_n_u32(1013904223);\n\n  const uint16_t* srcBase =\n      reinterpret_cast<uint16_t*>(img->planes[VPX_PLANE_Y]);\n  uint8_t* dstBase = reinterpret_cast<uint8_t*>(data);\n  // In units of uint16_t, so /2 from raw stride\n  const int srcStride = img->stride[VPX_PLANE_Y] / 2;\n  const int dstStride = img->stride[VPX_PLANE_Y];\n\n  for (int y = 0; y < img->d_h; y++) {\n    const uint16_t* src = srcBase;\n    uint8_t* dst = dstBase;\n\n    // Each read consumes 4 2-byte samples, but to reduce branches and\n    // random steps we unroll to four rounds, so each loop consumes 16\n    // samples.\n    const int imax = img->d_w & ~15;\n    int i;\n    for (i = 0; i < imax; i += 16) {\n      // Run a round of the RNG.\n      lcg_val = vmla_u32(LCG_INCR, lcg_val, LCG_MULT);\n\n      // The lower two bits of this LCG parameterization are garbage,\n      // leaving streaks on the image. We access the upper bits of each\n      // 16-bit lane by shifting. (We use this both as an 8- and 16-bit\n      // vector, so the choice of which one to keep it as is arbitrary.)\n      uint8x8_t randvec =\n          vreinterpret_u8_u16(vshr_n_u16(vreinterpret_u16_u32(lcg_val), 8));\n\n      // We retrieve the values and shift them so that the bits we'll\n      // shift out (after biasing) are in the upper 8 bits of each 16-bit\n      // lane.\n      uint16x4_t values = vshl_n_u16(vld1_u16(src), 6);\n      src += 4;\n\n      // We add the bias bits in the lower 8 to the shifted values to get\n      // the final values in the upper 8 bits.\n      uint16x4_t added1 = vqadd_u16(values, vreinterpret_u16_u8(randvec));\n\n      // Shifting the randvec bits left by 2 bits, as an 8-bit vector,\n      // should leave us with enough bias to get the needed rounding\n      // operation.\n      randvec = vshl_n_u8(randvec, 2);\n\n      // Retrieve and sum the next 4 pixels.\n      values = vshl_n_u16(vld1_u16(src), 6);\n      src += 4;\n      uint16x4_t added2 = vqadd_u16(values, vreinterpret_u16_u8(randvec));\n\n      // Reinterpret the two added vectors as 8x8, zip them together, and\n      // discard the lower portions.\n      uint8x8_t zipped =\n          vuzp_u8(vreinterpret_u8_u16(added1), vreinterpret_u8_u16(added2))\n              .val[1];\n      vst1_u8(dst, zipped);\n      dst += 8;\n\n      // Run it again with the next two rounds using the remaining\n      // entropy in randvec.\n      randvec = vshl_n_u8(randvec, 2);\n      values = vshl_n_u16(vld1_u16(src), 6);\n      src += 4;\n      added1 = vqadd_u16(values, vreinterpret_u16_u8(randvec));\n      randvec = vshl_n_u8(randvec, 2);\n      values = vshl_n_u16(vld1_u16(src), 6);\n      src += 4;\n      added2 = vqadd_u16(values, vreinterpret_u16_u8(randvec));\n      zipped = vuzp_u8(vreinterpret_u8_u16(added1), vreinterpret_u8_u16(added2))\n                   .val[1];\n      vst1_u8(dst, zipped);\n      dst += 8;\n    }\n\n    uint32_t randval = 0;\n    // For the remaining pixels in each row - usually none, as most\n    // standard sizes are divisible by 32 - convert them \"by hand\".\n    while (i < img->d_w) {\n      if (!randval) randval = random();\n      dstBase[i] = (srcBase[i] + (randval & 3)) >> 2;\n      i++;\n      randval >>= 2;\n    }\n\n    srcBase += srcStride;\n    dstBase += dstStride;\n  }\n\n  const uint16_t* srcUBase =\n      reinterpret_cast<uint16_t*>(img->planes[VPX_PLANE_U]);\n  const uint16_t* srcVBase =\n      reinterpret_cast<uint16_t*>(img->planes[VPX_PLANE_V]);\n  const int32_t uvWidth = (img->d_w + 1) / 2;\n  uint8_t* dstUBase = reinterpret_cast<uint8_t*>(data + yLength);\n  uint8_t* dstVBase = reinterpret_cast<uint8_t*>(data + yLength + uvLength);\n  const int srcUVStride = img->stride[VPX_PLANE_V] / 2;\n  const int dstUVStride = img->stride[VPX_PLANE_V];\n\n  for (int y = 0; y < uvHeight; y++) {\n    const uint16_t* srcU = srcUBase;\n    const uint16_t* srcV = srcVBase;\n    uint8_t* dstU = dstUBase;\n    uint8_t* dstV = dstVBase;\n\n    // As before, each i++ consumes 4 samples (8 bytes). For simplicity we\n    // don't unroll these loops more than we have to, which is 8 samples.\n    const int imax = uvWidth & ~7;\n    int i;\n    for (i = 0; i < imax; i += 8) {\n      lcg_val = vmla_u32(LCG_INCR, lcg_val, LCG_MULT);\n      uint8x8_t randvec =\n          vreinterpret_u8_u16(vshr_n_u16(vreinterpret_u16_u32(lcg_val), 8));\n      uint16x4_t uVal1 = vqadd_u16(vshl_n_u16(vld1_u16(srcU), 6),\n                                   vreinterpret_u16_u8(randvec));\n      srcU += 4;\n      randvec = vshl_n_u8(randvec, 2);\n      uint16x4_t vVal1 = vqadd_u16(vshl_n_u16(vld1_u16(srcV), 6),\n                                   vreinterpret_u16_u8(randvec));\n      srcV += 4;\n      randvec = vshl_n_u8(randvec, 2);\n      uint16x4_t uVal2 = vqadd_u16(vshl_n_u16(vld1_u16(srcU), 6),\n                                   vreinterpret_u16_u8(randvec));\n      srcU += 4;\n      randvec = vshl_n_u8(randvec, 2);\n      uint16x4_t vVal2 = vqadd_u16(vshl_n_u16(vld1_u16(srcV), 6),\n                                   vreinterpret_u16_u8(randvec));\n      srcV += 4;\n      vst1_u8(dstU,\n              vuzp_u8(vreinterpret_u8_u16(uVal1), vreinterpret_u8_u16(uVal2))\n                  .val[1]);\n      dstU += 8;\n      vst1_u8(dstV,\n              vuzp_u8(vreinterpret_u8_u16(vVal1), vreinterpret_u8_u16(vVal2))\n                  .val[1]);\n      dstV += 8;\n    }\n\n    uint32_t randval = 0;\n    while (i < uvWidth) {\n      if (!randval) randval = random();\n      dstUBase[i] = (srcUBase[i] + (randval & 3)) >> 2;\n      randval >>= 2;\n      dstVBase[i] = (srcVBase[i] + (randval & 3)) >> 2;\n      randval >>= 2;\n      i++;\n    }\n\n    srcUBase += srcUVStride;\n    srcVBase += srcUVStride;\n    dstUBase += dstUVStride;\n    dstVBase += dstUVStride;\n  }\n\n  return 1;\n}\n\n#endif  // __ARM_NEON__\n\nstatic void convert_16_to_8_standard(const vpx_image_t* const img,\n                                     jbyte* const data, const int32_t uvHeight,\n                                     const int32_t yLength,\n                                     const int32_t uvLength) {\n  // Y\n  int sampleY = 0;\n  for (int y = 0; y < img->d_h; y++) {\n    const uint16_t* srcBase = reinterpret_cast<uint16_t*>(\n        img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y);\n    int8_t* destBase = data + img->stride[VPX_PLANE_Y] * y;\n    for (int x = 0; x < img->d_w; x++) {\n      // Lightweight dither. Carryover the remainder of each 10->8 bit\n      // conversion to the next pixel.\n      sampleY += *srcBase++;\n      *destBase++ = sampleY >> 2;\n      sampleY = sampleY & 3;  // Remainder.\n    }\n  }\n  // UV\n  int sampleU = 0;\n  int sampleV = 0;\n  const int32_t uvWidth = (img->d_w + 1) / 2;\n  for (int y = 0; y < uvHeight; y++) {\n    const uint16_t* srcUBase = reinterpret_cast<uint16_t*>(\n        img->planes[VPX_PLANE_U] + img->stride[VPX_PLANE_U] * y);\n    const uint16_t* srcVBase = reinterpret_cast<uint16_t*>(\n        img->planes[VPX_PLANE_V] + img->stride[VPX_PLANE_V] * y);\n    int8_t* destUBase = data + yLength + img->stride[VPX_PLANE_U] * y;\n    int8_t* destVBase =\n        data + yLength + uvLength + img->stride[VPX_PLANE_V] * y;\n    for (int x = 0; x < uvWidth; x++) {\n      // Lightweight dither. Carryover the remainder of each 10->8 bit\n      // conversion to the next pixel.\n      sampleU += *srcUBase++;\n      *destUBase++ = sampleU >> 2;\n      sampleU = sampleU & 3;  // Remainder.\n      sampleV += *srcVBase++;\n      *destVBase++ = sampleV >> 2;\n      sampleV = sampleV & 3;  // Remainder.\n    }\n  }\n}\n\nstruct JniFrameBuffer {\n  friend class JniBufferManager;\n\n  int stride[4];\n  uint8_t* planes[4];\n  int d_w;\n  int d_h;\n\n private:\n  int id;\n  int ref_count;\n  vpx_codec_frame_buffer_t vpx_fb;\n};\n\nclass JniBufferManager {\n  static const int MAX_FRAMES = 32;\n\n  JniFrameBuffer* all_buffers[MAX_FRAMES];\n  int all_buffer_count = 0;\n\n  JniFrameBuffer* free_buffers[MAX_FRAMES];\n  int free_buffer_count = 0;\n\n  pthread_mutex_t mutex;\n\n public:\n  JniBufferManager() { pthread_mutex_init(&mutex, NULL); }\n\n  ~JniBufferManager() {\n    while (all_buffer_count--) {\n      free(all_buffers[all_buffer_count]->vpx_fb.data);\n    }\n  }\n\n  int get_buffer(size_t min_size, vpx_codec_frame_buffer_t* fb) {\n    pthread_mutex_lock(&mutex);\n    JniFrameBuffer* out_buffer;\n    if (free_buffer_count) {\n      out_buffer = free_buffers[--free_buffer_count];\n      if (out_buffer->vpx_fb.size < min_size) {\n        free(out_buffer->vpx_fb.data);\n        out_buffer->vpx_fb.data = (uint8_t*)malloc(min_size);\n        out_buffer->vpx_fb.size = min_size;\n      }\n    } else {\n      out_buffer = new JniFrameBuffer();\n      out_buffer->id = all_buffer_count;\n      all_buffers[all_buffer_count++] = out_buffer;\n      out_buffer->vpx_fb.data = (uint8_t*)malloc(min_size);\n      out_buffer->vpx_fb.size = min_size;\n      out_buffer->vpx_fb.priv = &out_buffer->id;\n    }\n    *fb = out_buffer->vpx_fb;\n    int retVal = 0;\n    if (!out_buffer->vpx_fb.data || all_buffer_count >= MAX_FRAMES) {\n      LOGE(\"ERROR: JniBufferManager get_buffer OOM.\");\n      retVal = -1;\n    } else {\n      memset(fb->data, 0, fb->size);\n    }\n    out_buffer->ref_count = 1;\n    pthread_mutex_unlock(&mutex);\n    return retVal;\n  }\n\n  JniFrameBuffer* get_buffer(int id) const {\n    if (id < 0 || id >= all_buffer_count) {\n      LOGE(\"ERROR: JniBufferManager get_buffer invalid id %d.\", id);\n      return NULL;\n    }\n    return all_buffers[id];\n  }\n\n  void add_ref(int id) {\n    if (id < 0 || id >= all_buffer_count) {\n      LOGE(\"ERROR: JniBufferManager add_ref invalid id %d.\", id);\n      return;\n    }\n    pthread_mutex_lock(&mutex);\n    all_buffers[id]->ref_count++;\n    pthread_mutex_unlock(&mutex);\n  }\n\n  int release(int id) {\n    if (id < 0 || id >= all_buffer_count) {\n      LOGE(\"ERROR: JniBufferManager release invalid id %d.\", id);\n      return -1;\n    }\n    pthread_mutex_lock(&mutex);\n    JniFrameBuffer* buffer = all_buffers[id];\n    if (!buffer->ref_count) {\n      LOGE(\"ERROR: JniBufferManager release, buffer already released.\");\n      pthread_mutex_unlock(&mutex);\n      return -1;\n    }\n    if (!--buffer->ref_count) {\n      free_buffers[free_buffer_count++] = buffer;\n    }\n    pthread_mutex_unlock(&mutex);\n    return 0;\n  }\n};\n\nstruct JniCtx {\n  JniCtx() { buffer_manager = new JniBufferManager(); }\n\n  ~JniCtx() {\n    if (native_window) {\n      ANativeWindow_release(native_window);\n    }\n    if (buffer_manager) {\n      delete buffer_manager;\n    }\n  }\n\n  void acquire_native_window(JNIEnv* env, jobject new_surface) {\n    if (surface != new_surface) {\n      if (native_window) {\n        ANativeWindow_release(native_window);\n      }\n      native_window = ANativeWindow_fromSurface(env, new_surface);\n      surface = new_surface;\n      width = 0;\n    }\n  }\n\n  JniBufferManager* buffer_manager = NULL;\n  vpx_codec_ctx_t* decoder = NULL;\n  ANativeWindow* native_window = NULL;\n  jobject surface = NULL;\n  int width = 0;\n  int height = 0;\n};\n\nint vpx_get_frame_buffer(void* priv, size_t min_size,\n                         vpx_codec_frame_buffer_t* fb) {\n  JniBufferManager* const buffer_manager =\n      reinterpret_cast<JniBufferManager*>(priv);\n  return buffer_manager->get_buffer(min_size, fb);\n}\n\nint vpx_release_frame_buffer(void* priv, vpx_codec_frame_buffer_t* fb) {\n  JniBufferManager* const buffer_manager =\n      reinterpret_cast<JniBufferManager*>(priv);\n  return buffer_manager->release(*(int*)fb->priv);\n}\n\nDECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,\n             jboolean enableRowMultiThreadMode, jint threads) {\n  JniCtx* context = new JniCtx();\n  context->decoder = new vpx_codec_ctx_t();\n  vpx_codec_dec_cfg_t cfg = {0, 0, 0};\n  cfg.threads = threads;\n  errorCode = 0;\n  vpx_codec_err_t err =\n      vpx_codec_dec_init(context->decoder, &vpx_codec_vp9_dx_algo, &cfg, 0);\n  if (err) {\n    LOGE(\"ERROR: Failed to initialize libvpx decoder, error = %d.\", err);\n    errorCode = err;\n    return 0;\n  }\n#ifdef VPX_CTRL_VP9_DECODE_SET_ROW_MT\n  err = vpx_codec_control(context->decoder, VP9D_SET_ROW_MT,\n                          enableRowMultiThreadMode);\n  if (err) {\n    LOGE(\"ERROR: Failed to enable row multi thread mode, error = %d.\", err);\n  }\n#endif\n  if (disableLoopFilter) {\n    err = vpx_codec_control(context->decoder, VP9_SET_SKIP_LOOP_FILTER, true);\n    if (err) {\n      LOGE(\"ERROR: Failed to shut off libvpx loop filter, error = %d.\", err);\n    }\n#ifdef VPX_CTRL_VP9_SET_LOOP_FILTER_OPT\n  } else {\n    err = vpx_codec_control(context->decoder, VP9D_SET_LOOP_FILTER_OPT, true);\n    if (err) {\n      LOGE(\"ERROR: Failed to enable loop filter optimization, error = %d.\",\n           err);\n    }\n#endif\n  }\n  err = vpx_codec_set_frame_buffer_functions(\n      context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer,\n      context->buffer_manager);\n  if (err) {\n    LOGE(\"ERROR: Failed to set libvpx frame buffer functions, error = %d.\",\n         err);\n  }\n\n  // Populate JNI References.\n  const jclass outputBufferClass = env->FindClass(\n      \"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer\");\n  initForYuvFrame = env->GetMethodID(outputBufferClass, \"initForYuvFrame\",\n                                     \"(IIIII)Z\");\n  initForPrivateFrame =\n      env->GetMethodID(outputBufferClass, \"initForPrivateFrame\", \"(II)V\");\n  dataField = env->GetFieldID(outputBufferClass, \"data\",\n                              \"Ljava/nio/ByteBuffer;\");\n  outputModeField = env->GetFieldID(outputBufferClass, \"mode\", \"I\");\n  decoderPrivateField =\n      env->GetFieldID(outputBufferClass, \"decoderPrivate\", \"I\");\n  return reinterpret_cast<intptr_t>(context);\n}\n\nDECODER_FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) {\n  JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);\n  const uint8_t* const buffer =\n      reinterpret_cast<const uint8_t*>(env->GetDirectBufferAddress(encoded));\n  const vpx_codec_err_t status =\n      vpx_codec_decode(context->decoder, buffer, len, NULL, 0);\n  errorCode = 0;\n  if (status != VPX_CODEC_OK) {\n    LOGE(\"ERROR: vpx_codec_decode() failed, status= %d\", status);\n    errorCode = status;\n    return -1;\n  }\n  return 0;\n}\n\nDECODER_FUNC(jlong, vpxSecureDecode, jlong jContext, jobject encoded, jint len,\n    jobject mediaCrypto, jint inputMode, jbyteArray&, jbyteArray&,\n    jint inputNumSubSamples, jintArray numBytesOfClearData,\n    jintArray numBytesOfEncryptedData) {\n  // Doesn't support\n  // Java client should have checked vpxSupportSecureDecode\n  // and avoid calling this\n  // return -2 (DRM Error)\n  return -2;\n}\n\nDECODER_FUNC(jlong, vpxClose, jlong jContext) {\n  JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);\n  vpx_codec_destroy(context->decoder);\n  delete context;\n  return 0;\n}\n\nDECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {\n  JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);\n  vpx_codec_iter_t iter = NULL;\n  const vpx_image_t* const img = vpx_codec_get_frame(context->decoder, &iter);\n\n  if (img == NULL) {\n    return 1;\n  }\n\n  const int kOutputModeYuv = 0;\n  const int kOutputModeSurfaceYuv = 1;\n\n  int outputMode = env->GetIntField(jOutputBuffer, outputModeField);\n  if (outputMode == kOutputModeYuv) {\n    const int kColorspaceUnknown = 0;\n    const int kColorspaceBT601 = 1;\n    const int kColorspaceBT709 = 2;\n    const int kColorspaceBT2020 = 3;\n\n    int colorspace = kColorspaceUnknown;\n    switch (img->cs) {\n      case VPX_CS_BT_601:\n        colorspace = kColorspaceBT601;\n        break;\n      case VPX_CS_BT_709:\n        colorspace = kColorspaceBT709;\n        break;\n    case VPX_CS_BT_2020:\n        colorspace = kColorspaceBT2020;\n        break;\n      default:\n        break;\n    }\n\n    // resize buffer if required.\n    jboolean initResult = env->CallBooleanMethod(\n        jOutputBuffer, initForYuvFrame, img->d_w, img->d_h,\n        img->stride[VPX_PLANE_Y], img->stride[VPX_PLANE_U], colorspace);\n    if (env->ExceptionCheck() || !initResult) {\n      return -1;\n    }\n\n    // get pointer to the data buffer.\n    const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField);\n    jbyte* const data =\n        reinterpret_cast<jbyte*>(env->GetDirectBufferAddress(dataObject));\n\n    const int32_t uvHeight = (img->d_h + 1) / 2;\n    const uint64_t yLength = img->stride[VPX_PLANE_Y] * img->d_h;\n    const uint64_t uvLength = img->stride[VPX_PLANE_U] * uvHeight;\n    if (img->fmt == VPX_IMG_FMT_I42016) {  // HBD planar 420.\n      // Note: The stride for BT2020 is twice of what we use so this is wasting\n      // memory. The long term goal however is to upload half-float/short so\n      // it's not important to optimize the stride at this time.\n      int converted = 0;\n#ifdef __ARM_NEON__\n      converted = convert_16_to_8_neon(img, data, uvHeight, yLength, uvLength);\n#endif  // __ARM_NEON__\n      if (!converted) {\n        convert_16_to_8_standard(img, data, uvHeight, yLength, uvLength);\n      }\n    } else {\n      // TODO: This copy can be eliminated by using external frame\n      // buffers. This is insignificant for smaller videos but takes ~1.5ms\n      // for 1080p clips. So this should eventually be gotten rid of.\n      memcpy(data, img->planes[VPX_PLANE_Y], yLength);\n      memcpy(data + yLength, img->planes[VPX_PLANE_U], uvLength);\n      memcpy(data + yLength + uvLength, img->planes[VPX_PLANE_V], uvLength);\n    }\n  } else if (outputMode == kOutputModeSurfaceYuv &&\n             img->fmt != VPX_IMG_FMT_I42016) {\n    int id = *(int*)img->fb_priv;\n    context->buffer_manager->add_ref(id);\n    JniFrameBuffer* jfb = context->buffer_manager->get_buffer(id);\n    for (int i = 2; i >= 0; i--) {\n      jfb->stride[i] = img->stride[i];\n      jfb->planes[i] = (uint8_t*)img->planes[i];\n    }\n    jfb->d_w = img->d_w;\n    jfb->d_h = img->d_h;\n    env->CallVoidMethod(jOutputBuffer, initForPrivateFrame, img->d_w, img->d_h);\n    if (env->ExceptionCheck()) {\n      return -1;\n    }\n    env->SetIntField(jOutputBuffer, decoderPrivateField,\n                     id + kDecoderPrivateBase);\n  }\n  return 0;\n}\n\nDECODER_FUNC(jint, vpxRenderFrame, jlong jContext, jobject jSurface,\n             jobject jOutputBuffer) {\n  JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);\n  const int id = env->GetIntField(jOutputBuffer, decoderPrivateField) -\n                 kDecoderPrivateBase;\n  JniFrameBuffer* srcBuffer = context->buffer_manager->get_buffer(id);\n  context->acquire_native_window(env, jSurface);\n  if (context->native_window == NULL || !srcBuffer) {\n    return 1;\n  }\n  if (context->width != srcBuffer->d_w || context->height != srcBuffer->d_h) {\n    ANativeWindow_setBuffersGeometry(context->native_window, srcBuffer->d_w,\n                                     srcBuffer->d_h, kHalPixelFormatYV12);\n    context->width = srcBuffer->d_w;\n    context->height = srcBuffer->d_h;\n  }\n  ANativeWindow_Buffer buffer;\n  int result = ANativeWindow_lock(context->native_window, &buffer, NULL);\n  if (buffer.bits == NULL || result) {\n    return -1;\n  }\n  // Y\n  const size_t src_y_stride = srcBuffer->stride[VPX_PLANE_Y];\n  int stride = srcBuffer->d_w;\n  const uint8_t* src_base =\n      reinterpret_cast<uint8_t*>(srcBuffer->planes[VPX_PLANE_Y]);\n  uint8_t* dest_base = (uint8_t*)buffer.bits;\n  for (int y = 0; y < srcBuffer->d_h; y++) {\n    memcpy(dest_base, src_base, stride);\n    src_base += src_y_stride;\n    dest_base += buffer.stride;\n  }\n  // UV\n  const int src_uv_stride = srcBuffer->stride[VPX_PLANE_U];\n  const int dest_uv_stride = (buffer.stride / 2 + 15) & (~15);\n  const int32_t buffer_uv_height = (buffer.height + 1) / 2;\n  const int32_t height =\n      std::min((int32_t)(srcBuffer->d_h + 1) / 2, buffer_uv_height);\n  stride = (srcBuffer->d_w + 1) / 2;\n  src_base = reinterpret_cast<uint8_t*>(srcBuffer->planes[VPX_PLANE_U]);\n  const uint8_t* src_v_base =\n      reinterpret_cast<uint8_t*>(srcBuffer->planes[VPX_PLANE_V]);\n  uint8_t* dest_v_base =\n      ((uint8_t*)buffer.bits) + buffer.stride * buffer.height;\n  dest_base = dest_v_base + buffer_uv_height * dest_uv_stride;\n  for (int y = 0; y < height; y++) {\n    memcpy(dest_base, src_base, stride);\n    memcpy(dest_v_base, src_v_base, stride);\n    src_base += src_uv_stride;\n    src_v_base += src_uv_stride;\n    dest_base += dest_uv_stride;\n    dest_v_base += dest_uv_stride;\n  }\n  return ANativeWindow_unlockAndPost(context->native_window);\n}\n\nDECODER_FUNC(void, vpxReleaseFrame, jlong jContext, jobject jOutputBuffer) {\n  JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);\n  const int id = env->GetIntField(jOutputBuffer, decoderPrivateField) -\n                 kDecoderPrivateBase;\n  env->SetIntField(jOutputBuffer, decoderPrivateField, -1);\n  context->buffer_manager->release(id);\n}\n\nDECODER_FUNC(jstring, vpxGetErrorMessage, jlong jContext) {\n  JniCtx* const context = reinterpret_cast<JniCtx*>(jContext);\n  return env->NewStringUTF(vpx_codec_error(context->decoder));\n}\n\nDECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) { return errorCode; }\n\nLIBRARY_FUNC(jstring, vpxIsSecureDecodeSupported) {\n  // Doesn't support\n  return 0;\n}\n\nLIBRARY_FUNC(jstring, vpxGetVersion) {\n  return env->NewStringUTF(vpx_codec_version_str());\n}\n\nLIBRARY_FUNC(jstring, vpxGetBuildConfig) {\n  return env->NewStringUTF(vpx_codec_build_config());\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<manifest package=\"com.google.android.exoplayer2.ext.vp9\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/vp9/src/test/java/com/google/android/exoplayer2/ext/vp9/DefaultRenderersFactoryTest.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.ext.vp9;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link DefaultRenderersFactoryTest} with {@link LibvpxVideoRenderer}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DefaultRenderersFactoryTest {\n\n  @Test\n  public void createRenderers_instantiatesVpxRenderer() {\n    DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(\n        LibvpxVideoRenderer.class, C.TRACK_TYPE_VIDEO);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/workmanager/README.md",
    "content": "# ExoPlayer WorkManager extension\n\nThis extension provides a Scheduler implementation which uses [WorkManager][].\n\n[WorkManager]: https://developer.android.com/topic/libraries/architecture/workmanager.html\n\n## Getting the extension\n\nThe easiest way to use the extension is to add it as a gradle dependency:\n\n```gradle\nimplementation 'com.google.android.exoplayer:extension-workmanager:2.X.X'\n```\n\nwhere `2.X.X` is the version, which must match the version of the ExoPlayer\nlibrary being used.\n\nAlternatively, you can clone the ExoPlayer repository and depend on the module\nlocally. Instructions for doing this can be found in ExoPlayer's\n[top level README][].\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/workmanager/build.gradle",
    "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 */\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    implementation 'androidx.work:work-runtime:2.1.0'\n}\n\next {\n    javadocTitle = 'WorkManager extension'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'extension-workmanager'\n    releaseDescription = 'WorkManager extension for ExoPlayer.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/workmanager/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\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<manifest package=\"com.google.android.exoplayer2.ext.workmanager\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.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.ext.workmanager;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.content.Intent;\nimport androidx.work.Constraints;\nimport androidx.work.Data;\nimport androidx.work.ExistingWorkPolicy;\nimport androidx.work.NetworkType;\nimport androidx.work.OneTimeWorkRequest;\nimport androidx.work.WorkManager;\nimport androidx.work.Worker;\nimport androidx.work.WorkerParameters;\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.Util;\n\n/** A {@link Scheduler} that uses {@link WorkManager}. */\npublic final class WorkManagerScheduler implements Scheduler {\n\n  private static final boolean DEBUG = false;\n  private static final String TAG = \"WorkManagerScheduler\";\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 String workName;\n\n  /**\n   * @param workName A name for work scheduled by this instance. If the same name was used by a\n   *     previous instance, anything scheduled by the previous instance will be canceled by this\n   *     instance if {@link #schedule(Requirements, String, String)} or {@link #cancel()} are\n   *     called.\n   */\n  public WorkManagerScheduler(String workName) {\n    this.workName = workName;\n  }\n\n  @Override\n  public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {\n    Constraints constraints = buildConstraints(requirements);\n    Data inputData = buildInputData(requirements, servicePackage, serviceAction);\n    OneTimeWorkRequest workRequest = buildWorkRequest(constraints, inputData);\n    logd(\"Scheduling work: \" + workName);\n    WorkManager.getInstance().enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest);\n    return true;\n  }\n\n  @Override\n  public boolean cancel() {\n    logd(\"Canceling work: \" + workName);\n    WorkManager.getInstance().cancelUniqueWork(workName);\n    return true;\n  }\n\n  private static Constraints buildConstraints(Requirements requirements) {\n    Constraints.Builder builder = new Constraints.Builder();\n\n    if (requirements.isUnmeteredNetworkRequired()) {\n      builder.setRequiredNetworkType(NetworkType.UNMETERED);\n    } else if (requirements.isNetworkRequired()) {\n      builder.setRequiredNetworkType(NetworkType.CONNECTED);\n    } else {\n      builder.setRequiredNetworkType(NetworkType.NOT_REQUIRED);\n    }\n\n    if (requirements.isChargingRequired()) {\n      builder.setRequiresCharging(true);\n    }\n\n    if (requirements.isIdleRequired() && Util.SDK_INT >= 23) {\n      setRequiresDeviceIdle(builder);\n    }\n\n    return builder.build();\n  }\n\n  @TargetApi(23)\n  private static void setRequiresDeviceIdle(Constraints.Builder builder) {\n    builder.setRequiresDeviceIdle(true);\n  }\n\n  private static Data buildInputData(\n      Requirements requirements, String servicePackage, String serviceAction) {\n    Data.Builder builder = new Data.Builder();\n\n    builder.putInt(KEY_REQUIREMENTS, requirements.getRequirements());\n    builder.putString(KEY_SERVICE_PACKAGE, servicePackage);\n    builder.putString(KEY_SERVICE_ACTION, serviceAction);\n\n    return builder.build();\n  }\n\n  private static OneTimeWorkRequest buildWorkRequest(Constraints constraints, Data inputData) {\n    OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SchedulerWorker.class);\n\n    builder.setConstraints(constraints);\n    builder.setInputData(inputData);\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 Worker} that starts the target service if the requirements are met. */\n  // This class needs to be public so that WorkManager can instantiate it.\n  public static final class SchedulerWorker extends Worker {\n\n    private final WorkerParameters workerParams;\n    private final Context context;\n\n    public SchedulerWorker(Context context, WorkerParameters workerParams) {\n      super(context, workerParams);\n      this.workerParams = workerParams;\n      this.context = context;\n    }\n\n    @Override\n    public Result doWork() {\n      logd(\"SchedulerWorker is started\");\n      Data inputData = workerParams.getInputData();\n      Assertions.checkNotNull(inputData, \"Work started without input data.\");\n      Requirements requirements = new Requirements(inputData.getInt(KEY_REQUIREMENTS, 0));\n      if (requirements.checkRequirements(context)) {\n        logd(\"Requirements are met\");\n        String serviceAction = inputData.getString(KEY_SERVICE_ACTION);\n        String servicePackage = inputData.getString(KEY_SERVICE_PACKAGE);\n        Assertions.checkNotNull(serviceAction, \"Service action missing.\");\n        Assertions.checkNotNull(servicePackage, \"Service package missing.\");\n        Intent intent = new Intent(serviceAction).setPackage(servicePackage);\n        logd(\"Starting service action: \" + serviceAction + \" package: \" + servicePackage);\n        Util.startForegroundService(context, intent);\n        return Result.success();\n      } else {\n        logd(\"Requirements are not met\");\n        return Result.retry();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/gradle/wrapper/gradle-wrapper.properties",
    "content": "#Mon Oct 07 17:24:00 BST 2019\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": "exoplayer-amzn-2.10.6/gradle.properties",
    "content": "## Project-wide Gradle settings.\nandroid.useAndroidX=true\nandroid.enableJetifier=true\nandroid.enableUnitTestBinaryResources=true\nbuildDir=buildout\norg.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\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\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\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\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched.\nif $cygwin ; then\n    [ -n \"$JAVA_HOME\" ] && JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\nfi\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\\\"`/\" >&-\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >&-\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\" ] ; 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\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# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windowz variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\ngoto execute\n\n:4NT_args\n@rem Get arguments from the 4NT Shell from JP Software\nset CMD_LINE_ARGS=%$\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\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%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/issues/player-accessed-on-wrong-thread.md",
    "content": "---\npermalink: /issues/player-accessed-on-wrong-thread\nredirect_to:\n  - https://exoplayer.dev/troubleshooting.html#what-do-player-is-accessed-on-the-wrong-thread-warnings-mean\n---\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/javadoc_combined.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.\napply from: \"${buildscript.sourceFile.parentFile}/javadoc_util.gradle\"\n\nclass CombinedJavadocPlugin implements Plugin<Project> {\n\n  static final String TASK_NAME = \"generateCombinedJavadoc\"\n\n  @Override\n  void apply(Project project) {\n    project.gradle.projectsEvaluated {\n      Set<Project> libraryModules = getLibraryModules(project)\n      if (!libraryModules.isEmpty()) {\n        project.task(TASK_NAME, type: Javadoc) {\n          description = \"Generates combined Javadoc.\"\n          title = \"ExoPlayer library\"\n          source = libraryModules.generateJavadoc.source\n          classpath = project.files([])\n          destinationDir = project.file(\"$project.buildDir/docs/javadoc\")\n          options {\n            links \"https://docs.oracle.com/javase/7/docs/api/\",\n                \"https://developer.android.com/reference\"\n            encoding = \"UTF-8\"\n          }\n          exclude \"**/BuildConfig.java\"\n          exclude \"**/R.java\"\n          doFirst {\n            libraryModules.each { libraryModule ->\n              libraryModule.android.libraryVariants.all { variant ->\n                def name = variant.buildType.name\n                if (name == \"release\") {\n                  classpath +=\n                      libraryModule.project.files(\n                          variant.javaCompileProvider.get().classpath.files,\n                          libraryModule.project.android.getBootClasspath())\n                }\n              }\n            }\n          }\n          doLast {\n            libraryModules.each { libraryModule ->\n              project.copy {\n                from \"${libraryModule.projectDir}/src/main/javadoc\"\n                into \"${project.buildDir}/docs/javadoc\"\n              }\n            }\n            project.fixJavadoc()\n          }\n        }\n      }\n    }\n  }\n\n  // Returns Android library modules that declare a generateJavadoc task.\n  private static Set<Project> getLibraryModules(Project project) {\n    project.subprojects.findAll {\n      it.plugins.findPlugin(\"com.android.library\") &&\n      it.tasks.findByName(\"generateJavadoc\")\n    }\n  }\n\n}\n\napply plugin: CombinedJavadocPlugin\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/javadoc_library.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.\napply from: \"${buildscript.sourceFile.parentFile}/javadoc_util.gradle\"\n\nandroid.libraryVariants.all { variant ->\n    def name = variant.buildType.name\n    if (!name.equals(\"release\")) {\n        return; // Skip non-release builds.\n    }\n    def allSourceDirs = variant.sourceSets.inject ([]) {\n        acc, val -> acc << val.javaDirectories\n    }\n\n    if (!tasks.findByName('generateJavadoc')) {\n        task(\"generateJavadoc\", type: Javadoc) {\n            description = \"Generates Javadoc for the ${javadocTitle}.\"\n            title = \"ExoPlayer ${javadocTitle}\"\n            source = allSourceDirs\n            options {\n                links \"http://docs.oracle.com/javase/7/docs/api/\"\n                linksOffline \"https://developer.android.com/reference\",\n                        \"${android.sdkDirectory}/docs/reference\"\n                encoding = \"UTF-8\"\n            }\n            exclude \"**/BuildConfig.java\"\n            exclude \"**/R.java\"\n            doFirst {\n                classpath =\n                        files(\n                                variant.javaCompileProvider.get().classpath.files,\n                                project.android.getBootClasspath())\n            }\n            doLast {\n                copy {\n                    from \"src/main/javadoc\"\n                    into \"$buildDir/docs/javadoc\"\n                }\n                project.fixJavadoc()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/javadoc_util.gradle",
    "content": "// 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.\next.fixJavadoc = {\n  def javadocPath = \"${project.buildDir}/docs/javadoc\"\n  // Fix external Android links to target the top frame.\n  def androidRoot = \"https://developer.android.com/reference/\"\n  def androidLink = \"<a href=\\\"(${androidRoot}.*?)\\\\?is-external=true\\\"\"\n  def androidFixed = \"<a href=\\\"\\\\1\\\" target=\\\"_top\\\"\"\n  ant.replaceregexp(match:androidLink, replace:androidFixed, flags:'g') {\n    fileset(dir: \"${javadocPath}\", includes: \"**/*.html\")\n  }\n  // Fix external Oracle links to use frames and target the top frame.\n  def oracleRoot = \"https://docs.oracle.com/javase/7/docs/api/\"\n  def oracleLink = \"<a href=\\\"(${oracleRoot})(.*?)\\\\?is-external=true\\\"\"\n  def oracleFixed = \"<a href=\\\"\\\\1index.html\\\\?\\\\2\\\" target=\\\"_top\\\"\"\n  ant.replaceregexp(match:oracleLink, replace:oracleFixed, flags:'g') {\n    fileset(dir: \"${javadocPath}\", includes: \"**/*.html\")\n  }\n  // Remove date metadata that changes every time Javadoc is generated.\n  def javadocGeneratedBy = \"<!-- Generated by javadoc.*?-->\\n\"\n  ant.replaceregexp(match:javadocGeneratedBy, replace:\"\") {\n    fileset(dir: \"${javadocPath}\", includes: \"**/*.html\")\n  }\n  def dateMeta = \"<meta name=\\\"date\\\".*?>\\n\"\n  ant.replaceregexp(match:dateMeta, replace:\"\") {\n    fileset(dir: \"${javadocPath}\", includes: \"**/*.html\")\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/README.md",
    "content": "# ExoPlayer library #\n\nThe ExoPlayer library is split into multiple modules. See ExoPlayer's\n[top level README][] for more information about the available library modules\nand how to use them.\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/all/README.md",
    "content": "# ExoPlayer full library #\n\nAn empty module that depends on all of the other library modules. Depending on\nthe full library is equivalent to depending on all of the other library modules\nindividually. See ExoPlayer's [top level README][] for more information.\n\n[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md\n\n## Links ##\n\n* [Javadoc][]: Note that this Javadoc is combined with that of other modules.\n\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/all/build.gradle",
    "content": "// Copyright (C) 2016 The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    // MOD: FIX warning: source value 7 is obsolete and will be removed in a future release\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    compileSdkVersion project.ext.compileSdkVersion\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    // gradle 4.6 migration: disable dimensions mechanism\n    // more: https://proandroiddev.com/advanced-android-flavors-part-4-a-new-version-fc2ad80c01bb\n    flavorDimensions \"default\"\n\n    productFlavors {\n        stbeta {}\n        ststable {}\n        stfdroid {}\n    }\n}\n\ndependencies {\n    api project(modulePrefix + 'library-core')\n    api project(modulePrefix + 'library-dash')\n    api project(modulePrefix + 'library-sabr')\n    api project(modulePrefix + 'library-hls')\n    api project(modulePrefix + 'library-smoothstreaming')\n    api project(modulePrefix + 'library-ui')\n}\n\next {\n    releaseArtifact = 'exoplayer'\n    releaseDescription = 'The ExoPlayer library (all modules).'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/all/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest package=\"com.google.android.exoplayer2\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/README.md",
    "content": "# ExoPlayer core library module #\n\nThe core of the ExoPlayer library.\n\n## Links ##\n\n* [Javadoc][]: Note that this Javadoc is combined with that of other modules.\n\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/build.gradle",
    "content": "// Copyright (C) 2016 The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\napply plugin: 'com.android.library'\napply from: '../../constants.gradle'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n        consumerProguardFiles 'proguard-rules.txt'\n\n        testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'\n\n        // The following argument makes the Android Test Orchestrator run its\n        // \"pm clear\" command after each test invocation. This command ensures\n        // that the app's state is completely cleared between tests.\n        testInstrumentationRunnerArguments clearPackageData: 'true'\n    }\n\n    // MOD: comment out to fix: intellij: root already belongs to module\n    // Workaround to prevent circular dependency on project :testutils.\n    sourceSets {\n        androidTest {\n            java.srcDirs += '../../testutils/src/main/java/'\n        }\n        test {\n            java.srcDirs += '../../testutils/src/main/java/'\n            java.srcDirs += '../../testutils_robolectric/src/main/java/'\n        }\n    }\n\n    buildTypes {\n        // Re-enable test coverage when the following issue is fixed:\n        // https://issuetracker.google.com/issues/37019591\n        // debug {\n        //    testCoverageEnabled = true\n        // }\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    // MODIFIED: add custom dns resolver\n    implementation project(':sharedutils')\n    implementation 'androidx.annotation:annotation:' + annotationXVersion\n    compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion\n    compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion\n    androidTestImplementation 'androidx.test:runner:' + androidXTestVersion\n    androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion\n    androidTestImplementation 'androidx.test.ext:truth:' + androidXTestVersion\n    androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion\n    androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion\n    androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion\n    androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion\n    androidTestAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion\n    testImplementation 'androidx.test:core:' + androidXTestVersion\n    testImplementation 'androidx.test.ext:junit:' + androidXTestVersion\n    testImplementation 'androidx.test.ext:truth:' + androidXTestVersion\n    testImplementation 'org.mockito:mockito-core:' + mockitoVersion\n    testImplementation 'org.robolectric:robolectric:' + robolectricVersion\n    testImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion\n    testAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion\n}\n\next {\n    javadocTitle = 'Core module'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'exoplayer-core'\n    releaseDescription = 'The ExoPlayer library core module.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/proguard-rules.txt",
    "content": "# Proguard rules specific to the core module.\n\n# Constructors accessed via reflection in DefaultRenderersFactory\n-dontnote com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer\n-keepclassmembers class com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer {\n  <init>(long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int);\n}\n-dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer\n-keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer {\n  <init>(android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioProcessor[]);\n}\n-dontnote com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer\n-keepclassmembers class com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer {\n  <init>(android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioProcessor[]);\n}\n-dontnote com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer\n-keepclassmembers class com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer {\n  <init>(android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioProcessor[]);\n}\n\n# Constructors accessed via reflection in DefaultExtractorsFactory\n-dontnote com.google.android.exoplayer2.ext.flac.FlacExtractor\n-keepclassmembers class com.google.android.exoplayer2.ext.flac.FlacExtractor {\n  <init>();\n}\n\n# Constructors accessed via reflection in DefaultDataSource\n-dontnote com.google.android.exoplayer2.ext.rtmp.RtmpDataSource\n-keepclassmembers class com.google.android.exoplayer2.ext.rtmp.RtmpDataSource {\n  <init>();\n}\n\n# Constructors accessed via reflection in DefaultDownloaderFactory\n-dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloader\n-keepclassmembers class com.google.android.exoplayer2.source.dash.offline.DashDownloader {\n  <init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper);\n}\n-dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloader\n-keepclassmembers class com.google.android.exoplayer2.source.hls.offline.HlsDownloader {\n  <init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper);\n}\n-dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader\n-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader {\n  <init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper);\n}\n\n# Constructors accessed via reflection in DownloadHelper\n-dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory\n-keepclasseswithmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory {\n  <init>(com.google.android.exoplayer2.upstream.DataSource$Factory);\n  ** setStreamKeys(java.util.List);\n  com.google.android.exoplayer2.source.dash.DashMediaSource createMediaSource(android.net.Uri);\n}\n-dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory\n-keepclasseswithmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory {\n  <init>(com.google.android.exoplayer2.upstream.DataSource$Factory);\n  ** setStreamKeys(java.util.List);\n  com.google.android.exoplayer2.source.hls.HlsMediaSource createMediaSource(android.net.Uri);\n}\n-dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory\n-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {\n  <init>(com.google.android.exoplayer2.upstream.DataSource$Factory);\n  ** setStreamKeys(java.util.List);\n  com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource createMediaSource(android.net.Uri);\n}\n\n# Don't warn about checkerframework\n-dontwarn org.checkerframework.**\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/androidTest/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"com.google.android.exoplayer2.core.test\">\n\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n  <uses-sdk/>\n\n  <application\n      android:allowBackup=\"false\"\n      tools:ignore=\"MissingApplicationIcon,HardcodedDebugMode\">\n    <uses-library android:name=\"android.test.runner\"/>\n    <provider\n      android:authorities=\"com.google.android.exoplayer2.core.test\"\n      android:name=\"com.google.android.exoplayer2.upstream.ContentDataSourceTest$TestContentProvider\"/>\n  </application>\n\n  <instrumentation\n      android:targetPackage=\"com.google.android.exoplayer2.core.test\"\n      android:name=\"androidx.test.runner.AndroidJUnitRunner\"/>\n\n</manifest>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.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.common.truth.Truth.assertThat;\nimport static junit.framework.Assert.fail;\n\nimport android.content.ContentProvider;\nimport android.content.ContentResolver;\nimport android.content.ContentValues;\nimport android.content.res.AssetFileDescriptor;\nimport android.database.Cursor;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.os.ParcelFileDescriptor;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.test.InstrumentationRegistry;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link ContentDataSource}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ContentDataSourceTest {\n\n  private static final String AUTHORITY = \"com.google.android.exoplayer2.core.test\";\n  private static final String DATA_PATH = \"binary/1024_incrementing_bytes.mp3\";\n\n  @Test\n  public void testRead() throws Exception {\n    assertData(0, C.LENGTH_UNSET, false);\n  }\n\n  @Test\n  public void testReadPipeMode() throws Exception {\n    assertData(0, C.LENGTH_UNSET, true);\n  }\n\n  @Test\n  public void testReadFixedLength() throws Exception {\n    assertData(0, 100, false);\n  }\n\n  @Test\n  public void testReadFromOffsetToEndOfInput() throws Exception {\n    assertData(1, C.LENGTH_UNSET, false);\n  }\n\n  @Test\n  public void testReadFromOffsetToEndOfInputPipeMode() throws Exception {\n    assertData(1, C.LENGTH_UNSET, true);\n  }\n\n  @Test\n  public void testReadFromOffsetFixedLength() throws Exception {\n    assertData(1, 100, false);\n  }\n\n  @Test\n  public void testReadInvalidUri() throws Exception {\n    ContentDataSource dataSource =\n        new ContentDataSource(InstrumentationRegistry.getTargetContext());\n    Uri contentUri = TestContentProvider.buildUri(\"does/not.exist\", false);\n    DataSpec dataSpec = new DataSpec(contentUri);\n    try {\n      dataSource.open(dataSpec);\n      fail();\n    } catch (ContentDataSource.ContentDataSourceException e) {\n      // Expected.\n      assertThat(e).hasCauseThat().isInstanceOf(FileNotFoundException.class);\n    } finally {\n      dataSource.close();\n    }\n  }\n\n  private static void assertData(int offset, int length, boolean pipeMode) throws IOException {\n    Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode);\n    ContentDataSource dataSource =\n        new ContentDataSource(InstrumentationRegistry.getTargetContext());\n    try {\n      DataSpec dataSpec = new DataSpec(contentUri, offset, length, null);\n      byte[] completeData =\n          TestUtil.getByteArray(InstrumentationRegistry.getTargetContext(), DATA_PATH);\n      byte[] expectedData = Arrays.copyOfRange(completeData, offset,\n          length == C.LENGTH_UNSET ? completeData.length : offset + length);\n      TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode);\n    } finally {\n      dataSource.close();\n    }\n  }\n\n  /**\n   * A {@link ContentProvider} for the test.\n   */\n  public static final class TestContentProvider extends ContentProvider\n      implements ContentProvider.PipeDataWriter<Object> {\n\n    private static final String PARAM_PIPE_MODE = \"pipe-mode\";\n\n    public static Uri buildUri(String filePath, boolean pipeMode) {\n      Uri.Builder builder = new Uri.Builder()\n          .scheme(ContentResolver.SCHEME_CONTENT)\n          .authority(AUTHORITY)\n          .path(filePath);\n      if (pipeMode) {\n        builder.appendQueryParameter(TestContentProvider.PARAM_PIPE_MODE, \"1\");\n      }\n      return builder.build();\n    }\n\n    @Override\n    public boolean onCreate() {\n      return true;\n    }\n\n    @Override\n    public Cursor query(@NonNull Uri uri, String[] projection, String selection,\n        String[] selectionArgs, String sortOrder) {\n      throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode)\n        throws FileNotFoundException {\n      if (uri.getPath() == null) {\n        return null;\n      }\n      try {\n        String fileName = getFileName(uri);\n        boolean pipeMode = uri.getQueryParameter(PARAM_PIPE_MODE) != null;\n        if (pipeMode) {\n          ParcelFileDescriptor fileDescriptor = openPipeHelper(uri, null, null, null, this);\n          return new AssetFileDescriptor(fileDescriptor, 0, C.LENGTH_UNSET);\n        } else {\n          return getContext().getAssets().openFd(fileName);\n        }\n      } catch (IOException e) {\n        FileNotFoundException exception = new FileNotFoundException(e.getMessage());\n        exception.initCause(e);\n        throw exception;\n      }\n    }\n\n    @Override\n    public String getType(@NonNull Uri uri) {\n      throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Uri insert(@NonNull Uri uri, ContentValues values) {\n      throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {\n      throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public int update(@NonNull Uri uri, ContentValues values, String selection,\n        String[] selectionArgs) {\n      throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public void writeDataToPipe(@NonNull ParcelFileDescriptor output, @NonNull Uri uri,\n        @NonNull String mimeType, @Nullable Bundle opts, @Nullable Object args) {\n      try {\n        byte[] data = TestUtil.getByteArray(getContext(), getFileName(uri));\n        FileOutputStream outputStream = new FileOutputStream(output.getFileDescriptor());\n        outputStream.write(data);\n        outputStream.close();\n      } catch (IOException e) {\n        throw new RuntimeException(\"Error writing to pipe\", e);\n      }\n    }\n\n    private static String getFileName(Uri uri) {\n      return uri.getPath().replaceFirst(\"/\", \"\");\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.google.android.exoplayer2.core\">\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n</manifest>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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    int windowIndex = getCurrentWindowIndex();\n    Timeline timeline = getCurrentTimeline();\n    return windowIndex >= timeline.getWindowCount()\n        ? null\n        : timeline.getWindow(windowIndex, window, /* setTag= */ true).tag;\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 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 Player.EventListener listener;\n\n    private boolean released;\n\n    public ListenerHolder(Player.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 Player.EventListener} method. */\n  protected interface ListenerInvocation {\n\n    /** Executes the invocation on the given {@link Player.EventListener}. */\n    void invokeListener(Player.EventListener listener);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\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.source.SampleStream;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MediaClock;\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\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\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    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  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  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    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    onReset();\n  }\n\n  // RendererCapabilities implementation.\n\n  @Override\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 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  /**\n   * Returns the index of the renderer within the player.\n   */\n  protected final int getIndex() {\n    return index;\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\n   *     {@link 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(FormatHolder formatHolder, DecoderInputBuffer buffer,\n      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-amzn-2.10.6/library/core/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.AudioFormat;\nimport android.media.AudioManager;\nimport android.media.MediaCodec;\nimport android.media.MediaFormat;\nimport androidx.annotation.IntDef;\nimport android.view.Surface;\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.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 android.media.AudioAttributes#CONTENT_TYPE_MOVIE\n   */\n  public static final int CONTENT_TYPE_MOVIE = android.media.AudioAttributes.CONTENT_TYPE_MOVIE;\n  /**\n   * @see android.media.AudioAttributes#CONTENT_TYPE_MUSIC\n   */\n  public static final int CONTENT_TYPE_MUSIC = android.media.AudioAttributes.CONTENT_TYPE_MUSIC;\n  /**\n   * @see android.media.AudioAttributes#CONTENT_TYPE_SONIFICATION\n   */\n  public static final int CONTENT_TYPE_SONIFICATION =\n      android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION;\n  /**\n   * @see android.media.AudioAttributes#CONTENT_TYPE_SPEECH\n   */\n  public static final int CONTENT_TYPE_SPEECH =\n      android.media.AudioAttributes.CONTENT_TYPE_SPEECH;\n  /**\n   * @see android.media.AudioAttributes#CONTENT_TYPE_UNKNOWN\n   */\n  public static final int CONTENT_TYPE_UNKNOWN =\n      android.media.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 android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED\n   */\n  public static final int FLAG_AUDIBILITY_ENFORCED =\n      android.media.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 android.media.AudioAttributes#USAGE_ALARM\n   */\n  public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM;\n  /** @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY */\n  public static final int USAGE_ASSISTANCE_ACCESSIBILITY =\n      android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;\n  /**\n   * @see android.media.AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE\n   */\n  public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE =\n      android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;\n  /**\n   * @see android.media.AudioAttributes#USAGE_ASSISTANCE_SONIFICATION\n   */\n  public static final int USAGE_ASSISTANCE_SONIFICATION =\n      android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;\n  /** @see android.media.AudioAttributes#USAGE_ASSISTANT */\n  public static final int USAGE_ASSISTANT = android.media.AudioAttributes.USAGE_ASSISTANT;\n  /**\n   * @see android.media.AudioAttributes#USAGE_GAME\n   */\n  public static final int USAGE_GAME = android.media.AudioAttributes.USAGE_GAME;\n  /**\n   * @see android.media.AudioAttributes#USAGE_MEDIA\n   */\n  public static final int USAGE_MEDIA = android.media.AudioAttributes.USAGE_MEDIA;\n  /**\n   * @see android.media.AudioAttributes#USAGE_NOTIFICATION\n   */\n  public static final int USAGE_NOTIFICATION = android.media.AudioAttributes.USAGE_NOTIFICATION;\n  /**\n   * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED\n   */\n  public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED =\n      android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED;\n  /**\n   * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT\n   */\n  public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT =\n      android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT;\n  /**\n   * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST\n   */\n  public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST =\n      android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST;\n  /**\n   * @see android.media.AudioAttributes#USAGE_NOTIFICATION_EVENT\n   */\n  public static final int USAGE_NOTIFICATION_EVENT =\n      android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT;\n  /**\n   * @see android.media.AudioAttributes#USAGE_NOTIFICATION_RINGTONE\n   */\n  public static final int USAGE_NOTIFICATION_RINGTONE =\n      android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;\n  /**\n   * @see android.media.AudioAttributes#USAGE_UNKNOWN\n   */\n  public static final int USAGE_UNKNOWN = android.media.AudioAttributes.USAGE_UNKNOWN;\n  /**\n   * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION\n   */\n  public static final int USAGE_VOICE_COMMUNICATION =\n      android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION;\n  /**\n   * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING\n   */\n  public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING =\n      android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING;\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_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 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  @SuppressWarnings(\"NumericOverflow\")\n  public static final int BUFFER_FLAG_DECODE_ONLY = 1 << 31; // 0x80000000\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 C.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 C.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   * 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.\n     *\n     * @param newPlaybackParameters The newly active {@link PlaybackParameters}.\n     */\n    void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters);\n\n  }\n\n  private final StandaloneMediaClock standaloneMediaClock;\n  private final PlaybackParameterListener listener;\n\n  private @Nullable Renderer rendererClockSource;\n  private @Nullable MediaClock rendererClock;\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.standaloneMediaClock = new StandaloneMediaClock(clock);\n  }\n\n  /**\n   * Starts the standalone fallback clock.\n   */\n  public void start() {\n    standaloneMediaClock.start();\n  }\n\n  /**\n   * Stops the standalone fallback clock.\n   */\n  public void stop() {\n    standaloneMediaClock.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    standaloneMediaClock.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(standaloneMediaClock.getPlaybackParameters());\n      ensureSynced();\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    }\n  }\n\n  /**\n   * Syncs internal clock if needed and returns current clock position in microseconds.\n   */\n  public long syncAndGetPositionUs() {\n    if (isUsingRendererClock()) {\n      ensureSynced();\n      return rendererClock.getPositionUs();\n    } else {\n      return standaloneMediaClock.getPositionUs();\n    }\n  }\n\n  // MediaClock implementation.\n\n  @Override\n  public long getPositionUs() {\n    if (isUsingRendererClock()) {\n      return rendererClock.getPositionUs();\n    } else {\n      return standaloneMediaClock.getPositionUs();\n    }\n  }\n\n  @Override\n  public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {\n    if (rendererClock != null) {\n      playbackParameters = rendererClock.setPlaybackParameters(playbackParameters);\n    }\n    standaloneMediaClock.setPlaybackParameters(playbackParameters);\n    listener.onPlaybackParametersChanged(playbackParameters);\n    return playbackParameters;\n  }\n\n  @Override\n  public PlaybackParameters getPlaybackParameters() {\n    return rendererClock != null ? rendererClock.getPlaybackParameters()\n        : standaloneMediaClock.getPlaybackParameters();\n  }\n\n  private void ensureSynced() {\n    long rendererClockPositionUs = rendererClock.getPositionUs();\n    standaloneMediaClock.resetPosition(rendererClockPositionUs);\n    PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters();\n    if (!playbackParameters.equals(standaloneMediaClock.getPlaybackParameters())) {\n      standaloneMediaClock.setPlaybackParameters(playbackParameters);\n      listener.onPlaybackParametersChanged(playbackParameters);\n    }\n  }\n\n  private boolean isUsingRendererClock() {\n    // Use the renderer clock if the providing renderer has not ended or needs the next sample\n    // stream to reenter the ready state. The latter case uses the standalone clock to avoid getting\n    // stuck if tracks in the current period have uneven durations.\n    // See: https://github.com/google/ExoPlayer/issues/1874.\n    return rendererClockSource != null && !rendererClockSource.isEnded()\n        && (rendererClockSource.isReady() || !rendererClockSource.hasReadStreamToEnd());\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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} or {@link ExoPlayerFactory}.\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} or {@link ExoPlayerFactory}.\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} or {@link ExoPlayerFactory}.\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              android.os.Handler.class,\n              com.google.android.exoplayer2.video.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\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              android.os.Handler.class,\n              com.google.android.exoplayer2.audio.AudioRendererEventListener.class,\n              com.google.android.exoplayer2.audio.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              android.os.Handler.class,\n              com.google.android.exoplayer2.audio.AudioRendererEventListener.class,\n              com.google.android.exoplayer2.audio.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              android.os.Handler.class,\n              com.google.android.exoplayer2.audio.AudioRendererEventListener.class,\n              com.google.android.exoplayer2.audio.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-amzn-2.10.6/library/core/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 androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\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  @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, /* rendererIndex= */ C.INDEX_UNSET);\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   * @return The created instance.\n   */\n  public static ExoPlaybackException createForRenderer(Exception cause, int rendererIndex) {\n    return new ExoPlaybackException(TYPE_RENDERER, cause, rendererIndex);\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, /* rendererIndex= */ C.INDEX_UNSET);\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, /* rendererIndex= */ C.INDEX_UNSET);\n  }\n\n  private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) {\n    super(cause);\n    this.type = type;\n    this.cause = cause;\n    this.rendererIndex = rendererIndex;\n  }\n\n  private ExoPlaybackException(@Type int type, String message) {\n    super(message);\n    this.type = type;\n    rendererIndex = C.INDEX_UNSET;\n    cause = null;\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-amzn-2.10.6/library/core/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.os.Looper;\nimport androidx.annotation.Nullable;\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.DataSource;\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 * ExoPlayerFactory}.\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  /** @deprecated Use {@link PlayerMessage.Target} instead. */\n  @Deprecated\n  interface ExoPlayerComponent extends PlayerMessage.Target {}\n\n  /** @deprecated Use {@link PlayerMessage} instead. */\n  @Deprecated\n  final class ExoPlayerMessage {\n\n    /** The target to receive the message. */\n    public final PlayerMessage.Target target;\n    /** The type of the message. */\n    public final int messageType;\n    /** The message. */\n    public final Object message;\n\n    /** @deprecated Use {@link ExoPlayer#createMessage(PlayerMessage.Target)} instead. */\n    @Deprecated\n    public ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object message) {\n      this.target = target;\n      this.messageType = messageType;\n      this.message = message;\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\n   * {@code 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  /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  void sendMessages(ExoPlayerMessage... messages);\n\n  /**\n   * @deprecated Use {@link #createMessage(PlayerMessage.Target)} with {@link\n   *     PlayerMessage#blockUntilDelivered()}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  void blockingSendMessages(ExoPlayerMessage... messages);\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-amzn-2.10.6/library/core/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.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/**\n * A factory for {@link ExoPlayer} instances.\n */\npublic final class ExoPlayerFactory {\n\n  private static @Nullable BandwidthMeter singletonBandwidthMeter;\n\n  private ExoPlayerFactory() {}\n\n  /**\n   * Creates a {@link SimpleExoPlayer} instance.\n   *\n   * @param context A {@link Context}.\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 extensionRendererMode The extension renderer mode, which determines if and how available\n   *     extension renderers are used. Note that extensions must be included in the application\n   *     build for them to be considered available.\n   * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,\n   *     LoadControl, DrmSessionManager)}.\n   */\n  @Deprecated\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   * Creates a {@link SimpleExoPlayer} instance.\n   *\n   * @param context A {@link Context}.\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 extensionRendererMode The extension renderer mode, which determines if and how available\n   *     extension renderers are used. Note that extensions must be included in the application\n   *     build for them to be considered available.\n   * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to\n   *     seamlessly join an ongoing playback.\n   * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector,\n   *     LoadControl, DrmSessionManager)}.\n   */\n  @Deprecated\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  /**\n   * Creates a {@link SimpleExoPlayer} instance.\n   *\n   * @param context A {@link Context}.\n   */\n  public static SimpleExoPlayer newSimpleInstance(Context context) {\n    return newSimpleInstance(context, new DefaultTrackSelector());\n  }\n\n  /**\n   * Creates a {@link SimpleExoPlayer} instance.\n   *\n   * @param context A {@link Context}.\n   * @param trackSelector The {@link TrackSelector} that will be used by the instance.\n   */\n  public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) {\n    return newSimpleInstance(context, new DefaultRenderersFactory(context), trackSelector);\n  }\n\n  /**\n   * Creates a {@link SimpleExoPlayer} instance.\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   */\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context, RenderersFactory renderersFactory, TrackSelector trackSelector) {\n    return newSimpleInstance(context, renderersFactory, trackSelector, new DefaultLoadControl());\n  }\n\n  /**\n   * Creates a {@link SimpleExoPlayer} instance.\n   *\n   * @param context A {@link Context}.\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   */\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   * Creates a {@link SimpleExoPlayer} instance. Available extension renderers are not used.\n   *\n   * @param context A {@link Context}.\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   */\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   * Creates a {@link SimpleExoPlayer} instance.\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 drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance\n   *     will not be used for DRM protected playbacks.\n   */\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  /**\n   * Creates a {@link SimpleExoPlayer} instance.\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   */\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   * Creates a {@link SimpleExoPlayer} instance.\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   */\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   * Creates a {@link SimpleExoPlayer} instance.\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   */\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.Factory(),\n        Util.getLooper());\n  }\n\n  /**\n   * Creates a {@link SimpleExoPlayer} instance.\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 analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that\n   *     will collect and forward all player events.\n   */\n  public static SimpleExoPlayer newSimpleInstance(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      AnalyticsCollector.Factory analyticsCollectorFactory) {\n    return newSimpleInstance(\n        context,\n        renderersFactory,\n        trackSelector,\n        loadControl,\n        drmSessionManager,\n        analyticsCollectorFactory,\n        Util.getLooper());\n  }\n\n  /**\n   * Creates a {@link SimpleExoPlayer} instance.\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 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  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.Factory(),\n        looper);\n  }\n\n  /**\n   * Creates a {@link SimpleExoPlayer} instance.\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 analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that\n   *     will collect and forward all player events.\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  public static SimpleExoPlayer newSimpleInstance(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      AnalyticsCollector.Factory analyticsCollectorFactory,\n      Looper looper) {\n    return newSimpleInstance(\n        context,\n        renderersFactory,\n        trackSelector,\n        loadControl,\n        drmSessionManager,\n        getDefaultBandwidthMeter(context),\n        analyticsCollectorFactory,\n        looper);\n  }\n\n  /**\n   * Creates a {@link SimpleExoPlayer} instance.\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 analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that\n   *     will collect and forward all player events.\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  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.Factory analyticsCollectorFactory,\n      Looper looper) {\n    return new SimpleExoPlayer(\n        context,\n        renderersFactory,\n        trackSelector,\n        loadControl,\n        drmSessionManager,\n        bandwidthMeter,\n        analyticsCollectorFactory,\n        looper);\n  }\n\n  /**\n   * Creates an {@link ExoPlayer} instance.\n   *\n   * @param context A {@link Context}.\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   */\n  public static ExoPlayer newInstance(\n      Context context, Renderer[] renderers, TrackSelector trackSelector) {\n    return newInstance(context, renderers, trackSelector, new DefaultLoadControl());\n  }\n\n  /**\n   * Creates an {@link ExoPlayer} instance.\n   *\n   * @param context A {@link Context}.\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   */\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  /**\n   * Creates an {@link ExoPlayer} instance.\n   *\n   * @param context A {@link Context}.\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 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  public static ExoPlayer newInstance(\n      Context context,\n      Renderer[] renderers,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      Looper looper) {\n    return newInstance(\n        context, renderers, trackSelector, loadControl, getDefaultBandwidthMeter(context), looper);\n  }\n\n  /**\n   * Creates an {@link ExoPlayer} instance.\n   *\n   * @param context A {@link Context}.\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 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(\"unused\")\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  private static synchronized BandwidthMeter getDefaultBandwidthMeter(Context context) {\n    if (singletonBandwidthMeter == null) {\n      singletonBandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();\n    }\n    return singletonBandwidthMeter;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.util.Pair;\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.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\n/** An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayerFactory}. */\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 PlaybackParameters playbackParameters;\n  private SeekParameters seekParameters;\n  private @Nullable ExoPlaybackException playbackError;\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(Player.EventListener listener) {\n    listeners.addIfAbsent(new ListenerHolder(listener));\n  }\n\n  @Override\n  public void removeListener(Player.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  public int getPlaybackState() {\n    return playbackInfo.playbackState;\n  }\n\n  @PlaybackSuppressionReason\n  public int getPlaybackSuppressionReason() {\n    return playbackSuppressionReason;\n  }\n\n  @Override\n  @Nullable\n  public ExoPlaybackException getPlaybackError() {\n    return playbackError;\n  }\n\n  @Override\n  public void retry() {\n    if (mediaSource != null\n        && (playbackError != 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    playbackError = null;\n    this.mediaSource = mediaSource;\n    PlaybackInfo playbackInfo =\n        getResetPlaybackInfo(\n            resetPosition, resetState, /* 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  @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    internalPlayer.setPlaybackParameters(playbackParameters);\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      playbackError = null;\n      mediaSource = null;\n    }\n    PlaybackInfo playbackInfo =\n        getResetPlaybackInfo(\n            /* resetPosition= */ reset,\n            /* resetState= */ 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            /* playbackState= */ Player.STATE_IDLE);\n  }\n\n  @Override\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public void sendMessages(ExoPlayerMessage... messages) {\n    for (ExoPlayerMessage message : messages) {\n      createMessage(message.target).setType(message.messageType).setPayload(message.message).send();\n    }\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  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public void blockingSendMessages(ExoPlayerMessage... messages) {\n    List<PlayerMessage> playerMessages = new ArrayList<>();\n    for (ExoPlayerMessage message : messages) {\n      playerMessages.add(\n          createMessage(message.target)\n              .setType(message.messageType)\n              .setPayload(message.message)\n              .send());\n    }\n    boolean wasInterrupted = false;\n    for (PlayerMessage message : playerMessages) {\n      boolean blockMessage = true;\n      while (blockMessage) {\n        try {\n          message.blockUntilDelivered();\n          blockMessage = false;\n        } catch (InterruptedException e) {\n          wasInterrupted = true;\n        }\n      }\n    }\n    if (wasInterrupted) {\n      // Restore the interrupted status.\n      Thread.currentThread().interrupt();\n    }\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  @Override\n  public Object getCurrentManifest() {\n    return playbackInfo.manifest;\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        PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj;\n        if (!this.playbackParameters.equals(playbackParameters)) {\n          this.playbackParameters = playbackParameters;\n          notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParameters));\n        }\n        break;\n      case ExoPlayerImplInternal.MSG_ERROR:\n        ExoPlaybackException playbackError = (ExoPlaybackException) msg.obj;\n        this.playbackError = playbackError;\n        notifyListeners(listener -> listener.onPlayerError(playbackError));\n        break;\n      default:\n        throw new IllegalStateException();\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.resetToNewPosition(\n                playbackInfo.periodId, /* startPositionUs= */ 0, playbackInfo.contentPositionUs);\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      @Player.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, boolean resetState, 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)\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        resetState ? null : playbackInfo.manifest,\n        mediaPeriodId,\n        startPositionUs,\n        contentPositionUs,\n        playbackState,\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      @Player.DiscontinuityReason int positionDiscontinuityReason,\n      @Player.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 @Player.DiscontinuityReason int positionDiscontinuityReason;\n    private final @Player.TimelineChangeReason int timelineChangeReason;\n    private final boolean seekProcessed;\n    private final boolean playbackStateChanged;\n    private final boolean timelineOrManifestChanged;\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      timelineOrManifestChanged =\n          previousPlaybackInfo.timeline != playbackInfo.timeline\n              || previousPlaybackInfo.manifest != playbackInfo.manifest;\n      isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading;\n      trackSelectorResultChanged =\n          previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;\n    }\n\n    @Override\n    public void run() {\n      if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {\n        invokeAll(\n            listenerSnapshot,\n            listener ->\n                listener.onTimelineChanged(\n                    playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason));\n      }\n      if (positionDiscontinuity) {\n        invokeAll(\n            listenerSnapshot,\n            listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason));\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-amzn-2.10.6/library/core/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 androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport android.util.Pair;\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.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        MediaSource.SourceInfoRefreshListener,\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  public static final int MSG_ERROR = 2;\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 PREPARING_SOURCE_INTERVAL_MS = 10;\n  private static final int RENDERING_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  @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\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 = new HandlerThread(\"ExoPlayerImplInternal:Handler\",\n        Process.THREAD_PRIORITY_AUDIO);\n    internalPlaybackThread.start();\n    handler = clock.createHandler(internalPlaybackThread.getLooper(), this);\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.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) {\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 (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() && !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\n  public synchronized void release() {\n    if (released) {\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.SourceInfoRefreshListener implementation.\n\n  @Override\n  public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) {\n    handler.obtainMessage(MSG_REFRESH_SOURCE_INFO,\n        new MediaSourceRefreshInfo(source, timeline, manifest)).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    handler\n        .obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL, playbackParameters)\n        .sendToTarget();\n  }\n\n  // Handler.Callback implementation.\n\n  @SuppressWarnings(\"unchecked\")\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((PlaybackParameters) msg.obj);\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, \"Playback error.\", e);\n      eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();\n      stopInternal(\n          /* forceResetRenderers= */ true,\n          /* resetPositionAndState= */ false,\n          /* acknowledgeStop= */ false);\n      maybeNotifyPlaybackInfoChanged();\n    } catch (IOException e) {\n      Log.e(TAG, \"Source error.\", e);\n      eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget();\n      stopInternal(\n          /* forceResetRenderers= */ false,\n          /* resetPositionAndState= */ false,\n          /* acknowledgeStop= */ false);\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      eventHandler.obtainMessage(MSG_ERROR, error).sendToTarget();\n      stopInternal(\n          /* forceResetRenderers= */ true,\n          /* resetPositionAndState= */ false,\n          /* acknowledgeStop= */ false);\n      maybeNotifyPlaybackInfoChanged();\n    }\n    return true;\n  }\n\n  // Private methods.\n\n  private void setState(int state) {\n    if (playbackInfo.playbackState != state) {\n      playbackInfo = playbackInfo.copyWithPlaybackState(state);\n    }\n  }\n\n  private void setIsLoading(boolean isLoading) {\n    if (playbackInfo.isLoading != isLoading) {\n      playbackInfo = playbackInfo.copyWithIsLoading(isLoading);\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, /* releaseMediaSource= */ true, resetPosition, resetState);\n    loadControl.onPrepared();\n    this.mediaSource = mediaSource;\n    setState(Player.STATE_BUFFERING);\n    mediaSource.prepareSource(/* listener= */ 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 =\n          playbackInfo.copyWithNewPosition(\n              periodId,\n              newPositionUs,\n              playbackInfo.contentPositionUs,\n              getTotalBufferedDurationUs());\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    if (!queue.hasPlayingPeriod()) {\n      return;\n    }\n\n    // Update the playback position.\n    MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();\n    long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity();\n    if (periodPositionUs != C.TIME_UNSET) {\n      resetRendererPosition(periodPositionUs);\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 (periodPositionUs != playbackInfo.positionUs) {\n        playbackInfo =\n            playbackInfo.copyWithNewPosition(\n                playbackInfo.periodId,\n                periodPositionUs,\n                playbackInfo.contentPositionUs,\n                getTotalBufferedDurationUs());\n        playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);\n      }\n    } else {\n      rendererPositionUs = mediaClock.syncAndGetPositionUs();\n      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    if (!queue.hasPlayingPeriod()) {\n      // We're still waiting for the first period to be prepared.\n      maybeThrowPeriodPrepareError();\n      scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS);\n      return;\n    }\n    MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();\n\n    TraceUtil.beginSection(\"doSomeWork\");\n\n    updatePlaybackPositions();\n    long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;\n\n    playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs - backBufferDurationUs,\n        retainBackBufferFromKeyframe);\n\n    boolean renderersEnded = true;\n    boolean renderersReadyOrEnded = true;\n    for (Renderer renderer : enabledRenderers) {\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 is ready (or ended). We override to assume the renderer is\n      // ready if it needs the next sample stream. This is necessary to avoid getting stuck if\n      // tracks in the current period have uneven durations. See:\n      // https://github.com/google/ExoPlayer/issues/1874\n      boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded()\n          || rendererWaitingForNextStream(renderer);\n      if (!rendererReadyOrEnded) {\n        renderer.maybeThrowStreamError();\n      }\n      renderersReadyOrEnded = renderersReadyOrEnded && rendererReadyOrEnded;\n    }\n    if (!renderersReadyOrEnded) {\n      maybeThrowPeriodPrepareError();\n    }\n\n    long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;\n    if (renderersEnded\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(renderersReadyOrEnded)) {\n      setState(Player.STATE_READY);\n      if (playWhenReady) {\n        startRenderers();\n      }\n    } else if (playbackInfo.playbackState == Player.STATE_READY\n        && !(enabledRenderers.length == 0 ? isTimelineReady() : renderersReadyOrEnded)) {\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, RENDERING_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);\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      } 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 && 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 =\n          playbackInfo.copyWithNewPosition(\n              periodId, periodPositionUs, contentPositionUs, getTotalBufferedDurationUs());\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    setState(Player.STATE_BUFFERING);\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    rendererPositionUs =\n        !queue.hasPlayingPeriod()\n            ? periodPositionUs\n            : queue.getPlayingPeriod().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  }\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    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    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    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= */ !resetPosition);\n    setIsLoading(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)\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            resetState ? null : playbackInfo.manifest,\n            mediaPeriodId,\n            startPositionUs,\n            contentPositionUs,\n            playbackInfo.playbackState,\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(/* listener= */ 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    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.\n    if (playbackInfo.startPositionUs == oldPeriodPositionUs) {\n      oldPeriodPositionUs--;\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    if (!queue.hasPlayingPeriod()) {\n      // We don't have tracks yet, so we don't care.\n      return;\n    }\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 != null) {\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            playbackInfo.copyWithNewPosition(\n                playbackInfo.periodId,\n                periodPositionUs,\n                playbackInfo.contentPositionUs,\n                getTotalBufferedDurationUs());\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.getFrontPeriod();\n    while (periodHolder != null && periodHolder.prepared) {\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.getFrontPeriod();\n    while (periodHolder != null) {\n      TrackSelectorResult trackSelectorResult = periodHolder.getTrackSelectorResult();\n      if (trackSelectorResult != null) {\n        TrackSelection[] trackSelections = trackSelectorResult.selections.getAll();\n        for (TrackSelection trackSelection : trackSelections) {\n          if (trackSelection != null) {\n            trackSelection.onDiscontinuity();\n          }\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    MediaPeriodHolder nextPeriodHolder = playingPeriodHolder.getNext();\n    long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;\n    return playingPeriodDurationUs == C.TIME_UNSET\n        || playbackInfo.positionUs < playingPeriodDurationUs\n        || (nextPeriodHolder != null\n            && (nextPeriodHolder.prepared || nextPeriodHolder.info.id.isAd()));\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 maybeThrowPeriodPrepareError() throws IOException {\n    MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();\n    MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();\n    if (loadingPeriodHolder != null\n        && !loadingPeriodHolder.prepared\n        && (readingPeriodHolder == null || readingPeriodHolder.getNext() == loadingPeriodHolder)) {\n      for (Renderer renderer : enabledRenderers) {\n        if (!renderer.hasReadStreamToEnd()) {\n          return;\n        }\n      }\n      loadingPeriodHolder.mediaPeriod.maybeThrowPrepareError();\n    }\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    Object manifest = sourceRefreshInfo.manifest;\n    queue.setTimeline(timeline);\n    playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest);\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.getFrontPeriod();\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 =\n          playbackInfo.copyWithNewPosition(\n              newPeriodId, seekedToPositionUs, newContentPositionUs, getTotalBufferedDurationUs());\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    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    setState(Player.STATE_ENDED);\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  }\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 = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode,\n          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  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 = seekTimeline.getPeriodPosition(window, period, seekPosition.windowIndex,\n          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      Object periodUid = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline);\n      if (periodUid != null) {\n        // We found one. Map the SeekPosition onto the corresponding default position.\n        return getPeriodPosition(\n            timeline, timeline.getPeriod(periodIndex, 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\n    // Update the loading period if required.\n    maybeUpdateLoadingPeriod();\n    MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();\n    if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) {\n      setIsLoading(false);\n    } else if (!playbackInfo.isLoading) {\n      maybeContinueLoading();\n    }\n\n    if (!queue.hasPlayingPeriod()) {\n      // We're waiting for the first period to be prepared.\n      return;\n    }\n\n    // Advance the playing period if necessary.\n    MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();\n    MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();\n    boolean advancedPlayingPeriod = false;\n    while (playWhenReady\n        && playingPeriodHolder != readingPeriodHolder\n        && rendererPositionUs >= playingPeriodHolder.getNext().getStartPositionRendererTime()) {\n      // All enabled renderers' streams have been read to the end, and the playback position reached\n      // the end of the playing period, so advance playback to the next period.\n      if (advancedPlayingPeriod) {\n        // If we advance more than one period at a time, notify listeners after each update.\n        maybeNotifyPlaybackInfoChanged();\n      }\n      int discontinuityReason =\n          playingPeriodHolder.info.isLastInTimelinePeriod\n              ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION\n              : Player.DISCONTINUITY_REASON_AD_INSERTION;\n      MediaPeriodHolder oldPlayingPeriodHolder = playingPeriodHolder;\n      playingPeriodHolder = queue.advancePlayingPeriod();\n      updatePlayingPeriodRenderers(oldPlayingPeriodHolder);\n      playbackInfo =\n          playbackInfo.copyWithNewPosition(\n              playingPeriodHolder.info.id,\n              playingPeriodHolder.info.startPositionUs,\n              playingPeriodHolder.info.contentPositionUs,\n              getTotalBufferedDurationUs());\n      playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason);\n      updatePlaybackPositions();\n      advancedPlayingPeriod = true;\n    }\n\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 && renderer.getStream() == sampleStream\n            && renderer.hasReadStreamToEnd()) {\n          renderer.setCurrentStreamFinal();\n        }\n      }\n      return;\n    }\n\n    // Advance the reading period if necessary.\n    if (readingPeriodHolder.getNext() == null) {\n      // We don't have a successor to advance the reading period to.\n      return;\n    }\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;\n      }\n    }\n\n    if (!readingPeriodHolder.getNext().prepared) {\n      // The successor is not prepared yet.\n      maybeThrowPeriodPrepareError();\n      return;\n    }\n\n    TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();\n    readingPeriodHolder = queue.advanceReadingPeriod();\n    TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();\n\n    boolean initialDiscontinuity =\n        readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET;\n    for (int i = 0; i < renderers.length; i++) {\n      Renderer renderer = renderers[i];\n      boolean rendererWasEnabled = oldTrackSelectorResult.isRendererEnabled(i);\n      if (!rendererWasEnabled) {\n        // The renderer was disabled and will be enabled when we play the next period.\n      } else if (initialDiscontinuity) {\n        // The new period starts with a discontinuity, so the renderer will play out all data then\n        // be disabled and re-enabled when it starts playing the next period.\n        renderer.setCurrentStreamFinal();\n      } else if (!renderer.isCurrentStreamFinal()) {\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(formats, 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 maybeUpdateLoadingPeriod() throws 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        MediaPeriod mediaPeriod =\n            queue.enqueueNextMediaPeriod(\n                rendererCapabilities,\n                trackSelector,\n                loadControl.getAllocator(),\n                mediaSource,\n                info);\n        mediaPeriod.prepare(this, info.startPositionUs);\n        setIsLoading(true);\n        handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);\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 (!queue.hasPlayingPeriod()) {\n      // This is the first prepared period, so start playing it.\n      MediaPeriodHolder playingPeriodHolder = queue.advancePlayingPeriod();\n      resetRendererPosition(playingPeriodHolder.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(PlaybackParameters playbackParameters)\n      throws ExoPlaybackException {\n    eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).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    MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();\n    long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs();\n    if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {\n      setIsLoading(false);\n      return;\n    }\n    long bufferedDurationUs =\n        getTotalBufferedDurationUs(/* bufferedPositionInLoadingPeriodUs= */ nextLoadPositionUs);\n    boolean continueLoading =\n        loadControl.shouldContinueLoading(\n            bufferedDurationUs, mediaClock.getPlaybackParameters().speed);\n    setIsLoading(continueLoading);\n    if (continueLoading) {\n      loadingPeriodHolder.continueLoading(rendererPositionUs);\n    }\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(rendererConfiguration, formats,\n          playingPeriodHolder.sampleStreams[rendererIndex], rendererPositionUs,\n          joining, playingPeriodHolder.getRendererOffset());\n      mediaClock.onRendererEnabled(renderer);\n      // Start the renderer if playing.\n      if (playing) {\n        renderer.start();\n      }\n    }\n  }\n\n  private boolean rendererWaitingForNextStream(Renderer renderer) {\n    MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();\n    MediaPeriodHolder nextPeriodHolder = readingPeriodHolder.getNext();\n    return nextPeriodHolder != null && nextPeriodHolder.prepared && renderer.hasReadStreamToEnd();\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 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    public @Nullable 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(@NonNull 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    public final Object manifest;\n\n    public MediaSourceRefreshInfo(MediaSource source, Timeline timeline, Object manifest) {\n      this.source = source;\n      this.timeline = timeline;\n      this.manifest = manifest;\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-amzn-2.10.6/library/core/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.10.6\";\n\n  /** The version of the library expressed as {@code \"AmznExoPlayerLib/\" + VERSION}. */\n  // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.\n  public static final String VERSION_SLASHY = \"AmznExoPlayerLib/2.10.6\";\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 = 2010006;\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-amzn-2.10.6/library/core/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.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  public final @Nullable String id;\n  /** The human readable label, or null if unknown or not applicable. */\n  public final @Nullable 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  public final @Nullable String codecs;\n  /** Metadata, or null if unknown or not applicable. */\n  public final @Nullable Metadata metadata;\n\n  // Container specific.\n\n  /** The mime type of the container, or null if unknown or not applicable. */\n  public final @Nullable 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  public final @Nullable 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  public final @Nullable 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  public final @Nullable byte[] projectionData;\n  /** The color metadata associated with the video, helps with accurate color reproduction. */\n  public final @Nullable 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  public final @Nullable String language;\n  /**\n   * The Accessibility channel, or {@link #NO_VALUE} if not known or applicable.\n   */\n  public final int accessibilityChannel;\n\n  // MOD: Sabr specific\n  public final boolean isDrc;\n  public final long lastModified;\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      String sampleMimeType,\n      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      String sampleMimeType,\n      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 createVideoContainerFormat(\n        id,\n        label,\n        containerMimeType,\n        sampleMimeType,\n        codecs,\n        metadata,\n        bitrate,\n        width,\n        height,\n        frameRate,\n        initializationData,\n        selectionFlags,\n        roleFlags,\n        /* lastModified */ NO_VALUE);\n  }\n\n  public static Format createVideoContainerFormat(\n      @Nullable String id,\n      @Nullable String label,\n      @Nullable String containerMimeType,\n      String sampleMimeType,\n      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      long lastModified) {\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        /* isDrc */ false,\n        lastModified);\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      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  }\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 createAudioContainerFormat(\n        id,\n        label,\n        containerMimeType,\n        sampleMimeType,\n        codecs,\n        metadata,\n        bitrate,\n        channelCount,\n        sampleRate,\n        initializationData,\n        selectionFlags,\n        roleFlags,\n        language,\n        /* isDrc */ false,\n        /* lastModified */ NO_VALUE);\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      boolean isDrc,\n      long lastModified) {\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        isDrc,\n        lastModified);\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          @Nullable List<byte[]> initializationData,\n          @Nullable DrmInitData drmInitData,\n          @C.SelectionFlags int selectionFlags,\n          @Nullable String language,\n          boolean isDrc) {\n    return createAudioSampleFormat(\n            id,\n            sampleMimeType,\n            codecs,\n            bitrate,\n            maxInputSize,\n            channelCount,\n            sampleRate,\n            /* pcmEncoding= */ NO_VALUE,\n            /* encoderDelay= */ NO_VALUE,\n            /* encoderPadding= */ NO_VALUE,\n            initializationData,\n            drmInitData,\n            selectionFlags,\n            language,\n            /* metadata= */ null,\n            isDrc);\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 createAudioSampleFormat(\n            id,\n            sampleMimeType,\n            codecs,\n            bitrate,\n            maxInputSize,\n            channelCount,\n            sampleRate,\n            pcmEncoding,\n            encoderDelay,\n            encoderPadding,\n            initializationData,\n            drmInitData,\n            selectionFlags,\n            language,\n            metadata,\n            false);\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      boolean isDrc) {\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        isDrc,\n        NO_VALUE);\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  }\n\n  public static Format createTextSampleFormat(\n      @Nullable String id,\n      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      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      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  }\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  }\n\n  // Generic.\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  }\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  }\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  }\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    this(\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      /* isDrc */ false,\n      /* lastModified */ NO_VALUE);\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      boolean isDrc,\n      long lastModified) {\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    // MOD: Sabr specific\n    this.isDrc = isDrc;\n    this.lastModified = lastModified;\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    // Sabr specific\n    this.isDrc = false;\n    this.lastModified = in.readLong();\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  }\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  }\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  }\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      float frameRate, // MOD: add fps to hls format info\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  }\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  }\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  }\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  }\n\n  public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) {\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  }\n\n  public Format copyWithMetadata(@Nullable Metadata metadata) {\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  }\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  }\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  }\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      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(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    // MOD: Sabr specific\n    dest.writeLong(lastModified);\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-amzn-2.10.6/library/core/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.DecryptionResource;\n\n/**\n * Holds a {@link Format}.\n */\npublic final class FormatHolder {\n\n  /**\n   * Whether the object expected to populate {@link #format} is also expected to populate {@link\n   * #decryptionResource}.\n   */\n  // TODO: Remove once all Renderers and MediaSources have migrated to the new DRM model [Internal\n  // ref: b/129764794].\n  public boolean decryptionResourceIsProvided;\n\n  /** An accompanying context for decrypting samples in the format. */\n  @Nullable public DecryptionResource<?> decryptionResource;\n\n  /** The held {@link Format}. */\n  @Nullable public Format format;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  @Nullable private TrackGroupArray trackGroups;\n  @Nullable 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   */\n  public MediaPeriodHolder(\n      RendererCapabilities[] rendererCapabilities,\n      long rendererPositionOffsetUs,\n      TrackSelector trackSelector,\n      Allocator allocator,\n      MediaSource mediaSource,\n      MediaPeriodInfo info) {\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    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 =\n        Assertions.checkNotNull(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 and returns the new result if the selection changed. Must only be\n   * called if {@link #prepared} is {@code true}.\n   *\n   * @param playbackSpeed The current playback speed.\n   * @param timeline The current {@link Timeline}.\n   * @return The {@link TrackSelectorResult} if the result changed. Or null if nothing changed.\n   * @throws ExoPlaybackException If an error occurs during track selection.\n   */\n  @Nullable\n  public TrackSelectorResult selectTracks(float playbackSpeed, Timeline timeline)\n      throws ExoPlaybackException {\n    TrackSelectorResult selectorResult =\n        trackSelector.selectTracks(rendererCapabilities, getTrackGroups(), info.id, timeline);\n    if (selectorResult.isEquivalent(trackSelectorResult)) {\n      return null;\n    }\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    trackSelectorResult = null;\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  /**\n   * Returns the {@link TrackGroupArray} exposed by this media period. Must only be called if {@link\n   * #prepared} is {@code true}.\n   */\n  public TrackGroupArray getTrackGroups() {\n    return Assertions.checkNotNull(trackGroups);\n  }\n\n  /**\n   * Returns the {@link TrackSelectorResult} which is currently applied. Must only be called if\n   * {@link #prepared} is {@code true}.\n   */\n  public TrackSelectorResult getTrackSelectorResult() {\n    return Assertions.checkNotNull(trackSelectorResult);\n  }\n\n  private void enableTrackSelectionsInResult() {\n    TrackSelectorResult trackSelectorResult = this.trackSelectorResult;\n    if (!isLoadingMediaPeriod() || trackSelectorResult == null) {\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    TrackSelectorResult trackSelectorResult = this.trackSelectorResult;\n    if (!isLoadingMediaPeriod() || trackSelectorResult == null) {\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    TrackSelectorResult trackSelectorResult = Assertions.checkNotNull(this.trackSelectorResult);\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.util.Pair;\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.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  private @Nullable MediaPeriodHolder playing;\n  private @Nullable MediaPeriodHolder reading;\n  private @Nullable MediaPeriodHolder loading;\n  private int length;\n  private @Nullable 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 based on the specified information as the new loading media period,\n   * 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   */\n  public MediaPeriod enqueueNextMediaPeriod(\n      RendererCapabilities[] rendererCapabilities,\n      TrackSelector trackSelector,\n      Allocator allocator,\n      MediaSource mediaSource,\n      MediaPeriodInfo info) {\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    if (loading != null) {\n      Assertions.checkState(hasPlayingPeriod());\n      loading.setNext(newPeriodHolder);\n    }\n    oldFrontPeriodUid = null;\n    loading = newPeriodHolder;\n    length++;\n    return newPeriodHolder.mediaPeriod;\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  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 or hasn't started playing.\n   */\n  public MediaPeriodHolder getPlayingPeriod() {\n    return playing;\n  }\n\n  /**\n   * Returns the reading period holder, or null if the queue is empty or the player hasn't started\n   * reading.\n   */\n  public MediaPeriodHolder getReadingPeriod() {\n    return reading;\n  }\n\n  /**\n   * Returns the period holder in the front of the queue which is the playing period holder when\n   * playing, or null if the queue is empty.\n   */\n  public MediaPeriodHolder getFrontPeriod() {\n    return hasPlayingPeriod() ? playing : loading;\n  }\n\n  /** Returns whether the reading and playing period holders are set. */\n  public boolean hasPlayingPeriod() {\n    return playing != null;\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. If the playing period holder is unset, set it to the\n   * item in the front of the queue.\n   *\n   * @return The updated playing period holder, or null if the queue is or becomes empty.\n   */\n  public MediaPeriodHolder advancePlayingPeriod() {\n    if (playing != null) {\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    } else {\n      playing = loading;\n      reading = loading;\n    }\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 = getFrontPeriod();\n    if (front != null) {\n      oldFrontPeriodUid = keepFrontPeriodUid ? front.uid : null;\n      oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber;\n      front.release();\n      removeAfter(front);\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 = getFrontPeriod();\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 = getFrontPeriod();\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 = getFrontPeriod();\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    return nextWindowSequenceNumber++;\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 = getFrontPeriod();\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 || !hasPlayingPeriod();\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-amzn-2.10.6/library/core/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;\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  private RendererConfiguration configuration;\n  private int index;\n  private int state;\n  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  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  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  public int supportsFormat(Format format) throws ExoPlaybackException {\n    return FORMAT_UNSUPPORTED_TYPE;\n  }\n\n  @Override\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.\n   */\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 current manifest. */\n  public final @Nullable Object manifest;\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  public final int playbackState;\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        /* manifest= */ null,\n        DUMMY_MEDIA_PERIOD_ID,\n        startPositionUs,\n        /* contentPositionUs= */ C.TIME_UNSET,\n        Player.STATE_IDLE,\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 manifest See {@link #manifest}.\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      @Nullable Object manifest,\n      MediaPeriodId periodId,\n      long startPositionUs,\n      long contentPositionUs,\n      int playbackState,\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.manifest = manifest;\n    this.periodId = periodId;\n    this.startPositionUs = startPositionUs;\n    this.contentPositionUs = contentPositionUs;\n    this.playbackState = playbackState;\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   * @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) {\n    if (timeline.isEmpty()) {\n      return DUMMY_MEDIA_PERIOD_ID;\n    }\n    int firstPeriodIndex =\n        timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window)\n            .firstPeriodIndex;\n    return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex));\n  }\n\n  /**\n   * Copies playback info and resets playing and loading position.\n   *\n   * @param periodId New playing and loading {@link MediaPeriodId}.\n   * @param startPositionUs New start position. See {@link #startPositionUs}.\n   * @param contentPositionUs New content position. See {@link #contentPositionUs}. Value is ignored\n   *     if {@code periodId.isAd()} is true.\n   * @return Copied playback info with reset position.\n   */\n  @CheckResult\n  public PlaybackInfo resetToNewPosition(\n      MediaPeriodId periodId, long startPositionUs, long contentPositionUs) {\n    return new PlaybackInfo(\n        timeline,\n        manifest,\n        periodId,\n        startPositionUs,\n        periodId.isAd() ? contentPositionUs : C.TIME_UNSET,\n        playbackState,\n        isLoading,\n        trackGroups,\n        trackSelectorResult,\n        periodId,\n        startPositionUs,\n        /* totalBufferedDurationUs= */ 0,\n        startPositionUs);\n  }\n\n  /**\n   * Copied 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        manifest,\n        periodId,\n        positionUs,\n        periodId.isAd() ? contentPositionUs : C.TIME_UNSET,\n        playbackState,\n        isLoading,\n        trackGroups,\n        trackSelectorResult,\n        loadingMediaPeriodId,\n        bufferedPositionUs,\n        totalBufferedDurationUs,\n        positionUs);\n  }\n\n  /**\n   * Copies playback info with new timeline and manifest.\n   *\n   * @param timeline New timeline. See {@link #timeline}.\n   * @param manifest New manifest. See {@link #manifest}.\n   * @return Copied playback info with new timeline and manifest.\n   */\n  @CheckResult\n  public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) {\n    return new PlaybackInfo(\n        timeline,\n        manifest,\n        periodId,\n        startPositionUs,\n        contentPositionUs,\n        playbackState,\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        manifest,\n        periodId,\n        startPositionUs,\n        contentPositionUs,\n        playbackState,\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        manifest,\n        periodId,\n        startPositionUs,\n        contentPositionUs,\n        playbackState,\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        manifest,\n        periodId,\n        startPositionUs,\n        contentPositionUs,\n        playbackState,\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        manifest,\n        periodId,\n        startPositionUs,\n        contentPositionUs,\n        playbackState,\n        isLoading,\n        trackGroups,\n        trackSelectorResult,\n        loadingMediaPeriodId,\n        bufferedPositionUs,\n        totalBufferedDurationUs,\n        positionUs);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\nimport android.view.SurfaceView;\nimport android.view.TextureView;\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.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(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(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(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(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(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(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(TextureView textureView);\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 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     */\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 One of the {@code STATE} constants.\n     */\n    default void onPlayerStateChanged(boolean playWhenReady, 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, Object, int)} is called in this\n     * 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    @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, Object, int)} instead. */\n    @Deprecated\n    public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) {\n      // Do nothing.\n    }\n  }\n\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 and/or manifest changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED},\n   * {@link #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  /**\n   * Timeline and manifest changed as a result of a player initialization with new media.\n   */\n  int TIMELINE_CHANGE_REASON_PREPARED = 0;\n  /**\n   * Timeline and manifest changed as a result of a player reset.\n   */\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 state of the player.\n   *\n   * @return One of the {@code STATE} constants defined in this interface.\n   */\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 Player.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 A 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   * <p>\n   * Playback parameters changes may cause the player to buffer.\n   * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever\n   * the currently active playback parameters change. When that listener is called, the parameters\n   * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch\n   * may be out of range, in which case they are constrained to a set of permitted values. If it is\n   * not possible to change the playback parameters, the listener will not be invoked.\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 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-amzn-2.10.6/library/core/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  private @Nullable 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  public @Nullable 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-amzn-2.10.6/library/core/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 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\n   * {@link MediaClock} from this method.\n   *\n   * @return The {@link MediaClock} tracking the playback position of the renderer, or null.\n   */\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  /**\n   * Returns the {@link SampleStream} being consumed, or null if the renderer is disabled.\n   */\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-amzn-2.10.6/library/core/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 com.google.android.exoplayer2.util.MimeTypes;\n\n/**\n * Defines the capabilities of a {@link Renderer}.\n */\npublic interface RendererCapabilities {\n\n  /**\n   * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of\n   * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM},\n   * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}.\n   */\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   * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of\n   * {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}.\n   */\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   * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of\n   * {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}.\n   */\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   * 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. The returned value is\n   * the bitwise OR of three properties:\n   * <ul>\n   * <li>The level of support for the format itself. One of {@link #FORMAT_HANDLED},\n   * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM},\n   * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}.</li>\n   * <li>The level of support for adapting from the format to another format of the same mime type.\n   * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and\n   * {@link #ADAPTIVE_NOT_SUPPORTED}. Only set if the level of support for the format itself is\n   * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}.</li>\n   * <li>The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and\n   * {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of support for the format itself is\n   * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}.</li>\n   * </ul>\n   * The individual properties can be retrieved by performing a bitwise AND with\n   * {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and\n   * {@link #TUNNELING_SUPPORT_MASK} respectively.\n   *\n   * @param format The format.\n   * @return The extent to which the renderer is capable of supporting the given format.\n   * @throws ExoPlaybackException If an error occurs.\n   */\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 extent to which the renderer supports adapting between supported formats that have\n   *     different mime types. One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and\n   *     {@link #ADAPTIVE_NOT_SUPPORTED}.\n   * @throws ExoPlaybackException If an error occurs.\n   */\n  int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException;\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\nimport android.view.SurfaceView;\nimport android.view.TextureView;\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.AudioFocusManager;\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.TrackSelectionArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelector;\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.PriorityTaskManager;\nimport com.google.android.exoplayer2.util.Util;\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 ExoPlayerFactory}.\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  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 AudioFocusManager audioFocusManager;\n\n  @Nullable private Format videoFormat;\n  @Nullable private Format audioFormat;\n\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\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 drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance\n   *     will not be used for DRM protected playbacks.\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  protected SimpleExoPlayer(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      BandwidthMeter bandwidthMeter,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      Looper looper) {\n    this(\n        context,\n        renderersFactory,\n        trackSelector,\n        loadControl,\n        drmSessionManager,\n        bandwidthMeter,\n        new AnalyticsCollector.Factory(),\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 analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that\n   *     will collect and forward all player events.\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  protected SimpleExoPlayer(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      BandwidthMeter bandwidthMeter,\n      AnalyticsCollector.Factory analyticsCollectorFactory,\n      Looper looper) {\n    this(\n        context,\n        renderersFactory,\n        trackSelector,\n        loadControl,\n        drmSessionManager,\n        bandwidthMeter,\n        analyticsCollectorFactory,\n        Clock.DEFAULT,\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 analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that\n   *     will 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  protected SimpleExoPlayer(\n      Context context,\n      RenderersFactory renderersFactory,\n      TrackSelector trackSelector,\n      LoadControl loadControl,\n      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n      BandwidthMeter bandwidthMeter,\n      AnalyticsCollector.Factory analyticsCollectorFactory,\n      Clock clock,\n      Looper looper) {\n    this.bandwidthMeter = bandwidthMeter;\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 = analyticsCollectorFactory.createAnalyticsCollector(player, clock);\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    audioFocusManager = new AudioFocusManager(context, componentListener);\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 android.view.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    setVideoSurface(null);\n  }\n\n  @Override\n  public void clearVideoSurface(Surface surface) {\n    verifyApplicationThread();\n    if (surface != null && surface == this.surface) {\n      setVideoSurface(null);\n    }\n  }\n\n  @Override\n  public void setVideoSurface(@Nullable Surface surface) {\n    verifyApplicationThread();\n    removeSurfaceCallbacks();\n    setVideoSurfaceInternal(surface, false);\n    int newSurfaceSize = surface == null ? 0 : C.LENGTH_UNSET;\n    maybeNotifySurfaceSizeChanged(/* width= */ newSurfaceSize, /* height= */ newSurfaceSize);\n  }\n\n  @Override\n  public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) {\n    verifyApplicationThread();\n    removeSurfaceCallbacks();\n    this.surfaceHolder = surfaceHolder;\n    if (surfaceHolder == null) {\n      setVideoSurfaceInternal(null, 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(SurfaceHolder surfaceHolder) {\n    verifyApplicationThread();\n    if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) {\n      setVideoSurfaceHolder(null);\n    }\n  }\n\n  @Override\n  public void setVideoSurfaceView(SurfaceView surfaceView) {\n    setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());\n  }\n\n  @Override\n  public void clearVideoSurfaceView(SurfaceView surfaceView) {\n    clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());\n  }\n\n  @Override\n  public void setVideoTextureView(TextureView textureView) {\n    verifyApplicationThread();\n    removeSurfaceCallbacks();\n    this.textureView = textureView;\n    if (textureView == null) {\n      setVideoSurfaceInternal(null, 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 = textureView.isAvailable() ? textureView.getSurfaceTexture()\n          : 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(TextureView textureView) {\n    verifyApplicationThread();\n    if (textureView != null && textureView == this.textureView) {\n      setVideoTextureView(null);\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 (!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   * <p>\n   * Setting the stream type during playback may introduce a short gap in audio output as the audio\n   * track is recreated. A new audio session id will also be generated.\n   * <p>\n   * Calling this method overwrites any attributes set previously by calling\n   * {@link #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 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(Player.EventListener listener) {\n    verifyApplicationThread();\n    player.addListener(listener);\n  }\n\n  @Override\n  public void removeListener(Player.EventListener listener) {\n    verifyApplicationThread();\n    player.removeListener(listener);\n  }\n\n  @Override\n  public int getPlaybackState() {\n    verifyApplicationThread();\n    return player.getPlaybackState();\n  }\n\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    audioFocusManager.handleStop();\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  }\n\n  @Override\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public void sendMessages(ExoPlayerMessage... messages) {\n    player.sendMessages(messages);\n  }\n\n  @Override\n  public PlayerMessage createMessage(PlayerMessage.Target target) {\n    verifyApplicationThread();\n    return player.createMessage(target);\n  }\n\n  @Override\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public void blockingSendMessages(ExoPlayerMessage... messages) {\n    player.blockingSendMessages(messages);\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  @Nullable\n  public Object getCurrentManifest() {\n    verifyApplicationThread();\n    return player.getCurrentManifest();\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  // 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 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          Player.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(String decoderName, long initializedTimestampMs,\n        long initializationDurationMs) {\n      for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {\n        videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs,\n            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(int width, int height, int unappliedRotationDegrees,\n        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(width, height, unappliedRotationDegrees,\n            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(String decoderName, long initializedTimestampMs,\n        long initializationDurationMs) {\n      for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {\n        audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs,\n            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(int bufferSize, long bufferSizeMs,\n        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(null, 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), 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(null, 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    // 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}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.util.Pair;\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 * <p>\n * A timeline consists of related {@link Period}s and {@link Window}s. A period defines a single\n * logical piece of media, for example a media file. It may also define groups of ads inserted into\n * the media, along with information about whether those ads have been loaded and played. A window\n * spans one or more periods, defining the region within those periods that's currently available\n * for playback along with additional information such as whether seeking is supported within the\n * window. Each window defines a default position, which is the position from which playback will\n * start when the player starts playing the window. The following examples illustrate timelines for\n * various use cases.\n *\n * <h3 id=\"single-file\">Single media file or on-demand stream</h3>\n * <p align=\"center\">\n *   <img src=\"doc-files/timeline-single-file.svg\" alt=\"Example timeline for a single file\">\n * </p>\n * A timeline for a single media file or on-demand stream consists of a single period and window.\n * The window spans the whole period, indicating that all parts of the media are available for\n * playback. The window's default position is typically at the start of the period (indicated by the\n * black dot in the figure above).\n *\n * <h3>Playlist of media files or on-demand streams</h3>\n * <p align=\"center\">\n *   <img src=\"doc-files/timeline-playlist.svg\" alt=\"Example timeline for a playlist of files\">\n * </p>\n * A timeline for a playlist of media files or on-demand streams consists of multiple periods, each\n * with its own window. Each window spans the whole of the corresponding period, and typically has a\n * default position at the start of the period. The properties of the periods and windows (e.g.\n * their durations and whether the window is seekable) will often only become known when the player\n * starts buffering the corresponding file or stream.\n *\n * <h3 id=\"live-limited\">Live stream with limited availability</h3>\n * <p align=\"center\">\n *   <img src=\"doc-files/timeline-live-limited.svg\" alt=\"Example timeline for a live stream with\n *       limited availability\">\n * </p>\n * A timeline for a live stream consists of a period whose duration is unknown, since it's\n * continually extending as more content is broadcast. If content only remains available for a\n * limited period of time then the window may start at a non-zero position, defining the region of\n * content that can still be played. The window will have {@link Window#isDynamic} set to true if\n * the stream is still live. Its default position is typically near to the live edge (indicated by\n * the black dot in the figure above).\n *\n * <h3>Live stream with indefinite availability</h3>\n * <p align=\"center\">\n *   <img src=\"doc-files/timeline-live-indefinite.svg\" alt=\"Example timeline for a live stream with\n *       indefinite availability\">\n * </p>\n * A timeline for a live stream with indefinite availability is similar to the\n * <a href=\"#live-limited\">Live stream with limited availability</a> case, except that the window\n * starts at the beginning of the period to indicate that all of the previously broadcast content\n * can still be played.\n *\n * <h3 id=\"live-multi-period\">Live stream with multiple periods</h3>\n * <p align=\"center\">\n *   <img src=\"doc-files/timeline-live-multi-period.svg\" alt=\"Example timeline for a live stream\n *       with multiple periods\">\n * </p>\n * This case arises when a live stream is explicitly divided into separate periods, for example at\n * content boundaries. This case is similar to the <a href=\"#live-limited\">Live stream with limited\n * availability</a> case, except that the window may span more than one period. Multiple periods are\n * also possible in the indefinite availability case.\n *\n * <h3>On-demand stream followed by live stream</h3>\n * <p align=\"center\">\n *   <img src=\"doc-files/timeline-advanced.svg\" alt=\"Example timeline for an on-demand stream\n *       followed by a live stream\">\n * </p>\n * This case is the concatenation of the <a href=\"#single-file\">Single media file or on-demand\n * stream</a> and <a href=\"#multi-period\">Live stream with multiple periods</a> cases. When playback\n * of the on-demand stream ends, playback of the live stream will start from its default position\n * near the live edge.\n *\n * <h3 id=\"single-file-midrolls\">On-demand stream with mid-roll ads</h3>\n * <p align=\"center\">\n *   <img src=\"doc-files/timeline-single-file-midrolls.svg\" alt=\"Example timeline for an on-demand\n *       stream with mid-roll ad groups\">\n * </p>\n * This case includes mid-roll ad groups, which are defined as part of the timeline's single period.\n * The period can be queried for information 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 defines a region of media\n   * currently available for playback along with additional information such as whether seeking is\n   * supported within the window. The figure below shows some of the information defined by a\n   * window, as well as how this information relates to corresponding {@link Period}s in the\n   * timeline.\n   * <p align=\"center\">\n   *   <img src=\"doc-files/timeline-window.svg\" alt=\"Information defined by a timeline window\">\n   * </p>\n   */\n  public static final class Window {\n\n    /** A tag for the window. Not necessarily unique. */\n    @Nullable public Object tag;\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     * The index of the first period that belongs to this window.\n     */\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    /** Sets the data held by this window. */\n    public Window set(\n        @Nullable Object tag,\n        long presentationStartTimeMs,\n        long windowStartTimeMs,\n        boolean isSeekable,\n        boolean isDynamic,\n        long defaultPositionUs,\n        long durationUs,\n        int firstPeriodIndex,\n        int lastPeriodIndex,\n        long positionInFirstPeriodUs) {\n      this.tag = tag;\n      this.presentationStartTimeMs = presentationStartTimeMs;\n      this.windowStartTimeMs = windowStartTimeMs;\n      this.isSeekable = isSeekable;\n      this.isDynamic = isDynamic;\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.\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(\n            int windowIndex, Window window, boolean setTag, 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. Does not populate\n   * {@link Window#tag}.\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, false);\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 setTag Whether {@link Window#tag} should be populated. If false, the field will be set\n   *     to null. The caller should pass false for efficiency reasons unless the field is required.\n   * @return The populated {@link Window}, for convenience.\n   */\n  public final Window getWindow(int windowIndex, Window window, boolean setTag) {\n    return getWindow(windowIndex, window, setTag, 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 setTag Whether {@link Window#tag} should be populated. If false, the field will be set\n   *     to null. The caller should pass false for efficiency reasons unless the field is required.\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, boolean setTag, 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, false, 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 {@code id}, or {@link C#INDEX_UNSET}\n   * 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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.view.Surface;\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  /** Factory for an analytics collector. */\n  public static class Factory {\n\n    /**\n     * Creates an analytics collector for the specified player.\n     *\n     * @param player The {@link Player} for which data will be collected. Can be null, if the player\n     *     is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics\n     *     collector.\n     * @param clock A {@link Clock} used to generate timestamps.\n     * @return An analytics collector.\n     */\n    public AnalyticsCollector createAnalyticsCollector(@Nullable Player player, Clock clock) {\n      return new AnalyticsCollector(player, clock);\n    }\n  }\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 for the specified player.\n   *\n   * @param player The {@link Player} for which data will be collected. Can be null, if the player\n   *     is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics\n   *     collector.\n   * @param clock A {@link Clock} used to generate timestamps.\n   */\n  protected AnalyticsCollector(@Nullable Player player, Clock clock) {\n    if (player != null) {\n      this.player = player;\n    }\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(\n      Timeline timeline, @Nullable Object manifest, @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, 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 =\n        error.type == ExoPlaybackException.TYPE_SOURCE\n            ? generateLoadingMediaPeriodEventTime()\n            : generatePlayingMediaPeriodEventTime();\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    private @Nullable MediaPeriodInfo lastReportedPlayingMediaPeriod;\n    private @Nullable 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    public @Nullable 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    public @Nullable 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    public @Nullable 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    public @Nullable 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    public @Nullable 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    public @Nullable 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      updateLastReportedPlayingMediaPeriod();\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      updateLastReportedPlayingMediaPeriod();\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      updateLastReportedPlayingMediaPeriod();\n    }\n\n    /** Updates the queue with a newly created media period. */\n    public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {\n      boolean isInTimeline = timeline.getIndexOfPeriod(mediaPeriodId.periodUid) != C.INDEX_UNSET;\n      MediaPeriodInfo mediaPeriodInfo =\n          new MediaPeriodInfo(mediaPeriodId, isInTimeline ? timeline : Timeline.EMPTY, windowIndex);\n      mediaPeriodInfoQueue.add(mediaPeriodInfo);\n      mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo);\n      if (mediaPeriodInfoQueue.size() == 1 && !timeline.isEmpty()) {\n        updateLastReportedPlayingMediaPeriod();\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      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 void updateLastReportedPlayingMediaPeriod() {\n      if (!mediaPeriodInfoQueue.isEmpty()) {\n        lastReportedPlayingMediaPeriod = mediaPeriodInfoQueue.get(0);\n      }\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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.view.Surface;\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    public final @Nullable 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 One of the {@link Player}.STATE constants.\n   */\n  default void onPlayerStateChanged(\n      EventTime eventTime, boolean playWhenReady, 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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, 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, 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-amzn-2.10.6/library/core/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 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, 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-amzn-2.10.6/library/core/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;\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\n    /**\n     * Creates a new builder for {@link AudioAttributes}.\n     * <p>\n     * By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is\n     * {@link C#USAGE_MEDIA}, and no flags are set.\n     */\n    public Builder() {\n      contentType = C.CONTENT_TYPE_UNKNOWN;\n      flags = 0;\n      usage = C.USAGE_MEDIA;\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    /**\n     * Creates an {@link AudioAttributes} instance from this builder.\n     */\n    public AudioAttributes build() {\n      return new AudioAttributes(contentType, flags, usage);\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\n  private @Nullable android.media.AudioAttributes audioAttributesV21;\n\n  private AudioAttributes(@C.AudioContentType int contentType, @C.AudioFlags int flags,\n      @C.AudioUsage int usage) {\n    this.contentType = contentType;\n    this.flags = flags;\n    this.usage = usage;\n  }\n\n  @TargetApi(21)\n  public android.media.AudioAttributes getAudioAttributesV21() {\n    if (audioAttributesV21 == null) {\n      audioAttributesV21 = new android.media.AudioAttributes.Builder()\n          .setContentType(contentType)\n          .setFlags(flags)\n          .setUsage(usage)\n          .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 && this.flags == other.flags\n        && this.usage == other.usage;\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    return result;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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.ContentResolver;\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  public static final AudioCapabilities EXTERNAL_SURROUND_SOUND_CAPABILITIES = // AMZN_CHANGE_ONELINE\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  // AMZN_CHANGE_BEGIN\n   /** For Optical output, we read this global setting to detect if dolby\n     * output is enabled. If USE_EXTERNAL_SURROUND_SOUND_FLAG is not set, then\n     * we fallback on the HDMI audio intent.\n     */\n public static final String USE_EXTERNAL_SURROUND_SOUND_FLAG = \"use_external_surround_sound_flag\";\n  // AMZN_CHANGE_END\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\n    // AMZN_CHANGE_BEGIN\n    boolean useSurroundSoundFlag = false;\n    boolean isSurroundSoundEnabled = false;\n\n    // read global surround sound amazon specific settings\n    if (Util.SDK_INT >= 17) {\n        ContentResolver resolver = context.getContentResolver();\n        useSurroundSoundFlag = useSurroundSoundFlagV17(resolver);\n        isSurroundSoundEnabled = isSurroundSoundEnabledV17(resolver);\n    }\n    //  If use surround sound enabled flag is set, then ignore the hmdi plug\n    //  encodings.  Rely only on EXTERNAL_SURROUND_SOUND_CAPABILITIES to\n    //  determine if dolby is supported.\n    if (useSurroundSoundFlag) {\n        return isSurroundSoundEnabled ? EXTERNAL_SURROUND_SOUND_CAPABILITIES :\n                DEFAULT_AUDIO_CAPABILITIES;\n    }\n    // AMZN_CHANGE_END\n\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  // AMZN_CHANGE_BEGIN\n  public static boolean useSurroundSoundFlagV17(ContentResolver\n        resolver) {\n    return Global.getInt(resolver, USE_EXTERNAL_SURROUND_SOUND_FLAG,\n            0) == 1;\n  }\n  @TargetApi(17)\n  public static boolean isSurroundSoundEnabledV17(ContentResolver resolver) {\n    return Global.getInt(resolver, EXTERNAL_SURROUND_SOUND_KEY, 0) == 1;\n  }\n  // AMZN_CHANGE_END\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 android.media.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 android.media.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-amzn-2.10.6/library/core/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  // AMZN_CHANGE_BEGIN\n  private final ContentResolver resolver;\n  // AMZN_CHANGE_END\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    // TODO: revisit\n    handler = new Handler(Util.getLooper());\n    Uri externalSurroundSoundUri = AudioCapabilities.getExternalSurroundSoundGlobalSettingUri();\n    externalSurroundSoundSettingObserver =\n        externalSurroundSoundUri != null\n            ? new ExternalSurroundSoundSettingObserver(\n                handler, context.getContentResolver(), externalSurroundSoundUri)\n            : null;\n    // AMZN_CHANGE_BEGIN\n    boolean useSurroundSoundFlag = false;\n    if (Util.SDK_INT >= 17) {\n      this.resolver = context.getContentResolver();\n      useSurroundSoundFlag = AudioCapabilities.useSurroundSoundFlagV17(\n            resolver);\n    } else {\n      this.resolver = null;\n    }\n    // Don't listen for audio plug encodings if useSurroundSoundFlag is set.\n    // If useSurroundSoundFlag is set then the platform controls what the\n    // audio output is by using the iSurroundSoundEnabled setting.\n    this.receiver = (Util.SDK_INT >= 21 && !useSurroundSoundFlag) ?\n            new HdmiAudioPlugBroadcastReceiver() : null;\n    // AMZN_CHANGE_END\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      super.onChange(selfChange);\n      onNewAudioCapabilities(AudioCapabilities.getCapabilities(context));\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/src/main/java/com/google/android/exoplayer2/audio/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.audio;\n\nimport android.content.Context;\nimport android.media.AudioFocusRequest;\nimport android.media.AudioManager;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RequiresApi;\nimport androidx.annotation.VisibleForTesting;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.Player;\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. */\npublic 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  private @Nullable AudioAttributes audioAttributes;\n\n  private @AudioFocusState int audioFocusState;\n  private int focusGain;\n  private float volumeMultiplier = 1.0f;\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 playerControl A {@link PlayerControl} to handle commands from this instance.\n   */\n  public AudioFocusManager(Context context, PlayerControl playerControl) {\n    this.audioManager =\n        (AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE);\n    this.playerControl = playerControl;\n    this.focusListener = new AudioFocusListener();\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  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  // Internal audio focus listener.\n\n  private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener {\n    @Override\n    public void onAudioFocusChange(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}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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>Call {@link #configure(int, int, int)} to configure the processor to receive input audio, then\n * call {@link #isActive()} to determine whether the processor is active in the new configuration.\n * {@link #queueInput(ByteBuffer)}, {@link #getOutputChannelCount()}, {@link #getOutputEncoding()}\n * and {@link #getOutputSampleRateHz()} may only be called if the processor is active. Call {@link\n * #reset()} to reset the processor to its unconfigured state and release any resources.\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  /** Exception thrown when a processor can't be configured for a given input audio format. */\n  final class UnhandledFormatException extends Exception {\n\n    public UnhandledFormatException(\n        int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) {\n      super(\"Unhandled format: \" + sampleRateHz + \" Hz, \" + channelCount + \" channels in encoding \"\n          + encoding);\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.\n   *\n   * <p>If the audio processor is active after configuration, call {@link #getOutputSampleRateHz()},\n   * {@link #getOutputChannelCount()} and {@link #getOutputEncoding()} to get its new output format.\n   *\n   * <p>If this method returns {@code true}, it is necessary to {@link #flush()} the processor\n   * before queueing more data, but you can (optionally) first drain output in the previous\n   * configuration by calling {@link #queueEndOfStream()} and {@link #getOutput()}. If this method\n   * returns {@code false}, it is safe to queue new input immediately.\n   *\n   * @param sampleRateHz The sample rate of input audio in Hz.\n   * @param channelCount The number of interleaved channels in input audio.\n   * @param encoding The encoding of input audio.\n   * @return Whether the processor must be {@link #flush() flushed} before queueing more input.\n   * @throws UnhandledFormatException Thrown if the specified format can't be handled as input.\n   */\n  boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding)\n      throws UnhandledFormatException;\n\n  /** Returns whether the processor is configured and will process input buffers. */\n  boolean isActive();\n\n  /**\n   * Returns the number of audio channels in the data output by the processor. The value may change\n   * as a result of calling {@link #configure(int, int, int)}.\n   */\n  int getOutputChannelCount();\n\n  /**\n   * Returns the audio encoding used in the data output by the processor. The value may change as a\n   * result of calling {@link #configure(int, int, int)}.\n   */\n  @C.PcmEncoding\n  int getOutputEncoding();\n\n  /**\n   * Returns the sample rate of audio output by the processor, in hertz. The value may change as a\n   * result of calling {@link #configure(int, int, int)}.\n   */\n  int getOutputSampleRateHz();\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. */\n  void reset();\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 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 (listener != null) {\n        handler.post(() -> 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 (listener != null) {\n        handler.post(\n            () ->\n                listener.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 (listener != null) {\n        handler.post(() -> 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 (listener != null) {\n        handler.post(\n            () -> listener.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 (listener != null) {\n        handler.post(\n            () -> {\n              counters.ensureUpdated();\n              listener.onAudioDisabled(counters);\n            });\n      }\n    }\n\n    /**\n     * Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}.\n     */\n    public void audioSessionId(final int audioSessionId) {\n      if (listener != null) {\n        handler.post(() -> listener.onAudioSessionId(audioSessionId));\n      }\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 and returns the active playback parameters, which may\n   * differ from those passed in.\n   *\n   * @param playbackParameters The new playback parameters to attempt to set.\n   * @return The active playback parameters.\n   */\n  PlaybackParameters 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-amzn-2.10.6/library/core/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  private final @Nullable 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  // AMZN_CHANGE_BEGIN\n  public boolean maybePollTimestamp(long systemTimeUs) {\n    return maybePollTimestamp(systemTimeUs, false);\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, boolean applyDolbyPassthroughQuirk) {\n    if (!applyDolbyPassthroughQuirk\n            && (audioTimestamp == null\n                || (systemTimeUs - lastTimestampSampleTimeUs) < sampleIntervalUs)) {\n      return false;\n    }\n    // AMZN_CHANGE_END\n    lastTimestampSampleTimeUs = systemTimeUs;\n    boolean updatedTimestamp = audioTimestamp.maybeUpdateTimestamp();\n    switch (state) {\n      case STATE_INITIALIZING:\n        if (updatedTimestamp) {\n          if (audioTimestamp.getTimestampSystemTimeUs() >= initializeSystemTimeUs\n                  || applyDolbyPassthroughQuirk) {// AMZN_CHANGE_ONELINE\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-amzn-2.10.6/library/core/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 android.util.Log;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.AmazonQuirks;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Logger;\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, boolean)} 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  private static final String TAG = AudioTrackPositionTracker.class.getSimpleName();\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  private @Nullable AudioTrack audioTrack;\n  private int outputPcmFrameSize;\n  private int bufferSize;\n\n  // AMZN_CHANGE_BEGIN\n  private boolean applyDolbyPassThroughQuirk;\n  private boolean isLatencyQuirkEnabled;\n  private long resumeSystemTimeUs;\n  private final Logger log = new Logger(Logger.Module.Audio, TAG);\n  private final boolean DBG = log.allowDebug();\n  private final boolean VDBG = log.allowVerbose(); \n  // AMZN_CHANGE_END\n\n  private @Nullable 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  private @Nullable 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                                   boolean isLatencyQuirkEnabled) { // AMZN_CHANGE_ONELINE\n    this.listener = Assertions.checkNotNull(listener);\n    if (Util.SDK_INT >= 18) {\n      try {\n        getLatencyMethod = AudioTrack.class.getMethod(\"getLatency\", (Class<?>[]) null);\n      } catch (Throwable e) { //AMZN_CHANGE_ONELINE: Some legacy devices throw unexpected errors\n        // There's no guarantee this method exists. Do nothing.\n      }\n    }\n    playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];\n    this.isLatencyQuirkEnabled = isLatencyQuirkEnabled; // AMZN_CHANGE_ONELINE\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      boolean applyDolbyPassThroughQuirk) { // AMZN_CHANGE_ONELINE\n    this.audioTrack = audioTrack;\n    this.outputPcmFrameSize = outputPcmFrameSize;\n    this.bufferSize = bufferSize;\n    this.applyDolbyPassThroughQuirk = applyDolbyPassThroughQuirk; // AMZN_CHANGE_ONELINE\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    // AMZN_CHANGE_BEGIN\n    // for dolby passthrough case, we don't need to sync sample\n    // params because we don't depend on play head position for timestamp\n    if (Assertions.checkNotNull(this.audioTrack).getPlayState() == PLAYSTATE_PLAYING && !applyDolbyPassThroughQuirk) {\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    // for dolby passthrough case, we just depend on getTimeStamp API\n    // for audio video synchronization.\n    if (applyDolbyPassThroughQuirk) {\n      long positionUs;\n      boolean audioTimestampSet = audioTimestampPoller.maybePollTimestamp(systemTimeUs, true);\n      if (audioTimestampSet) {\n        positionUs = audioTimestampPoller.getTimestampSystemTimeUs();\n      } else {\n        positionUs = 0;\n      }\n      if (VDBG) {\n        log.v(\"getCurrentPositionUs : applyDolbyPassThroughQuirk positionUs = \"  + positionUs);\n      }\n      return positionUs;\n    } else if (audioTimestampPoller.hasTimestamp()) { // AMZN_CHANGE_END\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        if (VDBG) {\n          log.v(\"getCurrentPositionUs : hasTimestamp: not advancing: positionUs = \"  + timestampPositionUs);\n        }\n        return timestampPositionUs;\n      }\n      // AMZN_CHANGE_BEGIN\n      long timestampSysTimeUs = audioTimestampPoller.getTimestampSystemTimeUs();\n      long elapsedSinceTimestampUs = systemTimeUs - timestampSysTimeUs ;\n      long positionUs = timestampPositionUs + elapsedSinceTimestampUs;\n      if (VDBG) {\n        log.v(\"getCurrentPositionUs : hasTimestamp: positionUs = \"  + positionUs +\n            \" timestampPositionFrames = \" + timestampPositionFrames +\n            \" timestampPositionUs = \" + timestampPositionUs +\n            \" elapsedSinceTimestampUs = \" + elapsedSinceTimestampUs +\n            \" systemTimeUs = \" + systemTimeUs +\n            \" timestampSysTimeUs  = \" + timestampSysTimeUs);\n      }\n      return positionUs;\n      // AMZN_CHANGE_END\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        if (VDBG) {\n          log.v(\"getCurrentPositionUs : pre-latency adjustment positionUs = \"  + positionUs);\n        }\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        if (VDBG) {\n          log.v(\"getCurrentPositionUs : pre-latency adjustment positionUs = \"  + positionUs +\n              \" smoothedPlayheadOffsetUs = \" + smoothedPlayheadOffsetUs +\n              \" systemTimeUs = \" + systemTimeUs);\n        }\n      }\n      if (!sourceEnded) {\n        positionUs -= latencyUs;\n      }\n      if (VDBG) { \n        log.v(\"getCurrentPositionUs : post-latency adjustment positionUs = \"  + positionUs +\n            \" latencyUs = \" + latencyUs);\n      }\n\n      return positionUs;\n    }\n  }\n\n  /** Starts position tracking. Must be called immediately before {@link AudioTrack#play()}. */\n  public void start() {\n    if (DBG) { \n      log.d(\"start\");\n    }\n    Assertions.checkNotNull(audioTimestampPoller).reset();\n    resumeSystemTimeUs = System.nanoTime() / 1000; // AMZN_CHANGE_ONELINE\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 && !applyDolbyPassThroughQuirk) {// AMZN_CHANGE_ONELINE\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) {// AMZN_CHANGE_ONELINE\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    // AMZN_CHANGE_BEGIN\n    boolean hasPendingData = applyDolbyPassThroughQuirk\n            || writtenFrames > getPlaybackHeadPosition()\n            || forceHasPendingData();\n    if (VDBG) {\n      log.v(\"hasPendingData = \" + hasPendingData);\n    }\n    return hasPendingData;\n    // AMZN_CHANGE_END\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    if (DBG) {\n      log.d(\"pause\");\n    }\n \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, boolean)} is no longer in use.\n   */\n  public void reset() {\n    if (DBG) {\n      log.d(\"reset\");\n    }\n\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    // AMZN_CHANGE_BEGIN\n    if (isLatencyQuirkEnabled) {\n      latencyUs = AmazonQuirks.getAudioHWLatency();\n    } else\n    // AMZN_CHANGE_END\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    // AMZN_CHANGE_BEGIN\n    boolean hasPendingPassthroughData = needsPassthroughWorkarounds\n        && Assertions.checkNotNull(audioTrack).getPlayState() == AudioTrack.PLAYSTATE_PAUSED\n        && getPlaybackHeadPosition() == 0;\n    if (hasPendingPassthroughData) {\n      return true;\n    }\n\n    boolean hasPendingDataQuirk = AmazonQuirks.isLatencyQuirkEnabled()\n            && ( Assertions.checkNotNull(audioTrack).getPlayState() == AudioTrack.PLAYSTATE_PLAYING )\n            && ( ((System.nanoTime() / 1000) - resumeSystemTimeUs) < C.MICROS_PER_SECOND );\n\n    return hasPendingDataQuirk;\n    //AMZN_CHANGE_END\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    // AMZN_CHANGE_BEGIN\n    long rawPlaybackHeadPosition = 0;\n    if (isLatencyQuirkEnabled) {\n      int php = audioTrack.getPlaybackHeadPosition();\n      if (VDBG) {\n        log.v(\"php = \" + php );\n      }\n\n      // if audio track includes latency while returning play head position\n      // we try to compensate it back by adding the latency back to it,\n      // if the track is in playing state or if pause state and php is non-zero\n      int trackState = audioTrack.getPlayState();\n      if (trackState == PLAYSTATE_PLAYING ||\n              (trackState == PLAYSTATE_PAUSED && php != 0)) {\n        php += getAudioSWLatencies();\n      }\n      if (php < 0 && ((System.nanoTime() / 1000) - resumeSystemTimeUs) < C.MICROS_PER_SECOND) {\n        php = 0;\n        log.i(\"php is negative during latency stabilization phase ...resetting to 0\");\n      }\n      rawPlaybackHeadPosition = 0xFFFFFFFFL & php;\n    } else {\n      // AMZN_CHANGE_END\n      rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();\n      if (VDBG) {\n        log.v(\"rawPlaybackHeadPosition = \" + rawPlaybackHeadPosition);\n      }\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\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    // AMZN_CHANGE_BEGIN\n    if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition\n            && lastRawPlaybackHeadPosition > 0x7FFFFFFFL\n            && (lastRawPlaybackHeadPosition - rawPlaybackHeadPosition >= 0x7FFFFFFFL)) {\n      // The value must have wrapped around.\n      log.i(\"The playback head position wrapped around\");\n      rawPlaybackHeadWrapCount++;\n    }\n    // AMZN_CHANGE_END\n    lastRawPlaybackHeadPosition = rawPlaybackHeadPosition;\n    return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32);\n  }\n\n  private int getAudioSWLatencies() {\n    if (getLatencyMethod == null) {\n      return 0;\n    }\n\n    try {\n      Integer swLatencyMs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null);\n      return swLatencyMs * (outputSampleRate / 1000);\n    } catch (Exception e) {\n      return 0;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 android.media.AudioTrack#attachAuxEffect(int)\n   */\n  public final int effectId;\n  /**\n   * The send level for the effect.\n   *\n   * @see android.media.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-amzn-2.10.6/library/core/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 com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\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.\n */\npublic abstract class BaseAudioProcessor implements AudioProcessor {\n\n  /** The configured input sample rate, in Hertz, or {@link Format#NO_VALUE} if not configured. */\n  protected int sampleRateHz;\n  /** The configured input channel count, or {@link Format#NO_VALUE} if not configured. */\n  protected int channelCount;\n  /** The configured input encoding, or {@link Format#NO_VALUE} if not configured. */\n  @C.PcmEncoding protected int encoding;\n\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    channelCount = Format.NO_VALUE;\n    sampleRateHz = Format.NO_VALUE;\n    encoding = Format.NO_VALUE;\n  }\n\n  @Override\n  public boolean isActive() {\n    return sampleRateHz != Format.NO_VALUE;\n  }\n\n  @Override\n  public int getOutputChannelCount() {\n    return channelCount;\n  }\n\n  @Override\n  public int getOutputEncoding() {\n    return encoding;\n  }\n\n  @Override\n  public int getOutputSampleRateHz() {\n    return sampleRateHz;\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    onFlush();\n  }\n\n  @Override\n  public final void reset() {\n    flush();\n    buffer = EMPTY_BUFFER;\n    sampleRateHz = Format.NO_VALUE;\n    channelCount = Format.NO_VALUE;\n    encoding = Format.NO_VALUE;\n    onReset();\n  }\n\n  /** Sets the input format of this processor, returning whether the input format has changed. */\n  protected final boolean setInputFormat(\n      int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) {\n    if (sampleRateHz == this.sampleRateHz\n        && channelCount == this.channelCount\n        && encoding == this.encoding) {\n      return false;\n    }\n    this.sampleRateHz = sampleRateHz;\n    this.channelCount = channelCount;\n    this.encoding = encoding;\n    return true;\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 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-amzn-2.10.6/library/core/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;\nimport java.util.Arrays;\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/* package */ final class ChannelMappingAudioProcessor extends BaseAudioProcessor {\n\n  @Nullable private int[] pendingOutputChannels;\n\n  private boolean active;\n  @Nullable private int[] outputChannels;\n\n  /**\n   * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)}\n   * to 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 boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding)\n      throws UnhandledFormatException {\n    boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels);\n    outputChannels = pendingOutputChannels;\n\n    int[] outputChannels = this.outputChannels;\n    if (outputChannels == null) {\n      active = false;\n      return outputChannelsChanged;\n    }\n    if (encoding != C.ENCODING_PCM_16BIT) {\n      throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);\n    }\n    if (!outputChannelsChanged && !setInputFormat(sampleRateHz, channelCount, encoding)) {\n      return false;\n    }\n\n    active = channelCount != outputChannels.length;\n    for (int i = 0; i < outputChannels.length; i++) {\n      int channelIndex = outputChannels[i];\n      if (channelIndex >= channelCount) {\n        throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);\n      }\n      active |= (channelIndex != i);\n    }\n    return true;\n  }\n\n  @Override\n  public boolean isActive() {\n    return active;\n  }\n\n  @Override\n  public int getOutputChannelCount() {\n    return outputChannels == null ? channelCount : outputChannels.length;\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) / (2 * channelCount);\n    int outputSize = frameCount * outputChannels.length * 2;\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 += channelCount * 2;\n    }\n    inputBuffer.position(limit);\n    buffer.flip();\n  }\n\n  @Override\n  protected void onReset() {\n    outputChannels = null;\n    pendingOutputChannels = null;\n    active = false;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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.Format;\nimport com.google.android.exoplayer2.util.AmazonQuirks;\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 com.google.android.exoplayer2.util.Logger;\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\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()}). */\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  // AMZN_CHANGE_BEGIN\n  private final Logger log = new Logger(Logger.Module.Audio, TAG);\n  private final boolean DBG = log.allowDebug();\n  private final boolean VDBG = log.allowVerbose();\n  private static final boolean isLatencyQuirkEnabled = AmazonQuirks.isLatencyQuirkEnabled();\n  private static final boolean isLegacyPassthroughQuirkEnabled = AmazonQuirks.isDolbyPassthroughQuirkEnabled();\n  // AMZN_CHANGE_END\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                                      isLatencyQuirkEnabled); // AMZN_CHANGE_ONELINE\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    // AMZN_CHANGE_BEGIN\n    log.i(\"Amazon quirks:\"\n            + \" Latency:\" + (isLatencyQuirkEnabled ? \"on\" : \"off\")\n            + \"; Dolby\" + (isLegacyPassthroughQuirkEnabled ? \"on\" : \"off\")\n            + \". On Sdk: \" + Util.SDK_INT);\n    // AMZN_CHANGE_END\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  // AMZN_CHANGE_BEGIN\n  // This API is called from MediaCodecAudioTrackRenderer to skip\n  // calling hasPendingData  to detect if the playback has ended or not since these APIs\n  // always return true and fake the buffering state of audio track.\n  // there is no way for us to depend on the audio track states to decide\n  // if the playback has ended or not.\n  public boolean applyDolbyPassthroughQuirk() {\n    return (!configuration.isInputPcm && isLegacyPassthroughQuirkEnabled);\n  }\n  // AMZN_CHANGE_END\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    boolean flushAudioProcessors = false;\n    if (processingEnabled) {\n      trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames);\n      channelMappingAudioProcessor.setChannelMap(outputChannels);\n      for (AudioProcessor audioProcessor : availableAudioProcessors) {\n        try {\n          flushAudioProcessors |= audioProcessor.configure(sampleRate, channelCount, encoding);\n        } catch (AudioProcessor.UnhandledFormatException e) {\n          throw new ConfigurationException(e);\n        }\n        if (audioProcessor.isActive()) {\n          channelCount = audioProcessor.getOutputChannelCount();\n          sampleRate = audioProcessor.getOutputSampleRateHz();\n          encoding = audioProcessor.getOutputEncoding();\n        }\n      }\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 we have a pending configuration already, we always drain audio processors as the preceding\n    // configuration may have required it (even if this one doesn't).\n    boolean drainAudioProcessors = flushAudioProcessors || this.pendingConfiguration != null;\n    if (isInitialized()\n        && (!pendingConfiguration.canReuseAudioTrack(configuration) || drainAudioProcessors)) {\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       applyDolbyPassthroughQuirk()); // AMZN_CHANGE_ONELINE\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        log.i(\"Setting StartMediaTimeUs = \" + startMediaTimeUs);\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.w(\"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 (DBG) {\n      log.d(\"writeBuffer : offset = \" + buffer.position() + \", limit = \" + buffer.limit() +\n              \", presentationTimeUs = \" + avSyncPresentationTimeUs);\n    }\n    if (!buffer.hasRemaining()) {\n      return;\n    }\n    if (outputBuffer != null) {\n      Assertions.checkArgument(outputBuffer == buffer);\n    } else {\n      outputBuffer = buffer;\n      // AMZN: we need to copy data to temp buffer in case of dolby passthrough also\n      // irrespective of SDK version.\n      if (Util.SDK_INT < 21 || applyDolbyPassthroughQuirk()) { // AMZN_CHANGE_ONELINE\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\n    // AMZN_CHANGE_BEGIN\n    // for dolby passthrough case, just write into the DolbyPassthroughAudioTrack\n    // since its implementation is different than standard pcm audio track.\n    // The DolbyPassthroughAudioTrack takes care of writing only in play state\n    // and also writes into the track asynchronously. Also, we\n    // cannot depend on playback head position to decide how much more data to write.\n    if (applyDolbyPassthroughQuirk()) {\n      // if there are no free buffers in AudioTrack, the write returns 0, indicating\n      // it did not consume the buffer.\n      bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesRemaining);\n      if (bytesWritten > 0) {\n        preV21OutputBufferOffset += bytesWritten;\n        buffer.position(buffer.position() + bytesWritten);\n      }\n    } else if (Util.SDK_INT < 21) { // isInputPcm  == true\n      // AMZN_CHANGE_END\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  /**\n   * Plays out remaining audio. {@link #isEnded()} will return {@code true} when playback has ended.\n   *\n   * @throws WriteException If an error occurs draining data to the track.\n   */\n  @Override\n  public void playToEndOfStream() throws WriteException {\n    log.i(\"calling playToEndOfStream\");\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 &&\n            (applyDolbyPassthroughQuirk() || !hasPendingData()));// AMZN_CHANGE_ONELINE\n  }\n\n  @Override\n  public boolean hasPendingData() {\n    return isInitialized() && audioTrackPositionTracker.hasPendingData(getWrittenFrames());\n  }\n\n  @Override\n  public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {\n    if (configuration != null && !configuration.canApplyPlaybackParameters) {\n      this.playbackParameters = PlaybackParameters.DEFAULT;\n      return this.playbackParameters;\n    }\n    PlaybackParameters lastSetPlaybackParameters =\n        afterDrainPlaybackParameters != null\n            ? afterDrainPlaybackParameters\n            : !playbackParametersCheckpoints.isEmpty()\n                ? playbackParametersCheckpoints.getLast().playbackParameters\n                : this.playbackParameters;\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    return this.playbackParameters;\n  }\n\n  @Override\n  public PlaybackParameters getPlaybackParameters() {\n    return 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    log.i(\"calling setAudioSessionId = \" + 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    log.i(\"calling enableTunnelingV21 = \" + 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      log.i(\"setVolume: 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    log.i(\"calling pause\");\n    playing = false;\n    if (isInitialized() && audioTrackPositionTracker.pause()) {\n      audioTrack.pause();\n    }\n  }\n\n  @Override\n  public void flush() {\n    log.i(\"calling flush/reset\");\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            log.i(\"audioTrack.flush\");\n            toRelease.flush();\n            log.i(\"audioTrack.release\");\n            toRelease.release();\n          } finally {\n            releasingConditionVariable.open();\n          }\n        }\n      }.start();\n    }\n  }\n\n  @Override\n  public void reset() {\n    log.i(\"calling 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        log.i(\"audioTrack.release\");\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  @SuppressWarnings(\"deprecation\")\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      // The audio processors have drained, so drain the underlying audio track.\n      // AMZN_CHANGE_BEGIN\n      if (!applyDolbyPassthroughQuirk()) {\n        audioTrackPositionTracker.handleEndOfStream(getWrittenFrames());\n      }\n      // AMZN_CHANGE_END\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(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(message);\n    }\n\n    @Override\n    public void onInvalidLatency(long latencyUs) {\n      log.w(\"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 boolean applyDolbyPassthroughQuirk() {\n        return (!isInputPcm && isLegacyPassthroughQuirkEnabled);\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        // AMZN_CHANGE_BEGIN\n        int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage);\n        if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {\n          if (applyDolbyPassthroughQuirk()) {\n            audioTrack = new DolbyPassthroughAudioTrack(\n                            streamType,\n                            outputSampleRate,\n                            outputChannelConfig,\n                            outputEncoding,\n                            bufferSize,\n                            MODE_STREAM);\n          } else {\n            audioTrack =\n                    new AudioTrack(\n                            streamType,\n                            outputSampleRate,\n                            outputChannelConfig,\n                            outputEncoding,\n                            bufferSize,\n                            MODE_STREAM);\n          }\n        } else {\n          // Re-attach to the same audio session.\n          if (applyDolbyPassthroughQuirk()) {\n            audioTrack = new DolbyPassthroughAudioTrack(\n                            streamType,\n                            outputSampleRate,\n                            outputChannelConfig,\n                            outputEncoding,\n                            bufferSize,\n                            MODE_STREAM, audioSessionId);\n          } else {\n            audioTrack =\n                    new AudioTrack(\n                            streamType,\n                            outputSampleRate,\n                            outputChannelConfig,\n                            outputEncoding,\n                            bufferSize,\n                            MODE_STREAM,\n                            audioSessionId);\n          }\n        }\n        // AMZN_CHANGE_END\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\n     // AMZN_CHANGE_BEGIN\n     audioSessionId = audioSessionId != C.AUDIO_SESSION_ID_UNSET ? audioSessionId\n         : AudioManager.AUDIO_SESSION_ID_GENERATE;\n     if (applyDolbyPassthroughQuirk()) {\n       return new DolbyPassthroughAudioTrack(attributes, format, bufferSize, MODE_STREAM,\n               audioSessionId);\n     } else {\n       return new AudioTrack(attributes, format, bufferSize, MODE_STREAM,\n               audioSessionId);\n \n     }\n     // AMZN_CHANGE_END\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-amzn-2.10.6/library/core/src/main/java/com/google/android/exoplayer2/audio/DolbyPassthroughAudioTrack.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.audio;\n\nimport android.media.*;\nimport com.google.android.exoplayer2.util.Logger;\nimport android.os.ConditionVariable;\nimport android.os.HandlerThread;\nimport android.os.Handler;\nimport android.os.Message;\n\nimport java.util.concurrent.Semaphore;\n\n/**\n * This class extends to an {@link android.media.AudioTrack} and handles\n * asynchronous writes to underlying DirectTrack implementation on Fire TV\n * family of devices to support Dolby Pass through playback.\n * APIs needs to be called from single thread.\n */\n\npublic final class DolbyPassthroughAudioTrack extends android.media.AudioTrack {\n\n  private final String TAG = DolbyPassthroughAudioTrack.class.getSimpleName();\n\n  // handle thread related\n  private HandlerThread trackHandlerThread = null;\n  private static final String TRACK_HANDLER_THREAD_NAME = \"dolbyTrackHandlerThread\";\n  private Handler trackHandler = null;\n  private ConditionVariable trackHandlerGate = null;\n\n  // handler messages\n  private static final int MSG_WRITE_TO_TRACK = 1;\n  private static final int MSG_PAUSE_TRACK = 2;\n  private static final int MSG_PLAY_TRACK = 3;\n  private static final int MSG_FLUSH_TRACK = 4;\n  private static final int MSG_STOP_TRACK = 5;\n  private static final int MSG_RELEASE_TRACK = 6;\n\n  // required for handling buffers\n  // we allocate fixed number of buffers and cycle through them\n  private static final int BUFFER_COUNT = 2;\n  // Counting Semaphore for tracking ping/pong buffers\n  private Semaphore pendingWriteSem = null;\n  private byte[][] audioBuffer = null;\n  // Next free buffer to be used to copy incoming writes\n  private int nextBufferIndex = 0;\n  private final Logger log = new Logger(Logger.Module.Audio, TAG);\n\n  public DolbyPassthroughAudioTrack(android.media.AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,\n                    int mode, int sessionId) {\n    super(attributes, format, bufferSizeInBytes, mode, sessionId);\n    initialize();\n  }\n  public DolbyPassthroughAudioTrack(int streamType, int sampleRateInHz,\n                                    int channelConfig, int audioFormat,\n                                    int bufferSizeInBytes, int mode)\n          throws IllegalArgumentException {\n    this(streamType, sampleRateInHz, channelConfig, audioFormat,\n            bufferSizeInBytes, mode, 0);\n  }\n\n  public DolbyPassthroughAudioTrack(int streamType, int sampleRateInHz,\n                                    int channelConfig, int audioFormat,\n                                    int bufferSizeInBytes, int mode, int sessionId)\n          throws IllegalArgumentException {\n    super(streamType, sampleRateInHz, channelConfig, audioFormat,\n            bufferSizeInBytes, mode, sessionId);\n    initialize();\n  }\n\n  private void initialize() {\n    log.i(\"initialize\" );\n    trackHandlerGate = new ConditionVariable(true);\n    trackHandlerThread = new HandlerThread(TRACK_HANDLER_THREAD_NAME);\n    pendingWriteSem = new Semaphore(BUFFER_COUNT);\n    audioBuffer = new byte[BUFFER_COUNT][];\n\n    trackHandlerThread.start();\n    /**\n     * This handler thread serializes all the base audio track APIs.\n     */\n    trackHandler = new Handler(trackHandlerThread.getLooper()) {\n      public void handleMessage(Message msg) {\n        switch(msg.what) {\n          case MSG_WRITE_TO_TRACK: {\n            int size = msg.arg1;\n            int bufferIndex = msg.arg2;\n            if (log.allowVerbose()) {\n              log.v(\"writing to track : size = \" + size +\n                                         \", bufferIndex = \" + bufferIndex);\n            }\n            DolbyPassthroughAudioTrack.super.write( audioBuffer[ bufferIndex ], 0, size );\n            if (log.allowVerbose()) {\n              log.v(\"writing to  track  done\");\n            }\n            pendingWriteSem.release();\n            break;\n          }\n          case MSG_PAUSE_TRACK : {\n            log.i(\"pausing track\");\n            DolbyPassthroughAudioTrack.super.pause();\n            trackHandlerGate.open();\n            break;\n          }\n          case MSG_PLAY_TRACK : {\n            log.i(\"playing track\");\n            DolbyPassthroughAudioTrack.super.play();\n            trackHandlerGate.open();\n            break;\n          }\n          case MSG_FLUSH_TRACK : {\n            log.i(\"flushing track\");\n            DolbyPassthroughAudioTrack.super.flush();\n            trackHandlerGate.open();\n            break;\n          }\n          case MSG_STOP_TRACK : {\n            log.i(\"stopping track\");\n            DolbyPassthroughAudioTrack.super.stop();\n            trackHandlerGate.open();\n            break;\n          }\n          case MSG_RELEASE_TRACK : {\n            log.i(\"releasing track\");\n            if (DolbyPassthroughAudioTrack.super.getPlayState() != PLAYSTATE_STOPPED) {\n              log.i(\"not in stopped state...stopping\");\n              DolbyPassthroughAudioTrack.super.stop();\n            }\n            DolbyPassthroughAudioTrack.super.release();\n            trackHandlerGate.open();\n            break;\n          }\n          default: {\n            log.w(\"unknown message..ignoring!!!\");\n            break;\n          }\n        }\n      }\n    };\n  }\n\n  /**\n   * Play will block until previous messages to handler Thread\n   * are executed.\n   * We need to serialize play, write, pause and release because otherwise,\n   * base audio track  will misbehave.\n   */\n  @Override\n  public void play() throws IllegalStateException {\n    log.i(\"play\");\n    trackHandlerGate.close();\n    Message msg = trackHandler.obtainMessage(MSG_PLAY_TRACK);\n    if (log.allowDebug()) {\n      log.d(\"Sending play to DirectTrack handler thread\");\n    }\n    trackHandler.sendMessage(msg);\n    trackHandlerGate.block();\n    if (log.allowDebug()) {\n      log.d(\"DirectTrack Play done\");\n    }\n  }\n\n  /**\n   * Pause will block until previous (possibly write) messages to handler Thread\n   * are executed.\n   * We need to serialize play, write and pause because otherwise,\n   * base audio track  will misbehave.\n   */\n  @Override\n  public void pause() throws IllegalStateException {\n    log.i(\"pause\");\n    trackHandlerGate.close();\n    Message msg = trackHandler.obtainMessage(MSG_PAUSE_TRACK);\n    if (log.allowDebug()) {\n      log.d(\"Sending pause DirectTrack handler thread\");\n    }\n    trackHandler.sendMessage(msg);\n    trackHandlerGate.block();\n    if (log.allowDebug()) {\n      log.d(\"Pausing DirectTrack Done\");\n    }\n  }\n\n  /**\n   * FLush will block until previous (possibly write) messages to handler Thread\n   * are executed.\n   */\n  @Override\n  public void flush()\n          throws IllegalStateException {\n    log.i(\"flush\");\n    trackHandlerGate.close();\n    Message msg = trackHandler.obtainMessage(MSG_FLUSH_TRACK);\n    if (log.allowDebug()) {\n      log.d(\"Sending flush DirectTrack handler thread\");\n    }\n    trackHandler.sendMessage(msg);\n    trackHandlerGate.block();\n    if (log.allowDebug()) {\n      log.d(\"Flushing DirectTrack Done\");\n    }\n  }\n\n  /**\n   * Stop will block until previous (possibly write) messages to handler Thread\n   * are executed.\n   */\n  @Override\n  public void stop()\n          throws IllegalStateException {\n    log.i(\"stop\");\n    if (getPlayState() == PLAYSTATE_STOPPED) {\n      log.i(\"already in stopped state\");\n      return;\n    }\n    trackHandlerGate.close();\n    Message msg = trackHandler.obtainMessage(MSG_STOP_TRACK);\n    if (log.allowDebug()) {\n      log.d(\"Sending stop DirectTrack handler thread\");\n    }\n    trackHandler.sendMessage(msg);\n    trackHandlerGate.block();\n    if (log.allowDebug()) {\n      log.d(\"Stopping DirectTrack Done\");\n    }\n  }\n\n  /**\n   * Queues up Write messages to the handler thread. The\n   * writes happen only when the base audio track is in playing state.\n   * We also use {@link DolbyPassthroughAudioTrack#BUFFER_COUNT} number\n   * of buffers in a cyclic manner.\n   */\n  @Override\n  public int write(byte[] audioData, int offsetInBytes, int sizeInBytes) {\n    if (getPlayState() != android.media.AudioTrack.PLAYSTATE_PLAYING ) {\n      return 0;\n    }\n    if (!pendingWriteSem.tryAcquire()) {\n      return 0;\n    }\n    if (audioBuffer[nextBufferIndex] == null ||\n            audioBuffer[nextBufferIndex].length < sizeInBytes) {\n      if (log.allowVerbose()) {\n        log.v(\"Allocating buffer index = \" + nextBufferIndex +\n                                     \", size = \" + sizeInBytes);\n      }\n      audioBuffer[nextBufferIndex] = new byte[sizeInBytes];\n    }\n    System.arraycopy(audioData,offsetInBytes,audioBuffer[nextBufferIndex],0,sizeInBytes);\n    Message msg = trackHandler.obtainMessage(MSG_WRITE_TO_TRACK,\n            sizeInBytes,\n            nextBufferIndex);\n\n    trackHandler.sendMessage(msg);\n    nextBufferIndex = ((nextBufferIndex + 1) % BUFFER_COUNT);\n\n    return sizeInBytes;\n  }\n\n  /**\n   * Release will block until previous messages to handler Thread\n   * are executed.\n   * We need to serialize play, write, pause and release because otherwise,\n   * base audio track  will misbehave.\n   */\n  @Override\n  public void release() {\n    log.i(\"release\");\n    trackHandlerGate.close();\n    Message msg = trackHandler.obtainMessage(MSG_RELEASE_TRACK);\n    if (log.allowDebug()) {\n      log.d(\"Sending release DirectTrack handler thread\");\n    }\n    trackHandler.sendMessage(msg);\n    trackHandlerGate.block();\n\n    trackHandlerThread.quit();\n    trackHandlerThread = null;\n    trackHandler = null;\n    trackHandlerGate = null;\n    pendingWriteSem = null;\n    audioBuffer = null;\n    if (log.allowDebug()) {\n      log.d(\"Release track done\");\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 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, String language, 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-amzn-2.10.6/library/core/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.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 boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding)\n      throws UnhandledFormatException {\n    if (!Util.isEncodingHighResolutionIntegerPcm(encoding)) {\n      throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);\n    }\n    return setInputFormat(sampleRateHz, channelCount, encoding);\n  }\n\n  @Override\n  public boolean isActive() {\n    return Util.isEncodingHighResolutionIntegerPcm(encoding);\n  }\n\n  @Override\n  public int getOutputEncoding() {\n    return C.ENCODING_PCM_FLOAT;\n  }\n\n  @Override\n  public void queueInput(ByteBuffer inputBuffer) {\n    boolean isInput32Bit = 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 {\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-amzn-2.10.6/library/core/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.PlaybackParameters;\nimport com.google.android.exoplayer2.PlayerMessage.Target;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;\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.DecoderQueryException;\nimport com.google.android.exoplayer2.mediacodec.MediaFormatUtil;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.AmazonQuirks;\nimport com.google.android.exoplayer2.util.MediaClock;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Logger;\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 com.google.android.exoplayer2.audio.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 android.media.MediaFormat passthroughMediaFormat;\n  private @C.Encoding int pcmEncoding;\n  private int channelCount;\n  private int encoderDelay;\n  private int encoderPadding;\n  private long currentPositionUs;\n  private boolean allowFirstBufferPositionDiscontinuity;\n  private boolean allowPositionDiscontinuity;\n  private long lastInputTimeUs;\n  private int pendingStreamChangeCount;\n\n  private final Logger log = new Logger(Logger.Module.Audio, TAG);\n\n  /**\n   * @param context A context.\n   * @param mediaCodecSelector A decoder selector.\n   */\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   */\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  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   */\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   */\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   */\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 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   */\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  protected int supportsFormat(MediaCodecSelector mediaCodecSelector,\n      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, Format format)\n      throws DecoderQueryException {\n    String mimeType = format.sampleMimeType;\n    if (!MimeTypes.isAudio(mimeType)) {\n      return FORMAT_UNSUPPORTED_TYPE;\n    }\n    int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;\n    boolean supportsFormatDrm = supportsFormatDrm(drmSessionManager, format.drmInitData);\n    if (supportsFormatDrm\n        && allowPassthrough(format.channelCount, mimeType)\n        && mediaCodecSelector.getPassthroughDecoderInfo() != null) {\n      return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED;\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 FORMAT_UNSUPPORTED_SUBTYPE;\n    }\n    boolean requiresSecureDecryption = false;\n    DrmInitData drmInitData = format.drmInitData;\n    if (drmInitData != null) {\n      for (int i = 0; i < drmInitData.schemeDataCount; i++) {\n        requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption;\n      }\n    }\n    List<MediaCodecInfo> decoderInfos =\n        mediaCodecSelector.getDecoderInfos(\n            format.sampleMimeType, requiresSecureDecryption, /* requiresTunnelingDecoder= */ false);\n    if (decoderInfos.isEmpty()) {\n      return requiresSecureDecryption\n              && !mediaCodecSelector\n                  .getDecoderInfos(\n                      format.sampleMimeType,\n                      /* requiresSecureDecoder= */ false,\n                      /* requiresTunnelingDecoder= */ false)\n                  .isEmpty()\n          ? FORMAT_UNSUPPORTED_DRM\n          : FORMAT_UNSUPPORTED_SUBTYPE;\n    }\n    if (!supportsFormatDrm) {\n      return 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    int adaptiveSupport =\n        isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format)\n            ? ADAPTIVE_SEAMLESS\n            : ADAPTIVE_NOT_SEAMLESS;\n    int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;\n    return adaptiveSupport | tunnelingSupport | formatSupport;\n  }\n\n  @Override\n  protected List<MediaCodecInfo> getDecoderInfos(\n      MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder)\n      throws DecoderQueryException {\n    if (allowPassthrough(format.channelCount, format.sampleMimeType)\n            && AmazonQuirks.useDefaultPassthroughDecoder()) { // AMZN_CHANGE_ONELINE\n      MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo();\n      if (passthroughDecoderInfo != null) {\n        return Collections.singletonList(passthroughDecoderInfo);\n      }\n    }\n    List<MediaCodecInfo> decoderInfos =\n        mediaCodecSelector.getDecoderInfos(\n            format.sampleMimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false);\n    if (MimeTypes.AUDIO_E_AC3_JOC.equals(format.sampleMimeType)) {\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      MediaCrypto crypto,\n      float codecOperatingRate) {\n    codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats());\n    log.setTAG(codecInfo.name + \"-\" + TAG); // AMZN_CHANGE_ONELINE\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 (areCodecConfigurationCompatible(oldFormat, newFormat)) {\n      return KEEP_CODEC_RESULT_YES_WITH_FLUSH;\n    } else {\n      return KEEP_CODEC_RESULT_NO;\n    }\n  }\n\n  @Override\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(Format newFormat) throws ExoPlaybackException {\n    super.onInputFormatChanged(newFormat);\n    eventDispatcher.inputFormatChanged(newFormat);\n    // If the input format is anything other than PCM then we assume that the audio decoder will\n    // output 16-bit PCM.\n    pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding\n        : C.ENCODING_PCM_16BIT;\n    channelCount = newFormat.channelCount;\n    encoderDelay = newFormat.encoderDelay;\n    encoderPadding = newFormat.encoderPadding;\n  }\n\n  @Override\n  protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat)\n      throws ExoPlaybackException {\n    log.i(\"onOutputFormatChanged: outputFormat:\" + outputFormat\n            + \", codec:\" + codec);\n    @C.Encoding int encoding;\n    MediaFormat format;\n    if (passthroughMediaFormat != null) {\n      format = passthroughMediaFormat;\n      encoding =\n          getPassthroughEncoding(\n              format.getInteger(MediaFormat.KEY_CHANNEL_COUNT),\n              format.getString(MediaFormat.KEY_MIME));\n    } else {\n      format = outputFormat;\n      // AMZN_CHANGE_BEGIN\n      // In Amazon Devices, some platform dolby decoders may output mime types depending on the\n      // audio capabilities of the connected device and Dolby settings. So, as a general rule, if\n      // platform decoder is being used instead of OMX.google.raw.decoder, need to\n      // configure audio track based on the output mime type returned by the media codec.\n      encoding = AmazonQuirks.isAmazonDevice() ?\n              MimeTypes.getEncoding(format.getString(MediaFormat.KEY_MIME)) : pcmEncoding;\n      // AMZN_CHANGE_END\n    }\n    int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);\n    int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);\n    int[] channelMap;\n    if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) {\n      channelMap = new int[this.channelCount];\n      for (int i = 0; i < this.channelCount; i++) {\n        channelMap[i] = i;\n      }\n    } else {\n      channelMap = null;\n    }\n\n    try {\n      audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay,\n          encoderPadding);\n    } catch (AudioSink.ConfigurationException e) {\n      throw ExoPlaybackException.createForRenderer(e, getIndex());\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      if (audioSink.supportsOutput(channelCount, 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 PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {\n    return 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    // AMZN_CHANGE_BEGIN\n    if (log.allowDebug()) {\n      log.d(\"processOutputBuffer: positionUs = \" + positionUs +\n              \", elapsedRealtimeUs =  \" + elapsedRealtimeUs +\n              \", bufferIndex = \" + bufferIndex +\n              \", isDecodeOnlyBuffer = \" + isDecodeOnlyBuffer +\n              \", isLastBuffer = \" + isLastBuffer +\n              \", bufferPresentationTimeUs = \" + bufferPresentationTimeUs);\n    }\n    // AMZN_CHANGE_END\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      throw ExoPlaybackException.createForRenderer(e, getIndex());\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      throw ExoPlaybackException.createForRenderer(e, getIndex());\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 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 format.\n   *\n   * @param codecInfo A {@link MediaCodecInfo} describing the decoder.\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 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 whether two {@link Format}s will cause the same codec to be configured in an identical\n   * way, excluding {@link MediaFormat#KEY_MAX_INPUT_SIZE} and configuration that does not come from\n   * the {@link Format}.\n   *\n   * @param oldFormat The first format.\n   * @param newFormat The second format.\n   * @return Whether the two formats will cause a codec to be configured in an identical way,\n   *     excluding {@link MediaFormat#KEY_MAX_INPUT_SIZE} and configuration that does not come from\n   *     the {@link Format}.\n   */\n  protected boolean areCodecConfigurationCompatible(Format oldFormat, Format newFormat) {\n    return Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType)\n        && oldFormat.channelCount == newFormat.channelCount\n        && oldFormat.sampleRate == newFormat.sampleRate\n        && oldFormat.initializationDataEquals(newFormat);\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 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 media format.\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  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-amzn-2.10.6/library/core/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 boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding)\n      throws UnhandledFormatException {\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 UnhandledFormatException(sampleRateHz, channelCount, encoding);\n    }\n    return setInputFormat(sampleRateHz, channelCount, encoding);\n  }\n\n  @Override\n  public boolean isActive() {\n    return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_PCM_16BIT;\n  }\n\n  @Override\n  public int getOutputEncoding() {\n    return C.ENCODING_PCM_16BIT;\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 (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 (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-amzn-2.10.6/library/core/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. Calling this method will discard any data buffered\n   * within the processor, and may update the value returned by {@link #isActive()}.\n   *\n   * @param enabled Whether to skip silence in the input.\n   */\n  public void setEnabled(boolean enabled) {\n    this.enabled = enabled;\n    flush();\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 boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding)\n      throws UnhandledFormatException {\n    if (encoding != C.ENCODING_PCM_16BIT) {\n      throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);\n    }\n    bytesPerFrame = channelCount * 2;\n    return setInputFormat(sampleRateHz, channelCount, encoding);\n  }\n\n  @Override\n  public boolean isActive() {\n    return super.isActive() && 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 (isActive()) {\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 * sampleRateHz) / 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-amzn-2.10.6/library/core/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.Looper;\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.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 com.google.android.exoplayer2.audio.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 FormatHolder formatHolder;\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    formatHolder = new FormatHolder();\n    flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();\n    decoderReinitializationState = REINITIALIZATION_STATE_NONE;\n    audioTrackNeedsConfigure = true;\n  }\n\n  @Override\n  public MediaClock getMediaClock() {\n    return this;\n  }\n\n  @Override\n  public final int supportsFormat(Format format) {\n    if (!MimeTypes.isAudio(format.sampleMimeType)) {\n      return FORMAT_UNSUPPORTED_TYPE;\n    }\n    int formatSupport = supportsFormatInternal(drmSessionManager, format);\n    if (formatSupport <= FORMAT_UNSUPPORTED_DRM) {\n      return formatSupport;\n    }\n    int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;\n    return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;\n  }\n\n  /**\n   * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for {@link\n   * #supportsFormat(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 extent to which the renderer supports the format itself.\n   */\n  protected abstract int supportsFormatInternal(\n      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 ExoPlaybackException.createForRenderer(e, getIndex());\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      flagsOnlyBuffer.clear();\n      int result = readSource(formatHolder, flagsOnlyBuffer, true);\n      if (result == C.RESULT_FORMAT_READ) {\n        onInputFormatChanged(formatHolder.format);\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 ExoPlaybackException.createForRenderer(e, getIndex());\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<DecoderInputBuffer, ? extends SimpleOutputBuffer,\n      ? extends AudioDecoderException> createDecoder(Format format, 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  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    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.format);\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 || (!bufferEncrypted && playClearSamplesWithoutKeys)) {\n      return false;\n    }\n    @DrmSession.State int drmSessionState = decoderDrmSession.getState();\n    if (drmSessionState == DrmSession.STATE_ERROR) {\n      throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());\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      throw ExoPlaybackException.createForRenderer(e, getIndex());\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 PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {\n    return 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 ExoPlaybackException.createForRenderer(e, getIndex());\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<ExoMediaCrypto> previous = sourceDrmSession;\n    sourceDrmSession = session;\n    releaseDrmSessionIfUnused(previous);\n  }\n\n  private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {\n    DrmSession<ExoMediaCrypto> previous = decoderDrmSession;\n    decoderDrmSession = session;\n    releaseDrmSessionIfUnused(previous);\n  }\n\n  private void releaseDrmSessionIfUnused(@Nullable DrmSession<ExoMediaCrypto> session) {\n    if (session != null && session != decoderDrmSession && session != sourceDrmSession) {\n      drmSessionManager.releaseSession(session);\n    }\n  }\n\n  private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {\n    Format oldFormat = inputFormat;\n    inputFormat = newFormat;\n\n    boolean drmInitDataChanged = !Util.areEqual(inputFormat.drmInitData, oldFormat == null ? null\n        : oldFormat.drmInitData);\n    if (drmInitDataChanged) {\n      if (inputFormat.drmInitData != null) {\n        if (drmSessionManager == null) {\n          throw ExoPlaybackException.createForRenderer(\n              new IllegalStateException(\"Media requires a DrmSessionManager\"), getIndex());\n        }\n        DrmSession<ExoMediaCrypto> session =\n            drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);\n        if (session == decoderDrmSession || session == sourceDrmSession) {\n          // We already had this session. The manager must be reference counting, so release it once\n          // to get the count attributed to this renderer back down to 1.\n          drmSessionManager.releaseSession(session);\n        }\n        setSourceDrmSession(session);\n      } else {\n        setSourceDrmSession(null);\n      }\n    }\n\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    encoderDelay = newFormat.encoderDelay;\n    encoderPadding = newFormat.encoderPadding;\n\n    eventDispatcher.inputFormatChanged(newFormat);\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.C.Encoding;\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 channelCount;\n  private int sampleRateHz;\n  private float speed;\n  private float pitch;\n  private int outputSampleRateHz;\n  private int pendingOutputSampleRateHz;\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    channelCount = Format.NO_VALUE;\n    sampleRateHz = Format.NO_VALUE;\n    outputSampleRateHz = Format.NO_VALUE;\n    buffer = EMPTY_BUFFER;\n    shortBuffer = buffer.asShortBuffer();\n    outputBuffer = EMPTY_BUFFER;\n    pendingOutputSampleRateHz = SAMPLE_RATE_NO_CHANGE;\n  }\n\n  /**\n   * Sets the playback speed. Calling this method will discard any data buffered within the\n   * processor, and may update the value returned by {@link #isActive()}.\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    flush();\n    return speed;\n  }\n\n  /**\n   * Sets the playback pitch. Calling this method will discard any data buffered within the\n   * processor, and may update the value returned by {@link #isActive()}.\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    flush();\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\n   * {@link #configure(int, int, int)} to start using the new sample rate.\n   *\n   * @param sampleRateHz The sample rate for output audio, in hertz.\n   * @see #configure(int, int, int)\n   */\n  public void setOutputSampleRateHz(int sampleRateHz) {\n    pendingOutputSampleRateHz = 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 outputSampleRateHz == sampleRateHz\n          ? Util.scaleLargeTimestamp(duration, inputBytes, outputBytes)\n          : Util.scaleLargeTimestamp(duration, inputBytes * outputSampleRateHz,\n              outputBytes * sampleRateHz);\n    } else {\n      return (long) ((double) speed * duration);\n    }\n  }\n\n  @Override\n  public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding)\n      throws UnhandledFormatException {\n    if (encoding != C.ENCODING_PCM_16BIT) {\n      throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);\n    }\n    int outputSampleRateHz = pendingOutputSampleRateHz == SAMPLE_RATE_NO_CHANGE\n        ? sampleRateHz : pendingOutputSampleRateHz;\n    if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount\n        && this.outputSampleRateHz == outputSampleRateHz) {\n      return false;\n    }\n    this.sampleRateHz = sampleRateHz;\n    this.channelCount = channelCount;\n    this.outputSampleRateHz = outputSampleRateHz;\n    pendingSonicRecreation = true;\n    return true;\n  }\n\n  @Override\n  public boolean isActive() {\n    return sampleRateHz != Format.NO_VALUE\n        && (Math.abs(speed - 1f) >= CLOSE_THRESHOLD\n            || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD\n            || outputSampleRateHz != sampleRateHz);\n  }\n\n  @Override\n  public int getOutputChannelCount() {\n    return channelCount;\n  }\n\n  @Override\n  public int getOutputEncoding() {\n    return C.ENCODING_PCM_16BIT;\n  }\n\n  @Override\n  public int getOutputSampleRateHz() {\n    return outputSampleRateHz;\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      if (pendingSonicRecreation) {\n        sonic = new Sonic(sampleRateHz, channelCount, speed, pitch, outputSampleRateHz);\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    channelCount = Format.NO_VALUE;\n    sampleRateHz = Format.NO_VALUE;\n    outputSampleRateHz = Format.NO_VALUE;\n    buffer = EMPTY_BUFFER;\n    shortBuffer = buffer.asShortBuffer();\n    outputBuffer = EMPTY_BUFFER;\n    pendingOutputSampleRateHz = 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-amzn-2.10.6/library/core/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 com.google.android.exoplayer2.audio.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 boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) {\n    return setInputFormat(sampleRateHz, channelCount, encoding);\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 onFlush() {\n    if (isActive()) {\n      audioBufferSink.flush(sampleRateHz, channelCount, 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-amzn-2.10.6/library/core/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 boolean isActive;\n  private int trimStartFrames;\n  private int trimEndFrames;\n  private int bytesPerFrame;\n  private boolean receivedInputSinceConfigure;\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(int, int, int)} 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 boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding)\n      throws UnhandledFormatException {\n    if (encoding != OUTPUT_ENCODING) {\n      throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);\n    }\n    if (endBufferSize > 0) {\n      trimmedFrameCount += endBufferSize / bytesPerFrame;\n    }\n    bytesPerFrame = Util.getPcmFrameSize(OUTPUT_ENCODING, channelCount);\n    endBuffer = new byte[trimEndFrames * bytesPerFrame];\n    endBufferSize = 0;\n    pendingTrimStartBytes = trimStartFrames * bytesPerFrame;\n    boolean wasActive = isActive;\n    isActive = trimStartFrames != 0 || trimEndFrames != 0;\n    receivedInputSinceConfigure = false;\n    setInputFormat(sampleRateHz, channelCount, encoding);\n    return wasActive != isActive;\n  }\n\n  @Override\n  public boolean isActive() {\n    return isActive;\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    receivedInputSinceConfigure = true;\n\n    // Trim any pending start bytes from the input buffer.\n    int trimBytes = Math.min(remaining, pendingTrimStartBytes);\n    trimmedFrameCount += trimBytes / 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  @SuppressWarnings(\"ReferenceEquality\")\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 be\n      // always be called, which clears the end buffer as needed. When audio is actually ending we\n      // play the padding data which is incorrect. This behavior can be fixed once we have the\n      // timestamps associated with input buffers.\n      replaceOutputBuffer(endBufferSize).put(endBuffer, 0, endBufferSize).flip();\n      endBufferSize = 0;\n    }\n    return super.getOutput();\n  }\n\n  @SuppressWarnings(\"ReferenceEquality\")\n  @Override\n  public boolean isEnded() {\n    return super.isEnded() && endBufferSize == 0;\n  }\n\n  @Override\n  protected void onFlush() {\n    if (receivedInputSinceConfigure) {\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-amzn-2.10.6/library/core/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 = Util.getIntegerCodeForString(\"RIFF\");\n  /** Four character code for \"WAVE\". */\n  public static final int WAVE_FOURCC = Util.getIntegerCodeForString(\"WAVE\");\n  /** Four character code for \"fmt \". */\n  public static final int FMT_FOURCC = Util.getIntegerCodeForString(\"fmt \");\n  /** Four character code for \"data\". */\n  public static final int DATA_FOURCC = Util.getIntegerCodeForString(\"data\");\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  /**\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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\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  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  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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 com.google.android.exoplayer2.C;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.nio.ByteBuffer;\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  /**\n   * The buffer's data, or {@code null} if no data has been set.\n   */\n  public ByteBuffer data;\n\n  /**\n   * The time at which the sample should be presented.\n   */\n  public long timeUs;\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   * 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  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.position(0);\n      data.limit(position);\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} 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  }\n\n  @Override\n  public void clear() {\n    super.clear();\n    if (data != null) {\n      data.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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.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  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  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  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  @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-amzn-2.10.6/library/core/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 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  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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionResource.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\n/**\n * A reference-counted resource used in the decryption of media samples.\n *\n * @param <T> The reference type with which to make {@link Owner#onLastReferenceReleased} calls.\n *     Subclasses are expected to pass themselves.\n */\npublic abstract class DecryptionResource<T extends DecryptionResource<T>> {\n\n  /**\n   * Implemented by the class in charge of managing a {@link DecryptionResource resource's}\n   * lifecycle.\n   */\n  public interface Owner<T extends DecryptionResource<T>> {\n\n    /**\n     * Called when the last reference to a {@link DecryptionResource} is {@link #releaseReference()\n     * released}.\n     */\n    void onLastReferenceReleased(T resource);\n  }\n\n  // TODO: Consider adding a handler on which the owner should be called.\n  private final DecryptionResource.Owner<T> owner;\n  private int referenceCount;\n\n  /**\n   * Creates a new instance with reference count zero.\n   *\n   * @param owner The owner of this instance.\n   */\n  public DecryptionResource(Owner<T> owner) {\n    this.owner = owner;\n    referenceCount = 0;\n  }\n\n  /** Increases by one the reference count for this resource. */\n  public void acquireReference() {\n    referenceCount++;\n  }\n\n  /**\n   * Decreases by one the reference count for this resource, and notifies the owner if said count\n   * reached zero as a result of this operation.\n   *\n   * <p>Must only be called as releasing counter-part of {@link #acquireReference()}.\n   */\n  @SuppressWarnings(\"unchecked\")\n  public void releaseReference() {\n    if (--referenceCount == 0) {\n      owner.onLastReferenceReleased((T) this);\n    } else if (referenceCount < 0) {\n      throw new IllegalStateException(\"Illegal release of resource.\");\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.util.Pair;\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.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.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/**\n * A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}.\n */\n@TargetApi(18)\n/* package */ class DefaultDrmSession<T extends ExoMediaCrypto> implements DrmSession<T> {\n\n  /**\n   * Manages provisioning requests.\n   */\n  public interface ProvisioningManager<T extends ExoMediaCrypto> {\n\n    /**\n     * Called when a session requires provisioning. The manager <em>may</em> call\n     * {@link #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    /**\n     * Called by a session when it successfully completes a provisioning operation.\n     */\n    void onProvisionCompleted();\n\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 = 60;\n\n  /** The DRM scheme datas, or null if this session uses offline keys. */\n  public final @Nullable List<SchemeData> schemeDatas;\n\n  private final ExoMediaDrm<T> mediaDrm;\n  private final ProvisioningManager<T> provisioningManager;\n  private final @DefaultDrmSessionManager.Mode int mode;\n  private final @Nullable HashMap<String, String> optionalKeyRequestParameters;\n  private final EventDispatcher<DefaultDrmSessionEventListener> eventDispatcher;\n  private final int initialDrmRequestRetryCount;\n\n  /* package */ final MediaDrmCallback callback;\n  /* package */ final UUID uuid;\n  /* package */ final PostResponseHandler postResponseHandler;\n\n  private @DrmSession.State int state;\n  private int openCount;\n  private HandlerThread requestHandlerThread;\n  private PostRequestHandler postRequestHandler;\n  private @Nullable T mediaCrypto;\n  private @Nullable DrmSessionException lastException;\n  private byte @MonotonicNonNull [] sessionId;\n  private byte @MonotonicNonNull [] offlineLicenseKeySetId;\n\n  private @Nullable KeyRequest currentKeyRequest;\n  private @Nullable 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 schemeDatas DRM scheme datas for this session, or null if an {@code\n   *     offlineLicenseKeySetId} is provided.\n   * @param mode The DRM mode.\n   * @param offlineLicenseKeySetId The offline license key set identifier, or null when not using\n   *     offline keys.\n   * @param optionalKeyRequestParameters The optional 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 initialDrmRequestRetryCount The number of times to retry for initial provisioning and\n   *     key request before reporting error.\n   */\n  public DefaultDrmSession(\n      UUID uuid,\n      ExoMediaDrm<T> mediaDrm,\n      ProvisioningManager<T> provisioningManager,\n      @Nullable List<SchemeData> schemeDatas,\n      @DefaultDrmSessionManager.Mode int mode,\n      @Nullable byte[] offlineLicenseKeySetId,\n      @Nullable HashMap<String, String> optionalKeyRequestParameters,\n      MediaDrmCallback callback,\n      Looper playbackLooper,\n      EventDispatcher<DefaultDrmSessionEventListener> eventDispatcher,\n      int initialDrmRequestRetryCount) {\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.mediaDrm = mediaDrm;\n    this.mode = mode;\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.optionalKeyRequestParameters = optionalKeyRequestParameters;\n    this.callback = callback;\n    this.initialDrmRequestRetryCount = initialDrmRequestRetryCount;\n    this.eventDispatcher = eventDispatcher;\n    state = STATE_OPENING;\n\n    postResponseHandler = new PostResponseHandler(playbackLooper);\n    requestHandlerThread = new HandlerThread(\"DrmRequestHandler\");\n    requestHandlerThread.start();\n    postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());\n  }\n\n  // Life cycle.\n\n  public void acquire() {\n    if (++openCount == 1) {\n      if (state == STATE_ERROR) {\n        return;\n      }\n      if (openInternal(true)) {\n        doLicense(true);\n      }\n    }\n  }\n\n  /** @return True if the session is closed and cleaned up, false otherwise. */\n  // Assigning null to various non-null variables for clean-up. Class won't be used after release.\n  @SuppressWarnings(\"assignment.type.incompatible\")\n  public boolean release() {\n    if (--openCount == 0) {\n      state = STATE_RELEASED;\n      postResponseHandler.removeCallbacksAndMessages(null);\n      postRequestHandler.removeCallbacksAndMessages(null);\n      postRequestHandler = null;\n      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      return true;\n    }\n    return false;\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    postRequestHandler.post(MSG_PROVISION, currentProvisionRequest, /* 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  @DrmSession.State\n  public final int getState() {\n    return state;\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  public @Nullable Map<String, String> queryKeyStatus() {\n    return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId);\n  }\n\n  @Override\n  public @Nullable byte[] getOfflineLicenseKeySetId() {\n    return offlineLicenseKeySetId;\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      eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionAcquired);\n      mediaCrypto = mediaDrm.createMediaCrypto(sessionId);\n      state = STATE_OPENED;\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    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) {\n            Log.d(TAG, \"Offline license has expired or will expire soon. \"\n                + \"Remaining seconds: \" + 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) {\n          postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry);\n        } else {\n          // Renew\n          if (restoreKeys()) {\n            postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry);\n          }\n        }\n        break;\n      case DefaultDrmSessionManager.MODE_RELEASE:\n        Assertions.checkNotNull(offlineLicenseKeySetId);\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 Widevine 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 =\n          mediaDrm.getKeyRequest(scope, schemeDatas, type, optionalKeyRequestParameters);\n      postRequestHandler.post(MSG_KEYS, 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 && offlineLicenseKeySetId != null))\n            && keySetId != null && 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 PostResponseHandler extends Handler {\n\n    public PostResponseHandler(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  }\n\n  @SuppressLint(\"HandlerLeak\")\n  private class PostRequestHandler extends Handler {\n\n    public PostRequestHandler(Looper backgroundLooper) {\n      super(backgroundLooper);\n    }\n\n    void post(int what, Object request, boolean allowRetry) {\n      int allowRetryInt = allowRetry ? 1 : 0;\n      int errorCount = 0;\n      obtainMessage(what, allowRetryInt, errorCount, request).sendToTarget();\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public void handleMessage(Message msg) {\n      Object request = msg.obj;\n      Object response;\n      try {\n        switch (msg.what) {\n          case MSG_PROVISION:\n            response = callback.executeProvisionRequest(uuid, (ProvisionRequest) request);\n            break;\n          case MSG_KEYS:\n            response = callback.executeKeyRequest(uuid, (KeyRequest) request);\n            break;\n          default:\n            throw new RuntimeException();\n        }\n      } catch (Exception e) {\n        if (maybeRetryRequest(msg)) {\n          return;\n        }\n        response = e;\n      }\n      postResponseHandler.obtainMessage(msg.what, Pair.create(request, response)).sendToTarget();\n    }\n\n    private boolean maybeRetryRequest(Message originalMsg) {\n      boolean allowRetry = originalMsg.arg1 == 1;\n      if (!allowRetry) {\n        return false;\n      }\n      int errorCount = originalMsg.arg2 + 1;\n      if (errorCount > initialDrmRequestRetryCount) {\n        return false;\n      }\n      Message retryMsg = Message.obtain(originalMsg);\n      retryMsg.arg2 = errorCount;\n      sendMessageDelayed(retryMsg, getRetryDelayMillis(errorCount));\n      return true;\n    }\n\n    private long getRetryDelayMillis(int errorCount) {\n      return Math.min((errorCount - 1) * 1000, 5000);\n    }\n\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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  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  void onDrmSessionManagerError(Exception error);\n\n  /** Called each time offline keys are restored. */\n  void onDrmKeysRestored();\n\n  /** Called each time offline keys are removed. */\n  void onDrmKeysRemoved();\n\n  /** Called each time a drm session is released. */\n  default void onDrmSessionReleased() {}\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 android.text.TextUtils;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager;\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.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.HashMap;\nimport java.util.List;\nimport java.util.UUID;\n\n/**\n * A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}.\n */\n@TargetApi(18)\npublic class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>,\n    ProvisioningManager<T> {\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   * The key to use when passing CustomData to a PlayReady instance in an optional parameter 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  /**\n   * Restores an offline license to allow its status to be queried.\n   */\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<T> mediaDrm;\n  private final MediaDrmCallback callback;\n  private final @Nullable HashMap<String, String> optionalKeyRequestParameters;\n  private final EventDispatcher<DefaultDrmSessionEventListener> eventDispatcher;\n  private final boolean multiSession;\n  private final int initialDrmRequestRetryCount;\n\n  private final List<DefaultDrmSession<T>> sessions;\n  private final List<DefaultDrmSession<T>> provisioningSessions;\n\n  private @Nullable Looper playbackLooper;\n  private int mode;\n  private @Nullable byte[] offlineLicenseKeySetId;\n\n  /* package */ volatile @Nullable MediaDrmHandler mediaDrmHandler;\n\n  /**\n   * Instantiates a new instance using the Widevine scheme.\n   *\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 ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.\n   * @throws UnsupportedDrmException If the specified DRM scheme is not supported.\n   */\n  public static DefaultDrmSessionManager<FrameworkMediaCrypto> newWidevineInstance(\n      MediaDrmCallback callback, @Nullable HashMap<String, String> optionalKeyRequestParameters)\n      throws UnsupportedDrmException {\n    return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters);\n  }\n\n  /**\n   * Instantiates a new instance using the PlayReady scheme.\n   *\n   * <p>Note that PlayReady is unsupported by most Android devices, with the exception of Android TV\n   * devices, which do provide support.\n   *\n   * @param callback Performs key and provisioning requests.\n   * @param customData Optional custom data to include in requests generated by the instance.\n   * @throws UnsupportedDrmException If the specified DRM scheme is not supported.\n   */\n  public static DefaultDrmSessionManager<FrameworkMediaCrypto> newPlayReadyInstance(\n      MediaDrmCallback callback, @Nullable String customData) throws UnsupportedDrmException {\n    HashMap<String, String> optionalKeyRequestParameters;\n    if (!TextUtils.isEmpty(customData)) {\n      optionalKeyRequestParameters = new HashMap<>();\n      optionalKeyRequestParameters.put(PLAYREADY_CUSTOM_DATA_KEY, customData);\n    } else {\n      optionalKeyRequestParameters = null;\n    }\n    return newFrameworkInstance(C.PLAYREADY_UUID, callback, optionalKeyRequestParameters);\n  }\n\n  /**\n   * Instantiates a new instance.\n   *\n   * @param uuid The UUID of the drm scheme.\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 ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.\n   * @throws UnsupportedDrmException If the specified DRM scheme is not supported.\n   */\n  public static DefaultDrmSessionManager<FrameworkMediaCrypto> newFrameworkInstance(\n      UUID uuid,\n      MediaDrmCallback callback,\n      @Nullable HashMap<String, String> optionalKeyRequestParameters)\n      throws UnsupportedDrmException {\n    return new DefaultDrmSessionManager<>(\n        uuid,\n        FrameworkMediaDrm.newInstance(uuid),\n        callback,\n        optionalKeyRequestParameters,\n        /* multiSession= */ false,\n        INITIAL_DRM_REQUEST_RETRY_COUNT);\n  }\n\n  /**\n   * @param uuid The UUID of the drm scheme.\n   * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager.\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 ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.\n   */\n  public DefaultDrmSessionManager(\n      UUID uuid,\n      ExoMediaDrm<T> mediaDrm,\n      MediaDrmCallback callback,\n      @Nullable HashMap<String, String> optionalKeyRequestParameters) {\n    this(\n        uuid,\n        mediaDrm,\n        callback,\n        optionalKeyRequestParameters,\n        /* multiSession= */ false,\n        INITIAL_DRM_REQUEST_RETRY_COUNT);\n  }\n\n  /**\n   * @param uuid The UUID of the drm scheme.\n   * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager.\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 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   */\n  public DefaultDrmSessionManager(\n      UUID uuid,\n      ExoMediaDrm<T> mediaDrm,\n      MediaDrmCallback callback,\n      @Nullable HashMap<String, String> optionalKeyRequestParameters,\n      boolean multiSession) {\n    this(\n        uuid,\n        mediaDrm,\n        callback,\n        optionalKeyRequestParameters,\n        multiSession,\n        INITIAL_DRM_REQUEST_RETRY_COUNT);\n  }\n\n  /**\n   * @param uuid The UUID of the drm scheme.\n   * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager.\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 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   */\n  public DefaultDrmSessionManager(\n      UUID uuid,\n      ExoMediaDrm<T> mediaDrm,\n      MediaDrmCallback callback,\n      @Nullable HashMap<String, String> optionalKeyRequestParameters,\n      boolean multiSession,\n      int initialDrmRequestRetryCount) {\n    Assertions.checkNotNull(uuid);\n    Assertions.checkNotNull(mediaDrm);\n    Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), \"Use C.CLEARKEY_UUID instead\");\n    this.uuid = uuid;\n    this.mediaDrm = mediaDrm;\n    this.callback = callback;\n    this.optionalKeyRequestParameters = optionalKeyRequestParameters;\n    this.eventDispatcher = new EventDispatcher<>();\n    this.multiSession = multiSession;\n    this.initialDrmRequestRetryCount = initialDrmRequestRetryCount;\n    mode = MODE_PLAYBACK;\n    sessions = new ArrayList<>();\n    provisioningSessions = new ArrayList<>();\n    if (multiSession && C.WIDEVINE_UUID.equals(uuid) && Util.SDK_INT >= 19) {\n      // TODO: Enabling session sharing probably doesn't do anything useful here. It would only be\n      // useful if DefaultDrmSession instances were aware of one another's state, which is not\n      // implemented. Or if custom renderers are being used that allow playback to proceed before\n      // keys, which seems unlikely to be true in practice.\n      mediaDrm.setPropertyString(\"sessionSharing\", \"enable\");\n    }\n    mediaDrm.setOnEventListener(new MediaDrmEventListener());\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   * Provides access to {@link ExoMediaDrm#getPropertyString(String)}.\n   * <p>\n   * This method may be called when the manager is in any state.\n   *\n   * @param key The key to request.\n   * @return The retrieved property.\n   */\n  public final String getPropertyString(String key) {\n    return mediaDrm.getPropertyString(key);\n  }\n\n  /**\n   * Provides access to {@link ExoMediaDrm#setPropertyString(String, String)}.\n   * <p>\n   * This method may be called when the manager is in any state.\n   *\n   * @param key The property to write.\n   * @param value The value to write.\n   */\n  public final void setPropertyString(String key, String value) {\n    mediaDrm.setPropertyString(key, value);\n  }\n\n  /**\n   * Provides access to {@link ExoMediaDrm#getPropertyByteArray(String)}.\n   * <p>\n   * This method may be called when the manager is in any state.\n   *\n   * @param key The key to request.\n   * @return The retrieved property.\n   */\n  public final byte[] getPropertyByteArray(String key) {\n    return mediaDrm.getPropertyByteArray(key);\n  }\n\n  /**\n   * Provides access to {@link ExoMediaDrm#setPropertyByteArray(String, byte[])}.\n   * <p>\n   * This method may be called when the manager is in any state.\n   *\n   * @param key The property to write.\n   * @param value The value to write.\n   */\n  public final void setPropertyByteArray(String key, byte[] value) {\n    mediaDrm.setPropertyByteArray(key, value);\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)} 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 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) || 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  public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {\n    Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper);\n    if (sessions.isEmpty()) {\n      this.playbackLooper = playbackLooper;\n      if (mediaDrmHandler == null) {\n        mediaDrmHandler = new MediaDrmHandler(playbackLooper);\n      }\n    }\n\n    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    DefaultDrmSession<T> session;\n    if (!multiSession) {\n      session = sessions.isEmpty() ? null : sessions.get(0);\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 =\n          new DefaultDrmSession<>(\n              uuid,\n              mediaDrm,\n              this,\n              schemeDatas,\n              mode,\n              offlineLicenseKeySetId,\n              optionalKeyRequestParameters,\n              callback,\n              playbackLooper,\n              eventDispatcher,\n              initialDrmRequestRetryCount);\n      sessions.add(session);\n    }\n    session.acquire();\n    return session;\n  }\n\n  @Override\n  public void releaseSession(DrmSession<T> session) {\n    if (session instanceof ErrorStateDrmSession) {\n      // Do nothing.\n      return;\n    }\n\n    DefaultDrmSession<T> drmSession = (DefaultDrmSession<T>) session;\n    if (drmSession.release()) {\n      sessions.remove(drmSession);\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  // ProvisioningManager implementation.\n\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  // Internal methods.\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 = 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\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\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\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  public final @Nullable 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  public @Nullable 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 Parcelable.Creator<DrmInitData> CREATOR =\n      new Parcelable.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    public final @Nullable 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    public final @Nullable byte[] data;\n    /**\n     * Whether secure decryption is required.\n     */\n    public final boolean requiresSecureDecryption;\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, mimeType, data, false);\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 mimeType See {@link #mimeType}.\n     * @param data See {@link #data}.\n     * @param requiresSecureDecryption See {@link #requiresSecureDecryption}.\n     */\n    public SchemeData(\n        UUID uuid, String mimeType, @Nullable byte[] data, boolean requiresSecureDecryption) {\n      this(uuid, /* licenseServerUrl= */ null, mimeType, data, requiresSecureDecryption);\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     * @param requiresSecureDecryption See {@link #requiresSecureDecryption}.\n     */\n    public SchemeData(\n        UUID uuid,\n        @Nullable String licenseServerUrl,\n        String mimeType,\n        @Nullable byte[] data,\n        boolean requiresSecureDecryption) {\n      this.uuid = Assertions.checkNotNull(uuid);\n      this.licenseServerUrl = licenseServerUrl;\n      this.mimeType = Assertions.checkNotNull(mimeType);\n      this.data = data;\n      this.requiresSecureDecryption = requiresSecureDecryption;\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      requiresSecureDecryption = in.readByte() != 0;\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, requiresSecureDecryption);\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      dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0));\n    }\n\n    @SuppressWarnings(\"hiding\")\n    public static final Parcelable.Creator<SchemeData> CREATOR =\n        new Parcelable.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-amzn-2.10.6/library/core/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.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   * Wraps the throwable which is the cause of the error state.\n   */\n  class DrmSessionException extends Exception {\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  /**\n   * The session is open, but does not yet have the keys required for decryption.\n   */\n  int STATE_OPENED = 3;\n  /**\n   * The session is open and has the keys required for decryption.\n   */\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  /**\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"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\n\n/**\n * Manages a DRM session.\n */\npublic interface DrmSessionManager<T extends ExoMediaCrypto> {\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   * Acquires a {@link DrmSession} for the specified {@link DrmInitData}. The {@link DrmSession}\n   * must be returned to {@link #releaseSession(DrmSession)} when it is no longer required.\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   * Releases a {@link DrmSession}.\n   */\n  void releaseSession(DrmSession<T> drmSession);\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 @Nullable DrmSessionException getError() {\n    return error;\n  }\n\n  @Override\n  public @Nullable T getMediaCrypto() {\n    return null;\n  }\n\n  @Override\n  public @Nullable Map<String, String> queryKeyStatus() {\n    return null;\n  }\n\n  @Override\n  public @Nullable byte[] getOfflineLicenseKeySetId() {\n    return null;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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 android.media.MediaDrm}.\n */\npublic interface ExoMediaDrm<T extends ExoMediaCrypto> {\n\n  /**\n   * @see MediaDrm#EVENT_KEY_REQUIRED\n   */\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 android.media.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 android.media.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 android.media.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 android.media.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 android.media.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   * @see MediaDrm#release()\n   */\n  void release();\n\n  /**\n   * @see MediaDrm#restoreKeys(byte[], byte[])\n   */\n  void restoreKeys(byte[] sessionId, byte[] keySetId);\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"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 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  /** 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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\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/**\n * An {@link ExoMediaDrm} implementation that wraps the framework {@link MediaDrm}.\n */\n@TargetApi(23)\npublic final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto> {\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  private static final String TAG = \"FrameworkMediaDrm\";\n\n  private final UUID uuid;\n  private final MediaDrm mediaDrm;\n\n  /**\n   * Creates an instance for the specified scheme UUID.\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    if (C.WIDEVINE_UUID.equals(uuid) && needsForceWidevineL3Workaround()) {\n      forceWidevineL3(mediaDrm);\n    }\n  }\n\n  @Override\n  public void setOnEventListener(\n      final ExoMediaDrm.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 ExoMediaDrm.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<DrmInitData.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 void release() {\n    mediaDrm.release();\n  }\n\n  @Override\n  public void restoreKeys(byte[] sessionId, byte[] keySetId) {\n    mediaDrm.restoreKeys(sessionId, keySetId);\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  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 (schemeData.requiresSecureDecryption == firstSchemeData.requiresSecureDecryption\n            && 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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\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, Util.EMPTY_BYTE_ARRAY, 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      byte[] data,\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              data,\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.media.MediaDrm;\nimport android.os.ConditionVariable;\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport androidx.annotation.Nullable;\nimport android.util.Pair;\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.HashMap;\nimport java.util.UUID;\n\n/**\n * Helper class to download, renew and release offline licenses.\n */\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(byte[], byte[], String, int, HashMap)}. 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#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm,\n   *     MediaDrmCallback, HashMap)\n   */\n  public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(\n      String defaultLicenseUrl,\n      boolean forceDefaultLicenseUrl,\n      Factory httpDataSourceFactory,\n      @Nullable HashMap<String, String> optionalKeyRequestParameters)\n      throws UnsupportedDrmException {\n    return new OfflineLicenseHelper<>(C.WIDEVINE_UUID,\n        FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID),\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 mediaDrm An underlying {@link ExoMediaDrm} for use by the manager.\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(byte[], byte[], String, int, HashMap)}. May be null.\n   * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm,\n   *     MediaDrmCallback, HashMap)\n   */\n  public OfflineLicenseHelper(\n      UUID uuid,\n      ExoMediaDrm<T> mediaDrm,\n      MediaDrmCallback callback,\n      @Nullable HashMap<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    drmSessionManager =\n        new DefaultDrmSessionManager<>(uuid, mediaDrm, callback, optionalKeyRequestParameters);\n    drmSessionManager.addListener(new Handler(handlerThread.getLooper()), eventListener);\n  }\n\n  /**\n   * @see DefaultDrmSessionManager#getPropertyByteArray\n   */\n  public synchronized byte[] getPropertyByteArray(String key) {\n    return drmSessionManager.getPropertyByteArray(key);\n  }\n\n  /**\n   * @see DefaultDrmSessionManager#setPropertyByteArray\n   */\n  public synchronized void setPropertyByteArray(String key, byte[] value) {\n    drmSessionManager.setPropertyByteArray(key, value);\n  }\n\n  /**\n   * @see DefaultDrmSessionManager#getPropertyString\n   */\n  public synchronized String getPropertyString(String key) {\n    return drmSessionManager.getPropertyString(key);\n  }\n\n  /**\n   * @see DefaultDrmSessionManager#setPropertyString\n   */\n  public synchronized void setPropertyString(String key, String value) {\n    drmSessionManager.setPropertyString(key, value);\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    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    drmSessionManager.releaseSession(drmSession);\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    DrmSession<T> drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId,\n        drmInitData);\n    DrmSessionException error = drmSession.getError();\n    byte[] keySetId = drmSession.getOfflineLicenseKeySetId();\n    drmSessionManager.releaseSession(drmSession);\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.util.Pair;\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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(long, 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            /* firstStreamSampleTimestampUs= */ 0,\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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   * @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-amzn-2.10.6/library/core/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;\n\n/**\n * Provides data to be consumed by an {@link Extractor}.\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   * <p>\n   * If the end of the input is found having read no data, then behavior is dependent on\n   * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned.\n   * Otherwise an {@link EOFException} is thrown.\n   * <p>\n   * Encountering the end of input having partially satisfied the read is always considered an\n   * error, and will result in an {@link EOFException} being 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 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.\n   * @return True if the read was successful. False if the end of the input was encountered having\n   *     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.\n   * @return True if the skip was successful. False if the end of the input was encountered having\n   *     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   * <p>\n   * If the end of the input is found having peeked no data, then behavior is dependent on\n   * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned.\n   * Otherwise an {@link EOFException} is thrown.\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 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.\n   * @return True if the peek was successful. False if the end of the input was encountered having\n   *     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.\n   * <p>\n   * If the end of the input is encountered before advancing the peek position, then behavior is\n   * dependent on {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is\n   * returned. Otherwise an {@link EOFException} is thrown.\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.\n   * @return True if advancing the peek position was successful. False if the end of the input was\n   *     encountered before the peek position could be advanced.\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.\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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, 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-amzn-2.10.6/library/core/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 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  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}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  private @Nullable 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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;\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 = Util.getIntegerCodeForString(\"FLV\");\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.CallSuper;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport android.util.Pair;\nimport android.util.SparseArray;\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_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_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  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. The 12 byte end timecode\n   * starting at {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be\n   * replaced with the duration of the subtitle.\n   * <p>\n   * Equivalent to the UTF-8 string: \"1\\n00:00:00,000 --> 00:00:00,000\\n\".\n   */\n  private static final byte[] SUBRIP_PREFIX = new byte[] {49, 10, 48, 48, 58, 48, 48, 58, 48, 48,\n      44, 48, 48, 48, 32, 45, 45, 62, 32, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 10};\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   * A special end timecode indicating that a subrip subtitle should be displayed until the next\n   * subtitle, or until the end of the media in the case of the last subtitle.\n   * <p>\n   * Equivalent to the UTF-8 string: \"            \".\n   */\n  private static final byte[] SUBRIP_TIMECODE_EMPTY =\n      new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32};\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. The 10 byte end timecode\n   * starting at {@link #SSA_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be\n   * replaced with the duration of the subtitle.\n   * <p>\n   * Equivalent to the UTF-8 string: \"Dialogue: 0:00:00:00,0:00:00:00,\".\n   */\n  private static final byte[] SSA_PREFIX = new byte[] {68, 105, 97, 108, 111, 103, 117, 101, 58, 32,\n      48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44};\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   * A special end timecode indicating that an SSA subtitle should be displayed until the next\n   * subtitle, or until the end of the media in the case of the last subtitle.\n   * <p>\n   * Equivalent to the UTF-8 string: \"          \".\n   */\n  private static final byte[] SSA_TIMECODE_EMPTY =\n      new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32};\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 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  // Block reading state.\n  private int blockState;\n  private long blockTimeUs;\n  private long blockDurationUs;\n  private int blockLacingSampleIndex;\n  private int blockLacingSampleCount;\n  private int[] blockLacingSampleSizes;\n  private int blockTrackNumber;\n  private int blockTrackNumberLength;\n  @C.BufferFlags\n  private int blockFlags;\n\n  // Sample reading state.\n  private int sampleBytesRead;\n  private boolean sampleEncodingHandled;\n  private boolean sampleSignalByteRead;\n  private boolean sampleInitializationVectorRead;\n  private boolean samplePartitionCountRead;\n  private byte sampleSignalByte;\n  private int samplePartitionCount;\n  private int sampleCurrentNalBytesRemaining;\n  private int sampleBytesWritten;\n  private boolean sampleRead;\n  private boolean sampleSeenReferenceBlock;\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  }\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    resetSample();\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 int read(ExtractorInput input, PositionHolder seekPosition)\n      throws IOException, InterruptedException {\n    sampleRead = false;\n    boolean continueReading = true;\n    while (continueReading && !sampleRead) {\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_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_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        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        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        sampleSeenReferenceBlock = 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        // If the ReferenceBlock element was not found for this sample, then it is a keyframe.\n        if (!sampleSeenReferenceBlock) {\n          blockFlags |= C.BUFFER_FLAG_KEY_FRAME;\n        }\n        commitSampleToOutput(tracks.get(blockTrackNumber), blockTimeUs);\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_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        sampleSeenReferenceBlock = 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      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            blockLacingSampleCount = 1;\n            blockLacingSampleSizes = ensureArrayCapacity(blockLacingSampleSizes, 1);\n            blockLacingSampleSizes[0] = contentSize - blockTrackNumberLength - 3;\n          } else {\n            if (id != ID_SIMPLE_BLOCK) {\n              throw new ParserException(\"Lacing only supported in SimpleBlocks.\");\n            }\n\n            // Read the sample count (1 byte).\n            readScratch(input, 4);\n            blockLacingSampleCount = (scratch.data[3] & 0xFF) + 1;\n            blockLacingSampleSizes =\n                ensureArrayCapacity(blockLacingSampleSizes, blockLacingSampleCount);\n            if (lacing == LACING_FIXED_SIZE) {\n              int blockLacingSampleSize =\n                  (contentSize - blockTrackNumberLength - 4) / blockLacingSampleCount;\n              Arrays.fill(blockLacingSampleSizes, 0, blockLacingSampleCount, blockLacingSampleSize);\n            } else if (lacing == LACING_XIPH) {\n              int totalSamplesSize = 0;\n              int headerSize = 4;\n              for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) {\n                blockLacingSampleSizes[sampleIndex] = 0;\n                int byteValue;\n                do {\n                  readScratch(input, ++headerSize);\n                  byteValue = scratch.data[headerSize - 1] & 0xFF;\n                  blockLacingSampleSizes[sampleIndex] += byteValue;\n                } while (byteValue == 0xFF);\n                totalSamplesSize += blockLacingSampleSizes[sampleIndex];\n              }\n              blockLacingSampleSizes[blockLacingSampleCount - 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 < blockLacingSampleCount - 1; sampleIndex++) {\n                blockLacingSampleSizes[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                blockLacingSampleSizes[sampleIndex] = sampleIndex == 0\n                    ? intReadValue : blockLacingSampleSizes[sampleIndex - 1] + intReadValue;\n                totalSamplesSize += blockLacingSampleSizes[sampleIndex];\n              }\n              blockLacingSampleSizes[blockLacingSampleCount - 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          blockLacingSampleIndex = 0;\n        }\n\n        if (id == ID_SIMPLE_BLOCK) {\n          // For SimpleBlock, we have metadata for each sample here.\n          while (blockLacingSampleIndex < blockLacingSampleCount) {\n            writeSampleData(input, track, blockLacingSampleSizes[blockLacingSampleIndex]);\n            long sampleTimeUs = blockTimeUs\n                + (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000;\n            commitSampleToOutput(track, sampleTimeUs);\n            blockLacingSampleIndex++;\n          }\n          blockState = BLOCK_STATE_START;\n        } else {\n          // For Block, we send the metadata at the end of the BlockGroup element since we'll know\n          // if the sample is a keyframe or not only at that point.\n          writeSampleData(input, track, blockLacingSampleSizes[0]);\n        }\n\n        break;\n      default:\n        throw new ParserException(\"Unexpected id: \" + id);\n    }\n  }\n\n  private void commitSampleToOutput(Track track, long timeUs) {\n    if (track.trueHdSampleRechunker != null) {\n      track.trueHdSampleRechunker.sampleMetadata(track, timeUs);\n    } else {\n      if (CODEC_ID_SUBRIP.equals(track.codecId)) {\n        commitSubtitleSample(\n            track,\n            SUBRIP_TIMECODE_FORMAT,\n            SUBRIP_PREFIX_END_TIMECODE_OFFSET,\n            SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR,\n            SUBRIP_TIMECODE_EMPTY);\n      } else if (CODEC_ID_ASS.equals(track.codecId)) {\n        commitSubtitleSample(\n            track,\n            SSA_TIMECODE_FORMAT,\n            SSA_PREFIX_END_TIMECODE_OFFSET,\n            SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR,\n            SSA_TIMECODE_EMPTY);\n      }\n      track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData);\n    }\n    sampleRead = true;\n    resetSample();\n  }\n\n  private void resetSample() {\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  /**\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  private void 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;\n    } else if (CODEC_ID_ASS.equals(track.codecId)) {\n      writeSubtitleSampleData(input, SSA_PREFIX, size);\n      return;\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      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          readToTarget(input, nalLengthData, nalUnitLengthFieldLengthDiff,\n              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          sampleCurrentNalBytesRemaining -=\n              readToOutput(input, output, sampleCurrentNalBytesRemaining);\n        }\n      }\n    } else {\n      if (track.trueHdSampleRechunker != null) {\n        Assertions.checkState(sampleStrippedBytes.limit() == 0);\n        track.trueHdSampleRechunker.startSample(input, blockFlags, size);\n      }\n      while (sampleBytesRead < size) {\n        readToOutput(input, output, size - sampleBytesRead);\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\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  private void commitSubtitleSample(Track track, String timecodeFormat, int endTimecodeOffset,\n      long lastTimecodeValueScalingFactor, byte[] emptyTimecode) {\n    setSampleDuration(subtitleSample.data, blockDurationUs, timecodeFormat, endTimecodeOffset,\n        lastTimecodeValueScalingFactor, emptyTimecode);\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    sampleBytesWritten += subtitleSample.limit();\n  }\n\n  private static void setSampleDuration(byte[] subripSampleData, long durationUs,\n      String timecodeFormat, int endTimecodeOffset, long lastTimecodeValueScalingFactor,\n      byte[] emptyTimecode) {\n    byte[] timeCodeData;\n    if (durationUs == C.TIME_UNSET) {\n      timeCodeData = emptyTimecode;\n    } else {\n      int hours = (int) (durationUs / (3600 * C.MICROS_PER_SECOND));\n      durationUs -= (hours * 3600 * C.MICROS_PER_SECOND);\n      int minutes = (int) (durationUs / (60 * C.MICROS_PER_SECOND));\n      durationUs -= (minutes * 60 * C.MICROS_PER_SECOND);\n      int seconds = (int) (durationUs / C.MICROS_PER_SECOND);\n      durationUs -= (seconds * C.MICROS_PER_SECOND);\n      int lastValue = (int) (durationUs / lastTimecodeValueScalingFactor);\n      timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, timecodeFormat, hours, minutes,\n          seconds, lastValue));\n    }\n    System.arraycopy(timeCodeData, 0, subripSampleData, endTimecodeOffset, emptyTimecode.length);\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 readToTarget(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    sampleBytesRead += length;\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 readToOutput(ExtractorInput input, TrackOutput output, int length)\n      throws IOException, InterruptedException {\n    int bytesRead;\n    int strippedBytesLeft = sampleStrippedBytes.bytesLeft();\n    if (strippedBytesLeft > 0) {\n      bytesRead = Math.min(length, strippedBytesLeft);\n      output.sampleData(sampleStrippedBytes, bytesRead);\n    } else {\n      bytesRead = output.sampleData(input, length, false);\n    }\n    sampleBytesRead += bytesRead;\n    sampleBytesWritten += bytesRead;\n    return bytesRead;\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    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 sampleCount;\n    private int chunkSize;\n    private long timeUs;\n    private @C.BufferFlags int blockFlags;\n\n    public TrueHdSampleRechunker() {\n      syncframePrefix = new byte[Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH];\n    }\n\n    public void reset() {\n      foundSyncframe = false;\n    }\n\n    public void startSample(ExtractorInput input, @C.BufferFlags int blockFlags, int size)\n        throws IOException, InterruptedException {\n      if (!foundSyncframe) {\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        sampleCount = 0;\n      }\n      if (sampleCount == 0) {\n        // This is the first sample in the chunk, so reset the block flags and chunk size.\n        this.blockFlags = blockFlags;\n        chunkSize = 0;\n      }\n      chunkSize += size;\n    }\n\n    public void sampleMetadata(Track track, long timeUs) {\n      if (!foundSyncframe) {\n        return;\n      }\n      if (sampleCount++ == 0) {\n        // This is the first sample in the chunk, so update the timestamp.\n        this.timeUs = timeUs;\n      }\n      if (sampleCount < Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) {\n        // We haven't read enough samples to output a chunk.\n        return;\n      }\n      track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData);\n      sampleCount = 0;\n    }\n\n    public void outputPendingSampleMetadata(Track track) {\n      if (foundSyncframe && sampleCount > 0) {\n        track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData);\n        sampleCount = 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 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.nativeOrder()).putLong(codecDelayNs).array());\n          initializationData.add(\n              ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).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    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);\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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;\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 = Util.getIntegerCodeForString(\"Xing\");\n  private static final int SEEK_HEADER_INFO = Util.getIntegerCodeForString(\"Info\");\n  private static final int SEEK_HEADER_VBRI = Util.getIntegerCodeForString(\"VBRI\");\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      Id3Decoder.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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  private final @Nullable 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-amzn-2.10.6/library/core/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 com.google.android.exoplayer2.util.Util;\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 = Util.getIntegerCodeForString(\"ftyp\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_avc1 = Util.getIntegerCodeForString(\"avc1\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_avc3 = Util.getIntegerCodeForString(\"avc3\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_avcC = Util.getIntegerCodeForString(\"avcC\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_hvc1 = Util.getIntegerCodeForString(\"hvc1\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_hev1 = Util.getIntegerCodeForString(\"hev1\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_hvcC = Util.getIntegerCodeForString(\"hvcC\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_vp08 = Util.getIntegerCodeForString(\"vp08\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_vp09 = Util.getIntegerCodeForString(\"vp09\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_vpcC = Util.getIntegerCodeForString(\"vpcC\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_av01 = Util.getIntegerCodeForString(\"av01\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_av1C = Util.getIntegerCodeForString(\"av1C\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dvav = Util.getIntegerCodeForString(\"dvav\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dva1 = Util.getIntegerCodeForString(\"dva1\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dvhe = Util.getIntegerCodeForString(\"dvhe\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dvh1 = Util.getIntegerCodeForString(\"dvh1\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dvcC = Util.getIntegerCodeForString(\"dvcC\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dvvC = Util.getIntegerCodeForString(\"dvvC\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_s263 = Util.getIntegerCodeForString(\"s263\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_d263 = Util.getIntegerCodeForString(\"d263\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mdat = Util.getIntegerCodeForString(\"mdat\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mp4a = Util.getIntegerCodeForString(\"mp4a\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE__mp3 = Util.getIntegerCodeForString(\".mp3\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_wave = Util.getIntegerCodeForString(\"wave\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_lpcm = Util.getIntegerCodeForString(\"lpcm\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sowt = Util.getIntegerCodeForString(\"sowt\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ac_3 = Util.getIntegerCodeForString(\"ac-3\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dac3 = Util.getIntegerCodeForString(\"dac3\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ec_3 = Util.getIntegerCodeForString(\"ec-3\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dec3 = Util.getIntegerCodeForString(\"dec3\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ac_4 = Util.getIntegerCodeForString(\"ac-4\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dac4 = Util.getIntegerCodeForString(\"dac4\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dtsc = Util.getIntegerCodeForString(\"dtsc\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dtsh = Util.getIntegerCodeForString(\"dtsh\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dtsl = Util.getIntegerCodeForString(\"dtsl\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dtse = Util.getIntegerCodeForString(\"dtse\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ddts = Util.getIntegerCodeForString(\"ddts\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_tfdt = Util.getIntegerCodeForString(\"tfdt\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_tfhd = Util.getIntegerCodeForString(\"tfhd\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_trex = Util.getIntegerCodeForString(\"trex\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_trun = Util.getIntegerCodeForString(\"trun\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sidx = Util.getIntegerCodeForString(\"sidx\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_moov = Util.getIntegerCodeForString(\"moov\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mvhd = Util.getIntegerCodeForString(\"mvhd\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_trak = Util.getIntegerCodeForString(\"trak\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mdia = Util.getIntegerCodeForString(\"mdia\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_minf = Util.getIntegerCodeForString(\"minf\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stbl = Util.getIntegerCodeForString(\"stbl\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_esds = Util.getIntegerCodeForString(\"esds\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_moof = Util.getIntegerCodeForString(\"moof\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_traf = Util.getIntegerCodeForString(\"traf\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mvex = Util.getIntegerCodeForString(\"mvex\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mehd = Util.getIntegerCodeForString(\"mehd\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_tkhd = Util.getIntegerCodeForString(\"tkhd\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_edts = Util.getIntegerCodeForString(\"edts\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_elst = Util.getIntegerCodeForString(\"elst\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mdhd = Util.getIntegerCodeForString(\"mdhd\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_hdlr = Util.getIntegerCodeForString(\"hdlr\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stsd = Util.getIntegerCodeForString(\"stsd\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_pssh = Util.getIntegerCodeForString(\"pssh\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sinf = Util.getIntegerCodeForString(\"sinf\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_schm = Util.getIntegerCodeForString(\"schm\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_schi = Util.getIntegerCodeForString(\"schi\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_tenc = Util.getIntegerCodeForString(\"tenc\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_encv = Util.getIntegerCodeForString(\"encv\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_enca = Util.getIntegerCodeForString(\"enca\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_frma = Util.getIntegerCodeForString(\"frma\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_saiz = Util.getIntegerCodeForString(\"saiz\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_saio = Util.getIntegerCodeForString(\"saio\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sbgp = Util.getIntegerCodeForString(\"sbgp\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sgpd = Util.getIntegerCodeForString(\"sgpd\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_uuid = Util.getIntegerCodeForString(\"uuid\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_senc = Util.getIntegerCodeForString(\"senc\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_pasp = Util.getIntegerCodeForString(\"pasp\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_TTML = Util.getIntegerCodeForString(\"TTML\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_vmhd = Util.getIntegerCodeForString(\"vmhd\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mp4v = Util.getIntegerCodeForString(\"mp4v\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stts = Util.getIntegerCodeForString(\"stts\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stss = Util.getIntegerCodeForString(\"stss\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ctts = Util.getIntegerCodeForString(\"ctts\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stsc = Util.getIntegerCodeForString(\"stsc\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stsz = Util.getIntegerCodeForString(\"stsz\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stz2 = Util.getIntegerCodeForString(\"stz2\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stco = Util.getIntegerCodeForString(\"stco\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_co64 = Util.getIntegerCodeForString(\"co64\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_tx3g = Util.getIntegerCodeForString(\"tx3g\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_wvtt = Util.getIntegerCodeForString(\"wvtt\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_stpp = Util.getIntegerCodeForString(\"stpp\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_c608 = Util.getIntegerCodeForString(\"c608\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_samr = Util.getIntegerCodeForString(\"samr\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sawb = Util.getIntegerCodeForString(\"sawb\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_udta = Util.getIntegerCodeForString(\"udta\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_meta = Util.getIntegerCodeForString(\"meta\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_keys = Util.getIntegerCodeForString(\"keys\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ilst = Util.getIntegerCodeForString(\"ilst\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_mean = Util.getIntegerCodeForString(\"mean\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_name = Util.getIntegerCodeForString(\"name\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_data = Util.getIntegerCodeForString(\"data\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_emsg = Util.getIntegerCodeForString(\"emsg\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_st3d = Util.getIntegerCodeForString(\"st3d\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_sv3d = Util.getIntegerCodeForString(\"sv3d\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_proj = Util.getIntegerCodeForString(\"proj\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_camm = Util.getIntegerCodeForString(\"camm\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_alac = Util.getIntegerCodeForString(\"alac\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_alaw = Util.getIntegerCodeForString(\"alaw\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_ulaw = Util.getIntegerCodeForString(\"ulaw\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_Opus = Util.getIntegerCodeForString(\"Opus\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dOps = Util.getIntegerCodeForString(\"dOps\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_fLaC = Util.getIntegerCodeForString(\"fLaC\");\n\n  @SuppressWarnings(\"ConstantCaseForConstants\")\n  public static final int TYPE_dfLa = Util.getIntegerCodeForString(\"dfLa\");\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    public @Nullable 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    public @Nullable 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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\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.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\", \"ConstantCaseForConstants\"})\n/* package */ final class AtomParsers {\n\n  private static final String TAG = \"AtomParsers\";\n\n  private static final int TYPE_vide = Util.getIntegerCodeForString(\"vide\");\n  private static final int TYPE_soun = Util.getIntegerCodeForString(\"soun\");\n  private static final int TYPE_text = Util.getIntegerCodeForString(\"text\");\n  private static final int TYPE_sbtl = Util.getIntegerCodeForString(\"sbtl\");\n  private static final int TYPE_subt = Util.getIntegerCodeForString(\"subt\");\n  private static final int TYPE_clcp = Util.getIntegerCodeForString(\"clcp\");\n  private static final int TYPE_meta = Util.getIntegerCodeForString(\"meta\");\n  private static final int TYPE_mdta = Util.getIntegerCodeForString(\"mdta\");\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 || gaplessInfoHolder.hasGaplessInfo()) {\n      // There is no edit list, or we are ignoring it as we already have gapless metadata to apply.\n      // This implementation does not support applying both gapless metadata and an edit list.\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        // TODO: Support profiles 4, 8 and 9 once we have a way to fall back to AVC/HEVC decoding.\n        if (dolbyVisionConfig != null && dolbyVisionConfig.profile == 5) {\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            // TODO: Do we really need to do this? See [Internal: b/10903778]\n            // Update sampleRate and channelCount from the AudioSpecificConfig initialization data.\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      }\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport android.util.Pair;\nimport android.util.SparseArray;\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. */\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(\"ConstantField\")\n  private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString(\"seig\");\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  private final @Nullable Track sideloadedTrack;\n\n  // Sideloaded data.\n  private final List<Format> closedCaptionFormats;\n  private final @Nullable 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  private final @Nullable 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  private final @Nullable 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      Atom.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      Atom.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      Atom.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<Atom.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-amzn-2.10.6/library/core/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 Parcelable.Creator<MdtaMetadataEntry> CREATOR =\n      new Parcelable.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-amzn-2.10.6/library/core/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 com.google.android.exoplayer2.util.Util;\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 = Util.getIntegerCodeForString(\"nam\");\n  private static final int SHORT_TYPE_NAME_2 = Util.getIntegerCodeForString(\"trk\");\n  private static final int SHORT_TYPE_COMMENT = Util.getIntegerCodeForString(\"cmt\");\n  private static final int SHORT_TYPE_YEAR = Util.getIntegerCodeForString(\"day\");\n  private static final int SHORT_TYPE_ARTIST = Util.getIntegerCodeForString(\"ART\");\n  private static final int SHORT_TYPE_ENCODER = Util.getIntegerCodeForString(\"too\");\n  private static final int SHORT_TYPE_ALBUM = Util.getIntegerCodeForString(\"alb\");\n  private static final int SHORT_TYPE_COMPOSER_1 = Util.getIntegerCodeForString(\"com\");\n  private static final int SHORT_TYPE_COMPOSER_2 = Util.getIntegerCodeForString(\"wrt\");\n  private static final int SHORT_TYPE_LYRICS = Util.getIntegerCodeForString(\"lyr\");\n  private static final int SHORT_TYPE_GENRE = Util.getIntegerCodeForString(\"gen\");\n\n  // Codes that have equivalent ID3 frames.\n  private static final int TYPE_COVER_ART = Util.getIntegerCodeForString(\"covr\");\n  private static final int TYPE_GENRE = Util.getIntegerCodeForString(\"gnre\");\n  private static final int TYPE_GROUPING = Util.getIntegerCodeForString(\"grp\");\n  private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString(\"disk\");\n  private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString(\"trkn\");\n  private static final int TYPE_TEMPO = Util.getIntegerCodeForString(\"tmpo\");\n  private static final int TYPE_COMPILATION = Util.getIntegerCodeForString(\"cpil\");\n  private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString(\"aART\");\n  private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString(\"sonm\");\n  private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString(\"soal\");\n  private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString(\"soar\");\n  private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString(\"soaa\");\n  private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString(\"soco\");\n\n  // Types that do not have equivalent ID3 frames.\n  private static final int TYPE_RATING = Util.getIntegerCodeForString(\"rtng\");\n  private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString(\"pgap\");\n  private static final int TYPE_TV_SORT_SHOW = Util.getIntegerCodeForString(\"sosn\");\n  private static final int TYPE_TV_SHOW = Util.getIntegerCodeForString(\"tvsh\");\n\n  // Type for items that are intended for internal use by the player.\n  private static final int TYPE_INTERNAL = Util.getIntegerCodeForString(\"----\");\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-amzn-2.10.6/library/core/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 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.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 = Util.getIntegerCodeForString(\"qt  \");\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      containerAtoms.push(new ContainerAtom(atomType, endPosition));\n      if (atomSize == atomHeaderBytesRead) {\n        processAtomEnded(endPosition);\n      } else {\n        if (atomType == Atom.TYPE_meta) {\n          maybeSkipRemainingMetaAtomHeaderBytes(input);\n        }\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      Atom.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    Atom.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      Atom.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      Atom.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-amzn-2.10.6/library/core/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  @SuppressWarnings(\"ParameterNotNullable\")\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-amzn-2.10.6/library/core/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 com.google.android.exoplayer2.util.Util;\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        Util.getIntegerCodeForString(\"isom\"),\n        Util.getIntegerCodeForString(\"iso2\"),\n        Util.getIntegerCodeForString(\"iso3\"),\n        Util.getIntegerCodeForString(\"iso4\"),\n        Util.getIntegerCodeForString(\"iso5\"),\n        Util.getIntegerCodeForString(\"iso6\"),\n        Util.getIntegerCodeForString(\"avc1\"),\n        Util.getIntegerCodeForString(\"hvc1\"),\n        Util.getIntegerCodeForString(\"hev1\"),\n        Util.getIntegerCodeForString(\"av01\"),\n        Util.getIntegerCodeForString(\"mp41\"),\n        Util.getIntegerCodeForString(\"mp42\"),\n        Util.getIntegerCodeForString(\"3g2a\"),\n        Util.getIntegerCodeForString(\"3g2b\"),\n        Util.getIntegerCodeForString(\"3gr6\"),\n        Util.getIntegerCodeForString(\"3gs6\"),\n        Util.getIntegerCodeForString(\"3ge6\"),\n        Util.getIntegerCodeForString(\"3gg6\"),\n        Util.getIntegerCodeForString(\"M4V \"),\n        Util.getIntegerCodeForString(\"M4A \"),\n        Util.getIntegerCodeForString(\"f4v \"),\n        Util.getIntegerCodeForString(\"kddi\"),\n        Util.getIntegerCodeForString(\"M4VP\"),\n        Util.getIntegerCodeForString(\"qt  \"), // Apple QuickTime\n        Util.getIntegerCodeForString(\"MSNV\"), // Sony PSP\n        Util.getIntegerCodeForString(\"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 (inputLength != C.LENGTH_UNSET && bytesSearched + atomSize > inputLength) {\n        // The file is invalid because the atom extends past the end of the file.\n        return false;\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 == Util.getIntegerCodeForString(\"3gp\")) {\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-amzn-2.10.6/library/core/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  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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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      throws IOException, InterruptedException {\n    byte[] data = packet.data;\n    if (streamMetadata == null) {\n      streamMetadata = new FlacStreamMetadata(data, 17);\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              null,\n              MimeTypes.AUDIO_FLAC,\n              null,\n              Format.NO_VALUE,\n              streamMetadata.bitRate(),\n              streamMetadata.channels,\n              streamMetadata.sampleRate,\n              initializationData,\n              null,\n              0,\n              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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 com.google.android.exoplayer2.util.Util;\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 = Util.getIntegerCodeForString(\"OggS\");\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 com.google.android.exoplayer2.util.Util;\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 = Util.getIntegerCodeForString(\"Opus\");\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 VorbisUtil.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 VorbisUtil.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      switch (mappingType) {\n        case 0:\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          break;\n        default:\n          Log.e(TAG, \"mapping type other than 0 not supported: \" + mappingType);\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-amzn-2.10.6/library/core/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 com.google.android.exoplayer2.util.Util;\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 = Util.getIntegerCodeForString(\"RCC\\u0001\");\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-amzn-2.10.6/library/core/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;\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 com.google.android.exoplayer2.util.Util;\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  private static final int ID3_TAG = Util.getIntegerCodeForString(\"ID3\");\n\n  private final long firstSampleTimestampUs;\n  private final Ac3Reader reader;\n  private final ParsableByteArray sampleData;\n\n  private boolean startedPacket;\n\n  public Ac3Extractor() {\n    this(0);\n  }\n\n  public Ac3Extractor(long firstSampleTimestampUs) {\n    this.firstSampleTimestampUs = firstSampleTimestampUs;\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(10);\n    int startPosition = 0;\n    while (true) {\n      input.peekFully(scratch.data, 0, 10);\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(firstSampleTimestampUs, 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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;\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 com.google.android.exoplayer2.util.Util;\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 static final int ID3_TAG = Util.getIntegerCodeForString(\"ID3\");\n\n  private final long firstSampleTimestampUs;\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    this(/* firstSampleTimestampUs= */ 0);\n  }\n\n  /** Creates a new extractor for AC-4 bitstreams, using the specified first sample timestamp. */\n  public Ac4Extractor(long firstSampleTimestampUs) {\n    this.firstSampleTimestampUs = firstSampleTimestampUs;\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(10);\n    int startPosition = 0;\n    while (true) {\n      input.peekFully(scratch.data, /* offset= */ 0, /* length= */ 10);\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(firstSampleTimestampUs, 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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;\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 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;\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  private static final int ID3_TAG = Util.getIntegerCodeForString(\"ID3\");\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  private final long firstStreamSampleTimestampUs;\n\n  private @Nullable 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  public AdtsExtractor() {\n    this(0);\n  }\n\n  public AdtsExtractor(long firstStreamSampleTimestampUs) {\n    this(/* firstStreamSampleTimestampUs= */ firstStreamSampleTimestampUs, /* flags= */ 0);\n  }\n\n  /**\n   * @param firstStreamSampleTimestampUs The timestamp to be used for the first sample of the stream\n   *     output from this extractor.\n   * @param flags Flags that control the extractor's behavior.\n   */\n  public AdtsExtractor(long firstStreamSampleTimestampUs, @Flags int flags) {\n    this.firstStreamSampleTimestampUs = firstStreamSampleTimestampUs;\n    this.firstSampleTimestampUs = firstStreamSampleTimestampUs;\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    scratch = new ParsableByteArray(10);\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 = firstStreamSampleTimestampUs + 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, 0, 10);\n      scratch.setPosition(0);\n      if (scratch.readUnsignedInt24() != ID3_TAG) {\n        break;\n      }\n      scratch.skipBytes(3);\n      int length = scratch.readSynchSafeInt();\n      firstFramePosition += 10 + 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    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    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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.IntDef;\nimport android.util.SparseArray;\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  public 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-amzn-2.10.6/library/core/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          NalUnitUtil.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        NalUnitUtil.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<NalUnitUtil.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(NalUnitUtil.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      NalUnitUtil.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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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;\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 static final int ID3_HEADER_SIZE = 10;\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_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    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_SIZE) {\n      // We're still reading the ID3 header.\n      int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_SIZE - sampleBytesRead);\n      System.arraycopy(data.data, data.getPosition(), id3Header.data, sampleBytesRead,\n          headerBytesAvailable);\n      if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_SIZE) {\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_SIZE + 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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\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.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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 PesReader.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-amzn-2.10.6/library/core/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.crc(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-amzn-2.10.6/library/core/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/**\n * Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}.\n */\n/* package */ 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.IntDef;\nimport android.util.SparseArray;\nimport android.util.SparseBooleanArray;\nimport android.util.SparseIntArray;\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 = Util.getIntegerCodeForString(\"AC-3\");\n  private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString(\"EAC3\");\n  private static final long AC4_FORMAT_IDENTIFIER = Util.getIntegerCodeForString(\"AC-4\");\n  private static final long HEVC_FORMAT_IDENTIFIER = Util.getIntegerCodeForString(\"HEVC\");\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-amzn-2.10.6/library/core/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 androidx.annotation.IntDef;\nimport android.util.SparseArray;\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 PesReader.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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 chanels. */\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-amzn-2.10.6/library/core/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 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  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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.util.Pair;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.AmazonQuirks;\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  public final @Nullable 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  public final @Nullable 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  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        /* 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 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 forceDisableAdaptive,\n      boolean forceSecure) {\n    return new MediaCodecInfo(\n        name,\n        mimeType,\n        codecMimeType,\n        capabilities,\n        /* passthrough= */ false,\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 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    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.codecs)) {\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 given {@code codec}. If there is insufficient information to\n   * decide, returns true.\n   *\n   * @param codec Codec string as defined in RFC 6381.\n   * @return True if the given codec is supported by the decoder.\n   */\n  public boolean isCodecSupported(String codec) {\n    if (codec == null || mimeType == null) {\n      return true;\n    }\n    String codecMimeType = MimeTypes.getMediaMimeType(codec);\n    if (codecMimeType == null) {\n      return true;\n    }\n    if (!mimeType.equals(codecMimeType)) {\n      logNoSupport(\"codec.mime \" + codec + \", \" + codecMimeType);\n      return false;\n    }\n    Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(codec);\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 (AmazonQuirks.shouldSkipProfileLevelCheck() || // AMZN_CHANGE_ONELINE\n            (!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, \" + codec + \", \" + codecMimeType);\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 =\n          MediaCodecUtil.getCodecProfileAndLevel(format.codecs);\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.codecs);\n      Pair<Integer, Integer> newCodecProfileLevel =\n          MediaCodecUtil.getCodecProfileAndLevel(newFormat.codecs);\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   * <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   * @param frameRate Optional frame rate in frames per second. Ignored if set to\n   *     {@link 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      // Capabilities are known to be inaccurately reported for vertical resolutions on some devices\n      // (b/31387661). If the video is vertical and the capabilities indicate support if the width\n      // and height are swapped, we assume that the vertical resolution is also supported.\n      if (width >= height\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"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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.Looper;\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.MimeTypes;\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\nimport com.google.android.exoplayer2.util.Logger;\n/**\n * An abstract renderer that uses {@link MediaCodec} to decode samples for rendering.\n */\npublic abstract class MediaCodecRenderer extends BaseRenderer {\n\n  /**\n   * Thrown when a failure occurs instantiating a decoder.\n   */\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 name of the decoder that failed to initialize. Null if no suitable decoder was found.\n     */\n    public final String decoderName;\n\n    /**\n     * An optional developer-readable diagnostic information string. May be null.\n     */\n    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    public final @Nullable 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          /* decoderName= */ null,\n          buildCustomDiagnosticInfo(errorCode),\n          /* fallbackDecoderInitializationException= */ null);\n    }\n\n    public DecoderInitializationException(Format format, Throwable cause,\n        boolean secureDecoderRequired, String decoderName) {\n      this(\n          \"Decoder init failed: \" + decoderName + \", \" + format,\n          cause,\n          format.sampleMimeType,\n          secureDecoderRequired,\n          decoderName,\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 String decoderName,\n        @Nullable String diagnosticInfo,\n        @Nullable DecoderInitializationException fallbackDecoderInitializationException) {\n      super(message, cause);\n      this.mimeType = mimeType;\n      this.secureDecoderRequired = secureDecoderRequired;\n      this.decoderName = decoderName;\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          decoderName,\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.exoplayer.MediaCodecTrackRenderer_\" + sign + Math.abs(errorCode);\n    }\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 FormatHolder formatHolder;\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 lastBufferInStreamPresentationTimeUs;\n  private long largestQueuedPresentationTimeUs;\n  private boolean inputStreamEnded;\n  private boolean outputStreamEnded;\n  private boolean waitingForKeys;\n  private boolean waitingForFirstSyncSample;\n  private boolean waitingForFirstSampleInFormat;\n\n  protected DecoderCounters decoderCounters;\n\n  // AMZN_CHANGE_ONELINE\n  private final Logger log = new Logger(Logger.Module.AudioVideoCommon, TAG);\n  protected String codecName = \"CodecNameUnknown\";\n  // AMZN_CHANGE_END\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    formatHolder = new FormatHolder();\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  @Override\n  public final int supportsMixedMimeTypeAdaptation() {\n    return ADAPTIVE_NOT_SEAMLESS;\n  }\n\n  @Override\n  public final int supportsFormat(Format format) throws ExoPlaybackException {\n    try {\n      return supportsFormat(mediaCodecSelector, drmSessionManager, format);\n    } catch (DecoderQueryException e) {\n      throw ExoPlaybackException.createForRenderer(e, getIndex());\n    }\n  }\n\n  /**\n   * Returns the extent to which the renderer is capable of supporting a given format.\n   *\n   * @param mediaCodecSelector The decoder selector.\n   * @param drmSessionManager The renderer's {@link DrmSessionManager}.\n   * @param format The format.\n   * @return The extent to which the renderer is capable of supporting the given format. See\n   *     {@link #supportsFormat(Format)} for more detail.\n   * @throws DecoderQueryException If there was an error querying decoders.\n   */\n  protected abstract int supportsFormat(MediaCodecSelector mediaCodecSelector,\n      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, 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 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 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      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 ExoPlaybackException.createForRenderer(e, getIndex());\n          }\n          mediaCryptoRequiresSecureDecoder =\n              !sessionMediaCrypto.forceAllowInsecureDecoderComponents\n                  && mediaCrypto.requiresSecureDecoderComponent(mimeType);\n        }\n      }\n      if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {\n        @DrmSession.State int drmSessionState = codecDrmSession.getState();\n        if (drmSessionState == DrmSession.STATE_ERROR) {\n          throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());\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 ExoPlaybackException.createForRenderer(e, getIndex());\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    log.i(\"onPositionReset: \"+ positionUs + \", joining: \" + joining);\n    inputStreamEnded = false;\n    outputStreamEnded = 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    log.i(\"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          codec.stop();\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 (outputStreamEnded) {\n      renderToEndOfStream();\n      return;\n    }\n    if (log.allowVerbose()) {\n      log.v(\"render: positionUs = \" + positionUs\n              + \", elapsedRealtimeUs = \" + elapsedRealtimeUs);\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 may\n      // 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  }\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  /** Reads into {@link #flagsOnlyBuffer} and returns whether a format was read. */\n  private boolean readToFlagsOnlyBuffer(boolean requireFormat) throws ExoPlaybackException {\n    flagsOnlyBuffer.clear();\n    int result = readSource(formatHolder, flagsOnlyBuffer, requireFormat);\n    if (result == C.RESULT_FORMAT_READ) {\n      onInputFormatChanged(formatHolder.format);\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.name);\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    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<FrameworkMediaCrypto> previous = sourceDrmSession;\n    sourceDrmSession = session;\n    releaseDrmSessionIfUnused(previous);\n  }\n\n  private void setCodecDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> session) {\n    DrmSession<FrameworkMediaCrypto> previous = codecDrmSession;\n    codecDrmSession = session;\n    releaseDrmSessionIfUnused(previous);\n  }\n\n  private void releaseDrmSessionIfUnused(@Nullable DrmSession<FrameworkMediaCrypto> session) {\n    if (session != null && session != sourceDrmSession && session != codecDrmSession) {\n      drmSessionManager.releaseSession(session);\n    }\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        if (log.allowVerbose()) {\n          log.v(\"dequeueInputBuffer returned \" + inputIndex + \"... returning false\");\n        }\n        return false;\n      }\n      if (log.allowDebug()) {\n        log.d(\"dequeueInputBuffer returned \" + inputIndex);\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    int adaptiveReconfigurationBytes = 0;\n    if (waitingForKeys) {\n      log.i(\"We've already read an encrypted sample into sampleHolder, 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        log.i(\"Appending reconfiguration data at start of the buffer\");\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      log.i(\"Source returned SampleSource.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        log.i(\"We received two formats in a row.\");\n        buffer.clear();\n        codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;\n      }\n      onInputFormatChanged(formatHolder.format);\n      return true;\n    }\n\n    // We've read a buffer.\n    if (buffer.isEndOfStream()) {\n      log.i(\"Reached end of buffer\");\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          if (log.allowDebug()) {\n            log.d(\"queueInputBuffer: inputIndex = \" + inputIndex + \", flag = BUFFER_FLAG_END_OF_STREAM\");\n          }\n          codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);\n          resetInputBuffer();\n        }\n      } catch (CryptoException e) {\n        throw ExoPlaybackException.createForRenderer(e, getIndex());\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      log.i(\"Waiting For Keys!!!\");\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      onQueueInputBuffer(buffer);\n\n      if (bufferEncrypted) {\n        MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(buffer,\n            adaptiveReconfigurationBytes);\n        if (log.allowDebug()) {\n          log.d(\"queueSecureInputBuffer: inputIndex = \" + inputIndex +\n                                  \", presentationTimeUs = \" + presentationTimeUs);\n        }\n        codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0);\n      } else {\n        if (log.allowDebug()) {\n          log.d(\"queueInputBuffer: inputIndex = \" + inputIndex +\n                                \", bufferSize = \" + buffer.data.limit() +\n                                \", presentationTimeUs = \" + presentationTimeUs);\n        }\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 ExoPlaybackException.createForRenderer(e, getIndex());\n    }\n    return true;\n  }\n\n  private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {\n    if (codecDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {\n      return false;\n    }\n    @DrmSession.State int drmSessionState = codecDrmSession.getState();\n    if (drmSessionState == DrmSession.STATE_ERROR) {\n      throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());\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 format is read from the upstream {@link MediaPeriod}.\n   *\n   * @param newFormat The new format.\n   * @throws ExoPlaybackException If an error occurs re-initializing the {@link MediaCodec}.\n   */\n  protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {\n    log.i(\"onInputFormatChanged: format = \" + formatHolder.format);\n    Format oldFormat = inputFormat;\n    inputFormat = newFormat;\n    waitingForFirstSampleInFormat = true;\n\n    boolean drmInitDataChanged =\n        !Util.areEqual(newFormat.drmInitData, oldFormat == null ? null : oldFormat.drmInitData);\n    if (drmInitDataChanged) {\n      if (newFormat.drmInitData != null) {\n        if (drmSessionManager == null) {\n          throw ExoPlaybackException.createForRenderer(\n              new IllegalStateException(\"Media requires a DrmSessionManager\"), getIndex());\n        }\n        DrmSession<FrameworkMediaCrypto> session =\n            drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);\n        if (session == sourceDrmSession || session == codecDrmSession) {\n          // We already had this session. The manager must be reference counting, so release it once\n          // to get the count attributed to this renderer back down to 1.\n          drmSessionManager.releaseSession(session);\n        }\n        setSourceDrmSession(session);\n      } else {\n        setSourceDrmSession(null);\n      }\n    }\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 format of the {@link MediaCodec} changes.\n   * <p>\n   * The default implementation is a no-op.\n   *\n   * @param codec The {@link MediaCodec} instance.\n   * @param outputFormat The new output format.\n   * @throws ExoPlaybackException Thrown if an error occurs handling the new output format.\n   */\n  protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat)\n      throws ExoPlaybackException {\n    // Do nothing.\n  }\n\n  /**\n   * Called immediately before an input buffer is queued into the codec.\n   * <p>\n   * 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 format, and if it can\n   * 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 format for which the existing instance is configured.\n   * @param newFormat The new 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 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 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 (log.allowVerbose()) {\n      log.v(\"drainOutputBuffer: positionUs = \" + positionUs + \", elapsedRealtimeUs = \" + elapsedRealtimeUs);\n    }\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  /**\n   * Processes a new output format.\n   */\n  private void processOutputFormat() throws ExoPlaybackException {\n    MediaFormat format = codec.getOutputFormat();\n    if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER\n        && format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT\n        && format.getInteger(MediaFormat.KEY_HEIGHT) == 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      format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);\n    }\n    onOutputFormatChanged(codec, format);\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 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  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 ExoPlaybackException.createForRenderer(e, getIndex());\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  /**\n   * Returns whether the device needs keys to have been loaded into the {@link DrmSession} before\n   * codec configuration.\n   */\n  private boolean deviceNeedsDrmKeysToConfigureCodecWorkaround() {\n    return \"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\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   * <p>\n   * If true is returned, the renderer will work around the issue by discarding data up to the SPS.\n   *\n   * @param name The name of the decoder.\n   * @param format The 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 format\n   * to 2 for the given input format, whilst only actually outputting a single channel.\n   * <p>\n   * If true is returned then we explicitly override the number of channels in the output format,\n   * setting it to 1.\n   *\n   * @param name The decoder name.\n   * @param format The input format.\n   * @return True if the decoder is known to set the number of audio channels in the output format\n   *     to 2 for the given input format, whilst only actually outputting a single channel. False\n   *     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-amzn-2.10.6/library/core/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        public @Nullable 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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\nimport android.util.Pair;\nimport android.util.SparseIntArray;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\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  // 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  private static final String CODEC_ID_DVHE = \"dvhe\";\n  private static final String CODEC_ID_DVH1 = \"dvh1\";\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    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    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 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      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 given\n   * codec description string (as defined by RFC 6381).\n   *\n   * @param codec A codec description string, as defined by RFC 6381, or {@code null} if not known.\n   * @return A pair (profile constant, level constant) if {@code codec} is well-formed and\n   *     recognized, or null otherwise\n   */\n  @Nullable\n  public static Pair<Integer, Integer> getCodecProfileAndLevel(@Nullable String codec) {\n    if (codec == null) {\n      return null;\n    }\n    // TODO: Check codec profile/level for AV1 once targeting Android Q and [Internal: b/128552878]\n    // has been fixed.\n    String[] parts = codec.split(\"\\\\.\");\n    switch (parts[0]) {\n      case CODEC_ID_AVC1:\n      case CODEC_ID_AVC2:\n        return getAvcProfileAndLevel(codec, parts);\n      case CODEC_ID_HEV1:\n      case CODEC_ID_HVC1:\n        return getHevcProfileAndLevel(codec, parts);\n      case CODEC_ID_DVHE:\n      case CODEC_ID_DVH1:\n        return getDolbyVisionProfileAndLevel(codec, parts);\n      case CODEC_ID_MP4A:\n        return getAacCodecProfileAndLevel(codec, parts);\n      default:\n        return null;\n    }\n  }\n\n  // Internal methods.\n\n  /**\n   * Returns {@link MediaCodecInfo}s for the given codec {@code key} 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(CodecKey key,\n      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        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 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                    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                    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)\n        && \"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      Collections.sort(decoderInfos, new RawAudioCodecComparator());\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        Collections.sort(decoderInfos, new PreferOmxGoogleCodecComparator());\n      }\n    }\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  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    String profileString = matcher.group(1);\n    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    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  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    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    String levelString = parts[3];\n    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  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  /**\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      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  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    private android.media.MediaCodecInfo[] mediaCodecInfos;\n\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    @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    private void ensureMediaCodecInfosInitialized() {\n      if (mediaCodecInfos == null) {\n        mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos();\n      }\n    }\n\n  }\n\n  @SuppressWarnings(\"deprecation\")\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 == null) ? 0 : 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  /**\n   * Comparator for ordering media codecs that handle {@link MimeTypes#AUDIO_RAW} to work around\n   * possible inconsistent behavior across different devices. A list sorted with this comparator has\n   * more preferred codecs first.\n   */\n  private static final class RawAudioCodecComparator implements Comparator<MediaCodecInfo> {\n    @Override\n    public int compare(MediaCodecInfo a, MediaCodecInfo b) {\n      return scoreMediaCodecInfo(a) - scoreMediaCodecInfo(b);\n    }\n\n    private static int scoreMediaCodecInfo(MediaCodecInfo mediaCodecInfo) {\n      String name = mediaCodecInfo.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 precedence. See\n        // [Internal: b/62337687].\n        return 1;\n      }\n      return 0;\n    }\n  }\n\n  /** Comparator for preferring OMX.google media codecs. */\n  private static final class PreferOmxGoogleCodecComparator implements Comparator<MediaCodecInfo> {\n    @Override\n    public int compare(MediaCodecInfo a, MediaCodecInfo b) {\n      return scoreMediaCodecInfo(a) - scoreMediaCodecInfo(b);\n    }\n\n    private static int scoreMediaCodecInfo(MediaCodecInfo mediaCodecInfo) {\n      return mediaCodecInfo.name.startsWith(\"OMX.google\") ? -1 : 0;\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    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    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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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;\nimport org.checkerframework.checker.nullness.compatqual.NullableType;\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 == null ? new Entry[0] : entries;\n  }\n\n  /**\n   * @param entries The metadata entries.\n   */\n  public Metadata(List<? extends Entry> entries) {\n    if (entries != null) {\n      this.entries = new Entry[entries.size()];\n      entries.toArray(this.entries);\n    } else {\n      this.entries = new Entry[0];\n    }\n  }\n\n  /* package */ Metadata(Parcel in) {\n    entries = new Metadata.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 Metadata.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    @NullableType Entry[] merged = Arrays.copyOf(entries, entries.length + entriesToAppend.length);\n    System.arraycopy(entriesToAppend, 0, merged, entries.length, entriesToAppend.length);\n    return new Metadata(Util.castNonNullTypeArray(merged));\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 Parcelable.Creator<Metadata> CREATOR =\n      new Parcelable.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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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          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          switch (format.sampleMimeType) {\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              throw new IllegalArgumentException(\n                  \"Attempted to create decoder for unsupported format\");\n          }\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.FormatHolder;\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 renderer for metadata.\n */\npublic final class MetadataRenderer extends BaseRenderer implements Callback {\n\n  /**\n   * @deprecated Use {@link MetadataOutput}.\n   */\n  @Deprecated\n  public interface Output extends MetadataOutput {}\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  private final @Nullable Handler outputHandler;\n  private final FormatHolder formatHolder;\n  private final MetadataInputBuffer buffer;\n  private final Metadata[] pendingMetadata;\n  private final long[] pendingMetadataTimestamps;\n\n  private int pendingMetadataIndex;\n  private int pendingMetadataCount;\n  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    formatHolder = new FormatHolder();\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  public int supportsFormat(Format format) {\n    if (decoderFactory.supportsFormat(format)) {\n      return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM;\n    } else {\n      return FORMAT_UNSUPPORTED_TYPE;\n    }\n  }\n\n  @Override\n  protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {\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) throws ExoPlaybackException {\n    if (!inputStreamEnded && pendingMetadataCount < MAX_PENDING_METADATA_COUNT) {\n      buffer.clear();\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          Metadata metadata = 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 = formatHolder.format.subsampleOffsetUs;\n      }\n    }\n\n    if (pendingMetadataCount > 0 && pendingMetadataTimestamps[pendingMetadataIndex] <= positionUs) {\n      invokeRenderer(pendingMetadata[pendingMetadataIndex]);\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      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        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  @SuppressWarnings(\"unchecked\")\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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.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  @Nullable\n  public Metadata decode(MetadataInputBuffer inputBuffer) {\n    ByteBuffer buffer = inputBuffer.data;\n    byte[] data = buffer.array();\n    int size = buffer.limit();\n    EventMessage decodedEventMessage = decode(new ParsableByteArray(data, size));\n    if (decodedEventMessage == null) {\n      return null;\n    } else {\n      return new Metadata(decodedEventMessage);\n    }\n  }\n\n  @Nullable\n  public EventMessage decode(ParsableByteArray emsgData) {\n    try {\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    } catch (RuntimeException e) {\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.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.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 String TAG = \"IcyDecoder\";\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 = 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    String name = null;\n    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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  public final @Nullable 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 = castNonNull(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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.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  /**\n   * The first three bytes of a well formed ID3 tag header.\n   */\n  public static final int ID3_TAG = Util.getIntegerCodeForString(\"ID3\");\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  private final @Nullable 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  public @Nullable Metadata decode(MetadataInputBuffer inputBuffer) {\n    ByteBuffer buffer = 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  public @Nullable 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  private static @Nullable 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: \" + 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  private static @Nullable 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  private static @Nullable TextInformationFrame decodeTxxxFrame(\n      ParsableByteArray id3Data, int frameSize) 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  private static @Nullable 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  private static @Nullable 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  private static @Nullable 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  public final @Nullable 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-amzn-2.10.6/library/core/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  public final @Nullable 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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.ParsableBitArray;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport java.nio.ByteBuffer;\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  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    // 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    ByteBuffer buffer = inputBuffer.data;\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    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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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    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    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    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-amzn-2.10.6/library/core/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 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   * @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  @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    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-amzn-2.10.6/library/core/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.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(@Download.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      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.beginTransaction();\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  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(@Download.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    return new Download(\n        request,\n        /* state= */ cursor.getInt(COLUMN_INDEX_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= */ cursor.getInt(COLUMN_INDEX_FAILURE_REASON),\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.checkState((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED));\n    if (stopReason != 0) {\n      Assertions.checkState(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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.net.Uri;\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.Message;\nimport androidx.annotation.Nullable;\nimport android.util.SparseIntArray;\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.ProgressiveMediaSource;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\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.lang.reflect.Method;\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   * The default parameters used for track selection for downloading. This default selects the\n   * highest bitrate audio and video tracks which are supported by the renderers.\n   */\n  public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS =\n      new DefaultTrackSelector.ParametersBuilder().setForceHighestSupportedBitrate(true).build();\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  private static final MediaSourceFactory DASH_FACTORY =\n      getMediaSourceFactory(\"com.google.android.exoplayer2.source.dash.DashMediaSource$Factory\");\n  private static final MediaSourceFactory SS_FACTORY =\n      getMediaSourceFactory(\n          \"com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory\");\n  private static final MediaSourceFactory HLS_FACTORY =\n      getMediaSourceFactory(\"com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory\");\n\n  /**\n   * Creates a {@link DownloadHelper} for progressive streams.\n   *\n   * @param uri A stream {@link Uri}.\n   * @return A {@link DownloadHelper} for progressive streams.\n   */\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 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(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,\n        /* rendererCapabilities= */ new RendererCapabilities[0]);\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   * @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, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) {\n    return forDash(\n        uri,\n        dataSourceFactory,\n        renderersFactory,\n        /* drmSessionManager= */ null,\n        DEFAULT_TRACK_SELECTOR_PARAMETERS);\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        DASH_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null),\n        trackSelectorParameters,\n        Util.getRendererCapabilities(renderersFactory, drmSessionManager));\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   * @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, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) {\n    return forHls(\n        uri,\n        dataSourceFactory,\n        renderersFactory,\n        /* drmSessionManager= */ null,\n        DEFAULT_TRACK_SELECTOR_PARAMETERS);\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        HLS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null),\n        trackSelectorParameters,\n        Util.getRendererCapabilities(renderersFactory, drmSessionManager));\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   * @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, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) {\n    return forSmoothStreaming(\n        uri,\n        dataSourceFactory,\n        renderersFactory,\n        /* drmSessionManager= */ null,\n        DEFAULT_TRACK_SELECTOR_PARAMETERS);\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        SS_FACTORY.createMediaSource(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    MediaSourceFactory factory;\n    switch (downloadRequest.type) {\n      case DownloadRequest.TYPE_DASH:\n        factory = DASH_FACTORY;\n        break;\n      case DownloadRequest.TYPE_SS:\n        factory = SS_FACTORY;\n        break;\n      case DownloadRequest.TYPE_HLS:\n        factory = HLS_FACTORY;\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 factory.createMediaSource(\n        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\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 = new DefaultTrackSelector(new DownloadTrackSelection.Factory());\n    this.rendererCapabilities = rendererCapabilities;\n    this.scratchSet = new SparseIntArray();\n    trackSelector.setParameters(trackSelectorParameters);\n    trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter());\n    callbackHandler = new Handler(Util.getLooper());\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.manifest;\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.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.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        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  private static MediaSourceFactory getMediaSourceFactory(String className) {\n    Constructor<?> constructor = null;\n    Method setStreamKeysMethod = null;\n    Method createMethod = null;\n    try {\n      // LINT.IfChange\n      Class<?> factoryClazz = Class.forName(className);\n      constructor = factoryClazz.getConstructor(Factory.class);\n      setStreamKeysMethod = factoryClazz.getMethod(\"setStreamKeys\", List.class);\n      createMethod = factoryClazz.getMethod(\"createMediaSource\", Uri.class);\n      // LINT.ThenChange(../../../../../../../../proguard-rules.txt)\n    } catch (ClassNotFoundException e) {\n      // Expected if the app was built without the respective module.\n    } catch (NoSuchMethodException | SecurityException e) {\n      // Something is wrong with the library or the proguard configuration.\n      throw new IllegalStateException(e);\n    }\n    return new MediaSourceFactory(constructor, setStreamKeysMethod, createMethod);\n  }\n\n  private static final class MediaSourceFactory {\n    @Nullable private final Constructor<?> constructor;\n    @Nullable private final Method setStreamKeysMethod;\n    @Nullable private final Method createMethod;\n\n    public MediaSourceFactory(\n        @Nullable Constructor<?> constructor,\n        @Nullable Method setStreamKeysMethod,\n        @Nullable Method createMethod) {\n      this.constructor = constructor;\n      this.setStreamKeysMethod = setStreamKeysMethod;\n      this.createMethod = createMethod;\n    }\n\n    private MediaSource createMediaSource(\n        Uri uri, Factory dataSourceFactory, @Nullable List<StreamKey> streamKeys) {\n      if (constructor == null || setStreamKeysMethod == null || createMethod == null) {\n        throw new IllegalStateException(\"Module missing to create media source.\");\n      }\n      try {\n        Object factory = constructor.newInstance(dataSourceFactory);\n        if (streamKeys != null) {\n          setStreamKeysMethod.invoke(factory, streamKeys);\n        }\n        return (MediaSource) Assertions.checkNotNull(createMethod.invoke(factory, uri));\n      } catch (Exception e) {\n        throw new IllegalStateException(\"Failed to instantiate media source.\", e);\n      }\n    }\n  }\n\n  private static final class MediaPreparer\n      implements MediaSource.SourceInfoRefreshListener, 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    @Nullable public Object manifest;\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(/* listener= */ 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.SourceInfoRefreshListener implementation.\n\n    @Override\n    public void onSourceInfoRefreshed(\n        MediaSource source, Timeline timeline, @Nullable Object manifest) {\n      if (this.timeline != null) {\n        // Ignore dynamic updates.\n        return;\n      }\n      this.timeline = timeline;\n      this.manifest = manifest;\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\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-amzn-2.10.6/library/core/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 java.io.IOException;\n\n/** An index of {@link Download Downloads}. */\npublic interface DownloadIndex {\n\n  /**\n   * Returns the {@link Download} with the given {@code id}, or null.\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   * @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-amzn-2.10.6/library/core/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        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      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      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        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      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    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      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        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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 Parcelable.Creator<DownloadRequest> CREATOR =\n      new Parcelable.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-amzn-2.10.6/library/core/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  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    String intentAction = null;\n    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    switch (intentAction) {\n      case ACTION_INIT:\n      case ACTION_RESTART:\n        // Do nothing.\n        break;\n      case ACTION_ADD_DOWNLOAD:\n        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        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 = downloadManagerListeners.get(getClass());\n    boolean unschedule = !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    private final Runnable updateRunnable;\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      this.updateRunnable = this::update;\n    }\n\n    public void startPeriodicUpdates() {\n      periodicUpdatesStarted = true;\n      update();\n    }\n\n    public void stopPeriodicUpdates() {\n      periodicUpdatesStarted = false;\n      handler.removeCallbacks(updateRunnable);\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 = downloadManager.getCurrentDownloads();\n      startForeground(notificationId, getForegroundNotification(downloads));\n      notificationDisplayed = true;\n      if (periodicUpdatesStarted) {\n        handler.removeCallbacks(updateRunnable);\n        handler.postDelayed(updateRunnable, 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(/* 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(/* enabled= */ !requirementsMet, requirements);\n      }\n    }\n\n    private void setSchedulerEnabled(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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.FileDataSourceFactory;\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 FileDataSourceFactory} will be 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 FileDataSourceFactory} will be 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 FileDataSourceFactory();\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 */\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-amzn-2.10.6/library/core/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 androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport android.util.Pair;\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.Assertions;\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.HashMap;\nimport java.util.List;\nimport java.util.NoSuchElementException;\nimport java.util.Queue;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ConcurrentLinkedQueue;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\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(@NonNull Segment other) {\n      return Util.compareLong(startTimeUs, other.startTimeUs);\n    }\n  }\n\n  private static final long MAX_MERGED_SEGMENT_START_TIME_DIFF_US = 20 * C.MICROS_PER_SECOND;\n\n  private final DataSpec manifestDataSpec;\n  private final Cache cache;\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  private final ResourcePoolProvider<CacheDataSource> cacheDataSources;\n  private final ResourcePoolProvider<byte[]> buffers;\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.offlineDataSource = constructorHelper.createOfflineCacheDataSource();\n    this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();\n    this.priorityTaskManager = constructorHelper.getPriorityTaskManager();\n    this.cacheDataSources = new ResourcePoolProvider<>(constructorHelper::createCacheDataSource);\n    this.buffers = new ResourcePoolProvider<>(() -> new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES]);\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    ExecutorService executorService = Executors.newFixedThreadPool(\n        Math.max(1, cacheKeyFactory.maxDownloadParallelSegments()));\n    priorityTaskManager.add(C.PRIORITY_DOWNLOAD);\n    try {\n      // Get the manifest and all of the segments.\n      List<Segment> segments;\n      CacheDataSource dataSource = cacheDataSources.obtain();\n      try {\n        M manifest = getManifest(dataSource, manifestDataSpec);\n        if (!streamKeys.isEmpty()) {\n          manifest = manifest.copy(streamKeys);\n        }\n        segments = getSegments(dataSource, manifest, /* allowIncompleteList= */\n            false);\n      } finally {\n        cacheDataSources.release(dataSource);\n      }\n\n      // Sort, merge and reassign segment cache key\n      Collections.sort(segments);\n      // TODO Merge segment can cause in playback with the current approach\n      //mergeSegments(segments, cacheKeyFactory);\n      reassignCacheKey(segments);\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\n      // Download the segments.\n      ProgressNotifier progressNotifier = null;\n      if (progressListener != null) {\n        progressNotifier =\n            new ProgressNotifier(\n                progressListener,\n                contentLength,\n                totalSegments,\n                bytesDownloaded,\n                segmentsDownloaded);\n      }\n      final int size = segments.size();\n      final AbortableCountDownLatch countDownLatch = new AbortableCountDownLatch(size);\n      final List<Future<SegmentDownloadTaskResult>> futures = new ArrayList<>(size);\n      for (Segment segment : segments) {\n        futures.add(\n            executorService.submit(\n                new SegmentDownloadTask<>(\n                    this,\n                    segment,\n                    progressNotifier,\n                    countDownLatch)));\n      }\n      countDownLatch.await();\n      executorService.shutdownNow();\n      for (Future<SegmentDownloadTaskResult> future : futures) {\n        try {\n          SegmentDownloadTaskResult result = future.get();\n          if (result.cause != null) {\n            throw result.cause;\n          }\n        } catch (ExecutionException ex) {\n          // ignore\n        }\n      }\n    } finally {\n      priorityTaskManager.remove(C.PRIORITY_DOWNLOAD);\n      executorService.shutdownNow();\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      reassignCacheKey(segments);\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 void mergeSegments(List<Segment> segments, CacheKeyFactory keyFactory) {\n    HashMap<String, Integer> lastIndexByCacheKey = new HashMap<>();\n    int nextOutIndex = 0;\n    for (int i = 0; i < segments.size(); i++) {\n      Segment segment = segments.get(i);\n      String cacheKey = keyFactory.buildCacheKey(segment.dataSpec);\n      @Nullable Integer lastIndex = lastIndexByCacheKey.get(cacheKey);\n      @Nullable Segment lastSegment = lastIndex == null ? null : segments.get(lastIndex);\n      if (lastSegment == null\n          || segment.startTimeUs > lastSegment.startTimeUs + MAX_MERGED_SEGMENT_START_TIME_DIFF_US\n          || !canMergeSegments(lastSegment.dataSpec, segment.dataSpec)) {\n        lastIndexByCacheKey.put(cacheKey, nextOutIndex);\n        segments.set(nextOutIndex, segment);\n        nextOutIndex++;\n      } else {\n        long mergedLength =\n            segment.dataSpec.length == C.LENGTH_UNSET\n                ? C.LENGTH_UNSET\n                : lastSegment.dataSpec.length + segment.dataSpec.length;\n        DataSpec mergedDataSpec = lastSegment.dataSpec.subrange(/* offset= */ 0, mergedLength);\n        segments.set(\n            Assertions.checkNotNull(lastIndex),\n            new Segment(lastSegment.startTimeUs, mergedDataSpec));\n      }\n    }\n    Util.removeRange(segments, /* fromIndex= */ nextOutIndex, /* toIndex= */ segments.size());\n  }\n\n  private static boolean canMergeSegments(DataSpec dataSpec1, DataSpec dataSpec2) {\n    return dataSpec1.uri.equals(dataSpec2.uri)\n        && dataSpec1.length != C.LENGTH_UNSET\n        && (dataSpec1.absoluteStreamPosition + dataSpec1.length == dataSpec2.absoluteStreamPosition)\n        && Util.areEqual(dataSpec1.key, dataSpec2.key)\n        && dataSpec1.flags == dataSpec2.flags\n        && dataSpec1.httpMethod == dataSpec2.httpMethod\n        && dataSpec1.httpRequestHeaders.equals(dataSpec2.httpRequestHeaders);\n  }\n\n  private void reassignCacheKey(List<Segment> segments) {\n    int size = segments.size();\n    for (int i = 0; i < size; i++) {\n      Segment segment = segments.get(i);\n      if (segment.dataSpec.key == null) {\n        DataSpec dataSpec = new DataSpec(\n            segment.dataSpec.uri,\n            segment.dataSpec.httpMethod,\n            segment.dataSpec.httpBody,\n            segment.dataSpec.absoluteStreamPosition,\n            segment.dataSpec.position,\n            segment.dataSpec.length,\n            cacheKeyFactory.buildCacheKey(segment.dataSpec),\n            segment.dataSpec.flags,\n            segment.dataSpec.httpRequestHeaders);\n        segments.set(i, new Segment(segment.startTimeUs, dataSpec));\n      }\n    }\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    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    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  private static final class SegmentDownloadTaskResult {\n    public final Segment segment;\n    public final IOException cause;\n\n    public SegmentDownloadTaskResult(Segment segment, IOException cause) {\n      this.segment = segment;\n      this.cause = cause;\n    }\n  }\n\n  private static final class SegmentDownloadTask<M extends FilterableManifest<M>> implements Callable<SegmentDownloadTaskResult> {\n    private final SegmentDownloader<M> downloader;\n    private final Segment segment;\n    private final ProgressNotifier progressNotifier;\n    private final AbortableCountDownLatch countDownLatch;\n\n    SegmentDownloadTask(\n        SegmentDownloader<M> downloader,\n        Segment segment,\n        ProgressNotifier progressNotifier,\n        AbortableCountDownLatch countDownLatch) {\n      this.downloader = downloader;\n      this.segment = segment;\n      this.progressNotifier = progressNotifier;\n      this.countDownLatch = countDownLatch;\n    }\n\n    @Override\n    public SegmentDownloadTaskResult call() {\n      boolean abort = false;\n      try {\n        byte[] buffer = downloader.buffers.obtain();\n        CacheDataSource cacheDataSource = downloader.cacheDataSources.obtain();\n        try {\n          CacheUtil.cache(\n              segment.dataSpec,\n              downloader.cache,\n              downloader.cacheKeyFactory,\n              cacheDataSource,\n              buffer,\n              downloader.priorityTaskManager,\n              C.PRIORITY_DOWNLOAD,\n              progressNotifier,\n              downloader.isCanceled,\n              true);\n          if (progressNotifier != null) {\n            progressNotifier.onSegmentDownloaded();\n          }\n          return new SegmentDownloadTaskResult(segment, null);\n        } finally {\n          downloader.cacheDataSources.release(cacheDataSource);\n          downloader.buffers.release(buffer);\n        }\n      } catch (Exception ex) {\n        abort = true;\n        if (ex instanceof IOException) {\n          return new SegmentDownloadTaskResult(segment, (IOException) ex);\n        }\n        return new SegmentDownloadTaskResult(segment, new IOException(ex));\n      } finally {\n        countDownLatch.countDown();\n        if (abort) {\n          countDownLatch.abort();\n        }\n      }\n    }\n  }\n\n  private static class AbortableCountDownLatch extends CountDownLatch {\n    public AbortableCountDownLatch(int count) {\n      super(count);\n    }\n\n    public void abort() {\n      while(getCount() > 0) {\n        countDown();\n      }\n    }\n  }\n\n  private static class ResourcePoolProvider<T> {\n    private final Queue<T> pool = new ConcurrentLinkedQueue<>();\n    private final ResourceProvider<T> provider;\n\n    ResourcePoolProvider(ResourceProvider<T> provider) {\n      this.provider = provider;\n    }\n\n    public T obtain() {\n      try {\n        return pool.remove();\n      } catch (NoSuchElementException ex) {\n        // create a new resource\n      }\n      return provider.provide();\n    }\n\n    public void release(T resource) {\n      pool.add(resource);\n    }\n  }\n\n  private interface ResourceProvider<T> {\n    T provide();\n  }\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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.NonNull;\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(@NonNull 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 Parcelable.Creator<StreamKey> CREATOR =\n      new Parcelable.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-amzn-2.10.6/library/core/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 java.io.IOException;\n\n/** A writable index of {@link Download Downloads}. */\npublic interface WritableDownloadIndex extends DownloadIndex {\n\n  /**\n   * Adds or replaces a {@link Download}.\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   * @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   * @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   * @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   * @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   * @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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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(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 Parcelable.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-amzn-2.10.6/library/core/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.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  private DeviceStatusChangeReceiver receiver;\n\n  @Requirements.RequirementFlags private int notMetRequirements;\n  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(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(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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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;\n\n/**\n * Abstract base class for the concatenation of one or more {@link Timeline}s.\n */\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  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  public static Object getChildPeriodUidFromConcatenatedUid(Object concatenatedUid) {\n    return ((Pair<?, ?>) concatenatedUid).second;\n  }\n\n  /**\n   * Returns concatenated UID for a period in a child timeline.\n   *\n   * @param childTimelineUid UID of the child timeline this period belongs to.\n   * @param childPeriodUid UID of the period in the child timeline.\n   * @return UID of the period in the concatenated timeline.\n   */\n  public static Object getConcatenatedUid(Object childTimelineUid, Object childPeriodUid) {\n    return Pair.create(childTimelineUid, childPeriodUid);\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(int windowIndex, @Player.RepeatMode int repeatMode,\n      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 = getTimelineByChildIndex(childIndex).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(int windowIndex, @Player.RepeatMode int repeatMode,\n      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 = getTimelineByChildIndex(childIndex).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(\n      int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {\n    int childIndex = getChildIndexByWindowIndex(windowIndex);\n    int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);\n    int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);\n    getTimelineByChildIndex(childIndex)\n        .getWindow(\n            windowIndex - firstWindowIndexInChild, window, setTag, defaultPositionProjectionUs);\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).getPeriod(periodIndex - firstPeriodIndexInChild, period,\n        setIds);\n    period.windowIndex += firstWindowIndexInChild;\n    if (setIds) {\n      period.uid = getConcatenatedUid(getChildUidByChildIndex(childIndex), 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 ? 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 ? 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 ? shuffleOrder.getPreviousIndex(childIndex)\n        : childIndex > 0 ? childIndex - 1 : C.INDEX_UNSET;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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;\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 and/or manifest, it must\n * call {@link #refreshSourceInfo(Timeline, Object)} to notify all listeners.\n */\npublic abstract class BaseMediaSource implements MediaSource {\n\n  private final ArrayList<SourceInfoRefreshListener> sourceInfoListeners;\n  private final MediaSourceEventListener.EventDispatcher eventDispatcher;\n\n  @Nullable private Looper looper;\n  @Nullable private Timeline timeline;\n  @Nullable private Object manifest;\n\n  public BaseMediaSource() {\n    sourceInfoListeners = new ArrayList<>(/* initialCapacity= */ 1);\n    eventDispatcher = new MediaSourceEventListener.EventDispatcher();\n  }\n\n  /**\n   * Starts source preparation. 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  /**\n   * Releases the source. This method is called exactly once after each call to {@link\n   * #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   * @param manifest The new manifest. May be null.\n   */\n  protected final void refreshSourceInfo(Timeline timeline, @Nullable Object manifest) {\n    this.timeline = timeline;\n    this.manifest = manifest;\n    for (SourceInfoRefreshListener listener : sourceInfoListeners) {\n      listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest);\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  @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      SourceInfoRefreshListener listener,\n      @Nullable TransferListener mediaTransferListener) {\n    Looper looper = Looper.myLooper();\n    Assertions.checkArgument(this.looper == null || this.looper == looper);\n    sourceInfoListeners.add(listener);\n    if (this.looper == null) {\n      this.looper = looper;\n      prepareSourceInternal(mediaTransferListener);\n    } else if (timeline != null) {\n      listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest);\n    }\n  }\n\n  @Override\n  public final void releaseSource(SourceInfoRefreshListener listener) {\n    sourceInfoListeners.remove(listener);\n    if (sourceInfoListeners.isEmpty()) {\n      looper = null;\n      timeline = null;\n      manifest = null;\n      releaseSourceInternal();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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;\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  private MediaPeriod.Callback callback;\n  private 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(MediaPeriod.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(TrackSelection[] selections, boolean[] mayRetainStreamFlags,\n      SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {\n    sampleStreams = new ClippingSampleStream[streams.length];\n    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 (streams[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  // MediaPeriod.Callback implementation.\n\n  @Override\n  public void onPrepared(MediaPeriod mediaPeriod) {\n    callback.onPrepared(this);\n  }\n\n  @Override\n  public void onContinueLoadingRequested(MediaPeriod source) {\n    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(long startUs, 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 = 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\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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  private @Nullable Object manifest;\n  private ClippingTimeline clippingTimeline;\n  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  public 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(clippingTimeline.timeline);\n    }\n  }\n\n  @Override\n  public void releaseSourceInternal() {\n    super.releaseSourceInternal();\n    clippingError = null;\n    clippingTimeline = null;\n  }\n\n  @Override\n  protected void onChildSourceInfoRefreshed(\n      Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) {\n    if (clippingError != null) {\n      return;\n    }\n    this.manifest = manifest;\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, manifest);\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(\n        int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {\n      timeline.getWindow(\n          /* windowIndex= */ 0, window, setTag, /* 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-amzn-2.10.6/library/core/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  private @Nullable Handler eventHandler;\n  private @Nullable TransferListener mediaTransferListener;\n\n  /** Create composite media source without child sources. */\n  protected CompositeMediaSource() {\n    childSources = new HashMap<>();\n  }\n\n  @Override\n  @CallSuper\n  public 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  public void releaseSourceInternal() {\n    for (MediaSourceAndListener childSource : childSources.values()) {\n      childSource.mediaSource.releaseSource(childSource.listener);\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   * @param manifest The manifest of the child source.\n   */\n  protected abstract void onChildSourceInfoRefreshed(\n      T id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest);\n\n  /**\n   * Prepares a child source.\n   *\n   * <p>{@link #onChildSourceInfoRefreshed(Object, MediaSource, Timeline, Object)} will be called\n   * when the child source updates its timeline and/or manifest with the same {@code id} passed to\n   * 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    SourceInfoRefreshListener sourceListener =\n        (source, timeline, manifest) -> onChildSourceInfoRefreshed(id, source, timeline, manifest);\n    MediaSourceEventListener eventListener = new ForwardingEventListener(id);\n    childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener, eventListener));\n    mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener);\n    mediaSource.prepareSource(sourceListener, mediaTransferListener);\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.listener);\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  private static final class MediaSourceAndListener {\n\n    public final MediaSource mediaSource;\n    public final SourceInfoRefreshListener listener;\n    public final MediaSourceEventListener eventListener;\n\n    public MediaSourceAndListener(\n        MediaSource mediaSource,\n        SourceInfoRefreshListener listener,\n        MediaSourceEventListener eventListener) {\n      this.mediaSource = mediaSource;\n      this.listener = listener;\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        eventDispatcher.mediaPeriodCreated();\n      }\n    }\n\n    @Override\n    public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {\n      if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {\n        eventDispatcher.mediaPeriodReleased();\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-amzn-2.10.6/library/core/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}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.NonNull;\nimport androidx.annotation.Nullable;\nimport android.util.Pair;\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.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 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 boolean isAtomic;\n  private final boolean useLazyPreparation;\n  private final Timeline.Window window;\n  private final Timeline.Period period;\n\n  private boolean timelineUpdateScheduled;\n  private Set<HandlerAndRunnable> nextTimelineUpdateOnCompletionActions;\n  private ShuffleOrder shuffleOrder;\n  private int windowCount;\n  private int periodCount;\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.isAtomic = isAtomic;\n    this.useLazyPreparation = useLazyPreparation;\n    window = new Timeline.Window();\n    period = new Timeline.Period();\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 final 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 final 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 final 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 final 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 final 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 final 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 final 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 final 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   */\n  public final synchronized void removeMediaSource(int index) {\n    removePublicMediaSources(index, index + 1, /* handler= */ null, /* onCompletionAction= */ null);\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   */\n  public final synchronized void removeMediaSource(\n      int index, Handler handler, Runnable onCompletionAction) {\n    removePublicMediaSources(index, index + 1, handler, onCompletionAction);\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 final 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 final 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 final 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 final 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 final 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 final 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 final 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 final 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 final 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 final 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  public final 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  @Override\n  @SuppressWarnings(\"MissingSuperCall\")\n  public void maybeThrowSourceInfoRefreshError() throws IOException {\n    // Do nothing. Source info refresh errors of the individual sources will be thrown when calling\n    // DeferredMediaPeriod.maybeThrowPrepareError.\n  }\n\n  @Override\n  public final MediaPeriod createPeriod(\n      MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    Object mediaSourceHolderUid = getMediaSourceHolderUid(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());\n      holder.hasStartedPreparing = true;\n    }\n    DeferredMediaPeriod mediaPeriod =\n        new DeferredMediaPeriod(holder.mediaSource, id, allocator, startPositionUs);\n    mediaSourceByMediaPeriod.put(mediaPeriod, holder);\n    holder.activeMediaPeriods.add(mediaPeriod);\n    if (!holder.hasStartedPreparing) {\n      holder.hasStartedPreparing = true;\n      prepareChildSource(holder, holder.mediaSource);\n    } else if (holder.isPrepared) {\n      MediaPeriodId idInSource = id.copyWithPeriodUid(getChildPeriodUid(holder, id.periodUid));\n      mediaPeriod.createPeriod(idInSource);\n    }\n    return mediaPeriod;\n  }\n\n  @Override\n  public final void releasePeriod(MediaPeriod mediaPeriod) {\n    MediaSourceHolder holder =\n        Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));\n    ((DeferredMediaPeriod) mediaPeriod).releasePeriod();\n    holder.activeMediaPeriods.remove(mediaPeriod);\n    maybeReleaseChildSource(holder);\n  }\n\n  @Override\n  public final synchronized void releaseSourceInternal() {\n    super.releaseSourceInternal();\n    mediaSourceHolders.clear();\n    mediaSourceByUid.clear();\n    shuffleOrder = shuffleOrder.cloneAndClear();\n    windowCount = 0;\n    periodCount = 0;\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 final void onChildSourceInfoRefreshed(\n      MediaSourceHolder mediaSourceHolder,\n      MediaSource mediaSource,\n      Timeline timeline,\n      @Nullable Object manifest) {\n    updateMediaSourceInternal(mediaSourceHolder, timeline);\n  }\n\n  @Override\n  protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(\n      MediaSourceHolder mediaSourceHolder, MediaPeriodId mediaPeriodId) {\n    for (int i = 0; i < mediaSourceHolder.activeMediaPeriods.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.activeMediaPeriods.get(i).id.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));\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(\n        new ConcatenatedTimeline(\n            mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic),\n        /* manifest= */ null);\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      newMediaSourceHolder.reset(\n          newIndex,\n          previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(),\n          previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount());\n    } else {\n      newMediaSourceHolder.reset(\n          newIndex, /* firstWindowIndexInChild= */ 0, /* firstPeriodIndexInChild= */ 0);\n    }\n    correctOffsets(\n        newIndex,\n        /* childIndexUpdate= */ 1,\n        newMediaSourceHolder.timeline.getWindowCount(),\n        newMediaSourceHolder.timeline.getPeriodCount());\n    mediaSourceHolders.add(newIndex, newMediaSourceHolder);\n    mediaSourceByUid.put(newMediaSourceHolder.uid, newMediaSourceHolder);\n    if (!useLazyPreparation) {\n      newMediaSourceHolder.hasStartedPreparing = true;\n      prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource);\n    }\n  }\n\n  private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) {\n    if (mediaSourceHolder == null) {\n      throw new IllegalArgumentException();\n    }\n    DeferredTimeline deferredTimeline = mediaSourceHolder.timeline;\n    if (deferredTimeline.getTimeline() == timeline) {\n      return;\n    }\n    int windowOffsetUpdate = timeline.getWindowCount() - deferredTimeline.getWindowCount();\n    int periodOffsetUpdate = timeline.getPeriodCount() - deferredTimeline.getPeriodCount();\n    if (windowOffsetUpdate != 0 || periodOffsetUpdate != 0) {\n      correctOffsets(\n          mediaSourceHolder.childIndex + 1,\n          /* childIndexUpdate= */ 0,\n          windowOffsetUpdate,\n          periodOffsetUpdate);\n    }\n    if (mediaSourceHolder.isPrepared) {\n      mediaSourceHolder.timeline = deferredTimeline.cloneWithUpdatedTimeline(timeline);\n    } else if (timeline.isEmpty()) {\n      mediaSourceHolder.timeline =\n          DeferredTimeline.createWithRealTimeline(timeline, DeferredTimeline.DUMMY_ID);\n    } else {\n      // We should have at most one deferred media period for the DummyTimeline because the duration\n      // is 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      Assertions.checkState(mediaSourceHolder.activeMediaPeriods.size() <= 1);\n      DeferredMediaPeriod deferredMediaPeriod =\n          mediaSourceHolder.activeMediaPeriods.isEmpty()\n              ? null\n              : mediaSourceHolder.activeMediaPeriods.get(0);\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      timeline.getWindow(/* windowIndex= */ 0, window);\n      long windowStartPositionUs = window.getDefaultPositionUs();\n      if (deferredMediaPeriod != null) {\n        long periodPreparePositionUs = deferredMediaPeriod.getPreparePositionUs();\n        if (periodPreparePositionUs != 0) {\n          windowStartPositionUs = periodPreparePositionUs;\n        }\n      }\n      Pair<Object, Long> periodPosition =\n          timeline.getPeriodPosition(window, period, /* windowIndex= */ 0, windowStartPositionUs);\n      Object periodUid = periodPosition.first;\n      long periodPositionUs = periodPosition.second;\n      mediaSourceHolder.timeline = DeferredTimeline.createWithRealTimeline(timeline, periodUid);\n      if (deferredMediaPeriod != null) {\n        deferredMediaPeriod.overridePreparePositionUs(periodPositionUs);\n        MediaPeriodId idInSource =\n            deferredMediaPeriod.id.copyWithPeriodUid(\n                getChildPeriodUid(mediaSourceHolder, deferredMediaPeriod.id.periodUid));\n        deferredMediaPeriod.createPeriod(idInSource);\n      }\n    }\n    mediaSourceHolder.isPrepared = true;\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.timeline;\n    correctOffsets(\n        index,\n        /* childIndexUpdate= */ -1,\n        -oldTimeline.getWindowCount(),\n        -oldTimeline.getPeriodCount());\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    int periodOffset = mediaSourceHolders.get(startIndex).firstPeriodIndexInChild;\n    mediaSourceHolders.add(newIndex, mediaSourceHolders.remove(currentIndex));\n    for (int i = startIndex; i <= endIndex; i++) {\n      MediaSourceHolder holder = mediaSourceHolders.get(i);\n      holder.firstWindowIndexInChild = windowOffset;\n      holder.firstPeriodIndexInChild = periodOffset;\n      windowOffset += holder.timeline.getWindowCount();\n      periodOffset += holder.timeline.getPeriodCount();\n    }\n  }\n\n  private void correctOffsets(\n      int startIndex, int childIndexUpdate, int windowOffsetUpdate, int periodOffsetUpdate) {\n    windowCount += windowOffsetUpdate;\n    periodCount += periodOffsetUpdate;\n    for (int i = startIndex; i < mediaSourceHolders.size(); i++) {\n      mediaSourceHolders.get(i).childIndex += childIndexUpdate;\n      mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate;\n      mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate;\n    }\n  }\n\n  private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {\n    // Release if the source has been removed from the playlist, but only if it has been previously\n    // prepared and only if we are not waiting for an existing media period to be released.\n    if (mediaSourceHolder.isRemoved\n        && mediaSourceHolder.hasStartedPreparing\n        && mediaSourceHolder.activeMediaPeriods.isEmpty()) {\n      releaseChildSource(mediaSourceHolder);\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(MediaSourceHolder holder, Object periodUid) {\n    Object childUid = ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid);\n    return childUid.equals(DeferredTimeline.DUMMY_ID) ? holder.timeline.replacedId : childUid;\n  }\n\n  private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) {\n    if (holder.timeline.replacedId.equals(childPeriodUid)) {\n      childPeriodUid = DeferredTimeline.DUMMY_ID;\n    }\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 implements Comparable<MediaSourceHolder> {\n\n    public final MediaSource mediaSource;\n    public final Object uid;\n    public final List<DeferredMediaPeriod> activeMediaPeriods;\n\n    public DeferredTimeline timeline;\n    public int childIndex;\n    public int firstWindowIndexInChild;\n    public int firstPeriodIndexInChild;\n    public boolean hasStartedPreparing;\n    public boolean isPrepared;\n    public boolean isRemoved;\n\n    public MediaSourceHolder(MediaSource mediaSource) {\n      this.mediaSource = mediaSource;\n      this.timeline = DeferredTimeline.createWithDummyTimeline(mediaSource.getTag());\n      this.activeMediaPeriods = new ArrayList<>();\n      this.uid = new Object();\n    }\n\n    public void reset(int childIndex, int firstWindowIndexInChild, int firstPeriodIndexInChild) {\n      this.childIndex = childIndex;\n      this.firstWindowIndexInChild = firstWindowIndexInChild;\n      this.firstPeriodIndexInChild = firstPeriodIndexInChild;\n      this.hasStartedPreparing = false;\n      this.isPrepared = false;\n      this.isRemoved = false;\n      this.activeMediaPeriods.clear();\n    }\n\n    @Override\n    public int compareTo(@NonNull MediaSourceHolder other) {\n      return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild;\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        int windowCount,\n        int periodCount,\n        ShuffleOrder shuffleOrder,\n        boolean isAtomic) {\n      super(isAtomic, shuffleOrder);\n      this.windowCount = windowCount;\n      this.periodCount = periodCount;\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      for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {\n        timelines[index] = mediaSourceHolder.timeline;\n        firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild;\n        firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild;\n        uids[index] = mediaSourceHolder.uid;\n        childIndexByUid.put(uids[index], index++);\n      }\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  /**\n   * Timeline used as placeholder for an unprepared media source. After preparation, a\n   * DeferredTimeline is used to keep the originally assigned dummy period ID.\n   */\n  private static final class DeferredTimeline extends ForwardingTimeline {\n\n    private static final Object DUMMY_ID = new Object();\n\n    private final Object replacedId;\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 DeferredTimeline createWithDummyTimeline(@Nullable Object windowTag) {\n      return new DeferredTimeline(new DummyTimeline(windowTag), DUMMY_ID);\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 firstPeriodUid The period UID in the timeline which will be replaced by the already\n     *     assigned dummy period UID.\n     */\n    public static DeferredTimeline createWithRealTimeline(\n        Timeline timeline, Object firstPeriodUid) {\n      return new DeferredTimeline(timeline, firstPeriodUid);\n    }\n\n    private DeferredTimeline(Timeline timeline, Object replacedId) {\n      super(timeline);\n      this.replacedId = replacedId;\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 DeferredTimeline cloneWithUpdatedTimeline(Timeline timeline) {\n      return new DeferredTimeline(timeline, replacedId);\n    }\n\n    /** Returns wrapped timeline. */\n    public Timeline getTimeline() {\n      return timeline;\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, replacedId)) {\n        period.uid = DUMMY_ID;\n      }\n      return period;\n    }\n\n    @Override\n    public int getIndexOfPeriod(Object uid) {\n      return timeline.getIndexOfPeriod(DUMMY_ID.equals(uid) ? replacedId : uid);\n    }\n\n    @Override\n    public Object getUidOfPeriod(int periodIndex) {\n      Object uid = timeline.getUidOfPeriod(periodIndex);\n      return Util.areEqual(uid, replacedId) ? DUMMY_ID : 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(\n        int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {\n      return window.set(\n          tag,\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          /* 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= */ DeferredTimeline.DUMMY_ID,\n          /* windowIndex= */ 0,\n          /* durationUs = */ C.TIME_UNSET,\n          /* positionInWindowUs= */ 0);\n    }\n\n    @Override\n    public int getIndexOfPeriod(Object uid) {\n      return uid == DeferredTimeline.DUMMY_ID ? 0 : C.INDEX_UNSET;\n    }\n\n    @Override\n    public Object getUidOfPeriod(int periodIndex) {\n      return DeferredTimeline.DUMMY_ID;\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport java.io.IOException;\n\n/**\n * A {@link MediaSourceEventListener} allowing selective overrides. All methods are implemented as\n * no-ops.\n */\npublic abstract class DefaultMediaSourceEventListener implements MediaSourceEventListener {\n\n  @Override\n  public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {\n    // Do nothing.\n  }\n\n  @Override\n  public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {\n    // Do nothing.\n  }\n\n  @Override\n  public void onLoadStarted(\n      int windowIndex,\n      @Nullable MediaPeriodId mediaPeriodId,\n      LoadEventInfo loadEventInfo,\n      MediaLoadData mediaLoadData) {\n    // Do nothing.\n  }\n\n  @Override\n  public void onLoadCompleted(\n      int windowIndex,\n      @Nullable MediaPeriodId mediaPeriodId,\n      LoadEventInfo loadEventInfo,\n      MediaLoadData mediaLoadData) {\n    // Do nothing.\n  }\n\n  @Override\n  public void onLoadCanceled(\n      int windowIndex,\n      @Nullable MediaPeriodId mediaPeriodId,\n      LoadEventInfo loadEventInfo,\n      MediaLoadData mediaLoadData) {\n    // Do nothing.\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    // Do nothing.\n  }\n\n  @Override\n  public void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {\n    // Do nothing.\n  }\n\n  @Override\n  public void onUpstreamDiscarded(\n      int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {\n    // Do nothing.\n  }\n\n  @Override\n  public void onDownstreamFormatChanged(\n      int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {\n    // Do nothing.\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.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 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;\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 DeferredMediaPeriod 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 deferred media period. */\n  public final MediaPeriodId id;\n\n  private final Allocator allocator;\n\n  private MediaPeriod mediaPeriod;\n  private Callback callback;\n  private long preparePositionUs;\n  private @Nullable PrepareErrorListener listener;\n  private boolean notifiedPrepareError;\n  private long preparePositionOverrideUs;\n\n  /**\n   * Creates a new deferred media period.\n   *\n   * @param mediaSource The media source to wrap.\n   * @param id The identifier used to create the deferred 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 DeferredMediaPeriod(\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 deferred 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 mediaPeriod.getTrackGroups();\n  }\n\n  @Override\n  public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,\n      SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {\n    if (preparePositionOverrideUs != C.TIME_UNSET && positionUs == preparePositionUs) {\n      positionUs = preparePositionOverrideUs;\n      preparePositionOverrideUs = C.TIME_UNSET;\n    }\n    return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags,\n        positionUs);\n  }\n\n  @Override\n  public void discardBuffer(long positionUs, boolean toKeyframe) {\n    mediaPeriod.discardBuffer(positionUs, toKeyframe);\n  }\n\n  @Override\n  public long readDiscontinuity() {\n    return mediaPeriod.readDiscontinuity();\n  }\n\n  @Override\n  public long getBufferedPositionUs() {\n    return mediaPeriod.getBufferedPositionUs();\n  }\n\n  @Override\n  public long seekToUs(long positionUs) {\n    return mediaPeriod.seekToUs(positionUs);\n  }\n\n  @Override\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    return mediaPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters);\n  }\n\n  @Override\n  public long getNextLoadPositionUs() {\n    return mediaPeriod.getNextLoadPositionUs();\n  }\n\n  @Override\n  public void reevaluateBuffer(long positionUs) {\n    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 void onContinueLoadingRequested(MediaPeriod source) {\n    callback.onContinueLoadingRequested(this);\n  }\n\n  // MediaPeriod.Callback implementation\n\n  @Override\n  public void onPrepared(MediaPeriod mediaPeriod) {\n    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-amzn-2.10.6/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.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/** @deprecated Use {@link ConcatenatingMediaSource} instead. */\n@Deprecated\npublic final class DynamicConcatenatingMediaSource extends ConcatenatingMediaSource {\n\n  /**\n   * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(MediaSource...)}\n   *     instead.\n   */\n  @Deprecated\n  public DynamicConcatenatingMediaSource() {}\n\n  /**\n   * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean,\n   *     MediaSource...)} instead.\n   */\n  @Deprecated\n  public DynamicConcatenatingMediaSource(boolean isAtomic) {\n    super(isAtomic);\n  }\n\n  /**\n   * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean, ShuffleOrder,\n   *     MediaSource...)} instead.\n   */\n  @Deprecated\n  public DynamicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) {\n    super(isAtomic, shuffleOrder);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.extractor.DefaultExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.source.ads.AdsMediaSource;\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 BaseMediaSource\n    implements MediaSource.SourceInfoRefreshListener {\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  /** Use {@link ProgressiveMediaSource.Factory} instead. */\n  @Deprecated\n  public static final class Factory implements AdsMediaSource.MediaSourceFactory {\n\n    private final DataSource.Factory dataSourceFactory;\n\n    private @Nullable ExtractorsFactory extractorsFactory;\n    private @Nullable String customCacheKey;\n    private @Nullable 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  @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      Handler eventHandler,\n      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      Handler eventHandler,\n      EventListener eventListener,\n      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      Handler eventHandler,\n      EventListener eventListener,\n      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            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  public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    progressiveMediaSource.prepareSource(/* listener= */ this, mediaTransferListener);\n  }\n\n  @Override\n  public void maybeThrowSourceInfoRefreshError() throws IOException {\n    progressiveMediaSource.maybeThrowSourceInfoRefreshError();\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  @Override\n  public void releaseSourceInternal() {\n    progressiveMediaSource.releaseSource(/* listener= */ this);\n  }\n\n  @Override\n  public void onSourceInfoRefreshed(\n      MediaSource source, Timeline timeline, @Nullable Object manifest) {\n    refreshSourceInfo(timeline, manifest);\n  }\n\n  @Deprecated\n  private static final class EventListenerWrapper extends DefaultMediaSourceEventListener {\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-amzn-2.10.6/library/core/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(\n      int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {\n    return timeline.getWindow(windowIndex, window, setTag, 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-amzn-2.10.6/library/core/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 it's 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-amzn-2.10.6/library/core/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  public 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(\n      Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) {\n    Timeline loopingTimeline =\n        loopCount != Integer.MAX_VALUE\n            ? new LoopingTimeline(timeline, loopCount)\n            : new InfinitelyLoopingTimeline(timeline);\n    refreshSourceInfo(loopingTimeline, manifest);\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-amzn-2.10.6/library/core/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.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\n   * MediaSource.SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object)}\n   * will be 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 received {@link TrackSelection TrackSelections} are no longer valid and\n   * references need to be replaced even if the corresponding {@link SampleStream} is kept.\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 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. 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  /**\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-amzn-2.10.6/library/core/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.ExoPlayer;\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 SourceInfoRefreshListener#onSourceInfoRefreshed}\n *       on the {@link SourceInfoRefreshListener}s passed to {@link\n *       #prepareSource(SourceInfoRefreshListener, 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  /** Listener for source events. */\n  interface SourceInfoRefreshListener {\n\n    /**\n     * Called when manifest and/or timeline has been refreshed.\n     * <p>\n     * 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     * @param manifest The loaded manifest. May be null.\n     */\n    void onSourceInfoRefreshed(MediaSource source, Timeline timeline, @Nullable Object manifest);\n\n  }\n\n  /**\n   * Identifier for a {@link MediaPeriod}.\n   */\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   * Starts source preparation if not yet started, and adds a listener for timeline and/or manifest\n   * updates.\n   *\n   * <p>Should not be called directly from application code.\n   *\n   * <p>The listener will be also be notified if the source already has a timeline and/or manifest.\n   *\n   * <p>For each call to this method, a call to {@link #releaseSource(SourceInfoRefreshListener)} is\n   * needed to remove the listener and to release the source if no longer required.\n   *\n   * @param listener The listener to be added.\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(\n      SourceInfoRefreshListener listener, @Nullable TransferListener mediaTransferListener);\n\n  /**\n   * Throws any pending error encountered while loading or refreshing source information.\n   * <p>\n   * Should not be called directly from application code.\n   */\n  void maybeThrowSourceInfoRefreshError() throws IOException;\n\n  /**\n   * Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called\n   * multiple times without an intervening call to {@link #releasePeriod(MediaPeriod)}.\n   *\n   * <p>Should not be called directly from application code.\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   * <p>\n   * 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   * Removes a listener for timeline and/or manifest updates and releases the source if no longer\n   * required.\n   *\n   * <p>Should not be called directly from application code.\n   *\n   * @param listener The listener to be removed.\n   */\n  void releaseSource(SourceInfoRefreshListener listener);\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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    public final @Nullable 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    public final @Nullable 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  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  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  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  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  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  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  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  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  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    public final @Nullable 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-amzn-2.10.6/library/core/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 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;\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  private Callback callback;\n  private TrackGroupArray trackGroups;\n\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  }\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 trackGroups;\n  }\n\n  @Override\n  public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,\n      SampleStream[] streams, boolean[] streamResetFlags, 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    SampleStream[] newStreams = new SampleStream[selections.length];\n    SampleStream[] childStreams = new SampleStream[selections.length];\n    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          Assertions.checkState(childStreams[j] != null);\n          newStreams[j] = childStreams[j];\n          periodEnabled = true;\n          streamPeriodIndices.put(childStreams[j], 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 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    return enabledPeriods[0].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    callback.onPrepared(this);\n  }\n\n  @Override\n  public void onContinueLoadingRequested(MediaPeriod ignored) {\n    callback.onContinueLoadingRequested(this);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 Object primaryManifest;\n  private int periodCount;\n  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  public 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  public void releaseSourceInternal() {\n    super.releaseSourceInternal();\n    Arrays.fill(timelines, null);\n    primaryManifest = 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, @Nullable Object manifest) {\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 (mediaSource == mediaSources[0]) {\n      primaryManifest = manifest;\n    }\n    if (pendingTimelineSources.isEmpty()) {\n      refreshSourceInfo(timelines[0], primaryManifest);\n    }\n  }\n\n  @Override\n  protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(\n      Integer id, MediaPeriodId mediaPeriodId) {\n    return id == 0 ? mediaPeriodId : null;\n  }\n\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-amzn-2.10.6/library/core/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.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 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 or ability to seek within the period changes.\n     *\n     * @param durationUs The duration of the period, or {@link C#TIME_UNSET}.\n     * @param isSeekable Whether the period is seekable.\n     */\n    void onSourceInfoRefreshed(long durationUs, boolean isSeekable);\n\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 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 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\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      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.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.discardToEnd();\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.reset();\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 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() && (loadingFinished || sampleQueues[track].hasNextSample());\n  }\n\n  /* package */ void maybeThrowError() throws IOException {\n    loader.maybeThrowError(loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType));\n  }\n\n  /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer,\n      boolean formatRequired) {\n    if (suppressRead()) {\n      return C.RESULT_NOTHING_READ;\n    }\n    maybeNotifyDownstreamFormat(track);\n    int result =\n        sampleQueues[track].read(\n            formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs);\n    if (result == C.RESULT_NOTHING_READ) {\n      maybeStartDeferredRetry(track);\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].hasNextSample()) {\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);\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);\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    dataType =\n        length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET\n            ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE\n            : C.DATA_TYPE_MEDIA;\n    preparedState =\n        new PreparedState(seekMap, new TrackGroupArray(trackArray), trackIsAudioVideoFlags);\n    prepared = true;\n    listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable());\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();\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_ALLOW_ICY_METADATA\n              | DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN\n              | DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION);\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    private @Nullable 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"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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.extractor.DefaultExtractorsFactory;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.ExtractorsFactory;\nimport com.google.android.exoplayer2.source.ads.AdsMediaSource;\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 AdsMediaSource.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 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      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     * @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 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 {@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 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 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          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 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  @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      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.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  public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    transferListener = mediaTransferListener;\n    notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable);\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        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  public void releaseSourceInternal() {\n    // Do nothing.\n  }\n\n  // ProgressiveMediaPeriod.Listener implementation.\n\n  @Override\n  public void onSourceInfoRefreshed(long durationUs, boolean isSeekable) {\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 && timelineIsSeekable == isSeekable) {\n      // Suppress no-op source info changes.\n      return;\n    }\n    notifySourceInfoRefreshed(durationUs, isSeekable);\n  }\n\n  // Internal methods.\n\n  private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) {\n    timelineDurationUs = durationUs;\n    timelineIsSeekable = isSeekable;\n    // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223.\n    refreshSourceInfo(\n        new SinglePeriodTimeline(\n            timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, tag),\n        /* manifest= */ null);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 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.extractor.TrackOutput.CryptoData;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\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 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 int upstreamSourceId;\n\n  public SampleMetadataQueue() {\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  /**\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    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   * Returns the current absolute start index.\n   */\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 int peekSourceId() {\n    int relativeReadIndex = getRelativeIndex(readPosition);\n    return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId;\n  }\n\n  /**\n   * Returns whether a sample is available to be read.\n   */\n  public synchronized boolean hasNextSample() {\n    return readPosition != length;\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   * 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 downstreamFormat The current downstream {@link Format}. If the format of the next sample\n   *     is different to the current downstream format then a format will be read.\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      Format downstreamFormat,\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\n          && (formatRequired || upstreamFormat != downstreamFormat)) {\n        formatHolder.format = upstreamFormat;\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      formatHolder.format = formats[relativeReadIndex];\n      return C.RESULT_FORMAT_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      // Suppress changes between equal formats so we can use referential equality in readData.\n      return false;\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\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  /**\n   * Finds the sample in the specified range that's before or at the specified time. If\n   * {@code 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-amzn-2.10.6/library/core/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.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 consuming thread.\n  private Format downstreamFormat;\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   * @param allocator An {@link Allocator} from which allocations for sample data can be obtained.\n   */\n  public SampleQueue(Allocator allocator) {\n    this.allocator = allocator;\n    allocationLength = allocator.getIndividualAllocationLength();\n    metadataQueue = new SampleMetadataQueue();\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   * Returns whether a sample is available to be read.\n   */\n  public boolean hasNextSample() {\n    return metadataQueue.hasNextSample();\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  /**\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   * @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  public int read(\n      FormatHolder formatHolder,\n      DecoderInputBuffer buffer,\n      boolean formatRequired,\n      boolean loadingFinished,\n      long decodeOnlyUntilUs) {\n    int result = metadataQueue.read(formatHolder, buffer, formatRequired, loadingFinished,\n        downstreamFormat, extrasHolder);\n    switch (result) {\n      case C.RESULT_FORMAT_READ:\n        downstreamFormat = formatHolder.format;\n        return C.RESULT_FORMAT_READ;\n      case C.RESULT_BUFFER_READ:\n        if (!buffer.isEndOfStream()) {\n          if (buffer.timeUs < decodeOnlyUntilUs) {\n            buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);\n          }\n          if (!buffer.isFlagsOnly()) {\n            // Read encryption data if the sample is encrypted.\n            if (buffer.isEncrypted()) {\n              readEncryptionData(buffer, extrasHolder);\n            }\n            // Write the sample data into the holder.\n            buffer.ensureSpaceForWrite(extrasHolder.size);\n            readData(extrasHolder.offset, buffer.data, extrasHolder.size);\n          }\n        }\n        return C.RESULT_BUFFER_READ;\n      case C.RESULT_NOTHING_READ:\n        return C.RESULT_NOTHING_READ;\n      default:\n        throw new IllegalStateException();\n    }\n  }\n\n  /**\n   * Reads encryption data for the current sample.\n   * <p>\n   * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and\n   * {@link SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The\n   * same 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  /**\n   * A node in a linked list of {@link Allocation}s held by the output.\n   */\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  /**\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    refreshSourceInfo(\n        new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false),\n        /* manifest= */ null);\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  public 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 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.addFlag(C.BUFFER_FLAG_KEY_FRAME);\n      buffer.data.put(SILENCE_SAMPLE, /* offset= */ 0, bytesToWrite);\n      buffer.timeUs = getAudioPositionUs(positionBytes);\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-amzn-2.10.6/library/core/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 @Nullable Object tag;\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   */\n  public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) {\n    this(durationUs, isSeekable, isDynamic, /* 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 tag A tag used for {@link Timeline.Window#tag}.\n   */\n  public SinglePeriodTimeline(\n      long durationUs, boolean isSeekable, boolean isDynamic, @Nullable Object tag) {\n    this(\n        durationUs,\n        durationUs,\n        /* windowPositionInPeriodUs= */ 0,\n        /* windowDefaultStartPositionUs= */ 0,\n        isSeekable,\n        isDynamic,\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 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      @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        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 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      @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.tag = tag;\n  }\n\n  @Override\n  public int getWindowCount() {\n    return 1;\n  }\n\n  @Override\n  public Window getWindow(\n      int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {\n    Assertions.checkIndex(windowIndex, 0, 1);\n    Object tag = setTag ? this.tag : null;\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        tag,\n        presentationStartTimeMs,\n        windowStartTimeMs,\n        isSeekable,\n        isDynamic,\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-amzn-2.10.6/library/core/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.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\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  private final @Nullable 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 */ boolean loadingSucceeded;\n  /* package */ byte[] 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(TrackSelection[] selections, boolean[] mayRetainStreamFlags,\n      SampleStream[] streams, boolean[] streamResetFlags, 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 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 = loadable.sampleData;\n    loadingFinished = true;\n    loadingSucceeded = 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 (loadingSucceeded) {\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    private byte[] sampleData;\n\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-amzn-2.10.6/library/core/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    private @Nullable 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  private @Nullable 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(durationUs, /* isSeekable= */ true, /* isDynamic= */ false, tag);\n  }\n\n  // MediaSource implementation.\n\n  @Override\n  @Nullable\n  public Object getTag() {\n    return tag;\n  }\n\n  @Override\n  public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    transferListener = mediaTransferListener;\n    refreshSourceInfo(timeline, /* manifest= */ null);\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  public 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 extends DefaultMediaSourceEventListener {\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-amzn-2.10.6/library/core/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 Parcelable.Creator<TrackGroup> CREATOR =\n      new Parcelable.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-amzn-2.10.6/library/core/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 Parcelable.Creator<TrackGroupArray> CREATOR =\n      new Parcelable.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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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;\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 Uri[] uris;\n    /** The state of each ad in the ad group. */\n    public final @AdState 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(int count, @AdState int[] states, 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(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      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      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      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 = Arrays.copyOf(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 = Arrays.copyOf(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 = Arrays.copyOf(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 = Arrays.copyOf(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 = Arrays.copyOf(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 = Arrays.copyOf(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 = Arrays.copyOf(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(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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.view.View;\nimport android.view.ViewGroup;\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-amzn-2.10.6/library/core/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.DeferredMediaPeriod;\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.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;\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  /** Factory for creating {@link MediaSource}s to play ad media. */\n  public interface MediaSourceFactory {\n\n    /**\n     * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}.\n     *\n     * @param uri The URI of the media or manifest to play.\n     * @return The new media source.\n     */\n    MediaSource createMediaSource(Uri uri);\n\n    /**\n     * Returns the content types supported by media sources created by this factory. Each element\n     * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or {@link\n     * C#TYPE_OTHER}.\n     *\n     * @return The content types supported by media sources created by this factory.\n     */\n    int[] getSupportedTypes();\n  }\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) 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<DeferredMediaPeriod>> deferredMediaPeriodByAdMediaSource;\n  private final Timeline.Period period;\n\n  // Accessed on the player thread.\n  private ComponentListener componentListener;\n  private Timeline contentTimeline;\n  private Object contentManifest;\n  private AdPlaybackState adPlaybackState;\n  private MediaSource[][] adGroupMediaSources;\n  private 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    deferredMediaPeriodByAdMediaSource = 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  public 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    if (adPlaybackState.adGroupCount > 0 && id.isAd()) {\n      int adGroupIndex = id.adGroupIndex;\n      int adIndexInAdGroup = id.adIndexInAdGroup;\n      Uri adUri = adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup];\n      if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) {\n        MediaSource adMediaSource = adMediaSourceFactory.createMediaSource(adUri);\n        int oldAdCount = adGroupMediaSources[adGroupIndex].length;\n        if (adIndexInAdGroup >= oldAdCount) {\n          int adCount = adIndexInAdGroup + 1;\n          adGroupMediaSources[adGroupIndex] =\n              Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount);\n          adGroupTimelines[adGroupIndex] = Arrays.copyOf(adGroupTimelines[adGroupIndex], adCount);\n        }\n        adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource;\n        deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList<>());\n        prepareChildSource(id, adMediaSource);\n      }\n      MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup];\n      DeferredMediaPeriod deferredMediaPeriod =\n          new DeferredMediaPeriod(mediaSource, id, allocator, startPositionUs);\n      deferredMediaPeriod.setPrepareErrorListener(\n          new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup));\n      List<DeferredMediaPeriod> mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource);\n      if (mediaPeriods == null) {\n        Object periodUid =\n            adGroupTimelines[adGroupIndex][adIndexInAdGroup].getUidOfPeriod(/* periodIndex= */ 0);\n        MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber);\n        deferredMediaPeriod.createPeriod(adSourceMediaPeriodId);\n      } else {\n        // Keep track of the deferred media period so it can be populated with the real media period\n        // when the source's info becomes available.\n        mediaPeriods.add(deferredMediaPeriod);\n      }\n      return deferredMediaPeriod;\n    } else {\n      DeferredMediaPeriod mediaPeriod =\n          new DeferredMediaPeriod(contentMediaSource, id, allocator, startPositionUs);\n      mediaPeriod.createPeriod(id);\n      return mediaPeriod;\n    }\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {\n    DeferredMediaPeriod deferredMediaPeriod = (DeferredMediaPeriod) mediaPeriod;\n    List<DeferredMediaPeriod> mediaPeriods =\n        deferredMediaPeriodByAdMediaSource.get(deferredMediaPeriod.mediaSource);\n    if (mediaPeriods != null) {\n      mediaPeriods.remove(deferredMediaPeriod);\n    }\n    deferredMediaPeriod.releasePeriod();\n  }\n\n  @Override\n  public void releaseSourceInternal() {\n    super.releaseSourceInternal();\n    componentListener.release();\n    componentListener = null;\n    deferredMediaPeriodByAdMediaSource.clear();\n    contentTimeline = null;\n    contentManifest = 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,\n      MediaSource mediaSource,\n      Timeline timeline,\n      @Nullable Object manifest) {\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, manifest);\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, Object manifest) {\n    Assertions.checkArgument(timeline.getPeriodCount() == 1);\n    contentTimeline = timeline;\n    contentManifest = manifest;\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<DeferredMediaPeriod> mediaPeriods = deferredMediaPeriodByAdMediaSource.remove(mediaSource);\n    if (mediaPeriods != null) {\n      Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);\n      for (int i = 0; i < mediaPeriods.size(); i++) {\n        DeferredMediaPeriod 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    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, contentManifest);\n    }\n  }\n\n  private static long[][] getAdDurations(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 DeferredMediaPeriod.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-amzn-2.10.6/library/core/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(\n      int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {\n    window = super.getWindow(windowIndex, window, setTag, defaultPositionProjectionUs);\n    if (window.durationUs == C.TIME_UNSET) {\n      window.durationUs = adPlaybackState.contentDurationUs;\n    }\n    return window;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 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      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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  public final @Nullable 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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.util.SparseArray;\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 com.google.android.exoplayer2.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 com.google.android.exoplayer2.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-amzn-2.10.6/library/core/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\n/**\n * Holds a chunk or an indication that the end of the stream has been reached.\n */\npublic final class ChunkHolder {\n\n  /**\n   * The chunk.\n   */\n  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-amzn-2.10.6/library/core/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.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.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.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  private final int[] embeddedTrackTypes;\n  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  private @Nullable 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 minLoadableRetryCount The minimum number of times that the source should retry a load\n   *     before propagating an error.\n   * @param eventDispatcher A dispatcher to notify of events.\n   * @deprecated Use {@link #ChunkSampleStream(int, int[], Format[], ChunkSource, Callback,\n   *     Allocator, long, LoadErrorHandlingPolicy, EventDispatcher)} instead.\n   */\n  @Deprecated\n  public ChunkSampleStream(\n      int primaryTrackType,\n      int[] embeddedTrackTypes,\n      Format[] embeddedTrackFormats,\n      T chunkSource,\n      Callback<ChunkSampleStream<T>> callback,\n      Allocator allocator,\n      long positionUs,\n      int minLoadableRetryCount,\n      EventDispatcher eventDispatcher) {\n    this(\n        primaryTrackType,\n        embeddedTrackTypes,\n        embeddedTrackFormats,\n        chunkSource,\n        callback,\n        allocator,\n        positionUs,\n        new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount),\n        eventDispatcher);\n  }\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 loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.\n   * @param eventDispatcher A dispatcher to notify of events.\n   */\n  public ChunkSampleStream(\n      int primaryTrackType,\n      int[] embeddedTrackTypes,\n      Format[] embeddedTrackFormats,\n      T chunkSource,\n      Callback<ChunkSampleStream<T>> callback,\n      Allocator allocator,\n      long positionUs,\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);\n    trackTypes[0] = primaryTrackType;\n    sampleQueues[0] = primarySampleQueue;\n\n    for (int i = 0; i < embeddedTrackCount; i++) {\n      SampleQueue sampleQueue = new SampleQueue(allocator);\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.discardToEnd();\n    for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {\n      embeddedSampleQueue.discardToEnd();\n    }\n    loader.release(this);\n  }\n\n  @Override\n  public void onLoaderReleased() {\n    primarySampleQueue.reset();\n    for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {\n      embeddedSampleQueue.reset();\n    }\n    if (releaseCallback != null) {\n      releaseCallback.onSampleStreamReleased(this);\n    }\n  }\n\n  // SampleStream implementation.\n\n  @Override\n  public boolean isReady() {\n    return loadingFinished || (!isPendingReset() && primarySampleQueue.hasNextSample());\n  }\n\n  @Override\n  public void maybeThrowError() throws IOException {\n    loader.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    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 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 loadingFinished || (!isPendingReset() && sampleQueue.hasNextSample());\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, buffer, formatRequired, loadingFinished, 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-amzn-2.10.6/library/core/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. Must be false if {@code cancelable} is false.\n   */\n  boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e, long blacklistDurationMs);\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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(DataSource dataSource, DataSpec dataSpec, int type, Format trackFormat,\n      int trackSelectionReason, Object trackSelectionData, 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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      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-amzn-2.10.6/library/core/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 java.util.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 java.util.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 java.util.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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.IntDef;\nimport android.view.accessibility.CaptioningManager;\nimport android.view.accessibility.CaptioningManager.CaptionStyle;\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  /**\n   * Default caption style.\n   */\n  public static final CaptionStyleCompat DEFAULT = new CaptionStyleCompat(\n      Color.WHITE, Color.BLACK, Color.TRANSPARENT, EDGE_TYPE_NONE, Color.WHITE, 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  /**\n   * The preferred typeface.\n   */\n  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      CaptioningManager.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(int foregroundColor, int backgroundColor, int windowColor,\n      @EdgeType int edgeType, int edgeColor, 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      CaptioningManager.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      CaptioningManager.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-amzn-2.10.6/library/core/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 androidx.annotation.IntDef;\nimport android.text.Layout.Alignment;\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 or width. */\n  public static final float DIMEN_UNSET = Float.MIN_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  public final CharSequence text;\n\n  /**\n   * The alignment of the cue text within the cue box, or null if the alignment is undefined.\n   */\n  public final Alignment textAlignment;\n\n  /**\n   * The cue image, or null if this is a text cue.\n   */\n  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      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      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      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      CharSequence text,\n      Alignment textAlignment,\n      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-amzn-2.10.6/library/core/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 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  /**\n   * @param name The name of the decoder.\n   */\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 = 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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          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          switch (format.sampleMimeType) {\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(format.sampleMimeType, 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              throw new IllegalArgumentException(\n                  \"Attempted to create decoder for unsupported format\");\n          }\n        }\n      };\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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  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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.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  /**\n   * @deprecated Use {@link TextOutput}.\n   */\n  @Deprecated\n  public interface Output extends TextOutput {}\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  private final @Nullable 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  private Format streamFormat;\n  private SubtitleDecoder decoder;\n  private SubtitleInputBuffer nextInputBuffer;\n  private SubtitleOutputBuffer subtitle;\n  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  public int supportsFormat(Format format) {\n    if (decoderFactory.supportsFormat(format)) {\n      return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM;\n    } else if (MimeTypes.isText(format.sampleMimeType)) {\n      return FORMAT_UNSUPPORTED_SUBTYPE;\n    } else {\n      return FORMAT_UNSUPPORTED_TYPE;\n    }\n  }\n\n  @Override\n  protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {\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 ExoPlaybackException.createForRenderer(e, getIndex());\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 ExoPlaybackException.createForRenderer(e, getIndex());\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.NonNull;\nimport android.text.Layout.Alignment;\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   * An unset priority.\n   */\n  public static final int PRIORITY_UNSET = -1;\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}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 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  public Cea708Decoder(int accessibilityChannel, 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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;\nimport com.google.android.exoplayer2.util.Util;\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 = Util.getIntegerCodeForString(\"GA94\");\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-amzn-2.10.6/library/core/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.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 DvbSubtitle 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-amzn-2.10.6/library/core/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 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;\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  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    if (subtitleService.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 = subtitleService.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          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          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        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            regionComposition.mergeFrom(service.regions.get(regionComposition.id));\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    byte[] topFieldData = null;\n    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  /**\n   * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas.\n   */\n  private static void paintPixelDataSubBlocks(ObjectData objectData, ClutDefinition clutDefinition,\n      int regionDepth, int horizontalAddress, int verticalAddress, Paint paint, 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  /**\n   * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas.\n   */\n  private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, int regionDepth,\n      int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) {\n    ParsableBitArray data = new ParsableBitArray(pixelData);\n    int column = horizontalAddress;\n    int line = verticalAddress;\n    byte[] clutMapTable2To4 = null;\n    byte[] clutMapTable2To8 = null;\n    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          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          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 = paint8BitPixelCodeString(data, clutEntries, 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          clutMapTable2To8 = 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  /**\n   * Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas.\n   */\n  private static int paint2BitPixelCodeString(ParsableBitArray data, int[] clutEntries,\n      byte[] clutMapTable, int column, int line, Paint paint, 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  /**\n   * Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas.\n   */\n  private static int paint4BitPixelCodeString(ParsableBitArray data, int[] clutEntries,\n      byte[] clutMapTable, int column, int line, Paint paint, 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  /**\n   * Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas.\n   */\n  private static int paint8BitPixelCodeString(ParsableBitArray data, int[] clutEntries,\n      byte[] clutMapTable, int column, int line, Paint paint, 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 = new SparseArray<>();\n    public final SparseArray<ClutDefinition> cluts = new SparseArray<>();\n    public final SparseArray<ObjectData> objects = new SparseArray<>();\n    public final SparseArray<ClutDefinition> ancillaryCluts = new SparseArray<>();\n    public final SparseArray<ObjectData> ancillaryObjects = new SparseArray<>();\n\n    public DisplayDefinition displayDefinition;\n    public PageComposition pageComposition;\n\n    public SubtitleService(int subtitlePageId, int ancillaryPageId) {\n      this.subtitlePageId = subtitlePageId;\n      this.ancillaryPageId = ancillaryPageId;\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      if (otherRegionComposition == null) {\n        return;\n      }\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\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.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.LongArray;\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 * A {@link SimpleSubtitleDecoder} for SSA/ASS.\n */\npublic final class SsaDecoder extends SimpleSubtitleDecoder {\n\n  private static final String TAG = \"SsaDecoder\";\n\n  private static final Pattern SSA_TIMECODE_PATTERN = Pattern.compile(\n      \"(?:(\\\\d+):)?(\\\\d+):(\\\\d+)(?::|\\\\.)(\\\\d+)\");\n  private static final String FORMAT_LINE_PREFIX = \"Format: \";\n  private static final String DIALOGUE_LINE_PREFIX = \"Dialogue: \";\n\n  private final boolean haveInitializationData;\n\n  private int formatKeyCount;\n  private int formatStartIndex;\n  private int formatEndIndex;\n  private int formatTextIndex;\n\n  public SsaDecoder() {\n    this(/* initializationData= */ null);\n  }\n\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.\n   */\n  public SsaDecoder(@Nullable List<byte[]> initializationData) {\n    super(\"SsaDecoder\");\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      parseFormatLine(formatLine);\n      parseHeader(new ParsableByteArray(initializationData.get(1)));\n    } else {\n      haveInitializationData = false;\n    }\n  }\n\n  @Override\n  protected SsaSubtitle decode(byte[] bytes, int length, boolean reset) {\n    ArrayList<Cue> cues = new ArrayList<>();\n    LongArray cueTimesUs = new LongArray();\n\n    ParsableByteArray data = new ParsableByteArray(bytes, length);\n    if (!haveInitializationData) {\n      parseHeader(data);\n    }\n    parseEventBody(data, cues, cueTimesUs);\n\n    Cue[] cuesArray = new Cue[cues.size()];\n    cues.toArray(cuesArray);\n    long[] cueTimesUsArray = cueTimesUs.toArray();\n    return new SsaSubtitle(cuesArray, cueTimesUsArray);\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    String currentLine;\n    while ((currentLine = data.readLine()) != null) {\n      // TODO: Parse useful data from the header.\n      if (currentLine.startsWith(\"[Events]\")) {\n        // We've reached the event body.\n        return;\n      }\n    }\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 An array to which parsed cue timestamps will be added.\n   */\n  private void parseEventBody(ParsableByteArray data, List<Cue> cues, LongArray cueTimesUs) {\n    String currentLine;\n    while ((currentLine = data.readLine()) != null) {\n      if (!haveInitializationData && currentLine.startsWith(FORMAT_LINE_PREFIX)) {\n        parseFormatLine(currentLine);\n      } else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) {\n        parseDialogueLine(currentLine, cues, cueTimesUs);\n      }\n    }\n  }\n\n  /**\n   * Parses a format line.\n   *\n   * @param formatLine The line to parse.\n   */\n  private void parseFormatLine(String formatLine) {\n    String[] values = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), \",\");\n    formatKeyCount = values.length;\n    formatStartIndex = C.INDEX_UNSET;\n    formatEndIndex = C.INDEX_UNSET;\n    formatTextIndex = C.INDEX_UNSET;\n    for (int i = 0; i < formatKeyCount; i++) {\n      String key = Util.toLowerInvariant(values[i].trim());\n      switch (key) {\n        case \"start\":\n          formatStartIndex = i;\n          break;\n        case \"end\":\n          formatEndIndex = i;\n          break;\n        case \"text\":\n          formatTextIndex = i;\n          break;\n        default:\n          // Do nothing.\n          break;\n      }\n    }\n    if (formatStartIndex == C.INDEX_UNSET\n        || formatEndIndex == C.INDEX_UNSET\n        || formatTextIndex == C.INDEX_UNSET) {\n      // Set to 0 so that parseDialogueLine skips lines until a complete format line is found.\n      formatKeyCount = 0;\n    }\n  }\n\n  /**\n   * Parses a dialogue line.\n   *\n   * @param dialogueLine The line to parse.\n   * @param cues A list to which parsed cues will be added.\n   * @param cueTimesUs An array to which parsed cue timestamps will be added.\n   */\n  private void parseDialogueLine(String dialogueLine, List<Cue> cues, LongArray cueTimesUs) {\n    if (formatKeyCount == 0) {\n      Log.w(TAG, \"Skipping dialogue line before complete format: \" + dialogueLine);\n      return;\n    }\n\n    String[] lineValues = dialogueLine.substring(DIALOGUE_LINE_PREFIX.length())\n        .split(\",\", formatKeyCount);\n    if (lineValues.length != formatKeyCount) {\n      Log.w(TAG, \"Skipping dialogue line with fewer columns than format: \" + dialogueLine);\n      return;\n    }\n\n    long startTimeUs = SsaDecoder.parseTimecodeUs(lineValues[formatStartIndex]);\n    if (startTimeUs == C.TIME_UNSET) {\n      Log.w(TAG, \"Skipping invalid timing: \" + dialogueLine);\n      return;\n    }\n\n    long endTimeUs = C.TIME_UNSET;\n    String endTimeString = lineValues[formatEndIndex];\n    if (!endTimeString.trim().isEmpty()) {\n      endTimeUs = SsaDecoder.parseTimecodeUs(endTimeString);\n      if (endTimeUs == C.TIME_UNSET) {\n        Log.w(TAG, \"Skipping invalid timing: \" + dialogueLine);\n        return;\n      }\n    }\n\n    String text = lineValues[formatTextIndex]\n        .replaceAll(\"\\\\{.*?\\\\}\", \"\")\n        .replaceAll(\"\\\\\\\\N\", \"\\n\")\n        .replaceAll(\"\\\\\\\\n\", \"\\n\");\n    cues.add(new Cue(text));\n    cueTimesUs.add(startTimeUs);\n    if (endTimeUs != C.TIME_UNSET) {\n      cues.add(Cue.EMPTY);\n      cueTimesUs.add(endTimeUs);\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  public static long parseTimecodeUs(String timeString) {\n    Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString);\n    if (!matcher.matches()) {\n      return C.TIME_UNSET;\n    }\n    long timestampUs = Long.parseLong(matcher.group(1)) * 60 * 60 * C.MICROS_PER_SECOND;\n    timestampUs += Long.parseLong(matcher.group(2)) * 60 * C.MICROS_PER_SECOND;\n    timestampUs += Long.parseLong(matcher.group(3)) * C.MICROS_PER_SECOND;\n    timestampUs += Long.parseLong(matcher.group(4)) * 10000; // 100ths of a second.\n    return timestampUs;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 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 SsaSubtitle(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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.text.Html;\nimport android.text.Spanned;\nimport android.text.TextUtils;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.SimpleSubtitleDecoder;\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  /* package */ static final float START_FRACTION = 0.08f;\n  /* package */ static final float END_FRACTION = 1 - START_FRACTION;\n  /* package */ 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 SubripSubtitle 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    String currentLine;\n\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      boolean haveEndTimecode = false;\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, 1));\n        if (!TextUtils.isEmpty(matcher.group(6))) {\n          haveEndTimecode = true;\n          cueTimesUs.add(parseTimecode(matcher, 6));\n        }\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      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\n      if (haveEndTimecode) {\n        cues.add(Cue.EMPTY);\n      }\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.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 TtmlSubtitle 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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.text.SpannableStringBuilder;\nimport android.util.Base64;\nimport android.util.Pair;\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.IntDef;\nimport android.text.Layout;\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 = Util.getIntegerCodeForString(\"styl\");\n  private static final int TYPE_TBOX = Util.getIntegerCodeForString(\"tbox\");\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 com.google.android.exoplayer2.util.ColorParser;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\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\n   * {@code ::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\n   *     (targets all cues) or null if an error was encountered.\n   */\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) || token == null) {\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  /* 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  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(Arrays.copyOfRange(classDivision, 1, classDivision.length));\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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.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/**\n * A {@link SimpleSubtitleDecoder} for Webvtt embedded in a Mp4 container file.\n */\npublic final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {\n\n  private static final int BOX_HEADER_SIZE = 8;\n\n  private static final int TYPE_payl = Util.getIntegerCodeForString(\"payl\");\n  private static final int TYPE_sttg = Util.getIntegerCodeForString(\"sttg\");\n  private static final int TYPE_vttc = Util.getIntegerCodeForString(\"vttc\");\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 Mp4WebvttSubtitle 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.IntDef;\nimport android.text.Layout;\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;\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  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 Layout.Alignment textAlign;\n\n  public WebvttCssStyle() {\n    reset();\n  }\n\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   *\n   * 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(String id, String tag, String[] classes, 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 tag.isEmpty() ? 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  public String getFontFamily() {\n    return fontFamily;\n  }\n\n  public WebvttCssStyle setFontFamily(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  public Layout.Alignment getTextAlign() {\n    return textAlign;\n  }\n\n  public WebvttCssStyle setTextAlign(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(int currentScore, String target, String actual,\n      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-amzn-2.10.6/library/core/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 android.text.Layout.Alignment;\nimport android.text.SpannableStringBuilder;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.util.Log;\n\n/**\n * A representation of a WebVTT cue.\n */\npublic final class WebvttCue extends Cue {\n\n  public final long startTime;\n  public final long endTime;\n\n  public WebvttCue(CharSequence text) {\n    this(0, 0, text);\n  }\n\n  public WebvttCue(long startTime, long endTime, CharSequence text) {\n    this(startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET,\n        Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);\n  }\n\n  public WebvttCue(long startTime, long endTime, CharSequence text, Alignment textAlignment,\n      float line, @Cue.LineType int lineType, @Cue.AnchorType int lineAnchor, float position,\n      @Cue.AnchorType int positionAnchor, 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 == DIMEN_UNSET);\n  }\n\n  /**\n   * Builder for WebVTT cues.\n   */\n  @SuppressWarnings(\"hiding\")\n  public static class Builder {\n\n    private static final String TAG = \"WebvttCueBuilder\";\n\n    private long startTime;\n    private long endTime;\n    private SpannableStringBuilder text;\n    private Alignment textAlignment;\n    private float line;\n    private int lineType;\n    private int lineAnchor;\n    private float position;\n    private int positionAnchor;\n    private float width;\n\n    // Initialization methods\n\n    public Builder() {\n      reset();\n    }\n\n    public void reset() {\n      startTime = 0;\n      endTime = 0;\n      text = null;\n      textAlignment = null;\n      line = Cue.DIMEN_UNSET;\n      lineType = Cue.TYPE_UNSET;\n      lineAnchor = Cue.TYPE_UNSET;\n      position = Cue.DIMEN_UNSET;\n      positionAnchor = Cue.TYPE_UNSET;\n      width = Cue.DIMEN_UNSET;\n    }\n\n    // Construction methods.\n\n    public WebvttCue build() {\n      if (position != Cue.DIMEN_UNSET && positionAnchor == Cue.TYPE_UNSET) {\n        derivePositionAnchorFromAlignment();\n      }\n      return new WebvttCue(startTime, endTime, text, textAlignment, line, lineType, lineAnchor,\n          position, positionAnchor, 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(SpannableStringBuilder aText) {\n      text = aText;\n      return this;\n    }\n\n    public Builder setTextAlignment(Alignment 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(int lineType) {\n      this.lineType = lineType;\n      return this;\n    }\n\n    public Builder setLineAnchor(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(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    private Builder derivePositionAnchorFromAlignment() {\n      if (textAlignment == null) {\n        positionAnchor = Cue.TYPE_UNSET;\n      } else {\n        switch (textAlignment) {\n          case ALIGN_NORMAL:\n            positionAnchor = Cue.ANCHOR_TYPE_START;\n            break;\n          case ALIGN_CENTER:\n            positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;\n            break;\n          case ALIGN_OPPOSITE:\n            positionAnchor = Cue.ANCHOR_TYPE_END;\n            break;\n          default:\n            Log.w(TAG, \"Unrecognized alignment: \" + textAlignment);\n            positionAnchor = Cue.ANCHOR_TYPE_START;\n            break;\n        }\n      }\n      return this;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 androidx.annotation.NonNull;\n\nimport android.text.Html;\nimport android.text.Layout.Alignment;\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 com.google.android.exoplayer2.text.Cue;\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.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues)\n */\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.\n   * @param styles List of styles defined by the CSS style blocks preceeding the cues.\n   * @return Whether a valid Cue was found.\n   */\n  public boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder,\n      List<WebvttCssStyle> styles) {\n    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    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 preceeding the cues.\n   * @param builder Output builder.\n   */\n  /* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder,\n      List<WebvttCssStyle> styles) {\n    // MOD: Fix embedded styles by decoding html entities\n    markup = Html.fromHtml(markup).toString();\n\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          String tagName = getTagName(fullTagExpression);\n          if (tagName == null || !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(String id, Matcher cueHeaderMatcher, ParsableByteArray webvttData,\n      WebvttCue.Builder builder, StringBuilder textBuilder, 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    String line;\n    while (!TextUtils.isEmpty(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      throws NumberFormatException {\n    int commaIndex = s.indexOf(',');\n    if (commaIndex != -1) {\n      builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));\n      s = s.substring(0, commaIndex);\n    } else {\n      builder.setLineAnchor(Cue.TYPE_UNSET);\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      throws NumberFormatException {\n    int commaIndex = s.indexOf(',');\n    if (commaIndex != -1) {\n      builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));\n      s = s.substring(0, commaIndex);\n    } else {\n      builder.setPositionAnchor(Cue.TYPE_UNSET);\n    }\n    builder.setPosition(WebvttParserUtil.parsePercentage(s));\n  }\n\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  private static Alignment parseTextAlignment(String s) {\n    switch (s) {\n      case \"start\":\n      case \"left\":\n        return Alignment.ALIGN_NORMAL;\n      case \"center\":\n      case \"middle\":\n        return Alignment.ALIGN_CENTER;\n      case \"end\":\n      case \"right\":\n        return Alignment.ALIGN_OPPOSITE;\n      default:\n        Log.w(TAG, \"Invalid alignment value: \" + s);\n        return null;\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(String cueId, StartTag startTag, SpannableStringBuilder text,\n      List<WebvttCssStyle> styles, 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    if (style.getTextAlign() != null) {\n      spannedText.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end,\n          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    if (tagExpression.isEmpty()) {\n      return null;\n    }\n    return Util.splitAtFirst(tagExpression, \"[ \\\\.]\")[0];\n  }\n\n  private static void getApplicableStyles(List<WebvttCssStyle> declaredStyles, String id,\n      StartTag tag, 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      if (fullTagExpression.isEmpty()) {\n        return null;\n      }\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 = Arrays.copyOfRange(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-amzn-2.10.6/library/core/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.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 WebvttSubtitle 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-amzn-2.10.6/library/core/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 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((\\u0020|\\u0009).*)?$\");\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    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  public static Matcher findNextCueHeader(ParsableByteArray input) {\n    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-amzn-2.10.6/library/core/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.Collections;\nimport java.util.List;\n\n/**\n * A representation of a WebVTT subtitle.\n */\n/* package */ final class WebvttSubtitle implements Subtitle {\n\n  private static final long MAX_CUE_LENGTH_US = 10_000_000; // NOTE: lower values may broke some videos\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      long startTime = cue.startTime;\n      long endTime = cue.endTime;\n      long length = endTime - startTime;\n      cueTimesUs[arrayIndex] = startTime;\n      // MOD: fix long lasting subs\n      //cueTimesUs[arrayIndex + 1] = cue.endTime;\n      cueTimesUs[arrayIndex + 1] = length <= MAX_CUE_LENGTH_US ? endTime : startTime + MAX_CUE_LENGTH_US;\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    ArrayList<Cue> list = null;\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        if (list == null) {\n          list = new ArrayList<>();\n        }\n        WebvttCue cue = cues.get(i);\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.append(firstNormalCue.text).append(\"\\n\").append(cue.text);\n          } else {\n            normalCueTextBuilder.append(\"\\n\").append(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(normalCueTextBuilder));\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\n    if (list != null) {\n      return list;\n    } else {\n      return Collections.emptyList();\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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.ExoPlayerFactory;\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 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    private final @Nullable 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    private TrackBitrateEstimator trackBitrateEstimator;\n    private boolean blockFixedTrackSelectionBandwidth;\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 ExoPlayerFactory}.\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 ExoPlayerFactory}.\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 ExoPlayerFactory}.\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      trackBitrateEstimator = TrackBitrateEstimator.DEFAULT;\n    }\n\n    /**\n     * Sets a TrackBitrateEstimator.\n     *\n     * <p>This method is experimental, and will be renamed or removed in a future release.\n     *\n     * @param trackBitrateEstimator A {@link TrackBitrateEstimator}.\n     */\n    public final void experimental_setTrackBitrateEstimator(\n        TrackBitrateEstimator trackBitrateEstimator) {\n      this.trackBitrateEstimator = trackBitrateEstimator;\n    }\n\n    /**\n     * Enables blocking of the total fixed track selection bandwidth.\n     *\n     * <p>This method is experimental, and will be renamed or removed in a future release.\n     */\n    public final void experimental_enableBlockFixedTrackSelectionBandwidth() {\n      this.blockFixedTrackSelectionBandwidth = true;\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      List<AdaptiveTrackSelection> adaptiveSelections = new ArrayList<>();\n      int totalFixedBandwidth = 0;\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) {\n          AdaptiveTrackSelection adaptiveSelection =\n              createAdaptiveTrackSelection(definition.group, bandwidthMeter, definition.tracks);\n          adaptiveSelection.experimental_setTrackBitrateEstimator(trackBitrateEstimator);\n          adaptiveSelections.add(adaptiveSelection);\n          selections[i] = adaptiveSelection;\n        } else {\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      if (blockFixedTrackSelectionBandwidth) {\n        for (int i = 0; i < adaptiveSelections.size(); i++) {\n          adaptiveSelections.get(i).experimental_setNonAllocatableBandwidth(totalFixedBandwidth);\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     * @return An {@link AdaptiveTrackSelection} for the specified tracks.\n     */\n    protected AdaptiveTrackSelection createAdaptiveTrackSelection(\n        TrackGroup group, BandwidthMeter bandwidthMeter, int[] tracks) {\n      return new AdaptiveTrackSelection(\n          group,\n          tracks,\n          new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction),\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  private final Format[] formats;\n  private final int[] formatBitrates;\n  private final int[] trackBitrates;\n\n  private TrackBitrateEstimator trackBitrateEstimator;\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        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 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 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),\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    trackBitrateEstimator = TrackBitrateEstimator.DEFAULT;\n    formats = new Format[length];\n    formatBitrates = new int[length];\n    trackBitrates = new int[length];\n    for (int i = 0; i < length; i++) {\n      @SuppressWarnings(\"nullness:method.invocation.invalid\")\n      Format format = getFormat(i);\n      formats[i] = format;\n      formatBitrates[i] = formats[i].bitrate;\n    }\n  }\n\n  /**\n   * Sets a TrackBitrateEstimator.\n   *\n   * <p>This method is experimental, and will be renamed or removed in a future release.\n   *\n   * @param trackBitrateEstimator A {@link TrackBitrateEstimator}.\n   */\n  public void experimental_setTrackBitrateEstimator(TrackBitrateEstimator trackBitrateEstimator) {\n    this.trackBitrateEstimator = trackBitrateEstimator;\n  }\n\n  /**\n   * Sets the non-allocatable bandwidth, which shouldn't be considered available.\n   *\n   * <p>This method is experimental, and will be renamed or removed in a future release.\n   *\n   * @param nonAllocatableBandwidth The non-allocatable bandwidth in bits per second.\n   */\n  public void experimental_setNonAllocatableBandwidth(long nonAllocatableBandwidth) {\n    ((DefaultBandwidthProvider) bandwidthProvider)\n        .experimental_setNonAllocatableBandwidth(nonAllocatableBandwidth);\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    // Update the estimated track bitrates.\n    trackBitrateEstimator.getBitrates(formats, queue, mediaChunkIterators, trackBitrates);\n\n    // Make initial selection\n    if (reason == C.SELECTION_REASON_UNKNOWN) {\n      reason = C.SELECTION_REASON_INITIAL;\n      selectedIndex = determineIdealSelectedIndex(nowMs, trackBitrates);\n      return;\n    }\n\n    // Stash the current selection, then make a new one.\n    int currentSelectedIndex = selectedIndex;\n    selectedIndex = determineIdealSelectedIndex(nowMs, trackBitrates);\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  public @Nullable 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, formatBitrates);\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   * @param trackBitrates The estimated track bitrates. May differ from format bitrates if more\n   *     accurate estimates of the current track bitrates are available.\n   */\n  private int determineIdealSelectedIndex(long nowMs, int[] trackBitrates) {\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, trackBitrates[i], 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\n    private long nonAllocatableBandwidth;\n\n    @Nullable private long[][] allocationCheckpoints;\n\n    /* package */ DefaultBandwidthProvider(BandwidthMeter bandwidthMeter, float bandwidthFraction) {\n      this.bandwidthMeter = bandwidthMeter;\n      this.bandwidthFraction = bandwidthFraction;\n    }\n\n    @Override\n    public long getAllocatedBandwidth() {\n      long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction);\n      long allocatableBandwidth = Math.max(0L, totalBandwidth - nonAllocatableBandwidth);\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_setNonAllocatableBandwidth(long nonAllocatableBandwidth) {\n      this.nonAllocatableBandwidth = nonAllocatableBandwidth;\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.util.Pair;\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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\nimport android.util.Pair;\nimport android.util.SparseArray;\nimport android.util.SparseBooleanArray;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayerFactory;\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.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.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, SelectionOverride>> selectionOverrides;\n    private final SparseBooleanArray rendererDisabledFlags;\n\n    /** Creates a builder with default initial values. */\n    public ParametersBuilder() {\n      this(Parameters.DEFAULT);\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 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    // 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, SelectionOverride override) {\n      Map<TrackGroupArray, SelectionOverride> overrides = 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, SelectionOverride> overrides = 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, SelectionOverride> overrides = 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 static SparseArray<Map<TrackGroupArray, SelectionOverride>> cloneSelectionOverrides(\n        SparseArray<Map<TrackGroupArray, SelectionOverride>> selectionOverrides) {\n      SparseArray<Map<TrackGroupArray, SelectionOverride>> clone = 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    /** An instance with default values. */\n    public static final Parameters DEFAULT = new Parameters();\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 {@link Integer#MAX_VALUE}\n     * (i.e. no constraint).\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 {@link\n     * Integer#MAX_VALUE} (i.e. no constraint).\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, SelectionOverride>> selectionOverrides;\n    private final SparseBooleanArray rendererDisabledFlags;\n\n    private Parameters() {\n      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          TrackSelectionParameters.DEFAULT.preferredAudioLanguage,\n          /* maxAudioChannelCount= */ Integer.MAX_VALUE,\n          /* maxAudioBitrate= */ Integer.MAX_VALUE,\n          /* exceedAudioConstraintsIfNecessary= */ true,\n          /* allowAudioMixedMimeTypeAdaptiveness= */ false,\n          /* allowAudioMixedSampleRateAdaptiveness= */ false,\n          /* allowAudioMixedChannelCountAdaptiveness= */ false,\n          // Text\n          TrackSelectionParameters.DEFAULT.preferredTextLanguage,\n          TrackSelectionParameters.DEFAULT.preferredTextRoleFlags,\n          TrackSelectionParameters.DEFAULT.selectUndeterminedTextLanguage,\n          TrackSelectionParameters.DEFAULT.disabledTextTrackSelectionFlags,\n          // General\n          /* forceLowestBitrate= */ false,\n          /* forceHighestSupportedBitrate= */ false,\n          /* exceedRendererCapabilitiesIfNecessary= */ true,\n          /* tunnelingAudioSessionId= */ C.AUDIO_SESSION_ID_UNSET,\n          new SparseArray<>(),\n          new SparseBooleanArray());\n    }\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, 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, SelectionOverride> overrides = 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, SelectionOverride> overrides = 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, SelectionOverride>> readSelectionOverrides(\n        Parcel in) {\n      int renderersWithOverridesCount = in.readInt();\n      SparseArray<Map<TrackGroupArray, 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, SelectionOverride> overrides = new HashMap<>(overrideCount);\n        for (int j = 0; j < overrideCount; j++) {\n          TrackGroupArray trackGroups = in.readParcelable(TrackGroupArray.class.getClassLoader());\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, SparseArray<Map<TrackGroupArray, 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, SelectionOverride> overrides = selectionOverrides.valueAt(i);\n        int overrideCount = overrides.size();\n        dest.writeInt(rendererIndex);\n        dest.writeInt(overrideCount);\n        for (Map.Entry<TrackGroupArray, SelectionOverride> override : 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, SelectionOverride>> first,\n        SparseArray<Map<TrackGroupArray, 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, SelectionOverride> first,\n        Map<TrackGroupArray, SelectionOverride> second) {\n      int firstSize = first.size();\n      if (second.size() != firstSize) {\n        return false;\n      }\n      for (Map.Entry<TrackGroupArray, SelectionOverride> firstEntry : 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 Parcelable.Creator<SelectionOverride> CREATOR =\n        new Parcelable.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  public DefaultTrackSelector() {\n    this(new AdaptiveTrackSelection.Factory());\n  }\n\n  /**\n   * @deprecated Use {@link #DefaultTrackSelector()} instead. Custom bandwidth meter should be\n   *     directly passed to the player in {@link ExoPlayerFactory}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DefaultTrackSelector(BandwidthMeter bandwidthMeter) {\n    this(new AdaptiveTrackSelection.Factory(bandwidthMeter));\n  }\n\n  /** @param trackSelectionFactory A factory for {@link TrackSelection}s. */\n  public DefaultTrackSelector(TrackSelection.Factory trackSelectionFactory) {\n    this.trackSelectionFactory = trackSelectionFactory;\n    parametersReference = new AtomicReference<>(Parameters.DEFAULT);\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, 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          int[][][] rendererFormatSupports,\n          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 result of {@link RendererCapabilities#supportsFormat} for\n   *     each mapped track, indexed by renderer, track group and track (in that order).\n   * @param rendererMixedMimeTypeAdaptationSupports The result of {@link\n   *     RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each 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      int[][][] rendererFormatSupports,\n      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 result of {@link RendererCapabilities#supportsFormat} for each mapped\n   *     track, indexed by track group index and track index (in that order).\n   * @param mixedMimeTypeAdaptationSupports The result of {@link\n   *     RendererCapabilities#supportsMixedMimeTypeAdaptation()} 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      int[][] formatSupports,\n      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      int[][] formatSupport,\n      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      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      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      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      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, 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      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 result of {@link RendererCapabilities#supportsFormat} for each mapped\n   *     track, indexed by track group index and track index (in that order).\n   * @param mixedMimeTypeAdaptationSupports The result of {@link\n   *     RendererCapabilities#supportsMixedMimeTypeAdaptation()} 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      int[][] formatSupports,\n      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      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      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      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      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 result of {@link RendererCapabilities#supportsFormat} for each mapped\n   *     track, indexed by track group index and track index (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      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      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 result of {@link RendererCapabilities#supportsFormat} for each mapped\n   *     track, indexed by track group index and track index (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, 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      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 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      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 result of {@link RendererCapabilities#supportsFormat} for each track,\n   *     indexed by group index and track 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      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      int trackFormatSupport = formatSupports[trackGroupIndex][selection.getIndexInTrackGroup(i)];\n      if ((trackFormatSupport & RendererCapabilities.TUNNELING_SUPPORT_MASK)\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   * Applies the {@link RendererCapabilities#FORMAT_SUPPORT_MASK} to a value obtained from\n   * {@link RendererCapabilities#supportsFormat(Format)}, returning true if the result is\n   * {@link RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set\n   * and the result is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}.\n   *\n   * @param formatSupport A value obtained from {@link RendererCapabilities#supportsFormat(Format)}.\n   * @param allowExceedsCapabilities Whether to return true if the format support component of the\n   *     value is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}.\n   * @return True if the format support component is {@link RendererCapabilities#FORMAT_HANDLED}, or\n   *     if {@code allowExceedsCapabilities} is set and the format support component is\n   *     {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}.\n   */\n  protected static boolean isSupported(int formatSupport, boolean allowExceedsCapabilities) {\n    int maskedSupport = formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK;\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, 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        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-amzn-2.10.6/library/core/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    private final @Nullable 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  private final @Nullable 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  public @Nullable Object getSelectionData() {\n    return data;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport android.util.Pair;\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.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    private final int[] rendererMixedMimeTypeAdaptiveSupports;\n    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 result of {@link\n     *     RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer.\n     * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for\n     *     each mapped track, indexed by 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        int[] rendererMixedMimeTypeAdaptiveSupports,\n        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 One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, {@link\n     *     #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, {@link\n     *     #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}.\n     */\n    public @RendererSupport int getRendererSupport(int rendererIndex) {\n      int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS;\n      int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex];\n      for (int i = 0; i < rendererFormatSupport.length; i++) {\n        for (int j = 0; j < rendererFormatSupport[i].length; j++) {\n          int trackRendererSupport;\n          switch (rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) {\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            default:\n              trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS;\n              break;\n          }\n          bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport);\n        }\n      }\n      return bestRendererSupport;\n    }\n\n    /** @deprecated Use {@link #getTypeSupport(int)}. */\n    @Deprecated\n    public @RendererSupport 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 One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, {@link\n     *     #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, {@link\n     *     #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}.\n     */\n    public @RendererSupport int getTypeSupport(int trackType) {\n      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    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 One of {@link RendererCapabilities#FORMAT_HANDLED}, {@link\n     *     RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, {@link\n     *     RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, {@link\n     *     RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and {@link\n     *     RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}.\n     */\n    public int getTrackSupport(int rendererIndex, int groupIndex, int trackIndex) {\n      return rendererFormatSupports[rendererIndex][groupIndex][trackIndex]\n          & RendererCapabilities.FORMAT_SUPPORT_MASK;\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 One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, {@link\n     *     RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and {@link\n     *     RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}.\n     */\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        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 One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, {@link\n     *     RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and {@link\n     *     RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}.\n     */\n    public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) {\n      int handledTrackCount = 0;\n      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                rendererFormatSupports[rendererIndex][groupIndex][i]\n                    & RendererCapabilities.ADAPTIVE_SUPPORT_MASK);\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  private @Nullable 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    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    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      int[] rendererFormatSupport = rendererIndex == rendererCapabilities.length\n          ? new int[group.length] : 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 result of {@link RendererCapabilities#supportsFormat} for\n   *     each mapped track, indexed by renderer, track group and track (in that order).\n   * @param rendererMixedMimeTypeAdaptationSupport The result of {@link\n   *     RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each 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          int[][][] rendererFormatSupports,\n          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    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        int formatSupportLevel = rendererCapability.supportsFormat(group.getFormat(trackIndex))\n            & RendererCapabilities.FORMAT_SUPPORT_MASK;\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\n   * {@link 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 the result of calling\n   *     {@link RendererCapabilities#supportsFormat} for each track in the group.\n   * @throws ExoPlaybackException If an error occurs determining the format support.\n   */\n  private static int[] getFormatSupport(RendererCapabilities rendererCapabilities, TrackGroup group)\n      throws ExoPlaybackException {\n    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 result of calling {@link\n   *     RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer.\n   * @throws ExoPlaybackException If an error occurs determining the adaptation support.\n   */\n  private static int[] getMixedMimeTypeAdaptationSupports(\n      RendererCapabilities[] rendererCapabilities) throws ExoPlaybackException {\n    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-amzn-2.10.6/library/core/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  public @Nullable Object getSelectionData() {\n    return null;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackBitrateEstimator.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.trackselection;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport java.util.List;\n\n/** Estimates track bitrate values. */\npublic interface TrackBitrateEstimator {\n\n  /**\n   * A {@link TrackBitrateEstimator} that returns the bitrate values defined in the track formats.\n   */\n  TrackBitrateEstimator DEFAULT =\n      (formats, queue, iterators, bitrates) ->\n          TrackSelectionUtil.getFormatBitrates(formats, bitrates);\n\n  /**\n   * Returns bitrate values for a set of tracks whose formats are given.\n   *\n   * @param formats The track formats.\n   * @param queue The queue of already buffered {@link MediaChunk} instances. Must not be modified.\n   * @param iterators An array of {@link MediaChunkIterator}s providing information about the\n   *     sequence of upcoming media chunks for each track.\n   * @param bitrates An array into which the bitrate values will be written. If non-null, this array\n   *     is the one that will be returned.\n   * @return Bitrate values for the tracks. As long as the format of a track has set bitrate, a\n   *     bitrate value is set in the returned array. Otherwise it might be set to {@link\n   *     Format#NO_VALUE}.\n   */\n  int[] getBitrates(\n      Format[] formats,\n      List<? extends MediaChunk> queue,\n      MediaChunkIterator[] iterators,\n      @Nullable int[] bitrates);\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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.trackselection.TrackSelectionUtil.AdaptiveTrackSelectionFactory;\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     * @deprecated Implement {@link #createTrackSelections(Definition[], BandwidthMeter)} instead.\n     *     Calling {@link TrackSelectionUtil#createTrackSelectionsForDefinitions(Definition[],\n     *     AdaptiveTrackSelectionFactory)} helps to create a single adaptive track selection in the\n     *     same way as using this deprecated method.\n     */\n    @Deprecated\n    default TrackSelection createTrackSelection(\n        TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) {\n      throw new UnsupportedOperationException();\n    }\n\n    /**\n     * Creates a new selection for each {@link Definition}.\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    @SuppressWarnings(\"deprecation\")\n    default @NullableType TrackSelection[] createTrackSelections(\n        @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {\n      return TrackSelectionUtil.createTrackSelectionsForDefinitions(\n          definitions,\n          definition -> createTrackSelection(definition.group, bandwidthMeter, definition.tracks));\n    }\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   * @deprecated Use and implement {@link #updateSelectedTrack(long, long, long, List,\n   *     MediaChunkIterator[])} instead.\n   */\n  @Deprecated\n  default void updateSelectedTrack(\n      long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) {\n    throw new UnsupportedOperationException();\n  }\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  default void updateSelectedTrack(\n      long playbackPositionUs,\n      long bufferedDurationUs,\n      long availableDurationUs,\n      List<? extends MediaChunk> queue,\n      MediaChunkIterator[] mediaChunkIterators) {\n    updateSelectedTrack(playbackPositionUs, bufferedDurationUs, availableDurationUs);\n  }\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-amzn-2.10.6/library/core/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  public @Nullable 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-amzn-2.10.6/library/core/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.os.Parcel;\nimport android.os.Parcelable;\nimport androidx.annotation.Nullable;\nimport android.text.TextUtils;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Util;\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    // Audio\n    @Nullable /* package */ String preferredAudioLanguage;\n    // Text\n    @Nullable /* package */ String preferredTextLanguage;\n    @C.RoleFlags /* package */ int preferredTextRoleFlags;\n    /* package */ boolean selectUndeterminedTextLanguage;\n    @C.SelectionFlags /* package */ int disabledTextTrackSelectionFlags;\n\n    /** Creates a builder with default initial values. */\n    public Builder() {\n      this(DEFAULT);\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      // Audio\n      preferredAudioLanguage = initialValues.preferredAudioLanguage;\n      // Text\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    // Text\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\n  /** An instance with default values. */\n  public static final TrackSelectionParameters DEFAULT = new TrackSelectionParameters();\n\n  /**\n   * The preferred language for audio and forced text tracks, as an ISO 639-2/T tag. {@code null}\n   * selects the default track, or the first track if there's no default. The default value is\n   * {@code null}.\n   */\n  @Nullable public final String preferredAudioLanguage;\n  // Text\n  /**\n   * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the default\n   * track if there is one, or no track otherwise. The default value is {@code null}.\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}.\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    this(\n        /* preferredAudioLanguage= */ null,\n        // Text\n        /* preferredTextLanguage= */ null,\n        /* preferredTextRoleFlags= */ 0,\n        /* selectUndeterminedTextLanguage= */ false,\n        /* disabledTextTrackSelectionFlags= */ 0);\n  }\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    // Audio\n    this.preferredAudioLanguage = in.readString();\n    // Text\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        // Text\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    // Audio\n    result = 31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode());\n    // Text\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    // Audio\n    dest.writeString(preferredAudioLanguage);\n    // Text\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-amzn-2.10.6/library/core/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 androidx.annotation.VisibleForTesting;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\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.source.chunk.MediaChunkListIterator;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;\nimport com.google.android.exoplayer2.trackselection.TrackSelection.Definition;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.Arrays;\nimport java.util.List;\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  /**\n   * Returns average bitrate for chunks in bits per second. Chunks are included in average until\n   * {@code maxDurationMs} or the first unknown length chunk.\n   *\n   * @param iterator Iterator for media chunk sequences.\n   * @param maxDurationUs Maximum duration of chunks to be included in average bitrate, in\n   *     microseconds.\n   * @return Average bitrate for chunks in bits per second, or {@link Format#NO_VALUE} if there are\n   *     no chunks or the first chunk length is unknown.\n   */\n  public static int getAverageBitrate(MediaChunkIterator iterator, long maxDurationUs) {\n    long totalDurationUs = 0;\n    long totalLength = 0;\n    while (iterator.next()) {\n      long chunkLength = iterator.getDataSpec().length;\n      if (chunkLength == C.LENGTH_UNSET) {\n        break;\n      }\n      long chunkDurationUs = iterator.getChunkEndTimeUs() - iterator.getChunkStartTimeUs();\n      if (totalDurationUs + chunkDurationUs >= maxDurationUs) {\n        totalLength += chunkLength * (maxDurationUs - totalDurationUs) / chunkDurationUs;\n        totalDurationUs = maxDurationUs;\n        break;\n      }\n      totalDurationUs += chunkDurationUs;\n      totalLength += chunkLength;\n    }\n    return totalDurationUs == 0\n        ? Format.NO_VALUE\n        : (int) (totalLength * C.BITS_PER_BYTE * C.MICROS_PER_SECOND / totalDurationUs);\n  }\n\n  /**\n   * Returns bitrate values for a set of tracks whose upcoming media chunk iterators and formats are\n   * given.\n   *\n   * <p>If an average bitrate can't be calculated, an estimation is calculated using average bitrate\n   * of another track and the ratio of the bitrate values defined in the formats of the two tracks.\n   *\n   * @param iterators An array of {@link MediaChunkIterator}s providing information about the\n   *     sequence of upcoming media chunks for each track.\n   * @param formats The track formats.\n   * @param maxDurationUs Maximum duration of chunks to be included in average bitrate values, in\n   *     microseconds.\n   * @param bitrates If not null, stores bitrate values in this array.\n   * @return Average bitrate values for the tracks. If for a track, an average bitrate or an\n   *     estimation can't be calculated, {@link Format#NO_VALUE} is set.\n   * @see #getAverageBitrate(MediaChunkIterator, long)\n   */\n  @VisibleForTesting\n  /* package */ static int[] getBitratesUsingFutureInfo(\n      MediaChunkIterator[] iterators,\n      Format[] formats,\n      long maxDurationUs,\n      @Nullable int[] bitrates) {\n    int trackCount = iterators.length;\n    Assertions.checkArgument(trackCount == formats.length);\n    if (trackCount == 0) {\n      return new int[0];\n    }\n    if (bitrates == null) {\n      bitrates = new int[trackCount];\n    }\n    if (maxDurationUs == 0) {\n      Arrays.fill(bitrates, Format.NO_VALUE);\n      return bitrates;\n    }\n\n    int[] formatBitrates = new int[trackCount];\n    float[] bitrateRatios = new float[trackCount];\n    boolean needEstimateBitrate = false;\n    boolean canEstimateBitrate = false;\n    for (int i = 0; i < trackCount; i++) {\n      int bitrate = getAverageBitrate(iterators[i], maxDurationUs);\n      if (bitrate != Format.NO_VALUE) {\n        int formatBitrate = formats[i].bitrate;\n        formatBitrates[i] = formatBitrate;\n        if (formatBitrate != Format.NO_VALUE) {\n          bitrateRatios[i] = ((float) bitrate) / formatBitrate;\n          canEstimateBitrate = true;\n        }\n      } else {\n        needEstimateBitrate = true;\n        formatBitrates[i] = Format.NO_VALUE;\n      }\n      bitrates[i] = bitrate;\n    }\n\n    if (needEstimateBitrate && canEstimateBitrate) {\n      estimateBitrates(bitrates, formats, formatBitrates, bitrateRatios);\n    }\n    return bitrates;\n  }\n\n  /**\n   * Returns bitrate values for a set of tracks whose formats are given, using the given queue of\n   * already buffered {@link MediaChunk} instances.\n   *\n   * @param queue The queue of already buffered {@link MediaChunk} instances. Must not be modified.\n   * @param formats The track formats.\n   * @param maxDurationUs Maximum duration of chunks to be included in average bitrate values, in\n   *     microseconds.\n   * @param bitrates If not null, calculates bitrate values only for indexes set to Format.NO_VALUE\n   *     and stores result in this array.\n   * @return Bitrate values for the tracks. If for a track, a bitrate value can't be calculated,\n   *     {@link Format#NO_VALUE} is set.\n   * @see #getBitratesUsingFutureInfo(MediaChunkIterator[], Format[], long, int[])\n   */\n  @VisibleForTesting\n  /* package */ static int[] getBitratesUsingPastInfo(\n      List<? extends MediaChunk> queue,\n      Format[] formats,\n      long maxDurationUs,\n      @Nullable int[] bitrates) {\n    if (bitrates == null) {\n      bitrates = new int[formats.length];\n      Arrays.fill(bitrates, Format.NO_VALUE);\n    }\n    if (maxDurationUs == 0) {\n      return bitrates;\n    }\n    int queueAverageBitrate = getAverageQueueBitrate(queue, maxDurationUs);\n    if (queueAverageBitrate == Format.NO_VALUE) {\n      return bitrates;\n    }\n    int queueFormatBitrate = queue.get(queue.size() - 1).trackFormat.bitrate;\n    if (queueFormatBitrate != Format.NO_VALUE) {\n      float queueBitrateRatio = ((float) queueAverageBitrate) / queueFormatBitrate;\n      estimateBitrates(\n          bitrates, formats, new int[] {queueFormatBitrate}, new float[] {queueBitrateRatio});\n    }\n    return bitrates;\n  }\n\n  /**\n   * Returns bitrate values for a set of tracks whose formats are given, using the given upcoming\n   * media chunk iterators and the queue of already buffered {@link MediaChunk}s.\n   *\n   * @param formats The track formats.\n   * @param queue The queue of already buffered {@link MediaChunk}s. Must not be modified.\n   * @param maxPastDurationUs Maximum duration of past chunks to be included in average bitrate\n   *     values, in microseconds.\n   * @param iterators An array of {@link MediaChunkIterator}s providing information about the\n   *     sequence of upcoming media chunks for each track.\n   * @param maxFutureDurationUs Maximum duration of future chunks to be included in average bitrate\n   *     values, in microseconds.\n   * @param useFormatBitrateAsLowerBound Whether to return the estimated bitrate only if it's higher\n   *     than the bitrate of the track's format.\n   * @param bitrates An array into which the bitrate values will be written. If non-null, this array\n   *     is the one that will be returned.\n   * @return Bitrate values for the tracks. As long as the format of a track has set bitrate, a\n   *     bitrate value is set in the returned array. Otherwise it might be set to {@link\n   *     Format#NO_VALUE}.\n   */\n  public static int[] getBitratesUsingPastAndFutureInfo(\n      Format[] formats,\n      List<? extends MediaChunk> queue,\n      long maxPastDurationUs,\n      MediaChunkIterator[] iterators,\n      long maxFutureDurationUs,\n      boolean useFormatBitrateAsLowerBound,\n      @Nullable int[] bitrates) {\n    bitrates = getBitratesUsingFutureInfo(iterators, formats, maxFutureDurationUs, bitrates);\n    getBitratesUsingPastInfo(queue, formats, maxPastDurationUs, bitrates);\n    for (int i = 0; i < bitrates.length; i++) {\n      int bitrate = bitrates[i];\n      if (bitrate == Format.NO_VALUE\n          || (useFormatBitrateAsLowerBound\n              && formats[i].bitrate != Format.NO_VALUE\n              && bitrate < formats[i].bitrate)) {\n        bitrates[i] = formats[i].bitrate;\n      }\n    }\n    return bitrates;\n  }\n\n  /**\n   * Returns an array containing {@link Format#bitrate} values for given each format in order.\n   *\n   * @param formats The format array to copy {@link Format#bitrate} values.\n   * @param bitrates If not null, stores bitrate values in this array.\n   * @return An array containing {@link Format#bitrate} values for given each format in order.\n   */\n  public static int[] getFormatBitrates(Format[] formats, @Nullable int[] bitrates) {\n    int trackCount = formats.length;\n    if (bitrates == null) {\n      bitrates = new int[trackCount];\n    }\n    for (int i = 0; i < trackCount; i++) {\n      bitrates[i] = formats[i].bitrate;\n    }\n    return bitrates;\n  }\n\n  /**\n   * Fills missing values in the given {@code bitrates} array by calculates an estimation using the\n   * closest reference bitrate value.\n   *\n   * @param bitrates An array of bitrates to be filled with estimations. Missing values are set to\n   *     {@link Format#NO_VALUE}.\n   * @param formats An array of formats, one for each bitrate.\n   * @param referenceBitrates An array of reference bitrates which are used to calculate\n   *     estimations.\n   * @param referenceBitrateRatios An array containing ratio of reference bitrates to their bitrate\n   *     estimates.\n   */\n  private static void estimateBitrates(\n      int[] bitrates, Format[] formats, int[] referenceBitrates, float[] referenceBitrateRatios) {\n    for (int i = 0; i < bitrates.length; i++) {\n      if (bitrates[i] == Format.NO_VALUE) {\n        int formatBitrate = formats[i].bitrate;\n        if (formatBitrate != Format.NO_VALUE) {\n          int closestReferenceBitrateIndex =\n              getClosestBitrateIndex(formatBitrate, referenceBitrates);\n          bitrates[i] =\n              (int) (referenceBitrateRatios[closestReferenceBitrateIndex] * formatBitrate);\n        }\n      }\n    }\n  }\n\n  private static int getAverageQueueBitrate(List<? extends MediaChunk> queue, long maxDurationUs) {\n    if (queue.isEmpty()) {\n      return Format.NO_VALUE;\n    }\n    MediaChunkListIterator iterator =\n        new MediaChunkListIterator(getSingleFormatSubQueue(queue), /* reverseOrder= */ true);\n    return getAverageBitrate(iterator, maxDurationUs);\n  }\n\n  private static List<? extends MediaChunk> getSingleFormatSubQueue(\n      List<? extends MediaChunk> queue) {\n    Format queueFormat = queue.get(queue.size() - 1).trackFormat;\n    int queueSize = queue.size();\n    for (int i = queueSize - 2; i >= 0; i--) {\n      if (!queue.get(i).trackFormat.equals(queueFormat)) {\n        return queue.subList(i + 1, queueSize);\n      }\n    }\n    return queue;\n  }\n\n  private static int getClosestBitrateIndex(int formatBitrate, int[] formatBitrates) {\n    int closestDistance = Integer.MAX_VALUE;\n    int closestFormat = C.INDEX_UNSET;\n    for (int j = 0; j < formatBitrates.length; j++) {\n      if (formatBitrates[j] != Format.NO_VALUE) {\n        int distance = Math.abs(formatBitrates[j] - formatBitrate);\n        if (distance < closestDistance) {\n          closestDistance = distance;\n          closestFormat = j;\n        }\n      }\n    }\n    return closestFormat;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 * determined 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  private @Nullable InvalidationListener listener;\n  private @Nullable 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-amzn-2.10.6/library/core/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 renderersEnabled array, track selections, and\n   * configurations for the 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-amzn-2.10.6/library/core/src/main/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimator.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.trackselection;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport java.util.List;\n\n/** A {@link TrackBitrateEstimator} which derives estimates from a window of time. */\npublic final class WindowedTrackBitrateEstimator implements TrackBitrateEstimator {\n\n  private final long maxPastDurationUs;\n  private final long maxFutureDurationUs;\n  private final boolean useFormatBitrateAsLowerBound;\n\n  /**\n   * @param maxPastDurationMs Maximum duration of past chunks to be included in average bitrate\n   *     values, in milliseconds.\n   * @param maxFutureDurationMs Maximum duration of future chunks to be included in average bitrate\n   *     values, in milliseconds.\n   * @param useFormatBitrateAsLowerBound Whether to use the bitrate of the track's format as a lower\n   *     bound for the estimated bitrate.\n   */\n  public WindowedTrackBitrateEstimator(\n      long maxPastDurationMs, long maxFutureDurationMs, boolean useFormatBitrateAsLowerBound) {\n    this.maxPastDurationUs = C.msToUs(maxPastDurationMs);\n    this.maxFutureDurationUs = C.msToUs(maxFutureDurationMs);\n    this.useFormatBitrateAsLowerBound = useFormatBitrateAsLowerBound;\n  }\n\n  @Override\n  public int[] getBitrates(\n      Format[] formats,\n      List<? extends MediaChunk> queue,\n      MediaChunkIterator[] iterators,\n      @Nullable int[] bitrates) {\n    if (maxFutureDurationUs > 0 || maxPastDurationUs > 0) {\n      return TrackSelectionUtil.getBitratesUsingPastAndFutureInfo(\n          formats,\n          queue,\n          maxPastDurationUs,\n          iterators,\n          maxFutureDurationUs,\n          useFormatBitrateAsLowerBound,\n          bitrates);\n    }\n    return TrackSelectionUtil.getFormatBitrates(formats, bitrates);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 android.content.Context;\nimport android.content.res.AssetManager;\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\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  private @Nullable Uri uri;\n  private @Nullable 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  /**\n   * @param context A context.\n   * @param listener An optional listener.\n   * @deprecated Use {@link #AssetDataSource(Context)} and {@link\n   *     #addTransferListener(TransferListener)}.\n   */\n  @Deprecated\n  public AssetDataSource(Context context, @Nullable TransferListener listener) {\n    this(context);\n    if (listener != null) {\n      addTransferListener(listener);\n    }\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws AssetDataSourceException {\n    try {\n      uri = dataSpec.uri;\n      String path = 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 = 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  public @Nullable 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  private @Nullable 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-amzn-2.10.6/library/core/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 com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\n\n/**\n * A {@link DataSink} for writing to a byte array.\n */\npublic final class ByteArrayDataSink implements DataSink {\n\n  private ByteArrayOutputStream stream;\n\n  @Override\n  public void open(DataSpec dataSpec) throws IOException {\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    stream.close();\n  }\n\n  @Override\n  public void write(byte[] buffer, int offset, int length) throws IOException {\n    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  public byte[] getData() {\n    return stream == null ? null : stream.toByteArray();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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  private @Nullable 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) throws IOException {\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  public @Nullable Uri getUri() {\n    return uri;\n  }\n\n  @Override\n  public void close() throws IOException {\n    if (opened) {\n      opened = false;\n      transferEnded();\n    }\n    uri = null;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 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  private @Nullable Uri uri;\n  private @Nullable AssetFileDescriptor assetFileDescriptor;\n  private @Nullable 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  /**\n   * @param context A context.\n   * @param listener An optional listener.\n   * @deprecated Use {@link #ContentDataSource(Context)} and {@link\n   *     #addTransferListener(TransferListener)}.\n   */\n  @Deprecated\n  public ContentDataSource(Context context, @Nullable TransferListener listener) {\n    this(context);\n    if (listener != null) {\n      addTransferListener(listener);\n    }\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws ContentDataSourceException {\n    try {\n      uri = dataSpec.uri;\n      transferInitializing(dataSpec);\n      assetFileDescriptor = resolver.openAssetFileDescriptor(uri, \"r\");\n      if (assetFileDescriptor == null) {\n        throw new FileNotFoundException(\"Could not open file descriptor for: \" + uri);\n      }\n      inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());\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 = 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  public @Nullable 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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.util.Base64;\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  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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 com.liskovsoft.sharedutils.helpers.Helpers;\n\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_ALLOW_ICY_METADATA}, {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN}\n   * and {@link #FLAG_ALLOW_CACHE_FRAGMENTATION}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef(\n      flag = true,\n      value = {\n        FLAG_ALLOW_GZIP,\n        FLAG_ALLOW_ICY_METADATA,\n        FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN,\n        FLAG_ALLOW_CACHE_FRAGMENTATION\n      })\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  /** Allows an underlying network stack to request that the stream contain ICY metadata. */\n  public static final int FLAG_ALLOW_ICY_METADATA = 1 << 1; // 2\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 << 2; // 4\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 << 3; // 8\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 body, null otherwise. If the body is non-null, then httpBody.length will be non-zero.\n   */\n  public final @Nullable byte[] httpBody;\n\n  /** @deprecated Use {@link #httpBody} instead. */\n  @Deprecated public final @Nullable byte[] postBody;\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  public final @Nullable 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} 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= */ postBody != null ? HTTP_METHOD_POST : HTTP_METHOD_GET,\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.postBody = this.httpBody;\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    //applyRangeQuery();\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   * MOD: Google throttle fix. Add range param to the video url. Found on googlevideo.com<br/>\n   * Source: https://github.com/yt-dlp/yt-dlp/issues/6369#issuecomment-1447763187<br/>\n   * Also see: {@link DefaultHttpDataSource}\n   */\n  private void applyRangeQuery() {\n    if (uri.getHost() != null && uri.getHost().endsWith(\".googlevideo.com\")\n            && !(position == 0 && length == C.LENGTH_UNSET)) {\n      String rangeRequest = \"range=\" + position + \"-\";\n      if (length != C.LENGTH_UNSET) {\n        rangeRequest += (position + length - 1);\n      }\n\n      String url = uri.toString() + \"&\" + rangeRequest;\n\n      Helpers.setField(this, \"uri\", Uri.parse(url));\n      // Required to skip Range header processing and bypass skipping when responseCode == 200 (Stream EOF fix)\n      Helpers.setField(this, \"position\", 0);\n      Helpers.setField(this, \"length\", C.LENGTH_UNSET);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.util.SparseArray;\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_400_000, 3_400_000, 1_900_000, 1_100_000, 400_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[] {170_000, 139_000, 122_000, 107_000, 90_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_100_000, 1_300_000, 960_000, 770_000, 450_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[] {6_000_000, 3_400_000, 2_100_000, 1_400_000, 570_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  /** 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  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 * 8000) / 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    @MonotonicNonNull private static 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, 0, 0, 1});\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, 2, 1, 1});\n    countryGroupAssignment.put(\"AI\", new int[] {1, 0, 1, 3});\n    countryGroupAssignment.put(\"AL\", new int[] {1, 2, 1, 1});\n    countryGroupAssignment.put(\"AM\", new int[] {2, 2, 3, 2});\n    countryGroupAssignment.put(\"AO\", new int[] {3, 4, 2, 0});\n    countryGroupAssignment.put(\"AQ\", new int[] {4, 2, 2, 2});\n    countryGroupAssignment.put(\"AR\", new int[] {2, 3, 2, 2});\n    countryGroupAssignment.put(\"AS\", new int[] {3, 3, 4, 1});\n    countryGroupAssignment.put(\"AT\", new int[] {0, 2, 0, 0});\n    countryGroupAssignment.put(\"AU\", new int[] {0, 1, 1, 1});\n    countryGroupAssignment.put(\"AW\", new int[] {1, 1, 0, 2});\n    countryGroupAssignment.put(\"AX\", new int[] {0, 2, 1, 0});\n    countryGroupAssignment.put(\"AZ\", new int[] {3, 3, 2, 2});\n    countryGroupAssignment.put(\"BA\", new int[] {1, 1, 1, 2});\n    countryGroupAssignment.put(\"BB\", new int[] {0, 1, 0, 0});\n    countryGroupAssignment.put(\"BD\", new int[] {2, 2, 3, 2});\n    countryGroupAssignment.put(\"BE\", new int[] {0, 0, 0, 1});\n    countryGroupAssignment.put(\"BF\", new int[] {4, 4, 3, 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, 3, 4, 4});\n    countryGroupAssignment.put(\"BJ\", new int[] {4, 3, 4, 4});\n    countryGroupAssignment.put(\"BL\", new int[] {1, 0, 2, 3});\n    countryGroupAssignment.put(\"BM\", new int[] {1, 0, 0, 0});\n    countryGroupAssignment.put(\"BN\", new int[] {4, 2, 3, 3});\n    countryGroupAssignment.put(\"BO\", new int[] {2, 2, 3, 2});\n    countryGroupAssignment.put(\"BQ\", new int[] {1, 0, 3, 4});\n    countryGroupAssignment.put(\"BR\", new int[] {2, 3, 3, 2});\n    countryGroupAssignment.put(\"BS\", new int[] {2, 0, 1, 4});\n    countryGroupAssignment.put(\"BT\", new int[] {3, 0, 2, 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, 3, 1});\n    countryGroupAssignment.put(\"CA\", new int[] {0, 3, 3, 3});\n    countryGroupAssignment.put(\"CD\", new int[] {4, 4, 3, 2});\n    countryGroupAssignment.put(\"CF\", new int[] {4, 3, 3, 4});\n    countryGroupAssignment.put(\"CG\", new int[] {4, 4, 4, 4});\n    countryGroupAssignment.put(\"CH\", new int[] {0, 0, 1, 1});\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[] {2, 2, 2, 3});\n    countryGroupAssignment.put(\"CM\", new int[] {3, 4, 2, 1});\n    countryGroupAssignment.put(\"CN\", new int[] {2, 2, 2, 3});\n    countryGroupAssignment.put(\"CO\", new int[] {2, 3, 2, 2});\n    countryGroupAssignment.put(\"CR\", new int[] {2, 2, 4, 4});\n    countryGroupAssignment.put(\"CU\", new int[] {4, 4, 3, 1});\n    countryGroupAssignment.put(\"CV\", new int[] {2, 3, 2, 4});\n    countryGroupAssignment.put(\"CW\", new int[] {1, 0, 0, 0});\n    countryGroupAssignment.put(\"CX\", new int[] {2, 2, 2, 2});\n    countryGroupAssignment.put(\"CY\", new int[] {1, 1, 1, 1});\n    countryGroupAssignment.put(\"CZ\", new int[] {0, 1, 0, 0});\n    countryGroupAssignment.put(\"DE\", new int[] {0, 2, 2, 2});\n    countryGroupAssignment.put(\"DJ\", new int[] {3, 3, 4, 0});\n    countryGroupAssignment.put(\"DK\", new int[] {0, 0, 0, 0});\n    countryGroupAssignment.put(\"DM\", new int[] {1, 0, 0, 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, 4, 4, 2});\n    countryGroupAssignment.put(\"EE\", new int[] {0, 0, 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, 2});\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, 1, 3, 3});\n    countryGroupAssignment.put(\"FK\", new int[] {4, 2, 2, 3});\n    countryGroupAssignment.put(\"FM\", new int[] {4, 2, 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, 1});\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, 0, 3});\n    countryGroupAssignment.put(\"GF\", new int[] {1, 2, 4, 4});\n    countryGroupAssignment.put(\"GG\", new int[] {0, 0, 0, 0});\n    countryGroupAssignment.put(\"GH\", new int[] {3, 3, 3, 2});\n    countryGroupAssignment.put(\"GI\", new int[] {0, 0, 0, 1});\n    countryGroupAssignment.put(\"GL\", new int[] {2, 2, 3, 4});\n    countryGroupAssignment.put(\"GM\", new int[] {4, 3, 3, 2});\n    countryGroupAssignment.put(\"GN\", new int[] {4, 4, 4, 0});\n    countryGroupAssignment.put(\"GP\", new int[] {2, 2, 1, 3});\n    countryGroupAssignment.put(\"GQ\", new int[] {4, 3, 3, 0});\n    countryGroupAssignment.put(\"GR\", new int[] {1, 1, 0, 1});\n    countryGroupAssignment.put(\"GT\", new int[] {3, 3, 3, 4});\n    countryGroupAssignment.put(\"GU\", new int[] {1, 2, 4, 4});\n    countryGroupAssignment.put(\"GW\", new int[] {4, 4, 4, 0});\n    countryGroupAssignment.put(\"GY\", new int[] {3, 4, 1, 0});\n    countryGroupAssignment.put(\"HK\", new int[] {0, 1, 4, 4});\n    countryGroupAssignment.put(\"HN\", new int[] {3, 3, 2, 2});\n    countryGroupAssignment.put(\"HR\", new int[] {1, 0, 0, 2});\n    countryGroupAssignment.put(\"HT\", new int[] {3, 4, 4, 3});\n    countryGroupAssignment.put(\"HU\", new int[] {0, 0, 1, 0});\n    countryGroupAssignment.put(\"ID\", new int[] {3, 2, 3, 4});\n    countryGroupAssignment.put(\"IE\", new int[] {0, 0, 3, 2});\n    countryGroupAssignment.put(\"IL\", new int[] {0, 1, 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, 4, 2, 2});\n    countryGroupAssignment.put(\"IQ\", new int[] {3, 3, 4, 4});\n    countryGroupAssignment.put(\"IR\", new int[] {1, 0, 1, 0});\n    countryGroupAssignment.put(\"IS\", new int[] {0, 0, 0, 0});\n    countryGroupAssignment.put(\"IT\", new int[] {1, 0, 1, 1});\n    countryGroupAssignment.put(\"JE\", new int[] {1, 0, 0, 1});\n    countryGroupAssignment.put(\"JM\", new int[] {3, 2, 2, 1});\n    countryGroupAssignment.put(\"JO\", new int[] {1, 1, 1, 2});\n    countryGroupAssignment.put(\"JP\", new int[] {0, 2, 2, 2});\n    countryGroupAssignment.put(\"KE\", new int[] {3, 3, 3, 3});\n    countryGroupAssignment.put(\"KG\", new int[] {1, 1, 2, 3});\n    countryGroupAssignment.put(\"KH\", new int[] {2, 0, 4, 4});\n    countryGroupAssignment.put(\"KI\", new int[] {4, 4, 4, 4});\n    countryGroupAssignment.put(\"KM\", new int[] {4, 4, 3, 3});\n    countryGroupAssignment.put(\"KN\", new int[] {1, 0, 1, 4});\n    countryGroupAssignment.put(\"KP\", new int[] {1, 2, 0, 2});\n    countryGroupAssignment.put(\"KR\", new int[] {0, 3, 0, 2});\n    countryGroupAssignment.put(\"KW\", new int[] {2, 2, 1, 2});\n    countryGroupAssignment.put(\"KY\", new int[] {1, 1, 0, 2});\n    countryGroupAssignment.put(\"KZ\", new int[] {1, 2, 2, 2});\n    countryGroupAssignment.put(\"LA\", new int[] {2, 1, 1, 0});\n    countryGroupAssignment.put(\"LB\", new int[] {3, 2, 0, 0});\n    countryGroupAssignment.put(\"LC\", new int[] {2, 1, 0, 0});\n    countryGroupAssignment.put(\"LI\", new int[] {0, 0, 2, 2});\n    countryGroupAssignment.put(\"LK\", new int[] {1, 1, 2, 2});\n    countryGroupAssignment.put(\"LR\", new int[] {3, 4, 4, 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, 1, 0, 0});\n    countryGroupAssignment.put(\"LV\", new int[] {0, 0, 0, 0});\n    countryGroupAssignment.put(\"LY\", new int[] {4, 3, 4, 4});\n    countryGroupAssignment.put(\"MA\", new int[] {2, 1, 2, 2});\n    countryGroupAssignment.put(\"MC\", new int[] {1, 0, 1, 0});\n    countryGroupAssignment.put(\"MD\", new int[] {1, 1, 0, 0});\n    countryGroupAssignment.put(\"ME\", new int[] {1, 2, 2, 3});\n    countryGroupAssignment.put(\"MF\", new int[] {1, 4, 2, 1});\n    countryGroupAssignment.put(\"MG\", new int[] {3, 4, 1, 3});\n    countryGroupAssignment.put(\"MH\", new int[] {4, 0, 2, 3});\n    countryGroupAssignment.put(\"MK\", new int[] {1, 0, 0, 0});\n    countryGroupAssignment.put(\"ML\", new int[] {4, 4, 4, 3});\n    countryGroupAssignment.put(\"MM\", new int[] {2, 3, 1, 2});\n    countryGroupAssignment.put(\"MN\", new int[] {2, 3, 2, 4});\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[] {1, 1, 1, 3});\n    countryGroupAssignment.put(\"MR\", new int[] {4, 4, 4, 4});\n    countryGroupAssignment.put(\"MS\", new int[] {1, 4, 0, 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[] {3, 2, 1, 1});\n    countryGroupAssignment.put(\"MW\", new int[] {4, 2, 1, 1});\n    countryGroupAssignment.put(\"MX\", new int[] {2, 4, 3, 2});\n    countryGroupAssignment.put(\"MY\", new int[] {2, 2, 2, 3});\n    countryGroupAssignment.put(\"MZ\", new int[] {3, 4, 2, 2});\n    countryGroupAssignment.put(\"NA\", new int[] {3, 2, 2, 1});\n    countryGroupAssignment.put(\"NC\", new int[] {2, 1, 3, 2});\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, 2});\n    countryGroupAssignment.put(\"NI\", new int[] {3, 3, 3, 4});\n    countryGroupAssignment.put(\"NL\", new int[] {0, 2, 4, 3});\n    countryGroupAssignment.put(\"NO\", new int[] {0, 1, 0, 0});\n    countryGroupAssignment.put(\"NP\", new int[] {3, 3, 2, 2});\n    countryGroupAssignment.put(\"NR\", new int[] {4, 0, 4, 0});\n    countryGroupAssignment.put(\"NU\", new int[] {2, 2, 2, 1});\n    countryGroupAssignment.put(\"NZ\", new int[] {0, 0, 0, 1});\n    countryGroupAssignment.put(\"OM\", new int[] {2, 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[] {3, 1, 0, 1});\n    countryGroupAssignment.put(\"PG\", new int[] {4, 3, 1, 1});\n    countryGroupAssignment.put(\"PH\", new int[] {3, 0, 4, 4});\n    countryGroupAssignment.put(\"PK\", new int[] {3, 3, 3, 3});\n    countryGroupAssignment.put(\"PL\", new int[] {1, 1, 1, 3});\n    countryGroupAssignment.put(\"PM\", new int[] {0, 2, 0, 0});\n    countryGroupAssignment.put(\"PR\", new int[] {2, 1, 3, 3});\n    countryGroupAssignment.put(\"PS\", new int[] {3, 3, 1, 4});\n    countryGroupAssignment.put(\"PT\", new int[] {1, 1, 0, 1});\n    countryGroupAssignment.put(\"PW\", new int[] {2, 2, 1, 1});\n    countryGroupAssignment.put(\"PY\", new int[] {3, 1, 3, 3});\n    countryGroupAssignment.put(\"QA\", new int[] {2, 3, 0, 1});\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[] {3, 4, 2, 4});\n    countryGroupAssignment.put(\"SA\", new int[] {2, 2, 1, 2});\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, 2});\n    countryGroupAssignment.put(\"SE\", new int[] {0, 1, 0, 0});\n    countryGroupAssignment.put(\"SG\", new int[] {1, 2, 3, 3});\n    countryGroupAssignment.put(\"SH\", new int[] {4, 4, 2, 3});\n    countryGroupAssignment.put(\"SI\", new int[] {0, 1, 0, 1});\n    countryGroupAssignment.put(\"SJ\", new int[] {0, 0, 2, 0});\n    countryGroupAssignment.put(\"SK\", new int[] {0, 1, 0, 1});\n    countryGroupAssignment.put(\"SL\", new int[] {4, 3, 2, 4});\n    countryGroupAssignment.put(\"SM\", new int[] {0, 0, 1, 3});\n    countryGroupAssignment.put(\"SN\", new int[] {4, 4, 4, 3});\n    countryGroupAssignment.put(\"SO\", new int[] {4, 4, 4, 4});\n    countryGroupAssignment.put(\"SR\", new int[] {3, 2, 2, 4});\n    countryGroupAssignment.put(\"SS\", new int[] {4, 2, 4, 2});\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, 4, 1, 0});\n    countryGroupAssignment.put(\"SZ\", new int[] {3, 4, 2, 3});\n    countryGroupAssignment.put(\"TC\", new int[] {1, 1, 3, 1});\n    countryGroupAssignment.put(\"TD\", new int[] {4, 4, 4, 3});\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, 3});\n    countryGroupAssignment.put(\"TN\", new int[] {2, 1, 1, 1});\n    countryGroupAssignment.put(\"TO\", new int[] {3, 3, 3, 1});\n    countryGroupAssignment.put(\"TR\", new int[] {1, 2, 0, 1});\n    countryGroupAssignment.put(\"TT\", new int[] {2, 3, 1, 2});\n    countryGroupAssignment.put(\"TV\", new int[] {4, 2, 2, 4});\n    countryGroupAssignment.put(\"TW\", new int[] {0, 0, 0, 1});\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, 2, 3});\n    countryGroupAssignment.put(\"US\", new int[] {0, 1, 3, 3});\n    countryGroupAssignment.put(\"UY\", new int[] {2, 2, 2, 2});\n    countryGroupAssignment.put(\"UZ\", new int[] {3, 2, 2, 2});\n    countryGroupAssignment.put(\"VA\", new int[] {1, 2, 2, 2});\n    countryGroupAssignment.put(\"VC\", new int[] {2, 1, 0, 0});\n    countryGroupAssignment.put(\"VE\", new int[] {4, 4, 4, 3});\n    countryGroupAssignment.put(\"VG\", new int[] {2, 1, 1, 2});\n    countryGroupAssignment.put(\"VI\", new int[] {1, 0, 2, 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, 2, 3, 1});\n    countryGroupAssignment.put(\"XK\", new int[] {1, 2, 1, 0});\n    countryGroupAssignment.put(\"YE\", new int[] {4, 4, 4, 2});\n    countryGroupAssignment.put(\"YT\", new int[] {2, 0, 2, 3});\n    countryGroupAssignment.put(\"ZA\", new int[] {2, 3, 2, 2});\n    countryGroupAssignment.put(\"ZM\", new int[] {3, 3, 2, 1});\n    countryGroupAssignment.put(\"ZW\", new int[] {3, 3, 3, 1});\n    return Collections.unmodifiableMap(countryGroupAssignment);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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, TransferListener, String,\n *       boolean)}, or any other schemes supported by a base data source if constructed using {@link\n *       #DefaultDataSource(Context, TransferListener, 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  private @Nullable 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            /* contentTypePredicate= */ null,\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  /**\n   * Constructs a new instance, optionally configured to follow cross-protocol redirects.\n   *\n   * @param context A context.\n   * @param listener An optional listener.\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   * @deprecated Use {@link #DefaultDataSource(Context, String, boolean)} and {@link\n   *     #addTransferListener(TransferListener)}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DefaultDataSource(\n      Context context,\n      @Nullable TransferListener listener,\n      String userAgent,\n      boolean allowCrossProtocolRedirects) {\n    this(context, listener, userAgent, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,\n        DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects);\n  }\n\n  /**\n   * Constructs a new instance, optionally configured to follow cross-protocol redirects.\n   *\n   * @param context A context.\n   * @param listener An optional listener.\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   * @deprecated Use {@link #DefaultDataSource(Context, String, int, int, boolean)} and {@link\n   *     #addTransferListener(TransferListener)}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DefaultDataSource(\n      Context context,\n      @Nullable TransferListener listener,\n      String userAgent,\n      int connectTimeoutMillis,\n      int readTimeoutMillis,\n      boolean allowCrossProtocolRedirects) {\n    this(\n        context,\n        listener,\n        new DefaultHttpDataSource(\n            userAgent,\n            /* contentTypePredicate= */ null,\n            listener,\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 listener An optional listener.\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   * @deprecated Use {@link #DefaultDataSource(Context, DataSource)} and {@link\n   *     #addTransferListener(TransferListener)}.\n   */\n  @Deprecated\n  public DefaultDataSource(\n      Context context, @Nullable TransferListener listener, DataSource baseDataSource) {\n    this(context, baseDataSource);\n    if (listener != null) {\n      transferListeners.add(listener);\n    }\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  public @Nullable 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-amzn-2.10.6/library/core/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  private final @Nullable TransferListener listener;\n  private final DataSource.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, TransferListener, DataSource)\n   */\n  public DefaultDataSourceFactory(Context context, DataSource.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, TransferListener, DataSource)\n   */\n  public DefaultDataSourceFactory(\n      Context context,\n      @Nullable TransferListener listener,\n      DataSource.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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.metadata.icy.IcyHeaders;\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 com.liskovsoft.sharedutils.helpers.NetworkHelpers;\n\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.List;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\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, Predicate, TransferListener, int, int, boolean,\n * RequestProperties)} constructor and passing {@code true} as the second last argument.\n */\npublic class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource {\n\n  /**\n   * The default connection timeout, in milliseconds.\n   */\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  private final @Nullable Predicate<String> contentTypePredicate;\n  private final @Nullable RequestProperties defaultRequestProperties;\n  private final RequestProperties requestProperties;\n\n  private @Nullable DataSpec dataSpec;\n  private @Nullable HttpURLConnection connection;\n  private @Nullable 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, /* contentTypePredicate= */ 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 HttpDataSource.InvalidContentTypeException} is thrown from {@link\n   *     #open(DataSpec)}.\n   */\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 HttpDataSource.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   */\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 HttpDataSource.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   */\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   * @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 HttpDataSource.InvalidContentTypeException} is thrown from {@link\n   *     #open(DataSpec)}.\n   * @param listener An optional listener.\n   * @deprecated Use {@link #DefaultHttpDataSource(String, Predicate)} and {@link\n   *     #addTransferListener(TransferListener)}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DefaultHttpDataSource(\n      String userAgent,\n      @Nullable Predicate<String> contentTypePredicate,\n      @Nullable TransferListener listener) {\n    this(userAgent, contentTypePredicate, listener, 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 HttpDataSource.InvalidContentTypeException} is thrown from {@link\n   *     #open(DataSpec)}.\n   * @param listener An optional listener.\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, Predicate, int, int)} and {@link\n   *     #addTransferListener(TransferListener)}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public DefaultHttpDataSource(\n      String userAgent,\n      @Nullable Predicate<String> contentTypePredicate,\n      @Nullable TransferListener listener,\n      int connectTimeoutMillis,\n      int readTimeoutMillis) {\n    this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false,\n        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 HttpDataSource.InvalidContentTypeException} is thrown from {@link\n   *     #open(DataSpec)}.\n   * @param listener An optional listener.\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, Predicate, int, int, boolean,\n   *     RequestProperties)} and {@link #addTransferListener(TransferListener)}.\n   */\n  @Deprecated\n  public DefaultHttpDataSource(\n      String userAgent,\n      @Nullable Predicate<String> contentTypePredicate,\n      @Nullable TransferListener listener,\n      int connectTimeoutMillis,\n      int readTimeoutMillis,\n      boolean allowCrossProtocolRedirects,\n      @Nullable RequestProperties defaultRequestProperties) {\n    this(\n        userAgent,\n        contentTypePredicate,\n        connectTimeoutMillis,\n        readTimeoutMillis,\n        allowCrossProtocolRedirects,\n        defaultRequestProperties);\n    if (listener != null) {\n      addTransferListener(listener);\n    }\n  }\n\n  @Override\n  public @Nullable 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  @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    if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {\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. Furthermore, there isn't a\n      // reliable way to determine whether the gzip was used or not. Always use the dataSpec length\n      // in this case.\n      bytesToRead = dataSpec.length;\n    }\n\n    try {\n      inputStream = connection.getInputStream();\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    boolean allowIcyMetadata = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA);\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          allowIcyMetadata,\n          /* followRedirects= */ true);\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              allowIcyMetadata,\n              /* followRedirects= */ false);\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 allowIcyMetadata Whether to allow ICY metadata.\n   * @param followRedirects Whether to follow redirects.\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 allowIcyMetadata,\n      boolean followRedirects)\n      throws IOException {\n    // MODIFIED: Add modern TLS ciphers to HttpUrlConnection and custom Dns\n    // https://stackoverflow.com/questions/16299531/how-to-override-the-cipherlist-sent-to-the-server-by-android-when-using-httpsurl\n    HttpURLConnection connection = (HttpURLConnection) url.openConnection();\n    // TODO: Exceptions on API 34 (maybe lowers too). Enable for old api or at least switch to Cronet on error\n    //HttpURLConnection connection = NetworkHelpers.getHttpsURLConnection(url);\n    connection.setConnectTimeout(connectTimeoutMillis);\n    connection.setReadTimeout(readTimeoutMillis);\n    if (defaultRequestProperties != null) {\n      for (Map.Entry<String, String> property : defaultRequestProperties.getSnapshot().entrySet()) {\n        connection.setRequestProperty(property.getKey(), property.getValue());\n      }\n    }\n    for (Map.Entry<String, String> property : requestProperties.getSnapshot().entrySet()) {\n      connection.setRequestProperty(property.getKey(), property.getValue());\n    }\n\n    // MOD: apply headers from DataSpec\n    if (dataSpec != null) {\n      for (Map.Entry<String, String> headerEntry : dataSpec.httpRequestHeaders.entrySet()) {\n        connection.setRequestProperty(headerEntry.getKey(), headerEntry.getValue());\n      }\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    if (!allowGzip) {\n      connection.setRequestProperty(\"Accept-Encoding\", \"identity\");\n    }\n    if (allowIcyMetadata) {\n      connection.setRequestProperty(\n          IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,\n          IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);\n    }\n    connection.setInstanceFollowRedirects(followRedirects);\n    connection.setDoOutput(httpBody != null);\n    connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod));\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  /**\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}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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  private final @Nullable 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            /* contentTypePredicate= */ null,\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-amzn-2.10.6/library/core/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          ? 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-amzn-2.10.6/library/core/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) throws IOException {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public @Nullable Uri getUri() {\n    return null;\n  }\n\n  @Override\n  public void close() throws IOException {\n    // do nothing.\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 android.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.io.EOFException;\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  /**\n   * Thrown when IOException is encountered during local file read operation.\n   */\n  public static class FileDataSourceException extends IOException {\n\n    public FileDataSourceException(IOException cause) {\n      super(cause);\n    }\n\n  }\n\n  private @Nullable RandomAccessFile file;\n  private @Nullable Uri uri;\n  private long bytesRemaining;\n  private boolean opened;\n\n  public FileDataSource() {\n    super(/* isNetwork= */ false);\n  }\n\n  /**\n   * @param listener An optional listener.\n   * @deprecated Use {@link #FileDataSource()} and {@link #addTransferListener(TransferListener)}\n   */\n  @Deprecated\n  public FileDataSource(@Nullable TransferListener listener) {\n    this();\n    if (listener != null) {\n      addTransferListener(listener);\n    }\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws FileDataSourceException {\n    try {\n      uri = dataSpec.uri;\n      transferInitializing(dataSpec);\n      file = new RandomAccessFile(dataSpec.uri.getPath(), \"r\");\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  @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 = 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  public @Nullable 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-amzn-2.10.6/library/core/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/**\n * A {@link DataSource.Factory} that produces {@link FileDataSource}.\n */\npublic final class FileDataSourceFactory implements DataSource.Factory {\n\n  private final @Nullable TransferListener listener;\n\n  public FileDataSourceFactory() {\n    this(null);\n  }\n\n  public FileDataSourceFactory(@Nullable TransferListener listener) {\n    this.listener = listener;\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"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport android.text.TextUtils;\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\n    @Override\n    public final void setDefaultRequestProperty(String name, String value) {\n      defaultRequestProperties.set(name, value);\n    }\n\n    @Deprecated\n    @Override\n    public final void clearDefaultRequestProperty(String name) {\n      defaultRequestProperties.remove(name);\n    }\n\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  @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   * @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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 loading task 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 load. The next call to {@link #maybeThrowError()} will throw the last load\n   * 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  private LoadTask<? extends Loadable> currentTask;\n  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 = Looper.myLooper();\n    Assertions.checkState(looper != null);\n    fatalError = null;\n    long startTimeMs = SystemClock.elapsedRealtime();\n    new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0);\n    return startTimeMs;\n  }\n\n  /**\n   * Returns whether the {@link Loader} is currently loading a {@link Loadable}.\n   */\n  public boolean isLoading() {\n    return currentTask != null;\n  }\n\n  /**\n   * Cancels the current load. This method should only be called when a load is in progress.\n   */\n  public void cancelLoading() {\n    currentTask.cancel(false);\n  }\n\n  /**\n   * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer\n   * required.\n   */\n  public void release() {\n    release(null);\n  }\n\n  /**\n   * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer\n   * 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    private @Nullable Loader.Callback<T> callback;\n    private IOException currentError;\n    private int errorCount;\n\n    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        if (executorThread != null) {\n          executorThread.interrupt();\n        }\n      }\n      if (released) {\n        finish();\n        long nowMs = SystemClock.elapsedRealtime();\n        callback.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      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(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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  public @Nullable 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-amzn-2.10.6/library/core/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 DataSource.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 DataSource.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-amzn-2.10.6/library/core/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 android.content.Context;\nimport android.content.res.AssetFileDescriptor;\nimport android.content.res.Resources;\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport android.text.TextUtils;\nimport com.google.android.exoplayer2.C;\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  private @Nullable Uri uri;\n  private @Nullable AssetFileDescriptor assetFileDescriptor;\n  private @Nullable 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  /**\n   * @param context A context.\n   * @param listener An optional listener.\n   * @deprecated Use {@link #RawResourceDataSource(Context)} and {@link\n   *     #addTransferListener(TransferListener)}.\n   */\n  @Deprecated\n  public RawResourceDataSource(Context context, @Nullable TransferListener listener) {\n    this(context);\n    if (listener != null) {\n      addTransferListener(listener);\n    }\n  }\n\n  @Override\n  public long open(DataSpec dataSpec) throws RawResourceDataSourceException {\n    try {\n      uri = dataSpec.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(uri.getLastPathSegment());\n      } catch (NumberFormatException e) {\n        throw new RawResourceDataSourceException(\"Resource identifier must be an integer.\");\n      }\n\n      transferInitializing(dataSpec);\n      assetFileDescriptor = resources.openRawResourceFd(resourceId);\n      inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());\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 = 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  public @Nullable 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  public @Nullable 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-amzn-2.10.6/library/core/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  public @Nullable 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  private @Nullable Uri uri;\n  private @Nullable DatagramSocket socket;\n  private @Nullable MulticastSocket multicastSocket;\n  private @Nullable InetAddress address;\n  private @Nullable 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  /**\n   * Constructs a new instance.\n   *\n   * @param listener An optional listener.\n   * @deprecated Use {@link #UdpDataSource()} and {@link #addTransferListener(TransferListener)}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public UdpDataSource(@Nullable TransferListener listener) {\n    this(listener, DEFAULT_MAX_PACKET_SIZE);\n  }\n\n  /**\n   * Constructs a new instance.\n   *\n   * @param listener An optional listener.\n   * @param maxPacketSize The maximum datagram packet size, in bytes.\n   * @deprecated Use {@link #UdpDataSource(int)} and {@link #addTransferListener(TransferListener)}.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public UdpDataSource(@Nullable TransferListener listener, int maxPacketSize) {\n    this(listener, maxPacketSize, DEFAULT_SOCKET_TIMEOUT_MILLIS);\n  }\n\n  /**\n   * Constructs a new instance.\n   *\n   * @param listener An optional listener.\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   * @deprecated Use {@link #UdpDataSource(int, int)} and {@link\n   *     #addTransferListener(TransferListener)}.\n   */\n  @Deprecated\n  public UdpDataSource(\n      @Nullable TransferListener listener, int maxPacketSize, int socketTimeoutMillis) {\n    this(maxPacketSize, socketTimeoutMillis);\n    if (listener != null) {\n      addTransferListener(listener);\n    }\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  public @Nullable 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-amzn-2.10.6/library/core/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 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  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   * @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  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   * @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  @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   * @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  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   * @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  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   * @param span The {@link CacheSpan} to remove.\n   * @throws CacheException If an error is encountered.\n   */\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   * @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  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-amzn-2.10.6/library/core/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  @SuppressWarnings(\"ThrowFromFinallyBlock\")\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  private final @Nullable 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  private @Nullable DataSource currentDataSource;\n  private boolean currentDataSpecLengthUnset;\n  @Nullable private Uri uri;\n  @Nullable private Uri actualUri;\n  @HttpMethod private int httpMethod;\n  private int flags;\n  private @Nullable String key;\n  private long readPosition;\n  private long bytesRemaining;\n  private @Nullable 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      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  public @Nullable 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    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, httpMethod, null, readPosition, readPosition, bytesRemaining, key, flags);\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      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(uri, httpMethod, null, readPosition, readPosition, length, key, flags);\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-amzn-2.10.6/library/core/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.FileDataSourceFactory;\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 FileDataSourceFactory(),\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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_INDEX_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  @MonotonicNonNull private String tableName;\n\n  /**\n   * Deletes index data for the specified cache.\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  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.beginTransaction();\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   * @param uid The cache UID.\n   * @throws DatabaseIOException If an error occurs initializing the index.\n   */\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.beginTransaction();\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   * @return The file metadata keyed by file name.\n   * @throws DatabaseIOException If an error occurs loading the metadata.\n   */\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   * @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  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   * @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  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   * @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  public void removeAll(Set<String> names) throws DatabaseIOException {\n    Assertions.checkNotNull(tableName);\n    try {\n      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n      writableDatabase.beginTransaction();\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-amzn-2.10.6/library/core/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  /**\n   * Returns the number of parallel tasks used to download segments.\n   *\n   * @return must be at least 1.\n   */\n  int maxDownloadParallelSegments();\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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  /**\n   * The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false.\n   */\n  public final @Nullable 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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.util.Pair;\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 = new CacheKeyFactory() {\n    private static final int MIN_PARALLEL_CHUNCKS_DOWNLOADS = 2;\n\n    @Override\n    public String buildCacheKey(DataSpec dataSpec) {\n      return dataSpec.key != null ? dataSpec.key : generateKey(dataSpec);\n    }\n\n    @Override\n    public int maxDownloadParallelSegments() {\n      // TODO Implement a better logic to determine the number of concurrent chunck downloads or\n      // provide a way to customize by client. For now it uses at least MIN_PARALLEL_CHUNCKS_DOWNLOADS\n      // and at most the half of the number of device processor.\n      return Math.max(MIN_PARALLEL_CHUNCKS_DOWNLOADS, Runtime.getRuntime().availableProcessors() / 2);\n    }\n  };\n\n  /**\n   * Generates a cache key out of the given {@link DataSpec}.\n   *\n   * @param dataSpec DataSpec of a content which the requested key is for.\n   */\n  public static String generateKey(DataSpec dataSpec) {\n    return dataSpec.toString() + \"-\" +  + dataSpec.absoluteStreamPosition;\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   * @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  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   * @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  public static void cache(\n      DataSpec dataSpec,\n      Cache cache,\n      @Nullable CacheKeyFactory cacheKeyFactory,\n      CacheDataSource dataSource,\n      byte[] buffer,\n      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      PriorityTaskManager priorityTaskManager,\n      int priority,\n      @Nullable ProgressNotifier progressNotifier,\n      boolean isLastBlock,\n      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   * @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  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   * @param cache A {@link Cache} to store the data.\n   * @param key The key whose data should be removed.\n   */\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(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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport androidx.annotation.VisibleForTesting;\nimport android.util.SparseArray;\nimport android.util.SparseBooleanArray;\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 final 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   * @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  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   * @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  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   * @throws IOException If an error occurs storing the index data.\n   */\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 java.lang.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.beginTransaction();\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.beginTransaction();\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.beginTransaction();\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.beginTransaction();\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.Comparator;\nimport java.util.TreeSet;\n\n/**\n * Evicts least recently used cache files first.\n */\npublic final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Comparator<CacheSpan> {\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<>(this);\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  @Override\n  public 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  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}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 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 static boolean cacheFolderLockingDisabled;\n  private static boolean cacheInitializationExceptionsDisabled;\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  @MonotonicNonNull private 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   * Disables locking the cache folders which {@link SimpleCache} instances are using and releases\n   * any previous lock.\n   *\n   * <p>The locking prevents multiple {@link SimpleCache} instances from being created for the same\n   * folder. Disabling it may cause the cache data to be corrupted. Use at your own risk.\n   *\n   * @deprecated Don't create multiple {@link SimpleCache} instances for the same cache folder. If\n   *     you need to create another instance, make sure you call {@link #release()} on the previous\n   *     instance.\n   */\n  @Deprecated\n  public static synchronized void disableCacheFolderLocking() {\n    cacheFolderLockingDisabled = true;\n    lockedCacheDirs.clear();\n  }\n\n  /**\n   * Disables throwing of cache initialization exceptions.\n   *\n   * @deprecated Don't use this. Provided for problematic upgrade cases only.\n   */\n  @Deprecated\n  public static void disableCacheInitializationExceptions() {\n    cacheInitializationExceptionsDisabled = true;\n  }\n\n  /**\n   * Deletes all content belonging to a cache instance.\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  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  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 (!cacheInitializationExceptionsDisabled && 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 SimpleCacheSpan startReadWrite(String key, long position)\n      throws InterruptedException, CacheException {\n    Assertions.checkState(!released);\n    checkInitialization();\n\n    while (true) {\n      SimpleCacheSpan span = startReadWriteNonBlocking(key, position);\n      if (span != null) {\n        return span;\n      } else {\n        // Write case, lock not available. We'll be woken up when a locked span is released (if the\n        // released lock is for the requested key then we'll be able to make progress) or when a\n        // span is added to the cache (if the span is for the requested key and covers the requested\n        // position, then we'll become a read and be able to make progress).\n        wait();\n      }\n    }\n  }\n\n  @Override\n  @Nullable\n  public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position)\n      throws CacheException {\n    Assertions.checkState(!released);\n    checkInitialization();\n\n    SimpleCacheSpan span = getSpan(key, position);\n\n    // Read case.\n    if (span.isCached) {\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    CachedContent cachedContent = contentIndex.getOrAdd(key);\n    if (!cachedContent.isLocked()) {\n      // Write case, lock available.\n      cachedContent.setLocked(true);\n      return span;\n    }\n\n    // Write case, 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  /**\n   * Returns the cache {@link SimpleCacheSpan} corresponding to the provided lookup {@link\n   * SimpleCacheSpan}.\n   *\n   * <p>If the lookup position is contained by an existing entry in the cache, then the returned\n   * {@link SimpleCacheSpan} defines the file in which the data is stored. If the lookup position is\n   * not contained by an existing entry, then the returned {@link SimpleCacheSpan} defines the\n   * 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.exists()) {\n        // The file has been deleted from under us. It's likely that other files will have been\n        // deleted too, so scan the whole in-memory representation.\n        removeStaleSpans();\n        continue;\n      }\n      return span;\n    }\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   * 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 files no\n   * longer exist.\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.exists()) {\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   * @throws IOException If there is an error loading or generating the UID.\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    if (cacheFolderLockingDisabled) {\n      return true;\n    }\n    return lockedCacheDirs.add(cacheDir.getAbsoluteFile());\n  }\n\n  private static synchronized void unlockFolder(File cacheDir) {\n    if (!cacheFolderLockingDisabled) {\n      lockedCacheDirs.remove(cacheDir.getAbsoluteFile());\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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      file = upgradeFile(file, index);\n      if (file == null) {\n        return null;\n      }\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 = getCacheFile(file.getParentFile(), index.assignIdForKey(key),\n        Long.parseLong(matcher.group(2)), 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-amzn-2.10.6/library/core/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 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  private final byte[] scratch;\n\n  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, 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      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        cipher.update(data, offset + bytesProcessed, bytesToProcess, scratch, 0);\n        wrappedDataSink.write(scratch, 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-amzn-2.10.6/library/core/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 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  private @Nullable 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    cipher.updateInPlace(data, offset, read);\n    return read;\n  }\n\n  @Override\n  public @Nullable 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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\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\n   * {@link #hashCode()}.\n   */\n  public static long getFNV64Hash(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-amzn-2.10.6/library/core/src/main/java/com/google/android/exoplayer2/util/AmazonQuirks.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.Build;\nimport android.util.Log;\n\npublic final class AmazonQuirks {\n\n    //ordering of the static initializations is important.\n    private static final String TAG = AmazonQuirks.class.getSimpleName();\n    private static final String FIRETV_GEN1_DEVICE_MODEL       = \"AFTB\";\n    private static final String FIRETV_GEN2_DEVICE_MODEL       = \"AFTS\";\n    private static final String FIRETV_STICK_DEVICE_MODEL      = \"AFTM\";\n    private static final String FIRETV_STICK_GEN2_DEVICE_MODEL = \"AFTT\";\n    private static final String KINDLE_TABLET_DEVICE_MODEL     = \"KF\";\n    private static final String FIRE_PHONE_DEVICE_MODEL        = \"SD\";\n    private static final String AMAZON                         = \"Amazon\";\n\n    private static final String DEVICEMODEL  = Build.MODEL;\n    private static final String MANUFACTURER = Build.MANUFACTURER;\n\n    private static final int AUDIO_HARDWARE_LATENCY_FOR_TABLETS = 90000;\n\n    //caching\n    private static final boolean isAmazonDevice;\n    private static final boolean isFireTVGen1;\n    private static final boolean isFireTVStick;\n    private static final boolean isFireTVGen2;\n    private static final boolean isKindleTablet;\n    private static final boolean isFirePhone;\n\n    private static boolean isSnappingToVsyncDisabled;\n    private static boolean skipProfileLevelCheck;\n\n    // This static block must be the last\n    //INIT ORDERING IS IMPORTANT IN THIS BLOCK!\n    static {\n        isAmazonDevice = MANUFACTURER.equalsIgnoreCase(AMAZON);\n        isFireTVGen1   = isAmazonDevice && DEVICEMODEL.equalsIgnoreCase(FIRETV_GEN1_DEVICE_MODEL);\n        isFireTVGen2   = isAmazonDevice && DEVICEMODEL.equalsIgnoreCase(FIRETV_GEN2_DEVICE_MODEL);\n        isFireTVStick  = isAmazonDevice && DEVICEMODEL.equalsIgnoreCase(FIRETV_STICK_DEVICE_MODEL);\n        isKindleTablet = isAmazonDevice && DEVICEMODEL.startsWith(KINDLE_TABLET_DEVICE_MODEL);\n        isFirePhone = isAmazonDevice && DEVICEMODEL.startsWith(FIRE_PHONE_DEVICE_MODEL);\n        loadForcedLogSettings();\n    }\n\n    private AmazonQuirks(){}\n\n    public static boolean isDolbyPassthroughQuirkEnabled() {\n        // Sets dolby passthrough quirk for Amazon Fire TV (Gen 1) Family\n        return isFireTVGen1Family();\n    }\n\n    public static boolean isAmazonDevice(){\n        return isAmazonDevice;\n    }\n\n    public static boolean isFireTVGen1Family() {\n        return isFireTVGen1 || isFireTVStick;\n    }\n\n    public static boolean isFireTVGen2() {\n        return isFireTVGen2;\n    }\n\n    // We assume that this function is called only for supported\n    // passthrough mimetypes such as AC3, EAC3 etc\n    public static boolean useDefaultPassthroughDecoder() {\n        //Use platform decoder only for\n        // - FireTV Gen1\n        // - FireTV Stick\n        if (isFireTVGen1Family()) {\n            Log.i(TAG, \"Using platform Dolby decoder\");\n            return false;\n        }\n\n        Log.i(TAG, \"Using default Dolby pass-through decoder\");\n        return true;\n    }\n\n    public static boolean isLatencyQuirkEnabled() {\n        // Sets latency quirk for Amazon KK and JB Tablets and Fire Phone\n        return (Util.SDK_INT <= 19) && (isKindleTablet || isFirePhone);\n    }\n\n    public static int getAudioHWLatency() {\n        // this function is called only when the above function\n        // returns true for latency quirk. So no need to check for\n        // SDK version and device type again\n        return AUDIO_HARDWARE_LATENCY_FOR_TABLETS;\n    }\n    /**\n      * Updates log level based on a local system property of the device. This can be very useful\n      * to enable logging in scenarios when a 3P developer has issues we need to assist\n      * Example:\n      * adb shell setprop com.amazon.exoplayer.forcelog Video:verbose#Audio:debug\n      */\n     private static void loadForcedLogSettings() {\n         String setting = getSystemProperty(\"com.amazon.exoplayer.forcelog\");\n         // this happens on release builds, and without disabling setenforce\n         if (setting == null || setting.equals(\"\")) {\n             return;\n         }\n         try {\n             String[] pairs = setting.split(\"#\");\n             for (String onePair : pairs) {\n                 String[] elements = onePair.split(\":\");\n                 Logger.Module module = Logger.Module.valueOf(elements[0]);\n                 int level = 0;\n                 String levelStr = elements[1];\n                 switch (levelStr.toLowerCase()) {\n                     case \"error\"  : level = Log.ERROR;   break;\n                     case \"info\"   : level = Log.INFO;    break;\n                     case \"verbose\": level = Log.VERBOSE; break;\n                     case \"warn\"   : level = Log.WARN;    break;\n                     default       : level = Log.DEBUG;   break;\n                 }\n                 Logger.setLogLevel(module, level);\n             }\n         } catch (Exception ex) {\n             Log.e(TAG, \"Could not set logging level.\", ex);\n         }\n     }\n\n     // for debugging purposes only, so it is a minimalistic solution\n     public static String getSystemProperty(String key) {\n         try {\n             Class<?> SP = Class.forName(\"android.os.SystemProperties\");\n             return (String) SP.getMethod(\"get\", String.class).invoke(null, key);\n         } catch (Exception e) {\n             return null;\n         }\n     }\n    /*\n     * To disable snapping the frame release times to VSYNC call this function with true\n     * By default, snapping to VSYNC is enabled if this function is not called.\n     */\n    public static void disableSnappingToVsync(boolean disable) {\n         isSnappingToVsyncDisabled = disable;\n    }\n\n    public static boolean isSnappingToVsyncDisabled() {\n         return isSnappingToVsyncDisabled;\n    }\n\n    /**\n     * Called to set quirk which determines if codec profile checks should be\n     * skipped.  Call this api with true to make the player skip checking the\n     * support for profile levels of the content. Only use this if you are sure\n     * that the target device is capable playing the content, but under reports\n     * the capabilities.\n     *\n     * @param  skip If true then skip codec profile check\n     */\n    public static void skipProfileLevelCheck(boolean skip) {\n        skipProfileLevelCheck = skip;\n    }\n\n    /**\n     * Returns the state of the skip codec profile check quirk.\n     *\n     * @return the value of the skip codec profile check quirk\n     */\n    public static boolean shouldSkipProfileLevelCheck() {\n        return skipProfileLevelCheck;\n    }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\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 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-amzn-2.10.6/library/core/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 androidx.annotation.NonNull;\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(@NonNull byte[] b) throws IOException {\n      fileOutputStream.write(b);\n    }\n\n    @Override\n    public void write(@NonNull byte[] b, int off, int len) throws IOException {\n      fileOutputStream.write(b, off, len);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.util.Pair;\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 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 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(ParsableBitArray bitArray,\n      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 numChannels The number of channels.\n   * @return The AudioSpecificConfig.\n   */\n  public static byte[] buildAacLcAudioSpecificConfig(int sampleRate, int numChannels) {\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 (numChannels == 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(\"Invalid sample rate or number of channels: \"\n          + sampleRate + \", \" + numChannels);\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   * 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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  private final @Nullable TextureImageListener callback;\n\n  private @Nullable EGLDisplay display;\n  private @Nullable EGLContext context;\n  private @Nullable EGLSurface surface;\n  private @Nullable 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, android.opengl.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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.view.Surface;\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.RendererCapabilities;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.analytics.AnalyticsListener;\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  private final @Nullable 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(EventTime eventTime, boolean playWhenReady, int state) {\n    logd(eventTime, \"state\", playWhenReady + \", \" + getStateString(state));\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        \"timelineChanged [\"\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, \"tracksChanged\", \"[]\");\n      return;\n    }\n    logd(\"tracksChanged [\" + 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(rendererIndex, groupIndex, 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                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              getFormatSupportString(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\", 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 onDecoderInitialized(\n      EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {\n    logd(eventTime, \"decoderInitialized\", getTrackTypeString(trackType) + \", \" + decoderName);\n  }\n\n  @Override\n  public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {\n    logd(\n        eventTime,\n        \"decoderInputFormatChanged\",\n        getTrackTypeString(trackType) + \", \" + Format.toLogString(format));\n  }\n\n  @Override\n  public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters counters) {\n    logd(eventTime, \"decoderDisabled\", 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, \"videoSizeChanged\", 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, \"surfaceSizeChanged\", 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, \"downstreamFormatChanged\", 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 getTimeString(eventTime.realtimeMs - startTimeMs)\n        + \", \"\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 getFormatSupportString(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        return \"?\";\n    }\n  }\n\n  private static String getAdaptiveSupportString(int trackCount, 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        return \"?\";\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 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"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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;\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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;\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, 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, 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(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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport android.text.TextUtils;\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-amzn-2.10.6/library/core/src/main/java/com/google/android/exoplayer2/util/Logger.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 java.lang.IllegalArgumentException;\nimport android.util.Log;\nimport java.util.Arrays;\n\n/**\n * A common logger module that allows any module to log.\n * Supports log level configuration for each module.\n * call {@link Logger#setLogLevel} API to configure the modules and log levels from the app\n */\npublic class Logger {\n    public enum Module {\n        Unknown,\n        /**\n         *  Module that includes MediaCodecTrackRenderer\n         */\n        AudioVideoCommon,\n        /**\n         *  Module that includes MediaCodecAudioTrackRenderer and AudioTrack\n         */\n        Audio,\n        /**\n         *  Module that includes MediaCodecVideoTrackRenderer\n         */\n        Video,\n        /**\n         *  Module that includes MOD_VIDEO, MOD_AUDIO and MOD_AUDIO_VIDEO_COMMON\n         */\n        AudioVideo,\n\n        Text,\n        Source,\n        Manifest,\n        Player,\n        /**\n         *  Includes all modules\n         */\n        All\n    }\n\n    private String mTag = \"UNKNOWN\";\n    private int mModule = Module.Unknown.ordinal();\n    private static final int[] enabledModules = new int[Module.All.ordinal()];\n    /**\n     * By default all modules are set to Log level Log.INFO\n     */\n    static {\n        Arrays.fill(enabledModules, Log.INFO);\n    }\n    /**\n     * Constructor for this class\n     * @param tag The file TAG to be used in logging\n     * @param module The module to which this file belongs to.\n     */\n    public Logger(Module module, String tag) {\n        if (tag == null) {\n            throw new IllegalArgumentException(\"Null Tag\");\n        }\n        mTag = tag;\n        mModule = module.ordinal();\n    }\n    /**\n     * Configures the log levels for a specific module\n     * @param module The module to which this file belongs to. One of the\n     *   MOD_xxx values\n     *   Setting All enables logging in all modules.\n     *   Setting AudioVideo enables logging in both Audio and Video\n     *   Setting Audio or Video enables logging in AudioVideoCommon\n     * @param logLevel Log level for this module. One of the constants in\n     *    android.util.Log. i.e Log.ERROR, Log.WARNING, Log.INFO, Log.DEBUG and Log.VERBOSE\n     *    Info , error and warning logs are always printed.\n     *    Setting to Log.INFO, Log.ERROR, Log.WARNING etc disables DEBUG and VERBOSE logs.\n     *    Setting to Log.VERBOSE prints Debug and Verbose logs.\n     *    Setting to Log.DEBUG prints Debug logs (excludes Verbose)\n     */\n    public static void setLogLevel(Module module, int logLevel) {\n        if (module.compareTo(Module.All) == 0) {\n            Arrays.fill(enabledModules, logLevel);\n        } else {\n            enabledModules[module.ordinal()] = logLevel;\n        }\n        if (module.compareTo(Module.Audio) >= 0 && module.compareTo(Module.AudioVideo) <= 0) {\n            enabledModules[Module.AudioVideoCommon.ordinal()] = logLevel;\n        }\n        if (module.compareTo(Module.AudioVideo) == 0) {\n            enabledModules[Module.Audio.ordinal()] = logLevel;\n            enabledModules[Module.Video.ordinal()] = logLevel;\n        }\n    }\n\n    /**\n     * Call this function to override the tag given in constructor\n     * @param tag The file TAG to be used in logging.\n     */\n    public void setTAG(String tag) {\n        if (tag == null) {\n            throw new IllegalArgumentException(\"Null Tag\");\n        }\n        mTag = tag;\n    }\n\n    /**\n     * Call this function to override the module given in constructor\n     * @param module The module to which this file belongs to.\n     */\n    public void setModule(Module module) {\n        mModule = module.ordinal();\n    }\n\n    public boolean allowVerbose() {\n        return (enabledModules[mModule] == Log.VERBOSE);\n    }\n\n    public boolean allowDebug() {\n        return (enabledModules[mModule] <= Log.DEBUG);\n    }\n\n    /**\n     * Function to print verbose level logs. Prints only\n     * if the log level is Log.VERBOSE\n     * @param msg The log message\n     */\n    public void v(String msg) {\n        if (enabledModules[mModule] == Log.VERBOSE) {\n            Log.v(mTag, msg);\n        }\n    }\n    /**\n     * Function to print debug level logs. Prints only\n     * if the log level is Log.DEBUG or Log.VERBOSE\n     * @param msg The log message\n     */\n    public void d(String msg) {\n        if (enabledModules[mModule] <= Log.DEBUG) {\n            Log.d(mTag, msg);\n        }\n    }\n    /**\n     * Function to print info level logs\n     * @param msg The log message\n     */\n    public void i(String msg) {\n        Log.i(mTag, msg);\n    }\n    /**\n     * Function to print warning level logs\n     * @param msg The log message\n     */\n    public void w(String msg) {\n        Log.w(mTag, msg);\n    }\n    /**\n     * Function to print error level logs\n     * @param msg The log message\n     */\n    public void e(String msg) {\n        Log.e(mTag, msg);\n    }\n\n    /**\n     * Function to print error level logs and throwable\n     * @param msg The log message\n     * @param tr The throwable\n     */\n    public void e(String msg, Throwable tr) {\n        Log.e(mTag, msg, tr);\n    }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 and returns the active playback parameters, which may\n   * differ from those passed in.\n   *\n   * @param playbackParameters The playback parameters.\n   * @return The active playback parameters.\n   */\n  PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters);\n\n  /**\n   * Returns the active playback parameters.\n   */\n  PlaybackParameters getPlaybackParameters();\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\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_CUSTOM_EC3 = BASE_TYPE_AUDIO + \"/ec3\";\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_CUSTOM_EC3: // AMZN_CHANGE_ONELINE\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      // AMZN_CHANGE_BEGIN\n      case MimeTypes.AUDIO_RAW:\n        return C.ENCODING_PCM_16BIT;\n      // AMZN_CHANGE_END\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 n 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 {@code numBits} bits into {@code buffer}.\n   *\n   * @param buffer The array into which the read data should be written. The trailing\n   *     {@code numBits % 8} bits are written into the most significant bits of the last modified\n   *     {@code buffer} 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-amzn-2.10.6/library/core/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  public @Nullable 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  public @Nullable 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 PlaybackParameters 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    return playbackParameters;\n  }\n\n  @Override\n  public PlaybackParameters getPlaybackParameters() {\n    return playbackParameters;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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;\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, 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, 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(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-amzn-2.10.6/library/core/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  private @Nullable 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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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;\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(String baseUri, 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   * <p>\n   * 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(String baseUri, 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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.telephony.TelephonyManager;\nimport android.text.TextUtils;\nimport android.view.Display;\nimport android.view.WindowManager;\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 android.os.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   * <p>\n   * 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(Object[] items, 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   * Concatenates 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  /**\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 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 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(int[] array, int 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 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   * 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}.\n   */\n  public static @Nullable String getCodecsOfType(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.\n   */\n  public static String[] splitCodecs(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, 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 with the specified bytes in a \"most significant bit first\"\n   * 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 crc(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 {@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 <= 28 && 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  @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        : SDK_INT >= 21 ? getSystemLocaleV21(config) : new String[] {config.locale.toString()};\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[] getSystemLocaleV21(Configuration config) {\n    return new String[] {config.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 calculation to be done byte by byte instead of bit per bit being the order\n   * \"most 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"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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  public final @Nullable 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 Parcelable.Creator<ColorInfo> CREATOR =\n      new Parcelable.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-amzn-2.10.6/library/core/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) {\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-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.view.Surface;\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    private @Nullable Error initError;\n    private @Nullable RuntimeException initException;\n    private @Nullable 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-amzn-2.10.6/library/core/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  public final @Nullable 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-amzn-2.10.6/library/core/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.MediaCrypto;\nimport android.media.MediaFormat;\nimport android.os.Handler;\nimport android.os.SystemClock;\nimport androidx.annotation.CallSuper;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport android.util.Pair;\nimport android.view.Surface;\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.PlayerMessage.Target;\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.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;\nimport com.google.android.exoplayer2.util.Logger;\nimport com.google.android.exoplayer2.util.AmazonQuirks;\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  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\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  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  private @Nullable VideoFrameMetadataListener frameMetadataListener;\n\n  private final Logger log = new Logger(Logger.Module.Video, TAG);\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  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   */\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 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   */\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    // AMZN_CHANGE_BEGIN\n    if (AmazonQuirks.isSnappingToVsyncDisabled()) {\n      frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper();\n    } else {\n      frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context);\n    }\n    // AMZN_CHANGE_END\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  protected int supportsFormat(MediaCodecSelector mediaCodecSelector,\n      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, Format format)\n      throws DecoderQueryException {\n    String mimeType = format.sampleMimeType;\n    if (!MimeTypes.isVideo(mimeType)) {\n      return FORMAT_UNSUPPORTED_TYPE;\n    }\n    boolean requiresSecureDecryption = false;\n    DrmInitData drmInitData = format.drmInitData;\n    if (drmInitData != null) {\n      for (int i = 0; i < drmInitData.schemeDataCount; i++) {\n        requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption;\n      }\n    }\n    List<MediaCodecInfo> decoderInfos =\n        getDecoderInfos(mediaCodecSelector, format, requiresSecureDecryption);\n    if (decoderInfos.isEmpty()) {\n      return requiresSecureDecryption\n              && !mediaCodecSelector\n                  .getDecoderInfos(\n                      format.sampleMimeType,\n                      /* requiresSecureDecoder= */ false,\n                      /* requiresTunnelingDecoder= */ false)\n                  .isEmpty()\n          ? FORMAT_UNSUPPORTED_DRM\n          : FORMAT_UNSUPPORTED_SUBTYPE;\n    }\n    if (!supportsFormatDrm(drmSessionManager, drmInitData)) {\n      return 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    int adaptiveSupport =\n        decoderInfo.isSeamlessAdaptationSupported(format)\n            ? ADAPTIVE_SEAMLESS\n            : ADAPTIVE_NOT_SEAMLESS;\n    int tunnelingSupport = TUNNELING_NOT_SUPPORTED;\n    if (isFormatSupported) {\n      List<MediaCodecInfo> tunnelingDecoderInfos =\n          mediaCodecSelector.getDecoderInfos(\n              format.sampleMimeType,\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    int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;\n    return adaptiveSupport | tunnelingSupport | formatSupport;\n  }\n\n  @Override\n  protected List<MediaCodecInfo> getDecoderInfos(\n      MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder)\n      throws DecoderQueryException {\n    List<MediaCodecInfo> decoderInfos =\n        mediaCodecSelector.getDecoderInfos(format.sampleMimeType, requiresSecureDecoder, tunneling);\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    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    // In tunneling mode we can't dequeue an end-of-stream buffer, so propagate it in the renderer.\n    return tunneling;\n  }\n\n  @Override\n  protected void configureCodec(\n      MediaCodecInfo codecInfo,\n      MediaCodec codec,\n      Format format,\n      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\n    // AMZN_CHANGE_BEGIN\n    log.setTAG(codecName + \"-\" + TAG);\n    log.i(\"configureCodec: codecName = \" + codec +\n            \", deviceNeedsNoPostProcessWorkaround = \" + deviceNeedsNoPostProcessWorkaround +\n            \", format = \" + format +\n            \", surface = \" + surface +\n            \", crypto = \" + crypto );\n    // AMZN_CHANGE_END\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  }\n\n  @Override\n  protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {\n    super.onInputFormatChanged(newFormat);\n    log.i(\"onInputFormatChanged: format = \" + newFormat);\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 outputFormat) {\n    log.i(\"onOutputFormatChanged: outputFormat:\" + outputFormat\n            + \", codec:\" + codec);\n    boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT)\n        && outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM)\n        && outputFormat.containsKey(KEY_CROP_TOP);\n    int width =\n        hasCrop\n            ? outputFormat.getInteger(KEY_CROP_RIGHT) - outputFormat.getInteger(KEY_CROP_LEFT) + 1\n            : outputFormat.getInteger(MediaFormat.KEY_WIDTH);\n    int height =\n        hasCrop\n            ? outputFormat.getInteger(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1\n            : outputFormat.getInteger(MediaFormat.KEY_HEIGHT);\n    processOutputFormat(codec, width, height);\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 (log.allowDebug()) {\n      log.d(\"processOutputBuffer: positionUs = \" + positionUs +\n          \", elapsedRealtimeUs = \" + elapsedRealtimeUs +\n          \", bufferIndex = \" + bufferIndex +\n          \", isDecodeOnlyBuffer = \" + isDecodeOnlyBuffer +\n          \", isLastBuffer = \" + isLastBuffer +\n          \", presentationTimeUs = \" + bufferPresentationTimeUs);\n    }\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    boolean isStarted = getState() == STATE_STARTED;\n    if (!renderedFirstFrame\n        || (isStarted\n            && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) {\n      long releaseTimeNs = System.nanoTime();\n      notifyFrameMetadataListener(presentationTimeUs, releaseTimeNs, format);\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    if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastBuffer)\n        && maybeDropBuffersToKeyframe(codec, bufferIndex, presentationTimeUs, positionUs)) {\n      return false;\n    } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs, isLastBuffer)) {\n      dropOutputBuffer(codec, bufferIndex, presentationTimeUs);\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(presentationTimeUs, adjustedReleaseTimeNs, format);\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(presentationTimeUs, adjustedReleaseTimeNs, format);\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 format changes.\n    codec.setVideoScalingMode(scalingMode);\n  }\n\n  private void notifyFrameMetadataListener(\n      long presentationTimeUs, long releaseTimeNs, Format format) {\n    if (frameMetadataListener != null) {\n      frameMetadataListener.onVideoFrameAboutToBeRendered(\n          presentationTimeUs, releaseTimeNs, format);\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  /**\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    }\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 android.os.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 android.os.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    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    log.i(\"skipOutputBuffer: bufferIndex = \" + index + \", PTS = \" + 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    log.i(\"dropOutputBuffer: bufferIndex = \" + index + \", PTS = \" + 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   * @return Whether any buffers were dropped.\n   * @throws ExoPlaybackException If an error occurs flushing the codec.\n   */\n  protected boolean maybeDropBuffersToKeyframe(MediaCodec codec, int index, long presentationTimeUs,\n      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 codec,\n    // which releases all pending buffers buffers including the current output buffer.\n    updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount);\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    if (log.allowDebug()) {\n      log.d(\"renderOutputBuffer: \" + index + \", PTS = \" + presentationTimeUs);\n    }\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    if (log.allowDebug()) {\n      log.d(\"renderOutputBufferV21: bufferIndex = \" + index + \", PTS = \" + presentationTimeUs +\n              \", releaseTimeNs = \" + releaseTimeNs);\n    }\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  /**\n   * MOD: Make function overridable\n   */\n  protected 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  /**\n   * MOD: Make function overridable\n   */\n  protected 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(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 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 =\n          MediaCodecUtil.getCodecProfileAndLevel(format.codecs);\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 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  /**\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 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 codec and 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          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 \"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              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 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(@NonNull MediaCodec codec, long presentationTimeUs, long nanoTime) {\n      if (this != tunnelingOnFrameRenderedListener) {\n        // Stale event.\n        return;\n      }\n      onProcessedTunneledBuffer(presentationTimeUs);\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 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   */\n  void onVideoFrameAboutToBeRendered(long presentationTimeUs, long releaseTimeNs, Format format);\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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 androidx.annotation.Nullable;\nimport android.view.Choreographer;\nimport android.view.Choreographer.FrameCallback;\nimport android.view.Display;\nimport android.view.WindowManager;\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        // MOD: swallow an exception\n        try {\n          updateDefaultDisplayRefreshRateParams();\n        } catch (IllegalStateException e) {\n          // IllegalStateException: Unable to locate mode -1\n          e.printStackTrace();\n        }\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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 android.os.Handler;\nimport android.os.SystemClock;\nimport androidx.annotation.Nullable;\nimport android.view.Surface;\nimport android.view.TextureView;\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 (listener != null) {\n        handler.post(() -> 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 (listener != null) {\n        handler.post(\n            () ->\n                listener.onVideoDecoderInitialized(\n                    decoderName, initializedTimestampMs, initializationDurationMs));\n      }\n    }\n\n    /** Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format)}. */\n    public void inputFormatChanged(Format format) {\n      if (listener != null) {\n        handler.post(() -> listener.onVideoInputFormatChanged(format));\n      }\n    }\n\n    /** Invokes {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */\n    public void droppedFrames(int droppedFrameCount, long elapsedMs) {\n      if (listener != null) {\n        handler.post(() -> 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 (listener != null) {\n        handler.post(\n            () ->\n                listener.onVideoSizeChanged(\n                    width, height, unappliedRotationDegrees, pixelWidthHeightRatio));\n      }\n    }\n\n    /** Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface)}. */\n    public void renderedFirstFrame(@Nullable Surface surface) {\n      if (listener != null) {\n        handler.post(() -> listener.onRenderedFirstFrame(surface));\n      }\n    }\n\n    /** Invokes {@link VideoRendererEventListener#onVideoDisabled(DecoderCounters)}. */\n    public void disabled(DecoderCounters counters) {\n      counters.ensureUpdated();\n      if (listener != null) {\n        handler.post(\n            () -> {\n              counters.ensureUpdated();\n              listener.onVideoDisabled(counters);\n            });\n      }\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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.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 FormatHolder formatHolder;\n  private final DecoderInputBuffer buffer;\n  private final ParsableByteArray scratch;\n\n  private long offsetUs;\n  private @Nullable CameraMotionListener listener;\n  private long lastTimestampUs;\n\n  public CameraMotionRenderer() {\n    super(C.TRACK_TYPE_CAMERA_MOTION);\n    formatHolder = new FormatHolder();\n    buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);\n    scratch = new ParsableByteArray();\n  }\n\n  @Override\n  public int supportsFormat(Format format) {\n    return MimeTypes.APPLICATION_CAMERA_MOTION.equals(format.sampleMimeType)\n        ? FORMAT_HANDLED\n        : 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      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(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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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-amzn-2.10.6/library/core/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 = Util.getIntegerCodeForString(\"ytmp\");\n  private static final int TYPE_MSHP = Util.getIntegerCodeForString(\"mshp\");\n  private static final int TYPE_RAW = Util.getIntegerCodeForString(\"raw \");\n  private static final int TYPE_DFL8 = Util.getIntegerCodeForString(\"dfl8\");\n  private static final int TYPE_MESH = Util.getIntegerCodeForString(\"mesh\");\n  private static final int TYPE_PROJ = Util.getIntegerCodeForString(\"proj\");\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-amzn-2.10.6/library/core/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest package=\"com.google.android.exoplayer2.core.test\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/amr/sample_nb.amr.0.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/3gpp\n    maxInputSize = 61\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 8000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 2834\n  sample count = 218\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 13, hash 371B046C\n  sample 1:\n    time = 20000\n    flags = 1\n    data = length 13, hash CE30BF5B\n  sample 2:\n    time = 40000\n    flags = 1\n    data = length 13, hash 19A59975\n  sample 3:\n    time = 60000\n    flags = 1\n    data = length 13, hash 4879773C\n  sample 4:\n    time = 80000\n    flags = 1\n    data = length 13, hash E8F83019\n  sample 5:\n    time = 100000\n    flags = 1\n    data = length 13, hash D265CDC9\n  sample 6:\n    time = 120000\n    flags = 1\n    data = length 13, hash 91653DAA\n  sample 7:\n    time = 140000\n    flags = 1\n    data = length 13, hash C79456F6\n  sample 8:\n    time = 160000\n    flags = 1\n    data = length 13, hash CDDC4422\n  sample 9:\n    time = 180000\n    flags = 1\n    data = length 13, hash D9ED3AF1\n  sample 10:\n    time = 200000\n    flags = 1\n    data = length 13, hash BAB75A33\n  sample 11:\n    time = 220000\n    flags = 1\n    data = length 13, hash 2221B4FF\n  sample 12:\n    time = 240000\n    flags = 1\n    data = length 13, hash 96400A0B\n  sample 13:\n    time = 260000\n    flags = 1\n    data = length 13, hash 582E6FB\n  sample 14:\n    time = 280000\n    flags = 1\n    data = length 13, hash C4E878E5\n  sample 15:\n    time = 300000\n    flags = 1\n    data = length 13, hash C849A1BD\n  sample 16:\n    time = 320000\n    flags = 1\n    data = length 13, hash CFA8A9ED\n  sample 17:\n    time = 340000\n    flags = 1\n    data = length 13, hash 70CA4907\n  sample 18:\n    time = 360000\n    flags = 1\n    data = length 13, hash B47D4454\n  sample 19:\n    time = 380000\n    flags = 1\n    data = length 13, hash 282998C1\n  sample 20:\n    time = 400000\n    flags = 1\n    data = length 13, hash 3F3F7A65\n  sample 21:\n    time = 420000\n    flags = 1\n    data = length 13, hash CC2EAB58\n  sample 22:\n    time = 440000\n    flags = 1\n    data = length 13, hash 279EF712\n  sample 23:\n    time = 460000\n    flags = 1\n    data = length 13, hash AA2F4B29\n  sample 24:\n    time = 480000\n    flags = 1\n    data = length 13, hash F6F658C4\n  sample 25:\n    time = 500000\n    flags = 1\n    data = length 13, hash D7DEBD17\n  sample 26:\n    time = 520000\n    flags = 1\n    data = length 13, hash 6DAB9A17\n  sample 27:\n    time = 540000\n    flags = 1\n    data = length 13, hash 6ECE1571\n  sample 28:\n    time = 560000\n    flags = 1\n    data = length 13, hash B3D0507F\n  sample 29:\n    time = 580000\n    flags = 1\n    data = length 13, hash 21E356B9\n  sample 30:\n    time = 600000\n    flags = 1\n    data = length 13, hash 410EA12\n  sample 31:\n    time = 620000\n    flags = 1\n    data = length 13, hash 533895A8\n  sample 32:\n    time = 640000\n    flags = 1\n    data = length 13, hash C61B3E5A\n  sample 33:\n    time = 660000\n    flags = 1\n    data = length 13, hash 982170E6\n  sample 34:\n    time = 680000\n    flags = 1\n    data = length 13, hash 7A0468C5\n  sample 35:\n    time = 700000\n    flags = 1\n    data = length 13, hash 9C85EAA7\n  sample 36:\n    time = 720000\n    flags = 1\n    data = length 13, hash B6B341B6\n  sample 37:\n    time = 740000\n    flags = 1\n    data = length 13, hash 6937532E\n  sample 38:\n    time = 760000\n    flags = 1\n    data = length 13, hash 8CF2A3A0\n  sample 39:\n    time = 780000\n    flags = 1\n    data = length 13, hash D2682AC6\n  sample 40:\n    time = 800000\n    flags = 1\n    data = length 13, hash BBC5710F\n  sample 41:\n    time = 820000\n    flags = 1\n    data = length 13, hash 59080B6C\n  sample 42:\n    time = 840000\n    flags = 1\n    data = length 13, hash E4118291\n  sample 43:\n    time = 860000\n    flags = 1\n    data = length 13, hash A1E5B296\n  sample 44:\n    time = 880000\n    flags = 1\n    data = length 13, hash D7B8F95B\n  sample 45:\n    time = 900000\n    flags = 1\n    data = length 13, hash CC839BE1\n  sample 46:\n    time = 920000\n    flags = 1\n    data = length 13, hash D459DFCE\n  sample 47:\n    time = 940000\n    flags = 1\n    data = length 13, hash D6AD19EC\n  sample 48:\n    time = 960000\n    flags = 1\n    data = length 13, hash D05E373D\n  sample 49:\n    time = 980000\n    flags = 1\n    data = length 13, hash 6A4460C7\n  sample 50:\n    time = 1000000\n    flags = 1\n    data = length 13, hash C9A0D93F\n  sample 51:\n    time = 1020000\n    flags = 1\n    data = length 13, hash 3FA819E7\n  sample 52:\n    time = 1040000\n    flags = 1\n    data = length 13, hash 1D3CBDFC\n  sample 53:\n    time = 1060000\n    flags = 1\n    data = length 13, hash 8BBBB403\n  sample 54:\n    time = 1080000\n    flags = 1\n    data = length 13, hash 21B4A0F9\n  sample 55:\n    time = 1100000\n    flags = 1\n    data = length 13, hash C0F921D1\n  sample 56:\n    time = 1120000\n    flags = 1\n    data = length 13, hash 5D812AAB\n  sample 57:\n    time = 1140000\n    flags = 1\n    data = length 13, hash 50C9F3F8\n  sample 58:\n    time = 1160000\n    flags = 1\n    data = length 13, hash 5C2BB5D1\n  sample 59:\n    time = 1180000\n    flags = 1\n    data = length 13, hash 6BF9BEA5\n  sample 60:\n    time = 1200000\n    flags = 1\n    data = length 13, hash 2738C1E6\n  sample 61:\n    time = 1220000\n    flags = 1\n    data = length 13, hash 5FC288A6\n  sample 62:\n    time = 1240000\n    flags = 1\n    data = length 13, hash 7E8E442A\n  sample 63:\n    time = 1260000\n    flags = 1\n    data = length 13, hash AEAA2BBA\n  sample 64:\n    time = 1280000\n    flags = 1\n    data = length 13, hash 4E2ACD2F\n  sample 65:\n    time = 1300000\n    flags = 1\n    data = length 13, hash D6C90ACF\n  sample 66:\n    time = 1320000\n    flags = 1\n    data = length 13, hash 6FD8A944\n  sample 67:\n    time = 1340000\n    flags = 1\n    data = length 13, hash A835BBF9\n  sample 68:\n    time = 1360000\n    flags = 1\n    data = length 13, hash F7713830\n  sample 69:\n    time = 1380000\n    flags = 1\n    data = length 13, hash 3AA966E5\n  sample 70:\n    time = 1400000\n    flags = 1\n    data = length 13, hash F939E829\n  sample 71:\n    time = 1420000\n    flags = 1\n    data = length 13, hash 7676DE49\n  sample 72:\n    time = 1440000\n    flags = 1\n    data = length 13, hash 93BB890A\n  sample 73:\n    time = 1460000\n    flags = 1\n    data = length 13, hash B57DBEC8\n  sample 74:\n    time = 1480000\n    flags = 1\n    data = length 13, hash 66B0A5B6\n  sample 75:\n    time = 1500000\n    flags = 1\n    data = length 13, hash D733E0D\n  sample 76:\n    time = 1520000\n    flags = 1\n    data = length 13, hash 80941726\n  sample 77:\n    time = 1540000\n    flags = 1\n    data = length 13, hash 556ED633\n  sample 78:\n    time = 1560000\n    flags = 1\n    data = length 13, hash C5EDF4E1\n  sample 79:\n    time = 1580000\n    flags = 1\n    data = length 13, hash 6B287445\n  sample 80:\n    time = 1600000\n    flags = 1\n    data = length 13, hash DC97C4A7\n  sample 81:\n    time = 1620000\n    flags = 1\n    data = length 13, hash DA8CBDF4\n  sample 82:\n    time = 1640000\n    flags = 1\n    data = length 13, hash 6F60FF77\n  sample 83:\n    time = 1660000\n    flags = 1\n    data = length 13, hash 3EB22B96\n  sample 84:\n    time = 1680000\n    flags = 1\n    data = length 13, hash B3C31AF5\n  sample 85:\n    time = 1700000\n    flags = 1\n    data = length 13, hash 1854AA92\n  sample 86:\n    time = 1720000\n    flags = 1\n    data = length 13, hash 6488264B\n  sample 87:\n    time = 1740000\n    flags = 1\n    data = length 13, hash 4CC8C5C1\n  sample 88:\n    time = 1760000\n    flags = 1\n    data = length 13, hash 19CC7523\n  sample 89:\n    time = 1780000\n    flags = 1\n    data = length 13, hash 9BE7B928\n  sample 90:\n    time = 1800000\n    flags = 1\n    data = length 13, hash 47EC7CFD\n  sample 91:\n    time = 1820000\n    flags = 1\n    data = length 13, hash EC940120\n  sample 92:\n    time = 1840000\n    flags = 1\n    data = length 13, hash 73BDA6D0\n  sample 93:\n    time = 1860000\n    flags = 1\n    data = length 13, hash FACB3314\n  sample 94:\n    time = 1880000\n    flags = 1\n    data = length 13, hash EC61D13B\n  sample 95:\n    time = 1900000\n    flags = 1\n    data = length 13, hash B28C7B6C\n  sample 96:\n    time = 1920000\n    flags = 1\n    data = length 13, hash B1A4CECD\n  sample 97:\n    time = 1940000\n    flags = 1\n    data = length 13, hash 56D41BA6\n  sample 98:\n    time = 1960000\n    flags = 1\n    data = length 13, hash 90499F4\n  sample 99:\n    time = 1980000\n    flags = 1\n    data = length 13, hash 65D9A9D3\n  sample 100:\n    time = 2000000\n    flags = 1\n    data = length 13, hash D9004CC\n  sample 101:\n    time = 2020000\n    flags = 1\n    data = length 13, hash 4139C6ED\n  sample 102:\n    time = 2040000\n    flags = 1\n    data = length 13, hash C4F8097C\n  sample 103:\n    time = 2060000\n    flags = 1\n    data = length 13, hash 94D424FA\n  sample 104:\n    time = 2080000\n    flags = 1\n    data = length 13, hash C2C6F5FD\n  sample 105:\n    time = 2100000\n    flags = 1\n    data = length 13, hash 15719008\n  sample 106:\n    time = 2120000\n    flags = 1\n    data = length 13, hash 4F64F524\n  sample 107:\n    time = 2140000\n    flags = 1\n    data = length 13, hash F9E01C1E\n  sample 108:\n    time = 2160000\n    flags = 1\n    data = length 13, hash 74C4EE74\n  sample 109:\n    time = 2180000\n    flags = 1\n    data = length 13, hash 7EE7553D\n  sample 110:\n    time = 2200000\n    flags = 1\n    data = length 13, hash 62DE6539\n  sample 111:\n    time = 2220000\n    flags = 1\n    data = length 13, hash 7F5EC222\n  sample 112:\n    time = 2240000\n    flags = 1\n    data = length 13, hash 644067F\n  sample 113:\n    time = 2260000\n    flags = 1\n    data = length 13, hash CDF6C9DC\n  sample 114:\n    time = 2280000\n    flags = 1\n    data = length 13, hash 8B5DBC80\n  sample 115:\n    time = 2300000\n    flags = 1\n    data = length 13, hash AD4BBA03\n  sample 116:\n    time = 2320000\n    flags = 1\n    data = length 13, hash 7A76340\n  sample 117:\n    time = 2340000\n    flags = 1\n    data = length 13, hash 3610F5B0\n  sample 118:\n    time = 2360000\n    flags = 1\n    data = length 13, hash 430BC60B\n  sample 119:\n    time = 2380000\n    flags = 1\n    data = length 13, hash 99CF1CA6\n  sample 120:\n    time = 2400000\n    flags = 1\n    data = length 13, hash 1331C70B\n  sample 121:\n    time = 2420000\n    flags = 1\n    data = length 13, hash BD76E69D\n  sample 122:\n    time = 2440000\n    flags = 1\n    data = length 13, hash 5DA652AC\n  sample 123:\n    time = 2460000\n    flags = 1\n    data = length 13, hash 3B7BF6CE\n  sample 124:\n    time = 2480000\n    flags = 1\n    data = length 13, hash ABBFD143\n  sample 125:\n    time = 2500000\n    flags = 1\n    data = length 13, hash E9447166\n  sample 126:\n    time = 2520000\n    flags = 1\n    data = length 13, hash EC40068C\n  sample 127:\n    time = 2540000\n    flags = 1\n    data = length 13, hash A2869400\n  sample 128:\n    time = 2560000\n    flags = 1\n    data = length 13, hash C7E0746B\n  sample 129:\n    time = 2580000\n    flags = 1\n    data = length 13, hash 60601BB1\n  sample 130:\n    time = 2600000\n    flags = 1\n    data = length 13, hash 975AAE9B\n  sample 131:\n    time = 2620000\n    flags = 1\n    data = length 13, hash 8BBC0EB2\n  sample 132:\n    time = 2640000\n    flags = 1\n    data = length 13, hash 57FB39E5\n  sample 133:\n    time = 2660000\n    flags = 1\n    data = length 13, hash 4CDCEEDB\n  sample 134:\n    time = 2680000\n    flags = 1\n    data = length 13, hash EA16E256\n  sample 135:\n    time = 2700000\n    flags = 1\n    data = length 13, hash 287E7D9E\n  sample 136:\n    time = 2720000\n    flags = 1\n    data = length 13, hash 55AB8FB9\n  sample 137:\n    time = 2740000\n    flags = 1\n    data = length 13, hash 129890EF\n  sample 138:\n    time = 2760000\n    flags = 1\n    data = length 13, hash 90834F57\n  sample 139:\n    time = 2780000\n    flags = 1\n    data = length 13, hash 5B3228E0\n  sample 140:\n    time = 2800000\n    flags = 1\n    data = length 13, hash DD19E175\n  sample 141:\n    time = 2820000\n    flags = 1\n    data = length 13, hash EE7EA342\n  sample 142:\n    time = 2840000\n    flags = 1\n    data = length 13, hash DB3AF473\n  sample 143:\n    time = 2860000\n    flags = 1\n    data = length 13, hash 25AEC43F\n  sample 144:\n    time = 2880000\n    flags = 1\n    data = length 13, hash EE9BF97F\n  sample 145:\n    time = 2900000\n    flags = 1\n    data = length 13, hash FFFBE047\n  sample 146:\n    time = 2920000\n    flags = 1\n    data = length 13, hash BEACFCB0\n  sample 147:\n    time = 2940000\n    flags = 1\n    data = length 13, hash AEB5096C\n  sample 148:\n    time = 2960000\n    flags = 1\n    data = length 13, hash B0D381B\n  sample 149:\n    time = 2980000\n    flags = 1\n    data = length 13, hash 3D9D5122\n  sample 150:\n    time = 3000000\n    flags = 1\n    data = length 13, hash 6C1DDB95\n  sample 151:\n    time = 3020000\n    flags = 1\n    data = length 13, hash ADACADCF\n  sample 152:\n    time = 3040000\n    flags = 1\n    data = length 13, hash 159E321E\n  sample 153:\n    time = 3060000\n    flags = 1\n    data = length 13, hash B1466264\n  sample 154:\n    time = 3080000\n    flags = 1\n    data = length 13, hash 4DDF7223\n  sample 155:\n    time = 3100000\n    flags = 1\n    data = length 13, hash C9BDB82A\n  sample 156:\n    time = 3120000\n    flags = 1\n    data = length 13, hash A49B2D9D\n  sample 157:\n    time = 3140000\n    flags = 1\n    data = length 13, hash D645E7E5\n  sample 158:\n    time = 3160000\n    flags = 1\n    data = length 13, hash 1C4232DC\n  sample 159:\n    time = 3180000\n    flags = 1\n    data = length 13, hash 83078219\n  sample 160:\n    time = 3200000\n    flags = 1\n    data = length 13, hash D6D8B072\n  sample 161:\n    time = 3220000\n    flags = 1\n    data = length 13, hash 975DB40\n  sample 162:\n    time = 3240000\n    flags = 1\n    data = length 13, hash A15FDD05\n  sample 163:\n    time = 3260000\n    flags = 1\n    data = length 13, hash 4B839E41\n  sample 164:\n    time = 3280000\n    flags = 1\n    data = length 13, hash 7418F499\n  sample 165:\n    time = 3300000\n    flags = 1\n    data = length 13, hash 7A4945E4\n  sample 166:\n    time = 3320000\n    flags = 1\n    data = length 13, hash 6249558C\n  sample 167:\n    time = 3340000\n    flags = 1\n    data = length 13, hash BD4C5BE3\n  sample 168:\n    time = 3360000\n    flags = 1\n    data = length 13, hash BAB30F1D\n  sample 169:\n    time = 3380000\n    flags = 1\n    data = length 13, hash 1E1C7012\n  sample 170:\n    time = 3400000\n    flags = 1\n    data = length 13, hash 9A3F8A89\n  sample 171:\n    time = 3420000\n    flags = 1\n    data = length 13, hash 20BE6D7B\n  sample 172:\n    time = 3440000\n    flags = 1\n    data = length 13, hash CAA0591D\n  sample 173:\n    time = 3460000\n    flags = 1\n    data = length 13, hash 6D554D17\n  sample 174:\n    time = 3480000\n    flags = 1\n    data = length 13, hash D97C3B31\n  sample 175:\n    time = 3500000\n    flags = 1\n    data = length 13, hash 75BC5C3\n  sample 176:\n    time = 3520000\n    flags = 1\n    data = length 13, hash 7BA1784B\n  sample 177:\n    time = 3540000\n    flags = 1\n    data = length 13, hash 1D175D92\n  sample 178:\n    time = 3560000\n    flags = 1\n    data = length 13, hash ADCA60FD\n  sample 179:\n    time = 3580000\n    flags = 1\n    data = length 13, hash 37018693\n  sample 180:\n    time = 3600000\n    flags = 1\n    data = length 13, hash 4553606F\n  sample 181:\n    time = 3620000\n    flags = 1\n    data = length 13, hash CF434565\n  sample 182:\n    time = 3640000\n    flags = 1\n    data = length 13, hash D264D757\n  sample 183:\n    time = 3660000\n    flags = 1\n    data = length 13, hash 4FB493EF\n  sample 184:\n    time = 3680000\n    flags = 1\n    data = length 13, hash 919F53A\n  sample 185:\n    time = 3700000\n    flags = 1\n    data = length 13, hash C22B009B\n  sample 186:\n    time = 3720000\n    flags = 1\n    data = length 13, hash 5981470\n  sample 187:\n    time = 3740000\n    flags = 1\n    data = length 13, hash A5D3937C\n  sample 188:\n    time = 3760000\n    flags = 1\n    data = length 13, hash A2504429\n  sample 189:\n    time = 3780000\n    flags = 1\n    data = length 13, hash AD1B70BE\n  sample 190:\n    time = 3800000\n    flags = 1\n    data = length 13, hash 2E39ED5E\n  sample 191:\n    time = 3820000\n    flags = 1\n    data = length 13, hash 13A8BE8E\n  sample 192:\n    time = 3840000\n    flags = 1\n    data = length 13, hash 1ACD740B\n  sample 193:\n    time = 3860000\n    flags = 1\n    data = length 13, hash 80F38B3\n  sample 194:\n    time = 3880000\n    flags = 1\n    data = length 13, hash DA9DA79F\n  sample 195:\n    time = 3900000\n    flags = 1\n    data = length 13, hash 21B95B7E\n  sample 196:\n    time = 3920000\n    flags = 1\n    data = length 13, hash CD22497B\n  sample 197:\n    time = 3940000\n    flags = 1\n    data = length 13, hash 718BB35D\n  sample 198:\n    time = 3960000\n    flags = 1\n    data = length 13, hash 69ABA6AD\n  sample 199:\n    time = 3980000\n    flags = 1\n    data = length 13, hash BAE19549\n  sample 200:\n    time = 4000000\n    flags = 1\n    data = length 13, hash 2A792FB3\n  sample 201:\n    time = 4020000\n    flags = 1\n    data = length 13, hash 71FCD8\n  sample 202:\n    time = 4040000\n    flags = 1\n    data = length 13, hash 44D2B5B3\n  sample 203:\n    time = 4060000\n    flags = 1\n    data = length 13, hash 1E87B11B\n  sample 204:\n    time = 4080000\n    flags = 1\n    data = length 13, hash 78CD2C11\n  sample 205:\n    time = 4100000\n    flags = 1\n    data = length 13, hash 9F198DF0\n  sample 206:\n    time = 4120000\n    flags = 1\n    data = length 13, hash B291F16A\n  sample 207:\n    time = 4140000\n    flags = 1\n    data = length 13, hash CF820EE0\n  sample 208:\n    time = 4160000\n    flags = 1\n    data = length 13, hash 4E24F683\n  sample 209:\n    time = 4180000\n    flags = 1\n    data = length 13, hash 52BCD68F\n  sample 210:\n    time = 4200000\n    flags = 1\n    data = length 13, hash 42588CB0\n  sample 211:\n    time = 4220000\n    flags = 1\n    data = length 13, hash EBBFECA2\n  sample 212:\n    time = 4240000\n    flags = 1\n    data = length 13, hash C11050CF\n  sample 213:\n    time = 4260000\n    flags = 1\n    data = length 13, hash 6F738603\n  sample 214:\n    time = 4280000\n    flags = 1\n    data = length 13, hash DAD06E5\n  sample 215:\n    time = 4300000\n    flags = 1\n    data = length 13, hash 5B036C64\n  sample 216:\n    time = 4320000\n    flags = 1\n    data = length 13, hash A58DC12E\n  sample 217:\n    time = 4340000\n    flags = 1\n    data = length 13, hash AC59BA7C\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/amr/sample_nb_cbr.amr.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 4360000\n  getPosition(0) = [[timeUs=0, position=6]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/3gpp\n    maxInputSize = 61\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 8000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 2834\n  sample count = 218\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 13, hash 371B046C\n  sample 1:\n    time = 20000\n    flags = 1\n    data = length 13, hash CE30BF5B\n  sample 2:\n    time = 40000\n    flags = 1\n    data = length 13, hash 19A59975\n  sample 3:\n    time = 60000\n    flags = 1\n    data = length 13, hash 4879773C\n  sample 4:\n    time = 80000\n    flags = 1\n    data = length 13, hash E8F83019\n  sample 5:\n    time = 100000\n    flags = 1\n    data = length 13, hash D265CDC9\n  sample 6:\n    time = 120000\n    flags = 1\n    data = length 13, hash 91653DAA\n  sample 7:\n    time = 140000\n    flags = 1\n    data = length 13, hash C79456F6\n  sample 8:\n    time = 160000\n    flags = 1\n    data = length 13, hash CDDC4422\n  sample 9:\n    time = 180000\n    flags = 1\n    data = length 13, hash D9ED3AF1\n  sample 10:\n    time = 200000\n    flags = 1\n    data = length 13, hash BAB75A33\n  sample 11:\n    time = 220000\n    flags = 1\n    data = length 13, hash 2221B4FF\n  sample 12:\n    time = 240000\n    flags = 1\n    data = length 13, hash 96400A0B\n  sample 13:\n    time = 260000\n    flags = 1\n    data = length 13, hash 582E6FB\n  sample 14:\n    time = 280000\n    flags = 1\n    data = length 13, hash C4E878E5\n  sample 15:\n    time = 300000\n    flags = 1\n    data = length 13, hash C849A1BD\n  sample 16:\n    time = 320000\n    flags = 1\n    data = length 13, hash CFA8A9ED\n  sample 17:\n    time = 340000\n    flags = 1\n    data = length 13, hash 70CA4907\n  sample 18:\n    time = 360000\n    flags = 1\n    data = length 13, hash B47D4454\n  sample 19:\n    time = 380000\n    flags = 1\n    data = length 13, hash 282998C1\n  sample 20:\n    time = 400000\n    flags = 1\n    data = length 13, hash 3F3F7A65\n  sample 21:\n    time = 420000\n    flags = 1\n    data = length 13, hash CC2EAB58\n  sample 22:\n    time = 440000\n    flags = 1\n    data = length 13, hash 279EF712\n  sample 23:\n    time = 460000\n    flags = 1\n    data = length 13, hash AA2F4B29\n  sample 24:\n    time = 480000\n    flags = 1\n    data = length 13, hash F6F658C4\n  sample 25:\n    time = 500000\n    flags = 1\n    data = length 13, hash D7DEBD17\n  sample 26:\n    time = 520000\n    flags = 1\n    data = length 13, hash 6DAB9A17\n  sample 27:\n    time = 540000\n    flags = 1\n    data = length 13, hash 6ECE1571\n  sample 28:\n    time = 560000\n    flags = 1\n    data = length 13, hash B3D0507F\n  sample 29:\n    time = 580000\n    flags = 1\n    data = length 13, hash 21E356B9\n  sample 30:\n    time = 600000\n    flags = 1\n    data = length 13, hash 410EA12\n  sample 31:\n    time = 620000\n    flags = 1\n    data = length 13, hash 533895A8\n  sample 32:\n    time = 640000\n    flags = 1\n    data = length 13, hash C61B3E5A\n  sample 33:\n    time = 660000\n    flags = 1\n    data = length 13, hash 982170E6\n  sample 34:\n    time = 680000\n    flags = 1\n    data = length 13, hash 7A0468C5\n  sample 35:\n    time = 700000\n    flags = 1\n    data = length 13, hash 9C85EAA7\n  sample 36:\n    time = 720000\n    flags = 1\n    data = length 13, hash B6B341B6\n  sample 37:\n    time = 740000\n    flags = 1\n    data = length 13, hash 6937532E\n  sample 38:\n    time = 760000\n    flags = 1\n    data = length 13, hash 8CF2A3A0\n  sample 39:\n    time = 780000\n    flags = 1\n    data = length 13, hash D2682AC6\n  sample 40:\n    time = 800000\n    flags = 1\n    data = length 13, hash BBC5710F\n  sample 41:\n    time = 820000\n    flags = 1\n    data = length 13, hash 59080B6C\n  sample 42:\n    time = 840000\n    flags = 1\n    data = length 13, hash E4118291\n  sample 43:\n    time = 860000\n    flags = 1\n    data = length 13, hash A1E5B296\n  sample 44:\n    time = 880000\n    flags = 1\n    data = length 13, hash D7B8F95B\n  sample 45:\n    time = 900000\n    flags = 1\n    data = length 13, hash CC839BE1\n  sample 46:\n    time = 920000\n    flags = 1\n    data = length 13, hash D459DFCE\n  sample 47:\n    time = 940000\n    flags = 1\n    data = length 13, hash D6AD19EC\n  sample 48:\n    time = 960000\n    flags = 1\n    data = length 13, hash D05E373D\n  sample 49:\n    time = 980000\n    flags = 1\n    data = length 13, hash 6A4460C7\n  sample 50:\n    time = 1000000\n    flags = 1\n    data = length 13, hash C9A0D93F\n  sample 51:\n    time = 1020000\n    flags = 1\n    data = length 13, hash 3FA819E7\n  sample 52:\n    time = 1040000\n    flags = 1\n    data = length 13, hash 1D3CBDFC\n  sample 53:\n    time = 1060000\n    flags = 1\n    data = length 13, hash 8BBBB403\n  sample 54:\n    time = 1080000\n    flags = 1\n    data = length 13, hash 21B4A0F9\n  sample 55:\n    time = 1100000\n    flags = 1\n    data = length 13, hash C0F921D1\n  sample 56:\n    time = 1120000\n    flags = 1\n    data = length 13, hash 5D812AAB\n  sample 57:\n    time = 1140000\n    flags = 1\n    data = length 13, hash 50C9F3F8\n  sample 58:\n    time = 1160000\n    flags = 1\n    data = length 13, hash 5C2BB5D1\n  sample 59:\n    time = 1180000\n    flags = 1\n    data = length 13, hash 6BF9BEA5\n  sample 60:\n    time = 1200000\n    flags = 1\n    data = length 13, hash 2738C1E6\n  sample 61:\n    time = 1220000\n    flags = 1\n    data = length 13, hash 5FC288A6\n  sample 62:\n    time = 1240000\n    flags = 1\n    data = length 13, hash 7E8E442A\n  sample 63:\n    time = 1260000\n    flags = 1\n    data = length 13, hash AEAA2BBA\n  sample 64:\n    time = 1280000\n    flags = 1\n    data = length 13, hash 4E2ACD2F\n  sample 65:\n    time = 1300000\n    flags = 1\n    data = length 13, hash D6C90ACF\n  sample 66:\n    time = 1320000\n    flags = 1\n    data = length 13, hash 6FD8A944\n  sample 67:\n    time = 1340000\n    flags = 1\n    data = length 13, hash A835BBF9\n  sample 68:\n    time = 1360000\n    flags = 1\n    data = length 13, hash F7713830\n  sample 69:\n    time = 1380000\n    flags = 1\n    data = length 13, hash 3AA966E5\n  sample 70:\n    time = 1400000\n    flags = 1\n    data = length 13, hash F939E829\n  sample 71:\n    time = 1420000\n    flags = 1\n    data = length 13, hash 7676DE49\n  sample 72:\n    time = 1440000\n    flags = 1\n    data = length 13, hash 93BB890A\n  sample 73:\n    time = 1460000\n    flags = 1\n    data = length 13, hash B57DBEC8\n  sample 74:\n    time = 1480000\n    flags = 1\n    data = length 13, hash 66B0A5B6\n  sample 75:\n    time = 1500000\n    flags = 1\n    data = length 13, hash D733E0D\n  sample 76:\n    time = 1520000\n    flags = 1\n    data = length 13, hash 80941726\n  sample 77:\n    time = 1540000\n    flags = 1\n    data = length 13, hash 556ED633\n  sample 78:\n    time = 1560000\n    flags = 1\n    data = length 13, hash C5EDF4E1\n  sample 79:\n    time = 1580000\n    flags = 1\n    data = length 13, hash 6B287445\n  sample 80:\n    time = 1600000\n    flags = 1\n    data = length 13, hash DC97C4A7\n  sample 81:\n    time = 1620000\n    flags = 1\n    data = length 13, hash DA8CBDF4\n  sample 82:\n    time = 1640000\n    flags = 1\n    data = length 13, hash 6F60FF77\n  sample 83:\n    time = 1660000\n    flags = 1\n    data = length 13, hash 3EB22B96\n  sample 84:\n    time = 1680000\n    flags = 1\n    data = length 13, hash B3C31AF5\n  sample 85:\n    time = 1700000\n    flags = 1\n    data = length 13, hash 1854AA92\n  sample 86:\n    time = 1720000\n    flags = 1\n    data = length 13, hash 6488264B\n  sample 87:\n    time = 1740000\n    flags = 1\n    data = length 13, hash 4CC8C5C1\n  sample 88:\n    time = 1760000\n    flags = 1\n    data = length 13, hash 19CC7523\n  sample 89:\n    time = 1780000\n    flags = 1\n    data = length 13, hash 9BE7B928\n  sample 90:\n    time = 1800000\n    flags = 1\n    data = length 13, hash 47EC7CFD\n  sample 91:\n    time = 1820000\n    flags = 1\n    data = length 13, hash EC940120\n  sample 92:\n    time = 1840000\n    flags = 1\n    data = length 13, hash 73BDA6D0\n  sample 93:\n    time = 1860000\n    flags = 1\n    data = length 13, hash FACB3314\n  sample 94:\n    time = 1880000\n    flags = 1\n    data = length 13, hash EC61D13B\n  sample 95:\n    time = 1900000\n    flags = 1\n    data = length 13, hash B28C7B6C\n  sample 96:\n    time = 1920000\n    flags = 1\n    data = length 13, hash B1A4CECD\n  sample 97:\n    time = 1940000\n    flags = 1\n    data = length 13, hash 56D41BA6\n  sample 98:\n    time = 1960000\n    flags = 1\n    data = length 13, hash 90499F4\n  sample 99:\n    time = 1980000\n    flags = 1\n    data = length 13, hash 65D9A9D3\n  sample 100:\n    time = 2000000\n    flags = 1\n    data = length 13, hash D9004CC\n  sample 101:\n    time = 2020000\n    flags = 1\n    data = length 13, hash 4139C6ED\n  sample 102:\n    time = 2040000\n    flags = 1\n    data = length 13, hash C4F8097C\n  sample 103:\n    time = 2060000\n    flags = 1\n    data = length 13, hash 94D424FA\n  sample 104:\n    time = 2080000\n    flags = 1\n    data = length 13, hash C2C6F5FD\n  sample 105:\n    time = 2100000\n    flags = 1\n    data = length 13, hash 15719008\n  sample 106:\n    time = 2120000\n    flags = 1\n    data = length 13, hash 4F64F524\n  sample 107:\n    time = 2140000\n    flags = 1\n    data = length 13, hash F9E01C1E\n  sample 108:\n    time = 2160000\n    flags = 1\n    data = length 13, hash 74C4EE74\n  sample 109:\n    time = 2180000\n    flags = 1\n    data = length 13, hash 7EE7553D\n  sample 110:\n    time = 2200000\n    flags = 1\n    data = length 13, hash 62DE6539\n  sample 111:\n    time = 2220000\n    flags = 1\n    data = length 13, hash 7F5EC222\n  sample 112:\n    time = 2240000\n    flags = 1\n    data = length 13, hash 644067F\n  sample 113:\n    time = 2260000\n    flags = 1\n    data = length 13, hash CDF6C9DC\n  sample 114:\n    time = 2280000\n    flags = 1\n    data = length 13, hash 8B5DBC80\n  sample 115:\n    time = 2300000\n    flags = 1\n    data = length 13, hash AD4BBA03\n  sample 116:\n    time = 2320000\n    flags = 1\n    data = length 13, hash 7A76340\n  sample 117:\n    time = 2340000\n    flags = 1\n    data = length 13, hash 3610F5B0\n  sample 118:\n    time = 2360000\n    flags = 1\n    data = length 13, hash 430BC60B\n  sample 119:\n    time = 2380000\n    flags = 1\n    data = length 13, hash 99CF1CA6\n  sample 120:\n    time = 2400000\n    flags = 1\n    data = length 13, hash 1331C70B\n  sample 121:\n    time = 2420000\n    flags = 1\n    data = length 13, hash BD76E69D\n  sample 122:\n    time = 2440000\n    flags = 1\n    data = length 13, hash 5DA652AC\n  sample 123:\n    time = 2460000\n    flags = 1\n    data = length 13, hash 3B7BF6CE\n  sample 124:\n    time = 2480000\n    flags = 1\n    data = length 13, hash ABBFD143\n  sample 125:\n    time = 2500000\n    flags = 1\n    data = length 13, hash E9447166\n  sample 126:\n    time = 2520000\n    flags = 1\n    data = length 13, hash EC40068C\n  sample 127:\n    time = 2540000\n    flags = 1\n    data = length 13, hash A2869400\n  sample 128:\n    time = 2560000\n    flags = 1\n    data = length 13, hash C7E0746B\n  sample 129:\n    time = 2580000\n    flags = 1\n    data = length 13, hash 60601BB1\n  sample 130:\n    time = 2600000\n    flags = 1\n    data = length 13, hash 975AAE9B\n  sample 131:\n    time = 2620000\n    flags = 1\n    data = length 13, hash 8BBC0EB2\n  sample 132:\n    time = 2640000\n    flags = 1\n    data = length 13, hash 57FB39E5\n  sample 133:\n    time = 2660000\n    flags = 1\n    data = length 13, hash 4CDCEEDB\n  sample 134:\n    time = 2680000\n    flags = 1\n    data = length 13, hash EA16E256\n  sample 135:\n    time = 2700000\n    flags = 1\n    data = length 13, hash 287E7D9E\n  sample 136:\n    time = 2720000\n    flags = 1\n    data = length 13, hash 55AB8FB9\n  sample 137:\n    time = 2740000\n    flags = 1\n    data = length 13, hash 129890EF\n  sample 138:\n    time = 2760000\n    flags = 1\n    data = length 13, hash 90834F57\n  sample 139:\n    time = 2780000\n    flags = 1\n    data = length 13, hash 5B3228E0\n  sample 140:\n    time = 2800000\n    flags = 1\n    data = length 13, hash DD19E175\n  sample 141:\n    time = 2820000\n    flags = 1\n    data = length 13, hash EE7EA342\n  sample 142:\n    time = 2840000\n    flags = 1\n    data = length 13, hash DB3AF473\n  sample 143:\n    time = 2860000\n    flags = 1\n    data = length 13, hash 25AEC43F\n  sample 144:\n    time = 2880000\n    flags = 1\n    data = length 13, hash EE9BF97F\n  sample 145:\n    time = 2900000\n    flags = 1\n    data = length 13, hash FFFBE047\n  sample 146:\n    time = 2920000\n    flags = 1\n    data = length 13, hash BEACFCB0\n  sample 147:\n    time = 2940000\n    flags = 1\n    data = length 13, hash AEB5096C\n  sample 148:\n    time = 2960000\n    flags = 1\n    data = length 13, hash B0D381B\n  sample 149:\n    time = 2980000\n    flags = 1\n    data = length 13, hash 3D9D5122\n  sample 150:\n    time = 3000000\n    flags = 1\n    data = length 13, hash 6C1DDB95\n  sample 151:\n    time = 3020000\n    flags = 1\n    data = length 13, hash ADACADCF\n  sample 152:\n    time = 3040000\n    flags = 1\n    data = length 13, hash 159E321E\n  sample 153:\n    time = 3060000\n    flags = 1\n    data = length 13, hash B1466264\n  sample 154:\n    time = 3080000\n    flags = 1\n    data = length 13, hash 4DDF7223\n  sample 155:\n    time = 3100000\n    flags = 1\n    data = length 13, hash C9BDB82A\n  sample 156:\n    time = 3120000\n    flags = 1\n    data = length 13, hash A49B2D9D\n  sample 157:\n    time = 3140000\n    flags = 1\n    data = length 13, hash D645E7E5\n  sample 158:\n    time = 3160000\n    flags = 1\n    data = length 13, hash 1C4232DC\n  sample 159:\n    time = 3180000\n    flags = 1\n    data = length 13, hash 83078219\n  sample 160:\n    time = 3200000\n    flags = 1\n    data = length 13, hash D6D8B072\n  sample 161:\n    time = 3220000\n    flags = 1\n    data = length 13, hash 975DB40\n  sample 162:\n    time = 3240000\n    flags = 1\n    data = length 13, hash A15FDD05\n  sample 163:\n    time = 3260000\n    flags = 1\n    data = length 13, hash 4B839E41\n  sample 164:\n    time = 3280000\n    flags = 1\n    data = length 13, hash 7418F499\n  sample 165:\n    time = 3300000\n    flags = 1\n    data = length 13, hash 7A4945E4\n  sample 166:\n    time = 3320000\n    flags = 1\n    data = length 13, hash 6249558C\n  sample 167:\n    time = 3340000\n    flags = 1\n    data = length 13, hash BD4C5BE3\n  sample 168:\n    time = 3360000\n    flags = 1\n    data = length 13, hash BAB30F1D\n  sample 169:\n    time = 3380000\n    flags = 1\n    data = length 13, hash 1E1C7012\n  sample 170:\n    time = 3400000\n    flags = 1\n    data = length 13, hash 9A3F8A89\n  sample 171:\n    time = 3420000\n    flags = 1\n    data = length 13, hash 20BE6D7B\n  sample 172:\n    time = 3440000\n    flags = 1\n    data = length 13, hash CAA0591D\n  sample 173:\n    time = 3460000\n    flags = 1\n    data = length 13, hash 6D554D17\n  sample 174:\n    time = 3480000\n    flags = 1\n    data = length 13, hash D97C3B31\n  sample 175:\n    time = 3500000\n    flags = 1\n    data = length 13, hash 75BC5C3\n  sample 176:\n    time = 3520000\n    flags = 1\n    data = length 13, hash 7BA1784B\n  sample 177:\n    time = 3540000\n    flags = 1\n    data = length 13, hash 1D175D92\n  sample 178:\n    time = 3560000\n    flags = 1\n    data = length 13, hash ADCA60FD\n  sample 179:\n    time = 3580000\n    flags = 1\n    data = length 13, hash 37018693\n  sample 180:\n    time = 3600000\n    flags = 1\n    data = length 13, hash 4553606F\n  sample 181:\n    time = 3620000\n    flags = 1\n    data = length 13, hash CF434565\n  sample 182:\n    time = 3640000\n    flags = 1\n    data = length 13, hash D264D757\n  sample 183:\n    time = 3660000\n    flags = 1\n    data = length 13, hash 4FB493EF\n  sample 184:\n    time = 3680000\n    flags = 1\n    data = length 13, hash 919F53A\n  sample 185:\n    time = 3700000\n    flags = 1\n    data = length 13, hash C22B009B\n  sample 186:\n    time = 3720000\n    flags = 1\n    data = length 13, hash 5981470\n  sample 187:\n    time = 3740000\n    flags = 1\n    data = length 13, hash A5D3937C\n  sample 188:\n    time = 3760000\n    flags = 1\n    data = length 13, hash A2504429\n  sample 189:\n    time = 3780000\n    flags = 1\n    data = length 13, hash AD1B70BE\n  sample 190:\n    time = 3800000\n    flags = 1\n    data = length 13, hash 2E39ED5E\n  sample 191:\n    time = 3820000\n    flags = 1\n    data = length 13, hash 13A8BE8E\n  sample 192:\n    time = 3840000\n    flags = 1\n    data = length 13, hash 1ACD740B\n  sample 193:\n    time = 3860000\n    flags = 1\n    data = length 13, hash 80F38B3\n  sample 194:\n    time = 3880000\n    flags = 1\n    data = length 13, hash DA9DA79F\n  sample 195:\n    time = 3900000\n    flags = 1\n    data = length 13, hash 21B95B7E\n  sample 196:\n    time = 3920000\n    flags = 1\n    data = length 13, hash CD22497B\n  sample 197:\n    time = 3940000\n    flags = 1\n    data = length 13, hash 718BB35D\n  sample 198:\n    time = 3960000\n    flags = 1\n    data = length 13, hash 69ABA6AD\n  sample 199:\n    time = 3980000\n    flags = 1\n    data = length 13, hash BAE19549\n  sample 200:\n    time = 4000000\n    flags = 1\n    data = length 13, hash 2A792FB3\n  sample 201:\n    time = 4020000\n    flags = 1\n    data = length 13, hash 71FCD8\n  sample 202:\n    time = 4040000\n    flags = 1\n    data = length 13, hash 44D2B5B3\n  sample 203:\n    time = 4060000\n    flags = 1\n    data = length 13, hash 1E87B11B\n  sample 204:\n    time = 4080000\n    flags = 1\n    data = length 13, hash 78CD2C11\n  sample 205:\n    time = 4100000\n    flags = 1\n    data = length 13, hash 9F198DF0\n  sample 206:\n    time = 4120000\n    flags = 1\n    data = length 13, hash B291F16A\n  sample 207:\n    time = 4140000\n    flags = 1\n    data = length 13, hash CF820EE0\n  sample 208:\n    time = 4160000\n    flags = 1\n    data = length 13, hash 4E24F683\n  sample 209:\n    time = 4180000\n    flags = 1\n    data = length 13, hash 52BCD68F\n  sample 210:\n    time = 4200000\n    flags = 1\n    data = length 13, hash 42588CB0\n  sample 211:\n    time = 4220000\n    flags = 1\n    data = length 13, hash EBBFECA2\n  sample 212:\n    time = 4240000\n    flags = 1\n    data = length 13, hash C11050CF\n  sample 213:\n    time = 4260000\n    flags = 1\n    data = length 13, hash 6F738603\n  sample 214:\n    time = 4280000\n    flags = 1\n    data = length 13, hash DAD06E5\n  sample 215:\n    time = 4300000\n    flags = 1\n    data = length 13, hash 5B036C64\n  sample 216:\n    time = 4320000\n    flags = 1\n    data = length 13, hash A58DC12E\n  sample 217:\n    time = 4340000\n    flags = 1\n    data = length 13, hash AC59BA7C\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/amr/sample_nb_cbr.amr.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 4360000\n  getPosition(0) = [[timeUs=0, position=6]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/3gpp\n    maxInputSize = 61\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 8000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 1898\n  sample count = 146\n  sample 0:\n    time = 1440000\n    flags = 1\n    data = length 13, hash 93BB890A\n  sample 1:\n    time = 1460000\n    flags = 1\n    data = length 13, hash B57DBEC8\n  sample 2:\n    time = 1480000\n    flags = 1\n    data = length 13, hash 66B0A5B6\n  sample 3:\n    time = 1500000\n    flags = 1\n    data = length 13, hash D733E0D\n  sample 4:\n    time = 1520000\n    flags = 1\n    data = length 13, hash 80941726\n  sample 5:\n    time = 1540000\n    flags = 1\n    data = length 13, hash 556ED633\n  sample 6:\n    time = 1560000\n    flags = 1\n    data = length 13, hash C5EDF4E1\n  sample 7:\n    time = 1580000\n    flags = 1\n    data = length 13, hash 6B287445\n  sample 8:\n    time = 1600000\n    flags = 1\n    data = length 13, hash DC97C4A7\n  sample 9:\n    time = 1620000\n    flags = 1\n    data = length 13, hash DA8CBDF4\n  sample 10:\n    time = 1640000\n    flags = 1\n    data = length 13, hash 6F60FF77\n  sample 11:\n    time = 1660000\n    flags = 1\n    data = length 13, hash 3EB22B96\n  sample 12:\n    time = 1680000\n    flags = 1\n    data = length 13, hash B3C31AF5\n  sample 13:\n    time = 1700000\n    flags = 1\n    data = length 13, hash 1854AA92\n  sample 14:\n    time = 1720000\n    flags = 1\n    data = length 13, hash 6488264B\n  sample 15:\n    time = 1740000\n    flags = 1\n    data = length 13, hash 4CC8C5C1\n  sample 16:\n    time = 1760000\n    flags = 1\n    data = length 13, hash 19CC7523\n  sample 17:\n    time = 1780000\n    flags = 1\n    data = length 13, hash 9BE7B928\n  sample 18:\n    time = 1800000\n    flags = 1\n    data = length 13, hash 47EC7CFD\n  sample 19:\n    time = 1820000\n    flags = 1\n    data = length 13, hash EC940120\n  sample 20:\n    time = 1840000\n    flags = 1\n    data = length 13, hash 73BDA6D0\n  sample 21:\n    time = 1860000\n    flags = 1\n    data = length 13, hash FACB3314\n  sample 22:\n    time = 1880000\n    flags = 1\n    data = length 13, hash EC61D13B\n  sample 23:\n    time = 1900000\n    flags = 1\n    data = length 13, hash B28C7B6C\n  sample 24:\n    time = 1920000\n    flags = 1\n    data = length 13, hash B1A4CECD\n  sample 25:\n    time = 1940000\n    flags = 1\n    data = length 13, hash 56D41BA6\n  sample 26:\n    time = 1960000\n    flags = 1\n    data = length 13, hash 90499F4\n  sample 27:\n    time = 1980000\n    flags = 1\n    data = length 13, hash 65D9A9D3\n  sample 28:\n    time = 2000000\n    flags = 1\n    data = length 13, hash D9004CC\n  sample 29:\n    time = 2020000\n    flags = 1\n    data = length 13, hash 4139C6ED\n  sample 30:\n    time = 2040000\n    flags = 1\n    data = length 13, hash C4F8097C\n  sample 31:\n    time = 2060000\n    flags = 1\n    data = length 13, hash 94D424FA\n  sample 32:\n    time = 2080000\n    flags = 1\n    data = length 13, hash C2C6F5FD\n  sample 33:\n    time = 2100000\n    flags = 1\n    data = length 13, hash 15719008\n  sample 34:\n    time = 2120000\n    flags = 1\n    data = length 13, hash 4F64F524\n  sample 35:\n    time = 2140000\n    flags = 1\n    data = length 13, hash F9E01C1E\n  sample 36:\n    time = 2160000\n    flags = 1\n    data = length 13, hash 74C4EE74\n  sample 37:\n    time = 2180000\n    flags = 1\n    data = length 13, hash 7EE7553D\n  sample 38:\n    time = 2200000\n    flags = 1\n    data = length 13, hash 62DE6539\n  sample 39:\n    time = 2220000\n    flags = 1\n    data = length 13, hash 7F5EC222\n  sample 40:\n    time = 2240000\n    flags = 1\n    data = length 13, hash 644067F\n  sample 41:\n    time = 2260000\n    flags = 1\n    data = length 13, hash CDF6C9DC\n  sample 42:\n    time = 2280000\n    flags = 1\n    data = length 13, hash 8B5DBC80\n  sample 43:\n    time = 2300000\n    flags = 1\n    data = length 13, hash AD4BBA03\n  sample 44:\n    time = 2320000\n    flags = 1\n    data = length 13, hash 7A76340\n  sample 45:\n    time = 2340000\n    flags = 1\n    data = length 13, hash 3610F5B0\n  sample 46:\n    time = 2360000\n    flags = 1\n    data = length 13, hash 430BC60B\n  sample 47:\n    time = 2380000\n    flags = 1\n    data = length 13, hash 99CF1CA6\n  sample 48:\n    time = 2400000\n    flags = 1\n    data = length 13, hash 1331C70B\n  sample 49:\n    time = 2420000\n    flags = 1\n    data = length 13, hash BD76E69D\n  sample 50:\n    time = 2440000\n    flags = 1\n    data = length 13, hash 5DA652AC\n  sample 51:\n    time = 2460000\n    flags = 1\n    data = length 13, hash 3B7BF6CE\n  sample 52:\n    time = 2480000\n    flags = 1\n    data = length 13, hash ABBFD143\n  sample 53:\n    time = 2500000\n    flags = 1\n    data = length 13, hash E9447166\n  sample 54:\n    time = 2520000\n    flags = 1\n    data = length 13, hash EC40068C\n  sample 55:\n    time = 2540000\n    flags = 1\n    data = length 13, hash A2869400\n  sample 56:\n    time = 2560000\n    flags = 1\n    data = length 13, hash C7E0746B\n  sample 57:\n    time = 2580000\n    flags = 1\n    data = length 13, hash 60601BB1\n  sample 58:\n    time = 2600000\n    flags = 1\n    data = length 13, hash 975AAE9B\n  sample 59:\n    time = 2620000\n    flags = 1\n    data = length 13, hash 8BBC0EB2\n  sample 60:\n    time = 2640000\n    flags = 1\n    data = length 13, hash 57FB39E5\n  sample 61:\n    time = 2660000\n    flags = 1\n    data = length 13, hash 4CDCEEDB\n  sample 62:\n    time = 2680000\n    flags = 1\n    data = length 13, hash EA16E256\n  sample 63:\n    time = 2700000\n    flags = 1\n    data = length 13, hash 287E7D9E\n  sample 64:\n    time = 2720000\n    flags = 1\n    data = length 13, hash 55AB8FB9\n  sample 65:\n    time = 2740000\n    flags = 1\n    data = length 13, hash 129890EF\n  sample 66:\n    time = 2760000\n    flags = 1\n    data = length 13, hash 90834F57\n  sample 67:\n    time = 2780000\n    flags = 1\n    data = length 13, hash 5B3228E0\n  sample 68:\n    time = 2800000\n    flags = 1\n    data = length 13, hash DD19E175\n  sample 69:\n    time = 2820000\n    flags = 1\n    data = length 13, hash EE7EA342\n  sample 70:\n    time = 2840000\n    flags = 1\n    data = length 13, hash DB3AF473\n  sample 71:\n    time = 2860000\n    flags = 1\n    data = length 13, hash 25AEC43F\n  sample 72:\n    time = 2880000\n    flags = 1\n    data = length 13, hash EE9BF97F\n  sample 73:\n    time = 2900000\n    flags = 1\n    data = length 13, hash FFFBE047\n  sample 74:\n    time = 2920000\n    flags = 1\n    data = length 13, hash BEACFCB0\n  sample 75:\n    time = 2940000\n    flags = 1\n    data = length 13, hash AEB5096C\n  sample 76:\n    time = 2960000\n    flags = 1\n    data = length 13, hash B0D381B\n  sample 77:\n    time = 2980000\n    flags = 1\n    data = length 13, hash 3D9D5122\n  sample 78:\n    time = 3000000\n    flags = 1\n    data = length 13, hash 6C1DDB95\n  sample 79:\n    time = 3020000\n    flags = 1\n    data = length 13, hash ADACADCF\n  sample 80:\n    time = 3040000\n    flags = 1\n    data = length 13, hash 159E321E\n  sample 81:\n    time = 3060000\n    flags = 1\n    data = length 13, hash B1466264\n  sample 82:\n    time = 3080000\n    flags = 1\n    data = length 13, hash 4DDF7223\n  sample 83:\n    time = 3100000\n    flags = 1\n    data = length 13, hash C9BDB82A\n  sample 84:\n    time = 3120000\n    flags = 1\n    data = length 13, hash A49B2D9D\n  sample 85:\n    time = 3140000\n    flags = 1\n    data = length 13, hash D645E7E5\n  sample 86:\n    time = 3160000\n    flags = 1\n    data = length 13, hash 1C4232DC\n  sample 87:\n    time = 3180000\n    flags = 1\n    data = length 13, hash 83078219\n  sample 88:\n    time = 3200000\n    flags = 1\n    data = length 13, hash D6D8B072\n  sample 89:\n    time = 3220000\n    flags = 1\n    data = length 13, hash 975DB40\n  sample 90:\n    time = 3240000\n    flags = 1\n    data = length 13, hash A15FDD05\n  sample 91:\n    time = 3260000\n    flags = 1\n    data = length 13, hash 4B839E41\n  sample 92:\n    time = 3280000\n    flags = 1\n    data = length 13, hash 7418F499\n  sample 93:\n    time = 3300000\n    flags = 1\n    data = length 13, hash 7A4945E4\n  sample 94:\n    time = 3320000\n    flags = 1\n    data = length 13, hash 6249558C\n  sample 95:\n    time = 3340000\n    flags = 1\n    data = length 13, hash BD4C5BE3\n  sample 96:\n    time = 3360000\n    flags = 1\n    data = length 13, hash BAB30F1D\n  sample 97:\n    time = 3380000\n    flags = 1\n    data = length 13, hash 1E1C7012\n  sample 98:\n    time = 3400000\n    flags = 1\n    data = length 13, hash 9A3F8A89\n  sample 99:\n    time = 3420000\n    flags = 1\n    data = length 13, hash 20BE6D7B\n  sample 100:\n    time = 3440000\n    flags = 1\n    data = length 13, hash CAA0591D\n  sample 101:\n    time = 3460000\n    flags = 1\n    data = length 13, hash 6D554D17\n  sample 102:\n    time = 3480000\n    flags = 1\n    data = length 13, hash D97C3B31\n  sample 103:\n    time = 3500000\n    flags = 1\n    data = length 13, hash 75BC5C3\n  sample 104:\n    time = 3520000\n    flags = 1\n    data = length 13, hash 7BA1784B\n  sample 105:\n    time = 3540000\n    flags = 1\n    data = length 13, hash 1D175D92\n  sample 106:\n    time = 3560000\n    flags = 1\n    data = length 13, hash ADCA60FD\n  sample 107:\n    time = 3580000\n    flags = 1\n    data = length 13, hash 37018693\n  sample 108:\n    time = 3600000\n    flags = 1\n    data = length 13, hash 4553606F\n  sample 109:\n    time = 3620000\n    flags = 1\n    data = length 13, hash CF434565\n  sample 110:\n    time = 3640000\n    flags = 1\n    data = length 13, hash D264D757\n  sample 111:\n    time = 3660000\n    flags = 1\n    data = length 13, hash 4FB493EF\n  sample 112:\n    time = 3680000\n    flags = 1\n    data = length 13, hash 919F53A\n  sample 113:\n    time = 3700000\n    flags = 1\n    data = length 13, hash C22B009B\n  sample 114:\n    time = 3720000\n    flags = 1\n    data = length 13, hash 5981470\n  sample 115:\n    time = 3740000\n    flags = 1\n    data = length 13, hash A5D3937C\n  sample 116:\n    time = 3760000\n    flags = 1\n    data = length 13, hash A2504429\n  sample 117:\n    time = 3780000\n    flags = 1\n    data = length 13, hash AD1B70BE\n  sample 118:\n    time = 3800000\n    flags = 1\n    data = length 13, hash 2E39ED5E\n  sample 119:\n    time = 3820000\n    flags = 1\n    data = length 13, hash 13A8BE8E\n  sample 120:\n    time = 3840000\n    flags = 1\n    data = length 13, hash 1ACD740B\n  sample 121:\n    time = 3860000\n    flags = 1\n    data = length 13, hash 80F38B3\n  sample 122:\n    time = 3880000\n    flags = 1\n    data = length 13, hash DA9DA79F\n  sample 123:\n    time = 3900000\n    flags = 1\n    data = length 13, hash 21B95B7E\n  sample 124:\n    time = 3920000\n    flags = 1\n    data = length 13, hash CD22497B\n  sample 125:\n    time = 3940000\n    flags = 1\n    data = length 13, hash 718BB35D\n  sample 126:\n    time = 3960000\n    flags = 1\n    data = length 13, hash 69ABA6AD\n  sample 127:\n    time = 3980000\n    flags = 1\n    data = length 13, hash BAE19549\n  sample 128:\n    time = 4000000\n    flags = 1\n    data = length 13, hash 2A792FB3\n  sample 129:\n    time = 4020000\n    flags = 1\n    data = length 13, hash 71FCD8\n  sample 130:\n    time = 4040000\n    flags = 1\n    data = length 13, hash 44D2B5B3\n  sample 131:\n    time = 4060000\n    flags = 1\n    data = length 13, hash 1E87B11B\n  sample 132:\n    time = 4080000\n    flags = 1\n    data = length 13, hash 78CD2C11\n  sample 133:\n    time = 4100000\n    flags = 1\n    data = length 13, hash 9F198DF0\n  sample 134:\n    time = 4120000\n    flags = 1\n    data = length 13, hash B291F16A\n  sample 135:\n    time = 4140000\n    flags = 1\n    data = length 13, hash CF820EE0\n  sample 136:\n    time = 4160000\n    flags = 1\n    data = length 13, hash 4E24F683\n  sample 137:\n    time = 4180000\n    flags = 1\n    data = length 13, hash 52BCD68F\n  sample 138:\n    time = 4200000\n    flags = 1\n    data = length 13, hash 42588CB0\n  sample 139:\n    time = 4220000\n    flags = 1\n    data = length 13, hash EBBFECA2\n  sample 140:\n    time = 4240000\n    flags = 1\n    data = length 13, hash C11050CF\n  sample 141:\n    time = 4260000\n    flags = 1\n    data = length 13, hash 6F738603\n  sample 142:\n    time = 4280000\n    flags = 1\n    data = length 13, hash DAD06E5\n  sample 143:\n    time = 4300000\n    flags = 1\n    data = length 13, hash 5B036C64\n  sample 144:\n    time = 4320000\n    flags = 1\n    data = length 13, hash A58DC12E\n  sample 145:\n    time = 4340000\n    flags = 1\n    data = length 13, hash AC59BA7C\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/amr/sample_nb_cbr.amr.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 4360000\n  getPosition(0) = [[timeUs=0, position=6]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/3gpp\n    maxInputSize = 61\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 8000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 949\n  sample count = 73\n  sample 0:\n    time = 2900000\n    flags = 1\n    data = length 13, hash FFFBE047\n  sample 1:\n    time = 2920000\n    flags = 1\n    data = length 13, hash BEACFCB0\n  sample 2:\n    time = 2940000\n    flags = 1\n    data = length 13, hash AEB5096C\n  sample 3:\n    time = 2960000\n    flags = 1\n    data = length 13, hash B0D381B\n  sample 4:\n    time = 2980000\n    flags = 1\n    data = length 13, hash 3D9D5122\n  sample 5:\n    time = 3000000\n    flags = 1\n    data = length 13, hash 6C1DDB95\n  sample 6:\n    time = 3020000\n    flags = 1\n    data = length 13, hash ADACADCF\n  sample 7:\n    time = 3040000\n    flags = 1\n    data = length 13, hash 159E321E\n  sample 8:\n    time = 3060000\n    flags = 1\n    data = length 13, hash B1466264\n  sample 9:\n    time = 3080000\n    flags = 1\n    data = length 13, hash 4DDF7223\n  sample 10:\n    time = 3100000\n    flags = 1\n    data = length 13, hash C9BDB82A\n  sample 11:\n    time = 3120000\n    flags = 1\n    data = length 13, hash A49B2D9D\n  sample 12:\n    time = 3140000\n    flags = 1\n    data = length 13, hash D645E7E5\n  sample 13:\n    time = 3160000\n    flags = 1\n    data = length 13, hash 1C4232DC\n  sample 14:\n    time = 3180000\n    flags = 1\n    data = length 13, hash 83078219\n  sample 15:\n    time = 3200000\n    flags = 1\n    data = length 13, hash D6D8B072\n  sample 16:\n    time = 3220000\n    flags = 1\n    data = length 13, hash 975DB40\n  sample 17:\n    time = 3240000\n    flags = 1\n    data = length 13, hash A15FDD05\n  sample 18:\n    time = 3260000\n    flags = 1\n    data = length 13, hash 4B839E41\n  sample 19:\n    time = 3280000\n    flags = 1\n    data = length 13, hash 7418F499\n  sample 20:\n    time = 3300000\n    flags = 1\n    data = length 13, hash 7A4945E4\n  sample 21:\n    time = 3320000\n    flags = 1\n    data = length 13, hash 6249558C\n  sample 22:\n    time = 3340000\n    flags = 1\n    data = length 13, hash BD4C5BE3\n  sample 23:\n    time = 3360000\n    flags = 1\n    data = length 13, hash BAB30F1D\n  sample 24:\n    time = 3380000\n    flags = 1\n    data = length 13, hash 1E1C7012\n  sample 25:\n    time = 3400000\n    flags = 1\n    data = length 13, hash 9A3F8A89\n  sample 26:\n    time = 3420000\n    flags = 1\n    data = length 13, hash 20BE6D7B\n  sample 27:\n    time = 3440000\n    flags = 1\n    data = length 13, hash CAA0591D\n  sample 28:\n    time = 3460000\n    flags = 1\n    data = length 13, hash 6D554D17\n  sample 29:\n    time = 3480000\n    flags = 1\n    data = length 13, hash D97C3B31\n  sample 30:\n    time = 3500000\n    flags = 1\n    data = length 13, hash 75BC5C3\n  sample 31:\n    time = 3520000\n    flags = 1\n    data = length 13, hash 7BA1784B\n  sample 32:\n    time = 3540000\n    flags = 1\n    data = length 13, hash 1D175D92\n  sample 33:\n    time = 3560000\n    flags = 1\n    data = length 13, hash ADCA60FD\n  sample 34:\n    time = 3580000\n    flags = 1\n    data = length 13, hash 37018693\n  sample 35:\n    time = 3600000\n    flags = 1\n    data = length 13, hash 4553606F\n  sample 36:\n    time = 3620000\n    flags = 1\n    data = length 13, hash CF434565\n  sample 37:\n    time = 3640000\n    flags = 1\n    data = length 13, hash D264D757\n  sample 38:\n    time = 3660000\n    flags = 1\n    data = length 13, hash 4FB493EF\n  sample 39:\n    time = 3680000\n    flags = 1\n    data = length 13, hash 919F53A\n  sample 40:\n    time = 3700000\n    flags = 1\n    data = length 13, hash C22B009B\n  sample 41:\n    time = 3720000\n    flags = 1\n    data = length 13, hash 5981470\n  sample 42:\n    time = 3740000\n    flags = 1\n    data = length 13, hash A5D3937C\n  sample 43:\n    time = 3760000\n    flags = 1\n    data = length 13, hash A2504429\n  sample 44:\n    time = 3780000\n    flags = 1\n    data = length 13, hash AD1B70BE\n  sample 45:\n    time = 3800000\n    flags = 1\n    data = length 13, hash 2E39ED5E\n  sample 46:\n    time = 3820000\n    flags = 1\n    data = length 13, hash 13A8BE8E\n  sample 47:\n    time = 3840000\n    flags = 1\n    data = length 13, hash 1ACD740B\n  sample 48:\n    time = 3860000\n    flags = 1\n    data = length 13, hash 80F38B3\n  sample 49:\n    time = 3880000\n    flags = 1\n    data = length 13, hash DA9DA79F\n  sample 50:\n    time = 3900000\n    flags = 1\n    data = length 13, hash 21B95B7E\n  sample 51:\n    time = 3920000\n    flags = 1\n    data = length 13, hash CD22497B\n  sample 52:\n    time = 3940000\n    flags = 1\n    data = length 13, hash 718BB35D\n  sample 53:\n    time = 3960000\n    flags = 1\n    data = length 13, hash 69ABA6AD\n  sample 54:\n    time = 3980000\n    flags = 1\n    data = length 13, hash BAE19549\n  sample 55:\n    time = 4000000\n    flags = 1\n    data = length 13, hash 2A792FB3\n  sample 56:\n    time = 4020000\n    flags = 1\n    data = length 13, hash 71FCD8\n  sample 57:\n    time = 4040000\n    flags = 1\n    data = length 13, hash 44D2B5B3\n  sample 58:\n    time = 4060000\n    flags = 1\n    data = length 13, hash 1E87B11B\n  sample 59:\n    time = 4080000\n    flags = 1\n    data = length 13, hash 78CD2C11\n  sample 60:\n    time = 4100000\n    flags = 1\n    data = length 13, hash 9F198DF0\n  sample 61:\n    time = 4120000\n    flags = 1\n    data = length 13, hash B291F16A\n  sample 62:\n    time = 4140000\n    flags = 1\n    data = length 13, hash CF820EE0\n  sample 63:\n    time = 4160000\n    flags = 1\n    data = length 13, hash 4E24F683\n  sample 64:\n    time = 4180000\n    flags = 1\n    data = length 13, hash 52BCD68F\n  sample 65:\n    time = 4200000\n    flags = 1\n    data = length 13, hash 42588CB0\n  sample 66:\n    time = 4220000\n    flags = 1\n    data = length 13, hash EBBFECA2\n  sample 67:\n    time = 4240000\n    flags = 1\n    data = length 13, hash C11050CF\n  sample 68:\n    time = 4260000\n    flags = 1\n    data = length 13, hash 6F738603\n  sample 69:\n    time = 4280000\n    flags = 1\n    data = length 13, hash DAD06E5\n  sample 70:\n    time = 4300000\n    flags = 1\n    data = length 13, hash 5B036C64\n  sample 71:\n    time = 4320000\n    flags = 1\n    data = length 13, hash A58DC12E\n  sample 72:\n    time = 4340000\n    flags = 1\n    data = length 13, hash AC59BA7C\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/amr/sample_nb_cbr.amr.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 4360000\n  getPosition(0) = [[timeUs=0, position=6]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/3gpp\n    maxInputSize = 61\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 8000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 13\n  sample count = 1\n  sample 0:\n    time = 4340000\n    flags = 1\n    data = length 13, hash AC59BA7C\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/amr/sample_nb_cbr.amr.unklen.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/3gpp\n    maxInputSize = 61\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 8000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 2834\n  sample count = 218\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 13, hash 371B046C\n  sample 1:\n    time = 20000\n    flags = 1\n    data = length 13, hash CE30BF5B\n  sample 2:\n    time = 40000\n    flags = 1\n    data = length 13, hash 19A59975\n  sample 3:\n    time = 60000\n    flags = 1\n    data = length 13, hash 4879773C\n  sample 4:\n    time = 80000\n    flags = 1\n    data = length 13, hash E8F83019\n  sample 5:\n    time = 100000\n    flags = 1\n    data = length 13, hash D265CDC9\n  sample 6:\n    time = 120000\n    flags = 1\n    data = length 13, hash 91653DAA\n  sample 7:\n    time = 140000\n    flags = 1\n    data = length 13, hash C79456F6\n  sample 8:\n    time = 160000\n    flags = 1\n    data = length 13, hash CDDC4422\n  sample 9:\n    time = 180000\n    flags = 1\n    data = length 13, hash D9ED3AF1\n  sample 10:\n    time = 200000\n    flags = 1\n    data = length 13, hash BAB75A33\n  sample 11:\n    time = 220000\n    flags = 1\n    data = length 13, hash 2221B4FF\n  sample 12:\n    time = 240000\n    flags = 1\n    data = length 13, hash 96400A0B\n  sample 13:\n    time = 260000\n    flags = 1\n    data = length 13, hash 582E6FB\n  sample 14:\n    time = 280000\n    flags = 1\n    data = length 13, hash C4E878E5\n  sample 15:\n    time = 300000\n    flags = 1\n    data = length 13, hash C849A1BD\n  sample 16:\n    time = 320000\n    flags = 1\n    data = length 13, hash CFA8A9ED\n  sample 17:\n    time = 340000\n    flags = 1\n    data = length 13, hash 70CA4907\n  sample 18:\n    time = 360000\n    flags = 1\n    data = length 13, hash B47D4454\n  sample 19:\n    time = 380000\n    flags = 1\n    data = length 13, hash 282998C1\n  sample 20:\n    time = 400000\n    flags = 1\n    data = length 13, hash 3F3F7A65\n  sample 21:\n    time = 420000\n    flags = 1\n    data = length 13, hash CC2EAB58\n  sample 22:\n    time = 440000\n    flags = 1\n    data = length 13, hash 279EF712\n  sample 23:\n    time = 460000\n    flags = 1\n    data = length 13, hash AA2F4B29\n  sample 24:\n    time = 480000\n    flags = 1\n    data = length 13, hash F6F658C4\n  sample 25:\n    time = 500000\n    flags = 1\n    data = length 13, hash D7DEBD17\n  sample 26:\n    time = 520000\n    flags = 1\n    data = length 13, hash 6DAB9A17\n  sample 27:\n    time = 540000\n    flags = 1\n    data = length 13, hash 6ECE1571\n  sample 28:\n    time = 560000\n    flags = 1\n    data = length 13, hash B3D0507F\n  sample 29:\n    time = 580000\n    flags = 1\n    data = length 13, hash 21E356B9\n  sample 30:\n    time = 600000\n    flags = 1\n    data = length 13, hash 410EA12\n  sample 31:\n    time = 620000\n    flags = 1\n    data = length 13, hash 533895A8\n  sample 32:\n    time = 640000\n    flags = 1\n    data = length 13, hash C61B3E5A\n  sample 33:\n    time = 660000\n    flags = 1\n    data = length 13, hash 982170E6\n  sample 34:\n    time = 680000\n    flags = 1\n    data = length 13, hash 7A0468C5\n  sample 35:\n    time = 700000\n    flags = 1\n    data = length 13, hash 9C85EAA7\n  sample 36:\n    time = 720000\n    flags = 1\n    data = length 13, hash B6B341B6\n  sample 37:\n    time = 740000\n    flags = 1\n    data = length 13, hash 6937532E\n  sample 38:\n    time = 760000\n    flags = 1\n    data = length 13, hash 8CF2A3A0\n  sample 39:\n    time = 780000\n    flags = 1\n    data = length 13, hash D2682AC6\n  sample 40:\n    time = 800000\n    flags = 1\n    data = length 13, hash BBC5710F\n  sample 41:\n    time = 820000\n    flags = 1\n    data = length 13, hash 59080B6C\n  sample 42:\n    time = 840000\n    flags = 1\n    data = length 13, hash E4118291\n  sample 43:\n    time = 860000\n    flags = 1\n    data = length 13, hash A1E5B296\n  sample 44:\n    time = 880000\n    flags = 1\n    data = length 13, hash D7B8F95B\n  sample 45:\n    time = 900000\n    flags = 1\n    data = length 13, hash CC839BE1\n  sample 46:\n    time = 920000\n    flags = 1\n    data = length 13, hash D459DFCE\n  sample 47:\n    time = 940000\n    flags = 1\n    data = length 13, hash D6AD19EC\n  sample 48:\n    time = 960000\n    flags = 1\n    data = length 13, hash D05E373D\n  sample 49:\n    time = 980000\n    flags = 1\n    data = length 13, hash 6A4460C7\n  sample 50:\n    time = 1000000\n    flags = 1\n    data = length 13, hash C9A0D93F\n  sample 51:\n    time = 1020000\n    flags = 1\n    data = length 13, hash 3FA819E7\n  sample 52:\n    time = 1040000\n    flags = 1\n    data = length 13, hash 1D3CBDFC\n  sample 53:\n    time = 1060000\n    flags = 1\n    data = length 13, hash 8BBBB403\n  sample 54:\n    time = 1080000\n    flags = 1\n    data = length 13, hash 21B4A0F9\n  sample 55:\n    time = 1100000\n    flags = 1\n    data = length 13, hash C0F921D1\n  sample 56:\n    time = 1120000\n    flags = 1\n    data = length 13, hash 5D812AAB\n  sample 57:\n    time = 1140000\n    flags = 1\n    data = length 13, hash 50C9F3F8\n  sample 58:\n    time = 1160000\n    flags = 1\n    data = length 13, hash 5C2BB5D1\n  sample 59:\n    time = 1180000\n    flags = 1\n    data = length 13, hash 6BF9BEA5\n  sample 60:\n    time = 1200000\n    flags = 1\n    data = length 13, hash 2738C1E6\n  sample 61:\n    time = 1220000\n    flags = 1\n    data = length 13, hash 5FC288A6\n  sample 62:\n    time = 1240000\n    flags = 1\n    data = length 13, hash 7E8E442A\n  sample 63:\n    time = 1260000\n    flags = 1\n    data = length 13, hash AEAA2BBA\n  sample 64:\n    time = 1280000\n    flags = 1\n    data = length 13, hash 4E2ACD2F\n  sample 65:\n    time = 1300000\n    flags = 1\n    data = length 13, hash D6C90ACF\n  sample 66:\n    time = 1320000\n    flags = 1\n    data = length 13, hash 6FD8A944\n  sample 67:\n    time = 1340000\n    flags = 1\n    data = length 13, hash A835BBF9\n  sample 68:\n    time = 1360000\n    flags = 1\n    data = length 13, hash F7713830\n  sample 69:\n    time = 1380000\n    flags = 1\n    data = length 13, hash 3AA966E5\n  sample 70:\n    time = 1400000\n    flags = 1\n    data = length 13, hash F939E829\n  sample 71:\n    time = 1420000\n    flags = 1\n    data = length 13, hash 7676DE49\n  sample 72:\n    time = 1440000\n    flags = 1\n    data = length 13, hash 93BB890A\n  sample 73:\n    time = 1460000\n    flags = 1\n    data = length 13, hash B57DBEC8\n  sample 74:\n    time = 1480000\n    flags = 1\n    data = length 13, hash 66B0A5B6\n  sample 75:\n    time = 1500000\n    flags = 1\n    data = length 13, hash D733E0D\n  sample 76:\n    time = 1520000\n    flags = 1\n    data = length 13, hash 80941726\n  sample 77:\n    time = 1540000\n    flags = 1\n    data = length 13, hash 556ED633\n  sample 78:\n    time = 1560000\n    flags = 1\n    data = length 13, hash C5EDF4E1\n  sample 79:\n    time = 1580000\n    flags = 1\n    data = length 13, hash 6B287445\n  sample 80:\n    time = 1600000\n    flags = 1\n    data = length 13, hash DC97C4A7\n  sample 81:\n    time = 1620000\n    flags = 1\n    data = length 13, hash DA8CBDF4\n  sample 82:\n    time = 1640000\n    flags = 1\n    data = length 13, hash 6F60FF77\n  sample 83:\n    time = 1660000\n    flags = 1\n    data = length 13, hash 3EB22B96\n  sample 84:\n    time = 1680000\n    flags = 1\n    data = length 13, hash B3C31AF5\n  sample 85:\n    time = 1700000\n    flags = 1\n    data = length 13, hash 1854AA92\n  sample 86:\n    time = 1720000\n    flags = 1\n    data = length 13, hash 6488264B\n  sample 87:\n    time = 1740000\n    flags = 1\n    data = length 13, hash 4CC8C5C1\n  sample 88:\n    time = 1760000\n    flags = 1\n    data = length 13, hash 19CC7523\n  sample 89:\n    time = 1780000\n    flags = 1\n    data = length 13, hash 9BE7B928\n  sample 90:\n    time = 1800000\n    flags = 1\n    data = length 13, hash 47EC7CFD\n  sample 91:\n    time = 1820000\n    flags = 1\n    data = length 13, hash EC940120\n  sample 92:\n    time = 1840000\n    flags = 1\n    data = length 13, hash 73BDA6D0\n  sample 93:\n    time = 1860000\n    flags = 1\n    data = length 13, hash FACB3314\n  sample 94:\n    time = 1880000\n    flags = 1\n    data = length 13, hash EC61D13B\n  sample 95:\n    time = 1900000\n    flags = 1\n    data = length 13, hash B28C7B6C\n  sample 96:\n    time = 1920000\n    flags = 1\n    data = length 13, hash B1A4CECD\n  sample 97:\n    time = 1940000\n    flags = 1\n    data = length 13, hash 56D41BA6\n  sample 98:\n    time = 1960000\n    flags = 1\n    data = length 13, hash 90499F4\n  sample 99:\n    time = 1980000\n    flags = 1\n    data = length 13, hash 65D9A9D3\n  sample 100:\n    time = 2000000\n    flags = 1\n    data = length 13, hash D9004CC\n  sample 101:\n    time = 2020000\n    flags = 1\n    data = length 13, hash 4139C6ED\n  sample 102:\n    time = 2040000\n    flags = 1\n    data = length 13, hash C4F8097C\n  sample 103:\n    time = 2060000\n    flags = 1\n    data = length 13, hash 94D424FA\n  sample 104:\n    time = 2080000\n    flags = 1\n    data = length 13, hash C2C6F5FD\n  sample 105:\n    time = 2100000\n    flags = 1\n    data = length 13, hash 15719008\n  sample 106:\n    time = 2120000\n    flags = 1\n    data = length 13, hash 4F64F524\n  sample 107:\n    time = 2140000\n    flags = 1\n    data = length 13, hash F9E01C1E\n  sample 108:\n    time = 2160000\n    flags = 1\n    data = length 13, hash 74C4EE74\n  sample 109:\n    time = 2180000\n    flags = 1\n    data = length 13, hash 7EE7553D\n  sample 110:\n    time = 2200000\n    flags = 1\n    data = length 13, hash 62DE6539\n  sample 111:\n    time = 2220000\n    flags = 1\n    data = length 13, hash 7F5EC222\n  sample 112:\n    time = 2240000\n    flags = 1\n    data = length 13, hash 644067F\n  sample 113:\n    time = 2260000\n    flags = 1\n    data = length 13, hash CDF6C9DC\n  sample 114:\n    time = 2280000\n    flags = 1\n    data = length 13, hash 8B5DBC80\n  sample 115:\n    time = 2300000\n    flags = 1\n    data = length 13, hash AD4BBA03\n  sample 116:\n    time = 2320000\n    flags = 1\n    data = length 13, hash 7A76340\n  sample 117:\n    time = 2340000\n    flags = 1\n    data = length 13, hash 3610F5B0\n  sample 118:\n    time = 2360000\n    flags = 1\n    data = length 13, hash 430BC60B\n  sample 119:\n    time = 2380000\n    flags = 1\n    data = length 13, hash 99CF1CA6\n  sample 120:\n    time = 2400000\n    flags = 1\n    data = length 13, hash 1331C70B\n  sample 121:\n    time = 2420000\n    flags = 1\n    data = length 13, hash BD76E69D\n  sample 122:\n    time = 2440000\n    flags = 1\n    data = length 13, hash 5DA652AC\n  sample 123:\n    time = 2460000\n    flags = 1\n    data = length 13, hash 3B7BF6CE\n  sample 124:\n    time = 2480000\n    flags = 1\n    data = length 13, hash ABBFD143\n  sample 125:\n    time = 2500000\n    flags = 1\n    data = length 13, hash E9447166\n  sample 126:\n    time = 2520000\n    flags = 1\n    data = length 13, hash EC40068C\n  sample 127:\n    time = 2540000\n    flags = 1\n    data = length 13, hash A2869400\n  sample 128:\n    time = 2560000\n    flags = 1\n    data = length 13, hash C7E0746B\n  sample 129:\n    time = 2580000\n    flags = 1\n    data = length 13, hash 60601BB1\n  sample 130:\n    time = 2600000\n    flags = 1\n    data = length 13, hash 975AAE9B\n  sample 131:\n    time = 2620000\n    flags = 1\n    data = length 13, hash 8BBC0EB2\n  sample 132:\n    time = 2640000\n    flags = 1\n    data = length 13, hash 57FB39E5\n  sample 133:\n    time = 2660000\n    flags = 1\n    data = length 13, hash 4CDCEEDB\n  sample 134:\n    time = 2680000\n    flags = 1\n    data = length 13, hash EA16E256\n  sample 135:\n    time = 2700000\n    flags = 1\n    data = length 13, hash 287E7D9E\n  sample 136:\n    time = 2720000\n    flags = 1\n    data = length 13, hash 55AB8FB9\n  sample 137:\n    time = 2740000\n    flags = 1\n    data = length 13, hash 129890EF\n  sample 138:\n    time = 2760000\n    flags = 1\n    data = length 13, hash 90834F57\n  sample 139:\n    time = 2780000\n    flags = 1\n    data = length 13, hash 5B3228E0\n  sample 140:\n    time = 2800000\n    flags = 1\n    data = length 13, hash DD19E175\n  sample 141:\n    time = 2820000\n    flags = 1\n    data = length 13, hash EE7EA342\n  sample 142:\n    time = 2840000\n    flags = 1\n    data = length 13, hash DB3AF473\n  sample 143:\n    time = 2860000\n    flags = 1\n    data = length 13, hash 25AEC43F\n  sample 144:\n    time = 2880000\n    flags = 1\n    data = length 13, hash EE9BF97F\n  sample 145:\n    time = 2900000\n    flags = 1\n    data = length 13, hash FFFBE047\n  sample 146:\n    time = 2920000\n    flags = 1\n    data = length 13, hash BEACFCB0\n  sample 147:\n    time = 2940000\n    flags = 1\n    data = length 13, hash AEB5096C\n  sample 148:\n    time = 2960000\n    flags = 1\n    data = length 13, hash B0D381B\n  sample 149:\n    time = 2980000\n    flags = 1\n    data = length 13, hash 3D9D5122\n  sample 150:\n    time = 3000000\n    flags = 1\n    data = length 13, hash 6C1DDB95\n  sample 151:\n    time = 3020000\n    flags = 1\n    data = length 13, hash ADACADCF\n  sample 152:\n    time = 3040000\n    flags = 1\n    data = length 13, hash 159E321E\n  sample 153:\n    time = 3060000\n    flags = 1\n    data = length 13, hash B1466264\n  sample 154:\n    time = 3080000\n    flags = 1\n    data = length 13, hash 4DDF7223\n  sample 155:\n    time = 3100000\n    flags = 1\n    data = length 13, hash C9BDB82A\n  sample 156:\n    time = 3120000\n    flags = 1\n    data = length 13, hash A49B2D9D\n  sample 157:\n    time = 3140000\n    flags = 1\n    data = length 13, hash D645E7E5\n  sample 158:\n    time = 3160000\n    flags = 1\n    data = length 13, hash 1C4232DC\n  sample 159:\n    time = 3180000\n    flags = 1\n    data = length 13, hash 83078219\n  sample 160:\n    time = 3200000\n    flags = 1\n    data = length 13, hash D6D8B072\n  sample 161:\n    time = 3220000\n    flags = 1\n    data = length 13, hash 975DB40\n  sample 162:\n    time = 3240000\n    flags = 1\n    data = length 13, hash A15FDD05\n  sample 163:\n    time = 3260000\n    flags = 1\n    data = length 13, hash 4B839E41\n  sample 164:\n    time = 3280000\n    flags = 1\n    data = length 13, hash 7418F499\n  sample 165:\n    time = 3300000\n    flags = 1\n    data = length 13, hash 7A4945E4\n  sample 166:\n    time = 3320000\n    flags = 1\n    data = length 13, hash 6249558C\n  sample 167:\n    time = 3340000\n    flags = 1\n    data = length 13, hash BD4C5BE3\n  sample 168:\n    time = 3360000\n    flags = 1\n    data = length 13, hash BAB30F1D\n  sample 169:\n    time = 3380000\n    flags = 1\n    data = length 13, hash 1E1C7012\n  sample 170:\n    time = 3400000\n    flags = 1\n    data = length 13, hash 9A3F8A89\n  sample 171:\n    time = 3420000\n    flags = 1\n    data = length 13, hash 20BE6D7B\n  sample 172:\n    time = 3440000\n    flags = 1\n    data = length 13, hash CAA0591D\n  sample 173:\n    time = 3460000\n    flags = 1\n    data = length 13, hash 6D554D17\n  sample 174:\n    time = 3480000\n    flags = 1\n    data = length 13, hash D97C3B31\n  sample 175:\n    time = 3500000\n    flags = 1\n    data = length 13, hash 75BC5C3\n  sample 176:\n    time = 3520000\n    flags = 1\n    data = length 13, hash 7BA1784B\n  sample 177:\n    time = 3540000\n    flags = 1\n    data = length 13, hash 1D175D92\n  sample 178:\n    time = 3560000\n    flags = 1\n    data = length 13, hash ADCA60FD\n  sample 179:\n    time = 3580000\n    flags = 1\n    data = length 13, hash 37018693\n  sample 180:\n    time = 3600000\n    flags = 1\n    data = length 13, hash 4553606F\n  sample 181:\n    time = 3620000\n    flags = 1\n    data = length 13, hash CF434565\n  sample 182:\n    time = 3640000\n    flags = 1\n    data = length 13, hash D264D757\n  sample 183:\n    time = 3660000\n    flags = 1\n    data = length 13, hash 4FB493EF\n  sample 184:\n    time = 3680000\n    flags = 1\n    data = length 13, hash 919F53A\n  sample 185:\n    time = 3700000\n    flags = 1\n    data = length 13, hash C22B009B\n  sample 186:\n    time = 3720000\n    flags = 1\n    data = length 13, hash 5981470\n  sample 187:\n    time = 3740000\n    flags = 1\n    data = length 13, hash A5D3937C\n  sample 188:\n    time = 3760000\n    flags = 1\n    data = length 13, hash A2504429\n  sample 189:\n    time = 3780000\n    flags = 1\n    data = length 13, hash AD1B70BE\n  sample 190:\n    time = 3800000\n    flags = 1\n    data = length 13, hash 2E39ED5E\n  sample 191:\n    time = 3820000\n    flags = 1\n    data = length 13, hash 13A8BE8E\n  sample 192:\n    time = 3840000\n    flags = 1\n    data = length 13, hash 1ACD740B\n  sample 193:\n    time = 3860000\n    flags = 1\n    data = length 13, hash 80F38B3\n  sample 194:\n    time = 3880000\n    flags = 1\n    data = length 13, hash DA9DA79F\n  sample 195:\n    time = 3900000\n    flags = 1\n    data = length 13, hash 21B95B7E\n  sample 196:\n    time = 3920000\n    flags = 1\n    data = length 13, hash CD22497B\n  sample 197:\n    time = 3940000\n    flags = 1\n    data = length 13, hash 718BB35D\n  sample 198:\n    time = 3960000\n    flags = 1\n    data = length 13, hash 69ABA6AD\n  sample 199:\n    time = 3980000\n    flags = 1\n    data = length 13, hash BAE19549\n  sample 200:\n    time = 4000000\n    flags = 1\n    data = length 13, hash 2A792FB3\n  sample 201:\n    time = 4020000\n    flags = 1\n    data = length 13, hash 71FCD8\n  sample 202:\n    time = 4040000\n    flags = 1\n    data = length 13, hash 44D2B5B3\n  sample 203:\n    time = 4060000\n    flags = 1\n    data = length 13, hash 1E87B11B\n  sample 204:\n    time = 4080000\n    flags = 1\n    data = length 13, hash 78CD2C11\n  sample 205:\n    time = 4100000\n    flags = 1\n    data = length 13, hash 9F198DF0\n  sample 206:\n    time = 4120000\n    flags = 1\n    data = length 13, hash B291F16A\n  sample 207:\n    time = 4140000\n    flags = 1\n    data = length 13, hash CF820EE0\n  sample 208:\n    time = 4160000\n    flags = 1\n    data = length 13, hash 4E24F683\n  sample 209:\n    time = 4180000\n    flags = 1\n    data = length 13, hash 52BCD68F\n  sample 210:\n    time = 4200000\n    flags = 1\n    data = length 13, hash 42588CB0\n  sample 211:\n    time = 4220000\n    flags = 1\n    data = length 13, hash EBBFECA2\n  sample 212:\n    time = 4240000\n    flags = 1\n    data = length 13, hash C11050CF\n  sample 213:\n    time = 4260000\n    flags = 1\n    data = length 13, hash 6F738603\n  sample 214:\n    time = 4280000\n    flags = 1\n    data = length 13, hash DAD06E5\n  sample 215:\n    time = 4300000\n    flags = 1\n    data = length 13, hash 5B036C64\n  sample 216:\n    time = 4320000\n    flags = 1\n    data = length 13, hash A58DC12E\n  sample 217:\n    time = 4340000\n    flags = 1\n    data = length 13, hash AC59BA7C\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/amr/sample_wb.amr.0.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/amr-wb\n    maxInputSize = 61\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 16000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 4056\n  sample count = 169\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 24, hash C3025798\n  sample 1:\n    time = 20000\n    flags = 1\n    data = length 24, hash 39CABAE9\n  sample 2:\n    time = 40000\n    flags = 1\n    data = length 24, hash 2752F470\n  sample 3:\n    time = 60000\n    flags = 1\n    data = length 24, hash 394F76F6\n  sample 4:\n    time = 80000\n    flags = 1\n    data = length 24, hash FF9EEF\n  sample 5:\n    time = 100000\n    flags = 1\n    data = length 24, hash 54ECB1B4\n  sample 6:\n    time = 120000\n    flags = 1\n    data = length 24, hash 6D7A3A5F\n  sample 7:\n    time = 140000\n    flags = 1\n    data = length 24, hash 684CD144\n  sample 8:\n    time = 160000\n    flags = 1\n    data = length 24, hash 87B7D176\n  sample 9:\n    time = 180000\n    flags = 1\n    data = length 24, hash 4C02F9A5\n  sample 10:\n    time = 200000\n    flags = 1\n    data = length 24, hash B4154108\n  sample 11:\n    time = 220000\n    flags = 1\n    data = length 24, hash 4448F477\n  sample 12:\n    time = 240000\n    flags = 1\n    data = length 24, hash 755A4939\n  sample 13:\n    time = 260000\n    flags = 1\n    data = length 24, hash 8C8BC6C3\n  sample 14:\n    time = 280000\n    flags = 1\n    data = length 24, hash BC37F63F\n  sample 15:\n    time = 300000\n    flags = 1\n    data = length 24, hash 3352C43C\n  sample 16:\n    time = 320000\n    flags = 1\n    data = length 24, hash 7998E1F2\n  sample 17:\n    time = 340000\n    flags = 1\n    data = length 24, hash A8ECBEFC\n  sample 18:\n    time = 360000\n    flags = 1\n    data = length 24, hash 944AC118\n  sample 19:\n    time = 380000\n    flags = 1\n    data = length 24, hash FD2C8E1F\n  sample 20:\n    time = 400000\n    flags = 1\n    data = length 24, hash B3D867AF\n  sample 21:\n    time = 420000\n    flags = 1\n    data = length 24, hash 3DC6E592\n  sample 22:\n    time = 440000\n    flags = 1\n    data = length 24, hash 32B276CD\n  sample 23:\n    time = 460000\n    flags = 1\n    data = length 24, hash 5488AEF3\n  sample 24:\n    time = 480000\n    flags = 1\n    data = length 24, hash 7A4D516\n  sample 25:\n    time = 500000\n    flags = 1\n    data = length 24, hash 570AE83F\n  sample 26:\n    time = 520000\n    flags = 1\n    data = length 24, hash E5CB3477\n  sample 27:\n    time = 540000\n    flags = 1\n    data = length 24, hash E04C00E4\n  sample 28:\n    time = 560000\n    flags = 1\n    data = length 24, hash 21B7C97\n  sample 29:\n    time = 580000\n    flags = 1\n    data = length 24, hash 1633F470\n  sample 30:\n    time = 600000\n    flags = 1\n    data = length 24, hash 28D65CA6\n  sample 31:\n    time = 620000\n    flags = 1\n    data = length 24, hash CC6A675C\n  sample 32:\n    time = 640000\n    flags = 1\n    data = length 24, hash 4C91080A\n  sample 33:\n    time = 660000\n    flags = 1\n    data = length 24, hash F6482FB5\n  sample 34:\n    time = 680000\n    flags = 1\n    data = length 24, hash 2C76F48C\n  sample 35:\n    time = 700000\n    flags = 1\n    data = length 24, hash 6E3B0D72\n  sample 36:\n    time = 720000\n    flags = 1\n    data = length 24, hash 799AA003\n  sample 37:\n    time = 740000\n    flags = 1\n    data = length 24, hash DFC0BA81\n  sample 38:\n    time = 760000\n    flags = 1\n    data = length 24, hash CBDF3826\n  sample 39:\n    time = 780000\n    flags = 1\n    data = length 24, hash 16862B75\n  sample 40:\n    time = 800000\n    flags = 1\n    data = length 24, hash 865A828E\n  sample 41:\n    time = 820000\n    flags = 1\n    data = length 24, hash 336BBDC9\n  sample 42:\n    time = 840000\n    flags = 1\n    data = length 24, hash 6CFC6C34\n  sample 43:\n    time = 860000\n    flags = 1\n    data = length 24, hash 32C8CD46\n  sample 44:\n    time = 880000\n    flags = 1\n    data = length 24, hash 9FE11C4C\n  sample 45:\n    time = 900000\n    flags = 1\n    data = length 24, hash AA5A12B7\n  sample 46:\n    time = 920000\n    flags = 1\n    data = length 24, hash AA0F4A4D\n  sample 47:\n    time = 940000\n    flags = 1\n    data = length 24, hash 34415484\n  sample 48:\n    time = 960000\n    flags = 1\n    data = length 24, hash 5018928E\n  sample 49:\n    time = 980000\n    flags = 1\n    data = length 24, hash 4A04D162\n  sample 50:\n    time = 1000000\n    flags = 1\n    data = length 24, hash 4C70F9F0\n  sample 51:\n    time = 1020000\n    flags = 1\n    data = length 24, hash 99EF3168\n  sample 52:\n    time = 1040000\n    flags = 1\n    data = length 24, hash C600DAF\n  sample 53:\n    time = 1060000\n    flags = 1\n    data = length 24, hash FDBB192E\n  sample 54:\n    time = 1080000\n    flags = 1\n    data = length 24, hash 99096A48\n  sample 55:\n    time = 1100000\n    flags = 1\n    data = length 24, hash D793F88B\n  sample 56:\n    time = 1120000\n    flags = 1\n    data = length 24, hash EEB921BD\n  sample 57:\n    time = 1140000\n    flags = 1\n    data = length 24, hash 8B941A4C\n  sample 58:\n    time = 1160000\n    flags = 1\n    data = length 24, hash ED5F5FEE\n  sample 59:\n    time = 1180000\n    flags = 1\n    data = length 24, hash A588E0BB\n  sample 60:\n    time = 1200000\n    flags = 1\n    data = length 24, hash 588CBC01\n  sample 61:\n    time = 1220000\n    flags = 1\n    data = length 24, hash DE22266C\n  sample 62:\n    time = 1240000\n    flags = 1\n    data = length 24, hash 921B6E5C\n  sample 63:\n    time = 1260000\n    flags = 1\n    data = length 24, hash EC11F041\n  sample 64:\n    time = 1280000\n    flags = 1\n    data = length 24, hash 5BA9E0A3\n  sample 65:\n    time = 1300000\n    flags = 1\n    data = length 24, hash DB6D52F3\n  sample 66:\n    time = 1320000\n    flags = 1\n    data = length 24, hash 8EEBE525\n  sample 67:\n    time = 1340000\n    flags = 1\n    data = length 24, hash 47A742AE\n  sample 68:\n    time = 1360000\n    flags = 1\n    data = length 24, hash E93F1E03\n  sample 69:\n    time = 1380000\n    flags = 1\n    data = length 24, hash 3251F57C\n  sample 70:\n    time = 1400000\n    flags = 1\n    data = length 24, hash 3EDBBBDD\n  sample 71:\n    time = 1420000\n    flags = 1\n    data = length 24, hash 2E98465A\n  sample 72:\n    time = 1440000\n    flags = 1\n    data = length 24, hash A09EA52E\n  sample 73:\n    time = 1460000\n    flags = 1\n    data = length 24, hash A2A86FA6\n  sample 74:\n    time = 1480000\n    flags = 1\n    data = length 24, hash 71DCD51C\n  sample 75:\n    time = 1500000\n    flags = 1\n    data = length 24, hash 2B02DEE1\n  sample 76:\n    time = 1520000\n    flags = 1\n    data = length 24, hash 7A725192\n  sample 77:\n    time = 1540000\n    flags = 1\n    data = length 24, hash 929AD483\n  sample 78:\n    time = 1560000\n    flags = 1\n    data = length 24, hash 68440BF5\n  sample 79:\n    time = 1580000\n    flags = 1\n    data = length 24, hash 5BD41AD6\n  sample 80:\n    time = 1600000\n    flags = 1\n    data = length 24, hash 91A381\n  sample 81:\n    time = 1620000\n    flags = 1\n    data = length 24, hash 8010C408\n  sample 82:\n    time = 1640000\n    flags = 1\n    data = length 24, hash 482274BE\n  sample 83:\n    time = 1660000\n    flags = 1\n    data = length 24, hash D7DB8BCC\n  sample 84:\n    time = 1680000\n    flags = 1\n    data = length 24, hash 680BD9DD\n  sample 85:\n    time = 1700000\n    flags = 1\n    data = length 24, hash E313577C\n  sample 86:\n    time = 1720000\n    flags = 1\n    data = length 24, hash 9C10B0CD\n  sample 87:\n    time = 1740000\n    flags = 1\n    data = length 24, hash 2D90AC02\n  sample 88:\n    time = 1760000\n    flags = 1\n    data = length 24, hash 64E8C245\n  sample 89:\n    time = 1780000\n    flags = 1\n    data = length 24, hash 3954AC1B\n  sample 90:\n    time = 1800000\n    flags = 1\n    data = length 24, hash ACB8999F\n  sample 91:\n    time = 1820000\n    flags = 1\n    data = length 24, hash 43AE3957\n  sample 92:\n    time = 1840000\n    flags = 1\n    data = length 24, hash 3C664DB7\n  sample 93:\n    time = 1860000\n    flags = 1\n    data = length 24, hash 9354B576\n  sample 94:\n    time = 1880000\n    flags = 1\n    data = length 24, hash B5B9C14E\n  sample 95:\n    time = 1900000\n    flags = 1\n    data = length 24, hash 7DA9C98F\n  sample 96:\n    time = 1920000\n    flags = 1\n    data = length 24, hash EFEE54C6\n  sample 97:\n    time = 1940000\n    flags = 1\n    data = length 24, hash 79DC8CBD\n  sample 98:\n    time = 1960000\n    flags = 1\n    data = length 24, hash A71A475C\n  sample 99:\n    time = 1980000\n    flags = 1\n    data = length 24, hash CA1CBB94\n  sample 100:\n    time = 2000000\n    flags = 1\n    data = length 24, hash 91922226\n  sample 101:\n    time = 2020000\n    flags = 1\n    data = length 24, hash C90278BC\n  sample 102:\n    time = 2040000\n    flags = 1\n    data = length 24, hash BD51986F\n  sample 103:\n    time = 2060000\n    flags = 1\n    data = length 24, hash 90AEF368\n  sample 104:\n    time = 2080000\n    flags = 1\n    data = length 24, hash 1D83C955\n  sample 105:\n    time = 2100000\n    flags = 1\n    data = length 24, hash 8FA9A915\n  sample 106:\n    time = 2120000\n    flags = 1\n    data = length 24, hash C6C753E0\n  sample 107:\n    time = 2140000\n    flags = 1\n    data = length 24, hash 85FA27A7\n  sample 108:\n    time = 2160000\n    flags = 1\n    data = length 24, hash A0277324\n  sample 109:\n    time = 2180000\n    flags = 1\n    data = length 24, hash B7696535\n  sample 110:\n    time = 2200000\n    flags = 1\n    data = length 24, hash D69D668C\n  sample 111:\n    time = 2220000\n    flags = 1\n    data = length 24, hash 34C057CD\n  sample 112:\n    time = 2240000\n    flags = 1\n    data = length 24, hash 4EC5E974\n  sample 113:\n    time = 2260000\n    flags = 1\n    data = length 24, hash 1C1CD40D\n  sample 114:\n    time = 2280000\n    flags = 1\n    data = length 24, hash 76CC54BC\n  sample 115:\n    time = 2300000\n    flags = 1\n    data = length 24, hash D497ACF5\n  sample 116:\n    time = 2320000\n    flags = 1\n    data = length 24, hash A1386080\n  sample 117:\n    time = 2340000\n    flags = 1\n    data = length 24, hash 7ED36954\n  sample 118:\n    time = 2360000\n    flags = 1\n    data = length 24, hash C11A3BF9\n  sample 119:\n    time = 2380000\n    flags = 1\n    data = length 24, hash 8FB69488\n  sample 120:\n    time = 2400000\n    flags = 1\n    data = length 24, hash C6225F59\n  sample 121:\n    time = 2420000\n    flags = 1\n    data = length 24, hash 122AB6D2\n  sample 122:\n    time = 2440000\n    flags = 1\n    data = length 24, hash 1E195E7D\n  sample 123:\n    time = 2460000\n    flags = 1\n    data = length 24, hash BD3DF418\n  sample 124:\n    time = 2480000\n    flags = 1\n    data = length 24, hash D8AE4A5\n  sample 125:\n    time = 2500000\n    flags = 1\n    data = length 24, hash 977BD182\n  sample 126:\n    time = 2520000\n    flags = 1\n    data = length 24, hash F361F060\n  sample 127:\n    time = 2540000\n    flags = 1\n    data = length 24, hash 11EC8CD0\n  sample 128:\n    time = 2560000\n    flags = 1\n    data = length 24, hash 3798F3D2\n  sample 129:\n    time = 2580000\n    flags = 1\n    data = length 24, hash B2C2517C\n  sample 130:\n    time = 2600000\n    flags = 1\n    data = length 24, hash FBE0D0D8\n  sample 131:\n    time = 2620000\n    flags = 1\n    data = length 24, hash 7033172F\n  sample 132:\n    time = 2640000\n    flags = 1\n    data = length 24, hash BE760029\n  sample 133:\n    time = 2660000\n    flags = 1\n    data = length 24, hash 590AF28C\n  sample 134:\n    time = 2680000\n    flags = 1\n    data = length 24, hash AD28C48F\n  sample 135:\n    time = 2700000\n    flags = 1\n    data = length 24, hash 640AA61B\n  sample 136:\n    time = 2720000\n    flags = 1\n    data = length 24, hash ABE659B\n  sample 137:\n    time = 2740000\n    flags = 1\n    data = length 24, hash ED2691D2\n  sample 138:\n    time = 2760000\n    flags = 1\n    data = length 24, hash D998C80E\n  sample 139:\n    time = 2780000\n    flags = 1\n    data = length 24, hash 8DC0DF5C\n  sample 140:\n    time = 2800000\n    flags = 1\n    data = length 24, hash 7692247B\n  sample 141:\n    time = 2820000\n    flags = 1\n    data = length 24, hash C1D1CCB9\n  sample 142:\n    time = 2840000\n    flags = 1\n    data = length 24, hash 362CE78E\n  sample 143:\n    time = 2860000\n    flags = 1\n    data = length 24, hash 54FA84A\n  sample 144:\n    time = 2880000\n    flags = 1\n    data = length 24, hash 29E88C84\n  sample 145:\n    time = 2900000\n    flags = 1\n    data = length 24, hash 1CD848AC\n  sample 146:\n    time = 2920000\n    flags = 1\n    data = length 24, hash 5C3D4A79\n  sample 147:\n    time = 2940000\n    flags = 1\n    data = length 24, hash 1AA8E604\n  sample 148:\n    time = 2960000\n    flags = 1\n    data = length 24, hash 186A4316\n  sample 149:\n    time = 2980000\n    flags = 1\n    data = length 24, hash 61ACE481\n  sample 150:\n    time = 3000000\n    flags = 1\n    data = length 24, hash D0C42780\n  sample 151:\n    time = 3020000\n    flags = 1\n    data = length 24, hash FAD51BA1\n  sample 152:\n    time = 3040000\n    flags = 1\n    data = length 24, hash F1A9AC71\n  sample 153:\n    time = 3060000\n    flags = 1\n    data = length 24, hash 24425449\n  sample 154:\n    time = 3080000\n    flags = 1\n    data = length 24, hash 37AAC3E6\n  sample 155:\n    time = 3100000\n    flags = 1\n    data = length 24, hash 91F68CB4\n  sample 156:\n    time = 3120000\n    flags = 1\n    data = length 24, hash F8C92820\n  sample 157:\n    time = 3140000\n    flags = 1\n    data = length 24, hash ECD39C3E\n  sample 158:\n    time = 3160000\n    flags = 1\n    data = length 24, hash B27D8F78\n  sample 159:\n    time = 3180000\n    flags = 1\n    data = length 24, hash C9EB3DFB\n  sample 160:\n    time = 3200000\n    flags = 1\n    data = length 24, hash 88DC54A2\n  sample 161:\n    time = 3220000\n    flags = 1\n    data = length 24, hash 7FC4C5BE\n  sample 162:\n    time = 3240000\n    flags = 1\n    data = length 24, hash E4F684EF\n  sample 163:\n    time = 3260000\n    flags = 1\n    data = length 24, hash 55C08B56\n  sample 164:\n    time = 3280000\n    flags = 1\n    data = length 24, hash E5A0F006\n  sample 165:\n    time = 3300000\n    flags = 1\n    data = length 24, hash DE3F3AA7\n  sample 166:\n    time = 3320000\n    flags = 1\n    data = length 24, hash 3F28AE7F\n  sample 167:\n    time = 3340000\n    flags = 1\n    data = length 24, hash 3949CAFF\n  sample 168:\n    time = 3360000\n    flags = 1\n    data = length 24, hash 772665A0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/amr/sample_wb_cbr.amr.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 3380000\n  getPosition(0) = [[timeUs=0, position=9]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/amr-wb\n    maxInputSize = 61\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 16000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 4056\n  sample count = 169\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 24, hash C3025798\n  sample 1:\n    time = 20000\n    flags = 1\n    data = length 24, hash 39CABAE9\n  sample 2:\n    time = 40000\n    flags = 1\n    data = length 24, hash 2752F470\n  sample 3:\n    time = 60000\n    flags = 1\n    data = length 24, hash 394F76F6\n  sample 4:\n    time = 80000\n    flags = 1\n    data = length 24, hash FF9EEF\n  sample 5:\n    time = 100000\n    flags = 1\n    data = length 24, hash 54ECB1B4\n  sample 6:\n    time = 120000\n    flags = 1\n    data = length 24, hash 6D7A3A5F\n  sample 7:\n    time = 140000\n    flags = 1\n    data = length 24, hash 684CD144\n  sample 8:\n    time = 160000\n    flags = 1\n    data = length 24, hash 87B7D176\n  sample 9:\n    time = 180000\n    flags = 1\n    data = length 24, hash 4C02F9A5\n  sample 10:\n    time = 200000\n    flags = 1\n    data = length 24, hash B4154108\n  sample 11:\n    time = 220000\n    flags = 1\n    data = length 24, hash 4448F477\n  sample 12:\n    time = 240000\n    flags = 1\n    data = length 24, hash 755A4939\n  sample 13:\n    time = 260000\n    flags = 1\n    data = length 24, hash 8C8BC6C3\n  sample 14:\n    time = 280000\n    flags = 1\n    data = length 24, hash BC37F63F\n  sample 15:\n    time = 300000\n    flags = 1\n    data = length 24, hash 3352C43C\n  sample 16:\n    time = 320000\n    flags = 1\n    data = length 24, hash 7998E1F2\n  sample 17:\n    time = 340000\n    flags = 1\n    data = length 24, hash A8ECBEFC\n  sample 18:\n    time = 360000\n    flags = 1\n    data = length 24, hash 944AC118\n  sample 19:\n    time = 380000\n    flags = 1\n    data = length 24, hash FD2C8E1F\n  sample 20:\n    time = 400000\n    flags = 1\n    data = length 24, hash B3D867AF\n  sample 21:\n    time = 420000\n    flags = 1\n    data = length 24, hash 3DC6E592\n  sample 22:\n    time = 440000\n    flags = 1\n    data = length 24, hash 32B276CD\n  sample 23:\n    time = 460000\n    flags = 1\n    data = length 24, hash 5488AEF3\n  sample 24:\n    time = 480000\n    flags = 1\n    data = length 24, hash 7A4D516\n  sample 25:\n    time = 500000\n    flags = 1\n    data = length 24, hash 570AE83F\n  sample 26:\n    time = 520000\n    flags = 1\n    data = length 24, hash E5CB3477\n  sample 27:\n    time = 540000\n    flags = 1\n    data = length 24, hash E04C00E4\n  sample 28:\n    time = 560000\n    flags = 1\n    data = length 24, hash 21B7C97\n  sample 29:\n    time = 580000\n    flags = 1\n    data = length 24, hash 1633F470\n  sample 30:\n    time = 600000\n    flags = 1\n    data = length 24, hash 28D65CA6\n  sample 31:\n    time = 620000\n    flags = 1\n    data = length 24, hash CC6A675C\n  sample 32:\n    time = 640000\n    flags = 1\n    data = length 24, hash 4C91080A\n  sample 33:\n    time = 660000\n    flags = 1\n    data = length 24, hash F6482FB5\n  sample 34:\n    time = 680000\n    flags = 1\n    data = length 24, hash 2C76F48C\n  sample 35:\n    time = 700000\n    flags = 1\n    data = length 24, hash 6E3B0D72\n  sample 36:\n    time = 720000\n    flags = 1\n    data = length 24, hash 799AA003\n  sample 37:\n    time = 740000\n    flags = 1\n    data = length 24, hash DFC0BA81\n  sample 38:\n    time = 760000\n    flags = 1\n    data = length 24, hash CBDF3826\n  sample 39:\n    time = 780000\n    flags = 1\n    data = length 24, hash 16862B75\n  sample 40:\n    time = 800000\n    flags = 1\n    data = length 24, hash 865A828E\n  sample 41:\n    time = 820000\n    flags = 1\n    data = length 24, hash 336BBDC9\n  sample 42:\n    time = 840000\n    flags = 1\n    data = length 24, hash 6CFC6C34\n  sample 43:\n    time = 860000\n    flags = 1\n    data = length 24, hash 32C8CD46\n  sample 44:\n    time = 880000\n    flags = 1\n    data = length 24, hash 9FE11C4C\n  sample 45:\n    time = 900000\n    flags = 1\n    data = length 24, hash AA5A12B7\n  sample 46:\n    time = 920000\n    flags = 1\n    data = length 24, hash AA0F4A4D\n  sample 47:\n    time = 940000\n    flags = 1\n    data = length 24, hash 34415484\n  sample 48:\n    time = 960000\n    flags = 1\n    data = length 24, hash 5018928E\n  sample 49:\n    time = 980000\n    flags = 1\n    data = length 24, hash 4A04D162\n  sample 50:\n    time = 1000000\n    flags = 1\n    data = length 24, hash 4C70F9F0\n  sample 51:\n    time = 1020000\n    flags = 1\n    data = length 24, hash 99EF3168\n  sample 52:\n    time = 1040000\n    flags = 1\n    data = length 24, hash C600DAF\n  sample 53:\n    time = 1060000\n    flags = 1\n    data = length 24, hash FDBB192E\n  sample 54:\n    time = 1080000\n    flags = 1\n    data = length 24, hash 99096A48\n  sample 55:\n    time = 1100000\n    flags = 1\n    data = length 24, hash D793F88B\n  sample 56:\n    time = 1120000\n    flags = 1\n    data = length 24, hash EEB921BD\n  sample 57:\n    time = 1140000\n    flags = 1\n    data = length 24, hash 8B941A4C\n  sample 58:\n    time = 1160000\n    flags = 1\n    data = length 24, hash ED5F5FEE\n  sample 59:\n    time = 1180000\n    flags = 1\n    data = length 24, hash A588E0BB\n  sample 60:\n    time = 1200000\n    flags = 1\n    data = length 24, hash 588CBC01\n  sample 61:\n    time = 1220000\n    flags = 1\n    data = length 24, hash DE22266C\n  sample 62:\n    time = 1240000\n    flags = 1\n    data = length 24, hash 921B6E5C\n  sample 63:\n    time = 1260000\n    flags = 1\n    data = length 24, hash EC11F041\n  sample 64:\n    time = 1280000\n    flags = 1\n    data = length 24, hash 5BA9E0A3\n  sample 65:\n    time = 1300000\n    flags = 1\n    data = length 24, hash DB6D52F3\n  sample 66:\n    time = 1320000\n    flags = 1\n    data = length 24, hash 8EEBE525\n  sample 67:\n    time = 1340000\n    flags = 1\n    data = length 24, hash 47A742AE\n  sample 68:\n    time = 1360000\n    flags = 1\n    data = length 24, hash E93F1E03\n  sample 69:\n    time = 1380000\n    flags = 1\n    data = length 24, hash 3251F57C\n  sample 70:\n    time = 1400000\n    flags = 1\n    data = length 24, hash 3EDBBBDD\n  sample 71:\n    time = 1420000\n    flags = 1\n    data = length 24, hash 2E98465A\n  sample 72:\n    time = 1440000\n    flags = 1\n    data = length 24, hash A09EA52E\n  sample 73:\n    time = 1460000\n    flags = 1\n    data = length 24, hash A2A86FA6\n  sample 74:\n    time = 1480000\n    flags = 1\n    data = length 24, hash 71DCD51C\n  sample 75:\n    time = 1500000\n    flags = 1\n    data = length 24, hash 2B02DEE1\n  sample 76:\n    time = 1520000\n    flags = 1\n    data = length 24, hash 7A725192\n  sample 77:\n    time = 1540000\n    flags = 1\n    data = length 24, hash 929AD483\n  sample 78:\n    time = 1560000\n    flags = 1\n    data = length 24, hash 68440BF5\n  sample 79:\n    time = 1580000\n    flags = 1\n    data = length 24, hash 5BD41AD6\n  sample 80:\n    time = 1600000\n    flags = 1\n    data = length 24, hash 91A381\n  sample 81:\n    time = 1620000\n    flags = 1\n    data = length 24, hash 8010C408\n  sample 82:\n    time = 1640000\n    flags = 1\n    data = length 24, hash 482274BE\n  sample 83:\n    time = 1660000\n    flags = 1\n    data = length 24, hash D7DB8BCC\n  sample 84:\n    time = 1680000\n    flags = 1\n    data = length 24, hash 680BD9DD\n  sample 85:\n    time = 1700000\n    flags = 1\n    data = length 24, hash E313577C\n  sample 86:\n    time = 1720000\n    flags = 1\n    data = length 24, hash 9C10B0CD\n  sample 87:\n    time = 1740000\n    flags = 1\n    data = length 24, hash 2D90AC02\n  sample 88:\n    time = 1760000\n    flags = 1\n    data = length 24, hash 64E8C245\n  sample 89:\n    time = 1780000\n    flags = 1\n    data = length 24, hash 3954AC1B\n  sample 90:\n    time = 1800000\n    flags = 1\n    data = length 24, hash ACB8999F\n  sample 91:\n    time = 1820000\n    flags = 1\n    data = length 24, hash 43AE3957\n  sample 92:\n    time = 1840000\n    flags = 1\n    data = length 24, hash 3C664DB7\n  sample 93:\n    time = 1860000\n    flags = 1\n    data = length 24, hash 9354B576\n  sample 94:\n    time = 1880000\n    flags = 1\n    data = length 24, hash B5B9C14E\n  sample 95:\n    time = 1900000\n    flags = 1\n    data = length 24, hash 7DA9C98F\n  sample 96:\n    time = 1920000\n    flags = 1\n    data = length 24, hash EFEE54C6\n  sample 97:\n    time = 1940000\n    flags = 1\n    data = length 24, hash 79DC8CBD\n  sample 98:\n    time = 1960000\n    flags = 1\n    data = length 24, hash A71A475C\n  sample 99:\n    time = 1980000\n    flags = 1\n    data = length 24, hash CA1CBB94\n  sample 100:\n    time = 2000000\n    flags = 1\n    data = length 24, hash 91922226\n  sample 101:\n    time = 2020000\n    flags = 1\n    data = length 24, hash C90278BC\n  sample 102:\n    time = 2040000\n    flags = 1\n    data = length 24, hash BD51986F\n  sample 103:\n    time = 2060000\n    flags = 1\n    data = length 24, hash 90AEF368\n  sample 104:\n    time = 2080000\n    flags = 1\n    data = length 24, hash 1D83C955\n  sample 105:\n    time = 2100000\n    flags = 1\n    data = length 24, hash 8FA9A915\n  sample 106:\n    time = 2120000\n    flags = 1\n    data = length 24, hash C6C753E0\n  sample 107:\n    time = 2140000\n    flags = 1\n    data = length 24, hash 85FA27A7\n  sample 108:\n    time = 2160000\n    flags = 1\n    data = length 24, hash A0277324\n  sample 109:\n    time = 2180000\n    flags = 1\n    data = length 24, hash B7696535\n  sample 110:\n    time = 2200000\n    flags = 1\n    data = length 24, hash D69D668C\n  sample 111:\n    time = 2220000\n    flags = 1\n    data = length 24, hash 34C057CD\n  sample 112:\n    time = 2240000\n    flags = 1\n    data = length 24, hash 4EC5E974\n  sample 113:\n    time = 2260000\n    flags = 1\n    data = length 24, hash 1C1CD40D\n  sample 114:\n    time = 2280000\n    flags = 1\n    data = length 24, hash 76CC54BC\n  sample 115:\n    time = 2300000\n    flags = 1\n    data = length 24, hash D497ACF5\n  sample 116:\n    time = 2320000\n    flags = 1\n    data = length 24, hash A1386080\n  sample 117:\n    time = 2340000\n    flags = 1\n    data = length 24, hash 7ED36954\n  sample 118:\n    time = 2360000\n    flags = 1\n    data = length 24, hash C11A3BF9\n  sample 119:\n    time = 2380000\n    flags = 1\n    data = length 24, hash 8FB69488\n  sample 120:\n    time = 2400000\n    flags = 1\n    data = length 24, hash C6225F59\n  sample 121:\n    time = 2420000\n    flags = 1\n    data = length 24, hash 122AB6D2\n  sample 122:\n    time = 2440000\n    flags = 1\n    data = length 24, hash 1E195E7D\n  sample 123:\n    time = 2460000\n    flags = 1\n    data = length 24, hash BD3DF418\n  sample 124:\n    time = 2480000\n    flags = 1\n    data = length 24, hash D8AE4A5\n  sample 125:\n    time = 2500000\n    flags = 1\n    data = length 24, hash 977BD182\n  sample 126:\n    time = 2520000\n    flags = 1\n    data = length 24, hash F361F060\n  sample 127:\n    time = 2540000\n    flags = 1\n    data = length 24, hash 11EC8CD0\n  sample 128:\n    time = 2560000\n    flags = 1\n    data = length 24, hash 3798F3D2\n  sample 129:\n    time = 2580000\n    flags = 1\n    data = length 24, hash B2C2517C\n  sample 130:\n    time = 2600000\n    flags = 1\n    data = length 24, hash FBE0D0D8\n  sample 131:\n    time = 2620000\n    flags = 1\n    data = length 24, hash 7033172F\n  sample 132:\n    time = 2640000\n    flags = 1\n    data = length 24, hash BE760029\n  sample 133:\n    time = 2660000\n    flags = 1\n    data = length 24, hash 590AF28C\n  sample 134:\n    time = 2680000\n    flags = 1\n    data = length 24, hash AD28C48F\n  sample 135:\n    time = 2700000\n    flags = 1\n    data = length 24, hash 640AA61B\n  sample 136:\n    time = 2720000\n    flags = 1\n    data = length 24, hash ABE659B\n  sample 137:\n    time = 2740000\n    flags = 1\n    data = length 24, hash ED2691D2\n  sample 138:\n    time = 2760000\n    flags = 1\n    data = length 24, hash D998C80E\n  sample 139:\n    time = 2780000\n    flags = 1\n    data = length 24, hash 8DC0DF5C\n  sample 140:\n    time = 2800000\n    flags = 1\n    data = length 24, hash 7692247B\n  sample 141:\n    time = 2820000\n    flags = 1\n    data = length 24, hash C1D1CCB9\n  sample 142:\n    time = 2840000\n    flags = 1\n    data = length 24, hash 362CE78E\n  sample 143:\n    time = 2860000\n    flags = 1\n    data = length 24, hash 54FA84A\n  sample 144:\n    time = 2880000\n    flags = 1\n    data = length 24, hash 29E88C84\n  sample 145:\n    time = 2900000\n    flags = 1\n    data = length 24, hash 1CD848AC\n  sample 146:\n    time = 2920000\n    flags = 1\n    data = length 24, hash 5C3D4A79\n  sample 147:\n    time = 2940000\n    flags = 1\n    data = length 24, hash 1AA8E604\n  sample 148:\n    time = 2960000\n    flags = 1\n    data = length 24, hash 186A4316\n  sample 149:\n    time = 2980000\n    flags = 1\n    data = length 24, hash 61ACE481\n  sample 150:\n    time = 3000000\n    flags = 1\n    data = length 24, hash D0C42780\n  sample 151:\n    time = 3020000\n    flags = 1\n    data = length 24, hash FAD51BA1\n  sample 152:\n    time = 3040000\n    flags = 1\n    data = length 24, hash F1A9AC71\n  sample 153:\n    time = 3060000\n    flags = 1\n    data = length 24, hash 24425449\n  sample 154:\n    time = 3080000\n    flags = 1\n    data = length 24, hash 37AAC3E6\n  sample 155:\n    time = 3100000\n    flags = 1\n    data = length 24, hash 91F68CB4\n  sample 156:\n    time = 3120000\n    flags = 1\n    data = length 24, hash F8C92820\n  sample 157:\n    time = 3140000\n    flags = 1\n    data = length 24, hash ECD39C3E\n  sample 158:\n    time = 3160000\n    flags = 1\n    data = length 24, hash B27D8F78\n  sample 159:\n    time = 3180000\n    flags = 1\n    data = length 24, hash C9EB3DFB\n  sample 160:\n    time = 3200000\n    flags = 1\n    data = length 24, hash 88DC54A2\n  sample 161:\n    time = 3220000\n    flags = 1\n    data = length 24, hash 7FC4C5BE\n  sample 162:\n    time = 3240000\n    flags = 1\n    data = length 24, hash E4F684EF\n  sample 163:\n    time = 3260000\n    flags = 1\n    data = length 24, hash 55C08B56\n  sample 164:\n    time = 3280000\n    flags = 1\n    data = length 24, hash E5A0F006\n  sample 165:\n    time = 3300000\n    flags = 1\n    data = length 24, hash DE3F3AA7\n  sample 166:\n    time = 3320000\n    flags = 1\n    data = length 24, hash 3F28AE7F\n  sample 167:\n    time = 3340000\n    flags = 1\n    data = length 24, hash 3949CAFF\n  sample 168:\n    time = 3360000\n    flags = 1\n    data = length 24, hash 772665A0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/amr/sample_wb_cbr.amr.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 3380000\n  getPosition(0) = [[timeUs=0, position=9]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/amr-wb\n    maxInputSize = 61\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 16000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 2712\n  sample count = 113\n  sample 0:\n    time = 1120000\n    flags = 1\n    data = length 24, hash EEB921BD\n  sample 1:\n    time = 1140000\n    flags = 1\n    data = length 24, hash 8B941A4C\n  sample 2:\n    time = 1160000\n    flags = 1\n    data = length 24, hash ED5F5FEE\n  sample 3:\n    time = 1180000\n    flags = 1\n    data = length 24, hash A588E0BB\n  sample 4:\n    time = 1200000\n    flags = 1\n    data = length 24, hash 588CBC01\n  sample 5:\n    time = 1220000\n    flags = 1\n    data = length 24, hash DE22266C\n  sample 6:\n    time = 1240000\n    flags = 1\n    data = length 24, hash 921B6E5C\n  sample 7:\n    time = 1260000\n    flags = 1\n    data = length 24, hash EC11F041\n  sample 8:\n    time = 1280000\n    flags = 1\n    data = length 24, hash 5BA9E0A3\n  sample 9:\n    time = 1300000\n    flags = 1\n    data = length 24, hash DB6D52F3\n  sample 10:\n    time = 1320000\n    flags = 1\n    data = length 24, hash 8EEBE525\n  sample 11:\n    time = 1340000\n    flags = 1\n    data = length 24, hash 47A742AE\n  sample 12:\n    time = 1360000\n    flags = 1\n    data = length 24, hash E93F1E03\n  sample 13:\n    time = 1380000\n    flags = 1\n    data = length 24, hash 3251F57C\n  sample 14:\n    time = 1400000\n    flags = 1\n    data = length 24, hash 3EDBBBDD\n  sample 15:\n    time = 1420000\n    flags = 1\n    data = length 24, hash 2E98465A\n  sample 16:\n    time = 1440000\n    flags = 1\n    data = length 24, hash A09EA52E\n  sample 17:\n    time = 1460000\n    flags = 1\n    data = length 24, hash A2A86FA6\n  sample 18:\n    time = 1480000\n    flags = 1\n    data = length 24, hash 71DCD51C\n  sample 19:\n    time = 1500000\n    flags = 1\n    data = length 24, hash 2B02DEE1\n  sample 20:\n    time = 1520000\n    flags = 1\n    data = length 24, hash 7A725192\n  sample 21:\n    time = 1540000\n    flags = 1\n    data = length 24, hash 929AD483\n  sample 22:\n    time = 1560000\n    flags = 1\n    data = length 24, hash 68440BF5\n  sample 23:\n    time = 1580000\n    flags = 1\n    data = length 24, hash 5BD41AD6\n  sample 24:\n    time = 1600000\n    flags = 1\n    data = length 24, hash 91A381\n  sample 25:\n    time = 1620000\n    flags = 1\n    data = length 24, hash 8010C408\n  sample 26:\n    time = 1640000\n    flags = 1\n    data = length 24, hash 482274BE\n  sample 27:\n    time = 1660000\n    flags = 1\n    data = length 24, hash D7DB8BCC\n  sample 28:\n    time = 1680000\n    flags = 1\n    data = length 24, hash 680BD9DD\n  sample 29:\n    time = 1700000\n    flags = 1\n    data = length 24, hash E313577C\n  sample 30:\n    time = 1720000\n    flags = 1\n    data = length 24, hash 9C10B0CD\n  sample 31:\n    time = 1740000\n    flags = 1\n    data = length 24, hash 2D90AC02\n  sample 32:\n    time = 1760000\n    flags = 1\n    data = length 24, hash 64E8C245\n  sample 33:\n    time = 1780000\n    flags = 1\n    data = length 24, hash 3954AC1B\n  sample 34:\n    time = 1800000\n    flags = 1\n    data = length 24, hash ACB8999F\n  sample 35:\n    time = 1820000\n    flags = 1\n    data = length 24, hash 43AE3957\n  sample 36:\n    time = 1840000\n    flags = 1\n    data = length 24, hash 3C664DB7\n  sample 37:\n    time = 1860000\n    flags = 1\n    data = length 24, hash 9354B576\n  sample 38:\n    time = 1880000\n    flags = 1\n    data = length 24, hash B5B9C14E\n  sample 39:\n    time = 1900000\n    flags = 1\n    data = length 24, hash 7DA9C98F\n  sample 40:\n    time = 1920000\n    flags = 1\n    data = length 24, hash EFEE54C6\n  sample 41:\n    time = 1940000\n    flags = 1\n    data = length 24, hash 79DC8CBD\n  sample 42:\n    time = 1960000\n    flags = 1\n    data = length 24, hash A71A475C\n  sample 43:\n    time = 1980000\n    flags = 1\n    data = length 24, hash CA1CBB94\n  sample 44:\n    time = 2000000\n    flags = 1\n    data = length 24, hash 91922226\n  sample 45:\n    time = 2020000\n    flags = 1\n    data = length 24, hash C90278BC\n  sample 46:\n    time = 2040000\n    flags = 1\n    data = length 24, hash BD51986F\n  sample 47:\n    time = 2060000\n    flags = 1\n    data = length 24, hash 90AEF368\n  sample 48:\n    time = 2080000\n    flags = 1\n    data = length 24, hash 1D83C955\n  sample 49:\n    time = 2100000\n    flags = 1\n    data = length 24, hash 8FA9A915\n  sample 50:\n    time = 2120000\n    flags = 1\n    data = length 24, hash C6C753E0\n  sample 51:\n    time = 2140000\n    flags = 1\n    data = length 24, hash 85FA27A7\n  sample 52:\n    time = 2160000\n    flags = 1\n    data = length 24, hash A0277324\n  sample 53:\n    time = 2180000\n    flags = 1\n    data = length 24, hash B7696535\n  sample 54:\n    time = 2200000\n    flags = 1\n    data = length 24, hash D69D668C\n  sample 55:\n    time = 2220000\n    flags = 1\n    data = length 24, hash 34C057CD\n  sample 56:\n    time = 2240000\n    flags = 1\n    data = length 24, hash 4EC5E974\n  sample 57:\n    time = 2260000\n    flags = 1\n    data = length 24, hash 1C1CD40D\n  sample 58:\n    time = 2280000\n    flags = 1\n    data = length 24, hash 76CC54BC\n  sample 59:\n    time = 2300000\n    flags = 1\n    data = length 24, hash D497ACF5\n  sample 60:\n    time = 2320000\n    flags = 1\n    data = length 24, hash A1386080\n  sample 61:\n    time = 2340000\n    flags = 1\n    data = length 24, hash 7ED36954\n  sample 62:\n    time = 2360000\n    flags = 1\n    data = length 24, hash C11A3BF9\n  sample 63:\n    time = 2380000\n    flags = 1\n    data = length 24, hash 8FB69488\n  sample 64:\n    time = 2400000\n    flags = 1\n    data = length 24, hash C6225F59\n  sample 65:\n    time = 2420000\n    flags = 1\n    data = length 24, hash 122AB6D2\n  sample 66:\n    time = 2440000\n    flags = 1\n    data = length 24, hash 1E195E7D\n  sample 67:\n    time = 2460000\n    flags = 1\n    data = length 24, hash BD3DF418\n  sample 68:\n    time = 2480000\n    flags = 1\n    data = length 24, hash D8AE4A5\n  sample 69:\n    time = 2500000\n    flags = 1\n    data = length 24, hash 977BD182\n  sample 70:\n    time = 2520000\n    flags = 1\n    data = length 24, hash F361F060\n  sample 71:\n    time = 2540000\n    flags = 1\n    data = length 24, hash 11EC8CD0\n  sample 72:\n    time = 2560000\n    flags = 1\n    data = length 24, hash 3798F3D2\n  sample 73:\n    time = 2580000\n    flags = 1\n    data = length 24, hash B2C2517C\n  sample 74:\n    time = 2600000\n    flags = 1\n    data = length 24, hash FBE0D0D8\n  sample 75:\n    time = 2620000\n    flags = 1\n    data = length 24, hash 7033172F\n  sample 76:\n    time = 2640000\n    flags = 1\n    data = length 24, hash BE760029\n  sample 77:\n    time = 2660000\n    flags = 1\n    data = length 24, hash 590AF28C\n  sample 78:\n    time = 2680000\n    flags = 1\n    data = length 24, hash AD28C48F\n  sample 79:\n    time = 2700000\n    flags = 1\n    data = length 24, hash 640AA61B\n  sample 80:\n    time = 2720000\n    flags = 1\n    data = length 24, hash ABE659B\n  sample 81:\n    time = 2740000\n    flags = 1\n    data = length 24, hash ED2691D2\n  sample 82:\n    time = 2760000\n    flags = 1\n    data = length 24, hash D998C80E\n  sample 83:\n    time = 2780000\n    flags = 1\n    data = length 24, hash 8DC0DF5C\n  sample 84:\n    time = 2800000\n    flags = 1\n    data = length 24, hash 7692247B\n  sample 85:\n    time = 2820000\n    flags = 1\n    data = length 24, hash C1D1CCB9\n  sample 86:\n    time = 2840000\n    flags = 1\n    data = length 24, hash 362CE78E\n  sample 87:\n    time = 2860000\n    flags = 1\n    data = length 24, hash 54FA84A\n  sample 88:\n    time = 2880000\n    flags = 1\n    data = length 24, hash 29E88C84\n  sample 89:\n    time = 2900000\n    flags = 1\n    data = length 24, hash 1CD848AC\n  sample 90:\n    time = 2920000\n    flags = 1\n    data = length 24, hash 5C3D4A79\n  sample 91:\n    time = 2940000\n    flags = 1\n    data = length 24, hash 1AA8E604\n  sample 92:\n    time = 2960000\n    flags = 1\n    data = length 24, hash 186A4316\n  sample 93:\n    time = 2980000\n    flags = 1\n    data = length 24, hash 61ACE481\n  sample 94:\n    time = 3000000\n    flags = 1\n    data = length 24, hash D0C42780\n  sample 95:\n    time = 3020000\n    flags = 1\n    data = length 24, hash FAD51BA1\n  sample 96:\n    time = 3040000\n    flags = 1\n    data = length 24, hash F1A9AC71\n  sample 97:\n    time = 3060000\n    flags = 1\n    data = length 24, hash 24425449\n  sample 98:\n    time = 3080000\n    flags = 1\n    data = length 24, hash 37AAC3E6\n  sample 99:\n    time = 3100000\n    flags = 1\n    data = length 24, hash 91F68CB4\n  sample 100:\n    time = 3120000\n    flags = 1\n    data = length 24, hash F8C92820\n  sample 101:\n    time = 3140000\n    flags = 1\n    data = length 24, hash ECD39C3E\n  sample 102:\n    time = 3160000\n    flags = 1\n    data = length 24, hash B27D8F78\n  sample 103:\n    time = 3180000\n    flags = 1\n    data = length 24, hash C9EB3DFB\n  sample 104:\n    time = 3200000\n    flags = 1\n    data = length 24, hash 88DC54A2\n  sample 105:\n    time = 3220000\n    flags = 1\n    data = length 24, hash 7FC4C5BE\n  sample 106:\n    time = 3240000\n    flags = 1\n    data = length 24, hash E4F684EF\n  sample 107:\n    time = 3260000\n    flags = 1\n    data = length 24, hash 55C08B56\n  sample 108:\n    time = 3280000\n    flags = 1\n    data = length 24, hash E5A0F006\n  sample 109:\n    time = 3300000\n    flags = 1\n    data = length 24, hash DE3F3AA7\n  sample 110:\n    time = 3320000\n    flags = 1\n    data = length 24, hash 3F28AE7F\n  sample 111:\n    time = 3340000\n    flags = 1\n    data = length 24, hash 3949CAFF\n  sample 112:\n    time = 3360000\n    flags = 1\n    data = length 24, hash 772665A0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/amr/sample_wb_cbr.amr.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 3380000\n  getPosition(0) = [[timeUs=0, position=9]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/amr-wb\n    maxInputSize = 61\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 16000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 1368\n  sample count = 57\n  sample 0:\n    time = 2240000\n    flags = 1\n    data = length 24, hash 4EC5E974\n  sample 1:\n    time = 2260000\n    flags = 1\n    data = length 24, hash 1C1CD40D\n  sample 2:\n    time = 2280000\n    flags = 1\n    data = length 24, hash 76CC54BC\n  sample 3:\n    time = 2300000\n    flags = 1\n    data = length 24, hash D497ACF5\n  sample 4:\n    time = 2320000\n    flags = 1\n    data = length 24, hash A1386080\n  sample 5:\n    time = 2340000\n    flags = 1\n    data = length 24, hash 7ED36954\n  sample 6:\n    time = 2360000\n    flags = 1\n    data = length 24, hash C11A3BF9\n  sample 7:\n    time = 2380000\n    flags = 1\n    data = length 24, hash 8FB69488\n  sample 8:\n    time = 2400000\n    flags = 1\n    data = length 24, hash C6225F59\n  sample 9:\n    time = 2420000\n    flags = 1\n    data = length 24, hash 122AB6D2\n  sample 10:\n    time = 2440000\n    flags = 1\n    data = length 24, hash 1E195E7D\n  sample 11:\n    time = 2460000\n    flags = 1\n    data = length 24, hash BD3DF418\n  sample 12:\n    time = 2480000\n    flags = 1\n    data = length 24, hash D8AE4A5\n  sample 13:\n    time = 2500000\n    flags = 1\n    data = length 24, hash 977BD182\n  sample 14:\n    time = 2520000\n    flags = 1\n    data = length 24, hash F361F060\n  sample 15:\n    time = 2540000\n    flags = 1\n    data = length 24, hash 11EC8CD0\n  sample 16:\n    time = 2560000\n    flags = 1\n    data = length 24, hash 3798F3D2\n  sample 17:\n    time = 2580000\n    flags = 1\n    data = length 24, hash B2C2517C\n  sample 18:\n    time = 2600000\n    flags = 1\n    data = length 24, hash FBE0D0D8\n  sample 19:\n    time = 2620000\n    flags = 1\n    data = length 24, hash 7033172F\n  sample 20:\n    time = 2640000\n    flags = 1\n    data = length 24, hash BE760029\n  sample 21:\n    time = 2660000\n    flags = 1\n    data = length 24, hash 590AF28C\n  sample 22:\n    time = 2680000\n    flags = 1\n    data = length 24, hash AD28C48F\n  sample 23:\n    time = 2700000\n    flags = 1\n    data = length 24, hash 640AA61B\n  sample 24:\n    time = 2720000\n    flags = 1\n    data = length 24, hash ABE659B\n  sample 25:\n    time = 2740000\n    flags = 1\n    data = length 24, hash ED2691D2\n  sample 26:\n    time = 2760000\n    flags = 1\n    data = length 24, hash D998C80E\n  sample 27:\n    time = 2780000\n    flags = 1\n    data = length 24, hash 8DC0DF5C\n  sample 28:\n    time = 2800000\n    flags = 1\n    data = length 24, hash 7692247B\n  sample 29:\n    time = 2820000\n    flags = 1\n    data = length 24, hash C1D1CCB9\n  sample 30:\n    time = 2840000\n    flags = 1\n    data = length 24, hash 362CE78E\n  sample 31:\n    time = 2860000\n    flags = 1\n    data = length 24, hash 54FA84A\n  sample 32:\n    time = 2880000\n    flags = 1\n    data = length 24, hash 29E88C84\n  sample 33:\n    time = 2900000\n    flags = 1\n    data = length 24, hash 1CD848AC\n  sample 34:\n    time = 2920000\n    flags = 1\n    data = length 24, hash 5C3D4A79\n  sample 35:\n    time = 2940000\n    flags = 1\n    data = length 24, hash 1AA8E604\n  sample 36:\n    time = 2960000\n    flags = 1\n    data = length 24, hash 186A4316\n  sample 37:\n    time = 2980000\n    flags = 1\n    data = length 24, hash 61ACE481\n  sample 38:\n    time = 3000000\n    flags = 1\n    data = length 24, hash D0C42780\n  sample 39:\n    time = 3020000\n    flags = 1\n    data = length 24, hash FAD51BA1\n  sample 40:\n    time = 3040000\n    flags = 1\n    data = length 24, hash F1A9AC71\n  sample 41:\n    time = 3060000\n    flags = 1\n    data = length 24, hash 24425449\n  sample 42:\n    time = 3080000\n    flags = 1\n    data = length 24, hash 37AAC3E6\n  sample 43:\n    time = 3100000\n    flags = 1\n    data = length 24, hash 91F68CB4\n  sample 44:\n    time = 3120000\n    flags = 1\n    data = length 24, hash F8C92820\n  sample 45:\n    time = 3140000\n    flags = 1\n    data = length 24, hash ECD39C3E\n  sample 46:\n    time = 3160000\n    flags = 1\n    data = length 24, hash B27D8F78\n  sample 47:\n    time = 3180000\n    flags = 1\n    data = length 24, hash C9EB3DFB\n  sample 48:\n    time = 3200000\n    flags = 1\n    data = length 24, hash 88DC54A2\n  sample 49:\n    time = 3220000\n    flags = 1\n    data = length 24, hash 7FC4C5BE\n  sample 50:\n    time = 3240000\n    flags = 1\n    data = length 24, hash E4F684EF\n  sample 51:\n    time = 3260000\n    flags = 1\n    data = length 24, hash 55C08B56\n  sample 52:\n    time = 3280000\n    flags = 1\n    data = length 24, hash E5A0F006\n  sample 53:\n    time = 3300000\n    flags = 1\n    data = length 24, hash DE3F3AA7\n  sample 54:\n    time = 3320000\n    flags = 1\n    data = length 24, hash 3F28AE7F\n  sample 55:\n    time = 3340000\n    flags = 1\n    data = length 24, hash 3949CAFF\n  sample 56:\n    time = 3360000\n    flags = 1\n    data = length 24, hash 772665A0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/amr/sample_wb_cbr.amr.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 3380000\n  getPosition(0) = [[timeUs=0, position=9]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/amr-wb\n    maxInputSize = 61\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 16000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 24\n  sample count = 1\n  sample 0:\n    time = 3360000\n    flags = 1\n    data = length 24, hash 772665A0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/amr/sample_wb_cbr.amr.unklen.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/amr-wb\n    maxInputSize = 61\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 16000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 4056\n  sample count = 169\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 24, hash C3025798\n  sample 1:\n    time = 20000\n    flags = 1\n    data = length 24, hash 39CABAE9\n  sample 2:\n    time = 40000\n    flags = 1\n    data = length 24, hash 2752F470\n  sample 3:\n    time = 60000\n    flags = 1\n    data = length 24, hash 394F76F6\n  sample 4:\n    time = 80000\n    flags = 1\n    data = length 24, hash FF9EEF\n  sample 5:\n    time = 100000\n    flags = 1\n    data = length 24, hash 54ECB1B4\n  sample 6:\n    time = 120000\n    flags = 1\n    data = length 24, hash 6D7A3A5F\n  sample 7:\n    time = 140000\n    flags = 1\n    data = length 24, hash 684CD144\n  sample 8:\n    time = 160000\n    flags = 1\n    data = length 24, hash 87B7D176\n  sample 9:\n    time = 180000\n    flags = 1\n    data = length 24, hash 4C02F9A5\n  sample 10:\n    time = 200000\n    flags = 1\n    data = length 24, hash B4154108\n  sample 11:\n    time = 220000\n    flags = 1\n    data = length 24, hash 4448F477\n  sample 12:\n    time = 240000\n    flags = 1\n    data = length 24, hash 755A4939\n  sample 13:\n    time = 260000\n    flags = 1\n    data = length 24, hash 8C8BC6C3\n  sample 14:\n    time = 280000\n    flags = 1\n    data = length 24, hash BC37F63F\n  sample 15:\n    time = 300000\n    flags = 1\n    data = length 24, hash 3352C43C\n  sample 16:\n    time = 320000\n    flags = 1\n    data = length 24, hash 7998E1F2\n  sample 17:\n    time = 340000\n    flags = 1\n    data = length 24, hash A8ECBEFC\n  sample 18:\n    time = 360000\n    flags = 1\n    data = length 24, hash 944AC118\n  sample 19:\n    time = 380000\n    flags = 1\n    data = length 24, hash FD2C8E1F\n  sample 20:\n    time = 400000\n    flags = 1\n    data = length 24, hash B3D867AF\n  sample 21:\n    time = 420000\n    flags = 1\n    data = length 24, hash 3DC6E592\n  sample 22:\n    time = 440000\n    flags = 1\n    data = length 24, hash 32B276CD\n  sample 23:\n    time = 460000\n    flags = 1\n    data = length 24, hash 5488AEF3\n  sample 24:\n    time = 480000\n    flags = 1\n    data = length 24, hash 7A4D516\n  sample 25:\n    time = 500000\n    flags = 1\n    data = length 24, hash 570AE83F\n  sample 26:\n    time = 520000\n    flags = 1\n    data = length 24, hash E5CB3477\n  sample 27:\n    time = 540000\n    flags = 1\n    data = length 24, hash E04C00E4\n  sample 28:\n    time = 560000\n    flags = 1\n    data = length 24, hash 21B7C97\n  sample 29:\n    time = 580000\n    flags = 1\n    data = length 24, hash 1633F470\n  sample 30:\n    time = 600000\n    flags = 1\n    data = length 24, hash 28D65CA6\n  sample 31:\n    time = 620000\n    flags = 1\n    data = length 24, hash CC6A675C\n  sample 32:\n    time = 640000\n    flags = 1\n    data = length 24, hash 4C91080A\n  sample 33:\n    time = 660000\n    flags = 1\n    data = length 24, hash F6482FB5\n  sample 34:\n    time = 680000\n    flags = 1\n    data = length 24, hash 2C76F48C\n  sample 35:\n    time = 700000\n    flags = 1\n    data = length 24, hash 6E3B0D72\n  sample 36:\n    time = 720000\n    flags = 1\n    data = length 24, hash 799AA003\n  sample 37:\n    time = 740000\n    flags = 1\n    data = length 24, hash DFC0BA81\n  sample 38:\n    time = 760000\n    flags = 1\n    data = length 24, hash CBDF3826\n  sample 39:\n    time = 780000\n    flags = 1\n    data = length 24, hash 16862B75\n  sample 40:\n    time = 800000\n    flags = 1\n    data = length 24, hash 865A828E\n  sample 41:\n    time = 820000\n    flags = 1\n    data = length 24, hash 336BBDC9\n  sample 42:\n    time = 840000\n    flags = 1\n    data = length 24, hash 6CFC6C34\n  sample 43:\n    time = 860000\n    flags = 1\n    data = length 24, hash 32C8CD46\n  sample 44:\n    time = 880000\n    flags = 1\n    data = length 24, hash 9FE11C4C\n  sample 45:\n    time = 900000\n    flags = 1\n    data = length 24, hash AA5A12B7\n  sample 46:\n    time = 920000\n    flags = 1\n    data = length 24, hash AA0F4A4D\n  sample 47:\n    time = 940000\n    flags = 1\n    data = length 24, hash 34415484\n  sample 48:\n    time = 960000\n    flags = 1\n    data = length 24, hash 5018928E\n  sample 49:\n    time = 980000\n    flags = 1\n    data = length 24, hash 4A04D162\n  sample 50:\n    time = 1000000\n    flags = 1\n    data = length 24, hash 4C70F9F0\n  sample 51:\n    time = 1020000\n    flags = 1\n    data = length 24, hash 99EF3168\n  sample 52:\n    time = 1040000\n    flags = 1\n    data = length 24, hash C600DAF\n  sample 53:\n    time = 1060000\n    flags = 1\n    data = length 24, hash FDBB192E\n  sample 54:\n    time = 1080000\n    flags = 1\n    data = length 24, hash 99096A48\n  sample 55:\n    time = 1100000\n    flags = 1\n    data = length 24, hash D793F88B\n  sample 56:\n    time = 1120000\n    flags = 1\n    data = length 24, hash EEB921BD\n  sample 57:\n    time = 1140000\n    flags = 1\n    data = length 24, hash 8B941A4C\n  sample 58:\n    time = 1160000\n    flags = 1\n    data = length 24, hash ED5F5FEE\n  sample 59:\n    time = 1180000\n    flags = 1\n    data = length 24, hash A588E0BB\n  sample 60:\n    time = 1200000\n    flags = 1\n    data = length 24, hash 588CBC01\n  sample 61:\n    time = 1220000\n    flags = 1\n    data = length 24, hash DE22266C\n  sample 62:\n    time = 1240000\n    flags = 1\n    data = length 24, hash 921B6E5C\n  sample 63:\n    time = 1260000\n    flags = 1\n    data = length 24, hash EC11F041\n  sample 64:\n    time = 1280000\n    flags = 1\n    data = length 24, hash 5BA9E0A3\n  sample 65:\n    time = 1300000\n    flags = 1\n    data = length 24, hash DB6D52F3\n  sample 66:\n    time = 1320000\n    flags = 1\n    data = length 24, hash 8EEBE525\n  sample 67:\n    time = 1340000\n    flags = 1\n    data = length 24, hash 47A742AE\n  sample 68:\n    time = 1360000\n    flags = 1\n    data = length 24, hash E93F1E03\n  sample 69:\n    time = 1380000\n    flags = 1\n    data = length 24, hash 3251F57C\n  sample 70:\n    time = 1400000\n    flags = 1\n    data = length 24, hash 3EDBBBDD\n  sample 71:\n    time = 1420000\n    flags = 1\n    data = length 24, hash 2E98465A\n  sample 72:\n    time = 1440000\n    flags = 1\n    data = length 24, hash A09EA52E\n  sample 73:\n    time = 1460000\n    flags = 1\n    data = length 24, hash A2A86FA6\n  sample 74:\n    time = 1480000\n    flags = 1\n    data = length 24, hash 71DCD51C\n  sample 75:\n    time = 1500000\n    flags = 1\n    data = length 24, hash 2B02DEE1\n  sample 76:\n    time = 1520000\n    flags = 1\n    data = length 24, hash 7A725192\n  sample 77:\n    time = 1540000\n    flags = 1\n    data = length 24, hash 929AD483\n  sample 78:\n    time = 1560000\n    flags = 1\n    data = length 24, hash 68440BF5\n  sample 79:\n    time = 1580000\n    flags = 1\n    data = length 24, hash 5BD41AD6\n  sample 80:\n    time = 1600000\n    flags = 1\n    data = length 24, hash 91A381\n  sample 81:\n    time = 1620000\n    flags = 1\n    data = length 24, hash 8010C408\n  sample 82:\n    time = 1640000\n    flags = 1\n    data = length 24, hash 482274BE\n  sample 83:\n    time = 1660000\n    flags = 1\n    data = length 24, hash D7DB8BCC\n  sample 84:\n    time = 1680000\n    flags = 1\n    data = length 24, hash 680BD9DD\n  sample 85:\n    time = 1700000\n    flags = 1\n    data = length 24, hash E313577C\n  sample 86:\n    time = 1720000\n    flags = 1\n    data = length 24, hash 9C10B0CD\n  sample 87:\n    time = 1740000\n    flags = 1\n    data = length 24, hash 2D90AC02\n  sample 88:\n    time = 1760000\n    flags = 1\n    data = length 24, hash 64E8C245\n  sample 89:\n    time = 1780000\n    flags = 1\n    data = length 24, hash 3954AC1B\n  sample 90:\n    time = 1800000\n    flags = 1\n    data = length 24, hash ACB8999F\n  sample 91:\n    time = 1820000\n    flags = 1\n    data = length 24, hash 43AE3957\n  sample 92:\n    time = 1840000\n    flags = 1\n    data = length 24, hash 3C664DB7\n  sample 93:\n    time = 1860000\n    flags = 1\n    data = length 24, hash 9354B576\n  sample 94:\n    time = 1880000\n    flags = 1\n    data = length 24, hash B5B9C14E\n  sample 95:\n    time = 1900000\n    flags = 1\n    data = length 24, hash 7DA9C98F\n  sample 96:\n    time = 1920000\n    flags = 1\n    data = length 24, hash EFEE54C6\n  sample 97:\n    time = 1940000\n    flags = 1\n    data = length 24, hash 79DC8CBD\n  sample 98:\n    time = 1960000\n    flags = 1\n    data = length 24, hash A71A475C\n  sample 99:\n    time = 1980000\n    flags = 1\n    data = length 24, hash CA1CBB94\n  sample 100:\n    time = 2000000\n    flags = 1\n    data = length 24, hash 91922226\n  sample 101:\n    time = 2020000\n    flags = 1\n    data = length 24, hash C90278BC\n  sample 102:\n    time = 2040000\n    flags = 1\n    data = length 24, hash BD51986F\n  sample 103:\n    time = 2060000\n    flags = 1\n    data = length 24, hash 90AEF368\n  sample 104:\n    time = 2080000\n    flags = 1\n    data = length 24, hash 1D83C955\n  sample 105:\n    time = 2100000\n    flags = 1\n    data = length 24, hash 8FA9A915\n  sample 106:\n    time = 2120000\n    flags = 1\n    data = length 24, hash C6C753E0\n  sample 107:\n    time = 2140000\n    flags = 1\n    data = length 24, hash 85FA27A7\n  sample 108:\n    time = 2160000\n    flags = 1\n    data = length 24, hash A0277324\n  sample 109:\n    time = 2180000\n    flags = 1\n    data = length 24, hash B7696535\n  sample 110:\n    time = 2200000\n    flags = 1\n    data = length 24, hash D69D668C\n  sample 111:\n    time = 2220000\n    flags = 1\n    data = length 24, hash 34C057CD\n  sample 112:\n    time = 2240000\n    flags = 1\n    data = length 24, hash 4EC5E974\n  sample 113:\n    time = 2260000\n    flags = 1\n    data = length 24, hash 1C1CD40D\n  sample 114:\n    time = 2280000\n    flags = 1\n    data = length 24, hash 76CC54BC\n  sample 115:\n    time = 2300000\n    flags = 1\n    data = length 24, hash D497ACF5\n  sample 116:\n    time = 2320000\n    flags = 1\n    data = length 24, hash A1386080\n  sample 117:\n    time = 2340000\n    flags = 1\n    data = length 24, hash 7ED36954\n  sample 118:\n    time = 2360000\n    flags = 1\n    data = length 24, hash C11A3BF9\n  sample 119:\n    time = 2380000\n    flags = 1\n    data = length 24, hash 8FB69488\n  sample 120:\n    time = 2400000\n    flags = 1\n    data = length 24, hash C6225F59\n  sample 121:\n    time = 2420000\n    flags = 1\n    data = length 24, hash 122AB6D2\n  sample 122:\n    time = 2440000\n    flags = 1\n    data = length 24, hash 1E195E7D\n  sample 123:\n    time = 2460000\n    flags = 1\n    data = length 24, hash BD3DF418\n  sample 124:\n    time = 2480000\n    flags = 1\n    data = length 24, hash D8AE4A5\n  sample 125:\n    time = 2500000\n    flags = 1\n    data = length 24, hash 977BD182\n  sample 126:\n    time = 2520000\n    flags = 1\n    data = length 24, hash F361F060\n  sample 127:\n    time = 2540000\n    flags = 1\n    data = length 24, hash 11EC8CD0\n  sample 128:\n    time = 2560000\n    flags = 1\n    data = length 24, hash 3798F3D2\n  sample 129:\n    time = 2580000\n    flags = 1\n    data = length 24, hash B2C2517C\n  sample 130:\n    time = 2600000\n    flags = 1\n    data = length 24, hash FBE0D0D8\n  sample 131:\n    time = 2620000\n    flags = 1\n    data = length 24, hash 7033172F\n  sample 132:\n    time = 2640000\n    flags = 1\n    data = length 24, hash BE760029\n  sample 133:\n    time = 2660000\n    flags = 1\n    data = length 24, hash 590AF28C\n  sample 134:\n    time = 2680000\n    flags = 1\n    data = length 24, hash AD28C48F\n  sample 135:\n    time = 2700000\n    flags = 1\n    data = length 24, hash 640AA61B\n  sample 136:\n    time = 2720000\n    flags = 1\n    data = length 24, hash ABE659B\n  sample 137:\n    time = 2740000\n    flags = 1\n    data = length 24, hash ED2691D2\n  sample 138:\n    time = 2760000\n    flags = 1\n    data = length 24, hash D998C80E\n  sample 139:\n    time = 2780000\n    flags = 1\n    data = length 24, hash 8DC0DF5C\n  sample 140:\n    time = 2800000\n    flags = 1\n    data = length 24, hash 7692247B\n  sample 141:\n    time = 2820000\n    flags = 1\n    data = length 24, hash C1D1CCB9\n  sample 142:\n    time = 2840000\n    flags = 1\n    data = length 24, hash 362CE78E\n  sample 143:\n    time = 2860000\n    flags = 1\n    data = length 24, hash 54FA84A\n  sample 144:\n    time = 2880000\n    flags = 1\n    data = length 24, hash 29E88C84\n  sample 145:\n    time = 2900000\n    flags = 1\n    data = length 24, hash 1CD848AC\n  sample 146:\n    time = 2920000\n    flags = 1\n    data = length 24, hash 5C3D4A79\n  sample 147:\n    time = 2940000\n    flags = 1\n    data = length 24, hash 1AA8E604\n  sample 148:\n    time = 2960000\n    flags = 1\n    data = length 24, hash 186A4316\n  sample 149:\n    time = 2980000\n    flags = 1\n    data = length 24, hash 61ACE481\n  sample 150:\n    time = 3000000\n    flags = 1\n    data = length 24, hash D0C42780\n  sample 151:\n    time = 3020000\n    flags = 1\n    data = length 24, hash FAD51BA1\n  sample 152:\n    time = 3040000\n    flags = 1\n    data = length 24, hash F1A9AC71\n  sample 153:\n    time = 3060000\n    flags = 1\n    data = length 24, hash 24425449\n  sample 154:\n    time = 3080000\n    flags = 1\n    data = length 24, hash 37AAC3E6\n  sample 155:\n    time = 3100000\n    flags = 1\n    data = length 24, hash 91F68CB4\n  sample 156:\n    time = 3120000\n    flags = 1\n    data = length 24, hash F8C92820\n  sample 157:\n    time = 3140000\n    flags = 1\n    data = length 24, hash ECD39C3E\n  sample 158:\n    time = 3160000\n    flags = 1\n    data = length 24, hash B27D8F78\n  sample 159:\n    time = 3180000\n    flags = 1\n    data = length 24, hash C9EB3DFB\n  sample 160:\n    time = 3200000\n    flags = 1\n    data = length 24, hash 88DC54A2\n  sample 161:\n    time = 3220000\n    flags = 1\n    data = length 24, hash 7FC4C5BE\n  sample 162:\n    time = 3240000\n    flags = 1\n    data = length 24, hash E4F684EF\n  sample 163:\n    time = 3260000\n    flags = 1\n    data = length 24, hash 55C08B56\n  sample 164:\n    time = 3280000\n    flags = 1\n    data = length 24, hash E5A0F006\n  sample 165:\n    time = 3300000\n    flags = 1\n    data = length 24, hash DE3F3AA7\n  sample 166:\n    time = 3320000\n    flags = 1\n    data = length 24, hash 3F28AE7F\n  sample 167:\n    time = 3340000\n    flags = 1\n    data = length 24, hash 3949CAFF\n  sample 168:\n    time = 3360000\n    flags = 1\n    data = length 24, hash 772665A0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/flv/sample.flv.0.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = 1136000\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 2\ntrack 8:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 2, hash 5F7\n  total output bytes = 9529\n  sample count = 45\n  sample 0:\n    time = 112000\n    flags = 1\n    data = length 23, hash 47DE9131\n  sample 1:\n    time = 135000\n    flags = 1\n    data = length 6, hash 31EC5206\n  sample 2:\n    time = 158000\n    flags = 1\n    data = length 148, hash 894A176B\n  sample 3:\n    time = 181000\n    flags = 1\n    data = length 189, hash CEF235A1\n  sample 4:\n    time = 205000\n    flags = 1\n    data = length 205, hash BBF5F7B0\n  sample 5:\n    time = 228000\n    flags = 1\n    data = length 210, hash F278B193\n  sample 6:\n    time = 251000\n    flags = 1\n    data = length 210, hash 82DA1589\n  sample 7:\n    time = 274000\n    flags = 1\n    data = length 207, hash 5BE231DF\n  sample 8:\n    time = 298000\n    flags = 1\n    data = length 225, hash 18819EE1\n  sample 9:\n    time = 321000\n    flags = 1\n    data = length 215, hash CA7FA67B\n  sample 10:\n    time = 344000\n    flags = 1\n    data = length 211, hash 581A1C18\n  sample 11:\n    time = 367000\n    flags = 1\n    data = length 216, hash ADB88187\n  sample 12:\n    time = 390000\n    flags = 1\n    data = length 229, hash 2E8BA4DC\n  sample 13:\n    time = 414000\n    flags = 1\n    data = length 232, hash 22F0C510\n  sample 14:\n    time = 437000\n    flags = 1\n    data = length 235, hash 867AD0DC\n  sample 15:\n    time = 460000\n    flags = 1\n    data = length 231, hash 84E823A8\n  sample 16:\n    time = 483000\n    flags = 1\n    data = length 226, hash 1BEF3A95\n  sample 17:\n    time = 507000\n    flags = 1\n    data = length 216, hash EAA345AE\n  sample 18:\n    time = 530000\n    flags = 1\n    data = length 229, hash 6957411F\n  sample 19:\n    time = 553000\n    flags = 1\n    data = length 219, hash 41275022\n  sample 20:\n    time = 576000\n    flags = 1\n    data = length 241, hash 6495DF96\n  sample 21:\n    time = 599000\n    flags = 1\n    data = length 228, hash 63D95906\n  sample 22:\n    time = 623000\n    flags = 1\n    data = length 238, hash 34F676F9\n  sample 23:\n    time = 646000\n    flags = 1\n    data = length 234, hash E5CBC045\n  sample 24:\n    time = 669000\n    flags = 1\n    data = length 231, hash 5FC43661\n  sample 25:\n    time = 692000\n    flags = 1\n    data = length 217, hash 682708ED\n  sample 26:\n    time = 716000\n    flags = 1\n    data = length 239, hash D43780FC\n  sample 27:\n    time = 739000\n    flags = 1\n    data = length 243, hash C5E17980\n  sample 28:\n    time = 762000\n    flags = 1\n    data = length 231, hash AC5837BA\n  sample 29:\n    time = 785000\n    flags = 1\n    data = length 230, hash 169EE895\n  sample 30:\n    time = 808000\n    flags = 1\n    data = length 238, hash C48FF3F1\n  sample 31:\n    time = 832000\n    flags = 1\n    data = length 225, hash 531E4599\n  sample 32:\n    time = 855000\n    flags = 1\n    data = length 232, hash CB3C6B8D\n  sample 33:\n    time = 878000\n    flags = 1\n    data = length 243, hash F8C94C7\n  sample 34:\n    time = 901000\n    flags = 1\n    data = length 232, hash A646A7D0\n  sample 35:\n    time = 925000\n    flags = 1\n    data = length 237, hash E8B787A5\n  sample 36:\n    time = 948000\n    flags = 1\n    data = length 228, hash 3FA7A29F\n  sample 37:\n    time = 971000\n    flags = 1\n    data = length 235, hash B9B33B0A\n  sample 38:\n    time = 994000\n    flags = 1\n    data = length 264, hash 71A4869E\n  sample 39:\n    time = 1017000\n    flags = 1\n    data = length 257, hash D049B54C\n  sample 40:\n    time = 1041000\n    flags = 1\n    data = length 227, hash 66757231\n  sample 41:\n    time = 1064000\n    flags = 1\n    data = length 227, hash BD374F1B\n  sample 42:\n    time = 1087000\n    flags = 1\n    data = length 235, hash 999477F6\n  sample 43:\n    time = 1110000\n    flags = 1\n    data = length 229, hash FFF98DF0\n  sample 44:\n    time = 1134000\n    flags = 1\n    data = length 6, hash 31B22286\ntrack 9:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = -1\n    width = 1080\n    height = 720\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 30, hash F6F3D010\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 89502\n  sample count = 30\n  sample 0:\n    time = 67000\n    flags = 1\n    data = length 36477, hash F0F36CFE\n  sample 1:\n    time = 134000\n    flags = 0\n    data = length 5341, hash 40B85E2\n  sample 2:\n    time = 100000\n    flags = 0\n    data = length 596, hash 357B4D92\n  sample 3:\n    time = 267000\n    flags = 0\n    data = length 7704, hash A39EDA06\n  sample 4:\n    time = 200000\n    flags = 0\n    data = length 989, hash 2813C72D\n  sample 5:\n    time = 167000\n    flags = 0\n    data = length 721, hash C50D1C73\n  sample 6:\n    time = 234000\n    flags = 0\n    data = length 519, hash 65FE1911\n  sample 7:\n    time = 400000\n    flags = 0\n    data = length 6160, hash E1CAC0EC\n  sample 8:\n    time = 334000\n    flags = 0\n    data = length 953, hash 7160C661\n  sample 9:\n    time = 300000\n    flags = 0\n    data = length 620, hash 7A7AE07C\n  sample 10:\n    time = 367000\n    flags = 0\n    data = length 405, hash 5CC7F4E7\n  sample 11:\n    time = 500000\n    flags = 0\n    data = length 4852, hash 9DB6979D\n  sample 12:\n    time = 467000\n    flags = 0\n    data = length 547, hash E31A6979\n  sample 13:\n    time = 434000\n    flags = 0\n    data = length 570, hash FEC40D00\n  sample 14:\n    time = 634000\n    flags = 0\n    data = length 5525, hash 7C478F7E\n  sample 15:\n    time = 567000\n    flags = 0\n    data = length 1082, hash DA07059A\n  sample 16:\n    time = 534000\n    flags = 0\n    data = length 807, hash 93478E6B\n  sample 17:\n    time = 600000\n    flags = 0\n    data = length 744, hash 9A8E6026\n  sample 18:\n    time = 767000\n    flags = 0\n    data = length 4732, hash C73B23C0\n  sample 19:\n    time = 700000\n    flags = 0\n    data = length 1004, hash 8A19A228\n  sample 20:\n    time = 667000\n    flags = 0\n    data = length 794, hash 8126022C\n  sample 21:\n    time = 734000\n    flags = 0\n    data = length 645, hash F08300E5\n  sample 22:\n    time = 900000\n    flags = 0\n    data = length 2684, hash 727FE378\n  sample 23:\n    time = 834000\n    flags = 0\n    data = length 787, hash 419A7821\n  sample 24:\n    time = 800000\n    flags = 0\n    data = length 649, hash 5C159346\n  sample 25:\n    time = 867000\n    flags = 0\n    data = length 509, hash F912D655\n  sample 26:\n    time = 1034000\n    flags = 0\n    data = length 1226, hash 29815C21\n  sample 27:\n    time = 967000\n    flags = 0\n    data = length 898, hash D997AD0A\n  sample 28:\n    time = 934000\n    flags = 0\n    data = length 476, hash A0423645\n  sample 29:\n    time = 1000000\n    flags = 0\n    data = length 486, hash DDF32CBB\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mkv/sample.mkv.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1072000\n  getPosition(0) = [[timeUs=67000, position=5576]]\nnumberOfTracks = 2\ntrack 1:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = -1\n    width = 1080\n    height = 720\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 30, hash F6F3D010\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 89502\n  sample count = 30\n  sample 0:\n    time = 67000\n    flags = 1\n    data = length 36477, hash F0F36CFE\n  sample 1:\n    time = 134000\n    flags = 0\n    data = length 5341, hash 40B85E2\n  sample 2:\n    time = 100000\n    flags = 0\n    data = length 596, hash 357B4D92\n  sample 3:\n    time = 267000\n    flags = 0\n    data = length 7704, hash A39EDA06\n  sample 4:\n    time = 200000\n    flags = 0\n    data = length 989, hash 2813C72D\n  sample 5:\n    time = 167000\n    flags = 0\n    data = length 721, hash C50D1C73\n  sample 6:\n    time = 234000\n    flags = 0\n    data = length 519, hash 65FE1911\n  sample 7:\n    time = 400000\n    flags = 0\n    data = length 6160, hash E1CAC0EC\n  sample 8:\n    time = 334000\n    flags = 0\n    data = length 953, hash 7160C661\n  sample 9:\n    time = 300000\n    flags = 0\n    data = length 620, hash 7A7AE07C\n  sample 10:\n    time = 367000\n    flags = 0\n    data = length 405, hash 5CC7F4E7\n  sample 11:\n    time = 500000\n    flags = 0\n    data = length 4852, hash 9DB6979D\n  sample 12:\n    time = 467000\n    flags = 0\n    data = length 547, hash E31A6979\n  sample 13:\n    time = 434000\n    flags = 0\n    data = length 570, hash FEC40D00\n  sample 14:\n    time = 634000\n    flags = 0\n    data = length 5525, hash 7C478F7E\n  sample 15:\n    time = 567000\n    flags = 0\n    data = length 1082, hash DA07059A\n  sample 16:\n    time = 534000\n    flags = 0\n    data = length 807, hash 93478E6B\n  sample 17:\n    time = 600000\n    flags = 0\n    data = length 744, hash 9A8E6026\n  sample 18:\n    time = 767000\n    flags = 0\n    data = length 4732, hash C73B23C0\n  sample 19:\n    time = 700000\n    flags = 0\n    data = length 1004, hash 8A19A228\n  sample 20:\n    time = 667000\n    flags = 0\n    data = length 794, hash 8126022C\n  sample 21:\n    time = 734000\n    flags = 0\n    data = length 645, hash F08300E5\n  sample 22:\n    time = 900000\n    flags = 0\n    data = length 2684, hash 727FE378\n  sample 23:\n    time = 834000\n    flags = 0\n    data = length 787, hash 419A7821\n  sample 24:\n    time = 800000\n    flags = 0\n    data = length 649, hash 5C159346\n  sample 25:\n    time = 867000\n    flags = 0\n    data = length 509, hash F912D655\n  sample 26:\n    time = 1034000\n    flags = 0\n    data = length 1226, hash 29815C21\n  sample 27:\n    time = 967000\n    flags = 0\n    data = length 898, hash D997AD0A\n  sample 28:\n    time = 934000\n    flags = 0\n    data = length 476, hash A0423645\n  sample 29:\n    time = 1000000\n    flags = 0\n    data = length 486, hash DDF32CBB\ntrack 2:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/ac3\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 1\n    language = und\n    drmInitData = -\n    initializationData:\n  total output bytes = 12120\n  sample count = 29\n  sample 0:\n    time = 129000\n    flags = 1\n    data = length 416, hash 211F2286\n  sample 1:\n    time = 164000\n    flags = 1\n    data = length 418, hash 77425A86\n  sample 2:\n    time = 198829\n    flags = 1\n    data = length 418, hash A0FE5CA1\n  sample 3:\n    time = 233000\n    flags = 1\n    data = length 418, hash 2309B066\n  sample 4:\n    time = 268000\n    flags = 1\n    data = length 418, hash 928A653B\n  sample 5:\n    time = 303000\n    flags = 1\n    data = length 418, hash 3422F0CB\n  sample 6:\n    time = 337829\n    flags = 1\n    data = length 418, hash EFF43D5B\n  sample 7:\n    time = 373000\n    flags = 1\n    data = length 418, hash FC8093C7\n  sample 8:\n    time = 408000\n    flags = 1\n    data = length 418, hash CCC08A16\n  sample 9:\n    time = 443000\n    flags = 1\n    data = length 418, hash 2A6EE863\n  sample 10:\n    time = 477829\n    flags = 1\n    data = length 418, hash D69A9251\n  sample 11:\n    time = 512000\n    flags = 1\n    data = length 418, hash BCFB758D\n  sample 12:\n    time = 547000\n    flags = 1\n    data = length 418, hash 11B66799\n  sample 13:\n    time = 581829\n    flags = 1\n    data = length 418, hash C824D392\n  sample 14:\n    time = 617000\n    flags = 1\n    data = length 418, hash C167D872\n  sample 15:\n    time = 652000\n    flags = 1\n    data = length 418, hash 4221C855\n  sample 16:\n    time = 687000\n    flags = 1\n    data = length 418, hash 4D4FF934\n  sample 17:\n    time = 721829\n    flags = 1\n    data = length 418, hash 984AA025\n  sample 18:\n    time = 757000\n    flags = 1\n    data = length 418, hash BB788B46\n  sample 19:\n    time = 791000\n    flags = 1\n    data = length 418, hash 9EFBFD97\n  sample 20:\n    time = 826000\n    flags = 1\n    data = length 418, hash DF1A460C\n  sample 21:\n    time = 860829\n    flags = 1\n    data = length 418, hash 2BDB56A\n  sample 22:\n    time = 896000\n    flags = 1\n    data = length 418, hash CA230060\n  sample 23:\n    time = 931000\n    flags = 1\n    data = length 418, hash D2F19F41\n  sample 24:\n    time = 965000\n    flags = 1\n    data = length 418, hash AF392D79\n  sample 25:\n    time = 999829\n    flags = 1\n    data = length 418, hash C5D7F2A3\n  sample 26:\n    time = 1035000\n    flags = 1\n    data = length 418, hash 733A35AE\n  sample 27:\n    time = 1069829\n    flags = 1\n    data = length 418, hash DE46E5D3\n  sample 28:\n    time = 1104000\n    flags = 1\n    data = length 418, hash 56AB8D37\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mkv/sample.mkv.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1072000\n  getPosition(0) = [[timeUs=67000, position=5576]]\nnumberOfTracks = 2\ntrack 1:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = -1\n    width = 1080\n    height = 720\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 30, hash F6F3D010\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 30995\n  sample count = 22\n  sample 0:\n    time = 334000\n    flags = 0\n    data = length 953, hash 7160C661\n  sample 1:\n    time = 300000\n    flags = 0\n    data = length 620, hash 7A7AE07C\n  sample 2:\n    time = 367000\n    flags = 0\n    data = length 405, hash 5CC7F4E7\n  sample 3:\n    time = 500000\n    flags = 0\n    data = length 4852, hash 9DB6979D\n  sample 4:\n    time = 467000\n    flags = 0\n    data = length 547, hash E31A6979\n  sample 5:\n    time = 434000\n    flags = 0\n    data = length 570, hash FEC40D00\n  sample 6:\n    time = 634000\n    flags = 0\n    data = length 5525, hash 7C478F7E\n  sample 7:\n    time = 567000\n    flags = 0\n    data = length 1082, hash DA07059A\n  sample 8:\n    time = 534000\n    flags = 0\n    data = length 807, hash 93478E6B\n  sample 9:\n    time = 600000\n    flags = 0\n    data = length 744, hash 9A8E6026\n  sample 10:\n    time = 767000\n    flags = 0\n    data = length 4732, hash C73B23C0\n  sample 11:\n    time = 700000\n    flags = 0\n    data = length 1004, hash 8A19A228\n  sample 12:\n    time = 667000\n    flags = 0\n    data = length 794, hash 8126022C\n  sample 13:\n    time = 734000\n    flags = 0\n    data = length 645, hash F08300E5\n  sample 14:\n    time = 900000\n    flags = 0\n    data = length 2684, hash 727FE378\n  sample 15:\n    time = 834000\n    flags = 0\n    data = length 787, hash 419A7821\n  sample 16:\n    time = 800000\n    flags = 0\n    data = length 649, hash 5C159346\n  sample 17:\n    time = 867000\n    flags = 0\n    data = length 509, hash F912D655\n  sample 18:\n    time = 1034000\n    flags = 0\n    data = length 1226, hash 29815C21\n  sample 19:\n    time = 967000\n    flags = 0\n    data = length 898, hash D997AD0A\n  sample 20:\n    time = 934000\n    flags = 0\n    data = length 476, hash A0423645\n  sample 21:\n    time = 1000000\n    flags = 0\n    data = length 486, hash DDF32CBB\ntrack 2:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/ac3\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 1\n    language = und\n    drmInitData = -\n    initializationData:\n  total output bytes = 8778\n  sample count = 21\n  sample 0:\n    time = 408000\n    flags = 1\n    data = length 418, hash CCC08A16\n  sample 1:\n    time = 443000\n    flags = 1\n    data = length 418, hash 2A6EE863\n  sample 2:\n    time = 477829\n    flags = 1\n    data = length 418, hash D69A9251\n  sample 3:\n    time = 512000\n    flags = 1\n    data = length 418, hash BCFB758D\n  sample 4:\n    time = 547000\n    flags = 1\n    data = length 418, hash 11B66799\n  sample 5:\n    time = 581829\n    flags = 1\n    data = length 418, hash C824D392\n  sample 6:\n    time = 617000\n    flags = 1\n    data = length 418, hash C167D872\n  sample 7:\n    time = 652000\n    flags = 1\n    data = length 418, hash 4221C855\n  sample 8:\n    time = 687000\n    flags = 1\n    data = length 418, hash 4D4FF934\n  sample 9:\n    time = 721829\n    flags = 1\n    data = length 418, hash 984AA025\n  sample 10:\n    time = 757000\n    flags = 1\n    data = length 418, hash BB788B46\n  sample 11:\n    time = 791000\n    flags = 1\n    data = length 418, hash 9EFBFD97\n  sample 12:\n    time = 826000\n    flags = 1\n    data = length 418, hash DF1A460C\n  sample 13:\n    time = 860829\n    flags = 1\n    data = length 418, hash 2BDB56A\n  sample 14:\n    time = 896000\n    flags = 1\n    data = length 418, hash CA230060\n  sample 15:\n    time = 931000\n    flags = 1\n    data = length 418, hash D2F19F41\n  sample 16:\n    time = 965000\n    flags = 1\n    data = length 418, hash AF392D79\n  sample 17:\n    time = 999829\n    flags = 1\n    data = length 418, hash C5D7F2A3\n  sample 18:\n    time = 1035000\n    flags = 1\n    data = length 418, hash 733A35AE\n  sample 19:\n    time = 1069829\n    flags = 1\n    data = length 418, hash DE46E5D3\n  sample 20:\n    time = 1104000\n    flags = 1\n    data = length 418, hash 56AB8D37\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mkv/sample.mkv.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1072000\n  getPosition(0) = [[timeUs=67000, position=5576]]\nnumberOfTracks = 2\ntrack 1:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = -1\n    width = 1080\n    height = 720\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 30, hash F6F3D010\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 10158\n  sample count = 11\n  sample 0:\n    time = 700000\n    flags = 0\n    data = length 1004, hash 8A19A228\n  sample 1:\n    time = 667000\n    flags = 0\n    data = length 794, hash 8126022C\n  sample 2:\n    time = 734000\n    flags = 0\n    data = length 645, hash F08300E5\n  sample 3:\n    time = 900000\n    flags = 0\n    data = length 2684, hash 727FE378\n  sample 4:\n    time = 834000\n    flags = 0\n    data = length 787, hash 419A7821\n  sample 5:\n    time = 800000\n    flags = 0\n    data = length 649, hash 5C159346\n  sample 6:\n    time = 867000\n    flags = 0\n    data = length 509, hash F912D655\n  sample 7:\n    time = 1034000\n    flags = 0\n    data = length 1226, hash 29815C21\n  sample 8:\n    time = 967000\n    flags = 0\n    data = length 898, hash D997AD0A\n  sample 9:\n    time = 934000\n    flags = 0\n    data = length 476, hash A0423645\n  sample 10:\n    time = 1000000\n    flags = 0\n    data = length 486, hash DDF32CBB\ntrack 2:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/ac3\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 1\n    language = und\n    drmInitData = -\n    initializationData:\n  total output bytes = 4180\n  sample count = 10\n  sample 0:\n    time = 791000\n    flags = 1\n    data = length 418, hash 9EFBFD97\n  sample 1:\n    time = 826000\n    flags = 1\n    data = length 418, hash DF1A460C\n  sample 2:\n    time = 860829\n    flags = 1\n    data = length 418, hash 2BDB56A\n  sample 3:\n    time = 896000\n    flags = 1\n    data = length 418, hash CA230060\n  sample 4:\n    time = 931000\n    flags = 1\n    data = length 418, hash D2F19F41\n  sample 5:\n    time = 965000\n    flags = 1\n    data = length 418, hash AF392D79\n  sample 6:\n    time = 999829\n    flags = 1\n    data = length 418, hash C5D7F2A3\n  sample 7:\n    time = 1035000\n    flags = 1\n    data = length 418, hash 733A35AE\n  sample 8:\n    time = 1069829\n    flags = 1\n    data = length 418, hash DE46E5D3\n  sample 9:\n    time = 1104000\n    flags = 1\n    data = length 418, hash 56AB8D37\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mkv/sample.mkv.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1072000\n  getPosition(0) = [[timeUs=67000, position=5576]]\nnumberOfTracks = 2\ntrack 1:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = -1\n    width = 1080\n    height = 720\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 30, hash F6F3D010\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 0\n  sample count = 0\ntrack 2:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/ac3\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 1\n    language = und\n    drmInitData = -\n    initializationData:\n  total output bytes = 1254\n  sample count = 3\n  sample 0:\n    time = 1035000\n    flags = 1\n    data = length 418, hash 733A35AE\n  sample 1:\n    time = 1069829\n    flags = 1\n    data = length 418, hash DE46E5D3\n  sample 2:\n    time = 1104000\n    flags = 1\n    data = length 418, hash 56AB8D37\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mkv/subsample_encrypted_altref.webm.0.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = 1000\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 1:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/x-vnd.on2.vp9\n    maxInputSize = -1\n    width = 360\n    height = 240\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = 1305012705\n    initializationData:\n  total output bytes = 39\n  sample count = 1\n  sample 0:\n    time = 0\n    flags = 1073741824\n    data = length 39, hash B7FE77F4\n    crypto mode = 1\n    encryption key = length 16, hash 4CE944CF\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mkv/subsample_encrypted_noaltref.webm.0.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = 1000\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 1:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/x-vnd.on2.vp9\n    maxInputSize = -1\n    width = 360\n    height = 240\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = 1305012705\n    initializationData:\n  total output bytes = 24\n  sample count = 1\n  sample 0:\n    time = 0\n    flags = 1073741824\n    data = length 24, hash E58668B1\n    crypto mode = 1\n    encryption key = length 16, hash 4CE944CF\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp3/bear.mp3.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2784000\n  getPosition(0) = [[timeUs=0, position=201]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/mpeg\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 956\n    encoderPadding = 3352\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 44544\n  sample count = 116\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 384, hash B1FBF8BD\n  sample 1:\n    time = 24000\n    flags = 1\n    data = length 384, hash 2B9A3B72\n  sample 2:\n    time = 48000\n    flags = 1\n    data = length 384, hash 33C65BA6\n  sample 3:\n    time = 72000\n    flags = 1\n    data = length 384, hash E64FE475\n  sample 4:\n    time = 96000\n    flags = 1\n    data = length 384, hash E9122D34\n  sample 5:\n    time = 120000\n    flags = 1\n    data = length 384, hash 9CC87327\n  sample 6:\n    time = 144000\n    flags = 1\n    data = length 384, hash 118CF6DA\n  sample 7:\n    time = 168000\n    flags = 1\n    data = length 384, hash 9610D9D6\n  sample 8:\n    time = 192000\n    flags = 1\n    data = length 384, hash 6ABFE405\n  sample 9:\n    time = 216000\n    flags = 1\n    data = length 384, hash EE5C93A9\n  sample 10:\n    time = 240000\n    flags = 1\n    data = length 384, hash 44E0D140\n  sample 11:\n    time = 264000\n    flags = 1\n    data = length 384, hash 3B3DE1D6\n  sample 12:\n    time = 288000\n    flags = 1\n    data = length 384, hash 3A572E7C\n  sample 13:\n    time = 312000\n    flags = 1\n    data = length 384, hash 240316E1\n  sample 14:\n    time = 336000\n    flags = 1\n    data = length 384, hash 9EDA9AA0\n  sample 15:\n    time = 360000\n    flags = 1\n    data = length 384, hash E31AB44F\n  sample 16:\n    time = 384000\n    flags = 1\n    data = length 384, hash A12497D6\n  sample 17:\n    time = 408000\n    flags = 1\n    data = length 384, hash 8A179B75\n  sample 18:\n    time = 432000\n    flags = 1\n    data = length 384, hash FCE9E107\n  sample 19:\n    time = 456000\n    flags = 1\n    data = length 384, hash 52CA9665\n  sample 20:\n    time = 480000\n    flags = 1\n    data = length 384, hash 9935EC4C\n  sample 21:\n    time = 504000\n    flags = 1\n    data = length 384, hash 33CA710A\n  sample 22:\n    time = 528000\n    flags = 1\n    data = length 384, hash 45B5D69\n  sample 23:\n    time = 552000\n    flags = 1\n    data = length 384, hash 7CEC655D\n  sample 24:\n    time = 576000\n    flags = 1\n    data = length 384, hash 3B5D8310\n  sample 25:\n    time = 600000\n    flags = 1\n    data = length 384, hash 3EB640F8\n  sample 26:\n    time = 624000\n    flags = 1\n    data = length 384, hash FAEC53B4\n  sample 27:\n    time = 648000\n    flags = 1\n    data = length 384, hash 92C8A6EE\n  sample 28:\n    time = 672000\n    flags = 1\n    data = length 384, hash 7CBAAE91\n  sample 29:\n    time = 696000\n    flags = 1\n    data = length 384, hash 74AC754E\n  sample 30:\n    time = 720000\n    flags = 1\n    data = length 384, hash 8242C434\n  sample 31:\n    time = 744000\n    flags = 1\n    data = length 384, hash 686C06FB\n  sample 32:\n    time = 768000\n    flags = 1\n    data = length 384, hash 1D872A3F\n  sample 33:\n    time = 792000\n    flags = 1\n    data = length 384, hash 900A20BC\n  sample 34:\n    time = 816000\n    flags = 1\n    data = length 384, hash B72FD8E7\n  sample 35:\n    time = 840000\n    flags = 1\n    data = length 384, hash 85C9A1FB\n  sample 36:\n    time = 864000\n    flags = 1\n    data = length 384, hash 1600DF3\n  sample 37:\n    time = 888000\n    flags = 1\n    data = length 384, hash D6C2138A\n  sample 38:\n    time = 912000\n    flags = 1\n    data = length 384, hash 737BA69E\n  sample 39:\n    time = 936000\n    flags = 1\n    data = length 384, hash F7E344F4\n  sample 40:\n    time = 960000\n    flags = 1\n    data = length 384, hash 14EF6AFD\n  sample 41:\n    time = 984000\n    flags = 1\n    data = length 384, hash 61C9B92C\n  sample 42:\n    time = 1008000\n    flags = 1\n    data = length 384, hash ABE1368\n  sample 43:\n    time = 1032000\n    flags = 1\n    data = length 384, hash 6A3B8547\n  sample 44:\n    time = 1056000\n    flags = 1\n    data = length 384, hash 30E905FA\n  sample 45:\n    time = 1080000\n    flags = 1\n    data = length 384, hash 21A267CD\n  sample 46:\n    time = 1104000\n    flags = 1\n    data = length 384, hash D96A2651\n  sample 47:\n    time = 1128000\n    flags = 1\n    data = length 384, hash 72340177\n  sample 48:\n    time = 1152000\n    flags = 1\n    data = length 384, hash 9345E744\n  sample 49:\n    time = 1176000\n    flags = 1\n    data = length 384, hash FDE39E3A\n  sample 50:\n    time = 1200000\n    flags = 1\n    data = length 384, hash F0B7465\n  sample 51:\n    time = 1224000\n    flags = 1\n    data = length 384, hash 3693AB86\n  sample 52:\n    time = 1248000\n    flags = 1\n    data = length 384, hash F39719B1\n  sample 53:\n    time = 1272000\n    flags = 1\n    data = length 384, hash DA3958DC\n  sample 54:\n    time = 1296000\n    flags = 1\n    data = length 384, hash FDC7599F\n  sample 55:\n    time = 1320000\n    flags = 1\n    data = length 384, hash AEFF8471\n  sample 56:\n    time = 1344000\n    flags = 1\n    data = length 384, hash 89C92C19\n  sample 57:\n    time = 1368000\n    flags = 1\n    data = length 384, hash 5C786A4B\n  sample 58:\n    time = 1392000\n    flags = 1\n    data = length 384, hash 5ACA8B\n  sample 59:\n    time = 1416000\n    flags = 1\n    data = length 384, hash 7755974C\n  sample 60:\n    time = 1440000\n    flags = 1\n    data = length 384, hash 3934B73C\n  sample 61:\n    time = 1464000\n    flags = 1\n    data = length 384, hash DDD70A2F\n  sample 62:\n    time = 1488000\n    flags = 1\n    data = length 384, hash 8FACE2EF\n  sample 63:\n    time = 1512000\n    flags = 1\n    data = length 384, hash 4A602591\n  sample 64:\n    time = 1536000\n    flags = 1\n    data = length 384, hash D019AA2D\n  sample 65:\n    time = 1560000\n    flags = 1\n    data = length 384, hash 8A680B9D\n  sample 66:\n    time = 1584000\n    flags = 1\n    data = length 384, hash B655C959\n  sample 67:\n    time = 1608000\n    flags = 1\n    data = length 384, hash 2168336B\n  sample 68:\n    time = 1632000\n    flags = 1\n    data = length 384, hash D77F6D31\n  sample 69:\n    time = 1656000\n    flags = 1\n    data = length 384, hash 524B4B2F\n  sample 70:\n    time = 1680000\n    flags = 1\n    data = length 384, hash 4752DDFC\n  sample 71:\n    time = 1704000\n    flags = 1\n    data = length 384, hash E786727F\n  sample 72:\n    time = 1728000\n    flags = 1\n    data = length 384, hash 5DA6FB8C\n  sample 73:\n    time = 1752000\n    flags = 1\n    data = length 384, hash 92F24269\n  sample 74:\n    time = 1776000\n    flags = 1\n    data = length 384, hash CD0A3BA1\n  sample 75:\n    time = 1800000\n    flags = 1\n    data = length 384, hash 7D00409F\n  sample 76:\n    time = 1824000\n    flags = 1\n    data = length 384, hash D7ADB5FA\n  sample 77:\n    time = 1848000\n    flags = 1\n    data = length 384, hash 4A140209\n  sample 78:\n    time = 1872000\n    flags = 1\n    data = length 384, hash E801184A\n  sample 79:\n    time = 1896000\n    flags = 1\n    data = length 384, hash 53C6CF9C\n  sample 80:\n    time = 1920000\n    flags = 1\n    data = length 384, hash 19A8D99F\n  sample 81:\n    time = 1944000\n    flags = 1\n    data = length 384, hash E47EB43F\n  sample 82:\n    time = 1968000\n    flags = 1\n    data = length 384, hash 4EA329E7\n  sample 83:\n    time = 1992000\n    flags = 1\n    data = length 384, hash 1CCAAE62\n  sample 84:\n    time = 2016000\n    flags = 1\n    data = length 384, hash ED3F8C66\n  sample 85:\n    time = 2040000\n    flags = 1\n    data = length 384, hash D3D646B6\n  sample 86:\n    time = 2064000\n    flags = 1\n    data = length 384, hash 68CD1574\n  sample 87:\n    time = 2088000\n    flags = 1\n    data = length 384, hash 8CEAB382\n  sample 88:\n    time = 2112000\n    flags = 1\n    data = length 384, hash D54B1C48\n  sample 89:\n    time = 2136000\n    flags = 1\n    data = length 384, hash FFE2EE90\n  sample 90:\n    time = 2160000\n    flags = 1\n    data = length 384, hash BFE8A673\n  sample 91:\n    time = 2184000\n    flags = 1\n    data = length 384, hash 978B1C92\n  sample 92:\n    time = 2208000\n    flags = 1\n    data = length 384, hash 810CC71E\n  sample 93:\n    time = 2232000\n    flags = 1\n    data = length 384, hash 44FE42D9\n  sample 94:\n    time = 2256000\n    flags = 1\n    data = length 384, hash 2F5BB02C\n  sample 95:\n    time = 2280000\n    flags = 1\n    data = length 384, hash 77DDB90\n  sample 96:\n    time = 2304000\n    flags = 1\n    data = length 384, hash 24FB5EDA\n  sample 97:\n    time = 2328000\n    flags = 1\n    data = length 384, hash E73203C6\n  sample 98:\n    time = 2352000\n    flags = 1\n    data = length 384, hash 14B525F1\n  sample 99:\n    time = 2376000\n    flags = 1\n    data = length 384, hash 5E0F4E2E\n  sample 100:\n    time = 2400000\n    flags = 1\n    data = length 384, hash 67EE4E31\n  sample 101:\n    time = 2424000\n    flags = 1\n    data = length 384, hash 2E04EC4C\n  sample 102:\n    time = 2448000\n    flags = 1\n    data = length 384, hash 852CABA7\n  sample 103:\n    time = 2472000\n    flags = 1\n    data = length 384, hash 19928903\n  sample 104:\n    time = 2496000\n    flags = 1\n    data = length 384, hash 5DA42021\n  sample 105:\n    time = 2520000\n    flags = 1\n    data = length 384, hash 45B20B7C\n  sample 106:\n    time = 2544000\n    flags = 1\n    data = length 384, hash D108A215\n  sample 107:\n    time = 2568000\n    flags = 1\n    data = length 384, hash BD25DB7C\n  sample 108:\n    time = 2592000\n    flags = 1\n    data = length 384, hash DA7F9861\n  sample 109:\n    time = 2616000\n    flags = 1\n    data = length 384, hash CCD576F\n  sample 110:\n    time = 2640000\n    flags = 1\n    data = length 384, hash 405C1EB5\n  sample 111:\n    time = 2664000\n    flags = 1\n    data = length 384, hash 6640B74E\n  sample 112:\n    time = 2688000\n    flags = 1\n    data = length 384, hash B4E5937A\n  sample 113:\n    time = 2712000\n    flags = 1\n    data = length 384, hash CEE17733\n  sample 114:\n    time = 2736000\n    flags = 1\n    data = length 384, hash 2A0DA733\n  sample 115:\n    time = 2760000\n    flags = 1\n    data = length 384, hash 97F4129B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp3/bear.mp3.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2784000\n  getPosition(0) = [[timeUs=0, position=201]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/mpeg\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 956\n    encoderPadding = 3352\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 29568\n  sample count = 77\n  sample 0:\n    time = 928568\n    flags = 1\n    data = length 384, hash F7E344F4\n  sample 1:\n    time = 952568\n    flags = 1\n    data = length 384, hash 14EF6AFD\n  sample 2:\n    time = 976568\n    flags = 1\n    data = length 384, hash 61C9B92C\n  sample 3:\n    time = 1000568\n    flags = 1\n    data = length 384, hash ABE1368\n  sample 4:\n    time = 1024568\n    flags = 1\n    data = length 384, hash 6A3B8547\n  sample 5:\n    time = 1048568\n    flags = 1\n    data = length 384, hash 30E905FA\n  sample 6:\n    time = 1072568\n    flags = 1\n    data = length 384, hash 21A267CD\n  sample 7:\n    time = 1096568\n    flags = 1\n    data = length 384, hash D96A2651\n  sample 8:\n    time = 1120568\n    flags = 1\n    data = length 384, hash 72340177\n  sample 9:\n    time = 1144568\n    flags = 1\n    data = length 384, hash 9345E744\n  sample 10:\n    time = 1168568\n    flags = 1\n    data = length 384, hash FDE39E3A\n  sample 11:\n    time = 1192568\n    flags = 1\n    data = length 384, hash F0B7465\n  sample 12:\n    time = 1216568\n    flags = 1\n    data = length 384, hash 3693AB86\n  sample 13:\n    time = 1240568\n    flags = 1\n    data = length 384, hash F39719B1\n  sample 14:\n    time = 1264568\n    flags = 1\n    data = length 384, hash DA3958DC\n  sample 15:\n    time = 1288568\n    flags = 1\n    data = length 384, hash FDC7599F\n  sample 16:\n    time = 1312568\n    flags = 1\n    data = length 384, hash AEFF8471\n  sample 17:\n    time = 1336568\n    flags = 1\n    data = length 384, hash 89C92C19\n  sample 18:\n    time = 1360568\n    flags = 1\n    data = length 384, hash 5C786A4B\n  sample 19:\n    time = 1384568\n    flags = 1\n    data = length 384, hash 5ACA8B\n  sample 20:\n    time = 1408568\n    flags = 1\n    data = length 384, hash 7755974C\n  sample 21:\n    time = 1432568\n    flags = 1\n    data = length 384, hash 3934B73C\n  sample 22:\n    time = 1456568\n    flags = 1\n    data = length 384, hash DDD70A2F\n  sample 23:\n    time = 1480568\n    flags = 1\n    data = length 384, hash 8FACE2EF\n  sample 24:\n    time = 1504568\n    flags = 1\n    data = length 384, hash 4A602591\n  sample 25:\n    time = 1528568\n    flags = 1\n    data = length 384, hash D019AA2D\n  sample 26:\n    time = 1552568\n    flags = 1\n    data = length 384, hash 8A680B9D\n  sample 27:\n    time = 1576568\n    flags = 1\n    data = length 384, hash B655C959\n  sample 28:\n    time = 1600568\n    flags = 1\n    data = length 384, hash 2168336B\n  sample 29:\n    time = 1624568\n    flags = 1\n    data = length 384, hash D77F6D31\n  sample 30:\n    time = 1648568\n    flags = 1\n    data = length 384, hash 524B4B2F\n  sample 31:\n    time = 1672568\n    flags = 1\n    data = length 384, hash 4752DDFC\n  sample 32:\n    time = 1696568\n    flags = 1\n    data = length 384, hash E786727F\n  sample 33:\n    time = 1720568\n    flags = 1\n    data = length 384, hash 5DA6FB8C\n  sample 34:\n    time = 1744568\n    flags = 1\n    data = length 384, hash 92F24269\n  sample 35:\n    time = 1768568\n    flags = 1\n    data = length 384, hash CD0A3BA1\n  sample 36:\n    time = 1792568\n    flags = 1\n    data = length 384, hash 7D00409F\n  sample 37:\n    time = 1816568\n    flags = 1\n    data = length 384, hash D7ADB5FA\n  sample 38:\n    time = 1840568\n    flags = 1\n    data = length 384, hash 4A140209\n  sample 39:\n    time = 1864568\n    flags = 1\n    data = length 384, hash E801184A\n  sample 40:\n    time = 1888568\n    flags = 1\n    data = length 384, hash 53C6CF9C\n  sample 41:\n    time = 1912568\n    flags = 1\n    data = length 384, hash 19A8D99F\n  sample 42:\n    time = 1936568\n    flags = 1\n    data = length 384, hash E47EB43F\n  sample 43:\n    time = 1960568\n    flags = 1\n    data = length 384, hash 4EA329E7\n  sample 44:\n    time = 1984568\n    flags = 1\n    data = length 384, hash 1CCAAE62\n  sample 45:\n    time = 2008568\n    flags = 1\n    data = length 384, hash ED3F8C66\n  sample 46:\n    time = 2032568\n    flags = 1\n    data = length 384, hash D3D646B6\n  sample 47:\n    time = 2056568\n    flags = 1\n    data = length 384, hash 68CD1574\n  sample 48:\n    time = 2080568\n    flags = 1\n    data = length 384, hash 8CEAB382\n  sample 49:\n    time = 2104568\n    flags = 1\n    data = length 384, hash D54B1C48\n  sample 50:\n    time = 2128568\n    flags = 1\n    data = length 384, hash FFE2EE90\n  sample 51:\n    time = 2152568\n    flags = 1\n    data = length 384, hash BFE8A673\n  sample 52:\n    time = 2176568\n    flags = 1\n    data = length 384, hash 978B1C92\n  sample 53:\n    time = 2200568\n    flags = 1\n    data = length 384, hash 810CC71E\n  sample 54:\n    time = 2224568\n    flags = 1\n    data = length 384, hash 44FE42D9\n  sample 55:\n    time = 2248568\n    flags = 1\n    data = length 384, hash 2F5BB02C\n  sample 56:\n    time = 2272568\n    flags = 1\n    data = length 384, hash 77DDB90\n  sample 57:\n    time = 2296568\n    flags = 1\n    data = length 384, hash 24FB5EDA\n  sample 58:\n    time = 2320568\n    flags = 1\n    data = length 384, hash E73203C6\n  sample 59:\n    time = 2344568\n    flags = 1\n    data = length 384, hash 14B525F1\n  sample 60:\n    time = 2368568\n    flags = 1\n    data = length 384, hash 5E0F4E2E\n  sample 61:\n    time = 2392568\n    flags = 1\n    data = length 384, hash 67EE4E31\n  sample 62:\n    time = 2416568\n    flags = 1\n    data = length 384, hash 2E04EC4C\n  sample 63:\n    time = 2440568\n    flags = 1\n    data = length 384, hash 852CABA7\n  sample 64:\n    time = 2464568\n    flags = 1\n    data = length 384, hash 19928903\n  sample 65:\n    time = 2488568\n    flags = 1\n    data = length 384, hash 5DA42021\n  sample 66:\n    time = 2512568\n    flags = 1\n    data = length 384, hash 45B20B7C\n  sample 67:\n    time = 2536568\n    flags = 1\n    data = length 384, hash D108A215\n  sample 68:\n    time = 2560568\n    flags = 1\n    data = length 384, hash BD25DB7C\n  sample 69:\n    time = 2584568\n    flags = 1\n    data = length 384, hash DA7F9861\n  sample 70:\n    time = 2608568\n    flags = 1\n    data = length 384, hash CCD576F\n  sample 71:\n    time = 2632568\n    flags = 1\n    data = length 384, hash 405C1EB5\n  sample 72:\n    time = 2656568\n    flags = 1\n    data = length 384, hash 6640B74E\n  sample 73:\n    time = 2680568\n    flags = 1\n    data = length 384, hash B4E5937A\n  sample 74:\n    time = 2704568\n    flags = 1\n    data = length 384, hash CEE17733\n  sample 75:\n    time = 2728568\n    flags = 1\n    data = length 384, hash 2A0DA733\n  sample 76:\n    time = 2752568\n    flags = 1\n    data = length 384, hash 97F4129B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp3/bear.mp3.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2784000\n  getPosition(0) = [[timeUs=0, position=201]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/mpeg\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 956\n    encoderPadding = 3352\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 14592\n  sample count = 38\n  sample 0:\n    time = 1871586\n    flags = 1\n    data = length 384, hash E801184A\n  sample 1:\n    time = 1895586\n    flags = 1\n    data = length 384, hash 53C6CF9C\n  sample 2:\n    time = 1919586\n    flags = 1\n    data = length 384, hash 19A8D99F\n  sample 3:\n    time = 1943586\n    flags = 1\n    data = length 384, hash E47EB43F\n  sample 4:\n    time = 1967586\n    flags = 1\n    data = length 384, hash 4EA329E7\n  sample 5:\n    time = 1991586\n    flags = 1\n    data = length 384, hash 1CCAAE62\n  sample 6:\n    time = 2015586\n    flags = 1\n    data = length 384, hash ED3F8C66\n  sample 7:\n    time = 2039586\n    flags = 1\n    data = length 384, hash D3D646B6\n  sample 8:\n    time = 2063586\n    flags = 1\n    data = length 384, hash 68CD1574\n  sample 9:\n    time = 2087586\n    flags = 1\n    data = length 384, hash 8CEAB382\n  sample 10:\n    time = 2111586\n    flags = 1\n    data = length 384, hash D54B1C48\n  sample 11:\n    time = 2135586\n    flags = 1\n    data = length 384, hash FFE2EE90\n  sample 12:\n    time = 2159586\n    flags = 1\n    data = length 384, hash BFE8A673\n  sample 13:\n    time = 2183586\n    flags = 1\n    data = length 384, hash 978B1C92\n  sample 14:\n    time = 2207586\n    flags = 1\n    data = length 384, hash 810CC71E\n  sample 15:\n    time = 2231586\n    flags = 1\n    data = length 384, hash 44FE42D9\n  sample 16:\n    time = 2255586\n    flags = 1\n    data = length 384, hash 2F5BB02C\n  sample 17:\n    time = 2279586\n    flags = 1\n    data = length 384, hash 77DDB90\n  sample 18:\n    time = 2303586\n    flags = 1\n    data = length 384, hash 24FB5EDA\n  sample 19:\n    time = 2327586\n    flags = 1\n    data = length 384, hash E73203C6\n  sample 20:\n    time = 2351586\n    flags = 1\n    data = length 384, hash 14B525F1\n  sample 21:\n    time = 2375586\n    flags = 1\n    data = length 384, hash 5E0F4E2E\n  sample 22:\n    time = 2399586\n    flags = 1\n    data = length 384, hash 67EE4E31\n  sample 23:\n    time = 2423586\n    flags = 1\n    data = length 384, hash 2E04EC4C\n  sample 24:\n    time = 2447586\n    flags = 1\n    data = length 384, hash 852CABA7\n  sample 25:\n    time = 2471586\n    flags = 1\n    data = length 384, hash 19928903\n  sample 26:\n    time = 2495586\n    flags = 1\n    data = length 384, hash 5DA42021\n  sample 27:\n    time = 2519586\n    flags = 1\n    data = length 384, hash 45B20B7C\n  sample 28:\n    time = 2543586\n    flags = 1\n    data = length 384, hash D108A215\n  sample 29:\n    time = 2567586\n    flags = 1\n    data = length 384, hash BD25DB7C\n  sample 30:\n    time = 2591586\n    flags = 1\n    data = length 384, hash DA7F9861\n  sample 31:\n    time = 2615586\n    flags = 1\n    data = length 384, hash CCD576F\n  sample 32:\n    time = 2639586\n    flags = 1\n    data = length 384, hash 405C1EB5\n  sample 33:\n    time = 2663586\n    flags = 1\n    data = length 384, hash 6640B74E\n  sample 34:\n    time = 2687586\n    flags = 1\n    data = length 384, hash B4E5937A\n  sample 35:\n    time = 2711586\n    flags = 1\n    data = length 384, hash CEE17733\n  sample 36:\n    time = 2735586\n    flags = 1\n    data = length 384, hash 2A0DA733\n  sample 37:\n    time = 2759586\n    flags = 1\n    data = length 384, hash 97F4129B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp3/bear.mp3.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2784000\n  getPosition(0) = [[timeUs=0, position=201]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/mpeg\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 956\n    encoderPadding = 3352\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 26125\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/mpeg\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 418\n  sample count = 1\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 418, hash B819987\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 26125\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/mpeg\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 418\n  sample count = 1\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 418, hash B819987\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 26125\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/mpeg\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 418\n  sample count = 1\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 418, hash B819987\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 26125\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/mpeg\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 418\n  sample count = 1\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 418, hash B819987\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp3/play-trimmed.mp3.unklen.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/mpeg\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 418\n  sample count = 1\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 418, hash B819987\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp4/sample.mp4.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1024000\n  getPosition(0) = [[timeUs=0, position=48]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = 36722\n    width = 1080\n    height = 720\n    frameRate = 29.970028\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 29, hash 4746B5D9\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 89876\n  sample count = 30\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 36692, hash D216076E\n  sample 1:\n    time = 66733\n    flags = 0\n    data = length 5312, hash D45D3CA0\n  sample 2:\n    time = 33366\n    flags = 0\n    data = length 599, hash 1BE7812D\n  sample 3:\n    time = 200200\n    flags = 0\n    data = length 7735, hash 4490F110\n  sample 4:\n    time = 133466\n    flags = 0\n    data = length 987, hash 560B5036\n  sample 5:\n    time = 100100\n    flags = 0\n    data = length 673, hash ED7CD8C7\n  sample 6:\n    time = 166833\n    flags = 0\n    data = length 523, hash 3020DF50\n  sample 7:\n    time = 333666\n    flags = 0\n    data = length 6061, hash 736C72B2\n  sample 8:\n    time = 266933\n    flags = 0\n    data = length 992, hash FE132F23\n  sample 9:\n    time = 233566\n    flags = 0\n    data = length 623, hash 5B2C1816\n  sample 10:\n    time = 300300\n    flags = 0\n    data = length 421, hash 742E69C1\n  sample 11:\n    time = 433766\n    flags = 0\n    data = length 4899, hash F72F86A1\n  sample 12:\n    time = 400400\n    flags = 0\n    data = length 568, hash 519A8E50\n  sample 13:\n    time = 367033\n    flags = 0\n    data = length 620, hash 3990AA39\n  sample 14:\n    time = 567233\n    flags = 0\n    data = length 5450, hash F06EC4AA\n  sample 15:\n    time = 500500\n    flags = 0\n    data = length 1051, hash 92DFA63A\n  sample 16:\n    time = 467133\n    flags = 0\n    data = length 874, hash 69587FB4\n  sample 17:\n    time = 533866\n    flags = 0\n    data = length 781, hash 36BE495B\n  sample 18:\n    time = 700700\n    flags = 0\n    data = length 4725, hash AC0C8CD3\n  sample 19:\n    time = 633966\n    flags = 0\n    data = length 1022, hash 5D8BFF34\n  sample 20:\n    time = 600600\n    flags = 0\n    data = length 790, hash 99413A99\n  sample 21:\n    time = 667333\n    flags = 0\n    data = length 610, hash 5E129290\n  sample 22:\n    time = 834166\n    flags = 0\n    data = length 2751, hash 769974CB\n  sample 23:\n    time = 767433\n    flags = 0\n    data = length 745, hash B78A477A\n  sample 24:\n    time = 734066\n    flags = 0\n    data = length 621, hash CF741E7A\n  sample 25:\n    time = 800800\n    flags = 0\n    data = length 505, hash 1DB4894E\n  sample 26:\n    time = 967633\n    flags = 0\n    data = length 1268, hash C15348DC\n  sample 27:\n    time = 900900\n    flags = 0\n    data = length 880, hash C2DE85D0\n  sample 28:\n    time = 867533\n    flags = 0\n    data = length 530, hash C98BC6A8\n  sample 29:\n    time = 934266\n    flags = 536870912\n    data = length 568, hash 4FE5C8EA\ntrack 1:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = 294\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n      data = length 2, hash 5F7\n  total output bytes = 9529\n  sample count = 45\n  sample 0:\n    time = 44000\n    flags = 1\n    data = length 23, hash 47DE9131\n  sample 1:\n    time = 67219\n    flags = 1\n    data = length 6, hash 31EC5206\n  sample 2:\n    time = 90439\n    flags = 1\n    data = length 148, hash 894A176B\n  sample 3:\n    time = 113659\n    flags = 1\n    data = length 189, hash CEF235A1\n  sample 4:\n    time = 136879\n    flags = 1\n    data = length 205, hash BBF5F7B0\n  sample 5:\n    time = 160099\n    flags = 1\n    data = length 210, hash F278B193\n  sample 6:\n    time = 183319\n    flags = 1\n    data = length 210, hash 82DA1589\n  sample 7:\n    time = 206539\n    flags = 1\n    data = length 207, hash 5BE231DF\n  sample 8:\n    time = 229759\n    flags = 1\n    data = length 225, hash 18819EE1\n  sample 9:\n    time = 252979\n    flags = 1\n    data = length 215, hash CA7FA67B\n  sample 10:\n    time = 276199\n    flags = 1\n    data = length 211, hash 581A1C18\n  sample 11:\n    time = 299419\n    flags = 1\n    data = length 216, hash ADB88187\n  sample 12:\n    time = 322639\n    flags = 1\n    data = length 229, hash 2E8BA4DC\n  sample 13:\n    time = 345859\n    flags = 1\n    data = length 232, hash 22F0C510\n  sample 14:\n    time = 369079\n    flags = 1\n    data = length 235, hash 867AD0DC\n  sample 15:\n    time = 392299\n    flags = 1\n    data = length 231, hash 84E823A8\n  sample 16:\n    time = 415519\n    flags = 1\n    data = length 226, hash 1BEF3A95\n  sample 17:\n    time = 438739\n    flags = 1\n    data = length 216, hash EAA345AE\n  sample 18:\n    time = 461959\n    flags = 1\n    data = length 229, hash 6957411F\n  sample 19:\n    time = 485179\n    flags = 1\n    data = length 219, hash 41275022\n  sample 20:\n    time = 508399\n    flags = 1\n    data = length 241, hash 6495DF96\n  sample 21:\n    time = 531619\n    flags = 1\n    data = length 228, hash 63D95906\n  sample 22:\n    time = 554839\n    flags = 1\n    data = length 238, hash 34F676F9\n  sample 23:\n    time = 578058\n    flags = 1\n    data = length 234, hash E5CBC045\n  sample 24:\n    time = 601278\n    flags = 1\n    data = length 231, hash 5FC43661\n  sample 25:\n    time = 624498\n    flags = 1\n    data = length 217, hash 682708ED\n  sample 26:\n    time = 647718\n    flags = 1\n    data = length 239, hash D43780FC\n  sample 27:\n    time = 670938\n    flags = 1\n    data = length 243, hash C5E17980\n  sample 28:\n    time = 694158\n    flags = 1\n    data = length 231, hash AC5837BA\n  sample 29:\n    time = 717378\n    flags = 1\n    data = length 230, hash 169EE895\n  sample 30:\n    time = 740598\n    flags = 1\n    data = length 238, hash C48FF3F1\n  sample 31:\n    time = 763818\n    flags = 1\n    data = length 225, hash 531E4599\n  sample 32:\n    time = 787038\n    flags = 1\n    data = length 232, hash CB3C6B8D\n  sample 33:\n    time = 810258\n    flags = 1\n    data = length 243, hash F8C94C7\n  sample 34:\n    time = 833478\n    flags = 1\n    data = length 232, hash A646A7D0\n  sample 35:\n    time = 856698\n    flags = 1\n    data = length 237, hash E8B787A5\n  sample 36:\n    time = 879918\n    flags = 1\n    data = length 228, hash 3FA7A29F\n  sample 37:\n    time = 903138\n    flags = 1\n    data = length 235, hash B9B33B0A\n  sample 38:\n    time = 926358\n    flags = 1\n    data = length 264, hash 71A4869E\n  sample 39:\n    time = 949578\n    flags = 1\n    data = length 257, hash D049B54C\n  sample 40:\n    time = 972798\n    flags = 1\n    data = length 227, hash 66757231\n  sample 41:\n    time = 996018\n    flags = 1\n    data = length 227, hash BD374F1B\n  sample 42:\n    time = 1019238\n    flags = 1\n    data = length 235, hash 999477F6\n  sample 43:\n    time = 1042458\n    flags = 1\n    data = length 229, hash FFF98DF0\n  sample 44:\n    time = 1065678\n    flags = 536870913\n    data = length 6, hash 31B22286\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp4/sample.mp4.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1024000\n  getPosition(0) = [[timeUs=0, position=48]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = 36722\n    width = 1080\n    height = 720\n    frameRate = 29.970028\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 29, hash 4746B5D9\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 89876\n  sample count = 30\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 36692, hash D216076E\n  sample 1:\n    time = 66733\n    flags = 0\n    data = length 5312, hash D45D3CA0\n  sample 2:\n    time = 33366\n    flags = 0\n    data = length 599, hash 1BE7812D\n  sample 3:\n    time = 200200\n    flags = 0\n    data = length 7735, hash 4490F110\n  sample 4:\n    time = 133466\n    flags = 0\n    data = length 987, hash 560B5036\n  sample 5:\n    time = 100100\n    flags = 0\n    data = length 673, hash ED7CD8C7\n  sample 6:\n    time = 166833\n    flags = 0\n    data = length 523, hash 3020DF50\n  sample 7:\n    time = 333666\n    flags = 0\n    data = length 6061, hash 736C72B2\n  sample 8:\n    time = 266933\n    flags = 0\n    data = length 992, hash FE132F23\n  sample 9:\n    time = 233566\n    flags = 0\n    data = length 623, hash 5B2C1816\n  sample 10:\n    time = 300300\n    flags = 0\n    data = length 421, hash 742E69C1\n  sample 11:\n    time = 433766\n    flags = 0\n    data = length 4899, hash F72F86A1\n  sample 12:\n    time = 400400\n    flags = 0\n    data = length 568, hash 519A8E50\n  sample 13:\n    time = 367033\n    flags = 0\n    data = length 620, hash 3990AA39\n  sample 14:\n    time = 567233\n    flags = 0\n    data = length 5450, hash F06EC4AA\n  sample 15:\n    time = 500500\n    flags = 0\n    data = length 1051, hash 92DFA63A\n  sample 16:\n    time = 467133\n    flags = 0\n    data = length 874, hash 69587FB4\n  sample 17:\n    time = 533866\n    flags = 0\n    data = length 781, hash 36BE495B\n  sample 18:\n    time = 700700\n    flags = 0\n    data = length 4725, hash AC0C8CD3\n  sample 19:\n    time = 633966\n    flags = 0\n    data = length 1022, hash 5D8BFF34\n  sample 20:\n    time = 600600\n    flags = 0\n    data = length 790, hash 99413A99\n  sample 21:\n    time = 667333\n    flags = 0\n    data = length 610, hash 5E129290\n  sample 22:\n    time = 834166\n    flags = 0\n    data = length 2751, hash 769974CB\n  sample 23:\n    time = 767433\n    flags = 0\n    data = length 745, hash B78A477A\n  sample 24:\n    time = 734066\n    flags = 0\n    data = length 621, hash CF741E7A\n  sample 25:\n    time = 800800\n    flags = 0\n    data = length 505, hash 1DB4894E\n  sample 26:\n    time = 967633\n    flags = 0\n    data = length 1268, hash C15348DC\n  sample 27:\n    time = 900900\n    flags = 0\n    data = length 880, hash C2DE85D0\n  sample 28:\n    time = 867533\n    flags = 0\n    data = length 530, hash C98BC6A8\n  sample 29:\n    time = 934266\n    flags = 536870912\n    data = length 568, hash 4FE5C8EA\ntrack 1:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = 294\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n      data = length 2, hash 5F7\n  total output bytes = 7464\n  sample count = 33\n  sample 0:\n    time = 322639\n    flags = 1\n    data = length 229, hash 2E8BA4DC\n  sample 1:\n    time = 345859\n    flags = 1\n    data = length 232, hash 22F0C510\n  sample 2:\n    time = 369079\n    flags = 1\n    data = length 235, hash 867AD0DC\n  sample 3:\n    time = 392299\n    flags = 1\n    data = length 231, hash 84E823A8\n  sample 4:\n    time = 415519\n    flags = 1\n    data = length 226, hash 1BEF3A95\n  sample 5:\n    time = 438739\n    flags = 1\n    data = length 216, hash EAA345AE\n  sample 6:\n    time = 461959\n    flags = 1\n    data = length 229, hash 6957411F\n  sample 7:\n    time = 485179\n    flags = 1\n    data = length 219, hash 41275022\n  sample 8:\n    time = 508399\n    flags = 1\n    data = length 241, hash 6495DF96\n  sample 9:\n    time = 531619\n    flags = 1\n    data = length 228, hash 63D95906\n  sample 10:\n    time = 554839\n    flags = 1\n    data = length 238, hash 34F676F9\n  sample 11:\n    time = 578058\n    flags = 1\n    data = length 234, hash E5CBC045\n  sample 12:\n    time = 601278\n    flags = 1\n    data = length 231, hash 5FC43661\n  sample 13:\n    time = 624498\n    flags = 1\n    data = length 217, hash 682708ED\n  sample 14:\n    time = 647718\n    flags = 1\n    data = length 239, hash D43780FC\n  sample 15:\n    time = 670938\n    flags = 1\n    data = length 243, hash C5E17980\n  sample 16:\n    time = 694158\n    flags = 1\n    data = length 231, hash AC5837BA\n  sample 17:\n    time = 717378\n    flags = 1\n    data = length 230, hash 169EE895\n  sample 18:\n    time = 740598\n    flags = 1\n    data = length 238, hash C48FF3F1\n  sample 19:\n    time = 763818\n    flags = 1\n    data = length 225, hash 531E4599\n  sample 20:\n    time = 787038\n    flags = 1\n    data = length 232, hash CB3C6B8D\n  sample 21:\n    time = 810258\n    flags = 1\n    data = length 243, hash F8C94C7\n  sample 22:\n    time = 833478\n    flags = 1\n    data = length 232, hash A646A7D0\n  sample 23:\n    time = 856698\n    flags = 1\n    data = length 237, hash E8B787A5\n  sample 24:\n    time = 879918\n    flags = 1\n    data = length 228, hash 3FA7A29F\n  sample 25:\n    time = 903138\n    flags = 1\n    data = length 235, hash B9B33B0A\n  sample 26:\n    time = 926358\n    flags = 1\n    data = length 264, hash 71A4869E\n  sample 27:\n    time = 949578\n    flags = 1\n    data = length 257, hash D049B54C\n  sample 28:\n    time = 972798\n    flags = 1\n    data = length 227, hash 66757231\n  sample 29:\n    time = 996018\n    flags = 1\n    data = length 227, hash BD374F1B\n  sample 30:\n    time = 1019238\n    flags = 1\n    data = length 235, hash 999477F6\n  sample 31:\n    time = 1042458\n    flags = 1\n    data = length 229, hash FFF98DF0\n  sample 32:\n    time = 1065678\n    flags = 536870913\n    data = length 6, hash 31B22286\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp4/sample.mp4.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1024000\n  getPosition(0) = [[timeUs=0, position=48]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = 36722\n    width = 1080\n    height = 720\n    frameRate = 29.970028\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 29, hash 4746B5D9\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 89876\n  sample count = 30\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 36692, hash D216076E\n  sample 1:\n    time = 66733\n    flags = 0\n    data = length 5312, hash D45D3CA0\n  sample 2:\n    time = 33366\n    flags = 0\n    data = length 599, hash 1BE7812D\n  sample 3:\n    time = 200200\n    flags = 0\n    data = length 7735, hash 4490F110\n  sample 4:\n    time = 133466\n    flags = 0\n    data = length 987, hash 560B5036\n  sample 5:\n    time = 100100\n    flags = 0\n    data = length 673, hash ED7CD8C7\n  sample 6:\n    time = 166833\n    flags = 0\n    data = length 523, hash 3020DF50\n  sample 7:\n    time = 333666\n    flags = 0\n    data = length 6061, hash 736C72B2\n  sample 8:\n    time = 266933\n    flags = 0\n    data = length 992, hash FE132F23\n  sample 9:\n    time = 233566\n    flags = 0\n    data = length 623, hash 5B2C1816\n  sample 10:\n    time = 300300\n    flags = 0\n    data = length 421, hash 742E69C1\n  sample 11:\n    time = 433766\n    flags = 0\n    data = length 4899, hash F72F86A1\n  sample 12:\n    time = 400400\n    flags = 0\n    data = length 568, hash 519A8E50\n  sample 13:\n    time = 367033\n    flags = 0\n    data = length 620, hash 3990AA39\n  sample 14:\n    time = 567233\n    flags = 0\n    data = length 5450, hash F06EC4AA\n  sample 15:\n    time = 500500\n    flags = 0\n    data = length 1051, hash 92DFA63A\n  sample 16:\n    time = 467133\n    flags = 0\n    data = length 874, hash 69587FB4\n  sample 17:\n    time = 533866\n    flags = 0\n    data = length 781, hash 36BE495B\n  sample 18:\n    time = 700700\n    flags = 0\n    data = length 4725, hash AC0C8CD3\n  sample 19:\n    time = 633966\n    flags = 0\n    data = length 1022, hash 5D8BFF34\n  sample 20:\n    time = 600600\n    flags = 0\n    data = length 790, hash 99413A99\n  sample 21:\n    time = 667333\n    flags = 0\n    data = length 610, hash 5E129290\n  sample 22:\n    time = 834166\n    flags = 0\n    data = length 2751, hash 769974CB\n  sample 23:\n    time = 767433\n    flags = 0\n    data = length 745, hash B78A477A\n  sample 24:\n    time = 734066\n    flags = 0\n    data = length 621, hash CF741E7A\n  sample 25:\n    time = 800800\n    flags = 0\n    data = length 505, hash 1DB4894E\n  sample 26:\n    time = 967633\n    flags = 0\n    data = length 1268, hash C15348DC\n  sample 27:\n    time = 900900\n    flags = 0\n    data = length 880, hash C2DE85D0\n  sample 28:\n    time = 867533\n    flags = 0\n    data = length 530, hash C98BC6A8\n  sample 29:\n    time = 934266\n    flags = 536870912\n    data = length 568, hash 4FE5C8EA\ntrack 1:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = 294\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n      data = length 2, hash 5F7\n  total output bytes = 4019\n  sample count = 18\n  sample 0:\n    time = 670938\n    flags = 1\n    data = length 243, hash C5E17980\n  sample 1:\n    time = 694158\n    flags = 1\n    data = length 231, hash AC5837BA\n  sample 2:\n    time = 717378\n    flags = 1\n    data = length 230, hash 169EE895\n  sample 3:\n    time = 740598\n    flags = 1\n    data = length 238, hash C48FF3F1\n  sample 4:\n    time = 763818\n    flags = 1\n    data = length 225, hash 531E4599\n  sample 5:\n    time = 787038\n    flags = 1\n    data = length 232, hash CB3C6B8D\n  sample 6:\n    time = 810258\n    flags = 1\n    data = length 243, hash F8C94C7\n  sample 7:\n    time = 833478\n    flags = 1\n    data = length 232, hash A646A7D0\n  sample 8:\n    time = 856698\n    flags = 1\n    data = length 237, hash E8B787A5\n  sample 9:\n    time = 879918\n    flags = 1\n    data = length 228, hash 3FA7A29F\n  sample 10:\n    time = 903138\n    flags = 1\n    data = length 235, hash B9B33B0A\n  sample 11:\n    time = 926358\n    flags = 1\n    data = length 264, hash 71A4869E\n  sample 12:\n    time = 949578\n    flags = 1\n    data = length 257, hash D049B54C\n  sample 13:\n    time = 972798\n    flags = 1\n    data = length 227, hash 66757231\n  sample 14:\n    time = 996018\n    flags = 1\n    data = length 227, hash BD374F1B\n  sample 15:\n    time = 1019238\n    flags = 1\n    data = length 235, hash 999477F6\n  sample 16:\n    time = 1042458\n    flags = 1\n    data = length 229, hash FFF98DF0\n  sample 17:\n    time = 1065678\n    flags = 536870913\n    data = length 6, hash 31B22286\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp4/sample.mp4.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1024000\n  getPosition(0) = [[timeUs=0, position=48]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = 36722\n    width = 1080\n    height = 720\n    frameRate = 29.970028\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 29, hash 4746B5D9\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 89876\n  sample count = 30\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 36692, hash D216076E\n  sample 1:\n    time = 66733\n    flags = 0\n    data = length 5312, hash D45D3CA0\n  sample 2:\n    time = 33366\n    flags = 0\n    data = length 599, hash 1BE7812D\n  sample 3:\n    time = 200200\n    flags = 0\n    data = length 7735, hash 4490F110\n  sample 4:\n    time = 133466\n    flags = 0\n    data = length 987, hash 560B5036\n  sample 5:\n    time = 100100\n    flags = 0\n    data = length 673, hash ED7CD8C7\n  sample 6:\n    time = 166833\n    flags = 0\n    data = length 523, hash 3020DF50\n  sample 7:\n    time = 333666\n    flags = 0\n    data = length 6061, hash 736C72B2\n  sample 8:\n    time = 266933\n    flags = 0\n    data = length 992, hash FE132F23\n  sample 9:\n    time = 233566\n    flags = 0\n    data = length 623, hash 5B2C1816\n  sample 10:\n    time = 300300\n    flags = 0\n    data = length 421, hash 742E69C1\n  sample 11:\n    time = 433766\n    flags = 0\n    data = length 4899, hash F72F86A1\n  sample 12:\n    time = 400400\n    flags = 0\n    data = length 568, hash 519A8E50\n  sample 13:\n    time = 367033\n    flags = 0\n    data = length 620, hash 3990AA39\n  sample 14:\n    time = 567233\n    flags = 0\n    data = length 5450, hash F06EC4AA\n  sample 15:\n    time = 500500\n    flags = 0\n    data = length 1051, hash 92DFA63A\n  sample 16:\n    time = 467133\n    flags = 0\n    data = length 874, hash 69587FB4\n  sample 17:\n    time = 533866\n    flags = 0\n    data = length 781, hash 36BE495B\n  sample 18:\n    time = 700700\n    flags = 0\n    data = length 4725, hash AC0C8CD3\n  sample 19:\n    time = 633966\n    flags = 0\n    data = length 1022, hash 5D8BFF34\n  sample 20:\n    time = 600600\n    flags = 0\n    data = length 790, hash 99413A99\n  sample 21:\n    time = 667333\n    flags = 0\n    data = length 610, hash 5E129290\n  sample 22:\n    time = 834166\n    flags = 0\n    data = length 2751, hash 769974CB\n  sample 23:\n    time = 767433\n    flags = 0\n    data = length 745, hash B78A477A\n  sample 24:\n    time = 734066\n    flags = 0\n    data = length 621, hash CF741E7A\n  sample 25:\n    time = 800800\n    flags = 0\n    data = length 505, hash 1DB4894E\n  sample 26:\n    time = 967633\n    flags = 0\n    data = length 1268, hash C15348DC\n  sample 27:\n    time = 900900\n    flags = 0\n    data = length 880, hash C2DE85D0\n  sample 28:\n    time = 867533\n    flags = 0\n    data = length 530, hash C98BC6A8\n  sample 29:\n    time = 934266\n    flags = 536870912\n    data = length 568, hash 4FE5C8EA\ntrack 1:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = 294\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n      data = length 2, hash 5F7\n  total output bytes = 470\n  sample count = 3\n  sample 0:\n    time = 1019238\n    flags = 1\n    data = length 235, hash 999477F6\n  sample 1:\n    time = 1042458\n    flags = 1\n    data = length 229, hash FFF98DF0\n  sample 2:\n    time = 1065678\n    flags = 536870913\n    data = length 6, hash 31B22286\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp4/sample_fragmented.mp4.0.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=1828]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = -1\n    width = 1080\n    height = 720\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 29, hash 4746B5D9\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 85933\n  sample count = 30\n  sample 0:\n    time = 66000\n    flags = 1\n    data = length 38070, hash B58E1AEE\n  sample 1:\n    time = 199000\n    flags = 0\n    data = length 8340, hash 8AC449FF\n  sample 2:\n    time = 132000\n    flags = 0\n    data = length 1295, hash C0DA5090\n  sample 3:\n    time = 100000\n    flags = 0\n    data = length 469, hash D6E0A200\n  sample 4:\n    time = 166000\n    flags = 0\n    data = length 564, hash E5F56C5B\n  sample 5:\n    time = 332000\n    flags = 0\n    data = length 6075, hash 8756E49E\n  sample 6:\n    time = 266000\n    flags = 0\n    data = length 847, hash DCC2B618\n  sample 7:\n    time = 233000\n    flags = 0\n    data = length 455, hash B9CCE047\n  sample 8:\n    time = 299000\n    flags = 0\n    data = length 467, hash 69806D94\n  sample 9:\n    time = 466000\n    flags = 0\n    data = length 4549, hash 3944F501\n  sample 10:\n    time = 399000\n    flags = 0\n    data = length 1087, hash 491BF106\n  sample 11:\n    time = 367000\n    flags = 0\n    data = length 380, hash 5FED016A\n  sample 12:\n    time = 433000\n    flags = 0\n    data = length 455, hash 8A0610\n  sample 13:\n    time = 599000\n    flags = 0\n    data = length 5190, hash B9031D8\n  sample 14:\n    time = 533000\n    flags = 0\n    data = length 1071, hash 684E7DC8\n  sample 15:\n    time = 500000\n    flags = 0\n    data = length 653, hash 8494F326\n  sample 16:\n    time = 566000\n    flags = 0\n    data = length 485, hash 2CCC85F4\n  sample 17:\n    time = 733000\n    flags = 0\n    data = length 4884, hash D16B6A96\n  sample 18:\n    time = 666000\n    flags = 0\n    data = length 997, hash 164FF210\n  sample 19:\n    time = 633000\n    flags = 0\n    data = length 640, hash F664125B\n  sample 20:\n    time = 700000\n    flags = 0\n    data = length 491, hash B5930C7C\n  sample 21:\n    time = 866000\n    flags = 0\n    data = length 2989, hash 92CF4FCF\n  sample 22:\n    time = 800000\n    flags = 0\n    data = length 838, hash 294A3451\n  sample 23:\n    time = 767000\n    flags = 0\n    data = length 544, hash FCCE2DE6\n  sample 24:\n    time = 833000\n    flags = 0\n    data = length 329, hash A654FFA1\n  sample 25:\n    time = 1000000\n    flags = 0\n    data = length 1517, hash 5F7EBF8B\n  sample 26:\n    time = 933000\n    flags = 0\n    data = length 803, hash 7A5C4C1D\n  sample 27:\n    time = 900000\n    flags = 0\n    data = length 415, hash B31BBC3B\n  sample 28:\n    time = 967000\n    flags = 0\n    data = length 415, hash 850DFEA3\n  sample 29:\n    time = 1033000\n    flags = 0\n    data = length 619, hash AB5E56CA\ntrack 1:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n      data = length 5, hash 2B7623A\n  total output bytes = 18257\n  sample count = 46\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 18, hash 96519432\n  sample 1:\n    time = 23000\n    flags = 1\n    data = length 4, hash EE9DF\n  sample 2:\n    time = 46000\n    flags = 1\n    data = length 4, hash EEDBF\n  sample 3:\n    time = 69000\n    flags = 1\n    data = length 157, hash E2F078F4\n  sample 4:\n    time = 92000\n    flags = 1\n    data = length 371, hash B9471F94\n  sample 5:\n    time = 116000\n    flags = 1\n    data = length 373, hash 2AB265CB\n  sample 6:\n    time = 139000\n    flags = 1\n    data = length 402, hash 1295477C\n  sample 7:\n    time = 162000\n    flags = 1\n    data = length 455, hash 2D8146C8\n  sample 8:\n    time = 185000\n    flags = 1\n    data = length 434, hash F2C5D287\n  sample 9:\n    time = 208000\n    flags = 1\n    data = length 450, hash 84143FCD\n  sample 10:\n    time = 232000\n    flags = 1\n    data = length 429, hash EF769D50\n  sample 11:\n    time = 255000\n    flags = 1\n    data = length 450, hash EC3DE692\n  sample 12:\n    time = 278000\n    flags = 1\n    data = length 447, hash 3E519E13\n  sample 13:\n    time = 301000\n    flags = 1\n    data = length 457, hash 1E4F23A0\n  sample 14:\n    time = 325000\n    flags = 1\n    data = length 447, hash A439EA97\n  sample 15:\n    time = 348000\n    flags = 1\n    data = length 456, hash 1E9034C6\n  sample 16:\n    time = 371000\n    flags = 1\n    data = length 398, hash 99DB7345\n  sample 17:\n    time = 394000\n    flags = 1\n    data = length 474, hash 3F05F10A\n  sample 18:\n    time = 417000\n    flags = 1\n    data = length 416, hash C105EE09\n  sample 19:\n    time = 441000\n    flags = 1\n    data = length 454, hash 5FDBE458\n  sample 20:\n    time = 464000\n    flags = 1\n    data = length 438, hash 41A93AC3\n  sample 21:\n    time = 487000\n    flags = 1\n    data = length 443, hash 10FDA652\n  sample 22:\n    time = 510000\n    flags = 1\n    data = length 412, hash 1F791E25\n  sample 23:\n    time = 534000\n    flags = 1\n    data = length 482, hash A6D983D\n  sample 24:\n    time = 557000\n    flags = 1\n    data = length 386, hash BED7392F\n  sample 25:\n    time = 580000\n    flags = 1\n    data = length 463, hash 5309F8C9\n  sample 26:\n    time = 603000\n    flags = 1\n    data = length 394, hash 21C7321F\n  sample 27:\n    time = 626000\n    flags = 1\n    data = length 489, hash 71B4730D\n  sample 28:\n    time = 650000\n    flags = 1\n    data = length 403, hash D9C6DE89\n  sample 29:\n    time = 673000\n    flags = 1\n    data = length 447, hash 9B14B73B\n  sample 30:\n    time = 696000\n    flags = 1\n    data = length 439, hash 4760D35B\n  sample 31:\n    time = 719000\n    flags = 1\n    data = length 463, hash 1601F88D\n  sample 32:\n    time = 743000\n    flags = 1\n    data = length 423, hash D4AE6773\n  sample 33:\n    time = 766000\n    flags = 1\n    data = length 497, hash A3C674D3\n  sample 34:\n    time = 789000\n    flags = 1\n    data = length 419, hash D3734A1F\n  sample 35:\n    time = 812000\n    flags = 1\n    data = length 474, hash DFB41F9\n  sample 36:\n    time = 835000\n    flags = 1\n    data = length 413, hash 53E7CB9F\n  sample 37:\n    time = 859000\n    flags = 1\n    data = length 445, hash D15B0E39\n  sample 38:\n    time = 882000\n    flags = 1\n    data = length 453, hash 77ED81E4\n  sample 39:\n    time = 905000\n    flags = 1\n    data = length 545, hash 3321AEB9\n  sample 40:\n    time = 928000\n    flags = 1\n    data = length 317, hash F557D0E\n  sample 41:\n    time = 952000\n    flags = 1\n    data = length 537, hash ED58CF7B\n  sample 42:\n    time = 975000\n    flags = 1\n    data = length 458, hash 51CDAA10\n  sample 43:\n    time = 998000\n    flags = 1\n    data = length 465, hash CBA1EFD7\n  sample 44:\n    time = 1021000\n    flags = 1\n    data = length 446, hash D6735B8A\n  sample 45:\n    time = 1044000\n    flags = 1\n    data = length 10, hash A453EEBE\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1067733\n  getPosition(0) = [[timeUs=66733, position=1325]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = -1\n    width = 1080\n    height = 720\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 29, hash 4746B5D9\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 85933\n  sample count = 30\n  sample 0:\n    time = 66000\n    flags = 1\n    data = length 38070, hash B58E1AEE\n  sample 1:\n    time = 199000\n    flags = 0\n    data = length 8340, hash 8AC449FF\n  sample 2:\n    time = 132000\n    flags = 0\n    data = length 1295, hash C0DA5090\n  sample 3:\n    time = 100000\n    flags = 0\n    data = length 469, hash D6E0A200\n  sample 4:\n    time = 166000\n    flags = 0\n    data = length 564, hash E5F56C5B\n  sample 5:\n    time = 332000\n    flags = 0\n    data = length 6075, hash 8756E49E\n  sample 6:\n    time = 266000\n    flags = 0\n    data = length 847, hash DCC2B618\n  sample 7:\n    time = 233000\n    flags = 0\n    data = length 455, hash B9CCE047\n  sample 8:\n    time = 299000\n    flags = 0\n    data = length 467, hash 69806D94\n  sample 9:\n    time = 466000\n    flags = 0\n    data = length 4549, hash 3944F501\n  sample 10:\n    time = 399000\n    flags = 0\n    data = length 1087, hash 491BF106\n  sample 11:\n    time = 367000\n    flags = 0\n    data = length 380, hash 5FED016A\n  sample 12:\n    time = 433000\n    flags = 0\n    data = length 455, hash 8A0610\n  sample 13:\n    time = 599000\n    flags = 0\n    data = length 5190, hash B9031D8\n  sample 14:\n    time = 533000\n    flags = 0\n    data = length 1071, hash 684E7DC8\n  sample 15:\n    time = 500000\n    flags = 0\n    data = length 653, hash 8494F326\n  sample 16:\n    time = 566000\n    flags = 0\n    data = length 485, hash 2CCC85F4\n  sample 17:\n    time = 733000\n    flags = 0\n    data = length 4884, hash D16B6A96\n  sample 18:\n    time = 666000\n    flags = 0\n    data = length 997, hash 164FF210\n  sample 19:\n    time = 633000\n    flags = 0\n    data = length 640, hash F664125B\n  sample 20:\n    time = 700000\n    flags = 0\n    data = length 491, hash B5930C7C\n  sample 21:\n    time = 866000\n    flags = 0\n    data = length 2989, hash 92CF4FCF\n  sample 22:\n    time = 800000\n    flags = 0\n    data = length 838, hash 294A3451\n  sample 23:\n    time = 767000\n    flags = 0\n    data = length 544, hash FCCE2DE6\n  sample 24:\n    time = 833000\n    flags = 0\n    data = length 329, hash A654FFA1\n  sample 25:\n    time = 1000000\n    flags = 0\n    data = length 1517, hash 5F7EBF8B\n  sample 26:\n    time = 933000\n    flags = 0\n    data = length 803, hash 7A5C4C1D\n  sample 27:\n    time = 900000\n    flags = 0\n    data = length 415, hash B31BBC3B\n  sample 28:\n    time = 967000\n    flags = 0\n    data = length 415, hash 850DFEA3\n  sample 29:\n    time = 1033000\n    flags = 0\n    data = length 619, hash AB5E56CA\ntrack 1:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n      data = length 5, hash 2B7623A\n  total output bytes = 18257\n  sample count = 46\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 18, hash 96519432\n  sample 1:\n    time = 23000\n    flags = 1\n    data = length 4, hash EE9DF\n  sample 2:\n    time = 46000\n    flags = 1\n    data = length 4, hash EEDBF\n  sample 3:\n    time = 69000\n    flags = 1\n    data = length 157, hash E2F078F4\n  sample 4:\n    time = 92000\n    flags = 1\n    data = length 371, hash B9471F94\n  sample 5:\n    time = 116000\n    flags = 1\n    data = length 373, hash 2AB265CB\n  sample 6:\n    time = 139000\n    flags = 1\n    data = length 402, hash 1295477C\n  sample 7:\n    time = 162000\n    flags = 1\n    data = length 455, hash 2D8146C8\n  sample 8:\n    time = 185000\n    flags = 1\n    data = length 434, hash F2C5D287\n  sample 9:\n    time = 208000\n    flags = 1\n    data = length 450, hash 84143FCD\n  sample 10:\n    time = 232000\n    flags = 1\n    data = length 429, hash EF769D50\n  sample 11:\n    time = 255000\n    flags = 1\n    data = length 450, hash EC3DE692\n  sample 12:\n    time = 278000\n    flags = 1\n    data = length 447, hash 3E519E13\n  sample 13:\n    time = 301000\n    flags = 1\n    data = length 457, hash 1E4F23A0\n  sample 14:\n    time = 325000\n    flags = 1\n    data = length 447, hash A439EA97\n  sample 15:\n    time = 348000\n    flags = 1\n    data = length 456, hash 1E9034C6\n  sample 16:\n    time = 371000\n    flags = 1\n    data = length 398, hash 99DB7345\n  sample 17:\n    time = 394000\n    flags = 1\n    data = length 474, hash 3F05F10A\n  sample 18:\n    time = 417000\n    flags = 1\n    data = length 416, hash C105EE09\n  sample 19:\n    time = 441000\n    flags = 1\n    data = length 454, hash 5FDBE458\n  sample 20:\n    time = 464000\n    flags = 1\n    data = length 438, hash 41A93AC3\n  sample 21:\n    time = 487000\n    flags = 1\n    data = length 443, hash 10FDA652\n  sample 22:\n    time = 510000\n    flags = 1\n    data = length 412, hash 1F791E25\n  sample 23:\n    time = 534000\n    flags = 1\n    data = length 482, hash A6D983D\n  sample 24:\n    time = 557000\n    flags = 1\n    data = length 386, hash BED7392F\n  sample 25:\n    time = 580000\n    flags = 1\n    data = length 463, hash 5309F8C9\n  sample 26:\n    time = 603000\n    flags = 1\n    data = length 394, hash 21C7321F\n  sample 27:\n    time = 626000\n    flags = 1\n    data = length 489, hash 71B4730D\n  sample 28:\n    time = 650000\n    flags = 1\n    data = length 403, hash D9C6DE89\n  sample 29:\n    time = 673000\n    flags = 1\n    data = length 447, hash 9B14B73B\n  sample 30:\n    time = 696000\n    flags = 1\n    data = length 439, hash 4760D35B\n  sample 31:\n    time = 719000\n    flags = 1\n    data = length 463, hash 1601F88D\n  sample 32:\n    time = 743000\n    flags = 1\n    data = length 423, hash D4AE6773\n  sample 33:\n    time = 766000\n    flags = 1\n    data = length 497, hash A3C674D3\n  sample 34:\n    time = 789000\n    flags = 1\n    data = length 419, hash D3734A1F\n  sample 35:\n    time = 812000\n    flags = 1\n    data = length 474, hash DFB41F9\n  sample 36:\n    time = 835000\n    flags = 1\n    data = length 413, hash 53E7CB9F\n  sample 37:\n    time = 859000\n    flags = 1\n    data = length 445, hash D15B0E39\n  sample 38:\n    time = 882000\n    flags = 1\n    data = length 453, hash 77ED81E4\n  sample 39:\n    time = 905000\n    flags = 1\n    data = length 545, hash 3321AEB9\n  sample 40:\n    time = 928000\n    flags = 1\n    data = length 317, hash F557D0E\n  sample 41:\n    time = 952000\n    flags = 1\n    data = length 537, hash ED58CF7B\n  sample 42:\n    time = 975000\n    flags = 1\n    data = length 458, hash 51CDAA10\n  sample 43:\n    time = 998000\n    flags = 1\n    data = length 465, hash CBA1EFD7\n  sample 44:\n    time = 1021000\n    flags = 1\n    data = length 446, hash D6735B8A\n  sample 45:\n    time = 1044000\n    flags = 1\n    data = length 10, hash A453EEBE\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1067733\n  getPosition(0) = [[timeUs=66733, position=1325]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = -1\n    width = 1080\n    height = 720\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 29, hash 4746B5D9\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 85933\n  sample count = 30\n  sample 0:\n    time = 66000\n    flags = 1\n    data = length 38070, hash B58E1AEE\n  sample 1:\n    time = 199000\n    flags = 0\n    data = length 8340, hash 8AC449FF\n  sample 2:\n    time = 132000\n    flags = 0\n    data = length 1295, hash C0DA5090\n  sample 3:\n    time = 100000\n    flags = 0\n    data = length 469, hash D6E0A200\n  sample 4:\n    time = 166000\n    flags = 0\n    data = length 564, hash E5F56C5B\n  sample 5:\n    time = 332000\n    flags = 0\n    data = length 6075, hash 8756E49E\n  sample 6:\n    time = 266000\n    flags = 0\n    data = length 847, hash DCC2B618\n  sample 7:\n    time = 233000\n    flags = 0\n    data = length 455, hash B9CCE047\n  sample 8:\n    time = 299000\n    flags = 0\n    data = length 467, hash 69806D94\n  sample 9:\n    time = 466000\n    flags = 0\n    data = length 4549, hash 3944F501\n  sample 10:\n    time = 399000\n    flags = 0\n    data = length 1087, hash 491BF106\n  sample 11:\n    time = 367000\n    flags = 0\n    data = length 380, hash 5FED016A\n  sample 12:\n    time = 433000\n    flags = 0\n    data = length 455, hash 8A0610\n  sample 13:\n    time = 599000\n    flags = 0\n    data = length 5190, hash B9031D8\n  sample 14:\n    time = 533000\n    flags = 0\n    data = length 1071, hash 684E7DC8\n  sample 15:\n    time = 500000\n    flags = 0\n    data = length 653, hash 8494F326\n  sample 16:\n    time = 566000\n    flags = 0\n    data = length 485, hash 2CCC85F4\n  sample 17:\n    time = 733000\n    flags = 0\n    data = length 4884, hash D16B6A96\n  sample 18:\n    time = 666000\n    flags = 0\n    data = length 997, hash 164FF210\n  sample 19:\n    time = 633000\n    flags = 0\n    data = length 640, hash F664125B\n  sample 20:\n    time = 700000\n    flags = 0\n    data = length 491, hash B5930C7C\n  sample 21:\n    time = 866000\n    flags = 0\n    data = length 2989, hash 92CF4FCF\n  sample 22:\n    time = 800000\n    flags = 0\n    data = length 838, hash 294A3451\n  sample 23:\n    time = 767000\n    flags = 0\n    data = length 544, hash FCCE2DE6\n  sample 24:\n    time = 833000\n    flags = 0\n    data = length 329, hash A654FFA1\n  sample 25:\n    time = 1000000\n    flags = 0\n    data = length 1517, hash 5F7EBF8B\n  sample 26:\n    time = 933000\n    flags = 0\n    data = length 803, hash 7A5C4C1D\n  sample 27:\n    time = 900000\n    flags = 0\n    data = length 415, hash B31BBC3B\n  sample 28:\n    time = 967000\n    flags = 0\n    data = length 415, hash 850DFEA3\n  sample 29:\n    time = 1033000\n    flags = 0\n    data = length 619, hash AB5E56CA\ntrack 1:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n      data = length 5, hash 2B7623A\n  total output bytes = 13359\n  sample count = 31\n  sample 0:\n    time = 348000\n    flags = 1\n    data = length 456, hash 1E9034C6\n  sample 1:\n    time = 371000\n    flags = 1\n    data = length 398, hash 99DB7345\n  sample 2:\n    time = 394000\n    flags = 1\n    data = length 474, hash 3F05F10A\n  sample 3:\n    time = 417000\n    flags = 1\n    data = length 416, hash C105EE09\n  sample 4:\n    time = 441000\n    flags = 1\n    data = length 454, hash 5FDBE458\n  sample 5:\n    time = 464000\n    flags = 1\n    data = length 438, hash 41A93AC3\n  sample 6:\n    time = 487000\n    flags = 1\n    data = length 443, hash 10FDA652\n  sample 7:\n    time = 510000\n    flags = 1\n    data = length 412, hash 1F791E25\n  sample 8:\n    time = 534000\n    flags = 1\n    data = length 482, hash A6D983D\n  sample 9:\n    time = 557000\n    flags = 1\n    data = length 386, hash BED7392F\n  sample 10:\n    time = 580000\n    flags = 1\n    data = length 463, hash 5309F8C9\n  sample 11:\n    time = 603000\n    flags = 1\n    data = length 394, hash 21C7321F\n  sample 12:\n    time = 626000\n    flags = 1\n    data = length 489, hash 71B4730D\n  sample 13:\n    time = 650000\n    flags = 1\n    data = length 403, hash D9C6DE89\n  sample 14:\n    time = 673000\n    flags = 1\n    data = length 447, hash 9B14B73B\n  sample 15:\n    time = 696000\n    flags = 1\n    data = length 439, hash 4760D35B\n  sample 16:\n    time = 719000\n    flags = 1\n    data = length 463, hash 1601F88D\n  sample 17:\n    time = 743000\n    flags = 1\n    data = length 423, hash D4AE6773\n  sample 18:\n    time = 766000\n    flags = 1\n    data = length 497, hash A3C674D3\n  sample 19:\n    time = 789000\n    flags = 1\n    data = length 419, hash D3734A1F\n  sample 20:\n    time = 812000\n    flags = 1\n    data = length 474, hash DFB41F9\n  sample 21:\n    time = 835000\n    flags = 1\n    data = length 413, hash 53E7CB9F\n  sample 22:\n    time = 859000\n    flags = 1\n    data = length 445, hash D15B0E39\n  sample 23:\n    time = 882000\n    flags = 1\n    data = length 453, hash 77ED81E4\n  sample 24:\n    time = 905000\n    flags = 1\n    data = length 545, hash 3321AEB9\n  sample 25:\n    time = 928000\n    flags = 1\n    data = length 317, hash F557D0E\n  sample 26:\n    time = 952000\n    flags = 1\n    data = length 537, hash ED58CF7B\n  sample 27:\n    time = 975000\n    flags = 1\n    data = length 458, hash 51CDAA10\n  sample 28:\n    time = 998000\n    flags = 1\n    data = length 465, hash CBA1EFD7\n  sample 29:\n    time = 1021000\n    flags = 1\n    data = length 446, hash D6735B8A\n  sample 30:\n    time = 1044000\n    flags = 1\n    data = length 10, hash A453EEBE\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1067733\n  getPosition(0) = [[timeUs=66733, position=1325]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = -1\n    width = 1080\n    height = 720\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 29, hash 4746B5D9\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 85933\n  sample count = 30\n  sample 0:\n    time = 66000\n    flags = 1\n    data = length 38070, hash B58E1AEE\n  sample 1:\n    time = 199000\n    flags = 0\n    data = length 8340, hash 8AC449FF\n  sample 2:\n    time = 132000\n    flags = 0\n    data = length 1295, hash C0DA5090\n  sample 3:\n    time = 100000\n    flags = 0\n    data = length 469, hash D6E0A200\n  sample 4:\n    time = 166000\n    flags = 0\n    data = length 564, hash E5F56C5B\n  sample 5:\n    time = 332000\n    flags = 0\n    data = length 6075, hash 8756E49E\n  sample 6:\n    time = 266000\n    flags = 0\n    data = length 847, hash DCC2B618\n  sample 7:\n    time = 233000\n    flags = 0\n    data = length 455, hash B9CCE047\n  sample 8:\n    time = 299000\n    flags = 0\n    data = length 467, hash 69806D94\n  sample 9:\n    time = 466000\n    flags = 0\n    data = length 4549, hash 3944F501\n  sample 10:\n    time = 399000\n    flags = 0\n    data = length 1087, hash 491BF106\n  sample 11:\n    time = 367000\n    flags = 0\n    data = length 380, hash 5FED016A\n  sample 12:\n    time = 433000\n    flags = 0\n    data = length 455, hash 8A0610\n  sample 13:\n    time = 599000\n    flags = 0\n    data = length 5190, hash B9031D8\n  sample 14:\n    time = 533000\n    flags = 0\n    data = length 1071, hash 684E7DC8\n  sample 15:\n    time = 500000\n    flags = 0\n    data = length 653, hash 8494F326\n  sample 16:\n    time = 566000\n    flags = 0\n    data = length 485, hash 2CCC85F4\n  sample 17:\n    time = 733000\n    flags = 0\n    data = length 4884, hash D16B6A96\n  sample 18:\n    time = 666000\n    flags = 0\n    data = length 997, hash 164FF210\n  sample 19:\n    time = 633000\n    flags = 0\n    data = length 640, hash F664125B\n  sample 20:\n    time = 700000\n    flags = 0\n    data = length 491, hash B5930C7C\n  sample 21:\n    time = 866000\n    flags = 0\n    data = length 2989, hash 92CF4FCF\n  sample 22:\n    time = 800000\n    flags = 0\n    data = length 838, hash 294A3451\n  sample 23:\n    time = 767000\n    flags = 0\n    data = length 544, hash FCCE2DE6\n  sample 24:\n    time = 833000\n    flags = 0\n    data = length 329, hash A654FFA1\n  sample 25:\n    time = 1000000\n    flags = 0\n    data = length 1517, hash 5F7EBF8B\n  sample 26:\n    time = 933000\n    flags = 0\n    data = length 803, hash 7A5C4C1D\n  sample 27:\n    time = 900000\n    flags = 0\n    data = length 415, hash B31BBC3B\n  sample 28:\n    time = 967000\n    flags = 0\n    data = length 415, hash 850DFEA3\n  sample 29:\n    time = 1033000\n    flags = 0\n    data = length 619, hash AB5E56CA\ntrack 1:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n      data = length 5, hash 2B7623A\n  total output bytes = 6804\n  sample count = 16\n  sample 0:\n    time = 696000\n    flags = 1\n    data = length 439, hash 4760D35B\n  sample 1:\n    time = 719000\n    flags = 1\n    data = length 463, hash 1601F88D\n  sample 2:\n    time = 743000\n    flags = 1\n    data = length 423, hash D4AE6773\n  sample 3:\n    time = 766000\n    flags = 1\n    data = length 497, hash A3C674D3\n  sample 4:\n    time = 789000\n    flags = 1\n    data = length 419, hash D3734A1F\n  sample 5:\n    time = 812000\n    flags = 1\n    data = length 474, hash DFB41F9\n  sample 6:\n    time = 835000\n    flags = 1\n    data = length 413, hash 53E7CB9F\n  sample 7:\n    time = 859000\n    flags = 1\n    data = length 445, hash D15B0E39\n  sample 8:\n    time = 882000\n    flags = 1\n    data = length 453, hash 77ED81E4\n  sample 9:\n    time = 905000\n    flags = 1\n    data = length 545, hash 3321AEB9\n  sample 10:\n    time = 928000\n    flags = 1\n    data = length 317, hash F557D0E\n  sample 11:\n    time = 952000\n    flags = 1\n    data = length 537, hash ED58CF7B\n  sample 12:\n    time = 975000\n    flags = 1\n    data = length 458, hash 51CDAA10\n  sample 13:\n    time = 998000\n    flags = 1\n    data = length 465, hash CBA1EFD7\n  sample 14:\n    time = 1021000\n    flags = 1\n    data = length 446, hash D6735B8A\n  sample 15:\n    time = 1044000\n    flags = 1\n    data = length 10, hash A453EEBE\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1067733\n  getPosition(0) = [[timeUs=66733, position=1325]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = -1\n    width = 1080\n    height = 720\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 29, hash 4746B5D9\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 85933\n  sample count = 30\n  sample 0:\n    time = 66000\n    flags = 1\n    data = length 38070, hash B58E1AEE\n  sample 1:\n    time = 199000\n    flags = 0\n    data = length 8340, hash 8AC449FF\n  sample 2:\n    time = 132000\n    flags = 0\n    data = length 1295, hash C0DA5090\n  sample 3:\n    time = 100000\n    flags = 0\n    data = length 469, hash D6E0A200\n  sample 4:\n    time = 166000\n    flags = 0\n    data = length 564, hash E5F56C5B\n  sample 5:\n    time = 332000\n    flags = 0\n    data = length 6075, hash 8756E49E\n  sample 6:\n    time = 266000\n    flags = 0\n    data = length 847, hash DCC2B618\n  sample 7:\n    time = 233000\n    flags = 0\n    data = length 455, hash B9CCE047\n  sample 8:\n    time = 299000\n    flags = 0\n    data = length 467, hash 69806D94\n  sample 9:\n    time = 466000\n    flags = 0\n    data = length 4549, hash 3944F501\n  sample 10:\n    time = 399000\n    flags = 0\n    data = length 1087, hash 491BF106\n  sample 11:\n    time = 367000\n    flags = 0\n    data = length 380, hash 5FED016A\n  sample 12:\n    time = 433000\n    flags = 0\n    data = length 455, hash 8A0610\n  sample 13:\n    time = 599000\n    flags = 0\n    data = length 5190, hash B9031D8\n  sample 14:\n    time = 533000\n    flags = 0\n    data = length 1071, hash 684E7DC8\n  sample 15:\n    time = 500000\n    flags = 0\n    data = length 653, hash 8494F326\n  sample 16:\n    time = 566000\n    flags = 0\n    data = length 485, hash 2CCC85F4\n  sample 17:\n    time = 733000\n    flags = 0\n    data = length 4884, hash D16B6A96\n  sample 18:\n    time = 666000\n    flags = 0\n    data = length 997, hash 164FF210\n  sample 19:\n    time = 633000\n    flags = 0\n    data = length 640, hash F664125B\n  sample 20:\n    time = 700000\n    flags = 0\n    data = length 491, hash B5930C7C\n  sample 21:\n    time = 866000\n    flags = 0\n    data = length 2989, hash 92CF4FCF\n  sample 22:\n    time = 800000\n    flags = 0\n    data = length 838, hash 294A3451\n  sample 23:\n    time = 767000\n    flags = 0\n    data = length 544, hash FCCE2DE6\n  sample 24:\n    time = 833000\n    flags = 0\n    data = length 329, hash A654FFA1\n  sample 25:\n    time = 1000000\n    flags = 0\n    data = length 1517, hash 5F7EBF8B\n  sample 26:\n    time = 933000\n    flags = 0\n    data = length 803, hash 7A5C4C1D\n  sample 27:\n    time = 900000\n    flags = 0\n    data = length 415, hash B31BBC3B\n  sample 28:\n    time = 967000\n    flags = 0\n    data = length 415, hash 850DFEA3\n  sample 29:\n    time = 1033000\n    flags = 0\n    data = length 619, hash AB5E56CA\ntrack 1:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n      data = length 5, hash 2B7623A\n  total output bytes = 10\n  sample count = 1\n  sample 0:\n    time = 1044000\n    flags = 1\n    data = length 10, hash A453EEBE\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=1828]]\nnumberOfTracks = 3\ntrack 0:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = video/avc\n    maxInputSize = -1\n    width = 1080\n    height = 720\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 29, hash 4746B5D9\n      data = length 10, hash 7A0D0F2B\n  total output bytes = 85933\n  sample count = 30\n  sample 0:\n    time = 66000\n    flags = 1\n    data = length 38070, hash B58E1AEE\n  sample 1:\n    time = 199000\n    flags = 0\n    data = length 8340, hash 8AC449FF\n  sample 2:\n    time = 132000\n    flags = 0\n    data = length 1295, hash C0DA5090\n  sample 3:\n    time = 100000\n    flags = 0\n    data = length 469, hash D6E0A200\n  sample 4:\n    time = 166000\n    flags = 0\n    data = length 564, hash E5F56C5B\n  sample 5:\n    time = 332000\n    flags = 0\n    data = length 6075, hash 8756E49E\n  sample 6:\n    time = 266000\n    flags = 0\n    data = length 847, hash DCC2B618\n  sample 7:\n    time = 233000\n    flags = 0\n    data = length 455, hash B9CCE047\n  sample 8:\n    time = 299000\n    flags = 0\n    data = length 467, hash 69806D94\n  sample 9:\n    time = 466000\n    flags = 0\n    data = length 4549, hash 3944F501\n  sample 10:\n    time = 399000\n    flags = 0\n    data = length 1087, hash 491BF106\n  sample 11:\n    time = 367000\n    flags = 0\n    data = length 380, hash 5FED016A\n  sample 12:\n    time = 433000\n    flags = 0\n    data = length 455, hash 8A0610\n  sample 13:\n    time = 599000\n    flags = 0\n    data = length 5190, hash B9031D8\n  sample 14:\n    time = 533000\n    flags = 0\n    data = length 1071, hash 684E7DC8\n  sample 15:\n    time = 500000\n    flags = 0\n    data = length 653, hash 8494F326\n  sample 16:\n    time = 566000\n    flags = 0\n    data = length 485, hash 2CCC85F4\n  sample 17:\n    time = 733000\n    flags = 0\n    data = length 4884, hash D16B6A96\n  sample 18:\n    time = 666000\n    flags = 0\n    data = length 997, hash 164FF210\n  sample 19:\n    time = 633000\n    flags = 0\n    data = length 640, hash F664125B\n  sample 20:\n    time = 700000\n    flags = 0\n    data = length 491, hash B5930C7C\n  sample 21:\n    time = 866000\n    flags = 0\n    data = length 2989, hash 92CF4FCF\n  sample 22:\n    time = 800000\n    flags = 0\n    data = length 838, hash 294A3451\n  sample 23:\n    time = 767000\n    flags = 0\n    data = length 544, hash FCCE2DE6\n  sample 24:\n    time = 833000\n    flags = 0\n    data = length 329, hash A654FFA1\n  sample 25:\n    time = 1000000\n    flags = 0\n    data = length 1517, hash 5F7EBF8B\n  sample 26:\n    time = 933000\n    flags = 0\n    data = length 803, hash 7A5C4C1D\n  sample 27:\n    time = 900000\n    flags = 0\n    data = length 415, hash B31BBC3B\n  sample 28:\n    time = 967000\n    flags = 0\n    data = length 415, hash 850DFEA3\n  sample 29:\n    time = 1033000\n    flags = 0\n    data = length 619, hash AB5E56CA\ntrack 1:\n  format:\n    bitrate = -1\n    id = 2\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n      data = length 5, hash 2B7623A\n  total output bytes = 18257\n  sample count = 46\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 18, hash 96519432\n  sample 1:\n    time = 23000\n    flags = 1\n    data = length 4, hash EE9DF\n  sample 2:\n    time = 46000\n    flags = 1\n    data = length 4, hash EEDBF\n  sample 3:\n    time = 69000\n    flags = 1\n    data = length 157, hash E2F078F4\n  sample 4:\n    time = 92000\n    flags = 1\n    data = length 371, hash B9471F94\n  sample 5:\n    time = 116000\n    flags = 1\n    data = length 373, hash 2AB265CB\n  sample 6:\n    time = 139000\n    flags = 1\n    data = length 402, hash 1295477C\n  sample 7:\n    time = 162000\n    flags = 1\n    data = length 455, hash 2D8146C8\n  sample 8:\n    time = 185000\n    flags = 1\n    data = length 434, hash F2C5D287\n  sample 9:\n    time = 208000\n    flags = 1\n    data = length 450, hash 84143FCD\n  sample 10:\n    time = 232000\n    flags = 1\n    data = length 429, hash EF769D50\n  sample 11:\n    time = 255000\n    flags = 1\n    data = length 450, hash EC3DE692\n  sample 12:\n    time = 278000\n    flags = 1\n    data = length 447, hash 3E519E13\n  sample 13:\n    time = 301000\n    flags = 1\n    data = length 457, hash 1E4F23A0\n  sample 14:\n    time = 325000\n    flags = 1\n    data = length 447, hash A439EA97\n  sample 15:\n    time = 348000\n    flags = 1\n    data = length 456, hash 1E9034C6\n  sample 16:\n    time = 371000\n    flags = 1\n    data = length 398, hash 99DB7345\n  sample 17:\n    time = 394000\n    flags = 1\n    data = length 474, hash 3F05F10A\n  sample 18:\n    time = 417000\n    flags = 1\n    data = length 416, hash C105EE09\n  sample 19:\n    time = 441000\n    flags = 1\n    data = length 454, hash 5FDBE458\n  sample 20:\n    time = 464000\n    flags = 1\n    data = length 438, hash 41A93AC3\n  sample 21:\n    time = 487000\n    flags = 1\n    data = length 443, hash 10FDA652\n  sample 22:\n    time = 510000\n    flags = 1\n    data = length 412, hash 1F791E25\n  sample 23:\n    time = 534000\n    flags = 1\n    data = length 482, hash A6D983D\n  sample 24:\n    time = 557000\n    flags = 1\n    data = length 386, hash BED7392F\n  sample 25:\n    time = 580000\n    flags = 1\n    data = length 463, hash 5309F8C9\n  sample 26:\n    time = 603000\n    flags = 1\n    data = length 394, hash 21C7321F\n  sample 27:\n    time = 626000\n    flags = 1\n    data = length 489, hash 71B4730D\n  sample 28:\n    time = 650000\n    flags = 1\n    data = length 403, hash D9C6DE89\n  sample 29:\n    time = 673000\n    flags = 1\n    data = length 447, hash 9B14B73B\n  sample 30:\n    time = 696000\n    flags = 1\n    data = length 439, hash 4760D35B\n  sample 31:\n    time = 719000\n    flags = 1\n    data = length 463, hash 1601F88D\n  sample 32:\n    time = 743000\n    flags = 1\n    data = length 423, hash D4AE6773\n  sample 33:\n    time = 766000\n    flags = 1\n    data = length 497, hash A3C674D3\n  sample 34:\n    time = 789000\n    flags = 1\n    data = length 419, hash D3734A1F\n  sample 35:\n    time = 812000\n    flags = 1\n    data = length 474, hash DFB41F9\n  sample 36:\n    time = 835000\n    flags = 1\n    data = length 413, hash 53E7CB9F\n  sample 37:\n    time = 859000\n    flags = 1\n    data = length 445, hash D15B0E39\n  sample 38:\n    time = 882000\n    flags = 1\n    data = length 453, hash 77ED81E4\n  sample 39:\n    time = 905000\n    flags = 1\n    data = length 545, hash 3321AEB9\n  sample 40:\n    time = 928000\n    flags = 1\n    data = length 317, hash F557D0E\n  sample 41:\n    time = 952000\n    flags = 1\n    data = length 537, hash ED58CF7B\n  sample 42:\n    time = 975000\n    flags = 1\n    data = length 458, hash 51CDAA10\n  sample 43:\n    time = 998000\n    flags = 1\n    data = length 465, hash CBA1EFD7\n  sample 44:\n    time = 1021000\n    flags = 1\n    data = length 446, hash D6735B8A\n  sample 45:\n    time = 1044000\n    flags = 1\n    data = length 10, hash A453EEBE\ntrack 3:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = application/cea-608\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/offline/action_file_no_data.exi",
    "content": ""
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear.opus.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2747500\n  getPosition(0) = [[timeUs=0, position=125]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/opus\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 19, hash BFE794DB\n      data = length 8, hash CA22068C\n      data = length 8, hash 79C07075\n  total output bytes = 25541\n  sample count = 275\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 234, hash B77BFFDA\n  sample 1:\n    time = 10000\n    flags = 1\n    data = length 165, hash F7B07C35\n  sample 2:\n    time = 20000\n    flags = 1\n    data = length 100, hash 21AFA81F\n  sample 3:\n    time = 30000\n    flags = 1\n    data = length 85, hash 9180DC2F\n  sample 4:\n    time = 40000\n    flags = 1\n    data = length 85, hash 6AE958C\n  sample 5:\n    time = 50000\n    flags = 1\n    data = length 86, hash C1C5AE60\n  sample 6:\n    time = 60000\n    flags = 1\n    data = length 87, hash B9BD2620\n  sample 7:\n    time = 70000\n    flags = 1\n    data = length 86, hash 5E69E6F9\n  sample 8:\n    time = 80000\n    flags = 1\n    data = length 90, hash C44C7DD9\n  sample 9:\n    time = 90000\n    flags = 1\n    data = length 86, hash C3FCDC6F\n  sample 10:\n    time = 100000\n    flags = 1\n    data = length 86, hash 44EA03BA\n  sample 11:\n    time = 110000\n    flags = 1\n    data = length 160, hash 9F4E1AE8\n  sample 12:\n    time = 120000\n    flags = 1\n    data = length 89, hash 31234460\n  sample 13:\n    time = 130000\n    flags = 1\n    data = length 91, hash 45012D79\n  sample 14:\n    time = 140000\n    flags = 1\n    data = length 90, hash B3E3AC75\n  sample 15:\n    time = 150000\n    flags = 1\n    data = length 87, hash B83B756B\n  sample 16:\n    time = 160000\n    flags = 1\n    data = length 86, hash 383921EB\n  sample 17:\n    time = 170000\n    flags = 1\n    data = length 97, hash 959AD270\n  sample 18:\n    time = 180000\n    flags = 1\n    data = length 92, hash 46C74FA8\n  sample 19:\n    time = 190000\n    flags = 1\n    data = length 91, hash CEA401DD\n  sample 20:\n    time = 200000\n    flags = 1\n    data = length 89, hash 48C41853\n  sample 21:\n    time = 210000\n    flags = 1\n    data = length 90, hash F23245BD\n  sample 22:\n    time = 220000\n    flags = 1\n    data = length 96, hash 95E8985D\n  sample 23:\n    time = 230000\n    flags = 1\n    data = length 96, hash 34295492\n  sample 24:\n    time = 240000\n    flags = 1\n    data = length 94, hash 4E3C9C0F\n  sample 25:\n    time = 250000\n    flags = 1\n    data = length 89, hash 28B74A29\n  sample 26:\n    time = 260000\n    flags = 1\n    data = length 87, hash BAC119A7\n  sample 27:\n    time = 270000\n    flags = 1\n    data = length 88, hash 7139FF38\n  sample 28:\n    time = 280000\n    flags = 1\n    data = length 85, hash 246E1D2A\n  sample 29:\n    time = 290000\n    flags = 1\n    data = length 86, hash 488A0900\n  sample 30:\n    time = 300000\n    flags = 1\n    data = length 90, hash 16FD17B1\n  sample 31:\n    time = 310000\n    flags = 1\n    data = length 87, hash 20E849D9\n  sample 32:\n    time = 320000\n    flags = 1\n    data = length 86, hash 23A0E9BA\n  sample 33:\n    time = 330000\n    flags = 1\n    data = length 87, hash EC935537\n  sample 34:\n    time = 340000\n    flags = 1\n    data = length 92, hash 4D9935AD\n  sample 35:\n    time = 350000\n    flags = 1\n    data = length 87, hash DEDE3FA\n  sample 36:\n    time = 360000\n    flags = 1\n    data = length 87, hash ADC25A6E\n  sample 37:\n    time = 370000\n    flags = 1\n    data = length 88, hash A1C828C5\n  sample 38:\n    time = 380000\n    flags = 1\n    data = length 89, hash 735C087A\n  sample 39:\n    time = 390000\n    flags = 1\n    data = length 89, hash 19AF5D10\n  sample 40:\n    time = 400000\n    flags = 1\n    data = length 90, hash BCCEA2BB\n  sample 41:\n    time = 410000\n    flags = 1\n    data = length 86, hash A7C934A0\n  sample 42:\n    time = 420000\n    flags = 1\n    data = length 86, hash 28BBC0A8\n  sample 43:\n    time = 430000\n    flags = 1\n    data = length 85, hash E60BB12D\n  sample 44:\n    time = 440000\n    flags = 1\n    data = length 141, hash 1D2B8920\n  sample 45:\n    time = 450000\n    flags = 1\n    data = length 121, hash 8AA3E19C\n  sample 46:\n    time = 460000\n    flags = 1\n    data = length 86, hash 24DF0F37\n  sample 47:\n    time = 470000\n    flags = 1\n    data = length 86, hash 1D1775FF\n  sample 48:\n    time = 480000\n    flags = 1\n    data = length 87, hash 5230399E\n  sample 49:\n    time = 490000\n    flags = 1\n    data = length 91, hash 6CD98305\n  sample 50:\n    time = 500000\n    flags = 1\n    data = length 88, hash 4069FBB\n  sample 51:\n    time = 510000\n    flags = 1\n    data = length 89, hash 76824ABF\n  sample 52:\n    time = 520000\n    flags = 1\n    data = length 87, hash BC1B1322\n  sample 53:\n    time = 530000\n    flags = 1\n    data = length 102, hash E01BA053\n  sample 54:\n    time = 540000\n    flags = 1\n    data = length 85, hash C09B626D\n  sample 55:\n    time = 550000\n    flags = 1\n    data = length 88, hash 6B7B404A\n  sample 56:\n    time = 560000\n    flags = 1\n    data = length 85, hash 74A15DC7\n  sample 57:\n    time = 570000\n    flags = 1\n    data = length 88, hash 38DB82E5\n  sample 58:\n    time = 580000\n    flags = 1\n    data = length 86, hash 1A39C081\n  sample 59:\n    time = 590000\n    flags = 1\n    data = length 87, hash 39FEC92\n  sample 60:\n    time = 600000\n    flags = 1\n    data = length 92, hash 278EA09\n  sample 61:\n    time = 610000\n    flags = 1\n    data = length 87, hash 28265F2D\n  sample 62:\n    time = 620000\n    flags = 1\n    data = length 86, hash CC2040C6\n  sample 63:\n    time = 630000\n    flags = 1\n    data = length 138, hash 9E07BC1F\n  sample 64:\n    time = 640000\n    flags = 1\n    data = length 85, hash 4F299670\n  sample 65:\n    time = 650000\n    flags = 1\n    data = length 125, hash B61123C3\n  sample 66:\n    time = 660000\n    flags = 1\n    data = length 89, hash 5CC688ED\n  sample 67:\n    time = 670000\n    flags = 1\n    data = length 88, hash 84AF64A6\n  sample 68:\n    time = 680000\n    flags = 1\n    data = length 89, hash A9BFC8DC\n  sample 69:\n    time = 690000\n    flags = 1\n    data = length 90, hash 2FF77060\n  sample 70:\n    time = 700000\n    flags = 1\n    data = length 96, hash E11AFD61\n  sample 71:\n    time = 710000\n    flags = 1\n    data = length 87, hash 85D14EDA\n  sample 72:\n    time = 720000\n    flags = 1\n    data = length 88, hash 5FC71D53\n  sample 73:\n    time = 730000\n    flags = 1\n    data = length 89, hash 957187B6\n  sample 74:\n    time = 740000\n    flags = 1\n    data = length 89, hash 5A776082\n  sample 75:\n    time = 750000\n    flags = 1\n    data = length 87, hash E8A83AFE\n  sample 76:\n    time = 760000\n    flags = 1\n    data = length 87, hash F1989133\n  sample 77:\n    time = 770000\n    flags = 1\n    data = length 87, hash FA06BCCA\n  sample 78:\n    time = 780000\n    flags = 1\n    data = length 86, hash BD97E0C0\n  sample 79:\n    time = 790000\n    flags = 1\n    data = length 88, hash E6AE022C\n  sample 80:\n    time = 800000\n    flags = 1\n    data = length 87, hash FB6C6169\n  sample 81:\n    time = 810000\n    flags = 1\n    data = length 87, hash DFFCD2CF\n  sample 82:\n    time = 820000\n    flags = 1\n    data = length 88, hash A4B5EB52\n  sample 83:\n    time = 830000\n    flags = 1\n    data = length 85, hash A63CF4EA\n  sample 84:\n    time = 840000\n    flags = 1\n    data = length 86, hash F126E7C7\n  sample 85:\n    time = 850000\n    flags = 1\n    data = length 86, hash 21A8B22F\n  sample 86:\n    time = 860000\n    flags = 1\n    data = length 87, hash 6520E7C1\n  sample 87:\n    time = 870000\n    flags = 1\n    data = length 88, hash 825B7423\n  sample 88:\n    time = 880000\n    flags = 1\n    data = length 88, hash DF3DBD48\n  sample 89:\n    time = 890000\n    flags = 1\n    data = length 87, hash B32C68D0\n  sample 90:\n    time = 900000\n    flags = 1\n    data = length 89, hash B99DFFCA\n  sample 91:\n    time = 910000\n    flags = 1\n    data = length 88, hash 9C8D5178\n  sample 92:\n    time = 920000\n    flags = 1\n    data = length 88, hash 48A0B19A\n  sample 93:\n    time = 930000\n    flags = 1\n    data = length 88, hash B62C94DD\n  sample 94:\n    time = 940000\n    flags = 1\n    data = length 92, hash 96DBDD46\n  sample 95:\n    time = 950000\n    flags = 1\n    data = length 87, hash 7B80E6F\n  sample 96:\n    time = 960000\n    flags = 1\n    data = length 86, hash 9C60225B\n  sample 97:\n    time = 970000\n    flags = 1\n    data = length 87, hash 45F7E6E8\n  sample 98:\n    time = 980000\n    flags = 1\n    data = length 87, hash DDC2D592\n  sample 99:\n    time = 990000\n    flags = 1\n    data = length 91, hash 173D3B26\n  sample 100:\n    time = 1000000\n    flags = 1\n    data = length 87, hash CF3629DF\n  sample 101:\n    time = 1010000\n    flags = 1\n    data = length 87, hash BBE2E7B3\n  sample 102:\n    time = 1020000\n    flags = 1\n    data = length 89, hash 89AFFB10\n  sample 103:\n    time = 1030000\n    flags = 1\n    data = length 88, hash 510DCC90\n  sample 104:\n    time = 1040000\n    flags = 1\n    data = length 88, hash CBA56E5F\n  sample 105:\n    time = 1050000\n    flags = 1\n    data = length 87, hash B4B1B3FF\n  sample 106:\n    time = 1060000\n    flags = 1\n    data = length 89, hash B976A537\n  sample 107:\n    time = 1070000\n    flags = 1\n    data = length 96, hash 43ECF2C1\n  sample 108:\n    time = 1080000\n    flags = 1\n    data = length 90, hash BB7ECB44\n  sample 109:\n    time = 1090000\n    flags = 1\n    data = length 89, hash B8E221A5\n  sample 110:\n    time = 1100000\n    flags = 1\n    data = length 86, hash B35BEF5B\n  sample 111:\n    time = 1110000\n    flags = 1\n    data = length 89, hash 9002F0EC\n  sample 112:\n    time = 1120000\n    flags = 1\n    data = length 85, hash F694B20\n  sample 113:\n    time = 1130000\n    flags = 1\n    data = length 87, hash D7CC386E\n  sample 114:\n    time = 1140000\n    flags = 1\n    data = length 89, hash EE9E0E79\n  sample 115:\n    time = 1150000\n    flags = 1\n    data = length 90, hash CA72C96B\n  sample 116:\n    time = 1160000\n    flags = 1\n    data = length 112, hash 4AD3D1B1\n  sample 117:\n    time = 1170000\n    flags = 1\n    data = length 87, hash FA568FAB\n  sample 118:\n    time = 1180000\n    flags = 1\n    data = length 90, hash 6E6948D2\n  sample 119:\n    time = 1190000\n    flags = 1\n    data = length 89, hash 5465A762\n  sample 120:\n    time = 1200000\n    flags = 1\n    data = length 87, hash BED19B40\n  sample 121:\n    time = 1210000\n    flags = 1\n    data = length 89, hash 5D05836A\n  sample 122:\n    time = 1220000\n    flags = 1\n    data = length 87, hash A8A3EF5A\n  sample 123:\n    time = 1230000\n    flags = 1\n    data = length 90, hash 6425A77A\n  sample 124:\n    time = 1240000\n    flags = 1\n    data = length 92, hash 7F320FA\n  sample 125:\n    time = 1250000\n    flags = 1\n    data = length 89, hash 2C7837D6\n  sample 126:\n    time = 1260000\n    flags = 1\n    data = length 86, hash 58D56685\n  sample 127:\n    time = 1270000\n    flags = 1\n    data = length 91, hash ADC5072F\n  sample 128:\n    time = 1280000\n    flags = 1\n    data = length 85, hash 53EFD93\n  sample 129:\n    time = 1290000\n    flags = 1\n    data = length 87, hash D006B535\n  sample 130:\n    time = 1300000\n    flags = 1\n    data = length 86, hash AE944625\n  sample 131:\n    time = 1310000\n    flags = 1\n    data = length 89, hash B5D3C81D\n  sample 132:\n    time = 1320000\n    flags = 1\n    data = length 86, hash 3BB1D0E7\n  sample 133:\n    time = 1330000\n    flags = 1\n    data = length 102, hash 16EEC441\n  sample 134:\n    time = 1340000\n    flags = 1\n    data = length 90, hash 1005B936\n  sample 135:\n    time = 1350000\n    flags = 1\n    data = length 85, hash 15EEBF9A\n  sample 136:\n    time = 1360000\n    flags = 1\n    data = length 87, hash 37C83AC2\n  sample 137:\n    time = 1370000\n    flags = 1\n    data = length 85, hash 2D27855D\n  sample 138:\n    time = 1380000\n    flags = 1\n    data = length 85, hash 753EB7C6\n  sample 139:\n    time = 1390000\n    flags = 1\n    data = length 91, hash C0813318\n  sample 140:\n    time = 1400000\n    flags = 1\n    data = length 89, hash 3A6468AC\n  sample 141:\n    time = 1410000\n    flags = 1\n    data = length 88, hash 3D220ABC\n  sample 142:\n    time = 1420000\n    flags = 1\n    data = length 140, hash 7949ABC7\n  sample 143:\n    time = 1430000\n    flags = 1\n    data = length 92, hash F19AFA45\n  sample 144:\n    time = 1440000\n    flags = 1\n    data = length 90, hash 3D21587C\n  sample 145:\n    time = 1450000\n    flags = 1\n    data = length 89, hash 5C12226C\n  sample 146:\n    time = 1460000\n    flags = 1\n    data = length 90, hash 22BA14FC\n  sample 147:\n    time = 1470000\n    flags = 1\n    data = length 88, hash F064B21C\n  sample 148:\n    time = 1480000\n    flags = 1\n    data = length 87, hash 6D7906B9\n  sample 149:\n    time = 1490000\n    flags = 1\n    data = length 88, hash 6756A484\n  sample 150:\n    time = 1500000\n    flags = 1\n    data = length 91, hash C95C00B6\n  sample 151:\n    time = 1510000\n    flags = 1\n    data = length 87, hash 728D8119\n  sample 152:\n    time = 1520000\n    flags = 1\n    data = length 90, hash C43DA1B4\n  sample 153:\n    time = 1530000\n    flags = 1\n    data = length 88, hash C181BB57\n  sample 154:\n    time = 1540000\n    flags = 1\n    data = length 84, hash F75B1639\n  sample 155:\n    time = 1550000\n    flags = 1\n    data = length 87, hash B6F32978\n  sample 156:\n    time = 1560000\n    flags = 1\n    data = length 90, hash 36D6E2D7\n  sample 157:\n    time = 1570000\n    flags = 1\n    data = length 87, hash 4C9657A7\n  sample 158:\n    time = 1580000\n    flags = 1\n    data = length 89, hash C3BDB9B7\n  sample 159:\n    time = 1590000\n    flags = 1\n    data = length 88, hash DB51087E\n  sample 160:\n    time = 1600000\n    flags = 1\n    data = length 86, hash 1550F998\n  sample 161:\n    time = 1610000\n    flags = 1\n    data = length 86, hash A445FAD4\n  sample 162:\n    time = 1620000\n    flags = 1\n    data = length 85, hash 60D3362C\n  sample 163:\n    time = 1630000\n    flags = 1\n    data = length 172, hash 945D63E4\n  sample 164:\n    time = 1640000\n    flags = 1\n    data = length 107, hash 585B7C04\n  sample 165:\n    time = 1650000\n    flags = 1\n    data = length 110, hash 74BECF69\n  sample 166:\n    time = 1660000\n    flags = 1\n    data = length 87, hash 63DE1D24\n  sample 167:\n    time = 1670000\n    flags = 1\n    data = length 90, hash 1C1D28DB\n  sample 168:\n    time = 1680000\n    flags = 1\n    data = length 87, hash CB382A67\n  sample 169:\n    time = 1690000\n    flags = 1\n    data = length 85, hash A227BA13\n  sample 170:\n    time = 1700000\n    flags = 1\n    data = length 86, hash EFD8B10B\n  sample 171:\n    time = 1710000\n    flags = 1\n    data = length 87, hash 47FF364A\n  sample 172:\n    time = 1720000\n    flags = 1\n    data = length 91, hash 31D4B48A\n  sample 173:\n    time = 1730000\n    flags = 1\n    data = length 91, hash DD69BD85\n  sample 174:\n    time = 1740000\n    flags = 1\n    data = length 88, hash AF1A95C6\n  sample 175:\n    time = 1750000\n    flags = 1\n    data = length 87, hash 2FB8AF74\n  sample 176:\n    time = 1760000\n    flags = 1\n    data = length 92, hash 173C707A\n  sample 177:\n    time = 1770000\n    flags = 1\n    data = length 88, hash 5F58F5E8\n  sample 178:\n    time = 1780000\n    flags = 1\n    data = length 91, hash D449785F\n  sample 179:\n    time = 1790000\n    flags = 1\n    data = length 91, hash CE2CB465\n  sample 180:\n    time = 1800000\n    flags = 1\n    data = length 93, hash ABC1C62E\n  sample 181:\n    time = 1810000\n    flags = 1\n    data = length 87, hash 83B4B9A0\n  sample 182:\n    time = 1820000\n    flags = 1\n    data = length 87, hash 3220D562\n  sample 183:\n    time = 1830000\n    flags = 1\n    data = length 86, hash 64D21AA1\n  sample 184:\n    time = 1840000\n    flags = 1\n    data = length 86, hash A1FAAF2C\n  sample 185:\n    time = 1850000\n    flags = 1\n    data = length 86, hash ECA80F7E\n  sample 186:\n    time = 1860000\n    flags = 1\n    data = length 86, hash FEB03B2C\n  sample 187:\n    time = 1870000\n    flags = 1\n    data = length 85, hash 2C2E6B2F\n  sample 188:\n    time = 1880000\n    flags = 1\n    data = length 89, hash A0D7AC3\n  sample 189:\n    time = 1890000\n    flags = 1\n    data = length 87, hash 83739547\n  sample 190:\n    time = 1900000\n    flags = 1\n    data = length 86, hash 991E531E\n  sample 191:\n    time = 1910000\n    flags = 1\n    data = length 88, hash 16B287A3\n  sample 192:\n    time = 1920000\n    flags = 1\n    data = length 86, hash FC86EED\n  sample 193:\n    time = 1930000\n    flags = 1\n    data = length 86, hash 96AF0248\n  sample 194:\n    time = 1940000\n    flags = 1\n    data = length 86, hash 288402C8\n  sample 195:\n    time = 1950000\n    flags = 1\n    data = length 87, hash 4BBA7912\n  sample 196:\n    time = 1960000\n    flags = 1\n    data = length 86, hash 4A59C719\n  sample 197:\n    time = 1970000\n    flags = 1\n    data = length 85, hash 906E8187\n  sample 198:\n    time = 1980000\n    flags = 1\n    data = length 90, hash CB8B755D\n  sample 199:\n    time = 1990000\n    flags = 1\n    data = length 87, hash C8E02C\n  sample 200:\n    time = 2000000\n    flags = 1\n    data = length 88, hash ACF4D89A\n  sample 201:\n    time = 2010000\n    flags = 1\n    data = length 86, hash 510FE048\n  sample 202:\n    time = 2020000\n    flags = 1\n    data = length 86, hash 64983E46\n  sample 203:\n    time = 2030000\n    flags = 1\n    data = length 86, hash CEA76A1E\n  sample 204:\n    time = 2040000\n    flags = 1\n    data = length 87, hash AADE498E\n  sample 205:\n    time = 2050000\n    flags = 1\n    data = length 127, hash 353A6D8C\n  sample 206:\n    time = 2060000\n    flags = 1\n    data = length 87, hash 29E18E62\n  sample 207:\n    time = 2070000\n    flags = 1\n    data = length 87, hash 2CF7B30F\n  sample 208:\n    time = 2080000\n    flags = 1\n    data = length 94, hash 758704C3\n  sample 209:\n    time = 2090000\n    flags = 1\n    data = length 88, hash C2153A4C\n  sample 210:\n    time = 2100000\n    flags = 1\n    data = length 86, hash A0A83DA5\n  sample 211:\n    time = 2110000\n    flags = 1\n    data = length 86, hash 41017D7F\n  sample 212:\n    time = 2120000\n    flags = 1\n    data = length 93, hash 686B0CA2\n  sample 213:\n    time = 2130000\n    flags = 1\n    data = length 86, hash 554D16CC\n  sample 214:\n    time = 2140000\n    flags = 1\n    data = length 88, hash 99D72771\n  sample 215:\n    time = 2150000\n    flags = 1\n    data = length 88, hash 7176DFBF\n  sample 216:\n    time = 2160000\n    flags = 1\n    data = length 86, hash BAA22669\n  sample 217:\n    time = 2170000\n    flags = 1\n    data = length 88, hash B00B0D3C\n  sample 218:\n    time = 2180000\n    flags = 1\n    data = length 89, hash 73FED83A\n  sample 219:\n    time = 2190000\n    flags = 1\n    data = length 86, hash 4A4138D3\n  sample 220:\n    time = 2200000\n    flags = 1\n    data = length 89, hash E0A860FF\n  sample 221:\n    time = 2210000\n    flags = 1\n    data = length 95, hash EE5A8AED\n  sample 222:\n    time = 2220000\n    flags = 1\n    data = length 92, hash 36DBD7FD\n  sample 223:\n    time = 2230000\n    flags = 1\n    data = length 88, hash EE47A7E4\n  sample 224:\n    time = 2240000\n    flags = 1\n    data = length 100, hash 2E1A603F\n  sample 225:\n    time = 2250000\n    flags = 1\n    data = length 89, hash 657ED4A3\n  sample 226:\n    time = 2260000\n    flags = 1\n    data = length 86, hash A833DC7B\n  sample 227:\n    time = 2270000\n    flags = 1\n    data = length 88, hash 81E80732\n  sample 228:\n    time = 2280000\n    flags = 1\n    data = length 91, hash FA256A0F\n  sample 229:\n    time = 2290000\n    flags = 1\n    data = length 88, hash A63A4DBA\n  sample 230:\n    time = 2300000\n    flags = 1\n    data = length 88, hash 67910A9F\n  sample 231:\n    time = 2310000\n    flags = 1\n    data = length 86, hash EB387DB6\n  sample 232:\n    time = 2320000\n    flags = 1\n    data = length 88, hash 5ACAAC2A\n  sample 233:\n    time = 2330000\n    flags = 1\n    data = length 86, hash 6ADF2E1F\n  sample 234:\n    time = 2340000\n    flags = 1\n    data = length 85, hash 9D064471\n  sample 235:\n    time = 2350000\n    flags = 1\n    data = length 87, hash F176C59\n  sample 236:\n    time = 2360000\n    flags = 1\n    data = length 89, hash 5CA40CE4\n  sample 237:\n    time = 2370000\n    flags = 1\n    data = length 88, hash 67B944FC\n  sample 238:\n    time = 2380000\n    flags = 1\n    data = length 86, hash B3A84EC8\n  sample 239:\n    time = 2390000\n    flags = 1\n    data = length 92, hash A6ACF94B\n  sample 240:\n    time = 2400000\n    flags = 1\n    data = length 88, hash CB0C9730\n  sample 241:\n    time = 2410000\n    flags = 1\n    data = length 88, hash C79FE804\n  sample 242:\n    time = 2420000\n    flags = 1\n    data = length 88, hash A74C7F0A\n  sample 243:\n    time = 2430000\n    flags = 1\n    data = length 91, hash 55F6F0A5\n  sample 244:\n    time = 2440000\n    flags = 1\n    data = length 93, hash 330F33E7\n  sample 245:\n    time = 2450000\n    flags = 1\n    data = length 89, hash 614AFBA0\n  sample 246:\n    time = 2460000\n    flags = 1\n    data = length 87, hash 3CE4652D\n  sample 247:\n    time = 2470000\n    flags = 1\n    data = length 87, hash 4EFD5467\n  sample 248:\n    time = 2480000\n    flags = 1\n    data = length 86, hash D81B3EB8\n  sample 249:\n    time = 2490000\n    flags = 1\n    data = length 88, hash 96CB6871\n  sample 250:\n    time = 2500000\n    flags = 1\n    data = length 88, hash E9DF2786\n  sample 251:\n    time = 2510000\n    flags = 1\n    data = length 89, hash 2CA33D96\n  sample 252:\n    time = 2520000\n    flags = 1\n    data = length 90, hash 96BDE594\n  sample 253:\n    time = 2530000\n    flags = 1\n    data = length 87, hash C261493C\n  sample 254:\n    time = 2540000\n    flags = 1\n    data = length 86, hash D037318E\n  sample 255:\n    time = 2550000\n    flags = 1\n    data = length 88, hash BC15BC88\n  sample 256:\n    time = 2560000\n    flags = 1\n    data = length 91, hash A8361A51\n  sample 257:\n    time = 2570000\n    flags = 1\n    data = length 87, hash 4AFDB5F2\n  sample 258:\n    time = 2580000\n    flags = 1\n    data = length 87, hash 6447F8CB\n  sample 259:\n    time = 2590000\n    flags = 1\n    data = length 89, hash 48305229\n  sample 260:\n    time = 2600000\n    flags = 1\n    data = length 87, hash 8741D9E7\n  sample 261:\n    time = 2610000\n    flags = 1\n    data = length 120, hash 761F020C\n  sample 262:\n    time = 2620000\n    flags = 1\n    data = length 139, hash AECE2E57\n  sample 263:\n    time = 2630000\n    flags = 1\n    data = length 166, hash 6288797A\n  sample 264:\n    time = 2640000\n    flags = 1\n    data = length 144, hash 437821A0\n  sample 265:\n    time = 2650000\n    flags = 1\n    data = length 113, hash FCCBEDF1\n  sample 266:\n    time = 2660000\n    flags = 1\n    data = length 108, hash C4040614\n  sample 267:\n    time = 2670000\n    flags = 1\n    data = length 125, hash E29064C2\n  sample 268:\n    time = 2680000\n    flags = 1\n    data = length 126, hash D42D24FF\n  sample 269:\n    time = 2690000\n    flags = 1\n    data = length 122, hash 30AF267D\n  sample 270:\n    time = 2700000\n    flags = 1\n    data = length 122, hash 45CEC1FB\n  sample 271:\n    time = 2710000\n    flags = 1\n    data = length 134, hash 59143FE2\n  sample 272:\n    time = 2720000\n    flags = 1\n    data = length 134, hash BD52A84\n  sample 273:\n    time = 2730000\n    flags = 1\n    data = length 120, hash 745C3714\n  sample 274:\n    time = 2740000\n    flags = 1\n    data = length 126, hash 505E117B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear.opus.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2747500\n  getPosition(0) = [[timeUs=0, position=125]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/opus\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 19, hash BFE794DB\n      data = length 8, hash CA22068C\n      data = length 8, hash 79C07075\n  total output bytes = 17031\n  sample count = 184\n  sample 0:\n    time = 910000\n    flags = 1\n    data = length 88, hash 9C8D5178\n  sample 1:\n    time = 920000\n    flags = 1\n    data = length 88, hash 48A0B19A\n  sample 2:\n    time = 930000\n    flags = 1\n    data = length 88, hash B62C94DD\n  sample 3:\n    time = 940000\n    flags = 1\n    data = length 92, hash 96DBDD46\n  sample 4:\n    time = 950000\n    flags = 1\n    data = length 87, hash 7B80E6F\n  sample 5:\n    time = 960000\n    flags = 1\n    data = length 86, hash 9C60225B\n  sample 6:\n    time = 970000\n    flags = 1\n    data = length 87, hash 45F7E6E8\n  sample 7:\n    time = 980000\n    flags = 1\n    data = length 87, hash DDC2D592\n  sample 8:\n    time = 990000\n    flags = 1\n    data = length 91, hash 173D3B26\n  sample 9:\n    time = 1000000\n    flags = 1\n    data = length 87, hash CF3629DF\n  sample 10:\n    time = 1010000\n    flags = 1\n    data = length 87, hash BBE2E7B3\n  sample 11:\n    time = 1020000\n    flags = 1\n    data = length 89, hash 89AFFB10\n  sample 12:\n    time = 1030000\n    flags = 1\n    data = length 88, hash 510DCC90\n  sample 13:\n    time = 1040000\n    flags = 1\n    data = length 88, hash CBA56E5F\n  sample 14:\n    time = 1050000\n    flags = 1\n    data = length 87, hash B4B1B3FF\n  sample 15:\n    time = 1060000\n    flags = 1\n    data = length 89, hash B976A537\n  sample 16:\n    time = 1070000\n    flags = 1\n    data = length 96, hash 43ECF2C1\n  sample 17:\n    time = 1080000\n    flags = 1\n    data = length 90, hash BB7ECB44\n  sample 18:\n    time = 1090000\n    flags = 1\n    data = length 89, hash B8E221A5\n  sample 19:\n    time = 1100000\n    flags = 1\n    data = length 86, hash B35BEF5B\n  sample 20:\n    time = 1110000\n    flags = 1\n    data = length 89, hash 9002F0EC\n  sample 21:\n    time = 1120000\n    flags = 1\n    data = length 85, hash F694B20\n  sample 22:\n    time = 1130000\n    flags = 1\n    data = length 87, hash D7CC386E\n  sample 23:\n    time = 1140000\n    flags = 1\n    data = length 89, hash EE9E0E79\n  sample 24:\n    time = 1150000\n    flags = 1\n    data = length 90, hash CA72C96B\n  sample 25:\n    time = 1160000\n    flags = 1\n    data = length 112, hash 4AD3D1B1\n  sample 26:\n    time = 1170000\n    flags = 1\n    data = length 87, hash FA568FAB\n  sample 27:\n    time = 1180000\n    flags = 1\n    data = length 90, hash 6E6948D2\n  sample 28:\n    time = 1190000\n    flags = 1\n    data = length 89, hash 5465A762\n  sample 29:\n    time = 1200000\n    flags = 1\n    data = length 87, hash BED19B40\n  sample 30:\n    time = 1210000\n    flags = 1\n    data = length 89, hash 5D05836A\n  sample 31:\n    time = 1220000\n    flags = 1\n    data = length 87, hash A8A3EF5A\n  sample 32:\n    time = 1230000\n    flags = 1\n    data = length 90, hash 6425A77A\n  sample 33:\n    time = 1240000\n    flags = 1\n    data = length 92, hash 7F320FA\n  sample 34:\n    time = 1250000\n    flags = 1\n    data = length 89, hash 2C7837D6\n  sample 35:\n    time = 1260000\n    flags = 1\n    data = length 86, hash 58D56685\n  sample 36:\n    time = 1270000\n    flags = 1\n    data = length 91, hash ADC5072F\n  sample 37:\n    time = 1280000\n    flags = 1\n    data = length 85, hash 53EFD93\n  sample 38:\n    time = 1290000\n    flags = 1\n    data = length 87, hash D006B535\n  sample 39:\n    time = 1300000\n    flags = 1\n    data = length 86, hash AE944625\n  sample 40:\n    time = 1310000\n    flags = 1\n    data = length 89, hash B5D3C81D\n  sample 41:\n    time = 1320000\n    flags = 1\n    data = length 86, hash 3BB1D0E7\n  sample 42:\n    time = 1330000\n    flags = 1\n    data = length 102, hash 16EEC441\n  sample 43:\n    time = 1340000\n    flags = 1\n    data = length 90, hash 1005B936\n  sample 44:\n    time = 1350000\n    flags = 1\n    data = length 85, hash 15EEBF9A\n  sample 45:\n    time = 1360000\n    flags = 1\n    data = length 87, hash 37C83AC2\n  sample 46:\n    time = 1370000\n    flags = 1\n    data = length 85, hash 2D27855D\n  sample 47:\n    time = 1380000\n    flags = 1\n    data = length 85, hash 753EB7C6\n  sample 48:\n    time = 1390000\n    flags = 1\n    data = length 91, hash C0813318\n  sample 49:\n    time = 1400000\n    flags = 1\n    data = length 89, hash 3A6468AC\n  sample 50:\n    time = 1410000\n    flags = 1\n    data = length 88, hash 3D220ABC\n  sample 51:\n    time = 1420000\n    flags = 1\n    data = length 140, hash 7949ABC7\n  sample 52:\n    time = 1430000\n    flags = 1\n    data = length 92, hash F19AFA45\n  sample 53:\n    time = 1440000\n    flags = 1\n    data = length 90, hash 3D21587C\n  sample 54:\n    time = 1450000\n    flags = 1\n    data = length 89, hash 5C12226C\n  sample 55:\n    time = 1460000\n    flags = 1\n    data = length 90, hash 22BA14FC\n  sample 56:\n    time = 1470000\n    flags = 1\n    data = length 88, hash F064B21C\n  sample 57:\n    time = 1480000\n    flags = 1\n    data = length 87, hash 6D7906B9\n  sample 58:\n    time = 1490000\n    flags = 1\n    data = length 88, hash 6756A484\n  sample 59:\n    time = 1500000\n    flags = 1\n    data = length 91, hash C95C00B6\n  sample 60:\n    time = 1510000\n    flags = 1\n    data = length 87, hash 728D8119\n  sample 61:\n    time = 1520000\n    flags = 1\n    data = length 90, hash C43DA1B4\n  sample 62:\n    time = 1530000\n    flags = 1\n    data = length 88, hash C181BB57\n  sample 63:\n    time = 1540000\n    flags = 1\n    data = length 84, hash F75B1639\n  sample 64:\n    time = 1550000\n    flags = 1\n    data = length 87, hash B6F32978\n  sample 65:\n    time = 1560000\n    flags = 1\n    data = length 90, hash 36D6E2D7\n  sample 66:\n    time = 1570000\n    flags = 1\n    data = length 87, hash 4C9657A7\n  sample 67:\n    time = 1580000\n    flags = 1\n    data = length 89, hash C3BDB9B7\n  sample 68:\n    time = 1590000\n    flags = 1\n    data = length 88, hash DB51087E\n  sample 69:\n    time = 1600000\n    flags = 1\n    data = length 86, hash 1550F998\n  sample 70:\n    time = 1610000\n    flags = 1\n    data = length 86, hash A445FAD4\n  sample 71:\n    time = 1620000\n    flags = 1\n    data = length 85, hash 60D3362C\n  sample 72:\n    time = 1630000\n    flags = 1\n    data = length 172, hash 945D63E4\n  sample 73:\n    time = 1640000\n    flags = 1\n    data = length 107, hash 585B7C04\n  sample 74:\n    time = 1650000\n    flags = 1\n    data = length 110, hash 74BECF69\n  sample 75:\n    time = 1660000\n    flags = 1\n    data = length 87, hash 63DE1D24\n  sample 76:\n    time = 1670000\n    flags = 1\n    data = length 90, hash 1C1D28DB\n  sample 77:\n    time = 1680000\n    flags = 1\n    data = length 87, hash CB382A67\n  sample 78:\n    time = 1690000\n    flags = 1\n    data = length 85, hash A227BA13\n  sample 79:\n    time = 1700000\n    flags = 1\n    data = length 86, hash EFD8B10B\n  sample 80:\n    time = 1710000\n    flags = 1\n    data = length 87, hash 47FF364A\n  sample 81:\n    time = 1720000\n    flags = 1\n    data = length 91, hash 31D4B48A\n  sample 82:\n    time = 1730000\n    flags = 1\n    data = length 91, hash DD69BD85\n  sample 83:\n    time = 1740000\n    flags = 1\n    data = length 88, hash AF1A95C6\n  sample 84:\n    time = 1750000\n    flags = 1\n    data = length 87, hash 2FB8AF74\n  sample 85:\n    time = 1760000\n    flags = 1\n    data = length 92, hash 173C707A\n  sample 86:\n    time = 1770000\n    flags = 1\n    data = length 88, hash 5F58F5E8\n  sample 87:\n    time = 1780000\n    flags = 1\n    data = length 91, hash D449785F\n  sample 88:\n    time = 1790000\n    flags = 1\n    data = length 91, hash CE2CB465\n  sample 89:\n    time = 1800000\n    flags = 1\n    data = length 93, hash ABC1C62E\n  sample 90:\n    time = 1810000\n    flags = 1\n    data = length 87, hash 83B4B9A0\n  sample 91:\n    time = 1820000\n    flags = 1\n    data = length 87, hash 3220D562\n  sample 92:\n    time = 1830000\n    flags = 1\n    data = length 86, hash 64D21AA1\n  sample 93:\n    time = 1840000\n    flags = 1\n    data = length 86, hash A1FAAF2C\n  sample 94:\n    time = 1850000\n    flags = 1\n    data = length 86, hash ECA80F7E\n  sample 95:\n    time = 1860000\n    flags = 1\n    data = length 86, hash FEB03B2C\n  sample 96:\n    time = 1870000\n    flags = 1\n    data = length 85, hash 2C2E6B2F\n  sample 97:\n    time = 1880000\n    flags = 1\n    data = length 89, hash A0D7AC3\n  sample 98:\n    time = 1890000\n    flags = 1\n    data = length 87, hash 83739547\n  sample 99:\n    time = 1900000\n    flags = 1\n    data = length 86, hash 991E531E\n  sample 100:\n    time = 1910000\n    flags = 1\n    data = length 88, hash 16B287A3\n  sample 101:\n    time = 1920000\n    flags = 1\n    data = length 86, hash FC86EED\n  sample 102:\n    time = 1930000\n    flags = 1\n    data = length 86, hash 96AF0248\n  sample 103:\n    time = 1940000\n    flags = 1\n    data = length 86, hash 288402C8\n  sample 104:\n    time = 1950000\n    flags = 1\n    data = length 87, hash 4BBA7912\n  sample 105:\n    time = 1960000\n    flags = 1\n    data = length 86, hash 4A59C719\n  sample 106:\n    time = 1970000\n    flags = 1\n    data = length 85, hash 906E8187\n  sample 107:\n    time = 1980000\n    flags = 1\n    data = length 90, hash CB8B755D\n  sample 108:\n    time = 1990000\n    flags = 1\n    data = length 87, hash C8E02C\n  sample 109:\n    time = 2000000\n    flags = 1\n    data = length 88, hash ACF4D89A\n  sample 110:\n    time = 2010000\n    flags = 1\n    data = length 86, hash 510FE048\n  sample 111:\n    time = 2020000\n    flags = 1\n    data = length 86, hash 64983E46\n  sample 112:\n    time = 2030000\n    flags = 1\n    data = length 86, hash CEA76A1E\n  sample 113:\n    time = 2040000\n    flags = 1\n    data = length 87, hash AADE498E\n  sample 114:\n    time = 2050000\n    flags = 1\n    data = length 127, hash 353A6D8C\n  sample 115:\n    time = 2060000\n    flags = 1\n    data = length 87, hash 29E18E62\n  sample 116:\n    time = 2070000\n    flags = 1\n    data = length 87, hash 2CF7B30F\n  sample 117:\n    time = 2080000\n    flags = 1\n    data = length 94, hash 758704C3\n  sample 118:\n    time = 2090000\n    flags = 1\n    data = length 88, hash C2153A4C\n  sample 119:\n    time = 2100000\n    flags = 1\n    data = length 86, hash A0A83DA5\n  sample 120:\n    time = 2110000\n    flags = 1\n    data = length 86, hash 41017D7F\n  sample 121:\n    time = 2120000\n    flags = 1\n    data = length 93, hash 686B0CA2\n  sample 122:\n    time = 2130000\n    flags = 1\n    data = length 86, hash 554D16CC\n  sample 123:\n    time = 2140000\n    flags = 1\n    data = length 88, hash 99D72771\n  sample 124:\n    time = 2150000\n    flags = 1\n    data = length 88, hash 7176DFBF\n  sample 125:\n    time = 2160000\n    flags = 1\n    data = length 86, hash BAA22669\n  sample 126:\n    time = 2170000\n    flags = 1\n    data = length 88, hash B00B0D3C\n  sample 127:\n    time = 2180000\n    flags = 1\n    data = length 89, hash 73FED83A\n  sample 128:\n    time = 2190000\n    flags = 1\n    data = length 86, hash 4A4138D3\n  sample 129:\n    time = 2200000\n    flags = 1\n    data = length 89, hash E0A860FF\n  sample 130:\n    time = 2210000\n    flags = 1\n    data = length 95, hash EE5A8AED\n  sample 131:\n    time = 2220000\n    flags = 1\n    data = length 92, hash 36DBD7FD\n  sample 132:\n    time = 2230000\n    flags = 1\n    data = length 88, hash EE47A7E4\n  sample 133:\n    time = 2240000\n    flags = 1\n    data = length 100, hash 2E1A603F\n  sample 134:\n    time = 2250000\n    flags = 1\n    data = length 89, hash 657ED4A3\n  sample 135:\n    time = 2260000\n    flags = 1\n    data = length 86, hash A833DC7B\n  sample 136:\n    time = 2270000\n    flags = 1\n    data = length 88, hash 81E80732\n  sample 137:\n    time = 2280000\n    flags = 1\n    data = length 91, hash FA256A0F\n  sample 138:\n    time = 2290000\n    flags = 1\n    data = length 88, hash A63A4DBA\n  sample 139:\n    time = 2300000\n    flags = 1\n    data = length 88, hash 67910A9F\n  sample 140:\n    time = 2310000\n    flags = 1\n    data = length 86, hash EB387DB6\n  sample 141:\n    time = 2320000\n    flags = 1\n    data = length 88, hash 5ACAAC2A\n  sample 142:\n    time = 2330000\n    flags = 1\n    data = length 86, hash 6ADF2E1F\n  sample 143:\n    time = 2340000\n    flags = 1\n    data = length 85, hash 9D064471\n  sample 144:\n    time = 2350000\n    flags = 1\n    data = length 87, hash F176C59\n  sample 145:\n    time = 2360000\n    flags = 1\n    data = length 89, hash 5CA40CE4\n  sample 146:\n    time = 2370000\n    flags = 1\n    data = length 88, hash 67B944FC\n  sample 147:\n    time = 2380000\n    flags = 1\n    data = length 86, hash B3A84EC8\n  sample 148:\n    time = 2390000\n    flags = 1\n    data = length 92, hash A6ACF94B\n  sample 149:\n    time = 2400000\n    flags = 1\n    data = length 88, hash CB0C9730\n  sample 150:\n    time = 2410000\n    flags = 1\n    data = length 88, hash C79FE804\n  sample 151:\n    time = 2420000\n    flags = 1\n    data = length 88, hash A74C7F0A\n  sample 152:\n    time = 2430000\n    flags = 1\n    data = length 91, hash 55F6F0A5\n  sample 153:\n    time = 2440000\n    flags = 1\n    data = length 93, hash 330F33E7\n  sample 154:\n    time = 2450000\n    flags = 1\n    data = length 89, hash 614AFBA0\n  sample 155:\n    time = 2460000\n    flags = 1\n    data = length 87, hash 3CE4652D\n  sample 156:\n    time = 2470000\n    flags = 1\n    data = length 87, hash 4EFD5467\n  sample 157:\n    time = 2480000\n    flags = 1\n    data = length 86, hash D81B3EB8\n  sample 158:\n    time = 2490000\n    flags = 1\n    data = length 88, hash 96CB6871\n  sample 159:\n    time = 2500000\n    flags = 1\n    data = length 88, hash E9DF2786\n  sample 160:\n    time = 2510000\n    flags = 1\n    data = length 89, hash 2CA33D96\n  sample 161:\n    time = 2520000\n    flags = 1\n    data = length 90, hash 96BDE594\n  sample 162:\n    time = 2530000\n    flags = 1\n    data = length 87, hash C261493C\n  sample 163:\n    time = 2540000\n    flags = 1\n    data = length 86, hash D037318E\n  sample 164:\n    time = 2550000\n    flags = 1\n    data = length 88, hash BC15BC88\n  sample 165:\n    time = 2560000\n    flags = 1\n    data = length 91, hash A8361A51\n  sample 166:\n    time = 2570000\n    flags = 1\n    data = length 87, hash 4AFDB5F2\n  sample 167:\n    time = 2580000\n    flags = 1\n    data = length 87, hash 6447F8CB\n  sample 168:\n    time = 2590000\n    flags = 1\n    data = length 89, hash 48305229\n  sample 169:\n    time = 2600000\n    flags = 1\n    data = length 87, hash 8741D9E7\n  sample 170:\n    time = 2610000\n    flags = 1\n    data = length 120, hash 761F020C\n  sample 171:\n    time = 2620000\n    flags = 1\n    data = length 139, hash AECE2E57\n  sample 172:\n    time = 2630000\n    flags = 1\n    data = length 166, hash 6288797A\n  sample 173:\n    time = 2640000\n    flags = 1\n    data = length 144, hash 437821A0\n  sample 174:\n    time = 2650000\n    flags = 1\n    data = length 113, hash FCCBEDF1\n  sample 175:\n    time = 2660000\n    flags = 1\n    data = length 108, hash C4040614\n  sample 176:\n    time = 2670000\n    flags = 1\n    data = length 125, hash E29064C2\n  sample 177:\n    time = 2680000\n    flags = 1\n    data = length 126, hash D42D24FF\n  sample 178:\n    time = 2690000\n    flags = 1\n    data = length 122, hash 30AF267D\n  sample 179:\n    time = 2700000\n    flags = 1\n    data = length 122, hash 45CEC1FB\n  sample 180:\n    time = 2710000\n    flags = 1\n    data = length 134, hash 59143FE2\n  sample 181:\n    time = 2720000\n    flags = 1\n    data = length 134, hash BD52A84\n  sample 182:\n    time = 2730000\n    flags = 1\n    data = length 120, hash 745C3714\n  sample 183:\n    time = 2740000\n    flags = 1\n    data = length 126, hash 505E117B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear.opus.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2747500\n  getPosition(0) = [[timeUs=0, position=125]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/opus\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 19, hash BFE794DB\n      data = length 8, hash CA22068C\n      data = length 8, hash 79C07075\n  total output bytes = 8698\n  sample count = 92\n  sample 0:\n    time = 1830000\n    flags = 1\n    data = length 86, hash 64D21AA1\n  sample 1:\n    time = 1840000\n    flags = 1\n    data = length 86, hash A1FAAF2C\n  sample 2:\n    time = 1850000\n    flags = 1\n    data = length 86, hash ECA80F7E\n  sample 3:\n    time = 1860000\n    flags = 1\n    data = length 86, hash FEB03B2C\n  sample 4:\n    time = 1870000\n    flags = 1\n    data = length 85, hash 2C2E6B2F\n  sample 5:\n    time = 1880000\n    flags = 1\n    data = length 89, hash A0D7AC3\n  sample 6:\n    time = 1890000\n    flags = 1\n    data = length 87, hash 83739547\n  sample 7:\n    time = 1900000\n    flags = 1\n    data = length 86, hash 991E531E\n  sample 8:\n    time = 1910000\n    flags = 1\n    data = length 88, hash 16B287A3\n  sample 9:\n    time = 1920000\n    flags = 1\n    data = length 86, hash FC86EED\n  sample 10:\n    time = 1930000\n    flags = 1\n    data = length 86, hash 96AF0248\n  sample 11:\n    time = 1940000\n    flags = 1\n    data = length 86, hash 288402C8\n  sample 12:\n    time = 1950000\n    flags = 1\n    data = length 87, hash 4BBA7912\n  sample 13:\n    time = 1960000\n    flags = 1\n    data = length 86, hash 4A59C719\n  sample 14:\n    time = 1970000\n    flags = 1\n    data = length 85, hash 906E8187\n  sample 15:\n    time = 1980000\n    flags = 1\n    data = length 90, hash CB8B755D\n  sample 16:\n    time = 1990000\n    flags = 1\n    data = length 87, hash C8E02C\n  sample 17:\n    time = 2000000\n    flags = 1\n    data = length 88, hash ACF4D89A\n  sample 18:\n    time = 2010000\n    flags = 1\n    data = length 86, hash 510FE048\n  sample 19:\n    time = 2020000\n    flags = 1\n    data = length 86, hash 64983E46\n  sample 20:\n    time = 2030000\n    flags = 1\n    data = length 86, hash CEA76A1E\n  sample 21:\n    time = 2040000\n    flags = 1\n    data = length 87, hash AADE498E\n  sample 22:\n    time = 2050000\n    flags = 1\n    data = length 127, hash 353A6D8C\n  sample 23:\n    time = 2060000\n    flags = 1\n    data = length 87, hash 29E18E62\n  sample 24:\n    time = 2070000\n    flags = 1\n    data = length 87, hash 2CF7B30F\n  sample 25:\n    time = 2080000\n    flags = 1\n    data = length 94, hash 758704C3\n  sample 26:\n    time = 2090000\n    flags = 1\n    data = length 88, hash C2153A4C\n  sample 27:\n    time = 2100000\n    flags = 1\n    data = length 86, hash A0A83DA5\n  sample 28:\n    time = 2110000\n    flags = 1\n    data = length 86, hash 41017D7F\n  sample 29:\n    time = 2120000\n    flags = 1\n    data = length 93, hash 686B0CA2\n  sample 30:\n    time = 2130000\n    flags = 1\n    data = length 86, hash 554D16CC\n  sample 31:\n    time = 2140000\n    flags = 1\n    data = length 88, hash 99D72771\n  sample 32:\n    time = 2150000\n    flags = 1\n    data = length 88, hash 7176DFBF\n  sample 33:\n    time = 2160000\n    flags = 1\n    data = length 86, hash BAA22669\n  sample 34:\n    time = 2170000\n    flags = 1\n    data = length 88, hash B00B0D3C\n  sample 35:\n    time = 2180000\n    flags = 1\n    data = length 89, hash 73FED83A\n  sample 36:\n    time = 2190000\n    flags = 1\n    data = length 86, hash 4A4138D3\n  sample 37:\n    time = 2200000\n    flags = 1\n    data = length 89, hash E0A860FF\n  sample 38:\n    time = 2210000\n    flags = 1\n    data = length 95, hash EE5A8AED\n  sample 39:\n    time = 2220000\n    flags = 1\n    data = length 92, hash 36DBD7FD\n  sample 40:\n    time = 2230000\n    flags = 1\n    data = length 88, hash EE47A7E4\n  sample 41:\n    time = 2240000\n    flags = 1\n    data = length 100, hash 2E1A603F\n  sample 42:\n    time = 2250000\n    flags = 1\n    data = length 89, hash 657ED4A3\n  sample 43:\n    time = 2260000\n    flags = 1\n    data = length 86, hash A833DC7B\n  sample 44:\n    time = 2270000\n    flags = 1\n    data = length 88, hash 81E80732\n  sample 45:\n    time = 2280000\n    flags = 1\n    data = length 91, hash FA256A0F\n  sample 46:\n    time = 2290000\n    flags = 1\n    data = length 88, hash A63A4DBA\n  sample 47:\n    time = 2300000\n    flags = 1\n    data = length 88, hash 67910A9F\n  sample 48:\n    time = 2310000\n    flags = 1\n    data = length 86, hash EB387DB6\n  sample 49:\n    time = 2320000\n    flags = 1\n    data = length 88, hash 5ACAAC2A\n  sample 50:\n    time = 2330000\n    flags = 1\n    data = length 86, hash 6ADF2E1F\n  sample 51:\n    time = 2340000\n    flags = 1\n    data = length 85, hash 9D064471\n  sample 52:\n    time = 2350000\n    flags = 1\n    data = length 87, hash F176C59\n  sample 53:\n    time = 2360000\n    flags = 1\n    data = length 89, hash 5CA40CE4\n  sample 54:\n    time = 2370000\n    flags = 1\n    data = length 88, hash 67B944FC\n  sample 55:\n    time = 2380000\n    flags = 1\n    data = length 86, hash B3A84EC8\n  sample 56:\n    time = 2390000\n    flags = 1\n    data = length 92, hash A6ACF94B\n  sample 57:\n    time = 2400000\n    flags = 1\n    data = length 88, hash CB0C9730\n  sample 58:\n    time = 2410000\n    flags = 1\n    data = length 88, hash C79FE804\n  sample 59:\n    time = 2420000\n    flags = 1\n    data = length 88, hash A74C7F0A\n  sample 60:\n    time = 2430000\n    flags = 1\n    data = length 91, hash 55F6F0A5\n  sample 61:\n    time = 2440000\n    flags = 1\n    data = length 93, hash 330F33E7\n  sample 62:\n    time = 2450000\n    flags = 1\n    data = length 89, hash 614AFBA0\n  sample 63:\n    time = 2460000\n    flags = 1\n    data = length 87, hash 3CE4652D\n  sample 64:\n    time = 2470000\n    flags = 1\n    data = length 87, hash 4EFD5467\n  sample 65:\n    time = 2480000\n    flags = 1\n    data = length 86, hash D81B3EB8\n  sample 66:\n    time = 2490000\n    flags = 1\n    data = length 88, hash 96CB6871\n  sample 67:\n    time = 2500000\n    flags = 1\n    data = length 88, hash E9DF2786\n  sample 68:\n    time = 2510000\n    flags = 1\n    data = length 89, hash 2CA33D96\n  sample 69:\n    time = 2520000\n    flags = 1\n    data = length 90, hash 96BDE594\n  sample 70:\n    time = 2530000\n    flags = 1\n    data = length 87, hash C261493C\n  sample 71:\n    time = 2540000\n    flags = 1\n    data = length 86, hash D037318E\n  sample 72:\n    time = 2550000\n    flags = 1\n    data = length 88, hash BC15BC88\n  sample 73:\n    time = 2560000\n    flags = 1\n    data = length 91, hash A8361A51\n  sample 74:\n    time = 2570000\n    flags = 1\n    data = length 87, hash 4AFDB5F2\n  sample 75:\n    time = 2580000\n    flags = 1\n    data = length 87, hash 6447F8CB\n  sample 76:\n    time = 2590000\n    flags = 1\n    data = length 89, hash 48305229\n  sample 77:\n    time = 2600000\n    flags = 1\n    data = length 87, hash 8741D9E7\n  sample 78:\n    time = 2610000\n    flags = 1\n    data = length 120, hash 761F020C\n  sample 79:\n    time = 2620000\n    flags = 1\n    data = length 139, hash AECE2E57\n  sample 80:\n    time = 2630000\n    flags = 1\n    data = length 166, hash 6288797A\n  sample 81:\n    time = 2640000\n    flags = 1\n    data = length 144, hash 437821A0\n  sample 82:\n    time = 2650000\n    flags = 1\n    data = length 113, hash FCCBEDF1\n  sample 83:\n    time = 2660000\n    flags = 1\n    data = length 108, hash C4040614\n  sample 84:\n    time = 2670000\n    flags = 1\n    data = length 125, hash E29064C2\n  sample 85:\n    time = 2680000\n    flags = 1\n    data = length 126, hash D42D24FF\n  sample 86:\n    time = 2690000\n    flags = 1\n    data = length 122, hash 30AF267D\n  sample 87:\n    time = 2700000\n    flags = 1\n    data = length 122, hash 45CEC1FB\n  sample 88:\n    time = 2710000\n    flags = 1\n    data = length 134, hash 59143FE2\n  sample 89:\n    time = 2720000\n    flags = 1\n    data = length 134, hash BD52A84\n  sample 90:\n    time = 2730000\n    flags = 1\n    data = length 120, hash 745C3714\n  sample 91:\n    time = 2740000\n    flags = 1\n    data = length 126, hash 505E117B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear.opus.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2747500\n  getPosition(0) = [[timeUs=0, position=125]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/opus\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 19, hash BFE794DB\n      data = length 8, hash CA22068C\n      data = length 8, hash 79C07075\n  total output bytes = 126\n  sample count = 1\n  sample 0:\n    time = 2741000\n    flags = 1\n    data = length 126, hash 505E117B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear.opus.unklen.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/opus\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 19, hash BFE794DB\n      data = length 8, hash CA22068C\n      data = length 8, hash 79C07075\n  total output bytes = 25541\n  sample count = 275\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 234, hash B77BFFDA\n  sample 1:\n    time = 10000\n    flags = 1\n    data = length 165, hash F7B07C35\n  sample 2:\n    time = 20000\n    flags = 1\n    data = length 100, hash 21AFA81F\n  sample 3:\n    time = 30000\n    flags = 1\n    data = length 85, hash 9180DC2F\n  sample 4:\n    time = 40000\n    flags = 1\n    data = length 85, hash 6AE958C\n  sample 5:\n    time = 50000\n    flags = 1\n    data = length 86, hash C1C5AE60\n  sample 6:\n    time = 60000\n    flags = 1\n    data = length 87, hash B9BD2620\n  sample 7:\n    time = 70000\n    flags = 1\n    data = length 86, hash 5E69E6F9\n  sample 8:\n    time = 80000\n    flags = 1\n    data = length 90, hash C44C7DD9\n  sample 9:\n    time = 90000\n    flags = 1\n    data = length 86, hash C3FCDC6F\n  sample 10:\n    time = 100000\n    flags = 1\n    data = length 86, hash 44EA03BA\n  sample 11:\n    time = 110000\n    flags = 1\n    data = length 160, hash 9F4E1AE8\n  sample 12:\n    time = 120000\n    flags = 1\n    data = length 89, hash 31234460\n  sample 13:\n    time = 130000\n    flags = 1\n    data = length 91, hash 45012D79\n  sample 14:\n    time = 140000\n    flags = 1\n    data = length 90, hash B3E3AC75\n  sample 15:\n    time = 150000\n    flags = 1\n    data = length 87, hash B83B756B\n  sample 16:\n    time = 160000\n    flags = 1\n    data = length 86, hash 383921EB\n  sample 17:\n    time = 170000\n    flags = 1\n    data = length 97, hash 959AD270\n  sample 18:\n    time = 180000\n    flags = 1\n    data = length 92, hash 46C74FA8\n  sample 19:\n    time = 190000\n    flags = 1\n    data = length 91, hash CEA401DD\n  sample 20:\n    time = 200000\n    flags = 1\n    data = length 89, hash 48C41853\n  sample 21:\n    time = 210000\n    flags = 1\n    data = length 90, hash F23245BD\n  sample 22:\n    time = 220000\n    flags = 1\n    data = length 96, hash 95E8985D\n  sample 23:\n    time = 230000\n    flags = 1\n    data = length 96, hash 34295492\n  sample 24:\n    time = 240000\n    flags = 1\n    data = length 94, hash 4E3C9C0F\n  sample 25:\n    time = 250000\n    flags = 1\n    data = length 89, hash 28B74A29\n  sample 26:\n    time = 260000\n    flags = 1\n    data = length 87, hash BAC119A7\n  sample 27:\n    time = 270000\n    flags = 1\n    data = length 88, hash 7139FF38\n  sample 28:\n    time = 280000\n    flags = 1\n    data = length 85, hash 246E1D2A\n  sample 29:\n    time = 290000\n    flags = 1\n    data = length 86, hash 488A0900\n  sample 30:\n    time = 300000\n    flags = 1\n    data = length 90, hash 16FD17B1\n  sample 31:\n    time = 310000\n    flags = 1\n    data = length 87, hash 20E849D9\n  sample 32:\n    time = 320000\n    flags = 1\n    data = length 86, hash 23A0E9BA\n  sample 33:\n    time = 330000\n    flags = 1\n    data = length 87, hash EC935537\n  sample 34:\n    time = 340000\n    flags = 1\n    data = length 92, hash 4D9935AD\n  sample 35:\n    time = 350000\n    flags = 1\n    data = length 87, hash DEDE3FA\n  sample 36:\n    time = 360000\n    flags = 1\n    data = length 87, hash ADC25A6E\n  sample 37:\n    time = 370000\n    flags = 1\n    data = length 88, hash A1C828C5\n  sample 38:\n    time = 380000\n    flags = 1\n    data = length 89, hash 735C087A\n  sample 39:\n    time = 390000\n    flags = 1\n    data = length 89, hash 19AF5D10\n  sample 40:\n    time = 400000\n    flags = 1\n    data = length 90, hash BCCEA2BB\n  sample 41:\n    time = 410000\n    flags = 1\n    data = length 86, hash A7C934A0\n  sample 42:\n    time = 420000\n    flags = 1\n    data = length 86, hash 28BBC0A8\n  sample 43:\n    time = 430000\n    flags = 1\n    data = length 85, hash E60BB12D\n  sample 44:\n    time = 440000\n    flags = 1\n    data = length 141, hash 1D2B8920\n  sample 45:\n    time = 450000\n    flags = 1\n    data = length 121, hash 8AA3E19C\n  sample 46:\n    time = 460000\n    flags = 1\n    data = length 86, hash 24DF0F37\n  sample 47:\n    time = 470000\n    flags = 1\n    data = length 86, hash 1D1775FF\n  sample 48:\n    time = 480000\n    flags = 1\n    data = length 87, hash 5230399E\n  sample 49:\n    time = 490000\n    flags = 1\n    data = length 91, hash 6CD98305\n  sample 50:\n    time = 500000\n    flags = 1\n    data = length 88, hash 4069FBB\n  sample 51:\n    time = 510000\n    flags = 1\n    data = length 89, hash 76824ABF\n  sample 52:\n    time = 520000\n    flags = 1\n    data = length 87, hash BC1B1322\n  sample 53:\n    time = 530000\n    flags = 1\n    data = length 102, hash E01BA053\n  sample 54:\n    time = 540000\n    flags = 1\n    data = length 85, hash C09B626D\n  sample 55:\n    time = 550000\n    flags = 1\n    data = length 88, hash 6B7B404A\n  sample 56:\n    time = 560000\n    flags = 1\n    data = length 85, hash 74A15DC7\n  sample 57:\n    time = 570000\n    flags = 1\n    data = length 88, hash 38DB82E5\n  sample 58:\n    time = 580000\n    flags = 1\n    data = length 86, hash 1A39C081\n  sample 59:\n    time = 590000\n    flags = 1\n    data = length 87, hash 39FEC92\n  sample 60:\n    time = 600000\n    flags = 1\n    data = length 92, hash 278EA09\n  sample 61:\n    time = 610000\n    flags = 1\n    data = length 87, hash 28265F2D\n  sample 62:\n    time = 620000\n    flags = 1\n    data = length 86, hash CC2040C6\n  sample 63:\n    time = 630000\n    flags = 1\n    data = length 138, hash 9E07BC1F\n  sample 64:\n    time = 640000\n    flags = 1\n    data = length 85, hash 4F299670\n  sample 65:\n    time = 650000\n    flags = 1\n    data = length 125, hash B61123C3\n  sample 66:\n    time = 660000\n    flags = 1\n    data = length 89, hash 5CC688ED\n  sample 67:\n    time = 670000\n    flags = 1\n    data = length 88, hash 84AF64A6\n  sample 68:\n    time = 680000\n    flags = 1\n    data = length 89, hash A9BFC8DC\n  sample 69:\n    time = 690000\n    flags = 1\n    data = length 90, hash 2FF77060\n  sample 70:\n    time = 700000\n    flags = 1\n    data = length 96, hash E11AFD61\n  sample 71:\n    time = 710000\n    flags = 1\n    data = length 87, hash 85D14EDA\n  sample 72:\n    time = 720000\n    flags = 1\n    data = length 88, hash 5FC71D53\n  sample 73:\n    time = 730000\n    flags = 1\n    data = length 89, hash 957187B6\n  sample 74:\n    time = 740000\n    flags = 1\n    data = length 89, hash 5A776082\n  sample 75:\n    time = 750000\n    flags = 1\n    data = length 87, hash E8A83AFE\n  sample 76:\n    time = 760000\n    flags = 1\n    data = length 87, hash F1989133\n  sample 77:\n    time = 770000\n    flags = 1\n    data = length 87, hash FA06BCCA\n  sample 78:\n    time = 780000\n    flags = 1\n    data = length 86, hash BD97E0C0\n  sample 79:\n    time = 790000\n    flags = 1\n    data = length 88, hash E6AE022C\n  sample 80:\n    time = 800000\n    flags = 1\n    data = length 87, hash FB6C6169\n  sample 81:\n    time = 810000\n    flags = 1\n    data = length 87, hash DFFCD2CF\n  sample 82:\n    time = 820000\n    flags = 1\n    data = length 88, hash A4B5EB52\n  sample 83:\n    time = 830000\n    flags = 1\n    data = length 85, hash A63CF4EA\n  sample 84:\n    time = 840000\n    flags = 1\n    data = length 86, hash F126E7C7\n  sample 85:\n    time = 850000\n    flags = 1\n    data = length 86, hash 21A8B22F\n  sample 86:\n    time = 860000\n    flags = 1\n    data = length 87, hash 6520E7C1\n  sample 87:\n    time = 870000\n    flags = 1\n    data = length 88, hash 825B7423\n  sample 88:\n    time = 880000\n    flags = 1\n    data = length 88, hash DF3DBD48\n  sample 89:\n    time = 890000\n    flags = 1\n    data = length 87, hash B32C68D0\n  sample 90:\n    time = 900000\n    flags = 1\n    data = length 89, hash B99DFFCA\n  sample 91:\n    time = 910000\n    flags = 1\n    data = length 88, hash 9C8D5178\n  sample 92:\n    time = 920000\n    flags = 1\n    data = length 88, hash 48A0B19A\n  sample 93:\n    time = 930000\n    flags = 1\n    data = length 88, hash B62C94DD\n  sample 94:\n    time = 940000\n    flags = 1\n    data = length 92, hash 96DBDD46\n  sample 95:\n    time = 950000\n    flags = 1\n    data = length 87, hash 7B80E6F\n  sample 96:\n    time = 960000\n    flags = 1\n    data = length 86, hash 9C60225B\n  sample 97:\n    time = 970000\n    flags = 1\n    data = length 87, hash 45F7E6E8\n  sample 98:\n    time = 980000\n    flags = 1\n    data = length 87, hash DDC2D592\n  sample 99:\n    time = 990000\n    flags = 1\n    data = length 91, hash 173D3B26\n  sample 100:\n    time = 1000000\n    flags = 1\n    data = length 87, hash CF3629DF\n  sample 101:\n    time = 1010000\n    flags = 1\n    data = length 87, hash BBE2E7B3\n  sample 102:\n    time = 1020000\n    flags = 1\n    data = length 89, hash 89AFFB10\n  sample 103:\n    time = 1030000\n    flags = 1\n    data = length 88, hash 510DCC90\n  sample 104:\n    time = 1040000\n    flags = 1\n    data = length 88, hash CBA56E5F\n  sample 105:\n    time = 1050000\n    flags = 1\n    data = length 87, hash B4B1B3FF\n  sample 106:\n    time = 1060000\n    flags = 1\n    data = length 89, hash B976A537\n  sample 107:\n    time = 1070000\n    flags = 1\n    data = length 96, hash 43ECF2C1\n  sample 108:\n    time = 1080000\n    flags = 1\n    data = length 90, hash BB7ECB44\n  sample 109:\n    time = 1090000\n    flags = 1\n    data = length 89, hash B8E221A5\n  sample 110:\n    time = 1100000\n    flags = 1\n    data = length 86, hash B35BEF5B\n  sample 111:\n    time = 1110000\n    flags = 1\n    data = length 89, hash 9002F0EC\n  sample 112:\n    time = 1120000\n    flags = 1\n    data = length 85, hash F694B20\n  sample 113:\n    time = 1130000\n    flags = 1\n    data = length 87, hash D7CC386E\n  sample 114:\n    time = 1140000\n    flags = 1\n    data = length 89, hash EE9E0E79\n  sample 115:\n    time = 1150000\n    flags = 1\n    data = length 90, hash CA72C96B\n  sample 116:\n    time = 1160000\n    flags = 1\n    data = length 112, hash 4AD3D1B1\n  sample 117:\n    time = 1170000\n    flags = 1\n    data = length 87, hash FA568FAB\n  sample 118:\n    time = 1180000\n    flags = 1\n    data = length 90, hash 6E6948D2\n  sample 119:\n    time = 1190000\n    flags = 1\n    data = length 89, hash 5465A762\n  sample 120:\n    time = 1200000\n    flags = 1\n    data = length 87, hash BED19B40\n  sample 121:\n    time = 1210000\n    flags = 1\n    data = length 89, hash 5D05836A\n  sample 122:\n    time = 1220000\n    flags = 1\n    data = length 87, hash A8A3EF5A\n  sample 123:\n    time = 1230000\n    flags = 1\n    data = length 90, hash 6425A77A\n  sample 124:\n    time = 1240000\n    flags = 1\n    data = length 92, hash 7F320FA\n  sample 125:\n    time = 1250000\n    flags = 1\n    data = length 89, hash 2C7837D6\n  sample 126:\n    time = 1260000\n    flags = 1\n    data = length 86, hash 58D56685\n  sample 127:\n    time = 1270000\n    flags = 1\n    data = length 91, hash ADC5072F\n  sample 128:\n    time = 1280000\n    flags = 1\n    data = length 85, hash 53EFD93\n  sample 129:\n    time = 1290000\n    flags = 1\n    data = length 87, hash D006B535\n  sample 130:\n    time = 1300000\n    flags = 1\n    data = length 86, hash AE944625\n  sample 131:\n    time = 1310000\n    flags = 1\n    data = length 89, hash B5D3C81D\n  sample 132:\n    time = 1320000\n    flags = 1\n    data = length 86, hash 3BB1D0E7\n  sample 133:\n    time = 1330000\n    flags = 1\n    data = length 102, hash 16EEC441\n  sample 134:\n    time = 1340000\n    flags = 1\n    data = length 90, hash 1005B936\n  sample 135:\n    time = 1350000\n    flags = 1\n    data = length 85, hash 15EEBF9A\n  sample 136:\n    time = 1360000\n    flags = 1\n    data = length 87, hash 37C83AC2\n  sample 137:\n    time = 1370000\n    flags = 1\n    data = length 85, hash 2D27855D\n  sample 138:\n    time = 1380000\n    flags = 1\n    data = length 85, hash 753EB7C6\n  sample 139:\n    time = 1390000\n    flags = 1\n    data = length 91, hash C0813318\n  sample 140:\n    time = 1400000\n    flags = 1\n    data = length 89, hash 3A6468AC\n  sample 141:\n    time = 1410000\n    flags = 1\n    data = length 88, hash 3D220ABC\n  sample 142:\n    time = 1420000\n    flags = 1\n    data = length 140, hash 7949ABC7\n  sample 143:\n    time = 1430000\n    flags = 1\n    data = length 92, hash F19AFA45\n  sample 144:\n    time = 1440000\n    flags = 1\n    data = length 90, hash 3D21587C\n  sample 145:\n    time = 1450000\n    flags = 1\n    data = length 89, hash 5C12226C\n  sample 146:\n    time = 1460000\n    flags = 1\n    data = length 90, hash 22BA14FC\n  sample 147:\n    time = 1470000\n    flags = 1\n    data = length 88, hash F064B21C\n  sample 148:\n    time = 1480000\n    flags = 1\n    data = length 87, hash 6D7906B9\n  sample 149:\n    time = 1490000\n    flags = 1\n    data = length 88, hash 6756A484\n  sample 150:\n    time = 1500000\n    flags = 1\n    data = length 91, hash C95C00B6\n  sample 151:\n    time = 1510000\n    flags = 1\n    data = length 87, hash 728D8119\n  sample 152:\n    time = 1520000\n    flags = 1\n    data = length 90, hash C43DA1B4\n  sample 153:\n    time = 1530000\n    flags = 1\n    data = length 88, hash C181BB57\n  sample 154:\n    time = 1540000\n    flags = 1\n    data = length 84, hash F75B1639\n  sample 155:\n    time = 1550000\n    flags = 1\n    data = length 87, hash B6F32978\n  sample 156:\n    time = 1560000\n    flags = 1\n    data = length 90, hash 36D6E2D7\n  sample 157:\n    time = 1570000\n    flags = 1\n    data = length 87, hash 4C9657A7\n  sample 158:\n    time = 1580000\n    flags = 1\n    data = length 89, hash C3BDB9B7\n  sample 159:\n    time = 1590000\n    flags = 1\n    data = length 88, hash DB51087E\n  sample 160:\n    time = 1600000\n    flags = 1\n    data = length 86, hash 1550F998\n  sample 161:\n    time = 1610000\n    flags = 1\n    data = length 86, hash A445FAD4\n  sample 162:\n    time = 1620000\n    flags = 1\n    data = length 85, hash 60D3362C\n  sample 163:\n    time = 1630000\n    flags = 1\n    data = length 172, hash 945D63E4\n  sample 164:\n    time = 1640000\n    flags = 1\n    data = length 107, hash 585B7C04\n  sample 165:\n    time = 1650000\n    flags = 1\n    data = length 110, hash 74BECF69\n  sample 166:\n    time = 1660000\n    flags = 1\n    data = length 87, hash 63DE1D24\n  sample 167:\n    time = 1670000\n    flags = 1\n    data = length 90, hash 1C1D28DB\n  sample 168:\n    time = 1680000\n    flags = 1\n    data = length 87, hash CB382A67\n  sample 169:\n    time = 1690000\n    flags = 1\n    data = length 85, hash A227BA13\n  sample 170:\n    time = 1700000\n    flags = 1\n    data = length 86, hash EFD8B10B\n  sample 171:\n    time = 1710000\n    flags = 1\n    data = length 87, hash 47FF364A\n  sample 172:\n    time = 1720000\n    flags = 1\n    data = length 91, hash 31D4B48A\n  sample 173:\n    time = 1730000\n    flags = 1\n    data = length 91, hash DD69BD85\n  sample 174:\n    time = 1740000\n    flags = 1\n    data = length 88, hash AF1A95C6\n  sample 175:\n    time = 1750000\n    flags = 1\n    data = length 87, hash 2FB8AF74\n  sample 176:\n    time = 1760000\n    flags = 1\n    data = length 92, hash 173C707A\n  sample 177:\n    time = 1770000\n    flags = 1\n    data = length 88, hash 5F58F5E8\n  sample 178:\n    time = 1780000\n    flags = 1\n    data = length 91, hash D449785F\n  sample 179:\n    time = 1790000\n    flags = 1\n    data = length 91, hash CE2CB465\n  sample 180:\n    time = 1800000\n    flags = 1\n    data = length 93, hash ABC1C62E\n  sample 181:\n    time = 1810000\n    flags = 1\n    data = length 87, hash 83B4B9A0\n  sample 182:\n    time = 1820000\n    flags = 1\n    data = length 87, hash 3220D562\n  sample 183:\n    time = 1830000\n    flags = 1\n    data = length 86, hash 64D21AA1\n  sample 184:\n    time = 1840000\n    flags = 1\n    data = length 86, hash A1FAAF2C\n  sample 185:\n    time = 1850000\n    flags = 1\n    data = length 86, hash ECA80F7E\n  sample 186:\n    time = 1860000\n    flags = 1\n    data = length 86, hash FEB03B2C\n  sample 187:\n    time = 1870000\n    flags = 1\n    data = length 85, hash 2C2E6B2F\n  sample 188:\n    time = 1880000\n    flags = 1\n    data = length 89, hash A0D7AC3\n  sample 189:\n    time = 1890000\n    flags = 1\n    data = length 87, hash 83739547\n  sample 190:\n    time = 1900000\n    flags = 1\n    data = length 86, hash 991E531E\n  sample 191:\n    time = 1910000\n    flags = 1\n    data = length 88, hash 16B287A3\n  sample 192:\n    time = 1920000\n    flags = 1\n    data = length 86, hash FC86EED\n  sample 193:\n    time = 1930000\n    flags = 1\n    data = length 86, hash 96AF0248\n  sample 194:\n    time = 1940000\n    flags = 1\n    data = length 86, hash 288402C8\n  sample 195:\n    time = 1950000\n    flags = 1\n    data = length 87, hash 4BBA7912\n  sample 196:\n    time = 1960000\n    flags = 1\n    data = length 86, hash 4A59C719\n  sample 197:\n    time = 1970000\n    flags = 1\n    data = length 85, hash 906E8187\n  sample 198:\n    time = 1980000\n    flags = 1\n    data = length 90, hash CB8B755D\n  sample 199:\n    time = 1990000\n    flags = 1\n    data = length 87, hash C8E02C\n  sample 200:\n    time = 2000000\n    flags = 1\n    data = length 88, hash ACF4D89A\n  sample 201:\n    time = 2010000\n    flags = 1\n    data = length 86, hash 510FE048\n  sample 202:\n    time = 2020000\n    flags = 1\n    data = length 86, hash 64983E46\n  sample 203:\n    time = 2030000\n    flags = 1\n    data = length 86, hash CEA76A1E\n  sample 204:\n    time = 2040000\n    flags = 1\n    data = length 87, hash AADE498E\n  sample 205:\n    time = 2050000\n    flags = 1\n    data = length 127, hash 353A6D8C\n  sample 206:\n    time = 2060000\n    flags = 1\n    data = length 87, hash 29E18E62\n  sample 207:\n    time = 2070000\n    flags = 1\n    data = length 87, hash 2CF7B30F\n  sample 208:\n    time = 2080000\n    flags = 1\n    data = length 94, hash 758704C3\n  sample 209:\n    time = 2090000\n    flags = 1\n    data = length 88, hash C2153A4C\n  sample 210:\n    time = 2100000\n    flags = 1\n    data = length 86, hash A0A83DA5\n  sample 211:\n    time = 2110000\n    flags = 1\n    data = length 86, hash 41017D7F\n  sample 212:\n    time = 2120000\n    flags = 1\n    data = length 93, hash 686B0CA2\n  sample 213:\n    time = 2130000\n    flags = 1\n    data = length 86, hash 554D16CC\n  sample 214:\n    time = 2140000\n    flags = 1\n    data = length 88, hash 99D72771\n  sample 215:\n    time = 2150000\n    flags = 1\n    data = length 88, hash 7176DFBF\n  sample 216:\n    time = 2160000\n    flags = 1\n    data = length 86, hash BAA22669\n  sample 217:\n    time = 2170000\n    flags = 1\n    data = length 88, hash B00B0D3C\n  sample 218:\n    time = 2180000\n    flags = 1\n    data = length 89, hash 73FED83A\n  sample 219:\n    time = 2190000\n    flags = 1\n    data = length 86, hash 4A4138D3\n  sample 220:\n    time = 2200000\n    flags = 1\n    data = length 89, hash E0A860FF\n  sample 221:\n    time = 2210000\n    flags = 1\n    data = length 95, hash EE5A8AED\n  sample 222:\n    time = 2220000\n    flags = 1\n    data = length 92, hash 36DBD7FD\n  sample 223:\n    time = 2230000\n    flags = 1\n    data = length 88, hash EE47A7E4\n  sample 224:\n    time = 2240000\n    flags = 1\n    data = length 100, hash 2E1A603F\n  sample 225:\n    time = 2250000\n    flags = 1\n    data = length 89, hash 657ED4A3\n  sample 226:\n    time = 2260000\n    flags = 1\n    data = length 86, hash A833DC7B\n  sample 227:\n    time = 2270000\n    flags = 1\n    data = length 88, hash 81E80732\n  sample 228:\n    time = 2280000\n    flags = 1\n    data = length 91, hash FA256A0F\n  sample 229:\n    time = 2290000\n    flags = 1\n    data = length 88, hash A63A4DBA\n  sample 230:\n    time = 2300000\n    flags = 1\n    data = length 88, hash 67910A9F\n  sample 231:\n    time = 2310000\n    flags = 1\n    data = length 86, hash EB387DB6\n  sample 232:\n    time = 2320000\n    flags = 1\n    data = length 88, hash 5ACAAC2A\n  sample 233:\n    time = 2330000\n    flags = 1\n    data = length 86, hash 6ADF2E1F\n  sample 234:\n    time = 2340000\n    flags = 1\n    data = length 85, hash 9D064471\n  sample 235:\n    time = 2350000\n    flags = 1\n    data = length 87, hash F176C59\n  sample 236:\n    time = 2360000\n    flags = 1\n    data = length 89, hash 5CA40CE4\n  sample 237:\n    time = 2370000\n    flags = 1\n    data = length 88, hash 67B944FC\n  sample 238:\n    time = 2380000\n    flags = 1\n    data = length 86, hash B3A84EC8\n  sample 239:\n    time = 2390000\n    flags = 1\n    data = length 92, hash A6ACF94B\n  sample 240:\n    time = 2400000\n    flags = 1\n    data = length 88, hash CB0C9730\n  sample 241:\n    time = 2410000\n    flags = 1\n    data = length 88, hash C79FE804\n  sample 242:\n    time = 2420000\n    flags = 1\n    data = length 88, hash A74C7F0A\n  sample 243:\n    time = 2430000\n    flags = 1\n    data = length 91, hash 55F6F0A5\n  sample 244:\n    time = 2440000\n    flags = 1\n    data = length 93, hash 330F33E7\n  sample 245:\n    time = 2450000\n    flags = 1\n    data = length 89, hash 614AFBA0\n  sample 246:\n    time = 2460000\n    flags = 1\n    data = length 87, hash 3CE4652D\n  sample 247:\n    time = 2470000\n    flags = 1\n    data = length 87, hash 4EFD5467\n  sample 248:\n    time = 2480000\n    flags = 1\n    data = length 86, hash D81B3EB8\n  sample 249:\n    time = 2490000\n    flags = 1\n    data = length 88, hash 96CB6871\n  sample 250:\n    time = 2500000\n    flags = 1\n    data = length 88, hash E9DF2786\n  sample 251:\n    time = 2510000\n    flags = 1\n    data = length 89, hash 2CA33D96\n  sample 252:\n    time = 2520000\n    flags = 1\n    data = length 90, hash 96BDE594\n  sample 253:\n    time = 2530000\n    flags = 1\n    data = length 87, hash C261493C\n  sample 254:\n    time = 2540000\n    flags = 1\n    data = length 86, hash D037318E\n  sample 255:\n    time = 2550000\n    flags = 1\n    data = length 88, hash BC15BC88\n  sample 256:\n    time = 2560000\n    flags = 1\n    data = length 91, hash A8361A51\n  sample 257:\n    time = 2570000\n    flags = 1\n    data = length 87, hash 4AFDB5F2\n  sample 258:\n    time = 2580000\n    flags = 1\n    data = length 87, hash 6447F8CB\n  sample 259:\n    time = 2590000\n    flags = 1\n    data = length 89, hash 48305229\n  sample 260:\n    time = 2600000\n    flags = 1\n    data = length 87, hash 8741D9E7\n  sample 261:\n    time = 2610000\n    flags = 1\n    data = length 120, hash 761F020C\n  sample 262:\n    time = 2620000\n    flags = 1\n    data = length 139, hash AECE2E57\n  sample 263:\n    time = 2630000\n    flags = 1\n    data = length 166, hash 6288797A\n  sample 264:\n    time = 2640000\n    flags = 1\n    data = length 144, hash 437821A0\n  sample 265:\n    time = 2650000\n    flags = 1\n    data = length 113, hash FCCBEDF1\n  sample 266:\n    time = 2660000\n    flags = 1\n    data = length 108, hash C4040614\n  sample 267:\n    time = 2670000\n    flags = 1\n    data = length 125, hash E29064C2\n  sample 268:\n    time = 2680000\n    flags = 1\n    data = length 126, hash D42D24FF\n  sample 269:\n    time = 2690000\n    flags = 1\n    data = length 122, hash 30AF267D\n  sample 270:\n    time = 2700000\n    flags = 1\n    data = length 122, hash 45CEC1FB\n  sample 271:\n    time = 2710000\n    flags = 1\n    data = length 134, hash 59143FE2\n  sample 272:\n    time = 2720000\n    flags = 1\n    data = length 134, hash BD52A84\n  sample 273:\n    time = 2730000\n    flags = 1\n    data = length 120, hash 745C3714\n  sample 274:\n    time = 2740000\n    flags = 1\n    data = length 126, hash 505E117B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_flac.ogg.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=8457]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/flac\n    maxInputSize = 768000\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 42, hash 83F6895\n  total output bytes = 164431\n  sample count = 33\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 5030, hash D2B60530\n  sample 1:\n    time = 85333\n    flags = 1\n    data = length 5066, hash 4C932A54\n  sample 2:\n    time = 170666\n    flags = 1\n    data = length 5112, hash 7E5A7B61\n  sample 3:\n    time = 256000\n    flags = 1\n    data = length 5044, hash 7EF93F13\n  sample 4:\n    time = 341333\n    flags = 1\n    data = length 4943, hash DE7E27F8\n  sample 5:\n    time = 426666\n    flags = 1\n    data = length 5121, hash 6D0D0B40\n  sample 6:\n    time = 512000\n    flags = 1\n    data = length 5068, hash 9924644F\n  sample 7:\n    time = 597333\n    flags = 1\n    data = length 5143, hash 6C34F0CE\n  sample 8:\n    time = 682666\n    flags = 1\n    data = length 5109, hash E3B7BEFB\n  sample 9:\n    time = 768000\n    flags = 1\n    data = length 5129, hash 44111D9B\n  sample 10:\n    time = 853333\n    flags = 1\n    data = length 5031, hash 9D55EA53\n  sample 11:\n    time = 938666\n    flags = 1\n    data = length 5119, hash E1CB9BA6\n  sample 12:\n    time = 1024000\n    flags = 1\n    data = length 5360, hash 17265C5D\n  sample 13:\n    time = 1109333\n    flags = 1\n    data = length 5340, hash A90FDDF1\n  sample 14:\n    time = 1194666\n    flags = 1\n    data = length 5162, hash 31F65AD5\n  sample 15:\n    time = 1280000\n    flags = 1\n    data = length 5168, hash F2394F2D\n  sample 16:\n    time = 1365333\n    flags = 1\n    data = length 5776, hash 58437AB3\n  sample 17:\n    time = 1450666\n    flags = 1\n    data = length 5394, hash EBAB20A8\n  sample 18:\n    time = 1536000\n    flags = 1\n    data = length 5168, hash BF37C7A5\n  sample 19:\n    time = 1621333\n    flags = 1\n    data = length 5324, hash 59546B7B\n  sample 20:\n    time = 1706666\n    flags = 1\n    data = length 5172, hash 6036EF0B\n  sample 21:\n    time = 1792000\n    flags = 1\n    data = length 5102, hash 5A131071\n  sample 22:\n    time = 1877333\n    flags = 1\n    data = length 5111, hash 3D9EBB3B\n  sample 23:\n    time = 1962666\n    flags = 1\n    data = length 5113, hash 61101D4F\n  sample 24:\n    time = 2048000\n    flags = 1\n    data = length 5229, hash D2E55742\n  sample 25:\n    time = 2133333\n    flags = 1\n    data = length 5162, hash 7F2E97FA\n  sample 26:\n    time = 2218666\n    flags = 1\n    data = length 5255, hash D92A782\n  sample 27:\n    time = 2304000\n    flags = 1\n    data = length 5196, hash 98FE5138\n  sample 28:\n    time = 2389333\n    flags = 1\n    data = length 5214, hash 3D35C38C\n  sample 29:\n    time = 2474666\n    flags = 1\n    data = length 5211, hash 7E25420F\n  sample 30:\n    time = 2560000\n    flags = 1\n    data = length 5230, hash 2AD96FBC\n  sample 31:\n    time = 2645333\n    flags = 1\n    data = length 3384, hash 938BCDD9\n  sample 32:\n    time = 2730666\n    flags = 1\n    data = length 445, hash A388E3D6\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_flac.ogg.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=8457]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/flac\n    maxInputSize = 768000\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 42, hash 83F6895\n  total output bytes = 113666\n  sample count = 23\n  sample 0:\n    time = 853333\n    flags = 1\n    data = length 5031, hash 9D55EA53\n  sample 1:\n    time = 938666\n    flags = 1\n    data = length 5119, hash E1CB9BA6\n  sample 2:\n    time = 1024000\n    flags = 1\n    data = length 5360, hash 17265C5D\n  sample 3:\n    time = 1109333\n    flags = 1\n    data = length 5340, hash A90FDDF1\n  sample 4:\n    time = 1194666\n    flags = 1\n    data = length 5162, hash 31F65AD5\n  sample 5:\n    time = 1280000\n    flags = 1\n    data = length 5168, hash F2394F2D\n  sample 6:\n    time = 1365333\n    flags = 1\n    data = length 5776, hash 58437AB3\n  sample 7:\n    time = 1450666\n    flags = 1\n    data = length 5394, hash EBAB20A8\n  sample 8:\n    time = 1536000\n    flags = 1\n    data = length 5168, hash BF37C7A5\n  sample 9:\n    time = 1621333\n    flags = 1\n    data = length 5324, hash 59546B7B\n  sample 10:\n    time = 1706666\n    flags = 1\n    data = length 5172, hash 6036EF0B\n  sample 11:\n    time = 1792000\n    flags = 1\n    data = length 5102, hash 5A131071\n  sample 12:\n    time = 1877333\n    flags = 1\n    data = length 5111, hash 3D9EBB3B\n  sample 13:\n    time = 1962666\n    flags = 1\n    data = length 5113, hash 61101D4F\n  sample 14:\n    time = 2048000\n    flags = 1\n    data = length 5229, hash D2E55742\n  sample 15:\n    time = 2133333\n    flags = 1\n    data = length 5162, hash 7F2E97FA\n  sample 16:\n    time = 2218666\n    flags = 1\n    data = length 5255, hash D92A782\n  sample 17:\n    time = 2304000\n    flags = 1\n    data = length 5196, hash 98FE5138\n  sample 18:\n    time = 2389333\n    flags = 1\n    data = length 5214, hash 3D35C38C\n  sample 19:\n    time = 2474666\n    flags = 1\n    data = length 5211, hash 7E25420F\n  sample 20:\n    time = 2560000\n    flags = 1\n    data = length 5230, hash 2AD96FBC\n  sample 21:\n    time = 2645333\n    flags = 1\n    data = length 3384, hash 938BCDD9\n  sample 22:\n    time = 2730666\n    flags = 1\n    data = length 445, hash A388E3D6\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_flac.ogg.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=8457]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/flac\n    maxInputSize = 768000\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 42, hash 83F6895\n  total output bytes = 55652\n  sample count = 12\n  sample 0:\n    time = 1792000\n    flags = 1\n    data = length 5102, hash 5A131071\n  sample 1:\n    time = 1877333\n    flags = 1\n    data = length 5111, hash 3D9EBB3B\n  sample 2:\n    time = 1962666\n    flags = 1\n    data = length 5113, hash 61101D4F\n  sample 3:\n    time = 2048000\n    flags = 1\n    data = length 5229, hash D2E55742\n  sample 4:\n    time = 2133333\n    flags = 1\n    data = length 5162, hash 7F2E97FA\n  sample 5:\n    time = 2218666\n    flags = 1\n    data = length 5255, hash D92A782\n  sample 6:\n    time = 2304000\n    flags = 1\n    data = length 5196, hash 98FE5138\n  sample 7:\n    time = 2389333\n    flags = 1\n    data = length 5214, hash 3D35C38C\n  sample 8:\n    time = 2474666\n    flags = 1\n    data = length 5211, hash 7E25420F\n  sample 9:\n    time = 2560000\n    flags = 1\n    data = length 5230, hash 2AD96FBC\n  sample 10:\n    time = 2645333\n    flags = 1\n    data = length 3384, hash 938BCDD9\n  sample 11:\n    time = 2730666\n    flags = 1\n    data = length 445, hash A388E3D6\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_flac.ogg.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=8457]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/flac\n    maxInputSize = 768000\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 42, hash 83F6895\n  total output bytes = 445\n  sample count = 1\n  sample 0:\n    time = 2730666\n    flags = 1\n    data = length 445, hash A388E3D6\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_flac.ogg.unklen.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=8457]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/flac\n    maxInputSize = 768000\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 42, hash 83F6895\n  total output bytes = 164431\n  sample count = 33\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 5030, hash D2B60530\n  sample 1:\n    time = 85333\n    flags = 1\n    data = length 5066, hash 4C932A54\n  sample 2:\n    time = 170666\n    flags = 1\n    data = length 5112, hash 7E5A7B61\n  sample 3:\n    time = 256000\n    flags = 1\n    data = length 5044, hash 7EF93F13\n  sample 4:\n    time = 341333\n    flags = 1\n    data = length 4943, hash DE7E27F8\n  sample 5:\n    time = 426666\n    flags = 1\n    data = length 5121, hash 6D0D0B40\n  sample 6:\n    time = 512000\n    flags = 1\n    data = length 5068, hash 9924644F\n  sample 7:\n    time = 597333\n    flags = 1\n    data = length 5143, hash 6C34F0CE\n  sample 8:\n    time = 682666\n    flags = 1\n    data = length 5109, hash E3B7BEFB\n  sample 9:\n    time = 768000\n    flags = 1\n    data = length 5129, hash 44111D9B\n  sample 10:\n    time = 853333\n    flags = 1\n    data = length 5031, hash 9D55EA53\n  sample 11:\n    time = 938666\n    flags = 1\n    data = length 5119, hash E1CB9BA6\n  sample 12:\n    time = 1024000\n    flags = 1\n    data = length 5360, hash 17265C5D\n  sample 13:\n    time = 1109333\n    flags = 1\n    data = length 5340, hash A90FDDF1\n  sample 14:\n    time = 1194666\n    flags = 1\n    data = length 5162, hash 31F65AD5\n  sample 15:\n    time = 1280000\n    flags = 1\n    data = length 5168, hash F2394F2D\n  sample 16:\n    time = 1365333\n    flags = 1\n    data = length 5776, hash 58437AB3\n  sample 17:\n    time = 1450666\n    flags = 1\n    data = length 5394, hash EBAB20A8\n  sample 18:\n    time = 1536000\n    flags = 1\n    data = length 5168, hash BF37C7A5\n  sample 19:\n    time = 1621333\n    flags = 1\n    data = length 5324, hash 59546B7B\n  sample 20:\n    time = 1706666\n    flags = 1\n    data = length 5172, hash 6036EF0B\n  sample 21:\n    time = 1792000\n    flags = 1\n    data = length 5102, hash 5A131071\n  sample 22:\n    time = 1877333\n    flags = 1\n    data = length 5111, hash 3D9EBB3B\n  sample 23:\n    time = 1962666\n    flags = 1\n    data = length 5113, hash 61101D4F\n  sample 24:\n    time = 2048000\n    flags = 1\n    data = length 5229, hash D2E55742\n  sample 25:\n    time = 2133333\n    flags = 1\n    data = length 5162, hash 7F2E97FA\n  sample 26:\n    time = 2218666\n    flags = 1\n    data = length 5255, hash D92A782\n  sample 27:\n    time = 2304000\n    flags = 1\n    data = length 5196, hash 98FE5138\n  sample 28:\n    time = 2389333\n    flags = 1\n    data = length 5214, hash 3D35C38C\n  sample 29:\n    time = 2474666\n    flags = 1\n    data = length 5211, hash 7E25420F\n  sample 30:\n    time = 2560000\n    flags = 1\n    data = length 5230, hash 2AD96FBC\n  sample 31:\n    time = 2645333\n    flags = 1\n    data = length 3384, hash 938BCDD9\n  sample 32:\n    time = 2730666\n    flags = 1\n    data = length 445, hash A388E3D6\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=8407]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/flac\n    maxInputSize = 768000\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 42, hash 83F6895\n  total output bytes = 164431\n  sample count = 33\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 5030, hash D2B60530\n  sample 1:\n    time = 85333\n    flags = 1\n    data = length 5066, hash 4C932A54\n  sample 2:\n    time = 170666\n    flags = 1\n    data = length 5112, hash 7E5A7B61\n  sample 3:\n    time = 256000\n    flags = 1\n    data = length 5044, hash 7EF93F13\n  sample 4:\n    time = 341333\n    flags = 1\n    data = length 4943, hash DE7E27F8\n  sample 5:\n    time = 426666\n    flags = 1\n    data = length 5121, hash 6D0D0B40\n  sample 6:\n    time = 512000\n    flags = 1\n    data = length 5068, hash 9924644F\n  sample 7:\n    time = 597333\n    flags = 1\n    data = length 5143, hash 6C34F0CE\n  sample 8:\n    time = 682666\n    flags = 1\n    data = length 5109, hash E3B7BEFB\n  sample 9:\n    time = 768000\n    flags = 1\n    data = length 5129, hash 44111D9B\n  sample 10:\n    time = 853333\n    flags = 1\n    data = length 5031, hash 9D55EA53\n  sample 11:\n    time = 938666\n    flags = 1\n    data = length 5119, hash E1CB9BA6\n  sample 12:\n    time = 1024000\n    flags = 1\n    data = length 5360, hash 17265C5D\n  sample 13:\n    time = 1109333\n    flags = 1\n    data = length 5340, hash A90FDDF1\n  sample 14:\n    time = 1194666\n    flags = 1\n    data = length 5162, hash 31F65AD5\n  sample 15:\n    time = 1280000\n    flags = 1\n    data = length 5168, hash F2394F2D\n  sample 16:\n    time = 1365333\n    flags = 1\n    data = length 5776, hash 58437AB3\n  sample 17:\n    time = 1450666\n    flags = 1\n    data = length 5394, hash EBAB20A8\n  sample 18:\n    time = 1536000\n    flags = 1\n    data = length 5168, hash BF37C7A5\n  sample 19:\n    time = 1621333\n    flags = 1\n    data = length 5324, hash 59546B7B\n  sample 20:\n    time = 1706666\n    flags = 1\n    data = length 5172, hash 6036EF0B\n  sample 21:\n    time = 1792000\n    flags = 1\n    data = length 5102, hash 5A131071\n  sample 22:\n    time = 1877333\n    flags = 1\n    data = length 5111, hash 3D9EBB3B\n  sample 23:\n    time = 1962666\n    flags = 1\n    data = length 5113, hash 61101D4F\n  sample 24:\n    time = 2048000\n    flags = 1\n    data = length 5229, hash D2E55742\n  sample 25:\n    time = 2133333\n    flags = 1\n    data = length 5162, hash 7F2E97FA\n  sample 26:\n    time = 2218666\n    flags = 1\n    data = length 5255, hash D92A782\n  sample 27:\n    time = 2304000\n    flags = 1\n    data = length 5196, hash 98FE5138\n  sample 28:\n    time = 2389333\n    flags = 1\n    data = length 5214, hash 3D35C38C\n  sample 29:\n    time = 2474666\n    flags = 1\n    data = length 5211, hash 7E25420F\n  sample 30:\n    time = 2560000\n    flags = 1\n    data = length 5230, hash 2AD96FBC\n  sample 31:\n    time = 2645333\n    flags = 1\n    data = length 3384, hash 938BCDD9\n  sample 32:\n    time = 2730666\n    flags = 1\n    data = length 445, hash A388E3D6\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=8407]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/flac\n    maxInputSize = 768000\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 42, hash 83F6895\n  total output bytes = 113666\n  sample count = 23\n  sample 0:\n    time = 853333\n    flags = 1\n    data = length 5031, hash 9D55EA53\n  sample 1:\n    time = 938666\n    flags = 1\n    data = length 5119, hash E1CB9BA6\n  sample 2:\n    time = 1024000\n    flags = 1\n    data = length 5360, hash 17265C5D\n  sample 3:\n    time = 1109333\n    flags = 1\n    data = length 5340, hash A90FDDF1\n  sample 4:\n    time = 1194666\n    flags = 1\n    data = length 5162, hash 31F65AD5\n  sample 5:\n    time = 1280000\n    flags = 1\n    data = length 5168, hash F2394F2D\n  sample 6:\n    time = 1365333\n    flags = 1\n    data = length 5776, hash 58437AB3\n  sample 7:\n    time = 1450666\n    flags = 1\n    data = length 5394, hash EBAB20A8\n  sample 8:\n    time = 1536000\n    flags = 1\n    data = length 5168, hash BF37C7A5\n  sample 9:\n    time = 1621333\n    flags = 1\n    data = length 5324, hash 59546B7B\n  sample 10:\n    time = 1706666\n    flags = 1\n    data = length 5172, hash 6036EF0B\n  sample 11:\n    time = 1792000\n    flags = 1\n    data = length 5102, hash 5A131071\n  sample 12:\n    time = 1877333\n    flags = 1\n    data = length 5111, hash 3D9EBB3B\n  sample 13:\n    time = 1962666\n    flags = 1\n    data = length 5113, hash 61101D4F\n  sample 14:\n    time = 2048000\n    flags = 1\n    data = length 5229, hash D2E55742\n  sample 15:\n    time = 2133333\n    flags = 1\n    data = length 5162, hash 7F2E97FA\n  sample 16:\n    time = 2218666\n    flags = 1\n    data = length 5255, hash D92A782\n  sample 17:\n    time = 2304000\n    flags = 1\n    data = length 5196, hash 98FE5138\n  sample 18:\n    time = 2389333\n    flags = 1\n    data = length 5214, hash 3D35C38C\n  sample 19:\n    time = 2474666\n    flags = 1\n    data = length 5211, hash 7E25420F\n  sample 20:\n    time = 2560000\n    flags = 1\n    data = length 5230, hash 2AD96FBC\n  sample 21:\n    time = 2645333\n    flags = 1\n    data = length 3384, hash 938BCDD9\n  sample 22:\n    time = 2730666\n    flags = 1\n    data = length 445, hash A388E3D6\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=8407]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/flac\n    maxInputSize = 768000\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 42, hash 83F6895\n  total output bytes = 55652\n  sample count = 12\n  sample 0:\n    time = 1792000\n    flags = 1\n    data = length 5102, hash 5A131071\n  sample 1:\n    time = 1877333\n    flags = 1\n    data = length 5111, hash 3D9EBB3B\n  sample 2:\n    time = 1962666\n    flags = 1\n    data = length 5113, hash 61101D4F\n  sample 3:\n    time = 2048000\n    flags = 1\n    data = length 5229, hash D2E55742\n  sample 4:\n    time = 2133333\n    flags = 1\n    data = length 5162, hash 7F2E97FA\n  sample 5:\n    time = 2218666\n    flags = 1\n    data = length 5255, hash D92A782\n  sample 6:\n    time = 2304000\n    flags = 1\n    data = length 5196, hash 98FE5138\n  sample 7:\n    time = 2389333\n    flags = 1\n    data = length 5214, hash 3D35C38C\n  sample 8:\n    time = 2474666\n    flags = 1\n    data = length 5211, hash 7E25420F\n  sample 9:\n    time = 2560000\n    flags = 1\n    data = length 5230, hash 2AD96FBC\n  sample 10:\n    time = 2645333\n    flags = 1\n    data = length 3384, hash 938BCDD9\n  sample 11:\n    time = 2730666\n    flags = 1\n    data = length 445, hash A388E3D6\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=8407]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/flac\n    maxInputSize = 768000\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 42, hash 83F6895\n  total output bytes = 445\n  sample count = 1\n  sample 0:\n    time = 2730666\n    flags = 1\n    data = length 445, hash A388E3D6\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_flac_noseektable.ogg.unklen.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/flac\n    maxInputSize = 768000\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 42, hash 83F6895\n  total output bytes = 164431\n  sample count = 33\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 5030, hash D2B60530\n  sample 1:\n    time = 85333\n    flags = 1\n    data = length 5066, hash 4C932A54\n  sample 2:\n    time = 170666\n    flags = 1\n    data = length 5112, hash 7E5A7B61\n  sample 3:\n    time = 256000\n    flags = 1\n    data = length 5044, hash 7EF93F13\n  sample 4:\n    time = 341333\n    flags = 1\n    data = length 4943, hash DE7E27F8\n  sample 5:\n    time = 426666\n    flags = 1\n    data = length 5121, hash 6D0D0B40\n  sample 6:\n    time = 512000\n    flags = 1\n    data = length 5068, hash 9924644F\n  sample 7:\n    time = 597333\n    flags = 1\n    data = length 5143, hash 6C34F0CE\n  sample 8:\n    time = 682666\n    flags = 1\n    data = length 5109, hash E3B7BEFB\n  sample 9:\n    time = 768000\n    flags = 1\n    data = length 5129, hash 44111D9B\n  sample 10:\n    time = 853333\n    flags = 1\n    data = length 5031, hash 9D55EA53\n  sample 11:\n    time = 938666\n    flags = 1\n    data = length 5119, hash E1CB9BA6\n  sample 12:\n    time = 1024000\n    flags = 1\n    data = length 5360, hash 17265C5D\n  sample 13:\n    time = 1109333\n    flags = 1\n    data = length 5340, hash A90FDDF1\n  sample 14:\n    time = 1194666\n    flags = 1\n    data = length 5162, hash 31F65AD5\n  sample 15:\n    time = 1280000\n    flags = 1\n    data = length 5168, hash F2394F2D\n  sample 16:\n    time = 1365333\n    flags = 1\n    data = length 5776, hash 58437AB3\n  sample 17:\n    time = 1450666\n    flags = 1\n    data = length 5394, hash EBAB20A8\n  sample 18:\n    time = 1536000\n    flags = 1\n    data = length 5168, hash BF37C7A5\n  sample 19:\n    time = 1621333\n    flags = 1\n    data = length 5324, hash 59546B7B\n  sample 20:\n    time = 1706666\n    flags = 1\n    data = length 5172, hash 6036EF0B\n  sample 21:\n    time = 1792000\n    flags = 1\n    data = length 5102, hash 5A131071\n  sample 22:\n    time = 1877333\n    flags = 1\n    data = length 5111, hash 3D9EBB3B\n  sample 23:\n    time = 1962666\n    flags = 1\n    data = length 5113, hash 61101D4F\n  sample 24:\n    time = 2048000\n    flags = 1\n    data = length 5229, hash D2E55742\n  sample 25:\n    time = 2133333\n    flags = 1\n    data = length 5162, hash 7F2E97FA\n  sample 26:\n    time = 2218666\n    flags = 1\n    data = length 5255, hash D92A782\n  sample 27:\n    time = 2304000\n    flags = 1\n    data = length 5196, hash 98FE5138\n  sample 28:\n    time = 2389333\n    flags = 1\n    data = length 5214, hash 3D35C38C\n  sample 29:\n    time = 2474666\n    flags = 1\n    data = length 5211, hash 7E25420F\n  sample 30:\n    time = 2560000\n    flags = 1\n    data = length 5230, hash 2AD96FBC\n  sample 31:\n    time = 2645333\n    flags = 1\n    data = length 3384, hash 938BCDD9\n  sample 32:\n    time = 2730666\n    flags = 1\n    data = length 445, hash A388E3D6\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_vorbis.ogg.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=3995]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 112000\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/vorbis\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 30, hash 9A8FF207\n      data = length 3832, hash 8A406249\n  total output bytes = 26873\n  sample count = 180\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 49, hash 2FFF94F0\n  sample 1:\n    time = 0\n    flags = 1\n    data = length 44, hash 3946418A\n  sample 2:\n    time = 2666\n    flags = 1\n    data = length 55, hash 2A0B878E\n  sample 3:\n    time = 5333\n    flags = 1\n    data = length 53, hash CC3B6879\n  sample 4:\n    time = 8000\n    flags = 1\n    data = length 215, hash 106AE950\n  sample 5:\n    time = 20000\n    flags = 1\n    data = length 192, hash 2B219F53\n  sample 6:\n    time = 41333\n    flags = 1\n    data = length 197, hash FBC39422\n  sample 7:\n    time = 62666\n    flags = 1\n    data = length 209, hash 386E8979\n  sample 8:\n    time = 84000\n    flags = 1\n    data = length 42, hash E81162C1\n  sample 9:\n    time = 96000\n    flags = 1\n    data = length 41, hash F15BEE36\n  sample 10:\n    time = 98666\n    flags = 1\n    data = length 42, hash D67EB19\n  sample 11:\n    time = 101333\n    flags = 1\n    data = length 42, hash F4DE4792\n  sample 12:\n    time = 104000\n    flags = 1\n    data = length 53, hash 80F66AC3\n  sample 13:\n    time = 106666\n    flags = 1\n    data = length 56, hash DCB9DFC4\n  sample 14:\n    time = 109333\n    flags = 1\n    data = length 55, hash 4E0C4E9D\n  sample 15:\n    time = 112000\n    flags = 1\n    data = length 203, hash 176B6862\n  sample 16:\n    time = 124000\n    flags = 1\n    data = length 193, hash AB13CB10\n  sample 17:\n    time = 145333\n    flags = 1\n    data = length 203, hash DE63DE9F\n  sample 18:\n    time = 166666\n    flags = 1\n    data = length 194, hash 4A9508A2\n  sample 19:\n    time = 188000\n    flags = 1\n    data = length 210, hash 196899B3\n  sample 20:\n    time = 209333\n    flags = 1\n    data = length 195, hash B68407F1\n  sample 21:\n    time = 230666\n    flags = 1\n    data = length 193, hash A1FA86E3\n  sample 22:\n    time = 252000\n    flags = 1\n    data = length 194, hash 5C0B9343\n  sample 23:\n    time = 273333\n    flags = 1\n    data = length 198, hash 789914B2\n  sample 24:\n    time = 294666\n    flags = 1\n    data = length 183, hash 1B82D11F\n  sample 25:\n    time = 316000\n    flags = 1\n    data = length 199, hash D5B848F4\n  sample 26:\n    time = 337333\n    flags = 1\n    data = length 192, hash B34427EA\n  sample 27:\n    time = 358666\n    flags = 1\n    data = length 199, hash C2599BB5\n  sample 28:\n    time = 380000\n    flags = 1\n    data = length 195, hash BFD83194\n  sample 29:\n    time = 401333\n    flags = 1\n    data = length 199, hash C9A7F7CA\n  sample 30:\n    time = 422666\n    flags = 1\n    data = length 44, hash 5D76EAD6\n  sample 31:\n    time = 434666\n    flags = 1\n    data = length 43, hash 8619C423\n  sample 32:\n    time = 437333\n    flags = 1\n    data = length 43, hash E490BBE\n  sample 33:\n    time = 440000\n    flags = 1\n    data = length 53, hash 8A557CAE\n  sample 34:\n    time = 442666\n    flags = 1\n    data = length 56, hash 81007BBA\n  sample 35:\n    time = 445333\n    flags = 1\n    data = length 56, hash 4E4DD67F\n  sample 36:\n    time = 448000\n    flags = 1\n    data = length 222, hash 414188AB\n  sample 37:\n    time = 460000\n    flags = 1\n    data = length 202, hash 67A07D30\n  sample 38:\n    time = 481333\n    flags = 1\n    data = length 200, hash E357D853\n  sample 39:\n    time = 502666\n    flags = 1\n    data = length 203, hash 4653DC90\n  sample 40:\n    time = 524000\n    flags = 1\n    data = length 192, hash A65E6C09\n  sample 41:\n    time = 545333\n    flags = 1\n    data = length 202, hash FBEAC508\n  sample 42:\n    time = 566666\n    flags = 1\n    data = length 202, hash E9B7B59F\n  sample 43:\n    time = 588000\n    flags = 1\n    data = length 204, hash E24AA78E\n  sample 44:\n    time = 609333\n    flags = 1\n    data = length 41, hash 3FBC5216\n  sample 45:\n    time = 621333\n    flags = 1\n    data = length 47, hash 153FBC55\n  sample 46:\n    time = 624000\n    flags = 1\n    data = length 42, hash 2B493D6C\n  sample 47:\n    time = 626666\n    flags = 1\n    data = length 42, hash 8303BEE3\n  sample 48:\n    time = 629333\n    flags = 1\n    data = length 62, hash 71AEE50B\n  sample 49:\n    time = 632000\n    flags = 1\n    data = length 54, hash 52F61908\n  sample 50:\n    time = 634666\n    flags = 1\n    data = length 45, hash 7BD3E3A1\n  sample 51:\n    time = 637333\n    flags = 1\n    data = length 41, hash E0F65472\n  sample 52:\n    time = 640000\n    flags = 1\n    data = length 45, hash 41838675\n  sample 53:\n    time = 642666\n    flags = 1\n    data = length 44, hash FCBC2147\n  sample 54:\n    time = 645333\n    flags = 1\n    data = length 45, hash 1A5987E3\n  sample 55:\n    time = 648000\n    flags = 1\n    data = length 43, hash 99074864\n  sample 56:\n    time = 650666\n    flags = 1\n    data = length 57, hash D4A9B60A\n  sample 57:\n    time = 653333\n    flags = 1\n    data = length 52, hash 302129DA\n  sample 58:\n    time = 656000\n    flags = 1\n    data = length 57, hash D8DD99C0\n  sample 59:\n    time = 658666\n    flags = 1\n    data = length 206, hash F4B9EF26\n  sample 60:\n    time = 670666\n    flags = 1\n    data = length 197, hash 7B8ACC8A\n  sample 61:\n    time = 692000\n    flags = 1\n    data = length 186, hash 161027CB\n  sample 62:\n    time = 713333\n    flags = 1\n    data = length 186, hash 1D6871B6\n  sample 63:\n    time = 734666\n    flags = 1\n    data = length 201, hash 536E9FDB\n  sample 64:\n    time = 756000\n    flags = 1\n    data = length 192, hash D38EFAC5\n  sample 65:\n    time = 777333\n    flags = 1\n    data = length 194, hash 4B394EF3\n  sample 66:\n    time = 798666\n    flags = 1\n    data = length 206, hash 1B31BA99\n  sample 67:\n    time = 820000\n    flags = 1\n    data = length 212, hash AD061F43\n  sample 68:\n    time = 841333\n    flags = 1\n    data = length 180, hash 6D1F7481\n  sample 69:\n    time = 862666\n    flags = 1\n    data = length 195, hash D80B21F\n  sample 70:\n    time = 884000\n    flags = 1\n    data = length 186, hash D367882\n  sample 71:\n    time = 905333\n    flags = 1\n    data = length 195, hash 2722159A\n  sample 72:\n    time = 926666\n    flags = 1\n    data = length 199, hash 10CEE97A\n  sample 73:\n    time = 948000\n    flags = 1\n    data = length 191, hash 2CF9FB3F\n  sample 74:\n    time = 969333\n    flags = 1\n    data = length 197, hash A725DA0\n  sample 75:\n    time = 990666\n    flags = 1\n    data = length 211, hash D4E5DB9E\n  sample 76:\n    time = 1012000\n    flags = 1\n    data = length 189, hash 1A90F496\n  sample 77:\n    time = 1033333\n    flags = 1\n    data = length 187, hash 44DB2689\n  sample 78:\n    time = 1054666\n    flags = 1\n    data = length 197, hash 6D3E5117\n  sample 79:\n    time = 1076000\n    flags = 1\n    data = length 208, hash 5B57B288\n  sample 80:\n    time = 1097333\n    flags = 1\n    data = length 198, hash D5FC05\n  sample 81:\n    time = 1118666\n    flags = 1\n    data = length 192, hash 350BBA45\n  sample 82:\n    time = 1140000\n    flags = 1\n    data = length 195, hash 5F96F2A8\n  sample 83:\n    time = 1161333\n    flags = 1\n    data = length 202, hash 61D7CC33\n  sample 84:\n    time = 1182666\n    flags = 1\n    data = length 202, hash 49D335F2\n  sample 85:\n    time = 1204000\n    flags = 1\n    data = length 192, hash 2FE9CB1A\n  sample 86:\n    time = 1225333\n    flags = 1\n    data = length 201, hash BF0763B2\n  sample 87:\n    time = 1246666\n    flags = 1\n    data = length 184, hash AD047421\n  sample 88:\n    time = 1268000\n    flags = 1\n    data = length 196, hash F9088F14\n  sample 89:\n    time = 1289333\n    flags = 1\n    data = length 190, hash AC6D38FD\n  sample 90:\n    time = 1310666\n    flags = 1\n    data = length 195, hash 8D1A66D2\n  sample 91:\n    time = 1332000\n    flags = 1\n    data = length 197, hash B46BFB6B\n  sample 92:\n    time = 1353333\n    flags = 1\n    data = length 195, hash D9761F23\n  sample 93:\n    time = 1374666\n    flags = 1\n    data = length 204, hash 3391B617\n  sample 94:\n    time = 1396000\n    flags = 1\n    data = length 42, hash 33A1FB52\n  sample 95:\n    time = 1408000\n    flags = 1\n    data = length 44, hash 408B146E\n  sample 96:\n    time = 1410666\n    flags = 1\n    data = length 44, hash 171C7E0D\n  sample 97:\n    time = 1413333\n    flags = 1\n    data = length 54, hash 6307E16C\n  sample 98:\n    time = 1416000\n    flags = 1\n    data = length 53, hash 4A319572\n  sample 99:\n    time = 1418666\n    flags = 1\n    data = length 215, hash BA9C445C\n  sample 100:\n    time = 1430666\n    flags = 1\n    data = length 201, hash 3120D234\n  sample 101:\n    time = 1452000\n    flags = 1\n    data = length 187, hash DB44993C\n  sample 102:\n    time = 1473333\n    flags = 1\n    data = length 196, hash CF2002D7\n  sample 103:\n    time = 1494666\n    flags = 1\n    data = length 185, hash E03B5D7\n  sample 104:\n    time = 1516000\n    flags = 1\n    data = length 187, hash DA399A2C\n  sample 105:\n    time = 1537333\n    flags = 1\n    data = length 191, hash 292AA0DB\n  sample 106:\n    time = 1558666\n    flags = 1\n    data = length 201, hash 221910E0\n  sample 107:\n    time = 1580000\n    flags = 1\n    data = length 194, hash F4ED7821\n  sample 108:\n    time = 1601333\n    flags = 1\n    data = length 43, hash FDDA515E\n  sample 109:\n    time = 1613333\n    flags = 1\n    data = length 42, hash F3571C0A\n  sample 110:\n    time = 1616000\n    flags = 1\n    data = length 38, hash 39F910B3\n  sample 111:\n    time = 1618666\n    flags = 1\n    data = length 41, hash 2D189531\n  sample 112:\n    time = 1621333\n    flags = 1\n    data = length 43, hash 1F7574DB\n  sample 113:\n    time = 1624000\n    flags = 1\n    data = length 43, hash 644D15E5\n  sample 114:\n    time = 1626666\n    flags = 1\n    data = length 49, hash E8A0878\n  sample 115:\n    time = 1629333\n    flags = 1\n    data = length 55, hash DFF2046D\n  sample 116:\n    time = 1632000\n    flags = 1\n    data = length 49, hash 9FB8A23\n  sample 117:\n    time = 1634666\n    flags = 1\n    data = length 41, hash E3E15E3B\n  sample 118:\n    time = 1637333\n    flags = 1\n    data = length 42, hash E5D17A32\n  sample 119:\n    time = 1640000\n    flags = 1\n    data = length 42, hash F308B653\n  sample 120:\n    time = 1642666\n    flags = 1\n    data = length 55, hash BB750D76\n  sample 121:\n    time = 1645333\n    flags = 1\n    data = length 51, hash 96772ABF\n  sample 122:\n    time = 1648000\n    flags = 1\n    data = length 197, hash E4524346\n  sample 123:\n    time = 1660000\n    flags = 1\n    data = length 188, hash AC3E1BB5\n  sample 124:\n    time = 1681333\n    flags = 1\n    data = length 195, hash F56DB8A5\n  sample 125:\n    time = 1702666\n    flags = 1\n    data = length 198, hash C8970FF7\n  sample 126:\n    time = 1724000\n    flags = 1\n    data = length 202, hash AF425C68\n  sample 127:\n    time = 1745333\n    flags = 1\n    data = length 196, hash 4215D839\n  sample 128:\n    time = 1766666\n    flags = 1\n    data = length 204, hash DB9BE8E3\n  sample 129:\n    time = 1788000\n    flags = 1\n    data = length 206, hash E5B20AB8\n  sample 130:\n    time = 1809333\n    flags = 1\n    data = length 209, hash D7F47B95\n  sample 131:\n    time = 1830666\n    flags = 1\n    data = length 193, hash FB54FB05\n  sample 132:\n    time = 1852000\n    flags = 1\n    data = length 199, hash D99C3106\n  sample 133:\n    time = 1873333\n    flags = 1\n    data = length 206, hash 253885B9\n  sample 134:\n    time = 1894666\n    flags = 1\n    data = length 191, hash FBDD8162\n  sample 135:\n    time = 1916000\n    flags = 1\n    data = length 183, hash 7290332F\n  sample 136:\n    time = 1937333\n    flags = 1\n    data = length 189, hash 1A9DC3DE\n  sample 137:\n    time = 1958666\n    flags = 1\n    data = length 201, hash 5D936764\n  sample 138:\n    time = 1980000\n    flags = 1\n    data = length 193, hash 6B03E75E\n  sample 139:\n    time = 2001333\n    flags = 1\n    data = length 199, hash 8A21BA83\n  sample 140:\n    time = 2022666\n    flags = 1\n    data = length 41, hash E6362210\n  sample 141:\n    time = 2034666\n    flags = 1\n    data = length 43, hash 36A57B44\n  sample 142:\n    time = 2037333\n    flags = 1\n    data = length 43, hash E51797D5\n  sample 143:\n    time = 2040000\n    flags = 1\n    data = length 43, hash 1F336C72\n  sample 144:\n    time = 2042666\n    flags = 1\n    data = length 42, hash 201AD367\n  sample 145:\n    time = 2045333\n    flags = 1\n    data = length 50, hash 606CCD6\n  sample 146:\n    time = 2048000\n    flags = 1\n    data = length 56, hash B15EBD7A\n  sample 147:\n    time = 2050666\n    flags = 1\n    data = length 212, hash 273B8D22\n  sample 148:\n    time = 2062666\n    flags = 1\n    data = length 194, hash 44F9CE1\n  sample 149:\n    time = 2084000\n    flags = 1\n    data = length 195, hash EDF9EBA1\n  sample 150:\n    time = 2105333\n    flags = 1\n    data = length 194, hash CE9F2D26\n  sample 151:\n    time = 2126666\n    flags = 1\n    data = length 192, hash 204F8A23\n  sample 152:\n    time = 2148000\n    flags = 1\n    data = length 206, hash DFA57E67\n  sample 153:\n    time = 2169333\n    flags = 1\n    data = length 196, hash 3CF084AB\n  sample 154:\n    time = 2190666\n    flags = 1\n    data = length 202, hash 2AF75C08\n  sample 155:\n    time = 2212000\n    flags = 1\n    data = length 203, hash 748EAF7\n  sample 156:\n    time = 2233333\n    flags = 1\n    data = length 205, hash ED82379D\n  sample 157:\n    time = 2254666\n    flags = 1\n    data = length 193, hash 61F26F22\n  sample 158:\n    time = 2276000\n    flags = 1\n    data = length 189, hash 85EF1D20\n  sample 159:\n    time = 2297333\n    flags = 1\n    data = length 187, hash 25E41FBF\n  sample 160:\n    time = 2318666\n    flags = 1\n    data = length 199, hash F365808\n  sample 161:\n    time = 2340000\n    flags = 1\n    data = length 197, hash 94205329\n  sample 162:\n    time = 2361333\n    flags = 1\n    data = length 201, hash FA2B2055\n  sample 163:\n    time = 2382666\n    flags = 1\n    data = length 194, hash AF95381F\n  sample 164:\n    time = 2404000\n    flags = 1\n    data = length 201, hash 923D3534\n  sample 165:\n    time = 2425333\n    flags = 1\n    data = length 198, hash 35F84C2E\n  sample 166:\n    time = 2446666\n    flags = 1\n    data = length 204, hash 6642CA40\n  sample 167:\n    time = 2468000\n    flags = 1\n    data = length 183, hash 3E2DC6BE\n  sample 168:\n    time = 2489333\n    flags = 1\n    data = length 197, hash B1E458CE\n  sample 169:\n    time = 2510666\n    flags = 1\n    data = length 193, hash E9218C84\n  sample 170:\n    time = 2532000\n    flags = 1\n    data = length 192, hash FEF08D4B\n  sample 171:\n    time = 2553333\n    flags = 1\n    data = length 201, hash FC411147\n  sample 172:\n    time = 2574666\n    flags = 1\n    data = length 218, hash 86893464\n  sample 173:\n    time = 2596000\n    flags = 1\n    data = length 226, hash 31C5320\n  sample 174:\n    time = 2617333\n    flags = 1\n    data = length 233, hash 9432BEE5\n  sample 175:\n    time = 2638666\n    flags = 1\n    data = length 213, hash B3FCC53E\n  sample 176:\n    time = 2660000\n    flags = 1\n    data = length 204, hash D70DD5A2\n  sample 177:\n    time = 2681333\n    flags = 1\n    data = length 212, hash A4EF1B69\n  sample 178:\n    time = 2702666\n    flags = 1\n    data = length 203, hash 8B0748B5\n  sample 179:\n    time = 2724000\n    flags = 1\n    data = length 149, hash E455335B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_vorbis.ogg.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=3995]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 112000\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/vorbis\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 30, hash 9A8FF207\n      data = length 3832, hash 8A406249\n  total output bytes = 17598\n  sample count = 109\n  sample 0:\n    time = 896000\n    flags = 1\n    data = length 195, hash 2722159A\n  sample 1:\n    time = 917333\n    flags = 1\n    data = length 199, hash 10CEE97A\n  sample 2:\n    time = 938666\n    flags = 1\n    data = length 191, hash 2CF9FB3F\n  sample 3:\n    time = 960000\n    flags = 1\n    data = length 197, hash A725DA0\n  sample 4:\n    time = 981333\n    flags = 1\n    data = length 211, hash D4E5DB9E\n  sample 5:\n    time = 1002666\n    flags = 1\n    data = length 189, hash 1A90F496\n  sample 6:\n    time = 1024000\n    flags = 1\n    data = length 187, hash 44DB2689\n  sample 7:\n    time = 1045333\n    flags = 1\n    data = length 197, hash 6D3E5117\n  sample 8:\n    time = 1066666\n    flags = 1\n    data = length 208, hash 5B57B288\n  sample 9:\n    time = 1088000\n    flags = 1\n    data = length 198, hash D5FC05\n  sample 10:\n    time = 1109333\n    flags = 1\n    data = length 192, hash 350BBA45\n  sample 11:\n    time = 1130666\n    flags = 1\n    data = length 195, hash 5F96F2A8\n  sample 12:\n    time = 1152000\n    flags = 1\n    data = length 202, hash 61D7CC33\n  sample 13:\n    time = 1173333\n    flags = 1\n    data = length 202, hash 49D335F2\n  sample 14:\n    time = 1194666\n    flags = 1\n    data = length 192, hash 2FE9CB1A\n  sample 15:\n    time = 1216000\n    flags = 1\n    data = length 201, hash BF0763B2\n  sample 16:\n    time = 1237333\n    flags = 1\n    data = length 184, hash AD047421\n  sample 17:\n    time = 1258666\n    flags = 1\n    data = length 196, hash F9088F14\n  sample 18:\n    time = 1280000\n    flags = 1\n    data = length 190, hash AC6D38FD\n  sample 19:\n    time = 1301333\n    flags = 1\n    data = length 195, hash 8D1A66D2\n  sample 20:\n    time = 1322666\n    flags = 1\n    data = length 197, hash B46BFB6B\n  sample 21:\n    time = 1344000\n    flags = 1\n    data = length 195, hash D9761F23\n  sample 22:\n    time = 1365333\n    flags = 1\n    data = length 204, hash 3391B617\n  sample 23:\n    time = 1386666\n    flags = 1\n    data = length 42, hash 33A1FB52\n  sample 24:\n    time = 1398666\n    flags = 1\n    data = length 44, hash 408B146E\n  sample 25:\n    time = 1401333\n    flags = 1\n    data = length 44, hash 171C7E0D\n  sample 26:\n    time = 1404000\n    flags = 1\n    data = length 54, hash 6307E16C\n  sample 27:\n    time = 1406666\n    flags = 1\n    data = length 53, hash 4A319572\n  sample 28:\n    time = 1409333\n    flags = 1\n    data = length 215, hash BA9C445C\n  sample 29:\n    time = 1421333\n    flags = 1\n    data = length 201, hash 3120D234\n  sample 30:\n    time = 1442666\n    flags = 1\n    data = length 187, hash DB44993C\n  sample 31:\n    time = 1464000\n    flags = 1\n    data = length 196, hash CF2002D7\n  sample 32:\n    time = 1485333\n    flags = 1\n    data = length 185, hash E03B5D7\n  sample 33:\n    time = 1506666\n    flags = 1\n    data = length 187, hash DA399A2C\n  sample 34:\n    time = 1528000\n    flags = 1\n    data = length 191, hash 292AA0DB\n  sample 35:\n    time = 1549333\n    flags = 1\n    data = length 201, hash 221910E0\n  sample 36:\n    time = 1570666\n    flags = 1\n    data = length 194, hash F4ED7821\n  sample 37:\n    time = 1592000\n    flags = 1\n    data = length 43, hash FDDA515E\n  sample 38:\n    time = 1604000\n    flags = 1\n    data = length 42, hash F3571C0A\n  sample 39:\n    time = 1606666\n    flags = 1\n    data = length 38, hash 39F910B3\n  sample 40:\n    time = 1609333\n    flags = 1\n    data = length 41, hash 2D189531\n  sample 41:\n    time = 1612000\n    flags = 1\n    data = length 43, hash 1F7574DB\n  sample 42:\n    time = 1614666\n    flags = 1\n    data = length 43, hash 644D15E5\n  sample 43:\n    time = 1617333\n    flags = 1\n    data = length 49, hash E8A0878\n  sample 44:\n    time = 1620000\n    flags = 1\n    data = length 55, hash DFF2046D\n  sample 45:\n    time = 1622666\n    flags = 1\n    data = length 49, hash 9FB8A23\n  sample 46:\n    time = 1625333\n    flags = 1\n    data = length 41, hash E3E15E3B\n  sample 47:\n    time = 1628000\n    flags = 1\n    data = length 42, hash E5D17A32\n  sample 48:\n    time = 1630666\n    flags = 1\n    data = length 42, hash F308B653\n  sample 49:\n    time = 1633333\n    flags = 1\n    data = length 55, hash BB750D76\n  sample 50:\n    time = 1636000\n    flags = 1\n    data = length 51, hash 96772ABF\n  sample 51:\n    time = 1638666\n    flags = 1\n    data = length 197, hash E4524346\n  sample 52:\n    time = 1650666\n    flags = 1\n    data = length 188, hash AC3E1BB5\n  sample 53:\n    time = 1672000\n    flags = 1\n    data = length 195, hash F56DB8A5\n  sample 54:\n    time = 1693333\n    flags = 1\n    data = length 198, hash C8970FF7\n  sample 55:\n    time = 1714666\n    flags = 1\n    data = length 202, hash AF425C68\n  sample 56:\n    time = 1736000\n    flags = 1\n    data = length 196, hash 4215D839\n  sample 57:\n    time = 1757333\n    flags = 1\n    data = length 204, hash DB9BE8E3\n  sample 58:\n    time = 1778666\n    flags = 1\n    data = length 206, hash E5B20AB8\n  sample 59:\n    time = 1800000\n    flags = 1\n    data = length 209, hash D7F47B95\n  sample 60:\n    time = 1821333\n    flags = 1\n    data = length 193, hash FB54FB05\n  sample 61:\n    time = 1842666\n    flags = 1\n    data = length 199, hash D99C3106\n  sample 62:\n    time = 1864000\n    flags = 1\n    data = length 206, hash 253885B9\n  sample 63:\n    time = 1885333\n    flags = 1\n    data = length 191, hash FBDD8162\n  sample 64:\n    time = 1906666\n    flags = 1\n    data = length 183, hash 7290332F\n  sample 65:\n    time = 1928000\n    flags = 1\n    data = length 189, hash 1A9DC3DE\n  sample 66:\n    time = 1949333\n    flags = 1\n    data = length 201, hash 5D936764\n  sample 67:\n    time = 1970666\n    flags = 1\n    data = length 193, hash 6B03E75E\n  sample 68:\n    time = 1992000\n    flags = 1\n    data = length 199, hash 8A21BA83\n  sample 69:\n    time = 2013333\n    flags = 1\n    data = length 41, hash E6362210\n  sample 70:\n    time = 2025333\n    flags = 1\n    data = length 43, hash 36A57B44\n  sample 71:\n    time = 2028000\n    flags = 1\n    data = length 43, hash E51797D5\n  sample 72:\n    time = 2030666\n    flags = 1\n    data = length 43, hash 1F336C72\n  sample 73:\n    time = 2033333\n    flags = 1\n    data = length 42, hash 201AD367\n  sample 74:\n    time = 2036000\n    flags = 1\n    data = length 50, hash 606CCD6\n  sample 75:\n    time = 2038666\n    flags = 1\n    data = length 56, hash B15EBD7A\n  sample 76:\n    time = 2041333\n    flags = 1\n    data = length 212, hash 273B8D22\n  sample 77:\n    time = 2053333\n    flags = 1\n    data = length 194, hash 44F9CE1\n  sample 78:\n    time = 2074666\n    flags = 1\n    data = length 195, hash EDF9EBA1\n  sample 79:\n    time = 2096000\n    flags = 1\n    data = length 194, hash CE9F2D26\n  sample 80:\n    time = 2117333\n    flags = 1\n    data = length 192, hash 204F8A23\n  sample 81:\n    time = 2138666\n    flags = 1\n    data = length 206, hash DFA57E67\n  sample 82:\n    time = 2160000\n    flags = 1\n    data = length 196, hash 3CF084AB\n  sample 83:\n    time = 2181333\n    flags = 1\n    data = length 202, hash 2AF75C08\n  sample 84:\n    time = 2202666\n    flags = 1\n    data = length 203, hash 748EAF7\n  sample 85:\n    time = 2224000\n    flags = 1\n    data = length 205, hash ED82379D\n  sample 86:\n    time = 2245333\n    flags = 1\n    data = length 193, hash 61F26F22\n  sample 87:\n    time = 2266666\n    flags = 1\n    data = length 189, hash 85EF1D20\n  sample 88:\n    time = 2288000\n    flags = 1\n    data = length 187, hash 25E41FBF\n  sample 89:\n    time = 2309333\n    flags = 1\n    data = length 199, hash F365808\n  sample 90:\n    time = 2330666\n    flags = 1\n    data = length 197, hash 94205329\n  sample 91:\n    time = 2352000\n    flags = 1\n    data = length 201, hash FA2B2055\n  sample 92:\n    time = 2373333\n    flags = 1\n    data = length 194, hash AF95381F\n  sample 93:\n    time = 2394666\n    flags = 1\n    data = length 201, hash 923D3534\n  sample 94:\n    time = 2416000\n    flags = 1\n    data = length 198, hash 35F84C2E\n  sample 95:\n    time = 2437333\n    flags = 1\n    data = length 204, hash 6642CA40\n  sample 96:\n    time = 2458666\n    flags = 1\n    data = length 183, hash 3E2DC6BE\n  sample 97:\n    time = 2480000\n    flags = 1\n    data = length 197, hash B1E458CE\n  sample 98:\n    time = 2501333\n    flags = 1\n    data = length 193, hash E9218C84\n  sample 99:\n    time = 2522666\n    flags = 1\n    data = length 192, hash FEF08D4B\n  sample 100:\n    time = 2544000\n    flags = 1\n    data = length 201, hash FC411147\n  sample 101:\n    time = 2565333\n    flags = 1\n    data = length 218, hash 86893464\n  sample 102:\n    time = 2586666\n    flags = 1\n    data = length 226, hash 31C5320\n  sample 103:\n    time = 2608000\n    flags = 1\n    data = length 233, hash 9432BEE5\n  sample 104:\n    time = 2629333\n    flags = 1\n    data = length 213, hash B3FCC53E\n  sample 105:\n    time = 2650666\n    flags = 1\n    data = length 204, hash D70DD5A2\n  sample 106:\n    time = 2672000\n    flags = 1\n    data = length 212, hash A4EF1B69\n  sample 107:\n    time = 2693333\n    flags = 1\n    data = length 203, hash 8B0748B5\n  sample 108:\n    time = 2714666\n    flags = 1\n    data = length 149, hash E455335B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_vorbis.ogg.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=3995]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 112000\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/vorbis\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 30, hash 9A8FF207\n      data = length 3832, hash 8A406249\n  total output bytes = 8658\n  sample count = 49\n  sample 0:\n    time = 1821333\n    flags = 1\n    data = length 193, hash FB54FB05\n  sample 1:\n    time = 1842666\n    flags = 1\n    data = length 199, hash D99C3106\n  sample 2:\n    time = 1864000\n    flags = 1\n    data = length 206, hash 253885B9\n  sample 3:\n    time = 1885333\n    flags = 1\n    data = length 191, hash FBDD8162\n  sample 4:\n    time = 1906666\n    flags = 1\n    data = length 183, hash 7290332F\n  sample 5:\n    time = 1928000\n    flags = 1\n    data = length 189, hash 1A9DC3DE\n  sample 6:\n    time = 1949333\n    flags = 1\n    data = length 201, hash 5D936764\n  sample 7:\n    time = 1970666\n    flags = 1\n    data = length 193, hash 6B03E75E\n  sample 8:\n    time = 1992000\n    flags = 1\n    data = length 199, hash 8A21BA83\n  sample 9:\n    time = 2013333\n    flags = 1\n    data = length 41, hash E6362210\n  sample 10:\n    time = 2025333\n    flags = 1\n    data = length 43, hash 36A57B44\n  sample 11:\n    time = 2028000\n    flags = 1\n    data = length 43, hash E51797D5\n  sample 12:\n    time = 2030666\n    flags = 1\n    data = length 43, hash 1F336C72\n  sample 13:\n    time = 2033333\n    flags = 1\n    data = length 42, hash 201AD367\n  sample 14:\n    time = 2036000\n    flags = 1\n    data = length 50, hash 606CCD6\n  sample 15:\n    time = 2038666\n    flags = 1\n    data = length 56, hash B15EBD7A\n  sample 16:\n    time = 2041333\n    flags = 1\n    data = length 212, hash 273B8D22\n  sample 17:\n    time = 2053333\n    flags = 1\n    data = length 194, hash 44F9CE1\n  sample 18:\n    time = 2074666\n    flags = 1\n    data = length 195, hash EDF9EBA1\n  sample 19:\n    time = 2096000\n    flags = 1\n    data = length 194, hash CE9F2D26\n  sample 20:\n    time = 2117333\n    flags = 1\n    data = length 192, hash 204F8A23\n  sample 21:\n    time = 2138666\n    flags = 1\n    data = length 206, hash DFA57E67\n  sample 22:\n    time = 2160000\n    flags = 1\n    data = length 196, hash 3CF084AB\n  sample 23:\n    time = 2181333\n    flags = 1\n    data = length 202, hash 2AF75C08\n  sample 24:\n    time = 2202666\n    flags = 1\n    data = length 203, hash 748EAF7\n  sample 25:\n    time = 2224000\n    flags = 1\n    data = length 205, hash ED82379D\n  sample 26:\n    time = 2245333\n    flags = 1\n    data = length 193, hash 61F26F22\n  sample 27:\n    time = 2266666\n    flags = 1\n    data = length 189, hash 85EF1D20\n  sample 28:\n    time = 2288000\n    flags = 1\n    data = length 187, hash 25E41FBF\n  sample 29:\n    time = 2309333\n    flags = 1\n    data = length 199, hash F365808\n  sample 30:\n    time = 2330666\n    flags = 1\n    data = length 197, hash 94205329\n  sample 31:\n    time = 2352000\n    flags = 1\n    data = length 201, hash FA2B2055\n  sample 32:\n    time = 2373333\n    flags = 1\n    data = length 194, hash AF95381F\n  sample 33:\n    time = 2394666\n    flags = 1\n    data = length 201, hash 923D3534\n  sample 34:\n    time = 2416000\n    flags = 1\n    data = length 198, hash 35F84C2E\n  sample 35:\n    time = 2437333\n    flags = 1\n    data = length 204, hash 6642CA40\n  sample 36:\n    time = 2458666\n    flags = 1\n    data = length 183, hash 3E2DC6BE\n  sample 37:\n    time = 2480000\n    flags = 1\n    data = length 197, hash B1E458CE\n  sample 38:\n    time = 2501333\n    flags = 1\n    data = length 193, hash E9218C84\n  sample 39:\n    time = 2522666\n    flags = 1\n    data = length 192, hash FEF08D4B\n  sample 40:\n    time = 2544000\n    flags = 1\n    data = length 201, hash FC411147\n  sample 41:\n    time = 2565333\n    flags = 1\n    data = length 218, hash 86893464\n  sample 42:\n    time = 2586666\n    flags = 1\n    data = length 226, hash 31C5320\n  sample 43:\n    time = 2608000\n    flags = 1\n    data = length 233, hash 9432BEE5\n  sample 44:\n    time = 2629333\n    flags = 1\n    data = length 213, hash B3FCC53E\n  sample 45:\n    time = 2650666\n    flags = 1\n    data = length 204, hash D70DD5A2\n  sample 46:\n    time = 2672000\n    flags = 1\n    data = length 212, hash A4EF1B69\n  sample 47:\n    time = 2693333\n    flags = 1\n    data = length 203, hash 8B0748B5\n  sample 48:\n    time = 2714666\n    flags = 1\n    data = length 149, hash E455335B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_vorbis.ogg.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 2741000\n  getPosition(0) = [[timeUs=0, position=3995]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 112000\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/vorbis\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 30, hash 9A8FF207\n      data = length 3832, hash 8A406249\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ogg/bear_vorbis.ogg.unklen.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 112000\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/vorbis\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 30, hash 9A8FF207\n      data = length 3832, hash 8A406249\n  total output bytes = 26873\n  sample count = 180\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 49, hash 2FFF94F0\n  sample 1:\n    time = 0\n    flags = 1\n    data = length 44, hash 3946418A\n  sample 2:\n    time = 2666\n    flags = 1\n    data = length 55, hash 2A0B878E\n  sample 3:\n    time = 5333\n    flags = 1\n    data = length 53, hash CC3B6879\n  sample 4:\n    time = 8000\n    flags = 1\n    data = length 215, hash 106AE950\n  sample 5:\n    time = 20000\n    flags = 1\n    data = length 192, hash 2B219F53\n  sample 6:\n    time = 41333\n    flags = 1\n    data = length 197, hash FBC39422\n  sample 7:\n    time = 62666\n    flags = 1\n    data = length 209, hash 386E8979\n  sample 8:\n    time = 84000\n    flags = 1\n    data = length 42, hash E81162C1\n  sample 9:\n    time = 96000\n    flags = 1\n    data = length 41, hash F15BEE36\n  sample 10:\n    time = 98666\n    flags = 1\n    data = length 42, hash D67EB19\n  sample 11:\n    time = 101333\n    flags = 1\n    data = length 42, hash F4DE4792\n  sample 12:\n    time = 104000\n    flags = 1\n    data = length 53, hash 80F66AC3\n  sample 13:\n    time = 106666\n    flags = 1\n    data = length 56, hash DCB9DFC4\n  sample 14:\n    time = 109333\n    flags = 1\n    data = length 55, hash 4E0C4E9D\n  sample 15:\n    time = 112000\n    flags = 1\n    data = length 203, hash 176B6862\n  sample 16:\n    time = 124000\n    flags = 1\n    data = length 193, hash AB13CB10\n  sample 17:\n    time = 145333\n    flags = 1\n    data = length 203, hash DE63DE9F\n  sample 18:\n    time = 166666\n    flags = 1\n    data = length 194, hash 4A9508A2\n  sample 19:\n    time = 188000\n    flags = 1\n    data = length 210, hash 196899B3\n  sample 20:\n    time = 209333\n    flags = 1\n    data = length 195, hash B68407F1\n  sample 21:\n    time = 230666\n    flags = 1\n    data = length 193, hash A1FA86E3\n  sample 22:\n    time = 252000\n    flags = 1\n    data = length 194, hash 5C0B9343\n  sample 23:\n    time = 273333\n    flags = 1\n    data = length 198, hash 789914B2\n  sample 24:\n    time = 294666\n    flags = 1\n    data = length 183, hash 1B82D11F\n  sample 25:\n    time = 316000\n    flags = 1\n    data = length 199, hash D5B848F4\n  sample 26:\n    time = 337333\n    flags = 1\n    data = length 192, hash B34427EA\n  sample 27:\n    time = 358666\n    flags = 1\n    data = length 199, hash C2599BB5\n  sample 28:\n    time = 380000\n    flags = 1\n    data = length 195, hash BFD83194\n  sample 29:\n    time = 401333\n    flags = 1\n    data = length 199, hash C9A7F7CA\n  sample 30:\n    time = 422666\n    flags = 1\n    data = length 44, hash 5D76EAD6\n  sample 31:\n    time = 434666\n    flags = 1\n    data = length 43, hash 8619C423\n  sample 32:\n    time = 437333\n    flags = 1\n    data = length 43, hash E490BBE\n  sample 33:\n    time = 440000\n    flags = 1\n    data = length 53, hash 8A557CAE\n  sample 34:\n    time = 442666\n    flags = 1\n    data = length 56, hash 81007BBA\n  sample 35:\n    time = 445333\n    flags = 1\n    data = length 56, hash 4E4DD67F\n  sample 36:\n    time = 448000\n    flags = 1\n    data = length 222, hash 414188AB\n  sample 37:\n    time = 460000\n    flags = 1\n    data = length 202, hash 67A07D30\n  sample 38:\n    time = 481333\n    flags = 1\n    data = length 200, hash E357D853\n  sample 39:\n    time = 502666\n    flags = 1\n    data = length 203, hash 4653DC90\n  sample 40:\n    time = 524000\n    flags = 1\n    data = length 192, hash A65E6C09\n  sample 41:\n    time = 545333\n    flags = 1\n    data = length 202, hash FBEAC508\n  sample 42:\n    time = 566666\n    flags = 1\n    data = length 202, hash E9B7B59F\n  sample 43:\n    time = 588000\n    flags = 1\n    data = length 204, hash E24AA78E\n  sample 44:\n    time = 609333\n    flags = 1\n    data = length 41, hash 3FBC5216\n  sample 45:\n    time = 621333\n    flags = 1\n    data = length 47, hash 153FBC55\n  sample 46:\n    time = 624000\n    flags = 1\n    data = length 42, hash 2B493D6C\n  sample 47:\n    time = 626666\n    flags = 1\n    data = length 42, hash 8303BEE3\n  sample 48:\n    time = 629333\n    flags = 1\n    data = length 62, hash 71AEE50B\n  sample 49:\n    time = 632000\n    flags = 1\n    data = length 54, hash 52F61908\n  sample 50:\n    time = 634666\n    flags = 1\n    data = length 45, hash 7BD3E3A1\n  sample 51:\n    time = 637333\n    flags = 1\n    data = length 41, hash E0F65472\n  sample 52:\n    time = 640000\n    flags = 1\n    data = length 45, hash 41838675\n  sample 53:\n    time = 642666\n    flags = 1\n    data = length 44, hash FCBC2147\n  sample 54:\n    time = 645333\n    flags = 1\n    data = length 45, hash 1A5987E3\n  sample 55:\n    time = 648000\n    flags = 1\n    data = length 43, hash 99074864\n  sample 56:\n    time = 650666\n    flags = 1\n    data = length 57, hash D4A9B60A\n  sample 57:\n    time = 653333\n    flags = 1\n    data = length 52, hash 302129DA\n  sample 58:\n    time = 656000\n    flags = 1\n    data = length 57, hash D8DD99C0\n  sample 59:\n    time = 658666\n    flags = 1\n    data = length 206, hash F4B9EF26\n  sample 60:\n    time = 670666\n    flags = 1\n    data = length 197, hash 7B8ACC8A\n  sample 61:\n    time = 692000\n    flags = 1\n    data = length 186, hash 161027CB\n  sample 62:\n    time = 713333\n    flags = 1\n    data = length 186, hash 1D6871B6\n  sample 63:\n    time = 734666\n    flags = 1\n    data = length 201, hash 536E9FDB\n  sample 64:\n    time = 756000\n    flags = 1\n    data = length 192, hash D38EFAC5\n  sample 65:\n    time = 777333\n    flags = 1\n    data = length 194, hash 4B394EF3\n  sample 66:\n    time = 798666\n    flags = 1\n    data = length 206, hash 1B31BA99\n  sample 67:\n    time = 820000\n    flags = 1\n    data = length 212, hash AD061F43\n  sample 68:\n    time = 841333\n    flags = 1\n    data = length 180, hash 6D1F7481\n  sample 69:\n    time = 862666\n    flags = 1\n    data = length 195, hash D80B21F\n  sample 70:\n    time = 884000\n    flags = 1\n    data = length 186, hash D367882\n  sample 71:\n    time = 905333\n    flags = 1\n    data = length 195, hash 2722159A\n  sample 72:\n    time = 926666\n    flags = 1\n    data = length 199, hash 10CEE97A\n  sample 73:\n    time = 948000\n    flags = 1\n    data = length 191, hash 2CF9FB3F\n  sample 74:\n    time = 969333\n    flags = 1\n    data = length 197, hash A725DA0\n  sample 75:\n    time = 990666\n    flags = 1\n    data = length 211, hash D4E5DB9E\n  sample 76:\n    time = 1012000\n    flags = 1\n    data = length 189, hash 1A90F496\n  sample 77:\n    time = 1033333\n    flags = 1\n    data = length 187, hash 44DB2689\n  sample 78:\n    time = 1054666\n    flags = 1\n    data = length 197, hash 6D3E5117\n  sample 79:\n    time = 1076000\n    flags = 1\n    data = length 208, hash 5B57B288\n  sample 80:\n    time = 1097333\n    flags = 1\n    data = length 198, hash D5FC05\n  sample 81:\n    time = 1118666\n    flags = 1\n    data = length 192, hash 350BBA45\n  sample 82:\n    time = 1140000\n    flags = 1\n    data = length 195, hash 5F96F2A8\n  sample 83:\n    time = 1161333\n    flags = 1\n    data = length 202, hash 61D7CC33\n  sample 84:\n    time = 1182666\n    flags = 1\n    data = length 202, hash 49D335F2\n  sample 85:\n    time = 1204000\n    flags = 1\n    data = length 192, hash 2FE9CB1A\n  sample 86:\n    time = 1225333\n    flags = 1\n    data = length 201, hash BF0763B2\n  sample 87:\n    time = 1246666\n    flags = 1\n    data = length 184, hash AD047421\n  sample 88:\n    time = 1268000\n    flags = 1\n    data = length 196, hash F9088F14\n  sample 89:\n    time = 1289333\n    flags = 1\n    data = length 190, hash AC6D38FD\n  sample 90:\n    time = 1310666\n    flags = 1\n    data = length 195, hash 8D1A66D2\n  sample 91:\n    time = 1332000\n    flags = 1\n    data = length 197, hash B46BFB6B\n  sample 92:\n    time = 1353333\n    flags = 1\n    data = length 195, hash D9761F23\n  sample 93:\n    time = 1374666\n    flags = 1\n    data = length 204, hash 3391B617\n  sample 94:\n    time = 1396000\n    flags = 1\n    data = length 42, hash 33A1FB52\n  sample 95:\n    time = 1408000\n    flags = 1\n    data = length 44, hash 408B146E\n  sample 96:\n    time = 1410666\n    flags = 1\n    data = length 44, hash 171C7E0D\n  sample 97:\n    time = 1413333\n    flags = 1\n    data = length 54, hash 6307E16C\n  sample 98:\n    time = 1416000\n    flags = 1\n    data = length 53, hash 4A319572\n  sample 99:\n    time = 1418666\n    flags = 1\n    data = length 215, hash BA9C445C\n  sample 100:\n    time = 1430666\n    flags = 1\n    data = length 201, hash 3120D234\n  sample 101:\n    time = 1452000\n    flags = 1\n    data = length 187, hash DB44993C\n  sample 102:\n    time = 1473333\n    flags = 1\n    data = length 196, hash CF2002D7\n  sample 103:\n    time = 1494666\n    flags = 1\n    data = length 185, hash E03B5D7\n  sample 104:\n    time = 1516000\n    flags = 1\n    data = length 187, hash DA399A2C\n  sample 105:\n    time = 1537333\n    flags = 1\n    data = length 191, hash 292AA0DB\n  sample 106:\n    time = 1558666\n    flags = 1\n    data = length 201, hash 221910E0\n  sample 107:\n    time = 1580000\n    flags = 1\n    data = length 194, hash F4ED7821\n  sample 108:\n    time = 1601333\n    flags = 1\n    data = length 43, hash FDDA515E\n  sample 109:\n    time = 1613333\n    flags = 1\n    data = length 42, hash F3571C0A\n  sample 110:\n    time = 1616000\n    flags = 1\n    data = length 38, hash 39F910B3\n  sample 111:\n    time = 1618666\n    flags = 1\n    data = length 41, hash 2D189531\n  sample 112:\n    time = 1621333\n    flags = 1\n    data = length 43, hash 1F7574DB\n  sample 113:\n    time = 1624000\n    flags = 1\n    data = length 43, hash 644D15E5\n  sample 114:\n    time = 1626666\n    flags = 1\n    data = length 49, hash E8A0878\n  sample 115:\n    time = 1629333\n    flags = 1\n    data = length 55, hash DFF2046D\n  sample 116:\n    time = 1632000\n    flags = 1\n    data = length 49, hash 9FB8A23\n  sample 117:\n    time = 1634666\n    flags = 1\n    data = length 41, hash E3E15E3B\n  sample 118:\n    time = 1637333\n    flags = 1\n    data = length 42, hash E5D17A32\n  sample 119:\n    time = 1640000\n    flags = 1\n    data = length 42, hash F308B653\n  sample 120:\n    time = 1642666\n    flags = 1\n    data = length 55, hash BB750D76\n  sample 121:\n    time = 1645333\n    flags = 1\n    data = length 51, hash 96772ABF\n  sample 122:\n    time = 1648000\n    flags = 1\n    data = length 197, hash E4524346\n  sample 123:\n    time = 1660000\n    flags = 1\n    data = length 188, hash AC3E1BB5\n  sample 124:\n    time = 1681333\n    flags = 1\n    data = length 195, hash F56DB8A5\n  sample 125:\n    time = 1702666\n    flags = 1\n    data = length 198, hash C8970FF7\n  sample 126:\n    time = 1724000\n    flags = 1\n    data = length 202, hash AF425C68\n  sample 127:\n    time = 1745333\n    flags = 1\n    data = length 196, hash 4215D839\n  sample 128:\n    time = 1766666\n    flags = 1\n    data = length 204, hash DB9BE8E3\n  sample 129:\n    time = 1788000\n    flags = 1\n    data = length 206, hash E5B20AB8\n  sample 130:\n    time = 1809333\n    flags = 1\n    data = length 209, hash D7F47B95\n  sample 131:\n    time = 1830666\n    flags = 1\n    data = length 193, hash FB54FB05\n  sample 132:\n    time = 1852000\n    flags = 1\n    data = length 199, hash D99C3106\n  sample 133:\n    time = 1873333\n    flags = 1\n    data = length 206, hash 253885B9\n  sample 134:\n    time = 1894666\n    flags = 1\n    data = length 191, hash FBDD8162\n  sample 135:\n    time = 1916000\n    flags = 1\n    data = length 183, hash 7290332F\n  sample 136:\n    time = 1937333\n    flags = 1\n    data = length 189, hash 1A9DC3DE\n  sample 137:\n    time = 1958666\n    flags = 1\n    data = length 201, hash 5D936764\n  sample 138:\n    time = 1980000\n    flags = 1\n    data = length 193, hash 6B03E75E\n  sample 139:\n    time = 2001333\n    flags = 1\n    data = length 199, hash 8A21BA83\n  sample 140:\n    time = 2022666\n    flags = 1\n    data = length 41, hash E6362210\n  sample 141:\n    time = 2034666\n    flags = 1\n    data = length 43, hash 36A57B44\n  sample 142:\n    time = 2037333\n    flags = 1\n    data = length 43, hash E51797D5\n  sample 143:\n    time = 2040000\n    flags = 1\n    data = length 43, hash 1F336C72\n  sample 144:\n    time = 2042666\n    flags = 1\n    data = length 42, hash 201AD367\n  sample 145:\n    time = 2045333\n    flags = 1\n    data = length 50, hash 606CCD6\n  sample 146:\n    time = 2048000\n    flags = 1\n    data = length 56, hash B15EBD7A\n  sample 147:\n    time = 2050666\n    flags = 1\n    data = length 212, hash 273B8D22\n  sample 148:\n    time = 2062666\n    flags = 1\n    data = length 194, hash 44F9CE1\n  sample 149:\n    time = 2084000\n    flags = 1\n    data = length 195, hash EDF9EBA1\n  sample 150:\n    time = 2105333\n    flags = 1\n    data = length 194, hash CE9F2D26\n  sample 151:\n    time = 2126666\n    flags = 1\n    data = length 192, hash 204F8A23\n  sample 152:\n    time = 2148000\n    flags = 1\n    data = length 206, hash DFA57E67\n  sample 153:\n    time = 2169333\n    flags = 1\n    data = length 196, hash 3CF084AB\n  sample 154:\n    time = 2190666\n    flags = 1\n    data = length 202, hash 2AF75C08\n  sample 155:\n    time = 2212000\n    flags = 1\n    data = length 203, hash 748EAF7\n  sample 156:\n    time = 2233333\n    flags = 1\n    data = length 205, hash ED82379D\n  sample 157:\n    time = 2254666\n    flags = 1\n    data = length 193, hash 61F26F22\n  sample 158:\n    time = 2276000\n    flags = 1\n    data = length 189, hash 85EF1D20\n  sample 159:\n    time = 2297333\n    flags = 1\n    data = length 187, hash 25E41FBF\n  sample 160:\n    time = 2318666\n    flags = 1\n    data = length 199, hash F365808\n  sample 161:\n    time = 2340000\n    flags = 1\n    data = length 197, hash 94205329\n  sample 162:\n    time = 2361333\n    flags = 1\n    data = length 201, hash FA2B2055\n  sample 163:\n    time = 2382666\n    flags = 1\n    data = length 194, hash AF95381F\n  sample 164:\n    time = 2404000\n    flags = 1\n    data = length 201, hash 923D3534\n  sample 165:\n    time = 2425333\n    flags = 1\n    data = length 198, hash 35F84C2E\n  sample 166:\n    time = 2446666\n    flags = 1\n    data = length 204, hash 6642CA40\n  sample 167:\n    time = 2468000\n    flags = 1\n    data = length 183, hash 3E2DC6BE\n  sample 168:\n    time = 2489333\n    flags = 1\n    data = length 197, hash B1E458CE\n  sample 169:\n    time = 2510666\n    flags = 1\n    data = length 193, hash E9218C84\n  sample 170:\n    time = 2532000\n    flags = 1\n    data = length 192, hash FEF08D4B\n  sample 171:\n    time = 2553333\n    flags = 1\n    data = length 201, hash FC411147\n  sample 172:\n    time = 2574666\n    flags = 1\n    data = length 218, hash 86893464\n  sample 173:\n    time = 2596000\n    flags = 1\n    data = length 226, hash 31C5320\n  sample 174:\n    time = 2617333\n    flags = 1\n    data = length 233, hash 9432BEE5\n  sample 175:\n    time = 2638666\n    flags = 1\n    data = length 213, hash B3FCC53E\n  sample 176:\n    time = 2660000\n    flags = 1\n    data = length 204, hash D70DD5A2\n  sample 177:\n    time = 2681333\n    flags = 1\n    data = length 212, hash A4EF1B69\n  sample 178:\n    time = 2702666\n    flags = 1\n    data = length 203, hash 8B0748B5\n  sample 179:\n    time = 2724000\n    flags = 1\n    data = length 149, hash E455335B\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/rawcc/sample.rawcc.0.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = null\n    containerMimeType = null\n    sampleMimeType = application/cea-608\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 978\n  sample count = 150\n  sample 0:\n    time = 37657512133\n    flags = 1\n    data = length 3, hash 7363\n  sample 1:\n    time = 37657528822\n    flags = 1\n    data = length 3, hash 7724\n  sample 2:\n    time = 37657545511\n    flags = 1\n    data = length 3, hash 766F\n  sample 3:\n    time = 37657562177\n    flags = 1\n    data = length 3, hash 7724\n  sample 4:\n    time = 37657578866\n    flags = 1\n    data = length 3, hash 767E\n  sample 5:\n    time = 37657595555\n    flags = 1\n    data = length 3, hash 7724\n  sample 6:\n    time = 37657612244\n    flags = 1\n    data = length 15, hash E4359178\n  sample 7:\n    time = 37657628911\n    flags = 1\n    data = length 3, hash 7724\n  sample 8:\n    time = 37657645600\n    flags = 1\n    data = length 12, hash 15EBEB66\n  sample 9:\n    time = 37657662288\n    flags = 1\n    data = length 3, hash 7724\n  sample 10:\n    time = 37657678977\n    flags = 1\n    data = length 3, hash 761D\n  sample 11:\n    time = 37657695644\n    flags = 1\n    data = length 3, hash 7724\n  sample 12:\n    time = 37657712333\n    flags = 1\n    data = length 30, hash E181418F\n  sample 13:\n    time = 37657729022\n    flags = 1\n    data = length 6, hash 36289CE2\n  sample 14:\n    time = 37657745711\n    flags = 1\n    data = length 12, hash 3C304F5B\n  sample 15:\n    time = 37657762377\n    flags = 1\n    data = length 3, hash 7724\n  sample 16:\n    time = 37657779066\n    flags = 1\n    data = length 12, hash 88DD8EF6\n  sample 17:\n    time = 37657795755\n    flags = 1\n    data = length 3, hash 7724\n  sample 18:\n    time = 37657812444\n    flags = 1\n    data = length 12, hash 8B411833\n  sample 19:\n    time = 37657829111\n    flags = 1\n    data = length 3, hash 7724\n  sample 20:\n    time = 37657845800\n    flags = 1\n    data = length 12, hash 742A2DF1\n  sample 21:\n    time = 37657862488\n    flags = 1\n    data = length 3, hash 7724\n  sample 22:\n    time = 37657879177\n    flags = 1\n    data = length 12, hash 9A2ECBEE\n  sample 23:\n    time = 37657895844\n    flags = 1\n    data = length 3, hash 7724\n  sample 24:\n    time = 37657912533\n    flags = 1\n    data = length 12, hash 562688EA\n  sample 25:\n    time = 37657929222\n    flags = 1\n    data = length 3, hash 7724\n  sample 26:\n    time = 37657945911\n    flags = 1\n    data = length 12, hash ADE4B953\n  sample 27:\n    time = 37657962577\n    flags = 1\n    data = length 3, hash 7724\n  sample 28:\n    time = 37657979266\n    flags = 1\n    data = length 12, hash F927E3E5\n  sample 29:\n    time = 37657995955\n    flags = 1\n    data = length 3, hash 7724\n  sample 30:\n    time = 37658012644\n    flags = 1\n    data = length 12, hash EA327945\n  sample 31:\n    time = 37658029311\n    flags = 1\n    data = length 3, hash 7724\n  sample 32:\n    time = 37658046000\n    flags = 1\n    data = length 12, hash 3E5DA13C\n  sample 33:\n    time = 37658062688\n    flags = 1\n    data = length 3, hash 7724\n  sample 34:\n    time = 37658079377\n    flags = 1\n    data = length 12, hash BF646AE3\n  sample 35:\n    time = 37658096044\n    flags = 1\n    data = length 3, hash 7724\n  sample 36:\n    time = 37658112733\n    flags = 1\n    data = length 12, hash 41E3BA78\n  sample 37:\n    time = 37658129422\n    flags = 1\n    data = length 3, hash 7724\n  sample 38:\n    time = 37658146111\n    flags = 1\n    data = length 12, hash A2945EF6\n  sample 39:\n    time = 37658162777\n    flags = 1\n    data = length 3, hash 7724\n  sample 40:\n    time = 37658179466\n    flags = 1\n    data = length 12, hash 26735812\n  sample 41:\n    time = 37658196155\n    flags = 1\n    data = length 3, hash 7724\n  sample 42:\n    time = 37658212844\n    flags = 1\n    data = length 12, hash DC14D3D8\n  sample 43:\n    time = 37658229511\n    flags = 1\n    data = length 3, hash 7724\n  sample 44:\n    time = 37658246200\n    flags = 1\n    data = length 12, hash 882191BE\n  sample 45:\n    time = 37658262888\n    flags = 1\n    data = length 3, hash 7724\n  sample 46:\n    time = 37658279577\n    flags = 1\n    data = length 12, hash 8B4886B1\n  sample 47:\n    time = 37658296244\n    flags = 1\n    data = length 3, hash 7724\n  sample 48:\n    time = 37658312933\n    flags = 1\n    data = length 12, hash 98D98F96\n  sample 49:\n    time = 37658329622\n    flags = 1\n    data = length 3, hash 7724\n  sample 50:\n    time = 37658346311\n    flags = 1\n    data = length 30, hash CF8E53E3\n  sample 51:\n    time = 37658362977\n    flags = 1\n    data = length 6, hash 36289CE2\n  sample 52:\n    time = 37658379666\n    flags = 1\n    data = length 12, hash F883C9EE\n  sample 53:\n    time = 37658396355\n    flags = 1\n    data = length 3, hash 7724\n  sample 54:\n    time = 37658413044\n    flags = 1\n    data = length 12, hash 6E6B2B9C\n  sample 55:\n    time = 37658429711\n    flags = 1\n    data = length 3, hash 7724\n  sample 56:\n    time = 37658446400\n    flags = 1\n    data = length 12, hash B4FE7F08\n  sample 57:\n    time = 37658463088\n    flags = 1\n    data = length 3, hash 7724\n  sample 58:\n    time = 37658479777\n    flags = 1\n    data = length 12, hash 5A1EA7C7\n  sample 59:\n    time = 37658496444\n    flags = 1\n    data = length 3, hash 7724\n  sample 60:\n    time = 37658513133\n    flags = 1\n    data = length 12, hash 46BD6CC9\n  sample 61:\n    time = 37658529822\n    flags = 1\n    data = length 3, hash 7724\n  sample 62:\n    time = 37658546511\n    flags = 1\n    data = length 12, hash 1B1E2554\n  sample 63:\n    time = 37658563177\n    flags = 1\n    data = length 3, hash 7724\n  sample 64:\n    time = 37658579866\n    flags = 1\n    data = length 12, hash 91FCC537\n  sample 65:\n    time = 37658596555\n    flags = 1\n    data = length 3, hash 7724\n  sample 66:\n    time = 37658613244\n    flags = 1\n    data = length 12, hash A9355E1B\n  sample 67:\n    time = 37658629911\n    flags = 1\n    data = length 3, hash 7724\n  sample 68:\n    time = 37658646600\n    flags = 1\n    data = length 12, hash 2511F69B\n  sample 69:\n    time = 37658663288\n    flags = 1\n    data = length 3, hash 7724\n  sample 70:\n    time = 37658679977\n    flags = 1\n    data = length 12, hash 90925736\n  sample 71:\n    time = 37658696644\n    flags = 1\n    data = length 3, hash 7724\n  sample 72:\n    time = 37658713333\n    flags = 1\n    data = length 21, hash 431EEE30\n  sample 73:\n    time = 37658730022\n    flags = 1\n    data = length 3, hash 7724\n  sample 74:\n    time = 37658746711\n    flags = 1\n    data = length 12, hash 7BDEF631\n  sample 75:\n    time = 37658763377\n    flags = 1\n    data = length 3, hash 7724\n  sample 76:\n    time = 37658780066\n    flags = 1\n    data = length 12, hash A2EEF59E\n  sample 77:\n    time = 37658796755\n    flags = 1\n    data = length 3, hash 7724\n  sample 78:\n    time = 37658813444\n    flags = 1\n    data = length 12, hash BFC6C022\n  sample 79:\n    time = 37658830111\n    flags = 1\n    data = length 3, hash 7724\n  sample 80:\n    time = 37658846800\n    flags = 1\n    data = length 12, hash CD4D8FCA\n  sample 81:\n    time = 37658863488\n    flags = 1\n    data = length 3, hash 7724\n  sample 82:\n    time = 37658880177\n    flags = 1\n    data = length 12, hash 2BDE8EFA\n  sample 83:\n    time = 37658896844\n    flags = 1\n    data = length 3, hash 7724\n  sample 84:\n    time = 37658913533\n    flags = 1\n    data = length 12, hash 8C858812\n  sample 85:\n    time = 37658930222\n    flags = 1\n    data = length 3, hash 7724\n  sample 86:\n    time = 37658946911\n    flags = 1\n    data = length 12, hash DE7D0E31\n  sample 87:\n    time = 37658963577\n    flags = 1\n    data = length 3, hash 7724\n  sample 88:\n    time = 37658980266\n    flags = 1\n    data = length 3, hash 7363\n  sample 89:\n    time = 37658996955\n    flags = 1\n    data = length 3, hash 7724\n  sample 90:\n    time = 37659013644\n    flags = 1\n    data = length 3, hash 7363\n  sample 91:\n    time = 37659030311\n    flags = 1\n    data = length 3, hash 7724\n  sample 92:\n    time = 37659047000\n    flags = 1\n    data = length 3, hash 7363\n  sample 93:\n    time = 37659063688\n    flags = 1\n    data = length 3, hash 7724\n  sample 94:\n    time = 37659080377\n    flags = 1\n    data = length 3, hash 7363\n  sample 95:\n    time = 37659097044\n    flags = 1\n    data = length 3, hash 7724\n  sample 96:\n    time = 37659113733\n    flags = 1\n    data = length 3, hash 7363\n  sample 97:\n    time = 37659130422\n    flags = 1\n    data = length 3, hash 7724\n  sample 98:\n    time = 37659147111\n    flags = 1\n    data = length 3, hash 7363\n  sample 99:\n    time = 37659163777\n    flags = 1\n    data = length 3, hash 7724\n  sample 100:\n    time = 37659180466\n    flags = 1\n    data = length 3, hash 7363\n  sample 101:\n    time = 37659197155\n    flags = 1\n    data = length 3, hash 7724\n  sample 102:\n    time = 37659213844\n    flags = 1\n    data = length 3, hash 7363\n  sample 103:\n    time = 37659230511\n    flags = 1\n    data = length 3, hash 7724\n  sample 104:\n    time = 37659247200\n    flags = 1\n    data = length 3, hash 7363\n  sample 105:\n    time = 37659263888\n    flags = 1\n    data = length 3, hash 7724\n  sample 106:\n    time = 37659280577\n    flags = 1\n    data = length 3, hash 7363\n  sample 107:\n    time = 37659297244\n    flags = 1\n    data = length 3, hash 7724\n  sample 108:\n    time = 37659313933\n    flags = 1\n    data = length 3, hash 7363\n  sample 109:\n    time = 37659330622\n    flags = 1\n    data = length 3, hash 7724\n  sample 110:\n    time = 37659347311\n    flags = 1\n    data = length 3, hash 7363\n  sample 111:\n    time = 37659363977\n    flags = 1\n    data = length 3, hash 7724\n  sample 112:\n    time = 37659380666\n    flags = 1\n    data = length 3, hash 7363\n  sample 113:\n    time = 37659397355\n    flags = 1\n    data = length 3, hash 7724\n  sample 114:\n    time = 37659414044\n    flags = 1\n    data = length 3, hash 7363\n  sample 115:\n    time = 37659430711\n    flags = 1\n    data = length 3, hash 7724\n  sample 116:\n    time = 37659447400\n    flags = 1\n    data = length 3, hash 7363\n  sample 117:\n    time = 37659464088\n    flags = 1\n    data = length 3, hash 7724\n  sample 118:\n    time = 37659480777\n    flags = 1\n    data = length 3, hash 7363\n  sample 119:\n    time = 37659497444\n    flags = 1\n    data = length 3, hash 7724\n  sample 120:\n    time = 37659514133\n    flags = 1\n    data = length 3, hash 7363\n  sample 121:\n    time = 37659530822\n    flags = 1\n    data = length 3, hash 7724\n  sample 122:\n    time = 37659547511\n    flags = 1\n    data = length 3, hash 7363\n  sample 123:\n    time = 37659564177\n    flags = 1\n    data = length 3, hash 7724\n  sample 124:\n    time = 37659580866\n    flags = 1\n    data = length 3, hash 7363\n  sample 125:\n    time = 37659597555\n    flags = 1\n    data = length 3, hash 7724\n  sample 126:\n    time = 37659614244\n    flags = 1\n    data = length 3, hash 766F\n  sample 127:\n    time = 37659630911\n    flags = 1\n    data = length 3, hash 7724\n  sample 128:\n    time = 37659647600\n    flags = 1\n    data = length 3, hash 767E\n  sample 129:\n    time = 37659664288\n    flags = 1\n    data = length 3, hash 7724\n  sample 130:\n    time = 37659680977\n    flags = 1\n    data = length 15, hash 191B585A\n  sample 131:\n    time = 37659697644\n    flags = 1\n    data = length 3, hash 7724\n  sample 132:\n    time = 37659714333\n    flags = 1\n    data = length 12, hash 15EC5FC5\n  sample 133:\n    time = 37659731022\n    flags = 1\n    data = length 3, hash 7724\n  sample 134:\n    time = 37659747711\n    flags = 1\n    data = length 3, hash 76A1\n  sample 135:\n    time = 37659764377\n    flags = 1\n    data = length 3, hash 7724\n  sample 136:\n    time = 37659781066\n    flags = 1\n    data = length 30, hash E8012479\n  sample 137:\n    time = 37659797755\n    flags = 1\n    data = length 6, hash 36289D5E\n  sample 138:\n    time = 37659814444\n    flags = 1\n    data = length 12, hash D32F29F3\n  sample 139:\n    time = 37659831111\n    flags = 1\n    data = length 3, hash 7724\n  sample 140:\n    time = 37659847800\n    flags = 1\n    data = length 21, hash 6258623\n  sample 141:\n    time = 37659864488\n    flags = 1\n    data = length 3, hash 7724\n  sample 142:\n    time = 37659881177\n    flags = 1\n    data = length 12, hash FE69ABA2\n  sample 143:\n    time = 37659897844\n    flags = 1\n    data = length 3, hash 7724\n  sample 144:\n    time = 37659914533\n    flags = 1\n    data = length 12, hash 958D0815\n  sample 145:\n    time = 37659931222\n    flags = 1\n    data = length 3, hash 7724\n  sample 146:\n    time = 37659947911\n    flags = 1\n    data = length 12, hash FF57BFD8\n  sample 147:\n    time = 37659964577\n    flags = 1\n    data = length 3, hash 7724\n  sample 148:\n    time = 37659981266\n    flags = 1\n    data = length 12, hash 922122E7\n  sample 149:\n    time = 37659997955\n    flags = 1\n    data = length 3, hash 7724\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ssa/empty",
    "content": ""
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ssa/invalid_timecodes",
    "content": "[Script Info]\nTitle: SomeTitle\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, Text\nDialogue: 0,Invalid,0:00:01.23,Default,Olly,This is the first subtitle{ignored}.\nDialogue: 0,0:00:02.34,Invalid,Default,Olly,This is the second subtitle \\nwith a newline \\Nand another.\nDialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ssa/no_end_timecodes",
    "content": "[Script Info]\nTitle: SomeTitle\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, Text\nDialogue: 0,0:00:00.00,          ,Default,Olly,This is the first subtitle.\nDialogue: 0,0:00:02.34,          ,Default,Olly,This is the second subtitle \\nwith a newline \\Nand another.\nDialogue: 0,0:00:04.56,          ,Default,Olly,This is the third subtitle, with a comma.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ssa/typical",
    "content": "[Script Info]\nTitle: SomeTitle\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, Text\nDialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,This is the first subtitle{ignored}.\nDialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,This is the second subtitle \\nwith a newline \\Nand another.\nDialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ssa/typical_dialogue",
    "content": "Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,This is the first subtitle{ignored}.\nDialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,This is the second subtitle \\nwith a newline \\Nand another.\nDialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ssa/typical_format",
    "content": "Format: Layer, Start, End, Style, Name, Text\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ssa/typical_header",
    "content": "[Script Info]\nTitle: SomeTitle\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/subrip/empty",
    "content": ""
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/subrip/no_end_timecodes",
    "content": "1\n00:00:00,000 -->\nSubRip doesn't technically allow missing end timecodes.\n\n2\n00:00:02,345 -->\nWe interpret it to mean that a subtitle extends to the start of the next one.\n\n3\n00:00:03,456 -->\nOr to the end of the media.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/subrip/typical",
    "content": "1\n00:00:00,000 --> 00:00:01,234\nThis is the first subtitle.\n\n2\n00:00:02,345 --> 00:00:03,456\nThis is the second subtitle.\nSecond subtitle with second line.\n\n3\n00:00:04,567 --> 00:00:08,901\nThis is the third subtitle.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/subrip/typical_extra_blank_line",
    "content": "1\n00:00:00,000 --> 00:00:01,234\nThis is the first subtitle.\n\n\n2\n00:00:02,345 --> 00:00:03,456\nThis is the second subtitle.\nSecond subtitle with second line.\n\n3\n00:00:04,567 --> 00:00:08,901\nThis is the third subtitle.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/subrip/typical_missing_sequence",
    "content": "1\n00:00:00,000 --> 00:00:01,234\nThis is the first subtitle.\n\n00:00:02,345 --> 00:00:03,456\nThis is the second subtitle.\nSecond subtitle with second line.\n\n3\n00:00:04,567 --> 00:00:08,901\nThis is the third subtitle.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/subrip/typical_missing_timecode",
    "content": "1\n00:00:00,000 --> 00:00:01,234\nThis is the first subtitle.\n\n2\nThis is the second subtitle.\nSecond subtitle with second line.\n\n3\n00:00:04,567 --> 00:00:08,901\nThis is the third subtitle.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/subrip/typical_negative_timestamps",
    "content": "1\n-0:00:04,567 --> -0:00:03,456\nThis is the first subtitle.\n\n2\n-00:00:02,345 --> 00:00:01,234\nThis is the second subtitle.\nSecond subtitle with second line.\n\n3\n00:00:04,567 --> 00:00:08,901\nThis is the third subtitle.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/subrip/typical_unexpected_end",
    "content": "1\n00:00:00,000 --> 00:00:01,234\nThis is the first subtitle.\n\n2\n00:00:02,345 --> 00:00:03,456\nThis is the second subtitle.\nSecond subtitle with second line.\n\n3\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/subrip/typical_with_byte_order_mark",
    "content": "﻿1\n00:00:00,000 --> 00:00:01,234\nThis is the first subtitle.\n\n2\n00:00:02,345 --> 00:00:03,456\nThis is the second subtitle.\nSecond subtitle with second line.\n\n3\n00:00:04,567 --> 00:00:08,901\nThis is the third subtitle.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/subrip/typical_with_tags",
    "content": "1\n00:00:00,000 --> 00:00:01,234\nThis is {\\an1} the first subtitle.\n\n2\n00:00:02,345 --> 00:00:03,456\nThis is the second subtitle.\nSecond {\\ an 2} subtitle with second line.\n\n3\n00:00:04,567 --> 00:00:08,901\nThis {\\an2} is the third {\\ tag} subtitle.\n\n4\n00:00:09,567 --> 00:00:12,901\nThis { \\an2} is not a valid tag due to the space after the opening bracket.\n\n5\n00:00:013,567 --> 00:00:14,901\nThis {\\an2} is the fifth subtitle with multiple {\\xyz} valid {\\qwe} tags.\n\n6\n00:00:015,567 --> 00:00:15,901\nThis {\\an1} is a lines.\n\n7\n00:00:016,567 --> 00:00:16,901\nThis {\\an2} is a line.\n\n8\n00:00:017,567 --> 00:00:17,901\nThis {\\an3} is a line.\n\n9\n00:00:018,567 --> 00:00:18,901\nThis {\\an4} is a line.\n\n10\n00:00:019,567 --> 00:00:19,901\nThis {\\an5} is a line.\n\n11\n00:00:020,567 --> 00:00:20,901\nThis {\\an6} is a line.\n\n12\n00:00:021,567 --> 00:00:22,901\nThis {\\an7} is a line.\n\n13\n00:00:023,567 --> 00:00:23,901\nThis {\\an8} is a line.\n\n14\n00:00:024,567 --> 00:00:24,901\nThis {\\an9} is a line.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.ac3.0.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = 0\n    containerMimeType = null\n    sampleMimeType = audio/ac3\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 6\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 13281\n  sample count = 8\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 1536, hash 7108D5C2\n  sample 1:\n    time = 32000\n    flags = 1\n    data = length 1536, hash 80BF3B34\n  sample 2:\n    time = 64000\n    flags = 1\n    data = length 1536, hash 5D09685\n  sample 3:\n    time = 96000\n    flags = 1\n    data = length 1536, hash A9A24E44\n  sample 4:\n    time = 128000\n    flags = 1\n    data = length 1536, hash 6F856273\n  sample 5:\n    time = 160000\n    flags = 1\n    data = length 1536, hash B1737D3C\n  sample 6:\n    time = 192000\n    flags = 1\n    data = length 1536, hash 98FDEB9D\n  sample 7:\n    time = 224000\n    flags = 1\n    data = length 1536, hash 99B9B943\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.ac4.0.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = 0\n    containerMimeType = null\n    sampleMimeType = audio/ac4\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 2\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 7594\n  sample count = 19\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 366, hash B4277F9E\n  sample 1:\n    time = 40000\n    flags = 1\n    data = length 366, hash E8E0A142\n  sample 2:\n    time = 80000\n    flags = 1\n    data = length 366, hash 2E5073D0\n  sample 3:\n    time = 120000\n    flags = 1\n    data = length 366, hash 850E71D8\n  sample 4:\n    time = 160000\n    flags = 1\n    data = length 366, hash 69CD444E\n  sample 5:\n    time = 200000\n    flags = 1\n    data = length 366, hash BD24F36D\n  sample 6:\n    time = 240000\n    flags = 1\n    data = length 366, hash E24F2490\n  sample 7:\n    time = 280000\n    flags = 1\n    data = length 366, hash EE6F1F06\n  sample 8:\n    time = 320000\n    flags = 1\n    data = length 366, hash 2DAB000F\n  sample 9:\n    time = 360000\n    flags = 1\n    data = length 366, hash 8102B7EC\n  sample 10:\n    time = 400000\n    flags = 1\n    data = length 366, hash 55BF59AC\n  sample 11:\n    time = 440000\n    flags = 1\n    data = length 494, hash CBC2E09F\n  sample 12:\n    time = 480000\n    flags = 1\n    data = length 519, hash 9DAF56E9\n  sample 13:\n    time = 520000\n    flags = 1\n    data = length 598, hash 8169EE2\n  sample 14:\n    time = 560000\n    flags = 1\n    data = length 435, hash 28C21246\n  sample 15:\n    time = 600000\n    flags = 1\n    data = length 365, hash FF14716D\n  sample 16:\n    time = 640000\n    flags = 1\n    data = length 392, hash 4CC96B29\n  sample 17:\n    time = 680000\n    flags = 1\n    data = length 373, hash D7AC6D4E\n  sample 18:\n    time = 720000\n    flags = 1\n    data = length 392, hash 99F2511F\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.adts.0.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 0\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 2, hash 5F7\n  total output bytes = 30797\n  sample count = 144\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 23, hash 47DE9131\n  sample 1:\n    time = 23219\n    flags = 1\n    data = length 6, hash 31CF3A46\n  sample 2:\n    time = 46438\n    flags = 1\n    data = length 6, hash 31CF3A46\n  sample 3:\n    time = 69657\n    flags = 1\n    data = length 6, hash 31CF3A46\n  sample 4:\n    time = 92876\n    flags = 1\n    data = length 6, hash 31EC5206\n  sample 5:\n    time = 116095\n    flags = 1\n    data = length 171, hash 4F6478F6\n  sample 6:\n    time = 139314\n    flags = 1\n    data = length 202, hash AF4068A3\n  sample 7:\n    time = 162533\n    flags = 1\n    data = length 210, hash E4C10618\n  sample 8:\n    time = 185752\n    flags = 1\n    data = length 217, hash 9ECCD0D9\n  sample 9:\n    time = 208971\n    flags = 1\n    data = length 212, hash 6BAC2CD9\n  sample 10:\n    time = 232190\n    flags = 1\n    data = length 223, hash 188B6010\n  sample 11:\n    time = 255409\n    flags = 1\n    data = length 222, hash C1A04D0C\n  sample 12:\n    time = 278628\n    flags = 1\n    data = length 220, hash D65F9768\n  sample 13:\n    time = 301847\n    flags = 1\n    data = length 227, hash B96C9E14\n  sample 14:\n    time = 325066\n    flags = 1\n    data = length 229, hash 9FB09972\n  sample 15:\n    time = 348285\n    flags = 1\n    data = length 220, hash 2271F053\n  sample 16:\n    time = 371504\n    flags = 1\n    data = length 226, hash 5EDD2F4F\n  sample 17:\n    time = 394723\n    flags = 1\n    data = length 239, hash 957510E0\n  sample 18:\n    time = 417942\n    flags = 1\n    data = length 224, hash 718A8F47\n  sample 19:\n    time = 441161\n    flags = 1\n    data = length 225, hash 5E11E293\n  sample 20:\n    time = 464380\n    flags = 1\n    data = length 227, hash FCE50D27\n  sample 21:\n    time = 487599\n    flags = 1\n    data = length 212, hash 77908C40\n  sample 22:\n    time = 510818\n    flags = 1\n    data = length 227, hash 34C4EB32\n  sample 23:\n    time = 534037\n    flags = 1\n    data = length 231, hash 95488307\n  sample 24:\n    time = 557256\n    flags = 1\n    data = length 226, hash 97F12D6F\n  sample 25:\n    time = 580475\n    flags = 1\n    data = length 236, hash 91A9D9A2\n  sample 26:\n    time = 603694\n    flags = 1\n    data = length 227, hash 27A608F9\n  sample 27:\n    time = 626913\n    flags = 1\n    data = length 229, hash 57DAAE4\n  sample 28:\n    time = 650132\n    flags = 1\n    data = length 235, hash ED30AC34\n  sample 29:\n    time = 673351\n    flags = 1\n    data = length 227, hash BD3D6280\n  sample 30:\n    time = 696570\n    flags = 1\n    data = length 233, hash 694B1087\n  sample 31:\n    time = 719789\n    flags = 1\n    data = length 232, hash 1EDFE047\n  sample 32:\n    time = 743008\n    flags = 1\n    data = length 228, hash E2A831F4\n  sample 33:\n    time = 766227\n    flags = 1\n    data = length 231, hash 757E6012\n  sample 34:\n    time = 789446\n    flags = 1\n    data = length 223, hash 4003D791\n  sample 35:\n    time = 812665\n    flags = 1\n    data = length 232, hash 3CF9A07C\n  sample 36:\n    time = 835884\n    flags = 1\n    data = length 228, hash 25AC3FF7\n  sample 37:\n    time = 859103\n    flags = 1\n    data = length 220, hash 2C1824CE\n  sample 38:\n    time = 882322\n    flags = 1\n    data = length 229, hash 46FDD8FB\n  sample 39:\n    time = 905541\n    flags = 1\n    data = length 237, hash F6988018\n  sample 40:\n    time = 928760\n    flags = 1\n    data = length 242, hash 60436B6B\n  sample 41:\n    time = 951979\n    flags = 1\n    data = length 275, hash 90EDFA8E\n  sample 42:\n    time = 975198\n    flags = 1\n    data = length 242, hash 5C86EFCB\n  sample 43:\n    time = 998417\n    flags = 1\n    data = length 233, hash E0A51B82\n  sample 44:\n    time = 1021636\n    flags = 1\n    data = length 235, hash 590DF14F\n  sample 45:\n    time = 1044855\n    flags = 1\n    data = length 238, hash 69AF4E6E\n  sample 46:\n    time = 1068074\n    flags = 1\n    data = length 235, hash E745AE8D\n  sample 47:\n    time = 1091293\n    flags = 1\n    data = length 223, hash 295F2A13\n  sample 48:\n    time = 1114512\n    flags = 1\n    data = length 228, hash E2F47B21\n  sample 49:\n    time = 1137731\n    flags = 1\n    data = length 229, hash 262C3CFE\n  sample 50:\n    time = 1160950\n    flags = 1\n    data = length 232, hash 4B5BF5E8\n  sample 51:\n    time = 1184169\n    flags = 1\n    data = length 233, hash F3D80836\n  sample 52:\n    time = 1207388\n    flags = 1\n    data = length 237, hash 32E0A11E\n  sample 53:\n    time = 1230607\n    flags = 1\n    data = length 228, hash E1B89F13\n  sample 54:\n    time = 1253826\n    flags = 1\n    data = length 237, hash 8BDD9E38\n  sample 55:\n    time = 1277045\n    flags = 1\n    data = length 235, hash 3C84161F\n  sample 56:\n    time = 1300264\n    flags = 1\n    data = length 227, hash A47E1789\n  sample 57:\n    time = 1323483\n    flags = 1\n    data = length 228, hash 869FDFD3\n  sample 58:\n    time = 1346702\n    flags = 1\n    data = length 233, hash 272ECE2\n  sample 59:\n    time = 1369921\n    flags = 1\n    data = length 227, hash DB6B9618\n  sample 60:\n    time = 1393140\n    flags = 1\n    data = length 212, hash 63214325\n  sample 61:\n    time = 1416359\n    flags = 1\n    data = length 221, hash 9BA588A1\n  sample 62:\n    time = 1439578\n    flags = 1\n    data = length 225, hash 21EFD50C\n  sample 63:\n    time = 1462797\n    flags = 1\n    data = length 231, hash F3AD0BF\n  sample 64:\n    time = 1486016\n    flags = 1\n    data = length 224, hash 822C9210\n  sample 65:\n    time = 1509235\n    flags = 1\n    data = length 195, hash D4EF53EE\n  sample 66:\n    time = 1532454\n    flags = 1\n    data = length 195, hash A816647A\n  sample 67:\n    time = 1555673\n    flags = 1\n    data = length 184, hash 9A2B7E6\n  sample 68:\n    time = 1578892\n    flags = 1\n    data = length 210, hash 956E3600\n  sample 69:\n    time = 1602111\n    flags = 1\n    data = length 234, hash 35CFDA0A\n  sample 70:\n    time = 1625330\n    flags = 1\n    data = length 239, hash 9E15AC1E\n  sample 71:\n    time = 1648549\n    flags = 1\n    data = length 228, hash F3B70641\n  sample 72:\n    time = 1671768\n    flags = 1\n    data = length 237, hash 124E3194\n  sample 73:\n    time = 1694987\n    flags = 1\n    data = length 231, hash 950CD7C8\n  sample 74:\n    time = 1718206\n    flags = 1\n    data = length 236, hash A12E49AF\n  sample 75:\n    time = 1741425\n    flags = 1\n    data = length 242, hash 43BC9C24\n  sample 76:\n    time = 1764644\n    flags = 1\n    data = length 241, hash DCF0B17\n  sample 77:\n    time = 1787863\n    flags = 1\n    data = length 251, hash C0B99968\n  sample 78:\n    time = 1811082\n    flags = 1\n    data = length 245, hash 9B38ED1C\n  sample 79:\n    time = 1834301\n    flags = 1\n    data = length 238, hash 1BA69079\n  sample 80:\n    time = 1857520\n    flags = 1\n    data = length 233, hash 44C8C6BF\n  sample 81:\n    time = 1880739\n    flags = 1\n    data = length 231, hash EABBEE02\n  sample 82:\n    time = 1903958\n    flags = 1\n    data = length 226, hash D09C44FB\n  sample 83:\n    time = 1927177\n    flags = 1\n    data = length 235, hash BE6A6608\n  sample 84:\n    time = 1950396\n    flags = 1\n    data = length 235, hash 2735F454\n  sample 85:\n    time = 1973615\n    flags = 1\n    data = length 238, hash B160DFE7\n  sample 86:\n    time = 1996834\n    flags = 1\n    data = length 232, hash 1B217D2E\n  sample 87:\n    time = 2020053\n    flags = 1\n    data = length 251, hash D1C14CEA\n  sample 88:\n    time = 2043272\n    flags = 1\n    data = length 256, hash 97C87F08\n  sample 89:\n    time = 2066491\n    flags = 1\n    data = length 237, hash 6645DB3\n  sample 90:\n    time = 2089710\n    flags = 1\n    data = length 235, hash 727A1C82\n  sample 91:\n    time = 2112929\n    flags = 1\n    data = length 234, hash 5015F8B5\n  sample 92:\n    time = 2136148\n    flags = 1\n    data = length 241, hash 9102144B\n  sample 93:\n    time = 2159367\n    flags = 1\n    data = length 224, hash 64E0D807\n  sample 94:\n    time = 2182586\n    flags = 1\n    data = length 228, hash 1922B852\n  sample 95:\n    time = 2205805\n    flags = 1\n    data = length 224, hash 953502D8\n  sample 96:\n    time = 2229024\n    flags = 1\n    data = length 214, hash 92B87FE7\n  sample 97:\n    time = 2252243\n    flags = 1\n    data = length 213, hash BB0C8D86\n  sample 98:\n    time = 2275462\n    flags = 1\n    data = length 206, hash 9AD21017\n  sample 99:\n    time = 2298681\n    flags = 1\n    data = length 209, hash C479FE94\n  sample 100:\n    time = 2321900\n    flags = 1\n    data = length 220, hash 3033DCE1\n  sample 101:\n    time = 2345119\n    flags = 1\n    data = length 217, hash 7D589C94\n  sample 102:\n    time = 2368338\n    flags = 1\n    data = length 216, hash AAF6C183\n  sample 103:\n    time = 2391557\n    flags = 1\n    data = length 206, hash 1EE1207F\n  sample 104:\n    time = 2414776\n    flags = 1\n    data = length 204, hash 4BEB1210\n  sample 105:\n    time = 2437995\n    flags = 1\n    data = length 213, hash 21A841C9\n  sample 106:\n    time = 2461214\n    flags = 1\n    data = length 207, hash B80B0424\n  sample 107:\n    time = 2484433\n    flags = 1\n    data = length 212, hash 4785A1C3\n  sample 108:\n    time = 2507652\n    flags = 1\n    data = length 205, hash 59BF7229\n  sample 109:\n    time = 2530871\n    flags = 1\n    data = length 208, hash FA313DDE\n  sample 110:\n    time = 2554090\n    flags = 1\n    data = length 211, hash 190D85FD\n  sample 111:\n    time = 2577309\n    flags = 1\n    data = length 211, hash BA050052\n  sample 112:\n    time = 2600528\n    flags = 1\n    data = length 211, hash F3080F10\n  sample 113:\n    time = 2623747\n    flags = 1\n    data = length 210, hash F41B7BE7\n  sample 114:\n    time = 2646966\n    flags = 1\n    data = length 207, hash 2176C97E\n  sample 115:\n    time = 2670185\n    flags = 1\n    data = length 220, hash 32087455\n  sample 116:\n    time = 2693404\n    flags = 1\n    data = length 213, hash 4E5649A8\n  sample 117:\n    time = 2716623\n    flags = 1\n    data = length 213, hash 5F12FDCF\n  sample 118:\n    time = 2739842\n    flags = 1\n    data = length 204, hash 1E895C2A\n  sample 119:\n    time = 2763061\n    flags = 1\n    data = length 219, hash 45382270\n  sample 120:\n    time = 2786280\n    flags = 1\n    data = length 205, hash D66C6A1D\n  sample 121:\n    time = 2809499\n    flags = 1\n    data = length 204, hash 467AD01F\n  sample 122:\n    time = 2832718\n    flags = 1\n    data = length 211, hash F0435574\n  sample 123:\n    time = 2855937\n    flags = 1\n    data = length 206, hash 8C96B75F\n  sample 124:\n    time = 2879156\n    flags = 1\n    data = length 200, hash 82553248\n  sample 125:\n    time = 2902375\n    flags = 1\n    data = length 180, hash 1E51E6CE\n  sample 126:\n    time = 2925594\n    flags = 1\n    data = length 196, hash 33151DC4\n  sample 127:\n    time = 2948813\n    flags = 1\n    data = length 197, hash 1E62A7D6\n  sample 128:\n    time = 2972032\n    flags = 1\n    data = length 206, hash 6A6C4CC9\n  sample 129:\n    time = 2995251\n    flags = 1\n    data = length 209, hash A72FABAA\n  sample 130:\n    time = 3018470\n    flags = 1\n    data = length 217, hash BA33B985\n  sample 131:\n    time = 3041689\n    flags = 1\n    data = length 235, hash 9919CFD9\n  sample 132:\n    time = 3064908\n    flags = 1\n    data = length 236, hash A22C7267\n  sample 133:\n    time = 3088127\n    flags = 1\n    data = length 213, hash 3D57C901\n  sample 134:\n    time = 3111346\n    flags = 1\n    data = length 205, hash 47F68FDE\n  sample 135:\n    time = 3134565\n    flags = 1\n    data = length 210, hash 9A756E9C\n  sample 136:\n    time = 3157784\n    flags = 1\n    data = length 210, hash BD45C31F\n  sample 137:\n    time = 3181003\n    flags = 1\n    data = length 207, hash 8774FF7B\n  sample 138:\n    time = 3204222\n    flags = 1\n    data = length 149, hash 4678C0E5\n  sample 139:\n    time = 3227441\n    flags = 1\n    data = length 161, hash E991035D\n  sample 140:\n    time = 3250660\n    flags = 1\n    data = length 197, hash C3013689\n  sample 141:\n    time = 3273879\n    flags = 1\n    data = length 208, hash E6C0237\n  sample 142:\n    time = 3297098\n    flags = 1\n    data = length 232, hash A330F188\n  sample 143:\n    time = 3320317\n    flags = 1\n    data = length 174, hash 2B69C34E\ntrack 1:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = application/id3\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.eac3.0.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = -1\n    id = 0\n    containerMimeType = null\n    sampleMimeType = audio/eac3\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 6\n    sampleRate = 48000\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 216000\n  sample count = 54\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 4000, hash BAEAFB2A\n  sample 1:\n    time = 5333\n    flags = 1\n    data = length 4000, hash E3C5EBF0\n  sample 2:\n    time = 10666\n    flags = 1\n    data = length 4000, hash 32E0F957\n  sample 3:\n    time = 15999\n    flags = 1\n    data = length 4000, hash 5354CC5D\n  sample 4:\n    time = 21332\n    flags = 1\n    data = length 4000, hash FF834906\n  sample 5:\n    time = 26665\n    flags = 1\n    data = length 4000, hash 6F571E61\n  sample 6:\n    time = 31998\n    flags = 1\n    data = length 4000, hash 5C931F6B\n  sample 7:\n    time = 37331\n    flags = 1\n    data = length 4000, hash B1FB2E57\n  sample 8:\n    time = 42664\n    flags = 1\n    data = length 4000, hash C71240EB\n  sample 9:\n    time = 47997\n    flags = 1\n    data = length 4000, hash C3E302EE\n  sample 10:\n    time = 53330\n    flags = 1\n    data = length 4000, hash 7994C27B\n  sample 11:\n    time = 58663\n    flags = 1\n    data = length 4000, hash 1ED4E6F3\n  sample 12:\n    time = 63996\n    flags = 1\n    data = length 4000, hash 1D5E6AAC\n  sample 13:\n    time = 69329\n    flags = 1\n    data = length 4000, hash 30058F51\n  sample 14:\n    time = 74662\n    flags = 1\n    data = length 4000, hash 15DD0E4A\n  sample 15:\n    time = 79995\n    flags = 1\n    data = length 4000, hash 37BE7C15\n  sample 16:\n    time = 85328\n    flags = 1\n    data = length 4000, hash 7CFDD34B\n  sample 17:\n    time = 90661\n    flags = 1\n    data = length 4000, hash 27F20D29\n  sample 18:\n    time = 95994\n    flags = 1\n    data = length 4000, hash 6F565894\n  sample 19:\n    time = 101327\n    flags = 1\n    data = length 4000, hash A6F07C4A\n  sample 20:\n    time = 106660\n    flags = 1\n    data = length 4000, hash 3A0CA15C\n  sample 21:\n    time = 111993\n    flags = 1\n    data = length 4000, hash DB365414\n  sample 22:\n    time = 117326\n    flags = 1\n    data = length 4000, hash 31E08469\n  sample 23:\n    time = 122659\n    flags = 1\n    data = length 4000, hash 315F5C28\n  sample 24:\n    time = 127992\n    flags = 1\n    data = length 4000, hash CC65DF80\n  sample 25:\n    time = 133325\n    flags = 1\n    data = length 4000, hash 503FB64C\n  sample 26:\n    time = 138658\n    flags = 1\n    data = length 4000, hash 817CF735\n  sample 27:\n    time = 143991\n    flags = 1\n    data = length 4000, hash 37391ADA\n  sample 28:\n    time = 149324\n    flags = 1\n    data = length 4000, hash 37391ADA\n  sample 29:\n    time = 154657\n    flags = 1\n    data = length 4000, hash 64DBF751\n  sample 30:\n    time = 159990\n    flags = 1\n    data = length 4000, hash 81AE828E\n  sample 31:\n    time = 165323\n    flags = 1\n    data = length 4000, hash 767D6C98\n  sample 32:\n    time = 170656\n    flags = 1\n    data = length 4000, hash A5F6D4E\n  sample 33:\n    time = 175989\n    flags = 1\n    data = length 4000, hash EABC6B0D\n  sample 34:\n    time = 181322\n    flags = 1\n    data = length 4000, hash F47EF742\n  sample 35:\n    time = 186655\n    flags = 1\n    data = length 4000, hash 9B2549DA\n  sample 36:\n    time = 191988\n    flags = 1\n    data = length 4000, hash A12733C9\n  sample 37:\n    time = 197321\n    flags = 1\n    data = length 4000, hash 95F62E99\n  sample 38:\n    time = 202654\n    flags = 1\n    data = length 4000, hash A4D858\n  sample 39:\n    time = 207987\n    flags = 1\n    data = length 4000, hash A4D858\n  sample 40:\n    time = 213320\n    flags = 1\n    data = length 4000, hash 22C1A129\n  sample 41:\n    time = 218653\n    flags = 1\n    data = length 4000, hash 2C51E4A1\n  sample 42:\n    time = 223986\n    flags = 1\n    data = length 4000, hash 3782E8BB\n  sample 43:\n    time = 229319\n    flags = 1\n    data = length 4000, hash 2C51E4A1\n  sample 44:\n    time = 234652\n    flags = 1\n    data = length 4000, hash BDB3D129\n  sample 45:\n    time = 239985\n    flags = 1\n    data = length 4000, hash F642A55\n  sample 46:\n    time = 245318\n    flags = 1\n    data = length 4000, hash 32F259F4\n  sample 47:\n    time = 250651\n    flags = 1\n    data = length 4000, hash 4C987B7C\n  sample 48:\n    time = 255984\n    flags = 1\n    data = length 4000, hash 57C98E1C\n  sample 49:\n    time = 261317\n    flags = 1\n    data = length 4000, hash 4C987B7C\n  sample 50:\n    time = 266650\n    flags = 1\n    data = length 4000, hash 4C987B7C\n  sample 51:\n    time = 271983\n    flags = 1\n    data = length 4000, hash 4C987B7C\n  sample 52:\n    time = 277316\n    flags = 1\n    data = length 4000, hash 4C987B7C\n  sample 53:\n    time = 282649\n    flags = 1\n    data = length 4000, hash 4C987B7C\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.ps.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 766\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 2\ntrack 192:\n  format:\n    bitrate = -1\n    id = 192\n    containerMimeType = null\n    sampleMimeType = audio/mpeg-L2\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 1671\n  sample count = 4\n  sample 0:\n    time = 29088\n    flags = 1\n    data = length 417, hash 5C710F78\n  sample 1:\n    time = 55210\n    flags = 1\n    data = length 418, hash 79CF71F8\n  sample 2:\n    time = 81332\n    flags = 1\n    data = length 418, hash 79CF71F8\n  sample 3:\n    time = 107454\n    flags = 1\n    data = length 418, hash 79CF71F8\ntrack 224:\n  format:\n    bitrate = -1\n    id = 224\n    containerMimeType = null\n    sampleMimeType = video/mpeg2\n    maxInputSize = -1\n    width = 640\n    height = 426\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 22, hash 743CC6F8\n  total output bytes = 44056\n  sample count = 2\n  sample 0:\n    time = 40000\n    flags = 1\n    data = length 20646, hash 576390B\n  sample 1:\n    time = 80000\n    flags = 0\n    data = length 17831, hash 5C5A57F5\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.ps.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 766\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 2\ntrack 192:\n  format:\n    bitrate = -1\n    id = 192\n    containerMimeType = null\n    sampleMimeType = audio/mpeg-L2\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntrack 224:\n  format:\n    bitrate = -1\n    id = 224\n    containerMimeType = null\n    sampleMimeType = video/mpeg2\n    maxInputSize = -1\n    width = 640\n    height = 426\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 22, hash 743CC6F8\n  total output bytes = 33949\n  sample count = 1\n  sample 0:\n    time = 80000\n    flags = 0\n    data = length 17831, hash 5C5A57F5\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.ps.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 766\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 2\ntrack 192:\n  format:\n    bitrate = -1\n    id = 192\n    containerMimeType = null\n    sampleMimeType = audio/mpeg-L2\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntrack 224:\n  format:\n    bitrate = -1\n    id = 224\n    containerMimeType = null\n    sampleMimeType = video/mpeg2\n    maxInputSize = -1\n    width = 640\n    height = 426\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 22, hash 743CC6F8\n  total output bytes = 19791\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.ps.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 766\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 2\ntrack 192:\n  format:\n    bitrate = -1\n    id = 192\n    containerMimeType = null\n    sampleMimeType = audio/mpeg-L2\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntrack 224:\n  format:\n    bitrate = -1\n    id = 224\n    containerMimeType = null\n    sampleMimeType = video/mpeg2\n    maxInputSize = -1\n    width = 640\n    height = 426\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 22, hash 743CC6F8\n  total output bytes = 1585\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.ps.unklen.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 2\ntrack 192:\n  format:\n    bitrate = -1\n    id = 192\n    containerMimeType = null\n    sampleMimeType = audio/mpeg-L2\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 1671\n  sample count = 4\n  sample 0:\n    time = 29088\n    flags = 1\n    data = length 417, hash 5C710F78\n  sample 1:\n    time = 55210\n    flags = 1\n    data = length 418, hash 79CF71F8\n  sample 2:\n    time = 81332\n    flags = 1\n    data = length 418, hash 79CF71F8\n  sample 3:\n    time = 107454\n    flags = 1\n    data = length 418, hash 79CF71F8\ntrack 224:\n  format:\n    bitrate = -1\n    id = 224\n    containerMimeType = null\n    sampleMimeType = video/mpeg2\n    maxInputSize = -1\n    width = 640\n    height = 426\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 22, hash 743CC6F8\n  total output bytes = 44056\n  sample count = 2\n  sample 0:\n    time = 40000\n    flags = 1\n    data = length 20646, hash 576390B\n  sample 1:\n    time = 80000\n    flags = 0\n    data = length 17831, hash 5C5A57F5\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.ts.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 66733\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 3\ntrack 256:\n  format:\n    bitrate = -1\n    id = 1/256\n    containerMimeType = null\n    sampleMimeType = video/mpeg2\n    maxInputSize = -1\n    width = 640\n    height = 426\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 22, hash CE183139\n  total output bytes = 45026\n  sample count = 2\n  sample 0:\n    time = 33366\n    flags = 1\n    data = length 20711, hash 34341E8\n  sample 1:\n    time = 66733\n    flags = 0\n    data = length 18112, hash EC44B35B\ntrack 257:\n  format:\n    bitrate = -1\n    id = 1/257\n    containerMimeType = null\n    sampleMimeType = audio/mpeg-L2\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n  total output bytes = 5015\n  sample count = 4\n  sample 0:\n    time = 22455\n    flags = 1\n    data = length 1253, hash 727FD1C6\n  sample 1:\n    time = 48577\n    flags = 1\n    data = length 1254, hash 73FB07B8\n  sample 2:\n    time = 74700\n    flags = 1\n    data = length 1254, hash 73FB07B8\n  sample 3:\n    time = 100822\n    flags = 1\n    data = length 1254, hash 73FB07B8\ntrack 8448:\n  format:\n    bitrate = -1\n    id = 1/8448\n    containerMimeType = null\n    sampleMimeType = application/cea-608\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.ts.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 66733\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 3\ntrack 256:\n  format:\n    bitrate = -1\n    id = 1/256\n    containerMimeType = null\n    sampleMimeType = video/mpeg2\n    maxInputSize = -1\n    width = 640\n    height = 426\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 22, hash CE183139\n  total output bytes = 45026\n  sample count = 2\n  sample 0:\n    time = 55610\n    flags = 1\n    data = length 20711, hash 34341E8\n  sample 1:\n    time = 88977\n    flags = 0\n    data = length 18112, hash EC44B35B\ntrack 257:\n  format:\n    bitrate = -1\n    id = 1/257\n    containerMimeType = null\n    sampleMimeType = audio/mpeg-L2\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n  total output bytes = 5015\n  sample count = 4\n  sample 0:\n    time = 44699\n    flags = 1\n    data = length 1253, hash 727FD1C6\n  sample 1:\n    time = 70821\n    flags = 1\n    data = length 1254, hash 73FB07B8\n  sample 2:\n    time = 96944\n    flags = 1\n    data = length 1254, hash 73FB07B8\n  sample 3:\n    time = 123066\n    flags = 1\n    data = length 1254, hash 73FB07B8\ntrack 8448:\n  format:\n    bitrate = -1\n    id = 1/8448\n    containerMimeType = null\n    sampleMimeType = application/cea-608\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.ts.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 66733\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 3\ntrack 256:\n  format:\n    bitrate = -1\n    id = 1/256\n    containerMimeType = null\n    sampleMimeType = video/mpeg2\n    maxInputSize = -1\n    width = 640\n    height = 426\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 22, hash CE183139\n  total output bytes = 45026\n  sample count = 2\n  sample 0:\n    time = 77854\n    flags = 1\n    data = length 20711, hash 34341E8\n  sample 1:\n    time = 111221\n    flags = 0\n    data = length 18112, hash EC44B35B\ntrack 257:\n  format:\n    bitrate = -1\n    id = 1/257\n    containerMimeType = null\n    sampleMimeType = audio/mpeg-L2\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n  total output bytes = 5015\n  sample count = 4\n  sample 0:\n    time = 66943\n    flags = 1\n    data = length 1253, hash 727FD1C6\n  sample 1:\n    time = 93065\n    flags = 1\n    data = length 1254, hash 73FB07B8\n  sample 2:\n    time = 119188\n    flags = 1\n    data = length 1254, hash 73FB07B8\n  sample 3:\n    time = 145310\n    flags = 1\n    data = length 1254, hash 73FB07B8\ntrack 8448:\n  format:\n    bitrate = -1\n    id = 1/8448\n    containerMimeType = null\n    sampleMimeType = application/cea-608\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.ts.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 66733\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 3\ntrack 256:\n  format:\n    bitrate = -1\n    id = 1/256\n    containerMimeType = null\n    sampleMimeType = video/mpeg2\n    maxInputSize = -1\n    width = 640\n    height = 426\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 22, hash CE183139\n  total output bytes = 0\n  sample count = 0\ntrack 257:\n  format:\n    bitrate = -1\n    id = 1/257\n    containerMimeType = null\n    sampleMimeType = audio/mpeg-L2\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n  total output bytes = 2508\n  sample count = 2\n  sample 0:\n    time = 66733\n    flags = 1\n    data = length 1254, hash 73FB07B8\n  sample 1:\n    time = 92855\n    flags = 1\n    data = length 1254, hash 73FB07B8\ntrack 8448:\n  format:\n    bitrate = -1\n    id = 1/8448\n    containerMimeType = null\n    sampleMimeType = application/cea-608\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample.ts.unklen.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 3\ntrack 256:\n  format:\n    bitrate = -1\n    id = 1/256\n    containerMimeType = null\n    sampleMimeType = video/mpeg2\n    maxInputSize = -1\n    width = 640\n    height = 426\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 22, hash CE183139\n  total output bytes = 45026\n  sample count = 2\n  sample 0:\n    time = 33366\n    flags = 1\n    data = length 20711, hash 34341E8\n  sample 1:\n    time = 66733\n    flags = 0\n    data = length 18112, hash EC44B35B\ntrack 257:\n  format:\n    bitrate = -1\n    id = 1/257\n    containerMimeType = null\n    sampleMimeType = audio/mpeg-L2\n    maxInputSize = 4096\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = und\n    drmInitData = -\n    initializationData:\n  total output bytes = 5015\n  sample count = 4\n  sample 0:\n    time = 22455\n    flags = 1\n    data = length 1253, hash 727FD1C6\n  sample 1:\n    time = 48577\n    flags = 1\n    data = length 1254, hash 73FB07B8\n  sample 2:\n    time = 74700\n    flags = 1\n    data = length 1254, hash 73FB07B8\n  sample 3:\n    time = 100822\n    flags = 1\n    data = length 1254, hash 73FB07B8\ntrack 8448:\n  format:\n    bitrate = -1\n    id = 1/8448\n    containerMimeType = null\n    sampleMimeType = application/cea-608\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample_cbs.adts.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 3356772\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 0\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 2, hash 5F7\n  total output bytes = 30797\n  sample count = 144\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 23, hash 47DE9131\n  sample 1:\n    time = 23219\n    flags = 1\n    data = length 6, hash 31CF3A46\n  sample 2:\n    time = 46438\n    flags = 1\n    data = length 6, hash 31CF3A46\n  sample 3:\n    time = 69657\n    flags = 1\n    data = length 6, hash 31CF3A46\n  sample 4:\n    time = 92876\n    flags = 1\n    data = length 6, hash 31EC5206\n  sample 5:\n    time = 116095\n    flags = 1\n    data = length 171, hash 4F6478F6\n  sample 6:\n    time = 139314\n    flags = 1\n    data = length 202, hash AF4068A3\n  sample 7:\n    time = 162533\n    flags = 1\n    data = length 210, hash E4C10618\n  sample 8:\n    time = 185752\n    flags = 1\n    data = length 217, hash 9ECCD0D9\n  sample 9:\n    time = 208971\n    flags = 1\n    data = length 212, hash 6BAC2CD9\n  sample 10:\n    time = 232190\n    flags = 1\n    data = length 223, hash 188B6010\n  sample 11:\n    time = 255409\n    flags = 1\n    data = length 222, hash C1A04D0C\n  sample 12:\n    time = 278628\n    flags = 1\n    data = length 220, hash D65F9768\n  sample 13:\n    time = 301847\n    flags = 1\n    data = length 227, hash B96C9E14\n  sample 14:\n    time = 325066\n    flags = 1\n    data = length 229, hash 9FB09972\n  sample 15:\n    time = 348285\n    flags = 1\n    data = length 220, hash 2271F053\n  sample 16:\n    time = 371504\n    flags = 1\n    data = length 226, hash 5EDD2F4F\n  sample 17:\n    time = 394723\n    flags = 1\n    data = length 239, hash 957510E0\n  sample 18:\n    time = 417942\n    flags = 1\n    data = length 224, hash 718A8F47\n  sample 19:\n    time = 441161\n    flags = 1\n    data = length 225, hash 5E11E293\n  sample 20:\n    time = 464380\n    flags = 1\n    data = length 227, hash FCE50D27\n  sample 21:\n    time = 487599\n    flags = 1\n    data = length 212, hash 77908C40\n  sample 22:\n    time = 510818\n    flags = 1\n    data = length 227, hash 34C4EB32\n  sample 23:\n    time = 534037\n    flags = 1\n    data = length 231, hash 95488307\n  sample 24:\n    time = 557256\n    flags = 1\n    data = length 226, hash 97F12D6F\n  sample 25:\n    time = 580475\n    flags = 1\n    data = length 236, hash 91A9D9A2\n  sample 26:\n    time = 603694\n    flags = 1\n    data = length 227, hash 27A608F9\n  sample 27:\n    time = 626913\n    flags = 1\n    data = length 229, hash 57DAAE4\n  sample 28:\n    time = 650132\n    flags = 1\n    data = length 235, hash ED30AC34\n  sample 29:\n    time = 673351\n    flags = 1\n    data = length 227, hash BD3D6280\n  sample 30:\n    time = 696570\n    flags = 1\n    data = length 233, hash 694B1087\n  sample 31:\n    time = 719789\n    flags = 1\n    data = length 232, hash 1EDFE047\n  sample 32:\n    time = 743008\n    flags = 1\n    data = length 228, hash E2A831F4\n  sample 33:\n    time = 766227\n    flags = 1\n    data = length 231, hash 757E6012\n  sample 34:\n    time = 789446\n    flags = 1\n    data = length 223, hash 4003D791\n  sample 35:\n    time = 812665\n    flags = 1\n    data = length 232, hash 3CF9A07C\n  sample 36:\n    time = 835884\n    flags = 1\n    data = length 228, hash 25AC3FF7\n  sample 37:\n    time = 859103\n    flags = 1\n    data = length 220, hash 2C1824CE\n  sample 38:\n    time = 882322\n    flags = 1\n    data = length 229, hash 46FDD8FB\n  sample 39:\n    time = 905541\n    flags = 1\n    data = length 237, hash F6988018\n  sample 40:\n    time = 928760\n    flags = 1\n    data = length 242, hash 60436B6B\n  sample 41:\n    time = 951979\n    flags = 1\n    data = length 275, hash 90EDFA8E\n  sample 42:\n    time = 975198\n    flags = 1\n    data = length 242, hash 5C86EFCB\n  sample 43:\n    time = 998417\n    flags = 1\n    data = length 233, hash E0A51B82\n  sample 44:\n    time = 1021636\n    flags = 1\n    data = length 235, hash 590DF14F\n  sample 45:\n    time = 1044855\n    flags = 1\n    data = length 238, hash 69AF4E6E\n  sample 46:\n    time = 1068074\n    flags = 1\n    data = length 235, hash E745AE8D\n  sample 47:\n    time = 1091293\n    flags = 1\n    data = length 223, hash 295F2A13\n  sample 48:\n    time = 1114512\n    flags = 1\n    data = length 228, hash E2F47B21\n  sample 49:\n    time = 1137731\n    flags = 1\n    data = length 229, hash 262C3CFE\n  sample 50:\n    time = 1160950\n    flags = 1\n    data = length 232, hash 4B5BF5E8\n  sample 51:\n    time = 1184169\n    flags = 1\n    data = length 233, hash F3D80836\n  sample 52:\n    time = 1207388\n    flags = 1\n    data = length 237, hash 32E0A11E\n  sample 53:\n    time = 1230607\n    flags = 1\n    data = length 228, hash E1B89F13\n  sample 54:\n    time = 1253826\n    flags = 1\n    data = length 237, hash 8BDD9E38\n  sample 55:\n    time = 1277045\n    flags = 1\n    data = length 235, hash 3C84161F\n  sample 56:\n    time = 1300264\n    flags = 1\n    data = length 227, hash A47E1789\n  sample 57:\n    time = 1323483\n    flags = 1\n    data = length 228, hash 869FDFD3\n  sample 58:\n    time = 1346702\n    flags = 1\n    data = length 233, hash 272ECE2\n  sample 59:\n    time = 1369921\n    flags = 1\n    data = length 227, hash DB6B9618\n  sample 60:\n    time = 1393140\n    flags = 1\n    data = length 212, hash 63214325\n  sample 61:\n    time = 1416359\n    flags = 1\n    data = length 221, hash 9BA588A1\n  sample 62:\n    time = 1439578\n    flags = 1\n    data = length 225, hash 21EFD50C\n  sample 63:\n    time = 1462797\n    flags = 1\n    data = length 231, hash F3AD0BF\n  sample 64:\n    time = 1486016\n    flags = 1\n    data = length 224, hash 822C9210\n  sample 65:\n    time = 1509235\n    flags = 1\n    data = length 195, hash D4EF53EE\n  sample 66:\n    time = 1532454\n    flags = 1\n    data = length 195, hash A816647A\n  sample 67:\n    time = 1555673\n    flags = 1\n    data = length 184, hash 9A2B7E6\n  sample 68:\n    time = 1578892\n    flags = 1\n    data = length 210, hash 956E3600\n  sample 69:\n    time = 1602111\n    flags = 1\n    data = length 234, hash 35CFDA0A\n  sample 70:\n    time = 1625330\n    flags = 1\n    data = length 239, hash 9E15AC1E\n  sample 71:\n    time = 1648549\n    flags = 1\n    data = length 228, hash F3B70641\n  sample 72:\n    time = 1671768\n    flags = 1\n    data = length 237, hash 124E3194\n  sample 73:\n    time = 1694987\n    flags = 1\n    data = length 231, hash 950CD7C8\n  sample 74:\n    time = 1718206\n    flags = 1\n    data = length 236, hash A12E49AF\n  sample 75:\n    time = 1741425\n    flags = 1\n    data = length 242, hash 43BC9C24\n  sample 76:\n    time = 1764644\n    flags = 1\n    data = length 241, hash DCF0B17\n  sample 77:\n    time = 1787863\n    flags = 1\n    data = length 251, hash C0B99968\n  sample 78:\n    time = 1811082\n    flags = 1\n    data = length 245, hash 9B38ED1C\n  sample 79:\n    time = 1834301\n    flags = 1\n    data = length 238, hash 1BA69079\n  sample 80:\n    time = 1857520\n    flags = 1\n    data = length 233, hash 44C8C6BF\n  sample 81:\n    time = 1880739\n    flags = 1\n    data = length 231, hash EABBEE02\n  sample 82:\n    time = 1903958\n    flags = 1\n    data = length 226, hash D09C44FB\n  sample 83:\n    time = 1927177\n    flags = 1\n    data = length 235, hash BE6A6608\n  sample 84:\n    time = 1950396\n    flags = 1\n    data = length 235, hash 2735F454\n  sample 85:\n    time = 1973615\n    flags = 1\n    data = length 238, hash B160DFE7\n  sample 86:\n    time = 1996834\n    flags = 1\n    data = length 232, hash 1B217D2E\n  sample 87:\n    time = 2020053\n    flags = 1\n    data = length 251, hash D1C14CEA\n  sample 88:\n    time = 2043272\n    flags = 1\n    data = length 256, hash 97C87F08\n  sample 89:\n    time = 2066491\n    flags = 1\n    data = length 237, hash 6645DB3\n  sample 90:\n    time = 2089710\n    flags = 1\n    data = length 235, hash 727A1C82\n  sample 91:\n    time = 2112929\n    flags = 1\n    data = length 234, hash 5015F8B5\n  sample 92:\n    time = 2136148\n    flags = 1\n    data = length 241, hash 9102144B\n  sample 93:\n    time = 2159367\n    flags = 1\n    data = length 224, hash 64E0D807\n  sample 94:\n    time = 2182586\n    flags = 1\n    data = length 228, hash 1922B852\n  sample 95:\n    time = 2205805\n    flags = 1\n    data = length 224, hash 953502D8\n  sample 96:\n    time = 2229024\n    flags = 1\n    data = length 214, hash 92B87FE7\n  sample 97:\n    time = 2252243\n    flags = 1\n    data = length 213, hash BB0C8D86\n  sample 98:\n    time = 2275462\n    flags = 1\n    data = length 206, hash 9AD21017\n  sample 99:\n    time = 2298681\n    flags = 1\n    data = length 209, hash C479FE94\n  sample 100:\n    time = 2321900\n    flags = 1\n    data = length 220, hash 3033DCE1\n  sample 101:\n    time = 2345119\n    flags = 1\n    data = length 217, hash 7D589C94\n  sample 102:\n    time = 2368338\n    flags = 1\n    data = length 216, hash AAF6C183\n  sample 103:\n    time = 2391557\n    flags = 1\n    data = length 206, hash 1EE1207F\n  sample 104:\n    time = 2414776\n    flags = 1\n    data = length 204, hash 4BEB1210\n  sample 105:\n    time = 2437995\n    flags = 1\n    data = length 213, hash 21A841C9\n  sample 106:\n    time = 2461214\n    flags = 1\n    data = length 207, hash B80B0424\n  sample 107:\n    time = 2484433\n    flags = 1\n    data = length 212, hash 4785A1C3\n  sample 108:\n    time = 2507652\n    flags = 1\n    data = length 205, hash 59BF7229\n  sample 109:\n    time = 2530871\n    flags = 1\n    data = length 208, hash FA313DDE\n  sample 110:\n    time = 2554090\n    flags = 1\n    data = length 211, hash 190D85FD\n  sample 111:\n    time = 2577309\n    flags = 1\n    data = length 211, hash BA050052\n  sample 112:\n    time = 2600528\n    flags = 1\n    data = length 211, hash F3080F10\n  sample 113:\n    time = 2623747\n    flags = 1\n    data = length 210, hash F41B7BE7\n  sample 114:\n    time = 2646966\n    flags = 1\n    data = length 207, hash 2176C97E\n  sample 115:\n    time = 2670185\n    flags = 1\n    data = length 220, hash 32087455\n  sample 116:\n    time = 2693404\n    flags = 1\n    data = length 213, hash 4E5649A8\n  sample 117:\n    time = 2716623\n    flags = 1\n    data = length 213, hash 5F12FDCF\n  sample 118:\n    time = 2739842\n    flags = 1\n    data = length 204, hash 1E895C2A\n  sample 119:\n    time = 2763061\n    flags = 1\n    data = length 219, hash 45382270\n  sample 120:\n    time = 2786280\n    flags = 1\n    data = length 205, hash D66C6A1D\n  sample 121:\n    time = 2809499\n    flags = 1\n    data = length 204, hash 467AD01F\n  sample 122:\n    time = 2832718\n    flags = 1\n    data = length 211, hash F0435574\n  sample 123:\n    time = 2855937\n    flags = 1\n    data = length 206, hash 8C96B75F\n  sample 124:\n    time = 2879156\n    flags = 1\n    data = length 200, hash 82553248\n  sample 125:\n    time = 2902375\n    flags = 1\n    data = length 180, hash 1E51E6CE\n  sample 126:\n    time = 2925594\n    flags = 1\n    data = length 196, hash 33151DC4\n  sample 127:\n    time = 2948813\n    flags = 1\n    data = length 197, hash 1E62A7D6\n  sample 128:\n    time = 2972032\n    flags = 1\n    data = length 206, hash 6A6C4CC9\n  sample 129:\n    time = 2995251\n    flags = 1\n    data = length 209, hash A72FABAA\n  sample 130:\n    time = 3018470\n    flags = 1\n    data = length 217, hash BA33B985\n  sample 131:\n    time = 3041689\n    flags = 1\n    data = length 235, hash 9919CFD9\n  sample 132:\n    time = 3064908\n    flags = 1\n    data = length 236, hash A22C7267\n  sample 133:\n    time = 3088127\n    flags = 1\n    data = length 213, hash 3D57C901\n  sample 134:\n    time = 3111346\n    flags = 1\n    data = length 205, hash 47F68FDE\n  sample 135:\n    time = 3134565\n    flags = 1\n    data = length 210, hash 9A756E9C\n  sample 136:\n    time = 3157784\n    flags = 1\n    data = length 210, hash BD45C31F\n  sample 137:\n    time = 3181003\n    flags = 1\n    data = length 207, hash 8774FF7B\n  sample 138:\n    time = 3204222\n    flags = 1\n    data = length 149, hash 4678C0E5\n  sample 139:\n    time = 3227441\n    flags = 1\n    data = length 161, hash E991035D\n  sample 140:\n    time = 3250660\n    flags = 1\n    data = length 197, hash C3013689\n  sample 141:\n    time = 3273879\n    flags = 1\n    data = length 208, hash E6C0237\n  sample 142:\n    time = 3297098\n    flags = 1\n    data = length 232, hash A330F188\n  sample 143:\n    time = 3320317\n    flags = 1\n    data = length 174, hash 2B69C34E\ntrack 1:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = application/id3\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample_cbs.adts.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 3356772\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 0\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 2, hash 5F7\n  total output bytes = 20533\n  sample count = 94\n  sample 0:\n    time = 1118924\n    flags = 1\n    data = length 232, hash 4B5BF5E8\n  sample 1:\n    time = 1142143\n    flags = 1\n    data = length 233, hash F3D80836\n  sample 2:\n    time = 1165362\n    flags = 1\n    data = length 237, hash 32E0A11E\n  sample 3:\n    time = 1188581\n    flags = 1\n    data = length 228, hash E1B89F13\n  sample 4:\n    time = 1211800\n    flags = 1\n    data = length 237, hash 8BDD9E38\n  sample 5:\n    time = 1235019\n    flags = 1\n    data = length 235, hash 3C84161F\n  sample 6:\n    time = 1258238\n    flags = 1\n    data = length 227, hash A47E1789\n  sample 7:\n    time = 1281457\n    flags = 1\n    data = length 228, hash 869FDFD3\n  sample 8:\n    time = 1304676\n    flags = 1\n    data = length 233, hash 272ECE2\n  sample 9:\n    time = 1327895\n    flags = 1\n    data = length 227, hash DB6B9618\n  sample 10:\n    time = 1351114\n    flags = 1\n    data = length 212, hash 63214325\n  sample 11:\n    time = 1374333\n    flags = 1\n    data = length 221, hash 9BA588A1\n  sample 12:\n    time = 1397552\n    flags = 1\n    data = length 225, hash 21EFD50C\n  sample 13:\n    time = 1420771\n    flags = 1\n    data = length 231, hash F3AD0BF\n  sample 14:\n    time = 1443990\n    flags = 1\n    data = length 224, hash 822C9210\n  sample 15:\n    time = 1467209\n    flags = 1\n    data = length 195, hash D4EF53EE\n  sample 16:\n    time = 1490428\n    flags = 1\n    data = length 195, hash A816647A\n  sample 17:\n    time = 1513647\n    flags = 1\n    data = length 184, hash 9A2B7E6\n  sample 18:\n    time = 1536866\n    flags = 1\n    data = length 210, hash 956E3600\n  sample 19:\n    time = 1560085\n    flags = 1\n    data = length 234, hash 35CFDA0A\n  sample 20:\n    time = 1583304\n    flags = 1\n    data = length 239, hash 9E15AC1E\n  sample 21:\n    time = 1606523\n    flags = 1\n    data = length 228, hash F3B70641\n  sample 22:\n    time = 1629742\n    flags = 1\n    data = length 237, hash 124E3194\n  sample 23:\n    time = 1652961\n    flags = 1\n    data = length 231, hash 950CD7C8\n  sample 24:\n    time = 1676180\n    flags = 1\n    data = length 236, hash A12E49AF\n  sample 25:\n    time = 1699399\n    flags = 1\n    data = length 242, hash 43BC9C24\n  sample 26:\n    time = 1722618\n    flags = 1\n    data = length 241, hash DCF0B17\n  sample 27:\n    time = 1745837\n    flags = 1\n    data = length 251, hash C0B99968\n  sample 28:\n    time = 1769056\n    flags = 1\n    data = length 245, hash 9B38ED1C\n  sample 29:\n    time = 1792275\n    flags = 1\n    data = length 238, hash 1BA69079\n  sample 30:\n    time = 1815494\n    flags = 1\n    data = length 233, hash 44C8C6BF\n  sample 31:\n    time = 1838713\n    flags = 1\n    data = length 231, hash EABBEE02\n  sample 32:\n    time = 1861932\n    flags = 1\n    data = length 226, hash D09C44FB\n  sample 33:\n    time = 1885151\n    flags = 1\n    data = length 235, hash BE6A6608\n  sample 34:\n    time = 1908370\n    flags = 1\n    data = length 235, hash 2735F454\n  sample 35:\n    time = 1931589\n    flags = 1\n    data = length 238, hash B160DFE7\n  sample 36:\n    time = 1954808\n    flags = 1\n    data = length 232, hash 1B217D2E\n  sample 37:\n    time = 1978027\n    flags = 1\n    data = length 251, hash D1C14CEA\n  sample 38:\n    time = 2001246\n    flags = 1\n    data = length 256, hash 97C87F08\n  sample 39:\n    time = 2024465\n    flags = 1\n    data = length 237, hash 6645DB3\n  sample 40:\n    time = 2047684\n    flags = 1\n    data = length 235, hash 727A1C82\n  sample 41:\n    time = 2070903\n    flags = 1\n    data = length 234, hash 5015F8B5\n  sample 42:\n    time = 2094122\n    flags = 1\n    data = length 241, hash 9102144B\n  sample 43:\n    time = 2117341\n    flags = 1\n    data = length 224, hash 64E0D807\n  sample 44:\n    time = 2140560\n    flags = 1\n    data = length 228, hash 1922B852\n  sample 45:\n    time = 2163779\n    flags = 1\n    data = length 224, hash 953502D8\n  sample 46:\n    time = 2186998\n    flags = 1\n    data = length 214, hash 92B87FE7\n  sample 47:\n    time = 2210217\n    flags = 1\n    data = length 213, hash BB0C8D86\n  sample 48:\n    time = 2233436\n    flags = 1\n    data = length 206, hash 9AD21017\n  sample 49:\n    time = 2256655\n    flags = 1\n    data = length 209, hash C479FE94\n  sample 50:\n    time = 2279874\n    flags = 1\n    data = length 220, hash 3033DCE1\n  sample 51:\n    time = 2303093\n    flags = 1\n    data = length 217, hash 7D589C94\n  sample 52:\n    time = 2326312\n    flags = 1\n    data = length 216, hash AAF6C183\n  sample 53:\n    time = 2349531\n    flags = 1\n    data = length 206, hash 1EE1207F\n  sample 54:\n    time = 2372750\n    flags = 1\n    data = length 204, hash 4BEB1210\n  sample 55:\n    time = 2395969\n    flags = 1\n    data = length 213, hash 21A841C9\n  sample 56:\n    time = 2419188\n    flags = 1\n    data = length 207, hash B80B0424\n  sample 57:\n    time = 2442407\n    flags = 1\n    data = length 212, hash 4785A1C3\n  sample 58:\n    time = 2465626\n    flags = 1\n    data = length 205, hash 59BF7229\n  sample 59:\n    time = 2488845\n    flags = 1\n    data = length 208, hash FA313DDE\n  sample 60:\n    time = 2512064\n    flags = 1\n    data = length 211, hash 190D85FD\n  sample 61:\n    time = 2535283\n    flags = 1\n    data = length 211, hash BA050052\n  sample 62:\n    time = 2558502\n    flags = 1\n    data = length 211, hash F3080F10\n  sample 63:\n    time = 2581721\n    flags = 1\n    data = length 210, hash F41B7BE7\n  sample 64:\n    time = 2604940\n    flags = 1\n    data = length 207, hash 2176C97E\n  sample 65:\n    time = 2628159\n    flags = 1\n    data = length 220, hash 32087455\n  sample 66:\n    time = 2651378\n    flags = 1\n    data = length 213, hash 4E5649A8\n  sample 67:\n    time = 2674597\n    flags = 1\n    data = length 213, hash 5F12FDCF\n  sample 68:\n    time = 2697816\n    flags = 1\n    data = length 204, hash 1E895C2A\n  sample 69:\n    time = 2721035\n    flags = 1\n    data = length 219, hash 45382270\n  sample 70:\n    time = 2744254\n    flags = 1\n    data = length 205, hash D66C6A1D\n  sample 71:\n    time = 2767473\n    flags = 1\n    data = length 204, hash 467AD01F\n  sample 72:\n    time = 2790692\n    flags = 1\n    data = length 211, hash F0435574\n  sample 73:\n    time = 2813911\n    flags = 1\n    data = length 206, hash 8C96B75F\n  sample 74:\n    time = 2837130\n    flags = 1\n    data = length 200, hash 82553248\n  sample 75:\n    time = 2860349\n    flags = 1\n    data = length 180, hash 1E51E6CE\n  sample 76:\n    time = 2883568\n    flags = 1\n    data = length 196, hash 33151DC4\n  sample 77:\n    time = 2906787\n    flags = 1\n    data = length 197, hash 1E62A7D6\n  sample 78:\n    time = 2930006\n    flags = 1\n    data = length 206, hash 6A6C4CC9\n  sample 79:\n    time = 2953225\n    flags = 1\n    data = length 209, hash A72FABAA\n  sample 80:\n    time = 2976444\n    flags = 1\n    data = length 217, hash BA33B985\n  sample 81:\n    time = 2999663\n    flags = 1\n    data = length 235, hash 9919CFD9\n  sample 82:\n    time = 3022882\n    flags = 1\n    data = length 236, hash A22C7267\n  sample 83:\n    time = 3046101\n    flags = 1\n    data = length 213, hash 3D57C901\n  sample 84:\n    time = 3069320\n    flags = 1\n    data = length 205, hash 47F68FDE\n  sample 85:\n    time = 3092539\n    flags = 1\n    data = length 210, hash 9A756E9C\n  sample 86:\n    time = 3115758\n    flags = 1\n    data = length 210, hash BD45C31F\n  sample 87:\n    time = 3138977\n    flags = 1\n    data = length 207, hash 8774FF7B\n  sample 88:\n    time = 3162196\n    flags = 1\n    data = length 149, hash 4678C0E5\n  sample 89:\n    time = 3185415\n    flags = 1\n    data = length 161, hash E991035D\n  sample 90:\n    time = 3208634\n    flags = 1\n    data = length 197, hash C3013689\n  sample 91:\n    time = 3231853\n    flags = 1\n    data = length 208, hash E6C0237\n  sample 92:\n    time = 3255072\n    flags = 1\n    data = length 232, hash A330F188\n  sample 93:\n    time = 3278291\n    flags = 1\n    data = length 174, hash 2B69C34E\ntrack 1:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = application/id3\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample_cbs.adts.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 3356772\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 0\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 2, hash 5F7\n  total output bytes = 10161\n  sample count = 49\n  sample 0:\n    time = 2237848\n    flags = 1\n    data = length 224, hash 953502D8\n  sample 1:\n    time = 2261067\n    flags = 1\n    data = length 214, hash 92B87FE7\n  sample 2:\n    time = 2284286\n    flags = 1\n    data = length 213, hash BB0C8D86\n  sample 3:\n    time = 2307505\n    flags = 1\n    data = length 206, hash 9AD21017\n  sample 4:\n    time = 2330724\n    flags = 1\n    data = length 209, hash C479FE94\n  sample 5:\n    time = 2353943\n    flags = 1\n    data = length 220, hash 3033DCE1\n  sample 6:\n    time = 2377162\n    flags = 1\n    data = length 217, hash 7D589C94\n  sample 7:\n    time = 2400381\n    flags = 1\n    data = length 216, hash AAF6C183\n  sample 8:\n    time = 2423600\n    flags = 1\n    data = length 206, hash 1EE1207F\n  sample 9:\n    time = 2446819\n    flags = 1\n    data = length 204, hash 4BEB1210\n  sample 10:\n    time = 2470038\n    flags = 1\n    data = length 213, hash 21A841C9\n  sample 11:\n    time = 2493257\n    flags = 1\n    data = length 207, hash B80B0424\n  sample 12:\n    time = 2516476\n    flags = 1\n    data = length 212, hash 4785A1C3\n  sample 13:\n    time = 2539695\n    flags = 1\n    data = length 205, hash 59BF7229\n  sample 14:\n    time = 2562914\n    flags = 1\n    data = length 208, hash FA313DDE\n  sample 15:\n    time = 2586133\n    flags = 1\n    data = length 211, hash 190D85FD\n  sample 16:\n    time = 2609352\n    flags = 1\n    data = length 211, hash BA050052\n  sample 17:\n    time = 2632571\n    flags = 1\n    data = length 211, hash F3080F10\n  sample 18:\n    time = 2655790\n    flags = 1\n    data = length 210, hash F41B7BE7\n  sample 19:\n    time = 2679009\n    flags = 1\n    data = length 207, hash 2176C97E\n  sample 20:\n    time = 2702228\n    flags = 1\n    data = length 220, hash 32087455\n  sample 21:\n    time = 2725447\n    flags = 1\n    data = length 213, hash 4E5649A8\n  sample 22:\n    time = 2748666\n    flags = 1\n    data = length 213, hash 5F12FDCF\n  sample 23:\n    time = 2771885\n    flags = 1\n    data = length 204, hash 1E895C2A\n  sample 24:\n    time = 2795104\n    flags = 1\n    data = length 219, hash 45382270\n  sample 25:\n    time = 2818323\n    flags = 1\n    data = length 205, hash D66C6A1D\n  sample 26:\n    time = 2841542\n    flags = 1\n    data = length 204, hash 467AD01F\n  sample 27:\n    time = 2864761\n    flags = 1\n    data = length 211, hash F0435574\n  sample 28:\n    time = 2887980\n    flags = 1\n    data = length 206, hash 8C96B75F\n  sample 29:\n    time = 2911199\n    flags = 1\n    data = length 200, hash 82553248\n  sample 30:\n    time = 2934418\n    flags = 1\n    data = length 180, hash 1E51E6CE\n  sample 31:\n    time = 2957637\n    flags = 1\n    data = length 196, hash 33151DC4\n  sample 32:\n    time = 2980856\n    flags = 1\n    data = length 197, hash 1E62A7D6\n  sample 33:\n    time = 3004075\n    flags = 1\n    data = length 206, hash 6A6C4CC9\n  sample 34:\n    time = 3027294\n    flags = 1\n    data = length 209, hash A72FABAA\n  sample 35:\n    time = 3050513\n    flags = 1\n    data = length 217, hash BA33B985\n  sample 36:\n    time = 3073732\n    flags = 1\n    data = length 235, hash 9919CFD9\n  sample 37:\n    time = 3096951\n    flags = 1\n    data = length 236, hash A22C7267\n  sample 38:\n    time = 3120170\n    flags = 1\n    data = length 213, hash 3D57C901\n  sample 39:\n    time = 3143389\n    flags = 1\n    data = length 205, hash 47F68FDE\n  sample 40:\n    time = 3166608\n    flags = 1\n    data = length 210, hash 9A756E9C\n  sample 41:\n    time = 3189827\n    flags = 1\n    data = length 210, hash BD45C31F\n  sample 42:\n    time = 3213046\n    flags = 1\n    data = length 207, hash 8774FF7B\n  sample 43:\n    time = 3236265\n    flags = 1\n    data = length 149, hash 4678C0E5\n  sample 44:\n    time = 3259484\n    flags = 1\n    data = length 161, hash E991035D\n  sample 45:\n    time = 3282703\n    flags = 1\n    data = length 197, hash C3013689\n  sample 46:\n    time = 3305922\n    flags = 1\n    data = length 208, hash E6C0237\n  sample 47:\n    time = 3329141\n    flags = 1\n    data = length 232, hash A330F188\n  sample 48:\n    time = 3352360\n    flags = 1\n    data = length 174, hash 2B69C34E\ntrack 1:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = application/id3\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample_cbs.adts.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 3356772\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 0\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 2, hash 5F7\n  total output bytes = 174\n  sample count = 1\n  sample 0:\n    time = 3356772\n    flags = 1\n    data = length 174, hash 2B69C34E\ntrack 1:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = application/id3\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ts/sample_cbs.adts.unklen.dump",
    "content": "seekMap:\n  isSeekable = false\n  duration = UNSET TIME\n  getPosition(0) = [[timeUs=0, position=0]]\nnumberOfTracks = 2\ntrack 0:\n  format:\n    bitrate = -1\n    id = 0\n    containerMimeType = null\n    sampleMimeType = audio/mp4a-latm\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n      data = length 2, hash 5F7\n  total output bytes = 30797\n  sample count = 144\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 23, hash 47DE9131\n  sample 1:\n    time = 23219\n    flags = 1\n    data = length 6, hash 31CF3A46\n  sample 2:\n    time = 46438\n    flags = 1\n    data = length 6, hash 31CF3A46\n  sample 3:\n    time = 69657\n    flags = 1\n    data = length 6, hash 31CF3A46\n  sample 4:\n    time = 92876\n    flags = 1\n    data = length 6, hash 31EC5206\n  sample 5:\n    time = 116095\n    flags = 1\n    data = length 171, hash 4F6478F6\n  sample 6:\n    time = 139314\n    flags = 1\n    data = length 202, hash AF4068A3\n  sample 7:\n    time = 162533\n    flags = 1\n    data = length 210, hash E4C10618\n  sample 8:\n    time = 185752\n    flags = 1\n    data = length 217, hash 9ECCD0D9\n  sample 9:\n    time = 208971\n    flags = 1\n    data = length 212, hash 6BAC2CD9\n  sample 10:\n    time = 232190\n    flags = 1\n    data = length 223, hash 188B6010\n  sample 11:\n    time = 255409\n    flags = 1\n    data = length 222, hash C1A04D0C\n  sample 12:\n    time = 278628\n    flags = 1\n    data = length 220, hash D65F9768\n  sample 13:\n    time = 301847\n    flags = 1\n    data = length 227, hash B96C9E14\n  sample 14:\n    time = 325066\n    flags = 1\n    data = length 229, hash 9FB09972\n  sample 15:\n    time = 348285\n    flags = 1\n    data = length 220, hash 2271F053\n  sample 16:\n    time = 371504\n    flags = 1\n    data = length 226, hash 5EDD2F4F\n  sample 17:\n    time = 394723\n    flags = 1\n    data = length 239, hash 957510E0\n  sample 18:\n    time = 417942\n    flags = 1\n    data = length 224, hash 718A8F47\n  sample 19:\n    time = 441161\n    flags = 1\n    data = length 225, hash 5E11E293\n  sample 20:\n    time = 464380\n    flags = 1\n    data = length 227, hash FCE50D27\n  sample 21:\n    time = 487599\n    flags = 1\n    data = length 212, hash 77908C40\n  sample 22:\n    time = 510818\n    flags = 1\n    data = length 227, hash 34C4EB32\n  sample 23:\n    time = 534037\n    flags = 1\n    data = length 231, hash 95488307\n  sample 24:\n    time = 557256\n    flags = 1\n    data = length 226, hash 97F12D6F\n  sample 25:\n    time = 580475\n    flags = 1\n    data = length 236, hash 91A9D9A2\n  sample 26:\n    time = 603694\n    flags = 1\n    data = length 227, hash 27A608F9\n  sample 27:\n    time = 626913\n    flags = 1\n    data = length 229, hash 57DAAE4\n  sample 28:\n    time = 650132\n    flags = 1\n    data = length 235, hash ED30AC34\n  sample 29:\n    time = 673351\n    flags = 1\n    data = length 227, hash BD3D6280\n  sample 30:\n    time = 696570\n    flags = 1\n    data = length 233, hash 694B1087\n  sample 31:\n    time = 719789\n    flags = 1\n    data = length 232, hash 1EDFE047\n  sample 32:\n    time = 743008\n    flags = 1\n    data = length 228, hash E2A831F4\n  sample 33:\n    time = 766227\n    flags = 1\n    data = length 231, hash 757E6012\n  sample 34:\n    time = 789446\n    flags = 1\n    data = length 223, hash 4003D791\n  sample 35:\n    time = 812665\n    flags = 1\n    data = length 232, hash 3CF9A07C\n  sample 36:\n    time = 835884\n    flags = 1\n    data = length 228, hash 25AC3FF7\n  sample 37:\n    time = 859103\n    flags = 1\n    data = length 220, hash 2C1824CE\n  sample 38:\n    time = 882322\n    flags = 1\n    data = length 229, hash 46FDD8FB\n  sample 39:\n    time = 905541\n    flags = 1\n    data = length 237, hash F6988018\n  sample 40:\n    time = 928760\n    flags = 1\n    data = length 242, hash 60436B6B\n  sample 41:\n    time = 951979\n    flags = 1\n    data = length 275, hash 90EDFA8E\n  sample 42:\n    time = 975198\n    flags = 1\n    data = length 242, hash 5C86EFCB\n  sample 43:\n    time = 998417\n    flags = 1\n    data = length 233, hash E0A51B82\n  sample 44:\n    time = 1021636\n    flags = 1\n    data = length 235, hash 590DF14F\n  sample 45:\n    time = 1044855\n    flags = 1\n    data = length 238, hash 69AF4E6E\n  sample 46:\n    time = 1068074\n    flags = 1\n    data = length 235, hash E745AE8D\n  sample 47:\n    time = 1091293\n    flags = 1\n    data = length 223, hash 295F2A13\n  sample 48:\n    time = 1114512\n    flags = 1\n    data = length 228, hash E2F47B21\n  sample 49:\n    time = 1137731\n    flags = 1\n    data = length 229, hash 262C3CFE\n  sample 50:\n    time = 1160950\n    flags = 1\n    data = length 232, hash 4B5BF5E8\n  sample 51:\n    time = 1184169\n    flags = 1\n    data = length 233, hash F3D80836\n  sample 52:\n    time = 1207388\n    flags = 1\n    data = length 237, hash 32E0A11E\n  sample 53:\n    time = 1230607\n    flags = 1\n    data = length 228, hash E1B89F13\n  sample 54:\n    time = 1253826\n    flags = 1\n    data = length 237, hash 8BDD9E38\n  sample 55:\n    time = 1277045\n    flags = 1\n    data = length 235, hash 3C84161F\n  sample 56:\n    time = 1300264\n    flags = 1\n    data = length 227, hash A47E1789\n  sample 57:\n    time = 1323483\n    flags = 1\n    data = length 228, hash 869FDFD3\n  sample 58:\n    time = 1346702\n    flags = 1\n    data = length 233, hash 272ECE2\n  sample 59:\n    time = 1369921\n    flags = 1\n    data = length 227, hash DB6B9618\n  sample 60:\n    time = 1393140\n    flags = 1\n    data = length 212, hash 63214325\n  sample 61:\n    time = 1416359\n    flags = 1\n    data = length 221, hash 9BA588A1\n  sample 62:\n    time = 1439578\n    flags = 1\n    data = length 225, hash 21EFD50C\n  sample 63:\n    time = 1462797\n    flags = 1\n    data = length 231, hash F3AD0BF\n  sample 64:\n    time = 1486016\n    flags = 1\n    data = length 224, hash 822C9210\n  sample 65:\n    time = 1509235\n    flags = 1\n    data = length 195, hash D4EF53EE\n  sample 66:\n    time = 1532454\n    flags = 1\n    data = length 195, hash A816647A\n  sample 67:\n    time = 1555673\n    flags = 1\n    data = length 184, hash 9A2B7E6\n  sample 68:\n    time = 1578892\n    flags = 1\n    data = length 210, hash 956E3600\n  sample 69:\n    time = 1602111\n    flags = 1\n    data = length 234, hash 35CFDA0A\n  sample 70:\n    time = 1625330\n    flags = 1\n    data = length 239, hash 9E15AC1E\n  sample 71:\n    time = 1648549\n    flags = 1\n    data = length 228, hash F3B70641\n  sample 72:\n    time = 1671768\n    flags = 1\n    data = length 237, hash 124E3194\n  sample 73:\n    time = 1694987\n    flags = 1\n    data = length 231, hash 950CD7C8\n  sample 74:\n    time = 1718206\n    flags = 1\n    data = length 236, hash A12E49AF\n  sample 75:\n    time = 1741425\n    flags = 1\n    data = length 242, hash 43BC9C24\n  sample 76:\n    time = 1764644\n    flags = 1\n    data = length 241, hash DCF0B17\n  sample 77:\n    time = 1787863\n    flags = 1\n    data = length 251, hash C0B99968\n  sample 78:\n    time = 1811082\n    flags = 1\n    data = length 245, hash 9B38ED1C\n  sample 79:\n    time = 1834301\n    flags = 1\n    data = length 238, hash 1BA69079\n  sample 80:\n    time = 1857520\n    flags = 1\n    data = length 233, hash 44C8C6BF\n  sample 81:\n    time = 1880739\n    flags = 1\n    data = length 231, hash EABBEE02\n  sample 82:\n    time = 1903958\n    flags = 1\n    data = length 226, hash D09C44FB\n  sample 83:\n    time = 1927177\n    flags = 1\n    data = length 235, hash BE6A6608\n  sample 84:\n    time = 1950396\n    flags = 1\n    data = length 235, hash 2735F454\n  sample 85:\n    time = 1973615\n    flags = 1\n    data = length 238, hash B160DFE7\n  sample 86:\n    time = 1996834\n    flags = 1\n    data = length 232, hash 1B217D2E\n  sample 87:\n    time = 2020053\n    flags = 1\n    data = length 251, hash D1C14CEA\n  sample 88:\n    time = 2043272\n    flags = 1\n    data = length 256, hash 97C87F08\n  sample 89:\n    time = 2066491\n    flags = 1\n    data = length 237, hash 6645DB3\n  sample 90:\n    time = 2089710\n    flags = 1\n    data = length 235, hash 727A1C82\n  sample 91:\n    time = 2112929\n    flags = 1\n    data = length 234, hash 5015F8B5\n  sample 92:\n    time = 2136148\n    flags = 1\n    data = length 241, hash 9102144B\n  sample 93:\n    time = 2159367\n    flags = 1\n    data = length 224, hash 64E0D807\n  sample 94:\n    time = 2182586\n    flags = 1\n    data = length 228, hash 1922B852\n  sample 95:\n    time = 2205805\n    flags = 1\n    data = length 224, hash 953502D8\n  sample 96:\n    time = 2229024\n    flags = 1\n    data = length 214, hash 92B87FE7\n  sample 97:\n    time = 2252243\n    flags = 1\n    data = length 213, hash BB0C8D86\n  sample 98:\n    time = 2275462\n    flags = 1\n    data = length 206, hash 9AD21017\n  sample 99:\n    time = 2298681\n    flags = 1\n    data = length 209, hash C479FE94\n  sample 100:\n    time = 2321900\n    flags = 1\n    data = length 220, hash 3033DCE1\n  sample 101:\n    time = 2345119\n    flags = 1\n    data = length 217, hash 7D589C94\n  sample 102:\n    time = 2368338\n    flags = 1\n    data = length 216, hash AAF6C183\n  sample 103:\n    time = 2391557\n    flags = 1\n    data = length 206, hash 1EE1207F\n  sample 104:\n    time = 2414776\n    flags = 1\n    data = length 204, hash 4BEB1210\n  sample 105:\n    time = 2437995\n    flags = 1\n    data = length 213, hash 21A841C9\n  sample 106:\n    time = 2461214\n    flags = 1\n    data = length 207, hash B80B0424\n  sample 107:\n    time = 2484433\n    flags = 1\n    data = length 212, hash 4785A1C3\n  sample 108:\n    time = 2507652\n    flags = 1\n    data = length 205, hash 59BF7229\n  sample 109:\n    time = 2530871\n    flags = 1\n    data = length 208, hash FA313DDE\n  sample 110:\n    time = 2554090\n    flags = 1\n    data = length 211, hash 190D85FD\n  sample 111:\n    time = 2577309\n    flags = 1\n    data = length 211, hash BA050052\n  sample 112:\n    time = 2600528\n    flags = 1\n    data = length 211, hash F3080F10\n  sample 113:\n    time = 2623747\n    flags = 1\n    data = length 210, hash F41B7BE7\n  sample 114:\n    time = 2646966\n    flags = 1\n    data = length 207, hash 2176C97E\n  sample 115:\n    time = 2670185\n    flags = 1\n    data = length 220, hash 32087455\n  sample 116:\n    time = 2693404\n    flags = 1\n    data = length 213, hash 4E5649A8\n  sample 117:\n    time = 2716623\n    flags = 1\n    data = length 213, hash 5F12FDCF\n  sample 118:\n    time = 2739842\n    flags = 1\n    data = length 204, hash 1E895C2A\n  sample 119:\n    time = 2763061\n    flags = 1\n    data = length 219, hash 45382270\n  sample 120:\n    time = 2786280\n    flags = 1\n    data = length 205, hash D66C6A1D\n  sample 121:\n    time = 2809499\n    flags = 1\n    data = length 204, hash 467AD01F\n  sample 122:\n    time = 2832718\n    flags = 1\n    data = length 211, hash F0435574\n  sample 123:\n    time = 2855937\n    flags = 1\n    data = length 206, hash 8C96B75F\n  sample 124:\n    time = 2879156\n    flags = 1\n    data = length 200, hash 82553248\n  sample 125:\n    time = 2902375\n    flags = 1\n    data = length 180, hash 1E51E6CE\n  sample 126:\n    time = 2925594\n    flags = 1\n    data = length 196, hash 33151DC4\n  sample 127:\n    time = 2948813\n    flags = 1\n    data = length 197, hash 1E62A7D6\n  sample 128:\n    time = 2972032\n    flags = 1\n    data = length 206, hash 6A6C4CC9\n  sample 129:\n    time = 2995251\n    flags = 1\n    data = length 209, hash A72FABAA\n  sample 130:\n    time = 3018470\n    flags = 1\n    data = length 217, hash BA33B985\n  sample 131:\n    time = 3041689\n    flags = 1\n    data = length 235, hash 9919CFD9\n  sample 132:\n    time = 3064908\n    flags = 1\n    data = length 236, hash A22C7267\n  sample 133:\n    time = 3088127\n    flags = 1\n    data = length 213, hash 3D57C901\n  sample 134:\n    time = 3111346\n    flags = 1\n    data = length 205, hash 47F68FDE\n  sample 135:\n    time = 3134565\n    flags = 1\n    data = length 210, hash 9A756E9C\n  sample 136:\n    time = 3157784\n    flags = 1\n    data = length 210, hash BD45C31F\n  sample 137:\n    time = 3181003\n    flags = 1\n    data = length 207, hash 8774FF7B\n  sample 138:\n    time = 3204222\n    flags = 1\n    data = length 149, hash 4678C0E5\n  sample 139:\n    time = 3227441\n    flags = 1\n    data = length 161, hash E991035D\n  sample 140:\n    time = 3250660\n    flags = 1\n    data = length 197, hash C3013689\n  sample 141:\n    time = 3273879\n    flags = 1\n    data = length 208, hash E6C0237\n  sample 142:\n    time = 3297098\n    flags = 1\n    data = length 232, hash A330F188\n  sample 143:\n    time = 3320317\n    flags = 1\n    data = length 174, hash 2B69C34E\ntrack 1:\n  format:\n    bitrate = -1\n    id = 1\n    containerMimeType = null\n    sampleMimeType = application/id3\n    maxInputSize = -1\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = -1\n    sampleRate = -1\n    pcmEncoding = -1\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 0\n  sample count = 0\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/bitmap_percentage_region.xml",
    "content": "<tt xmlns=\"http://www.w3.org/ns/ttml\" xmlns:ttm=\"http://www.w3.org/ns/ttml#metadata\" xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xmlns:smpte=\"http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt\" xml:lang=\"eng\">\n    <head>\n        <metadata>\n            <smpte:image imagetype=\"PNG\" encoding=\"Base64\" xml:id=\"img_0\">\n                iVBORw0KGgoAAAANSUhEUgAAAXAAAABICAYAAADvR65LAAAACXBIWXMAAAsTAAALEwEAmpwYAAANIUlEQVR4nO2d/5GjSA+GNVVfALMh4EyucAIXxDiGCWFiwKngukwghA1h74/vxMia/t3qNtjvU7W1u8Ygtbp53agFvL2/v/8hAAAAh+N/j3YAAABAGRBwAAA4KBBwAAA4KC8j4MMwEBHRuq4P9qQ/vdv+yrFOBTECFhxawFNPgnEc6Xw+ExHRPM90u92a+7YXerZ9HEc6nU50Op2IiGhZFprnGSKleOXxCGwxE/BxHLd/uwYkb1+WpeqE1iLBLMuy/XEdX54wRyEW01RCbbeyMQwDnc/nzRYRBfvjWUmN517Ho9V4eDTP0o4YJgLOJ+/pdHLOKMZxpMvlQkRE0zQVn9B8HC3eknmeq2zsBSmI8zw3EUJLG1K85Y/psiyWLu+aHn3WkqP7zzxLO1IwEXB92ezbzsEsQYu3Fge2wX+etcNKaC2iwzDc9cs0TU896wFgL5gKuEug9cldIqxyhk/0c5bNNng7xOMbGYuWcZF9jPgD0IdqAY8JdGx2nkJsYWxdV1rXFcLhoXVcQiktAEA7igScqz+I6MeCotwmt7N4l5ZP+VIntZT4k7NP7Luu7fqKQsc4N3Ytbej+1p9pm67PYrYs4l3SDzlYx7PVeAwdI8f/Hn1Zsm9tP9SOtdz21WYosgVclkAR3QdIpjnkdv77crls4ltaPlU72/N1bKzkTadxUvaRsXItrLrKyfgzPQhLY9fShus4cmzIdms/2Cbvp+NTG2+XDT4Gp3lcNlLbHotDajx7jkcr/3v0Zcm+pf1gNdb02A+NI+mrhO2mjr9sAT+dTj8cldtCAqtn4yVwh5T+ALDvLj9Pp5NTaIdhoMvl4my3r/KGbZ3PZ1qWxbuw6ion89kpzTO3suEbC7IaRbbb98Ovv1cab2lDnsCaZVl+nOjaBlFe6qk0nj3Ho6X/PfqyZN/cdliNtZxxFKqmy52NZws4/0IwoXpWKdhStHMFiG2yLT75SsqE2B9XG/h4evYgO1jvJ39FLXLNXMWhxVHarU0hWdmQcdQlhPrfEletuEyxWcQ71M/yhJPb+XP+k9qfNfFsMR7ZXuo5UeN/bV/6fC3ZN7cd1mONjy3HEJdP8/5sU44/uV8u2QJ+u902Zz4+PojIPe2XjnJgS3N067rSPM/O3JYMXsol2TzPd6I/DAMty7IFWp+4crDI6he5Hw8YCwFf15Wu1+uWS+OT2LK23coGjwV5c1VKX8v+4v/LWbpFvGP9zPblmJEzo9PplJTTJaqLp+V4lNuXZaHr9Rr1vdb/0r6M+Vqyb247rMeaFmk50eRtevLgSjdxW1KoqkKJXR5KR2vFh4/vynHJf8cuH7Wv67pug1BfCukFBtkO/lGR/ozjiEoYig8+LZyMZbxj/ewSDbm9FzXjUZ78epLjmr23ILUvc3zt0c7WY0366JsMuK48mi9iMjIAOemTGm632zawXTnMlEueHF907kzvyyebLwcG3Pgu7y3jbVmp1JKa8ejL70vhaC3gqX2Z42uPdrYea/pHWPooNYy/V9pPxQIuBVrDq7rsrCWy5lteusv8plU6g48nbWtk+3LypsAN4h2G48OTFR0PGb9Hx6fG1x7tbDnWfIKshf3r62ubAJfc9p8s4PohUvJvmUti5Hadd7SaFXAOVua82GavdIbuZNAWxPubI1351fj6qHa2GGvrutI0TbQsy12OnOg7Z9+kjNAl0nKbD520b4Er5wTAM5OSmtxLGqnG1yO1MxVebNV5dpkaJkqraksWcLnSHMofhbbV5HpS/Ou9AEV0/8t8tIF0RBDv/1Nb2dWTGl8f2c6asea6Q1nDQk70fWOPq3IlRLKAy/IcLq/hMhgJp0x4u5x1t+4Ea/HW+Sq9kiwXcvn7oBzEO8yjJikl1Pjao509xlpokVQjq+ykDzHNzFrElHWY7Jg2oJ22EG25KOrLoctLD6vKF70SfT6f70rPdHrIZ9M1EGWbYvSoKOhVtRDCKt57oEU8ZXxC5XMWz0ap9b/GV8t2+tphOdZ4kVXa0Hokt43jGNTOHIpupWeHXY3i7ZYnmMwNuWzLhQAim7pzeSxd6cK29fPJXXWejBbr0JoC0f2g1O2z+mHsYSOXmng/mh7xlPGRN8oxfJ7M85x8I08r/2t8rdk3tR1WY01mHLRNmXom+r5ZjO3IK4GSeBcLuEugLZ79HUM3kn1ipmkyXSzlSxvuJA5+ik3dOVz3mfpL63p8AH+ee3I+0kYONfHeA63j6YsP0f154EoL9Pa/xtfadqa0w3Ks+c5v12STv+9Dp55DFAl4LD1ilcJg5G2orudZsL1QmWLIH+mv63syP6UvjXx3ovF+8upB2tPPEPH5patrSuIaa3utjVj8UvyQlMY7xb6ln759U+LZajzGzoMe/lv5WrNvajtqxpq0JdMxof154it1TB8np+/e3t/f/yR98z94lu0TcIv8W4p9TWzGzy85DT35LNQul+3UqwzffvLzmF+S3Pr21LbX2EiJX8yPmF8p8a7t55R25Prt8qfFeCSyufK18D/lmKXnT+q+OeM6d6yN40hfX19E9D1Lzz2HdMaCKF83swUcAABeHSngn5+fD7vj1eSdmAAAAPoDAQcAgIMCAQcAgIMCAQcAgIMCAQcAgAL2cCcwBBwAADKRVSePfOY6yggBAOCgvP39B/od4p9fvxAgAB7EX79/vz3ahz2DFAoAABwUCDgAABwUCDgAABwUCPhOaP0QsBb08Hnvcdm7f6141XbvDQh4Q1IHOb8Pj4iy3kj9SHr4vPe47N2/Vrxqu/cIBNyYcRzvngvMyGcY+14JR0S7fVGBi5DP/LhRoro62UfFJdX/I/abBa/a7r0BATeEX5cUeuMOvwj6mS89+X2f/D7DPb7+LMTR/QevAwTcCC3erlcpyT+h92cehR4+HzEurwD6ZR9AwA3gGZt8756cZfObN3xv39nLbbk59PD5iHF5BdAv+wECboDrXXhyhr2uK63rGhzsRzwRevh8xLi8AuiXfQABN8KXOkklVLHi25ZbyuX6fk05mO948gdNL+jm2ukRF72vhf8lPliW5vn6JnRs3ifFh979AtxAwI0JLWD6CJVl6W1sw/WW+9DLhF37SH9zy8FcPvNnWgAvl8tmL8dO67j47JX47xP8mA86/Vbit68d7K/2Sy+iy+9LH5Zlcba1d78APxBwY/iEzxXEUFkWb5Mi4bLrqm5JqYzx2S3xWQsB+yavUPYQl5g9fYyY/9qXFB+GYaDL5eK1WVNjLY+p/ZeL6LLiRsM/WqH29uoX4AYCbgDPKHjg8ozKugztdDptthhpU89q9OxOpndcteq1LMtC0zRtbWekvy2qF3Lj4qPG/5K+keKt9+PPa8eObIe8F0GjhViO4VIfrPoF+IGAG7CuK83z7Myd8iC2uGyc5/nuB2EYBlqWhS6Xy2ZTzpakEPC+t9tty/P6Zl6lrOtK1+t1y3XySdp6ppUblxb+1/YN25C2WTyv12t+UP5Djj3+v15gn6Zp+zcR3flQ8yNv1S/ADwTcCB6Irhyq/HfNZbG+fF/XdTtB9YyaRZr3k3a5KsZ6Bv4ocuKyBx9038gfCD0ZqJ2psoiG9tfb2Hei7/FbYn8P/fLsQMANud1u2+DUQk50P6MpEfGc9INv0bL0eHtmD+0o7RseL67jhW78yvErp3ImlLcusQ3aAgE3RtZ8y+oPubBzPp+7XDpKkUCucV9w3/CPuuuuXfn/VuNFVyhZCjhoDwS8Ibfbbcs5E92vzo/jiPwfIKI2C8opyAol1wInRHz/QMA7oPOaODEAk3LjV4tUhBbvaZrurtQ+Pj62xUawXyDgnZCLNwAwehGzF/rGHn01iPz1MYCAd6S3eMsfjNht1KAfe/gxl+sj4LhAwA3gG2aIyFuy5buhphVSJHw3kvQQkNoqikfTwn8up4uVCbZ8doguE9QzcFwpHgMIuAG6bNC1GKTv7GstaLKWl4ju8p1E9TdpxGwzuu1HqIjp4b9cE9F9Q/TdP/M8V93I40Pbkp/pNoP9AgE3Rp/sRPezmWmaur2GikWCxYAfytRjduV6tAB/3kKQrGntP894WbzlA7N0CWGL9Jd8/EPIPtg3EHAD+GTU9d46ZRK6nT6UUolt4+36e/I2adet/UTuhzelEvNLV96UpI1axCXVbor/NT7Iu3ddKbaaxy/E2sxjY1mWH1eP8imCJcdv2S/gnre///x5tA+75p9fv7IC5Mstxy69+SW6vsd3+rZJmyEb8iW97M/5fN5KxT4/P7Pr0lP9kljasIhLiBT/LXxw2alN1cT8cn1X2ib6FvDc2Fv2y1+/f79F3H9pIOARcgX8SIzjSF9fX0RUJuAAtAYCHgYplBcGuU4Ajg0E/ImRl8b6clU/EQ8AcDwg4E+MrECRz2Ym+vnSAIg4AMcDAv7E6OdK88KRrpDBm1EAOCYQ8CeGH6JF9PNFE0Q/X/QAADgWqEKJ8AxVKJzv1nXgR7grErw2qEIJAwGP8AwCDsBRgYCH+RdHEwkWLXE/8gAAAABJRU5ErkJggg==\n            </smpte:image>\n            <smpte:image imagetype=\"PNG\" encoding=\"Base64\" xml:id=\"img_1\">\n                iVBORw0KGgoAAAANSUhEUgAAAaAAAAAkCAIAAABAAnl0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAIBklEQVR4nO1d65GrPAz1nfkKSAu4FWglqeXWQFohrZhW7o8zaBS/kB+QhE/nx86uA5YsnT1+CHb/3G43o1AoFFfEf592QKFQKI6CCpxCobgsVOAUCsVloQL3SxiGwRizruuht5TiBBM/hJ+Lxs85XAQVuJ/BOI7TNBljlmV5vV4H3XKCVxfGz0Xj5xwuRaXAjeOIb7ygoN05J58QqCuOoh7+PyAu8sZULjK3lOIEE5fBB6ORT1MK105fjcANwzBNk7XWU/1xHB+PhzFmnmehPOEWa63X7pxblsU5d8lZpSOQi2maEK4jZoUTTCjaoWmKokbgrLWQJOdc2I74VnRLd6Gfx+OBWUU1jlAXWMWZ0Bx9FeoFzhOyYRhI9Spmj2VZaJGM/jEdoU/VOMOCoNH4WmiOvg3FApcSstSyTg5ODpwmQOCmaVK6ABqH74fm6KsgFTjUkg0TMt5I7VC39sIzWGI3jOMY5Q05kLIVeiL0TXJZx4c2hO3hj5QOnpeObh9qopon8rTmTVTctbtT2R1U46hTDlekqdHDipHWpanuSkAkcFRLNix8tH+kdnx9PB6QucbC8+v1ggkgLNeSS8YY51xYkeAlcPOeeBQxUkShnqPdUo0l31WIYRhQhHHOPZ/PqJ/c1v1+hxUUbbzL8COndSbyIfUlbh9kojqAYbhCi8iXZyLqMPXj1cRS6aBoUAs8D6+JmpNQa3fI0XuL0pRC9/T1SlOR0RAigeOJ4Y3cM6+9y1Hrsiywa60dhoGvXHBCxy+GRS86dDt95X56zIYAhd1aa0mPotfYoKCcAepcfDh8LNQJL1XzKw2r6Idu0OIiE4dMBKLe9jXRHkCTTatzjv+2cxPmnR4IO/LrBSF8ciJa7o8u5aJPXUiolYKE7fI0pXBE+rqkqdSoB5HAQS+5017+SNRI17o84YEOU0rKfaDTuujGAb55Q4DQcNGkINJAaP0IPeLX8N7orrxY0KfoEz9623wSPs7RVHDmeeZzD7kU3iKJwAkmGgMosUhx82pWQB0n4TZ1S9wouj1Prd1OMmwvSlMKjemLClOmZ3maGjkjErjX6wWT9/vdBCtzSj8C3fcBHAicMYZPs+u6zvNsNsmARnhrXQ6UaBGLYRicc9gq8lDyiNPo6MAFhiisy7JgpGQabNudjbEm5Vnk8s2Fj9QtxdF1XZ/PJzlALlVH4AQT7QGUWAQ/6TeTFl9yNfHAucEjIDwPklArg122F6Upher04UqbLgY2pqmRM2VVVJtY91L+Tnu8kBvCJGbeBYKDO4yLvYUh7Qc97V7X1TvRN9u6Bu3rui7Lgq52F0TkTChq9Cn4RLZMp+eqdiNwgoleAdy16JGe2ruM0SPe7i0Sau2iiO11KEofpQnpozk7WgxsSVM7ZwoEjozt7k9PA7mUp++uY5TIzCj4NZ45osIu4XgWMU3xkIIomKDIVhcGn5CaoiBXB7DIYkfAQywZipIioZYQQrbXofp3BJqVmSxbRt3OmQKBIyHjjZigzGFsiy5kxnGkkfdKOfWQCZZl5WPyx1uO5U95vOnXMCHzevPar4H2AH4EfDnz9+9fzEbyN6Ik1MrgCLbXITUQnsru6WvkzI7ARYPr1XSonW+tu6w7UupGR358Mjkz65n5aheOHVHTnIFGbyb8yKL4HLQE8HzgFMxthT9+AN/4LNQuvoHtuzjHqzrO7AhcWJ82QU3aM790/bME3qTB8w3Oof1+v0NeT0BGdyR6RMsBCqPX4eUFrjGAHwEO8vl5EzJo09XDdnwD278E1ZzZETi31Xf5ZjhcKPLGXsdGtPk1QS3ZGDPPM2fVyccxjcXicKWG3kLhO61ocybaA/gpQObM9hTrdPCrhN/AdgmiO62+qObMjsBR+RkzCSq19Cm2pWgnDepFXL4k9NbA3ePID1lSTxLw+kAL+DEc9ex9E/3x19ErgN8AyA1NRZmnTyTUSuGrzmFTA+le8Y8are5hv8hAD56YYE3Bl299J2T+4Dg/0eMn9HxOa/y14ZWgaZqiT9bQNdHKdNErcjxt3uKXtxcNAc4fuixqNNExgB3hMYfvGzjGcYySfxcSaqVQx/aDmJAaSHQh0t1oNWekr2rZ2IMgaO8yMGvt/X53rPRLi3PeOX3PS7c29iZZKfiTNaEhLI/pGjoZoWsQByF9vRFx+Y4KXwb87YiD9rYdTfQKYBd4KabvQy5hunVbIaj0JEFCLYmHebafwIToQGghctCJSiNnCgRu6foH4EIT3rmpe3/QmTdibMS5Lrue1+tlt2Njr2fafWMWRaBD6/I9CM1L5l3saPdqSqK6bG/s0pl3d6XoZaJXALuAS9W0vZQavdJuTyqEH/HDmRQk1Ep5WMT2o5kQpo+cmee5b3UxY9SUcEYqcKl9qHChkULqdrf9yXLPolewt+w1t2jiU53TbMzbn8+ne38HGFdykaXDF+KQ21D0cAzmpdCHlG/54dAsZ4MHFYsikEJHE10CWDGosJHrDh+mCbQMVJzeX0dP+Ry1LqFWiAq2Z9KUQnv6woVIRc+ZW1o48+d2u2U+BrBYC+Wmy7kJP6QEJIsX/q9quKhH/wlOWORKjSj0J/XHW/g18tWW51vKAZOIan44UZ8rIhBFXxONASy1KPTEbe9LRrlk3nctpjBHRkatKIRsrzPRmL5M7jqmKRyakDMigVMoFIpfhP5fVIVCcVmowCkUistCBU6hUFwWKnAKheKyUIFTKBSXhQqcQqG4LFTgFArFZfEPuuTdBr3uWzgAAAAASUVORK5CYII=\n            </smpte:image>\n        </metadata>\n        <styling>\n            <style/>\n        </styling>\n        <layout>\n            <region xml:id=\"region_0\" tts:extent=\"51% 12%\" tts:origin=\"24% 78%\"/>\n            <region xml:id=\"region_1\" tts:extent=\"57% 6%\" tts:origin=\"21% 85%\"/>\n            <region xml:id=\"region_2\" tts:extent=\"51% 12%\" tts:origin=\"24% 28%\"/>\n            <region xml:id=\"region_3\" tts:extent=\"57% 6%\" tts:origin=\"21% 35%\"/>\n        </layout>\n    </head>\n    <body>\n        <div begin=\"00:00:00.200\" end=\"00:00:03.000\" region=\"region_2\" smpte:backgroundImage=\"#img_0\"/>\n        <div begin=\"00:00:03.200\" end=\"00:00:06.937\" region=\"region_3\" smpte:backgroundImage=\"#img_1\"/>\n        <div begin=\"00:00:07.200\" end=\"00:59:03.000\" region=\"region_2\" smpte:backgroundImage=\"#img_0\"/>\n    </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/bitmap_pixel_region.xml",
    "content": "<tt xmlns=\"http://www.w3.org/ns/ttml\" xmlns:ttm=\"http://www.w3.org/ns/ttml#metadata\" xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xmlns:smpte=\"http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt\" xml:lang=\"eng\" tts:extent=\"1280px 720px\">\n    <head>\n        <metadata>\n            <smpte:image imagetype=\"PNG\" encoding=\"Base64\" xml:id=\"img_0\">\n                iVBORw0KGgoAAAANSUhEUgAAAXAAAABICAYAAADvR65LAAAACXBIWXMAAAsTAAALEwEAmpwYAAANIUlEQVR4nO2d/5GjSA+GNVVfALMh4EyucAIXxDiGCWFiwKngukwghA1h74/vxMia/t3qNtjvU7W1u8Ygtbp53agFvL2/v/8hAAAAh+N/j3YAAABAGRBwAAA4KBBwAAA4KC8j4MMwEBHRuq4P9qQ/vdv+yrFOBTECFhxawFNPgnEc6Xw+ExHRPM90u92a+7YXerZ9HEc6nU50Op2IiGhZFprnGSKleOXxCGwxE/BxHLd/uwYkb1+WpeqE1iLBLMuy/XEdX54wRyEW01RCbbeyMQwDnc/nzRYRBfvjWUmN517Ho9V4eDTP0o4YJgLOJ+/pdHLOKMZxpMvlQkRE0zQVn9B8HC3eknmeq2zsBSmI8zw3EUJLG1K85Y/psiyWLu+aHn3WkqP7zzxLO1IwEXB92ezbzsEsQYu3Fge2wX+etcNKaC2iwzDc9cs0TU896wFgL5gKuEug9cldIqxyhk/0c5bNNng7xOMbGYuWcZF9jPgD0IdqAY8JdGx2nkJsYWxdV1rXFcLhoXVcQiktAEA7igScqz+I6MeCotwmt7N4l5ZP+VIntZT4k7NP7Luu7fqKQsc4N3Ytbej+1p9pm67PYrYs4l3SDzlYx7PVeAwdI8f/Hn1Zsm9tP9SOtdz21WYosgVclkAR3QdIpjnkdv77crls4ltaPlU72/N1bKzkTadxUvaRsXItrLrKyfgzPQhLY9fShus4cmzIdms/2Cbvp+NTG2+XDT4Gp3lcNlLbHotDajx7jkcr/3v0Zcm+pf1gNdb02A+NI+mrhO2mjr9sAT+dTj8cldtCAqtn4yVwh5T+ALDvLj9Pp5NTaIdhoMvl4my3r/KGbZ3PZ1qWxbuw6ion89kpzTO3suEbC7IaRbbb98Ovv1cab2lDnsCaZVl+nOjaBlFe6qk0nj3Ho6X/PfqyZN/cdliNtZxxFKqmy52NZws4/0IwoXpWKdhStHMFiG2yLT75SsqE2B9XG/h4evYgO1jvJ39FLXLNXMWhxVHarU0hWdmQcdQlhPrfEletuEyxWcQ71M/yhJPb+XP+k9qfNfFsMR7ZXuo5UeN/bV/6fC3ZN7cd1mONjy3HEJdP8/5sU44/uV8u2QJ+u902Zz4+PojIPe2XjnJgS3N067rSPM/O3JYMXsol2TzPd6I/DAMty7IFWp+4crDI6he5Hw8YCwFf15Wu1+uWS+OT2LK23coGjwV5c1VKX8v+4v/LWbpFvGP9zPblmJEzo9PplJTTJaqLp+V4lNuXZaHr9Rr1vdb/0r6M+Vqyb247rMeaFmk50eRtevLgSjdxW1KoqkKJXR5KR2vFh4/vynHJf8cuH7Wv67pug1BfCukFBtkO/lGR/ozjiEoYig8+LZyMZbxj/ewSDbm9FzXjUZ78epLjmr23ILUvc3zt0c7WY0366JsMuK48mi9iMjIAOemTGm632zawXTnMlEueHF907kzvyyebLwcG3Pgu7y3jbVmp1JKa8ejL70vhaC3gqX2Z42uPdrYea/pHWPooNYy/V9pPxQIuBVrDq7rsrCWy5lteusv8plU6g48nbWtk+3LypsAN4h2G48OTFR0PGb9Hx6fG1x7tbDnWfIKshf3r62ubAJfc9p8s4PohUvJvmUti5Hadd7SaFXAOVua82GavdIbuZNAWxPubI1351fj6qHa2GGvrutI0TbQsy12OnOg7Z9+kjNAl0nKbD520b4Er5wTAM5OSmtxLGqnG1yO1MxVebNV5dpkaJkqraksWcLnSHMofhbbV5HpS/Ou9AEV0/8t8tIF0RBDv/1Nb2dWTGl8f2c6asea6Q1nDQk70fWOPq3IlRLKAy/IcLq/hMhgJp0x4u5x1t+4Ea/HW+Sq9kiwXcvn7oBzEO8yjJikl1Pjao509xlpokVQjq+ykDzHNzFrElHWY7Jg2oJ22EG25KOrLoctLD6vKF70SfT6f70rPdHrIZ9M1EGWbYvSoKOhVtRDCKt57oEU8ZXxC5XMWz0ap9b/GV8t2+tphOdZ4kVXa0Hokt43jGNTOHIpupWeHXY3i7ZYnmMwNuWzLhQAim7pzeSxd6cK29fPJXXWejBbr0JoC0f2g1O2z+mHsYSOXmng/mh7xlPGRN8oxfJ7M85x8I08r/2t8rdk3tR1WY01mHLRNmXom+r5ZjO3IK4GSeBcLuEugLZ79HUM3kn1ipmkyXSzlSxvuJA5+ik3dOVz3mfpL63p8AH+ee3I+0kYONfHeA63j6YsP0f154EoL9Pa/xtfadqa0w3Ks+c5v12STv+9Dp55DFAl4LD1ilcJg5G2orudZsL1QmWLIH+mv63syP6UvjXx3ovF+8upB2tPPEPH5patrSuIaa3utjVj8UvyQlMY7xb6ln759U+LZajzGzoMe/lv5WrNvajtqxpq0JdMxof154it1TB8np+/e3t/f/yR98z94lu0TcIv8W4p9TWzGzy85DT35LNQul+3UqwzffvLzmF+S3Pr21LbX2EiJX8yPmF8p8a7t55R25Prt8qfFeCSyufK18D/lmKXnT+q+OeM6d6yN40hfX19E9D1Lzz2HdMaCKF83swUcAABeHSngn5+fD7vj1eSdmAAAAPoDAQcAgIMCAQcAgIMCAQcAgIMCAQcAgAL2cCcwBBwAADKRVSePfOY6yggBAOCgvP39B/od4p9fvxAgAB7EX79/vz3ahz2DFAoAABwUCDgAABwUCDgAABwUCPhOaP0QsBb08Hnvcdm7f6141XbvDQh4Q1IHOb8Pj4iy3kj9SHr4vPe47N2/Vrxqu/cIBNyYcRzvngvMyGcY+14JR0S7fVGBi5DP/LhRoro62UfFJdX/I/abBa/a7r0BATeEX5cUeuMOvwj6mS89+X2f/D7DPb7+LMTR/QevAwTcCC3erlcpyT+h92cehR4+HzEurwD6ZR9AwA3gGZt8756cZfObN3xv39nLbbk59PD5iHF5BdAv+wECboDrXXhyhr2uK63rGhzsRzwRevh8xLi8AuiXfQABN8KXOkklVLHi25ZbyuX6fk05mO948gdNL+jm2ukRF72vhf8lPliW5vn6JnRs3ifFh979AtxAwI0JLWD6CJVl6W1sw/WW+9DLhF37SH9zy8FcPvNnWgAvl8tmL8dO67j47JX47xP8mA86/Vbit68d7K/2Sy+iy+9LH5Zlcba1d78APxBwY/iEzxXEUFkWb5Mi4bLrqm5JqYzx2S3xWQsB+yavUPYQl5g9fYyY/9qXFB+GYaDL5eK1WVNjLY+p/ZeL6LLiRsM/WqH29uoX4AYCbgDPKHjg8ozKugztdDptthhpU89q9OxOpndcteq1LMtC0zRtbWekvy2qF3Lj4qPG/5K+keKt9+PPa8eObIe8F0GjhViO4VIfrPoF+IGAG7CuK83z7Myd8iC2uGyc5/nuB2EYBlqWhS6Xy2ZTzpakEPC+t9tty/P6Zl6lrOtK1+t1y3XySdp6ppUblxb+1/YN25C2WTyv12t+UP5Djj3+v15gn6Zp+zcR3flQ8yNv1S/ADwTcCB6Irhyq/HfNZbG+fF/XdTtB9YyaRZr3k3a5KsZ6Bv4ocuKyBx9038gfCD0ZqJ2psoiG9tfb2Hei7/FbYn8P/fLsQMANud1u2+DUQk50P6MpEfGc9INv0bL0eHtmD+0o7RseL67jhW78yvErp3ImlLcusQ3aAgE3RtZ8y+oPubBzPp+7XDpKkUCucV9w3/CPuuuuXfn/VuNFVyhZCjhoDwS8Ibfbbcs5E92vzo/jiPwfIKI2C8opyAol1wInRHz/QMA7oPOaODEAk3LjV4tUhBbvaZrurtQ+Pj62xUawXyDgnZCLNwAwehGzF/rGHn01iPz1MYCAd6S3eMsfjNht1KAfe/gxl+sj4LhAwA3gG2aIyFuy5buhphVSJHw3kvQQkNoqikfTwn8up4uVCbZ8doguE9QzcFwpHgMIuAG6bNC1GKTv7GstaLKWl4ju8p1E9TdpxGwzuu1HqIjp4b9cE9F9Q/TdP/M8V93I40Pbkp/pNoP9AgE3Rp/sRPezmWmaur2GikWCxYAfytRjduV6tAB/3kKQrGntP894WbzlA7N0CWGL9Jd8/EPIPtg3EHAD+GTU9d46ZRK6nT6UUolt4+36e/I2adet/UTuhzelEvNLV96UpI1axCXVbor/NT7Iu3ddKbaaxy/E2sxjY1mWH1eP8imCJcdv2S/gnre///x5tA+75p9fv7IC5Mstxy69+SW6vsd3+rZJmyEb8iW97M/5fN5KxT4/P7Pr0lP9kljasIhLiBT/LXxw2alN1cT8cn1X2ib6FvDc2Fv2y1+/f79F3H9pIOARcgX8SIzjSF9fX0RUJuAAtAYCHgYplBcGuU4Ajg0E/ImRl8b6clU/EQ8AcDwg4E+MrECRz2Ym+vnSAIg4AMcDAv7E6OdK88KRrpDBm1EAOCYQ8CeGH6JF9PNFE0Q/X/QAADgWqEKJ8AxVKJzv1nXgR7grErw2qEIJAwGP8AwCDsBRgYCH+RdHEwkWLXE/8gAAAABJRU5ErkJggg==\n            </smpte:image>\n            <smpte:image imagetype=\"PNG\" encoding=\"Base64\" xml:id=\"img_1\">\n                iVBORw0KGgoAAAANSUhEUgAAAaAAAAAkCAIAAABAAnl0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAIBklEQVR4nO1d65GrPAz1nfkKSAu4FWglqeXWQFohrZhW7o8zaBS/kB+QhE/nx86uA5YsnT1+CHb/3G43o1AoFFfEf592QKFQKI6CCpxCobgsVOAUCsVloQL3SxiGwRizruuht5TiBBM/hJ+Lxs85XAQVuJ/BOI7TNBljlmV5vV4H3XKCVxfGz0Xj5xwuRaXAjeOIb7ygoN05J58QqCuOoh7+PyAu8sZULjK3lOIEE5fBB6ORT1MK105fjcANwzBNk7XWU/1xHB+PhzFmnmehPOEWa63X7pxblsU5d8lZpSOQi2maEK4jZoUTTCjaoWmKokbgrLWQJOdc2I74VnRLd6Gfx+OBWUU1jlAXWMWZ0Bx9FeoFzhOyYRhI9Spmj2VZaJGM/jEdoU/VOMOCoNH4WmiOvg3FApcSstSyTg5ODpwmQOCmaVK6ABqH74fm6KsgFTjUkg0TMt5I7VC39sIzWGI3jOMY5Q05kLIVeiL0TXJZx4c2hO3hj5QOnpeObh9qopon8rTmTVTctbtT2R1U46hTDlekqdHDipHWpanuSkAkcFRLNix8tH+kdnx9PB6QucbC8+v1ggkgLNeSS8YY51xYkeAlcPOeeBQxUkShnqPdUo0l31WIYRhQhHHOPZ/PqJ/c1v1+hxUUbbzL8COndSbyIfUlbh9kojqAYbhCi8iXZyLqMPXj1cRS6aBoUAs8D6+JmpNQa3fI0XuL0pRC9/T1SlOR0RAigeOJ4Y3cM6+9y1Hrsiywa60dhoGvXHBCxy+GRS86dDt95X56zIYAhd1aa0mPotfYoKCcAepcfDh8LNQJL1XzKw2r6Idu0OIiE4dMBKLe9jXRHkCTTatzjv+2cxPmnR4IO/LrBSF8ciJa7o8u5aJPXUiolYKE7fI0pXBE+rqkqdSoB5HAQS+5017+SNRI17o84YEOU0rKfaDTuujGAb55Q4DQcNGkINJAaP0IPeLX8N7orrxY0KfoEz9623wSPs7RVHDmeeZzD7kU3iKJwAkmGgMosUhx82pWQB0n4TZ1S9wouj1Prd1OMmwvSlMKjemLClOmZ3maGjkjErjX6wWT9/vdBCtzSj8C3fcBHAicMYZPs+u6zvNsNsmARnhrXQ6UaBGLYRicc9gq8lDyiNPo6MAFhiisy7JgpGQabNudjbEm5Vnk8s2Fj9QtxdF1XZ/PJzlALlVH4AQT7QGUWAQ/6TeTFl9yNfHAucEjIDwPklArg122F6Upher04UqbLgY2pqmRM2VVVJtY91L+Tnu8kBvCJGbeBYKDO4yLvYUh7Qc97V7X1TvRN9u6Bu3rui7Lgq52F0TkTChq9Cn4RLZMp+eqdiNwgoleAdy16JGe2ruM0SPe7i0Sau2iiO11KEofpQnpozk7WgxsSVM7ZwoEjozt7k9PA7mUp++uY5TIzCj4NZ45osIu4XgWMU3xkIIomKDIVhcGn5CaoiBXB7DIYkfAQywZipIioZYQQrbXofp3BJqVmSxbRt3OmQKBIyHjjZigzGFsiy5kxnGkkfdKOfWQCZZl5WPyx1uO5U95vOnXMCHzevPar4H2AH4EfDnz9+9fzEbyN6Ik1MrgCLbXITUQnsru6WvkzI7ARYPr1XSonW+tu6w7UupGR358Mjkz65n5aheOHVHTnIFGbyb8yKL4HLQE8HzgFMxthT9+AN/4LNQuvoHtuzjHqzrO7AhcWJ82QU3aM790/bME3qTB8w3Oof1+v0NeT0BGdyR6RMsBCqPX4eUFrjGAHwEO8vl5EzJo09XDdnwD278E1ZzZETi31Xf5ZjhcKPLGXsdGtPk1QS3ZGDPPM2fVyccxjcXicKWG3kLhO61ocybaA/gpQObM9hTrdPCrhN/AdgmiO62+qObMjsBR+RkzCSq19Cm2pWgnDepFXL4k9NbA3ePID1lSTxLw+kAL+DEc9ex9E/3x19ErgN8AyA1NRZmnTyTUSuGrzmFTA+le8Y8are5hv8hAD56YYE3Bl299J2T+4Dg/0eMn9HxOa/y14ZWgaZqiT9bQNdHKdNErcjxt3uKXtxcNAc4fuixqNNExgB3hMYfvGzjGcYySfxcSaqVQx/aDmJAaSHQh0t1oNWekr2rZ2IMgaO8yMGvt/X53rPRLi3PeOX3PS7c29iZZKfiTNaEhLI/pGjoZoWsQByF9vRFx+Y4KXwb87YiD9rYdTfQKYBd4KabvQy5hunVbIaj0JEFCLYmHebafwIToQGghctCJSiNnCgRu6foH4EIT3rmpe3/QmTdibMS5Lrue1+tlt2Njr2fafWMWRaBD6/I9CM1L5l3saPdqSqK6bG/s0pl3d6XoZaJXALuAS9W0vZQavdJuTyqEH/HDmRQk1Ep5WMT2o5kQpo+cmee5b3UxY9SUcEYqcKl9qHChkULqdrf9yXLPolewt+w1t2jiU53TbMzbn8+ne38HGFdykaXDF+KQ21D0cAzmpdCHlG/54dAsZ4MHFYsikEJHE10CWDGosJHrDh+mCbQMVJzeX0dP+Ry1LqFWiAq2Z9KUQnv6woVIRc+ZW1o48+d2u2U+BrBYC+Wmy7kJP6QEJIsX/q9quKhH/wlOWORKjSj0J/XHW/g18tWW51vKAZOIan44UZ8rIhBFXxONASy1KPTEbe9LRrlk3nctpjBHRkatKIRsrzPRmL5M7jqmKRyakDMigVMoFIpfhP5fVIVCcVmowCkUistCBU6hUFwWKnAKheKyUIFTKBSXhQqcQqG4LFTgFArFZfEPuuTdBr3uWzgAAAAASUVORK5CYII=\n            </smpte:image>\n        </metadata>\n        <styling>\n            <style/>\n        </styling>\n        <layout>\n            <region xml:id=\"region_0\" tts:extent=\"653px 86px\" tts:origin=\"307px 562px\"/>\n            <region xml:id=\"region_1\" tts:extent=\"730px 43px\" tts:origin=\"269px 612px\"/>\n        </layout>\n    </head>\n    <body>\n        <div begin=\"00:00:00.200\" end=\"00:00:03.000\" region=\"region_0\" smpte:backgroundImage=\"#img_0\"/>\n        <div begin=\"00:00:03.200\" end=\"00:00:06.937\" region=\"region_1\" smpte:backgroundImage=\"#img_1\"/>\n    </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/bitmap_unsupported_region.xml",
    "content": "<tt xmlns=\"http://www.w3.org/ns/ttml\" xmlns:ttm=\"http://www.w3.org/ns/ttml#metadata\" xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xmlns:smpte=\"http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt\" xml:lang=\"eng\">\n    <head>\n        <metadata>\n            <smpte:image imagetype=\"PNG\" encoding=\"Base64\" xml:id=\"img_0\">\n                iVBORw0KGgoAAAANSUhEUgAAAXAAAABICAYAAADvR65LAAAACXBIWXMAAAsTAAALEwEAmpwYAAANIUlEQVR4nO2d/5GjSA+GNVVfALMh4EyucAIXxDiGCWFiwKngukwghA1h74/vxMia/t3qNtjvU7W1u8Ygtbp53agFvL2/v/8hAAAAh+N/j3YAAABAGRBwAAA4KBBwAAA4KC8j4MMwEBHRuq4P9qQ/vdv+yrFOBTECFhxawFNPgnEc6Xw+ExHRPM90u92a+7YXerZ9HEc6nU50Op2IiGhZFprnGSKleOXxCGwxE/BxHLd/uwYkb1+WpeqE1iLBLMuy/XEdX54wRyEW01RCbbeyMQwDnc/nzRYRBfvjWUmN517Ho9V4eDTP0o4YJgLOJ+/pdHLOKMZxpMvlQkRE0zQVn9B8HC3eknmeq2zsBSmI8zw3EUJLG1K85Y/psiyWLu+aHn3WkqP7zzxLO1IwEXB92ezbzsEsQYu3Fge2wX+etcNKaC2iwzDc9cs0TU896wFgL5gKuEug9cldIqxyhk/0c5bNNng7xOMbGYuWcZF9jPgD0IdqAY8JdGx2nkJsYWxdV1rXFcLhoXVcQiktAEA7igScqz+I6MeCotwmt7N4l5ZP+VIntZT4k7NP7Luu7fqKQsc4N3Ytbej+1p9pm67PYrYs4l3SDzlYx7PVeAwdI8f/Hn1Zsm9tP9SOtdz21WYosgVclkAR3QdIpjnkdv77crls4ltaPlU72/N1bKzkTadxUvaRsXItrLrKyfgzPQhLY9fShus4cmzIdms/2Cbvp+NTG2+XDT4Gp3lcNlLbHotDajx7jkcr/3v0Zcm+pf1gNdb02A+NI+mrhO2mjr9sAT+dTj8cldtCAqtn4yVwh5T+ALDvLj9Pp5NTaIdhoMvl4my3r/KGbZ3PZ1qWxbuw6ion89kpzTO3suEbC7IaRbbb98Ovv1cab2lDnsCaZVl+nOjaBlFe6qk0nj3Ho6X/PfqyZN/cdliNtZxxFKqmy52NZws4/0IwoXpWKdhStHMFiG2yLT75SsqE2B9XG/h4evYgO1jvJ39FLXLNXMWhxVHarU0hWdmQcdQlhPrfEletuEyxWcQ71M/yhJPb+XP+k9qfNfFsMR7ZXuo5UeN/bV/6fC3ZN7cd1mONjy3HEJdP8/5sU44/uV8u2QJ+u902Zz4+PojIPe2XjnJgS3N067rSPM/O3JYMXsol2TzPd6I/DAMty7IFWp+4crDI6he5Hw8YCwFf15Wu1+uWS+OT2LK23coGjwV5c1VKX8v+4v/LWbpFvGP9zPblmJEzo9PplJTTJaqLp+V4lNuXZaHr9Rr1vdb/0r6M+Vqyb247rMeaFmk50eRtevLgSjdxW1KoqkKJXR5KR2vFh4/vynHJf8cuH7Wv67pug1BfCukFBtkO/lGR/ozjiEoYig8+LZyMZbxj/ewSDbm9FzXjUZ78epLjmr23ILUvc3zt0c7WY0366JsMuK48mi9iMjIAOemTGm632zawXTnMlEueHF907kzvyyebLwcG3Pgu7y3jbVmp1JKa8ejL70vhaC3gqX2Z42uPdrYea/pHWPooNYy/V9pPxQIuBVrDq7rsrCWy5lteusv8plU6g48nbWtk+3LypsAN4h2G48OTFR0PGb9Hx6fG1x7tbDnWfIKshf3r62ubAJfc9p8s4PohUvJvmUti5Hadd7SaFXAOVua82GavdIbuZNAWxPubI1351fj6qHa2GGvrutI0TbQsy12OnOg7Z9+kjNAl0nKbD520b4Er5wTAM5OSmtxLGqnG1yO1MxVebNV5dpkaJkqraksWcLnSHMofhbbV5HpS/Ou9AEV0/8t8tIF0RBDv/1Nb2dWTGl8f2c6asea6Q1nDQk70fWOPq3IlRLKAy/IcLq/hMhgJp0x4u5x1t+4Ea/HW+Sq9kiwXcvn7oBzEO8yjJikl1Pjao509xlpokVQjq+ykDzHNzFrElHWY7Jg2oJ22EG25KOrLoctLD6vKF70SfT6f70rPdHrIZ9M1EGWbYvSoKOhVtRDCKt57oEU8ZXxC5XMWz0ap9b/GV8t2+tphOdZ4kVXa0Hokt43jGNTOHIpupWeHXY3i7ZYnmMwNuWzLhQAim7pzeSxd6cK29fPJXXWejBbr0JoC0f2g1O2z+mHsYSOXmng/mh7xlPGRN8oxfJ7M85x8I08r/2t8rdk3tR1WY01mHLRNmXom+r5ZjO3IK4GSeBcLuEugLZ79HUM3kn1ipmkyXSzlSxvuJA5+ik3dOVz3mfpL63p8AH+ee3I+0kYONfHeA63j6YsP0f154EoL9Pa/xtfadqa0w3Ks+c5v12STv+9Dp55DFAl4LD1ilcJg5G2orudZsL1QmWLIH+mv63syP6UvjXx3ovF+8upB2tPPEPH5patrSuIaa3utjVj8UvyQlMY7xb6ln759U+LZajzGzoMe/lv5WrNvajtqxpq0JdMxof154it1TB8np+/e3t/f/yR98z94lu0TcIv8W4p9TWzGzy85DT35LNQul+3UqwzffvLzmF+S3Pr21LbX2EiJX8yPmF8p8a7t55R25Prt8qfFeCSyufK18D/lmKXnT+q+OeM6d6yN40hfX19E9D1Lzz2HdMaCKF83swUcAABeHSngn5+fD7vj1eSdmAAAAPoDAQcAgIMCAQcAgIMCAQcAgIMCAQcAgAL2cCcwBBwAADKRVSePfOY6yggBAOCgvP39B/od4p9fvxAgAB7EX79/vz3ahz2DFAoAABwUCDgAABwUCDgAABwUCPhOaP0QsBb08Hnvcdm7f6141XbvDQh4Q1IHOb8Pj4iy3kj9SHr4vPe47N2/Vrxqu/cIBNyYcRzvngvMyGcY+14JR0S7fVGBi5DP/LhRoro62UfFJdX/I/abBa/a7r0BATeEX5cUeuMOvwj6mS89+X2f/D7DPb7+LMTR/QevAwTcCC3erlcpyT+h92cehR4+HzEurwD6ZR9AwA3gGZt8756cZfObN3xv39nLbbk59PD5iHF5BdAv+wECboDrXXhyhr2uK63rGhzsRzwRevh8xLi8AuiXfQABN8KXOkklVLHi25ZbyuX6fk05mO948gdNL+jm2ukRF72vhf8lPliW5vn6JnRs3ifFh979AtxAwI0JLWD6CJVl6W1sw/WW+9DLhF37SH9zy8FcPvNnWgAvl8tmL8dO67j47JX47xP8mA86/Vbit68d7K/2Sy+iy+9LH5Zlcba1d78APxBwY/iEzxXEUFkWb5Mi4bLrqm5JqYzx2S3xWQsB+yavUPYQl5g9fYyY/9qXFB+GYaDL5eK1WVNjLY+p/ZeL6LLiRsM/WqH29uoX4AYCbgDPKHjg8ozKugztdDptthhpU89q9OxOpndcteq1LMtC0zRtbWekvy2qF3Lj4qPG/5K+keKt9+PPa8eObIe8F0GjhViO4VIfrPoF+IGAG7CuK83z7Myd8iC2uGyc5/nuB2EYBlqWhS6Xy2ZTzpakEPC+t9tty/P6Zl6lrOtK1+t1y3XySdp6ppUblxb+1/YN25C2WTyv12t+UP5Djj3+v15gn6Zp+zcR3flQ8yNv1S/ADwTcCB6Irhyq/HfNZbG+fF/XdTtB9YyaRZr3k3a5KsZ6Bv4ocuKyBx9038gfCD0ZqJ2psoiG9tfb2Hei7/FbYn8P/fLsQMANud1u2+DUQk50P6MpEfGc9INv0bL0eHtmD+0o7RseL67jhW78yvErp3ImlLcusQ3aAgE3RtZ8y+oPubBzPp+7XDpKkUCucV9w3/CPuuuuXfn/VuNFVyhZCjhoDwS8Ibfbbcs5E92vzo/jiPwfIKI2C8opyAol1wInRHz/QMA7oPOaODEAk3LjV4tUhBbvaZrurtQ+Pj62xUawXyDgnZCLNwAwehGzF/rGHn01iPz1MYCAd6S3eMsfjNht1KAfe/gxl+sj4LhAwA3gG2aIyFuy5buhphVSJHw3kvQQkNoqikfTwn8up4uVCbZ8doguE9QzcFwpHgMIuAG6bNC1GKTv7GstaLKWl4ju8p1E9TdpxGwzuu1HqIjp4b9cE9F9Q/TdP/M8V93I40Pbkp/pNoP9AgE3Rp/sRPezmWmaur2GikWCxYAfytRjduV6tAB/3kKQrGntP894WbzlA7N0CWGL9Jd8/EPIPtg3EHAD+GTU9d46ZRK6nT6UUolt4+36e/I2adet/UTuhzelEvNLV96UpI1axCXVbor/NT7Iu3ddKbaaxy/E2sxjY1mWH1eP8imCJcdv2S/gnre///x5tA+75p9fv7IC5Mstxy69+SW6vsd3+rZJmyEb8iW97M/5fN5KxT4/P7Pr0lP9kljasIhLiBT/LXxw2alN1cT8cn1X2ib6FvDc2Fv2y1+/f79F3H9pIOARcgX8SIzjSF9fX0RUJuAAtAYCHgYplBcGuU4Ajg0E/ImRl8b6clU/EQ8AcDwg4E+MrECRz2Ym+vnSAIg4AMcDAv7E6OdK88KRrpDBm1EAOCYQ8CeGH6JF9PNFE0Q/X/QAADgWqEKJ8AxVKJzv1nXgR7grErw2qEIJAwGP8AwCDsBRgYCH+RdHEwkWLXE/8gAAAABJRU5ErkJggg==\n            </smpte:image>\n            <smpte:image imagetype=\"PNG\" encoding=\"Base64\" xml:id=\"img_1\">\n                iVBORw0KGgoAAAANSUhEUgAAAaAAAAAkCAIAAABAAnl0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAIBklEQVR4nO1d65GrPAz1nfkKSAu4FWglqeXWQFohrZhW7o8zaBS/kB+QhE/nx86uA5YsnT1+CHb/3G43o1AoFFfEf592QKFQKI6CCpxCobgsVOAUCsVloQL3SxiGwRizruuht5TiBBM/hJ+Lxs85XAQVuJ/BOI7TNBljlmV5vV4H3XKCVxfGz0Xj5xwuRaXAjeOIb7ygoN05J58QqCuOoh7+PyAu8sZULjK3lOIEE5fBB6ORT1MK105fjcANwzBNk7XWU/1xHB+PhzFmnmehPOEWa63X7pxblsU5d8lZpSOQi2maEK4jZoUTTCjaoWmKokbgrLWQJOdc2I74VnRLd6Gfx+OBWUU1jlAXWMWZ0Bx9FeoFzhOyYRhI9Spmj2VZaJGM/jEdoU/VOMOCoNH4WmiOvg3FApcSstSyTg5ODpwmQOCmaVK6ABqH74fm6KsgFTjUkg0TMt5I7VC39sIzWGI3jOMY5Q05kLIVeiL0TXJZx4c2hO3hj5QOnpeObh9qopon8rTmTVTctbtT2R1U46hTDlekqdHDipHWpanuSkAkcFRLNix8tH+kdnx9PB6QucbC8+v1ggkgLNeSS8YY51xYkeAlcPOeeBQxUkShnqPdUo0l31WIYRhQhHHOPZ/PqJ/c1v1+hxUUbbzL8COndSbyIfUlbh9kojqAYbhCi8iXZyLqMPXj1cRS6aBoUAs8D6+JmpNQa3fI0XuL0pRC9/T1SlOR0RAigeOJ4Y3cM6+9y1Hrsiywa60dhoGvXHBCxy+GRS86dDt95X56zIYAhd1aa0mPotfYoKCcAepcfDh8LNQJL1XzKw2r6Idu0OIiE4dMBKLe9jXRHkCTTatzjv+2cxPmnR4IO/LrBSF8ciJa7o8u5aJPXUiolYKE7fI0pXBE+rqkqdSoB5HAQS+5017+SNRI17o84YEOU0rKfaDTuujGAb55Q4DQcNGkINJAaP0IPeLX8N7orrxY0KfoEz9623wSPs7RVHDmeeZzD7kU3iKJwAkmGgMosUhx82pWQB0n4TZ1S9wouj1Prd1OMmwvSlMKjemLClOmZ3maGjkjErjX6wWT9/vdBCtzSj8C3fcBHAicMYZPs+u6zvNsNsmARnhrXQ6UaBGLYRicc9gq8lDyiNPo6MAFhiisy7JgpGQabNudjbEm5Vnk8s2Fj9QtxdF1XZ/PJzlALlVH4AQT7QGUWAQ/6TeTFl9yNfHAucEjIDwPklArg122F6Upher04UqbLgY2pqmRM2VVVJtY91L+Tnu8kBvCJGbeBYKDO4yLvYUh7Qc97V7X1TvRN9u6Bu3rui7Lgq52F0TkTChq9Cn4RLZMp+eqdiNwgoleAdy16JGe2ruM0SPe7i0Sau2iiO11KEofpQnpozk7WgxsSVM7ZwoEjozt7k9PA7mUp++uY5TIzCj4NZ45osIu4XgWMU3xkIIomKDIVhcGn5CaoiBXB7DIYkfAQywZipIioZYQQrbXofp3BJqVmSxbRt3OmQKBIyHjjZigzGFsiy5kxnGkkfdKOfWQCZZl5WPyx1uO5U95vOnXMCHzevPar4H2AH4EfDnz9+9fzEbyN6Ik1MrgCLbXITUQnsru6WvkzI7ARYPr1XSonW+tu6w7UupGR358Mjkz65n5aheOHVHTnIFGbyb8yKL4HLQE8HzgFMxthT9+AN/4LNQuvoHtuzjHqzrO7AhcWJ82QU3aM790/bME3qTB8w3Oof1+v0NeT0BGdyR6RMsBCqPX4eUFrjGAHwEO8vl5EzJo09XDdnwD278E1ZzZETi31Xf5ZjhcKPLGXsdGtPk1QS3ZGDPPM2fVyccxjcXicKWG3kLhO61ocybaA/gpQObM9hTrdPCrhN/AdgmiO62+qObMjsBR+RkzCSq19Cm2pWgnDepFXL4k9NbA3ePID1lSTxLw+kAL+DEc9ex9E/3x19ErgN8AyA1NRZmnTyTUSuGrzmFTA+le8Y8are5hv8hAD56YYE3Bl299J2T+4Dg/0eMn9HxOa/y14ZWgaZqiT9bQNdHKdNErcjxt3uKXtxcNAc4fuixqNNExgB3hMYfvGzjGcYySfxcSaqVQx/aDmJAaSHQh0t1oNWekr2rZ2IMgaO8yMGvt/X53rPRLi3PeOX3PS7c29iZZKfiTNaEhLI/pGjoZoWsQByF9vRFx+Y4KXwb87YiD9rYdTfQKYBd4KabvQy5hunVbIaj0JEFCLYmHebafwIToQGghctCJSiNnCgRu6foH4EIT3rmpe3/QmTdibMS5Lrue1+tlt2Njr2fafWMWRaBD6/I9CM1L5l3saPdqSqK6bG/s0pl3d6XoZaJXALuAS9W0vZQavdJuTyqEH/HDmRQk1Ep5WMT2o5kQpo+cmee5b3UxY9SUcEYqcKl9qHChkULqdrf9yXLPolewt+w1t2jiU53TbMzbn8+ne38HGFdykaXDF+KQ21D0cAzmpdCHlG/54dAsZ4MHFYsikEJHE10CWDGosJHrDh+mCbQMVJzeX0dP+Ry1LqFWiAq2Z9KUQnv6woVIRc+ZW1o48+d2u2U+BrBYC+Wmy7kJP6QEJIsX/q9quKhH/wlOWORKjSj0J/XHW/g18tWW51vKAZOIan44UZ8rIhBFXxONASy1KPTEbe9LRrlk3nctpjBHRkatKIRsrzPRmL5M7jqmKRyakDMigVMoFIpfhP5fVIVCcVmowCkUistCBU6hUFwWKnAKheKyUIFTKBSXhQqcQqG4LFTgFArFZfEPuuTdBr3uWzgAAAAASUVORK5CYII=\n            </smpte:image>\n        </metadata>\n        <styling>\n            <style/>\n        </styling>\n        <layout>\n            <region xml:id=\"region_0\" tts:extent=\"653px 86px\" tts:origin=\"307px 562px\"/>\n            <region xml:id=\"region_1\" tts:extent=\"730px 43px\" tts:origin=\"269px 612px\"/>\n        </layout>\n    </head>\n    <body>\n        <div begin=\"00:00:00.200\" end=\"00:00:03.000\" region=\"region_0\" smpte:backgroundImage=\"#img_0\"/>\n        <div begin=\"00:00:03.200\" end=\"00:00:06.937\" region=\"region_1\" smpte:backgroundImage=\"#img_1\"/>\n    </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/chain_multiple_styles.xml",
    "content": "<tt xmlns:ttm=\"http://www.w3.org/2006/10/ttaf1#metadata\"\n    xmlns:ttp=\"http://www.w3.org/2006/10/ttaf1#parameter\"\n    xmlns:tts=\"http://www.w3.org/2006/10/ttaf1#style\"\n    xmlns=\"http://www.w3.org/ns/ttml\"\n    xmlns=\"http://www.w3.org/2006/10/ttaf1\">\n  <head>\n    <styling>\n      <style id=\"s0\"\n        tts:backgroundColor=\"blue\"\n        tts:color=\"black\"\n        tts:fontWeight=\"bold\" />\n      <style id=\"s1\"\n        tts:backgroundColor=\"black\"\n        tts:color=\"red\"\n        tts:fontFamily=\"sansSerif\"\n        tts:fontStyle=\"italic\"\n        tts:textDecoration=\"lineThrough\" />\n      <!-- multiple ids defined -->\n      <style style=\"s0 s1\" id=\"s2\"\n        tts:fontFamily=\"serif\"\n        tts:backgroundColor=\"red\" />\n      <style style=\"s1 s0\" id=\"s3\"\n        tts:fontFamily=\"serif\"\n        tts:backgroundColor=\"red\" />\n    </styling>\n  </head>\n  <body>\n    <div>\n      <p style=\"s2\" begin=\"10s\" end=\"18s\">text 1</p>\n    </div>\n  </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/font_size.xml",
    "content": "<tt xmlns:ttm=\"http://www.w3.org/2006/10/ttaf1#metadata\"\n    xmlns:ttp=\"http://www.w3.org/2006/10/ttaf1#parameter\"\n    xmlns:tts=\"http://www.w3.org/2006/10/ttaf1#style\"\n    xmlns=\"http://www.w3.org/ns/ttml\"\n    xmlns=\"http://www.w3.org/2006/10/ttaf1\">\n  <head>\n    <styling>\n      <style id=\"s0\"\n        tts:textDecoration=\"underline\"/>\n      <style id=\"s1\"\n        tts:textDecoration=\"lineThrough\"/>\n    </styling>\n  </head>\n  <body>\n    <div>\n      <p begin=\"10s\" end=\"18s\" tts:fontSize=\"32px\">text 1</p>\n    </div>\n    <div>\n      <p begin=\"20s\" end=\"28s\" tts:fontSize=\"2.2em\">text 2</p>\n    </div>\n    <div>\n      <p begin=\"30s\" end=\"38s\" tts:fontSize=\"150%\">text 3</p>\n    </div>\n    <div>\n      <p begin=\"40s\" end=\"48s\" tts:fontSize=\"32px 16px\">two values</p>\n    </div>\n    <div>\n      <p begin=\"50s\" end=\"58s\" tts:fontSize=\".5em\">leading dot</p>\n    </div>\n  </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/font_size_empty.xml",
    "content": "<tt xmlns:ttm=\"http://www.w3.org/2006/10/ttaf1#metadata\" xmlns:ttp=\"http://www.w3.org/2006/10/ttaf1#parameter\"\n  xmlns:tts=\"http://www.w3.org/2006/10/ttaf1#style\"\n  xmlns=\"http://www.w3.org/ns/ttml\"\n  xmlns=\"http://www.w3.org/2006/10/ttaf1\">\n  <body>\n    <div>\n      <p begin=\"10s\" end=\"18s\" tts:fontSize=\"\">empty</p>\n    </div>\n  </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/font_size_invalid.xml",
    "content": "<tt xmlns:ttm=\"http://www.w3.org/2006/10/ttaf1#metadata\" xmlns:ttp=\"http://www.w3.org/2006/10/ttaf1#parameter\"\n  xmlns:tts=\"http://www.w3.org/2006/10/ttaf1#style\"\n  xmlns=\"http://www.w3.org/ns/ttml\"\n  xmlns=\"http://www.w3.org/2006/10/ttaf1\">\n  <body>\n    <div>\n      <p begin=\"10s\" end=\"18s\" tts:fontSize=\"px\">invalid</p>\n    </div>\n    <div>\n      <p begin=\"20s\" end=\"28s\" tts:fontSize=\"12px 10px 9px\">invalid</p>\n    </div>\n    <div>\n      <p begin=\"30s\" end=\"38s\" tts:fontSize=\"1.em\">invalid dot</p>\n    </div>\n  </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/font_size_no_unit.xml",
    "content": "<tt xmlns:ttm=\"http://www.w3.org/2006/10/ttaf1#metadata\" xmlns:ttp=\"http://www.w3.org/2006/10/ttaf1#parameter\"\n  xmlns:tts=\"http://www.w3.org/2006/10/ttaf1#style\"\n  xmlns=\"http://www.w3.org/ns/ttml\"\n  xmlns=\"http://www.w3.org/2006/10/ttaf1\">\n  <body>\n    <div>\n      <p begin=\"10s\" end=\"18s\" tts:fontSize=\"32\">no unit</p>\n    </div>\n  </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/frame_rate.xml",
    "content": "<tt xmlns=\"http://www.w3.org/ns/ttml\"\n    xmlns:ttp=\"http://www.w3.org/ns/ttml#parameter\"\n    ttp:frameRate=\"50\"\n    ttp:frameRateMultiplier=\"1000 1001\"\n    ttp:tickRate=\"100\">\n    <head>\n        <styling>\n        </styling>\n    </head>\n    <body>\n        <div>\n            <p begin=\"100t\" end=\"101t\">text 1</p>\n        </div>\n        <div>\n            <p begin=\"50000f\" end=\"100000f\">text 2</p>\n        </div>\n    </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/inherit_and_override_style.xml",
    "content": "<tt xmlns=\"http://www.w3.org/ns/ttml\"\n    xmlns=\"http://www.w3.org/2006/10/ttaf1\"\n    xmlns:ttp=\"http://www.w3.org/2006/10/ttaf1#parameter\"\n    xmlns:tts=\"http://www.w3.org/2006/10/ttaf1#style\"\n    xmlns:ttm=\"http://www.w3.org/2006/10/ttaf1#metadata\">\n    <head>\n        <styling>\n            <style id=\"s0\"\n                tts:fontWeight=\"bold\"\n                tts:fontStyle=\"italic\"\n                tts:fontFamily=\"serif\"\n                tts:textDecoration=\"underline\"\n                tts:backgroundColor=\"blue\"\n                tts:color=\"yellow\"/>\n        </styling>\n    </head>\n    <body>\n        <div>\n            <p style=\"s0\" begin=\"10s\" end=\"18s\">text 1</p>\n        </div>\n        <div>\n            <p style=\"s0\" begin=\"20s\" end=\"28s\"\n                tts:fontWeight=\"normal\"\n                tts:fontFamily=\"sansSerif\"\n                tts:backgroundColor=\"red\"\n                tts:color=\"yellow\"\n                >text 2</p>\n        </div>\n    </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/inherit_global_and_parent.xml",
    "content": "<tt xmlns=\"http://www.w3.org/ns/ttml\"\n    xmlns=\"http://www.w3.org/2006/10/ttaf1\"\n    xmlns:ttp=\"http://www.w3.org/2006/10/ttaf1#parameter\"\n    xmlns:tts=\"http://www.w3.org/2006/10/ttaf1#style\"\n    xmlns:ttm=\"http://www.w3.org/2006/10/ttaf1#metadata\">\n    <head>\n        <styling>\n            <style id=\"s0\"\n                tts:fontWeight=\"bold\"\n                tts:fontStyle=\"italic\"\n                tts:fontFamily=\"serif\"\n                tts:textDecoration=\"underline\"\n                tts:backgroundColor=\"blue\"\n                tts:color=\"yellow\"/>\n        </styling>\n    </head>\n    <body tts:textAlign=\"center\">\n        <div tts:fontWeight=\"normal\"\n            tts:fontStyle=\"normal\"\n            tts:fontFamily=\"sansSerif\"\n            tts:textDecoration=\"lineThrough\"\n            tts:backgroundColor=\"red\"\n            tts:color=\"lime\">\n            <p begin=\"10s\" end=\"18s\">text 1</p>\n        </div>\n        <div tts:fontWeight=\"normal\"\n            tts:fontStyle=\"normal\"\n            tts:fontFamily=\"sansSerif\"\n            tts:textDecoration=\"lineThrough\"\n            tts:backgroundColor=\"red\"\n            tts:color=\"lime\">\n            <p style=\"s0\" begin=\"20s\" end=\"28s\">text 2</p>\n        </div>\n    </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/inherit_multiple_styles.xml",
    "content": "<tt xmlns:ttm=\"http://www.w3.org/2006/10/ttaf1#metadata\"\n    xmlns:ttp=\"http://www.w3.org/2006/10/ttaf1#parameter\"\n    xmlns:tts=\"http://www.w3.org/2006/10/ttaf1#style\"\n    xmlns=\"http://www.w3.org/ns/ttml\"\n    xmlns=\"http://www.w3.org/2006/10/ttaf1\">\n  <head>\n    <styling>\n      <style id=\"s0\"\n        tts:backgroundColor=\"blue\"\n        tts:color=\"black\"\n        tts:fontWeight=\"bold\" />\n      <style id=\"s1\"\n        tts:backgroundColor=\"black\"\n        tts:color=\"red\"\n        tts:fontFamily=\"sansSerif\"\n        tts:fontStyle=\"italic\"\n        tts:textDecoration=\"lineThrough\" />\n      <style id=\"s2\"\n        tts:backgroundColor=\"red\" />\n      <style id=\"s3\"\n        tts:backgroundColor=\"green\"\n        tts:textDecoration=\"lineThrough\" />\n    </styling>\n  </head>\n  <body>\n    <div>\n      <p style=\"s0 s1\" begin=\"10s\" end=\"18s\" tts:color=\"yellow\">text 1</p>\n    </div>\n    <div>\n      <p style=\"s0 s1\" begin=\"20s\" end=\"28s\">text 2</p>\n    </div>\n    <div tts:color=\"yellow\" tts:textDecoration=\"underline\" tts:fontStyle=\"italic\" tts:fontFamily=\"sansSerifInline\">\n      <p style=\"s2                           s3\" begin=\"30s\" end=\"38s\">text 2.5</p>\n    </div>\n    <div>\n      <!-- empty style attribute -->\n      <p style=\" \" begin=\"40s\" end=\"48s\">text 3</p>\n    </div>\n    <div>\n      <p style=\"not_existing\" begin=\"50s\" end=\"58s\">text 4</p>\n    </div>\n    <div>\n      <p style=\"not_existing s2\" begin=\"60s\" end=\"68s\">text 5</p>\n    </div>\n  </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/inherit_style.xml",
    "content": "<tt xmlns=\"http://www.w3.org/ns/ttml\"\n    xmlns=\"http://www.w3.org/2006/10/ttaf1\"\n    xmlns:ttp=\"http://www.w3.org/2006/10/ttaf1#parameter\"\n    xmlns:tts=\"http://www.w3.org/2006/10/ttaf1#style\"\n    xmlns:ttm=\"http://www.w3.org/2006/10/ttaf1#metadata\">\n    <head>\n        <styling>\n            <style id=\"s0\"\n                tts:fontWeight=\"bold\"\n                tts:fontStyle=\"italic\"\n                tts:fontFamily=\"serif\"\n                tts:textDecoration=\"underline\"\n                tts:backgroundColor=\"blue\"\n                tts:color=\"yellow\"/>\n        </styling>\n    </head>\n    <body>\n        <div>\n            <p style=\"s0\" begin=\"10s\" end=\"18s\">text 1</p>\n        </div>\n    </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/inline_style_attributes.xml",
    "content": "<tt xmlns=\"http://www.w3.org/ns/ttml\"\n    xmlns=\"http://www.w3.org/2006/10/ttaf1\"\n    xmlns:ttp=\"http://www.w3.org/2006/10/ttaf1#parameter\"\n    xmlns:tts=\"http://www.w3.org/2006/10/ttaf1#style\"\n    xmlns:ttm=\"http://www.w3.org/2006/10/ttaf1#metadata\">\n    <body>\n        <div>\n            <p begin=\"10s\" end=\"18s\"\n                tts:fontWeight=\"bold\"\n                tts:fontStyle=\"italic\"\n                tts:fontFamily=\"serif\"\n                tts:textDecoration=\"underline\"\n                tts:backgroundColor=\"blue\"\n                tts:color=\"yellow\">text 1</p>\n        </div>\n        <div tts:fontWeight=\"normal\"\n            tts:fontStyle=\"italic\"\n            tts:fontFamily=\"sansSerif\"\n            tts:textDecoration=\"lineThrough\"\n            tts:backgroundColor=\"cyan\"\n            tts:color=\"lime\">\n            <p begin=\"20s\" end=\"28s\">text 2</p>\n        </div>\n    </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/multiple_regions.xml",
    "content": "<tt xmlns=\"http://www.w3.org/ns/ttml\"\n    xmlns=\"http://www.w3.org/2006/10/ttaf1\"\n    xmlns:id=\"http://www.w3.org/XML/1998/namespace\"\n    xmlns:ttp=\"http://www.w3.org/ns/ttml#parameter\"\n    xmlns:tts=\"http://www.w3.org/ns/ttml#styling\"\n    xmlns:ttm=\"http://www.w3.org/ns/ttml#metadata\">\n    <head>\n        <layout>\n            <region xml:id=\"region1\" ttm:origin=\"10% 10%\" extent=\"20% 20%\"/>\n            <region xml:id=\"region2\" ttm:origin=\"40% 40%\" extent=\"20% 20%\"/>\n            <region xml:id=\"region3\" ttm:origin=\"10% 80%\"/>\n            <region xml:id=\"region4\" ttm:origin=\"60% 10%\" extent=\"20% 20%\"/>\n            <region xml:id=\"ultimate\" ttm:origin=\"45% 45%\" extent=\"35% 35%\"/>\n        </layout>\n    </head>\n    <body>\n        <div>\n            <p begin=\"1s\" end=\"4s\" region=\"region1\">lorem</p>\n            <p begin=\"5s\" end=\"8s\" region=\"region2\">ipsum</p>\n            <p begin=\"9s\" end=\"18s\" region=\"region3\">dolor</p>\n            <p begin=\"1s\" end=\"4s\" region=\"region4\">amet</p>\n        </div>\n        <div region=\"ultimate\">\n            <p begin=\"21s\" end=\"34s\">She first said this</p>\n            <p begin=\"25s\" end=\"34s\">Then this</p>\n            <p begin=\"29s\" end=\"34s\">Finally this</p>\n        </div>\n    </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/ttml/no_underline_linethrough.xml",
    "content": "<tt xmlns:ttm=\"http://www.w3.org/2006/10/ttaf1#metadata\" xmlns:ttp=\"http://www.w3.org/2006/10/ttaf1#parameter\"\n  xmlns:tts=\"http://www.w3.org/2006/10/ttaf1#style\"\n  xmlns=\"http://www.w3.org/ns/ttml\"\n  xmlns=\"http://www.w3.org/2006/10/ttaf1\">\n  <head>\n    <styling>\n      <style id=\"s0\"\n        tts:textDecoration=\"underline\"/>\n      <style id=\"s1\"\n        tts:textDecoration=\"lineThrough\"/>\n    </styling>\n  </head>\n  <body>\n    <div>\n      <p style=\"s0\" begin=\"10s\" end=\"18s\" tts:textDecoration=\"noUnderline\">text 1</p>\n    </div>\n    <div>\n      <p style=\"s1\" begin=\"20s\" end=\"28s\" tts:textDecoration=\"noLineThrough\">text 1</p>\n    </div>\n  </body>\n</tt>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/wav/sample.wav.0.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1000000\n  getPosition(0) = [[timeUs=0, position=78]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 705600\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/raw\n    maxInputSize = 32768\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = 2\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 88200\n  sample count = 3\n  sample 0:\n    time = 0\n    flags = 1\n    data = length 32768, hash 9A8CEEBA\n  sample 1:\n    time = 371519\n    flags = 1\n    data = length 32768, hash C1717317\n  sample 2:\n    time = 743038\n    flags = 1\n    data = length 22664, hash 819F5F62\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/wav/sample.wav.1.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1000000\n  getPosition(0) = [[timeUs=0, position=78]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 705600\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/raw\n    maxInputSize = 32768\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = 2\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 58802\n  sample count = 2\n  sample 0:\n    time = 333310\n    flags = 1\n    data = length 32768, hash 42D6E860\n  sample 1:\n    time = 704829\n    flags = 1\n    data = length 26034, hash 62692C38\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/wav/sample.wav.2.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1000000\n  getPosition(0) = [[timeUs=0, position=78]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 705600\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/raw\n    maxInputSize = 32768\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = 2\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 29402\n  sample count = 1\n  sample 0:\n    time = 666643\n    flags = 1\n    data = length 29402, hash 4241604E\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/wav/sample.wav.3.dump",
    "content": "seekMap:\n  isSeekable = true\n  duration = 1000000\n  getPosition(0) = [[timeUs=0, position=78]]\nnumberOfTracks = 1\ntrack 0:\n  format:\n    bitrate = 705600\n    id = null\n    containerMimeType = null\n    sampleMimeType = audio/raw\n    maxInputSize = 32768\n    width = -1\n    height = -1\n    frameRate = -1.0\n    rotationDegrees = 0\n    pixelWidthHeightRatio = 1.0\n    channelCount = 1\n    sampleRate = 44100\n    pcmEncoding = 2\n    encoderDelay = 0\n    encoderPadding = 0\n    subsampleOffsetUs = 9223372036854775807\n    selectionFlags = 0\n    language = null\n    drmInitData = -\n    initializationData:\n  total output bytes = 2\n  sample count = 1\n  sample 0:\n    time = 999977\n    flags = 1\n    data = length 2, hash 116\ntracksEnded = true\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/webvtt/empty",
    "content": "\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/webvtt/typical",
    "content": "WEBVTT # This comment is allowed\n\n00:00.000 --> 00:01.234\nThis is the first subtitle.\n\n00:02.345 --> 00:03.456\nThis is the second subtitle.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/webvtt/typical_with_bad_timestamps",
    "content": "WEBVTT # This comment is allowed\n\n# First timestamp is missing the 1/1000ths component, but parse anyway.\n00:00 --> 00:01.234\nThis is the first subtitle.\n\n02.345 --> 00:03.456\nThis is the second subtitle.\n\n0.0.0 --> 00:05.678\nThis should be discarded (too many dots).\n\n00:06.789 --> not-a-timestamp\nThis should be discarded (not a timestamp).\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/webvtt/typical_with_comments",
    "content": "WEBVTT\n\nNOTE\nThis is a comment block\nwith multiple lines\n\n1\n00:00.000 --> 00:01.234\nThis is the first subtitle.\n\nNOTE Single line comment with a space\n\nNOTE\tSingle line comment with a tab\n\n2\n00:02.345 --> 00:03.456\nThis is the second subtitle.\n\nNOTE\nFile ending with a comment\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/webvtt/typical_with_identifiers",
    "content": "WEBVTT\n\n1\n00:00.000 --> 00:01.234\nThis is the first subtitle.\n\n2\n00:02.345 --> 00:03.456\nThis is the second subtitle.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/webvtt/with_bad_cue_header",
    "content": "WEBVTT # This comment is allowed\n\n00:00.000 --> 00:01.234\nThis is the first subtitle.\n\n00:02.badbadbadbadbadbad --> 00:03.456\nThis is the second subtitle.\n\n00:04.000 --> 00:05.000\nThis is the third subtitle.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/webvtt/with_bom",
    "content": "﻿WEBVTT # This comment is allowed\n\n00:00.000 --> 00:01.234\nThis is the first subtitle.\n\n00:02.345 --> 00:03.456\nThis is the second subtitle.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/webvtt/with_css_complex_selectors",
    "content": "WEBVTT\n\nSTYLE\n::cue(\\n#id ){text-decoration:underline;}\n\nSTYLE\n::cue(#id.class1.class2 ){ color: violet;}\n\nSTYLE\n::cue(lang){font-family:Courier}\n\nSTYLE\n::cue(.class.another ){font-weight: bold;}\n\nSTYLE\n::cue(v.class[voice=\"Strider Trancos\"] ){ font-weight:bold; }\n\nSTYLE\n::cue(v#anId.class1.class2[voice=\"Robert\"] ){ font-style:italic; }\n\nid\n00:00.000 --> 00:01.001\nThis should be underlined and <lang.class1.class2> courier and violet.\n\níd\n00:02.000 --> 00:02.001\nThis <lang.class1.class2>should be just courier.\n\n_id\n00:02.500 --> 00:02.501\nThis <lang.class.another>should be courier and bold.\n\n00:04.000 --> 00:04.001\nThis <v Strider Trancos> shouldn't be bold.</v>\nThis <v.class.clazz Strider Trancos> should be bold.\n\nanId\n00:05.000 --> 00:05.001\nThis is <v.class1.class3.class2 Pipo> specific </v>\n<v.class1.class3.class2 Robert> But this is more italic</v>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/webvtt/with_css_styles",
    "content": "WEBVTT\n\nSTYLE\n::cue {\n  background-color: green;\n  color: papayawhip;\n}\n/* Style blocks cannot use blank lines nor \"dash dash greater than\" */\n\nNOTE comment blocks can be used between style blocks.\n\nSTYLE\n::cue(#id2) {\n  color: peachpuff;\n}\n::cue(v[voice=\"LaGord\"]) { background-color: lime }\n\nSTYLE\n::cue(v[voice=\"The Frog\"]) { font-weight: bold }\n\nSTYLE\n::cue(v){text-decoration:underline}\n\nid1\n00:00.000 --> 00:01.234\nThis is the first subtitle.\n\nid2\n00:02.345 --> 00:03.456\nThis is the second subtitle.\n\n00:20.000 --> 00:21.000\nThis is a <v.clazz Mary>reference by element</v>\n\n00:25.000 --> 00:28.000\n<v  LaGord>You are an idiot</v>\n<v  The Frog>You don't have the guts</v>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/webvtt/with_positioning",
    "content": "WEBVTT\n\nNOTE Position with percentage and position alignment\n\n00:00:00.000 --> 00:00:01.234 position:10%,start align:start size:35%\nThis is the first subtitle.\n\nNOTE Wrong position provided. It should be provided as\na percentage value\n\n00:02.345 --> 00:03.456 position:10 align:end size:35%\nThis is the second subtitle.\n\nNOTE Line as percentage and line alignment\n\n00:04.000 --> 00:05.000 line:45%,end align:middle size:35%\nThis is the third subtitle.\n\nNOTE Line as absolute negative number and without line alignment.\n\n00:06.000 --> 00:07.000 line:-10 align:middle\nThis is the fourth subtitle.\n\nNOTE The positioning alignment should be inherited from align.\n\n00:07.000 --> 00:08.000 position:10% align:end size:10%\nThis is the fifth subtitle.\n\nNOTE In newer drafts, align:middle has been replaced by align:center\n\n00:10.000 --> 00:11.000 line:45%,end align:center size:35%\nThis is the sixth subtitle.\n\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/assets/webvtt/with_tags",
    "content": "WEBVTT\n\n00:00.000 --> 00:01.234\nThis is the <i>first</i> subtitle.\n\n00:02.345 --> 00:03.456\nThis is the <b><i>second</b></i> subtitle.\n\n00:04.000 --> 00:05.000\nThis is the <c.red.caps>third</c> subtitle.\n\n00:06.000 --> 00:07.000\nThis is the &lt;fourth&gt; &amp;subtitle.\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/CTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\n\nimport android.annotation.SuppressLint;\nimport android.media.MediaCodec;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link C}. */\n@RunWith(AndroidJUnit4.class)\npublic class CTest {\n\n  @SuppressLint(\"InlinedApi\")\n  @Test\n  public void testConstants() {\n    // Sanity check that constant values match those defined by the platform.\n    assertThat(C.BUFFER_FLAG_KEY_FRAME).isEqualTo(MediaCodec.BUFFER_FLAG_KEY_FRAME);\n    assertThat(C.BUFFER_FLAG_END_OF_STREAM).isEqualTo(MediaCodec.BUFFER_FLAG_END_OF_STREAM);\n    assertThat(C.CRYPTO_MODE_AES_CTR).isEqualTo(MediaCodec.CRYPTO_MODE_AES_CTR);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.DefaultLoadControl.Builder;\nimport com.google.android.exoplayer2.upstream.DefaultAllocator;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link DefaultLoadControl}. */\n@RunWith(AndroidJUnit4.class)\npublic class DefaultLoadControlTest {\n\n  private static final float SPEED = 1f;\n  private static final long MIN_BUFFER_US = C.msToUs(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS);\n  private static final long MAX_BUFFER_US = C.msToUs(DefaultLoadControl.DEFAULT_MAX_BUFFER_MS);\n  private static final int TARGET_BUFFER_BYTES = C.DEFAULT_BUFFER_SEGMENT_SIZE * 2;\n\n  private Builder builder;\n  private DefaultAllocator allocator;\n  private DefaultLoadControl loadControl;\n\n  @Before\n  public void setUp() throws Exception {\n    builder = new Builder();\n    allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);\n  }\n\n  @Test\n  public void testShouldContinueLoading_untilMaxBufferExceeded() {\n    createDefaultLoadControl();\n    assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue();\n    assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isTrue();\n    assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isTrue();\n    assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();\n  }\n\n  @Test\n  public void testShouldNotContinueLoadingOnceBufferingStopped_untilBelowMinBuffer() {\n    createDefaultLoadControl();\n    assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();\n    assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isFalse();\n    assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();\n    assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue();\n  }\n\n  @Test\n  public void testShouldContinueLoadingWithTargetBufferBytesReached_untilMinBufferReached() {\n    createDefaultLoadControl();\n    makeSureTargetBufferBytesReached();\n\n    assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue();\n    assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue();\n    assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();\n    assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();\n  }\n\n  @Test\n  public void testShouldNeverContinueLoading_ifMaxBufferReachedAndNotPrioritizeTimeOverSize() {\n    builder.setPrioritizeTimeOverSizeThresholds(false);\n    createDefaultLoadControl();\n    // Put loadControl in buffering state.\n    assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue();\n    makeSureTargetBufferBytesReached();\n\n    assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isFalse();\n    assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();\n    assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();\n  }\n\n  @Test\n  public void testShouldContinueLoadingWithMinBufferReached_inFastPlayback() {\n    createDefaultLoadControl();\n\n    // At normal playback speed, we stop buffering when the buffer reaches the minimum.\n    assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();\n\n    // At double playback speed, we continue loading.\n    assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, /* playbackSpeed= */ 2f)).isTrue();\n  }\n\n  @Test\n  public void testShouldNotContinueLoadingWithMaxBufferReached_inFastPlayback() {\n    createDefaultLoadControl();\n\n    assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, /* playbackSpeed= */ 100f))\n        .isFalse();\n  }\n\n  @Test\n  public void testStartsPlayback_whenMinBufferSizeReached() {\n    createDefaultLoadControl();\n    assertThat(loadControl.shouldStartPlayback(MIN_BUFFER_US, SPEED, /* rebuffering= */ false))\n        .isTrue();\n  }\n\n  private void createDefaultLoadControl() {\n    builder.setAllocator(allocator);\n    builder.setTargetBufferBytes(TARGET_BUFFER_BYTES);\n    loadControl = builder.createDefaultLoadControl();\n    loadControl.onTracksSelected(new Renderer[0], null, null);\n  }\n\n  private void makeSureTargetBufferBytesReached() {\n    while (allocator.getTotalBytesAllocated() < TARGET_BUFFER_BYTES) {\n      allocator.allocate();\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.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 static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyNoMoreInteractions;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener;\nimport com.google.android.exoplayer2.testutil.FakeClock;\nimport com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\n\n/** Unit test for {@link DefaultMediaClock}. */\n@RunWith(AndroidJUnit4.class)\npublic class DefaultMediaClockTest {\n\n  private static final long TEST_POSITION_US = 123456789012345678L;\n  private static final long SLEEP_TIME_MS = 1_000;\n  private static final PlaybackParameters TEST_PLAYBACK_PARAMETERS =\n      new PlaybackParameters(/* speed= */ 2f);\n\n  @Mock private PlaybackParameterListener listener;\n  private FakeClock fakeClock;\n  private DefaultMediaClock mediaClock;\n\n  @Before\n  public void initMediaClockWithFakeClock() {\n    initMocks(this);\n    fakeClock = new FakeClock(0);\n    mediaClock = new DefaultMediaClock(listener, fakeClock);\n  }\n\n  @Test\n  public void standaloneResetPosition_getPositionShouldReturnSameValue() throws Exception {\n    mediaClock.resetPosition(TEST_POSITION_US);\n    assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US);\n  }\n\n  @Test\n  public void standaloneGetAndResetPosition_shouldNotTriggerCallback() throws Exception {\n    mediaClock.resetPosition(TEST_POSITION_US);\n    mediaClock.syncAndGetPositionUs();\n    verifyNoMoreInteractions(listener);\n  }\n\n  @Test\n  public void standaloneClock_shouldNotAutoStart() throws Exception {\n    assertClockIsStopped();\n  }\n\n  @Test\n  public void standaloneResetPosition_shouldNotStartClock() throws Exception {\n    mediaClock.resetPosition(TEST_POSITION_US);\n    assertClockIsStopped();\n  }\n\n  @Test\n  public void standaloneStart_shouldStartClock() throws Exception {\n    mediaClock.start();\n    assertClockIsRunning();\n  }\n\n  @Test\n  public void standaloneStop_shouldKeepClockStopped() throws Exception {\n    mediaClock.stop();\n    assertClockIsStopped();\n  }\n\n  @Test\n  public void standaloneStartAndStop_shouldStopClock() throws Exception {\n    mediaClock.start();\n    mediaClock.stop();\n    assertClockIsStopped();\n  }\n\n  @Test\n  public void standaloneStartStopStart_shouldRestartClock() throws Exception {\n    mediaClock.start();\n    mediaClock.stop();\n    mediaClock.start();\n    assertClockIsRunning();\n  }\n\n  @Test\n  public void standaloneStartAndStop_shouldNotTriggerCallback() throws Exception {\n    mediaClock.start();\n    mediaClock.stop();\n    verifyNoMoreInteractions(listener);\n  }\n\n  @Test\n  public void standaloneGetPlaybackParameters_initializedWithDefaultPlaybackParameters() {\n    assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT);\n  }\n\n  @Test\n  public void standaloneSetPlaybackParameters_getPlaybackParametersShouldReturnSameValue() {\n    PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);\n    assertThat(parameters).isEqualTo(TEST_PLAYBACK_PARAMETERS);\n    assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS);\n  }\n\n  @Test\n  public void standaloneSetPlaybackParameters_shouldTriggerCallback() {\n    mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);\n    verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS);\n  }\n\n  @Test\n  public void standaloneSetPlaybackParameters_shouldApplyNewPlaybackSpeed() {\n    mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);\n    mediaClock.start();\n    // Asserts that clock is running with speed declared in getPlaybackParameters().\n    assertClockIsRunning();\n  }\n\n  @Test\n  public void standaloneSetOtherPlaybackParameters_getPlaybackParametersShouldReturnSameValue() {\n    mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);\n    PlaybackParameters parameters = mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT);\n    assertThat(parameters).isEqualTo(PlaybackParameters.DEFAULT);\n    assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT);\n  }\n\n  @Test\n  public void standaloneSetOtherPlaybackParameters_shouldTriggerCallbackAgain() {\n    mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);\n    mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT);\n    verify(listener).onPlaybackParametersChanged(PlaybackParameters.DEFAULT);\n  }\n\n  @Test\n  public void standaloneSetSamePlaybackParametersAgain_shouldTriggerCallbackAgain() {\n    mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);\n    mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);\n    verify(listener, times(2)).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS);\n  }\n\n  @Test\n  public void enableRendererMediaClock_shouldOverwriteRendererPlaybackParametersIfPossible()\n      throws ExoPlaybackException {\n    FakeMediaClockRenderer mediaClockRenderer =\n        new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ true);\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT);\n    verifyNoMoreInteractions(listener);\n  }\n\n  @Test\n  public void enableRendererMediaClockWithFixedParameters_usesRendererPlaybackParameters()\n      throws ExoPlaybackException {\n    FakeMediaClockRenderer mediaClockRenderer =\n        new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false);\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS);\n  }\n\n  @Test\n  public void enableRendererMediaClockWithFixedParameters_shouldTriggerCallback()\n      throws ExoPlaybackException {\n    FakeMediaClockRenderer mediaClockRenderer =\n        new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false);\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS);\n  }\n\n  @Test\n  public void enableRendererMediaClockWithFixedButSamePlaybackParameters_shouldNotTriggerCallback()\n      throws ExoPlaybackException {\n    FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT,\n        /* playbackParametersAreMutable= */ false);\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    verifyNoMoreInteractions(listener);\n  }\n\n  @Test\n  public void disableRendererMediaClock_shouldKeepPlaybackParameters()\n      throws ExoPlaybackException {\n    FakeMediaClockRenderer mediaClockRenderer =\n        new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false);\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    mediaClock.onRendererDisabled(mediaClockRenderer);\n    assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS);\n  }\n\n  @Test\n  public void rendererClockSetPlaybackParameters_getPlaybackParametersShouldReturnSameValue()\n      throws ExoPlaybackException {\n    FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT,\n        /* playbackParametersAreMutable= */ true);\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);\n    assertThat(parameters).isEqualTo(TEST_PLAYBACK_PARAMETERS);\n    assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS);\n  }\n\n  @Test\n  public void rendererClockSetPlaybackParameters_shouldTriggerCallback()\n      throws ExoPlaybackException {\n    FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT,\n        /* playbackParametersAreMutable= */ true);\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);\n    verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS);\n  }\n\n  @Test\n  public void rendererClockSetPlaybackParametersOverwrite_getParametersShouldReturnSameValue()\n      throws ExoPlaybackException {\n    FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT,\n        /* playbackParametersAreMutable= */ false);\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);\n    assertThat(parameters).isEqualTo(PlaybackParameters.DEFAULT);\n    assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT);\n  }\n\n  @Test\n  public void rendererClockSetPlaybackParametersOverwrite_shouldTriggerCallback()\n      throws ExoPlaybackException {\n    FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT,\n        /* playbackParametersAreMutable= */ false);\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS);\n    verify(listener).onPlaybackParametersChanged(PlaybackParameters.DEFAULT);\n  }\n\n  @Test\n  public void enableRendererMediaClock_usesRendererClockPosition() throws ExoPlaybackException {\n    MediaClockRenderer mediaClockRenderer = new MediaClockRenderer();\n    mediaClock.start();\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    mediaClockRenderer.positionUs = TEST_POSITION_US;\n    assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US);\n    // We're not advancing the renderer media clock. Thus, the clock should appear to be stopped.\n    assertClockIsStopped();\n  }\n\n  @Test\n  public void resetPositionWhileUsingRendererMediaClock_shouldHaveNoEffect()\n      throws ExoPlaybackException {\n    MediaClockRenderer mediaClockRenderer = new MediaClockRenderer();\n    mediaClock.start();\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    mediaClockRenderer.positionUs = TEST_POSITION_US;\n    assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US);\n    mediaClock.resetPosition(0);\n    assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US);\n  }\n\n  @Test\n  public void disableRendererMediaClock_standaloneShouldBeSynced() throws ExoPlaybackException {\n    MediaClockRenderer mediaClockRenderer = new MediaClockRenderer();\n    mediaClock.start();\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    mediaClockRenderer.positionUs = TEST_POSITION_US;\n    mediaClock.syncAndGetPositionUs();\n    mediaClock.onRendererDisabled(mediaClockRenderer);\n    fakeClock.advanceTime(SLEEP_TIME_MS);\n    assertThat(mediaClock.syncAndGetPositionUs())\n        .isEqualTo(TEST_POSITION_US + C.msToUs(SLEEP_TIME_MS));\n    assertClockIsRunning();\n  }\n\n  @Test\n  public void getPositionWithPlaybackParameterChange_shouldTriggerCallback()\n      throws ExoPlaybackException {\n    MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT,\n            /* playbackParametersAreMutable= */ true);\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    // Silently change playback parameters of renderer clock.\n    mediaClockRenderer.playbackParameters = TEST_PLAYBACK_PARAMETERS;\n    mediaClock.syncAndGetPositionUs();\n    verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS);\n  }\n\n  @Test\n  public void rendererNotReady_shouldStillUseRendererClock() throws ExoPlaybackException {\n    MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(/* isReady= */ false,\n        /* isEnded= */ false, /* hasReadStreamToEnd= */ false);\n    mediaClock.start();\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    // We're not advancing the renderer media clock. Thus, the clock should appear to be stopped.\n    assertClockIsStopped();\n  }\n\n  @Test\n  public void rendererNotReadyAndReadStreamToEnd_shouldFallbackToStandaloneClock()\n      throws ExoPlaybackException {\n    MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(/* isReady= */ false,\n        /* isEnded= */ false, /* hasReadStreamToEnd= */ true);\n    mediaClock.start();\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    assertClockIsRunning();\n  }\n\n  @Test\n  public void rendererEnded_shouldFallbackToStandaloneClock()\n      throws ExoPlaybackException {\n    MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(/* isReady= */ true,\n        /* isEnded= */ true, /* hasReadStreamToEnd= */ true);\n    mediaClock.start();\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    assertClockIsRunning();\n  }\n\n  @Test\n  public void staleDisableRendererClock_shouldNotThrow()\n      throws ExoPlaybackException {\n    MediaClockRenderer mediaClockRenderer = new MediaClockRenderer();\n    mediaClockRenderer.positionUs = TEST_POSITION_US;\n    mediaClock.onRendererDisabled(mediaClockRenderer);\n    assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(C.msToUs(fakeClock.elapsedRealtime()));\n  }\n\n  @Test\n  public void enableSameRendererClockTwice_shouldNotThrow()\n      throws ExoPlaybackException {\n    MediaClockRenderer mediaClockRenderer = new MediaClockRenderer();\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    mediaClock.onRendererEnabled(mediaClockRenderer);\n    mediaClockRenderer.positionUs = TEST_POSITION_US;\n    assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US);\n  }\n\n  @Test\n  public void enableOtherRendererClock_shouldThrow()\n      throws ExoPlaybackException {\n    MediaClockRenderer mediaClockRenderer1 = new MediaClockRenderer();\n    MediaClockRenderer mediaClockRenderer2 = new MediaClockRenderer();\n    mediaClockRenderer1.positionUs = TEST_POSITION_US;\n    mediaClock.onRendererEnabled(mediaClockRenderer1);\n    try {\n      mediaClock.onRendererEnabled(mediaClockRenderer2);\n      fail();\n    } catch (ExoPlaybackException e) {\n      // Expected.\n    }\n    assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US);\n  }\n\n  private void assertClockIsRunning() {\n    long clockStartUs = mediaClock.syncAndGetPositionUs();\n    fakeClock.advanceTime(SLEEP_TIME_MS);\n    assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(clockStartUs\n        + mediaClock.getPlaybackParameters().getMediaTimeUsForPlayoutTimeMs(SLEEP_TIME_MS));\n  }\n\n  private void assertClockIsStopped() {\n    long positionAtStartUs = mediaClock.syncAndGetPositionUs();\n    fakeClock.advanceTime(SLEEP_TIME_MS);\n    assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(positionAtStartUs);\n  }\n\n  @SuppressWarnings(\"HidingField\")\n  private static class MediaClockRenderer extends FakeMediaClockRenderer {\n\n    private final boolean playbackParametersAreMutable;\n    private final boolean isReady;\n    private final boolean isEnded;\n\n    public PlaybackParameters playbackParameters;\n    public long positionUs;\n\n    public MediaClockRenderer() throws ExoPlaybackException {\n      this(PlaybackParameters.DEFAULT, false, true, false, false);\n    }\n\n    public MediaClockRenderer(PlaybackParameters playbackParameters,\n        boolean playbackParametersAreMutable)\n        throws ExoPlaybackException {\n      this(playbackParameters, playbackParametersAreMutable, true, false, false);\n    }\n\n    public MediaClockRenderer(boolean isReady, boolean isEnded, boolean hasReadStreamToEnd)\n        throws ExoPlaybackException {\n      this(PlaybackParameters.DEFAULT, false, isReady, isEnded, hasReadStreamToEnd);\n    }\n\n    private MediaClockRenderer(PlaybackParameters playbackParameters,\n        boolean playbackParametersAreMutable, boolean isReady, boolean isEnded,\n        boolean hasReadStreamToEnd)\n        throws ExoPlaybackException {\n      this.playbackParameters = playbackParameters;\n      this.playbackParametersAreMutable = playbackParametersAreMutable;\n      this.isReady = isReady;\n      this.isEnded = isEnded;\n      this.positionUs = TEST_POSITION_US;\n      if (!hasReadStreamToEnd) {\n        resetPosition(0);\n      }\n    }\n\n    @Override\n    public long getPositionUs() {\n      return positionUs;\n    }\n\n    @Override\n    public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {\n      if (playbackParametersAreMutable) {\n        this.playbackParameters = playbackParameters;\n      }\n      return this.playbackParameters;\n    }\n\n    @Override\n    public PlaybackParameters getPlaybackParameters() {\n      return playbackParameters;\n    }\n\n    @Override\n    public boolean isReady() {\n      return isReady;\n    }\n\n    @Override\n    public boolean isEnded() {\n      return isEnded;\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.content.Context;\nimport android.graphics.SurfaceTexture;\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport android.view.Surface;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.Player.DiscontinuityReason;\nimport com.google.android.exoplayer2.Player.EventListener;\nimport com.google.android.exoplayer2.Timeline.Window;\nimport com.google.android.exoplayer2.source.ClippingMediaSource;\nimport com.google.android.exoplayer2.source.ConcatenatingMediaSource;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.source.ads.AdPlaybackState;\nimport com.google.android.exoplayer2.testutil.ActionSchedule;\nimport com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable;\nimport com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget;\nimport com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;\nimport com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;\nimport com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder;\nimport com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;\nimport com.google.android.exoplayer2.testutil.FakeMediaPeriod;\nimport com.google.android.exoplayer2.testutil.FakeMediaSource;\nimport com.google.android.exoplayer2.testutil.FakeRenderer;\nimport com.google.android.exoplayer2.testutil.FakeShuffleOrder;\nimport com.google.android.exoplayer2.testutil.FakeTimeline;\nimport com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;\nimport com.google.android.exoplayer2.testutil.FakeTrackSelection;\nimport com.google.android.exoplayer2.testutil.FakeTrackSelector;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.Clock;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.concurrent.atomic.AtomicReference;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/** Unit test for {@link ExoPlayer}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic final class ExoPlayerTest {\n\n  /**\n   * For tests that rely on the player transitioning to the ended state, the duration in\n   * milliseconds after starting the player before the test will time out. This is to catch cases\n   * where the player under test is not making progress, in which case the test should fail.\n   */\n  private static final int TIMEOUT_MS = 10000;\n\n  private Context context;\n\n  @Before\n  public void setUp() {\n    context = ApplicationProvider.getApplicationContext();\n  }\n\n  /**\n   * Tests playback of a source that exposes an empty timeline. Playback is expected to end without\n   * error.\n   */\n  @Test\n  public void testPlayEmptyTimeline() throws Exception {\n    Timeline timeline = Timeline.EMPTY;\n    FakeRenderer renderer = new FakeRenderer();\n    ExoPlayerTestRunner testRunner =\n        new Builder()\n            .setTimeline(timeline)\n            .setRenderers(renderer)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertNoPositionDiscontinuities();\n    testRunner.assertTimelinesEqual(timeline);\n    assertThat(renderer.formatReadCount).isEqualTo(0);\n    assertThat(renderer.sampleBufferReadCount).isEqualTo(0);\n    assertThat(renderer.isEnded).isFalse();\n  }\n\n  /** Tests playback of a source that exposes a single period. */\n  @Test\n  public void testPlaySinglePeriodTimeline() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    Object manifest = new Object();\n    FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    ExoPlayerTestRunner testRunner =\n        new Builder()\n            .setTimeline(timeline)\n            .setManifest(manifest)\n            .setRenderers(renderer)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertNoPositionDiscontinuities();\n    testRunner.assertTimelinesEqual(timeline);\n    testRunner.assertManifestsEqual(manifest);\n    testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);\n    testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)));\n    assertThat(renderer.formatReadCount).isEqualTo(1);\n    assertThat(renderer.sampleBufferReadCount).isEqualTo(1);\n    assertThat(renderer.isEnded).isTrue();\n  }\n\n  /** Tests playback of a source that exposes three periods. */\n  @Test\n  public void testPlayMultiPeriodTimeline() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 3);\n    FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    ExoPlayerTestRunner testRunner =\n        new Builder()\n            .setTimeline(timeline)\n            .setRenderers(renderer)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertPositionDiscontinuityReasonsEqual(\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);\n    testRunner.assertTimelinesEqual(timeline);\n    testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);\n    assertThat(renderer.formatReadCount).isEqualTo(3);\n    assertThat(renderer.sampleBufferReadCount).isEqualTo(3);\n    assertThat(renderer.isEnded).isTrue();\n  }\n\n  /** Tests playback of periods with very short duration. */\n  @Test\n  public void testPlayShortDurationPeriods() throws Exception {\n    // TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US / 100 = 1000 us per period.\n    Timeline timeline =\n        new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 100, /* id= */ 0));\n    FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    ExoPlayerTestRunner testRunner =\n        new Builder()\n            .setTimeline(timeline)\n            .setRenderers(renderer)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    Integer[] expectedReasons = new Integer[99];\n    Arrays.fill(expectedReasons, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);\n    testRunner.assertPositionDiscontinuityReasonsEqual(expectedReasons);\n    testRunner.assertTimelinesEqual(timeline);\n    testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);\n    assertThat(renderer.formatReadCount).isEqualTo(100);\n    assertThat(renderer.sampleBufferReadCount).isEqualTo(100);\n    assertThat(renderer.isEnded).isTrue();\n  }\n\n  /**\n   * Tests that the player does not unnecessarily reset renderers when playing a multi-period\n   * source.\n   */\n  @Test\n  public void testReadAheadToEndDoesNotResetRenderer() throws Exception {\n    // Use sufficiently short periods to ensure the player attempts to read all at once.\n    TimelineWindowDefinition windowDefinition0 =\n        new TimelineWindowDefinition(\n            /* periodCount= */ 1,\n            /* id= */ 0,\n            /* isSeekable= */ false,\n            /* isDynamic= */ false,\n            /* durationUs= */ 100_000);\n    TimelineWindowDefinition windowDefinition1 =\n        new TimelineWindowDefinition(\n            /* periodCount= */ 1,\n            /* id= */ 1,\n            /* isSeekable= */ false,\n            /* isDynamic= */ false,\n            /* durationUs= */ 100_000);\n    TimelineWindowDefinition windowDefinition2 =\n        new TimelineWindowDefinition(\n            /* periodCount= */ 1,\n            /* id= */ 2,\n            /* isSeekable= */ false,\n            /* isDynamic= */ false,\n            /* durationUs= */ 100_000);\n    Timeline timeline = new FakeTimeline(windowDefinition0, windowDefinition1, windowDefinition2);\n    final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    FakeMediaClockRenderer audioRenderer =\n        new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) {\n\n          @Override\n          public long getPositionUs() {\n            // Simulate the playback position lagging behind the reading position: the renderer\n            // media clock position will be the start of the timeline until the stream is set to be\n            // final, at which point it jumps to the end of the timeline allowing the playing period\n            // to advance.\n            return isCurrentStreamFinal() ? 30 : 0;\n          }\n\n          @Override\n          public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {\n            return PlaybackParameters.DEFAULT;\n          }\n\n          @Override\n          public PlaybackParameters getPlaybackParameters() {\n            return PlaybackParameters.DEFAULT;\n          }\n\n          @Override\n          public boolean isEnded() {\n            return videoRenderer.isEnded();\n          }\n        };\n    ExoPlayerTestRunner testRunner =\n        new Builder()\n            .setTimeline(timeline)\n            .setRenderers(videoRenderer, audioRenderer)\n            .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertPositionDiscontinuityReasonsEqual(\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);\n    testRunner.assertTimelinesEqual(timeline);\n    assertThat(audioRenderer.positionResetCount).isEqualTo(1);\n    assertThat(videoRenderer.isEnded).isTrue();\n    assertThat(audioRenderer.isEnded).isTrue();\n  }\n\n  @Test\n  public void testRepreparationGivesFreshSourceInfo() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    Object firstSourceManifest = new Object();\n    MediaSource firstSource =\n        new FakeMediaSource(timeline, firstSourceManifest, Builder.VIDEO_FORMAT);\n    final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1);\n    final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1);\n    MediaSource secondSource =\n        new FakeMediaSource(timeline, new Object(), Builder.VIDEO_FORMAT) {\n          @Override\n          public synchronized void prepareSourceInternal(\n              @Nullable TransferListener mediaTransferListener) {\n            super.prepareSourceInternal(mediaTransferListener);\n            // We've queued a source info refresh on the playback thread's event queue. Allow the\n            // test thread to prepare the player with the third source, and block this thread (the\n            // playback thread) until the test thread's call to prepare() has returned.\n            queuedSourceInfoCountDownLatch.countDown();\n            try {\n              completePreparationCountDownLatch.await();\n            } catch (InterruptedException e) {\n              throw new IllegalStateException(e);\n            }\n          }\n        };\n    Object thirdSourceManifest = new Object();\n    MediaSource thirdSource =\n        new FakeMediaSource(timeline, thirdSourceManifest, Builder.VIDEO_FORMAT);\n\n    // Prepare the player with a source with the first manifest and a non-empty timeline. Prepare\n    // the player again with a source and a new manifest, which will never be exposed. Allow the\n    // test thread to prepare the player with a third source, and block the playback thread until\n    // the test thread's call to prepare() has returned.\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testRepreparation\")\n            .waitForTimelineChanged(timeline)\n            .prepareSource(secondSource)\n            .executeRunnable(\n                () -> {\n                  try {\n                    queuedSourceInfoCountDownLatch.await();\n                  } catch (InterruptedException e) {\n                    // Ignore.\n                  }\n                })\n            .prepareSource(thirdSource)\n            .executeRunnable(completePreparationCountDownLatch::countDown)\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new Builder()\n            .setMediaSource(firstSource)\n            .setRenderers(renderer)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertNoPositionDiscontinuities();\n    // The first source's preparation completed with a non-empty timeline. When the player was\n    // re-prepared with the second source, it immediately exposed an empty timeline, but the source\n    // info refresh from the second source was suppressed as we re-prepared with the third source.\n    testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline);\n    testRunner.assertManifestsEqual(firstSourceManifest, null, thirdSourceManifest);\n    testRunner.assertTimelineChangeReasonsEqual(\n        Player.TIMELINE_CHANGE_REASON_PREPARED,\n        Player.TIMELINE_CHANGE_REASON_RESET,\n        Player.TIMELINE_CHANGE_REASON_PREPARED);\n    testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)));\n    assertThat(renderer.isEnded).isTrue();\n  }\n\n  @Test\n  public void testRepeatModeChanges() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 3);\n    FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testRepeatMode\")\n            .pause()\n            .waitForTimelineChanged(timeline)\n            .playUntilStartOfWindow(/* windowIndex= */ 1)\n            .setRepeatMode(Player.REPEAT_MODE_ONE)\n            .playUntilStartOfWindow(/* windowIndex= */ 1)\n            .setRepeatMode(Player.REPEAT_MODE_OFF)\n            .playUntilStartOfWindow(/* windowIndex= */ 2)\n            .setRepeatMode(Player.REPEAT_MODE_ONE)\n            .playUntilStartOfWindow(/* windowIndex= */ 2)\n            .setRepeatMode(Player.REPEAT_MODE_ALL)\n            .playUntilStartOfWindow(/* windowIndex= */ 0)\n            .setRepeatMode(Player.REPEAT_MODE_ONE)\n            .playUntilStartOfWindow(/* windowIndex= */ 0)\n            .playUntilStartOfWindow(/* windowIndex= */ 0)\n            .setRepeatMode(Player.REPEAT_MODE_OFF)\n            .play()\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setRenderers(renderer)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2);\n    testRunner.assertPositionDiscontinuityReasonsEqual(\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);\n    testRunner.assertTimelinesEqual(timeline);\n    testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);\n    assertThat(renderer.isEnded).isTrue();\n  }\n\n  @Test\n  public void testShuffleModeEnabledChanges() throws Exception {\n    Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1);\n    MediaSource[] fakeMediaSources = {\n      new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),\n      new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),\n      new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT)\n    };\n    ConcatenatingMediaSource mediaSource =\n        new ConcatenatingMediaSource(false, new FakeShuffleOrder(3), fakeMediaSources);\n    FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testShuffleModeEnabled\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .setRepeatMode(Player.REPEAT_MODE_ALL)\n            .playUntilStartOfWindow(/* windowIndex= */ 1)\n            .setShuffleModeEnabled(true)\n            .playUntilStartOfWindow(/* windowIndex= */ 1)\n            .setShuffleModeEnabled(false)\n            .setRepeatMode(Player.REPEAT_MODE_OFF)\n            .play()\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setMediaSource(mediaSource)\n            .setRenderers(renderer)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2);\n    testRunner.assertPositionDiscontinuityReasonsEqual(\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);\n    assertThat(renderer.isEnded).isTrue();\n  }\n\n  @Test\n  public void testAdGroupWithLoadErrorIsSkipped() throws Exception {\n    AdPlaybackState initialAdPlaybackState =\n        FakeTimeline.createAdPlaybackState(\n            /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 5 * C.MICROS_PER_SECOND);\n    Timeline fakeTimeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(\n                /* periodCount= */ 1,\n                /* id= */ 0,\n                /* isSeekable= */ true,\n                /* isDynamic= */ false,\n                /* durationUs= */ C.MICROS_PER_SECOND,\n                initialAdPlaybackState));\n    AdPlaybackState errorAdPlaybackState = initialAdPlaybackState.withAdLoadError(0, 0);\n    final Timeline adErrorTimeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(\n                /* periodCount= */ 1,\n                /* id= */ 0,\n                /* isSeekable= */ true,\n                /* isDynamic= */ false,\n                /* durationUs= */ C.MICROS_PER_SECOND,\n                errorAdPlaybackState));\n    final FakeMediaSource fakeMediaSource =\n        new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testAdGroupWithLoadErrorIsSkipped\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .executeRunnable(() -> fakeMediaSource.setNewSourceInfo(adErrorTimeline, null))\n            .waitForTimelineChanged(adErrorTimeline)\n            .play()\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setMediaSource(fakeMediaSource)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    // There is still one discontinuity from content to content for the failed ad insertion.\n    testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_AD_INSERTION);\n  }\n\n  @Test\n  public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception {\n    FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testPeriodHoldersReleased\")\n            .setRepeatMode(Player.REPEAT_MODE_ALL)\n            .waitForPositionDiscontinuity()\n            .seek(0) // Seek with repeat mode set to Player.REPEAT_MODE_ALL.\n            .waitForPositionDiscontinuity()\n            .setRepeatMode(Player.REPEAT_MODE_OFF) // Turn off repeat so that playback can finish.\n            .build();\n    new Builder()\n        .setRenderers(renderer)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(renderer.isEnded).isTrue();\n  }\n\n  @Test\n  public void testSeekProcessedCallback() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 2);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSeekProcessedCallback\")\n            // Initial seek. Expect immediate seek processed.\n            .pause()\n            .seek(5)\n            .waitForSeekProcessed()\n            // Multiple overlapping seeks while the player is still preparing. Expect only one seek\n            // processed.\n            .seek(2)\n            .seek(10)\n            // Wait until media source prepared and re-seek to same position. Expect a seek\n            // processed while still being in STATE_READY.\n            .waitForPlaybackState(Player.STATE_READY)\n            .seek(10)\n            // Start playback and wait until playback reaches second window.\n            .playUntilStartOfWindow(/* windowIndex= */ 1)\n            // Seek twice in concession, expecting the first seek to be replaced (and thus except\n            // only on seek processed callback).\n            .seek(5)\n            .seek(60)\n            .play()\n            .build();\n    final List<Integer> playbackStatesWhenSeekProcessed = new ArrayList<>();\n    EventListener eventListener =\n        new EventListener() {\n          private int currentPlaybackState = Player.STATE_IDLE;\n\n          @Override\n          public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n            currentPlaybackState = playbackState;\n          }\n\n          @Override\n          public void onSeekProcessed() {\n            playbackStatesWhenSeekProcessed.add(currentPlaybackState);\n          }\n        };\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setEventListener(eventListener)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertPositionDiscontinuityReasonsEqual(\n        Player.DISCONTINUITY_REASON_SEEK,\n        Player.DISCONTINUITY_REASON_SEEK,\n        Player.DISCONTINUITY_REASON_SEEK,\n        Player.DISCONTINUITY_REASON_SEEK,\n        Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,\n        Player.DISCONTINUITY_REASON_SEEK,\n        Player.DISCONTINUITY_REASON_SEEK);\n    assertThat(playbackStatesWhenSeekProcessed)\n        .containsExactly(\n            Player.STATE_BUFFERING,\n            Player.STATE_BUFFERING,\n            Player.STATE_READY,\n            Player.STATE_BUFFERING)\n        .inOrder();\n  }\n\n  @Test\n  public void testSeekProcessedCalledWithIllegalSeekPosition() throws Exception {\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSeekProcessedCalledWithIllegalSeekPosition\")\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            // The illegal seek position will end playback.\n            .seek(/* windowIndex= */ 100, /* positionMs= */ 0)\n            .waitForPlaybackState(Player.STATE_ENDED)\n            .build();\n    final boolean[] onSeekProcessedCalled = new boolean[1];\n    EventListener listener =\n        new EventListener() {\n          @Override\n          public void onSeekProcessed() {\n            onSeekProcessedCalled[0] = true;\n          }\n        };\n    ExoPlayerTestRunner testRunner =\n        new Builder().setActionSchedule(actionSchedule).setEventListener(listener).build(context);\n    testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);\n    assertThat(onSeekProcessedCalled[0]).isTrue();\n  }\n\n  @Test\n  public void testSeekDiscontinuity() throws Exception {\n    FakeTimeline timeline = new FakeTimeline(1);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSeekDiscontinuity\").seek(10).build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);\n  }\n\n  @Test\n  public void testSeekDiscontinuityWithAdjustment() throws Exception {\n    FakeTimeline timeline = new FakeTimeline(1);\n    FakeMediaSource mediaSource =\n        new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) {\n          @Override\n          protected FakeMediaPeriod createFakeMediaPeriod(\n              MediaPeriodId id,\n              TrackGroupArray trackGroupArray,\n              Allocator allocator,\n              EventDispatcher eventDispatcher,\n              @Nullable TransferListener transferListener) {\n            FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray, eventDispatcher);\n            mediaPeriod.setSeekToUsOffset(10);\n            return mediaPeriod;\n          }\n        };\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSeekDiscontinuityAdjust\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .seek(10)\n            .play()\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setMediaSource(mediaSource)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertPositionDiscontinuityReasonsEqual(\n        Player.DISCONTINUITY_REASON_SEEK, Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT);\n  }\n\n  @Test\n  public void testInternalDiscontinuityAtNewPosition() throws Exception {\n    FakeTimeline timeline = new FakeTimeline(1);\n    FakeMediaSource mediaSource =\n        new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) {\n          @Override\n          protected FakeMediaPeriod createFakeMediaPeriod(\n              MediaPeriodId id,\n              TrackGroupArray trackGroupArray,\n              Allocator allocator,\n              EventDispatcher eventDispatcher,\n              @Nullable TransferListener transferListener) {\n            FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray, eventDispatcher);\n            mediaPeriod.setDiscontinuityPositionUs(10);\n            return mediaPeriod;\n          }\n        };\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setMediaSource(mediaSource)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_INTERNAL);\n  }\n\n  @Test\n  public void testInternalDiscontinuityAtInitialPosition() throws Exception {\n    FakeTimeline timeline = new FakeTimeline(1);\n    FakeMediaSource mediaSource =\n        new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) {\n          @Override\n          protected FakeMediaPeriod createFakeMediaPeriod(\n              MediaPeriodId id,\n              TrackGroupArray trackGroupArray,\n              Allocator allocator,\n              EventDispatcher eventDispatcher,\n              @Nullable TransferListener transferListener) {\n            FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray, eventDispatcher);\n            mediaPeriod.setDiscontinuityPositionUs(0);\n            return mediaPeriod;\n          }\n        };\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setMediaSource(mediaSource)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    // If the position is unchanged we do not expect the discontinuity to be reported externally.\n    testRunner.assertNoPositionDiscontinuities();\n  }\n\n  @Test\n  public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    MediaSource mediaSource =\n        new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);\n    FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);\n    FakeTrackSelector trackSelector = new FakeTrackSelector();\n\n    new Builder()\n        .setMediaSource(mediaSource)\n        .setRenderers(videoRenderer, audioRenderer)\n        .setTrackSelector(trackSelector)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n\n    List<FakeTrackSelection> createdTrackSelections = trackSelector.getAllTrackSelections();\n    int numSelectionsEnabled = 0;\n    // Assert that all tracks selection are disabled at the end of the playback.\n    for (FakeTrackSelection trackSelection : createdTrackSelections) {\n      assertThat(trackSelection.isEnabled).isFalse();\n      numSelectionsEnabled += trackSelection.enableCount;\n    }\n    // There are 2 renderers, and track selections are made once (1 period).\n    assertThat(createdTrackSelections).hasSize(2);\n    assertThat(numSelectionsEnabled).isEqualTo(2);\n  }\n\n  @Test\n  public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 2);\n    MediaSource mediaSource =\n        new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);\n    FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);\n    FakeTrackSelector trackSelector = new FakeTrackSelector();\n\n    new Builder()\n        .setMediaSource(mediaSource)\n        .setRenderers(videoRenderer, audioRenderer)\n        .setTrackSelector(trackSelector)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n\n    List<FakeTrackSelection> createdTrackSelections = trackSelector.getAllTrackSelections();\n    int numSelectionsEnabled = 0;\n    // Assert that all tracks selection are disabled at the end of the playback.\n    for (FakeTrackSelection trackSelection : createdTrackSelections) {\n      assertThat(trackSelection.isEnabled).isFalse();\n      numSelectionsEnabled += trackSelection.enableCount;\n    }\n    // There are 2 renderers, and track selections are made twice (2 periods).\n    assertThat(createdTrackSelections).hasSize(4);\n    assertThat(numSelectionsEnabled).isEqualTo(4);\n  }\n\n  @Test\n  public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade()\n      throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    MediaSource mediaSource =\n        new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);\n    FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);\n    final FakeTrackSelector trackSelector = new FakeTrackSelector();\n    ActionSchedule disableTrackAction =\n        new ActionSchedule.Builder(\"testChangeTrackSelection\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .disableRenderer(0)\n            .play()\n            .build();\n\n    new Builder()\n        .setMediaSource(mediaSource)\n        .setRenderers(videoRenderer, audioRenderer)\n        .setTrackSelector(trackSelector)\n        .setActionSchedule(disableTrackAction)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n\n    List<FakeTrackSelection> createdTrackSelections = trackSelector.getAllTrackSelections();\n    int numSelectionsEnabled = 0;\n    // Assert that all tracks selection are disabled at the end of the playback.\n    for (FakeTrackSelection trackSelection : createdTrackSelections) {\n      assertThat(trackSelection.isEnabled).isFalse();\n      numSelectionsEnabled += trackSelection.enableCount;\n    }\n    // There are 2 renderers, and track selections are made twice. The second time one renderer is\n    // disabled, so only one out of the two track selections is enabled.\n    assertThat(createdTrackSelections).hasSize(3);\n    assertThat(numSelectionsEnabled).isEqualTo(3);\n  }\n\n  @Test\n  public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreReused()\n      throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    MediaSource mediaSource =\n        new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);\n    FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);\n    final FakeTrackSelector trackSelector = new FakeTrackSelector(/* reuse track selection */ true);\n    ActionSchedule disableTrackAction =\n        new ActionSchedule.Builder(\"testReuseTrackSelection\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .disableRenderer(0)\n            .play()\n            .build();\n\n    new Builder()\n        .setMediaSource(mediaSource)\n        .setRenderers(videoRenderer, audioRenderer)\n        .setTrackSelector(trackSelector)\n        .setActionSchedule(disableTrackAction)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n\n    List<FakeTrackSelection> createdTrackSelections = trackSelector.getAllTrackSelections();\n    int numSelectionsEnabled = 0;\n    // Assert that all tracks selection are disabled at the end of the playback.\n    for (FakeTrackSelection trackSelection : createdTrackSelections) {\n      assertThat(trackSelection.isEnabled).isFalse();\n      numSelectionsEnabled += trackSelection.enableCount;\n    }\n    // There are 2 renderers, and track selections are made twice. The second time one renderer is\n    // disabled, and the selector re-uses the previous selection for the enabled renderer. So we\n    // expect two track selections, one of which will have been enabled twice.\n    assertThat(createdTrackSelections).hasSize(2);\n    assertThat(numSelectionsEnabled).isEqualTo(3);\n  }\n\n  @Test\n  public void testDynamicTimelineChangeReason() throws Exception {\n    Timeline timeline1 = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000));\n    final Timeline timeline2 = new FakeTimeline(new TimelineWindowDefinition(false, false, 20000));\n    final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, null, Builder.VIDEO_FORMAT);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testDynamicTimelineChangeReason\")\n            .pause()\n            .waitForTimelineChanged(timeline1)\n            .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, null))\n            .waitForTimelineChanged(timeline2)\n            .play()\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setMediaSource(mediaSource)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertTimelinesEqual(timeline1, timeline2);\n    testRunner.assertTimelineChangeReasonsEqual(\n        Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_DYNAMIC);\n  }\n\n  @Test\n  public void testRepreparationWithPositionResetAndShufflingUsesFirstPeriod() throws Exception {\n    Timeline fakeTimeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(\n                /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 100000));\n    ConcatenatingMediaSource firstMediaSource =\n        new ConcatenatingMediaSource(\n            /* isAtomic= */ false,\n            new FakeShuffleOrder(/* length= */ 2),\n            new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),\n            new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT));\n    ConcatenatingMediaSource secondMediaSource =\n        new ConcatenatingMediaSource(\n            /* isAtomic= */ false,\n            new FakeShuffleOrder(/* length= */ 2),\n            new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),\n            new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT));\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testRepreparationWithShuffle\")\n            // Wait for first preparation and enable shuffling. Plays period 0.\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .setShuffleModeEnabled(true)\n            // Reprepare with second media source (keeping state, but with position reset).\n            // Plays period 1 and 0 because of the reversed fake shuffle order.\n            .prepareSource(secondMediaSource, /* resetPosition= */ true, /* resetState= */ false)\n            .play()\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setMediaSource(firstMediaSource)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertPlayedPeriodIndices(0, 1, 0);\n  }\n\n  @Test\n  public void testSetPlaybackParametersBeforePreparationCompletesSucceeds() throws Exception {\n    // Test that no exception is thrown when playback parameters are updated between creating a\n    // period and preparation of the period completing.\n    final CountDownLatch createPeriodCalledCountDownLatch = new CountDownLatch(1);\n    final FakeMediaPeriod[] fakeMediaPeriodHolder = new FakeMediaPeriod[1];\n    MediaSource mediaSource =\n        new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), null, Builder.VIDEO_FORMAT) {\n          @Override\n          protected FakeMediaPeriod createFakeMediaPeriod(\n              MediaPeriodId id,\n              TrackGroupArray trackGroupArray,\n              Allocator allocator,\n              EventDispatcher eventDispatcher,\n              @Nullable TransferListener transferListener) {\n            // Defer completing preparation of the period until playback parameters have been set.\n            fakeMediaPeriodHolder[0] =\n                new FakeMediaPeriod(trackGroupArray, eventDispatcher, /* deferOnPrepared= */ true);\n            createPeriodCalledCountDownLatch.countDown();\n            return fakeMediaPeriodHolder[0];\n          }\n        };\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSetPlaybackParametersBeforePreparationCompletesSucceeds\")\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            // Block until createPeriod has been called on the fake media source.\n            .executeRunnable(\n                () -> {\n                  try {\n                    createPeriodCalledCountDownLatch.await();\n                  } catch (InterruptedException e) {\n                    throw new IllegalStateException(e);\n                  }\n                })\n            // Set playback parameters (while the fake media period is not yet prepared).\n            .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f, /* pitch= */ 2f))\n            // Complete preparation of the fake media period.\n            .executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete())\n            .build();\n    new ExoPlayerTestRunner.Builder()\n        .setMediaSource(mediaSource)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n  }\n\n  @Test\n  public void testStopDoesNotResetPosition() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    final long[] positionHolder = new long[1];\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testStopDoesNotResetPosition\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 50)\n            .stop()\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    positionHolder[0] = player.getCurrentPosition();\n                  }\n                })\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilActionScheduleFinished(TIMEOUT_MS)\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertTimelinesEqual(timeline);\n    testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);\n    testRunner.assertNoPositionDiscontinuities();\n    assertThat(positionHolder[0]).isAtLeast(50L);\n  }\n\n  @Test\n  public void testStopWithoutResetDoesNotResetPosition() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    final long[] positionHolder = new long[1];\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testStopWithoutResetDoesNotReset\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 50)\n            .stop(/* reset= */ false)\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    positionHolder[0] = player.getCurrentPosition();\n                  }\n                })\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilActionScheduleFinished(TIMEOUT_MS)\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertTimelinesEqual(timeline);\n    testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);\n    testRunner.assertNoPositionDiscontinuities();\n    assertThat(positionHolder[0]).isAtLeast(50L);\n  }\n\n  @Test\n  public void testStopWithResetDoesResetPosition() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    final long[] positionHolder = new long[1];\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testStopWithResetDoesReset\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 50)\n            .stop(/* reset= */ true)\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    positionHolder[0] = player.getCurrentPosition();\n                  }\n                })\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilActionScheduleFinished(TIMEOUT_MS)\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY);\n    testRunner.assertTimelineChangeReasonsEqual(\n        Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_RESET);\n    testRunner.assertNoPositionDiscontinuities();\n    assertThat(positionHolder[0]).isEqualTo(0);\n  }\n\n  @Test\n  public void testStopWithoutResetReleasesMediaSource() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    final FakeMediaSource mediaSource =\n        new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testStopReleasesMediaSource\")\n            .waitForPlaybackState(Player.STATE_READY)\n            .stop(/* reset= */ false)\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilActionScheduleFinished(TIMEOUT_MS);\n    mediaSource.assertReleased();\n    testRunner.blockUntilEnded(TIMEOUT_MS);\n  }\n\n  @Test\n  public void testStopWithResetReleasesMediaSource() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    final FakeMediaSource mediaSource =\n        new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testStopReleasesMediaSource\")\n            .waitForPlaybackState(Player.STATE_READY)\n            .stop(/* reset= */ true)\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilActionScheduleFinished(TIMEOUT_MS);\n    mediaSource.assertReleased();\n    testRunner.blockUntilEnded(TIMEOUT_MS);\n  }\n\n  @Test\n  public void testRepreparationDoesNotResetAfterStopWithReset() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    MediaSource secondSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testRepreparationAfterStop\")\n            .waitForPlaybackState(Player.STATE_READY)\n            .stop(/* reset= */ true)\n            .waitForPlaybackState(Player.STATE_IDLE)\n            .prepareSource(secondSource)\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .setExpectedPlayerEndedCount(2)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline);\n    testRunner.assertTimelineChangeReasonsEqual(\n        Player.TIMELINE_CHANGE_REASON_PREPARED,\n        Player.TIMELINE_CHANGE_REASON_RESET,\n        Player.TIMELINE_CHANGE_REASON_PREPARED);\n    testRunner.assertNoPositionDiscontinuities();\n  }\n\n  @Test\n  public void testSeekBeforeRepreparationPossibleAfterStopWithReset() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2);\n    MediaSource secondSource = new FakeMediaSource(secondTimeline, null, Builder.VIDEO_FORMAT);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSeekAfterStopWithReset\")\n            .waitForPlaybackState(Player.STATE_READY)\n            .stop(/* reset= */ true)\n            .waitForPlaybackState(Player.STATE_IDLE)\n            // If we were still using the first timeline, this would throw.\n            .seek(/* windowIndex= */ 1, /* positionMs= */ 0)\n            .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true)\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .setExpectedPlayerEndedCount(2)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline);\n    testRunner.assertTimelineChangeReasonsEqual(\n        Player.TIMELINE_CHANGE_REASON_PREPARED,\n        Player.TIMELINE_CHANGE_REASON_RESET,\n        Player.TIMELINE_CHANGE_REASON_PREPARED);\n    testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);\n    testRunner.assertPlayedPeriodIndices(0, 1);\n  }\n\n  @Test\n  public void testReprepareAndKeepPositionWithNewMediaSource() throws Exception {\n    Timeline timeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object()));\n    Timeline secondTimeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object()));\n    MediaSource secondSource = new FakeMediaSource(secondTimeline, /* manifest= */ null);\n    AtomicLong positionAfterReprepare = new AtomicLong();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testReprepareAndKeepPositionWithNewMediaSource\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000)\n            .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true)\n            .waitForTimelineChanged(secondTimeline)\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    positionAfterReprepare.set(player.getCurrentPosition());\n                  }\n                })\n            .play()\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilActionScheduleFinished(TIMEOUT_MS)\n            .blockUntilEnded(TIMEOUT_MS);\n\n    testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline);\n    assertThat(positionAfterReprepare.get()).isAtLeast(2000L);\n  }\n\n  @Test\n  public void testStopDuringPreparationOverwritesPreparation() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testStopOverwritesPrepare\")\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .seek(0)\n            .stop(true)\n            .waitForSeekProcessed()\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilActionScheduleFinished(TIMEOUT_MS)\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertTimelinesEqual(Timeline.EMPTY);\n    testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);\n    testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);\n  }\n\n  @Test\n  public void testStopAndSeekAfterStopDoesNotResetTimeline() throws Exception {\n    // Combining additional stop and seek after initial stop in one test to get the seek processed\n    // callback which ensures that all operations have been processed by the player.\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testStopTwice\")\n            .waitForPlaybackState(Player.STATE_READY)\n            .stop(false)\n            .stop(false)\n            .seek(0)\n            .waitForSeekProcessed()\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilActionScheduleFinished(TIMEOUT_MS)\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertTimelinesEqual(timeline);\n    testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);\n    testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);\n  }\n\n  @Test\n  public void testReprepareAfterPlaybackError() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testReprepareAfterPlaybackError\")\n            .waitForPlaybackState(Player.STATE_READY)\n            .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))\n            .waitForPlaybackState(Player.STATE_IDLE)\n            .prepareSource(\n                new FakeMediaSource(timeline, /* manifest= */ null),\n                /* resetPosition= */ true,\n                /* resetState= */ false)\n            .waitForPlaybackState(Player.STATE_READY)\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .build(context);\n    try {\n      testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);\n      fail();\n    } catch (ExoPlaybackException e) {\n      // Expected exception.\n    }\n    testRunner.assertTimelinesEqual(timeline, timeline);\n    testRunner.assertTimelineChangeReasonsEqual(\n        Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED);\n  }\n\n  @Test\n  public void testSeekAndReprepareAfterPlaybackError() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    final long[] positionHolder = new long[2];\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testReprepareAfterPlaybackError\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))\n            .waitForPlaybackState(Player.STATE_IDLE)\n            .seek(/* positionMs= */ 50)\n            .waitForSeekProcessed()\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    positionHolder[0] = player.getCurrentPosition();\n                  }\n                })\n            .prepareSource(\n                new FakeMediaSource(timeline, /* manifest= */ null),\n                /* resetPosition= */ false,\n                /* resetState= */ false)\n            .waitForPlaybackState(Player.STATE_READY)\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    positionHolder[1] = player.getCurrentPosition();\n                  }\n                })\n            .play()\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .build(context);\n    try {\n      testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);\n      fail();\n    } catch (ExoPlaybackException e) {\n      // Expected exception.\n    }\n    testRunner.assertTimelinesEqual(timeline, timeline);\n    testRunner.assertTimelineChangeReasonsEqual(\n        Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED);\n    testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);\n    assertThat(positionHolder[0]).isEqualTo(50);\n    assertThat(positionHolder[1]).isEqualTo(50);\n  }\n\n  @Test\n  public void testInvalidSeekPositionAfterSourceInfoRefreshStillUpdatesTimeline() throws Exception {\n    final Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    final FakeMediaSource mediaSource =\n        new FakeMediaSource(/* timeline= */ null, /* manifest= */ null);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testInvalidSeekPositionSourceInfoRefreshStillUpdatesTimeline\")\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            // Seeking to an invalid position will end playback.\n            .seek(/* windowIndex= */ 100, /* positionMs= */ 0)\n            .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null))\n            .waitForPlaybackState(Player.STATE_ENDED)\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setMediaSource(mediaSource)\n            .setActionSchedule(actionSchedule)\n            .build(context);\n    testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);\n\n    testRunner.assertTimelinesEqual(timeline);\n    testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);\n  }\n\n  @Test\n  public void\n      testInvalidSeekPositionAfterSourceInfoRefreshWithShuffleModeEnabledUsesCorrectFirstPeriod()\n          throws Exception {\n    FakeMediaSource mediaSource =\n        new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null);\n    ConcatenatingMediaSource concatenatingMediaSource =\n        new ConcatenatingMediaSource(\n            /* isAtomic= */ false, new FakeShuffleOrder(0), mediaSource, mediaSource);\n    AtomicInteger windowIndexAfterUpdate = new AtomicInteger();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testInvalidSeekPositionSourceInfoRefreshUsesCorrectFirstPeriod\")\n            .setShuffleModeEnabled(true)\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            // Seeking to an invalid position will end playback.\n            .seek(/* windowIndex= */ 100, /* positionMs= */ 0)\n            .waitForPlaybackState(Player.STATE_ENDED)\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    windowIndexAfterUpdate.set(player.getCurrentWindowIndex());\n                  }\n                })\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setMediaSource(concatenatingMediaSource)\n            .setActionSchedule(actionSchedule)\n            .build(context);\n    testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);\n\n    assertThat(windowIndexAfterUpdate.get()).isEqualTo(1);\n  }\n\n  @Test\n  public void testRestartAfterEmptyTimelineWithShuffleModeEnabledUsesCorrectFirstPeriod()\n      throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    FakeMediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null);\n    ConcatenatingMediaSource concatenatingMediaSource =\n        new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0));\n    AtomicInteger windowIndexAfterAddingSources = new AtomicInteger();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testRestartAfterEmptyTimelineUsesCorrectFirstPeriod\")\n            .setShuffleModeEnabled(true)\n            // Preparing with an empty media source will transition to ended state.\n            .waitForPlaybackState(Player.STATE_ENDED)\n            // Add two sources at once such that the default start position in the shuffled order\n            // will be the second source.\n            .executeRunnable(\n                () ->\n                    concatenatingMediaSource.addMediaSources(\n                        Arrays.asList(mediaSource, mediaSource)))\n            .waitForTimelineChanged()\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    windowIndexAfterAddingSources.set(player.getCurrentWindowIndex());\n                  }\n                })\n            .build();\n    new ExoPlayerTestRunner.Builder()\n        .setMediaSource(concatenatingMediaSource)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilActionScheduleFinished(TIMEOUT_MS)\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(windowIndexAfterAddingSources.get()).isEqualTo(1);\n  }\n\n  @Test\n  public void testPlaybackErrorAndReprepareDoesNotResetPosition() throws Exception {\n    final Timeline timeline = new FakeTimeline(/* windowCount= */ 2);\n    final long[] positionHolder = new long[3];\n    final int[] windowIndexHolder = new int[3];\n    final FakeMediaSource secondMediaSource =\n        new FakeMediaSource(/* timeline= */ null, /* manifest= */ null);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testPlaybackErrorDoesNotResetPosition\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 500)\n            .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))\n            .waitForPlaybackState(Player.STATE_IDLE)\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    // Position while in error state\n                    positionHolder[0] = player.getCurrentPosition();\n                    windowIndexHolder[0] = player.getCurrentWindowIndex();\n                  }\n                })\n            .prepareSource(secondMediaSource, /* resetPosition= */ false, /* resetState= */ false)\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    // Position while repreparing.\n                    positionHolder[1] = player.getCurrentPosition();\n                    windowIndexHolder[1] = player.getCurrentWindowIndex();\n                    secondMediaSource.setNewSourceInfo(timeline, /* newManifest= */ null);\n                  }\n                })\n            .waitForPlaybackState(Player.STATE_READY)\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    // Position after repreparation finished.\n                    positionHolder[2] = player.getCurrentPosition();\n                    windowIndexHolder[2] = player.getCurrentWindowIndex();\n                  }\n                })\n            .play()\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .build(context);\n    try {\n      testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);\n      fail();\n    } catch (ExoPlaybackException e) {\n      // Expected exception.\n    }\n    assertThat(positionHolder[0]).isAtLeast(500L);\n    assertThat(positionHolder[1]).isEqualTo(positionHolder[0]);\n    assertThat(positionHolder[2]).isEqualTo(positionHolder[0]);\n    assertThat(windowIndexHolder[0]).isEqualTo(1);\n    assertThat(windowIndexHolder[1]).isEqualTo(1);\n    assertThat(windowIndexHolder[2]).isEqualTo(1);\n  }\n\n  @Test\n  public void testPlaybackErrorTwiceStillKeepsTimeline() throws Exception {\n    final Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    final FakeMediaSource mediaSource2 =\n        new FakeMediaSource(/* timeline= */ null, /* manifest= */ null);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testPlaybackErrorDoesNotResetPosition\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))\n            .waitForPlaybackState(Player.STATE_IDLE)\n            .prepareSource(mediaSource2, /* resetPosition= */ false, /* resetState= */ false)\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))\n            .waitForPlaybackState(Player.STATE_IDLE)\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setTimeline(timeline)\n            .setActionSchedule(actionSchedule)\n            .build(context);\n    try {\n      testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);\n      fail();\n    } catch (ExoPlaybackException e) {\n      // Expected exception.\n    }\n    testRunner.assertTimelinesEqual(timeline, timeline);\n    testRunner.assertTimelineChangeReasonsEqual(\n        Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED);\n  }\n\n  @Test\n  public void testSendMessagesDuringPreparation() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .sendMessage(target, /* positionMs= */ 50)\n            .play()\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target.positionMs).isAtLeast(50L);\n  }\n\n  @Test\n  public void testSendMessagesAfterPreparation() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForTimelineChanged(timeline)\n            .sendMessage(target, /* positionMs= */ 50)\n            .play()\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target.positionMs).isAtLeast(50L);\n  }\n\n  @Test\n  public void testMultipleSendMessages() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    PositionGrabbingMessageTarget target50 = new PositionGrabbingMessageTarget();\n    PositionGrabbingMessageTarget target80 = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .sendMessage(target80, /* positionMs= */ 80)\n            .sendMessage(target50, /* positionMs= */ 50)\n            .play()\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target50.positionMs).isAtLeast(50L);\n    assertThat(target80.positionMs).isAtLeast(80L);\n    assertThat(target80.positionMs).isAtLeast(target50.positionMs);\n  }\n\n  @Test\n  public void testMultipleSendMessagesAtSameTime() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget();\n    PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .sendMessage(target1, /* positionMs= */ 50)\n            .sendMessage(target2, /* positionMs= */ 50)\n            .play()\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target1.positionMs).isAtLeast(50L);\n    assertThat(target2.positionMs).isAtLeast(50L);\n  }\n\n  @Test\n  public void testSendMessagesMultiPeriodResolution() throws Exception {\n    Timeline timeline =\n        new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 10, /* id= */ 0));\n    PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .sendMessage(target, /* positionMs= */ 50)\n            .play()\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target.positionMs).isAtLeast(50L);\n  }\n\n  @Test\n  public void testSendMessagesAtStartAndEndOfPeriod() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 2);\n    PositionGrabbingMessageTarget targetStartFirstPeriod = new PositionGrabbingMessageTarget();\n    PositionGrabbingMessageTarget targetEndMiddlePeriod = new PositionGrabbingMessageTarget();\n    PositionGrabbingMessageTarget targetStartMiddlePeriod = new PositionGrabbingMessageTarget();\n    PositionGrabbingMessageTarget targetEndLastPeriod = new PositionGrabbingMessageTarget();\n    long duration1Ms = timeline.getWindow(0, new Window()).getDurationMs();\n    long duration2Ms = timeline.getWindow(1, new Window()).getDurationMs();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0)\n            .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms)\n            .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0)\n            .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms)\n            .play()\n            // Add additional prepare at end and wait until it's processed to ensure that\n            // messages sent at end of playback are received before test ends.\n            .waitForPlaybackState(Player.STATE_ENDED)\n            .prepareSource(\n                new FakeMediaSource(timeline, null),\n                /* resetPosition= */ false,\n                /* resetState= */ true)\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .waitForPlaybackState(Player.STATE_ENDED)\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilActionScheduleFinished(TIMEOUT_MS)\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(targetStartFirstPeriod.windowIndex).isEqualTo(0);\n    assertThat(targetStartFirstPeriod.positionMs).isAtLeast(0L);\n    assertThat(targetEndMiddlePeriod.windowIndex).isEqualTo(0);\n    assertThat(targetEndMiddlePeriod.positionMs).isAtLeast(duration1Ms);\n    assertThat(targetStartMiddlePeriod.windowIndex).isEqualTo(1);\n    assertThat(targetStartMiddlePeriod.positionMs).isAtLeast(0L);\n    assertThat(targetEndLastPeriod.windowIndex).isEqualTo(1);\n    assertThat(targetEndLastPeriod.positionMs).isAtLeast(duration2Ms);\n  }\n\n  @Test\n  public void testSendMessagesSeekOnDeliveryTimeDuringPreparation() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .sendMessage(target, /* positionMs= */ 50)\n            .seek(/* positionMs= */ 50)\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target.positionMs).isAtLeast(50L);\n  }\n\n  @Test\n  public void testSendMessagesSeekOnDeliveryTimeAfterPreparation() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .sendMessage(target, /* positionMs= */ 50)\n            .waitForTimelineChanged(timeline)\n            .seek(/* positionMs= */ 50)\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target.positionMs).isAtLeast(50L);\n  }\n\n  @Test\n  public void testSendMessagesSeekAfterDeliveryTimeDuringPreparation() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .sendMessage(target, /* positionMs= */ 50)\n            .seek(/* positionMs= */ 51)\n            .play()\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET);\n  }\n\n  @Test\n  public void testSendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .sendMessage(target, /* positionMs= */ 50)\n            .waitForTimelineChanged(timeline)\n            .seek(/* positionMs= */ 51)\n            .play()\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET);\n  }\n\n  @Test\n  public void testSendMessagesRepeatDoesNotRepost() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .sendMessage(target, /* positionMs= */ 50)\n            .setRepeatMode(Player.REPEAT_MODE_ALL)\n            .play()\n            .waitForPositionDiscontinuity()\n            .setRepeatMode(Player.REPEAT_MODE_OFF)\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target.messageCount).isEqualTo(1);\n    assertThat(target.positionMs).isAtLeast(50L);\n  }\n\n  @Test\n  public void testSendMessagesRepeatWithoutDeletingDoesRepost() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .sendMessage(\n                target,\n                /* windowIndex= */ 0,\n                /* positionMs= */ 50,\n                /* deleteAfterDelivery= */ false)\n            .setRepeatMode(Player.REPEAT_MODE_ALL)\n            .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 1)\n            .playUntilStartOfWindow(/* windowIndex= */ 0)\n            .setRepeatMode(Player.REPEAT_MODE_OFF)\n            .play()\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target.messageCount).isEqualTo(2);\n    assertThat(target.positionMs).isAtLeast(50L);\n  }\n\n  @Test\n  public void testSendMessagesMoveCurrentWindowIndex() throws Exception {\n    Timeline timeline =\n        new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0));\n    final Timeline secondTimeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1),\n            new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0));\n    final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT);\n    PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForTimelineChanged(timeline)\n            .sendMessage(target, /* positionMs= */ 50)\n            .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null))\n            .waitForTimelineChanged(secondTimeline)\n            .play()\n            .build();\n    new Builder()\n        .setMediaSource(mediaSource)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target.positionMs).isAtLeast(50L);\n    assertThat(target.windowIndex).isEqualTo(1);\n  }\n\n  @Test\n  public void testSendMessagesMultiWindowDuringPreparation() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 3);\n    PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50)\n            .play()\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target.windowIndex).isEqualTo(2);\n    assertThat(target.positionMs).isAtLeast(50L);\n  }\n\n  @Test\n  public void testSendMessagesMultiWindowAfterPreparation() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 3);\n    PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForTimelineChanged(timeline)\n            .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50)\n            .play()\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target.windowIndex).isEqualTo(2);\n    assertThat(target.positionMs).isAtLeast(50L);\n  }\n\n  @Test\n  public void testSendMessagesMoveWindowIndex() throws Exception {\n    Timeline timeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0),\n            new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1));\n    final Timeline secondTimeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1),\n            new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0));\n    final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT);\n    PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForTimelineChanged(timeline)\n            .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50)\n            .executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline, null))\n            .waitForTimelineChanged(secondTimeline)\n            .seek(/* windowIndex= */ 0, /* positionMs= */ 0)\n            .play()\n            .build();\n    new Builder()\n        .setMediaSource(mediaSource)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target.positionMs).isAtLeast(50L);\n    assertThat(target.windowIndex).isEqualTo(0);\n  }\n\n  @Test\n  public void testSendMessagesNonLinearPeriodOrder() throws Exception {\n    Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1);\n    MediaSource[] fakeMediaSources = {\n      new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),\n      new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),\n      new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT)\n    };\n    ConcatenatingMediaSource mediaSource =\n        new ConcatenatingMediaSource(false, new FakeShuffleOrder(3), fakeMediaSources);\n    PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget();\n    PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget();\n    PositionGrabbingMessageTarget target3 = new PositionGrabbingMessageTarget();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSendMessages\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .sendMessage(target1, /* windowIndex = */ 0, /* positionMs= */ 50)\n            .sendMessage(target2, /* windowIndex = */ 1, /* positionMs= */ 50)\n            .sendMessage(target3, /* windowIndex = */ 2, /* positionMs= */ 50)\n            .setShuffleModeEnabled(true)\n            .seek(/* windowIndex= */ 2, /* positionMs= */ 0)\n            .play()\n            .build();\n    new ExoPlayerTestRunner.Builder()\n        .setMediaSource(mediaSource)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(target1.windowIndex).isEqualTo(0);\n    assertThat(target2.windowIndex).isEqualTo(1);\n    assertThat(target3.windowIndex).isEqualTo(2);\n  }\n\n  @Test\n  public void testCancelMessageBeforeDelivery() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    final AtomicReference<PlayerMessage> message = new AtomicReference<>();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testCancelMessage\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    message.set(\n                        player.createMessage(target).setPosition(/* positionMs= */ 50).send());\n                  }\n                })\n            // Play a bit to ensure message arrived in internal player.\n            .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 30)\n            .executeRunnable(() -> message.get().cancel())\n            .play()\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(message.get().isCanceled()).isTrue();\n    assertThat(target.messageCount).isEqualTo(0);\n  }\n\n  @Test\n  public void testCancelRepeatedMessageAfterDelivery() throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();\n    final AtomicReference<PlayerMessage> message = new AtomicReference<>();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testCancelMessage\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    message.set(\n                        player\n                            .createMessage(target)\n                            .setPosition(/* positionMs= */ 50)\n                            .setDeleteAfterDelivery(/* deleteAfterDelivery= */ false)\n                            .send());\n                  }\n                })\n            // Play until the message has been delivered.\n            .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 51)\n            // Seek back, cancel the message, and play past the same position again.\n            .seek(/* positionMs= */ 0)\n            .executeRunnable(() -> message.get().cancel())\n            .play()\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(message.get().isCanceled()).isTrue();\n    assertThat(target.messageCount).isEqualTo(1);\n  }\n\n  @Test\n  public void testSetAndSwitchSurface() throws Exception {\n    final List<Integer> rendererMessages = new ArrayList<>();\n    Renderer videoRenderer =\n        new FakeRenderer(Builder.VIDEO_FORMAT) {\n          @Override\n          public void handleMessage(int what, @Nullable Object object) throws ExoPlaybackException {\n            super.handleMessage(what, object);\n            rendererMessages.add(what);\n          }\n        };\n    ActionSchedule actionSchedule =\n        addSurfaceSwitch(new ActionSchedule.Builder(\"testSetAndSwitchSurface\")).build();\n    new ExoPlayerTestRunner.Builder()\n        .setRenderers(videoRenderer)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilActionScheduleFinished(TIMEOUT_MS)\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(Collections.frequency(rendererMessages, C.MSG_SET_SURFACE)).isEqualTo(2);\n  }\n\n  @Test\n  public void testSwitchSurfaceOnEndedState() throws Exception {\n    ActionSchedule.Builder scheduleBuilder =\n        new ActionSchedule.Builder(\"testSwitchSurfaceOnEndedState\")\n            .waitForPlaybackState(Player.STATE_ENDED);\n    ActionSchedule waitForEndedAndSwitchSchedule = addSurfaceSwitch(scheduleBuilder).build();\n    new ExoPlayerTestRunner.Builder()\n        .setTimeline(Timeline.EMPTY)\n        .setActionSchedule(waitForEndedAndSwitchSchedule)\n        .build(context)\n        .start()\n        .blockUntilActionScheduleFinished(TIMEOUT_MS)\n        .blockUntilEnded(TIMEOUT_MS);\n  }\n\n  @Test\n  public void testTimelineUpdateDropsPrebufferedPeriods() throws Exception {\n    Timeline timeline1 =\n        new FakeTimeline(\n            new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1),\n            new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2));\n    final Timeline timeline2 =\n        new FakeTimeline(\n            new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1),\n            new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 3));\n    final FakeMediaSource mediaSource =\n        new FakeMediaSource(timeline1, /* manifest= */ null, Builder.VIDEO_FORMAT);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testTimelineUpdateDropsPeriods\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            // Ensure next period is pre-buffered by playing until end of first period.\n            .playUntilPosition(\n                /* windowIndex= */ 0,\n                /* positionMs= */ C.usToMs(TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US))\n            .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2, /* newManifest= */ null))\n            .waitForTimelineChanged(timeline2)\n            .play()\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setMediaSource(mediaSource)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n    testRunner.assertPlayedPeriodIndices(0, 1);\n    // Assert that the second period was re-created from the new timeline.\n    assertThat(mediaSource.getCreatedMediaPeriods()).hasSize(3);\n    assertThat(mediaSource.getCreatedMediaPeriods().get(0).periodUid)\n        .isEqualTo(timeline1.getUidOfPeriod(/* periodIndex= */ 0));\n    assertThat(mediaSource.getCreatedMediaPeriods().get(1).periodUid)\n        .isEqualTo(timeline1.getUidOfPeriod(/* periodIndex= */ 1));\n    assertThat(mediaSource.getCreatedMediaPeriods().get(2).periodUid)\n        .isEqualTo(timeline2.getUidOfPeriod(/* periodIndex= */ 1));\n    assertThat(mediaSource.getCreatedMediaPeriods().get(1).windowSequenceNumber)\n        .isGreaterThan(mediaSource.getCreatedMediaPeriods().get(0).windowSequenceNumber);\n    assertThat(mediaSource.getCreatedMediaPeriods().get(2).windowSequenceNumber)\n        .isGreaterThan(mediaSource.getCreatedMediaPeriods().get(1).windowSequenceNumber);\n  }\n\n  @Test\n  public void testRepeatedSeeksToUnpreparedPeriodInSameWindowKeepsWindowSequenceNumber()\n      throws Exception {\n    Timeline timeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(\n                /* periodCount= */ 2,\n                /* id= */ 0,\n                /* isSeekable= */ true,\n                /* isDynamic= */ false,\n                /* durationUs= */ 10 * C.MICROS_PER_SECOND));\n    FakeMediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testSeekToUnpreparedPeriod\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .seek(/* windowIndex= */ 0, /* positionMs= */ 9999)\n            .waitForSeekProcessed()\n            .seek(/* windowIndex= */ 0, /* positionMs= */ 1)\n            .waitForSeekProcessed()\n            .seek(/* windowIndex= */ 0, /* positionMs= */ 9999)\n            .waitForSeekProcessed()\n            .play()\n            .build();\n    ExoPlayerTestRunner testRunner =\n        new ExoPlayerTestRunner.Builder()\n            .setMediaSource(mediaSource)\n            .setActionSchedule(actionSchedule)\n            .build(context)\n            .start()\n            .blockUntilEnded(TIMEOUT_MS);\n\n    testRunner.assertPlayedPeriodIndices(0, 1, 0, 1);\n    assertThat(mediaSource.getCreatedMediaPeriods())\n        .containsAllOf(\n            new MediaPeriodId(\n                timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0),\n            new MediaPeriodId(\n                timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));\n    assertThat(mediaSource.getCreatedMediaPeriods())\n        .doesNotContain(\n            new MediaPeriodId(\n                timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1));\n  }\n\n  @Test\n  public void testRecursivePlayerChangesReportConsistentValuesForAllListeners() throws Exception {\n    // We add two listeners to the player. The first stops the player as soon as it's ready and both\n    // record the state change events they receive.\n    final AtomicReference<Player> playerReference = new AtomicReference<>();\n    final List<Integer> eventListener1States = new ArrayList<>();\n    final List<Integer> eventListener2States = new ArrayList<>();\n    final EventListener eventListener1 =\n        new EventListener() {\n          @Override\n          public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n            eventListener1States.add(playbackState);\n            if (playbackState == Player.STATE_READY) {\n              playerReference.get().stop(/* reset= */ true);\n            }\n          }\n        };\n    final EventListener eventListener2 =\n        new EventListener() {\n          @Override\n          public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n            eventListener2States.add(playbackState);\n          }\n        };\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testRecursivePlayerChanges\")\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    playerReference.set(player);\n                    player.addListener(eventListener1);\n                    player.addListener(eventListener2);\n                  }\n                })\n            .build();\n    new ExoPlayerTestRunner.Builder()\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n\n    assertThat(eventListener1States)\n        .containsExactly(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE)\n        .inOrder();\n    assertThat(eventListener2States)\n        .containsExactly(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE)\n        .inOrder();\n  }\n\n  @Test\n  public void testRecursivePlayerChangesAreReportedInCorrectOrder() throws Exception {\n    // The listener stops the player as soon as it's ready (which should report a timeline and state\n    // change) and sets playWhenReady to false when the timeline callback is received.\n    final AtomicReference<Player> playerReference = new AtomicReference<>();\n    final List<Boolean> eventListenerPlayWhenReady = new ArrayList<>();\n    final List<Integer> eventListenerStates = new ArrayList<>();\n    final EventListener eventListener =\n        new EventListener() {\n          @Override\n          public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {\n            if (timeline.isEmpty()) {\n              playerReference.get().setPlayWhenReady(/* playWhenReady= */ false);\n            }\n          }\n\n          @Override\n          public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n            eventListenerPlayWhenReady.add(playWhenReady);\n            eventListenerStates.add(playbackState);\n            if (playbackState == Player.STATE_READY) {\n              playerReference.get().stop(/* reset= */ true);\n            }\n          }\n        };\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testRecursivePlayerChanges\")\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    playerReference.set(player);\n                    player.addListener(eventListener);\n                  }\n                })\n            .build();\n    new ExoPlayerTestRunner.Builder()\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n\n    assertThat(eventListenerStates)\n        .containsExactly(\n            Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE, Player.STATE_IDLE)\n        .inOrder();\n    assertThat(eventListenerPlayWhenReady).containsExactly(true, true, true, false).inOrder();\n  }\n\n  @Test\n  public void testClippedLoopedPeriodsArePlayedFully() throws Exception {\n    long startPositionUs = 300_000;\n    long expectedDurationUs = 700_000;\n    MediaSource mediaSource =\n        new ClippingMediaSource(\n            new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null),\n            startPositionUs,\n            startPositionUs + expectedDurationUs);\n    Clock clock = new AutoAdvancingFakeClock();\n    AtomicReference<Player> playerReference = new AtomicReference<>();\n    AtomicLong positionAtDiscontinuityMs = new AtomicLong(C.TIME_UNSET);\n    AtomicLong clockAtStartMs = new AtomicLong(C.TIME_UNSET);\n    AtomicLong clockAtDiscontinuityMs = new AtomicLong(C.TIME_UNSET);\n    EventListener eventListener =\n        new EventListener() {\n          @Override\n          public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n            if (playbackState == Player.STATE_READY && clockAtStartMs.get() == C.TIME_UNSET) {\n              clockAtStartMs.set(clock.elapsedRealtime());\n            }\n          }\n\n          @Override\n          public void onPositionDiscontinuity(@DiscontinuityReason int reason) {\n            if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {\n              positionAtDiscontinuityMs.set(playerReference.get().getCurrentPosition());\n              clockAtDiscontinuityMs.set(clock.elapsedRealtime());\n            }\n          }\n        };\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testClippedLoopedPeriodsArePlayedFully\")\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    playerReference.set(player);\n                    player.addListener(eventListener);\n                  }\n                })\n            .pause()\n            .setRepeatMode(Player.REPEAT_MODE_ALL)\n            .waitForPlaybackState(Player.STATE_READY)\n            // Play until the media repeats once.\n            .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 1)\n            .playUntilStartOfWindow(/* windowIndex= */ 0)\n            .setRepeatMode(Player.REPEAT_MODE_OFF)\n            .play()\n            .build();\n    new ExoPlayerTestRunner.Builder()\n        .setClock(clock)\n        .setMediaSource(mediaSource)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n\n    assertThat(positionAtDiscontinuityMs.get()).isAtLeast(0L);\n    assertThat(clockAtDiscontinuityMs.get() - clockAtStartMs.get())\n        .isAtLeast(C.usToMs(expectedDurationUs));\n  }\n\n  @Test\n  public void testUpdateTrackSelectorThenSeekToUnpreparedPeriod_returnsEmptyTrackGroups()\n      throws Exception {\n    // Use unset duration to prevent pre-loading of the second window.\n    Timeline fakeTimeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(\n                /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ C.TIME_UNSET));\n    MediaSource[] fakeMediaSources = {\n      new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),\n      new FakeMediaSource(fakeTimeline, null, Builder.AUDIO_FORMAT)\n    };\n    MediaSource mediaSource = new ConcatenatingMediaSource(fakeMediaSources);\n    FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    DefaultTrackSelector trackSelector = new DefaultTrackSelector();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testUpdateTrackSelectorThenSeekToUnpreparedPeriod\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .seek(/* windowIndex= */ 1, /* positionMs= */ 0)\n            .play()\n            .build();\n    List<TrackGroupArray> trackGroupsList = new ArrayList<>();\n    List<TrackSelectionArray> trackSelectionsList = new ArrayList<>();\n    new Builder()\n        .setMediaSource(mediaSource)\n        .setTrackSelector(trackSelector)\n        .setRenderers(renderer)\n        .setActionSchedule(actionSchedule)\n        .setEventListener(\n            new EventListener() {\n              @Override\n              public void onTracksChanged(\n                  TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n                trackGroupsList.add(trackGroups);\n                trackSelectionsList.add(trackSelections);\n              }\n            })\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n    assertThat(trackGroupsList).hasSize(3);\n    // First track groups of the 1st period are reported.\n    // Then the seek to an unprepared period will result in empty track groups and selections being\n    // returned.\n    // Then the track groups of the 2nd period are reported.\n    assertThat(trackGroupsList.get(0).get(0).getFormat(0)).isEqualTo(Builder.VIDEO_FORMAT);\n    assertThat(trackGroupsList.get(1)).isEqualTo(TrackGroupArray.EMPTY);\n    assertThat(trackSelectionsList.get(1).get(0)).isNull();\n    assertThat(trackGroupsList.get(2).get(0).getFormat(0)).isEqualTo(Builder.AUDIO_FORMAT);\n  }\n\n  @Test\n  public void testSecondMediaSourceInPlaylistOnlyThrowsWhenPreviousPeriodIsFullyRead()\n      throws Exception {\n    Timeline fakeTimeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(\n                /* isSeekable= */ true,\n                /* isDynamic= */ false,\n                /* durationUs= */ 10 * C.MICROS_PER_SECOND));\n    MediaSource workingMediaSource =\n        new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT);\n    MediaSource failingMediaSource =\n        new FakeMediaSource(/* timeline= */ null, /* manifest= */ null, Builder.VIDEO_FORMAT) {\n          @Override\n          public void maybeThrowSourceInfoRefreshError() throws IOException {\n            throw new IOException();\n          }\n        };\n    ConcatenatingMediaSource concatenatingMediaSource =\n        new ConcatenatingMediaSource(workingMediaSource, failingMediaSource);\n    FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    ExoPlayerTestRunner testRunner =\n        new Builder()\n            .setMediaSource(concatenatingMediaSource)\n            .setRenderers(renderer)\n            .build(context);\n    try {\n      testRunner.start().blockUntilEnded(TIMEOUT_MS);\n      fail();\n    } catch (ExoPlaybackException e) {\n      // Expected exception.\n    }\n    assertThat(renderer.sampleBufferReadCount).isAtLeast(1);\n    assertThat(renderer.hasReadStreamToEnd()).isTrue();\n  }\n\n  @Test\n  public void\n      testDynamicallyAddedSecondMediaSourceInPlaylistOnlyThrowsWhenPreviousPeriodIsFullyRead()\n          throws Exception {\n    Timeline fakeTimeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(\n                /* isSeekable= */ true,\n                /* isDynamic= */ false,\n                /* durationUs= */ 10 * C.MICROS_PER_SECOND));\n    MediaSource workingMediaSource =\n        new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT);\n    MediaSource failingMediaSource =\n        new FakeMediaSource(/* timeline= */ null, /* manifest= */ null, Builder.VIDEO_FORMAT) {\n          @Override\n          public void maybeThrowSourceInfoRefreshError() throws IOException {\n            throw new IOException();\n          }\n        };\n    ConcatenatingMediaSource concatenatingMediaSource =\n        new ConcatenatingMediaSource(workingMediaSource);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testFailingSecondMediaSourceInPlaylistOnlyThrowsLater\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .executeRunnable(() -> concatenatingMediaSource.addMediaSource(failingMediaSource))\n            .play()\n            .build();\n    FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    ExoPlayerTestRunner testRunner =\n        new Builder()\n            .setMediaSource(concatenatingMediaSource)\n            .setActionSchedule(actionSchedule)\n            .setRenderers(renderer)\n            .build(context);\n    try {\n      testRunner.start().blockUntilEnded(TIMEOUT_MS);\n      fail();\n    } catch (ExoPlaybackException e) {\n      // Expected exception.\n    }\n    assertThat(renderer.sampleBufferReadCount).isAtLeast(1);\n    assertThat(renderer.hasReadStreamToEnd()).isTrue();\n  }\n\n  @Test\n  public void failingDynamicUpdateOnlyThrowsWhenAvailablePeriodHasBeenFullyRead() throws Exception {\n    Timeline fakeTimeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(\n                /* isSeekable= */ true,\n                /* isDynamic= */ true,\n                /* durationUs= */ 10 * C.MICROS_PER_SECOND));\n    AtomicReference<Boolean> wasReadyOnce = new AtomicReference<>(false);\n    MediaSource mediaSource =\n        new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT) {\n          @Override\n          public void maybeThrowSourceInfoRefreshError() throws IOException {\n            if (wasReadyOnce.get()) {\n              throw new IOException();\n            }\n          }\n        };\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"testFailingDynamicMediaSourceInTimelineOnlyThrowsLater\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .executeRunnable(() -> wasReadyOnce.set(true))\n            .play()\n            .build();\n    FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);\n    ExoPlayerTestRunner testRunner =\n        new Builder()\n            .setMediaSource(mediaSource)\n            .setActionSchedule(actionSchedule)\n            .setRenderers(renderer)\n            .build(context);\n    try {\n      testRunner.start().blockUntilEnded(TIMEOUT_MS);\n      fail();\n    } catch (ExoPlaybackException e) {\n      // Expected exception.\n    }\n    assertThat(renderer.sampleBufferReadCount).isAtLeast(1);\n    assertThat(renderer.hasReadStreamToEnd()).isTrue();\n  }\n\n  @Test\n  public void removingLoopingLastPeriodFromPlaylistDoesNotThrow() throws Exception {\n    Timeline timeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(\n                /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 100_000));\n    MediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null);\n    ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource(mediaSource);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"removingLoopingLastPeriodFromPlaylistDoesNotThrow\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            // Play almost to end to ensure the current period is fully buffered.\n            .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 90)\n            // Enable repeat mode to trigger the creation of new media periods.\n            .setRepeatMode(Player.REPEAT_MODE_ALL)\n            // Remove the media source.\n            .executeRunnable(concatenatingMediaSource::clear)\n            .build();\n    new Builder()\n        .setMediaSource(concatenatingMediaSource)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n  }\n\n  @Test\n  public void seekToUnpreparedWindowWithNonZeroOffsetInConcatenationStartsAtCorrectPosition()\n      throws Exception {\n    Timeline timeline = new FakeTimeline(/* windowCount= */ 1);\n    FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null, /* manifest= */ null);\n    MediaSource clippedMediaSource =\n        new ClippingMediaSource(\n            mediaSource,\n            /* startPositionUs= */ 3 * C.MICROS_PER_SECOND,\n            /* endPositionUs= */ C.TIME_END_OF_SOURCE);\n    MediaSource concatenatedMediaSource = new ConcatenatingMediaSource(clippedMediaSource);\n    AtomicLong positionWhenReady = new AtomicLong();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"seekToUnpreparedWindowWithNonZeroOffsetInConcatenation\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            .seek(/* positionMs= */ 10)\n            .waitForTimelineChanged()\n            .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null))\n            .waitForTimelineChanged()\n            .waitForPlaybackState(Player.STATE_READY)\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    positionWhenReady.set(player.getContentPosition());\n                  }\n                })\n            .play()\n            .build();\n    new Builder()\n        .setMediaSource(concatenatedMediaSource)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n\n    assertThat(positionWhenReady.get()).isEqualTo(10);\n  }\n\n  @Test\n  public void seekToUnpreparedWindowWithMultiplePeriodsInConcatenationStartsAtCorrectPeriod()\n      throws Exception {\n    long periodDurationMs = 5000;\n    Timeline timeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(\n                /* periodCount =*/ 2,\n                /* id= */ new Object(),\n                /* isSeekable= */ true,\n                /* isDynamic= */ false,\n                /* durationUs= */ 2 * periodDurationMs * 1000));\n    FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null, /* manifest= */ null);\n    MediaSource concatenatedMediaSource = new ConcatenatingMediaSource(mediaSource);\n    AtomicInteger periodIndexWhenReady = new AtomicInteger();\n    AtomicLong positionWhenReady = new AtomicLong();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"seekToUnpreparedWindowWithMultiplePeriodsInConcatenation\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_BUFFERING)\n            // Seek 10ms into the second period.\n            .seek(/* positionMs= */ periodDurationMs + 10)\n            .waitForTimelineChanged()\n            .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline, /* newManifest= */ null))\n            .waitForTimelineChanged()\n            .waitForPlaybackState(Player.STATE_READY)\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    periodIndexWhenReady.set(player.getCurrentPeriodIndex());\n                    positionWhenReady.set(player.getContentPosition());\n                  }\n                })\n            .play()\n            .build();\n    new Builder()\n        .setMediaSource(concatenatedMediaSource)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n\n    assertThat(periodIndexWhenReady.get()).isEqualTo(1);\n    assertThat(positionWhenReady.get()).isEqualTo(periodDurationMs + 10);\n  }\n\n  @Test\n  public void periodTransitionReportsCorrectBufferedPosition() throws Exception {\n    int periodCount = 3;\n    long periodDurationUs = 5 * C.MICROS_PER_SECOND;\n    long windowDurationUs = periodCount * periodDurationUs;\n    Timeline timeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(\n                periodCount,\n                /* id= */ new Object(),\n                /* isSeekable= */ true,\n                /* isDynamic= */ false,\n                windowDurationUs));\n    AtomicReference<Player> playerReference = new AtomicReference<>();\n    AtomicLong bufferedPositionAtFirstDiscontinuityMs = new AtomicLong(C.TIME_UNSET);\n    EventListener eventListener =\n        new EventListener() {\n          @Override\n          public void onPositionDiscontinuity(@DiscontinuityReason int reason) {\n            if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {\n              if (bufferedPositionAtFirstDiscontinuityMs.get() == C.TIME_UNSET) {\n                bufferedPositionAtFirstDiscontinuityMs.set(\n                    playerReference.get().getBufferedPosition());\n              }\n            }\n          }\n        };\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"periodTransitionReportsCorrectBufferedPosition\")\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    playerReference.set(player);\n                    player.addListener(eventListener);\n                  }\n                })\n            .pause()\n            // Wait until all periods are fully buffered.\n            .waitForIsLoading(/* targetIsLoading= */ true)\n            .waitForIsLoading(/* targetIsLoading= */ false)\n            .play()\n            .build();\n    new Builder()\n        .setTimeline(timeline)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n\n    assertThat(bufferedPositionAtFirstDiscontinuityMs.get()).isEqualTo(C.usToMs(windowDurationUs));\n  }\n\n  @Test\n  public void contentWithInitialSeekPositionAfterPrerollAdStartsAtSeekPosition() throws Exception {\n    AdPlaybackState adPlaybackState =\n        FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs= */ 0)\n            .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.parse(\"https://ad1\"))\n            .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, Uri.parse(\"https://ad2\"))\n            .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, Uri.parse(\"https://ad3\"));\n    Timeline fakeTimeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(\n                /* periodCount= */ 1,\n                /* id= */ 0,\n                /* isSeekable= */ true,\n                /* isDynamic= */ false,\n                /* durationUs= */ 10_000_000,\n                adPlaybackState));\n    final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline, null);\n    AtomicReference<Player> playerReference = new AtomicReference<>();\n    AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET);\n    EventListener eventListener =\n        new EventListener() {\n          @Override\n          public void onPositionDiscontinuity(@DiscontinuityReason int reason) {\n            if (reason == Player.DISCONTINUITY_REASON_AD_INSERTION) {\n              contentStartPositionMs.set(playerReference.get().getContentPosition());\n            }\n          }\n        };\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"contentWithInitialSeekAfterPrerollAd\")\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    playerReference.set(player);\n                    player.addListener(eventListener);\n                  }\n                })\n            .seek(5_000)\n            .build();\n    new ExoPlayerTestRunner.Builder()\n        .setMediaSource(fakeMediaSource)\n        .setActionSchedule(actionSchedule)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n\n    assertThat(contentStartPositionMs.get()).isAtLeast(5_000L);\n  }\n\n  @Test\n  public void simplePlaybackHasNoPlaybackSuppression() throws Exception {\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"simplePlaybackHasNoPlaybackSuppression\")\n            .play()\n            .waitForPlaybackState(Player.STATE_READY)\n            .pause()\n            .play()\n            .build();\n    AtomicBoolean seenPlaybackSuppression = new AtomicBoolean();\n    EventListener listener =\n        new EventListener() {\n          @Override\n          public void onPlaybackSuppressionReasonChanged(\n              @Player.PlaybackSuppressionReason int playbackSuppressionReason) {\n            seenPlaybackSuppression.set(true);\n          }\n        };\n    new ExoPlayerTestRunner.Builder()\n        .setActionSchedule(actionSchedule)\n        .setEventListener(listener)\n        .build(context)\n        .start()\n        .blockUntilEnded(TIMEOUT_MS);\n\n    assertThat(seenPlaybackSuppression.get()).isFalse();\n  }\n\n  // Internal methods.\n\n  private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {\n    final Surface surface1 = new Surface(new SurfaceTexture(/* texName= */ 0));\n    final Surface surface2 = new Surface(new SurfaceTexture(/* texName= */ 1));\n    return builder\n        .executeRunnable(\n            new PlayerRunnable() {\n              @Override\n              public void run(SimpleExoPlayer player) {\n                player.setVideoSurface(surface1);\n              }\n            })\n        .executeRunnable(\n            new PlayerRunnable() {\n              @Override\n              public void run(SimpleExoPlayer player) {\n                player.setVideoSurface(surface2);\n              }\n            });\n  }\n\n  // Internal classes.\n\n  private static final class PositionGrabbingMessageTarget extends PlayerTarget {\n\n    public int windowIndex;\n    public long positionMs;\n    public int messageCount;\n\n    public PositionGrabbingMessageTarget() {\n      windowIndex = C.INDEX_UNSET;\n      positionMs = C.POSITION_UNSET;\n    }\n\n    @Override\n    public void handleMessage(SimpleExoPlayer player, int messageType, @Nullable Object message) {\n      if (player != null) {\n        windowIndex = player.getCurrentWindowIndex();\n        positionMs = player.getCurrentPosition();\n      }\n      messageCount++;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.android.exoplayer2.C.WIDEVINE_UUID;\nimport static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4;\nimport static com.google.android.exoplayer2.util.MimeTypes.VIDEO_WEBM;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.id3.TextInformationFrame;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.video.ColorInfo;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link Format}. */\n@RunWith(AndroidJUnit4.class)\npublic final class FormatTest {\n\n  private static final List<byte[]> initData;\n  static {\n    byte[] initData1 = new byte[] {1, 2, 3};\n    byte[] initData2 = new byte[] {4, 5, 6};\n    List<byte[]> initDataList = new ArrayList<>();\n    initDataList.add(initData1);\n    initDataList.add(initData2);\n    initData = Collections.unmodifiableList(initDataList);\n  }\n\n  @Test\n  public void testParcelable() {\n    DrmInitData.SchemeData drmData1 = new DrmInitData.SchemeData(WIDEVINE_UUID, VIDEO_MP4,\n        TestUtil.buildTestData(128, 1 /* data seed */));\n    DrmInitData.SchemeData drmData2 = new DrmInitData.SchemeData(C.UUID_NIL, VIDEO_WEBM,\n        TestUtil.buildTestData(128, 1 /* data seed */));\n    DrmInitData drmInitData = new DrmInitData(drmData1, drmData2);\n    byte[] projectionData = new byte[] {1, 2, 3};\n    Metadata metadata = new Metadata(\n        new TextInformationFrame(\"id1\", \"description1\", \"value1\"),\n        new TextInformationFrame(\"id2\", \"description2\", \"value2\"));\n    ColorInfo colorInfo =  new ColorInfo(C.COLOR_SPACE_BT709,\n        C.COLOR_RANGE_LIMITED, C.COLOR_TRANSFER_SDR, new byte[] {1, 2, 3, 4, 5, 6, 7});\n\n    Format formatToParcel =\n        new Format(\n            \"id\",\n            \"label\",\n            C.SELECTION_FLAG_DEFAULT,\n            C.ROLE_FLAG_MAIN,\n            /* bitrate= */ 1024,\n            \"codec\",\n            metadata,\n            /* containerMimeType= */ MimeTypes.VIDEO_MP4,\n            /* sampleMimeType= */ MimeTypes.VIDEO_H264,\n            /* maxInputSize= */ 2048,\n            initData,\n            drmInitData,\n            Format.OFFSET_SAMPLE_RELATIVE,\n            /* width= */ 1920,\n            /* height= */ 1080,\n            /* frameRate= */ 24,\n            /* rotationDegrees= */ 90,\n            /* pixelWidthHeightRatio= */ 2,\n            projectionData,\n            C.STEREO_MODE_TOP_BOTTOM,\n            colorInfo,\n            /* channelCount= */ 6,\n            /* sampleRate= */ 44100,\n            C.ENCODING_PCM_24BIT,\n            /* encoderDelay= */ 1001,\n            /* encoderPadding= */ 1002,\n            \"language\",\n            /* accessibilityChannel= */ Format.NO_VALUE);\n\n    Parcel parcel = Parcel.obtain();\n    formatToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    Format formatFromParcel = Format.CREATOR.createFromParcel(parcel);\n    assertThat(formatFromParcel).isEqualTo(formatToParcel);\n\n    parcel.recycle();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.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 static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.assertNull;\nimport static org.mockito.Mockito.mock;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.SinglePeriodTimeline;\nimport com.google.android.exoplayer2.source.ads.AdPlaybackState;\nimport com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;\nimport com.google.android.exoplayer2.trackselection.TrackSelector;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link MediaPeriodQueue}. */\n@RunWith(AndroidJUnit4.class)\npublic final class MediaPeriodQueueTest {\n\n  private static final long CONTENT_DURATION_US = 30 * C.MICROS_PER_SECOND;\n  private static final long AD_DURATION_US = 10 * C.MICROS_PER_SECOND;\n  private static final long FIRST_AD_START_TIME_US = 10 * C.MICROS_PER_SECOND;\n  private static final long SECOND_AD_START_TIME_US = 20 * C.MICROS_PER_SECOND;\n\n  private static final Timeline CONTENT_TIMELINE =\n      new SinglePeriodTimeline(CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false);\n  private static final Uri AD_URI = Uri.EMPTY;\n\n  private MediaPeriodQueue mediaPeriodQueue;\n  private AdPlaybackState adPlaybackState;\n  private Timeline timeline;\n  private Object periodUid;\n\n  private PlaybackInfo playbackInfo;\n  private RendererCapabilities[] rendererCapabilities;\n  private TrackSelector trackSelector;\n  private Allocator allocator;\n  private MediaSource mediaSource;\n\n  @Before\n  public void setUp() {\n    mediaPeriodQueue = new MediaPeriodQueue();\n    mediaSource = mock(MediaSource.class);\n    rendererCapabilities = new RendererCapabilities[0];\n    trackSelector = mock(TrackSelector.class);\n    allocator = mock(Allocator.class);\n  }\n\n  @Test\n  public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() {\n    setupTimeline(/* initialPositionUs= */ 0);\n    assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(\n        /* startPositionUs= */ 0,\n        /* endPositionUs= */ C.TIME_UNSET,\n        /* durationUs= */ CONTENT_DURATION_US,\n        /* isLast= */ true,\n        /* nextAdGroupIndex= */ C.INDEX_UNSET);\n  }\n\n  @Test\n  public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() {\n    setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ 0);\n    setAdGroupLoaded(/* adGroupIndex= */ 0);\n    assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0);\n    advance();\n    assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(\n        /* startPositionUs= */ 0,\n        /* endPositionUs= */ C.TIME_UNSET,\n        /* durationUs= */ CONTENT_DURATION_US,\n        /* isLast= */ true,\n        /* nextAdGroupIndex= */ C.INDEX_UNSET);\n  }\n\n  @Test\n  public void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() {\n    setupTimeline(\n        /* initialPositionUs= */ 0,\n        /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,\n        SECOND_AD_START_TIME_US);\n    assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(\n        /* startPositionUs= */ 0,\n        /* endPositionUs= */ FIRST_AD_START_TIME_US,\n        /* durationUs= */ FIRST_AD_START_TIME_US,\n        /* isLast= */ false,\n        /* nextAdGroupIndex= */ 0);\n    // The next media period info should be null as we haven't loaded the ad yet.\n    advance();\n    assertNull(getNextMediaPeriodInfo());\n    setAdGroupLoaded(/* adGroupIndex= */ 0);\n    assertNextMediaPeriodInfoIsAd(\n        /* adGroupIndex= */ 0, /* contentPositionUs= */ FIRST_AD_START_TIME_US);\n    advance();\n    assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(\n        /* startPositionUs= */ FIRST_AD_START_TIME_US,\n        /* endPositionUs= */ SECOND_AD_START_TIME_US,\n        /* durationUs= */ SECOND_AD_START_TIME_US,\n        /* isLast= */ false,\n        /* nextAdGroupIndex= */ 1);\n    advance();\n    setAdGroupLoaded(/* adGroupIndex= */ 1);\n    assertNextMediaPeriodInfoIsAd(\n        /* adGroupIndex= */ 1, /* contentPositionUs= */ SECOND_AD_START_TIME_US);\n    advance();\n    assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(\n        /* startPositionUs= */ SECOND_AD_START_TIME_US,\n        /* endPositionUs= */ C.TIME_UNSET,\n        /* durationUs= */ CONTENT_DURATION_US,\n        /* isLast= */ true,\n        /* nextAdGroupIndex= */ C.INDEX_UNSET);\n  }\n\n  @Test\n  public void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() {\n    setupTimeline(\n        /* initialPositionUs= */ 0,\n        /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,\n        C.TIME_END_OF_SOURCE);\n    assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(\n        /* startPositionUs= */ 0,\n        /* endPositionUs= */ FIRST_AD_START_TIME_US,\n        /* durationUs= */ FIRST_AD_START_TIME_US,\n        /* isLast= */ false,\n        /* nextAdGroupIndex= */ 0);\n    advance();\n    setAdGroupLoaded(/* adGroupIndex= */ 0);\n    assertNextMediaPeriodInfoIsAd(\n        /* adGroupIndex= */ 0, /* contentPositionUs= */ FIRST_AD_START_TIME_US);\n    advance();\n    assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(\n        /* startPositionUs= */ FIRST_AD_START_TIME_US,\n        /* endPositionUs= */ C.TIME_END_OF_SOURCE,\n        /* durationUs= */ CONTENT_DURATION_US,\n        /* isLast= */ false,\n        /* nextAdGroupIndex= */ 1);\n    advance();\n    setAdGroupLoaded(/* adGroupIndex= */ 1);\n    assertNextMediaPeriodInfoIsAd(\n        /* adGroupIndex= */ 1, /* contentPositionUs= */ CONTENT_DURATION_US);\n    advance();\n    assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(\n        /* startPositionUs= */ CONTENT_DURATION_US,\n        /* endPositionUs= */ C.TIME_UNSET,\n        /* durationUs= */ CONTENT_DURATION_US,\n        /* isLast= */ true,\n        /* nextAdGroupIndex= */ C.INDEX_UNSET);\n  }\n\n  @Test\n  public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() {\n    setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ C.TIME_END_OF_SOURCE);\n    assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(\n        /* startPositionUs= */ 0,\n        /* endPositionUs= */ C.TIME_END_OF_SOURCE,\n        /* durationUs= */ CONTENT_DURATION_US,\n        /* isLast= */ false,\n        /* nextAdGroupIndex= */ 0);\n    advance();\n    setAdGroupFailedToLoad(/* adGroupIndex= */ 0);\n    assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(\n        /* startPositionUs= */ CONTENT_DURATION_US,\n        /* endPositionUs= */ C.TIME_UNSET,\n        /* durationUs= */ CONTENT_DURATION_US,\n        /* isLast= */ true,\n        /* nextAdGroupIndex= */ C.INDEX_UNSET);\n  }\n\n  @Test\n  public void\n      updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {\n    setupTimeline(\n        /* initialPositionUs= */ 0,\n        /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,\n        SECOND_AD_START_TIME_US);\n    setAdGroupLoaded(/* adGroupIndex= */ 0);\n    setAdGroupLoaded(/* adGroupIndex= */ 1);\n    enqueueNext(); // Content before first ad.\n    advancePlaying();\n    enqueueNext(); // First ad.\n    enqueueNext(); // Content between ads.\n    enqueueNext(); // Second ad.\n\n    // Change position of second ad (= change duration of content between ads).\n    setupTimeline(\n        /* initialPositionUs= */ 0,\n        /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,\n        SECOND_AD_START_TIME_US + 1);\n    setAdGroupLoaded(/* adGroupIndex= */ 0);\n    setAdGroupLoaded(/* adGroupIndex= */ 1);\n    boolean changeHandled =\n        mediaPeriodQueue.updateQueuedPeriods(\n            /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ 0);\n\n    assertThat(changeHandled).isTrue();\n    assertThat(getQueueLength()).isEqualTo(3);\n  }\n\n  @Test\n  public void\n      updateQueuedPeriods_withDurationChangeBeforeReadingPeriod_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {\n    setupTimeline(\n        /* initialPositionUs= */ 0,\n        /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,\n        SECOND_AD_START_TIME_US);\n    setAdGroupLoaded(/* adGroupIndex= */ 0);\n    setAdGroupLoaded(/* adGroupIndex= */ 1);\n    enqueueNext(); // Content before first ad.\n    advancePlaying();\n    enqueueNext(); // First ad.\n    enqueueNext(); // Content between ads.\n    enqueueNext(); // Second ad.\n    advanceReading(); // Reading first ad.\n\n    // Change position of first ad (= change duration of content before first ad).\n    setupTimeline(\n        /* initialPositionUs= */ 0,\n        /* adGroupTimesUs= */ FIRST_AD_START_TIME_US + 1,\n        SECOND_AD_START_TIME_US);\n    setAdGroupLoaded(/* adGroupIndex= */ 0);\n    setAdGroupLoaded(/* adGroupIndex= */ 1);\n    boolean changeHandled =\n        mediaPeriodQueue.updateQueuedPeriods(\n            /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ FIRST_AD_START_TIME_US);\n\n    assertThat(changeHandled).isFalse();\n    assertThat(getQueueLength()).isEqualTo(1);\n  }\n\n  @Test\n  public void\n      updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {\n    setupTimeline(\n        /* initialPositionUs= */ 0,\n        /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,\n        SECOND_AD_START_TIME_US);\n    setAdGroupLoaded(/* adGroupIndex= */ 0);\n    setAdGroupLoaded(/* adGroupIndex= */ 1);\n    enqueueNext(); // Content before first ad.\n    advancePlaying();\n    enqueueNext(); // First ad.\n    enqueueNext(); // Content between ads.\n    enqueueNext(); // Second ad.\n    advanceReading(); // Reading first ad.\n    advanceReading(); // Reading content between ads.\n\n    // Change position of second ad (= change duration of content between ads).\n    setupTimeline(\n        /* initialPositionUs= */ 0,\n        /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,\n        SECOND_AD_START_TIME_US - 1000);\n    setAdGroupLoaded(/* adGroupIndex= */ 0);\n    setAdGroupLoaded(/* adGroupIndex= */ 1);\n    long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US;\n    boolean changeHandled =\n        mediaPeriodQueue.updateQueuedPeriods(\n            /* rendererPositionUs= */ 0,\n            /* maxRendererReadPositionUs= */ readingPositionAtStartOfContentBetweenAds);\n\n    assertThat(changeHandled).isTrue();\n    assertThat(getQueueLength()).isEqualTo(3);\n  }\n\n  @Test\n  public void\n      updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {\n    setupTimeline(\n        /* initialPositionUs= */ 0,\n        /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,\n        SECOND_AD_START_TIME_US);\n    setAdGroupLoaded(/* adGroupIndex= */ 0);\n    setAdGroupLoaded(/* adGroupIndex= */ 1);\n    enqueueNext(); // Content before first ad.\n    advancePlaying();\n    enqueueNext(); // First ad.\n    enqueueNext(); // Content between ads.\n    enqueueNext(); // Second ad.\n    advanceReading(); // Reading first ad.\n    advanceReading(); // Reading content between ads.\n\n    // Change position of second ad (= change duration of content between ads).\n    setupTimeline(\n        /* initialPositionUs= */ 0,\n        /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,\n        SECOND_AD_START_TIME_US - 1000);\n    setAdGroupLoaded(/* adGroupIndex= */ 0);\n    setAdGroupLoaded(/* adGroupIndex= */ 1);\n    long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US;\n    boolean changeHandled =\n        mediaPeriodQueue.updateQueuedPeriods(\n            /* rendererPositionUs= */ 0,\n            /* maxRendererReadPositionUs= */ readingPositionAtEndOfContentBetweenAds);\n\n    assertThat(changeHandled).isFalse();\n    assertThat(getQueueLength()).isEqualTo(3);\n  }\n\n  @Test\n  public void\n      updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {\n    setupTimeline(\n        /* initialPositionUs= */ 0,\n        /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,\n        SECOND_AD_START_TIME_US);\n    setAdGroupLoaded(/* adGroupIndex= */ 0);\n    setAdGroupLoaded(/* adGroupIndex= */ 1);\n    enqueueNext(); // Content before first ad.\n    advancePlaying();\n    enqueueNext(); // First ad.\n    enqueueNext(); // Content between ads.\n    enqueueNext(); // Second ad.\n    advanceReading(); // Reading first ad.\n    advanceReading(); // Reading content between ads.\n\n    // Change position of second ad (= change duration of content between ads).\n    setupTimeline(\n        /* initialPositionUs= */ 0,\n        /* adGroupTimesUs= */ FIRST_AD_START_TIME_US,\n        SECOND_AD_START_TIME_US - 1000);\n    setAdGroupLoaded(/* adGroupIndex= */ 0);\n    setAdGroupLoaded(/* adGroupIndex= */ 1);\n    boolean changeHandled =\n        mediaPeriodQueue.updateQueuedPeriods(\n            /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ C.TIME_END_OF_SOURCE);\n\n    assertThat(changeHandled).isFalse();\n    assertThat(getQueueLength()).isEqualTo(3);\n  }\n\n  private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) {\n    adPlaybackState =\n        new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US);\n    timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);\n    periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);\n    mediaPeriodQueue.setTimeline(timeline);\n    playbackInfo =\n        new PlaybackInfo(\n            timeline,\n            /* manifest= */ null,\n            mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, initialPositionUs),\n            /* startPositionUs= */ 0,\n            /* contentPositionUs= */ 0,\n            Player.STATE_READY,\n            /* isLoading= */ false,\n            /* trackGroups= */ null,\n            /* trackSelectorResult= */ null,\n            /* loadingMediaPeriodId= */ null,\n            /* bufferedPositionUs= */ 0,\n            /* totalBufferedDurationUs= */ 0,\n            /* positionUs= */ 0);\n  }\n\n  private void advance() {\n    enqueueNext();\n    advancePlaying();\n  }\n\n  private void advancePlaying() {\n    mediaPeriodQueue.advancePlayingPeriod();\n  }\n\n  private void advanceReading() {\n    mediaPeriodQueue.advanceReadingPeriod();\n  }\n\n  private void enqueueNext() {\n    mediaPeriodQueue.enqueueNextMediaPeriod(\n        rendererCapabilities, trackSelector, allocator, mediaSource, getNextMediaPeriodInfo());\n  }\n\n  private MediaPeriodInfo getNextMediaPeriodInfo() {\n    return mediaPeriodQueue.getNextMediaPeriodInfo(/* rendererPositionUs= */ 0, playbackInfo);\n  }\n\n  private void setAdGroupLoaded(int adGroupIndex) {\n    long[][] newDurations = new long[adPlaybackState.adGroupCount][];\n    for (int i = 0; i < adPlaybackState.adGroupCount; i++) {\n      newDurations[i] =\n          i == adGroupIndex ? new long[] {AD_DURATION_US} : adPlaybackState.adGroups[i].durationsUs;\n    }\n    adPlaybackState =\n        adPlaybackState\n            .withAdCount(adGroupIndex, /* adCount= */ 1)\n            .withAdUri(adGroupIndex, /* adIndexInAdGroup= */ 0, AD_URI)\n            .withAdDurationsUs(newDurations);\n    updateTimeline();\n  }\n\n  private void setAdGroupFailedToLoad(int adGroupIndex) {\n    adPlaybackState =\n        adPlaybackState\n            .withAdCount(adGroupIndex, /* adCount= */ 1)\n            .withAdLoadError(adGroupIndex, /* adIndexInAdGroup= */ 0);\n    updateTimeline();\n  }\n\n  private void updateTimeline() {\n    timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);\n    mediaPeriodQueue.setTimeline(timeline);\n  }\n\n  private void assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(\n      long startPositionUs,\n      long endPositionUs,\n      long durationUs,\n      boolean isLast,\n      int nextAdGroupIndex) {\n    assertThat(getNextMediaPeriodInfo())\n        .isEqualTo(\n            new MediaPeriodInfo(\n                new MediaPeriodId(periodUid, /* windowSequenceNumber= */ 0, nextAdGroupIndex),\n                startPositionUs,\n                /* contentPositionUs= */ C.TIME_UNSET,\n                endPositionUs,\n                durationUs,\n                /* isLastInTimelinePeriod= */ isLast,\n                /* isFinal= */ isLast));\n  }\n\n  private void assertNextMediaPeriodInfoIsAd(int adGroupIndex, long contentPositionUs) {\n    assertThat(getNextMediaPeriodInfo())\n        .isEqualTo(\n            new MediaPeriodInfo(\n                new MediaPeriodId(\n                    periodUid,\n                    adGroupIndex,\n                    /* adIndexInAdGroup= */ 0,\n                    /* windowSequenceNumber= */ 0),\n                /* startPositionUs= */ 0,\n                contentPositionUs,\n                /* endPositionUs= */ C.TIME_UNSET,\n                /* durationUs= */ AD_DURATION_US,\n                /* isLastInTimelinePeriod= */ false,\n                /* isFinal= */ false));\n  }\n\n  private int getQueueLength() {\n    int length = 0;\n    MediaPeriodHolder periodHolder = mediaPeriodQueue.getFrontPeriod();\n    while (periodHolder != null) {\n      length++;\n      periodHolder = periodHolder.getNext();\n    }\n    return length;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.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.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.FakeTimeline;\nimport com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;\nimport com.google.android.exoplayer2.testutil.TimelineAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link Timeline}. */\n@RunWith(AndroidJUnit4.class)\npublic class TimelineTest {\n\n  @Test\n  public void testEmptyTimeline() {\n    TimelineAsserts.assertEmpty(Timeline.EMPTY);\n  }\n\n  @Test\n  public void testSinglePeriodTimeline() {\n    Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(1, 111));\n    TimelineAsserts.assertWindowTags(timeline, 111);\n    TimelineAsserts.assertPeriodCounts(timeline, 1);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);\n    TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);\n    TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);\n  }\n\n  @Test\n  public void testMultiPeriodTimeline() {\n    Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(5, 111));\n    TimelineAsserts.assertWindowTags(timeline, 111);\n    TimelineAsserts.assertPeriodCounts(timeline, 5);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);\n    TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);\n    TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.os.Handler;\nimport android.os.SystemClock;\nimport androidx.annotation.Nullable;\nimport android.view.Surface;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\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.Renderer;\nimport com.google.android.exoplayer2.RenderersFactory;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.Timeline.Window;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.source.ConcatenatingMediaSource;\nimport com.google.android.exoplayer2.source.MediaSource;\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.testutil.ActionSchedule;\nimport com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable;\nimport com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;\nimport com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder;\nimport com.google.android.exoplayer2.testutil.FakeMediaSource;\nimport com.google.android.exoplayer2.testutil.FakeRenderer;\nimport com.google.android.exoplayer2.testutil.FakeTimeline;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.VideoRendererEventListener;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/** Integration test for {@link AnalyticsCollector}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic final class AnalyticsCollectorTest {\n\n  private static final int EVENT_PLAYER_STATE_CHANGED = 0;\n  private static final int EVENT_TIMELINE_CHANGED = 1;\n  private static final int EVENT_POSITION_DISCONTINUITY = 2;\n  private static final int EVENT_SEEK_STARTED = 3;\n  private static final int EVENT_SEEK_PROCESSED = 4;\n  private static final int EVENT_PLAYBACK_PARAMETERS_CHANGED = 5;\n  private static final int EVENT_REPEAT_MODE_CHANGED = 6;\n  private static final int EVENT_SHUFFLE_MODE_CHANGED = 7;\n  private static final int EVENT_LOADING_CHANGED = 8;\n  private static final int EVENT_PLAYER_ERROR = 9;\n  private static final int EVENT_TRACKS_CHANGED = 10;\n  private static final int EVENT_LOAD_STARTED = 11;\n  private static final int EVENT_LOAD_COMPLETED = 12;\n  private static final int EVENT_LOAD_CANCELED = 13;\n  private static final int EVENT_LOAD_ERROR = 14;\n  private static final int EVENT_DOWNSTREAM_FORMAT_CHANGED = 15;\n  private static final int EVENT_UPSTREAM_DISCARDED = 16;\n  private static final int EVENT_MEDIA_PERIOD_CREATED = 17;\n  private static final int EVENT_MEDIA_PERIOD_RELEASED = 18;\n  private static final int EVENT_READING_STARTED = 19;\n  private static final int EVENT_BANDWIDTH_ESTIMATE = 20;\n  private static final int EVENT_SURFACE_SIZE_CHANGED = 21;\n  private static final int EVENT_METADATA = 23;\n  private static final int EVENT_DECODER_ENABLED = 24;\n  private static final int EVENT_DECODER_INIT = 25;\n  private static final int EVENT_DECODER_FORMAT_CHANGED = 26;\n  private static final int EVENT_DECODER_DISABLED = 27;\n  private static final int EVENT_AUDIO_SESSION_ID = 28;\n  private static final int EVENT_AUDIO_UNDERRUN = 29;\n  private static final int EVENT_DROPPED_VIDEO_FRAMES = 30;\n  private static final int EVENT_VIDEO_SIZE_CHANGED = 31;\n  private static final int EVENT_RENDERED_FIRST_FRAME = 32;\n  private static final int EVENT_DRM_KEYS_LOADED = 33;\n  private static final int EVENT_DRM_ERROR = 34;\n  private static final int EVENT_DRM_KEYS_RESTORED = 35;\n  private static final int EVENT_DRM_KEYS_REMOVED = 36;\n  private static final int EVENT_DRM_SESSION_ACQUIRED = 37;\n  private static final int EVENT_DRM_SESSION_RELEASED = 38;\n\n  private static final int TIMEOUT_MS = 10000;\n  private static final Timeline SINGLE_PERIOD_TIMELINE = new FakeTimeline(/* windowCount= */ 1);\n  private static final EventWindowAndPeriodId WINDOW_0 =\n      new EventWindowAndPeriodId(/* windowIndex= */ 0, /* mediaPeriodId= */ null);\n  private static final EventWindowAndPeriodId WINDOW_1 =\n      new EventWindowAndPeriodId(/* windowIndex= */ 1, /* mediaPeriodId= */ null);\n\n  private EventWindowAndPeriodId period0;\n  private EventWindowAndPeriodId period1;\n  private EventWindowAndPeriodId period0Seq0;\n  private EventWindowAndPeriodId period1Seq1;\n  private EventWindowAndPeriodId period0Seq1;\n  private EventWindowAndPeriodId period1Seq0;\n  private EventWindowAndPeriodId period1Seq2;\n  private EventWindowAndPeriodId window0Period1Seq0;\n  private EventWindowAndPeriodId window1Period0Seq1;\n\n  @Test\n  public void testEmptyTimeline() throws Exception {\n    FakeMediaSource mediaSource =\n        new FakeMediaSource(\n            Timeline.EMPTY, /* manifest= */ null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);\n    TestAnalyticsListener listener = runAnalyticsTest(mediaSource);\n\n    assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))\n        .containsExactly(\n            WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* ENDED */);\n    assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);\n    listener.assertNoMoreEvents();\n  }\n\n  @Test\n  public void testSinglePeriod() throws Exception {\n    FakeMediaSource mediaSource =\n        new FakeMediaSource(\n            SINGLE_PERIOD_TIMELINE,\n            /* manifest= */ null,\n            Builder.VIDEO_FORMAT,\n            Builder.AUDIO_FORMAT);\n    TestAnalyticsListener listener = runAnalyticsTest(mediaSource);\n\n    populateEventIds(SINGLE_PERIOD_TIMELINE);\n    assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))\n        .containsExactly(\n            WINDOW_0 /* setPlayWhenReady */,\n            WINDOW_0 /* BUFFERING */,\n            period0 /* READY */,\n            period0 /* ENDED */);\n    assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);\n    assertThat(listener.getEvents(EVENT_LOADING_CHANGED))\n        .containsExactly(period0 /* started */, period0 /* stopped */);\n    assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_LOAD_STARTED))\n        .containsExactly(WINDOW_0 /* manifest */, period0 /* media */);\n    assertThat(listener.getEvents(EVENT_LOAD_COMPLETED))\n        .containsExactly(WINDOW_0 /* manifest */, period0 /* media */);\n    assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED))\n        .containsExactly(period0 /* audio */, period0 /* video */);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_DECODER_ENABLED))\n        .containsExactly(period0 /* audio */, period0 /* video */);\n    assertThat(listener.getEvents(EVENT_DECODER_INIT))\n        .containsExactly(period0 /* audio */, period0 /* video */);\n    assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED))\n        .containsExactly(period0 /* audio */, period0 /* video */);\n    assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0);\n    listener.assertNoMoreEvents();\n  }\n\n  @Test\n  public void testAutomaticPeriodTransition() throws Exception {\n    MediaSource mediaSource =\n        new ConcatenatingMediaSource(\n            new FakeMediaSource(\n                SINGLE_PERIOD_TIMELINE,\n                /* manifest= */ null,\n                Builder.VIDEO_FORMAT,\n                Builder.AUDIO_FORMAT),\n            new FakeMediaSource(\n                SINGLE_PERIOD_TIMELINE,\n                /* manifest= */ null,\n                Builder.VIDEO_FORMAT,\n                Builder.AUDIO_FORMAT));\n    TestAnalyticsListener listener = runAnalyticsTest(mediaSource);\n\n    populateEventIds(listener.lastReportedTimeline);\n    assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))\n        .containsExactly(\n            WINDOW_0 /* setPlayWhenReady */,\n            WINDOW_0 /* BUFFERING */,\n            period0 /* READY */,\n            period1 /* ENDED */);\n    assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);\n    assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);\n    assertThat(listener.getEvents(EVENT_LOADING_CHANGED))\n        .containsExactly(period0, period0, period0, period0);\n    assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0, period1);\n    assertThat(listener.getEvents(EVENT_LOAD_STARTED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            period0 /* media */,\n            WINDOW_1 /* manifest */,\n            period1 /* media */);\n    assertThat(listener.getEvents(EVENT_LOAD_COMPLETED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            period0 /* media */,\n            WINDOW_1 /* manifest */,\n            period1 /* media */);\n    assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED))\n        .containsExactly(\n            period0 /* audio */, period0 /* video */, period1 /* audio */, period1 /* video */);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(period0, period1);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0, period1);\n    assertThat(listener.getEvents(EVENT_DECODER_ENABLED))\n        .containsExactly(period0 /* audio */, period0 /* video */);\n    assertThat(listener.getEvents(EVENT_DECODER_INIT))\n        .containsExactly(\n            period0 /* audio */, period0 /* video */, period1 /* audio */, period1 /* video */);\n    assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED))\n        .containsExactly(\n            period0 /* audio */, period0 /* video */, period1 /* audio */, period1 /* video */);\n    assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period1);\n    assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0);\n    listener.assertNoMoreEvents();\n  }\n\n  @Test\n  public void testPeriodTransitionWithRendererChange() throws Exception {\n    MediaSource mediaSource =\n        new ConcatenatingMediaSource(\n            new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT),\n            new FakeMediaSource(\n                SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.AUDIO_FORMAT));\n    TestAnalyticsListener listener = runAnalyticsTest(mediaSource);\n\n    populateEventIds(listener.lastReportedTimeline);\n    assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))\n        .containsExactly(\n            WINDOW_0 /* setPlayWhenReady */,\n            WINDOW_0 /* BUFFERING */,\n            period0 /* READY */,\n            period1 /* BUFFERING */,\n            period1 /* READY */,\n            period1 /* ENDED */);\n    assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);\n    assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);\n    assertThat(listener.getEvents(EVENT_LOADING_CHANGED))\n        .containsExactly(period0, period0, period0, period0);\n    assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0, period1);\n    assertThat(listener.getEvents(EVENT_LOAD_STARTED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            period0 /* media */,\n            WINDOW_1 /* manifest */,\n            period1 /* media */);\n    assertThat(listener.getEvents(EVENT_LOAD_COMPLETED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            period0 /* media */,\n            WINDOW_1 /* manifest */,\n            period1 /* media */);\n    assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED))\n        .containsExactly(period0 /* video */, period1 /* audio */);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(period0, period1);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0, period1);\n    assertThat(listener.getEvents(EVENT_DECODER_ENABLED))\n        .containsExactly(period0 /* video */, period1 /* audio */);\n    assertThat(listener.getEvents(EVENT_DECODER_INIT))\n        .containsExactly(period0 /* video */, period1 /* audio */);\n    assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED))\n        .containsExactly(period0 /* video */, period1 /* audio */);\n    assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1);\n    assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0);\n    listener.assertNoMoreEvents();\n  }\n\n  @Test\n  public void testSeekToOtherPeriod() throws Exception {\n    MediaSource mediaSource =\n        new ConcatenatingMediaSource(\n            new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT),\n            new FakeMediaSource(\n                SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.AUDIO_FORMAT));\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"AnalyticsCollectorTest\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .seek(/* windowIndex= */ 1, /* positionMs= */ 0)\n            .play()\n            .build();\n    TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);\n\n    populateEventIds(listener.lastReportedTimeline);\n    assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))\n        .containsExactly(\n            WINDOW_0 /* setPlayWhenReady=true */,\n            WINDOW_0 /* BUFFERING */,\n            WINDOW_0 /* setPlayWhenReady=false */,\n            period0 /* READY */,\n            period1 /* BUFFERING */,\n            period1 /* READY */,\n            period1 /* setPlayWhenReady=true */,\n            period1 /* ENDED */);\n    assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);\n    assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);\n    assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1);\n    List<EventWindowAndPeriodId> loadingEvents = listener.getEvents(EVENT_LOADING_CHANGED);\n    assertThat(loadingEvents).hasSize(4);\n    assertThat(loadingEvents).containsAllOf(period0, period0);\n    assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0, period1);\n    assertThat(listener.getEvents(EVENT_LOAD_STARTED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            period0 /* media */,\n            WINDOW_1 /* manifest */,\n            period1 /* media */);\n    assertThat(listener.getEvents(EVENT_LOAD_COMPLETED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            period0 /* media */,\n            WINDOW_1 /* manifest */,\n            period1 /* media */);\n    assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED))\n        .containsExactly(period0 /* video */, period1 /* audio */);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(period0, period1);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0, period1);\n    assertThat(listener.getEvents(EVENT_DECODER_ENABLED))\n        .containsExactly(period0 /* video */, period1 /* audio */);\n    assertThat(listener.getEvents(EVENT_DECODER_INIT))\n        .containsExactly(period0 /* video */, period1 /* audio */);\n    assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED))\n        .containsExactly(period0 /* video */, period1 /* audio */);\n    assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1);\n    assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0);\n    listener.assertNoMoreEvents();\n  }\n\n  @Test\n  public void testSeekBackAfterReadingAhead() throws Exception {\n    MediaSource mediaSource =\n        new ConcatenatingMediaSource(\n            new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT),\n            new FakeMediaSource(\n                SINGLE_PERIOD_TIMELINE,\n                /* manifest= */ null,\n                Builder.VIDEO_FORMAT,\n                Builder.AUDIO_FORMAT));\n    long periodDurationMs =\n        SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"AnalyticsCollectorTest\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .playUntilPosition(/* windowIndex= */ 0, periodDurationMs)\n            .seek(/* positionMs= */ 0)\n            .waitForPlaybackState(Player.STATE_READY)\n            .play()\n            .build();\n    TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);\n\n    populateEventIds(listener.lastReportedTimeline);\n    assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))\n        .containsExactly(\n            WINDOW_0 /* setPlayWhenReady=true */,\n            WINDOW_0 /* BUFFERING */,\n            WINDOW_0 /* setPlayWhenReady=false */,\n            period0 /* READY */,\n            period0 /* setPlayWhenReady=true */,\n            period0 /* setPlayWhenReady=false */,\n            period0 /* BUFFERING */,\n            period0 /* READY */,\n            period0 /* setPlayWhenReady=true */,\n            period1Seq2 /* BUFFERING */,\n            period1Seq2 /* READY */,\n            period1Seq2 /* ENDED */);\n    assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);\n    assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY))\n        .containsExactly(period0, period1Seq2);\n    assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_LOADING_CHANGED))\n        .containsExactly(period0, period0, period0, period0, period0, period0);\n    assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0, period1Seq2);\n    assertThat(listener.getEvents(EVENT_LOAD_STARTED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            period0 /* media */,\n            WINDOW_1 /* manifest */,\n            period1Seq1 /* media */,\n            period1Seq2 /* media */);\n    assertThat(listener.getEvents(EVENT_LOAD_COMPLETED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            period0 /* media */,\n            WINDOW_1 /* manifest */,\n            period1Seq1 /* media */,\n            period1Seq2 /* media */);\n    assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED))\n        .containsExactly(period0, period1Seq1, period1Seq2, period1Seq2);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED))\n        .containsExactly(period0, period1Seq1, period1Seq2);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED))\n        .containsExactly(period0, period1Seq1);\n    assertThat(listener.getEvents(EVENT_READING_STARTED))\n        .containsExactly(period0, period1Seq1, period1Seq2);\n    assertThat(listener.getEvents(EVENT_DECODER_ENABLED))\n        .containsExactly(period0, period0, period1Seq2);\n    assertThat(listener.getEvents(EVENT_DECODER_INIT))\n        .containsExactly(period0, period1Seq1, period1Seq2, period1Seq2);\n    assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED))\n        .containsExactly(period0, period1Seq1, period1Seq2, period1Seq2);\n    assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1Seq2);\n    assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES))\n        .containsExactly(period0, period0, period1Seq2);\n    assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0, period1Seq2);\n    assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME))\n        .containsExactly(period0, period1Seq2);\n    listener.assertNoMoreEvents();\n  }\n\n  @Test\n  public void testPrepareNewSource() throws Exception {\n    MediaSource mediaSource1 =\n        new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT);\n    MediaSource mediaSource2 =\n        new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"AnalyticsCollectorTest\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .prepareSource(mediaSource2)\n            .play()\n            .build();\n    TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule);\n\n    populateEventIds(SINGLE_PERIOD_TIMELINE);\n    assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))\n        .containsExactly(\n            WINDOW_0 /* setPlayWhenReady=true */,\n            WINDOW_0 /* BUFFERING */,\n            WINDOW_0 /* setPlayWhenReady=false */,\n            period0Seq0 /* READY */,\n            WINDOW_0 /* BUFFERING */,\n            WINDOW_0 /* setPlayWhenReady=true */,\n            period0Seq1 /* READY */,\n            period0Seq1 /* ENDED */);\n    assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))\n        .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* reset */, WINDOW_0 /* prepared */);\n    assertThat(listener.getEvents(EVENT_LOADING_CHANGED))\n        .containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1);\n    assertThat(listener.getEvents(EVENT_TRACKS_CHANGED))\n        .containsExactly(\n            period0Seq0 /* prepared */, WINDOW_0 /* reset */, period0Seq1 /* prepared */);\n    assertThat(listener.getEvents(EVENT_LOAD_STARTED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            period0Seq0 /* media */,\n            WINDOW_0 /* manifest */,\n            period0Seq1 /* media */);\n    assertThat(listener.getEvents(EVENT_LOAD_COMPLETED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            period0Seq0 /* media */,\n            WINDOW_0 /* manifest */,\n            period0Seq1 /* media */);\n    assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED))\n        .containsExactly(period0Seq0, period0Seq1);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED))\n        .containsExactly(period0Seq0, period0Seq1);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0Seq0);\n    assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0Seq0, period0Seq1);\n    assertThat(listener.getEvents(EVENT_DECODER_ENABLED)).containsExactly(period0Seq0, period0Seq1);\n    assertThat(listener.getEvents(EVENT_DECODER_INIT)).containsExactly(period0Seq0, period0Seq1);\n    assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED))\n        .containsExactly(period0Seq0, period0Seq1);\n    assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0Seq0);\n    assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0Seq1);\n    assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED))\n        .containsExactly(period0Seq0, period0Seq1);\n    assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME))\n        .containsExactly(period0Seq0, period0Seq1);\n    listener.assertNoMoreEvents();\n  }\n\n  @Test\n  public void testReprepareAfterError() throws Exception {\n    MediaSource mediaSource =\n        new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"AnalyticsCollectorTest\")\n            .waitForPlaybackState(Player.STATE_READY)\n            .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))\n            .waitForPlaybackState(Player.STATE_IDLE)\n            .prepareSource(mediaSource, /* resetPosition= */ false, /* resetState= */ false)\n            .waitForPlaybackState(Player.STATE_ENDED)\n            .build();\n    TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);\n\n    populateEventIds(SINGLE_PERIOD_TIMELINE);\n    assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))\n        .containsExactly(\n            WINDOW_0 /* setPlayWhenReady=true */,\n            WINDOW_0 /* BUFFERING */,\n            period0Seq0 /* READY */,\n            WINDOW_0 /* IDLE */,\n            WINDOW_0 /* BUFFERING */,\n            period0Seq0 /* READY */,\n            period0Seq0 /* ENDED */);\n    assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))\n        .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* prepared */);\n    assertThat(listener.getEvents(EVENT_LOADING_CHANGED))\n        .containsExactly(period0Seq0, period0Seq0, period0Seq0, period0Seq0);\n    assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period0Seq0);\n    assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0Seq0, period0Seq0);\n    assertThat(listener.getEvents(EVENT_LOAD_STARTED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            period0Seq0 /* media */,\n            WINDOW_0 /* manifest */,\n            period0Seq0 /* media */);\n    assertThat(listener.getEvents(EVENT_LOAD_COMPLETED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            period0Seq0 /* media */,\n            WINDOW_0 /* manifest */,\n            period0Seq0 /* media */);\n    assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED))\n        .containsExactly(period0Seq0, period0Seq0);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED))\n        .containsExactly(period0Seq0, period0Seq0);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0Seq0);\n    assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0Seq0, period0Seq0);\n    assertThat(listener.getEvents(EVENT_DECODER_ENABLED)).containsExactly(period0Seq0, period0Seq0);\n    assertThat(listener.getEvents(EVENT_DECODER_INIT)).containsExactly(period0Seq0, period0Seq0);\n    assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED))\n        .containsExactly(period0Seq0, period0Seq0);\n    assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0Seq0);\n    assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES))\n        .containsExactly(period0Seq0, period0Seq0);\n    assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED))\n        .containsExactly(period0Seq0, period0Seq0);\n    assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME))\n        .containsExactly(period0Seq0, period0Seq0);\n    listener.assertNoMoreEvents();\n  }\n\n  @Test\n  public void testDynamicTimelineChange() throws Exception {\n    MediaSource childMediaSource =\n        new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT);\n    final ConcatenatingMediaSource concatenatedMediaSource =\n        new ConcatenatingMediaSource(childMediaSource, childMediaSource);\n    long periodDurationMs =\n        SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs();\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"AnalyticsCollectorTest\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            // Ensure second period is already being read from.\n            .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ periodDurationMs)\n            .executeRunnable(\n                () ->\n                    concatenatedMediaSource.moveMediaSource(\n                        /* currentIndex= */ 0, /* newIndex= */ 1))\n            .waitForTimelineChanged()\n            .play()\n            .build();\n    TestAnalyticsListener listener = runAnalyticsTest(concatenatedMediaSource, actionSchedule);\n\n    populateEventIds(listener.lastReportedTimeline);\n    assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))\n        .containsExactly(\n            WINDOW_0 /* setPlayWhenReady=true */,\n            WINDOW_0 /* BUFFERING */,\n            WINDOW_0 /* setPlayWhenReady=false */,\n            window0Period1Seq0 /* READY */,\n            window0Period1Seq0 /* setPlayWhenReady=true */,\n            window0Period1Seq0 /* setPlayWhenReady=false */,\n            period1Seq0 /* setPlayWhenReady=true */,\n            period1Seq0 /* BUFFERING */,\n            period1Seq0 /* ENDED */);\n    assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0, period1Seq0);\n    assertThat(listener.getEvents(EVENT_LOADING_CHANGED))\n        .containsExactly(\n            window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0);\n    assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(window0Period1Seq0);\n    assertThat(listener.getEvents(EVENT_LOAD_STARTED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            window0Period1Seq0 /* media */,\n            window1Period0Seq1 /* media */);\n    assertThat(listener.getEvents(EVENT_LOAD_COMPLETED))\n        .containsExactly(\n            WINDOW_0 /* manifest */,\n            window0Period1Seq0 /* media */,\n            window1Period0Seq1 /* media */);\n    assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED))\n        .containsExactly(window0Period1Seq0, window1Period0Seq1);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED))\n        .containsExactly(window0Period1Seq0, window1Period0Seq1);\n    assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(window1Period0Seq1);\n    assertThat(listener.getEvents(EVENT_READING_STARTED))\n        .containsExactly(window0Period1Seq0, window1Period0Seq1);\n    assertThat(listener.getEvents(EVENT_DECODER_ENABLED))\n        .containsExactly(window0Period1Seq0, window0Period1Seq0);\n    assertThat(listener.getEvents(EVENT_DECODER_INIT))\n        .containsExactly(window0Period1Seq0, window1Period0Seq1);\n    assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED))\n        .containsExactly(window0Period1Seq0, window1Period0Seq1);\n    assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(window0Period1Seq0);\n    assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(window0Period1Seq0);\n    assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(window0Period1Seq0);\n    assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(window0Period1Seq0);\n    listener.assertNoMoreEvents();\n  }\n\n  @Test\n  public void testNotifyExternalEvents() throws Exception {\n    MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null);\n    ActionSchedule actionSchedule =\n        new ActionSchedule.Builder(\"AnalyticsCollectorTest\")\n            .pause()\n            .waitForPlaybackState(Player.STATE_READY)\n            .executeRunnable(\n                new PlayerRunnable() {\n                  @Override\n                  public void run(SimpleExoPlayer player) {\n                    player.getAnalyticsCollector().notifySeekStarted();\n                  }\n                })\n            .seek(/* positionMs= */ 0)\n            .play()\n            .build();\n    TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);\n\n    populateEventIds(SINGLE_PERIOD_TIMELINE);\n    assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);\n    assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0);\n  }\n\n  private void populateEventIds(Timeline timeline) {\n    period0 =\n        new EventWindowAndPeriodId(\n            /* windowIndex= */ 0,\n            new MediaPeriodId(\n                timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));\n    period0Seq0 = period0;\n    period0Seq1 =\n        new EventWindowAndPeriodId(\n            /* windowIndex= */ 0,\n            new MediaPeriodId(\n                timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1));\n    window1Period0Seq1 =\n        new EventWindowAndPeriodId(\n            /* windowIndex= */ 1,\n            new MediaPeriodId(\n                timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1));\n    if (timeline.getPeriodCount() > 1) {\n      period1 =\n          new EventWindowAndPeriodId(\n              /* windowIndex= */ 1,\n              new MediaPeriodId(\n                  timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1));\n      period1Seq1 = period1;\n      period1Seq0 =\n          new EventWindowAndPeriodId(\n              /* windowIndex= */ 1,\n              new MediaPeriodId(\n                  timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));\n      period1Seq2 =\n          new EventWindowAndPeriodId(\n              /* windowIndex= */ 1,\n              new MediaPeriodId(\n                  timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 2));\n      window0Period1Seq0 =\n          new EventWindowAndPeriodId(\n              /* windowIndex= */ 0,\n              new MediaPeriodId(\n                  timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));\n    }\n  }\n\n  private static TestAnalyticsListener runAnalyticsTest(MediaSource mediaSource) throws Exception {\n    return runAnalyticsTest(mediaSource, /* actionSchedule= */ null);\n  }\n\n  private static TestAnalyticsListener runAnalyticsTest(\n      MediaSource mediaSource, @Nullable ActionSchedule actionSchedule) throws Exception {\n    RenderersFactory renderersFactory =\n        (eventHandler,\n            videoRendererEventListener,\n            audioRendererEventListener,\n            textRendererOutput,\n            metadataRendererOutput,\n            drmSessionManager) ->\n            new Renderer[] {\n              new FakeVideoRenderer(eventHandler, videoRendererEventListener),\n              new FakeAudioRenderer(eventHandler, audioRendererEventListener)\n            };\n    TestAnalyticsListener listener = new TestAnalyticsListener();\n    try {\n      new ExoPlayerTestRunner.Builder()\n          .setMediaSource(mediaSource)\n          .setRenderersFactory(renderersFactory)\n          .setAnalyticsListener(listener)\n          .setActionSchedule(actionSchedule)\n          .build(ApplicationProvider.getApplicationContext())\n          .start()\n          .blockUntilActionScheduleFinished(TIMEOUT_MS)\n          .blockUntilEnded(TIMEOUT_MS);\n    } catch (ExoPlaybackException e) {\n      // Ignore ExoPlaybackException as these may be expected.\n    }\n    return listener;\n  }\n\n  private static final class FakeVideoRenderer extends FakeRenderer {\n\n    private final VideoRendererEventListener.EventDispatcher eventDispatcher;\n    private final DecoderCounters decoderCounters;\n    private Format format;\n    private boolean renderedFirstFrame;\n\n    public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) {\n      super(Builder.VIDEO_FORMAT);\n      eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener);\n      decoderCounters = new DecoderCounters();\n    }\n\n    @Override\n    protected void onEnabled(boolean joining) throws ExoPlaybackException {\n      super.onEnabled(joining);\n      eventDispatcher.enabled(decoderCounters);\n      renderedFirstFrame = false;\n    }\n\n    @Override\n    protected void onStopped() throws ExoPlaybackException {\n      super.onStopped();\n      eventDispatcher.droppedFrames(/* droppedFrameCount= */ 0, /* elapsedMs= */ 0);\n    }\n\n    @Override\n    protected void onDisabled() {\n      super.onDisabled();\n      eventDispatcher.disabled(decoderCounters);\n    }\n\n    @Override\n    protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {\n      super.onPositionReset(positionUs, joining);\n      renderedFirstFrame = false;\n    }\n\n    @Override\n    protected void onFormatChanged(Format format) {\n      eventDispatcher.inputFormatChanged(format);\n      eventDispatcher.decoderInitialized(\n          /* decoderName= */ \"fake.video.decoder\",\n          /* initializedTimestampMs= */ SystemClock.elapsedRealtime(),\n          /* initializationDurationMs= */ 0);\n      this.format = format;\n    }\n\n    @Override\n    protected void onBufferRead() {\n      if (!renderedFirstFrame) {\n        eventDispatcher.videoSizeChanged(\n            format.width, format.height, format.rotationDegrees, format.pixelWidthHeightRatio);\n        eventDispatcher.renderedFirstFrame(/* surface= */ null);\n        renderedFirstFrame = true;\n      }\n    }\n  }\n\n  private static final class FakeAudioRenderer extends FakeRenderer {\n\n    private final AudioRendererEventListener.EventDispatcher eventDispatcher;\n    private final DecoderCounters decoderCounters;\n    private boolean notifiedAudioSessionId;\n\n    public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) {\n      super(Builder.AUDIO_FORMAT);\n      eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener);\n      decoderCounters = new DecoderCounters();\n    }\n\n    @Override\n    protected void onEnabled(boolean joining) throws ExoPlaybackException {\n      super.onEnabled(joining);\n      eventDispatcher.enabled(decoderCounters);\n      notifiedAudioSessionId = false;\n    }\n\n    @Override\n    protected void onDisabled() {\n      super.onDisabled();\n      eventDispatcher.disabled(decoderCounters);\n    }\n\n    @Override\n    protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {\n      super.onPositionReset(positionUs, joining);\n    }\n\n    @Override\n    protected void onFormatChanged(Format format) {\n      eventDispatcher.inputFormatChanged(format);\n      eventDispatcher.decoderInitialized(\n          /* decoderName= */ \"fake.audio.decoder\",\n          /* initializedTimestampMs= */ SystemClock.elapsedRealtime(),\n          /* initializationDurationMs= */ 0);\n    }\n\n    @Override\n    protected void onBufferRead() {\n      if (!notifiedAudioSessionId) {\n        eventDispatcher.audioSessionId(/* audioSessionId= */ 1);\n        notifiedAudioSessionId = true;\n      }\n    }\n  }\n\n  private static final class EventWindowAndPeriodId {\n\n    private final int windowIndex;\n    private final @Nullable MediaPeriodId mediaPeriodId;\n\n    public EventWindowAndPeriodId(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {\n      this.windowIndex = windowIndex;\n      this.mediaPeriodId = mediaPeriodId;\n    }\n\n    @Override\n    public boolean equals(@Nullable Object other) {\n      if (!(other instanceof EventWindowAndPeriodId)) {\n        return false;\n      }\n      EventWindowAndPeriodId event = (EventWindowAndPeriodId) other;\n      return windowIndex == event.windowIndex && Util.areEqual(mediaPeriodId, event.mediaPeriodId);\n    }\n\n    @Override\n    public String toString() {\n      return mediaPeriodId != null\n          ? \"Event{\"\n              + \"window=\"\n              + windowIndex\n              + \", period=\"\n              + mediaPeriodId.periodUid\n              + \", sequence=\"\n              + mediaPeriodId.windowSequenceNumber\n              + '}'\n          : \"Event{\" + \"window=\" + windowIndex + \", period = null}\";\n    }\n\n    @Override\n    public int hashCode() {\n      return 31 * windowIndex + (mediaPeriodId == null ? 0 : mediaPeriodId.hashCode());\n    }\n  }\n\n  private static final class TestAnalyticsListener implements AnalyticsListener {\n\n    public Timeline lastReportedTimeline;\n\n    private final ArrayList<ReportedEvent> reportedEvents;\n\n    public TestAnalyticsListener() {\n      reportedEvents = new ArrayList<>();\n      lastReportedTimeline = Timeline.EMPTY;\n    }\n\n    public List<EventWindowAndPeriodId> getEvents(int eventType) {\n      ArrayList<EventWindowAndPeriodId> eventTimes = new ArrayList<>();\n      Iterator<ReportedEvent> eventIterator = reportedEvents.iterator();\n      while (eventIterator.hasNext()) {\n        ReportedEvent event = eventIterator.next();\n        if (event.eventType == eventType) {\n          eventTimes.add(event.eventWindowAndPeriodId);\n          eventIterator.remove();\n        }\n      }\n      return eventTimes;\n    }\n\n    public void assertNoMoreEvents() {\n      assertThat(reportedEvents).isEmpty();\n    }\n\n    @Override\n    public void onPlayerStateChanged(\n        EventTime eventTime, boolean playWhenReady, int playbackState) {\n      reportedEvents.add(new ReportedEvent(EVENT_PLAYER_STATE_CHANGED, eventTime));\n    }\n\n    @Override\n    public void onTimelineChanged(EventTime eventTime, int reason) {\n      lastReportedTimeline = eventTime.timeline;\n      reportedEvents.add(new ReportedEvent(EVENT_TIMELINE_CHANGED, eventTime));\n    }\n\n    @Override\n    public void onPositionDiscontinuity(EventTime eventTime, int reason) {\n      reportedEvents.add(new ReportedEvent(EVENT_POSITION_DISCONTINUITY, eventTime));\n    }\n\n    @Override\n    public void onSeekStarted(EventTime eventTime) {\n      reportedEvents.add(new ReportedEvent(EVENT_SEEK_STARTED, eventTime));\n    }\n\n    @Override\n    public void onSeekProcessed(EventTime eventTime) {\n      reportedEvents.add(new ReportedEvent(EVENT_SEEK_PROCESSED, eventTime));\n    }\n\n    @Override\n    public void onPlaybackParametersChanged(\n        EventTime eventTime, PlaybackParameters playbackParameters) {\n      reportedEvents.add(new ReportedEvent(EVENT_PLAYBACK_PARAMETERS_CHANGED, eventTime));\n    }\n\n    @Override\n    public void onRepeatModeChanged(EventTime eventTime, int repeatMode) {\n      reportedEvents.add(new ReportedEvent(EVENT_REPEAT_MODE_CHANGED, eventTime));\n    }\n\n    @Override\n    public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {\n      reportedEvents.add(new ReportedEvent(EVENT_SHUFFLE_MODE_CHANGED, eventTime));\n    }\n\n    @Override\n    public void onLoadingChanged(EventTime eventTime, boolean isLoading) {\n      reportedEvents.add(new ReportedEvent(EVENT_LOADING_CHANGED, eventTime));\n    }\n\n    @Override\n    public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {\n      reportedEvents.add(new ReportedEvent(EVENT_PLAYER_ERROR, eventTime));\n    }\n\n    @Override\n    public void onTracksChanged(\n        EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n      reportedEvents.add(new ReportedEvent(EVENT_TRACKS_CHANGED, eventTime));\n    }\n\n    @Override\n    public void onLoadStarted(\n        EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {\n      reportedEvents.add(new ReportedEvent(EVENT_LOAD_STARTED, eventTime));\n    }\n\n    @Override\n    public void onLoadCompleted(\n        EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {\n      reportedEvents.add(new ReportedEvent(EVENT_LOAD_COMPLETED, eventTime));\n    }\n\n    @Override\n    public void onLoadCanceled(\n        EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {\n      reportedEvents.add(new ReportedEvent(EVENT_LOAD_CANCELED, eventTime));\n    }\n\n    @Override\n    public void onLoadError(\n        EventTime eventTime,\n        LoadEventInfo loadEventInfo,\n        MediaLoadData mediaLoadData,\n        IOException error,\n        boolean wasCanceled) {\n      reportedEvents.add(new ReportedEvent(EVENT_LOAD_ERROR, eventTime));\n    }\n\n    @Override\n    public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {\n      reportedEvents.add(new ReportedEvent(EVENT_DOWNSTREAM_FORMAT_CHANGED, eventTime));\n    }\n\n    @Override\n    public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {\n      reportedEvents.add(new ReportedEvent(EVENT_UPSTREAM_DISCARDED, eventTime));\n    }\n\n    @Override\n    public void onMediaPeriodCreated(EventTime eventTime) {\n      reportedEvents.add(new ReportedEvent(EVENT_MEDIA_PERIOD_CREATED, eventTime));\n    }\n\n    @Override\n    public void onMediaPeriodReleased(EventTime eventTime) {\n      reportedEvents.add(new ReportedEvent(EVENT_MEDIA_PERIOD_RELEASED, eventTime));\n    }\n\n    @Override\n    public void onReadingStarted(EventTime eventTime) {\n      reportedEvents.add(new ReportedEvent(EVENT_READING_STARTED, eventTime));\n    }\n\n    @Override\n    public void onBandwidthEstimate(\n        EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {\n      reportedEvents.add(new ReportedEvent(EVENT_BANDWIDTH_ESTIMATE, eventTime));\n    }\n\n    @Override\n    public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {\n      reportedEvents.add(new ReportedEvent(EVENT_SURFACE_SIZE_CHANGED, eventTime));\n    }\n\n    @Override\n    public void onMetadata(EventTime eventTime, Metadata metadata) {\n      reportedEvents.add(new ReportedEvent(EVENT_METADATA, eventTime));\n    }\n\n    @Override\n    public void onDecoderEnabled(\n        EventTime eventTime, int trackType, DecoderCounters decoderCounters) {\n      reportedEvents.add(new ReportedEvent(EVENT_DECODER_ENABLED, eventTime));\n    }\n\n    @Override\n    public void onDecoderInitialized(\n        EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {\n      reportedEvents.add(new ReportedEvent(EVENT_DECODER_INIT, eventTime));\n    }\n\n    @Override\n    public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {\n      reportedEvents.add(new ReportedEvent(EVENT_DECODER_FORMAT_CHANGED, eventTime));\n    }\n\n    @Override\n    public void onDecoderDisabled(\n        EventTime eventTime, int trackType, DecoderCounters decoderCounters) {\n      reportedEvents.add(new ReportedEvent(EVENT_DECODER_DISABLED, eventTime));\n    }\n\n    @Override\n    public void onAudioSessionId(EventTime eventTime, int audioSessionId) {\n      reportedEvents.add(new ReportedEvent(EVENT_AUDIO_SESSION_ID, eventTime));\n    }\n\n    @Override\n    public void onAudioUnderrun(\n        EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {\n      reportedEvents.add(new ReportedEvent(EVENT_AUDIO_UNDERRUN, eventTime));\n    }\n\n    @Override\n    public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {\n      reportedEvents.add(new ReportedEvent(EVENT_DROPPED_VIDEO_FRAMES, eventTime));\n    }\n\n    @Override\n    public void onVideoSizeChanged(\n        EventTime eventTime,\n        int width,\n        int height,\n        int unappliedRotationDegrees,\n        float pixelWidthHeightRatio) {\n      reportedEvents.add(new ReportedEvent(EVENT_VIDEO_SIZE_CHANGED, eventTime));\n    }\n\n    @Override\n    public void onRenderedFirstFrame(EventTime eventTime, Surface surface) {\n      reportedEvents.add(new ReportedEvent(EVENT_RENDERED_FIRST_FRAME, eventTime));\n    }\n\n    @Override\n    public void onDrmSessionAcquired(EventTime eventTime) {\n      reportedEvents.add(new ReportedEvent(EVENT_DRM_SESSION_ACQUIRED, eventTime));\n    }\n\n    @Override\n    public void onDrmKeysLoaded(EventTime eventTime) {\n      reportedEvents.add(new ReportedEvent(EVENT_DRM_KEYS_LOADED, eventTime));\n    }\n\n    @Override\n    public void onDrmSessionManagerError(EventTime eventTime, Exception error) {\n      reportedEvents.add(new ReportedEvent(EVENT_DRM_ERROR, eventTime));\n    }\n\n    @Override\n    public void onDrmKeysRestored(EventTime eventTime) {\n      reportedEvents.add(new ReportedEvent(EVENT_DRM_KEYS_RESTORED, eventTime));\n    }\n\n    @Override\n    public void onDrmKeysRemoved(EventTime eventTime) {\n      reportedEvents.add(new ReportedEvent(EVENT_DRM_KEYS_REMOVED, eventTime));\n    }\n\n    @Override\n    public void onDrmSessionReleased(EventTime eventTime) {\n      reportedEvents.add(new ReportedEvent(EVENT_DRM_SESSION_RELEASED, eventTime));\n    }\n\n    private static final class ReportedEvent {\n\n      public final int eventType;\n      public final EventWindowAndPeriodId eventWindowAndPeriodId;\n\n      public ReportedEvent(int eventType, EventTime eventTime) {\n        this.eventType = eventType;\n        this.eventWindowAndPeriodId =\n            new EventWindowAndPeriodId(eventTime.windowIndex, eventTime.mediaPeriodId);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/audio/Ac3UtilTest.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.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.util.Util;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link Ac3Util}. */\n@RunWith(AndroidJUnit4.class)\npublic final class Ac3UtilTest {\n\n  private static final int TRUEHD_SYNCFRAME_SAMPLE_COUNT = 40;\n  private static final byte[] TRUEHD_SYNCFRAME_HEADER =\n      Util.getBytesFromHexString(\"C07504D8F8726FBA0097C00FB7520000\");\n  private static final byte[] TRUEHD_NON_SYNCFRAME_HEADER =\n      Util.getBytesFromHexString(\"A025048860224E6F6DEDB6D5B6DBAFE6\");\n\n  @Test\n  public void testParseTrueHdSyncframeAudioSampleCount_nonSyncframe() {\n    assertThat(Ac3Util.parseTrueHdSyncframeAudioSampleCount(TRUEHD_NON_SYNCFRAME_HEADER))\n        .isEqualTo(0);\n  }\n\n  @Test\n  public void testParseTrueHdSyncframeAudioSampleCount_syncframe() {\n    assertThat(Ac3Util.parseTrueHdSyncframeAudioSampleCount(TRUEHD_SYNCFRAME_HEADER))\n        .isEqualTo(TRUEHD_SYNCFRAME_SAMPLE_COUNT);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/audio/AudioFocusManagerTest.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.audio.AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY;\nimport static com.google.android.exoplayer2.audio.AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY;\nimport static com.google.android.exoplayer2.audio.AudioFocusManager.PLAYER_COMMAND_WAIT_FOR_CALLBACK;\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.content.Context;\nimport android.media.AudioFocusRequest;\nimport android.media.AudioManager;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.util.Util;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.Shadows;\nimport org.robolectric.annotation.Config;\nimport org.robolectric.shadows.ShadowAudioManager;\n\n/** Unit tests for {@link AudioFocusManager}. */\n@RunWith(AndroidJUnit4.class)\npublic class AudioFocusManagerTest {\n  private static final int NO_COMMAND_RECEIVED = ~PLAYER_COMMAND_WAIT_FOR_CALLBACK;\n\n  private AudioFocusManager audioFocusManager;\n  private TestPlayerControl testPlayerControl;\n\n  private AudioManager audioManager;\n\n  @Before\n  public void setUp() {\n    audioManager =\n        (AudioManager)\n            ApplicationProvider.getApplicationContext().getSystemService(Context.AUDIO_SERVICE);\n\n    testPlayerControl = new TestPlayerControl();\n    audioFocusManager =\n        new AudioFocusManager(ApplicationProvider.getApplicationContext(), testPlayerControl);\n  }\n\n  @Test\n  public void setAudioAttributes_withNullUsage_doesNotManageAudioFocus() {\n    // Ensure that NULL audio attributes -> don't manage audio focus\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                /* audioAttributes= */ null, /* playWhenReady= */ false, Player.STATE_IDLE))\n        .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                /* audioAttributes= */ null, /* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n    ShadowAudioManager.AudioFocusRequest request =\n        Shadows.shadowOf(audioManager).getLastAudioFocusRequest();\n    assertThat(request).isNull();\n  }\n\n  @Test\n  @Config(maxSdk = 25)\n  public void setAudioAttributes_withNullUsage_releasesAudioFocus() {\n    // Create attributes and request audio focus.\n    AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n    ShadowAudioManager.AudioFocusRequest request =\n        Shadows.shadowOf(audioManager).getLastAudioFocusRequest();\n    assertThat(request.durationHint).isEqualTo(AudioManager.AUDIOFOCUS_GAIN);\n\n    // Ensure that setting null audio attributes with audio focus releases audio focus.\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                /* audioAttributes= */ null, /* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n    AudioManager.OnAudioFocusChangeListener lastRequest =\n        Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener();\n    assertThat(lastRequest).isNotNull();\n  }\n\n  @Test\n  @Config(minSdk = 26)\n  public void setAudioAttributes_withNullUsage_releasesAudioFocus_v26() {\n    // Create attributes and request audio focus.\n    AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n    ShadowAudioManager.AudioFocusRequest request =\n        Shadows.shadowOf(audioManager).getLastAudioFocusRequest();\n    assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN);\n\n    // Ensure that setting null audio attributes with audio focus releases audio focus.\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                /* audioAttributes= */ null, /* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n    AudioFocusRequest lastRequest =\n        Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest();\n    assertThat(lastRequest).isNotNull();\n  }\n\n  @Test\n  public void setAudioAttributes_withUsageAlarm_throwsIllegalArgumentException() {\n    // Ensure that audio attributes that map to AUDIOFOCUS_GAIN_TRANSIENT* throw\n    AudioAttributes alarm = new AudioAttributes.Builder().setUsage(C.USAGE_ALARM).build();\n    try {\n      audioFocusManager.setAudioAttributes(alarm, /* playWhenReady= */ false, Player.STATE_IDLE);\n      fail();\n    } catch (IllegalArgumentException e) {\n      // Expected\n    }\n  }\n\n  @Test\n  public void setAudioAttributes_withUsageMedia_usesAudioFocusGain() {\n    // Ensure setting media type audio attributes requests AUDIOFOCUS_GAIN.\n    AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n    ShadowAudioManager.AudioFocusRequest request =\n        Shadows.shadowOf(audioManager).getLastAudioFocusRequest();\n    assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN);\n  }\n\n  @Test\n  public void setAudioAttributes_inStateEnded_requestsAudioFocus() {\n    // Ensure setting audio attributes when player is in STATE_ENDED requests audio focus.\n    AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ true, Player.STATE_ENDED))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n    ShadowAudioManager.AudioFocusRequest request =\n        Shadows.shadowOf(audioManager).getLastAudioFocusRequest();\n    assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN);\n  }\n\n  @Test\n  public void handlePrepare_afterSetAudioAttributes_setsPlayerCommandPlayWhenReady() {\n    // Ensure that when playWhenReady is true while the player is IDLE, audio focus is only\n    // requested after calling handlePrepare.\n    AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();\n\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ true, Player.STATE_IDLE))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n    assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull();\n    assertThat(audioFocusManager.handlePrepare(/* playWhenReady= */ true))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n  }\n\n  @Test\n  public void handleSetPlayWhenReady_afterSetAudioAttributes_setsPlayerCommandPlayWhenReady() {\n    // Ensure that audio focus is not requested until playWhenReady is true.\n    AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();\n\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n\n    assertThat(audioFocusManager.handlePrepare(/* playWhenReady= */ false))\n        .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);\n    assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull();\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ false, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);\n    assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull();\n    assertThat(\n            audioFocusManager.handleSetPlayWhenReady(/* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n  }\n\n  @Test\n  public void onAudioFocusChange_withDuckEnabled_volumeReducedAndRestored() {\n    // Ensure that the volume multiplier is adjusted when audio focus is lost to\n    // AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK, and returns to the default value after focus is\n    // regained.\n    AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();\n\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n\n    audioFocusManager\n        .getFocusListener()\n        .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);\n    assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f);\n    assertThat(testPlayerControl.lastPlayerCommand).isEqualTo(NO_COMMAND_RECEIVED);\n    audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN);\n    assertThat(testPlayerControl.lastVolumeMultiplier).isEqualTo(1.0f);\n  }\n\n  @Test\n  public void onAudioFocusChange_withPausedWhenDucked_sendsCommandWaitForCallback() {\n    // Ensure that the player is commanded to pause when audio focus is lost with\n    // AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK and the content type is CONTENT_TYPE_SPEECH.\n    AudioAttributes media =\n        new AudioAttributes.Builder()\n            .setUsage(C.USAGE_MEDIA)\n            .setContentType(C.CONTENT_TYPE_SPEECH)\n            .build();\n\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n\n    audioFocusManager\n        .getFocusListener()\n        .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);\n    assertThat(testPlayerControl.lastPlayerCommand).isEqualTo(PLAYER_COMMAND_WAIT_FOR_CALLBACK);\n    assertThat(testPlayerControl.lastVolumeMultiplier).isEqualTo(1.0f);\n    audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN);\n    assertThat(testPlayerControl.lastPlayerCommand).isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n  }\n\n  @Test\n  public void onAudioFocusChange_withTransientLost_sendsCommandWaitForCallback() {\n    // Ensure that the player is commanded to pause when audio focus is lost with\n    // AUDIOFOCUS_LOSS_TRANSIENT.\n    AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();\n\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n\n    audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);\n    assertThat(testPlayerControl.lastVolumeMultiplier).isEqualTo(1.0f);\n    assertThat(testPlayerControl.lastPlayerCommand).isEqualTo(PLAYER_COMMAND_WAIT_FOR_CALLBACK);\n  }\n\n  @Test\n  @Config(maxSdk = 25)\n  public void onAudioFocusChange_withAudioFocusLost_sendsDoNotPlayAndAbandondsFocus() {\n    // Ensure that AUDIOFOCUS_LOSS causes AudioFocusManager to pause playback and abandon audio\n    // focus.\n    AudioAttributes media =\n        new AudioAttributes.Builder()\n            .setUsage(C.USAGE_MEDIA)\n            .setContentType(C.CONTENT_TYPE_SPEECH)\n            .build();\n\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull();\n\n    ShadowAudioManager.AudioFocusRequest request =\n        Shadows.shadowOf(audioManager).getLastAudioFocusRequest();\n    request.listener.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS);\n    assertThat(testPlayerControl.lastPlayerCommand).isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener())\n        .isEqualTo(request.listener);\n  }\n\n  @Test\n  @Config(minSdk = 26)\n  public void onAudioFocusChange_withAudioFocusLost_sendsDoNotPlayAndAbandondsFocus_v26() {\n    // Ensure that AUDIOFOCUS_LOSS causes AudioFocusManager to pause playback and abandon audio\n    // focus.\n    AudioAttributes media =\n        new AudioAttributes.Builder()\n            .setUsage(C.USAGE_MEDIA)\n            .setContentType(C.CONTENT_TYPE_SPEECH)\n            .build();\n\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull();\n\n    audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS);\n    assertThat(testPlayerControl.lastPlayerCommand).isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest())\n        .isEqualTo(Shadows.shadowOf(audioManager).getLastAudioFocusRequest().audioFocusRequest);\n  }\n\n  @Test\n  @Config(maxSdk = 25)\n  public void handleStop_withAudioFocus_abandonsAudioFocus() {\n    // Ensure that handleStop causes AudioFocusManager to abandon audio focus.\n    AudioAttributes media =\n        new AudioAttributes.Builder()\n            .setUsage(C.USAGE_MEDIA)\n            .setContentType(C.CONTENT_TYPE_SPEECH)\n            .build();\n\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull();\n\n    ShadowAudioManager.AudioFocusRequest request =\n        Shadows.shadowOf(audioManager).getLastAudioFocusRequest();\n    audioFocusManager.handleStop();\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener())\n        .isEqualTo(request.listener);\n  }\n\n  @Test\n  @Config(minSdk = 26)\n  public void handleStop_withAudioFocus_abandonsAudioFocus_v26() {\n    // Ensure that handleStop causes AudioFocusManager to abandon audio focus.\n    AudioAttributes media =\n        new AudioAttributes.Builder()\n            .setUsage(C.USAGE_MEDIA)\n            .setContentType(C.CONTENT_TYPE_SPEECH)\n            .build();\n\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ true, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull();\n\n    ShadowAudioManager.AudioFocusRequest request =\n        Shadows.shadowOf(audioManager).getLastAudioFocusRequest();\n    audioFocusManager.handleStop();\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest())\n        .isEqualTo(request.audioFocusRequest);\n  }\n\n  @Test\n  @Config(maxSdk = 25)\n  public void handleStop_withoutAudioFocus_stillAbandonsFocus() {\n    // Ensure that handleStop causes AudioFocusManager to call through to abandon audio focus\n    // even if focus wasn't requested.\n    AudioAttributes media =\n        new AudioAttributes.Builder()\n            .setUsage(C.USAGE_MEDIA)\n            .setContentType(C.CONTENT_TYPE_SPEECH)\n            .build();\n\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                media, /* playWhenReady= */ false, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull();\n    ShadowAudioManager.AudioFocusRequest request =\n        Shadows.shadowOf(audioManager).getLastAudioFocusRequest();\n    assertThat(request).isNull();\n\n    audioFocusManager.handleStop();\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNotNull();\n  }\n\n  @Test\n  @Config(maxSdk = 25)\n  public void handleStop_withoutHandlingAudioFocus_isNoOp() {\n    // Ensure that handleStop is a no-op if audio focus isn't handled.\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                /* audioAttributes= */ null, /* playWhenReady= */ false, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull();\n    ShadowAudioManager.AudioFocusRequest request =\n        Shadows.shadowOf(audioManager).getLastAudioFocusRequest();\n    assertThat(request).isNull();\n\n    audioFocusManager.handleStop();\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull();\n  }\n\n  @Test\n  @Config(minSdk = 26)\n  public void handleStop_withoutHandlingAudioFocus_isNoOp_v26() {\n    // Ensure that handleStop is a no-op if audio focus isn't handled.\n    Shadows.shadowOf(audioManager)\n        .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n    assertThat(\n            audioFocusManager.setAudioAttributes(\n                /* audioAttributes= */ null, /* playWhenReady= */ false, Player.STATE_READY))\n        .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull();\n    ShadowAudioManager.AudioFocusRequest request =\n        Shadows.shadowOf(audioManager).getLastAudioFocusRequest();\n    assertThat(request).isNull();\n\n    audioFocusManager.handleStop();\n    assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull();\n  }\n\n  private int getAudioFocusGainFromRequest(ShadowAudioManager.AudioFocusRequest audioFocusRequest) {\n    return Util.SDK_INT >= 26\n        ? audioFocusRequest.audioFocusRequest.getFocusGain()\n        : audioFocusRequest.durationHint;\n  }\n\n  private static class TestPlayerControl implements AudioFocusManager.PlayerControl {\n    private float lastVolumeMultiplier = 1.0f;\n    private int lastPlayerCommand = NO_COMMAND_RECEIVED;\n\n    @Override\n    public void setVolumeMultiplier(float volumeMultiplier) {\n      lastVolumeMultiplier = volumeMultiplier;\n    }\n\n    @Override\n    public void executePlayerCommand(int playerCommand) {\n      lastPlayerCommand = playerCommand;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.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 static com.google.common.truth.Truth.assertThat;\nimport static org.robolectric.annotation.Config.NEWEST_SDK;\nimport static org.robolectric.annotation.Config.OLDEST_SDK;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.Arrays;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/**\n * Unit tests for {@link DefaultAudioSink}.\n *\n * <p>Note: the Robolectric-provided AudioTrack instantiated in the audio sink uses only the Java\n * part of AudioTrack with a {@code ShadowPlayerBase} underneath. This means it will not consume\n * data (i.e., the {@link android.media.AudioTrack#write} methods just return 0), so these tests are\n * currently limited to verifying behavior that doesn't rely on consuming data, and the position\n * will stay at its initial value. For example, we can't verify {@link\n * AudioSink#handleBuffer(ByteBuffer, long)} handling a complete buffer, or queueing audio then\n * draining to the end of the stream. This could be worked around by having a test-only mode where\n * {@link DefaultAudioSink} automatically treats audio as consumed.\n */\n@RunWith(AndroidJUnit4.class)\npublic final class DefaultAudioSinkTest {\n\n  private static final int CHANNEL_COUNT_MONO = 1;\n  private static final int CHANNEL_COUNT_STEREO = 2;\n  private static final int BYTES_PER_FRAME_16_BIT = 2;\n  private static final int SAMPLE_RATE_44_1 = 44100;\n  private static final int TRIM_100_MS_FRAME_COUNT = 4410;\n  private static final int TRIM_10_MS_FRAME_COUNT = 441;\n\n  private DefaultAudioSink defaultAudioSink;\n  private ArrayAudioBufferSink arrayAudioBufferSink;\n\n  @Before\n  public void setUp() {\n    // For capturing output.\n    arrayAudioBufferSink = new ArrayAudioBufferSink();\n    TeeAudioProcessor teeAudioProcessor = new TeeAudioProcessor(arrayAudioBufferSink);\n    defaultAudioSink =\n        new DefaultAudioSink(\n            AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,\n            new DefaultAudioSink.DefaultAudioProcessorChain(teeAudioProcessor),\n            /* enableConvertHighResIntPcmToFloat= */ false);\n  }\n\n  @Test\n  public void handlesSpecializedAudioProcessorArray() {\n    defaultAudioSink =\n        new DefaultAudioSink(\n            AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES, new TeeAudioProcessor[0]);\n  }\n\n  @Test\n  public void handlesBufferAfterReset() throws Exception {\n    configureDefaultAudioSink(CHANNEL_COUNT_STEREO);\n    defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0);\n\n    // After reset and re-configure we can successfully queue more input.\n    defaultAudioSink.reset();\n    configureDefaultAudioSink(CHANNEL_COUNT_STEREO);\n    defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0);\n  }\n\n  @Test\n  public void handlesBufferAfterReset_withPlaybackParameters() throws Exception {\n    PlaybackParameters playbackParameters = new PlaybackParameters(1.5f);\n    defaultAudioSink.setPlaybackParameters(playbackParameters);\n    configureDefaultAudioSink(CHANNEL_COUNT_STEREO);\n    defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0);\n\n    // After reset and re-configure we can successfully queue more input.\n    defaultAudioSink.reset();\n    configureDefaultAudioSink(CHANNEL_COUNT_STEREO);\n    defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0);\n    assertThat(defaultAudioSink.getPlaybackParameters()).isEqualTo(playbackParameters);\n  }\n\n  @Test\n  public void handlesBufferAfterReset_withFormatChange() throws Exception {\n    configureDefaultAudioSink(CHANNEL_COUNT_STEREO);\n    defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0);\n\n    // After reset and re-configure we can successfully queue more input.\n    defaultAudioSink.reset();\n    configureDefaultAudioSink(CHANNEL_COUNT_MONO);\n    defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0);\n  }\n\n  @Test\n  public void handlesBufferAfterReset_withFormatChangeAndPlaybackParameters() throws Exception {\n    PlaybackParameters playbackParameters = new PlaybackParameters(1.5f);\n    defaultAudioSink.setPlaybackParameters(playbackParameters);\n    configureDefaultAudioSink(CHANNEL_COUNT_STEREO);\n    defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0);\n\n    // After reset and re-configure we can successfully queue more input.\n    defaultAudioSink.reset();\n    configureDefaultAudioSink(CHANNEL_COUNT_MONO);\n    defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0);\n    assertThat(defaultAudioSink.getPlaybackParameters()).isEqualTo(playbackParameters);\n  }\n\n  @Test\n  public void trimsStartFrames() throws Exception {\n    configureDefaultAudioSink(\n        CHANNEL_COUNT_STEREO,\n        /* trimStartFrames= */ TRIM_100_MS_FRAME_COUNT,\n        /* trimEndFrames= */ 0);\n    defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0);\n\n    assertThat(arrayAudioBufferSink.output)\n        .hasLength(\n            (BYTES_PER_FRAME_16_BIT\n                * CHANNEL_COUNT_STEREO\n                * (SAMPLE_RATE_44_1 - TRIM_100_MS_FRAME_COUNT)));\n  }\n\n  @Test\n  public void trimsEndFrames() throws Exception {\n    configureDefaultAudioSink(\n        CHANNEL_COUNT_STEREO,\n        /* trimStartFrames= */ 0,\n        /* trimEndFrames= */ TRIM_10_MS_FRAME_COUNT);\n    defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0);\n\n    assertThat(arrayAudioBufferSink.output)\n        .hasLength(\n            (BYTES_PER_FRAME_16_BIT\n                * CHANNEL_COUNT_STEREO\n                * (SAMPLE_RATE_44_1 - TRIM_10_MS_FRAME_COUNT)));\n  }\n\n  @Test\n  public void trimsStartAndEndFrames() throws Exception {\n    configureDefaultAudioSink(\n        CHANNEL_COUNT_STEREO,\n        /* trimStartFrames= */ TRIM_100_MS_FRAME_COUNT,\n        /* trimEndFrames= */ TRIM_10_MS_FRAME_COUNT);\n    defaultAudioSink.handleBuffer(createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0);\n\n    assertThat(arrayAudioBufferSink.output)\n        .hasLength(\n            (BYTES_PER_FRAME_16_BIT\n                * CHANNEL_COUNT_STEREO\n                * (SAMPLE_RATE_44_1 - TRIM_100_MS_FRAME_COUNT - TRIM_10_MS_FRAME_COUNT)));\n  }\n\n  @Test\n  public void getCurrentPosition_returnsPositionFromFirstBuffer() throws Exception {\n    configureDefaultAudioSink(CHANNEL_COUNT_STEREO);\n    defaultAudioSink.handleBuffer(\n        createDefaultSilenceBuffer(), /* presentationTimeUs= */ 5 * C.MICROS_PER_SECOND);\n    assertThat(defaultAudioSink.getCurrentPositionUs(/* sourceEnded= */ false))\n        .isEqualTo(5 * C.MICROS_PER_SECOND);\n\n    defaultAudioSink.reset();\n    configureDefaultAudioSink(CHANNEL_COUNT_STEREO);\n    defaultAudioSink.handleBuffer(\n        createDefaultSilenceBuffer(), /* presentationTimeUs= */ 8 * C.MICROS_PER_SECOND);\n    assertThat(defaultAudioSink.getCurrentPositionUs(/* sourceEnded= */ false))\n        .isEqualTo(8 * C.MICROS_PER_SECOND);\n  }\n\n  @Config(minSdk = OLDEST_SDK, maxSdk = 20)\n  @Test\n  public void doesNotSupportFloatOutputBeforeApi21() {\n    assertThat(defaultAudioSink.supportsOutput(CHANNEL_COUNT_STEREO, C.ENCODING_PCM_FLOAT))\n        .isFalse();\n  }\n\n  @Config(minSdk = 21, maxSdk = NEWEST_SDK)\n  @Test\n  public void supportsFloatOutputFromApi21() {\n    assertThat(defaultAudioSink.supportsOutput(CHANNEL_COUNT_STEREO, C.ENCODING_PCM_FLOAT))\n        .isTrue();\n  }\n\n  private void configureDefaultAudioSink(int channelCount) throws AudioSink.ConfigurationException {\n    configureDefaultAudioSink(channelCount, /* trimStartFrames= */ 0, /* trimEndFrames= */ 0);\n  }\n\n  private void configureDefaultAudioSink(int channelCount, int trimStartFrames, int trimEndFrames)\n      throws AudioSink.ConfigurationException {\n    defaultAudioSink.configure(\n        C.ENCODING_PCM_16BIT,\n        channelCount,\n        SAMPLE_RATE_44_1,\n        /* specifiedBufferSize= */ 0,\n        /* outputChannels= */ null,\n        /* trimStartFrames= */ trimStartFrames,\n        /* trimEndFrames= */ trimEndFrames);\n  }\n\n  /** Creates a one second silence buffer for 44.1 kHz stereo 16-bit audio. */\n  private static ByteBuffer createDefaultSilenceBuffer() {\n    return ByteBuffer.allocateDirect(\n            SAMPLE_RATE_44_1 * CHANNEL_COUNT_STEREO * BYTES_PER_FRAME_16_BIT)\n        .order(ByteOrder.nativeOrder());\n  }\n\n  private static final class ArrayAudioBufferSink implements TeeAudioProcessor.AudioBufferSink {\n\n    private byte[] output;\n\n    public ArrayAudioBufferSink() {\n      output = new byte[0];\n    }\n\n    @Override\n    public void flush(int sampleRateHz, int channelCount, int encoding) {\n      output = new byte[0];\n    }\n\n    @Override\n    public void handleBuffer(ByteBuffer buffer) {\n      int position = buffer.position();\n      int remaining = buffer.remaining();\n      output = Arrays.copyOf(output, output.length + remaining);\n      buffer.get(output, 0, remaining);\n      buffer.position(position);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.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.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.audio.AudioProcessor.UnhandledFormatException;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.ShortBuffer;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link SilenceSkippingAudioProcessor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class SilenceSkippingAudioProcessorTest {\n\n  private static final int TEST_SIGNAL_SAMPLE_RATE_HZ = 1000;\n  private static final int TEST_SIGNAL_CHANNEL_COUNT = 2;\n  private static final int TEST_SIGNAL_SILENCE_DURATION_MS = 1000;\n  private static final int TEST_SIGNAL_NOISE_DURATION_MS = 1000;\n  private static final int TEST_SIGNAL_FRAME_COUNT = 100000;\n\n  private static final int INPUT_BUFFER_SIZE = 100;\n\n  private SilenceSkippingAudioProcessor silenceSkippingAudioProcessor;\n\n  @Before\n  public void setUp() {\n    silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor();\n  }\n\n  @Test\n  public void testEnabledProcessor_isActive() throws Exception {\n    // Given an enabled processor.\n    silenceSkippingAudioProcessor.setEnabled(true);\n\n    // When configuring it.\n    boolean reconfigured =\n        silenceSkippingAudioProcessor.configure(\n            TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);\n    silenceSkippingAudioProcessor.flush();\n\n    // It's active.\n    assertThat(reconfigured).isTrue();\n    assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();\n  }\n\n  @Test\n  public void testDisabledProcessor_isNotActive() throws Exception {\n    // Given a disabled processor.\n    silenceSkippingAudioProcessor.setEnabled(false);\n\n    // When configuring it.\n    silenceSkippingAudioProcessor.configure(\n        TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);\n\n    // It's not active.\n    assertThat(silenceSkippingAudioProcessor.isActive()).isFalse();\n  }\n\n  @Test\n  public void testDefaultProcessor_isNotEnabled() throws Exception {\n    // Given a processor in its default state.\n    // When reconfigured.\n    silenceSkippingAudioProcessor.configure(\n        TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);\n\n    // It's not active.\n    assertThat(silenceSkippingAudioProcessor.isActive()).isFalse();\n  }\n\n  @Test\n  public void testChangingSampleRate_requiresReconfiguration() throws Exception {\n    // Given an enabled processor and configured processor.\n    silenceSkippingAudioProcessor.setEnabled(true);\n    boolean reconfigured =\n        silenceSkippingAudioProcessor.configure(\n            TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);\n    if (reconfigured) {\n      silenceSkippingAudioProcessor.flush();\n    }\n\n    // When reconfiguring it with a different sample rate.\n    reconfigured =\n        silenceSkippingAudioProcessor.configure(\n            TEST_SIGNAL_SAMPLE_RATE_HZ * 2, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);\n\n    // It's reconfigured.\n    assertThat(reconfigured).isTrue();\n    assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();\n  }\n\n  @Test\n  public void testReconfiguringWithSameSampleRate_doesNotRequireReconfiguration() throws Exception {\n    // Given an enabled processor and configured processor.\n    silenceSkippingAudioProcessor.setEnabled(true);\n    boolean reconfigured =\n        silenceSkippingAudioProcessor.configure(\n            TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);\n    assertThat(reconfigured).isTrue();\n    silenceSkippingAudioProcessor.flush();\n\n    // When reconfiguring it with the same sample rate.\n    reconfigured =\n        silenceSkippingAudioProcessor.configure(\n            TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);\n\n    // It's not reconfigured but it is active.\n    assertThat(reconfigured).isFalse();\n    assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();\n  }\n\n  @Test\n  public void testSkipInSilentSignal_skipsEverything() throws Exception {\n    // Given a signal with only noise.\n    InputBufferProvider inputBufferProvider =\n        getInputBufferProviderForAlternatingSilenceAndNoise(\n            TEST_SIGNAL_SAMPLE_RATE_HZ,\n            TEST_SIGNAL_CHANNEL_COUNT,\n            TEST_SIGNAL_SILENCE_DURATION_MS,\n            /* noiseDurationMs= */ 0,\n            TEST_SIGNAL_FRAME_COUNT);\n\n    // When processing the entire signal.\n    silenceSkippingAudioProcessor.setEnabled(true);\n    boolean reconfigured =\n        silenceSkippingAudioProcessor.configure(\n            TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);\n    silenceSkippingAudioProcessor.flush();\n    assertThat(reconfigured).isTrue();\n    assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();\n    long totalOutputFrames =\n        process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE);\n\n    // The entire signal is skipped.\n    assertThat(totalOutputFrames).isEqualTo(0);\n    assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(TEST_SIGNAL_FRAME_COUNT);\n  }\n\n  @Test\n  public void testSkipInNoisySignal_skipsNothing() throws Exception {\n    // Given a signal with only silence.\n    InputBufferProvider inputBufferProvider =\n        getInputBufferProviderForAlternatingSilenceAndNoise(\n            TEST_SIGNAL_SAMPLE_RATE_HZ,\n            TEST_SIGNAL_CHANNEL_COUNT,\n            /* silenceDurationMs= */ 0,\n            TEST_SIGNAL_NOISE_DURATION_MS,\n            TEST_SIGNAL_FRAME_COUNT);\n\n    // When processing the entire signal.\n    SilenceSkippingAudioProcessor silenceSkippingAudioProcessor =\n        new SilenceSkippingAudioProcessor();\n    silenceSkippingAudioProcessor.setEnabled(true);\n    boolean reconfigured =\n        silenceSkippingAudioProcessor.configure(\n            TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);\n    silenceSkippingAudioProcessor.flush();\n    assertThat(reconfigured).isTrue();\n    assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();\n    long totalOutputFrames =\n        process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE);\n\n    // None of the signal is skipped.\n    assertThat(totalOutputFrames).isEqualTo(TEST_SIGNAL_FRAME_COUNT);\n    assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(0);\n  }\n\n  @Test\n  public void testSkipInAlternatingTestSignal_hasCorrectOutputAndSkippedFrameCounts()\n      throws Exception {\n    // Given a signal that alternates between silence and noise.\n    InputBufferProvider inputBufferProvider =\n        getInputBufferProviderForAlternatingSilenceAndNoise(\n            TEST_SIGNAL_SAMPLE_RATE_HZ,\n            TEST_SIGNAL_CHANNEL_COUNT,\n            TEST_SIGNAL_SILENCE_DURATION_MS,\n            TEST_SIGNAL_NOISE_DURATION_MS,\n            TEST_SIGNAL_FRAME_COUNT);\n\n    // When processing the entire signal.\n    SilenceSkippingAudioProcessor silenceSkippingAudioProcessor =\n        new SilenceSkippingAudioProcessor();\n    silenceSkippingAudioProcessor.setEnabled(true);\n    boolean reconfigured =\n        silenceSkippingAudioProcessor.configure(\n            TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);\n    silenceSkippingAudioProcessor.flush();\n    assertThat(reconfigured).isTrue();\n    assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();\n    long totalOutputFrames =\n        process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE);\n\n    // The right number of frames are skipped/output.\n    assertThat(totalOutputFrames).isEqualTo(57980);\n    assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(42020);\n  }\n\n  @Test\n  public void testSkipWithSmallerInputBufferSize_hasCorrectOutputAndSkippedFrameCounts()\n      throws Exception {\n    // Given a signal that alternates between silence and noise.\n    InputBufferProvider inputBufferProvider =\n        getInputBufferProviderForAlternatingSilenceAndNoise(\n            TEST_SIGNAL_SAMPLE_RATE_HZ,\n            TEST_SIGNAL_CHANNEL_COUNT,\n            TEST_SIGNAL_SILENCE_DURATION_MS,\n            TEST_SIGNAL_NOISE_DURATION_MS,\n            TEST_SIGNAL_FRAME_COUNT);\n\n    // When processing the entire signal with a smaller input buffer size.\n    SilenceSkippingAudioProcessor silenceSkippingAudioProcessor =\n        new SilenceSkippingAudioProcessor();\n    silenceSkippingAudioProcessor.setEnabled(true);\n    boolean reconfigured =\n        silenceSkippingAudioProcessor.configure(\n            TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);\n    silenceSkippingAudioProcessor.flush();\n    assertThat(reconfigured).isTrue();\n    assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();\n    long totalOutputFrames =\n        process(silenceSkippingAudioProcessor, inputBufferProvider, /* inputBufferSize= */ 80);\n\n    // The right number of frames are skipped/output.\n    assertThat(totalOutputFrames).isEqualTo(57980);\n    assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(42020);\n  }\n\n  @Test\n  public void testSkipWithLargerInputBufferSize_hasCorrectOutputAndSkippedFrameCounts()\n      throws Exception {\n    // Given a signal that alternates between silence and noise.\n    InputBufferProvider inputBufferProvider =\n        getInputBufferProviderForAlternatingSilenceAndNoise(\n            TEST_SIGNAL_SAMPLE_RATE_HZ,\n            TEST_SIGNAL_CHANNEL_COUNT,\n            TEST_SIGNAL_SILENCE_DURATION_MS,\n            TEST_SIGNAL_NOISE_DURATION_MS,\n            TEST_SIGNAL_FRAME_COUNT);\n\n    // When processing the entire signal with a larger input buffer size.\n    SilenceSkippingAudioProcessor silenceSkippingAudioProcessor =\n        new SilenceSkippingAudioProcessor();\n    silenceSkippingAudioProcessor.setEnabled(true);\n    boolean reconfigured =\n        silenceSkippingAudioProcessor.configure(\n            TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);\n    silenceSkippingAudioProcessor.flush();\n    assertThat(reconfigured).isTrue();\n    assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();\n    long totalOutputFrames =\n        process(silenceSkippingAudioProcessor, inputBufferProvider, /* inputBufferSize= */ 120);\n\n    // The right number of frames are skipped/output.\n    assertThat(totalOutputFrames).isEqualTo(57980);\n    assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(42020);\n  }\n\n  @Test\n  public void testSkipThenFlush_resetsSkippedFrameCount() throws Exception {\n    // Given a signal that alternates between silence and noise.\n    InputBufferProvider inputBufferProvider =\n        getInputBufferProviderForAlternatingSilenceAndNoise(\n            TEST_SIGNAL_SAMPLE_RATE_HZ,\n            TEST_SIGNAL_CHANNEL_COUNT,\n            TEST_SIGNAL_SILENCE_DURATION_MS,\n            TEST_SIGNAL_NOISE_DURATION_MS,\n            TEST_SIGNAL_FRAME_COUNT);\n\n    // When processing the entire signal then flushing.\n    SilenceSkippingAudioProcessor silenceSkippingAudioProcessor =\n        new SilenceSkippingAudioProcessor();\n    silenceSkippingAudioProcessor.setEnabled(true);\n    boolean reconfigured =\n        silenceSkippingAudioProcessor.configure(\n            TEST_SIGNAL_SAMPLE_RATE_HZ, TEST_SIGNAL_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);\n    silenceSkippingAudioProcessor.flush();\n    assertThat(reconfigured).isTrue();\n    assertThat(silenceSkippingAudioProcessor.isActive()).isTrue();\n    process(silenceSkippingAudioProcessor, inputBufferProvider, INPUT_BUFFER_SIZE);\n    silenceSkippingAudioProcessor.flush();\n\n    // The skipped frame count is zero.\n    assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(0);\n  }\n\n  /**\n   * Processes the entire stream provided by {@code inputBufferProvider} in chunks of {@code\n   * inputBufferSize} and returns the total number of output frames.\n   */\n  private static long process(\n      SilenceSkippingAudioProcessor processor,\n      InputBufferProvider inputBufferProvider,\n      int inputBufferSize)\n      throws UnhandledFormatException {\n    processor.flush();\n    long totalOutputFrames = 0;\n    while (inputBufferProvider.hasRemaining()) {\n      ByteBuffer inputBuffer = inputBufferProvider.getNextInputBuffer(inputBufferSize);\n      while (inputBuffer.hasRemaining()) {\n        processor.queueInput(inputBuffer);\n        ByteBuffer outputBuffer = processor.getOutput();\n        totalOutputFrames += outputBuffer.remaining() / (2 * processor.getOutputChannelCount());\n        outputBuffer.clear();\n      }\n    }\n    processor.queueEndOfStream();\n    while (!processor.isEnded()) {\n      ByteBuffer outputBuffer = processor.getOutput();\n      totalOutputFrames += outputBuffer.remaining() / (2 * processor.getOutputChannelCount());\n      outputBuffer.clear();\n    }\n    return totalOutputFrames;\n  }\n\n  /**\n   * Returns an {@link InputBufferProvider} that provides input buffers for a stream that alternates\n   * between silence/noise of the specified durations to fill {@code totalFrameCount}.\n   */\n  private static InputBufferProvider getInputBufferProviderForAlternatingSilenceAndNoise(\n      int sampleRateHz,\n      int channelCount,\n      int silenceDurationMs,\n      int noiseDurationMs,\n      int totalFrameCount) {\n    Pcm16BitAudioBuilder audioBuilder = new Pcm16BitAudioBuilder(channelCount, totalFrameCount);\n    while (!audioBuilder.isFull()) {\n      int silenceDurationFrames = (silenceDurationMs * sampleRateHz) / 1000;\n      audioBuilder.appendFrames(/* count= */ silenceDurationFrames, /* channelLevels= */ (short) 0);\n      int noiseDurationFrames = (noiseDurationMs * sampleRateHz) / 1000;\n      audioBuilder.appendFrames(\n          /* count= */ noiseDurationFrames, /* channelLevels= */ Short.MAX_VALUE);\n    }\n    return new InputBufferProvider(audioBuilder.build());\n  }\n\n  /**\n   * Wraps a {@link ShortBuffer} and provides a sequence of {@link ByteBuffer}s of specified sizes\n   * that contain copies of its data.\n   */\n  private static final class InputBufferProvider {\n\n    private final ShortBuffer buffer;\n\n    public InputBufferProvider(ShortBuffer buffer) {\n      this.buffer = buffer;\n    }\n\n    /** Returns the next buffer with size up to {@code sizeBytes}. */\n    public ByteBuffer getNextInputBuffer(int sizeBytes) {\n      ByteBuffer inputBuffer = ByteBuffer.allocate(sizeBytes).order(ByteOrder.nativeOrder());\n      ShortBuffer inputBufferAsShortBuffer = inputBuffer.asShortBuffer();\n      int limit = buffer.limit();\n      buffer.limit(Math.min(buffer.position() + sizeBytes / 2, limit));\n      inputBufferAsShortBuffer.put(buffer);\n      buffer.limit(limit);\n      inputBuffer.limit(inputBufferAsShortBuffer.position() * 2);\n      return inputBuffer;\n    }\n\n    /** Returns whether any more input can be provided via {@link #getNextInputBuffer(int)}. */\n    public boolean hasRemaining() {\n      return buffer.hasRemaining();\n    }\n  }\n\n  /** Builder for {@link ShortBuffer}s that contain 16-bit PCM audio samples. */\n  private static final class Pcm16BitAudioBuilder {\n\n    private final int channelCount;\n    private final ShortBuffer buffer;\n\n    private boolean built;\n\n    public Pcm16BitAudioBuilder(int channelCount, int frameCount) {\n      this.channelCount = channelCount;\n      buffer = ByteBuffer.allocate(frameCount * channelCount * 2).asShortBuffer();\n    }\n\n    /**\n     * Appends {@code count} audio frames, using the specified {@code channelLevels} in each frame.\n     */\n    public void appendFrames(int count, short... channelLevels) {\n      Assertions.checkState(!built);\n      for (int i = 0; i < count; i += channelCount) {\n        for (short channelLevel : channelLevels) {\n          buffer.put(channelLevel);\n        }\n      }\n    }\n\n    /** Returns whether the buffer is full. */\n    public boolean isFull() {\n      Assertions.checkState(!built);\n      return !buffer.hasRemaining();\n    }\n\n    /** Returns the built buffer. After calling this method the builder should not be reused. */\n    public ShortBuffer build() {\n      Assertions.checkState(!built);\n      built = true;\n      buffer.flip();\n      return buffer;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.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 static com.google.android.exoplayer2.RendererCapabilities.ADAPTIVE_NOT_SEAMLESS;\nimport static com.google.android.exoplayer2.RendererCapabilities.FORMAT_HANDLED;\nimport static com.google.android.exoplayer2.RendererCapabilities.TUNNELING_NOT_SUPPORTED;\nimport static com.google.android.exoplayer2.RendererCapabilities.TUNNELING_SUPPORTED;\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.RendererConfiguration;\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.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.ExoMediaCrypto;\nimport com.google.android.exoplayer2.testutil.FakeSampleStream;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.MockitoAnnotations;\nimport org.robolectric.annotation.Config;\n\n/** Unit test for {@link SimpleDecoderAudioRenderer}. */\n@RunWith(AndroidJUnit4.class)\npublic class SimpleDecoderAudioRendererTest {\n\n  private static final Format FORMAT = Format.createSampleFormat(null, MimeTypes.AUDIO_RAW, 0);\n\n  @Mock private AudioSink mockAudioSink;\n  private SimpleDecoderAudioRenderer audioRenderer;\n\n  @Before\n  public void setUp() throws Exception {\n    MockitoAnnotations.initMocks(this);\n    audioRenderer = new SimpleDecoderAudioRenderer(null, null, null, false, mockAudioSink) {\n      @Override\n      protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,\n          Format format) {\n        return FORMAT_HANDLED;\n      }\n\n      @Override\n      protected SimpleDecoder<DecoderInputBuffer, ? extends SimpleOutputBuffer,\n          ? extends AudioDecoderException> createDecoder(Format format, ExoMediaCrypto mediaCrypto)\n          throws AudioDecoderException {\n        return new FakeDecoder();\n      }\n    };\n  }\n\n  @Config(sdk = 19)\n  @Test\n  public void testSupportsFormatAtApi19() {\n    assertThat(audioRenderer.supportsFormat(FORMAT))\n        .isEqualTo(ADAPTIVE_NOT_SEAMLESS | TUNNELING_NOT_SUPPORTED | FORMAT_HANDLED);\n  }\n\n  @Config(sdk = 21)\n  @Test\n  public void testSupportsFormatAtApi21() {\n    // From API 21, tunneling is supported.\n    assertThat(audioRenderer.supportsFormat(FORMAT))\n        .isEqualTo(ADAPTIVE_NOT_SEAMLESS | TUNNELING_SUPPORTED | FORMAT_HANDLED);\n  }\n\n  @Test\n  public void testImmediatelyReadEndOfStreamPlaysAudioSinkToEndOfStream() throws Exception {\n    audioRenderer.enable(\n        RendererConfiguration.DEFAULT,\n        new Format[] {FORMAT},\n        new FakeSampleStream(FORMAT, /* eventDispatcher= */ null, /* shouldOutputSample= */ false),\n        0,\n        false,\n        0);\n    audioRenderer.setCurrentStreamFinal();\n    when(mockAudioSink.isEnded()).thenReturn(true);\n    while (!audioRenderer.isEnded()) {\n      audioRenderer.render(0, 0);\n    }\n    verify(mockAudioSink, times(1)).playToEndOfStream();\n    audioRenderer.disable();\n    audioRenderer.reset();\n    verify(mockAudioSink, times(1)).reset();\n  }\n\n  private static final class FakeDecoder\n      extends SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, AudioDecoderException> {\n\n    public FakeDecoder() {\n      super(new DecoderInputBuffer[1], new SimpleOutputBuffer[1]);\n    }\n\n    @Override\n    public String getName() {\n      return \"FakeDecoder\";\n    }\n\n    @Override\n    protected DecoderInputBuffer createInputBuffer() {\n      return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);\n    }\n\n    @Override\n    protected SimpleOutputBuffer createOutputBuffer() {\n      return new SimpleOutputBuffer(this);\n    }\n\n    @Override\n    protected AudioDecoderException createUnexpectedDecodeException(Throwable error) {\n      return new AudioDecoderException(\"Unexpected decode error\", error);\n    }\n\n    @Override\n    protected AudioDecoderException decode(DecoderInputBuffer inputBuffer,\n        SimpleOutputBuffer outputBuffer, boolean reset) {\n      if (inputBuffer.isEndOfStream()) {\n        outputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);\n      }\n      return null;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.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 static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link SonicAudioProcessor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class SonicAudioProcessorTest {\n\n  private SonicAudioProcessor sonicAudioProcessor;\n\n  @Before\n  public void setUp() {\n    sonicAudioProcessor = new SonicAudioProcessor();\n  }\n\n  @Test\n  public void testReconfigureWithSameSampleRate() throws Exception {\n    // When configured for resampling from 44.1 kHz to 48 kHz, the output sample rate is correct.\n    sonicAudioProcessor.setOutputSampleRateHz(48000);\n    sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT);\n    assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(48000);\n    assertThat(sonicAudioProcessor.isActive()).isTrue();\n    // When reconfigured with 48 kHz input, there is no resampling.\n    sonicAudioProcessor.configure(48000, 2, C.ENCODING_PCM_16BIT);\n    assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(48000);\n    assertThat(sonicAudioProcessor.isActive()).isFalse();\n    // When reconfigure with 44.1 kHz input, resampling is enabled again.\n    sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT);\n    assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(48000);\n    assertThat(sonicAudioProcessor.isActive()).isTrue();\n  }\n\n  @Test\n  public void testNoSampleRateChange() throws Exception {\n    // Configure for resampling 44.1 kHz to 48 kHz.\n    sonicAudioProcessor.setOutputSampleRateHz(48000);\n    sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT);\n    // Reconfigure to not modify the sample rate.\n    sonicAudioProcessor.setOutputSampleRateHz(SonicAudioProcessor.SAMPLE_RATE_NO_CHANGE);\n    sonicAudioProcessor.configure(22050, 2, C.ENCODING_PCM_16BIT);\n    // The sample rate is unmodified, and the audio processor is not active.\n    assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(22050);\n    assertThat(sonicAudioProcessor.isActive()).isFalse();\n  }\n\n  @Test\n  public void testBecomesActiveAfterConfigure() throws Exception {\n    sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT);\n    // Set a new sample rate.\n    sonicAudioProcessor.setOutputSampleRateHz(22050);\n    // The new sample rate is not active yet.\n    assertThat(sonicAudioProcessor.isActive()).isFalse();\n    assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(44100);\n  }\n\n  @Test\n  public void testSampleRateChangeBecomesActiveAfterConfigure() throws Exception {\n    // Configure for resampling 44.1 kHz to 48 kHz.\n    sonicAudioProcessor.setOutputSampleRateHz(48000);\n    sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT);\n    // Set a new sample rate, which isn't active yet.\n    sonicAudioProcessor.setOutputSampleRateHz(22050);\n    assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(48000);\n    // The new sample rate takes effect on reconfiguration.\n    sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT);\n    assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(22050);\n  }\n\n  @Test\n  public void testIsActiveWithSpeedChange() throws Exception {\n    sonicAudioProcessor.setSpeed(1.5f);\n    sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT);\n    sonicAudioProcessor.flush();\n    assertThat(sonicAudioProcessor.isActive()).isTrue();\n  }\n\n  @Test\n  public void testIsActiveWithPitchChange() throws Exception {\n    sonicAudioProcessor.setPitch(1.5f);\n    sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT);\n    sonicAudioProcessor.flush();\n    assertThat(sonicAudioProcessor.isActive()).isTrue();\n  }\n\n  @Test\n  public void testIsNotActiveWithNoChange() throws Exception {\n    sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT);\n    assertThat(sonicAudioProcessor.isActive()).isFalse();\n  }\n\n  @Test\n  public void testDoesNotSupportNon16BitInput() throws Exception {\n    try {\n      sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_8BIT);\n      fail();\n    } catch (AudioProcessor.UnhandledFormatException e) {\n      // Expected.\n    }\n    try {\n      sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_24BIT);\n      fail();\n    } catch (AudioProcessor.UnhandledFormatException e) {\n      // Expected.\n    }\n    try {\n      sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_32BIT);\n      fail();\n    } catch (AudioProcessor.UnhandledFormatException e) {\n      // Expected.\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.database.sqlite.SQLiteDatabase;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link VersionTable}. */\n@RunWith(AndroidJUnit4.class)\npublic class VersionTableTest {\n\n  private static final int FEATURE_1 = 1;\n  private static final int FEATURE_2 = 2;\n  private static final String INSTANCE_1 = \"1\";\n  private static final String INSTANCE_2 = \"2\";\n\n  private DatabaseProvider databaseProvider;\n  private SQLiteDatabase database;\n\n  @Before\n  public void setUp() {\n    databaseProvider = TestUtil.getTestDatabaseProvider();\n    database = databaseProvider.getWritableDatabase();\n  }\n\n  @Test\n  public void getVersion_unsetFeature_returnsVersionUnset() throws DatabaseIOException {\n    int version = VersionTable.getVersion(database, FEATURE_1, INSTANCE_1);\n    assertThat(version).isEqualTo(VersionTable.VERSION_UNSET);\n  }\n\n  @Test\n  public void getVersion_unsetVersion_returnsVersionUnset() throws DatabaseIOException {\n    VersionTable.setVersion(database, FEATURE_1, INSTANCE_1, 1);\n    int version = VersionTable.getVersion(database, FEATURE_1, INSTANCE_2);\n    assertThat(version).isEqualTo(VersionTable.VERSION_UNSET);\n  }\n\n  @Test\n  public void getVersion_returnsSetVersion() throws DatabaseIOException {\n    VersionTable.setVersion(database, FEATURE_1, INSTANCE_1, 1);\n    assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1)).isEqualTo(1);\n\n    VersionTable.setVersion(database, FEATURE_1, INSTANCE_1, 2);\n    assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1)).isEqualTo(2);\n\n    VersionTable.setVersion(database, FEATURE_2, INSTANCE_1, 3);\n    assertThat(VersionTable.getVersion(database, FEATURE_2, INSTANCE_1)).isEqualTo(3);\n    assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1)).isEqualTo(2);\n\n    VersionTable.setVersion(database, FEATURE_2, INSTANCE_2, 4);\n    assertThat(VersionTable.getVersion(database, FEATURE_2, INSTANCE_2)).isEqualTo(4);\n    assertThat(VersionTable.getVersion(database, FEATURE_2, INSTANCE_1)).isEqualTo(3);\n    assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1)).isEqualTo(2);\n  }\n\n  @Test\n  public void removeVersion_removesSetVersion() throws DatabaseIOException {\n    VersionTable.setVersion(database, FEATURE_1, INSTANCE_1, 1);\n    VersionTable.setVersion(database, FEATURE_1, INSTANCE_2, 2);\n    assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1)).isEqualTo(1);\n    assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_2)).isEqualTo(2);\n\n    VersionTable.removeVersion(database, FEATURE_1, INSTANCE_1);\n    assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_1))\n        .isEqualTo(VersionTable.VERSION_UNSET);\n    assertThat(VersionTable.getVersion(database, FEATURE_1, INSTANCE_2)).isEqualTo(2);\n  }\n\n  @Test\n  public void doesTableExist_nonExistingTable_returnsFalse() {\n    assertThat(VersionTable.tableExists(database, \"NonExistingTable\")).isFalse();\n  }\n\n  @Test\n  public void doesTableExist_existingTable_returnsTrue() {\n    String table = \"TestTable\";\n    databaseProvider.getWritableDatabase().execSQL(\"CREATE TABLE \" + table + \" (dummy INTEGER)\");\n    assertThat(VersionTable.tableExists(database, table)).isTrue();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.util.Util;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/** Unit test for {@link ClearKeyUtil}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ClearKeyUtilTest {\n\n  private static final byte[] SINGLE_KEY_RESPONSE =\n      Util.getUtf8Bytes(\n          \"{\"\n              + \"\\\"keys\\\":[\"\n              + \"{\"\n              + \"\\\"k\\\":\\\"abc_def-\\\",\"\n              + \"\\\"kid\\\":\\\"ab_cde-f\\\",\"\n              + \"\\\"kty\\\":\\\"o_c-t\\\",\"\n              + \"\\\"ignored\\\":\\\"ignored\\\"\"\n              + \"}\"\n              + \"],\"\n              + \"\\\"ignored\\\":\\\"ignored\\\"\"\n              + \"}\");\n  private static final byte[] MULTI_KEY_RESPONSE =\n      Util.getUtf8Bytes(\n          \"{\"\n              + \"\\\"keys\\\":[\"\n              + \"{\"\n              + \"\\\"k\\\":\\\"abc_def-\\\",\"\n              + \"\\\"kid\\\":\\\"ab_cde-f\\\",\"\n              + \"\\\"kty\\\":\\\"oct\\\",\"\n              + \"\\\"ignored\\\":\\\"ignored\\\"\"\n              + \"},{\"\n              + \"\\\"k\\\":\\\"ghi_jkl-\\\",\"\n              + \"\\\"kid\\\":\\\"gh_ijk-l\\\",\"\n              + \"\\\"kty\\\":\\\"oct\\\"\"\n              + \"}\"\n              + \"],\"\n              + \"\\\"ignored\\\":\\\"ignored\\\"\"\n              + \"}\");\n  private static final byte[] KEY_REQUEST =\n      Util.getUtf8Bytes(\n          \"{\"\n              + \"\\\"kids\\\":[\"\n              + \"\\\"abc+def/\\\",\"\n              + \"\\\"ab+cde/f\\\"\"\n              + \"],\"\n              + \"\\\"type\\\":\\\"temporary\\\"\"\n              + \"}\");\n\n  @Config(sdk = 26)\n  @Test\n  public void testAdjustSingleKeyResponseDataV26() {\n    // Everything but the keys should be removed. Within each key only the k, kid and kty parameters\n    // should remain. Any \"-\" and \"_\" characters in the k and kid values should be replaced with \"+\"\n    // and \"/\".\n    byte[] expected =\n        Util.getUtf8Bytes(\n            \"{\"\n                + \"\\\"keys\\\":[\"\n                + \"{\"\n                + \"\\\"k\\\":\\\"abc/def+\\\",\\\"kid\\\":\\\"ab/cde+f\\\",\\\"kty\\\":\\\"o_c-t\\\"\"\n                + \"}\"\n                + \"]\"\n                + \"}\");\n    assertThat(ClearKeyUtil.adjustResponseData(SINGLE_KEY_RESPONSE)).isEqualTo(expected);\n  }\n\n  @Config(sdk = 26)\n  @Test\n  public void testAdjustMultiKeyResponseDataV26() {\n    // Everything but the keys should be removed. Within each key only the k, kid and kty parameters\n    // should remain. Any \"-\" and \"_\" characters in the k and kid values should be replaced with \"+\"\n    // and \"/\".\n    byte[] expected =\n        Util.getUtf8Bytes(\n            \"{\"\n                + \"\\\"keys\\\":[\"\n                + \"{\"\n                + \"\\\"k\\\":\\\"abc/def+\\\",\\\"kid\\\":\\\"ab/cde+f\\\",\\\"kty\\\":\\\"oct\\\"\"\n                + \"},{\"\n                + \"\\\"k\\\":\\\"ghi/jkl+\\\",\\\"kid\\\":\\\"gh/ijk+l\\\",\\\"kty\\\":\\\"oct\\\"\"\n                + \"}\"\n                + \"]\"\n                + \"}\");\n    assertThat(ClearKeyUtil.adjustResponseData(MULTI_KEY_RESPONSE)).isEqualTo(expected);\n  }\n\n  @Config(sdk = 27)\n  @Test\n  public void testAdjustResponseDataV27() {\n    // Response should be unchanged.\n    assertThat(ClearKeyUtil.adjustResponseData(SINGLE_KEY_RESPONSE)).isEqualTo(SINGLE_KEY_RESPONSE);\n  }\n\n  @Config(sdk = 26)\n  @Test\n  public void testAdjustRequestDataV26() {\n    // We expect \"+\" and \"/\" to be replaced with \"-\" and \"_\" respectively, for \"kids\".\n    byte[] expected =\n        Util.getUtf8Bytes(\n            \"{\"\n                + \"\\\"kids\\\":[\"\n                + \"\\\"abc-def_\\\",\"\n                + \"\\\"ab-cde_f\\\"\"\n                + \"],\"\n                + \"\\\"type\\\":\\\"temporary\\\"\"\n                + \"}\");\n    assertThat(ClearKeyUtil.adjustRequestData(KEY_REQUEST)).isEqualTo(expected);\n  }\n\n  @Config(sdk = 27)\n  @Test\n  public void testAdjustRequestDataV27() {\n    // Request should be unchanged.\n    assertThat(ClearKeyUtil.adjustRequestData(KEY_REQUEST)).isEqualTo(KEY_REQUEST);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.android.exoplayer2.C.PLAYREADY_UUID;\nimport static com.google.android.exoplayer2.C.UUID_NIL;\nimport static com.google.android.exoplayer2.C.WIDEVINE_UUID;\nimport static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link DrmInitData}. */\n@RunWith(AndroidJUnit4.class)\npublic class DrmInitDataTest {\n\n  private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, VIDEO_MP4,\n      TestUtil.buildTestData(128, 1 /* data seed */));\n  private static final SchemeData DATA_2 = new SchemeData(PLAYREADY_UUID, VIDEO_MP4,\n      TestUtil.buildTestData(128, 2 /* data seed */));\n  private static final SchemeData DATA_1B = new SchemeData(WIDEVINE_UUID, VIDEO_MP4,\n      TestUtil.buildTestData(128, 1 /* data seed */));\n  private static final SchemeData DATA_2B = new SchemeData(PLAYREADY_UUID, VIDEO_MP4,\n      TestUtil.buildTestData(128, 2 /* data seed */));\n  private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, VIDEO_MP4,\n      TestUtil.buildTestData(128, 3 /* data seed */));\n\n  @Test\n  public void testParcelable() {\n    DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2);\n\n    Parcel parcel = Parcel.obtain();\n    drmInitDataToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    DrmInitData drmInitDataFromParcel = DrmInitData.CREATOR.createFromParcel(parcel);\n    assertThat(drmInitDataFromParcel).isEqualTo(drmInitDataToParcel);\n\n    parcel.recycle();\n  }\n\n  @Test\n  public void testEquals() {\n    DrmInitData drmInitData = new DrmInitData(DATA_1, DATA_2);\n\n    // Basic non-referential equality test.\n    DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2);\n    assertThat(testInitData).isEqualTo(drmInitData);\n    assertThat(testInitData.hashCode()).isEqualTo(drmInitData.hashCode());\n\n    // Basic non-referential equality test with non-referential scheme data.\n    testInitData = new DrmInitData(DATA_1B, DATA_2B);\n    assertThat(testInitData).isEqualTo(drmInitData);\n    assertThat(testInitData.hashCode()).isEqualTo(drmInitData.hashCode());\n\n    // Passing the scheme data in reverse order shouldn't affect equality.\n    testInitData = new DrmInitData(DATA_2, DATA_1);\n    assertThat(testInitData).isEqualTo(drmInitData);\n    assertThat(testInitData.hashCode()).isEqualTo(drmInitData.hashCode());\n\n    // Ditto.\n    testInitData = new DrmInitData(DATA_2B, DATA_1B);\n    assertThat(testInitData).isEqualTo(drmInitData);\n    assertThat(testInitData.hashCode()).isEqualTo(drmInitData.hashCode());\n\n    // Different number of tuples should affect equality.\n    testInitData = new DrmInitData(DATA_1);\n    assertThat(drmInitData).isNotEqualTo(testInitData);\n\n    // Different data in one of the tuples should affect equality.\n    testInitData = new DrmInitData(DATA_1, DATA_UNIVERSAL);\n    assertThat(testInitData).isNotEqualTo(drmInitData);\n  }\n\n  @Test\n  @SuppressWarnings(\"deprecation\")\n  public void testGetByUuid() {\n    // Basic matching.\n    DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2);\n    assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1);\n    assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_2);\n    assertThat(testInitData.get(UUID_NIL)).isNull();\n\n    // Basic matching including universal data.\n    testInitData = new DrmInitData(DATA_1, DATA_2, DATA_UNIVERSAL);\n    assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1);\n    assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_2);\n    assertThat(testInitData.get(UUID_NIL)).isEqualTo(DATA_UNIVERSAL);\n\n    // Passing the scheme data in reverse order shouldn't affect equality.\n    testInitData = new DrmInitData(DATA_UNIVERSAL, DATA_2, DATA_1);\n    assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1);\n    assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_2);\n    assertThat(testInitData.get(UUID_NIL)).isEqualTo(DATA_UNIVERSAL);\n\n    // Universal data should be returned in the absence of a specific match.\n    testInitData = new DrmInitData(DATA_1, DATA_UNIVERSAL);\n    assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1);\n    assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_UNIVERSAL);\n    assertThat(testInitData.get(UUID_NIL)).isEqualTo(DATA_UNIVERSAL);\n  }\n\n  @Test\n  public void testGetByIndex() {\n    DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2);\n    assertThat(getAllSchemeData(testInitData)).containsAllOf(DATA_1, DATA_2);\n  }\n\n  @Test\n  @SuppressWarnings(\"deprecation\")\n  public void testSchemeDatasWithSameUuid() {\n    DrmInitData testInitData = new DrmInitData(DATA_1, DATA_1B);\n    assertThat(testInitData.schemeDataCount).isEqualTo(2);\n    // Deprecated get method should return first entry.\n    assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1);\n    // Test retrieval of first and second entry.\n    assertThat(testInitData.get(0)).isEqualTo(DATA_1);\n    assertThat(testInitData.get(1)).isEqualTo(DATA_1B);\n  }\n\n  @Test\n  public void testSchemeDataMatches() {\n    assertThat(DATA_1.matches(WIDEVINE_UUID)).isTrue();\n    assertThat(DATA_1.matches(PLAYREADY_UUID)).isFalse();\n    assertThat(DATA_2.matches(UUID_NIL)).isFalse();\n\n    assertThat(DATA_2.matches(WIDEVINE_UUID)).isFalse();\n    assertThat(DATA_2.matches(PLAYREADY_UUID)).isTrue();\n    assertThat(DATA_2.matches(UUID_NIL)).isFalse();\n\n    assertThat(DATA_UNIVERSAL.matches(WIDEVINE_UUID)).isTrue();\n    assertThat(DATA_UNIVERSAL.matches(PLAYREADY_UUID)).isTrue();\n    assertThat(DATA_UNIVERSAL.matches(UUID_NIL)).isTrue();\n  }\n\n  private List<SchemeData> getAllSchemeData(DrmInitData drmInitData) {\n    ArrayList<SchemeData> schemeDatas = new ArrayList<>();\n    for (int i = 0; i < drmInitData.schemeDataCount; i++) {\n      schemeDatas.add(drmInitData.get(i));\n    }\n    return schemeDatas;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Mockito.when;\n\nimport android.util.Pair;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\nimport java.util.HashMap;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.MockitoAnnotations;\nimport org.robolectric.annotation.Config;\n\n/** Tests {@link OfflineLicenseHelper}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic class OfflineLicenseHelperTest {\n\n  private OfflineLicenseHelper<?> offlineLicenseHelper;\n  @Mock private MediaDrmCallback mediaDrmCallback;\n  @Mock private ExoMediaDrm<ExoMediaCrypto> mediaDrm;\n\n  @Before\n  public void setUp() throws Exception {\n    MockitoAnnotations.initMocks(this);\n    when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3});\n    offlineLicenseHelper =\n        new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, null);\n  }\n\n  @After\n  public void tearDown() throws Exception {\n    offlineLicenseHelper.release();\n    offlineLicenseHelper = null;\n  }\n\n  @Test\n  public void testDownloadRenewReleaseKey() throws Exception {\n    setStubLicenseAndPlaybackDurationValues(1000, 200);\n\n    byte[] keySetId = {2, 5, 8};\n    setStubKeySetId(keySetId);\n\n    byte[] offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(newDrmInitData());\n\n    assertOfflineLicenseKeySetIdEqual(keySetId, offlineLicenseKeySetId);\n\n    byte[] keySetId2 = {6, 7, 0, 1, 4};\n    setStubKeySetId(keySetId2);\n\n    byte[] offlineLicenseKeySetId2 = offlineLicenseHelper.renewLicense(offlineLicenseKeySetId);\n\n    assertOfflineLicenseKeySetIdEqual(keySetId2, offlineLicenseKeySetId2);\n\n    offlineLicenseHelper.releaseLicense(offlineLicenseKeySetId2);\n  }\n\n  @Test\n  public void testDownloadLicenseFailsIfNullInitData() throws Exception {\n    try {\n      offlineLicenseHelper.downloadLicense(null);\n      fail();\n    } catch (IllegalArgumentException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testDownloadLicenseFailsIfNoKeySetIdIsReturned() throws Exception {\n    setStubLicenseAndPlaybackDurationValues(1000, 200);\n\n    try {\n      offlineLicenseHelper.downloadLicense(newDrmInitData());\n      fail();\n    } catch (Exception e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testDownloadLicenseDoesNotFailIfDurationNotAvailable() throws Exception {\n    setDefaultStubKeySetId();\n\n    byte[] offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(newDrmInitData());\n\n    assertThat(offlineLicenseKeySetId).isNotNull();\n  }\n\n  @Test\n  public void testGetLicenseDurationRemainingSec() throws Exception {\n    long licenseDuration = 1000;\n    int playbackDuration = 200;\n    setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration);\n    setDefaultStubKeySetId();\n\n    byte[] offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(newDrmInitData());\n\n    Pair<Long, Long> licenseDurationRemainingSec =\n        offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId);\n\n    assertThat(licenseDurationRemainingSec.first).isEqualTo(licenseDuration);\n    assertThat(licenseDurationRemainingSec.second).isEqualTo(playbackDuration);\n  }\n\n  @Test\n  public void testGetLicenseDurationRemainingSecExpiredLicense() throws Exception {\n    long licenseDuration = 0;\n    int playbackDuration = 0;\n    setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration);\n    setDefaultStubKeySetId();\n\n    byte[] offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(newDrmInitData());\n\n    Pair<Long, Long> licenseDurationRemainingSec =\n        offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId);\n\n    assertThat(licenseDurationRemainingSec.first).isEqualTo(licenseDuration);\n    assertThat(licenseDurationRemainingSec.second).isEqualTo(playbackDuration);\n  }\n\n  private void setDefaultStubKeySetId()\n      throws android.media.NotProvisionedException, android.media.DeniedByServerException {\n    setStubKeySetId(new byte[] {2, 5, 8});\n  }\n\n  private void setStubKeySetId(byte[] keySetId)\n      throws android.media.NotProvisionedException, android.media.DeniedByServerException {\n    when(mediaDrm.provideKeyResponse(any(byte[].class), any())).thenReturn(keySetId);\n  }\n\n  private static void assertOfflineLicenseKeySetIdEqual(\n      byte[] expectedKeySetId, byte[] actualKeySetId) throws Exception {\n    assertThat(actualKeySetId).isNotNull();\n    assertThat(actualKeySetId).isEqualTo(expectedKeySetId);\n  }\n\n  private void setStubLicenseAndPlaybackDurationValues(\n      long licenseDuration, long playbackDuration) {\n    HashMap<String, String> keyStatus = new HashMap<>();\n    keyStatus.put(\n        WidevineUtil.PROPERTY_LICENSE_DURATION_REMAINING, String.valueOf(licenseDuration));\n    keyStatus.put(\n        WidevineUtil.PROPERTY_PLAYBACK_DURATION_REMAINING, String.valueOf(playbackDuration));\n    when(mediaDrm.queryKeyStatus(any(byte[].class))).thenReturn(keyStatus);\n  }\n\n  private static DrmInitData newDrmInitData() {\n    return new DrmInitData(\n        new SchemeData(C.WIDEVINE_UUID, \"mimeType\", new byte[] {1, 4, 7, 0, 3, 6}));\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ConstantBitrateSeekMapTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link ConstantBitrateSeekMap}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ConstantBitrateSeekMapTest {\n\n  private ConstantBitrateSeekMap constantBitrateSeekMap;\n\n  @Test\n  public void testIsSeekable_forKnownInputLength_returnSeekable() {\n    constantBitrateSeekMap =\n        new ConstantBitrateSeekMap(\n            /* inputLength= */ 1000,\n            /* firstFrameBytePosition= */ 0,\n            /* bitrate= */ 8_000,\n            /* frameSize= */ 100);\n    assertThat(constantBitrateSeekMap.isSeekable()).isTrue();\n  }\n\n  @Test\n  public void testIsSeekable_forUnknownInputLength_returnUnseekable() {\n    constantBitrateSeekMap =\n        new ConstantBitrateSeekMap(\n            /* inputLength= */ C.LENGTH_UNSET,\n            /* firstFrameBytePosition= */ 0,\n            /* bitrate= */ 8_000,\n            /* frameSize= */ 100);\n    assertThat(constantBitrateSeekMap.isSeekable()).isFalse();\n  }\n\n  @Test\n  public void testGetSeekPoints_forUnseekableInput_returnSeekPoint0() {\n    int firstBytePosition = 100;\n    constantBitrateSeekMap =\n        new ConstantBitrateSeekMap(\n            /* inputLength= */ C.LENGTH_UNSET,\n            /* firstFrameBytePosition= */ firstBytePosition,\n            /* bitrate= */ 8_000,\n            /* frameSize= */ 100);\n    SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 123);\n    assertThat(seekPoints.first.timeUs).isEqualTo(0);\n    assertThat(seekPoints.first.position).isEqualTo(firstBytePosition);\n    assertThat(seekPoints.second).isEqualTo(seekPoints.first);\n  }\n\n  @Test\n  public void testGetDurationUs_forKnownInputLength_returnCorrectDuration() {\n    constantBitrateSeekMap =\n        new ConstantBitrateSeekMap(\n            /* inputLength= */ 2_300,\n            /* firstFrameBytePosition= */ 100,\n            /* bitrate= */ 8_000,\n            /* frameSize= */ 100);\n    // Bitrate = 8000 (bits/s) = 1000 (bytes/s)\n    // FrameSize = 100 (bytes), so 1 frame = 1s = 100_000 us\n    // Input length = 2300 (bytes), first frame = 100, so duration = 2_200_000 us.\n    assertThat(constantBitrateSeekMap.getDurationUs()).isEqualTo(2_200_000);\n  }\n\n  @Test\n  public void testGetDurationUs_forUnnnownInputLength_returnUnknownDuration() {\n    constantBitrateSeekMap =\n        new ConstantBitrateSeekMap(\n            /* inputLength= */ C.LENGTH_UNSET,\n            /* firstFrameBytePosition= */ 100,\n            /* bitrate= */ 8_000,\n            /* frameSize= */ 100);\n    assertThat(constantBitrateSeekMap.getDurationUs()).isEqualTo(C.TIME_UNSET);\n  }\n\n  @Test\n  public void testGetSeekPoints_forSeekableInput_forSyncPosition0_return1SeekPoint() {\n    int firstBytePosition = 100;\n    constantBitrateSeekMap =\n        new ConstantBitrateSeekMap(\n            /* inputLength= */ 2_300,\n            /* firstFrameBytePosition= */ firstBytePosition,\n            /* bitrate= */ 8_000,\n            /* frameSize= */ 100);\n    SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 0);\n    assertThat(seekPoints.first.timeUs).isEqualTo(0);\n    assertThat(seekPoints.first.position).isEqualTo(firstBytePosition);\n    assertThat(seekPoints.second).isEqualTo(seekPoints.first);\n  }\n\n  @Test\n  public void testGetSeekPoints_forSeekableInput_forSeekPointAtSyncPosition_return1SeekPoint() {\n    constantBitrateSeekMap =\n        new ConstantBitrateSeekMap(\n            /* inputLength= */ 2_300,\n            /* firstFrameBytePosition= */ 100,\n            /* bitrate= */ 8_000,\n            /* frameSize= */ 100);\n    SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 1_200_000);\n    // Bitrate = 8000 (bits/s) = 1000 (bytes/s)\n    // FrameSize = 100 (bytes), so 1 frame = 1s = 100_000 us\n    assertThat(seekPoints.first.timeUs).isEqualTo(1_200_000);\n    assertThat(seekPoints.first.position).isEqualTo(1300);\n    assertThat(seekPoints.second).isEqualTo(seekPoints.first);\n  }\n\n  @Test\n  public void testGetSeekPoints_forSeekableInput_forNonSyncSeekPosition_return2SeekPoints() {\n    constantBitrateSeekMap =\n        new ConstantBitrateSeekMap(\n            /* inputLength= */ 2_300,\n            /* firstFrameBytePosition= */ 100,\n            /* bitrate= */ 8_000,\n            /* frameSize= */ 100);\n    SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 345_678);\n    // Bitrate = 8000 (bits/s) = 1000 (bytes/s)\n    // FrameSize = 100 (bytes), so 1 frame = 1s = 100_000 us\n    assertThat(seekPoints.first.timeUs).isEqualTo(300_000);\n    assertThat(seekPoints.first.position).isEqualTo(400);\n    assertThat(seekPoints.second.timeUs).isEqualTo(400_000);\n    assertThat(seekPoints.second.position).isEqualTo(500);\n  }\n\n  @Test\n  public void testGetSeekPoints_forSeekableInput_forSeekPointWithinLastFrame_return1SeekPoint() {\n    constantBitrateSeekMap =\n        new ConstantBitrateSeekMap(\n            /* inputLength= */ 2_300,\n            /* firstFrameBytePosition= */ 100,\n            /* bitrate= */ 8_000,\n            /* frameSize= */ 100);\n    SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 2_123_456);\n    assertThat(seekPoints.first.timeUs).isEqualTo(2_100_000);\n    assertThat(seekPoints.first.position).isEqualTo(2_200);\n    assertThat(seekPoints.second).isEqualTo(seekPoints.first);\n  }\n\n  @Test\n  public void testGetSeekPoints_forSeekableInput_forSeekPointAtEndOfStream_return1SeekPoint() {\n    constantBitrateSeekMap =\n        new ConstantBitrateSeekMap(\n            /* inputLength= */ 2_300,\n            /* firstFrameBytePosition= */ 100,\n            /* bitrate= */ 8_000,\n            /* frameSize= */ 100);\n    SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 2_200_000);\n    assertThat(seekPoints.first.timeUs).isEqualTo(2_100_000);\n    assertThat(seekPoints.first.position).isEqualTo(2_200);\n    assertThat(seekPoints.second).isEqualTo(seekPoints.first);\n  }\n\n  @Test\n  public void testGetTimeUsAtPosition_forPosition0_return0() {\n    constantBitrateSeekMap =\n        new ConstantBitrateSeekMap(\n            /* inputLength= */ 2_300,\n            /* firstFrameBytePosition= */ 100,\n            /* bitrate= */ 8_000,\n            /* frameSize= */ 100);\n    long timeUs = constantBitrateSeekMap.getTimeUsAtPosition(0);\n    assertThat(timeUs).isEqualTo(0);\n  }\n\n  @Test\n  public void testGetTimeUsAtPosition_forPositionWithinStream_returnCorrectTime() {\n    constantBitrateSeekMap =\n        new ConstantBitrateSeekMap(\n            /* inputLength= */ 2_300,\n            /* firstFrameBytePosition= */ 100,\n            /* bitrate= */ 8_000,\n            /* frameSize= */ 100);\n    long timeUs = constantBitrateSeekMap.getTimeUsAtPosition(1234);\n    assertThat(timeUs).isEqualTo(1_134_000);\n  }\n\n  @Test\n  public void testGetTimeUsAtPosition_forPositionAtEndOfStream_returnStreamDuration() {\n    constantBitrateSeekMap =\n        new ConstantBitrateSeekMap(\n            /* inputLength= */ 2_300,\n            /* firstFrameBytePosition= */ 100,\n            /* bitrate= */ 8_000,\n            /* frameSize= */ 100);\n    long timeUs = constantBitrateSeekMap.getTimeUsAtPosition(2300);\n    assertThat(timeUs).isEqualTo(constantBitrateSeekMap.getDurationUs());\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.android.exoplayer2.C.RESULT_END_OF_INPUT;\nimport static com.google.common.truth.Truth.assertThat;\nimport static java.util.Arrays.copyOf;\nimport static java.util.Arrays.copyOfRange;\nimport static org.junit.Assert.fail;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.FakeDataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link DefaultExtractorInput}. */\n@RunWith(AndroidJUnit4.class)\npublic class DefaultExtractorInputTest {\n\n  private static final String TEST_URI = \"http://www.google.com\";\n  private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8};\n  private static final int LARGE_TEST_DATA_LENGTH = 8192;\n\n  @Test\n  public void testInitialPosition() throws Exception {\n    FakeDataSource testDataSource = buildDataSource();\n    DefaultExtractorInput input =\n        new DefaultExtractorInput(testDataSource, 123, C.LENGTH_UNSET);\n    assertThat(input.getPosition()).isEqualTo(123);\n  }\n\n  @Test\n  public void testRead() throws Exception {\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    byte[] target = new byte[TEST_DATA.length];\n    // We expect to perform three reads of three bytes, as setup in buildTestDataSource.\n    int bytesRead = 0;\n    bytesRead += input.read(target, 0, TEST_DATA.length);\n    assertThat(bytesRead).isEqualTo(3);\n    bytesRead += input.read(target, 3, TEST_DATA.length);\n    assertThat(bytesRead).isEqualTo(6);\n    bytesRead += input.read(target, 6, TEST_DATA.length);\n    assertThat(bytesRead).isEqualTo(9);\n    // Check the read data is correct.\n    assertThat(Arrays.equals(TEST_DATA, target)).isTrue();\n    // Check we're now indicated that the end of input is reached.\n    int expectedEndOfInput = input.read(target, 0, TEST_DATA.length);\n    assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT);\n  }\n\n  @Test\n  public void testReadPeeked() throws Exception {\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    byte[] target = new byte[TEST_DATA.length];\n\n    input.advancePeekPosition(TEST_DATA.length);\n\n    int bytesRead = input.read(target, 0, TEST_DATA.length);\n    assertThat(bytesRead).isEqualTo(TEST_DATA.length);\n\n    // Check the read data is correct.\n    assertThat(Arrays.equals(TEST_DATA, target)).isTrue();\n  }\n\n  @Test\n  public void testReadMoreDataPeeked() throws Exception {\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    byte[] target = new byte[TEST_DATA.length];\n\n    input.advancePeekPosition(TEST_DATA.length);\n\n    int bytesRead = input.read(target, 0, TEST_DATA.length + 1);\n    assertThat(bytesRead).isEqualTo(TEST_DATA.length);\n\n    // Check the read data is correct.\n    assertThat(Arrays.equals(TEST_DATA, target)).isTrue();\n  }\n\n  @Test\n  public void testReadFullyOnce() throws Exception {\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    byte[] target = new byte[TEST_DATA.length];\n    input.readFully(target, 0, TEST_DATA.length);\n    // Check that we read the whole of TEST_DATA.\n    assertThat(Arrays.equals(TEST_DATA, target)).isTrue();\n    assertThat(input.getPosition()).isEqualTo(TEST_DATA.length);\n    // Check that we see end of input if we read again with allowEndOfInput set.\n    boolean result = input.readFully(target, 0, 1, true);\n    assertThat(result).isFalse();\n    // Check that we fail with EOFException we read again with allowEndOfInput unset.\n    try {\n      input.readFully(target, 0, 1);\n      fail();\n    } catch (EOFException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testReadFullyTwice() throws Exception {\n    // Read TEST_DATA in two parts.\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    byte[] target = new byte[5];\n    input.readFully(target, 0, 5);\n    assertThat(Arrays.equals(copyOf(TEST_DATA, 5), target)).isTrue();\n    assertThat(input.getPosition()).isEqualTo(5);\n    target = new byte[4];\n    input.readFully(target, 0, 4);\n    assertThat(Arrays.equals(copyOfRange(TEST_DATA, 5, 9), target)).isTrue();\n    assertThat(input.getPosition()).isEqualTo(5 + 4);\n  }\n\n  @Test\n  public void testReadFullyTooMuch() throws Exception {\n    // Read more than TEST_DATA. Should fail with an EOFException. Position should not update.\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    try {\n      byte[] target = new byte[TEST_DATA.length + 1];\n      input.readFully(target, 0, TEST_DATA.length + 1);\n      fail();\n    } catch (EOFException e) {\n      // Expected.\n    }\n    assertThat(input.getPosition()).isEqualTo(0);\n\n    // Read more than TEST_DATA with allowEndOfInput set. Should fail with an EOFException because\n    // the end of input isn't encountered immediately. Position should not update.\n    input = createDefaultExtractorInput();\n    try {\n      byte[] target = new byte[TEST_DATA.length + 1];\n      input.readFully(target, 0, TEST_DATA.length + 1, true);\n      fail();\n    } catch (EOFException e) {\n      // Expected.\n    }\n    assertThat(input.getPosition()).isEqualTo(0);\n  }\n\n  @Test\n  public void testReadFullyWithFailingDataSource() throws Exception {\n    FakeDataSource testDataSource = buildFailingDataSource();\n    DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET);\n    try {\n      byte[] target = new byte[TEST_DATA.length];\n      input.readFully(target, 0, TEST_DATA.length);\n      fail();\n    } catch (IOException e) {\n      // Expected.\n    }\n    // The position should not have advanced.\n    assertThat(input.getPosition()).isEqualTo(0);\n  }\n\n  @Test\n  public void testReadFullyHalfPeeked() throws Exception {\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    byte[] target = new byte[TEST_DATA.length];\n\n    input.advancePeekPosition(4);\n\n    input.readFully(target, 0, TEST_DATA.length);\n\n    // Check the read data is correct.\n    assertThat(Arrays.equals(TEST_DATA, target)).isTrue();\n    assertThat(input.getPosition()).isEqualTo(TEST_DATA.length);\n  }\n\n  @Test\n  public void testSkip() throws Exception {\n    FakeDataSource testDataSource = buildDataSource();\n    DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET);\n    // We expect to perform three skips of three bytes, as setup in buildTestDataSource.\n    for (int i = 0; i < 3; i++) {\n      assertThat(input.skip(TEST_DATA.length)).isEqualTo(3);\n    }\n    // Check we're now indicated that the end of input is reached.\n    int expectedEndOfInput = input.skip(TEST_DATA.length);\n    assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT);\n  }\n\n  @Test\n  public void testLargeSkip() throws Exception {\n    FakeDataSource testDataSource = buildLargeDataSource();\n    DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET);\n    // Check that skipping the entire data source succeeds.\n    int bytesToSkip = LARGE_TEST_DATA_LENGTH;\n    while (bytesToSkip > 0) {\n      bytesToSkip -= input.skip(bytesToSkip);\n    }\n  }\n\n  @Test\n  public void testSkipFullyOnce() throws Exception {\n    // Skip TEST_DATA.\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    input.skipFully(TEST_DATA.length);\n    assertThat(input.getPosition()).isEqualTo(TEST_DATA.length);\n    // Check that we see end of input if we skip again with allowEndOfInput set.\n    boolean result = input.skipFully(1, true);\n    assertThat(result).isFalse();\n    // Check that we fail with EOFException we skip again.\n    try {\n      input.skipFully(1);\n      fail();\n    } catch (EOFException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testSkipFullyTwice() throws Exception {\n    // Skip TEST_DATA in two parts.\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    input.skipFully(5);\n    assertThat(input.getPosition()).isEqualTo(5);\n    input.skipFully(4);\n    assertThat(input.getPosition()).isEqualTo(5 + 4);\n  }\n\n  @Test\n  public void testSkipFullyTwicePeeked() throws Exception {\n    // Skip TEST_DATA.\n    DefaultExtractorInput input = createDefaultExtractorInput();\n\n    input.advancePeekPosition(TEST_DATA.length);\n\n    int halfLength = TEST_DATA.length / 2;\n    input.skipFully(halfLength);\n    assertThat(input.getPosition()).isEqualTo(halfLength);\n\n    input.skipFully(TEST_DATA.length - halfLength);\n    assertThat(input.getPosition()).isEqualTo(TEST_DATA.length);\n  }\n\n  @Test\n  public void testSkipFullyTooMuch() throws Exception {\n    // Skip more than TEST_DATA. Should fail with an EOFException. Position should not update.\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    try {\n      input.skipFully(TEST_DATA.length + 1);\n      fail();\n    } catch (EOFException e) {\n      // Expected.\n    }\n    assertThat(input.getPosition()).isEqualTo(0);\n\n    // Skip more than TEST_DATA with allowEndOfInput set. Should fail with an EOFException because\n    // the end of input isn't encountered immediately. Position should not update.\n    input = createDefaultExtractorInput();\n    try {\n      input.skipFully(TEST_DATA.length + 1, true);\n      fail();\n    } catch (EOFException e) {\n      // Expected.\n    }\n    assertThat(input.getPosition()).isEqualTo(0);\n  }\n\n  @Test\n  public void testSkipFullyWithFailingDataSource() throws Exception {\n    FakeDataSource testDataSource = buildFailingDataSource();\n    DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET);\n    try {\n      input.skipFully(TEST_DATA.length);\n      fail();\n    } catch (IOException e) {\n      // Expected.\n    }\n    // The position should not have advanced.\n    assertThat(input.getPosition()).isEqualTo(0);\n  }\n\n  @Test\n  public void testSkipFullyLarge() throws Exception {\n    // Tests skipping an amount of data that's larger than any internal scratch space.\n    int largeSkipSize = 1024 * 1024;\n    FakeDataSource testDataSource = new FakeDataSource();\n    testDataSource.getDataSet().newDefaultData().appendReadData(new byte[largeSkipSize]);\n    testDataSource.open(new DataSpec(Uri.parse(TEST_URI)));\n\n    DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET);\n    input.skipFully(largeSkipSize);\n    assertThat(input.getPosition()).isEqualTo(largeSkipSize);\n    // Check that we fail with EOFException we skip again.\n    try {\n      input.skipFully(1);\n      fail();\n    } catch (EOFException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testPeekFully() throws Exception {\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    byte[] target = new byte[TEST_DATA.length];\n    input.peekFully(target, 0, TEST_DATA.length);\n\n    // Check that we read the whole of TEST_DATA.\n    assertThat(Arrays.equals(TEST_DATA, target)).isTrue();\n    assertThat(input.getPosition()).isEqualTo(0);\n    assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length);\n\n    // Check that we can read again from the buffer\n    byte[] target2 = new byte[TEST_DATA.length];\n    input.readFully(target2, 0, TEST_DATA.length);\n    assertThat(Arrays.equals(TEST_DATA, target2)).isTrue();\n    assertThat(input.getPosition()).isEqualTo(TEST_DATA.length);\n    assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length);\n\n    // Check that we fail with EOFException if we peek again\n    try {\n      input.peekFully(target, 0, 1);\n      fail();\n    } catch (EOFException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testPeekFullyAfterEofExceptionPeeksAsExpected() throws Exception {\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    byte[] target = new byte[TEST_DATA.length + 10];\n\n    try {\n      input.peekFully(target, /* offset= */ 0, target.length);\n      fail();\n    } catch (EOFException expected) {\n      // Do nothing. Expected.\n    }\n    input.peekFully(target, /* offset= */ 0, /* length= */ TEST_DATA.length);\n\n    assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length);\n    assertThat(Arrays.equals(TEST_DATA, Arrays.copyOf(target, TEST_DATA.length))).isTrue();\n  }\n\n  @Test\n  public void testResetPeekPosition() throws Exception {\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    byte[] target = new byte[TEST_DATA.length];\n    input.peekFully(target, 0, TEST_DATA.length);\n\n    // Check that we read the whole of TEST_DATA.\n    assertThat(Arrays.equals(TEST_DATA, target)).isTrue();\n    assertThat(input.getPosition()).isEqualTo(0);\n\n    // Check that we can peek again after resetting.\n    input.resetPeekPosition();\n    byte[] target2 = new byte[TEST_DATA.length];\n    input.peekFully(target2, 0, TEST_DATA.length);\n    assertThat(Arrays.equals(TEST_DATA, target2)).isTrue();\n\n    // Check that we fail with EOFException if we peek past the end of the input.\n    try {\n      input.peekFully(target, 0, 1);\n      fail();\n    } catch (EOFException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testPeekFullyAtEndOfStreamWithAllowEndOfInputSucceeds() throws Exception {\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    byte[] target = new byte[TEST_DATA.length];\n\n    // Check peeking up to the end of input succeeds.\n    assertThat(input.peekFully(target, 0, TEST_DATA.length, true)).isTrue();\n\n    // Check peeking at the end of input with allowEndOfInput signals the end of input.\n    assertThat(input.peekFully(target, 0, 1, true)).isFalse();\n  }\n\n  @Test\n  public void testPeekFullyAtEndThenReadEndOfInput() throws Exception {\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    byte[] target = new byte[TEST_DATA.length];\n\n    // Peek up to the end of the input.\n    assertThat(input.peekFully(target, 0, TEST_DATA.length, false)).isTrue();\n\n    // Peek the end of the input.\n    assertThat(input.peekFully(target, 0, 1, true)).isFalse();\n\n    // Read up to the end of the input.\n    assertThat(input.readFully(target, 0, TEST_DATA.length, false)).isTrue();\n\n    // Read the end of the input.\n    assertThat(input.readFully(target, 0, 1, true)).isFalse();\n  }\n\n  @Test\n  public void testPeekFullyAcrossEndOfInputWithAllowEndOfInputFails() throws Exception {\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    byte[] target = new byte[TEST_DATA.length];\n\n    // Check peeking before the end of input with allowEndOfInput succeeds.\n    assertThat(input.peekFully(target, 0, TEST_DATA.length - 1, true)).isTrue();\n\n    // Check peeking across the end of input with allowEndOfInput throws.\n    try {\n      input.peekFully(target, 0, 2, true);\n      fail();\n    } catch (EOFException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testResetAndPeekFullyPastEndOfStreamWithAllowEndOfInputFails() throws Exception {\n    DefaultExtractorInput input = createDefaultExtractorInput();\n    byte[] target = new byte[TEST_DATA.length];\n\n    // Check peeking up to the end of input succeeds.\n    assertThat(input.peekFully(target, 0, TEST_DATA.length, true)).isTrue();\n    input.resetPeekPosition();\n    try {\n      // Check peeking one more byte throws.\n      input.peekFully(target, 0, TEST_DATA.length + 1, true);\n      fail();\n    } catch (EOFException e) {\n      // Expected.\n    }\n  }\n\n  private static FakeDataSource buildDataSource() throws Exception {\n    FakeDataSource testDataSource = new FakeDataSource();\n    testDataSource.getDataSet().newDefaultData()\n        .appendReadData(Arrays.copyOfRange(TEST_DATA, 0, 3))\n        .appendReadData(Arrays.copyOfRange(TEST_DATA, 3, 6))\n        .appendReadData(Arrays.copyOfRange(TEST_DATA, 6, 9));\n    testDataSource.open(new DataSpec(Uri.parse(TEST_URI)));\n    return testDataSource;\n  }\n\n  private static FakeDataSource buildFailingDataSource() throws Exception {\n    FakeDataSource testDataSource = new FakeDataSource();\n    testDataSource.getDataSet().newDefaultData()\n        .appendReadData(Arrays.copyOfRange(TEST_DATA, 0, 6))\n        .appendReadError(new IOException())\n        .appendReadData(Arrays.copyOfRange(TEST_DATA, 6, 9));\n    testDataSource.open(new DataSpec(Uri.parse(TEST_URI)));\n    return testDataSource;\n  }\n\n  private static FakeDataSource buildLargeDataSource() throws Exception {\n    FakeDataSource testDataSource = new FakeDataSource();\n    testDataSource.getDataSet().newDefaultData()\n        .appendReadData(new byte[LARGE_TEST_DATA_LENGTH]);\n    testDataSource.open(new DataSpec(Uri.parse(TEST_URI)));\n    return testDataSource;\n  }\n\n  private static DefaultExtractorInput createDefaultExtractorInput() throws Exception {\n    FakeDataSource testDataSource = buildDataSource();\n    return new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\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.PsExtractor;\nimport com.google.android.exoplayer2.extractor.ts.TsExtractor;\nimport com.google.android.exoplayer2.extractor.wav.WavExtractor;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link DefaultExtractorsFactory}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DefaultExtractorsFactoryTest {\n\n  @Test\n  public void testCreateExtractors_returnExpectedClasses() {\n    DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();\n\n    Extractor[] extractors = defaultExtractorsFactory.createExtractors();\n    List<Class> listCreatedExtractorClasses = new ArrayList<>();\n    for (Extractor extractor : extractors) {\n      listCreatedExtractorClasses.add(extractor.getClass());\n    }\n\n    Class[] expectedExtractorClassses =\n        new Class[] {\n          MatroskaExtractor.class,\n          FragmentedMp4Extractor.class,\n          Mp4Extractor.class,\n          Mp3Extractor.class,\n          AdtsExtractor.class,\n          Ac3Extractor.class,\n          TsExtractor.class,\n          FlvExtractor.class,\n          OggExtractor.class,\n          PsExtractor.class,\n          WavExtractor.class,\n          AmrExtractor.class,\n          Ac4Extractor.class\n        };\n\n    assertThat(listCreatedExtractorClasses).containsNoDuplicates();\n    assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link Extractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ExtractorTest {\n\n  @Test\n  public void testConstants() {\n    // Sanity check that constant values match those defined by {@link C}.\n    assertThat(Extractor.RESULT_END_OF_INPUT).isEqualTo(C.RESULT_END_OF_INPUT);\n    // Sanity check that the other constant values don't overlap.\n    assertThat(C.RESULT_END_OF_INPUT != Extractor.RESULT_CONTINUE).isTrue();\n    assertThat(C.RESULT_END_OF_INPUT != Extractor.RESULT_SEEK).isTrue();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/Id3PeekerTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\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.Id3DecoderTest;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport java.io.IOException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link Id3Peeker}. */\n@RunWith(AndroidJUnit4.class)\npublic final class Id3PeekerTest {\n\n  @Test\n  public void testPeekId3Data_returnNull_ifId3TagNotPresentAtBeginningOfInput()\n      throws IOException, InterruptedException {\n    Id3Peeker id3Peeker = new Id3Peeker();\n    FakeExtractorInput input =\n        new FakeExtractorInput.Builder()\n            .setData(new byte[] {1, 'I', 'D', '3', 2, 3, 4, 5, 6, 7, 8, 9, 10})\n            .build();\n\n    Metadata metadata = id3Peeker.peekId3Data(input, /* id3FramePredicate= */ null);\n    assertThat(metadata).isNull();\n  }\n\n  @Test\n  public void testPeekId3Data_returnId3Tag_ifId3TagPresent()\n      throws IOException, InterruptedException {\n    Id3Peeker id3Peeker = new Id3Peeker();\n\n    byte[] rawId3 =\n        Id3DecoderTest.buildSingleFrameTag(\n            \"APIC\",\n            new byte[] {\n              3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32,\n              87, 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0\n            });\n    FakeExtractorInput input = new FakeExtractorInput.Builder().setData(rawId3).build();\n\n    Metadata metadata = id3Peeker.peekId3Data(input, /* id3FramePredicate= */ null);\n    assertThat(metadata.length()).isEqualTo(1);\n\n    ApicFrame apicFrame = (ApicFrame) metadata.get(0);\n    assertThat(apicFrame.mimeType).isEqualTo(\"image/jpeg\");\n    assertThat(apicFrame.pictureType).isEqualTo(16);\n    assertThat(apicFrame.description).isEqualTo(\"Hello World\");\n    assertThat(apicFrame.pictureData).hasLength(10);\n    assertThat(apicFrame.pictureData).isEqualTo(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0});\n  }\n\n  @Test\n  public void testPeekId3Data_returnId3TagAccordingToGivenPredicate_ifId3TagPresent()\n      throws IOException, InterruptedException {\n    Id3Peeker id3Peeker = new Id3Peeker();\n\n    byte[] rawId3 =\n        Id3DecoderTest.buildMultiFramesTag(\n            new Id3DecoderTest.FrameSpec(\n                \"COMM\",\n                new byte[] {\n                  3, 101, 110, 103, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 116,\n                  101, 120, 116, 0\n                }),\n            new Id3DecoderTest.FrameSpec(\n                \"APIC\",\n                new byte[] {\n                  3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111,\n                  32, 87, 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0\n                }));\n    FakeExtractorInput input = new FakeExtractorInput.Builder().setData(rawId3).build();\n\n    Metadata metadata =\n        id3Peeker.peekId3Data(\n            input,\n            (majorVersion, id0, id1, id2, id3) ->\n                id0 == 'C' && id1 == 'O' && id2 == 'M' && id3 == 'M');\n    assertThat(metadata.length()).isEqualTo(1);\n\n    CommentFrame commentFrame = (CommentFrame) metadata.get(0);\n    assertThat(commentFrame.language).isEqualTo(\"eng\");\n    assertThat(commentFrame.description).isEqualTo(\"description\");\n    assertThat(commentFrame.text).isEqualTo(\"text\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.testutil.FakeExtractorOutput;\nimport com.google.android.exoplayer2.testutil.FakeTrackOutput;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.upstream.DefaultDataSource;\nimport com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Random;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link AmrExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class AmrExtractorSeekTest {\n\n  private static final Random random = new Random(1234L);\n\n  private static final String NARROW_BAND_AMR_FILE = \"amr/sample_nb.amr\";\n  private static final int NARROW_BAND_FILE_DURATION_US = 4_360_000;\n\n  private static final String WIDE_BAND_AMR_FILE = \"amr/sample_wb.amr\";\n  private static final int WIDE_BAND_FILE_DURATION_US = 3_380_000;\n\n  private FakeTrackOutput expectedTrackOutput;\n  private DefaultDataSource dataSource;\n\n  @Before\n  public void setUp() {\n    dataSource =\n        new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), \"UserAgent\")\n            .createDataSource();\n  }\n\n  @Test\n  public void testAmrExtractorReads_returnSeekableSeekMap_forNarrowBandAmr()\n      throws IOException, InterruptedException {\n    String fileName = NARROW_BAND_AMR_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAmrExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n\n    AmrExtractor extractor = createAmrExtractor();\n    SeekMap seekMap =\n        TestUtil.extractSeekMap(extractor, new FakeExtractorOutput(), dataSource, fileUri);\n\n    assertThat(seekMap).isNotNull();\n    assertThat(seekMap.getDurationUs()).isEqualTo(NARROW_BAND_FILE_DURATION_US);\n    assertThat(seekMap.isSeekable()).isTrue();\n  }\n\n  @Test\n  public void testSeeking_handlesSeekingToPositionInFile_extractsCorrectFrame_forNarrowBandAmr()\n      throws IOException, InterruptedException {\n    String fileName = NARROW_BAND_AMR_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAmrExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n\n    AmrExtractor extractor = createAmrExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long targetSeekTimeUs = 980_000;\n    int extractedFrameIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testSeeking_handlesSeekToEoF_extractsLastFrame_forNarrowBandAmr()\n      throws IOException, InterruptedException {\n    String fileName = NARROW_BAND_AMR_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAmrExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n    AmrExtractor extractor = createAmrExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long targetSeekTimeUs = seekMap.getDurationUs();\n\n    int extractedFrameIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testSeeking_handlesSeekingBackward_extractsCorrectFrames_forNarrowBandAmr()\n      throws IOException, InterruptedException {\n    String fileName = NARROW_BAND_AMR_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAmrExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n    AmrExtractor extractor = createAmrExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long firstSeekTimeUs = 980_000;\n    TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    long targetSeekTimeUs = 0;\n    int extractedFrameIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testSeeking_handlesSeekingForward_extractsCorrectFrames_forNarrowBandAmr()\n      throws IOException, InterruptedException {\n    String fileName = NARROW_BAND_AMR_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAmrExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n    AmrExtractor extractor = createAmrExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long firstSeekTimeUs = 980_000;\n    TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    long targetSeekTimeUs = 1_200_000;\n    int extractedFrameIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testSeeking_handlesRandomSeeks_extractsCorrectFrames_forNarrowBandAmr()\n      throws IOException, InterruptedException {\n    String fileName = NARROW_BAND_AMR_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAmrExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n    AmrExtractor extractor = createAmrExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long numSeek = 100;\n    for (long i = 0; i < numSeek; i++) {\n      long targetSeekTimeUs = random.nextInt(NARROW_BAND_FILE_DURATION_US + 1);\n      int extractedFrameIndex =\n          TestUtil.seekToTimeUs(\n              extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n      assertThat(extractedFrameIndex).isNotEqualTo(-1);\n      assertFirstFrameAfterSeekContainTargetSeekTime(\n          trackOutput, targetSeekTimeUs, extractedFrameIndex);\n    }\n  }\n\n  @Test\n  public void testAmrExtractorReads_returnSeekableSeekMap_forWideBandAmr()\n      throws IOException, InterruptedException {\n    String fileName = WIDE_BAND_AMR_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAmrExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n\n    AmrExtractor extractor = createAmrExtractor();\n    SeekMap seekMap =\n        TestUtil.extractSeekMap(extractor, new FakeExtractorOutput(), dataSource, fileUri);\n\n    assertThat(seekMap).isNotNull();\n    assertThat(seekMap.getDurationUs()).isEqualTo(WIDE_BAND_FILE_DURATION_US);\n    assertThat(seekMap.isSeekable()).isTrue();\n  }\n\n  @Test\n  public void testSeeking_handlesSeekingToPositionInFile_extractsCorrectFrame_forWideBandAmr()\n      throws IOException, InterruptedException {\n    String fileName = WIDE_BAND_AMR_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAmrExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n\n    AmrExtractor extractor = createAmrExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long targetSeekTimeUs = 980_000;\n    int extractedFrameIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testSeeking_handlesSeekToEoF_extractsLastFrame_forWideBandAmr()\n      throws IOException, InterruptedException {\n    String fileName = WIDE_BAND_AMR_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAmrExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n    AmrExtractor extractor = createAmrExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long targetSeekTimeUs = seekMap.getDurationUs();\n\n    int extractedFrameIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testSeeking_handlesSeekingBackward_extractsCorrectFrames_forWideBandAmr()\n      throws IOException, InterruptedException {\n    String fileName = WIDE_BAND_AMR_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAmrExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n    AmrExtractor extractor = createAmrExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long firstSeekTimeUs = 980_000;\n    TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    long targetSeekTimeUs = 0;\n    int extractedFrameIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testSeeking_handlesSeekingForward_extractsCorrectFrames_forWideBandAmr()\n      throws IOException, InterruptedException {\n    String fileName = WIDE_BAND_AMR_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAmrExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n    AmrExtractor extractor = createAmrExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long firstSeekTimeUs = 980_000;\n    TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    long targetSeekTimeUs = 1_200_000;\n    int extractedFrameIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testSeeking_handlesRandomSeeks_extractsCorrectFrames_forWideBandAmr()\n      throws IOException, InterruptedException {\n    String fileName = WIDE_BAND_AMR_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAmrExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n    AmrExtractor extractor = createAmrExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long numSeek = 100;\n    for (long i = 0; i < numSeek; i++) {\n      long targetSeekTimeUs = random.nextInt(NARROW_BAND_FILE_DURATION_US + 1);\n      int extractedFrameIndex =\n          TestUtil.seekToTimeUs(\n              extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n      assertThat(extractedFrameIndex).isNotEqualTo(-1);\n      assertFirstFrameAfterSeekContainTargetSeekTime(\n          trackOutput, targetSeekTimeUs, extractedFrameIndex);\n    }\n  }\n\n  // Internal methods\n\n  private AmrExtractor createAmrExtractor() {\n    return new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);\n  }\n\n  private void assertFirstFrameAfterSeekContainTargetSeekTime(\n      FakeTrackOutput trackOutput, long seekTimeUs, int firstFrameIndexAfterSeek) {\n    int expectedSampleIndex = findTargetFrameInExpectedOutput(seekTimeUs);\n    // Assert that after seeking, the first sample frame written to output contains the sample\n    // at seek time.\n    trackOutput.assertSample(\n        firstFrameIndexAfterSeek,\n        expectedTrackOutput.getSampleData(expectedSampleIndex),\n        expectedTrackOutput.getSampleTimeUs(expectedSampleIndex),\n        expectedTrackOutput.getSampleFlags(expectedSampleIndex),\n        expectedTrackOutput.getSampleCryptoData(expectedSampleIndex));\n  }\n\n  private int findTargetFrameInExpectedOutput(long seekTimeUs) {\n    List<Long> sampleTimes = expectedTrackOutput.getSampleTimesUs();\n    for (int i = 0; i < sampleTimes.size() - 1; i++) {\n      long currentSampleTime = sampleTimes.get(i);\n      long nextSampleTime = sampleTimes.get(i + 1);\n      if (currentSampleTime <= seekTimeUs && nextSampleTime > seekTimeUs) {\n        return i;\n      }\n    }\n    return sampleTimes.size() - 1;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.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 static com.google.android.exoplayer2.extractor.amr.AmrExtractor.amrSignatureNb;\nimport static com.google.android.exoplayer2.extractor.amr.AmrExtractor.amrSignatureWb;\nimport static com.google.android.exoplayer2.extractor.amr.AmrExtractor.frameSizeBytesByTypeNb;\nimport static com.google.android.exoplayer2.extractor.amr.AmrExtractor.frameSizeBytesByTypeWb;\nimport static com.google.common.truth.Truth.assertThat;\nimport static junit.framework.Assert.fail;\n\nimport androidx.annotation.NonNull;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.FakeExtractorOutput;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.Random;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link AmrExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class AmrExtractorTest {\n\n  private static final Random RANDOM = new Random(1234);\n\n  @Test\n  public void testSniff_nonAmrSignature_returnFalse() throws IOException, InterruptedException {\n    AmrExtractor amrExtractor = setupAmrExtractorWithOutput();\n    FakeExtractorInput input = fakeExtractorInputWithData(Util.getUtf8Bytes(\"0#!AMR\\n123\"));\n\n    boolean result = amrExtractor.sniff(input);\n    assertThat(result).isFalse();\n  }\n\n  @Test\n  public void testRead_nonAmrSignature_throwParserException()\n      throws IOException, InterruptedException {\n    AmrExtractor amrExtractor = setupAmrExtractorWithOutput();\n    FakeExtractorInput input = fakeExtractorInputWithData(Util.getUtf8Bytes(\"0#!AMR-WB\\n\"));\n\n    try {\n      amrExtractor.read(input, new PositionHolder());\n      fail();\n    } catch (ParserException e) {\n      // expected\n    }\n  }\n\n  @Test\n  public void testRead_amrNb_returnParserException_forInvalidFrameType()\n      throws IOException, InterruptedException {\n    AmrExtractor amrExtractor = setupAmrExtractorWithOutput();\n\n    // Frame type 12-14 for narrow band is reserved for future usage.\n    byte[] amrFrame = newNarrowBandAmrFrameWithType(12);\n    byte[] data = joinData(amrSignatureNb(), amrFrame);\n    FakeExtractorInput input = fakeExtractorInputWithData(data);\n\n    try {\n      amrExtractor.read(input, new PositionHolder());\n      fail();\n    } catch (ParserException e) {\n      // expected\n    }\n  }\n\n  @Test\n  public void testRead_amrWb_returnParserException_forInvalidFrameType()\n      throws IOException, InterruptedException {\n    AmrExtractor amrExtractor = setupAmrExtractorWithOutput();\n\n    // Frame type 10-13 for wide band is reserved for future usage.\n    byte[] amrFrame = newWideBandAmrFrameWithType(13);\n    byte[] data = joinData(amrSignatureWb(), amrFrame);\n    FakeExtractorInput input = fakeExtractorInputWithData(data);\n\n    try {\n      amrExtractor.read(input, new PositionHolder());\n      fail();\n    } catch (ParserException e) {\n      // expected\n    }\n  }\n\n  @Test\n  public void testRead_amrNb_returnEndOfInput_ifInputEncountersEoF()\n      throws IOException, InterruptedException {\n    AmrExtractor amrExtractor = setupAmrExtractorWithOutput();\n\n    byte[] amrFrame = newNarrowBandAmrFrameWithType(3);\n    byte[] data = joinData(amrSignatureNb(), amrFrame);\n    FakeExtractorInput input = fakeExtractorInputWithData(data);\n\n    // Read 1st frame, which will put the input at EoF.\n    amrExtractor.read(input, new PositionHolder());\n\n    int result = amrExtractor.read(input, new PositionHolder());\n    assertThat(result).isEqualTo(Extractor.RESULT_END_OF_INPUT);\n  }\n\n  @Test\n  public void testRead_amrWb_returnEndOfInput_ifInputEncountersEoF()\n      throws IOException, InterruptedException {\n    AmrExtractor amrExtractor = setupAmrExtractorWithOutput();\n\n    byte[] amrFrame = newWideBandAmrFrameWithType(5);\n    byte[] data = joinData(amrSignatureWb(), amrFrame);\n    FakeExtractorInput input = fakeExtractorInputWithData(data);\n\n    // Read 1st frame, which will put the input at EoF.\n    amrExtractor.read(input, new PositionHolder());\n\n    int result = amrExtractor.read(input, new PositionHolder());\n    assertThat(result).isEqualTo(Extractor.RESULT_END_OF_INPUT);\n  }\n\n  @Test\n  public void testRead_amrNb_returnParserException_forInvalidFrameHeader()\n      throws IOException, InterruptedException {\n    AmrExtractor amrExtractor = setupAmrExtractorWithOutput();\n\n    byte[] invalidHeaderFrame = newNarrowBandAmrFrameWithType(4);\n\n    // The padding bits are at bit-1 positions in the following pattern: 1000 0011\n    // Padding bits must be 0.\n    invalidHeaderFrame[0] = (byte) (invalidHeaderFrame[0] | 0b01111101);\n\n    byte[] data = joinData(amrSignatureNb(), invalidHeaderFrame);\n    FakeExtractorInput input = fakeExtractorInputWithData(data);\n\n    try {\n      amrExtractor.read(input, new PositionHolder());\n      fail();\n    } catch (ParserException e) {\n      // expected\n    }\n  }\n\n  @Test\n  public void testRead_amrWb_returnParserException_forInvalidFrameHeader()\n      throws IOException, InterruptedException {\n    AmrExtractor amrExtractor = setupAmrExtractorWithOutput();\n\n    byte[] invalidHeaderFrame = newWideBandAmrFrameWithType(6);\n\n    // The padding bits are at bit-1 positions in the following pattern: 1000 0011\n    // Padding bits must be 0.\n    invalidHeaderFrame[0] = (byte) (invalidHeaderFrame[0] | 0b01111110);\n\n    byte[] data = joinData(amrSignatureWb(), invalidHeaderFrame);\n    FakeExtractorInput input = fakeExtractorInputWithData(data);\n\n    try {\n      amrExtractor.read(input, new PositionHolder());\n      fail();\n    } catch (ParserException e) {\n      // expected\n    }\n  }\n\n  @Test\n  public void testExtractingNarrowBandSamples() throws Exception {\n    ExtractorAsserts.assertBehavior(\n        createAmrExtractorFactory(/* withSeeking= */ false), \"amr/sample_nb.amr\");\n  }\n\n  @Test\n  public void testExtractingWideBandSamples() throws Exception {\n    ExtractorAsserts.assertBehavior(\n        createAmrExtractorFactory(/* withSeeking= */ false), \"amr/sample_wb.amr\");\n  }\n\n  @Test\n  public void testExtractingNarrowBandSamples_withSeeking() throws Exception {\n    ExtractorAsserts.assertBehavior(\n        createAmrExtractorFactory(/* withSeeking= */ true), \"amr/sample_nb_cbr.amr\");\n  }\n\n  @Test\n  public void testExtractingWideBandSamples_withSeeking() throws Exception {\n    ExtractorAsserts.assertBehavior(\n        createAmrExtractorFactory(/* withSeeking= */ true), \"amr/sample_wb_cbr.amr\");\n  }\n\n  private byte[] newWideBandAmrFrameWithType(int frameType) {\n    byte frameHeader = (byte) ((frameType << 3) & (0b01111100));\n    int frameContentInBytes = frameSizeBytesByTypeWb(frameType) - 1;\n\n    return joinData(new byte[] {frameHeader}, randomBytesArrayWithLength(frameContentInBytes));\n  }\n\n  private byte[] newNarrowBandAmrFrameWithType(int frameType) {\n    byte frameHeader = (byte) ((frameType << 3) & (0b01111100));\n    int frameContentInBytes = frameSizeBytesByTypeNb(frameType) - 1;\n\n    return joinData(new byte[] {frameHeader}, randomBytesArrayWithLength(frameContentInBytes));\n  }\n\n  private static byte[] randomBytesArrayWithLength(int length) {\n    byte[] result = new byte[length];\n    RANDOM.nextBytes(result);\n    return result;\n  }\n\n  private static byte[] joinData(byte[]... byteArrays) {\n    int totalLength = 0;\n    for (byte[] byteArray : byteArrays) {\n      totalLength += byteArray.length;\n    }\n    byte[] result = new byte[totalLength];\n    int offset = 0;\n    for (byte[] byteArray : byteArrays) {\n      System.arraycopy(byteArray, /* srcPos= */ 0, result, offset, byteArray.length);\n      offset += byteArray.length;\n    }\n    return result;\n  }\n\n  @NonNull\n  private static AmrExtractor setupAmrExtractorWithOutput() {\n    AmrExtractor amrExtractor = new AmrExtractor();\n    FakeExtractorOutput output = new FakeExtractorOutput();\n    amrExtractor.init(output);\n    return amrExtractor;\n  }\n\n  @NonNull\n  private static FakeExtractorInput fakeExtractorInputWithData(byte[] data) {\n    return new FakeExtractorInput.Builder().setData(data).build();\n  }\n\n  @NonNull\n  private static ExtractorAsserts.ExtractorFactory createAmrExtractorFactory(boolean withSeeking) {\n    return () -> {\n      if (!withSeeking) {\n        return new AmrExtractor();\n      } else {\n        return new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link FlvExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class FlvExtractorTest {\n\n  @Test\n  public void testSample() throws Exception {\n    ExtractorAsserts.assertBehavior(FlvExtractor::new, \"flv/sample.flv\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests {@link DefaultEbmlReader}. */\n@RunWith(AndroidJUnit4.class)\npublic class DefaultEbmlReaderTest {\n\n  @Test\n  public void testMasterElement() throws IOException, InterruptedException {\n    ExtractorInput input = createTestInput(0x1A, 0x45, 0xDF, 0xA3, 0x84, 0x42, 0x85, 0x81, 0x01);\n    TestProcessor expected = new TestProcessor();\n    expected.startMasterElement(TestProcessor.ID_EBML, 5, 4);\n    expected.integerElement(TestProcessor.ID_DOC_TYPE_READ_VERSION, 1);\n    expected.endMasterElement(TestProcessor.ID_EBML);\n    assertEvents(input, expected.events);\n  }\n\n  @Test\n  public void testMasterElementEmpty() throws IOException, InterruptedException {\n    ExtractorInput input = createTestInput(0x18, 0x53, 0x80, 0x67, 0x80);\n    TestProcessor expected = new TestProcessor();\n    expected.startMasterElement(TestProcessor.ID_SEGMENT, 5, 0);\n    expected.endMasterElement(TestProcessor.ID_SEGMENT);\n    assertEvents(input, expected.events);\n  }\n\n  @Test\n  public void testUnsignedIntegerElement() throws IOException, InterruptedException {\n    // 0xFE is chosen because for signed integers it should be interpreted as -2\n    ExtractorInput input = createTestInput(0x42, 0xF7, 0x81, 0xFE);\n    TestProcessor expected = new TestProcessor();\n    expected.integerElement(TestProcessor.ID_EBML_READ_VERSION, 254);\n    assertEvents(input, expected.events);\n  }\n\n  @Test\n  public void testUnsignedIntegerElementLarge() throws IOException, InterruptedException {\n    ExtractorInput input =\n        createTestInput(0x42, 0xF7, 0x88, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);\n    TestProcessor expected = new TestProcessor();\n    expected.integerElement(TestProcessor.ID_EBML_READ_VERSION, Long.MAX_VALUE);\n    assertEvents(input, expected.events);\n  }\n\n  @Test\n  public void testUnsignedIntegerElementTooLargeBecomesNegative()\n      throws IOException, InterruptedException {\n    ExtractorInput input =\n        createTestInput(0x42, 0xF7, 0x88, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);\n    TestProcessor expected = new TestProcessor();\n    expected.integerElement(TestProcessor.ID_EBML_READ_VERSION, -1);\n    assertEvents(input, expected.events);\n  }\n\n  @Test\n  public void testStringElement() throws IOException, InterruptedException {\n    ExtractorInput input = createTestInput(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x31, 0x32, 0x33);\n    TestProcessor expected = new TestProcessor();\n    expected.stringElement(TestProcessor.ID_DOC_TYPE, \"Abc123\");\n    assertEvents(input, expected.events);\n  }\n\n  @Test\n  public void testStringElementWithZeroPadding() throws IOException, InterruptedException {\n    ExtractorInput input = createTestInput(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x00, 0x00, 0x00);\n    TestProcessor expected = new TestProcessor();\n    expected.stringElement(TestProcessor.ID_DOC_TYPE, \"Abc\");\n    assertEvents(input, expected.events);\n  }\n\n  @Test\n  public void testStringElementEmpty() throws IOException, InterruptedException {\n    ExtractorInput input = createTestInput(0x42, 0x82, 0x80);\n    TestProcessor expected = new TestProcessor();\n    expected.stringElement(TestProcessor.ID_DOC_TYPE, \"\");\n    assertEvents(input, expected.events);\n  }\n\n  @Test\n  public void testFloatElementFourBytes() throws IOException, InterruptedException {\n    ExtractorInput input =\n        createTestInput(0x44, 0x89, 0x84, 0x3F, 0x80, 0x00, 0x00);\n    TestProcessor expected = new TestProcessor();\n    expected.floatElement(TestProcessor.ID_DURATION, 1.0);\n    assertEvents(input, expected.events);\n  }\n\n  @Test\n  public void testFloatElementEightBytes() throws IOException, InterruptedException {\n    ExtractorInput input =\n        createTestInput(0x44, 0x89, 0x88, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);\n    TestProcessor expected = new TestProcessor();\n    expected.floatElement(TestProcessor.ID_DURATION, -2.0);\n    assertEvents(input, expected.events);\n  }\n\n  @Test\n  public void testBinaryElement() throws IOException, InterruptedException {\n    ExtractorInput input =\n        createTestInput(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08);\n    TestProcessor expected = new TestProcessor();\n    expected.binaryElement(\n        TestProcessor.ID_SIMPLE_BLOCK,\n        8,\n        createTestInput(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08));\n    assertEvents(input, expected.events);\n  }\n\n  private static void assertEvents(ExtractorInput input, List<String> expectedEvents)\n      throws IOException, InterruptedException {\n    DefaultEbmlReader reader = new DefaultEbmlReader();\n    TestProcessor output = new TestProcessor();\n    reader.init(output);\n\n    // We expect the number of successful reads to equal the number of expected events.\n    for (int i = 0; i < expectedEvents.size(); i++) {\n      assertThat(reader.read(input)).isTrue();\n    }\n    // The next read should be unsuccessful.\n    assertThat(reader.read(input)).isFalse();\n    // Check that we really did get to the end of input.\n    assertThat(input.readFully(new byte[1], 0, 1, true)).isFalse();\n\n    assertThat(output.events).containsExactlyElementsIn(expectedEvents).inOrder();\n  }\n\n  /**\n   * Helper to build an {@link ExtractorInput} from byte data.\n   *\n   * @param data Zero or more integers with values between {@code 0x00} and {@code 0xFF}.\n   * @return An {@link ExtractorInput} from which the data can be read.\n   */\n  private static ExtractorInput createTestInput(int... data) {\n    return new FakeExtractorInput.Builder()\n        .setData(TestUtil.createByteArray(data))\n        .setSimulateUnknownLength(true)\n        .build();\n  }\n\n  /** An {@link EbmlProcessor} that records each event callback. */\n  private static final class TestProcessor implements EbmlProcessor {\n\n    // Element IDs\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\n    private static final int ID_SEGMENT = 0x18538067;\n    private static final int ID_DURATION = 0x4489;\n    private static final int ID_SIMPLE_BLOCK = 0xA3;\n\n    private final List<String> events = new ArrayList<>();\n\n    @Override\n    @EbmlProcessor.ElementType\n    public int getElementType(int id) {\n      switch (id) {\n        case ID_EBML:\n        case ID_SEGMENT:\n          return EbmlProcessor.ELEMENT_TYPE_MASTER;\n        case ID_EBML_READ_VERSION:\n        case ID_DOC_TYPE_READ_VERSION:\n          return EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT;\n        case ID_DOC_TYPE:\n          return EbmlProcessor.ELEMENT_TYPE_STRING;\n        case ID_SIMPLE_BLOCK:\n          return EbmlProcessor.ELEMENT_TYPE_BINARY;\n        case ID_DURATION:\n          return EbmlProcessor.ELEMENT_TYPE_FLOAT;\n        default:\n          return EbmlProcessor.ELEMENT_TYPE_UNKNOWN;\n      }\n    }\n\n    @Override\n    public boolean isLevel1Element(int id) {\n      return false;\n    }\n\n    @Override\n    public void startMasterElement(int id, long contentPosition, long contentSize) {\n      events.add(formatEvent(id, \"start contentPosition=\" + contentPosition\n          + \" contentSize=\" + contentSize));\n    }\n\n    @Override\n    public void endMasterElement(int id) {\n      events.add(formatEvent(id, \"end\"));\n    }\n\n    @Override\n    public void integerElement(int id, long value) {\n      events.add(formatEvent(id, \"integer=\" + value));\n    }\n\n    @Override\n    public void floatElement(int id, double value) {\n      events.add(formatEvent(id, \"float=\" + value));\n    }\n\n    @Override\n    public void stringElement(int id, String value) {\n      events.add(formatEvent(id, \"string=\" + value));\n    }\n\n    @Override\n    public void binaryElement(int id, int contentSize, ExtractorInput input)\n        throws IOException, InterruptedException {\n      byte[] bytes = new byte[contentSize];\n      input.readFully(bytes, 0, contentSize);\n      events.add(formatEvent(id, \"bytes=\" + Arrays.toString(bytes)));\n    }\n\n    private static String formatEvent(int id, String event) {\n      return \"[\" + Integer.toHexString(id) + \"] \" + event;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link MatroskaExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class MatroskaExtractorTest {\n\n  @Test\n  public void testMkvSample() throws Exception {\n    ExtractorAsserts.assertBehavior(MatroskaExtractor::new, \"mkv/sample.mkv\");\n  }\n\n  @Test\n  public void testWebmSubsampleEncryption() throws Exception {\n    ExtractorAsserts.assertBehavior(\n        MatroskaExtractor::new, \"mkv/subsample_encrypted_noaltref.webm\");\n  }\n\n  @Test\n  public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception {\n    ExtractorAsserts.assertBehavior(MatroskaExtractor::new, \"mkv/subsample_encrypted_altref.webm\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.android.exoplayer2.C.RESULT_END_OF_INPUT;\nimport static com.google.android.exoplayer2.C.RESULT_MAX_LENGTH_EXCEEDED;\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link VarintReader}. */\n@RunWith(AndroidJUnit4.class)\npublic final class VarintReaderTest {\n\n  private static final byte MAX_BYTE = (byte) 0xFF;\n\n  private static final byte[] DATA_1_BYTE_0 = new byte[] {(byte) 0x80};\n  private static final byte[] DATA_2_BYTE_0 = new byte[] {0x40, 0};\n  private static final byte[] DATA_3_BYTE_0 = new byte[] {0x20, 0, 0};\n  private static final byte[] DATA_4_BYTE_0 = new byte[] {0x10, 0, 0, 0};\n  private static final byte[] DATA_5_BYTE_0 = new byte[] {0x08, 0, 0, 0, 0};\n  private static final byte[] DATA_6_BYTE_0 = new byte[] {0x04, 0, 0, 0, 0, 0};\n  private static final byte[] DATA_7_BYTE_0 = new byte[] {0x02, 0, 0, 0, 0, 0, 0};\n  private static final byte[] DATA_8_BYTE_0 = new byte[] {0x01, 0, 0, 0, 0, 0, 0, 0};\n\n  private static final byte[] DATA_1_BYTE_64 = new byte[] {(byte) 0xC0};\n  private static final byte[] DATA_2_BYTE_64 = new byte[] {0x40, 0x40};\n  private static final byte[] DATA_3_BYTE_64 = new byte[] {0x20, 0, 0x40};\n  private static final byte[] DATA_4_BYTE_64 = new byte[] {0x10, 0, 0, 0x40};\n  private static final byte[] DATA_5_BYTE_64 = new byte[] {0x08, 0, 0, 0, 0x40};\n  private static final byte[] DATA_6_BYTE_64 = new byte[] {0x04, 0, 0, 0, 0, 0x40};\n  private static final byte[] DATA_7_BYTE_64 = new byte[] {0x02, 0, 0, 0, 0, 0, 0x40};\n  private static final byte[] DATA_8_BYTE_64 = new byte[] {0x01, 0, 0, 0, 0, 0, 0, 0x40};\n\n  private static final byte[] DATA_1_BYTE_MAX = new byte[] {MAX_BYTE};\n  private static final byte[] DATA_2_BYTE_MAX = new byte[] {0x7F, MAX_BYTE};\n  private static final byte[] DATA_3_BYTE_MAX = new byte[] {0x3F, MAX_BYTE, MAX_BYTE};\n  private static final byte[] DATA_4_BYTE_MAX = new byte[] {0x1F, MAX_BYTE, MAX_BYTE, MAX_BYTE};\n  private static final byte[] DATA_5_BYTE_MAX =\n      new byte[] {0x0F, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE};\n  private static final byte[] DATA_6_BYTE_MAX =\n      new byte[] {0x07, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE};\n  private static final byte[] DATA_7_BYTE_MAX =\n      new byte[] {0x03, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE};\n  private static final byte[] DATA_8_BYTE_MAX =\n      new byte[] {0x01, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE, MAX_BYTE};\n\n  private static final long VALUE_1_BYTE_MAX = 0x7F;\n  private static final long VALUE_1_BYTE_MAX_WITH_MASK = 0xFF;\n  private static final long VALUE_2_BYTE_MAX = 0x3FFF;\n  private static final long VALUE_2_BYTE_MAX_WITH_MASK = 0x7FFF;\n  private static final long VALUE_3_BYTE_MAX = 0x1FFFFF;\n  private static final long VALUE_3_BYTE_MAX_WITH_MASK = 0x3FFFFF;\n  private static final long VALUE_4_BYTE_MAX = 0xFFFFFFF;\n  private static final long VALUE_4_BYTE_MAX_WITH_MASK = 0x1FFFFFFF;\n  private static final long VALUE_5_BYTE_MAX = 0x7FFFFFFFFL;\n  private static final long VALUE_5_BYTE_MAX_WITH_MASK = 0xFFFFFFFFFL;\n  private static final long VALUE_6_BYTE_MAX = 0x3FFFFFFFFFFL;\n  private static final long VALUE_6_BYTE_MAX_WITH_MASK = 0x7FFFFFFFFFFL;\n  private static final long VALUE_7_BYTE_MAX = 0x1FFFFFFFFFFFFL;\n  private static final long VALUE_7_BYTE_MAX_WITH_MASK = 0x3FFFFFFFFFFFFL;\n  private static final long VALUE_8_BYTE_MAX = 0xFFFFFFFFFFFFFFL;\n  private static final long VALUE_8_BYTE_MAX_WITH_MASK = 0x1FFFFFFFFFFFFFFL;\n\n  @Test\n  public void testReadVarintEndOfInputAtStart() throws IOException, InterruptedException {\n    VarintReader reader = new VarintReader();\n    // Build an input with no data.\n    ExtractorInput input = new FakeExtractorInput.Builder()\n        .setSimulateUnknownLength(true)\n        .build();\n    // End of input allowed.\n    long result = reader.readUnsignedVarint(input, true, false, 8);\n    assertThat(result).isEqualTo(RESULT_END_OF_INPUT);\n    // End of input not allowed.\n    try {\n      reader.readUnsignedVarint(input, false, false, 8);\n      fail();\n    } catch (EOFException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testReadVarintExceedsMaximumAllowedLength() throws IOException, InterruptedException {\n    VarintReader reader = new VarintReader();\n    ExtractorInput input = new FakeExtractorInput.Builder()\n        .setData(DATA_8_BYTE_0)\n        .setSimulateUnknownLength(true)\n        .build();\n    long result = reader.readUnsignedVarint(input, false, true, 4);\n    assertThat(result).isEqualTo(RESULT_MAX_LENGTH_EXCEEDED);\n  }\n\n  @Test\n  public void testReadVarint() throws IOException, InterruptedException {\n    VarintReader reader = new VarintReader();\n    testReadVarint(reader, true, DATA_1_BYTE_0, 1, 0);\n    testReadVarint(reader, true, DATA_2_BYTE_0, 2, 0);\n    testReadVarint(reader, true, DATA_3_BYTE_0, 3, 0);\n    testReadVarint(reader, true, DATA_4_BYTE_0, 4, 0);\n    testReadVarint(reader, true, DATA_5_BYTE_0, 5, 0);\n    testReadVarint(reader, true, DATA_6_BYTE_0, 6, 0);\n    testReadVarint(reader, true, DATA_7_BYTE_0, 7, 0);\n    testReadVarint(reader, true, DATA_8_BYTE_0, 8, 0);\n    testReadVarint(reader, true, DATA_1_BYTE_64, 1, 64);\n    testReadVarint(reader, true, DATA_2_BYTE_64, 2, 64);\n    testReadVarint(reader, true, DATA_3_BYTE_64, 3, 64);\n    testReadVarint(reader, true, DATA_4_BYTE_64, 4, 64);\n    testReadVarint(reader, true, DATA_5_BYTE_64, 5, 64);\n    testReadVarint(reader, true, DATA_6_BYTE_64, 6, 64);\n    testReadVarint(reader, true, DATA_7_BYTE_64, 7, 64);\n    testReadVarint(reader, true, DATA_8_BYTE_64, 8, 64);\n    testReadVarint(reader, true, DATA_1_BYTE_MAX, 1, VALUE_1_BYTE_MAX);\n    testReadVarint(reader, true, DATA_2_BYTE_MAX, 2, VALUE_2_BYTE_MAX);\n    testReadVarint(reader, true, DATA_3_BYTE_MAX, 3, VALUE_3_BYTE_MAX);\n    testReadVarint(reader, true, DATA_4_BYTE_MAX, 4, VALUE_4_BYTE_MAX);\n    testReadVarint(reader, true, DATA_5_BYTE_MAX, 5, VALUE_5_BYTE_MAX);\n    testReadVarint(reader, true, DATA_6_BYTE_MAX, 6, VALUE_6_BYTE_MAX);\n    testReadVarint(reader, true, DATA_7_BYTE_MAX, 7, VALUE_7_BYTE_MAX);\n    testReadVarint(reader, true, DATA_8_BYTE_MAX, 8, VALUE_8_BYTE_MAX);\n    testReadVarint(reader, false, DATA_1_BYTE_MAX, 1, VALUE_1_BYTE_MAX_WITH_MASK);\n    testReadVarint(reader, false, DATA_2_BYTE_MAX, 2, VALUE_2_BYTE_MAX_WITH_MASK);\n    testReadVarint(reader, false, DATA_3_BYTE_MAX, 3, VALUE_3_BYTE_MAX_WITH_MASK);\n    testReadVarint(reader, false, DATA_4_BYTE_MAX, 4, VALUE_4_BYTE_MAX_WITH_MASK);\n    testReadVarint(reader, false, DATA_5_BYTE_MAX, 5, VALUE_5_BYTE_MAX_WITH_MASK);\n    testReadVarint(reader, false, DATA_6_BYTE_MAX, 6, VALUE_6_BYTE_MAX_WITH_MASK);\n    testReadVarint(reader, false, DATA_7_BYTE_MAX, 7, VALUE_7_BYTE_MAX_WITH_MASK);\n    testReadVarint(reader, false, DATA_8_BYTE_MAX, 8, VALUE_8_BYTE_MAX_WITH_MASK);\n  }\n\n  @Test\n  public void testReadVarintFlaky() throws IOException, InterruptedException {\n    VarintReader reader = new VarintReader();\n    testReadVarintFlaky(reader, true, DATA_1_BYTE_0, 1, 0);\n    testReadVarintFlaky(reader, true, DATA_2_BYTE_0, 2, 0);\n    testReadVarintFlaky(reader, true, DATA_3_BYTE_0, 3, 0);\n    testReadVarintFlaky(reader, true, DATA_4_BYTE_0, 4, 0);\n    testReadVarintFlaky(reader, true, DATA_5_BYTE_0, 5, 0);\n    testReadVarintFlaky(reader, true, DATA_6_BYTE_0, 6, 0);\n    testReadVarintFlaky(reader, true, DATA_7_BYTE_0, 7, 0);\n    testReadVarintFlaky(reader, true, DATA_8_BYTE_0, 8, 0);\n    testReadVarintFlaky(reader, true, DATA_1_BYTE_64, 1, 64);\n    testReadVarintFlaky(reader, true, DATA_2_BYTE_64, 2, 64);\n    testReadVarintFlaky(reader, true, DATA_3_BYTE_64, 3, 64);\n    testReadVarintFlaky(reader, true, DATA_4_BYTE_64, 4, 64);\n    testReadVarintFlaky(reader, true, DATA_5_BYTE_64, 5, 64);\n    testReadVarintFlaky(reader, true, DATA_6_BYTE_64, 6, 64);\n    testReadVarintFlaky(reader, true, DATA_7_BYTE_64, 7, 64);\n    testReadVarintFlaky(reader, true, DATA_8_BYTE_64, 8, 64);\n    testReadVarintFlaky(reader, true, DATA_1_BYTE_MAX, 1, VALUE_1_BYTE_MAX);\n    testReadVarintFlaky(reader, true, DATA_2_BYTE_MAX, 2, VALUE_2_BYTE_MAX);\n    testReadVarintFlaky(reader, true, DATA_3_BYTE_MAX, 3, VALUE_3_BYTE_MAX);\n    testReadVarintFlaky(reader, true, DATA_4_BYTE_MAX, 4, VALUE_4_BYTE_MAX);\n    testReadVarintFlaky(reader, true, DATA_5_BYTE_MAX, 5, VALUE_5_BYTE_MAX);\n    testReadVarintFlaky(reader, true, DATA_6_BYTE_MAX, 6, VALUE_6_BYTE_MAX);\n    testReadVarintFlaky(reader, true, DATA_7_BYTE_MAX, 7, VALUE_7_BYTE_MAX);\n    testReadVarintFlaky(reader, true, DATA_8_BYTE_MAX, 8, VALUE_8_BYTE_MAX);\n    testReadVarintFlaky(reader, false, DATA_1_BYTE_MAX, 1, VALUE_1_BYTE_MAX_WITH_MASK);\n    testReadVarintFlaky(reader, false, DATA_2_BYTE_MAX, 2, VALUE_2_BYTE_MAX_WITH_MASK);\n    testReadVarintFlaky(reader, false, DATA_3_BYTE_MAX, 3, VALUE_3_BYTE_MAX_WITH_MASK);\n    testReadVarintFlaky(reader, false, DATA_4_BYTE_MAX, 4, VALUE_4_BYTE_MAX_WITH_MASK);\n    testReadVarintFlaky(reader, false, DATA_5_BYTE_MAX, 5, VALUE_5_BYTE_MAX_WITH_MASK);\n    testReadVarintFlaky(reader, false, DATA_6_BYTE_MAX, 6, VALUE_6_BYTE_MAX_WITH_MASK);\n    testReadVarintFlaky(reader, false, DATA_7_BYTE_MAX, 7, VALUE_7_BYTE_MAX_WITH_MASK);\n    testReadVarintFlaky(reader, false, DATA_8_BYTE_MAX, 8, VALUE_8_BYTE_MAX_WITH_MASK);\n  }\n\n  private static void testReadVarint(VarintReader reader, boolean removeMask, byte[] data,\n      int expectedLength, long expectedValue) throws IOException, InterruptedException {\n    ExtractorInput input = new FakeExtractorInput.Builder()\n        .setData(data)\n        .setSimulateUnknownLength(true)\n        .build();\n    long result = reader.readUnsignedVarint(input, false, removeMask, 8);\n    assertThat(input.getPosition()).isEqualTo(expectedLength);\n    assertThat(result).isEqualTo(expectedValue);\n  }\n\n  private static void testReadVarintFlaky(VarintReader reader, boolean removeMask, byte[] data,\n      int expectedLength, long expectedValue) throws IOException, InterruptedException {\n    ExtractorInput input = new FakeExtractorInput.Builder()\n        .setData(data)\n        .setSimulateUnknownLength(true)\n        .setSimulateIOErrors(true)\n        .setSimulatePartialReads(true)\n        .build();\n    long result = -1;\n    while (result == -1) {\n      try {\n        result = reader.readUnsignedVarint(input, false, removeMask, 8);\n        if (result == C.RESULT_END_OF_INPUT || result == C.RESULT_MAX_LENGTH_EXCEEDED) {\n          // Unexpected.\n          fail();\n        }\n      } catch (SimulatedIOException e) {\n        // Expected.\n      }\n    }\n    assertThat(input.getPosition()).isEqualTo(expectedLength);\n    assertThat(result).isEqualTo(expectedValue);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link Mp3Extractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class Mp3ExtractorTest {\n\n  @Test\n  public void testMp3Sample() throws Exception {\n    ExtractorAsserts.assertBehavior(Mp3Extractor::new, \"mp3/bear.mp3\");\n  }\n\n  @Test\n  public void testTrimmedMp3Sample() throws Exception {\n    ExtractorAsserts.assertBehavior(Mp3Extractor::new, \"mp3/play-trimmed.mp3\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.MpegAudioHeader;\nimport com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;\nimport com.google.android.exoplayer2.extractor.SeekPoint;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link XingSeeker}. */\n@RunWith(AndroidJUnit4.class)\npublic final class XingSeekerTest {\n\n  // Xing header/payload from http://storage.googleapis.com/exoplayer-test-media-0/play.mp3.\n  private static final int XING_FRAME_HEADER_DATA = 0xFFFB3000;\n  private static final byte[] XING_FRAME_PAYLOAD = Util.getBytesFromHexString(\n      \"00000007000008dd000e7919000205080a0d0f1214171a1c1e212426292c2e303336383b3d404245484a4c4f5254\"\n      + \"575a5c5e616466696b6e707376787a7d808285878a8c8f929496999c9ea1a4a6a8abaeb0b3b5b8babdc0c2c4c7\"\n      + \"cacccfd2d4d6d9dcdee1e3e6e8ebeef0f2f5f8fafd\");\n  private static final int XING_FRAME_POSITION = 157;\n\n  /**\n   * Data size, as encoded in {@link #XING_FRAME_PAYLOAD}.\n   */\n  private static final int DATA_SIZE_BYTES = 948505;\n  /**\n   * Duration of the audio stream in microseconds, encoded in {@link #XING_FRAME_PAYLOAD}.\n   */\n  private static final int STREAM_DURATION_US = 59271836;\n  /**\n   * The length of the stream in bytes.\n   */\n  private static final int STREAM_LENGTH = XING_FRAME_POSITION + DATA_SIZE_BYTES;\n\n  private XingSeeker seeker;\n  private XingSeeker seekerWithInputLength;\n  private int xingFrameSize;\n\n  @Before\n  public void setUp() throws Exception {\n    MpegAudioHeader xingFrameHeader = new MpegAudioHeader();\n    MpegAudioHeader.populateHeader(XING_FRAME_HEADER_DATA, xingFrameHeader);\n    seeker = XingSeeker.create(C.LENGTH_UNSET, XING_FRAME_POSITION, xingFrameHeader,\n        new ParsableByteArray(XING_FRAME_PAYLOAD));\n    seekerWithInputLength = XingSeeker.create(STREAM_LENGTH,\n        XING_FRAME_POSITION, xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD));\n    xingFrameSize = xingFrameHeader.frameSize;\n  }\n\n  @Test\n  public void testGetTimeUsBeforeFirstAudioFrame() {\n    assertThat(seeker.getTimeUs(-1)).isEqualTo(0);\n    assertThat(seekerWithInputLength.getTimeUs(-1)).isEqualTo(0);\n  }\n\n  @Test\n  public void testGetTimeUsAtFirstAudioFrame() {\n    assertThat(seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize)).isEqualTo(0);\n    assertThat(seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize)).isEqualTo(0);\n  }\n\n  @Test\n  public void testGetTimeUsAtEndOfStream() {\n    assertThat(seeker.getTimeUs(STREAM_LENGTH))\n        .isEqualTo(STREAM_DURATION_US);\n    assertThat(\n        seekerWithInputLength.getTimeUs(STREAM_LENGTH))\n        .isEqualTo(STREAM_DURATION_US);\n  }\n\n  @Test\n  public void testGetSeekPointsAtStartOfStream() {\n    SeekPoints seekPoints = seeker.getSeekPoints(0);\n    SeekPoint seekPoint = seekPoints.first;\n    assertThat(seekPoint).isEqualTo(seekPoints.second);\n    assertThat(seekPoint.timeUs).isEqualTo(0);\n    assertThat(seekPoint.position).isEqualTo(XING_FRAME_POSITION + xingFrameSize);\n  }\n\n  @Test\n  public void testGetSeekPointsAtEndOfStream() {\n    SeekPoints seekPoints = seeker.getSeekPoints(STREAM_DURATION_US);\n    SeekPoint seekPoint = seekPoints.first;\n    assertThat(seekPoint).isEqualTo(seekPoints.second);\n    assertThat(seekPoint.timeUs).isEqualTo(STREAM_DURATION_US);\n    assertThat(seekPoint.position).isEqualTo(STREAM_LENGTH - 1);\n  }\n\n  @Test\n  public void testGetTimeForAllPositions() {\n    for (int offset = xingFrameSize; offset < DATA_SIZE_BYTES; offset++) {\n      int position = XING_FRAME_POSITION + offset;\n      // Test seeker.\n      long timeUs = seeker.getTimeUs(position);\n      SeekPoints seekPoints = seeker.getSeekPoints(timeUs);\n      SeekPoint seekPoint = seekPoints.first;\n      assertThat(seekPoint).isEqualTo(seekPoints.second);\n      assertThat(seekPoint.position).isEqualTo(position);\n      // Test seekerWithInputLength.\n      timeUs = seekerWithInputLength.getTimeUs(position);\n      seekPoints = seekerWithInputLength.getSeekPoints(timeUs);\n      seekPoint = seekPoints.first;\n      assertThat(seekPoint).isEqualTo(seekPoints.second);\n      assertThat(seekPoint.position).isEqualTo(position);\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link AtomParsers}. */\n@RunWith(AndroidJUnit4.class)\npublic final class AtomParsersTest {\n\n  private static final String ATOM_HEADER = \"000000000000000000000000\";\n  private static final String SAMPLE_COUNT = \"00000004\";\n  private static final byte[] FOUR_BIT_STZ2 = Util.getBytesFromHexString(ATOM_HEADER + \"00000004\"\n      + SAMPLE_COUNT + \"1234\");\n  private static final byte[] EIGHT_BIT_STZ2 = Util.getBytesFromHexString(ATOM_HEADER + \"00000008\"\n      + SAMPLE_COUNT + \"01020304\");\n  private static final byte[] SIXTEEN_BIT_STZ2 = Util.getBytesFromHexString(ATOM_HEADER + \"00000010\"\n      + SAMPLE_COUNT + \"0001000200030004\");\n\n  @Test\n  public void testParseCommonEncryptionSinfFromParentIgnoresUnknownSchemeType() {\n    byte[] cencSinf = new byte[] {\n        0, 0, 0, 24, 115, 105, 110, 102, // size (4), 'sinf' (4)\n        0, 0, 0, 16, 115, 99, 104, 109, // size (4), 'schm' (4)\n        0, 0, 0, 0, 88, 88, 88, 88}; // version (1), flags (3), 'xxxx' (4)\n    assertThat(AtomParsers.parseCommonEncryptionSinfFromParent(\n        new ParsableByteArray(cencSinf), 0, cencSinf.length)).isNull();\n  }\n\n  @Test\n  public void testStz2Parsing4BitFieldSize() {\n    verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(FOUR_BIT_STZ2)));\n  }\n\n  @Test\n  public void testStz2Parsing8BitFieldSize() {\n    verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(EIGHT_BIT_STZ2)));\n  }\n\n  @Test\n  public void testStz2Parsing16BitFieldSize() {\n    verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(SIXTEEN_BIT_STZ2)));\n  }\n\n  private static void verifyStz2Parsing(Atom.LeafAtom stz2Atom) {\n    AtomParsers.Stz2SampleSizeBox box = new AtomParsers.Stz2SampleSizeBox(stz2Atom);\n    assertThat(box.getSampleCount()).isEqualTo(4);\n    assertThat(box.isFixedSampleSize()).isFalse();\n    for (int i = 0; i < box.getSampleCount(); i++) {\n      assertThat(box.readNextSampleSize()).isEqualTo(i + 1);\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link FragmentedMp4Extractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class FragmentedMp4ExtractorTest {\n\n  @Test\n  public void testSample() throws Exception {\n    ExtractorAsserts.assertBehavior(\n        getExtractorFactory(Collections.emptyList()), \"mp4/sample_fragmented.mp4\");\n  }\n\n  @Test\n  public void testSampleSeekable() throws Exception {\n    ExtractorAsserts.assertBehavior(\n        getExtractorFactory(Collections.emptyList()), \"mp4/sample_fragmented_seekable.mp4\");\n  }\n\n  @Test\n  public void testSampleWithSeiPayloadParsing() throws Exception {\n    // Enabling the CEA-608 track enables SEI payload parsing.\n    ExtractorFactory extractorFactory =\n        getExtractorFactory(\n            Collections.singletonList(\n                Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)));\n    ExtractorAsserts.assertBehavior(extractorFactory, \"mp4/sample_fragmented_sei.mp4\");\n  }\n\n  private static ExtractorFactory getExtractorFactory(final List<Format> closedCaptionFormats) {\n    return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntryTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link MdtaMetadataEntry}. */\n@RunWith(AndroidJUnit4.class)\npublic final class MdtaMetadataEntryTest {\n\n  @Test\n  public void testParcelable() {\n    MdtaMetadataEntry mdtaMetadataEntryToParcel =\n        new MdtaMetadataEntry(\"test\", new byte[] {1, 2}, 3, 4);\n\n    Parcel parcel = Parcel.obtain();\n    mdtaMetadataEntryToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    MdtaMetadataEntry mdtaMetadataEntryFromParcel =\n        MdtaMetadataEntry.CREATOR.createFromParcel(parcel);\n    assertThat(mdtaMetadataEntryFromParcel).isEqualTo(mdtaMetadataEntryToParcel);\n\n    parcel.recycle();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link Mp4Extractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class Mp4ExtractorTest {\n\n  @Test\n  public void testMp4Sample() throws Exception {\n    ExtractorAsserts.assertBehavior(Mp4Extractor::new, \"mp4/sample.mp4\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.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.mp4;\n\nimport static com.google.android.exoplayer2.C.WIDEVINE_UUID;\nimport static com.google.android.exoplayer2.extractor.mp4.Atom.TYPE_pssh;\nimport static com.google.android.exoplayer2.extractor.mp4.Atom.parseFullAtomFlags;\nimport static com.google.android.exoplayer2.extractor.mp4.Atom.parseFullAtomVersion;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.UUID;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link PsshAtomUtil}. */\n@RunWith(AndroidJUnit4.class)\npublic final class PsshAtomUtilTest {\n\n  @Test\n  public void testBuildPsshAtom() {\n    byte[] schemeData = new byte[]{0, 1, 2, 3, 4, 5};\n    byte[] psshAtom = PsshAtomUtil.buildPsshAtom(C.WIDEVINE_UUID, schemeData);\n    // Read the PSSH atom back and assert its content is as expected.\n    ParsableByteArray parsablePsshAtom = new ParsableByteArray(psshAtom);\n    assertThat(parsablePsshAtom.readUnsignedIntToInt()).isEqualTo(psshAtom.length); // length\n    assertThat(parsablePsshAtom.readInt()).isEqualTo(TYPE_pssh); // type\n    int fullAtomInt = parsablePsshAtom.readInt(); // version + flags\n    assertThat(parseFullAtomVersion(fullAtomInt)).isEqualTo(0);\n    assertThat(parseFullAtomFlags(fullAtomInt)).isEqualTo(0);\n    UUID systemId = new UUID(parsablePsshAtom.readLong(), parsablePsshAtom.readLong());\n    assertThat(systemId).isEqualTo(WIDEVINE_UUID);\n    assertThat(parsablePsshAtom.readUnsignedIntToInt()).isEqualTo(schemeData.length);\n    byte[] psshSchemeData = new byte[schemeData.length];\n    parsablePsshAtom.readBytes(psshSchemeData, 0, schemeData.length);\n    assertThat(psshSchemeData).isEqualTo(schemeData);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.OggTestData;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.util.Random;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link DefaultOggSeeker}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DefaultOggSeekerTest {\n\n  private final Random random = new Random(0);\n\n  @Test\n  public void testSetupWithUnsetEndPositionFails() {\n    try {\n      new DefaultOggSeeker(\n          /* streamReader= */ new TestStreamReader(),\n          /* payloadStartPosition= */ 0,\n          /* payloadEndPosition= */ C.LENGTH_UNSET,\n          /* firstPayloadPageSize= */ 1,\n          /* firstPayloadPageGranulePosition= */ 1,\n          /* firstPayloadPageIsLastPage= */ false);\n      fail();\n    } catch (IllegalArgumentException e) {\n      // ignored\n    }\n  }\n\n  @Test\n  public void testSeeking() throws IOException, InterruptedException {\n    Random random = new Random(0);\n    for (int i = 0; i < 100; i++) {\n      testSeeking(random);\n    }\n  }\n\n  @Test\n  public void testSkipToNextPage() throws Exception {\n    FakeExtractorInput extractorInput =\n        OggTestData.createInput(\n            TestUtil.joinByteArrays(\n                TestUtil.buildTestData(4000, random),\n                new byte[] {'O', 'g', 'g', 'S'},\n                TestUtil.buildTestData(4000, random)),\n            false);\n    skipToNextPage(extractorInput);\n    assertThat(extractorInput.getPosition()).isEqualTo(4000);\n  }\n\n  @Test\n  public void testSkipToNextPageOverlap() throws Exception {\n    FakeExtractorInput extractorInput =\n        OggTestData.createInput(\n            TestUtil.joinByteArrays(\n                TestUtil.buildTestData(2046, random),\n                new byte[] {'O', 'g', 'g', 'S'},\n                TestUtil.buildTestData(4000, random)),\n            false);\n    skipToNextPage(extractorInput);\n    assertThat(extractorInput.getPosition()).isEqualTo(2046);\n  }\n\n  @Test\n  public void testSkipToNextPageInputShorterThanPeekLength() throws Exception {\n    FakeExtractorInput extractorInput =\n        OggTestData.createInput(\n            TestUtil.joinByteArrays(new byte[] {'x', 'O', 'g', 'g', 'S'}), false);\n    skipToNextPage(extractorInput);\n    assertThat(extractorInput.getPosition()).isEqualTo(1);\n  }\n\n  @Test\n  public void testSkipToNextPageNoMatch() throws Exception {\n    FakeExtractorInput extractorInput =\n        OggTestData.createInput(new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false);\n    try {\n      skipToNextPage(extractorInput);\n      fail();\n    } catch (EOFException e) {\n      // expected\n    }\n  }\n\n  @Test\n  public void testReadGranuleOfLastPage() throws IOException, InterruptedException {\n    FakeExtractorInput input =\n        OggTestData.createInput(\n            TestUtil.joinByteArrays(\n                TestUtil.buildTestData(100, random),\n                OggTestData.buildOggHeader(0x00, 20000, 66, 3),\n                TestUtil.createByteArray(254, 254, 254), // laces\n                TestUtil.buildTestData(3 * 254, random),\n                OggTestData.buildOggHeader(0x00, 40000, 67, 3),\n                TestUtil.createByteArray(254, 254, 254), // laces\n                TestUtil.buildTestData(3 * 254, random),\n                OggTestData.buildOggHeader(0x05, 60000, 68, 3),\n                TestUtil.createByteArray(254, 254, 254), // laces\n                TestUtil.buildTestData(3 * 254, random)),\n            false);\n    assertReadGranuleOfLastPage(input, 60000);\n  }\n\n  @Test\n  public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException {\n    FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false);\n    try {\n      assertReadGranuleOfLastPage(input, 60000);\n      fail();\n    } catch (EOFException e) {\n      // Ignored.\n    }\n  }\n\n  @Test\n  public void testReadGranuleOfLastPageWithUnboundedLength()\n      throws IOException, InterruptedException {\n    FakeExtractorInput input = OggTestData.createInput(new byte[0], true);\n    try {\n      assertReadGranuleOfLastPage(input, 60000);\n      fail();\n    } catch (IllegalArgumentException e) {\n      // Ignored.\n    }\n  }\n\n  private void testSeeking(Random random) throws IOException, InterruptedException {\n    OggTestFile testFile = OggTestFile.generate(random, 1000);\n    FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build();\n    TestStreamReader streamReader = new TestStreamReader();\n    DefaultOggSeeker oggSeeker =\n        new DefaultOggSeeker(\n            /* streamReader= */ streamReader,\n            /* payloadStartPosition= */ 0,\n            /* payloadEndPosition= */ testFile.data.length,\n            /* firstPayloadPageSize= */ testFile.firstPayloadPageSize,\n            /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranuleCount,\n            /* firstPayloadPageIsLastPage= */ false);\n    OggPageHeader pageHeader = new OggPageHeader();\n\n    while (true) {\n      long nextSeekPosition = oggSeeker.read(input);\n      if (nextSeekPosition == -1) {\n        break;\n      }\n      input.setPosition((int) nextSeekPosition);\n    }\n\n    // Test granule 0 from file start.\n    long granule = seekTo(input, oggSeeker, 0, 0);\n    assertThat(granule).isEqualTo(0);\n    assertThat(input.getPosition()).isEqualTo(0);\n\n    // Test granule 0 from file end.\n    granule = seekTo(input, oggSeeker, 0, testFile.data.length - 1);\n    assertThat(granule).isEqualTo(0);\n    assertThat(input.getPosition()).isEqualTo(0);\n\n    // Test last granule.\n    granule = seekTo(input, oggSeeker, testFile.granuleCount - 1, 0);\n    assertThat(granule).isEqualTo(testFile.granuleCount - testFile.lastPayloadPageGranuleCount);\n    assertThat(input.getPosition()).isEqualTo(testFile.data.length - testFile.lastPayloadPageSize);\n\n    for (int i = 0; i < 100; i += 1) {\n      long targetGranule = random.nextInt(testFile.granuleCount);\n      int initialPosition = random.nextInt(testFile.data.length);\n      granule = seekTo(input, oggSeeker, targetGranule, initialPosition);\n      long currentPosition = input.getPosition();\n      if (granule == 0) {\n        assertThat(currentPosition).isEqualTo(0);\n      } else {\n        int previousPageStart = testFile.findPreviousPageStart(currentPosition);\n        input.setPosition(previousPageStart);\n        pageHeader.populate(input, false);\n        assertThat(granule).isEqualTo(pageHeader.granulePosition);\n      }\n\n      input.setPosition((int) currentPosition);\n      pageHeader.populate(input, false);\n      // The target granule should be within the current page.\n      assertThat(granule).isAtMost(targetGranule);\n      assertThat(targetGranule).isLessThan(pageHeader.granulePosition);\n    }\n  }\n\n  private static void skipToNextPage(ExtractorInput extractorInput)\n      throws IOException, InterruptedException {\n    DefaultOggSeeker oggSeeker =\n        new DefaultOggSeeker(\n            /* streamReader= */ new FlacReader(),\n            /* payloadStartPosition= */ 0,\n            /* payloadEndPosition= */ extractorInput.getLength(),\n            /* firstPayloadPageSize= */ 1,\n            /* firstPayloadPageGranulePosition= */ 2,\n            /* firstPayloadPageIsLastPage= */ false);\n    while (true) {\n      try {\n        oggSeeker.skipToNextPage(extractorInput);\n        break;\n      } catch (FakeExtractorInput.SimulatedIOException e) {\n        /* ignored */\n      }\n    }\n  }\n\n  private static void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected)\n      throws IOException, InterruptedException {\n    DefaultOggSeeker oggSeeker =\n        new DefaultOggSeeker(\n            /* streamReader= */ new FlacReader(),\n            /* payloadStartPosition= */ 0,\n            /* payloadEndPosition= */ input.getLength(),\n            /* firstPayloadPageSize= */ 1,\n            /* firstPayloadPageGranulePosition= */ 2,\n            /* firstPayloadPageIsLastPage= */ false);\n    while (true) {\n      try {\n        assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected);\n        break;\n      } catch (FakeExtractorInput.SimulatedIOException e) {\n        // Ignored.\n      }\n    }\n  }\n\n  private static long seekTo(\n      FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition)\n      throws IOException, InterruptedException {\n    long nextSeekPosition = initialPosition;\n    oggSeeker.startSeek(targetGranule);\n    int count = 0;\n    while (nextSeekPosition >= 0) {\n      if (count++ > 100) {\n        fail(\"Seek failed to converge in 100 iterations\");\n      }\n      input.setPosition((int) nextSeekPosition);\n      nextSeekPosition = oggSeeker.read(input);\n    }\n    return -(nextSeekPosition + 2);\n  }\n\n  private static class TestStreamReader extends StreamReader {\n    @Override\n    protected long preparePayload(ParsableByteArray packet) {\n      return 0;\n    }\n\n    @Override\n    protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.OggTestData;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.io.IOException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link OggExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class OggExtractorTest {\n\n  private static final ExtractorFactory OGG_EXTRACTOR_FACTORY = OggExtractor::new;\n\n  @Test\n  public void testOpus() throws Exception {\n    ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, \"ogg/bear.opus\");\n  }\n\n  @Test\n  public void testFlac() throws Exception {\n    ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, \"ogg/bear_flac.ogg\");\n  }\n\n  @Test\n  public void testFlacNoSeektable() throws Exception {\n    ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, \"ogg/bear_flac_noseektable.ogg\");\n  }\n\n  @Test\n  public void testVorbis() throws Exception {\n    ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, \"ogg/bear_vorbis.ogg\");\n  }\n\n  @Test\n  public void testSniffVorbis() throws Exception {\n    byte[] data =\n        TestUtil.joinByteArrays(\n            OggTestData.buildOggHeader(0x02, 0, 1000, 1),\n            TestUtil.createByteArray(7), // Laces\n            new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'});\n    assertThat(sniff(data)).isTrue();\n  }\n\n  @Test\n  public void testSniffFlac() throws Exception {\n    byte[] data =\n        TestUtil.joinByteArrays(\n            OggTestData.buildOggHeader(0x02, 0, 1000, 1),\n            TestUtil.createByteArray(5), // Laces\n            new byte[] {0x7F, 'F', 'L', 'A', 'C'});\n    assertThat(sniff(data)).isTrue();\n  }\n\n  @Test\n  public void testSniffFailsOpusFile() throws Exception {\n    byte[] data =\n        TestUtil.joinByteArrays(\n            OggTestData.buildOggHeader(0x02, 0, 1000, 0x00), new byte[] {'O', 'p', 'u', 's'});\n    assertThat(sniff(data)).isFalse();\n  }\n\n  @Test\n  public void testSniffFailsInvalidOggHeader() throws Exception {\n    byte[] data = OggTestData.buildOggHeader(0x00, 0, 1000, 0x00);\n    assertThat(sniff(data)).isFalse();\n  }\n\n  @Test\n  public void testSniffInvalidHeader() throws Exception {\n    byte[] data =\n        TestUtil.joinByteArrays(\n            OggTestData.buildOggHeader(0x02, 0, 1000, 1),\n            TestUtil.createByteArray(7), // Laces\n            new byte[] {0x7F, 'X', 'o', 'r', 'b', 'i', 's'});\n    assertThat(sniff(data)).isFalse();\n  }\n\n  @Test\n  public void testSniffFailsEOF() throws Exception {\n    byte[] data = OggTestData.buildOggHeader(0x02, 0, 1000, 0x00);\n    assertThat(sniff(data)).isFalse();\n  }\n\n  private boolean sniff(byte[] data) throws InterruptedException, IOException {\n    FakeExtractorInput input =\n        new FakeExtractorInput.Builder()\n            .setData(data)\n            .setSimulateIOErrors(true)\n            .setSimulateUnknownLength(true)\n            .setSimulatePartialReads(true)\n            .build();\n    return TestUtil.sniffTestData(OGG_EXTRACTOR_FACTORY.create(), input);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.OggTestData;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.Random;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link OggPacket}. */\n@RunWith(AndroidJUnit4.class)\npublic final class OggPacketTest {\n\n  private static final String TEST_FILE = \"ogg/bear.opus\";\n\n  private Random random;\n  private OggPacket oggPacket;\n\n  @Before\n  public void setUp() throws Exception {\n    random = new Random(0);\n    oggPacket = new OggPacket();\n  }\n\n  @Test\n  public void testReadPacketsWithEmptyPage() throws Exception {\n    byte[] firstPacket = TestUtil.buildTestData(8, random);\n    byte[] secondPacket = TestUtil.buildTestData(272, random);\n    byte[] thirdPacket = TestUtil.buildTestData(256, random);\n    byte[] fourthPacket = TestUtil.buildTestData(271, random);\n\n    FakeExtractorInput input =\n        OggTestData.createInput(\n            TestUtil.joinByteArrays(\n                // First page with a single packet.\n                OggTestData.buildOggHeader(0x02, 0, 1000, 0x01),\n                TestUtil.createByteArray(0x08), // Laces\n                firstPacket,\n                // Second page with a single packet.\n                OggTestData.buildOggHeader(0x00, 16, 1001, 0x02),\n                TestUtil.createByteArray(0xFF, 0x11), // Laces\n                secondPacket,\n                // Third page with zero packets.\n                OggTestData.buildOggHeader(0x00, 16, 1002, 0x00),\n                // Fourth page with two packets.\n                OggTestData.buildOggHeader(0x04, 128, 1003, 0x04),\n                TestUtil.createByteArray(0xFF, 0x01, 0xFF, 0x10), // Laces\n                thirdPacket,\n                fourthPacket),\n            true);\n\n    assertReadPacket(input, firstPacket);\n    assertThat((oggPacket.getPageHeader().type & 0x02) == 0x02).isTrue();\n    assertThat((oggPacket.getPageHeader().type & 0x04) == 0x04).isFalse();\n    assertThat(oggPacket.getPageHeader().type).isEqualTo(0x02);\n    assertThat(oggPacket.getPageHeader().headerSize).isEqualTo(27 + 1);\n    assertThat(oggPacket.getPageHeader().bodySize).isEqualTo(8);\n    assertThat(oggPacket.getPageHeader().revision).isEqualTo(0x00);\n    assertThat(oggPacket.getPageHeader().pageSegmentCount).isEqualTo(1);\n    assertThat(oggPacket.getPageHeader().pageSequenceNumber).isEqualTo(1000);\n    assertThat(oggPacket.getPageHeader().streamSerialNumber).isEqualTo(4096);\n    assertThat(oggPacket.getPageHeader().granulePosition).isEqualTo(0);\n\n    assertReadPacket(input, secondPacket);\n    assertThat((oggPacket.getPageHeader().type & 0x02) == 0x02).isFalse();\n    assertThat((oggPacket.getPageHeader().type & 0x04) == 0x04).isFalse();\n    assertThat(oggPacket.getPageHeader().type).isEqualTo(0);\n    assertThat(oggPacket.getPageHeader().headerSize).isEqualTo(27 + 2);\n    assertThat(oggPacket.getPageHeader().bodySize).isEqualTo(255 + 17);\n    assertThat(oggPacket.getPageHeader().pageSegmentCount).isEqualTo(2);\n    assertThat(oggPacket.getPageHeader().pageSequenceNumber).isEqualTo(1001);\n    assertThat(oggPacket.getPageHeader().granulePosition).isEqualTo(16);\n\n    assertReadPacket(input, thirdPacket);\n    assertThat((oggPacket.getPageHeader().type & 0x02) == 0x02).isFalse();\n    assertThat((oggPacket.getPageHeader().type & 0x04) == 0x04).isTrue();\n    assertThat(oggPacket.getPageHeader().type).isEqualTo(4);\n    assertThat(oggPacket.getPageHeader().headerSize).isEqualTo(27 + 4);\n    assertThat(oggPacket.getPageHeader().bodySize).isEqualTo(255 + 1 + 255 + 16);\n    assertThat(oggPacket.getPageHeader().pageSegmentCount).isEqualTo(4);\n    // Page 1002 is empty, so current page is 1003.\n    assertThat(oggPacket.getPageHeader().pageSequenceNumber).isEqualTo(1003);\n    assertThat(oggPacket.getPageHeader().granulePosition).isEqualTo(128);\n\n    assertReadPacket(input, fourthPacket);\n\n    assertReadEof(input);\n  }\n\n  @Test\n  public void testReadPacketWithZeroSizeTerminator() throws Exception {\n    byte[] firstPacket = TestUtil.buildTestData(255, random);\n    byte[] secondPacket = TestUtil.buildTestData(8, random);\n\n    FakeExtractorInput input =\n        OggTestData.createInput(\n            TestUtil.joinByteArrays(\n                OggTestData.buildOggHeader(0x06, 0, 1000, 0x04),\n                TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces.\n                firstPacket,\n                secondPacket),\n            true);\n\n    assertReadPacket(input, firstPacket);\n    assertReadPacket(input, secondPacket);\n    assertReadEof(input);\n  }\n\n  @Test\n  public void testReadContinuedPacketOverTwoPages() throws Exception {\n    byte[] firstPacket = TestUtil.buildTestData(518);\n\n    FakeExtractorInput input =\n        OggTestData.createInput(\n            TestUtil.joinByteArrays(\n                // First page.\n                OggTestData.buildOggHeader(0x02, 0, 1000, 0x02),\n                TestUtil.createByteArray(0xFF, 0xFF), // Laces.\n                Arrays.copyOf(firstPacket, 510),\n                // Second page (continued packet).\n                OggTestData.buildOggHeader(0x05, 10, 1001, 0x01),\n                TestUtil.createByteArray(0x08), // Laces.\n                Arrays.copyOfRange(firstPacket, 510, 510 + 8)),\n            true);\n\n    assertReadPacket(input, firstPacket);\n    assertThat((oggPacket.getPageHeader().type & 0x04) == 0x04).isTrue();\n    assertThat((oggPacket.getPageHeader().type & 0x02) == 0x02).isFalse();\n    assertThat(oggPacket.getPageHeader().pageSequenceNumber).isEqualTo(1001);\n\n    assertReadEof(input);\n  }\n\n  @Test\n  public void testReadContinuedPacketOverFourPages() throws Exception {\n    byte[] firstPacket = TestUtil.buildTestData(1028);\n\n    FakeExtractorInput input =\n        OggTestData.createInput(\n            TestUtil.joinByteArrays(\n                // First page.\n                OggTestData.buildOggHeader(0x02, 0, 1000, 0x02),\n                TestUtil.createByteArray(0xFF, 0xFF), // Laces.\n                Arrays.copyOf(firstPacket, 510),\n                // Second page (continued packet).\n                OggTestData.buildOggHeader(0x01, 10, 1001, 0x01),\n                TestUtil.createByteArray(0xFF), // Laces.\n                Arrays.copyOfRange(firstPacket, 510, 510 + 255),\n                // Third page (continued packet).\n                OggTestData.buildOggHeader(0x01, 10, 1002, 0x01),\n                TestUtil.createByteArray(0xFF), // Laces.\n                Arrays.copyOfRange(firstPacket, 510 + 255, 510 + 255 + 255),\n                // Fourth page (continued packet).\n                OggTestData.buildOggHeader(0x05, 10, 1003, 0x01),\n                TestUtil.createByteArray(0x08), // Laces.\n                Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)),\n            true);\n\n    assertReadPacket(input, firstPacket);\n    assertThat((oggPacket.getPageHeader().type & 0x04) == 0x04).isTrue();\n    assertThat((oggPacket.getPageHeader().type & 0x02) == 0x02).isFalse();\n    assertThat(oggPacket.getPageHeader().pageSequenceNumber).isEqualTo(1003);\n\n    assertReadEof(input);\n  }\n\n  @Test\n  public void testReadDiscardContinuedPacketAtStart() throws Exception {\n    byte[] pageBody = TestUtil.buildTestData(256 + 8);\n\n    FakeExtractorInput input =\n        OggTestData.createInput(\n            TestUtil.joinByteArrays(\n                // Page with a continued packet at start.\n                OggTestData.buildOggHeader(0x01, 10, 1001, 0x03),\n                TestUtil.createByteArray(255, 1, 8), // Laces.\n                pageBody),\n            true);\n\n    // Expect the first partial packet to be discarded.\n    assertReadPacket(input, Arrays.copyOfRange(pageBody, 256, 256 + 8));\n    assertReadEof(input);\n  }\n\n  @Test\n  public void testReadZeroSizedPacketsAtEndOfStream() throws Exception {\n    byte[] firstPacket = TestUtil.buildTestData(8, random);\n    byte[] secondPacket = TestUtil.buildTestData(8, random);\n    byte[] thirdPacket = TestUtil.buildTestData(8, random);\n\n    FakeExtractorInput input =\n        OggTestData.createInput(\n            TestUtil.joinByteArrays(\n                OggTestData.buildOggHeader(0x02, 0, 1000, 0x01),\n                TestUtil.createByteArray(0x08), // Laces.\n                firstPacket,\n                OggTestData.buildOggHeader(0x04, 0, 1001, 0x03),\n                TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces.\n                secondPacket,\n                OggTestData.buildOggHeader(0x04, 0, 1002, 0x03),\n                TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces.\n                thirdPacket),\n            true);\n\n    assertReadPacket(input, firstPacket);\n    assertReadPacket(input, secondPacket);\n    assertReadPacket(input, thirdPacket);\n    assertReadEof(input);\n  }\n\n  @Test\n  public void testParseRealFile() throws IOException, InterruptedException {\n    byte[] data = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TEST_FILE);\n    FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();\n    int packetCounter = 0;\n    while (readPacket(input)) {\n      packetCounter++;\n    }\n    assertThat(packetCounter).isEqualTo(277);\n  }\n\n  private void assertReadPacket(FakeExtractorInput extractorInput, byte[] expected)\n      throws IOException, InterruptedException {\n    assertThat(readPacket(extractorInput)).isTrue();\n    ParsableByteArray payload = oggPacket.getPayload();\n    assertThat(Arrays.copyOf(payload.data, payload.limit())).isEqualTo(expected);\n  }\n\n  private void assertReadEof(FakeExtractorInput extractorInput)\n      throws IOException, InterruptedException {\n    assertThat(readPacket(extractorInput)).isFalse();\n  }\n\n  private boolean readPacket(FakeExtractorInput input) throws InterruptedException, IOException {\n    while (true) {\n      try {\n        return oggPacket.populate(input);\n      } catch (FakeExtractorInput.SimulatedIOException e) {\n        // Ignore.\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException;\nimport com.google.android.exoplayer2.testutil.OggTestData;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.io.IOException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link OggPageHeader}. */\n@RunWith(AndroidJUnit4.class)\npublic final class OggPageHeaderTest {\n\n  @Test\n  public void testPopulatePageHeader() throws IOException, InterruptedException {\n    FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays(\n        OggTestData.buildOggHeader(0x01, 123456, 4, 2),\n        TestUtil.createByteArray(2, 2)\n    ), true);\n    OggPageHeader header = new OggPageHeader();\n    populatePageHeader(input, header, false);\n\n    assertThat(header.type).isEqualTo(0x01);\n    assertThat(header.headerSize).isEqualTo(27 + 2);\n    assertThat(header.bodySize).isEqualTo(4);\n    assertThat(header.pageSegmentCount).isEqualTo(2);\n    assertThat(header.granulePosition).isEqualTo(123456);\n    assertThat(header.pageSequenceNumber).isEqualTo(4);\n    assertThat(header.streamSerialNumber).isEqualTo(0x1000);\n    assertThat(header.pageChecksum).isEqualTo(0x100000);\n    assertThat(header.revision).isEqualTo(0);\n  }\n\n  @Test\n  public void testPopulatePageHeaderQuiteOnExceptionLessThan27Bytes()\n      throws IOException, InterruptedException {\n    FakeExtractorInput input = OggTestData.createInput(TestUtil.createByteArray(2, 2), false);\n    OggPageHeader header = new OggPageHeader();\n    assertThat(populatePageHeader(input, header, true)).isFalse();\n  }\n\n  @Test\n  public void testPopulatePageHeaderQuiteOnExceptionNotOgg()\n      throws IOException, InterruptedException {\n    byte[] headerBytes = TestUtil.joinByteArrays(\n        OggTestData.buildOggHeader(0x01, 123456, 4, 2),\n        TestUtil.createByteArray(2, 2)\n    );\n    // change from 'O' to 'o'\n    headerBytes[0] = 'o';\n    FakeExtractorInput input = OggTestData.createInput(headerBytes, false);\n    OggPageHeader header = new OggPageHeader();\n    assertThat(populatePageHeader(input, header, true)).isFalse();\n  }\n\n  @Test\n  public void testPopulatePageHeaderQuiteOnExceptionWrongRevision()\n      throws IOException, InterruptedException {\n    byte[] headerBytes = TestUtil.joinByteArrays(\n        OggTestData.buildOggHeader(0x01, 123456, 4, 2),\n        TestUtil.createByteArray(2, 2)\n    );\n    // change revision from 0 to 1\n    headerBytes[4] = 0x01;\n    FakeExtractorInput input = OggTestData.createInput(headerBytes, false);\n    OggPageHeader header = new OggPageHeader();\n    assertThat(populatePageHeader(input, header, true)).isFalse();\n  }\n\n  private boolean populatePageHeader(FakeExtractorInput input, OggPageHeader header,\n      boolean quite) throws IOException, InterruptedException {\n    while (true) {\n      try {\n        return header.populate(input, quite);\n      } catch (SimulatedIOException e) {\n        // ignored\n      }\n    }\n  }\n\n}\n\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static org.junit.Assert.fail;\n\nimport com.google.android.exoplayer2.testutil.OggTestData;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.util.ArrayList;\nimport java.util.Random;\n\n/** Generates test data. */\n/* package */ final class OggTestFile {\n\n  private static final int MAX_PACKET_LENGTH = 2048;\n  private static final int MAX_SEGMENT_COUNT = 10;\n  private static final int MAX_GRANULES_IN_PAGE = 100000;\n\n  public final byte[] data;\n  public final int granuleCount;\n  public final int pageCount;\n  public final int firstPayloadPageSize;\n  public final int firstPayloadPageGranuleCount;\n  public final int lastPayloadPageSize;\n  public final int lastPayloadPageGranuleCount;\n\n  private OggTestFile(\n      byte[] data,\n      int granuleCount,\n      int pageCount,\n      int firstPayloadPageSize,\n      int firstPayloadPageGranuleCount,\n      int lastPayloadPageSize,\n      int lastPayloadPageGranuleCount) {\n    this.data = data;\n    this.granuleCount = granuleCount;\n    this.pageCount = pageCount;\n    this.firstPayloadPageSize = firstPayloadPageSize;\n    this.firstPayloadPageGranuleCount = firstPayloadPageGranuleCount;\n    this.lastPayloadPageSize = lastPayloadPageSize;\n    this.lastPayloadPageGranuleCount = lastPayloadPageGranuleCount;\n  }\n\n  public static OggTestFile generate(Random random, int pageCount) {\n    ArrayList<byte[]> fileData = new ArrayList<>();\n    int fileSize = 0;\n    int granuleCount = 0;\n    int firstPayloadPageSize = 0;\n    int firstPayloadPageGranuleCount = 0;\n    int lastPageloadPageSize = 0;\n    int lastPayloadPageGranuleCount = 0;\n    int packetLength = -1;\n\n    for (int i = 0; i < pageCount; i++) {\n      int headerType = 0x00;\n      if (packetLength >= 0) {\n        headerType |= 1;\n      }\n      if (i == 0) {\n        headerType |= 2;\n      }\n      if (i == pageCount - 1) {\n        headerType |= 4;\n      }\n      int pageGranuleCount = random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1;\n      int pageSegmentCount = random.nextInt(MAX_SEGMENT_COUNT);\n      granuleCount += pageGranuleCount;\n      byte[] header = OggTestData.buildOggHeader(headerType, granuleCount, 0, pageSegmentCount);\n      fileData.add(header);\n      int pageSize = header.length;\n\n      byte[] laces = new byte[pageSegmentCount];\n      int bodySize = 0;\n      for (int j = 0; j < pageSegmentCount; j++) {\n        if (packetLength < 0) {\n          if (i < pageCount - 1) {\n            packetLength = random.nextInt(MAX_PACKET_LENGTH);\n          } else {\n            int maxPacketLength = 255 * (pageSegmentCount - j) - 1;\n            packetLength = random.nextInt(maxPacketLength);\n          }\n        } else if (i == pageCount - 1 && j == pageSegmentCount - 1) {\n          packetLength = Math.min(packetLength, 254);\n        }\n        laces[j] = (byte) Math.min(packetLength, 255);\n        bodySize += laces[j] & 0xFF;\n        packetLength -= 255;\n      }\n      fileData.add(laces);\n      pageSize += laces.length;\n\n      byte[] payload = TestUtil.buildTestData(bodySize, random);\n      fileData.add(payload);\n      pageSize += payload.length;\n\n      fileSize += pageSize;\n      if (i == 0) {\n        firstPayloadPageSize = pageSize;\n        firstPayloadPageGranuleCount = pageGranuleCount;\n      } else if (i == pageCount - 1) {\n        lastPageloadPageSize = pageSize;\n        lastPayloadPageGranuleCount = pageGranuleCount;\n      }\n    }\n\n    byte[] file = new byte[fileSize];\n    int position = 0;\n    for (byte[] data : fileData) {\n      System.arraycopy(data, 0, file, position, data.length);\n      position += data.length;\n    }\n    return new OggTestFile(\n        file,\n        granuleCount,\n        pageCount,\n        firstPayloadPageSize,\n        firstPayloadPageGranuleCount,\n        lastPageloadPageSize,\n        lastPayloadPageGranuleCount);\n  }\n\n  public int findPreviousPageStart(long position) {\n    for (int i = (int) (position - 4); i >= 0; i--) {\n      if (data[i] == 'O' && data[i + 1] == 'g' && data[i + 2] == 'g' && data[i + 3] == 'S') {\n        return i;\n      }\n    }\n    fail();\n    return -1;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link VorbisBitArray}. */\n@RunWith(AndroidJUnit4.class)\npublic final class VorbisBitArrayTest {\n\n  @Test\n  public void testReadBit() {\n    VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x5c, 0x50));\n    assertThat(bitArray.readBit()).isFalse();\n    assertThat(bitArray.readBit()).isFalse();\n    assertThat(bitArray.readBit()).isTrue();\n    assertThat(bitArray.readBit()).isTrue();\n    assertThat(bitArray.readBit()).isTrue();\n    assertThat(bitArray.readBit()).isFalse();\n    assertThat(bitArray.readBit()).isTrue();\n    assertThat(bitArray.readBit()).isFalse();\n    assertThat(bitArray.readBit()).isFalse();\n    assertThat(bitArray.readBit()).isFalse();\n    assertThat(bitArray.readBit()).isFalse();\n    assertThat(bitArray.readBit()).isFalse();\n    assertThat(bitArray.readBit()).isTrue();\n    assertThat(bitArray.readBit()).isFalse();\n    assertThat(bitArray.readBit()).isTrue();\n    assertThat(bitArray.readBit()).isFalse();\n  }\n\n  @Test\n  public void testSkipBits() {\n    VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F));\n    bitArray.skipBits(10);\n    assertThat(bitArray.getPosition()).isEqualTo(10);\n    assertThat(bitArray.readBit()).isTrue();\n    assertThat(bitArray.readBit()).isTrue();\n    assertThat(bitArray.readBit()).isFalse();\n    bitArray.skipBits(1);\n    assertThat(bitArray.getPosition()).isEqualTo(14);\n    assertThat(bitArray.readBit()).isFalse();\n    assertThat(bitArray.readBit()).isFalse();\n  }\n\n  @Test\n  public void testGetPosition() throws Exception {\n    VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F));\n    assertThat(bitArray.getPosition()).isEqualTo(0);\n    bitArray.readBit();\n    assertThat(bitArray.getPosition()).isEqualTo(1);\n    bitArray.readBit();\n    bitArray.readBit();\n    bitArray.skipBits(4);\n    assertThat(bitArray.getPosition()).isEqualTo(7);\n  }\n\n  @Test\n  public void testSetPosition() throws Exception {\n    VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F));\n    assertThat(bitArray.getPosition()).isEqualTo(0);\n    bitArray.setPosition(4);\n    assertThat(bitArray.getPosition()).isEqualTo(4);\n    bitArray.setPosition(15);\n    assertThat(bitArray.readBit()).isFalse();\n  }\n\n  @Test\n  public void testReadInt32() {\n    VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F, 0xF0, 0x0F));\n    assertThat(bitArray.readBits(32)).isEqualTo(0x0FF00FF0);\n    bitArray = new VorbisBitArray(TestUtil.createByteArray(0x0F, 0xF0, 0x0F, 0xF0));\n    assertThat(bitArray.readBits(32)).isEqualTo(0xF00FF00F);\n  }\n\n  @Test\n  public void testReadBits() throws Exception {\n    VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x03, 0x22));\n    assertThat(bitArray.readBits(2)).isEqualTo(3);\n    bitArray.skipBits(6);\n    assertThat(bitArray.readBits(2)).isEqualTo(2);\n    bitArray.skipBits(2);\n    assertThat(bitArray.readBits(2)).isEqualTo(2);\n    bitArray.reset();\n    assertThat(bitArray.readBits(16)).isEqualTo(0x2203);\n  }\n\n  @Test\n  public void testRead4BitsBeyondBoundary() throws Exception {\n    VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x2e, 0x10));\n    assertThat(bitArray.readBits(7)).isEqualTo(0x2e);\n    assertThat(bitArray.getPosition()).isEqualTo(7);\n    assertThat(bitArray.readBits(4)).isEqualTo(0x0);\n  }\n\n  @Test\n  public void testReadBitsBeyondByteBoundaries() throws Exception {\n    VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xFF, 0x0F, 0xFF, 0x0F));\n    assertThat(bitArray.readBits(32)).isEqualTo(0x0FFF0FFF);\n\n    bitArray.reset();\n    bitArray.skipBits(4);\n    assertThat(bitArray.readBits(16)).isEqualTo(0xF0FF);\n\n    bitArray.reset();\n    bitArray.skipBits(6);\n    assertThat(bitArray.readBits(12)).isEqualTo(0xc3F);\n\n    bitArray.reset();\n    bitArray.skipBits(6);\n    assertThat(bitArray.readBit()).isTrue();\n    assertThat(bitArray.readBit()).isTrue();\n    assertThat(bitArray.bitsLeft()).isEqualTo(24);\n\n    bitArray.reset();\n    bitArray.skipBits(10);\n    assertThat(bitArray.readBits(5)).isEqualTo(3);\n    assertThat(bitArray.getPosition()).isEqualTo(15);\n  }\n\n  @Test\n  public void testReadBitsIllegalLengths() throws Exception {\n    VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x03, 0x22, 0x30));\n\n    // reading zero bits gets 0 without advancing position\n    // (like a zero-bit read is defined to yield zer0)\n    assertThat(bitArray.readBits(0)).isEqualTo(0);\n    assertThat(bitArray.getPosition()).isEqualTo(0);\n    bitArray.readBit();\n    assertThat(bitArray.getPosition()).isEqualTo(1);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.android.exoplayer2.extractor.ogg.VorbisReader.readBits;\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.ogg.VorbisReader.VorbisSetup;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException;\nimport com.google.android.exoplayer2.testutil.OggTestData;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.io.IOException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link VorbisReader}. */\n@RunWith(AndroidJUnit4.class)\npublic final class VorbisReaderTest {\n\n  @Test\n  public void testReadBits() throws Exception {\n    assertThat(readBits((byte) 0x00, 2, 2)).isEqualTo(0);\n    assertThat(readBits((byte) 0x02, 1, 1)).isEqualTo(1);\n    assertThat(readBits((byte) 0xF0, 4, 4)).isEqualTo(15);\n    assertThat(readBits((byte) 0x80, 1, 7)).isEqualTo(1);\n  }\n\n  @Test\n  public void testAppendNumberOfSamples() throws Exception {\n    ParsableByteArray buffer = new ParsableByteArray(4);\n    buffer.setLimit(0);\n    VorbisReader.appendNumberOfSamples(buffer, 0x01234567);\n    assertThat(buffer.limit()).isEqualTo(4);\n    assertThat(buffer.data[0]).isEqualTo(0x67);\n    assertThat(buffer.data[1]).isEqualTo(0x45);\n    assertThat(buffer.data[2]).isEqualTo(0x23);\n    assertThat(buffer.data[3]).isEqualTo(0x01);\n  }\n\n  @Test\n  public void testReadSetupHeadersWithIOExceptions() throws IOException, InterruptedException {\n    byte[] data = OggTestData.getVorbisHeaderPages();\n    ExtractorInput input = new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true)\n        .setSimulateUnknownLength(true).setSimulatePartialReads(true).build();\n\n    VorbisReader reader = new VorbisReader();\n    VorbisReader.VorbisSetup vorbisSetup = readSetupHeaders(reader, input);\n\n    assertThat(vorbisSetup.idHeader).isNotNull();\n    assertThat(vorbisSetup.commentHeader).isNotNull();\n    assertThat(vorbisSetup.setupHeaderData).isNotNull();\n    assertThat(vorbisSetup.modes).isNotNull();\n\n    assertThat(vorbisSetup.commentHeader.length).isEqualTo(45);\n    assertThat(vorbisSetup.idHeader.data).hasLength(30);\n    assertThat(vorbisSetup.setupHeaderData).hasLength(3597);\n\n    assertThat(vorbisSetup.idHeader.bitrateMax).isEqualTo(-1);\n    assertThat(vorbisSetup.idHeader.bitrateMin).isEqualTo(-1);\n    assertThat(vorbisSetup.idHeader.bitrateNominal).isEqualTo(66666);\n    assertThat(vorbisSetup.idHeader.blockSize0).isEqualTo(512);\n    assertThat(vorbisSetup.idHeader.blockSize1).isEqualTo(1024);\n    assertThat(vorbisSetup.idHeader.channels).isEqualTo(2);\n    assertThat(vorbisSetup.idHeader.framingFlag).isTrue();\n    assertThat(vorbisSetup.idHeader.sampleRate).isEqualTo(22050);\n    assertThat(vorbisSetup.idHeader.version).isEqualTo(0);\n\n    assertThat(vorbisSetup.commentHeader.vendor).isEqualTo(\"Xiph.Org libVorbis I 20030909\");\n    assertThat(vorbisSetup.iLogModes).isEqualTo(1);\n\n    assertThat(vorbisSetup.setupHeaderData[vorbisSetup.setupHeaderData.length - 1])\n        .isEqualTo(data[data.length - 1]);\n\n    assertThat(vorbisSetup.modes[0].blockFlag).isFalse();\n    assertThat(vorbisSetup.modes[1].blockFlag).isTrue();\n  }\n\n  private static VorbisSetup readSetupHeaders(VorbisReader reader, ExtractorInput input)\n      throws IOException, InterruptedException {\n    OggPacket oggPacket = new OggPacket();\n    while (true) {\n      try {\n        if (!oggPacket.populate(input)) {\n          fail();\n        }\n        VorbisSetup vorbisSetup = reader.readSetupHeaders(oggPacket.getPayload());\n        if (vorbisSetup != null) {\n          return vorbisSetup;\n        }\n      } catch (SimulatedIOException e) {\n        // Ignore.\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.android.exoplayer2.extractor.ogg.VorbisUtil.iLog;\nimport static com.google.android.exoplayer2.extractor.ogg.VorbisUtil.verifyVorbisHeaderCapturePattern;\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.testutil.OggTestData;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link VorbisUtil}. */\n@RunWith(AndroidJUnit4.class)\npublic final class VorbisUtilTest {\n\n  @Test\n  public void testILog() throws Exception {\n    assertThat(iLog(0)).isEqualTo(0);\n    assertThat(iLog(1)).isEqualTo(1);\n    assertThat(iLog(2)).isEqualTo(2);\n    assertThat(iLog(3)).isEqualTo(2);\n    assertThat(iLog(4)).isEqualTo(3);\n    assertThat(iLog(5)).isEqualTo(3);\n    assertThat(iLog(8)).isEqualTo(4);\n    assertThat(iLog(-1)).isEqualTo(0);\n    assertThat(iLog(-122)).isEqualTo(0);\n  }\n\n  @Test\n  public void testReadIdHeader() throws Exception {\n    byte[] data = OggTestData.getIdentificationHeaderData();\n    ParsableByteArray headerData = new ParsableByteArray(data, data.length);\n    VorbisUtil.VorbisIdHeader vorbisIdHeader =\n        VorbisUtil.readVorbisIdentificationHeader(headerData);\n\n    assertThat(vorbisIdHeader.sampleRate).isEqualTo(22050);\n    assertThat(vorbisIdHeader.version).isEqualTo(0);\n    assertThat(vorbisIdHeader.framingFlag).isTrue();\n    assertThat(vorbisIdHeader.channels).isEqualTo(2);\n    assertThat(vorbisIdHeader.blockSize0).isEqualTo(512);\n    assertThat(vorbisIdHeader.blockSize1).isEqualTo(1024);\n    assertThat(vorbisIdHeader.bitrateMax).isEqualTo(-1);\n    assertThat(vorbisIdHeader.bitrateMin).isEqualTo(-1);\n    assertThat(vorbisIdHeader.bitrateNominal).isEqualTo(66666);\n    assertThat(vorbisIdHeader.getApproximateBitrate()).isEqualTo(66666);\n  }\n\n  @Test\n  public void testReadCommentHeader() throws ParserException {\n    byte[] data = OggTestData.getCommentHeaderDataUTF8();\n    ParsableByteArray headerData = new ParsableByteArray(data, data.length);\n    VorbisUtil.CommentHeader commentHeader = VorbisUtil.readVorbisCommentHeader(headerData);\n\n    assertThat(commentHeader.vendor).isEqualTo(\"Xiph.Org libVorbis I 20120203 (Omnipresent)\");\n    assertThat(commentHeader.comments).hasLength(3);\n    assertThat(commentHeader.comments[0]).isEqualTo(\"ALBUM=äö\");\n    assertThat(commentHeader.comments[1]).isEqualTo(\"TITLE=A sample song\");\n    assertThat(commentHeader.comments[2]).isEqualTo(\"ARTIST=Google\");\n  }\n\n  @Test\n  public void testReadVorbisModes() throws ParserException {\n    byte[] data = OggTestData.getSetupHeaderData();\n    ParsableByteArray headerData = new ParsableByteArray(data, data.length);\n    VorbisUtil.Mode[] modes = VorbisUtil.readVorbisModes(headerData, 2);\n\n    assertThat(modes).hasLength(2);\n    assertThat(modes[0].blockFlag).isFalse();\n    assertThat(modes[0].mapping).isEqualTo(0);\n    assertThat(modes[0].transformType).isEqualTo(0);\n    assertThat(modes[0].windowType).isEqualTo(0);\n    assertThat(modes[1].blockFlag).isTrue();\n    assertThat(modes[1].mapping).isEqualTo(1);\n    assertThat(modes[1].transformType).isEqualTo(0);\n    assertThat(modes[1].windowType).isEqualTo(0);\n  }\n\n  @Test\n  public void testVerifyVorbisHeaderCapturePattern() throws ParserException {\n    ParsableByteArray header = new ParsableByteArray(\n        new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'});\n    assertThat(verifyVorbisHeaderCapturePattern(0x01, header, false)).isTrue();\n  }\n\n  @Test\n  public void testVerifyVorbisHeaderCapturePatternInvalidHeader() {\n    ParsableByteArray header = new ParsableByteArray(\n        new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'});\n    try {\n      VorbisUtil.verifyVorbisHeaderCapturePattern(0x99, header, false);\n      fail();\n    } catch (ParserException e) {\n      assertThat(e.getMessage()).isEqualTo(\"expected header type 99\");\n    }\n  }\n\n  @Test\n  public void testVerifyVorbisHeaderCapturePatternInvalidHeaderQuite() throws ParserException {\n    ParsableByteArray header = new ParsableByteArray(\n        new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'});\n    assertThat(verifyVorbisHeaderCapturePattern(0x99, header, true)).isFalse();\n  }\n\n  @Test\n  public void testVerifyVorbisHeaderCapturePatternInvalidPattern() {\n    ParsableByteArray header = new ParsableByteArray(\n        new byte[] {0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'});\n    try {\n      VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, false);\n      fail();\n    } catch (ParserException e) {\n      assertThat(e.getMessage()).isEqualTo(\"expected characters 'vorbis'\");\n    }\n  }\n\n  @Test\n  public void testVerifyVorbisHeaderCapturePatternQuiteInvalidPatternQuite()\n      throws ParserException {\n    ParsableByteArray header = new ParsableByteArray(\n        new byte[] {0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'});\n    assertThat(verifyVorbisHeaderCapturePattern(0x01, header, true)).isFalse();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link RawCcExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class RawCcExtractorTest {\n\n  @Test\n  public void testRawCcSample() throws Exception {\n    ExtractorAsserts.assertBehavior(\n        () ->\n            new RawCcExtractor(\n                Format.createTextContainerFormat(\n                    /* id= */ null,\n                    /* label= */ null,\n                    /* containerMimeType= */ null,\n                    /* sampleMimeType= */ MimeTypes.APPLICATION_CEA608,\n                    /* codecs= */ \"cea608\",\n                    /* bitrate= */ Format.NO_VALUE,\n                    /* selectionFlags= */ 0,\n                    /* roleFlags= */ 0,\n                    /* language= */ null,\n                    /* accessibilityChannel= */ 1)),\n        \"rawcc/sample.rawcc\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link Ac3Extractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class Ac3ExtractorTest {\n\n  @Test\n  public void testAc3Sample() throws Exception {\n    ExtractorAsserts.assertBehavior(Ac3Extractor::new, \"ts/sample.ac3\");\n  }\n\n  @Test\n  public void testEAc3Sample() throws Exception {\n    ExtractorAsserts.assertBehavior(Ac3Extractor::new, \"ts/sample.eac3\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.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.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link Ac4Extractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class Ac4ExtractorTest {\n\n  @Test\n  public void testAc4Sample() throws Exception {\n    ExtractorAsserts.assertBehavior(Ac4Extractor::new, \"ts/sample.ac4\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.testutil.FakeExtractorOutput;\nimport com.google.android.exoplayer2.testutil.FakeTrackOutput;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.upstream.DefaultDataSource;\nimport com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.Random;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link AdtsExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class AdtsExtractorSeekTest {\n\n  private static final Random random = new Random(1234L);\n\n  private static final String TEST_FILE = \"ts/sample.adts\";\n  private static final int FILE_DURATION_US = 3_356_772;\n  private static final long DELTA_TIMESTAMP_THRESHOLD_US = 200_000;\n\n  private FakeTrackOutput expectedTrackOutput;\n  private DefaultDataSource dataSource;\n\n  @Before\n  public void setUp() {\n    dataSource =\n        new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), \"UserAgent\")\n            .createDataSource();\n  }\n\n  @Test\n  public void testAdtsExtractorReads_returnSeekableSeekMap()\n      throws IOException, InterruptedException {\n    String fileName = TEST_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAdtsExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n\n    AdtsExtractor extractor = createAdtsExtractor();\n    SeekMap seekMap =\n        TestUtil.extractSeekMap(extractor, new FakeExtractorOutput(), dataSource, fileUri);\n\n    assertThat(seekMap).isNotNull();\n    assertThat(seekMap.getDurationUs()).isEqualTo(FILE_DURATION_US);\n    assertThat(seekMap.isSeekable()).isTrue();\n  }\n\n  @Test\n  public void testSeeking_handlesSeekingToPositionInFile_extractsCorrectSample()\n      throws IOException, InterruptedException {\n    String fileName = TEST_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAdtsExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n\n    AdtsExtractor extractor = createAdtsExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long targetSeekTimeUs = 980_000;\n    int extractedSampleIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedSampleIndex).isNotEqualTo(-1);\n    assertFirstSampleAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedSampleIndex);\n  }\n\n  @Test\n  public void testSeeking_handlesSeekToEoF_extractsLastSample()\n      throws IOException, InterruptedException {\n    String fileName = TEST_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAdtsExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n    AdtsExtractor extractor = createAdtsExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long targetSeekTimeUs = seekMap.getDurationUs();\n\n    int extractedSampleIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedSampleIndex).isNotEqualTo(-1);\n    assertFirstSampleAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedSampleIndex);\n  }\n\n  @Test\n  public void testSeeking_handlesSeekingBackward_extractsCorrectSamples()\n      throws IOException, InterruptedException {\n    String fileName = TEST_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAdtsExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n    AdtsExtractor extractor = createAdtsExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long firstSeekTimeUs = 980_000;\n    TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    long targetSeekTimeUs = 0;\n    int extractedSampleIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedSampleIndex).isNotEqualTo(-1);\n    assertFirstSampleAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedSampleIndex);\n  }\n\n  @Test\n  public void testSeeking_handlesSeekingForward_extractsCorrectSamples()\n      throws IOException, InterruptedException {\n    String fileName = TEST_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAdtsExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n    AdtsExtractor extractor = createAdtsExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long firstSeekTimeUs = 980_000;\n    TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    long targetSeekTimeUs = 1_200_000;\n    int extractedSampleIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedSampleIndex).isNotEqualTo(-1);\n    assertFirstSampleAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedSampleIndex);\n  }\n\n  @Test\n  public void testSeeking_handlesRandomSeeks_extractsCorrectSamples()\n      throws IOException, InterruptedException {\n    String fileName = TEST_FILE;\n    Uri fileUri = TestUtil.buildAssetUri(fileName);\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                createAdtsExtractor(), ApplicationProvider.getApplicationContext(), fileName)\n            .trackOutputs\n            .get(0);\n    AdtsExtractor extractor = createAdtsExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);\n\n    long numSeek = 100;\n    for (long i = 0; i < numSeek; i++) {\n      long targetSeekTimeUs = random.nextInt(FILE_DURATION_US + 1);\n      int extractedSampleIndex =\n          TestUtil.seekToTimeUs(\n              extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n      assertThat(extractedSampleIndex).isNotEqualTo(-1);\n      assertFirstSampleAfterSeekContainTargetSeekTime(\n          trackOutput, targetSeekTimeUs, extractedSampleIndex);\n    }\n  }\n\n  // Internal methods\n\n  private static AdtsExtractor createAdtsExtractor() {\n    return new AdtsExtractor(\n        /* firstStreamSampleTimestampUs= */ 0,\n        /* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);\n  }\n\n  private void assertFirstSampleAfterSeekContainTargetSeekTime(\n      FakeTrackOutput trackOutput, long seekTimeUs, int firstSampleIndexAfterSeek) {\n    long outputSampleTimeUs = trackOutput.getSampleTimeUs(firstSampleIndexAfterSeek);\n    int expectedSampleIndex =\n        findOutputSampleInExpectedOutput(trackOutput.getSampleData(firstSampleIndexAfterSeek));\n    // Assert that after seeking, the first sample written to output exists in the sample list\n    assertThat(expectedSampleIndex).isNotEqualTo(-1);\n    // Assert that the timestamp output for first sample after seek is near the seek point.\n    // For ADTS seeking, unfortunately we can't guarantee exact sample seeking, since most ADTS\n    // stream use VBR.\n    assertThat(Math.abs(outputSampleTimeUs - seekTimeUs)).isLessThan(DELTA_TIMESTAMP_THRESHOLD_US);\n    assertThat(\n            Math.abs(outputSampleTimeUs - expectedTrackOutput.getSampleTimeUs(expectedSampleIndex)))\n        .isLessThan(DELTA_TIMESTAMP_THRESHOLD_US);\n    trackOutput.assertSample(\n        firstSampleIndexAfterSeek,\n        expectedTrackOutput.getSampleData(expectedSampleIndex),\n        outputSampleTimeUs,\n        expectedTrackOutput.getSampleFlags(expectedSampleIndex),\n        expectedTrackOutput.getSampleCryptoData(expectedSampleIndex));\n  }\n\n  private int findOutputSampleInExpectedOutput(byte[] sampleData) {\n    for (int i = 0; i < expectedTrackOutput.getSampleCount(); i++) {\n      byte[] currentSampleData = expectedTrackOutput.getSampleData(i);\n      if (Arrays.equals(currentSampleData, sampleData)) {\n        return i;\n      }\n    }\n    return -1;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link AdtsExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class AdtsExtractorTest {\n\n  @Test\n  public void testSample() throws Exception {\n    ExtractorAsserts.assertBehavior(AdtsExtractor::new, \"ts/sample.adts\");\n  }\n\n  @Test\n  public void testSample_withSeeking() throws Exception {\n    ExtractorAsserts.assertBehavior(\n        () ->\n            new AdtsExtractor(\n                /* firstStreamSampleTimestampUs= */ 0,\n                /* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING),\n        \"ts/sample_cbs.adts\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.testutil.FakeExtractorOutput;\nimport com.google.android.exoplayer2.testutil.FakeTrackOutput;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport java.util.Arrays;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link AdtsReader}. */\n@RunWith(AndroidJUnit4.class)\npublic class AdtsReaderTest {\n\n  public static final byte[] ID3_DATA_1 =\n      TestUtil.createByteArray(\n          0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3d, 0x54, 0x58, 0x58, 0x58, 0x00,\n          0x00, 0x00, 0x33, 0x00, 0x00, 0x03, 0x00, 0x20, 0x2a, 0x2a, 0x2a, 0x20, 0x54, 0x48, 0x49,\n          0x53, 0x20, 0x49, 0x53, 0x20, 0x54, 0x69, 0x6d, 0x65, 0x64, 0x20, 0x4d, 0x65, 0x74, 0x61,\n          0x44, 0x61, 0x74, 0x61, 0x20, 0x40, 0x20, 0x2d, 0x2d, 0x20, 0x30, 0x30, 0x3a, 0x30, 0x30,\n          0x3a, 0x30, 0x30, 0x2e, 0x30, 0x20, 0x2a, 0x2a, 0x2a, 0x20, 0x00);\n\n  public static final byte[] ID3_DATA_2 =\n      TestUtil.createByteArray(\n          0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x50, 0x52, 0x49, 0x56, 0x00,\n          0x00, 0x00, 0x35, 0x00, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e,\n          0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73,\n          0x70, 0x6f, 0x72, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x69, 0x6d, 0x65, 0x73,\n          0x74, 0x61, 0x6d, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0xbb, 0xa0);\n\n  public static final byte[] ADTS_HEADER =\n      TestUtil.createByteArray(0xff, 0xf1, 0x50, 0x80, 0x01, 0xdf, 0xfc);\n\n  public static final byte[] ADTS_CONTENT =\n      TestUtil.createByteArray(0x20, 0x00, 0x20, 0x00, 0x00, 0x80, 0x0e);\n\n  private static final byte[] TEST_DATA =\n      TestUtil.joinByteArrays(ID3_DATA_1, ID3_DATA_2, ADTS_HEADER, ADTS_CONTENT);\n\n  private static final long ADTS_SAMPLE_DURATION = 23219L;\n\n  private FakeTrackOutput adtsOutput;\n  private FakeTrackOutput id3Output;\n  private AdtsReader adtsReader;\n  private ParsableByteArray data;\n  private boolean firstFeed;\n\n  @Before\n  public void setUp() throws Exception {\n    FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput();\n    adtsOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_AUDIO);\n    id3Output = fakeExtractorOutput.track(1, C.TRACK_TYPE_METADATA);\n    adtsReader = new AdtsReader(true);\n    TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1);\n    adtsReader.createTracks(fakeExtractorOutput, idGenerator);\n    data = new ParsableByteArray(TEST_DATA);\n    firstFeed = true;\n  }\n\n  @Test\n  public void testSkipToNextSample() throws Exception {\n    for (int i = 1; i <= ID3_DATA_1.length + ID3_DATA_2.length; i++) {\n      data.setPosition(i);\n      feed();\n      // Once the data position set to ID3_DATA_1.length, no more id3 samples are read\n      int id3SampleCount = Math.min(i, ID3_DATA_1.length);\n      assertSampleCounts(id3SampleCount, i);\n    }\n  }\n\n  @Test\n  public void testSkipToNextSampleResetsState() throws Exception {\n    data =\n        new ParsableByteArray(\n            TestUtil.joinByteArrays(\n                ADTS_HEADER,\n                ADTS_CONTENT,\n                ADTS_HEADER,\n                ADTS_CONTENT,\n                // Adts sample missing the first sync byte\n                // The Reader should be able to read the next sample.\n                Arrays.copyOfRange(ADTS_HEADER, 1, ADTS_HEADER.length),\n                ADTS_CONTENT,\n                ADTS_HEADER,\n                ADTS_CONTENT));\n    feed();\n    assertSampleCounts(0, 3);\n    for (int i = 0; i < 3; i++) {\n      adtsOutput.assertSample(\n          /* index= */ i,\n          /* data= */ ADTS_CONTENT,\n          /* timeUs= */ ADTS_SAMPLE_DURATION * i,\n          /* flags= */ C.BUFFER_FLAG_KEY_FRAME,\n          /* cryptoData= */ null);\n    }\n  }\n\n  @Test\n  public void testNoData() throws Exception {\n    feedLimited(0);\n    assertSampleCounts(0, 0);\n  }\n\n  @Test\n  public void testNotEnoughDataForIdentifier() throws Exception {\n    feedLimited(3 - 1);\n    assertSampleCounts(0, 0);\n  }\n\n  @Test\n  public void testNotEnoughDataForHeader() throws Exception {\n    feedLimited(10 - 1);\n    assertSampleCounts(0, 0);\n  }\n\n  @Test\n  public void testNotEnoughDataForWholeId3Packet() throws Exception {\n    feedLimited(ID3_DATA_1.length - 1);\n    assertSampleCounts(0, 0);\n  }\n\n  @Test\n  public void testConsumeWholeId3Packet() throws Exception {\n    feedLimited(ID3_DATA_1.length);\n    assertSampleCounts(1, 0);\n    id3Output.assertSample(0, ID3_DATA_1, 0, C.BUFFER_FLAG_KEY_FRAME, null);\n  }\n\n  @Test\n  public void testMultiId3Packet() throws Exception {\n    feedLimited(ID3_DATA_1.length + ID3_DATA_2.length - 1);\n    assertSampleCounts(1, 0);\n    id3Output.assertSample(0, ID3_DATA_1, 0, C.BUFFER_FLAG_KEY_FRAME, null);\n  }\n\n  @Test\n  public void testMultiId3PacketConsumed() throws Exception {\n    feedLimited(ID3_DATA_1.length + ID3_DATA_2.length);\n    assertSampleCounts(2, 0);\n    id3Output.assertSample(0, ID3_DATA_1, 0, C.BUFFER_FLAG_KEY_FRAME, null);\n    id3Output.assertSample(1, ID3_DATA_2, 0, C.BUFFER_FLAG_KEY_FRAME, null);\n  }\n\n  @Test\n  public void testMultiPacketConsumed() throws Exception {\n    for (int i = 0; i < 10; i++) {\n      data.setPosition(0);\n      feed();\n\n      long timeUs = ADTS_SAMPLE_DURATION * i;\n      int j = i * 2;\n      assertSampleCounts(j + 2, i + 1);\n\n      id3Output.assertSample(j, ID3_DATA_1, timeUs, C.BUFFER_FLAG_KEY_FRAME, null);\n      id3Output.assertSample(j + 1, ID3_DATA_2, timeUs, C.BUFFER_FLAG_KEY_FRAME, null);\n      adtsOutput.assertSample(i, ADTS_CONTENT, timeUs, C.BUFFER_FLAG_KEY_FRAME, null);\n    }\n  }\n\n  @Test\n  public void testAdtsDataOnly() throws ParserException {\n    data.setPosition(ID3_DATA_1.length + ID3_DATA_2.length);\n    feed();\n    assertSampleCounts(0, 1);\n    adtsOutput.assertSample(0, ADTS_CONTENT, 0, C.BUFFER_FLAG_KEY_FRAME, null);\n  }\n\n  private void feedLimited(int limit) throws ParserException {\n    maybeStartPacket();\n    data.setLimit(limit);\n    feed();\n  }\n\n  private void feed() throws ParserException {\n    maybeStartPacket();\n    adtsReader.consume(data);\n  }\n\n  private void maybeStartPacket() {\n    if (firstFeed) {\n      adtsReader.packetStarted(0, FLAG_DATA_ALIGNMENT_INDICATOR);\n      firstFeed = false;\n    }\n  }\n\n  private void assertSampleCounts(int id3SampleCount, int adtsSampleCount) {\n    id3Output.assertSampleCount(id3SampleCount);\n    adtsOutput.assertSampleCount(adtsSampleCount);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsDurationReaderTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.io.IOException;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link PsDurationReader}. */\n@RunWith(AndroidJUnit4.class)\npublic final class PsDurationReaderTest {\n\n  private PsDurationReader tsDurationReader;\n  private PositionHolder seekPositionHolder;\n\n  @Before\n  public void setUp() {\n    tsDurationReader = new PsDurationReader();\n    seekPositionHolder = new PositionHolder();\n  }\n\n  @Test\n  public void testIsDurationReadPending_returnFalseByDefault() {\n    assertThat(tsDurationReader.isDurationReadFinished()).isFalse();\n  }\n\n  @Test\n  public void testReadDuration_returnsCorrectDuration() throws IOException, InterruptedException {\n    FakeExtractorInput input =\n        new FakeExtractorInput.Builder()\n            .setData(\n                TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), \"ts/sample.ps\"))\n            .build();\n\n    int result = Extractor.RESULT_CONTINUE;\n    while (!tsDurationReader.isDurationReadFinished()) {\n      result = tsDurationReader.readDuration(input, seekPositionHolder);\n      if (result == Extractor.RESULT_SEEK) {\n        input.setPosition((int) seekPositionHolder.position);\n      }\n    }\n    assertThat(result).isNotEqualTo(Extractor.RESULT_END_OF_INPUT);\n    assertThat(tsDurationReader.getDurationUs()).isEqualTo(766);\n  }\n\n  @Test\n  public void testReadDuration_midStream_returnsCorrectDuration()\n      throws IOException, InterruptedException {\n    FakeExtractorInput input =\n        new FakeExtractorInput.Builder()\n            .setData(\n                TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), \"ts/sample.ps\"))\n            .build();\n\n    input.setPosition(1234);\n    int result = Extractor.RESULT_CONTINUE;\n    while (!tsDurationReader.isDurationReadFinished()) {\n      result = tsDurationReader.readDuration(input, seekPositionHolder);\n      if (result == Extractor.RESULT_SEEK) {\n        input.setPosition((int) seekPositionHolder.position);\n      }\n    }\n    assertThat(result).isNotEqualTo(Extractor.RESULT_END_OF_INPUT);\n    assertThat(tsDurationReader.getDurationUs()).isEqualTo(766);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\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.extractor.SeekMap;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.FakeExtractorOutput;\nimport com.google.android.exoplayer2.testutil.FakeTrackOutput;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.DefaultDataSource;\nimport com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.Random;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Seeking tests for {@link PsExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class PsExtractorSeekTest {\n\n  private static final String PS_FILE_PATH = \"ts/elephants_dream.mpg\";\n  private static final int DURATION_US = 30436333;\n  private static final int VIDEO_TRACK_ID = 224;\n  private static final long DELTA_TIMESTAMP_THRESHOLD_US = 500_000L;\n  private static final Random random = new Random(1234L);\n\n  private FakeExtractorOutput expectedOutput;\n  private FakeTrackOutput expectedTrackOutput;\n\n  private DefaultDataSource dataSource;\n  private PositionHolder positionHolder;\n  private long totalInputLength;\n\n  @Before\n  public void setUp() throws IOException, InterruptedException {\n    expectedOutput = new FakeExtractorOutput();\n    positionHolder = new PositionHolder();\n    extractAllSamplesFromFileToExpectedOutput(\n        ApplicationProvider.getApplicationContext(), PS_FILE_PATH);\n    expectedTrackOutput = expectedOutput.trackOutputs.get(VIDEO_TRACK_ID);\n\n    dataSource =\n        new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), \"UserAgent\")\n            .createDataSource();\n    totalInputLength = readInputLength();\n  }\n\n  @Test\n  public void testPsExtractorReads_nonSeekTableFile_returnSeekableSeekMap()\n      throws IOException, InterruptedException {\n    PsExtractor extractor = new PsExtractor();\n\n    SeekMap seekMap = extractSeekMapAndTracks(extractor, new FakeExtractorOutput());\n\n    assertThat(seekMap).isNotNull();\n    assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);\n    assertThat(seekMap.isSeekable()).isTrue();\n  }\n\n  @Test\n  public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    PsExtractor extractor = new PsExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID);\n\n    long targetSeekTimeUs = 987_000;\n    int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainsTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testHandlePendingSeek_handlesSeekToEoF() throws IOException, InterruptedException {\n    PsExtractor extractor = new PsExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID);\n\n    long targetSeekTimeUs = seekMap.getDurationUs();\n\n    int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);\n    // Assert that this seek will return a position at end of stream, without any frame.\n    assertThat(extractedFrameIndex).isEqualTo(-1);\n  }\n\n  @Test\n  public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    PsExtractor extractor = new PsExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID);\n\n    long firstSeekTimeUs = 987_000;\n    seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput);\n\n    long targetSeekTimeUs = 0;\n    int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainsTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    PsExtractor extractor = new PsExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID);\n\n    long firstSeekTimeUs = 987_000;\n    seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput);\n\n    long targetSeekTimeUs = 1_234_000;\n    int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainsTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    PsExtractor extractor = new PsExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = extractSeekMapAndTracks(extractor, extractorOutput);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID);\n\n    long numSeek = 100;\n    for (long i = 0; i < numSeek; i++) {\n      long targetSeekTimeUs = random.nextInt(DURATION_US + 1);\n      int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);\n\n      assertThat(extractedFrameIndex).isNotEqualTo(-1);\n      assertFirstFrameAfterSeekContainsTargetSeekTime(\n          trackOutput, targetSeekTimeUs, extractedFrameIndex);\n    }\n  }\n\n  @Test\n  public void testHandlePendingSeek_handlesRandomSeeksAfterReadingFileOnce_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    PsExtractor extractor = new PsExtractor();\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    readInputFileOnce(extractor, extractorOutput);\n    SeekMap seekMap = extractorOutput.seekMap;\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(VIDEO_TRACK_ID);\n\n    long numSeek = 100;\n    for (long i = 0; i < numSeek; i++) {\n      long targetSeekTimeUs = random.nextInt(DURATION_US + 1);\n      int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);\n\n      assertThat(extractedFrameIndex).isNotEqualTo(-1);\n      assertFirstFrameAfterSeekContainsTargetSeekTime(\n          trackOutput, targetSeekTimeUs, extractedFrameIndex);\n    }\n  }\n\n  // Internal methods\n\n  private long readInputLength() throws IOException {\n    DataSpec dataSpec =\n        new DataSpec(Uri.parse(\"asset:///\" + PS_FILE_PATH), 0, C.LENGTH_UNSET, null);\n    long totalInputLength = dataSource.open(dataSpec);\n    Util.closeQuietly(dataSource);\n    return totalInputLength;\n  }\n\n  /**\n   * Seeks to the given seek time and keeps reading from input until we can extract at least one\n   * frame from the seek position, or until end-of-input is reached.\n   *\n   * @return The index of the first extracted frame written to the given {@code trackOutput} after\n   *     the seek is completed, or -1 if the seek is completed without any extracted frame.\n   */\n  private int seekToTimeUs(\n      PsExtractor psExtractor, SeekMap seekMap, long seekTimeUs, FakeTrackOutput trackOutput)\n      throws IOException, InterruptedException {\n    int numSampleBeforeSeek = trackOutput.getSampleCount();\n    SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs);\n\n    long initialSeekLoadPosition = seekPoints.first.position;\n    psExtractor.seek(initialSeekLoadPosition, seekTimeUs);\n\n    positionHolder.position = C.POSITION_UNSET;\n    ExtractorInput extractorInput = getExtractorInputFromPosition(initialSeekLoadPosition);\n    int extractorReadResult = Extractor.RESULT_CONTINUE;\n    while (true) {\n      try {\n        // Keep reading until we can read at least one frame after seek\n        while (extractorReadResult == Extractor.RESULT_CONTINUE\n            && trackOutput.getSampleCount() == numSampleBeforeSeek) {\n          extractorReadResult = psExtractor.read(extractorInput, positionHolder);\n        }\n      } finally {\n        Util.closeQuietly(dataSource);\n      }\n\n      if (extractorReadResult == Extractor.RESULT_SEEK) {\n        extractorInput = getExtractorInputFromPosition(positionHolder.position);\n        extractorReadResult = Extractor.RESULT_CONTINUE;\n      } else if (extractorReadResult == Extractor.RESULT_END_OF_INPUT) {\n        return -1;\n      } else if (trackOutput.getSampleCount() > numSampleBeforeSeek) {\n        // First index after seek = num sample before seek.\n        return numSampleBeforeSeek;\n      }\n    }\n  }\n\n  private SeekMap extractSeekMapAndTracks(PsExtractor extractor, FakeExtractorOutput output)\n      throws IOException, InterruptedException {\n    ExtractorInput input = getExtractorInputFromPosition(0);\n    extractor.init(output);\n    int readResult = Extractor.RESULT_CONTINUE;\n    while (true) {\n      try {\n        // Keep reading until we can get the seek map\n        while (readResult == Extractor.RESULT_CONTINUE\n            && (output.seekMap == null || !output.tracksEnded)) {\n          readResult = extractor.read(input, positionHolder);\n        }\n      } finally {\n        Util.closeQuietly(dataSource);\n      }\n\n      if (readResult == Extractor.RESULT_SEEK) {\n        input = getExtractorInputFromPosition(positionHolder.position);\n        readResult = Extractor.RESULT_CONTINUE;\n      } else if (readResult == Extractor.RESULT_END_OF_INPUT) {\n        throw new IOException(\"EOF encountered without seekmap\");\n      }\n      if (output.seekMap != null) {\n        return output.seekMap;\n      }\n    }\n  }\n\n  private void readInputFileOnce(PsExtractor extractor, FakeExtractorOutput extractorOutput)\n      throws IOException, InterruptedException {\n    extractor.init(extractorOutput);\n    int readResult = Extractor.RESULT_CONTINUE;\n    ExtractorInput input = getExtractorInputFromPosition(0);\n    while (readResult != Extractor.RESULT_END_OF_INPUT) {\n      try {\n        while (readResult == Extractor.RESULT_CONTINUE) {\n          readResult = extractor.read(input, positionHolder);\n        }\n      } finally {\n        Util.closeQuietly(dataSource);\n      }\n      if (readResult == Extractor.RESULT_SEEK) {\n        input = getExtractorInputFromPosition(positionHolder.position);\n        readResult = Extractor.RESULT_CONTINUE;\n      }\n    }\n  }\n\n  private void assertFirstFrameAfterSeekContainsTargetSeekTime(\n      FakeTrackOutput trackOutput, long seekTimeUs, int firstFrameIndexAfterSeek) {\n    long outputSampleTimeUs = trackOutput.getSampleTimeUs(firstFrameIndexAfterSeek);\n    int expectedSampleIndex =\n        findOutputFrameInExpectedOutput(trackOutput.getSampleData(firstFrameIndexAfterSeek));\n    // Assert that after seeking, the first sample frame written to output exists in the sample list\n    assertThat(expectedSampleIndex).isNotEqualTo(C.INDEX_UNSET);\n\n    long sampleTimeUs = expectedTrackOutput.getSampleTimeUs(expectedSampleIndex);\n    if (sampleTimeUs != 0) {\n      // Assert that the timestamp output for first sample after seek is near the seek point.\n      // For Ps seeking, unfortunately we can't guarantee exact frame seeking, since PID timestamp\n      // is not too reliable.\n      assertThat(Math.abs(outputSampleTimeUs - seekTimeUs))\n          .isLessThan(DELTA_TIMESTAMP_THRESHOLD_US);\n    }\n    // Assert that the timestamp output for first sample after seek is near the actual sample\n    // at seek point.\n    // Note that the timestamp output for first sample after seek might *NOT* be equal to the\n    // timestamp of that same sample when reading from the beginning, because if first timestamp\n    // in the stream was not read before the seek, then the timestamp of the first sample after\n    // the seek is just approximated from the seek point.\n    assertThat(\n            Math.abs(outputSampleTimeUs - expectedTrackOutput.getSampleTimeUs(expectedSampleIndex)))\n        .isLessThan(DELTA_TIMESTAMP_THRESHOLD_US);\n    trackOutput.assertSample(\n        firstFrameIndexAfterSeek,\n        expectedTrackOutput.getSampleData(expectedSampleIndex),\n        outputSampleTimeUs,\n        expectedTrackOutput.getSampleFlags(expectedSampleIndex),\n        expectedTrackOutput.getSampleCryptoData(expectedSampleIndex));\n  }\n\n  private int findOutputFrameInExpectedOutput(byte[] sampleData) {\n    for (int i = 0; i < expectedTrackOutput.getSampleCount(); i++) {\n      byte[] currentSampleData = expectedTrackOutput.getSampleData(i);\n      if (Arrays.equals(currentSampleData, sampleData)) {\n        return i;\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n  private ExtractorInput getExtractorInputFromPosition(long position) throws IOException {\n    DataSpec dataSpec =\n        new DataSpec(\n            Uri.parse(\"asset:///\" + PS_FILE_PATH), position, C.LENGTH_UNSET, /* key= */ null);\n    dataSource.open(dataSpec);\n    return new DefaultExtractorInput(dataSource, position, totalInputLength);\n  }\n\n  private void extractAllSamplesFromFileToExpectedOutput(Context context, String fileName)\n      throws IOException, InterruptedException {\n    byte[] data = TestUtil.getByteArray(context, fileName);\n\n    PsExtractor extractor = new PsExtractor();\n    extractor.init(expectedOutput);\n    FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();\n\n    int readResult = Extractor.RESULT_CONTINUE;\n    while (readResult != Extractor.RESULT_END_OF_INPUT) {\n      readResult = extractor.read(input, positionHolder);\n      if (readResult == Extractor.RESULT_SEEK) {\n        input.setPosition((int) positionHolder.position);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link PsExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class PsExtractorTest {\n\n  @Test\n  public void testSample() throws Exception {\n    ExtractorAsserts.assertBehavior(PsExtractor::new, \"ts/sample.ps\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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;\nimport static com.google.common.truth.Truth.assertThat;\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.singletonList;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.testutil.FakeExtractorOutput;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link SectionReader}. */\n@RunWith(AndroidJUnit4.class)\npublic final class SectionReaderTest {\n\n  private byte[] packetPayload;\n  private CustomSectionPayloadReader payloadReader;\n  private SectionReader reader;\n\n  @Before\n  public void setUp() {\n    packetPayload = new byte[512];\n    Arrays.fill(packetPayload, (byte) 0xFF);\n    payloadReader = new CustomSectionPayloadReader();\n    reader = new SectionReader(payloadReader);\n    reader.init(new TimestampAdjuster(0), new FakeExtractorOutput(),\n        new TsPayloadReader.TrackIdGenerator(0, 1));\n  }\n\n  @Test\n  public void testSingleOnePacketSection() {\n    packetPayload[0] = 3;\n    insertTableSection(4, (byte) 99, 3);\n    reader.consume(new ParsableByteArray(packetPayload), FLAG_PAYLOAD_UNIT_START_INDICATOR);\n    assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(99));\n  }\n\n  @Test\n  public void testHeaderSplitAcrossPackets() {\n    packetPayload[0] = 3; // The first packet includes a pointer_field.\n    insertTableSection(4, (byte) 100, 3); // This section header spreads across both packets.\n\n    ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 5);\n    reader.consume(firstPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);\n    assertThat(payloadReader.parsedTableIds).isEmpty();\n\n    ParsableByteArray secondPacket = new ParsableByteArray(packetPayload);\n    secondPacket.setPosition(5);\n    reader.consume(secondPacket, /* flags= */ 0);\n    assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(100));\n  }\n\n  @Test\n  public void testFiveSectionsInTwoPackets() {\n    packetPayload[0] = 0; // The first packet includes a pointer_field.\n    insertTableSection(1, (byte) 101, 10);\n    insertTableSection(14, (byte) 102, 10);\n    insertTableSection(27, (byte) 103, 10);\n    packetPayload[40] = 0; // The second packet includes a pointer_field.\n    insertTableSection(41, (byte) 104, 10);\n    insertTableSection(54, (byte) 105, 10);\n\n    ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 40);\n    reader.consume(firstPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);\n    assertThat(payloadReader.parsedTableIds).isEqualTo(asList(101, 102, 103));\n\n    ParsableByteArray secondPacket = new ParsableByteArray(packetPayload);\n    secondPacket.setPosition(40);\n    reader.consume(secondPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);\n    assertThat(payloadReader.parsedTableIds).isEqualTo(asList(101, 102, 103, 104, 105));\n  }\n\n  @Test\n  public void testLongSectionAcrossFourPackets() {\n    packetPayload[0] = 13; // The first packet includes a pointer_field.\n    insertTableSection(1, (byte) 106, 10); // First section. Should be skipped.\n    // Second section spread across four packets. Should be consumed.\n    insertTableSection(14, (byte) 107, 300);\n    packetPayload[300] = 17; // The third packet includes a pointer_field.\n    // Third section, at the payload start of the fourth packet. Should be consumed.\n    insertTableSection(318, (byte) 108, 10);\n\n    ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100);\n    reader.consume(firstPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);\n    assertThat(payloadReader.parsedTableIds).isEmpty();\n\n    ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200);\n    secondPacket.setPosition(100);\n    reader.consume(secondPacket, /* flags= */ 0);\n    assertThat(payloadReader.parsedTableIds).isEmpty();\n\n    ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300);\n    thirdPacket.setPosition(200);\n    reader.consume(thirdPacket, /* flags= */ 0);\n    assertThat(payloadReader.parsedTableIds).isEmpty();\n\n    ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload);\n    fourthPacket.setPosition(300);\n    reader.consume(fourthPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);\n    assertThat(payloadReader.parsedTableIds).isEqualTo(asList(107, 108));\n  }\n\n  @Test\n  public void testSeek() {\n    packetPayload[0] = 13; // The first packet includes a pointer_field.\n    insertTableSection(1, (byte) 109, 10); // First section. Should be skipped.\n    // Second section spread across four packets. Should be consumed.\n    insertTableSection(14, (byte) 110, 300);\n    packetPayload[300] = 17; // The third packet includes a pointer_field.\n    // Third section, at the payload start of the fourth packet. Should be consumed.\n    insertTableSection(318, (byte) 111, 10);\n\n    ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100);\n    reader.consume(firstPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);\n    assertThat(payloadReader.parsedTableIds).isEmpty();\n\n    ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200);\n    secondPacket.setPosition(100);\n    reader.consume(secondPacket, /* flags= */ 0);\n    assertThat(payloadReader.parsedTableIds).isEmpty();\n\n    ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300);\n    thirdPacket.setPosition(200);\n    reader.consume(thirdPacket, /* flags= */ 0);\n    assertThat(payloadReader.parsedTableIds).isEmpty();\n\n    reader.seek();\n\n    ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload);\n    fourthPacket.setPosition(300);\n    reader.consume(fourthPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);\n    assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(111));\n  }\n\n  @Test\n  public void testCrcChecks() {\n    byte[] correctCrcPat = new byte[] {\n        (byte) 0x0, (byte) 0x0, (byte) 0xb0, (byte) 0xd, (byte) 0x0, (byte) 0x1, (byte) 0xc1,\n        (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x1, (byte) 0xe1, (byte) 0x0, (byte) 0xe8,\n        (byte) 0xf9, (byte) 0x5e, (byte) 0x7d};\n    byte[] incorrectCrcPat = Arrays.copyOf(correctCrcPat, correctCrcPat.length);\n    // Crc field is incorrect, and should not be passed to the payload reader.\n    incorrectCrcPat[16]--;\n    reader.consume(new ParsableByteArray(correctCrcPat), FLAG_PAYLOAD_UNIT_START_INDICATOR);\n    assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(0));\n    reader.consume(new ParsableByteArray(incorrectCrcPat), FLAG_PAYLOAD_UNIT_START_INDICATOR);\n    assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(0));\n  }\n\n  // Internal methods.\n\n  /**\n   * Inserts a private section header to {@link #packetPayload}.\n   *\n   * @param offset The position at which the header is inserted.\n   * @param tableId The table_id for the inserted section.\n   * @param sectionLength The value to use for private_section_length.\n   */\n  private void insertTableSection(int offset, byte tableId, int sectionLength) {\n    packetPayload[offset++] = tableId;\n    packetPayload[offset++] = (byte) ((sectionLength >> 8) & 0x0F);\n    packetPayload[offset] = (byte) (sectionLength & 0xFF);\n  }\n\n  // Internal classes.\n\n  private static final class CustomSectionPayloadReader implements SectionPayloadReader {\n\n    List<Integer> parsedTableIds;\n\n    @Override\n    public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,\n        TsPayloadReader.TrackIdGenerator idGenerator) {\n      parsedTableIds = new ArrayList<>();\n    }\n\n    @Override\n    public void consume(ParsableByteArray sectionData) {\n      parsedTableIds.add(sectionData.readUnsignedByte());\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.extractor.Extractor;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.io.IOException;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link TsDurationReader}. */\n@RunWith(AndroidJUnit4.class)\npublic final class TsDurationReaderTest {\n\n  private TsDurationReader tsDurationReader;\n  private PositionHolder seekPositionHolder;\n\n  @Before\n  public void setUp() {\n    tsDurationReader = new TsDurationReader();\n    seekPositionHolder = new PositionHolder();\n  }\n\n  @Test\n  public void testIsDurationReadPending_returnFalseByDefault() {\n    assertThat(tsDurationReader.isDurationReadFinished()).isFalse();\n  }\n\n  @Test\n  public void testReadDuration_returnsCorrectDuration() throws IOException, InterruptedException {\n    FakeExtractorInput input =\n        new FakeExtractorInput.Builder()\n            .setData(\n                TestUtil.getByteArray(\n                    ApplicationProvider.getApplicationContext(), \"ts/bbb_2500ms.ts\"))\n            .setSimulateIOErrors(false)\n            .setSimulateUnknownLength(false)\n            .setSimulatePartialReads(false)\n            .build();\n\n    while (!tsDurationReader.isDurationReadFinished()) {\n      int result = tsDurationReader.readDuration(input, seekPositionHolder, /* pcrPid= */ 256);\n      if (result == Extractor.RESULT_END_OF_INPUT) {\n        break;\n      }\n      if (result == Extractor.RESULT_SEEK) {\n        input.setPosition((int) seekPositionHolder.position);\n      }\n    }\n    assertThat(tsDurationReader.getDurationUs() / 1000).isEqualTo(2500);\n  }\n\n  @Test\n  public void testReadDuration_midStream_returnsCorrectDuration()\n      throws IOException, InterruptedException {\n    FakeExtractorInput input =\n        new FakeExtractorInput.Builder()\n            .setData(\n                TestUtil.getByteArray(\n                    ApplicationProvider.getApplicationContext(), \"ts/bbb_2500ms.ts\"))\n            .setSimulateIOErrors(false)\n            .setSimulateUnknownLength(false)\n            .setSimulatePartialReads(false)\n            .build();\n\n    input.setPosition(1234);\n    while (!tsDurationReader.isDurationReadFinished()) {\n      int result = tsDurationReader.readDuration(input, seekPositionHolder, /* pcrPid= */ 256);\n      if (result == Extractor.RESULT_END_OF_INPUT) {\n        break;\n      }\n      if (result == Extractor.RESULT_SEEK) {\n        input.setPosition((int) seekPositionHolder.position);\n      }\n    }\n    assertThat(tsDurationReader.getDurationUs() / 1000).isEqualTo(2500);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\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.extractor.SeekMap;\nimport com.google.android.exoplayer2.testutil.FakeExtractorOutput;\nimport com.google.android.exoplayer2.testutil.FakeTrackOutput;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.upstream.DefaultDataSource;\nimport com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.Random;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Seeking tests for {@link TsExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class TsExtractorSeekTest {\n\n  private static final String TEST_FILE = \"ts/bbb_2500ms.ts\";\n  private static final int DURATION_US = 2_500_000;\n  private static final int AUDIO_TRACK_ID = 257;\n  private static final long MAXIMUM_TIMESTAMP_DELTA_US = 500_000L;\n\n  private static final Random random = new Random(1234L);\n\n  private FakeTrackOutput expectedTrackOutput;\n  private DefaultDataSource dataSource;\n  private PositionHolder positionHolder;\n\n  @Before\n  public void setUp() throws IOException, InterruptedException {\n    positionHolder = new PositionHolder();\n    expectedTrackOutput =\n        TestUtil.extractAllSamplesFromFile(\n                new TsExtractor(), ApplicationProvider.getApplicationContext(), TEST_FILE)\n            .trackOutputs\n            .get(AUDIO_TRACK_ID);\n\n    dataSource =\n        new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), \"UserAgent\")\n            .createDataSource();\n  }\n\n  @Test\n  public void testTsExtractorReads_nonSeekTableFile_returnSeekableSeekMap()\n      throws IOException, InterruptedException {\n    Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);\n    TsExtractor extractor = new TsExtractor();\n\n    SeekMap seekMap =\n        TestUtil.extractSeekMap(extractor, new FakeExtractorOutput(), dataSource, fileUri);\n\n    assertThat(seekMap).isNotNull();\n    assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);\n    assertThat(seekMap.isSeekable()).isTrue();\n  }\n\n  @Test\n  public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    TsExtractor extractor = new TsExtractor();\n    Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID);\n\n    long targetSeekTimeUs = 987_000;\n    int extractedFrameIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame()\n      throws IOException, InterruptedException {\n    TsExtractor extractor = new TsExtractor();\n    Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID);\n\n    long targetSeekTimeUs = seekMap.getDurationUs();\n\n    int extractedFrameIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    TsExtractor extractor = new TsExtractor();\n    Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID);\n\n    long firstSeekTimeUs = 987_000;\n    TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    long targetSeekTimeUs = 0;\n    int extractedFrameIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    TsExtractor extractor = new TsExtractor();\n    Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID);\n\n    long firstSeekTimeUs = 987_000;\n    TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    long targetSeekTimeUs = 1_234_000;\n    int extractedFrameIndex =\n        TestUtil.seekToTimeUs(\n            extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n    assertThat(extractedFrameIndex).isNotEqualTo(-1);\n    assertFirstFrameAfterSeekContainTargetSeekTime(\n        trackOutput, targetSeekTimeUs, extractedFrameIndex);\n  }\n\n  @Test\n  public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    TsExtractor extractor = new TsExtractor();\n    Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID);\n\n    long numSeek = 100;\n    for (long i = 0; i < numSeek; i++) {\n      long targetSeekTimeUs = random.nextInt(DURATION_US + 1);\n      int extractedFrameIndex =\n          TestUtil.seekToTimeUs(\n              extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n      assertThat(extractedFrameIndex).isNotEqualTo(-1);\n      assertFirstFrameAfterSeekContainTargetSeekTime(\n          trackOutput, targetSeekTimeUs, extractedFrameIndex);\n    }\n  }\n\n  @Test\n  public void testHandlePendingSeek_handlesRandomSeeksAfterReadingFileOnce_extractsCorrectFrame()\n      throws IOException, InterruptedException {\n    TsExtractor extractor = new TsExtractor();\n    Uri fileUri = TestUtil.buildAssetUri(TEST_FILE);\n\n    FakeExtractorOutput extractorOutput = new FakeExtractorOutput();\n    readInputFileOnce(extractor, extractorOutput, fileUri);\n    SeekMap seekMap = extractorOutput.seekMap;\n    FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(AUDIO_TRACK_ID);\n\n    long numSeek = 100;\n    for (long i = 0; i < numSeek; i++) {\n      long targetSeekTimeUs = random.nextInt(DURATION_US + 1);\n      int extractedFrameIndex =\n          TestUtil.seekToTimeUs(\n              extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);\n\n      assertThat(extractedFrameIndex).isNotEqualTo(-1);\n      assertFirstFrameAfterSeekContainTargetSeekTime(\n          trackOutput, targetSeekTimeUs, extractedFrameIndex);\n    }\n  }\n\n  // Internal methods\n\n  private void readInputFileOnce(\n      TsExtractor extractor, FakeExtractorOutput extractorOutput, Uri fileUri)\n      throws IOException, InterruptedException {\n    extractor.init(extractorOutput);\n    int readResult = Extractor.RESULT_CONTINUE;\n    ExtractorInput input = TestUtil.getExtractorInputFromPosition(dataSource, 0, fileUri);\n    while (readResult != Extractor.RESULT_END_OF_INPUT) {\n      try {\n        while (readResult == Extractor.RESULT_CONTINUE) {\n          readResult = extractor.read(input, positionHolder);\n        }\n      } finally {\n        Util.closeQuietly(dataSource);\n      }\n      if (readResult == Extractor.RESULT_SEEK) {\n        input =\n            TestUtil.getExtractorInputFromPosition(dataSource, positionHolder.position, fileUri);\n        readResult = Extractor.RESULT_CONTINUE;\n      }\n    }\n  }\n\n  private void assertFirstFrameAfterSeekContainTargetSeekTime(\n      FakeTrackOutput trackOutput, long seekTimeUs, int firstFrameIndexAfterSeek) {\n    long outputSampleTimeUs = trackOutput.getSampleTimeUs(firstFrameIndexAfterSeek);\n    int expectedSampleIndex =\n        findOutputFrameInExpectedOutput(trackOutput.getSampleData(firstFrameIndexAfterSeek));\n    // Assert that after seeking, the first sample frame written to output exists in the sample list\n    assertThat(expectedSampleIndex).isNotEqualTo(-1);\n    // Assert that the timestamp output for first sample after seek is near the seek point.\n    // For Ts seeking, unfortunately we can't guarantee exact frame seeking, since PID timestamp is\n    // not too reliable.\n    assertThat(Math.abs(outputSampleTimeUs - seekTimeUs)).isLessThan(MAXIMUM_TIMESTAMP_DELTA_US);\n    // Assert that the timestamp output for first sample after seek is near the actual sample\n    // at seek point.\n    // Note that the timestamp output for first sample after seek might *NOT* be equal to the\n    // timestamp of that same sample when reading from the beginning, because if first timestamp in\n    // the stream was not read before the seek, then the timestamp of the first sample after the\n    // seek is just approximated from the seek point.\n    assertThat(\n            Math.abs(outputSampleTimeUs - expectedTrackOutput.getSampleTimeUs(expectedSampleIndex)))\n        .isLessThan(MAXIMUM_TIMESTAMP_DELTA_US);\n    trackOutput.assertSample(\n        firstFrameIndexAfterSeek,\n        expectedTrackOutput.getSampleData(expectedSampleIndex),\n        outputSampleTimeUs,\n        expectedTrackOutput.getSampleFlags(expectedSampleIndex),\n        expectedTrackOutput.getSampleCryptoData(expectedSampleIndex));\n  }\n\n  private int findOutputFrameInExpectedOutput(byte[] sampleData) {\n    for (int i = 0; i < expectedTrackOutput.getSampleCount(); i++) {\n      byte[] currentSampleData = expectedTrackOutput.getSampleData(i);\n      if (Arrays.equals(currentSampleData, sampleData)) {\n        return i;\n      }\n    }\n    return -1;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.common.truth.Truth.assertThat;\n\nimport android.util.SparseArray;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\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.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.PositionHolder;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;\nimport com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.testutil.FakeExtractorOutput;\nimport com.google.android.exoplayer2.testutil.FakeTrackOutput;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport java.io.ByteArrayOutputStream;\nimport java.util.Random;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link TsExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class TsExtractorTest {\n\n  private static final int TS_PACKET_SIZE = 188;\n  private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.\n\n  @Test\n  public void testSample() throws Exception {\n    ExtractorAsserts.assertBehavior(TsExtractor::new, \"ts/sample.ts\");\n  }\n\n  @Test\n  public void testStreamWithJunkData() throws Exception {\n    Random random = new Random(0);\n    byte[] fileData =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), \"ts/sample.ts\");\n    ByteArrayOutputStream out = new ByteArrayOutputStream(fileData.length * 2);\n    int bytesLeft = fileData.length;\n\n    writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);\n    out.write(fileData, 0, TS_PACKET_SIZE * 5);\n    bytesLeft -= TS_PACKET_SIZE * 5;\n\n    for (int i = TS_PACKET_SIZE * 5; i < fileData.length; i += 5 * TS_PACKET_SIZE) {\n      writeJunkData(out, random.nextInt(TS_PACKET_SIZE));\n      int length = Math.min(5 * TS_PACKET_SIZE, bytesLeft);\n      out.write(fileData, i, length);\n      bytesLeft -= length;\n    }\n    out.write(TS_SYNC_BYTE);\n    writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);\n    fileData = out.toByteArray();\n\n    ExtractorAsserts.assertOutput(\n        TsExtractor::new, \"ts/sample.ts\", fileData, ApplicationProvider.getApplicationContext());\n  }\n\n  @Test\n  public void testCustomPesReader() throws Exception {\n    CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false);\n    TsExtractor tsExtractor =\n        new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0), factory);\n    FakeExtractorInput input =\n        new FakeExtractorInput.Builder()\n            .setData(\n                TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), \"ts/sample.ts\"))\n            .setSimulateIOErrors(false)\n            .setSimulateUnknownLength(false)\n            .setSimulatePartialReads(false)\n            .build();\n    FakeExtractorOutput output = new FakeExtractorOutput();\n    tsExtractor.init(output);\n    PositionHolder seekPositionHolder = new PositionHolder();\n    int readResult = Extractor.RESULT_CONTINUE;\n    while (readResult != Extractor.RESULT_END_OF_INPUT) {\n      readResult = tsExtractor.read(input, seekPositionHolder);\n      if (readResult == Extractor.RESULT_SEEK) {\n        input.setPosition((int) seekPositionHolder.position);\n      }\n    }\n    CustomEsReader reader = factory.esReader;\n    assertThat(reader.packetsRead).isEqualTo(2);\n    TrackOutput trackOutput = reader.getTrackOutput();\n    assertThat(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */)).isTrue();\n    assertThat(((FakeTrackOutput) trackOutput).format)\n        .isEqualTo(Format.createTextSampleFormat(\"1/257\", \"mime\", null, 0, 0, \"und\", null, 0));\n  }\n\n  @Test\n  public void testCustomInitialSectionReader() throws Exception {\n    CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true);\n    TsExtractor tsExtractor =\n        new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0), factory);\n    FakeExtractorInput input =\n        new FakeExtractorInput.Builder()\n            .setData(\n                TestUtil.getByteArray(\n                    ApplicationProvider.getApplicationContext(), \"ts/sample_with_sdt.ts\"))\n            .setSimulateIOErrors(false)\n            .setSimulateUnknownLength(false)\n            .setSimulatePartialReads(false)\n            .build();\n    tsExtractor.init(new FakeExtractorOutput());\n    PositionHolder seekPositionHolder = new PositionHolder();\n    int readResult = Extractor.RESULT_CONTINUE;\n    while (readResult != Extractor.RESULT_END_OF_INPUT) {\n      readResult = tsExtractor.read(input, seekPositionHolder);\n      if (readResult == Extractor.RESULT_SEEK) {\n        input.setPosition((int) seekPositionHolder.position);\n      }\n    }\n    assertThat(factory.sdtReader.consumedSdts).isEqualTo(2);\n  }\n\n  private static void writeJunkData(ByteArrayOutputStream out, int length) {\n    for (int i = 0; i < length; i++) {\n      if (((byte) i) == TS_SYNC_BYTE) {\n        out.write(0);\n      } else {\n        out.write(i);\n      }\n    }\n  }\n\n  private static final class CustomTsPayloadReaderFactory implements TsPayloadReader.Factory {\n\n    private final boolean provideSdtReader;\n    private final boolean provideCustomEsReader;\n    private final TsPayloadReader.Factory defaultFactory;\n    private CustomEsReader esReader;\n    private SdtSectionReader sdtReader;\n\n    public CustomTsPayloadReaderFactory(boolean provideCustomEsReader, boolean provideSdtReader) {\n      this.provideCustomEsReader = provideCustomEsReader;\n      this.provideSdtReader = provideSdtReader;\n      defaultFactory = new DefaultTsPayloadReaderFactory();\n    }\n\n    @Override\n    public SparseArray<TsPayloadReader> createInitialPayloadReaders() {\n      if (provideSdtReader) {\n        assertThat(sdtReader).isNull();\n        SparseArray<TsPayloadReader> mapping = new SparseArray<>();\n        sdtReader = new SdtSectionReader();\n        mapping.put(17, new SectionReader(sdtReader));\n        return mapping;\n      } else {\n        return defaultFactory.createInitialPayloadReaders();\n      }\n    }\n\n    @Override\n    public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) {\n      if (provideCustomEsReader && streamType == 3) {\n        esReader = new CustomEsReader(esInfo.language);\n        return new PesReader(esReader);\n      } else {\n        return defaultFactory.createPayloadReader(streamType, esInfo);\n      }\n    }\n  }\n\n  private static final class CustomEsReader implements ElementaryStreamReader {\n\n    private final String language;\n    private TrackOutput output;\n    public int packetsRead = 0;\n\n    public CustomEsReader(String language) {\n      this.language = language;\n    }\n\n    @Override\n    public void seek() {}\n\n    @Override\n    public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {\n      idGenerator.generateNewId();\n      output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_UNKNOWN);\n      output.format(\n          Format.createTextSampleFormat(\n              idGenerator.getFormatId(), \"mime\", null, 0, 0, language, null, 0));\n    }\n\n    @Override\n    public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {}\n\n    @Override\n    public void consume(ParsableByteArray data) {}\n\n    @Override\n    public void packetFinished() {\n      packetsRead++;\n    }\n\n    public TrackOutput getTrackOutput() {\n      return output;\n    }\n  }\n\n  private static final class SdtSectionReader implements SectionPayloadReader {\n\n    private int consumedSdts;\n\n    @Override\n    public void init(\n        TimestampAdjuster timestampAdjuster,\n        ExtractorOutput extractorOutput,\n        TrackIdGenerator idGenerator) {\n      // Do nothing.\n    }\n\n    @Override\n    public void consume(ParsableByteArray sectionData) {\n      // table_id(8), section_syntax_indicator(1), reserved_future_use(1), reserved(2),\n      // section_length(12), transport_stream_id(16), reserved(2), version_number(5),\n      // current_next_indicator(1), section_number(8), last_section_number(8),\n      // original_network_id(16), reserved_future_use(8)\n      sectionData.skipBytes(11);\n      // Start of the service loop.\n      assertThat(sectionData.readUnsignedShort()).isEqualTo(0x5566 /* arbitrary service id */);\n      // reserved_future_use(6), EIT_schedule_flag(1), EIT_present_following_flag(1)\n      sectionData.skipBytes(1);\n      // Assert there is only one service.\n      // Remove running_status(3), free_CA_mode(1) from the descriptors_loop_length with the mask.\n      assertThat(sectionData.readUnsignedShort() & 0xFFF).isEqualTo(sectionData.bytesLeft());\n      while (sectionData.bytesLeft() > 0) {\n        int descriptorTag = sectionData.readUnsignedByte();\n        int descriptorLength = sectionData.readUnsignedByte();\n        if (descriptorTag == 72 /* service descriptor */) {\n          assertThat(sectionData.readUnsignedByte()).isEqualTo(1); // Service type: Digital TV.\n          int serviceProviderNameLength = sectionData.readUnsignedByte();\n          assertThat(sectionData.readString(serviceProviderNameLength)).isEqualTo(\"Some provider\");\n          int serviceNameLength = sectionData.readUnsignedByte();\n          assertThat(sectionData.readString(serviceNameLength)).isEqualTo(\"Some Channel\");\n        } else {\n          sectionData.skipBytes(descriptorLength);\n        }\n      }\n      consumedSdts++;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.ExtractorAsserts;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link WavExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic final class WavExtractorTest {\n\n  @Test\n  public void testSample() throws Exception {\n    ExtractorAsserts.assertBehavior(WavExtractor::new, \"wav/sample.wav\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.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.metadata;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static java.nio.charset.StandardCharsets.ISO_8859_1;\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessage;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder;\nimport com.google.android.exoplayer2.metadata.id3.TextInformationFrame;\nimport com.google.android.exoplayer2.metadata.scte35.TimeSignalCommand;\nimport com.google.android.exoplayer2.testutil.FakeSampleStream;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link MetadataRenderer}. */\n@RunWith(AndroidJUnit4.class)\npublic class MetadataRendererTest {\n\n  private static final byte[] SCTE35_TIME_SIGNAL_BYTES =\n      TestUtil.joinByteArrays(\n          TestUtil.createByteArray(\n              0, // table_id.\n              0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).\n              0x14, // section_length(8).\n              0x00, // protocol_version.\n              0x00), // encrypted_packet, encryption_algorithm, pts_adjustment(1).\n          TestUtil.createByteArray(0x00, 0x00, 0x00, 0x00), // pts_adjustment(32).\n          TestUtil.createByteArray(\n              0x00, // cw_index.\n              0x00, // tier(8).\n              0x00, // tier(4), splice_command_length(4).\n              0x05, // splice_command_length(8).\n              0x06, // splice_command_type = time_signal.\n              // Start of splice_time().\n              0x80), // time_specified_flag, reserved, pts_time(1).\n          TestUtil.createByteArray(\n              0x52, 0x03, 0x02, 0x8f), // pts_time(32). PTS for a second after playback position.\n          TestUtil.createByteArray(\n              0x00, 0x00, 0x00, 0x00)); // CRC_32 (ignored, check happens at extraction).\n\n  private static final Format EMSG_FORMAT =\n      Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE);\n\n  private final EventMessageEncoder eventMessageEncoder = new EventMessageEncoder();\n\n  @Test\n  public void decodeMetadata() throws Exception {\n    EventMessage emsg =\n        new EventMessage(\n            \"urn:test-scheme-id\",\n            /* value= */ \"\",\n            /* durationMs= */ 1,\n            /* id= */ 0,\n            \"Test data\".getBytes(UTF_8));\n\n    List<Metadata> metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg));\n\n    assertThat(metadata).hasSize(1);\n    assertThat(metadata.get(0).length()).isEqualTo(1);\n    assertThat(metadata.get(0).get(0)).isEqualTo(emsg);\n  }\n\n  @Test\n  public void decodeMetadata_skipsMalformed() throws Exception {\n    List<Metadata> metadata = runRenderer(EMSG_FORMAT, \"not valid emsg bytes\".getBytes(UTF_8));\n\n    assertThat(metadata).isEmpty();\n  }\n\n  @Test\n  public void decodeMetadata_handlesId3WrappedInEmsg() throws Exception {\n    EventMessage emsg =\n        new EventMessage(\n            EventMessage.ID3_SCHEME_ID_AOM,\n            /* value= */ \"\",\n            /* durationMs= */ 1,\n            /* id= */ 0,\n            encodeTxxxId3Frame(\"Test description\", \"Test value\"));\n\n    List<Metadata> metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg));\n\n    assertThat(metadata).hasSize(1);\n    assertThat(metadata.get(0).length()).isEqualTo(1);\n    TextInformationFrame expectedId3Frame =\n        new TextInformationFrame(\"TXXX\", \"Test description\", \"Test value\");\n    assertThat(metadata.get(0).get(0)).isEqualTo(expectedId3Frame);\n  }\n\n  @Test\n  public void decodeMetadata_handlesScte35WrappedInEmsg() throws Exception {\n\n    EventMessage emsg =\n        new EventMessage(\n            EventMessage.SCTE35_SCHEME_ID,\n            /* value= */ \"\",\n            /* durationMs= */ 1,\n            /* id= */ 0,\n            SCTE35_TIME_SIGNAL_BYTES);\n\n    List<Metadata> metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg));\n\n    assertThat(metadata).hasSize(1);\n    assertThat(metadata.get(0).length()).isEqualTo(1);\n    assertThat(metadata.get(0).get(0)).isInstanceOf(TimeSignalCommand.class);\n  }\n\n  @Test\n  public void decodeMetadata_skipsMalformedWrappedMetadata() throws Exception {\n    EventMessage emsg =\n        new EventMessage(\n            EventMessage.ID3_SCHEME_ID_AOM,\n            /* value= */ \"\",\n            /* durationMs= */ 1,\n            /* id= */ 0,\n            \"Not a real ID3 tag\".getBytes(ISO_8859_1));\n\n    List<Metadata> metadata = runRenderer(EMSG_FORMAT, eventMessageEncoder.encode(emsg));\n\n    assertThat(metadata).isEmpty();\n  }\n\n  private static List<Metadata> runRenderer(Format format, byte[] input)\n      throws ExoPlaybackException {\n    List<Metadata> metadata = new ArrayList<>();\n    MetadataRenderer renderer = new MetadataRenderer(metadata::add, /* outputLooper= */ null);\n    renderer.replaceStream(\n        new Format[] {format},\n        new FakeSampleStream(format, /* eventDispatcher= */ null, input),\n        /* offsetUs= */ 0L);\n    renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the format\n    renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0); // Read the data\n\n    return Collections.unmodifiableList(metadata);\n  }\n\n  /**\n   * Builds an ID3v2 tag containing a single 'user defined text information frame' (id='TXXX') with\n   * {@code description} and {@code value}.\n   *\n   * <ul>\n   *   <li><a href=\"http://id3.org/id3v2.4.0-structure\">ID3v2 overall structure</a>\n   *   <li><a href=\"http://id3.org/id3v2.4.0-frames\">'user defined text information frame' spec</a>\n   *       (item 4.2.6)\n   * </ul>\n   */\n  private static byte[] encodeTxxxId3Frame(String description, String value) {\n    byte[] id3FrameData =\n        TestUtil.joinByteArrays(\n            \"TXXX\".getBytes(ISO_8859_1), // ID for a 'user defined text information frame'\n            TestUtil.createByteArray(0, 0, 0, 0), // Frame size (set later)\n            TestUtil.createByteArray(0, 0), // Frame flags\n            TestUtil.createByteArray(0), // Character encoding = ISO-8859-1\n            description.getBytes(ISO_8859_1),\n            TestUtil.createByteArray(0), // String null terminator\n            value.getBytes(ISO_8859_1),\n            TestUtil.createByteArray(0)); // String null terminator\n    int frameSizeIndex = 7;\n    int frameSize = id3FrameData.length - 10;\n    Assertions.checkArgument(\n        frameSize < 128, \"frameSize must fit in 7 bits to avoid synch-safe encoding: \" + frameSize);\n    id3FrameData[frameSizeIndex] = (byte) frameSize;\n\n    byte[] id3Bytes =\n        TestUtil.joinByteArrays(\n            \"ID3\".getBytes(ISO_8859_1), // identifier\n            TestUtil.createByteArray(0x04, 0x00), // version\n            TestUtil.createByteArray(0), // Tag flags\n            TestUtil.createByteArray(0, 0, 0, 0), // Tag size (set later)\n            id3FrameData);\n    int tagSizeIndex = 9;\n    int tagSize = id3Bytes.length - 10;\n    Assertions.checkArgument(\n        tagSize < 128, \"tagSize must fit in 7 bits to avoid synch-safe encoding: \" + tagSize);\n    id3Bytes[tagSizeIndex] = (byte) tagSize;\n    return id3Bytes;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.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.testutil.TestUtil.createByteArray;\nimport static com.google.android.exoplayer2.testutil.TestUtil.joinByteArrays;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.MetadataInputBuffer;\nimport java.nio.ByteBuffer;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link EventMessageDecoder}. */\n@RunWith(AndroidJUnit4.class)\npublic final class EventMessageDecoderTest {\n\n  @Test\n  public void testDecodeEventMessage() {\n    byte[] rawEmsgBody =\n        joinByteArrays(\n            createByteArray(117, 114, 110, 58, 116, 101, 115, 116, 0), // scheme_id_uri = \"urn:test\"\n            createByteArray(49, 50, 51, 0), // value = \"123\"\n            createByteArray(0, 0, 11, 184), // event_duration_ms = 3000\n            createByteArray(0, 15, 67, 211), // id = 1000403\n            createByteArray(0, 1, 2, 3, 4)); // message_data = {0, 1, 2, 3, 4}\n    EventMessageDecoder decoder = new EventMessageDecoder();\n    MetadataInputBuffer buffer = new MetadataInputBuffer();\n    buffer.data = ByteBuffer.allocate(rawEmsgBody.length).put(rawEmsgBody);\n\n    Metadata metadata = decoder.decode(buffer);\n\n    assertThat(metadata.length()).isEqualTo(1);\n    EventMessage eventMessage = (EventMessage) metadata.get(0);\n    assertThat(eventMessage.schemeIdUri).isEqualTo(\"urn:test\");\n    assertThat(eventMessage.value).isEqualTo(\"123\");\n    assertThat(eventMessage.durationMs).isEqualTo(3000);\n    assertThat(eventMessage.id).isEqualTo(1000403);\n    assertThat(eventMessage.messageData).isEqualTo(new byte[]{0, 1, 2, 3, 4});\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.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.testutil.TestUtil.createByteArray;\nimport static com.google.android.exoplayer2.testutil.TestUtil.joinByteArrays;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.MetadataInputBuffer;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link EventMessageEncoder}. */\n@RunWith(AndroidJUnit4.class)\npublic final class EventMessageEncoderTest {\n\n  private static final EventMessage DECODED_MESSAGE =\n      new EventMessage(\"urn:test\", \"123\", 3000, 1000403, new byte[] {0, 1, 2, 3, 4});\n\n  private static final byte[] ENCODED_MESSAGE =\n      joinByteArrays(\n          createByteArray(117, 114, 110, 58, 116, 101, 115, 116, 0), // scheme_id_uri = \"urn:test\"\n          createByteArray(49, 50, 51, 0), // value = \"123\"\n          createByteArray(0, 0, 11, 184), // event_duration_ms = 3000\n          createByteArray(0, 15, 67, 211), // id = 1000403\n          createByteArray(0, 1, 2, 3, 4)); // message_data = {0, 1, 2, 3, 4}\n\n  @Test\n  public void testEncodeEventStream() throws IOException {\n    byte[] foo = new byte[] {1, 2, 3};\n\n    byte[] encodedByteArray = new EventMessageEncoder().encode(DECODED_MESSAGE);\n    assertThat(encodedByteArray).isEqualTo(ENCODED_MESSAGE);\n  }\n\n  @Test\n  public void testEncodeDecodeEventStream() throws IOException {\n    byte[] encodedByteArray = new EventMessageEncoder().encode(DECODED_MESSAGE);\n    MetadataInputBuffer buffer = new MetadataInputBuffer();\n    buffer.data = ByteBuffer.allocate(encodedByteArray.length).put(encodedByteArray);\n\n    EventMessageDecoder decoder = new EventMessageDecoder();\n    Metadata metadata = decoder.decode(buffer);\n    assertThat(metadata.length()).isEqualTo(1);\n    assertThat(metadata.get(0)).isEqualTo(DECODED_MESSAGE);\n  }\n\n  @Test\n  public void testEncodeEventStreamMultipleTimesWorkingCorrectly() throws IOException {\n    EventMessage eventMessage1 =\n        new EventMessage(\"urn:test\", \"123\", 3000, 1000402, new byte[] {4, 3, 2, 1, 0});\n    byte[] expectedEmsgBody1 =\n        joinByteArrays(\n            createByteArray(117, 114, 110, 58, 116, 101, 115, 116, 0), // scheme_id_uri = \"urn:test\"\n            createByteArray(49, 50, 51, 0), // value = \"123\"\n            createByteArray(0, 0, 11, 184), // event_duration_ms = 3000\n            createByteArray(0, 15, 67, 210), // id = 1000402\n            createByteArray(4, 3, 2, 1, 0)); // message_data = {4, 3, 2, 1, 0}\n\n    EventMessageEncoder eventMessageEncoder = new EventMessageEncoder();\n    byte[] encodedByteArray = eventMessageEncoder.encode(DECODED_MESSAGE);\n    assertThat(encodedByteArray).isEqualTo(ENCODED_MESSAGE);\n    byte[] encodedByteArray1 = eventMessageEncoder.encode(eventMessage1);\n    assertThat(encodedByteArray1).isEqualTo(expectedEmsgBody1);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.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.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link EventMessage}. */\n@RunWith(AndroidJUnit4.class)\npublic final class EventMessageTest {\n\n  @Test\n  public void testEventMessageParcelable() {\n    EventMessage eventMessage =\n        new EventMessage(\"urn:test\", \"123\", 3000, 1000403, new byte[] {0, 1, 2, 3, 4});\n    // Write to parcel.\n    Parcel parcel = Parcel.obtain();\n    eventMessage.writeToParcel(parcel, 0);\n    // Create from parcel.\n    parcel.setDataPosition(0);\n    EventMessage fromParcelEventMessage = EventMessage.CREATOR.createFromParcel(parcel);\n    // Assert equals.\n    assertThat(fromParcelEventMessage).isEqualTo(eventMessage);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.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.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link PictureFrame}. */\n@RunWith(AndroidJUnit4.class)\npublic final class PictureFrameTest {\n\n  @Test\n  public void testParcelable() {\n    PictureFrame pictureFrameToParcel = new PictureFrame(0, \"\", \"\", 0, 0, 0, 0, new byte[0]);\n\n    Parcel parcel = Parcel.obtain();\n    pictureFrameToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    PictureFrame pictureFrameFromParcel = PictureFrame.CREATOR.createFromParcel(parcel);\n    assertThat(pictureFrameFromParcel).isEqualTo(pictureFrameToParcel);\n\n    parcel.recycle();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.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.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link VorbisComment}. */\n@RunWith(AndroidJUnit4.class)\npublic final class VorbisCommentTest {\n\n  @Test\n  public void testParcelable() {\n    VorbisComment vorbisCommentFrameToParcel = new VorbisComment(\"key\", \"value\");\n\n    Parcel parcel = Parcel.obtain();\n    vorbisCommentFrameToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    VorbisComment vorbisCommentFrameFromParcel = VorbisComment.CREATOR.createFromParcel(parcel);\n    assertThat(vorbisCommentFrameFromParcel).isEqualTo(vorbisCommentFrameToParcel);\n\n    parcel.recycle();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyDecoderTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link IcyDecoder}. */\n@RunWith(AndroidJUnit4.class)\npublic final class IcyDecoderTest {\n\n  @Test\n  public void decode() {\n    IcyDecoder decoder = new IcyDecoder();\n    String icyContent = \"StreamTitle='test title';StreamURL='test_url';\";\n    Metadata metadata = decoder.decode(icyContent);\n\n    assertThat(metadata.length()).isEqualTo(1);\n    IcyInfo streamInfo = (IcyInfo) metadata.get(0);\n    assertThat(streamInfo.rawMetadata).isEqualTo(icyContent);\n    assertThat(streamInfo.title).isEqualTo(\"test title\");\n    assertThat(streamInfo.url).isEqualTo(\"test_url\");\n  }\n\n  @Test\n  public void decode_titleOnly() {\n    IcyDecoder decoder = new IcyDecoder();\n    String icyContent = \"StreamTitle='test title';\";\n    Metadata metadata = decoder.decode(icyContent);\n\n    assertThat(metadata.length()).isEqualTo(1);\n    IcyInfo streamInfo = (IcyInfo) metadata.get(0);\n    assertThat(streamInfo.rawMetadata).isEqualTo(icyContent);\n    assertThat(streamInfo.title).isEqualTo(\"test title\");\n    assertThat(streamInfo.url).isNull();\n  }\n\n  @Test\n  public void decode_extraTags() {\n    String icyContent =\n        \"StreamTitle='test title';StreamURL='test_url';CustomTag|withWeirdSeparator\";\n    IcyDecoder decoder = new IcyDecoder();\n    Metadata metadata = decoder.decode(icyContent);\n\n    assertThat(metadata.length()).isEqualTo(1);\n    IcyInfo streamInfo = (IcyInfo) metadata.get(0);\n    assertThat(streamInfo.rawMetadata).isEqualTo(icyContent);\n    assertThat(streamInfo.title).isEqualTo(\"test title\");\n    assertThat(streamInfo.url).isEqualTo(\"test_url\");\n  }\n\n  @Test\n  public void decode_emptyTitle() {\n    IcyDecoder decoder = new IcyDecoder();\n    String icyContent = \"StreamTitle='';StreamURL='test_url';\";\n    Metadata metadata = decoder.decode(icyContent);\n\n    assertThat(metadata.length()).isEqualTo(1);\n    IcyInfo streamInfo = (IcyInfo) metadata.get(0);\n    assertThat(streamInfo.rawMetadata).isEqualTo(icyContent);\n    assertThat(streamInfo.title).isEmpty();\n    assertThat(streamInfo.url).isEqualTo(\"test_url\");\n  }\n\n  @Test\n  public void decode_semiColonInTitle() {\n    IcyDecoder decoder = new IcyDecoder();\n    String icyContent = \"StreamTitle='test; title';StreamURL='test_url';\";\n    Metadata metadata = decoder.decode(icyContent);\n\n    assertThat(metadata.length()).isEqualTo(1);\n    IcyInfo streamInfo = (IcyInfo) metadata.get(0);\n    assertThat(streamInfo.rawMetadata).isEqualTo(icyContent);\n    assertThat(streamInfo.title).isEqualTo(\"test; title\");\n    assertThat(streamInfo.url).isEqualTo(\"test_url\");\n  }\n\n  @Test\n  public void decode_quoteInTitle() {\n    IcyDecoder decoder = new IcyDecoder();\n    String icyContent = \"StreamTitle='test' title';StreamURL='test_url';\";\n    Metadata metadata = decoder.decode(icyContent);\n\n    assertThat(metadata.length()).isEqualTo(1);\n    IcyInfo streamInfo = (IcyInfo) metadata.get(0);\n    assertThat(streamInfo.rawMetadata).isEqualTo(icyContent);\n    assertThat(streamInfo.title).isEqualTo(\"test' title\");\n    assertThat(streamInfo.url).isEqualTo(\"test_url\");\n  }\n\n  @Test\n  public void decode_lineTerminatorInTitle() {\n    IcyDecoder decoder = new IcyDecoder();\n    String icyContent = \"StreamTitle='test\\r\\ntitle';StreamURL='test_url';\";\n    Metadata metadata = decoder.decode(icyContent);\n\n    assertThat(metadata.length()).isEqualTo(1);\n    IcyInfo streamInfo = (IcyInfo) metadata.get(0);\n    assertThat(streamInfo.rawMetadata).isEqualTo(icyContent);\n    assertThat(streamInfo.title).isEqualTo(\"test\\r\\ntitle\");\n    assertThat(streamInfo.url).isEqualTo(\"test_url\");\n  }\n\n  @Test\n  public void decode_noReconisedHeaders() {\n    IcyDecoder decoder = new IcyDecoder();\n    Metadata metadata = decoder.decode(\"NotIcyData\");\n\n    assertThat(metadata.length()).isEqualTo(1);\n    IcyInfo streamInfo = (IcyInfo) metadata.get(0);\n    assertThat(streamInfo.rawMetadata).isEqualTo(\"NotIcyData\");\n    assertThat(streamInfo.title).isNull();\n    assertThat(streamInfo.url).isNull();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyHeadersTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link IcyHeaders}. */\n@RunWith(AndroidJUnit4.class)\npublic final class IcyHeadersTest {\n\n  @Test\n  public void parcelEquals() {\n    IcyHeaders icyHeaders =\n        new IcyHeaders(\n            /* bitrate= */ 1234,\n            \"genre\",\n            \"name\",\n            \"url\",\n            /* isPublic= */ true,\n            /* metadataInterval= */ 5678);\n    // Write to parcel.\n    Parcel parcel = Parcel.obtain();\n    icyHeaders.writeToParcel(parcel, 0);\n    // Create from parcel.\n    parcel.setDataPosition(0);\n    IcyHeaders fromParcelIcyHeaders = IcyHeaders.CREATOR.createFromParcel(parcel);\n    // Assert equals.\n    assertThat(fromParcelIcyHeaders).isEqualTo(icyHeaders);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/icy/IcyInfoTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link IcyInfo}. */\n@RunWith(AndroidJUnit4.class)\npublic final class IcyInfoTest {\n\n  @Test\n  public void parcelEquals() {\n    IcyInfo streamInfo = new IcyInfo(\"StreamName='name';StreamUrl='url'\", \"name\", \"url\");\n    // Write to parcel.\n    Parcel parcel = Parcel.obtain();\n    streamInfo.writeToParcel(parcel, 0);\n    // Create from parcel.\n    parcel.setDataPosition(0);\n    IcyInfo fromParcelStreamInfo = IcyInfo.CREATOR.createFromParcel(parcel);\n    // Assert equals.\n    assertThat(fromParcelStreamInfo).isEqualTo(streamInfo);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.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.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link ChapterFrame}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ChapterFrameTest {\n\n  @Test\n  public void testParcelable() {\n    Id3Frame[] subFrames = new Id3Frame[] {\n      new TextInformationFrame(\"TIT2\", null, \"title\"),\n      new UrlLinkFrame(\"WXXX\", \"description\", \"url\")\n    };\n    ChapterFrame chapterFrameToParcel = new ChapterFrame(\"id\", 0, 1, 2, 3, subFrames);\n\n    Parcel parcel = Parcel.obtain();\n    chapterFrameToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    ChapterFrame chapterFrameFromParcel = ChapterFrame.CREATOR.createFromParcel(parcel);\n    assertThat(chapterFrameFromParcel).isEqualTo(chapterFrameToParcel);\n\n    parcel.recycle();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.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.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link ChapterTocFrame}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ChapterTocFrameTest {\n\n  @Test\n  public void testParcelable() {\n    String[] children = new String[] {\"child0\", \"child1\"};\n    Id3Frame[] subFrames = new Id3Frame[] {\n        new TextInformationFrame(\"TIT2\", null, \"title\"),\n        new UrlLinkFrame(\"WXXX\", \"description\", \"url\")\n    };\n    ChapterTocFrame chapterTocFrameToParcel = new ChapterTocFrame(\"id\", false, true, children,\n        subFrames);\n\n    Parcel parcel = Parcel.obtain();\n    chapterTocFrameToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    ChapterTocFrame chapterTocFrameFromParcel = ChapterTocFrame.CREATOR.createFromParcel(parcel);\n    assertThat(chapterTocFrameFromParcel).isEqualTo(chapterTocFrameToParcel);\n\n    parcel.recycle();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.nio.charset.Charset;\nimport java.util.Arrays;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link Id3Decoder}. */\n@RunWith(AndroidJUnit4.class)\npublic final class Id3DecoderTest {\n\n  private static final byte[] TAG_HEADER = new byte[] {'I', 'D', '3', 4, 0, 0, 0, 0, 0, 0};\n  private static final int FRAME_HEADER_LENGTH = 10;\n  private static final int ID3_TEXT_ENCODING_UTF_8 = 3;\n\n  @Test\n  public void testDecodeTxxxFrame() {\n    byte[] rawId3 = buildSingleFrameTag(\"TXXX\", new byte[] {3, 0, 109, 100, 105, 97, 108, 111, 103,\n        95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54, 54, 52, 95, 115, 116, 97, 114, 116, 0});\n    Id3Decoder decoder = new Id3Decoder();\n    Metadata metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(1);\n    TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);\n    assertThat(textInformationFrame.id).isEqualTo(\"TXXX\");\n    assertThat(textInformationFrame.description).isEmpty();\n    assertThat(textInformationFrame.value).isEqualTo(\"mdialog_VINDICO1527664_start\");\n\n    // Test empty.\n    rawId3 = buildSingleFrameTag(\"TXXX\", new byte[0]);\n    metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(0);\n\n    // Test encoding byte only.\n    rawId3 = buildSingleFrameTag(\"TXXX\", new byte[] {ID3_TEXT_ENCODING_UTF_8});\n    metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(1);\n    textInformationFrame = (TextInformationFrame) metadata.get(0);\n    assertThat(textInformationFrame.id).isEqualTo(\"TXXX\");\n    assertThat(textInformationFrame.description).isEmpty();\n    assertThat(textInformationFrame.value).isEmpty();\n  }\n\n  @Test\n  public void testDecodeTextInformationFrame() {\n    byte[] rawId3 = buildSingleFrameTag(\"TIT2\", new byte[] {3, 72, 101, 108, 108, 111, 32, 87, 111,\n        114, 108, 100, 0});\n    Id3Decoder decoder = new Id3Decoder();\n    Metadata metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(1);\n    TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);\n    assertThat(textInformationFrame.id).isEqualTo(\"TIT2\");\n    assertThat(textInformationFrame.description).isNull();\n    assertThat(textInformationFrame.value).isEqualTo(\"Hello World\");\n\n    // Test empty.\n    rawId3 = buildSingleFrameTag(\"TIT2\", new byte[0]);\n    metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(0);\n\n    // Test encoding byte only.\n    rawId3 = buildSingleFrameTag(\"TIT2\", new byte[] {ID3_TEXT_ENCODING_UTF_8});\n    metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(1);\n    textInformationFrame = (TextInformationFrame) metadata.get(0);\n    assertThat(textInformationFrame.id).isEqualTo(\"TIT2\");\n    assertThat(textInformationFrame.description).isNull();\n    assertThat(textInformationFrame.value).isEmpty();\n  }\n\n  @Test\n  public void testDecodeWxxxFrame() {\n    byte[] rawId3 = buildSingleFrameTag(\"WXXX\", new byte[] {ID3_TEXT_ENCODING_UTF_8, 116, 101, 115,\n        116, 0, 104, 116, 116, 112, 115, 58, 47, 47, 116, 101, 115, 116, 46, 99, 111, 109, 47, 97,\n        98, 99, 63, 100, 101, 102});\n    Id3Decoder decoder = new Id3Decoder();\n    Metadata metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(1);\n    UrlLinkFrame urlLinkFrame = (UrlLinkFrame) metadata.get(0);\n    assertThat(urlLinkFrame.id).isEqualTo(\"WXXX\");\n    assertThat(urlLinkFrame.description).isEqualTo(\"test\");\n    assertThat(urlLinkFrame.url).isEqualTo(\"https://test.com/abc?def\");\n\n    // Test empty.\n    rawId3 = buildSingleFrameTag(\"WXXX\", new byte[0]);\n    metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(0);\n\n    // Test encoding byte only.\n    rawId3 = buildSingleFrameTag(\"WXXX\", new byte[] {ID3_TEXT_ENCODING_UTF_8});\n    metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(1);\n    urlLinkFrame = (UrlLinkFrame) metadata.get(0);\n    assertThat(urlLinkFrame.id).isEqualTo(\"WXXX\");\n    assertThat(urlLinkFrame.description).isEmpty();\n    assertThat(urlLinkFrame.url).isEmpty();\n  }\n\n  @Test\n  public void testDecodeUrlLinkFrame() {\n    byte[] rawId3 = buildSingleFrameTag(\"WCOM\", new byte[] {104, 116, 116, 112, 115, 58, 47, 47,\n        116, 101, 115, 116, 46, 99, 111, 109, 47, 97, 98, 99, 63, 100, 101, 102});\n    Id3Decoder decoder = new Id3Decoder();\n    Metadata metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(1);\n    UrlLinkFrame urlLinkFrame = (UrlLinkFrame) metadata.get(0);\n    assertThat(urlLinkFrame.id).isEqualTo(\"WCOM\");\n    assertThat(urlLinkFrame.description).isNull();\n    assertThat(urlLinkFrame.url).isEqualTo(\"https://test.com/abc?def\");\n\n    // Test empty.\n    rawId3 = buildSingleFrameTag(\"WCOM\", new byte[0]);\n    metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(1);\n    urlLinkFrame = (UrlLinkFrame) metadata.get(0);\n    assertThat(urlLinkFrame.id).isEqualTo(\"WCOM\");\n    assertThat(urlLinkFrame.description).isNull();\n    assertThat(urlLinkFrame.url).isEmpty();\n  }\n\n  @Test\n  public void testDecodePrivFrame() {\n    byte[] rawId3 = buildSingleFrameTag(\"PRIV\", new byte[] {116, 101, 115, 116, 0, 1, 2, 3, 4});\n    Id3Decoder decoder = new Id3Decoder();\n    Metadata metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(1);\n    PrivFrame privFrame = (PrivFrame) metadata.get(0);\n    assertThat(privFrame.owner).isEqualTo(\"test\");\n    assertThat(privFrame.privateData).isEqualTo(new byte[]{1, 2, 3, 4});\n\n    // Test empty.\n    rawId3 = buildSingleFrameTag(\"PRIV\", new byte[0]);\n    metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(1);\n    privFrame = (PrivFrame) metadata.get(0);\n    assertThat(privFrame.owner).isEmpty();\n    assertThat(privFrame.privateData).isEqualTo(new byte[0]);\n  }\n\n  @Test\n  public void testDecodeApicFrame() {\n    byte[] rawId3 = buildSingleFrameTag(\"APIC\", new byte[] {3, 105, 109, 97, 103, 101, 47, 106, 112,\n        101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7,\n        8, 9, 0});\n    Id3Decoder decoder = new Id3Decoder();\n    Metadata metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(1);\n    ApicFrame apicFrame = (ApicFrame) metadata.get(0);\n    assertThat(apicFrame.mimeType).isEqualTo(\"image/jpeg\");\n    assertThat(apicFrame.pictureType).isEqualTo(16);\n    assertThat(apicFrame.description).isEqualTo(\"Hello World\");\n    assertThat(apicFrame.pictureData).hasLength(10);\n    assertThat(apicFrame.pictureData).isEqualTo(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0});\n  }\n\n  @Test\n  public void testDecodeCommentFrame() {\n    byte[] rawId3 = buildSingleFrameTag(\"COMM\", new byte[] {ID3_TEXT_ENCODING_UTF_8, 101, 110, 103,\n        100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 116, 101, 120, 116, 0});\n    Id3Decoder decoder = new Id3Decoder();\n    Metadata metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(1);\n    CommentFrame commentFrame = (CommentFrame) metadata.get(0);\n    assertThat(commentFrame.language).isEqualTo(\"eng\");\n    assertThat(commentFrame.description).isEqualTo(\"description\");\n    assertThat(commentFrame.text).isEqualTo(\"text\");\n\n    // Test empty.\n    rawId3 = buildSingleFrameTag(\"COMM\", new byte[0]);\n    metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(0);\n\n    // Test language only.\n    rawId3 = buildSingleFrameTag(\"COMM\", new byte[] {ID3_TEXT_ENCODING_UTF_8, 101, 110, 103});\n    metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(1);\n    commentFrame = (CommentFrame) metadata.get(0);\n    assertThat(commentFrame.language).isEqualTo(\"eng\");\n    assertThat(commentFrame.description).isEmpty();\n    assertThat(commentFrame.text).isEmpty();\n  }\n\n  @Test\n  public void testDecodeMultiFrames() {\n    byte[] rawId3 =\n        buildMultiFramesTag(\n            new FrameSpec(\n                \"COMM\",\n                new byte[] {\n                  3, 101, 110, 103, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 0, 116,\n                  101, 120, 116, 0\n                }),\n            new FrameSpec(\n                \"APIC\",\n                new byte[] {\n                  3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111,\n                  32, 87, 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0\n                }));\n    Id3Decoder decoder = new Id3Decoder();\n    Metadata metadata = decoder.decode(rawId3, rawId3.length);\n    assertThat(metadata.length()).isEqualTo(2);\n    CommentFrame commentFrame = (CommentFrame) metadata.get(0);\n    ApicFrame apicFrame = (ApicFrame) metadata.get(1);\n\n    assertThat(commentFrame.language).isEqualTo(\"eng\");\n    assertThat(commentFrame.description).isEqualTo(\"description\");\n    assertThat(commentFrame.text).isEqualTo(\"text\");\n\n    assertThat(apicFrame.mimeType).isEqualTo(\"image/jpeg\");\n    assertThat(apicFrame.pictureType).isEqualTo(16);\n    assertThat(apicFrame.description).isEqualTo(\"Hello World\");\n    assertThat(apicFrame.pictureData).hasLength(10);\n    assertThat(apicFrame.pictureData).isEqualTo(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0});\n  }\n\n  public static byte[] buildSingleFrameTag(String frameId, byte[] frameData) {\n    return buildMultiFramesTag(new FrameSpec(frameId, frameData));\n  }\n\n  public static byte[] buildMultiFramesTag(FrameSpec... frames) {\n    int totalLength = TAG_HEADER.length;\n    for (FrameSpec frame : frames) {\n      byte[] frameData = frame.frameData;\n      totalLength += FRAME_HEADER_LENGTH + frameData.length;\n    }\n    byte[] tagData = Arrays.copyOf(TAG_HEADER, totalLength);\n    // Fill in the size part of the tag header.\n    int offset = TAG_HEADER.length - 4;\n    int tagSize = totalLength - TAG_HEADER.length;\n    tagData[offset++] = (byte) ((tagSize >> 21) & 0x7F);\n    tagData[offset++] = (byte) ((tagSize >> 14) & 0x7F);\n    tagData[offset++] = (byte) ((tagSize >> 7) & 0x7F);\n    tagData[offset++] = (byte) (tagSize & 0x7F);\n\n    for (FrameSpec frame : frames) {\n      byte[] frameData = frame.frameData;\n      String frameId = frame.frameId;\n      byte[] frameIdBytes = frameId.getBytes(Charset.forName(C.UTF8_NAME));\n      Assertions.checkState(frameIdBytes.length == 4);\n\n      // Fill in the frame header.\n      tagData[offset++] = frameIdBytes[0];\n      tagData[offset++] = frameIdBytes[1];\n      tagData[offset++] = frameIdBytes[2];\n      tagData[offset++] = frameIdBytes[3];\n      tagData[offset++] = (byte) ((frameData.length >> 24) & 0xFF);\n      tagData[offset++] = (byte) ((frameData.length >> 16) & 0xFF);\n      tagData[offset++] = (byte) ((frameData.length >> 8) & 0xFF);\n      tagData[offset++] = (byte) (frameData.length & 0xFF);\n      offset += 2; // Frame flags set to 0\n\n      // Fill in the frame data.\n      System.arraycopy(frameData, 0, tagData, offset, frameData.length);\n      offset += frameData.length;\n    }\n    return tagData;\n  }\n\n  /** Specify an ID3 frame. */\n  public static final class FrameSpec {\n    public final String frameId;\n    public final byte[] frameData;\n\n    public FrameSpec(String frameId, byte[] frameData) {\n      this.frameId = frameId;\n      this.frameData = frameData;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/MlltFrameTest.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.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link MlltFrame}. */\n@RunWith(AndroidJUnit4.class)\npublic final class MlltFrameTest {\n\n  @Test\n  public void testParcelable() {\n    MlltFrame mlltFrameToParcel =\n        new MlltFrame(\n            /* mpegFramesBetweenReference= */ 1,\n            /* bytesBetweenReference= */ 1,\n            /* millisecondsBetweenReference= */ 1,\n            /* bytesDeviations= */ new int[] {1, 2},\n            /* millisecondsDeviations= */ new int[] {1, 2});\n\n    Parcel parcel = Parcel.obtain();\n    mlltFrameToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    MlltFrame mlltFrameFromParcel = MlltFrame.CREATOR.createFromParcel(parcel);\n    assertThat(mlltFrameFromParcel).isEqualTo(mlltFrameToParcel);\n\n    parcel.recycle();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.android.exoplayer2.C.TIME_UNSET;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.MetadataInputBuffer;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport java.nio.ByteBuffer;\nimport java.util.List;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link SpliceInfoDecoder}. */\n@RunWith(AndroidJUnit4.class)\npublic final class SpliceInfoDecoderTest {\n\n  private SpliceInfoDecoder decoder;\n  private MetadataInputBuffer inputBuffer;\n\n  @Before\n  public void setUp() {\n    decoder = new SpliceInfoDecoder();\n    inputBuffer = new MetadataInputBuffer();\n  }\n\n  @Test\n  public void testWrappedAroundTimeSignalCommand() {\n    byte[] rawTimeSignalSection = new byte[] {\n        0, // table_id.\n        (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).\n        0x14, // section_length(8).\n        0x00, // protocol_version.\n        0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).\n        0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).\n        0x00, // cw_index.\n        0x00, // tier(8).\n        0x00, // tier(4), splice_command_length(4).\n        0x05, // splice_command_length(8).\n        0x06, // splice_command_type = time_signal.\n        // Start of splice_time().\n        (byte) 0x80, // time_specified_flag, reserved, pts_time(1).\n        0x52, 0x03, 0x02, (byte) 0x8f, // pts_time(32). PTS for a second after playback position.\n        0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).\n\n    // The playback position is 57:15:58.43 approximately.\n    // With this offset, the playback position pts before wrapping is 0x451ebf851.\n    Metadata metadata = feedInputBuffer(rawTimeSignalSection, 0x3000000000L, -0x50000L);\n    assertThat(metadata.length()).isEqualTo(1);\n    assertThat(((TimeSignalCommand) metadata.get(0)).playbackPositionUs)\n        .isEqualTo(removePtsConversionPrecisionError(0x3001000000L, inputBuffer.subsampleOffsetUs));\n  }\n\n  @Test\n  public void test2SpliceInsertCommands() {\n    byte[] rawSpliceInsertCommand1 = new byte[] {\n        0, // table_id.\n        (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).\n        0x19, // section_length(8).\n        0x00, // protocol_version.\n        0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).\n        0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).\n        0x00, // cw_index.\n        0x00, // tier(8).\n        0x00, // tier(4), splice_command_length(4).\n        0x0e, // splice_command_length(8).\n        0x05, // splice_command_type = splice_insert.\n        // Start of splice_insert().\n        0x00, 0x00, 0x00, 0x42, // splice_event_id.\n        0x00, // splice_event_cancel_indicator, reserved.\n        0x40, // out_of_network_indicator, program_splice_flag, duration_flag,\n              // splice_immediate_flag, reserved.\n        // start of splice_time().\n        (byte) 0x80, // time_specified_flag, reserved, pts_time(1).\n        0x00, 0x00, 0x00, 0x00, // PTS for playback position 3s.\n        0x00, 0x10, // unique_program_id.\n        0x01, // avail_num.\n        0x02, // avails_expected.\n        0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).\n\n    Metadata metadata = feedInputBuffer(rawSpliceInsertCommand1, 2000000, 3000000);\n    assertThat(metadata.length()).isEqualTo(1);\n    SpliceInsertCommand command = (SpliceInsertCommand) metadata.get(0);\n    assertThat(command.spliceEventId).isEqualTo(66);\n    assertThat(command.spliceEventCancelIndicator).isFalse();\n    assertThat(command.outOfNetworkIndicator).isFalse();\n    assertThat(command.programSpliceFlag).isTrue();\n    assertThat(command.spliceImmediateFlag).isFalse();\n    assertThat(command.programSplicePlaybackPositionUs).isEqualTo(3000000);\n    assertThat(command.breakDurationUs).isEqualTo(TIME_UNSET);\n    assertThat(command.uniqueProgramId).isEqualTo(16);\n    assertThat(command.availNum).isEqualTo(1);\n    assertThat(command.availsExpected).isEqualTo(2);\n\n    byte[] rawSpliceInsertCommand2 = new byte[] {\n        0, // table_id.\n        (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).\n        0x22, // section_length(8).\n        0x00, // protocol_version.\n        0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).\n        0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).\n        0x00, // cw_index.\n        0x00, // tier(8).\n        0x00, // tier(4), splice_command_length(4).\n        0x13, // splice_command_length(8).\n        0x05, // splice_command_type = splice_insert.\n        // Start of splice_insert().\n        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // splice_event_id.\n        0x00, // splice_event_cancel_indicator, reserved.\n        0x00, // out_of_network_indicator, program_splice_flag, duration_flag,\n              // splice_immediate_flag, reserved.\n        0x02, // component_count.\n        0x10, // component_tag.\n        // start of splice_time().\n        (byte) 0x81, // time_specified_flag, reserved, pts_time(1).\n        (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // PTS for playback position 10s.\n        // start of splice_time().\n        0x11, // component_tag.\n        0x00, // time_specified_flag, reserved.\n        0x00, 0x20, // unique_program_id.\n        0x01, // avail_num.\n        0x02, // avails_expected.\n        0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).\n\n    // By changing the subsample offset we force adjuster reconstruction.\n    long subsampleOffset = 1000011;\n    metadata = feedInputBuffer(rawSpliceInsertCommand2, 1000000, subsampleOffset);\n    assertThat(metadata.length()).isEqualTo(1);\n    command = (SpliceInsertCommand) metadata.get(0);\n    assertThat(command.spliceEventId).isEqualTo(0xffffffffL);\n    assertThat(command.spliceEventCancelIndicator).isFalse();\n    assertThat(command.outOfNetworkIndicator).isFalse();\n    assertThat(command.programSpliceFlag).isFalse();\n    assertThat(command.spliceImmediateFlag).isFalse();\n    assertThat(command.programSplicePlaybackPositionUs).isEqualTo(TIME_UNSET);\n    assertThat(command.breakDurationUs).isEqualTo(TIME_UNSET);\n    List<SpliceInsertCommand.ComponentSplice> componentSplices = command.componentSpliceList;\n    assertThat(componentSplices).hasSize(2);\n    assertThat(componentSplices.get(0).componentTag).isEqualTo(16);\n    assertThat(componentSplices.get(0).componentSplicePlaybackPositionUs).isEqualTo(1000000);\n    assertThat(componentSplices.get(1).componentTag).isEqualTo(17);\n    assertThat(componentSplices.get(1).componentSplicePts).isEqualTo(TIME_UNSET);\n    assertThat(command.uniqueProgramId).isEqualTo(32);\n    assertThat(command.availNum).isEqualTo(1);\n    assertThat(command.availsExpected).isEqualTo(2);\n  }\n\n  private Metadata feedInputBuffer(byte[] data, long timeUs, long subsampleOffset) {\n    inputBuffer.clear();\n    inputBuffer.data = ByteBuffer.allocate(data.length).put(data);\n    inputBuffer.timeUs = timeUs;\n    inputBuffer.subsampleOffsetUs = subsampleOffset;\n    return decoder.decode(inputBuffer);\n  }\n\n  private static long removePtsConversionPrecisionError(long timeUs, long offsetUs) {\n    return TimestampAdjuster.ptsToUs(TimestampAdjuster.usToPts(timeUs - offsetUs)) + offsetUs;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.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.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.util.Collections;\nimport org.junit.After;\nimport org.junit.Assert;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link ActionFile}. */\n@SuppressWarnings(\"deprecation\")\n@RunWith(AndroidJUnit4.class)\npublic class ActionFileTest {\n\n  private File tempFile;\n  private DownloadRequest expectedAction1;\n  private DownloadRequest expectedAction2;\n\n  @Before\n  public void setUp() throws Exception {\n    tempFile = Util.createTempFile(ApplicationProvider.getApplicationContext(), \"ExoPlayerTest\");\n    expectedAction1 =\n        buildExpectedRequest(Uri.parse(\"http://test1.uri\"), TestUtil.buildTestData(16));\n    expectedAction2 =\n        buildExpectedRequest(Uri.parse(\"http://test2.uri\"), TestUtil.buildTestData(32));\n  }\n\n  @After\n  public void tearDown() throws Exception {\n    tempFile.delete();\n  }\n\n  @Test\n  public void testLoadNoDataThrowsIOException() throws Exception {\n    ActionFile actionFile = getActionFile(\"offline/action_file_no_data.exi\");\n    try {\n      actionFile.load();\n      Assert.fail();\n    } catch (IOException e) {\n      // Expected exception.\n    }\n  }\n\n  @Test\n  public void testLoadIncompleteHeaderThrowsIOException() throws Exception {\n    ActionFile actionFile = getActionFile(\"offline/action_file_incomplete_header.exi\");\n    try {\n      actionFile.load();\n      Assert.fail();\n    } catch (IOException e) {\n      // Expected exception.\n    }\n  }\n\n  @Test\n  public void testLoadZeroActions() throws Exception {\n    ActionFile actionFile = getActionFile(\"offline/action_file_zero_actions.exi\");\n    DownloadRequest[] actions = actionFile.load();\n    assertThat(actions).isNotNull();\n    assertThat(actions).hasLength(0);\n  }\n\n  @Test\n  public void testLoadOneAction() throws Exception {\n    ActionFile actionFile = getActionFile(\"offline/action_file_one_action.exi\");\n    DownloadRequest[] actions = actionFile.load();\n    assertThat(actions).hasLength(1);\n    assertThat(actions[0]).isEqualTo(expectedAction1);\n  }\n\n  @Test\n  public void testLoadTwoActions() throws Exception {\n    ActionFile actionFile = getActionFile(\"offline/action_file_two_actions.exi\");\n    DownloadRequest[] actions = actionFile.load();\n    assertThat(actions).hasLength(2);\n    assertThat(actions[0]).isEqualTo(expectedAction1);\n    assertThat(actions[1]).isEqualTo(expectedAction2);\n  }\n\n  @Test\n  public void testLoadUnsupportedVersion() throws Exception {\n    ActionFile actionFile = getActionFile(\"offline/action_file_unsupported_version.exi\");\n    try {\n      actionFile.load();\n      Assert.fail();\n    } catch (IOException e) {\n      // Expected exception.\n    }\n  }\n\n  private ActionFile getActionFile(String fileName) throws IOException {\n    // Copy the test data from the asset to where the ActionFile expects it to be.\n    byte[] actionFileBytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), fileName);\n    try (FileOutputStream output = new FileOutputStream(tempFile)) {\n      output.write(actionFileBytes);\n    }\n    // Load the action file.\n    return new ActionFile(tempFile);\n  }\n\n  private static DownloadRequest buildExpectedRequest(Uri uri, byte[] data) {\n    return new DownloadRequest(\n        /* id= */ uri.toString(),\n        DownloadRequest.TYPE_PROGRESSIVE,\n        uri,\n        /* streamKeys= */ Collections.emptyList(),\n        /* customCacheKey= */ null,\n        data);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtilTest.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.DownloadRequest.TYPE_PROGRESSIVE;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.database.ExoDatabaseProvider;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.List;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link ActionFileUpgradeUtil}. */\n@RunWith(AndroidJUnit4.class)\npublic class ActionFileUpgradeUtilTest {\n\n  private static final long NOW_MS = 1234;\n\n  private File tempFile;\n  private ExoDatabaseProvider databaseProvider;\n  private DefaultDownloadIndex downloadIndex;\n\n  @Before\n  public void setUp() throws Exception {\n    tempFile = Util.createTempFile(ApplicationProvider.getApplicationContext(), \"ExoPlayerTest\");\n    databaseProvider = new ExoDatabaseProvider(ApplicationProvider.getApplicationContext());\n    downloadIndex = new DefaultDownloadIndex(databaseProvider);\n  }\n\n  @After\n  public void tearDown() {\n    databaseProvider.close();\n    tempFile.delete();\n  }\n\n  @Test\n  public void upgradeAndDelete_createsDownloads() throws IOException {\n    // Copy the test asset to a file.\n    byte[] actionFileBytes =\n        TestUtil.getByteArray(\n            ApplicationProvider.getApplicationContext(),\n            \"offline/action_file_for_download_index_upgrade.exi\");\n    try (FileOutputStream output = new FileOutputStream(tempFile)) {\n      output.write(actionFileBytes);\n    }\n\n    StreamKey expectedStreamKey1 =\n        new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5);\n    StreamKey expectedStreamKey2 =\n        new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2);\n    DownloadRequest expectedRequest1 =\n        new DownloadRequest(\n            \"key123\",\n            /* type= */ \"test\",\n            Uri.parse(\"https://www.test.com/download1\"),\n            asList(expectedStreamKey1),\n            /* customCacheKey= */ \"key123\",\n            new byte[] {1, 2, 3, 4});\n    DownloadRequest expectedRequest2 =\n        new DownloadRequest(\n            \"key234\",\n            /* type= */ \"test\",\n            Uri.parse(\"https://www.test.com/download2\"),\n            asList(expectedStreamKey2),\n            /* customCacheKey= */ \"key234\",\n            new byte[] {5, 4, 3, 2, 1});\n\n    ActionFileUpgradeUtil.upgradeAndDelete(\n        tempFile,\n        /* downloadIdProvider= */ null,\n        downloadIndex,\n        /* deleteOnFailure= */ true,\n        /* addNewDownloadsAsCompleted= */ false);\n\n    assertDownloadIndexContainsRequest(expectedRequest1, Download.STATE_QUEUED);\n    assertDownloadIndexContainsRequest(expectedRequest2, Download.STATE_QUEUED);\n  }\n\n  @Test\n  public void mergeRequest_nonExistingDownload_createsNewDownload() throws IOException {\n    byte[] data = new byte[] {1, 2, 3, 4};\n    DownloadRequest request =\n        new DownloadRequest(\n            \"id\",\n            TYPE_PROGRESSIVE,\n            Uri.parse(\"https://www.test.com/download\"),\n            asList(\n                new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2),\n                new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5)),\n            /* customCacheKey= */ \"key123\",\n            data);\n\n    ActionFileUpgradeUtil.mergeRequest(\n        request, downloadIndex, /* addNewDownloadAsCompleted= */ false, NOW_MS);\n\n    assertDownloadIndexContainsRequest(request, Download.STATE_QUEUED);\n  }\n\n  @Test\n  public void mergeRequest_existingDownload_createsMergedDownload() throws IOException {\n    StreamKey streamKey1 =\n        new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5);\n    StreamKey streamKey2 =\n        new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2);\n    DownloadRequest request1 =\n        new DownloadRequest(\n            \"id\",\n            TYPE_PROGRESSIVE,\n            Uri.parse(\"https://www.test.com/download1\"),\n            asList(streamKey1),\n            /* customCacheKey= */ \"key123\",\n            new byte[] {1, 2, 3, 4});\n    DownloadRequest request2 =\n        new DownloadRequest(\n            \"id\",\n            TYPE_PROGRESSIVE,\n            Uri.parse(\"https://www.test.com/download2\"),\n            asList(streamKey2),\n            /* customCacheKey= */ \"key123\",\n            new byte[] {5, 4, 3, 2, 1});\n    ActionFileUpgradeUtil.mergeRequest(\n        request1, downloadIndex, /* addNewDownloadAsCompleted= */ false, NOW_MS);\n    ActionFileUpgradeUtil.mergeRequest(\n        request2, downloadIndex, /* addNewDownloadAsCompleted= */ false, NOW_MS);\n\n    Download download = downloadIndex.getDownload(request2.id);\n    assertThat(download).isNotNull();\n    assertThat(download.request.type).isEqualTo(request2.type);\n    assertThat(download.request.customCacheKey).isEqualTo(request2.customCacheKey);\n    assertThat(download.request.data).isEqualTo(request2.data);\n    assertThat(download.request.uri).isEqualTo(request2.uri);\n    assertThat(download.request.streamKeys).containsExactly(streamKey1, streamKey2);\n    assertThat(download.state).isEqualTo(Download.STATE_QUEUED);\n  }\n\n  @Test\n  public void mergeRequest_addNewDownloadAsCompleted() throws IOException {\n    StreamKey streamKey1 =\n        new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5);\n    StreamKey streamKey2 =\n        new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2);\n    DownloadRequest request1 =\n        new DownloadRequest(\n            \"id1\",\n            TYPE_PROGRESSIVE,\n            Uri.parse(\"https://www.test.com/download1\"),\n            asList(streamKey1),\n            /* customCacheKey= */ \"key123\",\n            new byte[] {1, 2, 3, 4});\n    DownloadRequest request2 =\n        new DownloadRequest(\n            \"id2\",\n            TYPE_PROGRESSIVE,\n            Uri.parse(\"https://www.test.com/download2\"),\n            asList(streamKey2),\n            /* customCacheKey= */ \"key123\",\n            new byte[] {5, 4, 3, 2, 1});\n    ActionFileUpgradeUtil.mergeRequest(\n        request1, downloadIndex, /* addNewDownloadAsCompleted= */ false, NOW_MS);\n\n    // Merging existing download, keeps it queued.\n    ActionFileUpgradeUtil.mergeRequest(\n        request1, downloadIndex, /* addNewDownloadAsCompleted= */ true, NOW_MS);\n    assertThat(downloadIndex.getDownload(request1.id).state).isEqualTo(Download.STATE_QUEUED);\n\n    // New download is merged as completed.\n    ActionFileUpgradeUtil.mergeRequest(\n        request2, downloadIndex, /* addNewDownloadAsCompleted= */ true, NOW_MS);\n    assertThat(downloadIndex.getDownload(request2.id).state).isEqualTo(Download.STATE_COMPLETED);\n  }\n\n  private void assertDownloadIndexContainsRequest(DownloadRequest request, int state)\n      throws IOException {\n    Download download = downloadIndex.getDownload(request.id);\n    assertThat(download.request).isEqualTo(request);\n    assertThat(download.state).isEqualTo(state);\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private static List<StreamKey> asList(StreamKey... streamKeys) {\n    return Arrays.asList(streamKeys);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.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.common.truth.Truth.assertThat;\n\nimport android.database.sqlite.SQLiteDatabase;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.database.DatabaseIOException;\nimport com.google.android.exoplayer2.database.ExoDatabaseProvider;\nimport com.google.android.exoplayer2.database.VersionTable;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link DefaultDownloadIndex}. */\n@RunWith(AndroidJUnit4.class)\npublic class DefaultDownloadIndexTest {\n\n  private static final String EMPTY_NAME = \"\";\n\n  private ExoDatabaseProvider databaseProvider;\n  private DefaultDownloadIndex downloadIndex;\n\n  @Before\n  public void setUp() {\n    databaseProvider = new ExoDatabaseProvider(ApplicationProvider.getApplicationContext());\n    downloadIndex = new DefaultDownloadIndex(databaseProvider);\n  }\n\n  @After\n  public void tearDown() {\n    databaseProvider.close();\n  }\n\n  @Test\n  public void getDownload_nonExistingId_returnsNull() throws DatabaseIOException {\n    assertThat(downloadIndex.getDownload(\"non existing id\")).isNull();\n  }\n\n  @Test\n  public void addAndGetDownload_nonExistingId_returnsTheSameDownload() throws DatabaseIOException {\n    String id = \"id\";\n    Download download = new DownloadBuilder(id).build();\n\n    downloadIndex.putDownload(download);\n    Download readDownload = downloadIndex.getDownload(id);\n\n    assertEqual(readDownload, download);\n  }\n\n  @Test\n  public void addAndGetDownload_existingId_returnsUpdatedDownload() throws DatabaseIOException {\n    String id = \"id\";\n    DownloadBuilder downloadBuilder = new DownloadBuilder(id);\n    downloadIndex.putDownload(downloadBuilder.build());\n\n    Download download =\n        downloadBuilder\n            .setType(\"different type\")\n            .setUri(\"different uri\")\n            .setCacheKey(\"different cacheKey\")\n            .setState(Download.STATE_FAILED)\n            .setPercentDownloaded(50)\n            .setBytesDownloaded(200)\n            .setContentLength(400)\n            .setFailureReason(Download.FAILURE_REASON_UNKNOWN)\n            .setStopReason(0x12345678)\n            .setStartTimeMs(10)\n            .setUpdateTimeMs(20)\n            .setStreamKeys(\n                new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2),\n                new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5))\n            .setCustomMetadata(new byte[] {0, 1, 2, 3, 7, 8, 9, 10})\n            .build();\n    downloadIndex.putDownload(download);\n    Download readDownload = downloadIndex.getDownload(id);\n\n    assertThat(readDownload).isNotNull();\n    assertEqual(readDownload, download);\n  }\n\n  @Test\n  public void releaseAndRecreateDownloadIndex_returnsTheSameDownload() throws DatabaseIOException {\n    String id = \"id\";\n    Download download = new DownloadBuilder(id).build();\n    downloadIndex.putDownload(download);\n\n    downloadIndex = new DefaultDownloadIndex(databaseProvider);\n    Download readDownload = downloadIndex.getDownload(id);\n    assertThat(readDownload).isNotNull();\n    assertEqual(readDownload, download);\n  }\n\n  @Test\n  public void removeDownload_nonExistingId_doesNotFail() throws DatabaseIOException {\n    downloadIndex.removeDownload(\"non existing id\");\n  }\n\n  @Test\n  public void removeDownload_existingId_getDownloadReturnsNull() throws DatabaseIOException {\n    String id = \"id\";\n    Download download = new DownloadBuilder(id).build();\n    downloadIndex.putDownload(download);\n    downloadIndex.removeDownload(id);\n\n    Download readDownload = downloadIndex.getDownload(id);\n    assertThat(readDownload).isNull();\n  }\n\n  @Test\n  public void getDownloads_emptyDownloadIndex_returnsEmptyArray() throws DatabaseIOException {\n    assertThat(downloadIndex.getDownloads().getCount()).isEqualTo(0);\n  }\n\n  @Test\n  public void getDownloads_noState_returnsAllDownloadStatusSortedByStartTime()\n      throws DatabaseIOException {\n    Download download1 = new DownloadBuilder(\"id1\").setStartTimeMs(1).build();\n    downloadIndex.putDownload(download1);\n    Download download2 = new DownloadBuilder(\"id2\").setStartTimeMs(0).build();\n    downloadIndex.putDownload(download2);\n\n    try (DownloadCursor cursor = downloadIndex.getDownloads()) {\n      assertThat(cursor.getCount()).isEqualTo(2);\n      cursor.moveToNext();\n      assertEqual(cursor.getDownload(), download2);\n      cursor.moveToNext();\n      assertEqual(cursor.getDownload(), download1);\n    }\n  }\n\n  @Test\n  public void getDownloads_withStates_returnsAllDownloadStatusWithTheSameStates()\n      throws DatabaseIOException {\n    Download download1 =\n        new DownloadBuilder(\"id1\").setStartTimeMs(0).setState(Download.STATE_REMOVING).build();\n    downloadIndex.putDownload(download1);\n    Download download2 =\n        new DownloadBuilder(\"id2\").setStartTimeMs(1).setState(Download.STATE_STOPPED).build();\n    downloadIndex.putDownload(download2);\n    Download download3 =\n        new DownloadBuilder(\"id3\").setStartTimeMs(2).setState(Download.STATE_COMPLETED).build();\n    downloadIndex.putDownload(download3);\n\n    try (DownloadCursor cursor =\n        downloadIndex.getDownloads(Download.STATE_REMOVING, Download.STATE_COMPLETED)) {\n      assertThat(cursor.getCount()).isEqualTo(2);\n      cursor.moveToNext();\n      assertEqual(cursor.getDownload(), download1);\n      cursor.moveToNext();\n      assertEqual(cursor.getDownload(), download3);\n    }\n  }\n\n  @Test\n  public void putDownload_setsVersion() throws DatabaseIOException {\n    SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();\n    assertThat(VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, EMPTY_NAME))\n        .isEqualTo(VersionTable.VERSION_UNSET);\n\n    downloadIndex.putDownload(new DownloadBuilder(\"id1\").build());\n\n    assertThat(VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, EMPTY_NAME))\n        .isEqualTo(DefaultDownloadIndex.TABLE_VERSION);\n  }\n\n  @Test\n  public void downloadIndex_versionDowngradeWipesData() throws DatabaseIOException {\n    Download download1 = new DownloadBuilder(\"id1\").build();\n    downloadIndex.putDownload(download1);\n    DownloadCursor cursor = downloadIndex.getDownloads();\n    assertThat(cursor.getCount()).isEqualTo(1);\n    cursor.close();\n\n    SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();\n    VersionTable.setVersion(\n        writableDatabase, VersionTable.FEATURE_OFFLINE, EMPTY_NAME, Integer.MAX_VALUE);\n\n    downloadIndex = new DefaultDownloadIndex(databaseProvider);\n\n    cursor = downloadIndex.getDownloads();\n    assertThat(cursor.getCount()).isEqualTo(0);\n    cursor.close();\n    assertThat(VersionTable.getVersion(writableDatabase, VersionTable.FEATURE_OFFLINE, EMPTY_NAME))\n        .isEqualTo(DefaultDownloadIndex.TABLE_VERSION);\n  }\n\n  @Test\n  public void setStopReason_setReasonToNone() throws Exception {\n    String id = \"id\";\n    DownloadBuilder downloadBuilder =\n        new DownloadBuilder(id).setState(Download.STATE_COMPLETED).setStopReason(0x12345678);\n    Download download = downloadBuilder.build();\n    downloadIndex.putDownload(download);\n\n    downloadIndex.setStopReason(Download.STOP_REASON_NONE);\n\n    Download readDownload = downloadIndex.getDownload(id);\n    Download expectedDownload = downloadBuilder.setStopReason(Download.STOP_REASON_NONE).build();\n    assertEqual(readDownload, expectedDownload);\n  }\n\n  @Test\n  public void setStopReason_setReason() throws Exception {\n    String id = \"id\";\n    DownloadBuilder downloadBuilder =\n        new DownloadBuilder(id)\n            .setState(Download.STATE_FAILED)\n            .setFailureReason(Download.FAILURE_REASON_UNKNOWN);\n    Download download = downloadBuilder.build();\n    downloadIndex.putDownload(download);\n    int stopReason = 0x12345678;\n\n    downloadIndex.setStopReason(stopReason);\n\n    Download readDownload = downloadIndex.getDownload(id);\n    Download expectedDownload = downloadBuilder.setStopReason(stopReason).build();\n    assertEqual(readDownload, expectedDownload);\n  }\n\n  @Test\n  public void setStopReason_notTerminalState_doesNotSetStopReason() throws Exception {\n    String id = \"id\";\n    DownloadBuilder downloadBuilder = new DownloadBuilder(id).setState(Download.STATE_DOWNLOADING);\n    Download download = downloadBuilder.build();\n    downloadIndex.putDownload(download);\n    int notMetRequirements = 0x12345678;\n\n    downloadIndex.setStopReason(notMetRequirements);\n\n    Download readDownload = downloadIndex.getDownload(id);\n    assertEqual(readDownload, download);\n  }\n\n  @Test\n  public void setSingleDownloadStopReason_setReasonToNone() throws Exception {\n    String id = \"id\";\n    DownloadBuilder downloadBuilder =\n        new DownloadBuilder(id).setState(Download.STATE_COMPLETED).setStopReason(0x12345678);\n    Download download = downloadBuilder.build();\n    downloadIndex.putDownload(download);\n\n    downloadIndex.setStopReason(id, Download.STOP_REASON_NONE);\n\n    Download readDownload = downloadIndex.getDownload(id);\n    Download expectedDownload = downloadBuilder.setStopReason(Download.STOP_REASON_NONE).build();\n    assertEqual(readDownload, expectedDownload);\n  }\n\n  @Test\n  public void setSingleDownloadStopReason_setReason() throws Exception {\n    String id = \"id\";\n    DownloadBuilder downloadBuilder =\n        new DownloadBuilder(id)\n            .setState(Download.STATE_FAILED)\n            .setFailureReason(Download.FAILURE_REASON_UNKNOWN);\n    Download download = downloadBuilder.build();\n    downloadIndex.putDownload(download);\n    int stopReason = 0x12345678;\n\n    downloadIndex.setStopReason(id, stopReason);\n\n    Download readDownload = downloadIndex.getDownload(id);\n    Download expectedDownload = downloadBuilder.setStopReason(stopReason).build();\n    assertEqual(readDownload, expectedDownload);\n  }\n\n  @Test\n  public void setSingleDownloadStopReason_notTerminalState_doesNotSetStopReason() throws Exception {\n    String id = \"id\";\n    DownloadBuilder downloadBuilder = new DownloadBuilder(id).setState(Download.STATE_DOWNLOADING);\n    Download download = downloadBuilder.build();\n    downloadIndex.putDownload(download);\n    int notMetRequirements = 0x12345678;\n\n    downloadIndex.setStopReason(id, notMetRequirements);\n\n    Download readDownload = downloadIndex.getDownload(id);\n    assertEqual(readDownload, download);\n  }\n\n  private static void assertEqual(Download download, Download that) {\n    assertThat(download.request).isEqualTo(that.request);\n    assertThat(download.state).isEqualTo(that.state);\n    assertThat(download.startTimeMs).isEqualTo(that.startTimeMs);\n    assertThat(download.updateTimeMs).isEqualTo(that.updateTimeMs);\n    assertThat(download.contentLength).isEqualTo(that.contentLength);\n    assertThat(download.stopReason).isEqualTo(that.stopReason);\n    assertThat(download.failureReason).isEqualTo(that.failureReason);\n    assertThat(download.getPercentDownloaded()).isEqualTo(that.getPercentDownloaded());\n    assertThat(download.getBytesDownloaded()).isEqualTo(that.getBytesDownloaded());\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactoryTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.upstream.DummyDataSource;\nimport com.google.android.exoplayer2.upstream.cache.Cache;\nimport java.util.Collections;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mockito;\n\n/** Unit tests for {@link DefaultDownloaderFactory}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DefaultDownloaderFactoryTest {\n\n  @Test\n  public void createProgressiveDownloader() throws Exception {\n    DownloaderConstructorHelper constructorHelper =\n        new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);\n    DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);\n\n    Downloader downloader =\n        factory.createDownloader(\n            new DownloadRequest(\n                \"id\",\n                DownloadRequest.TYPE_PROGRESSIVE,\n                Uri.parse(\"https://www.test.com/download\"),\n                /* streamKeys= */ Collections.emptyList(),\n                /* customCacheKey= */ null,\n                /* data= */ null));\n    assertThat(downloader).isInstanceOf(ProgressiveDownloader.class);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadBuilder.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.net.Uri;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Builder for {@link Download}.\n *\n * <p>Defines default values for each field (except {@code id}) to facilitate {@link Download}\n * creation for tests. Tests must avoid depending on the default values but explicitly set tested\n * parameters during test initialization.\n */\n/* package */ final class DownloadBuilder {\n\n  private final DownloadProgress progress;\n\n  private String id;\n  private String type;\n  private Uri uri;\n  private List<StreamKey> streamKeys;\n  @Nullable private String cacheKey;\n  private byte[] customMetadata;\n\n  @Download.State private int state;\n  private long startTimeMs;\n  private long updateTimeMs;\n  private long contentLength;\n  private int stopReason;\n  private int failureReason;\n\n  /* package */ DownloadBuilder(String id) {\n    this(\n        id,\n        \"type\",\n        Uri.parse(\"uri\"),\n        /* streamKeys= */ Collections.emptyList(),\n        /* cacheKey= */ null,\n        new byte[0]);\n  }\n\n  /* package */ DownloadBuilder(DownloadRequest request) {\n    this(\n        request.id,\n        request.type,\n        request.uri,\n        request.streamKeys,\n        request.customCacheKey,\n        request.data);\n  }\n\n  /* package */ DownloadBuilder(\n      String id,\n      String type,\n      Uri uri,\n      List<StreamKey> streamKeys,\n      String cacheKey,\n      byte[] customMetadata) {\n    this.id = id;\n    this.type = type;\n    this.uri = uri;\n    this.streamKeys = streamKeys;\n    this.cacheKey = cacheKey;\n    this.customMetadata = customMetadata;\n    this.state = Download.STATE_QUEUED;\n    this.contentLength = C.LENGTH_UNSET;\n    this.failureReason = Download.FAILURE_REASON_NONE;\n    this.progress = new DownloadProgress();\n  }\n\n  public DownloadBuilder setId(String id) {\n    this.id = id;\n    return this;\n  }\n\n  public DownloadBuilder setType(String type) {\n    this.type = type;\n    return this;\n  }\n\n  public DownloadBuilder setUri(String uri) {\n    this.uri = Uri.parse(uri);\n    return this;\n  }\n\n  public DownloadBuilder setUri(Uri uri) {\n    this.uri = uri;\n    return this;\n  }\n\n  public DownloadBuilder setCacheKey(@Nullable String cacheKey) {\n    this.cacheKey = cacheKey;\n    return this;\n  }\n\n  public DownloadBuilder setState(@Download.State int state) {\n    this.state = state;\n    return this;\n  }\n\n  public DownloadBuilder setPercentDownloaded(float percentDownloaded) {\n    progress.percentDownloaded = percentDownloaded;\n    return this;\n  }\n\n  public DownloadBuilder setBytesDownloaded(long bytesDownloaded) {\n    progress.bytesDownloaded = bytesDownloaded;\n    return this;\n  }\n\n  public DownloadBuilder setContentLength(long contentLength) {\n    this.contentLength = contentLength;\n    return this;\n  }\n\n  public DownloadBuilder setFailureReason(int failureReason) {\n    this.failureReason = failureReason;\n    return this;\n  }\n\n  public DownloadBuilder setStopReason(int stopReason) {\n    this.stopReason = stopReason;\n    return this;\n  }\n\n  public DownloadBuilder setStartTimeMs(long startTimeMs) {\n    this.startTimeMs = startTimeMs;\n    return this;\n  }\n\n  public DownloadBuilder setUpdateTimeMs(long updateTimeMs) {\n    this.updateTimeMs = updateTimeMs;\n    return this;\n  }\n\n  public DownloadBuilder setStreamKeys(StreamKey... streamKeys) {\n    this.streamKeys = Arrays.asList(streamKeys);\n    return this;\n  }\n\n  public DownloadBuilder setCustomMetadata(byte[] customMetadata) {\n    this.customMetadata = customMetadata;\n    return this;\n  }\n\n  public Download build() {\n    DownloadRequest request =\n        new DownloadRequest(id, type, uri, streamKeys, cacheKey, customMetadata);\n    return new Download(\n        request,\n        state,\n        startTimeMs,\n        updateTimeMs,\n        contentLength,\n        stopReason,\n        failureReason,\n        progress);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.RenderersFactory;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.offline.DownloadHelper.Callback;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.testutil.FakeMediaPeriod;\nimport com.google.android.exoplayer2.testutil.FakeMediaSource;\nimport com.google.android.exoplayer2.testutil.FakeRenderer;\nimport com.google.android.exoplayer2.testutil.FakeTimeline;\nimport com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.util.ConditionVariable;\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.List;\nimport java.util.concurrent.atomic.AtomicReference;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\nimport org.robolectric.shadows.ShadowLooper;\n\n/** Unit tests for {@link DownloadHelper}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic class DownloadHelperTest {\n\n  private static final String TEST_DOWNLOAD_TYPE = \"downloadType\";\n  private static final String TEST_CACHE_KEY = \"cacheKey\";\n  private static final Timeline TEST_TIMELINE =\n      new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ new Object()));\n  private static final Object TEST_MANIFEST = new Object();\n\n  private static final Format VIDEO_FORMAT_LOW = createVideoFormat(/* bitrate= */ 200_000);\n  private static final Format VIDEO_FORMAT_HIGH = createVideoFormat(/* bitrate= */ 800_000);\n  private static final Format AUDIO_FORMAT_US = createAudioFormat(/* language= */ \"US\");\n  private static final Format AUDIO_FORMAT_ZH = createAudioFormat(/* language= */ \"ZH\");\n  private static final Format TEXT_FORMAT_US = createTextFormat(/* language= */ \"US\");\n  private static final Format TEXT_FORMAT_ZH = createTextFormat(/* language= */ \"ZH\");\n\n  private static final TrackGroup TRACK_GROUP_VIDEO_BOTH =\n      new TrackGroup(VIDEO_FORMAT_LOW, VIDEO_FORMAT_HIGH);\n  private static final TrackGroup TRACK_GROUP_VIDEO_SINGLE = new TrackGroup(VIDEO_FORMAT_LOW);\n  private static final TrackGroup TRACK_GROUP_AUDIO_US = new TrackGroup(AUDIO_FORMAT_US);\n  private static final TrackGroup TRACK_GROUP_AUDIO_ZH = new TrackGroup(AUDIO_FORMAT_ZH);\n  private static final TrackGroup TRACK_GROUP_TEXT_US = new TrackGroup(TEXT_FORMAT_US);\n  private static final TrackGroup TRACK_GROUP_TEXT_ZH = new TrackGroup(TEXT_FORMAT_ZH);\n  private static final TrackGroupArray TRACK_GROUP_ARRAY_ALL =\n      new TrackGroupArray(\n          TRACK_GROUP_VIDEO_BOTH,\n          TRACK_GROUP_AUDIO_US,\n          TRACK_GROUP_AUDIO_ZH,\n          TRACK_GROUP_TEXT_US,\n          TRACK_GROUP_TEXT_ZH);\n  private static final TrackGroupArray TRACK_GROUP_ARRAY_SINGLE =\n      new TrackGroupArray(TRACK_GROUP_VIDEO_SINGLE, TRACK_GROUP_AUDIO_US);\n  private static final TrackGroupArray[] TRACK_GROUP_ARRAYS =\n      new TrackGroupArray[] {TRACK_GROUP_ARRAY_ALL, TRACK_GROUP_ARRAY_SINGLE};\n\n  private Uri testUri;\n\n  private DownloadHelper downloadHelper;\n\n  @Before\n  public void setUp() {\n    testUri = Uri.parse(\"http://test.uri\");\n\n    FakeRenderer videoRenderer = new FakeRenderer(VIDEO_FORMAT_LOW, VIDEO_FORMAT_HIGH);\n    FakeRenderer audioRenderer = new FakeRenderer(AUDIO_FORMAT_US, AUDIO_FORMAT_ZH);\n    FakeRenderer textRenderer = new FakeRenderer(TEXT_FORMAT_US, TEXT_FORMAT_ZH);\n    RenderersFactory renderersFactory =\n        (handler, videoListener, audioListener, metadata, text, drm) ->\n            new Renderer[] {textRenderer, audioRenderer, videoRenderer};\n\n    downloadHelper =\n        new DownloadHelper(\n            TEST_DOWNLOAD_TYPE,\n            testUri,\n            TEST_CACHE_KEY,\n            new TestMediaSource(),\n            DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,\n            Util.getRendererCapabilities(renderersFactory, /* drmSessionManager= */ null));\n  }\n\n  @Test\n  public void getManifest_returnsManifest() throws Exception {\n    prepareDownloadHelper(downloadHelper);\n\n    Object manifest = downloadHelper.getManifest();\n\n    assertThat(manifest).isEqualTo(TEST_MANIFEST);\n  }\n\n  @Test\n  public void getPeriodCount_returnsPeriodCount() throws Exception {\n    prepareDownloadHelper(downloadHelper);\n\n    int periodCount = downloadHelper.getPeriodCount();\n\n    assertThat(periodCount).isEqualTo(2);\n  }\n\n  @Test\n  public void getTrackGroups_returnsTrackGroups() throws Exception {\n    prepareDownloadHelper(downloadHelper);\n\n    TrackGroupArray trackGroupArrayPeriod0 = downloadHelper.getTrackGroups(/* periodIndex= */ 0);\n    TrackGroupArray trackGroupArrayPeriod1 = downloadHelper.getTrackGroups(/* periodIndex= */ 1);\n\n    assertThat(trackGroupArrayPeriod0).isEqualTo(TRACK_GROUP_ARRAYS[0]);\n    assertThat(trackGroupArrayPeriod1).isEqualTo(TRACK_GROUP_ARRAYS[1]);\n  }\n\n  @Test\n  public void getMappedTrackInfo_returnsMappedTrackInfo() throws Exception {\n    prepareDownloadHelper(downloadHelper);\n\n    MappedTrackInfo mappedTracks0 = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);\n    MappedTrackInfo mappedTracks1 = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 1);\n\n    assertThat(mappedTracks0.getRendererCount()).isEqualTo(3);\n    assertThat(mappedTracks0.getRendererType(/* rendererIndex= */ 0)).isEqualTo(C.TRACK_TYPE_TEXT);\n    assertThat(mappedTracks0.getRendererType(/* rendererIndex= */ 1)).isEqualTo(C.TRACK_TYPE_AUDIO);\n    assertThat(mappedTracks0.getRendererType(/* rendererIndex= */ 2)).isEqualTo(C.TRACK_TYPE_VIDEO);\n    assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 0).length).isEqualTo(2);\n    assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 1).length).isEqualTo(2);\n    assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 2).length).isEqualTo(1);\n    assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 0).get(/* index= */ 0))\n        .isEqualTo(TRACK_GROUP_TEXT_US);\n    assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 0).get(/* index= */ 1))\n        .isEqualTo(TRACK_GROUP_TEXT_ZH);\n    assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 0))\n        .isEqualTo(TRACK_GROUP_AUDIO_US);\n    assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 1))\n        .isEqualTo(TRACK_GROUP_AUDIO_ZH);\n    assertThat(mappedTracks0.getTrackGroups(/* rendererIndex= */ 2).get(/* index= */ 0))\n        .isEqualTo(TRACK_GROUP_VIDEO_BOTH);\n\n    assertThat(mappedTracks1.getRendererCount()).isEqualTo(3);\n    assertThat(mappedTracks1.getRendererType(/* rendererIndex= */ 0)).isEqualTo(C.TRACK_TYPE_TEXT);\n    assertThat(mappedTracks1.getRendererType(/* rendererIndex= */ 1)).isEqualTo(C.TRACK_TYPE_AUDIO);\n    assertThat(mappedTracks1.getRendererType(/* rendererIndex= */ 2)).isEqualTo(C.TRACK_TYPE_VIDEO);\n    assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 0).length).isEqualTo(0);\n    assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 1).length).isEqualTo(1);\n    assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 2).length).isEqualTo(1);\n    assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 1).get(/* index= */ 0))\n        .isEqualTo(TRACK_GROUP_AUDIO_US);\n    assertThat(mappedTracks1.getTrackGroups(/* rendererIndex= */ 2).get(/* index= */ 0))\n        .isEqualTo(TRACK_GROUP_VIDEO_SINGLE);\n  }\n\n  @Test\n  public void getTrackSelections_returnsInitialSelection() throws Exception {\n    prepareDownloadHelper(downloadHelper);\n\n    List<TrackSelection> selectedText0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);\n    List<TrackSelection> selectedAudio0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);\n    List<TrackSelection> selectedVideo0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);\n    List<TrackSelection> selectedText1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);\n    List<TrackSelection> selectedAudio1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);\n    List<TrackSelection> selectedVideo1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);\n\n    assertSingleTrackSelectionEquals(selectedText0, TRACK_GROUP_TEXT_US, 0);\n    assertSingleTrackSelectionEquals(selectedAudio0, TRACK_GROUP_AUDIO_US, 0);\n    assertSingleTrackSelectionEquals(selectedVideo0, TRACK_GROUP_VIDEO_BOTH, 1);\n\n    assertThat(selectedText1).isEmpty();\n    assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0);\n    assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0);\n  }\n\n  @Test\n  public void getTrackSelections_afterClearTrackSelections_isEmpty() throws Exception {\n    prepareDownloadHelper(downloadHelper);\n\n    // Clear only one period selection to verify second period selection is untouched.\n    downloadHelper.clearTrackSelections(/* periodIndex= */ 0);\n    List<TrackSelection> selectedText0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);\n    List<TrackSelection> selectedAudio0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);\n    List<TrackSelection> selectedVideo0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);\n    List<TrackSelection> selectedText1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);\n    List<TrackSelection> selectedAudio1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);\n    List<TrackSelection> selectedVideo1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);\n\n    assertThat(selectedText0).isEmpty();\n    assertThat(selectedAudio0).isEmpty();\n    assertThat(selectedVideo0).isEmpty();\n\n    // Verify\n    assertThat(selectedText1).isEmpty();\n    assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0);\n    assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0);\n  }\n\n  @Test\n  public void getTrackSelections_afterReplaceTrackSelections_returnsNewSelections()\n      throws Exception {\n    prepareDownloadHelper(downloadHelper);\n    DefaultTrackSelector.Parameters parameters =\n        new ParametersBuilder()\n            .setPreferredAudioLanguage(\"ZH\")\n            .setPreferredTextLanguage(\"ZH\")\n            .setRendererDisabled(/* rendererIndex= */ 2, true)\n            .build();\n\n    // Replace only one period selection to verify second period selection is untouched.\n    downloadHelper.replaceTrackSelections(/* periodIndex= */ 0, parameters);\n    List<TrackSelection> selectedText0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);\n    List<TrackSelection> selectedAudio0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);\n    List<TrackSelection> selectedVideo0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);\n    List<TrackSelection> selectedText1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);\n    List<TrackSelection> selectedAudio1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);\n    List<TrackSelection> selectedVideo1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);\n\n    assertSingleTrackSelectionEquals(selectedText0, TRACK_GROUP_TEXT_ZH, 0);\n    assertSingleTrackSelectionEquals(selectedAudio0, TRACK_GROUP_AUDIO_ZH, 0);\n    assertThat(selectedVideo0).isEmpty();\n\n    assertThat(selectedText1).isEmpty();\n    assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0);\n    assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0);\n  }\n\n  @Test\n  public void getTrackSelections_afterAddTrackSelections_returnsCombinedSelections()\n      throws Exception {\n    prepareDownloadHelper(downloadHelper);\n    // Select parameters to require some merging of track groups because the new parameters add\n    // all video tracks to initial video single track selection.\n    DefaultTrackSelector.Parameters parameters =\n        new ParametersBuilder()\n            .setPreferredAudioLanguage(\"ZH\")\n            .setPreferredTextLanguage(\"US\")\n            .build();\n\n    // Add only to one period selection to verify second period selection is untouched.\n    downloadHelper.addTrackSelection(/* periodIndex= */ 0, parameters);\n    List<TrackSelection> selectedText0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);\n    List<TrackSelection> selectedAudio0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);\n    List<TrackSelection> selectedVideo0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);\n    List<TrackSelection> selectedText1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);\n    List<TrackSelection> selectedAudio1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);\n    List<TrackSelection> selectedVideo1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);\n\n    assertSingleTrackSelectionEquals(selectedText0, TRACK_GROUP_TEXT_US, 0);\n    assertThat(selectedAudio0).hasSize(2);\n    assertTrackSelectionEquals(selectedAudio0.get(0), TRACK_GROUP_AUDIO_US, 0);\n    assertTrackSelectionEquals(selectedAudio0.get(1), TRACK_GROUP_AUDIO_ZH, 0);\n    assertSingleTrackSelectionEquals(selectedVideo0, TRACK_GROUP_VIDEO_BOTH, 0, 1);\n\n    assertThat(selectedText1).isEmpty();\n    assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0);\n    assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0);\n  }\n\n  @Test\n  public void getTrackSelections_afterAddAudioLanguagesToSelection_returnsCombinedSelections()\n      throws Exception {\n    prepareDownloadHelper(downloadHelper);\n    downloadHelper.clearTrackSelections(/* periodIndex= */ 0);\n    downloadHelper.clearTrackSelections(/* periodIndex= */ 1);\n\n    // Add a non-default language, and a non-existing language (which will select the default).\n    downloadHelper.addAudioLanguagesToSelection(\"ZH\", \"Klingonese\");\n    List<TrackSelection> selectedText0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);\n    List<TrackSelection> selectedAudio0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);\n    List<TrackSelection> selectedVideo0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);\n    List<TrackSelection> selectedText1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);\n    List<TrackSelection> selectedAudio1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);\n    List<TrackSelection> selectedVideo1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);\n\n    assertThat(selectedVideo0).isEmpty();\n    assertThat(selectedText0).isEmpty();\n    assertThat(selectedAudio0).hasSize(2);\n    assertTrackSelectionEquals(selectedAudio0.get(0), TRACK_GROUP_AUDIO_ZH, 0);\n    assertTrackSelectionEquals(selectedAudio0.get(1), TRACK_GROUP_AUDIO_US, 0);\n\n    assertThat(selectedVideo1).isEmpty();\n    assertThat(selectedText1).isEmpty();\n    assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0);\n  }\n\n  @Test\n  public void getTrackSelections_afterAddTextLanguagesToSelection_returnsCombinedSelections()\n      throws Exception {\n    prepareDownloadHelper(downloadHelper);\n    downloadHelper.clearTrackSelections(/* periodIndex= */ 0);\n    downloadHelper.clearTrackSelections(/* periodIndex= */ 1);\n\n    // Add a non-default language, and a non-existing language (which will select the default).\n    downloadHelper.addTextLanguagesToSelection(\n        /* selectUndeterminedTextLanguage= */ true, \"ZH\", \"Klingonese\");\n    List<TrackSelection> selectedText0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);\n    List<TrackSelection> selectedAudio0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);\n    List<TrackSelection> selectedVideo0 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);\n    List<TrackSelection> selectedText1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);\n    List<TrackSelection> selectedAudio1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);\n    List<TrackSelection> selectedVideo1 =\n        downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);\n\n    assertThat(selectedVideo0).isEmpty();\n    assertThat(selectedAudio0).isEmpty();\n    assertThat(selectedText0).hasSize(2);\n    assertTrackSelectionEquals(selectedText0.get(0), TRACK_GROUP_TEXT_ZH, 0);\n    assertTrackSelectionEquals(selectedText0.get(1), TRACK_GROUP_TEXT_US, 0);\n\n    assertThat(selectedVideo1).isEmpty();\n    assertThat(selectedAudio1).isEmpty();\n    assertThat(selectedText1).isEmpty();\n  }\n\n  @Test\n  public void getDownloadRequest_createsDownloadRequest_withAllSelectedTracks() throws Exception {\n    prepareDownloadHelper(downloadHelper);\n    // Ensure we have track groups with multiple indices, renderers with multiple track groups and\n    // also renderers without any track groups.\n    DefaultTrackSelector.Parameters parameters =\n        new ParametersBuilder()\n            .setPreferredAudioLanguage(\"ZH\")\n            .setPreferredTextLanguage(\"US\")\n            .build();\n    downloadHelper.addTrackSelection(/* periodIndex= */ 0, parameters);\n    byte[] data = new byte[10];\n    Arrays.fill(data, (byte) 123);\n\n    DownloadRequest downloadRequest = downloadHelper.getDownloadRequest(data);\n\n    assertThat(downloadRequest.type).isEqualTo(TEST_DOWNLOAD_TYPE);\n    assertThat(downloadRequest.uri).isEqualTo(testUri);\n    assertThat(downloadRequest.customCacheKey).isEqualTo(TEST_CACHE_KEY);\n    assertThat(downloadRequest.data).isEqualTo(data);\n    assertThat(downloadRequest.streamKeys)\n        .containsExactly(\n            new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 0, /* trackIndex= */ 0),\n            new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 0, /* trackIndex= */ 1),\n            new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 0),\n            new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 2, /* trackIndex= */ 0),\n            new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 3, /* trackIndex= */ 0),\n            new StreamKey(/* periodIndex= */ 1, /* groupIndex= */ 0, /* trackIndex= */ 0),\n            new StreamKey(/* periodIndex= */ 1, /* groupIndex= */ 1, /* trackIndex= */ 0));\n  }\n\n  private static void prepareDownloadHelper(DownloadHelper downloadHelper) throws Exception {\n    AtomicReference<Exception> prepareException = new AtomicReference<>(null);\n    ConditionVariable preparedCondition = new ConditionVariable();\n    downloadHelper.prepare(\n        new Callback() {\n          @Override\n          public void onPrepared(DownloadHelper helper) {\n            preparedCondition.open();\n          }\n\n          @Override\n          public void onPrepareError(DownloadHelper helper, IOException e) {\n            prepareException.set(e);\n            preparedCondition.open();\n          }\n        });\n    while (!preparedCondition.block(0)) {\n      ShadowLooper.runMainLooperToNextTask();\n    }\n    if (prepareException.get() != null) {\n      throw prepareException.get();\n    }\n  }\n\n  private static Format createVideoFormat(int bitrate) {\n    return Format.createVideoSampleFormat(\n        /* id= */ null,\n        /* sampleMimeType= */ MimeTypes.VIDEO_H264,\n        /* codecs= */ null,\n        /* bitrate= */ bitrate,\n        /* maxInputSize= */ Format.NO_VALUE,\n        /* width= */ 480,\n        /* height= */ 360,\n        /* frameRate= */ Format.NO_VALUE,\n        /* initializationData= */ null,\n        /* drmInitData= */ null);\n  }\n\n  private static Format createAudioFormat(String language) {\n    return Format.createAudioSampleFormat(\n        /* id= */ null,\n        /* sampleMimeType= */ MimeTypes.AUDIO_AAC,\n        /* codecs= */ null,\n        /* bitrate= */ 48000,\n        /* maxInputSize= */ Format.NO_VALUE,\n        /* channelCount= */ 2,\n        /* sampleRate */ 44100,\n        /* initializationData= */ null,\n        /* drmInitData= */ null,\n        /* selectionFlags= */ C.SELECTION_FLAG_DEFAULT,\n        /* language= */ language);\n  }\n\n  private static Format createTextFormat(String language) {\n    return Format.createTextSampleFormat(\n        /* id= */ null,\n        /* sampleMimeType= */ MimeTypes.TEXT_VTT,\n        /* selectionFlags= */ C.SELECTION_FLAG_DEFAULT,\n        /* language= */ language);\n  }\n\n  private static void assertSingleTrackSelectionEquals(\n      List<TrackSelection> trackSelectionList, TrackGroup trackGroup, int... tracks) {\n    assertThat(trackSelectionList).hasSize(1);\n    assertTrackSelectionEquals(trackSelectionList.get(0), trackGroup, tracks);\n  }\n\n  private static void assertTrackSelectionEquals(\n      TrackSelection trackSelection, TrackGroup trackGroup, int... tracks) {\n    assertThat(trackSelection.getTrackGroup()).isEqualTo(trackGroup);\n    assertThat(trackSelection.length()).isEqualTo(tracks.length);\n    int[] selectedTracksInGroup = new int[trackSelection.length()];\n    for (int i = 0; i < trackSelection.length(); i++) {\n      selectedTracksInGroup[i] = trackSelection.getIndexInTrackGroup(i);\n    }\n    Arrays.sort(selectedTracksInGroup);\n    Arrays.sort(tracks);\n    assertThat(selectedTracksInGroup).isEqualTo(tracks);\n  }\n\n  private static final class TestMediaSource extends FakeMediaSource {\n\n    public TestMediaSource() {\n      super(TEST_TIMELINE, TEST_MANIFEST);\n    }\n\n    @Override\n    public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n      int periodIndex = TEST_TIMELINE.getIndexOfPeriod(id.periodUid);\n      return new FakeMediaPeriod(\n          TRACK_GROUP_ARRAYS[periodIndex],\n          new EventDispatcher()\n              .withParameters(/* windowIndex= */ 0, id, /* mediaTimeOffsetMs= */ 0)) {\n        @Override\n        public List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {\n          List<StreamKey> result = new ArrayList<>();\n          for (TrackSelection trackSelection : trackSelections) {\n            int groupIndex =\n                TRACK_GROUP_ARRAYS[periodIndex].indexOf(trackSelection.getTrackGroup());\n            for (int i = 0; i < trackSelection.length(); i++) {\n              result.add(\n                  new StreamKey(periodIndex, groupIndex, trackSelection.getIndexInTrackGroup(i)));\n            }\n          }\n          return result;\n        }\n      };\n    }\n\n    @Override\n    public void releasePeriod(MediaPeriod mediaPeriod) {\n      // Do nothing.\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.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.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.offline.Download.State;\nimport com.google.android.exoplayer2.scheduler.Requirements;\nimport com.google.android.exoplayer2.testutil.DummyMainThread;\nimport com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\nimport com.google.android.exoplayer2.testutil.TestDownloadManagerListener;\nimport com.google.android.exoplayer2.testutil.TestUtil;\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.concurrent.CountDownLatch;\nimport java.util.concurrent.TimeUnit;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.MockitoAnnotations;\nimport org.robolectric.annotation.Config;\nimport org.robolectric.shadows.ShadowLog;\n\n/** Tests {@link DownloadManager}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic class DownloadManagerTest {\n\n  /** Used to check if condition becomes true in this time interval. */\n  private static final int ASSERT_TRUE_TIMEOUT = 10000;\n  /** Used to check if condition stays false for this time interval. */\n  private static final int ASSERT_FALSE_TIME = 1000;\n  /** Maximum retry delay in DownloadManager. */\n  private static final int MAX_RETRY_DELAY = 5000;\n  /** Maximum number of times a downloader can be restarted before doing a released check. */\n  private static final int MAX_STARTS_BEFORE_RELEASED = 1;\n  /** A stop reason. */\n  private static final int APP_STOP_REASON = 1;\n  /** The minimum number of times a task must be retried before failing. */\n  private static final int MIN_RETRY_COUNT = 3;\n  /** Dummy value for the current time. */\n  private static final long NOW_MS = 1234;\n\n  private Uri uri1;\n  private Uri uri2;\n  private Uri uri3;\n  private DummyMainThread dummyMainThread;\n  private DefaultDownloadIndex downloadIndex;\n  private TestDownloadManagerListener downloadManagerListener;\n  private FakeDownloaderFactory downloaderFactory;\n  private DownloadManager downloadManager;\n\n  @Before\n  public void setUp() throws Exception {\n    ShadowLog.stream = System.out;\n    MockitoAnnotations.initMocks(this);\n    uri1 = Uri.parse(\"http://abc.com/media1\");\n    uri2 = Uri.parse(\"http://abc.com/media2\");\n    uri3 = Uri.parse(\"http://abc.com/media3\");\n    dummyMainThread = new DummyMainThread();\n    downloadIndex = new DefaultDownloadIndex(TestUtil.getTestDatabaseProvider());\n    downloaderFactory = new FakeDownloaderFactory();\n    setUpDownloadManager(100);\n  }\n\n  @After\n  public void tearDown() throws Exception {\n    releaseDownloadManager();\n    dummyMainThread.release();\n  }\n\n  @Test\n  public void downloadRunner_multipleInstancePerContent_throwsException() {\n    boolean exceptionThrown = false;\n    try {\n      new DownloadRunner(uri1);\n      new DownloadRunner(uri1);\n      // can't put fail() here as it would be caught in the catch below.\n    } catch (Throwable e) {\n      exceptionThrown = true;\n    }\n    assertThat(exceptionThrown).isTrue();\n  }\n\n  @Test\n  public void multipleRequestsForTheSameContent_executedOnTheSameTask() {\n    // Two download requests on first task\n    new DownloadRunner(uri1).postDownloadRequest().postDownloadRequest();\n    // One download, one remove requests on second task\n    new DownloadRunner(uri2).postDownloadRequest().postRemoveRequest();\n    // Two remove requests on third task\n    new DownloadRunner(uri3).postRemoveRequest().postRemoveRequest();\n  }\n\n  @Test\n  public void requestsForDifferentContent_executedOnDifferentTasks() {\n    TaskWrapper task1 = new DownloadRunner(uri1).postDownloadRequest().getTask();\n    TaskWrapper task2 = new DownloadRunner(uri2).postDownloadRequest().getTask();\n    TaskWrapper task3 = new DownloadRunner(uri3).postRemoveRequest().getTask();\n\n    assertThat(task1).isNoneOf(task2, task3);\n    assertThat(task2).isNotEqualTo(task3);\n  }\n\n  @Test\n  public void postDownloadRequest_downloads() throws Throwable {\n    DownloadRunner runner = new DownloadRunner(uri1);\n    TaskWrapper task = runner.postDownloadRequest().getTask();\n    task.assertDownloading();\n    runner.getDownloader(0).unblock().assertReleased().assertStartCount(1);\n    task.assertCompleted();\n    runner.assertCreatedDownloaderCount(1);\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n    assertThat(downloadManager.getCurrentDownloads()).isEmpty();\n  }\n\n  @Test\n  public void postRemoveRequest_removes() throws Throwable {\n    DownloadRunner runner = new DownloadRunner(uri1);\n    TaskWrapper task = runner.postDownloadRequest().postRemoveRequest().getTask();\n    task.assertRemoving();\n    runner.getDownloader(1).unblock().assertReleased().assertStartCount(1);\n    task.assertRemoved();\n    runner.assertCreatedDownloaderCount(2);\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n    assertThat(downloadManager.getCurrentDownloads()).isEmpty();\n  }\n\n  @Test\n  public void downloadFails_retriesThenTaskFails() throws Throwable {\n    DownloadRunner runner = new DownloadRunner(uri1);\n    runner.postDownloadRequest();\n    FakeDownloader downloader = runner.getDownloader(0);\n\n    for (int i = 0; i <= MIN_RETRY_COUNT; i++) {\n      downloader.assertStarted(MAX_RETRY_DELAY).fail();\n    }\n\n    downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1);\n    runner.getTask().assertFailed();\n    downloadManagerListener.blockUntilTasksComplete();\n    assertThat(downloadManager.getCurrentDownloads()).isEmpty();\n  }\n\n  @Test\n  public void downloadFails_retries() throws Throwable {\n    DownloadRunner runner = new DownloadRunner(uri1);\n    runner.postDownloadRequest();\n    FakeDownloader downloader = runner.getDownloader(0);\n\n    for (int i = 0; i < MIN_RETRY_COUNT; i++) {\n      downloader.assertStarted(MAX_RETRY_DELAY).fail();\n    }\n    downloader.assertStarted(MAX_RETRY_DELAY).unblock();\n\n    downloader.assertReleased().assertStartCount(MIN_RETRY_COUNT + 1);\n    runner.getTask().assertCompleted();\n    downloadManagerListener.blockUntilTasksComplete();\n    assertThat(downloadManager.getCurrentDownloads()).isEmpty();\n  }\n\n  @Test\n  public void downloadProgressOnRetry_retryCountResets() throws Throwable {\n    DownloadRunner runner = new DownloadRunner(uri1);\n    runner.postDownloadRequest();\n    FakeDownloader downloader = runner.getDownloader(0);\n\n    int tooManyRetries = MIN_RETRY_COUNT + 10;\n    for (int i = 0; i < tooManyRetries; i++) {\n      downloader.incrementBytesDownloaded();\n      downloader.assertStarted(MAX_RETRY_DELAY).fail();\n    }\n    downloader.assertStarted(MAX_RETRY_DELAY).unblock();\n\n    downloader.assertReleased().assertStartCount(tooManyRetries + 1);\n    runner.getTask().assertCompleted();\n    downloadManagerListener.blockUntilTasksComplete();\n  }\n\n  @Test\n  public void removeCancelsDownload() throws Throwable {\n    DownloadRunner runner = new DownloadRunner(uri1);\n    FakeDownloader downloader1 = runner.getDownloader(0);\n\n    runner.postDownloadRequest();\n    downloader1.assertStarted();\n    runner.postRemoveRequest();\n\n    downloader1.assertCanceled().assertStartCount(1);\n    runner.getDownloader(1).unblock().assertNotCanceled();\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n  }\n\n  @Test\n  public void downloadNotCancelRemove() throws Throwable {\n    DownloadRunner runner = new DownloadRunner(uri1);\n    FakeDownloader downloader1 = runner.getDownloader(1);\n\n    runner.postDownloadRequest().postRemoveRequest();\n    downloader1.assertStarted();\n    runner.postDownloadRequest();\n\n    downloader1.unblock().assertNotCanceled();\n    runner.getDownloader(2).unblock().assertNotCanceled();\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n  }\n\n  @Test\n  public void secondSameRemoveRequestIgnored() throws Throwable {\n    DownloadRunner runner = new DownloadRunner(uri1);\n    FakeDownloader downloader1 = runner.getDownloader(1);\n\n    runner.postDownloadRequest().postRemoveRequest();\n    downloader1.assertStarted();\n    runner.postRemoveRequest();\n\n    downloader1.unblock().assertNotCanceled();\n    runner.getTask().assertRemoved();\n    runner.assertCreatedDownloaderCount(2);\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n  }\n\n  @Test\n  public void removeAllDownloads_removesAllDownloads() throws Throwable {\n    // Finish one download and keep one running.\n    DownloadRunner runner1 = new DownloadRunner(uri1);\n    DownloadRunner runner2 = new DownloadRunner(uri2);\n    runner1.postDownloadRequest();\n    runner1.getDownloader(0).unblock();\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n    runner2.postDownloadRequest();\n\n    runner1.postRemoveAllRequest();\n    runner1.getDownloader(1).unblock();\n    runner2.getDownloader(1).unblock();\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n\n    runner1.getTask().assertRemoved();\n    runner2.getTask().assertRemoved();\n    assertThat(downloadManager.getCurrentDownloads()).isEmpty();\n    assertThat(downloadIndex.getDownloads().getCount()).isEqualTo(0);\n  }\n\n  @Test\n  public void differentDownloadRequestsMerged() throws Throwable {\n    DownloadRunner runner = new DownloadRunner(uri1);\n    FakeDownloader downloader1 = runner.getDownloader(0);\n\n    StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0);\n    StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1);\n\n    runner.postDownloadRequest(streamKey1);\n    downloader1.assertStarted();\n    runner.postDownloadRequest(streamKey2);\n\n    downloader1.assertCanceled();\n\n    FakeDownloader downloader2 = runner.getDownloader(1);\n    downloader2.assertStarted();\n    assertThat(downloader2.request.streamKeys).containsExactly(streamKey1, streamKey2);\n    downloader2.unblock();\n\n    runner.getTask().assertCompleted();\n    runner.assertCreatedDownloaderCount(2);\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n  }\n\n  @Test\n  public void requestsForDifferentContent_executedInParallel() throws Throwable {\n    DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadRequest();\n    DownloadRunner runner2 = new DownloadRunner(uri2).postDownloadRequest();\n    FakeDownloader downloader1 = runner1.getDownloader(0);\n    FakeDownloader downloader2 = runner2.getDownloader(0);\n\n    downloader1.assertStarted();\n    downloader2.assertStarted();\n    downloader1.unblock();\n    downloader2.unblock();\n\n    runner1.getTask().assertCompleted();\n    runner2.getTask().assertCompleted();\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n  }\n\n  @Test\n  public void requestsForDifferentContent_ifMaxDownloadIs1_executedSequentially() throws Throwable {\n    setUpDownloadManager(1);\n    DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadRequest();\n    DownloadRunner runner2 = new DownloadRunner(uri2).postDownloadRequest();\n    FakeDownloader downloader1 = runner1.getDownloader(0);\n    FakeDownloader downloader2 = runner2.getDownloader(0);\n\n    downloader1.assertStarted();\n    downloader2.assertDoesNotStart();\n    runner2.getTask().assertQueued();\n    downloader1.unblock();\n    downloader2.assertStarted();\n    downloader2.unblock();\n\n    runner1.getTask().assertCompleted();\n    runner2.getTask().assertCompleted();\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n  }\n\n  @Test\n  public void removeRequestForDifferentContent_ifMaxDownloadIs1_executedInParallel()\n      throws Throwable {\n    setUpDownloadManager(1);\n    DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadRequest();\n    DownloadRunner runner2 = new DownloadRunner(uri2).postDownloadRequest().postRemoveRequest();\n    FakeDownloader downloader1 = runner1.getDownloader(0);\n    FakeDownloader downloader2 = runner2.getDownloader(0);\n\n    downloader1.assertStarted();\n    downloader2.assertStarted();\n    downloader1.unblock();\n    downloader2.unblock();\n\n    runner1.getTask().assertCompleted();\n    runner2.getTask().assertRemoved();\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n  }\n\n  @Test\n  public void downloadRequestFollowingRemove_ifMaxDownloadIs1_isNotStarted() throws Throwable {\n    setUpDownloadManager(1);\n    DownloadRunner runner1 = new DownloadRunner(uri1).postDownloadRequest();\n    DownloadRunner runner2 = new DownloadRunner(uri2).postDownloadRequest().postRemoveRequest();\n    runner2.postDownloadRequest();\n    FakeDownloader downloader1 = runner1.getDownloader(0);\n    FakeDownloader downloader2 = runner2.getDownloader(0);\n    FakeDownloader downloader3 = runner2.getDownloader(1);\n\n    downloader1.assertStarted();\n    downloader2.assertStarted();\n    downloader2.unblock();\n    downloader3.assertDoesNotStart();\n    downloader1.unblock();\n    downloader3.assertStarted();\n    downloader3.unblock();\n\n    runner1.getTask().assertCompleted();\n    runner2.getTask().assertCompleted();\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n  }\n\n  @Test\n  public void getCurrentDownloads_returnsCurrentDownloads() {\n    TaskWrapper task1 = new DownloadRunner(uri1).postDownloadRequest().getTask();\n    TaskWrapper task2 = new DownloadRunner(uri2).postDownloadRequest().getTask();\n    TaskWrapper task3 =\n        new DownloadRunner(uri3).postDownloadRequest().postRemoveRequest().getTask();\n\n    task3.assertRemoving();\n    List<Download> downloads = downloadManager.getCurrentDownloads();\n\n    assertThat(downloads).hasSize(3);\n    String[] taskIds = {task1.taskId, task2.taskId, task3.taskId};\n    String[] downloadIds = {\n      downloads.get(0).request.id, downloads.get(1).request.id, downloads.get(2).request.id\n    };\n    assertThat(downloadIds).isEqualTo(taskIds);\n  }\n\n  @Test\n  public void pauseAndResume() throws Throwable {\n    DownloadRunner runner1 = new DownloadRunner(uri1);\n    DownloadRunner runner2 = new DownloadRunner(uri2);\n    DownloadRunner runner3 = new DownloadRunner(uri3);\n\n    runner1.postDownloadRequest().getTask().assertDownloading();\n    runner2.postDownloadRequest().postRemoveRequest().getTask().assertRemoving();\n    runner2.postDownloadRequest();\n\n    runOnMainThread(() -> downloadManager.pauseDownloads());\n\n    runner1.getTask().assertQueued();\n\n    // remove requests aren't stopped.\n    runner2.getDownloader(1).unblock().assertReleased();\n    runner2.getTask().assertQueued();\n    // Although remove2 is finished, download2 doesn't start.\n    runner2.getDownloader(2).assertDoesNotStart();\n\n    // When a new remove request is added, it cancels stopped download requests with the same media.\n    runner1.postRemoveRequest();\n    runner1.getDownloader(1).assertStarted().unblock();\n    runner1.getTask().assertRemoved();\n\n    // New download requests can be added but they don't start.\n    runner3.postDownloadRequest().getDownloader(0).assertDoesNotStart();\n\n    runOnMainThread(() -> downloadManager.resumeDownloads());\n\n    runner2.getDownloader(2).assertStarted().unblock();\n    runner3.getDownloader(0).assertStarted().unblock();\n\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n  }\n\n  @Test\n  public void setAndClearSingleDownloadStopReason() throws Throwable {\n    DownloadRunner runner = new DownloadRunner(uri1).postDownloadRequest();\n    TaskWrapper task = runner.getTask();\n\n    task.assertDownloading();\n\n    runOnMainThread(() -> downloadManager.setStopReason(task.taskId, APP_STOP_REASON));\n\n    task.assertStopped();\n\n    runOnMainThread(() -> downloadManager.setStopReason(task.taskId, Download.STOP_REASON_NONE));\n\n    runner.getDownloader(1).assertStarted().unblock();\n\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n  }\n\n  @Test\n  public void setSingleDownloadStopReasonThenRemove_removesDownload() throws Throwable {\n    DownloadRunner runner = new DownloadRunner(uri1).postDownloadRequest();\n    TaskWrapper task = runner.getTask();\n\n    task.assertDownloading();\n\n    runOnMainThread(() -> downloadManager.setStopReason(task.taskId, APP_STOP_REASON));\n\n    task.assertStopped();\n\n    runner.postRemoveRequest();\n    runner.getDownloader(1).assertStarted().unblock();\n    task.assertRemoved();\n\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n  }\n\n  @Test\n  public void setSingleDownloadStopReason_doesNotAffectOtherDownloads() throws Throwable {\n    DownloadRunner runner1 = new DownloadRunner(uri1);\n    DownloadRunner runner2 = new DownloadRunner(uri2);\n    DownloadRunner runner3 = new DownloadRunner(uri3);\n\n    runner1.postDownloadRequest().getTask().assertDownloading();\n    runner2.postDownloadRequest().postRemoveRequest().getTask().assertRemoving();\n\n    runOnMainThread(() -> downloadManager.setStopReason(runner1.getTask().taskId, APP_STOP_REASON));\n\n    runner1.getTask().assertStopped();\n\n    // Other downloads aren't affected.\n    runner2.getDownloader(1).unblock().assertReleased();\n\n    // New download requests can be added and they start.\n    runner3.postDownloadRequest().getDownloader(0).assertStarted().unblock();\n\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n  }\n\n  @Test\n  public void mergeRequest_removing_becomesRestarting() {\n    DownloadRequest downloadRequest = createDownloadRequest();\n    DownloadBuilder downloadBuilder =\n        new DownloadBuilder(downloadRequest).setState(Download.STATE_REMOVING);\n    Download download = downloadBuilder.build();\n\n    Download mergedDownload =\n        DownloadManager.mergeRequest(download, downloadRequest, download.stopReason, NOW_MS);\n\n    Download expectedDownload =\n        downloadBuilder.setStartTimeMs(NOW_MS).setState(Download.STATE_RESTARTING).build();\n    assertEqualIgnoringUpdateTime(mergedDownload, expectedDownload);\n  }\n\n  @Test\n  public void mergeRequest_failed_becomesQueued() {\n    DownloadRequest downloadRequest = createDownloadRequest();\n    DownloadBuilder downloadBuilder =\n        new DownloadBuilder(downloadRequest)\n            .setState(Download.STATE_FAILED)\n            .setFailureReason(Download.FAILURE_REASON_UNKNOWN);\n    Download download = downloadBuilder.build();\n\n    Download mergedDownload =\n        DownloadManager.mergeRequest(download, downloadRequest, download.stopReason, NOW_MS);\n\n    Download expectedDownload =\n        downloadBuilder\n            .setStartTimeMs(NOW_MS)\n            .setState(Download.STATE_QUEUED)\n            .setFailureReason(Download.FAILURE_REASON_NONE)\n            .build();\n    assertEqualIgnoringUpdateTime(mergedDownload, expectedDownload);\n  }\n\n  @Test\n  public void mergeRequest_stopped_staysStopped() {\n    DownloadRequest downloadRequest = createDownloadRequest();\n    DownloadBuilder downloadBuilder =\n        new DownloadBuilder(downloadRequest)\n            .setState(Download.STATE_STOPPED)\n            .setStopReason(/* stopReason= */ 1);\n    Download download = downloadBuilder.build();\n\n    Download mergedDownload =\n        DownloadManager.mergeRequest(download, downloadRequest, download.stopReason, NOW_MS);\n\n    assertEqualIgnoringUpdateTime(mergedDownload, download);\n  }\n\n  @Test\n  public void mergeRequest_completedWithStopReason_becomesStopped() {\n    DownloadRequest downloadRequest = createDownloadRequest();\n    DownloadBuilder downloadBuilder =\n        new DownloadBuilder(downloadRequest)\n            .setState(Download.STATE_COMPLETED)\n            .setStopReason(/* stopReason= */ 1);\n    Download download = downloadBuilder.build();\n\n    Download mergedDownload =\n        DownloadManager.mergeRequest(download, downloadRequest, download.stopReason, NOW_MS);\n\n    Download expectedDownload =\n        downloadBuilder.setStartTimeMs(NOW_MS).setState(Download.STATE_STOPPED).build();\n    assertEqualIgnoringUpdateTime(mergedDownload, expectedDownload);\n  }\n\n  private void setUpDownloadManager(final int maxParallelDownloads) throws Exception {\n    if (downloadManager != null) {\n      releaseDownloadManager();\n    }\n    try {\n      runOnMainThread(\n          () -> {\n            downloadManager =\n                new DownloadManager(\n                    ApplicationProvider.getApplicationContext(), downloadIndex, downloaderFactory);\n            downloadManager.setMaxParallelDownloads(maxParallelDownloads);\n            downloadManager.setMinRetryCount(MIN_RETRY_COUNT);\n            downloadManager.setRequirements(new Requirements(0));\n            downloadManager.resumeDownloads();\n            downloadManagerListener =\n                new TestDownloadManagerListener(downloadManager, dummyMainThread);\n          });\n      downloadManagerListener.waitUntilInitialized();\n    } catch (Throwable throwable) {\n      throw new Exception(throwable);\n    }\n  }\n\n  private void releaseDownloadManager() throws Exception {\n    try {\n      runOnMainThread(() -> downloadManager.release());\n    } catch (Throwable throwable) {\n      throw new Exception(throwable);\n    }\n  }\n\n  private void runOnMainThread(TestRunnable r) {\n    dummyMainThread.runTestOnMainThread(r);\n  }\n\n  private static void assertEqualIgnoringUpdateTime(Download download, Download that) {\n    assertThat(download.request).isEqualTo(that.request);\n    assertThat(download.state).isEqualTo(that.state);\n    assertThat(download.startTimeMs).isEqualTo(that.startTimeMs);\n    assertThat(download.contentLength).isEqualTo(that.contentLength);\n    assertThat(download.failureReason).isEqualTo(that.failureReason);\n    assertThat(download.stopReason).isEqualTo(that.stopReason);\n    assertThat(download.getPercentDownloaded()).isEqualTo(that.getPercentDownloaded());\n    assertThat(download.getBytesDownloaded()).isEqualTo(that.getBytesDownloaded());\n  }\n\n  private static DownloadRequest createDownloadRequest() {\n    return new DownloadRequest(\n        \"id\",\n        DownloadRequest.TYPE_DASH,\n        Uri.parse(\"https://www.test.com/download\"),\n        Collections.emptyList(),\n        /* customCacheKey= */ null,\n        /* data= */ null);\n  }\n\n  private final class DownloadRunner {\n\n    private final Uri uri;\n    private final String id;\n    private final ArrayList<FakeDownloader> downloaders;\n    private int createdDownloaderCount = 0;\n    private FakeDownloader downloader;\n    private final TaskWrapper taskWrapper;\n\n    private DownloadRunner(Uri uri) {\n      this.uri = uri;\n      id = uri.toString();\n      downloaders = new ArrayList<>();\n      downloader = addDownloader();\n      downloaderFactory.registerDownloadRunner(this);\n      taskWrapper = new TaskWrapper(id);\n    }\n\n    private DownloadRunner postRemoveRequest() {\n      runOnMainThread(() -> downloadManager.removeDownload(id));\n      return this;\n    }\n\n    private DownloadRunner postRemoveAllRequest() {\n      runOnMainThread(() -> downloadManager.removeAllDownloads());\n      return this;\n    }\n\n    private DownloadRunner postDownloadRequest(StreamKey... keys) {\n      DownloadRequest downloadRequest =\n          new DownloadRequest(\n              id,\n              DownloadRequest.TYPE_PROGRESSIVE,\n              uri,\n              Arrays.asList(keys),\n              /* customCacheKey= */ null,\n              /* data= */ null);\n      runOnMainThread(() -> downloadManager.addDownload(downloadRequest));\n      return this;\n    }\n\n    private synchronized FakeDownloader addDownloader() {\n      FakeDownloader fakeDownloader = new FakeDownloader();\n      downloaders.add(fakeDownloader);\n      return fakeDownloader;\n    }\n\n    private synchronized FakeDownloader getDownloader(int index) {\n      while (downloaders.size() <= index) {\n        addDownloader();\n      }\n      return downloaders.get(index);\n    }\n\n    private synchronized Downloader createDownloader(DownloadRequest request) {\n      downloader = getDownloader(createdDownloaderCount++);\n      downloader.request = request;\n      return downloader;\n    }\n\n    private TaskWrapper getTask() {\n      return taskWrapper;\n    }\n\n    private void assertCreatedDownloaderCount(int count) {\n      assertThat(createdDownloaderCount).isEqualTo(count);\n    }\n  }\n\n  private final class TaskWrapper {\n    private final String taskId;\n\n    private TaskWrapper(String taskId) {\n      this.taskId = taskId;\n    }\n\n    private TaskWrapper assertDownloading() {\n      return assertState(Download.STATE_DOWNLOADING);\n    }\n\n    private TaskWrapper assertCompleted() {\n      return assertState(Download.STATE_COMPLETED);\n    }\n\n    private TaskWrapper assertRemoving() {\n      return assertState(Download.STATE_REMOVING);\n    }\n\n    private TaskWrapper assertFailed() {\n      return assertState(Download.STATE_FAILED);\n    }\n\n    private TaskWrapper assertQueued() {\n      return assertState(Download.STATE_QUEUED);\n    }\n\n    private TaskWrapper assertStopped() {\n      return assertState(Download.STATE_STOPPED);\n    }\n\n    private TaskWrapper assertState(@State int expectedState) {\n      downloadManagerListener.assertState(taskId, expectedState, ASSERT_TRUE_TIMEOUT);\n      return this;\n    }\n\n    private TaskWrapper assertRemoved() {\n      downloadManagerListener.assertRemoved(taskId, ASSERT_TRUE_TIMEOUT);\n      return this;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n      if (this == o) {\n        return true;\n      }\n      if (o == null || getClass() != o.getClass()) {\n        return false;\n      }\n      return taskId.equals(((TaskWrapper) o).taskId);\n    }\n\n    @Override\n    public int hashCode() {\n      return taskId.hashCode();\n    }\n  }\n\n  private static final class FakeDownloaderFactory implements DownloaderFactory {\n\n    private final HashMap<Uri, DownloadRunner> downloaders;\n\n    public FakeDownloaderFactory() {\n      downloaders = new HashMap<>();\n    }\n\n    public void registerDownloadRunner(DownloadRunner downloadRunner) {\n      assertThat(downloaders.put(downloadRunner.uri, downloadRunner)).isNull();\n    }\n\n    @Override\n    public Downloader createDownloader(DownloadRequest request) {\n      return downloaders.get(request.uri).createDownloader(request);\n    }\n  }\n\n  private static final class FakeDownloader implements Downloader {\n\n    private final com.google.android.exoplayer2.util.ConditionVariable blocker;\n\n    private DownloadRequest request;\n    private CountDownLatch started;\n    private volatile boolean interrupted;\n    private volatile boolean cancelled;\n    private volatile boolean enableDownloadIOException;\n    private volatile int startCount;\n    private volatile int bytesDownloaded;\n\n    private FakeDownloader() {\n      this.started = new CountDownLatch(1);\n      this.blocker = new com.google.android.exoplayer2.util.ConditionVariable();\n    }\n\n    @SuppressWarnings({\"NonAtomicOperationOnVolatileField\", \"NonAtomicVolatileUpdate\"})\n    @Override\n    public void download(ProgressListener listener) throws InterruptedException, IOException {\n      // It's ok to update this directly as no other thread will update it.\n      startCount++;\n      started.countDown();\n      block();\n      if (bytesDownloaded > 0) {\n        listener.onProgress(C.LENGTH_UNSET, bytesDownloaded, C.PERCENTAGE_UNSET);\n      }\n      if (enableDownloadIOException) {\n        enableDownloadIOException = false;\n        throw new IOException();\n      }\n    }\n\n    @Override\n    public void cancel() {\n      cancelled = true;\n    }\n\n    @SuppressWarnings({\"NonAtomicOperationOnVolatileField\", \"NonAtomicVolatileUpdate\"})\n    @Override\n    public void remove() throws InterruptedException {\n      // It's ok to update this directly as no other thread will update it.\n      startCount++;\n      started.countDown();\n      block();\n    }\n\n    private void block() throws InterruptedException {\n      try {\n        while (true) {\n          try {\n            blocker.block();\n            break;\n          } catch (InterruptedException e) {\n            interrupted = true;\n            throw e;\n          }\n        }\n      } finally {\n        blocker.close();\n      }\n    }\n\n    private FakeDownloader assertStarted() throws InterruptedException {\n      return assertStarted(ASSERT_TRUE_TIMEOUT);\n    }\n\n    private FakeDownloader assertStarted(int timeout) throws InterruptedException {\n      assertThat(started.await(timeout, TimeUnit.MILLISECONDS)).isTrue();\n      started = new CountDownLatch(1);\n      return this;\n    }\n\n    private FakeDownloader assertStartCount(int count) {\n      assertThat(startCount).isEqualTo(count);\n      return this;\n    }\n\n    private FakeDownloader assertReleased() throws InterruptedException {\n      int count = 0;\n      while (started.await(ASSERT_TRUE_TIMEOUT, TimeUnit.MILLISECONDS)) {\n        if (count++ >= MAX_STARTS_BEFORE_RELEASED) {\n          fail();\n        }\n        started = new CountDownLatch(1);\n      }\n      return this;\n    }\n\n    private FakeDownloader assertCanceled() throws InterruptedException {\n      assertReleased();\n      assertThat(interrupted).isTrue();\n      assertThat(cancelled).isTrue();\n      return this;\n    }\n\n    private FakeDownloader assertNotCanceled() throws InterruptedException {\n      assertReleased();\n      assertThat(interrupted).isFalse();\n      assertThat(cancelled).isFalse();\n      return this;\n    }\n\n    private FakeDownloader unblock() {\n      blocker.open();\n      return this;\n    }\n\n    private FakeDownloader fail() {\n      enableDownloadIOException = true;\n      return unblock();\n    }\n\n    private void assertDoesNotStart() throws InterruptedException {\n      Thread.sleep(ASSERT_FALSE_TIME);\n      assertThat(started.getCount()).isEqualTo(1);\n    }\n\n    @SuppressWarnings({\"NonAtomicOperationOnVolatileField\", \"NonAtomicVolatileUpdate\"})\n    private void incrementBytesDownloaded() {\n      bytesDownloaded++;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadRequestTest.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 static com.google.android.exoplayer2.offline.DownloadRequest.TYPE_DASH;\nimport static com.google.android.exoplayer2.offline.DownloadRequest.TYPE_HLS;\nimport static com.google.android.exoplayer2.offline.DownloadRequest.TYPE_PROGRESSIVE;\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.net.Uri;\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link DownloadRequest}. */\n@RunWith(AndroidJUnit4.class)\npublic class DownloadRequestTest {\n\n  private Uri uri1;\n  private Uri uri2;\n\n  @Before\n  public void setUp() {\n    uri1 = Uri.parse(\"http://test/1.uri\");\n    uri2 = Uri.parse(\"http://test/2.uri\");\n  }\n\n  @Test\n  public void testMergeRequests_withDifferentIds_fails() {\n    DownloadRequest request1 =\n        new DownloadRequest(\n            \"id1\",\n            TYPE_DASH,\n            uri1,\n            /* streamKeys= */ Collections.emptyList(),\n            /* customCacheKey= */ null,\n            /* data= */ null);\n    DownloadRequest request2 =\n        new DownloadRequest(\n            \"id2\",\n            TYPE_DASH,\n            uri2,\n            /* streamKeys= */ Collections.emptyList(),\n            /* customCacheKey= */ null,\n            /* data= */ null);\n    try {\n      request1.copyWithMergedRequest(request2);\n      fail();\n    } catch (IllegalArgumentException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testMergeRequests_withDifferentTypes_fails() {\n    DownloadRequest request1 =\n        new DownloadRequest(\n            \"id1\",\n            TYPE_DASH,\n            uri1,\n            /* streamKeys= */ Collections.emptyList(),\n            /* customCacheKey= */ null,\n            /* data= */ null);\n    DownloadRequest request2 =\n        new DownloadRequest(\n            \"id1\",\n            TYPE_HLS,\n            uri1,\n            /* streamKeys= */ Collections.emptyList(),\n            /* customCacheKey= */ null,\n            /* data= */ null);\n    try {\n      request1.copyWithMergedRequest(request2);\n      fail();\n    } catch (IllegalArgumentException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testMergeRequest_withSameRequest() {\n    DownloadRequest request1 = createRequest(uri1, new StreamKey(0, 0, 0));\n\n    DownloadRequest mergedRequest = request1.copyWithMergedRequest(request1);\n    assertEqual(request1, mergedRequest);\n  }\n\n  @Test\n  public void testMergeRequests_withEmptyStreamKeys() {\n    DownloadRequest request1 = createRequest(uri1, new StreamKey(0, 0, 0));\n    DownloadRequest request2 = createRequest(uri1);\n\n    // If either of the requests have empty streamKeys, the merge should have empty streamKeys.\n    DownloadRequest mergedRequest = request1.copyWithMergedRequest(request2);\n    assertThat(mergedRequest.streamKeys).isEmpty();\n\n    mergedRequest = request2.copyWithMergedRequest(request1);\n    assertThat(mergedRequest.streamKeys).isEmpty();\n  }\n\n  @Test\n  public void testMergeRequests_withOverlappingStreamKeys() {\n    StreamKey streamKey1 = new StreamKey(0, 1, 2);\n    StreamKey streamKey2 = new StreamKey(3, 4, 5);\n    StreamKey streamKey3 = new StreamKey(6, 7, 8);\n    DownloadRequest request1 = createRequest(uri1, streamKey1, streamKey2);\n    DownloadRequest request2 = createRequest(uri1, streamKey2, streamKey3);\n\n    // Merged streamKeys should be in their original order without duplicates.\n    DownloadRequest mergedRequest = request1.copyWithMergedRequest(request2);\n    assertThat(mergedRequest.streamKeys).containsExactly(streamKey1, streamKey2, streamKey3);\n\n    mergedRequest = request2.copyWithMergedRequest(request1);\n    assertThat(mergedRequest.streamKeys).containsExactly(streamKey2, streamKey3, streamKey1);\n  }\n\n  @Test\n  public void testMergeRequests_withDifferentFields() {\n    byte[] data1 = new byte[] {0, 1, 2};\n    byte[] data2 = new byte[] {3, 4, 5};\n    DownloadRequest request1 =\n        new DownloadRequest(\n            \"id1\",\n            TYPE_PROGRESSIVE,\n            uri1,\n            /* streamKeys= */ Collections.emptyList(),\n            \"key1\",\n            /* data= */ data1);\n    DownloadRequest request2 =\n        new DownloadRequest(\n            \"id1\",\n            TYPE_PROGRESSIVE,\n            uri2,\n            /* streamKeys= */ Collections.emptyList(),\n            \"key2\",\n            /* data= */ data2);\n\n    // uri, customCacheKey and data should be from the request being merged.\n    DownloadRequest mergedRequest = request1.copyWithMergedRequest(request2);\n    assertThat(mergedRequest.uri).isEqualTo(uri2);\n    assertThat(mergedRequest.customCacheKey).isEqualTo(\"key2\");\n    assertThat(mergedRequest.data).isEqualTo(data2);\n\n    mergedRequest = request2.copyWithMergedRequest(request1);\n    assertThat(mergedRequest.uri).isEqualTo(uri1);\n    assertThat(mergedRequest.customCacheKey).isEqualTo(\"key1\");\n    assertThat(mergedRequest.data).isEqualTo(data1);\n  }\n\n  @Test\n  public void testParcelable() {\n    ArrayList<StreamKey> streamKeys = new ArrayList<>();\n    streamKeys.add(new StreamKey(1, 2, 3));\n    streamKeys.add(new StreamKey(4, 5, 6));\n    DownloadRequest requestToParcel =\n        new DownloadRequest(\n            \"id\",\n            \"type\",\n            Uri.parse(\"https://abc.def/ghi\"),\n            streamKeys,\n            \"key\",\n            new byte[] {1, 2, 3, 4, 5});\n    Parcel parcel = Parcel.obtain();\n    requestToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    DownloadRequest requestFromParcel = DownloadRequest.CREATOR.createFromParcel(parcel);\n    assertThat(requestFromParcel).isEqualTo(requestToParcel);\n\n    parcel.recycle();\n  }\n\n  @SuppressWarnings(\"EqualsWithItself\")\n  @Test\n  public void testEquals() {\n    DownloadRequest request1 = createRequest(uri1);\n    assertThat(request1.equals(request1)).isTrue();\n\n    DownloadRequest request2 = createRequest(uri1);\n    DownloadRequest request3 = createRequest(uri1);\n    assertEqual(request2, request3);\n\n    DownloadRequest request4 = createRequest(uri1);\n    DownloadRequest request5 = createRequest(uri1, new StreamKey(0, 0, 0));\n    assertNotEqual(request4, request5);\n\n    DownloadRequest request6 = createRequest(uri1, new StreamKey(0, 1, 1));\n    DownloadRequest request7 = createRequest(uri1, new StreamKey(0, 0, 0));\n    assertNotEqual(request6, request7);\n\n    DownloadRequest request8 = createRequest(uri1);\n    DownloadRequest request9 = createRequest(uri2);\n    assertNotEqual(request8, request9);\n\n    DownloadRequest request10 = createRequest(uri1, new StreamKey(0, 0, 0), new StreamKey(0, 1, 1));\n    DownloadRequest request11 = createRequest(uri1, new StreamKey(0, 1, 1), new StreamKey(0, 0, 0));\n    assertEqual(request10, request11);\n\n    DownloadRequest request12 = createRequest(uri1, new StreamKey(0, 0, 0));\n    DownloadRequest request13 = createRequest(uri1, new StreamKey(0, 1, 1), new StreamKey(0, 0, 0));\n    assertNotEqual(request12, request13);\n\n    DownloadRequest request14 = createRequest(uri1);\n    DownloadRequest request15 = createRequest(uri1);\n    assertEqual(request14, request15);\n  }\n\n  private static void assertNotEqual(DownloadRequest request1, DownloadRequest request2) {\n    assertThat(request1).isNotEqualTo(request2);\n    assertThat(request2).isNotEqualTo(request1);\n  }\n\n  private static void assertEqual(DownloadRequest request1, DownloadRequest request2) {\n    assertThat(request1).isEqualTo(request2);\n    assertThat(request2).isEqualTo(request1);\n  }\n\n  private static DownloadRequest createRequest(Uri uri, StreamKey... keys) {\n    return new DownloadRequest(\n        uri.toString(), TYPE_DASH, uri, toList(keys), /* customCacheKey= */ null, /* data= */ null);\n  }\n\n  private static List<StreamKey> toList(StreamKey... keys) {\n    ArrayList<StreamKey> keysList = new ArrayList<>();\n    Collections.addAll(keysList, keys);\n    return keysList;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/offline/StreamKeyTest.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.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link StreamKey}. */\n@RunWith(AndroidJUnit4.class)\npublic class StreamKeyTest {\n\n  @Test\n  public void testParcelable() {\n    StreamKey streamKeyToParcel = new StreamKey(1, 2, 3);\n    Parcel parcel = Parcel.obtain();\n    streamKeyToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    StreamKey streamKeyFromParcel = StreamKey.CREATOR.createFromParcel(parcel);\n    assertThat(streamKeyFromParcel).isEqualTo(streamKeyToParcel);\n\n    parcel.recycle();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.os.Handler;\nimport androidx.annotation.Nullable;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\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.source.ClippingMediaSource.IllegalClippingException;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;\nimport com.google.android.exoplayer2.testutil.FakeMediaPeriod;\nimport com.google.android.exoplayer2.testutil.FakeMediaSource;\nimport com.google.android.exoplayer2.testutil.FakeTimeline;\nimport com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;\nimport com.google.android.exoplayer2.testutil.MediaSourceTestRunner;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\nimport com.google.android.exoplayer2.testutil.TimelineAsserts;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport java.io.IOException;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/** Unit tests for {@link ClippingMediaSource}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic final class ClippingMediaSourceTest {\n\n  private static final long TEST_PERIOD_DURATION_US = 1000000;\n  private static final long TEST_CLIP_AMOUNT_US = 300000;\n\n  private Window window;\n  private Period period;\n\n  @Before\n  public void setUp() throws Exception {\n    window = new Timeline.Window();\n    period = new Timeline.Period();\n  }\n\n  @Test\n  public void testNoClipping() throws IOException {\n    Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false);\n\n    Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);\n\n    assertThat(clippedTimeline.getWindowCount()).isEqualTo(1);\n    assertThat(clippedTimeline.getPeriodCount()).isEqualTo(1);\n    assertThat(clippedTimeline.getWindow(0, window).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US);\n    assertThat(clippedTimeline.getPeriod(0, period).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US);\n  }\n\n  @Test\n  public void testClippingUnseekableWindowThrows() throws IOException {\n    Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, false, false);\n\n    // If the unseekable window isn't clipped, clipping succeeds.\n    getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);\n    try {\n      // If the unseekable window is clipped, clipping fails.\n      getClippedTimeline(timeline, 1, TEST_PERIOD_DURATION_US);\n      fail(\"Expected clipping to fail.\");\n    } catch (IllegalClippingException e) {\n      assertThat(e.reason).isEqualTo(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START);\n    }\n  }\n\n  @Test\n  public void testClippingStart() throws IOException {\n    Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false);\n\n    Timeline clippedTimeline =\n        getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US);\n    assertThat(clippedTimeline.getWindow(0, window).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimeline.getPeriod(0, period).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US);\n  }\n\n  @Test\n  public void testClippingEnd() throws IOException {\n    Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false);\n\n    Timeline clippedTimeline =\n        getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimeline.getWindow(0, window).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimeline.getPeriod(0, period).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);\n  }\n\n  @Test\n  public void testClippingStartAndEndInitial() throws IOException {\n    // Timeline that's dynamic and not seekable. A child source might report such a timeline prior\n    // to it having loaded sufficient data to establish its duration and seekability. Such timelines\n    // should not result in clipping failure.\n    Timeline timeline =\n        new SinglePeriodTimeline(C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ true);\n\n    Timeline clippedTimeline =\n        getClippedTimeline(\n            timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2);\n    assertThat(clippedTimeline.getWindow(0, window).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3);\n    assertThat(clippedTimeline.getPeriod(0, period).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2);\n  }\n\n  @Test\n  public void testClippingToEndOfSourceWithDurationSetsDuration() throws IOException {\n    // Create a child timeline that has a known duration.\n    Timeline timeline =\n        new SinglePeriodTimeline(\n            /* durationUs= */ TEST_PERIOD_DURATION_US,\n            /* isSeekable= */ true,\n            /* isDynamic= */ false);\n\n    // When clipping to the end, the clipped timeline should also have a duration.\n    Timeline clippedTimeline =\n        getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, C.TIME_END_OF_SOURCE);\n    assertThat(clippedTimeline.getWindow(/* windowIndex= */ 0, window).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);\n  }\n\n  @Test\n  public void testClippingToEndOfSourceWithUnsetDurationDoesNotSetDuration() throws IOException {\n    // Create a child timeline that has an unknown duration.\n    Timeline timeline =\n        new SinglePeriodTimeline(\n            /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ true, /* isDynamic= */ false);\n\n    // When clipping to the end, the clipped timeline should also have an unset duration.\n    Timeline clippedTimeline =\n        getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, C.TIME_END_OF_SOURCE);\n    assertThat(clippedTimeline.getWindow(/* windowIndex= */ 0, window).getDurationUs())\n        .isEqualTo(C.TIME_UNSET);\n  }\n\n  @Test\n  public void testClippingStartAndEnd() throws IOException {\n    Timeline timeline = new SinglePeriodTimeline(TEST_PERIOD_DURATION_US, true, false);\n\n    Timeline clippedTimeline =\n        getClippedTimeline(\n            timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2);\n    assertThat(clippedTimeline.getWindow(0, window).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3);\n    assertThat(clippedTimeline.getPeriod(0, period).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2);\n  }\n\n  @Test\n  public void testClippingFromDefaultPosition() throws IOException {\n    Timeline timeline =\n        new SinglePeriodTimeline(\n            /* periodDurationUs= */ 3 * TEST_PERIOD_DURATION_US,\n            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,\n            /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US,\n            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,\n            /* isSeekable= */ true,\n            /* isDynamic= */ true,\n            /* tag= */ null);\n\n    Timeline clippedTimeline = getClippedTimeline(timeline, /* durationUs= */ TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimeline.getWindow(0, window).getDurationUs()).isEqualTo(TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimeline.getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);\n    assertThat(clippedTimeline.getWindow(0, window).getPositionInFirstPeriodUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimeline.getPeriod(0, period).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US + 2 * TEST_CLIP_AMOUNT_US);\n  }\n\n  @Test\n  public void testAllowDynamicUpdatesWithOverlappingLiveWindow() throws IOException {\n    Timeline timeline1 =\n        new SinglePeriodTimeline(\n            /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US,\n            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,\n            /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US,\n            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,\n            /* isSeekable= */ true,\n            /* isDynamic= */ true,\n            /* tag= */ null);\n    Timeline timeline2 =\n        new SinglePeriodTimeline(\n            /* periodDurationUs= */ 3 * TEST_PERIOD_DURATION_US,\n            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,\n            /* windowPositionInPeriodUs= */ 2 * TEST_PERIOD_DURATION_US,\n            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,\n            /* isSeekable= */ true,\n            /* isDynamic= */ true,\n            /* tag= */ null);\n\n    Timeline[] clippedTimelines =\n        getClippedTimelines(\n            /* startUs= */ 0,\n            /* endUs= */ TEST_PERIOD_DURATION_US,\n            /* allowDynamicUpdates= */ true,\n            /* fromDefaultPosition= */ true,\n            timeline1,\n            timeline2);\n    assertThat(clippedTimelines[0].getWindow(0, window).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[0].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);\n    assertThat(clippedTimelines[0].getWindow(0, window).isDynamic).isTrue();\n    assertThat(clippedTimelines[0].getWindow(0, window).getPositionInFirstPeriodUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[0].getPeriod(0, period).getDurationUs())\n        .isEqualTo(2 * TEST_PERIOD_DURATION_US);\n    assertThat(clippedTimelines[1].getWindow(0, window).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[1].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);\n    assertThat(clippedTimelines[1].getWindow(0, window).isDynamic).isTrue();\n    assertThat(clippedTimelines[1].getWindow(0, window).getPositionInFirstPeriodUs())\n        .isEqualTo(2 * TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[1].getPeriod(0, period).getDurationUs())\n        .isEqualTo(3 * TEST_PERIOD_DURATION_US);\n  }\n\n  @Test\n  public void testAllowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOException {\n    Timeline timeline1 =\n        new SinglePeriodTimeline(\n            /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US,\n            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,\n            /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US,\n            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,\n            /* isSeekable= */ true,\n            /* isDynamic= */ true,\n            /* tag= */ null);\n    Timeline timeline2 =\n        new SinglePeriodTimeline(\n            /* periodDurationUs= */ 4 * TEST_PERIOD_DURATION_US,\n            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,\n            /* windowPositionInPeriodUs= */ 3 * TEST_PERIOD_DURATION_US,\n            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,\n            /* isSeekable= */ true,\n            /* isDynamic= */ true,\n            /* tag= */ null);\n\n    Timeline[] clippedTimelines =\n        getClippedTimelines(\n            /* startUs= */ 0,\n            /* endUs= */ TEST_PERIOD_DURATION_US,\n            /* allowDynamicUpdates= */ true,\n            /* fromDefaultPosition= */ true,\n            timeline1,\n            timeline2);\n    assertThat(clippedTimelines[0].getWindow(0, window).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[0].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);\n    assertThat(clippedTimelines[0].getWindow(0, window).isDynamic).isTrue();\n    assertThat(clippedTimelines[0].getWindow(0, window).getPositionInFirstPeriodUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[0].getPeriod(0, period).getDurationUs())\n        .isEqualTo(2 * TEST_PERIOD_DURATION_US);\n    assertThat(clippedTimelines[1].getWindow(0, window).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[1].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);\n    assertThat(clippedTimelines[1].getWindow(0, window).isDynamic).isTrue();\n    assertThat(clippedTimelines[1].getWindow(0, window).getPositionInFirstPeriodUs())\n        .isEqualTo(3 * TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[1].getPeriod(0, period).getDurationUs())\n        .isEqualTo(4 * TEST_PERIOD_DURATION_US);\n  }\n\n  @Test\n  public void testDisallowDynamicUpdatesWithOverlappingLiveWindow() throws IOException {\n    Timeline timeline1 =\n        new SinglePeriodTimeline(\n            /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US,\n            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,\n            /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US,\n            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,\n            /* isSeekable= */ true,\n            /* isDynamic= */ true,\n            /* tag= */ null);\n    Timeline timeline2 =\n        new SinglePeriodTimeline(\n            /* periodDurationUs= */ 3 * TEST_PERIOD_DURATION_US,\n            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,\n            /* windowPositionInPeriodUs= */ 2 * TEST_PERIOD_DURATION_US,\n            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,\n            /* isSeekable= */ true,\n            /* isDynamic= */ true,\n            /* tag= */ null);\n\n    Timeline[] clippedTimelines =\n        getClippedTimelines(\n            /* startUs= */ 0,\n            /* endUs= */ TEST_PERIOD_DURATION_US,\n            /* allowDynamicUpdates= */ false,\n            /* fromDefaultPosition= */ true,\n            timeline1,\n            timeline2);\n    assertThat(clippedTimelines[0].getWindow(0, window).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[0].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);\n    assertThat(clippedTimelines[0].getWindow(0, window).isDynamic).isTrue();\n    assertThat(clippedTimelines[0].getWindow(0, window).getPositionInFirstPeriodUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[0].getPeriod(0, period).getDurationUs())\n        .isEqualTo(2 * TEST_PERIOD_DURATION_US);\n    assertThat(clippedTimelines[1].getWindow(0, window).getDurationUs())\n        .isEqualTo(TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[1].getWindow(0, window).getDefaultPositionUs())\n        .isEqualTo(TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[1].getWindow(0, window).isDynamic).isFalse();\n    assertThat(clippedTimelines[1].getWindow(0, window).getPositionInFirstPeriodUs())\n        .isEqualTo(2 * TEST_PERIOD_DURATION_US);\n    assertThat(clippedTimelines[1].getPeriod(0, period).getDurationUs())\n        .isEqualTo(2 * TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);\n  }\n\n  @Test\n  public void testDisallowDynamicUpdatesWithNonOverlappingLiveWindow() throws IOException {\n    Timeline timeline1 =\n        new SinglePeriodTimeline(\n            /* periodDurationUs= */ 2 * TEST_PERIOD_DURATION_US,\n            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,\n            /* windowPositionInPeriodUs= */ TEST_PERIOD_DURATION_US,\n            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,\n            /* isSeekable= */ true,\n            /* isDynamic= */ true,\n            /* tag= */ null);\n    Timeline timeline2 =\n        new SinglePeriodTimeline(\n            /* periodDurationUs= */ 4 * TEST_PERIOD_DURATION_US,\n            /* windowDurationUs= */ TEST_PERIOD_DURATION_US,\n            /* windowPositionInPeriodUs= */ 3 * TEST_PERIOD_DURATION_US,\n            /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US,\n            /* isSeekable= */ true,\n            /* isDynamic= */ true,\n            /* tag= */ null);\n\n    Timeline[] clippedTimelines =\n        getClippedTimelines(\n            /* startUs= */ 0,\n            /* endUs= */ TEST_PERIOD_DURATION_US,\n            /* allowDynamicUpdates= */ false,\n            /* fromDefaultPosition= */ true,\n            timeline1,\n            timeline2);\n    assertThat(clippedTimelines[0].getWindow(0, window).getDurationUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[0].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);\n    assertThat(clippedTimelines[0].getWindow(0, window).isDynamic).isTrue();\n    assertThat(clippedTimelines[0].getWindow(0, window).getPositionInFirstPeriodUs())\n        .isEqualTo(TEST_PERIOD_DURATION_US + TEST_CLIP_AMOUNT_US);\n    assertThat(clippedTimelines[0].getPeriod(0, period).getDurationUs())\n        .isEqualTo(2 * TEST_PERIOD_DURATION_US);\n    assertThat(clippedTimelines[1].getWindow(0, window).getDurationUs()).isEqualTo(0);\n    assertThat(clippedTimelines[1].getWindow(0, window).getDefaultPositionUs()).isEqualTo(0);\n    assertThat(clippedTimelines[1].getWindow(0, window).isDynamic).isFalse();\n    assertThat(clippedTimelines[1].getWindow(0, window).getPositionInFirstPeriodUs())\n        .isEqualTo(3 * TEST_PERIOD_DURATION_US);\n    assertThat(clippedTimelines[1].getPeriod(0, period).getDurationUs())\n        .isEqualTo(3 * TEST_PERIOD_DURATION_US);\n  }\n\n  @Test\n  public void testWindowAndPeriodIndices() throws IOException {\n    Timeline timeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US));\n    Timeline clippedTimeline =\n        getClippedTimeline(\n            timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);\n    TimelineAsserts.assertWindowTags(clippedTimeline, 111);\n    TimelineAsserts.assertPeriodCounts(clippedTimeline, 1);\n    TimelineAsserts.assertPreviousWindowIndices(\n        clippedTimeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);\n    TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, false, 0);\n    TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0);\n    TimelineAsserts.assertNextWindowIndices(\n        clippedTimeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);\n    TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, false, 0);\n    TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0);\n  }\n\n  @Test\n  public void testEventTimeWithinClippedRange() throws IOException {\n    MediaLoadData mediaLoadData =\n        getClippingMediaSourceMediaLoadData(\n            /* clippingStartUs= */ TEST_CLIP_AMOUNT_US,\n            /* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,\n            /* eventStartUs= */ TEST_CLIP_AMOUNT_US + 1000,\n            /* eventEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US - 1000);\n    assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(1000);\n    assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs))\n        .isEqualTo(TEST_PERIOD_DURATION_US - 2 * TEST_CLIP_AMOUNT_US - 1000);\n  }\n\n  @Test\n  public void testEventTimeOutsideClippedRange() throws IOException {\n    MediaLoadData mediaLoadData =\n        getClippingMediaSourceMediaLoadData(\n            /* clippingStartUs= */ TEST_CLIP_AMOUNT_US,\n            /* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,\n            /* eventStartUs= */ TEST_CLIP_AMOUNT_US - 1000,\n            /* eventEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US + 1000);\n    assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(0);\n    assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs))\n        .isEqualTo(TEST_PERIOD_DURATION_US - 2 * TEST_CLIP_AMOUNT_US);\n  }\n\n  @Test\n  public void testUnsetEventTime() throws IOException {\n    MediaLoadData mediaLoadData =\n        getClippingMediaSourceMediaLoadData(\n            /* clippingStartUs= */ TEST_CLIP_AMOUNT_US,\n            /* clippingEndUs= */ TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,\n            /* eventStartUs= */ C.TIME_UNSET,\n            /* eventEndUs= */ C.TIME_UNSET);\n    assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(C.TIME_UNSET);\n    assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)).isEqualTo(C.TIME_UNSET);\n  }\n\n  @Test\n  public void testEventTimeWithUnsetDuration() throws IOException {\n    MediaLoadData mediaLoadData =\n        getClippingMediaSourceMediaLoadData(\n            /* clippingStartUs= */ TEST_CLIP_AMOUNT_US,\n            /* clippingEndUs= */ C.TIME_END_OF_SOURCE,\n            /* eventStartUs= */ TEST_CLIP_AMOUNT_US,\n            /* eventEndUs= */ TEST_CLIP_AMOUNT_US + 1_000_000);\n    assertThat(C.msToUs(mediaLoadData.mediaStartTimeMs)).isEqualTo(0);\n    assertThat(C.msToUs(mediaLoadData.mediaEndTimeMs)).isEqualTo(1_000_000);\n  }\n\n  /**\n   * Wraps a timeline of duration {@link #TEST_PERIOD_DURATION_US} in a {@link ClippingMediaSource},\n   * sends a media source event from the child source and returns the reported {@link MediaLoadData}\n   * for the clipping media source.\n   *\n   * @param clippingStartUs The start time of the media source clipping, in microseconds.\n   * @param clippingEndUs The end time of the media source clipping, in microseconds.\n   * @param eventStartUs The start time of the media source event (before clipping), in\n   *     microseconds.\n   * @param eventEndUs The end time of the media source event (before clipping), in microseconds.\n   * @return The reported {@link MediaLoadData} for that event.\n   */\n  private static MediaLoadData getClippingMediaSourceMediaLoadData(\n      long clippingStartUs, long clippingEndUs, final long eventStartUs, final long eventEndUs)\n      throws IOException {\n    Timeline timeline =\n        new SinglePeriodTimeline(\n            TEST_PERIOD_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false);\n    FakeMediaSource fakeMediaSource =\n        new FakeMediaSource(timeline, /* manifest= */ null) {\n          @Override\n          protected FakeMediaPeriod createFakeMediaPeriod(\n              MediaPeriodId id,\n              TrackGroupArray trackGroupArray,\n              Allocator allocator,\n              EventDispatcher eventDispatcher,\n              @Nullable TransferListener transferListener) {\n            eventDispatcher.downstreamFormatChanged(\n                new MediaLoadData(\n                    C.DATA_TYPE_MEDIA,\n                    C.TRACK_TYPE_UNKNOWN,\n                    /* trackFormat= */ null,\n                    C.SELECTION_REASON_UNKNOWN,\n                    /* trackSelectionData= */ null,\n                    C.usToMs(eventStartUs),\n                    C.usToMs(eventEndUs)));\n            return super.createFakeMediaPeriod(\n                id, trackGroupArray, allocator, eventDispatcher, transferListener);\n          }\n        };\n    final ClippingMediaSource clippingMediaSource =\n        new ClippingMediaSource(fakeMediaSource, clippingStartUs, clippingEndUs);\n    MediaSourceTestRunner testRunner =\n        new MediaSourceTestRunner(clippingMediaSource, /* allocator= */ null);\n    final MediaLoadData[] reportedMediaLoadData = new MediaLoadData[1];\n    try {\n      testRunner.runOnPlaybackThread(\n          () ->\n              clippingMediaSource.addEventListener(\n                  new Handler(),\n                  new DefaultMediaSourceEventListener() {\n                    @Override\n                    public void onDownstreamFormatChanged(\n                        int windowIndex,\n                        @Nullable MediaPeriodId mediaPeriodId,\n                        MediaLoadData mediaLoadData) {\n                      reportedMediaLoadData[0] = mediaLoadData;\n                    }\n                  }));\n      testRunner.prepareSource();\n      // Create period to send the test event configured above.\n      testRunner.createPeriod(\n          new MediaPeriodId(\n              timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));\n      assertThat(reportedMediaLoadData[0]).isNotNull();\n    } finally {\n      testRunner.release();\n    }\n    return reportedMediaLoadData[0];\n  }\n\n  /**\n   * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.\n   */\n  private static Timeline getClippedTimeline(Timeline timeline, long startUs, long endUs)\n      throws IOException {\n    FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, /* manifest= */ null);\n    ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startUs, endUs);\n    return getClippedTimelines(fakeMediaSource, mediaSource)[0];\n  }\n\n  /**\n   * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.\n   */\n  private static Timeline getClippedTimeline(Timeline timeline, long durationUs)\n      throws IOException {\n    FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, /* manifest= */ null);\n    ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, durationUs);\n    return getClippedTimelines(fakeMediaSource, mediaSource)[0];\n  }\n\n  /**\n   * Wraps the specified timelines in a {@link ClippingMediaSource} and returns the clipped timeline\n   * for each timeline update.\n   */\n  private static Timeline[] getClippedTimelines(\n      long startUs,\n      long endUs,\n      boolean allowDynamicUpdates,\n      boolean fromDefaultPosition,\n      Timeline firstTimeline,\n      Timeline... additionalTimelines)\n      throws IOException {\n    FakeMediaSource fakeMediaSource = new FakeMediaSource(firstTimeline, /* manifest= */ null);\n    ClippingMediaSource mediaSource =\n        new ClippingMediaSource(\n            fakeMediaSource,\n            startUs,\n            endUs,\n            /* enableInitialDiscontinuity= */ true,\n            allowDynamicUpdates,\n            fromDefaultPosition);\n    return getClippedTimelines(fakeMediaSource, mediaSource, additionalTimelines);\n  }\n\n  private static Timeline[] getClippedTimelines(\n      FakeMediaSource fakeMediaSource,\n      ClippingMediaSource clippingMediaSource,\n      Timeline... additionalTimelines)\n      throws IOException {\n    MediaSourceTestRunner testRunner =\n        new MediaSourceTestRunner(clippingMediaSource, /* allocator= */ null);\n    Timeline[] clippedTimelines = new Timeline[additionalTimelines.length + 1];\n    try {\n      clippedTimelines[0] = testRunner.prepareSource();\n      MediaPeriod mediaPeriod =\n          testRunner.createPeriod(\n              new MediaPeriodId(\n                  clippedTimelines[0].getUidOfPeriod(/* periodIndex= */ 0),\n                  /* windowSequenceNumber= */ 0));\n      for (int i = 0; i < additionalTimelines.length; i++) {\n        fakeMediaSource.setNewSourceInfo(additionalTimelines[i], /* newManifest= */ null);\n        clippedTimelines[i + 1] = testRunner.assertTimelineChangeBlocking();\n      }\n      testRunner.releasePeriod(mediaPeriod);\n      testRunner.releaseSource();\n      fakeMediaSource.assertReleased();\n      return clippedTimelines;\n    } finally {\n      testRunner.release();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.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.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link CompositeSequenceableLoader}. */\n@RunWith(AndroidJUnit4.class)\npublic final class CompositeSequenceableLoaderTest {\n\n  /**\n   * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns minimum buffered\n   * position among all sub-loaders.\n   */\n  @Test\n  public void testGetBufferedPositionUsReturnsMinimumLoaderBufferedPosition() {\n    FakeSequenceableLoader loader1 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000);\n    FakeSequenceableLoader loader2 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001);\n    CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader(\n        new SequenceableLoader[] {loader1, loader2});\n    assertThat(compositeSequenceableLoader.getBufferedPositionUs()).isEqualTo(1000);\n  }\n\n  /**\n   * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns minimum buffered\n   * position that is not {@link C#TIME_END_OF_SOURCE} among all sub-loaders.\n   */\n  @Test\n  public void testGetBufferedPositionUsReturnsMinimumNonEndOfSourceLoaderBufferedPosition() {\n    FakeSequenceableLoader loader1 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000);\n    FakeSequenceableLoader loader2 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2000);\n    FakeSequenceableLoader loader3 =\n        new FakeSequenceableLoader(\n            /* bufferedPositionUs */ C.TIME_END_OF_SOURCE,\n            /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE);\n    CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader(\n        new SequenceableLoader[] {loader1, loader2, loader3});\n    assertThat(compositeSequenceableLoader.getBufferedPositionUs()).isEqualTo(1000);\n  }\n\n  /**\n   * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns\n   * {@link C#TIME_END_OF_SOURCE} when all sub-loaders have buffered till end-of-source.\n   */\n  @Test\n  public void testGetBufferedPositionUsReturnsEndOfSourceWhenAllLoaderBufferedTillEndOfSource() {\n    FakeSequenceableLoader loader1 =\n        new FakeSequenceableLoader(\n            /* bufferedPositionUs */ C.TIME_END_OF_SOURCE,\n            /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE);\n    FakeSequenceableLoader loader2 =\n        new FakeSequenceableLoader(\n            /* bufferedPositionUs */ C.TIME_END_OF_SOURCE,\n            /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE);\n    CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader(\n        new SequenceableLoader[] {loader1, loader2});\n    assertThat(compositeSequenceableLoader.getBufferedPositionUs()).isEqualTo(C.TIME_END_OF_SOURCE);\n  }\n\n  /**\n   * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns minimum next\n   * load position among all sub-loaders.\n   */\n  @Test\n  public void testGetNextLoadPositionUsReturnMinimumLoaderNextLoadPositionUs() {\n    FakeSequenceableLoader loader1 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2001);\n    FakeSequenceableLoader loader2 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2000);\n    CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader(\n        new SequenceableLoader[] {loader1, loader2});\n    assertThat(compositeSequenceableLoader.getNextLoadPositionUs()).isEqualTo(2000);\n  }\n\n  /**\n   * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns minimum next\n   * load position that is not {@link C#TIME_END_OF_SOURCE} among all sub-loaders.\n   */\n  @Test\n  public void testGetNextLoadPositionUsReturnMinimumNonEndOfSourceLoaderNextLoadPositionUs() {\n    FakeSequenceableLoader loader1 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000);\n    FakeSequenceableLoader loader2 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001);\n    FakeSequenceableLoader loader3 =\n        new FakeSequenceableLoader(\n            /* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE);\n    CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader(\n        new SequenceableLoader[] {loader1, loader2, loader3});\n    assertThat(compositeSequenceableLoader.getNextLoadPositionUs()).isEqualTo(2000);\n  }\n\n  /**\n   * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns\n   * {@link C#TIME_END_OF_SOURCE} when all sub-loaders have next load position at end-of-source.\n   */\n  @Test\n  public void testGetNextLoadPositionUsReturnsEndOfSourceWhenAllLoaderLoadingLastChunk() {\n    FakeSequenceableLoader loader1 =\n        new FakeSequenceableLoader(\n            /* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE);\n    FakeSequenceableLoader loader2 =\n        new FakeSequenceableLoader(\n            /* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE);\n    CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader(\n        new SequenceableLoader[] {loader1, loader2});\n    assertThat(compositeSequenceableLoader.getNextLoadPositionUs()).isEqualTo(C.TIME_END_OF_SOURCE);\n  }\n\n  /**\n   * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} only allows the loader\n   * with minimum next load position to continue loading if next load positions are not behind\n   * current playback position.\n   */\n  @Test\n  public void testContinueLoadingOnlyAllowFurthestBehindLoaderToLoadIfNotBehindPlaybackPosition() {\n    FakeSequenceableLoader loader1 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000);\n    FakeSequenceableLoader loader2 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001);\n    CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader(\n        new SequenceableLoader[] {loader1, loader2});\n    compositeSequenceableLoader.continueLoading(100);\n\n    assertThat(loader1.numInvocations).isEqualTo(1);\n    assertThat(loader2.numInvocations).isEqualTo(0);\n  }\n\n  /**\n   * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} allows all loaders\n   * with next load position behind current playback position to continue loading.\n   */\n  @Test\n  public void testContinueLoadingReturnAllowAllLoadersBehindPlaybackPositionToLoad() {\n    FakeSequenceableLoader loader1 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000);\n    FakeSequenceableLoader loader2 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001);\n    FakeSequenceableLoader loader3 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1002, /* nextLoadPositionUs */ 2002);\n    CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader(\n        new SequenceableLoader[] {loader1, loader2, loader3});\n    compositeSequenceableLoader.continueLoading(3000);\n\n    assertThat(loader1.numInvocations).isEqualTo(1);\n    assertThat(loader2.numInvocations).isEqualTo(1);\n    assertThat(loader3.numInvocations).isEqualTo(1);\n  }\n\n  /**\n   * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} does not allow loader\n   * with next load position at end-of-source to continue loading.\n   */\n  @Test\n  public void testContinueLoadingOnlyNotAllowEndOfSourceLoaderToLoad() {\n    FakeSequenceableLoader loader1 =\n        new FakeSequenceableLoader(\n            /* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE);\n    FakeSequenceableLoader loader2 =\n        new FakeSequenceableLoader(\n            /* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE);\n    CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader(\n        new SequenceableLoader[] {loader1, loader2});\n    compositeSequenceableLoader.continueLoading(3000);\n\n    assertThat(loader1.numInvocations).isEqualTo(0);\n    assertThat(loader2.numInvocations).isEqualTo(0);\n  }\n\n  /**\n   * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} returns true if the loader\n   * with minimum next load position can make progress if next load positions are not behind\n   * current playback position.\n   */\n  @Test\n  public void testContinueLoadingReturnTrueIfFurthestBehindLoaderCanMakeProgress() {\n    FakeSequenceableLoader loader1 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000);\n    FakeSequenceableLoader loader2 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001);\n    loader1.setNextChunkDurationUs(1000);\n\n    CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader(\n        new SequenceableLoader[] {loader1, loader2});\n\n    assertThat(compositeSequenceableLoader.continueLoading(100)).isTrue();\n  }\n\n  /**\n   * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} returns true if any loader\n   * that are behind current playback position can make progress, even if it is not the one with\n   * minimum next load position.\n   */\n  @Test\n  public void testContinueLoadingReturnTrueIfLoaderBehindPlaybackPositionCanMakeProgress() {\n    FakeSequenceableLoader loader1 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000);\n    FakeSequenceableLoader loader2 =\n        new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001);\n    // loader2 is not the furthest behind, but it can make progress if allowed.\n    loader2.setNextChunkDurationUs(1000);\n\n    CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader(\n        new SequenceableLoader[] {loader1, loader2});\n\n    assertThat(compositeSequenceableLoader.continueLoading(3000)).isTrue();\n  }\n\n  private static class FakeSequenceableLoader implements SequenceableLoader {\n\n    private long bufferedPositionUs;\n    private long nextLoadPositionUs;\n    private int numInvocations;\n    private int nextChunkDurationUs;\n\n    private FakeSequenceableLoader(long bufferedPositionUs, long nextLoadPositionUs) {\n      this.bufferedPositionUs = bufferedPositionUs;\n      this.nextLoadPositionUs = nextLoadPositionUs;\n    }\n\n    @Override\n    public long getBufferedPositionUs() {\n      return bufferedPositionUs;\n    }\n\n    @Override\n    public long getNextLoadPositionUs() {\n      return nextLoadPositionUs;\n    }\n\n    @Override\n    public boolean continueLoading(long positionUs) {\n      numInvocations++;\n      boolean loaded = nextChunkDurationUs != 0;\n      // The current chunk has been loaded, advance to next chunk.\n      bufferedPositionUs = nextLoadPositionUs;\n      nextLoadPositionUs += nextChunkDurationUs;\n      nextChunkDurationUs = 0;\n      return loaded;\n    }\n\n    @Override\n    public void reevaluateBuffer(long positionUs) {\n      // Do nothing.\n    }\n\n    private void setNextChunkDurationUs(int nextChunkDurationUs) {\n      this.nextChunkDurationUs = nextChunkDurationUs;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.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.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\nimport static org.mockito.Mockito.mock;\n\nimport android.os.ConditionVariable;\nimport android.os.Handler;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.MediaSource.SourceInfoRefreshListener;\nimport com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;\nimport com.google.android.exoplayer2.testutil.DummyMainThread;\nimport com.google.android.exoplayer2.testutil.FakeMediaSource;\nimport com.google.android.exoplayer2.testutil.FakeShuffleOrder;\nimport com.google.android.exoplayer2.testutil.FakeTimeline;\nimport com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;\nimport com.google.android.exoplayer2.testutil.MediaSourceTestRunner;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\nimport com.google.android.exoplayer2.testutil.TimelineAsserts;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.concurrent.CountDownLatch;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/** Unit tests for {@link ConcatenatingMediaSource}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic final class ConcatenatingMediaSourceTest {\n\n  private ConcatenatingMediaSource mediaSource;\n  private MediaSourceTestRunner testRunner;\n\n  @Before\n  public void setUp() throws Exception {\n    mediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0));\n    testRunner = new MediaSourceTestRunner(mediaSource, null);\n  }\n\n  @After\n  public void tearDown() throws Exception {\n    testRunner.release();\n  }\n\n  @Test\n  public void testPlaylistChangesAfterPreparation() throws IOException, InterruptedException {\n    Timeline timeline = testRunner.prepareSource();\n    TimelineAsserts.assertEmpty(timeline);\n\n    FakeMediaSource[] childSources = createMediaSources(7);\n\n    // Add first source.\n    mediaSource.addMediaSource(childSources[0]);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 1);\n    TimelineAsserts.assertWindowTags(timeline, 111);\n\n    // Add at front of queue.\n    mediaSource.addMediaSource(0, childSources[1]);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 2, 1);\n    TimelineAsserts.assertWindowTags(timeline, 222, 111);\n\n    // Add at back of queue.\n    mediaSource.addMediaSource(childSources[2]);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3);\n    TimelineAsserts.assertWindowTags(timeline, 222, 111, 333);\n\n    // Add in the middle.\n    mediaSource.addMediaSource(1, childSources[3]);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3);\n    TimelineAsserts.assertWindowTags(timeline, 222, 444, 111, 333);\n\n    // Add bulk.\n    mediaSource.addMediaSources(\n        3, Arrays.asList(childSources[4], childSources[5], childSources[6]));\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);\n    TimelineAsserts.assertWindowTags(timeline, 222, 444, 111, 555, 666, 777, 333);\n\n    // Move sources.\n    mediaSource.moveMediaSource(2, 3);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 5, 1, 6, 7, 3);\n    TimelineAsserts.assertWindowTags(timeline, 222, 444, 555, 111, 666, 777, 333);\n    mediaSource.moveMediaSource(3, 2);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);\n    TimelineAsserts.assertWindowTags(timeline, 222, 444, 111, 555, 666, 777, 333);\n    mediaSource.moveMediaSource(0, 6);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 4, 1, 5, 6, 7, 3, 2);\n    TimelineAsserts.assertWindowTags(timeline, 444, 111, 555, 666, 777, 333, 222);\n    mediaSource.moveMediaSource(6, 0);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);\n    TimelineAsserts.assertWindowTags(timeline, 222, 444, 111, 555, 666, 777, 333);\n\n    // Remove in the middle.\n    mediaSource.removeMediaSource(3);\n    testRunner.assertTimelineChangeBlocking();\n    mediaSource.removeMediaSource(3);\n    testRunner.assertTimelineChangeBlocking();\n    mediaSource.removeMediaSource(3);\n    testRunner.assertTimelineChangeBlocking();\n    mediaSource.removeMediaSource(1);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3);\n    TimelineAsserts.assertWindowTags(timeline, 222, 111, 333);\n    for (int i = 3; i <= 6; i++) {\n      childSources[i].assertReleased();\n    }\n\n    // Assert the correct child source preparation load events have been returned (with the\n    // respective window index at the time of preparation).\n    testRunner.assertCompletedManifestLoads(0, 0, 2, 1, 3, 4, 5);\n\n    // Assert correct next and previous indices behavior after some insertions and removals.\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1);\n    TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);\n    TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1);\n    assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0);\n    assertThat(timeline.getLastWindowIndex(false)).isEqualTo(timeline.getWindowCount() - 1);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET);\n    TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);\n    TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0);\n    assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(timeline.getWindowCount() - 1);\n    assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0);\n\n    // Assert all periods can be prepared and the respective load events are returned.\n    testRunner.assertPrepareAndReleaseAllPeriods();\n    assertCompletedAllMediaPeriodLoads(timeline);\n\n    // Remove at front of queue.\n    mediaSource.removeMediaSource(0);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 3);\n    TimelineAsserts.assertWindowTags(timeline, 111, 333);\n    childSources[1].assertReleased();\n\n    // Remove at back of queue.\n    mediaSource.removeMediaSource(1);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 1);\n    TimelineAsserts.assertWindowTags(timeline, 111);\n    childSources[2].assertReleased();\n\n    // Remove last source.\n    mediaSource.removeMediaSource(0);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertEmpty(timeline);\n    childSources[3].assertReleased();\n  }\n\n  @Test\n  public void testPlaylistChangesBeforePreparation() throws IOException, InterruptedException {\n    FakeMediaSource[] childSources = createMediaSources(4);\n    mediaSource.addMediaSource(childSources[0]);\n    mediaSource.addMediaSource(childSources[1]);\n    mediaSource.addMediaSource(0, childSources[2]);\n    mediaSource.moveMediaSource(0, 2);\n    mediaSource.removeMediaSource(0);\n    mediaSource.moveMediaSource(1, 0);\n    mediaSource.addMediaSource(1, childSources[3]);\n    testRunner.assertNoTimelineChange();\n\n    Timeline timeline = testRunner.prepareSource();\n    TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2);\n    TimelineAsserts.assertWindowTags(timeline, 333, 444, 222);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET);\n\n    testRunner.assertPrepareAndReleaseAllPeriods();\n    testRunner.assertCompletedManifestLoads(0, 1, 2);\n    assertCompletedAllMediaPeriodLoads(timeline);\n    testRunner.releaseSource();\n    for (int i = 1; i < 4; i++) {\n      childSources[i].assertReleased();\n    }\n  }\n\n  @Test\n  public void testPlaylistWithLazyMediaSource() throws IOException, InterruptedException {\n    // Create some normal (immediately preparing) sources and some lazy sources whose timeline\n    // updates need to be triggered.\n    FakeMediaSource[] fastSources = createMediaSources(2);\n    final FakeMediaSource[] lazySources = new FakeMediaSource[4];\n    for (int i = 0; i < 4; i++) {\n      lazySources[i] = new FakeMediaSource(null, null);\n    }\n\n    // Add lazy sources and normal sources before preparation. Also remove one lazy source again\n    // before preparation to check it doesn't throw or change the result.\n    mediaSource.addMediaSource(lazySources[0]);\n    mediaSource.addMediaSource(0, fastSources[0]);\n    mediaSource.removeMediaSource(1);\n    mediaSource.addMediaSource(1, lazySources[1]);\n    testRunner.assertNoTimelineChange();\n\n    // Prepare and assert that the timeline contains all information for normal sources while having\n    // placeholder information for lazy sources.\n    Timeline timeline = testRunner.prepareSource();\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 1);\n    TimelineAsserts.assertWindowTags(timeline, 111, null);\n    TimelineAsserts.assertWindowIsDynamic(timeline, false, true);\n\n    // Trigger source info refresh for lazy source and check that the timeline now contains all\n    // information for all windows.\n    testRunner.runOnPlaybackThread(\n        () -> lazySources[1].setNewSourceInfo(createFakeTimeline(8), null));\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 9);\n    TimelineAsserts.assertWindowTags(timeline, 111, 999);\n    TimelineAsserts.assertWindowIsDynamic(timeline, false, false);\n    testRunner.assertPrepareAndReleaseAllPeriods();\n    testRunner.assertCompletedManifestLoads(0, 1);\n    assertCompletedAllMediaPeriodLoads(timeline);\n\n    // Add further lazy and normal sources after preparation. Also remove one lazy source again to\n    // check it doesn't throw or change the result.\n    mediaSource.addMediaSource(1, lazySources[2]);\n    testRunner.assertTimelineChangeBlocking();\n    mediaSource.addMediaSource(2, fastSources[1]);\n    testRunner.assertTimelineChangeBlocking();\n    mediaSource.addMediaSource(0, lazySources[3]);\n    testRunner.assertTimelineChangeBlocking();\n    mediaSource.removeMediaSource(2);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9);\n    TimelineAsserts.assertWindowTags(timeline, null, 111, 222, 999);\n    TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false);\n\n    // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not\n    // called yet.\n    MediaPeriod lazyPeriod =\n        testRunner.createPeriod(\n            new MediaPeriodId(\n                timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));\n    CountDownLatch preparedCondition = testRunner.preparePeriod(lazyPeriod, 0);\n    assertThat(preparedCondition.getCount()).isEqualTo(1);\n\n    // Assert that a second period can also be created and released without problems.\n    MediaPeriod secondLazyPeriod =\n        testRunner.createPeriod(\n            new MediaPeriodId(\n                timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));\n    testRunner.releasePeriod(secondLazyPeriod);\n\n    // Trigger source info refresh for lazy media source. Assert that now all information is\n    // available again and the previously created period now also finished preparing.\n    testRunner.runOnPlaybackThread(\n        () -> lazySources[3].setNewSourceInfo(createFakeTimeline(7), null));\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9);\n    TimelineAsserts.assertWindowTags(timeline, 888, 111, 222, 999);\n    TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false);\n    assertThat(preparedCondition.getCount()).isEqualTo(0);\n\n    // Release the period and source.\n    testRunner.releasePeriod(lazyPeriod);\n    testRunner.releaseSource();\n\n    // Assert all sources were fully released.\n    for (FakeMediaSource fastSource : fastSources) {\n      fastSource.assertReleased();\n    }\n    for (FakeMediaSource lazySource : lazySources) {\n      lazySource.assertReleased();\n    }\n  }\n\n  @Test\n  public void testEmptyTimelineMediaSource() throws IOException, InterruptedException {\n    Timeline timeline = testRunner.prepareSource();\n    TimelineAsserts.assertEmpty(timeline);\n\n    mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null));\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertEmpty(timeline);\n\n    mediaSource.addMediaSources(\n        Arrays.asList(\n            new MediaSource[] {\n              new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null),\n              new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null),\n              new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null)\n            }));\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertEmpty(timeline);\n    testRunner.assertCompletedManifestLoads(/* empty */ );\n\n    // Insert non-empty media source to leave empty sources at the start, the end, and the middle\n    // (with single and multiple empty sources in a row).\n    MediaSource[] mediaSources = createMediaSources(3);\n    mediaSource.addMediaSource(1, mediaSources[0]);\n    testRunner.assertTimelineChangeBlocking();\n    mediaSource.addMediaSource(4, mediaSources[1]);\n    testRunner.assertTimelineChangeBlocking();\n    mediaSource.addMediaSource(6, mediaSources[2]);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertWindowTags(timeline, 111, 222, 333);\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1);\n    TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);\n    TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET);\n    TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);\n    TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);\n    TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1);\n    assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0);\n    assertThat(timeline.getLastWindowIndex(false)).isEqualTo(2);\n    assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2);\n    assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0);\n    testRunner.assertPrepareAndReleaseAllPeriods();\n    testRunner.assertCompletedManifestLoads(0, 1, 2);\n    assertCompletedAllMediaPeriodLoads(timeline);\n  }\n\n  @Test\n  public void testDynamicChangeOfEmptyTimelines() throws IOException {\n    FakeMediaSource[] childSources =\n        new FakeMediaSource[] {\n          new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null),\n          new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null),\n          new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null),\n        };\n    Timeline nonEmptyTimeline = new FakeTimeline(/* windowCount = */ 1);\n\n    mediaSource.addMediaSources(Arrays.asList(childSources));\n    Timeline timeline = testRunner.prepareSource();\n    TimelineAsserts.assertEmpty(timeline);\n\n    childSources[0].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 1);\n\n    childSources[2].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 1);\n\n    childSources[1].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1);\n  }\n\n  @Test\n  public void testIllegalArguments() {\n    MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null);\n\n    // Null sources.\n    try {\n      mediaSource.addMediaSource(null);\n      fail(\"Null mediaSource not allowed.\");\n    } catch (NullPointerException e) {\n      // Expected.\n    }\n\n    MediaSource[] mediaSources = {validSource, null};\n    try {\n      mediaSource.addMediaSources(Arrays.asList(mediaSources));\n      fail(\"Null mediaSource not allowed.\");\n    } catch (NullPointerException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testCustomCallbackBeforePreparationAddSingle() {\n    ConditionVariable runnableInvoked = new ConditionVariable();\n\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    dummyMainThread.runOnMainThread(\n        () ->\n            mediaSource.addMediaSource(\n                createFakeMediaSource(), new Handler(), runnableInvoked::open));\n    runnableInvoked.block(MediaSourceTestRunner.TIMEOUT_MS);\n    dummyMainThread.release();\n\n    assertThat(runnableInvoked.block(0)).isTrue();\n  }\n\n  @Test\n  public void testCustomCallbackBeforePreparationAddMultiple() {\n    ConditionVariable runnableInvoked = new ConditionVariable();\n\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    dummyMainThread.runOnMainThread(\n        () ->\n            mediaSource.addMediaSources(\n                Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}),\n                new Handler(),\n                runnableInvoked::open));\n    runnableInvoked.block(MediaSourceTestRunner.TIMEOUT_MS);\n    dummyMainThread.release();\n\n    assertThat(runnableInvoked.block(0)).isTrue();\n  }\n\n  @Test\n  public void testCustomCallbackBeforePreparationAddSingleWithIndex() {\n    ConditionVariable runnableInvoked = new ConditionVariable();\n\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    dummyMainThread.runOnMainThread(\n        () ->\n            mediaSource.addMediaSource(\n                /* index */ 0, createFakeMediaSource(), new Handler(), runnableInvoked::open));\n    runnableInvoked.block(MediaSourceTestRunner.TIMEOUT_MS);\n    dummyMainThread.release();\n\n    assertThat(runnableInvoked.block(0)).isTrue();\n  }\n\n  @Test\n  public void testCustomCallbackBeforePreparationAddMultipleWithIndex() {\n    ConditionVariable runnableInvoked = new ConditionVariable();\n\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    dummyMainThread.runOnMainThread(\n        () ->\n            mediaSource.addMediaSources(\n                /* index */ 0,\n                Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}),\n                new Handler(),\n                runnableInvoked::open));\n    runnableInvoked.block(MediaSourceTestRunner.TIMEOUT_MS);\n    dummyMainThread.release();\n\n    assertThat(runnableInvoked.block(0)).isTrue();\n  }\n\n  @Test\n  public void testCustomCallbackBeforePreparationRemove() {\n    ConditionVariable runnableInvoked = new ConditionVariable();\n\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    dummyMainThread.runOnMainThread(\n        () -> {\n          mediaSource.addMediaSource(createFakeMediaSource());\n          mediaSource.removeMediaSource(/* index */ 0, new Handler(), runnableInvoked::open);\n        });\n    runnableInvoked.block(MediaSourceTestRunner.TIMEOUT_MS);\n    dummyMainThread.release();\n\n    assertThat(runnableInvoked.block(0)).isTrue();\n  }\n\n  @Test\n  public void testCustomCallbackBeforePreparationMove() {\n    ConditionVariable runnableInvoked = new ConditionVariable();\n\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    dummyMainThread.runOnMainThread(\n        () -> {\n          mediaSource.addMediaSources(\n              Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}));\n          mediaSource.moveMediaSource(\n              /* fromIndex */ 1, /* toIndex */ 0, new Handler(), runnableInvoked::open);\n        });\n    runnableInvoked.block(MediaSourceTestRunner.TIMEOUT_MS);\n    dummyMainThread.release();\n\n    assertThat(runnableInvoked.block(0)).isTrue();\n  }\n\n  @Test\n  public void testCustomCallbackAfterPreparationAddSingle() throws IOException {\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    try {\n      testRunner.prepareSource();\n      final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);\n      dummyMainThread.runOnMainThread(\n          () ->\n              mediaSource.addMediaSource(createFakeMediaSource(), new Handler(), timelineGrabber));\n      Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();\n      assertThat(timeline.getWindowCount()).isEqualTo(1);\n    } finally {\n      dummyMainThread.release();\n    }\n  }\n\n  @Test\n  public void testCustomCallbackAfterPreparationAddMultiple() throws IOException {\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    try {\n      testRunner.prepareSource();\n      final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);\n      dummyMainThread.runOnMainThread(\n          () ->\n              mediaSource.addMediaSources(\n                  Arrays.asList(\n                      new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}),\n                  new Handler(),\n                  timelineGrabber));\n      Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();\n      assertThat(timeline.getWindowCount()).isEqualTo(2);\n    } finally {\n      dummyMainThread.release();\n    }\n  }\n\n  @Test\n  public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws IOException {\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    try {\n      testRunner.prepareSource();\n      final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);\n      dummyMainThread.runOnMainThread(\n          () ->\n              mediaSource.addMediaSource(\n                  /* index */ 0, createFakeMediaSource(), new Handler(), timelineGrabber));\n      Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();\n      assertThat(timeline.getWindowCount()).isEqualTo(1);\n    } finally {\n      dummyMainThread.release();\n    }\n  }\n\n  @Test\n  public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws IOException {\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    try {\n      testRunner.prepareSource();\n      final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);\n      dummyMainThread.runOnMainThread(\n          () ->\n              mediaSource.addMediaSources(\n                  /* index */ 0,\n                  Arrays.asList(\n                      new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}),\n                  new Handler(),\n                  timelineGrabber));\n      Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();\n      assertThat(timeline.getWindowCount()).isEqualTo(2);\n    } finally {\n      dummyMainThread.release();\n    }\n  }\n\n  @Test\n  public void testCustomCallbackAfterPreparationRemove() throws IOException {\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    try {\n      testRunner.prepareSource();\n      dummyMainThread.runOnMainThread(() -> mediaSource.addMediaSource(createFakeMediaSource()));\n      testRunner.assertTimelineChangeBlocking();\n\n      final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);\n      dummyMainThread.runOnMainThread(\n          () -> mediaSource.removeMediaSource(/* index */ 0, new Handler(), timelineGrabber));\n      Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();\n      assertThat(timeline.getWindowCount()).isEqualTo(0);\n    } finally {\n      dummyMainThread.release();\n    }\n  }\n\n  @Test\n  public void testCustomCallbackAfterPreparationMove() throws IOException {\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    try {\n      testRunner.prepareSource();\n      dummyMainThread.runOnMainThread(\n          () ->\n              mediaSource.addMediaSources(\n                  Arrays.asList(\n                      new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})));\n      testRunner.assertTimelineChangeBlocking();\n\n      final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);\n      dummyMainThread.runOnMainThread(\n          () ->\n              mediaSource.moveMediaSource(\n                  /* fromIndex */ 1, /* toIndex */ 0, new Handler(), timelineGrabber));\n      Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();\n      assertThat(timeline.getWindowCount()).isEqualTo(2);\n    } finally {\n      dummyMainThread.release();\n    }\n  }\n\n  @Test\n  public void testCustomCallbackIsCalledAfterRelease() throws IOException {\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    ConditionVariable callbackCalledCondition = new ConditionVariable();\n    try {\n      dummyMainThread.runOnMainThread(\n          () -> {\n            SourceInfoRefreshListener listener = mock(SourceInfoRefreshListener.class);\n            mediaSource.addMediaSources(Arrays.asList(createMediaSources(2)));\n            mediaSource.prepareSource(listener, /* mediaTransferListener= */ null);\n            mediaSource.moveMediaSource(\n                /* currentIndex= */ 0,\n                /* newIndex= */ 1,\n                new Handler(),\n                callbackCalledCondition::open);\n            mediaSource.releaseSource(listener);\n          });\n      assertThat(callbackCalledCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue();\n    } finally {\n      dummyMainThread.release();\n    }\n  }\n\n  @Test\n  public void testPeriodCreationWithAds() throws IOException, InterruptedException {\n    // Create concatenated media source with ad child source.\n    Timeline timelineContentOnly =\n        new FakeTimeline(\n            new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND));\n    Timeline timelineWithAds =\n        new FakeTimeline(\n            new TimelineWindowDefinition(\n                2,\n                222,\n                true,\n                false,\n                10 * C.MICROS_PER_SECOND,\n                FakeTimeline.createAdPlaybackState(\n                    /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0)));\n    FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null);\n    FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null);\n    mediaSource.addMediaSource(mediaSourceContentOnly);\n    mediaSource.addMediaSource(mediaSourceWithAds);\n\n    Timeline timeline = testRunner.prepareSource();\n\n    // Assert the timeline contains ad groups.\n    TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1);\n\n    // Create all periods and assert period creation of child media sources has been called.\n    testRunner.assertPrepareAndReleaseAllPeriods();\n    Object timelineContentOnlyPeriodUid0 = timelineContentOnly.getUidOfPeriod(/* periodIndex= */ 0);\n    Object timelineContentOnlyPeriodUid1 = timelineContentOnly.getUidOfPeriod(/* periodIndex= */ 1);\n    Object timelineWithAdsPeriodUid0 = timelineWithAds.getUidOfPeriod(/* periodIndex= */ 0);\n    Object timelineWithAdsPeriodUid1 = timelineWithAds.getUidOfPeriod(/* periodIndex= */ 1);\n    mediaSourceContentOnly.assertMediaPeriodCreated(\n        new MediaPeriodId(timelineContentOnlyPeriodUid0, /* windowSequenceNumber= */ 0));\n    mediaSourceContentOnly.assertMediaPeriodCreated(\n        new MediaPeriodId(timelineContentOnlyPeriodUid1, /* windowSequenceNumber= */ 0));\n    mediaSourceWithAds.assertMediaPeriodCreated(\n        new MediaPeriodId(timelineWithAdsPeriodUid0, /* windowSequenceNumber= */ 1));\n    mediaSourceWithAds.assertMediaPeriodCreated(\n        new MediaPeriodId(timelineWithAdsPeriodUid1, /* windowSequenceNumber= */ 1));\n    mediaSourceWithAds.assertMediaPeriodCreated(\n        new MediaPeriodId(\n            timelineWithAdsPeriodUid0,\n            /* adGroupIndex= */ 0,\n            /* adIndexInAdGroup= */ 0,\n            /* windowSequenceNumber= */ 1));\n    mediaSourceWithAds.assertMediaPeriodCreated(\n        new MediaPeriodId(\n            timelineWithAdsPeriodUid1,\n            /* adGroupIndex= */ 0,\n            /* adIndexInAdGroup= */ 0,\n            /* windowSequenceNumber= */ 1));\n    testRunner.assertCompletedManifestLoads(0, 1);\n    assertCompletedAllMediaPeriodLoads(timeline);\n  }\n\n  @Test\n  public void testAtomicTimelineWindowOrder() throws IOException {\n    // Release default test runner with non-atomic media source and replace with new test runner.\n    testRunner.release();\n    ConcatenatingMediaSource mediaSource =\n        new ConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0));\n    testRunner = new MediaSourceTestRunner(mediaSource, null);\n    mediaSource.addMediaSources(Arrays.asList(createMediaSources(3)));\n    Timeline timeline = testRunner.prepareSource();\n    TimelineAsserts.assertWindowTags(timeline, 111, 222, 333);\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 1);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 2, 0, 1);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 2, 0, 1);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 2, 0, 1);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 1);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, C.INDEX_UNSET);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 2, C.INDEX_UNSET);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 1, 2, 0);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 1, 2, 0);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 0);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 2, 0);\n    assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0);\n    assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0);\n    assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(2);\n    assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2);\n  }\n\n  @Test\n  public void testNestedTimeline() throws IOException {\n    ConcatenatingMediaSource nestedSource1 =\n        new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0));\n    ConcatenatingMediaSource nestedSource2 =\n        new ConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0));\n    mediaSource.addMediaSource(nestedSource1);\n    mediaSource.addMediaSource(nestedSource2);\n    testRunner.prepareSource();\n    FakeMediaSource[] childSources = createMediaSources(4);\n    nestedSource1.addMediaSource(childSources[0]);\n    testRunner.assertTimelineChangeBlocking();\n    nestedSource1.addMediaSource(childSources[1]);\n    testRunner.assertTimelineChangeBlocking();\n    nestedSource2.addMediaSource(childSources[2]);\n    testRunner.assertTimelineChangeBlocking();\n    nestedSource2.addMediaSource(childSources[3]);\n    Timeline timeline = testRunner.assertTimelineChangeBlocking();\n\n    TimelineAsserts.assertWindowTags(timeline, 111, 222, 333, 444);\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3, 4);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1, 2);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 3, 0, 1, 2);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, 3, C.INDEX_UNSET);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 3, 0);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 3, C.INDEX_UNSET, 2);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2);\n    TimelineAsserts.assertPreviousWindowIndices(\n        timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 3, 0, 2);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 3, 1);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2);\n    TimelineAsserts.assertNextWindowIndices(\n        timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 3, 1);\n  }\n\n  @Test\n  public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException {\n    FakeMediaSource childSource = createFakeMediaSource();\n    mediaSource.addMediaSource(childSource);\n    Timeline timeline = testRunner.prepareSource();\n    MediaPeriod mediaPeriod =\n        testRunner.createPeriod(\n            new MediaPeriodId(\n                timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));\n    mediaSource.removeMediaSource(/* index= */ 0);\n    testRunner.assertTimelineChangeBlocking();\n    testRunner.releasePeriod(mediaPeriod);\n    childSource.assertReleased();\n    testRunner.releaseSource();\n  }\n\n  @Test\n  public void testDuplicateMediaSources() throws IOException, InterruptedException {\n    Timeline childTimeline = new FakeTimeline(/* windowCount= */ 2);\n    FakeMediaSource childSource = new FakeMediaSource(childTimeline, /* manifest= */ null);\n\n    mediaSource.addMediaSource(childSource);\n    mediaSource.addMediaSource(childSource);\n    testRunner.prepareSource();\n    mediaSource.addMediaSources(Arrays.asList(childSource, childSource));\n    Timeline timeline = testRunner.assertTimelineChangeBlocking();\n\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1);\n    testRunner.assertPrepareAndReleaseAllPeriods();\n    Object childPeriodUid0 = childTimeline.getUidOfPeriod(/* periodIndex= */ 0);\n    Object childPeriodUid1 = childTimeline.getUidOfPeriod(/* periodIndex= */ 1);\n    assertThat(childSource.getCreatedMediaPeriods())\n        .containsAllOf(\n            new MediaPeriodId(childPeriodUid0, /* windowSequenceNumber= */ 0),\n            new MediaPeriodId(childPeriodUid0, /* windowSequenceNumber= */ 2),\n            new MediaPeriodId(childPeriodUid0, /* windowSequenceNumber= */ 4),\n            new MediaPeriodId(childPeriodUid0, /* windowSequenceNumber= */ 6),\n            new MediaPeriodId(childPeriodUid1, /* windowSequenceNumber= */ 1),\n            new MediaPeriodId(childPeriodUid1, /* windowSequenceNumber= */ 3),\n            new MediaPeriodId(childPeriodUid1, /* windowSequenceNumber= */ 5),\n            new MediaPeriodId(childPeriodUid1, /* windowSequenceNumber= */ 7));\n    // Assert that only one manifest load is reported because the source is reused.\n    testRunner.assertCompletedManifestLoads(/* windowIndices= */ 0);\n    assertCompletedAllMediaPeriodLoads(timeline);\n\n    testRunner.releaseSource();\n    childSource.assertReleased();\n  }\n\n  @Test\n  public void testDuplicateNestedMediaSources() throws IOException, InterruptedException {\n    Timeline childTimeline = new FakeTimeline(/* windowCount= */ 1);\n    FakeMediaSource childSource = new FakeMediaSource(childTimeline, /* manifest= */ null);\n    ConcatenatingMediaSource nestedConcatenation = new ConcatenatingMediaSource();\n\n    testRunner.prepareSource();\n    mediaSource.addMediaSources(\n        Arrays.asList(childSource, nestedConcatenation, nestedConcatenation));\n    testRunner.assertTimelineChangeBlocking();\n    nestedConcatenation.addMediaSource(childSource);\n    testRunner.assertTimelineChangeBlocking();\n    nestedConcatenation.addMediaSource(childSource);\n    Timeline timeline = testRunner.assertTimelineChangeBlocking();\n\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1);\n    testRunner.assertPrepareAndReleaseAllPeriods();\n    Object childPeriodUid = childTimeline.getUidOfPeriod(/* periodIndex= */ 0);\n    assertThat(childSource.getCreatedMediaPeriods())\n        .containsAllOf(\n            new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 0),\n            new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 1),\n            new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 2),\n            new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 3),\n            new MediaPeriodId(childPeriodUid, /* windowSequenceNumber= */ 4));\n    // Assert that only one manifest load is needed because the source is reused.\n    testRunner.assertCompletedManifestLoads(/* windowIndices= */ 0);\n    assertCompletedAllMediaPeriodLoads(timeline);\n\n    testRunner.releaseSource();\n    childSource.assertReleased();\n  }\n\n  @Test\n  public void testClear() throws IOException {\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    final FakeMediaSource preparedChildSource = createFakeMediaSource();\n    final FakeMediaSource unpreparedChildSource =\n        new FakeMediaSource(/* timeline= */ null, /* manifest= */ null);\n    dummyMainThread.runOnMainThread(\n        () -> {\n          mediaSource.addMediaSource(preparedChildSource);\n          mediaSource.addMediaSource(unpreparedChildSource);\n        });\n    testRunner.prepareSource();\n    final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);\n\n    dummyMainThread.runOnMainThread(() -> mediaSource.clear(new Handler(), timelineGrabber));\n\n    Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();\n    assertThat(timeline.isEmpty()).isTrue();\n    preparedChildSource.assertReleased();\n    unpreparedChildSource.assertReleased();\n  }\n\n  @Test\n  public void testReleaseAndReprepareSource() throws IOException {\n    FakeMediaSource[] fakeMediaSources = createMediaSources(/* count= */ 2);\n    mediaSource.addMediaSource(fakeMediaSources[0]); // Child source with 1 period.\n    mediaSource.addMediaSource(fakeMediaSources[1]); // Child source with 2 periods.\n    Timeline timeline = testRunner.prepareSource();\n    Object periodId0 = timeline.getUidOfPeriod(/* periodIndex= */ 0);\n    Object periodId1 = timeline.getUidOfPeriod(/* periodIndex= */ 1);\n    Object periodId2 = timeline.getUidOfPeriod(/* periodIndex= */ 2);\n    testRunner.releaseSource();\n\n    mediaSource.moveMediaSource(/* currentIndex= */ 1, /* newIndex= */ 0);\n    timeline = testRunner.prepareSource();\n    Object newPeriodId0 = timeline.getUidOfPeriod(/* periodIndex= */ 0);\n    Object newPeriodId1 = timeline.getUidOfPeriod(/* periodIndex= */ 1);\n    Object newPeriodId2 = timeline.getUidOfPeriod(/* periodIndex= */ 2);\n    assertThat(newPeriodId0).isEqualTo(periodId1);\n    assertThat(newPeriodId1).isEqualTo(periodId2);\n    assertThat(newPeriodId2).isEqualTo(periodId0);\n  }\n\n  @Test\n  public void testChildTimelineChangeWithActiveMediaPeriod() throws IOException {\n    FakeMediaSource[] nestedChildSources = createMediaSources(/* count= */ 2);\n    ConcatenatingMediaSource childSource = new ConcatenatingMediaSource(nestedChildSources);\n    mediaSource.addMediaSource(childSource);\n\n    Timeline timeline = testRunner.prepareSource();\n    MediaPeriod mediaPeriod =\n        testRunner.createPeriod(\n            new MediaPeriodId(\n                timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));\n    childSource.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 1);\n    timeline = testRunner.assertTimelineChangeBlocking();\n    testRunner.preparePeriod(mediaPeriod, /* positionUs= */ 0);\n\n    testRunner.assertCompletedMediaPeriodLoads(\n        new MediaPeriodId(\n            timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));\n  }\n\n  @Test\n  public void testChildSourceIsNotPreparedWithLazyPreparation() throws IOException {\n    FakeMediaSource[] childSources = createMediaSources(/* count= */ 2);\n    mediaSource =\n        new ConcatenatingMediaSource(\n            /* isAtomic= */ false,\n            /* useLazyPreparation= */ true,\n            new DefaultShuffleOrder(0),\n            childSources);\n    testRunner = new MediaSourceTestRunner(mediaSource, /* allocator= */ null);\n    testRunner.prepareSource();\n\n    assertThat(childSources[0].isPrepared()).isFalse();\n    assertThat(childSources[1].isPrepared()).isFalse();\n  }\n\n  @Test\n  public void testChildSourceIsPreparedWithLazyPreparationAfterPeriodCreation() throws IOException {\n    FakeMediaSource[] childSources = createMediaSources(/* count= */ 2);\n    mediaSource =\n        new ConcatenatingMediaSource(\n            /* isAtomic= */ false,\n            /* useLazyPreparation= */ true,\n            new DefaultShuffleOrder(0),\n            childSources);\n    testRunner = new MediaSourceTestRunner(mediaSource, /* allocator= */ null);\n    Timeline timeline = testRunner.prepareSource();\n    testRunner.createPeriod(\n        new MediaPeriodId(\n            timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));\n\n    assertThat(childSources[0].isPrepared()).isTrue();\n    assertThat(childSources[1].isPrepared()).isFalse();\n  }\n\n  @Test\n  public void testChildSourceWithLazyPreparationOnlyPreparesSourceOnce() throws IOException {\n    FakeMediaSource[] childSources = createMediaSources(/* count= */ 2);\n    mediaSource =\n        new ConcatenatingMediaSource(\n            /* isAtomic= */ false,\n            /* useLazyPreparation= */ true,\n            new DefaultShuffleOrder(0),\n            childSources);\n    testRunner = new MediaSourceTestRunner(mediaSource, /* allocator= */ null);\n    Timeline timeline = testRunner.prepareSource();\n\n    // The lazy preparation must only be triggered once, even if we create multiple periods from\n    // the media source. FakeMediaSource.prepareSource asserts that it's not called twice, so\n    // creating two periods shouldn't throw.\n    MediaPeriodId mediaPeriodId =\n        new MediaPeriodId(\n            timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);\n    testRunner.createPeriod(mediaPeriodId);\n    testRunner.createPeriod(mediaPeriodId);\n  }\n\n  @Test\n  public void testRemoveUnpreparedChildSourceWithLazyPreparation() throws IOException {\n    FakeMediaSource[] childSources = createMediaSources(/* count= */ 2);\n    mediaSource =\n        new ConcatenatingMediaSource(\n            /* isAtomic= */ false,\n            /* useLazyPreparation= */ true,\n            new DefaultShuffleOrder(0),\n            childSources);\n    testRunner = new MediaSourceTestRunner(mediaSource, /* allocator= */ null);\n    testRunner.prepareSource();\n\n    // Check that removal doesn't throw even though the child sources are unprepared.\n    mediaSource.removeMediaSource(0);\n  }\n\n  @Test\n  public void testSetShuffleOrderBeforePreparation() throws Exception {\n    mediaSource.setShuffleOrder(new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 0));\n    mediaSource.addMediaSources(\n        Arrays.asList(createFakeMediaSource(), createFakeMediaSource(), createFakeMediaSource()));\n    Timeline timeline = testRunner.prepareSource();\n\n    assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0);\n  }\n\n  @Test\n  public void testSetShuffleOrderAfterPreparation() throws Exception {\n    mediaSource.addMediaSources(\n        Arrays.asList(createFakeMediaSource(), createFakeMediaSource(), createFakeMediaSource()));\n    testRunner.prepareSource();\n    mediaSource.setShuffleOrder(new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 3));\n    Timeline timeline = testRunner.assertTimelineChangeBlocking();\n\n    assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0);\n  }\n\n  @Test\n  public void testCustomCallbackBeforePreparationSetShuffleOrder() throws Exception {\n    ConditionVariable runnableInvoked = new ConditionVariable();\n\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    dummyMainThread.runOnMainThread(\n        () ->\n            mediaSource.setShuffleOrder(\n                new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 0),\n                new Handler(),\n                runnableInvoked::open));\n    runnableInvoked.block(MediaSourceTestRunner.TIMEOUT_MS);\n    dummyMainThread.release();\n\n    assertThat(runnableInvoked.block(0)).isTrue();\n  }\n\n  @Test\n  public void testCustomCallbackAfterPreparationSetShuffleOrder() throws Exception {\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    try {\n      mediaSource.addMediaSources(\n          Arrays.asList(createFakeMediaSource(), createFakeMediaSource(), createFakeMediaSource()));\n      testRunner.prepareSource();\n      TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);\n      dummyMainThread.runOnMainThread(\n          () ->\n              mediaSource.setShuffleOrder(\n                  new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 3),\n                  new Handler(),\n                  timelineGrabber));\n      Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();\n      assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0);\n    } finally {\n      dummyMainThread.release();\n    }\n  }\n\n  private void assertCompletedAllMediaPeriodLoads(Timeline timeline) {\n    Timeline.Period period = new Timeline.Period();\n    Timeline.Window window = new Timeline.Window();\n    ArrayList<MediaPeriodId> expectedMediaPeriodIds = new ArrayList<>();\n    for (int windowIndex = 0; windowIndex < timeline.getWindowCount(); windowIndex++) {\n      timeline.getWindow(windowIndex, window);\n      for (int periodIndex = window.firstPeriodIndex;\n          periodIndex <= window.lastPeriodIndex;\n          periodIndex++) {\n        timeline.getPeriod(periodIndex, period);\n        Object periodUid = timeline.getUidOfPeriod(periodIndex);\n        expectedMediaPeriodIds.add(new MediaPeriodId(periodUid, windowIndex));\n        for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) {\n          for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) {\n            expectedMediaPeriodIds.add(\n                new MediaPeriodId(periodUid, adGroupIndex, adIndex, windowIndex));\n          }\n        }\n      }\n    }\n    testRunner.assertCompletedMediaPeriodLoads(\n        expectedMediaPeriodIds.toArray(new MediaPeriodId[0]));\n  }\n\n  private static FakeMediaSource[] createMediaSources(int count) {\n    FakeMediaSource[] sources = new FakeMediaSource[count];\n    for (int i = 0; i < count; i++) {\n      sources[i] = new FakeMediaSource(createFakeTimeline(i), null);\n    }\n    return sources;\n  }\n\n  private static FakeMediaSource createFakeMediaSource() {\n    return new FakeMediaSource(createFakeTimeline(/* index */ 0), null);\n  }\n\n  private static FakeTimeline createFakeTimeline(int index) {\n    return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111));\n  }\n\n  private static final class TimelineGrabber implements Runnable {\n\n    private final MediaSourceTestRunner testRunner;\n    private final ConditionVariable finishedCondition;\n\n    private Timeline timeline;\n    private AssertionError error;\n\n    public TimelineGrabber(MediaSourceTestRunner testRunner) {\n      this.testRunner = testRunner;\n      finishedCondition = new ConditionVariable();\n    }\n\n    @Override\n    public void run() {\n      try {\n        timeline = testRunner.assertTimelineChange();\n      } catch (AssertionError e) {\n        error = e;\n      }\n      finishedCondition.open();\n    }\n\n    public Timeline assertTimelineChangeBlocking() {\n      assertThat(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue();\n      if (error != null) {\n        throw error;\n      }\n      return timeline;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.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 androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.testutil.FakeMediaSource;\nimport com.google.android.exoplayer2.testutil.FakeTimeline;\nimport com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;\nimport com.google.android.exoplayer2.testutil.MediaSourceTestRunner;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\nimport com.google.android.exoplayer2.testutil.TimelineAsserts;\nimport java.io.IOException;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/** Unit tests for {@link LoopingMediaSource}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic class LoopingMediaSourceTest {\n\n  private FakeTimeline multiWindowTimeline;\n\n  @Before\n  public void setUp() throws Exception {\n    multiWindowTimeline =\n        new FakeTimeline(\n            new TimelineWindowDefinition(1, 111),\n            new TimelineWindowDefinition(1, 222),\n            new TimelineWindowDefinition(1, 333));\n  }\n\n  @Test\n  public void testSingleLoopTimeline() throws IOException {\n    Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1);\n    TimelineAsserts.assertWindowTags(timeline, 111, 222, 333);\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1);\n    for (boolean shuffled : new boolean[] {false, true}) {\n      TimelineAsserts.assertPreviousWindowIndices(\n          timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1);\n      TimelineAsserts.assertPreviousWindowIndices(\n          timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2);\n      TimelineAsserts.assertPreviousWindowIndices(\n          timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1);\n      TimelineAsserts.assertNextWindowIndices(\n          timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, C.INDEX_UNSET);\n      TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2);\n      TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0);\n    }\n  }\n\n  @Test\n  public void testMultiLoopTimeline() throws IOException {\n    Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3);\n    TimelineAsserts.assertWindowTags(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333);\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1);\n    for (boolean shuffled : new boolean[] {false, true}) {\n      TimelineAsserts.assertPreviousWindowIndices(\n          timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8);\n      TimelineAsserts.assertPreviousWindowIndices(\n          timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2, 3, 4, 5, 6, 7, 8);\n      TimelineAsserts.assertPreviousWindowIndices(\n          timeline, Player.REPEAT_MODE_ALL, shuffled, 8, 0, 1, 2, 3, 4, 5, 6, 7);\n      TimelineAsserts.assertNextWindowIndices(\n          timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET);\n      TimelineAsserts.assertNextWindowIndices(\n          timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2, 3, 4, 5, 6, 7, 8);\n      TimelineAsserts.assertNextWindowIndices(\n          timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 3, 4, 5, 6, 7, 8, 0);\n    }\n  }\n\n  @Test\n  public void testInfiniteLoopTimeline() throws IOException {\n    Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE);\n    TimelineAsserts.assertWindowTags(timeline, 111, 222, 333);\n    TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1);\n    for (boolean shuffled : new boolean[] {false, true}) {\n      TimelineAsserts.assertPreviousWindowIndices(\n          timeline, Player.REPEAT_MODE_OFF, shuffled, 2, 0, 1);\n      TimelineAsserts.assertPreviousWindowIndices(\n          timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2);\n      TimelineAsserts.assertPreviousWindowIndices(\n          timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1);\n      TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, 0);\n      TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2);\n      TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0);\n    }\n  }\n\n  @Test\n  public void testEmptyTimelineLoop() throws IOException {\n    Timeline timeline = getLoopingTimeline(Timeline.EMPTY, 1);\n    TimelineAsserts.assertEmpty(timeline);\n\n    timeline = getLoopingTimeline(Timeline.EMPTY, 3);\n    TimelineAsserts.assertEmpty(timeline);\n\n    timeline = getLoopingTimeline(Timeline.EMPTY, Integer.MAX_VALUE);\n    TimelineAsserts.assertEmpty(timeline);\n  }\n\n  @Test\n  public void testSingleLoopPeriodCreation() throws Exception {\n    testMediaPeriodCreation(multiWindowTimeline, /* loopCount= */ 1);\n  }\n\n  @Test\n  public void testMultiLoopPeriodCreation() throws Exception {\n    testMediaPeriodCreation(multiWindowTimeline, /* loopCount= */ 3);\n  }\n\n  @Test\n  public void testInfiniteLoopPeriodCreation() throws Exception {\n    testMediaPeriodCreation(multiWindowTimeline, /* loopCount= */ Integer.MAX_VALUE);\n  }\n\n  /**\n   * Wraps the specified timeline in a {@link LoopingMediaSource} and returns the looping timeline.\n   */\n  private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) throws IOException {\n    FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null);\n    LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount);\n    MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);\n    try {\n      Timeline loopingTimeline = testRunner.prepareSource();\n      testRunner.releaseSource();\n      fakeMediaSource.assertReleased();\n      return loopingTimeline;\n    } finally {\n      testRunner.release();\n    }\n  }\n\n  /**\n   * Wraps the specified timeline in a {@link LoopingMediaSource} and asserts that all periods of\n   * the looping timeline can be created and prepared.\n   */\n  private static void testMediaPeriodCreation(Timeline timeline, int loopCount) throws Exception {\n    FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null);\n    LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount);\n    MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);\n    try {\n      testRunner.prepareSource();\n      testRunner.assertPrepareAndReleaseAllPeriods();\n      testRunner.releaseSource();\n    } finally {\n      testRunner.release();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.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.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.MergingMediaSource.IllegalMergeException;\nimport com.google.android.exoplayer2.testutil.FakeMediaSource;\nimport com.google.android.exoplayer2.testutil.FakeTimeline;\nimport com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;\nimport com.google.android.exoplayer2.testutil.MediaSourceTestRunner;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\nimport java.io.IOException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/** Unit tests for {@link MergingMediaSource}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic class MergingMediaSourceTest {\n\n  @Test\n  public void testMergingDynamicTimelines() throws IOException {\n    FakeTimeline firstTimeline =\n        new FakeTimeline(new TimelineWindowDefinition(true, true, C.TIME_UNSET));\n    FakeTimeline secondTimeline =\n        new FakeTimeline(new TimelineWindowDefinition(true, true, C.TIME_UNSET));\n    testMergingMediaSourcePrepare(firstTimeline, secondTimeline);\n  }\n\n  @Test\n  public void testMergingStaticTimelines() throws IOException {\n    FakeTimeline firstTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 20));\n    FakeTimeline secondTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 10));\n    testMergingMediaSourcePrepare(firstTimeline, secondTimeline);\n  }\n\n  @Test\n  public void testMergingTimelinesWithDifferentPeriodCounts() throws IOException {\n    FakeTimeline firstTimeline = new FakeTimeline(new TimelineWindowDefinition(1, null));\n    FakeTimeline secondTimeline = new FakeTimeline(new TimelineWindowDefinition(2, null));\n    try {\n      testMergingMediaSourcePrepare(firstTimeline, secondTimeline);\n      fail(\"Expected merging to fail.\");\n    } catch (IllegalMergeException e) {\n      assertThat(e.reason).isEqualTo(IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH);\n    }\n  }\n\n  @Test\n  public void testMergingMediaSourcePeriodCreation() throws Exception {\n    FakeMediaSource[] mediaSources = new FakeMediaSource[2];\n    for (int i = 0; i < mediaSources.length; i++) {\n      mediaSources[i] =\n          new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2), /* manifest= */ null);\n    }\n    MergingMediaSource mediaSource = new MergingMediaSource(mediaSources);\n    MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);\n    try {\n      testRunner.prepareSource();\n      testRunner.assertPrepareAndReleaseAllPeriods();\n      for (FakeMediaSource element : mediaSources) {\n        assertThat(element.getCreatedMediaPeriods()).isNotEmpty();\n      }\n      testRunner.releaseSource();\n    } finally {\n      testRunner.release();\n    }\n  }\n\n  /**\n   * Wraps the specified timelines in a {@link MergingMediaSource}, prepares it and checks that it\n   * forwards the first of the wrapped timelines.\n   */\n  private static void testMergingMediaSourcePrepare(Timeline... timelines) throws IOException {\n    FakeMediaSource[] mediaSources = new FakeMediaSource[timelines.length];\n    for (int i = 0; i < timelines.length; i++) {\n      mediaSources[i] = new FakeMediaSource(timelines[i], null);\n    }\n    MergingMediaSource mergingMediaSource = new MergingMediaSource(mediaSources);\n    MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mergingMediaSource, null);\n    try {\n      Timeline timeline = testRunner.prepareSource();\n      // The merged timeline should always be the one from the first child.\n      assertThat(timeline).isEqualTo(timelines[0]);\n      testRunner.releaseSource();\n      for (FakeMediaSource mediaSource : mediaSources) {\n        mediaSource.assertReleased();\n      }\n    } finally {\n      testRunner.release();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.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.C.RESULT_BUFFER_READ;\nimport static com.google.android.exoplayer2.C.RESULT_FORMAT_READ;\nimport static com.google.android.exoplayer2.C.RESULT_NOTHING_READ;\nimport static com.google.android.exoplayer2.source.SampleQueue.ADVANCE_FAILED;\nimport static com.google.common.truth.Truth.assertThat;\nimport static java.lang.Long.MIN_VALUE;\nimport static java.util.Arrays.copyOfRange;\n\nimport androidx.annotation.Nullable;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\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.testutil.TestUtil;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.DefaultAllocator;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link SampleQueue}. */\n@RunWith(AndroidJUnit4.class)\npublic final class SampleQueueTest {\n\n  private static final int ALLOCATION_SIZE = 16;\n\n  private static final Format FORMAT_1 = Format.createSampleFormat(\"1\", \"mimeType\", 0);\n  private static final Format FORMAT_2 = Format.createSampleFormat(\"2\", \"mimeType\", 0);\n  private static final Format FORMAT_1_COPY = Format.createSampleFormat(\"1\", \"mimeType\", 0);\n  private static final Format FORMAT_SPLICED = Format.createSampleFormat(\"spliced\", \"mimeType\", 0);\n  private static final byte[] DATA = TestUtil.buildTestData(ALLOCATION_SIZE * 10);\n\n  /*\n   * SAMPLE_SIZES and SAMPLE_OFFSETS are intended to test various boundary cases (with\n   * respect to the allocation size). SAMPLE_OFFSETS values are defined as the backward offsets\n   * (as expected by SampleQueue.sampleMetadata) assuming that DATA has been written to the\n   * sampleQueue in full. The allocations are filled as follows, where | indicates a boundary\n   * between allocations and x indicates a byte that doesn't belong to a sample:\n   *\n   * x<s1>|x<s2>x|x<s3>|<s4>x|<s5>|<s6|s6>|x<s7|s7>x|<s8>\n   */\n  private static final int[] SAMPLE_SIZES =\n      new int[] {\n        ALLOCATION_SIZE - 1,\n        ALLOCATION_SIZE - 2,\n        ALLOCATION_SIZE - 1,\n        ALLOCATION_SIZE - 1,\n        ALLOCATION_SIZE,\n        ALLOCATION_SIZE * 2,\n        ALLOCATION_SIZE * 2 - 2,\n        ALLOCATION_SIZE\n      };\n  private static final int[] SAMPLE_OFFSETS =\n      new int[] {\n        ALLOCATION_SIZE * 9,\n        ALLOCATION_SIZE * 8 + 1,\n        ALLOCATION_SIZE * 7,\n        ALLOCATION_SIZE * 6 + 1,\n        ALLOCATION_SIZE * 5,\n        ALLOCATION_SIZE * 3,\n        ALLOCATION_SIZE + 1,\n        0\n      };\n  private static final long[] SAMPLE_TIMESTAMPS =\n      new long[] {0, 1000, 2000, 3000, 4000, 5000, 6000, 7000};\n  private static final long LAST_SAMPLE_TIMESTAMP = SAMPLE_TIMESTAMPS[SAMPLE_TIMESTAMPS.length - 1];\n  private static final int[] SAMPLE_FLAGS =\n      new int[] {C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0, C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0};\n  private static final Format[] SAMPLE_FORMATS =\n      new Format[] {FORMAT_1, FORMAT_1, FORMAT_1, FORMAT_1, FORMAT_2, FORMAT_2, FORMAT_2, FORMAT_2};\n  private static final int DATA_SECOND_KEYFRAME_INDEX = 4;\n\n  private Allocator allocator;\n  private SampleQueue sampleQueue;\n  private FormatHolder formatHolder;\n  private DecoderInputBuffer inputBuffer;\n\n  @Before\n  public void setUp() throws Exception {\n    allocator = new DefaultAllocator(false, ALLOCATION_SIZE);\n    sampleQueue = new SampleQueue(allocator);\n    formatHolder = new FormatHolder();\n    inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);\n  }\n\n  @After\n  public void tearDown() throws Exception {\n    allocator = null;\n    sampleQueue = null;\n    formatHolder = null;\n    inputBuffer = null;\n  }\n\n  @Test\n  public void testResetReleasesAllocations() {\n    writeTestData();\n    assertAllocationCount(10);\n    sampleQueue.reset();\n    assertAllocationCount(0);\n  }\n\n  @Test\n  public void testReadWithoutWrite() {\n    assertNoSamplesToRead(null);\n  }\n\n  @Test\n  public void testReadFormatDeduplicated() {\n    sampleQueue.format(FORMAT_1);\n    assertReadFormat(false, FORMAT_1);\n    // If the same format is input then it should be de-duplicated (i.e. not output again).\n    sampleQueue.format(FORMAT_1);\n    assertNoSamplesToRead(FORMAT_1);\n    // The same applies for a format that's equal (but a different object).\n    sampleQueue.format(FORMAT_1_COPY);\n    assertNoSamplesToRead(FORMAT_1);\n  }\n\n  @Test\n  public void testReadSingleSamples() {\n    sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE);\n\n    assertAllocationCount(1);\n    // Nothing to read.\n    assertNoSamplesToRead(null);\n\n    sampleQueue.format(FORMAT_1);\n\n    // Read the format.\n    assertReadFormat(false, FORMAT_1);\n    // Nothing to read.\n    assertNoSamplesToRead(FORMAT_1);\n\n    sampleQueue.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null);\n\n    // If formatRequired, should read the format rather than the sample.\n    assertReadFormat(true, FORMAT_1);\n    // Otherwise should read the sample.\n    assertReadSample(1000, true, DATA, 0, ALLOCATION_SIZE);\n    // Allocation should still be held.\n    assertAllocationCount(1);\n    sampleQueue.discardToRead();\n    // The allocation should have been released.\n    assertAllocationCount(0);\n\n    // Nothing to read.\n    assertNoSamplesToRead(FORMAT_1);\n\n    // Write a second sample followed by one byte that does not belong to it.\n    sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE);\n    sampleQueue.sampleMetadata(2000, 0, ALLOCATION_SIZE - 1, 1, null);\n\n    // If formatRequired, should read the format rather than the sample.\n    assertReadFormat(true, FORMAT_1);\n    // Read the sample.\n    assertReadSample(2000, false, DATA, 0, ALLOCATION_SIZE - 1);\n    // Allocation should still be held.\n    assertAllocationCount(1);\n    sampleQueue.discardToRead();\n    // The last byte written to the sample queue may belong to a sample whose metadata has yet to be\n    // written, so an allocation should still be held.\n    assertAllocationCount(1);\n\n    // Write metadata for a third sample containing the remaining byte.\n    sampleQueue.sampleMetadata(3000, 0, 1, 0, null);\n\n    // If formatRequired, should read the format rather than the sample.\n    assertReadFormat(true, FORMAT_1);\n    // Read the sample.\n    assertReadSample(3000, false, DATA, ALLOCATION_SIZE - 1, 1);\n    // Allocation should still be held.\n    assertAllocationCount(1);\n    sampleQueue.discardToRead();\n    // The allocation should have been released.\n    assertAllocationCount(0);\n  }\n\n  @Test\n  public void testReadMultiSamples() {\n    writeTestData();\n    assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP);\n    assertAllocationCount(10);\n    assertReadTestData();\n    assertAllocationCount(10);\n    sampleQueue.discardToRead();\n    assertAllocationCount(0);\n  }\n\n  @Test\n  public void testReadMultiSamplesTwice() {\n    writeTestData();\n    writeTestData();\n    assertAllocationCount(20);\n    assertReadTestData(FORMAT_2);\n    assertReadTestData(FORMAT_2);\n    assertAllocationCount(20);\n    sampleQueue.discardToRead();\n    assertAllocationCount(0);\n  }\n\n  @Test\n  public void testReadMultiWithRewind() {\n    writeTestData();\n    assertReadTestData();\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(0);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(8);\n    assertAllocationCount(10);\n    // Rewind.\n    sampleQueue.rewind();\n    assertAllocationCount(10);\n    // Read again.\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(0);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(0);\n    assertReadTestData();\n  }\n\n  @Test\n  public void testRewindAfterDiscard() {\n    writeTestData();\n    assertReadTestData();\n    sampleQueue.discardToRead();\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(8);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(8);\n    assertAllocationCount(0);\n    // Rewind.\n    sampleQueue.rewind();\n    assertAllocationCount(0);\n    // Can't read again.\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(8);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(8);\n    assertReadEndOfStream(false);\n  }\n\n  @Test\n  public void testAdvanceToEnd() {\n    writeTestData();\n    sampleQueue.advanceToEnd();\n    assertAllocationCount(10);\n    sampleQueue.discardToRead();\n    assertAllocationCount(0);\n    // Despite skipping all samples, we should still read the last format, since this is the\n    // expected format for a subsequent sample.\n    assertReadFormat(false, FORMAT_2);\n    // Once the format has been read, there's nothing else to read.\n    assertNoSamplesToRead(FORMAT_2);\n  }\n\n  @Test\n  public void testAdvanceToEndRetainsUnassignedData() {\n    sampleQueue.format(FORMAT_1);\n    sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE);\n    sampleQueue.advanceToEnd();\n    assertAllocationCount(1);\n    sampleQueue.discardToRead();\n    // Skipping shouldn't discard data that may belong to a sample whose metadata has yet to be\n    // written.\n    assertAllocationCount(1);\n    // We should be able to read the format.\n    assertReadFormat(false, FORMAT_1);\n    // Once the format has been read, there's nothing else to read.\n    assertNoSamplesToRead(FORMAT_1);\n\n    sampleQueue.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null);\n    // Once the metadata has been written, check the sample can be read as expected.\n    assertReadSample(0, true, DATA, 0, ALLOCATION_SIZE);\n    assertNoSamplesToRead(FORMAT_1);\n    assertAllocationCount(1);\n    sampleQueue.discardToRead();\n    assertAllocationCount(0);\n  }\n\n  @Test\n  public void testAdvanceToBeforeBuffer() {\n    writeTestData();\n    int skipCount = sampleQueue.advanceTo(SAMPLE_TIMESTAMPS[0] - 1, true, false);\n    // Should fail and have no effect.\n    assertThat(skipCount).isEqualTo(ADVANCE_FAILED);\n    assertReadTestData();\n    assertNoSamplesToRead(FORMAT_2);\n  }\n\n  @Test\n  public void testAdvanceToStartOfBuffer() {\n    writeTestData();\n    int skipCount = sampleQueue.advanceTo(SAMPLE_TIMESTAMPS[0], true, false);\n    // Should succeed but have no effect (we're already at the first frame).\n    assertThat(skipCount).isEqualTo(0);\n    assertReadTestData();\n    assertNoSamplesToRead(FORMAT_2);\n  }\n\n  @Test\n  public void testAdvanceToEndOfBuffer() {\n    writeTestData();\n    int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false);\n    // Should succeed and skip to 2nd keyframe (the 4th frame).\n    assertThat(skipCount).isEqualTo(4);\n    assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX);\n    assertNoSamplesToRead(FORMAT_2);\n  }\n\n  @Test\n  public void testAdvanceToAfterBuffer() {\n    writeTestData();\n    int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false);\n    // Should fail and have no effect.\n    assertThat(skipCount).isEqualTo(ADVANCE_FAILED);\n    assertReadTestData();\n    assertNoSamplesToRead(FORMAT_2);\n  }\n\n  @Test\n  public void testAdvanceToAfterBufferAllowed() {\n    writeTestData();\n    int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true);\n    // Should succeed and skip to 2nd keyframe (the 4th frame).\n    assertThat(skipCount).isEqualTo(4);\n    assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX);\n    assertNoSamplesToRead(FORMAT_2);\n  }\n\n  @Test\n  public void testDiscardToEnd() {\n    writeTestData();\n    // Should discard everything.\n    sampleQueue.discardToEnd();\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(8);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(8);\n    assertAllocationCount(0);\n    // We should still be able to read the upstream format.\n    assertReadFormat(false, FORMAT_2);\n    // We should be able to write and read subsequent samples.\n    writeTestData();\n    assertReadTestData(FORMAT_2);\n  }\n\n  @Test\n  public void testDiscardToStopAtReadPosition() {\n    writeTestData();\n    // Shouldn't discard anything.\n    sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true);\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(0);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(0);\n    assertAllocationCount(10);\n    // Read the first sample.\n    assertReadTestData(null, 0, 1);\n    // Shouldn't discard anything.\n    sampleQueue.discardTo(SAMPLE_TIMESTAMPS[1] - 1, false, true);\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(0);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(1);\n    assertAllocationCount(10);\n    // Should discard the read sample.\n    sampleQueue.discardTo(SAMPLE_TIMESTAMPS[1], false, true);\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(1);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(1);\n    assertAllocationCount(9);\n    // Shouldn't discard anything.\n    sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true);\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(1);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(1);\n    assertAllocationCount(9);\n    // Should be able to read the remaining samples.\n    assertReadTestData(FORMAT_1, 1, 7);\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(1);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(8);\n    // Should discard up to the second last sample\n    sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP - 1, false, true);\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(6);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(8);\n    assertAllocationCount(3);\n    // Should discard up the last sample\n    sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true);\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(7);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(8);\n    assertAllocationCount(1);\n  }\n\n  @Test\n  public void testDiscardToDontStopAtReadPosition() {\n    writeTestData();\n    // Shouldn't discard anything.\n    sampleQueue.discardTo(SAMPLE_TIMESTAMPS[1] - 1, false, false);\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(0);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(0);\n    assertAllocationCount(10);\n    // Should discard the first sample.\n    sampleQueue.discardTo(SAMPLE_TIMESTAMPS[1], false, false);\n    assertThat(sampleQueue.getFirstIndex()).isEqualTo(1);\n    assertThat(sampleQueue.getReadIndex()).isEqualTo(1);\n    assertAllocationCount(9);\n    // Should be able to read the remaining samples.\n    assertReadTestData(FORMAT_1, 1, 7);\n  }\n\n  @Test\n  public void testDiscardUpstream() {\n    writeTestData();\n    sampleQueue.discardUpstreamSamples(8);\n    assertAllocationCount(10);\n    sampleQueue.discardUpstreamSamples(7);\n    assertAllocationCount(9);\n    sampleQueue.discardUpstreamSamples(6);\n    assertAllocationCount(7);\n    sampleQueue.discardUpstreamSamples(5);\n    assertAllocationCount(5);\n    sampleQueue.discardUpstreamSamples(4);\n    assertAllocationCount(4);\n    sampleQueue.discardUpstreamSamples(3);\n    assertAllocationCount(3);\n    sampleQueue.discardUpstreamSamples(2);\n    assertAllocationCount(2);\n    sampleQueue.discardUpstreamSamples(1);\n    assertAllocationCount(1);\n    sampleQueue.discardUpstreamSamples(0);\n    assertAllocationCount(0);\n    assertReadFormat(false, FORMAT_2);\n    assertNoSamplesToRead(FORMAT_2);\n  }\n\n  @Test\n  public void testDiscardUpstreamMulti() {\n    writeTestData();\n    sampleQueue.discardUpstreamSamples(4);\n    assertAllocationCount(4);\n    sampleQueue.discardUpstreamSamples(0);\n    assertAllocationCount(0);\n    assertReadFormat(false, FORMAT_2);\n    assertNoSamplesToRead(FORMAT_2);\n  }\n\n  @Test\n  public void testDiscardUpstreamBeforeRead() {\n    writeTestData();\n    sampleQueue.discardUpstreamSamples(4);\n    assertAllocationCount(4);\n    assertReadTestData(null, 0, 4);\n    assertReadFormat(false, FORMAT_2);\n    assertNoSamplesToRead(FORMAT_2);\n  }\n\n  @Test\n  public void testDiscardUpstreamAfterRead() {\n    writeTestData();\n    assertReadTestData(null, 0, 3);\n    sampleQueue.discardUpstreamSamples(8);\n    assertAllocationCount(10);\n    sampleQueue.discardToRead();\n    assertAllocationCount(7);\n    sampleQueue.discardUpstreamSamples(7);\n    assertAllocationCount(6);\n    sampleQueue.discardUpstreamSamples(6);\n    assertAllocationCount(4);\n    sampleQueue.discardUpstreamSamples(5);\n    assertAllocationCount(2);\n    sampleQueue.discardUpstreamSamples(4);\n    assertAllocationCount(1);\n    sampleQueue.discardUpstreamSamples(3);\n    assertAllocationCount(0);\n    assertReadFormat(false, FORMAT_2);\n    assertNoSamplesToRead(FORMAT_2);\n  }\n\n  @Test\n  public void testLargestQueuedTimestampWithDiscardUpstream() {\n    writeTestData();\n    assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP);\n    sampleQueue.discardUpstreamSamples(SAMPLE_TIMESTAMPS.length - 1);\n    // Discarding from upstream should reduce the largest timestamp.\n    assertThat(sampleQueue.getLargestQueuedTimestampUs())\n        .isEqualTo(SAMPLE_TIMESTAMPS[SAMPLE_TIMESTAMPS.length - 2]);\n    sampleQueue.discardUpstreamSamples(0);\n    // Discarding everything from upstream without reading should unset the largest timestamp.\n    assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE);\n  }\n\n  @Test\n  public void testLargestQueuedTimestampWithDiscardUpstreamDecodeOrder() {\n    long[] decodeOrderTimestamps = new long[] {0, 3000, 2000, 1000, 4000, 7000, 6000, 5000};\n    writeTestData(\n        DATA, SAMPLE_SIZES, SAMPLE_OFFSETS, decodeOrderTimestamps, SAMPLE_FORMATS, SAMPLE_FLAGS);\n    assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000);\n    sampleQueue.discardUpstreamSamples(SAMPLE_TIMESTAMPS.length - 2);\n    // Discarding the last two samples should not change the largest timestamp, due to the decode\n    // ordering of the timestamps.\n    assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000);\n    sampleQueue.discardUpstreamSamples(SAMPLE_TIMESTAMPS.length - 3);\n    // Once a third sample is discarded, the largest timestamp should have changed.\n    assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(4000);\n    sampleQueue.discardUpstreamSamples(0);\n    // Discarding everything from upstream without reading should unset the largest timestamp.\n    assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE);\n  }\n\n  @Test\n  public void testLargestQueuedTimestampWithRead() {\n    writeTestData();\n    assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP);\n    assertReadTestData();\n    // Reading everything should not reduce the largest timestamp.\n    assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP);\n  }\n\n  @Test\n  public void testSetSampleOffset() {\n    long sampleOffsetUs = 1000;\n    sampleQueue.setSampleOffsetUs(sampleOffsetUs);\n    writeTestData();\n    assertReadTestData(null, 0, 8, sampleOffsetUs);\n    assertReadEndOfStream(false);\n  }\n\n  @Test\n  public void testSplice() {\n    writeTestData();\n    sampleQueue.splice();\n    // Splice should succeed, replacing the last 4 samples with the sample being written.\n    long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[4];\n    writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME);\n    assertReadTestData(null, 0, 4);\n    assertReadFormat(false, FORMAT_SPLICED);\n    assertReadSample(spliceSampleTimeUs, true, DATA, 0, DATA.length);\n    assertReadEndOfStream(false);\n  }\n\n  @Test\n  public void testSpliceAfterRead() {\n    writeTestData();\n    assertReadTestData(null, 0, 4);\n    sampleQueue.splice();\n    // Splice should fail, leaving the last 4 samples unchanged.\n    long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[3];\n    writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME);\n    assertReadTestData(SAMPLE_FORMATS[3], 4, 4);\n    assertReadEndOfStream(false);\n\n    sampleQueue.rewind();\n    assertReadTestData(null, 0, 4);\n    sampleQueue.splice();\n    // Splice should succeed, replacing the last 4 samples with the sample being written\n    spliceSampleTimeUs = SAMPLE_TIMESTAMPS[3] + 1;\n    writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME);\n    assertReadFormat(false, FORMAT_SPLICED);\n    assertReadSample(spliceSampleTimeUs, true, DATA, 0, DATA.length);\n    assertReadEndOfStream(false);\n  }\n\n  @Test\n  public void testSpliceWithSampleOffset() {\n    long sampleOffsetUs = 30000;\n    sampleQueue.setSampleOffsetUs(sampleOffsetUs);\n    writeTestData();\n    sampleQueue.splice();\n    // Splice should succeed, replacing the last 4 samples with the sample being written.\n    long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[4];\n    writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME);\n    assertReadTestData(null, 0, 4, sampleOffsetUs);\n    assertReadFormat(false, FORMAT_SPLICED.copyWithSubsampleOffsetUs(sampleOffsetUs));\n    assertReadSample(spliceSampleTimeUs + sampleOffsetUs, true, DATA, 0, DATA.length);\n    assertReadEndOfStream(false);\n  }\n\n  // Internal methods.\n\n  /**\n   * Writes standard test data to {@code sampleQueue}.\n   */\n  private void writeTestData() {\n    writeTestData(\n        DATA, SAMPLE_SIZES, SAMPLE_OFFSETS, SAMPLE_TIMESTAMPS, SAMPLE_FORMATS, SAMPLE_FLAGS);\n  }\n\n  /**\n   * Writes the specified test data to {@code sampleQueue}.\n   */\n  @SuppressWarnings(\"ReferenceEquality\")\n  private void writeTestData(byte[] data, int[] sampleSizes, int[] sampleOffsets,\n      long[] sampleTimestamps, Format[] sampleFormats, int[] sampleFlags) {\n    sampleQueue.sampleData(new ParsableByteArray(data), data.length);\n    Format format = null;\n    for (int i = 0; i < sampleTimestamps.length; i++) {\n      if (sampleFormats[i] != format) {\n        sampleQueue.format(sampleFormats[i]);\n        format = sampleFormats[i];\n      }\n      sampleQueue.sampleMetadata(sampleTimestamps[i], sampleFlags[i], sampleSizes[i],\n          sampleOffsets[i], null);\n    }\n  }\n\n  /** Writes a single sample to {@code sampleQueue}. */\n  private void writeSample(byte[] data, long timestampUs, Format format, int sampleFlags) {\n    sampleQueue.format(format);\n    sampleQueue.sampleData(new ParsableByteArray(data), data.length);\n    sampleQueue.sampleMetadata(timestampUs, sampleFlags, data.length, 0, null);\n  }\n\n  /**\n   * Asserts correct reading of standard test data from {@code sampleQueue}.\n   */\n  private void assertReadTestData() {\n    assertReadTestData(null, 0);\n  }\n\n  /**\n   * Asserts correct reading of standard test data from {@code sampleQueue}.\n   *\n   * @param startFormat The format of the last sample previously read from {@code sampleQueue}.\n   */\n  private void assertReadTestData(Format startFormat) {\n    assertReadTestData(startFormat, 0);\n  }\n\n  /**\n   * Asserts correct reading of standard test data from {@code sampleQueue}.\n   *\n   * @param startFormat The format of the last sample previously read from {@code sampleQueue}.\n   * @param firstSampleIndex The index of the first sample that's expected to be read.\n   */\n  private void assertReadTestData(Format startFormat, int firstSampleIndex) {\n    assertReadTestData(startFormat, firstSampleIndex, SAMPLE_TIMESTAMPS.length - firstSampleIndex);\n  }\n\n  /**\n   * Asserts correct reading of standard test data from {@code sampleQueue}.\n   *\n   * @param startFormat The format of the last sample previously read from {@code sampleQueue}.\n   * @param firstSampleIndex The index of the first sample that's expected to be read.\n   * @param sampleCount The number of samples to read.\n   */\n  private void assertReadTestData(Format startFormat, int firstSampleIndex, int sampleCount) {\n    assertReadTestData(startFormat, firstSampleIndex, sampleCount, 0);\n  }\n\n  /**\n   * Asserts correct reading of standard test data from {@code sampleQueue}.\n   *\n   * @param startFormat The format of the last sample previously read from {@code sampleQueue}.\n   * @param firstSampleIndex The index of the first sample that's expected to be read.\n   * @param sampleCount The number of samples to read.\n   * @param sampleOffsetUs The expected sample offset.\n   */\n  private void assertReadTestData(\n      Format startFormat, int firstSampleIndex, int sampleCount, long sampleOffsetUs) {\n    Format format = adjustFormat(startFormat, sampleOffsetUs);\n    for (int i = firstSampleIndex; i < firstSampleIndex + sampleCount; i++) {\n      // Use equals() on the read side despite using referential equality on the write side, since\n      // sampleQueue de-duplicates written formats using equals().\n      Format testSampleFormat = adjustFormat(SAMPLE_FORMATS[i], sampleOffsetUs);\n      if (!testSampleFormat.equals(format)) {\n        // If the format has changed, we should read it.\n        assertReadFormat(false, testSampleFormat);\n        format = testSampleFormat;\n      }\n      // If we require the format, we should always read it.\n      assertReadFormat(true, testSampleFormat);\n      // Assert the sample is as expected.\n      assertReadSample(\n          SAMPLE_TIMESTAMPS[i] + sampleOffsetUs,\n          (SAMPLE_FLAGS[i] & C.BUFFER_FLAG_KEY_FRAME) != 0,\n          DATA,\n          DATA.length - SAMPLE_OFFSETS[i] - SAMPLE_SIZES[i],\n          SAMPLE_SIZES[i]);\n    }\n  }\n\n  /**\n   * Asserts {@link SampleQueue#read} is behaving correctly, given there are no samples to read and\n   * the last format to be written to the sample queue is {@code endFormat}.\n   *\n   * @param endFormat The last format to be written to the sample queue, or null of no format has\n   *     been written.\n   */\n  private void assertNoSamplesToRead(Format endFormat) {\n    // If not formatRequired or loadingFinished, should read nothing.\n    assertReadNothing(false);\n    // If formatRequired, should read the end format if set, else read nothing.\n    if (endFormat == null) {\n      assertReadNothing(true);\n    } else {\n      assertReadFormat(true, endFormat);\n    }\n    // If loadingFinished, should read end of stream.\n    assertReadEndOfStream(false);\n    assertReadEndOfStream(true);\n    // Having read end of stream should not affect other cases.\n    assertReadNothing(false);\n    if (endFormat == null) {\n      assertReadNothing(true);\n    } else {\n      assertReadFormat(true, endFormat);\n    }\n  }\n\n  /**\n   * Asserts {@link SampleQueue#read} returns {@link C#RESULT_NOTHING_READ}.\n   *\n   * @param formatRequired The value of {@code formatRequired} passed to readData.\n   */\n  private void assertReadNothing(boolean formatRequired) {\n    clearFormatHolderAndInputBuffer();\n    int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0);\n    assertThat(result).isEqualTo(RESULT_NOTHING_READ);\n    // formatHolder should not be populated.\n    assertThat(formatHolder.format).isNull();\n    // inputBuffer should not be populated.\n    assertInputBufferContainsNoSampleData();\n    assertInputBufferHasNoDefaultFlagsSet();\n  }\n\n  /**\n   * Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the\n   * {@link DecoderInputBuffer#isEndOfStream()} is set.\n   *\n   * @param formatRequired The value of {@code formatRequired} passed to readData.\n   */\n  private void assertReadEndOfStream(boolean formatRequired) {\n    clearFormatHolderAndInputBuffer();\n    int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, true, 0);\n    assertThat(result).isEqualTo(RESULT_BUFFER_READ);\n    // formatHolder should not be populated.\n    assertThat(formatHolder.format).isNull();\n    // inputBuffer should not contain sample data, but end of stream flag should be set.\n    assertInputBufferContainsNoSampleData();\n    assertThat(inputBuffer.isEndOfStream()).isTrue();\n    assertThat(inputBuffer.isDecodeOnly()).isFalse();\n    assertThat(inputBuffer.isEncrypted()).isFalse();\n  }\n\n  /**\n   * Asserts {@link SampleQueue#read} returns {@link C#RESULT_FORMAT_READ} and that the format\n   * holder is filled with a {@link Format} that equals {@code format}.\n   *\n   * @param formatRequired The value of {@code formatRequired} passed to readData.\n   * @param format The expected format.\n   */\n  private void assertReadFormat(boolean formatRequired, Format format) {\n    clearFormatHolderAndInputBuffer();\n    int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0);\n    assertThat(result).isEqualTo(RESULT_FORMAT_READ);\n    // formatHolder should be populated.\n    assertThat(formatHolder.format).isEqualTo(format);\n    // inputBuffer should not be populated.\n    assertInputBufferContainsNoSampleData();\n    assertInputBufferHasNoDefaultFlagsSet();\n  }\n\n  /**\n   * Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the buffer is\n   * filled with the specified sample data.\n   *\n   * @param timeUs The expected buffer timestamp.\n   * @param isKeyframe The expected keyframe flag.\n   * @param sampleData An array containing the expected sample data.\n   * @param offset The offset in {@code sampleData} of the expected sample data.\n   * @param length The length of the expected sample data.\n   */\n  private void assertReadSample(\n      long timeUs, boolean isKeyframe, byte[] sampleData, int offset, int length) {\n    clearFormatHolderAndInputBuffer();\n    int result = sampleQueue.read(formatHolder, inputBuffer, false, false, 0);\n    assertThat(result).isEqualTo(RESULT_BUFFER_READ);\n    // formatHolder should not be populated.\n    assertThat(formatHolder.format).isNull();\n    // inputBuffer should be populated.\n    assertThat(inputBuffer.timeUs).isEqualTo(timeUs);\n    assertThat(inputBuffer.isKeyFrame()).isEqualTo(isKeyframe);\n    assertThat(inputBuffer.isDecodeOnly()).isFalse();\n    assertThat(inputBuffer.isEncrypted()).isFalse();\n    inputBuffer.flip();\n    assertThat(inputBuffer.data.limit()).isEqualTo(length);\n    byte[] readData = new byte[length];\n    inputBuffer.data.get(readData);\n    assertThat(readData).isEqualTo(copyOfRange(sampleData, offset, offset + length));\n  }\n\n  /**\n   * Asserts the number of allocations currently in use by {@code sampleQueue}.\n   *\n   * @param count The expected number of allocations.\n   */\n  private void assertAllocationCount(int count) {\n    assertThat(allocator.getTotalBytesAllocated()).isEqualTo(ALLOCATION_SIZE * count);\n  }\n\n  /**\n   * Asserts {@code inputBuffer} does not contain any sample data.\n   */\n  private void assertInputBufferContainsNoSampleData() {\n    if (inputBuffer.data == null) {\n      return;\n    }\n    inputBuffer.flip();\n    assertThat(inputBuffer.data.limit()).isEqualTo(0);\n  }\n\n  private void assertInputBufferHasNoDefaultFlagsSet() {\n    assertThat(inputBuffer.isEndOfStream()).isFalse();\n    assertThat(inputBuffer.isDecodeOnly()).isFalse();\n    assertThat(inputBuffer.isEncrypted()).isFalse();\n  }\n\n  private void clearFormatHolderAndInputBuffer() {\n    formatHolder.format = null;\n    inputBuffer.clear();\n  }\n\n  private static Format adjustFormat(@Nullable Format format, long sampleOffsetUs) {\n    return format == null || sampleOffsetUs == 0\n        ? format\n        : format.copyWithSubsampleOffsetUs(sampleOffsetUs);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.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.C.INDEX_UNSET;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;\nimport com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link ShuffleOrder}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ShuffleOrderTest {\n\n  public static final long RANDOM_SEED = 1234567890L;\n\n  @Test\n  public void testDefaultShuffleOrder() {\n    assertShuffleOrderCorrectness(new DefaultShuffleOrder(0, RANDOM_SEED), 0);\n    assertShuffleOrderCorrectness(new DefaultShuffleOrder(1, RANDOM_SEED), 1);\n    assertShuffleOrderCorrectness(new DefaultShuffleOrder(5, RANDOM_SEED), 5);\n    for (int initialLength = 0; initialLength < 4; initialLength++) {\n      for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) {\n        testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 0);\n        testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 1);\n        testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 5);\n      }\n    }\n    testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 0, 1);\n    testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 2, 3);\n    testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 4, 5);\n    testCloneAndRemove(new DefaultShuffleOrder(1, RANDOM_SEED), 0, 1);\n    testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 0, 1000);\n    testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 0, 999);\n    testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 0, 500);\n    testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 100, 600);\n    testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 500, 1000);\n  }\n\n  @Test\n  public void testDefaultShuffleOrderSideloaded() {\n    int[] shuffledIndices = new int[] {2, 1, 0, 4, 3};\n    ShuffleOrder shuffleOrder = new DefaultShuffleOrder(shuffledIndices, RANDOM_SEED);\n    assertThat(shuffleOrder.getFirstIndex()).isEqualTo(2);\n    assertThat(shuffleOrder.getLastIndex()).isEqualTo(3);\n    for (int i = 0; i < 4; i++) {\n      assertThat(shuffleOrder.getNextIndex(shuffledIndices[i])).isEqualTo(shuffledIndices[i + 1]);\n    }\n    assertThat(shuffleOrder.getNextIndex(3)).isEqualTo(C.INDEX_UNSET);\n    for (int i = 4; i > 0; i--) {\n      assertThat(shuffleOrder.getPreviousIndex(shuffledIndices[i]))\n          .isEqualTo(shuffledIndices[i - 1]);\n    }\n    assertThat(shuffleOrder.getPreviousIndex(2)).isEqualTo(C.INDEX_UNSET);\n  }\n\n  @Test\n  public void testUnshuffledShuffleOrder() {\n    assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(0), 0);\n    assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(1), 1);\n    assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(5), 5);\n    for (int initialLength = 0; initialLength < 4; initialLength++) {\n      for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) {\n        testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 0);\n        testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 1);\n        testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 5);\n      }\n    }\n    testCloneAndRemove(new UnshuffledShuffleOrder(5), 0, 1);\n    testCloneAndRemove(new UnshuffledShuffleOrder(5), 2, 3);\n    testCloneAndRemove(new UnshuffledShuffleOrder(5), 4, 5);\n    testCloneAndRemove(new UnshuffledShuffleOrder(1), 0, 1);\n    testCloneAndRemove(new UnshuffledShuffleOrder(1000), 0, 1000);\n    testCloneAndRemove(new UnshuffledShuffleOrder(1000), 0, 999);\n    testCloneAndRemove(new UnshuffledShuffleOrder(1000), 0, 500);\n    testCloneAndRemove(new UnshuffledShuffleOrder(1000), 100, 600);\n    testCloneAndRemove(new UnshuffledShuffleOrder(1000), 500, 1000);\n  }\n\n  @Test\n  public void testUnshuffledShuffleOrderIsUnshuffled() {\n    ShuffleOrder shuffleOrder = new UnshuffledShuffleOrder(5);\n    assertThat(shuffleOrder.getFirstIndex()).isEqualTo(0);\n    assertThat(shuffleOrder.getLastIndex()).isEqualTo(4);\n    for (int i = 0; i < 4; i++) {\n      assertThat(shuffleOrder.getNextIndex(i)).isEqualTo(i + 1);\n    }\n  }\n\n  private static void assertShuffleOrderCorrectness(ShuffleOrder shuffleOrder, int length) {\n    assertThat(shuffleOrder.getLength()).isEqualTo(length);\n    if (length == 0) {\n      assertThat(shuffleOrder.getFirstIndex()).isEqualTo(INDEX_UNSET);\n      assertThat(shuffleOrder.getLastIndex()).isEqualTo(INDEX_UNSET);\n    } else {\n      int[] indices = new int[length];\n      indices[0] = shuffleOrder.getFirstIndex();\n      assertThat(shuffleOrder.getPreviousIndex(indices[0])).isEqualTo(INDEX_UNSET);\n      for (int i = 1; i < length; i++) {\n        indices[i] = shuffleOrder.getNextIndex(indices[i - 1]);\n        assertThat(shuffleOrder.getPreviousIndex(indices[i])).isEqualTo(indices[i - 1]);\n        for (int j = 0; j < i; j++) {\n          assertThat(indices[i] != indices[j]).isTrue();\n        }\n      }\n      assertThat(shuffleOrder.getLastIndex()).isEqualTo(indices[length - 1]);\n      assertThat(shuffleOrder.getNextIndex(indices[length - 1])).isEqualTo(INDEX_UNSET);\n      for (int i = 0; i < length; i++) {\n        assertThat(indices[i] >= 0 && indices[i] < length).isTrue();\n      }\n    }\n  }\n\n  private static void testCloneAndInsert(ShuffleOrder shuffleOrder, int position, int count) {\n    ShuffleOrder newOrder = shuffleOrder.cloneAndInsert(position, count);\n    assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() + count);\n    // Assert all elements still have the relative same order\n    for (int i = 0; i < shuffleOrder.getLength(); i++) {\n      int expectedNextIndex = shuffleOrder.getNextIndex(i);\n      if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) {\n        expectedNextIndex += count;\n      }\n      int newNextIndex = newOrder.getNextIndex(i < position ? i : i + count);\n      while (newNextIndex >= position && newNextIndex < position + count) {\n        newNextIndex = newOrder.getNextIndex(newNextIndex);\n      }\n      assertThat(newNextIndex).isEqualTo(expectedNextIndex);\n    }\n  }\n\n  private static void testCloneAndRemove(\n      ShuffleOrder shuffleOrder, int indexFrom, int indexToExclusive) {\n    int numberOfElementsToRemove = indexToExclusive - indexFrom;\n    ShuffleOrder newOrder = shuffleOrder.cloneAndRemove(indexFrom, indexToExclusive);\n    assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() - numberOfElementsToRemove);\n    // Assert all elements still have the relative same order\n    for (int i = 0; i < shuffleOrder.getLength(); i++) {\n      if (i >= indexFrom && i < indexToExclusive) {\n        continue;\n      }\n      int expectedNextIndex = shuffleOrder.getNextIndex(i);\n      while (expectedNextIndex >= indexFrom && expectedNextIndex < indexToExclusive) {\n        expectedNextIndex = shuffleOrder.getNextIndex(expectedNextIndex);\n      }\n      if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= indexFrom) {\n        expectedNextIndex -= numberOfElementsToRemove;\n      }\n      int newNextIndex = newOrder.getNextIndex(i < indexFrom ? i : i - numberOfElementsToRemove);\n      assertThat(newNextIndex).isEqualTo(expectedNextIndex);\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.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.common.truth.Truth.assertThat;\n\nimport android.util.Pair;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Timeline.Period;\nimport com.google.android.exoplayer2.Timeline.Window;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link SinglePeriodTimeline}. */\n@RunWith(AndroidJUnit4.class)\npublic final class SinglePeriodTimelineTest {\n\n  private Window window;\n  private Period period;\n\n  @Before\n  public void setUp() throws Exception {\n    window = new Window();\n    period = new Period();\n  }\n\n  @Test\n  public void testGetPeriodPositionDynamicWindowUnknownDuration() {\n    SinglePeriodTimeline timeline = new SinglePeriodTimeline(C.TIME_UNSET, false, true);\n    // Should return null with any positive position projection.\n    Pair<Object, Long> position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 1);\n    assertThat(position).isNull();\n    // Should return (0, 0) without a position projection.\n    position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0);\n    assertThat(position.first).isEqualTo(timeline.getUidOfPeriod(0));\n    assertThat(position.second).isEqualTo(0);\n  }\n\n  @Test\n  public void testGetPeriodPositionDynamicWindowKnownDuration() {\n    long windowDurationUs = 1000;\n    SinglePeriodTimeline timeline =\n        new SinglePeriodTimeline(\n            windowDurationUs,\n            windowDurationUs,\n            /* windowPositionInPeriodUs= */ 0,\n            /* windowDefaultStartPositionUs= */ 0,\n            /* isSeekable= */ false,\n            /* isDynamic= */ true,\n            /* tag= */ null);\n    // Should return null with a positive position projection beyond window duration.\n    Pair<Object, Long> position =\n        timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, windowDurationUs + 1);\n    assertThat(position).isNull();\n    // Should return (0, duration) with a projection equal to window duration.\n    position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, windowDurationUs);\n    assertThat(position.first).isEqualTo(timeline.getUidOfPeriod(0));\n    assertThat(position.second).isEqualTo(windowDurationUs);\n    // Should return (0, 0) without a position projection.\n    position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0);\n    assertThat(position.first).isEqualTo(timeline.getUidOfPeriod(0));\n    assertThat(position.second).isEqualTo(0);\n  }\n\n  @Test\n  public void setNullTag_returnsNullTag_butUsesDefaultUid() {\n    SinglePeriodTimeline timeline =\n        new SinglePeriodTimeline(\n            /* durationUs= */ C.TIME_UNSET,\n            /* isSeekable= */ false,\n            /* isDynamic= */ false,\n            /* tag= */ null);\n\n    assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ false).tag).isNull();\n    assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ true).tag).isNull();\n    assertThat(timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ false).id).isNull();\n    assertThat(timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).id).isNull();\n    assertThat(timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ false).uid).isNull();\n    assertThat(timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid)\n        .isNotNull();\n  }\n\n  @Test\n  public void setTag_isUsedForWindowTag() {\n    Object tag = new Object();\n    SinglePeriodTimeline timeline =\n        new SinglePeriodTimeline(\n            /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, tag);\n\n    assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ false).tag).isNull();\n    assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ true).tag)\n        .isEqualTo(tag);\n  }\n\n  @Test\n  public void getIndexOfPeriod_returnsPeriod() {\n    SinglePeriodTimeline timeline =\n        new SinglePeriodTimeline(\n            /* durationUs= */ C.TIME_UNSET,\n            /* isSeekable= */ false,\n            /* isDynamic= */ false,\n            /* tag= */ null);\n    Object uid = timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid;\n\n    assertThat(timeline.getIndexOfPeriod(uid)).isEqualTo(0);\n    assertThat(timeline.getIndexOfPeriod(/* uid= */ null)).isEqualTo(C.INDEX_UNSET);\n    assertThat(timeline.getIndexOfPeriod(/* uid= */ new Object())).isEqualTo(C.INDEX_UNSET);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupArrayTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link TrackGroupArray}. */\n@RunWith(AndroidJUnit4.class)\npublic final class TrackGroupArrayTest {\n\n  @Test\n  public void testParcelable() {\n    Format format1 = Format.createSampleFormat(\"1\", MimeTypes.VIDEO_H264, 0);\n    Format format2 = Format.createSampleFormat(\"2\", MimeTypes.AUDIO_AAC, 0);\n    Format format3 = Format.createSampleFormat(\"3\", MimeTypes.VIDEO_H264, 0);\n\n    TrackGroup trackGroup1 = new TrackGroup(format1, format2);\n    TrackGroup trackGroup2 = new TrackGroup(format3);\n\n    TrackGroupArray trackGroupArrayToParcel = new TrackGroupArray(trackGroup1, trackGroup2);\n\n    Parcel parcel = Parcel.obtain();\n    trackGroupArrayToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    TrackGroupArray trackGroupArrayFromParcel = TrackGroupArray.CREATOR.createFromParcel(parcel);\n    assertThat(trackGroupArrayFromParcel).isEqualTo(trackGroupArrayToParcel);\n\n    parcel.recycle();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/source/TrackGroupTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.os.Parcel;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link TrackGroup}. */\n@RunWith(AndroidJUnit4.class)\npublic final class TrackGroupTest {\n\n  @Test\n  public void testParcelable() {\n    Format format1 = Format.createSampleFormat(\"1\", MimeTypes.VIDEO_H264, 0);\n    Format format2 = Format.createSampleFormat(\"2\", MimeTypes.AUDIO_AAC, 0);\n\n    TrackGroup trackGroupToParcel = new TrackGroup(format1, format2);\n\n    Parcel parcel = Parcel.obtain();\n    trackGroupToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    TrackGroup trackGroupFromParcel = TrackGroup.CREATOR.createFromParcel(parcel);\n    assertThat(trackGroupFromParcel).isEqualTo(trackGroupToParcel);\n\n    parcel.recycle();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.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.ads;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link AdPlaybackState}. */\n@RunWith(AndroidJUnit4.class)\npublic final class AdPlaybackStateTest {\n\n  private static final long[] TEST_AD_GROUP_TMES_US = new long[] {0, C.msToUs(10_000)};\n  private static final Uri TEST_URI = Uri.EMPTY;\n\n  private AdPlaybackState state;\n\n  @Before\n  public void setUp() {\n    state = new AdPlaybackState(TEST_AD_GROUP_TMES_US);\n  }\n\n  @Test\n  public void testSetAdCount() {\n    assertThat(state.adGroups[0].count).isEqualTo(C.LENGTH_UNSET);\n    state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);\n    assertThat(state.adGroups[0].count).isEqualTo(1);\n  }\n\n  @Test\n  public void testSetAdUriBeforeAdCount() {\n    state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, TEST_URI);\n    state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2);\n\n    assertThat(state.adGroups[0].uris[0]).isNull();\n    assertThat(state.adGroups[0].states[0]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);\n    assertThat(state.adGroups[0].uris[1]).isSameAs(TEST_URI);\n    assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE);\n  }\n\n  @Test\n  public void testSetAdErrorBeforeAdCount() {\n    state = state.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);\n    state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2);\n\n    assertThat(state.adGroups[0].uris[0]).isNull();\n    assertThat(state.adGroups[0].states[0]).isEqualTo(AdPlaybackState.AD_STATE_ERROR);\n    assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);\n  }\n\n  @Test\n  public void testGetFirstAdIndexToPlayIsZero() {\n    state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);\n    state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);\n    state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI);\n\n    assertThat(state.adGroups[0].getFirstAdIndexToPlay()).isEqualTo(0);\n  }\n\n  @Test\n  public void testGetFirstAdIndexToPlaySkipsPlayedAd() {\n    state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);\n    state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);\n    state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI);\n\n    state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);\n\n    assertThat(state.adGroups[0].getFirstAdIndexToPlay()).isEqualTo(1);\n    assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);\n    assertThat(state.adGroups[0].states[2]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE);\n  }\n\n  @Test\n  public void testGetFirstAdIndexToPlaySkipsSkippedAd() {\n    state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);\n    state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);\n    state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI);\n\n    state = state.withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);\n\n    assertThat(state.adGroups[0].getFirstAdIndexToPlay()).isEqualTo(1);\n    assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);\n    assertThat(state.adGroups[0].states[2]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE);\n  }\n\n  @Test\n  public void testGetFirstAdIndexToPlaySkipsErrorAds() {\n    state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);\n    state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);\n    state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI);\n\n    state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);\n    state = state.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1);\n\n    assertThat(state.adGroups[0].getFirstAdIndexToPlay()).isEqualTo(2);\n  }\n\n  @Test\n  public void testGetNextAdIndexToPlaySkipsErrorAds() {\n    state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);\n    state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, TEST_URI);\n\n    state = state.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1);\n\n    assertThat(state.adGroups[0].getNextAdIndexToPlay(0)).isEqualTo(2);\n  }\n\n  @Test\n  public void testSetAdStateTwiceThrows() {\n    state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);\n    state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);\n    try {\n      state.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);\n      fail();\n    } catch (Exception e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testSkipAllWithoutAdCount() {\n    state = state.withSkippedAdGroup(0);\n    state = state.withSkippedAdGroup(1);\n    assertThat(state.adGroups[0].count).isEqualTo(0);\n    assertThat(state.adGroups[1].count).isEqualTo(0);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/source/chunk/MediaChunkListIteratorTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.testutil.FakeMediaChunk;\nimport java.util.Arrays;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link MediaChunkListIterator}. */\n@RunWith(AndroidJUnit4.class)\npublic class MediaChunkListIteratorTest {\n\n  private static final Format TEST_FORMAT = Format.createSampleFormat(null, null, 0);\n\n  private FakeMediaChunk testChunk1;\n  private FakeMediaChunk testChunk2;\n\n  @Before\n  public void setUp() {\n    testChunk1 = new FakeMediaChunk(TEST_FORMAT, 0, 10);\n    testChunk2 = new FakeMediaChunk(TEST_FORMAT, 10, 20);\n  }\n\n  @Test\n  public void iterator_reverseOrderFalse_returnsItemsInNormalOrder() {\n    MediaChunkListIterator iterator =\n        new MediaChunkListIterator(\n            Arrays.asList(testChunk1, testChunk2), /* reverseOrder= */ false);\n    assertThat(iterator.isEnded()).isFalse();\n    assertThat(iterator.next()).isTrue();\n    assertEqual(iterator, testChunk1);\n    assertThat(iterator.next()).isTrue();\n    assertEqual(iterator, testChunk2);\n    assertThat(iterator.next()).isFalse();\n    assertThat(iterator.isEnded()).isTrue();\n  }\n\n  @Test\n  public void iterator_reverseOrderTrue_returnsItemsInReverseOrder() {\n    MediaChunkListIterator iterator =\n        new MediaChunkListIterator(\n            Arrays.asList(testChunk1, testChunk2), /* reverseOrder= */ true);\n    assertThat(iterator.isEnded()).isFalse();\n    assertThat(iterator.next()).isTrue();\n    assertEqual(iterator, testChunk2);\n    assertThat(iterator.next()).isTrue();\n    assertEqual(iterator, testChunk1);\n    assertThat(iterator.next()).isFalse();\n    assertThat(iterator.isEnded()).isTrue();\n  }\n\n  private static void assertEqual(MediaChunkListIterator iterator, FakeMediaChunk chunk) {\n    assertThat(iterator.getChunkStartTimeUs()).isEqualTo(chunk.startTimeUs);\n    assertThat(iterator.getChunkEndTimeUs()).isEqualTo(chunk.endTimeUs);\n    assertThat(iterator.getDataSpec()).isEqualTo(chunk.dataSpec);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.common.truth.Truth.assertThat;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link SsaDecoder}. */\n@RunWith(AndroidJUnit4.class)\npublic final class SsaDecoderTest {\n\n  private static final String EMPTY = \"ssa/empty\";\n  private static final String TYPICAL = \"ssa/typical\";\n  private static final String TYPICAL_HEADER_ONLY = \"ssa/typical_header\";\n  private static final String TYPICAL_DIALOGUE_ONLY = \"ssa/typical_dialogue\";\n  private static final String TYPICAL_FORMAT_ONLY = \"ssa/typical_format\";\n  private static final String INVALID_TIMECODES = \"ssa/invalid_timecodes\";\n  private static final String NO_END_TIMECODES = \"ssa/no_end_timecodes\";\n\n  @Test\n  public void testDecodeEmpty() throws IOException {\n    SsaDecoder decoder = new SsaDecoder();\n    byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY);\n    SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(0);\n    assertThat(subtitle.getCues(0).isEmpty()).isTrue();\n  }\n\n  @Test\n  public void testDecodeTypical() throws IOException {\n    SsaDecoder decoder = new SsaDecoder();\n    byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL);\n    SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(6);\n    assertTypicalCue1(subtitle, 0);\n    assertTypicalCue2(subtitle, 2);\n    assertTypicalCue3(subtitle, 4);\n  }\n\n  @Test\n  public void testDecodeTypicalWithInitializationData() throws IOException {\n    byte[] headerBytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_HEADER_ONLY);\n    byte[] formatBytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FORMAT_ONLY);\n    ArrayList<byte[]> initializationData = new ArrayList<>();\n    initializationData.add(formatBytes);\n    initializationData.add(headerBytes);\n    SsaDecoder decoder = new SsaDecoder(initializationData);\n    byte[] bytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_DIALOGUE_ONLY);\n    SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(6);\n    assertTypicalCue1(subtitle, 0);\n    assertTypicalCue2(subtitle, 2);\n    assertTypicalCue3(subtitle, 4);\n  }\n\n  @Test\n  public void testDecodeInvalidTimecodes() throws IOException {\n    // Parsing should succeed, parsing the third cue only.\n    SsaDecoder decoder = new SsaDecoder();\n    byte[] bytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_TIMECODES);\n    SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(2);\n    assertTypicalCue3(subtitle, 0);\n  }\n\n  @Test\n  public void testDecodeNoEndTimecodes() throws IOException {\n    SsaDecoder decoder = new SsaDecoder();\n    byte[] bytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NO_END_TIMECODES);\n    SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(3);\n\n    assertThat(subtitle.getEventTime(0)).isEqualTo(0);\n    assertThat(subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString())\n        .isEqualTo(\"This is the first subtitle.\");\n\n    assertThat(subtitle.getEventTime(1)).isEqualTo(2340000);\n    assertThat(subtitle.getCues(subtitle.getEventTime(1)).get(0).text.toString())\n        .isEqualTo(\"This is the second subtitle \\nwith a newline \\nand another.\");\n\n    assertThat(subtitle.getEventTime(2)).isEqualTo(4560000);\n    assertThat(subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString())\n        .isEqualTo(\"This is the third subtitle, with a comma.\");\n  }\n\n  private static void assertTypicalCue1(SsaSubtitle subtitle, int eventIndex) {\n    assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0);\n    assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())\n        .isEqualTo(\"This is the first subtitle.\");\n    assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(1230000);\n  }\n\n  private static void assertTypicalCue2(SsaSubtitle subtitle, int eventIndex) {\n    assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(2340000);\n    assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())\n        .isEqualTo(\"This is the second subtitle \\nwith a newline \\nand another.\");\n    assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(3450000);\n  }\n\n  private static void assertTypicalCue3(SsaSubtitle subtitle, int eventIndex) {\n    assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(4560000);\n    assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())\n        .isEqualTo(\"This is the third subtitle, with a comma.\");\n    assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(8900000);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.text.Cue;\nimport java.io.IOException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link SubripDecoder}. */\n@RunWith(AndroidJUnit4.class)\npublic final class SubripDecoderTest {\n\n  private static final String EMPTY_FILE = \"subrip/empty\";\n  private static final String TYPICAL_FILE = \"subrip/typical\";\n  private static final String TYPICAL_WITH_BYTE_ORDER_MARK = \"subrip/typical_with_byte_order_mark\";\n  private static final String TYPICAL_EXTRA_BLANK_LINE = \"subrip/typical_extra_blank_line\";\n  private static final String TYPICAL_MISSING_TIMECODE = \"subrip/typical_missing_timecode\";\n  private static final String TYPICAL_MISSING_SEQUENCE = \"subrip/typical_missing_sequence\";\n  private static final String TYPICAL_NEGATIVE_TIMESTAMPS = \"subrip/typical_negative_timestamps\";\n  private static final String TYPICAL_UNEXPECTED_END = \"subrip/typical_unexpected_end\";\n  private static final String TYPICAL_WITH_TAGS = \"subrip/typical_with_tags\";\n  private static final String NO_END_TIMECODES_FILE = \"subrip/no_end_timecodes\";\n\n  @Test\n  public void testDecodeEmpty() throws IOException {\n    SubripDecoder decoder = new SubripDecoder();\n    byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE);\n    SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(0);\n    assertThat(subtitle.getCues(0).isEmpty()).isTrue();\n  }\n\n  @Test\n  public void testDecodeTypical() throws IOException {\n    SubripDecoder decoder = new SubripDecoder();\n    byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE);\n    SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(6);\n    assertTypicalCue1(subtitle, 0);\n    assertTypicalCue2(subtitle, 2);\n    assertTypicalCue3(subtitle, 4);\n  }\n\n  @Test\n  public void testDecodeTypicalWithByteOrderMark() throws IOException {\n    SubripDecoder decoder = new SubripDecoder();\n    byte[] bytes =\n        TestUtil.getByteArray(\n            ApplicationProvider.getApplicationContext(), TYPICAL_WITH_BYTE_ORDER_MARK);\n    SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(6);\n    assertTypicalCue1(subtitle, 0);\n    assertTypicalCue2(subtitle, 2);\n    assertTypicalCue3(subtitle, 4);\n  }\n\n  @Test\n  public void testDecodeTypicalExtraBlankLine() throws IOException {\n    SubripDecoder decoder = new SubripDecoder();\n    byte[] bytes =\n        TestUtil.getByteArray(\n            ApplicationProvider.getApplicationContext(), TYPICAL_EXTRA_BLANK_LINE);\n    SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(6);\n    assertTypicalCue1(subtitle, 0);\n    assertTypicalCue2(subtitle, 2);\n    assertTypicalCue3(subtitle, 4);\n  }\n\n  @Test\n  public void testDecodeTypicalMissingTimecode() throws IOException {\n    // Parsing should succeed, parsing the first and third cues only.\n    SubripDecoder decoder = new SubripDecoder();\n    byte[] bytes =\n        TestUtil.getByteArray(\n            ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_TIMECODE);\n    SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n    assertTypicalCue1(subtitle, 0);\n    assertTypicalCue3(subtitle, 2);\n  }\n\n  @Test\n  public void testDecodeTypicalMissingSequence() throws IOException {\n    // Parsing should succeed, parsing the first and third cues only.\n    SubripDecoder decoder = new SubripDecoder();\n    byte[] bytes =\n        TestUtil.getByteArray(\n            ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_SEQUENCE);\n    SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n    assertTypicalCue1(subtitle, 0);\n    assertTypicalCue3(subtitle, 2);\n  }\n\n  @Test\n  public void testDecodeTypicalNegativeTimestamps() throws IOException {\n    // Parsing should succeed, parsing the third cue only.\n    SubripDecoder decoder = new SubripDecoder();\n    byte[] bytes =\n        TestUtil.getByteArray(\n            ApplicationProvider.getApplicationContext(), TYPICAL_NEGATIVE_TIMESTAMPS);\n    SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(2);\n    assertTypicalCue3(subtitle, 0);\n  }\n\n  @Test\n  public void testDecodeTypicalUnexpectedEnd() throws IOException {\n    // Parsing should succeed, parsing the first and second cues only.\n    SubripDecoder decoder = new SubripDecoder();\n    byte[] bytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UNEXPECTED_END);\n    SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n    assertTypicalCue1(subtitle, 0);\n    assertTypicalCue2(subtitle, 2);\n  }\n\n  @Test\n  public void testDecodeNoEndTimecodes() throws IOException {\n    SubripDecoder decoder = new SubripDecoder();\n    byte[] bytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NO_END_TIMECODES_FILE);\n    SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(3);\n\n    assertThat(subtitle.getEventTime(0)).isEqualTo(0);\n    assertThat(subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString())\n        .isEqualTo(\"SubRip doesn't technically allow missing end timecodes.\");\n\n    assertThat(subtitle.getEventTime(1)).isEqualTo(2345000);\n    assertThat(subtitle.getCues(subtitle.getEventTime(1)).get(0).text.toString())\n        .isEqualTo(\"We interpret it to mean that a subtitle extends to the start of the next one.\");\n\n    assertThat(subtitle.getEventTime(2)).isEqualTo(3456000);\n    assertThat(subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString())\n        .isEqualTo(\"Or to the end of the media.\");\n  }\n\n  @Test\n  public void testDecodeCueWithTag() throws IOException {\n    SubripDecoder decoder = new SubripDecoder();\n    byte[] bytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS);\n    SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);\n\n    assertTypicalCue1(subtitle, 0);\n    assertTypicalCue2(subtitle, 2);\n    assertTypicalCue3(subtitle, 4);\n\n    assertThat(subtitle.getCues(subtitle.getEventTime(6)).get(0).text.toString())\n        .isEqualTo(\"This { \\\\an2} is not a valid tag due to the space after the opening bracket.\");\n\n    assertThat(subtitle.getCues(subtitle.getEventTime(8)).get(0).text.toString())\n        .isEqualTo(\"This is the fifth subtitle with multiple valid tags.\");\n\n    assertAlignmentCue(subtitle, 10, Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_START); // {/an1}\n    assertAlignmentCue(subtitle, 12, Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_MIDDLE); // {/an2}\n    assertAlignmentCue(subtitle, 14, Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_END); // {/an3}\n    assertAlignmentCue(subtitle, 16, Cue.ANCHOR_TYPE_MIDDLE, Cue.ANCHOR_TYPE_START); // {/an4}\n    assertAlignmentCue(subtitle, 18, Cue.ANCHOR_TYPE_MIDDLE, Cue.ANCHOR_TYPE_MIDDLE); // {/an5}\n    assertAlignmentCue(subtitle, 20, Cue.ANCHOR_TYPE_MIDDLE, Cue.ANCHOR_TYPE_END); // {/an6}\n    assertAlignmentCue(subtitle, 22, Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_START); // {/an7}\n    assertAlignmentCue(subtitle, 24, Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_MIDDLE); // {/an8}\n    assertAlignmentCue(subtitle, 26, Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_END); // {/an9}\n  }\n\n  private static void assertTypicalCue1(SubripSubtitle subtitle, int eventIndex) {\n    assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0);\n    assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())\n        .isEqualTo(\"This is the first subtitle.\");\n    assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(1234000);\n  }\n\n  private static void assertTypicalCue2(SubripSubtitle subtitle, int eventIndex) {\n    assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(2345000);\n    assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())\n        .isEqualTo(\"This is the second subtitle.\\nSecond subtitle with second line.\");\n    assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(3456000);\n  }\n\n  private static void assertTypicalCue3(SubripSubtitle subtitle, int eventIndex) {\n    assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(4567000);\n    assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())\n        .isEqualTo(\"This is the third subtitle.\");\n    assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(8901000);\n  }\n\n  private static void assertAlignmentCue(\n      SubripSubtitle subtitle,\n      int eventIndex,\n      @Cue.AnchorType int lineAnchor,\n      @Cue.AnchorType int positionAnchor) {\n    long eventTimeUs = subtitle.getEventTime(eventIndex);\n    Cue cue = subtitle.getCues(eventTimeUs).get(0);\n    assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);\n    assertThat(cue.lineAnchor).isEqualTo(lineAnchor);\n    assertThat(cue.line).isEqualTo(SubripDecoder.getFractionalPositionForAnchorType(lineAnchor));\n    assertThat(cue.positionAnchor).isEqualTo(positionAnchor);\n    assertThat(cue.position)\n        .isEqualTo(SubripDecoder.getFractionalPositionForAnchorType(positionAnchor));\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\nimport static com.google.common.truth.Truth.assertWithMessage;\n\nimport android.text.Layout;\nimport android.text.Spannable;\nimport android.text.SpannableStringBuilder;\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.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.SubtitleDecoderException;\nimport com.google.android.exoplayer2.util.ColorParser;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link TtmlDecoder}. */\n@RunWith(AndroidJUnit4.class)\npublic final class TtmlDecoderTest {\n\n  private static final String INLINE_ATTRIBUTES_TTML_FILE = \"ttml/inline_style_attributes.xml\";\n  private static final String INHERIT_STYLE_TTML_FILE = \"ttml/inherit_style.xml\";\n  private static final String INHERIT_STYLE_OVERRIDE_TTML_FILE =\n      \"ttml/inherit_and_override_style.xml\";\n  private static final String INHERIT_GLOBAL_AND_PARENT_TTML_FILE =\n      \"ttml/inherit_global_and_parent.xml\";\n  private static final String INHERIT_MULTIPLE_STYLES_TTML_FILE =\n      \"ttml/inherit_multiple_styles.xml\";\n  private static final String CHAIN_MULTIPLE_STYLES_TTML_FILE = \"ttml/chain_multiple_styles.xml\";\n  private static final String MULTIPLE_REGIONS_TTML_FILE = \"ttml/multiple_regions.xml\";\n  private static final String NO_UNDERLINE_LINETHROUGH_TTML_FILE =\n      \"ttml/no_underline_linethrough.xml\";\n  private static final String FONT_SIZE_TTML_FILE = \"ttml/font_size.xml\";\n  private static final String FONT_SIZE_MISSING_UNIT_TTML_FILE = \"ttml/font_size_no_unit.xml\";\n  private static final String FONT_SIZE_INVALID_TTML_FILE = \"ttml/font_size_invalid.xml\";\n  private static final String FONT_SIZE_EMPTY_TTML_FILE = \"ttml/font_size_empty.xml\";\n  private static final String FRAME_RATE_TTML_FILE = \"ttml/frame_rate.xml\";\n  private static final String BITMAP_REGION_FILE = \"ttml/bitmap_percentage_region.xml\";\n  private static final String BITMAP_PIXEL_REGION_FILE = \"ttml/bitmap_pixel_region.xml\";\n  private static final String BITMAP_UNSUPPORTED_REGION_FILE = \"ttml/bitmap_unsupported_region.xml\";\n\n  @Test\n  public void testInlineAttributes() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n\n    TtmlNode root = subtitle.getRoot();\n\n    TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);\n    TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);\n    TtmlStyle firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0).style;\n    assertThat(firstPStyle.getFontColor()).isEqualTo(ColorParser.parseTtmlColor(\"yellow\"));\n    assertThat(firstPStyle.getBackgroundColor()).isEqualTo(ColorParser.parseTtmlColor(\"blue\"));\n    assertThat(firstPStyle.getFontFamily()).isEqualTo(\"serif\");\n    assertThat(firstPStyle.getStyle()).isEqualTo(TtmlStyle.STYLE_BOLD_ITALIC);\n    assertThat(firstPStyle.isUnderline()).isTrue();\n  }\n\n  @Test\n  public void testInheritInlineAttributes() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n    assertSpans(\n        subtitle,\n        20,\n        \"text 2\",\n        \"sansSerif\",\n        TtmlStyle.STYLE_ITALIC,\n        0xFF00FFFF,\n        ColorParser.parseTtmlColor(\"lime\"),\n        false,\n        true,\n        null);\n  }\n\n  /**\n   * Regression test for devices on JellyBean where some named colors are not correctly defined on\n   * framework level. Tests that <i>lime</i> resolves to <code>#FF00FF00</code> not <code>#00FF00\n   * </code>.\n   *\n   * @see <a\n   *     href=\"https://github.com/android/platform_frameworks_base/blob/jb-mr2-release/graphics/java/android/graphics/Color.java#L414\">\n   *     JellyBean Color</a> <a\n   *     href=\"https://github.com/android/platform_frameworks_base/blob/kitkat-mr2.2-release/graphics/java/android/graphics/Color.java#L414\">\n   *     Kitkat Color</a>\n   * @throws IOException thrown if reading subtitle file fails.\n   */\n  @Test\n  public void testLime() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n    assertSpans(\n        subtitle,\n        20,\n        \"text 2\",\n        \"sansSerif\",\n        TtmlStyle.STYLE_ITALIC,\n        0xFF00FFFF,\n        0xFF00FF00,\n        false,\n        true,\n        null);\n  }\n\n  @Test\n  public void testInheritGlobalStyle() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(2);\n    assertSpans(\n        subtitle,\n        10,\n        \"text 1\",\n        \"serif\",\n        TtmlStyle.STYLE_BOLD_ITALIC,\n        0xFF0000FF,\n        0xFFFFFF00,\n        true,\n        false,\n        null);\n  }\n\n  @Test\n  public void testInheritGlobalStyleOverriddenByInlineAttributes()\n      throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_OVERRIDE_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n\n    assertSpans(\n        subtitle,\n        10,\n        \"text 1\",\n        \"serif\",\n        TtmlStyle.STYLE_BOLD_ITALIC,\n        0xFF0000FF,\n        0xFFFFFF00,\n        true,\n        false,\n        null);\n    assertSpans(\n        subtitle,\n        20,\n        \"text 2\",\n        \"sansSerif\",\n        TtmlStyle.STYLE_ITALIC,\n        0xFFFF0000,\n        0xFFFFFF00,\n        true,\n        false,\n        null);\n  }\n\n  @Test\n  public void testInheritGlobalAndParent() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(INHERIT_GLOBAL_AND_PARENT_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n\n    assertSpans(\n        subtitle,\n        10,\n        \"text 1\",\n        \"sansSerif\",\n        TtmlStyle.STYLE_NORMAL,\n        0xFFFF0000,\n        ColorParser.parseTtmlColor(\"lime\"),\n        false,\n        true,\n        Layout.Alignment.ALIGN_CENTER);\n    assertSpans(\n        subtitle,\n        20,\n        \"text 2\",\n        \"serif\",\n        TtmlStyle.STYLE_BOLD_ITALIC,\n        0xFF0000FF,\n        0xFFFFFF00,\n        true,\n        true,\n        Layout.Alignment.ALIGN_CENTER);\n  }\n\n  @Test\n  public void testInheritMultipleStyles() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(12);\n    assertSpans(\n        subtitle,\n        10,\n        \"text 1\",\n        \"sansSerif\",\n        TtmlStyle.STYLE_BOLD_ITALIC,\n        0xFF0000FF,\n        0xFFFFFF00,\n        false,\n        true,\n        null);\n  }\n\n  @Test\n  public void testInheritMultipleStylesWithoutLocalAttributes()\n      throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(12);\n    assertSpans(\n        subtitle,\n        20,\n        \"text 2\",\n        \"sansSerif\",\n        TtmlStyle.STYLE_BOLD_ITALIC,\n        0xFF0000FF,\n        0xFF000000,\n        false,\n        true,\n        null);\n  }\n\n  @Test\n  public void testMergeMultipleStylesWithParentStyle()\n      throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(12);\n    assertSpans(\n        subtitle,\n        30,\n        \"text 2.5\",\n        \"sansSerifInline\",\n        TtmlStyle.STYLE_ITALIC,\n        0xFFFF0000,\n        0xFFFFFF00,\n        true,\n        true,\n        null);\n  }\n\n  @Test\n  public void testMultipleRegions() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE);\n    List<Cue> cues = subtitle.getCues(1000000);\n    assertThat(cues).hasSize(2);\n    Cue cue = cues.get(0);\n    assertThat(cue.text.toString()).isEqualTo(\"lorem\");\n    assertThat(cue.position).isEqualTo(10f / 100f);\n    assertThat(cue.line).isEqualTo(10f / 100f);\n    assertThat(cue.size).isEqualTo(20f / 100f);\n\n    cue = cues.get(1);\n    assertThat(cue.text.toString()).isEqualTo(\"amet\");\n    assertThat(cue.position).isEqualTo(60f / 100f);\n    assertThat(cue.line).isEqualTo(10f / 100f);\n    assertThat(cue.size).isEqualTo(20f / 100f);\n\n    cues = subtitle.getCues(5000000);\n    assertThat(cues).hasSize(1);\n    cue = cues.get(0);\n    assertThat(cue.text.toString()).isEqualTo(\"ipsum\");\n    assertThat(cue.position).isEqualTo(40f / 100f);\n    assertThat(cue.line).isEqualTo(40f / 100f);\n    assertThat(cue.size).isEqualTo(20f / 100f);\n\n    cues = subtitle.getCues(9000000);\n    assertThat(cues).hasSize(1);\n    cue = cues.get(0);\n    assertThat(cue.text.toString()).isEqualTo(\"dolor\");\n    assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);\n    assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);\n    assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);\n    // TODO: Should be as below, once https://github.com/google/ExoPlayer/issues/2953 is fixed.\n    // assertEquals(10f / 100f, cue.position);\n    // assertEquals(80f / 100f, cue.line);\n    // assertEquals(1f, cue.size);\n\n    cues = subtitle.getCues(21000000);\n    assertThat(cues).hasSize(1);\n    cue = cues.get(0);\n    assertThat(cue.text.toString()).isEqualTo(\"She first said this\");\n    assertThat(cue.position).isEqualTo(45f / 100f);\n    assertThat(cue.line).isEqualTo(45f / 100f);\n    assertThat(cue.size).isEqualTo(35f / 100f);\n    cues = subtitle.getCues(25000000);\n    cue = cues.get(0);\n    assertThat(cue.text.toString()).isEqualTo(\"She first said this\\nThen this\");\n    cues = subtitle.getCues(29000000);\n    assertThat(cues).hasSize(1);\n    cue = cues.get(0);\n    assertThat(cue.text.toString()).isEqualTo(\"She first said this\\nThen this\\nFinally this\");\n    assertThat(cue.position).isEqualTo(45f / 100f);\n    assertThat(cue.line).isEqualTo(45f / 100f);\n  }\n\n  @Test\n  public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(12);\n\n    TtmlNode root = subtitle.getRoot();\n    TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);\n    TtmlNode fourthDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 3);\n\n    assertThat(queryChildrenForTag(fourthDiv, TtmlNode.TAG_P, 0).getStyleIds()).isNull();\n  }\n\n  @Test\n  public void testNonexistingStyleId() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(12);\n\n    TtmlNode root = subtitle.getRoot();\n    TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);\n    TtmlNode fifthDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 4);\n\n    assertThat(queryChildrenForTag(fifthDiv, TtmlNode.TAG_P, 0).getStyleIds()).hasLength(1);\n  }\n\n  @Test\n  public void testNonExistingAndExistingStyleIdWithRedundantSpaces()\n      throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(12);\n\n    TtmlNode root = subtitle.getRoot();\n    TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);\n    TtmlNode sixthDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 5);\n\n    String[] styleIds = queryChildrenForTag(sixthDiv, TtmlNode.TAG_P, 0).getStyleIds();\n    assertThat(styleIds).hasLength(2);\n  }\n\n  @Test\n  public void testMultipleChaining() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(CHAIN_MULTIPLE_STYLES_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(2);\n\n    Map<String, TtmlStyle> globalStyles = subtitle.getGlobalStyles();\n\n    TtmlStyle style = globalStyles.get(\"s2\");\n    assertThat(style.getFontFamily()).isEqualTo(\"serif\");\n    assertThat(style.getBackgroundColor()).isEqualTo(0xFFFF0000);\n    assertThat(style.getFontColor()).isEqualTo(0xFF000000);\n    assertThat(style.getStyle()).isEqualTo(TtmlStyle.STYLE_BOLD_ITALIC);\n    assertThat(style.isLinethrough()).isTrue();\n\n    style = globalStyles.get(\"s3\");\n    // only difference: color must be RED\n    assertThat(style.getFontColor()).isEqualTo(0xFFFF0000);\n    assertThat(style.getFontFamily()).isEqualTo(\"serif\");\n    assertThat(style.getBackgroundColor()).isEqualTo(0xFFFF0000);\n    assertThat(style.getStyle()).isEqualTo(TtmlStyle.STYLE_BOLD_ITALIC);\n    assertThat(style.isLinethrough()).isTrue();\n  }\n\n  @Test\n  public void testNoUnderline() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(NO_UNDERLINE_LINETHROUGH_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n\n    TtmlNode root = subtitle.getRoot();\n    TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);\n    TtmlNode div = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);\n\n    TtmlStyle style = queryChildrenForTag(div, TtmlNode.TAG_P, 0).style;\n    assertWithMessage(\"noUnderline from inline attribute expected\")\n        .that(style.isUnderline())\n        .isFalse();\n  }\n\n  @Test\n  public void testNoLinethrough() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(NO_UNDERLINE_LINETHROUGH_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n\n    TtmlNode root = subtitle.getRoot();\n    TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);\n    TtmlNode div = queryChildrenForTag(body, TtmlNode.TAG_DIV, 1);\n\n    TtmlStyle style = queryChildrenForTag(div, TtmlNode.TAG_P, 0).style;\n    assertWithMessage(\"noLineThrough from inline attribute expected in second pNode\")\n        .that(style.isLinethrough())\n        .isFalse();\n  }\n\n  @Test\n  public void testFontSizeSpans() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(10);\n\n    List<Cue> cues = subtitle.getCues(10 * 1000000);\n    assertThat(cues).hasSize(1);\n    SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text;\n    assertThat(String.valueOf(spannable)).isEqualTo(\"text 1\");\n    assertAbsoluteFontSize(spannable, 32);\n\n    cues = subtitle.getCues(20 * 1000000);\n    assertThat(cues).hasSize(1);\n    spannable = (SpannableStringBuilder) cues.get(0).text;\n    assertThat(String.valueOf(cues.get(0).text)).isEqualTo(\"text 2\");\n    assertRelativeFontSize(spannable, 2.2f);\n\n    cues = subtitle.getCues(30 * 1000000);\n    assertThat(cues).hasSize(1);\n    spannable = (SpannableStringBuilder) cues.get(0).text;\n    assertThat(String.valueOf(cues.get(0).text)).isEqualTo(\"text 3\");\n    assertRelativeFontSize(spannable, 1.5f);\n\n    cues = subtitle.getCues(40 * 1000000);\n    assertThat(cues).hasSize(1);\n    spannable = (SpannableStringBuilder) cues.get(0).text;\n    assertThat(String.valueOf(cues.get(0).text)).isEqualTo(\"two values\");\n    assertAbsoluteFontSize(spannable, 16);\n\n    cues = subtitle.getCues(50 * 1000000);\n    assertThat(cues).hasSize(1);\n    spannable = (SpannableStringBuilder) cues.get(0).text;\n    assertThat(String.valueOf(cues.get(0).text)).isEqualTo(\"leading dot\");\n    assertRelativeFontSize(spannable, 0.5f);\n  }\n\n  @Test\n  public void testFontSizeWithMissingUnitIsIgnored() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_MISSING_UNIT_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(2);\n    List<Cue> cues = subtitle.getCues(10 * 1000000);\n    assertThat(cues).hasSize(1);\n    SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text;\n    assertThat(String.valueOf(spannable)).isEqualTo(\"no unit\");\n    assertThat(spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class)).hasLength(0);\n    assertThat(spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class)).hasLength(0);\n  }\n\n  @Test\n  public void testFontSizeWithInvalidValueIsIgnored() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_INVALID_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(6);\n\n    List<Cue> cues = subtitle.getCues(10 * 1000000);\n    assertThat(cues).hasSize(1);\n    SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text;\n    assertThat(String.valueOf(spannable)).isEqualTo(\"invalid\");\n    assertThat(spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class)).hasLength(0);\n    assertThat(spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class)).hasLength(0);\n\n    cues = subtitle.getCues(20 * 1000000);\n    assertThat(cues).hasSize(1);\n    spannable = (SpannableStringBuilder) cues.get(0).text;\n    assertThat(String.valueOf(spannable)).isEqualTo(\"invalid\");\n    assertThat(spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class)).hasLength(0);\n    assertThat(spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class)).hasLength(0);\n\n    cues = subtitle.getCues(30 * 1000000);\n    assertThat(cues).hasSize(1);\n    spannable = (SpannableStringBuilder) cues.get(0).text;\n    assertThat(String.valueOf(spannable)).isEqualTo(\"invalid dot\");\n    assertThat(spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class)).hasLength(0);\n    assertThat(spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class)).hasLength(0);\n  }\n\n  @Test\n  public void testFontSizeWithEmptyValueIsIgnored() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_EMPTY_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(2);\n    List<Cue> cues = subtitle.getCues(10 * 1000000);\n    assertThat(cues).hasSize(1);\n    SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text;\n    assertThat(String.valueOf(spannable)).isEqualTo(\"empty\");\n    assertThat(spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class)).hasLength(0);\n    assertThat(spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class)).hasLength(0);\n  }\n\n  @Test\n  public void testFrameRate() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(FRAME_RATE_TTML_FILE);\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n    assertThat(subtitle.getEventTime(0)).isEqualTo(1_000_000);\n    assertThat(subtitle.getEventTime(1)).isEqualTo(1_010_000);\n    assertThat((double) subtitle.getEventTime(2)).isWithin(1000).of(1_001_000_000);\n    assertThat((double) subtitle.getEventTime(3)).isWithin(2000).of(2_002_000_000);\n  }\n\n  @Test\n  public void testBitmapPercentageRegion() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(BITMAP_REGION_FILE);\n\n    List<Cue> cues = subtitle.getCues(1000000);\n    assertThat(cues).hasSize(1);\n    Cue cue = cues.get(0);\n    assertThat(cue.text).isNull();\n    assertThat(cue.bitmap).isNotNull();\n    assertThat(cue.position).isEqualTo(24f / 100f);\n    assertThat(cue.line).isEqualTo(28f / 100f);\n    assertThat(cue.size).isEqualTo(51f / 100f);\n    assertThat(cue.bitmapHeight).isEqualTo(12f / 100f);\n\n    cues = subtitle.getCues(4000000);\n    assertThat(cues).hasSize(1);\n    cue = cues.get(0);\n    assertThat(cue.text).isNull();\n    assertThat(cue.bitmap).isNotNull();\n    assertThat(cue.position).isEqualTo(21f / 100f);\n    assertThat(cue.line).isEqualTo(35f / 100f);\n    assertThat(cue.size).isEqualTo(57f / 100f);\n    assertThat(cue.bitmapHeight).isEqualTo(6f / 100f);\n\n    cues = subtitle.getCues(7500000);\n    assertThat(cues).hasSize(1);\n    cue = cues.get(0);\n    assertThat(cue.text).isNull();\n    assertThat(cue.bitmap).isNotNull();\n    assertThat(cue.position).isEqualTo(24f / 100f);\n    assertThat(cue.line).isEqualTo(28f / 100f);\n    assertThat(cue.size).isEqualTo(51f / 100f);\n    assertThat(cue.bitmapHeight).isEqualTo(12f / 100f);\n  }\n\n  @Test\n  public void testBitmapPixelRegion() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(BITMAP_PIXEL_REGION_FILE);\n\n    List<Cue> cues = subtitle.getCues(1000000);\n    assertThat(cues).hasSize(1);\n    Cue cue = cues.get(0);\n    assertThat(cue.text).isNull();\n    assertThat(cue.bitmap).isNotNull();\n    assertThat(cue.position).isEqualTo(307f / 1280f);\n    assertThat(cue.line).isEqualTo(562f / 720f);\n    assertThat(cue.size).isEqualTo(653f / 1280f);\n    assertThat(cue.bitmapHeight).isEqualTo(86f / 720f);\n\n    cues = subtitle.getCues(4000000);\n    assertThat(cues).hasSize(1);\n    cue = cues.get(0);\n    assertThat(cue.text).isNull();\n    assertThat(cue.bitmap).isNotNull();\n    assertThat(cue.position).isEqualTo(269f / 1280f);\n    assertThat(cue.line).isEqualTo(612f / 720f);\n    assertThat(cue.size).isEqualTo(730f / 1280f);\n    assertThat(cue.bitmapHeight).isEqualTo(43f / 720f);\n  }\n\n  @Test\n  public void testBitmapUnsupportedRegion() throws IOException, SubtitleDecoderException {\n    TtmlSubtitle subtitle = getSubtitle(BITMAP_UNSUPPORTED_REGION_FILE);\n\n    List<Cue> cues = subtitle.getCues(1000000);\n    assertThat(cues).hasSize(1);\n    Cue cue = cues.get(0);\n    assertThat(cue.text).isNull();\n    assertThat(cue.bitmap).isNotNull();\n    assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);\n    assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);\n    assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);\n    assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);\n\n    cues = subtitle.getCues(4000000);\n    assertThat(cues).hasSize(1);\n    cue = cues.get(0);\n    assertThat(cue.text).isNull();\n    assertThat(cue.bitmap).isNotNull();\n    assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);\n    assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);\n    assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);\n    assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);\n  }\n\n  private void assertSpans(\n      TtmlSubtitle subtitle,\n      int second,\n      String text,\n      String font,\n      int fontStyle,\n      int backgroundColor,\n      int color,\n      boolean isUnderline,\n      boolean isLinethrough,\n      Layout.Alignment alignment) {\n\n    long timeUs = second * 1000000L;\n    List<Cue> cues = subtitle.getCues(timeUs);\n\n    assertThat(cues).hasSize(1);\n    assertThat(String.valueOf(cues.get(0).text)).isEqualTo(text);\n    assertWithMessage(\"single cue expected for timeUs: \" + timeUs).that(cues.size()).isEqualTo(1);\n    SpannableStringBuilder spannable = (SpannableStringBuilder) cues.get(0).text;\n\n    assertFont(spannable, font);\n    assertStyle(spannable, fontStyle);\n    assertUnderline(spannable, isUnderline);\n    assertStrikethrough(spannable, isLinethrough);\n    assertUnderline(spannable, isUnderline);\n    assertBackground(spannable, backgroundColor);\n    assertForeground(spannable, color);\n    assertAlignment(spannable, alignment);\n  }\n\n  private void assertAbsoluteFontSize(Spannable spannable, int absoluteFontSize) {\n    AbsoluteSizeSpan[] absoluteSizeSpans =\n        spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class);\n    assertThat(absoluteSizeSpans).hasLength(1);\n    assertThat(absoluteSizeSpans[0].getSize()).isEqualTo(absoluteFontSize);\n  }\n\n  private void assertRelativeFontSize(Spannable spannable, float relativeFontSize) {\n    RelativeSizeSpan[] relativeSizeSpans =\n        spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class);\n    assertThat(relativeSizeSpans).hasLength(1);\n    assertThat(relativeSizeSpans[0].getSizeChange()).isEqualTo(relativeFontSize);\n  }\n\n  private void assertFont(Spannable spannable, String font) {\n    TypefaceSpan[] typefaceSpans = spannable.getSpans(0, spannable.length(), TypefaceSpan.class);\n    assertThat(typefaceSpans[typefaceSpans.length - 1].getFamily()).isEqualTo(font);\n  }\n\n  private void assertStyle(Spannable spannable, int fontStyle) {\n    StyleSpan[] styleSpans = spannable.getSpans(0, spannable.length(), StyleSpan.class);\n    assertThat(styleSpans[styleSpans.length - 1].getStyle()).isEqualTo(fontStyle);\n  }\n\n  private void assertUnderline(Spannable spannable, boolean isUnderline) {\n    UnderlineSpan[] underlineSpans = spannable.getSpans(0, spannable.length(), UnderlineSpan.class);\n    assertWithMessage(isUnderline ? \"must be underlined\" : \"must not be underlined\")\n        .that(underlineSpans)\n        .hasLength(isUnderline ? 1 : 0);\n  }\n\n  private void assertStrikethrough(Spannable spannable, boolean isStrikethrough) {\n    StrikethroughSpan[] striketroughSpans =\n        spannable.getSpans(0, spannable.length(), StrikethroughSpan.class);\n    assertWithMessage(isStrikethrough ? \"must be strikethrough\" : \"must not be strikethrough\")\n        .that(striketroughSpans)\n        .hasLength(isStrikethrough ? 1 : 0);\n  }\n\n  private void assertBackground(Spannable spannable, int backgroundColor) {\n    BackgroundColorSpan[] backgroundColorSpans =\n        spannable.getSpans(0, spannable.length(), BackgroundColorSpan.class);\n    if (backgroundColor != 0) {\n      assertThat(backgroundColorSpans[backgroundColorSpans.length - 1].getBackgroundColor())\n          .isEqualTo(backgroundColor);\n    } else {\n      assertThat(backgroundColorSpans).hasLength(0);\n    }\n  }\n\n  private void assertForeground(Spannable spannable, int foregroundColor) {\n    ForegroundColorSpan[] foregroundColorSpans =\n        spannable.getSpans(0, spannable.length(), ForegroundColorSpan.class);\n    assertThat(foregroundColorSpans[foregroundColorSpans.length - 1].getForegroundColor())\n        .isEqualTo(foregroundColor);\n  }\n\n  private void assertAlignment(Spannable spannable, Layout.Alignment alignment) {\n    if (alignment != null) {\n      AlignmentSpan.Standard[] alignmentSpans =\n          spannable.getSpans(0, spannable.length(), AlignmentSpan.Standard.class);\n      assertThat(alignmentSpans).hasLength(1);\n      assertThat(alignmentSpans[0].getAlignment()).isEqualTo(alignment);\n    } else {\n      assertThat(spannable.getSpans(0, spannable.length(), AlignmentSpan.Standard.class))\n          .hasLength(0);\n    }\n  }\n\n  private TtmlNode queryChildrenForTag(TtmlNode node, String tag, int pos) {\n    int count = 0;\n    for (int i = 0; i < node.getChildCount(); i++) {\n      if (tag.equals(node.getChild(i).tag)) {\n        if (pos == count++) {\n          return node.getChild(i);\n        }\n      }\n    }\n    throw new IllegalStateException(\"tag not found\");\n  }\n\n  private TtmlSubtitle getSubtitle(String file) throws IOException, SubtitleDecoderException {\n    TtmlDecoder ttmlDecoder = new TtmlDecoder();\n    byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file);\n    return ttmlDecoder.decode(bytes, bytes.length, false);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static android.graphics.Color.BLACK;\nimport static android.graphics.Color.RED;\nimport static android.graphics.Color.YELLOW;\nimport static com.google.android.exoplayer2.text.ttml.TtmlRenderUtil.resolveStyle;\nimport static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD;\nimport static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD_ITALIC;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.graphics.Color;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link TtmlRenderUtil}. */\n@RunWith(AndroidJUnit4.class)\npublic final class TtmlRenderUtilTest {\n\n  @Test\n  public void testResolveStyleNoStyleAtAll() {\n    assertThat(resolveStyle(null, null, null)).isNull();\n  }\n\n  @Test\n  public void testResolveStyleSingleReferentialStyle() {\n    Map<String, TtmlStyle> globalStyles = getGlobalStyles();\n    String[] styleIds = {\"s0\"};\n\n    assertThat(TtmlRenderUtil.resolveStyle(null, styleIds, globalStyles))\n        .isSameAs(globalStyles.get(\"s0\"));\n  }\n\n  @Test\n  public void testResolveStyleMultipleReferentialStyles() {\n    Map<String, TtmlStyle> globalStyles = getGlobalStyles();\n    String[] styleIds = {\"s0\", \"s1\"};\n\n    TtmlStyle resolved = TtmlRenderUtil.resolveStyle(null, styleIds, globalStyles);\n    assertThat(resolved).isNotSameAs(globalStyles.get(\"s0\"));\n    assertThat(resolved).isNotSameAs(globalStyles.get(\"s1\"));\n    assertThat(resolved.getId()).isNull();\n\n    // inherited from s0\n    assertThat(resolved.getBackgroundColor()).isEqualTo(BLACK);\n    // inherited from s1\n    assertThat(resolved.getFontColor()).isEqualTo(RED);\n    // merged from s0 and s1\n    assertThat(resolved.getStyle()).isEqualTo(STYLE_BOLD_ITALIC);\n  }\n\n  @Test\n  public void testResolveMergeSingleReferentialStyleIntoInlineStyle() {\n    Map<String, TtmlStyle> globalStyles = getGlobalStyles();\n    String[] styleIds = {\"s0\"};\n    TtmlStyle style = new TtmlStyle();\n    style.setBackgroundColor(Color.YELLOW);\n\n    TtmlStyle resolved = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);\n    assertThat(resolved).isSameAs(style);\n\n    // inline attribute not overridden\n    assertThat(resolved.getBackgroundColor()).isEqualTo(YELLOW);\n    // inherited from referential style\n    assertThat(resolved.getStyle()).isEqualTo(STYLE_BOLD);\n  }\n\n  @Test\n  public void testResolveMergeMultipleReferentialStylesIntoInlineStyle() {\n    Map<String, TtmlStyle> globalStyles = getGlobalStyles();\n    String[] styleIds = {\"s0\", \"s1\"};\n    TtmlStyle style = new TtmlStyle();\n    style.setBackgroundColor(Color.YELLOW);\n\n    TtmlStyle resolved = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);\n    assertThat(resolved).isSameAs(style);\n\n    // inline attribute not overridden\n    assertThat(resolved.getBackgroundColor()).isEqualTo(YELLOW);\n    // inherited from both referential style\n    assertThat(resolved.getStyle()).isEqualTo(STYLE_BOLD_ITALIC);\n  }\n\n  @Test\n  public void testResolveStyleOnlyInlineStyle() {\n    TtmlStyle inlineStyle = new TtmlStyle();\n    assertThat(TtmlRenderUtil.resolveStyle(inlineStyle, null, null)).isSameAs(inlineStyle);\n  }\n\n  private static Map<String, TtmlStyle> getGlobalStyles() {\n    Map<String, TtmlStyle> globalStyles = new HashMap<>();\n\n    TtmlStyle s0 = new TtmlStyle();\n    s0.setId(\"s0\");\n    s0.setBackgroundColor(Color.BLACK);\n    s0.setBold(true);\n    globalStyles.put(s0.getId(), s0);\n\n    TtmlStyle s1 = new TtmlStyle();\n    s1.setId(\"s1\");\n    s1.setBackgroundColor(Color.RED);\n    s1.setFontColor(Color.RED);\n    s1.setItalic(true);\n    globalStyles.put(s1.getId(), s1);\n\n    return globalStyles;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static android.graphics.Color.BLACK;\nimport static android.graphics.Color.WHITE;\nimport static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD;\nimport static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD_ITALIC;\nimport static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_ITALIC;\nimport static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_NORMAL;\nimport static com.google.android.exoplayer2.text.ttml.TtmlStyle.UNSPECIFIED;\nimport static com.google.common.truth.Truth.assertThat;\nimport static com.google.common.truth.Truth.assertWithMessage;\n\nimport android.graphics.Color;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link TtmlStyle}. */\n@RunWith(AndroidJUnit4.class)\npublic final class TtmlStyleTest {\n\n  private static final String FONT_FAMILY = \"serif\";\n  private static final String ID = \"id\";\n  public static final int FOREGROUND_COLOR = Color.WHITE;\n  public static final int BACKGROUND_COLOR = Color.BLACK;\n  private TtmlStyle style;\n\n  @Before\n  public void setUp() throws Exception {\n    style = new TtmlStyle();\n  }\n\n  @Test\n  public void testInheritStyle() {\n    style.inherit(createAncestorStyle());\n    assertWithMessage(\"id must not be inherited\").that(style.getId()).isNull();\n    assertThat(style.isUnderline()).isTrue();\n    assertThat(style.isLinethrough()).isTrue();\n    assertThat(style.getStyle()).isEqualTo(STYLE_BOLD_ITALIC);\n    assertThat(style.getFontFamily()).isEqualTo(FONT_FAMILY);\n    assertThat(style.getFontColor()).isEqualTo(WHITE);\n    assertWithMessage(\"do not inherit backgroundColor\").that(style.hasBackgroundColor()).isFalse();\n  }\n\n  @Test\n  public void testChainStyle() {\n    style.chain(createAncestorStyle());\n    assertWithMessage(\"id must not be inherited\").that(style.getId()).isNull();\n    assertThat(style.isUnderline()).isTrue();\n    assertThat(style.isLinethrough()).isTrue();\n    assertThat(style.getStyle()).isEqualTo(STYLE_BOLD_ITALIC);\n    assertThat(style.getFontFamily()).isEqualTo(FONT_FAMILY);\n    assertThat(style.getFontColor()).isEqualTo(FOREGROUND_COLOR);\n    // do inherit backgroundColor when chaining\n    assertWithMessage(\"do not inherit backgroundColor when chaining\")\n        .that(style.getBackgroundColor()).isEqualTo(BACKGROUND_COLOR);\n  }\n\n  private static TtmlStyle createAncestorStyle() {\n    TtmlStyle ancestor = new TtmlStyle();\n    ancestor.setId(ID);\n    ancestor.setItalic(true);\n    ancestor.setBold(true);\n    ancestor.setBackgroundColor(BACKGROUND_COLOR);\n    ancestor.setFontColor(FOREGROUND_COLOR);\n    ancestor.setLinethrough(true);\n    ancestor.setUnderline(true);\n    ancestor.setFontFamily(FONT_FAMILY);\n    return ancestor;\n  }\n\n  @Test\n  public void testStyle() {\n    assertThat(style.getStyle()).isEqualTo(UNSPECIFIED);\n    style.setItalic(true);\n    assertThat(style.getStyle()).isEqualTo(STYLE_ITALIC);\n    style.setBold(true);\n    assertThat(style.getStyle()).isEqualTo(STYLE_BOLD_ITALIC);\n    style.setItalic(false);\n    assertThat(style.getStyle()).isEqualTo(STYLE_BOLD);\n    style.setBold(false);\n    assertThat(style.getStyle()).isEqualTo(STYLE_NORMAL);\n  }\n\n  @Test\n  public void testLinethrough() {\n    assertThat(style.isLinethrough()).isFalse();\n    style.setLinethrough(true);\n    assertThat(style.isLinethrough()).isTrue();\n    style.setLinethrough(false);\n    assertThat(style.isLinethrough()).isFalse();\n  }\n\n  @Test\n  public void testUnderline() {\n    assertThat(style.isUnderline()).isFalse();\n    style.setUnderline(true);\n    assertThat(style.isUnderline()).isTrue();\n    style.setUnderline(false);\n    assertThat(style.isUnderline()).isFalse();\n  }\n\n  @Test\n  public void testFontFamily() {\n    assertThat(style.getFontFamily()).isNull();\n    style.setFontFamily(FONT_FAMILY);\n    assertThat(style.getFontFamily()).isEqualTo(FONT_FAMILY);\n    style.setFontFamily(null);\n    assertThat(style.getFontFamily()).isNull();\n  }\n\n  @Test\n  public void testColor() {\n    assertThat(style.hasFontColor()).isFalse();\n    style.setFontColor(Color.BLACK);\n    assertThat(style.getFontColor()).isEqualTo(BLACK);\n    assertThat(style.hasFontColor()).isTrue();\n  }\n\n  @Test\n  public void testBackgroundColor() {\n    assertThat(style.hasBackgroundColor()).isFalse();\n    style.setBackgroundColor(Color.BLACK);\n    assertThat(style.getBackgroundColor()).isEqualTo(BLACK);\n    assertThat(style.hasBackgroundColor()).isTrue();\n  }\n\n  @Test\n  public void testId() {\n    assertThat(style.getId()).isNull();\n    style.setId(ID);\n    assertThat(style.getId()).isEqualTo(ID);\n    style.setId(null);\n    assertThat(style.getId()).isNull();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.graphics.Color;\nimport android.graphics.Typeface;\nimport android.text.SpannedString;\nimport android.text.style.ForegroundColorSpan;\nimport android.text.style.StyleSpan;\nimport android.text.style.TypefaceSpan;\nimport android.text.style.UnderlineSpan;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.text.SubtitleDecoderException;\nimport java.io.IOException;\nimport java.util.Collections;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link Tx3gDecoder}. */\n@RunWith(AndroidJUnit4.class)\npublic final class Tx3gDecoderTest {\n\n  private static final String NO_SUBTITLE = \"tx3g/no_subtitle\";\n  private static final String SAMPLE_JUST_TEXT = \"tx3g/sample_just_text\";\n  private static final String SAMPLE_WITH_STYL = \"tx3g/sample_with_styl\";\n  private static final String SAMPLE_WITH_STYL_ALL_DEFAULTS = \"tx3g/sample_with_styl_all_defaults\";\n  private static final String SAMPLE_UTF16_BE_NO_STYL = \"tx3g/sample_utf16_be_no_styl\";\n  private static final String SAMPLE_UTF16_LE_NO_STYL = \"tx3g/sample_utf16_le_no_styl\";\n  private static final String SAMPLE_WITH_MULTIPLE_STYL = \"tx3g/sample_with_multiple_styl\";\n  private static final String SAMPLE_WITH_OTHER_EXTENSION = \"tx3g/sample_with_other_extension\";\n  private static final String SAMPLE_WITH_TBOX = \"tx3g/sample_with_tbox\";\n  private static final String INITIALIZATION = \"tx3g/initialization\";\n  private static final String INITIALIZATION_ALL_DEFAULTS = \"tx3g/initialization_all_defaults\";\n\n  @Test\n  public void testDecodeNoSubtitle() throws IOException, SubtitleDecoderException {\n    Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());\n    byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NO_SUBTITLE);\n    Subtitle subtitle = decoder.decode(bytes, bytes.length, false);\n    assertThat(subtitle.getCues(0)).isEmpty();\n  }\n\n  @Test\n  public void testDecodeJustText() throws IOException, SubtitleDecoderException {\n    Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());\n    byte[] bytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_JUST_TEXT);\n    Subtitle subtitle = decoder.decode(bytes, bytes.length, false);\n    SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);\n    assertThat(text.toString()).isEqualTo(\"CC Test\");\n    assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(0);\n    assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);\n  }\n\n  @Test\n  public void testDecodeWithStyl() throws IOException, SubtitleDecoderException {\n    Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());\n    byte[] bytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL);\n    Subtitle subtitle = decoder.decode(bytes, bytes.length, false);\n    SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);\n    assertThat(text.toString()).isEqualTo(\"CC Test\");\n    assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(3);\n    StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class);\n    assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD_ITALIC);\n    findSpan(text, 0, 6, UnderlineSpan.class);\n    ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);\n    assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);\n    assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);\n  }\n\n  @Test\n  public void testDecodeWithStylAllDefaults() throws IOException, SubtitleDecoderException {\n    Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());\n    byte[] bytes =\n        TestUtil.getByteArray(\n            ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL_ALL_DEFAULTS);\n    Subtitle subtitle = decoder.decode(bytes, bytes.length, false);\n    SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);\n    assertThat(text.toString()).isEqualTo(\"CC Test\");\n    assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(0);\n    assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);\n  }\n\n  @Test\n  public void testDecodeUtf16BeNoStyl() throws IOException, SubtitleDecoderException {\n    Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());\n    byte[] bytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_UTF16_BE_NO_STYL);\n    Subtitle subtitle = decoder.decode(bytes, bytes.length, false);\n    SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);\n    assertThat(text.toString()).isEqualTo(\"你好\");\n    assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(0);\n    assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);\n  }\n\n  @Test\n  public void testDecodeUtf16LeNoStyl() throws IOException, SubtitleDecoderException {\n    Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());\n    byte[] bytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_UTF16_LE_NO_STYL);\n    Subtitle subtitle = decoder.decode(bytes, bytes.length, false);\n    SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);\n    assertThat(text.toString()).isEqualTo(\"你好\");\n    assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(0);\n    assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);\n  }\n\n  @Test\n  public void testDecodeWithMultipleStyl() throws IOException, SubtitleDecoderException {\n    Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());\n    byte[] bytes =\n        TestUtil.getByteArray(\n            ApplicationProvider.getApplicationContext(), SAMPLE_WITH_MULTIPLE_STYL);\n    Subtitle subtitle = decoder.decode(bytes, bytes.length, false);\n    SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);\n    assertThat(text.toString()).isEqualTo(\"Line 2\\nLine 3\");\n    assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(4);\n    StyleSpan styleSpan = findSpan(text, 0, 5, StyleSpan.class);\n    assertThat(styleSpan.getStyle()).isEqualTo(Typeface.ITALIC);\n    findSpan(text, 7, 12, UnderlineSpan.class);\n    ForegroundColorSpan colorSpan = findSpan(text, 0, 5, ForegroundColorSpan.class);\n    assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);\n    colorSpan = findSpan(text, 7, 12, ForegroundColorSpan.class);\n    assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);\n    assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);\n  }\n\n  @Test\n  public void testDecodeWithOtherExtension() throws IOException, SubtitleDecoderException {\n    Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());\n    byte[] bytes =\n        TestUtil.getByteArray(\n            ApplicationProvider.getApplicationContext(), SAMPLE_WITH_OTHER_EXTENSION);\n    Subtitle subtitle = decoder.decode(bytes, bytes.length, false);\n    SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);\n    assertThat(text.toString()).isEqualTo(\"CC Test\");\n    assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(2);\n    StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class);\n    assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD);\n    ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);\n    assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);\n    assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);\n  }\n\n  @Test\n  public void testInitializationDecodeWithStyl() throws IOException, SubtitleDecoderException {\n    byte[] initBytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INITIALIZATION);\n    Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));\n    byte[] bytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL);\n    Subtitle subtitle = decoder.decode(bytes, bytes.length, false);\n    SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);\n    assertThat(text.toString()).isEqualTo(\"CC Test\");\n    assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(5);\n    StyleSpan styleSpan = findSpan(text, 0, text.length(), StyleSpan.class);\n    assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD_ITALIC);\n    findSpan(text, 0, text.length(), UnderlineSpan.class);\n    TypefaceSpan typefaceSpan = findSpan(text, 0, text.length(), TypefaceSpan.class);\n    assertThat(typefaceSpan.getFamily()).isEqualTo(C.SERIF_NAME);\n    ForegroundColorSpan colorSpan = findSpan(text, 0, text.length(), ForegroundColorSpan.class);\n    assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.RED);\n    colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);\n    assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);\n    assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1f);\n  }\n\n  @Test\n  public void testInitializationDecodeWithTbox() throws IOException, SubtitleDecoderException {\n    byte[] initBytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INITIALIZATION);\n    Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));\n    byte[] bytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_TBOX);\n    Subtitle subtitle = decoder.decode(bytes, bytes.length, false);\n    SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);\n    assertThat(text.toString()).isEqualTo(\"CC Test\");\n    assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(4);\n    StyleSpan styleSpan = findSpan(text, 0, text.length(), StyleSpan.class);\n    assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD_ITALIC);\n    findSpan(text, 0, text.length(), UnderlineSpan.class);\n    TypefaceSpan typefaceSpan = findSpan(text, 0, text.length(), TypefaceSpan.class);\n    assertThat(typefaceSpan.getFamily()).isEqualTo(C.SERIF_NAME);\n    ForegroundColorSpan colorSpan = findSpan(text, 0, text.length(), ForegroundColorSpan.class);\n    assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.RED);\n    assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1875f);\n  }\n\n  @Test\n  public void testInitializationAllDefaultsDecodeWithStyl()\n      throws IOException, SubtitleDecoderException {\n    byte[] initBytes =\n        TestUtil.getByteArray(\n            ApplicationProvider.getApplicationContext(), INITIALIZATION_ALL_DEFAULTS);\n    Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));\n    byte[] bytes =\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL);\n    Subtitle subtitle = decoder.decode(bytes, bytes.length, false);\n    SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);\n    assertThat(text.toString()).isEqualTo(\"CC Test\");\n    assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(3);\n    StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class);\n    assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD_ITALIC);\n    findSpan(text, 0, 6, UnderlineSpan.class);\n    ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);\n    assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);\n    assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);\n  }\n\n  private static <T> T findSpan(\n      SpannedString testObject, int expectedStart, int expectedEnd, Class<T> expectedType) {\n    T[] spans = testObject.getSpans(0, testObject.length(), expectedType);\n    for (T span : spans) {\n      if (testObject.getSpanStart(span) == expectedStart\n          && testObject.getSpanEnd(span) == expectedEnd) {\n        return span;\n      }\n    }\n    fail(\"Span not found.\");\n    return null;\n  }\n\n  private static void assertFractionalLinePosition(Cue cue, float expectedFraction) {\n    assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);\n    assertThat(cue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START);\n    assertThat(Math.abs(expectedFraction - cue.line) < 1e-6).isTrue();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 com.google.android.exoplayer2.text.webvtt.CssParser.parseNextToken;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.List;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link CssParser}. */\n@RunWith(AndroidJUnit4.class)\npublic final class CssParserTest {\n\n  private CssParser parser;\n\n  @Before\n  public void setUp() {\n    parser = new CssParser();\n  }\n\n  @Test\n  public void testSkipWhitespacesAndComments() {\n    // Skip only whitespaces\n    String skipOnlyWhitespaces = \" \\t\\r\\n\\f End of skip\\n /*  */\";\n    assertSkipsToEndOfSkip(\"End of skip\", skipOnlyWhitespaces);\n\n    // Skip only comments.\n    String skipOnlyComments = \"/*A comment***//*/It even has spaces in it*/End of skip\";\n    assertSkipsToEndOfSkip(\"End of skip\", skipOnlyComments);\n\n    // Skip interleaved.\n    String skipInterleaved = \" /* We have comments and */\\t\\n/* whitespaces*/ End of skip\";\n    assertSkipsToEndOfSkip(\"End of skip\", skipInterleaved);\n\n    // Skip nothing.\n    String skipNothing = \"End of skip\\n \\t \\r\";\n    assertSkipsToEndOfSkip(\"End of skip\", skipNothing);\n\n    // Skip everything.\n    String skipEverything = \"\\t/* Comment */\\n\\r/* And another */\";\n    assertSkipsToEndOfSkip(null, skipEverything);\n  }\n\n  @Test\n  public void testGetInputLimit() {\n    // \\r After 3 lines.\n    String threeLinesThen3Cr = \"One Line\\nThen other\\rAnd finally\\r\\r\\r\";\n    assertInputLimit(\"\", threeLinesThen3Cr);\n\n    // \\r\\r After 3 lines\n    String threeLinesThen2Cr = \"One Line\\nThen other\\r\\nAnd finally\\r\\r\";\n    assertInputLimit(null, threeLinesThen2Cr);\n\n    // \\n\\n After 3 lines.\n    String threeLinesThen2Lf = \"One Line\\nThen other\\r\\nAnd finally\\n\\nFinal\\n\\n\\nLine\";\n    assertInputLimit(\"Final\", threeLinesThen2Lf);\n\n    // \\r\\n\\n After 3 lines.\n    String threeLinesThenCr2Lf = \" \\n \\r\\n \\r\\n\\nFinal\\n\\n\\nLine\";\n    assertInputLimit(\"Final\", threeLinesThenCr2Lf);\n\n    // Limit immediately.\n    String immediateEmptyLine = \"\\nLine\\nEnd\";\n    assertInputLimit(\"Line\", immediateEmptyLine);\n\n    // Empty string.\n    assertInputLimit(null, \"\");\n  }\n\n  @Test\n  public void testParseMethodSimpleInput() {\n    WebvttCssStyle expectedStyle = new WebvttCssStyle();\n    String styleBlock1 = \" ::cue { color : black; background-color: PapayaWhip }\";\n    expectedStyle.setFontColor(0xFF000000);\n    expectedStyle.setBackgroundColor(0xFFFFEFD5);\n    assertParserProduces(styleBlock1, expectedStyle);\n\n    String styleBlock2 = \" ::cue { color : black }\\n\\n::cue { color : invalid }\";\n    expectedStyle = new WebvttCssStyle();\n    expectedStyle.setFontColor(0xFF000000);\n    assertParserProduces(styleBlock2, expectedStyle);\n\n    String styleBlock3 = \"::cue {\\n background-color\\n:#00fFFe}\";\n    expectedStyle = new WebvttCssStyle();\n    expectedStyle.setBackgroundColor(0xFF00FFFE);\n    assertParserProduces(styleBlock3, expectedStyle);\n  }\n\n  @Test\n  public void testParseMethodMultipleRulesInBlockInput() {\n    String styleBlock =\n        \"::cue {\\n background-color\\n:#00fFFe}      \\n::cue {\\n background-color\\n:#00000000}\\n\";\n    WebvttCssStyle expectedStyle = new WebvttCssStyle();\n    expectedStyle.setBackgroundColor(0xFF00FFFE);\n    WebvttCssStyle secondExpectedStyle = new WebvttCssStyle();\n    secondExpectedStyle.setBackgroundColor(0x000000);\n    assertParserProduces(styleBlock, expectedStyle, secondExpectedStyle);\n  }\n\n  @Test\n  public void testMultiplePropertiesInBlock() {\n    String styleBlock = \"::cue(#id){text-decoration:underline; background-color:green;\"\n        + \"color:red; font-family:Courier; font-weight:bold}\";\n    WebvttCssStyle expectedStyle = new WebvttCssStyle();\n    expectedStyle.setTargetId(\"id\");\n    expectedStyle.setUnderline(true);\n    expectedStyle.setBackgroundColor(0xFF008000);\n    expectedStyle.setFontColor(0xFFFF0000);\n    expectedStyle.setFontFamily(\"courier\");\n    expectedStyle.setBold(true);\n\n    assertParserProduces(styleBlock, expectedStyle);\n  }\n\n  @Test\n  public void testRgbaColorExpression() {\n    String styleBlock = \"::cue(#rgb){background-color: rgba(\\n10/* Ugly color */,11\\t, 12\\n,.1);\"\n        + \"color:rgb(1,1,\\n1)}\";\n    WebvttCssStyle expectedStyle = new WebvttCssStyle();\n    expectedStyle.setTargetId(\"rgb\");\n    expectedStyle.setBackgroundColor(0x190A0B0C);\n    expectedStyle.setFontColor(0xFF010101);\n\n    assertParserProduces(styleBlock, expectedStyle);\n  }\n\n  @Test\n  public void testGetNextToken() {\n    String stringInput = \" lorem:ipsum\\n{dolor}#sit,amet;lorem:ipsum\\r\\t\\f\\ndolor(())\\n\";\n    ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(stringInput));\n    StringBuilder builder = new StringBuilder();\n    assertThat(parseNextToken(input, builder)).isEqualTo(\"lorem\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\":\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\"ipsum\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\"{\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\"dolor\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\"}\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\"#sit\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\",\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\"amet\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\";\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\"lorem\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\":\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\"ipsum\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\"dolor\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\"(\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\"(\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\")\");\n    assertThat(parseNextToken(input, builder)).isEqualTo(\")\");\n    assertThat(parseNextToken(input, builder)).isNull();\n  }\n\n  @Test\n  public void testStyleScoreSystem() {\n    WebvttCssStyle style = new WebvttCssStyle();\n    // Universal selector.\n    assertThat(style.getSpecificityScore(\"\", \"\", new String[0], \"\")).isEqualTo(1);\n    // Class match without tag match.\n    style.setTargetClasses(new String[] { \"class1\", \"class2\"});\n    assertThat(style.getSpecificityScore(\"\", \"\", new String[]{\"class1\", \"class2\", \"class3\"},\n        \"\")).isEqualTo(8);\n    // Class and tag match\n    style.setTargetTagName(\"b\");\n    assertThat(style.getSpecificityScore(\"\", \"b\",\n        new String[]{\"class1\", \"class2\", \"class3\"}, \"\")).isEqualTo(10);\n    // Class insufficiency.\n    assertThat(style.getSpecificityScore(\"\", \"b\", new String[]{\"class1\", \"class\"}, \"\"))\n        .isEqualTo(0);\n    // Voice, classes and tag match.\n    style.setTargetVoice(\"Manuel Cráneo\");\n    assertThat(style.getSpecificityScore(\"\", \"b\",\n        new String[]{\"class1\", \"class2\", \"class3\"}, \"Manuel Cráneo\")).isEqualTo(14);\n    // Voice mismatch.\n    assertThat(style.getSpecificityScore(null, \"b\",\n        new String[]{\"class1\", \"class2\", \"class3\"}, \"Manuel Craneo\")).isEqualTo(0);\n    // Id, voice, classes and tag match.\n    style.setTargetId(\"id\");\n    assertThat(style.getSpecificityScore(\"id\", \"b\",\n        new String[]{\"class1\", \"class2\", \"class3\"}, \"Manuel Cráneo\")).isEqualTo(0x40000000 + 14);\n    // Id mismatch.\n    assertThat(style.getSpecificityScore(\"id1\", \"b\",\n        new String[]{\"class1\", \"class2\", \"class3\"}, \"\")).isEqualTo(0);\n  }\n\n  // Utility methods.\n\n  private void assertSkipsToEndOfSkip(String expectedLine, String s) {\n    ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s));\n    CssParser.skipWhitespaceAndComments(input);\n    assertThat(input.readLine()).isEqualTo(expectedLine);\n  }\n\n  private void assertInputLimit(String expectedLine, String s) {\n    ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s));\n    CssParser.skipStyleBlock(input);\n    assertThat(input.readLine()).isEqualTo(expectedLine);\n  }\n\n  private void assertParserProduces(String styleBlock, WebvttCssStyle... expectedStyles) {\n    ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(styleBlock));\n    List<WebvttCssStyle> styles = parser.parseBlock(input);\n    assertThat(styles.size()).isEqualTo(expectedStyles.length);\n    for (int i = 0; i < expectedStyles.length; i++) {\n      WebvttCssStyle expected = expectedStyles[i];\n      WebvttCssStyle actualElem = styles.get(i);\n      assertThat(actualElem.hasBackgroundColor()).isEqualTo(expected.hasBackgroundColor());\n      if (expected.hasBackgroundColor()) {\n        assertThat(actualElem.getBackgroundColor()).isEqualTo(expected.getBackgroundColor());\n      }\n      assertThat(actualElem.hasFontColor()).isEqualTo(expected.hasFontColor());\n      if (expected.hasFontColor()) {\n        assertThat(actualElem.getFontColor()).isEqualTo(expected.getFontColor());\n      }\n      assertThat(actualElem.getFontFamily()).isEqualTo(expected.getFontFamily());\n      assertThat(actualElem.getFontSize()).isEqualTo(expected.getFontSize());\n      assertThat(actualElem.getFontSizeUnit()).isEqualTo(expected.getFontSizeUnit());\n      assertThat(actualElem.getStyle()).isEqualTo(expected.getStyle());\n      assertThat(actualElem.isLinethrough()).isEqualTo(expected.isLinethrough());\n      assertThat(actualElem.isUnderline()).isEqualTo(expected.isUnderline());\n      assertThat(actualElem.getTextAlign()).isEqualTo(expected.getTextAlign());\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.text.SubtitleDecoderException;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link Mp4WebvttDecoder}. */\n@RunWith(AndroidJUnit4.class)\npublic final class Mp4WebvttDecoderTest {\n\n  private static final byte[] SINGLE_CUE_SAMPLE = {\n      0x00, 0x00, 0x00, 0x1C,  // Size\n      0x76, 0x74, 0x74, 0x63,  // \"vttc\" Box type. VTT Cue box begins:\n\n      0x00, 0x00, 0x00, 0x14,  // Contained payload box's size\n      0x70, 0x61, 0x79, 0x6c,  // Contained payload box's type (payl), Cue Payload Box begins:\n\n      0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x0a // Hello World\\n\n  };\n\n  private static final byte[] DOUBLE_CUE_SAMPLE = {\n      0x00, 0x00, 0x00, 0x1B,  // Size\n      0x76, 0x74, 0x74, 0x63,  // \"vttc\" Box type. First VTT Cue box begins:\n\n      0x00, 0x00, 0x00, 0x13,  // First contained payload box's size\n      0x70, 0x61, 0x79, 0x6c,  // First contained payload box's type (payl), Cue Payload Box begins:\n\n      0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, // Hello World\n\n      0x00, 0x00, 0x00, 0x17,  // Size\n      0x76, 0x74, 0x74, 0x63,  // \"vttc\" Box type. Second VTT Cue box begins:\n\n      0x00, 0x00, 0x00, 0x0F,  // Contained payload box's size\n      0x70, 0x61, 0x79, 0x6c,  // Contained payload box's type (payl), Payload begins:\n\n      0x42, 0x79, 0x65, 0x20, 0x42, 0x79, 0x65  // Bye Bye\n  };\n\n  private static final byte[] NO_CUE_SAMPLE = {\n      0x00, 0x00, 0x00, 0x1B,  // Size\n      0x74, 0x74, 0x74, 0x63,  // \"tttc\" Box type, which is not a Cue. Should be skipped:\n\n      0x00, 0x00, 0x00, 0x13,  // Contained payload box's size\n      0x70, 0x61, 0x79, 0x6c,  // Contained payload box's type (payl), Cue Payload Box begins:\n\n      0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64 // Hello World\n  };\n\n  private static final byte[] INCOMPLETE_HEADER_SAMPLE = {\n      0x00, 0x00, 0x00, 0x23,  // Size\n      0x76, 0x74, 0x74, 0x63,  // \"vttc\" Box type. VTT Cue box begins:\n\n      0x00, 0x00, 0x00, 0x14,  // Contained payload box's size\n      0x70, 0x61, 0x79, 0x6c,  // Contained payload box's type (payl), Cue Payload Box begins:\n\n      0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x0a, // Hello World\\n\n\n      0x00, 0x00, 0x00, 0x07, // Size of an incomplete header, which belongs to the first vttc box.\n      0x76, 0x74, 0x74\n  };\n\n  // Positive tests.\n\n  @Test\n  public void testSingleCueSample() throws SubtitleDecoderException {\n    Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();\n    Subtitle result = decoder.decode(SINGLE_CUE_SAMPLE, SINGLE_CUE_SAMPLE.length, false);\n    Cue expectedCue = new Cue(\"Hello World\"); // Line feed must be trimmed by the decoder\n    assertMp4WebvttSubtitleEquals(result, expectedCue);\n  }\n\n  @Test\n  public void testTwoCuesSample() throws SubtitleDecoderException {\n    Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();\n    Subtitle result = decoder.decode(DOUBLE_CUE_SAMPLE, DOUBLE_CUE_SAMPLE.length, false);\n    Cue firstExpectedCue = new Cue(\"Hello World\");\n    Cue secondExpectedCue = new Cue(\"Bye Bye\");\n    assertMp4WebvttSubtitleEquals(result, firstExpectedCue, secondExpectedCue);\n  }\n\n  @Test\n  public void testNoCueSample() throws SubtitleDecoderException {\n    Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();\n    Subtitle result = decoder.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length, false);\n    assertMp4WebvttSubtitleEquals(result);\n  }\n\n  // Negative tests.\n\n  @Test\n  public void testSampleWithIncompleteHeader() {\n    Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();\n    try {\n      decoder.decode(INCOMPLETE_HEADER_SAMPLE, INCOMPLETE_HEADER_SAMPLE.length, false);\n    } catch (SubtitleDecoderException e) {\n      return;\n    }\n    fail();\n  }\n\n  // Util methods\n\n  /**\n   * Asserts that the Subtitle's cues (which are all part of the event at t=0) are equal to the\n   * expected Cues.\n   *\n   * @param subtitle The {@link Subtitle} to check.\n   * @param expectedCues The expected {@link Cue}s.\n   */\n  private static void assertMp4WebvttSubtitleEquals(Subtitle subtitle, Cue... expectedCues) {\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(1);\n    assertThat(subtitle.getEventTime(0)).isEqualTo(0);\n    List<Cue> subtitleCues = subtitle.getCues(0);\n    assertThat(subtitleCues).hasSize(expectedCues.length);\n    for (int i = 0; i < subtitleCues.size(); i++) {\n      assertCueEquals(expectedCues[i], subtitleCues.get(i));\n    }\n  }\n\n  /**\n   * Asserts that two cues are equal.\n   */\n  private static void assertCueEquals(Cue expected, Cue actual) {\n    assertThat(actual.line).isEqualTo(expected.line);\n    assertThat(actual.lineAnchor).isEqualTo(expected.lineAnchor);\n    assertThat(actual.lineType).isEqualTo(expected.lineType);\n    assertThat(actual.position).isEqualTo(expected.position);\n    assertThat(actual.positionAnchor).isEqualTo(expected.positionAnchor);\n    assertThat(actual.size).isEqualTo(expected.size);\n    assertThat(actual.text.toString()).isEqualTo(expected.text.toString());\n    assertThat(actual.textAlignment).isEqualTo(expected.textAlignment);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 android.graphics.Typeface.BOLD;\nimport static android.graphics.Typeface.ITALIC;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.graphics.Typeface;\nimport android.text.Spanned;\nimport android.text.style.StyleSpan;\nimport android.text.style.UnderlineSpan;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport java.util.Collections;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link WebvttCueParser}. */\n@RunWith(AndroidJUnit4.class)\npublic final class WebvttCueParserTest {\n\n  @Test\n  public void testParseStrictValidClassesAndTrailingTokens() throws Exception {\n    Spanned text = parseCueText(\"<v.first.loud Esme>\"\n        + \"This <u.style1.style2 some stuff>is</u> text with <b.foo><i.bar>html</i></b> tags\");\n\n    assertThat(text.toString()).isEqualTo(\"This is text with html tags\");\n\n    UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class);\n    StyleSpan[] styleSpans = getSpans(text, StyleSpan.class);\n    assertThat(underlineSpans).hasLength(1);\n    assertThat(styleSpans).hasLength(2);\n    assertThat(styleSpans[0].getStyle()).isEqualTo(ITALIC);\n    assertThat(styleSpans[1].getStyle()).isEqualTo(BOLD);\n\n    assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(5);\n    assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(7);\n    assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(18);\n    assertThat(text.getSpanStart(styleSpans[1])).isEqualTo(18);\n    assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(22);\n    assertThat(text.getSpanEnd(styleSpans[1])).isEqualTo(22);\n  }\n\n  @Test\n  public void testParseStrictValidUnsupportedTagsStrippedOut() throws Exception {\n    Spanned text = parseCueText(\"<v.first.loud Esme>This <unsupported>is</unsupported> text with \"\n        + \"<notsupp><invalid>html</invalid></notsupp> tags\");\n    assertThat(text.toString()).isEqualTo(\"This is text with html tags\");\n    assertThat(getSpans(text, UnderlineSpan.class)).hasLength(0);\n    assertThat(getSpans(text, StyleSpan.class)).hasLength(0);\n  }\n\n  @Test\n  public void testParseWellFormedUnclosedEndAtCueEnd() throws Exception {\n    Spanned text = parseCueText(\"An <u some trailing stuff>unclosed u tag with \"\n        + \"<i>italic</i> inside\");\n\n    assertThat(text.toString()).isEqualTo(\"An unclosed u tag with italic inside\");\n\n    UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class);\n    StyleSpan[] styleSpans = getSpans(text, StyleSpan.class);\n    assertThat(underlineSpans).hasLength(1);\n    assertThat(styleSpans).hasLength(1);\n    assertThat(styleSpans[0].getStyle()).isEqualTo(ITALIC);\n\n    assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(3);\n    assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(23);\n    assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(29);\n    assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(36);\n  }\n\n  @Test\n  public void testParseWellFormedUnclosedEndAtParent() throws Exception {\n    Spanned text = parseCueText(\"An unclosed u tag with <i><u>underline and italic</i> inside\");\n\n    assertThat(text.toString()).isEqualTo(\"An unclosed u tag with underline and italic inside\");\n\n    UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class);\n    StyleSpan[] styleSpans = getSpans(text, StyleSpan.class);\n    assertThat(underlineSpans).hasLength(1);\n    assertThat(styleSpans).hasLength(1);\n\n    assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(23);\n    assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(23);\n    assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(43);\n    assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(43);\n\n    assertThat(styleSpans[0].getStyle()).isEqualTo(ITALIC);\n  }\n\n  @Test\n  public void testParseMalformedNestedElements() throws Exception {\n    Spanned text = parseCueText(\"<b><u>An unclosed u tag with <i>italic</u> inside</i></b>\");\n    assertThat(text.toString()).isEqualTo(\"An unclosed u tag with italic inside\");\n\n    UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class);\n    StyleSpan[] styleSpans = getSpans(text, StyleSpan.class);\n    assertThat(underlineSpans).hasLength(1);\n    assertThat(styleSpans).hasLength(2);\n\n    // all tags applied until matching start tag found\n    assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(0);\n    assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(29);\n    if (styleSpans[0].getStyle() == Typeface.BOLD) {\n      assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(0);\n      assertThat(text.getSpanStart(styleSpans[1])).isEqualTo(23);\n      assertThat(text.getSpanEnd(styleSpans[1])).isEqualTo(29);\n      assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(36);\n    } else {\n      assertThat(text.getSpanStart(styleSpans[1])).isEqualTo(0);\n      assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(23);\n      assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(29);\n      assertThat(text.getSpanEnd(styleSpans[1])).isEqualTo(36);\n    }\n  }\n\n  @Test\n  public void testParseCloseNonExistingTag() throws Exception {\n    Spanned text = parseCueText(\"blah<b>blah</i>blah</b>blah\");\n    assertThat(text.toString()).isEqualTo(\"blahblahblahblah\");\n\n    StyleSpan[] spans = getSpans(text, StyleSpan.class);\n    assertThat(spans).hasLength(1);\n    assertThat(spans[0].getStyle()).isEqualTo(BOLD);\n    assertThat(text.getSpanStart(spans[0])).isEqualTo(4);\n    assertThat(text.getSpanEnd(spans[0])).isEqualTo(8); // should be 12 when valid\n  }\n\n  @Test\n  public void testParseEmptyTagName() throws Exception {\n    Spanned text = parseCueText(\"An unclosed u tag with <>italic inside\");\n    assertThat(text.toString()).isEqualTo(\"An unclosed u tag with italic inside\");\n  }\n\n  @Test\n  public void testParseEntities() throws Exception {\n    Spanned text = parseCueText(\"&amp; &gt; &lt; &nbsp;\");\n    assertThat(text.toString()).isEqualTo(\"& > <  \");\n  }\n\n  @Test\n  public void testParseEntitiesUnsupported() throws Exception {\n    Spanned text = parseCueText(\"&noway; &sure;\");\n    assertThat(text.toString()).isEqualTo(\" \");\n  }\n\n  @Test\n  public void testParseEntitiesNotTerminated() throws Exception {\n    Spanned text = parseCueText(\"&amp here comes text\");\n    assertThat(text.toString()).isEqualTo(\"& here comes text\");\n  }\n\n  @Test\n  public void testParseEntitiesNotTerminatedUnsupported() throws Exception {\n    Spanned text = parseCueText(\"&surenot here comes text\");\n    assertThat(text.toString()).isEqualTo(\" here comes text\");\n  }\n\n  @Test\n  public void testParseEntitiesNotTerminatedNoSpace() throws Exception {\n    Spanned text = parseCueText(\"&surenot\");\n    assertThat(text.toString()).isEqualTo(\"&surenot\");\n  }\n\n  @Test\n  public void testParseVoidTag() throws Exception {\n    Spanned text = parseCueText(\"here comes<br/> text<br/>\");\n    assertThat(text.toString()).isEqualTo(\"here comes text\");\n  }\n\n  @Test\n  public void testParseMultipleTagsOfSameKind() {\n    Spanned text = parseCueText(\"blah <b>blah</b> blah <b>foo</b>\");\n\n    assertThat(text.toString()).isEqualTo(\"blah blah blah foo\");\n    StyleSpan[] spans = getSpans(text, StyleSpan.class);\n    assertThat(spans).hasLength(2);\n    assertThat(text.getSpanStart(spans[0])).isEqualTo(5);\n    assertThat(text.getSpanEnd(spans[0])).isEqualTo(9);\n    assertThat(text.getSpanStart(spans[1])).isEqualTo(15);\n    assertThat(text.getSpanEnd(spans[1])).isEqualTo(18);\n    assertThat(spans[0].getStyle()).isEqualTo(BOLD);\n    assertThat(spans[1].getStyle()).isEqualTo(BOLD);\n  }\n\n  @Test\n  public void testParseInvalidVoidSlash() {\n    Spanned text = parseCueText(\"blah <b/.st1.st2 trailing stuff> blah\");\n\n    assertThat(text.toString()).isEqualTo(\"blah  blah\");\n    StyleSpan[] spans = getSpans(text, StyleSpan.class);\n    assertThat(spans).hasLength(0);\n  }\n\n  @Test\n  public void testParseMonkey() throws Exception {\n    Spanned text = parseCueText(\"< u>An unclosed u tag with <<<<< i>italic</u></u></u></u    >\"\n        + \"</i><u><u> inside\");\n    assertThat(text.toString()).isEqualTo(\"An unclosed u tag with italic inside\");\n    text = parseCueText(\">>>>>>>>>An unclosed u tag with <<<<< italic</u></u></u>\"\n        + \"</u  ></i><u><u> inside\");\n    assertThat(text.toString()).isEqualTo(\">>>>>>>>>An unclosed u tag with  inside\");\n  }\n\n  @Test\n  public void testParseCornerCases() throws Exception {\n    Spanned text = parseCueText(\">\");\n    assertThat(text.toString()).isEqualTo(\">\");\n\n    text = parseCueText(\"<\");\n    assertThat(text.toString()).isEmpty();\n\n    text = parseCueText(\"<b.st1.st2 annotation\");\n    assertThat(text.toString()).isEmpty();\n\n    text = parseCueText(\"<<<<<<<<<<<<<<<<\");\n    assertThat(text.toString()).isEmpty();\n\n    text = parseCueText(\"<<<<<<>><<<<<<<<<<\");\n    assertThat(text.toString()).isEqualTo(\">\");\n\n    text = parseCueText(\"<>\");\n    assertThat(text.toString()).isEmpty();\n\n    text = parseCueText(\"&\");\n    assertThat(text.toString()).isEqualTo(\"&\");\n\n    text = parseCueText(\"&&&&&&&\");\n    assertThat(text.toString()).isEqualTo(\"&&&&&&&\");\n  }\n\n  private static Spanned parseCueText(String string) {\n    WebvttCue.Builder builder = new WebvttCue.Builder();\n    WebvttCueParser.parseCueText(null, string, builder, Collections.emptyList());\n    return (Spanned) builder.build().text;\n  }\n\n  private static <T> T[] getSpans(Spanned text, Class<T> spanType) {\n    return text.getSpans(0, text.length(), spanType);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.graphics.Typeface;\nimport android.text.Layout.Alignment;\nimport android.text.Spanned;\nimport android.text.style.BackgroundColorSpan;\nimport android.text.style.ForegroundColorSpan;\nimport android.text.style.StyleSpan;\nimport android.text.style.TypefaceSpan;\nimport android.text.style.UnderlineSpan;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.SubtitleDecoderException;\nimport java.io.IOException;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link WebvttDecoder}. */\n@RunWith(AndroidJUnit4.class)\npublic class WebvttDecoderTest {\n\n  private static final String TYPICAL_FILE = \"webvtt/typical\";\n  private static final String TYPICAL_WITH_BAD_TIMESTAMPS = \"webvtt/typical_with_bad_timestamps\";\n  private static final String TYPICAL_WITH_IDS_FILE = \"webvtt/typical_with_identifiers\";\n  private static final String TYPICAL_WITH_COMMENTS_FILE = \"webvtt/typical_with_comments\";\n  private static final String WITH_POSITIONING_FILE = \"webvtt/with_positioning\";\n  private static final String WITH_BAD_CUE_HEADER_FILE = \"webvtt/with_bad_cue_header\";\n  private static final String WITH_TAGS_FILE = \"webvtt/with_tags\";\n  private static final String WITH_CSS_STYLES = \"webvtt/with_css_styles\";\n  private static final String WITH_CSS_COMPLEX_SELECTORS = \"webvtt/with_css_complex_selectors\";\n  private static final String WITH_BOM = \"webvtt/with_bom\";\n  private static final String EMPTY_FILE = \"webvtt/empty\";\n\n  @Test\n  public void testDecodeEmpty() throws IOException {\n    WebvttDecoder decoder = new WebvttDecoder();\n    byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE);\n    try {\n      decoder.decode(bytes, bytes.length, /* reset= */ false);\n      fail();\n    } catch (SubtitleDecoderException expected) {\n      // Do nothing.\n    }\n  }\n\n  @Test\n  public void testDecodeTypical() throws IOException, SubtitleDecoderException {\n    WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE);\n\n    // Test event count.\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n\n    // Test cues.\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 0,\n        /* startTimeUs= */ 0,\n        /* endTimeUs= */ 1234000,\n        \"This is the first subtitle.\");\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 2,\n        /* startTimeUs= */ 2345000,\n        /* endTimeUs= */ 3456000,\n        \"This is the second subtitle.\");\n  }\n\n  @Test\n  public void testDecodeWithBom() throws IOException, SubtitleDecoderException {\n    WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BOM);\n\n    // Test event count.\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n\n    // Test cues.\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 0,\n        /* startTimeUs= */ 0,\n        /* endTimeUs= */ 1234000,\n        \"This is the first subtitle.\");\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 2,\n        /* startTimeUs= */ 2345000,\n        /* endTimeUs= */ 3456000,\n        \"This is the second subtitle.\");\n  }\n\n  @Test\n  public void testDecodeTypicalWithBadTimestamps() throws IOException, SubtitleDecoderException {\n    WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_BAD_TIMESTAMPS);\n\n    // Test event count.\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n\n    // Test cues.\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 0,\n        /* startTimeUs= */ 0,\n        /* endTimeUs= */ 1234000,\n        \"This is the first subtitle.\");\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 2,\n        /* startTimeUs= */ 2345000,\n        /* endTimeUs= */ 3456000,\n        \"This is the second subtitle.\");\n  }\n\n  @Test\n  public void testDecodeTypicalWithIds() throws IOException, SubtitleDecoderException {\n    WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_IDS_FILE);\n\n    // Test event count.\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n\n    // Test cues.\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 0,\n        /* startTimeUs= */ 0,\n        /* endTimeUs= */ 1234000,\n        \"This is the first subtitle.\");\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 2,\n        /* startTimeUs= */ 2345000,\n        /* endTimeUs= */ 3456000,\n        \"This is the second subtitle.\");\n  }\n\n  @Test\n  public void testDecodeTypicalWithComments() throws IOException, SubtitleDecoderException {\n    WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_COMMENTS_FILE);\n\n    // test event count\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n\n    // test cues\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 0,\n        /* startTimeUs= */ 0,\n        /* endTimeUs= */ 1234000,\n        \"This is the first subtitle.\");\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 2,\n        /* startTimeUs= */ 2345000,\n        /* endTimeUs= */ 3456000,\n        \"This is the second subtitle.\");\n  }\n\n  @Test\n  public void testDecodeWithTags() throws IOException, SubtitleDecoderException {\n    WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE);\n\n    // Test event count.\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(8);\n\n    // Test cues.\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 0,\n        /* startTimeUs= */ 0,\n        /* endTimeUs= */ 1234000,\n        \"This is the first subtitle.\");\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 2,\n        /* startTimeUs= */ 2345000,\n        /* endTimeUs= */ 3456000,\n        \"This is the second subtitle.\");\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 4,\n        /* startTimeUs= */ 4000000,\n        /* endTimeUs= */ 5000000,\n        \"This is the third subtitle.\");\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 6,\n        /* startTimeUs= */ 6000000,\n        /* endTimeUs= */ 7000000,\n        \"This is the <fourth> &subtitle.\");\n  }\n\n  @Test\n  public void testDecodeWithPositioning() throws IOException, SubtitleDecoderException {\n    WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE);\n    // Test event count.\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(12);\n    // Test cues.\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 0,\n        /* startTimeUs= */ 0,\n        /* endTimeUs= */ 1234000,\n        \"This is the first subtitle.\",\n        Alignment.ALIGN_NORMAL,\n        /* line= */ Cue.DIMEN_UNSET,\n        /* lineType= */ Cue.TYPE_UNSET,\n        /* lineAnchor= */ Cue.TYPE_UNSET,\n        /* position= */ 0.1f,\n        /* positionAnchor= */ Cue.ANCHOR_TYPE_START,\n        /* size= */ 0.35f);\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 2,\n        /* startTimeUs= */ 2345000,\n        /* endTimeUs= */ 3456000,\n        \"This is the second subtitle.\",\n        Alignment.ALIGN_OPPOSITE,\n        /* line= */ Cue.DIMEN_UNSET,\n        /* lineType= */ Cue.TYPE_UNSET,\n        /* lineAnchor= */ Cue.TYPE_UNSET,\n        /* position= */ Cue.DIMEN_UNSET,\n        /* positionAnchor= */ Cue.TYPE_UNSET,\n        /* size= */ 0.35f);\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 4,\n        /* startTimeUs= */ 4000000,\n        /* endTimeUs= */ 5000000,\n        \"This is the third subtitle.\",\n        Alignment.ALIGN_CENTER,\n        /* line= */ 0.45f,\n        /* lineType= */ Cue.LINE_TYPE_FRACTION,\n        /* lineAnchor= */ Cue.ANCHOR_TYPE_END,\n        /* position= */ Cue.DIMEN_UNSET,\n        /* positionAnchor= */ Cue.TYPE_UNSET,\n        /* size= */ 0.35f);\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 6,\n        /* startTimeUs= */ 6000000,\n        /* endTimeUs= */ 7000000,\n        \"This is the fourth subtitle.\",\n        Alignment.ALIGN_CENTER,\n        /* line= */ -11f,\n        /* lineType= */ Cue.LINE_TYPE_NUMBER,\n        /* lineAnchor= */ Cue.TYPE_UNSET,\n        /* position= */ Cue.DIMEN_UNSET,\n        /* positionAnchor= */ Cue.TYPE_UNSET,\n        /* size= */ Cue.DIMEN_UNSET);\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 8,\n        /* startTimeUs= */ 7000000,\n        /* endTimeUs= */ 8000000,\n        \"This is the fifth subtitle.\",\n        Alignment.ALIGN_OPPOSITE,\n        /* line= */ Cue.DIMEN_UNSET,\n        /* lineType= */ Cue.TYPE_UNSET,\n        /* lineAnchor= */ Cue.TYPE_UNSET,\n        /* position= */ 0.1f,\n        /* positionAnchor= */ Cue.ANCHOR_TYPE_END,\n        /* size= */ 0.1f);\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 10,\n        /* startTimeUs= */ 10000000,\n        /* endTimeUs= */ 11000000,\n        \"This is the sixth subtitle.\",\n        Alignment.ALIGN_CENTER,\n        /* line= */ 0.45f,\n        /* lineType= */ Cue.LINE_TYPE_FRACTION,\n        /* lineAnchor= */ Cue.ANCHOR_TYPE_END,\n        /* position= */ Cue.DIMEN_UNSET,\n        /* positionAnchor= */ Cue.TYPE_UNSET,\n        /* size= */ 0.35f);\n  }\n\n  @Test\n  public void testDecodeWithBadCueHeader() throws IOException, SubtitleDecoderException {\n    WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BAD_CUE_HEADER_FILE);\n\n    // Test event count.\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(4);\n\n    // Test cues.\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 0,\n        /* startTimeUs= */ 0,\n        /* endTimeUs= */ 1234000,\n        \"This is the first subtitle.\");\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 2,\n        /* startTimeUs= */ 4000000,\n        /* endTimeUs= */ 5000000,\n        \"This is the third subtitle.\");\n  }\n\n  @Test\n  public void testWebvttWithCssStyle() throws IOException, SubtitleDecoderException {\n    WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES);\n\n    // Test event count.\n    assertThat(subtitle.getEventTimeCount()).isEqualTo(8);\n\n    // Test cues.\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 0,\n        /* startTimeUs= */ 0,\n        /* endTimeUs= */ 1234000,\n        \"This is the first subtitle.\");\n    assertCue(\n        subtitle,\n        /* eventTimeIndex= */ 2,\n        /* startTimeUs= */ 2345000,\n        /* endTimeUs= */ 3456000,\n        \"This is the second subtitle.\");\n\n    Spanned s1 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0);\n    Spanned s2 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2345000);\n    Spanned s3 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 20000000);\n    Spanned s4 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 25000000);\n    assertThat(s1.getSpans(/* start= */ 0, s1.length(), ForegroundColorSpan.class)).hasLength(1);\n    assertThat(s1.getSpans(/* start= */ 0, s1.length(), BackgroundColorSpan.class)).hasLength(1);\n    assertThat(s2.getSpans(/* start= */ 0, s2.length(), ForegroundColorSpan.class)).hasLength(2);\n    assertThat(s3.getSpans(/* start= */ 10, s3.length(), UnderlineSpan.class)).hasLength(1);\n    assertThat(s4.getSpans(/* start= */ 0, /* end= */ 16, BackgroundColorSpan.class)).hasLength(2);\n    assertThat(s4.getSpans(/* start= */ 17, s4.length(), StyleSpan.class)).hasLength(1);\n    assertThat(s4.getSpans(/* start= */ 17, s4.length(), StyleSpan.class)[0].getStyle())\n        .isEqualTo(Typeface.BOLD);\n  }\n\n  @Test\n  public void testWithComplexCssSelectors() throws IOException, SubtitleDecoderException {\n    WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS);\n    Spanned text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0);\n    assertThat(text.getSpans(/* start= */ 30, text.length(), ForegroundColorSpan.class))\n        .hasLength(1);\n    assertThat(\n            text.getSpans(/* start= */ 30, text.length(), ForegroundColorSpan.class)[0]\n                .getForegroundColor())\n        .isEqualTo(0xFFEE82EE);\n    assertThat(text.getSpans(/* start= */ 30, text.length(), TypefaceSpan.class)).hasLength(1);\n    assertThat(text.getSpans(/* start= */ 30, text.length(), TypefaceSpan.class)[0].getFamily())\n        .isEqualTo(\"courier\");\n\n    text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2000000);\n    assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)).hasLength(1);\n    assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)[0].getFamily())\n        .isEqualTo(\"courier\");\n\n    text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2500000);\n    assertThat(text.getSpans(/* start= */ 5, text.length(), StyleSpan.class)).hasLength(1);\n    assertThat(text.getSpans(/* start= */ 5, text.length(), StyleSpan.class)[0].getStyle())\n        .isEqualTo(Typeface.BOLD);\n    assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)).hasLength(1);\n    assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)[0].getFamily())\n        .isEqualTo(\"courier\");\n\n    text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4000000);\n    assertThat(text.getSpans(/* start= */ 6, /* end= */ 22, StyleSpan.class)).hasLength(0);\n    assertThat(text.getSpans(/* start= */ 30, text.length(), StyleSpan.class)).hasLength(1);\n    assertThat(text.getSpans(/* start= */ 30, text.length(), StyleSpan.class)[0].getStyle())\n        .isEqualTo(Typeface.BOLD);\n\n    text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5000000);\n    assertThat(text.getSpans(/* start= */ 9, /* end= */ 17, StyleSpan.class)).hasLength(0);\n    assertThat(text.getSpans(/* start= */ 19, text.length(), StyleSpan.class)).hasLength(1);\n    assertThat(text.getSpans(/* start= */ 19, text.length(), StyleSpan.class)[0].getStyle())\n        .isEqualTo(Typeface.ITALIC);\n  }\n\n  private WebvttSubtitle getSubtitleForTestAsset(String asset)\n      throws IOException, SubtitleDecoderException {\n    WebvttDecoder decoder = new WebvttDecoder();\n    byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), asset);\n    return decoder.decode(bytes, bytes.length, /* reset= */ false);\n  }\n\n  private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) {\n    return (Spanned) sub.getCues(timeUs).get(0).text;\n  }\n\n  private static void assertCue(\n      WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs, int endTimeUs, String text) {\n    assertCue(\n        subtitle,\n        eventTimeIndex,\n        startTimeUs,\n        endTimeUs,\n        text,\n        /* textAlignment= */ null,\n        /* line= */ Cue.DIMEN_UNSET,\n        /* lineType= */ Cue.TYPE_UNSET,\n        /* lineAnchor= */ Cue.TYPE_UNSET,\n        /* position= */ Cue.DIMEN_UNSET,\n        /* positionAnchor= */ Cue.TYPE_UNSET,\n        /* size= */ Cue.DIMEN_UNSET);\n  }\n\n  private static void assertCue(\n      WebvttSubtitle subtitle,\n      int eventTimeIndex,\n      long startTimeUs,\n      int endTimeUs,\n      String text,\n      Alignment textAlignment,\n      float line,\n      int lineType,\n      int lineAnchor,\n      float position,\n      int positionAnchor,\n      float size) {\n    assertThat(subtitle.getEventTime(eventTimeIndex)).isEqualTo(startTimeUs);\n    assertThat(subtitle.getEventTime(eventTimeIndex + 1)).isEqualTo(endTimeUs);\n    List<Cue> cues = subtitle.getCues(subtitle.getEventTime(eventTimeIndex));\n    assertThat(cues).hasSize(1);\n    // Assert cue properties.\n    Cue cue = cues.get(0);\n    assertThat(cue.text.toString()).isEqualTo(text);\n    assertThat(cue.textAlignment).isEqualTo(textAlignment);\n    assertThat(cue.line).isEqualTo(line);\n    assertThat(cue.lineType).isEqualTo(lineType);\n    assertThat(cue.lineAnchor).isEqualTo(lineAnchor);\n    assertThat(cue.position).isEqualTo(position);\n    assertThat(cue.positionAnchor).isEqualTo(positionAnchor);\n    assertThat(cue.size).isEqualTo(size);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 com.google.android.exoplayer2.C.INDEX_UNSET;\nimport static com.google.common.truth.Truth.assertThat;\nimport static java.lang.Long.MAX_VALUE;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.text.Cue;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link WebvttSubtitle}. */\n@RunWith(AndroidJUnit4.class)\npublic class WebvttSubtitleTest {\n\n  private static final String FIRST_SUBTITLE_STRING = \"This is the first subtitle.\";\n  private static final String SECOND_SUBTITLE_STRING = \"This is the second subtitle.\";\n  private static final String FIRST_AND_SECOND_SUBTITLE_STRING =\n      FIRST_SUBTITLE_STRING + \"\\n\" + SECOND_SUBTITLE_STRING;\n\n  private static final WebvttSubtitle emptySubtitle = new WebvttSubtitle(Collections.emptyList());\n\n  private static final WebvttSubtitle simpleSubtitle;\n  static {\n    ArrayList<WebvttCue> simpleSubtitleCues = new ArrayList<>();\n    WebvttCue firstCue = new WebvttCue(1000000, 2000000, FIRST_SUBTITLE_STRING);\n    simpleSubtitleCues.add(firstCue);\n    WebvttCue secondCue = new WebvttCue(3000000, 4000000, SECOND_SUBTITLE_STRING);\n    simpleSubtitleCues.add(secondCue);\n    simpleSubtitle = new WebvttSubtitle(simpleSubtitleCues);\n  }\n\n  private static final WebvttSubtitle overlappingSubtitle;\n  static {\n    ArrayList<WebvttCue> overlappingSubtitleCues = new ArrayList<>();\n    WebvttCue firstCue = new WebvttCue(1000000, 3000000, FIRST_SUBTITLE_STRING);\n    overlappingSubtitleCues.add(firstCue);\n    WebvttCue secondCue = new WebvttCue(2000000, 4000000, SECOND_SUBTITLE_STRING);\n    overlappingSubtitleCues.add(secondCue);\n    overlappingSubtitle = new WebvttSubtitle(overlappingSubtitleCues);\n  }\n\n  private static final WebvttSubtitle nestedSubtitle;\n  static {\n    ArrayList<WebvttCue> nestedSubtitleCues = new ArrayList<>();\n    WebvttCue firstCue = new WebvttCue(1000000, 4000000, FIRST_SUBTITLE_STRING);\n    nestedSubtitleCues.add(firstCue);\n    WebvttCue secondCue = new WebvttCue(2000000, 3000000, SECOND_SUBTITLE_STRING);\n    nestedSubtitleCues.add(secondCue);\n    nestedSubtitle = new WebvttSubtitle(nestedSubtitleCues);\n  }\n\n  @Test\n  public void testEventCount() {\n    assertThat(emptySubtitle.getEventTimeCount()).isEqualTo(0);\n    assertThat(simpleSubtitle.getEventTimeCount()).isEqualTo(4);\n    assertThat(overlappingSubtitle.getEventTimeCount()).isEqualTo(4);\n    assertThat(nestedSubtitle.getEventTimeCount()).isEqualTo(4);\n  }\n\n  @Test\n  public void testSimpleSubtitleEventTimes() {\n    testSubtitleEventTimesHelper(simpleSubtitle);\n  }\n\n  @Test\n  public void testSimpleSubtitleEventIndices() {\n    testSubtitleEventIndicesHelper(simpleSubtitle);\n  }\n\n  @Test\n  public void testSimpleSubtitleText() {\n    // Test before first subtitle\n    assertSingleCueEmpty(simpleSubtitle.getCues(0));\n    assertSingleCueEmpty(simpleSubtitle.getCues(500000));\n    assertSingleCueEmpty(simpleSubtitle.getCues(999999));\n\n    // Test first subtitle\n    assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1000000));\n    assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1500000));\n    assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1999999));\n\n    // Test after first subtitle, before second subtitle\n    assertSingleCueEmpty(simpleSubtitle.getCues(2000000));\n    assertSingleCueEmpty(simpleSubtitle.getCues(2500000));\n    assertSingleCueEmpty(simpleSubtitle.getCues(2999999));\n\n    // Test second subtitle\n    assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3000000));\n    assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3500000));\n    assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3999999));\n\n    // Test after second subtitle\n    assertSingleCueEmpty(simpleSubtitle.getCues(4000000));\n    assertSingleCueEmpty(simpleSubtitle.getCues(4500000));\n    assertSingleCueEmpty(simpleSubtitle.getCues(Long.MAX_VALUE));\n  }\n\n  @Test\n  public void testOverlappingSubtitleEventTimes() {\n    testSubtitleEventTimesHelper(overlappingSubtitle);\n  }\n\n  @Test\n  public void testOverlappingSubtitleEventIndices() {\n    testSubtitleEventIndicesHelper(overlappingSubtitle);\n  }\n\n  @Test\n  public void testOverlappingSubtitleText() {\n    // Test before first subtitle\n    assertSingleCueEmpty(overlappingSubtitle.getCues(0));\n    assertSingleCueEmpty(overlappingSubtitle.getCues(500000));\n    assertSingleCueEmpty(overlappingSubtitle.getCues(999999));\n\n    // Test first subtitle\n    assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1000000));\n    assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1500000));\n    assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1999999));\n\n    // Test after first and second subtitle\n    assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING,\n        overlappingSubtitle.getCues(2000000));\n    assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING,\n        overlappingSubtitle.getCues(2500000));\n    assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING,\n        overlappingSubtitle.getCues(2999999));\n\n    // Test second subtitle\n    assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3000000));\n    assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3500000));\n    assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3999999));\n\n    // Test after second subtitle\n    assertSingleCueEmpty(overlappingSubtitle.getCues(4000000));\n    assertSingleCueEmpty(overlappingSubtitle.getCues(4500000));\n    assertSingleCueEmpty(overlappingSubtitle.getCues(Long.MAX_VALUE));\n  }\n\n  @Test\n  public void testNestedSubtitleEventTimes() {\n    testSubtitleEventTimesHelper(nestedSubtitle);\n  }\n\n  @Test\n  public void testNestedSubtitleEventIndices() {\n    testSubtitleEventIndicesHelper(nestedSubtitle);\n  }\n\n  @Test\n  public void testNestedSubtitleText() {\n    // Test before first subtitle\n    assertSingleCueEmpty(nestedSubtitle.getCues(0));\n    assertSingleCueEmpty(nestedSubtitle.getCues(500000));\n    assertSingleCueEmpty(nestedSubtitle.getCues(999999));\n\n    // Test first subtitle\n    assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1000000));\n    assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1500000));\n    assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1999999));\n\n    // Test after first and second subtitle\n    assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2000000));\n    assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2500000));\n    assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2999999));\n\n    // Test first subtitle\n    assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3000000));\n    assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3500000));\n    assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3999999));\n\n    // Test after second subtitle\n    assertSingleCueEmpty(nestedSubtitle.getCues(4000000));\n    assertSingleCueEmpty(nestedSubtitle.getCues(4500000));\n    assertSingleCueEmpty(nestedSubtitle.getCues(Long.MAX_VALUE));\n  }\n\n  private void testSubtitleEventTimesHelper(WebvttSubtitle subtitle) {\n    assertThat(subtitle.getEventTime(0)).isEqualTo(1000000);\n    assertThat(subtitle.getEventTime(1)).isEqualTo(2000000);\n    assertThat(subtitle.getEventTime(2)).isEqualTo(3000000);\n    assertThat(subtitle.getEventTime(3)).isEqualTo(4000000);\n  }\n\n  private void testSubtitleEventIndicesHelper(WebvttSubtitle subtitle) {\n    // Test first event\n    assertThat(subtitle.getNextEventTimeIndex(0)).isEqualTo(0);\n    assertThat(subtitle.getNextEventTimeIndex(500000)).isEqualTo(0);\n    assertThat(subtitle.getNextEventTimeIndex(999999)).isEqualTo(0);\n\n    // Test second event\n    assertThat(subtitle.getNextEventTimeIndex(1000000)).isEqualTo(1);\n    assertThat(subtitle.getNextEventTimeIndex(1500000)).isEqualTo(1);\n    assertThat(subtitle.getNextEventTimeIndex(1999999)).isEqualTo(1);\n\n    // Test third event\n    assertThat(subtitle.getNextEventTimeIndex(2000000)).isEqualTo(2);\n    assertThat(subtitle.getNextEventTimeIndex(2500000)).isEqualTo(2);\n    assertThat(subtitle.getNextEventTimeIndex(2999999)).isEqualTo(2);\n\n    // Test fourth event\n    assertThat(subtitle.getNextEventTimeIndex(3000000)).isEqualTo(3);\n    assertThat(subtitle.getNextEventTimeIndex(3500000)).isEqualTo(3);\n    assertThat(subtitle.getNextEventTimeIndex(3999999)).isEqualTo(3);\n\n    // Test null event (i.e. look for events after the last event)\n    assertThat(subtitle.getNextEventTimeIndex(4000000)).isEqualTo(INDEX_UNSET);\n    assertThat(subtitle.getNextEventTimeIndex(4500000)).isEqualTo(INDEX_UNSET);\n    assertThat(subtitle.getNextEventTimeIndex(MAX_VALUE)).isEqualTo(INDEX_UNSET);\n  }\n\n  private void assertSingleCueEmpty(List<Cue> cues) {\n    assertThat(cues).isEmpty();\n  }\n\n  private void assertSingleCueTextEquals(String expected, List<Cue> cues) {\n    assertThat(cues).hasSize(1);\n    assertThat(cues.get(0).text.toString()).isEqualTo(expected);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.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 static com.google.common.truth.Truth.assertThat;\nimport static org.mockito.ArgumentMatchers.argThat;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Matchers.eq;\nimport static org.mockito.Mockito.atLeastOnce;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.verifyZeroInteractions;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\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.MediaChunkIterator;\nimport com.google.android.exoplayer2.testutil.FakeClock;\nimport com.google.android.exoplayer2.testutil.FakeMediaChunk;\nimport com.google.android.exoplayer2.trackselection.TrackSelection.Definition;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.ArgumentMatcher;\nimport org.mockito.Mock;\n\n/** Unit test for {@link AdaptiveTrackSelection}. */\n@RunWith(AndroidJUnit4.class)\npublic final class AdaptiveTrackSelectionTest {\n\n  private static final MediaChunkIterator[] THREE_EMPTY_MEDIA_CHUNK_ITERATORS =\n      new MediaChunkIterator[] {\n        MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY\n      };\n\n  @Mock private BandwidthMeter mockBandwidthMeter;\n  private FakeClock fakeClock;\n\n  private AdaptiveTrackSelection adaptiveTrackSelection;\n\n  @Before\n  public void setUp() {\n    initMocks(this);\n    fakeClock = new FakeClock(0);\n  }\n\n  @Test\n  @SuppressWarnings(\"deprecation\")\n  public void testFactoryUsesInitiallyProvidedBandwidthMeter() {\n    BandwidthMeter initialBandwidthMeter = mock(BandwidthMeter.class);\n    BandwidthMeter injectedBandwidthMeter = mock(BandwidthMeter.class);\n    Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);\n    Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);\n    TrackSelection[] trackSelections =\n        new AdaptiveTrackSelection.Factory(initialBandwidthMeter)\n            .createTrackSelections(\n                new Definition[] {\n                  new Definition(new TrackGroup(format1, format2), /* tracks= */ 0, 1)\n                },\n                injectedBandwidthMeter);\n    trackSelections[0].updateSelectedTrack(\n        /* playbackPositionUs= */ 0,\n        /* bufferedDurationUs= */ 0,\n        /* availableDurationUs= */ C.TIME_UNSET,\n        /* queue= */ Collections.emptyList(),\n        /* mediaChunkIterators= */ new MediaChunkIterator[] {MediaChunkIterator.EMPTY});\n\n    verify(initialBandwidthMeter, atLeastOnce()).getBitrateEstimate();\n    verifyZeroInteractions(injectedBandwidthMeter);\n  }\n\n  @Test\n  public void testSelectInitialIndexUseMaxInitialBitrateIfNoBandwidthEstimate() {\n    Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);\n    Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);\n    Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);\n    TrackGroup trackGroup = new TrackGroup(format1, format2, format3);\n\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L);\n    adaptiveTrackSelection = adaptiveTrackSelection(trackGroup);\n\n    assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2);\n    assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL);\n  }\n\n  @Test\n  public void testSelectInitialIndexUseBandwidthEstimateIfAvailable() {\n    Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);\n    Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);\n    Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);\n    TrackGroup trackGroup = new TrackGroup(format1, format2, format3);\n\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L);\n    adaptiveTrackSelection = adaptiveTrackSelection(trackGroup);\n\n    assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format1);\n    assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL);\n  }\n\n  @Test\n  public void testUpdateSelectedTrackDoNotSwitchUpIfNotBufferedEnough() {\n    Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);\n    Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);\n    Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);\n    TrackGroup trackGroup = new TrackGroup(format1, format2, format3);\n\n    // The second measurement onward returns 2000L, which prompts the track selection to switch up\n    // if possible.\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L, 2000L);\n    adaptiveTrackSelection =\n        adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs(\n            trackGroup, /* minDurationForQualityIncreaseMs= */ 10_000);\n\n    adaptiveTrackSelection.updateSelectedTrack(\n        /* playbackPositionUs= */ 0,\n        /* bufferedDurationUs= */ 9_999_000,\n        /* availableDurationUs= */ C.TIME_UNSET,\n        /* queue= */ Collections.emptyList(),\n        /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS);\n\n    // When bandwidth estimation is updated to 2000L, we can switch up to use a higher bitrate\n    // format. However, since we only buffered 9_999_000 us, which is smaller than\n    // minDurationForQualityIncreaseMs, we should defer switch up.\n    assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2);\n    assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL);\n  }\n\n  @Test\n  public void testUpdateSelectedTrackSwitchUpIfBufferedEnough() {\n    Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);\n    Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);\n    Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);\n    TrackGroup trackGroup = new TrackGroup(format1, format2, format3);\n\n    // The second measurement onward returns 2000L, which prompts the track selection to switch up\n    // if possible.\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L, 2000L);\n    adaptiveTrackSelection =\n        adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs(\n            trackGroup, /* minDurationForQualityIncreaseMs= */ 10_000);\n\n    adaptiveTrackSelection.updateSelectedTrack(\n        /* playbackPositionUs= */ 0,\n        /* bufferedDurationUs= */ 10_000_000,\n        /* availableDurationUs= */ C.TIME_UNSET,\n        /* queue= */ Collections.emptyList(),\n        /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS);\n\n    // When bandwidth estimation is updated to 2000L, we can switch up to use a higher bitrate\n    // format. When we have buffered enough (10_000_000 us, which is equal to\n    // minDurationForQualityIncreaseMs), we should switch up now.\n    assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format3);\n    assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE);\n  }\n\n  @Test\n  public void testUpdateSelectedTrackDoNotSwitchDownIfBufferedEnough() {\n    Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);\n    Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);\n    Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);\n    TrackGroup trackGroup = new TrackGroup(format1, format2, format3);\n\n    // The second measurement onward returns 500L, which prompts the track selection to switch down\n    // if necessary.\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L, 500L);\n    adaptiveTrackSelection =\n        adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs(\n            trackGroup, /* maxDurationForQualityDecreaseMs= */ 25_000);\n\n    adaptiveTrackSelection.updateSelectedTrack(\n        /* playbackPositionUs= */ 0,\n        /* bufferedDurationUs= */ 25_000_000,\n        /* availableDurationUs= */ C.TIME_UNSET,\n        /* queue= */ Collections.emptyList(),\n        /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS);\n\n    // When bandwidth estimation is updated to 500L, we should switch down to use a lower bitrate\n    // format. However, since we have enough buffer at higher quality (25_000_000 us, which is equal\n    // to maxDurationForQualityDecreaseMs), we should defer switch down.\n    assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2);\n    assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL);\n  }\n\n  @Test\n  public void testUpdateSelectedTrackSwitchDownIfNotBufferedEnough() {\n    Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);\n    Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);\n    Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);\n    TrackGroup trackGroup = new TrackGroup(format1, format2, format3);\n\n    // The second measurement onward returns 500L, which prompts the track selection to switch down\n    // if necessary.\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L, 500L);\n    adaptiveTrackSelection =\n        adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs(\n            trackGroup, /* maxDurationForQualityDecreaseMs= */ 25_000);\n\n    adaptiveTrackSelection.updateSelectedTrack(\n        /* playbackPositionUs= */ 0,\n        /* bufferedDurationUs= */ 24_999_000,\n        /* availableDurationUs= */ C.TIME_UNSET,\n        /* queue= */ Collections.emptyList(),\n        /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS);\n\n    // When bandwidth estimation is updated to 500L, we should switch down to use a lower bitrate\n    // format. When we don't have enough buffer at higher quality (24_999_000 us is smaller than\n    // maxDurationForQualityDecreaseMs), we should switch down now.\n    assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format1);\n    assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE);\n  }\n\n  @Test\n  public void testUpdateSelectedTrackSwitchUpIfTrackBitrateEstimateIsLow() {\n    Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);\n    Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);\n    Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);\n    TrackGroup trackGroup = new TrackGroup(format1, format2, format3);\n\n    // The second measurement onward returns 1500L, which isn't enough to switch up to format3 as\n    // the format bitrate is 2000.\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L, 1500L);\n\n    // But TrackBitrateEstimator returns 1500 for 3rd track so it should switch up.\n    TrackBitrateEstimator estimator = mock(TrackBitrateEstimator.class);\n    when(estimator.getBitrates(any(), any(), any(), any()))\n        .then(\n            (invocation) -> {\n              int[] returnValue = new int[] {500, 1000, 1500};\n              int[] inputArray = (int[]) invocation.getArguments()[3];\n              System.arraycopy(returnValue, 0, inputArray, 0, returnValue.length);\n              return returnValue;\n            });\n\n    adaptiveTrackSelection = adaptiveTrackSelection(trackGroup);\n    adaptiveTrackSelection.experimental_setTrackBitrateEstimator(estimator);\n\n    adaptiveTrackSelection.updateSelectedTrack(\n        /* playbackPositionUs= */ 0,\n        /* bufferedDurationUs= */ AdaptiveTrackSelection\n                .DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS\n            * 1000,\n        /* availableDurationUs= */ C.TIME_UNSET,\n        /* queue= */ Collections.emptyList(),\n        /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS);\n\n    ArgumentMatcher<Format[]> matcher =\n        new ArgumentMatcher<Format[]>() {\n          @Override\n          public boolean matches(Format[] argument) {\n            Format[] formats = (Format[]) argument;\n            return formats.length == 3\n                && Arrays.asList(formats).containsAll(Arrays.asList(format1, format2, format3));\n          }\n        };\n    verify(estimator)\n        .getBitrates(\n            argThat(matcher),\n            eq(Collections.emptyList()),\n            eq(THREE_EMPTY_MEDIA_CHUNK_ITERATORS),\n            any());\n    assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format3);\n    assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE);\n  }\n\n  @Test\n  public void testEvaluateQueueSizeReturnQueueSizeIfBandwidthIsNotImproved() {\n    Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);\n    Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);\n    Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);\n    TrackGroup trackGroup = new TrackGroup(format1, format2, format3);\n\n    FakeMediaChunk chunk1 =\n        new FakeMediaChunk(format1, /* startTimeUs= */ 0, /* endTimeUs= */ 10_000_000);\n    FakeMediaChunk chunk2 =\n        new FakeMediaChunk(format1, /* startTimeUs= */ 10_000_000, /* endTimeUs= */ 20_000_000);\n    FakeMediaChunk chunk3 =\n        new FakeMediaChunk(format1, /* startTimeUs= */ 20_000_000, /* endTimeUs= */ 30_000_000);\n    List<FakeMediaChunk> queue = new ArrayList<>();\n    queue.add(chunk1);\n    queue.add(chunk2);\n    queue.add(chunk3);\n\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L);\n    adaptiveTrackSelection = adaptiveTrackSelection(trackGroup);\n\n    int size = adaptiveTrackSelection.evaluateQueueSize(0, queue);\n    assertThat(size).isEqualTo(3);\n  }\n\n  @Test\n  public void testEvaluateQueueSizeDoNotReevaluateUntilAfterMinTimeBetweenBufferReevaluation() {\n    Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);\n    Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);\n    Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);\n    TrackGroup trackGroup = new TrackGroup(format1, format2, format3);\n\n    FakeMediaChunk chunk1 =\n        new FakeMediaChunk(format1, /* startTimeUs= */ 0, /* endTimeUs= */ 10_000_000);\n    FakeMediaChunk chunk2 =\n        new FakeMediaChunk(format1, /* startTimeUs= */ 10_000_000, /* endTimeUs= */ 20_000_000);\n    FakeMediaChunk chunk3 =\n        new FakeMediaChunk(format1, /* startTimeUs= */ 20_000_000, /* endTimeUs= */ 30_000_000);\n    List<FakeMediaChunk> queue = new ArrayList<>();\n    queue.add(chunk1);\n    queue.add(chunk2);\n    queue.add(chunk3);\n\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L);\n    adaptiveTrackSelection =\n        adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs(\n            trackGroup,\n            /* durationToRetainAfterDiscardMs= */ 15_000,\n            /* minTimeBetweenBufferReevaluationMs= */ 2000);\n\n    int initialQueueSize = adaptiveTrackSelection.evaluateQueueSize(0, queue);\n\n    fakeClock.advanceTime(1999);\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L);\n\n    // When bandwidth estimation is updated, we can discard chunks at the end of the queue now.\n    // However, since min duration between buffer reevaluation = 2000, we will not reevaluate\n    // queue size if time now is only 1999 ms after last buffer reevaluation.\n    int newSize = adaptiveTrackSelection.evaluateQueueSize(0, queue);\n    assertThat(newSize).isEqualTo(initialQueueSize);\n  }\n\n  @Test\n  public void testEvaluateQueueSizeRetainMoreThanMinimumDurationAfterDiscard() {\n    Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);\n    Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);\n    Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);\n    TrackGroup trackGroup = new TrackGroup(format1, format2, format3);\n\n    FakeMediaChunk chunk1 =\n        new FakeMediaChunk(format1, /* startTimeUs= */ 0, /* endTimeUs= */ 10_000_000);\n    FakeMediaChunk chunk2 =\n        new FakeMediaChunk(format1, /* startTimeUs= */ 10_000_000, /* endTimeUs= */ 20_000_000);\n    FakeMediaChunk chunk3 =\n        new FakeMediaChunk(format1, /* startTimeUs= */ 20_000_000, /* endTimeUs= */ 30_000_000);\n    List<FakeMediaChunk> queue = new ArrayList<>();\n    queue.add(chunk1);\n    queue.add(chunk2);\n    queue.add(chunk3);\n\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L);\n    adaptiveTrackSelection =\n        adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs(\n            trackGroup,\n            /* durationToRetainAfterDiscardMs= */ 15_000,\n            /* minTimeBetweenBufferReevaluationMs= */ 2000);\n\n    int initialQueueSize = adaptiveTrackSelection.evaluateQueueSize(0, queue);\n    assertThat(initialQueueSize).isEqualTo(3);\n\n    fakeClock.advanceTime(2000);\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L);\n\n    // When bandwidth estimation is updated and time has advanced enough, we can discard chunks at\n    // the end of the queue now.\n    // However, since duration to retain after discard = 15 000 ms, we need to retain at least the\n    // first 2 chunks\n    int newSize = adaptiveTrackSelection.evaluateQueueSize(0, queue);\n    assertThat(newSize).isEqualTo(2);\n  }\n\n  private AdaptiveTrackSelection adaptiveTrackSelection(TrackGroup trackGroup) {\n    return adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs(\n        trackGroup, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS);\n  }\n\n  private AdaptiveTrackSelection adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs(\n      TrackGroup trackGroup, long minDurationForQualityIncreaseMs) {\n    return prepareTrackSelection(\n        new AdaptiveTrackSelection(\n            trackGroup,\n            selectedAllTracksInGroup(trackGroup),\n            mockBandwidthMeter,\n            minDurationForQualityIncreaseMs,\n            AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,\n            AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,\n            /* bandwidthFraction= */ 1.0f,\n            AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,\n            AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,\n            fakeClock));\n  }\n\n  private AdaptiveTrackSelection adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs(\n      TrackGroup trackGroup, long maxDurationForQualityDecreaseMs) {\n    return prepareTrackSelection(\n        new AdaptiveTrackSelection(\n            trackGroup,\n            selectedAllTracksInGroup(trackGroup),\n            mockBandwidthMeter,\n            AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,\n            maxDurationForQualityDecreaseMs,\n            AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,\n            /* bandwidthFraction= */ 1.0f,\n            AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,\n            AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,\n            fakeClock));\n  }\n\n  private AdaptiveTrackSelection adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs(\n      TrackGroup trackGroup,\n      long durationToRetainAfterDiscardMs,\n      long minTimeBetweenBufferReevaluationMs) {\n    return prepareTrackSelection(\n        new AdaptiveTrackSelection(\n            trackGroup,\n            selectedAllTracksInGroup(trackGroup),\n            mockBandwidthMeter,\n            AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,\n            AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,\n            durationToRetainAfterDiscardMs,\n            /* bandwidthFraction= */ 1.0f,\n            AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,\n            minTimeBetweenBufferReevaluationMs,\n            fakeClock));\n  }\n\n  private AdaptiveTrackSelection prepareTrackSelection(\n      AdaptiveTrackSelection adaptiveTrackSelection) {\n    adaptiveTrackSelection.enable();\n    adaptiveTrackSelection.updateSelectedTrack(\n        /* playbackPositionUs= */ 0,\n        /* bufferedDurationUs= */ 0,\n        /* availableDurationUs= */ C.TIME_UNSET,\n        /* queue= */ Collections.emptyList(),\n        /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS);\n    return adaptiveTrackSelection;\n  }\n\n  private int[] selectedAllTracksInGroup(TrackGroup trackGroup) {\n    int[] listIndices = new int[trackGroup.length];\n    for (int i = 0; i < trackGroup.length; i++) {\n      listIndices[i] = i;\n    }\n    return listIndices;\n  }\n\n  private static Format videoFormat(int bitrate, int width, int height) {\n    return Format.createVideoSampleFormat(\n        /* id= */ null,\n        /* sampleMimeType= */ MimeTypes.VIDEO_H264,\n        /* codecs= */ null,\n        /* bitrate= */ bitrate,\n        /* maxInputSize= */ Format.NO_VALUE,\n        /* width= */ width,\n        /* height= */ height,\n        /* frameRate= */ Format.NO_VALUE,\n        /* initializationData= */ null,\n        /* drmInitData= */ null);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.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 static com.google.common.truth.Truth.assertThat;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport android.util.Pair;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\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.MediaChunkIterator;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.Collections;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\n\n/** Unit test for the track selection created by {@link BufferSizeAdaptationBuilder}. */\n@RunWith(AndroidJUnit4.class)\npublic final class BufferSizeAdaptiveTrackSelectionTest {\n\n  private static final int MIN_BUFFER_MS = 15_000;\n  private static final int MAX_BUFFER_MS = 50_000;\n  private static final int HYSTERESIS_BUFFER_MS = 10_000;\n  private static final float BANDWIDTH_FRACTION = 0.5f;\n  private static final int MIN_BUFFER_FOR_QUALITY_INCREASE_MS = 10_000;\n\n  /**\n   * Factor between bitrates is always the same (=2.2). That means buffer levels should be linearly\n   * distributed between MIN_BUFFER=15s and MAX_BUFFER-HYSTERESIS=50s-10s=40s.\n   */\n  private static final Format format1 =\n      createVideoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);\n\n  private static final Format format2 =\n      createVideoFormat(/* bitrate= */ 1100, /* width= */ 640, /* height= */ 480);\n  private static final Format format3 =\n      createVideoFormat(/* bitrate= */ 2420, /* width= */ 960, /* height= */ 720);\n  private static final int BUFFER_LEVEL_FORMAT_2 =\n      (MIN_BUFFER_MS + MAX_BUFFER_MS - HYSTERESIS_BUFFER_MS) / 2;\n  private static final int BUFFER_LEVEL_FORMAT_3 = MAX_BUFFER_MS - HYSTERESIS_BUFFER_MS;\n\n  @Mock private BandwidthMeter mockBandwidthMeter;\n  private TrackSelection trackSelection;\n\n  @Before\n  public void setUp() {\n    initMocks(this);\n    Pair<TrackSelection.Factory, LoadControl> trackSelectionFactoryAndLoadControl =\n        new BufferSizeAdaptationBuilder()\n            .setBufferDurationsMs(\n                MIN_BUFFER_MS,\n                MAX_BUFFER_MS,\n                /* bufferForPlaybackMs= */ 1000,\n                /* bufferForPlaybackAfterRebufferMs= */ 1000)\n            .setHysteresisBufferMs(HYSTERESIS_BUFFER_MS)\n            .setStartUpTrackSelectionParameters(\n                BANDWIDTH_FRACTION, MIN_BUFFER_FOR_QUALITY_INCREASE_MS)\n            .buildPlayerComponents();\n    trackSelection =\n        trackSelectionFactoryAndLoadControl\n            .first\n            .createTrackSelections(\n                new TrackSelection.Definition[] {\n                  new TrackSelection.Definition(\n                      new TrackGroup(format1, format2, format3), /* tracks= */ 0, 1, 2)\n                },\n                mockBandwidthMeter)[0];\n    trackSelection.enable();\n  }\n\n  @Test\n  public void updateSelectedTrack_usesBandwidthEstimateForInitialSelection() {\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2));\n\n    updateSelectedTrack(/* bufferedDurationMs= */ 0);\n\n    assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2);\n    assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL);\n  }\n\n  @Test\n  public void updateSelectedTrack_withLowerBandwidthEstimateDuringStartUp_switchesDown() {\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2));\n    updateSelectedTrack(/* bufferedDurationMs= */ 0);\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L);\n\n    updateSelectedTrack(/* bufferedDurationMs= */ 0);\n\n    assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1);\n    assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE);\n  }\n\n  @Test\n  public void\n      updateSelectedTrack_withHigherBandwidthEstimateDuringStartUp_andLowBuffer_keepsSelection() {\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2));\n    updateSelectedTrack(/* bufferedDurationMs= */ 0);\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3));\n\n    updateSelectedTrack(/* bufferedDurationMs= */ MIN_BUFFER_FOR_QUALITY_INCREASE_MS - 1);\n\n    assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2);\n    assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL);\n  }\n\n  @Test\n  public void\n      updateSelectedTrack_withHigherBandwidthEstimateDuringStartUp_andHighBuffer_switchesUp() {\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2));\n    updateSelectedTrack(/* bufferedDurationMs= */ 0);\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3));\n\n    updateSelectedTrack(/* bufferedDurationMs= */ MIN_BUFFER_FOR_QUALITY_INCREASE_MS);\n\n    assertThat(trackSelection.getSelectedFormat()).isEqualTo(format3);\n    assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE);\n  }\n\n  @Test\n  public void\n      updateSelectedTrack_withIncreasedBandwidthEstimate_onceSteadyStateBufferIsReached_keepsSelection() {\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2));\n    updateSelectedTrack(/* bufferedDurationMs= */ 0);\n    updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2);\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3));\n\n    updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2);\n\n    assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2);\n    assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL);\n  }\n\n  @Test\n  public void\n      updateSelectedTrack_withDecreasedBandwidthEstimate_onceSteadyStateBufferIsReached_keepsSelection() {\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2));\n    updateSelectedTrack(/* bufferedDurationMs= */ 0);\n    updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2);\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L);\n\n    updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2);\n\n    assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2);\n    assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL);\n  }\n\n  @Test\n  public void updateSelectedTrack_withIncreasedBufferInSteadyState_switchesUp() {\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2));\n    updateSelectedTrack(/* bufferedDurationMs= */ 0);\n    updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2);\n\n    updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_3);\n\n    assertThat(trackSelection.getSelectedFormat()).isEqualTo(format3);\n    assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE);\n  }\n\n  @Test\n  public void updateSelectedTrack_withDecreasedBufferInSteadyState_switchesDown() {\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2));\n    updateSelectedTrack(/* bufferedDurationMs= */ 0);\n    updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2);\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L);\n\n    updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - HYSTERESIS_BUFFER_MS - 1);\n\n    assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1);\n    assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE);\n  }\n\n  @Test\n  public void\n      updateSelectedTrack_withDecreasedBufferInSteadyState_withinHysteresis_keepsSelection() {\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2));\n    updateSelectedTrack(/* bufferedDurationMs= */ 0);\n    updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2);\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L);\n\n    updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - HYSTERESIS_BUFFER_MS);\n\n    assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2);\n    assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL);\n  }\n\n  @Test\n  public void onDiscontinuity_switchesBackToStartUpState() {\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2));\n    updateSelectedTrack(/* bufferedDurationMs= */ 0);\n    updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2);\n    when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L);\n\n    trackSelection.onDiscontinuity();\n    updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - 1);\n\n    assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1);\n    assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE);\n  }\n\n  private void updateSelectedTrack(long bufferedDurationMs) {\n    trackSelection.updateSelectedTrack(\n        /* playbackPositionUs= */ 0,\n        /* bufferedDurationUs= */ C.msToUs(bufferedDurationMs),\n        /* availableDurationUs= */ C.TIME_UNSET,\n        /* queue= */ Collections.emptyList(),\n        /* mediaChunkIterators= */ new MediaChunkIterator[] {\n          MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY\n        });\n  }\n\n  private static Format createVideoFormat(int bitrate, int width, int height) {\n    return Format.createVideoSampleFormat(\n        /* id= */ null,\n        /* sampleMimeType= */ MimeTypes.VIDEO_H264,\n        /* codecs= */ null,\n        /* bitrate= */ bitrate,\n        /* maxInputSize= */ Format.NO_VALUE,\n        /* width= */ width,\n        /* height= */ height,\n        /* frameRate= */ Format.NO_VALUE,\n        /* initializationData= */ null,\n        /* drmInitData= */ null);\n  }\n\n  private static long getBitrateEstimateEnoughFor(Format format) {\n    return (long) (format.bitrate / BANDWIDTH_FRACTION) + 1;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.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 static com.google.android.exoplayer2.RendererCapabilities.ADAPTIVE_NOT_SEAMLESS;\nimport static com.google.android.exoplayer2.RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES;\nimport static com.google.android.exoplayer2.RendererCapabilities.FORMAT_HANDLED;\nimport static com.google.android.exoplayer2.RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE;\nimport static com.google.android.exoplayer2.RendererConfiguration.DEFAULT;\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\nimport android.os.Parcel;\nimport android.util.SparseArray;\nimport android.util.SparseBooleanArray;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\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.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.testutil.FakeTimeline;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;\nimport com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.junit.Before;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\n\n/** Unit tests for {@link DefaultTrackSelector}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DefaultTrackSelectorTest {\n\n  private static final RendererCapabilities ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES =\n      new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO);\n  private static final RendererCapabilities ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES =\n      new FakeRendererCapabilities(C.TRACK_TYPE_TEXT);\n  private static final RendererCapabilities ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES =\n      new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO, FORMAT_EXCEEDS_CAPABILITIES);\n\n  private static final RendererCapabilities VIDEO_CAPABILITIES =\n      new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO);\n  private static final RendererCapabilities AUDIO_CAPABILITIES =\n      new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO);\n  private static final RendererCapabilities NO_SAMPLE_CAPABILITIES =\n      new FakeRendererCapabilities(C.TRACK_TYPE_NONE);\n  private static final RendererCapabilities[] RENDERER_CAPABILITIES =\n      new RendererCapabilities[] {VIDEO_CAPABILITIES, AUDIO_CAPABILITIES};\n  private static final RendererCapabilities[] RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER =\n      new RendererCapabilities[] {VIDEO_CAPABILITIES, NO_SAMPLE_CAPABILITIES};\n\n  private static final Format VIDEO_FORMAT = buildVideoFormat(\"video\");\n  private static final Format AUDIO_FORMAT = buildAudioFormat(\"audio\");\n  private static final TrackGroup VIDEO_TRACK_GROUP = new TrackGroup(VIDEO_FORMAT);\n  private static final TrackGroup AUDIO_TRACK_GROUP = new TrackGroup(AUDIO_FORMAT);\n  private static final TrackGroupArray TRACK_GROUPS =\n      new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP);\n  private static final TrackSelection[] TRACK_SELECTIONS =\n      new TrackSelection[] {\n        new FixedTrackSelection(VIDEO_TRACK_GROUP, 0), new FixedTrackSelection(AUDIO_TRACK_GROUP, 0)\n      };\n  private static final TrackSelection[] TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER =\n      new TrackSelection[] {new FixedTrackSelection(VIDEO_TRACK_GROUP, 0), null};\n\n  private static final Timeline TIMELINE = new FakeTimeline(/* windowCount= */ 1);\n\n  private static MediaPeriodId periodId;\n\n  @Mock private InvalidationListener invalidationListener;\n  @Mock private BandwidthMeter bandwidthMeter;\n\n  private DefaultTrackSelector trackSelector;\n\n  @BeforeClass\n  public static void setUpBeforeClass() {\n    periodId = new MediaPeriodId(TIMELINE.getUidOfPeriod(/* periodIndex= */ 0));\n  }\n\n  @Before\n  public void setUp() {\n    initMocks(this);\n    when(bandwidthMeter.getBitrateEstimate()).thenReturn(1000000L);\n    trackSelector = new DefaultTrackSelector();\n    trackSelector.init(invalidationListener, bandwidthMeter);\n  }\n\n  /** Tests {@link Parameters} {@link android.os.Parcelable} implementation. */\n  @Test\n  public void testParametersParcelable() {\n    SparseArray<Map<TrackGroupArray, SelectionOverride>> selectionOverrides = new SparseArray<>();\n    Map<TrackGroupArray, SelectionOverride> videoOverrides = new HashMap<>();\n    videoOverrides.put(new TrackGroupArray(VIDEO_TRACK_GROUP), new SelectionOverride(0, 1));\n    selectionOverrides.put(2, videoOverrides);\n\n    SparseBooleanArray rendererDisabledFlags = new SparseBooleanArray();\n    rendererDisabledFlags.put(3, true);\n\n    Parameters parametersToParcel =\n        new Parameters(\n            // Video\n            /* maxVideoWidth= */ 0,\n            /* maxVideoHeight= */ 1,\n            /* maxVideoFrameRate= */ 2,\n            /* maxVideoBitrate= */ 3,\n            /* exceedVideoConstraintsIfNecessary= */ false,\n            /* allowVideoMixedMimeTypeAdaptiveness= */ true,\n            /* allowVideoNonSeamlessAdaptiveness= */ false,\n            /* viewportWidth= */ 4,\n            /* viewportHeight= */ 5,\n            /* viewportOrientationMayChange= */ true,\n            // Audio\n            /* preferredAudioLanguage= */ \"en\",\n            /* maxAudioChannelCount= */ 6,\n            /* maxAudioBitrate= */ 7,\n            /* exceedAudioConstraintsIfNecessary= */ false,\n            /* allowAudioMixedMimeTypeAdaptiveness= */ true,\n            /* allowAudioMixedSampleRateAdaptiveness= */ false,\n            /* allowAudioMixedChannelCountAdaptiveness= */ true,\n            // Text\n            /* preferredTextLanguage= */ \"de\",\n            /* preferredTextRoleFlags= */ C.ROLE_FLAG_CAPTION,\n            /* selectUndeterminedTextLanguage= */ true,\n            /* disabledTextTrackSelectionFlags= */ 8,\n            // General\n            /* forceLowestBitrate= */ false,\n            /* forceHighestSupportedBitrate= */ true,\n            /* exceedRendererCapabilitiesIfNecessary= */ false,\n            /* tunnelingAudioSessionId= */ C.AUDIO_SESSION_ID_UNSET,\n            // Overrides\n            selectionOverrides,\n            rendererDisabledFlags);\n\n    Parcel parcel = Parcel.obtain();\n    parametersToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    Parameters parametersFromParcel = Parameters.CREATOR.createFromParcel(parcel);\n    assertThat(parametersFromParcel).isEqualTo(parametersToParcel);\n\n    parcel.recycle();\n  }\n\n  /** Tests {@link SelectionOverride}'s {@link android.os.Parcelable} implementation. */\n  @Test\n  public void testSelectionOverrideParcelable() {\n    int[] tracks = new int[] {2, 3};\n    SelectionOverride selectionOverrideToParcel =\n        new SelectionOverride(/* groupIndex= */ 1, tracks);\n\n    Parcel parcel = Parcel.obtain();\n    selectionOverrideToParcel.writeToParcel(parcel, 0);\n    parcel.setDataPosition(0);\n\n    SelectionOverride selectionOverrideFromParcel =\n        SelectionOverride.CREATOR.createFromParcel(parcel);\n    assertThat(selectionOverrideFromParcel).isEqualTo(selectionOverrideToParcel);\n\n    parcel.recycle();\n  }\n\n  /** Tests that a null override clears a track selection. */\n  @Test\n  public void testSelectTracksWithNullOverride() throws ExoPlaybackException {\n    trackSelector.setParameters(\n        trackSelector\n            .buildUponParameters()\n            .setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null));\n    TrackSelectorResult result =\n        trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS, periodId, TIMELINE);\n    assertSelections(result, new TrackSelection[] {null, TRACK_SELECTIONS[1]});\n    assertThat(result.rendererConfigurations)\n        .isEqualTo(new RendererConfiguration[] {null, DEFAULT});\n  }\n\n  /** Tests that a null override can be cleared. */\n  @Test\n  public void testSelectTracksWithClearedNullOverride() throws ExoPlaybackException {\n    trackSelector.setParameters(\n        trackSelector\n            .buildUponParameters()\n            .setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null)\n            .clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP)));\n    TrackSelectorResult result =\n        trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS, periodId, TIMELINE);\n    assertSelections(result, TRACK_SELECTIONS);\n    assertThat(result.rendererConfigurations)\n        .isEqualTo(new RendererConfiguration[] {DEFAULT, DEFAULT});\n  }\n\n  /** Tests that an override is not applied for a different set of available track groups. */\n  @Test\n  public void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException {\n    trackSelector.setParameters(\n        trackSelector\n            .buildUponParameters()\n            .setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null));\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            RENDERER_CAPABILITIES,\n            new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP),\n            periodId,\n            TIMELINE);\n    assertSelections(result, TRACK_SELECTIONS);\n    assertThat(result.rendererConfigurations)\n        .isEqualTo(new RendererConfiguration[] {DEFAULT, DEFAULT});\n  }\n\n  /** Tests disabling a renderer. */\n  @Test\n  public void testSelectTracksWithDisabledRenderer() throws ExoPlaybackException {\n    trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setRendererDisabled(1, true));\n    TrackSelectorResult result =\n        trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS, periodId, TIMELINE);\n    assertSelections(result, new TrackSelection[] {TRACK_SELECTIONS[0], null});\n    assertThat(new RendererConfiguration[] {DEFAULT, null})\n        .isEqualTo(result.rendererConfigurations);\n  }\n\n  /** Tests that a disabled renderer can be enabled again. */\n  @Test\n  public void testSelectTracksWithClearedDisabledRenderer() throws ExoPlaybackException {\n    trackSelector.setParameters(\n        trackSelector\n            .buildUponParameters()\n            .setRendererDisabled(1, true)\n            .setRendererDisabled(1, false));\n    TrackSelectorResult result =\n        trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS, periodId, TIMELINE);\n    assertSelections(result, TRACK_SELECTIONS);\n    assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT})\n        .isEqualTo(result.rendererConfigurations);\n  }\n\n  /** Tests a no-sample renderer is enabled without a track selection by default. */\n  @Test\n  public void testSelectTracksWithNoSampleRenderer() throws ExoPlaybackException {\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS, periodId, TIMELINE);\n    assertSelections(result, TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER);\n    assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT})\n        .isEqualTo(result.rendererConfigurations);\n  }\n\n  /** Tests disabling a no-sample renderer. */\n  @Test\n  public void testSelectTracksWithDisabledNoSampleRenderer() throws ExoPlaybackException {\n    trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setRendererDisabled(1, true));\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS, periodId, TIMELINE);\n    assertSelections(result, TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER);\n    assertThat(new RendererConfiguration[] {DEFAULT, null})\n        .isEqualTo(result.rendererConfigurations);\n  }\n\n  /**\n   * Tests that track selector will not call\n   * {@link InvalidationListener#onTrackSelectionsInvalidated()} when it's set with default\n   * values of {@link Parameters}.\n   */\n  @Test\n  public void testSetParameterWithDefaultParametersDoesNotNotifyInvalidationListener()\n      throws Exception {\n    trackSelector.setParameters(Parameters.DEFAULT);\n    verify(invalidationListener, never()).onTrackSelectionsInvalidated();\n  }\n\n  /**\n   * Tests that track selector will call {@link InvalidationListener#onTrackSelectionsInvalidated()}\n   * when it's set with non-default values of {@link Parameters}.\n   */\n  @Test\n  public void testSetParameterWithNonDefaultParameterNotifyInvalidationListener()\n      throws Exception {\n    Parameters parameters = Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage(\"eng\").build();\n    trackSelector.setParameters(parameters);\n    verify(invalidationListener).onTrackSelectionsInvalidated();\n  }\n\n  /**\n   * Tests that track selector will not call\n   * {@link InvalidationListener#onTrackSelectionsInvalidated()} again when it's set with\n   * the same values of {@link Parameters}.\n   */\n  @Test\n  public void testSetParameterWithSameParametersDoesNotNotifyInvalidationListenerAgain()\n      throws Exception {\n    ParametersBuilder builder = Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage(\"eng\");\n    trackSelector.setParameters(builder.build());\n    trackSelector.setParameters(builder.build());\n    verify(invalidationListener, times(1)).onTrackSelectionsInvalidated();\n  }\n\n  /**\n   * Tests that track selector will select audio track with {@link C#SELECTION_FLAG_DEFAULT}\n   * given default values of {@link Parameters}.\n   */\n  @Test\n  public void testSelectTracksSelectTrackWithSelectionFlag() throws Exception {\n    Format audioFormat =\n        buildAudioFormatWithLanguageAndFlags(\n            \"audio\", /* language= */ null, /* selectionFlags= */ 0);\n    Format formatWithSelectionFlag =\n        buildAudioFormatWithLanguageAndFlags(\n            \"audio\", /* language= */ null, C.SELECTION_FLAG_DEFAULT);\n    TrackGroupArray trackGroups = wrapFormats(audioFormat, formatWithSelectionFlag);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, formatWithSelectionFlag);\n  }\n\n  /** Tests that adaptive audio track selections respect the maximum audio bitrate. */\n  public void testSelectAdaptiveAudioTrackGroupWithMaxBitrate() throws ExoPlaybackException {\n    Format format128k =\n        Format.createAudioSampleFormat(\n            /* id= */ \"128\",\n            /* sampleMimeType= */ MimeTypes.AUDIO_AAC,\n            /* codecs= */ \"mp4a.40.2\",\n            /* bitrate= */ 128 * 1024,\n            /* maxInputSize= */ Format.NO_VALUE,\n            /* channelCount= */ 2,\n            /* sampleRate= */ 44100,\n            /* initializationData= */ null,\n            /* drmInitData= */ null,\n            /* selectionFlags= */ 0,\n            /* language= */ null);\n    Format format192k =\n        Format.createAudioSampleFormat(\n            /* id= */ \"192\",\n            /* sampleMimeType= */ MimeTypes.AUDIO_AAC,\n            /* codecs= */ \"mp4a.40.2\",\n            /* bitrate= */ 192 * 1024,\n            /* maxInputSize= */ Format.NO_VALUE,\n            /* channelCount= */ 2,\n            /* sampleRate= */ 44100,\n            /* initializationData= */ null,\n            /* drmInitData= */ null,\n            /* selectionFlags= */ 0,\n            /* language= */ null);\n    Format format256k =\n        Format.createAudioSampleFormat(\n            /* id= */ \"256\",\n            /* sampleMimeType= */ MimeTypes.AUDIO_AAC,\n            /* codecs= */ \"mp4a.40.2\",\n            /* bitrate= */ 256 * 1024,\n            /* maxInputSize= */ Format.NO_VALUE,\n            /* channelCount= */ 2,\n            /* sampleRate= */ 44100,\n            /* initializationData= */ null,\n            /* drmInitData= */ null,\n            /* selectionFlags= */ 0,\n            /* language= */ null);\n    RendererCapabilities[] rendererCapabilities = {\n      ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES\n    };\n    TrackGroupArray trackGroups =\n        new TrackGroupArray(new TrackGroup(format192k, format128k, format256k));\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1, 2);\n\n    trackSelector.setParameters(\n        trackSelector.buildUponParameters().setMaxAudioBitrate(256 * 1024 - 1));\n    result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);\n\n    trackSelector.setParameters(trackSelector.buildUponParameters().setMaxAudioBitrate(192 * 1024));\n    result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);\n\n    trackSelector.setParameters(\n        trackSelector.buildUponParameters().setMaxAudioBitrate(192 * 1024 - 1));\n    result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1);\n\n    trackSelector.setParameters(trackSelector.buildUponParameters().setMaxAudioBitrate(10));\n    result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1);\n  }\n\n  /**\n   * Tests that track selector will select audio track with language that match preferred language\n   * given by {@link Parameters}.\n   */\n  @Test\n  public void testSelectTracksSelectPreferredAudioLanguage()\n      throws Exception {\n    Format frAudioFormat =\n        Format.createAudioSampleFormat(\"audio\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 44100, null, null, 0, \"fra\");\n    Format enAudioFormat =\n        Format.createAudioSampleFormat(\"audio\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 44100, null, null, 0, \"eng\");\n    TrackGroupArray trackGroups = wrapFormats(frAudioFormat, enAudioFormat);\n\n    trackSelector.setParameters(\n        Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage(\"eng\").build());\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES},\n            wrapFormats(frAudioFormat, enAudioFormat),\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, enAudioFormat);\n  }\n\n  /**\n   * Tests that track selector will prefer selecting audio track with language that match preferred\n   * language given by {@link Parameters} over track with {@link C#SELECTION_FLAG_DEFAULT}.\n   */\n  @Test\n  public void testSelectTracksSelectPreferredAudioLanguageOverSelectionFlag()\n      throws Exception {\n    Format frAudioFormat =\n        Format.createAudioSampleFormat(\"audio\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, \"fra\");\n    Format enAudioFormat =\n        Format.createAudioSampleFormat(\"audio\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 44100, null, null, 0, \"eng\");\n    TrackGroupArray trackGroups = wrapFormats(frAudioFormat, enAudioFormat);\n\n    trackSelector.setParameters(\n        Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage(\"eng\").build());\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, enAudioFormat);\n  }\n\n  /**\n   * Tests that track selector will prefer tracks that are within renderer's capabilities over\n   * track that exceed renderer's capabilities.\n   */\n  @Test\n  public void testSelectTracksPreferTrackWithinCapabilities() throws Exception {\n    Format supportedFormat = buildAudioFormat(\"supportedFormat\");\n    Format exceededFormat = buildAudioFormat(\"exceededFormat\");\n    TrackGroupArray trackGroups = wrapFormats(exceededFormat, supportedFormat);\n\n    Map<String, Integer> mappedCapabilities = new HashMap<>();\n    mappedCapabilities.put(supportedFormat.id, FORMAT_HANDLED);\n    mappedCapabilities.put(exceededFormat.id, FORMAT_EXCEEDS_CAPABILITIES);\n    RendererCapabilities mappedAudioRendererCapabilities =\n        new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {mappedAudioRendererCapabilities},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, supportedFormat);\n  }\n\n  /**\n   * Tests that track selector will select a track that exceeds the renderer's capabilities when\n   * there are no other choice, given the default {@link Parameters}.\n   */\n  @Test\n  public void testSelectTracksWithNoTrackWithinCapabilitiesSelectExceededCapabilityTrack()\n      throws Exception {\n    Format audioFormat =\n        Format.createAudioSampleFormat(\"audio\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 44100, null, null, 0, null);\n    TrackGroupArray trackGroups = singleTrackGroup(audioFormat);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, audioFormat);\n  }\n\n  /**\n   * Tests that track selector will return a null track selection for a renderer when\n   * all tracks exceed that renderer's capabilities when {@link Parameters} does not allow\n   * exceeding-capabilities tracks.\n   */\n  @Test\n  public void testSelectTracksWithNoTrackWithinCapabilitiesAndSetByParamsReturnNoSelection()\n      throws Exception {\n    Format audioFormat =\n        Format.createAudioSampleFormat(\"audio\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 44100, null, null, 0, null);\n    TrackGroupArray trackGroups = singleTrackGroup(audioFormat);\n\n    trackSelector.setParameters(\n        Parameters.DEFAULT.buildUpon().setExceedRendererCapabilitiesIfNecessary(false).build());\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertNoSelection(result.selections.get(0));\n  }\n\n  /**\n   * Tests that track selector will prefer tracks that are within renderer's capabilities over\n   * tracks that have {@link C#SELECTION_FLAG_DEFAULT} but exceed renderer's capabilities.\n   */\n  @Test\n  public void testSelectTracksPreferTrackWithinCapabilitiesOverSelectionFlag()\n      throws Exception {\n    Format exceededWithSelectionFlagFormat =\n        Format.createAudioSampleFormat(\n            \"exceededFormat\",\n            MimeTypes.AUDIO_AAC,\n            null,\n            Format.NO_VALUE,\n            Format.NO_VALUE,\n            2,\n            44100,\n            null,\n            null,\n            C.SELECTION_FLAG_DEFAULT,\n            null);\n    Format supportedFormat =\n        Format.createAudioSampleFormat(\"supportedFormat\", MimeTypes.AUDIO_AAC, null,\n            Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null);\n    TrackGroupArray trackGroups = wrapFormats(exceededWithSelectionFlagFormat, supportedFormat);\n\n    Map<String, Integer> mappedCapabilities = new HashMap<>();\n    mappedCapabilities.put(supportedFormat.id, FORMAT_HANDLED);\n    mappedCapabilities.put(exceededWithSelectionFlagFormat.id, FORMAT_EXCEEDS_CAPABILITIES);\n    RendererCapabilities mappedAudioRendererCapabilities =\n        new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {mappedAudioRendererCapabilities},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, supportedFormat);\n  }\n\n  /**\n   * Tests that track selector will prefer tracks that are within renderer's capabilities over\n   * track that have language matching preferred audio given by {@link Parameters} but exceed\n   * renderer's capabilities.\n   */\n  @Test\n  public void testSelectTracksPreferTrackWithinCapabilitiesOverPreferredLanguage()\n      throws Exception {\n    Format exceededEnFormat =\n        Format.createAudioSampleFormat(\n            \"exceededFormat\",\n            MimeTypes.AUDIO_AAC,\n            null,\n            Format.NO_VALUE,\n            Format.NO_VALUE,\n            2,\n            44100,\n            null,\n            null,\n            0,\n            \"eng\");\n    Format supportedFrFormat =\n        Format.createAudioSampleFormat(\"supportedFormat\", MimeTypes.AUDIO_AAC, null,\n            Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, \"fra\");\n    TrackGroupArray trackGroups = wrapFormats(exceededEnFormat, supportedFrFormat);\n\n    Map<String, Integer> mappedCapabilities = new HashMap<>();\n    mappedCapabilities.put(exceededEnFormat.id, FORMAT_EXCEEDS_CAPABILITIES);\n    mappedCapabilities.put(supportedFrFormat.id, FORMAT_HANDLED);\n    RendererCapabilities mappedAudioRendererCapabilities =\n        new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities);\n\n    trackSelector.setParameters(\n        Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage(\"eng\").build());\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {mappedAudioRendererCapabilities},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, supportedFrFormat);\n  }\n\n  /**\n   * Tests that track selector will prefer tracks that are within renderer's capabilities over\n   * track that have both language matching preferred audio given by {@link Parameters} and\n   * {@link C#SELECTION_FLAG_DEFAULT}, but exceed renderer's capabilities.\n   */\n  @Test\n  public void testSelectTracksPreferTrackWithinCapabilitiesOverSelectionFlagAndPreferredLanguage()\n      throws Exception {\n    Format exceededDefaultSelectionEnFormat =\n        Format.createAudioSampleFormat(\n            \"exceededFormat\",\n            MimeTypes.AUDIO_AAC,\n            null,\n            Format.NO_VALUE,\n            Format.NO_VALUE,\n            2,\n            44100,\n            null,\n            null,\n            C.SELECTION_FLAG_DEFAULT,\n            \"eng\");\n    Format supportedFrFormat =\n        Format.createAudioSampleFormat(\"supportedFormat\", MimeTypes.AUDIO_AAC, null,\n            Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, \"fra\");\n    TrackGroupArray trackGroups = wrapFormats(exceededDefaultSelectionEnFormat, supportedFrFormat);\n\n    Map<String, Integer> mappedCapabilities = new HashMap<>();\n    mappedCapabilities.put(exceededDefaultSelectionEnFormat.id, FORMAT_EXCEEDS_CAPABILITIES);\n    mappedCapabilities.put(supportedFrFormat.id, FORMAT_HANDLED);\n    RendererCapabilities mappedAudioRendererCapabilities =\n        new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities);\n\n    trackSelector.setParameters(\n        Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage(\"eng\").build());\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {mappedAudioRendererCapabilities},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, supportedFrFormat);\n  }\n\n  /**\n   * Tests that track selector will select audio tracks with higher num channel when other factors\n   * are the same, and tracks are within renderer's capabilities.\n   */\n  @Test\n  public void testSelectTracksWithinCapabilitiesSelectHigherNumChannel()\n      throws Exception {\n    Format higherChannelFormat =\n        Format.createAudioSampleFormat(\n            \"audioFormat\",\n            MimeTypes.AUDIO_AAC,\n            null,\n            Format.NO_VALUE,\n            Format.NO_VALUE,\n            6,\n            44100,\n            null,\n            null,\n            0,\n            null);\n    Format lowerChannelFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 44100, null, null, 0, null);\n    TrackGroupArray trackGroups = wrapFormats(higherChannelFormat, lowerChannelFormat);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, higherChannelFormat);\n  }\n\n  /**\n   * Tests that track selector will select audio tracks with higher sample rate when other factors\n   * are the same, and tracks are within renderer's capabilities.\n   */\n  @Test\n  public void testSelectTracksWithinCapabilitiesSelectHigherSampleRate()\n      throws Exception {\n    Format higherSampleRateFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 44100, null, null, 0, null);\n    Format lowerSampleRateFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 22050, null, null, 0, null);\n    TrackGroupArray trackGroups = wrapFormats(higherSampleRateFormat, lowerSampleRateFormat);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, higherSampleRateFormat);\n  }\n\n  /**\n   * Tests that track selector will select audio tracks with higher bit rate when other factors are\n   * the same, and tracks are within renderer's capabilities, and have the same language.\n   */\n  @Test\n  public void selectAudioTracks_withinCapabilities_andSameLanguage_selectsHigherBitrate()\n      throws Exception {\n    Format lowerBitrateFormat =\n        Format.createAudioSampleFormat(\n            \"audioFormat\",\n            MimeTypes.AUDIO_AAC,\n            /* codecs= */ null,\n            /* bitrate= */ 15000,\n            /* maxInputSize= */ Format.NO_VALUE,\n            /* channelCount= */ 2,\n            /* sampleRate= */ 44100,\n            /* initializationData= */ null,\n            /* drmInitData= */ null,\n            /* selectionFlags= */ 0,\n            /* language= */ \"hi\");\n    Format higherBitrateFormat =\n        Format.createAudioSampleFormat(\n            \"audioFormat\",\n            MimeTypes.AUDIO_AAC,\n            /* codecs= */ null,\n            /* bitrate= */ 30000,\n            /* maxInputSize= */ Format.NO_VALUE,\n            /* channelCount= */ 2,\n            /* sampleRate= */ 44100,\n            /* initializationData= */ null,\n            /* drmInitData= */ null,\n            /* selectionFlags= */ 0,\n            /* language= */ \"hi\");\n    TrackGroupArray trackGroups = wrapFormats(lowerBitrateFormat, higherBitrateFormat);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, higherBitrateFormat);\n  }\n\n  /**\n   * Tests that track selector will select the first audio track even if other tracks with a\n   * different language have higher bit rates, all other factors are the same, and tracks are within\n   * renderer's capabilities.\n   */\n  @Test\n  public void selectAudioTracks_withinCapabilities_andDifferentLanguage_selectsFirstTrack()\n      throws Exception {\n    Format firstLanguageFormat =\n        Format.createAudioSampleFormat(\n            \"audioFormat\",\n            MimeTypes.AUDIO_AAC,\n            /* codecs= */ null,\n            /* bitrate= */ 15000,\n            /* maxInputSize= */ Format.NO_VALUE,\n            /* channelCount= */ 2,\n            /* sampleRate= */ 44100,\n            /* initializationData= */ null,\n            /* drmInitData= */ null,\n            /* selectionFlags= */ 0,\n            /* language= */ \"hi\");\n    Format higherBitrateFormat =\n        Format.createAudioSampleFormat(\n            \"audioFormat\",\n            MimeTypes.AUDIO_AAC,\n            /* codecs= */ null,\n            /* bitrate= */ 30000,\n            /* maxInputSize= */ Format.NO_VALUE,\n            /* channelCount= */ 2,\n            /* sampleRate= */ 44100,\n            /* initializationData= */ null,\n            /* drmInitData= */ null,\n            /* selectionFlags= */ 0,\n            /* language= */ \"te\");\n    TrackGroupArray trackGroups = wrapFormats(firstLanguageFormat, higherBitrateFormat);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, firstLanguageFormat);\n  }\n\n  /**\n   * Tests that track selector will prefer audio tracks with higher channel count over tracks with\n   * higher sample rate when other factors are the same, and tracks are within renderer's\n   * capabilities.\n   */\n  @Test\n  public void testSelectTracksPreferHigherNumChannelBeforeSampleRate() throws Exception {\n    Format higherChannelLowerSampleRateFormat =\n        Format.createAudioSampleFormat(\n            \"audioFormat\",\n            MimeTypes.AUDIO_AAC,\n            null,\n            Format.NO_VALUE,\n            Format.NO_VALUE,\n            6,\n            22050,\n            null,\n            null,\n            0,\n            null);\n    Format lowerChannelHigherSampleRateFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 44100, null, null, 0, null);\n    TrackGroupArray trackGroups =\n        wrapFormats(higherChannelLowerSampleRateFormat, lowerChannelHigherSampleRateFormat);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, higherChannelLowerSampleRateFormat);\n  }\n\n  /**\n   * Tests that track selector will prefer audio tracks with higher sample rate over tracks with\n   * higher bitrate when other factors are the same, and tracks are within renderer's\n   * capabilities.\n   */\n  @Test\n  public void testSelectTracksPreferHigherSampleRateBeforeBitrate()\n      throws Exception {\n    Format higherSampleRateLowerBitrateFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, 15000,\n            Format.NO_VALUE, 2, 44100, null, null, 0, null);\n    Format lowerSampleRateHigherBitrateFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, 30000,\n            Format.NO_VALUE, 2, 22050, null, null, 0, null);\n    TrackGroupArray trackGroups =\n        wrapFormats(higherSampleRateLowerBitrateFormat, lowerSampleRateHigherBitrateFormat);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, higherSampleRateLowerBitrateFormat);\n  }\n\n  /**\n   * Tests that track selector will select audio tracks with lower num channel when other factors\n   * are the same, and tracks exceed renderer's capabilities.\n   */\n  @Test\n  public void testSelectTracksExceedingCapabilitiesSelectLowerNumChannel()\n      throws Exception {\n    Format higherChannelFormat =\n        Format.createAudioSampleFormat(\n            \"audioFormat\",\n            MimeTypes.AUDIO_AAC,\n            null,\n            Format.NO_VALUE,\n            Format.NO_VALUE,\n            6,\n            44100,\n            null,\n            null,\n            0,\n            null);\n    Format lowerChannelFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 44100, null, null, 0, null);\n    TrackGroupArray trackGroups = wrapFormats(higherChannelFormat, lowerChannelFormat);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, lowerChannelFormat);\n  }\n\n  /**\n   * Tests that track selector will select audio tracks with lower sample rate when other factors\n   * are the same, and tracks exceed renderer's capabilities.\n   */\n  @Test\n  public void testSelectTracksExceedingCapabilitiesSelectLowerSampleRate()\n      throws Exception {\n    Format lowerSampleRateFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 22050, null, null, 0, null);\n    Format higherSampleRateFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 44100, null, null, 0, null);\n    TrackGroupArray trackGroups = wrapFormats(higherSampleRateFormat, lowerSampleRateFormat);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, lowerSampleRateFormat);\n  }\n\n  /**\n   * Tests that track selector will select audio tracks with lower bit-rate when other factors\n   * are the same, and tracks exceed renderer's capabilities.\n   */\n  @Test\n  public void testSelectTracksExceedingCapabilitiesSelectLowerBitrate()\n      throws Exception {\n    Format lowerBitrateFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, 15000,\n            Format.NO_VALUE, 2, 44100, null, null, 0, null);\n    Format higherBitrateFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, 30000,\n            Format.NO_VALUE, 2, 44100, null, null, 0, null);\n    TrackGroupArray trackGroups = wrapFormats(lowerBitrateFormat, higherBitrateFormat);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, lowerBitrateFormat);\n  }\n\n  /**\n   * Tests that track selector will prefer audio tracks with lower channel count over tracks with\n   * lower sample rate when other factors are the same, and tracks are within renderer's\n   * capabilities.\n   */\n  @Test\n  public void testSelectTracksExceedingCapabilitiesPreferLowerNumChannelBeforeSampleRate()\n      throws Exception {\n    Format lowerChannelHigherSampleRateFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 2, 44100, null, null, 0, null);\n    Format higherChannelLowerSampleRateFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n            Format.NO_VALUE, 6, 22050, null, null, 0, null);\n    TrackGroupArray trackGroups =\n        wrapFormats(higherChannelLowerSampleRateFormat, lowerChannelHigherSampleRateFormat);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, lowerChannelHigherSampleRateFormat);\n  }\n\n  /**\n   * Tests that track selector will prefer audio tracks with lower sample rate over tracks with\n   * lower bitrate when other factors are the same, and tracks are within renderer's\n   * capabilities.\n   */\n  @Test\n  public void testSelectTracksExceedingCapabilitiesPreferLowerSampleRateBeforeBitrate()\n      throws Exception {\n    Format higherSampleRateLowerBitrateFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, 15000,\n            Format.NO_VALUE, 2, 44100, null, null, 0, null);\n    Format lowerSampleRateHigherBitrateFormat =\n        Format.createAudioSampleFormat(\"audioFormat\", MimeTypes.AUDIO_AAC, null, 30000,\n            Format.NO_VALUE, 2, 22050, null, null, 0, null);\n    TrackGroupArray trackGroups =\n        wrapFormats(higherSampleRateLowerBitrateFormat, lowerSampleRateHigherBitrateFormat);\n\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, lowerSampleRateHigherBitrateFormat);\n  }\n\n  /** Tests text track selection flags. */\n  @Test\n  public void testTextTrackSelectionFlags() throws ExoPlaybackException {\n    Format forcedOnly = buildTextFormat(\"forcedOnly\", \"eng\", C.SELECTION_FLAG_FORCED);\n    Format forcedDefault =\n        buildTextFormat(\"forcedDefault\", \"eng\", C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT);\n    Format defaultOnly = buildTextFormat(\"defaultOnly\", \"eng\", C.SELECTION_FLAG_DEFAULT);\n    Format noFlag = buildTextFormat(\"noFlag\", \"eng\");\n\n    RendererCapabilities[] textRendererCapabilities =\n        new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES};\n\n    // There is no text language preference, the first track flagged as default should be selected.\n    TrackGroupArray trackGroups = wrapFormats(forcedOnly, forcedDefault, defaultOnly, noFlag);\n    TrackSelectorResult result =\n        trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, forcedDefault);\n\n    // Ditto.\n    trackGroups = wrapFormats(forcedOnly, noFlag, defaultOnly);\n    result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, defaultOnly);\n\n    // Default flags are disabled and no language preference is provided, so no text track is\n    // selected.\n    trackGroups = wrapFormats(defaultOnly, noFlag, forcedOnly, forcedDefault);\n    trackSelector.setParameters(\n        Parameters.DEFAULT\n            .buildUpon()\n            .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT)\n            .build());\n    result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertNoSelection(result.selections.get(0));\n\n    // All selection flags are disabled and there is no language preference, so nothing should be\n    // selected.\n    trackGroups = wrapFormats(forcedOnly, forcedDefault, defaultOnly, noFlag);\n    trackSelector.setParameters(\n        trackSelector\n            .getParameters()\n            .buildUpon()\n            .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT | C.SELECTION_FLAG_FORCED)\n            .build());\n    result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertNoSelection(result.selections.get(0));\n\n    // There is a preferred language, so a language-matching track flagged as default should\n    // be selected, and the one without forced flag should be preferred.\n    trackSelector.setParameters(\n        Parameters.DEFAULT.buildUpon().setPreferredTextLanguage(\"eng\").build());\n    result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, defaultOnly);\n\n    // Same as above, but the default flag is disabled. If multiple tracks match the preferred\n    // language, those not flagged as forced are preferred, as they likely include the contents of\n    // forced subtitles.\n    trackGroups = wrapFormats(noFlag, forcedOnly, forcedDefault, defaultOnly);\n    trackSelector.setParameters(\n        trackSelector\n            .getParameters()\n            .buildUpon()\n            .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT)\n            .build());\n    result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, noFlag);\n  }\n\n  /**\n   * Tests that the default track selector will select a forced text track matching the selected\n   * audio language when no text language preferences match.\n   */\n  @Test\n  public void testSelectingForcedTextTrackMatchesAudioLanguage() throws ExoPlaybackException {\n    Format forcedEnglish =\n        buildTextFormat(/* id= */ \"forcedEnglish\", /* language= */ \"eng\", C.SELECTION_FLAG_FORCED);\n    Format forcedGerman =\n        buildTextFormat(/* id= */ \"forcedGerman\", /* language= */ \"deu\", C.SELECTION_FLAG_FORCED);\n    Format forcedNoLanguage =\n        buildTextFormat(\n            /* id= */ \"forcedNoLanguage\",\n            /* language= */ C.LANGUAGE_UNDETERMINED,\n            C.SELECTION_FLAG_FORCED);\n    Format audio = buildAudioFormat(/* id= */ \"audio\");\n    Format germanAudio =\n        buildAudioFormat(\n            /* id= */ \"germanAudio\",\n            MimeTypes.AUDIO_AAC,\n            /* bitrate= */ Format.NO_VALUE,\n            \"deu\",\n            /* selectionFlags= */ 0,\n            /* channelCount= */ Format.NO_VALUE,\n            /* sampleRate= */ Format.NO_VALUE);\n\n    RendererCapabilities[] rendererCapabilities =\n        new RendererCapabilities[] {\n          ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES,\n          ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES\n        };\n\n    // Neither the audio nor the forced text track define a language. We select them both under the\n    // assumption that they have matching language.\n    TrackGroupArray trackGroups = wrapFormats(audio, forcedNoLanguage);\n    TrackSelectorResult result =\n        trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(1), trackGroups, forcedNoLanguage);\n\n    // No forced text track should be selected because none of the forced text tracks' languages\n    // matches the selected audio language.\n    trackGroups = wrapFormats(audio, forcedEnglish, forcedGerman);\n    result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertNoSelection(result.selections.get(1));\n\n    // The audio declares german. The german forced track should be selected.\n    trackGroups = wrapFormats(germanAudio, forcedGerman, forcedEnglish);\n    result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(1), trackGroups, forcedGerman);\n\n    // Ditto\n    trackGroups = wrapFormats(germanAudio, forcedEnglish, forcedGerman);\n    result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(1), trackGroups, forcedGerman);\n  }\n\n  /**\n   * Tests that the default track selector will select a text track with undetermined language if no\n   * text track with the preferred language is available but\n   * {@link Parameters#selectUndeterminedTextLanguage} is true.\n   */\n  @Test\n  public void testSelectUndeterminedTextLanguageAsFallback() throws ExoPlaybackException{\n    Format spanish = buildTextFormat(\"spanish\", \"spa\");\n    Format german = buildTextFormat(\"german\", \"de\");\n    Format undeterminedUnd = buildTextFormat(\"undeterminedUnd\", \"und\");\n    Format undeterminedNull = buildTextFormat(\"undeterminedNull\", null);\n\n    RendererCapabilities[] textRendererCapabilites =\n        new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES};\n\n    TrackGroupArray trackGroups = wrapFormats(spanish, german, undeterminedUnd, undeterminedNull);\n    TrackSelectorResult result =\n        trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);\n    assertNoSelection(result.selections.get(0));\n\n    trackSelector.setParameters(\n        Parameters.DEFAULT.buildUpon().setSelectUndeterminedTextLanguage(true).build());\n    result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, undeterminedUnd);\n\n    ParametersBuilder builder = Parameters.DEFAULT.buildUpon().setPreferredTextLanguage(\"spa\");\n    trackSelector.setParameters(builder.build());\n    result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, spanish);\n\n    trackGroups = wrapFormats(german, undeterminedUnd, undeterminedNull);\n\n    result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);\n    assertNoSelection(result.selections.get(0));\n\n    trackSelector.setParameters(builder.setSelectUndeterminedTextLanguage(true).build());\n    result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, undeterminedUnd);\n\n    trackGroups = wrapFormats(german, undeterminedNull);\n    result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, undeterminedNull);\n\n    trackGroups = wrapFormats(german);\n    result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);\n    assertNoSelection(result.selections.get(0));\n  }\n\n  /** Tests audio track selection when there are multiple audio renderers. */\n  @Test\n  public void testSelectPreferredTextTrackMultipleRenderers() throws Exception {\n    Format english = buildTextFormat(\"en\", \"en\");\n    Format german = buildTextFormat(\"de\", \"de\");\n\n    // First renderer handles english.\n    Map<String, Integer> firstRendererMappedCapabilities = new HashMap<>();\n    firstRendererMappedCapabilities.put(english.id, FORMAT_HANDLED);\n    firstRendererMappedCapabilities.put(german.id, FORMAT_UNSUPPORTED_SUBTYPE);\n    RendererCapabilities firstRendererCapabilities =\n        new FakeMappedRendererCapabilities(C.TRACK_TYPE_TEXT, firstRendererMappedCapabilities);\n\n    // Second renderer handles german.\n    Map<String, Integer> secondRendererMappedCapabilities = new HashMap<>();\n    secondRendererMappedCapabilities.put(english.id, FORMAT_UNSUPPORTED_SUBTYPE);\n    secondRendererMappedCapabilities.put(german.id, FORMAT_HANDLED);\n    RendererCapabilities secondRendererCapabilities =\n        new FakeMappedRendererCapabilities(C.TRACK_TYPE_TEXT, secondRendererMappedCapabilities);\n\n    RendererCapabilities[] rendererCapabilities =\n        new RendererCapabilities[] {firstRendererCapabilities, secondRendererCapabilities};\n    TrackGroupArray trackGroups = wrapFormats(english, german);\n\n    // Without an explicit language preference, nothing should be selected.\n    TrackSelectorResult result =\n        trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertNoSelection(result.selections.get(0));\n    assertNoSelection(result.selections.get(1));\n\n    // Explicit language preference for english. First renderer should be used.\n    trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setPreferredTextLanguage(\"en\"));\n    result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, english);\n    assertNoSelection(result.selections.get(1));\n\n    // Explicit language preference for German. Second renderer should be used.\n    trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setPreferredTextLanguage(\"de\"));\n    result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertNoSelection(result.selections.get(0));\n    assertFixedSelection(result.selections.get(1), trackGroups, german);\n  }\n\n  /**\n   * Tests that track selector will select the lowest bitrate supported audio track when {@link\n   * Parameters#forceLowestBitrate} is set.\n   */\n  @Test\n  public void testSelectTracksWithinCapabilitiesAndForceLowestBitrateSelectLowerBitrate()\n      throws Exception {\n    Format unsupportedLowBitrateFormat = buildAudioFormatWithBitrate(\"unsupportedLowBitrate\", 5000);\n    Format lowerBitrateFormat = buildAudioFormatWithBitrate(\"lowBitrate\", 15000);\n    Format higherBitrateFormat = buildAudioFormatWithBitrate(\"highBitrate\", 30000);\n    TrackGroupArray trackGroups =\n        wrapFormats(unsupportedLowBitrateFormat, lowerBitrateFormat, higherBitrateFormat);\n\n    Map<String, Integer> mappedCapabilities = new HashMap<>();\n    mappedCapabilities.put(unsupportedLowBitrateFormat.id, FORMAT_EXCEEDS_CAPABILITIES);\n    mappedCapabilities.put(lowerBitrateFormat.id, FORMAT_HANDLED);\n    mappedCapabilities.put(higherBitrateFormat.id, FORMAT_HANDLED);\n    RendererCapabilities mappedAudioRendererCapabilities =\n        new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities);\n\n    trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setForceLowestBitrate(true).build());\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {mappedAudioRendererCapabilities},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, lowerBitrateFormat);\n  }\n\n  /**\n   * Tests that track selector will select the highest bitrate supported audio track when {@link\n   * Parameters#forceHighestSupportedBitrate} is set.\n   */\n  @Test\n  public void testSelectTracksWithinCapabilitiesAndForceHighestBitrateSelectHigherBitrate()\n      throws Exception {\n    Format lowerBitrateFormat = buildAudioFormatWithBitrate(\"lowerBitrateFormat\", 5000);\n    Format higherBitrateFormat = buildAudioFormatWithBitrate(\"higherBitrateFormat\", 15000);\n    Format exceedsBitrateFormat = buildAudioFormatWithBitrate(\"exceedsBitrateFormat\", 30000);\n    TrackGroupArray trackGroups =\n        wrapFormats(lowerBitrateFormat, higherBitrateFormat, exceedsBitrateFormat);\n\n    Map<String, Integer> mappedCapabilities = new HashMap<>();\n    mappedCapabilities.put(lowerBitrateFormat.id, FORMAT_HANDLED);\n    mappedCapabilities.put(higherBitrateFormat.id, FORMAT_HANDLED);\n    mappedCapabilities.put(exceedsBitrateFormat.id, FORMAT_EXCEEDS_CAPABILITIES);\n    RendererCapabilities mappedAudioRendererCapabilities =\n        new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities);\n\n    trackSelector.setParameters(\n        new ParametersBuilder().setForceHighestSupportedBitrate(true).build());\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {mappedAudioRendererCapabilities},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, higherBitrateFormat);\n  }\n\n  @Test\n  public void testSelectTracksWithMultipleAudioTracks() throws Exception {\n    TrackGroupArray trackGroups = singleTrackGroup(buildAudioFormat(\"0\"), buildAudioFormat(\"1\"));\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n\n    assertThat(result.length).isEqualTo(1);\n    assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);\n  }\n\n  @Test\n  public void testSelectTracksWithMultipleAudioTracksWithMixedSampleRates() throws Exception {\n    Format highSampleRateAudioFormat =\n        buildAudioFormatWithSampleRate(\"44100\", /* sampleRate= */ 44100);\n    Format lowSampleRateAudioFormat =\n        buildAudioFormatWithSampleRate(\"22050\", /* sampleRate= */ 22050);\n\n    // Should not adapt between mixed sample rates by default, so we expect a fixed selection\n    // containing the higher sample rate stream.\n    TrackGroupArray trackGroups =\n        singleTrackGroup(highSampleRateAudioFormat, lowSampleRateAudioFormat);\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertFixedSelection(result.selections.get(0), trackGroups, highSampleRateAudioFormat);\n\n    // The same applies if the tracks are provided in the opposite order.\n    trackGroups = singleTrackGroup(lowSampleRateAudioFormat, highSampleRateAudioFormat);\n    result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertFixedSelection(result.selections.get(0), trackGroups, highSampleRateAudioFormat);\n\n    // If we explicitly enable mixed sample rate adaptiveness, expect an adaptive selection.\n    trackSelector.setParameters(\n        Parameters.DEFAULT.buildUpon().setAllowAudioMixedSampleRateAdaptiveness(true));\n    result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);\n  }\n\n  @Test\n  public void testSelectTracksWithMultipleAudioTracksWithMixedMimeTypes() throws Exception {\n    Format aacAudioFormat = buildAudioFormatWithMimeType(\"aac\", MimeTypes.AUDIO_AAC);\n    Format opusAudioFormat = buildAudioFormatWithMimeType(\"opus\", MimeTypes.AUDIO_OPUS);\n\n    // Should not adapt between mixed mime types by default, so we expect a fixed selection\n    // containing the first stream.\n    TrackGroupArray trackGroups = singleTrackGroup(aacAudioFormat, opusAudioFormat);\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertFixedSelection(result.selections.get(0), trackGroups, aacAudioFormat);\n\n    // The same applies if the tracks are provided in the opposite order.\n    trackGroups = singleTrackGroup(opusAudioFormat, aacAudioFormat);\n    result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertFixedSelection(result.selections.get(0), trackGroups, opusAudioFormat);\n\n    // If we explicitly enable mixed mime type adaptiveness, expect an adaptive selection.\n    trackSelector.setParameters(\n        Parameters.DEFAULT.buildUpon().setAllowAudioMixedMimeTypeAdaptiveness(true));\n    result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);\n  }\n\n  @Test\n  public void testSelectTracksWithMultipleAudioTracksWithMixedChannelCounts() throws Exception {\n    Format stereoAudioFormat =\n        buildAudioFormatWithChannelCount(\"2-channels\", /* channelCount= */ 2);\n    Format surroundAudioFormat =\n        buildAudioFormatWithChannelCount(\"5-channels\", /* channelCount= */ 5);\n\n    // Should not adapt between different channel counts, so we expect a fixed selection containing\n    // the track with more channels.\n    TrackGroupArray trackGroups = singleTrackGroup(stereoAudioFormat, surroundAudioFormat);\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertFixedSelection(result.selections.get(0), trackGroups, surroundAudioFormat);\n\n    // The same applies if the tracks are provided in the opposite order.\n    trackGroups = singleTrackGroup(surroundAudioFormat, stereoAudioFormat);\n    result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertFixedSelection(result.selections.get(0), trackGroups, surroundAudioFormat);\n\n    // If we constrain the channel count to 4 we expect a fixed selection containing the track with\n    // fewer channels.\n    trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setMaxAudioChannelCount(4));\n    result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertFixedSelection(result.selections.get(0), trackGroups, stereoAudioFormat);\n\n    // If we constrain the channel count to 2 we expect a fixed selection containing the track with\n    // fewer channels.\n    trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setMaxAudioChannelCount(2));\n    result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertFixedSelection(result.selections.get(0), trackGroups, stereoAudioFormat);\n\n    // If we constrain the channel count to 1 we expect a fixed selection containing the track with\n    // fewer channels.\n    trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setMaxAudioChannelCount(1));\n    result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertFixedSelection(result.selections.get(0), trackGroups, stereoAudioFormat);\n\n    // If we disable exceeding of constraints we expect no selection.\n    trackSelector.setParameters(\n        Parameters.DEFAULT\n            .buildUpon()\n            .setMaxAudioChannelCount(1)\n            .setExceedAudioConstraintsIfNecessary(false));\n    result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertNoSelection(result.selections.get(0));\n  }\n\n  @Test\n  public void testSelectTracksWithMultipleAudioTracksOverrideReturnsAdaptiveTrackSelection()\n      throws Exception {\n    TrackGroupArray trackGroups =\n        singleTrackGroup(buildAudioFormat(\"0\"), buildAudioFormat(\"1\"), buildAudioFormat(\"2\"));\n    trackSelector.setParameters(\n        trackSelector\n            .buildUponParameters()\n            .setSelectionOverride(\n                /* rendererIndex= */ 0,\n                trackGroups,\n                new SelectionOverride(/* groupIndex= */ 0, /* tracks= */ 1, 2)));\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n\n    assertThat(result.length).isEqualTo(1);\n    assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1, 2);\n  }\n\n  /** Tests audio track selection when there are multiple audio renderers. */\n  @Test\n  public void testSelectPreferredAudioTrackMultipleRenderers() throws Exception {\n    Format english = buildAudioFormatWithLanguage(\"en\", \"en\");\n    Format german = buildAudioFormatWithLanguage(\"de\", \"de\");\n\n    // First renderer handles english.\n    Map<String, Integer> firstRendererMappedCapabilities = new HashMap<>();\n    firstRendererMappedCapabilities.put(english.id, FORMAT_HANDLED);\n    firstRendererMappedCapabilities.put(german.id, FORMAT_UNSUPPORTED_SUBTYPE);\n    RendererCapabilities firstRendererCapabilities =\n        new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, firstRendererMappedCapabilities);\n\n    // Second renderer handles german.\n    Map<String, Integer> secondRendererMappedCapabilities = new HashMap<>();\n    secondRendererMappedCapabilities.put(english.id, FORMAT_UNSUPPORTED_SUBTYPE);\n    secondRendererMappedCapabilities.put(german.id, FORMAT_HANDLED);\n    RendererCapabilities secondRendererCapabilities =\n        new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, secondRendererMappedCapabilities);\n\n    RendererCapabilities[] rendererCapabilities =\n        new RendererCapabilities[] {firstRendererCapabilities, secondRendererCapabilities};\n\n    // Without an explicit language preference, prefer the first renderer.\n    TrackGroupArray trackGroups = wrapFormats(english, german);\n    TrackSelectorResult result =\n        trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, english);\n    assertNoSelection(result.selections.get(1));\n\n    // Explicit language preference for english. First renderer should be used.\n    trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage(\"en\"));\n    result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertFixedSelection(result.selections.get(0), trackGroups, english);\n    assertNoSelection(result.selections.get(1));\n\n    // Explicit language preference for German. Second renderer should be used.\n    trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage(\"de\"));\n    result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);\n    assertNoSelection(result.selections.get(0));\n    assertFixedSelection(result.selections.get(1), trackGroups, german);\n  }\n\n  @Test\n  public void testSelectTracksWithMultipleVideoTracks() throws Exception {\n    TrackGroupArray trackGroups = singleTrackGroup(buildVideoFormat(\"0\"), buildVideoFormat(\"1\"));\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n\n    assertThat(result.length).isEqualTo(1);\n    assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);\n  }\n\n  @Test\n  public void testSelectTracksWithMultipleVideoTracksWithNonSeamlessAdaptiveness()\n      throws Exception {\n    FakeRendererCapabilities nonSeamlessVideoCapabilities =\n        new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO, FORMAT_HANDLED | ADAPTIVE_NOT_SEAMLESS);\n\n    // Should do non-seamless adaptiveness by default, so expect an adaptive selection.\n    TrackGroupArray trackGroups = singleTrackGroup(buildVideoFormat(\"0\"), buildVideoFormat(\"1\"));\n    trackSelector.setParameters(\n        Parameters.DEFAULT.buildUpon().setAllowVideoNonSeamlessAdaptiveness(true));\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {nonSeamlessVideoCapabilities},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);\n\n    // If we explicitly disable non-seamless adaptiveness, expect a fixed selection.\n    trackSelector.setParameters(\n        Parameters.DEFAULT.buildUpon().setAllowVideoNonSeamlessAdaptiveness(false));\n    result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {nonSeamlessVideoCapabilities},\n            trackGroups,\n            periodId,\n            TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertFixedSelection(result.selections.get(0), trackGroups.get(0), 0);\n  }\n\n  @Test\n  public void testSelectTracksWithMultipleVideoTracksWithMixedMimeTypes() throws Exception {\n    Format h264VideoFormat = buildVideoFormatWithMimeType(\"h264\", MimeTypes.VIDEO_H264);\n    Format h265VideoFormat = buildVideoFormatWithMimeType(\"h265\", MimeTypes.VIDEO_H265);\n\n    // Should not adapt between mixed mime types by default, so we expect a fixed selection\n    // containing the first stream.\n    TrackGroupArray trackGroups = singleTrackGroup(h264VideoFormat, h265VideoFormat);\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertFixedSelection(result.selections.get(0), trackGroups, h264VideoFormat);\n\n    // The same applies if the tracks are provided in the opposite order.\n    trackGroups = singleTrackGroup(h265VideoFormat, h264VideoFormat);\n    result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertFixedSelection(result.selections.get(0), trackGroups, h265VideoFormat);\n\n    // If we explicitly enable mixed mime type adaptiveness, expect an adaptive selection.\n    trackSelector.setParameters(\n        Parameters.DEFAULT.buildUpon().setAllowVideoMixedMimeTypeAdaptiveness(true));\n    result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n    assertThat(result.length).isEqualTo(1);\n    assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);\n  }\n\n  @Test\n  public void testSelectTracksWithMultipleVideoTracksOverrideReturnsAdaptiveTrackSelection()\n      throws Exception {\n    TrackGroupArray trackGroups =\n        singleTrackGroup(buildVideoFormat(\"0\"), buildVideoFormat(\"1\"), buildVideoFormat(\"2\"));\n    trackSelector.setParameters(\n        trackSelector\n            .buildUponParameters()\n            .setSelectionOverride(\n                /* rendererIndex= */ 0,\n                trackGroups,\n                new SelectionOverride(/* groupIndex= */ 0, /* tracks= */ 1, 2)));\n    TrackSelectorResult result =\n        trackSelector.selectTracks(\n            new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);\n\n    assertThat(result.length).isEqualTo(1);\n    assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1, 2);\n  }\n\n  private static void assertSelections(TrackSelectorResult result, TrackSelection[] expected) {\n    assertThat(result.length).isEqualTo(expected.length);\n    for (int i = 0; i < expected.length; i++) {\n      assertThat(result.selections.get(i)).isEqualTo(expected[i]);\n    }\n  }\n\n  private static void assertFixedSelection(\n      TrackSelection selection, TrackGroupArray trackGroups, Format expectedFormat) {\n    int trackGroupIndex = -1;\n    for (int i = 0; i < trackGroups.length; i++) {\n      int expectedTrack = trackGroups.get(i).indexOf(expectedFormat);\n      if (expectedTrack != -1) {\n        assertThat(trackGroupIndex).isEqualTo(-1);\n        assertFixedSelection(selection, trackGroups.get(i), expectedTrack);\n        trackGroupIndex = i;\n      }\n    }\n    // Assert that we found the expected format in a track group\n    assertThat(trackGroupIndex).isNotEqualTo(-1);\n  }\n\n  private static void assertFixedSelection(\n      TrackSelection selection, TrackGroup expectedTrackGroup, int expectedTrack) {\n    assertThat(selection).isInstanceOf(FixedTrackSelection.class);\n    assertThat(selection.getTrackGroup()).isEqualTo(expectedTrackGroup);\n    assertThat(selection.length()).isEqualTo(1);\n    assertThat(selection.getIndexInTrackGroup(0)).isEqualTo(expectedTrack);\n    assertThat(selection.getFormat(0))\n        .isSameAs(expectedTrackGroup.getFormat(selection.getIndexInTrackGroup(0)));\n  }\n\n  private static void assertNoSelection(TrackSelection selection) {\n    assertThat(selection).isNull();\n  }\n\n  private static void assertAdaptiveSelection(\n      TrackSelection selection, TrackGroup expectedTrackGroup, int... expectedTracks) {\n    assertThat(selection).isInstanceOf(AdaptiveTrackSelection.class);\n    assertThat(selection.getTrackGroup()).isEqualTo(expectedTrackGroup);\n    assertThat(selection.length()).isEqualTo(expectedTracks.length);\n    for (int i = 0; i < expectedTracks.length; i++) {\n      assertThat(selection.getIndexInTrackGroup(i)).isEqualTo(expectedTracks[i]);\n      assertThat(selection.getFormat(i))\n          .isSameAs(expectedTrackGroup.getFormat(selection.getIndexInTrackGroup(i)));\n    }\n  }\n\n  private static TrackGroupArray singleTrackGroup(Format... formats) {\n    return new TrackGroupArray(new TrackGroup(formats));\n  }\n\n  private static TrackGroupArray wrapFormats(Format... formats) {\n    TrackGroup[] trackGroups = new TrackGroup[formats.length];\n    for (int i = 0; i < trackGroups.length; i++) {\n      trackGroups[i] = new TrackGroup(formats[i]);\n    }\n    return new TrackGroupArray(trackGroups);\n  }\n\n  private static Format buildVideoFormatWithMimeType(String id, String mimeType) {\n    return Format.createVideoSampleFormat(\n        id,\n        mimeType,\n        null,\n        Format.NO_VALUE,\n        Format.NO_VALUE,\n        1024,\n        768,\n        Format.NO_VALUE,\n        null,\n        null);\n  }\n\n  private static Format buildVideoFormat(String id) {\n    return buildVideoFormatWithMimeType(id, MimeTypes.VIDEO_H264);\n  }\n\n  private static Format buildAudioFormatWithLanguage(String id, String language) {\n    return buildAudioFormatWithLanguageAndFlags(id, language, /* selectionFlags= */ 0);\n  }\n\n  private static Format buildAudioFormatWithLanguageAndFlags(\n      String id, String language, int selectionFlags) {\n    return buildAudioFormat(\n        id,\n        MimeTypes.AUDIO_AAC,\n        /* bitrate= */ Format.NO_VALUE,\n        language,\n        selectionFlags,\n        /* channelCount= */ 2,\n        /* sampleRate= */ 44100);\n  }\n\n  private static Format buildAudioFormatWithBitrate(String id, int bitrate) {\n    return buildAudioFormat(\n        id,\n        MimeTypes.AUDIO_AAC,\n        bitrate,\n        /* language= */ null,\n        /* selectionFlags= */ 0,\n        /* channelCount= */ 2,\n        /* sampleRate= */ 44100);\n  }\n\n  private static Format buildAudioFormatWithSampleRate(String id, int sampleRate) {\n    return buildAudioFormat(\n        id,\n        MimeTypes.AUDIO_AAC,\n        /* bitrate= */ Format.NO_VALUE,\n        /* language= */ null,\n        /* selectionFlags= */ 0,\n        /* channelCount= */ 2,\n        sampleRate);\n  }\n\n  private static Format buildAudioFormatWithChannelCount(String id, int channelCount) {\n    return buildAudioFormat(\n        id,\n        MimeTypes.AUDIO_AAC,\n        /* bitrate= */ Format.NO_VALUE,\n        /* language= */ null,\n        /* selectionFlags= */ 0,\n        channelCount,\n        /* sampleRate= */ 44100);\n  }\n\n  private static Format buildAudioFormatWithMimeType(String id, String mimeType) {\n    return buildAudioFormat(\n        id,\n        mimeType,\n        /* bitrate= */ Format.NO_VALUE,\n        /* language= */ null,\n        /* selectionFlags= */ 0,\n        /* channelCount= */ 2,\n        /* sampleRate= */ 44100);\n  }\n\n  private static Format buildAudioFormat(String id) {\n    return buildAudioFormat(\n        id,\n        MimeTypes.AUDIO_AAC,\n        /* bitrate= */ Format.NO_VALUE,\n        /* language= */ null,\n        /* selectionFlags= */ 0,\n        /* channelCount= */ 2,\n        /* sampleRate= */ 44100);\n  }\n\n  private static Format buildAudioFormat(\n      String id,\n      String mimeType,\n      int bitrate,\n      String language,\n      int selectionFlags,\n      int channelCount,\n      int sampleRate) {\n    return Format.createAudioSampleFormat(\n        id,\n        mimeType,\n        /* codecs= */ null,\n        bitrate,\n        /* maxInputSize= */ Format.NO_VALUE,\n        channelCount,\n        sampleRate,\n        /* initializationData= */ null,\n        /* drmInitData= */ null,\n        selectionFlags,\n        language);\n  }\n\n  private static Format buildTextFormat(String id, String language) {\n    return buildTextFormat(id, language, /* selectionFlags= */ 0);\n  }\n\n  private static Format buildTextFormat(String id, String language, int selectionFlags) {\n    return Format.createTextContainerFormat(\n        id,\n        /* label= */ null,\n        /* containerMimeType= */ null,\n        /* sampleMimeType= */ MimeTypes.TEXT_VTT,\n        /* codecs= */ null,\n        /* bitrate= */ Format.NO_VALUE,\n        selectionFlags,\n        /* roleFlags= */ 0,\n        language);\n  }\n\n  /**\n   * A {@link RendererCapabilities} that advertises support for all formats of a given type using\n   * a provided support value. For any format that does not have the given track type,\n   * {@link #supportsFormat(Format)} will return {@link #FORMAT_UNSUPPORTED_TYPE}.\n   */\n  private static final class FakeRendererCapabilities implements RendererCapabilities {\n\n    private final int trackType;\n    private final int supportValue;\n\n    /**\n     * Returns {@link FakeRendererCapabilities} that advertises adaptive support for all\n     * tracks of the given type.\n     *\n     * @param trackType the track type of all formats that this renderer capabilities advertises\n     * support for.\n     */\n    FakeRendererCapabilities(int trackType) {\n      this(trackType, FORMAT_HANDLED | ADAPTIVE_SEAMLESS);\n    }\n\n    /**\n     * Returns {@link FakeRendererCapabilities} that advertises support level using given value\n     * for all tracks of the given type.\n     *\n     * @param trackType the track type of all formats that this renderer capabilities advertises\n     * support for.\n     * @param supportValue the support level value that will be returned for formats with\n     * the given type.\n     */\n    FakeRendererCapabilities(int trackType, int supportValue) {\n      this.trackType = trackType;\n      this.supportValue = supportValue;\n    }\n\n    @Override\n    public int getTrackType() {\n      return trackType;\n    }\n\n    @Override\n    public int supportsFormat(Format format) throws ExoPlaybackException {\n      return MimeTypes.getTrackType(format.sampleMimeType) == trackType\n          ? (supportValue) : FORMAT_UNSUPPORTED_TYPE;\n    }\n\n    @Override\n    public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {\n      return ADAPTIVE_SEAMLESS;\n    }\n\n  }\n\n  /**\n   * A {@link RendererCapabilities} that advertises support for different formats using a mapping\n   * between format ID and format-support value.\n   */\n  private static final class FakeMappedRendererCapabilities implements RendererCapabilities {\n\n    private final int trackType;\n    private final Map<String, Integer> formatToCapability;\n\n    /**\n     * Returns {@link FakeRendererCapabilities} that advertises support level using the given\n     * mapping between format ID and format-support value.\n     *\n     * @param trackType the track type to be returned for {@link #getTrackType()}\n     * @param formatToCapability a map of (format id, support level) that will be used to return\n     * support level for any given format. For any format that's not in the map,\n     * {@link #supportsFormat(Format)} will return {@link #FORMAT_UNSUPPORTED_TYPE}.\n     */\n    FakeMappedRendererCapabilities(int trackType, Map<String, Integer> formatToCapability) {\n      this.trackType = trackType;\n      this.formatToCapability = new HashMap<>(formatToCapability);\n    }\n\n    @Override\n    public int getTrackType() {\n      return trackType;\n    }\n\n    @Override\n    public int supportsFormat(Format format) throws ExoPlaybackException {\n      return format.id != null && formatToCapability.containsKey(format.id)\n          ? formatToCapability.get(format.id)\n          : FORMAT_UNSUPPORTED_TYPE;\n    }\n\n    @Override\n    public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {\n      return ADAPTIVE_SEAMLESS;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\n\nimport android.util.Pair;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\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.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.testutil.FakeTimeline;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport org.junit.BeforeClass;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link MappingTrackSelector}. */\n@RunWith(AndroidJUnit4.class)\npublic final class MappingTrackSelectorTest {\n\n  private static final RendererCapabilities VIDEO_CAPABILITIES =\n      new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO);\n  private static final RendererCapabilities AUDIO_CAPABILITIES =\n      new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO);\n  private static final RendererCapabilities[] RENDERER_CAPABILITIES = new RendererCapabilities[] {\n      VIDEO_CAPABILITIES, AUDIO_CAPABILITIES\n  };\n  private static final TrackGroup VIDEO_TRACK_GROUP = new TrackGroup(\n      Format.createVideoSampleFormat(\"video\", MimeTypes.VIDEO_H264, null, Format.NO_VALUE,\n          Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, null));\n  private static final TrackGroup AUDIO_TRACK_GROUP = new TrackGroup(\n      Format.createAudioSampleFormat(\"audio\", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE,\n          Format.NO_VALUE, 2, 44100, null, null, 0, null));\n  private static final TrackGroupArray TRACK_GROUPS = new TrackGroupArray(\n      VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP);\n  private static final Timeline TIMELINE = new FakeTimeline(/* windowCount= */ 1);\n\n  private static MediaPeriodId periodId;\n\n  @BeforeClass\n  public static void setUpBeforeClass() {\n    periodId = new MediaPeriodId(TIMELINE.getUidOfPeriod(/* periodIndex= */ 0));\n  }\n\n  /**\n   * Tests that the video and audio track groups are mapped onto the correct renderers.\n   */\n  @Test\n  public void testMapping() throws ExoPlaybackException {\n    FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector();\n    trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS, periodId, TIMELINE);\n    trackSelector.assertMappedTrackGroups(0, VIDEO_TRACK_GROUP);\n    trackSelector.assertMappedTrackGroups(1, AUDIO_TRACK_GROUP);\n  }\n\n  /**\n   * Tests that the video and audio track groups are mapped onto the correct renderers when the\n   * renderer ordering is reversed.\n   */\n  @Test\n  public void testMappingReverseOrder() throws ExoPlaybackException {\n    FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector();\n    RendererCapabilities[] reverseOrderRendererCapabilities = new RendererCapabilities[] {\n        AUDIO_CAPABILITIES, VIDEO_CAPABILITIES};\n    trackSelector.selectTracks(reverseOrderRendererCapabilities, TRACK_GROUPS, periodId, TIMELINE);\n    trackSelector.assertMappedTrackGroups(0, AUDIO_TRACK_GROUP);\n    trackSelector.assertMappedTrackGroups(1, VIDEO_TRACK_GROUP);\n  }\n\n  /**\n   * Tests video and audio track groups are mapped onto the correct renderers when there are\n   * multiple track groups of the same type.\n   */\n  @Test\n  public void testMappingMulti() throws ExoPlaybackException {\n    FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector();\n    TrackGroupArray multiTrackGroups = new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP,\n        VIDEO_TRACK_GROUP);\n    trackSelector.selectTracks(RENDERER_CAPABILITIES, multiTrackGroups, periodId, TIMELINE);\n    trackSelector.assertMappedTrackGroups(0, VIDEO_TRACK_GROUP, VIDEO_TRACK_GROUP);\n    trackSelector.assertMappedTrackGroups(1, AUDIO_TRACK_GROUP);\n  }\n\n  /**\n   * A {@link MappingTrackSelector} that stashes the {@link MappedTrackInfo} passed to {@link\n   * #selectTracks(MappedTrackInfo, int[][][], int[])}.\n   */\n  private static final class FakeMappingTrackSelector extends MappingTrackSelector {\n\n    private MappedTrackInfo lastMappedTrackInfo;\n\n    @Override\n    protected Pair<RendererConfiguration[], TrackSelection[]> selectTracks(\n        MappedTrackInfo mappedTrackInfo,\n        int[][][] rendererFormatSupports,\n        int[] rendererMixedMimeTypeAdaptationSupports)\n        throws ExoPlaybackException {\n      int rendererCount = mappedTrackInfo.getRendererCount();\n      lastMappedTrackInfo = mappedTrackInfo;\n      return Pair.create(\n          new RendererConfiguration[rendererCount], new TrackSelection[rendererCount]);\n    }\n\n    public void assertMappedTrackGroups(int rendererIndex, TrackGroup... expected) {\n      TrackGroupArray rendererTrackGroupArray = lastMappedTrackInfo.getTrackGroups(rendererIndex);\n      assertThat(rendererTrackGroupArray.length).isEqualTo(expected.length);\n      for (int i = 0; i < expected.length; i++) {\n        assertThat(rendererTrackGroupArray.get(i)).isEqualTo(expected[i]);\n      }\n    }\n\n  }\n\n  /**\n   * A {@link RendererCapabilities} that advertises adaptive support for all tracks of a given type.\n   */\n  private static final class FakeRendererCapabilities implements RendererCapabilities {\n\n    private final int trackType;\n\n    public FakeRendererCapabilities(int trackType) {\n      this.trackType = trackType;\n    }\n\n    @Override\n    public int getTrackType() {\n      return trackType;\n    }\n\n    @Override\n    public int supportsFormat(Format format) throws ExoPlaybackException {\n      return MimeTypes.getTrackType(format.sampleMimeType) == trackType\n          ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE;\n    }\n\n    @Override\n    public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {\n      return ADAPTIVE_SEAMLESS;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtilTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport com.google.android.exoplayer2.testutil.FakeMediaChunk;\nimport com.google.android.exoplayer2.testutil.FakeMediaChunkIterator;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** {@link TrackSelectionUtil} tests. */\n@RunWith(AndroidJUnit4.class)\npublic class TrackSelectionUtilTest {\n\n  public static final long MAX_DURATION_US = 30 * C.MICROS_PER_SECOND;\n\n  @Test\n  public void getAverageBitrate_emptyIterator_returnsNoValue() {\n    assertThat(TrackSelectionUtil.getAverageBitrate(MediaChunkIterator.EMPTY, MAX_DURATION_US))\n        .isEqualTo(Format.NO_VALUE);\n  }\n\n  @Test\n  public void getAverageBitrate_oneChunk_returnsChunkBitrate() {\n    long[] chunkTimeBoundariesSec = {12, 17};\n    long[] chunkLengths = {10};\n\n    FakeMediaChunkIterator iterator =\n        new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths);\n\n    assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)).isEqualTo(16);\n  }\n\n  @Test\n  public void getAverageBitrate_multipleSameDurationChunks_returnsAverageChunkBitrate() {\n    long[] chunkTimeBoundariesSec = {0, 5, 10};\n    long[] chunkLengths = {10, 20};\n\n    FakeMediaChunkIterator iterator =\n        new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths);\n\n    assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)).isEqualTo(24);\n  }\n\n  @Test\n  public void getAverageBitrate_multipleDifferentDurationChunks_returnsAverageChunkBitrate() {\n    long[] chunkTimeBoundariesSec = {0, 5, 15, 30};\n    long[] chunkLengths = {10, 20, 30};\n\n    FakeMediaChunkIterator iterator =\n        new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths);\n\n    assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)).isEqualTo(16);\n  }\n\n  @Test\n  public void getAverageBitrate_firstChunkLengthUnset_returnsNoValue() {\n    long[] chunkTimeBoundariesSec = {0, 5, 15, 30};\n    long[] chunkLengths = {C.LENGTH_UNSET, 20, 30};\n\n    FakeMediaChunkIterator iterator =\n        new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths);\n\n    assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US))\n        .isEqualTo(Format.NO_VALUE);\n  }\n\n  @Test\n  public void getAverageBitrate_secondChunkLengthUnset_returnsFirstChunkBitrate() {\n    long[] chunkTimeBoundariesSec = {0, 5, 15, 30};\n    long[] chunkLengths = {10, C.LENGTH_UNSET, 30};\n\n    FakeMediaChunkIterator iterator =\n        new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths);\n\n    assertThat(TrackSelectionUtil.getAverageBitrate(iterator, MAX_DURATION_US)).isEqualTo(16);\n  }\n\n  @Test\n  public void\n      getAverageBitrate_chunksExceedingMaxDuration_returnsAverageChunkBitrateUpToMaxDuration() {\n    long[] chunkTimeBoundariesSec = {0, 5, 15, 45, 50};\n    long[] chunkLengths = {10, 20, 30, 100};\n    FakeMediaChunkIterator iterator =\n        new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths);\n\n    long maxDurationUs = 30 * C.MICROS_PER_SECOND;\n    int averageBitrate = TrackSelectionUtil.getAverageBitrate(iterator, maxDurationUs);\n\n    assertThat(averageBitrate).isEqualTo(12);\n  }\n\n  @Test\n  public void getAverageBitrate_zeroMaxDuration_returnsNoValue() {\n    long[] chunkTimeBoundariesSec = {0, 5, 10};\n    long[] chunkLengths = {10, 20};\n\n    FakeMediaChunkIterator iterator =\n        new FakeMediaChunkIterator(chunkTimeBoundariesSec, chunkLengths);\n\n    assertThat(TrackSelectionUtil.getAverageBitrate(iterator, /* maxDurationUs= */ 0))\n        .isEqualTo(Format.NO_VALUE);\n  }\n\n  @Test\n  public void getBitratesUsingFutureInfo_noIterator_returnsEmptyArray() {\n    assertThat(\n            TrackSelectionUtil.getBitratesUsingFutureInfo(\n                new MediaChunkIterator[0], new Format[0], MAX_DURATION_US, /* bitrates= */ null))\n        .hasLength(0);\n  }\n\n  @Test\n  public void getBitratesUsingFutureInfo_emptyIterator_returnsNoValue() {\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingFutureInfo(\n            new MediaChunkIterator[] {MediaChunkIterator.EMPTY},\n            new Format[] {createFormatWithBitrate(10)},\n            MAX_DURATION_US,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(Format.NO_VALUE);\n  }\n\n  @Test\n  public void getBitratesUsingFutureInfo_twoTracksZeroMaxDuration_returnsNoValue() {\n    FakeMediaChunkIterator iterator1 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10});\n    FakeMediaChunkIterator iterator2 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30},\n            /* chunkLengths= */ new long[] {10, 20, 30});\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingFutureInfo(\n            new MediaChunkIterator[] {iterator1, iterator2},\n            new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)},\n            /* maxDurationUs= */ 0,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(Format.NO_VALUE, Format.NO_VALUE);\n  }\n\n  @Test\n  public void getBitratesUsingFutureInfo_twoTracks_returnsBitrates() {\n    FakeMediaChunkIterator iterator1 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10});\n    FakeMediaChunkIterator iterator2 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30},\n            /* chunkLengths= */ new long[] {10, 20, 30});\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingFutureInfo(\n            new MediaChunkIterator[] {iterator1, iterator2},\n            new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)},\n            MAX_DURATION_US,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(8, 16).inOrder();\n  }\n\n  @Test\n  public void getBitratesUsingFutureInfo_bitratesArrayGiven_returnsTheSameArray() {\n    FakeMediaChunkIterator iterator1 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10});\n    FakeMediaChunkIterator iterator2 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30},\n            /* chunkLengths= */ new long[] {10, 20, 30});\n\n    int[] bitratesArrayToUse = new int[2];\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingFutureInfo(\n            new MediaChunkIterator[] {iterator1, iterator2},\n            new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)},\n            MAX_DURATION_US,\n            bitratesArrayToUse);\n\n    assertThat(bitrates).isSameAs(bitratesArrayToUse);\n  }\n\n  @Test\n  public void getBitratesUsingFutureInfo_emptyIterator_returnsEstimationUsingClosest() {\n    FakeMediaChunkIterator iterator1 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {10});\n    Format format1 = createFormatWithBitrate(10);\n    MediaChunkIterator iterator2 = MediaChunkIterator.EMPTY;\n    Format format2 = createFormatWithBitrate(20);\n    FakeMediaChunkIterator iterator3 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {50});\n    Format format3 = createFormatWithBitrate(25);\n    FakeMediaChunkIterator iterator4 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {20});\n    Format format4 = createFormatWithBitrate(30);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingFutureInfo(\n            new MediaChunkIterator[] {iterator1, iterator2, iterator3, iterator4},\n            new Format[] {format1, format2, format3, format4},\n            MAX_DURATION_US,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(16, 64, 80, 32).inOrder();\n  }\n\n  @Test\n  public void getBitratesUsingFutureInfo_formatWithoutBitrate_returnsNoValueForEmpty() {\n    FakeMediaChunkIterator iterator1 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 5}, /* chunkLengths= */ new long[] {10});\n    Format format1 = createFormatWithBitrate(10);\n    MediaChunkIterator iterator2 = MediaChunkIterator.EMPTY;\n    Format format2 = createFormatWithBitrate(Format.NO_VALUE);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingFutureInfo(\n            new MediaChunkIterator[] {iterator1, iterator2},\n            new Format[] {format1, format2},\n            MAX_DURATION_US,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(16, Format.NO_VALUE).inOrder();\n  }\n\n  @Test\n  public void getBitratesUsingPastInfo_noFormat_returnsEmptyArray() {\n    FakeMediaChunk chunk =\n        createChunk(\n            createFormatWithBitrate(10),\n            /* length= */ 10,\n            /* startTimeSec= */ 0,\n            /* endTimeSec= */ 10);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastInfo(\n            Collections.singletonList(chunk), new Format[0], MAX_DURATION_US, /* bitrates= */ null);\n\n    assertThat(bitrates).hasLength(0);\n  }\n\n  @Test\n  public void getBitratesUsingPastInfo_emptyQueue_returnsNoValue() {\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastInfo(\n            Collections.emptyList(),\n            new Format[] {createFormatWithBitrate(10)},\n            MAX_DURATION_US,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(Format.NO_VALUE);\n  }\n\n  @Test\n  public void getBitratesUsingPastInfo_oneChunkFormatNoBitrate_returnsNoValue() {\n    Format format = createFormatWithBitrate(Format.NO_VALUE);\n    FakeMediaChunk chunk =\n        createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 10);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastInfo(\n            Collections.singletonList(chunk),\n            new Format[] {format},\n            MAX_DURATION_US,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(Format.NO_VALUE);\n  }\n\n  @Test\n  public void getBitratesUsingPastInfo_oneChunkNoLength_returnsNoValue() {\n    Format format = createFormatWithBitrate(10);\n    FakeMediaChunk chunk =\n        createChunk(\n            format, /* length= */ C.LENGTH_UNSET, /* startTimeSec= */ 0, /* endTimeSec= */ 10);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastInfo(\n            Collections.singletonList(chunk),\n            new Format[] {format},\n            MAX_DURATION_US,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(Format.NO_VALUE);\n  }\n\n  @Test\n  public void getBitratesUsingPastInfo_oneChunkWithSameFormat_returnsBitrates() {\n    Format format = createFormatWithBitrate(10);\n    FakeMediaChunk chunk =\n        createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 10);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastInfo(\n            Collections.singletonList(chunk),\n            new Format[] {format},\n            MAX_DURATION_US,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(8).inOrder();\n  }\n\n  @Test\n  public void getBitratesUsingPastInfo_zeroMaxDuration_returnsNoValue() {\n    Format format = createFormatWithBitrate(10);\n    FakeMediaChunk chunk =\n        createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 10);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastInfo(\n            Collections.singletonList(chunk),\n            new Format[] {format},\n            /* maxDurationUs= */ 0,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(Format.NO_VALUE).inOrder();\n  }\n\n  @Test\n  public void getBitratesUsingPastInfo_multipleChunkWithSameFormat_returnsAverageBitrate() {\n    Format format = createFormatWithBitrate(10);\n    FakeMediaChunk chunk =\n        createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 10);\n    FakeMediaChunk chunk2 =\n        createChunk(format, /* length= */ 20, /* startTimeSec= */ 10, /* endTimeSec= */ 20);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastInfo(\n            Arrays.asList(chunk, chunk2),\n            new Format[] {format},\n            MAX_DURATION_US,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(12).inOrder();\n  }\n\n  @Test\n  public void getBitratesUsingPastInfo_oneChunkWithDifferentFormat_returnsEstimationBitrate() {\n    FakeMediaChunk chunk =\n        createChunk(\n            createFormatWithBitrate(10),\n            /* length= */ 10,\n            /* startTimeSec= */ 0,\n            /* endTimeSec= */ 10);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastInfo(\n            Collections.singletonList(chunk),\n            new Format[] {createFormatWithBitrate(20)},\n            MAX_DURATION_US,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(16).inOrder();\n  }\n\n  @Test\n  public void getBitratesUsingPastInfo_trackFormatNoBitrate_returnsNoValue() {\n    FakeMediaChunk chunk =\n        createChunk(\n            createFormatWithBitrate(10),\n            /* length= */ 10,\n            /* startTimeSec= */ 0,\n            /* endTimeSec= */ 10);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastInfo(\n            Collections.singletonList(chunk),\n            new Format[] {createFormatWithBitrate(Format.NO_VALUE)},\n            MAX_DURATION_US,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(Format.NO_VALUE);\n  }\n\n  @Test\n  public void getBitratesUsingPastInfo_multipleTracks_returnsBitrates() {\n    FakeMediaChunk chunk =\n        createChunk(\n            createFormatWithBitrate(10),\n            /* length= */ 10,\n            /* startTimeSec= */ 0,\n            /* endTimeSec= */ 10);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastInfo(\n            Collections.singletonList(chunk),\n            new Format[] {createFormatWithBitrate(20), createFormatWithBitrate(30)},\n            MAX_DURATION_US,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(16, 24).inOrder();\n  }\n\n  @Test\n  public void getBitratesUsingPastInfo_bitratesArrayGiven_returnsTheSameArray() {\n    FakeMediaChunk chunk =\n        createChunk(\n            createFormatWithBitrate(10),\n            /* length= */ 10,\n            /* startTimeSec= */ 0,\n            /* endTimeSec= */ 10);\n\n    int[] bitratesArrayToUse = new int[2];\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastInfo(\n            Collections.singletonList(chunk),\n            new Format[] {createFormatWithBitrate(20), createFormatWithBitrate(30)},\n            MAX_DURATION_US,\n            bitratesArrayToUse);\n\n    assertThat(bitrates).isSameAs(bitratesArrayToUse);\n  }\n\n  @Test\n  public void\n      getBitratesUsingPastInfo_multipleChunkExceedingMaxDuration_returnsAverageUntilMaxDuration() {\n    Format format = createFormatWithBitrate(10);\n    FakeMediaChunk chunk =\n        createChunk(format, /* length= */ 10, /* startTimeSec= */ 0, /* endTimeSec= */ 20);\n    FakeMediaChunk chunk2 =\n        createChunk(format, /* length= */ 40, /* startTimeSec= */ 20, /* endTimeSec= */ 40);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastInfo(\n            Arrays.asList(chunk, chunk2),\n            new Format[] {format},\n            /* maxDurationUs= */ 30 * C.MICROS_PER_SECOND,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(12).inOrder();\n  }\n\n  @Test\n  public void\n      getBitratesUsingPastInfo_chunksWithDifferentFormats_returnsChunkAverageBitrateForLastFormat() {\n    FakeMediaChunk chunk =\n        createChunk(\n            createFormatWithBitrate(10),\n            /* length= */ 10,\n            /* startTimeSec= */ 0,\n            /* endTimeSec= */ 10);\n    FakeMediaChunk chunk2 =\n        createChunk(\n            createFormatWithBitrate(20),\n            /* length= */ 40,\n            /* startTimeSec= */ 10,\n            /* endTimeSec= */ 20);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastInfo(\n            Arrays.asList(chunk, chunk2),\n            new Format[] {createFormatWithBitrate(10)},\n            MAX_DURATION_US,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(16).inOrder();\n  }\n\n  @Test\n  public void getBitratesUsingPastAndFutureInfo_noPastInfo_returnsBitratesUsingOnlyFutureInfo() {\n    FakeMediaChunkIterator iterator1 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10});\n    FakeMediaChunkIterator iterator2 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30},\n            /* chunkLengths= */ new long[] {10, 20, 30});\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastAndFutureInfo(\n            new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)},\n            Collections.emptyList(),\n            MAX_DURATION_US,\n            new MediaChunkIterator[] {iterator1, iterator2},\n            MAX_DURATION_US,\n            /* useFormatBitrateAsLowerBound= */ false,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(8, 16).inOrder();\n  }\n\n  @Test\n  public void getBitratesUsingPastAndFutureInfo_noFutureInfo_returnsBitratesUsingOnlyPastInfo() {\n    FakeMediaChunk chunk =\n        createChunk(\n            createFormatWithBitrate(10),\n            /* length= */ 10,\n            /* startTimeSec= */ 0,\n            /* endTimeSec= */ 10);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastAndFutureInfo(\n            new Format[] {createFormatWithBitrate(20), createFormatWithBitrate(30)},\n            Collections.singletonList(chunk),\n            MAX_DURATION_US,\n            new MediaChunkIterator[] {MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY},\n            MAX_DURATION_US,\n            /* useFormatBitrateAsLowerBound= */ false,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(16, 24).inOrder();\n  }\n\n  @Test\n  public void\n      getBitratesUsingPastAndFutureInfo_pastAndFutureInfo_returnsBitratesUsingOnlyFutureInfo() {\n    FakeMediaChunk chunk =\n        createChunk(\n            createFormatWithBitrate(5),\n            /* length= */ 10,\n            /* startTimeSec= */ 0,\n            /* endTimeSec= */ 10);\n    FakeMediaChunkIterator iterator1 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 10}, /* chunkLengths= */ new long[] {10});\n    FakeMediaChunkIterator iterator2 =\n        new FakeMediaChunkIterator(\n            /* chunkTimeBoundariesSec= */ new long[] {0, 5, 15, 30},\n            /* chunkLengths= */ new long[] {10, 20, 30});\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastAndFutureInfo(\n            new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)},\n            Collections.singletonList(chunk),\n            MAX_DURATION_US,\n            new MediaChunkIterator[] {iterator1, iterator2},\n            MAX_DURATION_US,\n            /* useFormatBitrateAsLowerBound= */ false,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(8, 16).inOrder();\n  }\n\n  @Test\n  public void getBitratesUsingPastAndFutureInfo_noPastAndFutureInfo_returnsBitratesOfFormats() {\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastAndFutureInfo(\n            new Format[] {createFormatWithBitrate(10), createFormatWithBitrate(20)},\n            Collections.emptyList(),\n            MAX_DURATION_US,\n            new MediaChunkIterator[] {MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY},\n            MAX_DURATION_US,\n            /* useFormatBitrateAsLowerBound= */ false,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(10, 20).inOrder();\n  }\n\n  @Test\n  public void\n      getBitratesUsingPastAndFutureInfo_estimatesLowerAndUseFormatBitrateAsLowerBoundTrue_returnsBitratesOfFormats() {\n    FakeMediaChunk chunk =\n        createChunk(\n            createFormatWithBitrate(10),\n            /* length= */ 10,\n            /* startTimeSec= */ 0,\n            /* endTimeSec= */ 10);\n\n    int[] bitrates =\n        TrackSelectionUtil.getBitratesUsingPastAndFutureInfo(\n            new Format[] {createFormatWithBitrate(20), createFormatWithBitrate(30)},\n            Collections.singletonList(chunk),\n            MAX_DURATION_US,\n            new MediaChunkIterator[] {MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY},\n            MAX_DURATION_US,\n            /* useFormatBitrateAsLowerBound= */ true,\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(20, 30).inOrder();\n  }\n\n  private static FakeMediaChunk createChunk(\n      Format format, int length, int startTimeSec, int endTimeSec) {\n    DataSpec dataSpec =\n        new DataSpec(\n            Uri.EMPTY, /* absoluteStreamPosition= */ 0, length, /* key= */ null, /* flags= */ 0);\n    return new FakeMediaChunk(\n        dataSpec, format, startTimeSec * C.MICROS_PER_SECOND, endTimeSec * C.MICROS_PER_SECOND);\n  }\n\n  private static Format createFormatWithBitrate(int bitrate) {\n    return Format.createSampleFormat(\n        /* id= */ null,\n        /* sampleMimeType= */ null,\n        /* codecs= */ null,\n        bitrate,\n        /* drmInitData= */ null);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectorTest.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 static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.RendererCapabilities;\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.trackselection.TrackSelector.InvalidationListener;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mockito;\n\n/** Unit test for {@link TrackSelector}. */\n@RunWith(AndroidJUnit4.class)\npublic class TrackSelectorTest {\n\n  private TrackSelector trackSelector;\n\n  @Before\n  public void setUp() {\n    trackSelector =\n        new TrackSelector() {\n          @Override\n          public TrackSelectorResult selectTracks(\n              RendererCapabilities[] rendererCapabilities,\n              TrackGroupArray trackGroups,\n              MediaPeriodId periodId,\n              Timeline timeline)\n              throws ExoPlaybackException {\n            throw new UnsupportedOperationException();\n          }\n\n          @Override\n          public void onSelectionActivated(Object info) {}\n        };\n  }\n\n  @Test\n  public void getBandwidthMeter_beforeInitialization_throwsException() {\n    try {\n      trackSelector.getBandwidthMeter();\n      fail();\n    } catch (Exception e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void getBandwidthMeter_afterInitialization_returnsProvidedBandwidthMeter() {\n    InvalidationListener invalidationListener = Mockito.mock(InvalidationListener.class);\n    BandwidthMeter bandwidthMeter = Mockito.mock(BandwidthMeter.class);\n    trackSelector.init(invalidationListener, bandwidthMeter);\n\n    assertThat(trackSelector.getBandwidthMeter()).isEqualTo(bandwidthMeter);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimatorTest.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.trackselection;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport com.google.android.exoplayer2.testutil.FakeMediaChunk;\nimport com.google.android.exoplayer2.testutil.FakeMediaChunkIterator;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport java.util.Collections;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** {@link WindowedTrackBitrateEstimator} tests. */\n@RunWith(AndroidJUnit4.class)\npublic class WindowedTrackBitrateEstimatorTest {\n\n  private static final long MAX_DURATION_MS = 30_000;\n\n  @Test\n  public void getBitrates_zeroMaxDuration_returnsFormatBitrates() {\n    WindowedTrackBitrateEstimator estimator =\n        new WindowedTrackBitrateEstimator(\n            /* maxPastDurationMs= */ 0,\n            /* maxFutureDurationMs= */ 0,\n            /* useFormatBitrateAsLowerBound= */ false);\n    MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10);\n    MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(8);\n    MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16);\n    Format format1 = createFormatWithBitrate(10);\n    Format format2 = createFormatWithBitrate(20);\n\n    int[] bitrates =\n        estimator.getBitrates(\n            new Format[] {format1, format2},\n            Collections.singletonList(chunk),\n            new MediaChunkIterator[] {iterator1, iterator2},\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(10, 20).inOrder();\n  }\n\n  @Test\n  public void getBitrates_futureMaxDurationSet_returnsEstimateUsingFutureChunks() {\n    WindowedTrackBitrateEstimator estimator =\n        new WindowedTrackBitrateEstimator(\n            /* maxPastDurationMs= */ 0, MAX_DURATION_MS, /* useFormatBitrateAsLowerBound= */ false);\n    MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10);\n    MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(8);\n    MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16);\n    Format format1 = createFormatWithBitrate(10);\n    Format format2 = createFormatWithBitrate(20);\n\n    int[] bitrates =\n        estimator.getBitrates(\n            new Format[] {format1, format2},\n            Collections.singletonList(chunk),\n            new MediaChunkIterator[] {iterator1, iterator2},\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(8, 16).inOrder();\n  }\n\n  @Test\n  public void getBitrates_pastMaxDurationSet_returnsEstimateUsingPastChunks() {\n    WindowedTrackBitrateEstimator estimator =\n        new WindowedTrackBitrateEstimator(\n            MAX_DURATION_MS,\n            /* maxFutureDurationMs= */ 0,\n            /* useFormatBitrateAsLowerBound= */ false);\n    MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10);\n    MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(8);\n    MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16);\n    Format format1 = createFormatWithBitrate(10);\n    Format format2 = createFormatWithBitrate(20);\n\n    int[] bitrates =\n        estimator.getBitrates(\n            new Format[] {format1, format2},\n            Collections.singletonList(chunk),\n            new MediaChunkIterator[] {iterator1, iterator2},\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(16, 32).inOrder();\n  }\n\n  @Test\n  public void\n      getBitrates_useFormatBitrateAsLowerBoundSetTrue_returnsEstimateIfOnlyHigherThanFormat() {\n    WindowedTrackBitrateEstimator estimator =\n        new WindowedTrackBitrateEstimator(\n            MAX_DURATION_MS, MAX_DURATION_MS, /* useFormatBitrateAsLowerBound= */ true);\n    MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10);\n    MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(80);\n    MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16);\n    Format format1 = createFormatWithBitrate(10);\n    Format format2 = createFormatWithBitrate(20);\n\n    int[] bitrates =\n        estimator.getBitrates(\n            new Format[] {format1, format2},\n            Collections.singletonList(chunk),\n            new MediaChunkIterator[] {iterator1, iterator2},\n            /* bitrates= */ null);\n\n    assertThat(bitrates).asList().containsExactly(80, 20).inOrder();\n  }\n\n  @Test\n  public void getBitrates_bitratesArrayGiven_returnsTheSameArray() {\n    WindowedTrackBitrateEstimator estimator =\n        new WindowedTrackBitrateEstimator(\n            MAX_DURATION_MS, MAX_DURATION_MS, /* useFormatBitrateAsLowerBound= */ true);\n    MediaChunk chunk = createMediaChunk(/* formatBitrate= */ 5, /* actualBitrate= */ 10);\n    MediaChunkIterator iterator1 = createMediaChunkIteratorWithBitrate(8);\n    MediaChunkIterator iterator2 = createMediaChunkIteratorWithBitrate(16);\n    Format format1 = createFormatWithBitrate(10);\n    Format format2 = createFormatWithBitrate(20);\n\n    int[] bitratesArrayToUse = new int[2];\n    int[] bitrates =\n        estimator.getBitrates(\n            new Format[] {format1, format2},\n            Collections.singletonList(chunk),\n            new MediaChunkIterator[] {iterator1, iterator2},\n            bitratesArrayToUse);\n\n    assertThat(bitrates).isSameAs(bitratesArrayToUse);\n  }\n\n  private static MediaChunk createMediaChunk(int formatBitrate, int actualBitrate) {\n    int length = actualBitrate / C.BITS_PER_BYTE;\n    DataSpec dataSpec =\n        new DataSpec(\n            Uri.EMPTY, /* absoluteStreamPosition= */ 0, length, /* key= */ null, /* flags= */ 0);\n    Format format = createFormatWithBitrate(formatBitrate);\n    return new FakeMediaChunk(\n        dataSpec, format, /* startTimeUs= */ 0L, /* endTimeUs= */ C.MICROS_PER_SECOND);\n  }\n\n  private static Format createFormatWithBitrate(int bitrate) {\n    return Format.createSampleFormat(\n        /* id= */ null,\n        /* sampleMimeType= */ null,\n        /* codecs= */ null,\n        bitrate,\n        /* drmInitData= */ null);\n  }\n\n  private static MediaChunkIterator createMediaChunkIteratorWithBitrate(int bitrate) {\n    return new FakeMediaChunkIterator(\n        /* chunkTimeBoundariesSec= */ new long[] {0, 1},\n        /* chunkLengths= */ new long[] {bitrate / C.BITS_PER_BYTE});\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.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.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link AssetDataSource}. */\n@RunWith(AndroidJUnit4.class)\npublic final class AssetDataSourceTest {\n\n  private static final String DATA_PATH = \"binary/1024_incrementing_bytes.mp3\";\n\n  @Test\n  public void testReadFileUri() throws Exception {\n    AssetDataSource dataSource = new AssetDataSource(ApplicationProvider.getApplicationContext());\n    DataSpec dataSpec = new DataSpec(Uri.parse(\"file:///android_asset/\" + DATA_PATH));\n    TestUtil.assertDataSourceContent(\n        dataSource,\n        dataSpec,\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), DATA_PATH),\n        true);\n  }\n\n  @Test\n  public void testReadAssetUri() throws Exception {\n    AssetDataSource dataSource = new AssetDataSource(ApplicationProvider.getApplicationContext());\n    DataSpec dataSpec = new DataSpec(Uri.parse(\"asset:///\" + DATA_PATH));\n    TestUtil.assertDataSourceContent(\n        dataSource,\n        dataSpec,\n        TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), DATA_PATH),\n        true);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.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.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport java.io.IOException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link BaseDataSource}. */\n@RunWith(AndroidJUnit4.class)\npublic class BaseDataSourceTest {\n\n  @Test\n  public void dataTransfer_withLocalSource_isReported() throws IOException {\n    TestSource testSource = new TestSource(/* isNetwork= */ false);\n    TestTransferListener transferListener = new TestTransferListener();\n    testSource.addTransferListener(transferListener);\n\n    DataSpec dataSpec = new DataSpec(Uri.EMPTY);\n    testSource.open(dataSpec);\n    testSource.read(/* buffer= */ null, /* offset= */ 0, /* readLength= */ 100);\n    testSource.close();\n\n    assertThat(transferListener.lastTransferInitializingSource).isSameAs(testSource);\n    assertThat(transferListener.lastTransferStartSource).isSameAs(testSource);\n    assertThat(transferListener.lastBytesTransferredSource).isSameAs(testSource);\n    assertThat(transferListener.lastTransferEndSource).isSameAs(testSource);\n\n    assertThat(transferListener.lastTransferInitializingDataSpec).isEqualTo(dataSpec);\n    assertThat(transferListener.lastTransferStartDataSpec).isEqualTo(dataSpec);\n    assertThat(transferListener.lastBytesTransferredDataSpec).isEqualTo(dataSpec);\n    assertThat(transferListener.lastTransferEndDataSpec).isEqualTo(dataSpec);\n\n    assertThat(transferListener.lastTransferInitializingIsNetwork).isEqualTo(false);\n    assertThat(transferListener.lastTransferStartIsNetwork).isEqualTo(false);\n    assertThat(transferListener.lastBytesTransferredIsNetwork).isEqualTo(false);\n    assertThat(transferListener.lastTransferEndIsNetwork).isEqualTo(false);\n\n    assertThat(transferListener.lastBytesTransferred).isEqualTo(100);\n  }\n\n  @Test\n  public void dataTransfer_withRemoteSource_isReported() throws IOException {\n    TestSource testSource = new TestSource(/* isNetwork= */ true);\n    TestTransferListener transferListener = new TestTransferListener();\n    testSource.addTransferListener(transferListener);\n\n    DataSpec dataSpec = new DataSpec(Uri.EMPTY);\n    testSource.open(dataSpec);\n    testSource.read(/* buffer= */ null, /* offset= */ 0, /* readLength= */ 100);\n    testSource.close();\n\n    assertThat(transferListener.lastTransferInitializingSource).isSameAs(testSource);\n    assertThat(transferListener.lastTransferStartSource).isSameAs(testSource);\n    assertThat(transferListener.lastBytesTransferredSource).isSameAs(testSource);\n    assertThat(transferListener.lastTransferEndSource).isSameAs(testSource);\n\n    assertThat(transferListener.lastTransferInitializingDataSpec).isEqualTo(dataSpec);\n    assertThat(transferListener.lastTransferStartDataSpec).isEqualTo(dataSpec);\n    assertThat(transferListener.lastBytesTransferredDataSpec).isEqualTo(dataSpec);\n    assertThat(transferListener.lastTransferEndDataSpec).isEqualTo(dataSpec);\n\n    assertThat(transferListener.lastTransferInitializingIsNetwork).isEqualTo(true);\n    assertThat(transferListener.lastTransferStartIsNetwork).isEqualTo(true);\n    assertThat(transferListener.lastBytesTransferredIsNetwork).isEqualTo(true);\n    assertThat(transferListener.lastTransferEndIsNetwork).isEqualTo(true);\n\n    assertThat(transferListener.lastBytesTransferred).isEqualTo(100);\n  }\n\n  private static final class TestSource extends BaseDataSource {\n\n    public TestSource(boolean isNetwork) {\n      super(isNetwork);\n    }\n\n    @Override\n    public long open(DataSpec dataSpec) throws IOException {\n      transferInitializing(dataSpec);\n      transferStarted(dataSpec);\n      return C.LENGTH_UNSET;\n    }\n\n    @Override\n    public int read(byte[] buffer, int offset, int readLength) throws IOException {\n      bytesTransferred(readLength);\n      return readLength;\n    }\n\n    @Override\n    public @Nullable Uri getUri() {\n      throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public void close() throws IOException {\n      transferEnded();\n    }\n  }\n\n  private static final class TestTransferListener implements TransferListener {\n\n    public Object lastTransferInitializingSource;\n    public DataSpec lastTransferInitializingDataSpec;\n    public boolean lastTransferInitializingIsNetwork;\n\n    public Object lastTransferStartSource;\n    public DataSpec lastTransferStartDataSpec;\n    public boolean lastTransferStartIsNetwork;\n\n    public Object lastBytesTransferredSource;\n    public DataSpec lastBytesTransferredDataSpec;\n    public boolean lastBytesTransferredIsNetwork;\n    public int lastBytesTransferred;\n\n    public Object lastTransferEndSource;\n    public DataSpec lastTransferEndDataSpec;\n    public boolean lastTransferEndIsNetwork;\n\n    @Override\n    public void onTransferInitializing(DataSource source, DataSpec dataSpec, boolean isNetwork) {\n      lastTransferInitializingSource = source;\n      lastTransferInitializingDataSpec = dataSpec;\n      lastTransferInitializingIsNetwork = isNetwork;\n    }\n\n    @Override\n    public void onTransferStart(DataSource source, DataSpec dataSpec, boolean isNetwork) {\n      lastTransferStartSource = source;\n      lastTransferStartDataSpec = dataSpec;\n      lastTransferStartIsNetwork = isNetwork;\n    }\n\n    @Override\n    public void onBytesTransferred(\n        DataSource source, DataSpec dataSpec, boolean isNetwork, int bytesTransferred) {\n      lastBytesTransferredSource = source;\n      lastBytesTransferredDataSpec = dataSpec;\n      lastBytesTransferredIsNetwork = isNetwork;\n      lastBytesTransferred = bytesTransferred;\n    }\n\n    @Override\n    public void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) {\n      lastTransferEndSource = source;\n      lastTransferEndDataSpec = dataSpec;\n      lastTransferEndIsNetwork = isNetwork;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport java.io.IOException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link ByteArrayDataSource}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ByteArrayDataSourceTest {\n\n  private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};\n  private static final byte[] TEST_DATA_ODD = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};\n\n  @Test\n  public void testFullReadSingleBytes() {\n    readTestData(TEST_DATA, 0, C.LENGTH_UNSET, 1, 0, 1, false);\n  }\n\n  @Test\n  public void testFullReadAllBytes() {\n    readTestData(TEST_DATA, 0, C.LENGTH_UNSET, 100, 0, 100, false);\n  }\n\n  @Test\n  public void testLimitReadSingleBytes() {\n    // Limit set to the length of the data.\n    readTestData(TEST_DATA, 0, TEST_DATA.length, 1, 0, 1, false);\n    // And less.\n    readTestData(TEST_DATA, 0, 6, 1, 0, 1, false);\n  }\n\n  @Test\n  public void testFullReadTwoBytes() {\n    // Try with the total data length an exact multiple of the size of each individual read.\n    readTestData(TEST_DATA, 0, C.LENGTH_UNSET, 2, 0, 2, false);\n    // And not.\n    readTestData(TEST_DATA_ODD, 0, C.LENGTH_UNSET, 2, 0, 2, false);\n  }\n\n  @Test\n  public void testLimitReadTwoBytes() {\n    // Try with the limit an exact multiple of the size of each individual read.\n    readTestData(TEST_DATA, 0, 6, 2, 0, 2, false);\n    // And not.\n    readTestData(TEST_DATA, 0, 7, 2, 0, 2, false);\n  }\n\n  @Test\n  public void testReadFromValidOffsets() {\n    // Read from an offset without bound.\n    readTestData(TEST_DATA, 1, C.LENGTH_UNSET, 1, 0, 1, false);\n    // And with bound.\n    readTestData(TEST_DATA, 1, 6, 1, 0, 1, false);\n    // Read from the last possible offset without bound.\n    readTestData(TEST_DATA, TEST_DATA.length - 1, C.LENGTH_UNSET, 1, 0, 1, false);\n    // And with bound.\n    readTestData(TEST_DATA, TEST_DATA.length - 1, 1, 1, 0, 1, false);\n  }\n\n  @Test\n  public void testReadFromInvalidOffsets() {\n    // Read from first invalid offset and check failure without bound.\n    readTestData(TEST_DATA, TEST_DATA.length, C.LENGTH_UNSET, 1, 0, 1, true);\n    // And with bound.\n    readTestData(TEST_DATA, TEST_DATA.length, 1, 1, 0, 1, true);\n  }\n\n  @Test\n  public void testReadWithInvalidLength() {\n    // Read more data than is available.\n    readTestData(TEST_DATA, 0, TEST_DATA.length + 1, 1, 0, 1, true);\n    // And with bound.\n    readTestData(TEST_DATA, 1, TEST_DATA.length, 1, 0, 1, true);\n  }\n\n  /**\n   * Tests reading from a {@link ByteArrayDataSource} with various parameters.\n   *\n   * @param testData The data that the {@link ByteArrayDataSource} will wrap.\n   * @param dataOffset The offset from which to read data.\n   * @param dataLength The total length of data to read.\n   * @param outputBufferLength The length of the target buffer for each read.\n   * @param writeOffset The offset into {@code outputBufferLength} for each read.\n   * @param maxReadLength The maximum length of each read.\n   * @param expectFailOnOpen Whether it is expected that opening the source will fail.\n   */\n  private void readTestData(byte[] testData, int dataOffset, int dataLength, int outputBufferLength,\n      int writeOffset, int maxReadLength, boolean expectFailOnOpen) {\n    int expectedFinalBytesRead =\n        dataLength == C.LENGTH_UNSET ? (testData.length - dataOffset) : dataLength;\n    ByteArrayDataSource dataSource = new ByteArrayDataSource(testData);\n    boolean opened = false;\n    try {\n      // Open the source.\n      long length = dataSource.open(new DataSpec(null, dataOffset, dataLength, null));\n      opened = true;\n      assertThat(expectFailOnOpen).isFalse();\n\n      // Verify the resolved length is as we expect.\n      assertThat(length).isEqualTo(expectedFinalBytesRead);\n\n      byte[] outputBuffer = new byte[outputBufferLength];\n      int accumulatedBytesRead = 0;\n      while (true) {\n        // Calculate a valid length for the next read, constraining by the specified output buffer\n        // length, write offset and maximum write length input parameters.\n        int requestedReadLength = Math.min(maxReadLength, outputBufferLength - writeOffset);\n        assertThat(requestedReadLength).isGreaterThan(0);\n\n        int bytesRead = dataSource.read(outputBuffer, writeOffset, requestedReadLength);\n        if (bytesRead != C.RESULT_END_OF_INPUT) {\n          assertThat(bytesRead).isGreaterThan(0);\n          assertThat(bytesRead).isAtMost(requestedReadLength);\n          // Check the data read was correct.\n          for (int i = 0; i < bytesRead; i++) {\n            assertThat(outputBuffer[writeOffset + i])\n                .isEqualTo(testData[dataOffset + accumulatedBytesRead + i]);\n          }\n          // Check that we haven't read more data than we were expecting.\n          accumulatedBytesRead += bytesRead;\n          assertThat(accumulatedBytesRead).isAtMost(expectedFinalBytesRead);\n          // If we haven't read all of the bytes the request should have been satisfied in full.\n          assertThat(accumulatedBytesRead == expectedFinalBytesRead\n              || bytesRead == requestedReadLength).isTrue();\n        } else {\n          // We're done. Check we read the expected number of bytes.\n          assertThat(accumulatedBytesRead).isEqualTo(expectedFinalBytesRead);\n          return;\n        }\n      }\n    } catch (IOException e) {\n      if (expectFailOnOpen && !opened) {\n        // Expected.\n        return;\n      }\n      // Unexpected failure.\n      fail();\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.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.C.RESULT_END_OF_INPUT;\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link DataSchemeDataSource}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DataSchemeDataSourceTest {\n\n  private static final String DATA_SCHEME_URI =\n      \"data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiLCJjb250ZW50X2lkIjoiTWpBeE5WOTBaV\"\n          + \"0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiXX0=\";\n  private DataSource schemeDataDataSource;\n\n  @Before\n  public void setUp() {\n    schemeDataDataSource = new DataSchemeDataSource();\n  }\n\n  @Test\n  public void testBase64Data() throws IOException {\n    DataSpec dataSpec = buildDataSpec(DATA_SCHEME_URI);\n    DataSourceAsserts.assertDataSourceContent(\n        schemeDataDataSource,\n        dataSpec,\n        Util.getUtf8Bytes(\n            \"{\\\"provider\\\":\\\"widevine_test\\\",\\\"content_id\\\":\\\"MjAxNV90ZWFycw==\\\",\\\"key_ids\\\":\"\n                + \"[\\\"00000000000000000000000000000000\\\"]}\"));\n  }\n\n  @Test\n  public void testAsciiData() throws IOException {\n    DataSourceAsserts.assertDataSourceContent(\n        schemeDataDataSource,\n        buildDataSpec(\"data:,A%20brief%20note\"),\n        Util.getUtf8Bytes(\"A brief note\"));\n  }\n\n  @Test\n  public void testPartialReads() throws IOException {\n    byte[] buffer = new byte[18];\n    DataSpec dataSpec = buildDataSpec(\"data:,012345678901234567\");\n    assertThat(schemeDataDataSource.open(dataSpec)).isEqualTo(18);\n    assertThat(schemeDataDataSource.read(buffer, 0, 9)).isEqualTo(9);\n    assertThat(schemeDataDataSource.read(buffer, 3, 0)).isEqualTo(0);\n    assertThat(schemeDataDataSource.read(buffer, 9, 15)).isEqualTo(9);\n    assertThat(schemeDataDataSource.read(buffer, 1, 0)).isEqualTo(0);\n    assertThat(schemeDataDataSource.read(buffer, 1, 1)).isEqualTo(RESULT_END_OF_INPUT);\n    assertThat(Util.fromUtf8Bytes(buffer, 0, 18)).isEqualTo(\"012345678901234567\");\n  }\n\n  @Test\n  public void testSequentialRangeRequests() throws IOException {\n    DataSpec dataSpec =\n        buildDataSpec(DATA_SCHEME_URI, /* position= */ 1, /* length= */ C.LENGTH_UNSET);\n    DataSourceAsserts.assertDataSourceContent(\n        schemeDataDataSource,\n        dataSpec,\n        Util.getUtf8Bytes(\n            \"\\\"provider\\\":\\\"widevine_test\\\",\\\"content_id\\\":\\\"MjAxNV90ZWFycw==\\\",\\\"key_ids\\\":\"\n                + \"[\\\"00000000000000000000000000000000\\\"]}\"));\n    dataSpec = buildDataSpec(DATA_SCHEME_URI, /* position= */ 10, /* length= */ C.LENGTH_UNSET);\n    DataSourceAsserts.assertDataSourceContent(\n        schemeDataDataSource,\n        dataSpec,\n        Util.getUtf8Bytes(\n            \"\\\":\\\"widevine_test\\\",\\\"content_id\\\":\\\"MjAxNV90ZWFycw==\\\",\\\"key_ids\\\":\"\n                + \"[\\\"00000000000000000000000000000000\\\"]}\"));\n    dataSpec = buildDataSpec(DATA_SCHEME_URI, /* position= */ 15, /* length= */ 5);\n    DataSourceAsserts.assertDataSourceContent(\n        schemeDataDataSource, dataSpec, Util.getUtf8Bytes(\"devin\"));\n  }\n\n  @Test\n  public void testInvalidStartPositionRequest() throws IOException {\n    try {\n      // Try to open a range starting one byte beyond the resource's length.\n      schemeDataDataSource.open(\n          buildDataSpec(DATA_SCHEME_URI, /* position= */ 108, /* length= */ C.LENGTH_UNSET));\n      fail();\n    } catch (DataSourceException e) {\n      assertThat(e.reason).isEqualTo(DataSourceException.POSITION_OUT_OF_RANGE);\n    }\n  }\n\n  @Test\n  public void testRangeExceedingResourceLengthRequest() throws IOException {\n    try {\n      // Try to open a range exceeding the resource's length.\n      schemeDataDataSource.open(\n          buildDataSpec(DATA_SCHEME_URI, /* position= */ 97, /* length= */ 11));\n      fail();\n    } catch (DataSourceException e) {\n      assertThat(e.reason).isEqualTo(DataSourceException.POSITION_OUT_OF_RANGE);\n    }\n  }\n\n  @Test\n  public void testIncorrectScheme() {\n    try {\n      schemeDataDataSource.open(buildDataSpec(\"http://www.google.com\"));\n      fail();\n    } catch (IOException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testMalformedData() {\n    try {\n      schemeDataDataSource.open(buildDataSpec(\"data:text/plain;base64,,This%20is%20Content\"));\n      fail();\n    } catch (IOException e) {\n      // Expected.\n    }\n    try {\n      schemeDataDataSource.open(buildDataSpec(\"data:text/plain;base64,IncorrectPadding==\"));\n      fail();\n    } catch (IOException e) {\n      // Expected.\n    }\n  }\n\n  private static DataSpec buildDataSpec(String uriString) {\n    return buildDataSpec(uriString, /* position= */ 0, /* length= */ C.LENGTH_UNSET);\n  }\n\n  private static DataSpec buildDataSpec(String uriString, int position, int length) {\n    return new DataSpec(Uri.parse(uriString), position, length, /* key= */ null);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceAsserts.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.common.truth.Truth.assertThat;\n\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.io.IOException;\n\n/**\n * Assertions for data source tests.\n */\n/* package */ final class DataSourceAsserts {\n\n  /**\n   * Asserts that data read from a {@link DataSource} matches {@code expected}.\n   *\n   * @param dataSource The {@link DataSource} through which to read.\n   * @param dataSpec The {@link DataSpec} to use when opening the {@link DataSource}.\n   * @param expectedData The expected data.\n   * @throws IOException If an error occurs reading fom the {@link DataSource}.\n   */\n  public static void assertDataSourceContent(DataSource dataSource, DataSpec dataSpec,\n      byte[] expectedData) throws IOException {\n    try {\n      long length = dataSource.open(dataSpec);\n      assertThat(length).isEqualTo(expectedData.length);\n      byte[] readData = TestUtil.readToEnd(dataSource);\n      assertThat(readData).isEqualTo(expectedData);\n    } finally {\n      dataSource.close();\n    }\n  }\n\n  private DataSourceAsserts() {}\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.FakeDataSource;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link DataSourceInputStream}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DataSourceInputStreamTest {\n\n  private static final byte[] TEST_DATA = TestUtil.buildTestData(16);\n\n  @Test\n  public void testReadSingleBytes() throws IOException {\n    DataSourceInputStream inputStream = buildTestInputStream();\n    // No bytes read yet.\n    assertThat(inputStream.bytesRead()).isEqualTo(0);\n    // Read bytes.\n    for (int i = 0; i < TEST_DATA.length; i++) {\n      int readByte = inputStream.read();\n      assertThat(0 <= readByte && readByte < 256).isTrue();\n      assertThat(readByte).isEqualTo(TEST_DATA[i] & 0xFF);\n      assertThat(inputStream.bytesRead()).isEqualTo(i + 1);\n    }\n    // Check end of stream.\n    assertThat(inputStream.read()).isEqualTo(-1);\n    assertThat(inputStream.bytesRead()).isEqualTo(TEST_DATA.length);\n    // Check close succeeds.\n    inputStream.close();\n  }\n\n  @Test\n  public void testRead() throws IOException {\n    DataSourceInputStream inputStream = buildTestInputStream();\n    // Read bytes.\n    byte[] readBytes = new byte[TEST_DATA.length];\n    int totalBytesRead = 0;\n    while (totalBytesRead < TEST_DATA.length) {\n      int bytesRead = inputStream.read(readBytes, totalBytesRead,\n          TEST_DATA.length - totalBytesRead);\n      assertThat(bytesRead).isGreaterThan(0);\n      totalBytesRead += bytesRead;\n      assertThat(inputStream.bytesRead()).isEqualTo(totalBytesRead);\n    }\n    // Check the read data.\n    assertThat(readBytes).isEqualTo(TEST_DATA);\n    // Check end of stream.\n    assertThat(inputStream.bytesRead()).isEqualTo(TEST_DATA.length);\n    assertThat(totalBytesRead).isEqualTo(TEST_DATA.length);\n    assertThat(inputStream.read()).isEqualTo(-1);\n    // Check close succeeds.\n    inputStream.close();\n  }\n\n  @Test\n  public void testSkip() throws IOException {\n    DataSourceInputStream inputStream = buildTestInputStream();\n    // Skip bytes.\n    long totalBytesSkipped = 0;\n    while (totalBytesSkipped < TEST_DATA.length) {\n      long bytesSkipped = inputStream.skip(Long.MAX_VALUE);\n      assertThat(bytesSkipped > 0).isTrue();\n      totalBytesSkipped += bytesSkipped;\n      assertThat(inputStream.bytesRead()).isEqualTo(totalBytesSkipped);\n    }\n    // Check end of stream.\n    assertThat(inputStream.bytesRead()).isEqualTo(TEST_DATA.length);\n    assertThat(totalBytesSkipped).isEqualTo(TEST_DATA.length);\n    assertThat(inputStream.read()).isEqualTo(-1);\n    // Check close succeeds.\n    inputStream.close();\n  }\n\n  private static DataSourceInputStream buildTestInputStream() {\n    FakeDataSource fakeDataSource = new FakeDataSource();\n    fakeDataSource.getDataSet().newDefaultData()\n        .appendReadData(Arrays.copyOfRange(TEST_DATA, 0, 5))\n        .appendReadData(Arrays.copyOfRange(TEST_DATA, 5, 10))\n        .appendReadData(Arrays.copyOfRange(TEST_DATA, 10, 15))\n        .appendReadData(Arrays.copyOfRange(TEST_DATA, 15, TEST_DATA.length));\n    return new DataSourceInputStream(fakeDataSource, new DataSpec(Uri.EMPTY));\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSpecTest.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.upstream;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static junit.framework.TestCase.fail;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport java.util.HashMap;\nimport java.util.Map;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link DataSpec}. */\n@RunWith(AndroidJUnit4.class)\npublic class DataSpecTest {\n\n  @Test\n  public void createDataSpec_withDefaultValues_setsEmptyHttpRequestParameters() {\n    Uri uri = Uri.parse(\"www.google.com\");\n    DataSpec dataSpec = new DataSpec(uri);\n\n    assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue();\n\n    dataSpec = new DataSpec(uri, /*flags= */ 0);\n    assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue();\n\n    dataSpec =\n        new DataSpec(\n            uri,\n            /* httpMethod= */ 0,\n            /* httpBody= */ new byte[] {0, 0, 0, 0},\n            /* absoluteStreamPosition= */ 0,\n            /* position= */ 0,\n            /* length= */ 1,\n            /* key= */ \"key\",\n            /* flags= */ 0);\n    assertThat(dataSpec.httpRequestHeaders.isEmpty()).isTrue();\n  }\n\n  @Test\n  public void createDataSpec_setsHttpRequestParameters() {\n    Map<String, String> httpRequestParameters = new HashMap<>();\n    httpRequestParameters.put(\"key1\", \"value1\");\n    httpRequestParameters.put(\"key2\", \"value2\");\n    httpRequestParameters.put(\"key3\", \"value3\");\n\n    DataSpec dataSpec =\n        new DataSpec(\n            Uri.parse(\"www.google.com\"),\n            /* httpMethod= */ 0,\n            /* httpBody= */ new byte[] {0, 0, 0, 0},\n            /* absoluteStreamPosition= */ 0,\n            /* position= */ 0,\n            /* length= */ 1,\n            /* key= */ \"key\",\n            /* flags= */ 0,\n            httpRequestParameters);\n\n    assertThat(dataSpec.httpRequestHeaders).isEqualTo(httpRequestParameters);\n  }\n\n  @Test\n  public void httpRequestParameters_areReadOnly() {\n    DataSpec dataSpec =\n        new DataSpec(\n            Uri.parse(\"www.google.com\"),\n            /* httpMethod= */ 0,\n            /* httpBody= */ new byte[] {0, 0, 0, 0},\n            /* absoluteStreamPosition= */ 0,\n            /* position= */ 0,\n            /* length= */ 1,\n            /* key= */ \"key\",\n            /* flags= */ 0,\n            /* httpRequestHeaders= */ new HashMap<>());\n\n    try {\n      dataSpec.httpRequestHeaders.put(\"key\", \"value\");\n      fail();\n    } catch (UnsupportedOperationException expected) {\n      // Expected\n    }\n  }\n\n  @Test\n  public void copyMethods_copiesHttpRequestHeaders() {\n    Map<String, String> httpRequestParameters = new HashMap<>();\n    httpRequestParameters.put(\"key1\", \"value1\");\n    httpRequestParameters.put(\"key2\", \"value2\");\n    httpRequestParameters.put(\"key3\", \"value3\");\n\n    DataSpec dataSpec =\n        new DataSpec(\n            Uri.parse(\"www.google.com\"),\n            /* httpMethod= */ 0,\n            /* httpBody= */ new byte[] {0, 0, 0, 0},\n            /* absoluteStreamPosition= */ 0,\n            /* position= */ 0,\n            /* length= */ 1,\n            /* key= */ \"key\",\n            /* flags= */ 0,\n            httpRequestParameters);\n\n    DataSpec dataSpecCopy = dataSpec.withUri(Uri.parse(\"www.new-uri.com\"));\n    assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters);\n\n    dataSpecCopy = dataSpec.subrange(2);\n    assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters);\n\n    dataSpecCopy = dataSpec.subrange(2, 2);\n    assertThat(dataSpecCopy.httpRequestHeaders).isEqualTo(httpRequestParameters);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.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.common.truth.Truth.assertThat;\n\nimport android.content.Context;\nimport android.net.ConnectivityManager;\nimport android.net.NetworkInfo;\nimport android.net.NetworkInfo.DetailedState;\nimport android.net.Uri;\nimport android.telephony.TelephonyManager;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.FakeClock;\nimport com.google.android.exoplayer2.testutil.FakeDataSource;\nimport java.util.Random;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.Shadows;\nimport org.robolectric.shadows.ShadowNetworkInfo;\n\n/** Unit test for {@link DefaultBandwidthMeter}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DefaultBandwidthMeterTest {\n\n  private static final int SIMULATED_TRANSFER_COUNT = 100;\n  private static final String FAST_COUNTRY_ISO = \"EE\";\n  private static final String SLOW_COUNTRY_ISO = \"PG\";\n\n  private TelephonyManager telephonyManager;\n  private ConnectivityManager connectivityManager;\n  private NetworkInfo networkInfoOffline;\n  private NetworkInfo networkInfoWifi;\n  private NetworkInfo networkInfo2g;\n  private NetworkInfo networkInfo3g;\n  private NetworkInfo networkInfo4g;\n  private NetworkInfo networkInfoEthernet;\n\n  @Before\n  public void setUp() {\n    connectivityManager =\n        (ConnectivityManager)\n            ApplicationProvider.getApplicationContext()\n                .getSystemService(Context.CONNECTIVITY_SERVICE);\n    telephonyManager =\n        (TelephonyManager)\n            ApplicationProvider.getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);\n    Shadows.shadowOf(telephonyManager).setNetworkCountryIso(FAST_COUNTRY_ISO);\n    networkInfoOffline =\n        ShadowNetworkInfo.newInstance(\n            DetailedState.DISCONNECTED,\n            ConnectivityManager.TYPE_WIFI,\n            /* subType= */ 0,\n            /* isAvailable= */ false,\n            /* isConnected= */ false);\n    networkInfoWifi =\n        ShadowNetworkInfo.newInstance(\n            DetailedState.CONNECTED,\n            ConnectivityManager.TYPE_WIFI,\n            /* subType= */ 0,\n            /* isAvailable= */ true,\n            /* isConnected= */ true);\n    networkInfo2g =\n        ShadowNetworkInfo.newInstance(\n            DetailedState.CONNECTED,\n            ConnectivityManager.TYPE_MOBILE,\n            TelephonyManager.NETWORK_TYPE_GPRS,\n            /* isAvailable= */ true,\n            /* isConnected= */ true);\n    networkInfo3g =\n        ShadowNetworkInfo.newInstance(\n            DetailedState.CONNECTED,\n            ConnectivityManager.TYPE_MOBILE,\n            TelephonyManager.NETWORK_TYPE_HSDPA,\n            /* isAvailable= */ true,\n            /* isConnected= */ true);\n    networkInfo4g =\n        ShadowNetworkInfo.newInstance(\n            DetailedState.CONNECTED,\n            ConnectivityManager.TYPE_MOBILE,\n            TelephonyManager.NETWORK_TYPE_LTE,\n            /* isAvailable= */ true,\n            /* isConnected= */ true);\n    networkInfoEthernet =\n        ShadowNetworkInfo.newInstance(\n            DetailedState.CONNECTED,\n            ConnectivityManager.TYPE_ETHERNET,\n            /* subType= */ 0,\n            /* isAvailable= */ true,\n            /* isConnected= */ true);\n  }\n  \n  @Test\n  public void defaultInitialBitrateEstimate_forWifi_isGreaterThanEstimateFor2G() {\n    setActiveNetworkInfo(networkInfoWifi);\n    DefaultBandwidthMeter bandwidthMeterWifi =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateWifi = bandwidthMeterWifi.getBitrateEstimate();\n\n    setActiveNetworkInfo(networkInfo2g);\n    DefaultBandwidthMeter bandwidthMeter2g =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimate2g = bandwidthMeter2g.getBitrateEstimate();\n\n    assertThat(initialEstimateWifi).isGreaterThan(initialEstimate2g);\n  }\n\n  @Test\n  public void defaultInitialBitrateEstimate_forWifi_isGreaterThanEstimateFor3G() {\n    setActiveNetworkInfo(networkInfoWifi);\n    DefaultBandwidthMeter bandwidthMeterWifi =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateWifi = bandwidthMeterWifi.getBitrateEstimate();\n\n    setActiveNetworkInfo(networkInfo3g);\n    DefaultBandwidthMeter bandwidthMeter3g =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimate3g = bandwidthMeter3g.getBitrateEstimate();\n\n    assertThat(initialEstimateWifi).isGreaterThan(initialEstimate3g);\n  }\n\n  @Test\n  public void defaultInitialBitrateEstimate_forEthernet_isGreaterThanEstimateFor2G() {\n    setActiveNetworkInfo(networkInfoEthernet);\n    DefaultBandwidthMeter bandwidthMeterEthernet =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateEthernet = bandwidthMeterEthernet.getBitrateEstimate();\n\n    setActiveNetworkInfo(networkInfo2g);\n    DefaultBandwidthMeter bandwidthMeter2g =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimate2g = bandwidthMeter2g.getBitrateEstimate();\n\n    assertThat(initialEstimateEthernet).isGreaterThan(initialEstimate2g);\n  }\n\n  @Test\n  public void defaultInitialBitrateEstimate_forEthernet_isGreaterThanEstimateFor3G() {\n    setActiveNetworkInfo(networkInfoEthernet);\n    DefaultBandwidthMeter bandwidthMeterEthernet =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateEthernet = bandwidthMeterEthernet.getBitrateEstimate();\n\n    setActiveNetworkInfo(networkInfo3g);\n    DefaultBandwidthMeter bandwidthMeter3g =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimate3g = bandwidthMeter3g.getBitrateEstimate();\n\n    assertThat(initialEstimateEthernet).isGreaterThan(initialEstimate3g);\n  }\n\n  @Test\n  public void defaultInitialBitrateEstimate_for4G_isGreaterThanEstimateFor2G() {\n    setActiveNetworkInfo(networkInfo4g);\n    DefaultBandwidthMeter bandwidthMeter4g =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimate4g = bandwidthMeter4g.getBitrateEstimate();\n\n    setActiveNetworkInfo(networkInfo2g);\n    DefaultBandwidthMeter bandwidthMeter2g =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimate2g = bandwidthMeter2g.getBitrateEstimate();\n\n    assertThat(initialEstimate4g).isGreaterThan(initialEstimate2g);\n  }\n\n  @Test\n  public void defaultInitialBitrateEstimate_for4G_isGreaterThanEstimateFor3G() {\n    setActiveNetworkInfo(networkInfo4g);\n    DefaultBandwidthMeter bandwidthMeter4g =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimate4g = bandwidthMeter4g.getBitrateEstimate();\n\n    setActiveNetworkInfo(networkInfo3g);\n    DefaultBandwidthMeter bandwidthMeter3g =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimate3g = bandwidthMeter3g.getBitrateEstimate();\n\n    assertThat(initialEstimate4g).isGreaterThan(initialEstimate3g);\n  }\n\n  @Test\n  public void defaultInitialBitrateEstimate_for3G_isGreaterThanEstimateFor2G() {\n    setActiveNetworkInfo(networkInfo3g);\n    DefaultBandwidthMeter bandwidthMeter3g =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimate3g = bandwidthMeter3g.getBitrateEstimate();\n\n    setActiveNetworkInfo(networkInfo2g);\n    DefaultBandwidthMeter bandwidthMeter2g =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimate2g = bandwidthMeter2g.getBitrateEstimate();\n\n    assertThat(initialEstimate3g).isGreaterThan(initialEstimate2g);\n  }\n\n  @Test\n  public void defaultInitialBitrateEstimate_forOffline_isReasonable() {\n    setActiveNetworkInfo(networkInfoOffline);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isGreaterThan(100_000L);\n    assertThat(initialEstimate).isLessThan(50_000_000L);\n  }\n\n  @Test\n  public void\n      defaultInitialBitrateEstimate_forWifi_forFastCountry_isGreaterThanEstimateForSlowCountry() {\n    setActiveNetworkInfo(networkInfoWifi);\n    setNetworkCountryIso(FAST_COUNTRY_ISO);\n    DefaultBandwidthMeter bandwidthMeterFast =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate();\n\n    setNetworkCountryIso(SLOW_COUNTRY_ISO);\n    DefaultBandwidthMeter bandwidthMeterSlow =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();\n\n    assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow);\n  }\n\n  @Test\n  public void\n      defaultInitialBitrateEstimate_forEthernet_forFastCountry_isGreaterThanEstimateForSlowCountry() {\n    setActiveNetworkInfo(networkInfoEthernet);\n    setNetworkCountryIso(FAST_COUNTRY_ISO);\n    DefaultBandwidthMeter bandwidthMeterFast =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate();\n\n    setNetworkCountryIso(SLOW_COUNTRY_ISO);\n    DefaultBandwidthMeter bandwidthMeterSlow =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();\n\n    assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow);\n  }\n\n  @Test\n  public void\n      defaultInitialBitrateEstimate_for2G_forFastCountry_isGreaterThanEstimateForSlowCountry() {\n    setActiveNetworkInfo(networkInfo2g);\n    setNetworkCountryIso(FAST_COUNTRY_ISO);\n    DefaultBandwidthMeter bandwidthMeterFast =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate();\n\n    setNetworkCountryIso(SLOW_COUNTRY_ISO);\n    DefaultBandwidthMeter bandwidthMeterSlow =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();\n\n    assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow);\n  }\n\n  @Test\n  public void\n      defaultInitialBitrateEstimate_for3G_forFastCountry_isGreaterThanEstimateForSlowCountry() {\n    setActiveNetworkInfo(networkInfo3g);\n    setNetworkCountryIso(FAST_COUNTRY_ISO);\n    DefaultBandwidthMeter bandwidthMeterFast =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate();\n\n    setNetworkCountryIso(SLOW_COUNTRY_ISO);\n    DefaultBandwidthMeter bandwidthMeterSlow =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();\n\n    assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow);\n  }\n\n  @Test\n  public void\n      defaultInitialBitrateEstimate_for4g_forFastCountry_isGreaterThanEstimateForSlowCountry() {\n    setActiveNetworkInfo(networkInfo4g);\n    setNetworkCountryIso(FAST_COUNTRY_ISO);\n    DefaultBandwidthMeter bandwidthMeterFast =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateFast = bandwidthMeterFast.getBitrateEstimate();\n\n    setNetworkCountryIso(SLOW_COUNTRY_ISO);\n    DefaultBandwidthMeter bandwidthMeterSlow =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();\n\n    assertThat(initialEstimateFast).isGreaterThan(initialEstimateSlow);\n  }\n\n  @Test\n  public void initialBitrateEstimateOverwrite_whileConnectedToNetwork_setsInitialEstimate() {\n    setActiveNetworkInfo(networkInfoWifi);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isEqualTo(123456789);\n  }\n\n  @Test\n  public void initialBitrateEstimateOverwrite_whileOffline_setsInitialEstimate() {\n    setActiveNetworkInfo(networkInfoOffline);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isEqualTo(123456789);\n  }\n\n  @Test\n  public void initialBitrateEstimateOverwrite_forWifi_whileConnectedToWifi_setsInitialEstimate() {\n    setActiveNetworkInfo(networkInfoWifi);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(C.NETWORK_TYPE_WIFI, 123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isEqualTo(123456789);\n  }\n\n  @Test\n  public void\n      initialBitrateEstimateOverwrite_forWifi_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() {\n    setActiveNetworkInfo(networkInfo2g);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(C.NETWORK_TYPE_WIFI, 123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isNotEqualTo(123456789);\n  }\n\n  @Test\n  public void\n      initialBitrateEstimateOverwrite_forEthernet_whileConnectedToEthernet_setsInitialEstimate() {\n    setActiveNetworkInfo(networkInfoEthernet);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(C.NETWORK_TYPE_ETHERNET, 123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isEqualTo(123456789);\n  }\n\n  @Test\n  public void\n      initialBitrateEstimateOverwrite_forEthernet_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() {\n    setActiveNetworkInfo(networkInfo2g);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(C.NETWORK_TYPE_WIFI, 123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isNotEqualTo(123456789);\n  }\n\n  @Test\n  public void initialBitrateEstimateOverwrite_for2G_whileConnectedTo2G_setsInitialEstimate() {\n    setActiveNetworkInfo(networkInfo2g);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(C.NETWORK_TYPE_2G, 123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isEqualTo(123456789);\n  }\n\n  @Test\n  public void\n      initialBitrateEstimateOverwrite_for2G_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() {\n    setActiveNetworkInfo(networkInfoWifi);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(C.NETWORK_TYPE_2G, 123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isNotEqualTo(123456789);\n  }\n\n  @Test\n  public void initialBitrateEstimateOverwrite_for3G_whileConnectedTo3G_setsInitialEstimate() {\n    setActiveNetworkInfo(networkInfo3g);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(C.NETWORK_TYPE_3G, 123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isEqualTo(123456789);\n  }\n\n  @Test\n  public void\n      initialBitrateEstimateOverwrite_for3G_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() {\n    setActiveNetworkInfo(networkInfoWifi);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(C.NETWORK_TYPE_3G, 123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isNotEqualTo(123456789);\n  }\n\n  @Test\n  public void initialBitrateEstimateOverwrite_for4G_whileConnectedTo4G_setsInitialEstimate() {\n    setActiveNetworkInfo(networkInfo4g);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(C.NETWORK_TYPE_4G, 123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isEqualTo(123456789);\n  }\n\n  @Test\n  public void\n      initialBitrateEstimateOverwrite_for4G_whileConnectedToOtherNetwork_doesNotSetInitialEstimate() {\n    setActiveNetworkInfo(networkInfoWifi);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(C.NETWORK_TYPE_4G, 123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isNotEqualTo(123456789);\n  }\n\n  @Test\n  public void initialBitrateEstimateOverwrite_forOffline_whileOffline_setsInitialEstimate() {\n    setActiveNetworkInfo(networkInfoOffline);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(C.NETWORK_TYPE_OFFLINE, 123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isEqualTo(123456789);\n  }\n\n  @Test\n  public void\n      initialBitrateEstimateOverwrite_forOffline_whileConnectedToNetwork_doesNotSetInitialEstimate() {\n    setActiveNetworkInfo(networkInfoWifi);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(C.NETWORK_TYPE_OFFLINE, 123456789)\n            .build();\n    long initialEstimate = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimate).isNotEqualTo(123456789);\n  }\n\n  @Test\n  public void initialBitrateEstimateOverwrite_forCountry_usesDefaultValuesForCountry() {\n    setNetworkCountryIso(SLOW_COUNTRY_ISO);\n    DefaultBandwidthMeter bandwidthMeterSlow =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateSlow = bandwidthMeterSlow.getBitrateEstimate();\n\n    setNetworkCountryIso(FAST_COUNTRY_ISO);\n    DefaultBandwidthMeter bandwidthMeterFastWithSlowOverwrite =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setInitialBitrateEstimate(SLOW_COUNTRY_ISO)\n            .build();\n    long initialEstimateFastWithSlowOverwrite =\n        bandwidthMeterFastWithSlowOverwrite.getBitrateEstimate();\n\n    assertThat(initialEstimateFastWithSlowOverwrite).isEqualTo(initialEstimateSlow);\n  }\n\n  @Test\n  public void networkTypeOverride_updatesBitrateEstimate() {\n    setActiveNetworkInfo(networkInfoEthernet);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext()).build();\n    long initialEstimateEthernet = bandwidthMeter.getBitrateEstimate();\n\n    bandwidthMeter.setNetworkTypeOverride(C.NETWORK_TYPE_2G);\n    long initialEstimate2g = bandwidthMeter.getBitrateEstimate();\n\n    assertThat(initialEstimateEthernet).isGreaterThan(initialEstimate2g);\n  }\n\n  @Test\n  public void networkTypeOverride_doesFullReset() {\n    // Simulate transfers for an ethernet connection.\n    setActiveNetworkInfo(networkInfoEthernet);\n    FakeClock clock = new FakeClock(/* initialTimeMs= */ 0);\n    DefaultBandwidthMeter bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setClock(clock)\n            .build();\n    long[] bitrateEstimatesWithNewInstance = simulateTransfers(bandwidthMeter, clock);\n\n    // Create a new instance and seed with some transfers.\n    setActiveNetworkInfo(networkInfo2g);\n    bandwidthMeter =\n        new DefaultBandwidthMeter.Builder(ApplicationProvider.getApplicationContext())\n            .setClock(clock)\n            .build();\n    simulateTransfers(bandwidthMeter, clock);\n\n    // Override the network type to ethernet and simulate transfers again.\n    bandwidthMeter.setNetworkTypeOverride(C.NETWORK_TYPE_ETHERNET);\n    long[] bitrateEstimatesAfterReset = simulateTransfers(bandwidthMeter, clock);\n\n    // If overriding the network type fully reset the bandwidth meter, we expect the bitrate\n    // estimates generated during simulation to be the same.\n    assertThat(bitrateEstimatesAfterReset).isEqualTo(bitrateEstimatesWithNewInstance);\n  }\n\n  @Test\n  @SuppressWarnings(\"deprecation\")\n  public void defaultInitialBitrateEstimate_withoutContext_isReasonable() {\n    DefaultBandwidthMeter bandwidthMeterWithBuilder =\n        new DefaultBandwidthMeter.Builder(/* context= */ null).build();\n    long initialEstimateWithBuilder = bandwidthMeterWithBuilder.getBitrateEstimate();\n\n    DefaultBandwidthMeter bandwidthMeterWithoutBuilder = new DefaultBandwidthMeter();\n    long initialEstimateWithoutBuilder = bandwidthMeterWithoutBuilder.getBitrateEstimate();\n\n    assertThat(initialEstimateWithBuilder).isGreaterThan(100_000L);\n    assertThat(initialEstimateWithBuilder).isLessThan(50_000_000L);\n    assertThat(initialEstimateWithoutBuilder).isGreaterThan(100_000L);\n    assertThat(initialEstimateWithoutBuilder).isLessThan(50_000_000L);\n  }\n\n  private void setActiveNetworkInfo(NetworkInfo networkInfo) {\n    Shadows.shadowOf(connectivityManager).setActiveNetworkInfo(networkInfo);\n  }\n\n  private void setNetworkCountryIso(String countryIso) {\n    Shadows.shadowOf(telephonyManager).setNetworkCountryIso(countryIso);\n  }\n\n  private static long[] simulateTransfers(DefaultBandwidthMeter bandwidthMeter, FakeClock clock) {\n    long[] bitrateEstimates = new long[SIMULATED_TRANSFER_COUNT];\n    Random random = new Random(/* seed= */ 0);\n    DataSource dataSource = new FakeDataSource();\n    DataSpec dataSpec = new DataSpec(Uri.parse(\"https://dummy.com\"));\n    for (int i = 0; i < SIMULATED_TRANSFER_COUNT; i++) {\n      bandwidthMeter.onTransferStart(dataSource, dataSpec, /* isNetwork= */ true);\n      clock.advanceTime(random.nextInt(/* bound= */ 5000));\n      bandwidthMeter.onBytesTransferred(\n          dataSource,\n          dataSpec,\n          /* isNetwork= */ true,\n          /* bytes= */ random.nextInt(5 * 1024 * 1024));\n      bandwidthMeter.onTransferEnd(dataSource, dataSpec, /* isNetwork= */ true);\n      bitrateEstimates[i] = bandwidthMeter.getBitrateEstimate();\n    }\n    return bitrateEstimates;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicyTest.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.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;\nimport java.io.IOException;\nimport java.util.Collections;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link DefaultLoadErrorHandlingPolicy}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DefaultLoadErrorHandlingPolicyTest {\n\n  @Test\n  public void getBlacklistDurationMsFor_blacklist404() {\n    InvalidResponseCodeException exception =\n        new InvalidResponseCodeException(\n            404, \"Not Found\", Collections.emptyMap(), new DataSpec(Uri.EMPTY));\n    assertThat(getDefaultPolicyBlacklistOutputFor(exception))\n        .isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS);\n  }\n\n  @Test\n  public void getBlacklistDurationMsFor_blacklist410() {\n    InvalidResponseCodeException exception =\n        new InvalidResponseCodeException(\n            410, \"Gone\", Collections.emptyMap(), new DataSpec(Uri.EMPTY));\n    assertThat(getDefaultPolicyBlacklistOutputFor(exception))\n        .isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS);\n  }\n\n  @Test\n  public void getBlacklistDurationMsFor_dontBlacklistUnexpectedHttpCodes() {\n    InvalidResponseCodeException exception =\n        new InvalidResponseCodeException(\n            500, \"Internal Server Error\", Collections.emptyMap(), new DataSpec(Uri.EMPTY));\n    assertThat(getDefaultPolicyBlacklistOutputFor(exception)).isEqualTo(C.TIME_UNSET);\n  }\n\n  @Test\n  public void getBlacklistDurationMsFor_dontBlacklistUnexpectedExceptions() {\n    IOException exception = new IOException();\n    assertThat(getDefaultPolicyBlacklistOutputFor(exception)).isEqualTo(C.TIME_UNSET);\n  }\n\n  @Test\n  public void getRetryDelayMsFor_dontRetryParserException() {\n    assertThat(getDefaultPolicyRetryDelayOutputFor(new ParserException(), 1))\n        .isEqualTo(C.TIME_UNSET);\n  }\n\n  @Test\n  public void getRetryDelayMsFor_successiveRetryDelays() {\n    assertThat(getDefaultPolicyRetryDelayOutputFor(new IOException(), 3)).isEqualTo(2000);\n    assertThat(getDefaultPolicyRetryDelayOutputFor(new IOException(), 5)).isEqualTo(4000);\n    assertThat(getDefaultPolicyRetryDelayOutputFor(new IOException(), 9)).isEqualTo(5000);\n  }\n\n  private static long getDefaultPolicyBlacklistOutputFor(IOException exception) {\n    return new DefaultLoadErrorHandlingPolicy()\n        .getBlacklistDurationMsFor(\n            C.DATA_TYPE_MEDIA, /* loadDurationMs= */ 1000, exception, /* errorCount= */ 1);\n  }\n\n  private static long getDefaultPolicyRetryDelayOutputFor(IOException exception, int errorCount) {\n    return new DefaultLoadErrorHandlingPolicy()\n        .getRetryDelayMsFor(C.DATA_TYPE_MEDIA, /* loadDurationMs= */ 1000, exception, errorCount);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.CacheAsserts;\nimport com.google.android.exoplayer2.testutil.FakeDataSet.FakeData;\nimport com.google.android.exoplayer2.testutil.FakeDataSource;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.FileDataSource;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.NavigableSet;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link CacheDataSource}. */\n@RunWith(AndroidJUnit4.class)\npublic final class CacheDataSourceTest {\n\n  private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};\n  private static final int CACHE_FRAGMENT_SIZE = 3;\n  private static final String DATASPEC_KEY = \"dataSpecKey\";\n  private static final int MIN_PARALLEL_CHUNCKS_DOWNLOADS = 2;\n\n  private Uri testDataUri;\n  private DataSpec unboundedDataSpec;\n  private DataSpec boundedDataSpec;\n  private DataSpec unboundedDataSpecWithKey;\n  private DataSpec boundedDataSpecWithKey;\n  private String defaultCacheKey;\n  private String customCacheKey;\n  private CacheKeyFactory cacheKeyFactory;\n  private File tempFolder;\n  private SimpleCache cache;\n\n  @Before\n  public void setUp() throws Exception {\n    testDataUri = Uri.parse(\"https://www.test.com/data\");\n    unboundedDataSpec = buildDataSpec(/* unbounded= */ true, /* key= */ null);\n    boundedDataSpec = buildDataSpec(/* unbounded= */ false, /* key= */ null);\n    unboundedDataSpecWithKey = buildDataSpec(/* unbounded= */ true, DATASPEC_KEY);\n    boundedDataSpecWithKey = buildDataSpec(/* unbounded= */ false, DATASPEC_KEY);\n    defaultCacheKey = CacheUtil.DEFAULT_CACHE_KEY_FACTORY.buildCacheKey(unboundedDataSpec);\n    customCacheKey = \"customKey.\" + defaultCacheKey;\n    cacheKeyFactory = new CacheKeyFactory() {\n      @Override\n      public String buildCacheKey(DataSpec dataSpec) {\n        return customCacheKey;\n      }\n\n      @Override\n      public int maxDownloadParallelSegments() {\n        // TODO Implement a better logic to determine the number of concurrent chunck downloads or\n        // provide a way to customize by client. For now it uses at least MIN_PARALLEL_CHUNCKS_DOWNLOADS\n        // and at most the half of the number of device processor.\n        return Math.max(MIN_PARALLEL_CHUNCKS_DOWNLOADS, Runtime.getRuntime().availableProcessors() / 2);\n      }\n    };\n    tempFolder =\n        Util.createTempDirectory(ApplicationProvider.getApplicationContext(), \"ExoPlayerTest\");\n    cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());\n  }\n\n  @After\n  public void tearDown() throws Exception {\n    Util.recursiveDelete(tempFolder);\n  }\n\n  @Test\n  public void testFragmentSize() throws Exception {\n    CacheDataSource cacheDataSource = createCacheDataSource(false, false);\n    assertReadDataContentLength(cacheDataSource, boundedDataSpec, false, false);\n    for (String key : cache.getKeys()) {\n      for (CacheSpan cacheSpan : cache.getCachedSpans(key)) {\n        assertThat(cacheSpan.length <= CACHE_FRAGMENT_SIZE).isTrue();\n        assertThat(cacheSpan.file.length() <= CACHE_FRAGMENT_SIZE).isTrue();\n      }\n    }\n  }\n\n  @Test\n  public void testCacheAndReadUnboundedRequest() throws Exception {\n    assertCacheAndRead(unboundedDataSpec, /* unknownLength= */ false);\n  }\n\n  @Test\n  public void testCacheAndReadUnknownLength() throws Exception {\n    assertCacheAndRead(boundedDataSpec, /* unknownLength= */ true);\n  }\n\n  @Test\n  public void testCacheAndReadUnboundedRequestUnknownLength() throws Exception {\n    assertCacheAndRead(unboundedDataSpec, /* unknownLength= */ true);\n  }\n\n  @Test\n  public void testCacheAndRead() throws Exception {\n    assertCacheAndRead(boundedDataSpec, /* unknownLength= */ false);\n  }\n\n  @Test\n  public void testUnsatisfiableRange() throws Exception {\n    // Bounded request but the content length is unknown. This forces all data to be cached but not\n    // the length.\n    assertCacheAndRead(boundedDataSpec, /* unknownLength= */ true);\n\n    // Now do an unbounded request. This will read all of the data from cache and then try to read\n    // more from upstream which will cause to a 416 so CDS will store the length.\n    CacheDataSource cacheDataSource =\n        createCacheDataSource(/* setReadException= */ true, /* unknownLength= */ true);\n    assertReadDataContentLength(\n        cacheDataSource, unboundedDataSpec, /* unknownLength= */ true, /* customCacheKey= */ false);\n\n    // If the user try to access off range then it should throw an IOException.\n    try {\n      cacheDataSource =\n          createCacheDataSource(/* setReadException= */ false, /* unknownLength= */ false);\n      cacheDataSource.open(buildDataSpec(TEST_DATA.length, /* length= */ 1, defaultCacheKey));\n      fail();\n    } catch (IOException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testCacheAndReadUnboundedRequestWithCacheKeyFactoryWithNullDataSpecCacheKey()\n      throws Exception {\n    assertCacheAndRead(unboundedDataSpec, /* unknownLength= */ false, cacheKeyFactory);\n  }\n\n  @Test\n  public void testCacheAndReadUnknownLengthWithCacheKeyFactoryOverridingWithNullDataSpecCacheKey()\n      throws Exception {\n    assertCacheAndRead(boundedDataSpec, /* unknownLength= */ true, cacheKeyFactory);\n  }\n\n  @Test\n  public void\n      testCacheAndReadUnboundedRequestUnknownLengthWithCacheKeyFactoryWithNullDataSpecCacheKey()\n          throws Exception {\n    assertCacheAndRead(unboundedDataSpec, /* unknownLength= */ true, cacheKeyFactory);\n  }\n\n  @Test\n  public void testCacheAndReadWithCacheKeyFactoryWithNullDataSpecCacheKey() throws Exception {\n    assertCacheAndRead(boundedDataSpec, /* unknownLength= */ false, cacheKeyFactory);\n  }\n\n  @Test\n  public void testUnsatisfiableRangeWithCacheKeyFactoryNullDataSpecCacheKey() throws Exception {\n    // Bounded request but the content length is unknown. This forces all data to be cached but not\n    // the length.\n    assertCacheAndRead(boundedDataSpec, /* unknownLength= */ true, cacheKeyFactory);\n\n    // Now do an unbounded request. This will read all of the data from cache and then try to read\n    // more from upstream which will cause to a 416 so CDS will store the length.\n    CacheDataSource cacheDataSource =\n        createCacheDataSource(\n            /* setReadException= */ true, /* unknownLength= */ true, cacheKeyFactory);\n    assertReadDataContentLength(\n        cacheDataSource, unboundedDataSpec, /* unknownLength= */ true, /* customCacheKey= */ true);\n\n    // If the user try to access off range then it should throw an IOException.\n    try {\n      cacheDataSource =\n          createCacheDataSource(\n              /* setReadException= */ false, /* unknownLength= */ false, cacheKeyFactory);\n      cacheDataSource.open(buildDataSpec(TEST_DATA.length, /* length= */ 1, customCacheKey));\n      fail();\n    } catch (IOException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testCacheAndReadUnboundedRequestWithCacheKeyFactoryOverridingDataSpecCacheKey()\n      throws Exception {\n    assertCacheAndRead(unboundedDataSpecWithKey, false, cacheKeyFactory);\n  }\n\n  @Test\n  public void testCacheAndReadUnknownLengthWithCacheKeyFactoryOverridingDataSpecCacheKey()\n      throws Exception {\n    assertCacheAndRead(boundedDataSpecWithKey, true, cacheKeyFactory);\n  }\n\n  @Test\n  public void\n      testCacheAndReadUnboundedRequestUnknownLengthWithCacheKeyFactoryOverridingDataSpecCacheKey()\n          throws Exception {\n    assertCacheAndRead(unboundedDataSpecWithKey, /* unknownLength= */ true, cacheKeyFactory);\n  }\n\n  @Test\n  public void testCacheAndReadWithCacheKeyFactoryOverridingDataSpecCacheKey() throws Exception {\n    assertCacheAndRead(boundedDataSpecWithKey, /* unknownLength= */ false, cacheKeyFactory);\n  }\n\n  @Test\n  public void testUnsatisfiableRangeWithCacheKeyFactoryOverridingDataSpecCacheKey()\n      throws Exception {\n    // Bounded request but the content length is unknown. This forces all data to be cached but not\n    // the length.\n    assertCacheAndRead(boundedDataSpecWithKey, /* unknownLength= */ true, cacheKeyFactory);\n\n    // Now do an unbounded request. This will read all of the data from cache and then try to read\n    // more from upstream which will cause to a 416 so CDS will store the length.\n    CacheDataSource cacheDataSource =\n        createCacheDataSource(\n            /* setReadException= */ true, /* unknownLength= */ true, cacheKeyFactory);\n    assertReadDataContentLength(\n        cacheDataSource,\n        unboundedDataSpecWithKey,\n        /* unknownLength= */ true,\n        /* customCacheKey= */ true);\n\n    // If the user try to access off range then it should throw an IOException.\n    try {\n      cacheDataSource =\n          createCacheDataSource(\n              /* setReadException= */ false, /* unknownLength= */ false, cacheKeyFactory);\n      cacheDataSource.open(buildDataSpec(TEST_DATA.length, /* length= */ 1, customCacheKey));\n      fail();\n    } catch (IOException e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testContentLengthEdgeCases() throws Exception {\n    DataSpec dataSpec = buildDataSpec(TEST_DATA.length - 2, 2);\n\n    // Read partial at EOS but don't cross it so length is unknown.\n    CacheDataSource cacheDataSource = createCacheDataSource(false, true);\n    assertReadData(cacheDataSource, dataSpec, true);\n    assertThat(ContentMetadata.getContentLength(cache.getContentMetadata(defaultCacheKey)))\n        .isEqualTo(C.LENGTH_UNSET);\n\n    // Now do an unbounded request for whole data. This will cause a bounded request from upstream.\n    // End of data from upstream shouldn't be mixed up with EOS and cause length set wrong.\n    cacheDataSource = createCacheDataSource(false, true);\n    assertReadDataContentLength(cacheDataSource, unboundedDataSpec, true, false);\n\n    // Now the length set correctly do an unbounded request with offset.\n    assertThat(\n            cacheDataSource.open(\n                buildDataSpec(TEST_DATA.length - 2, C.LENGTH_UNSET, defaultCacheKey)))\n        .isEqualTo(2);\n\n    // An unbounded request with offset for not cached content.\n    dataSpec =\n        new DataSpec(\n            Uri.parse(\"https://www.test.com/other\"),\n            TEST_DATA.length - 2,\n            C.LENGTH_UNSET,\n            /* key= */ null);\n    assertThat(cacheDataSource.open(dataSpec)).isEqualTo(C.LENGTH_UNSET);\n  }\n\n  @Test\n  public void testUnknownLengthContentReadInOneConnectionAndLengthIsResolved() throws Exception {\n    FakeDataSource upstream = new FakeDataSource();\n    upstream\n        .getDataSet()\n        .newData(testDataUri)\n        .appendReadData(TEST_DATA)\n        .setSimulateUnknownLength(true);\n    CacheDataSource cacheDataSource = new CacheDataSource(cache, upstream, 0);\n\n    cacheDataSource.open(unboundedDataSpec);\n    TestUtil.readToEnd(cacheDataSource);\n    cacheDataSource.close();\n\n    assertThat(upstream.getAndClearOpenedDataSpecs()).hasLength(1);\n    assertThat(ContentMetadata.getContentLength(cache.getContentMetadata(defaultCacheKey)))\n        .isEqualTo(TEST_DATA.length);\n  }\n\n  @Test\n  public void testIgnoreCacheForUnsetLengthRequests() throws Exception {\n    FakeDataSource upstream = new FakeDataSource();\n    upstream.getDataSet().setData(testDataUri, TEST_DATA);\n    CacheDataSource cacheDataSource =\n        new CacheDataSource(\n            cache, upstream, CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS);\n\n    cacheDataSource.open(unboundedDataSpec);\n    TestUtil.readToEnd(cacheDataSource);\n    cacheDataSource.close();\n\n    assertThat(cache.getKeys()).isEmpty();\n  }\n\n  @Test\n  public void testReadOnlyCache() throws Exception {\n    CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null);\n    assertReadDataContentLength(cacheDataSource, boundedDataSpec, false, false);\n    assertCacheEmpty(cache);\n  }\n\n  @Test\n  public void testSwitchToCacheSourceWithReadOnlyCacheDataSource() throws Exception {\n    // Create a fake data source with a 1 MB default data.\n    FakeDataSource upstream = new FakeDataSource();\n    FakeData fakeData = upstream.getDataSet().newDefaultData().appendReadData(1024 * 1024 - 1);\n    // Insert an action just before the end of the data to fail the test if reading from upstream\n    // reaches end of the data.\n    fakeData\n        .appendReadAction(() -> fail(\"Read from upstream shouldn't reach to the end of the data.\"))\n        .appendReadData(1);\n    // Create cache read-only CacheDataSource.\n    CacheDataSource cacheDataSource =\n        new CacheDataSource(cache, upstream, new FileDataSource(), null, 0, null);\n\n    // Open source and read some data from upstream as the data hasn't cached yet.\n    cacheDataSource.open(unboundedDataSpec);\n    byte[] buffer = new byte[1024];\n    cacheDataSource.read(buffer, 0, buffer.length);\n\n    // Cache the data. Although we use another FakeDataSource instance, it shouldn't matter.\n    FakeDataSource upstream2 =\n        new FakeDataSource(\n            new FakeDataSource()\n                .getDataSet()\n                .newDefaultData()\n                .appendReadData(1024 * 1024)\n                .endData());\n    CacheUtil.cache(\n        unboundedDataSpec,\n        cache,\n        /* cacheKeyFactory= */ null,\n        upstream2,\n        /* progressListener= */ null,\n        /* isCanceled= */ null);\n\n    // Read the rest of the data.\n    TestUtil.readToEnd(cacheDataSource);\n    cacheDataSource.close();\n  }\n\n  @Test\n  public void testSwitchToCacheSourceWithNonBlockingCacheDataSource() throws Exception {\n    // Create a fake data source with a 1 MB default data.\n    FakeDataSource upstream = new FakeDataSource();\n    FakeData fakeData = upstream.getDataSet().newDefaultData().appendReadData(1024 * 1024 - 1);\n    // Insert an action just before the end of the data to fail the test if reading from upstream\n    // reaches end of the data.\n    fakeData\n        .appendReadAction(() -> fail(\"Read from upstream shouldn't reach to the end of the data.\"))\n        .appendReadData(1);\n\n    // Lock the content on the cache.\n    SimpleCacheSpan cacheSpan = cache.startReadWriteNonBlocking(defaultCacheKey, 0);\n    assertThat(cacheSpan).isNotNull();\n    assertThat(cacheSpan.isHoleSpan()).isTrue();\n\n    // Create non blocking CacheDataSource.\n    CacheDataSource cacheDataSource = new CacheDataSource(cache, upstream, 0);\n\n    // Open source and read some data from upstream without writing to cache as the data is locked.\n    cacheDataSource.open(unboundedDataSpec);\n    byte[] buffer = new byte[1024];\n    cacheDataSource.read(buffer, 0, buffer.length);\n\n    // Unlock the span.\n    cache.releaseHoleSpan(cacheSpan);\n    assertCacheEmpty(cache);\n\n    // Cache the data. Although we use another FakeDataSource instance, it shouldn't matter.\n    FakeDataSource upstream2 =\n        new FakeDataSource(\n            new FakeDataSource()\n                .getDataSet()\n                .newDefaultData()\n                .appendReadData(1024 * 1024)\n                .endData());\n    CacheUtil.cache(\n        unboundedDataSpec,\n        cache,\n        /* cacheKeyFactory= */ null,\n        upstream2,\n        /* progressListener= */ null,\n        /* isCanceled= */ null);\n\n    // Read the rest of the data.\n    TestUtil.readToEnd(cacheDataSource);\n    cacheDataSource.close();\n  }\n\n  @Test\n  public void testDeleteCachedWhileReadingFromUpstreamWithReadOnlyCacheDataSourceDoesNotCrash()\n      throws Exception {\n    // Create a fake data source with a 1 KB default data.\n    FakeDataSource upstream = new FakeDataSource();\n    int dataLength = 1024;\n    upstream.getDataSet().newDefaultData().appendReadData(dataLength).endData();\n\n    // Cache the latter half of the data.\n    int halfDataLength = 512;\n    DataSpec dataSpec = buildDataSpec(halfDataLength, C.LENGTH_UNSET);\n    CacheUtil.cache(\n        dataSpec,\n        cache,\n        /* cacheKeyFactory= */ null,\n        upstream,\n        /* progressListener= */ null,\n        /* isCanceled= */ null);\n\n    // Create cache read-only CacheDataSource.\n    CacheDataSource cacheDataSource =\n        new CacheDataSource(cache, upstream, new FileDataSource(), null, 0, null);\n\n    // Open source and read some data from upstream as the data hasn't cached yet.\n    cacheDataSource.open(unboundedDataSpec);\n    TestUtil.readExactly(cacheDataSource, 100);\n\n    // Delete cached data.\n    CacheUtil.remove(unboundedDataSpec, cache, /* cacheKeyFactory= */ null);\n    assertCacheEmpty(cache);\n\n    // Read the rest of the data.\n    TestUtil.readToEnd(cacheDataSource);\n    cacheDataSource.close();\n  }\n\n  @Test\n  public void testDeleteCachedWhileReadingFromUpstreamWithBlockingCacheDataSourceDoesNotBlock()\n      throws Exception {\n    // Create a fake data source with a 1 KB default data.\n    FakeDataSource upstream = new FakeDataSource();\n    int dataLength = 1024;\n    upstream.getDataSet().newDefaultData().appendReadData(dataLength).endData();\n\n    // Cache the latter half of the data.\n    int halfDataLength = 512;\n    DataSpec dataSpec = buildDataSpec(/* position= */ 0, halfDataLength);\n    CacheUtil.cache(\n        dataSpec,\n        cache,\n        /* cacheKeyFactory= */ null,\n        upstream,\n        /* progressListener= */ null,\n        /* isCanceled= */ null);\n\n    // Create blocking CacheDataSource.\n    CacheDataSource cacheDataSource =\n        new CacheDataSource(cache, upstream, CacheDataSource.FLAG_BLOCK_ON_CACHE);\n\n    cacheDataSource.open(unboundedDataSpec);\n\n    // Read the first half from upstream as it hasn't cached yet.\n    TestUtil.readExactly(cacheDataSource, halfDataLength);\n\n    // Delete the cached latter half.\n    NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(defaultCacheKey);\n    for (CacheSpan cachedSpan : cachedSpans) {\n      if (cachedSpan.position >= halfDataLength) {\n        cache.removeSpan(cachedSpan);\n      }\n    }\n\n    // Read the rest of the data.\n    TestUtil.readToEnd(cacheDataSource);\n    cacheDataSource.close();\n  }\n\n  private void assertCacheAndRead(DataSpec dataSpec, boolean unknownLength) throws IOException {\n    assertCacheAndRead(dataSpec, unknownLength, /* cacheKeyFactory= */ null);\n  }\n\n  private void assertCacheAndRead(\n      DataSpec dataSpec, boolean unknownLength, @Nullable CacheKeyFactory cacheKeyFactory)\n      throws IOException {\n    // Read all data from upstream and write to cache.\n    CacheDataSource cacheDataSource =\n        createCacheDataSource(/* setReadException= */ false, unknownLength, cacheKeyFactory);\n    assertReadDataContentLength(cacheDataSource, dataSpec, unknownLength, cacheKeyFactory != null);\n\n    // Just read from cache.\n    cacheDataSource =\n        createCacheDataSource(/* setReadException= */ true, unknownLength, cacheKeyFactory);\n    assertReadDataContentLength(\n        cacheDataSource,\n        dataSpec,\n        /* unknownLength= */ false,\n        /* customCacheKey= */ cacheKeyFactory != null);\n  }\n\n  /**\n   * Reads data until EOI and compares it to {@link #TEST_DATA}. Also checks content length returned\n   * from open() call and the cached content length.\n   */\n  private void assertReadDataContentLength(\n      CacheDataSource cacheDataSource,\n      DataSpec dataSpec,\n      boolean unknownLength,\n      boolean customCacheKey)\n      throws IOException {\n    assertReadData(cacheDataSource, dataSpec, unknownLength);\n    // If the request was unbounded then the content length should be cached, either because the\n    // content length was known or because EOS was read. If the request was bounded then the content\n    // length will not have been determined.\n    ContentMetadata metadata =\n        cache.getContentMetadata(customCacheKey ? this.customCacheKey : defaultCacheKey);\n    assertThat(ContentMetadata.getContentLength(metadata))\n        .isEqualTo(dataSpec.length == C.LENGTH_UNSET ? TEST_DATA.length : C.LENGTH_UNSET);\n  }\n\n  private void assertReadData(\n      CacheDataSource cacheDataSource, DataSpec dataSpec, boolean unknownLength)\n      throws IOException {\n    int position = (int) dataSpec.absoluteStreamPosition;\n    int requestLength = (int) dataSpec.length;\n    int readLength = TEST_DATA.length - position;\n    if (requestLength != C.LENGTH_UNSET) {\n      readLength = Math.min(readLength, requestLength);\n    }\n    assertThat(cacheDataSource.open(dataSpec))\n        .isEqualTo(unknownLength ? requestLength : readLength);\n    cacheDataSource.close();\n\n    byte[] expected = Arrays.copyOfRange(TEST_DATA, position, position + readLength);\n    CacheAsserts.assertReadData(cacheDataSource, dataSpec, expected);\n  }\n\n  private CacheDataSource createCacheDataSource(boolean setReadException, boolean unknownLength) {\n    return createCacheDataSource(\n        setReadException, unknownLength, CacheDataSource.FLAG_BLOCK_ON_CACHE);\n  }\n\n  private CacheDataSource createCacheDataSource(\n      boolean setReadException, boolean unknownLength, CacheKeyFactory cacheKeyFactory) {\n    return createCacheDataSource(\n        setReadException,\n        unknownLength,\n        CacheDataSource.FLAG_BLOCK_ON_CACHE,\n        new CacheDataSink(cache, CACHE_FRAGMENT_SIZE),\n        cacheKeyFactory);\n  }\n\n  private CacheDataSource createCacheDataSource(\n      boolean setReadException, boolean unknownLength, @CacheDataSource.Flags int flags) {\n    return createCacheDataSource(\n        setReadException, unknownLength, flags, new CacheDataSink(cache, CACHE_FRAGMENT_SIZE));\n  }\n\n  private CacheDataSource createCacheDataSource(\n      boolean setReadException,\n      boolean unknownLength,\n      @CacheDataSource.Flags int flags,\n      CacheDataSink cacheWriteDataSink) {\n    return createCacheDataSource(\n        setReadException, unknownLength, flags, cacheWriteDataSink, /* cacheKeyFactory= */ null);\n  }\n\n  private CacheDataSource createCacheDataSource(\n      boolean setReadException,\n      boolean unknownLength,\n      @CacheDataSource.Flags int flags,\n      CacheDataSink cacheWriteDataSink,\n      CacheKeyFactory cacheKeyFactory) {\n    FakeDataSource upstream = new FakeDataSource();\n    FakeData fakeData =\n        upstream\n            .getDataSet()\n            .newDefaultData()\n            .setSimulateUnknownLength(unknownLength)\n            .appendReadData(TEST_DATA);\n    if (setReadException) {\n      fakeData.appendReadError(new IOException(\"Shouldn't read from upstream\"));\n    }\n    return new CacheDataSource(\n        cache,\n        upstream,\n        new FileDataSource(),\n        cacheWriteDataSink,\n        flags,\n        /* eventListener= */ null,\n        cacheKeyFactory);\n  }\n\n  private DataSpec buildDataSpec(boolean unbounded, @Nullable String key) {\n    return buildDataSpec(/* position= */ 0, unbounded ? C.LENGTH_UNSET : TEST_DATA.length, key);\n  }\n\n  private DataSpec buildDataSpec(long position, long length) {\n    return buildDataSpec(position, length, /* key= */ null);\n  }\n\n  private DataSpec buildDataSpec(long position, long length, @Nullable String key) {\n    return new DataSpec(\n        testDataUri, position, length, key, DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.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 static com.google.common.truth.Truth.assertThat;\nimport static java.util.Arrays.copyOf;\nimport static java.util.Arrays.copyOfRange;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.FakeDataSource;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.upstream.DataSink;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.FileDataSource;\nimport com.google.android.exoplayer2.upstream.cache.Cache.CacheException;\nimport com.google.android.exoplayer2.upstream.crypto.AesCipherDataSink;\nimport com.google.android.exoplayer2.upstream.crypto.AesCipherDataSource;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Random;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Additional tests for {@link CacheDataSource}. */\n@RunWith(AndroidJUnit4.class)\npublic final class CacheDataSourceTest2 {\n\n  private static final String EXO_CACHE_DIR = \"exo\";\n  private static final int EXO_CACHE_MAX_FILESIZE = 128;\n\n  private static final Uri URI = Uri.parse(\"http://test.com/content\");\n  private static final String KEY = \"key\";\n  private static final byte[] DATA = TestUtil.buildTestData(8 * EXO_CACHE_MAX_FILESIZE + 1);\n\n  // A DataSpec that covers the full file.\n  private static final DataSpec FULL = new DataSpec(URI, 0, DATA.length, KEY);\n\n  private static final int OFFSET_ON_BOUNDARY = EXO_CACHE_MAX_FILESIZE;\n  // A DataSpec that starts at 0 and extends to a cache file boundary.\n  private static final DataSpec END_ON_BOUNDARY = new DataSpec(URI, 0, OFFSET_ON_BOUNDARY, KEY);\n  // A DataSpec that starts on the same boundary and extends to the end of the file.\n  private static final DataSpec START_ON_BOUNDARY = new DataSpec(URI, OFFSET_ON_BOUNDARY,\n      DATA.length - OFFSET_ON_BOUNDARY, KEY);\n\n  private static final int OFFSET_OFF_BOUNDARY = EXO_CACHE_MAX_FILESIZE * 2 + 1;\n  // A DataSpec that starts at 0 and extends to just past a cache file boundary.\n  private static final DataSpec END_OFF_BOUNDARY = new DataSpec(URI, 0, OFFSET_OFF_BOUNDARY, KEY);\n  // A DataSpec that starts on the same boundary and extends to the end of the file.\n  private static final DataSpec START_OFF_BOUNDARY = new DataSpec(URI, OFFSET_OFF_BOUNDARY,\n      DATA.length - OFFSET_OFF_BOUNDARY, KEY);\n\n  @Test\n  public void testWithoutEncryption() throws IOException {\n    testReads(false);\n  }\n\n  @Test\n  public void testWithEncryption() throws IOException {\n    testReads(true);\n  }\n\n  private void testReads(boolean useEncryption) throws IOException {\n    FakeDataSource upstreamSource = buildFakeUpstreamSource();\n    CacheDataSource source =\n        buildCacheDataSource(\n            ApplicationProvider.getApplicationContext(), upstreamSource, useEncryption);\n    // First read, should arrive from upstream.\n    testRead(END_ON_BOUNDARY, source);\n    assertSingleOpen(upstreamSource, 0, OFFSET_ON_BOUNDARY);\n    // Second read, should arrive from upstream.\n    testRead(START_OFF_BOUNDARY, source);\n    assertSingleOpen(upstreamSource, OFFSET_OFF_BOUNDARY, DATA.length);\n    // Second read, should arrive part from cache and part from upstream.\n    testRead(END_OFF_BOUNDARY, source);\n    assertSingleOpen(upstreamSource, OFFSET_ON_BOUNDARY, OFFSET_OFF_BOUNDARY);\n    // Third read, should arrive from cache.\n    testRead(FULL, source);\n    assertNoOpen(upstreamSource);\n    // Various reads, should all arrive from cache.\n    testRead(FULL, source);\n    assertNoOpen(upstreamSource);\n    testRead(START_ON_BOUNDARY, source);\n    assertNoOpen(upstreamSource);\n    testRead(END_ON_BOUNDARY, source);\n    assertNoOpen(upstreamSource);\n    testRead(START_OFF_BOUNDARY, source);\n    assertNoOpen(upstreamSource);\n    testRead(END_OFF_BOUNDARY, source);\n    assertNoOpen(upstreamSource);\n  }\n\n  private void testRead(DataSpec dataSpec, CacheDataSource source) throws IOException {\n    byte[] scratch = new byte[4096];\n    Random random = new Random(0);\n    source.open(dataSpec);\n    int position = (int) dataSpec.absoluteStreamPosition;\n    int bytesRead = 0;\n    while (bytesRead != C.RESULT_END_OF_INPUT) {\n      int maxBytesToRead = random.nextInt(scratch.length) + 1;\n      bytesRead = source.read(scratch, 0, maxBytesToRead);\n      if (bytesRead != C.RESULT_END_OF_INPUT) {\n        assertThat(copyOf(scratch, bytesRead))\n            .isEqualTo(copyOfRange(DATA, position, position + bytesRead));\n        position += bytesRead;\n      }\n    }\n    source.close();\n  }\n\n  /**\n   * Asserts that a single {@link DataSource#open(DataSpec)} call has been made to the upstream\n   * source, with the specified start (inclusive) and end (exclusive) positions.\n   */\n  private void assertSingleOpen(FakeDataSource upstreamSource, int start, int end) {\n    DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs();\n    assertThat(openedDataSpecs).hasLength(1);\n    assertThat(openedDataSpecs[0].position).isEqualTo(start);\n    assertThat(openedDataSpecs[0].absoluteStreamPosition).isEqualTo(start);\n    assertThat(openedDataSpecs[0].length).isEqualTo(end - start);\n  }\n\n  /**\n   * Asserts that the upstream source was not opened.\n   */\n  private void assertNoOpen(FakeDataSource upstreamSource) {\n    DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs();\n    assertThat(openedDataSpecs).hasLength(0);\n  }\n\n  private static FakeDataSource buildFakeUpstreamSource() {\n    FakeDataSource fakeDataSource = new FakeDataSource();\n    fakeDataSource.getDataSet().newDefaultData().appendReadData(DATA);\n    return fakeDataSource;\n  }\n\n  private static CacheDataSource buildCacheDataSource(Context context, DataSource upstreamSource,\n      boolean useAesEncryption) throws CacheException {\n    File cacheDir = context.getExternalCacheDir();\n    Cache cache = new SimpleCache(new File(cacheDir, EXO_CACHE_DIR), new NoOpCacheEvictor());\n    emptyCache(cache);\n\n    // Source and cipher\n    final String secretKey = \"testKey:12345678\";\n    DataSource file = new FileDataSource();\n    DataSource cacheReadDataSource = useAesEncryption\n        ? new AesCipherDataSource(Util.getUtf8Bytes(secretKey), file) : file;\n\n    // Sink and cipher\n    CacheDataSink cacheSink = new CacheDataSink(cache, EXO_CACHE_MAX_FILESIZE);\n    byte[] scratch = new byte[3897];\n    DataSink cacheWriteDataSink = useAesEncryption\n        ? new AesCipherDataSink(Util.getUtf8Bytes(secretKey), cacheSink, scratch) : cacheSink;\n\n    return new CacheDataSource(cache,\n        upstreamSource,\n        cacheReadDataSource,\n        cacheWriteDataSink,\n        CacheDataSource.FLAG_BLOCK_ON_CACHE,\n        null); // eventListener\n  }\n\n  private static void emptyCache(Cache cache) throws CacheException {\n    for (String key : cache.getKeys()) {\n      for (CacheSpan span : cache.getCachedSpans(key)) {\n        cache.removeSpan(span);\n      }\n    }\n    // Sanity check that the cache really is empty now.\n    assertThat(cache.getKeys().isEmpty()).isTrue();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.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 static com.google.android.exoplayer2.C.LENGTH_UNSET;\nimport static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;\nimport static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.net.Uri;\nimport android.util.Pair;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.FakeDataSet;\nimport com.google.android.exoplayer2.testutil.FakeDataSource;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.FileDataSource;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.EOFException;\nimport java.io.File;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Answers;\nimport org.mockito.Mock;\nimport org.mockito.MockitoAnnotations;\n\n/** Tests {@link CacheUtil}. */\n@RunWith(AndroidJUnit4.class)\npublic final class CacheUtilTest {\n\n  /**\n   * Abstract fake Cache implementation used by the test. This class must be public so Mockito can\n   * create a proxy for it.\n   */\n  public abstract static class AbstractFakeCache implements Cache {\n\n    // This array is set to alternating length of cached and not cached regions in tests:\n    // spansAndGaps = {<length of 1st cached region>, <length of 1st not cached region>,\n    //    <length of 2nd cached region>, <length of 2nd not cached region>, ... }\n    // Ideally it should end with a cached region but it shouldn't matter for any code.\n    private int[] spansAndGaps;\n    private long contentLength;\n\n    private void init() {\n      spansAndGaps = new int[] {};\n      contentLength = C.LENGTH_UNSET;\n    }\n\n    @Override\n    public long getCachedLength(String key, long position, long length) {\n      for (int i = 0; i < spansAndGaps.length; i++) {\n        int spanOrGap = spansAndGaps[i];\n        if (position < spanOrGap) {\n          long left = Math.min(spanOrGap - position, length);\n          return (i & 1) == 1 ? -left : left;\n        }\n        position -= spanOrGap;\n      }\n      return -length;\n    }\n\n    @Override\n    public ContentMetadata getContentMetadata(String key) {\n      DefaultContentMetadata metadata = new DefaultContentMetadata();\n      ContentMetadataMutations mutations = new ContentMetadataMutations();\n      ContentMetadataMutations.setContentLength(mutations, contentLength);\n      return metadata.copyWithMutationsApplied(mutations);\n    }\n  }\n\n  @Mock(answer = Answers.CALLS_REAL_METHODS) private AbstractFakeCache mockCache;\n  private File tempFolder;\n  private SimpleCache cache;\n\n  @Before\n  public void setUp() throws Exception {\n    MockitoAnnotations.initMocks(this);\n    mockCache.init();\n    tempFolder =\n        Util.createTempDirectory(ApplicationProvider.getApplicationContext(), \"ExoPlayerTest\");\n    cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());\n  }\n\n  @After\n  public void tearDown() {\n    Util.recursiveDelete(tempFolder);\n  }\n\n  @Test\n  public void testGenerateKey() {\n    assertThat(CacheUtil.generateKey(new DataSpec(Uri.EMPTY))).isNotNull();\n\n    Uri testUri = Uri.parse(\"test\");\n    DataSpec testDataSpec = new DataSpec(testUri);\n    String key = CacheUtil.generateKey(testDataSpec);\n    assertThat(key).isNotNull();\n\n    // Should generate the same key for the same input.\n    assertThat(CacheUtil.generateKey(testDataSpec)).isEqualTo(key);\n\n    // Should generate different key for different input.\n    assertThat(key.equals(CacheUtil.generateKey(new DataSpec(Uri.parse(\"test2\"))))).isFalse();\n  }\n\n  @Test\n  public void testDefaultCacheKeyFactory_buildCacheKey() {\n    Uri testUri = Uri.parse(\"test\");\n    String key = \"key\";\n    // If DataSpec.key is present, returns it.\n    assertThat(\n            CacheUtil.DEFAULT_CACHE_KEY_FACTORY.buildCacheKey(\n                new DataSpec(testUri, 0, LENGTH_UNSET, key)))\n        .isEqualTo(key);\n    // If not generates a new one using DataSpec.uri.\n    assertThat(\n            CacheUtil.DEFAULT_CACHE_KEY_FACTORY.buildCacheKey(\n                new DataSpec(testUri, 0, LENGTH_UNSET, null)))\n        .isEqualTo(testUri.toString());\n  }\n\n  @Test\n  public void testGetCachedNoData() {\n    Pair<Long, Long> contentLengthAndBytesCached =\n        CacheUtil.getCached(\n            new DataSpec(Uri.parse(\"test\")), mockCache, /* cacheKeyFactory= */ null);\n\n    assertThat(contentLengthAndBytesCached.first).isEqualTo(C.LENGTH_UNSET);\n    assertThat(contentLengthAndBytesCached.second).isEqualTo(0);\n  }\n\n  @Test\n  public void testGetCachedDataUnknownLength() {\n    // Mock there is 100 bytes cached at the beginning\n    mockCache.spansAndGaps = new int[] {100};\n    Pair<Long, Long> contentLengthAndBytesCached =\n        CacheUtil.getCached(\n            new DataSpec(Uri.parse(\"test\")), mockCache, /* cacheKeyFactory= */ null);\n\n    assertThat(contentLengthAndBytesCached.first).isEqualTo(C.LENGTH_UNSET);\n    assertThat(contentLengthAndBytesCached.second).isEqualTo(100);\n  }\n\n  @Test\n  public void testGetCachedNoDataKnownLength() {\n    mockCache.contentLength = 1000;\n    Pair<Long, Long> contentLengthAndBytesCached =\n        CacheUtil.getCached(\n            new DataSpec(Uri.parse(\"test\")), mockCache, /* cacheKeyFactory= */ null);\n\n    assertThat(contentLengthAndBytesCached.first).isEqualTo(1000);\n    assertThat(contentLengthAndBytesCached.second).isEqualTo(0);\n  }\n\n  @Test\n  public void testGetCached() {\n    mockCache.contentLength = 1000;\n    mockCache.spansAndGaps = new int[] {100, 100, 200};\n    Pair<Long, Long> contentLengthAndBytesCached =\n        CacheUtil.getCached(\n            new DataSpec(Uri.parse(\"test\")), mockCache, /* cacheKeyFactory= */ null);\n\n    assertThat(contentLengthAndBytesCached.first).isEqualTo(1000);\n    assertThat(contentLengthAndBytesCached.second).isEqualTo(300);\n  }\n\n  @Test\n  public void testGetCachedFromNonZeroPosition() {\n    mockCache.contentLength = 1000;\n    mockCache.spansAndGaps = new int[] {100, 100, 200};\n    Pair<Long, Long> contentLengthAndBytesCached =\n        CacheUtil.getCached(\n            new DataSpec(\n                Uri.parse(\"test\"),\n                /* absoluteStreamPosition= */ 100,\n                /* length= */ C.LENGTH_UNSET,\n                /* key= */ null),\n            mockCache,\n            /* cacheKeyFactory= */ null);\n\n    assertThat(contentLengthAndBytesCached.first).isEqualTo(900);\n    assertThat(contentLengthAndBytesCached.second).isEqualTo(200);\n  }\n\n  @Test\n  public void testCache() throws Exception {\n    FakeDataSet fakeDataSet = new FakeDataSet().setRandomData(\"test_data\", 100);\n    FakeDataSource dataSource = new FakeDataSource(fakeDataSet);\n\n    CachingCounters counters = new CachingCounters();\n    CacheUtil.cache(\n        new DataSpec(Uri.parse(\"test_data\")),\n        cache,\n        /* cacheKeyFactory= */ null,\n        dataSource,\n        counters,\n        /* isCanceled= */ null);\n\n    counters.assertValues(0, 100, 100);\n    assertCachedData(cache, fakeDataSet);\n  }\n\n  @Test\n  public void testCacheSetOffsetAndLength() throws Exception {\n    FakeDataSet fakeDataSet = new FakeDataSet().setRandomData(\"test_data\", 100);\n    FakeDataSource dataSource = new FakeDataSource(fakeDataSet);\n\n    Uri testUri = Uri.parse(\"test_data\");\n    DataSpec dataSpec = new DataSpec(testUri, 10, 20, null);\n    CachingCounters counters = new CachingCounters();\n    CacheUtil.cache(\n        dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);\n\n    counters.assertValues(0, 20, 20);\n    counters.reset();\n\n    CacheUtil.cache(\n        new DataSpec(testUri),\n        cache,\n        /* cacheKeyFactory= */ null,\n        dataSource,\n        counters,\n        /* isCanceled= */ null);\n\n    counters.assertValues(20, 80, 100);\n    assertCachedData(cache, fakeDataSet);\n  }\n\n  @Test\n  public void testCacheUnknownLength() throws Exception {\n    FakeDataSet fakeDataSet = new FakeDataSet().newData(\"test_data\")\n        .setSimulateUnknownLength(true)\n        .appendReadData(TestUtil.buildTestData(100)).endData();\n    FakeDataSource dataSource = new FakeDataSource(fakeDataSet);\n\n    DataSpec dataSpec = new DataSpec(Uri.parse(\"test_data\"));\n    CachingCounters counters = new CachingCounters();\n    CacheUtil.cache(\n        dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);\n\n    counters.assertValues(0, 100, 100);\n    assertCachedData(cache, fakeDataSet);\n  }\n\n  @Test\n  public void testCacheUnknownLengthPartialCaching() throws Exception {\n    FakeDataSet fakeDataSet = new FakeDataSet().newData(\"test_data\")\n        .setSimulateUnknownLength(true)\n        .appendReadData(TestUtil.buildTestData(100)).endData();\n    FakeDataSource dataSource = new FakeDataSource(fakeDataSet);\n\n    Uri testUri = Uri.parse(\"test_data\");\n    DataSpec dataSpec = new DataSpec(testUri, 10, 20, null);\n    CachingCounters counters = new CachingCounters();\n    CacheUtil.cache(\n        dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);\n\n    counters.assertValues(0, 20, 20);\n    counters.reset();\n\n    CacheUtil.cache(\n        new DataSpec(testUri),\n        cache,\n        /* cacheKeyFactory= */ null,\n        dataSource,\n        counters,\n        /* isCanceled= */ null);\n\n    counters.assertValues(20, 80, 100);\n    assertCachedData(cache, fakeDataSet);\n  }\n\n  @Test\n  public void testCacheLengthExceedsActualDataLength() throws Exception {\n    FakeDataSet fakeDataSet = new FakeDataSet().setRandomData(\"test_data\", 100);\n    FakeDataSource dataSource = new FakeDataSource(fakeDataSet);\n\n    Uri testUri = Uri.parse(\"test_data\");\n    DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null);\n    CachingCounters counters = new CachingCounters();\n    CacheUtil.cache(\n        dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);\n\n    counters.assertValues(0, 100, 1000);\n    assertCachedData(cache, fakeDataSet);\n  }\n\n  @Test\n  public void testCacheThrowEOFException() throws Exception {\n    FakeDataSet fakeDataSet = new FakeDataSet().setRandomData(\"test_data\", 100);\n    FakeDataSource dataSource = new FakeDataSource(fakeDataSet);\n\n    Uri testUri = Uri.parse(\"test_data\");\n    DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null);\n\n    try {\n      CacheUtil.cache(\n          dataSpec,\n          cache,\n          /* cacheKeyFactory= */ null,\n          new CacheDataSource(cache, dataSource),\n          new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES],\n          /* priorityTaskManager= */ null,\n          /* priority= */ 0,\n          /* progressListener= */ null,\n          /* isCanceled= */ null,\n          /* enableEOFException= */ true);\n      fail();\n    } catch (EOFException e) {\n      // Do nothing.\n    }\n  }\n\n  @Test\n  public void testCachePolling() throws Exception {\n    final CachingCounters counters = new CachingCounters();\n    FakeDataSet fakeDataSet =\n        new FakeDataSet()\n            .newData(\"test_data\")\n            .appendReadData(TestUtil.buildTestData(100))\n            .appendReadAction(() -> counters.assertValues(0, 100, 300))\n            .appendReadData(TestUtil.buildTestData(100))\n            .appendReadAction(() -> counters.assertValues(0, 200, 300))\n            .appendReadData(TestUtil.buildTestData(100))\n            .endData();\n    FakeDataSource dataSource = new FakeDataSource(fakeDataSet);\n\n    CacheUtil.cache(\n        new DataSpec(Uri.parse(\"test_data\")),\n        cache,\n        /* cacheKeyFactory= */ null,\n        dataSource,\n        counters,\n        /* isCanceled= */ null);\n\n    counters.assertValues(0, 300, 300);\n    assertCachedData(cache, fakeDataSet);\n  }\n\n  @Test\n  public void testRemove() throws Exception {\n    FakeDataSet fakeDataSet = new FakeDataSet().setRandomData(\"test_data\", 100);\n    FakeDataSource dataSource = new FakeDataSource(fakeDataSet);\n\n    Uri uri = Uri.parse(\"test_data\");\n    DataSpec dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION);\n    CacheUtil.cache(\n        dataSpec,\n        cache,\n        /* cacheKeyFactory= */ null,\n        // Set fragmentSize to 10 to make sure there are multiple spans.\n        new CacheDataSource(\n            cache,\n            dataSource,\n            new FileDataSource(),\n            new CacheDataSink(cache, /* fragmentSize= */ 10),\n            /* flags= */ 0,\n            /* eventListener= */ null),\n        new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES],\n        /* priorityTaskManager= */ null,\n        /* priority= */ 0,\n        /* progressListener= */ null,\n        /* isCanceled= */ null,\n        true);\n    CacheUtil.remove(dataSpec, cache, /* cacheKeyFactory= */ null);\n\n    assertCacheEmpty(cache);\n  }\n\n  private static final class CachingCounters implements CacheUtil.ProgressListener {\n\n    private long contentLength = C.LENGTH_UNSET;\n    private long bytesAlreadyCached;\n    private long bytesNewlyCached;\n    private boolean seenFirstProgressUpdate;\n\n    @Override\n    public void onProgress(long contentLength, long bytesCached, long newBytesCached) {\n      this.contentLength = contentLength;\n      if (!seenFirstProgressUpdate) {\n        bytesAlreadyCached = bytesCached;\n        seenFirstProgressUpdate = true;\n      }\n      bytesNewlyCached = bytesCached - bytesAlreadyCached;\n    }\n\n    public void assertValues(int bytesAlreadyCached, int bytesNewlyCached, int contentLength) {\n      assertThat(this.bytesAlreadyCached).isEqualTo(bytesAlreadyCached);\n      assertThat(this.bytesNewlyCached).isEqualTo(bytesNewlyCached);\n      assertThat(this.contentLength).isEqualTo(contentLength);\n    }\n\n    public void reset() {\n      contentLength = C.LENGTH_UNSET;\n      bytesAlreadyCached = 0;\n      bytesNewlyCached = 0;\n      seenFirstProgressUpdate = false;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.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 static com.google.common.truth.Truth.assertThat;\nimport static com.google.common.truth.Truth.assertWithMessage;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport android.util.SparseArray;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.util.Collection;\nimport java.util.Set;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests {@link CachedContentIndex}. */\n@RunWith(AndroidJUnit4.class)\npublic class CachedContentIndexTest {\n\n  private final byte[] testIndexV1File = {\n      0, 0, 0, 1, // version\n      0, 0, 0, 0, // flags\n      0, 0, 0, 2, // number_of_CachedContent\n      0, 0, 0, 5, // cache_id 5\n      0, 5, 65, 66, 67, 68, 69, // cache_key \"ABCDE\"\n      0, 0, 0, 0, 0, 0, 0, 10, // original_content_length\n      0, 0, 0, 2, // cache_id 2\n      0, 5, 75, 76, 77, 78, 79, // cache_key \"KLMNO\"\n      0, 0, 0, 0, 0, 0, 10, 0, // original_content_length\n      (byte) 0xF6, (byte) 0xFB, 0x50, 0x41 // hashcode_of_CachedContent_array\n  };\n\n  private final byte[] testIndexV2File = {\n      0, 0, 0, 2, // version\n      0, 0, 0, 0, // flags\n      0, 0, 0, 2, // number_of_CachedContent\n      0, 0, 0, 5, // cache_id 5\n      0, 5, 65, 66, 67, 68, 69, // cache_key \"ABCDE\"\n      0, 0, 0, 2, // metadata count\n      0, 9, 101, 120, 111, 95, 114, 101, 100, 105, 114, // \"exo_redir\"\n      0, 0, 0, 5, // value length\n      97, 98, 99, 100, 101, // Redirected Uri \"abcde\"\n      0, 7, 101, 120, 111, 95, 108, 101, 110, // \"exo_len\"\n      0, 0, 0, 8, // value length\n      0, 0, 0, 0, 0, 0, 0, 10, // original_content_length\n      0, 0, 0, 2, // cache_id 2\n      0, 5, 75, 76, 77, 78, 79, // cache_key \"KLMNO\"\n      0, 0, 0, 1, // metadata count\n      0, 7, 101, 120, 111, 95, 108, 101, 110, // \"exo_len\"\n      0, 0, 0, 8, // value length\n      0, 0, 0, 0, 0, 0, 10, 0, // original_content_length\n      0x12, 0x15, 0x66, (byte) 0x8A // hashcode_of_CachedContent_array\n  };\n  private File cacheDir;\n\n  @Before\n  public void setUp() throws Exception {\n    cacheDir =\n        Util.createTempDirectory(ApplicationProvider.getApplicationContext(), \"ExoPlayerTest\");\n  }\n\n  @After\n  public void tearDown() {\n    Util.recursiveDelete(cacheDir);\n  }\n\n  @Test\n  public void testAddGetRemove() throws Exception {\n    final String key1 = \"key1\";\n    final String key2 = \"key2\";\n    final String key3 = \"key3\";\n\n    CachedContentIndex index = newInstance();\n\n    // Add two CachedContents with add methods\n    CachedContent cachedContent1 = index.getOrAdd(key1);\n    CachedContent cachedContent2 = index.getOrAdd(key2);\n    assertThat(cachedContent1.id != cachedContent2.id).isTrue();\n\n    // add a span\n    int cacheFileLength = 20;\n    File cacheSpanFile =\n        SimpleCacheSpanTest.createCacheSpanFile(\n            cacheDir,\n            cachedContent1.id,\n            /* offset= */ 10,\n            cacheFileLength,\n            /* lastTouchTimestamp= */ 30);\n    SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, cacheFileLength, index);\n    assertThat(span).isNotNull();\n    cachedContent1.addSpan(span);\n\n    // Check if they are added and get method returns null if the key isn't found\n    assertThat(index.get(key1)).isEqualTo(cachedContent1);\n    assertThat(index.get(key2)).isEqualTo(cachedContent2);\n    assertThat(index.get(key3)).isNull();\n\n    // test getAll()\n    Collection<CachedContent> cachedContents = index.getAll();\n    assertThat(cachedContents).containsExactly(cachedContent1, cachedContent2);\n\n    // test getKeys()\n    Set<String> keys = index.getKeys();\n    assertThat(keys).containsExactly(key1, key2);\n\n    // test getKeyForId()\n    assertThat(index.getKeyForId(cachedContent1.id)).isEqualTo(key1);\n    assertThat(index.getKeyForId(cachedContent2.id)).isEqualTo(key2);\n\n    // test remove()\n    index.maybeRemove(key2);\n    index.maybeRemove(key3);\n    assertThat(index.get(key1)).isEqualTo(cachedContent1);\n    assertThat(index.get(key2)).isNull();\n    assertThat(cacheSpanFile.exists()).isTrue();\n\n    // test removeEmpty()\n    index.getOrAdd(key2);\n    index.removeEmpty();\n    assertThat(index.get(key1)).isEqualTo(cachedContent1);\n    assertThat(index.get(key2)).isNull();\n    assertThat(cacheSpanFile.exists()).isTrue();\n  }\n\n  @Test\n  public void testLegacyStoreAndLoad() throws Exception {\n    assertStoredAndLoadedEqual(newLegacyInstance(), newLegacyInstance());\n  }\n\n  @Test\n  public void testLegacyLoadV1() throws Exception {\n    CachedContentIndex index = newLegacyInstance();\n\n    FileOutputStream fos =\n        new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC));\n    fos.write(testIndexV1File);\n    fos.close();\n\n    index.initialize(/* uid= */ 0);\n    assertThat(index.getAll()).hasSize(2);\n\n    assertThat(index.assignIdForKey(\"ABCDE\")).isEqualTo(5);\n    ContentMetadata metadata = index.get(\"ABCDE\").getMetadata();\n    assertThat(ContentMetadata.getContentLength(metadata)).isEqualTo(10);\n\n    assertThat(index.assignIdForKey(\"KLMNO\")).isEqualTo(2);\n    ContentMetadata metadata2 = index.get(\"KLMNO\").getMetadata();\n    assertThat(ContentMetadata.getContentLength(metadata2)).isEqualTo(2560);\n  }\n\n  @Test\n  public void testLegacyLoadV2() throws Exception {\n    CachedContentIndex index = newLegacyInstance();\n\n    FileOutputStream fos =\n        new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC));\n    fos.write(testIndexV2File);\n    fos.close();\n\n    index.initialize(/* uid= */ 0);\n    assertThat(index.getAll()).hasSize(2);\n\n    assertThat(index.assignIdForKey(\"ABCDE\")).isEqualTo(5);\n    ContentMetadata metadata = index.get(\"ABCDE\").getMetadata();\n    assertThat(ContentMetadata.getContentLength(metadata)).isEqualTo(10);\n    assertThat(ContentMetadata.getRedirectedUri(metadata)).isEqualTo(Uri.parse(\"abcde\"));\n\n    assertThat(index.assignIdForKey(\"KLMNO\")).isEqualTo(2);\n    ContentMetadata metadata2 = index.get(\"KLMNO\").getMetadata();\n    assertThat(ContentMetadata.getContentLength(metadata2)).isEqualTo(2560);\n  }\n\n  @Test\n  public void testAssignIdForKeyAndGetKeyForId() {\n    CachedContentIndex index = newInstance();\n    final String key1 = \"key1\";\n    final String key2 = \"key2\";\n    int id1 = index.assignIdForKey(key1);\n    int id2 = index.assignIdForKey(key2);\n    assertThat(index.getKeyForId(id1)).isEqualTo(key1);\n    assertThat(index.getKeyForId(id2)).isEqualTo(key2);\n    assertThat(id1 != id2).isTrue();\n    assertThat(index.assignIdForKey(key1)).isEqualTo(id1);\n    assertThat(index.assignIdForKey(key2)).isEqualTo(id2);\n  }\n\n  @Test\n  public void testGetNewId() {\n    SparseArray<String> idToKey = new SparseArray<>();\n    assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);\n    idToKey.put(10, \"\");\n    assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(11);\n    idToKey.put(Integer.MAX_VALUE, \"\");\n    assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);\n    idToKey.put(0, \"\");\n    assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1);\n  }\n\n  @Test\n  public void testLegacyEncryption() throws Exception {\n    byte[] key = Util.getUtf8Bytes(\"Bar12345Bar12345\"); // 128 bit key\n    byte[] key2 = Util.getUtf8Bytes(\"Foo12345Foo12345\"); // 128 bit key\n\n    assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance(key));\n\n    // Rename the index file from the test above\n    File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC);\n    File file2 = new File(cacheDir, \"file2compare\");\n    assertThat(file1.renameTo(file2)).isTrue();\n\n    // Write a new index file\n    assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance(key));\n\n    assertThat(file1.length()).isEqualTo(file2.length());\n    // Assert file content is different\n    FileInputStream fis1 = new FileInputStream(file1);\n    FileInputStream fis2 = new FileInputStream(file2);\n    for (int b; (b = fis1.read()) == fis2.read(); ) {\n      assertThat(b != -1).isTrue();\n    }\n\n    boolean threw = false;\n    try {\n      assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance(key2));\n    } catch (AssertionError e) {\n      threw = true;\n    }\n    assertWithMessage(\"Encrypted index file can not be read with different encryption key\")\n        .that(threw)\n        .isTrue();\n\n    try {\n      assertStoredAndLoadedEqual(newLegacyInstance(key), newLegacyInstance());\n    } catch (AssertionError e) {\n      threw = true;\n    }\n    assertWithMessage(\"Encrypted index file can not be read without encryption key\")\n        .that(threw)\n        .isTrue();\n\n    // Non encrypted index file can be read even when encryption key provided.\n    assertStoredAndLoadedEqual(newLegacyInstance(), newLegacyInstance(key));\n\n    // Test multiple store() calls\n    CachedContentIndex index = newLegacyInstance(key);\n    index.getOrAdd(\"key3\");\n    index.store();\n    assertStoredAndLoadedEqual(index, newLegacyInstance(key));\n  }\n\n  @Test\n  public void testRemoveEmptyNotLockedCachedContent() {\n    CachedContentIndex index = newInstance();\n    CachedContent cachedContent = index.getOrAdd(\"key1\");\n\n    index.maybeRemove(cachedContent.key);\n\n    assertThat(index.get(cachedContent.key)).isNull();\n  }\n\n  @Test\n  public void testCantRemoveNotEmptyCachedContent() throws Exception {\n    CachedContentIndex index = newInstance();\n\n    CachedContent cachedContent = index.getOrAdd(\"key1\");\n    long cacheFileLength = 20;\n    File cacheFile =\n        SimpleCacheSpanTest.createCacheSpanFile(\n            cacheDir,\n            cachedContent.id,\n            /* offset= */ 10,\n            cacheFileLength,\n            /* lastTouchTimestamp= */ 30);\n    SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheFile, cacheFileLength, index);\n    cachedContent.addSpan(span);\n\n    index.maybeRemove(cachedContent.key);\n\n    assertThat(index.get(cachedContent.key)).isNotNull();\n  }\n\n  @Test\n  public void testCantRemoveLockedCachedContent() {\n    CachedContentIndex index = newInstance();\n    CachedContent cachedContent = index.getOrAdd(\"key1\");\n    cachedContent.setLocked(true);\n\n    index.maybeRemove(cachedContent.key);\n\n    assertThat(index.get(cachedContent.key)).isNotNull();\n  }\n\n  private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)\n      throws IOException {\n    ContentMetadataMutations mutations1 = new ContentMetadataMutations();\n    ContentMetadataMutations.setContentLength(mutations1, 2560);\n    index.getOrAdd(\"KLMNO\").applyMetadataMutations(mutations1);\n    ContentMetadataMutations mutations2 = new ContentMetadataMutations();\n    ContentMetadataMutations.setContentLength(mutations2, 10);\n    ContentMetadataMutations.setRedirectedUri(mutations2, Uri.parse(\"abcde\"));\n    index.getOrAdd(\"ABCDE\").applyMetadataMutations(mutations2);\n    index.store();\n\n    index2.initialize(/* uid= */ 0);\n    Set<String> keys = index.getKeys();\n    Set<String> keys2 = index2.getKeys();\n    assertThat(keys2).isEqualTo(keys);\n    for (String key : keys) {\n      assertThat(index2.get(key)).isEqualTo(index.get(key));\n    }\n  }\n\n  private CachedContentIndex newInstance() {\n    return new CachedContentIndex(TestUtil.getTestDatabaseProvider());\n  }\n\n  private CachedContentIndex newLegacyInstance() {\n    return newLegacyInstance(null);\n  }\n\n  private CachedContentIndex newLegacyInstance(@Nullable byte[] key) {\n    return new CachedContentIndex(\n        /* databaseProvider= */ null,\n        cacheDir,\n        /* legacyStorageSecretKey= */ key,\n        /* legacyStorageEncrypt= */ key != null,\n        /* preferLegacyStorage= */ true);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.anyString;\nimport static org.mockito.Mockito.when;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.extractor.ChunkIndex;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.util.TreeSet;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.MockitoAnnotations;\n\n/** Tests for {@link CachedRegionTracker}. */\n@RunWith(AndroidJUnit4.class)\npublic final class CachedRegionTrackerTest {\n\n  private static final String CACHE_KEY = \"abc\";\n  private static final long MS_IN_US = 1000;\n\n  // 5 chunks, each 20 bytes long and 100 ms long.\n  private static final ChunkIndex CHUNK_INDEX =\n      new ChunkIndex(\n          new int[] {20, 20, 20, 20, 20},\n          new long[] {100, 120, 140, 160, 180},\n          new long[] {\n            100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US\n          },\n          new long[] {0, 100 * MS_IN_US, 200 * MS_IN_US, 300 * MS_IN_US, 400 * MS_IN_US});\n\n  @Mock private Cache cache;\n  private CachedRegionTracker tracker;\n\n  private CachedContentIndex index;\n  private File cacheDir;\n\n  @Before\n  public void setUp() throws Exception {\n    MockitoAnnotations.initMocks(this);\n    when(cache.addListener(anyString(), any(Cache.Listener.class))).thenReturn(new TreeSet<>());\n    tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);\n    cacheDir =\n        Util.createTempDirectory(ApplicationProvider.getApplicationContext(), \"ExoPlayerTest\");\n    index = new CachedContentIndex(TestUtil.getTestDatabaseProvider());\n  }\n\n  @After\n  public void tearDown() throws Exception {\n    Util.recursiveDelete(cacheDir);\n  }\n\n  @Test\n  public void testGetRegion_noSpansInCache() {\n    assertThat(tracker.getRegionEndTimeMs(100)).isEqualTo(CachedRegionTracker.NOT_CACHED);\n    assertThat(tracker.getRegionEndTimeMs(150)).isEqualTo(CachedRegionTracker.NOT_CACHED);\n  }\n\n  @Test\n  public void testGetRegion_fullyCached() throws Exception {\n    tracker.onSpanAdded(cache, newCacheSpan(100, 100));\n\n    assertThat(tracker.getRegionEndTimeMs(101)).isEqualTo(CachedRegionTracker.CACHED_TO_END);\n    assertThat(tracker.getRegionEndTimeMs(121)).isEqualTo(CachedRegionTracker.CACHED_TO_END);\n  }\n\n  @Test\n  public void testGetRegion_partiallyCached() throws Exception {\n    tracker.onSpanAdded(cache, newCacheSpan(100, 40));\n\n    assertThat(tracker.getRegionEndTimeMs(101)).isEqualTo(200);\n    assertThat(tracker.getRegionEndTimeMs(121)).isEqualTo(200);\n  }\n\n  @Test\n  public void testGetRegion_multipleSpanAddsJoinedCorrectly() throws Exception {\n    tracker.onSpanAdded(cache, newCacheSpan(100, 20));\n    tracker.onSpanAdded(cache, newCacheSpan(120, 20));\n\n    assertThat(tracker.getRegionEndTimeMs(101)).isEqualTo(200);\n    assertThat(tracker.getRegionEndTimeMs(121)).isEqualTo(200);\n  }\n\n  @Test\n  public void testGetRegion_fullyCachedThenPartiallyRemoved() throws Exception {\n    // Start with the full stream in cache.\n    tracker.onSpanAdded(cache, newCacheSpan(100, 100));\n\n    // Remove the middle bit.\n    tracker.onSpanRemoved(cache, newCacheSpan(140, 40));\n\n    assertThat(tracker.getRegionEndTimeMs(101)).isEqualTo(200);\n    assertThat(tracker.getRegionEndTimeMs(121)).isEqualTo(200);\n\n    assertThat(tracker.getRegionEndTimeMs(181)).isEqualTo(CachedRegionTracker.CACHED_TO_END);\n  }\n\n  @Test\n  public void testGetRegion_subchunkEstimation() throws Exception {\n    tracker.onSpanAdded(cache, newCacheSpan(100, 10));\n\n    assertThat(tracker.getRegionEndTimeMs(101)).isEqualTo(50);\n    assertThat(tracker.getRegionEndTimeMs(111)).isEqualTo(CachedRegionTracker.NOT_CACHED);\n  }\n\n  private CacheSpan newCacheSpan(int position, int length) throws IOException {\n    int id = index.assignIdForKey(CACHE_KEY);\n    File cacheFile = createCacheSpanFile(cacheDir, id, position, length, 0);\n    return SimpleCacheSpan.createCacheEntry(cacheFile, length, index);\n  }\n\n  public static File createCacheSpanFile(\n      File cacheDir, int id, long offset, int length, long lastTouchTimestamp) throws IOException {\n    File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastTouchTimestamp);\n    createTestFile(cacheFile, length);\n    return cacheFile;\n  }\n\n  private static void createTestFile(File file, int length) throws IOException {\n    FileOutputStream output = new FileOutputStream(file);\n    for (int i = 0; i < length; i++) {\n      output.write(i);\n    }\n    output.close();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests {@link DefaultContentMetadata}. */\n@RunWith(AndroidJUnit4.class)\npublic class DefaultContentMetadataTest {\n\n  private DefaultContentMetadata contentMetadata;\n\n  @Before\n  public void setUp() throws Exception {\n    contentMetadata = createContentMetadata();\n  }\n\n  @Test\n  public void testContainsReturnsFalseWhenEmpty() throws Exception {\n    assertThat(contentMetadata.contains(\"test metadata\")).isFalse();\n  }\n\n  @Test\n  public void testContainsReturnsTrueForInitialValue() throws Exception {\n    contentMetadata = createContentMetadata(\"metadata name\", \"value\");\n    assertThat(contentMetadata.contains(\"metadata name\")).isTrue();\n  }\n\n  @Test\n  public void testGetReturnsDefaultValueWhenValueIsNotAvailable() throws Exception {\n    assertThat(contentMetadata.get(\"metadata name\", \"default value\")).isEqualTo(\"default value\");\n  }\n\n  @Test\n  public void testGetReturnsInitialValue() throws Exception {\n    contentMetadata = createContentMetadata(\"metadata name\", \"value\");\n    assertThat(contentMetadata.get(\"metadata name\", \"default value\")).isEqualTo(\"value\");\n  }\n\n  @Test\n  public void testEmptyMutationDoesNotFail() throws Exception {\n    ContentMetadataMutations mutations = new ContentMetadataMutations();\n    DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations);\n  }\n\n  @Test\n  public void testAddNewMetadata() throws Exception {\n    ContentMetadataMutations mutations = new ContentMetadataMutations();\n    mutations.set(\"metadata name\", \"value\");\n    contentMetadata = contentMetadata.copyWithMutationsApplied(mutations);\n    assertThat(contentMetadata.get(\"metadata name\", \"default value\")).isEqualTo(\"value\");\n  }\n\n  @Test\n  public void testAddNewIntMetadata() throws Exception {\n    ContentMetadataMutations mutations = new ContentMetadataMutations();\n    mutations.set(\"metadata name\", 5);\n    contentMetadata = contentMetadata.copyWithMutationsApplied(mutations);\n    assertThat(contentMetadata.get(\"metadata name\", 0)).isEqualTo(5);\n  }\n\n  @Test\n  public void testAddNewByteArrayMetadata() throws Exception {\n    ContentMetadataMutations mutations = new ContentMetadataMutations();\n    byte[] value = {1, 2, 3};\n    mutations.set(\"metadata name\", value);\n    contentMetadata = contentMetadata.copyWithMutationsApplied(mutations);\n    assertThat(contentMetadata.get(\"metadata name\", new byte[] {})).isEqualTo(value);\n  }\n\n  @Test\n  public void testNewMetadataNotWrittenBeforeCommitted() throws Exception {\n    ContentMetadataMutations mutations = new ContentMetadataMutations();\n    mutations.set(\"metadata name\", \"value\");\n    assertThat(contentMetadata.get(\"metadata name\", \"default value\")).isEqualTo(\"default value\");\n  }\n\n  @Test\n  public void testEditMetadata() throws Exception {\n    contentMetadata = createContentMetadata(\"metadata name\", \"value\");\n    ContentMetadataMutations mutations = new ContentMetadataMutations();\n    mutations.set(\"metadata name\", \"edited value\");\n    contentMetadata = contentMetadata.copyWithMutationsApplied(mutations);\n    assertThat(contentMetadata.get(\"metadata name\", \"default value\")).isEqualTo(\"edited value\");\n  }\n\n  @Test\n  public void testRemoveMetadata() throws Exception {\n    contentMetadata = createContentMetadata(\"metadata name\", \"value\");\n    ContentMetadataMutations mutations = new ContentMetadataMutations();\n    mutations.remove(\"metadata name\");\n    contentMetadata = contentMetadata.copyWithMutationsApplied(mutations);\n    assertThat(contentMetadata.get(\"metadata name\", \"default value\")).isEqualTo(\"default value\");\n  }\n\n  @Test\n  public void testAddAndRemoveMetadata() throws Exception {\n    ContentMetadataMutations mutations = new ContentMetadataMutations();\n    mutations.set(\"metadata name\", \"value\");\n    mutations.remove(\"metadata name\");\n    contentMetadata = contentMetadata.copyWithMutationsApplied(mutations);\n    assertThat(contentMetadata.get(\"metadata name\", \"default value\")).isEqualTo(\"default value\");\n  }\n\n  @Test\n  public void testRemoveAndAddMetadata() throws Exception {\n    ContentMetadataMutations mutations = new ContentMetadataMutations();\n    mutations.remove(\"metadata name\");\n    mutations.set(\"metadata name\", \"value\");\n    contentMetadata = contentMetadata.copyWithMutationsApplied(mutations);\n    assertThat(contentMetadata.get(\"metadata name\", \"default value\")).isEqualTo(\"value\");\n  }\n\n  @Test\n  public void testEqualsStringValues() throws Exception {\n    DefaultContentMetadata metadata1 = createContentMetadata(\"metadata1\", \"value\");\n    DefaultContentMetadata metadata2 = createContentMetadata(\"metadata1\", \"value\");\n    assertThat(metadata1).isEqualTo(metadata2);\n  }\n\n  @Test\n  public void testEquals() throws Exception {\n    DefaultContentMetadata metadata1 =\n        createContentMetadata(\n            \"metadata1\", \"value\", \"metadata2\", 12345, \"metadata3\", new byte[] {1, 2, 3});\n    DefaultContentMetadata metadata2 =\n        createContentMetadata(\n            \"metadata2\", 12345, \"metadata3\", new byte[] {1, 2, 3}, \"metadata1\", \"value\");\n    assertThat(metadata1).isEqualTo(metadata2);\n    assertThat(metadata1.hashCode()).isEqualTo(metadata2.hashCode());\n  }\n\n  @Test\n  public void testNotEquals() throws Exception {\n    DefaultContentMetadata metadata1 = createContentMetadata(\"metadata1\", new byte[] {1, 2, 3});\n    DefaultContentMetadata metadata2 = createContentMetadata(\"metadata1\", new byte[] {3, 2, 1});\n    assertThat(metadata1).isNotEqualTo(metadata2);\n    assertThat(metadata1.hashCode()).isNotEqualTo(metadata2.hashCode());\n  }\n\n  private DefaultContentMetadata createContentMetadata(Object... pairs) {\n    assertThat(pairs.length % 2).isEqualTo(0);\n    ContentMetadataMutations mutations = new ContentMetadataMutations();\n    for (int i = 0; i < pairs.length; i += 2) {\n      String name = (String) pairs[i];\n      Object value = pairs[i + 1];\n      if (value instanceof String) {\n        mutations.set(name, (String) value);\n      } else if (value instanceof byte[]) {\n        mutations.set(name, (byte[]) value);\n      } else if (value instanceof Number) {\n        mutations.set(name, ((Number) value).longValue());\n      } else {\n        throw new IllegalArgumentException();\n      }\n    }\n    return DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.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 androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mockito;\nimport org.mockito.MockitoAnnotations;\n\n/** Unit tests for {@link LeastRecentlyUsedCacheEvictor}. */\n@RunWith(AndroidJUnit4.class)\npublic class LeastRecentlyUsedCacheEvictorTest {\n\n  @Before\n  public void setUp() throws Exception {\n    MockitoAnnotations.initMocks(this);\n  }\n\n  @Test\n  public void testContentBiggerThanMaxSizeDoesNotThrowException() throws Exception {\n    int maxBytes = 100;\n    LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(maxBytes);\n    evictor.onCacheInitialized();\n    evictor.onStartFile(Mockito.mock(Cache.class), \"key\", 0, maxBytes + 1);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\nimport static com.google.common.truth.Truth.assertWithMessage;\n\nimport android.util.LongSparseArray;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.util.Set;\nimport java.util.TreeSet;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link SimpleCacheSpan}. */\n@RunWith(AndroidJUnit4.class)\npublic class SimpleCacheSpanTest {\n\n  public static File createCacheSpanFile(\n      File cacheDir, int id, long offset, long length, long lastTouchTimestamp) throws IOException {\n    File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastTouchTimestamp);\n    createTestFile(cacheFile, length);\n    return cacheFile;\n  }\n\n  private CachedContentIndex index;\n  private File cacheDir;\n\n  @Before\n  public void setUp() throws Exception {\n    cacheDir =\n        Util.createTempDirectory(ApplicationProvider.getApplicationContext(), \"ExoPlayerTest\");\n    index = new CachedContentIndex(TestUtil.getTestDatabaseProvider());\n  }\n\n  @After\n  public void tearDown() {\n    Util.recursiveDelete(cacheDir);\n  }\n\n  @Test\n  public void testCacheFile() throws Exception {\n    assertCacheSpan(\"key1\", 0, 0);\n    assertCacheSpan(\"key2\", 1, 2);\n    assertCacheSpan(\"<>:\\\"/\\\\|?*%\", 1, 2);\n    assertCacheSpan(\"key3\", 1, 2);\n\n    assertNullCacheSpan(new File(\"parent\"), \"key4\", -1, 2);\n    assertNullCacheSpan(new File(\"parent\"), \"key5\", 1, -2);\n\n    assertCacheSpan(\n        \"A newline (line feed) character \\n\"\n            + \"A carriage-return character followed immediately by a newline character \\r\\n\"\n            + \"A standalone carriage-return character \\r\"\n            + \"A next-line character \\u0085\"\n            + \"A line-separator character \\u2028\"\n            + \"A paragraph-separator character \\u2029\", 1, 2);\n  }\n\n  @Test\n  public void testUpgradeFileName() throws Exception {\n    String key = \"abc%def\";\n    int id = index.assignIdForKey(key);\n    File v3file = createTestFile(id + \".0.1.v3.exo\");\n    File v2file = createTestFile(\"abc%25def.1.2.v2.exo\"); // %25 is '%' after escaping\n    File wrongEscapedV2file = createTestFile(\"abc%2Gdef.3.4.v2.exo\"); // 2G is invalid hex\n    File v1File = createTestFile(\"abc%def.5.6.v1.exo\"); // V1 did not escape\n\n    for (File file : cacheDir.listFiles()) {\n      SimpleCacheSpan cacheEntry = SimpleCacheSpan.createCacheEntry(file, file.length(), index);\n      if (file.equals(wrongEscapedV2file)) {\n        assertThat(cacheEntry).isNull();\n      } else {\n        assertThat(cacheEntry).isNotNull();\n      }\n    }\n\n    assertThat(v3file.exists()).isTrue();\n    assertThat(v2file.exists()).isFalse();\n    assertThat(wrongEscapedV2file.exists()).isTrue();\n    assertThat(v1File.exists()).isFalse();\n\n    File[] files = cacheDir.listFiles();\n    assertThat(files).hasLength(4);\n\n    Set<String> keys = index.getKeys();\n    assertWithMessage(\"There should be only one key for all files.\").that(keys).hasSize(1);\n    assertThat(keys).contains(key);\n\n    TreeSet<SimpleCacheSpan> spans = index.get(key).getSpans();\n    assertWithMessage(\"upgradeOldFiles() shouldn't add any spans.\").that(spans.isEmpty()).isTrue();\n\n    LongSparseArray<Long> cachedPositions = new LongSparseArray<>();\n    for (File file : files) {\n      SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(file, file.length(), index);\n      if (cacheSpan != null) {\n        assertThat(cacheSpan.key).isEqualTo(key);\n        cachedPositions.put(cacheSpan.position, cacheSpan.lastTouchTimestamp);\n      }\n    }\n\n    assertThat(cachedPositions.get(0)).isEqualTo(1);\n    assertThat(cachedPositions.get(1)).isEqualTo(2);\n    assertThat(cachedPositions.get(5)).isEqualTo(6);\n  }\n\n  private static void createTestFile(File file, long length) throws IOException {\n    FileOutputStream output = new FileOutputStream(file);\n    for (int i = 0; i < length; i++) {\n      output.write(i);\n    }\n    output.close();\n  }\n\n  private File createTestFile(String name) throws IOException {\n    File file = new File(cacheDir, name);\n    createTestFile(file, 1);\n    return file;\n  }\n\n  private void assertCacheSpan(String key, long offset, long lastTouchTimestamp)\n      throws IOException {\n    int id = index.assignIdForKey(key);\n    long cacheFileLength = 1;\n    File cacheFile = createCacheSpanFile(cacheDir, id, offset, cacheFileLength, lastTouchTimestamp);\n    SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, cacheFileLength, index);\n    String message = cacheFile.toString();\n    assertWithMessage(message).that(cacheSpan).isNotNull();\n    assertWithMessage(message).that(cacheFile.getParentFile()).isEqualTo(cacheDir);\n    assertWithMessage(message).that(cacheSpan.key).isEqualTo(key);\n    assertWithMessage(message).that(cacheSpan.position).isEqualTo(offset);\n    assertWithMessage(message).that(cacheSpan.length).isEqualTo(1);\n    assertWithMessage(message).that(cacheSpan.isCached).isTrue();\n    assertWithMessage(message).that(cacheSpan.file).isEqualTo(cacheFile);\n    assertWithMessage(message).that(cacheSpan.lastTouchTimestamp).isEqualTo(lastTouchTimestamp);\n  }\n\n  private void assertNullCacheSpan(File parent, String key, long offset, long lastTouchTimestamp) {\n    long cacheFileLength = 0;\n    File cacheFile =\n        SimpleCacheSpan.getCacheFile(parent, index.assignIdForKey(key), offset, lastTouchTimestamp);\n    CacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, cacheFileLength, index);\n    assertWithMessage(cacheFile.toString()).that(cacheSpan).isNull();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.android.exoplayer2.C.LENGTH_UNSET;\nimport static com.google.android.exoplayer2.util.Util.toByteArray;\nimport static com.google.common.truth.Truth.assertThat;\nimport static com.google.common.truth.Truth.assertWithMessage;\nimport static org.mockito.Mockito.doAnswer;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.upstream.cache.Cache.CacheException;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.util.NavigableSet;\nimport java.util.Random;\nimport java.util.Set;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mockito;\nimport org.mockito.MockitoAnnotations;\n\n/** Unit tests for {@link SimpleCache}. */\n@RunWith(AndroidJUnit4.class)\npublic class SimpleCacheTest {\n\n  private static final String KEY_1 = \"key1\";\n  private static final String KEY_2 = \"key2\";\n\n  private File cacheDir;\n\n  @Before\n  public void setUp() throws Exception {\n    MockitoAnnotations.initMocks(this);\n    cacheDir = Util.createTempFile(ApplicationProvider.getApplicationContext(), \"ExoPlayerTest\");\n    // Delete the file. SimpleCache initialization should create a directory with the same name.\n    assertThat(cacheDir.delete()).isTrue();\n  }\n\n  @After\n  public void tearDown() {\n    Util.recursiveDelete(cacheDir);\n  }\n\n  @Test\n  public void testCacheInitialization() {\n    SimpleCache cache = getSimpleCache();\n\n    // Cache initialization should have created a non-negative UID.\n    long uid = cache.getUid();\n    assertThat(uid).isAtLeast(0L);\n    // And the cache directory.\n    assertThat(cacheDir.exists()).isTrue();\n\n    // Reinitialization should load the same non-negative UID.\n    cache.release();\n    cache = getSimpleCache();\n    assertThat(cache.getUid()).isEqualTo(uid);\n  }\n\n  @Test\n  public void testCacheInitializationError() throws IOException {\n    // Creating a file where the cache should be will cause an error during initialization.\n    assertThat(cacheDir.createNewFile()).isTrue();\n\n    // Cache initialization should not throw an exception, but no UID will be generated.\n    SimpleCache cache = getSimpleCache();\n    long uid = cache.getUid();\n    assertThat(uid).isEqualTo(-1L);\n  }\n\n  @Test\n  public void testCommittingOneFile() throws Exception {\n    SimpleCache simpleCache = getSimpleCache();\n\n    CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);\n    assertThat(cacheSpan1.isCached).isFalse();\n    assertThat(cacheSpan1.isOpenEnded()).isTrue();\n\n    assertThat(simpleCache.startReadWriteNonBlocking(KEY_1, 0)).isNull();\n\n    NavigableSet<CacheSpan> cachedSpans = simpleCache.getCachedSpans(KEY_1);\n    assertThat(cachedSpans.isEmpty()).isTrue();\n    assertThat(simpleCache.getCacheSpace()).isEqualTo(0);\n    assertNoCacheFiles(cacheDir);\n\n    addCache(simpleCache, KEY_1, 0, 15);\n\n    Set<String> cachedKeys = simpleCache.getKeys();\n    assertThat(cachedKeys).containsExactly(KEY_1);\n    cachedSpans = simpleCache.getCachedSpans(KEY_1);\n    assertThat(cachedSpans).contains(cacheSpan1);\n    assertThat(simpleCache.getCacheSpace()).isEqualTo(15);\n\n    simpleCache.releaseHoleSpan(cacheSpan1);\n\n    CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);\n    assertThat(cacheSpan2.isCached).isTrue();\n    assertThat(cacheSpan2.isOpenEnded()).isFalse();\n    assertThat(cacheSpan2.length).isEqualTo(15);\n    assertCachedDataReadCorrect(cacheSpan2);\n  }\n\n  @Test\n  public void testReadCacheWithoutReleasingWriteCacheSpan() throws Exception {\n    SimpleCache simpleCache = getSimpleCache();\n\n    CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);\n    addCache(simpleCache, KEY_1, 0, 15);\n    CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);\n    assertCachedDataReadCorrect(cacheSpan2);\n    simpleCache.releaseHoleSpan(cacheSpan1);\n  }\n\n  @Test\n  public void testSetGetContentMetadata() throws Exception {\n    SimpleCache simpleCache = getSimpleCache();\n\n    assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1)))\n        .isEqualTo(LENGTH_UNSET);\n\n    ContentMetadataMutations mutations = new ContentMetadataMutations();\n    ContentMetadataMutations.setContentLength(mutations, 15);\n    simpleCache.applyContentMetadataMutations(KEY_1, mutations);\n    assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1)))\n        .isEqualTo(15);\n\n    simpleCache.startReadWrite(KEY_1, 0);\n    addCache(simpleCache, KEY_1, 0, 15);\n\n    mutations = new ContentMetadataMutations();\n    ContentMetadataMutations.setContentLength(mutations, 150);\n    simpleCache.applyContentMetadataMutations(KEY_1, mutations);\n    assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1)))\n        .isEqualTo(150);\n\n    addCache(simpleCache, KEY_1, 140, 10);\n\n    simpleCache.release();\n\n    // Check if values are kept after cache is reloaded.\n    SimpleCache simpleCache2 = getSimpleCache();\n    assertThat(ContentMetadata.getContentLength(simpleCache2.getContentMetadata(KEY_1)))\n        .isEqualTo(150);\n\n    // Removing the last span shouldn't cause the length be change next time cache loaded\n    SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145);\n    simpleCache2.removeSpan(lastSpan);\n    simpleCache2.release();\n    simpleCache2 = getSimpleCache();\n    assertThat(ContentMetadata.getContentLength(simpleCache2.getContentMetadata(KEY_1)))\n        .isEqualTo(150);\n  }\n\n  @Test\n  public void testReloadCache() throws Exception {\n    SimpleCache simpleCache = getSimpleCache();\n\n    // write data\n    CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);\n    addCache(simpleCache, KEY_1, 0, 15);\n    simpleCache.releaseHoleSpan(cacheSpan1);\n    simpleCache.release();\n\n    // Reload cache\n    simpleCache = getSimpleCache();\n\n    // read data back\n    CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);\n    assertCachedDataReadCorrect(cacheSpan2);\n  }\n\n  @Test\n  public void testReloadCacheWithoutRelease() throws Exception {\n    SimpleCache simpleCache = getSimpleCache();\n\n    // Write data for KEY_1.\n    CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);\n    addCache(simpleCache, KEY_1, 0, 15);\n    simpleCache.releaseHoleSpan(cacheSpan1);\n    // Write and remove data for KEY_2.\n    CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_2, 0);\n    addCache(simpleCache, KEY_2, 0, 15);\n    simpleCache.releaseHoleSpan(cacheSpan2);\n    simpleCache.removeSpan(simpleCache.getCachedSpans(KEY_2).first());\n\n    // Don't release the cache. This means the index file wont have been written to disk after the\n    // data for KEY_2 was removed. Move the cache instead, so we can reload it without failing the\n    // folder locking check.\n    File cacheDir2 =\n        Util.createTempFile(ApplicationProvider.getApplicationContext(), \"ExoPlayerTest\");\n    cacheDir2.delete();\n    cacheDir.renameTo(cacheDir2);\n\n    // Reload the cache from its new location.\n    simpleCache = new SimpleCache(cacheDir2, new NoOpCacheEvictor());\n\n    // Read data back for KEY_1.\n    CacheSpan cacheSpan3 = simpleCache.startReadWrite(KEY_1, 0);\n    assertCachedDataReadCorrect(cacheSpan3);\n\n    // Check the entry for KEY_2 was removed when the cache was reloaded.\n    assertThat(simpleCache.getCachedSpans(KEY_2)).isEmpty();\n\n    Util.recursiveDelete(cacheDir2);\n  }\n\n  @Test\n  public void testEncryptedIndex() throws Exception {\n    byte[] key = Util.getUtf8Bytes(\"Bar12345Bar12345\"); // 128 bit key\n    SimpleCache simpleCache = getEncryptedSimpleCache(key);\n\n    // write data\n    CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);\n    addCache(simpleCache, KEY_1, 0, 15);\n    simpleCache.releaseHoleSpan(cacheSpan1);\n    simpleCache.release();\n\n    // Reload cache\n    simpleCache = getEncryptedSimpleCache(key);\n\n    // read data back\n    CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);\n    assertCachedDataReadCorrect(cacheSpan2);\n  }\n\n  @Test\n  public void testEncryptedIndexWrongKey() throws Exception {\n    byte[] key = Util.getUtf8Bytes(\"Bar12345Bar12345\"); // 128 bit key\n    SimpleCache simpleCache = getEncryptedSimpleCache(key);\n\n    // write data\n    CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);\n    addCache(simpleCache, KEY_1, 0, 15);\n    simpleCache.releaseHoleSpan(cacheSpan1);\n    simpleCache.release();\n\n    // Reload cache\n    byte[] key2 = Util.getUtf8Bytes(\"Foo12345Foo12345\"); // 128 bit key\n    simpleCache = getEncryptedSimpleCache(key2);\n\n    // Cache should be cleared\n    assertThat(simpleCache.getKeys()).isEmpty();\n    assertNoCacheFiles(cacheDir);\n  }\n\n  @Test\n  public void testEncryptedIndexLostKey() throws Exception {\n    byte[] key = Util.getUtf8Bytes(\"Bar12345Bar12345\"); // 128 bit key\n    SimpleCache simpleCache = getEncryptedSimpleCache(key);\n\n    // write data\n    CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);\n    addCache(simpleCache, KEY_1, 0, 15);\n    simpleCache.releaseHoleSpan(cacheSpan1);\n    simpleCache.release();\n\n    // Reload cache\n    simpleCache = getSimpleCache();\n\n    // Cache should be cleared\n    assertThat(simpleCache.getKeys()).isEmpty();\n    assertNoCacheFiles(cacheDir);\n  }\n\n  @Test\n  public void testGetCachedLength() throws Exception {\n    SimpleCache simpleCache = getSimpleCache();\n    CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);\n\n    // No cached bytes, returns -'length'\n    assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(-100);\n\n    // Position value doesn't affect the return value\n    assertThat(simpleCache.getCachedLength(KEY_1, 20, 100)).isEqualTo(-100);\n\n    addCache(simpleCache, KEY_1, 0, 15);\n\n    // Returns the length of a single span\n    assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(15);\n\n    // Value is capped by the 'length'\n    assertThat(simpleCache.getCachedLength(KEY_1, 0, 10)).isEqualTo(10);\n\n    addCache(simpleCache, KEY_1, 15, 35);\n\n    // Returns the length of two adjacent spans\n    assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(50);\n\n    addCache(simpleCache, KEY_1, 60, 10);\n\n    // Not adjacent span doesn't affect return value\n    assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(50);\n\n    // Returns length of hole up to the next cached span\n    assertThat(simpleCache.getCachedLength(KEY_1, 55, 100)).isEqualTo(-5);\n\n    simpleCache.releaseHoleSpan(cacheSpan);\n  }\n\n  /* Tests https://github.com/google/ExoPlayer/issues/3260 case. */\n  @Test\n  public void testExceptionDuringEvictionByLeastRecentlyUsedCacheEvictorNotHang() throws Exception {\n    CachedContentIndex contentIndex =\n        Mockito.spy(new CachedContentIndex(TestUtil.getTestDatabaseProvider()));\n    SimpleCache simpleCache =\n        new SimpleCache(\n            cacheDir, new LeastRecentlyUsedCacheEvictor(20), contentIndex, /* fileIndex= */ null);\n\n    // Add some content.\n    CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);\n    addCache(simpleCache, KEY_1, 0, 15);\n\n    // Make index.store() throw exception from now on.\n    doAnswer(\n            invocation -> {\n              throw new CacheException(\"SimpleCacheTest\");\n            })\n        .when(contentIndex)\n        .store();\n\n    // Adding more content will make LeastRecentlyUsedCacheEvictor evict previous content.\n    try {\n      addCache(simpleCache, KEY_1, 15, 15);\n      assertWithMessage(\"Exception was expected\").fail();\n    } catch (CacheException e) {\n      // do nothing.\n    }\n\n    simpleCache.releaseHoleSpan(cacheSpan);\n\n    // Although store() has failed, it should remove the first span and add the new one.\n    NavigableSet<CacheSpan> cachedSpans = simpleCache.getCachedSpans(KEY_1);\n    assertThat(cachedSpans).isNotEmpty();\n    assertThat(cachedSpans).hasSize(1);\n    assertThat(cachedSpans.pollFirst().position).isEqualTo(15);\n  }\n\n  @Test\n  public void testUsingReleasedSimpleCacheThrowsException() throws Exception {\n    SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());\n    simpleCache.release();\n\n    try {\n      simpleCache.startReadWriteNonBlocking(KEY_1, 0);\n      assertWithMessage(\"Exception was expected\").fail();\n    } catch (RuntimeException e) {\n      // Expected. Do nothing.\n    }\n  }\n\n  @Test\n  public void testMultipleSimpleCacheWithSameCacheDirThrowsException() throws Exception {\n    new SimpleCache(cacheDir, new NoOpCacheEvictor());\n\n    try {\n      new SimpleCache(cacheDir, new NoOpCacheEvictor());\n      assertWithMessage(\"Exception was expected\").fail();\n    } catch (IllegalStateException e) {\n      // Expected. Do nothing.\n    }\n  }\n\n  @Test\n  public void testMultipleSimpleCacheWithSameCacheDirDoesNotThrowsExceptionAfterRelease()\n      throws Exception {\n    SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());\n    simpleCache.release();\n\n    new SimpleCache(cacheDir, new NoOpCacheEvictor());\n  }\n\n  private SimpleCache getSimpleCache() {\n    return new SimpleCache(cacheDir, new NoOpCacheEvictor());\n  }\n\n  private SimpleCache getEncryptedSimpleCache(byte[] secretKey) {\n    return new SimpleCache(cacheDir, new NoOpCacheEvictor(), secretKey);\n  }\n\n  private static void addCache(SimpleCache simpleCache, String key, int position, int length)\n      throws IOException {\n    File file = simpleCache.startFile(key, position, length);\n    FileOutputStream fos = new FileOutputStream(file);\n    try {\n      fos.write(generateData(key, position, length));\n    } finally {\n      fos.close();\n    }\n    simpleCache.commitFile(file, length);\n  }\n\n  private static void assertCachedDataReadCorrect(CacheSpan cacheSpan) throws IOException {\n    assertThat(cacheSpan.isCached).isTrue();\n    byte[] expected = generateData(cacheSpan.key, (int) cacheSpan.position, (int) cacheSpan.length);\n    FileInputStream inputStream = new FileInputStream(cacheSpan.file);\n    try {\n      assertThat(toByteArray(inputStream)).isEqualTo(expected);\n    } finally {\n      inputStream.close();\n    }\n  }\n\n  private static void assertNoCacheFiles(File dir) {\n    File[] files = dir.listFiles();\n    if (files == null) {\n      return;\n    }\n    for (File file : files) {\n      if (file.isDirectory()) {\n        assertNoCacheFiles(file);\n      } else {\n        assertThat(file.getName().endsWith(SimpleCacheSpan.COMMON_SUFFIX)).isFalse();\n      }\n    }\n  }\n\n  private static byte[] generateData(String key, int position, int length) {\n    byte[] bytes = new byte[length];\n    new Random((long) (key.hashCode() ^ position)).nextBytes(bytes);\n    return bytes;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Random;\nimport javax.crypto.Cipher;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link AesFlushingCipher}. */\n@RunWith(AndroidJUnit4.class)\npublic class AesFlushingCipherTest {\n\n  private static final int DATA_LENGTH = 65536;\n  private static final byte[] KEY = Util.getUtf8Bytes(\"testKey:12345678\");\n  private static final long NONCE = 0;\n  private static final long START_OFFSET = 11;\n  private static final long RANDOM_SEED = 0x12345678;\n\n  private AesFlushingCipher encryptCipher;\n  private AesFlushingCipher decryptCipher;\n\n  @Before\n  public void setUp() {\n    encryptCipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, KEY, NONCE, START_OFFSET);\n    decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, START_OFFSET);\n  }\n\n  @After\n  public void tearDown() {\n    encryptCipher = null;\n    decryptCipher = null;\n  }\n\n  private static long getMaxUnchangedBytesAllowedPostEncryption(long length) {\n    // Assuming that not more than 10% of the resultant bytes should be identical.\n    // The value of 10% is arbitrary, ciphers standards do not name a value.\n    return length / 10;\n  }\n\n  // Count the number of bytes that do not match.\n  private static int getDifferingByteCount(byte[] data1, byte[] data2, int startOffset) {\n    int count = 0;\n    for (int i = startOffset; i < data1.length; i++) {\n      if (data1[i] != data2[i]) {\n        count++;\n      }\n    }\n    return count;\n  }\n\n  // Count the number of bytes that do not match.\n  private static int getDifferingByteCount(byte[] data1, byte[] data2) {\n    return getDifferingByteCount(data1, data2, 0);\n  }\n\n  // Test a single encrypt and decrypt call.\n  @Test\n  public void testSingle() {\n    byte[] reference = TestUtil.buildTestData(DATA_LENGTH);\n    byte[] data = reference.clone();\n\n    encryptCipher.updateInPlace(data, 0, data.length);\n    int unchangedByteCount = data.length - getDifferingByteCount(reference, data);\n    assertThat(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length))\n        .isTrue();\n\n    decryptCipher.updateInPlace(data, 0, data.length);\n    int differingByteCount = getDifferingByteCount(reference, data);\n    assertThat(differingByteCount).isEqualTo(0);\n  }\n\n  // Test several encrypt and decrypt calls, each aligned on a 16 byte block size.\n  @Test\n  public void testAligned() {\n    byte[] reference = TestUtil.buildTestData(DATA_LENGTH);\n    byte[] data = reference.clone();\n    Random random = new Random(RANDOM_SEED);\n\n    int offset = 0;\n    while (offset < data.length) {\n      int bytes = (1 + random.nextInt(50)) * 16;\n      bytes = Math.min(bytes, data.length - offset);\n      assertThat(bytes % 16).isEqualTo(0);\n      encryptCipher.updateInPlace(data, offset, bytes);\n      offset += bytes;\n    }\n\n    int unchangedByteCount = data.length - getDifferingByteCount(reference, data);\n    assertThat(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length))\n        .isTrue();\n\n    offset = 0;\n    while (offset < data.length) {\n      int bytes = (1 + random.nextInt(50)) * 16;\n      bytes = Math.min(bytes, data.length - offset);\n      assertThat(bytes % 16).isEqualTo(0);\n      decryptCipher.updateInPlace(data, offset, bytes);\n      offset += bytes;\n    }\n\n    int differingByteCount = getDifferingByteCount(reference, data);\n    assertThat(differingByteCount).isEqualTo(0);\n  }\n\n  // Test several encrypt and decrypt calls, not aligned on block boundary.\n  @Test\n  public void testUnAligned() {\n    byte[] reference = TestUtil.buildTestData(DATA_LENGTH);\n    byte[] data = reference.clone();\n    Random random = new Random(RANDOM_SEED);\n\n    // Encrypt\n    int offset = 0;\n    while (offset < data.length) {\n      int bytes = 1 + random.nextInt(4095);\n      bytes = Math.min(bytes, data.length - offset);\n      encryptCipher.updateInPlace(data, offset, bytes);\n      offset += bytes;\n    }\n\n    int unchangedByteCount = data.length - getDifferingByteCount(reference, data);\n    assertThat(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length))\n        .isTrue();\n\n    offset = 0;\n    while (offset < data.length) {\n      int bytes = 1 + random.nextInt(4095);\n      bytes = Math.min(bytes, data.length - offset);\n      decryptCipher.updateInPlace(data, offset, bytes);\n      offset += bytes;\n    }\n\n    int differingByteCount = getDifferingByteCount(reference, data);\n    assertThat(differingByteCount).isEqualTo(0);\n  }\n\n  // Test decryption starting from the middle of an encrypted block.\n  @Test\n  public void testMidJoin() {\n    byte[] reference = TestUtil.buildTestData(DATA_LENGTH);\n    byte[] data = reference.clone();\n    Random random = new Random(RANDOM_SEED);\n\n    // Encrypt\n    int offset = 0;\n    while (offset < data.length) {\n      int bytes = 1 + random.nextInt(4095);\n      bytes = Math.min(bytes, data.length - offset);\n      encryptCipher.updateInPlace(data, offset, bytes);\n      offset += bytes;\n    }\n\n    // Verify\n    int unchangedByteCount = data.length - getDifferingByteCount(reference, data);\n    assertThat(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length))\n        .isTrue();\n\n    // Setup decryption from random location\n    offset = random.nextInt(4096);\n    decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, offset + START_OFFSET);\n    int remainingLength = data.length - offset;\n    int originalOffset = offset;\n\n    // Decrypt\n    while (remainingLength > 0) {\n      int bytes = 1 + random.nextInt(4095);\n      bytes = Math.min(bytes, remainingLength);\n      decryptCipher.updateInPlace(data, offset, bytes);\n      offset += bytes;\n      remainingLength -= bytes;\n    }\n\n    // Verify\n    int differingByteCount = getDifferingByteCount(reference, data, originalOffset);\n    assertThat(differingByteCount).isEqualTo(0);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests {@link AtomicFile}. */\n@RunWith(AndroidJUnit4.class)\npublic final class AtomicFileTest {\n\n  private File tempFolder;\n  private File file;\n  private AtomicFile atomicFile;\n\n  @Before\n  public void setUp() throws Exception {\n    tempFolder =\n        Util.createTempDirectory(ApplicationProvider.getApplicationContext(), \"ExoPlayerTest\");\n    file = new File(tempFolder, \"atomicFile\");\n    atomicFile = new AtomicFile(file);\n  }\n\n  @After\n  public void tearDown() throws Exception {\n    Util.recursiveDelete(tempFolder);\n  }\n\n  @Test\n  public void testDelete() throws Exception {\n    assertThat(file.createNewFile()).isTrue();\n    atomicFile.delete();\n    assertThat(file.exists()).isFalse();\n  }\n\n  @Test\n  public void testWriteRead() throws Exception {\n    OutputStream output = atomicFile.startWrite();\n    output.write(5);\n    atomicFile.endWrite(output);\n    output.close();\n\n    assertRead();\n\n    output = atomicFile.startWrite();\n    output.write(5);\n    output.write(6);\n    output.close();\n\n    assertRead();\n\n    output = atomicFile.startWrite();\n    output.write(6);\n\n    assertRead();\n    output.close();\n\n    output = atomicFile.startWrite();\n\n    assertRead();\n    output.close();\n  }\n\n  private void assertRead() throws IOException {\n    InputStream input = atomicFile.openRead();\n    assertThat(input.read()).isEqualTo(5);\n    assertThat(input.read()).isEqualTo(-1);\n    input.close();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.graphics.Color.BLACK;\nimport static android.graphics.Color.RED;\nimport static android.graphics.Color.WHITE;\nimport static android.graphics.Color.argb;\nimport static android.graphics.Color.parseColor;\nimport static com.google.android.exoplayer2.util.ColorParser.parseTtmlColor;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.graphics.Color;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link ColorParser}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ColorParserTest {\n\n  // Negative tests.\n\n  @Test(expected = IllegalArgumentException.class)\n  public void testParseUnknownColor() {\n    ColorParser.parseTtmlColor(\"colorOfAnElectron\");\n  }\n\n  @Test(expected = IllegalArgumentException.class)\n  public void testParseNull() {\n    ColorParser.parseTtmlColor(null);\n  }\n\n  @Test(expected = IllegalArgumentException.class)\n  public void testParseEmpty() {\n    ColorParser.parseTtmlColor(\"\");\n  }\n\n  @Test(expected = IllegalArgumentException.class)\n  public void testRgbColorParsingRgbValuesNegative() {\n    ColorParser.parseTtmlColor(\"rgb(-4, 55, 209)\");\n  }\n\n  // Positive tests.\n\n  @Test\n  public void testHexCodeParsing() {\n    assertThat(parseTtmlColor(\"#FFFFFF\")).isEqualTo(WHITE);\n    assertThat(parseTtmlColor(\"#FFFFFFFF\")).isEqualTo(WHITE);\n    assertThat(parseTtmlColor(\"#123456\")).isEqualTo(parseColor(\"#FF123456\"));\n    // Hex colors in ColorParser are RGBA, where-as {@link Color#parseColor} takes ARGB.\n    assertThat(parseTtmlColor(\"#FFFFFF00\")).isEqualTo(parseColor(\"#00FFFFFF\"));\n    assertThat(parseTtmlColor(\"#12345678\")).isEqualTo(parseColor(\"#78123456\"));\n  }\n\n  @Test\n  public void testRgbColorParsing() {\n    assertThat(parseTtmlColor(\"rgb(255,255,255)\")).isEqualTo(WHITE);\n    // Spaces are ignored.\n    assertThat(parseTtmlColor(\"   rgb (      255, 255, 255)\")).isEqualTo(WHITE);\n  }\n\n  @Test\n  public void testRgbColorParsingRgbValuesOutOfBounds() {\n    int outOfBounds = ColorParser.parseTtmlColor(\"rgb(999, 999, 999)\");\n    int color = Color.rgb(999, 999, 999);\n    // Behave like the framework does.\n    assertThat(outOfBounds).isEqualTo(color);\n  }\n\n  @Test\n  public void testRgbaColorParsing() {\n    assertThat(parseTtmlColor(\"rgba(255,255,255,255)\")).isEqualTo(WHITE);\n    assertThat(parseTtmlColor(\"rgba(255,255,255,255)\"))\n        .isEqualTo(argb(255, 255, 255, 255));\n    assertThat(parseTtmlColor(\"rgba(0, 0, 0, 255)\")).isEqualTo(BLACK);\n    assertThat(parseTtmlColor(\"rgba(0, 0, 255, 0)\"))\n        .isEqualTo(argb(0, 0, 0, 255));\n    assertThat(parseTtmlColor(\"rgba(255, 0, 0, 255)\")).isEqualTo(RED);\n    assertThat(parseTtmlColor(\"rgba(255, 0, 255, 0)\"))\n        .isEqualTo(argb(0, 255, 0, 255));\n    assertThat(parseTtmlColor(\"rgba(255, 0, 0, 205)\"))\n        .isEqualTo(argb(205, 255, 0, 0));\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.flac.VorbisComment;\nimport java.util.ArrayList;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link FlacStreamMetadata}. */\n@RunWith(AndroidJUnit4.class)\npublic final class FlacStreamMetadataTest {\n\n  @Test\n  public void parseVorbisComments() {\n    ArrayList<String> commentsList = new ArrayList<>();\n    commentsList.add(\"Title=Song\");\n    commentsList.add(\"Artist=Singer\");\n\n    Metadata metadata =\n        new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata;\n\n    assertThat(metadata.length()).isEqualTo(2);\n    VorbisComment commentFrame = (VorbisComment) metadata.get(0);\n    assertThat(commentFrame.key).isEqualTo(\"Title\");\n    assertThat(commentFrame.value).isEqualTo(\"Song\");\n    commentFrame = (VorbisComment) metadata.get(1);\n    assertThat(commentFrame.key).isEqualTo(\"Artist\");\n    assertThat(commentFrame.value).isEqualTo(\"Singer\");\n  }\n\n  @Test\n  public void parseEmptyVorbisComments() {\n    ArrayList<String> commentsList = new ArrayList<>();\n\n    Metadata metadata =\n        new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata;\n\n    assertThat(metadata).isNull();\n  }\n\n  @Test\n  public void parseVorbisCommentWithEqualsInValue() {\n    ArrayList<String> commentsList = new ArrayList<>();\n    commentsList.add(\"Title=So=ng\");\n\n    Metadata metadata =\n        new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata;\n\n    assertThat(metadata.length()).isEqualTo(1);\n    VorbisComment commentFrame = (VorbisComment) metadata.get(0);\n    assertThat(commentFrame.key).isEqualTo(\"Title\");\n    assertThat(commentFrame.value).isEqualTo(\"So=ng\");\n  }\n\n  @Test\n  public void parseInvalidVorbisComment() {\n    ArrayList<String> commentsList = new ArrayList<>();\n    commentsList.add(\"TitleSong\");\n    commentsList.add(\"Artist=Singer\");\n\n    Metadata metadata =\n        new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata;\n\n    assertThat(metadata.length()).isEqualTo(1);\n    VorbisComment commentFrame = (VorbisComment) metadata.get(0);\n    assertThat(commentFrame.key).isEqualTo(\"Artist\");\n    assertThat(commentFrame.value).isEqualTo(\"Singer\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.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 com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link MimeTypes}. */\n@RunWith(AndroidJUnit4.class)\npublic final class MimeTypesTest {\n\n  @Test\n  public void testGetMediaMimeType_fromValidCodecs_returnsCorrectMimeType() {\n    assertThat(MimeTypes.getMediaMimeType(\"avc1\")).isEqualTo(MimeTypes.VIDEO_H264);\n    assertThat(MimeTypes.getMediaMimeType(\"avc1.42E01E\")).isEqualTo(MimeTypes.VIDEO_H264);\n    assertThat(MimeTypes.getMediaMimeType(\"avc1.42E01F\")).isEqualTo(MimeTypes.VIDEO_H264);\n    assertThat(MimeTypes.getMediaMimeType(\"avc1.4D401F\")).isEqualTo(MimeTypes.VIDEO_H264);\n    assertThat(MimeTypes.getMediaMimeType(\"avc1.4D4028\")).isEqualTo(MimeTypes.VIDEO_H264);\n    assertThat(MimeTypes.getMediaMimeType(\"avc1.640028\")).isEqualTo(MimeTypes.VIDEO_H264);\n    assertThat(MimeTypes.getMediaMimeType(\"avc1.640029\")).isEqualTo(MimeTypes.VIDEO_H264);\n    assertThat(MimeTypes.getMediaMimeType(\"avc3\")).isEqualTo(MimeTypes.VIDEO_H264);\n    assertThat(MimeTypes.getMediaMimeType(\"hev1\")).isEqualTo(MimeTypes.VIDEO_H265);\n    assertThat(MimeTypes.getMediaMimeType(\"hvc1\")).isEqualTo(MimeTypes.VIDEO_H265);\n    assertThat(MimeTypes.getMediaMimeType(\"vp08\")).isEqualTo(MimeTypes.VIDEO_VP8);\n    assertThat(MimeTypes.getMediaMimeType(\"vp8\")).isEqualTo(MimeTypes.VIDEO_VP8);\n    assertThat(MimeTypes.getMediaMimeType(\"vp09\")).isEqualTo(MimeTypes.VIDEO_VP9);\n    assertThat(MimeTypes.getMediaMimeType(\"vp9\")).isEqualTo(MimeTypes.VIDEO_VP9);\n\n    assertThat(MimeTypes.getMediaMimeType(\"ac-3\")).isEqualTo(MimeTypes.AUDIO_AC3);\n    assertThat(MimeTypes.getMediaMimeType(\"dac3\")).isEqualTo(MimeTypes.AUDIO_AC3);\n    assertThat(MimeTypes.getMediaMimeType(\"dec3\")).isEqualTo(MimeTypes.AUDIO_E_AC3);\n    assertThat(MimeTypes.getMediaMimeType(\"ec-3\")).isEqualTo(MimeTypes.AUDIO_E_AC3);\n    assertThat(MimeTypes.getMediaMimeType(\"ec+3\")).isEqualTo(MimeTypes.AUDIO_E_AC3_JOC);\n    assertThat(MimeTypes.getMediaMimeType(\"dtsc\")).isEqualTo(MimeTypes.AUDIO_DTS);\n    assertThat(MimeTypes.getMediaMimeType(\"dtse\")).isEqualTo(MimeTypes.AUDIO_DTS);\n    assertThat(MimeTypes.getMediaMimeType(\"dtsh\")).isEqualTo(MimeTypes.AUDIO_DTS_HD);\n    assertThat(MimeTypes.getMediaMimeType(\"dtsl\")).isEqualTo(MimeTypes.AUDIO_DTS_HD);\n    assertThat(MimeTypes.getMediaMimeType(\"opus\")).isEqualTo(MimeTypes.AUDIO_OPUS);\n    assertThat(MimeTypes.getMediaMimeType(\"vorbis\")).isEqualTo(MimeTypes.AUDIO_VORBIS);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a\")).isEqualTo(MimeTypes.AUDIO_AAC);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.40.02\")).isEqualTo(MimeTypes.AUDIO_AAC);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.40.05\")).isEqualTo(MimeTypes.AUDIO_AAC);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.40.2\")).isEqualTo(MimeTypes.AUDIO_AAC);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.40.5\")).isEqualTo(MimeTypes.AUDIO_AAC);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.40.29\")).isEqualTo(MimeTypes.AUDIO_AAC);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.66\")).isEqualTo(MimeTypes.AUDIO_AAC);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.67\")).isEqualTo(MimeTypes.AUDIO_AAC);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.68\")).isEqualTo(MimeTypes.AUDIO_AAC);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.69\")).isEqualTo(MimeTypes.AUDIO_MPEG);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.6B\")).isEqualTo(MimeTypes.AUDIO_MPEG);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.a5\")).isEqualTo(MimeTypes.AUDIO_AC3);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.A5\")).isEqualTo(MimeTypes.AUDIO_AC3);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.a6\")).isEqualTo(MimeTypes.AUDIO_E_AC3);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.A6\")).isEqualTo(MimeTypes.AUDIO_E_AC3);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.A9\")).isEqualTo(MimeTypes.AUDIO_DTS);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.AC\")).isEqualTo(MimeTypes.AUDIO_DTS);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.AA\")).isEqualTo(MimeTypes.AUDIO_DTS_HD);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.AB\")).isEqualTo(MimeTypes.AUDIO_DTS_HD);\n    assertThat(MimeTypes.getMediaMimeType(\"mp4a.AD\")).isEqualTo(MimeTypes.AUDIO_OPUS);\n  }\n\n  @Test\n  public void testGetMimeTypeFromMp4ObjectType_forValidObjectType_returnsCorrectMimeType() {\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x60)).isEqualTo(MimeTypes.VIDEO_MPEG2);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x61)).isEqualTo(MimeTypes.VIDEO_MPEG2);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x20)).isEqualTo(MimeTypes.VIDEO_MP4V);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x21)).isEqualTo(MimeTypes.VIDEO_H264);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x23)).isEqualTo(MimeTypes.VIDEO_H265);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x6B)).isEqualTo(MimeTypes.AUDIO_MPEG);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x40)).isEqualTo(MimeTypes.AUDIO_AAC);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x66)).isEqualTo(MimeTypes.AUDIO_AAC);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x67)).isEqualTo(MimeTypes.AUDIO_AAC);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x68)).isEqualTo(MimeTypes.AUDIO_AAC);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0xA5)).isEqualTo(MimeTypes.AUDIO_AC3);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0xA6)).isEqualTo(MimeTypes.AUDIO_E_AC3);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0xA9)).isEqualTo(MimeTypes.AUDIO_DTS);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0xAC)).isEqualTo(MimeTypes.AUDIO_DTS);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0xAA)).isEqualTo(MimeTypes.AUDIO_DTS_HD);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0xAB)).isEqualTo(MimeTypes.AUDIO_DTS_HD);\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0xAD)).isEqualTo(MimeTypes.AUDIO_OPUS);\n  }\n\n  @Test\n  public void testGetMimeTypeFromMp4ObjectType_forInvalidObjectType_returnsNull() {\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0)).isNull();\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x600)).isNull();\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x01)).isNull();\n    assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(-1)).isNull();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 com.google.android.exoplayer2.testutil.TestUtil.createByteArray;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport java.nio.ByteBuffer;\nimport java.util.Arrays;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link NalUnitUtil}. */\n@RunWith(AndroidJUnit4.class)\npublic final class NalUnitUtilTest {\n\n  private static final int TEST_PARTIAL_NAL_POSITION = 4;\n  private static final int TEST_NAL_POSITION = 10;\n  private static final byte[] SPS_TEST_DATA = createByteArray(0x00, 0x00, 0x01, 0x67, 0x4D, 0x40,\n      0x16, 0xEC, 0xA0, 0x50, 0x17, 0xFC, 0xB8, 0x08, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00,\n      0x00, 0x0F, 0x47, 0x8B, 0x16, 0xCB);\n  private static final int SPS_TEST_DATA_OFFSET = 3;\n\n  @Test\n  public void testFindNalUnit() {\n    byte[] data = buildTestData();\n\n    // Should find NAL unit.\n    int result = NalUnitUtil.findNalUnit(data, 0, data.length, null);\n    assertThat(result).isEqualTo(TEST_NAL_POSITION);\n    // Should find NAL unit whose prefix ends one byte before the limit.\n    result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 4, null);\n    assertThat(result).isEqualTo(TEST_NAL_POSITION);\n    // Shouldn't find NAL unit whose prefix ends at the limit (since the limit is exclusive).\n    result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 3, null);\n    assertThat(result).isEqualTo(TEST_NAL_POSITION + 3);\n    // Should find NAL unit whose prefix starts at the offset.\n    result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION, data.length, null);\n    assertThat(result).isEqualTo(TEST_NAL_POSITION);\n    // Shouldn't find NAL unit whose prefix starts one byte past the offset.\n    result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION + 1, data.length, null);\n    assertThat(result).isEqualTo(data.length);\n  }\n\n  @Test\n  public void testFindNalUnitWithPrefix() {\n    byte[] data = buildTestData();\n\n    // First byte of NAL unit in data1, rest in data2.\n    boolean[] prefixFlags = new boolean[3];\n    byte[] data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1);\n    byte[] data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, data.length);\n    int result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags);\n    assertThat(result).isEqualTo(data1.length);\n    result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags);\n    assertThat(result).isEqualTo(-1);\n    assertPrefixFlagsCleared(prefixFlags);\n\n    // First three bytes of NAL unit in data1, rest in data2.\n    prefixFlags = new boolean[3];\n    data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 3);\n    data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 3, data.length);\n    result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags);\n    assertThat(result).isEqualTo(data1.length);\n    result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags);\n    assertThat(result).isEqualTo(-3);\n    assertPrefixFlagsCleared(prefixFlags);\n\n    // First byte of NAL unit in data1, second byte in data2, rest in data3.\n    prefixFlags = new boolean[3];\n    data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1);\n    data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2);\n    byte[] data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length);\n    result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags);\n    assertThat(result).isEqualTo(data1.length);\n    result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags);\n    assertThat(result).isEqualTo(data2.length);\n    result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags);\n    assertThat(result).isEqualTo(-2);\n    assertPrefixFlagsCleared(prefixFlags);\n\n    // NAL unit split with one byte in four arrays.\n    prefixFlags = new boolean[3];\n    data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1);\n    data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2);\n    data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, TEST_NAL_POSITION + 3);\n    byte[] data4 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length);\n    result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags);\n    assertThat(result).isEqualTo(data1.length);\n    result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags);\n    assertThat(result).isEqualTo(data2.length);\n    result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags);\n    assertThat(result).isEqualTo(data3.length);\n    result = NalUnitUtil.findNalUnit(data4, 0, data4.length, prefixFlags);\n    assertThat(result).isEqualTo(-3);\n    assertPrefixFlagsCleared(prefixFlags);\n\n    // NAL unit entirely in data2. data1 ends with partial prefix.\n    prefixFlags = new boolean[3];\n    data1 = Arrays.copyOfRange(data, 0, TEST_PARTIAL_NAL_POSITION + 2);\n    data2 = Arrays.copyOfRange(data, TEST_PARTIAL_NAL_POSITION + 2, data.length);\n    result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags);\n    assertThat(result).isEqualTo(data1.length);\n    result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags);\n    assertThat(result).isEqualTo(4);\n    assertPrefixFlagsCleared(prefixFlags);\n  }\n\n  @Test\n  public void testParseSpsNalUnit() {\n    NalUnitUtil.SpsData data = NalUnitUtil.parseSpsNalUnit(SPS_TEST_DATA, SPS_TEST_DATA_OFFSET,\n        SPS_TEST_DATA.length);\n    assertThat(data.width).isEqualTo(640);\n    assertThat(data.height).isEqualTo(360);\n    assertThat(data.deltaPicOrderAlwaysZeroFlag).isFalse();\n    assertThat(data.frameMbsOnlyFlag).isTrue();\n    assertThat(data.frameNumLength).isEqualTo(4);\n    assertThat(data.picOrderCntLsbLength).isEqualTo(6);\n    assertThat(data.seqParameterSetId).isEqualTo(0);\n    assertThat(data.pixelWidthAspectRatio).isEqualTo(1.0f);\n    assertThat(data.picOrderCountType).isEqualTo(0);\n    assertThat(data.separateColorPlaneFlag).isFalse();\n  }\n\n  @Test\n  public void testUnescapeDoesNotModifyBuffersWithoutStartCodes() {\n    assertUnescapeDoesNotModify(\"\");\n    assertUnescapeDoesNotModify(\"0000\");\n    assertUnescapeDoesNotModify(\"172BF38A3C\");\n    assertUnescapeDoesNotModify(\"000004\");\n  }\n\n  @Test\n  public void testUnescapeModifiesBuffersWithStartCodes() {\n    assertUnescapeMatchesExpected(\"00000301\", \"000001\");\n    assertUnescapeMatchesExpected(\"0000030200000300\", \"000002000000\");\n  }\n\n  @Test\n  public void testDiscardToSps() {\n    assertDiscardToSpsMatchesExpected(\"\", \"\");\n    assertDiscardToSpsMatchesExpected(\"00\", \"\");\n    assertDiscardToSpsMatchesExpected(\"FFFF000001\", \"\");\n    assertDiscardToSpsMatchesExpected(\"00000001\", \"\");\n    assertDiscardToSpsMatchesExpected(\"00000001FF67\", \"\");\n    assertDiscardToSpsMatchesExpected(\"00000001000167\", \"\");\n    assertDiscardToSpsMatchesExpected(\"0000000167\", \"0000000167\");\n    assertDiscardToSpsMatchesExpected(\"0000000167FF\", \"0000000167FF\");\n    assertDiscardToSpsMatchesExpected(\"0000000167FF\", \"0000000167FF\");\n    assertDiscardToSpsMatchesExpected(\"0000000167FF000000016700\", \"0000000167FF000000016700\");\n    assertDiscardToSpsMatchesExpected(\"000000000167FF\", \"0000000167FF\");\n    assertDiscardToSpsMatchesExpected(\"0001670000000167FF\", \"0000000167FF\");\n    assertDiscardToSpsMatchesExpected(\"FF00000001660000000167FF\", \"0000000167FF\");\n  }\n\n  private static byte[] buildTestData() {\n    byte[] data = new byte[20];\n    for (int i = 0; i < data.length; i++) {\n      data[i] = (byte) 0xFF;\n    }\n    // Insert an incomplete NAL unit start code.\n    data[TEST_PARTIAL_NAL_POSITION] = 0;\n    data[TEST_PARTIAL_NAL_POSITION + 1] = 0;\n    // Insert a complete NAL unit start code.\n    data[TEST_NAL_POSITION] = 0;\n    data[TEST_NAL_POSITION + 1] = 0;\n    data[TEST_NAL_POSITION + 2] = 1;\n    data[TEST_NAL_POSITION + 3] = 5;\n    return data;\n  }\n\n  private static void assertPrefixFlagsCleared(boolean[] flags) {\n    assertThat(flags[0] || flags[1] || flags[2]).isEqualTo(false);\n  }\n\n  private static void assertUnescapeDoesNotModify(String input) {\n    assertUnescapeMatchesExpected(input, input);\n  }\n\n  private static void assertUnescapeMatchesExpected(String input, String expectedOutput) {\n    byte[] bitstream = Util.getBytesFromHexString(input);\n    byte[] expectedOutputBitstream = Util.getBytesFromHexString(expectedOutput);\n    int count = NalUnitUtil.unescapeStream(bitstream, bitstream.length);\n    assertThat(count).isEqualTo(expectedOutputBitstream.length);\n    byte[] outputBitstream = new byte[count];\n    System.arraycopy(bitstream, 0, outputBitstream, 0, count);\n    assertThat(outputBitstream).isEqualTo(expectedOutputBitstream);\n  }\n\n  private static void assertDiscardToSpsMatchesExpected(String input, String expectedOutput) {\n    byte[] bitstream = Util.getBytesFromHexString(input);\n    byte[] expectedOutputBitstream = Util.getBytesFromHexString(expectedOutput);\n    ByteBuffer buffer = ByteBuffer.wrap(bitstream);\n    buffer.position(buffer.limit());\n    NalUnitUtil.discardToSps(buffer);\n    assertThat(Arrays.copyOf(buffer.array(), buffer.position())).isEqualTo(expectedOutputBitstream);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link ParsableBitArray}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ParsableBitArrayTest {\n\n  private static final byte[] TEST_DATA = new byte[] {0x3C, (byte) 0xD2, (byte) 0x5F, (byte) 0x01,\n      (byte) 0xFF, (byte) 0x14, (byte) 0x60, (byte) 0x99};\n\n  private ParsableBitArray testArray;\n\n  @Before\n  public void setUp() {\n    testArray = new ParsableBitArray(TEST_DATA);\n  }\n\n  @Test\n  public void testReadAllBytes() {\n    byte[] bytesRead = new byte[TEST_DATA.length];\n    testArray.readBytes(bytesRead, 0, TEST_DATA.length);\n    assertThat(bytesRead).isEqualTo(TEST_DATA);\n    assertThat(testArray.getPosition()).isEqualTo(TEST_DATA.length * 8);\n    assertThat(testArray.getBytePosition()).isEqualTo(TEST_DATA.length);\n  }\n\n  @Test\n  public void testReadBit() {\n    assertReadBitsToEnd(0);\n  }\n\n  @Test\n  public void testReadBits() {\n    assertThat(testArray.readBits(5)).isEqualTo(getTestDataBits(0, 5));\n    assertThat(testArray.readBits(0)).isEqualTo(getTestDataBits(5, 0));\n    assertThat(testArray.readBits(3)).isEqualTo(getTestDataBits(5, 3));\n    assertThat(testArray.readBits(16)).isEqualTo(getTestDataBits(8, 16));\n    assertThat(testArray.readBits(3)).isEqualTo(getTestDataBits(24, 3));\n    assertThat(testArray.readBits(18)).isEqualTo(getTestDataBits(27, 18));\n    assertThat(testArray.readBits(5)).isEqualTo(getTestDataBits(45, 5));\n    assertThat(testArray.readBits(14)).isEqualTo(getTestDataBits(50, 14));\n  }\n\n  @Test\n  public void testReadBitsToByteArray() {\n    byte[] result = new byte[TEST_DATA.length];\n    // Test read within byte boundaries.\n    testArray.readBits(result, 0, 6);\n    assertThat(result[0]).isEqualTo((byte) (TEST_DATA[0] & 0xFC));\n    // Test read across byte boundaries.\n    testArray.readBits(result, 0, 8);\n    assertThat(result[0]).isEqualTo(\n        (byte) (((TEST_DATA[0] & 0x03) << 6) | ((TEST_DATA[1] & 0xFC) >> 2)));\n    // Test reading across multiple bytes.\n    testArray.readBits(result, 1, 50);\n    for (int i = 1; i < 7; i++) {\n      assertThat(result[i])\n          .isEqualTo((byte) (((TEST_DATA[i] & 0x03) << 6) | ((TEST_DATA[i + 1] & 0xFC) >> 2)));\n    }\n    assertThat(result[7]).isEqualTo((byte) ((TEST_DATA[7] & 0x03) << 6));\n    assertThat(testArray.bitsLeft()).isEqualTo(0);\n    // Test read last buffer byte across input data bytes.\n    testArray.setPosition(31);\n    result[3] = 0;\n    testArray.readBits(result, 3, 3);\n    assertThat(result[3]).isEqualTo((byte) 0xE0);\n    // Test read bits in the middle of a input data byte.\n    result[0] = 0;\n    assertThat(testArray.getPosition()).isEqualTo(34);\n    testArray.readBits(result, 0, 3);\n    assertThat(result[0]).isEqualTo((byte) 0xE0);\n    // Test read 0 bits.\n    testArray.setPosition(32);\n    result[1] = 0;\n    testArray.readBits(result, 1, 0);\n    assertThat(result[1]).isEqualTo((byte) 0);\n    // Test reading a number of bits divisible by 8.\n    testArray.setPosition(0);\n    testArray.readBits(result, 0, 16);\n    assertThat(result[0]).isEqualTo(TEST_DATA[0]);\n    assertThat(result[1]).isEqualTo(TEST_DATA[1]);\n    // Test least significant bits are unmodified.\n    result[1] = (byte) 0xFF;\n    testArray.readBits(result, 0, 9);\n    assertThat(result[0]).isEqualTo((byte) 0x5F);\n    assertThat(result[1]).isEqualTo((byte) 0x7F);\n  }\n\n  @Test\n  public void testRead32BitsByteAligned() {\n    assertThat(testArray.readBits(32)).isEqualTo(getTestDataBits(0, 32));\n    assertThat(testArray.readBits(32)).isEqualTo(getTestDataBits(32, 32));\n  }\n\n  @Test\n  public void testRead32BitsNonByteAligned() {\n    assertThat(testArray.readBits(5)).isEqualTo(getTestDataBits(0, 5));\n    assertThat(testArray.readBits(32)).isEqualTo(getTestDataBits(5, 32));\n  }\n\n  @Test\n  public void testSkipBytes() {\n    testArray.skipBytes(2);\n    assertReadBitsToEnd(16);\n  }\n\n  @Test\n  public void testSkipBitsByteAligned() {\n    testArray.skipBits(16);\n    assertReadBitsToEnd(16);\n  }\n\n  @Test\n  public void testSkipBitsNonByteAligned() {\n    testArray.skipBits(5);\n    assertReadBitsToEnd(5);\n  }\n\n  @Test\n  public void testSetPositionByteAligned() {\n    testArray.setPosition(16);\n    assertReadBitsToEnd(16);\n  }\n\n  @Test\n  public void testSetPositionNonByteAligned() {\n    testArray.setPosition(5);\n    assertReadBitsToEnd(5);\n  }\n\n  @Test\n  public void testByteAlignFromNonByteAligned() {\n    testArray.setPosition(11);\n    testArray.byteAlign();\n    assertThat(testArray.getBytePosition()).isEqualTo(2);\n    assertThat(testArray.getPosition()).isEqualTo(16);\n    assertReadBitsToEnd(16);\n  }\n\n  @Test\n  public void testByteAlignFromByteAligned() {\n    testArray.setPosition(16);\n    testArray.byteAlign(); // Should be a no-op.\n    assertThat(testArray.getBytePosition()).isEqualTo(2);\n    assertThat(testArray.getPosition()).isEqualTo(16);\n    assertReadBitsToEnd(16);\n  }\n\n  @Test\n  public void testPutBitsWithinByte() {\n    ParsableBitArray output = new ParsableBitArray(new byte[4]);\n    output.skipBits(1);\n\n    output.putInt(0x3F, 5);\n\n    output.setPosition(0);\n    assertThat(output.readBits(8)).isEqualTo(0x1F << 2); // Check that only 5 bits are modified.\n  }\n\n  @Test\n  public void testPutBitsAcrossTwoBytes() {\n    ParsableBitArray output = new ParsableBitArray(new byte[4]);\n    output.setPosition(12);\n\n    output.putInt(0xFF, 8);\n    output.setPosition(8);\n\n    assertThat(output.readBits(16)).isEqualTo(0x0FF0);\n  }\n\n  @Test\n  public void testPutBitsAcrossMultipleBytes() {\n    ParsableBitArray output = new ParsableBitArray(new byte[8]);\n    output.setPosition(31); // Writing starts at 31 to test the 30th bit is not modified.\n\n    output.putInt(0xFF146098, 30); // Write only 30 to test the 61st bit is not modified.\n\n    output.setPosition(30);\n    assertThat(output.readBits(32)).isEqualTo(0x3F146098 << 1);\n  }\n\n  @Test\n  public void testPut32Bits() {\n    ParsableBitArray output = new ParsableBitArray(new byte[5]);\n    output.setPosition(4);\n\n    output.putInt(0xFF146098, 32);\n\n    output.setPosition(4);\n    assertThat(output.readBits(32)).isEqualTo(0xFF146098);\n  }\n\n  @Test\n  public void testPutFullBytes() {\n    ParsableBitArray output = new ParsableBitArray(new byte[2]);\n\n    output.putInt(0x81, 8);\n\n    output.setPosition(0);\n    assertThat(output.readBits(8)).isEqualTo(0x81);\n  }\n\n  @Test\n  public void testNoOverwriting() {\n    ParsableBitArray output =\n        new ParsableBitArray(\n            new byte[] {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff});\n    output.setPosition(1);\n\n    output.putInt(0, 30);\n\n    output.setPosition(0);\n    assertThat(output.readBits(32)).isEqualTo(0x80000001);\n  }\n\n  private void assertReadBitsToEnd(int expectedStartPosition) {\n    int position = testArray.getPosition();\n    assertThat(position).isEqualTo(expectedStartPosition);\n    for (int i = position; i < TEST_DATA.length * 8; i++) {\n      assertThat(testArray.readBit()).isEqualTo(getTestDataBit(i));\n      assertThat(testArray.getPosition()).isEqualTo(i + 1);\n    }\n  }\n\n  private static int getTestDataBits(int bitPosition, int length) {\n    int result = 0;\n    for (int i = 0; i < length; i++) {\n      result = result << 1;\n      if (getTestDataBit(bitPosition++)) {\n        result |= 0x1;\n      }\n    }\n    return result;\n  }\n\n  private static boolean getTestDataBit(int bitPosition) {\n    return (TEST_DATA[bitPosition / 8] & (0x80 >>> (bitPosition % 8))) != 0;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 com.google.common.truth.Truth.assertThat;\nimport static java.nio.charset.Charset.forName;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport java.nio.ByteBuffer;\nimport java.util.Arrays;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link ParsableByteArray}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ParsableByteArrayTest {\n\n  private static final byte[] TEST_DATA =\n      new byte[] {0x0F, (byte) 0xFF, (byte) 0x42, (byte) 0x0F, 0x00, 0x00, 0x00, 0x00};\n\n  private static ParsableByteArray getTestDataArray() {\n    ParsableByteArray testArray = new ParsableByteArray(TEST_DATA.length);\n    System.arraycopy(TEST_DATA, 0, testArray.data, 0, TEST_DATA.length);\n    return testArray;\n  }\n\n  @Test\n  public void testReadShort() {\n    testReadShort((short) -1);\n    testReadShort((short) 0);\n    testReadShort((short) 1);\n    testReadShort(Short.MIN_VALUE);\n    testReadShort(Short.MAX_VALUE);\n  }\n\n  private static void testReadShort(short testValue) {\n    ParsableByteArray testArray = new ParsableByteArray(\n        ByteBuffer.allocate(4).putShort(testValue).array());\n    int readValue = testArray.readShort();\n\n    // Assert that the value we read was the value we wrote.\n    assertThat(readValue).isEqualTo(testValue);\n    // And that the position advanced as expected.\n    assertThat(testArray.getPosition()).isEqualTo(2);\n\n    // And that skipping back and reading gives the same results.\n    testArray.skipBytes(-2);\n    readValue = testArray.readShort();\n    assertThat(readValue).isEqualTo(testValue);\n    assertThat(testArray.getPosition()).isEqualTo(2);\n  }\n\n  @Test\n  public void testReadInt() {\n    testReadInt(0);\n    testReadInt(1);\n    testReadInt(-1);\n    testReadInt(Integer.MIN_VALUE);\n    testReadInt(Integer.MAX_VALUE);\n  }\n\n  private static void testReadInt(int testValue) {\n    ParsableByteArray testArray = new ParsableByteArray(\n        ByteBuffer.allocate(4).putInt(testValue).array());\n    int readValue = testArray.readInt();\n\n    // Assert that the value we read was the value we wrote.\n    assertThat(readValue).isEqualTo(testValue);\n    // And that the position advanced as expected.\n    assertThat(testArray.getPosition()).isEqualTo(4);\n\n    // And that skipping back and reading gives the same results.\n    testArray.skipBytes(-4);\n    readValue = testArray.readInt();\n    assertThat(readValue).isEqualTo(testValue);\n    assertThat(testArray.getPosition()).isEqualTo(4);\n  }\n\n  @Test\n  public void testReadUnsignedInt() {\n    testReadUnsignedInt(0);\n    testReadUnsignedInt(1);\n    testReadUnsignedInt(Integer.MAX_VALUE);\n    testReadUnsignedInt(Integer.MAX_VALUE + 1L);\n    testReadUnsignedInt(0xFFFFFFFFL);\n  }\n\n  private static void testReadUnsignedInt(long testValue) {\n    ParsableByteArray testArray = new ParsableByteArray(\n        Arrays.copyOfRange(ByteBuffer.allocate(8).putLong(testValue).array(), 4, 8));\n    long readValue = testArray.readUnsignedInt();\n\n    // Assert that the value we read was the value we wrote.\n    assertThat(readValue).isEqualTo(testValue);\n    // And that the position advanced as expected.\n    assertThat(testArray.getPosition()).isEqualTo(4);\n\n    // And that skipping back and reading gives the same results.\n    testArray.skipBytes(-4);\n    readValue = testArray.readUnsignedInt();\n    assertThat(readValue).isEqualTo(testValue);\n    assertThat(testArray.getPosition()).isEqualTo(4);\n  }\n\n  @Test\n  public void testReadUnsignedIntToInt() {\n    testReadUnsignedIntToInt(0);\n    testReadUnsignedIntToInt(1);\n    testReadUnsignedIntToInt(Integer.MAX_VALUE);\n    try {\n      testReadUnsignedIntToInt(-1);\n      fail();\n    } catch (IllegalStateException e) {\n      // Expected.\n    }\n    try {\n      testReadUnsignedIntToInt(Integer.MIN_VALUE);\n      fail();\n    } catch (IllegalStateException e) {\n      // Expected.\n    }\n  }\n\n  private static void testReadUnsignedIntToInt(int testValue) {\n    ParsableByteArray testArray = new ParsableByteArray(\n        ByteBuffer.allocate(4).putInt(testValue).array());\n    int readValue = testArray.readUnsignedIntToInt();\n\n    // Assert that the value we read was the value we wrote.\n    assertThat(readValue).isEqualTo(testValue);\n    // And that the position advanced as expected.\n    assertThat(testArray.getPosition()).isEqualTo(4);\n\n    // And that skipping back and reading gives the same results.\n    testArray.skipBytes(-4);\n    readValue = testArray.readUnsignedIntToInt();\n    assertThat(readValue).isEqualTo(testValue);\n    assertThat(testArray.getPosition()).isEqualTo(4);\n  }\n\n  @Test\n  public void testReadUnsignedLongToLong() {\n    testReadUnsignedLongToLong(0);\n    testReadUnsignedLongToLong(1);\n    testReadUnsignedLongToLong(Long.MAX_VALUE);\n    try {\n      testReadUnsignedLongToLong(-1);\n      fail();\n    } catch (IllegalStateException e) {\n      // Expected.\n    }\n    try {\n      testReadUnsignedLongToLong(Long.MIN_VALUE);\n      fail();\n    } catch (IllegalStateException e) {\n      // Expected.\n    }\n  }\n\n  private static void testReadUnsignedLongToLong(long testValue) {\n    ParsableByteArray testArray = new ParsableByteArray(\n        ByteBuffer.allocate(8).putLong(testValue).array());\n    long readValue = testArray.readUnsignedLongToLong();\n\n    // Assert that the value we read was the value we wrote.\n    assertThat(readValue).isEqualTo(testValue);\n    // And that the position advanced as expected.\n    assertThat(testArray.getPosition()).isEqualTo(8);\n\n    // And that skipping back and reading gives the same results.\n    testArray.skipBytes(-8);\n    readValue = testArray.readUnsignedLongToLong();\n    assertThat(readValue).isEqualTo(testValue);\n    assertThat(testArray.getPosition()).isEqualTo(8);\n  }\n\n  @Test\n  public void testReadLong() {\n    testReadLong(0);\n    testReadLong(1);\n    testReadLong(-1);\n    testReadLong(Long.MIN_VALUE);\n    testReadLong(Long.MAX_VALUE);\n  }\n\n  private static void testReadLong(long testValue) {\n    ParsableByteArray testArray = new ParsableByteArray(\n        ByteBuffer.allocate(8).putLong(testValue).array());\n    long readValue = testArray.readLong();\n\n    // Assert that the value we read was the value we wrote.\n    assertThat(readValue).isEqualTo(testValue);\n    // And that the position advanced as expected.\n    assertThat(testArray.getPosition()).isEqualTo(8);\n\n    // And that skipping back and reading gives the same results.\n    testArray.skipBytes(-8);\n    readValue = testArray.readLong();\n    assertThat(readValue).isEqualTo(testValue);\n    assertThat(testArray.getPosition()).isEqualTo(8);\n  }\n\n  @Test\n  public void testReadingMovesPosition() {\n    ParsableByteArray parsableByteArray = getTestDataArray();\n\n    // Given an array at the start\n    assertThat(parsableByteArray.getPosition()).isEqualTo(0);\n    // When reading an integer, the position advances\n    parsableByteArray.readUnsignedInt();\n    assertThat(parsableByteArray.getPosition()).isEqualTo(4);\n  }\n\n  @Test\n  public void testOutOfBoundsThrows() {\n    ParsableByteArray parsableByteArray = getTestDataArray();\n\n    // Given an array at the end\n    parsableByteArray.readUnsignedLongToLong();\n    assertThat(parsableByteArray.getPosition()).isEqualTo(TEST_DATA.length);\n    // Then reading more data throws.\n    try {\n      parsableByteArray.readUnsignedInt();\n      fail();\n    } catch (Exception e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testModificationsAffectParsableArray() {\n    ParsableByteArray parsableByteArray = getTestDataArray();\n\n    // When modifying the wrapped byte array\n    byte[] data = parsableByteArray.data;\n    long readValue = parsableByteArray.readUnsignedInt();\n    data[0] = (byte) (TEST_DATA[0] + 1);\n    parsableByteArray.setPosition(0);\n    // Then the parsed value changes.\n    assertThat(parsableByteArray.readUnsignedInt()).isNotEqualTo(readValue);\n  }\n\n  @Test\n  public void testReadingUnsignedLongWithMsbSetThrows() {\n    ParsableByteArray parsableByteArray = getTestDataArray();\n\n    // Given an array with the most-significant bit set on the top byte\n    byte[] data = parsableByteArray.data;\n    data[0] = (byte) 0x80;\n    // Then reading an unsigned long throws.\n    try {\n      parsableByteArray.readUnsignedLongToLong();\n      fail();\n    } catch (Exception e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testReadUnsignedFixedPoint1616() {\n    ParsableByteArray parsableByteArray = getTestDataArray();\n\n    // When reading the integer part of a 16.16 fixed point value\n    int value = parsableByteArray.readUnsignedFixedPoint1616();\n    // Then the read value is equal to the array elements interpreted as a short.\n    assertThat(value).isEqualTo((0xFF & TEST_DATA[0]) << 8 | (TEST_DATA[1] & 0xFF));\n    assertThat(parsableByteArray.getPosition()).isEqualTo(4);\n  }\n\n  @Test\n  public void testReadingBytesReturnsCopy() {\n    ParsableByteArray parsableByteArray = getTestDataArray();\n\n    // When reading all the bytes back\n    int length = parsableByteArray.limit();\n    assertThat(length).isEqualTo(TEST_DATA.length);\n    byte[] copy = new byte[length];\n    parsableByteArray.readBytes(copy, 0, length);\n    // Then the array elements are the same.\n    assertThat(copy).isEqualTo(parsableByteArray.data);\n  }\n\n  @Test\n  public void testReadLittleEndianLong() {\n    ParsableByteArray byteArray = new ParsableByteArray(new byte[] {\n        0x01, 0x00, 0x00, 0x00,\n        0x00, 0x00, 0x00, (byte) 0xFF\n    });\n    assertThat(byteArray.readLittleEndianLong()).isEqualTo(0xFF00000000000001L);\n    assertThat(byteArray.getPosition()).isEqualTo(8);\n  }\n\n  @Test\n  public void testReadLittleEndianUnsignedInt() {\n    ParsableByteArray byteArray = new ParsableByteArray(new byte[] {\n        0x10, 0x00, 0x00, (byte) 0xFF\n    });\n    assertThat(byteArray.readLittleEndianUnsignedInt()).isEqualTo(0xFF000010L);\n    assertThat(byteArray.getPosition()).isEqualTo(4);\n  }\n\n  @Test\n  public void testReadLittleEndianInt() {\n    ParsableByteArray byteArray = new ParsableByteArray(new byte[] {\n        0x01, 0x00, 0x00, (byte) 0xFF\n    });\n    assertThat(byteArray.readLittleEndianInt()).isEqualTo(0xFF000001);\n    assertThat(byteArray.getPosition()).isEqualTo(4);\n  }\n\n  @Test\n  public void testReadLittleEndianUnsignedInt24() {\n    byte[] data = {0x01, 0x02, (byte) 0xFF};\n    ParsableByteArray byteArray = new ParsableByteArray(data);\n    assertThat(byteArray.readLittleEndianUnsignedInt24()).isEqualTo(0xFF0201);\n    assertThat(byteArray.getPosition()).isEqualTo(3);\n  }\n\n  @Test\n  public void testReadInt24Positive() {\n    byte[] data = {0x01, 0x02, (byte) 0xFF};\n    ParsableByteArray byteArray = new ParsableByteArray(data);\n    assertThat(byteArray.readInt24()).isEqualTo(0x0102FF);\n    assertThat(byteArray.getPosition()).isEqualTo(3);\n  }\n\n  @Test\n  public void testReadInt24Negative() {\n    byte[] data = {(byte) 0xFF, 0x02, (byte) 0x01};\n    ParsableByteArray byteArray = new ParsableByteArray(data);\n    assertThat(byteArray.readInt24()).isEqualTo(0xFFFF0201);\n    assertThat(byteArray.getPosition()).isEqualTo(3);\n  }\n\n  @Test\n  public void testReadLittleEndianUnsignedShort() {\n    ParsableByteArray byteArray = new ParsableByteArray(new byte[] {\n        0x01, (byte) 0xFF, 0x02, (byte) 0xFF\n    });\n    assertThat(byteArray.readLittleEndianUnsignedShort()).isEqualTo(0xFF01);\n    assertThat(byteArray.getPosition()).isEqualTo(2);\n    assertThat(byteArray.readLittleEndianUnsignedShort()).isEqualTo(0xFF02);\n    assertThat(byteArray.getPosition()).isEqualTo(4);\n  }\n\n  @Test\n  public void testReadLittleEndianShort() {\n    ParsableByteArray byteArray = new ParsableByteArray(new byte[] {\n        0x01, (byte) 0xFF, 0x02, (byte) 0xFF\n    });\n    assertThat(byteArray.readLittleEndianShort()).isEqualTo((short) 0xFF01);\n    assertThat(byteArray.getPosition()).isEqualTo(2);\n    assertThat(byteArray.readLittleEndianShort()).isEqualTo((short) 0xFF02);\n    assertThat(byteArray.getPosition()).isEqualTo(4);\n  }\n\n  @Test\n  public void testReadString() {\n    byte[] data = {\n        (byte) 0xC3, (byte) 0xA4, (byte) 0x20,\n        (byte) 0xC3, (byte) 0xB6, (byte) 0x20,\n        (byte) 0xC2, (byte) 0xAE, (byte) 0x20,\n        (byte) 0xCF, (byte) 0x80, (byte) 0x20,\n        (byte) 0xE2, (byte) 0x88, (byte) 0x9A, (byte) 0x20,\n        (byte) 0xC2, (byte) 0xB1, (byte) 0x20,\n        (byte) 0xE8, (byte) 0xB0, (byte) 0xA2, (byte) 0x20,\n    };\n    ParsableByteArray byteArray = new ParsableByteArray(data);\n    assertThat(byteArray.readString(data.length)).isEqualTo(\"ä ö ® π √ ± 谢 \");\n    assertThat(byteArray.getPosition()).isEqualTo(data.length);\n  }\n\n  @Test\n  public void testReadAsciiString() {\n    byte[] data = new byte[] {'t', 'e', 's', 't'};\n    ParsableByteArray testArray = new ParsableByteArray(data);\n    assertThat(testArray.readString(data.length, forName(\"US-ASCII\"))).isEqualTo(\"test\");\n    assertThat(testArray.getPosition()).isEqualTo(data.length);\n  }\n\n  @Test\n  public void testReadStringOutOfBoundsDoesNotMovePosition() {\n    byte[] data = {\n        (byte) 0xC3, (byte) 0xA4, (byte) 0x20\n    };\n    ParsableByteArray byteArray = new ParsableByteArray(data);\n    try {\n      byteArray.readString(data.length + 1);\n      fail();\n    } catch (StringIndexOutOfBoundsException e) {\n      assertThat(byteArray.getPosition()).isEqualTo(0);\n    }\n  }\n\n  @Test\n  public void testReadEmptyString() {\n    byte[] bytes = new byte[0];\n    ParsableByteArray parser = new ParsableByteArray(bytes);\n    assertThat(parser.readLine()).isNull();\n  }\n\n  @Test\n  public void testReadNullTerminatedStringWithLengths() {\n    byte[] bytes = new byte[] {\n        'f', 'o', 'o', 0, 'b', 'a', 'r', 0\n    };\n    // Test with lengths that match NUL byte positions.\n    ParsableByteArray parser = new ParsableByteArray(bytes);\n    assertThat(parser.readNullTerminatedString(4)).isEqualTo(\"foo\");\n    assertThat(parser.getPosition()).isEqualTo(4);\n    assertThat(parser.readNullTerminatedString(4)).isEqualTo(\"bar\");\n    assertThat(parser.getPosition()).isEqualTo(8);\n    assertThat(parser.readNullTerminatedString()).isNull();\n    // Test with lengths that do not match NUL byte positions.\n    parser = new ParsableByteArray(bytes);\n    assertThat(parser.readNullTerminatedString(2)).isEqualTo(\"fo\");\n    assertThat(parser.getPosition()).isEqualTo(2);\n    assertThat(parser.readNullTerminatedString(2)).isEqualTo(\"o\");\n    assertThat(parser.getPosition()).isEqualTo(4);\n    assertThat(parser.readNullTerminatedString(3)).isEqualTo(\"bar\");\n    assertThat(parser.getPosition()).isEqualTo(7);\n    assertThat(parser.readNullTerminatedString(1)).isEqualTo(\"\");\n    assertThat(parser.getPosition()).isEqualTo(8);\n    assertThat(parser.readNullTerminatedString()).isNull();\n    // Test with limit at NUL\n    parser = new ParsableByteArray(bytes, 4);\n    assertThat(parser.readNullTerminatedString(4)).isEqualTo(\"foo\");\n    assertThat(parser.getPosition()).isEqualTo(4);\n    assertThat(parser.readNullTerminatedString()).isNull();\n    // Test with limit before NUL\n    parser = new ParsableByteArray(bytes, 3);\n    assertThat(parser.readNullTerminatedString(3)).isEqualTo(\"foo\");\n    assertThat(parser.getPosition()).isEqualTo(3);\n    assertThat(parser.readNullTerminatedString()).isNull();\n  }\n\n  @Test\n  public void testReadNullTerminatedString() {\n    byte[] bytes = new byte[] {\n        'f', 'o', 'o', 0, 'b', 'a', 'r', 0\n    };\n    // Test normal case.\n    ParsableByteArray parser = new ParsableByteArray(bytes);\n    assertThat(parser.readNullTerminatedString()).isEqualTo(\"foo\");\n    assertThat(parser.getPosition()).isEqualTo(4);\n    assertThat(parser.readNullTerminatedString()).isEqualTo(\"bar\");\n    assertThat(parser.getPosition()).isEqualTo(8);\n    assertThat(parser.readNullTerminatedString()).isNull();\n    // Test with limit at NUL.\n    parser = new ParsableByteArray(bytes, 4);\n    assertThat(parser.readNullTerminatedString()).isEqualTo(\"foo\");\n    assertThat(parser.getPosition()).isEqualTo(4);\n    assertThat(parser.readNullTerminatedString()).isNull();\n    // Test with limit before NUL.\n    parser = new ParsableByteArray(bytes, 3);\n    assertThat(parser.readNullTerminatedString()).isEqualTo(\"foo\");\n    assertThat(parser.getPosition()).isEqualTo(3);\n    assertThat(parser.readNullTerminatedString()).isNull();\n  }\n\n  @Test\n  public void testReadNullTerminatedStringWithoutEndingNull() {\n    byte[] bytes = new byte[] {\n        'f', 'o', 'o', 0, 'b', 'a', 'r'\n    };\n    ParsableByteArray parser = new ParsableByteArray(bytes);\n    assertThat(parser.readNullTerminatedString()).isEqualTo(\"foo\");\n    assertThat(parser.readNullTerminatedString()).isEqualTo(\"bar\");\n    assertThat(parser.readNullTerminatedString()).isNull();\n  }\n\n  @Test\n  public void testReadSingleLineWithoutEndingTrail() {\n    byte[] bytes = new byte[] {\n      'f', 'o', 'o'\n    };\n    ParsableByteArray parser = new ParsableByteArray(bytes);\n    assertThat(parser.readLine()).isEqualTo(\"foo\");\n    assertThat(parser.readLine()).isNull();\n  }\n\n  @Test\n  public void testReadSingleLineWithEndingLf() {\n    byte[] bytes = new byte[] {\n      'f', 'o', 'o', '\\n'\n    };\n    ParsableByteArray parser = new ParsableByteArray(bytes);\n    assertThat(parser.readLine()).isEqualTo(\"foo\");\n    assertThat(parser.readLine()).isNull();\n  }\n\n  @Test\n  public void testReadTwoLinesWithCrFollowedByLf() {\n    byte[] bytes = new byte[] {\n      'f', 'o', 'o', '\\r', '\\n', 'b', 'a', 'r'\n    };\n    ParsableByteArray parser = new ParsableByteArray(bytes);\n    assertThat(parser.readLine()).isEqualTo(\"foo\");\n    assertThat(parser.readLine()).isEqualTo(\"bar\");\n    assertThat(parser.readLine()).isNull();\n  }\n\n  @Test\n  public void testReadThreeLinesWithEmptyLine() {\n    byte[] bytes = new byte[] {\n      'f', 'o', 'o', '\\r', '\\n', '\\r', 'b', 'a', 'r'\n    };\n    ParsableByteArray parser = new ParsableByteArray(bytes);\n    assertThat(parser.readLine()).isEqualTo(\"foo\");\n    assertThat(parser.readLine()).isEqualTo(\"\");\n    assertThat(parser.readLine()).isEqualTo(\"bar\");\n    assertThat(parser.readLine()).isNull();\n  }\n\n  @Test\n  public void testReadFourLinesWithLfFollowedByCr() {\n    byte[] bytes = new byte[] {\n      'f', 'o', 'o', '\\n', '\\r', '\\r', 'b', 'a', 'r', '\\r', '\\n'\n    };\n    ParsableByteArray parser = new ParsableByteArray(bytes);\n    assertThat(parser.readLine()).isEqualTo(\"foo\");\n    assertThat(parser.readLine()).isEqualTo(\"\");\n    assertThat(parser.readLine()).isEqualTo(\"\");\n    assertThat(parser.readLine()).isEqualTo(\"bar\");\n    assertThat(parser.readLine()).isNull();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 com.google.android.exoplayer2.testutil.TestUtil.createByteArray;\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link ParsableNalUnitBitArray}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ParsableNalUnitBitArrayTest {\n\n  private static final byte[] NO_ESCAPING_TEST_DATA = createByteArray(0, 3, 0, 1, 3, 0, 0);\n  private static final byte[] ALL_ESCAPING_TEST_DATA = createByteArray(0, 0, 3, 0, 0, 3, 0, 0, 3);\n  private static final byte[] MIX_TEST_DATA = createByteArray(255, 0, 0, 3, 255, 0, 0, 127);\n\n  @Test\n  public void testReadNoEscaping() {\n    ParsableNalUnitBitArray array =\n        new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, NO_ESCAPING_TEST_DATA.length);\n    assertThat(array.readBits(24)).isEqualTo(0x000300);\n    assertThat(array.readBits(7)).isEqualTo(0);\n    assertThat(array.readBit()).isTrue();\n    assertThat(array.readBits(24)).isEqualTo(0x030000);\n    assertThat(array.canReadBits(1)).isFalse();\n    assertThat(array.canReadBits(8)).isFalse();\n  }\n\n  @Test\n  public void testReadNoEscapingTruncated() {\n    ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, 4);\n    assertThat(array.canReadBits(32)).isTrue();\n    array.skipBits(32);\n    assertThat(array.canReadBits(1)).isFalse();\n    try {\n      array.readBit();\n      fail();\n    } catch (Exception e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testReadAllEscaping() {\n    ParsableNalUnitBitArray array =\n        new ParsableNalUnitBitArray(ALL_ESCAPING_TEST_DATA, 0, ALL_ESCAPING_TEST_DATA.length);\n    assertThat(array.canReadBits(48)).isTrue();\n    assertThat(array.canReadBits(49)).isFalse();\n    assertThat(array.readBits(15)).isEqualTo(0);\n    assertThat(array.readBit()).isFalse();\n    assertThat(array.readBits(17)).isEqualTo(0);\n    assertThat(array.readBits(15)).isEqualTo(0);\n  }\n\n  @Test\n  public void testReadMix() {\n    ParsableNalUnitBitArray array =\n        new ParsableNalUnitBitArray(MIX_TEST_DATA, 0, MIX_TEST_DATA.length);\n    assertThat(array.canReadBits(56)).isTrue();\n    assertThat(array.canReadBits(57)).isFalse();\n    assertThat(array.readBits(7)).isEqualTo(127);\n    assertThat(array.readBits(2)).isEqualTo(2);\n    assertThat(array.readBits(17)).isEqualTo(3);\n    assertThat(array.readBits(7)).isEqualTo(126);\n    assertThat(array.readBits(23)).isEqualTo(127);\n    assertThat(array.canReadBits(1)).isFalse();\n  }\n\n  @Test\n  public void testReadExpGolomb() {\n    ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0x9E), 0, 1);\n    assertThat(array.canReadExpGolombCodedNum()).isTrue();\n    assertThat(array.readUnsignedExpGolombCodedInt()).isEqualTo(0);\n    assertThat(array.readUnsignedExpGolombCodedInt()).isEqualTo(6);\n    assertThat(array.readUnsignedExpGolombCodedInt()).isEqualTo(0);\n    assertThat(array.canReadExpGolombCodedNum()).isFalse();\n    try {\n      array.readUnsignedExpGolombCodedInt();\n      fail();\n    } catch (Exception e) {\n      // Expected.\n    }\n  }\n\n  @Test\n  public void testReadExpGolombWithEscaping() {\n    ParsableNalUnitBitArray array =\n        new ParsableNalUnitBitArray(createByteArray(0, 0, 3, 128, 0), 0, 5);\n    assertThat(array.canReadExpGolombCodedNum()).isFalse();\n    array.skipBit();\n    assertThat(array.canReadExpGolombCodedNum()).isTrue();\n    assertThat(array.readUnsignedExpGolombCodedInt()).isEqualTo(32767);\n    assertThat(array.canReadBits(1)).isFalse();\n  }\n\n  @Test\n  public void testReset() {\n    ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0, 0), 0, 2);\n    assertThat(array.canReadExpGolombCodedNum()).isFalse();\n    assertThat(array.canReadBits(16)).isTrue();\n    assertThat(array.canReadBits(17)).isFalse();\n    array.reset(createByteArray(0, 0, 3, 0), 0, 4);\n    assertThat(array.canReadBits(24)).isTrue();\n    assertThat(array.canReadBits(25)).isFalse();\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport java.io.ByteArrayOutputStream;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests {@link ReusableBufferedOutputStream}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ReusableBufferedOutputStreamTest {\n\n  private static final byte[] TEST_DATA_1 = Util.getUtf8Bytes(\"test data 1\");\n  private static final byte[] TEST_DATA_2 = Util.getUtf8Bytes(\"2 test data\");\n\n  @Test\n  public void testReset() throws Exception {\n    ByteArrayOutputStream byteArrayOutputStream1 = new ByteArrayOutputStream(1000);\n    ReusableBufferedOutputStream outputStream = new ReusableBufferedOutputStream(\n        byteArrayOutputStream1, 1000);\n    outputStream.write(TEST_DATA_1);\n    outputStream.close();\n\n    ByteArrayOutputStream byteArrayOutputStream2 = new ByteArrayOutputStream(1000);\n    outputStream.reset(byteArrayOutputStream2);\n    outputStream.write(TEST_DATA_2);\n    outputStream.close();\n\n    assertThat(byteArrayOutputStream1.toByteArray()).isEqualTo(TEST_DATA_1);\n    assertThat(byteArrayOutputStream2.toByteArray()).isEqualTo(TEST_DATA_2);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/util/TimedValueQueueTest.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 com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link TimedValueQueue}. */\n@RunWith(AndroidJUnit4.class)\npublic class TimedValueQueueTest {\n\n  private TimedValueQueue<String> queue;\n\n  @Before\n  public void setUp() throws Exception {\n    queue = new TimedValueQueue<>();\n  }\n\n  @Test\n  public void testAddAndPollValues() {\n    queue.add(0, \"a\");\n    queue.add(1, \"b\");\n    queue.add(2, \"c\");\n    assertThat(queue.poll(0)).isEqualTo(\"a\");\n    assertThat(queue.poll(1)).isEqualTo(\"b\");\n    assertThat(queue.poll(2)).isEqualTo(\"c\");\n  }\n\n  @Test\n  public void testBufferCapacityIncreasesAutomatically() {\n    queue = new TimedValueQueue<>(1);\n    for (int i = 0; i < 20; i++) {\n      queue.add(i, \"\" + i);\n      if ((i & 1) == 1) {\n        assertThat(queue.poll(0)).isEqualTo(\"\" + (i / 2));\n      }\n    }\n    assertThat(queue.size()).isEqualTo(10);\n  }\n\n  @Test\n  public void testTimeDiscontinuityClearsValues() {\n    queue.add(1, \"b\");\n    queue.add(2, \"c\");\n    queue.add(0, \"a\");\n    assertThat(queue.size()).isEqualTo(1);\n    assertThat(queue.poll(0)).isEqualTo(\"a\");\n  }\n\n  @Test\n  public void testTimeDiscontinuityOnFullBufferClearsValues() {\n    queue = new TimedValueQueue<>(2);\n    queue.add(1, \"b\");\n    queue.add(3, \"c\");\n    queue.add(2, \"a\");\n    assertThat(queue.size()).isEqualTo(1);\n    assertThat(queue.poll(2)).isEqualTo(\"a\");\n  }\n\n  @Test\n  public void testPollReturnsClosestValue() {\n    queue.add(0, \"a\");\n    queue.add(3, \"b\");\n    assertThat(queue.poll(2)).isEqualTo(\"b\");\n    assertThat(queue.size()).isEqualTo(0);\n  }\n\n  @Test\n  public void testPollRemovesPreviousValues() {\n    queue.add(0, \"a\");\n    queue.add(1, \"b\");\n    queue.add(2, \"c\");\n    assertThat(queue.poll(1)).isEqualTo(\"b\");\n    assertThat(queue.size()).isEqualTo(1);\n  }\n\n  @Test\n  public void testPollFloorReturnsClosestPreviousValue() {\n    queue.add(0, \"a\");\n    queue.add(3, \"b\");\n    assertThat(queue.pollFloor(2)).isEqualTo(\"a\");\n    assertThat(queue.pollFloor(2)).isEqualTo(null);\n    assertThat(queue.pollFloor(3)).isEqualTo(\"b\");\n    assertThat(queue.size()).isEqualTo(0);\n  }\n\n  @Test\n  public void testPollFloorRemovesPreviousValues() {\n    queue.add(0, \"a\");\n    queue.add(1, \"b\");\n    queue.add(2, \"c\");\n    assertThat(queue.pollFloor(1)).isEqualTo(\"b\");\n    assertThat(queue.size()).isEqualTo(1);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 com.google.android.exoplayer2.util.UriUtil.removeQueryParameter;\nimport static com.google.android.exoplayer2.util.UriUtil.resolve;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link UriUtil}. */\n@RunWith(AndroidJUnit4.class)\npublic final class UriUtilTest {\n\n  /**\n   * Tests normal usage of {@link UriUtil#resolve(String, String)}.\n   * <p>\n   * The test cases are taken from RFC-3986 5.4.1.\n   */\n  @Test\n  public void testResolveNormal() {\n    String base = \"http://a/b/c/d;p?q\";\n\n    assertThat(resolve(base, \"g:h\")).isEqualTo(\"g:h\");\n    assertThat(resolve(base, \"g\")).isEqualTo(\"http://a/b/c/g\");\n    assertThat(resolve(base, \"g/\")).isEqualTo(\"http://a/b/c/g/\");\n    assertThat(resolve(base, \"/g\")).isEqualTo(\"http://a/g\");\n    assertThat(resolve(base, \"//g\")).isEqualTo(\"http://g\");\n    assertThat(resolve(base, \"?y\")).isEqualTo(\"http://a/b/c/d;p?y\");\n    assertThat(resolve(base, \"g?y\")).isEqualTo(\"http://a/b/c/g?y\");\n    assertThat(resolve(base, \"#s\")).isEqualTo(\"http://a/b/c/d;p?q#s\");\n    assertThat(resolve(base, \"g#s\")).isEqualTo(\"http://a/b/c/g#s\");\n    assertThat(resolve(base, \"g?y#s\")).isEqualTo(\"http://a/b/c/g?y#s\");\n    assertThat(resolve(base, \";x\")).isEqualTo(\"http://a/b/c/;x\");\n    assertThat(resolve(base, \"g;x\")).isEqualTo(\"http://a/b/c/g;x\");\n    assertThat(resolve(base, \"g;x?y#s\")).isEqualTo(\"http://a/b/c/g;x?y#s\");\n    assertThat(resolve(base, \"\")).isEqualTo(\"http://a/b/c/d;p?q\");\n    assertThat(resolve(base, \".\")).isEqualTo(\"http://a/b/c/\");\n    assertThat(resolve(base, \"./\")).isEqualTo(\"http://a/b/c/\");\n    assertThat(resolve(base, \"..\")).isEqualTo(\"http://a/b/\");\n    assertThat(resolve(base, \"../\")).isEqualTo(\"http://a/b/\");\n    assertThat(resolve(base, \"../g\")).isEqualTo(\"http://a/b/g\");\n    assertThat(resolve(base, \"../..\")).isEqualTo(\"http://a/\");\n    assertThat(resolve(base, \"../../\")).isEqualTo(\"http://a/\");\n    assertThat(resolve(base, \"../../g\")).isEqualTo(\"http://a/g\");\n  }\n\n  /**\n   * Tests abnormal usage of {@link UriUtil#resolve(String, String)}.\n   * <p>\n   * The test cases are taken from RFC-3986 5.4.2.\n   */\n  @Test\n  public void testResolveAbnormal() {\n    String base = \"http://a/b/c/d;p?q\";\n\n    assertThat(resolve(base, \"../../../g\")).isEqualTo(\"http://a/g\");\n    assertThat(resolve(base, \"../../../../g\")).isEqualTo(\"http://a/g\");\n\n    assertThat(resolve(base, \"/./g\")).isEqualTo(\"http://a/g\");\n    assertThat(resolve(base, \"/../g\")).isEqualTo(\"http://a/g\");\n    assertThat(resolve(base, \"g.\")).isEqualTo(\"http://a/b/c/g.\");\n    assertThat(resolve(base, \".g\")).isEqualTo(\"http://a/b/c/.g\");\n    assertThat(resolve(base, \"g..\")).isEqualTo(\"http://a/b/c/g..\");\n    assertThat(resolve(base, \"..g\")).isEqualTo(\"http://a/b/c/..g\");\n\n    assertThat(resolve(base, \"./../g\")).isEqualTo(\"http://a/b/g\");\n    assertThat(resolve(base, \"./g/.\")).isEqualTo(\"http://a/b/c/g/\");\n    assertThat(resolve(base, \"g/./h\")).isEqualTo(\"http://a/b/c/g/h\");\n    assertThat(resolve(base, \"g/../h\")).isEqualTo(\"http://a/b/c/h\");\n    assertThat(resolve(base, \"g;x=1/./y\")).isEqualTo(\"http://a/b/c/g;x=1/y\");\n    assertThat(resolve(base, \"g;x=1/../y\")).isEqualTo(\"http://a/b/c/y\");\n\n    assertThat(resolve(base, \"g?y/./x\")).isEqualTo(\"http://a/b/c/g?y/./x\");\n    assertThat(resolve(base, \"g?y/../x\")).isEqualTo(\"http://a/b/c/g?y/../x\");\n    assertThat(resolve(base, \"g#s/./x\")).isEqualTo(\"http://a/b/c/g#s/./x\");\n    assertThat(resolve(base, \"g#s/../x\")).isEqualTo(\"http://a/b/c/g#s/../x\");\n\n    assertThat(resolve(base, \"http:g\")).isEqualTo(\"http:g\");\n  }\n\n  /**\n   * Tests additional abnormal usage of {@link UriUtil#resolve(String, String)}.\n   */\n  @Test\n  public void testResolveAbnormalAdditional() {\n    assertThat(resolve(\"http://a/b\", \"c:d/../e\")).isEqualTo(\"c:e\");\n    assertThat(resolve(\"a:b\", \"../c\")).isEqualTo(\"a:c\");\n  }\n\n  @Test\n  public void removeOnlyQueryParameter() {\n    Uri uri = Uri.parse(\"http://uri?query=value\");\n    assertThat(removeQueryParameter(uri, \"query\").toString()).isEqualTo(\"http://uri\");\n  }\n\n  @Test\n  public void removeFirstQueryParameter() {\n    Uri uri = Uri.parse(\"http://uri?query=value&second=value2\");\n    assertThat(removeQueryParameter(uri, \"query\").toString()).isEqualTo(\"http://uri?second=value2\");\n  }\n\n  @Test\n  public void removeMiddleQueryParameter() {\n    Uri uri = Uri.parse(\"http://uri?first=value1&query=value&last=value2\");\n    assertThat(removeQueryParameter(uri, \"query\").toString())\n        .isEqualTo(\"http://uri?first=value1&last=value2\");\n  }\n\n  @Test\n  public void removeLastQueryParameter() {\n    Uri uri = Uri.parse(\"http://uri?first=value1&query=value\");\n    assertThat(removeQueryParameter(uri, \"query\").toString()).isEqualTo(\"http://uri?first=value1\");\n  }\n\n  @Test\n  public void removeNonExistentQueryParameter() {\n    Uri uri = Uri.parse(\"http://uri\");\n    assertThat(removeQueryParameter(uri, \"foo\").toString()).isEqualTo(\"http://uri\");\n    uri = Uri.parse(\"http://uri?query=value\");\n    assertThat(removeQueryParameter(uri, \"foo\").toString()).isEqualTo(\"http://uri?query=value\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 com.google.android.exoplayer2.util.Util.binarySearchCeil;\nimport static com.google.android.exoplayer2.util.Util.binarySearchFloor;\nimport static com.google.android.exoplayer2.util.Util.escapeFileName;\nimport static com.google.android.exoplayer2.util.Util.getCodecsOfType;\nimport static com.google.android.exoplayer2.util.Util.parseXsDateTime;\nimport static com.google.android.exoplayer2.util.Util.parseXsDuration;\nimport static com.google.android.exoplayer2.util.Util.unescapeFileName;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Random;\nimport java.util.zip.Deflater;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/** Unit tests for {@link Util}. */\n@RunWith(AndroidJUnit4.class)\npublic class UtilTest {\n\n  @Test\n  public void testAddWithOverflowDefault() {\n    long res = Util.addWithOverflowDefault(5, 10, /* overflowResult= */ 0);\n    assertThat(res).isEqualTo(15);\n\n    res = Util.addWithOverflowDefault(Long.MAX_VALUE - 1, 1, /* overflowResult= */ 12345);\n    assertThat(res).isEqualTo(Long.MAX_VALUE);\n\n    res = Util.addWithOverflowDefault(Long.MIN_VALUE + 1, -1, /* overflowResult= */ 12345);\n    assertThat(res).isEqualTo(Long.MIN_VALUE);\n\n    res = Util.addWithOverflowDefault(Long.MAX_VALUE, 1, /* overflowResult= */ 12345);\n    assertThat(res).isEqualTo(12345);\n\n    res = Util.addWithOverflowDefault(Long.MIN_VALUE, -1, /* overflowResult= */ 12345);\n    assertThat(res).isEqualTo(12345);\n  }\n\n  @Test\n  public void testSubtrackWithOverflowDefault() {\n    long res = Util.subtractWithOverflowDefault(5, 10, /* overflowResult= */ 0);\n    assertThat(res).isEqualTo(-5);\n\n    res = Util.subtractWithOverflowDefault(Long.MIN_VALUE + 1, 1, /* overflowResult= */ 12345);\n    assertThat(res).isEqualTo(Long.MIN_VALUE);\n\n    res = Util.subtractWithOverflowDefault(Long.MAX_VALUE - 1, -1, /* overflowResult= */ 12345);\n    assertThat(res).isEqualTo(Long.MAX_VALUE);\n\n    res = Util.subtractWithOverflowDefault(Long.MIN_VALUE, 1, /* overflowResult= */ 12345);\n    assertThat(res).isEqualTo(12345);\n\n    res = Util.subtractWithOverflowDefault(Long.MAX_VALUE, -1, /* overflowResult= */ 12345);\n    assertThat(res).isEqualTo(12345);\n  }\n\n  @Test\n  public void testInferContentType() {\n    assertThat(Util.inferContentType(\"http://a.b/c.ism\")).isEqualTo(C.TYPE_SS);\n    assertThat(Util.inferContentType(\"http://a.b/c.isml\")).isEqualTo(C.TYPE_SS);\n    assertThat(Util.inferContentType(\"http://a.b/c.ism/Manifest\")).isEqualTo(C.TYPE_SS);\n    assertThat(Util.inferContentType(\"http://a.b/c.isml/manifest\")).isEqualTo(C.TYPE_SS);\n    assertThat(Util.inferContentType(\"http://a.b/c.isml/manifest(filter=x)\")).isEqualTo(C.TYPE_SS);\n\n    assertThat(Util.inferContentType(\"http://a.b/c.ism/prefix-manifest\")).isEqualTo(C.TYPE_OTHER);\n    assertThat(Util.inferContentType(\"http://a.b/c.ism/manifest-suffix\")).isEqualTo(C.TYPE_OTHER);\n  }\n\n  @Test\n  public void testArrayBinarySearchFloor() {\n    long[] values = new long[0];\n    assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1);\n    assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0);\n\n    values = new long[] {1, 3, 5};\n    assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1);\n    assertThat(binarySearchFloor(values, 0, true, false)).isEqualTo(-1);\n    assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0);\n    assertThat(binarySearchFloor(values, 0, true, true)).isEqualTo(0);\n\n    assertThat(binarySearchFloor(values, 1, false, false)).isEqualTo(-1);\n    assertThat(binarySearchFloor(values, 1, true, false)).isEqualTo(0);\n    assertThat(binarySearchFloor(values, 1, false, true)).isEqualTo(0);\n    assertThat(binarySearchFloor(values, 1, true, true)).isEqualTo(0);\n\n    assertThat(binarySearchFloor(values, 4, false, false)).isEqualTo(1);\n    assertThat(binarySearchFloor(values, 4, true, false)).isEqualTo(1);\n\n    assertThat(binarySearchFloor(values, 5, false, false)).isEqualTo(1);\n    assertThat(binarySearchFloor(values, 5, true, false)).isEqualTo(2);\n\n    assertThat(binarySearchFloor(values, 6, false, false)).isEqualTo(2);\n    assertThat(binarySearchFloor(values, 6, true, false)).isEqualTo(2);\n  }\n\n  @Test\n  public void testListBinarySearchFloor() {\n    List<Integer> values = new ArrayList<>();\n    assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1);\n    assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0);\n\n    values.add(1);\n    values.add(3);\n    values.add(5);\n    assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1);\n    assertThat(binarySearchFloor(values, 0, true, false)).isEqualTo(-1);\n    assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0);\n    assertThat(binarySearchFloor(values, 0, true, true)).isEqualTo(0);\n\n    assertThat(binarySearchFloor(values, 1, false, false)).isEqualTo(-1);\n    assertThat(binarySearchFloor(values, 1, true, false)).isEqualTo(0);\n    assertThat(binarySearchFloor(values, 1, false, true)).isEqualTo(0);\n    assertThat(binarySearchFloor(values, 1, true, true)).isEqualTo(0);\n\n    assertThat(binarySearchFloor(values, 4, false, false)).isEqualTo(1);\n    assertThat(binarySearchFloor(values, 4, true, false)).isEqualTo(1);\n\n    assertThat(binarySearchFloor(values, 5, false, false)).isEqualTo(1);\n    assertThat(binarySearchFloor(values, 5, true, false)).isEqualTo(2);\n\n    assertThat(binarySearchFloor(values, 6, false, false)).isEqualTo(2);\n    assertThat(binarySearchFloor(values, 6, true, false)).isEqualTo(2);\n  }\n\n  @Test\n  public void testArrayBinarySearchCeil() {\n    long[] values = new long[0];\n    assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0);\n    assertThat(binarySearchCeil(values, 0, false, true)).isEqualTo(-1);\n\n    values = new long[] {1, 3, 5};\n    assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0);\n    assertThat(binarySearchCeil(values, 0, true, false)).isEqualTo(0);\n\n    assertThat(binarySearchCeil(values, 1, false, false)).isEqualTo(1);\n    assertThat(binarySearchCeil(values, 1, true, false)).isEqualTo(0);\n\n    assertThat(binarySearchCeil(values, 2, false, false)).isEqualTo(1);\n    assertThat(binarySearchCeil(values, 2, true, false)).isEqualTo(1);\n\n    assertThat(binarySearchCeil(values, 5, false, false)).isEqualTo(3);\n    assertThat(binarySearchCeil(values, 5, true, false)).isEqualTo(2);\n    assertThat(binarySearchCeil(values, 5, false, true)).isEqualTo(2);\n    assertThat(binarySearchCeil(values, 5, true, true)).isEqualTo(2);\n\n    assertThat(binarySearchCeil(values, 6, false, false)).isEqualTo(3);\n    assertThat(binarySearchCeil(values, 6, true, false)).isEqualTo(3);\n    assertThat(binarySearchCeil(values, 6, false, true)).isEqualTo(2);\n    assertThat(binarySearchCeil(values, 6, true, true)).isEqualTo(2);\n  }\n\n  @Test\n  public void testListBinarySearchCeil() {\n    List<Integer> values = new ArrayList<>();\n    assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0);\n    assertThat(binarySearchCeil(values, 0, false, true)).isEqualTo(-1);\n\n    values.add(1);\n    values.add(3);\n    values.add(5);\n    assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0);\n    assertThat(binarySearchCeil(values, 0, true, false)).isEqualTo(0);\n\n    assertThat(binarySearchCeil(values, 1, false, false)).isEqualTo(1);\n    assertThat(binarySearchCeil(values, 1, true, false)).isEqualTo(0);\n\n    assertThat(binarySearchCeil(values, 2, false, false)).isEqualTo(1);\n    assertThat(binarySearchCeil(values, 2, true, false)).isEqualTo(1);\n\n    assertThat(binarySearchCeil(values, 5, false, false)).isEqualTo(3);\n    assertThat(binarySearchCeil(values, 5, true, false)).isEqualTo(2);\n    assertThat(binarySearchCeil(values, 5, false, true)).isEqualTo(2);\n    assertThat(binarySearchCeil(values, 5, true, true)).isEqualTo(2);\n\n    assertThat(binarySearchCeil(values, 6, false, false)).isEqualTo(3);\n    assertThat(binarySearchCeil(values, 6, true, false)).isEqualTo(3);\n    assertThat(binarySearchCeil(values, 6, false, true)).isEqualTo(2);\n    assertThat(binarySearchCeil(values, 6, true, true)).isEqualTo(2);\n  }\n\n  @Test\n  public void testParseXsDuration() {\n    assertThat(parseXsDuration(\"PT150.279S\")).isEqualTo(150279L);\n    assertThat(parseXsDuration(\"PT1.500S\")).isEqualTo(1500L);\n  }\n\n  @Test\n  public void testParseXsDateTime() throws Exception {\n    assertThat(parseXsDateTime(\"2014-06-19T23:07:42\")).isEqualTo(1403219262000L);\n    assertThat(parseXsDateTime(\"2014-08-06T11:00:00Z\")).isEqualTo(1407322800000L);\n    assertThat(parseXsDateTime(\"2014-08-06T11:00:00,000Z\")).isEqualTo(1407322800000L);\n    assertThat(parseXsDateTime(\"2014-09-19T13:18:55-08:00\")).isEqualTo(1411161535000L);\n    assertThat(parseXsDateTime(\"2014-09-19T13:18:55-0800\")).isEqualTo(1411161535000L);\n    assertThat(parseXsDateTime(\"2014-09-19T13:18:55.000-0800\")).isEqualTo(1411161535000L);\n    assertThat(parseXsDateTime(\"2014-09-19T13:18:55.000-800\")).isEqualTo(1411161535000L);\n  }\n\n  @Test\n  public void testGetCodecsOfType() {\n    assertThat(getCodecsOfType(null, C.TRACK_TYPE_VIDEO)).isNull();\n    assertThat(getCodecsOfType(\"avc1.64001e,vp9.63.1\", C.TRACK_TYPE_AUDIO)).isNull();\n    assertThat(getCodecsOfType(\" vp9.63.1, ec-3 \", C.TRACK_TYPE_AUDIO)).isEqualTo(\"ec-3\");\n    assertThat(getCodecsOfType(\"avc1.61e, vp9.63.1, ec-3 \", C.TRACK_TYPE_VIDEO))\n        .isEqualTo(\"avc1.61e,vp9.63.1\");\n    assertThat(getCodecsOfType(\"avc1.61e, vp9.63.1, ec-3 \", C.TRACK_TYPE_VIDEO))\n        .isEqualTo(\"avc1.61e,vp9.63.1\");\n    assertThat(getCodecsOfType(\"invalidCodec1, invalidCodec2 \", C.TRACK_TYPE_AUDIO)).isNull();\n  }\n\n  @Test\n  public void testUnescapeInvalidFileName() {\n    assertThat(Util.unescapeFileName(\"%a\")).isNull();\n    assertThat(Util.unescapeFileName(\"%xyz\")).isNull();\n  }\n\n  @Test\n  public void testEscapeUnescapeFileName() {\n    assertEscapeUnescapeFileName(\"just+a regular+fileName\", \"just+a regular+fileName\");\n    assertEscapeUnescapeFileName(\"key:value\", \"key%3avalue\");\n    assertEscapeUnescapeFileName(\"<>:\\\"/\\\\|?*%\", \"%3c%3e%3a%22%2f%5c%7c%3f%2a%25\");\n\n    Random random = new Random(0);\n    for (int i = 0; i < 1000; i++) {\n      String string = TestUtil.buildTestString(1000, random);\n      assertEscapeUnescapeFileName(string);\n    }\n  }\n\n  @Test\n  public void testInflate() {\n    byte[] testData = TestUtil.buildTestData(/*arbitrary test data size*/ 256 * 1024);\n    byte[] compressedData = new byte[testData.length * 2];\n    Deflater compresser = new Deflater(9);\n    compresser.setInput(testData);\n    compresser.finish();\n    int compressedDataLength = compresser.deflate(compressedData);\n    compresser.end();\n\n    ParsableByteArray input = new ParsableByteArray(compressedData, compressedDataLength);\n    ParsableByteArray output = new ParsableByteArray();\n    assertThat(Util.inflate(input, output, /* inflater= */ null)).isTrue();\n    assertThat(output.limit()).isEqualTo(testData.length);\n    assertThat(Arrays.copyOf(output.data, output.limit())).isEqualTo(testData);\n  }\n\n  @Test\n  @Config(sdk = 21)\n  public void testNormalizeLanguageCodeV21() {\n    assertThat(Util.normalizeLanguageCode(null)).isNull();\n    assertThat(Util.normalizeLanguageCode(\"\")).isEmpty();\n    assertThat(Util.normalizeLanguageCode(\"es\")).isEqualTo(\"es\");\n    assertThat(Util.normalizeLanguageCode(\"spa\")).isEqualTo(\"es\");\n    assertThat(Util.normalizeLanguageCode(\"es-AR\")).isEqualTo(\"es-ar\");\n    assertThat(Util.normalizeLanguageCode(\"SpA-ar\")).isEqualTo(\"es-ar\");\n    assertThat(Util.normalizeLanguageCode(\"es_AR\")).isEqualTo(\"es-ar\");\n    assertThat(Util.normalizeLanguageCode(\"spa_ar\")).isEqualTo(\"es-ar\");\n    assertThat(Util.normalizeLanguageCode(\"es-AR-dialect\")).isEqualTo(\"es-ar-dialect\");\n    assertThat(Util.normalizeLanguageCode(\"ES-419\")).isEqualTo(\"es-419\");\n    assertThat(Util.normalizeLanguageCode(\"zh-hans-tw\")).isEqualTo(\"zh-hans-tw\");\n    assertThat(Util.normalizeLanguageCode(\"zh-tw-hans\")).isEqualTo(\"zh-tw\");\n    assertThat(Util.normalizeLanguageCode(\"zho-hans-tw\")).isEqualTo(\"zh-hans-tw\");\n    assertThat(Util.normalizeLanguageCode(\"und\")).isEqualTo(\"und\");\n    assertThat(Util.normalizeLanguageCode(\"DoesNotExist\")).isEqualTo(\"doesnotexist\");\n  }\n\n  @Test\n  @Config(sdk = 16)\n  public void testNormalizeLanguageCode() {\n    assertThat(Util.normalizeLanguageCode(null)).isNull();\n    assertThat(Util.normalizeLanguageCode(\"\")).isEmpty();\n    assertThat(Util.normalizeLanguageCode(\"es\")).isEqualTo(\"es\");\n    assertThat(Util.normalizeLanguageCode(\"spa\")).isEqualTo(\"es\");\n    assertThat(Util.normalizeLanguageCode(\"es-AR\")).isEqualTo(\"es-ar\");\n    assertThat(Util.normalizeLanguageCode(\"SpA-ar\")).isEqualTo(\"es-ar\");\n    assertThat(Util.normalizeLanguageCode(\"es_AR\")).isEqualTo(\"es-ar\");\n    assertThat(Util.normalizeLanguageCode(\"spa_ar\")).isEqualTo(\"es-ar\");\n    assertThat(Util.normalizeLanguageCode(\"es-AR-dialect\")).isEqualTo(\"es-ar-dialect\");\n    assertThat(Util.normalizeLanguageCode(\"ES-419\")).isEqualTo(\"es-419\");\n    assertThat(Util.normalizeLanguageCode(\"zh-hans-tw\")).isEqualTo(\"zh-hans-tw\");\n    // Doesn't work on API < 21 because we can't use Locale syntax verification.\n    // assertThat(Util.normalizeLanguageCode(\"zh-tw-hans\")).isEqualTo(\"zh-tw\");\n    assertThat(Util.normalizeLanguageCode(\"zho-hans-tw\")).isEqualTo(\"zh-hans-tw\");\n    assertThat(Util.normalizeLanguageCode(\"und\")).isEqualTo(\"und\");\n    assertThat(Util.normalizeLanguageCode(\"DoesNotExist\")).isEqualTo(\"doesnotexist\");\n  }\n\n  @Test\n  public void testNormalizeIso6392BibliographicalAndTextualCodes() {\n    // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes.\n    assertThat(Util.normalizeLanguageCode(\"alb\")).isEqualTo(Util.normalizeLanguageCode(\"sqi\"));\n    assertThat(Util.normalizeLanguageCode(\"arm\")).isEqualTo(Util.normalizeLanguageCode(\"hye\"));\n    assertThat(Util.normalizeLanguageCode(\"baq\")).isEqualTo(Util.normalizeLanguageCode(\"eus\"));\n    assertThat(Util.normalizeLanguageCode(\"bur\")).isEqualTo(Util.normalizeLanguageCode(\"mya\"));\n    assertThat(Util.normalizeLanguageCode(\"chi\")).isEqualTo(Util.normalizeLanguageCode(\"zho\"));\n    assertThat(Util.normalizeLanguageCode(\"cze\")).isEqualTo(Util.normalizeLanguageCode(\"ces\"));\n    assertThat(Util.normalizeLanguageCode(\"dut\")).isEqualTo(Util.normalizeLanguageCode(\"nld\"));\n    assertThat(Util.normalizeLanguageCode(\"fre\")).isEqualTo(Util.normalizeLanguageCode(\"fra\"));\n    assertThat(Util.normalizeLanguageCode(\"geo\")).isEqualTo(Util.normalizeLanguageCode(\"kat\"));\n    assertThat(Util.normalizeLanguageCode(\"ger\")).isEqualTo(Util.normalizeLanguageCode(\"deu\"));\n    assertThat(Util.normalizeLanguageCode(\"gre\")).isEqualTo(Util.normalizeLanguageCode(\"ell\"));\n    assertThat(Util.normalizeLanguageCode(\"ice\")).isEqualTo(Util.normalizeLanguageCode(\"isl\"));\n    assertThat(Util.normalizeLanguageCode(\"mac\")).isEqualTo(Util.normalizeLanguageCode(\"mkd\"));\n    assertThat(Util.normalizeLanguageCode(\"mao\")).isEqualTo(Util.normalizeLanguageCode(\"mri\"));\n    assertThat(Util.normalizeLanguageCode(\"may\")).isEqualTo(Util.normalizeLanguageCode(\"msa\"));\n    assertThat(Util.normalizeLanguageCode(\"per\")).isEqualTo(Util.normalizeLanguageCode(\"fas\"));\n    assertThat(Util.normalizeLanguageCode(\"rum\")).isEqualTo(Util.normalizeLanguageCode(\"ron\"));\n    assertThat(Util.normalizeLanguageCode(\"slo\")).isEqualTo(Util.normalizeLanguageCode(\"slk\"));\n    assertThat(Util.normalizeLanguageCode(\"tib\")).isEqualTo(Util.normalizeLanguageCode(\"bod\"));\n    assertThat(Util.normalizeLanguageCode(\"wel\")).isEqualTo(Util.normalizeLanguageCode(\"cym\"));\n  }\n\n  private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) {\n    assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName);\n    assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);\n  }\n\n  private static void assertEscapeUnescapeFileName(String fileName) {\n    String escapedFileName = Util.escapeFileName(fileName);\n    assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/FrameRotationQueueTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.opengl.Matrix;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests {@link FrameRotationQueue}. */\n@RunWith(AndroidJUnit4.class)\npublic class FrameRotationQueueTest {\n\n  private FrameRotationQueue frameRotationQueue;\n  private float[] rotationMatrix;\n\n  @Before\n  public void setUp() throws Exception {\n    frameRotationQueue = new FrameRotationQueue();\n    rotationMatrix = new float[16];\n  }\n\n  @Test\n  public void testGetRotationMatrixReturnsNull_whenEmpty() throws Exception {\n    assertThat(frameRotationQueue.pollRotationMatrix(rotationMatrix, 0)).isFalse();\n  }\n\n  @Test\n  public void testGetRotationMatrixReturnsNotNull_whenNotEmpty() throws Exception {\n    frameRotationQueue.setRotation(0, new float[] {1, 2, 3});\n    assertThat(frameRotationQueue.pollRotationMatrix(rotationMatrix, 0)).isTrue();\n    assertThat(rotationMatrix).hasLength(16);\n  }\n\n  @Test\n  public void testConvertsAngleAxisToRotationMatrix() throws Exception {\n    doTestAngleAxisToRotationMatrix(/* angleRadian= */ 0, /* x= */ 1, /* y= */ 0, /* z= */ 0);\n    frameRotationQueue.reset();\n    doTestAngleAxisToRotationMatrix(/* angleRadian= */ 1, /* x= */ 1, /* y= */ 0, /* z= */ 0);\n    frameRotationQueue.reset();\n    doTestAngleAxisToRotationMatrix(/* angleRadian= */ 1, /* x= */ 0, /* y= */ 0, /* z= */ 1);\n    // Don't reset frameRotationQueue as we use recenter matrix from previous calls.\n    doTestAngleAxisToRotationMatrix(/* angleRadian= */ -1, /* x= */ 0, /* y= */ 1, /* z= */ 0);\n    doTestAngleAxisToRotationMatrix(/* angleRadian= */ 1, /* x= */ 1, /* y= */ 1, /* z= */ 1);\n  }\n\n  @Test\n  public void testRecentering_justYaw() throws Exception {\n    float[] actualMatrix =\n        getRotationMatrixFromAngleAxis(\n            /* angleRadian= */ (float) Math.PI, /* x= */ 0, /* y= */ 1, /* z= */ 0);\n    float[] expectedMatrix = new float[16];\n    Matrix.setIdentityM(expectedMatrix, 0);\n    assertEquals(actualMatrix, expectedMatrix);\n  }\n\n  @Test\n  public void testRecentering_yawAndPitch() throws Exception {\n    float[] matrix =\n        getRotationMatrixFromAngleAxis(\n            /* angleRadian= */ (float) Math.PI, /* x= */ 1, /* y= */ 1, /* z= */ 0);\n    assertMultiplication(\n        /* xr= */ 0, /* yr= */ 0, /* zr= */ 1, matrix, /* x= */ 0, /* y= */ 0, /* z= */ 1);\n  }\n\n  @Test\n  public void testRecentering_yawAndPitch2() throws Exception {\n    float[] matrix =\n        getRotationMatrixFromAngleAxis(\n            /* angleRadian= */ (float) Math.PI / 2, /* x= */ 1, /* y= */ 1, /* z= */ 0);\n    float sqrt2 = (float) Math.sqrt(2);\n    assertMultiplication(\n        /* xr= */ sqrt2, /* yr= */ 0, /* zr= */ 0, matrix, /* x= */ 1, /* y= */ -1, /* z= */ 0);\n  }\n\n  @Test\n  public void testRecentering_yawAndPitchAndRoll() throws Exception {\n    float[] matrix =\n        getRotationMatrixFromAngleAxis(\n            /* angleRadian= */ (float) Math.PI * 2 / 3, /* x= */ 1, /* y= */ 1, /* z= */ 1);\n    assertMultiplication(\n        /* xr= */ 0, /* yr= */ 0, /* zr= */ 1, matrix, /* x= */ 0, /* y= */ 0, /* z= */ 1);\n  }\n\n  private void doTestAngleAxisToRotationMatrix(float angleRadian, int x, int y, int z) {\n    float[] actualMatrix = getRotationMatrixFromAngleAxis(angleRadian, x, y, z);\n    float[] expectedMatrix = createRotationMatrix(angleRadian, x, y, z);\n    assertEquals(actualMatrix, expectedMatrix);\n  }\n\n  private float[] getRotationMatrixFromAngleAxis(float angleRadian, int x, int y, int z) {\n    float length = Matrix.length(x, y, z);\n    float factor = angleRadian / length;\n    // Negate y and z to revert OpenGL coordinate system conversion.\n    frameRotationQueue.setRotation(0, new float[] {x * factor, -y * factor, -z * factor});\n    frameRotationQueue.pollRotationMatrix(rotationMatrix, 0);\n    return rotationMatrix;\n  }\n\n  private static void assertMultiplication(\n      float xr, float yr, float zr, float[] actualMatrix, float x, float y, float z) {\n    float[] vector = new float[] {x, y, z, 0};\n    float[] resultVec = new float[4];\n    Matrix.multiplyMV(resultVec, 0, actualMatrix, 0, vector, 0);\n    assertEquals(resultVec, new float[] {xr, yr, zr, 0});\n  }\n\n  private static float[] createRotationMatrix(float angleRadian, int x, int y, int z) {\n    float[] expectedMatrix = new float[16];\n    Matrix.setRotateM(expectedMatrix, 0, (float) Math.toDegrees(angleRadian), x, y, z);\n    return expectedMatrix;\n  }\n\n  private static void assertEquals(float[] actual, float[] expected) {\n    assertThat(actual).usingTolerance(1.0e-5).containsExactly(expected).inOrder();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoderTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\nimport junit.framework.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link ProjectionDecoder}. */\n@RunWith(AndroidJUnit4.class)\npublic final class ProjectionDecoderTest {\n\n  private static final byte[] PROJ_DATA =\n      Util.getBytesFromHexString(\n          \"0000008D70726F6A0000008579746D7000000000ABA158D672617720000000716D65736800000006BF800000\"\n              + \"3F8000003F0000003F2AAAAB000000003EAAAAAB000000100024200104022430010421034020400123\"\n              + \"1020401013020010102222001001003100200010320010000000010000000000240084009066080420\"\n              + \"9020108421002410860214C1200660\");\n\n  private static final int MSHP_OFFSET = 16;\n  private static final int VERTEX_COUNT = 36;\n  private static final float[] FIRST_VERTEX = {-1.0f, -1.0f, 1.0f};\n  private static final float[] LAST_VERTEX = {1.0f, -1.0f, -1.0f};\n  private static final float[] FIRST_UV = {0.5f, 1.0f};\n  private static final float[] LAST_UV = {1.0f, 1.0f};\n\n  @Test\n  public void testDecodeProj() {\n    testDecoding(PROJ_DATA);\n  }\n\n  @Test\n  public void testDecodeMshp() {\n    testDecoding(Arrays.copyOfRange(PROJ_DATA, MSHP_OFFSET, PROJ_DATA.length));\n  }\n\n  private static void testDecoding(byte[] data) {\n    Projection projection = ProjectionDecoder.decode(data, C.STEREO_MODE_MONO);\n    assertThat(projection).isNotNull();\n    assertThat(projection.stereoMode).isEqualTo(C.STEREO_MODE_MONO);\n    assertThat(projection.leftMesh).isNotNull();\n    assertThat(projection.rightMesh).isNotNull();\n    assertThat(projection.singleMesh).isTrue();\n    testSubMesh(projection.leftMesh);\n  }\n\n  /** Tests the that SubMesh (mesh with the video) contains expected data. */\n  private static void testSubMesh(Projection.Mesh leftMesh) {\n    assertThat(leftMesh.getSubMeshCount()).isEqualTo(1);\n\n    Projection.SubMesh subMesh = leftMesh.getSubMesh(0);\n    assertThat(subMesh.mode).isEqualTo(Projection.DRAW_MODE_TRIANGLES);\n\n    float[] vertices = subMesh.vertices;\n    float[] uv = subMesh.textureCoords;\n    assertThat(vertices.length).isEqualTo(VERTEX_COUNT * 3);\n    assertThat(subMesh.textureCoords.length).isEqualTo(VERTEX_COUNT * 2);\n\n    // Test first vertex\n    testCoordinate(FIRST_VERTEX, vertices, 0, 3);\n    // Test last vertex\n    testCoordinate(LAST_VERTEX, vertices, VERTEX_COUNT * 3 - 3, 3);\n\n    // Test first uv\n    testCoordinate(FIRST_UV, uv, 0, 2);\n    // Test last uv\n    testCoordinate(LAST_UV, uv, VERTEX_COUNT * 2 - 2, 2);\n  }\n\n  /** Tests that the output coordinates match the expected. */\n  private static void testCoordinate(float[] expected, float[] output, int offset, int count) {\n    for (int i = 0; i < count; i++) {\n      Assert.assertEquals(expected[i], output[i + offset]);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/core/src/test/java/com/google/android/exoplayer2/video/spherical/ProjectionTest.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 static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.assertEquals;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link Projection}. */\n@RunWith(AndroidJUnit4.class)\npublic class ProjectionTest {\n  private static final float EPSILON = .00001f;\n\n  // Default 360 sphere.\n  private static final float RADIUS = 1;\n  private static final int LATITUDES = 12;\n  private static final int LONGITUDES = 24;\n  private static final float VERTICAL_FOV_DEGREES = 180;\n  private static final float HORIZONTAL_FOV_DEGREES = 360;\n\n  @Test\n  public void testSphericalMesh() throws Exception {\n    // Only the first param is important in this test.\n    Projection projection =\n        Projection.createEquirectangular(\n            RADIUS,\n            LATITUDES,\n            LONGITUDES,\n            VERTICAL_FOV_DEGREES,\n            HORIZONTAL_FOV_DEGREES,\n            C.STEREO_MODE_MONO);\n\n    Projection.SubMesh subMesh = projection.leftMesh.getSubMesh(0);\n    assertThat(subMesh.getVertexCount()).isGreaterThan(LATITUDES * LONGITUDES);\n\n    float[] data = subMesh.vertices;\n    for (int i = 0; i < data.length; ) {\n      float x = data[i++];\n      float y = data[i++];\n      float z = data[i++];\n      assertEquals(RADIUS, Math.sqrt(x * x + y * y + z * z), EPSILON);\n    }\n  }\n\n  @Test\n  public void testArgumentValidation() {\n    checkIllegalArgumentException(0, 1, 1, 1, 1);\n    checkIllegalArgumentException(1, 0, 1, 1, 1);\n    checkIllegalArgumentException(1, 1, 0, 1, 1);\n    checkIllegalArgumentException(1, 1, 1, 0, 1);\n    checkIllegalArgumentException(1, 1, 1, 181, 1);\n    checkIllegalArgumentException(1, 1, 1, 1, 0);\n    checkIllegalArgumentException(1, 1, 1, 1, 361);\n  }\n\n  private void checkIllegalArgumentException(\n      float radius,\n      int latitudes,\n      int longitudes,\n      float verticalFovDegrees,\n      float horizontalFovDegrees) {\n    try {\n      Projection.createEquirectangular(\n          radius,\n          latitudes,\n          longitudes,\n          verticalFovDegrees,\n          horizontalFovDegrees,\n          C.STEREO_MODE_MONO);\n      fail();\n    } catch (IllegalArgumentException e) {\n      // Do nothing. Expected.\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/README.md",
    "content": "# ExoPlayer DASH library module #\n\nProvides support for Dynamic Adaptive Streaming over HTTP (DASH) content. To\nplay DASH content, instantiate a `DashMediaSource` and pass it to\n`ExoPlayer.prepare`.\n\n## Links ##\n\n* [Developer Guide][].\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.dash.*`\n  belong to this module.\n\n[Developer Guide]: https://exoplayer.dev/dash.html\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/build.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.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    buildTypes {\n        // Re-enable test coverage when the following issue is fixed:\n        // https://issuetracker.google.com/issues/37019591\n        // debug {\n        //    testCoverageEnabled = true\n        // }\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n\n    // gradle 4.6 migration: disable dimensions mechanism\n    // more: https://proandroiddev.com/advanced-android-flavors-part-4-a-new-version-fc2ad80c01bb\n    flavorDimensions \"default\"\n\n    productFlavors {\n        stbeta {}\n        ststable {}\n        stfdroid {}\n    }\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    implementation project(':mediaserviceinterfaces')\n    implementation project(':sharedutils')\n    implementation project(':youtubeapi')\n    compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion\n    implementation 'androidx.annotation:annotation:1.1.0'\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n}\n\next {\n    javadocTitle = 'DASH module'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'exoplayer-dash'\n    releaseDescription = 'The ExoPlayer library DASH module.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n\n<manifest package=\"com.google.android.exoplayer2.source.dash\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/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-amzn-2.10.6/library/dash/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-amzn-2.10.6/library/dash/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 androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport android.util.Pair;\nimport android.util.SparseIntArray;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.SeekParameters;\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;\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  private final @Nullable TransferListener transferListener;\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  private @Nullable 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      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.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 = buildTrackGroups(period.adaptationSets,\n        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 i = 0; i < trackIndices.length; i++) {\n        while (trackIndices[i]\n            >= 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                trackIndices[i] - totalTracksInPreviousAdaptationSets));\n      }\n    }\n    return streamKeys;\n  }\n\n  @Override\n  public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,\n      SampleStream[] streams, boolean[] streamResetFlags, 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 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      List<AdaptationSet> adaptationSets, 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            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 (int j = 0; j < extraAdaptationSetIds.length; j++) {\n          int extraIndex =\n              idToIndexMap.get(\n                  Integer.parseInt(extraAdaptationSetIds[j]), /* 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      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        formats[j] = representations.get(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            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  @SuppressWarnings(\"unchecked\")\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-amzn-2.10.6/library/dash/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\nimport android.util.SparseArray;\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.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.SequenceableLoader;\nimport com.google.android.exoplayer2.source.ads.AdsMediaSource;\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 AdsMediaSource.MediaSourceFactory {\n\n    private final DashChunkSource.Factory chunkSourceFactory;\n    @Nullable private final DataSource.Factory manifestDataSourceFactory;\n\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      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(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    /** @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 a list of {@link StreamKey stream keys} by which the manifest is filtered.\n     *\n     * @param streamKeys A list of {@link StreamKey stream keys}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setStreamKeys(List<StreamKey> streamKeys) {\n      Assertions.checkState(!isCreateCalled);\n      this.streamKeys = streamKeys;\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          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     * 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          loadErrorHandlingPolicy,\n          livePresentationDelayMs,\n          livePresentationDelayOverridesManifest,\n          tag);\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    @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   * SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the\n   * source's {@link 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 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  private final @Nullable Object tag;\n\n  private DataSource dataSource;\n  private Loader loader;\n  private @Nullable 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      Handler eventHandler,\n      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      Handler eventHandler,\n      MediaSourceEventListener eventListener) {\n    this(\n        manifest,\n        /* manifestUri= */ null,\n        /* manifestDataSourceFactory= */ null,\n        /* manifestParser= */ null,\n        chunkSourceFactory,\n        new DefaultCompositeSequenceableLoaderFactory(),\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      Handler eventHandler,\n      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      Handler eventHandler,\n      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      Handler eventHandler,\n      MediaSourceEventListener eventListener) {\n    this(\n        /* manifest= */ null,\n        manifestUri,\n        manifestDataSourceFactory,\n        manifestParser,\n        chunkSourceFactory,\n        new DefaultCompositeSequenceableLoaderFactory(),\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      DashManifest manifest,\n      Uri manifestUri,\n      DataSource.Factory manifestDataSourceFactory,\n      ParsingLoadable.Parser<? extends DashManifest> manifestParser,\n      DashChunkSource.Factory chunkSourceFactory,\n      CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\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.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  public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    this.mediaTransferListener = mediaTransferListener;\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            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  public 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  }\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 = manifest.availabilityStartTimeMs\n        + manifest.getPeriod(0).startMs + C.usToMs(currentStartTimeUs);\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, manifest);\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    private final @Nullable 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(\n        int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {\n      Assertions.checkIndex(windowIndex, 0, 1);\n      long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs(\n          defaultPositionProjectionUs);\n      Object tag = setTag ? windowTag : null;\n      boolean isDynamic =\n          manifest.dynamic\n              && manifest.minUpdatePeriodMs != C.TIME_UNSET\n              && manifest.durationMs == C.TIME_UNSET;\n      return window.set(\n          tag,\n          presentationStartTimeMs,\n          windowStartTimeMs,\n          /* isSeekable= */ true,\n          isDynamic,\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 (!manifest.dynamic) {\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\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-amzn-2.10.6/library/dash/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-amzn-2.10.6/library/dash/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  public static @Nullable 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  public static @Nullable 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  public static @Nullable 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  private static @Nullable 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  private static @Nullable 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}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/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-amzn-2.10.6/library/dash/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, 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    public final @Nullable 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        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        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-amzn-2.10.6/library/dash/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.setFlags(C.BUFFER_FLAG_KEY_FRAME);\n      buffer.data.put(serializedEvent);\n      buffer.timeUs = eventTimesUs[sampleIndex];\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-amzn-2.10.6/library/dash/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.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(new SampleQueue(allocator));\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.hasNextSample()) {\n        MetadataInputBuffer inputBuffer = dequeueSample();\n        if (inputBuffer == null) {\n          continue;\n        }\n        long eventTimeUs = inputBuffer.timeUs;\n        Metadata metadata = decoder.decode(inputBuffer);\n        if (metadata == null) {\n          continue;\n        }\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 = sampleQueue.read(formatHolder, buffer, false, false, 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-amzn-2.10.6/library/dash/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-amzn-2.10.6/library/dash/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  public final UtcTimingElement utcTiming;\n\n  /**\n   * The location of this manifest.\n   */\n  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      UtcTimingElement utcTiming,\n      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      UtcTimingElement utcTiming,\n      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-amzn-2.10.6/library/dash/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 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.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 = typeString != null && \"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      ProgramInformation programInformation,\n      UtcTimingElement utcTiming,\n      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));\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);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentTemplate\")) {\n        segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList());\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(XmlPullParser xpp, String baseUrl,\n      SegmentBase segmentBase) 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        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);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentTemplate\")) {\n        segmentBase =\n            parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, supplementalProperties);\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<String, SchemeData> parseContentProtection(XmlPullParser xpp)\n      throws XmlPullParserException, IOException {\n    String schemeType = null;\n    String licenseServerUrl = null;\n    byte[] data = null;\n    UUID uuid = null;\n    boolean requiresSecureDecoder = false;\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 (XmlPullParserUtil.isStartTag(xpp, \"widevine:license\")) {\n        String robustnessLevel = xpp.getAttributeValue(null, \"robustness_level\");\n        requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith(\"HW\");\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\n            ? new SchemeData(\n                uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder)\n            : 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      String adaptationSetMimeType,\n      String adaptationSetCodecs,\n      int adaptationSetWidth,\n      int adaptationSetHeight,\n      float adaptationSetFrameRate,\n      int adaptationSetAudioChannels,\n      int adaptationSetAudioSamplingRate,\n      String adaptationSetLanguage,\n      List<Descriptor> adaptationSetRoleDescriptors,\n      List<Descriptor> adaptationSetAccessibilityDescriptors,\n      List<Descriptor> adaptationSetSupplementalProperties,\n      SegmentBase segmentBase)\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);\n      } else if (XmlPullParserUtil.isStartTag(xpp, \"SegmentTemplate\")) {\n        segmentBase =\n            parseSegmentTemplate(\n                xpp, (SegmentTemplate) segmentBase, adaptationSetSupplementalProperties);\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      String id,\n      String containerMimeType,\n      int width,\n      int height,\n      float frameRate,\n      int audioChannels,\n      int audioSamplingRate,\n      int bitrate,\n      String language,\n      List<Descriptor> roleDescriptors,\n      List<Descriptor> accessibilityDescriptors,\n      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      String label,\n      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(XmlPullParser xpp, 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(XmlPullParser xpp, SegmentList 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    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);\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      List<SegmentTimelineElement> timeline,\n      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      SegmentTemplate parent,\n      List<Descriptor> adaptationSetSupplementalProperties)\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);\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      UrlTemplate initializationTemplate,\n      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(XmlPullParser xpp)\n      throws XmlPullParserException, IOException {\n    List<SegmentTimelineElement> segmentTimeline = new ArrayList<>();\n    long elapsedTime = 0;\n    do {\n      xpp.next();\n      if (XmlPullParserUtil.isStartTag(xpp, \"S\")) {\n        elapsedTime = parseLong(xpp, \"t\", elapsedTime);\n        long duration = parseLong(xpp, \"d\", C.TIME_UNSET);\n        int count = 1 + parseInt(xpp, \"r\", 0);\n        for (int i = 0; i < count; i++) {\n          segmentTimeline.add(buildSegmentTimelineElement(elapsedTime, duration));\n          elapsedTime += duration;\n        }\n      } else {\n        maybeSkipTag(xpp);\n      }\n    } while (!XmlPullParserUtil.isEndTag(xpp, \"SegmentTimeline\"));\n    return segmentTimeline;\n  }\n\n  protected SegmentTimelineElement buildSegmentTimelineElement(long elapsedTime, long duration) {\n    return new SegmentTimelineElement(elapsedTime, duration);\n  }\n\n  protected UrlTemplate parseUrlTemplate(XmlPullParser xpp, String name,\n      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(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(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  private static String getSampleMimeType(String containerMimeType, 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(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\n   * {@link IllegalStateException} if the languages are inconsistent.\n   * <p>\n   * 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  private static String checkLanguageConsistency(String firstLanguage, 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,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    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(Format format, String baseUrl, SegmentBase segmentBase,\n        String drmSchemeType, ArrayList<SchemeData> drmSchemeDatas,\n        ArrayList<Descriptor> inbandEventStreams, 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-amzn-2.10.6/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser2.java",
    "content": "package com.google.android.exoplayer2.source.dash.manifest;\r\n\r\nimport android.text.TextUtils;\r\nimport android.util.Pair;\r\n\r\nimport androidx.annotation.NonNull;\r\n\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.drm.DrmInitData;\r\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\r\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentList;\r\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTemplate;\r\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement;\r\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;\r\nimport com.google.android.exoplayer2.util.MimeTypes;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaFormat;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemFormatInfo;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaSubtitle;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.youtubeapi.formatbuilders.mpdbuilder.MediaFormatComparator;\r\nimport com.liskovsoft.youtubeapi.formatbuilders.utils.ITagUtils;\r\nimport com.liskovsoft.youtubeapi.formatbuilders.utils.MediaFormatUtils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Collections;\r\nimport java.util.HashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\nimport java.util.Set;\r\nimport java.util.TreeSet;\r\n\r\npublic class DashManifestParser2 {\r\n    private static final String TAG = DashManifestParser2.class.getSimpleName();\r\n    private int mId;\r\n    private static final String NULL_INDEX_RANGE = \"0-0\";\r\n    private static final String NULL_CONTENT_LENGTH = \"0\";\r\n    private static final int MAX_DURATION_SEC = 48 * 60 * 60;\r\n    private MediaItemFormatInfo mFormatInfo;\r\n    private Set<MediaFormat> mMP4Videos;\r\n    private Set<MediaFormat> mWEBMVideos;\r\n    private Map<String, Set<MediaFormat>> mMP4Audios;\r\n    private Map<String, Set<MediaFormat>> mWEBMAudios;\r\n    private List<MediaSubtitle> mSubs;\r\n\r\n    public DashManifest parse(@NonNull MediaItemFormatInfo formatInfo) {\r\n        mFormatInfo = formatInfo;\r\n        MediaFormatComparator comp = new MediaFormatComparator();\r\n        mMP4Videos = new TreeSet<>(comp);\r\n        mWEBMVideos = new TreeSet<>(comp);\r\n        mMP4Audios = new HashMap<>();\r\n        mWEBMAudios = new HashMap<>();\r\n        mSubs = new ArrayList<>();\r\n        return parseDashManifest(formatInfo);\r\n    }\r\n\r\n    private DashManifest parseDashManifest(MediaItemFormatInfo formatInfo) {\r\n        long availabilityStartTime = C.TIME_UNSET;\r\n        long durationMs = getDurationMs(formatInfo);\r\n        long minBufferTimeMs = 1500; // \"PT1.500S\"\r\n        long timeShiftBufferDepthMs = C.TIME_UNSET;\r\n        long suggestedPresentationDelayMs = C.TIME_UNSET;\r\n        long publishTimeMs = C.TIME_UNSET;\r\n        boolean dynamic = false;\r\n        long minUpdateTimeMs = C.TIME_UNSET; // 3155690800000L, \"P100Y\" no refresh (there is no dash url)\r\n\r\n        List<Period> periods = new ArrayList<>();\r\n\r\n        Pair<Period, Long> periodWithDurationMs = parsePeriod(formatInfo);\r\n        if (periodWithDurationMs != null) {\r\n            Period period = periodWithDurationMs.first;\r\n            periods.add(period);\r\n        }\r\n\r\n        return new DashManifest(\r\n                availabilityStartTime,\r\n                durationMs,\r\n                minBufferTimeMs,\r\n                dynamic,\r\n                minUpdateTimeMs,\r\n                timeShiftBufferDepthMs,\r\n                suggestedPresentationDelayMs,\r\n                publishTimeMs,\r\n                null,\r\n                null,\r\n                null,\r\n                periods);\r\n    }\r\n\r\n    private static long getDurationMs(MediaItemFormatInfo formatInfo) {\r\n        long lenSeconds = Helpers.parseLong(formatInfo.getLengthSeconds());\r\n        return lenSeconds > 0 ? lenSeconds * 1_000 : C.TIME_UNSET;\r\n    }\r\n\r\n    private Pair<Period, Long> parsePeriod(MediaItemFormatInfo formatInfo) {\r\n        String id = formatInfo.getVideoId();\r\n        long startMs = 0; // Should add real start time or make it unset?\r\n        long durationMs = getDurationMs(formatInfo);\r\n        List<AdaptationSet> adaptationSets = new ArrayList<>();\r\n\r\n        for (MediaFormat format : formatInfo.getAdaptiveFormats()) {\r\n            append(format);\r\n        }\r\n\r\n        if (formatInfo.getSubtitles() != null) {\r\n            append(formatInfo.getSubtitles());\r\n        }\r\n\r\n        // MXPlayer fix: write high quality formats first\r\n        if (!mMP4Videos.isEmpty()) {\r\n            adaptationSets.add(parseAdaptationSet(mMP4Videos));\r\n        }\r\n        if (!mWEBMVideos.isEmpty()) {\r\n            adaptationSets.add(parseAdaptationSet(mWEBMVideos));\r\n        }\r\n\r\n        for (Set<MediaFormat> formats : mMP4Audios.values()) {\r\n            adaptationSets.add(parseAdaptationSet(formats));\r\n        }\r\n\r\n        for (Set<MediaFormat> formats : mWEBMAudios.values()) {\r\n            adaptationSets.add(parseAdaptationSet(formats));\r\n        }\r\n\r\n        for (MediaSubtitle subtitle : mSubs) {\r\n            adaptationSets.add(parseAdaptationSet(Collections.singletonList(subtitle)));\r\n        }\r\n\r\n        return Pair.create(new Period(id, startMs, adaptationSets), durationMs);\r\n    }\r\n\r\n    private AdaptationSet parseAdaptationSet(Set<MediaFormat> formats) {\r\n        int id = mId++;\r\n        int contentType = C.TRACK_TYPE_UNKNOWN;\r\n        String label = null;\r\n        String drmSchemeType = null;\r\n        ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();\r\n        List<RepresentationInfo> representationInfos = new ArrayList<>();\r\n\r\n        for (MediaFormat format : formats) {\r\n            RepresentationInfo representationInfo = parseRepresentation(format);\r\n            if (contentType == C.TRACK_TYPE_UNKNOWN) {\r\n                contentType = getContentType(representationInfo.format);\r\n            }\r\n            representationInfos.add(representationInfo);\r\n        }\r\n\r\n        // Build the representations.\r\n        List<Representation> representations = new ArrayList<>(representationInfos.size());\r\n        for (int i = 0; i < representationInfos.size(); i++) {\r\n            representations.add(\r\n                    buildRepresentation(\r\n                            representationInfos.get(i),\r\n                            label,\r\n                            drmSchemeType,\r\n                            drmSchemeDatas));\r\n        }\r\n\r\n        return new AdaptationSet(id, contentType, representations, new ArrayList<>(), new ArrayList<>());\r\n    }\r\n\r\n    private AdaptationSet parseAdaptationSet(List<MediaSubtitle> formats) {\r\n        int id = mId++;\r\n        int contentType = C.TRACK_TYPE_UNKNOWN;\r\n        String label = null;\r\n        String drmSchemeType = null;\r\n        ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();\r\n        List<RepresentationInfo> representationInfos = new ArrayList<>();\r\n\r\n        for (MediaSubtitle format : formats) {\r\n            RepresentationInfo representationInfo = parseRepresentation(format);\r\n            if (contentType == C.TRACK_TYPE_UNKNOWN) {\r\n                contentType = getContentType(representationInfo.format);\r\n            }\r\n            representationInfos.add(representationInfo);\r\n        }\r\n\r\n        // Build the representations.\r\n        List<Representation> representations = new ArrayList<>(representationInfos.size());\r\n        for (int i = 0; i < representationInfos.size(); i++) {\r\n            representations.add(\r\n                    buildRepresentation(\r\n                            representationInfos.get(i),\r\n                            label,\r\n                            drmSchemeType,\r\n                            drmSchemeDatas));\r\n        }\r\n\r\n        return new AdaptationSet(id, contentType, representations, new ArrayList<>(), new ArrayList<>());\r\n    }\r\n\r\n    private SegmentTemplate parseSegmentTemplate(MediaFormat format) {\r\n        int unitsPerSecond = 1_000_000;\r\n\r\n        // Present on live streams only.\r\n        int segmentDurationUs = mFormatInfo.getSegmentDurationUs();\r\n\r\n        if (segmentDurationUs <= 0) {\r\n            // Inaccurate. Present on past (!) live streams.\r\n            segmentDurationUs = format.getTargetDurationSec() * 1_000_000;\r\n        }\r\n\r\n        int lengthSeconds = Integer.parseInt(mFormatInfo.getLengthSeconds());\r\n\r\n        if (mFormatInfo.isLive() || lengthSeconds <= 0) {\r\n            // For premiere streams (length > 0) or regular streams (length == 0) set window that exceeds normal limits - 48hrs\r\n            lengthSeconds = MAX_DURATION_SEC;\r\n        }\r\n\r\n        // To make long streams (12hrs) seekable we should decrease size of the segment a bit\r\n        //long segmentDurationUnits = (long) targetDurationSec * unitsPerSecond * 9999 / 10000;\r\n        int segmentDurationUnits = (int)(segmentDurationUs * (long) unitsPerSecond / 1_000_000);\r\n        // Increase count a bit to compensate previous tweak\r\n        //long segmentCount = (long) lengthSeconds / targetDurationSec * 10000 / 9999;\r\n        //int segmentCount = (int)(lengthSeconds * (long) unitsPerSecond / segmentDurationUnits);\r\n        int segmentCount = (int) Math.ceil(lengthSeconds * (double) unitsPerSecond / segmentDurationUnits);\r\n        // Increase offset a bit to compensate previous tweaks\r\n        // Streams to check:\r\n        // https://www.youtube.com/watch?v=drdemkJpgao\r\n        long offsetUnits = (long) segmentDurationUnits * mFormatInfo.getStartSegmentNum();\r\n\r\n        long timescale = unitsPerSecond;\r\n        long presentationTimeOffset = offsetUnits;\r\n        long duration = segmentDurationUnits;\r\n        long startNumber = mFormatInfo.getStartSegmentNum();\r\n        long endNumber = C.INDEX_UNSET;\r\n        UrlTemplate mediaTemplate = UrlTemplate.compile(format.getUrl() + \"&sq=$Number$\");\r\n        //UrlTemplate initializationTemplate = UrlTemplate.compile(format.getOtfInitUrl()); // ?\r\n        UrlTemplate initializationTemplate = null; // ?\r\n\r\n        RangedUri initialization = parseRangedUrl(format.getSourceUrl(), format.getInit());\r\n\r\n        List<SegmentTimelineElement> timeline = parseSegmentTimeline(offsetUnits, segmentDurationUnits, segmentCount);\r\n\r\n        return new SegmentTemplate(\r\n                initialization,\r\n                timescale,\r\n                presentationTimeOffset,\r\n                startNumber,\r\n                endNumber,\r\n                duration,\r\n                timeline,\r\n                initializationTemplate,\r\n                mediaTemplate);\r\n    }\r\n\r\n    private SegmentList parseSegmentList(MediaFormat format) {\r\n        long timescale = 1;\r\n        long presentationTimeOffset = 0;\r\n        long duration = C.TIME_UNSET;\r\n        long startNumber = 1;\r\n\r\n        RangedUri initialization = parseRangedUrl(format.getSourceUrl(), format.getInit());\r\n\r\n        List<SegmentTimelineElement> timeline = parseSegmentTimeline(format);\r\n\r\n        List<RangedUri> segments = parseSegmentUrl(format);\r\n\r\n        return new SegmentList(initialization, timescale, presentationTimeOffset,\r\n                startNumber, duration, timeline, segments);\r\n    }\r\n\r\n    private RangedUri parseRangedUrl(String urlText, String rangeText) {\r\n        long rangeStart = 0;\r\n        long rangeLength = C.LENGTH_UNSET;\r\n        if (rangeText != null) {\r\n            String[] rangeTextArray = rangeText.split(\"-\");\r\n            rangeStart = Long.parseLong(rangeTextArray[0]);\r\n            if (rangeTextArray.length == 2) {\r\n                rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1;\r\n            }\r\n        }\r\n\r\n        return new RangedUri(urlText, rangeStart, rangeLength);\r\n    }\r\n\r\n    private List<SegmentTimelineElement> parseSegmentTimeline(MediaFormat format) {\r\n        List<SegmentTimelineElement> timeline = new ArrayList<>();\r\n\r\n        if (format.getGlobalSegmentList() == null) {\r\n            return timeline;\r\n        }\r\n\r\n        // From writeGlobalSegmentList\r\n        long elapsedTime = 0;\r\n\r\n        // SegmentURL tag\r\n        for (String segment : format.getGlobalSegmentList()) {\r\n            long duration = Helpers.parseLong(segment, C.TIME_UNSET);\r\n            int count = 1;\r\n            for (int i = 0; i < count; i++) {\r\n                timeline.add(new SegmentTimelineElement(elapsedTime, duration));\r\n                elapsedTime += duration;\r\n            }\r\n        }\r\n\r\n        return timeline;\r\n    }\r\n\r\n    private List<SegmentTimelineElement> parseSegmentTimeline(long elapsedTime, long duration, int segmentCount) {\r\n        List<SegmentTimelineElement> timeline = new ArrayList<>();\r\n\r\n        // From writeLiveMediaSegmentList\r\n        int count = 1 + segmentCount;\r\n        for (int i = 0; i < count; i++) {\r\n            timeline.add(new SegmentTimelineElement(elapsedTime, duration));\r\n            elapsedTime += duration;\r\n        }\r\n\r\n        return timeline;\r\n    }\r\n\r\n    private List<RangedUri> parseSegmentUrl(MediaFormat format) {\r\n        List<RangedUri> segments = new ArrayList<>();\r\n\r\n        if (format.getSegmentUrlList() == null) {\r\n            return segments;\r\n        }\r\n\r\n        // SegmentURL tag\r\n        for (String url : format.getSegmentUrlList()) {\r\n            segments.add(parseRangedUrl(url, null));\r\n        }\r\n\r\n        return segments;\r\n    }\r\n\r\n    private SingleSegmentBase parseSegmentBase(MediaFormat format) {\r\n        long timescale = 1000;\r\n        long presentationTimeOffset = 0;\r\n\r\n        long indexStart = 0;\r\n        long indexLength = 0;\r\n        String indexRangeText = format.getIndex();\r\n        if (indexRangeText != null) {\r\n            String[] indexRange = indexRangeText.split(\"-\");\r\n            indexStart = Long.parseLong(indexRange[0]);\r\n            indexLength = Long.parseLong(indexRange[1]) - indexStart + 1;\r\n        }\r\n\r\n        RangedUri initialization = parseRangedUrl(format.getSourceUrl(), format.getInit());\r\n\r\n\r\n        return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, indexStart,\r\n                indexLength);\r\n    }\r\n\r\n    private RepresentationInfo parseRepresentation(MediaFormat mediaFormat) {\r\n        int roleFlags = C.ROLE_FLAG_MAIN;\r\n        int selectionFlags = C.SELECTION_FLAG_DEFAULT;\r\n        String id = mediaFormat.getITag();\r\n        int bandwidth = Helpers.parseInt(mediaFormat.getBitrate(), Format.NO_VALUE);\r\n        String mimeType = MediaFormatUtils.extractMimeType(mediaFormat);\r\n        String codecs = MediaFormatUtils.extractCodecs(mediaFormat);\r\n        int width = mediaFormat.getWidth();\r\n        int height = mediaFormat.getHeight();\r\n        float frameRate = Helpers.parseFloat(mediaFormat.getFps(), Format.NO_VALUE);\r\n        int audioChannels = Format.NO_VALUE;\r\n        int audioSamplingRate = Helpers.parseInt(ITagUtils.getAudioRateByTag(mediaFormat.getITag()), Format.NO_VALUE);\r\n        String language = mediaFormat.getLanguage();\r\n        String baseUrl = mediaFormat.getUrl();\r\n        String label = mediaFormat.getQualityLabel();\r\n        boolean isDrc = mediaFormat.isDrc();\r\n        long lastModified = Helpers.parseLong(mediaFormat.getLmt());\r\n        String drmSchemeType = null;\r\n        ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();\r\n\r\n        Format format =\r\n                buildFormat(\r\n                        id,\r\n                        label,\r\n                        mimeType,\r\n                        width,\r\n                        height,\r\n                        frameRate,\r\n                        audioChannels,\r\n                        audioSamplingRate,\r\n                        bandwidth,\r\n                        language,\r\n                        roleFlags,\r\n                        selectionFlags,\r\n                        codecs,\r\n                        isDrc,\r\n                        lastModified);\r\n\r\n        SegmentBase segmentBase = null;\r\n\r\n        if (MediaFormatUtils.isLiveMedia(mediaFormat)) {\r\n            segmentBase = parseSegmentTemplate(mediaFormat);\r\n        } else if (mediaFormat.getSegmentUrlList() != null) {\r\n            segmentBase = parseSegmentList(mediaFormat);\r\n        } else if (mediaFormat.getIndex() != null &&\r\n                !mediaFormat.getIndex().equals(NULL_INDEX_RANGE)) { // json mediaFormat fix: index is null\r\n            segmentBase = parseSegmentBase(mediaFormat);\r\n        }\r\n\r\n        segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase();\r\n\r\n        return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, Representation.REVISION_ID_DEFAULT);\r\n    }\r\n\r\n    private RepresentationInfo parseRepresentation(MediaSubtitle sub) {\r\n        int roleFlags = C.ROLE_FLAG_SUBTITLE;\r\n        int selectionFlags = 0;\r\n        //String id = String.valueOf(mId++);\r\n        // keep mId incrementing, but use vssId as id to make it unique and more meaningful\r\n        mId++;\r\n        int bandwidth = 268;\r\n        String mimeType = sub.getMimeType();\r\n        String codecs = sub.getCodecs();\r\n        int width = Format.NO_VALUE;\r\n        int height = Format.NO_VALUE;\r\n        float frameRate = Format.NO_VALUE;\r\n        int audioChannels = Format.NO_VALUE;\r\n        int audioSamplingRate = Format.NO_VALUE;\r\n        String language = sub.getName() == null ? sub.getLanguageCode() : sub.getName();\r\n        String baseUrl = sub.getBaseUrl();\r\n        String label = null;\r\n        boolean isDrc = false;\r\n        long lastModified = Format.NO_VALUE;\r\n        String drmSchemeType = null;\r\n        ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();\r\n\r\n        Format format =\r\n                buildFormat(\r\n                        //id,\r\n                        // use vssId as id to make it unique\r\n                        sub.getVssId(),\r\n                        label,\r\n                        mimeType,\r\n                        width,\r\n                        height,\r\n                        frameRate,\r\n                        audioChannels,\r\n                        audioSamplingRate,\r\n                        bandwidth,\r\n                        language,\r\n                        roleFlags,\r\n                        selectionFlags,\r\n                        codecs,\r\n                        isDrc,\r\n                        lastModified);\r\n\r\n        SegmentBase segmentBase = new SingleSegmentBase();\r\n\r\n        return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, Representation.REVISION_ID_DEFAULT);\r\n    }\r\n\r\n    protected Representation buildRepresentation(\r\n            RepresentationInfo representationInfo,\r\n            String label,\r\n            String extraDrmSchemeType,\r\n            ArrayList<SchemeData> extraDrmSchemeDatas) {\r\n        Format format = representationInfo.format;\r\n        if (label != null) {\r\n            format = format.copyWithLabel(label);\r\n        }\r\n        String drmSchemeType = representationInfo.drmSchemeType != null\r\n                ? representationInfo.drmSchemeType : extraDrmSchemeType;\r\n        ArrayList<SchemeData> drmSchemeDatas = representationInfo.drmSchemeDatas;\r\n        drmSchemeDatas.addAll(extraDrmSchemeDatas);\r\n        if (!drmSchemeDatas.isEmpty()) {\r\n            filterRedundantIncompleteSchemeDatas(drmSchemeDatas);\r\n            DrmInitData drmInitData = new DrmInitData(drmSchemeType, drmSchemeDatas);\r\n            format = format.copyWithDrmInitData(drmInitData);\r\n        }\r\n        return Representation.newInstance(\r\n                representationInfo.revisionId,\r\n                format,\r\n                representationInfo.baseUrl,\r\n                representationInfo.segmentBase,\r\n                new ArrayList<>());\r\n    }\r\n\r\n    protected Format buildFormat(\r\n            String id,\r\n            String label,\r\n            String containerMimeType,\r\n            int width,\r\n            int height,\r\n            float frameRate,\r\n            int audioChannels,\r\n            int audioSamplingRate,\r\n            int bitrate,\r\n            String language,\r\n            @C.RoleFlags int roleFlags,\r\n            @C.SelectionFlags int selectionFlags,\r\n            String codecs,\r\n            boolean isDrc,\r\n            long lastModified) {\r\n        String sampleMimeType = getSampleMimeType(containerMimeType, codecs);\r\n        if (sampleMimeType != null) {\r\n            if (MimeTypes.isVideo(sampleMimeType)) {\r\n                return Format.createVideoContainerFormat(\r\n                        id,\r\n                        label,\r\n                        containerMimeType,\r\n                        sampleMimeType,\r\n                        codecs,\r\n                        /* metadata= */ null,\r\n                        bitrate,\r\n                        width,\r\n                        height,\r\n                        frameRate,\r\n                        /* initializationData= */ null,\r\n                        selectionFlags,\r\n                        roleFlags,\r\n                        lastModified);\r\n            } else if (MimeTypes.isAudio(sampleMimeType)) {\r\n                return Format.createAudioContainerFormat(\r\n                        id,\r\n                        label,\r\n                        containerMimeType,\r\n                        sampleMimeType,\r\n                        codecs,\r\n                        /* metadata= */ null,\r\n                        bitrate,\r\n                        audioChannels,\r\n                        audioSamplingRate,\r\n                        /* initializationData= */ null,\r\n                        selectionFlags,\r\n                        roleFlags,\r\n                        language,\r\n                        isDrc,\r\n                        lastModified);\r\n            } else if (mimeTypeIsRawText(sampleMimeType)) {\r\n                return Format.createTextContainerFormat(\r\n                        id,\r\n                        label,\r\n                        containerMimeType,\r\n                        sampleMimeType,\r\n                        codecs,\r\n                        bitrate,\r\n                        selectionFlags,\r\n                        roleFlags,\r\n                        language,\r\n                        Format.NO_VALUE);\r\n            }\r\n        }\r\n        return Format.createContainerFormat(\r\n                id,\r\n                label,\r\n                containerMimeType,\r\n                sampleMimeType,\r\n                codecs,\r\n                bitrate,\r\n                selectionFlags,\r\n                roleFlags,\r\n                language);\r\n    }\r\n\r\n    /**\r\n     * Derives a sample mimeType from a container mimeType and codecs attribute.\r\n     *\r\n     * @param containerMimeType The mimeType of the container.\r\n     * @param codecs The codecs attribute.\r\n     * @return The derived sample mimeType, or null if it could not be derived.\r\n     */\r\n    private static String getSampleMimeType(String containerMimeType, String codecs) {\r\n        if (MimeTypes.isAudio(containerMimeType)) {\r\n            return MimeTypes.getAudioMediaMimeType(codecs);\r\n        } else if (MimeTypes.isVideo(containerMimeType)) {\r\n            return MimeTypes.getVideoMediaMimeType(codecs);\r\n        } else if (mimeTypeIsRawText(containerMimeType)) {\r\n            return containerMimeType;\r\n        } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) {\r\n            if (codecs != null) {\r\n                if (codecs.startsWith(\"stpp\")) {\r\n                    return MimeTypes.APPLICATION_TTML;\r\n                } else if (codecs.startsWith(\"wvtt\")) {\r\n                    return MimeTypes.APPLICATION_MP4VTT;\r\n                }\r\n            }\r\n        } else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {\r\n            if (codecs != null) {\r\n                if (codecs.contains(\"cea708\")) {\r\n                    return MimeTypes.APPLICATION_CEA708;\r\n                } else if (codecs.contains(\"eia608\") || codecs.contains(\"cea608\")) {\r\n                    return MimeTypes.APPLICATION_CEA608;\r\n                }\r\n            }\r\n            return null;\r\n        }\r\n        return null;\r\n    }\r\n\r\n    /**\r\n     * Returns whether a mimeType is a text sample mimeType.\r\n     *\r\n     * @param mimeType The mimeType.\r\n     * @return Whether the mimeType is a text sample mimeType.\r\n     */\r\n    private static boolean mimeTypeIsRawText(String mimeType) {\r\n        return MimeTypes.isText(mimeType)\r\n                || MimeTypes.APPLICATION_TTML.equals(mimeType)\r\n                || MimeTypes.APPLICATION_MP4VTT.equals(mimeType)\r\n                || MimeTypes.APPLICATION_CEA708.equals(mimeType)\r\n                || MimeTypes.APPLICATION_CEA608.equals(mimeType);\r\n    }\r\n\r\n    /**\r\n     * Removes unnecessary {@link SchemeData}s with null {@link SchemeData#data}.\r\n     */\r\n    private static void filterRedundantIncompleteSchemeDatas(ArrayList<SchemeData> schemeDatas) {\r\n        for (int i = schemeDatas.size() - 1; i >= 0; i--) {\r\n            SchemeData schemeData = schemeDatas.get(i);\r\n            if (!schemeData.hasData()) {\r\n                for (int j = 0; j < schemeDatas.size(); j++) {\r\n                    if (schemeDatas.get(j).canReplace(schemeData)) {\r\n                        // schemeData is incomplete, but there is another matching SchemeData which does contain\r\n                        // data, so we remove the incomplete one.\r\n                        schemeDatas.remove(i);\r\n                        break;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    private void append(List<MediaSubtitle> subs) {\r\n        mSubs.addAll(subs);\r\n    }\r\n\r\n    private void append(MediaSubtitle sub) {\r\n        mSubs.add(sub);\r\n    }\r\n\r\n    private void append(MediaFormat mediaItem) {\r\n        if (!MediaFormatUtils.checkMediaUrl(mediaItem)) {\r\n            Log.e(TAG, \"Media item doesn't contain required url field!\");\r\n            return;\r\n        }\r\n\r\n        // NOTE: FORMAT_STREAM_TYPE_OTF not supported\r\n        if (!MediaFormatUtils.isDash(mediaItem)) {\r\n            return;\r\n        }\r\n\r\n        //fixOTF(mediaItem);\r\n\r\n        Set<MediaFormat> placeholder = null;\r\n        String mimeType = MediaFormatUtils.extractMimeType(mediaItem);\r\n        if (mimeType != null) {\r\n            switch (mimeType) {\r\n                case MediaFormatUtils.MIME_MP4_VIDEO:\r\n                    placeholder = mMP4Videos;\r\n                    break;\r\n                case MediaFormatUtils.MIME_WEBM_VIDEO:\r\n                    placeholder = mWEBMVideos;\r\n                    break;\r\n                case MediaFormatUtils.MIME_MP4_AUDIO:\r\n                    placeholder = getMP4Audios(mediaItem.getLanguage());\r\n                    break;\r\n                case MediaFormatUtils.MIME_WEBM_AUDIO:\r\n                    placeholder = getWEBMAudios(mediaItem.getLanguage());\r\n                    break;\r\n            }\r\n        }\r\n\r\n        if (placeholder != null) {\r\n            placeholder.add(mediaItem); // NOTE: reverse order\r\n        }\r\n    }\r\n\r\n    private Set<MediaFormat> getMP4Audios(String language) {\r\n        return getFormats(mMP4Audios, language);\r\n    }\r\n\r\n    private Set<MediaFormat> getWEBMAudios(String language) {\r\n        return getFormats(mWEBMAudios, language);\r\n    }\r\n\r\n    private static Set<MediaFormat> getFormats(Map<String, Set<MediaFormat>> formatMap, String language) {\r\n        if (language == null) {\r\n            language = \"default\";\r\n        }\r\n\r\n        Set<MediaFormat> mediaFormats = formatMap.get(language);\r\n\r\n        if (mediaFormats == null) {\r\n            mediaFormats = new TreeSet<>(new MediaFormatComparator());\r\n            formatMap.put(language, mediaFormats);\r\n        }\r\n\r\n        return mediaFormats;\r\n    }\r\n\r\n    protected int getContentType(Format format) {\r\n        String sampleMimeType = format.sampleMimeType;\r\n        if (TextUtils.isEmpty(sampleMimeType)) {\r\n            return C.TRACK_TYPE_UNKNOWN;\r\n        } else if (MimeTypes.isVideo(sampleMimeType)) {\r\n            return C.TRACK_TYPE_VIDEO;\r\n        } else if (MimeTypes.isAudio(sampleMimeType)) {\r\n            return C.TRACK_TYPE_AUDIO;\r\n        } else if (mimeTypeIsRawText(sampleMimeType)) {\r\n            return C.TRACK_TYPE_TEXT;\r\n        }\r\n        return C.TRACK_TYPE_UNKNOWN;\r\n    }\r\n\r\n    /** A parsed Representation element. */\r\n    protected static final class RepresentationInfo {\r\n\r\n        public final Format format;\r\n        public final String baseUrl;\r\n        public final SegmentBase segmentBase;\r\n        public final String drmSchemeType;\r\n        public final ArrayList<SchemeData> drmSchemeDatas;\r\n        public final long revisionId;\r\n\r\n        public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase,\r\n                                  String drmSchemeType, ArrayList<SchemeData> drmSchemeDatas,\r\n                                  long revisionId) {\r\n            this.format = format;\r\n            this.baseUrl = baseUrl;\r\n            this.segmentBase = segmentBase;\r\n            this.drmSchemeType = drmSchemeType;\r\n            this.drmSchemeDatas = drmSchemeDatas;\r\n            this.revisionId = revisionId;\r\n        }\r\n\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/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.NonNull;\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  /**\n   * The scheme URI.\n   */\n  @NonNull 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(@NonNull 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 != null ? schemeIdUri.hashCode() : 0);\n    result = 31 * result + (value != null ? value.hashCode() : 0);\n    result = 31 * result + (id != null ? id.hashCode() : 0);\n    return result;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/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-amzn-2.10.6/library/dash/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-amzn-2.10.6/library/dash/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  public final String title;\n\n  /** Information about the original source of the media presentation. */\n  public final String source;\n\n  /** A copyright statement for the media presentation. */\n  public final String copyright;\n\n  /** A URL that provides more information about the media presentation. */\n  public final String moreInformationURL;\n\n  /** Declares the language code(s) for this ProgramInformation. */\n  public final String lang;\n\n  public ProgramInformation(\n      String title, String source, String copyright, String moreInformationURL, 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-amzn-2.10.6/library/dash/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 optional base Uri.\n   * @return The merged {@link RangedUri} if the merge was successful. Null otherwise.\n   */\n  public @Nullable 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-amzn-2.10.6/library/dash/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 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  /**\n   * The in-band event streams in the representation. Never null, but may be empty.\n   */\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, 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      List<Descriptor> inbandEventStreams) {\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 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      List<Descriptor> inbandEventStreams,\n      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      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  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  public abstract RangedUri getIndexUri();\n\n  /**\n   * Returns an index if the representation provides one directly, or null otherwise.\n   */\n  public abstract DashSegmentIndex getIndex();\n\n  /** Returns a cache key for the representation if set, or null. */\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    private final String cacheKey;\n    private final RangedUri indexUri;\n    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        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        List<Descriptor> inbandEventStreams,\n        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    public RangedUri getIndexUri() {\n      return indexUri;\n    }\n\n    @Override\n    public DashSegmentIndex getIndex() {\n      return segmentIndex;\n    }\n\n    @Override\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        List<Descriptor> inbandEventStreams) {\n      super(revisionId, format, baseUrl, segmentBase, inbandEventStreams);\n      this.segmentBase = segmentBase;\n    }\n\n    @Override\n    public RangedUri getIndexUri() {\n      return null;\n    }\n\n    @Override\n    public DashSegmentIndex getIndex() {\n      return this;\n    }\n\n    @Override\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-amzn-2.10.6/library/dash/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 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 */ 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(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  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(RangedUri initialization, long timescale, long presentationTimeOffset,\n        long indexStart, long indexLength) {\n      super(initialization, timescale, presentationTimeOffset);\n      this.indexStart = indexStart;\n      this.indexLength = indexLength;\n    }\n\n    public SingleSegmentBase() {\n      this(null, 1, 0, 0, 0);\n    }\n\n    public RangedUri getIndex() {\n      return indexLength <= 0 ? null : new RangedUri(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 */ 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        RangedUri initialization,\n        long timescale,\n        long presentationTimeOffset,\n        long startNumber,\n        long duration,\n        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 */ 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        List<SegmentTimelineElement> segmentTimeline,\n        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 */ final UrlTemplate initializationTemplate;\n    /* package */ 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        List<SegmentTimelineElement> segmentTimeline,\n        UrlTemplate initializationTemplate,\n        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    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  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/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-amzn-2.10.6/library/dash/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-amzn-2.10.6/library/dash/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-amzn-2.10.6/library/dash/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-amzn-2.10.6/library/dash/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n\n<manifest package=\"com.google.android.exoplayer2.source.dash.test\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/assets/sample_mpd",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    availabilityStartTime=\"2014-06-19T23:07:42\"\n    minBufferTime=\"PT1.500S\"\n    minimumUpdatePeriod=\"PT5.000S\"\n    profiles=\"urn:mpeg:dash:profile:isoff-main:2011\"\n    timeShiftBufferDepth=\"PT129600.000S\"\n    type=\"dynamic\"\n    xmlns=\"urn:mpeg:DASH:schema:MPD:2011\"\n    xsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd\"\n    yt:earliestMediaSequence=\"1266404\" >\n    <ProgramInformation lang=\"enUs\"\n        moreInformationURL=\"www.example.com\">\n        <Title>MediaTitle</Title>\n        <Source>MediaSource</Source>\n        <Copyright>MediaCopyright</Copyright>\n    </ProgramInformation>\n    <Period start=\"PT6462826.784S\" >\n        <SegmentList\n            presentationTimeOffset=\"34740095\"\n            startNumber=\"1292317\"\n            timescale=\"1000\" >\n            <SegmentTimeline>\n                <S d=\"4804\" />\n                <S d=\"5338\" />\n                <S d=\"4938\" />\n            </SegmentTimeline>\n        </SegmentList>\n        <AdaptationSet\n            mimeType=\"audio/mp4\"\n            subsegmentAlignment=\"true\" >\n            <Role\n                schemeIdUri=\"urn:mpeg:DASH:role:2011\"\n                value=\"main\" />\n            <Representation\n                id=\"141\"\n                audioSamplingRate=\"48000\"\n                bandwidth=\"272000\"\n                codecs=\"mp4a.40.2\"\n                startWithSAP=\"1\" >\n                <AudioChannelConfiguration\n                    schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\"\n                    value=\"2\" />\n                <BaseURL>\nhttp://www.test.com/141\n                </BaseURL>\n                <SegmentList>\n                    <Initialization\n                        range=\"0-591\"\n                        sourceURL=\"sq/0/clen/79480/lmt/1403219262956762/dur/4.805\" />\n                    <SegmentURL media=\"sq/1292317/clen/77447/lmt/1409671169987621/dur/4.805\" />\n                    <SegmentURL media=\"sq/1292318/clen/86958/lmt/1409671174832549/dur/5.339\" />\n                    <SegmentURL media=\"sq/1292319/clen/85018/lmt/1409671179719956/dur/4.938\" />\n                </SegmentList>\n            </Representation>\n        </AdaptationSet>\n        <AdaptationSet\n            mimeType=\"video/mp4\"\n            subsegmentAlignment=\"true\" >\n            <Role\n                schemeIdUri=\"urn:mpeg:DASH:role:2011\"\n                value=\"main\" />\n            <Representation\n                id=\"135\"\n                bandwidth=\"1116000\"\n                codecs=\"avc1.42c01f\"\n                height=\"480\"\n                startWithSAP=\"1\"\n                width=\"854\" >\n                <BaseURL>\nhttp://www.test.com/135\n                </BaseURL>\n                <SegmentList>\n                    <Initialization\n                        range=\"0-671\"\n                        sourceURL=\"sq/0/clen/1221137/lmt/1403219262956762/dur/4.805\" />\n                    <SegmentURL media=\"sq/1292317/clen/1279915/lmt/1409671169987621/dur/4.805\" />\n                    <SegmentURL media=\"sq/1292318/clen/1310650/lmt/1409671174832549/dur/5.339\" />\n                    <SegmentURL media=\"sq/1292319/clen/1486558/lmt/1409671179719956/dur/4.938\" />\n                </SegmentList>\n            </Representation>\n        </AdaptationSet>\n        <AdaptationSet\n            lang=\"en\"\n            mimeType=\"text/vtt\" >\n            <Role\n                schemeIdUri=\"urn:mpeg:DASH:role:2011\"\n                value=\"caption\" />\n            <Representation\n                id=\"en\"\n                bandwidth=\"0\"\n                codecs=\"\" >\n                <BaseURL>\nhttp://www.test.com/vtt\n                </BaseURL>\n                <SegmentList>\n                    <SegmentURL media=\"sq/1292317\" />\n                    <SegmentURL media=\"sq/1292318\" />\n                    <SegmentURL media=\"sq/1292319\" />\n                </SegmentList>\n            </Representation>\n        </AdaptationSet>\n    </Period>\n</MPD>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/assets/sample_mpd_event_stream",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"urn:mpeg:DASH:schema:MPD:2011\" xmlns:yt=\"http://youtube.com/yt/2012/10/10\" xsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd\" minBufferTime=\"PT1.500S\" profiles=\"urn:mpeg:dash:profile:isoff-main:2011\" type=\"dynamic\" availabilityStartTime=\"2016-10-14T17:00:17\" timeShiftBufferDepth=\"PT7200.000S\" minimumUpdatePeriod=\"PT2.000S\" yt:earliestMediaSequence=\"0\" yt:mpdRequestTime=\"2016-10-14T18:29:17.082\" yt:mpdResponseTime=\"2016-10-14T18:29:17.194\">\n <Period start=\"PT0.000S\" yt:segmentIngestTime=\"2016-10-14T17:00:14.257\">\n   <EventStream schemeIdUri=\"urn:uuid:XYZY\" timescale=\"1000\" value=\"call\">\n     <Event presentationTime=\"0\" duration=\"10000\" id=\"0\" messageData=\"+ 1 800 10101010\"/>\n   </EventStream>\n   <EventStream schemeIdUri=\"urn:dvb:iptv:cpm:2014\">\n     <Event presentationTime=\"300\" duration=\"1500\" id=\"1\"><![CDATA[<BroadcastEvent>\n      <Program crid=\"crid://broadcaster.example.com/ABCDEF\"/>\n      <InstanceDescription>\n      <Title xml:lang=\"en\">The title</Title>\n      <Synopsis xml:lang=\"en\" length=\"medium\">The description</Synopsis>\n      <ParentalGuidance>\n      <mpeg7:ParentalRating href=\"urn:dvb:iptv:rating:2014:15\"/>\n      <mpeg7:Region>GB</mpeg7:Region>\n      </ParentalGuidance>\n      </InstanceDescription>\n      </BroadcastEvent>]]></Event>\n   </EventStream>\n   <EventStream schemeIdUri=\"urn:scte:scte35:2014:xml+bin\">\n     <Event timescale=\"90000\" presentationTime=\"1000\" duration=\"1000\" id=\"2\"><scte35:Signal>\n         <scte35:Binary>\n         /DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==\n         </scte35:Binary>\n       </scte35:Signal></Event>\n   </EventStream>\n  <SegmentTemplate startNumber=\"0\" timescale=\"1000\" media=\"sq/$Number$\">\n   <SegmentTimeline>\n    <S d=\"2002\" t=\"6009\" r=\"2\"/>\n   </SegmentTimeline>\n  </SegmentTemplate>\n  <AdaptationSet id=\"0\" mimeType=\"audio/mp4\" subsegmentAlignment=\"true\">\n   <Role schemeIdUri=\"urn:mpeg:DASH:role:2011\" value=\"main\"/>\n   <Representation id=\"140\" codecs=\"mp4a.40.2\" audioSamplingRate=\"48000\" startWithSAP=\"1\" bandwidth=\"144000\">\n    <AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"2\"/>\n    <BaseURL>http://www.dummy.url/</BaseURL>\n   </Representation>\n  </AdaptationSet>\n  <AdaptationSet id=\"1\" mimeType=\"video/mp4\" subsegmentAlignment=\"true\">\n   <Role schemeIdUri=\"urn:mpeg:DASH:role:2011\" value=\"main\"/>\n   <Representation id=\"133\" codecs=\"avc1.4d4015\" width=\"426\" height=\"240\" startWithSAP=\"1\" maxPlayoutRate=\"1\" bandwidth=\"258000\" frameRate=\"30\">\n    <BaseURL>http://www.dummy.url/</BaseURL>\n   </Representation>\n  </AdaptationSet>\n </Period>\n</MPD>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/assets/sample_mpd_labels",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<MPD type=\"static\" duration=\"1s\" mediaPresentationDuration=\"PT1S\">\n <Period>\n  <SegmentTemplate startNumber=\"0\" timescale=\"1000\" media=\"sq/$Number$\">\n   <SegmentTimeline>\n    <S d=\"1000\"/>\n   </SegmentTimeline>\n  </SegmentTemplate>\n  <AdaptationSet id=\"0\" mimeType=\"audio/mp4\" subsegmentAlignment=\"true\" label=\"audio label\">\n   <Representation id=\"0\" codecs=\"mp4a.40.2\" audioSamplingRate=\"48000\" bandwidth=\"144000\">\n    <BaseURL>https://test.com/0</BaseURL>\n   </Representation>\n  </AdaptationSet>\n  <AdaptationSet id=\"1\" mimeType=\"video/mp4\" subsegmentAlignment=\"true\" label=\"ignored label\">\n   <Representation id=\"1\" codecs=\"avc1.4d4015\" width=\"426\" height=\"240\" bandwidth=\"258000\">\n    <BaseURL>https://test.com/1</BaseURL>\n   </Representation>\n   <Label>video label</Label>\n  </AdaptationSet>\n </Period>\n</MPD>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/assets/sample_mpd_segment_template",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"urn:mpeg:DASH:schema:MPD:2011\" xmlns:yt=\"http://youtube.com/yt/2012/10/10\" xsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd\" minBufferTime=\"PT1.500S\" profiles=\"urn:mpeg:dash:profile:isoff-main:2011\" type=\"dynamic\" availabilityStartTime=\"2016-10-14T17:00:17\" timeShiftBufferDepth=\"PT7200.000S\" minimumUpdatePeriod=\"PT2.000S\" yt:earliestMediaSequence=\"0\" yt:mpdRequestTime=\"2016-10-14T18:29:17.082\" yt:mpdResponseTime=\"2016-10-14T18:29:17.194\">\n <Period start=\"PT0.000S\" yt:segmentIngestTime=\"2016-10-14T17:00:14.257\">\n  <SegmentTemplate startNumber=\"0\" timescale=\"1000\" media=\"sq/$Number$\">\n   <SegmentTimeline>\n    <S d=\"2002\" t=\"6009\" r=\"2\"/>\n    <S d=\"1985\"/>\n    <S d=\"2000\"/>\n   </SegmentTimeline>\n  </SegmentTemplate>\n  <AdaptationSet id=\"0\" mimeType=\"audio/mp4\" subsegmentAlignment=\"true\">\n   <Role schemeIdUri=\"urn:mpeg:DASH:role:2011\" value=\"main\"/>\n   <Representation id=\"140\" codecs=\"mp4a.40.2\" audioSamplingRate=\"48000\" startWithSAP=\"1\" bandwidth=\"144000\">\n    <AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"2\"/>\n    <BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/140/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/audio%2Fmp4/live/1/gir/yes/noclen/1/signature/B5137EA0CC278C07DD056D204E863CC81EDEB39E.1AD5D242EBC94922EDA7165353A89A5E08A4103A/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>\n   </Representation>\n  </AdaptationSet>\n  <AdaptationSet id=\"1\" mimeType=\"video/mp4\" subsegmentAlignment=\"true\">\n   <Role schemeIdUri=\"urn:mpeg:DASH:role:2011\" value=\"main\"/>\n   <Representation id=\"133\" codecs=\"avc1.4d4015\" width=\"426\" height=\"240\" startWithSAP=\"1\" maxPlayoutRate=\"1\" bandwidth=\"258000\" frameRate=\"30\">\n    <BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/133/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/90154AE9C5C9D9D519CBF2E43AB0A1778375992D.40E2E855ADFB38FA7E95E168FEEEA6796B080BD7/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>\n   </Representation>\n   <Representation id=\"134\" codecs=\"avc1.4d401e\" width=\"640\" height=\"360\" startWithSAP=\"1\" maxPlayoutRate=\"1\" bandwidth=\"646000\" frameRate=\"30\">\n    <BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/134/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/5C094AEFDCEB1A4D2F3C05F8BD095C336EF0E1C3.7AE6B9951B0237AAE6F031927AACAC4974BAFFAA/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>\n   </Representation>\n   <Representation id=\"135\" codecs=\"avc1.4d401f\" width=\"854\" height=\"480\" startWithSAP=\"1\" maxPlayoutRate=\"1\" bandwidth=\"1171000\" frameRate=\"30\">\n    <BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/135/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/1F7660CA4E5B4AE4D60E18795680E34CDD2EF3C9.800B0A1D5F490DE142CCF4C88C64FD21D42129/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>\n   </Representation>\n   <Representation id=\"160\" codecs=\"avc1.42c00b\" width=\"256\" height=\"144\" startWithSAP=\"1\" maxPlayoutRate=\"1\" bandwidth=\"124000\" frameRate=\"30\">\n    <BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/160/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/94EB61673784DF0C4237A1A866F2E171C8A64ADB.AEC00AA06C2278FEA8702FB62693B70D8977F46C/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>\n   </Representation>\n   <Representation id=\"136\" codecs=\"avc1.4d401f\" width=\"1280\" height=\"720\" startWithSAP=\"1\" maxPlayoutRate=\"1\" bandwidth=\"2326000\" frameRate=\"30\">\n    <BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/136/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/6D8C34FC30A1F1A4F700B61180D1C4CCF6274844.29EBCB4A837DE626C52C66CF650519E61C2FF0BF/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>\n   </Representation>\n  </AdaptationSet>\n </Period>\n</MPD>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/assets/sample_mpd_unknown_mime_type",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    availabilityStartTime=\"2014-06-19T23:07:42\"\n    minBufferTime=\"PT1.500S\"\n    minimumUpdatePeriod=\"PT5.000S\"\n    profiles=\"urn:mpeg:dash:profile:isoff-main:2011\"\n    timeShiftBufferDepth=\"PT129600.000S\"\n    type=\"dynamic\"\n    xmlns=\"urn:mpeg:DASH:schema:MPD:2011\"\n    xsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd\"\n    yt:earliestMediaSequence=\"1266404\" >\n    <Period start=\"PT6462826.784S\" >\n        <SegmentList\n            presentationTimeOffset=\"34740095\"\n            startNumber=\"1292317\"\n            timescale=\"1000\" >\n            <SegmentTimeline>\n                <S d=\"4804\" />\n                <S d=\"5338\" />\n                <S d=\"4938\" />\n            </SegmentTimeline>\n        </SegmentList>\n        <AdaptationSet\n            mimeType=\"audio/mp4\"\n            subsegmentAlignment=\"true\" >\n            <Role\n                schemeIdUri=\"urn:mpeg:DASH:role:2011\"\n                value=\"main\" />\n            <Representation\n                id=\"141\"\n                audioSamplingRate=\"48000\"\n                bandwidth=\"272000\"\n                codecs=\"mp4a.40.2\"\n                startWithSAP=\"1\" >\n                <AudioChannelConfiguration\n                    schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\"\n                    value=\"2\" />\n                <BaseURL>\nhttp://www.test.com/141\n                </BaseURL>\n                <SegmentList>\n                    <Initialization\n                        range=\"0-591\"\n                        sourceURL=\"sq/0/clen/79480/lmt/1403219262956762/dur/4.805\" />\n                    <SegmentURL media=\"sq/1292317/clen/77447/lmt/1409671169987621/dur/4.805\" />\n                    <SegmentURL media=\"sq/1292318/clen/86958/lmt/1409671174832549/dur/5.339\" />\n                    <SegmentURL media=\"sq/1292319/clen/85018/lmt/1409671179719956/dur/4.938\" />\n                </SegmentList>\n            </Representation>\n        </AdaptationSet>\n        <AdaptationSet\n            mimeType=\"video/mp4\"\n            subsegmentAlignment=\"true\" >\n            <Role\n                schemeIdUri=\"urn:mpeg:DASH:role:2011\"\n                value=\"main\" />\n            <Representation\n                id=\"135\"\n                bandwidth=\"1116000\"\n                codecs=\"avc1.42c01f\"\n                height=\"480\"\n                startWithSAP=\"1\"\n                width=\"854\" >\n                <BaseURL>\nhttp://www.test.com/135\n                </BaseURL>\n                <SegmentList>\n                    <Initialization\n                        range=\"0-671\"\n                        sourceURL=\"sq/0/clen/1221137/lmt/1403219262956762/dur/4.805\" />\n                    <SegmentURL media=\"sq/1292317/clen/1279915/lmt/1409671169987621/dur/4.805\" />\n                    <SegmentURL media=\"sq/1292318/clen/1310650/lmt/1409671174832549/dur/5.339\" />\n                    <SegmentURL media=\"sq/1292319/clen/1486558/lmt/1409671179719956/dur/4.938\" />\n                </SegmentList>\n            </Representation>\n        </AdaptationSet>\n        <AdaptationSet\n            lang=\"en\"\n            mimeType=\"text/vtt\" >\n            <Role\n                schemeIdUri=\"urn:mpeg:DASH:role:2011\"\n                value=\"caption\" />\n            <Representation\n                id=\"en\"\n                bandwidth=\"0\"\n                codecs=\"\" >\n                <BaseURL>\nhttp://www.test.com/vtt\n                </BaseURL>\n                <SegmentList>\n                    <SegmentURL media=\"sq/1292317\" />\n                    <SegmentURL media=\"sq/1292318\" />\n                    <SegmentURL media=\"sq/1292319\" />\n                </SegmentList>\n            </Representation>\n        </AdaptationSet>\n        <AdaptationSet\n                    mimeType=\"application/octet-stream\" >\n                    <Role\n                        schemeIdUri=\"urn:mpeg:DASH:role:2011\"\n                        value=\"caption\" />\n                    <Representation\n                        id=\"608\"\n                        bandwidth=\"0\"\n                        codecs=\"\" >\n                        <BaseURL>\n        http://www.test.com/608\n                        </BaseURL>\n                        <SegmentList>\n                            <SegmentURL media=\"sq/1292317\" />\n                            <SegmentURL media=\"sq/1292318\" />\n                            <SegmentURL media=\"sq/1292319\" />\n                        </SegmentList>\n                    </Representation>\n                </AdaptationSet>\n    </Period>\n</MPD>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.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 static org.mockito.Mockito.mock;\n\nimport android.net.Uri;\nimport androidx.annotation.Nullable;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\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.Descriptor;\nimport com.google.android.exoplayer2.source.dash.manifest.Period;\nimport com.google.android.exoplayer2.source.dash.manifest.Representation;\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;\nimport com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;\nimport com.google.android.exoplayer2.testutil.MediaPeriodAsserts;\nimport com.google.android.exoplayer2.testutil.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\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 java.util.Arrays;\nimport java.util.Collections;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/** Unit tests for {@link DashMediaPeriod}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic final class DashMediaPeriodTest {\n\n  @Test\n  public void getSteamKeys_isCompatibleWithDashManifestFilter() {\n    // Test manifest which covers various edge cases:\n    //  - Multiple periods.\n    //  - Single and multiple representations per adaptation set.\n    //  - Switch descriptors combining multiple adaptations sets.\n    //  - Embedded track groups.\n    // All cases are deliberately combined in one test to catch potential indexing problems which\n    // only occur in combination.\n    DashManifest testManifest =\n        createDashManifest(\n            createPeriod(\n                createAdaptationSet(\n                    /* id= */ 0,\n                    /* trackType= */ C.TRACK_TYPE_VIDEO,\n                    /* descriptor= */ null,\n                    createVideoRepresentation(/* bitrate= */ 1000000))),\n            createPeriod(\n                createAdaptationSet(\n                    /* id= */ 100,\n                    /* trackType= */ C.TRACK_TYPE_VIDEO,\n                    /* descriptor= */ createSwitchDescriptor(/* ids= */ 103, 104),\n                    createVideoRepresentationWithInbandEventStream(/* bitrate= */ 200000),\n                    createVideoRepresentationWithInbandEventStream(/* bitrate= */ 400000),\n                    createVideoRepresentationWithInbandEventStream(/* bitrate= */ 600000)),\n                createAdaptationSet(\n                    /* id= */ 101,\n                    /* trackType= */ C.TRACK_TYPE_AUDIO,\n                    /* descriptor= */ createSwitchDescriptor(/* ids= */ 102),\n                    createAudioRepresentation(/* bitrate= */ 48000),\n                    createAudioRepresentation(/* bitrate= */ 96000)),\n                createAdaptationSet(\n                    /* id= */ 102,\n                    /* trackType= */ C.TRACK_TYPE_AUDIO,\n                    /* descriptor= */ createSwitchDescriptor(/* ids= */ 101),\n                    createAudioRepresentation(/* bitrate= */ 256000)),\n                createAdaptationSet(\n                    /* id= */ 103,\n                    /* trackType= */ C.TRACK_TYPE_VIDEO,\n                    /* descriptor= */ createSwitchDescriptor(/* ids= */ 100, 104),\n                    createVideoRepresentationWithInbandEventStream(/* bitrate= */ 800000),\n                    createVideoRepresentationWithInbandEventStream(/* bitrate= */ 1000000)),\n                createAdaptationSet(\n                    /* id= */ 104,\n                    /* trackType= */ C.TRACK_TYPE_VIDEO,\n                    /* descriptor= */ createSwitchDescriptor(/* ids= */ 100, 103),\n                    createVideoRepresentationWithInbandEventStream(/* bitrate= */ 2000000)),\n                createAdaptationSet(\n                    /* id= */ 105,\n                    /* trackType= */ C.TRACK_TYPE_TEXT,\n                    /* descriptor= */ null,\n                    createTextRepresentation(/* language= */ \"eng\")),\n                createAdaptationSet(\n                    /* id= */ 105,\n                    /* trackType= */ C.TRACK_TYPE_TEXT,\n                    /* descriptor= */ null,\n                    createTextRepresentation(/* language= */ \"ger\"))));\n    FilterableManifestMediaPeriodFactory<DashManifest> mediaPeriodFactory =\n        (manifest, periodIndex) ->\n            new DashMediaPeriod(\n                /* id= */ periodIndex,\n                manifest,\n                periodIndex,\n                mock(DashChunkSource.Factory.class),\n                mock(TransferListener.class),\n                mock(LoadErrorHandlingPolicy.class),\n                new EventDispatcher()\n                    .withParameters(\n                        /* windowIndex= */ 0,\n                        /* mediaPeriodId= */ new MediaPeriodId(/* periodUid= */ new Object()),\n                        /* mediaTimeOffsetMs= */ 0),\n                /* elapsedRealtimeOffsetMs= */ 0,\n                mock(LoaderErrorThrower.class),\n                mock(Allocator.class),\n                mock(CompositeSequenceableLoaderFactory.class),\n                mock(PlayerEmsgCallback.class));\n\n    // Ignore embedded metadata as we don't want to select primary group just to get embedded track.\n    MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(\n        mediaPeriodFactory,\n        testManifest,\n        /* periodIndex= */ 1,\n        /* ignoredMimeType= */ \"application/x-emsg\");\n  }\n\n  private static DashManifest createDashManifest(Period... periods) {\n    return new DashManifest(\n        /* availabilityStartTimeMs= */ 0,\n        /* durationMs= */ 5000,\n        /* minBufferTimeMs= */ 1,\n        /* dynamic= */ false,\n        /* minUpdatePeriodMs= */ 2,\n        /* timeShiftBufferDepthMs= */ 3,\n        /* suggestedPresentationDelayMs= */ 4,\n        /* publishTimeMs= */ 12345,\n        /* programInformation= */ null,\n        new UtcTimingElement(\"\", \"\"),\n        Uri.EMPTY,\n        Arrays.asList(periods));\n  }\n\n  private static Period createPeriod(AdaptationSet... adaptationSets) {\n    return new Period(/* id= */ null, /* startMs= */ 0, Arrays.asList(adaptationSets));\n  }\n\n  private static AdaptationSet createAdaptationSet(\n      int id, int trackType, @Nullable Descriptor descriptor, Representation... representations) {\n    return new AdaptationSet(\n        id,\n        trackType,\n        Arrays.asList(representations),\n        /* accessibilityDescriptors= */ Collections.emptyList(),\n        descriptor == null ? Collections.emptyList() : Collections.singletonList(descriptor));\n  }\n\n  private static Representation createVideoRepresentation(int bitrate) {\n    return Representation.newInstance(\n        /* revisionId= */ 0,\n        createVideoFormat(bitrate),\n        /* baseUrl= */ \"\",\n        new SingleSegmentBase());\n  }\n\n  private static Representation createVideoRepresentationWithInbandEventStream(int bitrate) {\n    return Representation.newInstance(\n        /* revisionId= */ 0,\n        createVideoFormat(bitrate),\n        /* baseUrl= */ \"\",\n        new SingleSegmentBase(),\n        Collections.singletonList(getInbandEventDescriptor()));\n  }\n\n  private static Format createVideoFormat(int bitrate) {\n    return Format.createContainerFormat(\n        /* id= */ null,\n        /* label= */ null,\n        MimeTypes.VIDEO_MP4,\n        MimeTypes.VIDEO_H264,\n        /* codecs= */ null,\n        bitrate,\n        /* selectionFlags= */ 0,\n        /* roleFlags= */ 0,\n        /* language= */ null);\n  }\n\n  private static Representation createAudioRepresentation(int bitrate) {\n    return Representation.newInstance(\n        /* revisionId= */ 0,\n        Format.createContainerFormat(\n            /* id= */ null,\n            /* label= */ null,\n            MimeTypes.AUDIO_MP4,\n            MimeTypes.AUDIO_AAC,\n            /* codecs= */ null,\n            bitrate,\n            /* selectionFlags= */ 0,\n            /* roleFlags= */ 0,\n            /* language= */ null),\n        /* baseUrl= */ \"\",\n        new SingleSegmentBase());\n  }\n\n  private static Representation createTextRepresentation(String language) {\n    return Representation.newInstance(\n        /* revisionId= */ 0,\n        Format.createContainerFormat(\n            /* id= */ null,\n            /* label= */ null,\n            MimeTypes.APPLICATION_MP4,\n            MimeTypes.TEXT_VTT,\n            /* codecs= */ null,\n            /* bitrate= */ Format.NO_VALUE,\n            /* selectionFlags= */ 0,\n            /* roleFlags= */ 0,\n            language),\n        /* baseUrl= */ \"\",\n        new SingleSegmentBase());\n  }\n\n  private static Descriptor createSwitchDescriptor(int... ids) {\n    StringBuilder idString = new StringBuilder();\n    idString.append(ids[0]);\n    for (int i = 1; i < ids.length; i++) {\n      idString.append(\",\").append(ids[i]);\n    }\n    return new Descriptor(\n        /* schemeIdUri= */ \"urn:mpeg:dash:adaptation-set-switching:2016\",\n        /* value= */ idString.toString(),\n        /* id= */ null);\n  }\n\n  private static Descriptor getInbandEventDescriptor() {\n    return new Descriptor(\n        /* schemeIdUri= */ \"inBandSchemeIdUri\", /* value= */ \"inBandValue\", /* id= */ \"inBandId\");\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.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.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.upstream.ParsingLoadable;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link DashMediaSource}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DashMediaSourceTest {\n\n  @Test\n  public void testIso8601ParserParse() throws IOException {\n    DashMediaSource.Iso8601Parser parser = new DashMediaSource.Iso8601Parser();\n    // UTC.\n    assertParseStringToLong(1512381697000L, parser, \"2017-12-04T10:01:37Z\");\n    assertParseStringToLong(1512381697000L, parser, \"2017-12-04T10:01:37+00:00\");\n    assertParseStringToLong(1512381697000L, parser, \"2017-12-04T10:01:37+0000\");\n    assertParseStringToLong(1512381697000L, parser, \"2017-12-04T10:01:37+00\");\n    // Positive timezone offsets.\n    assertParseStringToLong(1512381697000L - 4980000L, parser, \"2017-12-04T10:01:37+01:23\");\n    assertParseStringToLong(1512381697000L - 4980000L, parser, \"2017-12-04T10:01:37+0123\");\n    assertParseStringToLong(1512381697000L - 3600000L, parser, \"2017-12-04T10:01:37+01\");\n    // Negative timezone offsets with minus character.\n    assertParseStringToLong(1512381697000L + 4980000L, parser, \"2017-12-04T10:01:37-01:23\");\n    assertParseStringToLong(1512381697000L + 4980000L, parser, \"2017-12-04T10:01:37-0123\");\n    assertParseStringToLong(1512381697000L + 3600000L, parser, \"2017-12-04T10:01:37-01:00\");\n    assertParseStringToLong(1512381697000L + 3600000L, parser, \"2017-12-04T10:01:37-0100\");\n    assertParseStringToLong(1512381697000L + 3600000L, parser, \"2017-12-04T10:01:37-01\");\n    // Negative timezone offsets with hyphen character.\n    assertParseStringToLong(1512381697000L + 4980000L, parser, \"2017-12-04T10:01:37−01:23\");\n    assertParseStringToLong(1512381697000L + 4980000L, parser, \"2017-12-04T10:01:37−0123\");\n    assertParseStringToLong(1512381697000L + 3600000L, parser, \"2017-12-04T10:01:37−01:00\");\n    assertParseStringToLong(1512381697000L + 3600000L, parser, \"2017-12-04T10:01:37−0100\");\n    assertParseStringToLong(1512381697000L + 3600000L, parser, \"2017-12-04T10:01:37−01\");\n  }\n\n  @Test\n  public void testIso8601ParserParseMissingTimezone() throws IOException {\n    DashMediaSource.Iso8601Parser parser = new DashMediaSource.Iso8601Parser();\n    try {\n      assertParseStringToLong(0, parser, \"2017-12-04T10:01:37\");\n      fail();\n    } catch (ParserException e) {\n      // Expected.\n    }\n  }\n\n  private static void assertParseStringToLong(\n      long expected, ParsingLoadable.Parser<Long> parser, String data) throws IOException {\n    long actual = parser.parse(null, new ByteArrayInputStream(Util.getUtf8Bytes(data)));\n    assertThat(actual).isEqualTo(expected);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.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.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\nimport com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;\nimport com.google.android.exoplayer2.source.dash.manifest.Period;\nimport com.google.android.exoplayer2.source.dash.manifest.Representation;\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;\nimport com.google.android.exoplayer2.upstream.DummyDataSource;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.Arrays;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link DashUtil}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DashUtilTest {\n\n  @Test\n  public void testLoadDrmInitDataFromManifest() throws Exception {\n    Period period = newPeriod(newAdaptationSets(newRepresentations(newDrmInitData())));\n    DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period);\n    assertThat(drmInitData).isEqualTo(newDrmInitData());\n  }\n\n  @Test\n  public void testLoadDrmInitDataMissing() throws Exception {\n    Period period = newPeriod(newAdaptationSets(newRepresentations(null /* no init data */)));\n    DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period);\n    assertThat(drmInitData).isNull();\n  }\n\n  @Test\n  public void testLoadDrmInitDataNoRepresentations() throws Exception {\n    Period period = newPeriod(newAdaptationSets(/* no representation */ ));\n    DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period);\n    assertThat(drmInitData).isNull();\n  }\n\n  @Test\n  public void testLoadDrmInitDataNoAdaptationSets() throws Exception {\n    Period period = newPeriod(/* no adaptation set */ );\n    DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period);\n    assertThat(drmInitData).isNull();\n  }\n\n  private static Period newPeriod(AdaptationSet... adaptationSets) {\n    return new Period(\"\", 0, Arrays.asList(adaptationSets));\n  }\n\n  private static AdaptationSet newAdaptationSets(Representation... representations) {\n    return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null, null);\n  }\n\n  private static Representation newRepresentations(DrmInitData drmInitData) {\n    Format format =\n        Format.createVideoContainerFormat(\n            \"id\",\n            \"label\",\n            MimeTypes.VIDEO_MP4,\n            MimeTypes.VIDEO_H264,\n            /* codecs= */ \"\",\n            /* metadata= */ null,\n            Format.NO_VALUE,\n            /* width= */ 1024,\n            /* height= */ 768,\n            Format.NO_VALUE,\n            /* initializationData= */ null,\n            /* selectionFlags= */ 0,\n            /* roleFlags= */ 0);\n    if (drmInitData != null) {\n      format = format.copyWithDrmInitData(drmInitData);\n    }\n    return Representation.newInstance(0, format, \"\", new SingleSegmentBase());\n  }\n\n  private static DrmInitData newDrmInitData() {\n    return new DrmInitData(\n        new SchemeData(C.WIDEVINE_UUID, \"mimeType\", new byte[] {1, 4, 7, 0, 3, 6}));\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/EventSampleStreamTest.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.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\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.MetadataInputBuffer;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessage;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder;\nimport com.google.android.exoplayer2.source.dash.manifest.EventStream;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link EventSampleStream}. */\n@RunWith(AndroidJUnit4.class)\npublic final class EventSampleStreamTest {\n\n  private static final String SCHEME_ID = \"urn:test\";\n  private static final String VALUE = \"123\";\n  private static final Format FORMAT = Format.createSampleFormat(\"urn:test/123\",\n      MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null);\n  private static final byte[] MESSAGE_DATA = new byte[] {1, 2, 3, 4};\n  private static final long DURATION_MS = 3000;\n  private static final long TIME_SCALE = 1000;\n\n  private FormatHolder formatHolder;\n  private MetadataInputBuffer inputBuffer;\n  private EventMessageEncoder eventMessageEncoder;\n\n  @Before\n  public void setUp() {\n    formatHolder = new FormatHolder();\n    inputBuffer = new MetadataInputBuffer();\n    eventMessageEncoder = new EventMessageEncoder();\n  }\n\n  /**\n   * Tests that {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} will\n   * return format for the first call.\n   */\n  @Test\n  public void testReadDataReturnFormatForFirstRead() {\n    EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[0], new EventMessage[0]);\n    EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);\n\n    int result = readData(sampleStream);\n    assertThat(result).isEqualTo(C.RESULT_FORMAT_READ);\n    assertThat(formatHolder.format).isEqualTo(FORMAT);\n  }\n\n  /**\n   * Tests that a non-dynamic {@link EventSampleStream} will return a buffer with\n   * {@link C#BUFFER_FLAG_END_OF_STREAM} when trying to read sample out-of-bound.\n   */\n  @Test\n  public void testReadDataOutOfBoundReturnEndOfStreamAfterFormatForNonDynamicEventSampleStream() {\n    EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[0], new EventMessage[0]);\n    EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);\n    // first read - read format\n    readData(sampleStream);\n\n    int result = readData(sampleStream);\n    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);\n    assertThat(inputBuffer.isEndOfStream()).isTrue();\n  }\n\n  /**\n   * Tests that a dynamic {@link EventSampleStream} will return {@link C#RESULT_NOTHING_READ}\n   * when trying to read sample out-of-bound.\n   */\n  @Test\n  public void testReadDataOutOfBoundReturnEndOfStreamAfterFormatForDynamicEventSampleStream() {\n    EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[0], new EventMessage[0]);\n    EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, true);\n    // first read - read format\n    readData(sampleStream);\n\n    int result = readData(sampleStream);\n    assertThat(result).isEqualTo(C.RESULT_NOTHING_READ);\n  }\n\n  /**\n   * Tests that {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} will\n   * return sample data after the first call.\n   */\n  @Test\n  public void testReadDataReturnDataAfterFormat() {\n    long presentationTimeUs = 1000000;\n    EventMessage eventMessage = newEventMessageWithId(1);\n    EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[] {presentationTimeUs}, new EventMessage[] {eventMessage});\n    EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);\n    // first read - read format\n    readData(sampleStream);\n\n    int result = readData(sampleStream);\n    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);\n    assertThat(inputBuffer.data.array())\n        .isEqualTo(getEncodedMessage(eventMessage));\n  }\n\n  /**\n   * Tests that {@link EventSampleStream#skipData(long)} will skip until the given position, and\n   * the next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call\n   * will return sample data from that position.\n   */\n  @Test\n  public void testSkipDataThenReadDataReturnDataFromSkippedPosition() {\n    long presentationTimeUs1 = 1000000;\n    long presentationTimeUs2 = 2000000;\n    EventMessage eventMessage1 = newEventMessageWithId(1);\n    EventMessage eventMessage2 = newEventMessageWithId(2);\n    EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[] {presentationTimeUs1, presentationTimeUs2},\n        new EventMessage[] {eventMessage1, eventMessage2});\n    EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);\n    // first read - read format\n    readData(sampleStream);\n\n    int skipped = sampleStream.skipData(presentationTimeUs2);\n    int result = readData(sampleStream);\n    assertThat(skipped).isEqualTo(1);\n    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);\n    assertThat(inputBuffer.data.array())\n        .isEqualTo(getEncodedMessage(eventMessage2));\n  }\n\n  /**\n   * Tests that {@link EventSampleStream#seekToUs(long)} (long)} will seek to the given position,\n   * and the next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call\n   * will return sample data from that position.\n   */\n  @Test\n  public void testSeekToUsThenReadDataReturnDataFromSeekPosition() {\n    long presentationTimeUs1 = 1000000;\n    long presentationTimeUs2 = 2000000;\n    EventMessage eventMessage1 = newEventMessageWithId(1);\n    EventMessage eventMessage2 = newEventMessageWithId(2);\n    EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[] {presentationTimeUs1, presentationTimeUs2},\n        new EventMessage[] {eventMessage1, eventMessage2});\n    EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false);\n    // first read - read format\n    readData(sampleStream);\n\n    sampleStream.seekToUs(presentationTimeUs2);\n    int result = readData(sampleStream);\n    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);\n    assertThat(inputBuffer.data.array())\n        .isEqualTo(getEncodedMessage(eventMessage2));\n  }\n\n  /**\n   * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the\n   * underlying event stream, but keep the read timestamp, so the next\n   * {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call\n   * will return sample data from after the last read sample timestamp.\n   */\n  @Test\n  public void testUpdateEventStreamContinueToReadAfterLastReadSamplePresentationTime() {\n    long presentationTimeUs1 = 1000000;\n    long presentationTimeUs2 = 2000000;\n    long presentationTimeUs3 = 3000000;\n    EventMessage eventMessage1 = newEventMessageWithId(1);\n    EventMessage eventMessage2 = newEventMessageWithId(2);\n    EventMessage eventMessage3 = newEventMessageWithId(3);\n    EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[] {presentationTimeUs1, presentationTimeUs2},\n        new EventMessage[] {eventMessage1, eventMessage2});\n    EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},\n        new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});\n    EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);\n    // first read - read format\n    readData(sampleStream);\n    // read first and second sample.\n    readData(sampleStream);\n    readData(sampleStream);\n\n    sampleStream.updateEventStream(eventStream2, true);\n    int result = readData(sampleStream);\n    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);\n    assertThat(inputBuffer.data.array())\n        .isEqualTo(getEncodedMessage(eventMessage3));\n  }\n\n  /**\n   * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the\n   * underlying event stream, but keep the timestamp the stream has skipped to, so the next\n   * {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call\n   * will return sample data from the skipped position.\n   */\n  @Test\n  public void testSkipDataThenUpdateStreamContinueToReadFromSkippedPosition() {\n    long presentationTimeUs1 = 1000000;\n    long presentationTimeUs2 = 2000000;\n    long presentationTimeUs3 = 3000000;\n    EventMessage eventMessage1 = newEventMessageWithId(1);\n    EventMessage eventMessage2 = newEventMessageWithId(2);\n    EventMessage eventMessage3 = newEventMessageWithId(3);\n    EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[] {presentationTimeUs1, presentationTimeUs2},\n        new EventMessage[] {eventMessage1, eventMessage2});\n    EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},\n        new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});\n    EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);\n    // first read - read format\n    readData(sampleStream);\n    sampleStream.skipData(presentationTimeUs2 + 1);\n\n    sampleStream.updateEventStream(eventStream2, true);\n    int result = readData(sampleStream);\n    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);\n    assertThat(inputBuffer.data.array())\n        .isEqualTo(getEncodedMessage(eventMessage3));\n  }\n\n  /**\n   * Tests that {@link EventSampleStream#skipData(long)} will only skip to the point right after\n   * it last event. A following {@link EventSampleStream#updateEventStream(EventStream, boolean)}\n   * will update the underlying event stream and keep the timestamp the stream has skipped to, so\n   * the next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call\n   * will return sample data from the skipped position.\n   */\n  @Test\n  public void testSkipDataThenUpdateStreamContinueToReadDoNotSkippedMoreThanAvailable() {\n    long presentationTimeUs1 = 1000000;\n    long presentationTimeUs2 = 2000000;\n    long presentationTimeUs3 = 3000000;\n    EventMessage eventMessage1 = newEventMessageWithId(1);\n    EventMessage eventMessage2 = newEventMessageWithId(2);\n    EventMessage eventMessage3 = newEventMessageWithId(3);\n    EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[] {presentationTimeUs1},\n        new EventMessage[] {eventMessage1});\n    EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},\n        new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});\n    EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);\n    // first read - read format\n    readData(sampleStream);\n    // even though the skip call is to 2000001, since eventStream1 only contains sample until\n    // 1000000, it will only skip to 1000001.\n    sampleStream.skipData(presentationTimeUs2 + 1);\n\n    sampleStream.updateEventStream(eventStream2, true);\n    int result = readData(sampleStream);\n    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);\n    assertThat(inputBuffer.data.array())\n        .isEqualTo(getEncodedMessage(eventMessage2));\n  }\n\n  /**\n   * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the\n   * underlying event stream, but keep the timestamp the stream has seek to, so the next\n   * {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call\n   * will return sample data from the seek position.\n   */\n  @Test\n  public void testSeekToUsThenUpdateStreamContinueToReadFromSeekPosition() {\n    long presentationTimeUs1 = 1000000;\n    long presentationTimeUs2 = 2000000;\n    long presentationTimeUs3 = 3000000;\n    EventMessage eventMessage1 = newEventMessageWithId(1);\n    EventMessage eventMessage2 = newEventMessageWithId(2);\n    EventMessage eventMessage3 = newEventMessageWithId(3);\n    EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[] {presentationTimeUs1, presentationTimeUs2},\n        new EventMessage[] {eventMessage1, eventMessage2});\n    EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},\n        new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});\n    EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);\n    // first read - read format\n    readData(sampleStream);\n    sampleStream.seekToUs(presentationTimeUs2);\n\n    sampleStream.updateEventStream(eventStream2, true);\n    int result = readData(sampleStream);\n    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);\n    assertThat(inputBuffer.data.array())\n        .isEqualTo(getEncodedMessage(eventMessage2));\n  }\n\n  /**\n   * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the\n   * underlying event stream, but keep the timestamp the stream has seek to, so the next\n   * {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call\n   * will return sample data from the seek position.\n   */\n  @Test\n  public void testSeekToThenUpdateStreamContinueToReadFromSeekPositionEvenSeekMoreThanAvailable() {\n    long presentationTimeUs1 = 1000000;\n    long presentationTimeUs2 = 2000000;\n    long presentationTimeUs3 = 3000000;\n    EventMessage eventMessage1 = newEventMessageWithId(1);\n    EventMessage eventMessage2 = newEventMessageWithId(2);\n    EventMessage eventMessage3 = newEventMessageWithId(3);\n    EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[] {presentationTimeUs1},\n        new EventMessage[] {eventMessage1});\n    EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE,\n        new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3},\n        new EventMessage[] {eventMessage1, eventMessage2, eventMessage3});\n    EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true);\n    // first read - read format\n    readData(sampleStream);\n    sampleStream.seekToUs(presentationTimeUs2 + 1);\n\n    sampleStream.updateEventStream(eventStream2, true);\n    int result = readData(sampleStream);\n    assertThat(result).isEqualTo(C.RESULT_BUFFER_READ);\n    assertThat(inputBuffer.data.array())\n        .isEqualTo(getEncodedMessage(eventMessage3));\n  }\n\n  private int readData(EventSampleStream sampleStream) {\n    inputBuffer.clear();\n    return sampleStream.readData(formatHolder, inputBuffer, false);\n  }\n\n  private EventMessage newEventMessageWithId(int id) {\n    return new EventMessage(SCHEME_ID, VALUE, DURATION_MS, id, MESSAGE_DATA);\n  }\n\n  private byte[] getEncodedMessage(EventMessage eventMessage) {\n    return eventMessageEncoder.encode(eventMessage);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.metadata.emsg.EventMessage;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.io.StringReader;\nimport java.nio.charset.Charset;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.xmlpull.v1.XmlPullParser;\nimport org.xmlpull.v1.XmlPullParserFactory;\n\n/** Unit tests for {@link DashManifestParser}. */\n@RunWith(AndroidJUnit4.class)\npublic class DashManifestParserTest {\n\n  private static final String SAMPLE_MPD = \"sample_mpd\";\n  private static final String SAMPLE_MPD_UNKNOWN_MIME_TYPE = \"sample_mpd_unknown_mime_type\";\n  private static final String SAMPLE_MPD_SEGMENT_TEMPLATE = \"sample_mpd_segment_template\";\n  private static final String SAMPLE_MPD_EVENT_STREAM = \"sample_mpd_event_stream\";\n  private static final String SAMPLE_MPD_LABELS = \"sample_mpd_labels\";\n\n  private static final String NEXT_TAG_NAME = \"Next\";\n  private static final String NEXT_TAG = \"<\" + NEXT_TAG_NAME + \"/>\";\n\n  /** Simple test to ensure the sample manifests parse without any exceptions being thrown. */\n  @Test\n  public void parseMediaPresentationDescription() throws IOException {\n    DashManifestParser parser = new DashManifestParser();\n    parser.parse(\n        Uri.parse(\"https://example.com/test.mpd\"),\n        TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD));\n    parser.parse(\n        Uri.parse(\"https://example.com/test.mpd\"),\n        TestUtil.getInputStream(\n            ApplicationProvider.getApplicationContext(), SAMPLE_MPD_UNKNOWN_MIME_TYPE));\n  }\n\n  @Test\n  public void parseMediaPresentationDescription_segmentTemplate() throws IOException {\n    DashManifestParser parser = new DashManifestParser();\n    DashManifest mpd =\n        parser.parse(\n            Uri.parse(\"https://example.com/test.mpd\"),\n            TestUtil.getInputStream(\n                ApplicationProvider.getApplicationContext(), SAMPLE_MPD_SEGMENT_TEMPLATE));\n\n    assertThat(mpd.getPeriodCount()).isEqualTo(1);\n\n    Period period = mpd.getPeriod(0);\n    assertThat(period).isNotNull();\n    assertThat(period.adaptationSets).hasSize(2);\n\n    for (AdaptationSet adaptationSet : period.adaptationSets) {\n      assertThat(adaptationSet).isNotNull();\n      for (Representation representation : adaptationSet.representations) {\n        if (representation instanceof Representation.MultiSegmentRepresentation) {\n          Representation.MultiSegmentRepresentation multiSegmentRepresentation =\n              (Representation.MultiSegmentRepresentation) representation;\n          long firstSegmentIndex = multiSegmentRepresentation.getFirstSegmentNum();\n          RangedUri uri = multiSegmentRepresentation.getSegmentUrl(firstSegmentIndex);\n          assertThat(\n                  uri.resolveUriString(representation.baseUrl)\n                      .contains(\"redirector.googlevideo.com\"))\n              .isTrue();\n        }\n      }\n    }\n  }\n\n  @Test\n  public void parseMediaPresentationDescription_eventStream() throws IOException {\n    DashManifestParser parser = new DashManifestParser();\n    DashManifest mpd =\n        parser.parse(\n            Uri.parse(\"https://example.com/test.mpd\"),\n            TestUtil.getInputStream(\n                ApplicationProvider.getApplicationContext(), SAMPLE_MPD_EVENT_STREAM));\n\n    Period period = mpd.getPeriod(0);\n    assertThat(period.eventStreams).hasSize(3);\n\n    // assert text-only event stream\n    EventStream eventStream1 = period.eventStreams.get(0);\n    assertThat(eventStream1.events.length).isEqualTo(1);\n    EventMessage expectedEvent1 =\n        new EventMessage(\n            \"urn:uuid:XYZY\",\n            \"call\",\n            10000,\n            0,\n            \"+ 1 800 10101010\".getBytes(Charset.forName(C.UTF8_NAME)));\n    assertThat(eventStream1.events[0]).isEqualTo(expectedEvent1);\n    assertThat(eventStream1.presentationTimesUs[0]).isEqualTo(0);\n\n    // assert CData-structured event stream\n    EventStream eventStream2 = period.eventStreams.get(1);\n    assertThat(eventStream2.events.length).isEqualTo(1);\n    EventMessage expectedEvent2 =\n        new EventMessage(\n            \"urn:dvb:iptv:cpm:2014\",\n            \"\",\n            1500000,\n            1,\n            Util.getUtf8Bytes(\n                \"<![CDATA[<BroadcastEvent>\\n\"\n                    + \"      <Program crid=\\\"crid://broadcaster.example.com/ABCDEF\\\"/>\\n\"\n                    + \"      <InstanceDescription>\\n\"\n                    + \"      <Title xml:lang=\\\"en\\\">The title</Title>\\n\"\n                    + \"      <Synopsis xml:lang=\\\"en\\\" length=\\\"medium\\\">\"\n                    + \"The description</Synopsis>\\n\"\n                    + \"      <ParentalGuidance>\\n\"\n                    + \"      <mpeg7:ParentalRating href=\\\"urn:dvb:iptv:rating:2014:15\\\"/>\\n\"\n                    + \"      <mpeg7:Region>GB</mpeg7:Region>\\n\"\n                    + \"      </ParentalGuidance>\\n\"\n                    + \"      </InstanceDescription>\\n\"\n                    + \"      </BroadcastEvent>]]>\"));\n\n    assertThat(eventStream2.events[0]).isEqualTo(expectedEvent2);\n    assertThat(eventStream2.presentationTimesUs[0]).isEqualTo(300000000);\n\n    // assert xml-structured event stream\n    EventStream eventStream3 = period.eventStreams.get(2);\n    assertThat(eventStream3.events.length).isEqualTo(1);\n    EventMessage expectedEvent3 =\n        new EventMessage(\n            \"urn:scte:scte35:2014:xml+bin\",\n            \"\",\n            1000000,\n            2,\n            Util.getUtf8Bytes(\n                \"<scte35:Signal>\\n\"\n                    + \"         <scte35:Binary>\\n\"\n                    + \"         /DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==\\n\"\n                    + \"         </scte35:Binary>\\n\"\n                    + \"       </scte35:Signal>\"));\n    assertThat(eventStream3.events[0]).isEqualTo(expectedEvent3);\n    assertThat(eventStream3.presentationTimesUs[0]).isEqualTo(1000000000);\n  }\n\n  @Test\n  public void parseMediaPresentationDescription_programInformation() throws IOException {\n    DashManifestParser parser = new DashManifestParser();\n    DashManifest mpd =\n        parser.parse(\n            Uri.parse(\"Https://example.com/test.mpd\"),\n            TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD));\n    ProgramInformation expectedProgramInformation =\n        new ProgramInformation(\n            \"MediaTitle\", \"MediaSource\", \"MediaCopyright\", \"www.example.com\", \"enUs\");\n    assertThat(mpd.programInformation).isEqualTo(expectedProgramInformation);\n  }\n\n  @Test\n  public void parseMediaPresentationDescription_labels() throws IOException {\n    DashManifestParser parser = new DashManifestParser();\n    DashManifest manifest =\n        parser.parse(\n            Uri.parse(\"https://example.com/test.mpd\"),\n            TestUtil.getInputStream(\n                ApplicationProvider.getApplicationContext(), SAMPLE_MPD_LABELS));\n\n    List<AdaptationSet> adaptationSets = manifest.getPeriod(0).adaptationSets;\n\n    assertThat(adaptationSets.get(0).representations.get(0).format.label).isEqualTo(\"audio label\");\n    assertThat(adaptationSets.get(1).representations.get(0).format.label).isEqualTo(\"video label\");\n  }\n\n  @Test\n  public void parseLabel() throws Exception {\n    DashManifestParser parser = new DashManifestParser();\n    XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();\n    xpp.setInput(new StringReader(\"<Label>test label</Label>\" + NEXT_TAG));\n    xpp.next();\n\n    String label = parser.parseLabel(xpp);\n    assertThat(label).isEqualTo(\"test label\");\n    assertNextTag(xpp);\n  }\n\n  @Test\n  public void parseLabel_noText() throws Exception {\n    DashManifestParser parser = new DashManifestParser();\n    XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();\n    xpp.setInput(new StringReader(\"<Label/>\" + NEXT_TAG));\n    xpp.next();\n\n    String label = parser.parseLabel(xpp);\n    assertThat(label).isEqualTo(\"\");\n    assertNextTag(xpp);\n  }\n\n  @Test\n  public void parseCea608AccessibilityChannel() {\n    assertThat(\n            DashManifestParser.parseCea608AccessibilityChannel(\n                buildCea608AccessibilityDescriptors(\"CC1=eng\")))\n        .isEqualTo(1);\n    assertThat(\n            DashManifestParser.parseCea608AccessibilityChannel(\n                buildCea608AccessibilityDescriptors(\"CC2=eng\")))\n        .isEqualTo(2);\n    assertThat(\n            DashManifestParser.parseCea608AccessibilityChannel(\n                buildCea608AccessibilityDescriptors(\"CC3=eng\")))\n        .isEqualTo(3);\n    assertThat(\n            DashManifestParser.parseCea608AccessibilityChannel(\n                buildCea608AccessibilityDescriptors(\"CC4=eng\")))\n        .isEqualTo(4);\n\n    assertThat(\n            DashManifestParser.parseCea608AccessibilityChannel(\n                buildCea608AccessibilityDescriptors(null)))\n        .isEqualTo(Format.NO_VALUE);\n    assertThat(\n            DashManifestParser.parseCea608AccessibilityChannel(\n                buildCea608AccessibilityDescriptors(\"\")))\n        .isEqualTo(Format.NO_VALUE);\n    assertThat(\n            DashManifestParser.parseCea608AccessibilityChannel(\n                buildCea608AccessibilityDescriptors(\"CC0=eng\")))\n        .isEqualTo(Format.NO_VALUE);\n    assertThat(\n            DashManifestParser.parseCea608AccessibilityChannel(\n                buildCea608AccessibilityDescriptors(\"CC5=eng\")))\n        .isEqualTo(Format.NO_VALUE);\n    assertThat(\n            DashManifestParser.parseCea608AccessibilityChannel(\n                buildCea608AccessibilityDescriptors(\"Wrong format\")))\n        .isEqualTo(Format.NO_VALUE);\n  }\n\n  @Test\n  public void parseCea708AccessibilityChannel() {\n    assertThat(\n            DashManifestParser.parseCea708AccessibilityChannel(\n                buildCea708AccessibilityDescriptors(\"1=lang:eng\")))\n        .isEqualTo(1);\n    assertThat(\n            DashManifestParser.parseCea708AccessibilityChannel(\n                buildCea708AccessibilityDescriptors(\"2=lang:eng\")))\n        .isEqualTo(2);\n    assertThat(\n            DashManifestParser.parseCea708AccessibilityChannel(\n                buildCea708AccessibilityDescriptors(\"3=lang:eng\")))\n        .isEqualTo(3);\n    assertThat(\n            DashManifestParser.parseCea708AccessibilityChannel(\n                buildCea708AccessibilityDescriptors(\"62=lang:eng\")))\n        .isEqualTo(62);\n    assertThat(\n            DashManifestParser.parseCea708AccessibilityChannel(\n                buildCea708AccessibilityDescriptors(\"63=lang:eng\")))\n        .isEqualTo(63);\n\n    assertThat(\n            DashManifestParser.parseCea708AccessibilityChannel(\n                buildCea708AccessibilityDescriptors(null)))\n        .isEqualTo(Format.NO_VALUE);\n    assertThat(\n            DashManifestParser.parseCea708AccessibilityChannel(\n                buildCea708AccessibilityDescriptors(\"\")))\n        .isEqualTo(Format.NO_VALUE);\n    assertThat(\n            DashManifestParser.parseCea708AccessibilityChannel(\n                buildCea708AccessibilityDescriptors(\"0=lang:eng\")))\n        .isEqualTo(Format.NO_VALUE);\n    assertThat(\n            DashManifestParser.parseCea708AccessibilityChannel(\n                buildCea708AccessibilityDescriptors(\"64=lang:eng\")))\n        .isEqualTo(Format.NO_VALUE);\n    assertThat(\n            DashManifestParser.parseCea708AccessibilityChannel(\n                buildCea708AccessibilityDescriptors(\"Wrong format\")))\n        .isEqualTo(Format.NO_VALUE);\n  }\n\n  private static List<Descriptor> buildCea608AccessibilityDescriptors(String value) {\n    return Collections.singletonList(new Descriptor(\"urn:scte:dash:cc:cea-608:2015\", value, null));\n  }\n\n  private static List<Descriptor> buildCea708AccessibilityDescriptors(String value) {\n    return Collections.singletonList(new Descriptor(\"urn:scte:dash:cc:cea-708:2015\", value, null));\n  }\n\n  private static void assertNextTag(XmlPullParser xpp) throws Exception {\n    xpp.next();\n    assertThat(xpp.getEventType()).isEqualTo(XmlPullParser.START_TAG);\n    assertThat(xpp.getName()).isEqualTo(NEXT_TAG_NAME);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.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 static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Random;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link DashManifest}. */\n@RunWith(AndroidJUnit4.class)\npublic class DashManifestTest {\n\n  private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement(\"\", \"\");\n  private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase();\n  private static final Format DUMMY_FORMAT = Format.createSampleFormat(\"\", \"\", 0);\n\n  @Test\n  public void testCopy() throws Exception {\n    Representation[][][] representations = newRepresentations(3, 2, 3);\n    DashManifest sourceManifest =\n        newDashManifest(\n            10,\n            newPeriod(\n                \"1\",\n                1,\n                newAdaptationSet(2, representations[0][0]),\n                newAdaptationSet(3, representations[0][1])),\n            newPeriod(\n                \"4\",\n                4,\n                newAdaptationSet(5, representations[1][0]),\n                newAdaptationSet(6, representations[1][1])),\n            newPeriod(\n                \"7\",\n                7,\n                newAdaptationSet(8, representations[2][0]),\n                newAdaptationSet(9, representations[2][1])));\n\n    List<StreamKey> keys =\n        Arrays.asList(\n            new StreamKey(0, 0, 0),\n            new StreamKey(0, 0, 1),\n            new StreamKey(0, 1, 2),\n            new StreamKey(1, 0, 1),\n            new StreamKey(1, 1, 0),\n            new StreamKey(1, 1, 2),\n            new StreamKey(2, 0, 1),\n            new StreamKey(2, 0, 2),\n            new StreamKey(2, 1, 0));\n    // Keys don't need to be in any particular order\n    Collections.shuffle(keys, new Random(0));\n\n    DashManifest copyManifest = sourceManifest.copy(keys);\n\n    DashManifest expectedManifest =\n        newDashManifest(\n            10,\n            newPeriod(\n                \"1\",\n                1,\n                newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),\n                newAdaptationSet(3, representations[0][1][2])),\n            newPeriod(\n                \"4\",\n                4,\n                newAdaptationSet(5, representations[1][0][1]),\n                newAdaptationSet(6, representations[1][1][0], representations[1][1][2])),\n            newPeriod(\n                \"7\",\n                7,\n                newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),\n                newAdaptationSet(9, representations[2][1][0])));\n    assertManifestEquals(expectedManifest, copyManifest);\n  }\n\n  @Test\n  public void testCopySameAdaptationIndexButDifferentPeriod() throws Exception {\n    Representation[][][] representations = newRepresentations(2, 1, 1);\n    DashManifest sourceManifest =\n        newDashManifest(\n            10,\n            newPeriod(\"1\", 1, newAdaptationSet(2, representations[0][0])),\n            newPeriod(\"4\", 4, newAdaptationSet(5, representations[1][0])));\n\n    DashManifest copyManifest =\n        sourceManifest.copy(Arrays.asList(new StreamKey(0, 0, 0), new StreamKey(1, 0, 0)));\n\n    DashManifest expectedManifest =\n        newDashManifest(\n            10,\n            newPeriod(\"1\", 1, newAdaptationSet(2, representations[0][0])),\n            newPeriod(\"4\", 4, newAdaptationSet(5, representations[1][0])));\n    assertManifestEquals(expectedManifest, copyManifest);\n  }\n\n  @Test\n  public void testCopySkipPeriod() throws Exception {\n    Representation[][][] representations = newRepresentations(3, 2, 3);\n    DashManifest sourceManifest =\n        newDashManifest(\n            10,\n            newPeriod(\n                \"1\",\n                1,\n                newAdaptationSet(2, representations[0][0]),\n                newAdaptationSet(3, representations[0][1])),\n            newPeriod(\n                \"4\",\n                4,\n                newAdaptationSet(5, representations[1][0]),\n                newAdaptationSet(6, representations[1][1])),\n            newPeriod(\n                \"7\",\n                7,\n                newAdaptationSet(8, representations[2][0]),\n                newAdaptationSet(9, representations[2][1])));\n\n    DashManifest copyManifest =\n        sourceManifest.copy(\n            Arrays.asList(\n                new StreamKey(0, 0, 0),\n                new StreamKey(0, 0, 1),\n                new StreamKey(0, 1, 2),\n                new StreamKey(2, 0, 1),\n                new StreamKey(2, 0, 2),\n                new StreamKey(2, 1, 0)));\n\n    DashManifest expectedManifest =\n        newDashManifest(\n            7,\n            newPeriod(\n                \"1\",\n                1,\n                newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),\n                newAdaptationSet(3, representations[0][1][2])),\n            newPeriod(\n                \"7\",\n                4,\n                newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),\n                newAdaptationSet(9, representations[2][1][0])));\n    assertManifestEquals(expectedManifest, copyManifest);\n  }\n\n  private static void assertManifestEquals(DashManifest expected, DashManifest actual) {\n    assertThat(actual.availabilityStartTimeMs).isEqualTo(expected.availabilityStartTimeMs);\n    assertThat(actual.durationMs).isEqualTo(expected.durationMs);\n    assertThat(actual.minBufferTimeMs).isEqualTo(expected.minBufferTimeMs);\n    assertThat(actual.dynamic).isEqualTo(expected.dynamic);\n    assertThat(actual.minUpdatePeriodMs).isEqualTo(expected.minUpdatePeriodMs);\n    assertThat(actual.timeShiftBufferDepthMs).isEqualTo(expected.timeShiftBufferDepthMs);\n    assertThat(actual.suggestedPresentationDelayMs)\n        .isEqualTo(expected.suggestedPresentationDelayMs);\n    assertThat(actual.publishTimeMs).isEqualTo(expected.publishTimeMs);\n    assertThat(actual.utcTiming).isEqualTo(expected.utcTiming);\n    assertThat(actual.location).isEqualTo(expected.location);\n    assertThat(actual.getPeriodCount()).isEqualTo(expected.getPeriodCount());\n    for (int i = 0; i < expected.getPeriodCount(); i++) {\n      Period expectedPeriod = expected.getPeriod(i);\n      Period actualPeriod = actual.getPeriod(i);\n      assertThat(actualPeriod.id).isEqualTo(expectedPeriod.id);\n      assertThat(actualPeriod.startMs).isEqualTo(expectedPeriod.startMs);\n      List<AdaptationSet> expectedAdaptationSets = expectedPeriod.adaptationSets;\n      List<AdaptationSet> actualAdaptationSets = actualPeriod.adaptationSets;\n      assertThat(actualAdaptationSets).hasSize(expectedAdaptationSets.size());\n      for (int j = 0; j < expectedAdaptationSets.size(); j++) {\n        AdaptationSet expectedAdaptationSet = expectedAdaptationSets.get(j);\n        AdaptationSet actualAdaptationSet = actualAdaptationSets.get(j);\n        assertThat(actualAdaptationSet.id).isEqualTo(expectedAdaptationSet.id);\n        assertThat(actualAdaptationSet.type).isEqualTo(expectedAdaptationSet.type);\n        assertThat(actualAdaptationSet.accessibilityDescriptors)\n            .isEqualTo(expectedAdaptationSet.accessibilityDescriptors);\n        assertThat(actualAdaptationSet.representations)\n            .isEqualTo(expectedAdaptationSet.representations);\n      }\n    }\n  }\n\n  private static Representation[][][] newRepresentations(\n      int periodCount, int adaptationSetCounts, int representationCounts) {\n    Representation[][][] representations = new Representation[periodCount][][];\n    for (int i = 0; i < periodCount; i++) {\n      representations[i] = new Representation[adaptationSetCounts][];\n      for (int j = 0; j < adaptationSetCounts; j++) {\n        representations[i][j] = new Representation[representationCounts];\n        for (int k = 0; k < representationCounts; k++) {\n          representations[i][j][k] = newRepresentation();\n        }\n      }\n    }\n    return representations;\n  }\n\n  private static Representation newRepresentation() {\n    return Representation.newInstance(\n        /* revisionId= */ 0, DUMMY_FORMAT, /* baseUrl= */ \"\", DUMMY_SEGMENT_BASE);\n  }\n\n  private static DashManifest newDashManifest(int duration, Period... periods) {\n    return new DashManifest(\n        /* availabilityStartTimeMs= */ 0,\n        duration,\n        /* minBufferTimeMs= */ 1,\n        /* dynamic= */ false,\n        /* minUpdatePeriodMs= */ 2,\n        /* timeShiftBufferDepthMs= */ 3,\n        /* suggestedPresentationDelayMs= */ 4,\n        /* publishTimeMs= */ 12345,\n        /* programInformation= */ null,\n        DUMMY_UTC_TIMING,\n        Uri.EMPTY,\n        Arrays.asList(periods));\n  }\n\n  private static Period newPeriod(String id, int startMs, AdaptationSet... adaptationSets) {\n    return new Period(id, startMs, Arrays.asList(adaptationSets));\n  }\n\n  private static AdaptationSet newAdaptationSet(int seed, Representation... representations) {\n    return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), null, null);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link RangedUri}. */\n@RunWith(AndroidJUnit4.class)\npublic class RangedUriTest {\n\n  private static final String BASE_URI = \"http://www.test.com/\";\n  private static final String PARTIAL_URI = \"path/file.ext\";\n  private static final String FULL_URI = BASE_URI + PARTIAL_URI;\n\n  @Test\n  public void testMerge() {\n    RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);\n    RangedUri rangeB = new RangedUri(FULL_URI, 10, 10);\n    RangedUri expected = new RangedUri(FULL_URI, 0, 20);\n    assertMerge(rangeA, rangeB, expected, null);\n  }\n\n  @Test\n  public void testMergeUnbounded() {\n    RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);\n    RangedUri rangeB = new RangedUri(FULL_URI, 10, C.LENGTH_UNSET);\n    RangedUri expected = new RangedUri(FULL_URI, 0, C.LENGTH_UNSET);\n    assertMerge(rangeA, rangeB, expected, null);\n  }\n\n  @Test\n  public void testNonMerge() {\n    // A and B do not overlap, so should not merge\n    RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);\n    RangedUri rangeB = new RangedUri(FULL_URI, 11, 10);\n    assertNonMerge(rangeA, rangeB, null);\n\n    // A and B do not overlap, so should not merge\n    rangeA = new RangedUri(FULL_URI, 0, 10);\n    rangeB = new RangedUri(FULL_URI, 11, C.LENGTH_UNSET);\n    assertNonMerge(rangeA, rangeB, null);\n\n    // A and B are bounded but overlap, so should not merge\n    rangeA = new RangedUri(FULL_URI, 0, 11);\n    rangeB = new RangedUri(FULL_URI, 10, 10);\n    assertNonMerge(rangeA, rangeB, null);\n\n    // A and B overlap due to unboundedness, so should not merge\n    rangeA = new RangedUri(FULL_URI, 0, C.LENGTH_UNSET);\n    rangeB = new RangedUri(FULL_URI, 10, C.LENGTH_UNSET);\n    assertNonMerge(rangeA, rangeB, null);\n  }\n\n  @Test\n  public void testMergeWithBaseUri() {\n    RangedUri rangeA = new RangedUri(PARTIAL_URI, 0, 10);\n    RangedUri rangeB = new RangedUri(FULL_URI, 10, 10);\n    RangedUri expected = new RangedUri(FULL_URI, 0, 20);\n    assertMerge(rangeA, rangeB, expected, BASE_URI);\n  }\n\n  private void assertMerge(RangedUri rangeA, RangedUri rangeB, RangedUri expected, String baseUrl) {\n    RangedUri merged = rangeA.attemptMerge(rangeB, baseUrl);\n    assertThat(merged).isEqualTo(expected);\n    merged = rangeB.attemptMerge(rangeA, baseUrl);\n    assertThat(merged).isEqualTo(expected);\n  }\n\n  private void assertNonMerge(RangedUri rangeA, RangedUri rangeB, String baseUrl) {\n    RangedUri merged = rangeA.attemptMerge(rangeB, baseUrl);\n    assertThat(merged).isNull();\n    merged = rangeB.attemptMerge(rangeA, baseUrl);\n    assertThat(merged).isNull();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link UrlTemplate}. */\n@RunWith(AndroidJUnit4.class)\npublic class UrlTemplateTest {\n\n  @Test\n  public void testRealExamples() {\n    String template = \"QualityLevels($Bandwidth$)/Fragments(video=$Time$,format=mpd-time-csf)\";\n    UrlTemplate urlTemplate = UrlTemplate.compile(template);\n    String url = urlTemplate.buildUri(\"abc1\", 10, 650000, 5000);\n    assertThat(url).isEqualTo(\"QualityLevels(650000)/Fragments(video=5000,format=mpd-time-csf)\");\n\n    template = \"$RepresentationID$/$Number$\";\n    urlTemplate = UrlTemplate.compile(template);\n    url = urlTemplate.buildUri(\"abc1\", 10, 650000, 5000);\n    assertThat(url).isEqualTo(\"abc1/10\");\n\n    template = \"chunk_ctvideo_cfm4s_rid$RepresentationID$_cn$Number$_w2073857842_mpd.m4s\";\n    urlTemplate = UrlTemplate.compile(template);\n    url = urlTemplate.buildUri(\"abc1\", 10, 650000, 5000);\n    assertThat(url).isEqualTo(\"chunk_ctvideo_cfm4s_ridabc1_cn10_w2073857842_mpd.m4s\");\n  }\n\n  @Test\n  public void testFull() {\n    String template = \"$Bandwidth$_a_$RepresentationID$_b_$Time$_c_$Number$\";\n    UrlTemplate urlTemplate = UrlTemplate.compile(template);\n    String url = urlTemplate.buildUri(\"abc1\", 10, 650000, 5000);\n    assertThat(url).isEqualTo(\"650000_a_abc1_b_5000_c_10\");\n  }\n\n  @Test\n  public void testFullWithDollarEscaping() {\n    String template = \"$$$Bandwidth$$$_a$$_$RepresentationID$_b_$Time$_c_$Number$$$\";\n    UrlTemplate urlTemplate = UrlTemplate.compile(template);\n    String url = urlTemplate.buildUri(\"abc1\", 10, 650000, 5000);\n    assertThat(url).isEqualTo(\"$650000$_a$_abc1_b_5000_c_10$\");\n  }\n\n  @Test\n  public void testInvalidSubstitution() {\n    String template = \"$IllegalId$\";\n    try {\n      UrlTemplate.compile(template);\n      fail();\n    } catch (IllegalArgumentException e) {\n      // Expected.\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.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 com.google.android.exoplayer2.C;\nimport java.nio.charset.Charset;\n\n/** Data for DASH downloading tests. */\n/* package */ interface DashDownloadTestData {\n\n  String TEST_ID = \"test.mpd\";\n  Uri TEST_MPD_URI = Uri.parse(TEST_ID);\n\n  byte[] TEST_MPD =\n      (\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\\n\"\n              + \"<MPD xmlns:xsi=\\\"http://www.w3.org/2001/XMLSchema-instance\\\" type=\\\"static\\\" \"\n              + \"    mediaPresentationDuration=\\\"PT31S\\\">\\n\"\n              + \"    <Period duration=\\\"PT16S\\\" >\\n\"\n              + \"        <AdaptationSet>\\n\"\n              + \"            <SegmentList>\\n\"\n              + \"                <SegmentTimeline>\\n\"\n              + \"                    <S d=\\\"5\\\" />\\n\"\n              + \"                    <S d=\\\"5\\\" />\\n\"\n              + \"                    <S d=\\\"5\\\" />\\n\"\n              + \"                </SegmentTimeline>\\n\"\n              + \"            </SegmentList>\\n\"\n              + \"            <Representation>\\n\"\n              + \"                <SegmentList>\\n\"\n              // Bounded range data\n              + \"                    <Initialization\\n\"\n              + \"                        range=\\\"0-9\\\" sourceURL=\\\"audio_init_data\\\" />\\n\"\n              // Unbounded range data\n              + \"                    <SegmentURL media=\\\"audio_segment_1\\\" />\\n\"\n              + \"                    <SegmentURL media=\\\"audio_segment_2\\\" />\\n\"\n              + \"                    <SegmentURL media=\\\"audio_segment_3\\\" />\\n\"\n              + \"                </SegmentList>\\n\"\n              + \"            </Representation>\\n\"\n              + \"        </AdaptationSet>\\n\"\n              + \"        <AdaptationSet>\\n\"\n              // This segment list has a 1 second offset to make sure the progressive download order\n              + \"            <SegmentList>\\n\"\n              + \"                <SegmentTimeline>\\n\"\n              + \"                    <S t=\\\"1\\\" d=\\\"5\\\" />\\n\" // 1s offset\n              + \"                    <S d=\\\"5\\\" />\\n\"\n              + \"                    <S d=\\\"5\\\" />\\n\"\n              + \"                </SegmentTimeline>\\n\"\n              + \"            </SegmentList>\\n\"\n              + \"            <Representation>\\n\"\n              + \"                <SegmentList>\\n\"\n              + \"                    <SegmentURL media=\\\"text_segment_1\\\" />\\n\"\n              + \"                    <SegmentURL media=\\\"text_segment_2\\\" />\\n\"\n              + \"                    <SegmentURL media=\\\"text_segment_3\\\" />\\n\"\n              + \"                </SegmentList>\\n\"\n              + \"            </Representation>\\n\"\n              + \"        </AdaptationSet>\\n\"\n              + \"    </Period>\\n\"\n              + \"    <Period>\\n\"\n              + \"        <SegmentList>\\n\"\n              + \"            <SegmentTimeline>\\n\"\n              + \"                <S d=\\\"5\\\" />\\n\"\n              + \"                <S d=\\\"5\\\" />\\n\"\n              + \"                <S d=\\\"5\\\" />\\n\"\n              + \"            </SegmentTimeline>\\n\"\n              + \"        </SegmentList>\\n\"\n              + \"        <AdaptationSet>\\n\"\n              + \"            <Representation>\\n\"\n              + \"                <SegmentList>\\n\"\n              + \"                    <SegmentURL media=\\\"period_2_segment_1\\\" />\\n\"\n              + \"                    <SegmentURL media=\\\"period_2_segment_2\\\" />\\n\"\n              + \"                    <SegmentURL media=\\\"period_2_segment_3\\\" />\\n\"\n              + \"                </SegmentList>\\n\"\n              + \"            </Representation>\\n\"\n              + \"        </AdaptationSet>\\n\"\n              + \"    </Period>\\n\"\n              + \"</MPD>\")\n          .getBytes(Charset.forName(C.UTF8_NAME));\n\n  byte[] TEST_MPD_NO_INDEX =\n      (\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\\n\"\n              + \"<MPD xmlns:xsi=\\\"http://www.w3.org/2001/XMLSchema-instance\\\" type=\\\"dynamic\\\">\\n\"\n              + \"    <Period start=\\\"PT6462826.784S\\\" >\\n\"\n              + \"        <AdaptationSet>\\n\"\n              + \"            <Representation>\\n\"\n              + \"                <SegmentBase indexRange='0-10'/>\\n\"\n              + \"            </Representation>\\n\"\n              + \"        </AdaptationSet>\\n\"\n              + \"    </Period>\\n\"\n              + \"</MPD>\")\n          .getBytes(Charset.forName(C.UTF8_NAME));\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.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 static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD;\nimport static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_NO_INDEX;\nimport static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_URI;\nimport static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;\nimport static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport android.net.Uri;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.offline.DefaultDownloaderFactory;\nimport com.google.android.exoplayer2.offline.DownloadException;\nimport com.google.android.exoplayer2.offline.DownloadRequest;\nimport com.google.android.exoplayer2.offline.Downloader;\nimport com.google.android.exoplayer2.offline.DownloaderConstructorHelper;\nimport com.google.android.exoplayer2.offline.DownloaderFactory;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet;\nimport com.google.android.exoplayer2.testutil.FakeDataSet;\nimport com.google.android.exoplayer2.testutil.FakeDataSource;\nimport com.google.android.exoplayer2.testutil.FakeDataSource.Factory;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.DummyDataSource;\nimport com.google.android.exoplayer2.upstream.cache.Cache;\nimport com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;\nimport com.google.android.exoplayer2.upstream.cache.SimpleCache;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mockito;\nimport org.mockito.MockitoAnnotations;\n\n/** Unit tests for {@link DashDownloader}. */\n@RunWith(AndroidJUnit4.class)\npublic class DashDownloaderTest {\n\n  private SimpleCache cache;\n  private File tempFolder;\n  private ProgressListener progressListener;\n\n  @Before\n  public void setUp() throws Exception {\n    MockitoAnnotations.initMocks(this);\n    tempFolder =\n        Util.createTempDirectory(ApplicationProvider.getApplicationContext(), \"ExoPlayerTest\");\n    cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());\n    progressListener = new ProgressListener();\n  }\n\n  @After\n  public void tearDown() {\n    Util.recursiveDelete(tempFolder);\n  }\n\n  @Test\n  public void testCreateWithDefaultDownloaderFactory() {\n    DownloaderConstructorHelper constructorHelper =\n        new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);\n    DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);\n\n    Downloader downloader =\n        factory.createDownloader(\n            new DownloadRequest(\n                \"id\",\n                DownloadRequest.TYPE_DASH,\n                Uri.parse(\"https://www.test.com/download\"),\n                Collections.singletonList(new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0)),\n                /* customCacheKey= */ null,\n                /* data= */ null));\n    assertThat(downloader).isInstanceOf(DashDownloader.class);\n  }\n\n  @Test\n  public void testDownloadRepresentation() throws Exception {\n    FakeDataSet fakeDataSet =\n        new FakeDataSet()\n            .setData(TEST_MPD_URI, TEST_MPD)\n            .setRandomData(\"audio_init_data\", 10)\n            .setRandomData(\"audio_segment_1\", 4)\n            .setRandomData(\"audio_segment_2\", 5)\n            .setRandomData(\"audio_segment_3\", 6);\n\n    DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));\n    dashDownloader.download(progressListener);\n    assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor(\"audio_init_data\"));\n  }\n\n  @Test\n  public void testDownloadRepresentationInSmallParts() throws Exception {\n    FakeDataSet fakeDataSet =\n        new FakeDataSet()\n            .setData(TEST_MPD_URI, TEST_MPD)\n            .setRandomData(\"audio_init_data\", 10)\n            .newData(\"audio_segment_1\")\n            .appendReadData(TestUtil.buildTestData(10))\n            .appendReadData(TestUtil.buildTestData(10))\n            .appendReadData(TestUtil.buildTestData(10))\n            .endData()\n            .setRandomData(\"audio_segment_2\", 5)\n            .setRandomData(\"audio_segment_3\", 6);\n\n    DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));\n    dashDownloader.download(progressListener);\n    assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor(\"audio_init_data\"));\n  }\n\n  @Test\n  public void testDownloadRepresentations() throws Exception {\n    FakeDataSet fakeDataSet =\n        new FakeDataSet()\n            .setData(TEST_MPD_URI, TEST_MPD)\n            .setRandomData(\"audio_init_data\", 10)\n            .setRandomData(\"audio_segment_1\", 4)\n            .setRandomData(\"audio_segment_2\", 5)\n            .setRandomData(\"audio_segment_3\", 6)\n            .setRandomData(\"text_segment_1\", 1)\n            .setRandomData(\"text_segment_2\", 2)\n            .setRandomData(\"text_segment_3\", 3);\n\n    DashDownloader dashDownloader =\n        getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));\n    dashDownloader.download(progressListener);\n    assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor(\"audio_init_data\"));\n  }\n\n  @Test\n  public void testDownloadAllRepresentations() throws Exception {\n    FakeDataSet fakeDataSet =\n        new FakeDataSet()\n            .setData(TEST_MPD_URI, TEST_MPD)\n            .setRandomData(\"audio_init_data\", 10)\n            .setRandomData(\"audio_segment_1\", 4)\n            .setRandomData(\"audio_segment_2\", 5)\n            .setRandomData(\"audio_segment_3\", 6)\n            .setRandomData(\"text_segment_1\", 1)\n            .setRandomData(\"text_segment_2\", 2)\n            .setRandomData(\"text_segment_3\", 3)\n            .setRandomData(\"period_2_segment_1\", 1)\n            .setRandomData(\"period_2_segment_2\", 2)\n            .setRandomData(\"period_2_segment_3\", 3);\n\n    DashDownloader dashDownloader = getDashDownloader(fakeDataSet);\n    dashDownloader.download(progressListener);\n    assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor(\"audio_init_data\"));\n  }\n\n  @Test\n  public void testProgressiveDownload() throws Exception {\n    FakeDataSet fakeDataSet =\n        new FakeDataSet()\n            .setData(TEST_MPD_URI, TEST_MPD)\n            .setRandomData(\"audio_init_data\", 10)\n            .setRandomData(\"audio_segment_1\", 4)\n            .setRandomData(\"audio_segment_2\", 5)\n            .setRandomData(\"audio_segment_3\", 6)\n            .setRandomData(\"text_segment_1\", 1)\n            .setRandomData(\"text_segment_2\", 2)\n            .setRandomData(\"text_segment_3\", 3);\n    FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet);\n    Factory factory = mock(Factory.class);\n    when(factory.createDataSource()).thenReturn(fakeDataSource);\n\n    DashDownloader dashDownloader =\n        getDashDownloader(factory, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));\n    dashDownloader.download(progressListener);\n\n    DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs();\n    assertThat(openedDataSpecs.length).isEqualTo(8);\n    assertThat(openedDataSpecs[0].uri).isEqualTo(TEST_MPD_URI);\n    assertThat(openedDataSpecs[1].uri.getPath()).isEqualTo(\"audio_init_data\");\n    assertThat(openedDataSpecs[2].uri.getPath()).isEqualTo(\"audio_segment_1\");\n    assertThat(openedDataSpecs[3].uri.getPath()).isEqualTo(\"text_segment_1\");\n    assertThat(openedDataSpecs[4].uri.getPath()).isEqualTo(\"audio_segment_2\");\n    assertThat(openedDataSpecs[5].uri.getPath()).isEqualTo(\"text_segment_2\");\n    assertThat(openedDataSpecs[6].uri.getPath()).isEqualTo(\"audio_segment_3\");\n    assertThat(openedDataSpecs[7].uri.getPath()).isEqualTo(\"text_segment_3\");\n  }\n\n  @Test\n  public void testProgressiveDownloadSeparatePeriods() throws Exception {\n    FakeDataSet fakeDataSet =\n        new FakeDataSet()\n            .setData(TEST_MPD_URI, TEST_MPD)\n            .setRandomData(\"audio_init_data\", 10)\n            .setRandomData(\"audio_segment_1\", 4)\n            .setRandomData(\"audio_segment_2\", 5)\n            .setRandomData(\"audio_segment_3\", 6)\n            .setRandomData(\"period_2_segment_1\", 1)\n            .setRandomData(\"period_2_segment_2\", 2)\n            .setRandomData(\"period_2_segment_3\", 3);\n    FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet);\n    Factory factory = mock(Factory.class);\n    when(factory.createDataSource()).thenReturn(fakeDataSource);\n\n    DashDownloader dashDownloader =\n        getDashDownloader(factory, new StreamKey(0, 0, 0), new StreamKey(1, 0, 0));\n    dashDownloader.download(progressListener);\n\n    DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs();\n    assertThat(openedDataSpecs.length).isEqualTo(8);\n    assertThat(openedDataSpecs[0].uri).isEqualTo(TEST_MPD_URI);\n    assertThat(openedDataSpecs[1].uri.getPath()).isEqualTo(\"audio_init_data\");\n    assertThat(openedDataSpecs[2].uri.getPath()).isEqualTo(\"audio_segment_1\");\n    assertThat(openedDataSpecs[3].uri.getPath()).isEqualTo(\"audio_segment_2\");\n    assertThat(openedDataSpecs[4].uri.getPath()).isEqualTo(\"audio_segment_3\");\n    assertThat(openedDataSpecs[5].uri.getPath()).isEqualTo(\"period_2_segment_1\");\n    assertThat(openedDataSpecs[6].uri.getPath()).isEqualTo(\"period_2_segment_2\");\n    assertThat(openedDataSpecs[7].uri.getPath()).isEqualTo(\"period_2_segment_3\");\n  }\n\n  @Test\n  public void testDownloadRepresentationFailure() throws Exception {\n    FakeDataSet fakeDataSet =\n        new FakeDataSet()\n            .setData(TEST_MPD_URI, TEST_MPD)\n            .setRandomData(\"audio_init_data\", 10)\n            .setRandomData(\"audio_segment_1\", 4)\n            .newData(\"audio_segment_2\")\n            .appendReadData(TestUtil.buildTestData(2))\n            .appendReadError(new IOException())\n            .appendReadData(TestUtil.buildTestData(3))\n            .endData()\n            .setRandomData(\"audio_segment_3\", 6);\n\n    DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));\n    try {\n      dashDownloader.download(progressListener);\n      fail();\n    } catch (IOException e) {\n      // Expected.\n    }\n    dashDownloader.download(progressListener);\n    assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor(\"audio_init_data\"));\n  }\n\n  @Test\n  public void testCounters() throws Exception {\n    FakeDataSet fakeDataSet =\n        new FakeDataSet()\n            .setData(TEST_MPD_URI, TEST_MPD)\n            .setRandomData(\"audio_init_data\", 10)\n            .setRandomData(\"audio_segment_1\", 4)\n            .newData(\"audio_segment_2\")\n            .appendReadData(TestUtil.buildTestData(2))\n            .appendReadError(new IOException())\n            .appendReadData(TestUtil.buildTestData(3))\n            .endData()\n            .setRandomData(\"audio_segment_3\", 6);\n\n    DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));\n\n    try {\n      dashDownloader.download(progressListener);\n      fail();\n    } catch (IOException e) {\n      // Failure expected after downloading init data, segment 1 and 2 bytes in segment 2.\n    }\n    progressListener.assertBytesDownloaded(10 + 4 + 2);\n\n    dashDownloader.download(progressListener);\n    progressListener.assertBytesDownloaded(10 + 4 + 5 + 6);\n  }\n\n  @Test\n  public void testRemove() throws Exception {\n    FakeDataSet fakeDataSet =\n        new FakeDataSet()\n            .setData(TEST_MPD_URI, TEST_MPD)\n            .setRandomData(\"audio_init_data\", 10)\n            .setRandomData(\"audio_segment_1\", 4)\n            .setRandomData(\"audio_segment_2\", 5)\n            .setRandomData(\"audio_segment_3\", 6)\n            .setRandomData(\"text_segment_1\", 1)\n            .setRandomData(\"text_segment_2\", 2)\n            .setRandomData(\"text_segment_3\", 3);\n\n    DashDownloader dashDownloader =\n        getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0));\n    dashDownloader.download(progressListener);\n    dashDownloader.remove();\n    assertCacheEmpty(cache);\n  }\n\n  @Test\n  public void testRepresentationWithoutIndex() throws Exception {\n    FakeDataSet fakeDataSet =\n        new FakeDataSet()\n            .setData(TEST_MPD_URI, TEST_MPD_NO_INDEX)\n            .setRandomData(\"test_segment_1\", 4);\n\n    DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0));\n    try {\n      dashDownloader.download(progressListener);\n      fail();\n    } catch (DownloadException e) {\n      // Expected.\n    }\n    dashDownloader.remove();\n    assertCacheEmpty(cache);\n  }\n\n  private DashDownloader getDashDownloader(FakeDataSet fakeDataSet, StreamKey... keys) {\n    return getDashDownloader(new Factory().setFakeDataSet(fakeDataSet), keys);\n  }\n\n  private DashDownloader getDashDownloader(Factory factory, StreamKey... keys) {\n    return new DashDownloader(\n        TEST_MPD_URI, keysList(keys), new DownloaderConstructorHelper(cache, factory));\n  }\n\n  private static ArrayList<StreamKey> keysList(StreamKey... keys) {\n    ArrayList<StreamKey> keysList = new ArrayList<>();\n    Collections.addAll(keysList, keys);\n    return keysList;\n  }\n\n  private static final class ProgressListener implements Downloader.ProgressListener {\n\n    private long bytesDownloaded;\n\n    @Override\n    public void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded) {\n      this.bytesDownloaded = bytesDownloaded;\n    }\n\n    public void assertBytesDownloaded(long bytesDownloaded) {\n      assertThat(this.bytesDownloaded).isEqualTo(bytesDownloaded);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.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.offline;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.offline.DownloadHelper;\nimport com.google.android.exoplayer2.testutil.FakeDataSource;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test to verify creation of a DASH {@link DownloadHelper}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DownloadHelperTest {\n\n  @Test\n  public void staticDownloadHelperForDash_doesNotThrow() {\n    DownloadHelper.forDash(\n        Uri.parse(\"http://uri\"),\n        new FakeDataSource.Factory(),\n        (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]);\n    DownloadHelper.forDash(\n        Uri.parse(\"http://uri\"),\n        new FakeDataSource.Factory(),\n        (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0],\n        /* drmSessionManager= */ null,\n        DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.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 static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_ID;\nimport static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD;\nimport static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_URI;\nimport static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;\nimport static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.content.Context;\nimport android.os.ConditionVariable;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.offline.DefaultDownloadIndex;\nimport com.google.android.exoplayer2.offline.DefaultDownloaderFactory;\nimport com.google.android.exoplayer2.offline.DownloadManager;\nimport com.google.android.exoplayer2.offline.DownloadRequest;\nimport com.google.android.exoplayer2.offline.DownloaderConstructorHelper;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.scheduler.Requirements;\nimport com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet;\nimport com.google.android.exoplayer2.testutil.DummyMainThread;\nimport com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable;\nimport com.google.android.exoplayer2.testutil.FakeDataSet;\nimport com.google.android.exoplayer2.testutil.FakeDataSource;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\nimport com.google.android.exoplayer2.testutil.TestDownloadManagerListener;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.upstream.DataSource.Factory;\nimport com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;\nimport com.google.android.exoplayer2.upstream.cache.SimpleCache;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.MockitoAnnotations;\nimport org.robolectric.annotation.Config;\nimport org.robolectric.shadows.ShadowLog;\n\n/** Tests {@link DownloadManager}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic class DownloadManagerDashTest {\n\n  private static final int ASSERT_TRUE_TIMEOUT = 1000;\n\n  private SimpleCache cache;\n  private File tempFolder;\n  private FakeDataSet fakeDataSet;\n  private DownloadManager downloadManager;\n  private StreamKey fakeStreamKey1;\n  private StreamKey fakeStreamKey2;\n  private TestDownloadManagerListener downloadManagerListener;\n  private DefaultDownloadIndex downloadIndex;\n  private DummyMainThread dummyMainThread;\n\n  @Before\n  public void setUp() throws Exception {\n    ShadowLog.stream = System.out;\n    dummyMainThread = new DummyMainThread();\n    Context context = ApplicationProvider.getApplicationContext();\n    tempFolder = Util.createTempDirectory(context, \"ExoPlayerTest\");\n    File cacheFolder = new File(tempFolder, \"cache\");\n    cacheFolder.mkdir();\n    cache = new SimpleCache(cacheFolder, new NoOpCacheEvictor());\n    MockitoAnnotations.initMocks(this);\n    fakeDataSet =\n        new FakeDataSet()\n            .setData(TEST_MPD_URI, TEST_MPD)\n            .setRandomData(\"audio_init_data\", 10)\n            .setRandomData(\"audio_segment_1\", 4)\n            .setRandomData(\"audio_segment_2\", 5)\n            .setRandomData(\"audio_segment_3\", 6)\n            .setRandomData(\"text_segment_1\", 1)\n            .setRandomData(\"text_segment_2\", 2)\n            .setRandomData(\"text_segment_3\", 3);\n\n    fakeStreamKey1 = new StreamKey(0, 0, 0);\n    fakeStreamKey2 = new StreamKey(0, 1, 0);\n    downloadIndex = new DefaultDownloadIndex(TestUtil.getTestDatabaseProvider());\n    createDownloadManager();\n  }\n\n  @After\n  public void tearDown() {\n    runOnMainThread(() -> downloadManager.release());\n    Util.recursiveDelete(tempFolder);\n    dummyMainThread.release();\n  }\n\n  // Disabled due to flakiness.\n  @Ignore\n  @Test\n  public void testSaveAndLoadActionFile() throws Throwable {\n    // Configure fakeDataSet to block until interrupted when TEST_MPD is read.\n    fakeDataSet\n        .newData(TEST_MPD_URI)\n        .appendReadAction(\n            () -> {\n              try {\n                // Wait until interrupted.\n                while (true) {\n                  Thread.sleep(100000);\n                }\n              } catch (InterruptedException ignored) {\n                Thread.currentThread().interrupt();\n              }\n            })\n        .appendReadData(TEST_MPD)\n        .endData();\n\n    // Run DM accessing code on UI/main thread as it should be. Also not to block handling of loaded\n    // actions.\n    runOnMainThread(\n        () -> {\n          // Setup an Action and immediately release the DM.\n          DownloadRequest request = getDownloadRequest(fakeStreamKey1, fakeStreamKey2);\n          downloadManager.addDownload(request);\n          downloadManager.release();\n        });\n\n    assertCacheEmpty(cache);\n\n    // Revert fakeDataSet to normal.\n    fakeDataSet.setData(TEST_MPD_URI, TEST_MPD);\n\n    dummyMainThread.runOnMainThread(this::createDownloadManager);\n\n    // Block on the test thread.\n    blockUntilTasksCompleteAndThrowAnyDownloadError();\n    assertCachedData(cache, fakeDataSet);\n  }\n\n  @Test\n  public void testHandleDownloadRequest() throws Throwable {\n    handleDownloadRequest(fakeStreamKey1, fakeStreamKey2);\n    blockUntilTasksCompleteAndThrowAnyDownloadError();\n    assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor(\"audio_init_data\"));\n  }\n\n  @Test\n  public void testHandleMultipleDownloadRequest() throws Throwable {\n    handleDownloadRequest(fakeStreamKey1);\n    handleDownloadRequest(fakeStreamKey2);\n    blockUntilTasksCompleteAndThrowAnyDownloadError();\n    assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor(\"audio_init_data\"));\n  }\n\n  @Test\n  public void testHandleInterferingDownloadRequest() throws Throwable {\n    fakeDataSet\n        .newData(\"audio_segment_2\")\n        .appendReadAction(() -> handleDownloadRequest(fakeStreamKey2))\n        .appendReadData(TestUtil.buildTestData(5))\n        .endData();\n\n    handleDownloadRequest(fakeStreamKey1);\n\n    blockUntilTasksCompleteAndThrowAnyDownloadError();\n    assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor(\"audio_init_data\"));\n  }\n\n  @Test\n  public void testHandleRemoveAction() throws Throwable {\n    handleDownloadRequest(fakeStreamKey1);\n\n    blockUntilTasksCompleteAndThrowAnyDownloadError();\n\n    handleRemoveAction();\n\n    blockUntilTasksCompleteAndThrowAnyDownloadError();\n\n    assertCacheEmpty(cache);\n  }\n\n  // Disabled due to flakiness.\n  @Test\n  public void testHandleRemoveActionBeforeDownloadFinish() throws Throwable {\n    handleDownloadRequest(fakeStreamKey1);\n    handleRemoveAction();\n\n    blockUntilTasksCompleteAndThrowAnyDownloadError();\n\n    assertCacheEmpty(cache);\n  }\n\n  // Disabled due to flakiness [Internal: b/122290449].\n  @Test\n  public void testHandleInterferingRemoveAction() throws Throwable {\n    final ConditionVariable downloadInProgressCondition = new ConditionVariable();\n    fakeDataSet\n        .newData(\"audio_segment_2\")\n        .appendReadAction(downloadInProgressCondition::open)\n        .appendReadData(TestUtil.buildTestData(5))\n        .endData();\n\n    handleDownloadRequest(fakeStreamKey1);\n\n    assertThat(downloadInProgressCondition.block(ASSERT_TRUE_TIMEOUT)).isTrue();\n\n    handleRemoveAction();\n\n    blockUntilTasksCompleteAndThrowAnyDownloadError();\n\n    assertCacheEmpty(cache);\n  }\n\n  private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable {\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n  }\n\n  private void handleDownloadRequest(StreamKey... keys) {\n    DownloadRequest request = getDownloadRequest(keys);\n    runOnMainThread(() -> downloadManager.addDownload(request));\n  }\n\n  private DownloadRequest getDownloadRequest(StreamKey... keys) {\n    ArrayList<StreamKey> keysList = new ArrayList<>();\n    Collections.addAll(keysList, keys);\n    return new DownloadRequest(\n        TEST_ID,\n        DownloadRequest.TYPE_DASH,\n        TEST_MPD_URI,\n        keysList,\n        /* customCacheKey= */ null,\n        null);\n  }\n\n  private void handleRemoveAction() {\n    runOnMainThread(() -> downloadManager.removeDownload(TEST_ID));\n  }\n\n  private void createDownloadManager() {\n    runOnMainThread(\n        () -> {\n          Factory fakeDataSourceFactory = new FakeDataSource.Factory().setFakeDataSet(fakeDataSet);\n          downloadManager =\n              new DownloadManager(\n                  ApplicationProvider.getApplicationContext(),\n                  downloadIndex,\n                  new DefaultDownloaderFactory(\n                      new DownloaderConstructorHelper(cache, fakeDataSourceFactory)));\n          downloadManager.setRequirements(new Requirements(0));\n\n          downloadManagerListener =\n              new TestDownloadManagerListener(\n                  downloadManager, dummyMainThread, /* timeout= */ 3000);\n          downloadManager.resumeDownloads();\n        });\n  }\n\n  private void runOnMainThread(TestRunnable r) {\n    dummyMainThread.runTestOnMainThread(r);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.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 static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_ID;\nimport static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD;\nimport static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_URI;\nimport static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;\nimport static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;\n\nimport android.app.Notification;\nimport android.content.Context;\nimport android.content.Intent;\nimport androidx.annotation.Nullable;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.offline.DefaultDownloadIndex;\nimport com.google.android.exoplayer2.offline.DefaultDownloaderFactory;\nimport com.google.android.exoplayer2.offline.Download;\nimport com.google.android.exoplayer2.offline.DownloadManager;\nimport com.google.android.exoplayer2.offline.DownloadRequest;\nimport com.google.android.exoplayer2.offline.DownloadService;\nimport com.google.android.exoplayer2.offline.DownloaderConstructorHelper;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.scheduler.Scheduler;\nimport com.google.android.exoplayer2.testutil.DummyMainThread;\nimport com.google.android.exoplayer2.testutil.FakeDataSet;\nimport com.google.android.exoplayer2.testutil.FakeDataSource;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\nimport com.google.android.exoplayer2.testutil.TestDownloadManagerListener;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;\nimport com.google.android.exoplayer2.upstream.cache.SimpleCache;\nimport com.google.android.exoplayer2.util.ConditionVariable;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Ignore;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/** Unit tests for {@link DownloadService}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic class DownloadServiceDashTest {\n\n  private SimpleCache cache;\n  private File tempFolder;\n  private FakeDataSet fakeDataSet;\n  private StreamKey fakeStreamKey1;\n  private StreamKey fakeStreamKey2;\n  private Context context;\n  private DownloadService dashDownloadService;\n  private ConditionVariable pauseDownloadCondition;\n  private TestDownloadManagerListener downloadManagerListener;\n  private DummyMainThread dummyMainThread;\n\n  @Before\n  public void setUp() throws IOException {\n    dummyMainThread = new DummyMainThread();\n    context = ApplicationProvider.getApplicationContext();\n    tempFolder = Util.createTempDirectory(context, \"ExoPlayerTest\");\n    cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());\n\n    Runnable pauseAction =\n        () -> {\n          if (pauseDownloadCondition != null) {\n            try {\n              pauseDownloadCondition.block();\n            } catch (InterruptedException e) {\n              Thread.currentThread().interrupt();\n            }\n          }\n        };\n    fakeDataSet =\n        new FakeDataSet()\n            .setData(TEST_MPD_URI, TEST_MPD)\n            .newData(\"audio_init_data\")\n            .appendReadAction(pauseAction)\n            .appendReadData(TestUtil.buildTestData(10))\n            .endData()\n            .setRandomData(\"audio_segment_1\", 4)\n            .setRandomData(\"audio_segment_2\", 5)\n            .setRandomData(\"audio_segment_3\", 6)\n            .setRandomData(\"text_segment_1\", 1)\n            .setRandomData(\"text_segment_2\", 2)\n            .setRandomData(\"text_segment_3\", 3);\n    final DataSource.Factory fakeDataSourceFactory =\n        new FakeDataSource.Factory().setFakeDataSet(fakeDataSet);\n    fakeStreamKey1 = new StreamKey(0, 0, 0);\n    fakeStreamKey2 = new StreamKey(0, 1, 0);\n\n    dummyMainThread.runTestOnMainThread(\n        () -> {\n          DefaultDownloadIndex downloadIndex =\n              new DefaultDownloadIndex(TestUtil.getTestDatabaseProvider());\n          final DownloadManager dashDownloadManager =\n              new DownloadManager(\n                  ApplicationProvider.getApplicationContext(),\n                  downloadIndex,\n                  new DefaultDownloaderFactory(\n                      new DownloaderConstructorHelper(cache, fakeDataSourceFactory)));\n          downloadManagerListener =\n              new TestDownloadManagerListener(dashDownloadManager, dummyMainThread);\n          dashDownloadManager.resumeDownloads();\n\n          dashDownloadService =\n              new DownloadService(DownloadService.FOREGROUND_NOTIFICATION_ID_NONE) {\n                @Override\n                protected DownloadManager getDownloadManager() {\n                  return dashDownloadManager;\n                }\n\n                @Nullable\n                @Override\n                protected Scheduler getScheduler() {\n                  return null;\n                }\n\n                @Override\n                protected Notification getForegroundNotification(List<Download> downloads) {\n                  throw new UnsupportedOperationException();\n                }\n              };\n          dashDownloadService.onCreate();\n        });\n  }\n\n  @After\n  public void tearDown() {\n    dummyMainThread.runOnMainThread(() -> dashDownloadService.onDestroy());\n    Util.recursiveDelete(tempFolder);\n    dummyMainThread.release();\n  }\n\n  @Ignore // b/78877092\n  @Test\n  public void testMultipleDownloadRequest() throws Throwable {\n    downloadKeys(fakeStreamKey1);\n    downloadKeys(fakeStreamKey2);\n\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n\n    assertCachedData(cache, fakeDataSet);\n  }\n\n  @Ignore // b/78877092\n  @Test\n  public void testRemoveAction() throws Throwable {\n    downloadKeys(fakeStreamKey1, fakeStreamKey2);\n\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n\n    removeAll();\n\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n\n    assertCacheEmpty(cache);\n  }\n\n  @Ignore // b/78877092\n  @Test\n  public void testRemoveBeforeDownloadComplete() throws Throwable {\n    pauseDownloadCondition = new ConditionVariable();\n    downloadKeys(fakeStreamKey1, fakeStreamKey2);\n\n    removeAll();\n\n    downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();\n\n    assertCacheEmpty(cache);\n  }\n\n  private void removeAll() {\n    dummyMainThread.runOnMainThread(\n        () -> {\n          Intent startIntent =\n              DownloadService.buildRemoveDownloadIntent(\n                  context, DownloadService.class, TEST_ID, /* foreground= */ false);\n          dashDownloadService.onStartCommand(startIntent, 0, 0);\n        });\n  }\n\n  private void downloadKeys(StreamKey... keys) {\n    ArrayList<StreamKey> keysList = new ArrayList<>();\n    Collections.addAll(keysList, keys);\n    DownloadRequest action =\n        new DownloadRequest(\n            TEST_ID,\n            DownloadRequest.TYPE_DASH,\n            TEST_MPD_URI,\n            keysList,\n            /* customCacheKey= */ null,\n            null);\n    dummyMainThread.runOnMainThread(\n        () -> {\n          Intent startIntent =\n              DownloadService.buildAddDownloadIntent(\n                  context, DownloadService.class, action, /* foreground= */ false);\n          dashDownloadService.onStartCommand(startIntent, 0, 0);\n        });\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/README.md",
    "content": "# ExoPlayer HLS library module #\n\nProvides support for HTTP Live Streaming (HLS) content. To play HLS content,\ninstantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`.\n\n## Links ##\n\n* [Developer Guide][].\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.hls.*`\n  belong to this module.\n\n[Developer Guide]: https://exoplayer.dev/hls.html\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/build.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.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    buildTypes {\n        // Re-enable test coverage when the following issue is fixed:\n        // https://issuetracker.google.com/issues/37019591\n        // debug {\n        //    testCoverageEnabled = true\n        // }\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    implementation 'androidx.annotation:annotation:1.1.0'\n    compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion\n    implementation project(modulePrefix + 'library-core')\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n}\n\next {\n    javadocTitle = 'HLS module'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'exoplayer-hls'\n    releaseDescription = 'The ExoPlayer library HLS module.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n\n<manifest package=\"com.google.android.exoplayer2.source.hls\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/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  private @Nullable 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  public final @Nullable 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-amzn-2.10.6/library/hls/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-amzn-2.10.6/library/hls/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\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      Extractor previousExtractor,\n      Uri uri,\n      Format format,\n      List<Format> muxedCaptionFormats,\n      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      List<Format> muxedCaptionFormats,\n      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      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      DrmInitData drmInitData,\n      @Nullable List<Format> muxedCaptionFormats) {\n    boolean isVariant = false;\n    for (int i = 0; i < format.metadata.length(); i++) {\n      Metadata.Entry entry = format.metadata.get(i);\n      if (entry instanceof HlsTrackMetadataEntry) {\n        isVariant = !((HlsTrackMetadataEntry) entry).variantInfos.isEmpty();\n        break;\n      }\n    }\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= */ isVariant ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0,\n        timestampAdjuster,\n        /* sideloadedTrack= */ null,\n        drmInitData,\n        muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList());\n  }\n\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-amzn-2.10.6/library/hls/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.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Source of Hls (possibly adaptive) chunks.\n */\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    /**\n     * The chunk to be loaded next.\n     */\n    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    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  private final List<Format> muxedCaptionFormats;\n  private final FullSegmentEncryptionKeyCache keyCache;\n\n  private boolean isTimestampMaster;\n  private byte[] scratchSpace;\n  private IOException fatalError;\n  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      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();\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 out A holder to populate.\n   */\n  public void getNextChunk(\n      long playbackPositionUs, long loadPositionUs, List<HlsMediaChunk> queue, 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    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        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    if (segmentIndexInPlaylist >= mediaPlaylist.segments.size()) {\n      if (mediaPlaylist.hasEndTag) {\n        out.endOfStream = true;\n      } else /* Live */ {\n        out.playlistUrl = selectedPlaylistUrl;\n        seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl);\n        expectedPlaylistUrl = selectedPlaylistUrl;\n      }\n      return;\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(encryptionKeyChunk.dataSpec.uri, 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      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    if (keyCache.containsKey(keyUri)) {\n      // The key is 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, keyCache.remove(keyUri));\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    public Object getSelectionData() {\n      return null;\n    }\n\n  }\n\n  private static final class EncryptionKeyChunk extends DataChunk {\n\n    private byte[] result;\n\n    public EncryptionKeyChunk(\n        DataSource dataSource,\n        DataSpec dataSpec,\n        Format trackFormat,\n        int trackSelectionReason,\n        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    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  /**\n   * LRU cache that holds up to {@link #KEY_CACHE_SIZE} full-segment-encryption keys. Which each\n   * addition, once the cache's size exceeds {@link #KEY_CACHE_SIZE}, the oldest item (according to\n   * insertion order) is removed.\n   */\n  private static final class FullSegmentEncryptionKeyCache extends LinkedHashMap<Uri, byte[]> {\n\n    public FullSegmentEncryptionKeyCache() {\n      super(\n          /* initialCapacity= */ KEY_CACHE_SIZE * 2, /* loadFactor= */ 1, /* accessOrder= */ false);\n    }\n\n    @Override\n    public byte[] get(Object keyUri) {\n      if (keyUri == null) {\n        return null;\n      }\n      return super.get(keyUri);\n    }\n\n    @Override\n    public byte[] put(Uri keyUri, byte[] key) {\n      return super.put(keyUri, Assertions.checkNotNull(key));\n    }\n\n    @Override\n    protected boolean removeEldestEntry(Map.Entry<Uri, byte[]> entry) {\n      return size() > KEY_CACHE_SIZE;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/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-amzn-2.10.6/library/hls/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 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      Extractor previousExtractor,\n      Uri uri,\n      Format format,\n      List<Format> muxedCaptionFormats,\n      DrmInitData drmInitData,\n      TimestampAdjuster timestampAdjuster,\n      Map<String, List<String>> responseHeaders,\n      ExtractorInput sniffingExtractorInput)\n      throws InterruptedException, IOException;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/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-amzn-2.10.6/library/hls/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.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.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;\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 ? getEncryptionIvArray(mediaSegment.encryptionIV) : 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 ? getEncryptionIvArray(initSegment.encryptionIV) : 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\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  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  @Nullable private final Extractor previousExtractor;\n  private final Id3Decoder id3Decoder;\n  private final ParsableByteArray scratchId3Data;\n  private final boolean mediaSegmentEncrypted;\n  private final boolean initSegmentEncrypted;\n\n  private Extractor extractor;\n  private boolean isExtractorReusable;\n  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      DataSource initDataSource,\n      @Nullable DataSpec initDataSpec,\n      boolean initSegmentEncrypted,\n      Uri playlistUrl,\n      @Nullable List<Format> muxedCaptionFormats,\n      int trackSelectionReason,\n      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.initDataSource = initDataSource;\n    this.initDataSpec = initDataSpec;\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    initDataLoadRequired = initDataSpec != null;\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    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  private void maybeLoadInitData() throws IOException, InterruptedException {\n    if (!initDataLoadRequired) {\n      return;\n    }\n    feedDataToExtractor(initDataSource, initDataSpec, initSegmentEncrypted);\n    nextLoadPosition = 0;\n    initDataLoadRequired = false;\n  }\n\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  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, /* seekPosition= */ null);\n        }\n      } finally {\n        nextLoadPosition = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);\n      }\n    } finally {\n      Util.closeQuietly(dataSource);\n    }\n  }\n\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  private static DataSource buildDataSource(DataSource dataSource, byte[] fullSegmentEncryptionKey,\n      byte[] encryptionIv) {\n    if (fullSegmentEncryptionKey != null) {\n      return new Aes128DataSource(dataSource, fullSegmentEncryptionKey, encryptionIv);\n    }\n    return dataSource;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\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.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.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.IdentityHashMap;\nimport java.util.List;\nimport java.util.Map;\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  private final @Nullable TransferListener mediaTransferListener;\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 @HlsMetadataType int metadataType;\n  private final boolean useSessionKeys;\n\n  private @Nullable Callback callback;\n  private int pendingPrepareCount;\n  private 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 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      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      EventDispatcher eventDispatcher,\n      Allocator allocator,\n      CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\n      boolean allowChunklessPreparation,\n      @HlsMetadataType int metadataType,\n      boolean useSessionKeys) {\n    this.extractorFactory = extractorFactory;\n    this.playlistTracker = playlistTracker;\n    this.dataSourceFactory = dataSourceFactory;\n    this.mediaTransferListener = mediaTransferListener;\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    return 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(TrackSelection[] selections, boolean[] mayRetainStreamFlags,\n      SampleStream[] streams, boolean[] streamResetFlags, 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    SampleStream[] childStreams = new SampleStream[selections.length];\n    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        if (selectionChildIndices[j] == i) {\n          // Assert that the child provided a stream for the selection.\n          Assertions.checkState(childStreams[j] != null);\n          newStreams[j] = childStreams[j];\n          wrapperEnabled = true;\n          streamWrapperIndices.put(childStreams[j], 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 (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 = Arrays.copyOf(newEnabledSampleStreamWrappers,\n        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 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 TrackGroupArray(new TrackGroup(subtitleRendition.format)), 0, TrackGroupArray.EMPTY);\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          new TrackGroupArray(muxedTrackGroups.toArray(new TrackGroup[0])),\n          0,\n          new TrackGroupArray(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(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 TrackGroupArray(new TrackGroup(renditionFormats)), 0, TrackGroupArray.EMPTY);\n      }\n    }\n  }\n\n  private HlsSampleStreamWrapper buildSampleStreamWrapper(\n      int trackType,\n      Uri[] playlistUrls,\n      Format[] playlistFormats,\n      Format muxedAudioFormat,\n      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        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, 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-amzn-2.10.6/library/hls/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 android.net.Uri;\nimport android.os.Handler;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\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.SequenceableLoader;\nimport com.google.android.exoplayer2.source.SinglePeriodTimeline;\nimport com.google.android.exoplayer2.source.ads.AdsMediaSource;\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.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  /** Factory for {@link HlsMediaSource}s. */\n  public static final class Factory implements AdsMediaSource.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 LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n    private boolean allowChunklessPreparation;\n    @HlsMetadataType 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      loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();\n      compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();\n      metadataType = HlsMetadataType.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(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 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 a list of {@link StreamKey stream keys} by which the playlists are filtered.\n     *\n     * @param streamKeys A list of {@link StreamKey stream keys}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setStreamKeys(List<StreamKey> streamKeys) {\n      Assertions.checkState(!isCreateCalled);\n      this.streamKeys = streamKeys;\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     * HlsMetadataType#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 HlsMetadataType#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 HlsMetadataType#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(@HlsMetadataType 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     * 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          loadErrorHandlingPolicy,\n          playlistTrackerFactory.createTracker(\n              hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),\n          allowChunklessPreparation,\n          metadataType,\n          useSessionKeys,\n          tag);\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    @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 LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final boolean allowChunklessPreparation;\n  private final @HlsMetadataType int metadataType;\n  private final boolean useSessionKeys;\n  private final HlsPlaylistTracker playlistTracker;\n  private final @Nullable Object tag;\n\n  private @Nullable TransferListener mediaTransferListener;\n\n  private HlsMediaSource(\n      Uri manifestUri,\n      HlsDataSourceFactory dataSourceFactory,\n      HlsExtractorFactory extractorFactory,\n      CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      HlsPlaylistTracker playlistTracker,\n      boolean allowChunklessPreparation,\n      @HlsMetadataType 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.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  public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    this.mediaTransferListener = mediaTransferListener;\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        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  public void releaseSourceInternal() {\n    playlistTracker.stop();\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    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 = segments.isEmpty() ? 0\n            : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs;\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              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              tag);\n    }\n    refreshSourceInfo(timeline, new HlsManifest(playlistTracker.getMasterPlaylist(), playlist));\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.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.source.hls;\n\nimport static java.lang.annotation.RetentionPolicy.SOURCE;\n\nimport androidx.annotation.IntDef;\nimport java.lang.annotation.Retention;\n\n/**\n * The types of metadata that can be extracted from HLS streams.\n *\n * <p>See {@link HlsMediaSource.Factory#setMetadataType(int)}.\n */\n@Retention(SOURCE)\n@IntDef({HlsMetadataType.ID3, HlsMetadataType.EMSG})\npublic @interface HlsMetadataType {\n  int ID3 = 1;\n  int EMSG = 3;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/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    }\n    sampleStreamWrapper.maybeThrowError();\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-amzn-2.10.6/library/hls/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.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.drm.DrmInitData;\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.qual.MonotonicNonNull;\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  private final Format muxedAudioFormat;\n  private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\n  private final Loader loader;\n  private final EventDispatcher eventDispatcher;\n  private final @HlsMetadataType int metadataType;\n  private final HlsChunkSource.HlsChunkHolder nextChunkHolder;\n  private final ArrayList<HlsMediaChunk> mediaChunks;\n  private final List<HlsMediaChunk> readOnlyMediaChunks;\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  private TrackOutput emsgUnwrappingTrackOutput;\n  private int primarySampleQueueType;\n  private int primarySampleQueueIndex;\n  private boolean sampleQueuesBuilt;\n  private boolean prepared;\n  private int enabledTrackGroupCount;\n  private Format upstreamTrackFormat;\n  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  private TrackGroupArray trackGroups;\n  private TrackGroupArray optionalTrackGroups;\n  // Indexed by track group.\n  private int[] 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 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      Format muxedAudioFormat,\n      LoadErrorHandlingPolicy loadErrorHandlingPolicy,\n      EventDispatcher eventDispatcher,\n      @HlsMetadataType 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.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    maybeFinishPrepareRunnable = this::maybeFinishPrepare;\n    onTracksEndedRunnable = this::onTracksEnded;\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 TrackGroupArray} to expose.\n   * @param primaryTrackGroupIndex The index of the adaptive track group.\n   * @param optionalTrackGroups A subset of {@code trackGroups} that should not trigger a failure if\n   *     not found in the media playlist's segments.\n   */\n  public void prepareWithMasterPlaylistInfo(\n      TrackGroupArray trackGroups,\n      int primaryTrackGroupIndex,\n      TrackGroupArray optionalTrackGroups) {\n    prepared = true;\n    this.trackGroups = trackGroups;\n    this.optionalTrackGroups = optionalTrackGroups;\n    this.primaryTrackGroupIndex = primaryTrackGroupIndex;\n    handler.post(callback::onPrepared);\n  }\n\n  public void maybeThrowPrepareError() throws IOException {\n    maybeThrowError();\n  }\n\n  public TrackGroupArray getTrackGroups() {\n    return trackGroups;\n  }\n\n  public int getPrimaryTrackGroupIndex() {\n    return primaryTrackGroupIndex;\n  }\n\n  public int bindSampleQueueToSampleStream(int trackGroupIndex) {\n    int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];\n    if (sampleQueueIndex == C.INDEX_UNSET) {\n      return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET\n          ? SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL\n          : SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_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    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(TrackSelection[] selections, boolean[] mayRetainStreamFlags,\n      SampleStream[] streams, boolean[] streamResetFlags, long positionUs, boolean forceReset) {\n    Assertions.checkState(prepared);\n    int oldEnabledTrackGroupCount = enabledTrackGroupCount;\n    // Deselect old tracks.\n    for (int i = 0; i < selections.length; i++) {\n      if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {\n        enabledTrackGroupCount--;\n        ((HlsSampleStream) streams[i]).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        }\n        // If there's still a chance of avoiding a seek, try and seek within the sample queue.\n        if (sampleQueuesBuilt && !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 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\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.discardToEnd();\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    resetSampleQueues();\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 loadingFinished || (!isPendingReset() && sampleQueues[sampleQueueIndex].hasNextSample());\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 = 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                : upstreamTrackFormat;\n        format = format.copyWithManifestFormatInfo(trackFormat);\n      }\n      if (format.drmInitData != null) {\n        DrmInitData drmInitData = overridingDrmInitData.get(format.drmInitData.schemeType);\n        if (drmInitData != null) {\n          format = format.copyWithDrmInitData(drmInitData);\n        }\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(positionUs, loadPositionUs, chunkQueue, 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 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 = new PrivTimestampStrippingSampleQueue(allocator);\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 = Arrays.copyOf(sampleQueues, trackCount + 1);\n    sampleQueues[trackCount] = 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(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      prepared = true;\n      callback.onPrepared();\n    }\n  }\n\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  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 = new TrackGroupArray(trackGroups);\n    Assertions.checkState(optionalTrackGroups == null);\n    optionalTrackGroups = TrackGroupArray.EMPTY;\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  /**\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      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        playlistFormat.frameRate, // MOD: add fps to hls format info\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 PrivTimestampStrippingSampleQueue extends SampleQueue {\n\n    public PrivTimestampStrippingSampleQueue(Allocator allocator) {\n      super(allocator);\n    }\n\n    @Override\n    public void format(Format format) {\n      super.format(format.copyWithMetadata(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(TrackOutput delegate, @HlsMetadataType int metadataType) {\n      this.emsgDecoder = new EventMessageDecoder();\n      this.delegate = delegate;\n      switch (metadataType) {\n        case HlsMetadataType.ID3:\n          delegateFormat = ID3_FORMAT;\n          break;\n        case HlsMetadataType.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.checkState(format != null);\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-amzn-2.10.6/library/hls/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\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 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-amzn-2.10.6/library/hls/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-amzn-2.10.6/library/hls/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-amzn-2.10.6/library/hls/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 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.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;\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  private final String language;\n  private final TimestampAdjuster timestampAdjuster;\n  private final ParsableByteArray sampleDataWrapper;\n\n  private ExtractorOutput output;\n\n  private byte[] sampleData;\n  private int sampleSize;\n\n  public WebvttExtractor(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    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  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    String line;\n    while (!TextUtils.isEmpty(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  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-amzn-2.10.6/library/hls/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-amzn-2.10.6/library/hls/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-amzn-2.10.6/library/hls/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  public @Nullable HlsMasterPlaylist getMasterPlaylist() {\n    return masterPlaylist;\n  }\n\n  @Override\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    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    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-amzn-2.10.6/library/hls/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-amzn-2.10.6/library/hls/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  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  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      Format muxedAudioFormat,\n      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= */ null,\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-amzn-2.10.6/library/hls/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.NonNull;\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        String fullSegmentEncryptionKeyUri,\n        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(@NonNull 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-amzn-2.10.6/library/hls/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-amzn-2.10.6/library/hls/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 androidx.annotation.Nullable;\nimport android.text.TextUtils;\nimport android.util.Base64;\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.Queue;\nimport java.util.TreeMap;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\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        String averageBandwidthString =\n            parseOptionalStringAttr(line, REGEX_AVERAGE_BANDWIDTH, variableDefinitions);\n        if (averageBandwidthString != null) {\n          // If available, the average bandwidth attribute is used as the variant's bitrate.\n          bitrate = Integer.parseInt(averageBandwidthString);\n        }\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        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, /* name= */ null, 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      // MOD: Don't add 'Default' to format id\n      String formatId = groupId;\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          int channelCount = parseChannelsAttribute(line, variableDefinitions);\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  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  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  private static int parseChannelsAttribute(String line, Map<String, String> variableDefinitions) {\n    String channelsString = parseOptionalStringAttr(line, REGEX_CHANNELS, variableDefinitions);\n    return channelsString != null\n        ? Integer.parseInt(Util.splitAtFirst(channelsString, \"/\")[0])\n        : Format.NO_VALUE;\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 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    private String next;\n\n    public LineIterator(Queue<String> extraLines, BufferedReader reader) {\n      this.extraLines = extraLines;\n      this.reader = reader;\n    }\n\n    public boolean hasNext() throws IOException {\n      if (next != null) {\n        return true;\n      }\n      if (!extraLines.isEmpty()) {\n        next = 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    public String next() throws IOException {\n      String result = null;\n      if (hasNext()) {\n        result = next;\n        next = null;\n      }\n      return result;\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/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-amzn-2.10.6/library/hls/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-amzn-2.10.6/library/hls/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n\n<manifest package=\"com.google.android.exoplayer2.source.hls.test\" />\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/Aes128DataSourceTest.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;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\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.security.NoSuchAlgorithmException;\nimport javax.crypto.Cipher;\nimport javax.crypto.NoSuchPaddingException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link Aes128DataSource}. */\n@RunWith(AndroidJUnit4.class)\npublic class Aes128DataSourceTest {\n\n  @Test\n  public void test_OpenCallsUpstreamOpen_CloseCallsUpstreamClose() throws IOException {\n    UpstreamDataSource upstream = new UpstreamDataSource();\n    Aes128DataSource testInstance = new TestAes123DataSource(upstream, new byte[16], new byte[16]);\n    assertThat(upstream.opened).isFalse();\n\n    Uri uri = Uri.parse(\"http.abc.com/def\");\n    testInstance.open(new DataSpec(uri));\n    assertThat(upstream.opened).isTrue();\n\n    testInstance.close();\n    assertThat(upstream.opened).isFalse();\n  }\n\n  @Test\n  public void test_OpenCallsUpstreamThrowingOpen_CloseCallsUpstreamClose() throws IOException {\n    UpstreamDataSource upstream =\n        new UpstreamDataSource() {\n          @Override\n          public long open(DataSpec dataSpec) throws IOException {\n            throw new IOException();\n          }\n        };\n    Aes128DataSource testInstance = new TestAes123DataSource(upstream, new byte[16], new byte[16]);\n    assertThat(upstream.opened).isFalse();\n\n    Uri uri = Uri.parse(\"http.abc.com/def\");\n    try {\n      testInstance.open(new DataSpec(uri));\n    } catch (IOException e) {\n      // Expected.\n    }\n    assertThat(upstream.opened).isFalse();\n    assertThat(upstream.closedCalled).isFalse();\n\n    // Even though the upstream open call failed, close should still call close on the upstream as\n    // per the contract of DataSource.\n    testInstance.close();\n    assertThat(upstream.closedCalled).isTrue();\n  }\n\n  private static class TestAes123DataSource extends Aes128DataSource {\n\n    public TestAes123DataSource(DataSource upstream, byte[] encryptionKey, byte[] encryptionIv) {\n      super(upstream, encryptionKey, encryptionIv);\n    }\n\n    @Override\n    protected Cipher getCipherInstance() throws NoSuchPaddingException, NoSuchAlgorithmException {\n      try {\n        return super.getCipherInstance();\n      } catch (NoSuchAlgorithmException e) {\n        // Some host machines may not provide an algorithm for \"AES/CBC/PKCS7Padding\", however on\n        // such machines it's possible to get a functionally identical algorithm by requesting\n        // \"AES/CBC/PKCS5Padding\".\n        return Cipher.getInstance(\"AES/CBC/PKCS5Padding\");\n      }\n    }\n  }\n\n  private static class UpstreamDataSource implements DataSource {\n\n    public boolean opened;\n    public boolean closedCalled;\n\n    @Override\n    public void addTransferListener(TransferListener transferListener) {}\n\n    @Override\n    public long open(DataSpec dataSpec) throws IOException {\n      opened = true;\n      return C.LENGTH_UNSET;\n    }\n\n    @Override\n    public int read(byte[] buffer, int offset, int readLength) {\n      return C.RESULT_END_OF_INPUT;\n    }\n\n    @Override\n    public Uri getUri() {\n      return null;\n    }\n\n    @Override\n    public void close() {\n      opened = false;\n      closedCalled = true;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.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;\n\nimport static org.mockito.Matchers.anyInt;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\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.HlsPlaylist;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;\nimport com.google.android.exoplayer2.testutil.MediaPeriodAsserts;\nimport com.google.android.exoplayer2.testutil.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\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.MimeTypes;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/** Unit test for {@link HlsMediaPeriod}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic final class HlsMediaPeriodTest {\n\n  @Test\n  public void getSteamKeys_isCompatibleWithHlsMasterPlaylistFilter() {\n    HlsMasterPlaylist testMasterPlaylist =\n        createMasterPlaylist(\n            /* variants= */ Arrays.asList(\n                createAudioOnlyVariant(/* bitrate= */ 10000),\n                createMuxedVideoAudioVariant(/* bitrate= */ 200000),\n                createAudioOnlyVariant(/* bitrate= */ 300000),\n                createMuxedVideoAudioVariant(/* bitrate= */ 400000),\n                createMuxedVideoAudioVariant(/* bitrate= */ 600000)),\n            /* audios= */ Arrays.asList(\n                createAudioRendition(/* language= */ \"spa\"),\n                createAudioRendition(/* language= */ \"ger\"),\n                createAudioRendition(/* language= */ \"tur\")),\n            /* subtitles= */ Arrays.asList(\n                createSubtitleRendition(/* language= */ \"spa\"),\n                createSubtitleRendition(/* language= */ \"ger\"),\n                createSubtitleRendition(/* language= */ \"tur\")),\n            /* muxedAudioFormat= */ createAudioFormat(\"eng\"),\n            /* muxedCaptionFormats= */ Arrays.asList(\n                createSubtitleFormat(\"eng\"), createSubtitleFormat(\"gsw\")));\n    FilterableManifestMediaPeriodFactory<HlsPlaylist> mediaPeriodFactory =\n        (playlist, periodIndex) -> {\n          HlsDataSourceFactory mockDataSourceFactory = mock(HlsDataSourceFactory.class);\n          when(mockDataSourceFactory.createDataSource(anyInt())).thenReturn(mock(DataSource.class));\n          HlsPlaylistTracker mockPlaylistTracker = mock(HlsPlaylistTracker.class);\n          when(mockPlaylistTracker.getMasterPlaylist()).thenReturn((HlsMasterPlaylist) playlist);\n          return new HlsMediaPeriod(\n              mock(HlsExtractorFactory.class),\n              mockPlaylistTracker,\n              mockDataSourceFactory,\n              mock(TransferListener.class),\n              mock(LoadErrorHandlingPolicy.class),\n              new EventDispatcher()\n                  .withParameters(\n                      /* windowIndex= */ 0,\n                      /* mediaPeriodId= */ new MediaPeriodId(/* periodUid= */ new Object()),\n                      /* mediaTimeOffsetMs= */ 0),\n              mock(Allocator.class),\n              mock(CompositeSequenceableLoaderFactory.class),\n              /* allowChunklessPreparation =*/ true,\n              HlsMetadataType.ID3,\n              /* useSessionKeys= */ false);\n        };\n\n    MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(\n        mediaPeriodFactory, testMasterPlaylist);\n  }\n\n  private static HlsMasterPlaylist createMasterPlaylist(\n      List<Variant> variants,\n      List<Rendition> audios,\n      List<Rendition> subtitles,\n      Format muxedAudioFormat,\n      List<Format> muxedCaptionFormats) {\n    return new HlsMasterPlaylist(\n        \"http://baseUri\",\n        /* tags= */ Collections.emptyList(),\n        variants,\n        /* videos= */ Collections.emptyList(),\n        audios,\n        subtitles,\n        /* closedCaptions= */ Collections.emptyList(),\n        muxedAudioFormat,\n        muxedCaptionFormats,\n        /* hasIndependentSegments= */ true,\n        /* variableDefinitions= */ Collections.emptyMap(),\n        /* sessionKeyDrmInitData= */ Collections.emptyList());\n  }\n\n  private static Variant createMuxedVideoAudioVariant(int bitrate) {\n    return createVariant(\n        Format.createVideoContainerFormat(\n            /* id= */ null,\n            /* label= */ null,\n            /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,\n            /* sampleMimeType= */ null,\n            /* codecs= */ \"avc1.100.41,mp4a.40.2\",\n            /* metadata= */ null,\n            bitrate,\n            /* width= */ Format.NO_VALUE,\n            /* height= */ Format.NO_VALUE,\n            /* frameRate= */ Format.NO_VALUE,\n            /* initializationData= */ null,\n            /* selectionFlags= */ 0,\n            /* roleFlags= */ 0));\n  }\n\n  private static Variant createAudioOnlyVariant(int bitrate) {\n    return createVariant(\n        Format.createVideoContainerFormat(\n            /* id= */ null,\n            /* label= */ null,\n            /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,\n            /* sampleMimeType= */ null,\n            /* codecs= */ \"mp4a.40.2\",\n            /* metadata= */ null,\n            bitrate,\n            /* width= */ Format.NO_VALUE,\n            /* height= */ Format.NO_VALUE,\n            /* frameRate= */ Format.NO_VALUE,\n            /* initializationData= */ null,\n            /* selectionFlags= */ 0,\n            /* roleFlags= */ 0));\n  }\n\n  private static Rendition createAudioRendition(String language) {\n    return createRendition(createAudioFormat(language), \"\", \"\");\n  }\n\n  private static Rendition createSubtitleRendition(String language) {\n    return createRendition(createSubtitleFormat(language), \"\", \"\");\n  }\n\n  private static Variant createVariant(Format format) {\n    return new Variant(Uri.parse(\"https://variant\"), format, null, null, null, null);\n  }\n\n  private static Rendition createRendition(Format format, String groupId, String name) {\n    return new Rendition(Uri.parse(\"https://rendition\"), format, groupId, name);\n  }\n\n  private static Format createAudioFormat(String language) {\n    return Format.createAudioContainerFormat(\n        /* id= */ null,\n        /* label= */ null,\n        /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,\n        MimeTypes.getMediaMimeType(\"mp4a.40.2\"),\n        /* codecs= */ \"mp4a.40.2\",\n        /* metadata= */ null,\n        /* bitrate= */ Format.NO_VALUE,\n        /* channelCount= */ Format.NO_VALUE,\n        /* sampleRate= */ Format.NO_VALUE,\n        /* initializationData= */ null,\n        /* selectionFlags= */ 0,\n        /* roleFlags= */ 0,\n        language);\n  }\n\n  private static Format createSubtitleFormat(String language) {\n    return Format.createTextContainerFormat(\n        /* id= */ null,\n        /* label= */ null,\n        /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,\n        /* sampleMimeType= */ MimeTypes.TEXT_VTT,\n        /* codecs= */ null,\n        /* bitrate= */ Format.NO_VALUE,\n        /* selectionFlags= */ 0,\n        /* roleFlags= */ 0,\n        language);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/WebvttExtractorTest.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;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput;\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link WebvttExtractor}. */\n@RunWith(AndroidJUnit4.class)\npublic class WebvttExtractorTest {\n\n  @Test\n  public void sniff_sniffsWebvttHeaderWithTrailingSpace() throws IOException, InterruptedException {\n    byte[] data = new byte[] {'W', 'E', 'B', 'V', 'T', 'T', ' ', '\\t'};\n    assertThat(sniffData(data)).isTrue();\n  }\n\n  @Test\n  public void sniff_discardsByteOrderMark() throws IOException, InterruptedException {\n    byte[] data =\n        new byte[] {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF, 'W', 'E', 'B', 'V', 'T', 'T', '\\n', ' '};\n    assertThat(sniffData(data)).isTrue();\n  }\n\n  @Test\n  public void sniff_failsForIncorrectBom() throws IOException, InterruptedException {\n    byte[] data =\n        new byte[] {(byte) 0xEF, (byte) 0xBB, (byte) 0xBB, 'W', 'E', 'B', 'V', 'T', 'T', '\\n'};\n    assertThat(sniffData(data)).isFalse();\n  }\n\n  @Test\n  public void sniff_failsForIncompleteHeader() throws IOException, InterruptedException {\n    byte[] data = new byte[] {'W', 'E', 'B', 'V', 'T', '\\n'};\n    assertThat(sniffData(data)).isFalse();\n  }\n\n  @Test\n  public void sniff_failsForIncorrectHeader() throws IOException, InterruptedException {\n    byte[] data =\n        new byte[] {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF, 'W', 'e', 'B', 'V', 'T', 'T', '\\n'};\n    assertThat(sniffData(data)).isFalse();\n  }\n\n  private static boolean sniffData(byte[] data) throws IOException, InterruptedException {\n    ExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();\n    try {\n      return new WebvttExtractor(/* language= */ null, new TimestampAdjuster(0)).sniff(input);\n    } catch (EOFException e) {\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/DownloadHelperTest.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.offline;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.offline.DownloadHelper;\nimport com.google.android.exoplayer2.testutil.FakeDataSource;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test to verify creation of a HLS {@link DownloadHelper}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DownloadHelperTest {\n\n  @Test\n  public void staticDownloadHelperForHls_doesNotThrow() {\n    DownloadHelper.forHls(\n        Uri.parse(\"http://uri\"),\n        new FakeDataSource.Factory(),\n        (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]);\n    DownloadHelper.forHls(\n        Uri.parse(\"http://uri\"),\n        new FakeDataSource.Factory(),\n        (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0],\n        /* drmSessionManager= */ null,\n        DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.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 com.google.android.exoplayer2.C;\nimport java.nio.charset.Charset;\n\n/** Data for HLS downloading tests. */\n/* package */ interface HlsDownloadTestData {\n\n  String MASTER_PLAYLIST_URI = \"test.m3u8\";\n  int MASTER_MEDIA_PLAYLIST_1_INDEX = 0;\n  int MASTER_MEDIA_PLAYLIST_2_INDEX = 1;\n  int MASTER_MEDIA_PLAYLIST_3_INDEX = 2;\n  int MASTER_MEDIA_PLAYLIST_0_INDEX = 3;\n\n  String MEDIA_PLAYLIST_0_DIR = \"gear0/\";\n  String MEDIA_PLAYLIST_0_URI = MEDIA_PLAYLIST_0_DIR + \"prog_index.m3u8\";\n  String MEDIA_PLAYLIST_1_DIR = \"gear1/\";\n  String MEDIA_PLAYLIST_1_URI = MEDIA_PLAYLIST_1_DIR + \"prog_index.m3u8\";\n  String MEDIA_PLAYLIST_2_DIR = \"gear2/\";\n  String MEDIA_PLAYLIST_2_URI = MEDIA_PLAYLIST_2_DIR + \"prog_index.m3u8\";\n  String MEDIA_PLAYLIST_3_DIR = \"gear3/\";\n  String MEDIA_PLAYLIST_3_URI = MEDIA_PLAYLIST_3_DIR + \"prog_index.m3u8\";\n\n  byte[] MASTER_PLAYLIST_DATA =\n      (\"#EXTM3U\\n\"\n              + \"#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\\\"mp4a.40.2, avc1.4d4015\\\"\\n\"\n              + MEDIA_PLAYLIST_1_URI\n              + \"\\n\"\n              + \"#EXT-X-STREAM-INF:BANDWIDTH=649879,CODECS=\\\"mp4a.40.2, avc1.4d401e\\\"\\n\"\n              + MEDIA_PLAYLIST_2_URI\n              + \"\\n\"\n              + \"#EXT-X-STREAM-INF:BANDWIDTH=991714,CODECS=\\\"mp4a.40.2, avc1.4d401e\\\"\\n\"\n              + MEDIA_PLAYLIST_3_URI\n              + \"\\n\"\n              + \"#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS=\\\"mp4a.40.2\\\"\\n\"\n              + MEDIA_PLAYLIST_0_URI)\n          .getBytes(Charset.forName(C.UTF8_NAME));\n\n  byte[] MEDIA_PLAYLIST_DATA =\n      (\"#EXTM3U\\n\"\n              + \"#EXT-X-TARGETDURATION:10\\n\"\n              + \"#EXT-X-VERSION:3\\n\"\n              + \"#EXT-X-MEDIA-SEQUENCE:0\\n\"\n              + \"#EXT-X-PLAYLIST-TYPE:VOD\\n\"\n              + \"#EXTINF:9.97667,\\n\"\n              + \"fileSequence0.ts\\n\"\n              + \"#EXTINF:9.97667,\\n\"\n              + \"fileSequence1.ts\\n\"\n              + \"#EXTINF:9.97667,\\n\"\n              + \"fileSequence2.ts\\n\"\n              + \"#EXT-X-ENDLIST\")\n          .getBytes(Charset.forName(C.UTF8_NAME));\n\n  String ENC_MEDIA_PLAYLIST_URI = \"enc_index.m3u8\";\n\n  byte[] ENC_MEDIA_PLAYLIST_DATA =\n      (\"#EXTM3U\\n\"\n              + \"#EXT-X-TARGETDURATION:10\\n\"\n              + \"#EXT-X-VERSION:3\\n\"\n              + \"#EXT-X-MEDIA-SEQUENCE:0\\n\"\n              + \"#EXT-X-PLAYLIST-TYPE:VOD\\n\"\n              + \"#EXT-X-KEY:METHOD=AES-128,URI=\\\"enc.key\\\"\\n\"\n              + \"#EXTINF:9.97667,\\n\"\n              + \"fileSequence0.ts\\n\"\n              + \"#EXTINF:9.97667,\\n\"\n              + \"fileSequence1.ts\\n\"\n              + \"#EXT-X-KEY:METHOD=AES-128,URI=\\\"enc2.key\\\"\\n\"\n              + \"#EXTINF:9.97667,\\n\"\n              + \"fileSequence2.ts\\n\"\n              + \"#EXT-X-ENDLIST\")\n          .getBytes(Charset.forName(C.UTF8_NAME));\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.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 static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_DATA;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_URI;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_MEDIA_PLAYLIST_1_INDEX;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_MEDIA_PLAYLIST_2_INDEX;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_DATA;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_URI;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_DIR;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_URI;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_1_DIR;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_1_URI;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_2_DIR;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_2_URI;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_DIR;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_URI;\nimport static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_DATA;\nimport static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;\nimport static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.offline.DefaultDownloaderFactory;\nimport com.google.android.exoplayer2.offline.DownloadRequest;\nimport com.google.android.exoplayer2.offline.Downloader;\nimport com.google.android.exoplayer2.offline.DownloaderConstructorHelper;\nimport com.google.android.exoplayer2.offline.DownloaderFactory;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;\nimport com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet;\nimport com.google.android.exoplayer2.testutil.FakeDataSet;\nimport com.google.android.exoplayer2.testutil.FakeDataSource.Factory;\nimport com.google.android.exoplayer2.upstream.DummyDataSource;\nimport com.google.android.exoplayer2.upstream.cache.Cache;\nimport com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;\nimport com.google.android.exoplayer2.upstream.cache.SimpleCache;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mockito;\n\n/** Unit tests for {@link HlsDownloader}. */\n@RunWith(AndroidJUnit4.class)\npublic class HlsDownloaderTest {\n\n  private SimpleCache cache;\n  private File tempFolder;\n  private ProgressListener progressListener;\n  private FakeDataSet fakeDataSet;\n\n  @Before\n  public void setUp() throws Exception {\n    tempFolder =\n        Util.createTempDirectory(ApplicationProvider.getApplicationContext(), \"ExoPlayerTest\");\n    cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());\n    progressListener = new ProgressListener();\n    fakeDataSet =\n        new FakeDataSet()\n            .setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA)\n            .setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA)\n            .setRandomData(MEDIA_PLAYLIST_1_DIR + \"fileSequence0.ts\", 10)\n            .setRandomData(MEDIA_PLAYLIST_1_DIR + \"fileSequence1.ts\", 11)\n            .setRandomData(MEDIA_PLAYLIST_1_DIR + \"fileSequence2.ts\", 12)\n            .setData(MEDIA_PLAYLIST_2_URI, MEDIA_PLAYLIST_DATA)\n            .setRandomData(MEDIA_PLAYLIST_2_DIR + \"fileSequence0.ts\", 13)\n            .setRandomData(MEDIA_PLAYLIST_2_DIR + \"fileSequence1.ts\", 14)\n            .setRandomData(MEDIA_PLAYLIST_2_DIR + \"fileSequence2.ts\", 15);\n  }\n\n  @After\n  public void tearDown() {\n    Util.recursiveDelete(tempFolder);\n  }\n\n  @Test\n  public void testCreateWithDefaultDownloaderFactory() {\n    DownloaderConstructorHelper constructorHelper =\n        new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);\n    DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);\n\n    Downloader downloader =\n        factory.createDownloader(\n            new DownloadRequest(\n                \"id\",\n                DownloadRequest.TYPE_HLS,\n                Uri.parse(\"https://www.test.com/download\"),\n                Collections.singletonList(new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0)),\n                /* customCacheKey= */ null,\n                /* data= */ null));\n    assertThat(downloader).isInstanceOf(HlsDownloader.class);\n  }\n\n  @Test\n  public void testCounterMethods() throws Exception {\n    HlsDownloader downloader =\n        getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX));\n    downloader.download(progressListener);\n\n    progressListener.assertBytesDownloaded(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12);\n  }\n\n  @Test\n  public void testDownloadRepresentation() throws Exception {\n    HlsDownloader downloader =\n        getHlsDownloader(MASTER_PLAYLIST_URI, getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX));\n    downloader.download(progressListener);\n\n    assertCachedData(\n        cache,\n        new RequestSet(fakeDataSet)\n            .subset(\n                MASTER_PLAYLIST_URI,\n                MEDIA_PLAYLIST_1_URI,\n                MEDIA_PLAYLIST_1_DIR + \"fileSequence0.ts\",\n                MEDIA_PLAYLIST_1_DIR + \"fileSequence1.ts\",\n                MEDIA_PLAYLIST_1_DIR + \"fileSequence2.ts\"));\n  }\n\n  @Test\n  public void testDownloadMultipleRepresentations() throws Exception {\n    HlsDownloader downloader =\n        getHlsDownloader(\n            MASTER_PLAYLIST_URI,\n            getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX));\n    downloader.download(progressListener);\n\n    assertCachedData(cache, fakeDataSet);\n  }\n\n  @Test\n  public void testDownloadAllRepresentations() throws Exception {\n    // Add data for the rest of the playlists\n    fakeDataSet\n        .setData(MEDIA_PLAYLIST_0_URI, MEDIA_PLAYLIST_DATA)\n        .setRandomData(MEDIA_PLAYLIST_0_DIR + \"fileSequence0.ts\", 10)\n        .setRandomData(MEDIA_PLAYLIST_0_DIR + \"fileSequence1.ts\", 11)\n        .setRandomData(MEDIA_PLAYLIST_0_DIR + \"fileSequence2.ts\", 12)\n        .setData(MEDIA_PLAYLIST_3_URI, MEDIA_PLAYLIST_DATA)\n        .setRandomData(MEDIA_PLAYLIST_3_DIR + \"fileSequence0.ts\", 13)\n        .setRandomData(MEDIA_PLAYLIST_3_DIR + \"fileSequence1.ts\", 14)\n        .setRandomData(MEDIA_PLAYLIST_3_DIR + \"fileSequence2.ts\", 15);\n\n    HlsDownloader downloader = getHlsDownloader(MASTER_PLAYLIST_URI, getKeys());\n    downloader.download(progressListener);\n\n    assertCachedData(cache, fakeDataSet);\n  }\n\n  @Test\n  public void testRemove() throws Exception {\n    HlsDownloader downloader =\n        getHlsDownloader(\n            MASTER_PLAYLIST_URI,\n            getKeys(MASTER_MEDIA_PLAYLIST_1_INDEX, MASTER_MEDIA_PLAYLIST_2_INDEX));\n    downloader.download(progressListener);\n    downloader.remove();\n\n    assertCacheEmpty(cache);\n  }\n\n  @Test\n  public void testDownloadMediaPlaylist() throws Exception {\n    HlsDownloader downloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI, getKeys());\n    downloader.download(progressListener);\n\n    assertCachedData(\n        cache,\n        new RequestSet(fakeDataSet)\n            .subset(\n                MEDIA_PLAYLIST_1_URI,\n                MEDIA_PLAYLIST_1_DIR + \"fileSequence0.ts\",\n                MEDIA_PLAYLIST_1_DIR + \"fileSequence1.ts\",\n                MEDIA_PLAYLIST_1_DIR + \"fileSequence2.ts\"));\n  }\n\n  @Test\n  public void testDownloadEncMediaPlaylist() throws Exception {\n    fakeDataSet =\n        new FakeDataSet()\n            .setData(ENC_MEDIA_PLAYLIST_URI, ENC_MEDIA_PLAYLIST_DATA)\n            .setRandomData(\"enc.key\", 8)\n            .setRandomData(\"enc2.key\", 9)\n            .setRandomData(\"fileSequence0.ts\", 10)\n            .setRandomData(\"fileSequence1.ts\", 11)\n            .setRandomData(\"fileSequence2.ts\", 12);\n\n    HlsDownloader downloader = getHlsDownloader(ENC_MEDIA_PLAYLIST_URI, getKeys());\n    downloader.download(progressListener);\n    assertCachedData(cache, fakeDataSet);\n  }\n\n  private HlsDownloader getHlsDownloader(String mediaPlaylistUri, List<StreamKey> keys) {\n    Factory factory = new Factory().setFakeDataSet(fakeDataSet);\n    return new HlsDownloader(\n        Uri.parse(mediaPlaylistUri), keys, new DownloaderConstructorHelper(cache, factory));\n  }\n\n  private static ArrayList<StreamKey> getKeys(int... variantIndices) {\n    ArrayList<StreamKey> streamKeys = new ArrayList<>();\n    for (int variantIndex : variantIndices) {\n      streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, variantIndex));\n    }\n    return streamKeys;\n  }\n\n  private static final class ProgressListener implements Downloader.ProgressListener {\n\n    private long bytesDownloaded;\n\n    @Override\n    public void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded) {\n      this.bytesDownloaded = bytesDownloaded;\n    }\n\n    public void assertBytesDownloaded(long bytesDownloaded) {\n      assertThat(this.bytesDownloaded).isEqualTo(bytesDownloaded);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link HlsMasterPlaylistParserTest}. */\n@RunWith(AndroidJUnit4.class)\npublic class HlsMasterPlaylistParserTest {\n\n  private static final String PLAYLIST_URI = \"https://example.com/test.m3u8\";\n\n  private static final String PLAYLIST_SIMPLE =\n      \" #EXTM3U \\n\"\n          + \"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=1280000,\"\n          + \"CODECS=\\\"mp4a.40.2,avc1.66.30\\\",RESOLUTION=304x128\\n\"\n          + \"http://example.com/low.m3u8\\n\"\n          + \"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\\\"mp4a.40.2 , avc1.66.30 \\\"\\n\"\n          + \"http://example.com/spaces_in_codecs.m3u8\\n\"\n          + \"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=2560000,FRAME-RATE=25,RESOLUTION=384x160\\n\"\n          + \"http://example.com/mid.m3u8\\n\"\n          + \"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=7680000,FRAME-RATE=29.997\\n\"\n          + \"http://example.com/hi.m3u8\\n\"\n          + \"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\\\"mp4a.40.5\\\"\\n\"\n          + \"http://example.com/audio-only.m3u8\";\n\n  private static final String PLAYLIST_WITH_AVG_BANDWIDTH =\n      \" #EXTM3U \\n\"\n          + \"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=1280000,\"\n          + \"CODECS=\\\"mp4a.40.2,avc1.66.30\\\",RESOLUTION=304x128\\n\"\n          + \"http://example.com/low.m3u8\\n\"\n          + \"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1270000,\"\n          + \"CODECS=\\\"mp4a.40.2 , avc1.66.30 \\\"\\n\"\n          + \"http://example.com/spaces_in_codecs.m3u8\\n\";\n\n  private static final String PLAYLIST_WITH_INVALID_HEADER =\n      \"#EXTMU3\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=1280000,\"\n          + \"CODECS=\\\"mp4a.40.2,avc1.66.30\\\",RESOLUTION=304x128\\n\"\n          + \"http://example.com/low.m3u8\\n\";\n\n  private static final String PLAYLIST_WITH_CC =\n      \" #EXTM3U \\n\"\n          + \"#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\\\"cc1\\\",\"\n          + \"LANGUAGE=\\\"es\\\",NAME=\\\"Eng\\\",INSTREAM-ID=\\\"SERVICE4\\\"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=1280000,\"\n          + \"CODECS=\\\"mp4a.40.2,avc1.66.30\\\",RESOLUTION=304x128\\n\"\n          + \"http://example.com/low.m3u8\\n\";\n\n  private static final String PLAYLIST_WITH_CHANNELS_ATTRIBUTE =\n      \" #EXTM3U \\n\"\n          + \"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\\\"audio\\\",CHANNELS=\\\"6\\\",NAME=\\\"Eng6\\\",\"\n          + \"URI=\\\"something.m3u8\\\"\\n\"\n          + \"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\\\"audio\\\",CHANNELS=\\\"2/6\\\",NAME=\\\"Eng26\\\",\"\n          + \"URI=\\\"something2.m3u8\\\"\\n\"\n          + \"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\\\"audio\\\",NAME=\\\"Eng\\\",\"\n          + \"URI=\\\"something3.m3u8\\\"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=1280000,\"\n          + \"CODECS=\\\"mp4a.40.2,avc1.66.30\\\",AUDIO=\\\"audio\\\",RESOLUTION=304x128\\n\"\n          + \"http://example.com/low.m3u8\\n\";\n\n  private static final String PLAYLIST_WITHOUT_CC =\n      \" #EXTM3U \\n\"\n          + \"#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\\\"cc1\\\",\"\n          + \"LANGUAGE=\\\"es\\\",NAME=\\\"Eng\\\",INSTREAM-ID=\\\"SERVICE4\\\"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=1280000,\"\n          + \"CODECS=\\\"mp4a.40.2,avc1.66.30\\\",RESOLUTION=304x128,\"\n          + \"CLOSED-CAPTIONS=NONE\\n\"\n          + \"http://example.com/low.m3u8\\n\";\n\n  private static final String PLAYLIST_WITH_SUBTITLES =\n      \" #EXTM3U \\n\"\n          + \"#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\\\"sub1\\\",\"\n          + \"LANGUAGE=\\\"es\\\",NAME=\\\"Eng\\\"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=1280000,\"\n          + \"CODECS=\\\"mp4a.40.2,avc1.66.30\\\",RESOLUTION=304x128\\n\"\n          + \"http://example.com/low.m3u8\\n\";\n\n  private static final String PLAYLIST_WITH_AUDIO_MEDIA_TAG =\n      \"#EXTM3U\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=2227464,CODECS=\\\"avc1.640020,mp4a.40.2\\\",AUDIO=\\\"aud1\\\"\\n\"\n          + \"uri1.m3u8\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=8178040,CODECS=\\\"avc1.64002a,mp4a.40.2\\\",AUDIO=\\\"aud1\\\"\\n\"\n          + \"uri2.m3u8\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=2448841,CODECS=\\\"avc1.640020,ac-3\\\",AUDIO=\\\"aud2\\\"\\n\"\n          + \"uri1.m3u8\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=8399417,CODECS=\\\"avc1.64002a,ac-3\\\",AUDIO=\\\"aud2\\\"\\n\"\n          + \"uri2.m3u8\\n\"\n          + \"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\\\"aud1\\\",LANGUAGE=\\\"en\\\",NAME=\\\"English\\\",\"\n          + \"AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\\\"2\\\",URI=\\\"a1/prog_index.m3u8\\\"\\n\"\n          + \"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\\\"aud2\\\",LANGUAGE=\\\"en\\\",NAME=\\\"English\\\",\"\n          + \"AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\\\"6\\\",URI=\\\"a2/prog_index.m3u8\\\"\\n\";\n\n  private static final String PLAYLIST_WITH_INDEPENDENT_SEGMENTS =\n      \" #EXTM3U\\n\"\n          + \"\\n\"\n          + \"#EXT-X-INDEPENDENT-SEGMENTS\\n\"\n          + \"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=1280000,\"\n          + \"CODECS=\\\"mp4a.40.2,avc1.66.30\\\",RESOLUTION=304x128\\n\"\n          + \"http://example.com/low.m3u8\\n\"\n          + \"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\\\"mp4a.40.2 , avc1.66.30 \\\"\\n\"\n          + \"http://example.com/spaces_in_codecs.m3u8\\n\";\n\n  private static final String PLAYLIST_WITH_VARIABLE_SUBSTITUTION =\n      \" #EXTM3U \\n\"\n          + \"\\n\"\n          + \"#EXT-X-DEFINE:NAME=\\\"codecs\\\",VALUE=\\\"mp4a.40.5\\\"\\n\"\n          + \"#EXT-X-DEFINE:NAME=\\\"tricky\\\",VALUE=\\\"This/{$nested}/reference/shouldnt/work\\\"\\n\"\n          + \"#EXT-X-DEFINE:NAME=\\\"nested\\\",VALUE=\\\"This should not be inserted\\\"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\\\"{$codecs}\\\"\\n\"\n          + \"http://example.com/{$tricky}\\n\";\n\n  private static final String PLAYLIST_WITH_MATCHING_STREAM_INF_URLS =\n      \"#EXTM3U\\n\"\n          + \"#EXT-X-VERSION:6\\n\"\n          + \"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=2227464,\"\n          + \"CLOSED-CAPTIONS=\\\"cc1\\\",AUDIO=\\\"aud1\\\",SUBTITLES=\\\"sub1\\\"\\n\"\n          + \"v5/prog_index.m3u8\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=6453202,\"\n          + \"CLOSED-CAPTIONS=\\\"cc1\\\",AUDIO=\\\"aud1\\\",SUBTITLES=\\\"sub1\\\"\\n\"\n          + \"v8/prog_index.m3u8\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=5054232,\"\n          + \"CLOSED-CAPTIONS=\\\"cc1\\\",AUDIO=\\\"aud1\\\",SUBTITLES=\\\"sub1\\\"\\n\"\n          + \"v7/prog_index.m3u8\\n\"\n          + \"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=2448841,\"\n          + \"CLOSED-CAPTIONS=\\\"cc1\\\",AUDIO=\\\"aud2\\\",SUBTITLES=\\\"sub1\\\"\\n\"\n          + \"v5/prog_index.m3u8\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=8399417,\"\n          + \"CLOSED-CAPTIONS=\\\"cc1\\\",AUDIO=\\\"aud2\\\",SUBTITLES=\\\"sub1\\\"\\n\"\n          + \"v9/prog_index.m3u8\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=5275609,\"\n          + \"CLOSED-CAPTIONS=\\\"cc1\\\",AUDIO=\\\"aud2\\\",SUBTITLES=\\\"sub1\\\"\\n\"\n          + \"v7/prog_index.m3u8\\n\"\n          + \"\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=2256841,\"\n          + \"CLOSED-CAPTIONS=\\\"cc1\\\",AUDIO=\\\"aud3\\\",SUBTITLES=\\\"sub1\\\"\\n\"\n          + \"v5/prog_index.m3u8\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=8207417,\"\n          + \"CLOSED-CAPTIONS=\\\"cc1\\\",AUDIO=\\\"aud3\\\",SUBTITLES=\\\"sub1\\\"\\n\"\n          + \"v9/prog_index.m3u8\\n\"\n          + \"#EXT-X-STREAM-INF:BANDWIDTH=6482579,\"\n          + \"CLOSED-CAPTIONS=\\\"cc1\\\",AUDIO=\\\"aud3\\\",SUBTITLES=\\\"sub1\\\"\\n\"\n          + \"v8/prog_index.m3u8\\n\"\n          + \"\\n\"\n          + \"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\\\"aud1\\\",NAME=\\\"English\\\",URI=\\\"a1/index.m3u8\\\"\\n\"\n          + \"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\\\"aud2\\\",NAME=\\\"English\\\",URI=\\\"a2/index.m3u8\\\"\\n\"\n          + \"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\\\"aud3\\\",NAME=\\\"English\\\",URI=\\\"a3/index.m3u8\\\"\\n\"\n          + \"\\n\"\n          + \"#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,\"\n          + \"GROUP-ID=\\\"cc1\\\",NAME=\\\"English\\\",INSTREAM-ID=\\\"CC1\\\"\\n\"\n          + \"\\n\"\n          + \"#EXT-X-MEDIA:TYPE=SUBTITLES,\"\n          + \"GROUP-ID=\\\"sub1\\\",NAME=\\\"English\\\",URI=\\\"s1/en/prog_index.m3u8\\\"\\n\";\n\n  @Test\n  public void testParseMasterPlaylist() throws IOException {\n    HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);\n\n    List<HlsMasterPlaylist.Variant> variants = masterPlaylist.variants;\n    assertThat(variants).hasSize(5);\n    assertThat(masterPlaylist.muxedCaptionFormats).isNull();\n\n    assertThat(variants.get(0).format.bitrate).isEqualTo(1280000);\n    assertThat(variants.get(0).format.codecs).isEqualTo(\"mp4a.40.2,avc1.66.30\");\n    assertThat(variants.get(0).format.width).isEqualTo(304);\n    assertThat(variants.get(0).format.height).isEqualTo(128);\n    assertThat(variants.get(0).url).isEqualTo(Uri.parse(\"http://example.com/low.m3u8\"));\n\n    assertThat(variants.get(1).format.bitrate).isEqualTo(1280000);\n    assertThat(variants.get(1).format.codecs).isEqualTo(\"mp4a.40.2 , avc1.66.30 \");\n    assertThat(variants.get(1).url)\n        .isEqualTo(Uri.parse(\"http://example.com/spaces_in_codecs.m3u8\"));\n\n    assertThat(variants.get(2).format.bitrate).isEqualTo(2560000);\n    assertThat(variants.get(2).format.codecs).isNull();\n    assertThat(variants.get(2).format.width).isEqualTo(384);\n    assertThat(variants.get(2).format.height).isEqualTo(160);\n    assertThat(variants.get(2).format.frameRate).isEqualTo(25.0f);\n    assertThat(variants.get(2).url).isEqualTo(Uri.parse(\"http://example.com/mid.m3u8\"));\n\n    assertThat(variants.get(3).format.bitrate).isEqualTo(7680000);\n    assertThat(variants.get(3).format.codecs).isNull();\n    assertThat(variants.get(3).format.width).isEqualTo(Format.NO_VALUE);\n    assertThat(variants.get(3).format.height).isEqualTo(Format.NO_VALUE);\n    assertThat(variants.get(3).format.frameRate).isEqualTo(29.997f);\n    assertThat(variants.get(3).url).isEqualTo(Uri.parse(\"http://example.com/hi.m3u8\"));\n\n    assertThat(variants.get(4).format.bitrate).isEqualTo(65000);\n    assertThat(variants.get(4).format.codecs).isEqualTo(\"mp4a.40.5\");\n    assertThat(variants.get(4).format.width).isEqualTo(Format.NO_VALUE);\n    assertThat(variants.get(4).format.height).isEqualTo(Format.NO_VALUE);\n    assertThat(variants.get(4).format.frameRate).isEqualTo((float) Format.NO_VALUE);\n    assertThat(variants.get(4).url).isEqualTo(Uri.parse(\"http://example.com/audio-only.m3u8\"));\n  }\n\n  @Test\n  public void testMasterPlaylistWithBandwdithAverage() throws IOException {\n    HlsMasterPlaylist masterPlaylist =\n        parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AVG_BANDWIDTH);\n\n    List<HlsMasterPlaylist.Variant> variants = masterPlaylist.variants;\n\n    assertThat(variants.get(0).format.bitrate).isEqualTo(1280000);\n    assertThat(variants.get(1).format.bitrate).isEqualTo(1270000);\n  }\n\n  @Test\n  public void testPlaylistWithInvalidHeader() throws IOException {\n    try {\n      parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);\n      fail(\"Expected exception not thrown.\");\n    } catch (ParserException e) {\n      // Expected due to invalid header.\n    }\n  }\n\n  @Test\n  public void testPlaylistWithClosedCaption() throws IOException {\n    HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC);\n    assertThat(playlist.muxedCaptionFormats).hasSize(1);\n    Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0);\n    assertThat(closedCaptionFormat.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA708);\n    assertThat(closedCaptionFormat.accessibilityChannel).isEqualTo(4);\n    assertThat(closedCaptionFormat.language).isEqualTo(\"es\");\n  }\n\n  @Test\n  public void testPlaylistWithChannelsAttribute() throws IOException {\n    HlsMasterPlaylist playlist =\n        parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CHANNELS_ATTRIBUTE);\n    List<HlsMasterPlaylist.Rendition> audios = playlist.audios;\n    assertThat(audios).hasSize(3);\n    assertThat(audios.get(0).format.channelCount).isEqualTo(6);\n    assertThat(audios.get(1).format.channelCount).isEqualTo(2);\n    assertThat(audios.get(2).format.channelCount).isEqualTo(Format.NO_VALUE);\n  }\n\n  @Test\n  public void testPlaylistWithoutClosedCaptions() throws IOException {\n    HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC);\n    assertThat(playlist.muxedCaptionFormats).isEmpty();\n  }\n\n  @Test\n  public void testCodecPropagation() throws IOException {\n    HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG);\n\n    Format firstAudioFormat = playlist.audios.get(0).format;\n    assertThat(firstAudioFormat.codecs).isEqualTo(\"mp4a.40.2\");\n    assertThat(firstAudioFormat.sampleMimeType).isEqualTo(MimeTypes.AUDIO_AAC);\n\n    Format secondAudioFormat = playlist.audios.get(1).format;\n    assertThat(secondAudioFormat.codecs).isEqualTo(\"ac-3\");\n    assertThat(secondAudioFormat.sampleMimeType).isEqualTo(MimeTypes.AUDIO_AC3);\n  }\n\n  @Test\n  public void testAudioIdPropagation() throws IOException {\n    HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG);\n\n    Format firstAudioFormat = playlist.audios.get(0).format;\n    assertThat(firstAudioFormat.id).isEqualTo(\"aud1:English\");\n\n    Format secondAudioFormat = playlist.audios.get(1).format;\n    assertThat(secondAudioFormat.id).isEqualTo(\"aud2:English\");\n  }\n\n  @Test\n  public void testCCIdPropagation() throws IOException {\n    HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC);\n\n    Format firstTextFormat = playlist.muxedCaptionFormats.get(0);\n    assertThat(firstTextFormat.id).isEqualTo(\"cc1:Eng\");\n  }\n\n  @Test\n  public void testSubtitleIdPropagation() throws IOException {\n    HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_SUBTITLES);\n\n    Format firstTextFormat = playlist.subtitles.get(0).format;\n    assertThat(firstTextFormat.id).isEqualTo(\"sub1:Eng\");\n  }\n\n  @Test\n  public void testIndependentSegments() throws IOException {\n    HlsMasterPlaylist playlistWithIndependentSegments =\n        parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INDEPENDENT_SEGMENTS);\n    assertThat(playlistWithIndependentSegments.hasIndependentSegments).isTrue();\n\n    HlsMasterPlaylist playlistWithoutIndependentSegments =\n        parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);\n    assertThat(playlistWithoutIndependentSegments.hasIndependentSegments).isFalse();\n  }\n\n  @Test\n  public void testVariableSubstitution() throws IOException {\n    HlsMasterPlaylist playlistWithSubstitutions =\n        parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_VARIABLE_SUBSTITUTION);\n    HlsMasterPlaylist.Variant variant = playlistWithSubstitutions.variants.get(0);\n    assertThat(variant.format.codecs).isEqualTo(\"mp4a.40.5\");\n    assertThat(variant.url)\n        .isEqualTo(Uri.parse(\"http://example.com/This/{$nested}/reference/shouldnt/work\"));\n  }\n\n  @Test\n  public void testHlsMetadata() throws IOException {\n    HlsMasterPlaylist playlist =\n        parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_MATCHING_STREAM_INF_URLS);\n    assertThat(playlist.variants).hasSize(4);\n    assertThat(playlist.variants.get(0).format.metadata)\n        .isEqualTo(\n            createExtXStreamInfMetadata(\n                createVariantInfo(/* bitrate= */ 2227464, /* audioGroupId= */ \"aud1\"),\n                createVariantInfo(/* bitrate= */ 2448841, /* audioGroupId= */ \"aud2\"),\n                createVariantInfo(/* bitrate= */ 2256841, /* audioGroupId= */ \"aud3\")));\n    assertThat(playlist.variants.get(1).format.metadata)\n        .isEqualTo(\n            createExtXStreamInfMetadata(\n                createVariantInfo(/* bitrate= */ 6453202, /* audioGroupId= */ \"aud1\"),\n                createVariantInfo(/* bitrate= */ 6482579, /* audioGroupId= */ \"aud3\")));\n    assertThat(playlist.variants.get(2).format.metadata)\n        .isEqualTo(\n            createExtXStreamInfMetadata(\n                createVariantInfo(/* bitrate= */ 5054232, /* audioGroupId= */ \"aud1\"),\n                createVariantInfo(/* bitrate= */ 5275609, /* audioGroupId= */ \"aud2\")));\n    assertThat(playlist.variants.get(3).format.metadata)\n        .isEqualTo(\n            createExtXStreamInfMetadata(\n                createVariantInfo(/* bitrate= */ 8399417, /* audioGroupId= */ \"aud2\"),\n                createVariantInfo(/* bitrate= */ 8207417, /* audioGroupId= */ \"aud3\")));\n\n    assertThat(playlist.audios).hasSize(3);\n    assertThat(playlist.audios.get(0).format.metadata)\n        .isEqualTo(createExtXMediaMetadata(/* groupId= */ \"aud1\", /* name= */ \"English\"));\n    assertThat(playlist.audios.get(1).format.metadata)\n        .isEqualTo(createExtXMediaMetadata(/* groupId= */ \"aud2\", /* name= */ \"English\"));\n    assertThat(playlist.audios.get(2).format.metadata)\n        .isEqualTo(createExtXMediaMetadata(/* groupId= */ \"aud3\", /* name= */ \"English\"));\n  }\n\n  private static Metadata createExtXStreamInfMetadata(HlsTrackMetadataEntry.VariantInfo... infos) {\n    return new Metadata(\n        new HlsTrackMetadataEntry(/* groupId= */ null, /* name= */ null, Arrays.asList(infos)));\n  }\n\n  private static Metadata createExtXMediaMetadata(String groupId, String name) {\n    return new Metadata(new HlsTrackMetadataEntry(groupId, name, Collections.emptyList()));\n  }\n\n  private static HlsTrackMetadataEntry.VariantInfo createVariantInfo(\n      long bitrate, String audioGroupId) {\n    return new HlsTrackMetadataEntry.VariantInfo(\n        bitrate,\n        /* videoGroupId= */ null,\n        audioGroupId,\n        /* subtitleGroupId= */ \"sub1\",\n        /* captionGroupId= */ \"cc1\");\n  }\n\n  private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString)\n      throws IOException {\n    Uri playlistUri = Uri.parse(uri);\n    ByteArrayInputStream inputStream =\n        new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));\n    return (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test for {@link HlsMediaPlaylistParserTest}. */\n@RunWith(AndroidJUnit4.class)\npublic class HlsMediaPlaylistParserTest {\n\n  @Test\n  public void testParseMediaPlaylist() throws Exception {\n    Uri playlistUri = Uri.parse(\"https://example.com/test.m3u8\");\n    String playlistString =\n        \"#EXTM3U\\n\"\n            + \"#EXT-X-VERSION:3\\n\"\n            + \"#EXT-X-PLAYLIST-TYPE:VOD\\n\"\n            + \"#EXT-X-START:TIME-OFFSET=-25\"\n            + \"#EXT-X-TARGETDURATION:8\\n\"\n            + \"#EXT-X-MEDIA-SEQUENCE:2679\\n\"\n            + \"#EXT-X-DISCONTINUITY-SEQUENCE:4\\n\"\n            + \"#EXT-X-ALLOW-CACHE:YES\\n\"\n            + \"\\n\"\n            + \"#EXTINF:7.975,\\n\"\n            + \"#EXT-X-BYTERANGE:51370@0\\n\"\n            + \"https://priv.example.com/fileSequence2679.ts\\n\"\n            + \"\\n\"\n            + \"#EXT-X-KEY:METHOD=AES-128,\"\n            + \"URI=\\\"https://priv.example.com/key.php?r=2680\\\",IV=0x1566B\\n\"\n            + \"#EXTINF:7.975,segment title\\n\"\n            + \"#EXT-X-BYTERANGE:51501@2147483648\\n\"\n            + \"https://priv.example.com/fileSequence2680.ts\\n\"\n            + \"\\n\"\n            + \"#EXT-X-KEY:METHOD=NONE\\n\"\n            + \"#EXTINF:7.941,segment title .,:/# with interesting chars\\n\"\n            + \"#EXT-X-BYTERANGE:51501\\n\" // @2147535149\n            + \"https://priv.example.com/fileSequence2681.ts\\n\"\n            + \"\\n\"\n            + \"#EXT-X-DISCONTINUITY\\n\"\n            + \"#EXT-X-KEY:METHOD=AES-128,URI=\\\"https://priv.example.com/key.php?r=2682\\\"\\n\"\n            + \"#EXTINF:7.975\\n\" // Trailing comma is omitted.\n            + \"#EXT-X-BYTERANGE:51740\\n\" // @2147586650\n            + \"https://priv.example.com/fileSequence2682.ts\\n\"\n            + \"\\n\"\n            + \"#EXTINF:7.975,\\n\"\n            + \"https://priv.example.com/fileSequence2683.ts\\n\"\n            + \"#EXT-X-ENDLIST\";\n    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));\n    HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream);\n\n    HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;\n    assertThat(mediaPlaylist.playlistType).isEqualTo(HlsMediaPlaylist.PLAYLIST_TYPE_VOD);\n    assertThat(mediaPlaylist.startOffsetUs).isEqualTo(mediaPlaylist.durationUs - 25000000);\n\n    assertThat(mediaPlaylist.mediaSequence).isEqualTo(2679);\n    assertThat(mediaPlaylist.version).isEqualTo(3);\n    assertThat(mediaPlaylist.hasEndTag).isTrue();\n    assertThat(mediaPlaylist.protectionSchemes).isNull();\n    List<Segment> segments = mediaPlaylist.segments;\n    assertThat(segments).isNotNull();\n    assertThat(segments).hasSize(5);\n\n    Segment segment = segments.get(0);\n    assertThat(mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence)\n        .isEqualTo(4);\n    assertThat(segment.durationUs).isEqualTo(7975000);\n    assertThat(segment.title).isEqualTo(\"\");\n    assertThat(segment.fullSegmentEncryptionKeyUri).isNull();\n    assertThat(segment.encryptionIV).isNull();\n    assertThat(segment.byterangeLength).isEqualTo(51370);\n    assertThat(segment.byterangeOffset).isEqualTo(0);\n    assertThat(segment.url).isEqualTo(\"https://priv.example.com/fileSequence2679.ts\");\n\n    segment = segments.get(1);\n    assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0);\n    assertThat(segment.durationUs).isEqualTo(7975000);\n    assertThat(segment.title).isEqualTo(\"segment title\");\n    assertThat(segment.fullSegmentEncryptionKeyUri)\n        .isEqualTo(\"https://priv.example.com/key.php?r=2680\");\n    assertThat(segment.encryptionIV).isEqualTo(\"0x1566B\");\n    assertThat(segment.byterangeLength).isEqualTo(51501);\n    assertThat(segment.byterangeOffset).isEqualTo(2147483648L);\n    assertThat(segment.url).isEqualTo(\"https://priv.example.com/fileSequence2680.ts\");\n\n    segment = segments.get(2);\n    assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0);\n    assertThat(segment.durationUs).isEqualTo(7941000);\n    assertThat(segment.title).isEqualTo(\"segment title .,:/# with interesting chars\");\n    assertThat(segment.fullSegmentEncryptionKeyUri).isNull();\n    assertThat(segment.encryptionIV).isEqualTo(null);\n    assertThat(segment.byterangeLength).isEqualTo(51501);\n    assertThat(segment.byterangeOffset).isEqualTo(2147535149L);\n    assertThat(segment.url).isEqualTo(\"https://priv.example.com/fileSequence2681.ts\");\n\n    segment = segments.get(3);\n    assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1);\n    assertThat(segment.durationUs).isEqualTo(7975000);\n    assertThat(segment.title).isEqualTo(\"\");\n    assertThat(segment.fullSegmentEncryptionKeyUri)\n        .isEqualTo(\"https://priv.example.com/key.php?r=2682\");\n    // 0xA7A == 2682.\n    assertThat(segment.encryptionIV).isNotNull();\n    assertThat(Util.toUpperInvariant(segment.encryptionIV)).isEqualTo(\"A7A\");\n    assertThat(segment.byterangeLength).isEqualTo(51740);\n    assertThat(segment.byterangeOffset).isEqualTo(2147586650L);\n    assertThat(segment.url).isEqualTo(\"https://priv.example.com/fileSequence2682.ts\");\n\n    segment = segments.get(4);\n    assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1);\n    assertThat(segment.durationUs).isEqualTo(7975000);\n    assertThat(segment.title).isEqualTo(\"\");\n    assertThat(segment.fullSegmentEncryptionKeyUri)\n        .isEqualTo(\"https://priv.example.com/key.php?r=2682\");\n    // 0xA7B == 2683.\n    assertThat(segment.encryptionIV).isNotNull();\n    assertThat(Util.toUpperInvariant(segment.encryptionIV)).isEqualTo(\"A7B\");\n    assertThat(segment.byterangeLength).isEqualTo(C.LENGTH_UNSET);\n    assertThat(segment.byterangeOffset).isEqualTo(0);\n    assertThat(segment.url).isEqualTo(\"https://priv.example.com/fileSequence2683.ts\");\n  }\n\n  @Test\n  public void testParseSampleAesMethod() throws Exception {\n    Uri playlistUri = Uri.parse(\"https://example.com/test.m3u8\");\n    String playlistString =\n        \"#EXTM3U\\n\"\n            + \"#EXT-X-MEDIA-SEQUENCE:0\\n\"\n            + \"#EXTINF:8,\\n\"\n            + \"https://priv.example.com/1.ts\\n\"\n            + \"\\n\"\n            + \"#EXT-X-KEY:METHOD=SAMPLE-AES,URI=\"\n            + \"\\\"data:text/plain;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\\\",\"\n            + \"IV=0x9358382AEB449EE23C3D809DA0B9CCD3,KEYFORMATVERSIONS=\\\"1\\\",\"\n            + \"KEYFORMAT=\\\"com.widevine\\\",IV=0x1566B\\n\"\n            + \"#EXTINF:8,\\n\"\n            + \"https://priv.example.com/2.ts\\n\"\n            + \"#EXT-X-ENDLIST\\n\";\n    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));\n    HlsMediaPlaylist playlist =\n        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);\n    assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cbcs);\n    assertThat(playlist.protectionSchemes.get(0).matches(C.WIDEVINE_UUID)).isTrue();\n    assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse();\n\n    assertThat(playlist.segments.get(0).drmInitData).isNull();\n\n    assertThat(playlist.segments.get(1).drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue();\n    assertThat(playlist.segments.get(1).drmInitData.get(0).hasData()).isTrue();\n  }\n\n  @Test\n  public void testParseSampleAesCencMethod() throws Exception {\n    Uri playlistUri = Uri.parse(\"https://example.com/test.m3u8\");\n    String playlistString =\n        \"#EXTM3U\\n\"\n            + \"#EXT-X-MEDIA-SEQUENCE:0\\n\"\n            + \"#EXTINF:8,\\n\"\n            + \"https://priv.example.com/1.ts\\n\"\n            + \"\\n\"\n            + \"#EXT-X-KEY:URI=\\\"data:text/plain;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\\\",\"\n            + \"IV=0x9358382AEB449EE23C3D809DA0B9CCD3,KEYFORMATVERSIONS=\\\"1\\\",\"\n            + \"KEYFORMAT=\\\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\\\",\"\n            + \"IV=0x1566B,METHOD=SAMPLE-AES-CENC \\n\"\n            + \"#EXTINF:8,\\n\"\n            + \"https://priv.example.com/2.ts\\n\"\n            + \"#EXT-X-ENDLIST\\n\";\n    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));\n    HlsMediaPlaylist playlist =\n        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);\n    assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cenc);\n    assertThat(playlist.protectionSchemes.get(0).matches(C.WIDEVINE_UUID)).isTrue();\n    assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse();\n  }\n\n  @Test\n  public void testParseSampleAesCtrMethod() throws Exception {\n    Uri playlistUri = Uri.parse(\"https://example.com/test.m3u8\");\n    String playlistString =\n        \"#EXTM3U\\n\"\n            + \"#EXT-X-MEDIA-SEQUENCE:0\\n\"\n            + \"#EXTINF:8,\\n\"\n            + \"https://priv.example.com/1.ts\\n\"\n            + \"\\n\"\n            + \"#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI=\"\n            + \"\\\"data:text/plain;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\\\",\"\n            + \"IV=0x9358382AEB449EE23C3D809DA0B9CCD3,KEYFORMATVERSIONS=\\\"1\\\",\"\n            + \"KEYFORMAT=\\\"com.widevine\\\",IV=0x1566B\\n\"\n            + \"#EXTINF:8,\\n\"\n            + \"https://priv.example.com/2.ts\\n\"\n            + \"#EXT-X-ENDLIST\\n\";\n    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));\n    HlsMediaPlaylist playlist =\n        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);\n    assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cenc);\n    assertThat(playlist.protectionSchemes.get(0).matches(C.WIDEVINE_UUID)).isTrue();\n    assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse();\n  }\n\n  @Test\n  public void testMultipleExtXKeysForSingleSegment() throws Exception {\n    Uri playlistUri = Uri.parse(\"https://example.com/test.m3u8\");\n    String playlistString =\n        \"#EXTM3U\\n\"\n            + \"#EXT-X-VERSION:6\\n\"\n            + \"#EXT-X-TARGETDURATION:6\\n\"\n            + \"#EXT-X-MAP:URI=\\\"map.mp4\\\"\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"s000000.mp4\\n\"\n            + \"#EXT-X-KEY:METHOD=SAMPLE-AES,\"\n            + \"KEYFORMAT=\\\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\\\",\"\n            + \"KEYFORMATVERSIONS=\\\"1\\\",\"\n            + \"URI=\\\"data:text/plain;base64,Tm90aGluZyB0byBzZWUgaGVyZQ==\\\"\\n\"\n            + \"#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\\\"com.microsoft.playready\\\",\"\n            + \"KEYFORMATVERSIONS=\\\"1\\\",\"\n            + \"URI=\\\"data:text/plain;charset=UTF-16;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\\\"\\n\"\n            + \"#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\\\"com.apple.streamingkeydelivery\\\",\"\n            + \"KEYFORMATVERSIONS=\\\"1\\\",URI=\\\"skd://QW5vdGhlciBlYXN0ZXIgZWdn\\\"\\n\"\n            + \"#EXT-X-MAP:URI=\\\"map.mp4\\\"\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"s000000.mp4\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"s000001.mp4\\n\"\n            + \"#EXT-X-KEY:METHOD=SAMPLE-AES,\"\n            + \"KEYFORMAT=\\\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\\\",\"\n            + \"KEYFORMATVERSIONS=\\\"1\\\",\"\n            + \"URI=\\\"data:text/plain;base64,RG9uJ3QgeW91IGdldCB0aXJlZCBvZiBkb2luZyB0aGlzPw==\\\"\"\n            + \"\\n\"\n            + \"#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\\\"com.microsoft.playready\\\",\"\n            + \"KEYFORMATVERSIONS=\\\"1\\\",\"\n            + \"URI=\\\"data:text/plain;charset=UTF-16;base64,T2ssIGl0J3Mgbm90IGZ1biBhbnltb3Jl\\\"\\n\"\n            + \"#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\\\"com.apple.streamingkeydelivery\\\",\"\n            + \"KEYFORMATVERSIONS=\\\"1\\\",\"\n            + \"URI=\\\"skd://V2FpdCB1bnRpbCB5b3Ugc2VlIHRoZSBuZXh0IG9uZSE=\\\"\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"s000024.mp4\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"s000025.mp4\\n\"\n            + \"#EXT-X-KEY:METHOD=NONE\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"s000026.mp4\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"s000026.mp4\\n\";\n    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));\n    HlsMediaPlaylist playlist =\n        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);\n    assertThat(playlist.protectionSchemes.schemeType).isEqualTo(C.CENC_TYPE_cbcs);\n    // Unsupported protection schemes like com.apple.streamingkeydelivery are ignored.\n    assertThat(playlist.protectionSchemes.schemeDataCount).isEqualTo(2);\n    assertThat(playlist.protectionSchemes.get(0).matches(C.PLAYREADY_UUID)).isTrue();\n    assertThat(playlist.protectionSchemes.get(0).hasData()).isFalse();\n    assertThat(playlist.protectionSchemes.get(1).matches(C.WIDEVINE_UUID)).isTrue();\n    assertThat(playlist.protectionSchemes.get(1).hasData()).isFalse();\n\n    assertThat(playlist.segments.get(0).drmInitData).isNull();\n\n    assertThat(playlist.segments.get(1).drmInitData.get(0).matches(C.PLAYREADY_UUID)).isTrue();\n    assertThat(playlist.segments.get(1).drmInitData.get(0).hasData()).isTrue();\n    assertThat(playlist.segments.get(1).drmInitData.get(1).matches(C.WIDEVINE_UUID)).isTrue();\n    assertThat(playlist.segments.get(1).drmInitData.get(1).hasData()).isTrue();\n\n    assertThat(playlist.segments.get(1).drmInitData)\n        .isEqualTo(playlist.segments.get(2).drmInitData);\n    assertThat(playlist.segments.get(2).drmInitData)\n        .isNotEqualTo(playlist.segments.get(3).drmInitData);\n    assertThat(playlist.segments.get(3).drmInitData.get(0).matches(C.PLAYREADY_UUID)).isTrue();\n    assertThat(playlist.segments.get(3).drmInitData.get(0).hasData()).isTrue();\n    assertThat(playlist.segments.get(3).drmInitData.get(1).matches(C.WIDEVINE_UUID)).isTrue();\n    assertThat(playlist.segments.get(3).drmInitData.get(1).hasData()).isTrue();\n\n    assertThat(playlist.segments.get(3).drmInitData)\n        .isEqualTo(playlist.segments.get(4).drmInitData);\n    assertThat(playlist.segments.get(5).drmInitData).isNull();\n    assertThat(playlist.segments.get(6).drmInitData).isNull();\n  }\n\n  @Test\n  public void testGapTag() throws IOException {\n    Uri playlistUri = Uri.parse(\"https://example.com/test2.m3u8\");\n    String playlistString =\n        \"#EXTM3U\\n\"\n            + \"#EXT-X-VERSION:3\\n\"\n            + \"#EXT-X-TARGETDURATION:5\\n\"\n            + \"#EXT-X-PLAYLIST-TYPE:VOD\\n\"\n            + \"#EXT-X-MEDIA-SEQUENCE:0\\n\"\n            + \"#EXT-X-PROGRAM-DATE-TIME:2016-09-22T02:00:01+00:00\\n\"\n            + \"#EXT-X-KEY:METHOD=AES-128,URI=\\\"https://example.com/key?value=something\\\"\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/27.ts\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/32.ts\\n\"\n            + \"#EXT-X-KEY:METHOD=NONE\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"#EXT-X-GAP \\n\"\n            + \"../dummy.ts\\n\"\n            + \"#EXT-X-KEY:METHOD=AES-128,URI=\\\"https://key-service.bamgrid.com/1.0/key?\"\n            + \"hex-value=9FB8989D15EEAAF8B21B860D7ED3072A\\\",IV=0x410C8AC18AA42EFA18B5155484F5FC34\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/42.ts\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/47.ts\\n\";\n    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));\n    HlsMediaPlaylist playlist =\n        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);\n\n    assertThat(playlist.hasEndTag).isFalse();\n    assertThat(playlist.segments.get(1).hasGapTag).isFalse();\n    assertThat(playlist.segments.get(2).hasGapTag).isTrue();\n    assertThat(playlist.segments.get(3).hasGapTag).isFalse();\n  }\n\n  @Test\n  public void testMapTag() throws IOException {\n    Uri playlistUri = Uri.parse(\"https://example.com/test3.m3u8\");\n    String playlistString =\n        \"#EXTM3U\\n\"\n            + \"#EXT-X-VERSION:3\\n\"\n            + \"#EXT-X-TARGETDURATION:5\\n\"\n            + \"#EXT-X-MEDIA-SEQUENCE:10\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/27.ts\\n\"\n            + \"#EXT-X-MAP:URI=\\\"init1.ts\\\"\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/32.ts\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/42.ts\\n\"\n            + \"#EXT-X-MAP:URI=\\\"init2.ts\\\"\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/47.ts\\n\";\n    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));\n    HlsMediaPlaylist playlist =\n        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);\n\n    List<Segment> segments = playlist.segments;\n    assertThat(segments.get(0).initializationSegment).isNull();\n    assertThat(segments.get(1).initializationSegment)\n        .isSameAs(segments.get(2).initializationSegment);\n    assertThat(segments.get(1).initializationSegment.url).isEqualTo(\"init1.ts\");\n    assertThat(segments.get(3).initializationSegment.url).isEqualTo(\"init2.ts\");\n  }\n\n  @Test\n  public void testEncryptedMapTag() throws IOException {\n    Uri playlistUri = Uri.parse(\"https://example.com/test3.m3u8\");\n    String playlistString =\n        \"#EXTM3U\\n\"\n            + \"#EXT-X-VERSION:3\\n\"\n            + \"#EXT-X-TARGETDURATION:5\\n\"\n            + \"#EXT-X-MEDIA-SEQUENCE:10\\n\"\n            + \"#EXT-X-KEY:METHOD=AES-128,\"\n            + \"URI=\\\"https://priv.example.com/key.php?r=2680\\\",IV=0x1566B\\n\"\n            + \"#EXT-X-MAP:URI=\\\"init1.ts\\\"\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/32.ts\\n\"\n            + \"#EXT-X-KEY:METHOD=NONE\\n\"\n            + \"#EXT-X-MAP:URI=\\\"init2.ts\\\"\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/47.ts\\n\";\n    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));\n    HlsMediaPlaylist playlist =\n        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);\n\n    List<Segment> segments = playlist.segments;\n    Segment initSegment1 = segments.get(0).initializationSegment;\n    assertThat(initSegment1.fullSegmentEncryptionKeyUri)\n        .isEqualTo(\"https://priv.example.com/key.php?r=2680\");\n    assertThat(initSegment1.encryptionIV).isEqualTo(\"0x1566B\");\n    Segment initSegment2 = segments.get(1).initializationSegment;\n    assertThat(initSegment2.fullSegmentEncryptionKeyUri).isNull();\n    assertThat(initSegment2.encryptionIV).isNull();\n  }\n\n  @Test\n  public void testEncryptedMapTagWithNoIvFails() throws IOException {\n    Uri playlistUri = Uri.parse(\"https://example.com/test3.m3u8\");\n    String playlistString =\n        \"#EXTM3U\\n\"\n            + \"#EXT-X-VERSION:3\\n\"\n            + \"#EXT-X-TARGETDURATION:5\\n\"\n            + \"#EXT-X-MEDIA-SEQUENCE:10\\n\"\n            + \"#EXT-X-KEY:METHOD=AES-128,\"\n            + \"URI=\\\"https://priv.example.com/key.php?r=2680\\\"\\n\"\n            + \"#EXT-X-MAP:URI=\\\"init1.ts\\\"\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/32.ts\\n\";\n    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));\n\n    try {\n      new HlsPlaylistParser().parse(playlistUri, inputStream);\n      fail();\n    } catch (ParserException e) {\n      // Expected because the initialization segment does not have a defined initialization vector,\n      // although it is affected by an EXT-X-KEY tag.\n    }\n  }\n\n  @Test\n  public void testMasterPlaylistAttributeInheritance() throws IOException {\n    Uri playlistUri = Uri.parse(\"https://example.com/test3.m3u8\");\n    String playlistString =\n        \"#EXTM3U\\n\"\n            + \"#EXT-X-VERSION:3\\n\"\n            + \"#EXT-X-TARGETDURATION:5\\n\"\n            + \"#EXT-X-MEDIA-SEQUENCE:10\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/27.ts\\n\"\n            + \"#EXT-X-MAP:URI=\\\"init1.ts\\\"\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/32.ts\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/42.ts\\n\"\n            + \"#EXT-X-MAP:URI=\\\"init2.ts\\\"\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"02/00/47.ts\\n\";\n    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));\n    HlsMediaPlaylist standalonePlaylist =\n        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);\n    assertThat(standalonePlaylist.hasIndependentSegments).isFalse();\n\n    inputStream.reset();\n    HlsMasterPlaylist masterPlaylist =\n        new HlsMasterPlaylist(\n            /* baseUri= */ \"https://example.com/\",\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= */ null,\n            /* hasIndependentSegments= */ true,\n            /* variableDefinitions= */ Collections.emptyMap(),\n            /* sessionKeyDrmInitData= */ Collections.emptyList());\n    HlsMediaPlaylist playlistWithInheritance =\n        (HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream);\n    assertThat(playlistWithInheritance.hasIndependentSegments).isTrue();\n  }\n\n  @Test\n  public void testVariableSubstitution() throws IOException {\n    Uri playlistUri = Uri.parse(\"https://example.com/substitution.m3u8\");\n    String playlistString =\n        \"#EXTM3U\\n\"\n            + \"#EXT-X-VERSION:8\\n\"\n            + \"#EXT-X-DEFINE:NAME=\\\"underscore_1\\\",VALUE=\\\"{\\\"\\n\"\n            + \"#EXT-X-DEFINE:NAME=\\\"dash-1\\\",VALUE=\\\"replaced_value.ts\\\"\\n\"\n            + \"#EXT-X-TARGETDURATION:5\\n\"\n            + \"#EXT-X-MEDIA-SEQUENCE:10\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"segment1.ts\\n\"\n            + \"#EXT-X-MAP:URI=\\\"{$dash-1}\\\"\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"segment{$underscore_1}$name_1}\\n\";\n    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));\n    HlsMediaPlaylist playlist =\n        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);\n    Segment segment = playlist.segments.get(1);\n    assertThat(segment.initializationSegment.url).isEqualTo(\"replaced_value.ts\");\n    assertThat(segment.url).isEqualTo(\"segment{$name_1}\");\n  }\n\n  @Test\n  public void testInheritedVariableSubstitution() throws IOException {\n    Uri playlistUri = Uri.parse(\"https://example.com/test3.m3u8\");\n    String playlistString =\n        \"#EXTM3U\\n\"\n            + \"#EXT-X-VERSION:8\\n\"\n            + \"#EXT-X-TARGETDURATION:5\\n\"\n            + \"#EXT-X-MEDIA-SEQUENCE:10\\n\"\n            + \"#EXT-X-DEFINE:IMPORT=\\\"imported_base\\\"\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"{$imported_base}1.ts\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"{$imported_base}2.ts\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"{$imported_base}3.ts\\n\"\n            + \"#EXTINF:5.005,\\n\"\n            + \"{$imported_base}4.ts\\n\";\n    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));\n    HashMap<String, String> variableDefinitions = new HashMap<>();\n    variableDefinitions.put(\"imported_base\", \"long_path\");\n    HlsMasterPlaylist masterPlaylist =\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,\n            /* sessionKeyDrmInitData= */ Collections.emptyList());\n    HlsMediaPlaylist playlist =\n        (HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream);\n    for (int i = 1; i <= 4; i++) {\n      assertThat(playlist.segments.get(i - 1).url).isEqualTo(\"long_path\" + i + \".ts\");\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/README.md",
    "content": "# ExoPlayer SABR library module #\n\nProvides support for YouTube Dynamic Adaptive Streaming (SABR) content. To\nplay SABR content, instantiate a `SabrMediaSource` and pass it to\n`ExoPlayer.prepare`.\n\n## Links ##\n\n* [Developer Guide][].\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.sabr.*`\n  belong to this module.\n\n[Developer Guide]: https://exoplayer.dev/sabr.html\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/build.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.\nplugins {\n    id \"com.google.protobuf\" version \"0.9.5\"\n}\n\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    buildTypes {\n        // Re-enable test coverage when the following issue is fixed:\n        // https://issuetracker.google.com/issues/37019591\n        // debug {\n        //    testCoverageEnabled = true\n        // }\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n\n    // gradle 4.6 migration: disable dimensions mechanism\n    // more: https://proandroiddev.com/advanced-android-flavors-part-4-a-new-version-fc2ad80c01bb\n    flavorDimensions \"default\"\n\n    productFlavors {\n        stbeta {}\n        ststable {}\n        stfdroid {}\n    }\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    implementation project(':mediaserviceinterfaces')\n    implementation project(':sharedutils')\n    implementation project(':youtubeapi')\n    compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion\n    implementation 'androidx.annotation:annotation:1.1.0'\n    implementation 'com.google.protobuf:protobuf-javalite:' + protobufVersion\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n    testImplementation project(modulePrefix + 'library-sabr')\n}\n\nprotobuf {\n    protoc {\n        artifact = 'com.google.protobuf:protoc:' + protobufVersion\n    }\n\n    generateProtoTasks {\n        all().configureEach { task ->\n            task.builtins {\n                java {\n                    option \"lite\"\n                }\n            }\n        }\n    }\n}\n\next {\n    javadocTitle = 'SABR module'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'exoplayer-sabr'\n    releaseDescription = 'The ExoPlayer library SABR module.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n\n<manifest package=\"com.google.android.exoplayer2.source.sabr\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/DefaultSabrChunkSource.java",
    "content": "package com.google.android.exoplayer2.source.sabr;\r\n\r\nimport android.net.Uri;\r\nimport android.os.SystemClock;\r\n\r\nimport androidx.annotation.CheckResult;\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.SeekParameters;\r\nimport com.google.android.exoplayer2.extractor.Extractor;\r\nimport com.google.android.exoplayer2.extractor.TrackOutput;\r\nimport com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor;\r\nimport com.google.android.exoplayer2.source.BehindLiveWindowException;\r\nimport com.google.android.exoplayer2.source.chunk.Chunk;\r\nimport com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;\r\nimport com.google.android.exoplayer2.source.chunk.ChunkHolder;\r\nimport com.google.android.exoplayer2.source.chunk.ContainerMediaChunk;\r\nimport com.google.android.exoplayer2.source.chunk.InitializationChunk;\r\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\r\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\r\nimport com.google.android.exoplayer2.source.sabr.PlayerEmsgHandler.PlayerTrackEmsgHandler;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.AdaptationSet;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.RangedUri;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.Representation;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.SabrManifest;\r\nimport com.google.android.exoplayer2.source.sabr.parser.adapter.SabrFragmentedMp4Adapter;\r\nimport com.google.android.exoplayer2.source.sabr.parser.adapter.SabrMatroskaAdapter;\r\nimport com.google.android.exoplayer2.source.sabr.parser.SabrStream;\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.AudioSelector;\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.CaptionSelector;\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.FormatSelector;\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.VideoSelector;\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\r\nimport com.google.android.exoplayer2.upstream.DataSource;\r\nimport com.google.android.exoplayer2.upstream.DataSpec;\r\nimport com.google.android.exoplayer2.upstream.LoaderErrorThrower;\r\nimport com.google.android.exoplayer2.upstream.TransferListener;\r\nimport com.google.android.exoplayer2.util.Log;\r\nimport com.google.android.exoplayer2.util.MimeTypes;\r\n\r\nimport java.io.IOException;\r\nimport java.util.ArrayList;\r\nimport java.util.HashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\n\r\npublic class DefaultSabrChunkSource implements SabrChunkSource {\r\n    public static final class Factory implements SabrChunkSource.Factory {\r\n\r\n        private final DataSource.Factory dataSourceFactory;\r\n        private final int maxSegmentsPerLoad;\r\n\r\n        public Factory(DataSource.Factory dataSourceFactory) {\r\n            this(dataSourceFactory, 1);\r\n        }\r\n\r\n        public Factory(DataSource.Factory dataSourceFactory, int maxSegmentsPerLoad) {\r\n            this.dataSourceFactory = dataSourceFactory;\r\n            this.maxSegmentsPerLoad = maxSegmentsPerLoad;\r\n        }\r\n\r\n        @Override\r\n        public SabrChunkSource createSabrChunkSource(\r\n                LoaderErrorThrower manifestLoaderErrorThrower,\r\n                SabrManifest manifest,\r\n                int periodIndex,\r\n                int[] adaptationSetIndices,\r\n                TrackSelection trackSelection,\r\n                int trackType,\r\n                long elapsedRealtimeOffsetMs,\r\n                boolean enableEventMessageTrack,\r\n                List<Format> closedCaptionFormats,\r\n                @Nullable PlayerTrackEmsgHandler playerEmsgHandler,\r\n                @Nullable TransferListener transferListener) {\r\n            DataSource dataSource = dataSourceFactory.createDataSource();\r\n            if (transferListener != null) {\r\n                dataSource.addTransferListener(transferListener);\r\n            }\r\n            return new DefaultSabrChunkSource(\r\n                    manifestLoaderErrorThrower,\r\n                    manifest,\r\n                    periodIndex,\r\n                    adaptationSetIndices,\r\n                    trackSelection,\r\n                    trackType,\r\n                    dataSource,\r\n                    elapsedRealtimeOffsetMs,\r\n                    maxSegmentsPerLoad,\r\n                    enableEventMessageTrack,\r\n                    closedCaptionFormats,\r\n                    playerEmsgHandler);\r\n        }\r\n\r\n    }\r\n\r\n    private static final String TAG = DefaultSabrChunkSource.class.getSimpleName();\r\n    private final LoaderErrorThrower manifestLoaderErrorThrower;\r\n    private final int[] adaptationSetIndices;\r\n    private final int trackType;\r\n    private final DataSource dataSource;\r\n    private final long elapsedRealtimeOffsetMs;\r\n    private final int maxSegmentsPerLoad;\r\n    @Nullable private final PlayerTrackEmsgHandler playerTrackEmsgHandler;\r\n\r\n    protected final RepresentationHolder[] representationHolders;\r\n\r\n    private TrackSelection trackSelection;\r\n    private FormatSelector formatSelector;\r\n    private SabrManifest manifest;\r\n    private int periodIndex;\r\n    private IOException fatalError;\r\n    private boolean missingLastSegment;\r\n    private long liveEdgeTimeUs;\r\n\r\n    private final SabrStream sabrStream;\r\n    private final Map<String, String> sabrHeaders;\r\n    private int nexChunkIdx = -1;\r\n\r\n    /**\r\n     * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.\r\n     * @param manifest The initial manifest.\r\n     * @param periodIndex The index of the period in the manifest.\r\n     * @param adaptationSetIndices The indices of the adaptation sets in the period.\r\n     * @param trackSelection The track selection.\r\n     * @param trackType The type of the tracks in the selection.\r\n     * @param dataSource A {@link DataSource} suitable for loading the media data.\r\n     * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between\r\n     *     server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified\r\n     *     as the server's unix time minus the local elapsed time. If unknown, set to 0.\r\n     * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. Note\r\n     *     that segments will only be combined if their {@link Uri}s are the same and if their data\r\n     *     ranges are adjacent.\r\n     * @param enableEventMessageTrack Whether to output an event message track.\r\n     * @param closedCaptionFormats The {@link Format Formats} of closed caption tracks to be output.\r\n     * @param playerTrackEmsgHandler The {@link PlayerTrackEmsgHandler} instance to handle emsg\r\n     *     messages targeting the player. Maybe null if this is not necessary.\r\n     */\r\n    public DefaultSabrChunkSource(\r\n            LoaderErrorThrower manifestLoaderErrorThrower,\r\n            SabrManifest manifest,\r\n            int periodIndex,\r\n            int[] adaptationSetIndices,\r\n            TrackSelection trackSelection,\r\n            int trackType,\r\n            DataSource dataSource,\r\n            long elapsedRealtimeOffsetMs,\r\n            int maxSegmentsPerLoad,\r\n            boolean enableEventMessageTrack,\r\n            List<Format> closedCaptionFormats,\r\n            @Nullable PlayerTrackEmsgHandler playerTrackEmsgHandler) {\r\n        this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;\r\n        this.manifest = manifest;\r\n        this.adaptationSetIndices = adaptationSetIndices;\r\n        this.trackSelection = trackSelection;\r\n        this.formatSelector = createFormatSelector(trackType, trackSelection);\r\n        this.trackType = trackType;\r\n        this.dataSource = dataSource;\r\n        this.periodIndex = periodIndex;\r\n        this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;\r\n        this.maxSegmentsPerLoad = maxSegmentsPerLoad;\r\n        this.playerTrackEmsgHandler = playerTrackEmsgHandler;\r\n\r\n        long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);\r\n        liveEdgeTimeUs = C.TIME_UNSET;\r\n        \r\n        this.sabrStream = manifest.getSabrStream(trackType);\r\n        //this.sabrStream.setAudioSelection(createAudioSelection(trackType, trackSelection));\r\n        //this.sabrStream.setVideoSelection(createVideoSelection(trackType, trackSelection));\r\n        //this.sabrStream.setCaptionSelection(createCaptionSelection(trackType, trackSelection));\r\n        this.sabrStream.setFormatSelector(formatSelector);\r\n\r\n        sabrHeaders = new HashMap<>();\r\n        sabrHeaders.put(\"Content-Type\", \"application/x-protobuf\");\r\n        //sabrHeaders.put(\"Accept-Encoding\", \"identity\");\r\n        sabrHeaders.put(\"Accept\", \"application/vnd.yt-ump\");\r\n\r\n        List<Representation> representations = getRepresentations();\r\n        representationHolders = new RepresentationHolder[trackSelection.length()];\r\n        for (int i = 0; i < representationHolders.length; i++) {\r\n            Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));\r\n            representationHolders[i] =\r\n                    new RepresentationHolder(\r\n                            periodDurationUs,\r\n                            trackType,\r\n                            representation,\r\n                            enableEventMessageTrack,\r\n                            closedCaptionFormats,\r\n                            playerTrackEmsgHandler,\r\n                            sabrStream);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void updateManifest(SabrManifest newManifest, int newPeriodIndex) {\r\n        try {\r\n            manifest = newManifest;\r\n            periodIndex = newPeriodIndex;\r\n            long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);\r\n            List<Representation> representations = getRepresentations();\r\n            for (int i = 0; i < representationHolders.length; i++) {\r\n                Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));\r\n                representationHolders[i] =\r\n                        representationHolders[i].copyWithNewRepresentation(periodDurationUs, representation);\r\n            }\r\n        } catch (BehindLiveWindowException e) {\r\n            fatalError = e;\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public void updateTrackSelection(TrackSelection trackSelection) {\r\n        this.trackSelection = trackSelection;\r\n        this.formatSelector = createFormatSelector(trackType, trackSelection);\r\n    }\r\n\r\n    @Override\r\n    public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\r\n        // Segments are aligned across representations, so any segment index will do.\r\n        //for (RepresentationHolder representationHolder : representationHolders) {\r\n        //    if (representationHolder.segmentIndex != null) {\r\n        //        long segmentNum = representationHolder.getSegmentNum(positionUs);\r\n        //        long firstSyncUs = representationHolder.getSegmentStartTimeUs(segmentNum);\r\n        //        long secondSyncUs =\r\n        //                firstSyncUs < positionUs && segmentNum < representationHolder.getSegmentCount() - 1\r\n        //                        ? representationHolder.getSegmentStartTimeUs(segmentNum + 1)\r\n        //                        : firstSyncUs;\r\n        //        return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs);\r\n        //    }\r\n        //}\r\n        // We don't have a segment index to adjust the seek position with yet.\r\n        return positionUs;\r\n    }\r\n\r\n    @Override\r\n    public void maybeThrowError() throws IOException {\r\n        if (fatalError != null) {\r\n            throw fatalError;\r\n        } else {\r\n            manifestLoaderErrorThrower.maybeThrowError();\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {\r\n        if (fatalError != null || trackSelection.length() < 2) {\r\n            return queue.size();\r\n        }\r\n        return trackSelection.evaluateQueueSize(playbackPositionUs, queue);\r\n    }\r\n\r\n    @Override\r\n    public void getNextChunk(long playbackPositionUs, long loadPositionUs, List<? extends MediaChunk> queue, ChunkHolder out) {\r\n        if (fatalError != null) {\r\n            return;\r\n        }\r\n\r\n        long bufferedDurationUs = loadPositionUs - playbackPositionUs;\r\n        long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs);\r\n        long presentationPositionUs =\r\n                C.msToUs(manifest.availabilityStartTimeMs)\r\n                        + C.msToUs(manifest.getPeriod(periodIndex).startMs)\r\n                        + loadPositionUs;\r\n\r\n        if (playerTrackEmsgHandler != null\r\n                && playerTrackEmsgHandler.maybeRefreshManifestBeforeLoadingNextChunk(\r\n                presentationPositionUs)) {\r\n            return;\r\n        }\r\n\r\n        long nowUnixTimeUs = getNowUnixTimeUs();\r\n        MediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1);\r\n        MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()];\r\n        //for (int i = 0; i < chunkIterators.length; i++) {\r\n        //    RepresentationHolder representationHolder = representationHolders[i];\r\n        //    if (representationHolder.segmentIndex == null) {\r\n        //        chunkIterators[i] = MediaChunkIterator.EMPTY;\r\n        //    } else {\r\n        //        long firstAvailableSegmentNum =\r\n        //                representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs);\r\n        //        long lastAvailableSegmentNum =\r\n        //                representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs);\r\n        //        long segmentNum =\r\n        //                getSegmentNum(\r\n        //                        representationHolder,\r\n        //                        previous,\r\n        //                        loadPositionUs,\r\n        //                        firstAvailableSegmentNum,\r\n        //                        lastAvailableSegmentNum);\r\n        //        if (segmentNum < firstAvailableSegmentNum) {\r\n        //            chunkIterators[i] = MediaChunkIterator.EMPTY;\r\n        //        } else {\r\n        //            chunkIterators[i] =\r\n        //                    new RepresentationSegmentIterator(\r\n        //                            representationHolder, segmentNum, lastAvailableSegmentNum);\r\n        //        }\r\n        //    }\r\n        //}\r\n\r\n        trackSelection.updateSelectedTrack(\r\n                playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, chunkIterators);\r\n\r\n        RepresentationHolder representationHolder =\r\n                representationHolders[trackSelection.getSelectedIndex()];\r\n\r\n        if (representationHolder.extractorWrapper != null) {\r\n            Representation selectedRepresentation = representationHolder.representation;\r\n            RangedUri pendingInitializationUri = null;\r\n            RangedUri pendingIndexUri = null;\r\n            if (representationHolder.extractorWrapper.getSampleFormats() == null) {\r\n                pendingInitializationUri = selectedRepresentation.getInitializationUri();\r\n            }\r\n            // No segment index in SABR\r\n            //if (representationHolder.segmentIndex == null) {\r\n            //    pendingIndexUri = selectedRepresentation.getIndexUri();\r\n            //}\r\n            //if (pendingInitializationUri != null || pendingIndexUri != null) {\r\n            if (pendingInitializationUri != null) {\r\n                // We have initialization and/or index requests to make.\r\n                out.chunk = newInitializationChunk(representationHolder, dataSource,\r\n                        trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(),\r\n                        trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri);\r\n                return;\r\n            }\r\n        }\r\n\r\n        long periodDurationUs = representationHolder.periodDurationUs;\r\n        boolean periodEnded = periodDurationUs != C.TIME_UNSET;\r\n\r\n        //if (representationHolder.getSegmentCount() == 0) {\r\n        //    // The index doesn't define any segments.\r\n        //    out.endOfStream = periodEnded;\r\n        //    return;\r\n        //}\r\n\r\n        //long firstAvailableSegmentNum =\r\n        //        representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs);\r\n        //long lastAvailableSegmentNum =\r\n        //        representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs);\r\n\r\n        //updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum);\r\n\r\n        //long segmentNum =\r\n        //        getSegmentNum(\r\n        //                representationHolder,\r\n        //                previous,\r\n        //                loadPositionUs,\r\n        //                firstAvailableSegmentNum,\r\n        //                lastAvailableSegmentNum);\r\n        //if (segmentNum < firstAvailableSegmentNum) {\r\n        //    // This is before the first chunk in the current manifest.\r\n        //    fatalError = new BehindLiveWindowException();\r\n        //    return;\r\n        //}\r\n        //\r\n        //if (segmentNum > lastAvailableSegmentNum\r\n        //        || (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) {\r\n        //    // The segment is beyond the end of the period.\r\n        //    out.endOfStream = periodEnded;\r\n        //    return;\r\n        //}\r\n\r\n        //if (periodEnded && representationHolder.getSegmentStartTimeUs(segmentNum) >= periodDurationUs) {\r\n        //    // The period duration clips the period to a position before the segment.\r\n        //    out.endOfStream = true;\r\n        //    return;\r\n        //}\r\n\r\n        //int maxSegmentCount =\r\n        //        (int) Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1);\r\n        //if (periodDurationUs != C.TIME_UNSET) {\r\n        //    while (maxSegmentCount > 1\r\n        //            && representationHolder.getSegmentStartTimeUs(segmentNum + maxSegmentCount - 1)\r\n        //            >= periodDurationUs) {\r\n        //        // The period duration clips the period to a position before the last segment in the range\r\n        //        // [segmentNum, segmentNum + maxSegmentCount - 1]. Reduce maxSegmentCount.\r\n        //        maxSegmentCount--;\r\n        //    }\r\n        //}\r\n\r\n        long seekTimeUs = queue.isEmpty() ? loadPositionUs : C.TIME_UNSET;\r\n        out.chunk =\r\n                newMediaChunk(\r\n                        representationHolder,\r\n                        dataSource,\r\n                        trackType,\r\n                        trackSelection.getSelectedFormat(),\r\n                        trackSelection.getSelectionReason(),\r\n                        trackSelection.getSelectionData(),\r\n                        C.INDEX_UNSET, // TODO: does sabr has segment num?\r\n                        //segmentNum,\r\n                        //maxSegmentCount,\r\n                        seekTimeUs);\r\n    }\r\n\r\n    @Override\r\n    public void onChunkLoadCompleted(Chunk chunk) {\r\n        if (chunk instanceof InitializationChunk) {\r\n            InitializationChunk initializationChunk = (InitializationChunk) chunk;\r\n            int trackIndex = trackSelection.indexOf(initializationChunk.trackFormat);\r\n            RepresentationHolder representationHolder = representationHolders[trackIndex];\r\n            // The null check avoids overwriting an index obtained from the manifest with one obtained\r\n            // from the stream. If the manifest defines an index then the stream shouldn't, but in cases\r\n            // where it does we should ignore it.\r\n            //if (representationHolder.segmentIndex == null) {\r\n            //    SeekMap seekMap = representationHolder.extractorWrapper.getSeekMap();\r\n            //    if (seekMap != null) {\r\n            //        representationHolders[trackIndex] =\r\n            //                representationHolder.copyWithNewSegmentIndex(\r\n            //                        new SabrWrappingSegmentIndex(\r\n            //                                (ChunkIndex) seekMap,\r\n            //                                representationHolder.representation.presentationTimeOffsetUs));\r\n            //    }\r\n            //}\r\n        }\r\n        if (playerTrackEmsgHandler != null) {\r\n            playerTrackEmsgHandler.onChunkLoadCompleted(chunk);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e, long blacklistDurationMs) {\r\n        Log.e(TAG, \"Chunk load failed: \" + e.getMessage());\r\n        if (!cancelable) {\r\n            return false;\r\n        }\r\n        if (playerTrackEmsgHandler != null\r\n                && playerTrackEmsgHandler.maybeRefreshManifestOnLoadingError(chunk)) {\r\n            return true;\r\n        }\r\n        // Workaround for missing segment at the end of the period\r\n        //if (!manifest.dynamic && chunk instanceof MediaChunk\r\n        //        && e instanceof InvalidResponseCodeException\r\n        //        && ((InvalidResponseCodeException) e).responseCode == 404) {\r\n        //    RepresentationHolder representationHolder =\r\n        //            representationHolders[trackSelection.indexOf(chunk.trackFormat)];\r\n        //    int segmentCount = representationHolder.getSegmentCount();\r\n        //    if (segmentCount != SabrSegmentIndex.INDEX_UNBOUNDED && segmentCount != 0) {\r\n        //        long lastAvailableSegmentNum = representationHolder.getFirstSegmentNum() + segmentCount - 1;\r\n        //        if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) {\r\n        //            missingLastSegment = true;\r\n        //            return true;\r\n        //        }\r\n        //    }\r\n        //}\r\n        return blacklistDurationMs != C.TIME_UNSET\r\n                && trackSelection.blacklist(trackSelection.indexOf(chunk.trackFormat), blacklistDurationMs);\r\n    }\r\n\r\n    private ArrayList<Representation> getRepresentations() {\r\n        List<AdaptationSet> manifestAdaptationSets = manifest.getPeriod(periodIndex).adaptationSets;\r\n        ArrayList<Representation> representations = new ArrayList<>();\r\n        for (int adaptationSetIndex : adaptationSetIndices) {\r\n            representations.addAll(manifestAdaptationSets.get(adaptationSetIndex).representations);\r\n        }\r\n        return representations;\r\n    }\r\n\r\n    private long resolveTimeToLiveEdgeUs(long playbackPositionUs) {\r\n        boolean resolveTimeToLiveEdgePossible = manifest.dynamic && liveEdgeTimeUs != C.TIME_UNSET;\r\n        return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET;\r\n    }\r\n\r\n    private long getNowUnixTimeUs() {\r\n        if (elapsedRealtimeOffsetMs != 0) {\r\n            return (SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs) * 1000;\r\n        } else {\r\n            return System.currentTimeMillis() * 1000;\r\n        }\r\n    }\r\n\r\n    //private long getSegmentNum(\r\n    //        RepresentationHolder representationHolder,\r\n    //        @Nullable MediaChunk previousChunk,\r\n    //        long loadPositionUs,\r\n    //        long firstAvailableSegmentNum,\r\n    //        long lastAvailableSegmentNum) {\r\n    //    return previousChunk != null\r\n    //            ? previousChunk.getNextChunkIndex()\r\n    //            : Util.constrainValue(\r\n    //            representationHolder.getSegmentNum(loadPositionUs),\r\n    //            firstAvailableSegmentNum,\r\n    //            lastAvailableSegmentNum);\r\n    //}\r\n\r\n    //private void updateLiveEdgeTimeUs(\r\n    //        RepresentationHolder representationHolder, long lastAvailableSegmentNum) {\r\n    //    liveEdgeTimeUs = manifest.dynamic\r\n    //            ? representationHolder.getSegmentEndTimeUs(lastAvailableSegmentNum) : C.TIME_UNSET;\r\n    //}\r\n\r\n    protected Chunk newInitializationChunk(\r\n            RepresentationHolder representationHolder,\r\n            DataSource dataSource,\r\n            Format trackFormat,\r\n            int trackSelectionReason,\r\n            Object trackSelectionData,\r\n            RangedUri initializationUri,\r\n            RangedUri indexUri) {\r\n        //RangedUri requestUri;\r\n        //String baseUrl = representationHolder.representation.baseUrl;\r\n        //baseUrl = Utils.updateQuery(baseUrl, \"rn\", sabrStream.getIncSabrRequestNumber());\r\n        //if (initializationUri != null) {\r\n        //    // It's common for initialization and index data to be stored adjacently. Attempt to merge\r\n        //    // the two requests together to request both at once.\r\n        //    requestUri = initializationUri.attemptMerge(indexUri, baseUrl);\r\n        //    if (requestUri == null) {\r\n        //        requestUri = initializationUri;\r\n        //    }\r\n        //} else {\r\n        //    requestUri = indexUri;\r\n        //}\r\n        // NOTE: first protobuf request (before the video start off)\r\n        // MOD: add protobuf data\r\n        //DataSpec dataSpec = new DataSpec(requestUri.resolveUri(baseUrl), requestUri.start,\r\n        //        requestUri.length, representationHolder.representation.getCacheKey());\r\n        DataSpec dataSpec = new DataSpec(\r\n                //Uri.parse(sabrStream.getRequestUrl()),\r\n                Uri.parse(manifest.getRequestUrl(trackType)),\r\n                DataSpec.HTTP_METHOD_POST,\r\n                //sabrStream.createVideoPlaybackAbrRequest(trackType, true).toByteArray(),\r\n                manifest.createVideoPlaybackAbrRequest(trackType, true).toByteArray(),\r\n                0, 0, C.LENGTH_UNSET,\r\n                //requestUri.start,\r\n                //requestUri.start,\r\n                //requestUri.length,\r\n                representationHolder.representation.getCacheKey(),\r\n                0,\r\n                sabrHeaders);\r\n        Log.e(TAG, \"Load init chunk: track=\" + trackType + \", rn=\" + manifest.getSabrRequestNumber());\r\n        return new InitializationChunk(dataSource, dataSpec, trackFormat,\r\n                trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper);\r\n    }\r\n\r\n    protected Chunk newMediaChunk(\r\n            RepresentationHolder representationHolder,\r\n            DataSource dataSource,\r\n            int trackType,\r\n            Format trackFormat,\r\n            int trackSelectionReason,\r\n            Object trackSelectionData,\r\n            long firstSegmentNum,\r\n            //int maxSegmentCount,\r\n            long seekTimeUs) {\r\n        boolean isInit = nexChunkIdx == -1;\r\n        FormatId formatId = formatSelector.getSelectedFormatId();\r\n        int iTag = formatId != null ? formatId.getItag() : -1;\r\n\r\n        if (nexChunkIdx == -1) {\r\n            sabrStream.reset(iTag);\r\n        }\r\n\r\n        nexChunkIdx++;\r\n\r\n        Representation representation = representationHolder.representation;\r\n\r\n        //long startTimeMs = sabrStream.getSegmentStartTimeMs(trackType);\r\n        //long durationMs = sabrStream.getSegmentDurationMs(trackType);\r\n        long startTimeMs = sabrStream.getSegmentStartTimeMs(iTag);\r\n        long durationMs = sabrStream.getSegmentDurationMs(iTag);\r\n        boolean isSabrTimeSet = startTimeMs >= 0 && durationMs > 0;\r\n\r\n        long startTimeUs = isSabrTimeSet ? startTimeMs * 1_000L : C.TIME_UNSET;\r\n        long endTimeUs = isSabrTimeSet ? startTimeUs + (durationMs * 1_000L) : C.TIME_UNSET;\r\n        long clippedEndTimeUs = C.TIME_UNSET;\r\n        int segmentCount = C.INDEX_UNSET; // TODO: replace with SABR value?\r\n        //long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum);\r\n        //RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum);\r\n        //String baseUrl = representation.baseUrl;\r\n        //baseUrl = Utils.updateQuery(baseUrl, \"rn\", sabrStream.getIncSabrRequestNumber());\r\n\r\n        DataSpec dataSpec = new DataSpec(\r\n                //Uri.parse(sabrStream.getRequestUrl()),\r\n                Uri.parse(manifest.getRequestUrl(trackType)),\r\n                //segmentUri.resolveUri(baseUrl),\r\n                DataSpec.HTTP_METHOD_POST,\r\n                //sabrStream.createVideoPlaybackAbrRequest(trackType, false).toByteArray(),\r\n                manifest.createVideoPlaybackAbrRequest(trackType, false).toByteArray(),\r\n                0, 0, C.LENGTH_UNSET,\r\n                //segmentUri.start,\r\n                //segmentUri.start,\r\n                //segmentUri.length,\r\n                representation.getCacheKey(),\r\n                0,\r\n                sabrHeaders);\r\n        long sampleOffsetUs = -representation.presentationTimeOffsetUs;\r\n        Log.e(TAG, \"Load media chunk: track=\" + trackType + \", rn=\" + manifest.getSabrRequestNumber() + \", startTimeMs=\" + startTimeMs);\r\n        return new ContainerMediaChunk(\r\n                dataSource,\r\n                dataSpec,\r\n                trackFormat,\r\n                trackSelectionReason,\r\n                trackSelectionData,\r\n                startTimeUs,\r\n                endTimeUs,\r\n                seekTimeUs,\r\n                clippedEndTimeUs,\r\n                firstSegmentNum,\r\n                segmentCount,\r\n                sampleOffsetUs,\r\n                representationHolder.extractorWrapper);\r\n\r\n        //if (representationHolder.extractorWrapper == null) {\r\n        //    long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum);\r\n        //    DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl),\r\n        //            segmentUri.start, segmentUri.length, representation.getCacheKey());\r\n        //    return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason,\r\n        //            trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackType, trackFormat);\r\n        //} else {\r\n        //    int segmentCount = 1;\r\n        //    for (int i = 1; i < maxSegmentCount; i++) {\r\n        //        RangedUri nextSegmentUri = representationHolder.getSegmentUrl(firstSegmentNum + i);\r\n        //        RangedUri mergedSegmentUri = segmentUri.attemptMerge(nextSegmentUri, baseUrl);\r\n        //        if (mergedSegmentUri == null) {\r\n        //            // Unable to merge segment fetches because the URIs do not merge.\r\n        //            break;\r\n        //        }\r\n        //        segmentUri = mergedSegmentUri;\r\n        //        segmentCount++;\r\n        //    }\r\n        //    long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum + segmentCount - 1);\r\n        //    long periodDurationUs = representationHolder.periodDurationUs;\r\n        //    long clippedEndTimeUs =\r\n        //            periodDurationUs != C.TIME_UNSET && periodDurationUs <= endTimeUs\r\n        //                    ? periodDurationUs\r\n        //                    : C.TIME_UNSET;\r\n        //    // NOTE: next protobuf requests (during the playback)\r\n        //    // MOD: add protobuf data\r\n        //    //DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl),\r\n        //    //        segmentUri.start, segmentUri.length, representation.getCacheKey());\r\n        //    int sabrRequestNumber = sabrStream.getIncSabrRequestNumber();\r\n        //    DataSpec dataSpec = new DataSpec(\r\n        //            segmentUri.resolveUri(baseUrl),\r\n        //            DataSpec.HTTP_METHOD_POST,\r\n        //            sabrStream.buildInitVideoPlaybackAbrRequest(trackType).toByteArray(),\r\n        //            0, 0, C.LENGTH_UNSET,\r\n        //            //segmentUri.start,\r\n        //            //segmentUri.start,\r\n        //            //segmentUri.length,\r\n        //            representation.getCacheKey(),\r\n        //            0,\r\n        //            sabrHeaders);\r\n        //    long sampleOffsetUs = -representation.presentationTimeOffsetUs;\r\n        //    return new ContainerMediaChunk(\r\n        //            dataSource,\r\n        //            dataSpec,\r\n        //            trackFormat,\r\n        //            trackSelectionReason,\r\n        //            trackSelectionData,\r\n        //            startTimeUs,\r\n        //            endTimeUs,\r\n        //            seekTimeUs,\r\n        //            clippedEndTimeUs,\r\n        //            firstSegmentNum,\r\n        //            segmentCount,\r\n        //            sampleOffsetUs,\r\n        //            representationHolder.extractorWrapper);\r\n        //}\r\n    }\r\n\r\n    private static AudioSelector createAudioSelection(int trackType, TrackSelection trackSelection) {\r\n        if (trackType != C.TRACK_TYPE_AUDIO) {\r\n            return null;\r\n        }\r\n\r\n        Format selectedFormat = trackSelection.getSelectedFormat();\r\n\r\n        return new AudioSelector(\"selected_audio\", false, selectedFormat);\r\n    }\r\n\r\n    private static VideoSelector createVideoSelection(int trackType, TrackSelection trackSelection) {\r\n        if (trackType != C.TRACK_TYPE_VIDEO) {\r\n            return null;\r\n        }\r\n\r\n        Format selectedFormat = trackSelection.getSelectedFormat();\r\n\r\n        return new VideoSelector(\"selected_video\", false, selectedFormat);\r\n    }\r\n\r\n    private static CaptionSelector createCaptionSelection(int trackType, TrackSelection trackSelection) {\r\n        if (trackType != C.TRACK_TYPE_TEXT) {\r\n            return null;\r\n        }\r\n\r\n        Format selectedFormat = trackSelection.getSelectedFormat();\r\n\r\n        return new CaptionSelector(\"selected_caption\", false, selectedFormat);\r\n    }\r\n\r\n    private static FormatSelector createFormatSelector(int trackType, TrackSelection trackSelection) {\r\n        Format selectedFormat = trackSelection.getSelectedFormat();\r\n\r\n        switch (trackType) {\r\n            case C.TRACK_TYPE_AUDIO:\r\n                return new FormatSelector(\"selected_audio\", false, selectedFormat);\r\n            case C.TRACK_TYPE_VIDEO:\r\n                return new FormatSelector(\"selected_video\", false, selectedFormat);\r\n            case C.TRACK_TYPE_TEXT:\r\n                return new FormatSelector(\"selected_caption\", false, selectedFormat);\r\n        }\r\n\r\n        throw new IllegalStateException(\"Unknown track type\");\r\n    }\r\n\r\n    /** {@link MediaChunkIterator} wrapping a {@link RepresentationHolder}. */\r\n    //protected static final class RepresentationSegmentIterator extends BaseMediaChunkIterator {\r\n    //\r\n    //    private final RepresentationHolder representationHolder;\r\n    //\r\n    //    /**\r\n    //     * Creates iterator.\r\n    //     *\r\n    //     * @param representation The {@link RepresentationHolder} to wrap.\r\n    //     * @param firstAvailableSegmentNum The number of the first available segment.\r\n    //     * @param lastAvailableSegmentNum The number of the last available segment.\r\n    //     */\r\n    //    public RepresentationSegmentIterator(\r\n    //            RepresentationHolder representation,\r\n    //            long firstAvailableSegmentNum,\r\n    //            long lastAvailableSegmentNum) {\r\n    //        super(/* fromIndex= */ firstAvailableSegmentNum, /* toIndex= */ lastAvailableSegmentNum);\r\n    //        this.representationHolder = representation;\r\n    //    }\r\n    //\r\n    //    @Override\r\n    //    public DataSpec getDataSpec() {\r\n    //        checkInBounds();\r\n    //        Representation representation = representationHolder.representation;\r\n    //        RangedUri segmentUri = representationHolder.getSegmentUrl(getCurrentIndex());\r\n    //        Uri resolvedUri = segmentUri.resolveUri(representation.baseUrl);\r\n    //        String cacheKey = representation.getCacheKey();\r\n    //        return new DataSpec(resolvedUri, segmentUri.start, segmentUri.length, cacheKey);\r\n    //    }\r\n    //\r\n    //    @Override\r\n    //    public long getChunkStartTimeUs() {\r\n    //        checkInBounds();\r\n    //        return representationHolder.getSegmentStartTimeUs(getCurrentIndex());\r\n    //    }\r\n    //\r\n    //    @Override\r\n    //    public long getChunkEndTimeUs() {\r\n    //        checkInBounds();\r\n    //        return representationHolder.getSegmentEndTimeUs(getCurrentIndex());\r\n    //    }\r\n    //}\r\n\r\n    /** Holds information about a snapshot of a single {@link Representation}. */\r\n    protected static final class RepresentationHolder {\r\n\r\n        /* package */ final @Nullable ChunkExtractorWrapper extractorWrapper;\r\n\r\n        public final Representation representation;\r\n        //public final @Nullable SabrSegmentIndex segmentIndex;\r\n\r\n        private final long periodDurationUs;\r\n        private final long segmentNumShift;\r\n\r\n        /* package */ RepresentationHolder(\r\n                long periodDurationUs,\r\n                int trackType,\r\n                Representation representation,\r\n                boolean enableEventMessageTrack,\r\n                List<Format> closedCaptionFormats,\r\n                TrackOutput playerEmsgTrackOutput,\r\n                SabrStream sabrStream) {\r\n            this(\r\n                    periodDurationUs,\r\n                    representation,\r\n                    createExtractorWrapper(\r\n                            trackType,\r\n                            representation,\r\n                            enableEventMessageTrack,\r\n                            closedCaptionFormats,\r\n                            playerEmsgTrackOutput,\r\n                            sabrStream),\r\n                    /* segmentNumShift= */ 0\r\n                    //representation.getIndex()\r\n            );\r\n        }\r\n\r\n        private RepresentationHolder(\r\n                long periodDurationUs,\r\n                Representation representation,\r\n                @Nullable ChunkExtractorWrapper extractorWrapper,\r\n                long segmentNumShift\r\n                //@Nullable SabrSegmentIndex segmentIndex\r\n        ) {\r\n            this.periodDurationUs = periodDurationUs;\r\n            this.representation = representation;\r\n            this.segmentNumShift = segmentNumShift;\r\n            this.extractorWrapper = extractorWrapper;\r\n            //this.segmentIndex = segmentIndex;\r\n        }\r\n\r\n        @CheckResult\r\n            /* package */ RepresentationHolder copyWithNewRepresentation(\r\n                long newPeriodDurationUs, Representation newRepresentation)\r\n                throws BehindLiveWindowException {\r\n\r\n            return new RepresentationHolder(\r\n                    newPeriodDurationUs, newRepresentation, extractorWrapper, segmentNumShift);\r\n\r\n            //SabrSegmentIndex oldIndex = representation.getIndex();\r\n            //SabrSegmentIndex newIndex = newRepresentation.getIndex();\r\n\r\n            //if (oldIndex == null) {\r\n            //    // Segment numbers cannot shift if the index isn't defined by the manifest.\r\n            //    return new RepresentationHolder(\r\n            //            newPeriodDurationUs, newRepresentation, extractorWrapper, segmentNumShift, oldIndex);\r\n            //}\r\n            //\r\n            //if (!oldIndex.isExplicit()) {\r\n            //    // Segment numbers cannot shift if the index isn't explicit.\r\n            //    return new RepresentationHolder(\r\n            //            newPeriodDurationUs, newRepresentation, extractorWrapper, segmentNumShift, newIndex);\r\n            //}\r\n            //\r\n            //int oldIndexSegmentCount = oldIndex.getSegmentCount(newPeriodDurationUs);\r\n            //if (oldIndexSegmentCount == 0) {\r\n            //    // Segment numbers cannot shift if the old index was empty.\r\n            //    return new RepresentationHolder(\r\n            //            newPeriodDurationUs, newRepresentation, extractorWrapper, segmentNumShift, newIndex);\r\n            //}\r\n            //\r\n            //long oldIndexFirstSegmentNum = oldIndex.getFirstSegmentNum();\r\n            //long oldIndexStartTimeUs = oldIndex.getTimeUs(oldIndexFirstSegmentNum);\r\n            //long oldIndexLastSegmentNum = oldIndexFirstSegmentNum + oldIndexSegmentCount - 1;\r\n            //long oldIndexEndTimeUs =\r\n            //        oldIndex.getTimeUs(oldIndexLastSegmentNum)\r\n            //                + oldIndex.getDurationUs(oldIndexLastSegmentNum, newPeriodDurationUs);\r\n            //long newIndexFirstSegmentNum = newIndex.getFirstSegmentNum();\r\n            //long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum);\r\n            //long newSegmentNumShift = segmentNumShift;\r\n            //if (oldIndexEndTimeUs == newIndexStartTimeUs) {\r\n            //    // The new index continues where the old one ended, with no overlap.\r\n            //    newSegmentNumShift += oldIndexLastSegmentNum + 1 - newIndexFirstSegmentNum;\r\n            //} else if (oldIndexEndTimeUs < newIndexStartTimeUs) {\r\n            //    // There's a gap between the old index and the new one which means we've slipped behind the\r\n            //    // live window and can't proceed.\r\n            //    throw new BehindLiveWindowException();\r\n            //} else if (newIndexStartTimeUs < oldIndexStartTimeUs) {\r\n            //    // The new index overlaps with (but does not have a start position contained within) the old\r\n            //    // index. This can only happen if extra segments have been added to the start of the index.\r\n            //    newSegmentNumShift -=\r\n            //            newIndex.getSegmentNum(oldIndexStartTimeUs, newPeriodDurationUs)\r\n            //                    - oldIndexFirstSegmentNum;\r\n            //} else {\r\n            //    // The new index overlaps with (and has a start position contained within) the old index.\r\n            //    newSegmentNumShift +=\r\n            //            oldIndex.getSegmentNum(newIndexStartTimeUs, newPeriodDurationUs)\r\n            //                    - newIndexFirstSegmentNum;\r\n            //}\r\n            //return new RepresentationHolder(\r\n            //        newPeriodDurationUs, newRepresentation, extractorWrapper, newSegmentNumShift, newIndex);\r\n        }\r\n\r\n        //@CheckResult\r\n        //    /* package */ RepresentationHolder copyWithNewSegmentIndex(SabrSegmentIndex segmentIndex) {\r\n        //    return new RepresentationHolder(\r\n        //            periodDurationUs, representation, extractorWrapper, segmentNumShift, segmentIndex);\r\n        //}\r\n\r\n        //public long getFirstSegmentNum() {\r\n        //    return segmentIndex.getFirstSegmentNum() + segmentNumShift;\r\n        //}\r\n        //\r\n        //public int getSegmentCount() {\r\n        //    return segmentIndex.getSegmentCount(periodDurationUs);\r\n        //}\r\n        //\r\n        //public long getSegmentStartTimeUs(long segmentNum) {\r\n        //    return segmentIndex.getTimeUs(segmentNum - segmentNumShift);\r\n        //}\r\n        //\r\n        //public long getSegmentEndTimeUs(long segmentNum) {\r\n        //    return getSegmentStartTimeUs(segmentNum)\r\n        //            + segmentIndex.getDurationUs(segmentNum - segmentNumShift, periodDurationUs);\r\n        //}\r\n        //\r\n        //public long getSegmentNum(long positionUs) {\r\n        //    return segmentIndex.getSegmentNum(positionUs, periodDurationUs) + segmentNumShift;\r\n        //}\r\n        //\r\n        //public RangedUri getSegmentUrl(long segmentNum) {\r\n        //    return segmentIndex.getSegmentUrl(segmentNum - segmentNumShift);\r\n        //}\r\n\r\n        //public long getFirstAvailableSegmentNum(\r\n        //        SabrManifest manifest, int periodIndex, long nowUnixTimeUs) {\r\n        //    if (getSegmentCount() == SabrSegmentIndex.INDEX_UNBOUNDED\r\n        //            && manifest.timeShiftBufferDepthMs != C.TIME_UNSET) {\r\n        //        // The index is itself unbounded. We need to use the current time to calculate the range of\r\n        //        // available segments.\r\n        //        long liveEdgeTimeUs = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs);\r\n        //        long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs);\r\n        //        long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs;\r\n        //        long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs);\r\n        //        return Math.max(\r\n        //                getFirstSegmentNum(), getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs));\r\n        //    }\r\n        //    return getFirstSegmentNum();\r\n        //}\r\n        //\r\n        //public long getLastAvailableSegmentNum(\r\n        //        SabrManifest manifest, int periodIndex, long nowUnixTimeUs) {\r\n        //    int availableSegmentCount = getSegmentCount();\r\n        //    if (availableSegmentCount == SabrSegmentIndex.INDEX_UNBOUNDED) {\r\n        //        // The index is itself unbounded. We need to use the current time to calculate the range of\r\n        //        // available segments.\r\n        //        long liveEdgeTimeUs = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs);\r\n        //        long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs);\r\n        //        long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs;\r\n        //        // getSegmentNum(liveEdgeTimeInPeriodUs) will not be completed yet, so subtract one to get\r\n        //        // the index of the last completed segment.\r\n        //        return getSegmentNum(liveEdgeTimeInPeriodUs) - 1;\r\n        //    }\r\n        //    return getFirstSegmentNum() + availableSegmentCount - 1;\r\n        //}\r\n\r\n        private static boolean mimeTypeIsWebm(String mimeType) {\r\n            return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM)\r\n                    || mimeType.startsWith(MimeTypes.APPLICATION_WEBM);\r\n        }\r\n\r\n        private static boolean mimeTypeIsRawText(String mimeType) {\r\n            return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType);\r\n        }\r\n\r\n        private static @Nullable ChunkExtractorWrapper createExtractorWrapper(\r\n                int trackType,\r\n                Representation representation,\r\n                boolean enableEventMessageTrack,\r\n                List<Format> closedCaptionFormats,\r\n                TrackOutput playerEmsgTrackOutput,\r\n                SabrStream sabrStream) {\r\n            String containerMimeType = representation.format.containerMimeType;\r\n            if (containerMimeType == null || mimeTypeIsRawText(containerMimeType)) {\r\n                return null;\r\n            }\r\n\r\n            // MOD: replaced with SabrExtractor\r\n            //Extractor extractor;\r\n            //if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {\r\n            //    extractor = new RawCcExtractor(representation.format);\r\n            //} else if (mimeTypeIsWebm(containerMimeType)) {\r\n            //    extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES);\r\n            //} else {\r\n            //    int flags = 0;\r\n            //    if (enableEventMessageTrack) {\r\n            //        flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK;\r\n            //    }\r\n            //    extractor =\r\n            //            new FragmentedMp4Extractor(\r\n            //                    flags, null, null, null, closedCaptionFormats, playerEmsgTrackOutput);\r\n            //}\r\n\r\n            //Extractor extractor = new SabrExtractor(trackType, representation.format, sabrStream);\r\n\r\n            Extractor extractor;\r\n            if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {\r\n                extractor = new RawCcExtractor(representation.format);\r\n            } else if (mimeTypeIsWebm(containerMimeType)) {\r\n                extractor = new SabrMatroskaAdapter(SabrMatroskaAdapter.FLAG_DISABLE_SEEK_FOR_CUES, sabrStream);\r\n            } else {\r\n                int flags = 0;\r\n                if (enableEventMessageTrack) {\r\n                    flags |= SabrFragmentedMp4Adapter.FLAG_ENABLE_EMSG_TRACK;\r\n                }\r\n                extractor =\r\n                        new SabrFragmentedMp4Adapter(\r\n                                flags, null, null, null, closedCaptionFormats, playerEmsgTrackOutput, sabrStream);\r\n            }\r\n\r\n            // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream,\r\n            // as per DASH IF Interoperability Recommendations V3.0, 7.5.3.\r\n            return new ChunkExtractorWrapper(extractor, trackType, representation.format);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/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.sabr;\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.sabr.manifest.EventStream;\nimport com.google.android.exoplayer2.util.Util;\n\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.setFlags(C.BUFFER_FLAG_KEY_FRAME);\n      buffer.data.put(serializedEvent);\n      buffer.timeUs = eventTimesUs[sampleIndex];\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-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/PlayerEmsgHandler.java",
    "content": "package com.google.android.exoplayer2.source.sabr;\r\n\r\nimport android.os.Handler;\r\nimport android.os.Message;\r\n\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.FormatHolder;\r\nimport com.google.android.exoplayer2.ParserException;\r\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\r\nimport com.google.android.exoplayer2.extractor.TrackOutput;\r\nimport com.google.android.exoplayer2.metadata.Metadata;\r\nimport com.google.android.exoplayer2.metadata.MetadataInputBuffer;\r\nimport com.google.android.exoplayer2.metadata.emsg.EventMessage;\r\nimport com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;\r\nimport com.google.android.exoplayer2.source.SampleQueue;\r\nimport com.google.android.exoplayer2.source.chunk.Chunk;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.SabrManifest;\r\nimport com.google.android.exoplayer2.upstream.Allocator;\r\nimport com.google.android.exoplayer2.util.ParsableByteArray;\r\nimport com.google.android.exoplayer2.util.Util;\r\n\r\nimport java.io.IOException;\r\nimport java.util.Iterator;\r\nimport java.util.Map;\r\nimport java.util.Map.Entry;\r\nimport java.util.TreeMap;\r\n\r\npublic final class PlayerEmsgHandler implements Handler.Callback {\r\n    /** Callbacks for player emsg events encountered during DASH live stream. */\r\n    public interface PlayerEmsgCallback {\r\n\r\n        /** Called when the current manifest should be refreshed. */\r\n        void onDashManifestRefreshRequested();\r\n\r\n        /**\r\n         * Called when the manifest with the publish time has been expired.\r\n         *\r\n         * @param expiredManifestPublishTimeUs The manifest publish time that has been expired.\r\n         */\r\n        void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs);\r\n    }\r\n\r\n    private final Allocator allocator;\r\n    private final PlayerEmsgCallback playerEmsgCallback;\r\n    private final EventMessageDecoder decoder;\r\n    private SabrManifest manifest;\r\n    private final Handler handler;\r\n    private final TreeMap<Long, Long> manifestPublishTimeToExpiryTimeUs;\r\n\r\n    private long expiredManifestPublishTimeUs;\r\n    private long lastLoadedChunkEndTimeUs;\r\n    private long lastLoadedChunkEndTimeBeforeRefreshUs;\r\n    private boolean isWaitingForManifestRefresh;\r\n    private boolean released;\r\n\r\n    /**\r\n     * @param manifest The initial manifest.\r\n     * @param playerEmsgCallback The callback that this event handler can invoke when handling emsg\r\n     *     messages that generate DASH media source events.\r\n     * @param allocator An {@link Allocator} from which allocations can be obtained.\r\n     */\r\n    public PlayerEmsgHandler(\r\n            SabrManifest manifest, PlayerEmsgCallback playerEmsgCallback, Allocator allocator) {\r\n        this.manifest = manifest;\r\n        this.playerEmsgCallback = playerEmsgCallback;\r\n        this.allocator = allocator;\r\n\r\n        manifestPublishTimeToExpiryTimeUs = new TreeMap<>();\r\n        handler = Util.createHandler(/* callback= */ this);\r\n        decoder = new EventMessageDecoder();\r\n    }\r\n\r\n    /**\r\n     * Updates the {@link SabrManifest} that this handler works on.\r\n     *\r\n     * @param newManifest The updated manifest.\r\n     */\r\n    public void updateManifest(SabrManifest newManifest) {\r\n        isWaitingForManifestRefresh = false;\r\n        expiredManifestPublishTimeUs = C.TIME_UNSET;\r\n        this.manifest = newManifest;\r\n        removePreviouslyExpiredManifestPublishTimeValues();\r\n    }\r\n\r\n    /* package */ boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) {\r\n        if (!manifest.dynamic) {\r\n            return false;\r\n        }\r\n        if (isWaitingForManifestRefresh) {\r\n            return true;\r\n        }\r\n        boolean manifestRefreshNeeded = false;\r\n        // Find the smallest publishTime (greater than or equal to the current manifest's publish time)\r\n        // that has a corresponding expiry time.\r\n        Map.Entry<Long, Long> expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs);\r\n        if (expiredEntry != null) {\r\n            long expiredPointUs = expiredEntry.getValue();\r\n            if (expiredPointUs < presentationPositionUs) {\r\n                expiredManifestPublishTimeUs = expiredEntry.getKey();\r\n                notifyManifestPublishTimeExpired();\r\n                manifestRefreshNeeded = true;\r\n            }\r\n        }\r\n        if (manifestRefreshNeeded) {\r\n            maybeNotifyDashManifestRefreshNeeded();\r\n        }\r\n        return manifestRefreshNeeded;\r\n    }\r\n\r\n    /**\r\n     * For live streaming with emsg event stream, forward seeking can seek pass the emsg messages that\r\n     * signals end-of-stream or Manifest expiry, which results in load error. In this case, we should\r\n     * notify the Dash media source to refresh its manifest.\r\n     *\r\n     * @param chunk The chunk whose load encountered the error.\r\n     * @return True if manifest refresh has been requested, false otherwise.\r\n     */\r\n    /* package */ boolean maybeRefreshManifestOnLoadingError(Chunk chunk) {\r\n        if (!manifest.dynamic) {\r\n            return false;\r\n        }\r\n        if (isWaitingForManifestRefresh) {\r\n            return true;\r\n        }\r\n        boolean isAfterForwardSeek =\r\n                lastLoadedChunkEndTimeUs != C.TIME_UNSET && lastLoadedChunkEndTimeUs < chunk.startTimeUs;\r\n        if (isAfterForwardSeek) {\r\n            // if we are after a forward seek, and the playback is dynamic with embedded emsg stream,\r\n            // there's a chance that we have seek over the emsg messages, in which case we should ask\r\n            // media source for a refresh.\r\n            maybeNotifyDashManifestRefreshNeeded();\r\n            return true;\r\n        }\r\n        return false;\r\n    }\r\n\r\n    /**\r\n     * Called when the a new chunk in the current media stream has been loaded.\r\n     *\r\n     * @param chunk The chunk whose load has been completed.\r\n     */\r\n    /* package */ void onChunkLoadCompleted(Chunk chunk) {\r\n        if (lastLoadedChunkEndTimeUs != C.TIME_UNSET || chunk.endTimeUs > lastLoadedChunkEndTimeUs) {\r\n            lastLoadedChunkEndTimeUs = chunk.endTimeUs;\r\n        }\r\n    }\r\n\r\n    private @Nullable Map.Entry<Long, Long> ceilingExpiryEntryForPublishTime(long publishTimeMs) {\r\n        return manifestPublishTimeToExpiryTimeUs.ceilingEntry(publishTimeMs);\r\n    }\r\n\r\n    private void removePreviouslyExpiredManifestPublishTimeValues() {\r\n        for (Iterator<Entry<Long, Long>> it =\r\n             manifestPublishTimeToExpiryTimeUs.entrySet().iterator();\r\n             it.hasNext(); ) {\r\n            Map.Entry<Long, Long> entry = it.next();\r\n            long expiredManifestPublishTime = entry.getKey();\r\n            if (expiredManifestPublishTime < manifest.publishTimeMs) {\r\n                it.remove();\r\n            }\r\n        }\r\n    }\r\n\r\n    private void notifyManifestPublishTimeExpired() {\r\n        playerEmsgCallback.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs);\r\n    }\r\n\r\n    /** Requests DASH media manifest to be refreshed if necessary. */\r\n    private void maybeNotifyDashManifestRefreshNeeded() {\r\n        if (lastLoadedChunkEndTimeBeforeRefreshUs != C.TIME_UNSET\r\n                && lastLoadedChunkEndTimeBeforeRefreshUs == lastLoadedChunkEndTimeUs) {\r\n            // Already requested manifest refresh.\r\n            return;\r\n        }\r\n        isWaitingForManifestRefresh = true;\r\n        lastLoadedChunkEndTimeBeforeRefreshUs = lastLoadedChunkEndTimeUs;\r\n        playerEmsgCallback.onDashManifestRefreshRequested();\r\n    }\r\n\r\n    /** Returns a {@link TrackOutput} that emsg messages could be written to. */\r\n    public PlayerTrackEmsgHandler newPlayerTrackEmsgHandler() {\r\n        return new PlayerTrackEmsgHandler(new SampleQueue(allocator));\r\n    }\r\n\r\n    /** Release this emsg handler. It should not be reused after this call. */\r\n    public void release() {\r\n        released = true;\r\n    }\r\n\r\n    @Override\r\n    public boolean handleMessage(Message message) {\r\n        if (released) {\r\n            return true;\r\n        }\r\n        return false;\r\n    }\r\n\r\n    /**\r\n     * Returns whether an event with given schemeIdUri and value is a DASH emsg event targeting the\r\n     * player.\r\n     */\r\n    public static boolean isPlayerEmsgEvent(String schemeIdUri, String value) {\r\n        return \"urn:mpeg:sabr:event:2025\".equals(schemeIdUri)\r\n                && (\"1\".equals(value) || \"2\".equals(value) || \"3\".equals(value));\r\n    }\r\n\r\n    /** Handles emsg messages for a specific track for the player. */\r\n    public final class PlayerTrackEmsgHandler implements TrackOutput {\r\n        private final SampleQueue sampleQueue;\r\n        private final FormatHolder formatHolder;\r\n        private final MetadataInputBuffer buffer;\r\n\r\n        public PlayerTrackEmsgHandler(SampleQueue sampleQueue) {\r\n            this.sampleQueue = sampleQueue;\r\n\r\n            formatHolder = new FormatHolder();\r\n            buffer = new MetadataInputBuffer();\r\n        }\r\n\r\n        @Override\r\n        public void format(Format format) {\r\n            sampleQueue.format(format);\r\n        }\r\n\r\n        @Override\r\n        public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) throws IOException, InterruptedException {\r\n            return sampleQueue.sampleData(input, length, allowEndOfInput);\r\n        }\r\n\r\n        @Override\r\n        public void sampleData(ParsableByteArray data, int length) {\r\n            sampleQueue.sampleData(data, length);\r\n        }\r\n\r\n        @Override\r\n        public void sampleMetadata(long timeUs, int flags, int size, int offset, @Nullable CryptoData encryptionData) {\r\n            sampleQueue.sampleMetadata(timeUs, flags, size, offset, encryptionData);\r\n            parseAndDiscardSamples();\r\n        }\r\n\r\n        /**\r\n         * For live streaming, check if the DASH manifest is expired before the next segment start time.\r\n         * If it is, the DASH media source will be notified to refresh the manifest.\r\n         *\r\n         * @param presentationPositionUs The next load position in presentation time.\r\n         * @return True if manifest refresh has been requested, false otherwise.\r\n         */\r\n        public boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) {\r\n            return PlayerEmsgHandler.this.maybeRefreshManifestBeforeLoadingNextChunk(\r\n                    presentationPositionUs);\r\n        }\r\n\r\n        /**\r\n         * Called when the a new chunk in the current media stream has been loaded.\r\n         *\r\n         * @param chunk The chunk whose load has been completed.\r\n         */\r\n        public void onChunkLoadCompleted(Chunk chunk) {\r\n            PlayerEmsgHandler.this.onChunkLoadCompleted(chunk);\r\n        }\r\n\r\n        /**\r\n         * For live streaming with emsg event stream, forward seeking can seek pass the emsg messages\r\n         * that signals end-of-stream or Manifest expiry, which results in load error. In this case, we\r\n         * should notify the Dash media source to refresh its manifest.\r\n         *\r\n         * @param chunk The chunk whose load encountered the error.\r\n         * @return True if manifest refresh has been requested, false otherwise.\r\n         */\r\n        public boolean maybeRefreshManifestOnLoadingError(Chunk chunk) {\r\n            return PlayerEmsgHandler.this.maybeRefreshManifestOnLoadingError(chunk);\r\n        }\r\n\r\n        /** Release this track emsg handler. It should not be reused after this call. */\r\n        public void release() {\r\n            sampleQueue.reset();\r\n        }\r\n\r\n        private void parseAndDiscardSamples() {\r\n            while (sampleQueue.hasNextSample()) {\r\n                MetadataInputBuffer inputBuffer = dequeueSample();\r\n                if (inputBuffer == null) {\r\n                    continue;\r\n                }\r\n                long eventTimeUs = inputBuffer.timeUs;\r\n                Metadata metadata = decoder.decode(inputBuffer);\r\n                if (metadata == null) {\r\n                    continue;\r\n                }\r\n                EventMessage eventMessage = (EventMessage) metadata.get(0);\r\n                if (isPlayerEmsgEvent(eventMessage.schemeIdUri, eventMessage.value)) {\r\n                    parsePlayerEmsgEvent(eventTimeUs, eventMessage);\r\n                }\r\n            }\r\n            sampleQueue.discardToRead();\r\n        }\r\n\r\n        private void parsePlayerEmsgEvent(long eventTimeUs, EventMessage eventMessage) {\r\n            // NOP\r\n        }\r\n\r\n        @Nullable\r\n        private MetadataInputBuffer dequeueSample() {\r\n            buffer.clear();\r\n            int result = sampleQueue.read(formatHolder, buffer, false, false, 0);\r\n            if (result == C.RESULT_BUFFER_READ) {\r\n                buffer.flip();\r\n                return buffer;\r\n            }\r\n            return null;\r\n        }\r\n    }\r\n\r\n    /** Holds information related to a manifest expiry event. */\r\n    private static final class ManifestExpiryEventInfo {\r\n\r\n        public final long eventTimeUs;\r\n        public final long manifestPublishTimeMsInEmsg;\r\n\r\n        public ManifestExpiryEventInfo(long eventTimeUs, long manifestPublishTimeMsInEmsg) {\r\n            this.eventTimeUs = eventTimeUs;\r\n            this.manifestPublishTimeMsInEmsg = manifestPublishTimeMsInEmsg;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/SabrChunkSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.sabr;\n\nimport android.os.SystemClock;\n\nimport androidx.annotation.Nullable;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.chunk.ChunkSource;\nimport com.google.android.exoplayer2.source.sabr.PlayerEmsgHandler.PlayerTrackEmsgHandler;\nimport com.google.android.exoplayer2.source.sabr.manifest.SabrManifest;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.LoaderErrorThrower;\nimport com.google.android.exoplayer2.upstream.TransferListener;\n\nimport java.util.List;\n\n/**\n * An {@link ChunkSource} for DASH streams.\n */\npublic interface SabrChunkSource extends ChunkSource {\n\n  /** Factory for {@link SabrChunkSource}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 SabrChunkSource}.\n     */\n    SabrChunkSource createSabrChunkSource(\n        LoaderErrorThrower manifestLoaderErrorThrower,\n        SabrManifest 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(SabrManifest 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-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/SabrMediaPeriod.java",
    "content": "package com.google.android.exoplayer2.source.sabr;\r\n\r\nimport android.util.Pair;\r\nimport android.util.SparseIntArray;\r\n\r\nimport androidx.annotation.IntDef;\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.SeekParameters;\r\nimport com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;\r\nimport com.google.android.exoplayer2.source.EmptySampleStream;\r\nimport com.google.android.exoplayer2.source.MediaPeriod;\r\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\r\nimport com.google.android.exoplayer2.source.SampleStream;\r\nimport com.google.android.exoplayer2.source.SequenceableLoader;\r\nimport com.google.android.exoplayer2.source.TrackGroup;\r\nimport com.google.android.exoplayer2.source.TrackGroupArray;\r\nimport com.google.android.exoplayer2.source.chunk.ChunkSampleStream;\r\nimport com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream;\r\nimport com.google.android.exoplayer2.source.sabr.PlayerEmsgHandler.PlayerEmsgCallback;\r\nimport com.google.android.exoplayer2.source.sabr.PlayerEmsgHandler.PlayerTrackEmsgHandler;\r\nimport com.google.android.exoplayer2.source.sabr.SabrChunkSource.Factory;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.AdaptationSet;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.EventStream;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.Period;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.Representation;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.SabrManifest;\r\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\r\nimport com.google.android.exoplayer2.upstream.Allocator;\r\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\r\nimport com.google.android.exoplayer2.upstream.LoaderErrorThrower;\r\nimport com.google.android.exoplayer2.upstream.TransferListener;\r\nimport com.google.android.exoplayer2.util.MimeTypes;\r\nimport com.google.android.exoplayer2.util.Util;\r\n\r\nimport java.io.IOException;\r\nimport java.lang.annotation.Documented;\r\nimport java.lang.annotation.Retention;\r\nimport java.lang.annotation.RetentionPolicy;\r\nimport java.util.ArrayList;\r\nimport java.util.Arrays;\r\nimport java.util.IdentityHashMap;\r\nimport java.util.List;\r\nimport java.util.regex.Matcher;\r\n\r\nfinal class SabrMediaPeriod\r\n   implements MediaPeriod,\r\n        SequenceableLoader.Callback<ChunkSampleStream<SabrChunkSource>>,\r\n        ChunkSampleStream.ReleaseCallback<SabrChunkSource> {\r\n    /* package */ final int id;\r\n    private final Factory chunkSourceFactory;\r\n    @Nullable\r\n    private final TransferListener transferListener;\r\n    private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\r\n    private final EventDispatcher eventDispatcher;\r\n    private final long elapsedRealtimeOffsetMs;\r\n    private final LoaderErrorThrower manifestLoaderErrorThrower;\r\n    private final TrackGroupArray trackGroups;\r\n    private final TrackGroupInfo[] trackGroupInfos;\r\n    private final Allocator allocator;\r\n    private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\r\n    private final PlayerEmsgHandler playerEmsgHandler;\r\n    private final IdentityHashMap<ChunkSampleStream<SabrChunkSource>, PlayerTrackEmsgHandler>\r\n            trackEmsgHandlerBySampleStream;\r\n\r\n    private @Nullable Callback callback;\r\n    private ChunkSampleStream<SabrChunkSource>[] sampleStreams;\r\n    private SequenceableLoader compositeSequenceableLoader;\r\n    private EventSampleStream[] eventSampleStreams;\r\n    private SabrManifest manifest;\r\n    private int periodIndex;\r\n    private List<EventStream> eventStreams;\r\n    private boolean notifiedReadingStarted;\r\n\r\n    public SabrMediaPeriod(\r\n            int id,\r\n            SabrManifest manifest,\r\n            int periodIndex,\r\n            SabrChunkSource.Factory chunkSourceFactory,\r\n            @Nullable TransferListener transferListener,\r\n            LoadErrorHandlingPolicy loadErrorHandlingPolicy,\r\n            EventDispatcher eventDispatcher,\r\n            long elapsedRealtimeOffsetMs,\r\n            LoaderErrorThrower manifestLoaderErrorThrower,\r\n            Allocator allocator,\r\n            CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\r\n            PlayerEmsgCallback playerEmsgCallback) {\r\n        this.id = id;\r\n        this.manifest = manifest;\r\n        this.periodIndex = periodIndex;\r\n        this.chunkSourceFactory = chunkSourceFactory;\r\n        this.transferListener = transferListener;\r\n        this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\r\n        this.eventDispatcher = eventDispatcher;\r\n        this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;\r\n        this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;\r\n        this.allocator = allocator;\r\n        this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;\r\n        playerEmsgHandler = new PlayerEmsgHandler(manifest, playerEmsgCallback, allocator);\r\n        sampleStreams = newSampleStreamArray(0);\r\n        eventSampleStreams = new EventSampleStream[0];\r\n        trackEmsgHandlerBySampleStream = new IdentityHashMap<>();\r\n        compositeSequenceableLoader =\r\n                compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);\r\n        Period period = manifest.getPeriod(periodIndex);\r\n        Pair<TrackGroupArray, TrackGroupInfo[]> result = buildTrackGroups(period.adaptationSets);\r\n        trackGroups = result.first;\r\n        trackGroupInfos = result.second;\r\n    }\r\n\r\n    @Override\r\n    public void prepare(Callback callback, long positionUs) {\r\n        this.callback = callback;\r\n        callback.onPrepared(this);\r\n    }\r\n\r\n    @Override\r\n    public void maybeThrowPrepareError() throws IOException {\r\n        manifestLoaderErrorThrower.maybeThrowError();\r\n    }\r\n\r\n    @Override\r\n    public TrackGroupArray getTrackGroups() {\r\n        return trackGroups;\r\n    }\r\n\r\n    @Override\r\n    public long selectTracks(\r\n            @Nullable TrackSelection[] selections,\r\n            boolean[] mayRetainStreamFlags,\r\n            @Nullable SampleStream[] streams,\r\n            boolean[] streamResetFlags,\r\n            long positionUs) {\r\n        int[] streamIndexToTrackGroupIndex = getStreamIndexToTrackGroupIndex(selections);\r\n        releaseDisabledStreams(selections, mayRetainStreamFlags, streams);\r\n        releaseOrphanEmbeddedStreams(selections, streams, streamIndexToTrackGroupIndex);\r\n        selectNewStreams(\r\n                selections, streams, streamResetFlags, positionUs, streamIndexToTrackGroupIndex);\r\n\r\n        ArrayList<ChunkSampleStream<SabrChunkSource>> sampleStreamList = new ArrayList<>();\r\n        ArrayList<EventSampleStream> eventSampleStreamList = new ArrayList<>();\r\n        for (SampleStream sampleStream : streams) {\r\n            if (sampleStream instanceof ChunkSampleStream) {\r\n                @SuppressWarnings(\"unchecked\")\r\n                ChunkSampleStream<SabrChunkSource> stream =\r\n                        (ChunkSampleStream<SabrChunkSource>) sampleStream;\r\n                sampleStreamList.add(stream);\r\n            } else if (sampleStream instanceof EventSampleStream) {\r\n                eventSampleStreamList.add((EventSampleStream) sampleStream);\r\n            }\r\n        }\r\n        sampleStreams = newSampleStreamArray(sampleStreamList.size());\r\n        sampleStreamList.toArray(sampleStreams);\r\n        eventSampleStreams = new EventSampleStream[eventSampleStreamList.size()];\r\n        eventSampleStreamList.toArray(eventSampleStreams);\r\n\r\n        compositeSequenceableLoader =\r\n                compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);\r\n        return positionUs;\r\n    }\r\n\r\n    @Override\r\n    public void discardBuffer(long positionUs, boolean toKeyframe) {\r\n        for (ChunkSampleStream<SabrChunkSource> sampleStream : sampleStreams) {\r\n            sampleStream.discardBuffer(positionUs, toKeyframe);\r\n        }\r\n    }\r\n\r\n    @Override\r\n    public long readDiscontinuity() {\r\n        if (!notifiedReadingStarted) {\r\n            eventDispatcher.readingStarted();\r\n            notifiedReadingStarted = true;\r\n        }\r\n        return C.TIME_UNSET;\r\n    }\r\n\r\n    @Override\r\n    public long seekToUs(long positionUs) {\r\n        for (ChunkSampleStream<SabrChunkSource> sampleStream : sampleStreams) {\r\n            sampleStream.seekToUs(positionUs);\r\n        }\r\n        for (EventSampleStream sampleStream : eventSampleStreams) {\r\n            sampleStream.seekToUs(positionUs);\r\n        }\r\n        return positionUs;\r\n    }\r\n\r\n    @Override\r\n    public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\r\n        for (ChunkSampleStream<SabrChunkSource> sampleStream : sampleStreams) {\r\n            if (sampleStream.primaryTrackType == C.TRACK_TYPE_VIDEO) {\r\n                return sampleStream.getAdjustedSeekPositionUs(positionUs, seekParameters);\r\n            }\r\n        }\r\n        return positionUs;\r\n    }\r\n\r\n    @Override\r\n    public long getBufferedPositionUs() {\r\n        return compositeSequenceableLoader.getBufferedPositionUs();\r\n    }\r\n\r\n    @Override\r\n    public long getNextLoadPositionUs() {\r\n        return compositeSequenceableLoader.getNextLoadPositionUs();\r\n    }\r\n\r\n    @Override\r\n    public boolean continueLoading(long positionUs) {\r\n        return compositeSequenceableLoader.continueLoading(positionUs);\r\n    }\r\n\r\n    @Override\r\n    public void reevaluateBuffer(long positionUs) {\r\n        compositeSequenceableLoader.reevaluateBuffer(positionUs);\r\n    }\r\n\r\n    // SequenceableLoader.Callback implementation.\r\n    @Override\r\n    public void onContinueLoadingRequested(ChunkSampleStream<SabrChunkSource> source) {\r\n        callback.onContinueLoadingRequested(this);\r\n    }\r\n\r\n    @Override\r\n    public synchronized void onSampleStreamReleased(ChunkSampleStream<SabrChunkSource> stream) {\r\n        PlayerTrackEmsgHandler trackEmsgHandler = trackEmsgHandlerBySampleStream.remove(stream);\r\n        if (trackEmsgHandler != null) {\r\n            trackEmsgHandler.release();\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Updates the {@link SabrManifest} and the index of this period in the manifest.\r\n     *\r\n     * @param manifest The updated manifest.\r\n     * @param periodIndex the new index of this period in the updated manifest.\r\n     */\r\n    public void updateManifest(SabrManifest manifest, int periodIndex) {\r\n        this.manifest = manifest;\r\n        this.periodIndex = periodIndex;\r\n        playerEmsgHandler.updateManifest(manifest);\r\n        if (sampleStreams != null) {\r\n            for (ChunkSampleStream<SabrChunkSource> sampleStream : sampleStreams) {\r\n                sampleStream.getChunkSource().updateManifest(manifest, periodIndex);\r\n            }\r\n            callback.onContinueLoadingRequested(this);\r\n        }\r\n    }\r\n\r\n    public void release() {\r\n        playerEmsgHandler.release();\r\n        for (ChunkSampleStream<SabrChunkSource> sampleStream : sampleStreams) {\r\n            sampleStream.release(this);\r\n        }\r\n        callback = null;\r\n        eventDispatcher.mediaPeriodReleased();\r\n    }\r\n\r\n    @SuppressWarnings(\"unchecked\")\r\n    private static ChunkSampleStream<SabrChunkSource>[] newSampleStreamArray(int length) {\r\n        return new ChunkSampleStream[length];\r\n    }\r\n\r\n    private static Pair<TrackGroupArray, TrackGroupInfo[]> buildTrackGroups(\r\n            List<AdaptationSet> adaptationSets) {\r\n        int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets);\r\n\r\n        int primaryGroupCount = groupedAdaptationSetIndices.length;\r\n        boolean[] primaryGroupHasEventMessageTrackFlags = new boolean[primaryGroupCount];\r\n        Format[][] primaryGroupCea608TrackFormats = new Format[primaryGroupCount][];\r\n        int totalEmbeddedTrackGroupCount =\r\n                identifyEmbeddedTracks(\r\n                        primaryGroupCount,\r\n                        adaptationSets,\r\n                        groupedAdaptationSetIndices,\r\n                        primaryGroupHasEventMessageTrackFlags,\r\n                        primaryGroupCea608TrackFormats);\r\n\r\n        int totalGroupCount = primaryGroupCount + totalEmbeddedTrackGroupCount;\r\n        TrackGroup[] trackGroups = new TrackGroup[totalGroupCount];\r\n        TrackGroupInfo[] trackGroupInfos = new TrackGroupInfo[totalGroupCount];\r\n\r\n        int trackGroupCount =\r\n                buildPrimaryAndEmbeddedTrackGroupInfos(\r\n                        adaptationSets,\r\n                        groupedAdaptationSetIndices,\r\n                        primaryGroupCount,\r\n                        primaryGroupHasEventMessageTrackFlags,\r\n                        primaryGroupCea608TrackFormats,\r\n                        trackGroups,\r\n                        trackGroupInfos);\r\n\r\n        return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos);\r\n    }\r\n\r\n    private static int[][] getGroupedAdaptationSetIndices(List<AdaptationSet> adaptationSets) {\r\n        int adaptationSetCount = adaptationSets.size();\r\n        SparseIntArray idToIndexMap = new SparseIntArray(adaptationSetCount);\r\n        for (int i = 0; i < adaptationSetCount; i++) {\r\n            idToIndexMap.put(adaptationSets.get(i).id, i);\r\n        }\r\n\r\n        int[][] groupedAdaptationSetIndices = new int[adaptationSetCount][];\r\n        boolean[] adaptationSetUsedFlags = new boolean[adaptationSetCount];\r\n\r\n        int groupCount = 0;\r\n        for (int i = 0; i < adaptationSetCount; i++) {\r\n            if (adaptationSetUsedFlags[i]) {\r\n                // This adaptation set has already been included in a group.\r\n                continue;\r\n            }\r\n            adaptationSetUsedFlags[i] = true;\r\n            groupedAdaptationSetIndices[groupCount++] = new int[] {i};\r\n        }\r\n\r\n        return groupCount < adaptationSetCount\r\n                ? Arrays.copyOf(groupedAdaptationSetIndices, groupCount) : groupedAdaptationSetIndices;\r\n    }\r\n\r\n    /**\r\n     * Iterates through list of primary track groups and identifies embedded tracks.\r\n     *\r\n     * @param primaryGroupCount The number of primary track groups.\r\n     * @param adaptationSets The list of {@link AdaptationSet} of the current DASH period.\r\n     * @param groupedAdaptationSetIndices The indices of {@link AdaptationSet} that belongs to the\r\n     *     same primary group, grouped in primary track groups order.\r\n     * @param primaryGroupHasEventMessageTrackFlags An output array to be filled with flags indicating\r\n     *     whether each of the primary track groups contains an embedded event message track.\r\n     * @param primaryGroupCea608TrackFormats An output array to be filled with track formats for\r\n     *     CEA-608 tracks embedded in each of the primary track groups.\r\n     * @return Total number of embedded track groups.\r\n     */\r\n    private static int identifyEmbeddedTracks(\r\n            int primaryGroupCount,\r\n            List<AdaptationSet> adaptationSets,\r\n            int[][] groupedAdaptationSetIndices,\r\n            boolean[] primaryGroupHasEventMessageTrackFlags,\r\n            Format[][] primaryGroupCea608TrackFormats) {\r\n        int numEmbeddedTrackGroups = 0;\r\n        for (int i = 0; i < primaryGroupCount; i++) {\r\n            primaryGroupCea608TrackFormats[i] =\r\n                    getCea608TrackFormats(adaptationSets, groupedAdaptationSetIndices[i]);\r\n            if (primaryGroupCea608TrackFormats[i].length != 0) {\r\n                numEmbeddedTrackGroups++;\r\n            }\r\n        }\r\n        return numEmbeddedTrackGroups;\r\n    }\r\n\r\n    private static int buildPrimaryAndEmbeddedTrackGroupInfos(\r\n            List<AdaptationSet> adaptationSets,\r\n            int[][] groupedAdaptationSetIndices,\r\n            int primaryGroupCount,\r\n            boolean[] primaryGroupHasEventMessageTrackFlags,\r\n            Format[][] primaryGroupCea608TrackFormats,\r\n            TrackGroup[] trackGroups,\r\n            TrackGroupInfo[] trackGroupInfos) {\r\n        int trackGroupCount = 0;\r\n        for (int i = 0; i < primaryGroupCount; i++) {\r\n            int[] adaptationSetIndices = groupedAdaptationSetIndices[i];\r\n            List<Representation> representations = new ArrayList<>();\r\n            for (int adaptationSetIndex : adaptationSetIndices) {\r\n                representations.addAll(adaptationSets.get(adaptationSetIndex).representations);\r\n            }\r\n            Format[] formats = new Format[representations.size()];\r\n            for (int j = 0; j < formats.length; j++) {\r\n                formats[j] = representations.get(j).format;\r\n            }\r\n\r\n            AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]);\r\n            int primaryTrackGroupIndex = trackGroupCount++;\r\n            int eventMessageTrackGroupIndex =\r\n                    primaryGroupHasEventMessageTrackFlags[i] ? trackGroupCount++ : C.INDEX_UNSET;\r\n            int cea608TrackGroupIndex =\r\n                    primaryGroupCea608TrackFormats[i].length != 0 ? trackGroupCount++ : C.INDEX_UNSET;\r\n\r\n            trackGroups[primaryTrackGroupIndex] = new TrackGroup(formats);\r\n            trackGroupInfos[primaryTrackGroupIndex] =\r\n                    TrackGroupInfo.primaryTrack(\r\n                            firstAdaptationSet.type,\r\n                            adaptationSetIndices,\r\n                            primaryTrackGroupIndex,\r\n                            eventMessageTrackGroupIndex,\r\n                            cea608TrackGroupIndex);\r\n            if (eventMessageTrackGroupIndex != C.INDEX_UNSET) {\r\n                Format format = Format.createSampleFormat(firstAdaptationSet.id + \":emsg\",\r\n                        MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null);\r\n                trackGroups[eventMessageTrackGroupIndex] = new TrackGroup(format);\r\n                trackGroupInfos[eventMessageTrackGroupIndex] =\r\n                        TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices, primaryTrackGroupIndex);\r\n            }\r\n            if (cea608TrackGroupIndex != C.INDEX_UNSET) {\r\n                trackGroups[cea608TrackGroupIndex] = new TrackGroup(primaryGroupCea608TrackFormats[i]);\r\n                trackGroupInfos[cea608TrackGroupIndex] =\r\n                        TrackGroupInfo.embeddedCea608Track(adaptationSetIndices, primaryTrackGroupIndex);\r\n            }\r\n        }\r\n        return trackGroupCount;\r\n    }\r\n\r\n    private static Format[] getCea608TrackFormats(\r\n            List<AdaptationSet> adaptationSets, int[] adaptationSetIndices) {\r\n        return new Format[0];\r\n    }\r\n\r\n    private int[] getStreamIndexToTrackGroupIndex(TrackSelection[] selections) {\r\n        int[] streamIndexToTrackGroupIndex = new int[selections.length];\r\n        for (int i = 0; i < selections.length; i++) {\r\n            if (selections[i] != null) {\r\n                streamIndexToTrackGroupIndex[i] = trackGroups.indexOf(selections[i].getTrackGroup());\r\n            } else {\r\n                streamIndexToTrackGroupIndex[i] = C.INDEX_UNSET;\r\n            }\r\n        }\r\n        return streamIndexToTrackGroupIndex;\r\n    }\r\n\r\n    private void releaseDisabledStreams(\r\n            TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams) {\r\n        for (int i = 0; i < selections.length; i++) {\r\n            if (selections[i] == null || !mayRetainStreamFlags[i]) {\r\n                if (streams[i] instanceof ChunkSampleStream) {\r\n                    @SuppressWarnings(\"unchecked\")\r\n                    ChunkSampleStream<SabrChunkSource> stream =\r\n                            (ChunkSampleStream<SabrChunkSource>) streams[i];\r\n                    stream.release(this);\r\n                } else if (streams[i] instanceof ChunkSampleStream.EmbeddedSampleStream) {\r\n                    ((EmbeddedSampleStream) streams[i]).release();\r\n                }\r\n                streams[i] = null;\r\n            }\r\n        }\r\n    }\r\n\r\n    private void releaseOrphanEmbeddedStreams(\r\n            TrackSelection[] selections, SampleStream[] streams, int[] streamIndexToTrackGroupIndex) {\r\n        for (int i = 0; i < selections.length; i++) {\r\n            if (streams[i] instanceof EmptySampleStream || streams[i] instanceof EmbeddedSampleStream) {\r\n                // We need to release an embedded stream if the corresponding primary stream is released.\r\n                int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex);\r\n                boolean mayRetainStream;\r\n                if (primaryStreamIndex == C.INDEX_UNSET) {\r\n                    // If the corresponding primary stream is not selected, we may retain an existing\r\n                    // EmptySampleStream.\r\n                    mayRetainStream = streams[i] instanceof EmptySampleStream;\r\n                } else {\r\n                    // If the corresponding primary stream is selected, we may retain the embedded stream if\r\n                    // the stream's parent still matches.\r\n                    mayRetainStream =\r\n                            (streams[i] instanceof EmbeddedSampleStream)\r\n                                    && ((EmbeddedSampleStream) streams[i]).parent == streams[primaryStreamIndex];\r\n                }\r\n                if (!mayRetainStream) {\r\n                    if (streams[i] instanceof EmbeddedSampleStream) {\r\n                        ((EmbeddedSampleStream) streams[i]).release();\r\n                    }\r\n                    streams[i] = null;\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    private int getPrimaryStreamIndex(int embeddedStreamIndex, int[] streamIndexToTrackGroupIndex) {\r\n        int embeddedTrackGroupIndex = streamIndexToTrackGroupIndex[embeddedStreamIndex];\r\n        if (embeddedTrackGroupIndex == C.INDEX_UNSET) {\r\n            return C.INDEX_UNSET;\r\n        }\r\n        int primaryTrackGroupIndex = trackGroupInfos[embeddedTrackGroupIndex].primaryTrackGroupIndex;\r\n        for (int i = 0; i < streamIndexToTrackGroupIndex.length; i++) {\r\n            int trackGroupIndex = streamIndexToTrackGroupIndex[i];\r\n            if (trackGroupIndex == primaryTrackGroupIndex\r\n                    && trackGroupInfos[trackGroupIndex].trackGroupCategory\r\n                    == TrackGroupInfo.CATEGORY_PRIMARY) {\r\n                return i;\r\n            }\r\n        }\r\n        return C.INDEX_UNSET;\r\n    }\r\n\r\n    private void selectNewStreams(\r\n            TrackSelection[] selections,\r\n            SampleStream[] streams,\r\n            boolean[] streamResetFlags,\r\n            long positionUs,\r\n            int[] streamIndexToTrackGroupIndex) {\r\n        // Create newly selected primary and event streams.\r\n        for (int i = 0; i < selections.length; i++) {\r\n            TrackSelection selection = selections[i];\r\n            if (selection == null) {\r\n                continue;\r\n            }\r\n            if (streams[i] == null) {\r\n                // Create new stream for selection.\r\n                streamResetFlags[i] = true;\r\n                int trackGroupIndex = streamIndexToTrackGroupIndex[i];\r\n                TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];\r\n                if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) {\r\n                    streams[i] = buildSampleStream(trackGroupInfo, selection, positionUs);\r\n                } else if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) {\r\n                    EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex);\r\n                    Format format = selection.getTrackGroup().getFormat(0);\r\n                    streams[i] = new EventSampleStream(eventStream, format, manifest.dynamic);\r\n                }\r\n            } else if (streams[i] instanceof ChunkSampleStream) {\r\n                // Update selection in existing stream.\r\n                @SuppressWarnings(\"unchecked\")\r\n                ChunkSampleStream<SabrChunkSource> stream = (ChunkSampleStream<SabrChunkSource>) streams[i];\r\n                stream.getChunkSource().updateTrackSelection(selection);\r\n            }\r\n        }\r\n        // Create newly selected embedded streams from the corresponding primary stream. Note that this\r\n        // second pass is needed because the primary stream may not have been created yet in a first\r\n        // pass if the index of the primary stream is greater than the index of the embedded stream.\r\n        for (int i = 0; i < selections.length; i++) {\r\n            if (streams[i] == null && selections[i] != null) {\r\n                int trackGroupIndex = streamIndexToTrackGroupIndex[i];\r\n                TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];\r\n                if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) {\r\n                    int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex);\r\n                    if (primaryStreamIndex == C.INDEX_UNSET) {\r\n                        // If an embedded track is selected without the corresponding primary track, create an\r\n                        // empty sample stream instead.\r\n                        streams[i] = new EmptySampleStream();\r\n                    } else {\r\n                        streams[i] =\r\n                                ((ChunkSampleStream) streams[primaryStreamIndex])\r\n                                        .selectEmbeddedTrack(positionUs, trackGroupInfo.trackType);\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    private ChunkSampleStream<SabrChunkSource> buildSampleStream(TrackGroupInfo trackGroupInfo,\r\n                                                                 TrackSelection selection, long positionUs) {\r\n        int embeddedTrackCount = 0;\r\n        boolean enableEventMessageTrack =\r\n                trackGroupInfo.embeddedEventMessageTrackGroupIndex != C.INDEX_UNSET;\r\n        TrackGroup embeddedEventMessageTrackGroup = null;\r\n        if (enableEventMessageTrack) {\r\n            embeddedEventMessageTrackGroup =\r\n                    trackGroups.get(trackGroupInfo.embeddedEventMessageTrackGroupIndex);\r\n            embeddedTrackCount++;\r\n        }\r\n        boolean enableCea608Tracks = trackGroupInfo.embeddedCea608TrackGroupIndex != C.INDEX_UNSET;\r\n        TrackGroup embeddedCea608TrackGroup = null;\r\n        if (enableCea608Tracks) {\r\n            embeddedCea608TrackGroup = trackGroups.get(trackGroupInfo.embeddedCea608TrackGroupIndex);\r\n            embeddedTrackCount += embeddedCea608TrackGroup.length;\r\n        }\r\n\r\n        Format[] embeddedTrackFormats = new Format[embeddedTrackCount];\r\n        int[] embeddedTrackTypes = new int[embeddedTrackCount];\r\n        embeddedTrackCount = 0;\r\n        if (enableEventMessageTrack) {\r\n            embeddedTrackFormats[embeddedTrackCount] = embeddedEventMessageTrackGroup.getFormat(0);\r\n            embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_METADATA;\r\n            embeddedTrackCount++;\r\n        }\r\n        List<Format> embeddedCea608TrackFormats = new ArrayList<>();\r\n        if (enableCea608Tracks) {\r\n            for (int i = 0; i < embeddedCea608TrackGroup.length; i++) {\r\n                embeddedTrackFormats[embeddedTrackCount] = embeddedCea608TrackGroup.getFormat(i);\r\n                embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_TEXT;\r\n                embeddedCea608TrackFormats.add(embeddedTrackFormats[embeddedTrackCount]);\r\n                embeddedTrackCount++;\r\n            }\r\n        }\r\n\r\n        PlayerTrackEmsgHandler trackPlayerEmsgHandler =\r\n                manifest.dynamic && enableEventMessageTrack\r\n                        ? playerEmsgHandler.newPlayerTrackEmsgHandler()\r\n                        : null;\r\n        SabrChunkSource chunkSource =\r\n                chunkSourceFactory.createSabrChunkSource(\r\n                        manifestLoaderErrorThrower,\r\n                        manifest,\r\n                        periodIndex,\r\n                        trackGroupInfo.adaptationSetIndices,\r\n                        selection,\r\n                        trackGroupInfo.trackType,\r\n                        elapsedRealtimeOffsetMs,\r\n                        enableEventMessageTrack,\r\n                        embeddedCea608TrackFormats,\r\n                        trackPlayerEmsgHandler,\r\n                        transferListener);\r\n        ChunkSampleStream<SabrChunkSource> stream =\r\n                new ChunkSampleStream<>(\r\n                        trackGroupInfo.trackType,\r\n                        embeddedTrackTypes,\r\n                        embeddedTrackFormats,\r\n                        chunkSource,\r\n                        this,\r\n                        allocator,\r\n                        positionUs,\r\n                        loadErrorHandlingPolicy,\r\n                        eventDispatcher);\r\n        synchronized (this) {\r\n            // The map is also accessed on the loading thread so synchronize access.\r\n            trackEmsgHandlerBySampleStream.put(stream, trackPlayerEmsgHandler);\r\n        }\r\n        return stream;\r\n    }\r\n\r\n    private static final class TrackGroupInfo {\r\n\r\n        @Documented\r\n        @Retention(RetentionPolicy.SOURCE)\r\n        @IntDef({CATEGORY_PRIMARY, CATEGORY_EMBEDDED, CATEGORY_MANIFEST_EVENTS})\r\n        public @interface TrackGroupCategory {}\r\n\r\n        /**\r\n         * A normal track group that has its samples drawn from the stream.\r\n         * For example: a video Track Group or an audio Track Group.\r\n         */\r\n        private static final int CATEGORY_PRIMARY = 0;\r\n\r\n        /**\r\n         * A track group whose samples are embedded within one of the primary streams. For example: an\r\n         * EMSG track has its sample embedded in emsg atoms in one of the primary streams.\r\n         */\r\n        private static final int CATEGORY_EMBEDDED = 1;\r\n\r\n        /**\r\n         * A track group that has its samples listed explicitly in the DASH manifest file.\r\n         * For example: an EventStream track has its sample (Events) included directly in the DASH\r\n         * manifest file.\r\n         */\r\n        private static final int CATEGORY_MANIFEST_EVENTS = 2;\r\n\r\n        public final int[] adaptationSetIndices;\r\n        public final int trackType;\r\n        @TrackGroupCategory public final int trackGroupCategory;\r\n\r\n        public final int eventStreamGroupIndex;\r\n        public final int primaryTrackGroupIndex;\r\n        public final int embeddedEventMessageTrackGroupIndex;\r\n        public final int embeddedCea608TrackGroupIndex;\r\n\r\n        public static TrackGroupInfo primaryTrack(\r\n                int trackType,\r\n                int[] adaptationSetIndices,\r\n                int primaryTrackGroupIndex,\r\n                int embeddedEventMessageTrackGroupIndex,\r\n                int embeddedCea608TrackGroupIndex) {\r\n            return new TrackGroupInfo(\r\n                    trackType,\r\n                    CATEGORY_PRIMARY,\r\n                    adaptationSetIndices,\r\n                    primaryTrackGroupIndex,\r\n                    embeddedEventMessageTrackGroupIndex,\r\n                    embeddedCea608TrackGroupIndex,\r\n                    /* eventStreamGroupIndex= */ -1);\r\n        }\r\n\r\n        public static TrackGroupInfo embeddedEmsgTrack(int[] adaptationSetIndices,\r\n                                                       int primaryTrackGroupIndex) {\r\n            return new TrackGroupInfo(\r\n                    C.TRACK_TYPE_METADATA,\r\n                    CATEGORY_EMBEDDED,\r\n                    adaptationSetIndices,\r\n                    primaryTrackGroupIndex,\r\n                    C.INDEX_UNSET,\r\n                    C.INDEX_UNSET,\r\n                    /* eventStreamGroupIndex= */ -1);\r\n        }\r\n\r\n        public static TrackGroupInfo embeddedCea608Track(int[] adaptationSetIndices,\r\n                                                         int primaryTrackGroupIndex) {\r\n            return new TrackGroupInfo(\r\n                    C.TRACK_TYPE_TEXT,\r\n                    CATEGORY_EMBEDDED,\r\n                    adaptationSetIndices,\r\n                    primaryTrackGroupIndex,\r\n                    C.INDEX_UNSET,\r\n                    C.INDEX_UNSET,\r\n                    /* eventStreamGroupIndex= */ -1);\r\n        }\r\n\r\n        public static TrackGroupInfo mpdEventTrack(int eventStreamIndex) {\r\n            return new TrackGroupInfo(\r\n                    C.TRACK_TYPE_METADATA,\r\n                    CATEGORY_MANIFEST_EVENTS,\r\n                    new int[0],\r\n                    /* primaryTrackGroupIndex= */ -1,\r\n                    C.INDEX_UNSET,\r\n                    C.INDEX_UNSET,\r\n                    eventStreamIndex);\r\n        }\r\n\r\n        private TrackGroupInfo(\r\n                int trackType,\r\n                @TrackGroupCategory int trackGroupCategory,\r\n                int[] adaptationSetIndices,\r\n                int primaryTrackGroupIndex,\r\n                int embeddedEventMessageTrackGroupIndex,\r\n                int embeddedCea608TrackGroupIndex,\r\n                int eventStreamGroupIndex) {\r\n            this.trackType = trackType;\r\n            this.adaptationSetIndices = adaptationSetIndices;\r\n            this.trackGroupCategory = trackGroupCategory;\r\n            this.primaryTrackGroupIndex = primaryTrackGroupIndex;\r\n            this.embeddedEventMessageTrackGroupIndex = embeddedEventMessageTrackGroupIndex;\r\n            this.embeddedCea608TrackGroupIndex = embeddedCea608TrackGroupIndex;\r\n            this.eventStreamGroupIndex = eventStreamGroupIndex;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/SabrMediaSource.java",
    "content": "package com.google.android.exoplayer2.source.sabr;\r\n\r\nimport android.net.Uri;\r\nimport android.os.Handler;\r\nimport android.os.SystemClock;\r\nimport android.util.SparseArray;\r\n\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.Timeline;\r\nimport com.google.android.exoplayer2.source.BaseMediaSource;\r\nimport com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;\r\nimport com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;\r\nimport com.google.android.exoplayer2.source.MediaPeriod;\r\nimport com.google.android.exoplayer2.source.MediaSource;\r\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\r\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\r\nimport com.google.android.exoplayer2.source.ads.AdsMediaSource;\r\nimport com.google.android.exoplayer2.source.sabr.PlayerEmsgHandler.PlayerEmsgCallback;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.AdaptationSet;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.SabrManifest;\r\nimport com.google.android.exoplayer2.upstream.Allocator;\r\nimport com.google.android.exoplayer2.upstream.DataSource;\r\nimport com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;\r\nimport com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;\r\nimport com.google.android.exoplayer2.upstream.Loader;\r\nimport com.google.android.exoplayer2.upstream.LoaderErrorThrower;\r\nimport com.google.android.exoplayer2.upstream.TransferListener;\r\nimport com.google.android.exoplayer2.util.Assertions;\r\n\r\nimport java.io.IOException;\r\n\r\npublic final class SabrMediaSource extends BaseMediaSource {\r\n    /**\r\n     * The interval in milliseconds between invocations of {@link\r\n     * SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the\r\n     * source's {@link Timeline} is changing dynamically (for example, for incomplete live streams).\r\n     */\r\n    private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000;\r\n    /**\r\n     * The minimum default start position for live streams, relative to the start of the live window.\r\n     */\r\n    private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000;\r\n    private final SabrManifest manifest;\r\n    private final SabrChunkSource.Factory chunkSourceFactory;\r\n    private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\r\n    private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;\r\n    private @Nullable TransferListener mediaTransferListener;\r\n    private final LoaderErrorThrower manifestLoadErrorThrower;\r\n    private final PlayerEmsgCallback playerEmsgCallback;\r\n    private Loader loader;\r\n    private IOException manifestFatalError;\r\n    private final long livePresentationDelayMs;\r\n    private final SparseArray<SabrMediaPeriod> periodsById;\r\n    private final @Nullable Object tag;\r\n    private long elapsedRealtimeOffsetMs;\r\n    private int firstPeriodId;\r\n    private final boolean livePresentationDelayOverridesManifest;\r\n\r\n    /**\r\n     * The default presentation delay for live streams. The presentation delay is the duration by\r\n     * which the default start position precedes the end of the live window.\r\n     */\r\n    private static final long DEFAULT_LIVE_PRESENTATION_DELAY_MS = 30000;\r\n\r\n    private SabrMediaSource(\r\n            SabrManifest manifest,\r\n            SabrChunkSource.Factory chunkSourceFactory,\r\n            CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\r\n            LoadErrorHandlingPolicy loadErrorHandlingPolicy,\r\n            long livePresentationDelayMs,\r\n            boolean livePresentationDelayOverridesManifest,\r\n            @Nullable Object tag\r\n    ) {\r\n        this.manifest = manifest;\r\n        this.chunkSourceFactory = chunkSourceFactory;\r\n        this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;\r\n        this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\r\n        this.livePresentationDelayMs = livePresentationDelayMs;\r\n        this.livePresentationDelayOverridesManifest = livePresentationDelayOverridesManifest;\r\n        this.tag = tag;\r\n        periodsById = new SparseArray<>();\r\n        playerEmsgCallback = new DefaultPlayerEmsgCallback();\r\n        manifestLoadErrorThrower = new ManifestLoadErrorThrower();\r\n    }\r\n\r\n    @Override\r\n    protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\r\n        this.mediaTransferListener = mediaTransferListener;\r\n        loader = new Loader(\"Loader:SabrMediaSource\");\r\n        processManifest();\r\n    }\r\n\r\n    @Override\r\n    protected void releaseSourceInternal() {\r\n        if (loader != null) {\r\n            loader.release();\r\n            loader = null;\r\n        }\r\n        elapsedRealtimeOffsetMs = 0;\r\n        manifestFatalError = null;\r\n        firstPeriodId = 0;\r\n        periodsById.clear();\r\n    }\r\n\r\n    @Override\r\n    public void maybeThrowSourceInfoRefreshError() throws IOException {\r\n        manifestLoadErrorThrower.maybeThrowError();\r\n    }\r\n\r\n    @Override\r\n    public MediaPeriod createPeriod(MediaPeriodId periodId, Allocator allocator, long startPositionUs) {\r\n        int periodIndex = (Integer) periodId.periodUid - firstPeriodId;\r\n        EventDispatcher periodEventDispatcher =\r\n                createEventDispatcher(periodId, manifest.getPeriod(periodIndex).startMs);\r\n        SabrMediaPeriod mediaPeriod = new SabrMediaPeriod(\r\n                firstPeriodId + periodIndex,\r\n                manifest,\r\n                periodIndex,\r\n                chunkSourceFactory,\r\n                mediaTransferListener,\r\n                loadErrorHandlingPolicy,\r\n                periodEventDispatcher,\r\n                elapsedRealtimeOffsetMs,\r\n                manifestLoadErrorThrower,\r\n                allocator,\r\n                compositeSequenceableLoaderFactory,\r\n                playerEmsgCallback);\r\n        periodsById.put(mediaPeriod.id, mediaPeriod);\r\n        return mediaPeriod;\r\n    }\r\n\r\n    @Override\r\n    public void releasePeriod(MediaPeriod mediaPeriod) {\r\n        SabrMediaPeriod sabrMediaPeriod = (SabrMediaPeriod) mediaPeriod;\r\n        sabrMediaPeriod.release();\r\n        periodsById.remove(sabrMediaPeriod.id);\r\n    }\r\n\r\n    private void processManifest() {\r\n        // Update any periods.\r\n        for (int i = 0; i < periodsById.size(); i++) {\r\n            int id = periodsById.keyAt(i);\r\n            if (id >= firstPeriodId) {\r\n                periodsById.valueAt(i).updateManifest(manifest, id - firstPeriodId);\r\n            } else {\r\n                // This period has been removed from the manifest so it doesn't need to be updated.\r\n            }\r\n        }\r\n        // Update the window.\r\n        boolean windowChangingImplicitly = false;\r\n        int lastPeriodIndex = manifest.getPeriodCount() - 1;\r\n        PeriodSeekInfo firstPeriodSeekInfo = PeriodSeekInfo.createPeriodSeekInfo(manifest.getPeriod(0),\r\n                manifest.getPeriodDurationUs(0));\r\n        PeriodSeekInfo lastPeriodSeekInfo = PeriodSeekInfo.createPeriodSeekInfo(\r\n                manifest.getPeriod(lastPeriodIndex), manifest.getPeriodDurationUs(lastPeriodIndex));\r\n        // Get the period-relative start/end times.\r\n        long currentStartTimeUs = firstPeriodSeekInfo.availableStartTimeUs;\r\n        long currentEndTimeUs = lastPeriodSeekInfo.availableEndTimeUs;\r\n        if (manifest.dynamic && !lastPeriodSeekInfo.isIndexExplicit) {\r\n            // The manifest describes an incomplete live stream. Update the start/end times to reflect the\r\n            // live stream duration and the manifest's time shift buffer depth.\r\n            long liveStreamDurationUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTimeMs);\r\n            long liveStreamEndPositionInLastPeriodUs = liveStreamDurationUs\r\n                    - C.msToUs(manifest.getPeriod(lastPeriodIndex).startMs);\r\n            currentEndTimeUs = Math.min(liveStreamEndPositionInLastPeriodUs, currentEndTimeUs);\r\n            if (manifest.timeShiftBufferDepthMs != C.TIME_UNSET) {\r\n                long timeShiftBufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs);\r\n                long offsetInPeriodUs = currentEndTimeUs - timeShiftBufferDepthUs;\r\n                int periodIndex = lastPeriodIndex;\r\n                while (offsetInPeriodUs < 0 && periodIndex > 0) {\r\n                    offsetInPeriodUs += manifest.getPeriodDurationUs(--periodIndex);\r\n                }\r\n                if (periodIndex == 0) {\r\n                    currentStartTimeUs = Math.max(currentStartTimeUs, offsetInPeriodUs);\r\n                } else {\r\n                    // The time shift buffer starts after the earliest period.\r\n                    // TODO: Does this ever happen?\r\n                    currentStartTimeUs = manifest.getPeriodDurationUs(0);\r\n                }\r\n            }\r\n            windowChangingImplicitly = true;\r\n        }\r\n        long windowDurationUs = currentEndTimeUs - currentStartTimeUs;\r\n        for (int i = 0; i < manifest.getPeriodCount() - 1; i++) {\r\n            windowDurationUs += manifest.getPeriodDurationUs(i);\r\n        }\r\n        long windowDefaultStartPositionUs = 0;\r\n        if (manifest.dynamic) {\r\n            long presentationDelayForManifestMs = livePresentationDelayMs;\r\n            if (!livePresentationDelayOverridesManifest\r\n                    && manifest.suggestedPresentationDelayMs != C.TIME_UNSET) {\r\n                presentationDelayForManifestMs = manifest.suggestedPresentationDelayMs;\r\n            }\r\n            // Snap the default position to the start of the segment containing it.\r\n            windowDefaultStartPositionUs = windowDurationUs - C.msToUs(presentationDelayForManifestMs);\r\n            if (windowDefaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) {\r\n                // The default start position is too close to the start of the live window. Set it to the\r\n                // minimum default start position provided the window is at least twice as big. Else set\r\n                // it to the middle of the window.\r\n                windowDefaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US,\r\n                        windowDurationUs / 2);\r\n            }\r\n        }\r\n        long windowStartTimeMs = manifest.availabilityStartTimeMs\r\n                + manifest.getPeriod(0).startMs + C.usToMs(currentStartTimeUs);\r\n        SabrTimeline timeline =\r\n                new SabrTimeline(\r\n                        manifest.availabilityStartTimeMs,\r\n                        windowStartTimeMs,\r\n                        firstPeriodId,\r\n                        currentStartTimeUs,\r\n                        windowDurationUs,\r\n                        windowDefaultStartPositionUs,\r\n                        manifest,\r\n                        tag);\r\n        refreshSourceInfo(timeline, manifest);\r\n    }\r\n\r\n    private long getNowUnixTimeUs() {\r\n        if (elapsedRealtimeOffsetMs != 0) {\r\n            return C.msToUs(SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs);\r\n        } else {\r\n            return C.msToUs(System.currentTimeMillis());\r\n        }\r\n    }\r\n\r\n    public static final class Factory implements AdsMediaSource.MediaSourceFactory {\r\n        private final SabrChunkSource.Factory chunkSourceFactory;\r\n        @Nullable private final DataSource.Factory manifestDataSourceFactory;\r\n        private LoadErrorHandlingPolicy loadErrorHandlingPolicy;\r\n        private final DefaultCompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;\r\n        private long livePresentationDelayMs;\r\n        private boolean livePresentationDelayOverridesManifest;\r\n        private boolean isCreateCalled;\r\n        @Nullable private Object tag;\r\n\r\n        /**\r\n         * Creates a new factory for {@link SabrMediaSource}s.\r\n         *\r\n         * @param chunkSourceFactory A factory for {@link SabrChunkSource} instances.\r\n         * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used\r\n         *     to load (and refresh) the manifest. May be {@code null} if the factory will only ever be\r\n         *     used to create create media sources with sideloaded manifests via {@link\r\n         *     #createMediaSource(SabrManifest, Handler, MediaSourceEventListener)}.\r\n         */\r\n        public Factory(\r\n                SabrChunkSource.Factory chunkSourceFactory,\r\n                @Nullable DataSource.Factory manifestDataSourceFactory) {\r\n            this.chunkSourceFactory = chunkSourceFactory;\r\n            this.manifestDataSourceFactory = manifestDataSourceFactory;\r\n            loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();\r\n            livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS;\r\n            compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();\r\n        }\r\n\r\n        @Override\r\n        public MediaSource createMediaSource(Uri uri) {\r\n            return null;\r\n        }\r\n\r\n        /**\r\n         * Returns a new {@link SabrMediaSource} using the current parameters and the specified\r\n         * sideloaded manifest.\r\n         *\r\n         * @param manifest The manifest.\r\n         * @return The new {@link SabrMediaSource}.\r\n         */\r\n        public SabrMediaSource createMediaSource(SabrManifest manifest) {\r\n            isCreateCalled = true;\r\n            return new SabrMediaSource(\r\n                    manifest,\r\n                    chunkSourceFactory,\r\n                    compositeSequenceableLoaderFactory,\r\n                    loadErrorHandlingPolicy,\r\n                    livePresentationDelayMs,\r\n                    livePresentationDelayOverridesManifest,\r\n                    tag\r\n            );\r\n        }\r\n\r\n        /**\r\n         * @deprecated Use {@link #createMediaSource(SabrManifest)} and {@link\r\n         *     #addEventListener(Handler, MediaSourceEventListener)} instead.\r\n         */\r\n        @Deprecated\r\n        public SabrMediaSource createMediaSource(\r\n                SabrManifest manifest,\r\n                @Nullable Handler eventHandler,\r\n                @Nullable MediaSourceEventListener eventListener) {\r\n            isCreateCalled = true;\r\n            SabrMediaSource mediaSource = createMediaSource(manifest);\r\n            if (eventHandler != null && eventListener != null) {\r\n                mediaSource.addEventListener(eventHandler, eventListener);\r\n            }\r\n            return mediaSource;\r\n        }\r\n\r\n        @Override\r\n        public int[] getSupportedTypes() {\r\n            return new int[0];\r\n        }\r\n\r\n        /**\r\n         * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link\r\n         * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}.\r\n         *\r\n         * <p>Calling this method overrides any calls to {@link #setMinLoadableRetryCount(int)}.\r\n         *\r\n         * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.\r\n         * @return This factory, for convenience.\r\n         * @throws IllegalStateException If one of the {@code create} methods has already been called.\r\n         */\r\n        public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {\r\n            Assertions.checkState(!isCreateCalled);\r\n            this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\r\n            return this;\r\n        }\r\n\r\n        /**\r\n         * Sets the minimum number of times to retry if a loading error occurs. See {@link\r\n         * #setLoadErrorHandlingPolicy} for the default value.\r\n         *\r\n         * <p>Calling this method is equivalent to calling {@link #setLoadErrorHandlingPolicy} with\r\n         * {@link DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy(int)\r\n         * DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)}\r\n         *\r\n         * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.\r\n         * @return This factory, for convenience.\r\n         * @throws IllegalStateException If one of the {@code create} methods has already been called.\r\n         * @deprecated Use {@link #setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)} instead.\r\n         */\r\n        @Deprecated\r\n        public Factory setMinLoadableRetryCount(int minLoadableRetryCount) {\r\n            return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount));\r\n        }\r\n\r\n        /**\r\n         * Sets a tag for the media source which will be published in the {@link\r\n         * com.google.android.exoplayer2.Timeline} of the source as {@link\r\n         * com.google.android.exoplayer2.Timeline.Window#tag}.\r\n         *\r\n         * @param tag A tag for the media source.\r\n         * @return This factory, for convenience.\r\n         * @throws IllegalStateException If one of the {@code create} methods has already been called.\r\n         */\r\n        public Factory setTag(Object tag) {\r\n            Assertions.checkState(!isCreateCalled);\r\n            this.tag = tag;\r\n            return this;\r\n        }\r\n\r\n        /**\r\n         * Sets the duration in milliseconds by which the default start position should precede the end\r\n         * of the live window for live playbacks. The {@code overridesManifest} parameter specifies\r\n         * whether the value is used in preference to one in the manifest, if present. The default value\r\n         * is {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}, and by default {@code overridesManifest} is\r\n         * false.\r\n         *\r\n         * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the\r\n         *     default start position should precede the end of the live window.\r\n         * @param overridesManifest Whether the value is used in preference to one in the manifest, if\r\n         *     present.\r\n         * @return This factory, for convenience.\r\n         * @throws IllegalStateException If one of the {@code create} methods has already been called.\r\n         */\r\n        public Factory setLivePresentationDelayMs(\r\n                long livePresentationDelayMs, boolean overridesManifest) {\r\n            Assertions.checkState(!isCreateCalled);\r\n            this.livePresentationDelayMs = livePresentationDelayMs;\r\n            this.livePresentationDelayOverridesManifest = overridesManifest;\r\n            return this;\r\n        }\r\n    }\r\n\r\n    /**\r\n     * A {@link LoaderErrorThrower} that throws fatal {@link IOException} that has occurred during\r\n     * manifest loading from the manifest {@code loader}, or exception with the loaded manifest.\r\n     */\r\n    /* package */ final class ManifestLoadErrorThrower implements LoaderErrorThrower {\r\n\r\n        @Override\r\n        public void maybeThrowError() throws IOException {\r\n            loader.maybeThrowError();\r\n            maybeThrowManifestError();\r\n        }\r\n\r\n        @Override\r\n        public void maybeThrowError(int minRetryCount) throws IOException {\r\n            loader.maybeThrowError(minRetryCount);\r\n            maybeThrowManifestError();\r\n        }\r\n\r\n        private void maybeThrowManifestError() throws IOException {\r\n            if (manifestFatalError != null) {\r\n                throw manifestFatalError;\r\n            }\r\n        }\r\n    }\r\n\r\n    private static final class DefaultPlayerEmsgCallback implements PlayerEmsgCallback {\r\n        @Override\r\n        public void onDashManifestRefreshRequested() {\r\n            //SabrMediaSource.this.onDashManifestRefreshRequested();\r\n        }\r\n\r\n        @Override\r\n        public void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) {\r\n            //SabrMediaSource.this.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs);\r\n        }\r\n    }\r\n\r\n    private static final class SabrTimeline extends Timeline {\r\n\r\n        private final long presentationStartTimeMs;\r\n        private final long windowStartTimeMs;\r\n\r\n        private final int firstPeriodId;\r\n        private final long offsetInFirstPeriodUs;\r\n        private final long windowDurationUs;\r\n        private final long windowDefaultStartPositionUs;\r\n        private final SabrManifest manifest;\r\n        private final @Nullable Object windowTag;\r\n\r\n        public SabrTimeline(\r\n                long presentationStartTimeMs,\r\n                long windowStartTimeMs,\r\n                int firstPeriodId,\r\n                long offsetInFirstPeriodUs,\r\n                long windowDurationUs,\r\n                long windowDefaultStartPositionUs,\r\n                SabrManifest manifest,\r\n                @Nullable Object windowTag) {\r\n            this.presentationStartTimeMs = presentationStartTimeMs;\r\n            this.windowStartTimeMs = windowStartTimeMs;\r\n            this.firstPeriodId = firstPeriodId;\r\n            this.offsetInFirstPeriodUs = offsetInFirstPeriodUs;\r\n            this.windowDurationUs = windowDurationUs;\r\n            this.windowDefaultStartPositionUs = windowDefaultStartPositionUs;\r\n            this.manifest = manifest;\r\n            this.windowTag = windowTag;\r\n        }\r\n\r\n        @Override\r\n        public int getPeriodCount() {\r\n            return manifest.getPeriodCount();\r\n        }\r\n\r\n        @Override\r\n        public Period getPeriod(int periodIndex, Period period, boolean setIdentifiers) {\r\n            Assertions.checkIndex(periodIndex, 0, getPeriodCount());\r\n            Object id = setIdentifiers ? manifest.getPeriod(periodIndex).id : null;\r\n            Object uid = setIdentifiers ? (firstPeriodId + periodIndex) : null;\r\n            return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex),\r\n                    C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs)\r\n                            - offsetInFirstPeriodUs);\r\n        }\r\n\r\n        @Override\r\n        public int getWindowCount() {\r\n            return 1;\r\n        }\r\n\r\n        @Override\r\n        public Window getWindow(\r\n                int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {\r\n            Assertions.checkIndex(windowIndex, 0, 1);\r\n            long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs(\r\n                    defaultPositionProjectionUs);\r\n            Object tag = setTag ? windowTag : null;\r\n            boolean isDynamic =\r\n                    manifest.dynamic\r\n                            && manifest.minUpdatePeriodMs != C.TIME_UNSET\r\n                            && manifest.durationMs == C.TIME_UNSET;\r\n            return window.set(\r\n                    tag,\r\n                    presentationStartTimeMs,\r\n                    windowStartTimeMs,\r\n                    /* isSeekable= */ true,\r\n                    isDynamic,\r\n                    windowDefaultStartPositionUs,\r\n                    windowDurationUs,\r\n                    /* firstPeriodIndex= */ 0,\r\n                    /* lastPeriodIndex= */ getPeriodCount() - 1,\r\n                    offsetInFirstPeriodUs);\r\n        }\r\n\r\n        @Override\r\n        public int getIndexOfPeriod(Object uid) {\r\n            if (!(uid instanceof Integer)) {\r\n                return C.INDEX_UNSET;\r\n            }\r\n            int periodId = (int) uid;\r\n            int periodIndex = periodId - firstPeriodId;\r\n            return periodIndex < 0 || periodIndex >= getPeriodCount() ? C.INDEX_UNSET : periodIndex;\r\n        }\r\n\r\n        private long getAdjustedWindowDefaultStartPositionUs(long defaultPositionProjectionUs) {\r\n            long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs;\r\n            if (!manifest.dynamic) {\r\n                return windowDefaultStartPositionUs;\r\n            }\r\n            if (defaultPositionProjectionUs > 0) {\r\n                windowDefaultStartPositionUs += defaultPositionProjectionUs;\r\n                if (windowDefaultStartPositionUs > windowDurationUs) {\r\n                    // The projection takes us beyond the end of the live window.\r\n                    return C.TIME_UNSET;\r\n                }\r\n            }\r\n            // Attempt to snap to the start of the corresponding video segment.\r\n            int periodIndex = 0;\r\n            long defaultStartPositionInPeriodUs = offsetInFirstPeriodUs + windowDefaultStartPositionUs;\r\n            long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);\r\n            while (periodIndex < manifest.getPeriodCount() - 1\r\n                    && defaultStartPositionInPeriodUs >= periodDurationUs) {\r\n                defaultStartPositionInPeriodUs -= periodDurationUs;\r\n                periodIndex++;\r\n                periodDurationUs = manifest.getPeriodDurationUs(periodIndex);\r\n            }\r\n            com.google.android.exoplayer2.source.sabr.manifest.Period period =\r\n                    manifest.getPeriod(periodIndex);\r\n            int videoAdaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO);\r\n            if (videoAdaptationSetIndex == C.INDEX_UNSET) {\r\n                // No video adaptation set for snapping.\r\n                return windowDefaultStartPositionUs;\r\n            }\r\n            // If there are multiple video adaptation sets with unaligned segments, the initial time may\r\n            // not correspond to the start of a segment in both, but this is an edge case.\r\n            //SabrSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex)\r\n            //        .representations.get(0).getIndex();\r\n            //if (snapIndex == null || snapIndex.getSegmentCount(periodDurationUs) == 0) {\r\n            //    // Video adaptation set does not include a non-empty index for snapping.\r\n            //    return windowDefaultStartPositionUs;\r\n            //}\r\n            //long segmentNum = snapIndex.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs);\r\n            //return windowDefaultStartPositionUs + snapIndex.getTimeUs(segmentNum)\r\n            //        - defaultStartPositionInPeriodUs;\r\n\r\n            long startTimeUs = 0; // TODO: calc SABR start time\r\n\r\n            return windowDefaultStartPositionUs + startTimeUs\r\n                    - defaultStartPositionInPeriodUs;\r\n        }\r\n\r\n        @Override\r\n        public Object getUidOfPeriod(int periodIndex) {\r\n            Assertions.checkIndex(periodIndex, 0, getPeriodCount());\r\n            return firstPeriodId + periodIndex;\r\n        }\r\n    }\r\n\r\n    private static final class PeriodSeekInfo {\r\n\r\n        public static PeriodSeekInfo createPeriodSeekInfo(\r\n                com.google.android.exoplayer2.source.sabr.manifest.Period period, long durationUs) {\r\n            int adaptationSetCount = period.adaptationSets.size();\r\n            long availableStartTimeUs = 0;\r\n            long availableEndTimeUs = Long.MAX_VALUE; // TODO: calc SABR start time\r\n            boolean isIndexExplicit = false;\r\n            boolean seenEmptyIndex = false;\r\n\r\n            boolean haveAudioVideoAdaptationSets = false;\r\n            for (int i = 0; i < adaptationSetCount; i++) {\r\n                int type = period.adaptationSets.get(i).type;\r\n                if (type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO) {\r\n                    haveAudioVideoAdaptationSets = true;\r\n                    break;\r\n                }\r\n            }\r\n\r\n            for (int i = 0; i < adaptationSetCount; i++) {\r\n                AdaptationSet adaptationSet = period.adaptationSets.get(i);\r\n                // Exclude text adaptation sets from duration calculations, if we have at least one audio\r\n                // or video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029\r\n                if (haveAudioVideoAdaptationSets && adaptationSet.type == C.TRACK_TYPE_TEXT) {\r\n                    continue;\r\n                }\r\n\r\n                //SabrSegmentIndex index = adaptationSet.representations.get(0).getIndex();\r\n                //if (index == null) {\r\n                //    return new PeriodSeekInfo(true, 0, durationUs);\r\n                //}\r\n                //isIndexExplicit |= index.isExplicit();\r\n                //int segmentCount = index.getSegmentCount(durationUs);\r\n                //if (segmentCount == 0) {\r\n                //    seenEmptyIndex = true;\r\n                //    availableStartTimeUs = 0;\r\n                //    availableEndTimeUs = 0;\r\n                //} else if (!seenEmptyIndex) {\r\n                //    long firstSegmentNum = index.getFirstSegmentNum();\r\n                //    long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum);\r\n                //    availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs);\r\n                //    if (segmentCount != SabrSegmentIndex.INDEX_UNBOUNDED) {\r\n                //        long lastSegmentNum = firstSegmentNum + segmentCount - 1;\r\n                //        long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum)\r\n                //                + index.getDurationUs(lastSegmentNum, durationUs);\r\n                //        availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs);\r\n                //    }\r\n                //}\r\n            }\r\n            return new PeriodSeekInfo(isIndexExplicit, availableStartTimeUs, availableEndTimeUs);\r\n        }\r\n\r\n        public final boolean isIndexExplicit;\r\n        public final long availableStartTimeUs;\r\n        public final long availableEndTimeUs;\r\n\r\n        private PeriodSeekInfo(boolean isIndexExplicit, long availableStartTimeUs,\r\n                               long availableEndTimeUs) {\r\n            this.isIndexExplicit = isIndexExplicit;\r\n            this.availableStartTimeUs = availableStartTimeUs;\r\n            this.availableEndTimeUs = availableEndTimeUs;\r\n        }\r\n\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/SabrSegmentIndex.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.sabr;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.source.sabr.manifest.RangedUri;\n\n/**\n * Indexes the segments within a media stream.\n */\npublic interface SabrSegmentIndex {\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-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/SabrWrappingSegmentIndex.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.sabr;\n\nimport com.google.android.exoplayer2.extractor.ChunkIndex;\nimport com.google.android.exoplayer2.source.sabr.manifest.RangedUri;\n\n/**\n * An implementation of {@link SabrSegmentIndex} that wraps a {@link ChunkIndex} parsed from a\n * media stream.\n */\npublic final class SabrWrappingSegmentIndex implements SabrSegmentIndex {\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 SabrWrappingSegmentIndex(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-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/manifest/AdaptationSet.java",
    "content": "package com.google.android.exoplayer2.source.sabr.manifest;\r\n\r\nimport java.util.Collections;\r\nimport java.util.List;\r\n\r\n/**\r\n * Represents a set of interchangeable encoded versions of a media content component.\r\n */\r\npublic class AdaptationSet {\r\n\r\n    /**\r\n     * Value of {@link #id} indicating no value is set.=\r\n     */\r\n    public static final int ID_UNSET = -1;\r\n\r\n    /**\r\n     * A non-negative identifier for the adaptation set that's unique in the scope of its containing\r\n     * period, or {@link #ID_UNSET} if not specified.\r\n     */\r\n    public final int id;\r\n\r\n    /**\r\n     * The type of the adaptation set. One of the {@link com.google.android.exoplayer2.C}\r\n     * {@code TRACK_TYPE_*} constants.\r\n     */\r\n    public final int type;\r\n\r\n    /**\r\n     * {@link Representation}s in the adaptation set.\r\n     */\r\n    public final List<Representation> representations;\r\n\r\n    /**\r\n     * @param id A non-negative identifier for the adaptation set that's unique in the scope of its\r\n     *     containing period, or {@link #ID_UNSET} if not specified.\r\n     * @param type The type of the adaptation set. One of the {@link com.google.android.exoplayer2.C}\r\n     *     {@code TRACK_TYPE_*} constants.\r\n     * @param representations {@link Representation}s in the adaptation set.\r\n     */\r\n    public AdaptationSet(int id, int type, List<Representation> representations) {\r\n        this.id = id;\r\n        this.type = type;\r\n        this.representations = Collections.unmodifiableList(representations);\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/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.sabr.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-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/manifest/Period.java",
    "content": "package com.google.android.exoplayer2.source.sabr.manifest;\r\n\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.google.android.exoplayer2.C;\r\n\r\nimport java.util.Collections;\r\nimport java.util.List;\r\n\r\n/**\r\n * Encapsulates media content components over a contiguous period of time.\r\n */\r\npublic class Period {\r\n    /**\r\n     * The period identifier, if one exists.\r\n     */\r\n    @Nullable\r\n    public final String id;\r\n\r\n    /**\r\n     * The start time of the period in milliseconds.\r\n     */\r\n    public final long startMs;\r\n\r\n    /**\r\n     * The adaptation sets belonging to the period.\r\n     */\r\n    public final List<AdaptationSet> adaptationSets;\r\n\r\n    /**\r\n     * @param id The period identifier. May be null.\r\n     * @param startMs The start time of the period in milliseconds.\r\n     * @param adaptationSets The adaptation sets belonging to the period.\r\n     */\r\n    public Period(@Nullable String id, long startMs, List<AdaptationSet> adaptationSets) {\r\n        this.id = id;\r\n        this.startMs = startMs;\r\n        this.adaptationSets = Collections.unmodifiableList(adaptationSets);\r\n    }\r\n\r\n    /**\r\n     * Returns the index of the first adaptation set of a given type, or {@link C#INDEX_UNSET} if no\r\n     * adaptation set of the specified type exists.\r\n     *\r\n     * @param type An adaptation set type.\r\n     * @return The index of the first adaptation set of the specified type, or {@link C#INDEX_UNSET}.\r\n     */\r\n    public int getAdaptationSetIndex(int type) {\r\n        int adaptationCount = adaptationSets.size();\r\n        for (int i = 0; i < adaptationCount; i++) {\r\n            if (adaptationSets.get(i).type == type) {\r\n                return i;\r\n            }\r\n        }\r\n        return C.INDEX_UNSET;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/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.sabr.manifest;\n\nimport android.net.Uri;\n\nimport androidx.annotation.Nullable;\n\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 optional base Uri.\n   * @return The merged {@link RangedUri} if the merge was successful. Null otherwise.\n   */\n  public @Nullable 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-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/manifest/Representation.java",
    "content": "package com.google.android.exoplayer2.source.sabr.manifest;\r\n\r\nimport android.net.Uri;\r\n\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.source.sabr.SabrSegmentIndex;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.SegmentBase.MultiSegmentBase;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.SegmentBase.SingleSegmentBase;\r\n\r\nimport java.util.List;\r\n\r\n/**\r\n * A SABR representation.\r\n */\r\npublic abstract class Representation {\r\n    /**\r\n     * A default value for {@link #revisionId}.\r\n     */\r\n    public static final long REVISION_ID_DEFAULT = -1;\r\n\r\n    /**\r\n     * Identifies the revision of the media contained within the representation. If the media can\r\n     * change over time (e.g. as a result of it being re-encoded), then this identifier can be set to\r\n     * uniquely identify the revision of the media. The timestamp at which the media was encoded is\r\n     * often a suitable.\r\n     */\r\n    public final long revisionId;\r\n    /**\r\n     * The format of the representation.\r\n     */\r\n    public final Format format;\r\n    /**\r\n     * The base URL of the representation.\r\n     */\r\n    public final String baseUrl;\r\n    /**\r\n     * The offset of the presentation timestamps in the media stream relative to media time.\r\n     */\r\n    public final long presentationTimeOffsetUs;\r\n\r\n    private final RangedUri initializationUri;\r\n\r\n    public static Representation newInstance(\r\n            Format format,\r\n            String baseUrl,\r\n            SegmentBase segmentBase) {\r\n        return newInstance(REVISION_ID_DEFAULT, format, baseUrl, segmentBase, null);\r\n    }\r\n\r\n    public static Representation newInstance(\r\n            long revisionId,\r\n            Format format,\r\n            String baseUrl,\r\n            SegmentBase segmentBase) {\r\n        return newInstance(revisionId, format, baseUrl, segmentBase, null);\r\n    }\r\n\r\n    public static Representation newInstance(\r\n            long revisionId,\r\n            Format format,\r\n            String baseUrl,\r\n            SegmentBase segmentBase,\r\n            String cacheKey) {\r\n        if (segmentBase instanceof SingleSegmentBase) {\r\n            return new SingleSegmentRepresentation(\r\n                    revisionId,\r\n                    format,\r\n                    baseUrl,\r\n                    (SingleSegmentBase) segmentBase,\r\n                    cacheKey,\r\n                    C.LENGTH_UNSET);\r\n        } else if (segmentBase instanceof MultiSegmentBase) {\r\n            return new MultiSegmentRepresentation(\r\n                    revisionId, format, baseUrl, (MultiSegmentBase) segmentBase);\r\n        } else {\r\n            throw new IllegalArgumentException(\"segmentBase must be of type SingleSegmentBase or \"\r\n                    + \"MultiSegmentBase\");\r\n        }\r\n    }\r\n\r\n    private Representation(\r\n            long revisionId,\r\n            Format format,\r\n            String baseUrl,\r\n            SegmentBase segmentBase) {\r\n        this.revisionId = revisionId;\r\n        this.format = format;\r\n        this.baseUrl = baseUrl;\r\n        initializationUri = segmentBase.getInitialization(this);\r\n        presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs();\r\n    }\r\n\r\n    /**\r\n     * Returns a {@link RangedUri} defining the location of the representation's initialization data,\r\n     * or null if no initialization data exists.\r\n     */\r\n    public RangedUri getInitializationUri() {\r\n        return initializationUri;\r\n    }\r\n\r\n    /**\r\n     * Returns a {@link RangedUri} defining the location of the representation's segment index, or\r\n     * null if the representation provides an index directly.\r\n     */\r\n    public abstract RangedUri getIndexUri();\r\n\r\n    /**\r\n     * Returns an index if the representation provides one directly, or null otherwise.\r\n     */\r\n    //public abstract SabrSegmentIndex getIndex();\r\n\r\n    /** Returns a cache key for the representation if set, or null. */\r\n    public abstract String getCacheKey();\r\n\r\n    /**\r\n     * A DASH representation consisting of a single segment.\r\n     */\r\n    public static class SingleSegmentRepresentation extends Representation {\r\n\r\n        /**\r\n         * The uri of the single segment.\r\n         */\r\n        public final Uri uri;\r\n\r\n        /**\r\n         * The content length, or {@link C#LENGTH_UNSET} if unknown.\r\n         */\r\n        public final long contentLength;\r\n\r\n        private final String cacheKey;\r\n        private final RangedUri indexUri;\r\n        //private final SingleSegmentIndex segmentIndex;\r\n        \r\n        public static SingleSegmentRepresentation newInstance(\r\n                long revisionId,\r\n                Format format,\r\n                String uri,\r\n                long initializationStart,\r\n                long initializationEnd,\r\n                long indexStart,\r\n                long indexEnd,\r\n                String cacheKey,\r\n                long contentLength) {\r\n            RangedUri rangedUri = new RangedUri(null, initializationStart,\r\n                    initializationEnd - initializationStart + 1);\r\n            SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, indexStart,\r\n                    indexEnd - indexStart + 1);\r\n            return new SingleSegmentRepresentation(\r\n                    revisionId, format, uri, segmentBase, cacheKey, contentLength);\r\n        }\r\n\r\n        public SingleSegmentRepresentation(\r\n                long revisionId,\r\n                Format format,\r\n                String baseUrl,\r\n                SingleSegmentBase segmentBase,\r\n                String cacheKey,\r\n                long contentLength) {\r\n            super(revisionId, format, baseUrl, segmentBase);\r\n            this.uri = Uri.parse(baseUrl);\r\n            this.indexUri = segmentBase.getIndex();\r\n            this.cacheKey = cacheKey;\r\n            this.contentLength = contentLength;\r\n            // If we have an index uri then the index is defined externally, and we shouldn't return one\r\n            // directly. If we don't, then we can't do better than an index defining a single segment.\r\n            //segmentIndex = indexUri != null ? null\r\n            //        : new SingleSegmentIndex(new RangedUri(null, 0, contentLength));\r\n        }\r\n\r\n        @Override\r\n        public RangedUri getIndexUri() {\r\n            return indexUri;\r\n        }\r\n\r\n        //@Override\r\n        //public SabrSegmentIndex getIndex() {\r\n        //    return segmentIndex;\r\n        //}\r\n\r\n        @Override\r\n        public String getCacheKey() {\r\n            return cacheKey;\r\n        }\r\n\r\n    }\r\n\r\n    /**\r\n     * A DASH representation consisting of multiple segments.\r\n     */\r\n    public static class MultiSegmentRepresentation extends Representation {\r\n\r\n        private final MultiSegmentBase segmentBase;\r\n\r\n        /**\r\n         * @param revisionId Identifies the revision of the content.\r\n         * @param format The format of the representation.\r\n         * @param baseUrl The base URL of the representation.\r\n         * @param segmentBase The segment base underlying the representation.\r\n         */\r\n        public MultiSegmentRepresentation(\r\n                long revisionId,\r\n                Format format,\r\n                String baseUrl,\r\n                MultiSegmentBase segmentBase) {\r\n            super(revisionId, format, baseUrl, segmentBase);\r\n            this.segmentBase = segmentBase;\r\n        }\r\n\r\n        @Override\r\n        public RangedUri getIndexUri() {\r\n            return null;\r\n        }\r\n\r\n        //@Override\r\n        //public SabrSegmentIndex getIndex() {\r\n        //    return this;\r\n        //}\r\n\r\n        @Override\r\n        public String getCacheKey() {\r\n            return null;\r\n        }\r\n\r\n        // DashSegmentIndex implementation.\r\n\r\n        //@Override\r\n        //public RangedUri getSegmentUrl(long segmentIndex) {\r\n        //    return segmentBase.getSegmentUrl(this, segmentIndex);\r\n        //}\r\n        //\r\n        //@Override\r\n        //public long getSegmentNum(long timeUs, long periodDurationUs) {\r\n        //    return segmentBase.getSegmentNum(timeUs, periodDurationUs);\r\n        //}\r\n        //\r\n        //@Override\r\n        //public long getTimeUs(long segmentIndex) {\r\n        //    return segmentBase.getSegmentTimeUs(segmentIndex);\r\n        //}\r\n        //\r\n        //@Override\r\n        //public long getDurationUs(long segmentIndex, long periodDurationUs) {\r\n        //    return segmentBase.getSegmentDurationUs(segmentIndex, periodDurationUs);\r\n        //}\r\n        //\r\n        //@Override\r\n        //public long getFirstSegmentNum() {\r\n        //    return segmentBase.getFirstSegmentNum();\r\n        //}\r\n        //\r\n        //@Override\r\n        //public int getSegmentCount(long periodDurationUs) {\r\n        //    return segmentBase.getSegmentCount(periodDurationUs);\r\n        //}\r\n        //\r\n        //@Override\r\n        //public boolean isExplicit() {\r\n        //    return segmentBase.isExplicit();\r\n        //}\r\n\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/manifest/SabrManifest.java",
    "content": "package com.google.android.exoplayer2.source.sabr.manifest;\r\n\r\nimport android.util.Base64;\r\nimport android.util.Pair;\r\n\r\nimport androidx.annotation.NonNull;\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.offline.FilterableManifest;\r\nimport com.google.android.exoplayer2.offline.StreamKey;\r\nimport com.google.android.exoplayer2.source.sabr.parser.SabrStream;\r\nimport com.google.android.exoplayer2.source.sabr.parser.misc.EnabledTrackTypes;\r\nimport com.google.android.exoplayer2.source.sabr.parser.misc.Utils;\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.FormatSelector;\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.BufferedRange;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.ClientAbrState;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.MediaHeader;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.StreamerContext;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.StreamerContext.ClientInfo;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.TimeRange;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.VideoPlaybackAbrRequest;\r\nimport com.google.protobuf.ByteString;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.HashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\n\r\n/**\r\n * Represents a SABR media presentation\r\n */\r\npublic class SabrManifest implements FilterableManifest<SabrManifest> {\r\n    /**\r\n     * The {@code availabilityStartTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if\r\n     * not present.\r\n     */\r\n    public final long availabilityStartTimeMs;\r\n\r\n    /**\r\n     * The duration of the presentation in milliseconds, or {@link C#TIME_UNSET} if not applicable.\r\n     */\r\n    public final long durationMs;\r\n\r\n    /**\r\n     * The {@code minBufferTime} value in milliseconds, or {@link C#TIME_UNSET} if not present.\r\n     */\r\n    public final long minBufferTimeMs;\r\n\r\n    /**\r\n     * The {@code timeShiftBufferDepth} value in milliseconds, or {@link C#TIME_UNSET} if not\r\n     * present.\r\n     */\r\n    public final long timeShiftBufferDepthMs;\r\n\r\n    /**\r\n     * The {@code suggestedPresentationDelay} value in milliseconds, or {@link C#TIME_UNSET} if not\r\n     * present.\r\n     */\r\n    public final long suggestedPresentationDelayMs;\r\n\r\n    /**\r\n     * The {@code publishTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if\r\n     * not present.\r\n     */\r\n    public final long publishTimeMs;\r\n\r\n    public final List<Period> periods;\r\n\r\n    /**\r\n     * Whether the manifest has value \"dynamic\" for the {@code type} attribute.\r\n     */\r\n    public final boolean dynamic;\r\n\r\n    /**\r\n     * The {@code minimumUpdatePeriod} value in milliseconds, or {@link C#TIME_UNSET} if not\r\n     * applicable.\r\n     */\r\n    public final long minUpdatePeriodMs;\r\n\r\n    private final String videoId;\r\n    private final String serverAbrStreamingUrl;\r\n    private final String videoPlaybackUstreamerConfig;\r\n    private final String poToken;\r\n    private final ClientInfo clientInfo;\r\n    private final Map<Integer, SabrStream> sabrStreams;\r\n    private int sabrRequestNumber = -1;\r\n    private final FormatSelector emptySelector;\r\n\r\n    public SabrManifest(\r\n            long availabilityStartTimeMs,\r\n            long durationMs,\r\n            long minBufferTimeMs,\r\n            boolean dynamic,\r\n            long minUpdatePeriodMs,\r\n            long timeShiftBufferDepthMs,\r\n            long suggestedPresentationDelayMs,\r\n            long publishTimeMs,\r\n            List<Period> periods,\r\n            String serverAbrStreamingUrl,\r\n            String videoPlaybackUstreamerConfig,\r\n            String poToken,\r\n            String videoId,\r\n            ClientInfo clientInfo) {\r\n        this.availabilityStartTimeMs = availabilityStartTimeMs;\r\n        this.durationMs = durationMs;\r\n        this.minBufferTimeMs = minBufferTimeMs;\r\n        this.dynamic = dynamic;\r\n        this.minUpdatePeriodMs = minUpdatePeriodMs;\r\n        this.timeShiftBufferDepthMs = timeShiftBufferDepthMs;\r\n        this.suggestedPresentationDelayMs = suggestedPresentationDelayMs;\r\n        this.publishTimeMs = publishTimeMs;\r\n        this.periods = periods;\r\n        this.videoId = videoId;\r\n        this.serverAbrStreamingUrl = serverAbrStreamingUrl;\r\n        this.videoPlaybackUstreamerConfig = videoPlaybackUstreamerConfig;\r\n        this.clientInfo = clientInfo;\r\n        this.poToken = poToken;\r\n        this.sabrStreams = new HashMap<>();\r\n        this.emptySelector = new FormatSelector(\"ignored\", true);\r\n    }\r\n\r\n    public final int getPeriodCount() {\r\n        return periods.size();\r\n    }\r\n\r\n    public final Period getPeriod(int index) {\r\n        return periods.get(index);\r\n    }\r\n\r\n    public final long getPeriodDurationMs(int index) {\r\n        return index == periods.size() - 1\r\n                ? (durationMs == C.TIME_UNSET ? C.TIME_UNSET : (durationMs - periods.get(index).startMs))\r\n                : (periods.get(index + 1).startMs - periods.get(index).startMs);\r\n    }\r\n\r\n    public final long getPeriodDurationUs(int index) {\r\n        return C.msToUs(getPeriodDurationMs(index));\r\n    }\r\n\r\n    @Override\r\n    public SabrManifest copy(List<StreamKey> streamKeys) {\r\n        return null;\r\n    }\r\n\r\n    public final String getVideoId() {\r\n        return videoId;\r\n    }\r\n\r\n    public SabrStream getSabrStream(int trackType) {\r\n        SabrStream sabrStream = sabrStreams.get(trackType);\r\n\r\n        if (sabrStream != null) {\r\n            return sabrStream;\r\n        }\r\n\r\n        sabrStream = new SabrStream(\r\n                serverAbrStreamingUrl,\r\n                videoPlaybackUstreamerConfig,\r\n                clientInfo,\r\n                -1,\r\n                -1,\r\n                -1,\r\n                poToken,\r\n                false,\r\n                videoId,\r\n                durationMs\r\n        );\r\n\r\n        sabrStreams.put(trackType, sabrStream);\r\n\r\n        return sabrStream;\r\n    }\r\n\r\n    public int getSabrRequestNumber() {\r\n        return sabrRequestNumber;\r\n    }\r\n\r\n    public String getRequestUrl(int trackType) {\r\n        SabrStream activeStream = sabrStreams.get(trackType);\r\n\r\n        if (activeStream == null) {\r\n            throw new IllegalStateException(\"Active SabrStream not found for track type \" + trackType);\r\n        }\r\n\r\n        return Utils.updateQuery(activeStream.getUrl(), \"rn\", ++sabrRequestNumber);\r\n    }\r\n\r\n    public VideoPlaybackAbrRequest createVideoPlaybackAbrRequest(int trackType, boolean isInit) {\r\n        SabrStream activeStream = sabrStreams.get(trackType);\r\n\r\n        if (activeStream == null) {\r\n            throw new IllegalStateException(\"Active SabrStream not found for track type \" + trackType);\r\n        }\r\n\r\n        Format selectedVideoFormat = getFormatSelector(C.TRACK_TYPE_VIDEO).getSelectedFormat();\r\n        Format selectedAudioFormat = getFormatSelector(C.TRACK_TYPE_AUDIO).getSelectedFormat();\r\n        int height = trackType == C.TRACK_TYPE_VIDEO && selectedVideoFormat != null\r\n                ? selectedVideoFormat.height : -1;\r\n        int bandwidthEstimate = trackType == C.TRACK_TYPE_VIDEO && selectedVideoFormat != null\r\n                ? selectedVideoFormat.bitrate : selectedAudioFormat != null ? selectedAudioFormat.bitrate : -1;\r\n\r\n        FormatId formatId = getFormatSelector(trackType).getSelectedFormatId();\r\n        long startTimeMs = isInit ? 0 : activeStream.getSegmentStartTimeMs(formatId != null ? formatId.getItag() : -1);\r\n\r\n        ClientAbrState.Builder clientAbrStateBuilder = ClientAbrState.newBuilder()\r\n                .setSabrForceMaxNetworkInterruptionDurationMs(0)\r\n                .setPlaybackRate(1)\r\n                .setPlayerTimeMs(startTimeMs)\r\n                .setClientViewportIsFlexible(false)\r\n                .setBandwidthEstimate(bandwidthEstimate)\r\n                .setDrcEnabled(false)\r\n                .setEnabledTrackTypesBitfield(height != -1 ? EnabledTrackTypes.VIDEO_ONLY : EnabledTrackTypes.AUDIO_ONLY);\r\n\r\n        if (height != -1) {\r\n            clientAbrStateBuilder\r\n                    .setStickyResolution(height)\r\n                    .setLastManualSelectedResolution(height);\r\n        }\r\n\r\n        ClientAbrState clientAbrState = clientAbrStateBuilder.build();\r\n\r\n        Pair<List<BufferedRange>, FormatId> bufferRanges = addBufferingInfoToAbrRequest(trackType);\r\n\r\n        List<FormatId> selectedFormats = createSelectedFormatIds(trackType);\r\n\r\n        if (isInit) {\r\n            selectedFormats.clear();\r\n        }\r\n\r\n        if (bufferRanges.second != null) {\r\n            selectedFormats.add(0, bufferRanges.second);\r\n        }\r\n\r\n        return VideoPlaybackAbrRequest.newBuilder()\r\n                .setClientAbrState(clientAbrState)\r\n                .addAllPreferredVideoFormatIds(getFormatSelector(C.TRACK_TYPE_VIDEO).formatIds)\r\n                .addAllPreferredAudioFormatIds(getFormatSelector(C.TRACK_TYPE_AUDIO).formatIds)\r\n                .addAllPreferredSubtitleFormatIds(getFormatSelector(C.TRACK_TYPE_TEXT).formatIds)\r\n                .addAllSelectedFormatIds(selectedFormats)\r\n                .addAllBufferedRanges(bufferRanges.first)\r\n                .setVideoPlaybackUstreamerConfig(\r\n                        ByteString.copyFrom(\r\n                                Base64.decode(videoPlaybackUstreamerConfig, Base64.URL_SAFE)\r\n                        )\r\n                )\r\n                .setStreamerContext(createStreamerContext(trackType))\r\n                .build();\r\n    }\r\n\r\n    /**\r\n     * Adds buffering information to the ABR request for all active formats.<br/><br/>\r\n     *\r\n     * NOTE:\r\n     * On the web, mobile, and TV clients, buffered ranges in combination to player time is what dictates the segments you get.\r\n     * In our case, we are cheating a bit by abusing the player time field (in clientAbrState), setting it to the exact start\r\n     * time value of the segment we want, while YouTube simply uses the actual player time.<br/><br/>\r\n     *\r\n     * We don't have to fully replicate this behavior for two reasons:\r\n     * 1. The SABR server will only send so much segments for a given player time. That means players like Shaka would\r\n     * not be able to buffer more than what the server thinks is enough. It would behave like YouTube's.\r\n     * 2. We don't have to know what segment a buffered range starts/ends at. It is easy to do in Shaka, but not in other players.\r\n     *\r\n     * @return The format to discard (if any) - typically formats that are active but not currently requested.\r\n     */\r\n    private Pair<List<BufferedRange>, FormatId> addBufferingInfoToAbrRequest(int trackType) {\r\n        SabrStream activeStream = sabrStreams.get(trackType);\r\n\r\n        if (activeStream == null) {\r\n            throw new IllegalStateException(\"Active SabrStream not found for track type \" + trackType);\r\n        }\r\n\r\n        FormatId audioFormat = getFormatSelector(C.TRACK_TYPE_AUDIO).getSelectedFormatId();\r\n        FormatId videoFormat = getFormatSelector(C.TRACK_TYPE_VIDEO).getSelectedFormatId();\r\n\r\n        FormatId formatToDiscard = null;\r\n        List<BufferedRange> bufferedRanges = new ArrayList<>();\r\n\r\n        FormatId currentFormat = trackType == C.TRACK_TYPE_VIDEO ? videoFormat : audioFormat;\r\n        int currentFormatKey = currentFormat != null ? currentFormat.getItag() : -1;\r\n\r\n        for (FormatId activeFormat : new FormatId[]{videoFormat, audioFormat}) {\r\n            if (activeFormat == null) {\r\n                continue;\r\n            }\r\n\r\n            int activeFormatKey = activeFormat.getItag();\r\n            boolean shouldDiscard = currentFormatKey != activeFormatKey;\r\n            MediaHeader initializedFormat = getInitializedFormat(activeFormatKey);\r\n\r\n            BufferedRange bufferedRange = shouldDiscard ? createFullBufferRange(activeFormat) : createPartialBufferRange(initializedFormat);\r\n\r\n            if (bufferedRange != null) {\r\n                bufferedRanges.add(bufferedRange);\r\n\r\n                if (shouldDiscard) {\r\n                    formatToDiscard = activeFormat;\r\n                }\r\n            }\r\n        }\r\n\r\n        return new Pair<>(bufferedRanges, formatToDiscard);\r\n    }\r\n\r\n    private @Nullable MediaHeader getInitializedFormat(int iTag) {\r\n        MediaHeader initializedFormat = null;\r\n\r\n        for (SabrStream sabrStream : sabrStreams.values()) {\r\n            MediaHeader mediaHeader = sabrStream.getInitializedFormat(iTag);\r\n\r\n            if (mediaHeader != null) {\r\n                initializedFormat = mediaHeader;\r\n                break;\r\n            }\r\n        }\r\n\r\n        return initializedFormat;\r\n    }\r\n\r\n    private @NonNull FormatSelector getFormatSelector(int trackType) {\r\n        SabrStream sabrStream = sabrStreams.get(trackType);\r\n\r\n        if (sabrStream == null) {\r\n            return emptySelector;\r\n        }\r\n\r\n        return sabrStream.getFormatSelector();\r\n    }\r\n\r\n    /**\r\n     * Creates a bogus buffered range for a format. Used when we want to signal to the server to not send any\r\n     * segments for this format.\r\n     * @param format - The format to create a full buffer range for.\r\n     * @return A BufferedRange object indicating the entire format is buffered.\r\n     */\r\n    private BufferedRange createFullBufferRange(@NonNull FormatId format) {\r\n        return BufferedRange.newBuilder()\r\n                .setFormatId(format)\r\n                .setDurationMs(Integer.MAX_VALUE)\r\n                .setStartTimeMs(0)\r\n                .setStartSegmentIndex(Integer.MAX_VALUE)\r\n                .setEndSegmentIndex(Integer.MAX_VALUE)\r\n                .setTimeRange(TimeRange.newBuilder()\r\n                        .setDurationTicks(Integer.MAX_VALUE)\r\n                        .setStartTicks(0)\r\n                        .setTimescale(1_000)\r\n                        .build())\r\n                .build();\r\n    }\r\n\r\n    /**\r\n     * Creates a buffered range representing a partially buffered format.\r\n     * @param initializedFormat - The format with initialization data.\r\n     * @return A BufferedRange object with segment information, or null if no metadata is available.\r\n     */\r\n    private BufferedRange createPartialBufferRange(MediaHeader initializedFormat) {\r\n        if (initializedFormat == null) {\r\n            return null;\r\n        }\r\n\r\n        int sequenceNumber = initializedFormat.hasSequenceNumber() ? initializedFormat.getSequenceNumber() : 1;\r\n        TimeRange timeRange = initializedFormat.hasTimeRange() ? initializedFormat.getTimeRange() : null;\r\n        int timeScale = timeRange != null && timeRange.hasTimescale() ? timeRange.getTimescale() : 1_000;\r\n        long startMs = initializedFormat.hasStartMs() ? initializedFormat.getStartMs() : 0;\r\n        long durationMs = initializedFormat.hasDurationMs() ? initializedFormat.getDurationMs() : 0;\r\n        return BufferedRange.newBuilder()\r\n                .setFormatId(initializedFormat.getFormatId())\r\n                .setStartSegmentIndex(sequenceNumber) // should be the real start position\r\n                .setEndSegmentIndex(sequenceNumber) // should be the real start position\r\n                .setStartTimeMs(0) // not used\r\n                .setDurationMs(durationMs)\r\n                .setTimeRange(TimeRange.newBuilder()\r\n                        .setTimescale(timeScale)\r\n                        .setStartTicks(0) // not used\r\n                        .setDurationTicks(durationMs)\r\n                        .build())\r\n                .build();\r\n    }\r\n\r\n    private List<FormatId> createSelectedFormatIds(int trackType) {\r\n        FormatSelector formatSelector = getFormatSelector(trackType);\r\n\r\n        return new ArrayList<>(formatSelector.formatIds);\r\n    }\r\n\r\n    private StreamerContext createStreamerContext(int trackType) {\r\n        SabrStream activeStream = sabrStreams.get(trackType);\r\n\r\n        if (activeStream == null) {\r\n            throw new IllegalStateException(\"Active SabrStream not found for track type \" + trackType);\r\n        }\r\n\r\n        return activeStream.createStreamerContext();\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/manifest/SabrManifestParser.java",
    "content": "package com.google.android.exoplayer2.source.sabr.manifest;\r\n\r\nimport android.text.TextUtils;\r\nimport android.util.Pair;\r\n\r\nimport androidx.annotation.NonNull;\r\n\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.drm.DrmInitData;\r\nimport com.google.android.exoplayer2.drm.DrmInitData.SchemeData;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.SegmentBase.SegmentList;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.SegmentBase.SegmentTemplate;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.SegmentBase.SegmentTimelineElement;\r\nimport com.google.android.exoplayer2.source.sabr.manifest.SegmentBase.SingleSegmentBase;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.StreamerContext.ClientInfo;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.StreamerContext.ClientName;\r\nimport com.google.android.exoplayer2.util.MimeTypes;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaFormat;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaItemFormatInfo;\r\nimport com.liskovsoft.mediaserviceinterfaces.data.MediaSubtitle;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.youtubeapi.formatbuilders.mpdbuilder.MediaFormatComparator;\r\nimport com.liskovsoft.youtubeapi.formatbuilders.utils.ITagUtils;\r\nimport com.liskovsoft.youtubeapi.formatbuilders.utils.MediaFormatUtils;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Collections;\r\nimport java.util.HashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\nimport java.util.Set;\r\nimport java.util.TreeSet;\r\n\r\npublic class SabrManifestParser {\r\n    private static final String TAG = SabrManifestParser.class.getSimpleName();\r\n    private int mId;\r\n    private static final String NULL_INDEX_RANGE = \"0-0\";\r\n    private static final String NULL_CONTENT_LENGTH = \"0\";\r\n    private static final int MAX_DURATION_SEC = 48 * 60 * 60;\r\n    private MediaItemFormatInfo mFormatInfo;\r\n    private Set<MediaFormat> mMP4Videos;\r\n    private Set<MediaFormat> mWEBMVideos;\r\n    private Map<String, Set<MediaFormat>> mMP4Audios;\r\n    private Map<String, Set<MediaFormat>> mWEBMAudios;\r\n    private List<MediaSubtitle> mSubs;\r\n\r\n    public SabrManifest parse(@NonNull MediaItemFormatInfo formatInfo) {\r\n        mFormatInfo = formatInfo;\r\n        MediaFormatComparator comp = new MediaFormatComparator();\r\n        mMP4Videos = new TreeSet<>(comp);\r\n        mWEBMVideos = new TreeSet<>(comp);\r\n        mMP4Audios = new HashMap<>();\r\n        mWEBMAudios = new HashMap<>();\r\n        mSubs = new ArrayList<>();\r\n        return parseSabrManifest(formatInfo);\r\n    }\r\n\r\n    private SabrManifest parseSabrManifest(MediaItemFormatInfo formatInfo) {\r\n        long availabilityStartTime = C.TIME_UNSET;\r\n        long durationMs = getDurationMs(formatInfo);\r\n        long minBufferTimeMs = 1500; // \"PT1.500S\"\r\n        long timeShiftBufferDepthMs = C.TIME_UNSET;\r\n        long suggestedPresentationDelayMs = C.TIME_UNSET;\r\n        long publishTimeMs = C.TIME_UNSET;\r\n        boolean dynamic = false;\r\n        long minUpdateTimeMs = C.TIME_UNSET; // 3155690800000L, \"P100Y\" no refresh (there is no dash url)\r\n\r\n        List<Period> periods = new ArrayList<>();\r\n\r\n        Pair<Period, Long> periodWithDurationMs = parsePeriod(formatInfo);\r\n        if (periodWithDurationMs != null) {\r\n            Period period = periodWithDurationMs.first;\r\n            periods.add(period);\r\n        }\r\n\r\n        return new SabrManifest(\r\n                availabilityStartTime,\r\n                durationMs,\r\n                minBufferTimeMs,\r\n                dynamic,\r\n                minUpdateTimeMs,\r\n                timeShiftBufferDepthMs,\r\n                suggestedPresentationDelayMs,\r\n                publishTimeMs,\r\n                periods,\r\n                formatInfo.getServerAbrStreamingUrl(),\r\n                formatInfo.getVideoPlaybackUstreamerConfig(),\r\n                formatInfo.getPoToken(),\r\n                formatInfo.getVideoId(),\r\n                createClientInfo(formatInfo));\r\n    }\r\n\r\n    private static long getDurationMs(MediaItemFormatInfo formatInfo) {\r\n        long lenSeconds = Helpers.parseLong(formatInfo.getLengthSeconds());\r\n        return lenSeconds > 0 ? lenSeconds * 1_000 : C.TIME_UNSET;\r\n    }\r\n\r\n    private Pair<Period, Long> parsePeriod(MediaItemFormatInfo formatInfo) {\r\n        String id = formatInfo.getVideoId();\r\n        long startMs = 0; // Should add real start time or make it unset?\r\n        long durationMs = getDurationMs(formatInfo);\r\n        List<AdaptationSet> adaptationSets = new ArrayList<>();\r\n\r\n        for (MediaFormat format : formatInfo.getAdaptiveFormats()) {\r\n            append(format);\r\n        }\r\n\r\n        if (formatInfo.getSubtitles() != null) {\r\n            append(formatInfo.getSubtitles());\r\n        }\r\n\r\n        // MXPlayer fix: write high quality formats first\r\n        if (!mMP4Videos.isEmpty()) {\r\n            adaptationSets.add(parseAdaptationSet(mMP4Videos));\r\n        }\r\n        if (!mWEBMVideos.isEmpty()) {\r\n            adaptationSets.add(parseAdaptationSet(mWEBMVideos));\r\n        }\r\n\r\n        for (Set<MediaFormat> formats : mMP4Audios.values()) {\r\n            adaptationSets.add(parseAdaptationSet(formats));\r\n        }\r\n\r\n        for (Set<MediaFormat> formats : mWEBMAudios.values()) {\r\n            adaptationSets.add(parseAdaptationSet(formats));\r\n        }\r\n\r\n        for (MediaSubtitle subtitle : mSubs) {\r\n            adaptationSets.add(parseAdaptationSet(Collections.singletonList(subtitle)));\r\n        }\r\n\r\n        return Pair.create(new Period(id, startMs, adaptationSets), durationMs);\r\n    }\r\n\r\n    private AdaptationSet parseAdaptationSet(Set<MediaFormat> formats) {\r\n        int id = mId++;\r\n        int contentType = C.TRACK_TYPE_UNKNOWN;\r\n        String label = null;\r\n        String drmSchemeType = null;\r\n        ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();\r\n        List<RepresentationInfo> representationInfos = new ArrayList<>();\r\n\r\n        for (MediaFormat format : formats) {\r\n            RepresentationInfo representationInfo = parseRepresentation(format);\r\n            if (contentType == C.TRACK_TYPE_UNKNOWN) {\r\n                contentType = getContentType(representationInfo.format);\r\n            }\r\n            representationInfos.add(representationInfo);\r\n        }\r\n\r\n        // Build the representations.\r\n        List<Representation> representations = new ArrayList<>(representationInfos.size());\r\n        for (int i = 0; i < representationInfos.size(); i++) {\r\n            representations.add(\r\n                    buildRepresentation(\r\n                            representationInfos.get(i),\r\n                            label,\r\n                            drmSchemeType,\r\n                            drmSchemeDatas));\r\n        }\r\n\r\n        return new AdaptationSet(id, contentType, representations);\r\n    }\r\n\r\n    private AdaptationSet parseAdaptationSet(List<MediaSubtitle> formats) {\r\n        int id = mId++;\r\n        int contentType = C.TRACK_TYPE_UNKNOWN;\r\n        String label = null;\r\n        String drmSchemeType = null;\r\n        ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();\r\n        List<RepresentationInfo> representationInfos = new ArrayList<>();\r\n\r\n        for (MediaSubtitle format : formats) {\r\n            RepresentationInfo representationInfo = parseRepresentation(format);\r\n            if (contentType == C.TRACK_TYPE_UNKNOWN) {\r\n                contentType = getContentType(representationInfo.format);\r\n            }\r\n            representationInfos.add(representationInfo);\r\n        }\r\n\r\n        // Build the representations.\r\n        List<Representation> representations = new ArrayList<>(representationInfos.size());\r\n        for (int i = 0; i < representationInfos.size(); i++) {\r\n            representations.add(\r\n                    buildRepresentation(\r\n                            representationInfos.get(i),\r\n                            label,\r\n                            drmSchemeType,\r\n                            drmSchemeDatas));\r\n        }\r\n\r\n        return new AdaptationSet(id, contentType, representations);\r\n    }\r\n\r\n    private SegmentTemplate parseSegmentTemplate(MediaFormat format) {\r\n        int unitsPerSecond = 1_000_000;\r\n\r\n        // Present on live streams only.\r\n        int segmentDurationUs = mFormatInfo.getSegmentDurationUs();\r\n\r\n        if (segmentDurationUs <= 0) {\r\n            // Inaccurate. Present on past (!) live streams.\r\n            segmentDurationUs = format.getTargetDurationSec() * 1_000_000;\r\n        }\r\n\r\n        int lengthSeconds = Integer.parseInt(mFormatInfo.getLengthSeconds());\r\n\r\n        if (mFormatInfo.isLive() || lengthSeconds <= 0) {\r\n            // For premiere streams (length > 0) or regular streams (length == 0) set window that exceeds normal limits - 48hrs\r\n            lengthSeconds = MAX_DURATION_SEC;\r\n        }\r\n\r\n        // To make long streams (12hrs) seekable we should decrease size of the segment a bit\r\n        //long segmentDurationUnits = (long) targetDurationSec * unitsPerSecond * 9999 / 10000;\r\n        int segmentDurationUnits = (int)(segmentDurationUs * (long) unitsPerSecond / 1_000_000);\r\n        // Increase count a bit to compensate previous tweak\r\n        //long segmentCount = (long) lengthSeconds / targetDurationSec * 10000 / 9999;\r\n        //int segmentCount = (int)(lengthSeconds * (long) unitsPerSecond / segmentDurationUnits);\r\n        int segmentCount = (int) Math.ceil(lengthSeconds * (double) unitsPerSecond / segmentDurationUnits);\r\n        // Increase offset a bit to compensate previous tweaks\r\n        // Streams to check:\r\n        // https://www.youtube.com/watch?v=drdemkJpgao\r\n        long offsetUnits = (long) segmentDurationUnits * mFormatInfo.getStartSegmentNum();\r\n\r\n        long timescale = unitsPerSecond;\r\n        long presentationTimeOffset = offsetUnits;\r\n        long duration = segmentDurationUnits;\r\n        long startNumber = mFormatInfo.getStartSegmentNum();\r\n        long endNumber = C.INDEX_UNSET;\r\n        String url = mFormatInfo.getServerAbrStreamingUrl();\r\n        UrlTemplate mediaTemplate = UrlTemplate.compile(url + \"&sq=$Number$\");\r\n        //UrlTemplate initializationTemplate = UrlTemplate.compile(format.getOtfInitUrl()); // ?\r\n        UrlTemplate initializationTemplate = null; // ?\r\n\r\n        RangedUri initialization = parseRangedUrl(format.getSourceUrl(), format.getInit());\r\n\r\n        List<SegmentTimelineElement> timeline = parseSegmentTimeline(offsetUnits, segmentDurationUnits, segmentCount);\r\n\r\n        return new SegmentTemplate(\r\n                initialization,\r\n                timescale,\r\n                presentationTimeOffset,\r\n                startNumber,\r\n                endNumber,\r\n                duration,\r\n                timeline,\r\n                initializationTemplate,\r\n                mediaTemplate);\r\n    }\r\n\r\n    private SegmentList parseSegmentList(MediaFormat format) {\r\n        long timescale = 1;\r\n        long presentationTimeOffset = 0;\r\n        long duration = C.TIME_UNSET;\r\n        long startNumber = 1;\r\n\r\n        RangedUri initialization = parseRangedUrl(format.getSourceUrl(), format.getInit());\r\n\r\n        List<SegmentTimelineElement> timeline = parseSegmentTimeline(format);\r\n\r\n        List<RangedUri> segments = parseSegmentUrl(format);\r\n\r\n        return new SegmentList(initialization, timescale, presentationTimeOffset,\r\n                startNumber, duration, timeline, segments);\r\n    }\r\n\r\n    private RangedUri parseRangedUrl(String urlText, String rangeText) {\r\n        long rangeStart = 0;\r\n        long rangeLength = C.LENGTH_UNSET;\r\n        if (rangeText != null) {\r\n            String[] rangeTextArray = rangeText.split(\"-\");\r\n            rangeStart = Long.parseLong(rangeTextArray[0]);\r\n            if (rangeTextArray.length == 2) {\r\n                rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1;\r\n            }\r\n        }\r\n\r\n        return new RangedUri(urlText, rangeStart, rangeLength);\r\n    }\r\n\r\n    private List<SegmentTimelineElement> parseSegmentTimeline(MediaFormat format) {\r\n        List<SegmentTimelineElement> timeline = new ArrayList<>();\r\n\r\n        if (format.getGlobalSegmentList() == null) {\r\n            return timeline;\r\n        }\r\n\r\n        // From writeGlobalSegmentList\r\n        long elapsedTime = 0;\r\n\r\n        // SegmentURL tag\r\n        for (String segment : format.getGlobalSegmentList()) {\r\n            long duration = Helpers.parseLong(segment, C.TIME_UNSET);\r\n            int count = 1;\r\n            for (int i = 0; i < count; i++) {\r\n                timeline.add(new SegmentTimelineElement(elapsedTime, duration));\r\n                elapsedTime += duration;\r\n            }\r\n        }\r\n\r\n        return timeline;\r\n    }\r\n\r\n    private List<SegmentTimelineElement> parseSegmentTimeline(long elapsedTime, long duration, int segmentCount) {\r\n        List<SegmentTimelineElement> timeline = new ArrayList<>();\r\n\r\n        // From writeLiveMediaSegmentList\r\n        int count = 1 + segmentCount;\r\n        for (int i = 0; i < count; i++) {\r\n            timeline.add(new SegmentTimelineElement(elapsedTime, duration));\r\n            elapsedTime += duration;\r\n        }\r\n\r\n        return timeline;\r\n    }\r\n\r\n    private List<RangedUri> parseSegmentUrl(MediaFormat format) {\r\n        List<RangedUri> segments = new ArrayList<>();\r\n\r\n        if (format.getSegmentUrlList() == null) {\r\n            return segments;\r\n        }\r\n\r\n        // SegmentURL tag\r\n        for (String url : format.getSegmentUrlList()) {\r\n            segments.add(parseRangedUrl(url, null));\r\n        }\r\n\r\n        return segments;\r\n    }\r\n\r\n    private SingleSegmentBase parseSegmentBase(MediaFormat format) {\r\n        long timescale = 1000;\r\n        long presentationTimeOffset = 0;\r\n\r\n        long indexStart = 0;\r\n        long indexLength = 0;\r\n        String indexRangeText = format.getIndex();\r\n        if (indexRangeText != null) {\r\n            String[] indexRange = indexRangeText.split(\"-\");\r\n            indexStart = Long.parseLong(indexRange[0]);\r\n            indexLength = Long.parseLong(indexRange[1]) - indexStart + 1;\r\n        }\r\n\r\n        RangedUri initialization = parseRangedUrl(format.getSourceUrl(), format.getInit());\r\n\r\n\r\n        return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, indexStart,\r\n                indexLength);\r\n    }\r\n\r\n    private RepresentationInfo parseRepresentation(MediaFormat mediaFormat) {\r\n        int roleFlags = C.ROLE_FLAG_MAIN;\r\n        int selectionFlags = C.SELECTION_FLAG_DEFAULT;\r\n        String id = mediaFormat.getITag();\r\n        int bandwidth = Helpers.parseInt(mediaFormat.getBitrate(), Format.NO_VALUE);\r\n        String mimeType = MediaFormatUtils.extractMimeType(mediaFormat);\r\n        String codecs = MediaFormatUtils.extractCodecs(mediaFormat);\r\n        int width = mediaFormat.getWidth();\r\n        int height = mediaFormat.getHeight();\r\n        float frameRate = Helpers.parseFloat(mediaFormat.getFps(), Format.NO_VALUE);\r\n        int audioChannels = Format.NO_VALUE;\r\n        int audioSamplingRate = Helpers.parseInt(ITagUtils.getAudioRateByTag(mediaFormat.getITag()), Format.NO_VALUE);\r\n        String language = mediaFormat.getLanguage();\r\n        String baseUrl = mFormatInfo.getServerAbrStreamingUrl();\r\n        String label = mediaFormat.getQualityLabel();\r\n        boolean isDrc = mediaFormat.isDrc();\r\n        long lastModified = Helpers.parseLong(mediaFormat.getLmt());\r\n        String drmSchemeType = null;\r\n        ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();\r\n\r\n        Format format =\r\n                buildFormat(\r\n                        id,\r\n                        label,\r\n                        mimeType,\r\n                        width,\r\n                        height,\r\n                        frameRate,\r\n                        audioChannels,\r\n                        audioSamplingRate,\r\n                        bandwidth,\r\n                        language,\r\n                        roleFlags,\r\n                        selectionFlags,\r\n                        codecs,\r\n                        isDrc,\r\n                        lastModified);\r\n\r\n        SegmentBase segmentBase = null;\r\n\r\n        if (MediaFormatUtils.isLiveMedia(baseUrl)) {\r\n            segmentBase = parseSegmentTemplate(mediaFormat);\r\n        } else if (mediaFormat.getSegmentUrlList() != null) {\r\n            segmentBase = parseSegmentList(mediaFormat);\r\n        } else if (mediaFormat.getIndex() != null &&\r\n                !mediaFormat.getIndex().equals(NULL_INDEX_RANGE)) { // json mediaFormat fix: index is null\r\n            segmentBase = parseSegmentBase(mediaFormat);\r\n        }\r\n\r\n        segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase();\r\n\r\n        return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, Representation.REVISION_ID_DEFAULT);\r\n    }\r\n\r\n    private RepresentationInfo parseRepresentation(MediaSubtitle sub) {\r\n        int roleFlags = C.ROLE_FLAG_SUBTITLE;\r\n        int selectionFlags = 0;\r\n        //String id = String.valueOf(mId++);\r\n        // keep mId incrementing, but use vssId as id to make it unique and more meaningful\r\n        mId++;\r\n        int bandwidth = 268;\r\n        String mimeType = sub.getMimeType();\r\n        String codecs = sub.getCodecs();\r\n        int width = Format.NO_VALUE;\r\n        int height = Format.NO_VALUE;\r\n        float frameRate = Format.NO_VALUE;\r\n        int audioChannels = Format.NO_VALUE;\r\n        int audioSamplingRate = Format.NO_VALUE;\r\n        String language = sub.getName() == null ? sub.getLanguageCode() : sub.getName();\r\n        String baseUrl = sub.getBaseUrl();\r\n        String label = null;\r\n        boolean isDrc = false;\r\n        long lastModified = Format.NO_VALUE;\r\n        String drmSchemeType = null;\r\n        ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();\r\n\r\n        Format format =\r\n                buildFormat(\r\n                        //id,\r\n                        // use vssId as id to make it unique\r\n                        sub.getVssId(),\r\n                        label,\r\n                        mimeType,\r\n                        width,\r\n                        height,\r\n                        frameRate,\r\n                        audioChannels,\r\n                        audioSamplingRate,\r\n                        bandwidth,\r\n                        language,\r\n                        roleFlags,\r\n                        selectionFlags,\r\n                        codecs,\r\n                        isDrc,\r\n                        lastModified);\r\n\r\n        SegmentBase segmentBase = new SingleSegmentBase();\r\n\r\n        return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, Representation.REVISION_ID_DEFAULT);\r\n    }\r\n\r\n    protected Representation buildRepresentation(\r\n            RepresentationInfo representationInfo,\r\n            String label,\r\n            String extraDrmSchemeType,\r\n            ArrayList<SchemeData> extraDrmSchemeDatas) {\r\n        Format format = representationInfo.format;\r\n        if (label != null) {\r\n            format = format.copyWithLabel(label);\r\n        }\r\n        String drmSchemeType = representationInfo.drmSchemeType != null\r\n                ? representationInfo.drmSchemeType : extraDrmSchemeType;\r\n        ArrayList<SchemeData> drmSchemeDatas = representationInfo.drmSchemeDatas;\r\n        drmSchemeDatas.addAll(extraDrmSchemeDatas);\r\n        if (!drmSchemeDatas.isEmpty()) {\r\n            filterRedundantIncompleteSchemeDatas(drmSchemeDatas);\r\n            DrmInitData drmInitData = new DrmInitData(drmSchemeType, drmSchemeDatas);\r\n            format = format.copyWithDrmInitData(drmInitData);\r\n        }\r\n        return Representation.newInstance(\r\n                representationInfo.revisionId,\r\n                format,\r\n                representationInfo.baseUrl,\r\n                representationInfo.segmentBase);\r\n    }\r\n\r\n    protected Format buildFormat(\r\n            String id,\r\n            String label,\r\n            String containerMimeType,\r\n            int width,\r\n            int height,\r\n            float frameRate,\r\n            int audioChannels,\r\n            int audioSamplingRate,\r\n            int bitrate,\r\n            String language,\r\n            @C.RoleFlags int roleFlags,\r\n            @C.SelectionFlags int selectionFlags,\r\n            String codecs,\r\n            boolean isDrc,\r\n            long lastModified) {\r\n        String sampleMimeType = getSampleMimeType(containerMimeType, codecs);\r\n        if (sampleMimeType != null) {\r\n            if (MimeTypes.isVideo(sampleMimeType)) {\r\n                return Format.createVideoContainerFormat(\r\n                        id,\r\n                        label,\r\n                        containerMimeType,\r\n                        sampleMimeType,\r\n                        codecs,\r\n                        /* metadata= */ null,\r\n                        bitrate,\r\n                        width,\r\n                        height,\r\n                        frameRate,\r\n                        /* initializationData= */ null,\r\n                        selectionFlags,\r\n                        roleFlags,\r\n                        lastModified);\r\n            } else if (MimeTypes.isAudio(sampleMimeType)) {\r\n                return Format.createAudioContainerFormat(\r\n                        id,\r\n                        label,\r\n                        containerMimeType,\r\n                        sampleMimeType,\r\n                        codecs,\r\n                        /* metadata= */ null,\r\n                        bitrate,\r\n                        audioChannels,\r\n                        audioSamplingRate,\r\n                        /* initializationData= */ null,\r\n                        selectionFlags,\r\n                        roleFlags,\r\n                        language,\r\n                        isDrc,\r\n                        lastModified);\r\n            } else if (mimeTypeIsRawText(sampleMimeType)) {\r\n                return Format.createTextContainerFormat(\r\n                        id,\r\n                        label,\r\n                        containerMimeType,\r\n                        sampleMimeType,\r\n                        codecs,\r\n                        bitrate,\r\n                        selectionFlags,\r\n                        roleFlags,\r\n                        language,\r\n                        Format.NO_VALUE);\r\n            }\r\n        }\r\n        return Format.createContainerFormat(\r\n                id,\r\n                label,\r\n                containerMimeType,\r\n                sampleMimeType,\r\n                codecs,\r\n                bitrate,\r\n                selectionFlags,\r\n                roleFlags,\r\n                language);\r\n    }\r\n\r\n    /**\r\n     * Derives a sample mimeType from a container mimeType and codecs attribute.\r\n     *\r\n     * @param containerMimeType The mimeType of the container.\r\n     * @param codecs The codecs attribute.\r\n     * @return The derived sample mimeType, or null if it could not be derived.\r\n     */\r\n    private static String getSampleMimeType(String containerMimeType, String codecs) {\r\n        if (MimeTypes.isAudio(containerMimeType)) {\r\n            return MimeTypes.getAudioMediaMimeType(codecs);\r\n        } else if (MimeTypes.isVideo(containerMimeType)) {\r\n            return MimeTypes.getVideoMediaMimeType(codecs);\r\n        } else if (mimeTypeIsRawText(containerMimeType)) {\r\n            return containerMimeType;\r\n        } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) {\r\n            if (codecs != null) {\r\n                if (codecs.startsWith(\"stpp\")) {\r\n                    return MimeTypes.APPLICATION_TTML;\r\n                } else if (codecs.startsWith(\"wvtt\")) {\r\n                    return MimeTypes.APPLICATION_MP4VTT;\r\n                }\r\n            }\r\n        } else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {\r\n            if (codecs != null) {\r\n                if (codecs.contains(\"cea708\")) {\r\n                    return MimeTypes.APPLICATION_CEA708;\r\n                } else if (codecs.contains(\"eia608\") || codecs.contains(\"cea608\")) {\r\n                    return MimeTypes.APPLICATION_CEA608;\r\n                }\r\n            }\r\n            return null;\r\n        }\r\n        return null;\r\n    }\r\n\r\n    /**\r\n     * Returns whether a mimeType is a text sample mimeType.\r\n     *\r\n     * @param mimeType The mimeType.\r\n     * @return Whether the mimeType is a text sample mimeType.\r\n     */\r\n    private static boolean mimeTypeIsRawText(String mimeType) {\r\n        return MimeTypes.isText(mimeType)\r\n                || MimeTypes.APPLICATION_TTML.equals(mimeType)\r\n                || MimeTypes.APPLICATION_MP4VTT.equals(mimeType)\r\n                || MimeTypes.APPLICATION_CEA708.equals(mimeType)\r\n                || MimeTypes.APPLICATION_CEA608.equals(mimeType);\r\n    }\r\n\r\n    /**\r\n     * Removes unnecessary {@link SchemeData}s with null {@link SchemeData#data}.\r\n     */\r\n    private static void filterRedundantIncompleteSchemeDatas(ArrayList<SchemeData> schemeDatas) {\r\n        for (int i = schemeDatas.size() - 1; i >= 0; i--) {\r\n            SchemeData schemeData = schemeDatas.get(i);\r\n            if (!schemeData.hasData()) {\r\n                for (int j = 0; j < schemeDatas.size(); j++) {\r\n                    if (schemeDatas.get(j).canReplace(schemeData)) {\r\n                        // schemeData is incomplete, but there is another matching SchemeData which does contain\r\n                        // data, so we remove the incomplete one.\r\n                        schemeDatas.remove(i);\r\n                        break;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    private void append(List<MediaSubtitle> subs) {\r\n        mSubs.addAll(subs);\r\n    }\r\n\r\n    private void append(MediaSubtitle sub) {\r\n        mSubs.add(sub);\r\n    }\r\n\r\n    private void append(MediaFormat mediaItem) {\r\n        //if (!MediaFormatUtils.checkMediaUrl(mediaItem)) {\r\n        //    Log.e(TAG, \"Media item doesn't contain required url field!\");\r\n        //    return;\r\n        //}\r\n\r\n        // NOTE: FORMAT_STREAM_TYPE_OTF not supported\r\n        if (!MediaFormatUtils.isDash(mediaItem)) {\r\n            return;\r\n        }\r\n\r\n        //fixOTF(mediaItem);\r\n\r\n        Set<MediaFormat> placeholder = null;\r\n        String mimeType = MediaFormatUtils.extractMimeType(mediaItem);\r\n        if (mimeType != null) {\r\n            switch (mimeType) {\r\n                case MediaFormatUtils.MIME_MP4_VIDEO:\r\n                    placeholder = mMP4Videos;\r\n                    break;\r\n                case MediaFormatUtils.MIME_WEBM_VIDEO:\r\n                    placeholder = mWEBMVideos;\r\n                    break;\r\n                case MediaFormatUtils.MIME_MP4_AUDIO:\r\n                    placeholder = getMP4Audios(mediaItem.getLanguage());\r\n                    break;\r\n                case MediaFormatUtils.MIME_WEBM_AUDIO:\r\n                    placeholder = getWEBMAudios(mediaItem.getLanguage());\r\n                    break;\r\n            }\r\n        }\r\n\r\n        if (placeholder != null) {\r\n            placeholder.add(mediaItem); // NOTE: reverse order\r\n        }\r\n    }\r\n\r\n    private Set<MediaFormat> getMP4Audios(String language) {\r\n        return getFormats(mMP4Audios, language);\r\n    }\r\n\r\n    private Set<MediaFormat> getWEBMAudios(String language) {\r\n        return getFormats(mWEBMAudios, language);\r\n    }\r\n\r\n    private static Set<MediaFormat> getFormats(Map<String, Set<MediaFormat>> formatMap, String language) {\r\n        if (language == null) {\r\n            language = \"default\";\r\n        }\r\n\r\n        Set<MediaFormat> mediaFormats = formatMap.get(language);\r\n\r\n        if (mediaFormats == null) {\r\n            mediaFormats = new TreeSet<>(new MediaFormatComparator());\r\n            formatMap.put(language, mediaFormats);\r\n        }\r\n\r\n        return mediaFormats;\r\n    }\r\n\r\n    protected int getContentType(Format format) {\r\n        String sampleMimeType = format.sampleMimeType;\r\n        if (TextUtils.isEmpty(sampleMimeType)) {\r\n            return C.TRACK_TYPE_UNKNOWN;\r\n        } else if (MimeTypes.isVideo(sampleMimeType)) {\r\n            return C.TRACK_TYPE_VIDEO;\r\n        } else if (MimeTypes.isAudio(sampleMimeType)) {\r\n            return C.TRACK_TYPE_AUDIO;\r\n        } else if (mimeTypeIsRawText(sampleMimeType)) {\r\n            return C.TRACK_TYPE_TEXT;\r\n        }\r\n        return C.TRACK_TYPE_UNKNOWN;\r\n    }\r\n\r\n    private ClientInfo createClientInfo(MediaItemFormatInfo formatInfo) {\r\n        MediaItemFormatInfo.ClientInfo clientInfo = formatInfo.getClientInfo();\r\n\r\n        return ClientInfo.newBuilder()\r\n                .setClientName(ClientName.valueOf(clientInfo.getClientName()))\r\n                .setClientVersion(clientInfo.getClientVersion())\r\n                .setOsName(clientInfo.getOsName())\r\n                .setOsVersion(clientInfo.getOsVersion())\r\n                .build();\r\n    }\r\n\r\n    /** A parsed Representation element. */\r\n    protected static final class RepresentationInfo {\r\n\r\n        public final Format format;\r\n        public final String baseUrl;\r\n        public final SegmentBase segmentBase;\r\n        public final String drmSchemeType;\r\n        public final ArrayList<SchemeData> drmSchemeDatas;\r\n        public final long revisionId;\r\n\r\n        public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase,\r\n                                  String drmSchemeType, ArrayList<SchemeData> drmSchemeDatas,\r\n                                  long revisionId) {\r\n            this.format = format;\r\n            this.baseUrl = baseUrl;\r\n            this.segmentBase = segmentBase;\r\n            this.drmSchemeType = drmSchemeType;\r\n            this.drmSchemeDatas = drmSchemeDatas;\r\n            this.revisionId = revisionId;\r\n        }\r\n\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/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.sabr.manifest;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.source.sabr.SabrSegmentIndex;\nimport com.google.android.exoplayer2.util.Util;\n\nimport java.util.List;\n\n/**\n * An approximate representation of a SegmentBase manifest element.\n */\npublic abstract class SegmentBase {\n\n  /* package */ 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(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  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(RangedUri initialization, long timescale, long presentationTimeOffset,\n        long indexStart, long indexLength) {\n      super(initialization, timescale, presentationTimeOffset);\n      this.indexStart = indexStart;\n      this.indexLength = indexLength;\n    }\n\n    public SingleSegmentBase() {\n      this(null, 1, 0, 0, 0);\n    }\n\n    public RangedUri getIndex() {\n      return indexLength <= 0 ? null : new RangedUri(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 */ 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        RangedUri initialization,\n        long timescale,\n        long presentationTimeOffset,\n        long startNumber,\n        long duration,\n        List<SegmentTimelineElement> segmentTimeline) {\n      super(initialization, timescale, presentationTimeOffset);\n      this.startNumber = startNumber;\n      this.duration = duration;\n      this.segmentTimeline = segmentTimeline;\n    }\n\n    /** @see SabrSegmentIndex#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 == SabrSegmentIndex.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 SabrSegmentIndex#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 != SabrSegmentIndex.INDEX_UNBOUNDED\n            && sequenceNumber == (getFirstSegmentNum() + segmentCount - 1)\n            ? (periodDurationUs - getSegmentTimeUs(sequenceNumber))\n            : ((duration * C.MICROS_PER_SECOND) / timescale);\n      }\n    }\n\n    /** @see SabrSegmentIndex#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 SabrSegmentIndex#getSegmentUrl(long)\n     */\n    public abstract RangedUri getSegmentUrl(Representation representation, long index);\n\n    /** @see SabrSegmentIndex#getFirstSegmentNum() */\n    public long getFirstSegmentNum() {\n      return startNumber;\n    }\n\n    /**\n     * @see SabrSegmentIndex#getSegmentCount(long)\n     */\n    public abstract int getSegmentCount(long periodDurationUs);\n\n    /**\n     * @see SabrSegmentIndex#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 */ 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        List<SegmentTimelineElement> segmentTimeline,\n        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 */ final UrlTemplate initializationTemplate;\n    /* package */ 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        List<SegmentTimelineElement> segmentTimeline,\n        UrlTemplate initializationTemplate,\n        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    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 SabrSegmentIndex.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  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/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.sabr.manifest;\n\nimport com.google.android.exoplayer2.source.sabr.SabrSegmentIndex;\n\n/**\n * A {@link SabrSegmentIndex} that defines a single segment.\n */\n/* package */ final class SingleSegmentIndex implements SabrSegmentIndex {\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-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/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.sabr.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-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/SabrProcessor.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser;\r\n\r\nimport android.util.Base64;\r\n\r\nimport androidx.annotation.NonNull;\r\n\r\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\r\nimport com.google.android.exoplayer2.source.sabr.parser.exceptions.SabrStreamError;\r\nimport com.google.android.exoplayer2.source.sabr.parser.misc.Utils;\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.ConsumedRange;\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.FormatSelector;\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.Segment;\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.SelectedFormat;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.FormatInitializedSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSeekSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSegmentDataSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSegmentEndSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSegmentInitSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.PoTokenStatusSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.PoTokenStatusSabrPart.PoTokenStatus;\r\nimport com.google.android.exoplayer2.source.sabr.parser.results.ProcessFormatInitializationMetadataResult;\r\nimport com.google.android.exoplayer2.source.sabr.parser.results.ProcessLiveMetadataResult;\r\nimport com.google.android.exoplayer2.source.sabr.parser.results.ProcessMediaEndResult;\r\nimport com.google.android.exoplayer2.source.sabr.parser.results.ProcessMediaHeaderResult;\r\nimport com.google.android.exoplayer2.source.sabr.parser.results.ProcessMediaResult;\r\nimport com.google.android.exoplayer2.source.sabr.parser.results.ProcessSabrSeekResult;\r\nimport com.google.android.exoplayer2.source.sabr.parser.results.ProcessStreamProtectionStatusResult;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.FormatInitializationMetadata;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.LiveMetadata;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.MediaHeader;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.NextRequestPolicy;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.SabrContextSendingPolicy;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.SabrContextUpdate;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.SabrSeek;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.StreamProtectionStatus;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.StreamProtectionStatus.Status;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.StreamerContext;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.StreamerContext.ClientInfo;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.StreamerContext.SabrContext;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.TimeRange;\r\nimport com.google.protobuf.ByteString;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\n\r\nimport java.io.IOException;\r\nimport java.util.ArrayList;\r\nimport java.util.HashMap;\r\nimport java.util.HashSet;\r\nimport java.util.List;\r\nimport java.util.Map;\r\nimport java.util.Set;\r\n\r\npublic class SabrProcessor {\r\n    private static final String TAG = SabrProcessor.class.getSimpleName();\r\n    private static final int NO_VALUE = -1;\r\n    private final String videoPlaybackUstreamerConfig;\r\n    private final ClientInfo clientInfo;\r\n    private FormatSelector formatSelector;\r\n    private final int liveSegmentTargetDurationToleranceMs;\r\n    private final int liveSegmentTargetDurationSec;\r\n    private long playerTimeMs;\r\n    private final String poToken;\r\n    private final boolean postLive;\r\n    private final String videoId;\r\n    private final long durationMs;\r\n    private final Map<Long, Segment> partialSegments;\r\n    private final Map<String, SelectedFormat> selectedFormats;\r\n    private Status streamProtectionStatus;\r\n    private boolean isLive;\r\n    private LiveMetadata liveMetadata;\r\n    private long totalDurationMs;\r\n    private NextRequestPolicy nextRequestPolicy;\r\n    private final Map<Integer, SabrContextUpdate> sabrContextUpdates;\r\n    private final Set<Integer> sabrContextsToSend;\r\n    private final Map<Integer, MediaHeader> initializedFormats;\r\n    private final FormatSelector emptySelector;\r\n\r\n    public SabrProcessor(\r\n            @NonNull String videoPlaybackUstreamerConfig,\r\n            @NonNull ClientInfo clientInfo,\r\n            int liveSegmentTargetDurationSec,\r\n            int liveSegmentTargetDurationToleranceMs,\r\n            long playerTimeMs,\r\n            String poToken,\r\n            boolean postLive,\r\n            String videoId,\r\n            long durationMs\r\n    ) {\r\n        this.videoPlaybackUstreamerConfig = videoPlaybackUstreamerConfig;\r\n        this.poToken = poToken;\r\n        this.clientInfo = clientInfo;\r\n        this.liveSegmentTargetDurationSec = liveSegmentTargetDurationSec != NO_VALUE ? liveSegmentTargetDurationSec : 5;\r\n        this.liveSegmentTargetDurationToleranceMs = liveSegmentTargetDurationToleranceMs != NO_VALUE ? liveSegmentTargetDurationToleranceMs : 100;\r\n        if (this.liveSegmentTargetDurationToleranceMs >= (this.liveSegmentTargetDurationSec * 1_000) / 2) {\r\n            throw new IllegalArgumentException(\"liveSegmentTargetDurationToleranceMs must be less than half of liveSegmentTargetDurationSec in milliseconds\");\r\n        }\r\n        this.playerTimeMs = playerTimeMs != NO_VALUE ? playerTimeMs : 0;\r\n        if (this.playerTimeMs < 0) {\r\n            throw new IllegalArgumentException(\"start_time_ms must be greater than or equal to 0\");\r\n        }\r\n\r\n        this.postLive = postLive;\r\n        isLive = false;\r\n        this.videoId = videoId;\r\n        this.durationMs = durationMs;\r\n\r\n        //audioFormatSelector = audioSelection;\r\n        //videoFormatSelector = videoSelection;\r\n        //captionFormatSelector = captionSelection;\r\n\r\n        // IMPORTANT: initialized formats is assumed to contain only ACTIVE formats\r\n        selectedFormats = new HashMap<>();\r\n\r\n        partialSegments = new HashMap<>();\r\n        totalDurationMs = NO_VALUE;\r\n        sabrContextsToSend = new HashSet<>();\r\n        sabrContextUpdates = new HashMap<>();\r\n        initializedFormats = new HashMap<>();\r\n        emptySelector = new FormatSelector(\"ignore\", true);\r\n        initializeFormatSelector();\r\n    }\r\n\r\n    public long getPlayerTimeMs() {\r\n        return playerTimeMs;\r\n    }\r\n\r\n    public void setPlayerTimeMs(long playerTimeMs) {\r\n        this.playerTimeMs = playerTimeMs;\r\n    }\r\n\r\n    private void initializeFormatSelector() {\r\n        if (formatSelector == null) {\r\n            formatSelector = emptySelector;\r\n        }\r\n\r\n        //int enabledTrackTypesBitfield = 0;  // Audio+Video\r\n\r\n        //if (videoFormatSelector.discardMedia) {\r\n        //    enabledTrackTypesBitfield = 1; // Audio only\r\n        //}\r\n        //\r\n        //if (!captionFormatSelector.discardMedia) {\r\n        //    // SABR does not support caption-only or audio+captions only - can only get audio+video with captions\r\n        //    // If audio or video is not selected, the tracks will be initialized but marked as buffered.\r\n        //    enabledTrackTypesBitfield = 7;\r\n        //}\r\n        //\r\n        //Log.d(TAG, \"Starting playback at: %sms\", playerTimeMs);\r\n        //clientAbrState = ClientAbrState.newBuilder()\r\n        //        .setPlayerTimeMs(playerTimeMs)\r\n        //        .setEnabledTrackTypesBitfield(enabledTrackTypesBitfield)\r\n        //        .setDrcEnabled(false) // Required to stream DRC formats\r\n        //        .build();\r\n    }\r\n\r\n    public ProcessMediaHeaderResult processMediaHeader(MediaHeader mediaHeader) {\r\n        if (mediaHeader.hasVideoId() && videoId != null && !Helpers.equals(mediaHeader.getVideoId(), videoId)) {\r\n            throw new SabrStreamError(\r\n                    String.format(\"Received unexpected MediaHeader for video %s (expecting %s)\", mediaHeader.getVideoId(), videoId));\r\n        }\r\n\r\n        if (!mediaHeader.hasFormatId()) {\r\n            throw new SabrStreamError(String.format(\"FormatId not found in MediaHeader (media_header=%s)\", mediaHeader));\r\n        }\r\n\r\n        // MOD: triggered even when no match found (probably multi thread issue?)\r\n        // Guard. This should not happen, except if we don't clear partial segments\r\n        //if (partialSegments.containsKey(Utils.toLong(mediaHeader.getHeaderId()))) {\r\n        //    throw new SabrStreamError(String.format(\"Header ID %s already exists\", mediaHeader.getHeaderId()));\r\n        //}\r\n\r\n        SelectedFormat initializedFormat = selectedFormats.get(mediaHeader.getFormatId().toString());\r\n\r\n        if (initializedFormat == null) {\r\n            throw new SabrStreamError(String.format(\"Initialized format not found for %s\", mediaHeader.getFormatId()));\r\n        }\r\n\r\n        if (mediaHeader.hasCompressionAlgorithm()) {\r\n            // Unknown when this is used, but it is not supported currently\r\n            throw new SabrStreamError(String.format(\"Compression not supported in MediaHeader (media_header=%s)\", mediaHeader));\r\n        }\r\n\r\n        int sequenceNumber = mediaHeader.hasSequenceNumber() ? mediaHeader.getSequenceNumber() : NO_VALUE;\r\n        boolean isInitSegment = mediaHeader.getIsInitSeg();\r\n\r\n        if (sequenceNumber == NO_VALUE && !isInitSegment) {\r\n            throw new SabrStreamError(String.format(\"Sequence number not found in MediaHeader (media_header=%s)\", mediaHeader));\r\n        }\r\n\r\n        initializedFormat.sequenceLmt = mediaHeader.hasSequenceLmt() ? mediaHeader.getSequenceLmt() : NO_VALUE;\r\n\r\n        TimeRange timeRange = mediaHeader.hasTimeRange() ? mediaHeader.getTimeRange() : null;\r\n        long startMs = mediaHeader.hasStartMs() ? mediaHeader.getStartMs()\r\n                : timeRange != null && timeRange.hasStartTicks() && timeRange.hasTimescale()\r\n                    ? Utils.ticksToMs(timeRange.getStartTicks(), timeRange.getTimescale())\r\n                : 0;\r\n\r\n        // Calculate duration of this segment\r\n        // For videos, either duration_ms or time_range should be present\r\n        // For live streams, calculate segment duration based on live metadata target segment duration\r\n        long actualDurationMs = mediaHeader.hasDurationMs() ? mediaHeader.getDurationMs()\r\n                : timeRange != null && timeRange.hasDurationTicks() && timeRange.hasTimescale()\r\n                    ? Utils.ticksToMs(timeRange.getDurationTicks(), timeRange.getTimescale())\r\n                : NO_VALUE;\r\n\r\n        int estimatedDurationMs = NO_VALUE;\r\n        if (isLive()) {\r\n            // Underestimate the duration of the segment slightly as\r\n            // the real duration may be slightly shorter than the target duration.\r\n            estimatedDurationMs = (getLiveSegmentTargetDurationSec() * 1_000) - getLiveSegmentTargetDurationToleranceMs();\r\n        } else if (isInitSegment) {\r\n            estimatedDurationMs = 0;\r\n        }\r\n\r\n        long durationMs = actualDurationMs != NO_VALUE ? actualDurationMs : estimatedDurationMs;\r\n\r\n        // Guard: Bail out if we cannot determine the duration, which we need to progress.\r\n        if (durationMs == NO_VALUE) {\r\n            throw new SabrStreamError(\r\n                    String.format(\"Cannot determine duration of segment %s (media_header=%s)\", sequenceNumber, mediaHeader));\r\n        }\r\n\r\n        long estimatedContentLength = NO_VALUE;\r\n        if (isLive() && !mediaHeader.hasContentLength() && mediaHeader.hasBitrateBps()) {\r\n            estimatedContentLength = (long) Math.ceil(mediaHeader.getBitrateBps() * ((double) durationMs / 1_000));\r\n        }\r\n\r\n        Segment segment = new Segment(\r\n                mediaHeader,\r\n                mediaHeader.getFormatId(),\r\n                isInitSegment,\r\n                durationMs,\r\n                mediaHeader.hasStartRange() ? mediaHeader.getStartRange() : NO_VALUE,\r\n                sequenceNumber,\r\n                mediaHeader.hasContentLength() ? mediaHeader.getContentLength() : estimatedContentLength,\r\n                estimatedContentLength != NO_VALUE,\r\n                startMs,\r\n                initializedFormat,\r\n                actualDurationMs == 0 || actualDurationMs == NO_VALUE,\r\n                false,\r\n                false,\r\n                mediaHeader.hasSequenceLmt() ? mediaHeader.getSequenceLmt() : NO_VALUE\r\n        );\r\n\r\n        partialSegments.put(Utils.toLong(mediaHeader.getHeaderId()), segment);\r\n\r\n        ProcessMediaHeaderResult result = new ProcessMediaHeaderResult();\r\n\r\n        if (!segment.discard) {\r\n            result.sabrPart = new MediaSegmentInitSabrPart(\r\n                    segment.initializedFormat.formatSelector,\r\n                    segment.formatId,\r\n                    playerTimeMs > 0 ? playerTimeMs : NO_VALUE,\r\n                    segment.sequenceNumber,\r\n                    segment.initializedFormat.totalSegments,\r\n                    segment.durationMs,\r\n                    segment.durationEstimated,\r\n                    segment.startRange,\r\n                    segment.startMs,\r\n                    segment.isInitSegment,\r\n                    segment.contentLength,\r\n                    segment.contentLengthEstimated\r\n            );\r\n        }\r\n\r\n        Log.d(TAG, \"Initialized Media Header %s for sequence %s. Segment: %s\",\r\n                Utils.toLong(mediaHeader.getHeaderId()), sequenceNumber, segment);\r\n\r\n        return result;\r\n    }\r\n\r\n    public ProcessMediaResult processMedia(long headerId, int contentLength, ExtractorInput data) throws IOException, InterruptedException {\r\n        Segment segment = partialSegments.get(headerId);\r\n        if (segment == null) {\r\n            Log.e(TAG, \"Header ID %s not found\", headerId);\r\n            throw new SabrStreamError(String.format(\"Header ID %s not found in partial segments\", headerId));\r\n        }\r\n\r\n        int segmentStartBytes = segment.receivedDataLength;\r\n        segment.receivedDataLength += contentLength;\r\n\r\n        ProcessMediaResult result = new ProcessMediaResult();\r\n\r\n        if (!segment.discard) {\r\n            Log.d(TAG, \"processMedia: part created. contentLength: %s\", contentLength);\r\n            result.sabrPart = new MediaSegmentDataSabrPart(\r\n                    segment.initializedFormat.formatSelector,\r\n                    segment.formatId,\r\n                    segment.sequenceNumber,\r\n                    segment.isInitSegment,\r\n                    segment.initializedFormat.totalSegments,\r\n                    segment.startMs,\r\n                    data,\r\n                    contentLength,\r\n                    segmentStartBytes\r\n            );\r\n        } else {\r\n            Log.e(TAG, \"processMedia: part discarded. contentLength: %s, itag: %s\", contentLength, segment.formatId.getItag());\r\n            data.skipFully(contentLength);\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    public ProcessMediaEndResult processMediaEnd(long headerId) {\r\n        Segment segment = partialSegments.remove(headerId);\r\n        if (segment == null) {\r\n            Log.e(TAG, \"Header ID %s not found\", headerId);\r\n            throw new SabrStreamError(String.format(\"Header ID %s not found in partial segments\", headerId));\r\n        }\r\n\r\n        if (!segment.mediaHeader.getIsInitSeg()) {\r\n            initializedFormats.put(segment.mediaHeader.getItag(), segment.mediaHeader);\r\n        }\r\n\r\n        Log.d(TAG, \"MediaEnd for %s (sequence %s, data length = %s)\",\r\n                segment.formatId, segment.sequenceNumber, segment.receivedDataLength);\r\n\r\n        // MOD: remove. receivedDataLength != segment.contentLength\r\n        //if (segment.contentLength != -1 && segment.receivedDataLength != segment.contentLength) {\r\n        //    if (segment.contentLengthEstimated) {\r\n        //        Log.d(TAG, \"Content length for %s (sequence %s) was estimated, \" +\r\n        //                \"estimated %s bytes, got %s bytes\",\r\n        //                segment.formatId, segment.sequenceNumber, segment.contentLength, segment.receivedDataLength);\r\n        //    } else {\r\n        //        throw new SabrStreamError(String.format(\"Content length mismatch for %s (sequence %s): \" +\r\n        //                \"expected %s bytes, got %s bytes\",\r\n        //                segment.formatId, segment.sequenceNumber, segment.contentLength, segment.receivedDataLength));\r\n        //    }\r\n        //}\r\n\r\n        ProcessMediaEndResult result = new ProcessMediaEndResult();\r\n\r\n        // Only count received segments as new segments if they are not consumed.\r\n        // Discarded segments that are not consumed are considered new segments.\r\n        if (!segment.consumed) {\r\n            result.isNewSegment = true;\r\n        }\r\n\r\n        // Return the segment here instead of during MEDIA part(s) because:\r\n        // 1. We can validate that we received the correct data length\r\n        // 2. In the case of a retry during segment media, the partial data is not sent to the consumer\r\n        if (!segment.discard) {\r\n            // This needs to be yielded AFTER we have processed the segment\r\n            // So the consumer can see the updated consumed ranges and use them for e.g. syncing between concurrent streams\r\n            result.sabrPart = new MediaSegmentEndSabrPart(\r\n                    segment.initializedFormat.formatSelector,\r\n                    segment.formatId,\r\n                    segment.sequenceNumber,\r\n                    segment.isInitSegment,\r\n                    segment.initializedFormat.totalSegments,\r\n                    segment.startMs,\r\n                    segment.durationMs\r\n            );\r\n        } else {\r\n            Log.d(TAG, \"Discarding media for %s\", segment.initializedFormat.formatId);\r\n        }\r\n\r\n        if (segment.isInitSegment) {\r\n            segment.initializedFormat.initSegment = segment;\r\n            // Do not create a consumed range for init segments\r\n            return result;\r\n        }\r\n\r\n        if (segment.initializedFormat.currentSegment != null && isLive()) {\r\n            Segment previousSegment = segment.initializedFormat.currentSegment;\r\n            Log.d(TAG, \"Previous segment %s for format %s \" +\r\n                    \"estimated duration difference from this segment (%s): %sms\",\r\n                    previousSegment.sequenceNumber, segment.formatId, segment.sequenceNumber,\r\n                    segment.startMs - (previousSegment.startMs + previousSegment.durationMs));\r\n        }\r\n\r\n        segment.initializedFormat.currentSegment = segment;\r\n\r\n        if (segment.consumed) {\r\n            // Segment is already consumed, do not create a new consumed range. It was probably discarded.\r\n            // This can be expected to happen in the case of video-only, where we discard the audio track (and mark it as entirely buffered)\r\n            // We still want to create/update consumed range for discarded media IF it is not already consumed\r\n            Log.d(TAG, \"%s} segment %s already consumed, not creating or updating consumed range (discard=%s)\",\r\n                    segment.formatId, segment.sequenceNumber, segment.discard);\r\n            return result;\r\n        }\r\n\r\n        // Try to find a consumed range for this segment in sequence\r\n        ConsumedRange consumedRange =\r\n                Helpers.findFirst(segment.initializedFormat.consumedRanges, cr -> cr.endSequenceNumber == segment.sequenceNumber - 1);\r\n\r\n        if (consumedRange == null) {\r\n            // Create a new consumed range starting from this segment\r\n            segment.initializedFormat.consumedRanges.add(new ConsumedRange(\r\n                    segment.startMs,\r\n                    segment.durationMs,\r\n                    segment.sequenceNumber,\r\n                    segment.sequenceNumber\r\n            ));\r\n            Log.d(TAG, \"Created new consumed range for %s %s\",\r\n                    segment.initializedFormat.formatId, segment.initializedFormat.consumedRanges.get(segment.initializedFormat.consumedRanges.size() - 1));\r\n            return result;\r\n        }\r\n\r\n        // Update the existing consumed range to include this segment\r\n        consumedRange.endSequenceNumber = segment.sequenceNumber;\r\n        consumedRange.durationMs = (segment.startMs - consumedRange.startTimeMs) + segment.durationMs;\r\n\r\n        // TODO: Conduct a seek on consumed ranges\r\n\r\n        return result;\r\n    }\r\n\r\n    public ProcessStreamProtectionStatusResult processStreamProtectionStatus(StreamProtectionStatus streamProtectionStatus) {\r\n        this.streamProtectionStatus = streamProtectionStatus.hasStatus() ? streamProtectionStatus.getStatus() : null;\r\n        Status status = streamProtectionStatus.getStatus();\r\n        String poToken = this.poToken;\r\n        PoTokenStatus resultStatus = null;\r\n\r\n        if (status == Status.OK) {\r\n            resultStatus = poToken != null ? PoTokenStatus.OK : PoTokenStatus.NOT_REQUIRED;\r\n        } else if (status == Status.ATTESTATION_PENDING) {\r\n            resultStatus = poToken != null ? PoTokenStatus.PENDING : PoTokenStatus.PENDING_MISSING;\r\n        } else if (status == Status.ATTESTATION_REQUIRED) {\r\n            resultStatus = poToken != null ? PoTokenStatus.INVALID : PoTokenStatus.MISSING;\r\n        } else {\r\n            Log.w(TAG, \"Received an unknown StreamProtectionStatus: %s\", streamProtectionStatus);\r\n        }\r\n\r\n        ProcessStreamProtectionStatusResult result = new ProcessStreamProtectionStatusResult();\r\n\r\n        if (resultStatus != null) {\r\n            result.sabrPart = new PoTokenStatusSabrPart(resultStatus);\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    public ProcessFormatInitializationMetadataResult processFormatInitializationMetadata(FormatInitializationMetadata formatInitMetadata) {\r\n        ProcessFormatInitializationMetadataResult result = new ProcessFormatInitializationMetadataResult();\r\n\r\n        if (formatInitMetadata.hasFormatId() && selectedFormats.containsKey(formatInitMetadata.getFormatId().toString())) {\r\n            Log.d(TAG, \"Format %s already initialized\", formatInitMetadata.getFormatId());\r\n            return result;\r\n        }\r\n\r\n        if (formatInitMetadata.hasVideoId() && videoId != null && !formatInitMetadata.getVideoId().equals(videoId)) {\r\n            throw new SabrStreamError(String.format(\"Received unexpected Format Initialization Metadata for video\" +\r\n                    \"  %s (expecting %s)\", formatInitMetadata.getVideoId(), videoId));\r\n        }\r\n\r\n        FormatSelector formatSelector = matchFormatSelector(formatInitMetadata);\r\n\r\n        if (formatSelector == null) {\r\n            // Should not happen. If we ignored the format the server may refuse to send us any more data\r\n            throw new SabrStreamError(String.format(\"Received format %s but it does not match any format selector\", formatInitMetadata.getFormatId()));\r\n        }\r\n\r\n        // Guard: Check if the format selector is already in use by another initialized format.\r\n        // This can happen when the server changes the format to use (e.g. changing quality).\r\n        //\r\n        // Changing a format will require adding some logic to handle inactive formats.\r\n        // Given we only provide one FormatId currently, and this should not occur in this case,\r\n        // we will mark this as not currently supported and bail.\r\n        for (SelectedFormat selectedFormat : selectedFormats.values()) {\r\n            if (selectedFormat.formatSelector == formatSelector) {\r\n                throw new SabrStreamError(\"Server changed format. Changing formats is not currently supported\");\r\n            }\r\n        }\r\n\r\n        long durationMs = Utils.ticksToMs(\r\n                formatInitMetadata.hasDurationUnits() ? formatInitMetadata.getDurationUnits() : -1,\r\n                formatInitMetadata.hasDurationTimescale() ? formatInitMetadata.getDurationTimescale() : -1\r\n        );\r\n\r\n        long totalSegments = formatInitMetadata.hasEndSegmentNumber() ? formatInitMetadata.getEndSegmentNumber() : -1;\r\n\r\n        if (totalSegments == -1 && liveMetadata != null && liveMetadata.hasHeadSequenceNumber()) {\r\n            totalSegments = liveMetadata.getHeadSequenceNumber();\r\n        }\r\n\r\n        SelectedFormat initializedFormat = new SelectedFormat(\r\n                formatInitMetadata.hasFormatId() ? formatInitMetadata.getFormatId() : null,\r\n                durationMs,\r\n                formatInitMetadata.hasEndTimeMs() ? formatInitMetadata.getEndTimeMs() : -1,\r\n                formatInitMetadata.hasMimeType() ? formatInitMetadata.getMimeType() : null,\r\n                formatInitMetadata.hasVideoId() ? formatInitMetadata.getVideoId() : null,\r\n                formatSelector,\r\n                totalSegments,\r\n                formatSelector.isDiscardMedia()\r\n        );\r\n\r\n        totalDurationMs = Math.max(\r\n                totalDurationMs != -1 ? totalDurationMs : 0,\r\n                Math.max(formatInitMetadata.hasEndTimeMs() ? formatInitMetadata.getEndTimeMs() : 0, durationMs != -1 ? durationMs : 0)\r\n        );\r\n\r\n        if (initializedFormat.discard) {\r\n            // Mark the entire format as buffered into oblivion if we plan to discard all media.\r\n            // This stops the server sending us any more data for this format.\r\n            // Note: Using JS_MAX_SAFE_INTEGER but could use any maximum value as long as the server accepts it.\r\n            initializedFormat.consumedRanges.clear();\r\n            initializedFormat.consumedRanges.add(new ConsumedRange(\r\n                    0,\r\n                    Integer.MAX_VALUE, // ((long) Math.pow(2, 53)) - 1\r\n                    0,\r\n                    Integer.MAX_VALUE // ((long) Math.pow(2, 53)) - 1\r\n            ));\r\n        }\r\n\r\n        if (formatInitMetadata.hasFormatId()) {\r\n            selectedFormats.put(formatInitMetadata.getFormatId().toString(), initializedFormat);\r\n            Log.d(TAG, \"Initialized Format: %s\", initializedFormat.formatId);\r\n        }\r\n\r\n        if (!initializedFormat.discard) {\r\n            result.sabrPart = new FormatInitializedSabrPart(\r\n                    formatInitMetadata.hasFormatId() ? formatInitMetadata.getFormatId() : null,\r\n                    formatSelector,\r\n                    formatInitMetadata.hasEndTimeMs() ? formatInitMetadata.getEndTimeMs() : -1\r\n            );\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    public void processNextRequestPolicy(NextRequestPolicy nextRequestPolicy) {\r\n        this.nextRequestPolicy = nextRequestPolicy;\r\n        Log.d(TAG, \"Registered new NextRequestPolicy: %s\", nextRequestPolicy);\r\n    }\r\n\r\n    public ProcessLiveMetadataResult processLiveMetadata(LiveMetadata liveMetadata) {\r\n        this.liveMetadata = liveMetadata;\r\n\r\n        if (liveMetadata.hasHeadSequenceTimeMs()) {\r\n            totalDurationMs = liveMetadata.getHeadSequenceTimeMs();\r\n        }\r\n\r\n        // If we have a head sequence number, we need to update the total sequences for each initialized format\r\n        // For livestreams, it is not available in the format initialization metadata\r\n        if (liveMetadata.hasHeadSequenceNumber()) {\r\n            for (SelectedFormat izf : selectedFormats.values()) {\r\n                izf.totalSegments = liveMetadata.getHeadSequenceNumber();\r\n            }\r\n        }\r\n\r\n        ProcessLiveMetadataResult result = new ProcessLiveMetadataResult();\r\n\r\n        // If the current player time is less than the min dvr time, simulate a server seek to the min dvr time.\r\n        // The server SHOULD send us a SABR_SEEK part in this case, but it does not always happen (e.g. ANDROID_VR)\r\n        // The server SHOULD NOT send us segments before the min dvr time, so we should assume that the player time is correct.\r\n        long minSeekableTimeMs = Utils.ticksToMs(liveMetadata.hasMinSeekableTimeTicks() ? liveMetadata.getMinSeekableTimeTicks() : -1,\r\n                liveMetadata.hasMinSeekableTimescale() ? liveMetadata.getMinSeekableTimescale() : -1);\r\n        if (minSeekableTimeMs != -1 && playerTimeMs > 0 && playerTimeMs < minSeekableTimeMs) {\r\n            Log.d(TAG, \"Player time %s is less than min seekable time %s, simulating server seek\",\r\n                    playerTimeMs, minSeekableTimeMs);\r\n            playerTimeMs = minSeekableTimeMs;\r\n            for (SelectedFormat izf : selectedFormats.values()) {\r\n                izf.currentSegment = null; // Clear the current segment as we expect segments to no longer be in order.\r\n                result.seekSabrParts.add(\r\n                        new MediaSeekSabrPart(\r\n                                MediaSeekSabrPart.Reason.SERVER_SEEK,\r\n                                izf.formatId,\r\n                                izf.formatSelector\r\n                        )\r\n                );\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    public ProcessSabrSeekResult processSabrSeek(SabrSeek sabrSeek) {\r\n        long seekTo = Utils.ticksToMs(sabrSeek.hasSeekMediaTime() ? sabrSeek.getSeekMediaTime() : -1, sabrSeek.hasSeekMediaTimescale() ? sabrSeek.getSeekMediaTimescale() : -1);\r\n        if (seekTo == -1) {\r\n            throw new SabrStreamError(String.format(\"Server sent a SabrSeek part that is missing required seek data: %s\", sabrSeek));\r\n        }\r\n        Log.d(TAG, \"Seeking to %sms\", seekTo);\r\n        playerTimeMs = seekTo;\r\n\r\n        ProcessSabrSeekResult result = new ProcessSabrSeekResult();\r\n\r\n        // Clear latest segment of each initialized format\r\n        // as we expect them to no longer be in order.\r\n        for (SelectedFormat initializedFormat : selectedFormats.values()) {\r\n            initializedFormat.currentSegment = null;\r\n            result.seekSabrParts.add(\r\n                    new MediaSeekSabrPart(\r\n                            MediaSeekSabrPart.Reason.SERVER_SEEK,\r\n                            initializedFormat.formatId,\r\n                            initializedFormat.formatSelector\r\n                    )\r\n            );\r\n        }\r\n        return result;\r\n    }\r\n\r\n    public void processSabrContextUpdate(SabrContextUpdate sabrCtxUpdate) {\r\n        if (!sabrCtxUpdate.hasType() || !sabrCtxUpdate.hasValue() || !sabrCtxUpdate.hasWritePolicy()) {\r\n            Log.w(TAG, \"Received an invalid SabrContextUpdate, ignoring\");\r\n            return;\r\n        }\r\n\r\n        if (sabrCtxUpdate.getWritePolicy() == SabrContextUpdate.SabrContextWritePolicy.SABR_CONTEXT_WRITE_POLICY_KEEP_EXISTING\r\n                && sabrContextUpdates.containsKey(sabrCtxUpdate.getType())) {\r\n            Log.d(TAG, \"Received a SABR Context Update with write_policy=KEEP_EXISTING\" +\r\n                    \" matching an existing SABR Context Update. Ignoring update\");\r\n            return;\r\n        }\r\n\r\n        Log.w(TAG, \"Received a SABR Context Update. YouTube is likely trying to force ads on the client. \" +\r\n                \"This may cause issues with playback.\");\r\n\r\n        sabrContextUpdates.put(sabrCtxUpdate.getType(), sabrCtxUpdate);\r\n        if (sabrCtxUpdate.hasSendByDefault()) {\r\n            sabrContextsToSend.add(sabrCtxUpdate.getType());\r\n        }\r\n        Log.d(TAG, \"Registered SabrContextUpdate %s\", sabrCtxUpdate);\r\n    }\r\n\r\n    public void processSabrContextSendingPolicy(SabrContextSendingPolicy sabrCtxSendingPolicy) {\r\n        for (int startType : sabrCtxSendingPolicy.getStartPolicyList()) {\r\n            if (!sabrContextsToSend.contains(startType)) {\r\n                Log.d(TAG, \"Server requested to enable SABR Context Update for type %s\", startType);\r\n                sabrContextsToSend.add(startType);\r\n            }\r\n        }\r\n\r\n        for (int stopType : sabrCtxSendingPolicy.getStopPolicyList()) {\r\n            if (!sabrContextsToSend.contains(stopType)) {\r\n                Log.d(TAG, \"Server requested to disable SABR Context Update for type %s\", stopType);\r\n                sabrContextsToSend.remove(stopType);\r\n            }\r\n        }\r\n\r\n        for (int discardType : sabrCtxSendingPolicy.getDiscardPolicyList()) {\r\n            if (!sabrContextsToSend.contains(discardType)) {\r\n                Log.d(TAG, \"Server requested to discard SABR Context Update for type %s\", discardType);\r\n                sabrContextUpdates.remove(discardType);\r\n            }\r\n        }\r\n    }\r\n\r\n    public boolean isLive() {\r\n        return liveMetadata != null || isLive;\r\n    }\r\n\r\n    public void setLive(boolean isLive) {\r\n        this.isLive = isLive;\r\n    }\r\n\r\n    public int getLiveSegmentTargetDurationToleranceMs() {\r\n        return liveSegmentTargetDurationToleranceMs;\r\n    }\r\n\r\n    public int getLiveSegmentTargetDurationSec() {\r\n        return liveSegmentTargetDurationSec;\r\n    }\r\n\r\n    public long getSegmentStartTimeMs(int iTag) {\r\n        MediaHeader mediaHeader = initializedFormats.get(iTag);\r\n\r\n        if (mediaHeader == null || mediaHeader.getStartMs() == -1) {\r\n            return 0;\r\n        }\r\n\r\n        return mediaHeader.getStartMs() + mediaHeader.getDurationMs();\r\n    }\r\n\r\n    public long getSegmentDurationMs(int iTag) {\r\n        MediaHeader mediaHeader = initializedFormats.get(iTag);\r\n\r\n        if (mediaHeader == null) {\r\n            return 0;\r\n        }\r\n\r\n        return mediaHeader.getDurationMs();\r\n    }\r\n\r\n    //private List<FormatId> createSelectedFormatIds() {\r\n    //    List<FormatId> result = new ArrayList<>();\r\n    //\r\n    //    for (SelectedFormat selectedFormat : selectedFormats.values()) {\r\n    //        result.add(selectedFormat.formatId);\r\n    //    }\r\n    //\r\n    //    return result;\r\n    //}\r\n\r\n    public StreamerContext createStreamerContext() {\r\n        return StreamerContext.newBuilder()\r\n                .setPoToken(\r\n                        ByteString.copyFrom(\r\n                                Base64.decode(poToken, Base64.URL_SAFE)\r\n                        )\r\n                )\r\n                .setPlaybackCookie(\r\n                        nextRequestPolicy != null ?\r\n                                nextRequestPolicy.getPlaybackCookie().toByteString() : ByteString.EMPTY\r\n                )\r\n                .setClientInfo(clientInfo)\r\n                .addAllSabrContexts(createSabrContexts())\r\n                .addAllUnsentSabrContexts(createUnsentSabrContexts())\r\n                .build();\r\n    }\r\n\r\n    private List<SabrContext> createSabrContexts() {\r\n        List<SabrContext> result = new ArrayList<>();\r\n\r\n        for (SabrContextUpdate context : sabrContextUpdates.values()) {\r\n            if (sabrContextsToSend.contains(context.getType())) {\r\n                result.add(\r\n                        SabrContext.newBuilder()\r\n                            .setType(context.getType())\r\n                            .setValue(context.getValue())\r\n                            .build()\r\n                );\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    private List<Integer> createUnsentSabrContexts() {\r\n        List<Integer> result = new ArrayList<>();\r\n\r\n        for (Integer contextType : sabrContextsToSend) {\r\n            if (!sabrContextUpdates.containsKey(contextType)) {\r\n                result.add(contextType);\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    private FormatSelector matchFormatSelector(FormatInitializationMetadata formatInitMetadata) {\r\n        if (formatSelector == null) {\r\n            return null;\r\n        }\r\n\r\n        if (formatSelector.match(formatInitMetadata.getFormatId(), formatInitMetadata.getMimeType())) {\r\n            return formatSelector;\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    public @NonNull FormatSelector getFormatSelector() {\r\n        return formatSelector;\r\n    }\r\n\r\n    public void setFormatSelector(FormatSelector formatSelector) {\r\n        this.formatSelector = formatSelector;\r\n        initializeFormatSelector();\r\n    }\r\n\r\n    public @NonNull Map<Integer, MediaHeader> getInitializedFormats() {\r\n        return initializedFormats;\r\n    }\r\n\r\n    public void reset(int iTag) {\r\n        MediaHeader mediaHeader = initializedFormats.get(iTag);\r\n\r\n        if (mediaHeader != null) {\r\n            MediaHeader newHeader = mediaHeader.toBuilder()\r\n                    .setStartMs(-1)\r\n                    .setSequenceNumber(0)\r\n                    .build();\r\n            initializedFormats.put(iTag, newHeader);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/SabrStream.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser;\r\n\r\nimport androidx.annotation.NonNull;\r\n\r\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\r\nimport com.google.android.exoplayer2.source.sabr.parser.exceptions.MediaSegmentMismatchError;\r\nimport com.google.android.exoplayer2.source.sabr.parser.exceptions.SabrStreamError;\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.FormatSelector;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.FormatInitializedSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSeekSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSegmentDataSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSegmentEndSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSegmentInitSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.PoTokenStatusSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.RefreshPlayerResponseSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.SabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.results.ProcessFormatInitializationMetadataResult;\r\nimport com.google.android.exoplayer2.source.sabr.parser.results.ProcessMediaEndResult;\r\nimport com.google.android.exoplayer2.source.sabr.parser.results.ProcessMediaHeaderResult;\r\nimport com.google.android.exoplayer2.source.sabr.parser.results.ProcessMediaResult;\r\nimport com.google.android.exoplayer2.source.sabr.parser.results.ProcessStreamProtectionStatusResult;\r\nimport com.google.android.exoplayer2.source.sabr.parser.ump.UMPDecoder;\r\nimport com.google.android.exoplayer2.source.sabr.parser.ump.UMPPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.ump.UMPPartId;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.FormatInitializationMetadata;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.LiveMetadata;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.MediaHeader;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.NextRequestPolicy;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.ReloadPlayerResponse;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.SabrContextSendingPolicy;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.SabrContextUpdate;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.SabrError;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.SabrRedirect;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.SabrSeek;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.StreamProtectionStatus;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.StreamerContext;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.StreamerContext.ClientInfo;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\nimport com.liskovsoft.sharedutils.querystringparser.UrlQueryString;\r\nimport com.liskovsoft.sharedutils.querystringparser.UrlQueryStringFactory;\r\n\r\nimport java.io.IOException;\r\nimport java.util.HashSet;\r\nimport java.util.List;\r\nimport java.util.Set;\r\n\r\npublic class SabrStream {\r\n    private static final String TAG = SabrStream.class.getSimpleName();\r\n    private final int[] KNOWN_PARTS = {\r\n            UMPPartId.MEDIA_HEADER,\r\n            UMPPartId.MEDIA,\r\n            UMPPartId.MEDIA_END,\r\n            UMPPartId.STREAM_PROTECTION_STATUS,\r\n            UMPPartId.SABR_REDIRECT,\r\n            UMPPartId.FORMAT_INITIALIZATION_METADATA,\r\n            UMPPartId.NEXT_REQUEST_POLICY,\r\n            //UMPPartId.LIVE_METADATA,\r\n            //UMPPartId.SABR_SEEK,\r\n            UMPPartId.SABR_ERROR,\r\n            UMPPartId.SABR_CONTEXT_UPDATE,\r\n            UMPPartId.SABR_CONTEXT_SENDING_POLICY,\r\n            UMPPartId.RELOAD_PLAYER_RESPONSE,\r\n            //UMPPartId.SNACKBAR_MESSAGE // ???\r\n    };\r\n    private final int[] IGNORED_PARTS = {\r\n            UMPPartId.REQUEST_IDENTIFIER,\r\n            UMPPartId.REQUEST_CANCELLATION_POLICY,\r\n            UMPPartId.PLAYBACK_START_POLICY,\r\n            UMPPartId.ALLOWED_CACHED_FORMATS,\r\n            UMPPartId.PAUSE_BW_SAMPLING_HINT,\r\n            UMPPartId.START_BW_SAMPLING_HINT,\r\n            UMPPartId.REQUEST_PIPELINING,\r\n            UMPPartId.SELECTABLE_FORMATS,\r\n            UMPPartId.PREWARM_CONNECTION,\r\n    };\r\n    private final UMPDecoder decoder;\r\n    private final SabrProcessor processor;\r\n    private final NoSegmentsTracker noNewSegmentsTracker;\r\n    private final Set<Integer> unknownPartTypes;\r\n    private int sqMismatchForwardCount;\r\n    private int sqMismatchBacktrackCount;\r\n    private boolean receivedNewSegments;\r\n    private String url;\r\n    private List<? extends  SabrPart> multiResult = null;\r\n\r\n    private static class NoSegmentsTracker {\r\n        public int consecutiveRequests = 0;\r\n        public float timestampStarted = -1;\r\n        public int liveHeadSegmentStarted = -1;\r\n\r\n        public void reset() {\r\n             consecutiveRequests = 0;\r\n             timestampStarted = -1;\r\n             liveHeadSegmentStarted = -1;\r\n        }\r\n\r\n        public void increment(int liveHeadSegment) {\r\n            if (consecutiveRequests == 0) {\r\n                timestampStarted = System.currentTimeMillis() * 1_000;\r\n                liveHeadSegmentStarted = liveHeadSegment;\r\n            }\r\n            consecutiveRequests += 1;\r\n        }\r\n    }\r\n\r\n    public SabrStream(\r\n            @NonNull String serverAbrStreamingUrl,\r\n            @NonNull String videoPlaybackUstreamerConfig,\r\n            @NonNull ClientInfo clientInfo,\r\n            int liveSegmentTargetDurationSec,\r\n            int liveSegmentTargetDurationToleranceMs,\r\n            long startTimeMs,\r\n            String poToken,\r\n            boolean postLive,\r\n            String videoId,\r\n            long durationMs) {\r\n        decoder = new UMPDecoder();\r\n        processor = new SabrProcessor(\r\n                videoPlaybackUstreamerConfig,\r\n                clientInfo,\r\n                liveSegmentTargetDurationSec,\r\n                liveSegmentTargetDurationToleranceMs,\r\n                startTimeMs,\r\n                poToken,\r\n                postLive,\r\n                videoId,\r\n                durationMs\r\n        );\r\n        url = serverAbrStreamingUrl;\r\n\r\n        // Whether we got any new (not consumed) segments in the request\r\n        noNewSegmentsTracker = new NoSegmentsTracker();\r\n        unknownPartTypes = new HashSet<>();\r\n\r\n        sqMismatchBacktrackCount = 0;\r\n        sqMismatchForwardCount = 0;\r\n    }\r\n\r\n\r\n    public SabrPart parse(@NonNull ExtractorInput extractorInput) {\r\n        SabrPart result = null;\r\n\r\n        while (result == null && (multiResult == null || multiResult.isEmpty())) {\r\n            UMPPart part = nextKnownUMPPart(extractorInput);\r\n\r\n            if (part == null) {\r\n                break;\r\n            }\r\n\r\n            result = parsePart(part);\r\n\r\n            if (result == null) {\r\n                multiResult = parseMultiPart(part);\r\n            }\r\n        }\r\n\r\n        return result != null ? result : multiResult != null && !multiResult.isEmpty() ? multiResult.remove(0) : null;\r\n    }\r\n\r\n    public void reset() {\r\n        noNewSegmentsTracker.reset();\r\n    }\r\n\r\n    public void reset(int iTag) {\r\n        processor.reset(iTag);\r\n    }\r\n\r\n    public FormatSelector getFormatSelector() {\r\n        return processor.getFormatSelector();\r\n    }\r\n\r\n    public void setFormatSelector(FormatSelector formatSelector) {\r\n        processor.setFormatSelector(formatSelector);\r\n    }\r\n\r\n    public long getSegmentStartTimeMs(int iTag) {\r\n        return processor.getSegmentStartTimeMs(iTag);\r\n    }\r\n\r\n    public long getSegmentDurationMs(int iTag) {\r\n        return processor.getSegmentDurationMs(iTag);\r\n    }\r\n\r\n    public MediaHeader getInitializedFormat(int iTag) {\r\n        return processor.getInitializedFormats().get(iTag);\r\n    }\r\n\r\n    public StreamerContext createStreamerContext() {\r\n        return processor.createStreamerContext();\r\n    }\r\n\r\n    private SabrPart parsePart(UMPPart part) {\r\n        switch (part.partId) {\r\n            case UMPPartId.MEDIA_HEADER:\r\n                return processMediaHeader(part);\r\n            case UMPPartId.MEDIA:\r\n                return processMedia(part);\r\n            case UMPPartId.MEDIA_END:\r\n                return processMediaEnd(part);\r\n            case UMPPartId.STREAM_PROTECTION_STATUS:\r\n                return processStreamProtectionStatus(part);\r\n            case UMPPartId.SABR_REDIRECT:\r\n                processSabrRedirect(part);\r\n                return null;\r\n            case UMPPartId.FORMAT_INITIALIZATION_METADATA:\r\n                return processFormatInitializationMetadata(part);\r\n            case UMPPartId.NEXT_REQUEST_POLICY:\r\n                processNextRequestPolicy(part);\r\n                return null;\r\n            case UMPPartId.SABR_ERROR:\r\n                processSabrError(part);\r\n                return null;\r\n            case UMPPartId.SABR_CONTEXT_UPDATE:\r\n                processSabrContextUpdate(part);\r\n                return null;\r\n            case UMPPartId.SABR_CONTEXT_SENDING_POLICY:\r\n                processSabrContextSendingPolicy(part);\r\n                return null;\r\n            case UMPPartId.RELOAD_PLAYER_RESPONSE:\r\n                return processReloadPlayerResponse(part);\r\n        }\r\n\r\n        if (!contains(IGNORED_PARTS, part.partId)) {\r\n            unknownPartTypes.add(part.partId);\r\n        }\r\n\r\n        Log.d(TAG, \"Unhandled part type %s\", part.partId);\r\n\r\n        return null;\r\n    }\r\n\r\n    private List<? extends SabrPart> parseMultiPart(UMPPart part) {\r\n        switch (part.partId) {\r\n            case UMPPartId.LIVE_METADATA:\r\n                return processLiveMetadata(part);\r\n            case UMPPartId.SABR_SEEK:\r\n                return processSabrSeek(part);\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    private MediaSegmentInitSabrPart processMediaHeader(UMPPart part) {\r\n        MediaHeader mediaHeader;\r\n\r\n        try {\r\n            mediaHeader = MediaHeader.parseFrom(part.toStream());\r\n        } catch (IOException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n\r\n        try {\r\n            ProcessMediaHeaderResult result = processor.processMediaHeader(mediaHeader);\r\n\r\n            return result.sabrPart;\r\n        } catch (MediaSegmentMismatchError e) {\r\n            // For livestreams, the server may not know the exact segment for a given player time.\r\n            // For segments near stream head, it estimates using segment duration, which can cause off-by-one segment mismatches.\r\n            // If a segment is much longer or shorter than expected, the server may return a segment ahead or behind.\r\n            // In such cases, retry with an adjusted player time to resync.\r\n            if (processor.isLive() && e.receivedSequenceNumber == e.expectedSequenceNumber - 1) {\r\n                // The segment before the previous segment was possibly longer than expected.\r\n                // Move the player time forward to try to adjust for this.;\r\n                processor.setPlayerTimeMs(processor.getPlayerTimeMs() + processor.getLiveSegmentTargetDurationToleranceMs());\r\n                sqMismatchForwardCount += 1;\r\n                return null;\r\n            } else if (processor.isLive() && e.receivedSequenceNumber == e.expectedSequenceNumber + 2) {\r\n                // The previous segment was possibly shorter than expected\r\n                // Move the player time backwards to try to adjust for this.\r\n                processor.setPlayerTimeMs(Math.max(0, processor.getPlayerTimeMs() - processor.getLiveSegmentTargetDurationToleranceMs()));\r\n                sqMismatchBacktrackCount += 1;\r\n                return null;\r\n            }\r\n\r\n            throw e;\r\n        }\r\n    }\r\n\r\n    private MediaSegmentDataSabrPart processMedia(UMPPart part) {\r\n        try {\r\n            long position = part.data.getPosition();\r\n            long headerId = decoder.readVarInt(part.data);\r\n            long offset = part.data.getPosition() - position;\r\n            int contentLength = part.size - (int) offset;\r\n\r\n            ProcessMediaResult result = processor.processMedia(headerId, contentLength, part.data);\r\n\r\n            return result.sabrPart;\r\n        } catch (IOException | InterruptedException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n    }\r\n\r\n    private MediaSegmentEndSabrPart processMediaEnd(UMPPart part) {\r\n        try {\r\n            long headerId = decoder.readVarInt(part.data);\r\n            Log.d(TAG, \"Header ID: %s\", headerId);\r\n\r\n            ProcessMediaEndResult result = processor.processMediaEnd(headerId);\r\n\r\n            if (result.isNewSegment) {\r\n                receivedNewSegments = true;\r\n            }\r\n\r\n            return result.sabrPart;\r\n        } catch (IOException | InterruptedException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n    }\r\n\r\n    private PoTokenStatusSabrPart processStreamProtectionStatus(UMPPart part) {\r\n        StreamProtectionStatus sps;\r\n\r\n        try {\r\n            sps = StreamProtectionStatus.parseFrom(part.toStream());\r\n        } catch (IOException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n\r\n        Log.d(TAG, \"Process StreamProtectionStatus: %s\", sps);\r\n        ProcessStreamProtectionStatusResult result = processor.processStreamProtectionStatus(sps);\r\n\r\n        return result.sabrPart;\r\n    }\r\n\r\n    private void processSabrRedirect(UMPPart part) {\r\n        SabrRedirect sabrRedirect;\r\n\r\n        try {\r\n            sabrRedirect = SabrRedirect.parseFrom(part.toStream());\r\n        } catch (IOException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n\r\n        Log.d(TAG, \"Process SabrRedirect: %s\", sabrRedirect);\r\n\r\n        if (!sabrRedirect.hasRedirectUrl()) {\r\n            Log.d(TAG, \"Server requested to redirect to an invalid URL\");\r\n            return;\r\n        }\r\n\r\n        setUrl(sabrRedirect.getRedirectUrl());\r\n    }\r\n\r\n    private FormatInitializedSabrPart processFormatInitializationMetadata(UMPPart part) {\r\n        FormatInitializationMetadata fmtInitMetadata;\r\n\r\n        try {\r\n            fmtInitMetadata = FormatInitializationMetadata.parseFrom(part.toStream());\r\n        } catch (IOException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n\r\n        Log.d(TAG, \"Process FormatInitializationMetadata: %s\", fmtInitMetadata);\r\n        ProcessFormatInitializationMetadataResult result = processor.processFormatInitializationMetadata(fmtInitMetadata);\r\n\r\n        return result.sabrPart;\r\n    }\r\n\r\n    private void processNextRequestPolicy(UMPPart part) {\r\n        NextRequestPolicy nextRequestPolicy;\r\n\r\n        try {\r\n            nextRequestPolicy = NextRequestPolicy.parseFrom(part.toStream());\r\n        } catch (IOException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n\r\n        Log.d(TAG, \"Process NextRequestPolicy: %s\", nextRequestPolicy);\r\n        processor.processNextRequestPolicy(nextRequestPolicy);\r\n    }\r\n\r\n    private void processSabrError(UMPPart part) {\r\n        SabrError sabrError;\r\n\r\n        try {\r\n            sabrError = SabrError.parseFrom(part.toStream());\r\n        } catch (IOException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n\r\n        Log.d(TAG, \"Process SabrError: %s\", sabrError);\r\n        throw new SabrStreamError(String.format(\"SABR Protocol Error: %s\", sabrError));\r\n    }\r\n\r\n    private void processSabrContextUpdate(UMPPart part) {\r\n        SabrContextUpdate sabrCtxUpdate;\r\n\r\n        try {\r\n            sabrCtxUpdate = SabrContextUpdate.parseFrom(part.toStream());\r\n        } catch (IOException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n\r\n        Log.d(TAG, \"Process SabrContextUpdate: %s\", sabrCtxUpdate);\r\n        processor.processSabrContextUpdate(sabrCtxUpdate);\r\n    }\r\n\r\n    private void processSabrContextSendingPolicy(UMPPart part) {\r\n        SabrContextSendingPolicy sabrCtxSendingPolicy;\r\n\r\n        try {\r\n            sabrCtxSendingPolicy = SabrContextSendingPolicy.parseFrom(part.toStream());\r\n        } catch (IOException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n\r\n        Log.d(TAG, \"Process SabrContextSendingPolicy: %s\", sabrCtxSendingPolicy);\r\n        processor.processSabrContextSendingPolicy(sabrCtxSendingPolicy);\r\n    }\r\n\r\n    private RefreshPlayerResponseSabrPart processReloadPlayerResponse(UMPPart part) {\r\n        ReloadPlayerResponse reloadPlayerResponse;\r\n\r\n        try {\r\n            reloadPlayerResponse = ReloadPlayerResponse.parseFrom(part.toStream());\r\n        } catch (IOException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n\r\n        Log.d(TAG, \"Process ReloadPlayerResponse: %s\", reloadPlayerResponse);\r\n        return new RefreshPlayerResponseSabrPart(\r\n                RefreshPlayerResponseSabrPart.Reason.SABR_RELOAD_PLAYER_RESPONSE,\r\n                reloadPlayerResponse.hasReloadPlaybackParams() && reloadPlayerResponse.getReloadPlaybackParams().hasToken()\r\n                        ? reloadPlayerResponse.getReloadPlaybackParams().getToken() : null\r\n        );\r\n    }\r\n\r\n    private List<MediaSeekSabrPart> processLiveMetadata(UMPPart part) {\r\n        LiveMetadata liveMetadata;\r\n\r\n        try {\r\n            liveMetadata = LiveMetadata.parseFrom(part.toStream());\r\n        } catch (IOException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n\r\n        Log.d(TAG, \"Process LiveMetadata: %s\", liveMetadata);\r\n        return processor.processLiveMetadata(liveMetadata).seekSabrParts;\r\n    }\r\n\r\n    private List<MediaSeekSabrPart> processSabrSeek(UMPPart part) {\r\n        SabrSeek sabrSeek;\r\n\r\n        try {\r\n            sabrSeek = SabrSeek.parseFrom(part.toStream());\r\n        } catch (IOException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n\r\n        Log.d(TAG, \"Process SabrSeek: %s\", sabrSeek);\r\n        return processor.processSabrSeek(sabrSeek).seekSabrParts;\r\n    }\r\n\r\n    private static boolean contains(int[] array, int value) {\r\n        for (int num : array) {\r\n            if (num == value) {\r\n                return true;\r\n            }\r\n        }\r\n        return false;\r\n    }\r\n\r\n    private UMPPart nextKnownUMPPart(@NonNull ExtractorInput extractorInput) {\r\n        UMPPart part;\r\n\r\n        while (true) {\r\n            part = decoder.decode(extractorInput);\r\n\r\n            if (part == null) {\r\n                Log.d(TAG, \"The UMP stream is ended.\");\r\n                break;\r\n            }\r\n\r\n            // Normal reading: 47, 58. 52, 53, 42, 35, 20, 21, 22, 20...\r\n            if (contains(KNOWN_PARTS, part.partId)) {\r\n                Log.d(TAG, \"Found known part: id=%s, size=%s, position=%s\", part.partId, part.size, part.data.getPosition());\r\n                break;\r\n            } else {\r\n                String msg = String.format(\"Unknown part encountered: id=%s, size=%s, position=%s\", part.partId, part.size, part.data.getPosition());\r\n                if (part.partId > 100) {\r\n                    throw new IllegalStateException(msg);\r\n                }\r\n\r\n                Log.e(TAG, msg);\r\n                part.skip(); // an essential part to continue reading\r\n            }\r\n\r\n            // Debug\r\n            //Log.e(TAG, \"Unknown part encountered. id: %s, size: %s, position: %s\", part.partId, part.size, part.data.getPosition());\r\n            //part.skip(); // an essential part to continue reading\r\n        }\r\n\r\n        return part;\r\n    }\r\n\r\n    public String getUrl() {\r\n        return this.url;\r\n    }\r\n\r\n    private void setUrl(String url) {\r\n        Log.d(TAG, \"New URL: %s\", url);\r\n        UrlQueryString newQueryString = UrlQueryStringFactory.parse(url);\r\n        UrlQueryString oldQueryString = UrlQueryStringFactory.parse(this.url);\r\n        String bn = newQueryString.get(\"id\");\r\n        String bc = oldQueryString.get(\"id\");\r\n        if (processor.isLive() && this.url != null && !Helpers.equals(bn, bc)) {\r\n            throw new SabrStreamError(String.format(\"Broadcast ID changed from %s to %s. The download will need to be restarted.\", bc, bn));\r\n        }\r\n        this.url = url;\r\n        if (Helpers.equals(newQueryString.get(\"source\"), \"yt_live_broadcast\")) {\r\n            processor.setLive(true);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/adapter/SabrFragmentedMp4Adapter.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.adapter;\r\n\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.drm.DrmInitData;\r\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\r\nimport com.google.android.exoplayer2.extractor.PositionHolder;\r\nimport com.google.android.exoplayer2.extractor.TrackOutput;\r\nimport com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;\r\nimport com.google.android.exoplayer2.extractor.mp4.Track;\r\nimport com.google.android.exoplayer2.source.sabr.parser.SabrStream;\r\nimport com.google.android.exoplayer2.source.sabr.parser.misc.SabrExtractorInput;\r\nimport com.google.android.exoplayer2.util.TimestampAdjuster;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\n\r\nimport java.io.IOException;\r\nimport java.util.List;\r\n\r\npublic class SabrFragmentedMp4Adapter extends FragmentedMp4Extractor {\r\n    private static final String TAG = SabrFragmentedMp4Adapter.class.getSimpleName();\r\n    private final SabrExtractorInput extractorInput;\r\n\r\n    public SabrFragmentedMp4Adapter(SabrStream sabrStream) {\r\n        this.extractorInput = new SabrExtractorInput(sabrStream);\r\n    }\r\n\r\n    public SabrFragmentedMp4Adapter(int flags, SabrStream sabrStream) {\r\n        super(flags);\r\n        this.extractorInput = new SabrExtractorInput(sabrStream);\r\n    }\r\n\r\n    public SabrFragmentedMp4Adapter(\r\n            int flags,\r\n            @Nullable TimestampAdjuster timestampAdjuster,\r\n            SabrStream sabrStream) {\r\n        super(flags, timestampAdjuster);\r\n        this.extractorInput = new SabrExtractorInput(sabrStream);\r\n    }\r\n\r\n    public SabrFragmentedMp4Adapter(\r\n            int flags,\r\n            @Nullable TimestampAdjuster timestampAdjuster,\r\n            @Nullable Track sideloadedTrack,\r\n            @Nullable DrmInitData sideloadedDrmInitData,\r\n            SabrStream sabrStream) {\r\n        super(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData);\r\n        this.extractorInput = new SabrExtractorInput(sabrStream);\r\n    }\r\n\r\n    public SabrFragmentedMp4Adapter(\r\n            int flags,\r\n            @Nullable TimestampAdjuster timestampAdjuster,\r\n            @Nullable Track sideloadedTrack,\r\n            @Nullable DrmInitData sideloadedDrmInitData,\r\n            List<Format> closedCaptionFormats,\r\n            SabrStream sabrStream) {\r\n        super(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, closedCaptionFormats);\r\n        this.extractorInput = new SabrExtractorInput(sabrStream);\r\n    }\r\n\r\n    public SabrFragmentedMp4Adapter(\r\n            int flags,\r\n            @Nullable TimestampAdjuster timestampAdjuster,\r\n            @Nullable Track sideloadedTrack,\r\n            @Nullable DrmInitData sideloadedDrmInitData,\r\n            List<Format> closedCaptionFormats,\r\n            @Nullable TrackOutput additionalEmsgTrackOutput,\r\n            SabrStream sabrStream) {\r\n        super(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, closedCaptionFormats, additionalEmsgTrackOutput);\r\n        this.extractorInput = new SabrExtractorInput(sabrStream);\r\n    }\r\n\r\n    @Override\r\n    public int read(ExtractorInput input, PositionHolder seekPosition)\r\n            throws IOException, InterruptedException {\r\n        int result = RESULT_END_OF_INPUT;\r\n\r\n        try {\r\n            extractorInput.init(input);\r\n            result = super.read(extractorInput, seekPosition);\r\n        } finally {\r\n            if (result != RESULT_CONTINUE) {\r\n                Log.e(TAG, \"Mp4Adapter: disposing, result=%s\", result);\r\n                extractorInput.dispose();\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/adapter/SabrMatroskaAdapter.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.adapter;\r\n\r\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\r\nimport com.google.android.exoplayer2.extractor.PositionHolder;\r\nimport com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;\r\nimport com.google.android.exoplayer2.source.sabr.parser.SabrStream;\r\nimport com.google.android.exoplayer2.source.sabr.parser.misc.SabrExtractorInput;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\n\r\nimport java.io.IOException;\r\n\r\npublic class SabrMatroskaAdapter extends MatroskaExtractor {\r\n    private static final String TAG = SabrMatroskaAdapter.class.getSimpleName();\r\n    private final SabrExtractorInput extractorInput;\r\n\r\n    public SabrMatroskaAdapter(SabrStream sabrStream) {\r\n        this.extractorInput = new SabrExtractorInput(sabrStream);\r\n    }\r\n\r\n    public SabrMatroskaAdapter(int flags, SabrStream sabrStream) {\r\n        super(flags);\r\n        this.extractorInput = new SabrExtractorInput(sabrStream);\r\n    }\r\n\r\n    @Override\r\n    public int read(ExtractorInput input, PositionHolder seekPosition)\r\n            throws IOException, InterruptedException {\r\n        int result = RESULT_END_OF_INPUT;\r\n\r\n        try {\r\n            extractorInput.init(input);\r\n            result = super.read(extractorInput, seekPosition);\r\n        } finally {\r\n            if (result != RESULT_CONTINUE) {\r\n                Log.e(TAG, \"MatroskaAdapter: disposing, result=%s\", result);\r\n                extractorInput.dispose();\r\n            }\r\n        }\r\n\r\n        return result;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/exceptions/MediaSegmentMismatchError.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.exceptions;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\n\r\npublic class MediaSegmentMismatchError extends SabrStreamError {\r\n    public final long expectedSequenceNumber;\r\n    public final long receivedSequenceNumber;\r\n\r\n    public MediaSegmentMismatchError(FormatId formatId, long expectedSequenceNumber, long receivedSequenceNumber) {\r\n        super(String.format(\r\n                \"Segment sequence number mismatch for format %s: expected %s, received %s\",\r\n                formatId,\r\n                expectedSequenceNumber,\r\n                receivedSequenceNumber\r\n        ));\r\n        this.receivedSequenceNumber = receivedSequenceNumber;\r\n        this.expectedSequenceNumber = expectedSequenceNumber;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/exceptions/PoTokenError.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.exceptions;\r\n\r\npublic class PoTokenError extends SabrStreamError {\r\n    public PoTokenError(String msg) {\r\n        super(msg);\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/exceptions/SabrStreamConsumedError.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.exceptions;\r\n\r\npublic class SabrStreamConsumedError extends Exception {\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/exceptions/SabrStreamError.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.exceptions;\r\n\r\npublic class SabrStreamError extends RuntimeException {\r\n    public SabrStreamError(String msg) {\r\n        super(msg);\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/frames/AACFrameExtractor.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.frames;\r\n\r\nimport com.google.android.exoplayer2.extractor.TrackOutput;\r\n\r\npublic class AACFrameExtractor extends BaseFrameExtractor {\r\n    public AACFrameExtractor(TrackOutput trackOutput, long startTimeUs, long frameDurationUs) {\r\n        super(trackOutput, startTimeUs, frameDurationUs);\r\n    }\r\n\r\n    @Override\r\n    protected int findNextFrameStart(int from) {\r\n        // Look for ADTS syncword: 0xFFF (first 12 bits)\r\n        for (int i = from; i < buffer.limit() - 1; i++) {\r\n            int header = ((buffer.data[i] & 0xFF) << 4) | ((buffer.data[i+1] & 0xF0) >> 4);\r\n            if (header == 0xFFF) return i;\r\n        }\r\n        return -1;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/frames/AVCFrameExtractor.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.frames;\r\n\r\nimport com.google.android.exoplayer2.extractor.TrackOutput;\r\n\r\npublic class AVCFrameExtractor extends BaseFrameExtractor {\r\n    public AVCFrameExtractor(TrackOutput trackOutput, long startTimeUs, long frameDurationUs) {\r\n        super(trackOutput, startTimeUs, frameDurationUs);\r\n    }\r\n\r\n    @Override\r\n    protected int findNextFrameStart(int from) {\r\n        // Look for NAL unit start code: 0x000001 or 0x00000001\r\n        for (int i = from; i < buffer.limit() - 3; i++) {\r\n            if (buffer.data[i] == 0x00 && buffer.data[i+1] == 0x00) {\r\n                if (buffer.data[i+2] == 0x01) return i;\r\n                if (i + 3 < buffer.limit() && buffer.data[i+2] == 0x00 && buffer.data[i+3] == 0x01) return i;\r\n            }\r\n        }\r\n        return -1;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/frames/BaseFrameExtractor.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.frames;\r\n\r\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\r\nimport com.google.android.exoplayer2.extractor.TrackOutput;\r\nimport com.google.android.exoplayer2.util.ParsableByteArray;\r\nimport com.google.android.exoplayer2.C;\r\nimport java.io.IOException;\r\n\r\n/**\r\n * Base class for frame extractors of various media formats.\r\n * Subclasses implement findNextFrameStart() for their format.\r\n */\r\npublic abstract class BaseFrameExtractor {\r\n    protected final ParsableByteArray buffer = new ParsableByteArray(32 * 1024);\r\n    private final TrackOutput trackOutput;\r\n    private long timeUs;\r\n    private final long frameDurationUs;\r\n    private boolean firstFrameInSegment = true;\r\n\r\n    public BaseFrameExtractor(TrackOutput trackOutput, long startTimeUs, long frameDurationUs) {\r\n        this.trackOutput = trackOutput;\r\n        this.timeUs = startTimeUs;\r\n        this.frameDurationUs = frameDurationUs;\r\n    }\r\n\r\n    /**\r\n     * Read data from ExtractorInput and emit complete frames zero-copy.\r\n     */\r\n    public void readFromInput(ExtractorInput input, int maxLength) throws IOException, InterruptedException {\r\n        int bytesRead = input.read(buffer.data, buffer.limit(), maxLength);\r\n        if (bytesRead <= 0) return;\r\n\r\n        buffer.setLimit(buffer.limit() + bytesRead);\r\n\r\n        int frameStart;\r\n        while ((frameStart = findNextFrameStart(0)) >= 0) {\r\n            int frameEnd = findNextFrameStart(frameStart + 1);\r\n\r\n            // If no next frame start is found, send the remaining buffer as the last frame\r\n            if (frameEnd < 0) frameEnd = buffer.limit();\r\n\r\n            int frameSize = frameEnd - frameStart;\r\n            if (frameSize <= 0) break;\r\n\r\n            int flags = firstFrameInSegment ? C.BUFFER_FLAG_KEY_FRAME : 0;\r\n            firstFrameInSegment = false;\r\n\r\n            // Zero-copy: use buffer directly\r\n            buffer.setPosition(frameStart);\r\n            buffer.setLimit(frameEnd);\r\n            trackOutput.sampleData(buffer, frameSize);\r\n            trackOutput.sampleMetadata(timeUs, flags, frameSize, 0, null);\r\n\r\n            timeUs += frameDurationUs;\r\n\r\n            // Shift remaining data to the beginning of the buffer\r\n            int remaining = buffer.limit() - frameEnd;\r\n            if (remaining > 0) {\r\n                System.arraycopy(buffer.data, frameEnd, buffer.data, 0, remaining);\r\n            }\r\n            buffer.setPosition(0);\r\n            buffer.setLimit(remaining);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Reset state for a new segment. The first frame will be flagged as keyframe.\r\n     */\r\n    public void resetForNewSegment() {\r\n        firstFrameInSegment = true;\r\n    }\r\n\r\n    /**\r\n     * Subclasses implement this to find the next frame start in the buffer.\r\n     *\r\n     * @param from index to start searching\r\n     * @return index of the next frame start or -1 if not found\r\n     */\r\n    protected abstract int findNextFrameStart(int from);\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/frames/FrameExtractor.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.frames;\r\n\r\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\r\nimport java.io.IOException;\r\n\r\npublic interface FrameExtractor {\r\n    /**\r\n     * Read data from ExtractorInput into the buffer.\r\n     * Emit complete frames to TrackOutput immediately if possible.\r\n     *\r\n     * @param input     ExtractorInput to read from\r\n     * @param maxLength maximum number of bytes to read\r\n     */\r\n    void readFromInput(ExtractorInput input, int maxLength) throws IOException;\r\n\r\n    /**\r\n     * Reset state for a new segment (e.g., first frame is keyframe)\r\n     */\r\n    void resetForNewSegment();\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/frames/OpusFrameExtractor.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.frames;\r\n\r\nimport com.google.android.exoplayer2.extractor.TrackOutput;\r\n\r\npublic class OpusFrameExtractor extends BaseFrameExtractor {\r\n    public OpusFrameExtractor(TrackOutput trackOutput, long startTimeUs, long frameDurationUs) {\r\n        super(trackOutput, startTimeUs, frameDurationUs);\r\n    }\r\n\r\n    @Override\r\n    protected int findNextFrameStart(int from) {\r\n        // Opus packets start with TOC byte (0x00..0xFF)\r\n        // Simplified: treat each byte as possible start (or implement exact TOC parsing)\r\n        if (from < buffer.limit()) return from;\r\n        return -1;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/frames/VP9FrameExtractor.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.frames;\r\n\r\nimport com.google.android.exoplayer2.extractor.TrackOutput;\r\n\r\npublic class VP9FrameExtractor extends BaseFrameExtractor {\r\n    public VP9FrameExtractor(TrackOutput trackOutput, long startTimeUs, long frameDurationUs) {\r\n        super(trackOutput, startTimeUs, frameDurationUs);\r\n    }\r\n\r\n    @Override\r\n    protected int findNextFrameStart(int from) {\r\n        for (int i = from; i < buffer.limit(); i++) {\r\n            int b = buffer.data[i] & 0xFF;\r\n            if ((b & 0xC0) == 0x80) return i; // VP9 frame_marker\r\n        }\r\n        return -1;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/frames/VorbisFrameExtractor.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.frames;\r\n\r\nimport com.google.android.exoplayer2.extractor.TrackOutput;\r\n\r\npublic class VorbisFrameExtractor extends BaseFrameExtractor {\r\n    public VorbisFrameExtractor(TrackOutput trackOutput, long startTimeUs, long frameDurationUs) {\r\n        super(trackOutput, startTimeUs, frameDurationUs);\r\n    }\r\n\r\n    @Override\r\n    protected int findNextFrameStart(int from) {\r\n        // Vorbis is stored in Ogg pages, each page starts with \"OggS\"\r\n        for (int i = from; i < buffer.limit() - 3; i++) {\r\n            if (buffer.data[i] == 'O' && buffer.data[i+1] == 'g' &&\r\n                    buffer.data[i+2] == 'g' && buffer.data[i+3] == 'S') {\r\n                return i;\r\n            }\r\n        }\r\n        return -1;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/misc/EnabledTrackTypes.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.misc;\r\n\r\npublic class EnabledTrackTypes {\r\n    public static final int VIDEO_AND_AUDIO = 0;\r\n    public static final int AUDIO_ONLY = 1;\r\n    public static final int VIDEO_ONLY = 2;\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/misc/SabrExtractorInput.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.misc;\r\n\r\nimport com.google.android.exoplayer2.C;\r\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\r\nimport com.google.android.exoplayer2.source.sabr.parser.SabrStream;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSegmentDataSabrPart;\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.SabrPart;\r\nimport com.liskovsoft.sharedutils.mylogger.Log;\r\n\r\nimport java.io.EOFException;\r\nimport java.io.IOException;\r\n\r\npublic final class SabrExtractorInput implements ExtractorInput {\r\n    private static final String TAG = SabrExtractorInput.class.getSimpleName();\r\n    private static final boolean ALLOW_END_OF_INPUT = true;\r\n    private final SabrStream sabrStream;\r\n    private ExtractorInput input;\r\n    private long position;\r\n    private long startPosition;\r\n    private int remaining;\r\n    private MediaSegmentDataSabrPart data;\r\n\r\n    public SabrExtractorInput(SabrStream sabrStream) {\r\n        this.sabrStream = sabrStream;\r\n    }\r\n\r\n    /** Should be called before passing the extractor to a handler */\r\n    public void init(ExtractorInput input) {\r\n        if (this.input == input) {\r\n            return;\r\n        }\r\n\r\n        if (this.input != null) {\r\n            throw new IllegalStateException(\"The input should be disposed before initializing\");\r\n        }\r\n\r\n        this.input = input;\r\n        position = input.getPosition();\r\n        startPosition = position;\r\n        remaining = C.LENGTH_UNSET;\r\n    }\r\n\r\n    public void dispose() {\r\n        if (data != null) {\r\n            if (getAdvance() != data.contentLength) {\r\n                throw new IllegalStateException(\"The SABR read isn't finished yet\");\r\n            }\r\n        }\r\n\r\n        startPosition = 0;\r\n        input = null;\r\n        data = null;\r\n        position = C.POSITION_UNSET;\r\n        remaining = C.LENGTH_UNSET;\r\n    }\r\n\r\n    @Override\r\n    public int read(byte[] buffer, int offset, int length) throws IOException, InterruptedException {\r\n        return forward(length, newLength -> data.data.read(buffer, offset, newLength));\r\n    }\r\n\r\n    @Override\r\n    public void readFully(byte[] buffer, int offset, int length) throws IOException, InterruptedException {\r\n        readFully(buffer, offset, length, false);\r\n    }\r\n\r\n    @Override\r\n    public boolean readFully(\r\n            byte[] buffer,\r\n            int offset,\r\n            int length,\r\n            boolean allowEndOfInput) throws IOException, InterruptedException {\r\n        return forwardFully(length,\r\n                (total, newLength) -> data.data.readFully(buffer, offset + total, newLength, ALLOW_END_OF_INPUT)\r\n        );\r\n    }\r\n\r\n    @Override\r\n    public int skip(int length) throws IOException, InterruptedException {\r\n        return forward(length, newLength -> data.data.skip(newLength));\r\n    }\r\n\r\n    @Override\r\n    public void skipFully(int length) throws IOException, InterruptedException {\r\n        skipFully(length, false);\r\n    }\r\n\r\n    @Override\r\n    public boolean skipFully(\r\n            int length,\r\n            boolean allowEndOfInput) throws IOException, InterruptedException {\r\n        return forwardFully(length,\r\n                (total, newLength) -> data.data.skipFully(newLength, ALLOW_END_OF_INPUT));\r\n    }\r\n\r\n    @Override\r\n    public long getPosition() {\r\n        return getPositionInt();\r\n    }\r\n\r\n    @Override\r\n    public long getLength() {\r\n        return remaining;\r\n    }\r\n\r\n    @Override\r\n    public <E extends Throwable> void setRetryPosition(long p, E e) throws E {\r\n        throwShouldNotBeCalled();\r\n    }\r\n\r\n    @Override\r\n    public boolean peekFully(\r\n            byte[] target,\r\n            int offset,\r\n            int length,\r\n            boolean allowEndOfInput) {\r\n        throwShouldNotBeCalled();\r\n        return false;\r\n    }\r\n\r\n    @Override\r\n    public void peekFully(\r\n            byte[] target,\r\n            int offset,\r\n            int length) {\r\n        throwShouldNotBeCalled();\r\n    }\r\n\r\n    @Override\r\n    public boolean advancePeekPosition(\r\n            int length,\r\n            boolean allowEndOfInput) {\r\n        throwShouldNotBeCalled();\r\n        return false;\r\n    }\r\n\r\n    @Override\r\n    public void advancePeekPosition(int length) {\r\n        throwShouldNotBeCalled();\r\n    }\r\n\r\n    @Override\r\n    public void resetPeekPosition() {\r\n        throwShouldNotBeCalled();\r\n    }\r\n\r\n    @Override\r\n    public long getPeekPosition() {\r\n        throwShouldNotBeCalled();\r\n        return -1;\r\n    }\r\n\r\n    private void fetchData() {\r\n        while (true) {\r\n            if (data != null) {\r\n                long advance = getAdvance();\r\n                int length = data.contentLength;\r\n                if (advance < length) {\r\n                    break;\r\n                } else if (advance == length) {\r\n                    data = null;\r\n                } else {\r\n                    throwChunkBoundaryExceeded();\r\n                }\r\n            }\r\n\r\n            SabrPart sabrPart = sabrStream.parse(input);\r\n\r\n            if (sabrPart == null) {\r\n                break;\r\n            }\r\n\r\n            // Debug\r\n            //if (sabrPart instanceof MediaSegmentDataSabrPart) {\r\n            //    MediaSegmentDataSabrPart data = (MediaSegmentDataSabrPart) sabrPart;\r\n            //    Log.e(TAG, \"Consumed contentLength: \" + data.contentLength);\r\n            //    data.data.skipFully(data.contentLength);\r\n            //    continue;\r\n            //}\r\n\r\n            if (sabrPart instanceof MediaSegmentDataSabrPart) {\r\n                data = (MediaSegmentDataSabrPart) sabrPart;\r\n                startPosition = position;\r\n                break;\r\n            }\r\n        }\r\n    }\r\n\r\n    private interface ForwardCallback {\r\n        int forward(int newLength) throws IOException, InterruptedException;\r\n    }\r\n\r\n    private int forward(int length, ForwardCallback callback) throws IOException, InterruptedException {\r\n        if (remaining == 0) {\r\n            return C.RESULT_END_OF_INPUT;\r\n        }\r\n\r\n        if (remaining != C.LENGTH_UNSET) {\r\n            length = Math.min(length, remaining);\r\n        }\r\n\r\n        int result = forwardReal(length, callback);\r\n\r\n        if (remaining != C.LENGTH_UNSET && result > 0) {\r\n            remaining -= result;\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    private int forwardReal(int length, ForwardCallback callback) throws IOException, InterruptedException {\r\n        int result = C.RESULT_END_OF_INPUT;\r\n\r\n        fetchData();\r\n\r\n        if (data == null) {\r\n            return result;\r\n        }\r\n\r\n        int newLength = Math.min(getRemaining(), length);\r\n        result = callback.forward(newLength);\r\n\r\n        if (result > 0) {\r\n            position += result;\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    private interface ForwardFullyCallback {\r\n        boolean forwardFully(int total, int newLength) throws IOException, InterruptedException;\r\n    }\r\n\r\n    private boolean forwardFully(int length, ForwardFullyCallback callback) throws IOException, InterruptedException {\r\n        boolean exceeded = remaining != C.LENGTH_UNSET && length > remaining;\r\n\r\n        if (exceeded) {\r\n            throwChunkBoundaryExceeded();\r\n        }\r\n\r\n        if (remaining != C.LENGTH_UNSET) {\r\n            length = Math.min(length, remaining);\r\n        }\r\n\r\n        boolean result = forwardFullyReal(length, callback);\r\n\r\n        if (remaining != C.LENGTH_UNSET) {\r\n            remaining -= length;\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    private boolean forwardFullyReal(int length, ForwardFullyCallback callback) throws IOException, InterruptedException {\r\n        boolean result = false;\r\n        int total = 0;\r\n\r\n        while (true) {\r\n            fetchData();\r\n\r\n            if (data == null) {\r\n                if (total > 0) {\r\n                    throwEOFException();\r\n                }\r\n\r\n                break;\r\n            }\r\n\r\n            int newLength = Math.min(getRemaining(), length - total);\r\n            result = callback.forwardFully(total, newLength);\r\n\r\n            if (!result) {\r\n                throwEOFException();\r\n            }\r\n\r\n            position += newLength;\r\n\r\n            if (newLength == length - total) {\r\n                break;\r\n            }\r\n\r\n            total += newLength;\r\n\r\n            Log.e(TAG, \"Continue forwardFully: total=%s, length=%s\", total, length - total);\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    private long getPositionInt() {\r\n        return position;\r\n    }\r\n\r\n    private int getRemaining() {\r\n        return data.contentLength - (int) getAdvance();\r\n    }\r\n\r\n    private long getAdvance() {\r\n        return position - startPosition;\r\n    }\r\n\r\n    private static void throwEOFException() throws EOFException {\r\n        String msg = \"EOF should never happened when reading SABR part\";\r\n        Log.e(TAG, msg);\r\n        throw new EOFException(msg);\r\n    }\r\n\r\n    private static void throwShouldNotBeCalled() {\r\n        String msg = \"The peek methods shouldn't be called in SABR extractor\";\r\n        Log.e(TAG, msg);\r\n        throw new UnsupportedOperationException(msg);\r\n    }\r\n\r\n    private static void throwChunkBoundaryExceeded() {\r\n        String msg = \"SABR chunk boundary exceeded\";\r\n        Log.e(TAG, msg);\r\n        throw new IllegalStateException(msg);\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/misc/Utils.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.misc;\r\n\r\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\n\r\nimport java.io.ByteArrayInputStream;\r\nimport java.io.IOException;\r\n\r\npublic class Utils {\r\n    public static long ticksToMs(long timeTicks, long timescale) {\r\n        if (timeTicks == -1 || timescale == -1) {\r\n            return -1;\r\n        }\r\n\r\n        return (long) Math.ceil(((double) timeTicks / timescale) * 1_000);\r\n    }\r\n\r\n    public static byte[] readAllBytes(ByteArrayInputStream is) {\r\n        int streamLength = is.available();\r\n        byte[] result = new byte[streamLength];\r\n\r\n        is.read(result, 0, streamLength);\r\n\r\n        return result;\r\n    }\r\n\r\n    public static byte[] readExactBytes(ExtractorInput input, int length) throws IOException, InterruptedException {\r\n        byte[] result = new byte[length];\r\n        input.readFully(result, 0, length);\r\n        return result;\r\n    }\r\n\r\n    public static long toLong(int value) {\r\n        return Integer.toUnsignedLong(value);\r\n    }\r\n\r\n    public static String updateQuery(String baseUrl, String key, Object value) {\r\n        if (baseUrl == null || key == null || value == null) return baseUrl;\r\n        String separator = baseUrl.contains(\"?\") ? \"&\" : \"?\";\r\n        return baseUrl + separator + key + \"=\" + value;\r\n    }\r\n\r\n    public static int parseHeight(String displayName) {\r\n        if (displayName == null) {\r\n            return 0;\r\n        }\r\n\r\n        return Helpers.isNumeric(displayName) ? Helpers.parseInt(displayName) : Helpers.parseInt(displayName.replace(\"p\", \"\"));\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/models/AudioSelector.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.models;\r\n\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\n\r\npublic class AudioSelector extends FormatSelector {\r\n    public AudioSelector(String displayName, boolean discardMedia) {\r\n        super(displayName, discardMedia);\r\n    }\r\n\r\n    public AudioSelector(String displayName, boolean discardMedia, FormatId... formatIds) {\r\n        super(displayName, discardMedia, formatIds);\r\n    }\r\n\r\n    public AudioSelector(String displayName, boolean discardMedia, Format... selectedFormats) {\r\n        super(displayName, discardMedia, selectedFormats);\r\n    }\r\n\r\n    @Override\r\n    public String getMimePrefix() {\r\n        return \"audio\";\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/models/CaptionSelector.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.models;\r\n\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\n\r\nimport java.util.List;\r\n\r\npublic class CaptionSelector extends FormatSelector {\r\n    public CaptionSelector(String displayName, boolean discardMedia) {\r\n        super(displayName, discardMedia);\r\n    }\r\n\r\n    public CaptionSelector(String displayName, boolean discardMedia, FormatId... formatIds) {\r\n        super(displayName, discardMedia, formatIds);\r\n    }\r\n\r\n    public CaptionSelector(String displayName, boolean discardMedia, Format... selectedFormats) {\r\n        super(displayName, discardMedia, selectedFormats);\r\n    }\r\n\r\n    @Override\r\n    public String getMimePrefix() {\r\n        return \"text\";\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/models/ConsumedRange.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.models;\r\n\r\npublic class ConsumedRange {\r\n    public int startSequenceNumber;\r\n    public int endSequenceNumber;\r\n    public long startTimeMs;\r\n    public long durationMs;\r\n\r\n    public ConsumedRange(long startTimeMs, long durationMs, int startSequenceNumber, int endSequenceNumber) {\r\n        this.startTimeMs = startTimeMs;\r\n        this.durationMs = durationMs;\r\n        this.startSequenceNumber = startSequenceNumber;\r\n        this.endSequenceNumber = endSequenceNumber;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/models/FormatSelector.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.models;\r\n\r\nimport androidx.annotation.Nullable;\r\n\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Arrays;\r\nimport java.util.List;\r\n\r\npublic class FormatSelector {\r\n    public final String displayName;\r\n    public final List<FormatId> formatIds = new ArrayList<>();\r\n    public final List<Format> formats = new ArrayList<>();\r\n    public final boolean discardMedia;\r\n\r\n    public FormatSelector(String displayName, boolean discardMedia) {\r\n        this(displayName, discardMedia, (FormatId[]) null);\r\n    }\r\n\r\n    public FormatSelector(String displayName, boolean discardMedia, FormatId... formatIds) {\r\n        this.displayName = displayName;\r\n        this.discardMedia = discardMedia;\r\n\r\n        if (formatIds != null) {\r\n            this.formatIds.addAll(Arrays.asList(formatIds));\r\n        }\r\n    }\r\n\r\n    public FormatSelector(String displayName, boolean discardMedia, Format... formats) {\r\n        this.displayName = displayName;\r\n        this.discardMedia = discardMedia;\r\n\r\n        if (formats != null) {\r\n            for (Format format : formats) {\r\n                this.formatIds.add(createFormatId(format));\r\n            }\r\n            this.formats.addAll(Arrays.asList(formats));\r\n        }\r\n    }\r\n\r\n    public String getMimePrefix() {\r\n        return null;\r\n    }\r\n\r\n    public boolean match(FormatId formatId, String mimeType) {\r\n        return formatIds.contains(formatId)\r\n                || (formatIds.isEmpty() && getMimePrefix() != null && mimeType != null && mimeType.toLowerCase().startsWith(getMimePrefix()))\r\n                || Helpers.findFirst(formatIds, fmt -> fmt.hasItag() && formatId.hasItag() && fmt.getItag() == formatId.getItag()) != null;\r\n    }\r\n\r\n    public boolean isDiscardMedia() {\r\n        return discardMedia;\r\n    }\r\n\r\n    public @Nullable Format getSelectedFormat() {\r\n        return !formats.isEmpty() ? formats.get(0) : null;\r\n    }\r\n\r\n    public @Nullable FormatId getSelectedFormatId() {\r\n        return !formatIds.isEmpty() ? formatIds.get(0) : null;\r\n    }\r\n\r\n    private static FormatId createFormatId(Format format) {\r\n        FormatId formatId = FormatId.newBuilder()\r\n                .setItag(Helpers.parseInt(format.id))\r\n                .setLastModified(format.lastModified)\r\n                .build();\r\n        return formatId;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/models/Segment.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.models;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\nimport com.google.android.exoplayer2.source.sabr.protos.videostreaming.MediaHeader;\r\n\r\npublic class Segment {\r\n    public final MediaHeader mediaHeader;\r\n    public final FormatId formatId;\r\n    public final boolean isInitSegment;\r\n    public final long durationMs;\r\n    public final long startRange;\r\n    public final int sequenceNumber;\r\n    public final long contentLength;\r\n    public final boolean contentLengthEstimated;\r\n    public final long startMs;\r\n    public final SelectedFormat initializedFormat;\r\n    public final boolean durationEstimated;\r\n    public final boolean discard;\r\n    public final boolean consumed;\r\n    public final long sequenceLmt;\r\n    public int receivedDataLength;\r\n\r\n    public Segment(MediaHeader mediaHeader,\r\n                   FormatId formatId,\r\n                   boolean isInitSegment,\r\n                   long durationMs,\r\n                   long startRange,\r\n                   int sequenceNumber,\r\n                   long contentLength,\r\n                   boolean contentLengthEstimated,\r\n                   long startMs,\r\n                   SelectedFormat initializedFormat,\r\n                   boolean durationEstimated,\r\n                   boolean discard,\r\n                   boolean consumed,\r\n                   long sequenceLmt) {\r\n        this.mediaHeader = mediaHeader;\r\n        this.formatId = formatId;\r\n        this.isInitSegment = isInitSegment;\r\n        this.durationMs = durationMs;\r\n        this.startRange = startRange;\r\n        this.sequenceNumber = sequenceNumber;\r\n        this.contentLength = contentLength;\r\n        this.contentLengthEstimated = contentLengthEstimated;\r\n        this.startMs = startMs;\r\n        this.initializedFormat = initializedFormat;\r\n        this.durationEstimated = durationEstimated;\r\n        this.discard = discard;\r\n        this.consumed = consumed;\r\n        this.sequenceLmt = sequenceLmt;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/models/SelectedFormat.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.models;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class SelectedFormat {\r\n    public final FormatId formatId;\r\n    public final long durationMs;\r\n    public final long endTimeMs;\r\n    public final String mimeType;\r\n    public final String videoId;\r\n    public final FormatSelector formatSelector;\r\n    public long totalSegments;\r\n    public final boolean discard;\r\n    public long sequenceLmt = -1;\r\n    public Segment currentSegment;\r\n    public Segment initSegment;\r\n    public final List<ConsumedRange> consumedRanges = new ArrayList<>();\r\n\r\n    public SelectedFormat(\r\n            FormatId formatId,\r\n            long durationMs,\r\n            long endTimeMs,\r\n            String mimeType,\r\n            String videoId,\r\n            FormatSelector formatSelector,\r\n            long totalSegments,\r\n            boolean discard\r\n    ) {\r\n        this.formatId = formatId;\r\n        this.durationMs = durationMs;\r\n        this.endTimeMs = endTimeMs;\r\n        this.mimeType = mimeType;\r\n        this.videoId = videoId;\r\n        this.formatSelector = formatSelector;\r\n        this.totalSegments = totalSegments;\r\n        this.discard = discard;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/models/VideoSelector.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.models;\r\n\r\nimport com.google.android.exoplayer2.Format;\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\n\r\npublic class VideoSelector extends FormatSelector {\r\n    public VideoSelector(String displayName, boolean discardMedia) {\r\n        super(displayName, discardMedia);\r\n    }\r\n\r\n    public VideoSelector(String displayName, boolean discardMedia, FormatId... formatIds) {\r\n        super(displayName, discardMedia, formatIds);\r\n    }\r\n\r\n    public VideoSelector(String displayName, boolean discardMedia, Format... selectedFormats) {\r\n        super(displayName, discardMedia, selectedFormats);\r\n    }\r\n\r\n    @Override\r\n    public String getMimePrefix() {\r\n        return \"video\";\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/parts/FormatInitializedSabrPart.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.parts;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.FormatSelector;\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\n\r\npublic class FormatInitializedSabrPart implements SabrPart {\r\n    public final FormatId formatId;\r\n    public final FormatSelector formatSelector;\r\n    public final long endTimeMs;\r\n\r\n    public FormatInitializedSabrPart(FormatId formatId, FormatSelector formatSelector, long endTimeMs) {\r\n        this.formatId = formatId;\r\n        this.formatSelector = formatSelector;\r\n        this.endTimeMs = endTimeMs;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/parts/MediaSeekSabrPart.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.parts;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.FormatSelector;\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\n\r\npublic class MediaSeekSabrPart implements SabrPart {\r\n    public Reason reason;\r\n    public FormatId formatId;\r\n    public FormatSelector formatSelector;\r\n\r\n    public MediaSeekSabrPart(Reason reason, FormatId formatId, FormatSelector formatSelector) {\r\n        this.reason = reason;\r\n        this.formatId = formatId;\r\n        this.formatSelector = formatSelector;\r\n    }\r\n\r\n    // Lets the consumer know the media sequence for a format may change\r\n    public enum Reason {\r\n        UNKNOWN,\r\n        SERVER_SEEK,         // SABR_SEEK from server\r\n        CONSUMED_SEEK        // Seeking as next fragment is already buffered\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/parts/MediaSegmentDataSabrPart.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.parts;\r\n\r\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.FormatSelector;\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\n\r\npublic class MediaSegmentDataSabrPart implements SabrPart {\r\n    public final FormatSelector formatSelector;\r\n    public final FormatId formatId;\r\n    public final long sequenceNumber;\r\n    public final boolean isInitSegment;\r\n    public final long totalSegments;\r\n    public final long startTimeMs;\r\n    public final ExtractorInput data;\r\n    public final int contentLength;\r\n    public final int segmentStartBytes;\r\n\r\n    public MediaSegmentDataSabrPart(\r\n            FormatSelector formatSelector,\r\n            FormatId formatId,\r\n            long sequenceNumber,\r\n            boolean isInitSegment,\r\n            long totalSegments,\r\n            long startTimeMs,\r\n            ExtractorInput data,\r\n            int contentLength,\r\n            int segmentStartBytes) {\r\n        this.formatSelector = formatSelector;\r\n        this.formatId = formatId;\r\n        this.sequenceNumber = sequenceNumber;\r\n        this.isInitSegment = isInitSegment;\r\n        this.totalSegments = totalSegments;\r\n        this.startTimeMs = startTimeMs;\r\n        this.data = data;\r\n        this.contentLength = contentLength;\r\n        this.segmentStartBytes = segmentStartBytes;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/parts/MediaSegmentEndSabrPart.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.parts;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.FormatSelector;\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\n\r\npublic class MediaSegmentEndSabrPart implements SabrPart {\r\n    public final FormatSelector formatSelector;\r\n    public final FormatId formatId;\r\n    public final long sequenceNumber;\r\n    public final boolean isInitSegment;\r\n    public final long totalSegments;\r\n    public final long startTimeMs;\r\n    public final long durationMs;\r\n\r\n    public MediaSegmentEndSabrPart(\r\n            FormatSelector formatSelector,\r\n            FormatId formatId,\r\n            long sequenceNumber,\r\n            boolean isInitSegment,\r\n            long totalSegments,\r\n            long startTimeMs,\r\n            long durationMs) {\r\n        this.formatSelector = formatSelector;\r\n        this.formatId = formatId;\r\n        this.sequenceNumber = sequenceNumber;\r\n        this.isInitSegment = isInitSegment;\r\n        this.totalSegments = totalSegments;\r\n        this.startTimeMs = startTimeMs;\r\n        this.durationMs = durationMs;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/parts/MediaSegmentInitSabrPart.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.parts;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.parser.models.FormatSelector;\r\nimport com.google.android.exoplayer2.source.sabr.protos.misc.FormatId;\r\n\r\npublic class MediaSegmentInitSabrPart implements SabrPart {\r\n    public final FormatSelector formatSelector;\r\n    public final FormatId formatId;\r\n    public final long playerTimeMs;\r\n    public final long sequenceNumber;\r\n    public final long totalSegments;\r\n    public final long durationMs;\r\n    public final boolean durationEstimated;\r\n    public final long startBytes;\r\n    public final long startTimeMs;\r\n    public final boolean isInitSegment;\r\n    public final long contentLength;\r\n    public final boolean contentLengthEstimate;\r\n\r\n    public MediaSegmentInitSabrPart(\r\n            FormatSelector formatSelector,\r\n            FormatId formatId,\r\n            long playerTimeMs,\r\n            long sequenceNumber,\r\n            long totalSegments,\r\n            long durationMs,\r\n            boolean durationEstimated,\r\n            long startBytes,\r\n            long startTimeMs,\r\n            boolean isInitSegment,\r\n            long contentLength,\r\n            boolean contentLengthEstimate) {\r\n        this.formatSelector = formatSelector;\r\n        this.formatId = formatId;\r\n        this.playerTimeMs = playerTimeMs;\r\n        this.sequenceNumber = sequenceNumber;\r\n        this.totalSegments = totalSegments;\r\n        this.durationMs = durationMs;\r\n        this.durationEstimated = durationEstimated;\r\n        this.startBytes = startBytes;\r\n        this.startTimeMs = startTimeMs;\r\n        this.isInitSegment = isInitSegment;\r\n        this.contentLength = contentLength;\r\n        this.contentLengthEstimate = contentLengthEstimate;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/parts/PoTokenStatusSabrPart.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.parts;\r\n\r\npublic class PoTokenStatusSabrPart implements SabrPart {\r\n    public final PoTokenStatus status;\r\n\r\n    public PoTokenStatusSabrPart(PoTokenStatus status) {\r\n        this.status = status;\r\n    }\r\n\r\n    public enum PoTokenStatus {\r\n        OK,                          // PO Token is provided and valid\r\n        MISSING,                     // PO Token is not provided, and is required. A PO Token should be provided ASAP\r\n        INVALID,                     // PO Token is provided, but is invalid. A new one should be generated ASAP\r\n        PENDING,                     // PO Token is provided, but probably only a cold start token. A full PO Token should be provided ASAP\r\n        NOT_REQUIRED,                // PO Token is not provided, and is not required\r\n        PENDING_MISSING              // PO Token is not provided, but is pending. A full PO Token should be (probably) provided ASAP\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/parts/RefreshPlayerResponseSabrPart.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.parts;\r\n\r\npublic class RefreshPlayerResponseSabrPart implements SabrPart {\r\n    public final Reason reason;\r\n    public final String reloadPlaybackToken;\r\n\r\n    public RefreshPlayerResponseSabrPart(Reason reason, String reloadPlaybackToken) {\r\n        this.reason = reason;\r\n        this.reloadPlaybackToken = reloadPlaybackToken;\r\n    }\r\n\r\n    public enum Reason {\r\n        UNKNOWN,\r\n        SABR_URL_EXPIRY,\r\n        SABR_RELOAD_PLAYER_RESPONSE\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/parts/SabrPart.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.parts;\r\n\r\npublic interface SabrPart {\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/results/ProcessFormatInitializationMetadataResult.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.results;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.FormatInitializedSabrPart;\r\n\r\npublic class ProcessFormatInitializationMetadataResult {\r\n    public FormatInitializedSabrPart sabrPart;\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/results/ProcessLiveMetadataResult.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.results;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSeekSabrPart;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class ProcessLiveMetadataResult {\r\n    public final List<MediaSeekSabrPart> seekSabrParts = new ArrayList<>();\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/results/ProcessMediaEndResult.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.results;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSegmentEndSabrPart;\r\n\r\npublic class ProcessMediaEndResult {\r\n    public MediaSegmentEndSabrPart sabrPart;\r\n    public boolean isNewSegment; // TODO: better name\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/results/ProcessMediaHeaderResult.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.results;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSegmentInitSabrPart;\r\n\r\npublic class ProcessMediaHeaderResult {\r\n    public MediaSegmentInitSabrPart sabrPart;\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/results/ProcessMediaResult.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.results;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSegmentDataSabrPart;\r\n\r\npublic class ProcessMediaResult {\r\n    public MediaSegmentDataSabrPart sabrPart;\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/results/ProcessSabrSeekResult.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.results;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.MediaSeekSabrPart;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class ProcessSabrSeekResult {\r\n    public final List<MediaSeekSabrPart> seekSabrParts = new ArrayList<>();\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/results/ProcessStreamProtectionStatusResult.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.results;\r\n\r\nimport com.google.android.exoplayer2.source.sabr.parser.parts.PoTokenStatusSabrPart;\r\n\r\npublic class ProcessStreamProtectionStatusResult {\r\n    public PoTokenStatusSabrPart sabrPart;\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/ump/UMPDecoder.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.ump;\r\n\r\nimport androidx.annotation.NonNull;\r\n\r\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\r\nimport com.liskovsoft.sharedutils.helpers.Helpers;\r\n\r\nimport java.io.ByteArrayInputStream;\r\nimport java.io.IOException;\r\n\r\npublic class UMPDecoder {\r\n    public UMPPart decode(@NonNull ExtractorInput extractorInput) {\r\n        try {\r\n            int partType = (int) readVarInt(extractorInput);\r\n            if (partType == -1) {\r\n                return null;\r\n            }\r\n\r\n            int partSize = (int) readVarInt(extractorInput);\r\n            if (partSize == -1) {\r\n                throw new IllegalStateException(\"Unexpected EOF while reading part size\");\r\n            }\r\n\r\n            return new UMPPart(partType, partSize, extractorInput);\r\n        } catch (IOException | InterruptedException e) {\r\n            throw new IllegalStateException(e);\r\n        }\r\n    }\r\n\r\n    private long readVarInt(StreamWrapper input) throws IOException, InterruptedException {\r\n        // https://web.archive.org/web/20250430054327/https://github.com/gsuberland/UMP_Format/blob/main/UMP_Format.md\r\n        // https://web.archive.org/web/20250429151021/https://github.com/davidzeng0/innertube/blob/main/googlevideo/ump.md\r\n        byte[] buffer = new byte[1];\r\n        boolean success = input.readFully(buffer, 0, 1, true);\r\n        if (!success) {\r\n            // Expected EOF\r\n            return -1;\r\n        }\r\n\r\n        long byteInt = buffer[0] & 0xFF; // convert to unsigned (0..255)\r\n        int size = varIntSize(byteInt);\r\n        long result = 0;\r\n        int shift = 0;\r\n\r\n        if (size != 5) {\r\n            shift = 8 - size;\r\n            int mask = (1 << shift) - 1;\r\n            result |= byteInt & mask;\r\n        }\r\n\r\n        for (int i : Helpers.range(1, size - 1, 1)) {\r\n            success = input.readFully(buffer, 0, 1, true);\r\n            if (!success) {\r\n                return -1;\r\n            }\r\n            byteInt = buffer[0] & 0xFF; // convert to unsigned (0..255)\r\n            result |= byteInt << shift;\r\n            shift += 8;\r\n        }\r\n\r\n        return result;\r\n    }\r\n\r\n    //private long readVarInt(StreamWrapper input) throws IOException, InterruptedException {\r\n    //    // https://web.archive.org/web/20250430054327/https://github.com/gsuberland/UMP_Format/blob/main/UMP_Format.md\r\n    //    // https://web.archive.org/web/20250429151021/https://github.com/davidzeng0/innertube/blob/main/googlevideo/ump.md\r\n    //    byte[] buffer = new byte[1];\r\n    //    if (!input.readFully(buffer, 0, 1, true)) {\r\n    //        return -1; // clean EOF before reading anything\r\n    //    }\r\n    //\r\n    //    long first = buffer[0] & 0xFF;\r\n    //    int size = varIntSize(first);\r\n    //\r\n    //    if (size < 1 || size > 5) {\r\n    //        throw new IOException(\"Invalid VarInt size: \" + size);\r\n    //    }\r\n    //\r\n    //    int payloadBits = 8 - (size + 1);\r\n    //    long result = first & ((1L << payloadBits) - 1);\r\n    //    int shift = payloadBits;\r\n    //\r\n    //    for (int i = 1; i < size; i++) {\r\n    //        if (!input.readFully(buffer, 0, 1, true)) {\r\n    //            throw new EOFException(\"Unexpected EOF in VarInt\");\r\n    //        }\r\n    //        long b = buffer[0] & 0xFF;\r\n    //        result |= b << shift;\r\n    //        shift += 8;\r\n    //    }\r\n    //\r\n    //    return result;\r\n    //}\r\n\r\n    public long readVarInt(ExtractorInput input) throws IOException, InterruptedException {\r\n        return readVarInt(input::readFully);\r\n    }\r\n\r\n    public long readVarInt(ByteArrayInputStream inputStream) throws IOException, InterruptedException {\r\n        return readVarInt((target, offset, length, allowEndOfInput) -> {\r\n            int numRead = inputStream.read(target, offset, length);\r\n            return numRead != -1;\r\n        });\r\n    }\r\n\r\n    private int varIntSize(long byteInt) {\r\n        return byteInt < 128 ? 1 : byteInt < 192 ? 2 : byteInt < 224 ? 3 : byteInt < 240 ? 4 : 5;\r\n    }\r\n\r\n    private interface StreamWrapper {\r\n        boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)\r\n                throws IOException, InterruptedException;\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/ump/UMPEncoder.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.ump;\r\n\r\npublic class UMPEncoder {\r\n    public void encode(UMPPart part) {\r\n        \r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/ump/UMPInputStream.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.ump;\r\n\r\nimport com.google.android.exoplayer2.C;\r\n\r\nimport java.io.IOException;\r\nimport java.io.InputStream;\r\n\r\npublic class UMPInputStream extends InputStream {\r\n    private final UMPPart part;\r\n    private int position = 0; // bytes read so far\r\n\r\n    public UMPInputStream(UMPPart part) {\r\n        this.part = part;\r\n    }\r\n\r\n    @Override\r\n    public int read() throws IOException {\r\n        if (position >= part.size) return -1;\r\n\r\n        byte[] buffer = new byte[1];\r\n        int read;\r\n        try {\r\n            read = part.data.read(buffer, 0, 1);\r\n        } catch (InterruptedException e) {\r\n            Thread.currentThread().interrupt(); // preserve interrupt status\r\n            throw new IOException(\"Interrupted while reading from ExtractorInput\", e);\r\n        }\r\n\r\n        if (read == C.RESULT_END_OF_INPUT) return -1;\r\n        position += read;\r\n        return buffer[0] & 0xFF;\r\n    }\r\n\r\n    @Override\r\n    public int read(byte[] b, int off, int len) throws IOException {\r\n        if (position >= part.size) return -1;\r\n\r\n        int toRead = Math.min(len, part.size - position);\r\n        int read;\r\n        try {\r\n            read = part.data.read(b, off, toRead);\r\n        } catch (InterruptedException e) {\r\n            Thread.currentThread().interrupt();\r\n            throw new IOException(\"Interrupted while reading from UMPPart\", e);\r\n        }\r\n\r\n        if (read == C.RESULT_END_OF_INPUT) return -1;\r\n        position += read;\r\n        return read;\r\n    }\r\n\r\n    @Override\r\n    public long skip(long n) throws IOException {\r\n        int toSkip = (int) Math.min(n, part.size - position);\r\n        int skipped;\r\n        try {\r\n            skipped = part.data.skip(toSkip);\r\n        } catch (InterruptedException e) {\r\n            Thread.currentThread().interrupt();\r\n            throw new IOException(\"Interrupted while skipping in UMPPart\", e);\r\n        }\r\n        position += skipped;\r\n        return skipped;\r\n    }\r\n\r\n    @Override\r\n    public int available() {\r\n        return part.size - position;\r\n    }\r\n}\r\n\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/ump/UMPPart.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.ump;\r\n\r\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\r\n\r\npublic class UMPPart {\r\n    public final int partId;\r\n    public final int size;\r\n    public final ExtractorInput data;\r\n\r\n    public UMPPart(int partId, int size, ExtractorInput data) {\r\n        this.partId = partId;\r\n        this.size = size;\r\n        this.data = data;\r\n    }\r\n\r\n    public UMPInputStream toStream() {\r\n        return new UMPInputStream(this);\r\n    }\r\n\r\n    public void skip() {\r\n        try {\r\n            data.skipFully(size);\r\n        } catch (Exception e) {\r\n            throw new IllegalStateException(\"Cannot skip part with the id: \" + partId + \", and size: \" + size, e);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/java/com/google/android/exoplayer2/source/sabr/parser/ump/UMPPartId.java",
    "content": "package com.google.android.exoplayer2.source.sabr.parser.ump;\r\n\r\npublic class UMPPartId {\r\n    public static final int UNKNOWN = -1;\r\n    public static final int ONESIE_HEADER = 10;\r\n    public static final int ONESIE_DATA = 11;\r\n    public static final int ONESIE_ENCRYPTED_MEDIA = 12;\r\n    public static final int MEDIA_HEADER = 20;\r\n    public static final int MEDIA = 21;\r\n    public static final int MEDIA_END = 22;\r\n    public static final int LIVE_METADATA = 31;\r\n    public static final int HOSTNAME_CHANGE_HINT = 32;\r\n    public static final int LIVE_METADATA_PROMISE = 33;\r\n    public static final int LIVE_METADATA_PROMISE_CANCELLATION = 34;\r\n    public static final int NEXT_REQUEST_POLICY = 35;\r\n    public static final int USTREAMER_VIDEO_AND_FORMAT_DATA = 36;\r\n    public static final int FORMAT_SELECTION_CONFIG = 37;\r\n    public static final int USTREAMER_SELECTED_MEDIA_STREAM = 38;\r\n    public static final int FORMAT_INITIALIZATION_METADATA = 42;\r\n    public static final int SABR_REDIRECT = 43;\r\n    public static final int SABR_ERROR = 44;\r\n    public static final int SABR_SEEK = 45;\r\n    public static final int RELOAD_PLAYER_RESPONSE = 46;\r\n    public static final int PLAYBACK_START_POLICY = 47;\r\n    public static final int ALLOWED_CACHED_FORMATS = 48;\r\n    public static final int START_BW_SAMPLING_HINT = 49;\r\n    public static final int PAUSE_BW_SAMPLING_HINT = 50;\r\n    public static final int SELECTABLE_FORMATS = 51;\r\n    public static final int REQUEST_IDENTIFIER = 52;\r\n    public static final int REQUEST_CANCELLATION_POLICY = 53;\r\n    public static final int ONESIE_PREFETCH_REJECTION = 54;\r\n    public static final int TIMELINE_CONTEXT = 55;\r\n    public static final int REQUEST_PIPELINING = 56;\r\n    public static final int SABR_CONTEXT_UPDATE = 57;\r\n    public static final int STREAM_PROTECTION_STATUS = 58;\r\n    public static final int SABR_CONTEXT_SENDING_POLICY = 59;\r\n    public static final int LAWNMOWER_POLICY = 60;\r\n    public static final int SABR_ACK = 61;\r\n    public static final int END_OF_TRACK = 62;\r\n    public static final int CACHE_LOAD_POLICY = 63;\r\n    public static final int LAWNMOWER_MESSAGING_POLICY = 64;\r\n    public static final int PREWARM_CONNECTION = 65;\r\n    public static final int PLAYBACK_DEBUG_INFO = 66;\r\n    public static final int SNACKBAR_MESSAGE = 67;\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/misc/common.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.misc;\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.misc\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage HttpHeader {\r\n  optional string name = 1;\r\n  optional string value = 2;\r\n}\r\n\r\nmessage FormatId {\r\n  optional int32 itag = 1;\r\n  optional uint64 last_modified = 2;\r\n  optional string xtags = 3;\r\n}\r\n\r\nmessage Range {\r\n  optional int32 legacy_start = 1;\r\n  optional int32 legacy_end = 2;\r\n  optional int32 start = 3;\r\n  optional int32 end = 4;\r\n}\r\n\r\nenum CompressionType {\r\n  UNKNOWN = 0;\r\n  GZIP = 1;\r\n  BROTLI = 2;\r\n}\r\n\r\nmessage IdentifierToken {\r\n  optional int32 request_number = 1;\r\n  optional int32 field5 = 5;\r\n}\r\n\r\nmessage KeyValuePair {\r\n  optional string key = 1;\r\n  optional string value = 2;\r\n}\r\n\r\nenum AudioQuality {\r\n  AUDIO_QUALITY_UNKNOWN = 0;\r\n  AUDIO_QUALITY_ULTRALOW = 5;\r\n  AUDIO_QUALITY_LOW = 10;\r\n  AUDIO_QUALITY_MEDIUM = 20;\r\n  AUDIO_QUALITY_HIGH = 30;\r\n}\r\n\r\nenum VideoQualitySetting {\r\n  VIDEO_QUALITY_SETTING_UNKNOWN = 0;\r\n  VIDEO_QUALITY_SETTING_HIGHER_QUALITY = 1;\r\n  VIDEO_QUALITY_SETTING_DATA_SAVER = 2;\r\n  VIDEO_QUALITY_SETTING_ADVANCED_MENU = 3;\r\n}\r\n\r\nenum PlaybackAudioRouteOutputType {\r\n  PLAYBACK_AUDIO_ROUTE_OUTPUT_TYPE_UNKNOWN = 0;\r\n  PLAYBACK_AUDIO_ROUTE_OUTPUT_TYPE_LINE_OUT = 1;\r\n  PLAYBACK_AUDIO_ROUTE_OUTPUT_TYPE_HEADPHONES = 2;\r\n  PLAYBACK_AUDIO_ROUTE_OUTPUT_TYPE_BLUETOOTH_A2DP = 3;\r\n  PLAYBACK_AUDIO_ROUTE_OUTPUT_TYPE_BUILT_IN_RECEIVER = 4;\r\n  PLAYBACK_AUDIO_ROUTE_OUTPUT_TYPE_BUILT_IN_SPEAKER = 5;\r\n  PLAYBACK_AUDIO_ROUTE_OUTPUT_TYPE_HDMI = 6;\r\n  PLAYBACK_AUDIO_ROUTE_OUTPUT_TYPE_AIR_PLAY = 7;\r\n  PLAYBACK_AUDIO_ROUTE_OUTPUT_TYPE_BLUETOOTH_LE = 8;\r\n  PLAYBACK_AUDIO_ROUTE_OUTPUT_TYPE_BLUETOOTH_HFP = 9;\r\n  PLAYBACK_AUDIO_ROUTE_OUTPUT_TYPE_USB_AUDIO = 10;\r\n  PLAYBACK_AUDIO_ROUTE_OUTPUT_TYPE_CAR_PLAY = 11;\r\n  PLAYBACK_AUDIO_ROUTE_OUTPUT_TYPE_ANDROID_AUDIO = 12;\r\n}\r\n\r\nenum NetworkMeteredState {\r\n  NETWORK_METERED_STATE_UNKNOWN = 0;\r\n  NETWORK_METERED_STATE_UNMETERED = 1;\r\n  NETWORK_METERED_STATE_METERED = 2;\r\n}\r\n\r\nenum SeekSource {\r\n  SEEK_SOURCE_UNKNOWN = 0;\r\n  SEEK_SOURCE_TIMESTAMP_IN_COMMENTS = 1;\r\n  SEEK_SOURCE_TIMESTAMP_IN_DESCRIPTION = 2;\r\n  SEEK_SOURCE_MACRO_MARKER_LIST_ITEM = 3;\r\n  SEEK_SOURCE_DOUBLE_TAP_TO_SEEK = 4;\r\n  SEEK_SOURCE_DOUBLE_TAP_TO_SKIP_CHAPTER = 5;\r\n  SEEK_SOURCE_PICK_UP_PLAY_HEAD = 6;\r\n  SEEK_SOURCE_SLIDE_ON_SCRUBBER_BAR = 7;\r\n  SEEK_SOURCE_SLIDE_ON_PLAYER = 8;\r\n  SEEK_SOURCE_SABR_PARTIAL_CHUNK = 9;\r\n  SEEK_SOURCE_SABR_SEEK_TO_HEAD = 10;\r\n  SEEK_SOURCE_SABR_LIVE_DVR_USER_SEEK = 11;\r\n  SEEK_SOURCE_SABR_SEEK_TO_DVR_LOWER_BOUND = 12;\r\n  SEEK_SOURCE_SABR_SEEK_TO_DVR_UPPER_BOUND = 13;\r\n  SEEK_SOURCE_SSDAI_INTERNAL = 14;\r\n  SEEK_SOURCE_START_PLAYBACK = 15;\r\n  SEEK_SOURCE_SABR_ACCURATE_SEEK = 17;\r\n  SEEK_SOURCE_START_PLAYBACK_SEEK_TO_END = 18;\r\n  SEEK_SOURCE_IOS_PLAYER_REMOVED_SEGMENTS = 19;\r\n  SEEK_SOURCE_IOS_PLAYER_SEGMENT_LIST = 20;\r\n  SEEK_SOURCE_IOS_PLAYER_ITEM_SEEK = 21;\r\n  SEEK_SOURCE_IOS_PLAYER_ITEM_SEEK_TO_END = 22;\r\n  SEEK_SOURCE_IOS_PLAYER_SEEK_TO_END_TO_RESYNC = 23;\r\n  SEEK_SOURCE_IOS_SEEK_ACCESSIBILITY_BUTTON = 24;\r\n  SEEK_SOURCE_FINE_SCRUBBER_SLIDE_ON_FILMSTRIP = 25;\r\n  SEEK_SOURCE_FINE_SCRUBBER_TAP_ON_FILMSTRIP = 26;\r\n  SEEK_SOURCE_FINE_SCRUBBER_SLIDE_ON_SCRUBBER_BAR = 27;\r\n  SEEK_SOURCE_SEEK_BUTTON_ON_PLAYER_CONTROL = 28;\r\n  SEEK_SOURCE_SABR_INGESTION_WALL_TIME_SEEK = 29;\r\n  SEEK_SOURCE_PLAYER_VIEW_REPARENT_INTERNAL = 30;\r\n  SEEK_SOURCE_PRESS_REWIND_PLAY_BACK_CONTROL = 31;\r\n  SEEK_SOURCE_PRESS_FAST_FORWARD_PLAY_BACK_CONTROL = 32;\r\n  SEEK_SOURCE_PRESS_LIVE_SYNC_ICON = 33;\r\n  SEEK_SOURCE_PEG_TO_LIVE = 34;\r\n  SEEK_SOURCE_ANDROID_MEDIA_SESSION = 35;\r\n  SEEK_SOURCE_TAP_ON_REPLAY_ACTION = 36;\r\n  SEEK_SOURCE_AUTOMATIC_REPLAY_ACTION = 37;\r\n  SEEK_SOURCE_NON_USER_SEEK_TO_PREVIOUS = 38;\r\n  SEEK_SOURCE_NON_USER_SEEK_TO_NEXT = 39;\r\n  SEEK_SOURCE_HIGHLIGHTS_TAP_PREVIOUS_PLAY = 66;\r\n  SEEK_SOURCE_HIGHLIGHTS_TAP_NEXT_PLAY = 40;\r\n  SEEK_SOURCE_HIGHLIGHTS_TAP_HIDDEN_NEXT_PLAY = 41;\r\n  SEEK_SOURCE_HIGHLIGHTS_TAP_LIST_ITEM = 42;\r\n  SEEK_SOURCE_HIGHLIGHTS_AUTOMATIC_NEXT_PLAY = 43;\r\n  SEEK_SOURCE_HIGHLIGHTS_SEEK_TO_FIRST_PLAY = 44;\r\n  SEEK_SOURCE_HIGHLIGHTS_SEEK_TO_END = 45;\r\n  SEEK_SOURCE_SEGMENTS_TAP_LIST_ITEM = 46;\r\n  SEEK_SOURCE_PIP_FAST_FORWARD_BUTTON = 47;\r\n  SEEK_SOURCE_PIP_REWIND_BUTTON = 48;\r\n  SEEK_SOURCE_PIP_RESUME_ON_HEAD = 49;\r\n  SEEK_SOURCE_MOVING_CLIP_FRAME = 50;\r\n  SEEK_SOURCE_RESUME_CLIP_PREVIOUS_POSITION = 51;\r\n  SEEK_SOURCE_SEEK_TO_NEXT_CHAPTER = 52;\r\n  SEEK_SOURCE_SEEK_TO_PREVIOUS_CHAPTER = 53;\r\n  SEEK_SOURCE_IOS_SHAREPLAY_PAUSE = 54;\r\n  SEEK_SOURCE_IOS_SHAREPLAY_SEEK = 55;\r\n  SEEK_SOURCE_IOS_SHAREPLAY_SYNC_RESPONSE = 56;\r\n  SEEK_SOURCE_SEEK_TO_HEAD_IMMERSIVE_LIVE_VIDEO = 57;\r\n  SEEK_SOURCE_SEEK_TO_START_OF_LOOPING_RANGE_OF_SHORTS = 58;\r\n  SEEK_SOURCE_SABR_SEEK_TO_CLOSEST_KEYFRAME = 59;\r\n  SEEK_SOURCE_SEEK_TO_END_OF_LOOPING_RANGE_OF_SHORTS = 60;\r\n  SEEK_SOURCE_CLIP_SLIDE_ON_FLIMSTRIP = 61;\r\n  SEEK_SOURCE_PICK_UP_CLIP_SLIDER = 62;\r\n  SEEK_SOURCE_FINE_SCRUBBER_CANCELLED = 63;\r\n  SEEK_SOURCE_INLINE_PLAYER_SEEK_CHAPTER = 64;\r\n  SEEK_SOURCE_INLINE_PLAYER_SEEK_SECONDS = 65;\r\n  SEEK_SOURCE_HIGHLIGHTS_PLAYER_EXIT_FULLSCREEN = 67;\r\n  SEEK_SOURCE_LARGE_CONTROLS_FORWARD_BUTTON = 68;\r\n  SEEK_SOURCE_LARGE_CONTROLS_REWIND_BUTTON = 69;\r\n  SEEK_SOURCE_LARGE_CONTROLS_SCRUBBER_BAR = 70;\r\n  SEEK_SOURCE_SEEK_BACKWARD_5S = 71;\r\n  SEEK_SOURCE_SEEK_FORWARD_5S = 72;\r\n  SEEK_SOURCE_SEEK_BACKWARD_10S = 73;\r\n  SEEK_SOURCE_SEEK_FORWARD_10S = 74;\r\n  SEEK_SOURCE_SEEK_FORWARD_60S = 75;\r\n  SEEK_SOURCE_SEEK_BACKWARD_60S = 76;\r\n  SEEK_SOURCE_SEEK_TO_NEXT_FRAME = 77;\r\n  SEEK_SOURCE_SEEK_TO_PREV_FRAME = 78;\r\n  SEEK_SOURCE_KEYBOARD_SEEK_TO_BEGINNING = 79;\r\n  SEEK_SOURCE_KEYBOARD_SEEK_TO_END = 80;\r\n  SEEK_SOURCE_SEEK_PERCENT_OF_VIDEO = 81;\r\n  SEEK_SOURCE_HIDDEN_FAST_FORWARD_BUTTON = 82;\r\n  SEEK_SOURCE_HIDDEN_REWIND_BUTTON = 83;\r\n  SEEK_SOURCE_TIMESTAMP = 84;\r\n  SEEK_SOURCE_LR_MEDIA_SESSION_SEEK = 87;\r\n  SEEK_SOURCE_MIDROLLS_WITH_TIME_RANGE = 88;\r\n  SEEK_SOURCE_SKIP_AD = 89;\r\n  SEEK_SOURCE_SEEK_TO_PREVIOUS = 90;\r\n  SEEK_SOURCE_SEEK_TO_NEXT = 91;\r\n  SEEK_SOURCE_LR_QUICK_SEEK = 92;\r\n  SEEK_SOURCE_ONESIE_LIVE = 93;\r\n  SEEK_SOURCE_LR_PLAYER_CONTROL_ACTION = 94;\r\n  SEEK_SOURCE_UNPLUGGED_LENS_START_CLIP = 95;\r\n  SEEK_SOURCE_LR_KEY_PLAYS = 96;\r\n  SEEK_SOURCE_SSAP_AD_FMT_FATAL = 97;\r\n  SEEK_SOURCE_TVHTML5_INPUT_SOURCE_KEY_EVENT = 98;\r\n  SEEK_SOURCE_TVHTML5_INPUT_SOURCE_CONTROLS = 99;\r\n  SEEK_SOURCE_TVHTML5_INPUT_SOURCE_TOUCH = 100;\r\n  SEEK_SOURCE_TVHTML5_INPUT_SOURCE_TOUCHPAD = 101;\r\n  SEEK_SOURCE_SEEK_TO_HEAD = 102;\r\n  SEEK_SOURCE_AUTOMATIC_PREVIEW_REPLAY_ACTION = 103;\r\n  SEEK_SOURCE_H5_MEDIA_ELEMENT_EVENT = 104;\r\n  SEEK_SOURCE_H5_WORKAROUND_SEEK = 105;\r\n  SEEK_SOURCE_MINIPLAYER_REWIND_BUTTON = 106;\r\n  SEEK_SOURCE_MINIPLAYER_FAST_FORWARD_BUTTON = 107;\r\n  SEEK_SOURCE_SABR_RELOAD_PLAYER_RESPONSE_TOKEN_SEEK = 108;\r\n  SEEK_SOURCE_SLIDE_ON_SCRUBBER_BAR_CHAPTER = 109;\r\n  SEEK_SOURCE_ANDROID_CLEAR_BUFFER = 110;\r\n}\r\n\r\nenum OnesieRequestTarget {\r\n  ONESIE_REQUEST_TARGET_UNKNOWN = 0;\r\n  ONESIE_REQUEST_TARGET_ENCRYPTED_PLAYER_SERVICE = 1;\r\n  ONESIE_REQUEST_TARGET_ENCRYPTED_WATCH_SERVICE_DEPRECATED = 2;\r\n  ONESIE_REQUEST_TARGET_ENCRYPTED_WATCH_SERVICE = 3;\r\n  ONESIE_REQUEST_TARGET_INNERTUBE_ENCRYPTED_SERVICE = 4;\r\n}\r\n\r\nmessage AuthorizedFormat {\r\n  optional int32 track_type = 1;\r\n  optional bool is_hdr = 2;\r\n}\r\n\r\nmessage PlaybackAuthorization {\r\n  repeated AuthorizedFormat authorized_formats = 1;\r\n  optional bytes sabr_license_constraint = 2;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/other/ump_part_id.proto",
    "content": "syntax = \"proto3\";\n\npackage sabr.other;\n\noption java_multiple_files = true;\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.other\";\noption optimize_for = LITE_RUNTIME;\n\nenum UmpPartId {\n  UNKNOWN = 0;\n  ONESIE_HEADER = 10;\n  ONESIE_DATA = 11;\n  ONESIE_ENCRYPTED_MEDIA = 12;\n  // Header for a media segment; includes sequence and timing information.\n  MEDIA_HEADER = 20;\n  // Chunk of media segment data.\n  MEDIA = 21;\n  // Indicates end of media segment; finalizes segment processing.\n  MEDIA_END = 22;\n  CONFIG = 30;\n  LIVE_METADATA = 31;\n  HOSTNAME_CHANGE_HINT_DEPRECATED = 32;\n  LIVE_METADATA_PROMISE = 33;\n  LIVE_METADATA_PROMISE_CANCELLATION = 34;\n  // Server's policy for the next request; includes backoff time and playback cookie.\n  NEXT_REQUEST_POLICY = 35;\n  USTREAMER_VIDEO_AND_FORMAT_METADATA = 36;\n  FORMAT_SELECTION_CONFIG = 37;\n  USTREAMER_SELECTED_MEDIA_STREAM = 38;\n  // Metadata for format initialization; contains total number of segments, duration, etc.\n  FORMAT_INITIALIZATION_METADATA = 42;\n  // Indicates a redirect to a different streaming URL.\n  SABR_REDIRECT = 43;\n  // Indicates a SABR error; happens when the payload is invalid or the server cannot process the request.\n  SABR_ERROR = 44;\n  SABR_SEEK = 45;\n  // Directive to reload the player with new parameters.\n  RELOAD_PLAYER_RESPONSE = 46;\n  PLAYBACK_START_POLICY = 47;\n  ALLOWED_CACHED_FORMATS = 48;\n  START_BW_SAMPLING_HINT = 49;\n  PAUSE_BW_SAMPLING_HINT = 50;\n  SELECTABLE_FORMATS = 51;\n  REQUEST_IDENTIFIER = 52;\n  REQUEST_CANCELLATION_POLICY = 53;\n  ONESIE_PREFETCH_REJECTION = 54;\n  TIMELINE_CONTEXT = 55;\n  REQUEST_PIPELINING = 56;\n  // Updates SABR context data; usually used for ads.\n  SABR_CONTEXT_UPDATE = 57;\n  // Status of stream protection; indicates whether attestation is required.\n  STREAM_PROTECTION_STATUS = 58;\n  // Policy indicating which SABR contexts to send or discard in future requests.\n  SABR_CONTEXT_SENDING_POLICY = 59;\n  LAWNMOWER_POLICY = 60;\n  SABR_ACK = 61;\n  END_OF_TRACK = 62;\n  CACHE_LOAD_POLICY = 63;\n  LAWNMOWER_MESSAGING_POLICY = 64;\n  PREWARM_CONNECTION = 65;\n  PLAYBACK_DEBUG_INFO = 66;\n  // Directive to show the user a notification message.\n  SNACKBAR_MESSAGE = 67;\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/buffered_range.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\nimport \"sabr/misc/common.proto\";\r\nimport \"sabr/videostreaming/time_range.proto\";\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage BufferedRange {\r\n  message UnknownMessage1 {\r\n    message UnknownInnerMessage {\r\n      optional string video_id = 1;\r\n      optional uint64 lmt = 2;\r\n    }\r\n    repeated UnknownInnerMessage field1 = 1;\r\n  }\r\n\r\n  message UnknownMessage2 {\r\n    optional int32 field1 = 1;\r\n    optional int32 field2 = 2;\r\n    optional int32 field3 = 3;\r\n  }\r\n\r\n  sabr.misc.FormatId format_id = 1;\r\n  int64 start_time_ms = 2;\r\n  int64 duration_ms = 3;\r\n  int32 start_segment_index = 4;\r\n  int32 end_segment_index = 5;\r\n  optional TimeRange time_range = 6;\r\n  optional UnknownMessage1 field9 = 9;\r\n  optional UnknownMessage2 field11 = 11;\r\n  optional UnknownMessage2 field12 = 12;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/client_abr_state.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\nimport \"sabr/misc/common.proto\";\r\nimport \"sabr/videostreaming/media_capabilities.proto\";\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage ClientAbrState {\r\n  optional int64 time_since_last_manual_format_selection_ms = 13;\r\n  optional sint32 last_manual_direction = 14;\r\n  optional int32 last_manual_selected_resolution = 16;\r\n  optional int32 detailed_network_type = 17;\r\n  optional int32 client_viewport_width = 18;\r\n  optional int32 client_viewport_height = 19;\r\n  optional int64 client_bitrate_cap_bytes_per_sec = 20;\r\n  optional int32 sticky_resolution = 21;\r\n  optional bool client_viewport_is_flexible = 22;\r\n  optional int64 bandwidth_estimate = 23;\r\n  optional sabr.misc.AudioQuality min_audio_quality = 24;\r\n  optional sabr.misc.AudioQuality max_audio_quality = 25;\r\n  optional sabr.misc.VideoQualitySetting video_quality_setting = 26;\r\n  optional sabr.misc.PlaybackAudioRouteOutputType audio_route = 27;\r\n  optional int64 player_time_ms = 28;\r\n  optional int64 time_since_last_seek = 29;\r\n  optional bool data_saver_mode = 30;\r\n  optional sabr.misc.NetworkMeteredState network_metered_state = 32;\r\n  optional int32 visibility = 34;\r\n  optional float playback_rate = 35;\r\n  optional int64 elapsed_wall_time_ms = 36;\r\n  optional MediaCapabilities media_capabilities = 38;\r\n  optional int64 time_since_last_action_ms = 39;\r\n  optional int32 enabled_track_types_bitfield = 40;\r\n  optional int32 max_pacing_rate = 43;\r\n  optional int64 player_state = 44;\r\n  optional bool drc_enabled = 46;\r\n  optional int32 field48 = 48;\r\n  optional int32 field50 = 50;\r\n  optional int32 field51 = 51;\r\n  optional int32 sabr_report_request_cancellation_info = 54;\r\n  optional bool disable_streaming_xhr = 56;\r\n  optional int64 field57 = 57;\r\n  optional bool prefer_vp9 = 58;\r\n  optional int32 av1_quality_threshold = 59; // 2160\r\n  optional int32 field60 = 60;\r\n  optional bool is_prefetch = 61;\r\n  optional bool sabr_support_quality_constraints = 62;\r\n  optional bytes sabr_license_constraint = 63;\r\n  optional int32 allow_proxima_live_latency = 64;\r\n  optional int32 sabr_force_proxima = 66;\r\n  optional int32 field67 = 67;\r\n  optional int64 sabr_force_max_network_interruption_duration_ms = 68;\r\n  optional string audio_track_id = 69;\r\n  optional bool enable_voice_boost = 76;\r\n  optional sabr.misc.PlaybackAuthorization playback_authorization = 79;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/crypto_params.proto",
    "content": "syntax = \"proto3\";\n\npackage sabr.videostreaming;\n\nimport \"sabr/misc/common.proto\";\n\noption java_multiple_files = true;\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\noption optimize_for = LITE_RUNTIME;\n\nmessage CryptoParams {\n  optional bytes hmac = 4;\n  optional bytes iv = 5;\n  optional sabr.misc.CompressionType compression_type = 6;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/format_initialization_metadata.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\nimport \"sabr/misc/common.proto\";\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage FormatInitializationMetadata {\r\n    optional string video_id = 1;\r\n    optional sabr.misc.FormatId format_id = 2;\r\n    optional int64 end_time_ms = 3;\r\n    optional int64 end_segment_number = 4;\r\n    optional string mime_type = 5;\r\n    optional sabr.misc.Range init_range = 6;\r\n    optional sabr.misc.Range index_range = 7;\r\n    optional int64 field8 = 8;\r\n    optional int64 duration_units = 9;\r\n    optional int64 duration_timescale = 10;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/innertube_request.proto",
    "content": "syntax = \"proto3\";\n\npackage sabr.videostreaming;\n\noption java_multiple_files = true;\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\noption optimize_for = LITE_RUNTIME;\n\nmessage InnertubeRequest {\n  optional bytes context = 1;\n  optional bytes encrypted_onesie_innertube_request = 2;\n  optional bytes encrypted_client_key = 5;\n  optional bytes iv = 6;\n  optional bytes hmac = 7;\n  optional string reverse_proxy_config = 9;\n  optional bool serialize_response_as_json = 10;\n  optional bool enable_ad_placements_preroll = 13;\n  optional bool enable_compression = 14;\n  optional UstreamerFlags ustreamer_flags = 15;\n  optional bytes unencrypted_onesie_innertube_request = 16;\n  optional bool use_jsonformatter_to_parse_player_response = 17;\n}\n\nmessage UstreamerFlags {\n  optional bool send_video_playback_config = 2;\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/live_metadata.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage LiveMetadata {\r\n  optional int32 head_sequence_number = 3;\r\n  optional int64 head_sequence_time_ms = 4;\r\n  optional int64 wall_time_ms = 5;\r\n  optional string video_id = 6;\r\n  optional string source = 7;\r\n\r\n  optional int64 min_seekable_time_ticks = 12;\r\n  optional int32 min_seekable_timescale = 13;\r\n  optional int64 max_seekable_time_ticks = 14;\r\n  optional int32 max_seekable_timescale = 15;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/media_capabilities.proto",
    "content": "syntax = \"proto3\";\n\npackage sabr.videostreaming;\n\noption java_multiple_files = true;\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\noption optimize_for = LITE_RUNTIME;\n\nmessage MediaCapabilities {\n  repeated VideoFormatCapability video_format_capabilities = 1;\n  repeated AudioFormatCapability audio_format_capabilities = 2;\n  optional int32 hdr_mode_bitmask = 5;\n\n  message VideoFormatCapability {\n    optional int32 video_codec = 1;\n    optional int32 max_height = 3;\n    optional int32 max_width = 4;\n    optional int32 max_framerate = 11;\n    optional int32 max_bitrate_bps = 12;\n    optional bool is_10_bit_supported = 15;\n  }\n\n  message AudioFormatCapability {\n    optional int32 audio_codec = 1;\n    optional int32 num_channels = 2;\n    optional int32 max_bitrate_bps = 3;\n    optional int32 spatial_capability_bitmask = 6;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/media_header.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\nimport \"sabr/misc/common.proto\";\r\nimport \"sabr/videostreaming/time_range.proto\";\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage MediaHeader {\r\n    optional uint32 header_id = 1;\r\n    optional string video_id = 2;\r\n    optional int32 itag = 3;\r\n    optional uint64 lmt = 4;\r\n    optional string xtags = 5;\r\n    optional int64 start_range = 6;\r\n    optional sabr.misc.CompressionType compression_algorithm = 7;\r\n    optional bool is_init_seg = 8;\r\n    optional int32 sequence_number = 9;\r\n    optional int64 bitrate_bps = 10;\r\n    optional int64 start_ms = 11;\r\n    optional int64 duration_ms = 12;\r\n    optional sabr.misc.FormatId format_id = 13;\r\n    optional int64 content_length = 14;\r\n    optional TimeRange time_range = 15;\r\n    optional uint64 sequence_lmt = 16;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/next_request_policy.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\nimport \"sabr/videostreaming/playback_cookie.proto\";\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage NextRequestPolicy {\r\n    optional int32 target_audio_readahead_ms = 1;\r\n    optional int32 target_video_readahead_ms = 2;\r\n    optional int32 max_time_since_last_request_ms = 3;\r\n    optional int32 backoff_time_ms = 4;\r\n    optional int32 min_audio_readahead_ms = 5;\r\n    optional int32 min_video_readahead_ms = 6;\r\n    optional PlaybackCookie playback_cookie = 7;\r\n    optional string video_id = 8;\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/onesie_header.proto",
    "content": "syntax = \"proto3\";\n\npackage sabr.videostreaming;\n\nimport \"sabr/videostreaming/crypto_params.proto\";\nimport \"sabr/videostreaming/onesie_header_type.proto\";\n\noption java_multiple_files = true;\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\noption optimize_for = LITE_RUNTIME;\n\nmessage OnesieHeader {\n  message UnknownMessage1 {\n    optional string video_id = 2;\n  }\n\n  message UnknownMessage2 {\n    repeated string itag_denylist = 1;\n  }\n\n  optional OnesieHeaderType type = 1;\n  optional string video_id = 2;\n  optional string itag = 3;\n  optional CryptoParams crypto_params = 4;\n  optional uint64 last_modified = 5;\n  optional int64 expected_media_size_bytes = 7;\n  repeated string restricted_formats = 11;\n  optional string xtags = 15;\n  optional int64 sequence_number = 18;\n  optional UnknownMessage1 field23 = 23;\n  optional UnknownMessage2 field34 = 34;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/onesie_header_type.proto",
    "content": "syntax = \"proto3\";\n\npackage sabr.videostreaming;\n\noption java_multiple_files = true;\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\noption optimize_for = LITE_RUNTIME;\n\nenum OnesieHeaderType {\n  ONESIE_PLAYER_RESPONSE = 0;\n  MEDIA = 1;\n  MEDIA_DECRYPTION_KEY = 2;\n  CLEAR_MEDIA = 3;\n  CLEAR_INIT_SEGMENT = 4;\n  ACK = 5;\n  MEDIA_STREAMER_HOSTNAME = 6;\n  MEDIA_SIZE_HINT = 7;\n  PLAYER_SERVICE_RESPONSE_PUSH_URL = 8;\n  LAST_HIGH_PRIORITY_HINT = 9;\n  STREAM_METADATA = 16;\n  ENCRYPTED_INNERTUBE_RESPONSE_PART = 25;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/onesie_innertube_request.proto",
    "content": "syntax = \"proto3\";\n\npackage sabr.videostreaming;\n\nimport \"sabr/misc/common.proto\";\n\noption java_multiple_files = true;\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\noption optimize_for = LITE_RUNTIME;\n\nmessage OnesieInnertubeRequest {\n  optional string url = 1;\n  repeated sabr.misc.HttpHeader headers = 2;\n  optional string body = 3;\n  optional bool proxied_by_trusted_bandaid = 4;\n  optional bool skip_response_encryption = 6;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/onesie_innertube_response.proto",
    "content": "syntax = \"proto3\";\n\npackage sabr.videostreaming;\n\nimport \"sabr/misc/common.proto\";\nimport \"sabr/videostreaming/onesie_proxy_status.proto\";\n\noption java_multiple_files = true;\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\noption optimize_for = LITE_RUNTIME;\n\nmessage OnesieInnertubeResponse {\n  optional OnesieProxyStatus onesie_proxy_status = 1;\n  optional int32 http_status = 2;\n  repeated sabr.misc.HttpHeader headers = 3;\n  optional bytes body = 4;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/onesie_proxy_status.proto",
    "content": "syntax = \"proto3\";\n\npackage sabr.videostreaming;\n\noption java_multiple_files = true;\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\noption optimize_for = LITE_RUNTIME;\n\nenum OnesieProxyStatus {\n  UNKNOWN = 0;\n  OK = 1;\n  DECRYPTION_FAILED = 2;\n  PARSING_FAILED = 3;\n  MISSING_X_FORWARDED_FOR = 4;\n  INVALID_X_FORWARDED_FOR = 5;\n  INVALID_CONTENT_TYPE = 6;\n  BACKEND_ERROR = 7;\n  CLIENT_ERROR = 8;\n  MISSING_CRYPTER = 9;\n  RESPONSE_JSON_SERIALIZATION_FAILED = 10;\n  DECOMPRESSION_FAILED = 11;\n  JSON_PARSING_FAILED = 12;\n  UNKNOWN_COMPRESSION_TYPE = 13;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/onesie_request.proto",
    "content": "syntax = \"proto3\";\n\npackage sabr.videostreaming;\n\nimport \"sabr/misc/common.proto\";\nimport \"sabr/videostreaming/buffered_range.proto\";\nimport \"sabr/videostreaming/client_abr_state.proto\";\nimport \"sabr/videostreaming/innertube_request.proto\";\nimport \"sabr/videostreaming/streamer_context.proto\";\nimport \"sabr/videostreaming/reload_player_response.proto\";\n\noption java_multiple_files = true;\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\noption optimize_for = LITE_RUNTIME;\n\nmessage OnesieRequest {\n  repeated string urls = 1;\n  optional ClientAbrState client_abr_state = 2;\n  optional InnertubeRequest innertube_request = 3;\n  optional bytes onesie_ustreamer_config = 4;\n  optional int32 max_vp9_height = 5;\n  optional int32 client_display_height = 6;\n  optional StreamerContext streamer_context = 10;\n  optional sabr.misc.OnesieRequestTarget request_target = 13; // MLOnesieRequestTarget\n  repeated BufferedRange buffered_ranges = 14;\n  optional ReloadPlaybackParams reload_playback_params = 15;\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/playback_cookie.proto",
    "content": "syntax = \"proto3\";\n\npackage sabr.videostreaming;\n\nimport \"sabr/misc/common.proto\";\n\noption java_multiple_files = true;\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\noption optimize_for = LITE_RUNTIME;\n\nmessage PlaybackCookie {\n  optional int32 resolution = 1; // Always 999999 when resolution is set manually, or if the auto selected one is the max available resolution.\n  optional int32 field2 = 2;\n  optional sabr.misc.FormatId video_fmt = 7;\n  optional sabr.misc.FormatId audio_fmt = 8;\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/reload_player_response.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage ReloadPlaybackParams {\r\n    optional string token = 1;\r\n}\r\n\r\nmessage ReloadPlayerResponse {\r\n    optional ReloadPlaybackParams reload_playback_params = 1;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/sabr_context_sending_policy.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage SabrContextSendingPolicy {\r\n   // Start sending the SabrContextUpdates of this type\r\n   repeated int32 start_policy = 1;\r\n\r\n   // Stop sending the SabrContextUpdates of this type\r\n   repeated int32 stop_policy = 2;\r\n\r\n   // Stop and discard the SabrContextUpdates of this type\r\n   repeated int32 discard_policy = 3;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/sabr_context_update.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage SabrContextUpdate {\r\n    enum SabrContextScope {\r\n      SABR_CONTEXT_SCOPE_UNKNOWN = 0;\r\n      SABR_CONTEXT_SCOPE_PLAYBACK = 1;\r\n      SABR_CONTEXT_SCOPE_REQUEST = 2;\r\n      SABR_CONTEXT_SCOPE_WATCH_ENDPOINT = 3;\r\n      SABR_CONTEXT_SCOPE_CONTENT_ADS = 4;\r\n    }\r\n\r\n    enum SabrContextWritePolicy {\r\n      SABR_CONTEXT_WRITE_POLICY_UNSPECIFIED = 0;\r\n      SABR_CONTEXT_WRITE_POLICY_OVERWRITE = 1;\r\n      SABR_CONTEXT_WRITE_POLICY_KEEP_EXISTING = 2;\r\n    }\r\n\r\n    optional int32 type = 1;\r\n    optional SabrContextScope scope = 2;\r\n    optional bytes value = 3;\r\n    optional bool send_by_default = 4;\r\n    optional SabrContextWritePolicy write_policy = 5;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/sabr_error.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage Error {\r\n    optional int32 status_code = 1;\r\n    optional int32 type = 4;\r\n}\r\n\r\nmessage SabrError {\r\n    optional string type = 1;\r\n    optional int32 action = 2;\r\n    optional Error error = 3;\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/sabr_redirect.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage SabrRedirect {\r\n  optional string redirect_url = 1;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/sabr_seek.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\nimport \"sabr/misc/common.proto\";\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage SabrSeek {\r\n    optional int64 seek_media_time = 1;\r\n    optional int32 seek_media_timescale = 2;\r\n    optional sabr.misc.SeekSource seek_source = 3;\r\n}\r\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/stream_protection_status.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage StreamProtectionStatus {\r\n  enum Status {\r\n    UNKNOWN = 0;\r\n    OK = 1;\r\n    ATTESTATION_PENDING = 2;\r\n    ATTESTATION_REQUIRED = 3;\r\n  }\r\n  optional Status status = 1;\r\n  optional int32 max_retries = 2;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/streamer_context.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\nimport \"sabr/videostreaming/playback_cookie.proto\";\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage StreamerContext {\r\n  message ClientInfo {\r\n    optional string hl = 1;\r\n    optional string gl = 2;\r\n    optional string remote_host = 4;\r\n\r\n    optional string device_make = 12;\r\n    optional string device_model = 13;\r\n    optional ClientName client_name = 16;\r\n    optional string client_version = 17;\r\n    optional string os_name = 18;\r\n    optional string os_version = 19;\r\n    optional string accept_language = 21;\r\n    optional string accept_region = 22;\r\n    optional int32 screen_width_points = 37;\r\n    optional int32 screen_height_points = 38;\r\n    optional float screen_width_inches = 39;\r\n    optional float screen_height_inches = 40;\r\n    optional int32 screen_pixel_density = 41;\r\n    optional ClientFormFactor client_form_factor = 46;\r\n    optional int32 gmscore_version_code = 50; // e.g. 243731017\r\n    optional int32 window_width_points = 55;\r\n    optional int32 window_height_points = 56;\r\n    optional int32 android_sdk_version = 64;\r\n    optional float screen_density_float = 65;\r\n    optional int64 utc_offset_minutes = 67;\r\n    optional string time_zone = 80;\r\n    optional string chipset = 92; // e.g. \"qcom;taro\"\r\n    optional GLDeviceInfo gl_device_info = 102;\r\n  }\r\n\r\n  enum ClientName {\r\n    UNKNOWN_INTERFACE = 0;\r\n    WEB = 1;\r\n    MWEB = 2;\r\n    ANDROID = 3;\r\n    IOS = 5;\r\n    TVHTML5 = 7;\r\n    TVLITE = 8;\r\n    TVANDROID = 10;\r\n    XBOX = 11;\r\n    CLIENTX = 12;\r\n    XBOXONEGUIDE = 13;\r\n    ANDROID_CREATOR = 14;\r\n    IOS_CREATOR = 15;\r\n    TVAPPLE = 16;\r\n    IOS_INSTANT = 17;\r\n    ANDROID_KIDS = 18;\r\n    IOS_KIDS = 19;\r\n    ANDROID_INSTANT = 20;\r\n    ANDROID_MUSIC = 21;\r\n    IOS_TABLOID = 22;\r\n    ANDROID_TV = 23;\r\n    ANDROID_GAMING = 24;\r\n    IOS_GAMING = 25;\r\n    IOS_MUSIC = 26;\r\n    MWEB_TIER_2 = 27;\r\n    ANDROID_VR = 28;\r\n    ANDROID_UNPLUGGED = 29;\r\n    ANDROID_TESTSUITE = 30;\r\n    WEB_MUSIC_ANALYTICS = 31;\r\n    WEB_GAMING = 32;\r\n    IOS_UNPLUGGED = 33;\r\n    ANDROID_WITNESS = 34;\r\n    IOS_WITNESS = 35;\r\n    ANDROID_SPORTS = 36;\r\n    IOS_SPORTS = 37;\r\n    ANDROID_LITE = 38;\r\n    IOS_EMBEDDED_PLAYER = 39;\r\n    IOS_DIRECTOR = 40;\r\n    WEB_UNPLUGGED = 41;\r\n    WEB_EXPERIMENTS = 42;\r\n    TVHTML5_CAST = 43;\r\n    WEB_EMBEDDED_PLAYER = 56;\r\n    TVHTML5_AUDIO = 57;\r\n    TV_UNPLUGGED_CAST = 58;\r\n    TVHTML5_KIDS = 59;\r\n    WEB_HEROES = 60;\r\n    WEB_MUSIC = 61;\r\n    WEB_CREATOR = 62;\r\n    TV_UNPLUGGED_ANDROID = 63;\r\n    IOS_LIVE_CREATION_EXTENSION = 64;\r\n    TVHTML5_UNPLUGGED = 65;\r\n    IOS_MESSAGES_EXTENSION = 66;\r\n    WEB_REMIX = 67;\r\n    IOS_UPTIME = 68;\r\n    WEB_UNPLUGGED_ONBOARDING = 69;\r\n    WEB_UNPLUGGED_OPS = 70;\r\n    WEB_UNPLUGGED_PUBLIC = 71;\r\n    TVHTML5_VR = 72;\r\n    WEB_LIVE_STREAMING = 73;\r\n    ANDROID_TV_KIDS = 74;\r\n    TVHTML5_SIMPLY = 75;\r\n    WEB_KIDS = 76;\r\n    MUSIC_INTEGRATIONS = 77;\r\n    TVHTML5_YONGLE = 80;\r\n    GOOGLE_ASSISTANT = 84;\r\n    TVHTML5_SIMPLY_EMBEDDED_PLAYER = 85;\r\n    WEB_MUSIC_EMBEDDED_PLAYER = 86;\r\n    WEB_INTERNAL_ANALYTICS = 87;\r\n    WEB_PARENT_TOOLS = 88;\r\n    GOOGLE_MEDIA_ACTIONS = 89;\r\n    WEB_PHONE_VERIFICATION = 90;\r\n    ANDROID_PRODUCER = 91;\r\n    IOS_PRODUCER = 92;\r\n    TVHTML5_FOR_KIDS = 93;\r\n    GOOGLE_LIST_RECS = 94;\r\n    MEDIA_CONNECT_FRONTEND = 95;\r\n    WEB_EFFECT_MAKER = 98;\r\n    WEB_SHOPPING_EXTENSION = 99;\r\n    WEB_PLAYABLES_PORTAL = 100;\r\n    VISIONOS = 101;\r\n    WEB_LIVE_APPS = 102;\r\n    WEB_MUSIC_INTEGRATIONS = 103;\r\n    ANDROID_MUSIC_AOSP = 104;\r\n  }\r\n\r\n  enum ClientFormFactor {\r\n    UNKNOWN_FORM_FACTOR = 0;\r\n    FORM_FACTOR_VAL1 = 1;\r\n    FORM_FACTOR_VAL2 = 2;\r\n  }\r\n\r\n  message GLDeviceInfo {\r\n    optional string gl_renderer = 1;\r\n    optional int32 gl_es_version_major = 2;\r\n    optional int32 gl_es_version_minor = 3;\r\n  }\r\n\r\n  message SabrContext {\r\n    optional int32 type = 1;\r\n    optional bytes value = 2;\r\n  }\r\n\r\n  message UnknownMessage1 {\r\n    message UnknownInnerMessage1 {\r\n      optional int32 code = 1;\r\n      optional string message = 2;\r\n    }\r\n\r\n    optional bytes field1 = 1;\r\n    optional UnknownInnerMessage1 field2 = 2;\r\n  }\r\n\r\n  optional ClientInfo client_info = 1;\r\n  optional bytes po_token = 2;\r\n  optional bytes playback_cookie = 3;\r\n  optional bytes field4 = 4;\r\n  repeated SabrContext sabr_contexts = 5;\r\n  repeated int32 unsent_sabr_contexts = 6;\r\n  optional string field7 = 7;\r\n  optional UnknownMessage1 field8 = 8;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/time_range.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage TimeRange {\r\n  optional int64 start_ticks = 1;\r\n  optional int64 duration_ticks = 2;\r\n  optional int32 timescale = 3;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/sabr/src/main/proto/sabr/videostreaming/video_playback_abr_request.proto",
    "content": "syntax = \"proto3\";\r\n\r\npackage sabr.videostreaming;\r\n\r\nimport \"sabr/videostreaming/client_abr_state.proto\";\r\nimport \"sabr/misc/common.proto\";\r\nimport \"sabr/videostreaming/buffered_range.proto\";\r\nimport \"sabr/videostreaming/streamer_context.proto\";\r\nimport \"sabr/videostreaming/time_range.proto\";\r\n\r\noption java_multiple_files = true;\r\noption java_package = \"com.google.android.exoplayer2.source.sabr.protos.videostreaming\";\r\noption optimize_for = LITE_RUNTIME;\r\n\r\nmessage VideoPlaybackAbrRequest {\r\n  optional ClientAbrState client_abr_state = 1;\r\n  repeated sabr.misc.FormatId selected_format_ids = 2;\r\n  repeated BufferedRange buffered_ranges = 3;\r\n  optional int64 player_time_ms = 4; // `osts` (Onesie Start Time Seconds) param on Onesie requests.\r\n  optional bytes video_playback_ustreamer_config = 5;\r\n  optional UnknownMessage1 field6 = 6;\r\n  repeated sabr.misc.FormatId preferred_audio_format_ids = 16; // `pai` (Preferred Audio Itags) param on Onesie requests.\r\n  repeated sabr.misc.FormatId preferred_video_format_ids = 17; // `pvi` (Preferred Video Itags) param on Onesie requests.\r\n  repeated sabr.misc.FormatId preferred_subtitle_format_ids = 18;\r\n  optional StreamerContext streamer_context = 19;\r\n  optional UnknownMessage2 field21 = 21;\r\n  optional int32 field22 = 22;\r\n  optional int32 field23 = 23;\r\n  repeated UnknownMessage3 field1000 = 1000;\r\n}\r\n\r\nmessage UnknownMessage1 {\r\n  optional sabr.misc.FormatId format_id = 1;\r\n  optional sint64 lmt = 2;\r\n  optional int32 sequence_number = 3;\r\n  optional TimeRange time_range = 4;\r\n  optional int32 field5 = 5;\r\n}\r\n\r\nmessage UnknownMessage2 {\r\n  repeated string field1 = 1;\r\n  optional bytes field2 = 2;\r\n  optional string field3 = 3;\r\n  optional int32 field4 = 4;\r\n  optional int32 field5 = 5;\r\n  optional string field6 = 6;\r\n}\r\n\r\nmessage UnknownMessage3 {\r\n  repeated sabr.misc.FormatId format_ids = 1;\r\n  repeated BufferedRange ud = 2;\r\n  optional string clip_id = 3;\r\n}"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/smoothstreaming/README.md",
    "content": "# ExoPlayer SmoothStreaming library module #\n\nProvides support for Smooth Streaming content. To play Smooth Streaming content,\ninstantiate a `SsMediaSource` and pass it to `ExoPlayer.prepare`.\n\n## Links ##\n\n* [Developer Guide][].\n* [Javadoc][]: Classes matching\n  `com.google.android.exoplayer2.source.smoothstreaming.*` belong to this\n  module.\n\n[Developer Guide]: https://exoplayer.dev/smoothstreaming.html\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/smoothstreaming/build.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.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    buildTypes {\n        // Re-enable test coverage when the following issue is fixed:\n        // https://issuetracker.google.com/issues/37019591\n        // debug {\n        //    testCoverageEnabled = true\n        // }\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    implementation project(modulePrefix + 'library-core')\n    compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion\n    implementation 'androidx.annotation:annotation:1.1.0'\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n}\n\next {\n    javadocTitle = 'SmoothStreaming module'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'exoplayer-smoothstreaming'\n    releaseDescription = 'The ExoPlayer library SmoothStreaming module.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/smoothstreaming/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n\n<manifest package=\"com.google.android.exoplayer2.source.smoothstreaming\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/smoothstreaming/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-amzn-2.10.6/library/smoothstreaming/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-amzn-2.10.6/library/smoothstreaming/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.SeekParameters;\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;\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  private final @Nullable TransferListener transferListener;\n  private final LoaderErrorThrower manifestLoaderErrorThrower;\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  private @Nullable 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      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.loadErrorHandlingPolicy = loadErrorHandlingPolicy;\n    this.eventDispatcher = eventDispatcher;\n    this.allocator = allocator;\n    this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;\n    trackGroups = buildTrackGroups(manifest);\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(TrackSelection[] selections, boolean[] mayRetainStreamFlags,\n      SampleStream[] streams, boolean[] streamResetFlags, 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 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        loadErrorHandlingPolicy,\n        eventDispatcher);\n  }\n\n  private static TrackGroupArray buildTrackGroups(SsManifest manifest) {\n    TrackGroup[] trackGroups = new TrackGroup[manifest.streamElements.length];\n    for (int i = 0; i < manifest.streamElements.length; i++) {\n      trackGroups[i] = new TrackGroup(manifest.streamElements[i].formats);\n    }\n    return new TrackGroupArray(trackGroups);\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private static ChunkSampleStream<SsChunkSource>[] newSampleStreamArray(int length) {\n    return new ChunkSampleStream[length];\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/smoothstreaming/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.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.SequenceableLoader;\nimport com.google.android.exoplayer2.source.SinglePeriodTimeline;\nimport com.google.android.exoplayer2.source.ads.AdsMediaSource;\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 AdsMediaSource.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 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      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(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 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 a list of {@link StreamKey stream keys} by which the manifest is filtered.\n     *\n     * @param streamKeys A list of {@link StreamKey stream keys}.\n     * @return This factory, for convenience.\n     * @throws IllegalStateException If one of the {@code create} methods has already been called.\n     */\n    public Factory setStreamKeys(List<StreamKey> streamKeys) {\n      Assertions.checkState(!isCreateCalled);\n      this.streamKeys = streamKeys;\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          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     * 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          loadErrorHandlingPolicy,\n          livePresentationDelayMs,\n          tag);\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    @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 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  private final @Nullable Object tag;\n\n  private DataSource manifestDataSource;\n  private Loader manifestLoader;\n  private LoaderErrorThrower manifestLoaderErrorThrower;\n  private @Nullable 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      Handler eventHandler,\n      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      Handler eventHandler,\n      MediaSourceEventListener eventListener) {\n    this(\n        manifest,\n        /* manifestUri= */ null,\n        /* manifestDataSourceFactory= */ null,\n        /* manifestParser= */ null,\n        chunkSourceFactory,\n        new DefaultCompositeSequenceableLoaderFactory(),\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      Handler eventHandler,\n      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      Handler eventHandler,\n      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      Handler eventHandler,\n      MediaSourceEventListener eventListener) {\n    this(\n        /* manifest= */ null,\n        manifestUri,\n        manifestDataSourceFactory,\n        manifestParser,\n        chunkSourceFactory,\n        new DefaultCompositeSequenceableLoaderFactory(),\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      SsManifest manifest,\n      Uri manifestUri,\n      DataSource.Factory manifestDataSourceFactory,\n      ParsingLoadable.Parser<? extends SsManifest> manifestParser,\n      SsChunkSource.Factory chunkSourceFactory,\n      CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,\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.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  public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    this.mediaTransferListener = mediaTransferListener;\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            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  public 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  }\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              manifest.isLive,\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              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              tag);\n    }\n    refreshSourceInfo(timeline, manifest);\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-amzn-2.10.6/library/smoothstreaming/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 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    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(String baseUri, String chunkTemplate, int type, String subType,\n        long timescale, String name, int maxWidth, int maxHeight, int displayWidth,\n        int displayHeight, String language, Format[] formats, 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(String baseUri, String chunkTemplate, int type, String subType,\n        long timescale, String name, int maxWidth, int maxHeight, int displayWidth,\n        int displayHeight, String language, Format[] formats, List<Long> chunkStartTimes,\n        long[] chunkStartTimesUs, 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  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      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      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-amzn-2.10.6/library/smoothstreaming/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 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.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    private final ElementParser parent;\n    private final List<Pair<String, Object>> normalizedAttributes;\n\n    public ElementParser(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     * <p>\n     * 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, 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\n     * the 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    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    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-amzn-2.10.6/library/smoothstreaming/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-amzn-2.10.6/library/smoothstreaming/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-amzn-2.10.6/library/smoothstreaming/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n\n<manifest package=\"com.google.android.exoplayer2.source.smoothstreaming.test\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/smoothstreaming/src/test/assets/sample_ismc_1",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SmoothStreamingMedia MajorVersion=\"2\" MinorVersion=\"0\"\n    Duration=\"2300000000\" TimeScale=\"10000000\">\n    <Protection>\n        <ProtectionHeader SystemID=\"9A04F079-9840-4286-AB92-E65BE0885F95\">\n            fgMAAAEAAQB0AzwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQgBhAFUATQBPAEcAYwBzAGgAVQBDAEQAZAB3ADMANABZAGMAawBmAFoAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBnADcATgBhAFIARABJAEkATwA5ADAAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAcwA6AC8ALwBUAC0ATwBOAEwASQBOAEUALgBEAFUATQBNAFkALQBTAEUAUgBWAEUAUgAvAEEAcgB0AGUAbQBpAHMATABpAGMAZQBuAHMAZQBTAGUAcgB2AGUAcgAvAFAAbABhAHkAUgBlAGEAZAB5AE0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwAQwBJAEQAPgAxADcANQA4ADIANgA8AC8AQwBJAEQAPgA8AEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4ANwAuADEALgAxADUANgA1AC4ANAA8AC8ASQBJAFMAXwBEAFIATQBfAFYARQBSAFMASQBPAE4APgA8AC8AQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A\n        </ProtectionHeader>\n    </Protection>\n\n    <StreamIndex\n        Type = \"video\"\n        Chunks = \"115\"\n        QualityLevels = \"2\"\n        MaxWidth = \"720\"\n        MaxHeight = \"480\"\n        TimeScale=\"10000000\"\n        Url =\n            \"QualityLevels({bitrate},{CustomAttributes})/Fragments(video={start_time})\"\n        Name = \"video\"\n        >\n        <QualityLevel Index=\"0\" Bitrate=\"1536000\" FourCC=\"WVC1\"\n            MaxWidth=\"720\" MaxHeight=\"480\"\n            CodecPrivateData = \"270000010FCBEE1670EF8A16783BF180C9089CC4AFA11C0000010E1207F840\"\n            >\n            <CustomAttributes>\n                <Attribute Name = \"Compatibility\" Value = \"Desktop\" />\n            </CustomAttributes>\n        </QualityLevel>\n\n\n        <QualityLevel Index=\"5\" Bitrate=\"307200\" FourCC=\"WVC1\"\n            MaxWidth=\"720\" MaxHeight=\"480\"\n            CodecPrivateData = \"270000010FCBEE1670EF8A16783BF180C9089CC4AFA11C0000010E1207F840\">\n            <CustomAttributes>\n                <Attribute Name = \"Compatibility\" Value = \"Handheld\" />\n            </CustomAttributes>\n        </QualityLevel>\n\n\n        <c t = \"0\" d = \"19680000\" />\n        <c n = \"1\" t = \"19680000\" d=\"8980000\" />\n\n    </StreamIndex>\n</SmoothStreamingMedia>\n\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/smoothstreaming/src/test/assets/sample_ismc_2",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SmoothStreamingMedia MajorVersion=\"2\" MinorVersion=\"0\"\n    Duration=\"2300000000\" TimeScale=\"10000000\">\n    <Protection>\n        <ProtectionHeader SystemID=\"{9A04F079-9840-4286-AB92-E65BE0885F95}\">\n            fgMAAAEAAQB0AzwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQgBhAFUATQBPAEcAYwBzAGgAVQBDAEQAZAB3ADMANABZAGMAawBmAFoAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBnADcATgBhAFIARABJAEkATwA5ADAAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAcwA6AC8ALwBUAC0ATwBOAEwASQBOAEUALgBEAFUATQBNAFkALQBTAEUAUgBWAEUAUgAvAEEAcgB0AGUAbQBpAHMATABpAGMAZQBuAHMAZQBTAGUAcgB2AGUAcgAvAFAAbABhAHkAUgBlAGEAZAB5AE0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwAQwBJAEQAPgAxADcANQA4ADIANgA8AC8AQwBJAEQAPgA8AEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4ANwAuADEALgAxADUANgA1AC4ANAA8AC8ASQBJAFMAXwBEAFIATQBfAFYARQBSAFMASQBPAE4APgA8AC8AQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A\n        </ProtectionHeader>\n    </Protection>\n\n    <StreamIndex\n        Type = \"video\"\n        Chunks = \"115\"\n        QualityLevels = \"2\"\n        MaxWidth = \"720\"\n        MaxHeight = \"480\"\n        TimeScale=\"10000000\"\n        Url =\n            \"QualityLevels({bitrate},{CustomAttributes})/Fragments(video={start_time})\"\n        Name = \"video\"\n        >\n        <QualityLevel Index=\"0\" Bitrate=\"1536000\" FourCC=\"WVC1\"\n            MaxWidth=\"720\" MaxHeight=\"480\"\n            CodecPrivateData = \"270000010FCBEE1670EF8A16783BF180C9089CC4AFA11C0000010E1207F840\"\n            >\n            <CustomAttributes>\n                <Attribute Name = \"Compatibility\" Value = \"Desktop\" />\n            </CustomAttributes>\n        </QualityLevel>\n\n\n        <QualityLevel Index=\"5\" Bitrate=\"307200\" FourCC=\"WVC1\"\n            MaxWidth=\"720\" MaxHeight=\"480\"\n            CodecPrivateData = \"270000010FCBEE1670EF8A16783BF180C9089CC4AFA11C0000010E1207F840\">\n            <CustomAttributes>\n                <Attribute Name = \"Compatibility\" Value = \"Handheld\" />\n            </CustomAttributes>\n        </QualityLevel>\n\n\n        <c t = \"0\" d = \"19680000\" />\n        <c n = \"1\" t = \"19680000\" d=\"8980000\" />\n\n    </StreamIndex>\n</SmoothStreamingMedia>\n\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.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;\n\nimport static com.google.android.exoplayer2.source.smoothstreaming.SsTestUtils.createSsManifest;\nimport static com.google.android.exoplayer2.source.smoothstreaming.SsTestUtils.createStreamElement;\nimport static org.mockito.Mockito.mock;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;\nimport com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;\nimport com.google.android.exoplayer2.testutil.MediaPeriodAsserts;\nimport com.google.android.exoplayer2.testutil.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory;\nimport com.google.android.exoplayer2.testutil.RobolectricUtil;\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 org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/** Unit tests for {@link SsMediaPeriod}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic class SsMediaPeriodTest {\n\n  @Test\n  public void getSteamKeys_isCompatibleWithSsManifestFilter() {\n    SsManifest testManifest =\n        createSsManifest(\n            createStreamElement(\n                /* name= */ \"video\",\n                C.TRACK_TYPE_VIDEO,\n                createVideoFormat(/* bitrate= */ 200000),\n                createVideoFormat(/* bitrate= */ 400000),\n                createVideoFormat(/* bitrate= */ 800000)),\n            createStreamElement(\n                /* name= */ \"audio\",\n                C.TRACK_TYPE_AUDIO,\n                createAudioFormat(/* bitrate= */ 48000),\n                createAudioFormat(/* bitrate= */ 96000)),\n            createStreamElement(\n                /* name= */ \"text\", C.TRACK_TYPE_TEXT, createTextFormat(/* language= */ \"eng\")));\n    FilterableManifestMediaPeriodFactory<SsManifest> mediaPeriodFactory =\n        (manifest, periodIndex) ->\n            new SsMediaPeriod(\n                manifest,\n                mock(SsChunkSource.Factory.class),\n                mock(TransferListener.class),\n                mock(CompositeSequenceableLoaderFactory.class),\n                mock(LoadErrorHandlingPolicy.class),\n                new EventDispatcher()\n                    .withParameters(\n                        /* windowIndex= */ 0,\n                        /* mediaPeriodId= */ new MediaPeriodId(/* periodUid= */ new Object()),\n                        /* mediaTimeOffsetMs= */ 0),\n                mock(LoaderErrorThrower.class),\n                mock(Allocator.class));\n\n    MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(\n        mediaPeriodFactory, testManifest);\n  }\n\n  private static Format createVideoFormat(int bitrate) {\n    return Format.createContainerFormat(\n        /* id= */ null,\n        /* label= */ null,\n        MimeTypes.VIDEO_MP4,\n        MimeTypes.VIDEO_H264,\n        /* codecs= */ null,\n        bitrate,\n        /* selectionFlags= */ 0,\n        /* roleFlags= */ 0,\n        /* language= */ null);\n  }\n\n  private static Format createAudioFormat(int bitrate) {\n    return Format.createContainerFormat(\n        /* id= */ null,\n        /* label= */ null,\n        MimeTypes.AUDIO_MP4,\n        MimeTypes.AUDIO_AAC,\n        /* codecs= */ null,\n        bitrate,\n        /* selectionFlags= */ 0,\n        /* roleFlags= */ 0,\n        /* language= */ null);\n  }\n\n  private static Format createTextFormat(String language) {\n    return Format.createContainerFormat(\n        /* id= */ null,\n        /* label= */ null,\n        MimeTypes.APPLICATION_MP4,\n        MimeTypes.TEXT_VTT,\n        /* codecs= */ null,\n        /* bitrate= */ Format.NO_VALUE,\n        /* selectionFlags= */ 0,\n        /* roleFlags= */ 0,\n        language);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsTestUtils.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;\n\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.source.smoothstreaming.manifest.SsManifest;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;\nimport java.util.Collections;\n\n/** Util methods for SmoothStreaming tests. */\npublic class SsTestUtils {\n\n  private static final int TEST_MAJOR_VERSION = 1;\n  private static final int TEST_MINOR_VERSION = 2;\n  private static final int TEST_TIMESCALE = 1000;\n  private static final int TEST_DURATION = 5000;\n  private static final int TEST_DVR_WINDOW_LENGTH = 0;\n  private static final int TEST_LOOKAHEAD_COUNT = 0;\n  private static final boolean TEST_IS_LIVE = false;\n  private static final String TEST_BASE_URI = \"baseUri\";\n  private static final String TEST_CHUNK_TEMPLATE = \"chunkTemplate\";\n  private static final String TEST_SUB_TYPE = \"subType\";\n  private static final int TEST_MAX_WIDTH = 1024;\n  private static final int TEST_MAX_HEIGHT = 768;\n  private static final String TEST_LANGUAGE = \"eng\";\n  private static final ProtectionElement TEST_PROTECTION_ELEMENT =\n      new ProtectionElement(C.WIDEVINE_UUID, new byte[0], new TrackEncryptionBox[0]);\n\n  private SsTestUtils() {}\n\n  /** Creates test manifest with the given stream elements. */\n  public static SsManifest createSsManifest(StreamElement... streamElements) {\n    return new SsManifest(\n        TEST_MAJOR_VERSION,\n        TEST_MINOR_VERSION,\n        TEST_TIMESCALE,\n        TEST_DURATION,\n        TEST_DVR_WINDOW_LENGTH,\n        TEST_LOOKAHEAD_COUNT,\n        TEST_IS_LIVE,\n        TEST_PROTECTION_ELEMENT,\n        streamElements);\n  }\n\n  /** Creates test video stream element with the given name, track type and formats. */\n  public static StreamElement createStreamElement(String name, int trackType, Format... formats) {\n    return new StreamElement(\n        TEST_BASE_URI,\n        TEST_CHUNK_TEMPLATE,\n        trackType,\n        TEST_SUB_TYPE,\n        TEST_TIMESCALE,\n        name,\n        TEST_MAX_WIDTH,\n        TEST_MAX_HEIGHT,\n        TEST_MAX_WIDTH,\n        TEST_MAX_HEIGHT,\n        TEST_LANGUAGE,\n        formats,\n        /* chunkStartTimes= */ Collections.emptyList(),\n        /* lastChunkDuration= */ 0);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.TestUtil;\nimport java.io.IOException;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link SsManifestParser}. */\n@RunWith(AndroidJUnit4.class)\npublic final class SsManifestParserTest {\n\n  private static final String SAMPLE_ISMC_1 = \"sample_ismc_1\";\n  private static final String SAMPLE_ISMC_2 = \"sample_ismc_2\";\n\n  /** Simple test to ensure the sample manifests parse without any exceptions being thrown. */\n  @Test\n  public void testParseSmoothStreamingManifest() throws IOException {\n    SsManifestParser parser = new SsManifestParser();\n    parser.parse(\n        Uri.parse(\"https://example.com/test.ismc\"),\n        TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_ISMC_1));\n    parser.parse(\n        Uri.parse(\"https://example.com/test.ismc\"),\n        TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_ISMC_2));\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.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.manifest;\n\nimport static com.google.android.exoplayer2.source.smoothstreaming.SsTestUtils.createSsManifest;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.smoothstreaming.SsTestUtils;\nimport com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Random;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit tests for {@link SsManifest}. */\n@RunWith(AndroidJUnit4.class)\npublic class SsManifestTest {\n\n  @Test\n  public void testCopy() throws Exception {\n    Format[][] formats = newFormats(2, 3);\n    SsManifest sourceManifest =\n        createSsManifest(\n            createStreamElement(\"1\", formats[0]), createStreamElement(\"2\", formats[1]));\n\n    List<StreamKey> keys =\n        Arrays.asList(new StreamKey(0, 0), new StreamKey(0, 2), new StreamKey(1, 0));\n    // Keys don't need to be in any particular order\n    Collections.shuffle(keys, new Random(0));\n\n    SsManifest copyManifest = sourceManifest.copy(keys);\n\n    SsManifest expectedManifest =\n        createSsManifest(\n            createStreamElement(\"1\", formats[0][0], formats[0][2]),\n            createStreamElement(\"2\", formats[1][0]));\n    assertManifestEquals(expectedManifest, copyManifest);\n  }\n\n  @Test\n  public void testCopyRemoveStreamElement() throws Exception {\n    Format[][] formats = newFormats(2, 3);\n    SsManifest sourceManifest =\n        createSsManifest(\n            createStreamElement(\"1\", formats[0]), createStreamElement(\"2\", formats[1]));\n\n    List<StreamKey> keys = Collections.singletonList(new StreamKey(1, 0));\n\n    SsManifest copyManifest = sourceManifest.copy(keys);\n\n    SsManifest expectedManifest = createSsManifest(createStreamElement(\"2\", formats[1][0]));\n    assertManifestEquals(expectedManifest, copyManifest);\n  }\n\n  private static void assertManifestEquals(SsManifest expected, SsManifest actual) {\n    assertThat(actual.durationUs).isEqualTo(expected.durationUs);\n    assertThat(actual.dvrWindowLengthUs).isEqualTo(expected.dvrWindowLengthUs);\n    assertThat(actual.isLive).isEqualTo(expected.isLive);\n    assertThat(actual.lookAheadCount).isEqualTo(expected.lookAheadCount);\n    assertThat(actual.majorVersion).isEqualTo(expected.majorVersion);\n    assertThat(actual.minorVersion).isEqualTo(expected.minorVersion);\n    assertThat(actual.protectionElement.uuid).isEqualTo(expected.protectionElement.uuid);\n    assertThat(actual.protectionElement).isEqualTo(expected.protectionElement);\n    for (int i = 0; i < expected.streamElements.length; i++) {\n      StreamElement expectedStreamElement = expected.streamElements[i];\n      StreamElement actualStreamElement = actual.streamElements[i];\n      assertThat(actualStreamElement.chunkCount).isEqualTo(expectedStreamElement.chunkCount);\n      assertThat(actualStreamElement.displayHeight).isEqualTo(expectedStreamElement.displayHeight);\n      assertThat(actualStreamElement.displayWidth).isEqualTo(expectedStreamElement.displayWidth);\n      assertThat(actualStreamElement.language).isEqualTo(expectedStreamElement.language);\n      assertThat(actualStreamElement.maxHeight).isEqualTo(expectedStreamElement.maxHeight);\n      assertThat(actualStreamElement.maxWidth).isEqualTo(expectedStreamElement.maxWidth);\n      assertThat(actualStreamElement.name).isEqualTo(expectedStreamElement.name);\n      assertThat(actualStreamElement.subType).isEqualTo(expectedStreamElement.subType);\n      assertThat(actualStreamElement.timescale).isEqualTo(expectedStreamElement.timescale);\n      assertThat(actualStreamElement.type).isEqualTo(expectedStreamElement.type);\n      assertThat(actualStreamElement.formats).isEqualTo(expectedStreamElement.formats);\n    }\n  }\n\n  private static Format[][] newFormats(int streamElementCount, int trackCounts) {\n    Format[][] formats = new Format[streamElementCount][];\n    for (int i = 0; i < streamElementCount; i++) {\n      formats[i] = new Format[trackCounts];\n      for (int j = 0; j < trackCounts; j++) {\n        formats[i][j] = newFormat(i + \".\" + j);\n      }\n    }\n    return formats;\n  }\n\n  private static StreamElement createStreamElement(String name, Format... formats) {\n    return SsTestUtils.createStreamElement(name, C.TRACK_TYPE_VIDEO, formats);\n  }\n\n  private static Format newFormat(String id) {\n    return Format.createContainerFormat(\n        id,\n        /* label= */ null,\n        MimeTypes.VIDEO_MP4,\n        MimeTypes.VIDEO_H264,\n        /* codecs= */ null,\n        /* bitrate= */ Format.NO_VALUE,\n        /* selectionFlags= */ 0,\n        /* roleFlags= */ 0,\n        /* language= */ null);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.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.offline;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.offline.DownloadHelper;\nimport com.google.android.exoplayer2.testutil.FakeDataSource;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test to verify creation of a SmoothStreaming {@link DownloadHelper}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DownloadHelperTest {\n\n  @Test\n  public void staticDownloadHelperForSmoothStreaming_doesNotThrow() {\n    DownloadHelper.forSmoothStreaming(\n        Uri.parse(\"http://uri\"),\n        new FakeDataSource.Factory(),\n        (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]);\n    DownloadHelper.forSmoothStreaming(\n        Uri.parse(\"http://uri\"),\n        new FakeDataSource.Factory(),\n        (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0],\n        /* drmSessionManager= */ null,\n        DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloaderTest.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.offline;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.offline.DefaultDownloaderFactory;\nimport com.google.android.exoplayer2.offline.DownloadRequest;\nimport com.google.android.exoplayer2.offline.Downloader;\nimport com.google.android.exoplayer2.offline.DownloaderConstructorHelper;\nimport com.google.android.exoplayer2.offline.DownloaderFactory;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.upstream.DummyDataSource;\nimport com.google.android.exoplayer2.upstream.cache.Cache;\nimport java.util.Collections;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mockito;\n\n/** Unit tests for {@link SsDownloader}. */\n@RunWith(AndroidJUnit4.class)\npublic final class SsDownloaderTest {\n\n  @Test\n  public void createWithDefaultDownloaderFactory() throws Exception {\n    DownloaderConstructorHelper constructorHelper =\n        new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);\n    DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);\n\n    Downloader downloader =\n        factory.createDownloader(\n            new DownloadRequest(\n                \"id\",\n                DownloadRequest.TYPE_SS,\n                Uri.parse(\"https://www.test.com/download\"),\n                Collections.singletonList(new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0)),\n                /* customCacheKey= */ null,\n                /* data= */ null));\n    assertThat(downloader).isInstanceOf(SsDownloader.class);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/README.md",
    "content": "# ExoPlayer UI library module #\n\nProvides UI components and resources for use with ExoPlayer.\n\n## Links ##\n\n* [Developer Guide][].\n* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ui.*`\n  belong to this module.\n\n[Developer Guide]: https://exoplayer.dev/ui-components.html\n[Javadoc]: https://exoplayer.dev/doc/reference/index.html\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/build.gradle",
    "content": "// Copyright (C) 2016 The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\napply from: '../../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    buildTypes {\n        // Re-enable test coverage when the following issue is fixed:\n        // https://issuetracker.google.com/issues/37019591\n        // debug {\n        //    testCoverageEnabled = true\n        // }\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    // MODIFIED: add subs bg padding\n    implementation project(':sharedutils')\n    implementation project(modulePrefix + 'library-core')\n    implementation 'androidx.media:media:1.0.1'\n    implementation 'androidx.annotation:annotation:1.1.0'\n    compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n}\n\next {\n    javadocTitle = 'UI module'\n}\napply from: '../../javadoc_library.gradle'\n\next {\n    releaseArtifact = 'exoplayer-ui'\n    releaseDescription = 'The ExoPlayer library UI module.'\n}\napply from: '../../publish.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest package=\"com.google.android.exoplayer2.ui\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ui;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport android.util.AttributeSet;\nimport android.widget.FrameLayout;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * A {@link FrameLayout} that resizes itself to match a specified aspect ratio.\n */\npublic final class AspectRatioFrameLayout extends FrameLayout {\n\n  /** Listener to be notified about changes of the aspect ratios of this view. */\n  public interface AspectRatioListener {\n\n    /**\n     * Called when either the target aspect ratio or the view aspect ratio is updated.\n     *\n     * @param targetAspectRatio The aspect ratio that has been set in {@link #setAspectRatio(float)}\n     * @param naturalAspectRatio The natural aspect ratio of this view (before its width and height\n     *     are modified to satisfy the target aspect ratio).\n     * @param aspectRatioMismatch Whether the target and natural aspect ratios differ enough for\n     *     changing the resize mode to have an effect.\n     */\n    void onAspectRatioUpdated(\n        float targetAspectRatio, float naturalAspectRatio, boolean aspectRatioMismatch);\n  }\n\n  // LINT.IfChange\n  /**\n   * Resize modes for {@link AspectRatioFrameLayout}. One of {@link #RESIZE_MODE_FIT}, {@link\n   * #RESIZE_MODE_FIXED_WIDTH}, {@link #RESIZE_MODE_FIXED_HEIGHT}, {@link #RESIZE_MODE_FILL} or\n   * {@link #RESIZE_MODE_ZOOM}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    RESIZE_MODE_FIT,\n    RESIZE_MODE_FIXED_WIDTH,\n    RESIZE_MODE_FIXED_HEIGHT,\n    RESIZE_MODE_FILL,\n    RESIZE_MODE_ZOOM\n  })\n  public @interface ResizeMode {}\n\n  /**\n   * Either the width or height is decreased to obtain the desired aspect ratio.\n   */\n  public static final int RESIZE_MODE_FIT = 0;\n  /**\n   * The width is fixed and the height is increased or decreased to obtain the desired aspect ratio.\n   */\n  public static final int RESIZE_MODE_FIXED_WIDTH = 1;\n  /**\n   * The height is fixed and the width is increased or decreased to obtain the desired aspect ratio.\n   */\n  public static final int RESIZE_MODE_FIXED_HEIGHT = 2;\n  /**\n   * The specified aspect ratio is ignored.\n   */\n  public static final int RESIZE_MODE_FILL = 3;\n  /**\n   * Either the width or height is increased to obtain the desired aspect ratio.\n   */\n  public static final int RESIZE_MODE_ZOOM = 4;\n  // LINT.ThenChange(../../../../../../res/values/attrs.xml)\n\n  /**\n   * The {@link FrameLayout} will not resize itself if the fractional difference between its natural\n   * aspect ratio and the requested aspect ratio falls below this threshold.\n   *\n   * <p>This tolerance allows the view to occupy the whole of the screen when the requested aspect\n   * ratio is very close, but not exactly equal to, the aspect ratio of the screen. This may reduce\n   * the number of view layers that need to be composited by the underlying system, which can help\n   * to reduce power consumption.\n   */\n  private static final float MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f;\n\n  private final AspectRatioUpdateDispatcher aspectRatioUpdateDispatcher;\n\n  @Nullable private AspectRatioListener aspectRatioListener;\n\n  private float videoAspectRatio;\n  @ResizeMode private int resizeMode;\n  private int zoomPercents;\n\n  public AspectRatioFrameLayout(Context context) {\n    this(context, /* attrs= */ null);\n  }\n\n  public AspectRatioFrameLayout(Context context, @Nullable AttributeSet attrs) {\n    super(context, attrs);\n    resizeMode = RESIZE_MODE_FIT;\n    zoomPercents = -1;\n    if (attrs != null) {\n      TypedArray a = context.getTheme().obtainStyledAttributes(attrs,\n          R.styleable.AspectRatioFrameLayout, 0, 0);\n      try {\n        resizeMode = a.getInt(R.styleable.AspectRatioFrameLayout_resize_mode, RESIZE_MODE_FIT);\n      } finally {\n        a.recycle();\n      }\n    }\n    aspectRatioUpdateDispatcher = new AspectRatioUpdateDispatcher();\n  }\n\n  /**\n   * Sets the aspect ratio that this view should satisfy.\n   *\n   * @param widthHeightRatio The width to height ratio.\n   */\n  public void setAspectRatio(float widthHeightRatio) {\n    if (this.videoAspectRatio != widthHeightRatio) {\n      this.videoAspectRatio = widthHeightRatio;\n      requestLayout();\n    }\n  }\n\n  /**\n   * Sets the {@link AspectRatioListener}.\n   *\n   * @param listener The listener to be notified about aspect ratios changes, or null to clear a\n   *     listener that was previously set.\n   */\n  public void setAspectRatioListener(@Nullable AspectRatioListener listener) {\n    this.aspectRatioListener = listener;\n  }\n\n  /** Returns the {@link ResizeMode}. */\n  public @ResizeMode int getResizeMode() {\n    return resizeMode;\n  }\n\n  /**\n   * Sets the {@link ResizeMode}\n   *\n   * @param resizeMode The {@link ResizeMode}.\n   */\n  public void setResizeMode(@ResizeMode int resizeMode) {\n    if (this.resizeMode != resizeMode) {\n      this.resizeMode = resizeMode;\n      requestLayout();\n    }\n  }\n\n  /**\n   * MODIFIED: Set video zoom in percents\n   */\n  public void setZoom(int percents) {\n    if (zoomPercents != percents) {\n      zoomPercents = percents;\n      requestLayout();\n    }\n  }\n\n  @Override\n  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n    super.onMeasure(widthMeasureSpec, heightMeasureSpec); // Zoom 100%\n    if (videoAspectRatio <= 0) {\n      // Aspect ratio not set.\n      return;\n    }\n\n    int width = getMeasuredWidth();\n    int height = getMeasuredHeight();\n    float viewAspectRatio = (float) width / height;\n    float aspectDeformation = videoAspectRatio / viewAspectRatio - 1;\n    if (Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION) {\n      // We're within the allowed tolerance.\n      aspectRatioUpdateDispatcher.scheduleUpdate(videoAspectRatio, viewAspectRatio, false);\n\n      // MODIFIED\n      if (zoomPercents > 0 && zoomPercents != 100) {\n        width = width * zoomPercents / 100;\n        height = height * zoomPercents / 100;\n\n        super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),\n                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));\n      }\n\n      return;\n    }\n\n    // MOD: don't stretch vertical videos\n    int tempResizeMode = resizeMode == RESIZE_MODE_FILL && videoAspectRatio > 0 && videoAspectRatio < 1 ? RESIZE_MODE_FIT : resizeMode;\n\n    //switch (resizeMode) {\n    switch (tempResizeMode) {\n      case RESIZE_MODE_FIXED_WIDTH:\n        height = (int) (width / videoAspectRatio);\n        break;\n      case RESIZE_MODE_FIXED_HEIGHT:\n        width = (int) (height * videoAspectRatio);\n        break;\n      case RESIZE_MODE_ZOOM:\n        if (aspectDeformation > 0) {\n          width = (int) (height * videoAspectRatio);\n        } else {\n          height = (int) (width / videoAspectRatio);\n        }\n        break;\n      case RESIZE_MODE_FIT:\n        if (aspectDeformation > 0) {\n          height = (int) (width / videoAspectRatio);\n        } else {\n          width = (int) (height * videoAspectRatio);\n        }\n        break;\n      case RESIZE_MODE_FILL:\n      default:\n        // Ignore target aspect ratio\n        break;\n    }\n\n    // MODIFIED\n    if (zoomPercents > 0 && zoomPercents != 100) {\n      width = (width / 100) * zoomPercents;\n      height = (height / 100) * zoomPercents;\n    }\n\n    aspectRatioUpdateDispatcher.scheduleUpdate(videoAspectRatio, viewAspectRatio, true);\n    super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),\n        MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));\n  }\n\n  /** Dispatches updates to {@link AspectRatioListener}. */\n  private final class AspectRatioUpdateDispatcher implements Runnable {\n\n    private float targetAspectRatio;\n    private float naturalAspectRatio;\n    private boolean aspectRatioMismatch;\n    private boolean isScheduled;\n\n    public void scheduleUpdate(\n        float targetAspectRatio, float naturalAspectRatio, boolean aspectRatioMismatch) {\n      this.targetAspectRatio = targetAspectRatio;\n      this.naturalAspectRatio = naturalAspectRatio;\n      this.aspectRatioMismatch = aspectRatioMismatch;\n\n      if (!isScheduled) {\n        isScheduled = true;\n        post(this);\n      }\n    }\n\n    @Override\n    public void run() {\n      isScheduled = false;\n      if (aspectRatioListener == null) {\n        return;\n      }\n      aspectRatioListener.onAspectRatioUpdated(\n          targetAspectRatio, naturalAspectRatio, aspectRatioMismatch);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ui;\n\nimport android.annotation.SuppressLint;\nimport android.os.Looper;\nimport android.widget.TextView;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.Locale;\n\n/**\n * A helper class for periodically updating a {@link TextView} with debug information obtained from\n * a {@link SimpleExoPlayer}.\n */\npublic class DebugTextViewHelper implements Player.EventListener, Runnable {\n\n  private static final int REFRESH_INTERVAL_MS = 1000;\n\n  private final SimpleExoPlayer player;\n  private final TextView textView;\n\n  private boolean started;\n\n  /**\n   * @param player The {@link SimpleExoPlayer} from which debug information should be obtained. Only\n   *     players which are accessed on the main thread are supported ({@code\n   *     player.getApplicationLooper() == Looper.getMainLooper()}).\n   * @param textView The {@link TextView} that should be updated to display the information.\n   */\n  public DebugTextViewHelper(SimpleExoPlayer player, TextView textView) {\n    Assertions.checkArgument(player.getApplicationLooper() == Looper.getMainLooper());\n    this.player = player;\n    this.textView = textView;\n  }\n\n  /**\n   * Starts periodic updates of the {@link TextView}. Must be called from the application's main\n   * thread.\n   */\n  public final void start() {\n    if (started) {\n      return;\n    }\n    started = true;\n    player.addListener(this);\n    updateAndPost();\n  }\n\n  /**\n   * Stops periodic updates of the {@link TextView}. Must be called from the application's main\n   * thread.\n   */\n  public final void stop() {\n    if (!started) {\n      return;\n    }\n    started = false;\n    player.removeListener(this);\n    textView.removeCallbacks(this);\n  }\n\n  // Player.EventListener implementation.\n\n  @Override\n  public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n    updateAndPost();\n  }\n\n  @Override\n  public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {\n    updateAndPost();\n  }\n\n  // Runnable implementation.\n\n  @Override\n  public final void run() {\n    updateAndPost();\n  }\n\n  // Protected methods.\n\n  @SuppressLint(\"SetTextI18n\")\n  protected final void updateAndPost() {\n    textView.setText(getDebugString());\n    textView.removeCallbacks(this);\n    textView.postDelayed(this, REFRESH_INTERVAL_MS);\n  }\n\n  /** Returns the debugging information string to be shown by the target {@link TextView}. */\n  protected String getDebugString() {\n    return getPlayerStateString() + getVideoString() + getAudioString();\n  }\n\n  /** Returns a string containing player state debugging information. */\n  protected String getPlayerStateString() {\n    String playbackStateString;\n    switch (player.getPlaybackState()) {\n      case Player.STATE_BUFFERING:\n        playbackStateString = \"buffering\";\n        break;\n      case Player.STATE_ENDED:\n        playbackStateString = \"ended\";\n        break;\n      case Player.STATE_IDLE:\n        playbackStateString = \"idle\";\n        break;\n      case Player.STATE_READY:\n        playbackStateString = \"ready\";\n        break;\n      default:\n        playbackStateString = \"unknown\";\n        break;\n    }\n    return String.format(\n        \"playWhenReady:%s playbackState:%s window:%s\",\n        player.getPlayWhenReady(), playbackStateString, player.getCurrentWindowIndex());\n  }\n\n  /** Returns a string containing video debugging information. */\n  protected String getVideoString() {\n    Format format = player.getVideoFormat();\n    DecoderCounters decoderCounters = player.getVideoDecoderCounters();\n    if (format == null || decoderCounters == null) {\n      return \"\";\n    }\n    return \"\\n\"\n        + format.sampleMimeType\n        + \"(id:\"\n        + format.id\n        + \" r:\"\n        + format.width\n        + \"x\"\n        + format.height\n        + getPixelAspectRatioString(format.pixelWidthHeightRatio)\n        + getDecoderCountersBufferCountString(decoderCounters)\n        + \")\";\n  }\n\n  /** Returns a string containing audio debugging information. */\n  protected String getAudioString() {\n    Format format = player.getAudioFormat();\n    DecoderCounters decoderCounters = player.getAudioDecoderCounters();\n    if (format == null || decoderCounters == null) {\n      return \"\";\n    }\n    return \"\\n\"\n        + format.sampleMimeType\n        + \"(id:\"\n        + format.id\n        + \" hz:\"\n        + format.sampleRate\n        + \" ch:\"\n        + format.channelCount\n        + getDecoderCountersBufferCountString(decoderCounters)\n        + \")\";\n  }\n\n  private static String getDecoderCountersBufferCountString(DecoderCounters counters) {\n    if (counters == null) {\n      return \"\";\n    }\n    counters.ensureUpdated();\n    return \" sib:\" + counters.skippedInputBufferCount\n        + \" sb:\" + counters.skippedOutputBufferCount\n        + \" rb:\" + counters.renderedOutputBufferCount\n        + \" db:\" + counters.droppedBufferCount\n        + \" mcdb:\" + counters.maxConsecutiveDroppedBufferCount\n        + \" dk:\" + counters.droppedToKeyframeCount;\n  }\n\n  private static String getPixelAspectRatioString(float pixelAspectRatio) {\n    return pixelAspectRatio == Format.NO_VALUE || pixelAspectRatio == 1f ? \"\"\n        : (\" par:\" + String.format(Locale.US, \"%.02f\", pixelAspectRatio));\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.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.ui;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.content.res.Resources;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Point;\nimport android.graphics.Rect;\nimport android.graphics.drawable.Drawable;\nimport android.os.Bundle;\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.Nullable;\nimport android.util.AttributeSet;\nimport android.util.DisplayMetrics;\nimport android.view.KeyEvent;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewParent;\nimport android.view.accessibility.AccessibilityEvent;\nimport android.view.accessibility.AccessibilityNodeInfo;\nimport android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Formatter;\nimport java.util.Locale;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\n/**\n * A time bar that shows a current position, buffered position, duration and ad markers.\n *\n * <p>A DefaultTimeBar can be customized by setting attributes, as outlined below.\n *\n * <h3>Attributes</h3>\n *\n * The following attributes can be set on a DefaultTimeBar when used in a layout XML file:\n *\n * <p>\n *\n * <ul>\n *   <li><b>{@code bar_height}</b> - Dimension for the height of the time bar.\n *       <ul>\n *         <li>Default: {@link #DEFAULT_BAR_HEIGHT_DP}\n *       </ul>\n *   <li><b>{@code touch_target_height}</b> - Dimension for the height of the area in which touch\n *       interactions with the time bar are handled. If no height is specified, this also determines\n *       the height of the view.\n *       <ul>\n *         <li>Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP}\n *       </ul>\n *   <li><b>{@code ad_marker_width}</b> - Dimension for the width of any ad markers shown on the\n *       bar. Ad markers are superimposed on the time bar to show the times at which ads will play.\n *       <ul>\n *         <li>Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP}\n *       </ul>\n *   <li><b>{@code scrubber_enabled_size}</b> - Dimension for the diameter of the circular scrubber\n *       handle when scrubbing is enabled but not in progress. Set to zero if no scrubber handle\n *       should be shown.\n *       <ul>\n *         <li>Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP}\n *       </ul>\n *   <li><b>{@code scrubber_disabled_size}</b> - Dimension for the diameter of the circular scrubber\n *       handle when scrubbing isn't enabled. Set to zero if no scrubber handle should be shown.\n *       <ul>\n *         <li>Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP}\n *       </ul>\n *   <li><b>{@code scrubber_dragged_size}</b> - Dimension for the diameter of the circular scrubber\n *       handle when scrubbing is in progress. Set to zero if no scrubber handle should be shown.\n *       <ul>\n *         <li>Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP}\n *       </ul>\n *   <li><b>{@code scrubber_drawable}</b> - Optional reference to a drawable to draw for the\n *       scrubber handle. If set, this overrides the default behavior, which is to draw a circle for\n *       the scrubber handle.\n *   <li><b>{@code played_color}</b> - Color for the portion of the time bar representing media\n *       before the current playback position.\n *       <ul>\n *         <li>Corresponding method: {@link #setPlayedColor(int)}\n *         <li>Default: {@link #DEFAULT_PLAYED_COLOR}\n *       </ul>\n *   <li><b>{@code scrubber_color}</b> - Color for the scrubber handle.\n *       <ul>\n *         <li>Corresponding method: {@link #setScrubberColor(int)}\n *         <li>Default: {@link #DEFAULT_SCRUBBER_COLOR}\n *       </ul>\n *   <li><b>{@code buffered_color}</b> - Color for the portion of the time bar after the current\n *       played position up to the current buffered position.\n *       <ul>\n *         <li>Corresponding method: {@link #setBufferedColor(int)}\n *         <li>Default: {@link #DEFAULT_BUFFERED_COLOR}\n *       </ul>\n *   <li><b>{@code unplayed_color}</b> - Color for the portion of the time bar after the current\n *       buffered position.\n *       <ul>\n *         <li>Corresponding method: {@link #setUnplayedColor(int)}\n *         <li>Default: {@link #DEFAULT_UNPLAYED_COLOR}\n *       </ul>\n *   <li><b>{@code ad_marker_color}</b> - Color for unplayed ad markers.\n *       <ul>\n *         <li>Corresponding method: {@link #setAdMarkerColor(int)}\n *         <li>Default: {@link #DEFAULT_AD_MARKER_COLOR}\n *       </ul>\n *   <li><b>{@code played_ad_marker_color}</b> - Color for played ad markers.\n *       <ul>\n *         <li>Corresponding method: {@link #setPlayedAdMarkerColor(int)}\n *         <li>Default: {@link #DEFAULT_PLAYED_AD_MARKER_COLOR}\n *       </ul>\n * </ul>\n */\npublic class DefaultTimeBar extends View implements TimeBar {\n\n  /**\n   * Default height for the time bar, in dp.\n   */\n  public static final int DEFAULT_BAR_HEIGHT_DP = 4;\n  /**\n   * Default height for the touch target, in dp.\n   */\n  public static final int DEFAULT_TOUCH_TARGET_HEIGHT_DP = 26;\n  /**\n   * Default width for ad markers, in dp.\n   */\n  public static final int DEFAULT_AD_MARKER_WIDTH_DP = 4;\n  /**\n   * Default diameter for the scrubber when enabled, in dp.\n   */\n  public static final int DEFAULT_SCRUBBER_ENABLED_SIZE_DP = 12;\n  /**\n   * Default diameter for the scrubber when disabled, in dp.\n   */\n  public static final int DEFAULT_SCRUBBER_DISABLED_SIZE_DP = 0;\n  /**\n   * Default diameter for the scrubber when dragged, in dp.\n   */\n  public static final int DEFAULT_SCRUBBER_DRAGGED_SIZE_DP = 16;\n  /**\n   * Default color for the played portion of the time bar.\n   */\n  public static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF;\n  /** Default color for the played portion of the time bar. */\n  public static final int DEFAULT_UNPLAYED_COLOR = 0x33FFFFFF;\n  /** Default color for the buffered portion of the time bar. */\n  public static final int DEFAULT_BUFFERED_COLOR = 0xCCFFFFFF;\n  /** Default color for the scrubber handle. */\n  public static final int DEFAULT_SCRUBBER_COLOR = 0xFFFFFFFF;\n  /** Default color for ad markers. */\n  public static final int DEFAULT_AD_MARKER_COLOR = 0xB2FFFF00;\n  /** Default color for played ad markers. */\n  public static final int DEFAULT_PLAYED_AD_MARKER_COLOR = 0x33FFFF00;\n\n  /**\n   * The threshold in dps above the bar at which touch events trigger fine scrub mode.\n   */\n  private static final int FINE_SCRUB_Y_THRESHOLD_DP = -50;\n  /**\n   * The ratio by which times are reduced in fine scrub mode.\n   */\n  private static final int FINE_SCRUB_RATIO = 3;\n  /**\n   * The time after which the scrubbing listener is notified that scrubbing has stopped after\n   * performing an incremental scrub using key input.\n   */\n  private static final long STOP_SCRUBBING_TIMEOUT_MS = 1000;\n  private static final int DEFAULT_INCREMENT_COUNT = 20;\n\n  /**\n   * The name of the Android SDK view that most closely resembles this custom view. Used as the\n   * class name for accessibility.\n   */\n  private static final String ACCESSIBILITY_CLASS_NAME = \"android.widget.SeekBar\";\n\n  private final Rect seekBounds;\n  private final Rect progressBar;\n  private final Rect bufferedBar;\n  private final Rect scrubberBar;\n  private final Paint playedPaint;\n  private final Paint bufferedPaint;\n  private final Paint unplayedPaint;\n  private final Paint adMarkerPaint;\n  private final Paint playedAdMarkerPaint;\n  private final Paint scrubberPaint;\n  private final @Nullable Drawable scrubberDrawable;\n  private final int barHeight;\n  private final int touchTargetHeight;\n  private final int adMarkerWidth;\n  private final int scrubberEnabledSize;\n  private final int scrubberDisabledSize;\n  private final int scrubberDraggedSize;\n  private final int scrubberPadding;\n  private final int fineScrubYThreshold;\n  private final StringBuilder formatBuilder;\n  private final Formatter formatter;\n  private final Runnable stopScrubbingRunnable;\n  private final CopyOnWriteArraySet<OnScrubListener> listeners;\n  private final int[] locationOnScreen;\n  private final Point touchPosition;\n  private final float density;\n\n  private int keyCountIncrement;\n  private long keyTimeIncrement;\n  private int lastCoarseScrubXPosition;\n\n  private boolean scrubbing;\n  private long scrubPosition;\n  private long duration;\n  private long position;\n  private long bufferedPosition;\n  private int adGroupCount;\n  private @Nullable long[] adGroupTimesMs;\n  private @Nullable boolean[] playedAdGroups;\n\n  public DefaultTimeBar(Context context) {\n    this(context, null);\n  }\n\n  public DefaultTimeBar(Context context, @Nullable AttributeSet attrs) {\n    this(context, attrs, 0);\n  }\n\n  public DefaultTimeBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n    this(context, attrs, defStyleAttr, attrs);\n  }\n\n  // Suppress warnings due to usage of View methods in the constructor.\n  @SuppressWarnings(\"nullness:method.invocation.invalid\")\n  public DefaultTimeBar(\n      Context context,\n      @Nullable AttributeSet attrs,\n      int defStyleAttr,\n      @Nullable AttributeSet timebarAttrs) {\n    super(context, attrs, defStyleAttr);\n    seekBounds = new Rect();\n    progressBar = new Rect();\n    bufferedBar = new Rect();\n    scrubberBar = new Rect();\n    playedPaint = new Paint();\n    bufferedPaint = new Paint();\n    unplayedPaint = new Paint();\n    adMarkerPaint = new Paint();\n    playedAdMarkerPaint = new Paint();\n    scrubberPaint = new Paint();\n    scrubberPaint.setAntiAlias(true);\n    listeners = new CopyOnWriteArraySet<>();\n    locationOnScreen = new int[2];\n    touchPosition = new Point();\n\n    // Calculate the dimensions and paints for drawn elements.\n    Resources res = context.getResources();\n    DisplayMetrics displayMetrics = res.getDisplayMetrics();\n    density = displayMetrics.density;\n    fineScrubYThreshold = dpToPx(density, FINE_SCRUB_Y_THRESHOLD_DP);\n    int defaultBarHeight = dpToPx(density, DEFAULT_BAR_HEIGHT_DP);\n    int defaultTouchTargetHeight = dpToPx(density, DEFAULT_TOUCH_TARGET_HEIGHT_DP);\n    int defaultAdMarkerWidth = dpToPx(density, DEFAULT_AD_MARKER_WIDTH_DP);\n    int defaultScrubberEnabledSize = dpToPx(density, DEFAULT_SCRUBBER_ENABLED_SIZE_DP);\n    int defaultScrubberDisabledSize = dpToPx(density, DEFAULT_SCRUBBER_DISABLED_SIZE_DP);\n    int defaultScrubberDraggedSize = dpToPx(density, DEFAULT_SCRUBBER_DRAGGED_SIZE_DP);\n    if (timebarAttrs != null) {\n      TypedArray a =\n          context.getTheme().obtainStyledAttributes(timebarAttrs, R.styleable.DefaultTimeBar, 0, 0);\n      try {\n        scrubberDrawable = a.getDrawable(R.styleable.DefaultTimeBar_scrubber_drawable);\n        if (scrubberDrawable != null) {\n          setDrawableLayoutDirection(scrubberDrawable);\n          defaultTouchTargetHeight =\n              Math.max(scrubberDrawable.getMinimumHeight(), defaultTouchTargetHeight);\n        }\n        barHeight = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_bar_height,\n            defaultBarHeight);\n        touchTargetHeight = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_touch_target_height,\n            defaultTouchTargetHeight);\n        adMarkerWidth = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_ad_marker_width,\n            defaultAdMarkerWidth);\n        scrubberEnabledSize = a.getDimensionPixelSize(\n            R.styleable.DefaultTimeBar_scrubber_enabled_size, defaultScrubberEnabledSize);\n        scrubberDisabledSize = a.getDimensionPixelSize(\n            R.styleable.DefaultTimeBar_scrubber_disabled_size, defaultScrubberDisabledSize);\n        scrubberDraggedSize = a.getDimensionPixelSize(\n            R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize);\n        int playedColor = a.getInt(R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR);\n        int scrubberColor =\n            a.getInt(R.styleable.DefaultTimeBar_scrubber_color, DEFAULT_SCRUBBER_COLOR);\n        int bufferedColor =\n            a.getInt(R.styleable.DefaultTimeBar_buffered_color, DEFAULT_BUFFERED_COLOR);\n        int unplayedColor =\n            a.getInt(R.styleable.DefaultTimeBar_unplayed_color, DEFAULT_UNPLAYED_COLOR);\n        int adMarkerColor = a.getInt(R.styleable.DefaultTimeBar_ad_marker_color,\n            DEFAULT_AD_MARKER_COLOR);\n        int playedAdMarkerColor =\n            a.getInt(\n                R.styleable.DefaultTimeBar_played_ad_marker_color, DEFAULT_PLAYED_AD_MARKER_COLOR);\n        playedPaint.setColor(playedColor);\n        scrubberPaint.setColor(scrubberColor);\n        bufferedPaint.setColor(bufferedColor);\n        unplayedPaint.setColor(unplayedColor);\n        adMarkerPaint.setColor(adMarkerColor);\n        playedAdMarkerPaint.setColor(playedAdMarkerColor);\n      } finally {\n        a.recycle();\n      }\n    } else {\n      barHeight = defaultBarHeight;\n      touchTargetHeight = defaultTouchTargetHeight;\n      adMarkerWidth = defaultAdMarkerWidth;\n      scrubberEnabledSize = defaultScrubberEnabledSize;\n      scrubberDisabledSize = defaultScrubberDisabledSize;\n      scrubberDraggedSize = defaultScrubberDraggedSize;\n      playedPaint.setColor(DEFAULT_PLAYED_COLOR);\n      scrubberPaint.setColor(DEFAULT_SCRUBBER_COLOR);\n      bufferedPaint.setColor(DEFAULT_BUFFERED_COLOR);\n      unplayedPaint.setColor(DEFAULT_UNPLAYED_COLOR);\n      adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR);\n      playedAdMarkerPaint.setColor(DEFAULT_PLAYED_AD_MARKER_COLOR);\n      scrubberDrawable = null;\n    }\n    formatBuilder = new StringBuilder();\n    formatter = new Formatter(formatBuilder, Locale.getDefault());\n    stopScrubbingRunnable = () -> stopScrubbing(/* canceled= */ false);\n    if (scrubberDrawable != null) {\n      scrubberPadding = (scrubberDrawable.getMinimumWidth() + 1) / 2;\n    } else {\n      scrubberPadding =\n          (Math.max(scrubberDisabledSize, Math.max(scrubberEnabledSize, scrubberDraggedSize)) + 1)\n              / 2;\n    }\n    duration = C.TIME_UNSET;\n    keyTimeIncrement = C.TIME_UNSET;\n    keyCountIncrement = DEFAULT_INCREMENT_COUNT;\n    setFocusable(true);\n    if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {\n      setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);\n    }\n  }\n\n  /**\n   * Sets the color for the portion of the time bar representing media before the playback position.\n   *\n   * @param playedColor The color for the portion of the time bar representing media before the\n   *     playback position.\n   */\n  public void setPlayedColor(@ColorInt int playedColor) {\n    playedPaint.setColor(playedColor);\n    invalidate(seekBounds);\n  }\n\n  /**\n   * Sets the color for the scrubber handle.\n   *\n   * @param scrubberColor The color for the scrubber handle.\n   */\n  public void setScrubberColor(@ColorInt int scrubberColor) {\n    scrubberPaint.setColor(scrubberColor);\n    invalidate(seekBounds);\n  }\n\n  /**\n   * Sets the color for the portion of the time bar after the current played position up to the\n   * current buffered position.\n   *\n   * @param bufferedColor The color for the portion of the time bar after the current played\n   *     position up to the current buffered position.\n   */\n  public void setBufferedColor(@ColorInt int bufferedColor) {\n    bufferedPaint.setColor(bufferedColor);\n    invalidate(seekBounds);\n  }\n\n  /**\n   * Sets the color for the portion of the time bar after the current played position.\n   *\n   * @param unplayedColor The color for the portion of the time bar after the current played\n   *     position.\n   */\n  public void setUnplayedColor(@ColorInt int unplayedColor) {\n    unplayedPaint.setColor(unplayedColor);\n    invalidate(seekBounds);\n  }\n\n  /**\n   * Sets the color for unplayed ad markers.\n   *\n   * @param adMarkerColor The color for unplayed ad markers.\n   */\n  public void setAdMarkerColor(@ColorInt int adMarkerColor) {\n    adMarkerPaint.setColor(adMarkerColor);\n    invalidate(seekBounds);\n  }\n\n  /**\n   * Sets the color for played ad markers.\n   *\n   * @param playedAdMarkerColor The color for played ad markers.\n   */\n  public void setPlayedAdMarkerColor(@ColorInt int playedAdMarkerColor) {\n    playedAdMarkerPaint.setColor(playedAdMarkerColor);\n    invalidate(seekBounds);\n  }\n\n  // TimeBar implementation.\n\n  @Override\n  public void addListener(OnScrubListener listener) {\n    listeners.add(listener);\n  }\n\n  @Override\n  public void removeListener(OnScrubListener listener) {\n    listeners.remove(listener);\n  }\n\n  @Override\n  public void setKeyTimeIncrement(long time) {\n    Assertions.checkArgument(time > 0);\n    keyCountIncrement = C.INDEX_UNSET;\n    keyTimeIncrement = time;\n  }\n\n  @Override\n  public void setKeyCountIncrement(int count) {\n    Assertions.checkArgument(count > 0);\n    keyCountIncrement = count;\n    keyTimeIncrement = C.TIME_UNSET;\n  }\n\n  @Override\n  public void setPosition(long position) {\n    this.position = position;\n    setContentDescription(getProgressText());\n    update();\n  }\n\n  @Override\n  public void setBufferedPosition(long bufferedPosition) {\n    this.bufferedPosition = bufferedPosition;\n    update();\n  }\n\n  @Override\n  public void setDuration(long duration) {\n    this.duration = duration;\n    if (scrubbing && duration == C.TIME_UNSET) {\n      stopScrubbing(/* canceled= */ true);\n    }\n    update();\n  }\n\n  @Override\n  public long getPreferredUpdateDelay() {\n    int timeBarWidthDp = pxToDp(density, progressBar.width());\n    return timeBarWidthDp == 0 || duration == 0 || duration == C.TIME_UNSET\n        ? Long.MAX_VALUE\n        : duration / timeBarWidthDp;\n  }\n\n  @Override\n  public void setAdGroupTimesMs(@Nullable long[] adGroupTimesMs, @Nullable boolean[] playedAdGroups,\n      int adGroupCount) {\n    Assertions.checkArgument(adGroupCount == 0\n        || (adGroupTimesMs != null && playedAdGroups != null));\n    this.adGroupCount = adGroupCount;\n    this.adGroupTimesMs = adGroupTimesMs;\n    this.playedAdGroups = playedAdGroups;\n    update();\n  }\n\n  // View methods.\n\n  @Override\n  public void setEnabled(boolean enabled) {\n    super.setEnabled(enabled);\n    if (scrubbing && !enabled) {\n      stopScrubbing(/* canceled= */ true);\n    }\n  }\n\n  @Override\n  public void onDraw(Canvas canvas) {\n    canvas.save();\n    drawTimeBar(canvas);\n    drawPlayhead(canvas);\n    canvas.restore();\n  }\n\n  @Override\n  public boolean onTouchEvent(MotionEvent event) {\n    if (!isEnabled() || duration <= 0) {\n      return false;\n    }\n    Point touchPosition = resolveRelativeTouchPosition(event);\n    int x = touchPosition.x;\n    int y = touchPosition.y;\n    switch (event.getAction()) {\n      case MotionEvent.ACTION_DOWN:\n        if (isInSeekBar(x, y)) {\n          positionScrubber(x);\n          startScrubbing(getScrubberPosition());\n          update();\n          invalidate();\n          return true;\n        }\n        break;\n      case MotionEvent.ACTION_MOVE:\n        if (scrubbing) {\n          if (y < fineScrubYThreshold) {\n            int relativeX = x - lastCoarseScrubXPosition;\n            positionScrubber(lastCoarseScrubXPosition + relativeX / FINE_SCRUB_RATIO);\n          } else {\n            lastCoarseScrubXPosition = x;\n            positionScrubber(x);\n          }\n          updateScrubbing(getScrubberPosition());\n          update();\n          invalidate();\n          return true;\n        }\n        break;\n      case MotionEvent.ACTION_UP:\n      case MotionEvent.ACTION_CANCEL:\n        if (scrubbing) {\n          stopScrubbing(/* canceled= */ event.getAction() == MotionEvent.ACTION_CANCEL);\n          return true;\n        }\n        break;\n      default:\n        // Do nothing.\n    }\n    return false;\n  }\n\n  @Override\n  public boolean onKeyDown(int keyCode, KeyEvent event) {\n    if (isEnabled()) {\n      long positionIncrement = getPositionIncrement();\n      switch (keyCode) {\n        case KeyEvent.KEYCODE_DPAD_LEFT:\n          positionIncrement = -positionIncrement;\n          // Fall through.\n        case KeyEvent.KEYCODE_DPAD_RIGHT:\n          if (scrubIncrementally(positionIncrement)) {\n            removeCallbacks(stopScrubbingRunnable);\n            postDelayed(stopScrubbingRunnable, STOP_SCRUBBING_TIMEOUT_MS);\n            return true;\n          }\n          break;\n        case KeyEvent.KEYCODE_DPAD_CENTER:\n        case KeyEvent.KEYCODE_ENTER:\n          if (scrubbing) {\n            stopScrubbing(/* canceled= */ false);\n            return true;\n          }\n          break;\n        default:\n          // Do nothing.\n      }\n    }\n    return super.onKeyDown(keyCode, event);\n  }\n\n  @Override\n  protected void onFocusChanged(\n      boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) {\n    super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);\n    if (scrubbing && !gainFocus) {\n      stopScrubbing(/* canceled= */ false);\n    }\n  }\n\n  @Override\n  protected void drawableStateChanged() {\n    super.drawableStateChanged();\n    updateDrawableState();\n  }\n\n  @Override\n  public void jumpDrawablesToCurrentState() {\n    super.jumpDrawablesToCurrentState();\n    if (scrubberDrawable != null) {\n      scrubberDrawable.jumpToCurrentState();\n    }\n  }\n\n  @Override\n  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n    int heightMode = MeasureSpec.getMode(heightMeasureSpec);\n    int heightSize = MeasureSpec.getSize(heightMeasureSpec);\n    int height = heightMode == MeasureSpec.UNSPECIFIED ? touchTargetHeight\n        : heightMode == MeasureSpec.EXACTLY ? heightSize : Math.min(touchTargetHeight, heightSize);\n    setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height);\n    updateDrawableState();\n  }\n\n  @Override\n  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n    int width = right - left;\n    int height = bottom - top;\n    int barY = (height - touchTargetHeight) / 2;\n    int seekLeft = getPaddingLeft();\n    int seekRight = width - getPaddingRight();\n    int progressY = barY + (touchTargetHeight - barHeight) / 2;\n    seekBounds.set(seekLeft, barY, seekRight, barY + touchTargetHeight);\n    progressBar.set(seekBounds.left + scrubberPadding, progressY,\n        seekBounds.right - scrubberPadding, progressY + barHeight);\n    update();\n  }\n\n  @Override\n  public void onRtlPropertiesChanged(int layoutDirection) {\n    if (scrubberDrawable != null && setDrawableLayoutDirection(scrubberDrawable, layoutDirection)) {\n      invalidate();\n    }\n  }\n\n  @Override\n  public void onInitializeAccessibilityEvent(AccessibilityEvent event) {\n    super.onInitializeAccessibilityEvent(event);\n    if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) {\n      event.getText().add(getProgressText());\n    }\n    event.setClassName(ACCESSIBILITY_CLASS_NAME);\n  }\n\n  @TargetApi(21)\n  @Override\n  public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {\n    super.onInitializeAccessibilityNodeInfo(info);\n    info.setClassName(ACCESSIBILITY_CLASS_NAME);\n    info.setContentDescription(getProgressText());\n    if (duration <= 0) {\n      return;\n    }\n    if (Util.SDK_INT >= 21) {\n      info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);\n      info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD);\n    } else {\n      info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);\n      info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);\n    }\n  }\n\n  @Override\n  public boolean performAccessibilityAction(int action, @Nullable Bundle args) {\n    if (super.performAccessibilityAction(action, args)) {\n      return true;\n    }\n    if (duration <= 0) {\n      return false;\n    }\n    if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {\n      if (scrubIncrementally(-getPositionIncrement())) {\n        stopScrubbing(/* canceled= */ false);\n      }\n    } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {\n      if (scrubIncrementally(getPositionIncrement())) {\n        stopScrubbing(/* canceled= */ false);\n      }\n    } else {\n      return false;\n    }\n    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);\n    return true;\n  }\n\n  // Internal methods.\n\n  private void startScrubbing(long scrubPosition) {\n    this.scrubPosition = scrubPosition;\n    scrubbing = true;\n    setPressed(true);\n    ViewParent parent = getParent();\n    if (parent != null) {\n      parent.requestDisallowInterceptTouchEvent(true);\n    }\n    for (OnScrubListener listener : listeners) {\n      listener.onScrubStart(this, scrubPosition);\n    }\n  }\n\n  private void updateScrubbing(long scrubPosition) {\n    if (this.scrubPosition == scrubPosition) {\n      return;\n    }\n    this.scrubPosition = scrubPosition;\n    for (OnScrubListener listener : listeners) {\n      listener.onScrubMove(this, scrubPosition);\n    }\n  }\n\n  private void stopScrubbing(boolean canceled) {\n    removeCallbacks(stopScrubbingRunnable);\n    scrubbing = false;\n    setPressed(false);\n    ViewParent parent = getParent();\n    if (parent != null) {\n      parent.requestDisallowInterceptTouchEvent(false);\n    }\n    invalidate();\n    for (OnScrubListener listener : listeners) {\n      listener.onScrubStop(this, scrubPosition, canceled);\n    }\n  }\n\n  /**\n   * Incrementally scrubs the position by {@code positionChange}.\n   *\n   * @param positionChange The change in the scrubber position, in milliseconds. May be negative.\n   * @return Returns whether the scrubber position changed.\n   */\n  private boolean scrubIncrementally(long positionChange) {\n    if (duration <= 0) {\n      return false;\n    }\n    long previousPosition = scrubbing ? scrubPosition : position;\n    long scrubPosition = Util.constrainValue(previousPosition + positionChange, 0, duration);\n    if (scrubPosition == previousPosition) {\n      return false;\n    }\n    if (!scrubbing) {\n      startScrubbing(scrubPosition);\n    } else {\n      updateScrubbing(scrubPosition);\n    }\n    update();\n    return true;\n  }\n\n  private void update() {\n    bufferedBar.set(progressBar);\n    scrubberBar.set(progressBar);\n    long newScrubberTime = scrubbing ? scrubPosition : position;\n    if (duration > 0) {\n      int bufferedPixelWidth = (int) ((progressBar.width() * bufferedPosition) / duration);\n      bufferedBar.right = Math.min(progressBar.left + bufferedPixelWidth, progressBar.right);\n      int scrubberPixelPosition = (int) ((progressBar.width() * newScrubberTime) / duration);\n      scrubberBar.right = Math.min(progressBar.left + scrubberPixelPosition, progressBar.right);\n    } else {\n      bufferedBar.right = progressBar.left;\n      scrubberBar.right = progressBar.left;\n    }\n    invalidate(seekBounds);\n  }\n\n  private void positionScrubber(float xPosition) {\n    scrubberBar.right = Util.constrainValue((int) xPosition, progressBar.left, progressBar.right);\n  }\n\n  private Point resolveRelativeTouchPosition(MotionEvent motionEvent) {\n    getLocationOnScreen(locationOnScreen);\n    touchPosition.set(\n        ((int) motionEvent.getRawX()) - locationOnScreen[0],\n        ((int) motionEvent.getRawY()) - locationOnScreen[1]);\n    return touchPosition;\n  }\n\n  private long getScrubberPosition() {\n    if (progressBar.width() <= 0 || duration == C.TIME_UNSET) {\n      return 0;\n    }\n    return (scrubberBar.width() * duration) / progressBar.width();\n  }\n\n  private boolean isInSeekBar(float x, float y) {\n    return seekBounds.contains((int) x, (int) y);\n  }\n\n  private void drawTimeBar(Canvas canvas) {\n    int progressBarHeight = progressBar.height();\n    int barTop = progressBar.centerY() - progressBarHeight / 2;\n    int barBottom = barTop + progressBarHeight;\n    if (duration <= 0) {\n      canvas.drawRect(progressBar.left, barTop, progressBar.right, barBottom, unplayedPaint);\n      return;\n    }\n    int bufferedLeft = bufferedBar.left;\n    int bufferedRight = bufferedBar.right;\n    int progressLeft = Math.max(Math.max(progressBar.left, bufferedRight), scrubberBar.right);\n    if (progressLeft < progressBar.right) {\n      canvas.drawRect(progressLeft, barTop, progressBar.right, barBottom, unplayedPaint);\n    }\n    bufferedLeft = Math.max(bufferedLeft, scrubberBar.right);\n    if (bufferedRight > bufferedLeft) {\n      canvas.drawRect(bufferedLeft, barTop, bufferedRight, barBottom, bufferedPaint);\n    }\n    if (scrubberBar.width() > 0) {\n      canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, playedPaint);\n    }\n    if (adGroupCount == 0) {\n      return;\n    }\n    long[] adGroupTimesMs = Assertions.checkNotNull(this.adGroupTimesMs);\n    boolean[] playedAdGroups = Assertions.checkNotNull(this.playedAdGroups);\n    int adMarkerOffset = adMarkerWidth / 2;\n    for (int i = 0; i < adGroupCount; i++) {\n      long adGroupTimeMs = Util.constrainValue(adGroupTimesMs[i], 0, duration);\n      int markerPositionOffset =\n          (int) (progressBar.width() * adGroupTimeMs / duration) - adMarkerOffset;\n      int markerLeft = progressBar.left + Math.min(progressBar.width() - adMarkerWidth,\n          Math.max(0, markerPositionOffset));\n      Paint paint = playedAdGroups[i] ? playedAdMarkerPaint : adMarkerPaint;\n      canvas.drawRect(markerLeft, barTop, markerLeft + adMarkerWidth, barBottom, paint);\n    }\n  }\n\n  private void drawPlayhead(Canvas canvas) {\n    if (duration <= 0) {\n      return;\n    }\n    int playheadX = Util.constrainValue(scrubberBar.right, scrubberBar.left, progressBar.right);\n    int playheadY = scrubberBar.centerY();\n    if (scrubberDrawable == null) {\n      int scrubberSize = (scrubbing || isFocused()) ? scrubberDraggedSize\n          : (isEnabled() ? scrubberEnabledSize : scrubberDisabledSize);\n      int playheadRadius = scrubberSize / 2;\n      canvas.drawCircle(playheadX, playheadY, playheadRadius, scrubberPaint);\n    } else {\n      int scrubberDrawableWidth = scrubberDrawable.getIntrinsicWidth();\n      int scrubberDrawableHeight = scrubberDrawable.getIntrinsicHeight();\n      scrubberDrawable.setBounds(\n          playheadX - scrubberDrawableWidth / 2,\n          playheadY - scrubberDrawableHeight / 2,\n          playheadX + scrubberDrawableWidth / 2,\n          playheadY + scrubberDrawableHeight / 2);\n      scrubberDrawable.draw(canvas);\n    }\n  }\n\n  private void updateDrawableState() {\n    if (scrubberDrawable != null && scrubberDrawable.isStateful()\n        && scrubberDrawable.setState(getDrawableState())) {\n      invalidate();\n    }\n  }\n\n  private String getProgressText() {\n    return Util.getStringForTime(formatBuilder, formatter, position);\n  }\n\n  private long getPositionIncrement() {\n    return keyTimeIncrement == C.TIME_UNSET\n        ? (duration == C.TIME_UNSET ? 0 : (duration / keyCountIncrement)) : keyTimeIncrement;\n  }\n\n  private boolean setDrawableLayoutDirection(Drawable drawable) {\n    return Util.SDK_INT >= 23 && setDrawableLayoutDirection(drawable, getLayoutDirection());\n  }\n\n  private static boolean setDrawableLayoutDirection(Drawable drawable, int layoutDirection) {\n    return Util.SDK_INT >= 23 && drawable.setLayoutDirection(layoutDirection);\n  }\n\n  private static int dpToPx(float density, int dps) {\n    return (int) (dps * density + 0.5f);\n  }\n\n  private static int pxToDp(float density, int px) {\n    return (int) (px / density);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.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.ui;\n\nimport android.content.res.Resources;\nimport android.text.TextUtils;\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.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Locale;\n\n/** A default {@link TrackNameProvider}. */\npublic class DefaultTrackNameProvider implements TrackNameProvider {\n\n  private final Resources resources;\n\n  /** @param resources Resources from which to obtain strings. */\n  public DefaultTrackNameProvider(Resources resources) {\n    this.resources = Assertions.checkNotNull(resources);\n  }\n\n  @Override\n  public String getTrackName(Format format) {\n    String trackName;\n    int trackType = inferPrimaryTrackType(format);\n    if (trackType == C.TRACK_TYPE_VIDEO) {\n      trackName =\n          joinWithSeparator(\n              buildRoleString(format), buildResolutionString(format), buildBitrateString(format));\n    } else if (trackType == C.TRACK_TYPE_AUDIO) {\n      trackName =\n          joinWithSeparator(\n              buildLanguageOrLabelString(format),\n              buildAudioChannelString(format),\n              buildBitrateString(format));\n    } else {\n      trackName = buildLanguageOrLabelString(format);\n    }\n    return trackName.length() == 0 ? resources.getString(R.string.exo_track_unknown) : trackName;\n  }\n\n  private String buildResolutionString(Format format) {\n    int width = format.width;\n    int height = format.height;\n    return width == Format.NO_VALUE || height == Format.NO_VALUE\n        ? \"\"\n        : resources.getString(R.string.exo_track_resolution, width, height);\n  }\n\n  private String buildBitrateString(Format format) {\n    int bitrate = format.bitrate;\n    return bitrate == Format.NO_VALUE\n        ? \"\"\n        : resources.getString(R.string.exo_track_bitrate, bitrate / 1000000f);\n  }\n\n  private String buildAudioChannelString(Format format) {\n    int channelCount = format.channelCount;\n    if (channelCount == Format.NO_VALUE || channelCount < 1) {\n      return \"\";\n    }\n    switch (channelCount) {\n      case 1:\n        return resources.getString(R.string.exo_track_mono);\n      case 2:\n        return resources.getString(R.string.exo_track_stereo);\n      case 6:\n      case 7:\n        return resources.getString(R.string.exo_track_surround_5_point_1);\n      case 8:\n        return resources.getString(R.string.exo_track_surround_7_point_1);\n      default:\n        return resources.getString(R.string.exo_track_surround);\n    }\n  }\n\n  private String buildLanguageOrLabelString(Format format) {\n    String languageAndRole =\n        joinWithSeparator(buildLanguageString(format), buildRoleString(format));\n    return TextUtils.isEmpty(languageAndRole) ? buildLabelString(format) : languageAndRole;\n  }\n\n  private String buildLabelString(Format format) {\n    return TextUtils.isEmpty(format.label) ? \"\" : format.label;\n  }\n\n  private String buildLanguageString(Format format) {\n    String language = format.language;\n    if (TextUtils.isEmpty(language) || C.LANGUAGE_UNDETERMINED.equals(language)) {\n      return \"\";\n    }\n    Locale locale = Util.SDK_INT >= 21 ? Locale.forLanguageTag(language) : new Locale(language);\n    return locale.getDisplayName();\n  }\n\n  private String buildRoleString(Format format) {\n    String roles = \"\";\n    if ((format.roleFlags & C.ROLE_FLAG_ALTERNATE) != 0) {\n      roles = resources.getString(R.string.exo_track_role_alternate);\n    }\n    if ((format.roleFlags & C.ROLE_FLAG_SUPPLEMENTARY) != 0) {\n      roles = joinWithSeparator(roles, resources.getString(R.string.exo_track_role_supplementary));\n    }\n    if ((format.roleFlags & C.ROLE_FLAG_COMMENTARY) != 0) {\n      roles = joinWithSeparator(roles, resources.getString(R.string.exo_track_role_commentary));\n    }\n    if ((format.roleFlags & (C.ROLE_FLAG_CAPTION | C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND)) != 0) {\n      roles =\n          joinWithSeparator(roles, resources.getString(R.string.exo_track_role_closed_captions));\n    }\n    return roles;\n  }\n\n  private String joinWithSeparator(String... items) {\n    String itemList = \"\";\n    for (String item : items) {\n      if (item.length() > 0) {\n        if (TextUtils.isEmpty(itemList)) {\n          itemList = item;\n        } else {\n          itemList = resources.getString(R.string.exo_item_list, itemList, item);\n        }\n      }\n    }\n    return itemList;\n  }\n\n  private static int inferPrimaryTrackType(Format format) {\n    int trackType = MimeTypes.getTrackType(format.sampleMimeType);\n    if (trackType != C.TRACK_TYPE_UNKNOWN) {\n      return trackType;\n    }\n    if (MimeTypes.getVideoMediaMimeType(format.codecs) != null) {\n      return C.TRACK_TYPE_VIDEO;\n    }\n    if (MimeTypes.getAudioMediaMimeType(format.codecs) != null) {\n      return C.TRACK_TYPE_AUDIO;\n    }\n    if (format.width != Format.NO_VALUE || format.height != Format.NO_VALUE) {\n      return C.TRACK_TYPE_VIDEO;\n    }\n    if (format.channelCount != Format.NO_VALUE || format.sampleRate != Format.NO_VALUE) {\n      return C.TRACK_TYPE_AUDIO;\n    }\n    return C.TRACK_TYPE_UNKNOWN;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.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.ui;\n\nimport android.app.Notification;\nimport android.app.PendingIntent;\nimport android.content.Context;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.StringRes;\nimport androidx.core.app.NotificationCompat;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.offline.Download;\nimport java.util.List;\n\n/** Helper for creating download notifications. */\npublic final class DownloadNotificationHelper {\n\n  private static final @StringRes int NULL_STRING_ID = 0;\n\n  private final Context context;\n  private final NotificationCompat.Builder notificationBuilder;\n\n  /**\n   * @param context A context.\n   * @param channelId The id of the notification channel to use.\n   */\n  public DownloadNotificationHelper(Context context, String channelId) {\n    context = context.getApplicationContext();\n    this.context = context;\n    this.notificationBuilder = new NotificationCompat.Builder(context, channelId);\n  }\n\n  /**\n   * Returns a progress notification for the given downloads.\n   *\n   * @param smallIcon A small icon for the notification.\n   * @param contentIntent An optional content intent to send when the notification is clicked.\n   * @param message An optional message to display on the notification.\n   * @param downloads The downloads.\n   * @return The notification.\n   */\n  public Notification buildProgressNotification(\n      @DrawableRes int smallIcon,\n      @Nullable PendingIntent contentIntent,\n      @Nullable String message,\n      List<Download> downloads) {\n    float totalPercentage = 0;\n    int downloadTaskCount = 0;\n    boolean allDownloadPercentagesUnknown = true;\n    boolean haveDownloadedBytes = false;\n    boolean haveDownloadTasks = false;\n    boolean haveRemoveTasks = false;\n    for (int i = 0; i < downloads.size(); i++) {\n      Download download = downloads.get(i);\n      if (download.state == Download.STATE_REMOVING) {\n        haveRemoveTasks = true;\n        continue;\n      }\n      if (download.state != Download.STATE_RESTARTING\n          && download.state != Download.STATE_DOWNLOADING) {\n        continue;\n      }\n      haveDownloadTasks = true;\n      float downloadPercentage = download.getPercentDownloaded();\n      if (downloadPercentage != C.PERCENTAGE_UNSET) {\n        allDownloadPercentagesUnknown = false;\n        totalPercentage += downloadPercentage;\n      }\n      haveDownloadedBytes |= download.getBytesDownloaded() > 0;\n      downloadTaskCount++;\n    }\n\n    int titleStringId =\n        haveDownloadTasks\n            ? R.string.exo_download_downloading\n            : (haveRemoveTasks ? R.string.exo_download_removing : NULL_STRING_ID);\n    int progress = 0;\n    boolean indeterminate = true;\n    if (haveDownloadTasks) {\n      progress = (int) (totalPercentage / downloadTaskCount);\n      indeterminate = allDownloadPercentagesUnknown && haveDownloadedBytes;\n    }\n    return buildNotification(\n        smallIcon,\n        contentIntent,\n        message,\n        titleStringId,\n        /* maxProgress= */ 100,\n        progress,\n        indeterminate,\n        /* ongoing= */ true,\n        /* showWhen= */ false);\n  }\n\n  /**\n   * Returns a notification for a completed download.\n   *\n   * @param smallIcon A small icon for the notifications.\n   * @param contentIntent An optional content intent to send when the notification is clicked.\n   * @param message An optional message to display on the notification.\n   * @return The notification.\n   */\n  public Notification buildDownloadCompletedNotification(\n      @DrawableRes int smallIcon, @Nullable PendingIntent contentIntent, @Nullable String message) {\n    int titleStringId = R.string.exo_download_completed;\n    return buildEndStateNotification(smallIcon, contentIntent, message, titleStringId);\n  }\n\n  /**\n   * Returns a notification for a failed download.\n   *\n   * @param smallIcon A small icon for the notifications.\n   * @param contentIntent An optional content intent to send when the notification is clicked.\n   * @param message An optional message to display on the notification.\n   * @return The notification.\n   */\n  public Notification buildDownloadFailedNotification(\n      @DrawableRes int smallIcon, @Nullable PendingIntent contentIntent, @Nullable String message) {\n    @StringRes int titleStringId = R.string.exo_download_failed;\n    return buildEndStateNotification(smallIcon, contentIntent, message, titleStringId);\n  }\n\n  private Notification buildEndStateNotification(\n      @DrawableRes int smallIcon,\n      @Nullable PendingIntent contentIntent,\n      @Nullable String message,\n      @StringRes int titleStringId) {\n    return buildNotification(\n        smallIcon,\n        contentIntent,\n        message,\n        titleStringId,\n        /* maxProgress= */ 0,\n        /* currentProgress= */ 0,\n        /* indeterminateProgress= */ false,\n        /* ongoing= */ false,\n        /* showWhen= */ true);\n  }\n\n  private Notification buildNotification(\n      @DrawableRes int smallIcon,\n      @Nullable PendingIntent contentIntent,\n      @Nullable String message,\n      @StringRes int titleStringId,\n      int maxProgress,\n      int currentProgress,\n      boolean indeterminateProgress,\n      boolean ongoing,\n      boolean showWhen) {\n    notificationBuilder.setSmallIcon(smallIcon);\n    notificationBuilder.setContentTitle(\n        titleStringId == NULL_STRING_ID ? null : context.getResources().getString(titleStringId));\n    notificationBuilder.setContentIntent(contentIntent);\n    notificationBuilder.setStyle(\n        message == null ? null : new NotificationCompat.BigTextStyle().bigText(message));\n    notificationBuilder.setProgress(maxProgress, currentProgress, indeterminateProgress);\n    notificationBuilder.setOngoing(ongoing);\n    notificationBuilder.setShowWhen(showWhen);\n    return notificationBuilder.build();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.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.ui;\n\nimport android.app.Notification;\nimport android.app.PendingIntent;\nimport android.content.Context;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.offline.Download;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.List;\n\n/**\n * @deprecated Using this class can cause notifications to flicker on devices with {@link\n *     Util#SDK_INT} &lt; 21. Use {@link DownloadNotificationHelper} instead.\n */\n@Deprecated\npublic final class DownloadNotificationUtil {\n\n  private DownloadNotificationUtil() {}\n\n  /**\n   * Returns a progress notification for the given downloads.\n   *\n   * @param context A context for accessing resources.\n   * @param smallIcon A small icon for the notification.\n   * @param channelId The id of the notification channel to use.\n   * @param contentIntent An optional content intent to send when the notification is clicked.\n   * @param message An optional message to display on the notification.\n   * @param downloads The downloads.\n   * @return The notification.\n   */\n  public static Notification buildProgressNotification(\n      Context context,\n      @DrawableRes int smallIcon,\n      String channelId,\n      @Nullable PendingIntent contentIntent,\n      @Nullable String message,\n      List<Download> downloads) {\n    return new DownloadNotificationHelper(context, channelId)\n        .buildProgressNotification(smallIcon, contentIntent, message, downloads);\n  }\n\n  /**\n   * Returns a notification for a completed download.\n   *\n   * @param context A context for accessing resources.\n   * @param smallIcon A small icon for the notifications.\n   * @param channelId The id of the notification channel to use.\n   * @param contentIntent An optional content intent to send when the notification is clicked.\n   * @param message An optional message to display on the notification.\n   * @return The notification.\n   */\n  public static Notification buildDownloadCompletedNotification(\n      Context context,\n      @DrawableRes int smallIcon,\n      String channelId,\n      @Nullable PendingIntent contentIntent,\n      @Nullable String message) {\n    return new DownloadNotificationHelper(context, channelId)\n        .buildDownloadCompletedNotification(smallIcon, contentIntent, message);\n  }\n\n  /**\n   * Returns a notification for a failed download.\n   *\n   * @param context A context for accessing resources.\n   * @param smallIcon A small icon for the notifications.\n   * @param channelId The id of the notification channel to use.\n   * @param contentIntent An optional content intent to send when the notification is clicked.\n   * @param message An optional message to display on the notification.\n   * @return The notification.\n   */\n  public static Notification buildDownloadFailedNotification(\n      Context context,\n      @DrawableRes int smallIcon,\n      String channelId,\n      @Nullable PendingIntent contentIntent,\n      @Nullable String message) {\n    return new DownloadNotificationHelper(context, channelId)\n        .buildDownloadFailedNotification(smallIcon, contentIntent, message);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ui;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\n/** @deprecated Use {@link PlayerControlView}. */\n@Deprecated\npublic class PlaybackControlView extends PlayerControlView {\n\n  /** @deprecated Use {@link com.google.android.exoplayer2.ControlDispatcher}. */\n  @Deprecated\n  public interface ControlDispatcher extends com.google.android.exoplayer2.ControlDispatcher {}\n\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  private static final class DefaultControlDispatcher\n      extends com.google.android.exoplayer2.DefaultControlDispatcher implements ControlDispatcher {}\n  /** @deprecated Use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new DefaultControlDispatcher();\n\n  public PlaybackControlView(Context context) {\n    super(context);\n  }\n\n  public PlaybackControlView(Context context, AttributeSet attrs) {\n    super(context, attrs);\n  }\n\n  public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n  }\n\n  public PlaybackControlView(\n      Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs) {\n    super(context, attrs, defStyleAttr, playbackAttrs);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ui;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.res.Resources;\nimport android.content.res.TypedArray;\nimport android.graphics.drawable.Drawable;\nimport android.os.Looper;\nimport android.os.SystemClock;\nimport androidx.annotation.Nullable;\nimport android.util.AttributeSet;\nimport android.view.KeyEvent;\nimport android.view.LayoutInflater;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\nimport android.widget.ImageView;\nimport android.widget.TextView;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.PlaybackPreparer;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.RepeatModeUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\nimport java.util.Formatter;\nimport java.util.Locale;\n\n/**\n * A view for controlling {@link Player} instances.\n *\n * <p>A PlayerControlView can be customized by setting attributes (or calling corresponding\n * methods), overriding the view's layout file or by specifying a custom view layout file, as\n * outlined below.\n *\n * <h3>Attributes</h3>\n *\n * The following attributes can be set on a PlayerControlView when used in a layout XML file:\n *\n * <ul>\n *   <li><b>{@code show_timeout}</b> - The time between the last user interaction and the controls\n *       being automatically hidden, in milliseconds. Use zero if the controls should not\n *       automatically timeout.\n *       <ul>\n *         <li>Corresponding method: {@link #setShowTimeoutMs(int)}\n *         <li>Default: {@link #DEFAULT_SHOW_TIMEOUT_MS}\n *       </ul>\n *   <li><b>{@code rewind_increment}</b> - The duration of the rewind applied when the user taps the\n *       rewind button, in milliseconds. Use zero to disable the rewind button.\n *       <ul>\n *         <li>Corresponding method: {@link #setRewindIncrementMs(int)}\n *         <li>Default: {@link #DEFAULT_REWIND_MS}\n *       </ul>\n *   <li><b>{@code fastforward_increment}</b> - Like {@code rewind_increment}, but for fast forward.\n *       <ul>\n *         <li>Corresponding method: {@link #setFastForwardIncrementMs(int)}\n *         <li>Default: {@link #DEFAULT_FAST_FORWARD_MS}\n *       </ul>\n *   <li><b>{@code repeat_toggle_modes}</b> - A flagged enumeration value specifying which repeat\n *       mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all},\n *       or {@code one|all}.\n *       <ul>\n *         <li>Corresponding method: {@link #setRepeatToggleModes(int)}\n *         <li>Default: {@link PlayerControlView#DEFAULT_REPEAT_TOGGLE_MODES}\n *       </ul>\n *   <li><b>{@code show_shuffle_button}</b> - Whether the shuffle button is shown.\n *       <ul>\n *         <li>Corresponding method: {@link #setShowShuffleButton(boolean)}\n *         <li>Default: false\n *       </ul>\n *   <li><b>{@code time_bar_min_update_interval}</b> - Specifies the minimum interval between time\n *       bar position updates.\n *       <ul>\n *         <li>Corresponding method: {@link #setTimeBarMinUpdateInterval(int)}\n *         <li>Default: {@link #DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS}\n *       </ul>\n *   <li><b>{@code controller_layout_id}</b> - Specifies the id of the layout to be inflated. See\n *       below for more details.\n *       <ul>\n *         <li>Corresponding method: None\n *         <li>Default: {@code R.layout.exo_player_control_view}\n *       </ul>\n *   <li>All attributes that can be set on {@link DefaultTimeBar} can also be set on a\n *       PlayerControlView, and will be propagated to the inflated {@link DefaultTimeBar} unless the\n *       layout is overridden to specify a custom {@code exo_progress} (see below).\n * </ul>\n *\n * <h3>Overriding the layout file</h3>\n *\n * To customize the layout of PlayerControlView throughout your app, or just for certain\n * configurations, you can define {@code exo_player_control_view.xml} layout files in your\n * application {@code res/layout*} directories. These layouts will override the one provided by the\n * ExoPlayer library, and will be inflated for use by PlayerControlView. The view identifies and\n * binds its children by looking for the following ids:\n *\n * <p>\n *\n * <ul>\n *   <li><b>{@code exo_play}</b> - The play button.\n *       <ul>\n *         <li>Type: {@link View}\n *       </ul>\n *   <li><b>{@code exo_pause}</b> - The pause button.\n *       <ul>\n *         <li>Type: {@link View}\n *       </ul>\n *   <li><b>{@code exo_ffwd}</b> - The fast forward button.\n *       <ul>\n *         <li>Type: {@link View}\n *       </ul>\n *   <li><b>{@code exo_rew}</b> - The rewind button.\n *       <ul>\n *         <li>Type: {@link View}\n *       </ul>\n *   <li><b>{@code exo_prev}</b> - The previous track button.\n *       <ul>\n *         <li>Type: {@link View}\n *       </ul>\n *   <li><b>{@code exo_next}</b> - The next track button.\n *       <ul>\n *         <li>Type: {@link View}\n *       </ul>\n *   <li><b>{@code exo_repeat_toggle}</b> - The repeat toggle button.\n *       <ul>\n *         <li>Type: {@link View}\n *       </ul>\n *   <li><b>{@code exo_shuffle}</b> - The shuffle button.\n *       <ul>\n *         <li>Type: {@link View}\n *       </ul>\n *   <li><b>{@code exo_vr}</b> - The VR mode button.\n *       <ul>\n *         <li>Type: {@link View}\n *       </ul>\n *   <li><b>{@code exo_position}</b> - Text view displaying the current playback position.\n *       <ul>\n *         <li>Type: {@link TextView}\n *       </ul>\n *   <li><b>{@code exo_duration}</b> - Text view displaying the current media duration.\n *       <ul>\n *         <li>Type: {@link TextView}\n *       </ul>\n *   <li><b>{@code exo_progress_placeholder}</b> - A placeholder that's replaced with the inflated\n *       {@link DefaultTimeBar}. Ignored if an {@code exo_progress} view exists.\n *       <ul>\n *         <li>Type: {@link View}\n *       </ul>\n *   <li><b>{@code exo_progress}</b> - Time bar that's updated during playback and allows seeking.\n *       {@link DefaultTimeBar} attributes set on the PlayerControlView will not be automatically\n *       propagated through to this instance. If a view exists with this id, any {@code\n *       exo_progress_placeholder} view will be ignored.\n *       <ul>\n *         <li>Type: {@link TimeBar}\n *       </ul>\n * </ul>\n *\n * <p>All child views are optional and so can be omitted if not required, however where defined they\n * must be of the expected type.\n *\n * <h3>Specifying a custom layout file</h3>\n *\n * Defining your own {@code exo_player_control_view.xml} is useful to customize the layout of\n * PlayerControlView throughout your application. It's also possible to customize the layout for a\n * single instance in a layout file. This is achieved by setting the {@code controller_layout_id}\n * attribute on a PlayerControlView. This will cause the specified layout to be inflated instead of\n * {@code exo_player_control_view.xml} for only the instance on which the attribute is set.\n */\npublic class PlayerControlView extends FrameLayout {\n\n  static {\n    ExoPlayerLibraryInfo.registerModule(\"goog.exo.ui\");\n  }\n\n  /** Listener to be notified about changes of the visibility of the UI control. */\n  public interface VisibilityListener {\n\n    /**\n     * Called when the visibility changes.\n     *\n     * @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}.\n     */\n    void onVisibilityChange(int visibility);\n  }\n\n  /** Listener to be notified when progress has been updated. */\n  public interface ProgressUpdateListener {\n\n    /**\n     * Called when progress needs to be updated.\n     *\n     * @param position The current position.\n     * @param bufferedPosition The current buffered position.\n     */\n    void onProgressUpdate(long position, long bufferedPosition);\n  }\n\n  /** The default fast forward increment, in milliseconds. */\n  public static final int DEFAULT_FAST_FORWARD_MS = 15000;\n  /** The default rewind increment, in milliseconds. */\n  public static final int DEFAULT_REWIND_MS = 5000;\n  /** The default show timeout, in milliseconds. */\n  public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000;\n  /** The default repeat toggle modes. */\n  public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =\n      RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE;\n  /** The default minimum interval between time bar position updates. */\n  public static final int DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS = 200;\n  /** The maximum number of windows that can be shown in a multi-window time bar. */\n  public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100;\n\n  private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000;\n  /** The maximum interval between time bar position updates. */\n  private static final int MAX_UPDATE_INTERVAL_MS = 1000;\n\n  private final ComponentListener componentListener;\n  private final View previousButton;\n  private final View nextButton;\n  private final View playButton;\n  private final View pauseButton;\n  private final View fastForwardButton;\n  private final View rewindButton;\n  private final ImageView repeatToggleButton;\n  private final ImageView shuffleButton;\n  private final View vrButton;\n  private final TextView durationView;\n  private final TextView positionView;\n  private final TimeBar timeBar;\n  private final StringBuilder formatBuilder;\n  private final Formatter formatter;\n  private final Timeline.Period period;\n  private final Timeline.Window window;\n  private final Runnable updateProgressAction;\n  private final Runnable hideAction;\n\n  private final Drawable repeatOffButtonDrawable;\n  private final Drawable repeatOneButtonDrawable;\n  private final Drawable repeatAllButtonDrawable;\n  private final String repeatOffButtonContentDescription;\n  private final String repeatOneButtonContentDescription;\n  private final String repeatAllButtonContentDescription;\n  private final Drawable shuffleOnButtonDrawable;\n  private final Drawable shuffleOffButtonDrawable;\n  private final float buttonAlphaEnabled;\n  private final float buttonAlphaDisabled;\n  private final String shuffleOnContentDescription;\n  private final String shuffleOffContentDescription;\n\n  @Nullable private Player player;\n  private com.google.android.exoplayer2.ControlDispatcher controlDispatcher;\n  @Nullable private VisibilityListener visibilityListener;\n  @Nullable private ProgressUpdateListener progressUpdateListener;\n  @Nullable private PlaybackPreparer playbackPreparer;\n\n  private boolean isAttachedToWindow;\n  private boolean showMultiWindowTimeBar;\n  private boolean multiWindowTimeBar;\n  private boolean scrubbing;\n  private int rewindMs;\n  private int fastForwardMs;\n  private int showTimeoutMs;\n  private int timeBarMinUpdateIntervalMs;\n  private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes;\n  private boolean showShuffleButton;\n  private long hideAtMs;\n  private long[] adGroupTimesMs;\n  private boolean[] playedAdGroups;\n  private long[] extraAdGroupTimesMs;\n  private boolean[] extraPlayedAdGroups;\n  private long currentWindowOffset;\n\n  public PlayerControlView(Context context) {\n    this(context, /* attrs= */ null);\n  }\n\n  public PlayerControlView(Context context, @Nullable AttributeSet attrs) {\n    this(context, attrs, 0);\n  }\n\n  public PlayerControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n    this(context, attrs, defStyleAttr, attrs);\n  }\n\n  public PlayerControlView(\n      Context context,\n      @Nullable AttributeSet attrs,\n      int defStyleAttr,\n      @Nullable AttributeSet playbackAttrs) {\n    super(context, attrs, defStyleAttr);\n    int controllerLayoutId = R.layout.exo_player_control_view;\n    rewindMs = DEFAULT_REWIND_MS;\n    fastForwardMs = DEFAULT_FAST_FORWARD_MS;\n    showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS;\n    repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES;\n    timeBarMinUpdateIntervalMs = DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS;\n    hideAtMs = C.TIME_UNSET;\n    showShuffleButton = false;\n    if (playbackAttrs != null) {\n      TypedArray a =\n          context\n              .getTheme()\n              .obtainStyledAttributes(playbackAttrs, R.styleable.PlayerControlView, 0, 0);\n      try {\n        rewindMs = a.getInt(R.styleable.PlayerControlView_rewind_increment, rewindMs);\n        fastForwardMs =\n            a.getInt(R.styleable.PlayerControlView_fastforward_increment, fastForwardMs);\n        showTimeoutMs = a.getInt(R.styleable.PlayerControlView_show_timeout, showTimeoutMs);\n        controllerLayoutId =\n            a.getResourceId(R.styleable.PlayerControlView_controller_layout_id, controllerLayoutId);\n        repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes);\n        showShuffleButton =\n            a.getBoolean(R.styleable.PlayerControlView_show_shuffle_button, showShuffleButton);\n        setTimeBarMinUpdateInterval(\n            a.getInt(\n                R.styleable.PlayerControlView_time_bar_min_update_interval,\n                timeBarMinUpdateIntervalMs));\n      } finally {\n        a.recycle();\n      }\n    }\n    period = new Timeline.Period();\n    window = new Timeline.Window();\n    formatBuilder = new StringBuilder();\n    formatter = new Formatter(formatBuilder, Locale.getDefault());\n    adGroupTimesMs = new long[0];\n    playedAdGroups = new boolean[0];\n    extraAdGroupTimesMs = new long[0];\n    extraPlayedAdGroups = new boolean[0];\n    componentListener = new ComponentListener();\n    controlDispatcher = new com.google.android.exoplayer2.DefaultControlDispatcher();\n    updateProgressAction = this::updateProgress;\n    hideAction = this::hide;\n\n    LayoutInflater.from(context).inflate(controllerLayoutId, this);\n    setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);\n\n    TimeBar customTimeBar = findViewById(R.id.exo_progress);\n    View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder);\n    if (customTimeBar != null) {\n      timeBar = customTimeBar;\n    } else if (timeBarPlaceholder != null) {\n      // Propagate attrs as timebarAttrs so that DefaultTimeBar's custom attributes are transferred,\n      // but standard attributes (e.g. background) are not.\n      DefaultTimeBar defaultTimeBar = new DefaultTimeBar(context, null, 0, playbackAttrs);\n      defaultTimeBar.setId(R.id.exo_progress);\n      defaultTimeBar.setLayoutParams(timeBarPlaceholder.getLayoutParams());\n      ViewGroup parent = ((ViewGroup) timeBarPlaceholder.getParent());\n      int timeBarIndex = parent.indexOfChild(timeBarPlaceholder);\n      parent.removeView(timeBarPlaceholder);\n      parent.addView(defaultTimeBar, timeBarIndex);\n      timeBar = defaultTimeBar;\n    } else {\n      timeBar = null;\n    }\n    durationView = findViewById(R.id.exo_duration);\n    positionView = findViewById(R.id.exo_position);\n\n    if (timeBar != null) {\n      timeBar.addListener(componentListener);\n    }\n    playButton = findViewById(R.id.exo_play);\n    if (playButton != null) {\n      playButton.setOnClickListener(componentListener);\n    }\n    pauseButton = findViewById(R.id.exo_pause);\n    if (pauseButton != null) {\n      pauseButton.setOnClickListener(componentListener);\n    }\n    previousButton = findViewById(R.id.exo_prev);\n    if (previousButton != null) {\n      previousButton.setOnClickListener(componentListener);\n    }\n    nextButton = findViewById(R.id.exo_next);\n    if (nextButton != null) {\n      nextButton.setOnClickListener(componentListener);\n    }\n    rewindButton = findViewById(R.id.exo_rew);\n    if (rewindButton != null) {\n      rewindButton.setOnClickListener(componentListener);\n    }\n    fastForwardButton = findViewById(R.id.exo_ffwd);\n    if (fastForwardButton != null) {\n      fastForwardButton.setOnClickListener(componentListener);\n    }\n    repeatToggleButton = findViewById(R.id.exo_repeat_toggle);\n    if (repeatToggleButton != null) {\n      repeatToggleButton.setOnClickListener(componentListener);\n    }\n    shuffleButton = findViewById(R.id.exo_shuffle);\n    if (shuffleButton != null) {\n      shuffleButton.setOnClickListener(componentListener);\n    }\n    vrButton = findViewById(R.id.exo_vr);\n    setShowVrButton(false);\n\n    Resources resources = context.getResources();\n\n    buttonAlphaEnabled =\n        (float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_enabled) / 100;\n    buttonAlphaDisabled =\n        (float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_disabled) / 100;\n\n    repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off);\n    repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one);\n    repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all);\n    shuffleOnButtonDrawable = resources.getDrawable(R.drawable.exo_controls_shuffle_on);\n    shuffleOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_shuffle_off);\n    repeatOffButtonContentDescription =\n        resources.getString(R.string.exo_controls_repeat_off_description);\n    repeatOneButtonContentDescription =\n        resources.getString(R.string.exo_controls_repeat_one_description);\n    repeatAllButtonContentDescription =\n        resources.getString(R.string.exo_controls_repeat_all_description);\n    shuffleOnContentDescription = resources.getString(R.string.exo_controls_shuffle_on_description);\n    shuffleOffContentDescription =\n        resources.getString(R.string.exo_controls_shuffle_off_description);\n  }\n\n  @SuppressWarnings(\"ResourceType\")\n  private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes(\n      TypedArray a, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {\n    return a.getInt(R.styleable.PlayerControlView_repeat_toggle_modes, repeatToggleModes);\n  }\n\n  /**\n   * Returns the {@link Player} currently being controlled by this view, or null if no player is\n   * set.\n   */\n  @Nullable\n  public Player getPlayer() {\n    return player;\n  }\n\n  /**\n   * Sets the {@link Player} to control.\n   *\n   * @param player The {@link Player} to control, or {@code null} to detach the current player. Only\n   *     players which are accessed on the main thread are supported ({@code\n   *     player.getApplicationLooper() == Looper.getMainLooper()}).\n   */\n  public void setPlayer(@Nullable Player player) {\n    Assertions.checkState(Looper.myLooper() == Looper.getMainLooper());\n    Assertions.checkArgument(\n        player == null || player.getApplicationLooper() == Looper.getMainLooper());\n    if (this.player == player) {\n      return;\n    }\n    if (this.player != null) {\n      this.player.removeListener(componentListener);\n    }\n    this.player = player;\n    if (player != null) {\n      player.addListener(componentListener);\n    }\n    updateAll();\n  }\n\n  /**\n   * Sets whether the time bar should show all windows, as opposed to just the current one. If the\n   * timeline has a period with unknown duration or more than {@link\n   * #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will fall back to showing a single\n   * window.\n   *\n   * @param showMultiWindowTimeBar Whether the time bar should show all windows.\n   */\n  public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {\n    this.showMultiWindowTimeBar = showMultiWindowTimeBar;\n    updateTimeline();\n  }\n\n  /**\n   * Sets the millisecond positions of extra ad markers relative to the start of the window (or\n   * timeline, if in multi-window mode) and whether each extra ad has been played or not. The\n   * markers are shown in addition to any ad markers for ads in the player's timeline.\n   *\n   * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or\n   *     {@code null} to show no extra ad markers.\n   * @param extraPlayedAdGroups Whether each ad has been played. Must be the same length as {@code\n   *     extraAdGroupTimesMs}, or {@code null} if {@code extraAdGroupTimesMs} is {@code null}.\n   */\n  public void setExtraAdGroupMarkers(\n      @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) {\n    if (extraAdGroupTimesMs == null) {\n      this.extraAdGroupTimesMs = new long[0];\n      this.extraPlayedAdGroups = new boolean[0];\n    } else {\n      extraPlayedAdGroups = Assertions.checkNotNull(extraPlayedAdGroups);\n      Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length);\n      this.extraAdGroupTimesMs = extraAdGroupTimesMs;\n      this.extraPlayedAdGroups = extraPlayedAdGroups;\n    }\n    updateTimeline();\n  }\n\n  /**\n   * Sets the {@link VisibilityListener}.\n   *\n   * @param listener The listener to be notified about visibility changes.\n   */\n  public void setVisibilityListener(VisibilityListener listener) {\n    this.visibilityListener = listener;\n  }\n\n  /**\n   * Sets the {@link ProgressUpdateListener}.\n   *\n   * @param listener The listener to be notified about when progress is updated.\n   */\n  public void setProgressUpdateListener(@Nullable ProgressUpdateListener listener) {\n    this.progressUpdateListener = listener;\n  }\n\n  /**\n   * Sets the {@link PlaybackPreparer}.\n   *\n   * @param playbackPreparer The {@link PlaybackPreparer}.\n   */\n  public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {\n    this.playbackPreparer = playbackPreparer;\n  }\n\n  /**\n   * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}.\n   *\n   * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}, or null\n   *     to use {@link com.google.android.exoplayer2.DefaultControlDispatcher}.\n   */\n  public void setControlDispatcher(\n      @Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) {\n    this.controlDispatcher =\n        controlDispatcher == null\n            ? new com.google.android.exoplayer2.DefaultControlDispatcher()\n            : controlDispatcher;\n  }\n\n  /**\n   * Sets the rewind increment in milliseconds.\n   *\n   * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the\n   *     rewind button to be disabled.\n   */\n  public void setRewindIncrementMs(int rewindMs) {\n    this.rewindMs = rewindMs;\n    updateNavigation();\n  }\n\n  /**\n   * Sets the fast forward increment in milliseconds.\n   *\n   * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will\n   *     cause the fast forward button to be disabled.\n   */\n  public void setFastForwardIncrementMs(int fastForwardMs) {\n    this.fastForwardMs = fastForwardMs;\n    updateNavigation();\n  }\n\n  /**\n   * Returns the playback controls timeout. The playback controls are automatically hidden after\n   * this duration of time has elapsed without user input.\n   *\n   * @return The duration in milliseconds. A non-positive value indicates that the controls will\n   *     remain visible indefinitely.\n   */\n  public int getShowTimeoutMs() {\n    return showTimeoutMs;\n  }\n\n  /**\n   * Sets the playback controls timeout. The playback controls are automatically hidden after this\n   * duration of time has elapsed without user input.\n   *\n   * @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls\n   *     to remain visible indefinitely.\n   */\n  public void setShowTimeoutMs(int showTimeoutMs) {\n    this.showTimeoutMs = showTimeoutMs;\n    if (isVisible()) {\n      // Reset the timeout.\n      hideAfterTimeout();\n    }\n  }\n\n  /**\n   * Returns which repeat toggle modes are enabled.\n   *\n   * @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}.\n   */\n  public @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes() {\n    return repeatToggleModes;\n  }\n\n  /**\n   * Sets which repeat toggle modes are enabled.\n   *\n   * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}.\n   */\n  public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {\n    this.repeatToggleModes = repeatToggleModes;\n    if (player != null) {\n      @Player.RepeatMode int currentMode = player.getRepeatMode();\n      if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE\n          && currentMode != Player.REPEAT_MODE_OFF) {\n        controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_OFF);\n      } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE\n          && currentMode == Player.REPEAT_MODE_ALL) {\n        controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ONE);\n      } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL\n          && currentMode == Player.REPEAT_MODE_ONE) {\n        controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ALL);\n      }\n    }\n    updateRepeatModeButton();\n  }\n\n  /** Returns whether the shuffle button is shown. */\n  public boolean getShowShuffleButton() {\n    return showShuffleButton;\n  }\n\n  /**\n   * Sets whether the shuffle button is shown.\n   *\n   * @param showShuffleButton Whether the shuffle button is shown.\n   */\n  public void setShowShuffleButton(boolean showShuffleButton) {\n    this.showShuffleButton = showShuffleButton;\n    updateShuffleButton();\n  }\n\n  /** Returns whether the VR button is shown. */\n  public boolean getShowVrButton() {\n    return vrButton != null && vrButton.getVisibility() == VISIBLE;\n  }\n\n  /**\n   * Sets whether the VR button is shown.\n   *\n   * @param showVrButton Whether the VR button is shown.\n   */\n  public void setShowVrButton(boolean showVrButton) {\n    if (vrButton != null) {\n      vrButton.setVisibility(showVrButton ? VISIBLE : GONE);\n    }\n  }\n\n  /**\n   * Sets listener for the VR button.\n   *\n   * @param onClickListener Listener for the VR button, or null to clear the listener.\n   */\n  public void setVrButtonListener(@Nullable OnClickListener onClickListener) {\n    if (vrButton != null) {\n      vrButton.setOnClickListener(onClickListener);\n    }\n  }\n\n  /**\n   * Sets the minimum interval between time bar position updates.\n   *\n   * <p>Note that smaller intervals, e.g. 33ms, will result in a smooth movement but will use more\n   * CPU resources while the time bar is visible, whereas larger intervals, e.g. 200ms, will result\n   * in a step-wise update with less CPU usage.\n   *\n   * @param minUpdateIntervalMs The minimum interval between time bar position updates, in\n   *     milliseconds.\n   */\n  public void setTimeBarMinUpdateInterval(int minUpdateIntervalMs) {\n    // Do not accept values below 16ms (60fps) and larger than the maximum update interval.\n    timeBarMinUpdateIntervalMs =\n        Util.constrainValue(minUpdateIntervalMs, 16, MAX_UPDATE_INTERVAL_MS);\n  }\n\n  /**\n   * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will\n   * be automatically hidden after this duration of time has elapsed without user input.\n   */\n  public void show() {\n    if (!isVisible()) {\n      setVisibility(VISIBLE);\n      if (visibilityListener != null) {\n        visibilityListener.onVisibilityChange(getVisibility());\n      }\n      updateAll();\n      requestPlayPauseFocus();\n    }\n    // Call hideAfterTimeout even if already visible to reset the timeout.\n    hideAfterTimeout();\n  }\n\n  /** Hides the controller. */\n  public void hide() {\n    if (isVisible()) {\n      setVisibility(GONE);\n      if (visibilityListener != null) {\n        visibilityListener.onVisibilityChange(getVisibility());\n      }\n      removeCallbacks(updateProgressAction);\n      removeCallbacks(hideAction);\n      hideAtMs = C.TIME_UNSET;\n    }\n  }\n\n  /** Returns whether the controller is currently visible. */\n  public boolean isVisible() {\n    return getVisibility() == VISIBLE;\n  }\n\n  private void hideAfterTimeout() {\n    removeCallbacks(hideAction);\n    if (showTimeoutMs > 0) {\n      hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs;\n      if (isAttachedToWindow) {\n        postDelayed(hideAction, showTimeoutMs);\n      }\n    } else {\n      hideAtMs = C.TIME_UNSET;\n    }\n  }\n\n  private void updateAll() {\n    updatePlayPauseButton();\n    updateNavigation();\n    updateRepeatModeButton();\n    updateShuffleButton();\n    updateTimeline();\n  }\n\n  private void updatePlayPauseButton() {\n    if (!isVisible() || !isAttachedToWindow) {\n      return;\n    }\n    boolean requestPlayPauseFocus = false;\n    boolean shouldShowPauseButton = shouldShowPauseButton();\n    if (playButton != null) {\n      requestPlayPauseFocus |= shouldShowPauseButton && playButton.isFocused();\n      playButton.setVisibility(shouldShowPauseButton ? GONE : VISIBLE);\n    }\n    if (pauseButton != null) {\n      requestPlayPauseFocus |= !shouldShowPauseButton && pauseButton.isFocused();\n      pauseButton.setVisibility(shouldShowPauseButton ? VISIBLE : GONE);\n    }\n    if (requestPlayPauseFocus) {\n      requestPlayPauseFocus();\n    }\n  }\n\n  private void updateNavigation() {\n    if (!isVisible() || !isAttachedToWindow) {\n      return;\n    }\n    boolean enableSeeking = false;\n    boolean enablePrevious = false;\n    boolean enableRewind = false;\n    boolean enableFastForward = false;\n    boolean enableNext = false;\n    if (player != null) {\n      Timeline timeline = player.getCurrentTimeline();\n      if (!timeline.isEmpty() && !player.isPlayingAd()) {\n        timeline.getWindow(player.getCurrentWindowIndex(), window);\n        boolean isSeekable = window.isSeekable;\n        enableSeeking = isSeekable;\n        enablePrevious = isSeekable || !window.isDynamic || player.hasPrevious();\n        enableRewind = isSeekable && rewindMs > 0;\n        enableFastForward = isSeekable && fastForwardMs > 0;\n        enableNext = window.isDynamic || player.hasNext();\n      }\n    }\n\n    setButtonEnabled(enablePrevious, previousButton);\n    setButtonEnabled(enableRewind, rewindButton);\n    setButtonEnabled(enableFastForward, fastForwardButton);\n    setButtonEnabled(enableNext, nextButton);\n    if (timeBar != null) {\n      timeBar.setEnabled(enableSeeking);\n    }\n  }\n\n  private void updateRepeatModeButton() {\n    if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) {\n      return;\n    }\n    if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {\n      repeatToggleButton.setVisibility(GONE);\n      return;\n    }\n    if (player == null) {\n      setButtonEnabled(false, repeatToggleButton);\n      repeatToggleButton.setImageDrawable(repeatOffButtonDrawable);\n      repeatToggleButton.setContentDescription(repeatOffButtonContentDescription);\n      return;\n    }\n    setButtonEnabled(true, repeatToggleButton);\n    switch (player.getRepeatMode()) {\n      case Player.REPEAT_MODE_OFF:\n        repeatToggleButton.setImageDrawable(repeatOffButtonDrawable);\n        repeatToggleButton.setContentDescription(repeatOffButtonContentDescription);\n        break;\n      case Player.REPEAT_MODE_ONE:\n        repeatToggleButton.setImageDrawable(repeatOneButtonDrawable);\n        repeatToggleButton.setContentDescription(repeatOneButtonContentDescription);\n        break;\n      case Player.REPEAT_MODE_ALL:\n        repeatToggleButton.setImageDrawable(repeatAllButtonDrawable);\n        repeatToggleButton.setContentDescription(repeatAllButtonContentDescription);\n        break;\n      default:\n        // Never happens.\n    }\n    repeatToggleButton.setVisibility(VISIBLE);\n  }\n\n  private void updateShuffleButton() {\n    if (!isVisible() || !isAttachedToWindow || shuffleButton == null) {\n      return;\n    }\n    if (!showShuffleButton) {\n      shuffleButton.setVisibility(GONE);\n    } else if (player == null) {\n      setButtonEnabled(false, shuffleButton);\n      shuffleButton.setImageDrawable(shuffleOffButtonDrawable);\n      shuffleButton.setContentDescription(shuffleOffContentDescription);\n    } else {\n      setButtonEnabled(true, shuffleButton);\n      shuffleButton.setImageDrawable(\n          player.getShuffleModeEnabled() ? shuffleOnButtonDrawable : shuffleOffButtonDrawable);\n      shuffleButton.setContentDescription(\n          player.getShuffleModeEnabled()\n              ? shuffleOnContentDescription\n              : shuffleOffContentDescription);\n    }\n  }\n\n  private void updateTimeline() {\n    if (player == null) {\n      return;\n    }\n    multiWindowTimeBar =\n        showMultiWindowTimeBar && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window);\n    currentWindowOffset = 0;\n    long durationUs = 0;\n    int adGroupCount = 0;\n    Timeline timeline = player.getCurrentTimeline();\n    if (!timeline.isEmpty()) {\n      int currentWindowIndex = player.getCurrentWindowIndex();\n      int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex;\n      int lastWindowIndex = multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex;\n      for (int i = firstWindowIndex; i <= lastWindowIndex; i++) {\n        if (i == currentWindowIndex) {\n          currentWindowOffset = C.usToMs(durationUs);\n        }\n        timeline.getWindow(i, window);\n        if (window.durationUs == C.TIME_UNSET) {\n          Assertions.checkState(!multiWindowTimeBar);\n          break;\n        }\n        for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) {\n          timeline.getPeriod(j, period);\n          int periodAdGroupCount = period.getAdGroupCount();\n          for (int adGroupIndex = 0; adGroupIndex < periodAdGroupCount; adGroupIndex++) {\n            long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex);\n            if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) {\n              if (period.durationUs == C.TIME_UNSET) {\n                // Don't show ad markers for postrolls in periods with unknown duration.\n                continue;\n              }\n              adGroupTimeInPeriodUs = period.durationUs;\n            }\n            long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs();\n            if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) {\n              if (adGroupCount == adGroupTimesMs.length) {\n                int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2;\n                adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength);\n                playedAdGroups = Arrays.copyOf(playedAdGroups, newLength);\n              }\n              adGroupTimesMs[adGroupCount] = C.usToMs(durationUs + adGroupTimeInWindowUs);\n              playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex);\n              adGroupCount++;\n            }\n          }\n        }\n        durationUs += window.durationUs;\n      }\n    }\n    long durationMs = C.usToMs(durationUs);\n    if (durationView != null) {\n      durationView.setText(Util.getStringForTime(formatBuilder, formatter, durationMs));\n    }\n    if (timeBar != null) {\n      timeBar.setDuration(durationMs);\n      int extraAdGroupCount = extraAdGroupTimesMs.length;\n      int totalAdGroupCount = adGroupCount + extraAdGroupCount;\n      if (totalAdGroupCount > adGroupTimesMs.length) {\n        adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount);\n        playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount);\n      }\n      System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount);\n      System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount);\n      timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount);\n    }\n    updateProgress();\n  }\n\n  private void updateProgress() {\n    if (!isVisible() || !isAttachedToWindow) {\n      return;\n    }\n\n    long position = 0;\n    long bufferedPosition = 0;\n    if (player != null) {\n      position = currentWindowOffset + player.getContentPosition();\n      bufferedPosition = currentWindowOffset + player.getContentBufferedPosition();\n    }\n    if (positionView != null && !scrubbing) {\n      positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));\n    }\n    if (timeBar != null) {\n      timeBar.setPosition(position);\n      timeBar.setBufferedPosition(bufferedPosition);\n    }\n    if (progressUpdateListener != null) {\n      progressUpdateListener.onProgressUpdate(position, bufferedPosition);\n    }\n\n    // Cancel any pending updates and schedule a new one if necessary.\n    removeCallbacks(updateProgressAction);\n    int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState();\n    if (player != null && player.isPlaying()) {\n      long mediaTimeDelayMs =\n          timeBar != null ? timeBar.getPreferredUpdateDelay() : MAX_UPDATE_INTERVAL_MS;\n\n      // Limit delay to the start of the next full second to ensure position display is smooth.\n      long mediaTimeUntilNextFullSecondMs = 1000 - position % 1000;\n      mediaTimeDelayMs = Math.min(mediaTimeDelayMs, mediaTimeUntilNextFullSecondMs);\n\n      // Calculate the delay until the next update in real time, taking playbackSpeed into account.\n      float playbackSpeed = player.getPlaybackParameters().speed;\n      long delayMs =\n          playbackSpeed > 0 ? (long) (mediaTimeDelayMs / playbackSpeed) : MAX_UPDATE_INTERVAL_MS;\n\n      // Constrain the delay to avoid too frequent / infrequent updates.\n      delayMs = Util.constrainValue(delayMs, timeBarMinUpdateIntervalMs, MAX_UPDATE_INTERVAL_MS);\n      postDelayed(updateProgressAction, delayMs);\n    } else if (playbackState != Player.STATE_ENDED && playbackState != Player.STATE_IDLE) {\n      postDelayed(updateProgressAction, MAX_UPDATE_INTERVAL_MS);\n    }\n  }\n\n  private void requestPlayPauseFocus() {\n    boolean shouldShowPauseButton = shouldShowPauseButton();\n    if (!shouldShowPauseButton && playButton != null) {\n      playButton.requestFocus();\n    } else if (shouldShowPauseButton && pauseButton != null) {\n      pauseButton.requestFocus();\n    }\n  }\n\n  private void setButtonEnabled(boolean enabled, View view) {\n    if (view == null) {\n      return;\n    }\n    view.setEnabled(enabled);\n    view.setAlpha(enabled ? buttonAlphaEnabled : buttonAlphaDisabled);\n    view.setVisibility(VISIBLE);\n  }\n\n  private void previous(Player player) {\n    Timeline timeline = player.getCurrentTimeline();\n    if (timeline.isEmpty() || player.isPlayingAd()) {\n      return;\n    }\n    int windowIndex = player.getCurrentWindowIndex();\n    timeline.getWindow(windowIndex, window);\n    int previousWindowIndex = player.getPreviousWindowIndex();\n    if (previousWindowIndex != C.INDEX_UNSET\n        && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS\n            || (window.isDynamic && !window.isSeekable))) {\n      seekTo(player, previousWindowIndex, C.TIME_UNSET);\n    } else {\n      seekTo(player, windowIndex, /* positionMs= */ 0);\n    }\n  }\n\n  private void next(Player player) {\n    Timeline timeline = player.getCurrentTimeline();\n    if (timeline.isEmpty() || player.isPlayingAd()) {\n      return;\n    }\n    int windowIndex = player.getCurrentWindowIndex();\n    int nextWindowIndex = player.getNextWindowIndex();\n    if (nextWindowIndex != C.INDEX_UNSET) {\n      seekTo(player, nextWindowIndex, C.TIME_UNSET);\n    } else if (timeline.getWindow(windowIndex, window).isDynamic) {\n      seekTo(player, windowIndex, C.TIME_UNSET);\n    }\n  }\n\n  private void rewind(Player player) {\n    if (player.isCurrentWindowSeekable() && rewindMs > 0) {\n      seekToOffset(player, -rewindMs);\n    }\n  }\n\n  private void fastForward(Player player) {\n    if (player.isCurrentWindowSeekable() && fastForwardMs > 0) {\n      seekToOffset(player, fastForwardMs);\n    }\n  }\n\n  private void seekToOffset(Player player, long offsetMs) {\n    long positionMs = player.getCurrentPosition() + offsetMs;\n    long durationMs = player.getDuration();\n    if (durationMs != C.TIME_UNSET) {\n      positionMs = Math.min(positionMs, durationMs);\n    }\n    positionMs = Math.max(positionMs, 0);\n    seekTo(player, player.getCurrentWindowIndex(), positionMs);\n  }\n\n  private void seekToTimeBarPosition(Player player, long positionMs) {\n    int windowIndex;\n    Timeline timeline = player.getCurrentTimeline();\n    if (multiWindowTimeBar && !timeline.isEmpty()) {\n      int windowCount = timeline.getWindowCount();\n      windowIndex = 0;\n      while (true) {\n        long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs();\n        if (positionMs < windowDurationMs) {\n          break;\n        } else if (windowIndex == windowCount - 1) {\n          // Seeking past the end of the last window should seek to the end of the timeline.\n          positionMs = windowDurationMs;\n          break;\n        }\n        positionMs -= windowDurationMs;\n        windowIndex++;\n      }\n    } else {\n      windowIndex = player.getCurrentWindowIndex();\n    }\n    boolean dispatched = seekTo(player, windowIndex, positionMs);\n    if (!dispatched) {\n      // The seek wasn't dispatched then the progress bar scrubber will be in the wrong position.\n      // Trigger a progress update to snap it back.\n      updateProgress();\n    }\n  }\n\n  private boolean seekTo(Player player, int windowIndex, long positionMs) {\n    return controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);\n  }\n\n  @Override\n  public void onAttachedToWindow() {\n    super.onAttachedToWindow();\n    isAttachedToWindow = true;\n    if (hideAtMs != C.TIME_UNSET) {\n      long delayMs = hideAtMs - SystemClock.uptimeMillis();\n      if (delayMs <= 0) {\n        hide();\n      } else {\n        postDelayed(hideAction, delayMs);\n      }\n    } else if (isVisible()) {\n      hideAfterTimeout();\n    }\n    updateAll();\n  }\n\n  @Override\n  public void onDetachedFromWindow() {\n    super.onDetachedFromWindow();\n    isAttachedToWindow = false;\n    removeCallbacks(updateProgressAction);\n    removeCallbacks(hideAction);\n  }\n\n  @Override\n  public final boolean dispatchTouchEvent(MotionEvent ev) {\n    if (ev.getAction() == MotionEvent.ACTION_DOWN) {\n      removeCallbacks(hideAction);\n    } else if (ev.getAction() == MotionEvent.ACTION_UP) {\n      hideAfterTimeout();\n    }\n    return super.dispatchTouchEvent(ev);\n  }\n\n  @Override\n  public boolean dispatchKeyEvent(KeyEvent event) {\n    return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event);\n  }\n\n  /**\n   * Called to process media key events. Any {@link KeyEvent} can be passed but only media key\n   * events will be handled.\n   *\n   * @param event A key event.\n   * @return Whether the key event was handled.\n   */\n  public boolean dispatchMediaKeyEvent(KeyEvent event) {\n    int keyCode = event.getKeyCode();\n    if (player == null || !isHandledMediaKey(keyCode)) {\n      return false;\n    }\n    if (event.getAction() == KeyEvent.ACTION_DOWN) {\n      if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {\n        fastForward(player);\n      } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) {\n        rewind(player);\n      } else if (event.getRepeatCount() == 0) {\n        switch (keyCode) {\n          case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:\n            controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady());\n            break;\n          case KeyEvent.KEYCODE_MEDIA_PLAY:\n            controlDispatcher.dispatchSetPlayWhenReady(player, true);\n            break;\n          case KeyEvent.KEYCODE_MEDIA_PAUSE:\n            controlDispatcher.dispatchSetPlayWhenReady(player, false);\n            break;\n          case KeyEvent.KEYCODE_MEDIA_NEXT:\n            next(player);\n            break;\n          case KeyEvent.KEYCODE_MEDIA_PREVIOUS:\n            previous(player);\n            break;\n          default:\n            break;\n        }\n      }\n    }\n    return true;\n  }\n\n  private boolean shouldShowPauseButton() {\n    return player != null\n        && player.getPlaybackState() != Player.STATE_ENDED\n        && player.getPlaybackState() != Player.STATE_IDLE\n        && player.getPlayWhenReady();\n  }\n\n  @SuppressLint(\"InlinedApi\")\n  private static boolean isHandledMediaKey(int keyCode) {\n    return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD\n        || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND\n        || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE\n        || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY\n        || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE\n        || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT\n        || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS;\n  }\n\n  /**\n   * Returns whether the specified {@code timeline} can be shown on a multi-window time bar.\n   *\n   * @param timeline The {@link Timeline} to check.\n   * @param window A scratch {@link Timeline.Window} instance.\n   * @return Whether the specified timeline can be shown on a multi-window time bar.\n   */\n  private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Window window) {\n    if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) {\n      return false;\n    }\n    int windowCount = timeline.getWindowCount();\n    for (int i = 0; i < windowCount; i++) {\n      if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  private final class ComponentListener\n      implements Player.EventListener, TimeBar.OnScrubListener, OnClickListener {\n\n    @Override\n    public void onScrubStart(TimeBar timeBar, long position) {\n      scrubbing = true;\n      if (positionView != null) {\n        positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));\n      }\n    }\n\n    @Override\n    public void onScrubMove(TimeBar timeBar, long position) {\n      if (positionView != null) {\n        positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));\n      }\n    }\n\n    @Override\n    public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {\n      scrubbing = false;\n      if (!canceled && player != null) {\n        seekToTimeBarPosition(player, position);\n      }\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n      updatePlayPauseButton();\n      updateProgress();\n    }\n\n    @Override\n    public void onIsPlayingChanged(boolean isPlaying) {\n      updateProgress();\n    }\n\n    @Override\n    public void onRepeatModeChanged(int repeatMode) {\n      updateRepeatModeButton();\n      updateNavigation();\n    }\n\n    @Override\n    public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {\n      updateShuffleButton();\n      updateNavigation();\n    }\n\n    @Override\n    public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {\n      updateNavigation();\n      updateTimeline();\n    }\n\n    @Override\n    public void onTimelineChanged(\n        Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {\n      updateNavigation();\n      updateTimeline();\n    }\n\n    @Override\n    public void onClick(View view) {\n      Player player = PlayerControlView.this.player;\n      if (player == null) {\n        return;\n      }\n      if (nextButton == view) {\n        next(player);\n      } else if (previousButton == view) {\n        previous(player);\n      } else if (fastForwardButton == view) {\n        fastForward(player);\n      } else if (rewindButton == view) {\n        rewind(player);\n      } else if (playButton == view) {\n        if (player.getPlaybackState() == Player.STATE_IDLE) {\n          if (playbackPreparer != null) {\n            playbackPreparer.preparePlayback();\n          }\n        } else if (player.getPlaybackState() == Player.STATE_ENDED) {\n          seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);\n        }\n        controlDispatcher.dispatchSetPlayWhenReady(player, true);\n      } else if (pauseButton == view) {\n        controlDispatcher.dispatchSetPlayWhenReady(player, false);\n      } else if (repeatToggleButton == view) {\n        controlDispatcher.dispatchSetRepeatMode(\n            player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes));\n      } else if (shuffleButton == view) {\n        controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled());\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.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.ui;\n\nimport android.app.Notification;\nimport android.app.PendingIntent;\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.graphics.Bitmap;\nimport android.graphics.Color;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.Looper;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.StringRes;\nimport androidx.core.app.NotificationCompat;\nimport androidx.core.app.NotificationManagerCompat;\nimport androidx.media.app.NotificationCompat.MediaStyle;\nimport android.support.v4.media.session.MediaSessionCompat;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ControlDispatcher;\nimport com.google.android.exoplayer2.DefaultControlDispatcher;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.PlaybackPreparer;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.NotificationUtil;\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.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.checkerframework.checker.nullness.qual.RequiresNonNull;\n\n/**\n * A notification manager to start, update and cancel a media style notification reflecting the\n * player state.\n *\n * <p>The notification is cancelled when {@code null} is passed to {@link #setPlayer(Player)} or\n * when the notification is dismissed by the user.\n *\n * <p>If the player is released it must be removed from the manager by calling {@code\n * setPlayer(null)} which will cancel the notification.\n *\n * <h3>Action customization</h3>\n *\n * Standard playback actions can be shown or omitted as follows:\n *\n * <ul>\n *   <li><b>{@code useNavigationActions}</b> - Sets whether the navigation previous and next actions\n *       are displayed.\n *       <ul>\n *         <li>Corresponding setter: {@link #setUseNavigationActions(boolean)}\n *         <li>Default: {@code true}\n *       </ul>\n *   <li><b>{@code useNavigationActionsInCompactView}</b> - Sets whether the navigation previous and\n *       next actions should are displayed in compact view (including the lock screen notification).\n *       <ul>\n *         <li>Corresponding setter: {@link #setUseNavigationActionsInCompactView(boolean)}\n *         <li>Default: {@code false}\n *       </ul>\n *   <li><b>{@code usePlayPauseActions}</b> - Sets whether the play and pause actions are displayed.\n *       <ul>\n *         <li>Corresponding setter: {@link #setUsePlayPauseActions(boolean)}\n *         <li>Default: {@code true}\n *       </ul>\n *   <li><b>{@code useStopAction}</b> - Sets whether the stop action is displayed.\n *       <ul>\n *         <li>Corresponding setter: {@link #setUseStopAction(boolean)}\n *         <li>Default: {@code false}\n *       </ul>\n *   <li><b>{@code rewindIncrementMs}</b> - Sets the rewind increment. If set to zero the rewind\n *       action is not displayed.\n *       <ul>\n *         <li>Corresponding setter: {@link #setRewindIncrementMs(long)}\n *         <li>Default: {@link #DEFAULT_REWIND_MS} (5000)\n *       </ul>\n *   <li><b>{@code fastForwardIncrementMs}</b> - Sets the fast forward increment. If set to zero the\n *       fast forward action is not included in the notification.\n *       <ul>\n *         <li>Corresponding setter: {@link #setFastForwardIncrementMs(long)}\n *         <li>Default: {@link #DEFAULT_FAST_FORWARD_MS} (5000)\n *       </ul>\n * </ul>\n */\npublic class PlayerNotificationManager {\n\n  /** An adapter to provide content assets of the media currently playing. */\n  public interface MediaDescriptionAdapter {\n\n    /**\n     * Gets the content title for the current media item.\n     *\n     * <p>See {@link NotificationCompat.Builder#setContentTitle(CharSequence)}.\n     *\n     * @param player The {@link Player} for which a notification is being built.\n     */\n    String getCurrentContentTitle(Player player);\n\n    /**\n     * Creates a content intent for the current media item.\n     *\n     * <p>See {@link NotificationCompat.Builder#setContentIntent(PendingIntent)}.\n     *\n     * @param player The {@link Player} for which a notification is being built.\n     */\n    @Nullable\n    PendingIntent createCurrentContentIntent(Player player);\n\n    /**\n     * Gets the content text for the current media item.\n     *\n     * <p>See {@link NotificationCompat.Builder#setContentText(CharSequence)}.\n     *\n     * @param player The {@link Player} for which a notification is being built.\n     */\n    @Nullable\n    String getCurrentContentText(Player player);\n\n    /**\n     * Gets the content sub text for the current media item.\n     *\n     * <p>See {@link NotificationCompat.Builder#setSubText(CharSequence)}.\n     *\n     * @param player The {@link Player} for which a notification is being built.\n     */\n    @Nullable\n    default String getCurrentSubText(Player player) {\n      return null;\n    }\n\n    /**\n     * Gets the large icon for the current media item.\n     *\n     * <p>When a bitmap initially needs to be asynchronously loaded, a placeholder (or null) can be\n     * returned and the bitmap asynchronously passed to the {@link BitmapCallback} once it is\n     * loaded. Because the adapter may be called multiple times for the same media item, the bitmap\n     * should be cached by the app and whenever possible be returned synchronously at subsequent\n     * calls for the same media item.\n     *\n     * <p>See {@link NotificationCompat.Builder#setLargeIcon(Bitmap)}.\n     *\n     * @param player The {@link Player} for which a notification is being built.\n     * @param callback A {@link BitmapCallback} to provide a {@link Bitmap} asynchronously.\n     */\n    @Nullable\n    Bitmap getCurrentLargeIcon(Player player, BitmapCallback callback);\n  }\n\n  /** Defines and handles custom actions. */\n  public interface CustomActionReceiver {\n\n    /**\n     * Gets the actions handled by this receiver.\n     *\n     * <p>If multiple {@link PlayerNotificationManager} instances are in use at the same time, the\n     * {@code instanceId} must be set as an intent extra with key {@link\n     * PlayerNotificationManager#EXTRA_INSTANCE_ID} to avoid sending the action to every custom\n     * action receiver. It's also necessary to ensure something is different about the actions. This\n     * may be any of the {@link Intent} attributes considered by {@link Intent#filterEquals}, or\n     * different request code integers when creating the {@link PendingIntent}s with {@link\n     * PendingIntent#getBroadcast}. The easiest approach is to use the {@code instanceId} as the\n     * request code.\n     *\n     * @param context The {@link Context}.\n     * @param instanceId The instance id of the {@link PlayerNotificationManager}.\n     * @return A map of custom actions.\n     */\n    Map<String, NotificationCompat.Action> createCustomActions(Context context, int instanceId);\n\n    /**\n     * Gets the actions to be included in the notification given the current player state.\n     *\n     * @param player The {@link Player} for which a notification is being built.\n     * @return The actions to be included in the notification.\n     */\n    List<String> getCustomActions(Player player);\n\n    /**\n     * Called when a custom action has been received.\n     *\n     * @param player The player.\n     * @param action The action from {@link Intent#getAction()}.\n     * @param intent The received {@link Intent}.\n     */\n    void onCustomAction(Player player, String action, Intent intent);\n  }\n\n  /** A listener for changes to the notification. */\n  public interface NotificationListener {\n\n    /**\n     * Called after the notification has been started.\n     *\n     * @param notificationId The id with which the notification has been posted.\n     * @param notification The {@link Notification}.\n     * @deprecated Use {@link #onNotificationPosted(int, Notification, boolean)} instead.\n     */\n    @Deprecated\n    default void onNotificationStarted(int notificationId, Notification notification) {}\n\n    /**\n     * Called after the notification has been cancelled.\n     *\n     * @param notificationId The id of the notification which has been cancelled.\n     * @deprecated Use {@link #onNotificationCancelled(int, boolean)}.\n     */\n    @Deprecated\n    default void onNotificationCancelled(int notificationId) {}\n\n    /**\n     * Called after the notification has been cancelled.\n     *\n     * @param notificationId The id of the notification which has been cancelled.\n     * @param dismissedByUser {@code true} if the notification is cancelled because the user\n     *     dismissed the notification.\n     */\n    default void onNotificationCancelled(int notificationId, boolean dismissedByUser) {}\n\n    /**\n     * Called each time after the notification has been posted.\n     *\n     * <p>For a service, the {@code ongoing} flag can be used as an indicator as to whether it\n     * should be in the foreground.\n     *\n     * @param notificationId The id of the notification which has been posted.\n     * @param notification The {@link Notification}.\n     * @param ongoing Whether the notification is ongoing.\n     */\n    default void onNotificationPosted(\n        int notificationId, Notification notification, boolean ongoing) {}\n  }\n\n  /** Receives a {@link Bitmap}. */\n  public final class BitmapCallback {\n    private final int notificationTag;\n\n    /** Create the receiver. */\n    private BitmapCallback(int notificationTag) {\n      this.notificationTag = notificationTag;\n    }\n\n    /**\n     * Called when {@link Bitmap} is available.\n     *\n     * @param bitmap The bitmap to use as the large icon of the notification.\n     */\n    public void onBitmap(final Bitmap bitmap) {\n      if (bitmap != null) {\n        mainHandler.post(\n            () -> {\n              if (player != null\n                  && notificationTag == currentNotificationTag\n                  && isNotificationStarted) {\n                startOrUpdateNotification(bitmap);\n              }\n            });\n      }\n    }\n  }\n\n  /** The action which starts playback. */\n  public static final String ACTION_PLAY = \"com.google.android.exoplayer.play\";\n  /** The action which pauses playback. */\n  public static final String ACTION_PAUSE = \"com.google.android.exoplayer.pause\";\n  /** The action which skips to the previous window. */\n  public static final String ACTION_PREVIOUS = \"com.google.android.exoplayer.prev\";\n  /** The action which skips to the next window. */\n  public static final String ACTION_NEXT = \"com.google.android.exoplayer.next\";\n  /** The action which fast forwards. */\n  public static final String ACTION_FAST_FORWARD = \"com.google.android.exoplayer.ffwd\";\n  /** The action which rewinds. */\n  public static final String ACTION_REWIND = \"com.google.android.exoplayer.rewind\";\n  /** The action which stops playback. */\n  public static final String ACTION_STOP = \"com.google.android.exoplayer.stop\";\n  /** The extra key of the instance id of the player notification manager. */\n  public static final String EXTRA_INSTANCE_ID = \"INSTANCE_ID\";\n  /**\n   * The action which is executed when the notification is dismissed. It cancels the notification\n   * and calls {@link NotificationListener#onNotificationCancelled(int, boolean)}.\n   */\n  private static final String ACTION_DISMISS = \"com.google.android.exoplayer.dismiss\";\n\n  /**\n   * Visibility of notification on the lock screen. One of {@link\n   * NotificationCompat#VISIBILITY_PRIVATE}, {@link NotificationCompat#VISIBILITY_PUBLIC} or {@link\n   * NotificationCompat#VISIBILITY_SECRET}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    NotificationCompat.VISIBILITY_PRIVATE,\n    NotificationCompat.VISIBILITY_PUBLIC,\n    NotificationCompat.VISIBILITY_SECRET\n  })\n  public @interface Visibility {}\n\n  /**\n   * Priority of the notification (required for API 25 and lower). One of {@link\n   * NotificationCompat#PRIORITY_DEFAULT}, {@link NotificationCompat#PRIORITY_MAX}, {@link\n   * NotificationCompat#PRIORITY_HIGH}, {@link NotificationCompat#PRIORITY_LOW }or {@link\n   * NotificationCompat#PRIORITY_MIN}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({\n    NotificationCompat.PRIORITY_DEFAULT,\n    NotificationCompat.PRIORITY_MAX,\n    NotificationCompat.PRIORITY_HIGH,\n    NotificationCompat.PRIORITY_LOW,\n    NotificationCompat.PRIORITY_MIN\n  })\n  public @interface Priority {}\n\n  /** The default fast forward increment, in milliseconds. */\n  public static final int DEFAULT_FAST_FORWARD_MS = 15000;\n  /** The default rewind increment, in milliseconds. */\n  public static final int DEFAULT_REWIND_MS = 5000;\n\n  private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000;\n\n  private static int instanceIdCounter;\n\n  private final Context context;\n  private final String channelId;\n  private final int notificationId;\n  private final MediaDescriptionAdapter mediaDescriptionAdapter;\n  @Nullable private final CustomActionReceiver customActionReceiver;\n  private final Handler mainHandler;\n  private final NotificationManagerCompat notificationManager;\n  private final IntentFilter intentFilter;\n  private final Player.EventListener playerListener;\n  private final NotificationBroadcastReceiver notificationBroadcastReceiver;\n  private final Map<String, NotificationCompat.Action> playbackActions;\n  private final Map<String, NotificationCompat.Action> customActions;\n  private final PendingIntent dismissPendingIntent;\n  private final int instanceId;\n  private final Timeline.Window window;\n\n  @Nullable private NotificationCompat.Builder builder;\n  @Nullable private ArrayList<NotificationCompat.Action> builderActions;\n  @Nullable private Player player;\n  @Nullable private PlaybackPreparer playbackPreparer;\n  private ControlDispatcher controlDispatcher;\n  private boolean isNotificationStarted;\n  private int currentNotificationTag;\n  @Nullable private NotificationListener notificationListener;\n  @Nullable private MediaSessionCompat.Token mediaSessionToken;\n  private boolean useNavigationActions;\n  private boolean useNavigationActionsInCompactView;\n  private boolean usePlayPauseActions;\n  private boolean useStopAction;\n  private long fastForwardMs;\n  private long rewindMs;\n  private int badgeIconType;\n  private boolean colorized;\n  private int defaults;\n  private int color;\n  @DrawableRes private int smallIconResourceId;\n  private int visibility;\n  @Priority private int priority;\n  private boolean useChronometer;\n\n  /**\n   * @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int,\n   *     MediaDescriptionAdapter)}.\n   */\n  @Deprecated\n  public static PlayerNotificationManager createWithNotificationChannel(\n      Context context,\n      String channelId,\n      @StringRes int channelName,\n      int notificationId,\n      MediaDescriptionAdapter mediaDescriptionAdapter) {\n    return createWithNotificationChannel(\n        context,\n        channelId,\n        channelName,\n        /* channelDescription= */ 0,\n        notificationId,\n        mediaDescriptionAdapter);\n  }\n\n  /**\n   * Creates a notification manager and a low-priority notification channel with the specified\n   * {@code channelId} and {@code channelName}.\n   *\n   * <p>If the player notification manager is intended to be used within a foreground service,\n   * {@link #createWithNotificationChannel(Context, String, int, int, MediaDescriptionAdapter,\n   * NotificationListener)} should be used to which a {@link NotificationListener} can be passed.\n   * This way you'll receive the notification to put the service into the foreground by calling\n   * {@link android.app.Service#startForeground(int, Notification)}.\n   *\n   * @param context The {@link Context}.\n   * @param channelId The id of the notification channel.\n   * @param channelName A string resource identifier for the user visible name of the notification\n   *     channel. The recommended maximum length is 40 characters. The string may be truncated if\n   *     it's too long.\n   * @param channelDescription A string resource identifier for the user visible description of the\n   *     notification channel, or 0 if no description is provided. The recommended maximum length is\n   *     300 characters. The value may be truncated if it is too long.\n   * @param notificationId The id of the notification.\n   * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}.\n   */\n  public static PlayerNotificationManager createWithNotificationChannel(\n      Context context,\n      String channelId,\n      @StringRes int channelName,\n      @StringRes int channelDescription,\n      int notificationId,\n      MediaDescriptionAdapter mediaDescriptionAdapter) {\n    NotificationUtil.createNotificationChannel(\n        context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW);\n    return new PlayerNotificationManager(\n        context, channelId, notificationId, mediaDescriptionAdapter);\n  }\n\n  /**\n   * @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int,\n   *     MediaDescriptionAdapter, NotificationListener)}.\n   */\n  @Deprecated\n  public static PlayerNotificationManager createWithNotificationChannel(\n      Context context,\n      String channelId,\n      @StringRes int channelName,\n      int notificationId,\n      MediaDescriptionAdapter mediaDescriptionAdapter,\n      @Nullable NotificationListener notificationListener) {\n    return createWithNotificationChannel(\n        context,\n        channelId,\n        channelName,\n        /* channelDescription= */ 0,\n        notificationId,\n        mediaDescriptionAdapter,\n        notificationListener);\n  }\n\n  /**\n   * Creates a notification manager and a low-priority notification channel with the specified\n   * {@code channelId} and {@code channelName}. The {@link NotificationListener} passed as the last\n   * parameter will be notified when the notification is created and cancelled.\n   *\n   * @param context The {@link Context}.\n   * @param channelId The id of the notification channel.\n   * @param channelName A string resource identifier for the user visible name of the channel. The\n   *     recommended maximum length is 40 characters. The string may be truncated if it's too long.\n   * @param channelDescription A string resource identifier for the user visible description of the\n   *     channel, or 0 if no description is provided.\n   * @param notificationId The id of the notification.\n   * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}.\n   * @param notificationListener The {@link NotificationListener}.\n   */\n  public static PlayerNotificationManager createWithNotificationChannel(\n      Context context,\n      String channelId,\n      @StringRes int channelName,\n      @StringRes int channelDescription,\n      int notificationId,\n      MediaDescriptionAdapter mediaDescriptionAdapter,\n      @Nullable NotificationListener notificationListener) {\n    NotificationUtil.createNotificationChannel(\n        context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW);\n    return new PlayerNotificationManager(\n        context, channelId, notificationId, mediaDescriptionAdapter, notificationListener);\n  }\n\n  /**\n   * Creates a notification manager using the specified notification {@code channelId}. The caller\n   * is responsible for creating the notification channel.\n   *\n   * <p>When used within a service, consider using {@link #PlayerNotificationManager(Context,\n   * String, int, MediaDescriptionAdapter, NotificationListener)} to which a {@link\n   * NotificationListener} can be passed.\n   *\n   * @param context The {@link Context}.\n   * @param channelId The id of the notification channel.\n   * @param notificationId The id of the notification.\n   * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}.\n   */\n  public PlayerNotificationManager(\n      Context context,\n      String channelId,\n      int notificationId,\n      MediaDescriptionAdapter mediaDescriptionAdapter) {\n    this(\n        context,\n        channelId,\n        notificationId,\n        mediaDescriptionAdapter,\n        /* notificationListener= */ null,\n        /* customActionReceiver */ null);\n  }\n\n  /**\n   * Creates a notification manager using the specified notification {@code channelId} and {@link\n   * NotificationListener}. The caller is responsible for creating the notification channel.\n   *\n   * @param context The {@link Context}.\n   * @param channelId The id of the notification channel.\n   * @param notificationId The id of the notification.\n   * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}.\n   * @param notificationListener The {@link NotificationListener}.\n   */\n  public PlayerNotificationManager(\n      Context context,\n      String channelId,\n      int notificationId,\n      MediaDescriptionAdapter mediaDescriptionAdapter,\n      @Nullable NotificationListener notificationListener) {\n    this(\n        context,\n        channelId,\n        notificationId,\n        mediaDescriptionAdapter,\n        notificationListener,\n        /* customActionReceiver*/ null);\n  }\n\n  /**\n   * Creates a notification manager using the specified notification {@code channelId} and {@link\n   * CustomActionReceiver}. The caller is responsible for creating the notification channel.\n   *\n   * <p>When used within a service, consider using {@link #PlayerNotificationManager(Context,\n   * String, int, MediaDescriptionAdapter, NotificationListener, CustomActionReceiver)} to which a\n   * {@link NotificationListener} can be passed.\n   *\n   * @param context The {@link Context}.\n   * @param channelId The id of the notification channel.\n   * @param notificationId The id of the notification.\n   * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}.\n   * @param customActionReceiver The {@link CustomActionReceiver}.\n   */\n  public PlayerNotificationManager(\n      Context context,\n      String channelId,\n      int notificationId,\n      MediaDescriptionAdapter mediaDescriptionAdapter,\n      @Nullable CustomActionReceiver customActionReceiver) {\n    this(\n        context,\n        channelId,\n        notificationId,\n        mediaDescriptionAdapter,\n        /* notificationListener */ null,\n        customActionReceiver);\n  }\n\n  /**\n   * Creates a notification manager using the specified notification {@code channelId}, {@link\n   * NotificationListener} and {@link CustomActionReceiver}. The caller is responsible for creating\n   * the notification channel.\n   *\n   * @param context The {@link Context}.\n   * @param channelId The id of the notification channel.\n   * @param notificationId The id of the notification.\n   * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}.\n   * @param notificationListener The {@link NotificationListener}.\n   * @param customActionReceiver The {@link CustomActionReceiver}.\n   */\n  public PlayerNotificationManager(\n      Context context,\n      String channelId,\n      int notificationId,\n      MediaDescriptionAdapter mediaDescriptionAdapter,\n      @Nullable NotificationListener notificationListener,\n      @Nullable CustomActionReceiver customActionReceiver) {\n    context = context.getApplicationContext();\n    this.context = context;\n    this.channelId = channelId;\n    this.notificationId = notificationId;\n    this.mediaDescriptionAdapter = mediaDescriptionAdapter;\n    this.notificationListener = notificationListener;\n    this.customActionReceiver = customActionReceiver;\n    controlDispatcher = new DefaultControlDispatcher();\n    window = new Timeline.Window();\n    instanceId = instanceIdCounter++;\n    mainHandler = new Handler(Looper.getMainLooper());\n    notificationManager = NotificationManagerCompat.from(context);\n    playerListener = new PlayerListener();\n    notificationBroadcastReceiver = new NotificationBroadcastReceiver();\n    intentFilter = new IntentFilter();\n    useNavigationActions = true;\n    usePlayPauseActions = true;\n    colorized = true;\n    useChronometer = true;\n    color = Color.TRANSPARENT;\n    smallIconResourceId = R.drawable.exo_notification_small_icon;\n    defaults = 0;\n    priority = NotificationCompat.PRIORITY_LOW;\n    fastForwardMs = DEFAULT_FAST_FORWARD_MS;\n    rewindMs = DEFAULT_REWIND_MS;\n    badgeIconType = NotificationCompat.BADGE_ICON_SMALL;\n    visibility = NotificationCompat.VISIBILITY_PUBLIC;\n\n    // initialize actions\n    playbackActions = createPlaybackActions(context, instanceId);\n    for (String action : playbackActions.keySet()) {\n      intentFilter.addAction(action);\n    }\n    customActions =\n        customActionReceiver != null\n            ? customActionReceiver.createCustomActions(context, instanceId)\n            : Collections.emptyMap();\n    for (String action : customActions.keySet()) {\n      intentFilter.addAction(action);\n    }\n    dismissPendingIntent = createBroadcastIntent(ACTION_DISMISS, context, instanceId);\n    intentFilter.addAction(ACTION_DISMISS);\n  }\n\n  /**\n   * Sets the {@link Player}.\n   *\n   * <p>Setting the player starts a notification immediately unless the player is in {@link\n   * Player#STATE_IDLE}, in which case the notification is started as soon as the player transitions\n   * away from being idle.\n   *\n   * <p>If the player is released it must be removed from the manager by calling {@code\n   * setPlayer(null)}. This will cancel the notification.\n   *\n   * @param player The {@link Player} to use, or {@code null} to remove the current player. Only\n   *     players which are accessed on the main thread are supported ({@code\n   *     player.getApplicationLooper() == Looper.getMainLooper()}).\n   */\n  public final void setPlayer(@Nullable Player player) {\n    Assertions.checkState(Looper.myLooper() == Looper.getMainLooper());\n    Assertions.checkArgument(\n        player == null || player.getApplicationLooper() == Looper.getMainLooper());\n    if (this.player == player) {\n      return;\n    }\n    if (this.player != null) {\n      this.player.removeListener(playerListener);\n      if (player == null) {\n        stopNotification(/* dismissedByUser= */ false);\n      }\n    }\n    this.player = player;\n    if (player != null) {\n      player.addListener(playerListener);\n      startOrUpdateNotification();\n    }\n  }\n\n  /**\n   * Sets the {@link PlaybackPreparer}.\n   *\n   * @param playbackPreparer The {@link PlaybackPreparer}.\n   */\n  public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {\n    this.playbackPreparer = playbackPreparer;\n  }\n\n  /**\n   * Sets the {@link ControlDispatcher}.\n   *\n   * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link\n   *     DefaultControlDispatcher}.\n   */\n  public final void setControlDispatcher(ControlDispatcher controlDispatcher) {\n    this.controlDispatcher =\n        controlDispatcher != null ? controlDispatcher : new DefaultControlDispatcher();\n  }\n\n  /**\n   * Sets the {@link NotificationListener}.\n   *\n   * <p>Please note that you should call this method before you call {@link #setPlayer(Player)} or\n   * you may not get the {@link NotificationListener#onNotificationStarted(int, Notification)}\n   * called on your listener.\n   *\n   * @param notificationListener The {@link NotificationListener}.\n   * @deprecated Pass the notification listener to the constructor instead.\n   */\n  @Deprecated\n  public final void setNotificationListener(NotificationListener notificationListener) {\n    this.notificationListener = notificationListener;\n  }\n\n  /**\n   * Sets the fast forward increment in milliseconds.\n   *\n   * @param fastForwardMs The fast forward increment in milliseconds. A value of zero will cause the\n   *     fast forward action to be disabled.\n   */\n  public final void setFastForwardIncrementMs(long fastForwardMs) {\n    if (this.fastForwardMs == fastForwardMs) {\n      return;\n    }\n    this.fastForwardMs = fastForwardMs;\n    invalidate();\n  }\n\n  /**\n   * Sets the rewind increment in milliseconds.\n   *\n   * @param rewindMs The rewind increment in milliseconds. A value of zero will cause the rewind\n   *     action to be disabled.\n   */\n  public final void setRewindIncrementMs(long rewindMs) {\n    if (this.rewindMs == rewindMs) {\n      return;\n    }\n    this.rewindMs = rewindMs;\n    invalidate();\n  }\n\n  /**\n   * Sets whether the navigation actions should be used.\n   *\n   * @param useNavigationActions Whether to use navigation actions or not.\n   */\n  public final void setUseNavigationActions(boolean useNavigationActions) {\n    if (this.useNavigationActions != useNavigationActions) {\n      this.useNavigationActions = useNavigationActions;\n      invalidate();\n    }\n  }\n\n  /**\n   * Sets whether navigation actions should be displayed in compact view.\n   *\n   * <p>If {@link #useNavigationActions} is set to {@code false} navigation actions are displayed\n   * neither in compact nor in full view mode of the notification.\n   *\n   * @param useNavigationActionsInCompactView Whether the navigation actions should be displayed in\n   *     compact view.\n   */\n  public final void setUseNavigationActionsInCompactView(\n      boolean useNavigationActionsInCompactView) {\n    if (this.useNavigationActionsInCompactView != useNavigationActionsInCompactView) {\n      this.useNavigationActionsInCompactView = useNavigationActionsInCompactView;\n      invalidate();\n    }\n  }\n\n  /**\n   * Sets whether the play and pause actions should be used.\n   *\n   * @param usePlayPauseActions Whether to use play and pause actions.\n   */\n  public final void setUsePlayPauseActions(boolean usePlayPauseActions) {\n    if (this.usePlayPauseActions != usePlayPauseActions) {\n      this.usePlayPauseActions = usePlayPauseActions;\n      invalidate();\n    }\n  }\n\n  /**\n   * Sets whether the stop action should be used.\n   *\n   * @param useStopAction Whether to use the stop action.\n   */\n  public final void setUseStopAction(boolean useStopAction) {\n    if (this.useStopAction == useStopAction) {\n      return;\n    }\n    this.useStopAction = useStopAction;\n    invalidate();\n  }\n\n  /**\n   * Sets the {@link MediaSessionCompat.Token}.\n   *\n   * @param token The {@link MediaSessionCompat.Token}.\n   */\n  public final void setMediaSessionToken(MediaSessionCompat.Token token) {\n    if (!Util.areEqual(this.mediaSessionToken, token)) {\n      mediaSessionToken = token;\n      invalidate();\n    }\n  }\n\n  /**\n   * Sets the badge icon type of the notification.\n   *\n   * <p>See {@link NotificationCompat.Builder#setBadgeIconType(int)}.\n   *\n   * @param badgeIconType The badge icon type.\n   */\n  public final void setBadgeIconType(@NotificationCompat.BadgeIconType int badgeIconType) {\n    if (this.badgeIconType == badgeIconType) {\n      return;\n    }\n    switch (badgeIconType) {\n      case NotificationCompat.BADGE_ICON_NONE:\n      case NotificationCompat.BADGE_ICON_SMALL:\n      case NotificationCompat.BADGE_ICON_LARGE:\n        this.badgeIconType = badgeIconType;\n        break;\n      default:\n        throw new IllegalArgumentException();\n    }\n    invalidate();\n  }\n\n  /**\n   * Sets whether the notification should be colorized. When set, the color set with {@link\n   * #setColor(int)} will be used as the background color for the notification.\n   *\n   * <p>See {@link NotificationCompat.Builder#setColorized(boolean)}.\n   *\n   * @param colorized Whether to colorize the notification.\n   */\n  public final void setColorized(boolean colorized) {\n    if (this.colorized != colorized) {\n      this.colorized = colorized;\n      invalidate();\n    }\n  }\n\n  /**\n   * Sets the defaults.\n   *\n   * <p>See {@link NotificationCompat.Builder#setDefaults(int)}.\n   *\n   * @param defaults The default notification options.\n   */\n  public final void setDefaults(int defaults) {\n    if (this.defaults != defaults) {\n      this.defaults = defaults;\n      invalidate();\n    }\n  }\n\n  /**\n   * Sets the accent color of the notification.\n   *\n   * <p>See {@link NotificationCompat.Builder#setColor(int)}.\n   *\n   * @param color The color, in ARGB integer form like the constants in {@link Color}.\n   */\n  public final void setColor(int color) {\n    if (this.color != color) {\n      this.color = color;\n      invalidate();\n    }\n  }\n\n  /**\n   * Sets the priority of the notification required for API 25 and lower.\n   *\n   * <p>See {@link NotificationCompat.Builder#setPriority(int)}.\n   *\n   * @param priority The priority which can be one of {@link NotificationCompat#PRIORITY_DEFAULT},\n   *     {@link NotificationCompat#PRIORITY_MAX}, {@link NotificationCompat#PRIORITY_HIGH}, {@link\n   *     NotificationCompat#PRIORITY_LOW} or {@link NotificationCompat#PRIORITY_MIN}. If not set\n   *     {@link NotificationCompat#PRIORITY_LOW} is used by default.\n   */\n  public final void setPriority(@Priority int priority) {\n    if (this.priority == priority) {\n      return;\n    }\n    switch (priority) {\n      case NotificationCompat.PRIORITY_DEFAULT:\n      case NotificationCompat.PRIORITY_MAX:\n      case NotificationCompat.PRIORITY_HIGH:\n      case NotificationCompat.PRIORITY_LOW:\n      case NotificationCompat.PRIORITY_MIN:\n        this.priority = priority;\n        break;\n      default:\n        throw new IllegalArgumentException();\n    }\n    invalidate();\n  }\n\n  /**\n   * Sets the small icon of the notification which is also shown in the system status bar.\n   *\n   * <p>See {@link NotificationCompat.Builder#setSmallIcon(int)}.\n   *\n   * @param smallIconResourceId The resource id of the small icon.\n   */\n  public final void setSmallIcon(@DrawableRes int smallIconResourceId) {\n    if (this.smallIconResourceId != smallIconResourceId) {\n      this.smallIconResourceId = smallIconResourceId;\n      invalidate();\n    }\n  }\n\n  /**\n   * Sets whether the elapsed time of the media playback should be displayed\n   *\n   * <p>See {@link NotificationCompat.Builder#setUsesChronometer(boolean)}.\n   *\n   * @param useChronometer Whether to use chronometer.\n   */\n  public final void setUseChronometer(boolean useChronometer) {\n    if (this.useChronometer != useChronometer) {\n      this.useChronometer = useChronometer;\n      invalidate();\n    }\n  }\n\n  /**\n   * Sets the visibility of the notification which determines whether and how the notification is\n   * shown when the device is in lock screen mode.\n   *\n   * <p>See {@link NotificationCompat.Builder#setVisibility(int)}.\n   *\n   * @param visibility The visibility which must be one of {@link\n   *     NotificationCompat#VISIBILITY_PUBLIC}, {@link NotificationCompat#VISIBILITY_PRIVATE} or\n   *     {@link NotificationCompat#VISIBILITY_SECRET}.\n   */\n  public final void setVisibility(@Visibility int visibility) {\n    if (this.visibility == visibility) {\n      return;\n    }\n    switch (visibility) {\n      case NotificationCompat.VISIBILITY_PRIVATE:\n      case NotificationCompat.VISIBILITY_PUBLIC:\n      case NotificationCompat.VISIBILITY_SECRET:\n        this.visibility = visibility;\n        break;\n      default:\n        throw new IllegalStateException();\n    }\n    invalidate();\n  }\n\n  /** Forces an update of the notification if already started. */\n  public void invalidate() {\n    if (isNotificationStarted && player != null) {\n      startOrUpdateNotification();\n    }\n  }\n\n  @Nullable\n  private Notification startOrUpdateNotification() {\n    Assertions.checkNotNull(this.player);\n    return startOrUpdateNotification(/* bitmap= */ null);\n  }\n\n  @RequiresNonNull(\"player\")\n  @Nullable\n  private Notification startOrUpdateNotification(@Nullable Bitmap bitmap) {\n    Player player = this.player;\n    boolean ongoing = getOngoing(player);\n    builder = createNotification(player, builder, ongoing, bitmap);\n    if (builder == null) {\n      stopNotification(/* dismissedByUser= */ false);\n      return null;\n    }\n    Notification notification = builder.build();\n    notificationManager.notify(notificationId, notification);\n    if (!isNotificationStarted) {\n      isNotificationStarted = true;\n      context.registerReceiver(notificationBroadcastReceiver, intentFilter);\n      if (notificationListener != null) {\n        notificationListener.onNotificationStarted(notificationId, notification);\n      }\n    }\n    NotificationListener listener = notificationListener;\n    if (listener != null) {\n      listener.onNotificationPosted(notificationId, notification, ongoing);\n    }\n    return notification;\n  }\n\n  private void stopNotification(boolean dismissedByUser) {\n    if (isNotificationStarted) {\n      isNotificationStarted = false;\n      notificationManager.cancel(notificationId);\n      context.unregisterReceiver(notificationBroadcastReceiver);\n      if (notificationListener != null) {\n        notificationListener.onNotificationCancelled(notificationId, dismissedByUser);\n        notificationListener.onNotificationCancelled(notificationId);\n      }\n    }\n  }\n\n  /**\n   * Creates the notification given the current player state.\n   *\n   * @param player The player for which state to build a notification.\n   * @param builder The builder used to build the last notification, or {@code null}. Re-using the\n   *     builder when possible can prevent notification flicker when {@code Util#SDK_INT} &lt; 21.\n   * @param ongoing Whether the notification should be ongoing.\n   * @param largeIcon The large icon to be used.\n   * @return The {@link NotificationCompat.Builder} on which to call {@link\n   *     NotificationCompat.Builder#build()} to obtain the notification, or {@code null} if no\n   *     notification should be displayed.\n   */\n  @Nullable\n  protected NotificationCompat.Builder createNotification(\n      Player player,\n      @Nullable NotificationCompat.Builder builder,\n      boolean ongoing,\n      @Nullable Bitmap largeIcon) {\n    if (player.getPlaybackState() == Player.STATE_IDLE\n        && (player.getCurrentTimeline().isEmpty() || playbackPreparer == null)) {\n      builderActions = null;\n      return null;\n    }\n\n    List<String> actionNames = getActions(player);\n    ArrayList<NotificationCompat.Action> actions = new ArrayList<>(actionNames.size());\n    for (int i = 0; i < actionNames.size(); i++) {\n      String actionName = actionNames.get(i);\n      NotificationCompat.Action action =\n          playbackActions.containsKey(actionName)\n              ? playbackActions.get(actionName)\n              : customActions.get(actionName);\n      if (action != null) {\n        actions.add(action);\n      }\n    }\n\n    if (builder == null || !actions.equals(builderActions)) {\n      builder = new NotificationCompat.Builder(context, channelId);\n      builderActions = actions;\n      for (int i = 0; i < actions.size(); i++) {\n        builder.addAction(actions.get(i));\n      }\n    }\n\n    MediaStyle mediaStyle = new MediaStyle();\n    if (mediaSessionToken != null) {\n      mediaStyle.setMediaSession(mediaSessionToken);\n    }\n    mediaStyle.setShowActionsInCompactView(getActionIndicesForCompactView(actionNames, player));\n    // Configure dismiss action prior to API 21 ('x' button).\n    mediaStyle.setShowCancelButton(!ongoing);\n    mediaStyle.setCancelButtonIntent(dismissPendingIntent);\n    builder.setStyle(mediaStyle);\n\n    // Set intent which is sent if the user selects 'clear all'\n    builder.setDeleteIntent(dismissPendingIntent);\n\n    // Set notification properties from getters.\n    builder\n        .setBadgeIconType(badgeIconType)\n        .setOngoing(ongoing)\n        .setColor(color)\n        .setColorized(colorized)\n        .setSmallIcon(smallIconResourceId)\n        .setVisibility(visibility)\n        .setPriority(priority)\n        .setDefaults(defaults);\n\n    // Changing \"showWhen\" causes notification flicker if SDK_INT < 21.\n    if (Util.SDK_INT >= 21\n        && useChronometer\n        && player.isPlaying()\n        && !player.isPlayingAd()\n        && !player.isCurrentWindowDynamic()) {\n      builder\n          .setWhen(System.currentTimeMillis() - player.getContentPosition())\n          .setShowWhen(true)\n          .setUsesChronometer(true);\n    } else {\n      builder.setShowWhen(false).setUsesChronometer(false);\n    }\n\n    // Set media specific notification properties from MediaDescriptionAdapter.\n    builder.setContentTitle(mediaDescriptionAdapter.getCurrentContentTitle(player));\n    builder.setContentText(mediaDescriptionAdapter.getCurrentContentText(player));\n    builder.setSubText(mediaDescriptionAdapter.getCurrentSubText(player));\n    if (largeIcon == null) {\n      largeIcon =\n          mediaDescriptionAdapter.getCurrentLargeIcon(\n              player, new BitmapCallback(++currentNotificationTag));\n    }\n    setLargeIcon(builder, largeIcon);\n    builder.setContentIntent(mediaDescriptionAdapter.createCurrentContentIntent(player));\n\n    return builder;\n  }\n\n  /**\n   * Gets the names and order of the actions to be included in the notification at the current\n   * player state.\n   *\n   * <p>The playback and custom actions are combined and placed in the following order if not\n   * omitted:\n   *\n   * <pre>\n   *   +------------------------------------------------------------------------+\n   *   | prev | &lt;&lt; | play/pause | &gt;&gt; | next | custom actions | stop |\n   *   +------------------------------------------------------------------------+\n   * </pre>\n   *\n   * <p>This method can be safely overridden. However, the names must be of the playback actions\n   * {@link #ACTION_PAUSE}, {@link #ACTION_PLAY}, {@link #ACTION_FAST_FORWARD}, {@link\n   * #ACTION_REWIND}, {@link #ACTION_NEXT} or {@link #ACTION_PREVIOUS}, or a key contained in the\n   * map returned by {@link CustomActionReceiver#createCustomActions(Context, int)}. Otherwise the\n   * action name is ignored.\n   */\n  protected List<String> getActions(Player player) {\n    boolean enablePrevious = false;\n    boolean enableRewind = false;\n    boolean enableFastForward = false;\n    boolean enableNext = false;\n    Timeline timeline = player.getCurrentTimeline();\n    if (!timeline.isEmpty() && !player.isPlayingAd()) {\n      timeline.getWindow(player.getCurrentWindowIndex(), window);\n      enablePrevious = window.isSeekable || !window.isDynamic || player.hasPrevious();\n      enableRewind = rewindMs > 0;\n      enableFastForward = fastForwardMs > 0;\n      enableNext = window.isDynamic || player.hasNext();\n    }\n\n    List<String> stringActions = new ArrayList<>();\n    if (useNavigationActions && enablePrevious) {\n      stringActions.add(ACTION_PREVIOUS);\n    }\n    if (enableRewind) {\n      stringActions.add(ACTION_REWIND);\n    }\n    if (usePlayPauseActions) {\n      if (shouldShowPauseButton(player)) {\n        stringActions.add(ACTION_PAUSE);\n      } else {\n        stringActions.add(ACTION_PLAY);\n      }\n    }\n    if (enableFastForward) {\n      stringActions.add(ACTION_FAST_FORWARD);\n    }\n    if (useNavigationActions && enableNext) {\n      stringActions.add(ACTION_NEXT);\n    }\n    if (customActionReceiver != null) {\n      stringActions.addAll(customActionReceiver.getCustomActions(player));\n    }\n    if (useStopAction) {\n      stringActions.add(ACTION_STOP);\n    }\n    return stringActions;\n  }\n\n  /**\n   * Gets an array with the indices of the buttons to be shown in compact mode.\n   *\n   * <p>This method can be overridden. The indices must refer to the list of actions passed as the\n   * first parameter.\n   *\n   * @param actionNames The names of the actions included in the notification.\n   * @param player The player for which a notification is being built.\n   */\n  @SuppressWarnings(\"unused\")\n  protected int[] getActionIndicesForCompactView(List<String> actionNames, Player player) {\n    int pauseActionIndex = actionNames.indexOf(ACTION_PAUSE);\n    int playActionIndex = actionNames.indexOf(ACTION_PLAY);\n    int skipPreviousActionIndex =\n        useNavigationActionsInCompactView ? actionNames.indexOf(ACTION_PREVIOUS) : -1;\n    int skipNextActionIndex =\n        useNavigationActionsInCompactView ? actionNames.indexOf(ACTION_NEXT) : -1;\n\n    int[] actionIndices = new int[3];\n    int actionCounter = 0;\n    if (skipPreviousActionIndex != -1) {\n      actionIndices[actionCounter++] = skipPreviousActionIndex;\n    }\n    boolean shouldShowPauseButton = shouldShowPauseButton(player);\n    if (pauseActionIndex != -1 && shouldShowPauseButton) {\n      actionIndices[actionCounter++] = pauseActionIndex;\n    } else if (playActionIndex != -1 && !shouldShowPauseButton) {\n      actionIndices[actionCounter++] = playActionIndex;\n    }\n    if (skipNextActionIndex != -1) {\n      actionIndices[actionCounter++] = skipNextActionIndex;\n    }\n    return Arrays.copyOf(actionIndices, actionCounter);\n  }\n\n  /** Returns whether the generated notification should be ongoing. */\n  protected boolean getOngoing(Player player) {\n    int playbackState = player.getPlaybackState();\n    return (playbackState == Player.STATE_BUFFERING || playbackState == Player.STATE_READY)\n        && player.getPlayWhenReady();\n  }\n\n  private void previous(Player player) {\n    Timeline timeline = player.getCurrentTimeline();\n    if (timeline.isEmpty() || player.isPlayingAd()) {\n      return;\n    }\n    int windowIndex = player.getCurrentWindowIndex();\n    timeline.getWindow(windowIndex, window);\n    int previousWindowIndex = player.getPreviousWindowIndex();\n    if (previousWindowIndex != C.INDEX_UNSET\n        && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS\n            || (window.isDynamic && !window.isSeekable))) {\n      seekTo(player, previousWindowIndex, C.TIME_UNSET);\n    } else {\n      seekTo(player, windowIndex, /* positionMs= */ 0);\n    }\n  }\n\n  private void next(Player player) {\n    Timeline timeline = player.getCurrentTimeline();\n    if (timeline.isEmpty() || player.isPlayingAd()) {\n      return;\n    }\n    int windowIndex = player.getCurrentWindowIndex();\n    int nextWindowIndex = player.getNextWindowIndex();\n    if (nextWindowIndex != C.INDEX_UNSET) {\n      seekTo(player, nextWindowIndex, C.TIME_UNSET);\n    } else if (timeline.getWindow(windowIndex, window).isDynamic) {\n      seekTo(player, windowIndex, C.TIME_UNSET);\n    }\n  }\n\n  private void rewind(Player player) {\n    if (player.isCurrentWindowSeekable() && rewindMs > 0) {\n      seekToOffset(player, /* offsetMs= */ -rewindMs);\n    }\n  }\n\n  private void fastForward(Player player) {\n    if (player.isCurrentWindowSeekable() && fastForwardMs > 0) {\n      seekToOffset(player, /* offsetMs= */ fastForwardMs);\n    }\n  }\n\n  private void seekToOffset(Player player, long offsetMs) {\n    long positionMs = player.getCurrentPosition() + offsetMs;\n    long durationMs = player.getDuration();\n    if (durationMs != C.TIME_UNSET) {\n      positionMs = Math.min(positionMs, durationMs);\n    }\n    positionMs = Math.max(positionMs, 0);\n    seekTo(player, player.getCurrentWindowIndex(), positionMs);\n  }\n\n  private void seekTo(Player player, int windowIndex, long positionMs) {\n    controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);\n  }\n\n  private boolean shouldShowPauseButton(Player player) {\n    return player.getPlaybackState() != Player.STATE_ENDED\n        && player.getPlaybackState() != Player.STATE_IDLE\n        && player.getPlayWhenReady();\n  }\n\n  private static Map<String, NotificationCompat.Action> createPlaybackActions(\n      Context context, int instanceId) {\n    Map<String, NotificationCompat.Action> actions = new HashMap<>();\n    actions.put(\n        ACTION_PLAY,\n        new NotificationCompat.Action(\n            R.drawable.exo_notification_play,\n            context.getString(R.string.exo_controls_play_description),\n            createBroadcastIntent(ACTION_PLAY, context, instanceId)));\n    actions.put(\n        ACTION_PAUSE,\n        new NotificationCompat.Action(\n            R.drawable.exo_notification_pause,\n            context.getString(R.string.exo_controls_pause_description),\n            createBroadcastIntent(ACTION_PAUSE, context, instanceId)));\n    actions.put(\n        ACTION_STOP,\n        new NotificationCompat.Action(\n            R.drawable.exo_notification_stop,\n            context.getString(R.string.exo_controls_stop_description),\n            createBroadcastIntent(ACTION_STOP, context, instanceId)));\n    actions.put(\n        ACTION_REWIND,\n        new NotificationCompat.Action(\n            R.drawable.exo_notification_rewind,\n            context.getString(R.string.exo_controls_rewind_description),\n            createBroadcastIntent(ACTION_REWIND, context, instanceId)));\n    actions.put(\n        ACTION_FAST_FORWARD,\n        new NotificationCompat.Action(\n            R.drawable.exo_notification_fastforward,\n            context.getString(R.string.exo_controls_fastforward_description),\n            createBroadcastIntent(ACTION_FAST_FORWARD, context, instanceId)));\n    actions.put(\n        ACTION_PREVIOUS,\n        new NotificationCompat.Action(\n            R.drawable.exo_notification_previous,\n            context.getString(R.string.exo_controls_previous_description),\n            createBroadcastIntent(ACTION_PREVIOUS, context, instanceId)));\n    actions.put(\n        ACTION_NEXT,\n        new NotificationCompat.Action(\n            R.drawable.exo_notification_next,\n            context.getString(R.string.exo_controls_next_description),\n            createBroadcastIntent(ACTION_NEXT, context, instanceId)));\n    return actions;\n  }\n\n  private static PendingIntent createBroadcastIntent(\n      String action, Context context, int instanceId) {\n    Intent intent = new Intent(action).setPackage(context.getPackageName());\n    intent.putExtra(EXTRA_INSTANCE_ID, instanceId);\n    // MOD: fix crashes on api >= 23\n    int flags = PendingIntent.FLAG_UPDATE_CURRENT;\n    if (Build.VERSION.SDK_INT >= 23) {\n      // IllegalArgumentException fix: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE...\n      flags |= PendingIntent.FLAG_IMMUTABLE;\n    }\n    return PendingIntent.getBroadcast(\n        context, instanceId, intent, flags);\n  }\n\n  @SuppressWarnings(\"nullness:argument.type.incompatible\")\n  private static void setLargeIcon(NotificationCompat.Builder builder, @Nullable Bitmap largeIcon) {\n    builder.setLargeIcon(largeIcon);\n  }\n\n  private class PlayerListener implements Player.EventListener {\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n      startOrUpdateNotification();\n    }\n\n    @Override\n    public void onIsPlayingChanged(boolean isPlaying) {\n      startOrUpdateNotification();\n    }\n\n    @Override\n    public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {\n      startOrUpdateNotification();\n    }\n\n    @Override\n    public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {\n      startOrUpdateNotification();\n    }\n\n    @Override\n    public void onPositionDiscontinuity(int reason) {\n      startOrUpdateNotification();\n    }\n\n    @Override\n    public void onRepeatModeChanged(int repeatMode) {\n      startOrUpdateNotification();\n    }\n  }\n\n  private class NotificationBroadcastReceiver extends BroadcastReceiver {\n\n    @Override\n    public void onReceive(Context context, Intent intent) {\n      Player player = PlayerNotificationManager.this.player;\n      if (player == null\n          || !isNotificationStarted\n          || intent.getIntExtra(EXTRA_INSTANCE_ID, instanceId) != instanceId) {\n        return;\n      }\n      String action = intent.getAction();\n      if (ACTION_PLAY.equals(action)) {\n        if (player.getPlaybackState() == Player.STATE_IDLE) {\n          if (playbackPreparer != null) {\n            playbackPreparer.preparePlayback();\n          }\n        } else if (player.getPlaybackState() == Player.STATE_ENDED) {\n          seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);\n        }\n        controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true);\n      } else if (ACTION_PAUSE.equals(action)) {\n        controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false);\n      } else if (ACTION_PREVIOUS.equals(action)) {\n        previous(player);\n      } else if (ACTION_REWIND.equals(action)) {\n        rewind(player);\n      } else if (ACTION_FAST_FORWARD.equals(action)) {\n        fastForward(player);\n      } else if (ACTION_NEXT.equals(action)) {\n        next(player);\n      } else if (ACTION_STOP.equals(action)) {\n        controlDispatcher.dispatchStop(player, /* reset= */ true);\n      } else if (ACTION_DISMISS.equals(action)) {\n        stopNotification(/* dismissedByUser= */ true);\n      } else if (action != null\n          && customActionReceiver != null\n          && customActions.containsKey(action)) {\n        customActionReceiver.onCustomAction(player, action, intent);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ui;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.res.Resources;\nimport android.content.res.TypedArray;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Matrix;\nimport android.graphics.RectF;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.os.Looper;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.Nullable;\nimport androidx.core.content.ContextCompat;\nimport android.util.AttributeSet;\nimport android.view.KeyEvent;\nimport android.view.LayoutInflater;\nimport android.view.MotionEvent;\nimport android.view.SurfaceView;\nimport android.view.TextureView;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\nimport android.widget.ImageView;\nimport android.widget.TextView;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ControlDispatcher;\nimport com.google.android.exoplayer2.DefaultControlDispatcher;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.PlaybackPreparer;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Player.DiscontinuityReason;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.metadata.flac.PictureFrame;\nimport com.google.android.exoplayer2.metadata.id3.ApicFrame;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.source.ads.AdsLoader;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.TextOutput;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;\nimport com.google.android.exoplayer2.ui.spherical.SingleTapListener;\nimport com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.ErrorMessageProvider;\nimport com.google.android.exoplayer2.util.RepeatModeUtil;\nimport com.google.android.exoplayer2.util.Util;\nimport com.google.android.exoplayer2.video.VideoListener;\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * A high level view for {@link Player} media playbacks. It displays video, subtitles and album art\n * during playback, and displays playback controls using a {@link PlayerControlView}.\n *\n * <p>A PlayerView can be customized by setting attributes (or calling corresponding methods),\n * overriding the view's layout file or by specifying a custom view layout file, as outlined below.\n *\n * <h3>Attributes</h3>\n *\n * The following attributes can be set on a PlayerView when used in a layout XML file:\n *\n * <ul>\n *   <li><b>{@code use_artwork}</b> - Whether artwork is used if available in audio streams.\n *       <ul>\n *         <li>Corresponding method: {@link #setUseArtwork(boolean)}\n *         <li>Default: {@code true}\n *       </ul>\n *   <li><b>{@code default_artwork}</b> - Default artwork to use if no artwork available in audio\n *       streams.\n *       <ul>\n *         <li>Corresponding method: {@link #setDefaultArtwork(Drawable)}\n *         <li>Default: {@code null}\n *       </ul>\n *   <li><b>{@code use_controller}</b> - Whether the playback controls can be shown.\n *       <ul>\n *         <li>Corresponding method: {@link #setUseController(boolean)}\n *         <li>Default: {@code true}\n *       </ul>\n *   <li><b>{@code hide_on_touch}</b> - Whether the playback controls are hidden by touch events.\n *       <ul>\n *         <li>Corresponding method: {@link #setControllerHideOnTouch(boolean)}\n *         <li>Default: {@code true}\n *       </ul>\n *   <li><b>{@code auto_show}</b> - Whether the playback controls are automatically shown when\n *       playback starts, pauses, ends, or fails. If set to false, the playback controls can be\n *       manually operated with {@link #showController()} and {@link #hideController()}.\n *       <ul>\n *         <li>Corresponding method: {@link #setControllerAutoShow(boolean)}\n *         <li>Default: {@code true}\n *       </ul>\n *   <li><b>{@code hide_during_ads}</b> - Whether the playback controls are hidden during ads.\n *       Controls are always shown during ads if they are enabled and the player is paused.\n *       <ul>\n *         <li>Corresponding method: {@link #setControllerHideDuringAds(boolean)}\n *         <li>Default: {@code true}\n *       </ul>\n *   <li><b>{@code show_buffering}</b> - Whether the buffering spinner is displayed when the player\n *       is buffering. Valid values are {@code never}, {@code when_playing} and {@code always}.\n *       <ul>\n *         <li>Corresponding method: {@link #setShowBuffering(int)}\n *         <li>Default: {@code never}\n *       </ul>\n *   <li><b>{@code resize_mode}</b> - Controls how video and album art is resized within the view.\n *       Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}.\n *       <ul>\n *         <li>Corresponding method: {@link #setResizeMode(int)}\n *         <li>Default: {@code fit}\n *       </ul>\n *   <li><b>{@code surface_type}</b> - The type of surface view used for video playbacks. Valid\n *       values are {@code surface_view}, {@code texture_view}, {@code spherical_view} and {@code\n *       none}. Using {@code none} is recommended for audio only applications, since creating the\n *       surface can be expensive. Using {@code surface_view} is recommended for video applications.\n *       Note, TextureView can only be used in a hardware accelerated window. When rendered in\n *       software, TextureView will draw nothing.\n *       <ul>\n *         <li>Corresponding method: None\n *         <li>Default: {@code surface_view}\n *       </ul>\n *   <li><b>{@code shutter_background_color}</b> - The background color of the {@code exo_shutter}\n *       view.\n *       <ul>\n *         <li>Corresponding method: {@link #setShutterBackgroundColor(int)}\n *         <li>Default: {@code unset}\n *       </ul>\n *   <li><b>{@code keep_content_on_player_reset}</b> - Whether the currently displayed video frame\n *       or media artwork is kept visible when the player is reset.\n *       <ul>\n *         <li>Corresponding method: {@link #setKeepContentOnPlayerReset(boolean)}\n *         <li>Default: {@code false}\n *       </ul>\n *   <li><b>{@code player_layout_id}</b> - Specifies the id of the layout to be inflated. See below\n *       for more details.\n *       <ul>\n *         <li>Corresponding method: None\n *         <li>Default: {@code R.layout.exo_player_view}\n *       </ul>\n *   <li><b>{@code controller_layout_id}</b> - Specifies the id of the layout resource to be\n *       inflated by the child {@link PlayerControlView}. See below for more details.\n *       <ul>\n *         <li>Corresponding method: None\n *         <li>Default: {@code R.layout.exo_player_control_view}\n *       </ul>\n *   <li>All attributes that can be set on {@link PlayerControlView} and {@link DefaultTimeBar} can\n *       also be set on a PlayerView, and will be propagated to the inflated {@link\n *       PlayerControlView} unless the layout is overridden to specify a custom {@code\n *       exo_controller} (see below).\n * </ul>\n *\n * <h3>Overriding the layout file</h3>\n *\n * To customize the layout of PlayerView throughout your app, or just for certain configurations,\n * you can define {@code exo_player_view.xml} layout files in your application {@code res/layout*}\n * directories. These layouts will override the one provided by the ExoPlayer library, and will be\n * inflated for use by PlayerView. The view identifies and binds its children by looking for the\n * following ids:\n *\n * <p>\n *\n * <ul>\n *   <li><b>{@code exo_content_frame}</b> - A frame whose aspect ratio is resized based on the video\n *       or album art of the media being played, and the configured {@code resize_mode}. The video\n *       surface view is inflated into this frame as its first child.\n *       <ul>\n *         <li>Type: {@link AspectRatioFrameLayout}\n *       </ul>\n *   <li><b>{@code exo_shutter}</b> - A view that's made visible when video should be hidden. This\n *       view is typically an opaque view that covers the video surface, thereby obscuring it when\n *       visible. Obscuring the surface in this way also helps to prevent flicker at the start of\n *       playback when {@code surface_type=\"surface_view\"}.\n *       <ul>\n *         <li>Type: {@link View}\n *       </ul>\n *   <li><b>{@code exo_buffering}</b> - A view that's made visible when the player is buffering.\n *       This view typically displays a buffering spinner or animation.\n *       <ul>\n *         <li>Type: {@link View}\n *       </ul>\n *   <li><b>{@code exo_subtitles}</b> - Displays subtitles.\n *       <ul>\n *         <li>Type: {@link SubtitleView}\n *       </ul>\n *   <li><b>{@code exo_artwork}</b> - Displays album art.\n *       <ul>\n *         <li>Type: {@link ImageView}\n *       </ul>\n *   <li><b>{@code exo_error_message}</b> - Displays an error message to the user if playback fails.\n *       <ul>\n *         <li>Type: {@link TextView}\n *       </ul>\n *   <li><b>{@code exo_controller_placeholder}</b> - A placeholder that's replaced with the inflated\n *       {@link PlayerControlView}. Ignored if an {@code exo_controller} view exists.\n *       <ul>\n *         <li>Type: {@link View}\n *       </ul>\n *   <li><b>{@code exo_controller}</b> - An already inflated {@link PlayerControlView}. Allows use\n *       of a custom extension of {@link PlayerControlView}. {@link PlayerControlView} and {@link\n *       DefaultTimeBar} attributes set on the PlayerView will not be automatically propagated\n *       through to this instance. If a view exists with this id, any {@code\n *       exo_controller_placeholder} view will be ignored.\n *       <ul>\n *         <li>Type: {@link PlayerControlView}\n *       </ul>\n *   <li><b>{@code exo_ad_overlay}</b> - A {@link FrameLayout} positioned on top of the player which\n *       is used to show ad UI (if applicable).\n *       <ul>\n *         <li>Type: {@link FrameLayout}\n *       </ul>\n *   <li><b>{@code exo_overlay}</b> - A {@link FrameLayout} positioned on top of the player which\n *       the app can access via {@link #getOverlayFrameLayout()}, provided for convenience.\n *       <ul>\n *         <li>Type: {@link FrameLayout}\n *       </ul>\n * </ul>\n *\n * <p>All child views are optional and so can be omitted if not required, however where defined they\n * must be of the expected type.\n *\n * <h3>Specifying a custom layout file</h3>\n *\n * Defining your own {@code exo_player_view.xml} is useful to customize the layout of PlayerView\n * throughout your application. It's also possible to customize the layout for a single instance in\n * a layout file. This is achieved by setting the {@code player_layout_id} attribute on a\n * PlayerView. This will cause the specified layout to be inflated instead of {@code\n * exo_player_view.xml} for only the instance on which the attribute is set.\n */\npublic class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider {\n\n  // LINT.IfChange\n  /**\n   * Determines when the buffering view is shown. One of {@link #SHOW_BUFFERING_NEVER}, {@link\n   * #SHOW_BUFFERING_WHEN_PLAYING} or {@link #SHOW_BUFFERING_ALWAYS}.\n   */\n  @Documented\n  @Retention(RetentionPolicy.SOURCE)\n  @IntDef({SHOW_BUFFERING_NEVER, SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS})\n  public @interface ShowBuffering {}\n  /** The buffering view is never shown. */\n  public static final int SHOW_BUFFERING_NEVER = 0;\n  /**\n   * The buffering view is shown when the player is in the {@link Player#STATE_BUFFERING buffering}\n   * state and {@link Player#getPlayWhenReady() playWhenReady} is {@code true}.\n   */\n  public static final int SHOW_BUFFERING_WHEN_PLAYING = 1;\n  /**\n   * The buffering view is always shown when the player is in the {@link Player#STATE_BUFFERING\n   * buffering} state.\n   */\n  public static final int SHOW_BUFFERING_ALWAYS = 2;\n  // LINT.ThenChange(../../../../../../res/values/attrs.xml)\n\n  // LINT.IfChange\n  private static final int SURFACE_TYPE_NONE = 0;\n  private static final int SURFACE_TYPE_SURFACE_VIEW = 1;\n  private static final int SURFACE_TYPE_TEXTURE_VIEW = 2;\n  private static final int SURFACE_TYPE_MONO360_VIEW = 3;\n  // LINT.ThenChange(../../../../../../res/values/attrs.xml)\n\n  @Nullable private final AspectRatioFrameLayout contentFrame;\n  private final View shutterView;\n  @Nullable private final View surfaceView;\n  private final ImageView artworkView;\n  private final SubtitleView subtitleView;\n  @Nullable private final View bufferingView;\n  @Nullable private final TextView errorMessageView;\n  @Nullable private final PlayerControlView controller;\n  private final ComponentListener componentListener;\n  @Nullable private final FrameLayout adOverlayFrameLayout;\n  @Nullable private final FrameLayout overlayFrameLayout;\n\n  private Player player;\n  private boolean useController;\n  private boolean useArtwork;\n  @Nullable private Drawable defaultArtwork;\n  private @ShowBuffering int showBuffering;\n  private boolean keepContentOnPlayerReset;\n  @Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;\n  @Nullable private CharSequence customErrorMessage;\n  private int controllerShowTimeoutMs;\n  private boolean controllerAutoShow;\n  private boolean controllerHideDuringAds;\n  private boolean controllerHideOnTouch;\n  private int textureViewRotation;\n  private boolean isTouching;\n  private static final int PICTURE_TYPE_FRONT_COVER = 3;\n  private static final int PICTURE_TYPE_NOT_SET = -1;\n\n  public PlayerView(Context context) {\n    this(context, null);\n  }\n\n  public PlayerView(Context context, AttributeSet attrs) {\n    this(context, attrs, 0);\n  }\n\n  public PlayerView(Context context, AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n\n    if (isInEditMode()) {\n      contentFrame = null;\n      shutterView = null;\n      surfaceView = null;\n      artworkView = null;\n      subtitleView = null;\n      bufferingView = null;\n      errorMessageView = null;\n      controller = null;\n      componentListener = null;\n      adOverlayFrameLayout = null;\n      overlayFrameLayout = null;\n      ImageView logo = new ImageView(context);\n      if (Util.SDK_INT >= 23) {\n        configureEditModeLogoV23(getResources(), logo);\n      } else {\n        configureEditModeLogo(getResources(), logo);\n      }\n      addView(logo);\n      return;\n    }\n\n    boolean shutterColorSet = false;\n    int shutterColor = 0;\n    int playerLayoutId = R.layout.exo_player_view;\n    boolean useArtwork = true;\n    int defaultArtworkId = 0;\n    boolean useController = true;\n    int surfaceType = SURFACE_TYPE_SURFACE_VIEW;\n    int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;\n    int controllerShowTimeoutMs = PlayerControlView.DEFAULT_SHOW_TIMEOUT_MS;\n    boolean controllerHideOnTouch = true;\n    boolean controllerAutoShow = true;\n    boolean controllerHideDuringAds = true;\n    int showBuffering = SHOW_BUFFERING_NEVER;\n    if (attrs != null) {\n      TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0);\n      try {\n        shutterColorSet = a.hasValue(R.styleable.PlayerView_shutter_background_color);\n        shutterColor = a.getColor(R.styleable.PlayerView_shutter_background_color, shutterColor);\n        playerLayoutId = a.getResourceId(R.styleable.PlayerView_player_layout_id, playerLayoutId);\n        useArtwork = a.getBoolean(R.styleable.PlayerView_use_artwork, useArtwork);\n        defaultArtworkId =\n            a.getResourceId(R.styleable.PlayerView_default_artwork, defaultArtworkId);\n        useController = a.getBoolean(R.styleable.PlayerView_use_controller, useController);\n        surfaceType = a.getInt(R.styleable.PlayerView_surface_type, surfaceType);\n        resizeMode = a.getInt(R.styleable.PlayerView_resize_mode, resizeMode);\n        controllerShowTimeoutMs =\n            a.getInt(R.styleable.PlayerView_show_timeout, controllerShowTimeoutMs);\n        controllerHideOnTouch =\n            a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch);\n        controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow);\n        showBuffering = a.getInteger(R.styleable.PlayerView_show_buffering, showBuffering);\n        keepContentOnPlayerReset =\n            a.getBoolean(\n                R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset);\n        controllerHideDuringAds =\n            a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds);\n      } finally {\n        a.recycle();\n      }\n    }\n\n    LayoutInflater.from(context).inflate(playerLayoutId, this);\n    componentListener = new ComponentListener();\n    setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);\n\n    // Content frame.\n    contentFrame = findViewById(R.id.exo_content_frame);\n    if (contentFrame != null) {\n      setResizeModeRaw(contentFrame, resizeMode);\n    }\n\n    // Shutter view.\n    shutterView = findViewById(R.id.exo_shutter);\n    if (shutterView != null && shutterColorSet) {\n      shutterView.setBackgroundColor(shutterColor);\n    }\n\n    // Create a surface view and insert it into the content frame, if there is one.\n    if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) {\n      ViewGroup.LayoutParams params =\n          new ViewGroup.LayoutParams(\n              ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);\n      switch (surfaceType) {\n        case SURFACE_TYPE_TEXTURE_VIEW:\n          surfaceView = new TextureView(context);\n          break;\n        case SURFACE_TYPE_MONO360_VIEW:\n          SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context);\n          sphericalSurfaceView.setSingleTapListener(componentListener);\n          surfaceView = sphericalSurfaceView;\n          break;\n        default:\n          surfaceView = new SurfaceView(context);\n          break;\n      }\n      surfaceView.setLayoutParams(params);\n      contentFrame.addView(surfaceView, 0);\n    } else {\n      surfaceView = null;\n    }\n\n    // Ad overlay frame layout.\n    adOverlayFrameLayout = findViewById(R.id.exo_ad_overlay);\n\n    // Overlay frame layout.\n    overlayFrameLayout = findViewById(R.id.exo_overlay);\n\n    // Artwork view.\n    artworkView = findViewById(R.id.exo_artwork);\n    this.useArtwork = useArtwork && artworkView != null;\n    if (defaultArtworkId != 0) {\n      defaultArtwork = ContextCompat.getDrawable(getContext(), defaultArtworkId);\n    }\n\n    // Subtitle view.\n    subtitleView = findViewById(R.id.exo_subtitles);\n    if (subtitleView != null) {\n      subtitleView.setUserDefaultStyle();\n      subtitleView.setUserDefaultTextSize();\n    }\n\n    // Buffering view.\n    bufferingView = findViewById(R.id.exo_buffering);\n    if (bufferingView != null) {\n      bufferingView.setVisibility(View.GONE);\n    }\n    this.showBuffering = showBuffering;\n\n    // Error message view.\n    errorMessageView = findViewById(R.id.exo_error_message);\n    if (errorMessageView != null) {\n      errorMessageView.setVisibility(View.GONE);\n    }\n\n    // Playback control view.\n    PlayerControlView customController = findViewById(R.id.exo_controller);\n    View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder);\n    if (customController != null) {\n      this.controller = customController;\n    } else if (controllerPlaceholder != null) {\n      // Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are\n      // transferred, but standard attributes (e.g. background) are not.\n      this.controller = new PlayerControlView(context, null, 0, attrs);\n      controller.setId(R.id.exo_controller);\n      controller.setLayoutParams(controllerPlaceholder.getLayoutParams());\n      ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent());\n      int controllerIndex = parent.indexOfChild(controllerPlaceholder);\n      parent.removeView(controllerPlaceholder);\n      parent.addView(controller, controllerIndex);\n    } else {\n      this.controller = null;\n    }\n    this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0;\n    this.controllerHideOnTouch = controllerHideOnTouch;\n    this.controllerAutoShow = controllerAutoShow;\n    this.controllerHideDuringAds = controllerHideDuringAds;\n    this.useController = useController && controller != null;\n    hideController();\n  }\n\n  /**\n   * Switches the view targeted by a given {@link Player}.\n   *\n   * @param player The player whose target view is being switched.\n   * @param oldPlayerView The old view to detach from the player.\n   * @param newPlayerView The new view to attach to the player.\n   */\n  public static void switchTargetView(\n      Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView) {\n    if (oldPlayerView == newPlayerView) {\n      return;\n    }\n    // We attach the new view before detaching the old one because this ordering allows the player\n    // to swap directly from one surface to another, without transitioning through a state where no\n    // surface is attached. This is significantly more efficient and achieves a more seamless\n    // transition when using platform provided video decoders.\n    if (newPlayerView != null) {\n      newPlayerView.setPlayer(player);\n    }\n    if (oldPlayerView != null) {\n      oldPlayerView.setPlayer(null);\n    }\n  }\n\n  /** Returns the player currently set on this view, or null if no player is set. */\n  public Player getPlayer() {\n    return player;\n  }\n\n  /**\n   * Set the {@link Player} to use.\n   *\n   * <p>To transition a {@link Player} from targeting one view to another, it's recommended to use\n   * {@link #switchTargetView(Player, PlayerView, PlayerView)} rather than this method. If you do\n   * wish to use this method directly, be sure to attach the player to the new view <em>before</em>\n   * calling {@code setPlayer(null)} to detach it from the old one. This ordering is significantly\n   * more efficient and may allow for more seamless transitions.\n   *\n   * @param player The {@link Player} to use, or {@code null} to detach the current player. Only\n   *     players which are accessed on the main thread are supported ({@code\n   *     player.getApplicationLooper() == Looper.getMainLooper()}).\n   */\n  public void setPlayer(@Nullable Player player) {\n    Assertions.checkState(Looper.myLooper() == Looper.getMainLooper());\n    Assertions.checkArgument(\n        player == null || player.getApplicationLooper() == Looper.getMainLooper());\n    if (this.player == player) {\n      return;\n    }\n    if (this.player != null) {\n      this.player.removeListener(componentListener);\n      Player.VideoComponent oldVideoComponent = this.player.getVideoComponent();\n      if (oldVideoComponent != null) {\n        oldVideoComponent.removeVideoListener(componentListener);\n        if (surfaceView instanceof TextureView) {\n          oldVideoComponent.clearVideoTextureView((TextureView) surfaceView);\n        } else if (surfaceView instanceof SphericalSurfaceView) {\n          ((SphericalSurfaceView) surfaceView).setVideoComponent(null);\n        } else if (surfaceView instanceof SurfaceView) {\n          oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView);\n        }\n      }\n      Player.TextComponent oldTextComponent = this.player.getTextComponent();\n      if (oldTextComponent != null) {\n        oldTextComponent.removeTextOutput(componentListener);\n      }\n    }\n    this.player = player;\n    if (useController) {\n      controller.setPlayer(player);\n    }\n    if (subtitleView != null) {\n      subtitleView.setCues(null);\n    }\n    updateBuffering();\n    updateErrorMessage();\n    updateForCurrentTrackSelections(/* isNewPlayer= */ true);\n    if (player != null) {\n      Player.VideoComponent newVideoComponent = player.getVideoComponent();\n      if (newVideoComponent != null) {\n        if (surfaceView instanceof TextureView) {\n          newVideoComponent.setVideoTextureView((TextureView) surfaceView);\n        } else if (surfaceView instanceof SphericalSurfaceView) {\n          ((SphericalSurfaceView) surfaceView).setVideoComponent(newVideoComponent);\n        } else if (surfaceView instanceof SurfaceView) {\n          newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView);\n        }\n        newVideoComponent.addVideoListener(componentListener);\n      }\n      Player.TextComponent newTextComponent = player.getTextComponent();\n      if (newTextComponent != null) {\n        newTextComponent.addTextOutput(componentListener);\n      }\n      player.addListener(componentListener);\n      maybeShowController(false);\n    } else {\n      hideController();\n    }\n  }\n\n  @Override\n  public void setVisibility(int visibility) {\n    super.setVisibility(visibility);\n    if (surfaceView instanceof SurfaceView) {\n      // Work around https://github.com/google/ExoPlayer/issues/3160.\n      surfaceView.setVisibility(visibility);\n    }\n  }\n\n  /**\n   * Sets the {@link ResizeMode}.\n   *\n   * @param resizeMode The {@link ResizeMode}.\n   */\n  public void setResizeMode(@ResizeMode int resizeMode) {\n    Assertions.checkState(contentFrame != null);\n    contentFrame.setResizeMode(resizeMode);\n  }\n\n  /** Returns the {@link ResizeMode}. */\n  public @ResizeMode int getResizeMode() {\n    Assertions.checkState(contentFrame != null);\n    return contentFrame.getResizeMode();\n  }\n\n  /** Returns whether artwork is displayed if present in the media. */\n  public boolean getUseArtwork() {\n    return useArtwork;\n  }\n\n  /**\n   * Sets whether artwork is displayed if present in the media.\n   *\n   * @param useArtwork Whether artwork is displayed.\n   */\n  public void setUseArtwork(boolean useArtwork) {\n    Assertions.checkState(!useArtwork || artworkView != null);\n    if (this.useArtwork != useArtwork) {\n      this.useArtwork = useArtwork;\n      updateForCurrentTrackSelections(/* isNewPlayer= */ false);\n    }\n  }\n\n  /** Returns the default artwork to display. */\n  public @Nullable Drawable getDefaultArtwork() {\n    return defaultArtwork;\n  }\n\n  /**\n   * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is\n   * present in the media.\n   *\n   * @param defaultArtwork the default artwork to display.\n   * @deprecated use (@link {@link #setDefaultArtwork(Drawable)} instead.\n   */\n  @Deprecated\n  public void setDefaultArtwork(@Nullable Bitmap defaultArtwork) {\n    setDefaultArtwork(\n        defaultArtwork == null ? null : new BitmapDrawable(getResources(), defaultArtwork));\n  }\n\n  /**\n   * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is\n   * present in the media.\n   *\n   * @param defaultArtwork the default artwork to display\n   */\n  public void setDefaultArtwork(@Nullable Drawable defaultArtwork) {\n    if (this.defaultArtwork != defaultArtwork) {\n      this.defaultArtwork = defaultArtwork;\n      updateForCurrentTrackSelections(/* isNewPlayer= */ false);\n    }\n  }\n\n  /** Returns whether the playback controls can be shown. */\n  public boolean getUseController() {\n    return useController;\n  }\n\n  /**\n   * Sets whether the playback controls can be shown. If set to {@code false} the playback controls\n   * are never visible and are disconnected from the player.\n   *\n   * @param useController Whether the playback controls can be shown.\n   */\n  public void setUseController(boolean useController) {\n    Assertions.checkState(!useController || controller != null);\n    if (this.useController == useController) {\n      return;\n    }\n    this.useController = useController;\n    if (useController) {\n      controller.setPlayer(player);\n    } else if (controller != null) {\n      controller.hide();\n      controller.setPlayer(null);\n    }\n  }\n\n  /**\n   * Sets the background color of the {@code exo_shutter} view.\n   *\n   * @param color The background color.\n   */\n  public void setShutterBackgroundColor(int color) {\n    if (shutterView != null) {\n      shutterView.setBackgroundColor(color);\n    }\n  }\n\n  /**\n   * Sets whether the currently displayed video frame or media artwork is kept visible when the\n   * player is reset. A player reset is defined to mean the player being re-prepared with different\n   * media, the player transitioning to unprepared media, {@link Player#stop(boolean)} being called\n   * with {@code reset=true}, or the player being replaced or cleared by calling {@link\n   * #setPlayer(Player)}.\n   *\n   * <p>If enabled, the currently displayed video frame or media artwork will be kept visible until\n   * the player set on the view has been successfully prepared with new media and loaded enough of\n   * it to have determined the available tracks. Hence enabling this option allows transitioning\n   * from playing one piece of media to another, or from using one player instance to another,\n   * without clearing the view's content.\n   *\n   * <p>If disabled, the currently displayed video frame or media artwork will be hidden as soon as\n   * the player is reset. Note that the video frame is hidden by making {@code exo_shutter} visible.\n   * Hence the video frame will not be hidden if using a custom layout that omits this view.\n   *\n   * @param keepContentOnPlayerReset Whether the currently displayed video frame or media artwork is\n   *     kept visible when the player is reset.\n   */\n  public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset) {\n    if (this.keepContentOnPlayerReset != keepContentOnPlayerReset) {\n      this.keepContentOnPlayerReset = keepContentOnPlayerReset;\n      updateForCurrentTrackSelections(/* isNewPlayer= */ false);\n    }\n  }\n\n  /**\n   * Sets whether a buffering spinner is displayed when the player is in the buffering state. The\n   * buffering spinner is not displayed by default.\n   *\n   * @deprecated Use {@link #setShowBuffering(int)}\n   * @param showBuffering Whether the buffering icon is displayed\n   */\n  @Deprecated\n  public void setShowBuffering(boolean showBuffering) {\n    setShowBuffering(showBuffering ? SHOW_BUFFERING_WHEN_PLAYING : SHOW_BUFFERING_NEVER);\n  }\n\n  /**\n   * Sets whether a buffering spinner is displayed when the player is in the buffering state. The\n   * buffering spinner is not displayed by default.\n   *\n   * @param showBuffering The mode that defines when the buffering spinner is displayed. One of\n   *     {@link #SHOW_BUFFERING_NEVER}, {@link #SHOW_BUFFERING_WHEN_PLAYING} and\n   *     {@link #SHOW_BUFFERING_ALWAYS}.\n   */\n  public void setShowBuffering(@ShowBuffering int showBuffering) {\n    if (this.showBuffering != showBuffering) {\n      this.showBuffering = showBuffering;\n      updateBuffering();\n    }\n  }\n\n  /**\n   * Sets the optional {@link ErrorMessageProvider}.\n   *\n   * @param errorMessageProvider The error message provider.\n   */\n  public void setErrorMessageProvider(\n      @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {\n    if (this.errorMessageProvider != errorMessageProvider) {\n      this.errorMessageProvider = errorMessageProvider;\n      updateErrorMessage();\n    }\n  }\n\n  /**\n   * Sets a custom error message to be displayed by the view. The error message will be displayed\n   * permanently, unless it is cleared by passing {@code null} to this method.\n   *\n   * @param message The message to display, or {@code null} to clear a previously set message.\n   */\n  public void setCustomErrorMessage(@Nullable CharSequence message) {\n    Assertions.checkState(errorMessageView != null);\n    customErrorMessage = message;\n    updateErrorMessage();\n  }\n\n  @Override\n  public boolean dispatchKeyEvent(KeyEvent event) {\n    if (player != null && player.isPlayingAd()) {\n      return super.dispatchKeyEvent(event);\n    }\n\n    boolean isDpadAndUseController = isDpadKey(event.getKeyCode()) && useController;\n    boolean handled = false;\n    if (isDpadAndUseController && !controller.isVisible()) {\n      // Handle the key event by showing the controller.\n      maybeShowController(true);\n      handled = true;\n    } else if (dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event)) {\n      // The key event was handled as a media key or by the super class. We should also show the\n      // controller, or extend its show timeout if already visible.\n      maybeShowController(true);\n      handled = true;\n    } else if (isDpadAndUseController) {\n      // The key event wasn't handled, but we should extend the controller's show timeout.\n      maybeShowController(true);\n    }\n    return handled;\n  }\n\n  /**\n   * Called to process media key events. Any {@link KeyEvent} can be passed but only media key\n   * events will be handled. Does nothing if playback controls are disabled.\n   *\n   * @param event A key event.\n   * @return Whether the key event was handled.\n   */\n  public boolean dispatchMediaKeyEvent(KeyEvent event) {\n    return useController && controller.dispatchMediaKeyEvent(event);\n  }\n\n  /** Returns whether the controller is currently visible. */\n  public boolean isControllerVisible() {\n    return controller != null && controller.isVisible();\n  }\n\n  /**\n   * Shows the playback controls. Does nothing if playback controls are disabled.\n   *\n   * <p>The playback controls are automatically hidden during playback after {{@link\n   * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet,\n   * is paused, has ended or failed.\n   */\n  public void showController() {\n    showController(shouldShowControllerIndefinitely());\n  }\n\n  /** Hides the playback controls. Does nothing if playback controls are disabled. */\n  public void hideController() {\n    if (controller != null) {\n      controller.hide();\n    }\n  }\n\n  /**\n   * Returns the playback controls timeout. The playback controls are automatically hidden after\n   * this duration of time has elapsed without user input and with playback or buffering in\n   * progress.\n   *\n   * @return The timeout in milliseconds. A non-positive value will cause the controller to remain\n   *     visible indefinitely.\n   */\n  public int getControllerShowTimeoutMs() {\n    return controllerShowTimeoutMs;\n  }\n\n  /**\n   * Sets the playback controls timeout. The playback controls are automatically hidden after this\n   * duration of time has elapsed without user input and with playback or buffering in progress.\n   *\n   * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the\n   *     controller to remain visible indefinitely.\n   */\n  public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) {\n    Assertions.checkState(controller != null);\n    this.controllerShowTimeoutMs = controllerShowTimeoutMs;\n    if (controller.isVisible()) {\n      // Update the controller's timeout if necessary.\n      showController();\n    }\n  }\n\n  /** Returns whether the playback controls are hidden by touch events. */\n  public boolean getControllerHideOnTouch() {\n    return controllerHideOnTouch;\n  }\n\n  /**\n   * Sets whether the playback controls are hidden by touch events.\n   *\n   * @param controllerHideOnTouch Whether the playback controls are hidden by touch events.\n   */\n  public void setControllerHideOnTouch(boolean controllerHideOnTouch) {\n    Assertions.checkState(controller != null);\n    this.controllerHideOnTouch = controllerHideOnTouch;\n  }\n\n  /**\n   * Returns whether the playback controls are automatically shown when playback starts, pauses,\n   * ends, or fails. If set to false, the playback controls can be manually operated with {@link\n   * #showController()} and {@link #hideController()}.\n   */\n  public boolean getControllerAutoShow() {\n    return controllerAutoShow;\n  }\n\n  /**\n   * Sets whether the playback controls are automatically shown when playback starts, pauses, ends,\n   * or fails. If set to false, the playback controls can be manually operated with {@link\n   * #showController()} and {@link #hideController()}.\n   *\n   * @param controllerAutoShow Whether the playback controls are allowed to show automatically.\n   */\n  public void setControllerAutoShow(boolean controllerAutoShow) {\n    this.controllerAutoShow = controllerAutoShow;\n  }\n\n  /**\n   * Sets whether the playback controls are hidden when ads are playing. Controls are always shown\n   * during ads if they are enabled and the player is paused.\n   *\n   * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing.\n   */\n  public void setControllerHideDuringAds(boolean controllerHideDuringAds) {\n    this.controllerHideDuringAds = controllerHideDuringAds;\n  }\n\n  /**\n   * Set the {@link PlayerControlView.VisibilityListener}.\n   *\n   * @param listener The listener to be notified about visibility changes.\n   */\n  public void setControllerVisibilityListener(PlayerControlView.VisibilityListener listener) {\n    Assertions.checkState(controller != null);\n    controller.setVisibilityListener(listener);\n  }\n\n  /**\n   * Sets the {@link PlaybackPreparer}.\n   *\n   * @param playbackPreparer The {@link PlaybackPreparer}.\n   */\n  public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {\n    Assertions.checkState(controller != null);\n    controller.setPlaybackPreparer(playbackPreparer);\n  }\n\n  /**\n   * Sets the {@link ControlDispatcher}.\n   *\n   * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link\n   *     DefaultControlDispatcher}.\n   */\n  public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) {\n    Assertions.checkState(controller != null);\n    controller.setControlDispatcher(controlDispatcher);\n  }\n\n  /**\n   * Sets the rewind increment in milliseconds.\n   *\n   * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the\n   *     rewind button to be disabled.\n   */\n  public void setRewindIncrementMs(int rewindMs) {\n    Assertions.checkState(controller != null);\n    controller.setRewindIncrementMs(rewindMs);\n  }\n\n  /**\n   * Sets the fast forward increment in milliseconds.\n   *\n   * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will\n   *     cause the fast forward button to be disabled.\n   */\n  public void setFastForwardIncrementMs(int fastForwardMs) {\n    Assertions.checkState(controller != null);\n    controller.setFastForwardIncrementMs(fastForwardMs);\n  }\n\n  /**\n   * Sets which repeat toggle modes are enabled.\n   *\n   * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}.\n   */\n  public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {\n    Assertions.checkState(controller != null);\n    controller.setRepeatToggleModes(repeatToggleModes);\n  }\n\n  /**\n   * Sets whether the shuffle button is shown.\n   *\n   * @param showShuffleButton Whether the shuffle button is shown.\n   */\n  public void setShowShuffleButton(boolean showShuffleButton) {\n    Assertions.checkState(controller != null);\n    controller.setShowShuffleButton(showShuffleButton);\n  }\n\n  /**\n   * Sets whether the time bar should show all windows, as opposed to just the current one.\n   *\n   * @param showMultiWindowTimeBar Whether to show all windows.\n   */\n  public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {\n    Assertions.checkState(controller != null);\n    controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar);\n  }\n\n  /**\n   * Sets the millisecond positions of extra ad markers relative to the start of the window (or\n   * timeline, if in multi-window mode) and whether each extra ad has been played or not. The\n   * markers are shown in addition to any ad markers for ads in the player's timeline.\n   *\n   * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or\n   *     {@code null} to show no extra ad markers.\n   * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad\n   *     markers.\n   */\n  public void setExtraAdGroupMarkers(\n      @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) {\n    Assertions.checkState(controller != null);\n    controller.setExtraAdGroupMarkers(extraAdGroupTimesMs, extraPlayedAdGroups);\n  }\n\n  /**\n   * Set the {@link AspectRatioFrameLayout.AspectRatioListener}.\n   *\n   * @param listener The listener to be notified about aspect ratios changes of the video content or\n   *     the content frame.\n   */\n  public void setAspectRatioListener(AspectRatioFrameLayout.AspectRatioListener listener) {\n    Assertions.checkState(contentFrame != null);\n    contentFrame.setAspectRatioListener(listener);\n  }\n\n  /**\n   * Gets the view onto which video is rendered. This is a:\n   *\n   * <ul>\n   *   <li>{@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code\n   *       surface_view}.\n   *   <li>{@link TextureView} if {@code surface_type} is {@code texture_view}.\n   *   <li>{@link SphericalSurfaceView} if {@code surface_type} is {@code spherical_view}.\n   *   <li>{@code null} if {@code surface_type} is {@code none}.\n   * </ul>\n   *\n   * @return The {@link SurfaceView}, {@link TextureView}, {@link SphericalSurfaceView} or {@code\n   *     null}.\n   */\n  public View getVideoSurfaceView() {\n    return surfaceView;\n  }\n\n  /**\n   * Gets the overlay {@link FrameLayout}, which can be populated with UI elements to show on top of\n   * the player.\n   *\n   * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and\n   *     the overlay is not present.\n   */\n  @Nullable\n  public FrameLayout getOverlayFrameLayout() {\n    return overlayFrameLayout;\n  }\n\n  /**\n   * Gets the {@link SubtitleView}.\n   *\n   * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the\n   *     subtitle view is not present.\n   */\n  public SubtitleView getSubtitleView() {\n    return subtitleView;\n  }\n\n  @Override\n  public boolean onTouchEvent(MotionEvent event) {\n    if (!useController || player == null) {\n      return false;\n    }\n    switch (event.getAction()) {\n      case MotionEvent.ACTION_DOWN:\n        isTouching = true;\n        return true;\n      case MotionEvent.ACTION_UP:\n        if (isTouching) {\n          isTouching = false;\n          performClick();\n          return true;\n        }\n        return false;\n      default:\n        return false;\n    }\n  }\n\n  @Override\n  public boolean performClick() {\n    super.performClick();\n    return toggleControllerVisibility();\n  }\n\n  @Override\n  public boolean onTrackballEvent(MotionEvent ev) {\n    if (!useController || player == null) {\n      return false;\n    }\n    maybeShowController(true);\n    return true;\n  }\n\n  /**\n   * Should be called when the player is visible to the user and if {@code surface_type} is {@code\n   * spherical_view}. It is the counterpart to {@link #onPause()}.\n   *\n   * <p>This method should typically be called in {@link Activity#onStart()}, or {@link\n   * Activity#onResume()} for API versions &lt;= 23.\n   */\n  public void onResume() {\n    if (surfaceView instanceof SphericalSurfaceView) {\n      ((SphericalSurfaceView) surfaceView).onResume();\n    }\n  }\n\n  /**\n   * Should be called when the player is no longer visible to the user and if {@code surface_type}\n   * is {@code spherical_view}. It is the counterpart to {@link #onResume()}.\n   *\n   * <p>This method should typically be called in {@link Activity#onStop()}, or {@link\n   * Activity#onPause()} for API versions &lt;= 23.\n   */\n  public void onPause() {\n    if (surfaceView instanceof SphericalSurfaceView) {\n      ((SphericalSurfaceView) surfaceView).onPause();\n    }\n  }\n\n  /**\n   * Called when there's a change in the aspect ratio of the content being displayed. The default\n   * implementation sets the aspect ratio of the content frame to that of the content, unless the\n   * content view is a {@link SphericalSurfaceView} in which case the frame's aspect ratio is\n   * cleared.\n   *\n   * @param contentAspectRatio The aspect ratio of the content.\n   * @param contentFrame The content frame, or {@code null}.\n   * @param contentView The view that holds the content being displayed, or {@code null}.\n   */\n  protected void onContentAspectRatioChanged(\n      float contentAspectRatio,\n      @Nullable AspectRatioFrameLayout contentFrame,\n      @Nullable View contentView) {\n    if (contentFrame != null) {\n      contentFrame.setAspectRatio(\n          contentView instanceof SphericalSurfaceView ? 0 : contentAspectRatio);\n    }\n  }\n\n  // AdsLoader.AdViewProvider implementation.\n\n  @Override\n  public ViewGroup getAdViewGroup() {\n    return Assertions.checkNotNull(\n        adOverlayFrameLayout, \"exo_ad_overlay must be present for ad playback\");\n  }\n\n  @Override\n  public View[] getAdOverlayViews() {\n    ArrayList<View> overlayViews = new ArrayList<>();\n    if (overlayFrameLayout != null) {\n      overlayViews.add(overlayFrameLayout);\n    }\n    if (controller != null) {\n      overlayViews.add(controller);\n    }\n    return overlayViews.toArray(new View[0]);\n  }\n\n  // Internal methods.\n\n  private boolean toggleControllerVisibility() {\n    if (!useController || player == null) {\n      return false;\n    }\n    if (!controller.isVisible()) {\n      maybeShowController(true);\n    } else if (controllerHideOnTouch) {\n      controller.hide();\n    }\n    return true;\n  }\n\n  /** Shows the playback controls, but only if forced or shown indefinitely. */\n  private void maybeShowController(boolean isForced) {\n    if (isPlayingAd() && controllerHideDuringAds) {\n      return;\n    }\n    if (useController) {\n      boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0;\n      boolean shouldShowIndefinitely = shouldShowControllerIndefinitely();\n      if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) {\n        showController(shouldShowIndefinitely);\n      }\n    }\n  }\n\n  private boolean shouldShowControllerIndefinitely() {\n    if (player == null) {\n      return true;\n    }\n    int playbackState = player.getPlaybackState();\n    return controllerAutoShow\n        && (playbackState == Player.STATE_IDLE\n            || playbackState == Player.STATE_ENDED\n            || !player.getPlayWhenReady());\n  }\n\n  private void showController(boolean showIndefinitely) {\n    if (!useController) {\n      return;\n    }\n    controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs);\n    controller.show();\n  }\n\n  private boolean isPlayingAd() {\n    return player != null && player.isPlayingAd() && player.getPlayWhenReady();\n  }\n\n  private void updateForCurrentTrackSelections(boolean isNewPlayer) {\n    if (player == null || player.getCurrentTrackGroups().isEmpty()) {\n      if (!keepContentOnPlayerReset) {\n        hideArtwork();\n        closeShutter();\n      }\n      return;\n    }\n\n    if (isNewPlayer && !keepContentOnPlayerReset) {\n      // Hide any video from the previous player.\n      closeShutter();\n    }\n\n    TrackSelectionArray selections = player.getCurrentTrackSelections();\n    for (int i = 0; i < selections.length; i++) {\n      if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) {\n        // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in\n        // onRenderedFirstFrame().\n        hideArtwork();\n        return;\n      }\n    }\n\n    // Video disabled so the shutter must be closed.\n    closeShutter();\n    // Display artwork if enabled and available, else hide it.\n    if (useArtwork) {\n      for (int i = 0; i < selections.length; i++) {\n        TrackSelection selection = selections.get(i);\n        if (selection != null) {\n          for (int j = 0; j < selection.length(); j++) {\n            Metadata metadata = selection.getFormat(j).metadata;\n            if (metadata != null && setArtworkFromMetadata(metadata)) {\n              return;\n            }\n          }\n        }\n      }\n      if (setDrawableArtwork(defaultArtwork)) {\n        return;\n      }\n    }\n    // Artwork disabled or unavailable.\n    hideArtwork();\n  }\n\n  private boolean setArtworkFromMetadata(Metadata metadata) {\n    boolean isArtworkSet = false;\n    int currentPictureType = PICTURE_TYPE_NOT_SET;\n    for (int i = 0; i < metadata.length(); i++) {\n      Metadata.Entry metadataEntry = metadata.get(i);\n      int pictureType;\n      byte[] bitmapData;\n      if (metadataEntry instanceof ApicFrame) {\n        bitmapData = ((ApicFrame) metadataEntry).pictureData;\n        pictureType = ((ApicFrame) metadataEntry).pictureType;\n      } else if (metadataEntry instanceof PictureFrame) {\n        bitmapData = ((PictureFrame) metadataEntry).pictureData;\n        pictureType = ((PictureFrame) metadataEntry).pictureType;\n      } else {\n        continue;\n      }\n      // Prefer the first front cover picture. If there aren't any, prefer the first picture.\n      if (currentPictureType == PICTURE_TYPE_NOT_SET || pictureType == PICTURE_TYPE_FRONT_COVER) {\n        Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length);\n        isArtworkSet = setDrawableArtwork(new BitmapDrawable(getResources(), bitmap));\n        currentPictureType = pictureType;\n        if (currentPictureType == PICTURE_TYPE_FRONT_COVER) {\n          break;\n        }\n      }\n    }\n    return isArtworkSet;\n  }\n\n  private boolean setDrawableArtwork(@Nullable Drawable drawable) {\n    if (drawable != null) {\n      int drawableWidth = drawable.getIntrinsicWidth();\n      int drawableHeight = drawable.getIntrinsicHeight();\n      if (drawableWidth > 0 && drawableHeight > 0) {\n        float artworkAspectRatio = (float) drawableWidth / drawableHeight;\n        onContentAspectRatioChanged(artworkAspectRatio, contentFrame, artworkView);\n        artworkView.setImageDrawable(drawable);\n        artworkView.setVisibility(VISIBLE);\n        return true;\n      }\n    }\n    return false;\n  }\n\n  private void hideArtwork() {\n    if (artworkView != null) {\n      artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference.\n      artworkView.setVisibility(INVISIBLE);\n    }\n  }\n\n  private void closeShutter() {\n    if (shutterView != null) {\n      shutterView.setVisibility(View.VISIBLE);\n    }\n  }\n\n  private void updateBuffering() {\n    if (bufferingView != null) {\n      boolean showBufferingSpinner =\n          player != null\n              && player.getPlaybackState() == Player.STATE_BUFFERING\n              && (showBuffering == SHOW_BUFFERING_ALWAYS\n                  || (showBuffering == SHOW_BUFFERING_WHEN_PLAYING && player.getPlayWhenReady()));\n      bufferingView.setVisibility(showBufferingSpinner ? View.VISIBLE : View.GONE);\n    }\n  }\n\n  private void updateErrorMessage() {\n    if (errorMessageView != null) {\n      if (customErrorMessage != null) {\n        errorMessageView.setText(customErrorMessage);\n        errorMessageView.setVisibility(View.VISIBLE);\n        return;\n      }\n      ExoPlaybackException error = null;\n      if (player != null\n          && player.getPlaybackState() == Player.STATE_IDLE\n          && errorMessageProvider != null) {\n        error = player.getPlaybackError();\n      }\n      if (error != null) {\n        CharSequence errorMessage = errorMessageProvider.getErrorMessage(error).second;\n        errorMessageView.setText(errorMessage);\n        errorMessageView.setVisibility(View.VISIBLE);\n      } else {\n        errorMessageView.setVisibility(View.GONE);\n      }\n    }\n  }\n\n  @TargetApi(23)\n  private static void configureEditModeLogoV23(Resources resources, ImageView logo) {\n    logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null));\n    logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null));\n  }\n\n  @SuppressWarnings(\"deprecation\")\n  private static void configureEditModeLogo(Resources resources, ImageView logo) {\n    logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo));\n    logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color));\n  }\n\n  @SuppressWarnings(\"ResourceType\")\n  private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode) {\n    aspectRatioFrame.setResizeMode(resizeMode);\n  }\n\n  /** Applies a texture rotation to a {@link TextureView}. */\n  private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) {\n    float textureViewWidth = textureView.getWidth();\n    float textureViewHeight = textureView.getHeight();\n    if (textureViewWidth == 0 || textureViewHeight == 0 || textureViewRotation == 0) {\n      textureView.setTransform(null);\n    } else {\n      Matrix transformMatrix = new Matrix();\n      float pivotX = textureViewWidth / 2;\n      float pivotY = textureViewHeight / 2;\n      transformMatrix.postRotate(textureViewRotation, pivotX, pivotY);\n\n      // After rotation, scale the rotated texture to fit the TextureView size.\n      RectF originalTextureRect = new RectF(0, 0, textureViewWidth, textureViewHeight);\n      RectF rotatedTextureRect = new RectF();\n      transformMatrix.mapRect(rotatedTextureRect, originalTextureRect);\n      transformMatrix.postScale(\n          textureViewWidth / rotatedTextureRect.width(),\n          textureViewHeight / rotatedTextureRect.height(),\n          pivotX,\n          pivotY);\n      textureView.setTransform(transformMatrix);\n    }\n  }\n\n  @SuppressLint(\"InlinedApi\")\n  private boolean isDpadKey(int keyCode) {\n    return keyCode == KeyEvent.KEYCODE_DPAD_UP\n        || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT\n        || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT\n        || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT\n        || keyCode == KeyEvent.KEYCODE_DPAD_DOWN\n        || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT\n        || keyCode == KeyEvent.KEYCODE_DPAD_LEFT\n        || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT\n        || keyCode == KeyEvent.KEYCODE_DPAD_CENTER;\n  }\n\n  private final class ComponentListener\n      implements Player.EventListener,\n          TextOutput,\n          VideoListener,\n          OnLayoutChangeListener,\n          SingleTapListener {\n\n    // TextOutput implementation\n\n    @Override\n    public void onCues(List<Cue> cues) {\n      if (subtitleView != null) {\n        subtitleView.onCues(cues);\n      }\n    }\n\n    // VideoListener implementation\n\n    @Override\n    public void onVideoSizeChanged(\n        int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {\n      float videoAspectRatio =\n          (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height;\n\n      if (surfaceView instanceof TextureView) {\n        // Try to apply rotation transformation when our surface is a TextureView.\n        if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) {\n          // We will apply a rotation 90/270 degree to the output texture of the TextureView.\n          // In this case, the output video's width and height will be swapped.\n          videoAspectRatio = 1 / videoAspectRatio;\n        }\n        if (textureViewRotation != 0) {\n          surfaceView.removeOnLayoutChangeListener(this);\n        }\n        textureViewRotation = unappliedRotationDegrees;\n        if (textureViewRotation != 0) {\n          // The texture view's dimensions might be changed after layout step.\n          // So add an OnLayoutChangeListener to apply rotation after layout step.\n          surfaceView.addOnLayoutChangeListener(this);\n        }\n        applyTextureViewRotation((TextureView) surfaceView, textureViewRotation);\n      }\n\n      onContentAspectRatioChanged(videoAspectRatio, contentFrame, surfaceView);\n    }\n\n    @Override\n    public void onRenderedFirstFrame() {\n      if (shutterView != null) {\n        shutterView.setVisibility(INVISIBLE);\n      }\n    }\n\n    @Override\n    public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) {\n      updateForCurrentTrackSelections(/* isNewPlayer= */ false);\n    }\n\n    // Player.EventListener implementation\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n      updateBuffering();\n      updateErrorMessage();\n      if (isPlayingAd() && controllerHideDuringAds) {\n        hideController();\n      } else {\n        maybeShowController(false);\n      }\n    }\n\n    @Override\n    public void onPositionDiscontinuity(@DiscontinuityReason int reason) {\n      if (isPlayingAd() && controllerHideDuringAds) {\n        hideController();\n      }\n    }\n\n    // OnLayoutChangeListener implementation\n\n    @Override\n    public void onLayoutChange(\n        View view,\n        int left,\n        int top,\n        int right,\n        int bottom,\n        int oldLeft,\n        int oldTop,\n        int oldRight,\n        int oldBottom) {\n      applyTextureViewRotation((TextureView) view, textureViewRotation);\n    }\n\n    // SingleTapListener implementation\n\n    @Override\n    public boolean onSingleTapUp(MotionEvent e) {\n      return toggleControllerVisibility();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ui;\n\nimport android.content.Context;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport android.util.AttributeSet;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\n\n/** @deprecated Use {@link PlayerView}. */\n@Deprecated\npublic final class SimpleExoPlayerView extends PlayerView {\n\n  public SimpleExoPlayerView(Context context) {\n    super(context);\n  }\n\n  public SimpleExoPlayerView(Context context, AttributeSet attrs) {\n    super(context, attrs);\n  }\n\n  public SimpleExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n  }\n\n  /**\n   * Switches the view targeted by a given {@link SimpleExoPlayer}.\n   *\n   * @param player The player whose target view is being switched.\n   * @param oldPlayerView The old view to detach from the player.\n   * @param newPlayerView The new view to attach to the player.\n   * @deprecated Use {@link PlayerView#switchTargetView(Player, PlayerView, PlayerView)} instead.\n   */\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public static void switchTargetView(\n      @NonNull SimpleExoPlayer player,\n      @Nullable SimpleExoPlayerView oldPlayerView,\n      @Nullable SimpleExoPlayerView newPlayerView) {\n    PlayerView.switchTargetView(player, oldPlayerView, newPlayerView);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ui;\n\nimport android.content.Context;\nimport android.content.res.Resources;\nimport android.content.res.TypedArray;\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Paint.Join;\nimport android.graphics.Paint.Style;\nimport android.graphics.Rect;\nimport android.text.Layout.Alignment;\nimport android.text.SpannableStringBuilder;\nimport android.text.Spanned;\nimport android.text.StaticLayout;\nimport android.text.TextPaint;\nimport android.text.TextUtils;\nimport android.text.style.AbsoluteSizeSpan;\nimport android.text.style.BackgroundColorSpan;\nimport android.text.style.RelativeSizeSpan;\nimport android.util.DisplayMetrics;\nimport com.google.android.exoplayer2.text.CaptionStyleCompat;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport com.liskovsoft.sharedutils.misc.PaddingBackgroundColorSpan;\nimport com.liskovsoft.sharedutils.misc.RoundedBackgroundSpan;\n\n/**\n * Paints subtitle {@link Cue}s.\n */\n/* package */ final class SubtitlePainter {\n\n  private static final String TAG = \"SubtitlePainter\";\n\n  /**\n   * Ratio of inner padding to font size.\n   */\n  private static final float INNER_PADDING_RATIO = 0.125f;\n\n  // Styled dimensions.\n  private final float outlineWidth;\n  private final float shadowRadius;\n  private final float shadowOffset;\n  private final float spacingMult;\n  private final float spacingAdd;\n\n  private final TextPaint textPaint;\n  private final Paint paint;\n\n  // Previous input variables.\n  private CharSequence cueText;\n  private Alignment cueTextAlignment;\n  private Bitmap cueBitmap;\n  private float cueLine;\n  @Cue.LineType\n  private int cueLineType;\n  @Cue.AnchorType\n  private int cueLineAnchor;\n  private float cuePosition;\n  @Cue.AnchorType\n  private int cuePositionAnchor;\n  private float cueSize;\n  private float cueBitmapHeight;\n  private boolean applyEmbeddedStyles;\n  private boolean applyEmbeddedFontSizes;\n  private int foregroundColor;\n  private int backgroundColor;\n  private int windowColor;\n  private int edgeColor;\n  @CaptionStyleCompat.EdgeType\n  private int edgeType;\n  private float defaultTextSizePx;\n  private float cueTextSizePx;\n  private float bottomPaddingFraction;\n  private int parentLeft;\n  private int parentTop;\n  private int parentRight;\n  private int parentBottom;\n\n  // Derived drawing variables.\n  private StaticLayout textLayout;\n  private int textLeft;\n  private int textTop;\n  private int textPaddingX;\n  private Rect bitmapRect;\n\n  @SuppressWarnings(\"ResourceType\")\n  public SubtitlePainter(Context context) {\n    int[] viewAttr = {android.R.attr.lineSpacingExtra, android.R.attr.lineSpacingMultiplier};\n    TypedArray styledAttributes = context.obtainStyledAttributes(null, viewAttr, 0, 0);\n    spacingAdd = styledAttributes.getDimensionPixelSize(0, 0);\n    spacingMult = styledAttributes.getFloat(1, 1);\n    styledAttributes.recycle();\n\n    Resources resources = context.getResources();\n    DisplayMetrics displayMetrics = resources.getDisplayMetrics();\n    int twoDpInPx = Math.round((2f * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT);\n    outlineWidth = twoDpInPx;\n    shadowRadius = twoDpInPx;\n    shadowOffset = twoDpInPx;\n\n    textPaint = new TextPaint();\n    textPaint.setAntiAlias(true);\n    textPaint.setSubpixelText(true);\n\n    paint = new Paint();\n    paint.setAntiAlias(true);\n    paint.setStyle(Style.FILL);\n  }\n\n  /**\n   * Draws the provided {@link Cue} into a canvas with the specified styling.\n   *\n   * <p>A call to this method is able to use cached results of calculations made during the previous\n   * call, and so an instance of this class is able to optimize repeated calls to this method in\n   * which the same parameters are passed.\n   *\n   * @param cue The cue to draw.\n   * @param applyEmbeddedStyles Whether styling embedded within the cue should be applied.\n   * @param applyEmbeddedFontSizes If {@code applyEmbeddedStyles} is true, defines whether font\n   *     sizes embedded within the cue should be applied. Otherwise, it is ignored.\n   * @param style The style to use when drawing the cue text.\n   * @param defaultTextSizePx The default text size to use when drawing the text, in pixels.\n   * @param cueTextSizePx The embedded text size of this cue, in pixels.\n   * @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is\n   *     {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height\n   * @param canvas The canvas into which to draw.\n   * @param cueBoxLeft The left position of the enclosing cue box.\n   * @param cueBoxTop The top position of the enclosing cue box.\n   * @param cueBoxRight The right position of the enclosing cue box.\n   * @param cueBoxBottom The bottom position of the enclosing cue box.\n   */\n  public void draw(\n      Cue cue,\n      boolean applyEmbeddedStyles,\n      boolean applyEmbeddedFontSizes,\n      CaptionStyleCompat style,\n      float defaultTextSizePx,\n      float cueTextSizePx,\n      float bottomPaddingFraction,\n      Canvas canvas,\n      int cueBoxLeft,\n      int cueBoxTop,\n      int cueBoxRight,\n      int cueBoxBottom) {\n    boolean isTextCue = cue.bitmap == null;\n    int windowColor = Color.BLACK;\n    if (isTextCue) {\n      if (TextUtils.isEmpty(cue.text)) {\n        // Nothing to draw.\n        return;\n      }\n      windowColor = (cue.windowColorSet && applyEmbeddedStyles)\n          ? cue.windowColor : style.windowColor;\n    }\n    if (areCharSequencesEqual(this.cueText, cue.text)\n        && Util.areEqual(this.cueTextAlignment, cue.textAlignment)\n        && this.cueBitmap == cue.bitmap\n        && this.cueLine == cue.line\n        && this.cueLineType == cue.lineType\n        && Util.areEqual(this.cueLineAnchor, cue.lineAnchor)\n        && this.cuePosition == cue.position\n        && Util.areEqual(this.cuePositionAnchor, cue.positionAnchor)\n        && this.cueSize == cue.size\n        && this.cueBitmapHeight == cue.bitmapHeight\n        && this.applyEmbeddedStyles == applyEmbeddedStyles\n        && this.applyEmbeddedFontSizes == applyEmbeddedFontSizes\n        && this.foregroundColor == style.foregroundColor\n        && this.backgroundColor == style.backgroundColor\n        && this.windowColor == windowColor\n        && this.edgeType == style.edgeType\n        && this.edgeColor == style.edgeColor\n        && Util.areEqual(this.textPaint.getTypeface(), style.typeface)\n        && this.defaultTextSizePx == defaultTextSizePx\n        && this.cueTextSizePx == cueTextSizePx\n        && this.bottomPaddingFraction == bottomPaddingFraction\n        && this.parentLeft == cueBoxLeft\n        && this.parentTop == cueBoxTop\n        && this.parentRight == cueBoxRight\n        && this.parentBottom == cueBoxBottom) {\n      // We can use the cached layout.\n      drawLayout(canvas, isTextCue);\n      return;\n    }\n\n    this.cueText = cue.text;\n    this.cueTextAlignment = cue.textAlignment;\n    this.cueBitmap = cue.bitmap;\n    this.cueLine = cue.line;\n    this.cueLineType = cue.lineType;\n    this.cueLineAnchor = cue.lineAnchor;\n    this.cuePosition = cue.position;\n    this.cuePositionAnchor = cue.positionAnchor;\n    this.cueSize = cue.size;\n    this.cueBitmapHeight = cue.bitmapHeight;\n    this.applyEmbeddedStyles = applyEmbeddedStyles;\n    this.applyEmbeddedFontSizes = applyEmbeddedFontSizes;\n    this.foregroundColor = style.foregroundColor;\n    this.backgroundColor = style.backgroundColor;\n    this.windowColor = windowColor;\n    this.edgeType = style.edgeType;\n    this.edgeColor = style.edgeColor;\n    this.textPaint.setTypeface(style.typeface);\n    this.defaultTextSizePx = defaultTextSizePx;\n    this.cueTextSizePx = cueTextSizePx;\n    this.bottomPaddingFraction = bottomPaddingFraction;\n    this.parentLeft = cueBoxLeft;\n    this.parentTop = cueBoxTop;\n    this.parentRight = cueBoxRight;\n    this.parentBottom = cueBoxBottom;\n\n    if (isTextCue) {\n      setupTextLayout();\n    } else {\n      setupBitmapLayout();\n    }\n    drawLayout(canvas, isTextCue);\n  }\n\n  private void setupTextLayout() {\n    int parentWidth = parentRight - parentLeft;\n    int parentHeight = parentBottom - parentTop;\n\n    textPaint.setTextSize(defaultTextSizePx);\n    int textPaddingX = (int) (defaultTextSizePx * INNER_PADDING_RATIO + 0.5f);\n\n    int availableWidth = parentWidth - textPaddingX * 2;\n    if (cueSize != Cue.DIMEN_UNSET) {\n      availableWidth = (int) (availableWidth * cueSize);\n    }\n    if (availableWidth <= 0) {\n      Log.w(TAG, \"Skipped drawing subtitle cue (insufficient space)\");\n      return;\n    }\n\n    CharSequence cueText = this.cueText;\n    // Remove embedded styling or font size if requested.\n    if (!applyEmbeddedStyles) {\n      cueText = cueText.toString(); // Equivalent to erasing all spans.\n    } else if (!applyEmbeddedFontSizes) {\n      SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText);\n      int cueLength = newCueText.length();\n      AbsoluteSizeSpan[] absSpans = newCueText.getSpans(0, cueLength, AbsoluteSizeSpan.class);\n      RelativeSizeSpan[] relSpans = newCueText.getSpans(0, cueLength, RelativeSizeSpan.class);\n      for (AbsoluteSizeSpan absSpan : absSpans) {\n        newCueText.removeSpan(absSpan);\n      }\n      for (RelativeSizeSpan relSpan : relSpans) {\n        newCueText.removeSpan(relSpan);\n      }\n      cueText = newCueText;\n    } else {\n      // Apply embedded styles & font size.\n      if (cueTextSizePx > 0) {\n        // Use a SpannableStringBuilder encompassing the whole cue text to apply the default\n        // cueTextSizePx.\n        SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText);\n        newCueText.setSpan(\n            new AbsoluteSizeSpan((int) cueTextSizePx),\n            /* start= */ 0,\n            /* end= */ newCueText.length(),\n            Spanned.SPAN_PRIORITY);\n        cueText = newCueText;\n      }\n    }\n\n    if (Color.alpha(backgroundColor) > 0) {\n      SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText);\n      // MOD: add subs bg padding\n      //newCueText.setSpan(\n      //    new BackgroundColorSpan(backgroundColor), 0, newCueText.length(), Spanned.SPAN_PRIORITY);\n      newCueText.setSpan(\n              new PaddingBackgroundColorSpan(backgroundColor), 0, newCueText.length(), Spanned.SPAN_PRIORITY);\n      //newCueText.setSpan(\n      //        new RoundedBackgroundSpan(backgroundColor), 0, newCueText.length(), Spanned.SPAN_PRIORITY);\n      cueText = newCueText;\n    }\n\n    Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment;\n    textLayout = new StaticLayout(cueText, textPaint, availableWidth, textAlignment, spacingMult,\n        spacingAdd, true);\n    // MOD: same height for multiline and single line subs (use 3 lines height)\n    //int textHeight = textLayout.getLineBaseline(0) * 3; // base line has same height across single and multi line\n    int textHeight = textLayout.getHeight();\n    int textWidth = 0;\n    int lineCount = textLayout.getLineCount();\n    for (int i = 0; i < lineCount; i++) {\n      textWidth = Math.max((int) Math.ceil(textLayout.getLineWidth(i)), textWidth);\n    }\n    if (cueSize != Cue.DIMEN_UNSET && textWidth < availableWidth) {\n      textWidth = availableWidth;\n    }\n    textWidth += textPaddingX * 2;\n\n    int textLeft;\n    int textRight;\n    if (cuePosition != Cue.DIMEN_UNSET) {\n      int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft;\n      textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth\n          : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2\n              : anchorPosition;\n      textLeft = Math.max(textLeft, parentLeft);\n      textRight = Math.min(textLeft + textWidth, parentRight);\n    } else {\n      textLeft = (parentWidth - textWidth) / 2 + parentLeft;\n      textRight = textLeft + textWidth;\n    }\n\n    textWidth = textRight - textLeft;\n    if (textWidth <= 0) {\n      Log.w(TAG, \"Skipped drawing subtitle cue (invalid horizontal positioning)\");\n      return;\n    }\n\n    int textTop;\n    if (cueLine != Cue.DIMEN_UNSET) {\n      int anchorPosition;\n      if (cueLineType == Cue.LINE_TYPE_FRACTION) {\n        anchorPosition = Math.round(parentHeight * cueLine) + parentTop;\n      } else {\n        // cueLineType == Cue.LINE_TYPE_NUMBER\n        int firstLineHeight = textLayout.getLineBottom(0) - textLayout.getLineTop(0);\n        if (cueLine >= 0) {\n          anchorPosition = Math.round(cueLine * firstLineHeight) + parentTop;\n        } else {\n          anchorPosition = Math.round((cueLine + 1) * firstLineHeight) + parentBottom;\n        }\n      }\n      textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight\n          : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textHeight) / 2\n              : anchorPosition;\n      if (textTop + textHeight > parentBottom) {\n        textTop = parentBottom - textHeight;\n      } else if (textTop < parentTop) {\n        textTop = parentTop;\n      }\n    } else {\n      textTop = parentBottom - textHeight - (int) (parentHeight * bottomPaddingFraction);\n    }\n\n    // Update the derived drawing variables.\n    this.textLayout = new StaticLayout(cueText, textPaint, textWidth, textAlignment, spacingMult,\n        spacingAdd, true);\n    this.textLeft = textLeft;\n    this.textTop = textTop;\n    this.textPaddingX = textPaddingX;\n  }\n\n  private void setupBitmapLayout() {\n    int parentWidth = parentRight - parentLeft;\n    int parentHeight = parentBottom - parentTop;\n    float anchorX = parentLeft + (parentWidth * cuePosition);\n    float anchorY = parentTop + (parentHeight * cueLine);\n    int width = Math.round(parentWidth * cueSize);\n    int height = cueBitmapHeight != Cue.DIMEN_UNSET ? Math.round(parentHeight * cueBitmapHeight)\n        : Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth()));\n    int x =\n        Math.round(\n            cuePositionAnchor == Cue.ANCHOR_TYPE_END\n                ? (anchorX - width)\n                : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX);\n    int y =\n        Math.round(\n            cueLineAnchor == Cue.ANCHOR_TYPE_END\n                ? (anchorY - height)\n                : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY);\n    bitmapRect = new Rect(x, y, x + width, y + height);\n  }\n\n  private void drawLayout(Canvas canvas, boolean isTextCue) {\n    if (isTextCue) {\n      drawTextLayout(canvas);\n    } else {\n      drawBitmapLayout(canvas);\n    }\n  }\n\n  private void drawTextLayout(Canvas canvas) {\n    StaticLayout layout = textLayout;\n    if (layout == null) {\n      // Nothing to draw.\n      return;\n    }\n\n    int saveCount = canvas.save();\n    canvas.translate(textLeft, textTop);\n\n    if (Color.alpha(windowColor) > 0) {\n      paint.setColor(windowColor);\n      canvas.drawRect(-textPaddingX, 0, layout.getWidth() + textPaddingX, layout.getHeight(),\n          paint);\n    }\n\n    if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) {\n      textPaint.setStrokeJoin(Join.ROUND);\n      textPaint.setStrokeWidth(outlineWidth);\n      textPaint.setColor(edgeColor);\n      textPaint.setStyle(Style.FILL_AND_STROKE);\n      layout.draw(canvas);\n    } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) {\n      textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, edgeColor);\n    } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED\n        || edgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) {\n      boolean raised = edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED;\n      int colorUp = raised ? Color.WHITE : edgeColor;\n      int colorDown = raised ? edgeColor : Color.WHITE;\n      float offset = shadowRadius / 2f;\n      textPaint.setColor(foregroundColor);\n      textPaint.setStyle(Style.FILL);\n      textPaint.setShadowLayer(shadowRadius, -offset, -offset, colorUp);\n      layout.draw(canvas);\n      textPaint.setShadowLayer(shadowRadius, offset, offset, colorDown);\n    }\n\n    textPaint.setColor(foregroundColor);\n    textPaint.setStyle(Style.FILL);\n    layout.draw(canvas);\n    textPaint.setShadowLayer(0, 0, 0, 0);\n\n    canvas.restoreToCount(saveCount);\n  }\n\n  private void drawBitmapLayout(Canvas canvas) {\n    canvas.drawBitmap(cueBitmap, null, bitmapRect, null);\n  }\n\n  /**\n   * This method is used instead of {@link TextUtils#equals(CharSequence, CharSequence)} because the\n   * latter only checks the text of each sequence, and does not check for equality of styling that\n   * may be embedded within the {@link CharSequence}s.\n   */\n  @SuppressWarnings(\"UndefinedEquals\")\n  private static boolean areCharSequencesEqual(CharSequence first, CharSequence second) {\n    // Some CharSequence implementations don't perform a cheap referential equality check in their\n    // equals methods, so we perform one explicitly here.\n    return first == second || (first != null && first.equals(second));\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.ui;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.content.res.Resources;\nimport android.graphics.Canvas;\nimport androidx.annotation.Nullable;\nimport android.util.AttributeSet;\nimport android.util.TypedValue;\nimport android.view.View;\nimport android.view.accessibility.CaptioningManager;\nimport com.google.android.exoplayer2.text.CaptionStyleCompat;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.TextOutput;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * A view for displaying subtitle {@link Cue}s.\n */\npublic final class SubtitleView extends View implements TextOutput {\n\n  /**\n   * The default fractional text size.\n   *\n   * @see #setFractionalTextSize(float, boolean)\n   */\n  public static final float DEFAULT_TEXT_SIZE_FRACTION = 0.0533f;\n\n  /**\n   * The default bottom padding to apply when {@link Cue#line} is {@link Cue#DIMEN_UNSET}, as a\n   * fraction of the viewport height.\n   *\n   * @see #setBottomPaddingFraction(float)\n   */\n  public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f;\n\n  private final List<SubtitlePainter> painters;\n\n  @Nullable private List<Cue> cues;\n  @Cue.TextSizeType private int textSizeType;\n  private float textSize;\n  private boolean applyEmbeddedStyles;\n  private boolean applyEmbeddedFontSizes;\n  private CaptionStyleCompat style;\n  private float bottomPaddingFraction;\n\n  public SubtitleView(Context context) {\n    this(context, /* attrs= */ null);\n  }\n\n  public SubtitleView(Context context, @Nullable AttributeSet attrs) {\n    super(context, attrs);\n    painters = new ArrayList<>();\n    textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL;\n    textSize = DEFAULT_TEXT_SIZE_FRACTION;\n    applyEmbeddedStyles = true;\n    applyEmbeddedFontSizes = true;\n    style = CaptionStyleCompat.DEFAULT;\n    bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION;\n  }\n\n  @Override\n  public void onCues(List<Cue> cues) {\n    setCues(cues);\n  }\n\n  /**\n   * Sets the cues to be displayed by the view.\n   *\n   * @param cues The cues to display, or null to clear the cues.\n   */\n  public void setCues(@Nullable List<Cue> cues) {\n    // MOD: fix overlapped subs\n    // All cues are rendered simultaneously, so remain only one of them\n    if (cues != null && cues.size() > 1) {\n      cues = cues.subList(cues.size() - 1, cues.size());\n    }\n\n    if (this.cues == cues) {\n      return;\n    }\n    this.cues = cues;\n    // Ensure we have sufficient painters.\n    int cueCount = (cues == null) ? 0 : cues.size();\n    while (painters.size() < cueCount) {\n      painters.add(new SubtitlePainter(getContext()));\n    }\n    // Invalidate to trigger drawing.\n    invalidate();\n  }\n\n  /**\n   * Set the text size to a given unit and value.\n   * <p>\n   * See {@link TypedValue} for the possible dimension units.\n   *\n   * @param unit The desired dimension unit.\n   * @param size The desired size in the given units.\n   */\n  public void setFixedTextSize(int unit, float size) {\n    Context context = getContext();\n    Resources resources;\n    if (context == null) {\n      resources = Resources.getSystem();\n    } else {\n      resources = context.getResources();\n    }\n    setTextSize(\n        Cue.TEXT_SIZE_TYPE_ABSOLUTE,\n        TypedValue.applyDimension(unit, size, resources.getDisplayMetrics()));\n  }\n\n  /**\n   * Sets the text size to one derived from {@link CaptioningManager#getFontScale()}, or to a\n   * default size before API level 19.\n   */\n  public void setUserDefaultTextSize() {\n    float fontScale = Util.SDK_INT >= 19 && !isInEditMode() ? getUserCaptionFontScaleV19() : 1f;\n    setFractionalTextSize(DEFAULT_TEXT_SIZE_FRACTION * fontScale);\n  }\n\n  /**\n   * Sets the text size to be a fraction of the view's remaining height after its top and bottom\n   * padding have been subtracted.\n   * <p>\n   * Equivalent to {@code #setFractionalTextSize(fractionOfHeight, false)}.\n   *\n   * @param fractionOfHeight A fraction between 0 and 1.\n   */\n  public void setFractionalTextSize(float fractionOfHeight) {\n    setFractionalTextSize(fractionOfHeight, false);\n  }\n\n  /**\n   * Sets the text size to be a fraction of the height of this view.\n   *\n   * @param fractionOfHeight A fraction between 0 and 1.\n   * @param ignorePadding Set to true if {@code fractionOfHeight} should be interpreted as a\n   *     fraction of this view's height ignoring any top and bottom padding. Set to false if\n   *     {@code fractionOfHeight} should be interpreted as a fraction of this view's remaining\n   *     height after the top and bottom padding has been subtracted.\n   */\n  public void setFractionalTextSize(float fractionOfHeight, boolean ignorePadding) {\n    setTextSize(\n        ignorePadding\n            ? Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING\n            : Cue.TEXT_SIZE_TYPE_FRACTIONAL,\n        fractionOfHeight);\n  }\n\n  private void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) {\n    if (this.textSizeType == textSizeType && this.textSize == textSize) {\n      return;\n    }\n    this.textSizeType = textSizeType;\n    this.textSize = textSize;\n    // Invalidate to trigger drawing.\n    invalidate();\n  }\n\n  /**\n   * Sets whether styling embedded within the cues should be applied. Enabled by default.\n   * Overrides any setting made with {@link SubtitleView#setApplyEmbeddedFontSizes}.\n   *\n   * @param applyEmbeddedStyles Whether styling embedded within the cues should be applied.\n   */\n  public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) {\n    if (this.applyEmbeddedStyles == applyEmbeddedStyles\n        && this.applyEmbeddedFontSizes == applyEmbeddedStyles) {\n      return;\n    }\n    this.applyEmbeddedStyles = applyEmbeddedStyles;\n    this.applyEmbeddedFontSizes = applyEmbeddedStyles;\n    // Invalidate to trigger drawing.\n    invalidate();\n  }\n\n  /**\n   * Sets whether font sizes embedded within the cues should be applied. Enabled by default.\n   * Only takes effect if {@link SubtitleView#setApplyEmbeddedStyles} is set to true.\n   *\n   * @param applyEmbeddedFontSizes Whether font sizes embedded within the cues should be applied.\n   */\n  public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) {\n    if (this.applyEmbeddedFontSizes == applyEmbeddedFontSizes) {\n      return;\n    }\n    this.applyEmbeddedFontSizes = applyEmbeddedFontSizes;\n    // Invalidate to trigger drawing.\n    invalidate();\n  }\n\n  /**\n   * Sets the caption style to be equivalent to the one returned by\n   * {@link CaptioningManager#getUserStyle()}, or to a default style before API level 19.\n   */\n  public void setUserDefaultStyle() {\n    setStyle(\n        Util.SDK_INT >= 19 && isCaptionManagerEnabled() && !isInEditMode()\n            ? getUserCaptionStyleV19()\n            : CaptionStyleCompat.DEFAULT);\n  }\n\n  /**\n   * Sets the caption style.\n   *\n   * @param style A style for the view.\n   */\n  public void setStyle(CaptionStyleCompat style) {\n    if (this.style == style) {\n      return;\n    }\n    this.style = style;\n    // Invalidate to trigger drawing.\n    invalidate();\n  }\n\n  /**\n   * Sets the bottom padding fraction to apply when {@link Cue#line} is {@link Cue#DIMEN_UNSET},\n   * as a fraction of the view's remaining height after its top and bottom padding have been\n   * subtracted.\n   * <p>\n   * Note that this padding is applied in addition to any standard view padding.\n   *\n   * @param bottomPaddingFraction The bottom padding fraction.\n   */\n  public void setBottomPaddingFraction(float bottomPaddingFraction) {\n    if (this.bottomPaddingFraction == bottomPaddingFraction) {\n      return;\n    }\n    this.bottomPaddingFraction = bottomPaddingFraction;\n    // Invalidate to trigger drawing.\n    invalidate();\n  }\n\n  @Override\n  public void dispatchDraw(Canvas canvas) {\n    List<Cue> cues = this.cues;\n    if (cues == null || cues.isEmpty()) {\n      return;\n    }\n\n    int rawViewHeight = getHeight();\n\n    // Calculate the cue box bounds relative to the canvas after padding is taken into account.\n    int left = getPaddingLeft();\n    int top = getPaddingTop();\n    int right = getWidth() - getPaddingRight();\n    int bottom = rawViewHeight - getPaddingBottom();\n    if (bottom <= top || right <= left) {\n      // No space to draw subtitles.\n      return;\n    }\n    int viewHeightMinusPadding = bottom - top;\n\n    float defaultViewTextSizePx =\n        resolveTextSize(textSizeType, textSize, rawViewHeight, viewHeightMinusPadding);\n    if (defaultViewTextSizePx <= 0) {\n      // Text has no height.\n      return;\n    }\n\n    int cueCount = cues.size();\n    for (int i = 0; i < cueCount; i++) {\n      Cue cue = cues.get(i);\n      float cueTextSizePx = resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding);\n      SubtitlePainter painter = painters.get(i);\n      painter.draw(\n          cue,\n          applyEmbeddedStyles,\n          applyEmbeddedFontSizes,\n          style,\n          defaultViewTextSizePx,\n          cueTextSizePx,\n          bottomPaddingFraction,\n          canvas,\n          left,\n          top,\n          right,\n          bottom);\n    }\n  }\n\n  private float resolveCueTextSize(Cue cue, int rawViewHeight, int viewHeightMinusPadding) {\n    if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) {\n      return 0;\n    }\n    float defaultCueTextSizePx =\n        resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding);\n    return Math.max(defaultCueTextSizePx, 0);\n  }\n\n  private float resolveTextSize(\n      @Cue.TextSizeType int textSizeType,\n      float textSize,\n      int rawViewHeight,\n      int viewHeightMinusPadding) {\n    switch (textSizeType) {\n      case Cue.TEXT_SIZE_TYPE_ABSOLUTE:\n        return textSize;\n      case Cue.TEXT_SIZE_TYPE_FRACTIONAL:\n        return textSize * viewHeightMinusPadding;\n      case Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING:\n        return textSize * rawViewHeight;\n      case Cue.TYPE_UNSET:\n      default:\n        return Cue.DIMEN_UNSET;\n    }\n  }\n\n  @TargetApi(19)\n  private boolean isCaptionManagerEnabled() {\n    CaptioningManager captioningManager =\n        (CaptioningManager) getContext().getSystemService(Context.CAPTIONING_SERVICE);\n    return captioningManager.isEnabled();\n  }\n\n  @TargetApi(19)\n  private float getUserCaptionFontScaleV19() {\n    CaptioningManager captioningManager =\n        (CaptioningManager) getContext().getSystemService(Context.CAPTIONING_SERVICE);\n    return captioningManager.getFontScale();\n  }\n\n  @TargetApi(19)\n  private CaptionStyleCompat getUserCaptionStyleV19() {\n    CaptioningManager captioningManager =\n        (CaptioningManager) getContext().getSystemService(Context.CAPTIONING_SERVICE);\n    return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.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.ui;\n\nimport androidx.annotation.Nullable;\nimport android.view.View;\n\n/**\n * Interface for time bar views that can display a playback position, buffered position, duration\n * and ad markers, and that have a listener for scrubbing (seeking) events.\n */\npublic interface TimeBar {\n\n  /**\n   * Adds a listener for scrubbing events.\n   *\n   * @param listener The listener to add.\n   */\n  void addListener(OnScrubListener listener);\n\n  /**\n   * Removes a listener for scrubbing events.\n   *\n   * @param listener The listener to remove.\n   */\n  void removeListener(OnScrubListener listener);\n\n  /**\n   * @see View#isEnabled()\n   */\n  void setEnabled(boolean enabled);\n\n  /**\n   * Sets the position increment for key presses and accessibility actions, in milliseconds.\n   * <p>\n   * Clears any increment specified in a preceding call to {@link #setKeyCountIncrement(int)}.\n   *\n   * @param time The time increment, in milliseconds.\n   */\n  void setKeyTimeIncrement(long time);\n\n  /**\n   * Sets the position increment for key presses and accessibility actions, as a number of\n   * increments that divide the duration of the media. For example, passing 20 will cause key\n   * presses to increment/decrement the position by 1/20th of the duration (if known).\n   * <p>\n   * Clears any increment specified in a preceding call to {@link #setKeyTimeIncrement(long)}.\n   *\n   * @param count The number of increments that divide the duration of the media.\n   */\n  void setKeyCountIncrement(int count);\n\n  /**\n   * Sets the current position.\n   *\n   * @param position The current position to show, in milliseconds.\n   */\n  void setPosition(long position);\n\n  /**\n   * Sets the buffered position.\n   *\n   * @param bufferedPosition The current buffered position to show, in milliseconds.\n   */\n  void setBufferedPosition(long bufferedPosition);\n\n  /**\n   * Sets the duration.\n   *\n   * @param duration The duration to show, in milliseconds.\n   */\n  void setDuration(long duration);\n\n  /**\n   * Returns the preferred delay in milliseconds of media time after which the time bar position\n   * should be updated.\n   *\n   * @return Preferred delay, in milliseconds of media time.\n   */\n  long getPreferredUpdateDelay();\n\n  /**\n   * Sets the times of ad groups and whether each ad group has been played.\n   *\n   * @param adGroupTimesMs An array where the first {@code adGroupCount} elements are the times of\n   *     ad groups in milliseconds. May be {@code null} if there are no ad groups.\n   * @param playedAdGroups An array where the first {@code adGroupCount} elements indicate whether\n   *     the corresponding ad groups have been played. May be {@code null} if there are no ad\n   *     groups.\n   * @param adGroupCount The number of ad groups.\n   */\n  void setAdGroupTimesMs(@Nullable long[] adGroupTimesMs, @Nullable boolean[] playedAdGroups,\n      int adGroupCount);\n\n  /**\n   * Listener for scrubbing events.\n   */\n  interface OnScrubListener {\n\n    /**\n     * Called when the user starts moving the scrubber.\n     *\n     * @param timeBar The time bar.\n     * @param position The scrub position in milliseconds.\n     */\n    void onScrubStart(TimeBar timeBar, long position);\n\n    /**\n     * Called when the user moves the scrubber.\n     *\n     * @param timeBar The time bar.\n     * @param position The scrub position in milliseconds.\n     */\n    void onScrubMove(TimeBar timeBar, long position);\n\n    /**\n     * Called when the user stops moving the scrubber.\n     *\n     * @param timeBar The time bar.\n     * @param position The scrub position in milliseconds.\n     * @param canceled Whether scrubbing was canceled.\n     */\n    void onScrubStop(TimeBar timeBar, long position, boolean canceled);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackNameProvider.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.ui;\n\nimport com.google.android.exoplayer2.Format;\n\n/** Converts {@link Format}s to user readable track names. */\npublic interface TrackNameProvider {\n\n  /** Returns a user readable track name for the given {@link Format}. */\n  String getTrackName(Format format);\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.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.ui;\n\nimport android.app.AlertDialog;\nimport android.app.Dialog;\nimport android.content.Context;\nimport androidx.annotation.Nullable;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionUtil;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.Collections;\nimport java.util.List;\n\n/** Builder for a dialog with a {@link TrackSelectionView}. */\npublic final class TrackSelectionDialogBuilder {\n\n  /** Callback which is invoked when a track selection has been made. */\n  public interface DialogCallback {\n\n    /**\n     * Called when tracks are selected.\n     *\n     * @param isDisabled Whether the renderer is disabled.\n     * @param overrides List of selected track selection overrides for the renderer.\n     */\n    void onTracksSelected(boolean isDisabled, List<SelectionOverride> overrides);\n  }\n\n  private final Context context;\n  private final CharSequence title;\n  private final MappedTrackInfo mappedTrackInfo;\n  private final int rendererIndex;\n  private final DialogCallback callback;\n\n  private boolean allowAdaptiveSelections;\n  private boolean allowMultipleOverrides;\n  private boolean showDisableOption;\n  @Nullable private TrackNameProvider trackNameProvider;\n  private boolean isDisabled;\n  private List<SelectionOverride> overrides;\n\n  /**\n   * Creates a builder for a track selection dialog.\n   *\n   * @param context The context of the dialog.\n   * @param title The title of the dialog.\n   * @param mappedTrackInfo The {@link MappedTrackInfo} containing the track information.\n   * @param rendererIndex The renderer index in the {@code mappedTrackInfo} for which the track\n   *     selection is shown.\n   * @param callback The {@link DialogCallback} invoked when a track selection has been made.\n   */\n  public TrackSelectionDialogBuilder(\n      Context context,\n      CharSequence title,\n      MappedTrackInfo mappedTrackInfo,\n      int rendererIndex,\n      DialogCallback callback) {\n    this.context = context;\n    this.title = title;\n    this.mappedTrackInfo = mappedTrackInfo;\n    this.rendererIndex = rendererIndex;\n    this.callback = callback;\n    overrides = Collections.emptyList();\n  }\n\n  /**\n   * Creates a builder for a track selection dialog which automatically updates a {@link\n   * DefaultTrackSelector}.\n   *\n   * @param context The context of the dialog.\n   * @param title The title of the dialog.\n   * @param trackSelector A {@link DefaultTrackSelector} whose current selection is used to set up\n   *     the dialog and which is updated when new tracks are selected in the dialog.\n   * @param rendererIndex The renderer index in the {@code trackSelector} for which the track\n   *     selection is shown.\n   */\n  public TrackSelectionDialogBuilder(\n      Context context, CharSequence title, DefaultTrackSelector trackSelector, int rendererIndex) {\n    this.context = context;\n    this.title = title;\n    this.mappedTrackInfo = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());\n    this.rendererIndex = rendererIndex;\n\n    TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);\n    DefaultTrackSelector.Parameters selectionParameters = trackSelector.getParameters();\n    isDisabled = selectionParameters.getRendererDisabled(rendererIndex);\n    SelectionOverride override =\n        selectionParameters.getSelectionOverride(rendererIndex, rendererTrackGroups);\n    overrides = override == null ? Collections.emptyList() : Collections.singletonList(override);\n\n    this.callback =\n        (newIsDisabled, newOverrides) ->\n            trackSelector.setParameters(\n                TrackSelectionUtil.updateParametersWithOverride(\n                    selectionParameters,\n                    rendererIndex,\n                    rendererTrackGroups,\n                    newIsDisabled,\n                    newOverrides.isEmpty() ? null : newOverrides.get(0)));\n  }\n\n  /**\n   * Sets whether the selection is initially shown as disabled.\n   *\n   * @param isDisabled Whether the selection is initially shown as disabled.\n   * @return This builder, for convenience.\n   */\n  public TrackSelectionDialogBuilder setIsDisabled(boolean isDisabled) {\n    this.isDisabled = isDisabled;\n    return this;\n  }\n\n  /**\n   * Sets the initial selection override to show.\n   *\n   * @param override The initial override to show, or null for no override.\n   * @return This builder, for convenience.\n   */\n  public TrackSelectionDialogBuilder setOverride(@Nullable SelectionOverride override) {\n    return setOverrides(\n        override == null ? Collections.emptyList() : Collections.singletonList(override));\n  }\n\n  /**\n   * Sets the list of initial selection overrides to show.\n   *\n   * <p>Note that only the first override will be used unless {@link\n   * #setAllowMultipleOverrides(boolean)} is set to {@code true}.\n   *\n   * @param overrides The list of initial overrides to show. There must be at most one override for\n   *     each track group.\n   * @return This builder, for convenience.\n   */\n  public TrackSelectionDialogBuilder setOverrides(List<SelectionOverride> overrides) {\n    this.overrides = overrides;\n    return this;\n  }\n\n  /**\n   * Sets whether adaptive selections (consisting of more than one track) can be made.\n   *\n   * <p>For the selection view to enable adaptive selection it is necessary both for this feature to\n   * be enabled, and for the target renderer to support adaptation between the available tracks.\n   *\n   * @param allowAdaptiveSelections Whether adaptive selection is enabled.\n   * @return This builder, for convenience.\n   */\n  public TrackSelectionDialogBuilder setAllowAdaptiveSelections(boolean allowAdaptiveSelections) {\n    this.allowAdaptiveSelections = allowAdaptiveSelections;\n    return this;\n  }\n\n  /**\n   * Sets whether multiple overrides can be set and selected, i.e. tracks from multiple track groups\n   * can be selected.\n   *\n   * @param allowMultipleOverrides Whether multiple track selection overrides are allowed.\n   * @return This builder, for convenience.\n   */\n  public TrackSelectionDialogBuilder setAllowMultipleOverrides(boolean allowMultipleOverrides) {\n    this.allowMultipleOverrides = allowMultipleOverrides;\n    return this;\n  }\n\n  /**\n   * Sets whether an option is available for disabling the renderer.\n   *\n   * @param showDisableOption Whether the disable option is shown.\n   * @return This builder, for convenience.\n   */\n  public TrackSelectionDialogBuilder setShowDisableOption(boolean showDisableOption) {\n    this.showDisableOption = showDisableOption;\n    return this;\n  }\n\n  /**\n   * Sets the {@link TrackNameProvider} used to generate the user visible name of each track and\n   * updates the view with track names queried from the specified provider.\n   *\n   * @param trackNameProvider The {@link TrackNameProvider} to use, or null to use the default.\n   */\n  public TrackSelectionDialogBuilder setTrackNameProvider(\n      @Nullable TrackNameProvider trackNameProvider) {\n    this.trackNameProvider = trackNameProvider;\n    return this;\n  }\n\n  /** Builds the dialog. */\n  public AlertDialog build() {\n    AlertDialog.Builder builder = new AlertDialog.Builder(context);\n\n    // Inflate with the builder's context to ensure the correct style is used.\n    LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());\n    View dialogView = dialogInflater.inflate(R.layout.exo_track_selection_dialog, /* root= */ null);\n\n    TrackSelectionView selectionView = dialogView.findViewById(R.id.exo_track_selection_view);\n    selectionView.setAllowMultipleOverrides(allowMultipleOverrides);\n    selectionView.setAllowAdaptiveSelections(allowAdaptiveSelections);\n    selectionView.setShowDisableOption(showDisableOption);\n    if (trackNameProvider != null) {\n      selectionView.setTrackNameProvider(trackNameProvider);\n    }\n    selectionView.init(mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ null);\n    Dialog.OnClickListener okClickListener =\n        (dialog, which) ->\n            callback.onTracksSelected(selectionView.getIsDisabled(), selectionView.getOverrides());\n\n    return builder\n        .setTitle(title)\n        .setView(dialogView)\n        .setPositiveButton(android.R.string.ok, okClickListener)\n        .setNegativeButton(android.R.string.cancel, null)\n        .create();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.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.ui;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport androidx.annotation.AttrRes;\nimport androidx.annotation.Nullable;\nimport android.util.AttributeSet;\nimport android.util.Pair;\nimport android.util.SparseArray;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.CheckedTextView;\nimport android.widget.LinearLayout;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.RequiresNonNull;\n\n/** A view for making track selections. */\npublic class TrackSelectionView extends LinearLayout {\n\n  /** Listener for changes to the selected tracks. */\n  public interface TrackSelectionListener {\n\n    /**\n     * Called when the selected tracks changed.\n     *\n     * @param isDisabled Whether the renderer is disabled.\n     * @param overrides List of selected track selection overrides for the renderer.\n     */\n    void onTrackSelectionChanged(boolean isDisabled, List<SelectionOverride> overrides);\n  }\n\n  private final int selectableItemBackgroundResourceId;\n  private final LayoutInflater inflater;\n  private final CheckedTextView disableView;\n  private final CheckedTextView defaultView;\n  private final ComponentListener componentListener;\n  private final SparseArray<SelectionOverride> overrides;\n\n  private boolean allowAdaptiveSelections;\n  private boolean allowMultipleOverrides;\n\n  private TrackNameProvider trackNameProvider;\n  private CheckedTextView[][] trackViews;\n\n  @MonotonicNonNull private MappedTrackInfo mappedTrackInfo;\n  private int rendererIndex;\n  private TrackGroupArray trackGroups;\n  private boolean isDisabled;\n  @Nullable private TrackSelectionListener listener;\n\n  /** Creates a track selection view. */\n  public TrackSelectionView(Context context) {\n    this(context, null);\n  }\n\n  /** Creates a track selection view. */\n  public TrackSelectionView(Context context, @Nullable AttributeSet attrs) {\n    this(context, attrs, 0);\n  }\n\n  /** Creates a track selection view. */\n  @SuppressWarnings(\"nullness\")\n  public TrackSelectionView(\n      Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n    setOrientation(LinearLayout.VERTICAL);\n\n    overrides = new SparseArray<>();\n\n    // Don't save view hierarchy as it needs to be reinitialized with a call to init.\n    setSaveFromParentEnabled(false);\n\n    TypedArray attributeArray =\n        context\n            .getTheme()\n            .obtainStyledAttributes(new int[] {android.R.attr.selectableItemBackground});\n    selectableItemBackgroundResourceId = attributeArray.getResourceId(0, 0);\n    attributeArray.recycle();\n\n    inflater = LayoutInflater.from(context);\n    componentListener = new ComponentListener();\n    trackNameProvider = new DefaultTrackNameProvider(getResources());\n    trackGroups = TrackGroupArray.EMPTY;\n\n    // View for disabling the renderer.\n    disableView =\n        (CheckedTextView)\n            inflater.inflate(android.R.layout.simple_list_item_single_choice, this, false);\n    disableView.setBackgroundResource(selectableItemBackgroundResourceId);\n    disableView.setText(R.string.exo_track_selection_none);\n    disableView.setEnabled(false);\n    disableView.setFocusable(true);\n    disableView.setOnClickListener(componentListener);\n    disableView.setVisibility(View.GONE);\n    addView(disableView);\n    // Divider view.\n    addView(inflater.inflate(R.layout.exo_list_divider, this, false));\n    // View for clearing the override to allow the selector to use its default selection logic.\n    defaultView =\n        (CheckedTextView)\n            inflater.inflate(android.R.layout.simple_list_item_single_choice, this, false);\n    defaultView.setBackgroundResource(selectableItemBackgroundResourceId);\n    defaultView.setText(R.string.exo_track_selection_auto);\n    defaultView.setEnabled(false);\n    defaultView.setFocusable(true);\n    defaultView.setOnClickListener(componentListener);\n    addView(defaultView);\n  }\n\n  /**\n   * Sets whether adaptive selections (consisting of more than one track) can be made using this\n   * selection view.\n   *\n   * <p>For the view to enable adaptive selection it is necessary both for this feature to be\n   * enabled, and for the target renderer to support adaptation between the available tracks.\n   *\n   * @param allowAdaptiveSelections Whether adaptive selection is enabled.\n   */\n  public void setAllowAdaptiveSelections(boolean allowAdaptiveSelections) {\n    if (this.allowAdaptiveSelections != allowAdaptiveSelections) {\n      this.allowAdaptiveSelections = allowAdaptiveSelections;\n      updateViews();\n    }\n  }\n\n  /**\n   * Sets whether tracks from multiple track groups can be selected. This results in multiple {@link\n   * SelectionOverride SelectionOverrides} to be returned by {@link #getOverrides()}.\n   *\n   * @param allowMultipleOverrides Whether multiple track selection overrides can be selected.\n   */\n  public void setAllowMultipleOverrides(boolean allowMultipleOverrides) {\n    if (this.allowMultipleOverrides != allowMultipleOverrides) {\n      this.allowMultipleOverrides = allowMultipleOverrides;\n      if (!allowMultipleOverrides && overrides.size() > 1) {\n        for (int i = overrides.size() - 1; i > 0; i--) {\n          overrides.remove(i);\n        }\n      }\n      updateViews();\n    }\n  }\n\n  /**\n   * Sets whether an option is available for disabling the renderer.\n   *\n   * @param showDisableOption Whether the disable option is shown.\n   */\n  public void setShowDisableOption(boolean showDisableOption) {\n    disableView.setVisibility(showDisableOption ? View.VISIBLE : View.GONE);\n  }\n\n  /**\n   * Sets the {@link TrackNameProvider} used to generate the user visible name of each track and\n   * updates the view with track names queried from the specified provider.\n   *\n   * @param trackNameProvider The {@link TrackNameProvider} to use.\n   */\n  public void setTrackNameProvider(TrackNameProvider trackNameProvider) {\n    this.trackNameProvider = Assertions.checkNotNull(trackNameProvider);\n    updateViews();\n  }\n\n  /**\n   * Initialize the view to select tracks for a specified renderer using {@link MappedTrackInfo} and\n   * a set of {@link DefaultTrackSelector.Parameters}.\n   *\n   * @param mappedTrackInfo The {@link MappedTrackInfo}.\n   * @param rendererIndex The index of the renderer.\n   * @param isDisabled Whether the renderer should be initially shown as disabled.\n   * @param overrides List of initial overrides to be shown for this renderer. There must be at most\n   *     one override for each track group. If {@link #setAllowMultipleOverrides(boolean)} hasn't\n   *     been set to {@code true}, only the first override is used.\n   * @param listener An optional listener for track selection updates.\n   */\n  public void init(\n      MappedTrackInfo mappedTrackInfo,\n      int rendererIndex,\n      boolean isDisabled,\n      List<SelectionOverride> overrides,\n      @Nullable TrackSelectionListener listener) {\n    this.mappedTrackInfo = mappedTrackInfo;\n    this.rendererIndex = rendererIndex;\n    this.isDisabled = isDisabled;\n    this.listener = listener;\n    int maxOverrides = allowMultipleOverrides ? overrides.size() : Math.min(overrides.size(), 1);\n    for (int i = 0; i < maxOverrides; i++) {\n      SelectionOverride override = overrides.get(i);\n      this.overrides.put(override.groupIndex, override);\n    }\n    updateViews();\n  }\n\n  /** Returns whether the renderer is disabled. */\n  public boolean getIsDisabled() {\n    return isDisabled;\n  }\n\n  /**\n   * Returns the list of selected track selection overrides. There will be at most one override for\n   * each track group.\n   */\n  public List<SelectionOverride> getOverrides() {\n    List<SelectionOverride> overrideList = new ArrayList<>(overrides.size());\n    for (int i = 0; i < overrides.size(); i++) {\n      overrideList.add(overrides.valueAt(i));\n    }\n    return overrideList;\n  }\n\n  // Private methods.\n\n  private void updateViews() {\n    // Remove previous per-track views.\n    for (int i = getChildCount() - 1; i >= 3; i--) {\n      removeViewAt(i);\n    }\n\n    if (mappedTrackInfo == null) {\n      // The view is not initialized.\n      disableView.setEnabled(false);\n      defaultView.setEnabled(false);\n      return;\n    }\n    disableView.setEnabled(true);\n    defaultView.setEnabled(true);\n\n    trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);\n\n    // Add per-track views.\n    trackViews = new CheckedTextView[trackGroups.length][];\n    boolean enableMultipleChoiceForMultipleOverrides = shouldEnableMultiGroupSelection();\n    for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {\n      TrackGroup group = trackGroups.get(groupIndex);\n      boolean enableMultipleChoiceForAdaptiveSelections = shouldEnableAdaptiveSelection(groupIndex);\n      trackViews[groupIndex] = new CheckedTextView[group.length];\n      for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {\n        if (trackIndex == 0) {\n          addView(inflater.inflate(R.layout.exo_list_divider, this, false));\n        }\n        int trackViewLayoutId =\n            enableMultipleChoiceForAdaptiveSelections || enableMultipleChoiceForMultipleOverrides\n                ? android.R.layout.simple_list_item_multiple_choice\n                : android.R.layout.simple_list_item_single_choice;\n        CheckedTextView trackView =\n            (CheckedTextView) inflater.inflate(trackViewLayoutId, this, false);\n        trackView.setBackgroundResource(selectableItemBackgroundResourceId);\n        trackView.setText(trackNameProvider.getTrackName(group.getFormat(trackIndex)));\n        if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)\n            == RendererCapabilities.FORMAT_HANDLED) {\n          trackView.setFocusable(true);\n          trackView.setTag(Pair.create(groupIndex, trackIndex));\n          trackView.setOnClickListener(componentListener);\n        } else {\n          trackView.setFocusable(false);\n          trackView.setEnabled(false);\n        }\n        trackViews[groupIndex][trackIndex] = trackView;\n        addView(trackView);\n      }\n    }\n\n    updateViewStates();\n  }\n\n  private void updateViewStates() {\n    disableView.setChecked(isDisabled);\n    defaultView.setChecked(!isDisabled && overrides.size() == 0);\n    for (int i = 0; i < trackViews.length; i++) {\n      SelectionOverride override = overrides.get(i);\n      for (int j = 0; j < trackViews[i].length; j++) {\n        trackViews[i][j].setChecked(override != null && override.containsTrack(j));\n      }\n    }\n  }\n\n  private void onClick(View view) {\n    if (view == disableView) {\n      onDisableViewClicked();\n    } else if (view == defaultView) {\n      onDefaultViewClicked();\n    } else {\n      onTrackViewClicked(view);\n    }\n    updateViewStates();\n    if (listener != null) {\n      listener.onTrackSelectionChanged(getIsDisabled(), getOverrides());\n    }\n  }\n\n  private void onDisableViewClicked() {\n    isDisabled = true;\n    overrides.clear();\n  }\n\n  private void onDefaultViewClicked() {\n    isDisabled = false;\n    overrides.clear();\n  }\n\n  private void onTrackViewClicked(View view) {\n    isDisabled = false;\n    @SuppressWarnings(\"unchecked\")\n    Pair<Integer, Integer> tag = (Pair<Integer, Integer>) view.getTag();\n    int groupIndex = tag.first;\n    int trackIndex = tag.second;\n    SelectionOverride override = overrides.get(groupIndex);\n    Assertions.checkNotNull(mappedTrackInfo);\n    if (override == null) {\n      // Start new override.\n      if (!allowMultipleOverrides && overrides.size() > 0) {\n        // Removed other overrides if we don't allow multiple overrides.\n        overrides.clear();\n      }\n      overrides.put(groupIndex, new SelectionOverride(groupIndex, trackIndex));\n    } else {\n      // An existing override is being modified.\n      int overrideLength = override.length;\n      int[] overrideTracks = override.tracks;\n      boolean isCurrentlySelected = ((CheckedTextView) view).isChecked();\n      boolean isAdaptiveAllowed = shouldEnableAdaptiveSelection(groupIndex);\n      boolean isUsingCheckBox = isAdaptiveAllowed || shouldEnableMultiGroupSelection();\n      if (isCurrentlySelected && isUsingCheckBox) {\n        // Remove the track from the override.\n        if (overrideLength == 1) {\n          // The last track is being removed, so the override becomes empty.\n          overrides.remove(groupIndex);\n        } else {\n          int[] tracks = getTracksRemoving(overrideTracks, trackIndex);\n          overrides.put(groupIndex, new SelectionOverride(groupIndex, tracks));\n        }\n      } else if (!isCurrentlySelected) {\n        if (isAdaptiveAllowed) {\n          // Add new track to adaptive override.\n          int[] tracks = getTracksAdding(overrideTracks, trackIndex);\n          overrides.put(groupIndex, new SelectionOverride(groupIndex, tracks));\n        } else {\n          // Replace existing track in override.\n          overrides.put(groupIndex, new SelectionOverride(groupIndex, trackIndex));\n        }\n      }\n    }\n  }\n\n  @RequiresNonNull(\"mappedTrackInfo\")\n  private boolean shouldEnableAdaptiveSelection(int groupIndex) {\n    return allowAdaptiveSelections\n        && trackGroups.get(groupIndex).length > 1\n        && mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)\n            != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED;\n  }\n\n  private boolean shouldEnableMultiGroupSelection() {\n    return allowMultipleOverrides && trackGroups.length > 1;\n  }\n\n  private static int[] getTracksAdding(int[] tracks, int addedTrack) {\n    tracks = Arrays.copyOf(tracks, tracks.length + 1);\n    tracks[tracks.length - 1] = addedTrack;\n    return tracks;\n  }\n\n  private static int[] getTracksRemoving(int[] tracks, int removedTrack) {\n    int[] newTracks = new int[tracks.length - 1];\n    int trackCount = 0;\n    for (int track : tracks) {\n      if (track != removedTrack) {\n        newTracks[trackCount++] = track;\n      }\n    }\n    return newTracks;\n  }\n\n  // Internal classes.\n\n  private class ComponentListener implements OnClickListener {\n\n    @Override\n    public void onClick(View view) {\n      TrackSelectionView.this.onClick(view);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.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.ui.spherical;\n\nimport static com.google.android.exoplayer2.util.GlUtil.checkGlError;\n\nimport android.graphics.Canvas;\nimport android.graphics.PointF;\nimport android.graphics.Rect;\nimport android.graphics.SurfaceTexture;\nimport android.opengl.GLES11Ext;\nimport android.opengl.GLES20;\nimport androidx.annotation.Nullable;\nimport android.view.Surface;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.GlUtil;\nimport java.nio.FloatBuffer;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/**\n * Renders a canvas on a quad.\n *\n * <p>A CanvasRenderer can be created on any thread, but {@link #init()} needs to be called on the\n * GL thread before it can be rendered.\n */\npublic final class CanvasRenderer {\n\n  private static final float WIDTH_UNIT = 0.8f;\n  private static final float DISTANCE_UNIT = 1f;\n  private static final float X_UNIT = -WIDTH_UNIT / 2;\n  private static final float Y_UNIT = -0.3f;\n\n  // Standard vertex shader that passes through the texture data.\n  private static final String[] VERTEX_SHADER_CODE = {\n    \"uniform mat4 uMvpMatrix;\",\n    // 3D position data.\n    \"attribute vec3 aPosition;\",\n    // 2D UV vertices.\n    \"attribute vec2 aTexCoords;\",\n    \"varying vec2 vTexCoords;\",\n\n    // Standard transformation.\n    \"void main() {\",\n    \"  gl_Position = uMvpMatrix * vec4(aPosition, 1);\",\n    \"  vTexCoords = aTexCoords;\",\n    \"}\"\n  };\n\n  private static final String[] FRAGMENT_SHADER_CODE = {\n    // This is required since the texture data is GL_TEXTURE_EXTERNAL_OES.\n    \"#extension GL_OES_EGL_image_external : require\",\n    \"precision mediump float;\",\n    \"uniform samplerExternalOES uTexture;\",\n    \"varying vec2 vTexCoords;\",\n    \"void main() {\",\n    \"  gl_FragColor = texture2D(uTexture, vTexCoords);\",\n    \"}\"\n  };\n\n  // The quad has 2 triangles built from 4 total vertices. Each vertex has 3 position and 2 texture\n  // coordinates.\n  private static final int POSITION_COORDS_PER_VERTEX = 3;\n  private static final int TEXTURE_COORDS_PER_VERTEX = 2;\n  private static final int COORDS_PER_VERTEX =\n      POSITION_COORDS_PER_VERTEX + TEXTURE_COORDS_PER_VERTEX;\n  private static final int VERTEX_STRIDE_BYTES = COORDS_PER_VERTEX * C.BYTES_PER_FLOAT;\n  private static final int VERTEX_COUNT = 4;\n  private static final float HALF_PI = (float) (Math.PI / 2);\n\n  private final FloatBuffer vertexBuffer;\n  private final AtomicBoolean surfaceDirty;\n\n  private int width;\n  private int height;\n  private float heightUnit;\n\n  // Program-related GL items. These are only valid if program != 0.\n  private int program = 0;\n  private int mvpMatrixHandle;\n  private int positionHandle;\n  private int textureCoordsHandle;\n  private int textureHandle;\n  private int textureId;\n\n  // Components used to manage the Canvas that the View is rendered to. These are only valid after\n  // GL initialization. The client of this class acquires a Canvas from the Surface, writes to it\n  // and posts it. This marks the Surface as dirty. The GL code then updates the SurfaceTexture\n  // when rendering only if it is dirty.\n  @MonotonicNonNull private SurfaceTexture displaySurfaceTexture;\n  @MonotonicNonNull private Surface displaySurface;\n\n  public CanvasRenderer() {\n    vertexBuffer = GlUtil.createBuffer(COORDS_PER_VERTEX * VERTEX_COUNT);\n    surfaceDirty = new AtomicBoolean();\n  }\n\n  public void setSize(int width, int height) {\n    this.width = width;\n    this.height = height;\n    heightUnit = WIDTH_UNIT * height / width;\n\n    float[] vertexData = new float[COORDS_PER_VERTEX * VERTEX_COUNT];\n    int vertexDataIndex = 0;\n    for (int y = 0; y < 2; y++) {\n      for (int x = 0; x < 2; x++) {\n        vertexData[vertexDataIndex++] = X_UNIT + (WIDTH_UNIT * x);\n        vertexData[vertexDataIndex++] = Y_UNIT + (heightUnit * y);\n        vertexData[vertexDataIndex++] = -DISTANCE_UNIT;\n        vertexData[vertexDataIndex++] = x;\n        vertexData[vertexDataIndex++] = 1 - y;\n      }\n    }\n    vertexBuffer.position(0);\n    vertexBuffer.put(vertexData);\n  }\n\n  /**\n   * Calls {@link Surface#lockCanvas(Rect)}.\n   *\n   * @return {@link Canvas} for the View to render to or {@code null} if {@link #init()} has not yet\n   *     been called.\n   */\n  @Nullable\n  public Canvas lockCanvas() {\n    return displaySurface == null ? null : displaySurface.lockCanvas(/* inOutDirty= */ null);\n  }\n\n  /**\n   * Calls {@link Surface#unlockCanvasAndPost(Canvas)} and marks the SurfaceTexture as dirty.\n   *\n   * @param canvas the canvas returned from {@link #lockCanvas()}\n   */\n  public void unlockCanvasAndPost(@Nullable Canvas canvas) {\n    if (canvas == null || displaySurface == null) {\n      // glInit() hasn't run yet.\n      return;\n    }\n    displaySurface.unlockCanvasAndPost(canvas);\n  }\n\n  /** Finishes constructing this object on the GL Thread. */\n  public void init() {\n    if (program != 0) {\n      return;\n    }\n\n    // Create the program.\n    program = GlUtil.compileProgram(VERTEX_SHADER_CODE, FRAGMENT_SHADER_CODE);\n    mvpMatrixHandle = GLES20.glGetUniformLocation(program, \"uMvpMatrix\");\n    positionHandle = GLES20.glGetAttribLocation(program, \"aPosition\");\n    textureCoordsHandle = GLES20.glGetAttribLocation(program, \"aTexCoords\");\n    textureHandle = GLES20.glGetUniformLocation(program, \"uTexture\");\n    textureId = GlUtil.createExternalTexture();\n    checkGlError();\n\n    // Create the underlying SurfaceTexture with the appropriate size.\n    displaySurfaceTexture = new SurfaceTexture(textureId);\n    displaySurfaceTexture.setOnFrameAvailableListener(surfaceTexture -> surfaceDirty.set(true));\n    displaySurfaceTexture.setDefaultBufferSize(width, height);\n    displaySurface = new Surface(displaySurfaceTexture);\n  }\n\n  /**\n   * Renders the quad.\n   *\n   * @param viewProjectionMatrix Array of floats containing the quad's 4x4 perspective matrix in the\n   *     {@link android.opengl.Matrix} format.\n   */\n  public void draw(float[] viewProjectionMatrix) {\n    if (displaySurfaceTexture == null) {\n      return;\n    }\n\n    GLES20.glUseProgram(program);\n    checkGlError();\n\n    GLES20.glEnableVertexAttribArray(positionHandle);\n    GLES20.glEnableVertexAttribArray(textureCoordsHandle);\n    checkGlError();\n\n    GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, viewProjectionMatrix, 0);\n    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);\n    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);\n    GLES20.glUniform1i(textureHandle, 0);\n    checkGlError();\n\n    // Load position data.\n    vertexBuffer.position(0);\n    GLES20.glVertexAttribPointer(\n        positionHandle,\n        POSITION_COORDS_PER_VERTEX,\n        GLES20.GL_FLOAT,\n        false,\n        VERTEX_STRIDE_BYTES,\n        vertexBuffer);\n    checkGlError();\n\n    // Load texture data.\n    vertexBuffer.position(POSITION_COORDS_PER_VERTEX);\n    GLES20.glVertexAttribPointer(\n        textureCoordsHandle,\n        TEXTURE_COORDS_PER_VERTEX,\n        GLES20.GL_FLOAT,\n        false,\n        VERTEX_STRIDE_BYTES,\n        vertexBuffer);\n    checkGlError();\n\n    if (surfaceDirty.compareAndSet(true, false)) {\n      // If the Surface has been written to, get the new data onto the SurfaceTexture.\n      displaySurfaceTexture.updateTexImage();\n    }\n\n    // Render.\n    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT);\n    checkGlError();\n\n    GLES20.glDisableVertexAttribArray(positionHandle);\n    GLES20.glDisableVertexAttribArray(textureCoordsHandle);\n  }\n\n  /** Frees GL resources. */\n  public void shutdown() {\n    if (program != 0) {\n      GLES20.glDeleteProgram(program);\n      GLES20.glDeleteTextures(1, new int[] {textureId}, 0);\n    }\n\n    if (displaySurfaceTexture != null) {\n      displaySurfaceTexture.release();\n    }\n    if (displaySurface != null) {\n      displaySurface.release();\n    }\n  }\n\n  /**\n   * Translates an orientation into pixel coordinates on the canvas.\n   *\n   * <p>This is a minimal hit detection system that works for this quad because it has no model\n   * matrix. All the math is based on the fact that its size and distance are hard-coded into this\n   * class. For a more complex 3D mesh, a general bounding box and ray collision system would be\n   * required.\n   *\n   * @param yaw Yaw of the orientation in radians.\n   * @param pitch Pitch of the orientation in radians.\n   * @return A {@link PointF} which contains the translated coordinate, or null if the point is\n   *     outside of the quad's bounds.\n   */\n  @Nullable\n  public PointF translateClick(float yaw, float pitch) {\n    return internalTranslateClick(\n        yaw, pitch, X_UNIT, Y_UNIT, WIDTH_UNIT, heightUnit, width, height);\n  }\n\n  @Nullable\n  /* package */ static PointF internalTranslateClick(\n      float yaw,\n      float pitch,\n      float xUnit,\n      float yUnit,\n      float widthUnit,\n      float heightUnit,\n      int widthPixel,\n      int heightPixel) {\n    if (yaw >= HALF_PI || yaw <= -HALF_PI || pitch >= HALF_PI || pitch <= -HALF_PI) {\n      return null;\n    }\n    double clickXUnit = Math.tan(yaw) * DISTANCE_UNIT - xUnit;\n    double clickYUnit = Math.tan(pitch) * DISTANCE_UNIT - yUnit;\n    if (clickXUnit < 0 || clickXUnit > widthUnit || clickYUnit < 0 || clickYUnit > heightUnit) {\n      return null;\n    }\n    // Convert from the polar coordinates of the controller to the rectangular coordinates of the\n    // View. Note the negative yaw and pitch used to generate Android-compliant x and y coordinates.\n    float clickXPixel = (float) (widthPixel - clickXUnit * widthPixel / widthUnit);\n    float clickYPixel = (float) (heightPixel - clickYUnit * heightPixel / heightUnit);\n    return new PointF(clickXPixel, clickYPixel);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/GlViewGroup.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.ui.spherical;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.PointF;\nimport android.graphics.PorterDuff;\nimport android.os.SystemClock;\nimport androidx.annotation.AnyThread;\nimport androidx.annotation.UiThread;\nimport android.view.LayoutInflater;\nimport android.view.MotionEvent;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\nimport com.google.android.exoplayer2.util.Assertions;\n\n/** This View uses standard Android APIs to render its child Views to a texture. */\npublic final class GlViewGroup extends FrameLayout {\n\n  private final CanvasRenderer canvasRenderer;\n\n  /**\n   * @param context The Context the view is running in, through which it can access the current\n   *     theme, resources, etc.\n   * @param layoutId ID for an XML layout resource to load (e.g., * <code>R.layout.main_page</code>)\n   */\n  public GlViewGroup(Context context, int layoutId) {\n    super(context);\n    this.canvasRenderer = new CanvasRenderer();\n\n    LayoutInflater.from(context).inflate(layoutId, this);\n\n    measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);\n    int width = getMeasuredWidth();\n    int height = getMeasuredHeight();\n    Assertions.checkState(width > 0 && height > 0);\n    canvasRenderer.setSize(width, height);\n    setLayoutParams(new FrameLayout.LayoutParams(width, height));\n  }\n\n  /** Returns whether the view is currently visible. */\n  @UiThread\n  public boolean isVisible() {\n    int childCount = getChildCount();\n    for (int i = 0; i < childCount; i++) {\n      if (getChildAt(i).getVisibility() == VISIBLE) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  @Override\n  public void dispatchDraw(Canvas notUsed) {\n    Canvas glCanvas = canvasRenderer.lockCanvas();\n    if (glCanvas == null) {\n      // This happens if Android tries to draw this View before GL initialization completes. We need\n      // to retry until the draw call happens after GL invalidation.\n      postInvalidate();\n      return;\n    }\n\n    // Clear the canvas first.\n    glCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);\n    // Have Android render the child views.\n    super.dispatchDraw(glCanvas);\n    // Commit the changes.\n    canvasRenderer.unlockCanvasAndPost(glCanvas);\n  }\n\n  /**\n   * Simulates a click on the view.\n   *\n   * @param action Click action.\n   * @param yaw Yaw of the click's orientation in radians.\n   * @param pitch Pitch of the click's orientation in radians.\n   * @return Whether the click was simulated. If false then the view is not visible or the click was\n   *     outside of its bounds.\n   */\n  @UiThread\n  public boolean simulateClick(int action, float yaw, float pitch) {\n    if (!isVisible()) {\n      return false;\n    }\n    PointF point = canvasRenderer.translateClick(yaw, pitch);\n    if (point == null) {\n      return false;\n    }\n    long now = SystemClock.uptimeMillis();\n    MotionEvent event = MotionEvent.obtain(now, now, action, point.x, point.y, /* metaState= */ 1);\n    dispatchTouchEvent(event);\n    return true;\n  }\n\n  @AnyThread\n  public CanvasRenderer getRenderer() {\n    return canvasRenderer;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/OrientationListener.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.ui.spherical;\n\nimport android.hardware.Sensor;\nimport android.hardware.SensorEvent;\nimport android.hardware.SensorEventListener;\nimport android.hardware.SensorManager;\nimport android.opengl.Matrix;\nimport androidx.annotation.BinderThread;\nimport android.view.Display;\nimport android.view.Surface;\nimport com.google.android.exoplayer2.video.spherical.FrameRotationQueue;\n\n/**\n * Listens for orientation sensor events, converts event data to rotation matrix and roll value, and\n * notifies its own listeners.\n */\n/* package */ final class OrientationListener implements SensorEventListener {\n  /** A listener for orientation changes. */\n  public interface Listener {\n    /**\n     * Called on device orientation change.\n     *\n     * @param deviceOrientationMatrix A 4x4 matrix defining device orientation.\n     * @param deviceRoll Device roll value, in radians. The range of values is -&pi;/2 to &pi;/2.\n     */\n    void onOrientationChange(float[] deviceOrientationMatrix, float deviceRoll);\n  }\n\n  private final float[] deviceOrientationMatrix4x4 = new float[16];\n  private final float[] tempMatrix4x4 = new float[16];\n  private final float[] recenterMatrix4x4 = new float[16];\n  private final float[] angles = new float[3];\n  private final Display display;\n  private final Listener[] listeners;\n  private boolean recenterMatrixComputed;\n\n  public OrientationListener(Display display, Listener... listeners) {\n    this.display = display;\n    this.listeners = listeners;\n  }\n\n  @Override\n  @BinderThread\n  public void onSensorChanged(SensorEvent event) {\n    SensorManager.getRotationMatrixFromVector(deviceOrientationMatrix4x4, event.values);\n    rotateAroundZ(deviceOrientationMatrix4x4, display.getRotation());\n    float roll = extractRoll(deviceOrientationMatrix4x4);\n    // Rotation vector sensor assumes Y is parallel to the ground.\n    rotateYtoSky(deviceOrientationMatrix4x4);\n    recenter(deviceOrientationMatrix4x4);\n    notifyListeners(deviceOrientationMatrix4x4, roll);\n  }\n\n  @Override\n  public void onAccuracyChanged(Sensor sensor, int accuracy) {\n    // Do nothing.\n  }\n\n  private void notifyListeners(float[] deviceOrientationMatrix, float roll) {\n    for (Listener listener : listeners) {\n      listener.onOrientationChange(deviceOrientationMatrix, roll);\n    }\n  }\n\n  private void recenter(float[] matrix) {\n    if (!recenterMatrixComputed) {\n      FrameRotationQueue.computeRecenterMatrix(recenterMatrix4x4, matrix);\n      recenterMatrixComputed = true;\n    }\n    System.arraycopy(matrix, 0, tempMatrix4x4, 0, tempMatrix4x4.length);\n    Matrix.multiplyMM(matrix, 0, tempMatrix4x4, 0, recenterMatrix4x4, 0);\n  }\n\n  private float extractRoll(float[] matrix) {\n    // Remapping is required since we need the calculated roll of the phone to be independent of the\n    // phone's pitch & yaw.\n    SensorManager.remapCoordinateSystem(\n        matrix, SensorManager.AXIS_X, SensorManager.AXIS_MINUS_Z, tempMatrix4x4);\n    SensorManager.getOrientation(tempMatrix4x4, angles);\n    return angles[2];\n  }\n\n  private void rotateAroundZ(float[] matrix, int rotation) {\n    int xAxis;\n    int yAxis;\n    switch (rotation) {\n      case Surface.ROTATION_270:\n        xAxis = SensorManager.AXIS_MINUS_Y;\n        yAxis = SensorManager.AXIS_X;\n        break;\n      case Surface.ROTATION_180:\n        xAxis = SensorManager.AXIS_MINUS_X;\n        yAxis = SensorManager.AXIS_MINUS_Y;\n        break;\n      case Surface.ROTATION_90:\n        xAxis = SensorManager.AXIS_Y;\n        yAxis = SensorManager.AXIS_MINUS_X;\n        break;\n      case Surface.ROTATION_0:\n        return;\n      default:\n        throw new IllegalStateException();\n    }\n    System.arraycopy(matrix, 0, tempMatrix4x4, 0, tempMatrix4x4.length);\n    SensorManager.remapCoordinateSystem(tempMatrix4x4, xAxis, yAxis, matrix);\n  }\n\n  private static void rotateYtoSky(float[] matrix) {\n    Matrix.rotateM(matrix, 0, 90, 1, 0, 0);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/PointerRenderer.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.ui.spherical;\n\nimport static com.google.android.exoplayer2.util.GlUtil.checkGlError;\n\nimport android.opengl.GLES20;\nimport android.opengl.Matrix;\nimport com.google.android.exoplayer2.util.GlUtil;\nimport java.nio.FloatBuffer;\n\n/** Renders a pointer. */\npublic final class PointerRenderer {\n  // The pointer quad is 2 * SIZE units.\n  private static final float SIZE = .01f;\n  private static final float DISTANCE = 1;\n\n  // Standard vertex shader.\n  private static final String[] VERTEX_SHADER_CODE =\n      new String[] {\n        \"uniform mat4 uMvpMatrix;\",\n        \"attribute vec3 aPosition;\",\n        \"varying vec2 vCoords;\",\n\n        // Pass through normalized vertex coordinates.\n        \"void main() {\",\n        \"  gl_Position = uMvpMatrix * vec4(aPosition, 1);\",\n        \"  vCoords = aPosition.xy / vec2(\" + SIZE + \", \" + SIZE + \");\",\n        \"}\"\n      };\n\n  // Procedurally render a ring on the quad between the specified radii.\n  private static final String[] FRAGMENT_SHADER_CODE =\n      new String[] {\n        \"precision mediump float;\",\n        \"varying vec2 vCoords;\",\n\n        // Simple ring shader that is white between the radii and transparent elsewhere.\n        \"void main() {\",\n        \"  float r = length(vCoords);\",\n        // Blend the edges of the ring at .55 +/- .05 and .85 +/- .05.\n        \"  float alpha = smoothstep(0.5, 0.6, r) * (1.0 - smoothstep(0.8, 0.9, r));\",\n        \"  if (alpha == 0.0) {\",\n        \"    discard;\",\n        \"  } else {\",\n        \"    gl_FragColor = vec4(alpha);\",\n        \"  }\",\n        \"}\"\n      };\n\n  // Simple quad mesh.\n  private static final int COORDS_PER_VERTEX = 3;\n  private static final float[] VERTEX_DATA = {\n    -SIZE, -SIZE, -DISTANCE, SIZE, -SIZE, -DISTANCE, -SIZE, SIZE, -DISTANCE, SIZE, SIZE, -DISTANCE,\n  };\n  private final FloatBuffer vertexBuffer;\n\n  // The pointer doesn't have a real modelMatrix. Its distance is baked into the mesh and it\n  // uses a rotation matrix when rendered.\n  private final float[] modelViewProjectionMatrix;\n  // This is accessed on the binder & GL Threads.\n  private final float[] controllerOrientationMatrix;\n\n  // Program-related GL items. These are only valid if program != 0.\n  private int program = 0;\n  private int mvpMatrixHandle;\n  private int positionHandle;\n\n  public PointerRenderer() {\n    vertexBuffer = GlUtil.createBuffer(VERTEX_DATA);\n    modelViewProjectionMatrix = new float[16];\n    controllerOrientationMatrix = new float[16];\n    Matrix.setIdentityM(controllerOrientationMatrix, 0);\n  }\n\n  /** Finishes initialization of this object on the GL thread. */\n  public void init() {\n    if (program != 0) {\n      return;\n    }\n\n    program = GlUtil.compileProgram(VERTEX_SHADER_CODE, FRAGMENT_SHADER_CODE);\n    mvpMatrixHandle = GLES20.glGetUniformLocation(program, \"uMvpMatrix\");\n    positionHandle = GLES20.glGetAttribLocation(program, \"aPosition\");\n    checkGlError();\n  }\n\n  /**\n   * Renders the pointer.\n   *\n   * @param viewProjectionMatrix Scene's view projection matrix.\n   */\n  public void draw(float[] viewProjectionMatrix) {\n    // Configure shader.\n    GLES20.glUseProgram(program);\n    checkGlError();\n\n    synchronized (controllerOrientationMatrix) {\n      Matrix.multiplyMM(\n          modelViewProjectionMatrix, 0, viewProjectionMatrix, 0, controllerOrientationMatrix, 0);\n    }\n    GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, modelViewProjectionMatrix, 0);\n    checkGlError();\n\n    // Render quad.\n    GLES20.glEnableVertexAttribArray(positionHandle);\n    checkGlError();\n\n    GLES20.glVertexAttribPointer(\n        positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, /* stride= */ 0, vertexBuffer);\n    checkGlError();\n\n    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_DATA.length / COORDS_PER_VERTEX);\n    checkGlError();\n\n    GLES20.glDisableVertexAttribArray(positionHandle);\n  }\n\n  /** Frees GL resources. */\n  public void shutdown() {\n    if (program != 0) {\n      GLES20.glDeleteProgram(program);\n    }\n  }\n\n  /** Updates the pointer's position with the latest Controller pose. */\n  public void setControllerOrientation(float[] rotationMatrix) {\n    synchronized (controllerOrientationMatrix) {\n      System.arraycopy(rotationMatrix, 0, controllerOrientationMatrix, 0, rotationMatrix.length);\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.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.ui.spherical;\n\nimport static com.google.android.exoplayer2.util.GlUtil.checkGlError;\n\nimport android.opengl.GLES11Ext;\nimport android.opengl.GLES20;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.util.GlUtil;\nimport com.google.android.exoplayer2.video.spherical.Projection;\nimport java.nio.FloatBuffer;\nimport org.checkerframework.checker.nullness.qual.Nullable;\n\n/**\n * Utility class to render spherical meshes for video or images. Call {@link #init()} on the GL\n * thread when ready.\n */\n/* package */ final class ProjectionRenderer {\n\n  /**\n   * Returns whether {@code projection} is supported. At least it should have left mesh and there\n   * should be only one sub mesh per mesh.\n   */\n  public static boolean isSupported(Projection projection) {\n    Projection.Mesh leftMesh = projection.leftMesh;\n    Projection.Mesh rightMesh = projection.rightMesh;\n    return leftMesh.getSubMeshCount() == 1\n        && leftMesh.getSubMesh(0).textureId == Projection.SubMesh.VIDEO_TEXTURE_ID\n        && rightMesh.getSubMeshCount() == 1\n        && rightMesh.getSubMesh(0).textureId == Projection.SubMesh.VIDEO_TEXTURE_ID;\n  }\n\n  // Basic vertex & fragment shaders to render a mesh with 3D position & 2D texture data.\n  private static final String[] VERTEX_SHADER_CODE =\n      new String[] {\n        \"uniform mat4 uMvpMatrix;\",\n        \"uniform mat3 uTexMatrix;\",\n        \"attribute vec4 aPosition;\",\n        \"attribute vec2 aTexCoords;\",\n        \"varying vec2 vTexCoords;\",\n\n        // Standard transformation.\n        \"void main() {\",\n        \"  gl_Position = uMvpMatrix * aPosition;\",\n        \"  vTexCoords = (uTexMatrix * vec3(aTexCoords, 1)).xy;\",\n        \"}\"\n      };\n  private static final String[] FRAGMENT_SHADER_CODE =\n      new String[] {\n        // This is required since the texture data is GL_TEXTURE_EXTERNAL_OES.\n        \"#extension GL_OES_EGL_image_external : require\",\n        \"precision mediump float;\",\n\n        // Standard texture rendering shader.\n        \"uniform samplerExternalOES uTexture;\",\n        \"varying vec2 vTexCoords;\",\n        \"void main() {\",\n        \"  gl_FragColor = texture2D(uTexture, vTexCoords);\",\n        \"}\"\n      };\n\n  // Texture transform matrices.\n  private static final float[] TEX_MATRIX_WHOLE = {\n    1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f\n  };\n  private static final float[] TEX_MATRIX_TOP = {\n    1.0f, 0.0f, 0.0f, 0.0f, -0.5f, 0.0f, 0.0f, 0.5f, 1.0f\n  };\n  private static final float[] TEX_MATRIX_BOTTOM = {\n    1.0f, 0.0f, 0.0f, 0.0f, -0.5f, 0.0f, 0.0f, 1.0f, 1.0f\n  };\n  private static final float[] TEX_MATRIX_LEFT = {\n    0.5f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f\n  };\n  private static final float[] TEX_MATRIX_RIGHT = {\n    0.5f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.5f, 1.0f, 1.0f\n  };\n\n  private int stereoMode;\n  private @Nullable MeshData leftMeshData;\n  private @Nullable MeshData rightMeshData;\n\n  // Program related GL items. These are only valid if program != 0.\n  private int program;\n  private int mvpMatrixHandle;\n  private int uTexMatrixHandle;\n  private int positionHandle;\n  private int texCoordsHandle;\n  private int textureHandle;\n\n  /**\n   * Sets a {@link Projection} to be used.\n   *\n   * @param projection Contains the projection data to be rendered.\n   * @see #isSupported(Projection)\n   */\n  public void setProjection(Projection projection) {\n    if (!isSupported(projection)) {\n      return;\n    }\n    stereoMode = projection.stereoMode;\n    leftMeshData = new MeshData(projection.leftMesh.getSubMesh(0));\n    rightMeshData =\n        projection.singleMesh ? leftMeshData : new MeshData(projection.rightMesh.getSubMesh(0));\n  }\n\n  /** Initializes of the GL components. */\n  /* package */ void init() {\n    program = GlUtil.compileProgram(VERTEX_SHADER_CODE, FRAGMENT_SHADER_CODE);\n    mvpMatrixHandle = GLES20.glGetUniformLocation(program, \"uMvpMatrix\");\n    uTexMatrixHandle = GLES20.glGetUniformLocation(program, \"uTexMatrix\");\n    positionHandle = GLES20.glGetAttribLocation(program, \"aPosition\");\n    texCoordsHandle = GLES20.glGetAttribLocation(program, \"aTexCoords\");\n    textureHandle = GLES20.glGetUniformLocation(program, \"uTexture\");\n  }\n\n  /**\n   * Renders the mesh. If the projection hasn't been set, does nothing. This must be called on the\n   * GL thread.\n   *\n   * @param textureId GL_TEXTURE_EXTERNAL_OES used for this mesh.\n   * @param mvpMatrix The Model View Projection matrix.\n   * @param rightEye Whether the right eye view should be drawn. If {@code false}, the left eye view\n   *     is drawn.\n   */\n  /* package */ void draw(int textureId, float[] mvpMatrix, boolean rightEye) {\n    MeshData meshData = rightEye ? rightMeshData : leftMeshData;\n    if (meshData == null) {\n      return;\n    }\n\n    // Configure shader.\n    GLES20.glUseProgram(program);\n    checkGlError();\n\n    GLES20.glEnableVertexAttribArray(positionHandle);\n    GLES20.glEnableVertexAttribArray(texCoordsHandle);\n    checkGlError();\n\n    float[] texMatrix;\n    if (stereoMode == C.STEREO_MODE_TOP_BOTTOM) {\n      texMatrix = rightEye ? TEX_MATRIX_BOTTOM : TEX_MATRIX_TOP;\n    } else if (stereoMode == C.STEREO_MODE_LEFT_RIGHT) {\n      texMatrix = rightEye ? TEX_MATRIX_RIGHT : TEX_MATRIX_LEFT;\n    } else {\n      texMatrix = TEX_MATRIX_WHOLE;\n    }\n    GLES20.glUniformMatrix3fv(uTexMatrixHandle, 1, false, texMatrix, 0);\n\n    GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0);\n    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);\n    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);\n    GLES20.glUniform1i(textureHandle, 0);\n    checkGlError();\n\n    // Load position data.\n    GLES20.glVertexAttribPointer(\n        positionHandle,\n        Projection.POSITION_COORDS_PER_VERTEX,\n        GLES20.GL_FLOAT,\n        false,\n        Projection.POSITION_COORDS_PER_VERTEX * C.BYTES_PER_FLOAT,\n        meshData.vertexBuffer);\n    checkGlError();\n\n    // Load texture data.\n    GLES20.glVertexAttribPointer(\n        texCoordsHandle,\n        Projection.TEXTURE_COORDS_PER_VERTEX,\n        GLES20.GL_FLOAT,\n        false,\n        Projection.TEXTURE_COORDS_PER_VERTEX * C.BYTES_PER_FLOAT,\n        meshData.textureBuffer);\n    checkGlError();\n\n    // Render.\n    GLES20.glDrawArrays(meshData.drawMode, 0, meshData.vertexCount);\n    checkGlError();\n\n    GLES20.glDisableVertexAttribArray(positionHandle);\n    GLES20.glDisableVertexAttribArray(texCoordsHandle);\n  }\n\n  /** Cleans up the GL resources. */\n  /* package */ void shutdown() {\n    if (program != 0) {\n      GLES20.glDeleteProgram(program);\n    }\n  }\n\n  private static class MeshData {\n    private final int vertexCount;\n    private final FloatBuffer vertexBuffer;\n    private final FloatBuffer textureBuffer;\n    @Projection.DrawMode private final int drawMode;\n\n    public MeshData(Projection.SubMesh subMesh) {\n      vertexCount = subMesh.getVertexCount();\n      vertexBuffer = GlUtil.createBuffer(subMesh.vertices);\n      textureBuffer = GlUtil.createBuffer(subMesh.textureCoords);\n      switch (subMesh.mode) {\n        case Projection.DRAW_MODE_TRIANGLES_STRIP:\n          drawMode = GLES20.GL_TRIANGLE_STRIP;\n          break;\n        case Projection.DRAW_MODE_TRIANGLES_FAN:\n          drawMode = GLES20.GL_TRIANGLE_FAN;\n          break;\n        case Projection.DRAW_MODE_TRIANGLES:\n        default:\n          drawMode = GLES20.GL_TRIANGLES;\n          break;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.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.ui.spherical;\n\nimport static com.google.android.exoplayer2.util.GlUtil.checkGlError;\n\nimport android.graphics.SurfaceTexture;\nimport android.opengl.GLES20;\nimport android.opengl.Matrix;\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.GlUtil;\nimport com.google.android.exoplayer2.util.TimedValueQueue;\nimport com.google.android.exoplayer2.video.VideoFrameMetadataListener;\nimport com.google.android.exoplayer2.video.spherical.CameraMotionListener;\nimport com.google.android.exoplayer2.video.spherical.FrameRotationQueue;\nimport com.google.android.exoplayer2.video.spherical.Projection;\nimport com.google.android.exoplayer2.video.spherical.ProjectionDecoder;\nimport java.util.Arrays;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\n\n/** Renders a GL Scene. */\npublic final class SceneRenderer implements VideoFrameMetadataListener, CameraMotionListener {\n\n  private final AtomicBoolean frameAvailable;\n  private final AtomicBoolean resetRotationAtNextFrame;\n  private final ProjectionRenderer projectionRenderer;\n  private final FrameRotationQueue frameRotationQueue;\n  private final TimedValueQueue<Long> sampleTimestampQueue;\n  private final TimedValueQueue<Projection> projectionQueue;\n  private final float[] rotationMatrix;\n  private final float[] tempMatrix;\n\n  // Used by GL thread only\n  private int textureId;\n  private @MonotonicNonNull SurfaceTexture surfaceTexture;\n\n  // Used by other threads only\n  private volatile @C.StreamType int defaultStereoMode;\n  private @C.StreamType int lastStereoMode;\n  private @Nullable byte[] lastProjectionData;\n\n  // Methods called on any thread.\n\n  public SceneRenderer() {\n    frameAvailable = new AtomicBoolean();\n    resetRotationAtNextFrame = new AtomicBoolean(true);\n    projectionRenderer = new ProjectionRenderer();\n    frameRotationQueue = new FrameRotationQueue();\n    sampleTimestampQueue = new TimedValueQueue<>();\n    projectionQueue = new TimedValueQueue<>();\n    rotationMatrix = new float[16];\n    tempMatrix = new float[16];\n    defaultStereoMode = C.STEREO_MODE_MONO;\n    lastStereoMode = Format.NO_VALUE;\n  }\n\n  /**\n   * Sets the default stereo mode. If the played video doesn't contain a stereo mode the default one\n   * is used.\n   *\n   * @param stereoMode A {@link C.StereoMode} value.\n   */\n  public void setDefaultStereoMode(@C.StereoMode int stereoMode) {\n    defaultStereoMode = stereoMode;\n  }\n\n  // Methods called on GL thread.\n\n  /** Initializes the renderer. */\n  public SurfaceTexture init() {\n    // Set the background frame color. This is only visible if the display mesh isn't a full sphere.\n    GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);\n    checkGlError();\n\n    projectionRenderer.init();\n    checkGlError();\n\n    textureId = GlUtil.createExternalTexture();\n    surfaceTexture = new SurfaceTexture(textureId);\n    surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> frameAvailable.set(true));\n    return surfaceTexture;\n  }\n\n  /**\n   * Draws the scene with a given eye pose and type.\n   *\n   * @param viewProjectionMatrix 16 element GL matrix.\n   * @param rightEye Whether the right eye view should be drawn. If {@code false}, the left eye view\n   *     is drawn.\n   */\n  public void drawFrame(float[] viewProjectionMatrix, boolean rightEye) {\n    // glClear isn't strictly necessary when rendering fully spherical panoramas, but it can improve\n    // performance on tiled renderers by causing the GPU to discard previous data.\n    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);\n    checkGlError();\n\n    if (frameAvailable.compareAndSet(true, false)) {\n      Assertions.checkNotNull(surfaceTexture).updateTexImage();\n      checkGlError();\n      if (resetRotationAtNextFrame.compareAndSet(true, false)) {\n        Matrix.setIdentityM(rotationMatrix, 0);\n      }\n      long lastFrameTimestampNs = surfaceTexture.getTimestamp();\n      Long sampleTimestampUs = sampleTimestampQueue.poll(lastFrameTimestampNs);\n      if (sampleTimestampUs != null) {\n        frameRotationQueue.pollRotationMatrix(rotationMatrix, sampleTimestampUs);\n      }\n      Projection projection = projectionQueue.pollFloor(lastFrameTimestampNs);\n      if (projection != null) {\n        projectionRenderer.setProjection(projection);\n      }\n    }\n    Matrix.multiplyMM(tempMatrix, 0, viewProjectionMatrix, 0, rotationMatrix, 0);\n    projectionRenderer.draw(textureId, tempMatrix, rightEye);\n  }\n\n  /** Cleans up the GL resources. */\n  public void shutdown() {\n    projectionRenderer.shutdown();\n  }\n\n  // Methods called on playback thread.\n\n  // VideoFrameMetadataListener implementation.\n\n  @Override\n  public void onVideoFrameAboutToBeRendered(\n      long presentationTimeUs, long releaseTimeNs, Format format) {\n    sampleTimestampQueue.add(releaseTimeNs, presentationTimeUs);\n    setProjection(format.projectionData, format.stereoMode, releaseTimeNs);\n  }\n\n  // CameraMotionListener implementation.\n\n  @Override\n  public void onCameraMotion(long timeUs, float[] rotation) {\n    frameRotationQueue.setRotation(timeUs, rotation);\n  }\n\n  @Override\n  public void onCameraMotionReset() {\n    sampleTimestampQueue.clear();\n    frameRotationQueue.reset();\n    resetRotationAtNextFrame.set(true);\n  }\n\n  /**\n   * Sets projection data and stereo mode of the media to be played.\n   *\n   * @param projectionData Contains the projection data to be rendered.\n   * @param stereoMode A {@link C.StereoMode} value.\n   * @param timeNs When then new projection should be used.\n   */\n  private void setProjection(\n      @Nullable byte[] projectionData, @C.StereoMode int stereoMode, long timeNs) {\n    byte[] oldProjectionData = lastProjectionData;\n    int oldStereoMode = lastStereoMode;\n    lastProjectionData = projectionData;\n    lastStereoMode = stereoMode == Format.NO_VALUE ? defaultStereoMode : stereoMode;\n    if (oldStereoMode == lastStereoMode && Arrays.equals(oldProjectionData, lastProjectionData)) {\n      return;\n    }\n\n    Projection projectionFromData = null;\n    if (lastProjectionData != null) {\n      projectionFromData = ProjectionDecoder.decode(lastProjectionData, lastStereoMode);\n    }\n    Projection projection =\n        projectionFromData != null && ProjectionRenderer.isSupported(projectionFromData)\n            ? projectionFromData\n            : Projection.createEquirectangular(lastStereoMode);\n    projectionQueue.add(timeNs, projection);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SingleTapListener.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.ui.spherical;\n\nimport android.view.MotionEvent;\n\n/** Listens tap events on a {@link android.view.View}. */\npublic interface SingleTapListener {\n  /**\n   * Notified when a tap occurs with the up {@link MotionEvent} that triggered it.\n   *\n   * @param e The up motion event that completed the first tap.\n   * @return Whether the event is consumed.\n   */\n  boolean onSingleTapUp(MotionEvent e);\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.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.ui.spherical;\n\nimport android.content.Context;\nimport android.graphics.PointF;\nimport android.graphics.SurfaceTexture;\nimport android.hardware.Sensor;\nimport android.hardware.SensorManager;\nimport android.opengl.GLES20;\nimport android.opengl.GLSurfaceView;\nimport android.opengl.Matrix;\nimport android.os.Handler;\nimport android.os.Looper;\nimport androidx.annotation.AnyThread;\nimport androidx.annotation.BinderThread;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.UiThread;\nimport androidx.annotation.VisibleForTesting;\nimport android.util.AttributeSet;\nimport android.view.Display;\nimport android.view.Surface;\nimport android.view.WindowManager;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport javax.microedition.khronos.egl.EGLConfig;\nimport javax.microedition.khronos.opengles.GL10;\n\n/**\n * Renders a GL scene in a non-VR Activity that is affected by phone orientation and touch input.\n *\n * <p>The two input components are the TYPE_GAME_ROTATION_VECTOR Sensor and a TouchListener. The GL\n * renderer combines these two inputs to render a scene with the appropriate camera orientation.\n *\n * <p>The primary complexity in this class is related to the various rotations. It is important to\n * apply the touch and sensor rotations in the correct order or the user's touch manipulations won't\n * match what they expect.\n */\npublic final class SphericalSurfaceView extends GLSurfaceView {\n\n  // Arbitrary vertical field of view.\n  private static final int FIELD_OF_VIEW_DEGREES = 90;\n  private static final float Z_NEAR = .1f;\n  private static final float Z_FAR = 100;\n\n  // TODO Calculate this depending on surface size and field of view.\n  private static final float PX_PER_DEGREES = 25;\n\n  /* package */ static final float UPRIGHT_ROLL = (float) Math.PI;\n\n  private final SensorManager sensorManager;\n  private final @Nullable Sensor orientationSensor;\n  private final OrientationListener orientationListener;\n  private final Renderer renderer;\n  private final Handler mainHandler;\n  private final TouchTracker touchTracker;\n  private final SceneRenderer scene;\n  private @Nullable SurfaceTexture surfaceTexture;\n  private @Nullable Surface surface;\n  private @Nullable Player.VideoComponent videoComponent;\n\n  public SphericalSurfaceView(Context context) {\n    this(context, null);\n  }\n\n  public SphericalSurfaceView(Context context, @Nullable AttributeSet attributeSet) {\n    super(context, attributeSet);\n    mainHandler = new Handler(Looper.getMainLooper());\n\n    // Configure sensors and touch.\n    sensorManager =\n        (SensorManager) Assertions.checkNotNull(context.getSystemService(Context.SENSOR_SERVICE));\n    Sensor orientationSensor = null;\n    if (Util.SDK_INT >= 18) {\n      // TYPE_GAME_ROTATION_VECTOR is the easiest sensor since it handles all the complex math for\n      // fusion. It's used instead of TYPE_ROTATION_VECTOR since the latter uses the magnetometer on\n      // devices. When used indoors, the magnetometer can take some time to settle depending on the\n      // device and amount of metal in the environment.\n      orientationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR);\n    }\n    if (orientationSensor == null) {\n      orientationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);\n    }\n    this.orientationSensor = orientationSensor;\n\n    scene = new SceneRenderer();\n    renderer = new Renderer(scene);\n\n    touchTracker = new TouchTracker(context, renderer, PX_PER_DEGREES);\n    WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);\n    Display display = Assertions.checkNotNull(windowManager).getDefaultDisplay();\n    orientationListener = new OrientationListener(display, touchTracker, renderer);\n\n    setEGLContextClientVersion(2);\n    setRenderer(renderer);\n    setOnTouchListener(touchTracker);\n  }\n\n  /**\n   * Sets the default stereo mode. If the played video doesn't contain a stereo mode the default one\n   * is used.\n   *\n   * @param stereoMode A {@link C.StereoMode} value.\n   */\n  public void setDefaultStereoMode(@C.StereoMode int stereoMode) {\n    scene.setDefaultStereoMode(stereoMode);\n  }\n\n  /** Sets the {@link Player.VideoComponent} to use. */\n  public void setVideoComponent(@Nullable Player.VideoComponent newVideoComponent) {\n    if (newVideoComponent == videoComponent) {\n      return;\n    }\n    if (videoComponent != null) {\n      if (surface != null) {\n        videoComponent.clearVideoSurface(surface);\n      }\n      videoComponent.clearVideoFrameMetadataListener(scene);\n      videoComponent.clearCameraMotionListener(scene);\n    }\n    videoComponent = newVideoComponent;\n    if (videoComponent != null) {\n      videoComponent.setVideoFrameMetadataListener(scene);\n      videoComponent.setCameraMotionListener(scene);\n      videoComponent.setVideoSurface(surface);\n    }\n  }\n\n  /** Sets the {@link SingleTapListener} used to listen to single tap events on this view. */\n  public void setSingleTapListener(@Nullable SingleTapListener listener) {\n    touchTracker.setSingleTapListener(listener);\n  }\n\n  @Override\n  public void onResume() {\n    super.onResume();\n    if (orientationSensor != null) {\n      sensorManager.registerListener(\n          orientationListener, orientationSensor, SensorManager.SENSOR_DELAY_FASTEST);\n    }\n  }\n\n  @Override\n  public void onPause() {\n    if (orientationSensor != null) {\n      sensorManager.unregisterListener(orientationListener);\n    }\n    super.onPause();\n  }\n\n  @Override\n  protected void onDetachedFromWindow() {\n    // This call stops GL thread.\n    super.onDetachedFromWindow();\n\n    // Post to make sure we occur in order with any onSurfaceTextureAvailable calls.\n    mainHandler.post(\n        () -> {\n          if (surface != null) {\n            if (videoComponent != null) {\n              videoComponent.clearVideoSurface(surface);\n            }\n            releaseSurface(surfaceTexture, surface);\n            surfaceTexture = null;\n            surface = null;\n          }\n        });\n  }\n\n  // Called on GL thread.\n  private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {\n    mainHandler.post(\n        () -> {\n          SurfaceTexture oldSurfaceTexture = this.surfaceTexture;\n          Surface oldSurface = this.surface;\n          this.surfaceTexture = surfaceTexture;\n          this.surface = new Surface(surfaceTexture);\n          if (videoComponent != null) {\n            videoComponent.setVideoSurface(surface);\n          }\n          releaseSurface(oldSurfaceTexture, oldSurface);\n        });\n  }\n\n  private static void releaseSurface(\n      @Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) {\n    if (oldSurfaceTexture != null) {\n      oldSurfaceTexture.release();\n    }\n    if (oldSurface != null) {\n      oldSurface.release();\n    }\n  }\n\n  /**\n   * Standard GL Renderer implementation. The notable code is the matrix multiplication in\n   * onDrawFrame and updatePitchMatrix.\n   */\n  @VisibleForTesting\n  /* package */ class Renderer\n      implements GLSurfaceView.Renderer, TouchTracker.Listener, OrientationListener.Listener {\n    private final SceneRenderer scene;\n    private final float[] projectionMatrix = new float[16];\n\n    // There is no model matrix for this scene so viewProjectionMatrix is used for the mvpMatrix.\n    private final float[] viewProjectionMatrix = new float[16];\n\n    // Device orientation is derived from sensor data. This is accessed in the sensor's thread and\n    // the GL thread.\n    private final float[] deviceOrientationMatrix = new float[16];\n\n    // Optional pitch and yaw rotations are applied to the sensor orientation. These are accessed on\n    // the UI, sensor and GL Threads.\n    private final float[] touchPitchMatrix = new float[16];\n    private final float[] touchYawMatrix = new float[16];\n    private float touchPitch;\n    private float deviceRoll;\n\n    // viewMatrix = touchPitch * deviceOrientation * touchYaw.\n    private final float[] viewMatrix = new float[16];\n    private final float[] tempMatrix = new float[16];\n\n    public Renderer(SceneRenderer scene) {\n      this.scene = scene;\n      Matrix.setIdentityM(deviceOrientationMatrix, 0);\n      Matrix.setIdentityM(touchPitchMatrix, 0);\n      Matrix.setIdentityM(touchYawMatrix, 0);\n      deviceRoll = UPRIGHT_ROLL;\n    }\n\n    @Override\n    public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) {\n      onSurfaceTextureAvailable(scene.init());\n    }\n\n    @Override\n    public void onSurfaceChanged(GL10 gl, int width, int height) {\n      GLES20.glViewport(0, 0, width, height);\n      float aspect = (float) width / height;\n      float fovY = calculateFieldOfViewInYDirection(aspect);\n      Matrix.perspectiveM(projectionMatrix, 0, fovY, aspect, Z_NEAR, Z_FAR);\n    }\n\n    @Override\n    public void onDrawFrame(GL10 gl) {\n      // Combine touch & sensor data.\n      // Orientation = pitch * sensor * yaw since that is closest to what most users expect the\n      // behavior to be.\n      synchronized (this) {\n        Matrix.multiplyMM(tempMatrix, 0, deviceOrientationMatrix, 0, touchYawMatrix, 0);\n        Matrix.multiplyMM(viewMatrix, 0, touchPitchMatrix, 0, tempMatrix, 0);\n      }\n\n      Matrix.multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0, viewMatrix, 0);\n      scene.drawFrame(viewProjectionMatrix, /* rightEye= */ false);\n    }\n\n    /** Adjusts the GL camera's rotation based on device rotation. Runs on the sensor thread. */\n    @Override\n    @BinderThread\n    public synchronized void onOrientationChange(float[] matrix, float deviceRoll) {\n      System.arraycopy(matrix, 0, deviceOrientationMatrix, 0, deviceOrientationMatrix.length);\n      this.deviceRoll = -deviceRoll;\n      updatePitchMatrix();\n    }\n\n    /**\n     * Updates the pitch matrix after a physical rotation or touch input. The pitch matrix rotation\n     * is applied on an axis that is dependent on device rotation so this must be called after\n     * either touch or sensor update.\n     */\n    @AnyThread\n    private void updatePitchMatrix() {\n      // The camera's pitch needs to be rotated along an axis that is parallel to the real world's\n      // horizon. This is the <1, 0, 0> axis after compensating for the device's roll.\n      Matrix.setRotateM(\n          touchPitchMatrix,\n          0,\n          -touchPitch,\n          (float) Math.cos(deviceRoll),\n          (float) Math.sin(deviceRoll),\n          0);\n    }\n\n    @Override\n    @UiThread\n    public synchronized void onScrollChange(PointF scrollOffsetDegrees) {\n      touchPitch = scrollOffsetDegrees.y;\n      updatePitchMatrix();\n      Matrix.setRotateM(touchYawMatrix, 0, -scrollOffsetDegrees.x, 0, 1, 0);\n    }\n\n    private float calculateFieldOfViewInYDirection(float aspect) {\n      boolean landscapeMode = aspect > 1;\n      if (landscapeMode) {\n        double halfFovX = FIELD_OF_VIEW_DEGREES / 2;\n        double tanY = Math.tan(Math.toRadians(halfFovX)) / aspect;\n        double halfFovY = Math.toDegrees(Math.atan(tanY));\n        return (float) (halfFovY * 2);\n      } else {\n        return FIELD_OF_VIEW_DEGREES;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.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.ui.spherical;\n\nimport android.content.Context;\nimport android.graphics.PointF;\nimport androidx.annotation.BinderThread;\nimport androidx.annotation.Nullable;\nimport android.view.GestureDetector;\nimport android.view.MotionEvent;\nimport android.view.View;\n\n/**\n * Basic touch input system.\n *\n * <p>Mixing touch input and gyro input results in a complicated UI so this should be used\n * carefully. This touch system implements a basic (X, Y) -> (yaw, pitch) transform. This works for\n * basic UI but fails in edge cases where the user tries to drag scene up or down. There is no good\n * UX solution for this. The least bad solution is to disable pitch manipulation and only let the\n * user adjust yaw. This example tries to limit the awkwardness by restricting pitch manipulation to\n * +/- 45 degrees.\n *\n * <p>It is also important to get the order of operations correct. To match what users expect, touch\n * interaction manipulates the scene by rotating the world by the yaw offset and tilting the camera\n * by the pitch offset. If the order of operations is incorrect, the sensors & touch rotations will\n * have strange interactions. The roll of the phone is also tracked so that the x & y are correctly\n * mapped to yaw & pitch no matter how the user holds their phone.\n *\n * <p>This class doesn't handle any scrolling inertia but Android's\n * com.google.vr.sdk.widgets.common.TouchTracker.FlingGestureListener can be used with this code for\n * a nicer UI. An even more advanced UI would reproject the user's touch point into 3D and drag the\n * Mesh as the user moves their finger. However, that requires quaternion interpolation.\n */\n/* package */ class TouchTracker extends GestureDetector.SimpleOnGestureListener\n    implements View.OnTouchListener, OrientationListener.Listener {\n\n  /* package */ interface Listener {\n    void onScrollChange(PointF scrollOffsetDegrees);\n  }\n\n  // Touch input won't change the pitch beyond +/- 45 degrees. This reduces awkward situations\n  // where the touch-based pitch and gyro-based pitch interact badly near the poles.\n  /* package */ static final float MAX_PITCH_DEGREES = 45;\n\n  // With every touch event, update the accumulated degrees offset by the new pixel amount.\n  private final PointF previousTouchPointPx = new PointF();\n  private final PointF accumulatedTouchOffsetDegrees = new PointF();\n\n  private final Listener listener;\n  private final float pxPerDegrees;\n  private final GestureDetector gestureDetector;\n  // The conversion from touch to yaw & pitch requires compensating for device roll. This is set\n  // on the sensor thread and read on the UI thread.\n  private volatile float roll;\n  private @Nullable SingleTapListener singleTapListener;\n\n  @SuppressWarnings({\n    \"nullness:assignment.type.incompatible\",\n    \"nullness:argument.type.incompatible\"\n  })\n  public TouchTracker(Context context, Listener listener, float pxPerDegrees) {\n    this.listener = listener;\n    this.pxPerDegrees = pxPerDegrees;\n    gestureDetector = new GestureDetector(context, this);\n    roll = SphericalSurfaceView.UPRIGHT_ROLL;\n  }\n\n  public void setSingleTapListener(@Nullable SingleTapListener listener) {\n    singleTapListener = listener;\n  }\n\n  /**\n   * Converts ACTION_MOVE events to pitch & yaw events while compensating for device roll.\n   *\n   * @return true if we handled the event\n   */\n  @Override\n  public boolean onTouch(View v, MotionEvent event) {\n    return gestureDetector.onTouchEvent(event);\n  }\n\n  @Override\n  public boolean onDown(MotionEvent e) {\n    // Initialize drag gesture.\n    previousTouchPointPx.set(e.getX(), e.getY());\n    return true;\n  }\n\n  @Override\n  public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {\n    // Calculate the touch delta in screen space.\n    float touchX = (e2.getX() - previousTouchPointPx.x) / pxPerDegrees;\n    float touchY = (e2.getY() - previousTouchPointPx.y) / pxPerDegrees;\n    previousTouchPointPx.set(e2.getX(), e2.getY());\n\n    float r = roll; // Copy volatile state.\n    float cr = (float) Math.cos(r);\n    float sr = (float) Math.sin(r);\n    // To convert from screen space to the 3D space, we need to adjust the drag vector based\n    // on the roll of the phone. This is standard rotationMatrix(roll) * vector math but has\n    // an inverted y-axis due to the screen-space coordinates vs GL coordinates.\n    // Handle yaw.\n    accumulatedTouchOffsetDegrees.x -= cr * touchX - sr * touchY;\n    // Handle pitch and limit it to 45 degrees.\n    accumulatedTouchOffsetDegrees.y += sr * touchX + cr * touchY;\n    accumulatedTouchOffsetDegrees.y =\n        Math.max(-MAX_PITCH_DEGREES, Math.min(MAX_PITCH_DEGREES, accumulatedTouchOffsetDegrees.y));\n\n    listener.onScrollChange(accumulatedTouchOffsetDegrees);\n    return true;\n  }\n\n  @Override\n  public boolean onSingleTapUp(MotionEvent e) {\n    if (singleTapListener != null) {\n      return singleTapListener.onSingleTapUp(e);\n    }\n    return false;\n  }\n\n  @Override\n  @BinderThread\n  public void onOrientationChange(float[] deviceOrientationMatrix, float roll) {\n    // We compensate for roll by rotating in the opposite direction.\n    this.roll = -roll;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable/exo_edit_mode_logo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright (C) 2017 The Android Open Source Project\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n          http://www.apache.org/licenses/LICENSE-2.0\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<vector android:height=\"24dp\" android:viewportHeight=\"312.5\"\n    android:viewportWidth=\"312.5\" android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#00000000\"\n        android:pathData=\"m171.46,133.99c0.02,-0.19 0.07,-0.37 0.07,-0.57l0,-29.16C171.5,104.3 171.48,104.34 171.46,104.38L171.46,133.99z\"\n        android:strokeAlpha=\"1\" android:strokeColor=\"#bcbbb9\"\n        android:strokeLineCap=\"round\" android:strokeLineJoin=\"round\" android:strokeWidth=\"3\"/>\n    <path android:fillColor=\"#00000000\"\n        android:pathData=\"m108.76,134.61c-2.16,-1.92 -3.95,-4.37 -5.17,-7.32 -3.49,-8.42 -0.79,-18.44 6.42,-23.81 9.5,-7.08 22.59,-3.99 28.26,6.19 1.12,2.01 1.86,4.14 2.24,6.29 0.47,2.65 3.21,4.05 5.57,3.15L171.46,104.06 103.58,63.79c-8.98,-5.33 -20.21,1.33 -20.21,11.99l0,80.31 24.64,-14.49C110.5,140.14 110.94,136.54 108.76,134.61z\"\n        android:strokeAlpha=\"1\" android:strokeColor=\"#bcbbb9\"\n        android:strokeLineCap=\"round\" android:strokeLineJoin=\"round\" android:strokeWidth=\"3\"/>\n    <path android:fillColor=\"#00000000\"\n        android:pathData=\"m83.65,156.16c-0.1,0 -0.19,0 -0.29,0.01l0,0L83.5,156.25 83.65,156.16z\"\n        android:strokeAlpha=\"1\" android:strokeColor=\"#bcbbb9\"\n        android:strokeLineCap=\"round\" android:strokeLineJoin=\"round\" android:strokeWidth=\"3\"/>\n    <path android:fillColor=\"#00000000\"\n        android:pathData=\"m140.48,196.78c-0.52,2.88 -1.68,5.71 -3.55,8.28 -5.33,7.34 -15.11,9.98 -23.25,6.29 -10.74,-4.87 -14.73,-18.05 -9.02,-28.21 1.13,-2.01 2.55,-3.73 4.17,-5.16 2.18,-1.91 1.76,-5.5 -0.72,-6.97l-24.74,-14.68 0,80.39c0,10.66 11.23,17.32 20.21,11.99L171.32,208.52 146.74,193.93C144.25,192.45 141.01,193.88 140.48,196.78z\"\n        android:strokeAlpha=\"1\" android:strokeColor=\"#bcbbb9\"\n        android:strokeLineCap=\"round\" android:strokeLineJoin=\"round\" android:strokeWidth=\"3\"/>\n    <path android:fillColor=\"#00000000\"\n        android:pathData=\"M83.37,156.17 L83.37,156.33 83.5,156.25 83.37,156.17z\"\n        android:strokeAlpha=\"1\" android:strokeColor=\"#bcbbb9\"\n        android:strokeLineCap=\"round\" android:strokeLineJoin=\"round\" android:strokeWidth=\"3\"/>\n    <path android:fillColor=\"#00000000\"\n        android:pathData=\"m165.98,175.13c-2,0.72 -4.15,1.12 -6.4,1.12 -11.42,0 -20.52,-10.14 -19.26,-22.14 0.98,-9.33 8.36,-16.75 17.45,-17.61 2.9,-0.27 5.68,0.12 8.22,1.04 2.52,0.92 5.12,-0.9 5.46,-3.54l0,-29.62c0.02,-0.04 0.05,-0.08 0.07,-0.11l0,-0 -0.07,0.04 0,-0.24 -25.37,15.05c-2.37,0.9 -5.11,-0.5 -5.57,-3.15 -0.38,-2.16 -1.12,-4.28 -2.24,-6.29 -5.67,-10.18 -18.76,-13.27 -28.26,-6.19 -7.21,5.38 -9.91,15.39 -6.42,23.81 1.22,2.95 3.01,5.4 5.17,7.32 2.17,1.93 1.74,5.53 -0.75,6.99l-24.64,14.49 -0.13,0.08c0.04,0 0.09,-0 0.13,-0 0.1,-0 0.19,-0 0.29,-0l-0.15,0.09 -0.14,0.08 0,0 0,0 24.74,14.68c2.48,1.47 2.89,5.06 0.72,6.97 -1.62,1.42 -3.04,3.15 -4.16,5.15 -5.71,10.16 -1.72,23.34 9.02,28.21 8.15,3.7 17.93,1.05 23.25,-6.28 1.86,-2.57 3.02,-5.39 3.55,-8.28 0.53,-2.9 3.77,-4.33 6.26,-2.85l24.58,14.59 0.13,0.08 0,-29.92C171.12,176.03 168.51,174.21 165.98,175.13z\"\n        android:strokeAlpha=\"1\" android:strokeColor=\"#bcbbb9\"\n        android:strokeLineCap=\"round\" android:strokeLineJoin=\"round\" android:strokeWidth=\"3\"/>\n    <path android:fillColor=\"#00000000\"\n        android:pathData=\"m239.41,144.34 l-67.88,-40.28 0,0c0,0 -0,-0 -0,-0l0,0.2 0,0 0,29.16c0,0.2 -0.04,0.38 -0.07,0.57 -0.34,2.64 -2.94,4.46 -5.46,3.54 -2.54,-0.92 -5.32,-1.31 -8.22,-1.04 -9.09,0.86 -16.47,8.28 -17.45,17.61 -1.26,11.99 7.85,22.14 19.26,22.14 2.24,0 4.4,-0.39 6.4,-1.12 2.53,-0.92 5.13,0.9 5.47,3.55 0.02,0.19 0.07,0.37 0.07,0.57l0,29.35c0,-0 0,-0 0,-0l0,0L239.41,168.32C248.39,162.99 248.39,149.67 239.41,144.34z\"\n        android:strokeAlpha=\"1\" android:strokeColor=\"#bcbbb9\"\n        android:strokeLineCap=\"round\" android:strokeLineJoin=\"round\" android:strokeWidth=\"3\"/>\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_fullscreen_enter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:width=\"48dp\"\n    android:height=\"48dp\">\n  <path\n      android:pathData=\"M7 14l-2 0 0 5 5 0 0 -2 -3 0 0 -3zm-2 -4l2 0 0 -3 3 0 0 -2 -5 0 0 5zm12 7l-3 0 0 2 5 0 0 -5 -2 0 0 3zm-3 -12l0 2 3 0 0 3 2 0 0 -5 -5 0z\"\n      android:fillColor=\"#FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_fullscreen_exit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:width=\"48dp\"\n    android:height=\"48dp\">\n  <path\n      android:pathData=\"M5 16l3 0 0 3 2 0 0 -5 -5 0 0 2zm3 -8l-3 0 0 2 5 0 0 -5 -2 0 0 3zm6 11l2 0 0 -3 3 0 0 -2 -5 0 0 5zm2 -11l0 -3 -2 0 0 5 5 0 0 -2 -3 0z\"\n      android:fillColor=\"#FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_all.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"32dp\"\n        android:height=\"32dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z\"/>\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_off.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"32dp\"\n        android:height=\"32dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#4EFFFFFF\"\n        android:pathData=\"M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z\"/>\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_one.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:height=\"32dp\"\n        android:width=\"32dp\"\n        android:viewportHeight=\"24.0\"\n        android:viewportWidth=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4zM13,15L13,9h-1l-2,1v1h1.5v4L13,15z\"/>\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_off.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"32dp\"\n    android:height=\"32dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n\n    <path\n        android:fillColor=\"#4EFFFFFF\"\n        android:pathData=\"M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20\n17.96 7.46 20 9.5V4h-5.5zm0.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04\n2.04-3.13-3.13z\" />\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle_on.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"32dp\"\n    android:height=\"32dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20\n17.96 7.46 20 9.5V4h-5.5zm0.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04\n2.04-3.13-3.13z\" />\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_fastforward.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"32dp\"\n    android:height=\"32dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\">\n\n  <path\n      android:fillColor=\"#FFFFFFFF\"\n      android:pathData=\"M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z\"/>\n\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_next.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"32dp\"\n    android:height=\"32dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\">\n\n  <path\n      android:fillColor=\"#FFFFFFFF\"\n      android:pathData=\"M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z\"/>\n\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_pause.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"32dp\"\n    android:height=\"32dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\">\n\n  <path\n      android:fillColor=\"#FFFFFFFF\"\n      android:pathData=\"M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z\"/>\n\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_play.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"32dp\"\n    android:height=\"32dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\">\n\n  <path\n      android:fillColor=\"#FFFFFFFF\"\n      android:pathData=\"M8,5v14l11,-7z\"/>\n\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_previous.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"32dp\"\n    android:height=\"32dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\">\n\n  <path\n      android:fillColor=\"#FFFFFFFF\"\n      android:pathData=\"M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z\"/>\n\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_rewind.xml",
    "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.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"32dp\"\n    android:height=\"32dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\">\n\n  <path\n      android:fillColor=\"#FFFFFFFF\"\n      android:pathData=\"M11,18L11,6l-8.5,6 8.5,6zM11.5,12l8.5,6L20,6l-8.5,6z\"/>\n\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/drawable-anydpi-v21/exo_icon_stop.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24\"\n        android:viewportHeight=\"24\">\n\n    <path\n        android:pathData=\"M0 0h24v24H0z\" />\n    <path\n        android:fillColor=\"#000000\"\n        android:pathData=\"M6 6h12v12H6z\" />\n</vector>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/layout/exo_list_divider.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<View xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"1px\"\n    android:background=\"?android:attr/listDivider\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/layout/exo_playback_control_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"wrap_content\"\n  android:layout_gravity=\"bottom\"\n  android:layoutDirection=\"ltr\"\n  android:background=\"#CC000000\"\n  android:orientation=\"vertical\"\n  tools:targetApi=\"28\">\n\n  <LinearLayout\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center\"\n    android:paddingTop=\"4dp\"\n    android:orientation=\"horizontal\">\n\n    <ImageButton android:id=\"@id/exo_prev\"\n      style=\"@style/ExoMediaButton.Previous\"/>\n\n    <ImageButton android:id=\"@id/exo_rew\"\n      style=\"@style/ExoMediaButton.Rewind\"/>\n\n    <ImageButton android:id=\"@id/exo_shuffle\"\n      style=\"@style/ExoMediaButton\"/>\n\n    <ImageButton android:id=\"@id/exo_repeat_toggle\"\n      style=\"@style/ExoMediaButton\"/>\n\n    <ImageButton android:id=\"@id/exo_play\"\n      style=\"@style/ExoMediaButton.Play\"/>\n\n    <ImageButton android:id=\"@id/exo_pause\"\n      style=\"@style/ExoMediaButton.Pause\"/>\n\n    <ImageButton android:id=\"@id/exo_ffwd\"\n      style=\"@style/ExoMediaButton.FastForward\"/>\n\n    <ImageButton android:id=\"@id/exo_next\"\n      style=\"@style/ExoMediaButton.Next\"/>\n\n    <ImageButton android:id=\"@id/exo_vr\"\n      style=\"@style/ExoMediaButton.VR\"/>\n\n  </LinearLayout>\n\n  <LinearLayout\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_marginTop=\"4dp\"\n    android:gravity=\"center_vertical\"\n    android:orientation=\"horizontal\">\n\n    <TextView android:id=\"@id/exo_position\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:textSize=\"14sp\"\n      android:textStyle=\"bold\"\n      android:paddingLeft=\"4dp\"\n      android:paddingRight=\"4dp\"\n      android:includeFontPadding=\"false\"\n      android:textColor=\"#FFBEBEBE\"/>\n\n    <View android:id=\"@id/exo_progress_placeholder\"\n      android:layout_width=\"0dp\"\n      android:layout_weight=\"1\"\n      android:layout_height=\"26dp\"/>\n\n    <TextView android:id=\"@id/exo_duration\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:textSize=\"14sp\"\n      android:textStyle=\"bold\"\n      android:paddingLeft=\"4dp\"\n      android:paddingRight=\"4dp\"\n      android:includeFontPadding=\"false\"\n      android:textColor=\"#FFBEBEBE\"/>\n\n  </LinearLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/layout/exo_player_control_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<merge>\n  <include layout=\"@layout/exo_playback_control_view\"/>\n</merge>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/layout/exo_player_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<merge>\n  <include layout=\"@layout/exo_simple_player_view\"/>\n</merge>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/layout/exo_simple_player_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n  <com.google.android.exoplayer2.ui.AspectRatioFrameLayout android:id=\"@id/exo_content_frame\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"\n      android:layout_gravity=\"center\">\n\n    <!-- Video surface will be inserted as the first child of the content frame. -->\n\n    <View android:id=\"@id/exo_shutter\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@android:color/black\"/>\n\n    <ImageView android:id=\"@id/exo_artwork\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:scaleType=\"fitXY\"/>\n\n    <com.google.android.exoplayer2.ui.SubtitleView android:id=\"@id/exo_subtitles\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"/>\n\n    <ProgressBar android:id=\"@id/exo_buffering\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:indeterminate=\"true\"\n        android:layout_gravity=\"center\"/>\n\n    <TextView android:id=\"@id/exo_error_message\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_gravity=\"center\"\n        android:gravity=\"center\"\n        android:background=\"@color/exo_error_message_background_color\"\n        android:padding=\"16dp\"/>\n\n  </com.google.android.exoplayer2.ui.AspectRatioFrameLayout>\n\n  <FrameLayout android:id=\"@id/exo_ad_overlay\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"/>\n\n  <FrameLayout android:id=\"@id/exo_overlay\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"/>\n\n  <View android:id=\"@id/exo_controller_placeholder\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"/>\n\n</merge>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/layout/exo_track_selection_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"match_parent\">\n\n  <com.google.android.exoplayer2.ui.TrackSelectionView android:id=\"@+id/exo_track_selection_view\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"/>\n\n</ScrollView>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<resources>\n\n  <!-- Must be kept in sync with AspectRatioFrameLayout -->\n  <attr name=\"resize_mode\" format=\"enum\">\n    <enum name=\"fit\" value=\"0\"/>\n    <enum name=\"fixed_width\" value=\"1\"/>\n    <enum name=\"fixed_height\" value=\"2\"/>\n    <enum name=\"fill\" value=\"3\"/>\n    <enum name=\"zoom\" value=\"4\"/>\n  </attr>\n\n  <!-- Must be kept in sync with PlayerView -->\n  <attr name=\"surface_type\" format=\"enum\">\n    <enum name=\"none\" value=\"0\"/>\n    <enum name=\"surface_view\" value=\"1\"/>\n    <enum name=\"texture_view\" value=\"2\"/>\n    <enum name=\"spherical_view\" value=\"3\"/>\n  </attr>\n\n  <!-- Must be kept in sync with RepeatModeUtil -->\n  <attr name=\"repeat_toggle_modes\">\n    <flag name=\"none\" value=\"0\"/>\n    <flag name=\"one\" value=\"1\"/>\n    <flag name=\"all\" value=\"2\"/>\n  </attr>\n\n  <!-- PlayerControlView attributes -->\n  <attr name=\"show_timeout\" format=\"integer\"/>\n  <attr name=\"rewind_increment\" format=\"integer\"/>\n  <attr name=\"fastforward_increment\" format=\"integer\"/>\n  <attr name=\"show_shuffle_button\" format=\"boolean\"/>\n  <attr name=\"time_bar_min_update_interval\" format=\"integer\"/>\n  <attr name=\"controller_layout_id\" format=\"reference\"/>\n\n  <!-- DefaultTimeBar attributes -->\n  <attr name=\"bar_height\" format=\"dimension\"/>\n  <attr name=\"touch_target_height\" format=\"dimension\"/>\n  <attr name=\"ad_marker_width\" format=\"dimension\"/>\n  <attr name=\"scrubber_enabled_size\" format=\"dimension\"/>\n  <attr name=\"scrubber_disabled_size\" format=\"dimension\"/>\n  <attr name=\"scrubber_dragged_size\" format=\"dimension\"/>\n  <attr name=\"scrubber_drawable\" format=\"reference\"/>\n  <attr name=\"played_color\" format=\"color\"/>\n  <attr name=\"scrubber_color\" format=\"color\"/>\n  <attr name=\"buffered_color\" format=\"color\"/>\n  <attr name=\"unplayed_color\" format=\"color\"/>\n  <attr name=\"ad_marker_color\" format=\"color\"/>\n  <attr name=\"played_ad_marker_color\" format=\"color\"/>\n\n  <declare-styleable name=\"PlayerView\">\n    <attr name=\"use_artwork\" format=\"boolean\"/>\n    <attr name=\"shutter_background_color\" format=\"color\"/>\n    <attr name=\"default_artwork\" format=\"reference\"/>\n    <attr name=\"use_controller\" format=\"boolean\"/>\n    <attr name=\"hide_on_touch\" format=\"boolean\"/>\n    <attr name=\"hide_during_ads\" format=\"boolean\"/>\n    <attr name=\"auto_show\" format=\"boolean\"/>\n    <attr name=\"show_buffering\" format=\"enum\">\n      <enum name=\"never\" value=\"0\"/>\n      <enum name=\"when_playing\" value=\"1\"/>\n      <enum name=\"always\" value=\"2\"/>\n    </attr>\n    <attr name=\"keep_content_on_player_reset\" format=\"boolean\"/>\n    <attr name=\"player_layout_id\" format=\"reference\"/>\n\n    <attr name=\"surface_type\"/>\n    <!-- AspectRatioFrameLayout attributes -->\n    <attr name=\"resize_mode\"/>\n    <!-- PlayerControlView attributes -->\n    <attr name=\"show_timeout\"/>\n    <attr name=\"rewind_increment\"/>\n    <attr name=\"fastforward_increment\"/>\n    <attr name=\"repeat_toggle_modes\"/>\n    <attr name=\"show_shuffle_button\"/>\n    <attr name=\"time_bar_min_update_interval\"/>\n    <attr name=\"controller_layout_id\"/>\n    <!-- DefaultTimeBar attributes -->\n    <attr name=\"bar_height\"/>\n    <attr name=\"touch_target_height\"/>\n    <attr name=\"ad_marker_width\"/>\n    <attr name=\"scrubber_enabled_size\"/>\n    <attr name=\"scrubber_disabled_size\"/>\n    <attr name=\"scrubber_dragged_size\"/>\n    <attr name=\"scrubber_drawable\"/>\n    <attr name=\"played_color\"/>\n    <attr name=\"scrubber_color\"/>\n    <attr name=\"buffered_color\" />\n    <attr name=\"unplayed_color\"/>\n    <attr name=\"ad_marker_color\"/>\n    <attr name=\"played_ad_marker_color\"/>\n  </declare-styleable>\n\n  <declare-styleable name=\"AspectRatioFrameLayout\">\n    <attr name=\"resize_mode\"/>\n  </declare-styleable>\n\n  <declare-styleable name=\"PlayerControlView\">\n    <attr name=\"show_timeout\"/>\n    <attr name=\"rewind_increment\"/>\n    <attr name=\"fastforward_increment\"/>\n    <attr name=\"repeat_toggle_modes\"/>\n    <attr name=\"show_shuffle_button\"/>\n    <attr name=\"time_bar_min_update_interval\"/>\n    <attr name=\"controller_layout_id\"/>\n    <!-- DefaultTimeBar attributes -->\n    <attr name=\"bar_height\"/>\n    <attr name=\"touch_target_height\"/>\n    <attr name=\"ad_marker_width\"/>\n    <attr name=\"scrubber_enabled_size\"/>\n    <attr name=\"scrubber_disabled_size\"/>\n    <attr name=\"scrubber_dragged_size\"/>\n    <attr name=\"scrubber_drawable\"/>\n    <attr name=\"played_color\"/>\n    <attr name=\"scrubber_color\"/>\n    <attr name=\"buffered_color\" />\n    <attr name=\"unplayed_color\"/>\n    <attr name=\"ad_marker_color\"/>\n    <attr name=\"played_ad_marker_color\"/>\n  </declare-styleable>\n\n  <declare-styleable name=\"DefaultTimeBar\">\n    <attr name=\"bar_height\"/>\n    <attr name=\"touch_target_height\"/>\n    <attr name=\"ad_marker_width\"/>\n    <attr name=\"scrubber_enabled_size\"/>\n    <attr name=\"scrubber_disabled_size\"/>\n    <attr name=\"scrubber_dragged_size\"/>\n    <attr name=\"scrubber_drawable\"/>\n    <attr name=\"played_color\"/>\n    <attr name=\"scrubber_color\"/>\n    <attr name=\"buffered_color\" />\n    <attr name=\"unplayed_color\"/>\n    <attr name=\"ad_marker_color\"/>\n    <attr name=\"played_ad_marker_color\"/>\n  </declare-styleable>\n\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values/constants.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<resources>\n\n  <dimen name=\"exo_media_button_width\">71dp</dimen>\n  <dimen name=\"exo_media_button_height\">52dp</dimen>\n\n  <integer name=\"exo_media_button_opacity_percentage_enabled\">100</integer>\n  <integer name=\"exo_media_button_opacity_percentage_disabled\">33</integer>\n\n  <color name=\"exo_error_message_background_color\">#AA000000</color>\n  <color name=\"exo_edit_mode_background_color\">#FFF4F3F0</color>\n\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values/drawables.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources>\n  <drawable name=\"exo_controls_play\">@drawable/exo_icon_play</drawable>\n  <drawable name=\"exo_controls_pause\">@drawable/exo_icon_pause</drawable>\n  <drawable name=\"exo_controls_next\">@drawable/exo_icon_next</drawable>\n  <drawable name=\"exo_controls_previous\">@drawable/exo_icon_previous</drawable>\n  <drawable name=\"exo_controls_fastforward\">@drawable/exo_icon_fastforward</drawable>\n  <drawable name=\"exo_controls_rewind\">@drawable/exo_icon_rewind</drawable>\n  <drawable name=\"exo_notification_play\">@drawable/exo_icon_play</drawable>\n  <drawable name=\"exo_notification_pause\">@drawable/exo_icon_pause</drawable>\n  <drawable name=\"exo_notification_next\">@drawable/exo_icon_next</drawable>\n  <drawable name=\"exo_notification_previous\">@drawable/exo_icon_previous</drawable>\n  <drawable name=\"exo_notification_fastforward\">@drawable/exo_icon_fastforward</drawable>\n  <drawable name=\"exo_notification_rewind\">@drawable/exo_icon_rewind</drawable>\n  <drawable name=\"exo_notification_stop\">@drawable/exo_icon_stop</drawable>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values/ids.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<resources>\n\n  <item name=\"exo_content_frame\" type=\"id\"/>\n  <item name=\"exo_shutter\" type=\"id\"/>\n  <item name=\"exo_subtitles\" type=\"id\"/>\n  <item name=\"exo_artwork\" type=\"id\"/>\n  <item name=\"exo_controller_placeholder\" type=\"id\"/>\n  <item name=\"exo_controller\" type=\"id\"/>\n  <item name=\"exo_ad_overlay\" type=\"id\"/>\n  <item name=\"exo_overlay\" type=\"id\"/>\n  <item name=\"exo_play\" type=\"id\"/>\n  <item name=\"exo_pause\" type=\"id\"/>\n  <item name=\"exo_rew\" type=\"id\"/>\n  <item name=\"exo_ffwd\" type=\"id\"/>\n  <item name=\"exo_prev\" type=\"id\"/>\n  <item name=\"exo_next\" type=\"id\"/>\n  <item name=\"exo_shuffle\" type=\"id\"/>\n  <item name=\"exo_repeat_toggle\" type=\"id\"/>\n  <item name=\"exo_duration\" type=\"id\"/>\n  <item name=\"exo_position\" type=\"id\"/>\n  <item name=\"exo_progress_placeholder\" type=\"id\"/>\n  <item name=\"exo_progress\" type=\"id\"/>\n  <item name=\"exo_buffering\" type=\"id\"/>\n  <item name=\"exo_error_message\" type=\"id\"/>\n  <item name=\"exo_vr\" type=\"id\"/>\n\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <!-- Description for a media control button that causes the previous track to be played. [CHAR LIMIT=30] -->\n  <string name=\"exo_controls_previous_description\">Previous track</string>\n  <!-- Description for a media control button that causes the next track to be played. [CHAR LIMIT=30] -->\n  <string name=\"exo_controls_next_description\">Next track</string>\n  <!-- Description for a media control button that causes playback to be paused. [CHAR LIMIT=30] -->\n  <string name=\"exo_controls_pause_description\">Pause</string>\n  <!-- Description for a media control button that causes playback to be started. [CHAR LIMIT=30] -->\n  <string name=\"exo_controls_play_description\">Play</string>\n  <!-- Description for a media control button that causes playback to be stopped. [CHAR LIMIT=30] -->\n  <string name=\"exo_controls_stop_description\">Stop</string>\n  <!-- Description for a media control button that causes playback to be rewound. [CHAR LIMIT=30] -->\n  <string name=\"exo_controls_rewind_description\">Rewind</string>\n  <!-- Description for a media control button that causes playback to be fast-forwarded. [CHAR LIMIT=30] -->\n  <string name=\"exo_controls_fastforward_description\">Fast forward</string>\n  <!-- Description for a button that controls the repeat mode of a media playback. In this mode media is not repeated. [CHAR LIMIT=30] -->\n  <string name=\"exo_controls_repeat_off_description\">Repeat none</string>\n  <!-- Description for a button that controls the repeat mode of a media playback. In this mode the current piece of media is repeated. [CHAR LIMIT=30] -->\n  <string name=\"exo_controls_repeat_one_description\">Repeat one</string>\n  <!-- Description for a button that controls the repeat mode of a media playback. In this mode the entire playlist is repeated. [CHAR LIMIT=30] -->\n  <string name=\"exo_controls_repeat_all_description\">Repeat all</string>\n  <!-- Description for a button that controls the shuffle mode of media playback. In this mode shuffle is on. [CHAR LIMIT=40] -->\n  <string name=\"exo_controls_shuffle_on_description\">Shuffle on</string>\n  <!-- Description for a button that controls the shuffle mode of media playback. In this mode shuffle is off. [CHAR LIMIT=40] -->\n  <string name=\"exo_controls_shuffle_off_description\">Shuffle off</string>\n  <!-- Description for a media control button that toggles whether a video playback is fullscreen. [CHAR LIMIT=30] -->\n  <string name=\"exo_controls_fullscreen_description\">Fullscreen mode</string>\n  <!-- Description for a media control button that toggles whether a video playback is in VR mode. [CHAR LIMIT=30] -->\n  <string name=\"exo_controls_vr_description\">VR mode</string>\n  <!-- Description for a button that downloads a piece of media content onto the device. [CHAR LIMIT=20] -->\n  <string name=\"exo_download_description\">Download</string>\n  <!-- Default name for a notification channel corresponding to media downloads. [CHAR LIMIT=40] -->\n  <string name=\"exo_download_notification_channel_name\">Downloads</string>\n  <!-- Shown in a notification or UI component to indicate a download is currently downloading. [CHAR LIMIT=40] -->\n  <string name=\"exo_download_downloading\">Downloading</string>\n  <!-- Shown in a notification or UI component to indicate a download has finished downloading. [CHAR LIMIT=40] -->\n  <string name=\"exo_download_completed\">Download completed</string>\n  <!-- Shown in a notification or UI component to indicate a download has failed. [CHAR LIMIT=40] -->\n  <string name=\"exo_download_failed\">Download failed</string>\n  <!-- Shown in a notification or UI component to indicate downloads are being removed from the device. [CHAR LIMIT=40] -->\n  <string name=\"exo_download_removing\">Removing downloads</string>\n  <!-- The title of a track selection view for video tracks. [CHAR LIMIT=20] -->\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <!-- The title of a track selection view for audio tracks. [CHAR LIMIT=20] -->\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <!-- The title of a track selection view for text (i.e. subtitle or caption) tracks. [CHAR LIMIT=20] -->\n  <string name=\"exo_track_selection_title_text\">Text</string>\n  <!-- An option in a track selection view (e.g. a view that allows the user to choose between multiple media tracks) to indicate that no track should be selected. Examples: When playing a video, if this option is selected for audio, playback will proceed without sound. If this option is selected for video, playback will proceed without visual output. [CHAR LIMIT=20] -->\n  <string name=\"exo_track_selection_none\">None</string>\n  <!-- An option in a track selection view (e.g. a view that allows the user to choose between multiple media tracks) to indicate that the automatic or default track should be selected. Examples: If there are multiple audio tracks in different languages, the automatic selection would normally be the track in the user's native language. If there are multiple video tracks at different qualities, the automatic selection would normally be the highest quality track that can be played without buffering over the current network. [CHAR LIMIT=20] -->\n  <string name=\"exo_track_selection_auto\">Auto</string>\n  <!-- Name of a media track about which nothing is known. [CHAR LIMIT=20] -->\n  <string name=\"exo_track_unknown\">Unknown</string>\n  <!-- Resolution of a video track. [CHAR LIMIT=20] -->\n  <string name=\"exo_track_resolution\"><xliff:g id=\"width\" example=\"1024\">%1$d</xliff:g> × <xliff:g id=\"height\" example=\"768\">%2$d</xliff:g></string>\n  <!-- Describes an audio track with one channel. [CHAR LIMIT=20] -->\n  <string name=\"exo_track_mono\">Mono</string>\n  <!-- Describes an audio track with two channels. [CHAR LIMIT=20] -->\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <!-- Describes a surround sound audio track. [CHAR LIMIT=40] -->\n  <string name=\"exo_track_surround\">Surround sound</string>\n  <!-- Describes a 5.1 surround sound (https://en.wikipedia.org/wiki/5.1_surround_sound) audio track. [CHAR LIMIT=40] -->\n  <string name=\"exo_track_surround_5_point_1\">5.1 surround sound</string>\n  <!-- Describes a 7.1 (https://en.wikipedia.org/wiki/7.1_surround_sound) audio track. [CHAR LIMIT=40] -->\n  <string name=\"exo_track_surround_7_point_1\">7.1 surround sound</string>\n  <!-- Describes an alternate track, e.g. a video recorded from a different viewpoint. [CHAR LIMIT=40] -->\n  <string name=\"exo_track_role_alternate\">Alternate</string>\n  <!-- Describes a supplementary track. [CHAR LIMIT=40] -->\n  <string name=\"exo_track_role_supplementary\">Supplementary</string>\n  <!-- Describes a track containing commentary. [CHAR LIMIT=40] -->\n  <string name=\"exo_track_role_commentary\">Commentary</string>\n  <!-- Describes a track with closed captions. [CHAR LIMIT=40] -->\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <!-- Describes the bitrate of a media track in Megabits (https://en.wikipedia.org/wiki/Megabit) per second. [CHAR LIMIT=20] -->\n  <string name=\"exo_track_bitrate\"><xliff:g id=\"bitrate\" example=\"5.2\">%1$.2f</xliff:g> Mbps</string>\n  <!-- Defines a way of appending an item to a list of items. For example appending \"banana\" to \"apple, pear\" to get \"apple, pear, banana\". Note: the command separator will appear between all consecutive list items, so do not use an equivalent of 'and'. [CHAR LIMIT=40] -->\n  <string name=\"exo_item_list\"><xliff:g id=\"list\" example=\"apple, pear\">%1$s</xliff:g>, <xliff:g id=\"item\" example=\"banana\">%2$s</xliff:g></string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<resources>\n\n  <style name=\"ExoMediaButton\">\n    <item name=\"android:background\">?android:attr/selectableItemBackground</item>\n    <item name=\"android:layout_width\">@dimen/exo_media_button_width</item>\n    <item name=\"android:layout_height\">@dimen/exo_media_button_height</item>\n  </style>\n\n  <style name=\"ExoMediaButton.Previous\">\n    <item name=\"android:src\">@drawable/exo_controls_previous</item>\n    <item name=\"android:contentDescription\">@string/exo_controls_previous_description</item>\n  </style>\n\n  <style name=\"ExoMediaButton.Next\">\n    <item name=\"android:src\">@drawable/exo_controls_next</item>\n    <item name=\"android:contentDescription\">@string/exo_controls_next_description</item>\n  </style>\n\n  <style name=\"ExoMediaButton.FastForward\">\n    <item name=\"android:src\">@drawable/exo_controls_fastforward</item>\n    <item name=\"android:contentDescription\">@string/exo_controls_fastforward_description</item>\n  </style>\n\n  <style name=\"ExoMediaButton.Rewind\">\n    <item name=\"android:src\">@drawable/exo_controls_rewind</item>\n    <item name=\"android:contentDescription\">@string/exo_controls_rewind_description</item>\n  </style>\n\n  <style name=\"ExoMediaButton.Play\">\n    <item name=\"android:src\">@drawable/exo_controls_play</item>\n    <item name=\"android:contentDescription\">@string/exo_controls_play_description</item>\n  </style>\n\n  <style name=\"ExoMediaButton.Pause\">\n    <item name=\"android:src\">@drawable/exo_controls_pause</item>\n    <item name=\"android:contentDescription\">@string/exo_controls_pause_description</item>\n  </style>\n\n  <style name=\"ExoMediaButton.VR\">\n    <item name=\"android:src\">@drawable/exo_icon_vr</item>\n    <item name=\"android:contentDescription\">@string/exo_controls_vr_description</item>\n  </style>\n\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-af/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Vorige snit</string>\n  <string name=\"exo_controls_next_description\">Volgende snit</string>\n  <string name=\"exo_controls_pause_description\">Onderbreek</string>\n  <string name=\"exo_controls_play_description\">Speel</string>\n  <string name=\"exo_controls_stop_description\">Stop</string>\n  <string name=\"exo_controls_rewind_description\">Spoel terug</string>\n  <string name=\"exo_controls_fastforward_description\">Spoel vorentoe</string>\n  <string name=\"exo_controls_repeat_off_description\">Herhaal niks</string>\n  <string name=\"exo_controls_repeat_one_description\">Herhaal een</string>\n  <string name=\"exo_controls_repeat_all_description\">Herhaal alles</string>\n  <string name=\"exo_controls_shuffle_on_description\">Skommel is aan</string>\n  <string name=\"exo_controls_shuffle_off_description\">Skommel is af</string>\n  <string name=\"exo_controls_fullscreen_description\">Volskermmodus</string>\n  <string name=\"exo_controls_vr_description\">VR-modus</string>\n  <string name=\"exo_download_description\">Aflaai</string>\n  <string name=\"exo_download_notification_channel_name\">Aflaaie</string>\n  <string name=\"exo_download_downloading\">Laai tans af</string>\n  <string name=\"exo_download_completed\">Aflaai is voltooi</string>\n  <string name=\"exo_download_failed\">Kon nie aflaai nie</string>\n  <string name=\"exo_download_removing\">Verwyder tans aflaaie</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Oudio</string>\n  <string name=\"exo_track_selection_title_text\">Teks</string>\n  <string name=\"exo_track_selection_none\">Geen</string>\n  <string name=\"exo_track_selection_auto\">Outo</string>\n  <string name=\"exo_track_unknown\">Onbekend</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Omringklank</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1-omringklank</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1-omringklank</string>\n  <string name=\"exo_track_role_alternate\">Afwisselend</string>\n  <string name=\"exo_track_role_supplementary\">Aanvullend</string>\n  <string name=\"exo_track_role_commentary\">Kommentaar</string>\n  <string name=\"exo_track_role_closed_captions\">Onderskrifte</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-am/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">ቀዳሚ ትራክ</string>\n  <string name=\"exo_controls_next_description\">ቀጣይ ትራክ</string>\n  <string name=\"exo_controls_pause_description\">ላፍታ አቁም</string>\n  <string name=\"exo_controls_play_description\">አጫውት</string>\n  <string name=\"exo_controls_stop_description\">አቁም</string>\n  <string name=\"exo_controls_rewind_description\">ወደኋላ መልስ</string>\n  <string name=\"exo_controls_fastforward_description\">በፍጥነት አሳልፍ</string>\n  <string name=\"exo_controls_repeat_off_description\">ምንም አትድገም</string>\n  <string name=\"exo_controls_repeat_one_description\">አንድ ድገም</string>\n  <string name=\"exo_controls_repeat_all_description\">ሁሉንም ድገም</string>\n  <string name=\"exo_controls_shuffle_on_description\">መበወዝ በርቷል</string>\n  <string name=\"exo_controls_shuffle_off_description\">መበወዝ ጠፍቷል</string>\n  <string name=\"exo_controls_fullscreen_description\">የሙሉ ማያ ሁነታ</string>\n  <string name=\"exo_controls_vr_description\">የቪአር ሁነታ</string>\n  <string name=\"exo_download_description\">አውርድ</string>\n  <string name=\"exo_download_notification_channel_name\">የወረዱ</string>\n  <string name=\"exo_download_downloading\">በማውረድ ላይ</string>\n  <string name=\"exo_download_completed\">ማውረድ ተጠናቋል</string>\n  <string name=\"exo_download_failed\">ማውረድ አልተሳካም</string>\n  <string name=\"exo_download_removing\">ውርዶችን በማስወገድ ላይ</string>\n  <string name=\"exo_track_selection_title_video\">ቪዲዮ</string>\n  <string name=\"exo_track_selection_title_audio\">ኦዲዮ</string>\n  <string name=\"exo_track_selection_title_text\">ጽሑፍ</string>\n  <string name=\"exo_track_selection_none\">ምንም</string>\n  <string name=\"exo_track_selection_auto\">ራስ-ሰር</string>\n  <string name=\"exo_track_unknown\">ያልታወቀ</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">ሞኖ</string>\n  <string name=\"exo_track_stereo\">ስቲሪዮ</string>\n  <string name=\"exo_track_surround\">የዙሪያ ድምፅ</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 የዙሪያ ድምፅ</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 የዙሪያ ድምፅ</string>\n  <string name=\"exo_track_role_alternate\">ተለዋጭ</string>\n  <string name=\"exo_track_role_supplementary\">ተጨማሪ</string>\n  <string name=\"exo_track_role_commentary\">ጥናታዊ</string>\n  <string name=\"exo_track_role_closed_captions\">የተዘጉ የመግለጫ ጽሑፎች</string>\n  <string name=\"exo_track_bitrate\">%1$.2f ሜብስ</string>\n  <string name=\"exo_item_list\">%1$s፣ %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-ar/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">المقطع الصوتي السابق</string>\n  <string name=\"exo_controls_next_description\">المقطع الصوتي التالي</string>\n  <string name=\"exo_controls_pause_description\">إيقاف مؤقت</string>\n  <string name=\"exo_controls_play_description\">تشغيل</string>\n  <string name=\"exo_controls_stop_description\">إيقاف</string>\n  <string name=\"exo_controls_rewind_description\">إرجاع</string>\n  <string name=\"exo_controls_fastforward_description\">تقديم سريع</string>\n  <string name=\"exo_controls_repeat_off_description\">عدم التكرار</string>\n  <string name=\"exo_controls_repeat_one_description\">تكرار مقطع صوتي واحد</string>\n  <string name=\"exo_controls_repeat_all_description\">تكرار الكل</string>\n  <string name=\"exo_controls_shuffle_on_description\">تفعيل الترتيب العشوائي</string>\n  <string name=\"exo_controls_shuffle_off_description\">إيقاف الترتيب العشوائي</string>\n  <string name=\"exo_controls_fullscreen_description\">وضع ملء الشاشة</string>\n  <string name=\"exo_controls_vr_description\">وضع VR</string>\n  <string name=\"exo_download_description\">تنزيل</string>\n  <string name=\"exo_download_notification_channel_name\">التنزيلات</string>\n  <string name=\"exo_download_downloading\">جارٍ التنزيل.</string>\n  <string name=\"exo_download_completed\">اكتمل التنزيل</string>\n  <string name=\"exo_download_failed\">تعذّر التنزيل</string>\n  <string name=\"exo_download_removing\">جارٍ إزالة التنزيلات</string>\n  <string name=\"exo_track_selection_title_video\">فيديو</string>\n  <string name=\"exo_track_selection_title_audio\">صوت</string>\n  <string name=\"exo_track_selection_title_text\">نص</string>\n  <string name=\"exo_track_selection_none\">بدون اختيار</string>\n  <string name=\"exo_track_selection_auto\">تلقائي</string>\n  <string name=\"exo_track_unknown\">غير معروف</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">قناة أحادية</string>\n  <string name=\"exo_track_stereo\">استريو</string>\n  <string name=\"exo_track_surround\">صوت مجسّم</string>\n  <string name=\"exo_track_surround_5_point_1\">صوت مجسّم 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">صوت مجسّم 7.1</string>\n  <string name=\"exo_track_role_alternate\">بديل</string>\n  <string name=\"exo_track_role_supplementary\">تكميلي</string>\n  <string name=\"exo_track_role_commentary\">التعليقات</string>\n  <string name=\"exo_track_role_closed_captions\">الترجمة والشرح</string>\n  <string name=\"exo_track_bitrate\">%1$.2f ميغابت في الثانية</string>\n  <string name=\"exo_item_list\">%1$s، %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-az/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Əvvəlki trek</string>\n  <string name=\"exo_controls_next_description\">Növbəti trek</string>\n  <string name=\"exo_controls_pause_description\">Pauza</string>\n  <string name=\"exo_controls_play_description\">Oxudun</string>\n  <string name=\"exo_controls_stop_description\">Dayandırın</string>\n  <string name=\"exo_controls_rewind_description\">Geri çəkin</string>\n  <string name=\"exo_controls_fastforward_description\">İrəli çəkin</string>\n  <string name=\"exo_controls_repeat_off_description\">Heç biri təkrarlanmasın</string>\n  <string name=\"exo_controls_repeat_one_description\">Biri təkrarlansın</string>\n  <string name=\"exo_controls_repeat_all_description\">Hamısı təkrarlansın</string>\n  <string name=\"exo_controls_shuffle_on_description\">Qarışdırma aktivdir</string>\n  <string name=\"exo_controls_shuffle_off_description\">Qarışdırma deaktivdir</string>\n  <string name=\"exo_controls_fullscreen_description\">Tam ekran rejimi</string>\n  <string name=\"exo_controls_vr_description\">VR rejimi</string>\n  <string name=\"exo_download_description\">Endirin</string>\n  <string name=\"exo_download_notification_channel_name\">Endirmələr</string>\n  <string name=\"exo_download_downloading\">Endirilir</string>\n  <string name=\"exo_download_completed\">Endirmə tamamlandı</string>\n  <string name=\"exo_download_failed\">Endirmə alınmadı</string>\n  <string name=\"exo_download_removing\">Endirilənlər silinir</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Mətn</string>\n  <string name=\"exo_track_selection_none\">Yoxdur</string>\n  <string name=\"exo_track_selection_auto\">Avtomatik</string>\n  <string name=\"exo_track_unknown\">Naməlum</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Yüksək səs</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 yüksək səs</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 yüksək səs</string>\n  <string name=\"exo_track_role_alternate\">Alternativ</string>\n  <string name=\"exo_track_role_supplementary\">Əlavə</string>\n  <string name=\"exo_track_role_commentary\">Şərh</string>\n  <string name=\"exo_track_role_closed_captions\">Nüsxəni alan</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-b+sr+Latn/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Prethodna pesma</string>\n  <string name=\"exo_controls_next_description\">Sledeća pesma</string>\n  <string name=\"exo_controls_pause_description\">Pauziraj</string>\n  <string name=\"exo_controls_play_description\">Pusti</string>\n  <string name=\"exo_controls_stop_description\">Zaustavi</string>\n  <string name=\"exo_controls_rewind_description\">Premotaj unazad</string>\n  <string name=\"exo_controls_fastforward_description\">Premotaj unapred</string>\n  <string name=\"exo_controls_repeat_off_description\">Ne ponavljaj nijednu</string>\n  <string name=\"exo_controls_repeat_one_description\">Ponovi jednu</string>\n  <string name=\"exo_controls_repeat_all_description\">Ponovi sve</string>\n  <string name=\"exo_controls_shuffle_on_description\">Nasumično puštanje je uključeno</string>\n  <string name=\"exo_controls_shuffle_off_description\">Nasumično puštanje je isključeno</string>\n  <string name=\"exo_controls_fullscreen_description\">Režim celog ekrana</string>\n  <string name=\"exo_controls_vr_description\">VR režim</string>\n  <string name=\"exo_download_description\">Preuzmi</string>\n  <string name=\"exo_download_notification_channel_name\">Preuzimanja</string>\n  <string name=\"exo_download_downloading\">Preuzima se</string>\n  <string name=\"exo_download_completed\">Preuzimanje je završeno</string>\n  <string name=\"exo_download_failed\">Preuzimanje nije uspelo</string>\n  <string name=\"exo_download_removing\">Preuzimanja se uklanjaju</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Tekst</string>\n  <string name=\"exo_track_selection_none\">Nijedna</string>\n  <string name=\"exo_track_selection_auto\">Automatski</string>\n  <string name=\"exo_track_unknown\">Nepoznato</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Zvučni sistem</string>\n  <string name=\"exo_track_surround_5_point_1\">Zvučni sistem 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Zvučni sistem 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternativno</string>\n  <string name=\"exo_track_role_supplementary\">Dodatno</string>\n  <string name=\"exo_track_role_commentary\">Komentar</string>\n  <string name=\"exo_track_role_closed_captions\">Titl</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mb/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-be/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Папярэдні трэк</string>\n  <string name=\"exo_controls_next_description\">Наступны трэк</string>\n  <string name=\"exo_controls_pause_description\">Паўза</string>\n  <string name=\"exo_controls_play_description\">Гуляць</string>\n  <string name=\"exo_controls_stop_description\">Спыніць</string>\n  <string name=\"exo_controls_rewind_description\">Пераматаць назад</string>\n  <string name=\"exo_controls_fastforward_description\">Пераматаць уперад</string>\n  <string name=\"exo_controls_repeat_off_description\">Не паўтараць нічога</string>\n  <string name=\"exo_controls_repeat_one_description\">Паўтарыць адзін элемент</string>\n  <string name=\"exo_controls_repeat_all_description\">Паўтарыць усе</string>\n  <string name=\"exo_controls_shuffle_on_description\">Перамешванне ўключана</string>\n  <string name=\"exo_controls_shuffle_off_description\">Перамешванне выключана</string>\n  <string name=\"exo_controls_fullscreen_description\">Поўнаэкранны рэжым</string>\n  <string name=\"exo_controls_vr_description\">VR-рэжым</string>\n  <string name=\"exo_download_description\">Спампаваць</string>\n  <string name=\"exo_download_notification_channel_name\">Спампоўкі</string>\n  <string name=\"exo_download_downloading\">Спампоўваецца</string>\n  <string name=\"exo_download_completed\">Спампоўка завершана</string>\n  <string name=\"exo_download_failed\">Збой спампоўкі</string>\n  <string name=\"exo_download_removing\">Выдаленне спамповак</string>\n  <string name=\"exo_track_selection_title_video\">Відэа</string>\n  <string name=\"exo_track_selection_title_audio\">Аўдыя</string>\n  <string name=\"exo_track_selection_title_text\">Тэкст</string>\n  <string name=\"exo_track_selection_none\">Няма</string>\n  <string name=\"exo_track_selection_auto\">Аўтаматычна</string>\n  <string name=\"exo_track_unknown\">Невядома</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Мона</string>\n  <string name=\"exo_track_stereo\">Стэрэа</string>\n  <string name=\"exo_track_surround\">Аб\\'ёмны гук</string>\n  <string name=\"exo_track_surround_5_point_1\">Аб\\'ёмны гук 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Аб\\'ёмны гук 7.1</string>\n  <string name=\"exo_track_role_alternate\">Альтэрнатыўны запіс</string>\n  <string name=\"exo_track_role_supplementary\">Дадатковы запіс</string>\n  <string name=\"exo_track_role_commentary\">Каментарыі</string>\n  <string name=\"exo_track_role_closed_captions\">Цітры</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Мбіт/с</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-bg/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Предишен запис</string>\n  <string name=\"exo_controls_next_description\">Следващ запис</string>\n  <string name=\"exo_controls_pause_description\">Поставяне на пауза</string>\n  <string name=\"exo_controls_play_description\">Възпроизвеждане</string>\n  <string name=\"exo_controls_stop_description\">Спиране</string>\n  <string name=\"exo_controls_rewind_description\">Превъртане назад</string>\n  <string name=\"exo_controls_fastforward_description\">Превъртане напред</string>\n  <string name=\"exo_controls_repeat_off_description\">Без повтаряне</string>\n  <string name=\"exo_controls_repeat_one_description\">Повтаряне на един елемент</string>\n  <string name=\"exo_controls_repeat_all_description\">Повтаряне на всички</string>\n  <string name=\"exo_controls_shuffle_on_description\">Разбъркването е включено</string>\n  <string name=\"exo_controls_shuffle_off_description\">Разбъркването е изключено</string>\n  <string name=\"exo_controls_fullscreen_description\">Режим на цял екран</string>\n  <string name=\"exo_controls_vr_description\">режим за VR</string>\n  <string name=\"exo_download_description\">Изтегляне</string>\n  <string name=\"exo_download_notification_channel_name\">Изтегляния</string>\n  <string name=\"exo_download_downloading\">Изтегля се</string>\n  <string name=\"exo_download_completed\">Изтеглянето завърши</string>\n  <string name=\"exo_download_failed\">Изтеглянето не бе успешно</string>\n  <string name=\"exo_download_removing\">Изтеглянията се премахват</string>\n  <string name=\"exo_track_selection_title_video\">Видеозапис</string>\n  <string name=\"exo_track_selection_title_audio\">Аудиозапис</string>\n  <string name=\"exo_track_selection_title_text\">Текст</string>\n  <string name=\"exo_track_selection_none\">Нищо</string>\n  <string name=\"exo_track_selection_auto\">Автоматично</string>\n  <string name=\"exo_track_unknown\">Неизвестно</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Моно</string>\n  <string name=\"exo_track_stereo\">Стерео</string>\n  <string name=\"exo_track_surround\">Обемен звук</string>\n  <string name=\"exo_track_surround_5_point_1\">5,1-канален обемен звук</string>\n  <string name=\"exo_track_surround_7_point_1\">7,1-канален обемен звук</string>\n  <string name=\"exo_track_role_alternate\">Алтернативен запис</string>\n  <string name=\"exo_track_role_supplementary\">Допълнителен запис</string>\n  <string name=\"exo_track_role_commentary\">Коментар</string>\n  <string name=\"exo_track_role_closed_captions\">Субтитри</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Мб/сек</string>\n  <string name=\"exo_item_list\">%1$s – %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-bn/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">আগের ট্র্যাক</string>\n  <string name=\"exo_controls_next_description\">পরবর্তী ট্র্যাক</string>\n  <string name=\"exo_controls_pause_description\">পজ করুন</string>\n  <string name=\"exo_controls_play_description\">চালান</string>\n  <string name=\"exo_controls_stop_description\">থামান</string>\n  <string name=\"exo_controls_rewind_description\">পিছিয়ে যান</string>\n  <string name=\"exo_controls_fastforward_description\">দ্রুত এগিয়ে যান</string>\n  <string name=\"exo_controls_repeat_off_description\">কোনও আইটেম আবার চালাবেন না</string>\n  <string name=\"exo_controls_repeat_one_description\">একটি আইটেম আবার চালান</string>\n  <string name=\"exo_controls_repeat_all_description\">সবগুলি আইটেম আবার চালান</string>\n  <string name=\"exo_controls_shuffle_on_description\">শাফেল মোড চালু করা হয়েছে</string>\n  <string name=\"exo_controls_shuffle_off_description\">শাফেল মোড বন্ধ করা হয়েছে</string>\n  <string name=\"exo_controls_fullscreen_description\">পূর্ণ স্ক্রিন মোড</string>\n  <string name=\"exo_controls_vr_description\">ভিআর মোড</string>\n  <string name=\"exo_download_description\">ডাউনলোড করুন</string>\n  <string name=\"exo_download_notification_channel_name\">ডাউনলোড</string>\n  <string name=\"exo_download_downloading\">ডাউনলোড হচ্ছে</string>\n  <string name=\"exo_download_completed\">ডাউনলোড হয়ে গেছে</string>\n  <string name=\"exo_download_failed\">ডাউনলোড করা যায়নি</string>\n  <string name=\"exo_download_removing\">ডাউনলোড করা কন্টেন্ট সরিয়ে দেওয়া হচ্ছে</string>\n  <string name=\"exo_track_selection_title_video\">ভিডিও</string>\n  <string name=\"exo_track_selection_title_audio\">অডিও</string>\n  <string name=\"exo_track_selection_title_text\">টেক্সট</string>\n  <string name=\"exo_track_selection_none\">কোনওটিই নয়</string>\n  <string name=\"exo_track_selection_auto\">অটো</string>\n  <string name=\"exo_track_unknown\">অজানা</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">মোনো</string>\n  <string name=\"exo_track_stereo\">স্টিরিও</string>\n  <string name=\"exo_track_surround\">সারাউন্ড সাউন্ড</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 সারাউন্ড সাউন্ড</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 সারাউন্ড সাউন্ড</string>\n  <string name=\"exo_track_role_alternate\">বিকল্প</string>\n  <string name=\"exo_track_role_supplementary\">অতিরিক্ত</string>\n  <string name=\"exo_track_role_commentary\">ধারাভাষ্য</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f এমবিপিএস</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-bs/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Prethodna numera</string>\n  <string name=\"exo_controls_next_description\">Sljedeća numera</string>\n  <string name=\"exo_controls_pause_description\">Pauza</string>\n  <string name=\"exo_controls_play_description\">Reproduciranje</string>\n  <string name=\"exo_controls_stop_description\">Zaustavljanje</string>\n  <string name=\"exo_controls_rewind_description\">Premotavanje unazad</string>\n  <string name=\"exo_controls_fastforward_description\">Premotavanje unaprijed</string>\n  <string name=\"exo_controls_repeat_off_description\">Ne ponavljaj</string>\n  <string name=\"exo_controls_repeat_one_description\">Ponovi jedno</string>\n  <string name=\"exo_controls_repeat_all_description\">Ponovi sve</string>\n  <string name=\"exo_controls_shuffle_on_description\">Uključi nasumično</string>\n  <string name=\"exo_controls_shuffle_off_description\">Isključi nasumično</string>\n  <string name=\"exo_controls_fullscreen_description\">Način rada preko cijelog ekrana</string>\n  <string name=\"exo_controls_vr_description\">VR način rada</string>\n  <string name=\"exo_download_description\">Preuzmi</string>\n  <string name=\"exo_download_notification_channel_name\">Preuzimanja</string>\n  <string name=\"exo_download_downloading\">Preuzimanje</string>\n  <string name=\"exo_download_completed\">Preuzimanje je završeno</string>\n  <string name=\"exo_download_failed\">Preuzimanje nije uspjelo</string>\n  <string name=\"exo_download_removing\">Uklanjanje preuzimanja</string>\n  <string name=\"exo_track_selection_title_video\">Videozapis</string>\n  <string name=\"exo_track_selection_title_audio\">Zvuk</string>\n  <string name=\"exo_track_selection_title_text\">Tekst</string>\n  <string name=\"exo_track_selection_none\">Ništa</string>\n  <string name=\"exo_track_selection_auto\">Automatski</string>\n  <string name=\"exo_track_unknown\">Nepoznato</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Prostorno ozvučenje</string>\n  <string name=\"exo_track_surround_5_point_1\">Prostorno ozvučenje 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Prostorno ozvučenje 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternativni zapis</string>\n  <string name=\"exo_track_role_supplementary\">Dodatni zapis</string>\n  <string name=\"exo_track_role_commentary\">Komentari</string>\n  <string name=\"exo_track_role_closed_captions\">Titlovi</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-ca/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Pista anterior</string>\n  <string name=\"exo_controls_next_description\">Pista següent</string>\n  <string name=\"exo_controls_pause_description\">Posa en pausa</string>\n  <string name=\"exo_controls_play_description\">Reprodueix</string>\n  <string name=\"exo_controls_stop_description\">Atura</string>\n  <string name=\"exo_controls_rewind_description\">Rebobina</string>\n  <string name=\"exo_controls_fastforward_description\">Avança ràpidament</string>\n  <string name=\"exo_controls_repeat_off_description\">No en repeteixis cap</string>\n  <string name=\"exo_controls_repeat_one_description\">Repeteix una</string>\n  <string name=\"exo_controls_repeat_all_description\">Repeteix tot</string>\n  <string name=\"exo_controls_shuffle_on_description\">Activa reproducció aleatòria</string>\n  <string name=\"exo_controls_shuffle_off_description\">Desactiva reproducció aleatòria</string>\n  <string name=\"exo_controls_fullscreen_description\">Mode de pantalla completa</string>\n  <string name=\"exo_controls_vr_description\">Mode RV</string>\n  <string name=\"exo_download_description\">Baixa</string>\n  <string name=\"exo_download_notification_channel_name\">Baixades</string>\n  <string name=\"exo_download_downloading\">S\\'està baixant</string>\n  <string name=\"exo_download_completed\">S\\'ha completat la baixada</string>\n  <string name=\"exo_download_failed\">No s\\'ha pogut baixar</string>\n  <string name=\"exo_download_removing\">S\\'estan suprimint les baixades</string>\n  <string name=\"exo_track_selection_title_video\">Vídeo</string>\n  <string name=\"exo_track_selection_title_audio\">Àudio</string>\n  <string name=\"exo_track_selection_title_text\">Text</string>\n  <string name=\"exo_track_selection_none\">Cap</string>\n  <string name=\"exo_track_selection_auto\">Automàtica</string>\n  <string name=\"exo_track_unknown\">Desconegut</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Estèreo</string>\n  <string name=\"exo_track_surround\">So envoltant</string>\n  <string name=\"exo_track_surround_5_point_1\">So envoltant 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">So envoltant 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternativa</string>\n  <string name=\"exo_track_role_supplementary\">Complementària</string>\n  <string name=\"exo_track_role_commentary\">Comentaris</string>\n  <string name=\"exo_track_role_closed_captions\">Subtítols</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-cs/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Předchozí skladba</string>\n  <string name=\"exo_controls_next_description\">Další skladba</string>\n  <string name=\"exo_controls_pause_description\">Pozastavit</string>\n  <string name=\"exo_controls_play_description\">Přehrát</string>\n  <string name=\"exo_controls_stop_description\">Zastavit</string>\n  <string name=\"exo_controls_rewind_description\">Přetočit zpět</string>\n  <string name=\"exo_controls_fastforward_description\">Rychle vpřed</string>\n  <string name=\"exo_controls_repeat_off_description\">Neopakovat</string>\n  <string name=\"exo_controls_repeat_one_description\">Opakovat jednu</string>\n  <string name=\"exo_controls_repeat_all_description\">Opakovat vše</string>\n  <string name=\"exo_controls_shuffle_on_description\">Náhodné přehrávání zapnuto</string>\n  <string name=\"exo_controls_shuffle_off_description\">Náhodné přehrávání vypnuto</string>\n  <string name=\"exo_controls_fullscreen_description\">Režim celé obrazovky</string>\n  <string name=\"exo_controls_vr_description\">Režim VR</string>\n  <string name=\"exo_download_description\">Stáhnout</string>\n  <string name=\"exo_download_notification_channel_name\">Stahování</string>\n  <string name=\"exo_download_downloading\">Stahování</string>\n  <string name=\"exo_download_completed\">Stahování bylo dokončeno</string>\n  <string name=\"exo_download_failed\">Stažení se nezdařilo</string>\n  <string name=\"exo_download_removing\">Odstraňování staženého obsahu</string>\n  <string name=\"exo_track_selection_title_video\">Videa</string>\n  <string name=\"exo_track_selection_title_audio\">Zvuk</string>\n  <string name=\"exo_track_selection_title_text\">Text</string>\n  <string name=\"exo_track_selection_none\">Žádné</string>\n  <string name=\"exo_track_selection_auto\">Automaticky</string>\n  <string name=\"exo_track_unknown\">Neznámé</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Prostorový zvuk</string>\n  <string name=\"exo_track_surround_5_point_1\">Prostorový zvuk 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Prostorový zvuk 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternativní</string>\n  <string name=\"exo_track_role_supplementary\">Dodatkové</string>\n  <string name=\"exo_track_role_commentary\">Komentář</string>\n  <string name=\"exo_track_role_closed_captions\">Titulky</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mb/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-da/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Afspil forrige</string>\n  <string name=\"exo_controls_next_description\">Afspil næste</string>\n  <string name=\"exo_controls_pause_description\">Sæt på pause</string>\n  <string name=\"exo_controls_play_description\">Afspil</string>\n  <string name=\"exo_controls_stop_description\">Stop</string>\n  <string name=\"exo_controls_rewind_description\">Spol tilbage</string>\n  <string name=\"exo_controls_fastforward_description\">Spol frem</string>\n  <string name=\"exo_controls_repeat_off_description\">Gentag ingen</string>\n  <string name=\"exo_controls_repeat_one_description\">Gentag én</string>\n  <string name=\"exo_controls_repeat_all_description\">Gentag alle</string>\n  <string name=\"exo_controls_shuffle_on_description\">Bland er slået til</string>\n  <string name=\"exo_controls_shuffle_off_description\">Bland er slået fra</string>\n  <string name=\"exo_controls_fullscreen_description\">Fuld skærm</string>\n  <string name=\"exo_controls_vr_description\">VR-tilstand</string>\n  <string name=\"exo_download_description\">Download</string>\n  <string name=\"exo_download_notification_channel_name\">Downloads</string>\n  <string name=\"exo_download_downloading\">Downloader</string>\n  <string name=\"exo_download_completed\">Downloaden er udført</string>\n  <string name=\"exo_download_failed\">Download mislykkedes</string>\n  <string name=\"exo_download_removing\">Fjerner downloads</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Lyd</string>\n  <string name=\"exo_track_selection_title_text\">Undertekst</string>\n  <string name=\"exo_track_selection_none\">Ingen</string>\n  <string name=\"exo_track_selection_auto\">Automatisk</string>\n  <string name=\"exo_track_unknown\">Ukendt</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Surroundsound</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 surround sound</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 surroundsound</string>\n  <string name=\"exo_track_role_alternate\">Alternativt spor</string>\n  <string name=\"exo_track_role_supplementary\">Supplerende spor</string>\n  <string name=\"exo_track_role_commentary\">Kommentarspor</string>\n  <string name=\"exo_track_role_closed_captions\">Undertekster</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-de/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Vorheriger Titel</string>\n  <string name=\"exo_controls_next_description\">Nächster Titel</string>\n  <string name=\"exo_controls_pause_description\">Pausieren</string>\n  <string name=\"exo_controls_play_description\">Wiedergeben</string>\n  <string name=\"exo_controls_stop_description\">Beenden</string>\n  <string name=\"exo_controls_rewind_description\">Zurückspulen</string>\n  <string name=\"exo_controls_fastforward_description\">Vorspulen</string>\n  <string name=\"exo_controls_repeat_off_description\">Keinen wiederholen</string>\n  <string name=\"exo_controls_repeat_one_description\">Einen wiederholen</string>\n  <string name=\"exo_controls_repeat_all_description\">Alle wiederholen</string>\n  <string name=\"exo_controls_shuffle_on_description\">Zufallsmix an</string>\n  <string name=\"exo_controls_shuffle_off_description\">Zufallsmix aus</string>\n  <string name=\"exo_controls_fullscreen_description\">Vollbildmodus</string>\n  <string name=\"exo_controls_vr_description\">VR-Modus</string>\n  <string name=\"exo_download_description\">Herunterladen</string>\n  <string name=\"exo_download_notification_channel_name\">Downloads</string>\n  <string name=\"exo_download_downloading\">Wird heruntergeladen</string>\n  <string name=\"exo_download_completed\">Download abgeschlossen</string>\n  <string name=\"exo_download_failed\">Download fehlgeschlagen</string>\n  <string name=\"exo_download_removing\">Downloads werden entfernt</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Text</string>\n  <string name=\"exo_track_selection_none\">Ohne</string>\n  <string name=\"exo_track_selection_auto\">Automatisch</string>\n  <string name=\"exo_track_unknown\">Unbekannt</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Surround-Sound</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1-Surround-Sound</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1-Surround-Sound</string>\n  <string name=\"exo_track_role_alternate\">Alternativer Track</string>\n  <string name=\"exo_track_role_supplementary\">Zusatzinhalte</string>\n  <string name=\"exo_track_role_commentary\">Kommentare</string>\n  <string name=\"exo_track_role_closed_captions\">Untertitel</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbit/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-el/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Προηγούμενο κομμάτι</string>\n  <string name=\"exo_controls_next_description\">Επόμενο κομμάτι</string>\n  <string name=\"exo_controls_pause_description\">Παύση</string>\n  <string name=\"exo_controls_play_description\">Αναπαραγωγή</string>\n  <string name=\"exo_controls_stop_description\">Διακοπή</string>\n  <string name=\"exo_controls_rewind_description\">Επαναφορά</string>\n  <string name=\"exo_controls_fastforward_description\">Γρήγορη προώθηση</string>\n  <string name=\"exo_controls_repeat_off_description\">Καμία επανάληψη</string>\n  <string name=\"exo_controls_repeat_one_description\">Επανάληψη ενός κομματιού</string>\n  <string name=\"exo_controls_repeat_all_description\">Επανάληψη όλων</string>\n  <string name=\"exo_controls_shuffle_on_description\">Τυχαία αναπαραγωγή: Ενεργή</string>\n  <string name=\"exo_controls_shuffle_off_description\">Τυχαία αναπαραγωγή: Ανενεργή</string>\n  <string name=\"exo_controls_fullscreen_description\">Λειτουργία πλήρους οθόνης</string>\n  <string name=\"exo_controls_vr_description\">Λειτουργία VR mode</string>\n  <string name=\"exo_download_description\">Λήψη</string>\n  <string name=\"exo_download_notification_channel_name\">Λήψεις</string>\n  <string name=\"exo_download_downloading\">Λήψη</string>\n  <string name=\"exo_download_completed\">Η λήψη ολοκληρώθηκε</string>\n  <string name=\"exo_download_failed\">Η λήψη απέτυχε</string>\n  <string name=\"exo_download_removing\">Κατάργηση λήψεων</string>\n  <string name=\"exo_track_selection_title_video\">Βίντεο</string>\n  <string name=\"exo_track_selection_title_audio\">Ήχος</string>\n  <string name=\"exo_track_selection_title_text\">Κείμενο</string>\n  <string name=\"exo_track_selection_none\">Κανένα</string>\n  <string name=\"exo_track_selection_auto\">Αυτόματο</string>\n  <string name=\"exo_track_unknown\">Άγνωστο</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Μονοφωνικό</string>\n  <string name=\"exo_track_stereo\">Στερεοφωνικό</string>\n  <string name=\"exo_track_surround\">Περιφερειακός ήχος</string>\n  <string name=\"exo_track_surround_5_point_1\">Περιφερειακός ήχος 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Περιφερειακός ήχος 7.1</string>\n  <string name=\"exo_track_role_alternate\">Εναλλακτικό</string>\n  <string name=\"exo_track_role_supplementary\">Συμπληρωματικό</string>\n  <string name=\"exo_track_role_commentary\">Σχολιασμός</string>\n  <string name=\"exo_track_role_closed_captions\">Υπότιτλοι</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-en-rAU/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Previous track</string>\n  <string name=\"exo_controls_next_description\">Next track</string>\n  <string name=\"exo_controls_pause_description\">Pause</string>\n  <string name=\"exo_controls_play_description\">Play</string>\n  <string name=\"exo_controls_stop_description\">Stop</string>\n  <string name=\"exo_controls_rewind_description\">Rewind</string>\n  <string name=\"exo_controls_fastforward_description\">Fast-forward</string>\n  <string name=\"exo_controls_repeat_off_description\">Repeat none</string>\n  <string name=\"exo_controls_repeat_one_description\">Repeat one</string>\n  <string name=\"exo_controls_repeat_all_description\">Repeat all</string>\n  <string name=\"exo_controls_shuffle_on_description\">Shuffle on</string>\n  <string name=\"exo_controls_shuffle_off_description\">Shuffle off</string>\n  <string name=\"exo_controls_fullscreen_description\">Full-screen mode</string>\n  <string name=\"exo_controls_vr_description\">VR mode</string>\n  <string name=\"exo_download_description\">Download</string>\n  <string name=\"exo_download_notification_channel_name\">Downloads</string>\n  <string name=\"exo_download_downloading\">Downloading</string>\n  <string name=\"exo_download_completed\">Download completed</string>\n  <string name=\"exo_download_failed\">Download failed</string>\n  <string name=\"exo_download_removing\">Removing downloads</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Text</string>\n  <string name=\"exo_track_selection_none\">None</string>\n  <string name=\"exo_track_selection_auto\">Auto</string>\n  <string name=\"exo_track_unknown\">Unknown</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Surround sound</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 surround sound</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 surround sound</string>\n  <string name=\"exo_track_role_alternate\">Alternative</string>\n  <string name=\"exo_track_role_supplementary\">Supplementary</string>\n  <string name=\"exo_track_role_commentary\">Commentary</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-en-rGB/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Previous track</string>\n  <string name=\"exo_controls_next_description\">Next track</string>\n  <string name=\"exo_controls_pause_description\">Pause</string>\n  <string name=\"exo_controls_play_description\">Play</string>\n  <string name=\"exo_controls_stop_description\">Stop</string>\n  <string name=\"exo_controls_rewind_description\">Rewind</string>\n  <string name=\"exo_controls_fastforward_description\">Fast-forward</string>\n  <string name=\"exo_controls_repeat_off_description\">Repeat none</string>\n  <string name=\"exo_controls_repeat_one_description\">Repeat one</string>\n  <string name=\"exo_controls_repeat_all_description\">Repeat all</string>\n  <string name=\"exo_controls_shuffle_on_description\">Shuffle on</string>\n  <string name=\"exo_controls_shuffle_off_description\">Shuffle off</string>\n  <string name=\"exo_controls_fullscreen_description\">Full-screen mode</string>\n  <string name=\"exo_controls_vr_description\">VR mode</string>\n  <string name=\"exo_download_description\">Download</string>\n  <string name=\"exo_download_notification_channel_name\">Downloads</string>\n  <string name=\"exo_download_downloading\">Downloading</string>\n  <string name=\"exo_download_completed\">Download completed</string>\n  <string name=\"exo_download_failed\">Download failed</string>\n  <string name=\"exo_download_removing\">Removing downloads</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Text</string>\n  <string name=\"exo_track_selection_none\">None</string>\n  <string name=\"exo_track_selection_auto\">Auto</string>\n  <string name=\"exo_track_unknown\">Unknown</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Surround sound</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 surround sound</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 surround sound</string>\n  <string name=\"exo_track_role_alternate\">Alternative</string>\n  <string name=\"exo_track_role_supplementary\">Supplementary</string>\n  <string name=\"exo_track_role_commentary\">Commentary</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-en-rIN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Previous track</string>\n  <string name=\"exo_controls_next_description\">Next track</string>\n  <string name=\"exo_controls_pause_description\">Pause</string>\n  <string name=\"exo_controls_play_description\">Play</string>\n  <string name=\"exo_controls_stop_description\">Stop</string>\n  <string name=\"exo_controls_rewind_description\">Rewind</string>\n  <string name=\"exo_controls_fastforward_description\">Fast-forward</string>\n  <string name=\"exo_controls_repeat_off_description\">Repeat none</string>\n  <string name=\"exo_controls_repeat_one_description\">Repeat one</string>\n  <string name=\"exo_controls_repeat_all_description\">Repeat all</string>\n  <string name=\"exo_controls_shuffle_on_description\">Shuffle on</string>\n  <string name=\"exo_controls_shuffle_off_description\">Shuffle off</string>\n  <string name=\"exo_controls_fullscreen_description\">Full-screen mode</string>\n  <string name=\"exo_controls_vr_description\">VR mode</string>\n  <string name=\"exo_download_description\">Download</string>\n  <string name=\"exo_download_notification_channel_name\">Downloads</string>\n  <string name=\"exo_download_downloading\">Downloading</string>\n  <string name=\"exo_download_completed\">Download completed</string>\n  <string name=\"exo_download_failed\">Download failed</string>\n  <string name=\"exo_download_removing\">Removing downloads</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Text</string>\n  <string name=\"exo_track_selection_none\">None</string>\n  <string name=\"exo_track_selection_auto\">Auto</string>\n  <string name=\"exo_track_unknown\">Unknown</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Surround sound</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 surround sound</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 surround sound</string>\n  <string name=\"exo_track_role_alternate\">Alternative</string>\n  <string name=\"exo_track_role_supplementary\">Supplementary</string>\n  <string name=\"exo_track_role_commentary\">Commentary</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-es/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Pista anterior</string>\n  <string name=\"exo_controls_next_description\">Siguiente pista</string>\n  <string name=\"exo_controls_pause_description\">Pausar</string>\n  <string name=\"exo_controls_play_description\">Reproducir</string>\n  <string name=\"exo_controls_stop_description\">Detener</string>\n  <string name=\"exo_controls_rewind_description\">Rebobinar</string>\n  <string name=\"exo_controls_fastforward_description\">Avanzar rápidamente</string>\n  <string name=\"exo_controls_repeat_off_description\">No repetir</string>\n  <string name=\"exo_controls_repeat_one_description\">Repetir uno</string>\n  <string name=\"exo_controls_repeat_all_description\">Repetir todo</string>\n  <string name=\"exo_controls_shuffle_on_description\">Con reproducción aleatoria</string>\n  <string name=\"exo_controls_shuffle_off_description\">Sin reproducción aleatoria</string>\n  <string name=\"exo_controls_fullscreen_description\">Modo de pantalla completa</string>\n  <string name=\"exo_controls_vr_description\">Modo RV</string>\n  <string name=\"exo_download_description\">Descargar</string>\n  <string name=\"exo_download_notification_channel_name\">Descargas</string>\n  <string name=\"exo_download_downloading\">Descargando</string>\n  <string name=\"exo_download_completed\">Descarga de archivos completado</string>\n  <string name=\"exo_download_failed\">No se ha podido descargar</string>\n  <string name=\"exo_download_removing\">Quitando descargas</string>\n  <string name=\"exo_track_selection_title_video\">Vídeo</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Texto</string>\n  <string name=\"exo_track_selection_none\">Ninguna</string>\n  <string name=\"exo_track_selection_auto\">Automático</string>\n  <string name=\"exo_track_unknown\">Desconocido</string>\n  <string name=\"exo_track_resolution\">%1$d×%2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Estéreo</string>\n  <string name=\"exo_track_surround\">Sonido envolvente</string>\n  <string name=\"exo_track_surround_5_point_1\">Sonido envolvente 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Sonido envolvente 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternativa</string>\n  <string name=\"exo_track_role_supplementary\">Complementaria</string>\n  <string name=\"exo_track_role_commentary\">Comentarios</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-es-rUS/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Pista anterior</string>\n  <string name=\"exo_controls_next_description\">Pista siguiente</string>\n  <string name=\"exo_controls_pause_description\">Pausar</string>\n  <string name=\"exo_controls_play_description\">Reproducir</string>\n  <string name=\"exo_controls_stop_description\">Detener</string>\n  <string name=\"exo_controls_rewind_description\">Retroceder</string>\n  <string name=\"exo_controls_fastforward_description\">Avanzar</string>\n  <string name=\"exo_controls_repeat_off_description\">No repetir</string>\n  <string name=\"exo_controls_repeat_one_description\">Repetir uno</string>\n  <string name=\"exo_controls_repeat_all_description\">Repetir todo</string>\n  <string name=\"exo_controls_shuffle_on_description\">Reprod. aleatoria activada</string>\n  <string name=\"exo_controls_shuffle_off_description\">Reprod. aleatoria desactivada</string>\n  <string name=\"exo_controls_fullscreen_description\">Modo de pantalla completa</string>\n  <string name=\"exo_controls_vr_description\">Modo RV</string>\n  <string name=\"exo_download_description\">Descargar</string>\n  <string name=\"exo_download_notification_channel_name\">Descargas</string>\n  <string name=\"exo_download_downloading\">Descargando</string>\n  <string name=\"exo_download_completed\">Se completó la descarga</string>\n  <string name=\"exo_download_failed\">No se pudo descargar</string>\n  <string name=\"exo_download_removing\">Quitando descargas</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Texto</string>\n  <string name=\"exo_track_selection_none\">Ninguna</string>\n  <string name=\"exo_track_selection_auto\">Automática</string>\n  <string name=\"exo_track_unknown\">Desconocido</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Estéreo</string>\n  <string name=\"exo_track_surround\">Sonido envolvente</string>\n  <string name=\"exo_track_surround_5_point_1\">Sonido envolvente 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Sonido envolvente 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternativa</string>\n  <string name=\"exo_track_role_supplementary\">Complementaria</string>\n  <string name=\"exo_track_role_commentary\">Comentarios</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-et/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Eelmine lugu</string>\n  <string name=\"exo_controls_next_description\">Järgmine lugu</string>\n  <string name=\"exo_controls_pause_description\">Peata</string>\n  <string name=\"exo_controls_play_description\">Esita</string>\n  <string name=\"exo_controls_stop_description\">Lõpeta</string>\n  <string name=\"exo_controls_rewind_description\">Keri tagasi</string>\n  <string name=\"exo_controls_fastforward_description\">Keri edasi</string>\n  <string name=\"exo_controls_repeat_off_description\">Ära korda ühtegi</string>\n  <string name=\"exo_controls_repeat_one_description\">Korda ühte</string>\n  <string name=\"exo_controls_repeat_all_description\">Korda kõiki</string>\n  <string name=\"exo_controls_shuffle_on_description\">Lülita juh. järj. esit. sisse</string>\n  <string name=\"exo_controls_shuffle_off_description\">Lülita juh. järj. esit. välja</string>\n  <string name=\"exo_controls_fullscreen_description\">Täisekraani režiim</string>\n  <string name=\"exo_controls_vr_description\">VR-režiim</string>\n  <string name=\"exo_download_description\">Allalaadimine</string>\n  <string name=\"exo_download_notification_channel_name\">Allalaadimised</string>\n  <string name=\"exo_download_downloading\">Allalaadimine</string>\n  <string name=\"exo_download_completed\">Allalaadimine lõpetati</string>\n  <string name=\"exo_download_failed\">Allalaadimine ebaõnnestus</string>\n  <string name=\"exo_download_removing\">Allalaadimiste eemaldamine</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Heli</string>\n  <string name=\"exo_track_selection_title_text\">Tekst</string>\n  <string name=\"exo_track_selection_none\">Ühtegi</string>\n  <string name=\"exo_track_selection_auto\">Automaatne</string>\n  <string name=\"exo_track_unknown\">Teadmata</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Ruumiline heli</string>\n  <string name=\"exo_track_surround_5_point_1\">Ruumiline heli 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Ruumiline heli 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternatiiv</string>\n  <string name=\"exo_track_role_supplementary\">Lisalugu</string>\n  <string name=\"exo_track_role_commentary\">Kommentaar</string>\n  <string name=\"exo_track_role_closed_captions\">Subtiitrid</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbit/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-eu/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Aurreko pista</string>\n  <string name=\"exo_controls_next_description\">Hurrengo pista</string>\n  <string name=\"exo_controls_pause_description\">Pausatu</string>\n  <string name=\"exo_controls_play_description\">Erreproduzitu</string>\n  <string name=\"exo_controls_stop_description\">Gelditu</string>\n  <string name=\"exo_controls_rewind_description\">Atzeratu</string>\n  <string name=\"exo_controls_fastforward_description\">Aurreratu</string>\n  <string name=\"exo_controls_repeat_off_description\">Ez errepikatu</string>\n  <string name=\"exo_controls_repeat_one_description\">Errepikatu bat</string>\n  <string name=\"exo_controls_repeat_all_description\">Errepikatu guztiak</string>\n  <string name=\"exo_controls_shuffle_on_description\">Ausazko erreprodukzioa aktibatuta</string>\n  <string name=\"exo_controls_shuffle_off_description\">Ausazko erreprodukzioa desaktibatuta</string>\n  <string name=\"exo_controls_fullscreen_description\">Pantaila osoko modua</string>\n  <string name=\"exo_controls_vr_description\">EB modua</string>\n  <string name=\"exo_download_description\">Deskargak</string>\n  <string name=\"exo_download_notification_channel_name\">Deskargak</string>\n  <string name=\"exo_download_downloading\">Deskargatzen</string>\n  <string name=\"exo_download_completed\">Osatu da deskarga</string>\n  <string name=\"exo_download_failed\">Ezin izan da deskargatu</string>\n  <string name=\"exo_download_removing\">Deskargak kentzen</string>\n  <string name=\"exo_track_selection_title_video\">Bideoa</string>\n  <string name=\"exo_track_selection_title_audio\">Audioa</string>\n  <string name=\"exo_track_selection_title_text\">Testua</string>\n  <string name=\"exo_track_selection_none\">Bat ere ez</string>\n  <string name=\"exo_track_selection_auto\">Automatikoa</string>\n  <string name=\"exo_track_unknown\">Ezezaguna</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Monoa</string>\n  <string name=\"exo_track_stereo\">Estereoa</string>\n  <string name=\"exo_track_surround\">Soinu inguratzailea</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 soinu inguratzailea</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 soinu inguratzailea</string>\n  <string name=\"exo_track_role_alternate\">Ordezkoa</string>\n  <string name=\"exo_track_role_supplementary\">Osagarria</string>\n  <string name=\"exo_track_role_commentary\">Iruzkinak</string>\n  <string name=\"exo_track_role_closed_captions\">Azpitituluak</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mb/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-fa/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">آهنگ قبلی</string>\n  <string name=\"exo_controls_next_description\">آهنگ بعدی</string>\n  <string name=\"exo_controls_pause_description\">مکث</string>\n  <string name=\"exo_controls_play_description\">پخش</string>\n  <string name=\"exo_controls_stop_description\">توقف</string>\n  <string name=\"exo_controls_rewind_description\">عقب بردن</string>\n  <string name=\"exo_controls_fastforward_description\">جلو بردن سریع</string>\n  <string name=\"exo_controls_repeat_off_description\">تکرار هیچ‌کدام</string>\n  <string name=\"exo_controls_repeat_one_description\">یکبار تکرار</string>\n  <string name=\"exo_controls_repeat_all_description\">تکرار همه</string>\n  <string name=\"exo_controls_shuffle_on_description\">پخش درهم روشن</string>\n  <string name=\"exo_controls_shuffle_off_description\">پخش درهم خاموش</string>\n  <string name=\"exo_controls_fullscreen_description\">حالت تمام‌صفحه</string>\n  <string name=\"exo_controls_vr_description\">حالت واقعیت مجازی</string>\n  <string name=\"exo_download_description\">بارگیری</string>\n  <string name=\"exo_download_notification_channel_name\">بارگیری‌ها</string>\n  <string name=\"exo_download_downloading\">درحال بارگیری</string>\n  <string name=\"exo_download_completed\">بارگیری کامل شد</string>\n  <string name=\"exo_download_failed\">بارگیری نشد</string>\n  <string name=\"exo_download_removing\">حذف بارگیری‌ها</string>\n  <string name=\"exo_track_selection_title_video\">ویدیو</string>\n  <string name=\"exo_track_selection_title_audio\">صوتی</string>\n  <string name=\"exo_track_selection_title_text\">نوشتار</string>\n  <string name=\"exo_track_selection_none\">هیچ‌کدام</string>\n  <string name=\"exo_track_selection_auto\">خودکار</string>\n  <string name=\"exo_track_unknown\">نامشخص</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">مونو</string>\n  <string name=\"exo_track_stereo\">استریو</string>\n  <string name=\"exo_track_surround\">صدای فراگیر</string>\n  <string name=\"exo_track_surround_5_point_1\">صدای فراگیر ۵.۱</string>\n  <string name=\"exo_track_surround_7_point_1\">صدای فراگیر ۷٫۱</string>\n  <string name=\"exo_track_role_alternate\">بدیل</string>\n  <string name=\"exo_track_role_supplementary\">تکمیلی</string>\n  <string name=\"exo_track_role_commentary\">شرح و نقد</string>\n  <string name=\"exo_track_role_closed_captions\">زیرنویس ناشنوایان</string>\n  <string name=\"exo_track_bitrate\">%1$.2f مگابیت در ثانیه</string>\n  <string name=\"exo_item_list\">%1$s،‏ %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-fi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Edellinen kappale</string>\n  <string name=\"exo_controls_next_description\">Seuraava kappale</string>\n  <string name=\"exo_controls_pause_description\">Keskeytä</string>\n  <string name=\"exo_controls_play_description\">Toista</string>\n  <string name=\"exo_controls_stop_description\">Lopeta</string>\n  <string name=\"exo_controls_rewind_description\">Kelaa taaksepäin</string>\n  <string name=\"exo_controls_fastforward_description\">Kelaa eteenpäin</string>\n  <string name=\"exo_controls_repeat_off_description\">Ei uudelleentoistoa</string>\n  <string name=\"exo_controls_repeat_one_description\">Toista yksi uudelleen</string>\n  <string name=\"exo_controls_repeat_all_description\">Toista kaikki uudelleen</string>\n  <string name=\"exo_controls_shuffle_on_description\">Satunnaistoisto käytössä</string>\n  <string name=\"exo_controls_shuffle_off_description\">Satunnaistoisto ei käytössä</string>\n  <string name=\"exo_controls_fullscreen_description\">Koko näytön tila</string>\n  <string name=\"exo_controls_vr_description\">VR-tila</string>\n  <string name=\"exo_download_description\">Lataa</string>\n  <string name=\"exo_download_notification_channel_name\">Lataukset</string>\n  <string name=\"exo_download_downloading\">Ladataan</string>\n  <string name=\"exo_download_completed\">Lataus valmis</string>\n  <string name=\"exo_download_failed\">Lataus epäonnistui</string>\n  <string name=\"exo_download_removing\">Poistetaan latauksia</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Ääni</string>\n  <string name=\"exo_track_selection_title_text\">Teksti</string>\n  <string name=\"exo_track_selection_none\">–</string>\n  <string name=\"exo_track_selection_auto\">Automaattinen</string>\n  <string name=\"exo_track_unknown\">Tuntematon</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Surround-ääni</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1-surround-ääni</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1-surround-ääni</string>\n  <string name=\"exo_track_role_alternate\">Vaihtoehtoinen</string>\n  <string name=\"exo_track_role_supplementary\">Lisämateriaali</string>\n  <string name=\"exo_track_role_commentary\">Kommenttiraita</string>\n  <string name=\"exo_track_role_closed_captions\">Tekstitykset</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mt/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-fr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Titre précédent</string>\n  <string name=\"exo_controls_next_description\">Titre suivant</string>\n  <string name=\"exo_controls_pause_description\">Pause</string>\n  <string name=\"exo_controls_play_description\">Lecture</string>\n  <string name=\"exo_controls_stop_description\">Arrêter</string>\n  <string name=\"exo_controls_rewind_description\">Retour arrière</string>\n  <string name=\"exo_controls_fastforward_description\">Avance rapide</string>\n  <string name=\"exo_controls_repeat_off_description\">Ne rien lire en boucle</string>\n  <string name=\"exo_controls_repeat_one_description\">Lire un titre en boucle</string>\n  <string name=\"exo_controls_repeat_all_description\">Tout lire en boucle</string>\n  <string name=\"exo_controls_shuffle_on_description\">Lecture aléatoire activée</string>\n  <string name=\"exo_controls_shuffle_off_description\">Lecture aléatoire désactivée</string>\n  <string name=\"exo_controls_fullscreen_description\">Mode plein écran</string>\n  <string name=\"exo_controls_vr_description\">Mode RV</string>\n  <string name=\"exo_download_description\">Télécharger</string>\n  <string name=\"exo_download_notification_channel_name\">Téléchargements</string>\n  <string name=\"exo_download_downloading\">Téléchargement…</string>\n  <string name=\"exo_download_completed\">Téléchargement terminé</string>\n  <string name=\"exo_download_failed\">Échec du téléchargement</string>\n  <string name=\"exo_download_removing\">Suppression des téléchargements…</string>\n  <string name=\"exo_track_selection_title_video\">Vidéo</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Texte</string>\n  <string name=\"exo_track_selection_none\">Aucun</string>\n  <string name=\"exo_track_selection_auto\">Automatique</string>\n  <string name=\"exo_track_unknown\">Inconnu</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stéréo</string>\n  <string name=\"exo_track_surround\">Son surround</string>\n  <string name=\"exo_track_surround_5_point_1\">Son surround 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Son surround 7.1</string>\n  <string name=\"exo_track_role_alternate\">Piste alternative</string>\n  <string name=\"exo_track_role_supplementary\">Piste supplémentaire</string>\n  <string name=\"exo_track_role_commentary\">Commentaires</string>\n  <string name=\"exo_track_role_closed_captions\">Sous-titres</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbit/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-fr-rCA/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Chanson précédente</string>\n  <string name=\"exo_controls_next_description\">Chanson suivante</string>\n  <string name=\"exo_controls_pause_description\">Pause</string>\n  <string name=\"exo_controls_play_description\">Lire</string>\n  <string name=\"exo_controls_stop_description\">Arrêter</string>\n  <string name=\"exo_controls_rewind_description\">Retour arrière</string>\n  <string name=\"exo_controls_fastforward_description\">Avance rapide</string>\n  <string name=\"exo_controls_repeat_off_description\">Ne rien lire en boucle</string>\n  <string name=\"exo_controls_repeat_one_description\">Lire une chanson en boucle</string>\n  <string name=\"exo_controls_repeat_all_description\">Tout lire en boucle</string>\n  <string name=\"exo_controls_shuffle_on_description\">Lecture aléatoire activée</string>\n  <string name=\"exo_controls_shuffle_off_description\">Lecture aléatoire désactivée</string>\n  <string name=\"exo_controls_fullscreen_description\">Mode Plein écran</string>\n  <string name=\"exo_controls_vr_description\">Mode RV</string>\n  <string name=\"exo_download_description\">Télécharger</string>\n  <string name=\"exo_download_notification_channel_name\">Téléchargements</string>\n  <string name=\"exo_download_downloading\">Téléchargement en cours…</string>\n  <string name=\"exo_download_completed\">Téléchargement terminé</string>\n  <string name=\"exo_download_failed\">Échec du téléchargement</string>\n  <string name=\"exo_download_removing\">Suppression des téléchargements en cours…</string>\n  <string name=\"exo_track_selection_title_video\">Vidéo</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Texte</string>\n  <string name=\"exo_track_selection_none\">Aucun</string>\n  <string name=\"exo_track_selection_auto\">Auto</string>\n  <string name=\"exo_track_unknown\">Inconnu</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stéréo</string>\n  <string name=\"exo_track_surround\">Son ambiophonique</string>\n  <string name=\"exo_track_surround_5_point_1\">Son ambiophonique 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Son ambiophonique 7.1</string>\n  <string name=\"exo_track_role_alternate\">Autre version</string>\n  <string name=\"exo_track_role_supplementary\">Supplémentaire</string>\n  <string name=\"exo_track_role_commentary\">Commentaires</string>\n  <string name=\"exo_track_role_closed_captions\">Sous-titres codés</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mb/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-gl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Pista anterior</string>\n  <string name=\"exo_controls_next_description\">Pista seguinte</string>\n  <string name=\"exo_controls_pause_description\">Pausar</string>\n  <string name=\"exo_controls_play_description\">Reproducir</string>\n  <string name=\"exo_controls_stop_description\">Deter</string>\n  <string name=\"exo_controls_rewind_description\">Rebobinar</string>\n  <string name=\"exo_controls_fastforward_description\">Avance rápido</string>\n  <string name=\"exo_controls_repeat_off_description\">Non repetir</string>\n  <string name=\"exo_controls_repeat_one_description\">Repetir unha pista</string>\n  <string name=\"exo_controls_repeat_all_description\">Repetir todas as pistas</string>\n  <string name=\"exo_controls_shuffle_on_description\">Reprodución aleatoria activada</string>\n  <string name=\"exo_controls_shuffle_off_description\">Reprodución aleat. desactivada</string>\n  <string name=\"exo_controls_fullscreen_description\">Modo de pantalla completa</string>\n  <string name=\"exo_controls_vr_description\">Modo RV</string>\n  <string name=\"exo_download_description\">Descargar</string>\n  <string name=\"exo_download_notification_channel_name\">Descargas</string>\n  <string name=\"exo_download_downloading\">Descargando</string>\n  <string name=\"exo_download_completed\">Completouse a descarga</string>\n  <string name=\"exo_download_failed\">Produciuse un erro na descarga</string>\n  <string name=\"exo_download_removing\">Quitando descargas</string>\n  <string name=\"exo_track_selection_title_video\">Vídeo</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Texto</string>\n  <string name=\"exo_track_selection_none\">Ningunha pista</string>\n  <string name=\"exo_track_selection_auto\">Pista automática</string>\n  <string name=\"exo_track_unknown\">Descoñecido</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Estéreo</string>\n  <string name=\"exo_track_surround\">Son envolvente</string>\n  <string name=\"exo_track_surround_5_point_1\">Son envolvente 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Son envolvente 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternativa</string>\n  <string name=\"exo_track_role_supplementary\">Complementaria</string>\n  <string name=\"exo_track_role_commentary\">Comentarios</string>\n  <string name=\"exo_track_role_closed_captions\">Subtítulos</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-gu/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">પહેલાંનો ટ્રૅક</string>\n  <string name=\"exo_controls_next_description\">આગલો ટ્રૅક</string>\n  <string name=\"exo_controls_pause_description\">થોભો</string>\n  <string name=\"exo_controls_play_description\">ચલાવો</string>\n  <string name=\"exo_controls_stop_description\">રોકો</string>\n  <string name=\"exo_controls_rewind_description\">રિવાઇન્ડ કરો</string>\n  <string name=\"exo_controls_fastforward_description\">ફાસ્ટ ફૉરવર્ડ કરો</string>\n  <string name=\"exo_controls_repeat_off_description\">કોઈ રિપીટ કરતા નહીં</string>\n  <string name=\"exo_controls_repeat_one_description\">એક રિપીટ કરો</string>\n  <string name=\"exo_controls_repeat_all_description\">બધાને રિપીટ કરો</string>\n  <string name=\"exo_controls_shuffle_on_description\">શફલ ચાલુ છે</string>\n  <string name=\"exo_controls_shuffle_off_description\">શફલ બંધ છે</string>\n  <string name=\"exo_controls_fullscreen_description\">પૂર્ણસ્ક્રીન મોડ</string>\n  <string name=\"exo_controls_vr_description\">VR મોડ</string>\n  <string name=\"exo_download_description\">ડાઉનલોડ કરો</string>\n  <string name=\"exo_download_notification_channel_name\">ડાઉનલોડ</string>\n  <string name=\"exo_download_downloading\">ડાઉનલોડ કરી રહ્યાં છીએ</string>\n  <string name=\"exo_download_completed\">ડાઉનલોડ પૂર્ણ થયું</string>\n  <string name=\"exo_download_failed\">ડાઉનલોડ નિષ્ફળ થયું</string>\n  <string name=\"exo_download_removing\">ડાઉનલોડ કાઢી નાખી રહ્યાં છીએ</string>\n  <string name=\"exo_track_selection_title_video\">વીડિઓ</string>\n  <string name=\"exo_track_selection_title_audio\">ઑડિઓ</string>\n  <string name=\"exo_track_selection_title_text\">ટેક્સ્ટ</string>\n  <string name=\"exo_track_selection_none\">એકપણ નહીં</string>\n  <string name=\"exo_track_selection_auto\">આપમેળે</string>\n  <string name=\"exo_track_unknown\">અજાણ્યો</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">મૉનો</string>\n  <string name=\"exo_track_stereo\">સ્ટીરિઓ</string>\n  <string name=\"exo_track_surround\">સરાઉન્ડ સાઉન્ડ</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 સરાઉન્ડ સાઉન્ડ</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 સરાઉન્ડ સાઉન્ડ</string>\n  <string name=\"exo_track_role_alternate\">વૈકલ્પિક</string>\n  <string name=\"exo_track_role_supplementary\">પૂરક</string>\n  <string name=\"exo_track_role_commentary\">કોમેન્ટ્રી</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-hi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">पिछला ट्रैक</string>\n  <string name=\"exo_controls_next_description\">अगला ट्रैक</string>\n  <string name=\"exo_controls_pause_description\">रोकें</string>\n  <string name=\"exo_controls_play_description\">चलाएं</string>\n  <string name=\"exo_controls_stop_description\">बंद करें</string>\n  <string name=\"exo_controls_rewind_description\">पीछे ले जाएं</string>\n  <string name=\"exo_controls_fastforward_description\">तेज़ी से आगे बढ़ाएं</string>\n  <string name=\"exo_controls_repeat_off_description\">किसी को न दोहराएं</string>\n  <string name=\"exo_controls_repeat_one_description\">एक को दोहराएं</string>\n  <string name=\"exo_controls_repeat_all_description\">सभी को दोहराएं</string>\n  <string name=\"exo_controls_shuffle_on_description\">शफ़ल करना चालू है</string>\n  <string name=\"exo_controls_shuffle_off_description\">शफ़ल करना बंद है</string>\n  <string name=\"exo_controls_fullscreen_description\">फ़ुलस्क्रीन मोड</string>\n  <string name=\"exo_controls_vr_description\">VR मोड</string>\n  <string name=\"exo_download_description\">डाउनलोड करें</string>\n  <string name=\"exo_download_notification_channel_name\">डाउनलोड की गई मीडिया फ़ाइलें</string>\n  <string name=\"exo_download_downloading\">डाउनलोड हो रहा है</string>\n  <string name=\"exo_download_completed\">डाउनलोड पूरा हुआ</string>\n  <string name=\"exo_download_failed\">डाउनलोड नहीं हो सका</string>\n  <string name=\"exo_download_removing\">डाउनलोड की गई फ़ाइलें हटाई जा रही हैं</string>\n  <string name=\"exo_track_selection_title_video\">वीडियो</string>\n  <string name=\"exo_track_selection_title_audio\">ऑडियो</string>\n  <string name=\"exo_track_selection_title_text\">लेख</string>\n  <string name=\"exo_track_selection_none\">कोई नहीं</string>\n  <string name=\"exo_track_selection_auto\">अपने आप</string>\n  <string name=\"exo_track_unknown\">अज्ञात</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">मोनो साउंड</string>\n  <string name=\"exo_track_stereo\">स्टीरियो साउंड</string>\n  <string name=\"exo_track_surround\">सराउंड साउंड</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 सराउंड साउंड</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 सराउंड साउंड</string>\n  <string name=\"exo_track_role_alternate\">विकल्प</string>\n  <string name=\"exo_track_role_supplementary\">सप्लिमेंट्री</string>\n  <string name=\"exo_track_role_commentary\">कमेंट्री</string>\n  <string name=\"exo_track_role_closed_captions\">सबटाइटल</string>\n  <string name=\"exo_track_bitrate\">%1$.2f एमबीपीएस</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-hr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Prethodni zapis</string>\n  <string name=\"exo_controls_next_description\">Sljedeći zapis</string>\n  <string name=\"exo_controls_pause_description\">Pauza</string>\n  <string name=\"exo_controls_play_description\">Reproduciraj</string>\n  <string name=\"exo_controls_stop_description\">Zaustavi</string>\n  <string name=\"exo_controls_rewind_description\">Unatrag</string>\n  <string name=\"exo_controls_fastforward_description\">Brzo unaprijed</string>\n  <string name=\"exo_controls_repeat_off_description\">Bez ponavljanja</string>\n  <string name=\"exo_controls_repeat_one_description\">Ponovi jedno</string>\n  <string name=\"exo_controls_repeat_all_description\">Ponovi sve</string>\n  <string name=\"exo_controls_shuffle_on_description\">Nasumična reproduk. uključena</string>\n  <string name=\"exo_controls_shuffle_off_description\">Nasumična reproduk. isključena</string>\n  <string name=\"exo_controls_fullscreen_description\">Prikaz na cijelom zaslonu</string>\n  <string name=\"exo_controls_vr_description\">VR način</string>\n  <string name=\"exo_download_description\">Preuzmi</string>\n  <string name=\"exo_download_notification_channel_name\">Preuzimanja</string>\n  <string name=\"exo_download_downloading\">Preuzimanje</string>\n  <string name=\"exo_download_completed\">Preuzimanje je dovršeno</string>\n  <string name=\"exo_download_failed\">Preuzimanje nije uspjelo</string>\n  <string name=\"exo_download_removing\">Uklanjanje preuzimanja</string>\n  <string name=\"exo_track_selection_title_video\">Videozapis</string>\n  <string name=\"exo_track_selection_title_audio\">Audiozapis</string>\n  <string name=\"exo_track_selection_title_text\">Tekst</string>\n  <string name=\"exo_track_selection_none\">Ništa</string>\n  <string name=\"exo_track_selection_auto\">Automatski</string>\n  <string name=\"exo_track_unknown\">Nepoznato</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Okružujući zvuk</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1-kanalni okružujući zvuk</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1-kanalni okružujući zvuk</string>\n  <string name=\"exo_track_role_alternate\">Alternativna verzija</string>\n  <string name=\"exo_track_role_supplementary\">Dopunska verzija</string>\n  <string name=\"exo_track_role_commentary\">Verzija s komentarima</string>\n  <string name=\"exo_track_role_closed_captions\">Verzija s titlovima</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-hu/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Előző szám</string>\n  <string name=\"exo_controls_next_description\">Következő szám</string>\n  <string name=\"exo_controls_pause_description\">Szüneteltetés</string>\n  <string name=\"exo_controls_play_description\">Lejátszás</string>\n  <string name=\"exo_controls_stop_description\">Leállítás</string>\n  <string name=\"exo_controls_rewind_description\">Visszatekerés</string>\n  <string name=\"exo_controls_fastforward_description\">Előretekerés</string>\n  <string name=\"exo_controls_repeat_off_description\">Nincs ismétlés</string>\n  <string name=\"exo_controls_repeat_one_description\">Egy szám ismétlése</string>\n  <string name=\"exo_controls_repeat_all_description\">Összes szám ismétlése</string>\n  <string name=\"exo_controls_shuffle_on_description\">Keverés bekapcsolva</string>\n  <string name=\"exo_controls_shuffle_off_description\">Keverés kikapcsolva</string>\n  <string name=\"exo_controls_fullscreen_description\">Teljes képernyős mód</string>\n  <string name=\"exo_controls_vr_description\">VR-mód</string>\n  <string name=\"exo_download_description\">Letöltés</string>\n  <string name=\"exo_download_notification_channel_name\">Letöltések</string>\n  <string name=\"exo_download_downloading\">Letöltés folyamatban</string>\n  <string name=\"exo_download_completed\">A letöltés befejeződött</string>\n  <string name=\"exo_download_failed\">Nem sikerült a letöltés</string>\n  <string name=\"exo_download_removing\">Letöltések törlése folyamatban</string>\n  <string name=\"exo_track_selection_title_video\">Videó</string>\n  <string name=\"exo_track_selection_title_audio\">Hang</string>\n  <string name=\"exo_track_selection_title_text\">Szöveg</string>\n  <string name=\"exo_track_selection_none\">Nincs</string>\n  <string name=\"exo_track_selection_auto\">Automatikus</string>\n  <string name=\"exo_track_unknown\">Ismeretlen</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Monó</string>\n  <string name=\"exo_track_stereo\">Sztereó</string>\n  <string name=\"exo_track_surround\">Térhatású hangzás</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1-es térhatású hangzás</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1-es térhatású hangzás</string>\n  <string name=\"exo_track_role_alternate\">Alternatív</string>\n  <string name=\"exo_track_role_supplementary\">Kiegészítő</string>\n  <string name=\"exo_track_role_commentary\">Kommentár</string>\n  <string name=\"exo_track_role_closed_captions\">Felirat</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-hy/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Նախորդը</string>\n  <string name=\"exo_controls_next_description\">Հաջորդը</string>\n  <string name=\"exo_controls_pause_description\">Դադարեցնել</string>\n  <string name=\"exo_controls_play_description\">Նվագարկել</string>\n  <string name=\"exo_controls_stop_description\">Կանգնեցնել</string>\n  <string name=\"exo_controls_rewind_description\">Հետ</string>\n  <string name=\"exo_controls_fastforward_description\">Առաջ</string>\n  <string name=\"exo_controls_repeat_off_description\">Չկրկնել</string>\n  <string name=\"exo_controls_repeat_one_description\">Կրկնել մեկը</string>\n  <string name=\"exo_controls_repeat_all_description\">Կրկնել բոլորը</string>\n  <string name=\"exo_controls_shuffle_on_description\">Խառնումը միացված է</string>\n  <string name=\"exo_controls_shuffle_off_description\">Խառնումն անջատված է</string>\n  <string name=\"exo_controls_fullscreen_description\">Լիաէկրան ռեժիմ</string>\n  <string name=\"exo_controls_vr_description\">VR ռեժիմ</string>\n  <string name=\"exo_download_description\">Ներբեռնել</string>\n  <string name=\"exo_download_notification_channel_name\">Ներբեռնումներ</string>\n  <string name=\"exo_download_downloading\">Ներբեռնում</string>\n  <string name=\"exo_download_completed\">Ներբեռնումն ավարտվեց</string>\n  <string name=\"exo_download_failed\">Չհաջողվեց ներբեռնել</string>\n  <string name=\"exo_download_removing\">Ներբեռնումները հեռացվում են</string>\n  <string name=\"exo_track_selection_title_video\">Տեսանյութ</string>\n  <string name=\"exo_track_selection_title_audio\">Աուդիո</string>\n  <string name=\"exo_track_selection_title_text\">Տեքստ</string>\n  <string name=\"exo_track_selection_none\">Ոչ մեկը</string>\n  <string name=\"exo_track_selection_auto\">Ավտոմատ</string>\n  <string name=\"exo_track_unknown\">Անհայտ</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Մոնո</string>\n  <string name=\"exo_track_stereo\">Ստերեո</string>\n  <string name=\"exo_track_surround\">Ծավալային ձայն</string>\n  <string name=\"exo_track_surround_5_point_1\">5․1 ծավալային ձայն</string>\n  <string name=\"exo_track_surround_7_point_1\">7․1 ծավալային ձայն</string>\n  <string name=\"exo_track_role_alternate\">Այլընտրանքային</string>\n  <string name=\"exo_track_role_supplementary\">Լրացուցիչ</string>\n  <string name=\"exo_track_role_commentary\">Մեկնաբանություններ</string>\n  <string name=\"exo_track_role_closed_captions\">Ենթագրեր</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Մբիթ/վ</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-in/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Lagu sebelumnya</string>\n  <string name=\"exo_controls_next_description\">Lagu berikutnya</string>\n  <string name=\"exo_controls_pause_description\">Jeda</string>\n  <string name=\"exo_controls_play_description\">Putar</string>\n  <string name=\"exo_controls_stop_description\">Berhenti</string>\n  <string name=\"exo_controls_rewind_description\">Putar Ulang</string>\n  <string name=\"exo_controls_fastforward_description\">Maju cepat</string>\n  <string name=\"exo_controls_repeat_off_description\">Jangan ulangi</string>\n  <string name=\"exo_controls_repeat_one_description\">Ulangi 1</string>\n  <string name=\"exo_controls_repeat_all_description\">Ulangi semua</string>\n  <string name=\"exo_controls_shuffle_on_description\">Acak aktif</string>\n  <string name=\"exo_controls_shuffle_off_description\">Acak tidak aktif</string>\n  <string name=\"exo_controls_fullscreen_description\">Mode layar penuh</string>\n  <string name=\"exo_controls_vr_description\">Mode VR</string>\n  <string name=\"exo_download_description\">Download</string>\n  <string name=\"exo_download_notification_channel_name\">Download</string>\n  <string name=\"exo_download_downloading\">Mendownload</string>\n  <string name=\"exo_download_completed\">Download selesai</string>\n  <string name=\"exo_download_failed\">Download gagal</string>\n  <string name=\"exo_download_removing\">Menghapus download</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Teks</string>\n  <string name=\"exo_track_selection_none\">Tidak ada</string>\n  <string name=\"exo_track_selection_auto\">Otomatis</string>\n  <string name=\"exo_track_unknown\">Tidak diketahui</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Suara surround</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 surround sound</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 surround sound</string>\n  <string name=\"exo_track_role_alternate\">Alternatif</string>\n  <string name=\"exo_track_role_supplementary\">Tambahan</string>\n  <string name=\"exo_track_role_commentary\">Komentar</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-is/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Fyrra lag</string>\n  <string name=\"exo_controls_next_description\">Næsta lag</string>\n  <string name=\"exo_controls_pause_description\">Hlé</string>\n  <string name=\"exo_controls_play_description\">Spila</string>\n  <string name=\"exo_controls_stop_description\">Stöðva</string>\n  <string name=\"exo_controls_rewind_description\">Spóla til baka</string>\n  <string name=\"exo_controls_fastforward_description\">Spóla áfram</string>\n  <string name=\"exo_controls_repeat_off_description\">Endurtaka ekkert</string>\n  <string name=\"exo_controls_repeat_one_description\">Endurtaka eitt</string>\n  <string name=\"exo_controls_repeat_all_description\">Endurtaka allt</string>\n  <string name=\"exo_controls_shuffle_on_description\">Kveikt á stokkun</string>\n  <string name=\"exo_controls_shuffle_off_description\">Slökkt á stokkun</string>\n  <string name=\"exo_controls_fullscreen_description\">Allur skjárinn</string>\n  <string name=\"exo_controls_vr_description\">sýndarveruleikastilling</string>\n  <string name=\"exo_download_description\">Sækja</string>\n  <string name=\"exo_download_notification_channel_name\">Niðurhal</string>\n  <string name=\"exo_download_downloading\">Sækir</string>\n  <string name=\"exo_download_completed\">Niðurhali lokið</string>\n  <string name=\"exo_download_failed\">Niðurhal mistókst</string>\n  <string name=\"exo_download_removing\">Fjarlægir niðurhal</string>\n  <string name=\"exo_track_selection_title_video\">Myndskeið</string>\n  <string name=\"exo_track_selection_title_audio\">Hljóð</string>\n  <string name=\"exo_track_selection_title_text\">Texti</string>\n  <string name=\"exo_track_selection_none\">Ekkert</string>\n  <string name=\"exo_track_selection_auto\">Sjálfvirkt</string>\n  <string name=\"exo_track_unknown\">Óþekkt</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Einóma</string>\n  <string name=\"exo_track_stereo\">Víðóma</string>\n  <string name=\"exo_track_surround\">Víðóma hljóð</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 víðóma hljóð</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 víðóma hljóð</string>\n  <string name=\"exo_track_role_alternate\">Annað</string>\n  <string name=\"exo_track_role_supplementary\">Auka</string>\n  <string name=\"exo_track_role_commentary\">Lýsingar</string>\n  <string name=\"exo_track_role_closed_captions\">Skjátextar</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mb/sek.</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-it/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Traccia precedente</string>\n  <string name=\"exo_controls_next_description\">Traccia successiva</string>\n  <string name=\"exo_controls_pause_description\">Pausa</string>\n  <string name=\"exo_controls_play_description\">Riproduci</string>\n  <string name=\"exo_controls_stop_description\">Interrompi</string>\n  <string name=\"exo_controls_rewind_description\">Riavvolgi</string>\n  <string name=\"exo_controls_fastforward_description\">Avanti veloce</string>\n  <string name=\"exo_controls_repeat_off_description\">Non ripetere nulla</string>\n  <string name=\"exo_controls_repeat_one_description\">Ripeti uno</string>\n  <string name=\"exo_controls_repeat_all_description\">Ripeti tutto</string>\n  <string name=\"exo_controls_shuffle_on_description\">Attiva riproduzione casuale</string>\n  <string name=\"exo_controls_shuffle_off_description\">Disattiva riproduzione casuale</string>\n  <string name=\"exo_controls_fullscreen_description\">Modalità a schermo intero</string>\n  <string name=\"exo_controls_vr_description\">Modalità VR</string>\n  <string name=\"exo_download_description\">Scarica</string>\n  <string name=\"exo_download_notification_channel_name\">Download</string>\n  <string name=\"exo_download_downloading\">Download…</string>\n  <string name=\"exo_download_completed\">Download completato</string>\n  <string name=\"exo_download_failed\">Download non riuscito</string>\n  <string name=\"exo_download_removing\">Rimozione dei download…</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Testo</string>\n  <string name=\"exo_track_selection_none\">Nessuno</string>\n  <string name=\"exo_track_selection_auto\">Auto</string>\n  <string name=\"exo_track_unknown\">Sconosciuta</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Audio surround</string>\n  <string name=\"exo_track_surround_5_point_1\">Audio surround 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Audio surround 7.1</string>\n  <string name=\"exo_track_role_alternate\">Versione alternativa</string>\n  <string name=\"exo_track_role_supplementary\">Contenuti supplementari</string>\n  <string name=\"exo_track_role_commentary\">Commenti</string>\n  <string name=\"exo_track_role_closed_captions\">Sottotitoli</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-iw/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">הרצועה הקודמת</string>\n  <string name=\"exo_controls_next_description\">הרצועה הבאה</string>\n  <string name=\"exo_controls_pause_description\">השהיה</string>\n  <string name=\"exo_controls_play_description\">הפעלה</string>\n  <string name=\"exo_controls_stop_description\">הפסקה</string>\n  <string name=\"exo_controls_rewind_description\">הרצה אחורה</string>\n  <string name=\"exo_controls_fastforward_description\">הרצה קדימה</string>\n  <string name=\"exo_controls_repeat_off_description\">אל תחזור על אף פריט</string>\n  <string name=\"exo_controls_repeat_one_description\">חזור על פריט אחד</string>\n  <string name=\"exo_controls_repeat_all_description\">חזור על הכול</string>\n  <string name=\"exo_controls_shuffle_on_description\">ההשמעה האקראית מופעלת</string>\n  <string name=\"exo_controls_shuffle_off_description\">ההשמעה האקראית מושבתת</string>\n  <string name=\"exo_controls_fullscreen_description\">מצב מסך מלא</string>\n  <string name=\"exo_controls_vr_description\">מצב VR</string>\n  <string name=\"exo_download_description\">הורדה</string>\n  <string name=\"exo_download_notification_channel_name\">הורדות</string>\n  <string name=\"exo_download_downloading\">ההורדה מתבצעת</string>\n  <string name=\"exo_download_completed\">ההורדה הושלמה</string>\n  <string name=\"exo_download_failed\">ההורדה לא הושלמה</string>\n  <string name=\"exo_download_removing\">מסיר הורדות</string>\n  <string name=\"exo_track_selection_title_video\">סרטון</string>\n  <string name=\"exo_track_selection_title_audio\">אודיו</string>\n  <string name=\"exo_track_selection_title_text\">טקסט</string>\n  <string name=\"exo_track_selection_none\">ללא</string>\n  <string name=\"exo_track_selection_auto\">אוטומטי</string>\n  <string name=\"exo_track_unknown\">לא ידוע</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">מונו</string>\n  <string name=\"exo_track_stereo\">סטריאו</string>\n  <string name=\"exo_track_surround\">סראונד</string>\n  <string name=\"exo_track_surround_5_point_1\">סראונד 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">סראונד 7.1</string>\n  <string name=\"exo_track_role_alternate\">חלופי</string>\n  <string name=\"exo_track_role_supplementary\">משלים</string>\n  <string name=\"exo_track_role_commentary\">פרשנות</string>\n  <string name=\"exo_track_role_closed_captions\">כתוביות</string>\n  <string name=\"exo_track_bitrate\">%1$.2f מגה סיביות לשנייה</string>\n  <string name=\"exo_item_list\">%1$s‏, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-ja/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">前のトラック</string>\n  <string name=\"exo_controls_next_description\">次のトラック</string>\n  <string name=\"exo_controls_pause_description\">一時停止</string>\n  <string name=\"exo_controls_play_description\">再生</string>\n  <string name=\"exo_controls_stop_description\">停止</string>\n  <string name=\"exo_controls_rewind_description\">巻き戻し</string>\n  <string name=\"exo_controls_fastforward_description\">早送り</string>\n  <string name=\"exo_controls_repeat_off_description\">リピートなし</string>\n  <string name=\"exo_controls_repeat_one_description\">1 曲をリピート</string>\n  <string name=\"exo_controls_repeat_all_description\">全曲をリピート</string>\n  <string name=\"exo_controls_shuffle_on_description\">シャッフル ON</string>\n  <string name=\"exo_controls_shuffle_off_description\">シャッフル OFF</string>\n  <string name=\"exo_controls_fullscreen_description\">全画面モード</string>\n  <string name=\"exo_controls_vr_description\">VR モード</string>\n  <string name=\"exo_download_description\">ダウンロード</string>\n  <string name=\"exo_download_notification_channel_name\">ダウンロード</string>\n  <string name=\"exo_download_downloading\">ダウンロードしています</string>\n  <string name=\"exo_download_completed\">ダウンロードが完了しました</string>\n  <string name=\"exo_download_failed\">ダウンロードに失敗しました</string>\n  <string name=\"exo_download_removing\">ダウンロードを削除しています</string>\n  <string name=\"exo_track_selection_title_video\">動画</string>\n  <string name=\"exo_track_selection_title_audio\">音声</string>\n  <string name=\"exo_track_selection_title_text\">文字</string>\n  <string name=\"exo_track_selection_none\">なし</string>\n  <string name=\"exo_track_selection_auto\">自動</string>\n  <string name=\"exo_track_unknown\">不明</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">モノラル</string>\n  <string name=\"exo_track_stereo\">ステレオ</string>\n  <string name=\"exo_track_surround\">サラウンド サウンド</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 サラウンド サウンド</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 サラウンド サウンド</string>\n  <string name=\"exo_track_role_alternate\">代替</string>\n  <string name=\"exo_track_role_supplementary\">補足</string>\n  <string name=\"exo_track_role_commentary\">解説</string>\n  <string name=\"exo_track_role_closed_captions\">字幕</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s、%2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-ka/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">წინა ჩანაწერი</string>\n  <string name=\"exo_controls_next_description\">შემდეგი ჩანაწერი</string>\n  <string name=\"exo_controls_pause_description\">პაუზა</string>\n  <string name=\"exo_controls_play_description\">დაკვრა</string>\n  <string name=\"exo_controls_stop_description\">შეწყვეტა</string>\n  <string name=\"exo_controls_rewind_description\">უკან გადახვევა</string>\n  <string name=\"exo_controls_fastforward_description\">წინ გადახვევა</string>\n  <string name=\"exo_controls_repeat_off_description\">არცერთის გამეორება</string>\n  <string name=\"exo_controls_repeat_one_description\">ერთის გამეორება</string>\n  <string name=\"exo_controls_repeat_all_description\">ყველას გამეორება</string>\n  <string name=\"exo_controls_shuffle_on_description\">არეულად დაკვრა ჩართულია</string>\n  <string name=\"exo_controls_shuffle_off_description\">არეულად დაკვრა გამორთულია</string>\n  <string name=\"exo_controls_fullscreen_description\">სრულეკრანიანი რეჟიმი</string>\n  <string name=\"exo_controls_vr_description\">VR რეჟიმი</string>\n  <string name=\"exo_download_description\">ჩამოტვირთვა</string>\n  <string name=\"exo_download_notification_channel_name\">ჩამოტვირთვები</string>\n  <string name=\"exo_download_downloading\">მიმდინარეობს ჩამოტვირთვა</string>\n  <string name=\"exo_download_completed\">ჩამოტვირთვა დასრულდა</string>\n  <string name=\"exo_download_failed\">ჩამოტვირთვა ვერ მოხერხდა</string>\n  <string name=\"exo_download_removing\">მიმდინარეობს ჩამოტვირთვების ამოშლა</string>\n  <string name=\"exo_track_selection_title_video\">ვიდეო</string>\n  <string name=\"exo_track_selection_title_audio\">აუდიო</string>\n  <string name=\"exo_track_selection_title_text\">ტექსტი</string>\n  <string name=\"exo_track_selection_none\">არცერთი</string>\n  <string name=\"exo_track_selection_auto\">ავტომატური</string>\n  <string name=\"exo_track_unknown\">უცნობი</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">მონო</string>\n  <string name=\"exo_track_stereo\">სტერეო</string>\n  <string name=\"exo_track_surround\">მოცულობითი ხმა</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 მოცულობითი ხმა</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 მოცულობითი ხმა</string>\n  <string name=\"exo_track_role_alternate\">ალტერნატიული</string>\n  <string name=\"exo_track_role_supplementary\">დამატებითი</string>\n  <string name=\"exo_track_role_commentary\">კომენტარი</string>\n  <string name=\"exo_track_role_closed_captions\">დახურული სუბტიტრები</string>\n  <string name=\"exo_track_bitrate\">%1$.2f მბიტ/წმ</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-kk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Алдыңғы аудиотрек</string>\n  <string name=\"exo_controls_next_description\">Келесі аудиотрек</string>\n  <string name=\"exo_controls_pause_description\">Кідірту</string>\n  <string name=\"exo_controls_play_description\">Ойнату</string>\n  <string name=\"exo_controls_stop_description\">Тоқтату</string>\n  <string name=\"exo_controls_rewind_description\">Артқа айналдыру</string>\n  <string name=\"exo_controls_fastforward_description\">Жылдам алға айналдыру</string>\n  <string name=\"exo_controls_repeat_off_description\">Ешқайсысын қайталамау</string>\n  <string name=\"exo_controls_repeat_one_description\">Біреуін қайталау</string>\n  <string name=\"exo_controls_repeat_all_description\">Барлығын қайталау</string>\n  <string name=\"exo_controls_shuffle_on_description\">Араластыру режимі қосулы.</string>\n  <string name=\"exo_controls_shuffle_off_description\">Араластыру режимі өшірулі.</string>\n  <string name=\"exo_controls_fullscreen_description\">Толық экран режимі</string>\n  <string name=\"exo_controls_vr_description\">VR режимі</string>\n  <string name=\"exo_download_description\">Жүктеп алу</string>\n  <string name=\"exo_download_notification_channel_name\">Жүктеп алынғандар</string>\n  <string name=\"exo_download_downloading\">Жүктеп алынуда</string>\n  <string name=\"exo_download_completed\">Жүктеп алынды</string>\n  <string name=\"exo_download_failed\">Жүктеп алынбады</string>\n  <string name=\"exo_download_removing\">Жүктеп алынғандар өшірілуде</string>\n  <string name=\"exo_track_selection_title_video\">Бейне</string>\n  <string name=\"exo_track_selection_title_audio\">Aудиомазмұн</string>\n  <string name=\"exo_track_selection_title_text\">Мәтін</string>\n  <string name=\"exo_track_selection_none\">Ешқайсысы</string>\n  <string name=\"exo_track_selection_auto\">Автоматты</string>\n  <string name=\"exo_track_unknown\">Белгісіз</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Моно</string>\n  <string name=\"exo_track_stereo\">Стерео</string>\n  <string name=\"exo_track_surround\">Көлемді дыбыс</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 көлемді дыбыс жүйесі</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 көлемді дыбыс жүйесі</string>\n  <string name=\"exo_track_role_alternate\">Балама</string>\n  <string name=\"exo_track_role_supplementary\">Қосымша</string>\n  <string name=\"exo_track_role_commentary\">Түсіндірме</string>\n  <string name=\"exo_track_role_closed_captions\">Субтитр</string>\n  <string name=\"exo_track_bitrate\">%1$.2f МБ/сек</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-km/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">សំនៀង​​មុន</string>\n  <string name=\"exo_controls_next_description\">សំនៀង​បន្ទាប់</string>\n  <string name=\"exo_controls_pause_description\">ផ្អាក</string>\n  <string name=\"exo_controls_play_description\">លេង</string>\n  <string name=\"exo_controls_stop_description\">ឈប់</string>\n  <string name=\"exo_controls_rewind_description\">ខា​ថយ​ក្រោយ</string>\n  <string name=\"exo_controls_fastforward_description\">ទៅ​មុខ​​​រហ័ស</string>\n  <string name=\"exo_controls_repeat_off_description\">មិន​លេង​ឡើងវិញ</string>\n  <string name=\"exo_controls_repeat_one_description\">លេង​ឡើង​វិញ​ម្ដង</string>\n  <string name=\"exo_controls_repeat_all_description\">លេង​ឡើងវិញ​ទាំងអស់</string>\n  <string name=\"exo_controls_shuffle_on_description\">បើក​ការ​ច្របល់</string>\n  <string name=\"exo_controls_shuffle_off_description\">បិទ​ការ​ច្របល់</string>\n  <string name=\"exo_controls_fullscreen_description\">មុខងារពេញ​អេក្រង់</string>\n  <string name=\"exo_controls_vr_description\">មុខងារ VR</string>\n  <string name=\"exo_download_description\">ទាញយក</string>\n  <string name=\"exo_download_notification_channel_name\">ទាញយក</string>\n  <string name=\"exo_download_downloading\">កំពុង​ទាញ​យក</string>\n  <string name=\"exo_download_completed\">បាន​បញ្ចប់​ការទាញយក</string>\n  <string name=\"exo_download_failed\">មិន​អាច​ទាញយក​បាន​ទេ</string>\n  <string name=\"exo_download_removing\">កំពុង​លុប​ការទាញយក</string>\n  <string name=\"exo_track_selection_title_video\">វីដេអូ</string>\n  <string name=\"exo_track_selection_title_audio\">សំឡេង</string>\n  <string name=\"exo_track_selection_title_text\">អក្សរ</string>\n  <string name=\"exo_track_selection_none\">គ្មាន</string>\n  <string name=\"exo_track_selection_auto\">ស្វ័យប្រវត្តិ</string>\n  <string name=\"exo_track_unknown\">មិនស្គាល់</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">ម៉ូ​ណូ</string>\n  <string name=\"exo_track_stereo\">ស្តេរ៉េអូ</string>\n  <string name=\"exo_track_surround\">សំឡេង​រងំ</string>\n  <string name=\"exo_track_surround_5_point_1\">សំឡេង​រងំ​ខ្នាត 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">សំឡេង​រងំ​ខ្នាត 7.1</string>\n  <string name=\"exo_track_role_alternate\">ជំនួស</string>\n  <string name=\"exo_track_role_supplementary\">បំពេញបន្ថែម</string>\n  <string name=\"exo_track_role_commentary\">ការអត្ថាធិប្បាយ</string>\n  <string name=\"exo_track_role_closed_captions\">អក្សររត់</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-kn/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">ಹಿಂದಿನ ಟ್ರ್ಯಾಕ್</string>\n  <string name=\"exo_controls_next_description\">ಮುಂದಿನ ಟ್ರ್ಯಾಕ್</string>\n  <string name=\"exo_controls_pause_description\">ವಿರಾಮ</string>\n  <string name=\"exo_controls_play_description\">ಪ್ಲೇ</string>\n  <string name=\"exo_controls_stop_description\">ನಿಲ್ಲಿಸಿ</string>\n  <string name=\"exo_controls_rewind_description\">ರಿವೈಂಡ್</string>\n  <string name=\"exo_controls_fastforward_description\">ಫಾಸ್ಟ್ ಫಾರ್ವರ್ಡ್</string>\n  <string name=\"exo_controls_repeat_off_description\">ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ</string>\n  <string name=\"exo_controls_repeat_one_description\">ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ</string>\n  <string name=\"exo_controls_repeat_all_description\">ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ</string>\n  <string name=\"exo_controls_shuffle_on_description\">ಶಫಲ್ ಆನ್ ಆಗಿದೆ</string>\n  <string name=\"exo_controls_shuffle_off_description\">ಶಫಲ್ ಆಫ್ ಆಗಿದೆ</string>\n  <string name=\"exo_controls_fullscreen_description\">ಪೂರ್ಣ ಪರದೆ ಮೋಡ್</string>\n  <string name=\"exo_controls_vr_description\">VR ಮೋಡ್</string>\n  <string name=\"exo_download_description\">ಡೌನ್‌ಲೋಡ್‌</string>\n  <string name=\"exo_download_notification_channel_name\">ಡೌನ್‌ಲೋಡ್‌ಗಳು</string>\n  <string name=\"exo_download_downloading\">ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ</string>\n  <string name=\"exo_download_completed\">ಡೌನ್‌ಲೋಡ್ ಪೂರ್ಣಗೊಂಡಿದೆ</string>\n  <string name=\"exo_download_failed\">ಡೌನ್‌ಲೋಡ್‌ ವಿಫಲಗೊಂಡಿದೆ</string>\n  <string name=\"exo_download_removing\">ಡೌನ್ಲೋಡ್‌ಗಳನ್ನು ತೆಗೆದುಹಾಕಲಾಗುತ್ತಿದೆ</string>\n  <string name=\"exo_track_selection_title_video\">ವೀಡಿಯೊ</string>\n  <string name=\"exo_track_selection_title_audio\">ಆಡಿಯೊ</string>\n  <string name=\"exo_track_selection_title_text\">ಪಠ್ಯ</string>\n  <string name=\"exo_track_selection_none\">ಯಾವುದೂ ಅಲ್ಲ</string>\n  <string name=\"exo_track_selection_auto\">ಸ್ವಯಂ</string>\n  <string name=\"exo_track_unknown\">ಅಪರಿಚಿತ</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">ಮೊನೊ</string>\n  <string name=\"exo_track_stereo\">ಸ್ಟೀರಿಯೊ</string>\n  <string name=\"exo_track_surround\">ಸರೌಂಡ್ ಶಬ್ದ</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 ಸರೌಂಡ್ ಶಬ್ದ</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 ಸರೌಂಡ್ ಶಬ್ದ</string>\n  <string name=\"exo_track_role_alternate\">ಪರ್ಯಾಯ</string>\n  <string name=\"exo_track_role_supplementary\">ಪೂರಕ</string>\n  <string name=\"exo_track_role_commentary\">ವ್ಯಾಖ್ಯಾನ</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-ko/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">이전 트랙</string>\n  <string name=\"exo_controls_next_description\">다음 트랙</string>\n  <string name=\"exo_controls_pause_description\">일시중지</string>\n  <string name=\"exo_controls_play_description\">재생</string>\n  <string name=\"exo_controls_stop_description\">중지</string>\n  <string name=\"exo_controls_rewind_description\">되감기</string>\n  <string name=\"exo_controls_fastforward_description\">빨리 감기</string>\n  <string name=\"exo_controls_repeat_off_description\">반복 안함</string>\n  <string name=\"exo_controls_repeat_one_description\">현재 미디어 반복</string>\n  <string name=\"exo_controls_repeat_all_description\">모두 반복</string>\n  <string name=\"exo_controls_shuffle_on_description\">셔플 사용</string>\n  <string name=\"exo_controls_shuffle_off_description\">셔플 사용 안함</string>\n  <string name=\"exo_controls_fullscreen_description\">전체화면 모드</string>\n  <string name=\"exo_controls_vr_description\">가상 현실 모드</string>\n  <string name=\"exo_download_description\">다운로드</string>\n  <string name=\"exo_download_notification_channel_name\">다운로드</string>\n  <string name=\"exo_download_downloading\">다운로드 중</string>\n  <string name=\"exo_download_completed\">다운로드 완료</string>\n  <string name=\"exo_download_failed\">다운로드 실패</string>\n  <string name=\"exo_download_removing\">다운로드 항목 삭제 중</string>\n  <string name=\"exo_track_selection_title_video\">동영상</string>\n  <string name=\"exo_track_selection_title_audio\">오디오</string>\n  <string name=\"exo_track_selection_title_text\">문자 메시지</string>\n  <string name=\"exo_track_selection_none\">없음</string>\n  <string name=\"exo_track_selection_auto\">자동</string>\n  <string name=\"exo_track_unknown\">알 수 없음</string>\n  <string name=\"exo_track_resolution\">%1$d×%2$d</string>\n  <string name=\"exo_track_mono\">모노</string>\n  <string name=\"exo_track_stereo\">스테레오</string>\n  <string name=\"exo_track_surround\">서라운드 사운드</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 서라운드 사운드</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 서라운드 사운드</string>\n  <string name=\"exo_track_role_alternate\">대체</string>\n  <string name=\"exo_track_role_supplementary\">추가</string>\n  <string name=\"exo_track_role_commentary\">논평</string>\n  <string name=\"exo_track_role_closed_captions\">자막</string>\n  <string name=\"exo_track_bitrate\">%1$.2fMbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-ky/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Мурунку трек</string>\n  <string name=\"exo_controls_next_description\">Кийинки трек</string>\n  <string name=\"exo_controls_pause_description\">Тындыруу</string>\n  <string name=\"exo_controls_play_description\">Ойнотуу</string>\n  <string name=\"exo_controls_stop_description\">Токтотуу</string>\n  <string name=\"exo_controls_rewind_description\">Артка түрүү</string>\n  <string name=\"exo_controls_fastforward_description\">Алдыга түрүү</string>\n  <string name=\"exo_controls_repeat_off_description\">Кайталанбасын</string>\n  <string name=\"exo_controls_repeat_one_description\">Бирөөнү кайталоо</string>\n  <string name=\"exo_controls_repeat_all_description\">Баарын кайталоо</string>\n  <string name=\"exo_controls_shuffle_on_description\">Аралаштыруу күйүк</string>\n  <string name=\"exo_controls_shuffle_off_description\">Аралаштыруу өчүк</string>\n  <string name=\"exo_controls_fullscreen_description\">Толук экран режими</string>\n  <string name=\"exo_controls_vr_description\">VR режими</string>\n  <string name=\"exo_download_description\">Жүктөп алуу</string>\n  <string name=\"exo_download_notification_channel_name\">Жүктөлүп алынгандар</string>\n  <string name=\"exo_download_downloading\">Жүктөлүп алынууда</string>\n  <string name=\"exo_download_completed\">Жүктөп алуу аяктады</string>\n  <string name=\"exo_download_failed\">Жүктөлүп алынбай калды</string>\n  <string name=\"exo_download_removing\">Жүктөлүп алынгандар өчүрүлүүдө</string>\n  <string name=\"exo_track_selection_title_video\">Видео</string>\n  <string name=\"exo_track_selection_title_audio\">Аудио</string>\n  <string name=\"exo_track_selection_title_text\">Текст</string>\n  <string name=\"exo_track_selection_none\">Жок</string>\n  <string name=\"exo_track_selection_auto\">Авто</string>\n  <string name=\"exo_track_unknown\">Белгисиз</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Моно</string>\n  <string name=\"exo_track_stereo\">Стерео</string>\n  <string name=\"exo_track_surround\">Көлөмдүү добуш</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 көлөмдүү добуш</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 көлөмдүү добуш</string>\n  <string name=\"exo_track_role_alternate\">Альтернативдүү трек</string>\n  <string name=\"exo_track_role_supplementary\">Кошумча трек</string>\n  <string name=\"exo_track_role_commentary\">Пикир</string>\n  <string name=\"exo_track_role_closed_captions\">Коштомо жазуулар</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Мб/сек.</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-lo/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">ເພງກ່ອນໜ້າ</string>\n  <string name=\"exo_controls_next_description\">ເພງຕໍ່ໄປ</string>\n  <string name=\"exo_controls_pause_description\">ຢຸດຊົ່ວຄາວ</string>\n  <string name=\"exo_controls_play_description\">ຫຼິ້ນ</string>\n  <string name=\"exo_controls_stop_description\">ຢຸດ</string>\n  <string name=\"exo_controls_rewind_description\">ຍ້ອນກັບ</string>\n  <string name=\"exo_controls_fastforward_description\">ເລື່ອນໄປໜ້າ</string>\n  <string name=\"exo_controls_repeat_off_description\">ບໍ່ຫຼິ້ນຊ້ຳ</string>\n  <string name=\"exo_controls_repeat_one_description\">ຫຼິ້ນຊໍ້າ</string>\n  <string name=\"exo_controls_repeat_all_description\">ຫຼິ້ນຊ້ຳທັງໝົດ</string>\n  <string name=\"exo_controls_shuffle_on_description\">ເປີດການສຸ່ມເພງແລ້ວ</string>\n  <string name=\"exo_controls_shuffle_off_description\">ປິດການສຸ່ມເພງແລ້ວ</string>\n  <string name=\"exo_controls_fullscreen_description\">ໂໝດເຕັມຈໍ</string>\n  <string name=\"exo_controls_vr_description\">ໂໝດ VR</string>\n  <string name=\"exo_download_description\">ດາວໂຫລດ</string>\n  <string name=\"exo_download_notification_channel_name\">ດາວໂຫລດ</string>\n  <string name=\"exo_download_downloading\">ກຳລັງດາວໂຫລດ</string>\n  <string name=\"exo_download_completed\">ດາວໂຫລດສຳເລັດແລ້ວ</string>\n  <string name=\"exo_download_failed\">ດາວໂຫຼດບໍ່ສຳເລັດ</string>\n  <string name=\"exo_download_removing\">ກຳລັງລຶບການດາວໂຫລດອອກ</string>\n  <string name=\"exo_track_selection_title_video\">ວິດີໂອ</string>\n  <string name=\"exo_track_selection_title_audio\">ສຽງ</string>\n  <string name=\"exo_track_selection_title_text\">ຂໍ້ຄວາມ</string>\n  <string name=\"exo_track_selection_none\">ບໍ່ມີ</string>\n  <string name=\"exo_track_selection_auto\">ອັດຕະໂນມັດ</string>\n  <string name=\"exo_track_unknown\">ບໍ່ຮູ້ຈັກ</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">ໂມໂນ</string>\n  <string name=\"exo_track_stereo\">ສະເຕຣິໂອ</string>\n  <string name=\"exo_track_surround\">ສຽງຮອບທິດທາງ</string>\n  <string name=\"exo_track_surround_5_point_1\">ສຽງຮອບທິດທາງ 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">ສຽງຮອບທິດທາງ 7.1</string>\n  <string name=\"exo_track_role_alternate\">ສຳຮອງ</string>\n  <string name=\"exo_track_role_supplementary\">ສ່ວນເສີມ</string>\n  <string name=\"exo_track_role_commentary\">ຄຳເຫັນ</string>\n  <string name=\"exo_track_role_closed_captions\">ຄຳບັນຍາຍ</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-lt/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Ankstesnis takelis</string>\n  <string name=\"exo_controls_next_description\">Kitas takelis</string>\n  <string name=\"exo_controls_pause_description\">Pristabdyti</string>\n  <string name=\"exo_controls_play_description\">Leisti</string>\n  <string name=\"exo_controls_stop_description\">Sustabdyti</string>\n  <string name=\"exo_controls_rewind_description\">Sukti atgal</string>\n  <string name=\"exo_controls_fastforward_description\">Sukti pirmyn</string>\n  <string name=\"exo_controls_repeat_off_description\">Nekartoti nieko</string>\n  <string name=\"exo_controls_repeat_one_description\">Kartoti vieną</string>\n  <string name=\"exo_controls_repeat_all_description\">Kartoti viską</string>\n  <string name=\"exo_controls_shuffle_on_description\">Maišymas įjungtas</string>\n  <string name=\"exo_controls_shuffle_off_description\">Maišymas išjungtas</string>\n  <string name=\"exo_controls_fullscreen_description\">Viso ekrano režimas</string>\n  <string name=\"exo_controls_vr_description\">VR režimas</string>\n  <string name=\"exo_download_description\">Atsisiųsti</string>\n  <string name=\"exo_download_notification_channel_name\">Atsisiuntimai</string>\n  <string name=\"exo_download_downloading\">Atsisiunčiama</string>\n  <string name=\"exo_download_completed\">Atsisiuntimo procesas baigtas</string>\n  <string name=\"exo_download_failed\">Nepavyko atsisiųsti</string>\n  <string name=\"exo_download_removing\">Pašalinami atsisiuntimai</string>\n  <string name=\"exo_track_selection_title_video\">Vaizdo įrašas</string>\n  <string name=\"exo_track_selection_title_audio\">Garso įrašas</string>\n  <string name=\"exo_track_selection_title_text\">Tekstas</string>\n  <string name=\"exo_track_selection_none\">Nėra</string>\n  <string name=\"exo_track_selection_auto\">Automatinė</string>\n  <string name=\"exo_track_unknown\">Nežinomas</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Monofoninis</string>\n  <string name=\"exo_track_stereo\">Stereofoninis</string>\n  <string name=\"exo_track_surround\">Erdvinis garsas</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 erdvinis garsas</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 erdvinis garsas</string>\n  <string name=\"exo_track_role_alternate\">Alternatyvus</string>\n  <string name=\"exo_track_role_supplementary\">Papildomas</string>\n  <string name=\"exo_track_role_commentary\">Komentaras</string>\n  <string name=\"exo_track_role_closed_captions\">Subtitrai</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mb/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-lv/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Iepriekšējais ieraksts</string>\n  <string name=\"exo_controls_next_description\">Nākamais ieraksts</string>\n  <string name=\"exo_controls_pause_description\">Pauzēt</string>\n  <string name=\"exo_controls_play_description\">Atskaņot</string>\n  <string name=\"exo_controls_stop_description\">Apturēt</string>\n  <string name=\"exo_controls_rewind_description\">Attīt atpakaļ</string>\n  <string name=\"exo_controls_fastforward_description\">Pārtīt uz priekšu</string>\n  <string name=\"exo_controls_repeat_off_description\">Neatkārtot nevienu</string>\n  <string name=\"exo_controls_repeat_one_description\">Atkārtot vienu</string>\n  <string name=\"exo_controls_repeat_all_description\">Atkārtot visu</string>\n  <string name=\"exo_controls_shuffle_on_description\">Atsk. jauktā secībā ieslēgta</string>\n  <string name=\"exo_controls_shuffle_off_description\">Atsk. jauktā secībā izslēgta</string>\n  <string name=\"exo_controls_fullscreen_description\">Pilnekrāna režīms</string>\n  <string name=\"exo_controls_vr_description\">VR režīms</string>\n  <string name=\"exo_download_description\">Lejupielādēt</string>\n  <string name=\"exo_download_notification_channel_name\">Lejupielādes</string>\n  <string name=\"exo_download_downloading\">Notiek lejupielāde</string>\n  <string name=\"exo_download_completed\">Lejupielāde ir pabeigta</string>\n  <string name=\"exo_download_failed\">Lejupielāde neizdevās</string>\n  <string name=\"exo_download_removing\">Notiek lejupielāžu noņemšana</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Teksts</string>\n  <string name=\"exo_track_selection_none\">Nav</string>\n  <string name=\"exo_track_selection_auto\">Automātisks</string>\n  <string name=\"exo_track_unknown\">Nezināms</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Ieskaujošā skaņa</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 ieskaujošā skaņa</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 ieskaujošā skaņa</string>\n  <string name=\"exo_track_role_alternate\">Cits</string>\n  <string name=\"exo_track_role_supplementary\">Papildu</string>\n  <string name=\"exo_track_role_commentary\">Komentāri</string>\n  <string name=\"exo_track_role_closed_captions\">Slēgtie paraksti</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mb/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-mk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Претходна песна</string>\n  <string name=\"exo_controls_next_description\">Следна песна</string>\n  <string name=\"exo_controls_pause_description\">Пауза</string>\n  <string name=\"exo_controls_play_description\">Пушти</string>\n  <string name=\"exo_controls_stop_description\">Сопри</string>\n  <string name=\"exo_controls_rewind_description\">Премотај наназад</string>\n  <string name=\"exo_controls_fastforward_description\">Премотај напред</string>\n  <string name=\"exo_controls_repeat_off_description\">Не повторувај ниту една</string>\n  <string name=\"exo_controls_repeat_one_description\">Повтори една</string>\n  <string name=\"exo_controls_repeat_all_description\">Повтори ги сите</string>\n  <string name=\"exo_controls_shuffle_on_description\">Мешањето е вклучено</string>\n  <string name=\"exo_controls_shuffle_off_description\">Мешањето е исклучено</string>\n  <string name=\"exo_controls_fullscreen_description\">Режим на цел екран</string>\n  <string name=\"exo_controls_vr_description\">Режим на VR</string>\n  <string name=\"exo_download_description\">Преземи</string>\n  <string name=\"exo_download_notification_channel_name\">Преземања</string>\n  <string name=\"exo_download_downloading\">Се презема</string>\n  <string name=\"exo_download_completed\">Преземањето заврши</string>\n  <string name=\"exo_download_failed\">Неуспешно преземање</string>\n  <string name=\"exo_download_removing\">Се отстрануваат преземањата</string>\n  <string name=\"exo_track_selection_title_video\">Видео</string>\n  <string name=\"exo_track_selection_title_audio\">Аудио</string>\n  <string name=\"exo_track_selection_title_text\">Текст</string>\n  <string name=\"exo_track_selection_none\">Нема</string>\n  <string name=\"exo_track_selection_auto\">Автоматскa</string>\n  <string name=\"exo_track_unknown\">Непозната</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Моно</string>\n  <string name=\"exo_track_stereo\">Стерео</string>\n  <string name=\"exo_track_surround\">Опкружувачки звук</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 опкружувачки звук</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 опкружувачки звук</string>\n  <string name=\"exo_track_role_alternate\">Алтернативна</string>\n  <string name=\"exo_track_role_supplementary\">Дополнителна</string>\n  <string name=\"exo_track_role_commentary\">Коментар</string>\n  <string name=\"exo_track_role_closed_captions\">Титлови</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mб/с</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-ml/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">മുമ്പത്തെ ട്രാക്ക്</string>\n  <string name=\"exo_controls_next_description\">അടുത്ത ട്രാക്ക്</string>\n  <string name=\"exo_controls_pause_description\">തൽക്കാലം നിർത്തുക</string>\n  <string name=\"exo_controls_play_description\">പ്ലേ ചെയ്യുക</string>\n  <string name=\"exo_controls_stop_description\">നിര്‍ത്തുക</string>\n  <string name=\"exo_controls_rewind_description\">പിന്നിലേക്ക് പോവുക</string>\n  <string name=\"exo_controls_fastforward_description\">വേഗത്തിൽ മുന്നോട്ട് പോവുക</string>\n  <string name=\"exo_controls_repeat_off_description\">ഒന്നും ആവർത്തിക്കരുത്</string>\n  <string name=\"exo_controls_repeat_one_description\">ഒരെണ്ണം ആവർത്തിക്കുക</string>\n  <string name=\"exo_controls_repeat_all_description\">എല്ലാം ആവർത്തിക്കുക</string>\n  <string name=\"exo_controls_shuffle_on_description\">ഇടകലർത്തൽ ഓണാക്കുക</string>\n  <string name=\"exo_controls_shuffle_off_description\">ഇടകലർത്തൽ ഓഫാക്കുക</string>\n  <string name=\"exo_controls_fullscreen_description\">പൂർണ്ണ സ്‌ക്രീൻ മോഡ്</string>\n  <string name=\"exo_controls_vr_description\">VR മോഡ്</string>\n  <string name=\"exo_download_description\">ഡൗൺലോഡ്</string>\n  <string name=\"exo_download_notification_channel_name\">ഡൗൺലോഡുകൾ</string>\n  <string name=\"exo_download_downloading\">ഡൗൺലോഡ് ചെയ്യുന്നു</string>\n  <string name=\"exo_download_completed\">ഡൗൺലോഡ് പൂർത്തിയായി</string>\n  <string name=\"exo_download_failed\">ഡൗൺലോഡ് പരാജയപ്പെട്ടു</string>\n  <string name=\"exo_download_removing\">ഡൗൺലോഡുകൾ നീക്കം ചെയ്യുന്നു</string>\n  <string name=\"exo_track_selection_title_video\">വീഡിയോ</string>\n  <string name=\"exo_track_selection_title_audio\">ഓഡിയോ</string>\n  <string name=\"exo_track_selection_title_text\">ടെക്‌സ്റ്റ്</string>\n  <string name=\"exo_track_selection_none\">ഒന്നുമില്ല</string>\n  <string name=\"exo_track_selection_auto\">സ്വമേധയാ</string>\n  <string name=\"exo_track_unknown\">അജ്ഞാതം</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">മോണോ</string>\n  <string name=\"exo_track_stereo\">സ്റ്റീരിയോ</string>\n  <string name=\"exo_track_surround\">സറൗണ്ട് സൗണ്ട്</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 സറൗണ്ട് സൗണ്ട്</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 സറൗണ്ട് സൗണ്ട്</string>\n  <string name=\"exo_track_role_alternate\">ഇതര ട്രാക്ക്</string>\n  <string name=\"exo_track_role_supplementary\">സപ്ലിമെന്ററി</string>\n  <string name=\"exo_track_role_commentary\">വിവരണം</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-mn/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Өмнөх бичлэг</string>\n  <string name=\"exo_controls_next_description\">Дараагийн бичлэг</string>\n  <string name=\"exo_controls_pause_description\">Түр зогсоох</string>\n  <string name=\"exo_controls_play_description\">Тоглуулах</string>\n  <string name=\"exo_controls_stop_description\">Зогсоох</string>\n  <string name=\"exo_controls_rewind_description\">Ухраах</string>\n  <string name=\"exo_controls_fastforward_description\">Хурдан урагшлуулах</string>\n  <string name=\"exo_controls_repeat_off_description\">Алийг нь ч дахин тоглуулахгүй</string>\n  <string name=\"exo_controls_repeat_one_description\">Одоогийн тоглуулж буй медиаг дахин тоглуулах</string>\n  <string name=\"exo_controls_repeat_all_description\">Бүгдийг нь дахин тоглуулах</string>\n  <string name=\"exo_controls_shuffle_on_description\">Холих асаалттай</string>\n  <string name=\"exo_controls_shuffle_off_description\">Холих унтраалттай</string>\n  <string name=\"exo_controls_fullscreen_description\">Бүтэн дэлгэцийн горим</string>\n  <string name=\"exo_controls_vr_description\">VR горим</string>\n  <string name=\"exo_download_description\">Татах</string>\n  <string name=\"exo_download_notification_channel_name\">Татaлт</string>\n  <string name=\"exo_download_downloading\">Татаж байна</string>\n  <string name=\"exo_download_completed\">Татаж дууссан</string>\n  <string name=\"exo_download_failed\">Татаж чадсангүй</string>\n  <string name=\"exo_download_removing\">Татаж авсан файлыг хасаж байна</string>\n  <string name=\"exo_track_selection_title_video\">Видео</string>\n  <string name=\"exo_track_selection_title_audio\">Дуу</string>\n  <string name=\"exo_track_selection_title_text\">Текст</string>\n  <string name=\"exo_track_selection_none\">Байхгүй</string>\n  <string name=\"exo_track_selection_auto\">Автомат</string>\n  <string name=\"exo_track_unknown\">Үл мэдэгдэх</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Моно</string>\n  <string name=\"exo_track_stereo\">Стерео</string>\n  <string name=\"exo_track_surround\">Орчин тойрны дуу</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 орчин тойрны дуу</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 орчин тойрны дуу</string>\n  <string name=\"exo_track_role_alternate\">Хувилбар</string>\n  <string name=\"exo_track_role_supplementary\">Нэмэлт</string>\n  <string name=\"exo_track_role_commentary\">Тайлбар</string>\n  <string name=\"exo_track_role_closed_captions\">Хаалттай тайлбар</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-mr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">मागील ट्रॅक</string>\n  <string name=\"exo_controls_next_description\">पुढील ट्रॅक</string>\n  <string name=\"exo_controls_pause_description\">विराम</string>\n  <string name=\"exo_controls_play_description\">प्‍ले करा</string>\n  <string name=\"exo_controls_stop_description\">थांबा</string>\n  <string name=\"exo_controls_rewind_description\">रीवाइंड करा</string>\n  <string name=\"exo_controls_fastforward_description\">फास्ट फॉरवर्ड करा</string>\n  <string name=\"exo_controls_repeat_off_description\">रीपीट करू नका</string>\n  <string name=\"exo_controls_repeat_one_description\">एक रीपीट करा</string>\n  <string name=\"exo_controls_repeat_all_description\">सर्व रीपीट करा</string>\n  <string name=\"exo_controls_shuffle_on_description\">शफल करा सुरू करा</string>\n  <string name=\"exo_controls_shuffle_off_description\">शफल करा बंद करा</string>\n  <string name=\"exo_controls_fullscreen_description\">फुल स्क्रीन मोड</string>\n  <string name=\"exo_controls_vr_description\">VR मोड</string>\n  <string name=\"exo_download_description\">डाउनलोड करा</string>\n  <string name=\"exo_download_notification_channel_name\">डाउनलोड</string>\n  <string name=\"exo_download_downloading\">डाउनलोड होत आहे</string>\n  <string name=\"exo_download_completed\">डाउनलोड पूर्ण झाले</string>\n  <string name=\"exo_download_failed\">डाउनलोड अयशस्वी झाले</string>\n  <string name=\"exo_download_removing\">डाउनलोड काढून टाकत आहे</string>\n  <string name=\"exo_track_selection_title_video\">व्हिडिओ</string>\n  <string name=\"exo_track_selection_title_audio\">ऑडिओ</string>\n  <string name=\"exo_track_selection_title_text\">मजकूर</string>\n  <string name=\"exo_track_selection_none\">काहीही नाही</string>\n  <string name=\"exo_track_selection_auto\">आपोआप</string>\n  <string name=\"exo_track_unknown\">अज्ञात</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">मोनो</string>\n  <string name=\"exo_track_stereo\">स्टिरिओ</string>\n  <string name=\"exo_track_surround\">सराउंड साउंड</string>\n  <string name=\"exo_track_surround_5_point_1\">५.१ सराउंड साउंड</string>\n  <string name=\"exo_track_surround_7_point_1\">७.१ सराउंड साउंड</string>\n  <string name=\"exo_track_role_alternate\">पर्यायी</string>\n  <string name=\"exo_track_role_supplementary\">साहाय्यक</string>\n  <string name=\"exo_track_role_commentary\">समालोचन</string>\n  <string name=\"exo_track_role_closed_captions\">सबटायटल</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-ms/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Lagu sebelumnya</string>\n  <string name=\"exo_controls_next_description\">Lagu seterusnya</string>\n  <string name=\"exo_controls_pause_description\">Jeda</string>\n  <string name=\"exo_controls_play_description\">Main</string>\n  <string name=\"exo_controls_stop_description\">Berhenti</string>\n  <string name=\"exo_controls_rewind_description\">Mandir</string>\n  <string name=\"exo_controls_fastforward_description\">Mundar laju</string>\n  <string name=\"exo_controls_repeat_off_description\">Jangan ulang</string>\n  <string name=\"exo_controls_repeat_one_description\">Ulang satu</string>\n  <string name=\"exo_controls_repeat_all_description\">Ulang semua</string>\n  <string name=\"exo_controls_shuffle_on_description\">Hidupkan rombak</string>\n  <string name=\"exo_controls_shuffle_off_description\">Matikan rombak</string>\n  <string name=\"exo_controls_fullscreen_description\">Mod skrin penuh</string>\n  <string name=\"exo_controls_vr_description\">Mod VR</string>\n  <string name=\"exo_download_description\">Muat turun</string>\n  <string name=\"exo_download_notification_channel_name\">Muat turun</string>\n  <string name=\"exo_download_downloading\">Memuat turun</string>\n  <string name=\"exo_download_completed\">Muat turun selesai</string>\n  <string name=\"exo_download_failed\">Muat turun gagal</string>\n  <string name=\"exo_download_removing\">Mengalih keluar muat turun</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Teks</string>\n  <string name=\"exo_track_selection_none\">Tiada</string>\n  <string name=\"exo_track_selection_auto\">Automatik</string>\n  <string name=\"exo_track_unknown\">Tidak diketahui</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Bunyi keliling</string>\n  <string name=\"exo_track_surround_5_point_1\">Bunyi keliling 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Bunyi keliling 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternatif</string>\n  <string name=\"exo_track_role_supplementary\">Tambahan</string>\n  <string name=\"exo_track_role_commentary\">Ulasan</string>\n  <string name=\"exo_track_role_closed_captions\">SK</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-my/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">ယခင် တစ်ပုဒ်</string>\n  <string name=\"exo_controls_next_description\">နောက် တစ်ပုဒ်</string>\n  <string name=\"exo_controls_pause_description\">ခဏရပ်ရန်</string>\n  <string name=\"exo_controls_play_description\">ဖွင့်ရန်</string>\n  <string name=\"exo_controls_stop_description\">ရပ်ရန်</string>\n  <string name=\"exo_controls_rewind_description\">ပြန်ရစ်ရန်</string>\n  <string name=\"exo_controls_fastforward_description\">ရှေ့သို့ အမြန်သွားရန်</string>\n  <string name=\"exo_controls_repeat_off_description\">မည်သည်ကိုမျှ ပြန်မကျော့ရန်</string>\n  <string name=\"exo_controls_repeat_one_description\">တစ်ခုကို ပြန်ကျော့ရန်</string>\n  <string name=\"exo_controls_repeat_all_description\">အားလုံး ပြန်ကျော့ရန်</string>\n  <string name=\"exo_controls_shuffle_on_description\">ရောသမမွှေကို ဖွင့်ထားသည်</string>\n  <string name=\"exo_controls_shuffle_off_description\">ရောသမမွှေကို ပိတ်ထားသည်</string>\n  <string name=\"exo_controls_fullscreen_description\">မျက်နှာပြင်အပြည့် မုဒ်</string>\n  <string name=\"exo_controls_vr_description\">VR မုဒ်</string>\n  <string name=\"exo_download_description\">ဒေါင်းလုဒ် လုပ်ရန်</string>\n  <string name=\"exo_download_notification_channel_name\">ဒေါင်းလုဒ်များ</string>\n  <string name=\"exo_download_downloading\">ဒေါင်းလုဒ်လုပ်နေသည်</string>\n  <string name=\"exo_download_completed\">ဒေါင်းလုဒ်လုပ်ပြီးပါပြီ</string>\n  <string name=\"exo_download_failed\">ဒေါင်းလုဒ်လုပ်၍ မရပါ</string>\n  <string name=\"exo_download_removing\">ဒေါင်းလုဒ်များ ဖယ်ရှားနေသည်</string>\n  <string name=\"exo_track_selection_title_video\">ဗီဒီယို</string>\n  <string name=\"exo_track_selection_title_audio\">အသံ</string>\n  <string name=\"exo_track_selection_title_text\">စာသား</string>\n  <string name=\"exo_track_selection_none\">မရှိ</string>\n  <string name=\"exo_track_selection_auto\">အလိုအလျောက်</string>\n  <string name=\"exo_track_unknown\">အမည်မသိ</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">မိုနို</string>\n  <string name=\"exo_track_stereo\">စတီရီယို</string>\n  <string name=\"exo_track_surround\">ပတ်လည် အသံစနစ်</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 ပတ်လည် အသံစနစ်</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 ပတ်လည် အသံစနစ်</string>\n  <string name=\"exo_track_role_alternate\">အရန်</string>\n  <string name=\"exo_track_role_supplementary\">နောက်ဆက်တွဲ</string>\n  <string name=\"exo_track_role_commentary\">ထင်မြင်သုံးသပ်ချက်</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s၊ %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-nb/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Forrige spor</string>\n  <string name=\"exo_controls_next_description\">Neste spor</string>\n  <string name=\"exo_controls_pause_description\">Sett på pause</string>\n  <string name=\"exo_controls_play_description\">Spill av</string>\n  <string name=\"exo_controls_stop_description\">Stopp</string>\n  <string name=\"exo_controls_rewind_description\">Spol tilbake</string>\n  <string name=\"exo_controls_fastforward_description\">Spol forover</string>\n  <string name=\"exo_controls_repeat_off_description\">Ikke gjenta noen</string>\n  <string name=\"exo_controls_repeat_one_description\">Gjenta én</string>\n  <string name=\"exo_controls_repeat_all_description\">Gjenta alle</string>\n  <string name=\"exo_controls_shuffle_on_description\">Tilfeldig rekkefølge er på</string>\n  <string name=\"exo_controls_shuffle_off_description\">Tilfeldig rekkefølge er av</string>\n  <string name=\"exo_controls_fullscreen_description\">Fullskjermmodus</string>\n  <string name=\"exo_controls_vr_description\">VR-modus</string>\n  <string name=\"exo_download_description\">Last ned</string>\n  <string name=\"exo_download_notification_channel_name\">Nedlastinger</string>\n  <string name=\"exo_download_downloading\">Laster ned</string>\n  <string name=\"exo_download_completed\">Nedlastingen er fullført</string>\n  <string name=\"exo_download_failed\">Nedlastingen mislyktes</string>\n  <string name=\"exo_download_removing\">Fjerner nedlastinger</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Lyd</string>\n  <string name=\"exo_track_selection_title_text\">Tekst</string>\n  <string name=\"exo_track_selection_none\">Ingen</string>\n  <string name=\"exo_track_selection_auto\">Automatisk</string>\n  <string name=\"exo_track_unknown\">Ukjent</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Surround-lyd</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 surround-lyd</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 surround-lyd</string>\n  <string name=\"exo_track_role_alternate\">Alternativt spor</string>\n  <string name=\"exo_track_role_supplementary\">Tilleggsspor</string>\n  <string name=\"exo_track_role_commentary\">Kommentarer</string>\n  <string name=\"exo_track_role_closed_captions\">Teksting</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-ne/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">अघिल्लो ट्रयाक</string>\n  <string name=\"exo_controls_next_description\">अर्को ट्र्याक</string>\n  <string name=\"exo_controls_pause_description\">पज गर्नुहोस्</string>\n  <string name=\"exo_controls_play_description\">प्ले गर्नुहोस्</string>\n  <string name=\"exo_controls_stop_description\">रोक्नुहोस्</string>\n  <string name=\"exo_controls_rewind_description\">रिवाइन्ड गर्नुहोस्</string>\n  <string name=\"exo_controls_fastforward_description\">फास्ट फर्वार्ड गर्नुहोस्</string>\n  <string name=\"exo_controls_repeat_off_description\">कुनै पनि नदोहोर्‍याउनुहोस्</string>\n  <string name=\"exo_controls_repeat_one_description\">एउटा दोहोर्‍याउनुहोस्</string>\n  <string name=\"exo_controls_repeat_all_description\">सबै दोहोर्‍याउनुहोस्</string>\n  <string name=\"exo_controls_shuffle_on_description\">मिसाउने सुविधा सक्रिय छ</string>\n  <string name=\"exo_controls_shuffle_off_description\">मिसाउने सुविधा निष्क्रिय छ</string>\n  <string name=\"exo_controls_fullscreen_description\">पूर्ण स्क्रिन मोड</string>\n  <string name=\"exo_controls_vr_description\">VR मोड</string>\n  <string name=\"exo_download_description\">डाउनलोड गर्नुहोस्</string>\n  <string name=\"exo_download_notification_channel_name\">डाउनलोडहरू</string>\n  <string name=\"exo_download_downloading\">डाउनलोड गरिँदै छ</string>\n  <string name=\"exo_download_completed\">डाउनलोड सम्पन्न भयो</string>\n  <string name=\"exo_download_failed\">डाउनलोड गर्न सकिएन</string>\n  <string name=\"exo_download_removing\">डाउनलोडहरू हटाउँदै</string>\n  <string name=\"exo_track_selection_title_video\">भिडियो</string>\n  <string name=\"exo_track_selection_title_audio\">अडियो</string>\n  <string name=\"exo_track_selection_title_text\">पाठ</string>\n  <string name=\"exo_track_selection_none\">कुनै पनि होइन</string>\n  <string name=\"exo_track_selection_auto\">स्वतः</string>\n  <string name=\"exo_track_unknown\">अज्ञात</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">मोनो</string>\n  <string name=\"exo_track_stereo\">स्टेरियो</string>\n  <string name=\"exo_track_surround\">सराउन्ड साउन्ड</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 सराउन्ड साउन्ड</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 सराउन्ड साउन्ड</string>\n  <string name=\"exo_track_role_alternate\">वैकल्पिक</string>\n  <string name=\"exo_track_role_supplementary\">पूरक</string>\n  <string name=\"exo_track_role_commentary\">टिप्पणी</string>\n  <string name=\"exo_track_role_closed_captions\">उपशीर्षकहरू</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-nl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Vorige track</string>\n  <string name=\"exo_controls_next_description\">Volgende track</string>\n  <string name=\"exo_controls_pause_description\">Pauzeren</string>\n  <string name=\"exo_controls_play_description\">Afspelen</string>\n  <string name=\"exo_controls_stop_description\">Stoppen</string>\n  <string name=\"exo_controls_rewind_description\">Terugspoelen</string>\n  <string name=\"exo_controls_fastforward_description\">Vooruitspoelen</string>\n  <string name=\"exo_controls_repeat_off_description\">Niets herhalen</string>\n  <string name=\"exo_controls_repeat_one_description\">Eén herhalen</string>\n  <string name=\"exo_controls_repeat_all_description\">Alles herhalen</string>\n  <string name=\"exo_controls_shuffle_on_description\">Shuffle aan</string>\n  <string name=\"exo_controls_shuffle_off_description\">Shuffle uit</string>\n  <string name=\"exo_controls_fullscreen_description\">Modus \\'Volledig scherm\\'</string>\n  <string name=\"exo_controls_vr_description\">VR-modus</string>\n  <string name=\"exo_download_description\">Downloaden</string>\n  <string name=\"exo_download_notification_channel_name\">Downloads</string>\n  <string name=\"exo_download_downloading\">Downloaden</string>\n  <string name=\"exo_download_completed\">Downloaden voltooid</string>\n  <string name=\"exo_download_failed\">Downloaden mislukt</string>\n  <string name=\"exo_download_removing\">Downloads verwijderen</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Tekst</string>\n  <string name=\"exo_track_selection_none\">Geen</string>\n  <string name=\"exo_track_selection_auto\">Auto</string>\n  <string name=\"exo_track_unknown\">Onbekend</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Surround sound</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 surroundgeluid</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 surroundgeluid</string>\n  <string name=\"exo_track_role_alternate\">Alternatief</string>\n  <string name=\"exo_track_role_supplementary\">Aanvullend</string>\n  <string name=\"exo_track_role_commentary\">Commentaar</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-pa/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">ਪਿਛਲਾ ਟਰੈਕ</string>\n  <string name=\"exo_controls_next_description\">ਅਗਲਾ ਟਰੈਕ</string>\n  <string name=\"exo_controls_pause_description\">ਰੋਕੋ</string>\n  <string name=\"exo_controls_play_description\">ਚਲਾਓ</string>\n  <string name=\"exo_controls_stop_description\">ਬੰਦ ਕਰੋ</string>\n  <string name=\"exo_controls_rewind_description\">ਪਿੱਛੇ ਕਰੋ</string>\n  <string name=\"exo_controls_fastforward_description\">ਤੇਜ਼ੀ ਨਾਲ ਅੱਗੇ ਕਰੋ</string>\n  <string name=\"exo_controls_repeat_off_description\">ਕਿਸੇ ਨੂੰ ਨਾ ਦੁਹਰਾਓ</string>\n  <string name=\"exo_controls_repeat_one_description\">ਇੱਕ ਵਾਰ ਦੁਹਰਾਓ</string>\n  <string name=\"exo_controls_repeat_all_description\">ਸਾਰਿਆਂ ਨੂੰ ਦੁਹਰਾਓ</string>\n  <string name=\"exo_controls_shuffle_on_description\">\\'ਬੇਤਰਤੀਬ ਕਰੋ\\' ਮੋਡ ਚਾਲੂ ਹੈ</string>\n  <string name=\"exo_controls_shuffle_off_description\">\\'ਬੇਤਰਤੀਬ ਕਰੋ\\' ਮੋਡ ਬੰਦ ਹੈ</string>\n  <string name=\"exo_controls_fullscreen_description\">ਪੂਰੀ-ਸਕ੍ਰੀਨ ਮੋਡ</string>\n  <string name=\"exo_controls_vr_description\">VR ਮੋਡ</string>\n  <string name=\"exo_download_description\">ਡਾਊਨਲੋਡ ਕਰੋ</string>\n  <string name=\"exo_download_notification_channel_name\">ਡਾਊਨਲੋਡ</string>\n  <string name=\"exo_download_downloading\">ਡਾਊਨਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string>\n  <string name=\"exo_download_completed\">ਡਾਊਨਲੋਡ ਮੁਕੰਮਲ ਹੋਇਆ</string>\n  <string name=\"exo_download_failed\">ਡਾਊਨਲੋਡ ਅਸਫਲ ਰਿਹਾ</string>\n  <string name=\"exo_download_removing\">ਡਾਊਨਲੋਡ ਕੀਤੀ ਸਮੱਗਰੀ ਹਟਾਈ ਜਾ ਰਹੀ ਹੈ</string>\n  <string name=\"exo_track_selection_title_video\">ਵੀਡੀਓ</string>\n  <string name=\"exo_track_selection_title_audio\">ਆਡੀਓ</string>\n  <string name=\"exo_track_selection_title_text\">ਲਿਖਤ</string>\n  <string name=\"exo_track_selection_none\">ਕੋਈ ਨਹੀਂ</string>\n  <string name=\"exo_track_selection_auto\">ਸਵੈਚਲਿਤ</string>\n  <string name=\"exo_track_unknown\">ਅਗਿਆਤ</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">ਮੋਨੋ</string>\n  <string name=\"exo_track_stereo\">ਸਟੀਰਿਓ</string>\n  <string name=\"exo_track_surround\">ਸਰਾਊਂਡ ਸਾਊਂਡ</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 ਸਰਾਊਂਡ ਸਾਊਂਡ</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 ਸਰਾਊਂਡ ਸਾਊਂਡ</string>\n  <string name=\"exo_track_role_alternate\">ਵਟਾਵਾਂ</string>\n  <string name=\"exo_track_role_supplementary\">ਪੂਰਕ</string>\n  <string name=\"exo_track_role_commentary\">ਕਮੈਂਟਰੀ</string>\n  <string name=\"exo_track_role_closed_captions\">ਬੰਦ ਸੁੁਰਖੀਆਂ</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-pl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Poprzedni utwór</string>\n  <string name=\"exo_controls_next_description\">Następny utwór</string>\n  <string name=\"exo_controls_pause_description\">Wstrzymaj</string>\n  <string name=\"exo_controls_play_description\">Odtwórz</string>\n  <string name=\"exo_controls_stop_description\">Zatrzymaj</string>\n  <string name=\"exo_controls_rewind_description\">Przewiń do tyłu</string>\n  <string name=\"exo_controls_fastforward_description\">Przewiń do przodu</string>\n  <string name=\"exo_controls_repeat_off_description\">Nie powtarzaj</string>\n  <string name=\"exo_controls_repeat_one_description\">Powtórz jeden</string>\n  <string name=\"exo_controls_repeat_all_description\">Powtórz wszystkie</string>\n  <string name=\"exo_controls_shuffle_on_description\">Włącz odtwarzanie losowe</string>\n  <string name=\"exo_controls_shuffle_off_description\">Wyłącz odtwarzanie losowe</string>\n  <string name=\"exo_controls_fullscreen_description\">Tryb pełnoekranowy</string>\n  <string name=\"exo_controls_vr_description\">Tryb VR</string>\n  <string name=\"exo_download_description\">Pobierz</string>\n  <string name=\"exo_download_notification_channel_name\">Pobieranie</string>\n  <string name=\"exo_download_downloading\">Pobieram</string>\n  <string name=\"exo_download_completed\">Zakończono pobieranie</string>\n  <string name=\"exo_download_failed\">Nie udało się pobrać</string>\n  <string name=\"exo_download_removing\">Usuwam pobrane</string>\n  <string name=\"exo_track_selection_title_video\">Film</string>\n  <string name=\"exo_track_selection_title_audio\">Dźwięk</string>\n  <string name=\"exo_track_selection_title_text\">Tekst</string>\n  <string name=\"exo_track_selection_none\">Brak</string>\n  <string name=\"exo_track_selection_auto\">Automatycznie</string>\n  <string name=\"exo_track_unknown\">Nieznany</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Dźwięk przestrzenny</string>\n  <string name=\"exo_track_surround_5_point_1\">System dźwięku przestrzennego 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">System dźwięku przestrzennego 7.1</string>\n  <string name=\"exo_track_role_alternate\">Wersja alternatywna</string>\n  <string name=\"exo_track_role_supplementary\">Materiały dodatkowe</string>\n  <string name=\"exo_track_role_commentary\">Komentarz</string>\n  <string name=\"exo_track_role_closed_captions\">Napisy</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mb/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-pt/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Faixa anterior</string>\n  <string name=\"exo_controls_next_description\">Próxima faixa</string>\n  <string name=\"exo_controls_pause_description\">Pausar</string>\n  <string name=\"exo_controls_play_description\">Reproduzir</string>\n  <string name=\"exo_controls_stop_description\">Parar</string>\n  <string name=\"exo_controls_rewind_description\">Retroceder</string>\n  <string name=\"exo_controls_fastforward_description\">Avançar</string>\n  <string name=\"exo_controls_repeat_off_description\">Não repetir</string>\n  <string name=\"exo_controls_repeat_one_description\">Repetir uma</string>\n  <string name=\"exo_controls_repeat_all_description\">Repetir tudo</string>\n  <string name=\"exo_controls_shuffle_on_description\">Ordem aleatória ativada</string>\n  <string name=\"exo_controls_shuffle_off_description\">Ordem aleatória desativada</string>\n  <string name=\"exo_controls_fullscreen_description\">Modo de tela cheia</string>\n  <string name=\"exo_controls_vr_description\">Modo RV</string>\n  <string name=\"exo_download_description\">Fazer o download</string>\n  <string name=\"exo_download_notification_channel_name\">Downloads</string>\n  <string name=\"exo_download_downloading\">Fazendo o download</string>\n  <string name=\"exo_download_completed\">Download concluído</string>\n  <string name=\"exo_download_failed\">Falha no download</string>\n  <string name=\"exo_download_removing\">Removendo downloads</string>\n  <string name=\"exo_track_selection_title_video\">Vídeo</string>\n  <string name=\"exo_track_selection_title_audio\">Áudio</string>\n  <string name=\"exo_track_selection_title_text\">Texto</string>\n  <string name=\"exo_track_selection_none\">Nenhuma</string>\n  <string name=\"exo_track_selection_auto\">Automática</string>\n  <string name=\"exo_track_unknown\">Desconhecido</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Estéreo</string>\n  <string name=\"exo_track_surround\">Sistema surround</string>\n  <string name=\"exo_track_surround_5_point_1\">Sistema surround 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Sistema surround 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternativa</string>\n  <string name=\"exo_track_role_supplementary\">Complementar</string>\n  <string name=\"exo_track_role_commentary\">Comentários</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-pt-rPT/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Faixa anterior</string>\n  <string name=\"exo_controls_next_description\">Faixa seguinte</string>\n  <string name=\"exo_controls_pause_description\">Colocar em pausa</string>\n  <string name=\"exo_controls_play_description\">Reproduzir</string>\n  <string name=\"exo_controls_stop_description\">Parar</string>\n  <string name=\"exo_controls_rewind_description\">Recuar</string>\n  <string name=\"exo_controls_fastforward_description\">Avançar</string>\n  <string name=\"exo_controls_repeat_off_description\">Não repetir nenhum</string>\n  <string name=\"exo_controls_repeat_one_description\">Repetir um</string>\n  <string name=\"exo_controls_repeat_all_description\">Repetir tudo</string>\n  <string name=\"exo_controls_shuffle_on_description\">Reprodução aleatória ativada</string>\n  <string name=\"exo_controls_shuffle_off_description\">Reprodução aleatória desativ.</string>\n  <string name=\"exo_controls_fullscreen_description\">Modo de ecrã inteiro</string>\n  <string name=\"exo_controls_vr_description\">Modo de RV</string>\n  <string name=\"exo_download_description\">Transferir</string>\n  <string name=\"exo_download_notification_channel_name\">Transferências</string>\n  <string name=\"exo_download_downloading\">A transferir…</string>\n  <string name=\"exo_download_completed\">Transferência concluída</string>\n  <string name=\"exo_download_failed\">Falha na transferência</string>\n  <string name=\"exo_download_removing\">A remover as transferências…</string>\n  <string name=\"exo_track_selection_title_video\">Vídeo</string>\n  <string name=\"exo_track_selection_title_audio\">Áudio</string>\n  <string name=\"exo_track_selection_title_text\">Texto</string>\n  <string name=\"exo_track_selection_none\">Nenhuma</string>\n  <string name=\"exo_track_selection_auto\">Automático</string>\n  <string name=\"exo_track_unknown\">Desconhecida</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Estéreo</string>\n  <string name=\"exo_track_surround\">Som surround</string>\n  <string name=\"exo_track_surround_5_point_1\">Som surround 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Som surround 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternativa</string>\n  <string name=\"exo_track_role_supplementary\">Suplementar</string>\n  <string name=\"exo_track_role_commentary\">Comentário</string>\n  <string name=\"exo_track_role_closed_captions\">Legendas</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-ro/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Melodia anterioară</string>\n  <string name=\"exo_controls_next_description\">Următoarea înregistrare</string>\n  <string name=\"exo_controls_pause_description\">Întrerupeți</string>\n  <string name=\"exo_controls_play_description\">Redați</string>\n  <string name=\"exo_controls_stop_description\">Opriți</string>\n  <string name=\"exo_controls_rewind_description\">Derulați înapoi</string>\n  <string name=\"exo_controls_fastforward_description\">Derulați rapid înainte</string>\n  <string name=\"exo_controls_repeat_off_description\">Nu repetați niciunul</string>\n  <string name=\"exo_controls_repeat_one_description\">Repetați unul</string>\n  <string name=\"exo_controls_repeat_all_description\">Repetați-le pe toate</string>\n  <string name=\"exo_controls_shuffle_on_description\">Redare aleatorie activată</string>\n  <string name=\"exo_controls_shuffle_off_description\">Redare aleatorie dezactivată</string>\n  <string name=\"exo_controls_fullscreen_description\">Modul Ecran complet</string>\n  <string name=\"exo_controls_vr_description\">Mod RV</string>\n  <string name=\"exo_download_description\">Descărcați</string>\n  <string name=\"exo_download_notification_channel_name\">Descărcări</string>\n  <string name=\"exo_download_downloading\">Se descarcă</string>\n  <string name=\"exo_download_completed\">Descărcarea a fost finalizată</string>\n  <string name=\"exo_download_failed\">Descărcarea nu a reușit</string>\n  <string name=\"exo_download_removing\">Se elimină descărcările</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Text</string>\n  <string name=\"exo_track_selection_none\">Fără</string>\n  <string name=\"exo_track_selection_auto\">Automat</string>\n  <string name=\"exo_track_unknown\">Necunoscut</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Sunet surround</string>\n  <string name=\"exo_track_surround_5_point_1\">Sunet surround 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Sunet surround 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternativ</string>\n  <string name=\"exo_track_role_supplementary\">Suplimentar</string>\n  <string name=\"exo_track_role_commentary\">Comentarii</string>\n  <string name=\"exo_track_role_closed_captions\">Subtitrări</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-ru/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Предыдущий трек</string>\n  <string name=\"exo_controls_next_description\">Следующий трек</string>\n  <string name=\"exo_controls_pause_description\">Приостановить</string>\n  <string name=\"exo_controls_play_description\">Воспроизвести</string>\n  <string name=\"exo_controls_stop_description\">Остановить</string>\n  <string name=\"exo_controls_rewind_description\">Перемотать назад</string>\n  <string name=\"exo_controls_fastforward_description\">Перемотать вперед</string>\n  <string name=\"exo_controls_repeat_off_description\">Не повторять</string>\n  <string name=\"exo_controls_repeat_one_description\">Повторять трек</string>\n  <string name=\"exo_controls_repeat_all_description\">Повторять все</string>\n  <string name=\"exo_controls_shuffle_on_description\">Перемешивание включено</string>\n  <string name=\"exo_controls_shuffle_off_description\">Перемешивание отключено</string>\n  <string name=\"exo_controls_fullscreen_description\">Полноэкранный режим</string>\n  <string name=\"exo_controls_vr_description\">VR-режим</string>\n  <string name=\"exo_download_description\">Скачать</string>\n  <string name=\"exo_download_notification_channel_name\">Скачивания</string>\n  <string name=\"exo_download_downloading\">Скачивание…</string>\n  <string name=\"exo_download_completed\">Скачивание завершено</string>\n  <string name=\"exo_download_failed\">Ошибка скачивания</string>\n  <string name=\"exo_download_removing\">Удаление скачанных файлов…</string>\n  <string name=\"exo_track_selection_title_video\">Видео</string>\n  <string name=\"exo_track_selection_title_audio\">Аудио</string>\n  <string name=\"exo_track_selection_title_text\">Текст</string>\n  <string name=\"exo_track_selection_none\">Нет</string>\n  <string name=\"exo_track_selection_auto\">Авто</string>\n  <string name=\"exo_track_unknown\">Неизвестный трек</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Моно</string>\n  <string name=\"exo_track_stereo\">Стерео</string>\n  <string name=\"exo_track_surround\">Объемный звук</string>\n  <string name=\"exo_track_surround_5_point_1\">Система объемного звука 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Система объемного звука 7.1</string>\n  <string name=\"exo_track_role_alternate\">Другая запись</string>\n  <string name=\"exo_track_role_supplementary\">Дополнительные материалы</string>\n  <string name=\"exo_track_role_commentary\">Комментарии</string>\n  <string name=\"exo_track_role_closed_captions\">Субтитры</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Мбит/сек</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-si/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">පෙර ඛණ්ඩය</string>\n  <string name=\"exo_controls_next_description\">ඊළඟ ඛණ්ඩය</string>\n  <string name=\"exo_controls_pause_description\">විරාම කරන්න</string>\n  <string name=\"exo_controls_play_description\">ධාවනය කරන්න</string>\n  <string name=\"exo_controls_stop_description\">නවත්වන්න</string>\n  <string name=\"exo_controls_rewind_description\">නැවත ඔතන්න</string>\n  <string name=\"exo_controls_fastforward_description\">වේගයෙන් ඉදිරියට</string>\n  <string name=\"exo_controls_repeat_off_description\">කිසිවක් පුනරාවර්තනය නොකරන්න</string>\n  <string name=\"exo_controls_repeat_one_description\">එකක් පුනරාවර්තනය කරන්න</string>\n  <string name=\"exo_controls_repeat_all_description\">සියල්ල පුනරාවර්තනය කරන්න</string>\n  <string name=\"exo_controls_shuffle_on_description\">කලවම් කිරීම ක්‍රියාත්මකයි</string>\n  <string name=\"exo_controls_shuffle_off_description\">කලවම් කිරීම ක්‍රියා විරහිතයි</string>\n  <string name=\"exo_controls_fullscreen_description\">සම්පූර්ණ තිර ප්‍රකාරය</string>\n  <string name=\"exo_controls_vr_description\">VR ප්‍රකාරය</string>\n  <string name=\"exo_download_description\">බාගන්න</string>\n  <string name=\"exo_download_notification_channel_name\">බාගැනීම්</string>\n  <string name=\"exo_download_downloading\">බාගනිමින්</string>\n  <string name=\"exo_download_completed\">බාගැනීම සම්පූර්ණ කරන ලදී</string>\n  <string name=\"exo_download_failed\">බාගැනීම අසමත් විය</string>\n  <string name=\"exo_download_removing\">බාගැනීම් ඉවත් කිරීම</string>\n  <string name=\"exo_track_selection_title_video\">වීඩියෝ</string>\n  <string name=\"exo_track_selection_title_audio\">ශ්‍රව්‍ය</string>\n  <string name=\"exo_track_selection_title_text\">පෙළ</string>\n  <string name=\"exo_track_selection_none\">කිසිවක් නැත</string>\n  <string name=\"exo_track_selection_auto\">ස්වයං</string>\n  <string name=\"exo_track_unknown\">නොදනී</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">ඒකල</string>\n  <string name=\"exo_track_stereo\">ස්ටීරියෝ</string>\n  <string name=\"exo_track_surround\">අවට ශබ්දය</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 අවට ශබ්දය</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 අවට ශබ්දය</string>\n  <string name=\"exo_track_role_alternate\">විකල්ප</string>\n  <string name=\"exo_track_role_supplementary\">පරිපූරක</string>\n  <string name=\"exo_track_role_commentary\">වර්ණනා</string>\n  <string name=\"exo_track_role_closed_captions\">සිරස්තල</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-sk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Predchádzajúca skladba</string>\n  <string name=\"exo_controls_next_description\">Ďalšia skladba</string>\n  <string name=\"exo_controls_pause_description\">Pozastaviť</string>\n  <string name=\"exo_controls_play_description\">Prehrať</string>\n  <string name=\"exo_controls_stop_description\">Zastaviť</string>\n  <string name=\"exo_controls_rewind_description\">Pretočiť späť</string>\n  <string name=\"exo_controls_fastforward_description\">Pretočiť dopredu</string>\n  <string name=\"exo_controls_repeat_off_description\">Neopakovať</string>\n  <string name=\"exo_controls_repeat_one_description\">Opakovať jednu</string>\n  <string name=\"exo_controls_repeat_all_description\">Opakovať všetko</string>\n  <string name=\"exo_controls_shuffle_on_description\">Náhodné prehrávanie je zapnuté</string>\n  <string name=\"exo_controls_shuffle_off_description\">Náhodné prehrávanie je vypnuté</string>\n  <string name=\"exo_controls_fullscreen_description\">Režim celej obrazovky</string>\n  <string name=\"exo_controls_vr_description\">režim VR</string>\n  <string name=\"exo_download_description\">Stiahnuť</string>\n  <string name=\"exo_download_notification_channel_name\">Stiahnuté</string>\n  <string name=\"exo_download_downloading\">Sťahuje sa</string>\n  <string name=\"exo_download_completed\">Sťahovanie bolo dokončené</string>\n  <string name=\"exo_download_failed\">Nepodarilo sa stiahnuť</string>\n  <string name=\"exo_download_removing\">Odstraňuje sa stiahnutý obsah</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Zvuk</string>\n  <string name=\"exo_track_selection_title_text\">Text</string>\n  <string name=\"exo_track_selection_none\">Žiadne</string>\n  <string name=\"exo_track_selection_auto\">Automaticky</string>\n  <string name=\"exo_track_unknown\">Neznáme</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Priestorový zvuk</string>\n  <string name=\"exo_track_surround_5_point_1\">Priestorový zvuk 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Priestorový zvuk 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternatívna stopa</string>\n  <string name=\"exo_track_role_supplementary\">Doplnková stopa</string>\n  <string name=\"exo_track_role_commentary\">Komentár</string>\n  <string name=\"exo_track_role_closed_captions\">Skryté titulky</string>\n  <string name=\"exo_track_bitrate\">%1$.2f MB/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-sl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Prejšnja skladba</string>\n  <string name=\"exo_controls_next_description\">Naslednja skladba</string>\n  <string name=\"exo_controls_pause_description\">Zaustavitev</string>\n  <string name=\"exo_controls_play_description\">Predvajanje</string>\n  <string name=\"exo_controls_stop_description\">Ustavitev</string>\n  <string name=\"exo_controls_rewind_description\">Previjanje nazaj</string>\n  <string name=\"exo_controls_fastforward_description\">Previjanje naprej</string>\n  <string name=\"exo_controls_repeat_off_description\">Brez ponavljanja</string>\n  <string name=\"exo_controls_repeat_one_description\">Ponavljanje ene</string>\n  <string name=\"exo_controls_repeat_all_description\">Ponavljanje vseh</string>\n  <string name=\"exo_controls_shuffle_on_description\">Naklj. predvajanje vklopljeno</string>\n  <string name=\"exo_controls_shuffle_off_description\">Naklj. predvajanje izklopljeno</string>\n  <string name=\"exo_controls_fullscreen_description\">Celozaslonski način</string>\n  <string name=\"exo_controls_vr_description\">Način VR</string>\n  <string name=\"exo_download_description\">Prenos</string>\n  <string name=\"exo_download_notification_channel_name\">Prenosi</string>\n  <string name=\"exo_download_downloading\">Prenašanje</string>\n  <string name=\"exo_download_completed\">Prenos je končan</string>\n  <string name=\"exo_download_failed\">Prenos ni uspel</string>\n  <string name=\"exo_download_removing\">Odstranjevanje prenosov</string>\n  <string name=\"exo_track_selection_title_video\">Videoposnetki</string>\n  <string name=\"exo_track_selection_title_audio\">Zvočni posnetki</string>\n  <string name=\"exo_track_selection_title_text\">Podnapisi</string>\n  <string name=\"exo_track_selection_none\">Nič</string>\n  <string name=\"exo_track_selection_auto\">Samodejno</string>\n  <string name=\"exo_track_unknown\">Neznano</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Prostorski zvok</string>\n  <string name=\"exo_track_surround_5_point_1\">Prostorski zvok 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Prostorski zvok 7.1</string>\n  <string name=\"exo_track_role_alternate\">Dodatni posnetek</string>\n  <string name=\"exo_track_role_supplementary\">Dodatek</string>\n  <string name=\"exo_track_role_commentary\">Komentar</string>\n  <string name=\"exo_track_role_closed_captions\">Podnapisi</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mb/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-sq/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Kënga e mëparshme</string>\n  <string name=\"exo_controls_next_description\">Kënga tjetër</string>\n  <string name=\"exo_controls_pause_description\">Pauzë</string>\n  <string name=\"exo_controls_play_description\">Luaj</string>\n  <string name=\"exo_controls_stop_description\">Ndalo</string>\n  <string name=\"exo_controls_rewind_description\">Rikthe</string>\n  <string name=\"exo_controls_fastforward_description\">Përparo me shpejtësi</string>\n  <string name=\"exo_controls_repeat_off_description\">Mos përsërit asnjë</string>\n  <string name=\"exo_controls_repeat_one_description\">Përsërit një</string>\n  <string name=\"exo_controls_repeat_all_description\">Përsërit të gjitha</string>\n  <string name=\"exo_controls_shuffle_on_description\">Përzierja aktive</string>\n  <string name=\"exo_controls_shuffle_off_description\">Përzierja joaktive</string>\n  <string name=\"exo_controls_fullscreen_description\">Modaliteti me ekran të plotë</string>\n  <string name=\"exo_controls_vr_description\">Modaliteti RV</string>\n  <string name=\"exo_download_description\">Shkarko</string>\n  <string name=\"exo_download_notification_channel_name\">Shkarkimet</string>\n  <string name=\"exo_download_downloading\">Po shkarkohet</string>\n  <string name=\"exo_download_completed\">Shkarkimi përfundoi</string>\n  <string name=\"exo_download_failed\">Shkarkimi dështoi</string>\n  <string name=\"exo_download_removing\">Shkarkimet po hiqen</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Tekst</string>\n  <string name=\"exo_track_selection_none\">Asnjë</string>\n  <string name=\"exo_track_selection_auto\">Automatike</string>\n  <string name=\"exo_track_unknown\">E panjohur</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Tingulli rrethues</string>\n  <string name=\"exo_track_surround_5_point_1\">Tingull rrethues 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Tingull rrethues 7.1</string>\n  <string name=\"exo_track_role_alternate\">Alternative</string>\n  <string name=\"exo_track_role_supplementary\">Plotësuese</string>\n  <string name=\"exo_track_role_commentary\">Komente</string>\n  <string name=\"exo_track_role_closed_captions\">Titrat</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-sr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Претходна песма</string>\n  <string name=\"exo_controls_next_description\">Следећа песма</string>\n  <string name=\"exo_controls_pause_description\">Паузирај</string>\n  <string name=\"exo_controls_play_description\">Пусти</string>\n  <string name=\"exo_controls_stop_description\">Заустави</string>\n  <string name=\"exo_controls_rewind_description\">Премотај уназад</string>\n  <string name=\"exo_controls_fastforward_description\">Премотај унапред</string>\n  <string name=\"exo_controls_repeat_off_description\">Не понављај ниједну</string>\n  <string name=\"exo_controls_repeat_one_description\">Понови једну</string>\n  <string name=\"exo_controls_repeat_all_description\">Понови све</string>\n  <string name=\"exo_controls_shuffle_on_description\">Насумично пуштање је укључено</string>\n  <string name=\"exo_controls_shuffle_off_description\">Насумично пуштање је искључено</string>\n  <string name=\"exo_controls_fullscreen_description\">Режим целог екрана</string>\n  <string name=\"exo_controls_vr_description\">ВР режим</string>\n  <string name=\"exo_download_description\">Преузми</string>\n  <string name=\"exo_download_notification_channel_name\">Преузимања</string>\n  <string name=\"exo_download_downloading\">Преузима се</string>\n  <string name=\"exo_download_completed\">Преузимање је завршено</string>\n  <string name=\"exo_download_failed\">Преузимање није успело</string>\n  <string name=\"exo_download_removing\">Преузимања се уклањају</string>\n  <string name=\"exo_track_selection_title_video\">Видео</string>\n  <string name=\"exo_track_selection_title_audio\">Аудио</string>\n  <string name=\"exo_track_selection_title_text\">Текст</string>\n  <string name=\"exo_track_selection_none\">Ниједна</string>\n  <string name=\"exo_track_selection_auto\">Аутоматски</string>\n  <string name=\"exo_track_unknown\">Непознато</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Моно</string>\n  <string name=\"exo_track_stereo\">Стерео</string>\n  <string name=\"exo_track_surround\">Звучни систем</string>\n  <string name=\"exo_track_surround_5_point_1\">Звучни систем 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Звучни систем 7.1</string>\n  <string name=\"exo_track_role_alternate\">Алтернативно</string>\n  <string name=\"exo_track_role_supplementary\">Додатно</string>\n  <string name=\"exo_track_role_commentary\">Коментар</string>\n  <string name=\"exo_track_role_closed_captions\">Титл</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mb/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-sv/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Föregående spår</string>\n  <string name=\"exo_controls_next_description\">Nästa spår</string>\n  <string name=\"exo_controls_pause_description\">Pausa</string>\n  <string name=\"exo_controls_play_description\">Spela upp</string>\n  <string name=\"exo_controls_stop_description\">Stoppa</string>\n  <string name=\"exo_controls_rewind_description\">Spola tillbaka</string>\n  <string name=\"exo_controls_fastforward_description\">Snabbspola framåt</string>\n  <string name=\"exo_controls_repeat_off_description\">Upprepa inga</string>\n  <string name=\"exo_controls_repeat_one_description\">Upprepa en</string>\n  <string name=\"exo_controls_repeat_all_description\">Upprepa alla</string>\n  <string name=\"exo_controls_shuffle_on_description\">Blanda spår</string>\n  <string name=\"exo_controls_shuffle_off_description\">Blanda inte spår</string>\n  <string name=\"exo_controls_fullscreen_description\">Helskärmsläge</string>\n  <string name=\"exo_controls_vr_description\">VR-läge</string>\n  <string name=\"exo_download_description\">Ladda ned</string>\n  <string name=\"exo_download_notification_channel_name\">Nedladdningar</string>\n  <string name=\"exo_download_downloading\">Laddar ned</string>\n  <string name=\"exo_download_completed\">Nedladdningen är klar</string>\n  <string name=\"exo_download_failed\">Nedladdningen misslyckades</string>\n  <string name=\"exo_download_removing\">Nedladdningar tas bort</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Ljud</string>\n  <string name=\"exo_track_selection_title_text\">Text</string>\n  <string name=\"exo_track_selection_none\">Ingen</string>\n  <string name=\"exo_track_selection_auto\">Automatiskt</string>\n  <string name=\"exo_track_unknown\">Okänt</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Surroundljud</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1-kanaligt surroundljud</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1-kanaligt surroundljud</string>\n  <string name=\"exo_track_role_alternate\">Alternativ</string>\n  <string name=\"exo_track_role_supplementary\">Tillägg</string>\n  <string name=\"exo_track_role_commentary\">Kommentar</string>\n  <string name=\"exo_track_role_closed_captions\">Undertexter</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbit/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-sw/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Wimbo uliotangulia</string>\n  <string name=\"exo_controls_next_description\">Wimbo unaofuata</string>\n  <string name=\"exo_controls_pause_description\">Sitisha</string>\n  <string name=\"exo_controls_play_description\">Cheza</string>\n  <string name=\"exo_controls_stop_description\">Simamisha</string>\n  <string name=\"exo_controls_rewind_description\">Rudisha nyuma</string>\n  <string name=\"exo_controls_fastforward_description\">Sogeza mbele haraka</string>\n  <string name=\"exo_controls_repeat_off_description\">Usirudie yoyote</string>\n  <string name=\"exo_controls_repeat_one_description\">Rudia moja</string>\n  <string name=\"exo_controls_repeat_all_description\">Rudia zote</string>\n  <string name=\"exo_controls_shuffle_on_description\">Hali ya kuchanganya imewashwa</string>\n  <string name=\"exo_controls_shuffle_off_description\">Hali ya kuchanganya imezimwa</string>\n  <string name=\"exo_controls_fullscreen_description\">Hali ya skrini nzima</string>\n  <string name=\"exo_controls_vr_description\">Hali ya Uhalisia Pepe</string>\n  <string name=\"exo_download_description\">Pakua</string>\n  <string name=\"exo_download_notification_channel_name\">Vipakuliwa</string>\n  <string name=\"exo_download_downloading\">Inapakua</string>\n  <string name=\"exo_download_completed\">Imepakuliwa</string>\n  <string name=\"exo_download_failed\">Imeshindwa kupakua</string>\n  <string name=\"exo_download_removing\">Inaondoa vipakuliwa</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Sauti</string>\n  <string name=\"exo_track_selection_title_text\">Maandishi</string>\n  <string name=\"exo_track_selection_none\">Hamna</string>\n  <string name=\"exo_track_selection_auto\">Otomatiki</string>\n  <string name=\"exo_track_unknown\">Haijulikani</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Sauti ya mzunguko</string>\n  <string name=\"exo_track_surround_5_point_1\">Sauti ya mzunguko ya 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Sauti ya mzunguko ya 7.1</string>\n  <string name=\"exo_track_role_alternate\">Mbadala</string>\n  <string name=\"exo_track_role_supplementary\">Wa ziada</string>\n  <string name=\"exo_track_role_commentary\">Uchambuzi</string>\n  <string name=\"exo_track_role_closed_captions\">Manukuu</string>\n  <string name=\"exo_track_bitrate\">Mbps %1$.2f</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-ta/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">முந்தைய டிராக்</string>\n  <string name=\"exo_controls_next_description\">அடுத்த டிராக்</string>\n  <string name=\"exo_controls_pause_description\">இடைநிறுத்து</string>\n  <string name=\"exo_controls_play_description\">இயக்கு</string>\n  <string name=\"exo_controls_stop_description\">நிறுத்து</string>\n  <string name=\"exo_controls_rewind_description\">பின்செல்</string>\n  <string name=\"exo_controls_fastforward_description\">வேகமாக முன்செல்</string>\n  <string name=\"exo_controls_repeat_off_description\">எதையும் மீண்டும் இயக்காதே</string>\n  <string name=\"exo_controls_repeat_one_description\">இதை மட்டும் மீண்டும் இயக்கு</string>\n  <string name=\"exo_controls_repeat_all_description\">அனைத்தையும் மீண்டும் இயக்கு</string>\n  <string name=\"exo_controls_shuffle_on_description\">கலைத்துப் போடுதல்: ஆன்</string>\n  <string name=\"exo_controls_shuffle_off_description\">கலைத்துப் போடுதல்: ஆஃப்</string>\n  <string name=\"exo_controls_fullscreen_description\">முழுத்திரைப் பயன்முறை</string>\n  <string name=\"exo_controls_vr_description\">VR பயன்முறை</string>\n  <string name=\"exo_download_description\">பதிவிறக்கும் பட்டன்</string>\n  <string name=\"exo_download_notification_channel_name\">பதிவிறக்கங்கள்</string>\n  <string name=\"exo_download_downloading\">பதிவிறக்குகிறது</string>\n  <string name=\"exo_download_completed\">பதிவிறக்கப்பட்டது</string>\n  <string name=\"exo_download_failed\">பதிவிறக்க முடியவில்லை</string>\n  <string name=\"exo_download_removing\">பதிவிறக்கங்கள் அகற்றப்படுகின்றன</string>\n  <string name=\"exo_track_selection_title_video\">வீடியோ</string>\n  <string name=\"exo_track_selection_title_audio\">ஆடியோ</string>\n  <string name=\"exo_track_selection_title_text\">உரை</string>\n  <string name=\"exo_track_selection_none\">ஏதுமில்லை</string>\n  <string name=\"exo_track_selection_auto\">தானியங்கு</string>\n  <string name=\"exo_track_unknown\">தெரியாதவை</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">மோனோ</string>\n  <string name=\"exo_track_stereo\">ஸ்டீரியோ</string>\n  <string name=\"exo_track_surround\">சரவுண்ட் சவுண்ட்</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 சரவுண்ட் சவுண்ட்</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 சரவுண்ட் சவுண்ட்</string>\n  <string name=\"exo_track_role_alternate\">மாற்று டிராக்</string>\n  <string name=\"exo_track_role_supplementary\">துணை டிராக்</string>\n  <string name=\"exo_track_role_commentary\">வர்ணனை</string>\n  <string name=\"exo_track_role_closed_captions\">வசனம்</string>\n  <string name=\"exo_track_bitrate\">%1$.2f மெ.பை./வி</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-te/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">మునుపటి ట్రాక్</string>\n  <string name=\"exo_controls_next_description\">తదుపరి ట్రాక్</string>\n  <string name=\"exo_controls_pause_description\">పాజ్ చేయండి</string>\n  <string name=\"exo_controls_play_description\">ప్లే చేయండి</string>\n  <string name=\"exo_controls_stop_description\">ఆపండి</string>\n  <string name=\"exo_controls_rewind_description\">రివైండ్ చేయండి</string>\n  <string name=\"exo_controls_fastforward_description\">వేగంగా ఫార్వార్డ్ చేయండి</string>\n  <string name=\"exo_controls_repeat_off_description\">దేన్నీ పునరావృతం చేయకండి</string>\n  <string name=\"exo_controls_repeat_one_description\">ఒకదాన్ని పునరావృతం చేయండి</string>\n  <string name=\"exo_controls_repeat_all_description\">అన్నింటినీ పునరావృతం చేయండి</string>\n  <string name=\"exo_controls_shuffle_on_description\">షఫుల్‌ను ఆన్ చేస్తుంది</string>\n  <string name=\"exo_controls_shuffle_off_description\">షఫుల్‌ను ఆఫ్ చేస్తుంది</string>\n  <string name=\"exo_controls_fullscreen_description\">పూర్తి స్క్రీన్ మోడ్</string>\n  <string name=\"exo_controls_vr_description\">వర్చువల్ రియాలిటీ మోడ్</string>\n  <string name=\"exo_download_description\">డౌన్‌లోడ్ చేయి</string>\n  <string name=\"exo_download_notification_channel_name\">డౌన్‌లోడ్‌లు</string>\n  <string name=\"exo_download_downloading\">డౌన్‌లోడ్ చేస్తోంది</string>\n  <string name=\"exo_download_completed\">డౌన్‌లోడ్ పూర్తయింది</string>\n  <string name=\"exo_download_failed\">డౌన్‌లోడ్ విఫలమైంది</string>\n  <string name=\"exo_download_removing\">డౌన్‌లోడ్‌లను తీసివేస్తోంది</string>\n  <string name=\"exo_track_selection_title_video\">వీడియో</string>\n  <string name=\"exo_track_selection_title_audio\">ఆడియో</string>\n  <string name=\"exo_track_selection_title_text\">వచనం</string>\n  <string name=\"exo_track_selection_none\">ఏదీ కాదు</string>\n  <string name=\"exo_track_selection_auto\">స్వీయ</string>\n  <string name=\"exo_track_unknown\">తెలియదు</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">మోనో</string>\n  <string name=\"exo_track_stereo\">స్టీరియో</string>\n  <string name=\"exo_track_surround\">సరౌండ్ ధ్వని</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 సరౌండ్ ధ్వని</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 సరౌండ్ ధ్వని</string>\n  <string name=\"exo_track_role_alternate\">ప్రత్యామ్నాయం</string>\n  <string name=\"exo_track_role_supplementary\">అనుబంధితం</string>\n  <string name=\"exo_track_role_commentary\">వ్యాఖ్యానం</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-th/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">แทร็กก่อนหน้า</string>\n  <string name=\"exo_controls_next_description\">แทร็กถัดไป</string>\n  <string name=\"exo_controls_pause_description\">หยุด</string>\n  <string name=\"exo_controls_play_description\">เล่น</string>\n  <string name=\"exo_controls_stop_description\">หยุด</string>\n  <string name=\"exo_controls_rewind_description\">กรอกลับ</string>\n  <string name=\"exo_controls_fastforward_description\">กรอไปข้างหน้า</string>\n  <string name=\"exo_controls_repeat_off_description\">ไม่เล่นซ้ำ</string>\n  <string name=\"exo_controls_repeat_one_description\">เล่นซ้ำเพลงเดียว</string>\n  <string name=\"exo_controls_repeat_all_description\">เล่นซ้ำทั้งหมด</string>\n  <string name=\"exo_controls_shuffle_on_description\">เปิดการสุ่มเพลงอยู่</string>\n  <string name=\"exo_controls_shuffle_off_description\">ปิดการสุ่มเพลงอยู่</string>\n  <string name=\"exo_controls_fullscreen_description\">โหมดเต็มหน้าจอ</string>\n  <string name=\"exo_controls_vr_description\">โหมด VR</string>\n  <string name=\"exo_download_description\">ดาวน์โหลด</string>\n  <string name=\"exo_download_notification_channel_name\">ดาวน์โหลด</string>\n  <string name=\"exo_download_downloading\">กำลังดาวน์โหลด</string>\n  <string name=\"exo_download_completed\">การดาวน์โหลดเสร็จสมบูรณ์</string>\n  <string name=\"exo_download_failed\">การดาวน์โหลดล้มเหลว</string>\n  <string name=\"exo_download_removing\">กำลังนำรายการที่ดาวน์โหลดออก</string>\n  <string name=\"exo_track_selection_title_video\">วิดีโอ</string>\n  <string name=\"exo_track_selection_title_audio\">เสียง</string>\n  <string name=\"exo_track_selection_title_text\">ข้อความ</string>\n  <string name=\"exo_track_selection_none\">ไม่มี</string>\n  <string name=\"exo_track_selection_auto\">ยานยนต์</string>\n  <string name=\"exo_track_unknown\">ไม่ทราบ</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">โมโน</string>\n  <string name=\"exo_track_stereo\">สเตอริโอ</string>\n  <string name=\"exo_track_surround\">เสียงเซอร์ราวด์</string>\n  <string name=\"exo_track_surround_5_point_1\">ระบบเสียง 5.1 เซอร์ราวด์</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 เสียงเซอร์ราวด์</string>\n  <string name=\"exo_track_role_alternate\">อื่นๆ</string>\n  <string name=\"exo_track_role_supplementary\">แทร็กเสริม</string>\n  <string name=\"exo_track_role_commentary\">ความคิดเห็น</string>\n  <string name=\"exo_track_role_closed_captions\">คำบรรยาย</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-tl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Nakaraang track</string>\n  <string name=\"exo_controls_next_description\">Susunod na track</string>\n  <string name=\"exo_controls_pause_description\">I-pause</string>\n  <string name=\"exo_controls_play_description\">I-play</string>\n  <string name=\"exo_controls_stop_description\">Ihinto</string>\n  <string name=\"exo_controls_rewind_description\">I-rewind</string>\n  <string name=\"exo_controls_fastforward_description\">I-fast forward</string>\n  <string name=\"exo_controls_repeat_off_description\">Walang uulitin</string>\n  <string name=\"exo_controls_repeat_one_description\">Mag-ulit ng isa</string>\n  <string name=\"exo_controls_repeat_all_description\">Ulitin lahat</string>\n  <string name=\"exo_controls_shuffle_on_description\">Naka-on ang pag-shuffle</string>\n  <string name=\"exo_controls_shuffle_off_description\">Naka-off ang pag-shuffle</string>\n  <string name=\"exo_controls_fullscreen_description\">Fullscreen mode</string>\n  <string name=\"exo_controls_vr_description\">VR mode</string>\n  <string name=\"exo_download_description\">I-download</string>\n  <string name=\"exo_download_notification_channel_name\">Mga Download</string>\n  <string name=\"exo_download_downloading\">Nagda-download</string>\n  <string name=\"exo_download_completed\">Tapos na ang pag-download</string>\n  <string name=\"exo_download_failed\">Hindi na-download</string>\n  <string name=\"exo_download_removing\">Inaalis ang mga na-download</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Text</string>\n  <string name=\"exo_track_selection_none\">Wala</string>\n  <string name=\"exo_track_selection_auto\">Awtomatiko</string>\n  <string name=\"exo_track_unknown\">Hindi Alam</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Surround sound</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 na surround sound</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 na surround sound</string>\n  <string name=\"exo_track_role_alternate\">Alternatibo</string>\n  <string name=\"exo_track_role_supplementary\">Karagdagan</string>\n  <string name=\"exo_track_role_commentary\">Komentaryo</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-tr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Önceki parça</string>\n  <string name=\"exo_controls_next_description\">Sonraki parça</string>\n  <string name=\"exo_controls_pause_description\">Duraklat</string>\n  <string name=\"exo_controls_play_description\">Çal</string>\n  <string name=\"exo_controls_stop_description\">Durdur</string>\n  <string name=\"exo_controls_rewind_description\">Geri sar</string>\n  <string name=\"exo_controls_fastforward_description\">İleri sar</string>\n  <string name=\"exo_controls_repeat_off_description\">Hiçbirini tekrarlama</string>\n  <string name=\"exo_controls_repeat_one_description\">Birini tekrarla</string>\n  <string name=\"exo_controls_repeat_all_description\">Tümünü tekrarla</string>\n  <string name=\"exo_controls_shuffle_on_description\">Karıştırma açık</string>\n  <string name=\"exo_controls_shuffle_off_description\">Karıştırma kapalı</string>\n  <string name=\"exo_controls_fullscreen_description\">Tam ekran modu</string>\n  <string name=\"exo_controls_vr_description\">VR modu</string>\n  <string name=\"exo_download_description\">İndir</string>\n  <string name=\"exo_download_notification_channel_name\">İndirilenler</string>\n  <string name=\"exo_download_downloading\">İndiriliyor</string>\n  <string name=\"exo_download_completed\">İndirme işlemi tamamlandı</string>\n  <string name=\"exo_download_failed\">İndirilemedi</string>\n  <string name=\"exo_download_removing\">İndirilenler kaldırılıyor</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Ses</string>\n  <string name=\"exo_track_selection_title_text\">Metin</string>\n  <string name=\"exo_track_selection_none\">Yok</string>\n  <string name=\"exo_track_selection_auto\">Otomatik</string>\n  <string name=\"exo_track_unknown\">Bilinmiyor</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Surround ses</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 surround ses</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 surround ses</string>\n  <string name=\"exo_track_role_alternate\">Alternatif</string>\n  <string name=\"exo_track_role_supplementary\">Ek kanal</string>\n  <string name=\"exo_track_role_commentary\">Anlatım</string>\n  <string name=\"exo_track_role_closed_captions\">Altyazı</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-uk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Попередня композиція</string>\n  <string name=\"exo_controls_next_description\">Наступна композиція</string>\n  <string name=\"exo_controls_pause_description\">Призупинити</string>\n  <string name=\"exo_controls_play_description\">Відтворити</string>\n  <string name=\"exo_controls_stop_description\">Припинити</string>\n  <string name=\"exo_controls_rewind_description\">Перемотати назад</string>\n  <string name=\"exo_controls_fastforward_description\">Перемотати вперед</string>\n  <string name=\"exo_controls_repeat_off_description\">Не повторювати</string>\n  <string name=\"exo_controls_repeat_one_description\">Повторити 1</string>\n  <string name=\"exo_controls_repeat_all_description\">Повторити всі</string>\n  <string name=\"exo_controls_shuffle_on_description\">Перемішування ввімкнено</string>\n  <string name=\"exo_controls_shuffle_off_description\">Перемішування вимкнено</string>\n  <string name=\"exo_controls_fullscreen_description\">Повноекранний режим</string>\n  <string name=\"exo_controls_vr_description\">Режим віртуальної реальності</string>\n  <string name=\"exo_download_description\">Завантажити</string>\n  <string name=\"exo_download_notification_channel_name\">Завантаження</string>\n  <string name=\"exo_download_downloading\">Завантажується</string>\n  <string name=\"exo_download_completed\">Завантаження завершено</string>\n  <string name=\"exo_download_failed\">Не вдалося завантажити</string>\n  <string name=\"exo_download_removing\">Завантаження видаляються</string>\n  <string name=\"exo_track_selection_title_video\">Відео</string>\n  <string name=\"exo_track_selection_title_audio\">Аудіо</string>\n  <string name=\"exo_track_selection_title_text\">Текст</string>\n  <string name=\"exo_track_selection_none\">Нічого</string>\n  <string name=\"exo_track_selection_auto\">Автоматично</string>\n  <string name=\"exo_track_unknown\">Невідомо</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Моно</string>\n  <string name=\"exo_track_stereo\">Стерео</string>\n  <string name=\"exo_track_surround\">Об’ємний звук</string>\n  <string name=\"exo_track_surround_5_point_1\">Об’ємний звук у форматі 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Об’ємний звук у форматі 7.1</string>\n  <string name=\"exo_track_role_alternate\">Альтернативна</string>\n  <string name=\"exo_track_role_supplementary\">Додаткова</string>\n  <string name=\"exo_track_role_commentary\">Коментарі</string>\n  <string name=\"exo_track_role_closed_captions\">Субтитри</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Мбіт/с</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-ur/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">پچھلا ٹریک</string>\n  <string name=\"exo_controls_next_description\">اگلا ٹریک</string>\n  <string name=\"exo_controls_pause_description\">موقوف کریں</string>\n  <string name=\"exo_controls_play_description\">چلائیں</string>\n  <string name=\"exo_controls_stop_description\">روکیں</string>\n  <string name=\"exo_controls_rewind_description\">ریوائینڈ کریں</string>\n  <string name=\"exo_controls_fastforward_description\">تیزی سے فارورڈ کریں</string>\n  <string name=\"exo_controls_repeat_off_description\">کسی کو نہ دہرائیں</string>\n  <string name=\"exo_controls_repeat_one_description\">ایک کو دہرائیں</string>\n  <string name=\"exo_controls_repeat_all_description\">سبھی کو دہرائیں</string>\n  <string name=\"exo_controls_shuffle_on_description\">شفل آن</string>\n  <string name=\"exo_controls_shuffle_off_description\">شفل آف</string>\n  <string name=\"exo_controls_fullscreen_description\">پوری اسکرین والی وضع</string>\n  <string name=\"exo_controls_vr_description\">VR موڈ</string>\n  <string name=\"exo_download_description\">ڈاؤن لوڈ کریں</string>\n  <string name=\"exo_download_notification_channel_name\">ڈاؤن لوڈز</string>\n  <string name=\"exo_download_downloading\">ڈاؤن لوڈ ہو رہا ہے</string>\n  <string name=\"exo_download_completed\">ڈاؤن لوڈ مکمل ہو گیا</string>\n  <string name=\"exo_download_failed\">ڈاؤن لوڈ ناکام ہو گیا</string>\n  <string name=\"exo_download_removing\">ڈاؤن لوڈز کو ہٹایا جا رہا ہے</string>\n  <string name=\"exo_track_selection_title_video\">ویڈیو</string>\n  <string name=\"exo_track_selection_title_audio\">آڈیو</string>\n  <string name=\"exo_track_selection_title_text\">متن</string>\n  <string name=\"exo_track_selection_none\">کوئی نہیں</string>\n  <string name=\"exo_track_selection_auto\">خودکار</string>\n  <string name=\"exo_track_unknown\">نامعلوم</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">مونو</string>\n  <string name=\"exo_track_stereo\">اسٹیریو</string>\n  <string name=\"exo_track_surround\">محیط آواز</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 محیط آواز</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 محیط آواز</string>\n  <string name=\"exo_track_role_alternate\">متبادل</string>\n  <string name=\"exo_track_role_supplementary\">اضافی</string>\n  <string name=\"exo_track_role_commentary\">تبصرہ</string>\n  <string name=\"exo_track_role_closed_captions\">سب ٹائٹلز</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s، %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-uz/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Avvalgi trek</string>\n  <string name=\"exo_controls_next_description\">Keyingi trek</string>\n  <string name=\"exo_controls_pause_description\">Pauza</string>\n  <string name=\"exo_controls_play_description\">Ijro</string>\n  <string name=\"exo_controls_stop_description\">To‘xtatish</string>\n  <string name=\"exo_controls_rewind_description\">Orqaga qaytarish</string>\n  <string name=\"exo_controls_fastforward_description\">Oldinga o‘tkazish</string>\n  <string name=\"exo_controls_repeat_off_description\">Takrorlanmasin</string>\n  <string name=\"exo_controls_repeat_one_description\">Bittasini takrorlash</string>\n  <string name=\"exo_controls_repeat_all_description\">Hammasini takrorlash</string>\n  <string name=\"exo_controls_shuffle_on_description\">Tasodifiy ijro yoqilgan</string>\n  <string name=\"exo_controls_shuffle_off_description\">Tasodifiy ijro yoqilmagan</string>\n  <string name=\"exo_controls_fullscreen_description\">Butun ekran rejimi</string>\n  <string name=\"exo_controls_vr_description\">VR rejimi</string>\n  <string name=\"exo_download_description\">Yuklab olish</string>\n  <string name=\"exo_download_notification_channel_name\">Yuklanmalar</string>\n  <string name=\"exo_download_downloading\">Yuklab olinmoqda</string>\n  <string name=\"exo_download_completed\">Yuklab olindi</string>\n  <string name=\"exo_download_failed\">Yuklab olinmadi</string>\n  <string name=\"exo_download_removing\">Yuklanmalar olib tashlanmoqda</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Audio</string>\n  <string name=\"exo_track_selection_title_text\">Matn</string>\n  <string name=\"exo_track_selection_none\">Hech qanday</string>\n  <string name=\"exo_track_selection_auto\">Avtomatik</string>\n  <string name=\"exo_track_unknown\">Noaniq</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Mono</string>\n  <string name=\"exo_track_stereo\">Stereo</string>\n  <string name=\"exo_track_surround\">Qamrovli ovoz</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 qamrovli ovoz</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 qamrovli ovoz</string>\n  <string name=\"exo_track_role_alternate\">Muqobil</string>\n  <string name=\"exo_track_role_supplementary\">Qoʻshimcha</string>\n  <string name=\"exo_track_role_commentary\">Sharh</string>\n  <string name=\"exo_track_role_closed_captions\">Taglavhalar</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbit/s</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-vi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Bản nhạc trước</string>\n  <string name=\"exo_controls_next_description\">Bản nhạc tiếp theo</string>\n  <string name=\"exo_controls_pause_description\">Tạm dừng</string>\n  <string name=\"exo_controls_play_description\">Phát</string>\n  <string name=\"exo_controls_stop_description\">Dừng</string>\n  <string name=\"exo_controls_rewind_description\">Tua lại</string>\n  <string name=\"exo_controls_fastforward_description\">Tua đi</string>\n  <string name=\"exo_controls_repeat_off_description\">Không lặp lại</string>\n  <string name=\"exo_controls_repeat_one_description\">Lặp lại một</string>\n  <string name=\"exo_controls_repeat_all_description\">Lặp lại tất cả</string>\n  <string name=\"exo_controls_shuffle_on_description\">Chế độ trộn bài đang bật</string>\n  <string name=\"exo_controls_shuffle_off_description\">Chế độ trộn bài đang tắt</string>\n  <string name=\"exo_controls_fullscreen_description\">Chế độ toàn màn hình</string>\n  <string name=\"exo_controls_vr_description\">Chế độ thực tế ảo</string>\n  <string name=\"exo_download_description\">Tải xuống</string>\n  <string name=\"exo_download_notification_channel_name\">Tài nguyên đã tải xuống</string>\n  <string name=\"exo_download_downloading\">Đang tải xuống</string>\n  <string name=\"exo_download_completed\">Đã hoàn tất tải xuống</string>\n  <string name=\"exo_download_failed\">Không tải xuống được</string>\n  <string name=\"exo_download_removing\">Đang xóa các mục đã tải xuống</string>\n  <string name=\"exo_track_selection_title_video\">Video</string>\n  <string name=\"exo_track_selection_title_audio\">Âm thanh</string>\n  <string name=\"exo_track_selection_title_text\">Văn bản</string>\n  <string name=\"exo_track_selection_none\">Không</string>\n  <string name=\"exo_track_selection_auto\">Tự động</string>\n  <string name=\"exo_track_unknown\">Không xác định</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Đơn âm</string>\n  <string name=\"exo_track_stereo\">Âm thanh nổi</string>\n  <string name=\"exo_track_surround\">Âm thanh vòm</string>\n  <string name=\"exo_track_surround_5_point_1\">Âm thanh vòm 5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Âm thanh vòm 7.1</string>\n  <string name=\"exo_track_role_alternate\">Thay thế</string>\n  <string name=\"exo_track_role_supplementary\">Bổ sung</string>\n  <string name=\"exo_track_role_commentary\">Bình luận</string>\n  <string name=\"exo_track_role_closed_captions\">Phụ đề</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mb/giây</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-zh-rCN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">上一曲</string>\n  <string name=\"exo_controls_next_description\">下一曲</string>\n  <string name=\"exo_controls_pause_description\">暂停</string>\n  <string name=\"exo_controls_play_description\">播放</string>\n  <string name=\"exo_controls_stop_description\">停止</string>\n  <string name=\"exo_controls_rewind_description\">快退</string>\n  <string name=\"exo_controls_fastforward_description\">快进</string>\n  <string name=\"exo_controls_repeat_off_description\">不重复播放</string>\n  <string name=\"exo_controls_repeat_one_description\">重复播放一项</string>\n  <string name=\"exo_controls_repeat_all_description\">全部重复播放</string>\n  <string name=\"exo_controls_shuffle_on_description\">随机播放功能已开启</string>\n  <string name=\"exo_controls_shuffle_off_description\">随机播放功能已关闭</string>\n  <string name=\"exo_controls_fullscreen_description\">全屏模式</string>\n  <string name=\"exo_controls_vr_description\">VR 模式</string>\n  <string name=\"exo_download_description\">下载</string>\n  <string name=\"exo_download_notification_channel_name\">下载内容</string>\n  <string name=\"exo_download_downloading\">正在下载</string>\n  <string name=\"exo_download_completed\">下载完毕</string>\n  <string name=\"exo_download_failed\">下载失败</string>\n  <string name=\"exo_download_removing\">正在移除下载内容</string>\n  <string name=\"exo_track_selection_title_video\">视频</string>\n  <string name=\"exo_track_selection_title_audio\">音频</string>\n  <string name=\"exo_track_selection_title_text\">文字</string>\n  <string name=\"exo_track_selection_none\">无</string>\n  <string name=\"exo_track_selection_auto\">自动</string>\n  <string name=\"exo_track_unknown\">未知</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">单声道</string>\n  <string name=\"exo_track_stereo\">立体声</string>\n  <string name=\"exo_track_surround\">环绕声</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 环绕声</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 环绕声</string>\n  <string name=\"exo_track_role_alternate\">替用轨</string>\n  <string name=\"exo_track_role_supplementary\">补充轨</string>\n  <string name=\"exo_track_role_commentary\">旁白</string>\n  <string name=\"exo_track_role_closed_captions\">字幕</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s，%2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-zh-rHK/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">上一首曲目</string>\n  <string name=\"exo_controls_next_description\">下一首曲目</string>\n  <string name=\"exo_controls_pause_description\">暫停</string>\n  <string name=\"exo_controls_play_description\">播放</string>\n  <string name=\"exo_controls_stop_description\">停止</string>\n  <string name=\"exo_controls_rewind_description\">倒轉</string>\n  <string name=\"exo_controls_fastforward_description\">向前快轉</string>\n  <string name=\"exo_controls_repeat_off_description\">不重複播放</string>\n  <string name=\"exo_controls_repeat_one_description\">重複播放單一項目</string>\n  <string name=\"exo_controls_repeat_all_description\">全部重複播放</string>\n  <string name=\"exo_controls_shuffle_on_description\">已開啟隨機播放功能</string>\n  <string name=\"exo_controls_shuffle_off_description\">已關閉隨機播放功能</string>\n  <string name=\"exo_controls_fullscreen_description\">全螢幕模式</string>\n  <string name=\"exo_controls_vr_description\">虛擬現實模式</string>\n  <string name=\"exo_download_description\">下載</string>\n  <string name=\"exo_download_notification_channel_name\">下載內容</string>\n  <string name=\"exo_download_downloading\">正在下載</string>\n  <string name=\"exo_download_completed\">下載完畢</string>\n  <string name=\"exo_download_failed\">下載失敗</string>\n  <string name=\"exo_download_removing\">正在移除下載內容</string>\n  <string name=\"exo_track_selection_title_video\">影片</string>\n  <string name=\"exo_track_selection_title_audio\">音訊</string>\n  <string name=\"exo_track_selection_title_text\">文字</string>\n  <string name=\"exo_track_selection_none\">無</string>\n  <string name=\"exo_track_selection_auto\">自動</string>\n  <string name=\"exo_track_unknown\">不明</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">單聲道</string>\n  <string name=\"exo_track_stereo\">立體聲</string>\n  <string name=\"exo_track_surround\">環迴立體聲</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 環迴立體聲</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 環迴立體聲</string>\n  <string name=\"exo_track_role_alternate\">其他</string>\n  <string name=\"exo_track_role_supplementary\">附加</string>\n  <string name=\"exo_track_role_commentary\">評論</string>\n  <string name=\"exo_track_role_closed_captions\">字幕</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s、%2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-zh-rTW/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">上一首曲目</string>\n  <string name=\"exo_controls_next_description\">下一首曲目</string>\n  <string name=\"exo_controls_pause_description\">暫停</string>\n  <string name=\"exo_controls_play_description\">播放</string>\n  <string name=\"exo_controls_stop_description\">停止</string>\n  <string name=\"exo_controls_rewind_description\">倒轉</string>\n  <string name=\"exo_controls_fastforward_description\">快轉</string>\n  <string name=\"exo_controls_repeat_off_description\">不重複播放</string>\n  <string name=\"exo_controls_repeat_one_description\">重複播放單一項目</string>\n  <string name=\"exo_controls_repeat_all_description\">重複播放所有項目</string>\n  <string name=\"exo_controls_shuffle_on_description\">隨機播放已開啟</string>\n  <string name=\"exo_controls_shuffle_off_description\">隨機播放已關閉</string>\n  <string name=\"exo_controls_fullscreen_description\">全螢幕模式</string>\n  <string name=\"exo_controls_vr_description\">虛擬實境模式</string>\n  <string name=\"exo_download_description\">下載</string>\n  <string name=\"exo_download_notification_channel_name\">下載</string>\n  <string name=\"exo_download_downloading\">下載中</string>\n  <string name=\"exo_download_completed\">下載完成</string>\n  <string name=\"exo_download_failed\">無法下載</string>\n  <string name=\"exo_download_removing\">正在移除下載內容</string>\n  <string name=\"exo_track_selection_title_video\">影片</string>\n  <string name=\"exo_track_selection_title_audio\">音訊</string>\n  <string name=\"exo_track_selection_title_text\">文字</string>\n  <string name=\"exo_track_selection_none\">無</string>\n  <string name=\"exo_track_selection_auto\">自動</string>\n  <string name=\"exo_track_unknown\">不明</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">單聲道</string>\n  <string name=\"exo_track_stereo\">立體聲</string>\n  <string name=\"exo_track_surround\">環繞音效</string>\n  <string name=\"exo_track_surround_5_point_1\">5.1 環繞音效</string>\n  <string name=\"exo_track_surround_7_point_1\">7.1 環繞音效</string>\n  <string name=\"exo_track_role_alternate\">替用軌</string>\n  <string name=\"exo_track_role_supplementary\">補充軌</string>\n  <string name=\"exo_track_role_commentary\">旁白</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s、%2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/main/res/values-zu/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n  <string name=\"exo_controls_previous_description\">Ithrekhi yangaphambilini</string>\n  <string name=\"exo_controls_next_description\">Ithrekhi elandelayo</string>\n  <string name=\"exo_controls_pause_description\">Phumula</string>\n  <string name=\"exo_controls_play_description\">Dlala</string>\n  <string name=\"exo_controls_stop_description\">Misa</string>\n  <string name=\"exo_controls_rewind_description\">Buyisela emuva</string>\n  <string name=\"exo_controls_fastforward_description\">Dlulisela phambili</string>\n  <string name=\"exo_controls_repeat_off_description\">Phinda okungekho</string>\n  <string name=\"exo_controls_repeat_one_description\">Phinda okukodwa</string>\n  <string name=\"exo_controls_repeat_all_description\">Phinda konke</string>\n  <string name=\"exo_controls_shuffle_on_description\">Ukushova kuvuliwe</string>\n  <string name=\"exo_controls_shuffle_off_description\">Ukushova kuvaliwe</string>\n  <string name=\"exo_controls_fullscreen_description\">Imodi yesikrini esigcwele</string>\n  <string name=\"exo_controls_vr_description\">Inqubo ye-VR</string>\n  <string name=\"exo_download_description\">Landa</string>\n  <string name=\"exo_download_notification_channel_name\">Ukulandwa</string>\n  <string name=\"exo_download_downloading\">Iyalanda</string>\n  <string name=\"exo_download_completed\">Ukulanda kuqedile</string>\n  <string name=\"exo_download_failed\">Ukulanda kuhlulekile</string>\n  <string name=\"exo_download_removing\">Kususwa okulandiwe</string>\n  <string name=\"exo_track_selection_title_video\">Ividiyo</string>\n  <string name=\"exo_track_selection_title_audio\">Umsindo</string>\n  <string name=\"exo_track_selection_title_text\">Umbhalo</string>\n  <string name=\"exo_track_selection_none\">Lutho</string>\n  <string name=\"exo_track_selection_auto\">Okuzenzakalelayo</string>\n  <string name=\"exo_track_unknown\">Akwaziwa</string>\n  <string name=\"exo_track_resolution\">%1$d × %2$d</string>\n  <string name=\"exo_track_mono\">Okukodwa</string>\n  <string name=\"exo_track_stereo\">I-Stereo</string>\n  <string name=\"exo_track_surround\">Umsindo ozungelezile</string>\n  <string name=\"exo_track_surround_5_point_1\">Umsindo ozungelezile ongu-5.1</string>\n  <string name=\"exo_track_surround_7_point_1\">Umsindo ozungelezile ongu-7.1</string>\n  <string name=\"exo_track_role_alternate\">Okunye</string>\n  <string name=\"exo_track_role_supplementary\">Okungeziwe</string>\n  <string name=\"exo_track_role_commentary\">Abahlaziyi</string>\n  <string name=\"exo_track_role_closed_captions\">CC</string>\n  <string name=\"exo_track_bitrate\">%1$.2f Mbps</string>\n  <string name=\"exo_item_list\">%1$s, %2$s</string>\n</resources>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\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\n<manifest package=\"com.google.android.exoplayer2.source.ui.test\" />\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/test/java/com/google/android/exoplayer2/ui/spherical/CanvasRendererTest.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.ui.spherical;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.graphics.PointF;\nimport androidx.annotation.Nullable;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link CanvasRenderer}. */\n@RunWith(AndroidJUnit4.class)\npublic class CanvasRendererTest {\n\n  private static final float JUST_BELOW_45_DEGREES = (float) (Math.PI / 4 - 1.0E-08);\n  private static final float JUST_ABOVE_45_DEGREES = (float) (Math.PI / 4 + 1.0E-08);\n  private static final float TOLERANCE = .00001f;\n\n  @Test\n  public void testClicksOnCanvas() {\n    assertClick(translateClick(JUST_BELOW_45_DEGREES, JUST_BELOW_45_DEGREES), 0, 0);\n    assertClick(translateClick(JUST_BELOW_45_DEGREES, -JUST_BELOW_45_DEGREES), 0, 100);\n    assertClick(translateClick(0, 0), 50, 50);\n    assertClick(translateClick(-JUST_BELOW_45_DEGREES, JUST_BELOW_45_DEGREES), 100, 0);\n    assertClick(translateClick(-JUST_BELOW_45_DEGREES, -JUST_BELOW_45_DEGREES), 100, 100);\n  }\n\n  @Test\n  public void testClicksNotOnCanvas() {\n    assertThat(translateClick(JUST_ABOVE_45_DEGREES, JUST_ABOVE_45_DEGREES)).isNull();\n    assertThat(translateClick(JUST_ABOVE_45_DEGREES, -JUST_ABOVE_45_DEGREES)).isNull();\n    assertThat(translateClick(-JUST_ABOVE_45_DEGREES, JUST_ABOVE_45_DEGREES)).isNull();\n    assertThat(translateClick(-JUST_ABOVE_45_DEGREES, -JUST_ABOVE_45_DEGREES)).isNull();\n    assertThat(translateClick((float) (Math.PI / 2), 0)).isNull();\n    assertThat(translateClick(0, (float) Math.PI)).isNull();\n  }\n\n  private static PointF translateClick(float yaw, float pitch) {\n    return CanvasRenderer.internalTranslateClick(\n        yaw,\n        pitch,\n        /* xUnit= */ -1,\n        /* yUnit= */ -1,\n        /* widthUnit= */ 2,\n        /* heightUnit= */ 2,\n        /* widthPixel= */ 100,\n        /* heightPixel= */ 100);\n  }\n\n  private static void assertClick(@Nullable PointF actual, float expectedX, float expectedY) {\n    assertThat(actual).isNotNull();\n    assertThat(actual.x).isWithin(TOLERANCE).of(expectedX);\n    assertThat(actual.y).isWithin(TOLERANCE).of(expectedY);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/library/ui/src/test/java/com/google/android/exoplayer2/ui/spherical/TouchTrackerTest.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.ui.spherical;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.content.Context;\nimport android.view.MotionEvent;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests for {@link TouchTracker}. */\n@RunWith(AndroidJUnit4.class)\npublic class TouchTrackerTest {\n  private static final float EPSILON = 0.00001f;\n  private static final int SWIPE_PX = 100;\n  private static final float PX_PER_DEGREES = 25;\n\n  private TouchTracker tracker;\n  private float yaw;\n  private float pitch;\n  private float[] dummyMatrix;\n\n  private static void swipe(TouchTracker tracker, float x0, float y0, float x1, float y1) {\n    tracker.onTouch(null, MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, x0, y0, 0));\n    tracker.onTouch(null, MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, x1, y1, 0));\n    tracker.onTouch(null, MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, x1, y1, 0));\n  }\n\n  @Before\n  public void setUp() {\n    Context context = ApplicationProvider.getApplicationContext();\n    tracker =\n        new TouchTracker(\n            context,\n            scrollOffsetDegrees -> {\n              pitch = scrollOffsetDegrees.y;\n              yaw = scrollOffsetDegrees.x;\n            },\n            PX_PER_DEGREES);\n    dummyMatrix = new float[16];\n    tracker.onOrientationChange(dummyMatrix, 0);\n  }\n\n  @Test\n  public void testTap() {\n    // Tap is a noop.\n    swipe(tracker, 0, 0, 0, 0);\n    assertThat(yaw).isWithin(EPSILON).of(0);\n    assertThat(pitch).isWithin(EPSILON).of(0);\n  }\n\n  @Test\n  public void testBasicYaw() {\n    swipe(tracker, 0, 0, SWIPE_PX, 0);\n    assertThat(yaw).isWithin(EPSILON).of(-SWIPE_PX / PX_PER_DEGREES);\n    assertThat(pitch).isWithin(EPSILON).of(0);\n  }\n\n  @Test\n  public void testBigYaw() {\n    swipe(tracker, 0, 0, -10 * SWIPE_PX, 0);\n    assertThat(yaw).isEqualTo(10 * SWIPE_PX / PX_PER_DEGREES);\n    assertThat(pitch).isWithin(EPSILON).of(0);\n  }\n\n  @Test\n  public void testYawUnaffectedByPitch() {\n    swipe(tracker, 0, 0, 0, SWIPE_PX);\n    assertThat(yaw).isWithin(EPSILON).of(0);\n\n    swipe(tracker, 0, 0, SWIPE_PX, SWIPE_PX);\n    assertThat(yaw).isWithin(EPSILON).of(-SWIPE_PX / PX_PER_DEGREES);\n  }\n\n  @Test\n  public void testBasicPitch() {\n    swipe(tracker, 0, 0, 0, SWIPE_PX);\n    assertThat(yaw).isWithin(EPSILON).of(0);\n    assertThat(pitch).isWithin(EPSILON).of(SWIPE_PX / PX_PER_DEGREES);\n  }\n\n  @Test\n  public void testPitchClipped() {\n    // Big reverse pitch should be clipped.\n    swipe(tracker, 0, 0, 0, -20 * SWIPE_PX);\n    assertThat(yaw).isWithin(EPSILON).of(0);\n    assertThat(pitch).isEqualTo(-TouchTracker.MAX_PITCH_DEGREES);\n\n    // Big forward pitch should be clipped.\n    swipe(tracker, 0, 0, 0, 50 * SWIPE_PX);\n    assertThat(yaw).isWithin(EPSILON).of(0);\n    assertThat(pitch).isEqualTo(TouchTracker.MAX_PITCH_DEGREES);\n  }\n\n  @Test\n  public void testWithRoll90() {\n    tracker.onOrientationChange(dummyMatrix, (float) Math.toRadians(90));\n\n    // Y-axis should now control yaw.\n    swipe(tracker, 0, 0, 0, 2 * SWIPE_PX);\n    assertThat(yaw).isWithin(EPSILON).of(-2 * SWIPE_PX / PX_PER_DEGREES);\n\n    // X-axis should now control reverse pitch.\n    swipe(tracker, 0, 0, -3 * SWIPE_PX, 0);\n    assertThat(pitch).isWithin(EPSILON).of(3 * SWIPE_PX / PX_PER_DEGREES);\n  }\n\n  @Test\n  public void testWithRoll180() {\n    tracker.onOrientationChange(dummyMatrix, (float) Math.toRadians(180));\n\n    // X-axis should now control reverse yaw.\n    swipe(tracker, 0, 0, -2 * SWIPE_PX, 0);\n    assertThat(yaw).isWithin(EPSILON).of(-2 * SWIPE_PX / PX_PER_DEGREES);\n\n    // Y-axis should now control reverse pitch.\n    swipe(tracker, 0, 0, 0, -3 * SWIPE_PX);\n    assertThat(pitch).isWithin(EPSILON).of(3 * SWIPE_PX / PX_PER_DEGREES);\n  }\n\n  @Test\n  public void testWithRoll270() {\n    tracker.onOrientationChange(dummyMatrix, (float) Math.toRadians(270));\n\n    // Y-axis should now control reverse yaw.\n    swipe(tracker, 0, 0, 0, -2 * SWIPE_PX);\n    assertThat(yaw).isWithin(EPSILON).of(-2 * SWIPE_PX / PX_PER_DEGREES);\n\n    // X-axis should now control pitch.\n    swipe(tracker, 0, 0, 3 * SWIPE_PX, 0);\n    assertThat(pitch).isWithin(EPSILON).of(3 * SWIPE_PX / PX_PER_DEGREES);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/playbacktests/build.gradle",
    "content": "// Copyright (C) 2016 The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\napply from: '../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n        testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    androidTestImplementation 'androidx.test:rules:' + androidXTestVersion\n    androidTestImplementation 'androidx.test:runner:' + androidXTestVersion\n    androidTestImplementation 'androidx.annotation:annotation:1.1.0'\n    androidTestImplementation project(modulePrefix + 'library-core')\n    androidTestImplementation project(modulePrefix + 'library-dash')\n    androidTestImplementation project(modulePrefix + 'library-hls')\n    androidTestImplementation project(modulePrefix + 'testutils')\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/playbacktests/src/androidTest/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"com.google.android.exoplayer2.playbacktests\">\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.WAKE_LOCK\"/>\n  <uses-sdk/>\n\n  <application\n      android:allowBackup=\"false\"\n      tools:ignore=\"MissingApplicationIcon,HardcodedDebugMode\">\n    <uses-library android:name=\"android.test.runner\"/>\n\n    <activity android:name=\"com.google.android.exoplayer2.testutil.HostActivity\"\n        android:configChanges=\"keyboardHidden|orientation|screenSize\"\n        android:label=\"ExoPlayerTest\"/>\n\n  </application>\n\n  <instrumentation\n      android:targetPackage=\"com.google.android.exoplayer2.playbacktests\"\n      android:name=\"androidx.test.runner.AndroidJUnitRunner\"/>\n\n</manifest>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.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.playbacktests.gts;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport androidx.test.rule.ActivityTestRule;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.testutil.ActionSchedule;\nimport com.google.android.exoplayer2.testutil.HostActivity;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Test playback of encrypted DASH streams using different CENC scheme types. */\n@RunWith(AndroidJUnit4.class)\npublic final class CommonEncryptionDrmTest {\n\n  private static final String TAG = \"CencDrmTest\";\n\n  private static final String ID_AUDIO = \"0\";\n  private static final String[] IDS_VIDEO = new String[] {\"1\", \"2\"};\n\n  // Seeks help reproduce playback issues in certain devices.\n  private static final ActionSchedule ACTION_SCHEDULE_WITH_SEEKS = new ActionSchedule.Builder(TAG)\n      .waitForPlaybackState(Player.STATE_READY).delay(30000).seekAndWait(300000).delay(10000)\n      .seekAndWait(270000).delay(10000).seekAndWait(200000).delay(10000).seekAndWait(732000)\n      .build();\n\n  @Rule public ActivityTestRule<HostActivity> testRule = new ActivityTestRule<>(HostActivity.class);\n\n  private DashTestRunner testRunner;\n\n  @Before\n  public void setUp() {\n    testRunner =\n        new DashTestRunner(TAG, testRule.getActivity())\n            .setWidevineInfo(MimeTypes.VIDEO_H264, false)\n            .setActionSchedule(ACTION_SCHEDULE_WITH_SEEKS)\n            .setAudioVideoFormats(ID_AUDIO, IDS_VIDEO)\n            .setCanIncludeAdditionalVideoFormats(true);\n  }\n\n  @After\n  public void tearDown() {\n    testRunner = null;\n  }\n\n  @Test\n  public void testCencSchemeTypeV18() {\n    if (Util.SDK_INT < 18) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_h264_scheme_cenc\")\n        .setManifestUrl(DashTestData.WIDEVINE_SCHEME_CENC)\n        .run();\n  }\n\n  @Test\n  public void testCbc1SchemeTypeV25() {\n    if (Util.SDK_INT < 25) {\n      // cbc1 support was added in API 24, but it is stable from API 25 onwards.\n      // See [internal: b/65634809].\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_h264_scheme_cbc1\")\n        .setManifestUrl(DashTestData.WIDEVINE_SCHEME_CBC1)\n        .run();\n  }\n\n  @Test\n  public void testCbcsSchemeTypeV25() {\n    if (Util.SDK_INT < 25) {\n      // cbcs support was added in API 24, but it is stable from API 25 onwards.\n      // See [internal: b/65634809].\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_h264_scheme_cbcs\")\n        .setManifestUrl(DashTestData.WIDEVINE_SCHEME_CBCS)\n        .run();\n  }\n\n  @Test\n  public void testCensSchemeTypeV25() {\n    // TODO: Implement once content is available. Track [internal: b/31219813].\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.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.playbacktests.gts;\n\nimport static com.google.common.truth.Truth.assertWithMessage;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport androidx.test.rule.ActivityTestRule;\nimport com.google.android.exoplayer2.offline.DownloaderConstructorHelper;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.dash.DashUtil;\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.Representation;\nimport com.google.android.exoplayer2.source.dash.offline.DashDownloader;\nimport com.google.android.exoplayer2.testutil.HostActivity;\nimport com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;\nimport com.google.android.exoplayer2.upstream.DummyDataSource;\nimport com.google.android.exoplayer2.upstream.cache.CacheDataSource;\nimport com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;\nimport com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;\nimport com.google.android.exoplayer2.upstream.cache.SimpleCache;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests downloaded DASH playbacks. */\n@RunWith(AndroidJUnit4.class)\npublic final class DashDownloadTest {\n\n  private static final String TAG = \"DashDownloadTest\";\n\n  private static final Uri MANIFEST_URI = Uri.parse(DashTestData.H264_MANIFEST);\n\n  @Rule public ActivityTestRule<HostActivity> testRule = new ActivityTestRule<>(HostActivity.class);\n\n  private DashTestRunner testRunner;\n  private File tempFolder;\n  private SimpleCache cache;\n  private DefaultHttpDataSourceFactory httpDataSourceFactory;\n  private CacheDataSourceFactory offlineDataSourceFactory;\n\n  @Before\n  public void setUp() throws Exception {\n    testRunner =\n        new DashTestRunner(TAG, testRule.getActivity())\n            .setManifestUrl(DashTestData.H264_MANIFEST)\n            .setFullPlaybackNoSeeking(true)\n            .setCanIncludeAdditionalVideoFormats(false)\n            .setAudioVideoFormats(\n                DashTestData.AAC_AUDIO_REPRESENTATION_ID, DashTestData.H264_CDD_FIXED);\n    tempFolder = Util.createTempDirectory(testRule.getActivity(), \"ExoPlayerTest\");\n    cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());\n    httpDataSourceFactory = new DefaultHttpDataSourceFactory(\"ExoPlayer\", null);\n    offlineDataSourceFactory =\n        new CacheDataSourceFactory(\n            cache, DummyDataSource.FACTORY, CacheDataSource.FLAG_BLOCK_ON_CACHE);\n  }\n\n  @After\n  public void tearDown() {\n    testRunner = null;\n    Util.recursiveDelete(tempFolder);\n    cache = null;\n  }\n\n  // Download tests\n\n  @Test\n  public void testDownload() throws Exception {\n    DashDownloader dashDownloader = downloadContent();\n    dashDownloader.download(/* progressListener= */ null);\n\n    testRunner\n        .setStreamName(\"test_h264_fixed_download\")\n        .setDataSourceFactory(offlineDataSourceFactory)\n        .run();\n\n    dashDownloader.remove();\n\n    assertWithMessage(\"There should be no cache key left\").that(cache.getKeys()).isEmpty();\n    assertWithMessage(\"There should be no content left\").that(cache.getCacheSpace()).isEqualTo(0);\n  }\n\n  private DashDownloader downloadContent() throws Exception {\n    DashManifest dashManifest =\n        DashUtil.loadManifest(httpDataSourceFactory.createDataSource(), MANIFEST_URI);\n    ArrayList<StreamKey> keys = new ArrayList<>();\n    for (int pIndex = 0; pIndex < dashManifest.getPeriodCount(); pIndex++) {\n      List<AdaptationSet> adaptationSets = dashManifest.getPeriod(pIndex).adaptationSets;\n      for (int aIndex = 0; aIndex < adaptationSets.size(); aIndex++) {\n        AdaptationSet adaptationSet = adaptationSets.get(aIndex);\n        List<Representation> representations = adaptationSet.representations;\n        for (int rIndex = 0; rIndex < representations.size(); rIndex++) {\n          String id = representations.get(rIndex).format.id;\n          if (DashTestData.AAC_AUDIO_REPRESENTATION_ID.equals(id)\n              || DashTestData.H264_CDD_FIXED.equals(id)) {\n            keys.add(new StreamKey(pIndex, aIndex, rIndex));\n          }\n        }\n      }\n    }\n    DownloaderConstructorHelper constructorHelper =\n        new DownloaderConstructorHelper(cache, httpDataSourceFactory);\n    return new DashDownloader(MANIFEST_URI, keys, constructorHelper);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.playbacktests.gts;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport androidx.test.rule.ActivityTestRule;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecInfo;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;\nimport com.google.android.exoplayer2.testutil.ActionSchedule;\nimport com.google.android.exoplayer2.testutil.HostActivity;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests DASH playbacks using {@link ExoPlayer}. */\n@RunWith(AndroidJUnit4.class)\npublic final class DashStreamingTest {\n\n  private static final String TAG = \"DashStreamingTest\";\n\n  private static final ActionSchedule SEEKING_SCHEDULE = new ActionSchedule.Builder(TAG)\n      .waitForPlaybackState(Player.STATE_READY)\n      .delay(10000).seekAndWait(15000)\n      .delay(10000).seek(30000).seek(31000).seek(32000).seek(33000).seekAndWait(34000)\n      .delay(1000).pause().delay(1000).play()\n      .delay(1000).pause().seekAndWait(120000).delay(1000).play()\n      .build();\n  private static final ActionSchedule RENDERER_DISABLING_SCHEDULE = new ActionSchedule.Builder(TAG)\n      .waitForPlaybackState(Player.STATE_READY)\n      // Wait 10 seconds, disable the video renderer, wait another 10 seconds and enable it again.\n      .delay(10000).disableRenderer(DashTestRunner.VIDEO_RENDERER_INDEX)\n      .delay(10000).enableRenderer(DashTestRunner.VIDEO_RENDERER_INDEX)\n      // Ditto for the audio renderer.\n      .delay(10000).disableRenderer(DashTestRunner.AUDIO_RENDERER_INDEX)\n      .delay(10000).enableRenderer(DashTestRunner.AUDIO_RENDERER_INDEX)\n      // Wait 10 seconds, then disable and enable the video renderer 5 times in quick succession.\n      .delay(10000).disableRenderer(DashTestRunner.VIDEO_RENDERER_INDEX)\n      .enableRenderer(DashTestRunner.VIDEO_RENDERER_INDEX)\n      .disableRenderer(DashTestRunner.VIDEO_RENDERER_INDEX)\n      .enableRenderer(DashTestRunner.VIDEO_RENDERER_INDEX)\n      .disableRenderer(DashTestRunner.VIDEO_RENDERER_INDEX)\n      .enableRenderer(DashTestRunner.VIDEO_RENDERER_INDEX)\n      .disableRenderer(DashTestRunner.VIDEO_RENDERER_INDEX)\n      .enableRenderer(DashTestRunner.VIDEO_RENDERER_INDEX)\n      .disableRenderer(DashTestRunner.VIDEO_RENDERER_INDEX)\n      .enableRenderer(DashTestRunner.VIDEO_RENDERER_INDEX)\n      // Ditto for the audio renderer.\n      .delay(10000).disableRenderer(DashTestRunner.AUDIO_RENDERER_INDEX)\n      .enableRenderer(DashTestRunner.AUDIO_RENDERER_INDEX)\n      .disableRenderer(DashTestRunner.AUDIO_RENDERER_INDEX)\n      .enableRenderer(DashTestRunner.AUDIO_RENDERER_INDEX)\n      .disableRenderer(DashTestRunner.AUDIO_RENDERER_INDEX)\n      .enableRenderer(DashTestRunner.AUDIO_RENDERER_INDEX)\n      .disableRenderer(DashTestRunner.AUDIO_RENDERER_INDEX)\n      .enableRenderer(DashTestRunner.AUDIO_RENDERER_INDEX)\n      .disableRenderer(DashTestRunner.AUDIO_RENDERER_INDEX)\n      .enableRenderer(DashTestRunner.AUDIO_RENDERER_INDEX)\n      // Wait 10 seconds, detach the surface, wait another 10 seconds and attach it again.\n      .delay(10000).clearVideoSurface()\n      .delay(10000).setVideoSurface()\n      // Wait 10 seconds, then seek to near end.\n      .delay(10000).seek(120000)\n      .build();\n\n  @Rule public ActivityTestRule<HostActivity> testRule = new ActivityTestRule<>(HostActivity.class);\n\n  private DashTestRunner testRunner;\n\n  @Before\n  public void setUp() {\n    testRunner = new DashTestRunner(TAG, testRule.getActivity());\n  }\n\n  @After\n  public void tearDown() {\n    testRunner = null;\n  }\n\n  // H264 CDD.\n\n  @Test\n  public void testH264Fixed() {\n    testRunner\n        .setStreamName(\"test_h264_fixed\")\n        .setManifestUrl(DashTestData.H264_MANIFEST)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(false)\n        .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID, DashTestData.H264_CDD_FIXED)\n        .run();\n  }\n\n  @Test\n  public void testH264Adaptive() throws DecoderQueryException {\n    if (shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_h264_adaptive\")\n        .setManifestUrl(DashTestData.H264_MANIFEST)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.H264_CDD_ADAPTIVE)\n        .run();\n  }\n\n  @Test\n  public void testH264AdaptiveWithSeeking() throws DecoderQueryException {\n    if (shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) {\n      // Pass.\n      return;\n    }\n    final String streamName = \"test_h264_adaptive_with_seeking\";\n    testRunner\n        .setStreamName(streamName)\n        .setManifestUrl(DashTestData.H264_MANIFEST)\n        .setFullPlaybackNoSeeking(false)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setActionSchedule(SEEKING_SCHEDULE)\n        .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.H264_CDD_ADAPTIVE)\n        .run();\n  }\n\n  @Test\n  public void testH264AdaptiveWithRendererDisabling() throws DecoderQueryException {\n    if (shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) {\n      // Pass.\n      return;\n    }\n    final String streamName = \"test_h264_adaptive_with_renderer_disabling\";\n    testRunner\n        .setStreamName(streamName)\n        .setManifestUrl(DashTestData.H264_MANIFEST)\n        .setFullPlaybackNoSeeking(false)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setActionSchedule(RENDERER_DISABLING_SCHEDULE)\n        .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.H264_CDD_ADAPTIVE)\n        .run();\n  }\n\n  // H265 CDD.\n\n  @Test\n  public void testH265FixedV23() {\n    if (Util.SDK_INT < 23) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_h265_fixed\")\n        .setManifestUrl(DashTestData.H265_MANIFEST)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(false)\n        .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID, DashTestData.H265_CDD_FIXED)\n        .run();\n  }\n\n  @Test\n  public void testH265AdaptiveV24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_h265_adaptive\")\n        .setManifestUrl(DashTestData.H265_MANIFEST)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.H265_CDD_ADAPTIVE)\n        .run();\n  }\n\n  @Test\n  public void testH265AdaptiveWithSeekingV24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_h265_adaptive_with_seeking\")\n        .setManifestUrl(DashTestData.H265_MANIFEST)\n        .setFullPlaybackNoSeeking(false)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setActionSchedule(SEEKING_SCHEDULE)\n        .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.H265_CDD_ADAPTIVE)\n        .run();\n  }\n\n  @Test\n  public void testH265AdaptiveWithRendererDisablingV24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_h265_adaptive_with_renderer_disabling\")\n        .setManifestUrl(DashTestData.H265_MANIFEST)\n        .setFullPlaybackNoSeeking(false)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setActionSchedule(RENDERER_DISABLING_SCHEDULE)\n        .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.H265_CDD_ADAPTIVE)\n        .run();\n  }\n\n  // VP9 (CDD).\n\n  @Test\n  public void testVp9Fixed360pV23() {\n    if (Util.SDK_INT < 23) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_vp9_fixed_360p\")\n        .setManifestUrl(DashTestData.VP9_MANIFEST)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(false)\n        .setAudioVideoFormats(DashTestData.VP9_VORBIS_AUDIO_REPRESENTATION_ID,\n            DashTestData.VP9_CDD_FIXED)\n        .run();\n  }\n\n  @Test\n  public void testVp9AdaptiveV24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_vp9_adaptive\")\n        .setManifestUrl(DashTestData.VP9_MANIFEST)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setAudioVideoFormats(DashTestData.VP9_VORBIS_AUDIO_REPRESENTATION_ID,\n            DashTestData.VP9_CDD_ADAPTIVE)\n        .run();\n  }\n\n  @Test\n  public void testVp9AdaptiveWithSeekingV24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_vp9_adaptive_with_seeking\")\n        .setManifestUrl(DashTestData.VP9_MANIFEST)\n        .setFullPlaybackNoSeeking(false)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setActionSchedule(SEEKING_SCHEDULE)\n        .setAudioVideoFormats(DashTestData.VP9_VORBIS_AUDIO_REPRESENTATION_ID,\n            DashTestData.VP9_CDD_ADAPTIVE)\n        .run();\n  }\n\n  @Test\n  public void testVp9AdaptiveWithRendererDisablingV24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_vp9_adaptive_with_renderer_disabling\")\n        .setManifestUrl(DashTestData.VP9_MANIFEST)\n        .setFullPlaybackNoSeeking(false)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setActionSchedule(RENDERER_DISABLING_SCHEDULE)\n        .setAudioVideoFormats(DashTestData.VP9_VORBIS_AUDIO_REPRESENTATION_ID,\n            DashTestData.VP9_CDD_ADAPTIVE)\n        .run();\n  }\n\n  // H264: Other frame-rates for output buffer count assertions.\n\n  // 23.976 fps.\n  @Test\n  public void test23FpsH264FixedV23() {\n    if (Util.SDK_INT < 23) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_23fps_h264_fixed\")\n        .setManifestUrl(DashTestData.H264_23_MANIFEST)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(false)\n        .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID)\n        .run();\n  }\n\n  // 24 fps.\n  @Test\n  public void test24FpsH264FixedV23() {\n    if (Util.SDK_INT < 23) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_24fps_h264_fixed\")\n        .setManifestUrl(DashTestData.H264_24_MANIFEST)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(false)\n        .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID)\n        .run();\n  }\n\n  // 29.97 fps.\n  @Test\n  public void test29FpsH264FixedV23() {\n    if (Util.SDK_INT < 23) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_29fps_h264_fixed\")\n        .setManifestUrl(DashTestData.H264_29_MANIFEST)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(false)\n        .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID)\n        .run();\n  }\n\n  // Widevine encrypted media tests.\n  // H264 CDD.\n\n  @Test\n  public void testWidevineH264FixedV18() throws DecoderQueryException {\n    if (Util.SDK_INT < 18) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_h264_fixed\")\n        .setManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_H264, true)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(false)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_H264_CDD_FIXED)\n        .run();\n  }\n\n  @Test\n  public void testWidevineH264AdaptiveV18() throws DecoderQueryException {\n    if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_h264_adaptive\")\n        .setManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_H264, true)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_H264_CDD_ADAPTIVE)\n        .run();\n  }\n\n  @Test\n  public void testWidevineH264AdaptiveWithSeekingV18() throws DecoderQueryException {\n    if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_h264_adaptive_with_seeking\")\n        .setManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_H264, true)\n        .setFullPlaybackNoSeeking(false)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setActionSchedule(SEEKING_SCHEDULE)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_H264_CDD_ADAPTIVE)\n        .run();\n  }\n\n  @Test\n  public void testWidevineH264AdaptiveWithRendererDisablingV18() throws DecoderQueryException {\n    if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_h264_adaptive_with_renderer_disabling\")\n        .setManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_H264, true)\n        .setFullPlaybackNoSeeking(false)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setActionSchedule(RENDERER_DISABLING_SCHEDULE)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_H264_CDD_ADAPTIVE)\n        .run();\n  }\n\n  // H265 CDD.\n\n  @Test\n  public void testWidevineH265FixedV23() throws DecoderQueryException {\n    if (Util.SDK_INT < 23) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_h265_fixed\")\n        .setManifestUrl(DashTestData.WIDEVINE_H265_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_H265, true)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(false)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_H265_CDD_FIXED)\n        .run();\n  }\n\n  @Test\n  public void testWidevineH265AdaptiveV24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_h265_adaptive\")\n        .setManifestUrl(DashTestData.WIDEVINE_H265_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_H265, true)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_H265_CDD_ADAPTIVE)\n        .run();\n  }\n\n  @Test\n  public void testWidevineH265AdaptiveWithSeekingV24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_h265_adaptive_with_seeking\")\n        .setManifestUrl(DashTestData.WIDEVINE_H265_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_H265, true)\n        .setFullPlaybackNoSeeking(false)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setActionSchedule(SEEKING_SCHEDULE)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_H265_CDD_ADAPTIVE)\n        .run();\n  }\n\n  @Test\n  public void testWidevineH265AdaptiveWithRendererDisablingV24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_h265_adaptive_with_renderer_disabling\")\n        .setManifestUrl(DashTestData.WIDEVINE_H265_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_H265, true)\n        .setFullPlaybackNoSeeking(false)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setActionSchedule(RENDERER_DISABLING_SCHEDULE)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_H265_CDD_ADAPTIVE)\n        .run();\n  }\n\n  // VP9 (CDD).\n\n  @Test\n  public void testWidevineVp9Fixed360pV23() throws DecoderQueryException {\n    if (Util.SDK_INT < 23) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_vp9_fixed_360p\")\n        .setManifestUrl(DashTestData.WIDEVINE_VP9_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_VP9, true)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(false)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_VP9_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_VP9_CDD_FIXED)\n        .run();\n  }\n\n  @Test\n  public void testWidevineVp9AdaptiveV24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_vp9_adaptive\")\n        .setManifestUrl(DashTestData.WIDEVINE_VP9_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_VP9, true)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_VP9_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_VP9_CDD_ADAPTIVE)\n        .run();\n  }\n\n  @Test\n  public void testWidevineVp9AdaptiveWithSeekingV24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_vp9_adaptive_with_seeking\")\n        .setManifestUrl(DashTestData.WIDEVINE_VP9_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_VP9, true)\n        .setFullPlaybackNoSeeking(false)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setActionSchedule(SEEKING_SCHEDULE)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_VP9_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_VP9_CDD_ADAPTIVE)\n        .run();\n  }\n\n  @Test\n  public void testWidevineVp9AdaptiveWithRendererDisablingV24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_vp9_adaptive_with_renderer_disabling\")\n        .setManifestUrl(DashTestData.WIDEVINE_VP9_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_VP9, true)\n        .setFullPlaybackNoSeeking(false)\n        .setCanIncludeAdditionalVideoFormats(true)\n        .setActionSchedule(RENDERER_DISABLING_SCHEDULE)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_VP9_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_VP9_CDD_ADAPTIVE)\n        .run();\n  }\n\n  // H264: Other frame-rates for output buffer count assertions.\n\n  // 23.976 fps.\n  @Test\n  public void testWidevine23FpsH264FixedV23() throws DecoderQueryException {\n    if (Util.SDK_INT < 23) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_23fps_h264_fixed\")\n        .setManifestUrl(DashTestData.WIDEVINE_H264_23_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_H264, true)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(false)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID)\n        .run();\n  }\n\n  // 24 fps.\n  @Test\n  public void testWidevine24FpsH264FixedV23() throws DecoderQueryException {\n    if (Util.SDK_INT < 23) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_24fps_h264_fixed\")\n        .setManifestUrl(DashTestData.WIDEVINE_H264_24_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_H264, true)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(false)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID)\n        .run();\n  }\n\n  // 29.97 fps.\n  @Test\n  public void testWidevine29FpsH264FixedV23() throws DecoderQueryException {\n    if (Util.SDK_INT < 23) {\n      // Pass.\n      return;\n    }\n    testRunner\n        .setStreamName(\"test_widevine_29fps_h264_fixed\")\n        .setManifestUrl(DashTestData.WIDEVINE_H264_29_MANIFEST)\n        .setWidevineInfo(MimeTypes.VIDEO_H264, true)\n        .setFullPlaybackNoSeeking(true)\n        .setCanIncludeAdditionalVideoFormats(false)\n        .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID,\n            DashTestData.WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID)\n        .run();\n  }\n\n  // Decoder info.\n\n  @Test\n  public void testDecoderInfoH264() throws DecoderQueryException {\n    MediaCodecInfo decoderInfo =\n        MediaCodecUtil.getDecoderInfo(\n            MimeTypes.VIDEO_H264, /* secure= */ false, /* tunneling= */ false);\n    assertThat(decoderInfo).isNotNull();\n    assertThat(Util.SDK_INT < 21 || decoderInfo.adaptive).isTrue();\n  }\n\n  @Test\n  public void testDecoderInfoH265V24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    assertThat(\n            MediaCodecUtil.getDecoderInfo(\n                    MimeTypes.VIDEO_H265, /* secure= */ false, /* tunneling= */ false)\n                .adaptive)\n        .isTrue();\n  }\n\n  @Test\n  public void testDecoderInfoVP9V24() throws DecoderQueryException {\n    if (Util.SDK_INT < 24) {\n      // Pass.\n      return;\n    }\n    assertThat(\n            MediaCodecUtil.getDecoderInfo(\n                    MimeTypes.VIDEO_VP9, /* secure= */ false, /* tunneling= */ false)\n                .adaptive)\n        .isTrue();\n  }\n\n  // Internal.\n\n  private static boolean shouldSkipAdaptiveTest(String mimeType) throws DecoderQueryException {\n    MediaCodecInfo decoderInfo =\n        MediaCodecUtil.getDecoderInfo(mimeType, /* secure= */ false, /* tunneling= */ false);\n    return decoderInfo == null || !decoderInfo.adaptive;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.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.playbacktests.gts;\n\nimport com.google.android.exoplayer2.util.Util;\n\n/**\n * Test data for DASH tests.\n */\npublic final class DashTestData {\n\n  private static final String BASE_URL =\n      \"https://storage.googleapis.com/exoplayer-test-media-1/gen-4/\";\n\n  private static final String BASE_URL_SCREENS = BASE_URL + \"screens/dash-vod-single-segment/\";\n  private static final String BASE_URL_COMMON_ENCRYPTION = BASE_URL + \"common-encryption/\";\n  // Clear content manifests.\n  public static final String H264_MANIFEST = BASE_URL_SCREENS + \"manifest-h264.mpd\";\n  public static final String H265_MANIFEST = BASE_URL_SCREENS + \"manifest-h265.mpd\";\n  public static final String VP9_MANIFEST = BASE_URL_SCREENS + \"manifest-vp9.mpd\";\n  public static final String H264_23_MANIFEST = BASE_URL_SCREENS + \"manifest-h264-23.mpd\";\n  public static final String H264_24_MANIFEST = BASE_URL_SCREENS + \"manifest-h264-24.mpd\";\n  public static final String H264_29_MANIFEST = BASE_URL_SCREENS + \"manifest-h264-29.mpd\";\n  // Widevine encrypted content manifests.\n  public static final String WIDEVINE_H264_MANIFEST = BASE_URL_SCREENS + \"manifest-h264-enc.mpd\";\n  public static final String WIDEVINE_H265_MANIFEST = BASE_URL_SCREENS + \"manifest-h265-enc.mpd\";\n  public static final String WIDEVINE_VP9_MANIFEST = BASE_URL_SCREENS + \"manifest-vp9-enc.mpd\";\n  public static final String WIDEVINE_H264_23_MANIFEST =\n      BASE_URL_SCREENS + \"manifest-h264-23-enc.mpd\";\n  public static final String WIDEVINE_H264_24_MANIFEST =\n      BASE_URL_SCREENS + \"manifest-h264-24-enc.mpd\";\n  public static final String WIDEVINE_H264_29_MANIFEST =\n      BASE_URL_SCREENS + \"manifest-h264-29-enc.mpd\";\n\n  // Widevine encrypted content manifests using different common encryption schemes.\n  public static final String WIDEVINE_SCHEME_CENC = BASE_URL_COMMON_ENCRYPTION + \"tears-cenc.mpd\";\n  public static final String WIDEVINE_SCHEME_CBC1 =\n      BASE_URL_COMMON_ENCRYPTION + \"tears-aes-cbc1.mpd\";\n  public static final String WIDEVINE_SCHEME_CBCS =\n      BASE_URL_COMMON_ENCRYPTION + \"tears-aes-cbcs.mpd\";\n\n  public static final String AAC_AUDIO_REPRESENTATION_ID = \"141\";\n  public static final String H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = \"avc-baseline-240\";\n  public static final String H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = \"avc-baseline-480\";\n  public static final String H264_MAIN_240P_VIDEO_REPRESENTATION_ID = \"avc-main-240\";\n  public static final String H264_MAIN_480P_VIDEO_REPRESENTATION_ID = \"avc-main-480\";\n  // The highest quality H264 format mandated by the Android CDD.\n  public static final String H264_CDD_FIXED = Util.SDK_INT < 23\n      ? H264_BASELINE_480P_VIDEO_REPRESENTATION_ID : H264_MAIN_480P_VIDEO_REPRESENTATION_ID;\n  // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile\n  // support from API level 23, but we opt to test only from 24 due to known issues on API level 23\n  // when switching between baseline and main profiles on certain devices.\n  public static final String[] H264_CDD_ADAPTIVE = Util.SDK_INT < 24\n      ? new String[] {\n          H264_BASELINE_240P_VIDEO_REPRESENTATION_ID,\n          H264_BASELINE_480P_VIDEO_REPRESENTATION_ID}\n      : new String[] {\n          H264_BASELINE_240P_VIDEO_REPRESENTATION_ID,\n          H264_BASELINE_480P_VIDEO_REPRESENTATION_ID,\n          H264_MAIN_240P_VIDEO_REPRESENTATION_ID,\n          H264_MAIN_480P_VIDEO_REPRESENTATION_ID};\n\n  public static final String H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID =\n      \"avc-baseline-480-23\";\n  public static final String H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID =\n      \"avc-baseline-480-24\";\n  public static final String H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID =\n      \"avc-baseline-480-29\";\n\n  public static final String H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = \"hevc-main-288\";\n  public static final String H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = \"hevc-main-360\";\n  // The highest quality H265 format mandated by the Android CDD.\n  public static final String H265_CDD_FIXED = H265_BASELINE_360P_VIDEO_REPRESENTATION_ID;\n  // Multiple H265 formats mandated by the Android CDD.\n  public static final String[] H265_CDD_ADAPTIVE =\n      new String[] {\n          H265_BASELINE_288P_VIDEO_REPRESENTATION_ID,\n          H265_BASELINE_360P_VIDEO_REPRESENTATION_ID};\n\n  public static final String VP9_VORBIS_AUDIO_REPRESENTATION_ID = \"4\";\n  public static final String VP9_180P_VIDEO_REPRESENTATION_ID = \"0\";\n  public static final String VP9_360P_VIDEO_REPRESENTATION_ID = \"1\";\n  // The highest quality VP9 format mandated by the Android CDD.\n  public static final String VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID;\n  // Multiple VP9 formats mandated by the Android CDD.\n  public static final String[] VP9_CDD_ADAPTIVE =\n      new String[] {\n          VP9_180P_VIDEO_REPRESENTATION_ID,\n          VP9_360P_VIDEO_REPRESENTATION_ID};\n\n  // Widevine encrypted content representation ids.\n  public static final String WIDEVINE_AAC_AUDIO_REPRESENTATION_ID = \"0\";\n  public static final String WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = \"2\";\n  public static final String WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = \"3\";\n  public static final String WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID = \"4\";\n  public static final String WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID = \"5\";\n  // The highest quality H264 format mandated by the Android CDD.\n  public static final String WIDEVINE_H264_CDD_FIXED = Util.SDK_INT < 23\n      ? WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID\n      : WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID;\n  // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile\n  // support from API level 23, but we opt to test only from 24 due to known issues on API level 23\n  // when switching between baseline and main profiles on certain devices.\n  public static final String[] WIDEVINE_H264_CDD_ADAPTIVE = Util.SDK_INT < 24\n      ? new String[] {\n          WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID,\n          WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID}\n      : new String[] {\n          WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID,\n          WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID,\n          WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID,\n          WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID};\n\n  public static final String WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = \"3\";\n  public static final String WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = \"3\";\n  public static final String WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = \"3\";\n\n  public static final String WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = \"2\";\n  public static final String WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = \"3\";\n  // The highest quality H265 format mandated by the Android CDD.\n  public static final String WIDEVINE_H265_CDD_FIXED =\n      WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID;\n  // Multiple H265 formats mandated by the Android CDD.\n  public static final String[] WIDEVINE_H265_CDD_ADAPTIVE =\n      new String[] {\n          WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID,\n          WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID};\n\n  public static final String WIDEVINE_VP9_AAC_AUDIO_REPRESENTATION_ID = \"0\";\n  public static final String WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID = \"2\";\n  public static final String WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID = \"3\";\n  // The highest quality VP9 format mandated by the Android CDD.\n  public static final String WIDEVINE_VP9_CDD_FIXED = WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID;\n  // Multiple VP9 formats mandated by the Android CDD.\n  public static final String[] WIDEVINE_VP9_CDD_ADAPTIVE =\n      new String[] {\n          WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID,\n          WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID};\n\n  private static final String WIDEVINE_LICENSE_URL =\n      \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\";\n  private static final String WIDEVINE_SW_CRYPTO_CONTENT_ID = \"&video_id=exoplayer_test_1\";\n  private static final String WIDEVINE_HW_SECURE_DECODE_CONTENT_ID = \"&video_id=exoplayer_test_2\";\n\n  public static String getWidevineLicenseUrl(boolean videoIdRequiredInLicenseUrl,\n      boolean useL1Widevine) {\n    if (!videoIdRequiredInLicenseUrl) {\n      return WIDEVINE_LICENSE_URL;\n    } else {\n      return WIDEVINE_LICENSE_URL\n          + (useL1Widevine ? WIDEVINE_HW_SECURE_DECODE_CONTENT_ID : WIDEVINE_SW_CRYPTO_CONTENT_ID);\n    }\n  }\n\n  private DashTestData() {\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.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.playbacktests.gts;\n\nimport static com.google.android.exoplayer2.C.WIDEVINE_UUID;\n\nimport android.annotation.TargetApi;\nimport android.media.MediaDrm;\nimport android.media.UnsupportedSchemeException;\nimport android.net.Uri;\nimport android.view.Surface;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.DefaultLoadControl;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayerFactory;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\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.drm.HttpMediaDrmCallback;\nimport com.google.android.exoplayer2.drm.MediaDrmCallback;\nimport com.google.android.exoplayer2.drm.UnsupportedDrmException;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.source.dash.DashMediaSource;\nimport com.google.android.exoplayer2.testutil.ActionSchedule;\nimport com.google.android.exoplayer2.testutil.DebugRenderersFactory;\nimport com.google.android.exoplayer2.testutil.DecoderCountersUtil;\nimport com.google.android.exoplayer2.testutil.ExoHostedTest;\nimport com.google.android.exoplayer2.testutil.HostActivity;\nimport com.google.android.exoplayer2.testutil.HostActivity.HostedTest;\nimport com.google.android.exoplayer2.testutil.MetricsLogger;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector;\nimport com.google.android.exoplayer2.trackselection.RandomTrackSelection;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\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.upstream.DefaultLoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/** {@link DashHostedTest} builder. */\npublic final class DashTestRunner {\n\n  static final int VIDEO_RENDERER_INDEX = 0;\n  static final int AUDIO_RENDERER_INDEX = 1;\n\n  private static final long TEST_TIMEOUT_MS = 5 * 60 * 1000;\n\n  // Whether adaptive tests should enable video formats beyond those mandated by the Android CDD\n  // if the device advertises support for them.\n  private static final boolean ALLOW_ADDITIONAL_VIDEO_FORMATS = Util.SDK_INT >= 24;\n\n  private static final String AUDIO_TAG_SUFFIX = \":Audio\";\n  private static final String VIDEO_TAG_SUFFIX = \":Video\";\n\n  private static final int MIN_LOADABLE_RETRY_COUNT = 10;\n  private static final int MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES = 10;\n  private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f;\n\n  private static final String WIDEVINE_SECURITY_LEVEL_1 = \"L1\";\n  private static final String WIDEVINE_SECURITY_LEVEL_3 = \"L3\";\n  private static final String SECURITY_LEVEL_PROPERTY = \"securityLevel\";\n\n  private final String tag;\n  private final HostActivity activity;\n\n  private String streamName;\n  private boolean fullPlaybackNoSeeking;\n  private String audioFormat;\n  private boolean canIncludeAdditionalVideoFormats;\n  private ActionSchedule actionSchedule;\n  private byte[] offlineLicenseKeySetId;\n  private String[] videoFormats;\n  private String manifestUrl;\n  private boolean useL1Widevine;\n  private String widevineLicenseUrl;\n  private DataSource.Factory dataSourceFactory;\n\n  @SuppressWarnings(\"ResourceType\")\n  public static boolean isL1WidevineAvailable(String mimeType) {\n    if (Util.SDK_INT >= 18) {\n      try {\n        // Force L3 if secure decoder is not available.\n        if (MediaCodecUtil.getDecoderInfo(mimeType, /* secure= */ true, /* tunneling= */ false)\n            == null) {\n          return false;\n        }\n        MediaDrm mediaDrm = MediaDrmBuilder.build();\n        String securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY);\n        mediaDrm.release();\n        return WIDEVINE_SECURITY_LEVEL_1.equals(securityProperty);\n      } catch (MediaCodecUtil.DecoderQueryException e) {\n        throw new IllegalStateException(e);\n      }\n    }\n    return false;\n  }\n\n  public DashTestRunner(String tag, HostActivity activity) {\n    this.tag = tag;\n    this.activity = activity;\n  }\n\n  public DashTestRunner setStreamName(String streamName) {\n    this.streamName = streamName;\n    return this;\n  }\n\n  public DashTestRunner setFullPlaybackNoSeeking(boolean fullPlaybackNoSeeking) {\n    this.fullPlaybackNoSeeking = fullPlaybackNoSeeking;\n    return this;\n  }\n\n  public DashTestRunner setCanIncludeAdditionalVideoFormats(\n      boolean canIncludeAdditionalVideoFormats) {\n    this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats\n        && ALLOW_ADDITIONAL_VIDEO_FORMATS;\n    return this;\n  }\n\n  public DashTestRunner setActionSchedule(ActionSchedule actionSchedule) {\n    this.actionSchedule = actionSchedule;\n    return this;\n  }\n\n  public DashTestRunner setOfflineLicenseKeySetId(byte[] offlineLicenseKeySetId) {\n    this.offlineLicenseKeySetId = offlineLicenseKeySetId;\n    return this;\n  }\n\n  public DashTestRunner setAudioVideoFormats(String audioFormat, String... videoFormats) {\n    this.audioFormat = audioFormat;\n    this.videoFormats = videoFormats;\n    return this;\n  }\n\n  public DashTestRunner setManifestUrl(String manifestUrl) {\n    this.manifestUrl = manifestUrl;\n    return this;\n  }\n\n  public DashTestRunner setWidevineInfo(String mimeType, boolean videoIdRequiredInLicenseUrl) {\n    this.useL1Widevine = isL1WidevineAvailable(mimeType);\n    this.widevineLicenseUrl = DashTestData.getWidevineLicenseUrl(videoIdRequiredInLicenseUrl,\n        useL1Widevine);\n    return this;\n  }\n\n  public DashTestRunner setDataSourceFactory(DataSource.Factory dataSourceFactory) {\n    this.dataSourceFactory = dataSourceFactory;\n    return this;\n  }\n\n  public void run() {\n    DashHostedTest test = createDashHostedTest(canIncludeAdditionalVideoFormats, false);\n    activity.runTest(test, TEST_TIMEOUT_MS);\n    // Retry test exactly once if adaptive test fails due to excessive dropped buffers when\n    // playing non-CDD required formats (b/28220076).\n    if (test.needsCddLimitedRetry) {\n      activity.runTest(createDashHostedTest(false, true), TEST_TIMEOUT_MS);\n    }\n  }\n\n  private DashHostedTest createDashHostedTest(\n      boolean canIncludeAdditionalVideoFormats, boolean isCddLimitedRetry) {\n    MetricsLogger metricsLogger = MetricsLogger.Factory.createDefault(tag);\n    return new DashHostedTest(tag, streamName, manifestUrl, metricsLogger, fullPlaybackNoSeeking,\n        audioFormat, canIncludeAdditionalVideoFormats, isCddLimitedRetry, actionSchedule,\n        offlineLicenseKeySetId, widevineLicenseUrl, useL1Widevine, dataSourceFactory,\n        videoFormats);\n  }\n\n  /**\n   * A {@link HostedTest} for DASH playback tests.\n   */\n  private static final class DashHostedTest extends ExoHostedTest {\n\n    private final String streamName;\n    private final String manifestUrl;\n    private final MetricsLogger metricsLogger;\n    private final boolean fullPlaybackNoSeeking;\n    private final boolean isCddLimitedRetry;\n    private final DashTestTrackSelector trackSelector;\n    private final byte[] offlineLicenseKeySetId;\n    private final String widevineLicenseUrl;\n    private final boolean useL1Widevine;\n    private final DataSource.Factory dataSourceFactory;\n\n    private boolean needsCddLimitedRetry;\n\n    /**\n     * @param tag A tag to use for logging.\n     * @param streamName The name of the test stream for metric logging.\n     * @param manifestUrl The manifest url.\n     * @param metricsLogger Logger to log metrics from the test.\n     * @param fullPlaybackNoSeeking Whether the test will play the entire source with no seeking.\n     * @param audioFormat The audio format.\n     * @param canIncludeAdditionalVideoFormats Whether to use video formats in addition to those\n     *     listed in the videoFormats argument, if the device is capable of playing them.\n     * @param isCddLimitedRetry Whether this is a CDD limited retry following a previous failure.\n     * @param actionSchedule The action schedule for the test.\n     * @param offlineLicenseKeySetId The key set id of the license to be used.\n     * @param widevineLicenseUrl If the video is Widevine encrypted, this is the license url\n     *     otherwise null.\n     * @param useL1Widevine Whether to use L1 Widevine.\n     * @param dataSourceFactory If not null, used to load manifest and media.\n     * @param videoFormats The video formats.\n     */\n    private DashHostedTest(String tag, String streamName, String manifestUrl,\n        MetricsLogger metricsLogger, boolean fullPlaybackNoSeeking, String audioFormat,\n        boolean canIncludeAdditionalVideoFormats, boolean isCddLimitedRetry,\n        ActionSchedule actionSchedule, byte[] offlineLicenseKeySetId, String widevineLicenseUrl,\n        boolean useL1Widevine, DataSource.Factory dataSourceFactory, String... videoFormats) {\n      super(tag, fullPlaybackNoSeeking);\n      Assertions.checkArgument(!(isCddLimitedRetry && canIncludeAdditionalVideoFormats));\n      this.streamName = streamName;\n      this.manifestUrl = manifestUrl;\n      this.metricsLogger = metricsLogger;\n      this.fullPlaybackNoSeeking = fullPlaybackNoSeeking;\n      this.isCddLimitedRetry = isCddLimitedRetry;\n      this.offlineLicenseKeySetId = offlineLicenseKeySetId;\n      this.widevineLicenseUrl = widevineLicenseUrl;\n      this.useL1Widevine = useL1Widevine;\n      this.dataSourceFactory = dataSourceFactory;\n      trackSelector = new DashTestTrackSelector(tag, audioFormat, videoFormats,\n          canIncludeAdditionalVideoFormats);\n      if (actionSchedule != null) {\n        setSchedule(actionSchedule);\n      }\n    }\n\n    @Override\n    protected DefaultTrackSelector buildTrackSelector(HostActivity host) {\n      return trackSelector;\n    }\n\n    @Override\n    protected DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManager(\n        final String userAgent) {\n      if (widevineLicenseUrl == null) {\n        return null;\n      }\n      try {\n        MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl,\n            new DefaultHttpDataSourceFactory(userAgent));\n        DefaultDrmSessionManager<FrameworkMediaCrypto> drmSessionManager =\n            DefaultDrmSessionManager.newWidevineInstance(drmCallback, null);\n        if (!useL1Widevine) {\n          drmSessionManager.setPropertyString(\n              SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3);\n        }\n        if (offlineLicenseKeySetId != null) {\n          drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK,\n              offlineLicenseKeySetId);\n        }\n        return drmSessionManager;\n      } catch (UnsupportedDrmException e) {\n        throw new IllegalStateException(e);\n      }\n    }\n\n    @Override\n    protected SimpleExoPlayer buildExoPlayer(\n        HostActivity host,\n        Surface surface,\n        MappingTrackSelector trackSelector,\n        DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {\n      SimpleExoPlayer player =\n          ExoPlayerFactory.newSimpleInstance(\n              host,\n              new DebugRenderersFactory(host),\n              trackSelector,\n              new DefaultLoadControl(),\n              drmSessionManager);\n      player.setVideoSurface(surface);\n      return player;\n    }\n\n    @Override\n    protected MediaSource buildSource(HostActivity host, String userAgent) {\n      DataSource.Factory dataSourceFactory =\n          this.dataSourceFactory != null\n              ? this.dataSourceFactory\n              : new DefaultDataSourceFactory(host, userAgent);\n      Uri manifestUri = Uri.parse(manifestUrl);\n      return new DashMediaSource.Factory(dataSourceFactory)\n          .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MIN_LOADABLE_RETRY_COUNT))\n          .createMediaSource(manifestUri);\n    }\n\n    @Override\n    protected void onTestFinished(DecoderCounters audioCounters, DecoderCounters videoCounters) {\n      metricsLogger.logMetric(MetricsLogger.KEY_TEST_NAME, streamName);\n      metricsLogger.logMetric(MetricsLogger.KEY_IS_CDD_LIMITED_RETRY, isCddLimitedRetry);\n      metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_DROPPED_COUNT,\n          videoCounters.droppedBufferCount);\n      metricsLogger.logMetric(MetricsLogger.KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT,\n          videoCounters.maxConsecutiveDroppedBufferCount);\n      metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_SKIPPED_COUNT,\n          videoCounters.skippedOutputBufferCount);\n      metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_RENDERED_COUNT,\n          videoCounters.renderedOutputBufferCount);\n      metricsLogger.close();\n\n      if (fullPlaybackNoSeeking) {\n        // We shouldn't have skipped any output buffers.\n        DecoderCountersUtil\n            .assertSkippedOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, 0);\n        DecoderCountersUtil\n            .assertSkippedOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, 0);\n        // We allow one fewer output buffer due to the way that MediaCodecRenderer and the\n        // underlying decoders handle the end of stream. This should be tightened up in the future.\n        DecoderCountersUtil.assertTotalBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters,\n            audioCounters.inputBufferCount - 1, audioCounters.inputBufferCount);\n        DecoderCountersUtil.assertTotalBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters,\n            videoCounters.inputBufferCount - 1, videoCounters.inputBufferCount);\n      }\n      try {\n        int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION\n            * DecoderCountersUtil.getTotalBufferCount(videoCounters));\n        // Assert that performance is acceptable.\n        // Assert that total dropped frames were within limit.\n        DecoderCountersUtil.assertDroppedBufferLimit(tag + VIDEO_TAG_SUFFIX, videoCounters,\n            droppedFrameLimit);\n        // Assert that consecutive dropped frames were within limit.\n        DecoderCountersUtil.assertConsecutiveDroppedBufferLimit(tag + VIDEO_TAG_SUFFIX,\n            videoCounters, MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES);\n      } catch (AssertionError e) {\n        if (trackSelector.includedAdditionalVideoFormats) {\n          // Retry limiting to CDD mandated formats (b/28220076).\n          Log.e(tag, \"Too many dropped or consecutive dropped frames.\", e);\n          needsCddLimitedRetry = true;\n        } else {\n          throw e;\n        }\n      }\n    }\n  }\n\n  private static final class DashTestTrackSelector extends DefaultTrackSelector {\n\n    private final String tag;\n    private final String audioFormatId;\n    private final String[] videoFormatIds;\n    private final boolean canIncludeAdditionalVideoFormats;\n\n    public boolean includedAdditionalVideoFormats;\n\n    private DashTestTrackSelector(String tag, String audioFormatId, String[] videoFormatIds,\n        boolean canIncludeAdditionalVideoFormats) {\n      super(new RandomTrackSelection.Factory(/* seed= */ 0));\n      this.tag = tag;\n      this.audioFormatId = audioFormatId;\n      this.videoFormatIds = videoFormatIds;\n      this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats;\n    }\n\n    @Override\n    protected TrackSelection.Definition[] selectAllTracks(\n        MappedTrackInfo mappedTrackInfo,\n        int[][][] rendererFormatSupports,\n        int[] rendererMixedMimeTypeAdaptationSupports,\n        Parameters parameters)\n        throws ExoPlaybackException {\n      Assertions.checkState(\n          mappedTrackInfo.getRendererType(VIDEO_RENDERER_INDEX) == C.TRACK_TYPE_VIDEO);\n      Assertions.checkState(\n          mappedTrackInfo.getRendererType(AUDIO_RENDERER_INDEX) == C.TRACK_TYPE_AUDIO);\n      TrackGroupArray videoTrackGroups = mappedTrackInfo.getTrackGroups(VIDEO_RENDERER_INDEX);\n      TrackGroupArray audioTrackGroups = mappedTrackInfo.getTrackGroups(AUDIO_RENDERER_INDEX);\n      Assertions.checkState(videoTrackGroups.length == 1);\n      Assertions.checkState(audioTrackGroups.length == 1);\n      TrackSelection.Definition[] definitions =\n          new TrackSelection.Definition[mappedTrackInfo.getRendererCount()];\n      definitions[VIDEO_RENDERER_INDEX] =\n          new TrackSelection.Definition(\n              videoTrackGroups.get(0),\n              getVideoTrackIndices(\n                  videoTrackGroups.get(0),\n                  rendererFormatSupports[VIDEO_RENDERER_INDEX][0],\n                  videoFormatIds,\n                  canIncludeAdditionalVideoFormats));\n      definitions[AUDIO_RENDERER_INDEX] =\n          new TrackSelection.Definition(\n              audioTrackGroups.get(0), getTrackIndex(audioTrackGroups.get(0), audioFormatId));\n      includedAdditionalVideoFormats =\n          definitions[VIDEO_RENDERER_INDEX].tracks.length > videoFormatIds.length;\n      return definitions;\n    }\n\n    private int[] getVideoTrackIndices(\n        TrackGroup trackGroup,\n        int[] formatSupports,\n        String[] formatIds,\n        boolean canIncludeAdditionalFormats) {\n      List<Integer> trackIndices = new ArrayList<>();\n\n      // Always select explicitly listed representations.\n      for (String formatId : formatIds) {\n        int trackIndex = getTrackIndex(trackGroup, formatId);\n        Log.d(tag, \"Adding base video format: \"\n            + Format.toLogString(trackGroup.getFormat(trackIndex)));\n        trackIndices.add(trackIndex);\n      }\n\n      // Select additional video representations, if supported by the device.\n      if (canIncludeAdditionalFormats) {\n        for (int i = 0; i < trackGroup.length; i++) {\n          if (!trackIndices.contains(i) && isFormatHandled(formatSupports[i])) {\n            Log.d(tag, \"Adding extra video format: \"\n                + Format.toLogString(trackGroup.getFormat(i)));\n            trackIndices.add(i);\n          }\n        }\n      }\n\n      int[] trackIndicesArray = Util.toArray(trackIndices);\n      Arrays.sort(trackIndicesArray);\n      return trackIndicesArray;\n    }\n\n    private static int getTrackIndex(TrackGroup trackGroup, String formatId) {\n      for (int i = 0; i < trackGroup.length; i++) {\n        if (trackGroup.getFormat(i).id.equals(formatId)) {\n          return i;\n        }\n      }\n      throw new IllegalStateException(\"Format \" + formatId + \" not found.\");\n    }\n\n    private static boolean isFormatHandled(int formatSupport) {\n      return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK)\n          == RendererCapabilities.FORMAT_HANDLED;\n    }\n\n  }\n\n  /**\n   * Creates a new {@code MediaDrm} object. The encapsulation ensures that the tests can be\n   * executed for API level < 18.\n   */\n  @TargetApi(18)\n  private static final class MediaDrmBuilder {\n\n    public static MediaDrm build () {\n      try {\n        return new MediaDrm(WIDEVINE_UUID);\n      } catch (UnsupportedSchemeException e) {\n        throw new IllegalStateException(e);\n      }\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.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.playbacktests.gts;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static com.google.common.truth.Truth.assertWithMessage;\nimport static org.junit.Assert.fail;\n\nimport android.media.MediaDrm.MediaDrmStateException;\nimport android.net.Uri;\nimport android.util.Pair;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport androidx.test.rule.ActivityTestRule;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.drm.OfflineLicenseHelper;\nimport com.google.android.exoplayer2.source.dash.DashUtil;\nimport com.google.android.exoplayer2.source.dash.manifest.DashManifest;\nimport com.google.android.exoplayer2.testutil.ActionSchedule;\nimport com.google.android.exoplayer2.testutil.HostActivity;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests Widevine encrypted DASH playbacks using offline keys. */\n@RunWith(AndroidJUnit4.class)\npublic final class DashWidevineOfflineTest {\n\n  private static final String TAG = \"DashWidevineOfflineTest\";\n  private static final String USER_AGENT = \"ExoPlayerPlaybackTests\";\n\n  private DashTestRunner testRunner;\n  private DefaultHttpDataSourceFactory httpDataSourceFactory;\n  private OfflineLicenseHelper<FrameworkMediaCrypto> offlineLicenseHelper;\n  private byte[] offlineLicenseKeySetId;\n\n  @Rule public ActivityTestRule<HostActivity> testRule = new ActivityTestRule<>(HostActivity.class);\n\n  @Before\n  public void setUp() throws Exception {\n    testRunner =\n        new DashTestRunner(TAG, testRule.getActivity())\n            .setStreamName(\"test_widevine_h264_fixed_offline\")\n            .setManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST)\n            .setWidevineInfo(MimeTypes.VIDEO_H264, true)\n            .setFullPlaybackNoSeeking(true)\n            .setCanIncludeAdditionalVideoFormats(false)\n            .setAudioVideoFormats(\n                DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID,\n                DashTestData.WIDEVINE_H264_CDD_FIXED);\n\n    boolean useL1Widevine = DashTestRunner.isL1WidevineAvailable(MimeTypes.VIDEO_H264);\n    String widevineLicenseUrl = DashTestData.getWidevineLicenseUrl(true, useL1Widevine);\n    httpDataSourceFactory = new DefaultHttpDataSourceFactory(USER_AGENT);\n    if (Util.SDK_INT >= 18) {\n      offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance(widevineLicenseUrl,\n          httpDataSourceFactory);\n    }\n  }\n\n  @After\n  public void tearDown() throws Exception {\n    testRunner = null;\n    if (offlineLicenseKeySetId != null) {\n      releaseLicense();\n    }\n    if (offlineLicenseHelper != null) {\n      offlineLicenseHelper.release();\n    }\n    offlineLicenseHelper = null;\n    httpDataSourceFactory = null;\n  }\n\n  // Offline license tests\n\n  @Test\n  public void testWidevineOfflineLicenseV22() throws Exception {\n    if (Util.SDK_INT < 22) {\n      return; // Pass.\n    }\n    downloadLicense();\n    testRunner.run();\n\n    // Renew license after playback should still work\n    offlineLicenseKeySetId = offlineLicenseHelper.renewLicense(offlineLicenseKeySetId);\n    assertThat(offlineLicenseKeySetId).isNotNull();\n  }\n\n  @Test\n  public void testWidevineOfflineReleasedLicenseV22() throws Throwable {\n    if (Util.SDK_INT < 22) {\n      return; // Pass.\n    }\n    downloadLicense();\n    releaseLicense(); // keySetId no longer valid.\n\n    try {\n      testRunner.run();\n      fail(\"Playback should fail because the license has been released.\");\n    } catch (Throwable e) {\n      // Get the root cause\n      while (true) {\n        Throwable cause = e.getCause();\n        if (cause == null || cause == e) {\n          break;\n        }\n        e = cause;\n      }\n      // It should be a MediaDrmStateException instance\n      if (!(e instanceof MediaDrmStateException)) {\n        throw e;\n      }\n    }\n  }\n\n  @Test\n  public void testWidevineOfflineExpiredLicenseV22() throws Exception {\n    if (Util.SDK_INT < 22) {\n      return; // Pass.\n    }\n    downloadLicense();\n\n    // Wait until the license expires\n    long licenseDuration =\n        offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId).first;\n    assertWithMessage(\n            \"License duration should be less than 30 sec. \" + \"Server settings might have changed.\")\n        .that(licenseDuration < 30)\n        .isTrue();\n    while (licenseDuration > 0) {\n      synchronized (this) {\n        wait(licenseDuration * 1000 + 2000);\n      }\n      long previousDuration = licenseDuration;\n      licenseDuration =\n          offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId).first;\n      assertWithMessage(\"License duration should be decreasing.\")\n          .that(previousDuration > licenseDuration)\n          .isTrue();\n    }\n\n    // DefaultDrmSessionManager should renew the license and stream play fine\n    testRunner.run();\n  }\n\n  @Test\n  public void testWidevineOfflineLicenseExpiresOnPauseV22() throws Exception {\n    if (Util.SDK_INT < 22) {\n      return; // Pass.\n    }\n    downloadLicense();\n\n    // During playback pause until the license expires then continue playback\n    Pair<Long, Long> licenseDurationRemainingSec =\n        offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId);\n    long licenseDuration = licenseDurationRemainingSec.first;\n    assertWithMessage(\n            \"License duration should be less than 30 sec. \" + \"Server settings might have changed.\")\n        .that(licenseDuration < 30)\n        .isTrue();\n    ActionSchedule schedule = new ActionSchedule.Builder(TAG)\n        .waitForPlaybackState(Player.STATE_READY)\n        .delay(3000).pause().delay(licenseDuration * 1000 + 2000).play().build();\n\n    // DefaultDrmSessionManager should renew the license and stream play fine\n    testRunner.setActionSchedule(schedule).run();\n  }\n\n  private void downloadLicense() throws InterruptedException, DrmSessionException, IOException {\n    DataSource dataSource = httpDataSourceFactory.createDataSource();\n    DashManifest dashManifest = DashUtil.loadManifest(dataSource,\n        Uri.parse(DashTestData.WIDEVINE_H264_MANIFEST));\n    DrmInitData drmInitData = DashUtil.loadDrmInitData(dataSource, dashManifest.getPeriod(0));\n    offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(drmInitData);\n    assertThat(offlineLicenseKeySetId).isNotNull();\n    assertThat(offlineLicenseKeySetId.length).isGreaterThan(0);\n    testRunner.setOfflineLicenseKeySetId(offlineLicenseKeySetId);\n  }\n\n  private void releaseLicense() throws DrmSessionException {\n    offlineLicenseHelper.releaseLicense(offlineLicenseKeySetId);\n    offlineLicenseKeySetId = null;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/EnumerateDecodersTest.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.playbacktests.gts;\n\n\nimport android.media.MediaCodecInfo.AudioCapabilities;\nimport android.media.MediaCodecInfo.CodecCapabilities;\nimport android.media.MediaCodecInfo.CodecProfileLevel;\nimport android.media.MediaCodecInfo.VideoCapabilities;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecInfo;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;\nimport com.google.android.exoplayer2.testutil.MetricsLogger;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\nimport java.util.List;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Tests enumeration of decoders using {@link MediaCodecUtil}. */\n@RunWith(AndroidJUnit4.class)\npublic class EnumerateDecodersTest {\n\n  private static final String TAG = \"EnumerateDecodersTest\";\n\n  private MetricsLogger metricsLogger;\n\n  @Before\n  public void setUp() {\n    metricsLogger = MetricsLogger.Factory.createDefault(TAG);\n  }\n\n  @Test\n  public void testEnumerateDecoders() throws Exception {\n    enumerateDecoders(MimeTypes.VIDEO_H263);\n    enumerateDecoders(MimeTypes.VIDEO_H264);\n    enumerateDecoders(MimeTypes.VIDEO_H265);\n    enumerateDecoders(MimeTypes.VIDEO_VP8);\n    enumerateDecoders(MimeTypes.VIDEO_VP9);\n    enumerateDecoders(MimeTypes.VIDEO_MP4V);\n    enumerateDecoders(MimeTypes.VIDEO_MPEG);\n    enumerateDecoders(MimeTypes.VIDEO_MPEG2);\n    enumerateDecoders(MimeTypes.VIDEO_VC1);\n    enumerateDecoders(MimeTypes.AUDIO_AAC);\n    enumerateDecoders(MimeTypes.AUDIO_MPEG_L1);\n    enumerateDecoders(MimeTypes.AUDIO_MPEG_L2);\n    enumerateDecoders(MimeTypes.AUDIO_MPEG);\n    enumerateDecoders(MimeTypes.AUDIO_RAW);\n    enumerateDecoders(MimeTypes.AUDIO_ALAW);\n    enumerateDecoders(MimeTypes.AUDIO_MLAW);\n    enumerateDecoders(MimeTypes.AUDIO_AC3);\n    enumerateDecoders(MimeTypes.AUDIO_E_AC3);\n    enumerateDecoders(MimeTypes.AUDIO_E_AC3_JOC);\n    enumerateDecoders(MimeTypes.AUDIO_TRUEHD);\n    enumerateDecoders(MimeTypes.AUDIO_DTS);\n    enumerateDecoders(MimeTypes.AUDIO_DTS_HD);\n    enumerateDecoders(MimeTypes.AUDIO_DTS_EXPRESS);\n    enumerateDecoders(MimeTypes.AUDIO_VORBIS);\n    enumerateDecoders(MimeTypes.AUDIO_OPUS);\n    enumerateDecoders(MimeTypes.AUDIO_AMR_NB);\n    enumerateDecoders(MimeTypes.AUDIO_AMR_WB);\n    enumerateDecoders(MimeTypes.AUDIO_FLAC);\n    enumerateDecoders(MimeTypes.AUDIO_ALAC);\n    enumerateDecoders(MimeTypes.AUDIO_MSGSM);\n  }\n\n  private void enumerateDecoders(String mimeType) throws DecoderQueryException {\n    logDecoderInfos(mimeType, /* secure= */ false, /* tunneling= */ false);\n    logDecoderInfos(mimeType, /* secure= */ true, /* tunneling= */ false);\n    logDecoderInfos(mimeType, /* secure= */ false, /* tunneling= */ true);\n    logDecoderInfos(mimeType, /* secure= */ true, /* tunneling= */ true);\n  }\n\n  private void logDecoderInfos(String mimeType, boolean secure, boolean tunneling)\n      throws DecoderQueryException {\n    List<MediaCodecInfo> mediaCodecInfos =\n        MediaCodecUtil.getDecoderInfos(mimeType, secure, tunneling);\n    for (MediaCodecInfo mediaCodecInfo : mediaCodecInfos) {\n      CodecCapabilities capabilities = Assertions.checkNotNull(mediaCodecInfo.capabilities);\n      metricsLogger.logMetric(\n          \"capabilities_\" + mediaCodecInfo.name, codecCapabilitiesToString(mimeType, capabilities));\n    }\n  }\n\n  private static String codecCapabilitiesToString(\n      String requestedMimeType, CodecCapabilities codecCapabilities) {\n    boolean isVideo = MimeTypes.isVideo(requestedMimeType);\n    boolean isAudio = MimeTypes.isAudio(requestedMimeType);\n    StringBuilder result = new StringBuilder();\n    result.append(\"[requestedMimeType=\").append(requestedMimeType);\n    if (Util.SDK_INT >= 21) {\n      result.append(\", mimeType=\").append(codecCapabilities.getMimeType());\n    }\n    result.append(\", profileLevels=\");\n    appendProfileLevels(codecCapabilities.profileLevels, result);\n    if (Util.SDK_INT >= 23) {\n      result\n          .append(\", maxSupportedInstances=\")\n          .append(codecCapabilities.getMaxSupportedInstances());\n    }\n    if (Util.SDK_INT >= 21) {\n      if (isVideo) {\n        result.append(\", videoCapabilities=\");\n        appendVideoCapabilities(codecCapabilities.getVideoCapabilities(), result);\n        result.append(\", colorFormats=\").append(Arrays.toString(codecCapabilities.colorFormats));\n      } else if (isAudio) {\n        result.append(\", audioCapabilities=\");\n        appendAudioCapabilities(codecCapabilities.getAudioCapabilities(), result);\n      }\n    }\n    if (Util.SDK_INT >= 19\n        && isVideo\n        && codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback)) {\n      result.append(\", FEATURE_AdaptivePlayback\");\n    }\n    if (Util.SDK_INT >= 21\n        && isVideo\n        && codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback)) {\n      result.append(\", FEATURE_SecurePlayback\");\n    }\n    if (Util.SDK_INT >= 26\n        && isVideo\n        && codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_PartialFrame)) {\n      result.append(\", FEATURE_PartialFrame\");\n    }\n    if (Util.SDK_INT >= 21\n        && (isVideo || isAudio)\n        && codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback)) {\n      result.append(\", FEATURE_TunneledPlayback\");\n    }\n    result.append(']');\n    return result.toString();\n  }\n\n  private static void appendAudioCapabilities(\n      AudioCapabilities audioCapabilities, StringBuilder result) {\n    result\n        .append(\"[bitrateRange=\")\n        .append(audioCapabilities.getBitrateRange())\n        .append(\", maxInputChannelCount=\")\n        .append(audioCapabilities.getMaxInputChannelCount())\n        .append(\", supportedSampleRateRanges=\")\n        .append(Arrays.toString(audioCapabilities.getSupportedSampleRateRanges()))\n        .append(']');\n  }\n\n  private static void appendVideoCapabilities(\n      VideoCapabilities videoCapabilities, StringBuilder result) {\n    result\n        .append(\"[bitrateRange=\")\n        .append(videoCapabilities.getBitrateRange())\n        .append(\", heightAlignment=\")\n        .append(videoCapabilities.getHeightAlignment())\n        .append(\", widthAlignment=\")\n        .append(videoCapabilities.getWidthAlignment())\n        .append(\", supportedWidths=\")\n        .append(videoCapabilities.getSupportedWidths())\n        .append(\", supportedHeights=\")\n        .append(videoCapabilities.getSupportedHeights())\n        .append(\", supportedFrameRates=\")\n        .append(videoCapabilities.getSupportedFrameRates())\n        .append(']');\n  }\n\n  private static void appendProfileLevels(CodecProfileLevel[] profileLevels, StringBuilder result) {\n    result.append('[');\n    int count = profileLevels.length;\n    for (int i = 0; i < count; i++) {\n      CodecProfileLevel profileLevel = profileLevels[i];\n      if (i != 0) {\n        result.append(\", \");\n      }\n      result\n          .append(\"[profile=\")\n          .append(profileLevel.profile)\n          .append(\", level=\")\n          .append(profileLevel.level)\n          .append(']');\n    }\n    result.append(']');\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/playbacktests/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest package=\"com.google.android.exoplayer2.playbacktests\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/publish.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.\nif (project.ext.has(\"exoplayerPublishEnabled\")\n        && project.ext.exoplayerPublishEnabled) {\n    apply plugin: 'bintray-release'\n    publish {\n        artifactId = releaseArtifact\n        desc = releaseDescription\n        publishVersion = releaseVersion\n        repoName = getBintrayRepo()\n        userOrg = 'google'\n        groupId = 'com.google.android.exoplayer'\n        website = 'https://github.com/google/ExoPlayer'\n    }\n\n    gradle.taskGraph.whenReady { taskGraph ->\n        project.tasks\n                .findAll { task -> task.name.contains(\"generatePomFileFor\") }\n                .forEach { task ->\n                    task.doLast {\n                        task.outputs.files\n                                .filter { File file ->\n                                    file.path.contains(\"publications\") \\\n                                        && file.name.matches(\"^pom-.+\\\\.xml\\$\")\n                                }\n                                .forEach { File file -> addLicense(file) }\n                    }\n                }\n    }\n}\n\ndef getBintrayRepo() {\n    boolean publicRepo = hasProperty('publicRepo') &&\n        property('publicRepo').toBoolean()\n    return publicRepo ? 'exoplayer' : 'exoplayer-test'\n}\n\nstatic void addLicense(File pom) {\n    def licenseNode = new Node(null, \"license\")\n    licenseNode.append(\n        new Node(null, \"name\", \"The Apache Software License, Version 2.0\"))\n    licenseNode.append(\n        new Node(null, \"url\", \"http://www.apache.org/licenses/LICENSE-2.0.txt\"))\n    licenseNode.append(new Node(null, \"distribution\", \"repo\"))\n    def licensesNode = new Node(null, \"licenses\")\n    licensesNode.append(licenseNode)\n\n    def xml = new XmlParser().parse(pom)\n    xml.append(licensesNode)\n\n    def writer = new PrintWriter(new FileWriter(pom))\n    writer.write(\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\\n\")\n    def printer = new XmlNodePrinter(writer)\n    printer.preserveWhitespace = true\n    printer.print(xml)\n    writer.close()\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/settings.gradle",
    "content": "// Copyright (C) 2016 The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\ngradle.ext.exoplayerRoot = settingsDir\n\ndef modulePrefix = ':'\nif (gradle.ext.has('exoplayerModulePrefix')) {\n    modulePrefix += gradle.ext.exoplayerModulePrefix\n}\n\ninclude modulePrefix + 'demo'\ninclude modulePrefix + 'demo-cast'\ninclude modulePrefix + 'demo-ima'\ninclude modulePrefix + 'playbacktests'\nproject(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main')\nproject(modulePrefix + 'demo-cast').projectDir = new File(rootDir, 'demos/cast')\nproject(modulePrefix + 'demo-ima').projectDir = new File(rootDir, 'demos/ima')\nproject(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests')\n\napply from: 'core_settings.gradle'\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/build.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.\napply from: '../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    lintOptions {\n        // Truth depends on JUnit, which depends on java.lang.management, which\n        // is not part of Android. Remove this when JUnit 4.13 or later is used.\n        // See: https://github.com/junit-team/junit4/pull/1187.\n        disable 'InvalidPackage'\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    api 'org.mockito:mockito-core:' + mockitoVersion\n    api 'androidx.test.ext:junit:' + androidXTestVersion\n    api 'androidx.test.ext:truth:' + androidXTestVersion\n    implementation 'androidx.annotation:annotation:1.1.0'\n    implementation project(modulePrefix + 'library-core')\n    implementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion\n    annotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion\n    testImplementation project(modulePrefix + 'testutils-robolectric')\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<manifest package=\"com.google.android.exoplayer2.testutil\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\nimport android.os.Handler;\nimport androidx.annotation.Nullable;\nimport android.view.Surface;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.PlayerMessage;\nimport com.google.android.exoplayer2.PlayerMessage.Target;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode;\nimport com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable;\nimport com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters;\nimport com.google.android.exoplayer2.util.ConditionVariable;\nimport com.google.android.exoplayer2.util.HandlerWrapper;\nimport com.google.android.exoplayer2.util.Log;\n\n/**\n * Base class for actions to perform during playback tests.\n */\npublic abstract class Action {\n\n  private final String tag;\n  private final @Nullable String description;\n\n  /**\n   * @param tag A tag to use for logging.\n   * @param description A description to be logged when the action is executed, or null if no\n   *     logging is required.\n   */\n  public Action(String tag, @Nullable String description) {\n    this.tag = tag;\n    this.description = description;\n  }\n\n  /**\n   * Executes the action and schedules the next.\n   *\n   * @param player The player to which the action should be applied.\n   * @param trackSelector The track selector to which the action should be applied.\n   * @param surface The surface to use when applying actions.\n   * @param handler The handler to use to pass to the next action.\n   * @param nextAction The next action to schedule immediately after this action finished.\n   */\n  public final void doActionAndScheduleNext(\n      SimpleExoPlayer player,\n      DefaultTrackSelector trackSelector,\n      Surface surface,\n      HandlerWrapper handler,\n      ActionNode nextAction) {\n    if (description != null) {\n      Log.i(tag, description);\n    }\n    doActionAndScheduleNextImpl(player, trackSelector, surface, handler, nextAction);\n  }\n\n  /**\n   * Called by {@link #doActionAndScheduleNext(SimpleExoPlayer, DefaultTrackSelector, Surface,\n   * HandlerWrapper, ActionNode)} to perform the action and to schedule the next action node.\n   *\n   * @param player The player to which the action should be applied.\n   * @param trackSelector The track selector to which the action should be applied.\n   * @param surface The surface to use when applying actions.\n   * @param handler The handler to use to pass to the next action.\n   * @param nextAction The next action to schedule immediately after this action finished.\n   */\n  protected void doActionAndScheduleNextImpl(\n      SimpleExoPlayer player,\n      DefaultTrackSelector trackSelector,\n      Surface surface,\n      HandlerWrapper handler,\n      ActionNode nextAction) {\n    doActionImpl(player, trackSelector, surface);\n    if (nextAction != null) {\n      nextAction.schedule(player, trackSelector, surface, handler);\n    }\n  }\n\n  /**\n   * Called by {@link #doActionAndScheduleNextImpl(SimpleExoPlayer, DefaultTrackSelector, Surface,\n   * HandlerWrapper, ActionNode)} to perform the action.\n   *\n   * @param player The player to which the action should be applied.\n   * @param trackSelector The track selector to which the action should be applied.\n   * @param surface The surface to use when applying actions.\n   */\n  protected abstract void doActionImpl(\n      SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface);\n\n  /**\n   * Calls {@link Player#seekTo(long)} or {@link Player#seekTo(int, long)}.\n   */\n  public static final class Seek extends Action {\n\n    private final Integer windowIndex;\n    private final long positionMs;\n\n    /**\n     * Action calls {@link Player#seekTo(long)}.\n     *\n     * @param tag A tag to use for logging.\n     * @param positionMs The seek position.\n     */\n    public Seek(String tag, long positionMs) {\n      super(tag, \"Seek:\" + positionMs);\n      this.windowIndex = null;\n      this.positionMs = positionMs;\n    }\n\n    /**\n     * Action calls {@link Player#seekTo(int, long)}.\n     *\n     * @param tag A tag to use for logging.\n     * @param windowIndex The window to seek to.\n     * @param positionMs The seek position.\n     */\n    public Seek(String tag, int windowIndex, long positionMs) {\n      super(tag, \"Seek:\" + positionMs);\n      this.windowIndex = windowIndex;\n      this.positionMs = positionMs;\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      if (windowIndex == null) {\n        player.seekTo(positionMs);\n      } else {\n        player.seekTo(windowIndex, positionMs);\n      }\n    }\n\n  }\n\n  /**\n   * Calls {@link Player#stop()} or {@link Player#stop(boolean)}.\n   */\n  public static final class Stop extends Action {\n\n    private static final String STOP_ACTION_TAG = \"Stop\";\n\n    private final Boolean reset;\n\n    /**\n     * Action will call {@link Player#stop()}.\n     *\n     * @param tag A tag to use for logging.\n     */\n    public Stop(String tag) {\n      super(tag, STOP_ACTION_TAG);\n      this.reset = null;\n    }\n\n    /**\n     * Action will call {@link Player#stop(boolean)}.\n     *\n     * @param tag A tag to use for logging.\n     * @param reset The value to pass to {@link Player#stop(boolean)}.\n     */\n    public Stop(String tag, boolean reset) {\n      super(tag, STOP_ACTION_TAG);\n      this.reset = reset;\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      if (reset == null) {\n        player.stop();\n      } else {\n        player.stop(reset);\n      }\n\n    }\n\n  }\n\n  /**\n   * Calls {@link Player#setPlayWhenReady(boolean)}.\n   */\n  public static final class SetPlayWhenReady extends Action {\n\n    private final boolean playWhenReady;\n\n    /**\n     * @param tag A tag to use for logging.\n     * @param playWhenReady The value to pass.\n     */\n    public SetPlayWhenReady(String tag, boolean playWhenReady) {\n      super(tag, playWhenReady ? \"Play\" : \"Pause\");\n      this.playWhenReady = playWhenReady;\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      player.setPlayWhenReady(playWhenReady);\n    }\n\n  }\n\n  /**\n   * Updates the {@link Parameters} of a {@link DefaultTrackSelector} to specify whether the\n   * renderer at a given index should be disabled.\n   */\n  public static final class SetRendererDisabled extends Action {\n\n    private final int rendererIndex;\n    private final boolean disabled;\n\n    /**\n     * @param tag A tag to use for logging.\n     * @param rendererIndex The index of the renderer.\n     * @param disabled Whether the renderer should be disabled.\n     */\n    public SetRendererDisabled(String tag, int rendererIndex, boolean disabled) {\n      super(tag, \"SetRendererDisabled:\" + rendererIndex + \":\" + disabled);\n      this.rendererIndex = rendererIndex;\n      this.disabled = disabled;\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      trackSelector.setParameters(\n          trackSelector.buildUponParameters().setRendererDisabled(rendererIndex, disabled));\n    }\n\n  }\n\n  /**\n   * Calls {@link SimpleExoPlayer#clearVideoSurface()}.\n   */\n  public static final class ClearVideoSurface extends Action {\n\n    /**\n     * @param tag A tag to use for logging.\n     */\n    public ClearVideoSurface(String tag) {\n      super(tag, \"ClearVideoSurface\");\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      player.clearVideoSurface();\n    }\n\n  }\n\n  /**\n   * Calls {@link SimpleExoPlayer#setVideoSurface(Surface)}.\n   */\n  public static final class SetVideoSurface extends Action {\n\n    /**\n     * @param tag A tag to use for logging.\n     */\n    public SetVideoSurface(String tag) {\n      super(tag, \"SetVideoSurface\");\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      player.setVideoSurface(surface);\n    }\n\n  }\n\n  /**\n   * Calls {@link ExoPlayer#prepare(MediaSource)}.\n   */\n  public static final class PrepareSource extends Action {\n\n    private final MediaSource mediaSource;\n    private final boolean resetPosition;\n    private final boolean resetState;\n\n    /**\n     * @param tag A tag to use for logging.\n     */\n    public PrepareSource(String tag, MediaSource mediaSource) {\n      this(tag, mediaSource, true, true);\n    }\n\n    /**\n     * @param tag A tag to use for logging.\n     */\n    public PrepareSource(String tag, MediaSource mediaSource, boolean resetPosition,\n        boolean resetState) {\n      super(tag, \"PrepareSource\");\n      this.mediaSource = mediaSource;\n      this.resetPosition = resetPosition;\n      this.resetState = resetState;\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      player.prepare(mediaSource, resetPosition, resetState);\n    }\n\n  }\n\n  /**\n   * Calls {@link Player#setRepeatMode(int)}.\n   */\n  public static final class SetRepeatMode extends Action {\n\n    private final @Player.RepeatMode int repeatMode;\n\n    /**\n     * @param tag A tag to use for logging.\n     */\n    public SetRepeatMode(String tag, @Player.RepeatMode int repeatMode) {\n      super(tag, \"SetRepeatMode:\" + repeatMode);\n      this.repeatMode = repeatMode;\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      player.setRepeatMode(repeatMode);\n    }\n\n  }\n\n  /**\n   * Calls {@link Player#setShuffleModeEnabled(boolean)}.\n   */\n  public static final class SetShuffleModeEnabled extends Action {\n\n    private final boolean shuffleModeEnabled;\n\n    /**\n     * @param tag A tag to use for logging.\n     */\n    public SetShuffleModeEnabled(String tag, boolean shuffleModeEnabled) {\n      super(tag, \"SetShuffleModeEnabled:\" + shuffleModeEnabled);\n      this.shuffleModeEnabled = shuffleModeEnabled;\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      player.setShuffleModeEnabled(shuffleModeEnabled);\n    }\n  }\n\n  /** Calls {@link ExoPlayer#createMessage(Target)} and {@link PlayerMessage#send()}. */\n  public static final class SendMessages extends Action {\n\n    private final Target target;\n    private final int windowIndex;\n    private final long positionMs;\n    private final boolean deleteAfterDelivery;\n\n    /**\n     * @param tag A tag to use for logging.\n     * @param target A message target.\n     * @param positionMs The position at which the message should be sent, in milliseconds.\n     */\n    public SendMessages(String tag, Target target, long positionMs) {\n      this(\n          tag,\n          target,\n          /* windowIndex= */ C.INDEX_UNSET,\n          positionMs,\n          /* deleteAfterDelivery= */ true);\n    }\n\n    /**\n     * @param tag A tag to use for logging.\n     * @param target A message target.\n     * @param windowIndex The window index at which the message should be sent, or {@link\n     *     C#INDEX_UNSET} for the current window.\n     * @param positionMs The position at which the message should be sent, in milliseconds.\n     * @param deleteAfterDelivery Whether the message will be deleted after delivery.\n     */\n    public SendMessages(\n        String tag, Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) {\n      super(tag, \"SendMessages\");\n      this.target = target;\n      this.windowIndex = windowIndex;\n      this.positionMs = positionMs;\n      this.deleteAfterDelivery = deleteAfterDelivery;\n    }\n\n    @Override\n    protected void doActionImpl(\n        final SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      if (target instanceof PlayerTarget) {\n        ((PlayerTarget) target).setPlayer(player);\n      }\n      PlayerMessage message = player.createMessage(target);\n      if (windowIndex != C.INDEX_UNSET) {\n        message.setPosition(windowIndex, positionMs);\n      } else {\n        message.setPosition(positionMs);\n      }\n      message.setHandler(new Handler());\n      message.setDeleteAfterDelivery(deleteAfterDelivery);\n      message.send();\n    }\n  }\n\n  /**\n   * Calls {@link Player#setPlaybackParameters(PlaybackParameters)}.\n   */\n  public static final class SetPlaybackParameters extends Action {\n\n    private final PlaybackParameters playbackParameters;\n\n    /**\n     * @param tag A tag to use for logging.\n     * @param playbackParameters The playback parameters.\n     */\n    public SetPlaybackParameters(String tag, PlaybackParameters playbackParameters) {\n      super(tag, \"SetPlaybackParameters:\" + playbackParameters);\n      this.playbackParameters = playbackParameters;\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      player.setPlaybackParameters(playbackParameters);\n    }\n\n  }\n\n  /** Throws a playback exception on the playback thread. */\n  public static final class ThrowPlaybackException extends Action {\n\n    private final ExoPlaybackException exception;\n\n    /**\n     * @param tag A tag to use for logging.\n     * @param exception The exception to throw.\n     */\n    public ThrowPlaybackException(String tag, ExoPlaybackException exception) {\n      super(tag, \"ThrowPlaybackException:\" + exception);\n      this.exception = exception;\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      player\n          .createMessage(\n              (messageType, payload) -> {\n                throw exception;\n              })\n          .send();\n    }\n  }\n\n  /**\n   * Schedules a play action to be executed, waits until the player reaches the specified position,\n   * and pauses the player again.\n   */\n  public static final class PlayUntilPosition extends Action {\n\n    private final int windowIndex;\n    private final long positionMs;\n\n    /**\n     * @param tag A tag to use for logging.\n     * @param windowIndex The window index at which the player should be paused again.\n     * @param positionMs The position in that window at which the player should be paused again.\n     */\n    public PlayUntilPosition(String tag, int windowIndex, long positionMs) {\n      super(tag, \"PlayUntilPosition:\" + windowIndex + \",\" + positionMs);\n      this.windowIndex = windowIndex;\n      this.positionMs = positionMs;\n    }\n\n    @Override\n    protected void doActionAndScheduleNextImpl(\n        final SimpleExoPlayer player,\n        final DefaultTrackSelector trackSelector,\n        final Surface surface,\n        final HandlerWrapper handler,\n        final ActionNode nextAction) {\n      Handler testThreadHandler = new Handler();\n      // Schedule a message on the playback thread to ensure the player is paused immediately.\n      player\n          .createMessage(\n              (messageType, payload) -> {\n                // Block playback thread until pause command has been sent from test thread.\n                ConditionVariable blockPlaybackThreadCondition = new ConditionVariable();\n                testThreadHandler.post(\n                    () -> {\n                      player.setPlayWhenReady(/* playWhenReady= */ false);\n                      blockPlaybackThreadCondition.open();\n                    });\n                try {\n                  blockPlaybackThreadCondition.block();\n                } catch (InterruptedException e) {\n                  // Ignore.\n                }\n              })\n          .setPosition(windowIndex, positionMs)\n          .send();\n      // Schedule another message on this test thread to continue action schedule.\n      player\n          .createMessage(\n              (messageType, payload) ->\n                  nextAction.schedule(player, trackSelector, surface, handler))\n          .setPosition(windowIndex, positionMs)\n          .setHandler(testThreadHandler)\n          .send();\n      player.setPlayWhenReady(true);\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      // Not triggered.\n    }\n  }\n\n  /**\n   * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)}.\n   */\n  public static final class WaitForTimelineChanged extends Action {\n\n    private final @Nullable Timeline expectedTimeline;\n\n    /**\n     * Creates action waiting for a timeline change.\n     *\n     * @param tag A tag to use for logging.\n     * @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline\n     *     change.\n     */\n    public WaitForTimelineChanged(String tag, @Nullable Timeline expectedTimeline) {\n      super(tag, \"WaitForTimelineChanged\");\n      this.expectedTimeline = expectedTimeline;\n    }\n\n    @Override\n    protected void doActionAndScheduleNextImpl(\n        final SimpleExoPlayer player,\n        final DefaultTrackSelector trackSelector,\n        final Surface surface,\n        final HandlerWrapper handler,\n        final ActionNode nextAction) {\n      if (nextAction == null) {\n        return;\n      }\n      Player.EventListener listener =\n          new Player.EventListener() {\n            @Override\n            public void onTimelineChanged(\n                Timeline timeline,\n                @Nullable Object manifest,\n                @Player.TimelineChangeReason int reason) {\n              if (expectedTimeline == null || timeline.equals(expectedTimeline)) {\n                player.removeListener(this);\n                nextAction.schedule(player, trackSelector, surface, handler);\n              }\n            }\n          };\n      player.addListener(listener);\n      if (expectedTimeline != null && player.getCurrentTimeline().equals(expectedTimeline)) {\n        player.removeListener(listener);\n        nextAction.schedule(player, trackSelector, surface, handler);\n      }\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      // Not triggered.\n    }\n  }\n\n  /**\n   * Waits for {@link Player.EventListener#onPositionDiscontinuity(int)}.\n   */\n  public static final class WaitForPositionDiscontinuity extends Action {\n\n    /**\n     * @param tag A tag to use for logging.\n     */\n    public WaitForPositionDiscontinuity(String tag) {\n      super(tag, \"WaitForPositionDiscontinuity\");\n    }\n\n    @Override\n    protected void doActionAndScheduleNextImpl(\n        final SimpleExoPlayer player,\n        final DefaultTrackSelector trackSelector,\n        final Surface surface,\n        final HandlerWrapper handler,\n        final ActionNode nextAction) {\n      if (nextAction == null) {\n        return;\n      }\n      player.addListener(\n          new Player.EventListener() {\n            @Override\n            public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {\n              player.removeListener(this);\n              nextAction.schedule(player, trackSelector, surface, handler);\n            }\n          });\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      // Not triggered.\n    }\n  }\n\n  /**\n   * Waits for a specified playback state, returning either immediately or after a call to\n   * {@link Player.EventListener#onPlayerStateChanged(boolean, int)}.\n   */\n  public static final class WaitForPlaybackState extends Action {\n\n    private final int targetPlaybackState;\n\n    /**\n     * @param tag A tag to use for logging.\n     */\n    public WaitForPlaybackState(String tag, int targetPlaybackState) {\n      super(tag, \"WaitForPlaybackState\");\n      this.targetPlaybackState = targetPlaybackState;\n    }\n\n    @Override\n    protected void doActionAndScheduleNextImpl(\n        final SimpleExoPlayer player,\n        final DefaultTrackSelector trackSelector,\n        final Surface surface,\n        final HandlerWrapper handler,\n        final ActionNode nextAction) {\n      if (nextAction == null) {\n        return;\n      }\n      if (targetPlaybackState == player.getPlaybackState()) {\n        nextAction.schedule(player, trackSelector, surface, handler);\n      } else {\n        player.addListener(\n            new Player.EventListener() {\n              @Override\n              public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n                if (targetPlaybackState == playbackState) {\n                  player.removeListener(this);\n                  nextAction.schedule(player, trackSelector, surface, handler);\n                }\n              }\n            });\n      }\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      // Not triggered.\n    }\n  }\n\n  /**\n   * Waits for a specified loading state, returning either immediately or after a call to {@link\n   * Player.EventListener#onLoadingChanged(boolean)}.\n   */\n  public static final class WaitForIsLoading extends Action {\n\n    private final boolean targetIsLoading;\n\n    /**\n     * @param tag A tag to use for logging.\n     * @param targetIsLoading The loading state to wait for.\n     */\n    public WaitForIsLoading(String tag, boolean targetIsLoading) {\n      super(tag, \"WaitForIsLoading\");\n      this.targetIsLoading = targetIsLoading;\n    }\n\n    @Override\n    protected void doActionAndScheduleNextImpl(\n        final SimpleExoPlayer player,\n        final DefaultTrackSelector trackSelector,\n        final Surface surface,\n        final HandlerWrapper handler,\n        final ActionNode nextAction) {\n      if (nextAction == null) {\n        return;\n      }\n      if (targetIsLoading == player.isLoading()) {\n        nextAction.schedule(player, trackSelector, surface, handler);\n      } else {\n        player.addListener(\n            new Player.EventListener() {\n              @Override\n              public void onLoadingChanged(boolean isLoading) {\n                if (targetIsLoading == isLoading) {\n                  player.removeListener(this);\n                  nextAction.schedule(player, trackSelector, surface, handler);\n                }\n              }\n            });\n      }\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      // Not triggered.\n    }\n  }\n\n  /**\n   * Waits for {@link Player.EventListener#onSeekProcessed()}.\n   */\n  public static final class WaitForSeekProcessed extends Action {\n\n    /**\n     * @param tag A tag to use for logging.\n     */\n    public WaitForSeekProcessed(String tag) {\n      super(tag, \"WaitForSeekProcessed\");\n    }\n\n    @Override\n    protected void doActionAndScheduleNextImpl(\n        final SimpleExoPlayer player,\n        final DefaultTrackSelector trackSelector,\n        final Surface surface,\n        final HandlerWrapper handler,\n        final ActionNode nextAction) {\n      if (nextAction == null) {\n        return;\n      }\n      player.addListener(\n          new Player.EventListener() {\n            @Override\n            public void onSeekProcessed() {\n              player.removeListener(this);\n              nextAction.schedule(player, trackSelector, surface, handler);\n            }\n          });\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      // Not triggered.\n    }\n  }\n\n  /**\n   * Calls {@link Runnable#run()}.\n   */\n  public static final class ExecuteRunnable extends Action {\n\n    private final Runnable runnable;\n\n    /**\n     * @param tag A tag to use for logging.\n     */\n    public ExecuteRunnable(String tag, Runnable runnable) {\n      super(tag, \"ExecuteRunnable\");\n      this.runnable = runnable;\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      if (runnable instanceof PlayerRunnable) {\n        ((PlayerRunnable) runnable).setPlayer(player);\n      }\n      runnable.run();\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\nimport android.os.Looper;\nimport androidx.annotation.Nullable;\nimport android.view.Surface;\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.PlayerMessage;\nimport com.google.android.exoplayer2.PlayerMessage.Target;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.testutil.Action.ClearVideoSurface;\nimport com.google.android.exoplayer2.testutil.Action.ExecuteRunnable;\nimport com.google.android.exoplayer2.testutil.Action.PlayUntilPosition;\nimport com.google.android.exoplayer2.testutil.Action.PrepareSource;\nimport com.google.android.exoplayer2.testutil.Action.Seek;\nimport com.google.android.exoplayer2.testutil.Action.SendMessages;\nimport com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady;\nimport com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters;\nimport com.google.android.exoplayer2.testutil.Action.SetRendererDisabled;\nimport com.google.android.exoplayer2.testutil.Action.SetRepeatMode;\nimport com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled;\nimport com.google.android.exoplayer2.testutil.Action.SetVideoSurface;\nimport com.google.android.exoplayer2.testutil.Action.Stop;\nimport com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException;\nimport com.google.android.exoplayer2.testutil.Action.WaitForIsLoading;\nimport com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState;\nimport com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity;\nimport com.google.android.exoplayer2.testutil.Action.WaitForSeekProcessed;\nimport com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.HandlerWrapper;\n\n/**\n * Schedules a sequence of {@link Action}s for execution during a test.\n */\npublic final class ActionSchedule {\n\n  /**\n   * Callback to notify listener that the action schedule has finished.\n   */\n  public interface Callback {\n\n    /**\n     * Called when action schedule finished executing all its actions.\n     */\n    void onActionScheduleFinished();\n\n  }\n\n  private final ActionNode rootNode;\n  private final CallbackAction callbackAction;\n\n  /**\n   * @param rootNode The first node in the sequence.\n   * @param callbackAction The final action which can be used to trigger a callback.\n   */\n  private ActionSchedule(ActionNode rootNode, CallbackAction callbackAction) {\n    this.rootNode = rootNode;\n    this.callbackAction = callbackAction;\n  }\n\n  /**\n   * Starts execution of the schedule.\n   *\n   * @param player The player to which actions should be applied.\n   * @param trackSelector The track selector to which actions should be applied.\n   * @param surface The surface to use when applying actions.\n   * @param mainHandler A handler associated with the main thread of the host activity.\n   * @param callback A {@link Callback} to notify when the action schedule finishes, or null if no\n   *     notification is needed.\n   */\n  /* package */ void start(\n      SimpleExoPlayer player,\n      DefaultTrackSelector trackSelector,\n      Surface surface,\n      HandlerWrapper mainHandler,\n      @Nullable Callback callback) {\n    callbackAction.setCallback(callback);\n    rootNode.schedule(player, trackSelector, surface, mainHandler);\n  }\n\n  /**\n   * A builder for {@link ActionSchedule} instances.\n   */\n  public static final class Builder {\n\n    private final String tag;\n    private final ActionNode rootNode;\n\n    private long currentDelayMs;\n    private ActionNode previousNode;\n\n    /**\n     * @param tag A tag to use for logging.\n     */\n    public Builder(String tag) {\n      this.tag = tag;\n      rootNode = new ActionNode(new RootAction(tag), 0);\n      previousNode = rootNode;\n    }\n\n    /**\n     * Schedules a delay between executing any previous actions and any subsequent ones.\n     *\n     * @param delayMs The delay in milliseconds.\n     * @return The builder, for convenience.\n     */\n    public Builder delay(long delayMs) {\n      currentDelayMs += delayMs;\n      return this;\n    }\n\n    /**\n     * Schedules an action to be executed.\n     *\n     * @param action The action to schedule.\n     * @return The builder, for convenience.\n     */\n    public Builder apply(Action action) {\n      return appendActionNode(new ActionNode(action, currentDelayMs));\n    }\n\n    /**\n     * Schedules an action to be executed repeatedly.\n     *\n     * @param action The action to schedule.\n     * @param intervalMs The interval between each repetition in milliseconds.\n     * @return The builder, for convenience.\n     */\n    public Builder repeat(Action action, long intervalMs) {\n      return appendActionNode(new ActionNode(action, currentDelayMs, intervalMs));\n    }\n\n    /**\n     * Schedules a seek action to be executed.\n     *\n     * @param positionMs The seek position.\n     * @return The builder, for convenience.\n     */\n    public Builder seek(long positionMs) {\n      return apply(new Seek(tag, positionMs));\n    }\n\n    /**\n     * Schedules a seek action to be executed.\n     *\n     * @param windowIndex The window to seek to.\n     * @param positionMs The seek position.\n     * @return The builder, for convenience.\n     */\n    public Builder seek(int windowIndex, long positionMs) {\n      return apply(new Seek(tag, windowIndex, positionMs));\n    }\n\n    /**\n     * Schedules a seek action to be executed and waits until playback resumes after the seek.\n     *\n     * @param positionMs The seek position.\n     * @return The builder, for convenience.\n     */\n    public Builder seekAndWait(long positionMs) {\n      return apply(new Seek(tag, positionMs))\n          .apply(new WaitForSeekProcessed(tag))\n          .apply(new WaitForPlaybackState(tag, Player.STATE_READY));\n    }\n\n    /**\n     * Schedules a delay until the player indicates that a seek has been processed.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder waitForSeekProcessed() {\n      return apply(new WaitForSeekProcessed(tag));\n    }\n\n    /**\n     * Schedules a playback parameters setting action to be executed.\n     *\n     * @param playbackParameters The playback parameters to set.\n     * @return The builder, for convenience.\n     * @see Player#setPlaybackParameters(PlaybackParameters)\n     */\n    public Builder setPlaybackParameters(PlaybackParameters playbackParameters) {\n      return apply(new SetPlaybackParameters(tag, playbackParameters));\n    }\n\n    /**\n     * Schedules a stop action to be executed.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder stop() {\n      return apply(new Stop(tag));\n    }\n\n    /**\n     * Schedules a stop action to be executed.\n     *\n     * @param reset Whether the player should be reset.\n     * @return The builder, for convenience.\n     */\n    public Builder stop(boolean reset) {\n      return apply(new Stop(tag, reset));\n    }\n\n    /**\n     * Schedules a play action to be executed.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder play() {\n      return apply(new SetPlayWhenReady(tag, true));\n    }\n\n    /**\n     * Schedules a play action to be executed, waits until the player reaches the specified\n     * position, and pauses the player again.\n     *\n     * @param windowIndex The window index at which the player should be paused again.\n     * @param positionMs The position in that window at which the player should be paused again.\n     * @return The builder, for convenience.\n     */\n    public Builder playUntilPosition(int windowIndex, long positionMs) {\n      return apply(new PlayUntilPosition(tag, windowIndex, positionMs));\n    }\n\n    /**\n     * Schedules a play action to be executed, waits until the player reaches the start of the\n     * specified window, and pauses the player again.\n     *\n     * @param windowIndex The window index at which the player should be paused again.\n     * @return The builder, for convenience.\n     */\n    public Builder playUntilStartOfWindow(int windowIndex) {\n      return apply(new PlayUntilPosition(tag, windowIndex, /* positionMs= */ 0));\n    }\n\n    /**\n     * Schedules a pause action to be executed.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder pause() {\n      return apply(new SetPlayWhenReady(tag, false));\n    }\n\n    /**\n     * Schedules a renderer enable action to be executed.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder enableRenderer(int index) {\n      return apply(new SetRendererDisabled(tag, index, false));\n    }\n\n    /**\n     * Schedules a renderer disable action to be executed.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder disableRenderer(int index) {\n      return apply(new SetRendererDisabled(tag, index, true));\n    }\n\n    /**\n     * Schedules a clear video surface action to be executed.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder clearVideoSurface() {\n      return apply(new ClearVideoSurface(tag));\n    }\n\n    /**\n     * Schedules a set video surface action to be executed.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder setVideoSurface() {\n      return apply(new SetVideoSurface(tag));\n    }\n\n    /**\n     * Schedules a new source preparation action to be executed.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder prepareSource(MediaSource mediaSource) {\n      return apply(new PrepareSource(tag, mediaSource));\n    }\n\n    /**\n     * Schedules a new source preparation action to be executed.\n     * @see com.google.android.exoplayer2.ExoPlayer#prepare(MediaSource, boolean, boolean).\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder prepareSource(MediaSource mediaSource, boolean resetPosition,\n        boolean resetState) {\n      return apply(new PrepareSource(tag, mediaSource, resetPosition, resetState));\n    }\n\n    /**\n     * Schedules a repeat mode setting action to be executed.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder setRepeatMode(@Player.RepeatMode int repeatMode) {\n      return apply(new SetRepeatMode(tag, repeatMode));\n    }\n\n    /**\n     * Schedules a shuffle setting action to be executed.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder setShuffleModeEnabled(boolean shuffleModeEnabled) {\n      return apply(new SetShuffleModeEnabled(tag, shuffleModeEnabled));\n    }\n\n    /**\n     * Schedules sending a {@link PlayerMessage}.\n     *\n     * @param positionMs The position in the current window at which the message should be sent, in\n     *     milliseconds.\n     * @return The builder, for convenience.\n     */\n    public Builder sendMessage(Target target, long positionMs) {\n      return apply(new SendMessages(tag, target, positionMs));\n    }\n\n    /**\n     * Schedules sending a {@link PlayerMessage}.\n     *\n     * @param target A message target.\n     * @param windowIndex The window index at which the message should be sent.\n     * @param positionMs The position at which the message should be sent, in milliseconds.\n     * @return The builder, for convenience.\n     */\n    public Builder sendMessage(Target target, int windowIndex, long positionMs) {\n      return apply(\n          new SendMessages(tag, target, windowIndex, positionMs, /* deleteAfterDelivery= */ true));\n    }\n\n    /**\n     * Schedules to send a {@link PlayerMessage}.\n     *\n     * @param target A message target.\n     * @param windowIndex The window index at which the message should be sent.\n     * @param positionMs The position at which the message should be sent, in milliseconds.\n     * @param deleteAfterDelivery Whether the message will be deleted after delivery.\n     * @return The builder, for convenience.\n     */\n    public Builder sendMessage(\n        Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) {\n      return apply(new SendMessages(tag, target, windowIndex, positionMs, deleteAfterDelivery));\n    }\n\n    /**\n     * Schedules a delay until any timeline change.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder waitForTimelineChanged() {\n      return apply(new WaitForTimelineChanged(tag, /* expectedTimeline= */ null));\n    }\n\n    /**\n     * Schedules a delay until the timeline changed to a specified expected timeline.\n     *\n     * @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline\n     *     change.\n     * @return The builder, for convenience.\n     */\n    public Builder waitForTimelineChanged(Timeline expectedTimeline) {\n      return apply(new WaitForTimelineChanged(tag, expectedTimeline));\n    }\n\n    /**\n     * Schedules a delay until the next position discontinuity.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder waitForPositionDiscontinuity() {\n      return apply(new WaitForPositionDiscontinuity(tag));\n    }\n\n    /**\n     * Schedules a delay until the playback state changed to the specified state.\n     *\n     * @param targetPlaybackState The target playback state.\n     * @return The builder, for convenience.\n     */\n    public Builder waitForPlaybackState(int targetPlaybackState) {\n      return apply(new WaitForPlaybackState(tag, targetPlaybackState));\n    }\n\n    /**\n     * Schedules a delay until {@code player.isLoading()} changes to the specified value.\n     *\n     * @param targetIsLoading The target value of {@code player.isLoading()}.\n     * @return The builder, for convenience.\n     */\n    public Builder waitForIsLoading(boolean targetIsLoading) {\n      return apply(new WaitForIsLoading(tag, targetIsLoading));\n    }\n\n    /**\n     * Schedules a {@link Runnable} to be executed.\n     *\n     * @return The builder, for convenience.\n     */\n    public Builder executeRunnable(Runnable runnable) {\n      return apply(new ExecuteRunnable(tag, runnable));\n    }\n\n    /**\n     * Schedules to throw a playback exception on the playback thread.\n     *\n     * @param exception The exception to throw.\n     * @return The builder, for convenience.\n     */\n    public Builder throwPlaybackException(ExoPlaybackException exception) {\n      return apply(new ThrowPlaybackException(tag, exception));\n    }\n\n    public ActionSchedule build() {\n      CallbackAction callbackAction = new CallbackAction(tag);\n      apply(callbackAction);\n      return new ActionSchedule(rootNode, callbackAction);\n    }\n\n    private Builder appendActionNode(ActionNode actionNode) {\n      previousNode.setNext(actionNode);\n      previousNode = actionNode;\n      currentDelayMs = 0;\n      return this;\n    }\n  }\n\n  /**\n   * Provides a wrapper for a {@link Target} which has access to the player when handling messages.\n   * Can be used with {@link Builder#sendMessage(Target, long)}.\n   */\n  public abstract static class PlayerTarget implements Target {\n\n    private SimpleExoPlayer player;\n\n    /** Handles the message send to the component and additionally provides access to the player. */\n    public abstract void handleMessage(\n        SimpleExoPlayer player, int messageType, @Nullable Object message);\n\n    /** Sets the player to be passed to {@link #handleMessage(SimpleExoPlayer, int, Object)}. */\n    /* package */ void setPlayer(SimpleExoPlayer player) {\n      this.player = player;\n    }\n\n    @Override\n    public final void handleMessage(int messageType, @Nullable Object message)\n        throws ExoPlaybackException {\n      handleMessage(player, messageType, message);\n    }\n  }\n\n  /**\n   * Provides a wrapper for a {@link Runnable} which has access to the player. Can be used with\n   * {@link Builder#executeRunnable(Runnable)}.\n   */\n  public abstract static class PlayerRunnable implements Runnable {\n\n    private SimpleExoPlayer player;\n\n    /** Executes Runnable with reference to player. */\n    public abstract void run(SimpleExoPlayer player);\n\n    /** Sets the player to be passed to {@link #run(SimpleExoPlayer)} . */\n    /* package */ void setPlayer(SimpleExoPlayer player) {\n      this.player = player;\n    }\n\n    @Override\n    public final void run() {\n      run(player);\n    }\n  }\n\n  /**\n   * Wraps an {@link Action}, allowing a delay and a next {@link Action} to be specified.\n   */\n  /* package */ static final class ActionNode implements Runnable {\n\n    private final Action action;\n    private final long delayMs;\n    private final long repeatIntervalMs;\n\n    private ActionNode next;\n\n    private SimpleExoPlayer player;\n    private DefaultTrackSelector trackSelector;\n    private Surface surface;\n    private HandlerWrapper mainHandler;\n\n    /**\n     * @param action The wrapped action.\n     * @param delayMs The delay between the node being scheduled and the action being executed.\n     */\n    public ActionNode(Action action, long delayMs) {\n      this(action, delayMs, C.TIME_UNSET);\n    }\n\n    /**\n     * @param action The wrapped action.\n     * @param delayMs The delay between the node being scheduled and the action being executed.\n     * @param repeatIntervalMs The interval between one execution and the next repetition. If set to\n     *     {@link C#TIME_UNSET}, the action is executed once only.\n     */\n    public ActionNode(Action action, long delayMs, long repeatIntervalMs) {\n      this.action = action;\n      this.delayMs = delayMs;\n      this.repeatIntervalMs = repeatIntervalMs;\n    }\n\n    /**\n     * Sets the next action.\n     *\n     * @param next The next {@link Action}.\n     */\n    public void setNext(ActionNode next) {\n      this.next = next;\n    }\n\n    /**\n     * Schedules {@link #action} to be executed after {@link #delayMs}. The {@link #next} node will\n     * be scheduled immediately after {@link #action} is executed.\n     *\n     * @param player The player to which actions should be applied.\n     * @param trackSelector The track selector to which actions should be applied.\n     * @param surface The surface to use when applying actions.\n     * @param mainHandler A handler associated with the main thread of the host activity.\n     */\n    public void schedule(\n        SimpleExoPlayer player,\n        DefaultTrackSelector trackSelector,\n        Surface surface,\n        HandlerWrapper mainHandler) {\n      this.player = player;\n      this.trackSelector = trackSelector;\n      this.surface = surface;\n      this.mainHandler = mainHandler;\n      if (delayMs == 0 && Looper.myLooper() == mainHandler.getLooper()) {\n        run();\n      } else {\n        mainHandler.postDelayed(this, delayMs);\n      }\n    }\n\n    @Override\n    public void run() {\n      action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, next);\n      if (repeatIntervalMs != C.TIME_UNSET) {\n        mainHandler.postDelayed(\n            new Runnable() {\n              @Override\n              public void run() {\n                action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, null);\n                mainHandler.postDelayed(this, repeatIntervalMs);\n              }\n            },\n            repeatIntervalMs);\n      }\n    }\n\n  }\n\n  /**\n   * A no-op root action.\n   */\n  private static final class RootAction extends Action {\n\n    public RootAction(String tag) {\n      super(tag, \"Root\");\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      // Do nothing.\n    }\n  }\n\n  /**\n   * An action calling a specified {@link ActionSchedule.Callback}.\n   */\n  private static final class CallbackAction extends Action {\n\n    private @Nullable Callback callback;\n\n    public CallbackAction(String tag) {\n      super(tag, \"FinishedCallback\");\n    }\n\n    public void setCallback(@Nullable Callback callback) {\n      this.callback = callback;\n    }\n\n    @Override\n    protected void doActionAndScheduleNextImpl(\n        SimpleExoPlayer player,\n        DefaultTrackSelector trackSelector,\n        Surface surface,\n        HandlerWrapper handler,\n        ActionNode nextAction) {\n      Assertions.checkArgument(nextAction == null);\n      if (callback != null) {\n        handler.post(() -> callback.onActionScheduleFinished());\n      }\n    }\n\n    @Override\n    protected void doActionImpl(\n        SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {\n      // Not triggered.\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/AutoAdvancingFakeClock.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.testutil;\n\nimport com.google.android.exoplayer2.util.HandlerWrapper;\n\n/**\n * {@link FakeClock} extension which automatically advances time whenever an empty message is\n * enqueued at a future time. The clock time is advanced to the time of the message. Only the first\n * Handler sending messages at a future time will be allowed to advance time to ensure there is only\n * one \"time master\". This should usually be the Handler of the internal playback loop.\n */\npublic final class AutoAdvancingFakeClock extends FakeClock {\n\n  private HandlerWrapper autoAdvancingHandler;\n\n  public AutoAdvancingFakeClock() {\n    super(/* initialTimeMs= */ 0);\n  }\n\n  @Override\n  protected synchronized boolean addHandlerMessageAtTime(\n      HandlerWrapper handler, int message, long timeMs) {\n    boolean result = super.addHandlerMessageAtTime(handler, message, timeMs);\n    if (autoAdvancingHandler == null || autoAdvancingHandler == handler) {\n      autoAdvancingHandler = handler;\n      long currentTimeMs = elapsedRealtime();\n      if (currentTimeMs < timeMs) {\n        advanceTime(timeMs - currentTimeMs);\n      }\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.media.MediaCodec;\nimport android.media.MediaCrypto;\nimport android.os.Handler;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.DefaultRenderersFactory;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.Renderer;\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.MediaCodecSelector;\nimport com.google.android.exoplayer2.video.MediaCodecVideoRenderer;\nimport com.google.android.exoplayer2.video.VideoRendererEventListener;\nimport java.nio.ByteBuffer;\nimport java.util.ArrayList;\n\n/**\n * A debug extension of {@link DefaultRenderersFactory}. Provides a video renderer that performs\n * video buffer timestamp assertions, and modifies the default value for {@link\n * #setAllowedVideoJoiningTimeMs(long)} to be {@code 0}.\n */\npublic class DebugRenderersFactory extends DefaultRenderersFactory {\n\n  public DebugRenderersFactory(Context context) {\n    super(context);\n    setAllowedVideoJoiningTimeMs(0);\n  }\n\n  @Override\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 DebugMediaCodecVideoRenderer(\n            context,\n            mediaCodecSelector,\n            allowedVideoJoiningTimeMs,\n            drmSessionManager,\n            playClearSamplesWithoutKeys,\n            eventHandler,\n            eventListener,\n            MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));\n  }\n\n  /**\n   * Decodes and renders video using {@link MediaCodecVideoRenderer}. Provides buffer timestamp\n   * assertions.\n   */\n  private static class DebugMediaCodecVideoRenderer extends MediaCodecVideoRenderer {\n\n    private static final int ARRAY_SIZE = 1000;\n\n    private final long[] timestampsList = new long[ARRAY_SIZE];\n\n    private int startIndex;\n    private int queueSize;\n    private int bufferCount;\n    private int minimumInsertIndex;\n    private boolean skipToPositionBeforeRenderingFirstFrame;\n\n    public DebugMediaCodecVideoRenderer(\n        Context context,\n        MediaCodecSelector mediaCodecSelector,\n        long allowedJoiningTimeMs,\n        DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n        boolean playClearSamplesWithoutKeys,\n        Handler eventHandler,\n        VideoRendererEventListener eventListener,\n        int maxDroppedFrameCountToNotify) {\n      super(\n          context,\n          mediaCodecSelector,\n          allowedJoiningTimeMs,\n          drmSessionManager,\n          playClearSamplesWithoutKeys,\n          eventHandler,\n          eventListener,\n          maxDroppedFrameCountToNotify);\n    }\n\n    @Override\n    protected void configureCodec(\n        MediaCodecInfo codecInfo,\n        MediaCodec codec,\n        Format format,\n        MediaCrypto crypto,\n        float operatingRate) {\n      // If the codec is being initialized whilst the renderer is started, default behavior is to\n      // render the first frame (i.e. the keyframe before the current position), then drop frames up\n      // to the current playback position. For test runs that place a maximum limit on the number of\n      // dropped frames allowed, this is not desired behavior. Hence we skip (rather than drop)\n      // frames up to the current playback position [Internal: b/66494991].\n      skipToPositionBeforeRenderingFirstFrame = getState() == Renderer.STATE_STARTED;\n      super.configureCodec(codecInfo, codec, format, crypto, operatingRate);\n    }\n\n    @Override\n    protected void releaseCodec() {\n      super.releaseCodec();\n      clearTimestamps();\n      skipToPositionBeforeRenderingFirstFrame = false;\n    }\n\n    @Override\n    protected boolean flushOrReleaseCodec() {\n      try {\n        return super.flushOrReleaseCodec();\n      } finally {\n        clearTimestamps();\n      }\n    }\n\n    @Override\n    protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {\n      super.onInputFormatChanged(newFormat);\n      // Ensure timestamps of buffers queued after this format change are never inserted into the\n      // queue of expected output timestamps before those of buffers that have already been queued.\n      minimumInsertIndex = startIndex + queueSize;\n    }\n\n    @Override\n    protected void onQueueInputBuffer(DecoderInputBuffer buffer) {\n      super.onQueueInputBuffer(buffer);\n      insertTimestamp(buffer.timeUs);\n      maybeShiftTimestampsList();\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 (skipToPositionBeforeRenderingFirstFrame && bufferPresentationTimeUs < positionUs) {\n        // After the codec has been initialized, don't render the first frame until we've caught up\n        // to the playback position. Else test runs on devices that do not support dummy surface\n        // will drop frames between rendering the first one and catching up [Internal: b/66494991].\n        isDecodeOnlyBuffer = true;\n      }\n      return super.processOutputBuffer(\n          positionUs,\n          elapsedRealtimeUs,\n          codec,\n          buffer,\n          bufferIndex,\n          bufferFlags,\n          bufferPresentationTimeUs,\n          isDecodeOnlyBuffer,\n          isLastBuffer,\n          format);\n    }\n\n    @Override\n    protected void renderOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) {\n      skipToPositionBeforeRenderingFirstFrame = false;\n      super.renderOutputBuffer(codec, index, presentationTimeUs);\n    }\n\n    @TargetApi(21)\n    @Override\n    protected void renderOutputBufferV21(MediaCodec codec, int index, long presentationTimeUs,\n        long releaseTimeNs) {\n      skipToPositionBeforeRenderingFirstFrame = false;\n      super.renderOutputBufferV21(codec, index, presentationTimeUs, releaseTimeNs);\n    }\n\n    @Override\n    protected void onProcessedOutputBuffer(long presentationTimeUs) {\n      super.onProcessedOutputBuffer(presentationTimeUs);\n      bufferCount++;\n      long expectedTimestampUs = dequeueTimestamp();\n      if (expectedTimestampUs != presentationTimeUs) {\n        throw new IllegalStateException(\"Expected to dequeue video buffer with presentation \"\n            + \"timestamp: \" + expectedTimestampUs + \". Instead got: \" + presentationTimeUs\n            + \" (Processed buffers since last flush: \" + bufferCount + \").\");\n      }\n    }\n\n    private void clearTimestamps() {\n      startIndex = 0;\n      queueSize = 0;\n      bufferCount = 0;\n      minimumInsertIndex = 0;\n    }\n\n    private void insertTimestamp(long presentationTimeUs) {\n      for (int i = startIndex + queueSize - 1; i >= minimumInsertIndex; i--) {\n        if (presentationTimeUs >= timestampsList[i]) {\n          timestampsList[i + 1] = presentationTimeUs;\n          queueSize++;\n          return;\n        }\n        timestampsList[i + 1] = timestampsList[i];\n      }\n      timestampsList[minimumInsertIndex] = presentationTimeUs;\n      queueSize++;\n    }\n\n    private void maybeShiftTimestampsList() {\n      if (startIndex + queueSize == ARRAY_SIZE) {\n        System.arraycopy(timestampsList, startIndex, timestampsList, 0, queueSize);\n        minimumInsertIndex -= startIndex;\n        startIndex = 0;\n      }\n    }\n\n    private long dequeueTimestamp() {\n      queueSize--;\n      startIndex++;\n      minimumInsertIndex = Math.max(minimumInsertIndex, startIndex);\n      return timestampsList[startIndex - 1];\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\nimport static com.google.common.truth.Truth.assertWithMessage;\n\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\n\n/**\n * Assertions for {@link DecoderCounters}.\n */\npublic final class DecoderCountersUtil {\n\n  private DecoderCountersUtil() {}\n\n  /**\n   * Returns the sum of the skipped, dropped and rendered buffers.\n   *\n   * @param counters The counters for which the total should be calculated.\n   * @return The sum of the skipped, dropped and rendered buffers.\n   */\n  public static int getTotalBufferCount(DecoderCounters counters) {\n    counters.ensureUpdated();\n    return counters.skippedOutputBufferCount + counters.droppedBufferCount\n        + counters.renderedOutputBufferCount;\n  }\n\n  public static void assertSkippedOutputBufferCount(String name, DecoderCounters counters,\n      int expected) {\n    counters.ensureUpdated();\n    int actual = counters.skippedOutputBufferCount;\n    assertWithMessage(\n            \"Codec(\" + name + \") skipped \" + actual + \" buffers. Expected \" + expected + \".\")\n        .that(actual)\n        .isEqualTo(expected);\n  }\n\n  public static void assertTotalBufferCount(String name, DecoderCounters counters, int minCount,\n      int maxCount) {\n    int actual = getTotalBufferCount(counters);\n    assertWithMessage(\n            \"Codec(\"\n                + name\n                + \") output \"\n                + actual\n                + \" buffers. Expected in range [\"\n                + minCount\n                + \", \"\n                + maxCount\n                + \"].\")\n        .that(minCount <= actual && actual <= maxCount)\n        .isTrue();\n  }\n\n  public static void assertDroppedBufferLimit(String name, DecoderCounters counters, int limit) {\n    counters.ensureUpdated();\n    int actual = counters.droppedBufferCount;\n    assertWithMessage(\n            \"Codec(\"\n                + name\n                + \") was late decoding: \"\n                + actual\n                + \" buffers. \"\n                + \"Limit: \"\n                + limit\n                + \".\")\n        .that(actual)\n        .isAtMost(limit);\n  }\n\n  public static void assertConsecutiveDroppedBufferLimit(String name, DecoderCounters counters,\n      int limit) {\n    counters.ensureUpdated();\n    int actual = counters.maxConsecutiveDroppedBufferCount;\n    assertWithMessage(\n            \"Codec(\"\n                + name\n                + \") was late decoding: \"\n                + actual\n                + \" buffers consecutively. \"\n                + \"Limit: \"\n                + limit\n                + \".\")\n        .that(actual)\n        .isAtMost(limit);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.os.ConditionVariable;\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.Looper;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.concurrent.atomic.AtomicReference;\n\n/** Helper class to simulate main/UI thread in tests. */\npublic final class DummyMainThread {\n\n  /** {@link Runnable} variant which can throw a checked exception. */\n  public interface TestRunnable {\n    void run() throws Exception;\n  }\n\n  /** Default timeout value used for {@link #runOnMainThread(Runnable)}. */\n  public static final int TIMEOUT_MS = 10000;\n\n  private final HandlerThread thread;\n  private final Handler handler;\n\n  public DummyMainThread() {\n    thread = new HandlerThread(\"DummyMainThread\");\n    thread.start();\n    handler = new Handler(thread.getLooper());\n  }\n\n  /**\n   * Runs the provided {@link Runnable} on the main thread, blocking until execution completes or\n   * until {@link #TIMEOUT_MS} milliseconds have passed.\n   *\n   * @param runnable The {@link Runnable} to run.\n   */\n  public void runOnMainThread(final Runnable runnable) {\n    runOnMainThread(TIMEOUT_MS, runnable);\n  }\n\n  /**\n   * Runs the provided {@link Runnable} on the main thread, blocking until execution completes or\n   * until timeout milliseconds have passed.\n   *\n   * @param timeoutMs The maximum time to wait in milliseconds.\n   * @param runnable The {@link Runnable} to run.\n   */\n  public void runOnMainThread(int timeoutMs, final Runnable runnable) {\n    runTestOnMainThread(timeoutMs, runnable::run);\n  }\n\n  /**\n   * Runs the provided {@link TestRunnable} on the main thread, blocking until execution completes\n   * or until {@link #TIMEOUT_MS} milliseconds have passed.\n   *\n   * @param runnable The {@link TestRunnable} to run.\n   */\n  public void runTestOnMainThread(final TestRunnable runnable) {\n    runTestOnMainThread(TIMEOUT_MS, runnable);\n  }\n\n  /**\n   * Runs the provided {@link TestRunnable} on the main thread, blocking until execution completes\n   * or until timeout milliseconds have passed.\n   *\n   * @param timeoutMs The maximum time to wait in milliseconds.\n   * @param runnable The {@link TestRunnable} to run.\n   */\n  public void runTestOnMainThread(int timeoutMs, final TestRunnable runnable) {\n    if (Looper.myLooper() == handler.getLooper()) {\n      try {\n        runnable.run();\n      } catch (Exception e) {\n        Util.sneakyThrow(e);\n      }\n    } else {\n      ConditionVariable finishedCondition = new ConditionVariable();\n      AtomicReference<Throwable> thrown = new AtomicReference<>();\n      handler.post(\n          () -> {\n            try {\n              runnable.run();\n            } catch (Throwable t) {\n              thrown.set(t);\n            }\n            finishedCondition.open();\n          });\n      assertThat(finishedCondition.block(timeoutMs)).isTrue();\n      if (thrown.get() != null) {\n        Util.sneakyThrow(thrown.get());\n      }\n    }\n  }\n\n  public void release() {\n    thread.quit();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/Dumper.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\nimport com.google.android.exoplayer2.C;\nimport java.util.Arrays;\nimport java.util.Locale;\n\n/**\n * Helper utility to dump field values.\n */\npublic final class Dumper {\n\n  /**\n   * Provides custom dump method.\n   */\n  public interface Dumpable {\n    /**\n     * Dumps the fields of the object using the {@code dumper}.\n     * @param dumper The {@link Dumper} to be used to dump fields.\n     */\n    void dump(Dumper dumper);\n  }\n\n  private static final int INDENT_SIZE_IN_SPACES = 2;\n\n  private final StringBuilder sb;\n  private int indent;\n\n  public Dumper() {\n    sb = new StringBuilder();\n  }\n\n  public Dumper add(String field, Object value) {\n    return addString(field + \" = \" + value + '\\n');\n  }\n\n  public Dumper add(Dumpable object) {\n    object.dump(this);\n    return this;\n  }\n\n  public Dumper add(String field, byte[] value) {\n    String string = String.format(Locale.US, \"%s = length %d, hash %X\\n\", field, value.length,\n        Arrays.hashCode(value));\n    return addString(string);\n  }\n\n  public Dumper addTime(String field, long time) {\n    return add(field, time == C.TIME_UNSET ? \"UNSET TIME\" : time);\n  }\n\n  public Dumper startBlock(String name) {\n    addString(name + \":\\n\");\n    indent += INDENT_SIZE_IN_SPACES;\n    return this;\n  }\n\n  public Dumper endBlock() {\n    indent -= INDENT_SIZE_IN_SPACES;\n    return this;\n  }\n\n  @Override\n  public String toString() {\n    return sb.toString();\n  }\n\n  private Dumper addString(String string) {\n    for (int i = 0; i < indent; i++) {\n      sb.append(' ');\n    }\n    sb.append(string);\n    return this;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\nimport static com.google.common.truth.Truth.assertWithMessage;\n\nimport android.os.ConditionVariable;\nimport android.os.Looper;\nimport android.os.SystemClock;\nimport android.view.Surface;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.DefaultLoadControl;\nimport com.google.android.exoplayer2.DefaultRenderersFactory;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.ExoPlayerFactory;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.RenderersFactory;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.analytics.AnalyticsListener;\nimport com.google.android.exoplayer2.audio.DefaultAudioSink;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\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.testutil.HostActivity.HostedTest;\nimport com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.util.EventLogger;\nimport com.google.android.exoplayer2.util.HandlerWrapper;\nimport com.google.android.exoplayer2.util.Log;\nimport com.google.android.exoplayer2.util.Util;\n\n/** A {@link HostedTest} for {@link ExoPlayer} playback tests. */\npublic abstract class ExoHostedTest implements AnalyticsListener, HostedTest {\n\n  static {\n    // DefaultAudioSink is able to work around spurious timestamps reported by the platform (by\n    // ignoring them). Disable this workaround, since we're interested in testing that the\n    // underlying platform is behaving correctly.\n    DefaultAudioSink.failOnSpuriousAudioTimestamp = true;\n  }\n\n  public static final long MAX_PLAYING_TIME_DISCREPANCY_MS = 2000;\n  public static final long EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS = -2;\n  public static final long EXPECTED_PLAYING_TIME_UNSET = -1;\n\n  protected final String tag;\n\n  private final boolean failOnPlayerError;\n  private final long expectedPlayingTimeMs;\n  private final DecoderCounters videoDecoderCounters;\n  private final DecoderCounters audioDecoderCounters;\n  private final ConditionVariable testFinished;\n\n  private ActionSchedule pendingSchedule;\n  private HandlerWrapper actionHandler;\n  private DefaultTrackSelector trackSelector;\n  private SimpleExoPlayer player;\n  private Surface surface;\n  private ExoPlaybackException playerError;\n  private boolean playerWasPrepared;\n\n  private boolean playing;\n  private long totalPlayingTimeMs;\n  private long lastPlayingStartTimeMs;\n  private long sourceDurationMs;\n\n  /**\n   * @param tag A tag to use for logging.\n   * @param fullPlaybackNoSeeking Whether the test will play the target media in full without\n   *     seeking. If set to true, the test will assert that the total time spent playing the media\n   *     was within {@link #MAX_PLAYING_TIME_DISCREPANCY_MS} of the media duration. If set to false,\n   *     the test will not assert an expected playing time.\n   */\n  public ExoHostedTest(String tag, boolean fullPlaybackNoSeeking) {\n    this(tag, fullPlaybackNoSeeking ? EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS\n        : EXPECTED_PLAYING_TIME_UNSET, true);\n  }\n\n  /**\n   * @param tag A tag to use for logging.\n   * @param expectedPlayingTimeMs The expected playing time. If set to a non-negative value, the\n   *     test will assert that the total time spent playing the media was within\n   *     {@link #MAX_PLAYING_TIME_DISCREPANCY_MS} of the specified value.\n   *     {@link #EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS} should be passed to assert that the\n   *     expected playing time equals the duration of the media being played. Else\n   *     {@link #EXPECTED_PLAYING_TIME_UNSET} should be passed to indicate that the test should not\n   *     assert an expected playing time.\n   * @param failOnPlayerError Whether a player error should be considered a test failure.\n   */\n  public ExoHostedTest(String tag, long expectedPlayingTimeMs, boolean failOnPlayerError) {\n    this.tag = tag;\n    this.expectedPlayingTimeMs = expectedPlayingTimeMs;\n    this.failOnPlayerError = failOnPlayerError;\n    this.testFinished = new ConditionVariable();\n    this.videoDecoderCounters = new DecoderCounters();\n    this.audioDecoderCounters = new DecoderCounters();\n  }\n\n  /**\n   * Sets a schedule to be applied during the test.\n   *\n   * @param schedule The schedule.\n   */\n  public final void setSchedule(ActionSchedule schedule) {\n    if (player == null) {\n      pendingSchedule = schedule;\n    } else {\n      schedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null);\n    }\n  }\n\n  // HostedTest implementation\n\n  @Override\n  public final void onStart(HostActivity host, Surface surface) {\n    this.surface = surface;\n    // Build the player.\n    trackSelector = buildTrackSelector(host);\n    String userAgent = \"ExoPlayerPlaybackTests\";\n    DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = buildDrmSessionManager(userAgent);\n    player = buildExoPlayer(host, surface, trackSelector, drmSessionManager);\n    player.setPlayWhenReady(true);\n    player.addAnalyticsListener(this);\n    player.addAnalyticsListener(new EventLogger(trackSelector, tag));\n    // Schedule any pending actions.\n    actionHandler = Clock.DEFAULT.createHandler(Looper.myLooper(), /* callback= */ null);\n    if (pendingSchedule != null) {\n      pendingSchedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null);\n      pendingSchedule = null;\n    }\n    player.prepare(buildSource(host, Util.getUserAgent(host, userAgent)));\n  }\n\n  @Override\n  public final boolean blockUntilStopped(long timeoutMs) {\n    return testFinished.block(timeoutMs);\n  }\n\n  @Override\n  public final boolean forceStop() {\n    return stopTest();\n  }\n\n  @Override\n  public final void onFinished() {\n    onTestFinished(audioDecoderCounters, videoDecoderCounters);\n    if (failOnPlayerError && playerError != null) {\n      throw new Error(playerError);\n    }\n    if (expectedPlayingTimeMs != EXPECTED_PLAYING_TIME_UNSET) {\n      long playingTimeToAssertMs = expectedPlayingTimeMs == EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS\n          ? sourceDurationMs : expectedPlayingTimeMs;\n      // Assert that the playback spanned the correct duration of time.\n      long minAllowedActualPlayingTimeMs = playingTimeToAssertMs - MAX_PLAYING_TIME_DISCREPANCY_MS;\n      long maxAllowedActualPlayingTimeMs = playingTimeToAssertMs + MAX_PLAYING_TIME_DISCREPANCY_MS;\n      assertWithMessage(\n              \"Total playing time: \" + totalPlayingTimeMs + \". Expected: \" + playingTimeToAssertMs)\n          .that(\n              minAllowedActualPlayingTimeMs <= totalPlayingTimeMs\n                  && totalPlayingTimeMs <= maxAllowedActualPlayingTimeMs)\n          .isTrue();\n    }\n  }\n\n  // AnalyticsListener\n\n  @Override\n  public final void onPlayerStateChanged(\n      EventTime eventTime, boolean playWhenReady, int playbackState) {\n    Log.d(tag, \"state [\" + playWhenReady + \", \" + playbackState + \"]\");\n    playerWasPrepared |= playbackState != Player.STATE_IDLE;\n    if (playbackState == Player.STATE_ENDED\n        || (playbackState == Player.STATE_IDLE && playerWasPrepared)) {\n      stopTest();\n    }\n    boolean playing = playWhenReady && playbackState == Player.STATE_READY;\n    if (!this.playing && playing) {\n      lastPlayingStartTimeMs = SystemClock.elapsedRealtime();\n    } else if (this.playing && !playing) {\n      totalPlayingTimeMs += SystemClock.elapsedRealtime() - lastPlayingStartTimeMs;\n    }\n    this.playing = playing;\n  }\n\n  @Override\n  public final void onPlayerError(EventTime eventTime, ExoPlaybackException error) {\n    playerWasPrepared = true;\n    playerError = error;\n    onPlayerErrorInternal(error);\n  }\n\n  @Override\n  public void onDecoderDisabled(\n      EventTime eventTime, int trackType, DecoderCounters decoderCounters) {\n    if (trackType == C.TRACK_TYPE_AUDIO) {\n      audioDecoderCounters.merge(decoderCounters);\n    } else if (trackType == C.TRACK_TYPE_VIDEO) {\n      videoDecoderCounters.merge(decoderCounters);\n    }\n  }\n\n  // Internal logic\n\n  private boolean stopTest() {\n    if (player == null) {\n      return false;\n    }\n    actionHandler.removeCallbacksAndMessages(null);\n    sourceDurationMs = player.getDuration();\n    player.release();\n    player = null;\n    // We post opening of the finished condition so that any events posted to the main thread as a\n    // result of player.release() are guaranteed to be handled before the test returns.\n    actionHandler.post(testFinished::open);\n    return true;\n  }\n\n  protected DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManager(String userAgent) {\n    // Do nothing. Interested subclasses may override.\n    return null;\n  }\n\n  protected DefaultTrackSelector buildTrackSelector(HostActivity host) {\n    return new DefaultTrackSelector(new AdaptiveTrackSelection.Factory());\n  }\n\n  protected SimpleExoPlayer buildExoPlayer(\n      HostActivity host,\n      Surface surface,\n      MappingTrackSelector trackSelector,\n      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {\n    RenderersFactory renderersFactory =\n        new DefaultRenderersFactory(\n            host,\n            DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF,\n            /* allowedVideoJoiningTimeMs= */ 0);\n    SimpleExoPlayer player =\n        ExoPlayerFactory.newSimpleInstance(\n            host, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager);\n    player.setVideoSurface(surface);\n    return player;\n  }\n\n  protected abstract MediaSource buildSource(HostActivity host, String userAgent);\n\n  protected void onPlayerErrorInternal(ExoPlaybackException error) {\n    // Do nothing. Interested subclasses may override.\n  }\n\n  protected void onTestFinished(DecoderCounters audioCounters, DecoderCounters videoCounters) {\n    // Do nothing. Subclasses may override to add clean-up and assertions.\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.content.Context;\nimport android.os.HandlerThread;\nimport android.os.Looper;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.DefaultLoadControl;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.LoadControl;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.RenderersFactory;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.analytics.AnalyticsCollector;\nimport com.google.android.exoplayer2.analytics.AnalyticsListener;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\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.Clock;\nimport com.google.android.exoplayer2.util.HandlerWrapper;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\n\n/** Helper class to run an ExoPlayer test. */\npublic final class ExoPlayerTestRunner implements Player.EventListener, ActionSchedule.Callback {\n\n  /**\n   * Builder to set-up a {@link ExoPlayerTestRunner}. Default fake implementations will be used for\n   * unset test properties.\n   */\n  public static final class Builder {\n\n    /**\n     * A generic video {@link Format} which can be used to set up media sources and renderers.\n     */\n    public static final Format VIDEO_FORMAT = Format.createVideoSampleFormat(null,\n        MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE,\n        null, null);\n\n    /**\n     * A generic audio {@link Format} which can be used to set up media sources and renderers.\n     */\n    public static final Format AUDIO_FORMAT = Format.createAudioSampleFormat(null,\n        MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null);\n\n    private Clock clock;\n    private Timeline timeline;\n    private Object manifest;\n    private MediaSource mediaSource;\n    private DefaultTrackSelector trackSelector;\n    private LoadControl loadControl;\n    private BandwidthMeter bandwidthMeter;\n    private Format[] supportedFormats;\n    private Renderer[] renderers;\n    private RenderersFactory renderersFactory;\n    private ActionSchedule actionSchedule;\n    private Player.EventListener eventListener;\n    private AnalyticsListener analyticsListener;\n    private Integer expectedPlayerEndedCount;\n\n    /**\n     * Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The\n     * default value is a seekable, non-dynamic {@link FakeTimeline} with a duration of\n     * {@link FakeTimeline.TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. Setting the\n     * timeline is not allowed after a call to {@link #setMediaSource(MediaSource)}.\n     *\n     * @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test\n     *     runner.\n     * @return This builder.\n     */\n    public Builder setTimeline(Timeline timeline) {\n      assertThat(mediaSource).isNull();\n      this.timeline = timeline;\n      return this;\n    }\n\n    /**\n     * Sets a manifest to be used by a {@link FakeMediaSource} in the test runner. The default value\n     * is null. Setting the manifest is not allowed after a call to\n     * {@link #setMediaSource(MediaSource)}.\n     *\n     * @param manifest A manifest to be used by a {@link FakeMediaSource} in the test runner.\n     * @return This builder.\n     */\n    public Builder setManifest(Object manifest) {\n      assertThat(mediaSource).isNull();\n      this.manifest = manifest;\n      return this;\n    }\n\n    /**\n     * Sets a {@link MediaSource} to be used by the test runner. The default value is a\n     * {@link FakeMediaSource} with the timeline and manifest provided by\n     * {@link #setTimeline(Timeline)} and {@link #setManifest(Object)}. Setting the media source is\n     * not allowed after calls to {@link #setTimeline(Timeline)} and/or\n     * {@link #setManifest(Object)}.\n     *\n     * @param mediaSource A {@link MediaSource} to be used by the test runner.\n     * @return This builder.\n     */\n    public Builder setMediaSource(MediaSource mediaSource) {\n      assertThat(timeline).isNull();\n      assertThat(manifest).isNull();\n      this.mediaSource = mediaSource;\n      return this;\n    }\n\n    /**\n     * Sets a {@link DefaultTrackSelector} to be used by the test runner. The default value is a\n     * {@link DefaultTrackSelector} in its initial configuration.\n     *\n     * @param trackSelector A {@link DefaultTrackSelector} to be used by the test runner.\n     * @return This builder.\n     */\n    public Builder setTrackSelector(DefaultTrackSelector trackSelector) {\n      this.trackSelector = trackSelector;\n      return this;\n    }\n\n    /**\n     * Sets a {@link LoadControl} to be used by the test runner. The default value is a\n     * {@link DefaultLoadControl}.\n     *\n     * @param loadControl A {@link LoadControl} to be used by the test runner.\n     * @return This builder.\n     */\n    public Builder setLoadControl(LoadControl loadControl) {\n      this.loadControl = loadControl;\n      return this;\n    }\n\n    /**\n     * Sets the {@link BandwidthMeter} to be used by the test runner. The default value is a {@link\n     * DefaultBandwidthMeter} in its default configuration.\n     *\n     * @param bandwidthMeter The {@link BandwidthMeter} to be used by the test runner.\n     * @return This builder.\n     */\n    public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) {\n      this.bandwidthMeter = bandwidthMeter;\n      return this;\n    }\n\n    /**\n     * Sets a list of {@link Format}s to be used by a {@link FakeMediaSource} to create media\n     * periods and for setting up a {@link FakeRenderer}. The default value is a single\n     * {@link #VIDEO_FORMAT}. Note that this parameter doesn't have any influence if both a media\n     * source with {@link #setMediaSource(MediaSource)} and renderers with\n     * {@link #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set.\n     *\n     * @param supportedFormats A list of supported {@link Format}s.\n     * @return This builder.\n     */\n    public Builder setSupportedFormats(Format... supportedFormats) {\n      this.supportedFormats = supportedFormats;\n      return this;\n    }\n\n    /**\n     * Sets the {@link Renderer}s to be used by the test runner. The default value is a single\n     * {@link FakeRenderer} supporting the formats set by {@link #setSupportedFormats(Format...)}.\n     * Setting the renderers is not allowed after a call to\n     * {@link #setRenderersFactory(RenderersFactory)}.\n     *\n     * @param renderers A list of {@link Renderer}s to be used by the test runner.\n     * @return This builder.\n     */\n    public Builder setRenderers(Renderer... renderers) {\n      assertThat(renderersFactory).isNull();\n      this.renderers = renderers;\n      return this;\n    }\n\n    /**\n     * Sets the {@link RenderersFactory} to be used by the test runner. The default factory creates\n     * all renderers set by {@link #setRenderers(Renderer...)}. Setting the renderer factory is not\n     * allowed after a call to {@link #setRenderers(Renderer...)}.\n     *\n     * @param renderersFactory A {@link RenderersFactory} to be used by the test runner.\n     * @return This builder.\n     */\n    public Builder setRenderersFactory(RenderersFactory renderersFactory) {\n      assertThat(renderers).isNull();\n      this.renderersFactory = renderersFactory;\n      return this;\n    }\n\n    /**\n     * Sets the {@link Clock} to be used by the test runner. The default value is a {@link\n     * AutoAdvancingFakeClock}.\n     *\n     * @param clock A {@link Clock} to be used by the test runner.\n     * @return This builder.\n     */\n    public Builder setClock(Clock clock) {\n      this.clock = clock;\n      return this;\n    }\n\n    /**\n     * Sets an {@link ActionSchedule} to be run by the test runner. The first action will be\n     * executed immediately before {@link SimpleExoPlayer#prepare(MediaSource)}.\n     *\n     * @param actionSchedule An {@link ActionSchedule} to be used by the test runner.\n     * @return This builder.\n     */\n    public Builder setActionSchedule(ActionSchedule actionSchedule) {\n      this.actionSchedule = actionSchedule;\n      return this;\n    }\n\n    /**\n     * Sets an {@link Player.EventListener} to be registered to listen to player events.\n     *\n     * @param eventListener A {@link Player.EventListener} to be registered by the test runner to\n     *     listen to player events.\n     * @return This builder.\n     */\n    public Builder setEventListener(Player.EventListener eventListener) {\n      this.eventListener = eventListener;\n      return this;\n    }\n\n    /**\n     * Sets an {@link AnalyticsListener} to be registered.\n     *\n     * @param analyticsListener An {@link AnalyticsListener} to be registered.\n     * @return This builder.\n     */\n    public Builder setAnalyticsListener(AnalyticsListener analyticsListener) {\n      this.analyticsListener = analyticsListener;\n      return this;\n    }\n\n    /**\n     * Sets the number of times the test runner is expected to reach the {@link Player#STATE_ENDED}\n     * or {@link Player#STATE_IDLE}. The default is 1. This affects how long\n     * {@link ExoPlayerTestRunner#blockUntilEnded(long)} waits.\n     *\n     * @param expectedPlayerEndedCount The number of times the player is expected to reach the ended\n     *     or idle state.\n     * @return This builder.\n     */\n    public Builder setExpectedPlayerEndedCount(int expectedPlayerEndedCount) {\n      this.expectedPlayerEndedCount = expectedPlayerEndedCount;\n      return this;\n    }\n\n    /**\n     * Builds an {@link ExoPlayerTestRunner} using the provided values or their defaults.\n     *\n     * @param context The context.\n     * @return The built {@link ExoPlayerTestRunner}.\n     */\n    public ExoPlayerTestRunner build(Context context) {\n      if (supportedFormats == null) {\n        supportedFormats = new Format[] {VIDEO_FORMAT};\n      }\n      if (trackSelector == null) {\n        trackSelector = new DefaultTrackSelector();\n      }\n      if (bandwidthMeter == null) {\n        bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();\n      }\n      if (renderersFactory == null) {\n        if (renderers == null) {\n          renderers = new Renderer[] {new FakeRenderer(supportedFormats)};\n        }\n        renderersFactory =\n            (eventHandler,\n                videoRendererEventListener,\n                audioRendererEventListener,\n                textRendererOutput,\n                metadataRendererOutput,\n                drmSessionManager) -> renderers;\n      }\n      if (loadControl == null) {\n        loadControl = new DefaultLoadControl();\n      }\n      if (clock == null) {\n        clock = new AutoAdvancingFakeClock();\n      }\n      if (mediaSource == null) {\n        if (timeline == null) {\n          timeline = new FakeTimeline(1);\n        }\n        mediaSource = new FakeMediaSource(timeline, manifest, supportedFormats);\n      }\n      if (expectedPlayerEndedCount == null) {\n        expectedPlayerEndedCount = 1;\n      }\n      return new ExoPlayerTestRunner(\n          context,\n          clock,\n          mediaSource,\n          renderersFactory,\n          trackSelector,\n          loadControl,\n          bandwidthMeter,\n          actionSchedule,\n          eventListener,\n          analyticsListener,\n          expectedPlayerEndedCount);\n    }\n  }\n\n  private final Context context;\n  private final Clock clock;\n  private final MediaSource mediaSource;\n  private final RenderersFactory renderersFactory;\n  private final DefaultTrackSelector trackSelector;\n  private final LoadControl loadControl;\n  private final BandwidthMeter bandwidthMeter;\n  private final @Nullable ActionSchedule actionSchedule;\n  private final @Nullable Player.EventListener eventListener;\n  private final @Nullable AnalyticsListener analyticsListener;\n\n  private final HandlerThread playerThread;\n  private final HandlerWrapper handler;\n  private final CountDownLatch endedCountDownLatch;\n  private final CountDownLatch actionScheduleFinishedCountDownLatch;\n  private final ArrayList<Timeline> timelines;\n  private final ArrayList<Object> manifests;\n  private final ArrayList<Integer> timelineChangeReasons;\n  private final ArrayList<Integer> periodIndices;\n  private final ArrayList<Integer> discontinuityReasons;\n\n  private SimpleExoPlayer player;\n  private Exception exception;\n  private TrackGroupArray trackGroups;\n  private boolean playerWasPrepared;\n\n  private ExoPlayerTestRunner(\n      Context context,\n      Clock clock,\n      MediaSource mediaSource,\n      RenderersFactory renderersFactory,\n      DefaultTrackSelector trackSelector,\n      LoadControl loadControl,\n      BandwidthMeter bandwidthMeter,\n      @Nullable ActionSchedule actionSchedule,\n      @Nullable Player.EventListener eventListener,\n      @Nullable AnalyticsListener analyticsListener,\n      int expectedPlayerEndedCount) {\n    this.context = context;\n    this.clock = clock;\n    this.mediaSource = mediaSource;\n    this.renderersFactory = renderersFactory;\n    this.trackSelector = trackSelector;\n    this.loadControl = loadControl;\n    this.bandwidthMeter = bandwidthMeter;\n    this.actionSchedule = actionSchedule;\n    this.eventListener = eventListener;\n    this.analyticsListener = analyticsListener;\n    this.timelines = new ArrayList<>();\n    this.manifests = new ArrayList<>();\n    this.timelineChangeReasons = new ArrayList<>();\n    this.periodIndices = new ArrayList<>();\n    this.discontinuityReasons = new ArrayList<>();\n    this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount);\n    this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0);\n    this.playerThread = new HandlerThread(\"ExoPlayerTest thread\");\n    playerThread.start();\n    this.handler = clock.createHandler(playerThread.getLooper(), /* callback= */ null);\n  }\n\n  // Called on the test thread to run the test.\n\n  /**\n   * Starts the test runner on its own thread. This will trigger the creation of the player, the\n   * listener registration, the start of the action schedule, and the preparation of the player\n   * with the provided media source.\n   *\n   * @return This test runner.\n   */\n  public ExoPlayerTestRunner start() {\n    handler.post(\n        () -> {\n          try {\n            player =\n                new TestSimpleExoPlayer(\n                    context, renderersFactory, trackSelector, loadControl, bandwidthMeter, clock);\n            player.addListener(ExoPlayerTestRunner.this);\n            if (eventListener != null) {\n              player.addListener(eventListener);\n            }\n            if (analyticsListener != null) {\n              player.addAnalyticsListener(analyticsListener);\n            }\n            player.setPlayWhenReady(true);\n            if (actionSchedule != null) {\n              actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this);\n            }\n            player.prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);\n          } catch (Exception e) {\n            handleException(e);\n          }\n        });\n    return this;\n  }\n\n  /**\n   * Blocks the current thread until the test runner finishes. A test is deemed to be finished when\n   * the playback state transitioned to {@link Player#STATE_ENDED} or {@link Player#STATE_IDLE} for\n   * the specified number of times. The test also finishes when an {@link ExoPlaybackException} is\n   * thrown.\n   *\n   * @param timeoutMs The maximum time to wait for the test runner to finish. If this time elapsed\n   *     the method will throw a {@link TimeoutException}.\n   * @return This test runner.\n   * @throws Exception If any exception occurred during playback, release, or due to a timeout.\n   */\n  public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception {\n    if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {\n      exception = new TimeoutException(\"Test playback timed out waiting for playback to end.\");\n    }\n    release();\n    // Throw any pending exception (from playback, timing out or releasing).\n    if (exception != null) {\n      throw exception;\n    }\n    return this;\n  }\n\n  /**\n   * Blocks the current thread until the action schedule finished. This does not release the test\n   * runner and the test must still call {@link #blockUntilEnded(long)}.\n   *\n   * @param timeoutMs The maximum time to wait for the action schedule to finish.\n   * @return This test runner.\n   * @throws TimeoutException If the action schedule did not finish within the specified timeout.\n   * @throws InterruptedException If the test thread gets interrupted while waiting.\n   */\n  public ExoPlayerTestRunner blockUntilActionScheduleFinished(long timeoutMs)\n      throws TimeoutException, InterruptedException {\n    if (!actionScheduleFinishedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {\n      throw new TimeoutException(\"Test playback timed out waiting for action schedule to finish.\");\n    }\n    return this;\n  }\n\n  // Assertions called on the test thread after test finished.\n\n  /**\n   * Asserts that the timelines reported by\n   * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided\n   * timelines.\n   *\n   * @param timelines A list of expected {@link Timeline}s.\n   */\n  public void assertTimelinesEqual(Timeline... timelines) {\n    assertThat(this.timelines).containsExactlyElementsIn(Arrays.asList(timelines)).inOrder();\n  }\n\n  /**\n   * Asserts that the manifests reported by\n   * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided\n   * manifest.\n   *\n   * @param manifests A list of expected manifests.\n   */\n  public void assertManifestsEqual(Object... manifests) {\n    assertThat(this.manifests).containsExactlyElementsIn(Arrays.asList(manifests)).inOrder();\n  }\n\n  /**\n   * Asserts that the timeline change reasons reported by {@link\n   * Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided\n   * timeline change reasons.\n   */\n  public void assertTimelineChangeReasonsEqual(Integer... reasons) {\n    assertThat(timelineChangeReasons).containsExactlyElementsIn(Arrays.asList(reasons)).inOrder();\n  }\n\n  /**\n   * Asserts that the last track group array reported by\n   * {@link Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to\n   * the provided track group array.\n   *\n   * @param trackGroupArray The expected {@link TrackGroupArray}.\n   */\n  public void assertTrackGroupsEqual(TrackGroupArray trackGroupArray) {\n    assertThat(this.trackGroups).isEqualTo(trackGroupArray);\n  }\n\n  /**\n   * Asserts that {@link Player.EventListener#onPositionDiscontinuity(int)} was not called.\n   */\n  public void assertNoPositionDiscontinuities() {\n    assertThat(discontinuityReasons).isEmpty();\n  }\n\n  /**\n   * Asserts that the discontinuity reasons reported by {@link\n   * Player.EventListener#onPositionDiscontinuity(int)} are equal to the provided values.\n   *\n   * @param discontinuityReasons The expected discontinuity reasons.\n   */\n  public void assertPositionDiscontinuityReasonsEqual(Integer... discontinuityReasons) {\n    assertThat(this.discontinuityReasons)\n        .containsExactlyElementsIn(Arrays.asList(discontinuityReasons))\n        .inOrder();\n  }\n\n  /**\n   * Asserts that the indices of played periods is equal to the provided list of periods. A period\n   * is considered to be played if it was the current period after a position discontinuity or a\n   * media source preparation. When the same period is repeated automatically due to enabled repeat\n   * modes, it is reported twice. Seeks within the current period are not reported.\n   *\n   * @param periodIndices A list of expected period indices.\n   */\n  public void assertPlayedPeriodIndices(Integer... periodIndices) {\n    assertThat(this.periodIndices)\n        .containsExactlyElementsIn(Arrays.asList(periodIndices))\n        .inOrder();\n  }\n\n  // Private implementation details.\n\n  private void release() throws InterruptedException {\n    handler.post(\n        () -> {\n          try {\n            if (player != null) {\n              player.release();\n            }\n          } catch (Exception e) {\n            handleException(e);\n          } finally {\n            playerThread.quit();\n          }\n        });\n    playerThread.join();\n  }\n\n  private void handleException(Exception exception) {\n    if (this.exception == null) {\n      this.exception = exception;\n    }\n    while (endedCountDownLatch.getCount() > 0) {\n      endedCountDownLatch.countDown();\n    }\n  }\n\n  // Player.EventListener\n\n  @Override\n  public void onTimelineChanged(\n      Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {\n    timelines.add(timeline);\n    manifests.add(manifest);\n    timelineChangeReasons.add(reason);\n    if (reason == Player.TIMELINE_CHANGE_REASON_PREPARED) {\n      periodIndices.add(player.getCurrentPeriodIndex());\n    }\n  }\n\n  @Override\n  public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n    this.trackGroups = trackGroups;\n  }\n\n  @Override\n  public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n    playerWasPrepared |= playbackState != Player.STATE_IDLE;\n    if (playbackState == Player.STATE_ENDED\n        || (playbackState == Player.STATE_IDLE && playerWasPrepared)) {\n      endedCountDownLatch.countDown();\n    }\n  }\n\n  @Override\n  public void onPlayerError(ExoPlaybackException error) {\n    handleException(error);\n  }\n\n  @Override\n  public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {\n    discontinuityReasons.add(reason);\n    int currentIndex = player.getCurrentPeriodIndex();\n    if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION\n        || periodIndices.isEmpty()\n        || periodIndices.get(periodIndices.size() - 1) != currentIndex) {\n      // Ignore seek or internal discontinuities within a period.\n      periodIndices.add(currentIndex);\n    }\n  }\n\n  // ActionSchedule.Callback\n\n  @Override\n  public void onActionScheduleFinished() {\n    actionScheduleFinishedCountDownLatch.countDown();\n  }\n\n  /** SimpleExoPlayer implementation using a custom Clock. */\n  private static final class TestSimpleExoPlayer extends SimpleExoPlayer {\n\n    public TestSimpleExoPlayer(\n        Context context,\n        RenderersFactory renderersFactory,\n        TrackSelector trackSelector,\n        LoadControl loadControl,\n        BandwidthMeter bandwidthMeter,\n        Clock clock) {\n      super(\n          context,\n          renderersFactory,\n          trackSelector,\n          loadControl,\n          /* drmSessionManager= */ null,\n          bandwidthMeter,\n          new AnalyticsCollector.Factory(),\n          clock,\n          Looper.myLooper());\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.content.Context;\nimport androidx.test.core.app.ApplicationProvider;\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.PositionHolder;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException;\nimport com.google.android.exoplayer2.util.Assertions;\nimport java.io.IOException;\nimport java.util.Arrays;\n\n/**\n * Assertion methods for {@link Extractor}.\n */\npublic final class ExtractorAsserts {\n\n  /**\n   * A factory for {@link Extractor} instances.\n   */\n  public interface ExtractorFactory {\n    Extractor create();\n  }\n\n  private static final String DUMP_EXTENSION = \".dump\";\n  private static final String UNKNOWN_LENGTH_EXTENSION = \".unklen\" + DUMP_EXTENSION;\n\n  /**\n   * Asserts that an extractor behaves correctly given valid input data. Can only be used from\n   * Robolectric tests.\n   *\n   * <ul>\n   *   <li>Calls {@link Extractor#seek(long, long)} and {@link Extractor#release()} without calling\n   *       {@link Extractor#init(ExtractorOutput)} to check these calls do not fail.\n   *   <li>Calls {@link #assertOutput(Extractor, String, byte[], Context, boolean, boolean, boolean,\n   *       boolean)} with all possible combinations of \"simulate\" parameters.\n   * </ul>\n   *\n   * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}\n   *     class which is to be tested.\n   * @param file The path to the input sample.\n   * @throws IOException If reading from the input fails.\n   * @throws InterruptedException If interrupted while reading from the input.\n   */\n  public static void assertBehavior(ExtractorFactory factory, String file)\n      throws IOException, InterruptedException {\n    // Check behavior prior to initialization.\n    Extractor extractor = factory.create();\n    extractor.seek(0, 0);\n    extractor.release();\n    // Assert output.\n    byte[] fileData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file);\n    assertOutput(factory, file, fileData, ApplicationProvider.getApplicationContext());\n  }\n\n  /**\n   * Asserts that an extractor behaves correctly given valid input data:\n   *\n   * <ul>\n   *   <li>Calls {@link Extractor#seek(long, long)} and {@link Extractor#release()} without calling\n   *       {@link Extractor#init(ExtractorOutput)} to check these calls do not fail.\n   *   <li>Calls {@link #assertOutput(Extractor, String, byte[], Context, boolean, boolean, boolean,\n   *       boolean)} with all possible combinations of \"simulate\" parameters.\n   * </ul>\n   *\n   * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}\n   *     class which is to be tested.\n   * @param file The path to the input sample.\n   * @param context To be used to load the sample file.\n   * @throws IOException If reading from the input fails.\n   * @throws InterruptedException If interrupted while reading from the input.\n   */\n  public static void assertBehavior(ExtractorFactory factory, String file, Context context)\n      throws IOException, InterruptedException {\n    // Check behavior prior to initialization.\n    Extractor extractor = factory.create();\n    extractor.seek(0, 0);\n    extractor.release();\n    // Assert output.\n    byte[] fileData = TestUtil.getByteArray(context, file);\n    assertOutput(factory, file, fileData, context);\n  }\n\n  /**\n   * Calls {@link #assertOutput(Extractor, String, byte[], Context, boolean, boolean, boolean,\n   * boolean)} with all possible combinations of \"simulate\" parameters with {@code sniffFirst} set\n   * to true, and makes one additional call with the \"simulate\" and {@code sniffFirst} parameters\n   * all set to false.\n   *\n   * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}\n   *     class which is to be tested.\n   * @param file The path to the input sample.\n   * @param data Content of the input file.\n   * @param context To be used to load the sample file.\n   * @throws IOException If reading from the input fails.\n   * @throws InterruptedException If interrupted while reading from the input.\n   */\n  public static void assertOutput(\n      ExtractorFactory factory, String file, byte[] data, Context context)\n      throws IOException, InterruptedException {\n    assertOutput(factory.create(), file, data, context, true, false, false, false);\n    assertOutput(factory.create(), file, data, context, true, false, false, true);\n    assertOutput(factory.create(), file, data, context, true, false, true, false);\n    assertOutput(factory.create(), file, data, context, true, false, true, true);\n    assertOutput(factory.create(), file, data, context, true, true, false, false);\n    assertOutput(factory.create(), file, data, context, true, true, false, true);\n    assertOutput(factory.create(), file, data, context, true, true, true, false);\n    assertOutput(factory.create(), file, data, context, true, true, true, true);\n    assertOutput(factory.create(), file, data, context, false, false, false, false);\n  }\n\n  /**\n   * Asserts that {@code extractor} consumes {@code sampleFile} successfully and its output equals\n   * to a prerecorded output dump file with the name {@code sampleFile} + \"{@value\n   * #DUMP_EXTENSION}\". If {@code simulateUnknownLength} is true and {@code sampleFile} + \"{@value\n   * #UNKNOWN_LENGTH_EXTENSION}\" exists, it's preferred.\n   *\n   * @param extractor The {@link Extractor} to be tested.\n   * @param file The path to the input sample.\n   * @param data Content of the input file.\n   * @param context To be used to load the sample file.\n   * @param sniffFirst Whether to sniff the data by calling {@link Extractor#sniff(ExtractorInput)}\n   *     prior to consuming it.\n   * @param simulateIOErrors Whether to simulate IO errors.\n   * @param simulateUnknownLength Whether to simulate unknown input length.\n   * @param simulatePartialReads Whether to simulate partial reads.\n   * @return The {@link FakeExtractorOutput} used in the test.\n   * @throws IOException If reading from the input fails.\n   * @throws InterruptedException If interrupted while reading from the input.\n   */\n  private static FakeExtractorOutput assertOutput(\n      Extractor extractor,\n      String file,\n      byte[] data,\n      Context context,\n      boolean sniffFirst,\n      boolean simulateIOErrors,\n      boolean simulateUnknownLength,\n      boolean simulatePartialReads)\n      throws IOException, InterruptedException {\n    FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data)\n        .setSimulateIOErrors(simulateIOErrors)\n        .setSimulateUnknownLength(simulateUnknownLength)\n        .setSimulatePartialReads(simulatePartialReads).build();\n\n    if (sniffFirst) {\n      assertThat(TestUtil.sniffTestData(extractor, input)).isTrue();\n      input.resetPeekPosition();\n    }\n\n    FakeExtractorOutput extractorOutput = consumeTestData(extractor, input, 0, true);\n    if (simulateUnknownLength && assetExists(context, file + UNKNOWN_LENGTH_EXTENSION)) {\n      extractorOutput.assertOutput(context, file + UNKNOWN_LENGTH_EXTENSION);\n    } else {\n      extractorOutput.assertOutput(context, file + \".0\" + DUMP_EXTENSION);\n    }\n\n    // Seeking to (timeUs=0, position=0) should always work, and cause the same data to be output.\n    extractorOutput.clearTrackOutputs();\n    input.reset();\n    consumeTestData(extractor, input, /* timeUs= */ 0, extractorOutput, false);\n    if (simulateUnknownLength && assetExists(context, file + UNKNOWN_LENGTH_EXTENSION)) {\n      extractorOutput.assertOutput(context, file + UNKNOWN_LENGTH_EXTENSION);\n    } else {\n      extractorOutput.assertOutput(context, file + \".0\" + DUMP_EXTENSION);\n    }\n\n    // If the SeekMap is seekable, test seeking to 4 positions in the stream.\n    SeekMap seekMap = extractorOutput.seekMap;\n    if (seekMap.isSeekable()) {\n      long durationUs = seekMap.getDurationUs();\n      for (int j = 0; j < 4; j++) {\n        extractorOutput.clearTrackOutputs();\n        long timeUs = (durationUs * j) / 3;\n        long position = seekMap.getSeekPoints(timeUs).first.position;\n        input.reset();\n        input.setPosition((int) position);\n        consumeTestData(extractor, input, timeUs, extractorOutput, false);\n        extractorOutput.assertOutput(context, file + '.' + j + DUMP_EXTENSION);\n      }\n    }\n\n    return extractorOutput;\n  }\n\n  /**\n   * Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all\n   * possible combinations of \"simulate\" parameters.\n   *\n   * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}\n   *     class which is to be tested.\n   * @param sampleFile The path to the input sample.\n   * @param context To be used to load the sample file.\n   * @param expectedThrowable Expected {@link Throwable} class.\n   * @throws IOException If reading from the input fails.\n   * @throws InterruptedException If interrupted while reading from the input.\n   * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)\n   */\n  public static void assertThrows(\n      ExtractorFactory factory,\n      String sampleFile,\n      Context context,\n      Class<? extends Throwable> expectedThrowable)\n      throws IOException, InterruptedException {\n    byte[] fileData = TestUtil.getByteArray(context, sampleFile);\n    assertThrows(factory, fileData, expectedThrowable);\n  }\n\n  /**\n   * Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all\n   * possible combinations of \"simulate\" parameters.\n   *\n   * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}\n   *     class which is to be tested.\n   * @param fileData Content of the input file.\n   * @param expectedThrowable Expected {@link Throwable} class.\n   * @throws IOException If reading from the input fails.\n   * @throws InterruptedException If interrupted while reading from the input.\n   * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)\n   */\n  private static void assertThrows(\n      ExtractorFactory factory, byte[] fileData, Class<? extends Throwable> expectedThrowable)\n      throws IOException, InterruptedException {\n    assertThrows(factory.create(), fileData, expectedThrowable, false, false, false);\n    assertThrows(factory.create(), fileData, expectedThrowable,  true, false, false);\n    assertThrows(factory.create(), fileData, expectedThrowable, false,  true, false);\n    assertThrows(factory.create(), fileData, expectedThrowable,  true,  true, false);\n    assertThrows(factory.create(), fileData, expectedThrowable, false, false,  true);\n    assertThrows(factory.create(), fileData, expectedThrowable,  true, false,  true);\n    assertThrows(factory.create(), fileData, expectedThrowable, false,  true,  true);\n    assertThrows(factory.create(), fileData, expectedThrowable,  true,  true,  true);\n  }\n\n  /**\n   * Asserts {@code extractor} throws {@code expectedThrowable} while consuming {@code sampleFile}.\n   *\n   * @param extractor The {@link Extractor} to be tested.\n   * @param fileData Content of the input file.\n   * @param expectedThrowable Expected {@link Throwable} class.\n   * @param simulateIOErrors If true simulates IOErrors.\n   * @param simulateUnknownLength If true simulates unknown input length.\n   * @param simulatePartialReads If true simulates partial reads.\n   * @throws IOException If reading from the input fails.\n   * @throws InterruptedException If interrupted while reading from the input.\n   */\n  private static void assertThrows(\n      Extractor extractor,\n      byte[] fileData,\n      Class<? extends Throwable> expectedThrowable,\n      boolean simulateIOErrors,\n      boolean simulateUnknownLength,\n      boolean simulatePartialReads)\n      throws IOException, InterruptedException {\n    FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData)\n        .setSimulateIOErrors(simulateIOErrors)\n        .setSimulateUnknownLength(simulateUnknownLength)\n        .setSimulatePartialReads(simulatePartialReads).build();\n    try {\n      consumeTestData(extractor, input, 0, true);\n      throw new AssertionError(expectedThrowable.getSimpleName() + \" expected but not thrown\");\n    } catch (Throwable throwable) {\n      if (expectedThrowable.equals(throwable.getClass())) {\n        return; // Pass!\n      }\n      throw throwable;\n    }\n  }\n\n  private ExtractorAsserts() {}\n\n  private static FakeExtractorOutput consumeTestData(Extractor extractor, FakeExtractorInput input,\n      long timeUs, boolean retryFromStartIfLive) throws IOException, InterruptedException {\n    FakeExtractorOutput output = new FakeExtractorOutput();\n    extractor.init(output);\n    consumeTestData(extractor, input, timeUs, output, retryFromStartIfLive);\n    return output;\n  }\n\n  private static void consumeTestData(Extractor extractor, FakeExtractorInput input, long timeUs,\n      FakeExtractorOutput output, boolean retryFromStartIfLive)\n      throws IOException, InterruptedException {\n    extractor.seek(input.getPosition(), timeUs);\n    PositionHolder seekPositionHolder = new PositionHolder();\n    int readResult = Extractor.RESULT_CONTINUE;\n    while (readResult != Extractor.RESULT_END_OF_INPUT) {\n      try {\n        // Extractor.read should not read seekPositionHolder.position. Set it to a value that's\n        // likely to cause test failure if a read does occur.\n        seekPositionHolder.position = Long.MIN_VALUE;\n        readResult = extractor.read(input, seekPositionHolder);\n        if (readResult == Extractor.RESULT_SEEK) {\n          long seekPosition = seekPositionHolder.position;\n          Assertions.checkState(0 <= seekPosition && seekPosition <= Integer.MAX_VALUE);\n          input.setPosition((int) seekPosition);\n        }\n      } catch (SimulatedIOException e) {\n        if (!retryFromStartIfLive) {\n          continue;\n        }\n        boolean isOnDemand = input.getLength() != C.LENGTH_UNSET\n            || (output.seekMap != null && output.seekMap.getDurationUs() != C.TIME_UNSET);\n        if (isOnDemand) {\n          continue;\n        }\n        input.setPosition(0);\n        for (int i = 0; i < output.numberOfTracks; i++) {\n          output.trackOutputs.valueAt(i).clear();\n        }\n        extractor.seek(0, 0);\n      }\n    }\n  }\n\n  private static boolean assetExists(Context context, String fileName) throws IOException {\n    int i = fileName.lastIndexOf('/');\n    String path = i >= 0 ? fileName.substring(0, i) : \"\";\n    String file = i >= 0 ? fileName.substring(i + 1) : fileName;\n    return Arrays.asList(context.getResources().getAssets().list(path)).contains(file);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.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.testutil;\n\nimport android.net.Uri;\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.BaseMediaChunkIterator;\nimport com.google.android.exoplayer2.source.chunk.MediaChunkIterator;\nimport com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport java.util.Random;\n\n/**\n * Fake data set emulating the data of an adaptive media source.\n * It provides chunk data for all {@link Format}s in the given {@link TrackGroup}.\n */\npublic final class FakeAdaptiveDataSet extends FakeDataSet {\n\n  /**\n   * Factory for {@link FakeAdaptiveDataSet}s.\n   */\n  public static final class Factory {\n\n    private static final Random random = new Random();\n\n    private final long chunkDurationUs;\n    private final double bitratePercentStdDev;\n\n    /**\n     * Set up factory for {@link FakeAdaptiveDataSet}s with a chunk duration and the standard\n     * deviation of the chunk size.\n     *\n     * @param chunkDurationUs The chunk duration to use in microseconds.\n     * @param bitratePercentStdDev The standard deviation used to generate the chunk sizes centered\n     *     around the average bitrate of the {@link Format}s. The standard deviation is given in\n     *     percent (of the average size).\n     */\n    public Factory(long chunkDurationUs, double bitratePercentStdDev) {\n      this.chunkDurationUs = chunkDurationUs;\n      this.bitratePercentStdDev = bitratePercentStdDev;\n    }\n\n    /**\n     * Returns a new {@link FakeAdaptiveDataSet} for the given {@link TrackGroup}.\n     *\n     * @param trackGroup The {@link TrackGroup} for which the data set is to be created.\n     * @param mediaDurationUs The total duration of the fake data set in microseconds.\n     */\n    public FakeAdaptiveDataSet createDataSet(TrackGroup trackGroup, long mediaDurationUs) {\n      return new FakeAdaptiveDataSet(trackGroup, mediaDurationUs, chunkDurationUs,\n          bitratePercentStdDev, random);\n    }\n\n  }\n\n  /** {@link MediaChunkIterator} for the chunks defined by a fake adaptive data set. */\n  public static final class Iterator extends BaseMediaChunkIterator {\n\n    private final FakeAdaptiveDataSet dataSet;\n    private final int trackGroupIndex;\n\n    /**\n     * Create iterator.\n     *\n     * @param dataSet The data set to iterate over.\n     * @param trackGroupIndex The index of the track group to iterate over.\n     * @param chunkIndex The chunk index to which the iterator points initially.\n     */\n    public Iterator(FakeAdaptiveDataSet dataSet, int trackGroupIndex, int chunkIndex) {\n      super(/* fromIndex= */ chunkIndex, /* toIndex= */ dataSet.getChunkCount() - 1);\n      this.dataSet = dataSet;\n      this.trackGroupIndex = trackGroupIndex;\n    }\n\n    @Override\n    public DataSpec getDataSpec() {\n      checkInBounds();\n      String uri = dataSet.getUri(trackGroupIndex);\n      int chunkIndex = (int) getCurrentIndex();\n      Segment fakeDataChunk = dataSet.getData(uri).getSegments().get(chunkIndex);\n      return new DataSpec(\n          Uri.parse(uri), fakeDataChunk.byteOffset, fakeDataChunk.length, /* key= */ null);\n    }\n\n    @Override\n    public long getChunkStartTimeUs() {\n      checkInBounds();\n      return dataSet.getStartTime((int) getCurrentIndex());\n    }\n\n    @Override\n    public long getChunkEndTimeUs() {\n      checkInBounds();\n      int chunkIndex = (int) getCurrentIndex();\n      return dataSet.getStartTime(chunkIndex) + dataSet.getChunkDuration(chunkIndex);\n    }\n  }\n\n  private final int chunkCount;\n  private final long chunkDurationUs;\n  private final long lastChunkDurationUs;\n\n  /**\n   * Create {@link FakeAdaptiveDataSet} using a {@link TrackGroup} and meta data about the media.\n   *\n   * @param trackGroup The {@link TrackGroup} for which the data set is to be created.\n   * @param mediaDurationUs The total duration of the fake data set in microseconds.\n   * @param chunkDurationUs The chunk duration to use in microseconds.\n   * @param bitratePercentStdDev  The standard deviation used to generate the chunk sizes centered\n   *     around the average bitrate of the {@link Format}s in the {@link TrackGroup}. The standard\n   *     deviation is given in percent (of the average size).\n   * @param random A {@link Random} instance used to generate random chunk sizes.\n   */\n  /* package */ FakeAdaptiveDataSet(TrackGroup trackGroup, long mediaDurationUs,\n      long chunkDurationUs, double bitratePercentStdDev, Random random) {\n    this.chunkDurationUs = chunkDurationUs;\n    long lastChunkDurationUs = mediaDurationUs % chunkDurationUs;\n    int fullChunks = (int) (mediaDurationUs / chunkDurationUs);\n    this.lastChunkDurationUs = lastChunkDurationUs == 0 ? chunkDurationUs : lastChunkDurationUs;\n    this.chunkCount = lastChunkDurationUs == 0 ? fullChunks : fullChunks + 1;\n    double[] bitrateFactors = new double[chunkCount];\n    for (int i = 0; i < chunkCount; i++) {\n      bitrateFactors[i] = 1.0 + random.nextGaussian() * bitratePercentStdDev / 100.0;\n    }\n    for (int i = 0; i < trackGroup.length; i++) {\n      String uri = getUri(i);\n      Format format = trackGroup.getFormat(i);\n      double avgChunkLength = format.bitrate * chunkDurationUs / (8 * C.MICROS_PER_SECOND);\n      FakeData newData = this.newData(uri);\n      for (int j = 0; j < fullChunks; j++) {\n        newData.appendReadData((int) (avgChunkLength * bitrateFactors[j]));\n      }\n      if (lastChunkDurationUs > 0) {\n        int lastChunkLength = (int) (format.bitrate * bitrateFactors[bitrateFactors.length - 1]\n            * (mediaDurationUs % chunkDurationUs) / (8 * C.MICROS_PER_SECOND));\n        newData.appendReadData(lastChunkLength);\n      }\n    }\n  }\n\n  public int getChunkCount() {\n    return chunkCount;\n  }\n\n  public String getUri(int trackIndex) {\n    return \"fake://adaptive.media/\" + trackIndex;\n  }\n\n  public long getChunkDuration(int chunkIndex) {\n    return chunkIndex == getChunkCount() - 1 ? lastChunkDurationUs : chunkDurationUs;\n  }\n\n  public long getStartTime(int chunkIndex) {\n    return chunkIndex * chunkDurationUs;\n  }\n\n  public int getChunkIndexByPosition(long positionUs) {\n    return (int) (positionUs / chunkDurationUs);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.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.testutil;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.source.CompositeSequenceableLoader;\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.TrackGroupArray;\nimport com.google.android.exoplayer2.source.chunk.ChunkSampleStream;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;\nimport com.google.android.exoplayer2.upstream.TransferListener;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Fake {@link MediaPeriod} that provides tracks from the given {@link TrackGroupArray}. Selecting a\n * track will give the player a {@link ChunkSampleStream<FakeChunkSource>}.\n */\npublic class FakeAdaptiveMediaPeriod extends FakeMediaPeriod\n    implements SequenceableLoader.Callback<ChunkSampleStream<FakeChunkSource>> {\n\n  private final Allocator allocator;\n  private final FakeChunkSource.Factory chunkSourceFactory;\n  private final @Nullable TransferListener transferListener;\n  private final long durationUs;\n\n  private Callback callback;\n  private ChunkSampleStream<FakeChunkSource>[] sampleStreams;\n  private SequenceableLoader sequenceableLoader;\n\n  public FakeAdaptiveMediaPeriod(\n      TrackGroupArray trackGroupArray,\n      EventDispatcher eventDispatcher,\n      Allocator allocator,\n      FakeChunkSource.Factory chunkSourceFactory,\n      long durationUs,\n      @Nullable TransferListener transferListener) {\n    super(trackGroupArray, eventDispatcher);\n    this.allocator = allocator;\n    this.chunkSourceFactory = chunkSourceFactory;\n    this.transferListener = transferListener;\n    this.durationUs = durationUs;\n    this.sampleStreams = newSampleStreamArray(0);\n  }\n\n  @Override\n  public void release() {\n    for (ChunkSampleStream<FakeChunkSource> sampleStream : sampleStreams) {\n      sampleStream.release();\n    }\n    super.release();\n  }\n\n  @Override\n  public synchronized void prepare(Callback callback, long positionUs) {\n    super.prepare(callback, positionUs);\n    this.callback = callback;\n  }\n\n  @Override\n  @SuppressWarnings(\"unchecked\")\n  public long selectTracks(\n      TrackSelection[] selections,\n      boolean[] mayRetainStreamFlags,\n      SampleStream[] streams,\n      boolean[] streamResetFlags,\n      long positionUs) {\n    long returnPositionUs = super.selectTracks(selections, mayRetainStreamFlags, streams,\n        streamResetFlags, positionUs);\n    List<ChunkSampleStream<FakeChunkSource>> validStreams = new ArrayList<>();\n    for (SampleStream stream : streams) {\n      if (stream != null) {\n        validStreams.add((ChunkSampleStream<FakeChunkSource>) stream);\n      }\n    }\n    this.sampleStreams = validStreams.toArray(newSampleStreamArray(validStreams.size()));\n    this.sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);\n    return returnPositionUs;\n  }\n\n  @Override\n  public void discardBuffer(long positionUs, boolean toKeyframe) {\n    super.discardBuffer(positionUs, toKeyframe);\n    for (ChunkSampleStream<FakeChunkSource> sampleStream : sampleStreams) {\n      sampleStream.discardBuffer(positionUs, toKeyframe);\n    }\n  }\n\n  @Override\n  public void reevaluateBuffer(long positionUs) {\n    super.reevaluateBuffer(positionUs);\n    sequenceableLoader.reevaluateBuffer(positionUs);\n  }\n\n  @Override\n  public long getBufferedPositionUs() {\n    super.getBufferedPositionUs();\n    return sequenceableLoader.getBufferedPositionUs();\n  }\n\n  @Override\n  public long seekToUs(long positionUs) {\n    for (ChunkSampleStream<FakeChunkSource> sampleStream : sampleStreams) {\n      sampleStream.seekToUs(positionUs);\n    }\n    return super.seekToUs(positionUs);\n  }\n\n  @Override\n  public long getNextLoadPositionUs() {\n    super.getNextLoadPositionUs();\n    return sequenceableLoader.getNextLoadPositionUs();\n  }\n\n  @Override\n  public boolean continueLoading(long positionUs) {\n    super.continueLoading(positionUs);\n    return sequenceableLoader.continueLoading(positionUs);\n  }\n\n  @Override\n  protected SampleStream createSampleStream(TrackSelection trackSelection) {\n    FakeChunkSource chunkSource =\n        chunkSourceFactory.createChunkSource(trackSelection, durationUs, transferListener);\n    return new ChunkSampleStream<>(\n        MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType),\n        /* embeddedTrackTypes= */ null,\n        /* embeddedTrackFormats= */ null,\n        chunkSource,\n        /* callback= */ this,\n        allocator,\n        /* positionUs= */ 0,\n        new DefaultLoadErrorHandlingPolicy(/* minimumLoadableRetryCount= */ 3),\n        eventDispatcher);\n  }\n\n  @Override\n  public void onContinueLoadingRequested(ChunkSampleStream<FakeChunkSource> source) {\n    callback.onContinueLoadingRequested(this);\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private static ChunkSampleStream<FakeChunkSource>[] newSampleStreamArray(int length) {\n    return new ChunkSampleStream[length];\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.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.testutil;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.Timeline.Period;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.upstream.Allocator;\nimport com.google.android.exoplayer2.upstream.TransferListener;\n\n/**\n * Fake {@link MediaSource} that provides a given timeline. Creating the period returns a\n * {@link FakeAdaptiveMediaPeriod} from the given {@link TrackGroupArray}.\n */\npublic class FakeAdaptiveMediaSource extends FakeMediaSource {\n\n  private final FakeChunkSource.Factory chunkSourceFactory;\n\n  public FakeAdaptiveMediaSource(\n      Timeline timeline,\n      Object manifest,\n      TrackGroupArray trackGroupArray,\n      FakeChunkSource.Factory chunkSourceFactory) {\n    super(timeline, manifest, trackGroupArray);\n    this.chunkSourceFactory = chunkSourceFactory;\n  }\n\n  @Override\n  protected FakeMediaPeriod createFakeMediaPeriod(\n      MediaPeriodId id,\n      TrackGroupArray trackGroupArray,\n      Allocator allocator,\n      EventDispatcher eventDispatcher,\n      @Nullable TransferListener transferListener) {\n    Period period = timeline.getPeriodByUid(id.periodUid, new Period());\n    return new FakeAdaptiveMediaPeriod(\n        trackGroupArray,\n        eventDispatcher,\n        allocator,\n        chunkSourceFactory,\n        period.durationUs,\n        transferListener);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.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.testutil;\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.source.chunk.Chunk;\nimport com.google.android.exoplayer2.source.chunk.ChunkHolder;\nimport com.google.android.exoplayer2.source.chunk.ChunkSource;\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.testutil.FakeDataSet.FakeData.Segment;\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.MimeTypes;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.List;\n\n/**\n * Fake {@link ChunkSource} with adaptive media chunks of a given duration.\n */\npublic final class FakeChunkSource implements ChunkSource {\n\n  /**\n   * Factory for a {@link FakeChunkSource}.\n   */\n  public static final class Factory {\n\n    private final FakeAdaptiveDataSet.Factory dataSetFactory;\n    private final FakeDataSource.Factory dataSourceFactory;\n\n    public Factory(FakeAdaptiveDataSet.Factory dataSetFactory,\n        FakeDataSource.Factory dataSourceFactory) {\n      this.dataSetFactory = dataSetFactory;\n      this.dataSourceFactory = dataSourceFactory;\n    }\n\n    public FakeChunkSource createChunkSource(\n        TrackSelection trackSelection,\n        long durationUs,\n        @Nullable TransferListener transferListener) {\n      FakeAdaptiveDataSet dataSet =\n          dataSetFactory.createDataSet(trackSelection.getTrackGroup(), durationUs);\n      dataSourceFactory.setFakeDataSet(dataSet);\n      DataSource dataSource = dataSourceFactory.createDataSource();\n      if (transferListener != null) {\n        dataSource.addTransferListener(transferListener);\n      }\n      return new FakeChunkSource(trackSelection, dataSource, dataSet);\n    }\n\n  }\n\n  private final TrackSelection trackSelection;\n  private final DataSource dataSource;\n  private final FakeAdaptiveDataSet dataSet;\n\n  public FakeChunkSource(TrackSelection trackSelection, DataSource dataSource,\n      FakeAdaptiveDataSet dataSet) {\n    this.trackSelection = trackSelection;\n    this.dataSource = dataSource;\n    this.dataSet = dataSet;\n  }\n\n  @Override\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    int chunkIndex = dataSet.getChunkIndexByPosition(positionUs);\n    long firstSyncUs = dataSet.getStartTime(chunkIndex);\n    long secondSyncUs =\n        firstSyncUs < positionUs && chunkIndex < dataSet.getChunkCount() - 1\n            ? dataSet.getStartTime(chunkIndex + 1)\n            : firstSyncUs;\n    return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs);\n  }\n\n  @Override\n  public void maybeThrowError() throws IOException {\n    // Do nothing.\n  }\n\n  @Override\n  public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {\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    long bufferedDurationUs = loadPositionUs - playbackPositionUs;\n    int chunkIndex =\n        queue.isEmpty()\n            ? dataSet.getChunkIndexByPosition(playbackPositionUs)\n            : (int) queue.get(queue.size() - 1).getNextChunkIndex();\n    MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()];\n    for (int i = 0; i < chunkIterators.length; i++) {\n      int trackGroupIndex = trackSelection.getIndexInTrackGroup(i);\n      chunkIterators[i] = new FakeAdaptiveDataSet.Iterator(dataSet, trackGroupIndex, chunkIndex);\n    }\n    trackSelection.updateSelectedTrack(\n        playbackPositionUs, bufferedDurationUs, C.TIME_UNSET, queue, chunkIterators);\n    if (chunkIndex >= dataSet.getChunkCount()) {\n      out.endOfStream = true;\n    } else {\n      Format selectedFormat = trackSelection.getSelectedFormat();\n      long startTimeUs = dataSet.getStartTime(chunkIndex);\n      long endTimeUs = startTimeUs + dataSet.getChunkDuration(chunkIndex);\n      int trackGroupIndex = trackSelection.getIndexInTrackGroup(trackSelection.getSelectedIndex());\n      String uri = dataSet.getUri(trackGroupIndex);\n      Segment fakeDataChunk = dataSet.getData(uri).getSegments().get(chunkIndex);\n      DataSpec dataSpec = new DataSpec(Uri.parse(uri), fakeDataChunk.byteOffset,\n          fakeDataChunk.length, null);\n      int trackType = MimeTypes.getTrackType(selectedFormat.sampleMimeType);\n      out.chunk = new SingleSampleMediaChunk(dataSource, dataSpec, selectedFormat,\n          trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs,\n          endTimeUs, chunkIndex, trackType, selectedFormat);\n    }\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 false;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.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.testutil;\n\nimport android.os.Handler.Callback;\nimport android.os.Looper;\nimport android.os.Message;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.util.HandlerWrapper;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/** Fake {@link Clock} implementation independent of {@link android.os.SystemClock}. */\npublic class FakeClock implements Clock {\n\n  private final List<Long> wakeUpTimes;\n  private final List<HandlerMessageData> handlerMessages;\n\n  private long currentTimeMs;\n\n  /**\n   * Create {@link FakeClock} with an arbitrary initial timestamp.\n   *\n   * @param initialTimeMs Initial timestamp in milliseconds.\n   */\n  public FakeClock(long initialTimeMs) {\n    this.currentTimeMs = initialTimeMs;\n    this.wakeUpTimes = new ArrayList<>();\n    this.handlerMessages = new ArrayList<>();\n  }\n\n  /**\n   * Advance timestamp of {@link FakeClock} by the specified duration.\n   *\n   * @param timeDiffMs The amount of time to add to the timestamp in milliseconds.\n   */\n  public synchronized void advanceTime(long timeDiffMs) {\n    currentTimeMs += timeDiffMs;\n    for (Long wakeUpTime : wakeUpTimes) {\n      if (wakeUpTime <= currentTimeMs) {\n        notifyAll();\n        break;\n      }\n    }\n    for (int i = handlerMessages.size() - 1; i >= 0; i--) {\n      if (handlerMessages.get(i).maybeSendToTarget(currentTimeMs)) {\n        handlerMessages.remove(i);\n      }\n    }\n  }\n\n  @Override\n  public synchronized long elapsedRealtime() {\n    return currentTimeMs;\n  }\n\n  @Override\n  public long uptimeMillis() {\n    return elapsedRealtime();\n  }\n\n  @Override\n  public synchronized void sleep(long sleepTimeMs) {\n    if (sleepTimeMs <= 0) {\n      return;\n    }\n    Long wakeUpTimeMs = currentTimeMs + sleepTimeMs;\n    wakeUpTimes.add(wakeUpTimeMs);\n    while (currentTimeMs < wakeUpTimeMs) {\n      try {\n        wait();\n      } catch (InterruptedException e) {\n        // Ignore InterruptedException as SystemClock.sleep does too.\n      }\n    }\n    wakeUpTimes.remove(wakeUpTimeMs);\n  }\n\n  @Override\n  public HandlerWrapper createHandler(Looper looper, Callback callback) {\n    return new ClockHandler(looper, callback);\n  }\n\n  /** Adds a handler post to list of pending messages. */\n  protected synchronized boolean addHandlerMessageAtTime(\n      HandlerWrapper handler, Runnable runnable, long timeMs) {\n    if (timeMs <= currentTimeMs) {\n      return handler.post(runnable);\n    }\n    handlerMessages.add(new HandlerMessageData(timeMs, handler, runnable));\n    return true;\n  }\n\n  /** Adds an empty handler message to list of pending messages. */\n  protected synchronized boolean addHandlerMessageAtTime(\n      HandlerWrapper handler, int message, long timeMs) {\n    if (timeMs <= currentTimeMs) {\n      return handler.sendEmptyMessage(message);\n    }\n    handlerMessages.add(new HandlerMessageData(timeMs, handler, message));\n    return true;\n  }\n\n  /** Message data saved to send messages or execute runnables at a later time on a Handler. */\n  private static final class HandlerMessageData {\n\n    private final long postTime;\n    private final HandlerWrapper handler;\n    private final Runnable runnable;\n    private final int message;\n\n    public HandlerMessageData(long postTime, HandlerWrapper handler, Runnable runnable) {\n      this.postTime = postTime;\n      this.handler = handler;\n      this.runnable = runnable;\n      this.message = 0;\n    }\n\n    public HandlerMessageData(long postTime, HandlerWrapper handler, int message) {\n      this.postTime = postTime;\n      this.handler = handler;\n      this.runnable = null;\n      this.message = message;\n    }\n\n    /** Sends the message and returns whether the message was sent to its target. */\n    public boolean maybeSendToTarget(long currentTimeMs) {\n      if (postTime <= currentTimeMs) {\n        if (runnable != null) {\n          handler.post(runnable);\n        } else {\n          handler.sendEmptyMessage(message);\n        }\n        return true;\n      }\n      return false;\n    }\n  }\n\n  /** HandlerWrapper implementation using the enclosing Clock to schedule delayed messages. */\n  private final class ClockHandler implements HandlerWrapper {\n\n    private final android.os.Handler handler;\n\n    public ClockHandler(Looper looper, Callback callback) {\n      handler = new android.os.Handler(looper, callback);\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, 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, 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 addHandlerMessageAtTime(this, what, uptimeMs);\n    }\n\n    @Override\n    public void removeMessages(int what) {\n      handler.removeMessages(what);\n    }\n\n    @Override\n    public void removeCallbacksAndMessages(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 addHandlerMessageAtTime(this, runnable, uptimeMillis() + delayMs);\n    }\n  }\n}\n\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.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.testutil;\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.util.Assertions;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\n\n/**\n * Collection of {@link FakeData} to be served by a {@link FakeDataSource}.\n *\n * <p>Multiple fake data can be defined by {@link FakeDataSet#setData(Uri, byte[])} and {@link\n * FakeDataSet#newData(Uri)} methods. It's also possible to define a default data by {@link\n * FakeDataSet#newDefaultData()}.\n *\n * <p>{@link FakeDataSet#newData(Uri)} and {@link FakeDataSet#newDefaultData()} return a {@link\n * FakeData} instance which can be used to define specific results during\n * {@link FakeDataSource#read(byte[], int, int)} calls.\n *\n * <p>The data that will be read from the source can be constructed by calling {@link\n * FakeData#appendReadData(byte[])} Calls to {@link FakeDataSource#read(byte[], int, int)} will not\n * span the boundaries between arrays passed to successive calls, and hence the boundaries control\n * the positions at which read requests to the source may only be partially satisfied.\n *\n * <p>Errors can be inserted by calling {@link FakeData#appendReadError(IOException)}. An inserted\n * error will be thrown from the first call to {@link FakeDataSource#read(byte[], int, int)} that\n * attempts to read from the corresponding position, and from all subsequent calls to\n * {@link FakeDataSource#read(byte[], int, int)} until the source is closed. If the source is closed\n * and re-opened having encountered an error, that error will not be thrown again.\n *\n * <p>Actions are inserted by calling {@link FakeData#appendReadAction(Runnable)}. An actions is\n * triggered when the reading reaches action's position. This can be used to make sure the code is\n * in a certain state while testing.\n *\n * <p>Example usage:\n *\n * <pre>\n *   // Create a FakeDataSource then add default data and two FakeData\n *   // \"test_file\" throws an IOException when tried to be read until closed and reopened.\n *   FakeDataSource fakeDataSource = new FakeDataSource();\n *   fakeDataSource.getDataSet()\n *       .newDefaultData()\n *         .appendReadData(defaultData)\n *         .endData()\n *       .setData(\"http://1\", data1)\n *       .newData(\"test_file\")\n *         .appendReadError(new IOException())\n *         .appendReadData(data2)\n *         .endData();\n * </pre>\n */\npublic class FakeDataSet {\n\n  /** Container of fake data to be served by a {@link FakeDataSource}. */\n  public static final class FakeData {\n\n    /**\n     * A segment of {@link FakeData}. May consist of an action or exception instead of actual data.\n     */\n    public static final class Segment {\n\n      public @Nullable final IOException exception;\n      public @Nullable final byte[] data;\n      public final int length;\n      public final long byteOffset;\n      public @Nullable final Runnable action;\n\n      public boolean exceptionThrown;\n      public boolean exceptionCleared;\n      public int bytesRead;\n\n      private Segment(byte[] data, Segment previousSegment) {\n        this(data, data.length, null, null, previousSegment);\n      }\n\n      private Segment(int length, Segment previousSegment) {\n        this(null, length, null, null, previousSegment);\n      }\n\n      private Segment(IOException exception, Segment previousSegment) {\n        this(null, 0, exception, null, previousSegment);\n      }\n\n      private Segment(Runnable action, Segment previousSegment) {\n        this(null, 0, null, action, previousSegment);\n      }\n\n      private Segment(@Nullable byte[] data, int length, @Nullable IOException exception,\n          @Nullable Runnable action, Segment previousSegment) {\n        this.exception = exception;\n        this.action = action;\n        this.data = data;\n        this.length = length;\n        this.byteOffset = previousSegment == null ? 0\n            : previousSegment.byteOffset + previousSegment.length;\n      }\n\n      public boolean isErrorSegment() {\n        return exception != null;\n      }\n\n      public boolean isActionSegment() {\n        return action != null;\n      }\n\n    }\n\n    /** Uri of the data or null if this is the default FakeData. */\n    public final Uri uri;\n    private final ArrayList<Segment> segments;\n    private final FakeDataSet dataSet;\n    private boolean simulateUnknownLength;\n\n    private FakeData(FakeDataSet dataSet, Uri uri) {\n      this.uri = uri;\n      this.segments = new ArrayList<>();\n      this.dataSet = dataSet;\n    }\n\n    /** Returns the {@link FakeDataSet} this FakeData belongs to. */\n    public FakeDataSet endData() {\n      return dataSet;\n    }\n\n    /**\n     * When set, {@link FakeDataSource#open(DataSpec)} will behave as though the source is unable to\n     * determine the length of the underlying data. Hence the return value will always be equal to\n     * the {@link DataSpec#length} of the argument, including the case where the length is equal to\n     * {@link C#LENGTH_UNSET}.\n     */\n    public FakeData setSimulateUnknownLength(boolean simulateUnknownLength) {\n      this.simulateUnknownLength = simulateUnknownLength;\n      return this;\n    }\n\n    /**\n     * Appends to the underlying data.\n     */\n    public FakeData appendReadData(byte[] data) {\n      Assertions.checkState(data != null && data.length > 0);\n      segments.add(new Segment(data, getLastSegment()));\n      return this;\n    }\n\n    /**\n     * Appends a data segment of the specified length. No actual data is available and the\n     * {@link FakeDataSource} will perform no copy operations when this data is read.\n     */\n    public FakeData appendReadData(int length) {\n      Assertions.checkState(length > 0);\n      segments.add(new Segment(length, getLastSegment()));\n      return this;\n    }\n\n    /**\n     * Appends an error in the underlying data.\n     */\n    public FakeData appendReadError(IOException exception) {\n      segments.add(new Segment(exception, getLastSegment()));\n      return this;\n    }\n\n    /**\n     * Appends an action.\n     */\n    public FakeData appendReadAction(Runnable action) {\n      segments.add(new Segment(action, getLastSegment()));\n      return this;\n    }\n\n    /** Returns the whole data added by {@link #appendReadData(byte[])}. */\n    public byte[] getData() {\n      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();\n      for (Segment segment : segments) {\n        if (segment.data != null) {\n          try {\n            outputStream.write(segment.data);\n          } catch (IOException e) {\n            throw new IllegalStateException(e);\n          }\n        }\n      }\n      return outputStream.toByteArray();\n    }\n\n    /** Returns the list of {@link Segment}s. */\n    public List<Segment> getSegments() {\n      return segments;\n    }\n\n    /** Retuns whether unknown length is simulated */\n    public boolean isSimulatingUnknownLength() {\n      return simulateUnknownLength;\n    }\n\n    private Segment getLastSegment() {\n      int count = segments.size();\n      return count > 0 ? segments.get(count - 1) : null;\n    }\n\n  }\n\n  private final HashMap<Uri, FakeData> dataMap;\n  private FakeData defaultData;\n\n  public FakeDataSet() {\n    dataMap = new HashMap<>();\n  }\n\n  /** Sets the default data, overwrites if there is one already. */\n  public FakeData newDefaultData() {\n    defaultData = new FakeData(this, null);\n    return defaultData;\n  }\n\n  /** Sets random data with the given {@code length} for the given {@code uri}. */\n  public FakeDataSet setRandomData(String uri, int length) {\n    return setRandomData(Uri.parse(uri), length);\n  }\n\n  /** Sets random data with the given {@code length} for the given {@code uri}. */\n  public FakeDataSet setRandomData(Uri uri, int length) {\n    return setData(uri, TestUtil.buildTestData(length));\n  }\n\n  /** Sets the given {@code data} for the given {@code uri}. */\n  public FakeDataSet setData(String uri, byte[] data) {\n    return setData(Uri.parse(uri), data);\n  }\n\n  /** Sets the given {@code data} for the given {@code uri}. */\n  public FakeDataSet setData(Uri uri, byte[] data) {\n    return newData(uri).appendReadData(data).endData();\n  }\n\n  /** Returns a new {@link FakeData} with the given {@code uri}. */\n  public FakeData newData(String uri) {\n    return newData(Uri.parse(uri));\n  }\n\n  /** Returns a new {@link FakeData} with the given {@code uri}. */\n  public FakeData newData(Uri uri) {\n    FakeData data = new FakeData(this, uri);\n    dataMap.put(uri, data);\n    return data;\n  }\n\n  /** Returns the data for the given {@code uri}, or {@code defaultData} if no data is set. */\n  public FakeData getData(String uri) {\n    return getData(Uri.parse(uri));\n  }\n\n  /** Returns the data for the given {@code uri}, or {@code defaultData} if no data is set. */\n  public FakeData getData(Uri uri) {\n    FakeData data = dataMap.get(uri);\n    return data != null ? data : defaultData;\n  }\n\n  /** Returns a list of all data including {@code defaultData}. */\n  public ArrayList<FakeData> getAllData() {\n    ArrayList<FakeData> fakeDatas = new ArrayList<>(dataMap.values());\n    if (defaultData != null) {\n      fakeDatas.add(defaultData);\n    }\n    return fakeDatas;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\nimport android.net.Uri;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.testutil.FakeDataSet.FakeData;\nimport com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment;\nimport com.google.android.exoplayer2.upstream.BaseDataSource;\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 java.io.IOException;\nimport java.util.ArrayList;\n\n/**\n * A fake {@link DataSource} capable of simulating various scenarios. It uses a {@link FakeDataSet}\n * instance which determines the response to data access calls.\n */\npublic class FakeDataSource extends BaseDataSource {\n\n  /**\n   * Factory to create a {@link FakeDataSource}.\n   */\n  public static class Factory implements DataSource.Factory {\n\n    protected FakeDataSet fakeDataSet;\n    protected boolean isNetwork;\n\n    public final Factory setFakeDataSet(FakeDataSet fakeDataSet) {\n      this.fakeDataSet = fakeDataSet;\n      return this;\n    }\n\n    public final Factory setIsNetwork(boolean isNetwork) {\n      this.isNetwork = isNetwork;\n      return this;\n    }\n\n    @Override\n    public FakeDataSource createDataSource() {\n      return new FakeDataSource(fakeDataSet, isNetwork);\n    }\n  }\n\n  private final FakeDataSet fakeDataSet;\n  private final ArrayList<DataSpec> openedDataSpecs;\n\n  private Uri uri;\n  private boolean openCalled;\n  private boolean sourceOpened;\n  private FakeData fakeData;\n  private int currentSegmentIndex;\n  private long bytesRemaining;\n\n  public FakeDataSource() {\n    this(new FakeDataSet());\n  }\n\n  public FakeDataSource(FakeDataSet fakeDataSet) {\n    this(fakeDataSet, /* isNetwork= */ false);\n  }\n\n  public FakeDataSource(FakeDataSet fakeDataSet, boolean isNetwork) {\n    super(isNetwork);\n    Assertions.checkNotNull(fakeDataSet);\n    this.fakeDataSet = fakeDataSet;\n    this.openedDataSpecs = new ArrayList<>();\n  }\n\n  public final FakeDataSet getDataSet() {\n    return fakeDataSet;\n  }\n\n  @Override\n  public final long open(DataSpec dataSpec) throws IOException {\n    Assertions.checkState(!openCalled);\n    openCalled = true;\n\n    // DataSpec requires a matching close call even if open fails.\n    uri = dataSpec.uri;\n    openedDataSpecs.add(dataSpec);\n\n    transferInitializing(dataSpec);\n    fakeData = fakeDataSet.getData(uri.toString());\n    if (fakeData == null) {\n      throw new IOException(\"Data not found: \" + dataSpec.uri);\n    }\n\n    long totalLength = 0;\n    for (Segment segment : fakeData.getSegments()) {\n      totalLength += segment.length;\n    }\n\n    if (totalLength == 0) {\n      throw new IOException(\"Data is empty: \" + dataSpec.uri);\n    }\n\n    // If the source knows that the request is unsatisfiable then fail.\n    if (dataSpec.position >= totalLength || (dataSpec.length != C.LENGTH_UNSET\n        && (dataSpec.position + dataSpec.length > totalLength))) {\n      throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);\n    }\n    // Scan through the segments, configuring them for the current read.\n    boolean findingCurrentSegmentIndex = true;\n    currentSegmentIndex = 0;\n    int scannedLength = 0;\n    for (Segment segment : fakeData.getSegments()) {\n      segment.bytesRead =\n          (int) Math.min(Math.max(0, dataSpec.position - scannedLength), segment.length);\n      scannedLength += segment.length;\n      findingCurrentSegmentIndex &= segment.isErrorSegment() ? segment.exceptionCleared\n          : (!segment.isActionSegment() && segment.bytesRead == segment.length);\n      if (findingCurrentSegmentIndex) {\n        currentSegmentIndex++;\n      }\n    }\n    sourceOpened = true;\n    transferStarted(dataSpec);\n    // Configure bytesRemaining, and return.\n    if (dataSpec.length == C.LENGTH_UNSET) {\n      bytesRemaining = totalLength - dataSpec.position;\n      return fakeData.isSimulatingUnknownLength() ? C.LENGTH_UNSET : bytesRemaining;\n    } else {\n      bytesRemaining = dataSpec.length;\n      return bytesRemaining;\n    }\n  }\n\n  @Override\n  public final int read(byte[] buffer, int offset, int readLength) throws IOException {\n    Assertions.checkState(sourceOpened);\n    while (true) {\n      if (currentSegmentIndex == fakeData.getSegments().size() || bytesRemaining == 0) {\n        return C.RESULT_END_OF_INPUT;\n      }\n      Segment current = fakeData.getSegments().get(currentSegmentIndex);\n      if (current.isErrorSegment()) {\n        if (!current.exceptionCleared) {\n          current.exceptionThrown = true;\n          throw (IOException) current.exception.fillInStackTrace();\n        } else {\n          currentSegmentIndex++;\n        }\n      } else if (current.isActionSegment()) {\n        currentSegmentIndex++;\n        current.action.run();\n      } else {\n        // Read at most bytesRemaining.\n        readLength = (int) Math.min(readLength, bytesRemaining);\n        // Do not allow crossing of the segment boundary.\n        readLength = Math.min(readLength, current.length - current.bytesRead);\n        // Perform the read and return.\n        Assertions.checkArgument(buffer.length - offset >= readLength);\n        if (current.data != null) {\n          System.arraycopy(current.data, current.bytesRead, buffer, offset, readLength);\n        }\n        onDataRead(readLength);\n        bytesTransferred(readLength);\n        bytesRemaining -= readLength;\n        current.bytesRead += readLength;\n        if (current.bytesRead == current.length) {\n          currentSegmentIndex++;\n        }\n        return readLength;\n      }\n    }\n  }\n\n  @Override\n  public final Uri getUri() {\n    return uri;\n  }\n\n  @Override\n  public final void close() throws IOException {\n    Assertions.checkState(openCalled);\n    openCalled = false;\n    uri = null;\n    if (fakeData != null && currentSegmentIndex < fakeData.getSegments().size()) {\n      Segment current = fakeData.getSegments().get(currentSegmentIndex);\n      if (current.isErrorSegment() && current.exceptionThrown) {\n        current.exceptionCleared = true;\n      }\n    }\n    if (sourceOpened) {\n      sourceOpened = false;\n      transferEnded();\n    }\n    fakeData = null;\n  }\n\n  /**\n   * Returns the {@link DataSpec} instances passed to {@link #open(DataSpec)} since the last call to\n   * this method.\n   */\n  public final DataSpec[] getAndClearOpenedDataSpecs() {\n    DataSpec[] dataSpecs = new DataSpec[openedDataSpecs.size()];\n    openedDataSpecs.toArray(dataSpecs);\n    openedDataSpecs.clear();\n    return dataSpecs;\n  }\n\n  /** Returns whether the data source is currently opened. */\n  public final boolean isOpened() {\n    return sourceOpened;\n  }\n\n  protected void onDataRead(int bytesRead) throws IOException {\n    // Do nothing. Can be overridden.\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.util.SparseBooleanArray;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.EOFException;\nimport java.io.IOException;\n\n/**\n * A fake {@link ExtractorInput} capable of simulating various scenarios.\n * <p>\n * Read, skip and peek errors can be simulated using {@link Builder#setSimulateIOErrors}. When\n * enabled each read and skip will throw a {@link SimulatedIOException} unless one has already been\n * thrown from the current position. Each peek will throw {@link SimulatedIOException} unless one\n * has already been thrown from the current peek position. When a {@link SimulatedIOException} is\n * thrown the read position is left unchanged and the peek position is reset back to the read\n * position.\n * <p>\n * Partial reads and skips can be simulated using {@link Builder#setSimulatePartialReads}. When\n * enabled, {@link #read(byte[], int, int)} and {@link #skip(int)} calls will only read or skip a\n * single byte unless a partial read or skip has already been performed that had the same target\n * position. For example, a first read request for 10 bytes will be partially satisfied by reading\n * a single byte and advancing the position to 1. If the following read request attempts to read 9\n * bytes then it will be fully satisfied, since it has the same target position of 10.\n * <p>\n * Unknown data length can be simulated using {@link Builder#setSimulateUnknownLength}. When enabled\n * {@link #getLength()} will return {@link C#LENGTH_UNSET} rather than the length of the data.\n */\npublic final class FakeExtractorInput implements ExtractorInput {\n\n  /**\n   * Thrown when simulating an {@link IOException}.\n   */\n  public static final class SimulatedIOException extends IOException {\n\n    public SimulatedIOException(String message) {\n      super(message);\n    }\n\n  }\n\n  private final byte[] data;\n  private final boolean simulateUnknownLength;\n  private final boolean simulatePartialReads;\n  private final boolean simulateIOErrors;\n\n  private int readPosition;\n  private int peekPosition;\n\n  private final SparseBooleanArray partiallySatisfiedTargetPositions;\n  private final SparseBooleanArray failedReadPositions;\n  private final SparseBooleanArray failedPeekPositions;\n\n  private FakeExtractorInput(byte[] data, boolean simulateUnknownLength,\n      boolean simulatePartialReads, boolean simulateIOErrors) {\n    this.data = data;\n    this.simulateUnknownLength = simulateUnknownLength;\n    this.simulatePartialReads = simulatePartialReads;\n    this.simulateIOErrors = simulateIOErrors;\n    partiallySatisfiedTargetPositions = new SparseBooleanArray();\n    failedReadPositions = new SparseBooleanArray();\n    failedPeekPositions = new SparseBooleanArray();\n  }\n\n  /** Resets the input to its initial state. */\n  public void reset() {\n    readPosition = 0;\n    peekPosition = 0;\n    partiallySatisfiedTargetPositions.clear();\n    failedReadPositions.clear();\n    failedPeekPositions.clear();\n  }\n\n  /**\n   * Sets the read and peek positions.\n   *\n   * @param position The position to set.\n   */\n  public void setPosition(int position) {\n    assertThat(0 <= position && position <= data.length).isTrue();\n    readPosition = position;\n    peekPosition = position;\n  }\n\n  @Override\n  public int read(byte[] target, int offset, int length) throws IOException {\n    checkIOException(readPosition, failedReadPositions);\n    length = getReadLength(length);\n    return readFullyInternal(target, offset, length, true) ? length : C.RESULT_END_OF_INPUT;\n  }\n\n  @Override\n  public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)\n      throws IOException {\n    checkIOException(readPosition, failedReadPositions);\n    return readFullyInternal(target, offset, length, allowEndOfInput);\n  }\n\n  @Override\n  public void readFully(byte[] target, int offset, int length) throws IOException {\n    readFully(target, offset, length, false);\n  }\n\n  @Override\n  public int skip(int length) throws IOException {\n    checkIOException(readPosition, failedReadPositions);\n    length = getReadLength(length);\n    return skipFullyInternal(length, true) ? length : C.RESULT_END_OF_INPUT;\n  }\n\n  @Override\n  public boolean skipFully(int length, boolean allowEndOfInput) throws IOException {\n    checkIOException(readPosition, failedReadPositions);\n    return skipFullyInternal(length, allowEndOfInput);\n  }\n\n  @Override\n  public void skipFully(int length) throws IOException {\n    skipFully(length, false);\n  }\n\n  @Override\n  public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)\n      throws IOException {\n    checkIOException(peekPosition, failedPeekPositions);\n    if (!checkXFully(allowEndOfInput, peekPosition, length)) {\n      return false;\n    }\n    System.arraycopy(data, peekPosition, target, offset, length);\n    peekPosition += length;\n    return true;\n  }\n\n  @Override\n  public void peekFully(byte[] target, int offset, int length) throws IOException {\n    peekFully(target, offset, length, false);\n  }\n\n  @Override\n  public boolean advancePeekPosition(int length, boolean allowEndOfInput) throws IOException {\n    checkIOException(peekPosition, failedPeekPositions);\n    if (!checkXFully(allowEndOfInput, peekPosition, length)) {\n      return false;\n    }\n    peekPosition += length;\n    return true;\n  }\n\n  @Override\n  public void advancePeekPosition(int length) throws IOException {\n    advancePeekPosition(length, false);\n  }\n\n  @Override\n  public void resetPeekPosition() {\n    peekPosition = readPosition;\n  }\n\n  @Override\n  public long getPeekPosition() {\n    return peekPosition;\n  }\n\n  @Override\n  public long getPosition() {\n    return readPosition;\n  }\n\n  @Override\n  public long getLength() {\n    return simulateUnknownLength ? C.LENGTH_UNSET : data.length;\n  }\n\n  @Override\n  public <E extends Throwable> void setRetryPosition(long position, E e) throws E {\n    assertThat(position >= 0).isTrue();\n    readPosition = (int) position;\n    throw e;\n  }\n\n  private void checkIOException(int position, SparseBooleanArray failedPositions)\n      throws SimulatedIOException {\n    if (simulateIOErrors && !failedPositions.get(position)) {\n      failedPositions.put(position, true);\n      peekPosition = readPosition;\n      throw new SimulatedIOException(\"Simulated IO error at position: \" + position);\n    }\n  }\n\n  private boolean checkXFully(boolean allowEndOfInput, int position, int length)\n      throws EOFException {\n    if (length > 0 && position == data.length) {\n      if (allowEndOfInput) {\n        return false;\n      }\n      throw new EOFException();\n    }\n    if (position + length > data.length) {\n      throw new EOFException(\"Attempted to move past end of data: (\" + position + \" + \"\n          + length + \") > \" + data.length);\n    }\n    return true;\n  }\n\n  private int getReadLength(int requestedLength) {\n    if (readPosition == data.length) {\n      // If the requested length is non-zero, the end of the input will be read.\n      return requestedLength == 0 ? 0 : Integer.MAX_VALUE;\n    }\n    int targetPosition = readPosition + requestedLength;\n    if (simulatePartialReads && requestedLength > 1\n        && !partiallySatisfiedTargetPositions.get(targetPosition)) {\n      partiallySatisfiedTargetPositions.put(targetPosition, true);\n      return 1;\n    }\n    return Math.min(requestedLength, data.length - readPosition);\n  }\n\n  private boolean readFullyInternal(byte[] target, int offset, int length, boolean allowEndOfInput)\n      throws EOFException {\n    if (!checkXFully(allowEndOfInput, readPosition, length)) {\n      return false;\n    }\n    System.arraycopy(data, readPosition, target, offset, length);\n    readPosition += length;\n    peekPosition = readPosition;\n    return true;\n  }\n\n  private boolean skipFullyInternal(int length, boolean allowEndOfInput) throws EOFException {\n    if (!checkXFully(allowEndOfInput, readPosition, length)) {\n      return false;\n    }\n    readPosition += length;\n    peekPosition = readPosition;\n    return true;\n  }\n\n  /**\n   * Builder of {@link FakeExtractorInput} instances.\n   */\n  public static final class Builder {\n\n    private byte[] data;\n    private boolean simulateUnknownLength;\n    private boolean simulatePartialReads;\n    private boolean simulateIOErrors;\n\n    public Builder() {\n      data = Util.EMPTY_BYTE_ARRAY;\n    }\n\n    public Builder setData(byte[] data) {\n      this.data = data;\n      return this;\n    }\n\n    public Builder setSimulateUnknownLength(boolean simulateUnknownLength) {\n      this.simulateUnknownLength = simulateUnknownLength;\n      return this;\n    }\n\n    public Builder setSimulatePartialReads(boolean simulatePartialReads) {\n      this.simulatePartialReads = simulatePartialReads;\n      return this;\n    }\n\n    public Builder setSimulateIOErrors(boolean simulateIOErrors) {\n      this.simulateIOErrors = simulateIOErrors;\n      return this;\n    }\n\n    public FakeExtractorInput build() {\n      return new FakeExtractorInput(data, simulateUnknownLength, simulatePartialReads,\n          simulateIOErrors);\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static com.google.common.truth.Truth.assertWithMessage;\n\nimport android.content.Context;\nimport android.util.SparseArray;\nimport com.google.android.exoplayer2.extractor.ExtractorOutput;\nimport com.google.android.exoplayer2.extractor.SeekMap;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.PrintWriter;\n\n/**\n * A fake {@link ExtractorOutput}.\n */\npublic final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpable {\n\n  /**\n   * If true, makes {@link #assertOutput(Context, String)} method write dump result to {@code\n   * /sdcard/Android/data/apk_package/ + dumpfile} file instead of comparing it with an existing\n   * file.\n   */\n  private static final boolean WRITE_DUMP = false;\n\n  public final SparseArray<FakeTrackOutput> trackOutputs;\n\n  public int numberOfTracks;\n  public boolean tracksEnded;\n  public SeekMap seekMap;\n\n  public FakeExtractorOutput() {\n    trackOutputs = new SparseArray<>();\n  }\n\n  @Override\n  public FakeTrackOutput track(int id, int type) {\n    FakeTrackOutput output = trackOutputs.get(id);\n    if (output == null) {\n      assertThat(tracksEnded).isFalse();\n      numberOfTracks++;\n      output = new FakeTrackOutput();\n      trackOutputs.put(id, output);\n    }\n    return output;\n  }\n\n  @Override\n  public void endTracks() {\n    tracksEnded = true;\n  }\n\n  @Override\n  public void seekMap(SeekMap seekMap) {\n    this.seekMap = seekMap;\n  }\n\n  public void clearTrackOutputs() {\n    for (int i = 0; i < numberOfTracks; i++) {\n      trackOutputs.valueAt(i).clear();\n    }\n  }\n\n  public void assertEquals(FakeExtractorOutput expected) {\n    assertThat(numberOfTracks).isEqualTo(expected.numberOfTracks);\n    assertThat(tracksEnded).isEqualTo(expected.tracksEnded);\n    if (expected.seekMap == null) {\n      assertThat(seekMap).isNull();\n    } else {\n      // TODO: Bulk up this check if possible.\n      assertThat(seekMap).isNotNull();\n      assertThat(seekMap.getClass()).isEqualTo(expected.seekMap.getClass());\n      assertThat(seekMap.isSeekable()).isEqualTo(expected.seekMap.isSeekable());\n      assertThat(seekMap.getSeekPoints(0)).isEqualTo(expected.seekMap.getSeekPoints(0));\n    }\n    for (int i = 0; i < numberOfTracks; i++) {\n      assertThat(trackOutputs.keyAt(i)).isEqualTo(expected.trackOutputs.keyAt(i));\n      trackOutputs.valueAt(i).assertEquals(expected.trackOutputs.valueAt(i));\n    }\n  }\n\n  /**\n   * Asserts that dump of this {@link FakeExtractorOutput} is equal to expected dump which is read\n   * from {@code dumpFile}.\n   *\n   * <p>If assertion fails because of an intended change in the output or a new dump file needs to\n   * be created, set {@link #WRITE_DUMP} flag to true and run the test again. Instead of assertion,\n   * actual dump will be written to {@code dumpFile}. This new dump file needs to be copied to the\n   * project, {@code library/src/androidTest/assets} folder manually.\n   */\n  public void assertOutput(Context context, String dumpFile) throws IOException {\n    String actual = new Dumper().add(this).toString();\n\n    if (WRITE_DUMP) {\n      File directory = context.getExternalFilesDir(null);\n      File file = new File(directory, dumpFile);\n      file.getParentFile().mkdirs();\n      PrintWriter out = new PrintWriter(file);\n      out.print(actual);\n      out.close();\n    } else {\n      String expected = TestUtil.getString(context, dumpFile);\n      assertWithMessage(dumpFile).that(actual).isEqualTo(expected);\n    }\n  }\n\n  @Override\n  public void dump(Dumper dumper) {\n    if (seekMap != null) {\n      dumper\n          .startBlock(\"seekMap\")\n          .add(\"isSeekable\", seekMap.isSeekable())\n          .addTime(\"duration\", seekMap.getDurationUs())\n          .add(\"getPosition(0)\", seekMap.getSeekPoints(0))\n          .endBlock();\n    }\n    dumper.add(\"numberOfTracks\", numberOfTracks);\n    for (int i = 0; i < numberOfTracks; i++) {\n      dumper.startBlock(\"track \" + trackOutputs.keyAt(i))\n          .add(trackOutputs.valueAt(i))\n          .endBlock();\n    }\n    dumper.add(\"tracksEnded\", tracksEnded);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\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.SeekParameters;\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.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport java.io.IOException;\nimport java.util.Collections;\n\n/**\n * Fake {@link MediaPeriod} that provides tracks from the given {@link TrackGroupArray}. Selecting\n * tracks will give the player {@link FakeSampleStream}s. Loading data completes immediately after\n * the period has finished preparing.\n */\npublic class FakeMediaPeriod implements MediaPeriod {\n\n  public static final DataSpec FAKE_DATA_SPEC = new DataSpec(Uri.parse(\"http://fake.uri\"));\n\n  private final TrackGroupArray trackGroupArray;\n  protected final EventDispatcher eventDispatcher;\n\n  @Nullable private Handler playerHandler;\n  @Nullable private Callback prepareCallback;\n\n  private boolean deferOnPrepared;\n  private boolean notifiedReadingStarted;\n  private boolean prepared;\n  private long seekOffsetUs;\n  private long discontinuityPositionUs;\n\n  /**\n   * @param trackGroupArray The track group array.\n   * @param eventDispatcher A dispatcher for media source events.\n   */\n  public FakeMediaPeriod(TrackGroupArray trackGroupArray, EventDispatcher eventDispatcher) {\n    this(trackGroupArray, eventDispatcher, /* deferOnPrepared */ false);\n  }\n\n  /**\n   * @param trackGroupArray The track group array.\n   * @param eventDispatcher A dispatcher for media source events.\n   * @param deferOnPrepared Whether {@link MediaPeriod.Callback#onPrepared(MediaPeriod)} should be\n   *     called only after {@link #setPreparationComplete()} has been called. If {@code false}\n   *     preparation completes immediately.\n   */\n  public FakeMediaPeriod(\n      TrackGroupArray trackGroupArray, EventDispatcher eventDispatcher, boolean deferOnPrepared) {\n    this.trackGroupArray = trackGroupArray;\n    this.eventDispatcher = eventDispatcher;\n    this.deferOnPrepared = deferOnPrepared;\n    discontinuityPositionUs = C.TIME_UNSET;\n    eventDispatcher.mediaPeriodCreated();\n  }\n\n  /**\n   * Sets a discontinuity position to be returned from the next call to\n   * {@link #readDiscontinuity()}.\n   *\n   * @param discontinuityPositionUs The position to be returned, in microseconds.\n   */\n  public void setDiscontinuityPositionUs(long discontinuityPositionUs) {\n    this.discontinuityPositionUs = discontinuityPositionUs;\n  }\n\n  /**\n   * Allows the fake media period to complete preparation. May be called on any thread.\n   */\n  public synchronized void setPreparationComplete() {\n    deferOnPrepared = false;\n    if (playerHandler != null && prepareCallback != null) {\n      playerHandler.post(this::finishPreparation);\n    }\n  }\n\n  /**\n   * Sets an offset to be applied to positions returned by {@link #seekToUs(long)}.\n   *\n   * @param seekOffsetUs The offset to be applied, in microseconds.\n   */\n  public void setSeekToUsOffset(long seekOffsetUs) {\n    this.seekOffsetUs = seekOffsetUs;\n  }\n\n  public void release() {\n    prepared = false;\n    eventDispatcher.mediaPeriodReleased();\n  }\n\n  @Override\n  public synchronized void prepare(Callback callback, long positionUs) {\n    eventDispatcher.loadStarted(\n        FAKE_DATA_SPEC,\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        /* mediaEndTimeUs = */ C.TIME_UNSET,\n        SystemClock.elapsedRealtime());\n    prepareCallback = callback;\n    if (deferOnPrepared) {\n      playerHandler = new Handler();\n    } else {\n      finishPreparation();\n    }\n  }\n\n  @Override\n  public void maybeThrowPrepareError() throws IOException {\n    // Do nothing.\n  }\n\n  @Override\n  public TrackGroupArray getTrackGroups() {\n    assertThat(prepared).isTrue();\n    return trackGroupArray;\n  }\n\n  @Override\n  public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,\n      SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {\n    assertThat(prepared).isTrue();\n    int rendererCount = selections.length;\n    for (int i = 0; i < rendererCount; i++) {\n      if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {\n        streams[i] = null;\n      }\n      if (streams[i] == null && selections[i] != null) {\n        TrackSelection selection = selections[i];\n        assertThat(selection.length()).isAtLeast(1);\n        TrackGroup trackGroup = selection.getTrackGroup();\n        assertThat(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET).isTrue();\n        int indexInTrackGroup = selection.getIndexInTrackGroup(selection.getSelectedIndex());\n        assertThat(indexInTrackGroup).isAtLeast(0);\n        assertThat(indexInTrackGroup).isLessThan(trackGroup.length);\n        streams[i] = createSampleStream(selection);\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 long readDiscontinuity() {\n    assertThat(prepared).isTrue();\n    if (!notifiedReadingStarted) {\n      eventDispatcher.readingStarted();\n      notifiedReadingStarted = true;\n    }\n    long positionDiscontinuityUs = this.discontinuityPositionUs;\n    this.discontinuityPositionUs = C.TIME_UNSET;\n    return positionDiscontinuityUs;\n  }\n\n  @Override\n  public long getBufferedPositionUs() {\n    assertThat(prepared).isTrue();\n    return C.TIME_END_OF_SOURCE;\n  }\n\n  @Override\n  public long seekToUs(long positionUs) {\n    assertThat(prepared).isTrue();\n    return positionUs + seekOffsetUs;\n  }\n\n  @Override\n  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {\n    return positionUs;\n  }\n\n  @Override\n  public long getNextLoadPositionUs() {\n    assertThat(prepared).isTrue();\n    return C.TIME_END_OF_SOURCE;\n  }\n\n  @Override\n  public boolean continueLoading(long positionUs) {\n    return false;\n  }\n\n  protected SampleStream createSampleStream(TrackSelection selection) {\n    return new FakeSampleStream(\n        selection.getSelectedFormat(), eventDispatcher, /* shouldOutputSample= */ true);\n  }\n\n  private void finishPreparation() {\n    prepared = true;\n    prepareCallback.onPrepared(this);\n    eventDispatcher.loadCompleted(\n        FAKE_DATA_SPEC,\n        FAKE_DATA_SPEC.uri,\n        /* responseHeaders= */ Collections.emptyMap(),\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        /* mediaEndTimeUs = */ C.TIME_UNSET,\n        SystemClock.elapsedRealtime(),\n        /* loadDurationMs= */ 0,\n        /* bytesLoaded= */ 100);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\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.Format;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.Timeline.Period;\nimport com.google.android.exoplayer2.source.BaseMediaSource;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;\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.upstream.Allocator;\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.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Fake {@link MediaSource} that provides a given timeline. Creating the period will return a {@link\n * FakeMediaPeriod} with a {@link TrackGroupArray} using the given {@link Format}s.\n */\npublic class FakeMediaSource extends BaseMediaSource {\n\n  private static final DataSpec FAKE_DATA_SPEC = new DataSpec(Uri.parse(\"http://manifest.uri\"));\n  private static final int MANIFEST_LOAD_BYTES = 100;\n\n  private final TrackGroupArray trackGroupArray;\n  private final ArrayList<FakeMediaPeriod> activeMediaPeriods;\n  private final ArrayList<MediaPeriodId> createdMediaPeriods;\n\n  protected Timeline timeline;\n  private Object manifest;\n  private boolean preparedSource;\n  private boolean releasedSource;\n  private Handler sourceInfoRefreshHandler;\n  private @Nullable TransferListener transferListener;\n\n  /**\n   * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with a\n   * {@link TrackGroupArray} using the given {@link Format}s. The provided {@link Timeline} may be\n   * null to prevent an immediate source info refresh message when preparing the media source. It\n   * can be manually set later using {@link #setNewSourceInfo(Timeline, Object)}.\n   */\n  public FakeMediaSource(@Nullable Timeline timeline, Object manifest, Format... formats) {\n    this(timeline, manifest, buildTrackGroupArray(formats));\n  }\n\n  /**\n   * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with the\n   * given {@link TrackGroupArray}. The provided {@link Timeline} may be null to prevent an\n   * immediate source info refresh message when preparing the media source. It can be manually set\n   * later using {@link #setNewSourceInfo(Timeline, Object)}.\n   */\n  public FakeMediaSource(@Nullable Timeline timeline, Object manifest,\n      TrackGroupArray trackGroupArray) {\n    this.timeline = timeline;\n    this.manifest = manifest;\n    this.activeMediaPeriods = new ArrayList<>();\n    this.createdMediaPeriods = new ArrayList<>();\n    this.trackGroupArray = trackGroupArray;\n  }\n\n  @Override\n  @Nullable\n  public Object getTag() {\n    boolean hasTimeline = timeline != null && !timeline.isEmpty();\n    return hasTimeline ? timeline.getWindow(0, new Timeline.Window()).tag : null;\n  }\n\n  @Override\n  public synchronized void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {\n    assertThat(preparedSource).isFalse();\n    transferListener = mediaTransferListener;\n    preparedSource = true;\n    releasedSource = false;\n    sourceInfoRefreshHandler = new Handler();\n    if (timeline != null) {\n      finishSourcePreparation();\n    }\n  }\n\n  @Override\n  public void maybeThrowSourceInfoRefreshError() throws IOException {\n    assertThat(preparedSource).isTrue();\n  }\n\n  @Override\n  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {\n    assertThat(preparedSource).isTrue();\n    assertThat(releasedSource).isFalse();\n    int periodIndex = timeline.getIndexOfPeriod(id.periodUid);\n    Assertions.checkArgument(periodIndex != C.INDEX_UNSET);\n    Period period = timeline.getPeriod(periodIndex, new Period());\n    EventDispatcher eventDispatcher =\n        createEventDispatcher(period.windowIndex, id, period.getPositionInWindowMs());\n    FakeMediaPeriod mediaPeriod =\n        createFakeMediaPeriod(id, trackGroupArray, allocator, eventDispatcher, transferListener);\n    activeMediaPeriods.add(mediaPeriod);\n    createdMediaPeriods.add(id);\n    return mediaPeriod;\n  }\n\n  @Override\n  public void releasePeriod(MediaPeriod mediaPeriod) {\n    assertThat(preparedSource).isTrue();\n    assertThat(releasedSource).isFalse();\n    FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod;\n    assertThat(activeMediaPeriods.remove(fakeMediaPeriod)).isTrue();\n    fakeMediaPeriod.release();\n  }\n\n  @Override\n  public void releaseSourceInternal() {\n    assertThat(preparedSource).isTrue();\n    assertThat(releasedSource).isFalse();\n    assertThat(activeMediaPeriods.isEmpty()).isTrue();\n    releasedSource = true;\n    preparedSource = false;\n    sourceInfoRefreshHandler.removeCallbacksAndMessages(null);\n    sourceInfoRefreshHandler = null;\n  }\n\n  /**\n   * Sets a new timeline and manifest. If the source is already prepared, this triggers a source\n   * info refresh message being sent to the listener.\n   */\n  public synchronized void setNewSourceInfo(final Timeline newTimeline, final Object newManifest) {\n    if (sourceInfoRefreshHandler != null) {\n      sourceInfoRefreshHandler.post(\n          () -> {\n            assertThat(releasedSource).isFalse();\n            assertThat(preparedSource).isTrue();\n            timeline = newTimeline;\n            manifest = newManifest;\n            finishSourcePreparation();\n          });\n    } else {\n      timeline = newTimeline;\n      manifest = newManifest;\n    }\n  }\n\n  /** Returns whether the source is currently prepared. */\n  public boolean isPrepared() {\n    return preparedSource;\n  }\n\n  /**\n   * Assert that the source and all periods have been released.\n   */\n  public void assertReleased() {\n    assertThat(releasedSource || !preparedSource).isTrue();\n  }\n\n  /**\n   * Assert that a media period for the given id has been created.\n   */\n  public void assertMediaPeriodCreated(MediaPeriodId mediaPeriodId) {\n    assertThat(createdMediaPeriods).contains(mediaPeriodId);\n  }\n\n  /** Returns a list of {@link MediaPeriodId}s, with one element for each created media period. */\n  public List<MediaPeriodId> getCreatedMediaPeriods() {\n    return createdMediaPeriods;\n  }\n\n  /**\n   * Creates a {@link FakeMediaPeriod} for this media source.\n   *\n   * @param id The identifier of the period.\n   * @param trackGroupArray The {@link TrackGroupArray} supported by the media period.\n   * @param allocator An {@link Allocator} from which to obtain media buffer allocations.\n   * @param eventDispatcher An {@link EventDispatcher} to dispatch media source events.\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 A new {@link FakeMediaPeriod}.\n   */\n  protected FakeMediaPeriod createFakeMediaPeriod(\n      MediaPeriodId id,\n      TrackGroupArray trackGroupArray,\n      Allocator allocator,\n      EventDispatcher eventDispatcher,\n      @Nullable TransferListener transferListener) {\n    return new FakeMediaPeriod(trackGroupArray, eventDispatcher);\n  }\n\n  private void finishSourcePreparation() {\n    refreshSourceInfo(timeline, manifest);\n    if (!timeline.isEmpty()) {\n      MediaLoadData mediaLoadData =\n          new MediaLoadData(\n              C.DATA_TYPE_MANIFEST,\n              C.TRACK_TYPE_UNKNOWN,\n              /* trackFormat= */ null,\n              C.SELECTION_REASON_UNKNOWN,\n              /* trackSelectionData= */ null,\n              /* mediaStartTimeMs= */ C.TIME_UNSET,\n              /* mediaEndTimeMs = */ C.TIME_UNSET);\n      long elapsedRealTimeMs = SystemClock.elapsedRealtime();\n      EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);\n      eventDispatcher.loadStarted(\n          new LoadEventInfo(\n              FAKE_DATA_SPEC,\n              FAKE_DATA_SPEC.uri,\n              /* responseHeaders= */ Collections.emptyMap(),\n              elapsedRealTimeMs,\n              /* loadDurationMs= */ 0,\n              /* bytesLoaded= */ 0),\n          mediaLoadData);\n      eventDispatcher.loadCompleted(\n          new LoadEventInfo(\n              FAKE_DATA_SPEC,\n              FAKE_DATA_SPEC.uri,\n              /* responseHeaders= */ Collections.emptyMap(),\n              elapsedRealTimeMs,\n              /* loadDurationMs= */ 0,\n              /* bytesLoaded= */ MANIFEST_LOAD_BYTES),\n          mediaLoadData);\n    }\n  }\n\n  private static TrackGroupArray buildTrackGroupArray(Format... formats) {\n    TrackGroup[] trackGroups = new TrackGroup[formats.length];\n    for (int i = 0; i < formats.length; i++) {\n      trackGroups[i] = new TrackGroup(formats[i]);\n    }\n    return new TrackGroupArray(trackGroups);\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\n\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.decoder.DecoderInputBuffer;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Fake {@link Renderer} that supports any format with the matching MIME type. The renderer\n * verifies that it reads one of the given {@link Format}s.\n */\npublic class FakeRenderer extends BaseRenderer {\n\n  /**\n   * The amount of time ahead of the current playback position that the renderer reads from the\n   * source. A real renderer will typically read ahead by a small amount due to pipelining through\n   * decoders and the media output path.\n   */\n  private static final long SOURCE_READAHEAD_US = 250000;\n\n  private final List<Format> expectedFormats;\n  private final DecoderInputBuffer buffer;\n  private final FormatHolder formatHolder;\n\n  private long playbackPositionUs;\n  private long lastSamplePositionUs;\n\n  public boolean isEnded;\n  public int positionResetCount;\n  public int formatReadCount;\n  public int sampleBufferReadCount;\n\n  public FakeRenderer(Format... expectedFormats) {\n    super(expectedFormats.length == 0 ? C.TRACK_TYPE_UNKNOWN\n        : MimeTypes.getTrackType(expectedFormats[0].sampleMimeType));\n    this.expectedFormats = Collections.unmodifiableList(Arrays.asList(expectedFormats));\n    buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);\n    formatHolder = new FormatHolder();\n    lastSamplePositionUs = Long.MIN_VALUE;\n  }\n\n  @Override\n  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {\n    playbackPositionUs = positionUs;\n    lastSamplePositionUs = Long.MIN_VALUE;\n    positionResetCount++;\n    isEnded = false;\n  }\n\n  @Override\n  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {\n    if (isEnded) {\n      return;\n    }\n    playbackPositionUs = positionUs;\n    while (lastSamplePositionUs < positionUs + SOURCE_READAHEAD_US) {\n      formatHolder.format = null;\n      buffer.clear();\n      int result = readSource(formatHolder, buffer, false);\n      if (result == C.RESULT_FORMAT_READ) {\n        formatReadCount++;\n        assertThat(expectedFormats).contains(formatHolder.format);\n        onFormatChanged(formatHolder.format);\n      } else if (result == C.RESULT_BUFFER_READ) {\n        if (buffer.isEndOfStream()) {\n          isEnded = true;\n          return;\n        }\n        lastSamplePositionUs = buffer.timeUs;\n        sampleBufferReadCount++;\n        onBufferRead();\n      } else {\n        Assertions.checkState(result == C.RESULT_NOTHING_READ);\n        return;\n      }\n    }\n  }\n\n  @Override\n  public boolean isReady() {\n    return lastSamplePositionUs >= playbackPositionUs || isSourceReady();\n  }\n\n  @Override\n  public boolean isEnded() {\n    return isEnded;\n  }\n\n  @Override\n  public int supportsFormat(Format format) throws ExoPlaybackException {\n    return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType)\n        ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE;\n  }\n\n  /** Called when the renderer reads a new format. */\n  protected void onFormatChanged(Format format) {}\n\n  /** Called when the renderer read a sample from the buffer. */\n  protected void onBufferRead() {}\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.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.testutil;\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.source.MediaSourceEventListener.EventDispatcher;\nimport com.google.android.exoplayer2.source.SampleStream;\nimport java.io.IOException;\n\n/**\n * Fake {@link SampleStream} that outputs a given {@link Format}, an optional sample containing a\n * single zero byte, then end of stream.\n */\npublic final class FakeSampleStream implements SampleStream {\n\n  private final Format format;\n  private final @Nullable EventDispatcher eventDispatcher;\n  private final byte[] sampleData;\n\n  private boolean notifiedDownstreamFormat;\n  private boolean readFormat;\n  private boolean readSample;\n\n  /**\n   * Creates fake sample stream which outputs the given {@link Format}, optionally one sample with\n   * zero bytes, then end of stream.\n   *\n   * @param format The {@link Format} to output.\n   * @param eventDispatcher An {@link EventDispatcher} to notify of read events.\n   * @param shouldOutputSample Whether the sample stream should output a sample.\n   */\n  public FakeSampleStream(\n      Format format, @Nullable EventDispatcher eventDispatcher, boolean shouldOutputSample) {\n    this(format, eventDispatcher, new byte[] {0});\n    readSample = !shouldOutputSample;\n  }\n\n  /**\n   * Creates fake sample stream which outputs the given {@link Format}, one sample with the provided\n   * bytes, then end of stream.\n   *\n   * @param format The {@link Format} to output.\n   * @param eventDispatcher An {@link EventDispatcher} to notify of read events.\n   * @param sampleData The sample data to output.\n   */\n  public FakeSampleStream(\n      Format format, @Nullable EventDispatcher eventDispatcher, byte[] sampleData) {\n    this.format = format;\n    this.eventDispatcher = eventDispatcher;\n    this.sampleData = sampleData;\n  }\n\n  @Override\n  public boolean isReady() {\n    return true;\n  }\n\n  @Override\n  public int readData(\n      FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) {\n    if (eventDispatcher != null && !notifiedDownstreamFormat) {\n      eventDispatcher.downstreamFormatChanged(\n          C.TRACK_TYPE_UNKNOWN,\n          format,\n          C.SELECTION_REASON_UNKNOWN,\n          /* trackSelectionData= */ null,\n          /* mediaTimeUs= */ 0);\n      notifiedDownstreamFormat = true;\n    }\n    if (formatRequired || !readFormat) {\n      formatHolder.format = format;\n      readFormat = true;\n      return C.RESULT_FORMAT_READ;\n    } else if (!readSample) {\n      buffer.timeUs = 0;\n      buffer.ensureSpaceForWrite(sampleData.length);\n      buffer.data.put(sampleData);\n      readSample = true;\n      return C.RESULT_BUFFER_READ;\n    } else {\n      buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);\n      return C.RESULT_BUFFER_READ;\n    }\n  }\n\n  @Override\n  public void maybeThrowError() throws IOException {\n    // Do nothing.\n  }\n\n  @Override\n  public int skipData(long positionUs) {\n    return 0;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.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.testutil;\n\nimport android.util.Pair;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.ads.AdPlaybackState;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.Arrays;\n\n/**\n * Fake {@link Timeline} which can be setup to return custom {@link TimelineWindowDefinition}s.\n */\npublic final class FakeTimeline extends Timeline {\n\n  /**\n   * Definition used to define a {@link FakeTimeline}.\n   */\n  public static final class TimelineWindowDefinition {\n\n    /** Default test window duration in microseconds. */\n    public static final long DEFAULT_WINDOW_DURATION_US = 10 * C.MICROS_PER_SECOND;\n\n    public final int periodCount;\n    public final Object id;\n    public final boolean isSeekable;\n    public final boolean isDynamic;\n    public final long durationUs;\n    public final AdPlaybackState adPlaybackState;\n\n    /**\n     * Creates a seekable, non-dynamic window definition with a duration of\n     * {@link #DEFAULT_WINDOW_DURATION_US}.\n     *\n     * @param periodCount The number of periods in the window. Each period get an equal slice of the\n     *     total window duration.\n     * @param id The UID of the window.\n     */\n    public TimelineWindowDefinition(int periodCount, Object id) {\n      this(periodCount, id, true, false, DEFAULT_WINDOW_DURATION_US);\n    }\n\n    /**\n     * Creates a window definition with one period.\n     *\n     * @param isSeekable Whether the window is seekable.\n     * @param isDynamic Whether the window is dynamic.\n     * @param durationUs The duration of the window in microseconds.\n     */\n    public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) {\n      this(1, 0, isSeekable, isDynamic, durationUs);\n    }\n\n    /**\n     * Creates a window definition.\n     *\n     * @param periodCount The number of periods in the window. Each period get an equal slice of the\n     *     total window duration.\n     * @param id The UID of the window.\n     * @param isSeekable Whether the window is seekable.\n     * @param isDynamic Whether the window is dynamic.\n     * @param durationUs The duration of the window in microseconds.\n     */\n    public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable,\n        boolean isDynamic, long durationUs) {\n      this(periodCount, id, isSeekable, isDynamic, durationUs, AdPlaybackState.NONE);\n    }\n\n    /**\n     * Creates a window definition with ad groups.\n     *\n     * @param periodCount The number of periods in the window. Each period get an equal slice of the\n     *     total window duration.\n     * @param id The UID of the window.\n     * @param isSeekable Whether the window is seekable.\n     * @param isDynamic Whether the window is dynamic.\n     * @param durationUs The duration of the window in microseconds.\n     * @param adPlaybackState The ad playback state.\n     */\n    public TimelineWindowDefinition(\n        int periodCount,\n        Object id,\n        boolean isSeekable,\n        boolean isDynamic,\n        long durationUs,\n        AdPlaybackState adPlaybackState) {\n      this.periodCount = periodCount;\n      this.id = id;\n      this.isSeekable = isSeekable;\n      this.isDynamic = isDynamic;\n      this.durationUs = durationUs;\n      this.adPlaybackState = adPlaybackState;\n    }\n\n  }\n\n  private static final long AD_DURATION_US = 10 * C.MICROS_PER_SECOND;\n\n  private final TimelineWindowDefinition[] windowDefinitions;\n  private final int[] periodOffsets;\n\n  /**\n   * Returns an ad playback state with the specified number of ads in each of the specified ad\n   * groups, each ten seconds long.\n   *\n   * @param adsPerAdGroup The number of ads per ad group.\n   * @param adGroupTimesUs The times of ad groups, in microseconds.\n   * @return The ad playback state.\n   */\n  public static AdPlaybackState createAdPlaybackState(int adsPerAdGroup, long... adGroupTimesUs) {\n    int adGroupCount = adGroupTimesUs.length;\n    AdPlaybackState adPlaybackState = new AdPlaybackState(adGroupTimesUs);\n    long[][] adDurationsUs = new long[adGroupCount][];\n    for (int i = 0; i < adGroupCount; i++) {\n      adPlaybackState = adPlaybackState.withAdCount(i, adsPerAdGroup);\n      adDurationsUs[i] = new long[adsPerAdGroup];\n      Arrays.fill(adDurationsUs[i], AD_DURATION_US);\n    }\n    adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);\n    return adPlaybackState;\n  }\n\n  /**\n   * Creates a fake timeline with the given number of seekable, non-dynamic windows with one period\n   * with a duration of {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US} each.\n   *\n   * @param windowCount The number of windows.\n   */\n  public FakeTimeline(int windowCount) {\n    this(createDefaultWindowDefinitions(windowCount));\n  }\n\n  /**\n   * Creates a fake timeline with the given window definitions.\n   *\n   * @param windowDefinitions A list of {@link TimelineWindowDefinition}s.\n   */\n  public FakeTimeline(TimelineWindowDefinition... windowDefinitions) {\n    this.windowDefinitions = windowDefinitions;\n    periodOffsets = new int[windowDefinitions.length + 1];\n    periodOffsets[0] = 0;\n    for (int i = 0; i < windowDefinitions.length; i++) {\n      periodOffsets[i + 1] = periodOffsets[i] + windowDefinitions[i].periodCount;\n    }\n  }\n\n  @Override\n  public int getWindowCount() {\n    return windowDefinitions.length;\n  }\n\n  @Override\n  public Window getWindow(\n      int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {\n    TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];\n    Object tag = setTag ? windowDefinition.id : null;\n    return window.set(\n        tag,\n        /* presentationStartTimeMs= */ C.TIME_UNSET,\n        /* windowStartTimeMs= */ C.TIME_UNSET,\n        windowDefinition.isSeekable,\n        windowDefinition.isDynamic,\n        /* defaultPositionUs= */ 0,\n        windowDefinition.durationUs,\n        periodOffsets[windowIndex],\n        periodOffsets[windowIndex + 1] - 1,\n        /* positionInFirstPeriodUs= */ 0);\n  }\n\n  @Override\n  public int getPeriodCount() {\n    return periodOffsets[periodOffsets.length - 1];\n  }\n\n  @Override\n  public Period getPeriod(int periodIndex, Period period, boolean setIds) {\n    int windowIndex = Util.binarySearchFloor(periodOffsets, periodIndex, true, false);\n    int windowPeriodIndex = periodIndex - periodOffsets[windowIndex];\n    TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];\n    Object id = setIds ? windowPeriodIndex : null;\n    Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null;\n    long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount;\n    long positionInWindowUs = periodDurationUs * windowPeriodIndex;\n    return period.set(\n        id,\n        uid,\n        windowIndex,\n        periodDurationUs,\n        positionInWindowUs,\n        windowDefinition.adPlaybackState);\n  }\n\n  @Override\n  public int getIndexOfPeriod(Object uid) {\n    for (int i = 0; i < getPeriodCount(); i++) {\n      if (getUidOfPeriod(i).equals(uid)) {\n        return i;\n      }\n    }\n    return C.INDEX_UNSET;\n  }\n\n  @Override\n  public Object getUidOfPeriod(int periodIndex) {\n    Assertions.checkIndex(periodIndex, 0, getPeriodCount());\n    int windowIndex =\n        Util.binarySearchFloor(\n            periodOffsets, periodIndex, /* inclusive= */ true, /* stayInBounds= */ false);\n    int windowPeriodIndex = periodIndex - periodOffsets[windowIndex];\n    TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];\n    return Pair.create(windowDefinition.id, windowPeriodIndex);\n  }\n\n  private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) {\n    TimelineWindowDefinition[] windowDefinitions = new TimelineWindowDefinition[windowCount];\n    for (int i = 0; i < windowCount; i++) {\n      windowDefinitions[i] = new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ i);\n    }\n    return windowDefinitions;\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.extractor.ExtractorInput;\nimport com.google.android.exoplayer2.extractor.TrackOutput;\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.List;\n\n/**\n * A fake {@link TrackOutput}.\n */\npublic final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable {\n\n  private final ArrayList<Long> sampleTimesUs;\n  private final ArrayList<Integer> sampleFlags;\n  private final ArrayList<Integer> sampleStartOffsets;\n  private final ArrayList<Integer> sampleEndOffsets;\n  private final ArrayList<CryptoData> cryptoDatas;\n\n  private byte[] sampleData;\n  public Format format;\n\n  public FakeTrackOutput() {\n    sampleData = Util.EMPTY_BYTE_ARRAY;\n    sampleTimesUs = new ArrayList<>();\n    sampleFlags = new ArrayList<>();\n    sampleStartOffsets = new ArrayList<>();\n    sampleEndOffsets = new ArrayList<>();\n    cryptoDatas = new ArrayList<>();\n  }\n\n  public void clear() {\n    sampleData = Util.EMPTY_BYTE_ARRAY;\n    sampleTimesUs.clear();\n    sampleFlags.clear();\n    sampleStartOffsets.clear();\n    sampleEndOffsets.clear();\n    cryptoDatas.clear();\n  }\n\n  @Override\n  public void format(Format format) {\n    this.format = format;\n  }\n\n  @Override\n  public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)\n      throws IOException, InterruptedException {\n    byte[] newData = new byte[length];\n    int bytesAppended = input.read(newData, 0, 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    newData = Arrays.copyOf(newData, bytesAppended);\n    sampleData = TestUtil.joinByteArrays(sampleData, newData);\n    return bytesAppended;\n  }\n\n  @Override\n  public void sampleData(ParsableByteArray data, int length) {\n    byte[] newData = new byte[length];\n    data.readBytes(newData, 0, length);\n    sampleData = TestUtil.joinByteArrays(sampleData, newData);\n  }\n\n  @Override\n  public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,\n      CryptoData cryptoData) {\n    sampleTimesUs.add(timeUs);\n    sampleFlags.add(flags);\n    sampleStartOffsets.add(sampleData.length - offset - size);\n    sampleEndOffsets.add(sampleData.length - offset);\n    cryptoDatas.add(cryptoData);\n  }\n\n  public void assertSampleCount(int count) {\n    assertThat(sampleTimesUs).hasSize(count);\n  }\n\n  public void assertSample(int index, byte[] data, long timeUs, int flags, CryptoData cryptoData) {\n    byte[] actualData = getSampleData(index);\n    assertThat(actualData).isEqualTo(data);\n    assertThat(sampleTimesUs.get(index)).isEqualTo(timeUs);\n    assertThat(sampleFlags.get(index)).isEqualTo(flags);\n    assertThat(cryptoDatas.get(index)).isEqualTo(cryptoData);\n  }\n\n  public byte[] getSampleData(int index) {\n    return Arrays.copyOfRange(sampleData, sampleStartOffsets.get(index),\n        sampleEndOffsets.get(index));\n  }\n\n  public long getSampleTimeUs(int index) {\n    return sampleTimesUs.get(index);\n  }\n\n  public int getSampleFlags(int index) {\n    return sampleFlags.get(index);\n  }\n\n  public CryptoData getSampleCryptoData(int index) {\n    return cryptoDatas.get(index);\n  }\n\n  public int getSampleCount() {\n    return sampleTimesUs.size();\n  }\n\n  public List<Long> getSampleTimesUs() {\n    return Collections.unmodifiableList(sampleTimesUs);\n  }\n\n  public void assertEquals(FakeTrackOutput expected) {\n    assertThat(format).isEqualTo(expected.format);\n    assertThat(sampleTimesUs).hasSize(expected.sampleTimesUs.size());\n    assertThat(sampleData).isEqualTo(expected.sampleData);\n    for (int i = 0; i < sampleTimesUs.size(); i++) {\n      assertThat(sampleTimesUs.get(i)).isEqualTo(expected.sampleTimesUs.get(i));\n      assertThat(sampleFlags.get(i)).isEqualTo(expected.sampleFlags.get(i));\n      assertThat(sampleStartOffsets.get(i)).isEqualTo(expected.sampleStartOffsets.get(i));\n      assertThat(sampleEndOffsets.get(i)).isEqualTo(expected.sampleEndOffsets.get(i));\n      if (expected.cryptoDatas.get(i) == null) {\n        assertThat(cryptoDatas.get(i)).isNull();\n      } else {\n        assertThat(cryptoDatas.get(i)).isEqualTo(expected.cryptoDatas.get(i));\n      }\n    }\n  }\n\n  @Override\n  public void dump(Dumper dumper) {\n    dumper.startBlock(\"format\")\n        .add(\"bitrate\", format.bitrate)\n        .add(\"id\", format.id)\n        .add(\"containerMimeType\", format.containerMimeType)\n        .add(\"sampleMimeType\", format.sampleMimeType)\n        .add(\"maxInputSize\", format.maxInputSize)\n        .add(\"width\", format.width)\n        .add(\"height\", format.height)\n        .add(\"frameRate\", format.frameRate)\n        .add(\"rotationDegrees\", format.rotationDegrees)\n        .add(\"pixelWidthHeightRatio\", format.pixelWidthHeightRatio)\n        .add(\"channelCount\", format.channelCount)\n        .add(\"sampleRate\", format.sampleRate)\n        .add(\"pcmEncoding\", format.pcmEncoding)\n        .add(\"encoderDelay\", format.encoderDelay)\n        .add(\"encoderPadding\", format.encoderPadding)\n        .add(\"subsampleOffsetUs\", format.subsampleOffsetUs)\n        .add(\"selectionFlags\", format.selectionFlags)\n        .add(\"language\", format.language)\n        .add(\"drmInitData\", format.drmInitData != null ? format.drmInitData.hashCode() : \"-\");\n\n    dumper.startBlock(\"initializationData\");\n    for (int i = 0; i < format.initializationData.size(); i++) {\n      dumper.add(\"data\", format.initializationData.get(i));\n    }\n    dumper.endBlock().endBlock();\n\n    dumper.add(\"total output bytes\", sampleData.length);\n    dumper.add(\"sample count\", sampleTimesUs.size());\n\n    for (int i = 0; i < sampleTimesUs.size(); i++) {\n      dumper.startBlock(\"sample \" + i)\n          .add(\"time\", sampleTimesUs.get(i))\n          .add(\"flags\", sampleFlags.get(i))\n          .add(\"data\", getSampleData(i));\n      CryptoData cryptoData = cryptoDatas.get(i);\n      if (cryptoData != null) {\n        dumper.add(\"crypto mode\", cryptoData.cryptoMode);\n        dumper.add(\"encryption key\", cryptoData.encryptionKey);\n      }\n      dumper.endBlock();\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\nimport static org.junit.Assert.fail;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.net.wifi.WifiManager;\nimport android.net.wifi.WifiManager.WifiLock;\nimport android.os.Bundle;\nimport android.os.ConditionVariable;\nimport android.os.PowerManager;\nimport android.os.PowerManager.WakeLock;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\nimport android.view.SurfaceView;\nimport android.view.Window;\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 host activity for performing playback tests.\n */\npublic final class HostActivity extends Activity implements SurfaceHolder.Callback {\n\n  /**\n   * Interface for tests that run inside of a {@link HostActivity}.\n   */\n  public interface HostedTest {\n\n    /**\n     * Called on the main thread when the test is started.\n     * <p>\n     * The test will not be started until the {@link HostActivity} has been resumed and its\n     * {@link Surface} has been created.\n     *\n     * @param host The {@link HostActivity} in which the test is being run.\n     * @param surface The {@link Surface}.\n     */\n    void onStart(HostActivity host, Surface surface);\n\n    /**\n     * Called on the main thread to block until the test has stopped or {@link #forceStop()} is\n     * called.\n     *\n     * @param timeoutMs The maximum time to block in milliseconds.\n     * @return Whether the test has stopped successful.\n     */\n    boolean blockUntilStopped(long timeoutMs);\n\n    /**\n     * Called on the main thread to force stop the test (if it is not stopped already).\n     *\n     * @return Whether the test was forced stopped.\n     */\n    boolean forceStop();\n\n    /**\n     * Called on the test thread after the test has finished and been stopped.\n     * <p>\n     * Implementations may use this method to assert that test criteria were met.\n     */\n    void onFinished();\n\n  }\n\n  private static final String TAG = \"HostActivity\";\n  private static final String LOCK_TAG = \"ExoPlayerTestUtil:\" + TAG;\n  private static final long START_TIMEOUT_MS = 5000;\n\n  private WakeLock wakeLock;\n  private WifiLock wifiLock;\n  private SurfaceView surfaceView;\n\n  private HostedTest hostedTest;\n  private boolean hostedTestStarted;\n  private ConditionVariable hostedTestStartedCondition;\n  private boolean forcedStopped;\n\n  /**\n   * Executes a {@link HostedTest} inside the host.\n   *\n   * @param hostedTest The test to execute.\n   * @param timeoutMs The number of milliseconds to wait for the test to finish. If the timeout\n   *     is exceeded then the test will fail.\n   */\n  public void runTest(HostedTest hostedTest, long timeoutMs) {\n    runTest(hostedTest, timeoutMs, /* failOnTimeoutOrForceStop= */ true);\n  }\n\n  /**\n   * Executes a {@link HostedTest} inside the host.\n   *\n   * @param hostedTest The test to execute.\n   * @param timeoutMs The number of milliseconds to wait for the test to finish.\n   * @param failOnTimeoutOrForceStop Whether the test fails when a timeout is exceeded or the test\n   *     is stopped forcefully.\n   */\n  public void runTest(\n      final HostedTest hostedTest, long timeoutMs, boolean failOnTimeoutOrForceStop) {\n    Assertions.checkArgument(timeoutMs > 0);\n    Assertions.checkState(Thread.currentThread() != getMainLooper().getThread());\n    Assertions.checkState(this.hostedTest == null);\n    Assertions.checkNotNull(hostedTest);\n    hostedTestStartedCondition = new ConditionVariable();\n    forcedStopped = false;\n    hostedTestStarted = false;\n\n    runOnUiThread(\n        () -> {\n          HostActivity.this.hostedTest = hostedTest;\n          maybeStartHostedTest();\n        });\n\n    if (!hostedTestStartedCondition.block(START_TIMEOUT_MS)) {\n      String message =\n          \"Test failed to start. Display may be turned off or keyguard may be present.\";\n      Log.e(TAG, message);\n      if (failOnTimeoutOrForceStop) {\n        fail(message);\n      }\n    }\n\n    if (hostedTest.blockUntilStopped(timeoutMs)) {\n      if (!forcedStopped) {\n        Log.d(TAG, \"Checking test pass conditions.\");\n        hostedTest.onFinished();\n        Log.d(TAG, \"Pass conditions checked.\");\n      } else {\n        String message = \"Test force stopped. Activity may have been paused whilst \"\n            + \"test was in progress.\";\n        Log.e(TAG, message);\n        if (failOnTimeoutOrForceStop) {\n          fail(message);\n        }\n      }\n    } else {\n      runOnUiThread(hostedTest::forceStop);\n      String message = \"Test timed out after \" + timeoutMs + \" ms.\";\n      Log.e(TAG, message);\n      if (failOnTimeoutOrForceStop) {\n        fail(message);\n      }\n    }\n    this.hostedTest = null;\n  }\n\n  // Activity lifecycle\n\n  @Override\n  public void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    requestWindowFeature(Window.FEATURE_NO_TITLE);\n    setContentView(\n        getResources().getIdentifier(\"exo_testutils_host_activity\", \"layout\", getPackageName()));\n    surfaceView = findViewById(\n        getResources().getIdentifier(\"surface_view\", \"id\", getPackageName()));\n    surfaceView.getHolder().addCallback(this);\n  }\n\n  @Override\n  public void onStart() {\n    Context appContext = getApplicationContext();\n    WifiManager wifiManager = (WifiManager) appContext.getSystemService(Context.WIFI_SERVICE);\n    wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, LOCK_TAG);\n    wifiLock.acquire();\n    PowerManager powerManager = (PowerManager) appContext.getSystemService(Context.POWER_SERVICE);\n    wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOCK_TAG);\n    wakeLock.acquire();\n    super.onStart();\n  }\n\n  @Override\n  public void onPause() {\n    super.onPause();\n    if (Util.SDK_INT <= 23) {\n      maybeStopHostedTest();\n    }\n  }\n\n  @Override\n  public void onStop() {\n    super.onStop();\n    if (Util.SDK_INT > 23) {\n      maybeStopHostedTest();\n    }\n    wakeLock.release();\n    wakeLock = null;\n    wifiLock.release();\n    wifiLock = null;\n  }\n\n  // SurfaceHolder.Callback\n\n  @Override\n  public void surfaceCreated(SurfaceHolder holder) {\n    maybeStartHostedTest();\n  }\n\n  @Override\n  public void surfaceDestroyed(SurfaceHolder holder) {\n    maybeStopHostedTest();\n  }\n\n  @Override\n  public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {\n    // Do nothing.\n  }\n\n  // Internal logic\n\n  private void maybeStartHostedTest() {\n    if (hostedTest == null || hostedTestStarted) {\n      return;\n    }\n    Surface surface = surfaceView.getHolder().getSurface();\n    if (surface != null && surface.isValid()) {\n      hostedTestStarted = true;\n      Log.d(TAG, \"Starting test.\");\n      hostedTest.onStart(this, surface);\n      hostedTestStartedCondition.open();\n    }\n  }\n\n  private void maybeStopHostedTest() {\n    if (hostedTest != null && hostedTestStarted && !forcedStopped) {\n      forcedStopped = hostedTest.forceStop();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/LogcatMetricsLogger.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\nimport com.google.android.exoplayer2.util.Log;\n\n/**\n * Implementation of {@link MetricsLogger} that prints the metrics to logcat.\n */\npublic final class LogcatMetricsLogger implements MetricsLogger {\n\n  private final String tag;\n\n  public LogcatMetricsLogger(String tag) {\n    this.tag = tag;\n  }\n\n  @Override\n  public void logMetric(String key, int value) {\n    Log.d(tag, key + \": \" + value);\n  }\n\n  @Override\n  public void logMetric(String key, double value) {\n    Log.d(tag, key + \": \" + value);\n  }\n\n  @Override\n  public void logMetric(String key, String value) {\n    Log.d(tag, key + \": \" + value);\n  }\n\n  @Override\n  public void logMetric(String key, boolean value) {\n    Log.d(tag, key + \": \" + value);\n  }\n\n  @Override\n  public void close() {\n    // Do nothing.\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/MetricsLogger.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\n/**\n * Metric Logging interface for ExoPlayer playback tests.\n */\npublic interface MetricsLogger {\n\n  String KEY_FRAMES_DROPPED_COUNT = \"frames_dropped_count\";\n  String KEY_FRAMES_RENDERED_COUNT = \"frames_rendered_count\";\n  String KEY_FRAMES_SKIPPED_COUNT = \"frames_skipped_count\";\n  String KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT = \"maximum_consecutive_frames_dropped_count\";\n  String KEY_TEST_NAME = \"test_name\";\n  String KEY_IS_CDD_LIMITED_RETRY = \"is_cdd_limited_retry\";\n\n  /**\n   * Logs an int metric provided from a test.\n   *\n   * @param key The key of the metric to be logged.\n   * @param value The value of the metric to be logged.\n   */\n  void logMetric(String key, int value);\n\n  /**\n   * Logs a double metric provided from a test.\n   *\n   * @param key The key of the metric to be logged.\n   * @param value The value of the metric to be logged.\n   */\n  void logMetric(String key, double value);\n\n  /**\n   * Logs a string metric provided from a test.\n   *\n   * @param key The key of the metric to be logged.\n   * @param value The value of the metric to be logged.\n   */\n  void logMetric(String key, String value);\n\n  /**\n   * Logs a boolean metric provided from a test.\n   *\n   * @param key The key of the metric to be logged.\n   * @param value The value of the metric to be logged.\n   */\n  void logMetric(String key, boolean value);\n\n  /**\n   * Closes the logger.\n   */\n  void close();\n\n  /**\n   * A factory for instantiating {@link MetricsLogger} instances.\n   */\n  final class Factory {\n\n    private Factory() {}\n\n    /**\n     * Obtains a new instance of {@link MetricsLogger}.\n     *\n     * @param tag The tag to be used for logcat logs.\n     */\n    public static MetricsLogger createDefault(String tag) {\n      return new LogcatMetricsLogger(tag);\n    }\n  }\n\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.content.Context;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteOpenHelper;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Color;\nimport android.net.Uri;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.database.DatabaseProvider;\nimport com.google.android.exoplayer2.database.DefaultDatabaseProvider;\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.extractor.SeekMap;\nimport com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException;\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;\nimport java.io.InputStream;\nimport java.util.Arrays;\nimport java.util.Random;\n\n/**\n * Utility methods for tests.\n */\npublic class TestUtil {\n\n  private TestUtil() {}\n\n  public static boolean sniffTestData(Extractor extractor, FakeExtractorInput input)\n      throws IOException, InterruptedException {\n    while (true) {\n      try {\n        return extractor.sniff(input);\n      } catch (SimulatedIOException e) {\n        // Ignore.\n      }\n    }\n  }\n\n  public static byte[] readToEnd(DataSource dataSource) throws IOException {\n    byte[] data = new byte[1024];\n    int position = 0;\n    int bytesRead = 0;\n    while (bytesRead != C.RESULT_END_OF_INPUT) {\n      if (position == data.length) {\n        data = Arrays.copyOf(data, data.length * 2);\n      }\n      bytesRead = dataSource.read(data, position, data.length - position);\n      if (bytesRead != C.RESULT_END_OF_INPUT) {\n        position += bytesRead;\n      }\n    }\n    return Arrays.copyOf(data, position);\n  }\n\n  public static byte[] readExactly(DataSource dataSource, int length) throws IOException {\n    byte[] data = new byte[length];\n    int position = 0;\n    while (position < length) {\n      int bytesRead = dataSource.read(data, position, data.length - position);\n      if (bytesRead == C.RESULT_END_OF_INPUT) {\n        fail(\"Not enough data could be read: \" + position + \" < \" + length);\n      } else {\n        position += bytesRead;\n      }\n    }\n    return data;\n  }\n\n  public static byte[] buildTestData(int length) {\n    return buildTestData(length, length);\n  }\n\n  public static byte[] buildTestData(int length, int seed) {\n    return buildTestData(length, new Random(seed));\n  }\n\n  public static byte[] buildTestData(int length, Random random) {\n    byte[] source = new byte[length];\n    random.nextBytes(source);\n    return source;\n  }\n\n  public static String buildTestString(int maxLength, Random random) {\n    int length = random.nextInt(maxLength);\n    StringBuilder builder = new StringBuilder(length);\n    for (int i = 0; i < length; i++) {\n      builder.append((char) random.nextInt());\n    }\n    return builder.toString();\n  }\n\n  /**\n   * Converts an array of integers in the range [0, 255] into an equivalent byte array.\n   *\n   * @param intArray An array of integers, all of which must be in the range [0, 255].\n   * @return The equivalent byte array.\n   */\n  public static byte[] createByteArray(int... intArray) {\n    byte[] byteArray = new byte[intArray.length];\n    for (int i = 0; i < byteArray.length; i++) {\n      Assertions.checkState(0x00 <= intArray[i] && intArray[i] <= 0xFF);\n      byteArray[i] = (byte) intArray[i];\n    }\n    return byteArray;\n  }\n\n  public static byte[] joinByteArrays(byte[]... byteArrays) {\n    int length = 0;\n    for (byte[] byteArray : byteArrays) {\n      length += byteArray.length;\n    }\n    byte[] joined = new byte[length];\n    length = 0;\n    for (byte[] byteArray : byteArrays) {\n      System.arraycopy(byteArray, 0, joined, length, byteArray.length);\n      length += byteArray.length;\n    }\n    return joined;\n  }\n\n  public static byte[] getByteArray(Context context, String fileName) throws IOException {\n    return Util.toByteArray(getInputStream(context, fileName));\n  }\n\n  public static InputStream getInputStream(Context context, String fileName) throws IOException {\n    return context.getResources().getAssets().open(fileName);\n  }\n\n  public static String getString(Context context, String fileName) throws IOException {\n    return Util.fromUtf8Bytes(getByteArray(context, fileName));\n  }\n\n  public static Bitmap readBitmapFromFile(Context context, String fileName) throws IOException {\n    return BitmapFactory.decodeStream(getInputStream(context, fileName));\n  }\n\n  public static DatabaseProvider getTestDatabaseProvider() {\n    // Provides an in-memory database.\n    return new DefaultDatabaseProvider(\n        new SQLiteOpenHelper(\n            /* context= */ null, /* name= */ null, /* factory= */ null, /* version= */ 1) {\n          @Override\n          public void onCreate(SQLiteDatabase db) {\n            // Do nothing.\n          }\n\n          @Override\n          public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {\n            // Do nothing.\n          }\n        });\n  }\n\n  /**\n   * Asserts that data read from a {@link DataSource} matches {@code expected}.\n   *\n   * @param dataSource The {@link DataSource} through which to read.\n   * @param dataSpec The {@link DataSpec} to use when opening the {@link DataSource}.\n   * @param expectedData The expected data.\n   * @param expectKnownLength Whether to assert that {@link DataSource#open} returns the expected\n   *     data length. If false then it's asserted that {@link C#LENGTH_UNSET} is returned.\n   * @throws IOException If an error occurs reading fom the {@link DataSource}.\n   */\n  public static void assertDataSourceContent(\n      DataSource dataSource, DataSpec dataSpec, byte[] expectedData, boolean expectKnownLength)\n      throws IOException {\n    try {\n      long length = dataSource.open(dataSpec);\n      assertThat(length).isEqualTo(expectKnownLength ? expectedData.length : C.LENGTH_UNSET);\n      byte[] readData = readToEnd(dataSource);\n      assertThat(readData).isEqualTo(expectedData);\n    } finally {\n      dataSource.close();\n    }\n  }\n\n  /**\n   * Asserts whether actual bitmap is very similar to the expected bitmap at some quality level.\n   *\n   * <p>This is defined as their PSNR value is greater than or equal to the threshold. The higher\n   * the threshold, the more similar they are.\n   *\n   * @param expectedBitmap The expected bitmap.\n   * @param actualBitmap The actual bitmap.\n   * @param psnrThresholdDb The PSNR threshold (in dB), at or above which bitmaps are considered\n   *     very similar.\n   */\n  public static void assertBitmapsAreSimilar(\n      Bitmap expectedBitmap, Bitmap actualBitmap, double psnrThresholdDb) {\n    assertThat(getPsnr(expectedBitmap, actualBitmap)).isAtLeast(psnrThresholdDb);\n  }\n\n  /**\n   * Calculates the Peak-Signal-to-Noise-Ratio value for 2 bitmaps.\n   *\n   * <p>This is the logarithmic decibel(dB) value of the average mean-squared-error of normalized\n   * (0.0-1.0) R/G/B values from the two bitmaps. The higher the value, the more similar they are.\n   *\n   * @param firstBitmap The first bitmap.\n   * @param secondBitmap The second bitmap.\n   * @return The PSNR value calculated from these 2 bitmaps.\n   */\n  private static double getPsnr(Bitmap firstBitmap, Bitmap secondBitmap) {\n    assertThat(firstBitmap.getWidth()).isEqualTo(secondBitmap.getWidth());\n    assertThat(firstBitmap.getHeight()).isEqualTo(secondBitmap.getHeight());\n    long mse = 0;\n    for (int i = 0; i < firstBitmap.getWidth(); i++) {\n      for (int j = 0; j < firstBitmap.getHeight(); j++) {\n        int firstColorInt = firstBitmap.getPixel(i, j);\n        int firstRed = Color.red(firstColorInt);\n        int firstGreen = Color.green(firstColorInt);\n        int firstBlue = Color.blue(firstColorInt);\n        int secondColorInt = secondBitmap.getPixel(i, j);\n        int secondRed = Color.red(secondColorInt);\n        int secondGreen = Color.green(secondColorInt);\n        int secondBlue = Color.blue(secondColorInt);\n        mse +=\n            ((firstRed - secondRed) * (firstRed - secondRed)\n                + (firstGreen - secondGreen) * (firstGreen - secondGreen)\n                + (firstBlue - secondBlue) * (firstBlue - secondBlue));\n      }\n    }\n    double normalizedMse =\n        mse / (255.0 * 255.0 * 3.0 * firstBitmap.getWidth() * firstBitmap.getHeight());\n    return 10 * Math.log10(1.0 / normalizedMse);\n  }\n\n  /** Returns the {@link Uri} for the given asset path. */\n  public static Uri buildAssetUri(String assetPath) {\n    return Uri.parse(\"asset:///\" + assetPath);\n  }\n\n  /**\n   * Reads from the given input using the given {@link Extractor}, until it can produce the {@link\n   * SeekMap} and all of the tracks have been identified, or until the extractor encounters EOF.\n   *\n   * @param extractor The {@link Extractor} to extractor from input.\n   * @param output The {@link FakeTrackOutput} to store the extracted {@link SeekMap} and track.\n   * @param dataSource The {@link DataSource} that will be used to read from the input.\n   * @param uri The Uri of the input.\n   * @return The extracted {@link SeekMap}.\n   * @throws IOException If an error occurred reading from the input, or if the extractor finishes\n   *     reading from input without extracting any {@link SeekMap}.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  public static SeekMap extractSeekMap(\n      Extractor extractor, FakeExtractorOutput output, DataSource dataSource, Uri uri)\n      throws IOException, InterruptedException {\n    ExtractorInput input = getExtractorInputFromPosition(dataSource, /* position= */ 0, uri);\n    extractor.init(output);\n    PositionHolder positionHolder = new PositionHolder();\n    int readResult = Extractor.RESULT_CONTINUE;\n    while (true) {\n      try {\n        // Keep reading until we can get the seek map\n        while (readResult == Extractor.RESULT_CONTINUE\n            && (output.seekMap == null || !output.tracksEnded)) {\n          readResult = extractor.read(input, positionHolder);\n        }\n      } finally {\n        Util.closeQuietly(dataSource);\n      }\n\n      if (readResult == Extractor.RESULT_SEEK) {\n        input = getExtractorInputFromPosition(dataSource, positionHolder.position, uri);\n        readResult = Extractor.RESULT_CONTINUE;\n      } else if (readResult == Extractor.RESULT_END_OF_INPUT) {\n        throw new IOException(\"EOF encountered without seekmap\");\n      }\n      if (output.seekMap != null) {\n        return output.seekMap;\n      }\n    }\n  }\n\n  /**\n   * Extracts all samples from the given file into a {@link FakeTrackOutput}.\n   *\n   * @param extractor The {@link Extractor} to extractor from input.\n   * @param context A {@link Context}.\n   * @param fileName The name of the input file.\n   * @return The {@link FakeTrackOutput} containing the extracted samples.\n   * @throws IOException If an error occurred reading from the input, or if the extractor finishes\n   *     reading from input without extracting any {@link SeekMap}.\n   * @throws InterruptedException If the thread was interrupted.\n   */\n  public static FakeExtractorOutput extractAllSamplesFromFile(\n      Extractor extractor, Context context, String fileName)\n      throws IOException, InterruptedException {\n    byte[] data = TestUtil.getByteArray(context, fileName);\n    FakeExtractorOutput expectedOutput = new FakeExtractorOutput();\n    extractor.init(expectedOutput);\n    FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();\n\n    PositionHolder positionHolder = new PositionHolder();\n    int readResult = Extractor.RESULT_CONTINUE;\n    while (readResult != Extractor.RESULT_END_OF_INPUT) {\n      while (readResult == Extractor.RESULT_CONTINUE) {\n        readResult = extractor.read(input, positionHolder);\n      }\n      if (readResult == Extractor.RESULT_SEEK) {\n        input.setPosition((int) positionHolder.position);\n        readResult = Extractor.RESULT_CONTINUE;\n      }\n    }\n    return expectedOutput;\n  }\n\n  /**\n   * Seeks to the given seek time of the stream from the given input, and keeps reading from the\n   * input until we can extract at least one sample following the seek position, or until\n   * end-of-input is reached.\n   *\n   * @param extractor The {@link Extractor} to extractor from input.\n   * @param seekMap The {@link SeekMap} of the stream from the given input.\n   * @param seekTimeUs The seek time, in micro-seconds.\n   * @param trackOutput The {@link FakeTrackOutput} to store the extracted samples.\n   * @param dataSource The {@link DataSource} that will be used to read from the input.\n   * @param uri The Uri of the input.\n   * @return The index of the first extracted sample written to the given {@code trackOutput} after\n   *     the seek is completed, or -1 if the seek is completed without any extracted sample.\n   */\n  public static int seekToTimeUs(\n      Extractor extractor,\n      SeekMap seekMap,\n      long seekTimeUs,\n      DataSource dataSource,\n      FakeTrackOutput trackOutput,\n      Uri uri)\n      throws IOException, InterruptedException {\n    int numSampleBeforeSeek = trackOutput.getSampleCount();\n    SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs);\n\n    long initialSeekLoadPosition = seekPoints.first.position;\n    extractor.seek(initialSeekLoadPosition, seekTimeUs);\n\n    PositionHolder positionHolder = new PositionHolder();\n    positionHolder.position = C.POSITION_UNSET;\n    ExtractorInput extractorInput =\n        TestUtil.getExtractorInputFromPosition(dataSource, initialSeekLoadPosition, uri);\n    int extractorReadResult = Extractor.RESULT_CONTINUE;\n    while (true) {\n      try {\n        // Keep reading until we can read at least one sample after seek\n        while (extractorReadResult == Extractor.RESULT_CONTINUE\n            && trackOutput.getSampleCount() == numSampleBeforeSeek) {\n          extractorReadResult = extractor.read(extractorInput, positionHolder);\n        }\n      } finally {\n        Util.closeQuietly(dataSource);\n      }\n\n      if (extractorReadResult == Extractor.RESULT_SEEK) {\n        extractorInput =\n            TestUtil.getExtractorInputFromPosition(dataSource, positionHolder.position, uri);\n        extractorReadResult = Extractor.RESULT_CONTINUE;\n      } else if (extractorReadResult == Extractor.RESULT_END_OF_INPUT) {\n        return -1;\n      } else if (trackOutput.getSampleCount() > numSampleBeforeSeek) {\n        // First index after seek = num sample before seek.\n        return numSampleBeforeSeek;\n      }\n    }\n  }\n\n  /** Returns an {@link ExtractorInput} to read from the given input at given position. */\n  public static ExtractorInput getExtractorInputFromPosition(\n      DataSource dataSource, long position, Uri uri) throws IOException {\n    DataSpec dataSpec = new DataSpec(uri, position, C.LENGTH_UNSET, /* key= */ null);\n    long length = dataSource.open(dataSpec);\n    if (length != C.LENGTH_UNSET) {\n      length += position;\n    }\n    return new DefaultExtractorInput(dataSource, position, length);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/main/res/layout/exo_testutils_host_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- Copyright (C) 2016 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS 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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/root\"\n    android:focusable=\"true\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:keepScreenOn=\"true\">\n\n  <SurfaceView android:id=\"@+id/surface_view\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"\n      android:layout_gravity=\"center\"/>\n\n</FrameLayout>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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-->\n\n<manifest package=\"com.google.android.exoplayer2.testutil.test\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSetTest.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.testutil.FakeDataSet.FakeData;\nimport com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport java.util.List;\nimport java.util.Random;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link FakeAdaptiveDataSet}. */\n@RunWith(AndroidJUnit4.class)\npublic final class FakeAdaptiveDataSetTest {\n\n  private static final Format[] TEST_FORMATS = {\n    Format.createVideoSampleFormat(\n        null,\n        MimeTypes.VIDEO_H264,\n        null,\n        1000000,\n        Format.NO_VALUE,\n        1280,\n        720,\n        Format.NO_VALUE,\n        null,\n        null),\n    Format.createVideoSampleFormat(\n        null,\n        MimeTypes.VIDEO_H264,\n        null,\n        300000,\n        Format.NO_VALUE,\n        640,\n        360,\n        Format.NO_VALUE,\n        null,\n        null)\n  };\n  private static final TrackGroup TRACK_GROUP = new TrackGroup(TEST_FORMATS);\n\n  @Test\n  public void testAdaptiveDataSet() {\n    long chunkDuration = 2 * C.MICROS_PER_SECOND;\n    FakeAdaptiveDataSet dataSet =\n        new FakeAdaptiveDataSet(\n            TRACK_GROUP, 10 * C.MICROS_PER_SECOND, chunkDuration, 0.0, new Random(0));\n    assertThat(dataSet.getAllData().size()).isEqualTo(TEST_FORMATS.length);\n    assertThat(dataSet.getUri(0).equals(dataSet.getUri(1))).isFalse();\n    assertThat(dataSet.getChunkCount()).isEqualTo(5);\n    assertThat(dataSet.getChunkIndexByPosition(4 * C.MICROS_PER_SECOND)).isEqualTo(2);\n    assertThat(dataSet.getChunkIndexByPosition(9 * C.MICROS_PER_SECOND)).isEqualTo(4);\n    for (int i = 0; i < dataSet.getChunkCount(); i++) {\n      assertThat(dataSet.getChunkDuration(i)).isEqualTo(chunkDuration);\n    }\n    assertChunkData(dataSet, chunkDuration);\n  }\n\n  @Test\n  public void testAdaptiveDataSetTrailingSmallChunk() {\n    long chunkDuration = 3 * C.MICROS_PER_SECOND;\n    FakeAdaptiveDataSet dataSet =\n        new FakeAdaptiveDataSet(\n            TRACK_GROUP, 10 * C.MICROS_PER_SECOND, chunkDuration, 0.0, new Random(0));\n    assertThat(dataSet.getAllData().size()).isEqualTo(TEST_FORMATS.length);\n    assertThat(dataSet.getUri(0).equals(dataSet.getUri(1))).isFalse();\n    assertThat(dataSet.getChunkCount()).isEqualTo(4);\n    assertThat(dataSet.getChunkIndexByPosition(4 * C.MICROS_PER_SECOND)).isEqualTo(1);\n    assertThat(dataSet.getChunkIndexByPosition(9 * C.MICROS_PER_SECOND)).isEqualTo(3);\n    for (int i = 0; i < dataSet.getChunkCount() - 1; i++) {\n      assertThat(dataSet.getChunkDuration(i)).isEqualTo(chunkDuration);\n    }\n    assertThat(dataSet.getChunkDuration(3)).isEqualTo(1 * C.MICROS_PER_SECOND);\n    assertChunkData(dataSet, chunkDuration);\n  }\n\n  @Test\n  public void testAdaptiveDataSetChunkSizeDistribution() {\n    double expectedStdDev = 4.0;\n    FakeAdaptiveDataSet dataSet =\n        new FakeAdaptiveDataSet(\n            TRACK_GROUP,\n            100000 * C.MICROS_PER_SECOND,\n            1 * C.MICROS_PER_SECOND,\n            expectedStdDev,\n            new Random(0));\n    for (int i = 0; i < TEST_FORMATS.length; i++) {\n      FakeData data = dataSet.getData(dataSet.getUri(i));\n      double mean = computeSegmentSizeMean(data.getSegments());\n      double stddev = computeSegmentSizeStdDev(data.getSegments(), mean);\n      double relativePercentStdDev = stddev / mean * 100.0;\n      assertThat(relativePercentStdDev).isWithin(0.02).of(expectedStdDev);\n      assertThat(mean * 8 / TEST_FORMATS[i].bitrate).isWithin(0.01).of(1.0);\n    }\n  }\n\n  private void assertChunkData(FakeAdaptiveDataSet dataSet, long chunkDuration) {\n    for (int i = 0; i < dataSet.getChunkCount(); i++) {\n      assertThat(dataSet.getStartTime(i)).isEqualTo(chunkDuration * i);\n    }\n    for (int s = 0; s < TEST_FORMATS.length; s++) {\n      FakeData data = dataSet.getData(dataSet.getUri(s));\n      assertThat(data.getSegments().size()).isEqualTo(dataSet.getChunkCount());\n      for (int i = 0; i < data.getSegments().size(); i++) {\n        long expectedLength =\n            TEST_FORMATS[s].bitrate * dataSet.getChunkDuration(i) / (8 * C.MICROS_PER_SECOND);\n        assertThat(data.getSegments().get(i).length).isEqualTo(expectedLength);\n      }\n    }\n  }\n\n  private static double computeSegmentSizeMean(List<Segment> segments) {\n    double totalSize = 0.0;\n    for (Segment segment : segments) {\n      totalSize += segment.length;\n    }\n    return totalSize / segments.size();\n  }\n\n  private static double computeSegmentSizeStdDev(List<Segment> segments, double mean) {\n    double totalSquaredSize = 0.0;\n    for (Segment segment : segments) {\n      totalSquaredSize += (double) segment.length * segment.length;\n    }\n    return Math.sqrt(totalSquaredSize / segments.size() - mean * mean);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.os.ConditionVariable;\nimport android.os.HandlerThread;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.google.android.exoplayer2.util.HandlerWrapper;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.TimeUnit;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.robolectric.annotation.Config;\n\n/** Unit test for {@link FakeClock}. */\n@RunWith(AndroidJUnit4.class)\n@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})\npublic final class FakeClockTest {\n\n  private static final long TIMEOUT_MS = 10000;\n\n  @Test\n  public void testAdvanceTime() {\n    FakeClock fakeClock = new FakeClock(2000);\n    assertThat(fakeClock.elapsedRealtime()).isEqualTo(2000);\n    fakeClock.advanceTime(500);\n    assertThat(fakeClock.elapsedRealtime()).isEqualTo(2500);\n    fakeClock.advanceTime(0);\n    assertThat(fakeClock.elapsedRealtime()).isEqualTo(2500);\n  }\n\n  @Test\n  public void testSleep() throws InterruptedException {\n    FakeClock fakeClock = new FakeClock(0);\n    SleeperThread sleeperThread = new SleeperThread(fakeClock, 1000);\n    sleeperThread.start();\n    assertThat(sleeperThread.waitUntilAsleep(TIMEOUT_MS)).isTrue();\n    assertThat(sleeperThread.isSleeping()).isTrue();\n    fakeClock.advanceTime(1000);\n    sleeperThread.join(TIMEOUT_MS);\n    assertThat(sleeperThread.isSleeping()).isFalse();\n\n    sleeperThread = new SleeperThread(fakeClock, 0);\n    sleeperThread.start();\n    sleeperThread.join();\n    assertThat(sleeperThread.isSleeping()).isFalse();\n\n    SleeperThread[] sleeperThreads = new SleeperThread[5];\n    sleeperThreads[0] = new SleeperThread(fakeClock, 1000);\n    sleeperThreads[1] = new SleeperThread(fakeClock, 1000);\n    sleeperThreads[2] = new SleeperThread(fakeClock, 2000);\n    sleeperThreads[3] = new SleeperThread(fakeClock, 3000);\n    sleeperThreads[4] = new SleeperThread(fakeClock, 4000);\n    for (SleeperThread thread : sleeperThreads) {\n      thread.start();\n      assertThat(thread.waitUntilAsleep(TIMEOUT_MS)).isTrue();\n    }\n    assertSleepingStates(new boolean[] {true, true, true, true, true}, sleeperThreads);\n    fakeClock.advanceTime(1500);\n    assertThat(sleeperThreads[0].waitUntilAwake(TIMEOUT_MS)).isTrue();\n    assertThat(sleeperThreads[1].waitUntilAwake(TIMEOUT_MS)).isTrue();\n    assertSleepingStates(new boolean[] {false, false, true, true, true}, sleeperThreads);\n    fakeClock.advanceTime(2000);\n    assertThat(sleeperThreads[2].waitUntilAwake(TIMEOUT_MS)).isTrue();\n    assertThat(sleeperThreads[3].waitUntilAwake(TIMEOUT_MS)).isTrue();\n    assertSleepingStates(new boolean[] {false, false, false, false, true}, sleeperThreads);\n    fakeClock.advanceTime(2000);\n    for (SleeperThread thread : sleeperThreads) {\n      thread.join(TIMEOUT_MS);\n    }\n    assertSleepingStates(new boolean[] {false, false, false, false, false}, sleeperThreads);\n  }\n\n  @Test\n  public void testPostDelayed() {\n    HandlerThread handlerThread = new HandlerThread(\"FakeClockTest thread\");\n    handlerThread.start();\n    FakeClock fakeClock = new FakeClock(0);\n    HandlerWrapper handler =\n        fakeClock.createHandler(handlerThread.getLooper(), /* callback= */ null);\n\n    TestRunnable[] testRunnables = {\n      new TestRunnable(),\n      new TestRunnable(),\n      new TestRunnable(),\n      new TestRunnable(),\n      new TestRunnable()\n    };\n    handler.postDelayed(testRunnables[0], 0);\n    handler.postDelayed(testRunnables[1], 100);\n    handler.postDelayed(testRunnables[2], 200);\n    waitForHandler(handler);\n    assertTestRunnableStates(new boolean[] {true, false, false, false, false}, testRunnables);\n\n    fakeClock.advanceTime(150);\n    handler.postDelayed(testRunnables[3], 50);\n    handler.postDelayed(testRunnables[4], 100);\n    waitForHandler(handler);\n    assertTestRunnableStates(new boolean[] {true, true, false, false, false}, testRunnables);\n\n    fakeClock.advanceTime(50);\n    waitForHandler(handler);\n    assertTestRunnableStates(new boolean[] {true, true, true, true, false}, testRunnables);\n\n    fakeClock.advanceTime(1000);\n    waitForHandler(handler);\n    assertTestRunnableStates(new boolean[] {true, true, true, true, true}, testRunnables);\n  }\n\n  private static void assertSleepingStates(boolean[] states, SleeperThread[] sleeperThreads) {\n    for (int i = 0; i < sleeperThreads.length; i++) {\n      assertThat(sleeperThreads[i].isSleeping()).isEqualTo(states[i]);\n    }\n  }\n\n  private static void waitForHandler(HandlerWrapper handler) {\n    final ConditionVariable handlerFinished = new ConditionVariable();\n    handler.post(handlerFinished::open);\n    handlerFinished.block();\n  }\n\n  private static void assertTestRunnableStates(boolean[] states, TestRunnable[] testRunnables) {\n    for (int i = 0; i < testRunnables.length; i++) {\n      assertThat(testRunnables[i].hasRun).isEqualTo(states[i]);\n    }\n  }\n\n  private static final class SleeperThread extends Thread {\n\n    private final Clock clock;\n    private final long sleepDurationMs;\n    private final CountDownLatch fallAsleepCountDownLatch;\n    private final CountDownLatch wakeUpCountDownLatch;\n\n    private volatile boolean isSleeping;\n\n    public SleeperThread(Clock clock, long sleepDurationMs) {\n      this.clock = clock;\n      this.sleepDurationMs = sleepDurationMs;\n      this.fallAsleepCountDownLatch = new CountDownLatch(1);\n      this.wakeUpCountDownLatch = new CountDownLatch(1);\n    }\n\n    public boolean waitUntilAsleep(long timeoutMs) throws InterruptedException {\n      return fallAsleepCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS);\n    }\n\n    public boolean waitUntilAwake(long timeoutMs) throws InterruptedException {\n      return wakeUpCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS);\n    }\n\n    public boolean isSleeping() {\n      return isSleeping;\n    }\n\n    @Override\n    public void run() {\n      // This relies on the FakeClock's methods synchronizing on its own monitor to ensure that\n      // any interactions with it occur only after sleep() has called wait() or returned.\n      synchronized (clock) {\n        isSleeping = true;\n        fallAsleepCountDownLatch.countDown();\n        clock.sleep(sleepDurationMs);\n        isSleeping = false;\n        wakeUpCountDownLatch.countDown();\n      }\n    }\n  }\n\n  private static final class TestRunnable implements Runnable {\n\n    public boolean hasRun;\n\n    @Override\n    public void run() {\n      hasRun = true;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSetTest.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment;\nimport java.io.IOException;\nimport java.util.List;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link FakeDataSet} */\n@RunWith(AndroidJUnit4.class)\npublic final class FakeDataSetTest {\n\n  @Test\n  public void testMultipleDataSets() {\n    byte[][] testData = new byte[4][];\n    Uri[] uris = new Uri[3];\n    for (int i = 0; i < 4; i++) {\n      testData[i] = TestUtil.buildTestData(10, i);\n      if (i > 0) {\n        uris[i - 1] = Uri.parse(\"test_uri_\" + i);\n      }\n    }\n    FakeDataSet fakeDataSet =\n        new FakeDataSet()\n            .newDefaultData()\n            .appendReadData(testData[0])\n            .endData()\n            .setData(uris[0], testData[1])\n            .newData(uris[1])\n            .appendReadData(testData[2])\n            .endData()\n            .setData(uris[2], testData[3]);\n\n    assertThat(fakeDataSet.getAllData().size()).isEqualTo(4);\n    assertThat(fakeDataSet.getData(\"unseen_uri\")).isEqualTo(fakeDataSet.getData((Uri) null));\n    for (int i = 0; i < 3; i++) {\n      assertThat(fakeDataSet.getData(uris[i]).uri).isEqualTo(uris[i]);\n    }\n    assertThat(fakeDataSet.getData((Uri) null).getData()).isEqualTo(testData[0]);\n    for (int i = 1; i < 4; i++) {\n      assertThat(fakeDataSet.getData(uris[i - 1]).getData()).isEqualTo(testData[i]);\n    }\n  }\n\n  @Test\n  public void testSegmentTypes() {\n    byte[] testData = TestUtil.buildTestData(3);\n    Runnable runnable =\n        () -> {\n          // Do nothing.\n        };\n    IOException exception = new IOException();\n    FakeDataSet fakeDataSet =\n        new FakeDataSet()\n            .newDefaultData()\n            .appendReadData(testData)\n            .appendReadData(testData)\n            .appendReadData(50)\n            .appendReadAction(runnable)\n            .appendReadError(exception)\n            .endData();\n\n    List<Segment> segments = fakeDataSet.getData((Uri) null).getSegments();\n    assertThat(segments.size()).isEqualTo(5);\n    assertSegment(segments.get(0), testData, 3, 0, null, null);\n    assertSegment(segments.get(1), testData, 3, 3, null, null);\n    assertSegment(segments.get(2), null, 50, 6, null, null);\n    assertSegment(segments.get(3), null, 0, 56, runnable, null);\n    assertSegment(segments.get(4), null, 0, 56, null, exception);\n\n    byte[] allData = new byte[6];\n    System.arraycopy(testData, 0, allData, 0, 3);\n    System.arraycopy(testData, 0, allData, 3, 3);\n    assertThat(fakeDataSet.getData((Uri) null).getData()).isEqualTo(allData);\n  }\n\n  private static void assertSegment(\n      Segment segment,\n      byte[] data,\n      int length,\n      long byteOffset,\n      Runnable runnable,\n      IOException exception) {\n    if (data != null) {\n      assertThat(segment.data).isEqualTo(data);\n      assertThat(data).hasLength(length);\n    } else {\n      assertThat(segment.data).isNull();\n    }\n    assertThat(segment.length).isEqualTo(length);\n    assertThat(segment.byteOffset).isEqualTo(byteOffset);\n    assertThat(segment.action).isEqualTo(runnable);\n    assertThat(segment.isActionSegment()).isEqualTo(runnable != null);\n    assertThat(segment.exception).isEqualTo(exception);\n    assertThat(segment.isErrorSegment()).isEqualTo(exception != null);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSourceTest.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.net.Uri;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/** Unit test for {@link FakeDataSource}. */\n@RunWith(AndroidJUnit4.class)\npublic final class FakeDataSourceTest {\n\n  private static final String URI_STRING = \"test://test.test\";\n  private static final byte[] BUFFER = new byte[500];\n  private static final byte[] TEST_DATA = TestUtil.buildTestData(15);\n  private static final byte[] TEST_DATA_PART_1 = Arrays.copyOf(TEST_DATA, 10);\n  private static final byte[] TEST_DATA_PART_2 = Arrays.copyOfRange(TEST_DATA, 10, 15);\n\n  private static Uri uri;\n  private static FakeDataSet fakeDataSet;\n\n  @Before\n  public void setUp() {\n    uri = Uri.parse(URI_STRING);\n    fakeDataSet =\n        new FakeDataSet()\n            .newData(uri.toString())\n            .appendReadData(TEST_DATA_PART_1)\n            .appendReadData(TEST_DATA_PART_2)\n            .endData();\n  }\n\n  @Test\n  public void testReadFull() throws IOException {\n    FakeDataSource dataSource = new FakeDataSource(fakeDataSet);\n    assertThat(dataSource.open(new DataSpec(uri))).isEqualTo(15);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(10);\n    assertBuffer(TEST_DATA_PART_1);\n    assertThat(dataSource.read(BUFFER, 10, BUFFER.length)).isEqualTo(5);\n    assertBuffer(TEST_DATA);\n    assertThat(dataSource.read(BUFFER, 15, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);\n    assertBuffer(TEST_DATA);\n    assertThat(dataSource.read(BUFFER, 20, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);\n    dataSource.close();\n  }\n\n  @Test\n  public void testReadPartialOpenEnded() throws IOException {\n    FakeDataSource dataSource = new FakeDataSource(fakeDataSet);\n    assertThat(dataSource.open(new DataSpec(uri, 7, C.LENGTH_UNSET, null))).isEqualTo(8);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(3);\n    assertBuffer(TEST_DATA_PART_1, 7, 3);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(5);\n    assertBuffer(TEST_DATA_PART_2);\n    assertThat(dataSource.read(BUFFER, 15, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);\n    dataSource.close();\n  }\n\n  @Test\n  public void testReadPartialBounded() throws IOException {\n    FakeDataSource dataSource = new FakeDataSource(fakeDataSet);\n    assertThat(dataSource.open(new DataSpec(uri, 9, 3, null))).isEqualTo(3);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(1);\n    assertBuffer(TEST_DATA_PART_1, 9, 1);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(2);\n    assertBuffer(TEST_DATA_PART_2, 0, 2);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);\n    dataSource.close();\n\n    assertThat(dataSource.open(new DataSpec(uri, 11, 4, null))).isEqualTo(4);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(4);\n    assertBuffer(TEST_DATA_PART_2, 1, 4);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);\n    dataSource.close();\n  }\n\n  @Test\n  public void testDummyData() throws IOException {\n    FakeDataSource dataSource =\n        new FakeDataSource(\n            new FakeDataSet()\n                .newData(uri.toString())\n                .appendReadData(100)\n                .appendReadData(TEST_DATA)\n                .appendReadData(200)\n                .endData());\n    assertThat(dataSource.open(new DataSpec(uri))).isEqualTo(315);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(100);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15);\n    assertBuffer(TEST_DATA);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(200);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);\n    dataSource.close();\n  }\n\n  @Test\n  public void testException() throws IOException {\n    String errorMessage = \"error, error, error\";\n    IOException exception = new IOException(errorMessage);\n    FakeDataSource dataSource =\n        new FakeDataSource(\n            new FakeDataSet()\n                .newData(uri.toString())\n                .appendReadData(TEST_DATA)\n                .appendReadError(exception)\n                .appendReadData(TEST_DATA)\n                .endData());\n    assertThat(dataSource.open(new DataSpec(uri))).isEqualTo(30);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15);\n    assertBuffer(TEST_DATA);\n    try {\n      dataSource.read(BUFFER, 0, BUFFER.length);\n      fail(\"IOException expected.\");\n    } catch (IOException e) {\n      assertThat(e).hasMessageThat().isEqualTo(errorMessage);\n    }\n    try {\n      dataSource.read(BUFFER, 0, BUFFER.length);\n      fail(\"IOException expected.\");\n    } catch (IOException e) {\n      assertThat(e).hasMessageThat().isEqualTo(errorMessage);\n    }\n    dataSource.close();\n    assertThat(dataSource.open(new DataSpec(uri, 15, 15, null))).isEqualTo(15);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15);\n    assertBuffer(TEST_DATA);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);\n    dataSource.close();\n  }\n\n  @Test\n  public void testRunnable() throws IOException {\n    TestRunnable[] runnables = new TestRunnable[3];\n    for (int i = 0; i < 3; i++) {\n      runnables[i] = new TestRunnable();\n    }\n    FakeDataSource dataSource =\n        new FakeDataSource(\n            new FakeDataSet()\n                .newData(uri.toString())\n                .appendReadData(TEST_DATA)\n                .appendReadAction(runnables[0])\n                .appendReadData(TEST_DATA)\n                .appendReadAction(runnables[1])\n                .appendReadAction(runnables[2])\n                .appendReadData(TEST_DATA)\n                .endData());\n    assertThat(dataSource.open(new DataSpec(uri))).isEqualTo(45);\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15);\n    assertBuffer(TEST_DATA);\n    for (int i = 0; i < 3; i++) {\n      assertThat(runnables[i].ran).isFalse();\n    }\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15);\n    assertBuffer(TEST_DATA);\n    assertThat(runnables[0].ran).isTrue();\n    assertThat(runnables[1].ran).isFalse();\n    assertThat(runnables[2].ran).isFalse();\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15);\n    assertBuffer(TEST_DATA);\n    for (int i = 0; i < 3; i++) {\n      assertThat(runnables[i].ran).isTrue();\n    }\n    assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT);\n    dataSource.close();\n  }\n\n  @Test\n  public void testOpenSourceFailures() throws IOException {\n    // Empty data.\n    FakeDataSource dataSource =\n        new FakeDataSource(new FakeDataSet().newData(uri.toString()).endData());\n    try {\n      dataSource.open(new DataSpec(uri));\n      fail(\"IOException expected.\");\n    } catch (IOException e) {\n      // Expected.\n    } finally {\n      dataSource.close();\n    }\n\n    // Non-existent data\n    dataSource = new FakeDataSource(new FakeDataSet());\n    try {\n      dataSource.open(new DataSpec(uri));\n      fail(\"IOException expected.\");\n    } catch (IOException e) {\n      // Expected.\n    } finally {\n      dataSource.close();\n    }\n\n    // DataSpec out of bounds.\n    dataSource =\n        new FakeDataSource(\n            new FakeDataSet()\n                .newDefaultData()\n                .appendReadData(TestUtil.buildTestData(10))\n                .endData());\n    try {\n      dataSource.open(new DataSpec(uri, 5, 10, null));\n      fail(\"IOException expected.\");\n    } catch (IOException e) {\n      // Expected.\n    } finally {\n      dataSource.close();\n    }\n  }\n\n  private static void assertBuffer(byte[] expected) {\n    assertBuffer(expected, 0, expected.length);\n  }\n\n  private static void assertBuffer(byte[] expected, int expectedStart, int expectedLength) {\n    for (int i = 0; i < expectedLength; i++) {\n      assertThat(BUFFER[i]).isEqualTo(expected[i + expectedStart]);\n    }\n  }\n\n  private static final class TestRunnable implements Runnable {\n\n    public boolean ran;\n\n    @Override\n    public void run() {\n      ran = true;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/build.gradle",
    "content": "// 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.\napply from: '../constants.gradle'\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion project.ext.compileSdkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    defaultConfig {\n        minSdkVersion project.ext.minSdkVersion\n        targetSdkVersion project.ext.targetSdkVersion\n    }\n\n    lintOptions {\n        // Robolectric depends on BouncyCastle, which depends on javax.naming,\n        // which is not part of Android.\n        disable 'InvalidPackage'\n    }\n\n    testOptions.unitTests.includeAndroidResources = true\n}\n\ndependencies {\n    api 'androidx.test:core:' + androidXTestVersion\n    api 'org.robolectric:robolectric:' + robolectricVersion\n    api project(modulePrefix + 'testutils')\n    implementation project(modulePrefix + 'library-core')\n    implementation 'androidx.annotation:annotation:1.1.0'\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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\n<manifest package=\"com.google.android.exoplayer2.testutil\"/>\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static com.google.common.truth.Truth.assertWithMessage;\n\nimport android.net.Uri;\nimport com.google.android.exoplayer2.testutil.FakeDataSet.FakeData;\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.DummyDataSource;\nimport com.google.android.exoplayer2.upstream.cache.Cache;\nimport com.google.android.exoplayer2.upstream.cache.CacheDataSource;\nimport com.google.android.exoplayer2.util.Util;\nimport java.io.IOException;\nimport java.util.ArrayList;\n\n/** Assertion methods for {@link Cache}. */\npublic final class CacheAsserts {\n\n  /** Defines a set of data requests. */\n  public static final class RequestSet {\n\n    private final FakeDataSet fakeDataSet;\n    private DataSpec[] dataSpecs;\n\n    public RequestSet(FakeDataSet fakeDataSet) {\n      this.fakeDataSet = fakeDataSet;\n      ArrayList<FakeData> allData = fakeDataSet.getAllData();\n      dataSpecs = new DataSpec[allData.size()];\n      for (int i = 0; i < dataSpecs.length; i++) {\n        dataSpecs[i] = new DataSpec(allData.get(i).uri);\n      }\n    }\n\n    public RequestSet subset(String... uriStrings) {\n      dataSpecs = new DataSpec[uriStrings.length];\n      for (int i = 0; i < dataSpecs.length; i++) {\n        dataSpecs[i] = new DataSpec(Uri.parse(uriStrings[i]));\n      }\n      return this;\n    }\n\n    public RequestSet subset(Uri... uris) {\n      dataSpecs = new DataSpec[uris.length];\n      for (int i = 0; i < dataSpecs.length; i++) {\n        dataSpecs[i] = new DataSpec(uris[i]);\n      }\n      return this;\n    }\n\n    public RequestSet subset(DataSpec... dataSpecs) {\n      this.dataSpecs = dataSpecs;\n      return this;\n    }\n\n    public int getCount() {\n      return dataSpecs.length;\n    }\n\n    public byte[] getData(int i) {\n      return fakeDataSet.getData(dataSpecs[i].uri).getData();\n    }\n\n    public DataSpec getDataSpec(int i) {\n      return dataSpecs[i];\n    }\n\n    public RequestSet useBoundedDataSpecFor(String uriString) {\n      FakeData data = fakeDataSet.getData(uriString);\n      for (int i = 0; i < dataSpecs.length; i++) {\n        DataSpec spec = dataSpecs[i];\n        if (spec.uri.getPath().equals(uriString)) {\n          dataSpecs[i] = spec.subrange(0, data.getData().length);\n          return this;\n        }\n      }\n      throw new IllegalStateException();\n    }\n  }\n\n  /**\n   * Asserts that the cache contains necessary data for the {@code requestSet}.\n   *\n   * @throws IOException If an error occurred reading from the Cache.\n   */\n  public static void assertCachedData(Cache cache, RequestSet requestSet) throws IOException {\n    int totalLength = 0;\n    for (int i = 0; i < requestSet.getCount(); i++) {\n      byte[] data = requestSet.getData(i);\n      assertDataCached(cache, requestSet.getDataSpec(i), data);\n      totalLength += data.length;\n    }\n    assertThat(cache.getCacheSpace()).isEqualTo(totalLength);\n  }\n\n  /**\n   * Asserts that the cache content is equal to the data in the {@code fakeDataSet}.\n   *\n   * @throws IOException If an error occurred reading from the Cache.\n   */\n  public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException {\n    assertCachedData(cache, new RequestSet(fakeDataSet));\n  }\n\n  /**\n   * Asserts that the cache contains the given data for {@code dataSpec}.\n   *\n   * @throws IOException If an error occurred reading from the Cache.\n   */\n  public static void assertDataCached(Cache cache, DataSpec dataSpec, byte[] expected)\n      throws IOException {\n    DataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0);\n    byte[] bytes;\n    try {\n      dataSource.open(dataSpec);\n      bytes = TestUtil.readToEnd(dataSource);\n    } catch (IOException e) {\n      throw new IOException(\"Opening/reading cache failed: \" + dataSpec, e);\n    } finally {\n      dataSource.close();\n    }\n    assertWithMessage(\"Cached data doesn't match expected for '\" + dataSpec.uri + \"',\")\n        .that(bytes)\n        .isEqualTo(expected);\n  }\n\n  /**\n   * Asserts that the read data from {@code dataSource} specified by {@code dataSpec} is equal to\n   * {@code expected} or not.\n   *\n   * @throws IOException If an error occurred reading from the Cache.\n   */\n  public static void assertReadData(DataSource dataSource, DataSpec dataSpec, byte[] expected)\n      throws IOException {\n    DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);\n    byte[] bytes = null;\n    try {\n      bytes = Util.toByteArray(inputStream);\n    } catch (IOException e) {\n      // Ignore\n    } finally {\n      inputStream.close();\n    }\n    assertThat(bytes).isEqualTo(expected);\n  }\n\n  /** Asserts that the cache is empty. */\n  public static void assertCacheEmpty(Cache cache) {\n    assertThat(cache.getCacheSpace()).isEqualTo(0);\n    assertThat(cache.getKeys()).isEmpty();\n  }\n\n  private CacheAsserts() {}\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.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.testutil;\n\nimport static com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;\nimport static com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON;\nimport static com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER;\nimport static com.google.common.truth.Truth.assertThat;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport androidx.test.core.app.ApplicationProvider;\nimport com.google.android.exoplayer2.DefaultRenderersFactory;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.video.VideoRendererEventListener;\nimport java.util.List;\n\n/** Assertions for {@link DefaultRenderersFactory}. */\npublic final class DefaultRenderersFactoryAsserts {\n\n  /**\n   * Asserts that an extension renderer of type {@code clazz} is not instantiated for {@link\n   * DefaultRenderersFactory#EXTENSION_RENDERER_MODE_OFF}, and that it's instantiated in the correct\n   * position relative to other renderers of the same type for {@link\n   * DefaultRenderersFactory#EXTENSION_RENDERER_MODE_ON} and {@link\n   * DefaultRenderersFactory#EXTENSION_RENDERER_MODE_PREFER}, assuming no other extension renderers\n   * can be loaded.\n   *\n   * @param clazz The extension renderer class.\n   * @param type The type of the renderer.\n   */\n  public static void assertExtensionRendererCreated(Class<? extends Renderer> clazz, int type) {\n    // In EXTENSION_RENDERER_MODE_OFF the renderer should not be created.\n    Renderer[] renderers = createRenderers(EXTENSION_RENDERER_MODE_OFF);\n    for (Renderer renderer : renderers) {\n      assertThat(renderer).isNotInstanceOf(clazz);\n    }\n\n    // In EXTENSION_RENDERER_MODE_ON the renderer should be created and last of its type.\n    renderers = createRenderers(EXTENSION_RENDERER_MODE_ON);\n    boolean found = false;\n    for (Renderer renderer : renderers) {\n      if (!found) {\n        if (clazz.isInstance(renderer)) {\n          found = true;\n        }\n      } else {\n        assertThat(renderer.getTrackType()).isNotEqualTo(type);\n      }\n    }\n    assertThat(found).isTrue();\n\n    // In EXTENSION_RENDERER_MODE_PREFER the renderer should be created and first of its type.\n    renderers = createRenderers(EXTENSION_RENDERER_MODE_PREFER);\n    found = false;\n    for (Renderer renderer : renderers) {\n      if (!found) {\n        if (clazz.isInstance(renderer)) {\n          found = true;\n        } else {\n          assertThat(renderer.getTrackType()).isNotEqualTo(type);\n        }\n      } else {\n        assertThat(renderer).isNotInstanceOf(clazz);\n      }\n    }\n    assertThat(found).isTrue();\n  }\n\n  private static Renderer[] createRenderers(\n      @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {\n    DefaultRenderersFactory factory =\n        new DefaultRenderersFactory(ApplicationProvider.getApplicationContext())\n            .setExtensionRendererMode(extensionRendererMode);\n    return factory.createRenderers(\n        new Handler(Looper.getMainLooper()),\n        new VideoRendererEventListener() {},\n        new AudioRendererEventListener() {},\n        (List<Cue> cues) -> {},\n        (Metadata metadata) -> {},\n        /* drmSessionManager= */ null);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.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.testutil;\n\nimport android.net.Uri;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.chunk.MediaChunk;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.DefaultHttpDataSource;\nimport java.io.IOException;\n\n/** Fake {@link MediaChunk}. */\npublic final class FakeMediaChunk extends MediaChunk {\n\n  private static final DataSource DATA_SOURCE = new DefaultHttpDataSource(\"TEST_AGENT\");\n\n  public FakeMediaChunk(Format trackFormat, long startTimeUs, long endTimeUs) {\n    this(new DataSpec(Uri.EMPTY), trackFormat, startTimeUs, endTimeUs);\n  }\n\n  public FakeMediaChunk(DataSpec dataSpec, Format trackFormat, long startTimeUs, long endTimeUs) {\n    super(\n        DATA_SOURCE,\n        dataSpec,\n        trackFormat,\n        C.SELECTION_REASON_ADAPTIVE,\n        /* trackSelectionData= */ null,\n        startTimeUs,\n        endTimeUs,\n        /* chunkIndex= */ 0);\n  }\n\n  @Override\n  public void cancelLoad() {\n    // Do nothing.\n  }\n\n  @Override\n  public void load() throws IOException, InterruptedException {\n    // Do nothing.\n  }\n\n  @Override\n  public boolean isLoadCompleted() {\n    return true;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.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.testutil;\n\nimport android.net.Uri;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator;\nimport com.google.android.exoplayer2.upstream.DataSpec;\n\n/** Fake {@link com.google.android.exoplayer2.source.chunk.MediaChunkIterator}. */\npublic final class FakeMediaChunkIterator extends BaseMediaChunkIterator {\n\n  private final long[] chunkTimeBoundariesSec;\n  private final long[] chunkLengths;\n\n  /**\n   * Creates a fake {@link com.google.android.exoplayer2.source.chunk.MediaChunkIterator}.\n   *\n   * @param chunkTimeBoundariesSec An array containing the time boundaries where one chunk ends and\n   *     the next one starts. The first value is the start time of the first chunk and the last\n   *     value is the end time of the last chunk. The array should be of length (chunk-count + 1).\n   * @param chunkLengths An array which contains the length of each chunk, should be of length\n   *     (chunk-count).\n   */\n  public FakeMediaChunkIterator(long[] chunkTimeBoundariesSec, long[] chunkLengths) {\n    super(/* fromIndex= */ 0, /* toIndex= */ chunkTimeBoundariesSec.length - 2);\n    this.chunkTimeBoundariesSec = chunkTimeBoundariesSec;\n    this.chunkLengths = chunkLengths;\n  }\n\n  @Override\n  public DataSpec getDataSpec() {\n    checkInBounds();\n    return new DataSpec(\n        Uri.EMPTY,\n        /* absoluteStreamPosition= */ 0,\n        chunkLengths[(int) getCurrentIndex()],\n        /* key= */ null);\n  }\n\n  @Override\n  public long getChunkStartTimeUs() {\n    checkInBounds();\n    return chunkTimeBoundariesSec[(int) getCurrentIndex()] * C.MICROS_PER_SECOND;\n  }\n\n  @Override\n  public long getChunkEndTimeUs() {\n    checkInBounds();\n    return chunkTimeBoundariesSec[(int) getCurrentIndex() + 1] * C.MICROS_PER_SECOND;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.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.testutil;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.util.MediaClock;\n\n/** Fake abstract {@link Renderer} which is also a {@link MediaClock}. */\npublic abstract class FakeMediaClockRenderer extends FakeRenderer implements MediaClock {\n\n  public FakeMediaClockRenderer(Format... expectedFormats) {\n    super(expectedFormats);\n  }\n\n  @Override\n  public MediaClock getMediaClock() {\n    return this;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.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.testutil;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.source.ShuffleOrder;\n\n/**\n * Fake {@link ShuffleOrder} which returns a reverse order. This order is thus deterministic but\n * different from the original order.\n */\npublic final class FakeShuffleOrder implements ShuffleOrder {\n\n  private final int length;\n\n  public FakeShuffleOrder(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 > 0 ? index - 1 : C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getPreviousIndex(int index) {\n    return index < length - 1 ? index + 1 : C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getLastIndex() {\n    return length > 0 ? 0 : C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getFirstIndex() {\n    return length > 0 ? length - 1 : C.INDEX_UNSET;\n  }\n\n  @Override\n  public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) {\n    return new FakeShuffleOrder(length + insertionCount);\n  }\n\n  @Override\n  public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) {\n    return new FakeShuffleOrder(length - indexToExclusive + indexFrom);\n  }\n\n  @Override\n  public ShuffleOrder cloneAndClear() {\n    return new FakeShuffleOrder(/* length= */ 0);\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\n\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.trackselection.TrackSelection;\nimport java.util.List;\n\n/**\n * A fake {@link TrackSelection} that only returns 1 fixed track, and allows querying the number of\n * calls to its methods.\n */\npublic final class FakeTrackSelection implements TrackSelection {\n\n  private final TrackGroup rendererTrackGroup;\n\n  public int enableCount;\n  public int releaseCount;\n  public boolean isEnabled;\n\n  public FakeTrackSelection(TrackGroup rendererTrackGroup) {\n    this.rendererTrackGroup = rendererTrackGroup;\n  }\n\n  @Override\n  public void enable() {\n    // assert that track selection is in disabled state before this call.\n    assertThat(isEnabled).isFalse();\n    enableCount++;\n    isEnabled = true;\n  }\n\n  @Override\n  public void disable() {\n    // assert that track selection is in enabled state before this call.\n    assertThat(isEnabled).isTrue();\n    releaseCount++;\n    isEnabled = false;\n  }\n\n  @Override\n  public TrackGroup getTrackGroup() {\n    return rendererTrackGroup;\n  }\n\n  @Override\n  public int length() {\n    return rendererTrackGroup.length;\n  }\n\n  @Override\n  public Format getFormat(int index) {\n    return rendererTrackGroup.getFormat(0);\n  }\n\n  @Override\n  public int getIndexInTrackGroup(int index) {\n    return 0;\n  }\n\n  @Override\n  public int indexOf(Format format) {\n    assertThat(isEnabled).isTrue();\n    return 0;\n  }\n\n  @Override\n  public int indexOf(int indexInTrackGroup) {\n    return 0;\n  }\n\n  @Override\n  public Format getSelectedFormat() {\n    return rendererTrackGroup.getFormat(0);\n  }\n\n  @Override\n  public int getSelectedIndexInTrackGroup() {\n    return 0;\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  @Override\n  public Object getSelectionData() {\n    return null;\n  }\n\n  @Override\n  public void onPlaybackSpeed(float speed) {\n    // Do nothing.\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    assertThat(isEnabled).isTrue();\n  }\n\n  @Override\n  public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {\n    assertThat(isEnabled).isTrue();\n    return 0;\n  }\n\n  @Override\n  public boolean blacklist(int index, long blacklistDurationMs) {\n    assertThat(isEnabled).isTrue();\n    return false;\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.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.testutil;\n\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.BandwidthMeter;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/** A fake {@link MappingTrackSelector} that returns {@link FakeTrackSelection}s. */\npublic class FakeTrackSelector extends DefaultTrackSelector {\n\n  private final FakeTrackSelectionFactory fakeTrackSelectionFactory;\n\n  public FakeTrackSelector() {\n    this(false);\n  }\n\n  /**\n   * @param mayReuseTrackSelection Whether this {@link FakeTrackSelector} will reuse {@link\n   *     TrackSelection}s during track selection, when it finds previously-selected track selection\n   *     using the same {@link TrackGroup}.\n   */\n  public FakeTrackSelector(boolean mayReuseTrackSelection) {\n    this(new FakeTrackSelectionFactory(mayReuseTrackSelection));\n  }\n\n  private FakeTrackSelector(FakeTrackSelectionFactory fakeTrackSelectionFactory) {\n    super(fakeTrackSelectionFactory);\n    this.fakeTrackSelectionFactory = fakeTrackSelectionFactory;\n  }\n\n  @Override\n  protected TrackSelection.Definition[] selectAllTracks(\n      MappedTrackInfo mappedTrackInfo,\n      int[][][] rendererFormatSupports,\n      int[] rendererMixedMimeTypeAdaptationSupports,\n      Parameters params) {\n    int rendererCount = mappedTrackInfo.getRendererCount();\n    TrackSelection.Definition[] definitions = new TrackSelection.Definition[rendererCount];\n    for (int i = 0; i < rendererCount; i++) {\n      TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i);\n      boolean hasTracks = trackGroupArray.length > 0;\n      definitions[i] = hasTracks ? new TrackSelection.Definition(trackGroupArray.get(0)) : null;\n    }\n    return definitions;\n  }\n\n  /** Returns list of all {@link FakeTrackSelection}s that this track selector has made so far. */\n  public List<FakeTrackSelection> getAllTrackSelections() {\n    return fakeTrackSelectionFactory.trackSelections;\n  }\n\n  private static class FakeTrackSelectionFactory implements TrackSelection.Factory {\n\n    private final List<FakeTrackSelection> trackSelections;\n    private final boolean mayReuseTrackSelection;\n\n    public FakeTrackSelectionFactory(boolean mayReuseTrackSelection) {\n      this.mayReuseTrackSelection = mayReuseTrackSelection;\n      trackSelections = new ArrayList<>();\n    }\n\n    @Override\n    public TrackSelection[] createTrackSelections(\n        TrackSelection.Definition[] definitions, BandwidthMeter bandwidthMeter) {\n      TrackSelection[] selections = new TrackSelection[definitions.length];\n      for (int i = 0; i < definitions.length; i++) {\n        TrackSelection.Definition definition = definitions[i];\n        if (definition != null) {\n          selections[i] = createTrackSelection(definition.group);\n        }\n      }\n      return selections;\n    }\n\n    private TrackSelection createTrackSelection(TrackGroup trackGroup) {\n      if (mayReuseTrackSelection) {\n        for (FakeTrackSelection trackSelection : trackSelections) {\n          if (trackSelection.getTrackGroup().equals(trackGroup)) {\n            return trackSelection;\n          }\n        }\n      }\n      FakeTrackSelection trackSelection = new FakeTrackSelection(trackGroup);\n      trackSelections.add(trackSelection);\n      return trackSelection;\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.offline.FilterableManifest;\nimport com.google.android.exoplayer2.offline.StreamKey;\nimport com.google.android.exoplayer2.source.MediaPeriod;\nimport com.google.android.exoplayer2.source.MediaPeriod.Callback;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.BaseTrackSelection;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.util.ConditionVariable;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicReference;\n\n/** Assertion methods for {@link MediaPeriod}. */\npublic final class MediaPeriodAsserts {\n\n  /**\n   * Interface to create media periods for testing based on a {@link FilterableManifest}.\n   *\n   * @param <T> The type of {@link FilterableManifest}.\n   */\n  public interface FilterableManifestMediaPeriodFactory<T extends FilterableManifest<T>> {\n\n    /** Returns media period based on the provided filterable manifest. */\n    MediaPeriod createMediaPeriod(T manifest, int periodIndex);\n  }\n\n  private MediaPeriodAsserts() {}\n\n  /**\n   * Asserts that the values returns by {@link MediaPeriod#getStreamKeys(List)} are compatible with\n   * a {@link FilterableManifest} using these stream keys.\n   *\n   * @param mediaPeriodFactory A factory to create a {@link MediaPeriod} based on a manifest.\n   * @param manifest The manifest which is to be tested.\n   */\n  public static <T extends FilterableManifest<T>>\n      void assertGetStreamKeysAndManifestFilterIntegration(\n          FilterableManifestMediaPeriodFactory<T> mediaPeriodFactory, T manifest) {\n    assertGetStreamKeysAndManifestFilterIntegration(\n        mediaPeriodFactory, manifest, /* periodIndex= */ 0, /* ignoredMimeType= */ null);\n  }\n\n  /**\n   * Asserts that the values returns by {@link MediaPeriod#getStreamKeys(List)} are compatible with\n   * a {@link FilterableManifest} using these stream keys.\n   *\n   * @param mediaPeriodFactory A factory to create a {@link MediaPeriod} based on a manifest.\n   * @param manifest The manifest which is to be tested.\n   * @param periodIndex The index of period in the manifest.\n   * @param ignoredMimeType Optional mime type whose existence in the filtered track groups is not\n   *     asserted.\n   */\n  public static <T extends FilterableManifest<T>>\n      void assertGetStreamKeysAndManifestFilterIntegration(\n          FilterableManifestMediaPeriodFactory<T> mediaPeriodFactory,\n          T manifest,\n          int periodIndex,\n          @Nullable String ignoredMimeType) {\n    MediaPeriod mediaPeriod = mediaPeriodFactory.createMediaPeriod(manifest, periodIndex);\n    TrackGroupArray trackGroupArray = getTrackGroups(mediaPeriod);\n\n    // Create test vector of query test selections:\n    //  - One selection with one track per group, two tracks or all tracks.\n    //  - Two selections with tracks from multiple groups, or tracks from a single group.\n    //  - Multiple selections with tracks from all groups.\n    List<List<TrackSelection>> testSelections = new ArrayList<>();\n    for (int i = 0; i < trackGroupArray.length; i++) {\n      TrackGroup trackGroup = trackGroupArray.get(i);\n      for (int j = 0; j < trackGroup.length; j++) {\n        testSelections.add(Collections.singletonList(new TestTrackSelection(trackGroup, j)));\n      }\n      if (trackGroup.length > 1) {\n        testSelections.add(Collections.singletonList(new TestTrackSelection(trackGroup, 0, 1)));\n        testSelections.add(\n            Arrays.asList(\n                new TrackSelection[] {\n                  new TestTrackSelection(trackGroup, 0), new TestTrackSelection(trackGroup, 1)\n                }));\n      }\n      if (trackGroup.length > 2) {\n        int[] allTracks = new int[trackGroup.length];\n        for (int j = 0; j < trackGroup.length; j++) {\n          allTracks[j] = j;\n        }\n        testSelections.add(\n            Collections.singletonList(new TestTrackSelection(trackGroup, allTracks)));\n      }\n    }\n    if (trackGroupArray.length > 1) {\n      for (int i = 0; i < trackGroupArray.length - 1; i++) {\n        for (int j = i + 1; j < trackGroupArray.length; j++) {\n          testSelections.add(\n              Arrays.asList(\n                  new TrackSelection[] {\n                    new TestTrackSelection(trackGroupArray.get(i), 0),\n                    new TestTrackSelection(trackGroupArray.get(j), 0)\n                  }));\n        }\n      }\n    }\n    if (trackGroupArray.length > 2) {\n      List<TrackSelection> selectionsFromAllGroups = new ArrayList<>();\n      for (int i = 0; i < trackGroupArray.length; i++) {\n        selectionsFromAllGroups.add(new TestTrackSelection(trackGroupArray.get(i), 0));\n      }\n      testSelections.add(selectionsFromAllGroups);\n    }\n\n    // Verify for each case that stream keys can be used to create filtered tracks which still\n    // contain at least all requested formats.\n    for (List<TrackSelection> testSelection : testSelections) {\n      List<StreamKey> streamKeys = mediaPeriod.getStreamKeys(testSelection);\n      if (streamKeys.isEmpty()) {\n        // Manifests won't be filtered if stream key is empty.\n        continue;\n      }\n      T filteredManifest = manifest.copy(streamKeys);\n      // The filtered manifest should only have one period left.\n      MediaPeriod filteredMediaPeriod =\n          mediaPeriodFactory.createMediaPeriod(filteredManifest, /* periodIndex= */ 0);\n      TrackGroupArray filteredTrackGroupArray = getTrackGroups(filteredMediaPeriod);\n      for (TrackSelection trackSelection : testSelection) {\n        if (ignoredMimeType != null\n            && ignoredMimeType.equals(trackSelection.getFormat(0).sampleMimeType)) {\n          continue;\n        }\n        Format[] expectedFormats = new Format[trackSelection.length()];\n        for (int k = 0; k < trackSelection.length(); k++) {\n          expectedFormats[k] = trackSelection.getFormat(k);\n        }\n        assertOneTrackGroupContainsFormats(filteredTrackGroupArray, expectedFormats);\n      }\n    }\n  }\n\n  private static void assertOneTrackGroupContainsFormats(\n      TrackGroupArray trackGroupArray, Format[] formats) {\n    boolean foundSubset = false;\n    for (int i = 0; i < trackGroupArray.length; i++) {\n      if (containsFormats(trackGroupArray.get(i), formats)) {\n        foundSubset = true;\n        break;\n      }\n    }\n    assertThat(foundSubset).isTrue();\n  }\n\n  private static boolean containsFormats(TrackGroup trackGroup, Format[] formats) {\n    HashSet<Format> allFormats = new HashSet<>();\n    for (int i = 0; i < trackGroup.length; i++) {\n      allFormats.add(trackGroup.getFormat(i));\n    }\n    for (int i = 0; i < formats.length; i++) {\n      if (!allFormats.remove(formats[i])) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  private static TrackGroupArray getTrackGroups(MediaPeriod mediaPeriod) {\n    AtomicReference<TrackGroupArray> trackGroupArray = new AtomicReference<>(null);\n    DummyMainThread dummyMainThread = new DummyMainThread();\n    ConditionVariable preparedCondition = new ConditionVariable();\n    dummyMainThread.runOnMainThread(\n        () -> {\n          mediaPeriod.prepare(\n              new Callback() {\n                @Override\n                public void onPrepared(MediaPeriod mediaPeriod) {\n                  trackGroupArray.set(mediaPeriod.getTrackGroups());\n                  preparedCondition.open();\n                }\n\n                @Override\n                public void onContinueLoadingRequested(MediaPeriod source) {\n                  // Ignore.\n                }\n              },\n              /* positionUs= */ 0);\n        });\n    try {\n      preparedCondition.block();\n    } catch (InterruptedException e) {\n      // Ignore.\n    }\n    dummyMainThread.release();\n    return trackGroupArray.get();\n  }\n\n  private static final class TestTrackSelection extends BaseTrackSelection {\n\n    public TestTrackSelection(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}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static com.google.common.truth.Truth.assertWithMessage;\n\nimport android.os.ConditionVariable;\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.Looper;\nimport androidx.annotation.Nullable;\nimport android.util.Pair;\nimport com.google.android.exoplayer2.Timeline;\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.upstream.Allocator;\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.HashSet;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.LinkedBlockingDeque;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicReference;\n\n/** A runner for {@link MediaSource} tests. */\npublic class MediaSourceTestRunner {\n\n  public static final int TIMEOUT_MS = 10000;\n\n  private final MediaSource mediaSource;\n  private final MediaSourceListener mediaSourceListener;\n  private final HandlerThread playbackThread;\n  private final Handler playbackHandler;\n  private final Allocator allocator;\n\n  private final LinkedBlockingDeque<Timeline> timelines;\n  private final CopyOnWriteArrayList<Pair<Integer, MediaPeriodId>> completedLoads;\n  private final AtomicReference<MediaPeriodId> lastCreatedMediaPeriod;\n  private final AtomicReference<MediaPeriodId> lastReleasedMediaPeriod;\n\n  private Timeline timeline;\n\n  /**\n   * @param mediaSource The source under test.\n   * @param allocator The allocator to use during the test run.\n   */\n  public MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator) {\n    this.mediaSource = mediaSource;\n    this.allocator = allocator;\n    playbackThread = new HandlerThread(\"PlaybackThread\");\n    playbackThread.start();\n    Looper playbackLooper = playbackThread.getLooper();\n    playbackHandler = new Handler(playbackLooper);\n    mediaSourceListener = new MediaSourceListener();\n    timelines = new LinkedBlockingDeque<>();\n    completedLoads = new CopyOnWriteArrayList<>();\n    lastCreatedMediaPeriod = new AtomicReference<>();\n    lastReleasedMediaPeriod = new AtomicReference<>();\n    mediaSource.addEventListener(playbackHandler, mediaSourceListener);\n  }\n\n  /**\n   * Runs the provided {@link Runnable} on the playback thread, blocking until execution completes.\n   *\n   * @param runnable The {@link Runnable} to run.\n   */\n  public void runOnPlaybackThread(final Runnable runnable) {\n    final Throwable[] throwable = new Throwable[1];\n    final ConditionVariable finishedCondition = new ConditionVariable();\n    playbackHandler.post(\n        () -> {\n          try {\n            runnable.run();\n          } catch (Throwable e) {\n            throwable[0] = e;\n          } finally {\n            finishedCondition.open();\n          }\n        });\n    assertThat(finishedCondition.block(TIMEOUT_MS)).isTrue();\n    if (throwable[0] != null) {\n      Util.sneakyThrow(throwable[0]);\n    }\n  }\n\n  /**\n   * Prepares the source on the playback thread, asserting that it provides an initial timeline.\n   *\n   * @return The initial {@link Timeline}.\n   */\n  public Timeline prepareSource() throws IOException {\n    final IOException[] prepareError = new IOException[1];\n    runOnPlaybackThread(\n        () -> {\n          mediaSource.prepareSource(mediaSourceListener, /* mediaTransferListener= */ null);\n          try {\n            // TODO: This only catches errors that are set synchronously in prepareSource. To\n            // capture async errors we'll need to poll maybeThrowSourceInfoRefreshError until the\n            // first call to onSourceInfoRefreshed.\n            mediaSource.maybeThrowSourceInfoRefreshError();\n          } catch (IOException e) {\n            prepareError[0] = e;\n          }\n        });\n    if (prepareError[0] != null) {\n      throw prepareError[0];\n    }\n    return assertTimelineChangeBlocking();\n  }\n\n  /**\n   * Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator, long)} with a zero\n   * start position on the playback thread, asserting that a non-null {@link MediaPeriod} is\n   * returned.\n   *\n   * @param periodId The id of the period to create.\n   * @return The created {@link MediaPeriod}.\n   */\n  public MediaPeriod createPeriod(final MediaPeriodId periodId) {\n    return createPeriod(periodId, /* startPositionUs= */ 0);\n  }\n\n  /**\n   * Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator, long)} on the\n   * playback thread, asserting that a non-null {@link MediaPeriod} is returned.\n   *\n   * @param periodId The id of the period to create.\n   * @return The created {@link MediaPeriod}.\n   */\n  public MediaPeriod createPeriod(final MediaPeriodId periodId, long startPositionUs) {\n    final MediaPeriod[] holder = new MediaPeriod[1];\n    runOnPlaybackThread(\n        () -> holder[0] = mediaSource.createPeriod(periodId, allocator, startPositionUs));\n    assertThat(holder[0]).isNotNull();\n    return holder[0];\n  }\n\n  /**\n   * Calls {@link MediaPeriod#prepare(MediaPeriod.Callback, long)} on the playback thread and blocks\n   * until the method has been called.\n   *\n   * @param mediaPeriod The {@link MediaPeriod} to prepare.\n   * @param positionUs The position at which to prepare.\n   * @return A {@link CountDownLatch} that will be counted down when preparation completes.\n   */\n  public CountDownLatch preparePeriod(final MediaPeriod mediaPeriod, final long positionUs) {\n    final ConditionVariable prepareCalled = new ConditionVariable();\n    final CountDownLatch preparedCountDown = new CountDownLatch(1);\n    runOnPlaybackThread(\n        () -> {\n          mediaPeriod.prepare(\n              new MediaPeriod.Callback() {\n                @Override\n                public void onPrepared(MediaPeriod mediaPeriod1) {\n                  preparedCountDown.countDown();\n                }\n\n                @Override\n                public void onContinueLoadingRequested(MediaPeriod source) {\n                  // Do nothing.\n                }\n              },\n              positionUs);\n          prepareCalled.open();\n        });\n    prepareCalled.block();\n    return preparedCountDown;\n  }\n\n  /**\n   * Calls {@link MediaSource#releasePeriod(MediaPeriod)} on the playback thread.\n   *\n   * @param mediaPeriod The {@link MediaPeriod} to release.\n   */\n  public void releasePeriod(final MediaPeriod mediaPeriod) {\n    runOnPlaybackThread(() -> mediaSource.releasePeriod(mediaPeriod));\n  }\n\n  /**\n   * Calls {@link MediaSource#releaseSource(MediaSource.SourceInfoRefreshListener)} on the playback\n   * thread.\n   */\n  public void releaseSource() {\n    runOnPlaybackThread(() -> mediaSource.releaseSource(mediaSourceListener));\n  }\n\n  /**\n   * Asserts that the source has not notified its listener of a timeline change since the last call\n   * to {@link #assertTimelineChangeBlocking()} or {@link #assertTimelineChange()} (or since the\n   * runner was created if neither method has been called).\n   */\n  public void assertNoTimelineChange() {\n    assertThat(timelines.isEmpty()).isTrue();\n  }\n\n  /**\n   * Asserts that the source has notified its listener of a single timeline change.\n   *\n   * @return The new {@link Timeline}.\n   */\n  public Timeline assertTimelineChange() {\n    timeline = timelines.removeFirst();\n    assertNoTimelineChange();\n    return timeline;\n  }\n\n  /**\n   * Asserts that the source notifies its listener of a single timeline change. If the source has\n   * not yet notified its listener, it has up to the timeout passed to the constructor to do so.\n   *\n   * @return The new {@link Timeline}.\n   */\n  public Timeline assertTimelineChangeBlocking() {\n    try {\n      timeline = timelines.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS);\n      assertThat(timeline).isNotNull(); // Null indicates the poll timed out.\n      assertNoTimelineChange();\n      return timeline;\n    } catch (InterruptedException e) {\n      // Should never happen.\n      throw new RuntimeException(e);\n    }\n  }\n\n  /**\n   * Creates and releases all periods (including ad periods) defined in the last timeline to be\n   * returned from {@link #prepareSource()}, {@link #assertTimelineChange()} or {@link\n   * #assertTimelineChangeBlocking()}. The {@link MediaPeriodId#windowSequenceNumber} is set to the\n   * index of the window.\n   */\n  public void assertPrepareAndReleaseAllPeriods() throws InterruptedException {\n    Timeline.Period period = new Timeline.Period();\n    for (int i = 0; i < timeline.getPeriodCount(); i++) {\n      timeline.getPeriod(i, period, /* setIds= */ true);\n      assertPrepareAndReleasePeriod(new MediaPeriodId(period.uid, period.windowIndex));\n      for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) {\n        for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) {\n          assertPrepareAndReleasePeriod(\n              new MediaPeriodId(period.uid, adGroupIndex, adIndex, period.windowIndex));\n        }\n      }\n    }\n  }\n\n  private void assertPrepareAndReleasePeriod(MediaPeriodId mediaPeriodId)\n      throws InterruptedException {\n    MediaPeriod mediaPeriod = createPeriod(mediaPeriodId);\n    assertThat(lastCreatedMediaPeriod.getAndSet(/* newValue= */ null)).isEqualTo(mediaPeriodId);\n    CountDownLatch preparedCondition = preparePeriod(mediaPeriod, 0);\n    assertThat(preparedCondition.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();\n    // MediaSource is supposed to support multiple calls to createPeriod without an intervening call\n    // to releasePeriod.\n    MediaPeriodId secondMediaPeriodId =\n        new MediaPeriodId(\n            mediaPeriodId.periodUid,\n            mediaPeriodId.adGroupIndex,\n            mediaPeriodId.adIndexInAdGroup,\n            mediaPeriodId.windowSequenceNumber + 1000);\n    MediaPeriod secondMediaPeriod = createPeriod(secondMediaPeriodId);\n    assertThat(lastCreatedMediaPeriod.getAndSet(/* newValue= */ null))\n        .isEqualTo(secondMediaPeriodId);\n    CountDownLatch secondPreparedCondition = preparePeriod(secondMediaPeriod, 0);\n    assertThat(secondPreparedCondition.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();\n    // Release the periods.\n    releasePeriod(mediaPeriod);\n    assertThat(lastReleasedMediaPeriod.getAndSet(/* newValue= */ null)).isEqualTo(mediaPeriodId);\n    releasePeriod(secondMediaPeriod);\n    assertThat(lastReleasedMediaPeriod.getAndSet(/* newValue= */ null))\n        .isEqualTo(secondMediaPeriodId);\n  }\n\n  /**\n   * Asserts that the media source reported completed loads via {@link\n   * MediaSourceEventListener#onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)} for\n   * each specified window index and a null period id. Also asserts that no other loads with media\n   * period id null are reported.\n   */\n  public void assertCompletedManifestLoads(Integer... windowIndices) {\n    List<Integer> expectedWindowIndices = new ArrayList<>(Arrays.asList(windowIndices));\n    for (Pair<Integer, MediaPeriodId> windowIndexAndMediaPeriodId : completedLoads) {\n      if (windowIndexAndMediaPeriodId.second == null) {\n        boolean loadExpected = expectedWindowIndices.remove(windowIndexAndMediaPeriodId.first);\n        assertThat(loadExpected).isTrue();\n      }\n    }\n    assertWithMessage(\"Not all expected media source loads have been completed.\")\n        .that(expectedWindowIndices)\n        .isEmpty();\n  }\n\n  /**\n   * Asserts that the media source reported completed loads via {@link\n   * MediaSourceEventListener#onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)} for\n   * each specified media period id, and asserts that the associated window index matches the one in\n   * the last known timeline returned from {@link #prepareSource()}, {@link #assertTimelineChange()}\n   * or {@link #assertTimelineChangeBlocking()}.\n   */\n  public void assertCompletedMediaPeriodLoads(MediaPeriodId... mediaPeriodIds) {\n    Timeline.Period period = new Timeline.Period();\n    HashSet<MediaPeriodId> expectedLoads = new HashSet<>(Arrays.asList(mediaPeriodIds));\n    for (Pair<Integer, MediaPeriodId> windowIndexAndMediaPeriodId : completedLoads) {\n      int windowIndex = windowIndexAndMediaPeriodId.first;\n      MediaPeriodId mediaPeriodId = windowIndexAndMediaPeriodId.second;\n      if (expectedLoads.remove(mediaPeriodId)) {\n        int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid);\n        assertThat(windowIndex).isEqualTo(timeline.getPeriod(periodIndex, period).windowIndex);\n      }\n    }\n    assertWithMessage(\"Not all expected media source loads have been completed.\")\n        .that(expectedLoads)\n        .isEmpty();\n  }\n\n  /** Releases the runner. Should be called when the runner is no longer required. */\n  public void release() {\n    playbackThread.quit();\n  }\n\n  private class MediaSourceListener\n      implements MediaSource.SourceInfoRefreshListener, MediaSourceEventListener {\n\n    // SourceInfoRefreshListener methods.\n\n    @Override\n    public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) {\n      Assertions.checkState(Looper.myLooper() == playbackThread.getLooper());\n      timelines.addLast(timeline);\n    }\n\n    // MediaSourceEventListener methods.\n\n    @Override\n    public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {\n      Assertions.checkState(Looper.myLooper() == playbackThread.getLooper());\n      lastCreatedMediaPeriod.set(mediaPeriodId);\n    }\n\n    @Override\n    public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {\n      Assertions.checkState(Looper.myLooper() == playbackThread.getLooper());\n      lastReleasedMediaPeriod.set(mediaPeriodId);\n    }\n\n    @Override\n    public void onLoadStarted(\n        int windowIndex,\n        @Nullable MediaPeriodId mediaPeriodId,\n        LoadEventInfo loadEventInfo,\n        MediaLoadData mediaLoadData) {\n      Assertions.checkState(Looper.myLooper() == playbackThread.getLooper());\n    }\n\n    @Override\n    public void onLoadCompleted(\n        int windowIndex,\n        @Nullable MediaPeriodId mediaPeriodId,\n        LoadEventInfo loadEventInfo,\n        MediaLoadData mediaLoadData) {\n      Assertions.checkState(Looper.myLooper() == playbackThread.getLooper());\n      completedLoads.add(Pair.create(windowIndex, mediaPeriodId));\n    }\n\n    @Override\n    public void onLoadCanceled(\n        int windowIndex,\n        @Nullable MediaPeriodId mediaPeriodId,\n        LoadEventInfo loadEventInfo,\n        MediaLoadData mediaLoadData) {\n      Assertions.checkState(Looper.myLooper() == playbackThread.getLooper());\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      Assertions.checkState(Looper.myLooper() == playbackThread.getLooper());\n    }\n\n    @Override\n    public void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {\n      Assertions.checkState(Looper.myLooper() == playbackThread.getLooper());\n    }\n\n    @Override\n    public void onUpstreamDiscarded(\n        int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {\n      Assertions.checkState(Looper.myLooper() == playbackThread.getLooper());\n    }\n\n    @Override\n    public void onDownstreamFormatChanged(\n        int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {\n      Assertions.checkState(Looper.myLooper() == playbackThread.getLooper());\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.exoplayer2.testutil;\n\n/** Provides ogg/vorbis test data in bytes for unit tests. */\npublic final class OggTestData {\n\n  public static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) {\n    return new FakeExtractorInput.Builder()\n        .setData(data)\n        .setSimulateIOErrors(true)\n        .setSimulateUnknownLength(simulateUnknownLength)\n        .setSimulatePartialReads(true)\n        .build();\n  }\n\n  public static byte[] buildOggHeader(\n      int headerType, long granule, int pageSequenceCounter, int pageSegmentCount) {\n    return TestUtil.createByteArray(\n        0x4F,\n        0x67,\n        0x67,\n        0x53, // Oggs.\n        0x00, // Stream revision.\n        headerType,\n        (int) (granule) & 0xFF,\n        (int) (granule >> 8) & 0xFF,\n        (int) (granule >> 16) & 0xFF,\n        (int) (granule >> 24) & 0xFF,\n        (int) (granule >> 32) & 0xFF,\n        (int) (granule >> 40) & 0xFF,\n        (int) (granule >> 48) & 0xFF,\n        (int) (granule >> 56) & 0xFF,\n        0x00, // LSB of data serial number.\n        0x10,\n        0x00,\n        0x00, // MSB of data serial number.\n        (pageSequenceCounter) & 0xFF,\n        (pageSequenceCounter >> 8) & 0xFF,\n        (pageSequenceCounter >> 16) & 0xFF,\n        (pageSequenceCounter >> 24) & 0xFF,\n        0x00, // LSB of page checksum.\n        0x00,\n        0x10,\n        0x00, // MSB of page checksum.\n        pageSegmentCount);\n  }\n\n  /**\n   * Returns the initial two pages of bytes which by spec contain the three vorbis header packets:\n   * identification, comment and setup header.\n   */\n  public static byte[] getVorbisHeaderPages() {\n    byte[] data = new byte[VORBIS_HEADER_PAGES.length];\n    System.arraycopy(VORBIS_HEADER_PAGES, 0, data, 0, VORBIS_HEADER_PAGES.length);\n    return data;\n  }\n\n  /** Returns a valid vorbis identification header in bytes. */\n  public static byte[] getIdentificationHeaderData() {\n    int idHeaderStart = 28;\n    int idHeaderLength = 30;\n    byte[] idHeaderData = new byte[idHeaderLength];\n    System.arraycopy(VORBIS_HEADER_PAGES, idHeaderStart, idHeaderData, 0, idHeaderLength);\n    return idHeaderData;\n  }\n\n  /** Returns a valid vorbis comment header with 3 comments including utf8 chars in bytes. */\n  public static byte[] getCommentHeaderDataUTF8() {\n    byte[] commentHeaderData = new byte[COMMENT_HEADER_WITH_UTF8.length];\n    System.arraycopy(\n        COMMENT_HEADER_WITH_UTF8, 0, commentHeaderData, 0, COMMENT_HEADER_WITH_UTF8.length);\n    return commentHeaderData;\n  }\n\n  /** Returns a valid vorbis setup header in bytes. */\n  public static byte[] getSetupHeaderData() {\n    int setupHeaderStart = 146;\n    int setupHeaderLength = VORBIS_HEADER_PAGES.length - setupHeaderStart;\n    byte[] setupHeaderData = new byte[setupHeaderLength];\n    System.arraycopy(VORBIS_HEADER_PAGES, setupHeaderStart, setupHeaderData, 0, setupHeaderLength);\n    return setupHeaderData;\n  }\n\n  private static final byte[] COMMENT_HEADER_WITH_UTF8 = {\n    (byte) 0x03, (byte) 0x76, (byte) 0x6f, (byte) 0x72, // 3, v, o, r,\n    (byte) 0x62, (byte) 0x69, (byte) 0x73, (byte) 0x2b, // b, i, s, .\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x58,\n    (byte) 0x69, (byte) 0x70, (byte) 0x68, (byte) 0x2e,\n    (byte) 0x4f, (byte) 0x72, (byte) 0x67, (byte) 0x20,\n    (byte) 0x6c, (byte) 0x69, (byte) 0x62, (byte) 0x56,\n    (byte) 0x6f, (byte) 0x72, (byte) 0x62, (byte) 0x69,\n    (byte) 0x73, (byte) 0x20, (byte) 0x49, (byte) 0x20,\n    (byte) 0x32, (byte) 0x30, (byte) 0x31, (byte) 0x32,\n    (byte) 0x30, (byte) 0x32, (byte) 0x30, (byte) 0x33,\n    (byte) 0x20, (byte) 0x28, (byte) 0x4f, (byte) 0x6d,\n    (byte) 0x6e, (byte) 0x69, (byte) 0x70, (byte) 0x72,\n    (byte) 0x65, (byte) 0x73, (byte) 0x65, (byte) 0x6e,\n    (byte) 0x74, (byte) 0x29, (byte) 0x03, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x0a, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0x4c,\n    (byte) 0x42, (byte) 0x55, (byte) 0x4d, (byte) 0x3d,\n    (byte) 0xc3, (byte) 0xa4, (byte) 0xc3, (byte) 0xb6,\n    (byte) 0x13, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x54, (byte) 0x49, (byte) 0x54, (byte) 0x4c,\n    (byte) 0x45, (byte) 0x3d, (byte) 0x41, (byte) 0x20,\n    (byte) 0x73, (byte) 0x61, (byte) 0x6d, (byte) 0x70,\n    (byte) 0x6c, (byte) 0x65, (byte) 0x20, (byte) 0x73,\n    (byte) 0x6f, (byte) 0x6e, (byte) 0x67, (byte) 0x0d,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x41,\n    (byte) 0x52, (byte) 0x54, (byte) 0x49, (byte) 0x53,\n    (byte) 0x54, (byte) 0x3d, (byte) 0x47, (byte) 0x6f,\n    (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65,\n    (byte) 0x01\n  };\n\n  // two OGG pages with 3 packets (id, comment and setup header)\n  // length: 3743 bytes\n  private static final byte[] VORBIS_HEADER_PAGES = { /* capture pattern ogg header 1 */\n    (byte) 0x4f, (byte) 0x67, (byte) 0x67, (byte) 0x53, // O,g,g,S  : start pos 0\n    (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x5e, (byte) 0x5f,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x83, (byte) 0x36,\n    (byte) 0xe3, (byte) 0x49, (byte) 0x01, (byte) 0x1e, /* capture pattern vorbis id header */\n    (byte) 0x01, (byte) 0x76, (byte) 0x6f, (byte) 0x72, // 1,v,o,r  : start pos 28\n    (byte) 0x62, (byte) 0x69, (byte) 0x73, (byte) 0x00, // b,i,s,.\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,\n    (byte) 0x22, (byte) 0x56, (byte) 0x00, (byte) 0x00,\n    (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,\n    (byte) 0x6a, (byte) 0x04, (byte) 0x01, (byte) 0x00,\n    (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, /* capture pattern ogg header 2 */\n    (byte) 0xa9, (byte) 0x01, (byte) 0x4f, (byte) 0x67, // .,.,O,g  : start pos 86\n    (byte) 0x67, (byte) 0x53, (byte) 0x00, (byte) 0x00, // g,S,.,.\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x5e, (byte) 0x5f, (byte) 0x00, (byte) 0x00,\n    (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x69, (byte) 0xf8, (byte) 0xeb, (byte) 0xe1,\n    (byte) 0x10, (byte) 0x2d, (byte) 0xff, (byte) 0xff,\n    (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,\n    (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,\n    (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, /* capture pattern vorbis comment header*/\n    (byte) 0x1b, (byte) 0x03, (byte) 0x76, (byte) 0x6f, // .,3,v,o  : start pos 101\n    (byte) 0x72, (byte) 0x62, (byte) 0x69, (byte) 0x73, // r,b,i,s\n    (byte) 0x1d, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x58, (byte) 0x69, (byte) 0x70, (byte) 0x68,\n    (byte) 0x2e, (byte) 0x4f, (byte) 0x72, (byte) 0x67,\n    (byte) 0x20, (byte) 0x6c, (byte) 0x69, (byte) 0x62,\n    (byte) 0x56, (byte) 0x6f, (byte) 0x72, (byte) 0x62,\n    (byte) 0x69, (byte) 0x73, (byte) 0x20, (byte) 0x49,\n    (byte) 0x20, (byte) 0x32, (byte) 0x30, (byte) 0x30,\n    (byte) 0x33, (byte) 0x30, (byte) 0x39, (byte) 0x30,\n    (byte) 0x39, (byte) 0x00, (byte) 0x00, (byte) 0x00, /* capture pattern vorbis setup header */\n    (byte) 0x00, (byte) 0x01, (byte) 0x05, (byte) 0x76, // .,.,5,v  : start pos 146\n    (byte) 0x6f, (byte) 0x72, (byte) 0x62, (byte) 0x69, // o,r,b,i\n    (byte) 0x73, (byte) 0x22, (byte) 0x42, (byte) 0x43, // s,.\n    (byte) 0x56, (byte) 0x01, (byte) 0x00, (byte) 0x40,\n    (byte) 0x00, (byte) 0x00, (byte) 0x18, (byte) 0x42,\n    (byte) 0x10, (byte) 0x2a, (byte) 0x05, (byte) 0xad,\n    (byte) 0x63, (byte) 0x8e, (byte) 0x3a, (byte) 0xc8,\n    (byte) 0x15, (byte) 0x21, (byte) 0x8c, (byte) 0x19,\n    (byte) 0xa2, (byte) 0xa0, (byte) 0x42, (byte) 0xca,\n    (byte) 0x29, (byte) 0xc7, (byte) 0x1d, (byte) 0x42,\n    (byte) 0xd0, (byte) 0x21, (byte) 0xa3, (byte) 0x24,\n    (byte) 0x43, (byte) 0x88, (byte) 0x3a, (byte) 0xc6,\n    (byte) 0x35, (byte) 0xc7, (byte) 0x18, (byte) 0x63,\n    (byte) 0x47, (byte) 0xb9, (byte) 0x64, (byte) 0x8a,\n    (byte) 0x42, (byte) 0xc9, (byte) 0x81, (byte) 0xd0,\n    (byte) 0x90, (byte) 0x55, (byte) 0x00, (byte) 0x00,\n    (byte) 0x40, (byte) 0x00, (byte) 0x00, (byte) 0xa4,\n    (byte) 0x1c, (byte) 0x57, (byte) 0x50, (byte) 0x72,\n    (byte) 0x49, (byte) 0x2d, (byte) 0xe7, (byte) 0x9c,\n    (byte) 0x73, (byte) 0xa3, (byte) 0x18, (byte) 0x57,\n    (byte) 0xcc, (byte) 0x71, (byte) 0xe8, (byte) 0x20,\n    (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xe5,\n    (byte) 0x20, (byte) 0x67, (byte) 0xcc, (byte) 0x71,\n    (byte) 0x09, (byte) 0x25, (byte) 0xe7, (byte) 0x9c,\n    (byte) 0x73, (byte) 0x8e, (byte) 0x39, (byte) 0xe7,\n    (byte) 0x92, (byte) 0x72, (byte) 0x8e, (byte) 0x31,\n    (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xa3,\n    (byte) 0x18, (byte) 0x57, (byte) 0x0e, (byte) 0x72,\n    (byte) 0x29, (byte) 0x2d, (byte) 0xe7, (byte) 0x9c,\n    (byte) 0x73, (byte) 0x81, (byte) 0x14, (byte) 0x47,\n    (byte) 0x8a, (byte) 0x71, (byte) 0xa7, (byte) 0x18,\n    (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xa4,\n    (byte) 0x1c, (byte) 0x47, (byte) 0x8a, (byte) 0x71,\n    (byte) 0xa8, (byte) 0x18, (byte) 0xe7, (byte) 0x9c,\n    (byte) 0x73, (byte) 0x6d, (byte) 0x31, (byte) 0xb7,\n    (byte) 0x92, (byte) 0x72, (byte) 0xce, (byte) 0x39,\n    (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xe6,\n    (byte) 0x20, (byte) 0x87, (byte) 0x52, (byte) 0x72,\n    (byte) 0xae, (byte) 0x35, (byte) 0xe7, (byte) 0x9c,\n    (byte) 0x73, (byte) 0xa4, (byte) 0x18, (byte) 0x67,\n    (byte) 0x0e, (byte) 0x72, (byte) 0x0b, (byte) 0x25,\n    (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xc6,\n    (byte) 0x20, (byte) 0x67, (byte) 0xcc, (byte) 0x71,\n    (byte) 0xeb, (byte) 0x20, (byte) 0xe7, (byte) 0x9c,\n    (byte) 0x73, (byte) 0x8c, (byte) 0x35, (byte) 0xb7,\n    (byte) 0xd4, (byte) 0x72, (byte) 0xce, (byte) 0x39,\n    (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xce,\n    (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x73,\n    (byte) 0xce, (byte) 0x39, (byte) 0xe7, (byte) 0x9c,\n    (byte) 0x73, (byte) 0x8c, (byte) 0x31, (byte) 0xe7,\n    (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39,\n    (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0x6e,\n    (byte) 0x31, (byte) 0xe7, (byte) 0x16, (byte) 0x73,\n    (byte) 0xae, (byte) 0x39, (byte) 0xe7, (byte) 0x9c,\n    (byte) 0x73, (byte) 0xce, (byte) 0x39, (byte) 0xe7,\n    (byte) 0x1c, (byte) 0x73, (byte) 0xce, (byte) 0x39,\n    (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0x20,\n    (byte) 0x34, (byte) 0x64, (byte) 0x15, (byte) 0x00,\n    (byte) 0x90, (byte) 0x00, (byte) 0x00, (byte) 0xa0,\n    (byte) 0xa1, (byte) 0x28, (byte) 0x8a, (byte) 0xe2,\n    (byte) 0x28, (byte) 0x0e, (byte) 0x10, (byte) 0x1a,\n    (byte) 0xb2, (byte) 0x0a, (byte) 0x00, (byte) 0xc8,\n    (byte) 0x00, (byte) 0x00, (byte) 0x10, (byte) 0x40,\n    (byte) 0x71, (byte) 0x14, (byte) 0x47, (byte) 0x91,\n    (byte) 0x14, (byte) 0x4b, (byte) 0xb1, (byte) 0x1c,\n    (byte) 0xcb, (byte) 0xd1, (byte) 0x24, (byte) 0x0d,\n    (byte) 0x08, (byte) 0x0d, (byte) 0x59, (byte) 0x05,\n    (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00,\n    (byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0xa0,\n    (byte) 0x48, (byte) 0x86, (byte) 0xa4, (byte) 0x48,\n    (byte) 0x8a, (byte) 0xa5, (byte) 0x58, (byte) 0x8e,\n    (byte) 0x66, (byte) 0x69, (byte) 0x9e, (byte) 0x26,\n    (byte) 0x7a, (byte) 0xa2, (byte) 0x28, (byte) 0x9a,\n    (byte) 0xa2, (byte) 0x2a, (byte) 0xab, (byte) 0xb2,\n    (byte) 0x69, (byte) 0xca, (byte) 0xb2, (byte) 0x2c,\n    (byte) 0xcb, (byte) 0xb2, (byte) 0xeb, (byte) 0xba,\n    (byte) 0x2e, (byte) 0x10, (byte) 0x1a, (byte) 0xb2,\n    (byte) 0x0a, (byte) 0x00, (byte) 0x48, (byte) 0x00,\n    (byte) 0x00, (byte) 0x50, (byte) 0x51, (byte) 0x14,\n    (byte) 0xc5, (byte) 0x70, (byte) 0x14, (byte) 0x07,\n    (byte) 0x08, (byte) 0x0d, (byte) 0x59, (byte) 0x05,\n    (byte) 0x00, (byte) 0x64, (byte) 0x00, (byte) 0x00,\n    (byte) 0x08, (byte) 0x60, (byte) 0x28, (byte) 0x8a,\n    (byte) 0xa3, (byte) 0x38, (byte) 0x8e, (byte) 0xe4,\n    (byte) 0x58, (byte) 0x92, (byte) 0xa5, (byte) 0x59,\n    (byte) 0x9e, (byte) 0x07, (byte) 0x84, (byte) 0x86,\n    (byte) 0xac, (byte) 0x02, (byte) 0x00, (byte) 0x80,\n    (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x00,\n    (byte) 0x00, (byte) 0x50, (byte) 0x0c, (byte) 0x47,\n    (byte) 0xb1, (byte) 0x14, (byte) 0x4d, (byte) 0xf1,\n    (byte) 0x24, (byte) 0xcf, (byte) 0xf2, (byte) 0x3c,\n    (byte) 0xcf, (byte) 0xf3, (byte) 0x3c, (byte) 0xcf,\n    (byte) 0xf3, (byte) 0x3c, (byte) 0xcf, (byte) 0xf3,\n    (byte) 0x3c, (byte) 0xcf, (byte) 0xf3, (byte) 0x3c,\n    (byte) 0xcf, (byte) 0xf3, (byte) 0x3c, (byte) 0xcf,\n    (byte) 0xf3, (byte) 0x3c, (byte) 0x0d, (byte) 0x08,\n    (byte) 0x0d, (byte) 0x59, (byte) 0x05, (byte) 0x00,\n    (byte) 0x20, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x82, (byte) 0x28, (byte) 0x64, (byte) 0x18,\n    (byte) 0x03, (byte) 0x42, (byte) 0x43, (byte) 0x56,\n    (byte) 0x01, (byte) 0x00, (byte) 0x40, (byte) 0x00,\n    (byte) 0x00, (byte) 0x08, (byte) 0x21, (byte) 0x1a,\n    (byte) 0x19, (byte) 0x43, (byte) 0x9d, (byte) 0x52,\n    (byte) 0x12, (byte) 0x5c, (byte) 0x0a, (byte) 0x16,\n    (byte) 0x42, (byte) 0x1c, (byte) 0x11, (byte) 0x43,\n    (byte) 0x1d, (byte) 0x42, (byte) 0xce, (byte) 0x43,\n    (byte) 0xa9, (byte) 0xa5, (byte) 0x83, (byte) 0xe0,\n    (byte) 0x29, (byte) 0x85, (byte) 0x25, (byte) 0x63,\n    (byte) 0xd2, (byte) 0x53, (byte) 0xac, (byte) 0x41,\n    (byte) 0x08, (byte) 0x21, (byte) 0x7c, (byte) 0xef,\n    (byte) 0x3d, (byte) 0xf7, (byte) 0xde, (byte) 0x7b,\n    (byte) 0xef, (byte) 0x81, (byte) 0xd0, (byte) 0x90,\n    (byte) 0x55, (byte) 0x00, (byte) 0x00, (byte) 0x10,\n    (byte) 0x00, (byte) 0x00, (byte) 0x61, (byte) 0x14,\n    (byte) 0x38, (byte) 0x88, (byte) 0x81, (byte) 0xc7,\n    (byte) 0x24, (byte) 0x08, (byte) 0x21, (byte) 0x84,\n    (byte) 0x62, (byte) 0x14, (byte) 0x27, (byte) 0x44,\n    (byte) 0x71, (byte) 0xa6, (byte) 0x20, (byte) 0x08,\n    (byte) 0x21, (byte) 0x84, (byte) 0xe5, (byte) 0x24,\n    (byte) 0x58, (byte) 0xca, (byte) 0x79, (byte) 0xe8,\n    (byte) 0x24, (byte) 0x08, (byte) 0xdd, (byte) 0x83,\n    (byte) 0x10, (byte) 0x42, (byte) 0xb8, (byte) 0x9c,\n    (byte) 0x7b, (byte) 0xcb, (byte) 0xb9, (byte) 0xf7,\n    (byte) 0xde, (byte) 0x7b, (byte) 0x20, (byte) 0x34,\n    (byte) 0x64, (byte) 0x15, (byte) 0x00, (byte) 0x00,\n    (byte) 0x08, (byte) 0x00, (byte) 0xc0, (byte) 0x20,\n    (byte) 0x84, (byte) 0x10, (byte) 0x42, (byte) 0x08,\n    (byte) 0x21, (byte) 0x84, (byte) 0x10, (byte) 0x42,\n    (byte) 0x08, (byte) 0x29, (byte) 0xa4, (byte) 0x94,\n    (byte) 0x52, (byte) 0x48, (byte) 0x29, (byte) 0xa6,\n    (byte) 0x98, (byte) 0x62, (byte) 0x8a, (byte) 0x29,\n    (byte) 0xc7, (byte) 0x1c, (byte) 0x73, (byte) 0xcc,\n    (byte) 0x31, (byte) 0xc7, (byte) 0x20, (byte) 0x83,\n    (byte) 0x0c, (byte) 0x32, (byte) 0xe8, (byte) 0xa0,\n    (byte) 0x93, (byte) 0x4e, (byte) 0x3a, (byte) 0xc9,\n    (byte) 0xa4, (byte) 0x92, (byte) 0x4e, (byte) 0x3a,\n    (byte) 0xca, (byte) 0x24, (byte) 0xa3, (byte) 0x8e,\n    (byte) 0x52, (byte) 0x6b, (byte) 0x29, (byte) 0xb5,\n    (byte) 0x14, (byte) 0x53, (byte) 0x4c, (byte) 0xb1,\n    (byte) 0xe5, (byte) 0x16, (byte) 0x63, (byte) 0xad,\n    (byte) 0xb5, (byte) 0xd6, (byte) 0x9c, (byte) 0x73,\n    (byte) 0xaf, (byte) 0x41, (byte) 0x29, (byte) 0x63,\n    (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18,\n    (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6,\n    (byte) 0x18, (byte) 0x63, (byte) 0x8c, (byte) 0x31,\n    (byte) 0xc6, (byte) 0x18, (byte) 0x23, (byte) 0x08,\n    (byte) 0x0d, (byte) 0x59, (byte) 0x05, (byte) 0x00,\n    (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x10,\n    (byte) 0x06, (byte) 0x19, (byte) 0x64, (byte) 0x90,\n    (byte) 0x41, (byte) 0x08, (byte) 0x21, (byte) 0x84,\n    (byte) 0x14, (byte) 0x52, (byte) 0x48, (byte) 0x29,\n    (byte) 0xa6, (byte) 0x98, (byte) 0x72, (byte) 0xcc,\n    (byte) 0x31, (byte) 0xc7, (byte) 0x1c, (byte) 0x03,\n    (byte) 0x42, (byte) 0x43, (byte) 0x56, (byte) 0x01,\n    (byte) 0x00, (byte) 0x80, (byte) 0x00, (byte) 0x00,\n    (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x1c, (byte) 0x45, (byte) 0x52, (byte) 0x24,\n    (byte) 0x47, (byte) 0x72, (byte) 0x24, (byte) 0x47,\n    (byte) 0x92, (byte) 0x24, (byte) 0xc9, (byte) 0x92,\n    (byte) 0x2c, (byte) 0x49, (byte) 0x93, (byte) 0x3c,\n    (byte) 0xcb, (byte) 0xb3, (byte) 0x3c, (byte) 0xcb,\n    (byte) 0xb3, (byte) 0x3c, (byte) 0x4d, (byte) 0xd4,\n    (byte) 0x44, (byte) 0x4d, (byte) 0x15, (byte) 0x55,\n    (byte) 0xd5, (byte) 0x55, (byte) 0x6d, (byte) 0xd7,\n    (byte) 0xf6, (byte) 0x6d, (byte) 0x5f, (byte) 0xf6,\n    (byte) 0x6d, (byte) 0xdf, (byte) 0xd5, (byte) 0x65,\n    (byte) 0xdf, (byte) 0xf6, (byte) 0x65, (byte) 0xdb,\n    (byte) 0xd5, (byte) 0x65, (byte) 0x5d, (byte) 0x96,\n    (byte) 0x65, (byte) 0xdd, (byte) 0xb5, (byte) 0x6d,\n    (byte) 0x5d, (byte) 0xd6, (byte) 0x5d, (byte) 0x5d,\n    (byte) 0xd7, (byte) 0x75, (byte) 0x5d, (byte) 0xd7,\n    (byte) 0x75, (byte) 0x5d, (byte) 0xd7, (byte) 0x75,\n    (byte) 0x5d, (byte) 0xd7, (byte) 0x75, (byte) 0x5d,\n    (byte) 0xd7, (byte) 0x75, (byte) 0x5d, (byte) 0xd7,\n    (byte) 0x81, (byte) 0xd0, (byte) 0x90, (byte) 0x55,\n    (byte) 0x00, (byte) 0x80, (byte) 0x04, (byte) 0x00,\n    (byte) 0x80, (byte) 0x8e, (byte) 0xe4, (byte) 0x38,\n    (byte) 0x8e, (byte) 0xe4, (byte) 0x38, (byte) 0x8e,\n    (byte) 0xe4, (byte) 0x48, (byte) 0x8e, (byte) 0xa4,\n    (byte) 0x48, (byte) 0x0a, (byte) 0x10, (byte) 0x1a,\n    (byte) 0xb2, (byte) 0x0a, (byte) 0x00, (byte) 0x90,\n    (byte) 0x01, (byte) 0x00, (byte) 0x10, (byte) 0x00,\n    (byte) 0x80, (byte) 0xa3, (byte) 0x38, (byte) 0x8a,\n    (byte) 0xe3, (byte) 0x48, (byte) 0x8e, (byte) 0xe4,\n    (byte) 0x58, (byte) 0x8e, (byte) 0x25, (byte) 0x59,\n    (byte) 0x92, (byte) 0x26, (byte) 0x69, (byte) 0x96,\n    (byte) 0x67, (byte) 0x79, (byte) 0x96, (byte) 0xa7,\n    (byte) 0x79, (byte) 0x9a, (byte) 0xa8, (byte) 0x89,\n    (byte) 0x1e, (byte) 0x10, (byte) 0x1a, (byte) 0xb2,\n    (byte) 0x0a, (byte) 0x00, (byte) 0x00, (byte) 0x04,\n    (byte) 0x00, (byte) 0x10, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80,\n    (byte) 0xa2, (byte) 0x28, (byte) 0x8a, (byte) 0xa3,\n    (byte) 0x38, (byte) 0x8e, (byte) 0x24, (byte) 0x59,\n    (byte) 0x96, (byte) 0xa6, (byte) 0x69, (byte) 0x9e,\n    (byte) 0xa7, (byte) 0x7a, (byte) 0xa2, (byte) 0x28,\n    (byte) 0x9a, (byte) 0xaa, (byte) 0xaa, (byte) 0x8a,\n    (byte) 0xa6, (byte) 0xa9, (byte) 0xaa, (byte) 0xaa,\n    (byte) 0x6a, (byte) 0x9a, (byte) 0xa6, (byte) 0x69,\n    (byte) 0x9a, (byte) 0xa6, (byte) 0x69, (byte) 0x9a,\n    (byte) 0xa6, (byte) 0x69, (byte) 0x9a, (byte) 0xa6,\n    (byte) 0x69, (byte) 0x9a, (byte) 0xa6, (byte) 0x69,\n    (byte) 0x9a, (byte) 0xa6, (byte) 0x69, (byte) 0x9a,\n    (byte) 0xa6, (byte) 0x69, (byte) 0x9a, (byte) 0xa6,\n    (byte) 0x69, (byte) 0x9a, (byte) 0xa6, (byte) 0x69,\n    (byte) 0x9a, (byte) 0xa6, (byte) 0x69, (byte) 0x9a,\n    (byte) 0xa6, (byte) 0x69, (byte) 0x02, (byte) 0xa1,\n    (byte) 0x21, (byte) 0xab, (byte) 0x00, (byte) 0x00,\n    (byte) 0x09, (byte) 0x00, (byte) 0x00, (byte) 0x1d,\n    (byte) 0xc7, (byte) 0x71, (byte) 0x1c, (byte) 0x47,\n    (byte) 0x71, (byte) 0x1c, (byte) 0xc7, (byte) 0x71,\n    (byte) 0x24, (byte) 0x47, (byte) 0x92, (byte) 0x24,\n    (byte) 0x20, (byte) 0x34, (byte) 0x64, (byte) 0x15,\n    (byte) 0x00, (byte) 0x20, (byte) 0x03, (byte) 0x00,\n    (byte) 0x20, (byte) 0x00, (byte) 0x00, (byte) 0x43,\n    (byte) 0x51, (byte) 0x1c, (byte) 0x45, (byte) 0x72,\n    (byte) 0x2c, (byte) 0xc7, (byte) 0x92, (byte) 0x34,\n    (byte) 0x4b, (byte) 0xb3, (byte) 0x3c, (byte) 0xcb,\n    (byte) 0xd3, (byte) 0x44, (byte) 0xcf, (byte) 0xf4,\n    (byte) 0x5c, (byte) 0x51, (byte) 0x36, (byte) 0x75,\n    (byte) 0x53, (byte) 0x57, (byte) 0x6d, (byte) 0x20,\n    (byte) 0x34, (byte) 0x64, (byte) 0x15, (byte) 0x00,\n    (byte) 0x00, (byte) 0x08, (byte) 0x00, (byte) 0x20,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0xc7, (byte) 0x73,\n    (byte) 0x3c, (byte) 0xc7, (byte) 0x73, (byte) 0x3c,\n    (byte) 0xc9, (byte) 0x93, (byte) 0x3c, (byte) 0xcb,\n    (byte) 0x73, (byte) 0x3c, (byte) 0xc7, (byte) 0x93,\n    (byte) 0x3c, (byte) 0x49, (byte) 0xd3, (byte) 0x34,\n    (byte) 0x4d, (byte) 0xd3, (byte) 0x34, (byte) 0x4d,\n    (byte) 0xd3, (byte) 0x34, (byte) 0x4d, (byte) 0xd3,\n    (byte) 0x34, (byte) 0x4d, (byte) 0xd3, (byte) 0x34,\n    (byte) 0x4d, (byte) 0xd3, (byte) 0x34, (byte) 0x4d,\n    (byte) 0xd3, (byte) 0x34, (byte) 0x4d, (byte) 0xd3,\n    (byte) 0x34, (byte) 0x4d, (byte) 0xd3, (byte) 0x34,\n    (byte) 0x4d, (byte) 0xd3, (byte) 0x34, (byte) 0x4d,\n    (byte) 0xd3, (byte) 0x34, (byte) 0x4d, (byte) 0xd3,\n    (byte) 0x34, (byte) 0x4d, (byte) 0xd3, (byte) 0x34,\n    (byte) 0x4d, (byte) 0xd3, (byte) 0x34, (byte) 0x4d,\n    (byte) 0x03, (byte) 0x42, (byte) 0x43, (byte) 0x56,\n    (byte) 0x02, (byte) 0x00, (byte) 0x64, (byte) 0x00,\n    (byte) 0x00, (byte) 0x90, (byte) 0x02, (byte) 0xcf,\n    (byte) 0x42, (byte) 0x29, (byte) 0x2d, (byte) 0x46,\n    (byte) 0x02, (byte) 0x1c, (byte) 0x88, (byte) 0x98,\n    (byte) 0xa3, (byte) 0xd8, (byte) 0x7b, (byte) 0xef,\n    (byte) 0xbd, (byte) 0xf7, (byte) 0xde, (byte) 0x7b,\n    (byte) 0x65, (byte) 0x3c, (byte) 0x92, (byte) 0x88,\n    (byte) 0x49, (byte) 0xed, (byte) 0x31, (byte) 0xf4,\n    (byte) 0xd4, (byte) 0x31, (byte) 0x07, (byte) 0xb1,\n    (byte) 0x67, (byte) 0xc6, (byte) 0x23, (byte) 0x66,\n    (byte) 0x94, (byte) 0xa3, (byte) 0xd8, (byte) 0x29,\n    (byte) 0xcf, (byte) 0x1c, (byte) 0x42, (byte) 0x0c,\n    (byte) 0x62, (byte) 0xe8, (byte) 0x3c, (byte) 0x74,\n    (byte) 0x4a, (byte) 0x31, (byte) 0x88, (byte) 0x29,\n    (byte) 0xf5, (byte) 0x52, (byte) 0x32, (byte) 0xc6,\n    (byte) 0x20, (byte) 0xc6, (byte) 0xd8, (byte) 0x63,\n    (byte) 0x0c, (byte) 0x21, (byte) 0x94, (byte) 0x18,\n    (byte) 0x08, (byte) 0x0d, (byte) 0x59, (byte) 0x21,\n    (byte) 0x00, (byte) 0x84, (byte) 0x66, (byte) 0x00,\n    (byte) 0x18, (byte) 0x24, (byte) 0x09, (byte) 0x90,\n    (byte) 0x34, (byte) 0x0d, (byte) 0x90, (byte) 0x34,\n    (byte) 0x0d, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x24, (byte) 0x4f, (byte) 0x03, (byte) 0x34,\n    (byte) 0x51, (byte) 0x04, (byte) 0x34, (byte) 0x4f,\n    (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x49, (byte) 0xf3, (byte) 0x00, (byte) 0x4d,\n    (byte) 0xf4, (byte) 0x00, (byte) 0x4d, (byte) 0x14,\n    (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x90, (byte) 0x3c, (byte) 0x0d, (byte) 0xf0,\n    (byte) 0x44, (byte) 0x11, (byte) 0xd0, (byte) 0x44,\n    (byte) 0x11, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x34, (byte) 0x51, (byte) 0x04, (byte) 0x44,\n    (byte) 0x51, (byte) 0x05, (byte) 0x44, (byte) 0xd5,\n    (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x4d, (byte) 0x14, (byte) 0x01, (byte) 0x4f,\n    (byte) 0x15, (byte) 0x01, (byte) 0xd1, (byte) 0x54,\n    (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x90, (byte) 0x34, (byte) 0x0f, (byte) 0xd0,\n    (byte) 0x44, (byte) 0x11, (byte) 0xf0, (byte) 0x44,\n    (byte) 0x11, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x34, (byte) 0x51, (byte) 0x04, (byte) 0x44,\n    (byte) 0xd5, (byte) 0x04, (byte) 0x3c, (byte) 0x51,\n    (byte) 0x05, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x4d, (byte) 0x14, (byte) 0x01, (byte) 0xd1,\n    (byte) 0x54, (byte) 0x01, (byte) 0x51, (byte) 0x15,\n    (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x04,\n    (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x38,\n    (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x58,\n    (byte) 0x08, (byte) 0x85, (byte) 0x86, (byte) 0xac,\n    (byte) 0x08, (byte) 0x00, (byte) 0xe2, (byte) 0x04,\n    (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10,\n    (byte) 0x00, (byte) 0x00, (byte) 0x30, (byte) 0xe0,\n    (byte) 0x00, (byte) 0x00, (byte) 0x10, (byte) 0x60,\n    (byte) 0x42, (byte) 0x19, (byte) 0x28, (byte) 0x34,\n    (byte) 0x64, (byte) 0x45, (byte) 0x00, (byte) 0x10,\n    (byte) 0x27, (byte) 0x00, (byte) 0x60, (byte) 0x70,\n    (byte) 0x1c, (byte) 0xcb, (byte) 0x02, (byte) 0x00,\n    (byte) 0x00, (byte) 0x47, (byte) 0x92, (byte) 0x34,\n    (byte) 0x0d, (byte) 0x00, (byte) 0x00, (byte) 0x1c,\n    (byte) 0x49, (byte) 0xd2, (byte) 0x34, (byte) 0x00,\n    (byte) 0x00, (byte) 0xd0, (byte) 0x34, (byte) 0x4d,\n    (byte) 0x14, (byte) 0x01, (byte) 0x00, (byte) 0xc0,\n    (byte) 0xd2, (byte) 0x34, (byte) 0x51, (byte) 0x04,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x30,\n    (byte) 0xe0, (byte) 0x00, (byte) 0x00, (byte) 0x10,\n    (byte) 0x60, (byte) 0x42, (byte) 0x19, (byte) 0x28,\n    (byte) 0x34, (byte) 0x64, (byte) 0x25, (byte) 0x00,\n    (byte) 0x10, (byte) 0x05, (byte) 0x00, (byte) 0x60,\n    (byte) 0x30, (byte) 0x14, (byte) 0x4d, (byte) 0x03,\n    (byte) 0x58, (byte) 0x16, (byte) 0xc0, (byte) 0xb2,\n    (byte) 0x00, (byte) 0x9a, (byte) 0x06, (byte) 0xd0,\n    (byte) 0x34, (byte) 0x80, (byte) 0xe7, (byte) 0x01,\n    (byte) 0x3c, (byte) 0x11, (byte) 0x60, (byte) 0x9a,\n    (byte) 0x00, (byte) 0x40, (byte) 0x00, (byte) 0x00,\n    (byte) 0x40, (byte) 0x81, (byte) 0x03, (byte) 0x00,\n    (byte) 0x40, (byte) 0x80, (byte) 0x0d, (byte) 0x9a,\n    (byte) 0x12, (byte) 0x8b, (byte) 0x03, (byte) 0x14,\n    (byte) 0x1a, (byte) 0xb2, (byte) 0x12, (byte) 0x00,\n    (byte) 0x88, (byte) 0x02, (byte) 0x00, (byte) 0x30,\n    (byte) 0x28, (byte) 0x8a, (byte) 0x24, (byte) 0x59,\n    (byte) 0x96, (byte) 0xe7, (byte) 0x41, (byte) 0xd3,\n    (byte) 0x34, (byte) 0x4d, (byte) 0x14, (byte) 0xa1,\n    (byte) 0x69, (byte) 0x9a, (byte) 0x26, (byte) 0x8a,\n    (byte) 0xf0, (byte) 0x3c, (byte) 0xcf, (byte) 0x13,\n    (byte) 0x45, (byte) 0x78, (byte) 0x9e, (byte) 0xe7,\n    (byte) 0x99, (byte) 0x26, (byte) 0x44, (byte) 0xd1,\n    (byte) 0xf3, (byte) 0x4c, (byte) 0x13, (byte) 0xa2,\n    (byte) 0xe8, (byte) 0x79, (byte) 0xa6, (byte) 0x09,\n    (byte) 0xd3, (byte) 0x14, (byte) 0x45, (byte) 0xd3,\n    (byte) 0x04, (byte) 0xa2, (byte) 0x68, (byte) 0x9a,\n    (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x0a,\n    (byte) 0x1c, (byte) 0x00, (byte) 0x00, (byte) 0x02,\n    (byte) 0x6c, (byte) 0xd0, (byte) 0x94, (byte) 0x58,\n    (byte) 0x1c, (byte) 0xa0, (byte) 0xd0, (byte) 0x90,\n    (byte) 0x95, (byte) 0x00, (byte) 0x40, (byte) 0x48,\n    (byte) 0x00, (byte) 0x80, (byte) 0x41, (byte) 0x51,\n    (byte) 0x2c, (byte) 0xcb, (byte) 0xf3, (byte) 0x44,\n    (byte) 0x51, (byte) 0x14, (byte) 0x4d, (byte) 0x53,\n    (byte) 0x55, (byte) 0x5d, (byte) 0x17, (byte) 0x9a,\n    (byte) 0xe6, (byte) 0x79, (byte) 0xa2, (byte) 0x28,\n    (byte) 0x8a, (byte) 0xa6, (byte) 0xa9, (byte) 0xaa,\n    (byte) 0xae, (byte) 0x0b, (byte) 0x4d, (byte) 0xf3,\n    (byte) 0x3c, (byte) 0x51, (byte) 0x14, (byte) 0x45,\n    (byte) 0xd3, (byte) 0x54, (byte) 0x55, (byte) 0xd7,\n    (byte) 0x85, (byte) 0xe7, (byte) 0x79, (byte) 0xa2,\n    (byte) 0x29, (byte) 0x9a, (byte) 0xa6, (byte) 0x69,\n    (byte) 0xaa, (byte) 0xaa, (byte) 0xeb, (byte) 0xc2,\n    (byte) 0xf3, (byte) 0x44, (byte) 0xd1, (byte) 0x34,\n    (byte) 0x4d, (byte) 0x53, (byte) 0x55, (byte) 0x55,\n    (byte) 0xd7, (byte) 0x75, (byte) 0xe1, (byte) 0x79,\n    (byte) 0xa2, (byte) 0x68, (byte) 0x9a, (byte) 0xa6,\n    (byte) 0xa9, (byte) 0xaa, (byte) 0xae, (byte) 0xeb,\n    (byte) 0xba, (byte) 0xf0, (byte) 0x3c, (byte) 0x51,\n    (byte) 0x34, (byte) 0x4d, (byte) 0xd3, (byte) 0x54,\n    (byte) 0x55, (byte) 0xd7, (byte) 0x95, (byte) 0x65,\n    (byte) 0x88, (byte) 0xa2, (byte) 0x28, (byte) 0x9a,\n    (byte) 0xa6, (byte) 0x69, (byte) 0xaa, (byte) 0xaa,\n    (byte) 0xeb, (byte) 0xca, (byte) 0x32, (byte) 0x10,\n    (byte) 0x45, (byte) 0xd3, (byte) 0x34, (byte) 0x4d,\n    (byte) 0x55, (byte) 0x75, (byte) 0x5d, (byte) 0x59,\n    (byte) 0x06, (byte) 0xa2, (byte) 0x68, (byte) 0x9a,\n    (byte) 0xaa, (byte) 0xea, (byte) 0xba, (byte) 0xae,\n    (byte) 0x2b, (byte) 0xcb, (byte) 0x40, (byte) 0x14,\n    (byte) 0x4d, (byte) 0x53, (byte) 0x55, (byte) 0x5d,\n    (byte) 0xd7, (byte) 0x75, (byte) 0x65, (byte) 0x19,\n    (byte) 0x98, (byte) 0xa6, (byte) 0x6a, (byte) 0xaa,\n    (byte) 0xaa, (byte) 0xeb, (byte) 0xca, (byte) 0xb2,\n    (byte) 0x2c, (byte) 0x03, (byte) 0x4c, (byte) 0x53,\n    (byte) 0x55, (byte) 0x5d, (byte) 0x57, (byte) 0x96,\n    (byte) 0x65, (byte) 0x19, (byte) 0xa0, (byte) 0xaa,\n    (byte) 0xae, (byte) 0xeb, (byte) 0xba, (byte) 0xb2,\n    (byte) 0x6c, (byte) 0xdb, (byte) 0x00, (byte) 0x55,\n    (byte) 0x75, (byte) 0x5d, (byte) 0xd7, (byte) 0x95,\n    (byte) 0x65, (byte) 0xdb, (byte) 0x06, (byte) 0xb8,\n    (byte) 0xae, (byte) 0xeb, (byte) 0xca, (byte) 0xb2,\n    (byte) 0x2c, (byte) 0xdb, (byte) 0x36, (byte) 0x00,\n    (byte) 0xd7, (byte) 0x95, (byte) 0x65, (byte) 0x59,\n    (byte) 0xb6, (byte) 0x6d, (byte) 0x01, (byte) 0x00,\n    (byte) 0x00, (byte) 0x07, (byte) 0x0e, (byte) 0x00,\n    (byte) 0x00, (byte) 0x01, (byte) 0x46, (byte) 0xd0,\n    (byte) 0x49, (byte) 0x46, (byte) 0x95, (byte) 0x45,\n    (byte) 0xd8, (byte) 0x68, (byte) 0xc2, (byte) 0x85,\n    (byte) 0x07, (byte) 0xa0, (byte) 0xd0, (byte) 0x90,\n    (byte) 0x15, (byte) 0x01, (byte) 0x40, (byte) 0x14,\n    (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x8c,\n    (byte) 0x52, (byte) 0x8a, (byte) 0x29, (byte) 0x65,\n    (byte) 0x18, (byte) 0x93, (byte) 0x50, (byte) 0x4a,\n    (byte) 0x09, (byte) 0x0d, (byte) 0x63, (byte) 0x52,\n    (byte) 0x4a, (byte) 0x2a, (byte) 0xa5, (byte) 0x92,\n    (byte) 0x92, (byte) 0x52, (byte) 0x4a, (byte) 0xa5,\n    (byte) 0x54, (byte) 0x12, (byte) 0x52, (byte) 0x4a,\n    (byte) 0xa9, (byte) 0x94, (byte) 0x4a, (byte) 0x4a,\n    (byte) 0x4a, (byte) 0x29, (byte) 0x95, (byte) 0x92,\n    (byte) 0x51, (byte) 0x4a, (byte) 0x29, (byte) 0xb5,\n    (byte) 0x96, (byte) 0x2a, (byte) 0x29, (byte) 0xa9,\n    (byte) 0x94, (byte) 0x94, (byte) 0x52, (byte) 0x25,\n    (byte) 0xa5, (byte) 0xa4, (byte) 0x92, (byte) 0x52,\n    (byte) 0x2a, (byte) 0x00, (byte) 0x00, (byte) 0xec,\n    (byte) 0xc0, (byte) 0x01, (byte) 0x00, (byte) 0xec,\n    (byte) 0xc0, (byte) 0x42, (byte) 0x28, (byte) 0x34,\n    (byte) 0x64, (byte) 0x25, (byte) 0x00, (byte) 0x90,\n    (byte) 0x07, (byte) 0x00, (byte) 0x40, (byte) 0x10,\n    (byte) 0x82, (byte) 0x14, (byte) 0x63, (byte) 0x8c,\n    (byte) 0x39, (byte) 0x27, (byte) 0xa5, (byte) 0x54,\n    (byte) 0x8a, (byte) 0x31, (byte) 0xe7, (byte) 0x9c,\n    (byte) 0x93, (byte) 0x52, (byte) 0x2a, (byte) 0xc5,\n    (byte) 0x98, (byte) 0x73, (byte) 0xce, (byte) 0x49,\n    (byte) 0x29, (byte) 0x19, (byte) 0x63, (byte) 0xcc,\n    (byte) 0x39, (byte) 0xe7, (byte) 0xa4, (byte) 0x94,\n    (byte) 0x8c, (byte) 0x31, (byte) 0xe6, (byte) 0x9c,\n    (byte) 0x73, (byte) 0x52, (byte) 0x4a, (byte) 0xc6,\n    (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39,\n    (byte) 0x29, (byte) 0x25, (byte) 0x63, (byte) 0xce,\n    (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x94,\n    (byte) 0xd2, (byte) 0x39, (byte) 0xe7, (byte) 0x9c,\n    (byte) 0x83, (byte) 0x50, (byte) 0x4a, (byte) 0x29,\n    (byte) 0xa5, (byte) 0x73, (byte) 0xce, (byte) 0x41,\n    (byte) 0x28, (byte) 0xa5, (byte) 0x94, (byte) 0x12,\n    (byte) 0x42, (byte) 0xe7, (byte) 0x20, (byte) 0x94,\n    (byte) 0x52, (byte) 0x4a, (byte) 0xe9, (byte) 0x9c,\n    (byte) 0x73, (byte) 0x10, (byte) 0x0a, (byte) 0x00,\n    (byte) 0x00, (byte) 0x2a, (byte) 0x70, (byte) 0x00,\n    (byte) 0x00, (byte) 0x08, (byte) 0xb0, (byte) 0x51,\n    (byte) 0x64, (byte) 0x73, (byte) 0x82, (byte) 0x91,\n    (byte) 0xa0, (byte) 0x42, (byte) 0x43, (byte) 0x56,\n    (byte) 0x02, (byte) 0x00, (byte) 0xa9, (byte) 0x00,\n    (byte) 0x00, (byte) 0x06, (byte) 0xc7, (byte) 0xb1,\n    (byte) 0x2c, (byte) 0x4d, (byte) 0xd3, (byte) 0x34,\n    (byte) 0xcf, (byte) 0x13, (byte) 0x45, (byte) 0x4b,\n    (byte) 0x92, (byte) 0x34, (byte) 0xcf, (byte) 0x13,\n    (byte) 0x3d, (byte) 0x4f, (byte) 0x14, (byte) 0x4d,\n    (byte) 0xd5, (byte) 0x92, (byte) 0x24, (byte) 0xcf,\n    (byte) 0x13, (byte) 0x45, (byte) 0xcf, (byte) 0x13,\n    (byte) 0x4d, (byte) 0x53, (byte) 0xe5, (byte) 0x79,\n    (byte) 0x9e, (byte) 0x28, (byte) 0x8a, (byte) 0xa2,\n    (byte) 0x68, (byte) 0x9a, (byte) 0xaa, (byte) 0x4a,\n    (byte) 0x14, (byte) 0x45, (byte) 0x4f, (byte) 0x14,\n    (byte) 0x45, (byte) 0xd1, (byte) 0x34, (byte) 0x55,\n    (byte) 0x95, (byte) 0x2c, (byte) 0x8b, (byte) 0xa2,\n    (byte) 0x69, (byte) 0x9a, (byte) 0xa6, (byte) 0xaa,\n    (byte) 0xba, (byte) 0x2e, (byte) 0x5b, (byte) 0x16,\n    (byte) 0x45, (byte) 0xd3, (byte) 0x34, (byte) 0x4d,\n    (byte) 0x55, (byte) 0x75, (byte) 0x5d, (byte) 0x98,\n    (byte) 0xa6, (byte) 0x28, (byte) 0xaa, (byte) 0xaa,\n    (byte) 0xeb, (byte) 0xca, (byte) 0x2e, (byte) 0x4c,\n    (byte) 0x53, (byte) 0x14, (byte) 0x4d, (byte) 0xd3,\n    (byte) 0x75, (byte) 0x65, (byte) 0x19, (byte) 0xb2,\n    (byte) 0xad, (byte) 0x9a, (byte) 0xaa, (byte) 0xea,\n    (byte) 0xba, (byte) 0xb2, (byte) 0x0d, (byte) 0xdb,\n    (byte) 0x36, (byte) 0x4d, (byte) 0x55, (byte) 0x75,\n    (byte) 0x5d, (byte) 0x59, (byte) 0x06, (byte) 0xae,\n    (byte) 0xeb, (byte) 0xba, (byte) 0xb2, (byte) 0x6c,\n    (byte) 0xeb, (byte) 0xc0, (byte) 0x75, (byte) 0x5d,\n    (byte) 0x57, (byte) 0x96, (byte) 0x6d, (byte) 0x5d,\n    (byte) 0x00, (byte) 0x00, (byte) 0x78, (byte) 0x82,\n    (byte) 0x03, (byte) 0x00, (byte) 0x50, (byte) 0x81,\n    (byte) 0x0d, (byte) 0xab, (byte) 0x23, (byte) 0x9c,\n    (byte) 0x14, (byte) 0x8d, (byte) 0x05, (byte) 0x16,\n    (byte) 0x1a, (byte) 0xb2, (byte) 0x12, (byte) 0x00,\n    (byte) 0xc8, (byte) 0x00, (byte) 0x00, (byte) 0x20,\n    (byte) 0x08, (byte) 0x41, (byte) 0x48, (byte) 0x29,\n    (byte) 0x85, (byte) 0x90, (byte) 0x52, (byte) 0x0a,\n    (byte) 0x21, (byte) 0xa5, (byte) 0x14, (byte) 0x42,\n    (byte) 0x4a, (byte) 0x29, (byte) 0x84, (byte) 0x04,\n    (byte) 0x00, (byte) 0x00, (byte) 0x0c, (byte) 0x38,\n    (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x98,\n    (byte) 0x50, (byte) 0x06, (byte) 0x0a, (byte) 0x0d,\n    (byte) 0x59, (byte) 0x09, (byte) 0x00, (byte) 0xa4,\n    (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x10,\n    (byte) 0x42, (byte) 0x08, (byte) 0x21, (byte) 0x84,\n    (byte) 0x10, (byte) 0x42, (byte) 0x08, (byte) 0x21,\n    (byte) 0x84, (byte) 0x10, (byte) 0x42, (byte) 0x08,\n    (byte) 0x21, (byte) 0x84, (byte) 0x10, (byte) 0x42,\n    (byte) 0x08, (byte) 0x21, (byte) 0x84, (byte) 0x10,\n    (byte) 0x42, (byte) 0x08, (byte) 0x21, (byte) 0x84,\n    (byte) 0x10, (byte) 0x42, (byte) 0x08, (byte) 0x21,\n    (byte) 0x84, (byte) 0x10, (byte) 0x42, (byte) 0x08,\n    (byte) 0x21, (byte) 0x84, (byte) 0x10, (byte) 0x42,\n    (byte) 0x08, (byte) 0x21, (byte) 0x84, (byte) 0x10,\n    (byte) 0x42, (byte) 0x08, (byte) 0x21, (byte) 0x84,\n    (byte) 0x10, (byte) 0x42, (byte) 0x08, (byte) 0x21,\n    (byte) 0x84, (byte) 0xce, (byte) 0x39, (byte) 0xe7,\n    (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39,\n    (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xce,\n    (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x73,\n    (byte) 0xce, (byte) 0x39, (byte) 0xe7, (byte) 0x9c,\n    (byte) 0x73, (byte) 0xce, (byte) 0x39, (byte) 0xe7,\n    (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39,\n    (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xce,\n    (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x73,\n    (byte) 0xce, (byte) 0x39, (byte) 0xe7, (byte) 0x9c,\n    (byte) 0x73, (byte) 0xce, (byte) 0x39, (byte) 0xe7,\n    (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39,\n    (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xce,\n    (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x73,\n    (byte) 0x02, (byte) 0x00, (byte) 0xb1, (byte) 0x2b,\n    (byte) 0x1c, (byte) 0x00, (byte) 0x76, (byte) 0x22,\n    (byte) 0x6c, (byte) 0x58, (byte) 0x1d, (byte) 0xe1,\n    (byte) 0xa4, (byte) 0x68, (byte) 0x2c, (byte) 0xb0,\n    (byte) 0xd0, (byte) 0x90, (byte) 0x95, (byte) 0x00,\n    (byte) 0x40, (byte) 0x38, (byte) 0x00, (byte) 0x00,\n    (byte) 0x60, (byte) 0x8c, (byte) 0x31, (byte) 0xce,\n    (byte) 0x59, (byte) 0xac, (byte) 0xb5, (byte) 0xd6,\n    (byte) 0x5a, (byte) 0x2b, (byte) 0xa5, (byte) 0x94,\n    (byte) 0x92, (byte) 0x50, (byte) 0x6b, (byte) 0xad,\n    (byte) 0xb5, (byte) 0xd6, (byte) 0x9a, (byte) 0x29,\n    (byte) 0xa4, (byte) 0x94, (byte) 0x84, (byte) 0x16,\n    (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6,\n    (byte) 0x98, (byte) 0x31, (byte) 0x08, (byte) 0x29,\n    (byte) 0xb5, (byte) 0x18, (byte) 0x63, (byte) 0x8c,\n    (byte) 0x31, (byte) 0xc6, (byte) 0x8c, (byte) 0x39,\n    (byte) 0x47, (byte) 0x2d, (byte) 0xc6, (byte) 0x18,\n    (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xb6,\n    (byte) 0x56, (byte) 0x4a, (byte) 0x6c, (byte) 0x31,\n    (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c,\n    (byte) 0xb1, (byte) 0xb5, (byte) 0x52, (byte) 0x62,\n    (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18,\n    (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6,\n    (byte) 0x16, (byte) 0x5b, (byte) 0x8c, (byte) 0x31,\n    (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c,\n    (byte) 0x31, (byte) 0xb6, (byte) 0x18, (byte) 0x63,\n    (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18,\n    (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6,\n    (byte) 0x18, (byte) 0x63, (byte) 0x8c, (byte) 0x31,\n    (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c,\n    (byte) 0x31, (byte) 0xb6, (byte) 0x18, (byte) 0x63,\n    (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18,\n    (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6,\n    (byte) 0x18, (byte) 0x63, (byte) 0x8c, (byte) 0x31,\n    (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c,\n    (byte) 0x31, (byte) 0xc6, (byte) 0x18, (byte) 0x63,\n    (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18,\n    (byte) 0x63, (byte) 0x6c, (byte) 0x31, (byte) 0xc6,\n    (byte) 0x18, (byte) 0x63, (byte) 0x8c, (byte) 0x31,\n    (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c,\n    (byte) 0x31, (byte) 0xc6, (byte) 0x18, (byte) 0x63,\n    (byte) 0x2c, (byte) 0x00, (byte) 0xc0, (byte) 0xe4,\n    (byte) 0xc1, (byte) 0x01, (byte) 0x00, (byte) 0x2a,\n    (byte) 0xc1, (byte) 0xc6, (byte) 0x19, (byte) 0x56,\n    (byte) 0x92, (byte) 0xce, (byte) 0x0a, (byte) 0x47,\n    (byte) 0x83, (byte) 0x0b, (byte) 0x0d, (byte) 0x59,\n    (byte) 0x09, (byte) 0x00, (byte) 0xe4, (byte) 0x06,\n    (byte) 0x00, (byte) 0x00, (byte) 0xc6, (byte) 0x28,\n    (byte) 0xc5, (byte) 0x98, (byte) 0x63, (byte) 0xce,\n    (byte) 0x41, (byte) 0x08, (byte) 0xa1, (byte) 0x94,\n    (byte) 0x12, (byte) 0x4a, (byte) 0x49, (byte) 0xad,\n    (byte) 0x75, (byte) 0xce, (byte) 0x39, (byte) 0x08,\n    (byte) 0x21, (byte) 0x94, (byte) 0x52, (byte) 0x4a,\n    (byte) 0x49, (byte) 0xa9, (byte) 0xb4, (byte) 0x94,\n    (byte) 0x62, (byte) 0xca, (byte) 0x98, (byte) 0x73,\n    (byte) 0xce, (byte) 0x41, (byte) 0x08, (byte) 0xa5,\n    (byte) 0x94, (byte) 0x12, (byte) 0x4a, (byte) 0x49,\n    (byte) 0xa9, (byte) 0xa5, (byte) 0xd4, (byte) 0x39,\n    (byte) 0xe7, (byte) 0x20, (byte) 0x94, (byte) 0x52,\n    (byte) 0x4a, (byte) 0x4a, (byte) 0x29, (byte) 0xa5,\n    (byte) 0x94, (byte) 0x5a, (byte) 0x6a, (byte) 0xad,\n    (byte) 0x73, (byte) 0x10, (byte) 0x42, (byte) 0x08,\n    (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x4a,\n    (byte) 0x4a, (byte) 0x29, (byte) 0xa5, (byte) 0xd4,\n    (byte) 0x52, (byte) 0x08, (byte) 0x21, (byte) 0x94,\n    (byte) 0x52, (byte) 0x4a, (byte) 0x2a, (byte) 0x29,\n    (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x6b,\n    (byte) 0xad, (byte) 0xa5, (byte) 0x10, (byte) 0x42,\n    (byte) 0x28, (byte) 0xa5, (byte) 0x94, (byte) 0x94,\n    (byte) 0x52, (byte) 0x4a, (byte) 0x29, (byte) 0xa5,\n    (byte) 0xd4, (byte) 0x5a, (byte) 0x8b, (byte) 0xa1,\n    (byte) 0x94, (byte) 0x90, (byte) 0x4a, (byte) 0x29,\n    (byte) 0x25, (byte) 0xa5, (byte) 0x94, (byte) 0x52,\n    (byte) 0x49, (byte) 0x2d, (byte) 0xb5, (byte) 0x96,\n    (byte) 0x5a, (byte) 0x2a, (byte) 0xa1, (byte) 0x94,\n    (byte) 0x54, (byte) 0x52, (byte) 0x4a, (byte) 0x29,\n    (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x6b,\n    (byte) 0xa9, (byte) 0xb5, (byte) 0x56, (byte) 0x4a,\n    (byte) 0x49, (byte) 0x25, (byte) 0xa5, (byte) 0x94,\n    (byte) 0x52, (byte) 0x4a, (byte) 0x29, (byte) 0xa5,\n    (byte) 0xd4, (byte) 0x62, (byte) 0x6b, (byte) 0x29,\n    (byte) 0x94, (byte) 0x92, (byte) 0x52, (byte) 0x49,\n    (byte) 0x29, (byte) 0xb5, (byte) 0x94, (byte) 0x52,\n    (byte) 0x4a, (byte) 0xad, (byte) 0xc5, (byte) 0xd8,\n    (byte) 0x62, (byte) 0x29, (byte) 0xad, (byte) 0xa4,\n    (byte) 0x94, (byte) 0x52, (byte) 0x4a, (byte) 0x29,\n    (byte) 0xa5, (byte) 0xd6, (byte) 0x52, (byte) 0x6c,\n    (byte) 0xad, (byte) 0xb5, (byte) 0xd8, (byte) 0x52,\n    (byte) 0x4a, (byte) 0x29, (byte) 0xa5, (byte) 0x96,\n    (byte) 0x5a, (byte) 0x4a, (byte) 0x29, (byte) 0xb5,\n    (byte) 0x16, (byte) 0x5b, (byte) 0x6a, (byte) 0x2d,\n    (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x4b,\n    (byte) 0x29, (byte) 0xa5, (byte) 0x96, (byte) 0x52,\n    (byte) 0x4b, (byte) 0x2d, (byte) 0xc6, (byte) 0xd6,\n    (byte) 0x5a, (byte) 0x4b, (byte) 0x29, (byte) 0xa5,\n    (byte) 0xd4, (byte) 0x52, (byte) 0x6a, (byte) 0xa9,\n    (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x6c,\n    (byte) 0xad, (byte) 0xb5, (byte) 0x98, (byte) 0x52,\n    (byte) 0x6a, (byte) 0x2d, (byte) 0xa5, (byte) 0xd4,\n    (byte) 0x52, (byte) 0x6b, (byte) 0x2d, (byte) 0xb5,\n    (byte) 0xd8, (byte) 0x52, (byte) 0x6a, (byte) 0x2d,\n    (byte) 0xb5, (byte) 0x94, (byte) 0x52, (byte) 0x6b,\n    (byte) 0xa9, (byte) 0xa5, (byte) 0x94, (byte) 0x5a,\n    (byte) 0x6b, (byte) 0x2d, (byte) 0xb6, (byte) 0xd8,\n    (byte) 0x5a, (byte) 0x6b, (byte) 0x29, (byte) 0xb5,\n    (byte) 0x94, (byte) 0x52, (byte) 0x4a, (byte) 0xa9,\n    (byte) 0xb5, (byte) 0x16, (byte) 0x5b, (byte) 0x8a,\n    (byte) 0xb1, (byte) 0xb5, (byte) 0xd4, (byte) 0x4a,\n    (byte) 0x4a, (byte) 0x29, (byte) 0xb5, (byte) 0xd4,\n    (byte) 0x5a, (byte) 0x6a, (byte) 0x2d, (byte) 0xb6,\n    (byte) 0x16, (byte) 0x5b, (byte) 0x6b, (byte) 0xad,\n    (byte) 0xa5, (byte) 0xd6, (byte) 0x5a, (byte) 0x6a,\n    (byte) 0x29, (byte) 0xa5, (byte) 0x16, (byte) 0x5b,\n    (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x16,\n    (byte) 0x63, (byte) 0x6b, (byte) 0x31, (byte) 0xa5,\n    (byte) 0x94, (byte) 0x52, (byte) 0x4b, (byte) 0xa9,\n    (byte) 0xa5, (byte) 0x02, (byte) 0x00, (byte) 0x80,\n    (byte) 0x0e, (byte) 0x1c, (byte) 0x00, (byte) 0x00,\n    (byte) 0x02, (byte) 0x8c, (byte) 0xa8, (byte) 0xb4,\n    (byte) 0x10, (byte) 0x3b, (byte) 0xcd, (byte) 0xb8,\n    (byte) 0xf2, (byte) 0x08, (byte) 0x1c, (byte) 0x51,\n    (byte) 0xc8, (byte) 0x30, (byte) 0x01, (byte) 0x15,\n    (byte) 0x1a, (byte) 0xb2, (byte) 0x12, (byte) 0x00,\n    (byte) 0x20, (byte) 0x03, (byte) 0x00, (byte) 0x20,\n    (byte) 0x90, (byte) 0x69, (byte) 0x92, (byte) 0x39,\n    (byte) 0x49, (byte) 0xa9, (byte) 0x11, (byte) 0x26,\n    (byte) 0x39, (byte) 0xc5, (byte) 0xa0, (byte) 0x94,\n    (byte) 0xe6, (byte) 0x9c, (byte) 0x53, (byte) 0x4a,\n    (byte) 0x29, (byte) 0xa5, (byte) 0x34, (byte) 0x44,\n    (byte) 0x96, (byte) 0x64, (byte) 0x90, (byte) 0x62,\n    (byte) 0x50, (byte) 0x1d, (byte) 0x99, (byte) 0x8c,\n    (byte) 0x39, (byte) 0x49, (byte) 0x39, (byte) 0x43,\n    (byte) 0xa4, (byte) 0x31, (byte) 0xa4, (byte) 0x20,\n    (byte) 0xf5, (byte) 0x4c, (byte) 0x91, (byte) 0xc7,\n    (byte) 0x94, (byte) 0x62, (byte) 0x10, (byte) 0x43,\n    (byte) 0x48, (byte) 0x2a, (byte) 0x74, (byte) 0x8a,\n    (byte) 0x39, (byte) 0x6c, (byte) 0x35, (byte) 0xf9,\n    (byte) 0x58, (byte) 0x42, (byte) 0x07, (byte) 0xb1,\n    (byte) 0x06, (byte) 0x65, (byte) 0x8c, (byte) 0x70,\n    (byte) 0x29, (byte) 0xc5, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x08, (byte) 0x02, (byte) 0x00,\n    (byte) 0x04, (byte) 0x84, (byte) 0x04, (byte) 0x00,\n    (byte) 0x18, (byte) 0x20, (byte) 0x28, (byte) 0x98,\n    (byte) 0x01, (byte) 0x00, (byte) 0x06, (byte) 0x07,\n    (byte) 0x08, (byte) 0x23, (byte) 0x07, (byte) 0x02,\n    (byte) 0x1d, (byte) 0x01, (byte) 0x04, (byte) 0x0e,\n    (byte) 0x6d, (byte) 0x00, (byte) 0x80, (byte) 0x81,\n    (byte) 0x08, (byte) 0x99, (byte) 0x09, (byte) 0x0c,\n    (byte) 0x0a, (byte) 0xa1, (byte) 0xc1, (byte) 0x41,\n    (byte) 0x26, (byte) 0x00, (byte) 0x3c, (byte) 0x40,\n    (byte) 0x44, (byte) 0x48, (byte) 0x05, (byte) 0x00,\n    (byte) 0x89, (byte) 0x09, (byte) 0x8a, (byte) 0xd2,\n    (byte) 0x85, (byte) 0x2e, (byte) 0x08, (byte) 0x21,\n    (byte) 0x82, (byte) 0x74, (byte) 0x11, (byte) 0x64,\n    (byte) 0xf1, (byte) 0xc0, (byte) 0x85, (byte) 0x13,\n    (byte) 0x37, (byte) 0x9e, (byte) 0xb8, (byte) 0xe1,\n    (byte) 0x84, (byte) 0x0e, (byte) 0x6d, (byte) 0x20,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0xf0,\n    (byte) 0x01, (byte) 0x00, (byte) 0x90, (byte) 0x50,\n    (byte) 0x00, (byte) 0x11, (byte) 0x11, (byte) 0xd1,\n    (byte) 0xcc, (byte) 0x55, (byte) 0x58, (byte) 0x5c,\n    (byte) 0x60, (byte) 0x64, (byte) 0x68, (byte) 0x6c,\n    (byte) 0x70, (byte) 0x74, (byte) 0x78, (byte) 0x7c,\n    (byte) 0x80, (byte) 0x84, (byte) 0x08, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x10, (byte) 0x00, (byte) 0x7c, (byte) 0x00,\n    (byte) 0x00, (byte) 0x24, (byte) 0x22, (byte) 0x40,\n    (byte) 0x44, (byte) 0x44, (byte) 0x34, (byte) 0x73,\n    (byte) 0x15, (byte) 0x16, (byte) 0x17, (byte) 0x18,\n    (byte) 0x19, (byte) 0x1a, (byte) 0x1b, (byte) 0x1c,\n    (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, (byte) 0x20,\n    (byte) 0x21, (byte) 0x01, (byte) 0x00, (byte) 0x80,\n    (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x00, (byte) 0x20, (byte) 0x80,\n    (byte) 0x00, (byte) 0x04, (byte) 0x04, (byte) 0x04,\n    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00,\n    (byte) 0x00, (byte) 0x04, (byte) 0x04\n  };\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.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.testutil;\n\nimport static org.robolectric.Shadows.shadowOf;\nimport static org.robolectric.util.ReflectionHelpers.callInstanceMethod;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\nimport android.os.MessageQueue;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport com.google.android.exoplayer2.util.Util;\nimport java.util.concurrent.CopyOnWriteArraySet;\nimport java.util.concurrent.PriorityBlockingQueue;\nimport java.util.concurrent.atomic.AtomicLong;\nimport org.robolectric.annotation.Implementation;\nimport org.robolectric.annotation.Implements;\nimport org.robolectric.shadows.ShadowLegacyLooper;\nimport org.robolectric.shadows.ShadowLegacyMessageQueue;\nimport org.robolectric.shadows.ShadowLooper;\n\n/** Collection of shadow classes used to run tests with Robolectric which require Loopers. */\npublic final class RobolectricUtil {\n\n  private static final AtomicLong sequenceNumberGenerator = new AtomicLong(0);\n  private static final int ANY_MESSAGE = Integer.MIN_VALUE;\n\n  private RobolectricUtil() {}\n\n  /**\n   * A custom implementation of Robolectric's ShadowLooper which runs all scheduled messages in the\n   * loop method of the looper. Also ensures to correctly emulate the message order of the real\n   * message loop and to avoid blocking caused by Robolectric's default implementation.\n   *\n   * <p>Only works in conjunction with {@link CustomMessageQueue}. Note that the test's {@code\n   * SystemClock} is not advanced automatically.\n   */\n  @Implements(Looper.class)\n  public static final class CustomLooper extends ShadowLegacyLooper {\n\n    private final PriorityBlockingQueue<PendingMessage> pendingMessages;\n    private final CopyOnWriteArraySet<RemovedMessage> removedMessages;\n\n    public CustomLooper() {\n      pendingMessages = new PriorityBlockingQueue<>();\n      removedMessages = new CopyOnWriteArraySet<>();\n    }\n\n    @Implementation\n    public static void loop() {\n      Looper looper = Looper.myLooper();\n      if (shadowOf(looper) instanceof CustomLooper) {\n        ((CustomLooper) shadowOf(looper)).doLoop();\n      }\n    }\n\n    @Implementation\n    @Override\n    public void quitUnchecked() {\n      super.quitUnchecked();\n      // Insert message at the front of the queue to quit loop as soon as possible.\n      addPendingMessage(/* message= */ null, /* when= */ Long.MIN_VALUE);\n    }\n\n    private void addPendingMessage(@Nullable Message message, long when) {\n      pendingMessages.put(new PendingMessage(message, when));\n    }\n\n    private void removeMessages(Handler handler, int what, Object object) {\n      RemovedMessage newRemovedMessage = new RemovedMessage(handler, what, object);\n      removedMessages.add(newRemovedMessage);\n      for (RemovedMessage removedMessage : removedMessages) {\n        if (removedMessage != newRemovedMessage\n            && removedMessage.handler == handler\n            && removedMessage.what == what\n            && removedMessage.object == object) {\n          removedMessages.remove(removedMessage);\n        }\n      }\n    }\n\n    private void doLoop() {\n      boolean wasInterrupted = false;\n      while (true) {\n        try {\n          PendingMessage pendingMessage = pendingMessages.take();\n          if (pendingMessage.message == null) {\n            // Null message is signal to end message loop.\n            return;\n          }\n          // Call through to real {@code Message.markInUse()} and {@code Message.recycle()} to\n          // ensure message recycling works. This is also done in Robolectric's own implementation\n          // of the message queue.\n          callInstanceMethod(pendingMessage.message, \"markInUse\");\n          Handler target = pendingMessage.message.getTarget();\n          if (target != null) {\n            boolean isRemoved = false;\n            for (RemovedMessage removedMessage : removedMessages) {\n              if (removedMessage.handler == target\n                  && (removedMessage.what == ANY_MESSAGE\n                      || removedMessage.what == pendingMessage.message.what)\n                  && (removedMessage.object == null\n                      || removedMessage.object == pendingMessage.message.obj)\n                  && pendingMessage.sequenceNumber < removedMessage.sequenceNumber) {\n                isRemoved = true;\n              }\n            }\n            if (!isRemoved) {\n              try {\n                if (wasInterrupted) {\n                  wasInterrupted = false;\n                  // Restore the interrupt status flag, so long-running messages will exit early.\n                  Thread.currentThread().interrupt();\n                }\n                target.dispatchMessage(pendingMessage.message);\n              } catch (Throwable t) {\n                // Interrupt the main thread to terminate the test. Robolectric's HandlerThread will\n                // print the rethrown error to standard output.\n                Looper.getMainLooper().getThread().interrupt();\n                throw t;\n              }\n            }\n          }\n          if (Util.SDK_INT >= 21) {\n            callInstanceMethod(pendingMessage.message, \"recycleUnchecked\");\n          } else {\n            callInstanceMethod(pendingMessage.message, \"recycle\");\n          }\n        } catch (InterruptedException e) {\n          wasInterrupted = true;\n        }\n      }\n    }\n  }\n\n  /**\n   * Custom implementation of Robolectric's ShadowMessageQueue which is needed to let {@link\n   * CustomLooper} work as intended.\n   */\n  @Implements(MessageQueue.class)\n  public static final class CustomMessageQueue extends ShadowLegacyMessageQueue {\n\n    private final Thread looperThread;\n\n    public CustomMessageQueue() {\n      looperThread = Thread.currentThread();\n    }\n\n    @Implementation\n    @Override\n    public boolean enqueueMessage(Message msg, long when) {\n      Looper looper = ShadowLooper.getLooperForThread(looperThread);\n      if (shadowOf(looper) instanceof CustomLooper\n          && shadowOf(looper) != shadowOf(Looper.getMainLooper())) {\n        ((CustomLooper) shadowOf(looper)).addPendingMessage(msg, when);\n      } else {\n        super.enqueueMessage(msg, when);\n      }\n      return true;\n    }\n\n    @Implementation\n    public void removeMessages(Handler handler, int what, Object object) {\n      Looper looper = ShadowLooper.getLooperForThread(looperThread);\n      if (shadowOf(looper) instanceof CustomLooper\n          && shadowOf(looper) != shadowOf(Looper.getMainLooper())) {\n        ((CustomLooper) shadowOf(looper)).removeMessages(handler, what, object);\n      }\n    }\n\n    @Implementation\n    public void removeCallbacksAndMessages(Handler handler, Object object) {\n      Looper looper = ShadowLooper.getLooperForThread(looperThread);\n      if (shadowOf(looper) instanceof CustomLooper\n          && shadowOf(looper) != shadowOf(Looper.getMainLooper())) {\n        ((CustomLooper) shadowOf(looper)).removeMessages(handler, ANY_MESSAGE, object);\n      }\n    }\n  }\n\n  private static final class PendingMessage implements Comparable<PendingMessage> {\n\n    public final @Nullable Message message;\n    public final long when;\n    public final long sequenceNumber;\n\n    public PendingMessage(@Nullable Message message, long when) {\n      this.message = message;\n      this.when = when;\n      sequenceNumber = sequenceNumberGenerator.getAndIncrement();\n    }\n\n    @Override\n    public int compareTo(@NonNull PendingMessage other) {\n      int res = Util.compareLong(this.when, other.when);\n      if (res == 0 && this != other) {\n        res = Util.compareLong(this.sequenceNumber, other.sequenceNumber);\n      }\n      return res;\n    }\n  }\n\n  private static final class RemovedMessage {\n\n    public final Handler handler;\n    public final int what;\n    public final Object object;\n    public final long sequenceNumber;\n\n    public RemovedMessage(Handler handler, int what, Object object) {\n      this.handler = handler;\n      this.what = what;\n      this.object = object;\n      this.sequenceNumber = sequenceNumberGenerator.get();\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.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.testutil;\n\nimport android.os.Looper;\nimport com.google.android.exoplayer2.BasePlayer;\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlayer;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.PlayerMessage;\nimport com.google.android.exoplayer2.SeekParameters;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\n\n/**\n * An abstract {@link ExoPlayer} implementation that throws {@link UnsupportedOperationException}\n * from every method.\n */\npublic abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {\n\n  @Override\n  public AudioComponent getAudioComponent() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public VideoComponent getVideoComponent() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public TextComponent getTextComponent() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public MetadataComponent getMetadataComponent() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public Looper getPlaybackLooper() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public Looper getApplicationLooper() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void addListener(Player.EventListener listener) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void removeListener(Player.EventListener listener) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public int getPlaybackState() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  @PlaybackSuppressionReason\n  public int getPlaybackSuppressionReason() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public ExoPlaybackException getPlaybackError() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void retry() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void prepare(MediaSource mediaSource) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void setPlayWhenReady(boolean playWhenReady) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public boolean getPlayWhenReady() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void setRepeatMode(@RepeatMode int repeatMode) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public int getRepeatMode() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void setShuffleModeEnabled(boolean shuffleModeEnabled) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public boolean getShuffleModeEnabled() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public boolean isLoading() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void seekTo(int windowIndex, long positionMs) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void setPlaybackParameters(PlaybackParameters playbackParameters) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public PlaybackParameters getPlaybackParameters() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void setSeekParameters(SeekParameters seekParameters) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public SeekParameters getSeekParameters() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void stop(boolean resetStateAndPosition) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void release() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public PlayerMessage createMessage(PlayerMessage.Target target) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public void sendMessages(ExoPlayerMessage... messages) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  @Deprecated\n  @SuppressWarnings(\"deprecation\")\n  public void blockingSendMessages(ExoPlayerMessage... messages) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public int getRendererCount() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public int getRendererType(int index) {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public TrackGroupArray getCurrentTrackGroups() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public TrackSelectionArray getCurrentTrackSelections() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public Object getCurrentManifest() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public Timeline getCurrentTimeline() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public int getCurrentPeriodIndex() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public int getCurrentWindowIndex() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public long getDuration() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public long getCurrentPosition() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public long getBufferedPosition() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public long getTotalBufferedDuration() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public boolean isPlayingAd() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public int getCurrentAdGroupIndex() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public int getCurrentAdIndexInAdGroup() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public long getContentPosition() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public long getContentBufferedPosition() {\n    throw new UnsupportedOperationException();\n  }\n\n  @Override\n  public void setForegroundMode(boolean foregroundMode) {\n    throw new UnsupportedOperationException();\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\nimport static org.junit.Assert.fail;\n\nimport android.os.ConditionVariable;\nimport com.google.android.exoplayer2.offline.Download;\nimport com.google.android.exoplayer2.offline.Download.State;\nimport com.google.android.exoplayer2.offline.DownloadManager;\nimport java.util.HashMap;\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.TimeUnit;\n\n/** A {@link DownloadManager.Listener} for testing. */\npublic final class TestDownloadManagerListener implements DownloadManager.Listener {\n\n  private static final int TIMEOUT = 1000;\n  private static final int INITIALIZATION_TIMEOUT = 10000;\n  private static final int STATE_REMOVED = -1;\n\n  private final DownloadManager downloadManager;\n  private final DummyMainThread dummyMainThread;\n  private final HashMap<String, ArrayBlockingQueue<Integer>> downloadStates;\n  private final ConditionVariable initializedCondition;\n  private final int timeout;\n\n  private CountDownLatch downloadFinishedCondition;\n  @Download.FailureReason private int failureReason;\n\n  public TestDownloadManagerListener(\n      DownloadManager downloadManager, DummyMainThread dummyMainThread) {\n    this(downloadManager, dummyMainThread, TIMEOUT);\n  }\n\n  public TestDownloadManagerListener(\n      DownloadManager downloadManager, DummyMainThread dummyMainThread, int timeout) {\n    this.downloadManager = downloadManager;\n    this.dummyMainThread = dummyMainThread;\n    this.timeout = timeout;\n    downloadStates = new HashMap<>();\n    initializedCondition = new ConditionVariable();\n    downloadManager.addListener(this);\n  }\n\n  public Integer pollStateChange(String taskId, long timeoutMs) throws InterruptedException {\n    return getStateQueue(taskId).poll(timeoutMs, TimeUnit.MILLISECONDS);\n  }\n\n  @Override\n  public void onInitialized(DownloadManager downloadManager) {\n    initializedCondition.open();\n  }\n\n  public void waitUntilInitialized() {\n    if (!downloadManager.isInitialized()) {\n      assertThat(initializedCondition.block(INITIALIZATION_TIMEOUT)).isTrue();\n    }\n  }\n\n  @Override\n  public void onDownloadChanged(DownloadManager downloadManager, Download download) {\n    if (download.state == Download.STATE_FAILED) {\n      failureReason = download.failureReason;\n    }\n    getStateQueue(download.request.id).add(download.state);\n  }\n\n  @Override\n  public void onDownloadRemoved(DownloadManager downloadManager, Download download) {\n    getStateQueue(download.request.id).add(STATE_REMOVED);\n  }\n\n  @Override\n  public synchronized void onIdle(DownloadManager downloadManager) {\n    if (downloadFinishedCondition != null) {\n      downloadFinishedCondition.countDown();\n    }\n  }\n\n  /**\n   * Blocks until all remove and download tasks are complete and throws an exception if there was an\n   * error.\n   */\n  public void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable {\n    blockUntilTasksComplete();\n    if (failureReason != Download.FAILURE_REASON_NONE) {\n      throw new Exception(\"Failure reason: \" + failureReason);\n    }\n  }\n\n  /** Blocks until all remove and download tasks are complete. Task errors are ignored. */\n  public void blockUntilTasksComplete() throws InterruptedException {\n    synchronized (this) {\n      downloadFinishedCondition = new CountDownLatch(1);\n    }\n    dummyMainThread.runOnMainThread(\n        () -> {\n          if (downloadManager.isIdle()) {\n            downloadFinishedCondition.countDown();\n          }\n        });\n    assertThat(downloadFinishedCondition.await(timeout, TimeUnit.MILLISECONDS)).isTrue();\n  }\n\n  private ArrayBlockingQueue<Integer> getStateQueue(String taskId) {\n    synchronized (downloadStates) {\n      if (!downloadStates.containsKey(taskId)) {\n        downloadStates.put(taskId, new ArrayBlockingQueue<>(10));\n      }\n      return downloadStates.get(taskId);\n    }\n  }\n\n  public void assertRemoved(String taskId, int timeoutMs) {\n    assertStateInternal(taskId, STATE_REMOVED, timeoutMs);\n  }\n\n  public void assertState(String taskId, @State int expectedState, int timeoutMs) {\n    assertStateInternal(taskId, expectedState, timeoutMs);\n  }\n\n  private void assertStateInternal(String taskId, int expectedState, int timeoutMs) {\n    while (true) {\n      Integer state = null;\n      try {\n        state = pollStateChange(taskId, timeoutMs);\n      } catch (InterruptedException e) {\n        fail(e.getMessage());\n      }\n      if (state != null) {\n        if (expectedState == state) {\n          return;\n        }\n      } else {\n        fail(\"Didn't receive expected state: \" + expectedState);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "exoplayer-amzn-2.10.6/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.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.testutil;\n\nimport static com.google.common.truth.Truth.assertThat;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.Timeline.Period;\nimport com.google.android.exoplayer2.Timeline.Window;\n\n/** Unit test for {@link Timeline}. */\npublic final class TimelineAsserts {\n\n  private static final int[] REPEAT_MODES = {\n    Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE, Player.REPEAT_MODE_ALL\n  };\n\n  private TimelineAsserts() {}\n\n  /** Assert that timeline is empty (i.e. has no windows or periods). */\n  public static void assertEmpty(Timeline timeline) {\n    assertWindowTags(timeline);\n    assertPeriodCounts(timeline);\n    for (boolean shuffled : new boolean[] {false, true}) {\n      assertThat(timeline.getFirstWindowIndex(shuffled)).isEqualTo(C.INDEX_UNSET);\n      assertThat(timeline.getLastWindowIndex(shuffled)).isEqualTo(C.INDEX_UNSET);\n    }\n  }\n\n  /**\n   * Asserts that window tags are set correctly.\n   *\n   * @param expectedWindowTags A list of expected window tags. If a tag is unknown or not important\n   *     {@code null} can be passed to skip this window.\n   */\n  public static void assertWindowTags(Timeline timeline, Object... expectedWindowTags) {\n    Window window = new Window();\n    assertThat(timeline.getWindowCount()).isEqualTo(expectedWindowTags.length);\n    for (int i = 0; i < timeline.getWindowCount(); i++) {\n      timeline.getWindow(i, window, true);\n      if (expectedWindowTags[i] != null) {\n        assertThat(window.tag).isEqualTo(expectedWindowTags[i]);\n      }\n    }\n  }\n\n  /** Asserts that window properties {@link Window}.isDynamic are set correctly. */\n  public static void assertWindowIsDynamic(Timeline timeline, boolean... windowIsDynamic) {\n    Window window = new Window();\n    for (int i = 0; i < timeline.getWindowCount(); i++) {\n      timeline.getWindow(i, window, true);\n      assertThat(window.isDynamic).isEqualTo(windowIsDynamic[i]);\n    }\n  }\n\n  /**\n   * Asserts that previous window indices for each window depending on the repeat mode and the\n   * shuffle mode are equal to the given sequence.\n   */\n  public static void assertPreviousWindowIndices(\n      Timeline timeline,\n      @Player.RepeatMode int repeatMode,\n      boolean shuffleModeEnabled,\n      int... expectedPreviousWindowIndices) {\n    for (int i = 0; i < timeline.getWindowCount(); i++) {\n      assertThat(timeline.getPreviousWindowIndex(i, repeatMode, shuffleModeEnabled))\n          .isEqualTo(expectedPreviousWindowIndices[i]);\n    }\n  }\n\n  /**\n   * Asserts that next window indices for each window depending on the repeat mode and the shuffle\n   * mode are equal to the given sequence.\n   */\n  public static void assertNextWindowIndices(\n      Timeline timeline,\n      @Player.RepeatMode int repeatMode,\n      boolean shuffleModeEnabled,\n      int... expectedNextWindowIndices) {\n    for (int i = 0; i < timeline.getWindowCount(); i++) {\n      assertThat(timeline.getNextWindowIndex(i, repeatMode, shuffleModeEnabled))\n          .isEqualTo(expectedNextWindowIndices[i]);\n    }\n  }\n\n  /**\n   * Asserts that the durations of the periods in the {@link Timeline} and the durations in the\n   * given sequence are equal.\n   */\n  public static void assertPeriodDurations(Timeline timeline, long... durationsUs) {\n    int periodCount = timeline.getPeriodCount();\n    assertThat(periodCount).isEqualTo(durationsUs.length);\n    Period period = new Period();\n    for (int i = 0; i < periodCount; i++) {\n      assertThat(timeline.getPeriod(i, period).durationUs).isEqualTo(durationsUs[i]);\n    }\n  }\n\n  /**\n   * Asserts that period counts for each window are set correctly. Also asserts that {@link\n   * Window#firstPeriodIndex} and {@link Window#lastPeriodIndex} are set correctly, and it asserts\n   * the correct behavior of {@link Timeline#getNextWindowIndex(int, int, boolean)}.\n   */\n  public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) {\n    int windowCount = timeline.getWindowCount();\n    assertThat(windowCount).isEqualTo(expectedPeriodCounts.length);\n    int[] accumulatedPeriodCounts = new int[windowCount + 1];\n    accumulatedPeriodCounts[0] = 0;\n    for (int i = 0; i < windowCount; i++) {\n      accumulatedPeriodCounts[i + 1] = accumulatedPeriodCounts[i] + expectedPeriodCounts[i];\n    }\n    assertThat(timeline.getPeriodCount())\n        .isEqualTo(accumulatedPeriodCounts[accumulatedPeriodCounts.length - 1]);\n    Window window = new Window();\n    Period period = new Period();\n    for (int i = 0; i < windowCount; i++) {\n      timeline.getWindow(i, window, true);\n      assertThat(window.firstPeriodIndex).isEqualTo(accumulatedPeriodCounts[i]);\n      assertThat(window.lastPeriodIndex).isEqualTo(accumulatedPeriodCounts[i + 1] - 1);\n    }\n    int expectedWindowIndex = 0;\n    for (int i = 0; i < timeline.getPeriodCount(); i++) {\n      timeline.getPeriod(i, period, true);\n      while (i >= accumulatedPeriodCounts[expectedWindowIndex + 1]) {\n        expectedWindowIndex++;\n      }\n      assertThat(period.windowIndex).isEqualTo(expectedWindowIndex);\n      assertThat(timeline.getIndexOfPeriod(period.uid)).isEqualTo(i);\n      assertThat(timeline.getUidOfPeriod(i)).isEqualTo(period.uid);\n      for (int repeatMode : REPEAT_MODES) {\n        if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) {\n          assertThat(timeline.getNextPeriodIndex(i, period, window, repeatMode, false))\n              .isEqualTo(i + 1);\n        } else {\n          int nextWindow = timeline.getNextWindowIndex(expectedWindowIndex, repeatMode, false);\n          int nextPeriod =\n              nextWindow == C.INDEX_UNSET ? C.INDEX_UNSET : accumulatedPeriodCounts[nextWindow];\n          assertThat(timeline.getNextPeriodIndex(i, period, window, repeatMode, false))\n              .isEqualTo(nextPeriod);\n        }\n      }\n    }\n  }\n\n  /** Asserts that periods' {@link Period#getAdGroupCount()} are set correctly. */\n  public static void assertAdGroupCounts(Timeline timeline, int... expectedAdGroupCounts) {\n    Period period = new Period();\n    for (int i = 0; i < timeline.getPeriodCount(); i++) {\n      timeline.getPeriod(i, period);\n      assertThat(period.getAdGroupCount()).isEqualTo(expectedAdGroupCounts[i]);\n    }\n  }\n}\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "content": "Browse media content with your own rules on Android TV"
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "content": "Browse media content with your own rules on Android TV"
  },
  {
    "path": "filepicker-lib/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "filepicker-lib/LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "filepicker-lib/README.md",
    "content": "# Material File Picker by Arte al Programar\n\nMaterial file picker library for Android by Arte al Programar\n\n![](ss/main.png)\n\n## What's new\n\n- Require Android Jelly Bean 4.1.x (API 16+)\n- Material You (Dynamics Color) Support\n- Night Mode Support\n- New Icon Designs\n\n## Add your project\n\nUsing Jcenter\n\n```\nbuild.gradle (Project)\n\nallprojects {\n    repositories {\n        maven { url 'https://jitpack.io' }\n    }\n}\n\n\nbuild.gradle (Module: app)\n\ndependencies {\n    ...\n    // Java\n    implementation 'androidx.activity:activity:1.4.0'\n    implementation 'androidx.fragment:fragment:1.4.1'\n\n    // Kotlin\n    implementation 'androidx.activity:activity-ktx:1.4.0'\n    implementation 'androidx.fragment:fragment-ktx:1.4.1'\n    implementation 'com.github.arteaprogramar:Android-MaterialFilePicker:3.0.1'\n}\n\n\n```\n\n## Using (IMPORTANT)\n\n- For Android 11 and above, you must request \"MANAGE_EXTERNAL_STORAGE\" permission in your\n  application, \"Material File Picker\" requires that permission to read and show user files.\n\n- Open your class and add the following code\n\n```\n...\nkotlin \n\n/** \n *  This library require \"Activity Result\" API \n **/\n\nprivate val startForResultFiles = registerForActivityResult(\n    ActivityResultContracts.StartActivityForResult()\n) { result: ActivityResult ->\n    onActivityResult(result.resultCode, result.data)\n} \n \n...\n\n// External Storage Path\nval externalStorage = FileUtils.getFile(applicationContext, null)\n\nMaterialFilePicker()\n        // Pass a source of context. Can be:\n        //    .withActivity(Activity activity)\n        //    .withFragment(Fragment fragment)\n        //    .withSupportFragment(androidx.fragment.app.Fragment fragment)\n        .withActivity(this)\n        // With cross icon on the right side of toolbar for closing picker straight away\n        .withCloseMenu(true)\n        // Entry point path (user will start from it)\n        //.withPath(alarmsFolder.absolutePath)\n        // Root path (user won't be able to come higher than it)\n        .withRootPath(externalStorage.absolutePath)\n        // Showing hidden files\n        .withHiddenFiles(true)\n        // Want to choose only jpg images\n        .withFilter(Pattern.compile(\".*\\\\.(jpg|jpeg)$\"))\n        // Don't apply filter to directories names\n        .withFilterDirectories(false)\n        .withTitle(\"Sample title\")\n        // Require \"Activity Result\" API\n        .withActivityResultApi(startForResultFiles)\n        .start()\n...\n\n\n/** \n *  For Android API 29+, You need \n *  <uses-permission android:name=\"android.permission.MANAGE_EXTERNAL_STORAGE\" /> \n * And some extra settings.\n * You can check the demo of the application\n **/\n\n\n```\n\nOverride on activity result:\n\n```\nkotlin\n\nprivate fun onActivityResult(resultCode: Int, data: Intent?) {\n    if (resultCode == Activity.RESULT_OK) {\n        val path: String? = data?.getStringExtra(FilePickerActivity.RESULT_FILE_PATH)\n\n        if (path != null) {\n            Log.d(\"Path: \", path)\n            Toast.makeText(this, \"Picked file: $path\", Toast.LENGTH_LONG).show()\n        }\n    }\n}\n\n```"
  },
  {
    "path": "filepicker-lib/build.gradle",
    "content": "apply from: gradle.ext.sharedModulesConstants\napply plugin: 'com.android.library'\n\ntask sourceJar(type: Jar) {\n    from android.sourceSets.main.java.srcDirs\n    classifier 'sources'\n}\n\nandroid {\n    compileSdkVersion project.properties.compileSdkVersion\n    buildToolsVersion project.properties.buildToolsVersion\n    defaultConfig {\n        minSdkVersion project.properties.minSdkVersion\n        targetSdkVersion project.properties.targetSdkVersion\n        versionCode 19\n        versionName '3.0.1'\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n\n    implementation 'androidx.recyclerview:recyclerview:' + recyclerviewXVersion\n    implementation 'com.google.android.material:material:' + materialVersion\n    implementation 'androidx.fragment:fragment:' + annotationXVersion\n\n    implementation project(':fragment-1.1.0')\n}"
  },
  {
    "path": "filepicker-lib/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\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"
  },
  {
    "path": "filepicker-lib/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"arte.programar.materialfile\">\n\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n\n    <application>\n        <activity\n            android:name=\".ui.FilePickerActivity\"\n            android:theme=\"@style/MaterialFileTheme\" />\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/MaterialFilePicker.java",
    "content": "package arte.programar.materialfile;\n\nimport android.app.Activity;\nimport android.app.Fragment;\nimport android.content.Intent;\n\n//import androidx.activity.result.ActivityResultLauncher;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.regex.Pattern;\n\nimport arte.programar.materialfile.filter.CompositeFilter;\nimport arte.programar.materialfile.filter.FileFilter;\nimport arte.programar.materialfile.filter.HiddenFilter;\nimport arte.programar.materialfile.filter.PatternFilter;\nimport arte.programar.materialfile.ui.FilePickerActivity;\n\npublic class MaterialFilePicker {\n    private Activity mActivity;\n\n    private Fragment mFragment;\n    private androidx.fragment.app.Fragment mSupportFragment;\n    //private ActivityResultLauncher<Intent> mStartForResultFiles;\n\n    private Class<? extends FilePickerActivity> mFilePickerClass = FilePickerActivity.class;\n\n    private Pattern mFileFilter;\n    private Boolean mDirectoriesFilter = false;\n    private String mRootPath;\n    private String mCurrentPath;\n    private Boolean mShowHidden = false;\n    private Boolean mCloseable = true;\n    private CharSequence mTitle;\n\n    public MaterialFilePicker() {\n    }\n\n    /**\n     * Specifies activity, which will be used to\n     * start file picker\n     */\n    public MaterialFilePicker withActivity(Activity activity) {\n        if (mSupportFragment != null || mFragment != null) {\n            throw new RuntimeException(\"You must pass either Activity, Fragment or SupportFragment\");\n        }\n\n        mActivity = activity;\n        return this;\n    }\n\n    /**\n     * Specifies fragment, which will be used to\n     * start file picker\n     */\n    public MaterialFilePicker withFragment(Fragment fragment) {\n        if (mSupportFragment != null || mActivity != null) {\n            throw new RuntimeException(\"You must pass either Activity, Fragment or SupportFragment\");\n        }\n\n        mFragment = fragment;\n        return this;\n    }\n\n    /**\n     * Specifies support fragment which will be used to\n     * start file picker\n     */\n    public MaterialFilePicker withSupportFragment(androidx.fragment.app.Fragment fragment) {\n        if (mActivity != null || mFragment != null) {\n            throw new RuntimeException(\"You must pass either Activity, Fragment or SupportFragment\");\n        }\n\n        mSupportFragment = fragment;\n        return this;\n    }\n\n    //public MaterialFilePicker withActivityResultApi(ActivityResultLauncher<Intent> startForResultFiles) {\n    //    mStartForResultFiles = startForResultFiles;\n    //    return this;\n    //}\n\n    /**\n     * Hides files that matched by specified regular expression.\n     * Use {@link MaterialFilePicker#withFilterDirectories withFilterDirectories} method\n     * to enable directories filtering\n     */\n    public MaterialFilePicker withFilter(Pattern pattern) {\n        mFileFilter = pattern;\n        return this;\n    }\n\n    /**\n     * If directoriesFilter is true directories will also be affected by filter,\n     * the default value of directories filter is false\n     *\n     * @see MaterialFilePicker#withFilter\n     */\n    public MaterialFilePicker withFilterDirectories(boolean directoriesFilter) {\n        mDirectoriesFilter = directoriesFilter;\n        return this;\n    }\n\n    /**\n     * Specifies root directory for picker,\n     * user can't go upper that specified path\n     */\n    public MaterialFilePicker withRootPath(String rootPath) {\n        mRootPath = rootPath;\n        return this;\n    }\n\n    /**\n     * Specifies start directory for picker,\n     * which will be shown to user at the beginning\n     */\n    public MaterialFilePicker withPath(String path) {\n        mCurrentPath = path;\n        return this;\n    }\n\n    /**\n     * Show or hide hidden files in picker\n     */\n    public MaterialFilePicker withHiddenFiles(boolean show) {\n        mShowHidden = show;\n        return this;\n    }\n\n    /**\n     * Show or hide close menu in picker\n     */\n    public MaterialFilePicker withCloseMenu(boolean closeable) {\n        mCloseable = closeable;\n        return this;\n    }\n\n    /**\n     * Set title of picker\n     */\n    public MaterialFilePicker withTitle(CharSequence title) {\n        mTitle = title;\n        return this;\n    }\n\n    public MaterialFilePicker withCustomActivity(Class<? extends FilePickerActivity> customActivityClass) {\n        mFilePickerClass = customActivityClass;\n        return this;\n    }\n\n    /**\n     * Open Material File Picker activity.\n     * You should set Activity or Fragment before calling this method\n     *\n     * @see MaterialFilePicker#withActivity(Activity)\n     * @see MaterialFilePicker#withFragment(Fragment)\n     * @see MaterialFilePicker#withSupportFragment(androidx.fragment.app.Fragment)\n     */\n    public void start(int requestCode) {\n        //mStartForResultFiles.launch(getIntent());\n        mActivity.startActivityForResult(getIntent(), requestCode);\n    }\n\n    // Public because of https://github.com/nbsp-team/MaterialFilePicker/issues/113\n    public Intent getIntent() {\n        CompositeFilter filter = getFilter();\n\n        Intent intent = new Intent(getActivity(), mFilePickerClass);\n        intent.putExtra(FilePickerActivity.ARG_FILTER, filter);\n        intent.putExtra(FilePickerActivity.ARG_CLOSEABLE, mCloseable);\n\n        if (mRootPath != null) {\n            intent.putExtra(FilePickerActivity.ARG_START_FILE, new File(mRootPath));\n        }\n\n        if (mCurrentPath != null) {\n            intent.putExtra(FilePickerActivity.ARG_CURRENT_FILE, new File(mCurrentPath));\n        }\n\n        if (mTitle != null) {\n            intent.putExtra(FilePickerActivity.ARG_TITLE, mTitle);\n        }\n\n        return intent;\n    }\n\n    private Activity getActivity() {\n        Activity activity = null;\n\n        //if (mActivity == null && mFragment == null && mSupportFragment == null && mStartForResultFiles == null) {\n        if (mActivity == null && mFragment == null && mSupportFragment == null) {\n            throw new RuntimeException(\n                    \"You must pass Activity/Fragment by calling \" +\n                            \"withActivity/withFragment/withSupportFragment and \" +\n                            \"withActivityResultApi method\"\n            );\n        }\n\n        if (mActivity != null) {\n            activity = mActivity;\n        } else if (mFragment != null) {\n            activity = mFragment.getActivity();\n        } else if (mSupportFragment != null) {\n            activity = mSupportFragment.getActivity();\n        }\n\n        return activity;\n    }\n\n    private CompositeFilter getFilter() {\n        final List<FileFilter> filters = new ArrayList<>();\n\n        if (!mShowHidden) {\n            filters.add(new HiddenFilter());\n        }\n\n        if (mFileFilter != null) {\n            filters.add(new PatternFilter(mFileFilter, mDirectoriesFilter));\n        }\n\n        return new CompositeFilter(filters);\n    }\n}\n"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/filter/CompositeFilter.java",
    "content": "package arte.programar.materialfile.filter;\n\nimport java.io.File;\nimport java.util.List;\n\npublic class CompositeFilter implements FileFilter {\n\n    private final List<FileFilter> mFilters;\n\n    public CompositeFilter(List<FileFilter> filters) {\n        mFilters = filters;\n    }\n\n    @Override\n    public boolean accept(File f) {\n        for (FileFilter filter : mFilters) {\n            if (!filter.accept(f)) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/filter/FileFilter.java",
    "content": "package arte.programar.materialfile.filter;\n\nimport java.io.File;\nimport java.io.Serializable;\n\npublic interface FileFilter extends Serializable {\n    boolean accept(File pathname);\n}\n"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/filter/HiddenFilter.java",
    "content": "package arte.programar.materialfile.filter;\n\nimport java.io.File;\n\npublic class HiddenFilter implements FileFilter {\n\n    @Override\n    public boolean accept(File f) {\n        return !f.isHidden();\n    }\n}\n"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/filter/PatternFilter.java",
    "content": "package arte.programar.materialfile.filter;\n\nimport java.io.File;\nimport java.util.regex.Pattern;\n\npublic class PatternFilter implements FileFilter {\n\n    private final Pattern mPattern;\n    private final boolean mDirectoriesFilter;\n\n    public PatternFilter(Pattern pattern, boolean directoriesFilter) {\n        mPattern = pattern;\n        mDirectoriesFilter = directoriesFilter;\n    }\n\n    @Override\n    public boolean accept(File f) {\n        return f.isDirectory() && !mDirectoriesFilter || mPattern.matcher(f.getName()).matches();\n    }\n}\n"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/ui/DirectoryAdapter.java",
    "content": "package arte.programar.materialfile.ui;\n\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport java.io.File;\nimport java.util.List;\n\nimport arte.programar.materialfile.R;\nimport arte.programar.materialfile.utils.FileTypeUtils;\n\nclass DirectoryAdapter extends RecyclerView.Adapter<DirectoryAdapter.DirectoryViewHolder> {\n\n    private final List<File> mFiles;\n    private OnItemClickListener mOnItemClickListener;\n\n    DirectoryAdapter(List<File> files) {\n        mFiles = files;\n    }\n\n    void setOnItemClickListener(OnItemClickListener listener) {\n        mOnItemClickListener = listener;\n    }\n\n    @NonNull\n    @Override\n    public DirectoryViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {\n        View view = LayoutInflater.from(parent.getContext())\n                .inflate(R.layout.item_file, parent, false);\n\n        return new DirectoryViewHolder(view, mOnItemClickListener);\n    }\n\n    @Override\n    public void onBindViewHolder(DirectoryViewHolder holder, int position) {\n        File currentFile = mFiles.get(position);\n\n        FileTypeUtils.FileType fileType = FileTypeUtils.getFileType(currentFile);\n        holder.mFileImage.setImageResource(fileType.getIcon());\n        holder.mFileSubtitle.setText(fileType.getDescription());\n        holder.mFileTitle.setText(currentFile.getName());\n    }\n\n    @Override\n    public int getItemCount() {\n        return mFiles.size();\n    }\n\n    File getModel(int index) {\n        return mFiles.get(index);\n    }\n\n    static class DirectoryViewHolder extends RecyclerView.ViewHolder {\n\n        private final ImageView mFileImage;\n        private final TextView mFileTitle;\n        private final TextView mFileSubtitle;\n\n        DirectoryViewHolder(View itemView, final OnItemClickListener clickListener) {\n            super(itemView);\n\n            itemView.setOnClickListener(v -> {\n                // MOD:\n                //clickListener.onItemClick(v, getBindingAdapterPosition());\n                clickListener.onItemClick(v, getAdapterPosition());\n            });\n\n            mFileImage = itemView.findViewById(R.id.item_file_image);\n            mFileTitle = itemView.findViewById(R.id.item_file_title);\n            mFileSubtitle = itemView.findViewById(R.id.item_file_subtitle);\n        }\n    }\n}"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/ui/DirectoryFragment.java",
    "content": "package arte.programar.materialfile.ui;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.Fragment;\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport java.io.File;\n\nimport arte.programar.materialfile.R;\nimport arte.programar.materialfile.filter.FileFilter;\nimport arte.programar.materialfile.utils.FileUtils;\nimport arte.programar.materialfile.widget.EmptyRecyclerView;\n\npublic class DirectoryFragment extends Fragment {\n\n    private static final String ARG_FILE = \"arg_file_path\";\n    private static final String ARG_FILTER = \"arg_filter\";\n\n    private View mEmptyView;\n    private File mFile;\n\n    private FileFilter mFilter;\n\n    private EmptyRecyclerView mDirectoryRecyclerView;\n    private DirectoryAdapter mDirectoryAdapter;\n    private FileClickListener mFileClickListener;\n\n    static DirectoryFragment getInstance(\n            File file,\n            FileFilter filter\n    ) {\n        final DirectoryFragment instance = new DirectoryFragment();\n\n        final Bundle args = new Bundle();\n        args.putSerializable(ARG_FILE, file);\n        args.putSerializable(ARG_FILTER, filter);\n        instance.setArguments(args);\n\n        return instance;\n    }\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        mFileClickListener = (FileClickListener) context;\n    }\n\n    @Override\n    public void onDetach() {\n        super.onDetach();\n        mFileClickListener = null;\n    }\n\n    @Nullable\n    @Override\n    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {\n        final View view = inflater.inflate(R.layout.fragment_directory, container, false);\n\n        mDirectoryRecyclerView = view.findViewById(R.id.directory_recycler_view);\n        mEmptyView = view.findViewById(R.id.directory_empty_view);\n\n        return view;\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n        initArgs();\n        initFilesList();\n    }\n\n    private void initFilesList() {\n        mDirectoryAdapter = new DirectoryAdapter(FileUtils.getFileList(mFile, mFilter));\n\n        mDirectoryAdapter.setOnItemClickListener(new ThrottleClickListener() {\n            @Override\n            void onItemClickThrottled(View view, int position) {\n                if (mFileClickListener != null) {\n                    mFileClickListener.onFileClicked(mDirectoryAdapter.getModel(position));\n                }\n            }\n        });\n\n        mDirectoryRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));\n        mDirectoryRecyclerView.setAdapter(mDirectoryAdapter);\n        mDirectoryRecyclerView.setEmptyView(mEmptyView);\n    }\n\n    private void initArgs() {\n        final Bundle arguments = requireArguments();\n\n        if (arguments.containsKey(ARG_FILE)) {\n            mFile = (File) getArguments().getSerializable(ARG_FILE);\n        }\n\n        mFilter = (FileFilter) getArguments().getSerializable(ARG_FILTER);\n    }\n\n    interface FileClickListener {\n        void onFileClicked(File clickedFile);\n    }\n}\n"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/ui/FilePickerActivity.java",
    "content": "package arte.programar.materialfile.ui;\n\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.os.Environment;\nimport android.os.Handler;\nimport android.text.TextUtils;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.appcompat.widget.Toolbar;\n\n//import com.google.android.material.color.DynamicColors;\n\nimport java.io.File;\nimport java.io.Serializable;\nimport java.lang.reflect.Field;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.regex.Pattern;\n\nimport arte.programar.materialfile.R;\nimport arte.programar.materialfile.filter.FileFilter;\nimport arte.programar.materialfile.filter.PatternFilter;\nimport arte.programar.materialfile.utils.FileUtils;\n\npublic class FilePickerActivity extends AppCompatActivity implements DirectoryFragment.FileClickListener {\n    public static final String ARG_START_FILE = \"arg_start_path\";\n    public static final String ARG_CURRENT_FILE = \"arg_current_path\";\n\n    public static final String ARG_FILTER = \"arg_filter\";\n    public static final String ARG_CLOSEABLE = \"arg_closeable\";\n    public static final String ARG_TITLE = \"arg_title\";\n\n    public static final String STATE_START_FILE = \"state_start_path\";\n    public static final String RESULT_FILE_PATH = \"result_file_path\";\n    private static final String STATE_CURRENT_FILE = \"state_current_path\";\n    private static final int HANDLE_CLICK_DELAY = 150;\n\n    private Toolbar mToolbar;\n\n    private File mStart = null;\n    private File mCurrent = null;\n\n    private CharSequence mTitle;\n\n    private Boolean mCloseable = true;\n\n    private FileFilter mFilter;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        // MOD:\n        //DynamicColors.applyIfAvailable(this);\n        setContentView(R.layout.activity_file_picker);\n\n        if (mStart == null) {\n            mStart = FileUtils.getFile(getApplicationContext(), \"\");\n            mCurrent = mStart;\n        }\n\n        initArguments(savedInstanceState);\n        initViews();\n        initToolbar();\n\n        if (savedInstanceState == null) {\n            initBackStackState();\n        }\n    }\n\n    private void initArguments(Bundle savedInstanceState) {\n        if (getIntent().hasExtra(ARG_FILTER)) {\n            Serializable filter = getIntent().getSerializableExtra(ARG_FILTER);\n\n            if (filter instanceof Pattern) {\n                mFilter = new PatternFilter((Pattern) filter, false);\n            } else {\n                mFilter = (FileFilter) filter;\n            }\n        }\n\n        if (savedInstanceState != null) {\n            mStart = (File) savedInstanceState.getSerializable(STATE_START_FILE);\n            mCurrent = (File) savedInstanceState.getSerializable(STATE_CURRENT_FILE);\n            updateTitle();\n        } else {\n            if (getIntent().hasExtra(ARG_START_FILE)) {\n                mStart = (File) getIntent().getSerializableExtra(ARG_START_FILE);\n                mCurrent = mStart;\n            }\n\n            if (getIntent().hasExtra(ARG_CURRENT_FILE)) {\n                File currentFile = (File) getIntent().getSerializableExtra(ARG_CURRENT_FILE);\n\n                if (FileUtils.isParent(currentFile, mStart)) {\n                    mCurrent = currentFile;\n                }\n            }\n        }\n\n        if (getIntent().hasExtra(ARG_TITLE)) {\n            mTitle = getIntent().getCharSequenceExtra(ARG_TITLE);\n        }\n\n        if (getIntent().hasExtra(ARG_CLOSEABLE)) {\n            mCloseable = getIntent().getBooleanExtra(ARG_CLOSEABLE, true);\n        }\n    }\n\n    private void initToolbar() {\n        setSupportActionBar(mToolbar);\n\n        // Truncate start of path\n        try {\n            Field f;\n            if (TextUtils.isEmpty(mTitle)) {\n                f = mToolbar.getClass().getDeclaredField(\"mTitleTextView\");\n            } else {\n                f = mToolbar.getClass().getDeclaredField(\"mSubtitleTextView\");\n            }\n\n            f.setAccessible(true);\n            TextView textView = (TextView) f.get(mToolbar);\n            textView.setEllipsize(TextUtils.TruncateAt.START);\n        } catch (Exception ignored) {\n        }\n\n        if (!TextUtils.isEmpty(mTitle)) {\n            setTitle(mTitle);\n        }\n        updateTitle();\n    }\n\n    private void initViews() {\n        mToolbar = findViewById(R.id.toolbar);\n    }\n\n    private void initBackStackState() {\n        final List<File> path = new ArrayList<>();\n\n        File current = mCurrent;\n\n        while (current != null) {\n            path.add(current);\n\n            if (current.equals(mStart)) {\n                break;\n            }\n\n            current = FileUtils.getParentOrNull(current);\n        }\n\n        Collections.reverse(path);\n\n        for (File file : path) {\n            addFragmentToBackStack(file);\n        }\n    }\n\n    private void updateTitle() {\n        if (getSupportActionBar() != null) {\n            //final String titlePath = mCurrent.getAbsolutePath();\n            // Show only path name\n            final String titlePath = mCurrent.getName();\n\n            getSupportActionBar().setDisplayHomeAsUpEnabled(!mCurrent.getPath().equals(mStart.getPath()));\n\n            if (TextUtils.isEmpty(mTitle)) {\n                getSupportActionBar().setTitle(titlePath);\n            } else {\n                getSupportActionBar().setSubtitle(titlePath);\n            }\n        }\n    }\n\n    private void addFragmentToBackStack(File file) {\n        getSupportFragmentManager()\n                .beginTransaction()\n                .replace(R.id.container, DirectoryFragment.getInstance(file, mFilter))\n                .addToBackStack(null)\n                .commit();\n    }\n\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        // MOD: dialog onStop fix\n        //getMenuInflater().inflate(R.menu.menu, menu);\n        //menu.findItem(R.id.action_close).setVisible(mCloseable);\n        return true;\n    }\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem menuItem) {\n        // MOD: dialog onStop fix\n        //if (menuItem.getItemId() == android.R.id.home) {\n        //    onBackPressed();\n        //} else if (menuItem.getItemId() == R.id.action_close) {\n        //    finish();\n        //}\n        return super.onOptionsItemSelected(menuItem);\n    }\n\n    @Override\n    public void onBackPressed() {\n        if (getSupportFragmentManager().getBackStackEntryCount() > 1) {\n            getSupportFragmentManager().popBackStack();\n            mCurrent = FileUtils.getParentOrNull(mCurrent);\n            updateTitle();\n        } else {\n            setResult(RESULT_CANCELED);\n            finish();\n        }\n    }\n\n    @Override\n    public void onSaveInstanceState(@NonNull Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putSerializable(STATE_CURRENT_FILE, mCurrent);\n        outState.putSerializable(STATE_START_FILE, mStart);\n    }\n\n    @Override\n    public void onFileClicked(final File clickedFile) {\n        new Handler().postDelayed(() -> handleFileClicked(clickedFile), HANDLE_CLICK_DELAY);\n    }\n\n    private void handleFileClicked(final File clickedFile) {\n        if (isFinishing()) {\n            return;\n        }\n\n        if (clickedFile.isDirectory()) {\n            mCurrent = clickedFile;\n            // If the user wanna go to the emulated directory, he will be taken to the\n            // corresponding user emulated folder.\n            if (mCurrent.getAbsolutePath().equals(\"/storage/emulated\")) {\n                mCurrent = Environment.getExternalStorageDirectory();\n            }\n            addFragmentToBackStack(mCurrent);\n            updateTitle();\n        } else {\n            setResultAndFinish(clickedFile);\n        }\n    }\n\n    private void setResultAndFinish(File file) {\n        Intent data = new Intent();\n\n        data.putExtra(RESULT_FILE_PATH, file.getPath());\n        setResult(RESULT_OK, data);\n\n        finish();\n    }\n}\n"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/ui/OnItemClickListener.java",
    "content": "package arte.programar.materialfile.ui;\n\nimport android.os.SystemClock;\nimport android.view.View;\n\ninterface OnItemClickListener {\n    void onItemClick(View view, int position);\n\n    abstract class ThrottleClickListener implements OnItemClickListener {\n\n        private static final long MIN_CLICK_INTERVAL = 600;\n\n        private long mLastClickTime;\n\n        abstract void onItemClickThrottled(View view, int position);\n\n        @Override\n        public void onItemClick(View view, int position) {\n            final long currentClickTime = SystemClock.uptimeMillis();\n            final long elapsedTime = currentClickTime - mLastClickTime;\n\n            mLastClickTime = currentClickTime;\n\n            if (elapsedTime <= MIN_CLICK_INTERVAL) {\n                return;\n            }\n\n            onItemClickThrottled(view, position);\n        }\n    }\n}\n"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/ui/ThrottleClickListener.java",
    "content": "package arte.programar.materialfile.ui;\n\nimport android.os.SystemClock;\nimport android.view.View;\n\nabstract class ThrottleClickListener implements OnItemClickListener {\n\n    private static final long MIN_CLICK_INTERVAL = 600;\n\n    private long mLastClickTime;\n\n    abstract void onItemClickThrottled(View view, int position);\n\n    @Override\n    public void onItemClick(View view, int position) {\n        final long currentClickTime = SystemClock.uptimeMillis();\n        final long elapsedTime = currentClickTime - mLastClickTime;\n\n        mLastClickTime = currentClickTime;\n\n        if (elapsedTime <= MIN_CLICK_INTERVAL) {\n            return;\n        }\n\n        onItemClickThrottled(view, position);\n    }\n}"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/utils/FileComparator.java",
    "content": "package arte.programar.materialfile.utils;\n\nimport java.io.File;\nimport java.util.Comparator;\n\npublic class FileComparator implements Comparator<File> {\n    @Override\n    public int compare(File f1, File f2) {\n        if (f1 == f2) {\n            return 0;\n        }\n        if (f1.isDirectory() && f2.isFile()) {\n            // Show directories above files\n            return -1;\n        }\n        if (f1.isFile() && f2.isDirectory()) {\n            // Show files below directories\n            return 1;\n        }\n        // Sort the directories alphabetically\n        return f1.getName().compareToIgnoreCase(f2.getName());\n    }\n}\n"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/utils/FileTypeUtils.java",
    "content": "package arte.programar.materialfile.utils;\n\nimport android.webkit.MimeTypeMap;\n\nimport java.io.File;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLEncoder;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport arte.programar.materialfile.R;\n\npublic class FileTypeUtils {\n\n    private static final Map<String, FileType> fileTypeExtensions = new HashMap<>();\n\n    static {\n        for (FileType fileType : FileType.values()) {\n            for (String extension : fileType.getExtensions()) {\n                fileTypeExtensions.put(extension, fileType);\n            }\n        }\n    }\n\n    public static FileType getFileType(File file) {\n        if (file.isDirectory()) {\n            return FileType.DIRECTORY;\n        }\n\n        FileType fileType = fileTypeExtensions.get(getExtension(file.getName()));\n        if (fileType != null) {\n            return fileType;\n        }\n\n        return FileType.FILE;\n    }\n\n    private static String getExtension(String fileName) {\n        String encoded;\n        try {\n            encoded = URLEncoder.encode(fileName, \"UTF-8\").replace(\"+\", \"%20\");\n        } catch (UnsupportedEncodingException e) {\n            encoded = fileName;\n        }\n        return MimeTypeMap.getFileExtensionFromUrl(encoded).toLowerCase();\n    }\n\n    public enum FileType {\n        DIRECTORY(R.drawable.ic_app_directory, R.string.type_directory),\n        FILE(R.drawable.ic_app_file, R.string.type_document),\n\n        APK(R.drawable.ic_app_apk, R.string.type_apk, \"apk\"),\n        CERTIFICATE(R.drawable.ic_app_certificate, R.string.type_certificate, \"cer\", \"der\", \"pfx\", \"p12\", \"arm\", \"pem\"),\n        COMPRESS(R.drawable.ic_app_compress, R.string.type_archive, \"cab\", \"7z\", \"alz\", \"arj\", \"bzip2\", \"bz2\", \"dmg\", \"gzip\", \"gz\", \"jar\", \"lz\", \"lzip\", \"lzma\", \"zip\", \"rar\", \"tar\", \"tgz\"),\n        WORD(R.drawable.ic_app_document, R.string.type_word, \"doc\", \"docm\", \"docx\", \"dot\", \"mcw\", \"rtf\", \"pages\", \"odt\", \"ott\"),\n        DRAWING(R.drawable.ic_app_drawing, R.string.type_drawing, \"ai\", \"cdr\", \"dfx\", \"eps\", \"svg\", \"stl\", \"wmf\", \"emf\", \"art\", \"xar\"),\n        JSON(R.drawable.ic_app_json, R.string.type_json, \"json\"),\n        IMAGE(R.drawable.ic_app_image, R.string.type_image, \"bmp\", \"gif\", \"ico\", \"jpeg\", \"jpg\", \"pcx\", \"png\", \"psd\", \"tga\", \"tiff\", \"tif\", \"xcf\"),\n        MUSIC(R.drawable.ic_app_music, R.string.type_music, \"aiff\", \"aif\", \"wav\", \"flac\", \"m4a\", \"wma\", \"amr\", \"mp2\", \"mp3\", \"wma\", \"aac\", \"mid\", \"m3u\"),\n        PDF(R.drawable.ic_app_pdf, R.string.type_pdf, \"pdf\"),\n        PRESENTATION(R.drawable.ic_app_presentation, R.string.type_power_point, \"pptx\", \"keynote\", \"ppt\", \"pps\", \"pot\", \"odp\", \"otp\"),\n        SPREADSHEET(R.drawable.ic_app_spreadsheet, R.string.type_excel, \"xls\", \"xlk\", \"xlsb\", \"xlsm\", \"xlsx\", \"xlr\", \"xltm\", \"xlw\", \"numbers\", \"ods\", \"ots\"),\n        VIDEO(R.drawable.ic_app_video, R.string.type_video, \"avi\", \"mov\", \"wmv\", \"mkv\", \"3gp\", \"f4v\", \"flv\", \"mp4\", \"mpeg\", \"webm\");\n\n        private final int icon;\n        private final int description;\n        private final String[] extensions;\n\n        FileType(int icon, int description, String... extensions) {\n            this.icon = icon;\n            this.description = description;\n            this.extensions = extensions;\n        }\n\n        public String[] getExtensions() {\n            return extensions;\n        }\n\n        public int getIcon() {\n            return icon;\n        }\n\n        public int getDescription() {\n            return description;\n        }\n    }\n}\n"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/utils/FileUtils.java",
    "content": "package arte.programar.materialfile.utils;\n\nimport android.content.Context;\nimport android.util.Log;\n\nimport androidx.annotation.Nullable;\nimport androidx.core.content.ContextCompat;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Locale;\n\nimport arte.programar.materialfile.filter.FileFilter;\n\npublic class FileUtils {\n\n    public static final String FILE_PATH_SEPARATOR = \"/\";\n\n    public static List<File> getFileList(File directory, FileFilter filter) {\n        File[] files = directory.listFiles(filter::accept);\n\n        if (files == null) {\n            return new ArrayList<>();\n        }\n\n        List<File> result = Arrays.asList(files);\n        Collections.sort(result, new FileComparator());\n        return result;\n    }\n\n    @Nullable\n    public static File getParentOrNull(File file) {\n        if (file.getParent() == null) {\n            return null;\n        }\n\n        return file.getParentFile();\n    }\n\n    public static boolean isParent(File maybeChild, File possibleParent) {\n        if (!possibleParent.exists() || !possibleParent.isDirectory()) {\n            return false;\n        }\n\n        File child = maybeChild;\n        while (child != null) {\n            if (child.equals(possibleParent)) {\n                return true;\n            }\n            child = child.getParentFile();\n        }\n\n        return false;\n    }\n\n    @Nullable\n    public static File getFile(Context context, String path) {\n        File filesDir = ContextCompat.getExternalFilesDirs(context, null)[0];\n\n        if (filesDir == null) { // storage device is unavailable\n            return null;\n        }\n\n        String npath = path == null ? \"\" : path;\n        String absolutePath = filesDir.getPath();\n        if (absolutePath.contains(\"/Android/data\")) {\n            int index = absolutePath.indexOf(\"/Android/data\");\n            String storage = absolutePath.substring(0, index).concat(npath.length() > 0 ? FILE_PATH_SEPARATOR : \"\");\n            absolutePath = String.format(Locale.getDefault(), \"%s%s\", storage, npath);\n        }\n\n        Log.d(\"TAG\", absolutePath);\n        return new File(absolutePath);\n    }\n}\n"
  },
  {
    "path": "filepicker-lib/src/main/java/arte/programar/materialfile/widget/EmptyRecyclerView.java",
    "content": "package arte.programar.materialfile.widget;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.recyclerview.widget.RecyclerView;\n\npublic class EmptyRecyclerView extends RecyclerView {\n\n    @Nullable\n    private View mEmptyView;\n\n    @NonNull\n    private final AdapterDataObserver observer = new AdapterDataObserver() {\n        @Override\n        public void onChanged() {\n            super.onChanged();\n            checkIfEmpty();\n        }\n    };\n\n    public EmptyRecyclerView(Context context) {\n        super(context);\n    }\n\n    public EmptyRecyclerView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public EmptyRecyclerView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n    }\n\n    void checkIfEmpty() {\n        final Adapter adapter = getAdapter();\n\n        if (mEmptyView != null && adapter != null) {\n            mEmptyView.setVisibility(adapter.getItemCount() > 0 ? GONE : VISIBLE);\n        }\n    }\n\n    @Override\n    public void setAdapter(@Nullable Adapter adapter) {\n        final Adapter oldAdapter = getAdapter();\n        if (oldAdapter != null) {\n            oldAdapter.unregisterAdapterDataObserver(observer);\n        }\n        super.setAdapter(adapter);\n        if (adapter != null) {\n            adapter.registerAdapterDataObserver(observer);\n        }\n    }\n\n    public void setEmptyView(@Nullable View mEmptyView) {\n        this.mEmptyView = mEmptyView;\n        checkIfEmpty();\n    }\n}\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/bg_clickable.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@color/compatibility_pressed_color\" android:state_pressed=\"true\" />\n    <item android:drawable=\"@color/compatibility_focus_color\" android:state_focused=\"true\" />\n    <item android:drawable=\"@color/transparent\" />\n</selector>"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_apk.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"16.933\"\n    android:viewportHeight=\"16.933\">\n    <path\n        android:fillColor=\"#84c835\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m3.4395,1.0583c-0.7329,0 -1.3229,0.59 -1.3229,1.3229v12.171c0,0.7328 0.59,1.3229 1.3229,1.3229h10.054c0.7329,0 1.3229,-0.59 1.3229,-1.3229V4.2332L11.6415,1.0585Z\"\n        android:strokeWidth=\"0.99998\" />\n    <path\n        android:fillAlpha=\"0.35\"\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M14.816,4.2331 L11.6411,1.0582v1.852c0,0.7329 0.59,1.3229 1.3229,1.3229z\"\n        android:strokeAlpha=\"0.35\" />\n    <path\n        android:fillAlpha=\"0.1\"\n        android:fillColor=\"#FF000000\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m11.641,2.9104v0.1545c0,0.794 0.59,1.433 1.3229,1.433h1.852l0.0001,-0.2646 -1.8522,0c-0.7329,0 -1.3229,-0.59 -1.3229,-1.3229z\"\n        android:strokeWidth=\"1.0408\"\n        android:strokeAlpha=\"0.1\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"m7.9377,7.673v1.7198c0,0.0733 0.059,0.1323 0.1323,0.1323h0.7937c0.0733,0 0.1323,-0.059 0.1323,-0.1323L8.996,7.673ZM8.2023,8.7313h0.5292v0.5292h-0.5292z\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"M8.4667,2.1168L7.9375,2.1168L7.9375,2.6459L8.4667,2.6459ZM8.4667,2.6459v0.5292h0.5292L8.9958,2.6459ZM8.4667,3.1751L7.9375,3.1751L7.9375,3.7042L8.4667,3.7042ZM8.4667,3.7042v0.5292h0.5292L8.9958,3.7042ZM8.4667,4.2334L7.9375,4.2334L7.9375,4.7626L8.4667,4.7626ZM8.4667,4.7626v0.5292h0.5292L8.9958,4.7626ZM8.4667,5.2917L7.9375,5.2917L7.9375,5.8209L8.4667,5.8209ZM8.4667,5.8209v0.5292h0.5292L8.9958,5.8209ZM8.4667,6.35L7.9375,6.35L7.9375,6.8792L8.4667,6.8792ZM8.4667,6.8792L8.4667,7.4084h0.5292L8.9958,6.8792Z\" />\n    <path\n        android:fillAlpha=\"0.5\"\n        android:fillColor=\"#0c2809\"\n        android:pathData=\"m10.6457,11.8283 l0.8026,-0.8026c0.1235,-0.1235 0.1235,-0.3149 0,-0.4383 -0.1235,-0.1235 -0.3149,-0.1235 -0.4383,0l-0.9137,0.9137c-0.4877,-0.2469 -1.0433,-0.3889 -1.6298,-0.3889 -0.5927,0 -1.1483,0.142 -1.6421,0.3889L5.9045,10.5874c-0.1235,-0.1235 -0.3149,-0.1235 -0.4383,0 -0.1235,0.1235 -0.1235,0.3149 0,0.4383l0.8087,0.8087c-0.9137,0.6729 -1.5125,1.7533 -1.5125,2.9818h7.4082c0,-1.2285 -0.5988,-2.3151 -1.5249,-2.988zM7.1436,13.4934h-0.5292v-0.5292h0.5292zM10.3185,13.4934h-0.5292v-0.5292h0.5292z\"\n        android:strokeWidth=\"0.61735\"\n        android:strokeAlpha=\"0.5\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_certificate.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"16.933\"\n    android:viewportHeight=\"16.933\">\n    <path\n        android:fillColor=\"#ff5555\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m3.4395,1.0583c-0.7329,0 -1.3229,0.59 -1.3229,1.3229v12.171c0,0.7328 0.59,1.3229 1.3229,1.3229h10.054c0.7329,0 1.3229,-0.59 1.3229,-1.3229V4.2332L11.6415,1.0585Z\"\n        android:strokeWidth=\"0.99998\" />\n    <path\n        android:fillAlpha=\"0.35\"\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M14.816,4.2331 L11.6411,1.0582v1.852c0,0.7329 0.59,1.3229 1.3229,1.3229z\"\n        android:strokeAlpha=\"0.35\" />\n    <path\n        android:fillAlpha=\"0.1\"\n        android:fillColor=\"#FF000000\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m11.641,2.9104v0.1545c0,0.794 0.59,1.433 1.3229,1.433h1.852l0.0001,-0.2646 -1.8522,0c-0.7329,0 -1.3229,-0.59 -1.3229,-1.3229z\"\n        android:strokeWidth=\"1.0408\"\n        android:strokeAlpha=\"0.1\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M8.1125,4.9459 L7.7709,5.3867 7.2284,5.2632 7.0811,5.8001 6.5323,5.8941 6.602,6.4466 6.1308,6.7437 6.4072,7.2273 6.0853,7.6821 6.5251,8.0232 6.4026,8.5663 6.9395,8.713l0.094,0.5488 0.215,-0.0274v2.7527l1.2154,-0.8842 1.2154,0.8842V9.3858l0.0258,0.0062L9.8525,8.8551 10.4013,8.7605 10.331,8.2081 10.8022,7.9115 10.5263,7.4273 10.8477,6.973 10.4079,6.6315 10.5309,6.0889 9.994,5.9416 9.9,5.3933 9.3476,5.4631 9.0504,4.9918 8.5668,5.2678ZM8.4665,6.0853A1.3229,1.3229 0,0 1,9.7894 7.4082,1.3229 1.3229,0 0,1 8.4665,8.7311 1.3229,1.3229 0,0 1,7.1436 7.4082,1.3229 1.3229,0 0,1 8.4665,6.0853Z\"\n        android:strokeWidth=\"0.264578\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_compress.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"64\"\n    android:viewportHeight=\"64\">\n    <path\n        android:fillColor=\"#ffd351\"\n        android:pathData=\"M30,8L54,8A2,2 0,0 1,56 10L56,38A2,2 0,0 1,54 40L30,40A2,2 0,0 1,28 38L28,10A2,2 0,0 1,30 8z\" />\n    <path\n        android:fillColor=\"#7bd57d\"\n        android:pathData=\"M10,6L34,6A2,2 0,0 1,36 8L36,36A2,2 0,0 1,34 38L10,38A2,2 0,0 1,8 36L8,8A2,2 0,0 1,10 6z\" />\n    <path\n        android:fillColor=\"#707ed0\"\n        android:pathData=\"M21,12L45,12A2,2 0,0 1,47 14L47,42A2,2 0,0 1,45 44L21,44A2,2 0,0 1,19 42L19,14A2,2 0,0 1,21 12z\" />\n    <path\n        android:fillColor=\"#c49a6c\"\n        android:pathData=\"m7.16,20c-1.1966,0 -2.16,0.9175 -2.16,2.0571v30.857c0,1.7095 1.445,3.0857 3.24,3.0857h47.52c1.795,0 3.24,-1.3762 3.24,-3.0857v-30.857c0,-1.1397 -0.9634,-2.0571 -2.16,-2.0571z\" />\n    <path\n        android:fillColor=\"#fff\"\n        android:pathData=\"m32,33a2,2 0,0 0,-2 2v6a2,2 0,0 0,2 2,2 2,0 0,0 2,-2v-6a2,2 0,0 0,-2 -2zM32,40a1,1 0,0 1,1 1,1 1,0 0,1 -1,1 1,1 0,0 1,-1 -1,1 1,0 0,1 1,-1z\" />\n    <path\n        android:fillColor=\"#f2f2f2\"\n        android:pathData=\"m32,21h2v2h-2z\" />\n    <path\n        android:fillColor=\"#e0e0e0\"\n        android:pathData=\"m30,23h2v2h-2z\" />\n    <path\n        android:fillColor=\"#f2f2f2\"\n        android:pathData=\"m32,25h2v2h-2z\" />\n    <path\n        android:fillColor=\"#e0e0e0\"\n        android:pathData=\"m30,27h2v2h-2z\" />\n    <path\n        android:fillColor=\"#f2f2f2\"\n        android:pathData=\"m32,29h2v2h-2z\" />\n    <path\n        android:fillColor=\"#e0e0e0\"\n        android:pathData=\"m30,31h2v2h-2z\" />\n    <path\n        android:fillColor=\"#a97b50\"\n        android:pathData=\"m42,47c-0.554,0 -1,0.446 -1,1v4c0,0.554 0.446,1 1,1h11c0.554,0 1,-0.446 1,-1v-4c0,-0.554 -0.446,-1 -1,-1h-11zM42,47.9981h2v4h-2v-4zM45,48h1v4h-1v-4zM47,48h1v4h-1v-4zM49,48h2v4h-2v-4zM52,48h1v4h-1v-4z\" />\n    <path\n        android:fillColor=\"#a97b50\"\n        android:pathData=\"m15,44 l-2,3h1v4h2v-4h1zM17,47h1v4h2v-4h1l-2,-3zM12,52v1h10v-1z\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_database.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"16.933\"\n    android:viewportHeight=\"16.933\">\n    <path\n        android:fillColor=\"#4caf50\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m3.4395,1.0583c-0.7329,0 -1.3229,0.59 -1.3229,1.3229v12.171c0,0.7328 0.59,1.3229 1.3229,1.3229h10.054c0.7329,0 1.3229,-0.59 1.3229,-1.3229V4.2332L11.6415,1.0585Z\"\n        android:strokeWidth=\"0.99998\" />\n    <path\n        android:fillAlpha=\"0.35\"\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M14.816,4.2331 L11.6411,1.0582v1.852c0,0.7329 0.59,1.3229 1.3229,1.3229z\"\n        android:strokeAlpha=\"0.35\" />\n    <path\n        android:fillAlpha=\"0.1\"\n        android:fillColor=\"#FF000000\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m11.641,2.9104v0.1545c0,0.794 0.59,1.433 1.3229,1.433h1.852l0.0001,-0.2646 -1.8522,0c-0.7329,0 -1.3229,-0.59 -1.3229,-1.3229z\"\n        android:strokeWidth=\"1.0408\"\n        android:strokeAlpha=\"0.1\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"m8.4665,4.7415c-1.8074,0 -3.272,0.5214 -3.272,1.1655l0,0.9313c0,0.6413 1.4646,1.1627 3.272,1.1627 1.8074,0 3.272,-0.5214 3.272,-1.1627L11.7385,5.907c0,-0.6441 -1.4646,-1.1655 -3.272,-1.1655zM8.4665,4.7415\"\n        android:strokeWidth=\"0.630919\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"m8.4567,8.4665c-1.4646,0 -2.6989,-0.3429 -3.1177,-0.8141 -0.0955,0.1087 -0.1445,0.2258 -0.1445,0.3485l0,0.9313c0,0.6413 1.4646,1.1627 3.272,1.1627 1.8074,0 3.272,-0.5214 3.272,-1.1627L11.7385,8.0009c0,-0.1227 -0.0588,-0.2398 -0.1567,-0.3485C11.1654,8.1236 9.9213,8.4665 8.4567,8.4665ZM8.4567,8.4665\"\n        android:strokeWidth=\"0.630919\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"m8.4567,10.5604c-1.4646,0 -2.6989,-0.3429 -3.1177,-0.8141 -0.0955,0.1115 -0.1445,0.2286 -0.1445,0.3485l0,0.9313c0,0.6441 1.4646,1.1655 3.272,1.1655 1.8074,0 3.272,-0.5214 3.272,-1.1655L11.7385,10.0948c0,-0.1199 -0.0588,-0.237 -0.1567,-0.3485C11.1654,10.2175 9.9213,10.5604 8.4567,10.5604ZM8.4567,10.5604\"\n        android:strokeWidth=\"0.630919\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_directory.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"16.933\"\n    android:viewportHeight=\"16.933\">\n    <!-- Old main color: #5677fc -->\n    <path\n        android:fillColor=\"#999999\"\n        android:pathData=\"M3.44,2.378c-0.294,0 -0.395,0.269 -0.53,0.53h-0.793a0.528,0.528 0,0 0,-0.53 0.528v5.292c0,0.293 0.237,0.53 0.53,0.53h12.7c0.293,0 0.529,-0.237 0.529,-0.53v-5.292a0.528,0.528 0,0 0,-0.53 -0.529L7.674,2.907c-0.155,-0.249 -0.236,-0.53 -0.53,-0.53z\" />\n    <path\n        android:fillAlpha=\"0.18\"\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M3.44,2.378c-0.294,0 -0.395,0.269 -0.53,0.53h-0.793a0.528,0.528 0,0 0,-0.53 0.528v5.292c0,0.293 0.237,0.53 0.53,0.53h12.7c0.293,0 0.529,-0.237 0.529,-0.53v-5.292a0.528,0.528 0,0 0,-0.53 -0.529L7.674,2.907c-0.155,-0.249 -0.236,-0.53 -0.53,-0.53z\"\n        android:strokeAlpha=\"0.18\" />\n    <path\n        android:fillColor=\"#fff\"\n        android:pathData=\"M2.646,3.7L14.287,3.7A0.265,0.265 0,0 1,14.552 3.965L14.552,8.462A0.265,0.265 0,0 1,14.287 8.727L2.646,8.727A0.265,0.265 0,0 1,2.381 8.462L2.381,3.965A0.265,0.265 0,0 1,2.646 3.7z\" />\n    <path\n        android:fillColor=\"#999999\"\n        android:pathData=\"m8.4671,4.2301c-0.293,0 -0.3822,0.2756 -0.5292,0.5286l-0.3101,0.5312l-6.5701,0c-0.2931,0 -0.5286,0.2361 -0.5286,0.5292l0,8.4671c0,0.2931 0.2356,0.5286 0.5286,0.5286l14.8175,0c0.2931,0 0.5286,-0.2345 0.5286,-0.5276l0,-9.5249c0,-0.294 -0.2356,-0.5323 -0.5286,-0.5323z\"\n        android:strokeWidth=\"3.7796\" />\n    <path\n        android:fillAlpha=\"0.25\"\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M3.969,2.64L6.614,2.64A0.265,0.2645 0,0 1,6.879 2.9045L6.879,2.9045A0.265,0.2645 0,0 1,6.614 3.169L3.969,3.169A0.265,0.2645 0,0 1,3.704 2.9045L3.704,2.9045A0.265,0.2645 0,0 1,3.969 2.64z\"\n        android:strokeAlpha=\"0.25\" />\n    <path\n        android:fillAlpha=\"0.1\"\n        android:pathData=\"m16.404,11.641 l-3.1744,3.1749h2.6458c0.2931,0 0.5286,-0.2356 0.5286,-0.5286z\"\n        android:strokeAlpha=\"0.1\"></path>\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_document.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"16.933\"\n    android:viewportHeight=\"16.933\">\n    <path\n        android:fillColor=\"#2196f3\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m3.4395,1.0583c-0.7329,0 -1.3229,0.59 -1.3229,1.3229v12.171c0,0.7328 0.59,1.3229 1.3229,1.3229h10.054c0.7329,0 1.3229,-0.59 1.3229,-1.3229V4.2332L11.6415,1.0585Z\"\n        android:strokeWidth=\"0.99998\" />\n    <path\n        android:fillAlpha=\"0.35\"\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M14.816,4.2331 L11.6411,1.0582v1.852c0,0.7329 0.59,1.3229 1.3229,1.3229z\"\n        android:strokeAlpha=\"0.35\" />\n    <path\n        android:fillAlpha=\"0.1\"\n        android:fillColor=\"#FF000000\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m11.641,2.9104v0.1545c0,0.794 0.59,1.433 1.3229,1.433h1.852l0.0001,-0.2646 -1.8522,0c-0.7329,0 -1.3229,-0.59 -1.3229,-1.3229z\"\n        android:strokeWidth=\"1.0408\"\n        android:strokeAlpha=\"0.1\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"M4.7624,11.1123L4.7624,10.5832L10.054,10.5832v0.5292zM4.7624,9.5248L4.7624,8.9957L12.1706,8.9957v0.5292zM4.7624,7.9373v-0.5292L12.1706,7.4082v0.5292zM4.7624,6.3498v-0.5292L12.1706,5.8207v0.5292z\"\n        android:strokeWidth=\"0.999999\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_drawing.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"16.933\"\n    android:viewportHeight=\"16.933\">\n    <path\n        android:fillColor=\"#2b898f\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m3.4395,1.0583c-0.7329,0 -1.3229,0.59 -1.3229,1.3229v12.171c0,0.7328 0.59,1.3229 1.3229,1.3229h10.054c0.7329,0 1.3229,-0.59 1.3229,-1.3229V4.2332L11.6415,1.0585Z\"\n        android:strokeWidth=\"0.99998\" />\n    <path\n        android:fillAlpha=\"0.35\"\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M14.816,4.2331 L11.6411,1.0582v1.852c0,0.7329 0.59,1.3229 1.3229,1.3229z\"\n        android:strokeAlpha=\"0.35\" />\n    <path\n        android:fillAlpha=\"0.1\"\n        android:fillColor=\"#FF000000\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m11.641,2.9104v0.1545c0,0.794 0.59,1.433 1.3229,1.433h1.852l0.0001,-0.2646 -1.8522,0c-0.7329,0 -1.3229,-0.59 -1.3229,-1.3229z\"\n        android:strokeWidth=\"1.0408\"\n        android:strokeAlpha=\"0.1\" />\n    <path\n        android:fillAlpha=\"0.35\"\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"M8.4665,8.4665m-3.9687,0a3.9687,3.9687 0,1 1,7.9373 0a3.9687,3.9687 0,1 1,-7.9373 0\"\n        android:strokeWidth=\"0.749994\"\n        android:strokeAlpha=\"0.35\" />\n    <path\n        android:fillAlpha=\"0.5\"\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"m9.3093,9.7813a1.191,1.191 94.4984,0 1,-1.6845 0,1.191 1.191,94.4984 0,1 0,-1.6837 1.191,1.191 94.4984,0 1,1.6837 0,1.191 1.191,94.4984 0,1 0,1.6837z\"\n        android:strokeAlpha=\"0.5\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"M8.4671,5.6128A0.0945,0.0945 92.6759,0 0,8.4011 5.6413L6.8246,7.2178a2.3812,2.3812 87.9149,0 0,-0.042 0.0375,2.3812 2.3812,87.9149 0,0 0,3.3675 2.3812,2.3812 87.9149,0 0,3.3675 0,2.3812 2.3812,87.9149 0,0 0.0007,-3.3675 2.3812,2.3812 87.9149,0 0,-0.057 -0.0525C10.0908,7.1991 10.0901,7.1953 10.0863,7.1916L8.5361,5.6413A0.0945,0.0945 92.6759,0 0,8.4671 5.6128ZM8.4843,7.7488a1.191,1.191 94.4984,0 1,0.825 0.3487,1.191 1.191,94.4984 0,1 0,1.6837 1.191,1.191 94.4984,0 1,-1.6845 0,1.191 1.191,94.4984 0,1 0,-1.6837 1.191,1.191 94.4984,0 1,0.8595 -0.3487z\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"M8.8833,8.3781 L8.4626,8.7988 8.0418,8.3781 7.9008,8.5183 8.3223,8.939 7.9098,9.3688 8.0418,9.5008 8.4618,9.0793 8.8833,9.5008 9.0326,9.3688 8.6028,8.939 9.0236,8.5191Z\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_file.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"16.933\"\n    android:viewportHeight=\"16.933\">\n    <path\n        android:fillColor=\"#f2f2f2\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m3.4395,1.0583c-0.7329,0 -1.3229,0.59 -1.3229,1.3229v12.171c0,0.7328 0.59,1.3229 1.3229,1.3229h10.054c0.7329,0 1.3229,-0.59 1.3229,-1.3229V4.2332L11.6415,1.0585Z\"\n        android:strokeWidth=\"0.99998\" />\n    <path\n        android:fillAlpha=\"0.35\"\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M14.816,4.2331 L11.6411,1.0582v1.852c0,0.7329 0.59,1.3229 1.3229,1.3229z\"\n        android:strokeAlpha=\"0.35\" />\n    <path\n        android:fillAlpha=\"0.1\"\n        android:fillColor=\"#FF000000\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m11.641,2.9104v0.1545c0,0.794 0.59,1.433 1.3229,1.433h1.852l0.0001,-0.2646 -1.8522,0c-0.7329,0 -1.3229,-0.59 -1.3229,-1.3229z\"\n        android:strokeWidth=\"1.0408\"\n        android:strokeAlpha=\"0.1\" />\n    <path\n        android:fillColor=\"#999999\"\n        android:pathData=\"m8.996,4.8949v0.2646h3.1749L12.1709,4.8949ZM8.996,5.6886v0.2646h3.1749v-0.2646zM8.996,6.4824v0.2646h3.1749v-0.2646zM8.996,7.2761v0.2646h3.1749L12.1709,7.2761ZM8.996,8.0698v0.2646h3.1749v-0.2646zM4.7628,8.8636v0.2646h7.4082v-0.2646zM4.7628,9.6573v0.2646L12.171,9.9219L12.171,9.6573ZM4.7628,10.451v0.2646h7.4082v-0.2646zM4.7628,11.2448v0.2646h7.4082v-0.2646zM4.7628,12.0385v0.2646L8.996,12.3031L8.996,12.0385Z\" />\n    <path\n        android:fillColor=\"#999999\"\n        android:pathData=\"m6.388,4.6299 l-1.3053,3.4395h-0.3206v0.2646h0.926L5.688,8.0694L5.3916,8.0694l0.3093,-0.7943h1.5347l0.3089,0.7943h-0.4005v0.2646h1.3283L8.4722,8.0694h-0.3664l-1.3056,-3.4395zM6.4682,5.2254 L7.1439,7.0117h-1.3401z\"\n        android:strokeWidth=\"0.066145\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_image.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"16.933\"\n    android:viewportHeight=\"16.933\">\n    <path\n        android:fillColor=\"#7f7fff\"\n        android:pathData=\"M1.0583,3.4395L1.0583,13.4935A1.3229,1.3229 0,0 0,2.3812 14.8164L14.5518,14.8164A1.3229,1.3229 0,0 0,15.8747 13.4935L15.8747,3.4395A1.3229,1.3229 0,0 0,14.5518 2.1166L2.3812,2.1166A1.3229,1.3229 0,0 0,1.0583 3.4395z\" />\n    <path\n        android:fillAlpha=\"0.15\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m12.1706,14.8164 l3.7041,-3.7041l0,2.3812c0,0.7329 -0.59,1.3229 -1.3229,1.3229z\"\n        android:strokeWidth=\"8.8191\"\n        android:strokeAlpha=\"0.15\"></path>\n    <path\n        android:fillAlpha=\"0.25\"\n        android:fillColor=\"#fff\"\n        android:pathData=\"m9.8556,8.202 l-1.0583,1.6283 1.5213,2.3404l2.1166,0l-2.5797,-3.9687z\"\n        android:strokeWidth=\"2.8347\"\n        android:strokeAlpha=\"0.25\" />\n    <path\n        android:fillAlpha=\"0.65\"\n        android:fillColor=\"#fff\"\n        android:pathData=\"m6.879,6.879 l-3.4395,5.2916h6.879z\"\n        android:strokeAlpha=\"0.65\" />\n    <path\n        android:fillAlpha=\"0.65\"\n        android:fillColor=\"#fff\"\n        android:pathData=\"m11.377,4.7624c-0.7306,0 -1.3229,0.5923 -1.3229,1.3229 0,0.7306 0.5923,1.3229 1.3229,1.3229 0.7306,0 1.3229,-0.5923 1.3229,-1.3229 0,-0.7306 -0.5923,-1.3229 -1.3229,-1.3229z\"\n        android:strokeAlpha=\"0.65\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_json.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"16.933\"\n    android:viewportHeight=\"16.933\">\n    <path\n        android:fillColor=\"#fbc02d\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m3.4395,1.0583c-0.7329,0 -1.3229,0.59 -1.3229,1.3229v12.171c0,0.7328 0.59,1.3229 1.3229,1.3229h10.054c0.7329,0 1.3229,-0.59 1.3229,-1.3229V4.2332L11.6415,1.0585Z\"\n        android:strokeWidth=\"0.99998\" />\n    <path\n        android:fillAlpha=\"0.35\"\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M14.816,4.2331 L11.6411,1.0582v1.852c0,0.7329 0.59,1.3229 1.3229,1.3229z\"\n        android:strokeAlpha=\"0.35\" />\n    <path\n        android:fillAlpha=\"0.1\"\n        android:fillColor=\"#FF000000\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m11.641,2.9104v0.1545c0,0.794 0.59,1.433 1.3229,1.433h1.852l0.0001,-0.2646 -1.8522,0c-0.7329,0 -1.3229,-0.59 -1.3229,-1.3229z\"\n        android:strokeWidth=\"1.0408\"\n        android:strokeAlpha=\"0.1\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m8.4665,5.1593c-1.7832,0 -3.3072,1.4074 -3.3072,3.3072 0,1.6536 1.1011,2.8948 2.2785,3.3072 -0.783,-0.5704 -1.5012,-1.6536 -1.5012,-2.8928 0,-1.43 0.9735,-2.4825 2.1051,-2.4825 1.0573,0 1.7832,1.071 1.787,2.068 0,0.6996 -0.541,1.2761 -0.9735,1.6536 0.7792,0 1.7508,-0.6852 1.7508,-2.066C10.6059,6.8129 9.6343,5.1593 8.4665,5.1593ZM8.4665,5.1593\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m8.4665,11.7737c1.7832,0 3.3072,-1.4074 3.3072,-3.3072 0,-1.6536 -1.1011,-2.8948 -2.2785,-3.3072 0.783,0.5704 1.5012,1.6536 1.5012,2.8948 0,1.4279 -0.9735,2.4784 -2.1051,2.4784 -1.0573,0 -1.7832,-1.0689 -1.787,-2.066 0,-0.6996 0.541,-1.2761 0.9735,-1.6536 -0.7792,0 -1.7508,0.6852 -1.7508,2.066 0,1.2412 0.9716,2.8948 2.1394,2.8948zM8.4665,11.7737\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_music.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"512dp\"\n    android:height=\"512dp\"\n    android:viewportWidth=\"512\"\n    android:viewportHeight=\"512\">\n    <path\n        android:fillAlpha=\"0.2\"\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M256,258m-240,0a240,240 0,1 1,480 0a240,240 0,1 1,-480 0\"\n        android:strokeAlpha=\"0.2\" />\n    <path\n        android:fillAlpha=\"0.1\"\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M256,260m-240,0a240,240 0,1 1,480 0a240,240 0,1 1,-480 0\"\n        android:strokeAlpha=\"0.1\" />\n    <path\n        android:fillAlpha=\"0.2\"\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M256,260m-240,0a240,240 0,1 1,480 0a240,240 0,1 1,-480 0\"\n        android:strokeAlpha=\"0.2\" />\n    <path\n        android:fillColor=\"#ff6d00\"\n        android:pathData=\"M256,256m-240,0a240,240 0,1 1,480 0a240,240 0,1 1,-480 0\" />\n    <path\n        android:fillAlpha=\"0.2\"\n        android:fillColor=\"#fff\"\n        android:pathData=\"m256,16a240,240 0,0 0,-240 240,240 240,0 0,0 0.041,1.172A240,240 0,0 1,256 18,240 240,0 0,1 495.959,256.828 240,240 0,0 0,496 256,240 240,0 0,0 256,16Z\"\n        android:strokeAlpha=\"0.2\" />\n    <path\n        android:fillAlpha=\"0.1\"\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M416,262L416,262A160,160 0,0 0,256 102L256,102A160,160 0,0 0,96 262L96,262A160,160 0,0 0,256 422L256,422A160,160 0,0 0,416 262z\"\n        android:strokeAlpha=\"0.1\" />\n    <path\n        android:fillAlpha=\"0.2\"\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M416,270L416,270A160,160 0,0 0,256 110L256,110A160,160 0,0 0,96 270L96,270A160,160 0,0 0,256 430L256,430A160,160 0,0 0,416 270z\"\n        android:strokeAlpha=\"0.2\" />\n    <path\n        android:fillColor=\"#ffbc00\"\n        android:pathData=\"M416,256L416,256A160,160 0,0 0,256 96L256,96A160,160 0,0 0,96 256L96,256A160,160 0,0 0,256 416L256,416A160,160 0,0 0,416 256z\" />\n    <path\n        android:fillAlpha=\"0.4\"\n        android:fillColor=\"#fff\"\n        android:pathData=\"m256,96c-88.64,0 -160,71.36 -160,160 0,0.334 0.012,0.666 0.014,1 0.535,-88.175 71.681,-159 159.99,-159s159.45,70.825 159.99,159c0.002,-0.334 0.014,-0.666 0.014,-1 0,-88.64 -71.36,-160 -160,-160z\"\n        android:strokeAlpha=\"0.4\" />\n    <path\n        android:fillColor=\"#fff\"\n        android:pathData=\"M216,196h24v104h-24z\" />\n    <path\n        android:fillColor=\"#fff\"\n        android:pathData=\"M312,196h24v104h-24z\" />\n    <path\n        android:fillColor=\"#fff\"\n        android:pathData=\"M204,300m-36,0a36,36 0,1 1,72 0a36,36 0,1 1,-72 0\" />\n    <path\n        android:fillColor=\"#fff\"\n        android:pathData=\"M216,168h120v48h-120z\" />\n    <path\n        android:fillColor=\"#fff\"\n        android:pathData=\"M300,300m-36,0a36,36 0,1 1,72 0a36,36 0,1 1,-72 0\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_pdf.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"16.933\"\n    android:viewportHeight=\"16.933\">\n    <path\n        android:fillColor=\"#e84f43\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m3.4395,1.0583c-0.7329,0 -1.3229,0.59 -1.3229,1.3229v12.171c0,0.7328 0.59,1.3229 1.3229,1.3229h10.054c0.7329,0 1.3229,-0.59 1.3229,-1.3229V4.2332L11.6415,1.0585Z\"\n        android:strokeWidth=\"0.99998\" />\n    <path\n        android:fillAlpha=\"0.35\"\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M14.816,4.2331 L11.6411,1.0582v1.852c0,0.7329 0.59,1.3229 1.3229,1.3229z\"\n        android:strokeAlpha=\"0.35\" />\n    <path\n        android:fillAlpha=\"0.1\"\n        android:fillColor=\"#FF000000\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m11.641,2.9104v0.1545c0,0.794 0.59,1.433 1.3229,1.433h1.852l0.0001,-0.2646 -1.8522,0c-0.7329,0 -1.3229,-0.59 -1.3229,-1.3229z\"\n        android:strokeWidth=\"1.0408\"\n        android:strokeAlpha=\"0.1\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"M6.942,9.1745C7.2057,8.6577 7.5053,8.0755 7.7452,7.4909l0.0948,-0.2308C7.5267,6.068 7.3389,5.1111 7.5067,4.4924c0.0452,-0.1613 0.232,-0.2592 0.4318,-0.2592l0.1217,0.0018h0.0223c0.2735,-0.0042 0.4022,0.3437 0.4169,0.4789 0.0241,0.2253 -0.0801,0.6065 -0.0801,0.6065 0,-0.1539 0.006,-0.4027 -0.0911,-0.6173 -0.1131,-0.2484 -0.2211,-0.3967 -0.318,-0.4203 -0.0489,0.0327 -0.0964,0.1003 -0.1127,0.2304 -0.0337,0.1824 -0.0438,0.4127 -0.0438,0.5314 0,0.4194 0.0826,0.9729 0.2449,1.5436 0.0306,-0.0883 0.0575,-0.1732 0.079,-0.2528 0.0333,-0.1253 0.2448,-0.9563 0.2448,-0.9563 0,0 -0.0533,1.1061 -0.1278,1.4408 -0.016,0.0707 -0.0336,0.1407 -0.0519,0.2123 0.2676,0.7477 0.6988,1.415 1.2132,1.8953 0.2028,0.1895 0.459,0.3424 0.7014,0.4817 0.5294,-0.0756 1.0169,-0.1114 1.4235,-0.1069 0.5395,0.0071 0.9356,0.0869 1.0959,0.2449 0.0785,0.0768 0.1104,0.1695 0.1203,0.2735 0.0023,0.0404 -0.0173,0.1355 -0.0231,0.1593 0.0058,-0.0289 0.0058,-0.1708 -0.4273,-0.3091 -0.3411,-0.109 -0.9795,-0.1057 -1.7455,-0.0241 0.886,0.4335 1.7492,0.6489 2.0228,0.5197 0.0669,-0.0326 0.148,-0.1437 0.148,-0.1437 0,0 -0.0482,0.2191 -0.0828,0.2738 -0.0442,0.0595 -0.1309,0.124 -0.213,0.1457 -0.432,0.1153 -1.5566,-0.1515 -2.537,-0.7116 -1.0953,0.1613 -2.2982,0.4593 -3.2625,0.7756 -0.9475,1.6606 -1.6599,2.4232 -2.2394,2.1331L4.2239,12.5322c-0.0866,-0.0495 -0.0998,-0.1701 -0.0798,-0.2683 0.0676,-0.3306 0.4821,-0.8285 1.3147,-1.3256 0.0896,-0.0542 0.4888,-0.2653 0.4888,-0.2653 0,0 -0.2955,0.286 -0.3648,0.3422 -0.6646,0.5446 -1.1551,1.2298 -1.1428,1.4954l0.0024,0.0232c0.5645,-0.0804 1.4111,-1.2295 2.4995,-3.3591M7.2869,9.3513c-0.1818,0.3424 -0.3595,0.6598 -0.5235,0.9513C7.6708,9.9224 8.6474,9.6791 9.5772,9.5062 9.4523,9.4199 9.331,9.3286 9.2169,9.232 8.7049,8.7985 8.3145,8.2578 8.0316,7.6887 7.8521,8.1721 7.6387,8.6858 7.2869,9.3513\"\n        android:strokeWidth=\"0.025916\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_presentation.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"16.933\"\n    android:viewportHeight=\"16.933\">\n    <path\n        android:fillColor=\"#f9ce1d\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m3.4395,1.0583c-0.7329,0 -1.3229,0.59 -1.3229,1.3229v12.171c0,0.7328 0.59,1.3229 1.3229,1.3229h10.054c0.7329,0 1.3229,-0.59 1.3229,-1.3229V4.2332L11.6415,1.0585Z\"\n        android:strokeWidth=\"0.99998\" />\n    <path\n        android:fillAlpha=\"0.35\"\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M14.816,4.2331 L11.6411,1.0582v1.852c0,0.7329 0.59,1.3229 1.3229,1.3229z\"\n        android:strokeAlpha=\"0.35\" />\n    <path\n        android:fillAlpha=\"0.1\"\n        android:fillColor=\"#FF000000\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m11.641,2.9104v0.1545c0,0.794 0.59,1.433 1.3229,1.433h1.852l0.0001,-0.2646 -1.8522,0c-0.7329,0 -1.3229,-0.59 -1.3229,-1.3229z\"\n        android:strokeWidth=\"1.0408\"\n        android:strokeAlpha=\"0.1\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"m6.3939,6.1735c-0.7793,0 -1.411,0.6317 -1.411,1.4111 0,0.7793 0.6318,1.4111 1.411,1.4111 0.7793,0 1.411,-0.6317 1.411,-1.4111L6.3939,7.5846ZM8.5106,6.5262L8.5106,7.0554L11.9501,7.0554L11.9501,6.5262ZM8.5486,8.1137 L8.5106,8.6429L11.9501,8.6429L11.9501,8.1137ZM5.3357,10.2303v0.5291h5.2914l0.0001,-0.5291z\"\n        android:strokeWidth=\"0.999999\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_spreadsheet.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"16.933\"\n    android:viewportHeight=\"16.933\">\n    <path\n        android:fillColor=\"#4caf50\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m3.4395,1.0583c-0.7329,0 -1.3229,0.59 -1.3229,1.3229v12.171c0,0.7328 0.59,1.3229 1.3229,1.3229h10.054c0.7329,0 1.3229,-0.59 1.3229,-1.3229V4.2332L11.6415,1.0585Z\"\n        android:strokeWidth=\"0.99998\" />\n    <path\n        android:fillAlpha=\"0.35\"\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M14.816,4.2331 L11.6411,1.0582v1.852c0,0.7329 0.59,1.3229 1.3229,1.3229z\"\n        android:strokeAlpha=\"0.35\" />\n    <path\n        android:fillAlpha=\"0.1\"\n        android:fillColor=\"#FF000000\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m11.641,2.9104v0.1545c0,0.794 0.59,1.433 1.3229,1.433h1.852l0.0001,-0.2646 -1.8522,0c-0.7329,0 -1.3229,-0.59 -1.3229,-1.3229z\"\n        android:strokeWidth=\"1.0408\"\n        android:strokeAlpha=\"0.1\" />\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"M5.2915,11.1123L11.6414,11.1123L11.6414,5.8207L5.2915,5.8207ZM5.8207,6.3499h1.0583v1.0583h-1.0583zM7.4082,6.3499L11.1123,6.3499v1.0583L7.4082,7.4082ZM5.8207,7.9374h1.0583v1.0583h-1.0583zM7.4082,7.9374L11.1123,7.9374L11.1123,8.9957L7.4082,8.9957ZM5.8207,9.5249h1.0583v1.0583h-1.0583zM7.4082,9.5249L11.1123,9.5249v1.0583L7.4082,10.5832Z\"\n        android:strokeWidth=\"0.999999\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_app_video.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"64\"\n    android:viewportHeight=\"64\">\n    <path\n        android:fillAlpha=\"0.15\"\n        android:fillColor=\"#FF000000\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m60.0011,13.9588c0,-2.77 -2.23,-5 -5,-5l-46.0015,0c-2.77,0 -5,2.23 -5,5l0,38.0001c0,2.77 2.23,5 5,5l39.0017,0l12.0002,-12.0002z\"\n        android:strokeWidth=\".26458\"\n        android:strokeAlpha=\"0.15\" />\n    <path\n        android:fillColor=\"#748290\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m60.0011,12.9999c0,-2.77 -2.23,-5 -5,-5l-46.0015,0c-2.77,0 -5,2.23 -5,5l0,38.0001c0,2.77 2.23,5 5,5l39.0017,0l12.0002,-12.0002z\"\n        android:strokeWidth=\".26458\" />\n    <path\n        android:fillAlpha=\"0.35\"\n        android:fillColor=\"#fff\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"m48.0009,55.9986 l11.9999,-11.9999l-6.9998,0c-2.77,0 -5,2.23 -5,5z\"\n        android:strokeAlpha=\"0.35\" />\n    <path\n        android:fillAlpha=\"0.2\"\n        android:fillColor=\"#fff\"\n        android:pathData=\"m6.0217,9c-1.2216,0.9099 -2.0215,2.3529 -2.0215,4h4v-4zM55.9997,9v4h4v-0.0371c-0.0119,-1.6316 -0.8074,-3.0598 -2.0195,-3.9629zM3.9997,15.0001v4h4v-4zM55.9997,15.0001v4h4v-4zM3.9997,21.0002v4h4v-4zM55.9997,21.0002v4h4v-4zM3.9997,27.0003v4h4v-4zM55.9997,27.0003v4h4v-4zM3.9997,33.0004v4h4v-4zM55.9997,33.0004v4h4v-4zM3.9997,39.0005v4h4v-4zM55.9997,39.0005v4h4v-4zM3.9997,45.0006v4h4v-4zM3.9997,51.0007c0,1.6471 0.7999,3.0901 2.0215,4h1.9785v-4z\"\n        android:strokeAlpha=\"0.2\" />\n    <path\n        android:fillAlpha=\"0.15\"\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"m22.963,22.173c-0.5319,-0.0003 -0.9631,0.4318 -0.9628,0.9647l0,19.7571c-0.0001,0.7329 0.7836,1.1981 1.425,0.8457l18.0748,-9.8784c0.6669,-0.3658 0.6669,-1.3255 0,-1.6914l-16.2682,-8.8911 -1.807,-0.9872c-0.1416,-0.0779 -0.3006,-0.1189 -0.4622,-0.1189z\"\n        android:strokeWidth=\".035862\"\n        android:strokeAlpha=\"0.15\" />\n    <path\n        android:fillColor=\"#edeef8\"\n        android:pathData=\"m22.963,21.156c-0.5319,-0.0003 -0.9631,0.4318 -0.9628,0.9647v19.757c-0.0001,0.7329 0.7836,1.1981 1.425,0.8457l18.075,-9.8784c0.6669,-0.3658 0.6669,-1.3255 0,-1.6914l-16.268,-8.8911 -1.807,-0.9872c-0.1416,-0.0779 -0.3006,-0.1189 -0.4622,-0.1189z\"\n        android:strokeWidth=\".13554\" />\n</vector>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable/ic_close.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"#000000\"\n        android:pathData=\"M3,16.74L7.76,12L3,7.26L7.26,3L12,7.76L16.74,3L21,7.26L16.24,12L21,16.74L16.74,21L12,16.24L7.26,21L3,16.74M12,13.41L16.74,18.16L18.16,16.74L13.41,12L18.16,7.26L16.74,5.84L12,10.59L7.26,5.84L5.84,7.26L10.59,12L5.84,16.74L7.26,18.16L12,13.41Z\" />\n\n</vector>"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable-night/ic_close.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"#FAFAFA\"\n        android:pathData=\"M3,16.74L7.76,12L3,7.26L7.26,3L12,7.76L16.74,3L21,7.26L16.24,12L21,16.74L16.74,21L12,16.24L7.26,21L3,16.74M12,13.41L16.74,18.16L18.16,16.74L13.41,12L18.16,7.26L16.74,5.84L12,10.59L7.26,5.84L5.84,7.26L10.59,12L5.84,16.74L7.26,18.16L12,13.41Z\" />\n\n</vector>"
  },
  {
    "path": "filepicker-lib/src/main/res/drawable-v21/bg_clickable.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"?android:colorControlHighlight\">\n    <item\n        android:id=\"@android:id/mask\"\n        android:drawable=\"@android:color/white\" />\n</ripple>"
  },
  {
    "path": "filepicker-lib/src/main/res/layout/activity_file_picker.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\">\n\n    <FrameLayout\n        android:id=\"@+id/container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/id_appbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:fitsSystemWindows=\"true\"\n        app:elevation=\"@dimen/appbar_elevation\"\n        app:liftOnScroll=\"true\">\n\n        <com.google.android.material.appbar.MaterialToolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            app:layout_collapseMode=\"pin\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "filepicker-lib/src/main/res/layout/fragment_directory.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <arte.programar.materialfile.widget.EmptyRecyclerView\n        android:id=\"@+id/directory_recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:scrollbars=\"vertical\" />\n\n    <androidx.appcompat.widget.LinearLayoutCompat\n        android:id=\"@+id/directory_empty_view\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:orientation=\"vertical\"\n        android:visibility=\"gone\">\n\n        <ImageView\n            android:layout_width=\"110dp\"\n            android:layout_height=\"110dp\"\n            android:layout_gravity=\"center\"\n            android:contentDescription=\"@android:string/yes\"\n            android:src=\"@drawable/ic_app_file\" />\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"4dp\"\n            android:fontFamily=\"sans-serif-medium\"\n            android:text=\"@string/empty_directory_hint\"\n            android:textColor=\"@color/empty_directory_hint\"\n            android:textSize=\"24sp\" />\n\n    </androidx.appcompat.widget.LinearLayoutCompat>\n\n</FrameLayout>"
  },
  {
    "path": "filepicker-lib/src/main/res/layout/item_file.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.appcompat.widget.LinearLayoutCompat xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"@drawable/bg_clickable\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:orientation=\"horizontal\">\n\n    <ImageView\n        android:id=\"@+id/item_file_image\"\n        android:layout_width=\"48dp\"\n        android:layout_height=\"48dp\"\n        android:layout_margin=\"16dp\"\n        android:alpha=\"0.6\"\n        android:contentDescription=\"@android:string/no\"\n        android:src=\"@drawable/ic_app_directory\" />\n\n    <androidx.appcompat.widget.LinearLayoutCompat\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_marginEnd=\"16dp\"\n        android:layout_marginRight=\"16dp\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@+id/item_file_title\"\n            style=\"@style/TextAppearance.AppCompat.Medium\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:singleLine=\"true\"\n            android:textStyle=\"bold\" />\n\n        <TextView\n            android:id=\"@+id/item_file_subtitle\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\" />\n\n    </androidx.appcompat.widget.LinearLayoutCompat>\n\n</androidx.appcompat.widget.LinearLayoutCompat>"
  },
  {
    "path": "filepicker-lib/src/main/res/values/attr.xml",
    "content": "<resources>\n    <attr name=\"mfp_toolbar_theme\" format=\"reference\" />\n</resources>"
  },
  {
    "path": "filepicker-lib/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <!-- Default Colors -->\n    <color name=\"textColorPrimary\">#212121</color>\n    <color name=\"colorControlHighlight\">#4000695C</color>\n\n    <!-- (API 21 Status Bar Color) (API 23 Navigation Bar Color)-->\n    <color name=\"colorPrimaryDarkVariant\">#8a000000</color>\n\n    <color name=\"empty_directory_hint\">#c7c7c7</color>\n\n    <color name=\"compatibility_pressed_color\">#1a000000</color>\n    <color name=\"compatibility_focus_color\">#48000000</color>\n    <color name=\"transparent\">#00000000</color>\n\n</resources>"
  },
  {
    "path": "filepicker-lib/src/main/res/values/dimen.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <!-- Toolbar, 4 dp baseline, https://material.io/guidelines/layout/metrics-keylines.html#metrics-keylines-baseline-grids -->\n    <dimen name=\"appbar_elevation\">4dp</dimen>\n\n</resources>"
  },
  {
    "path": "filepicker-lib/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"file_picker_app_name\" translatable=\"false\">MaterialFilePicker</string>\n    <string name=\"action_close\">Close</string>\n    <string name=\"empty_directory_hint\">Empty directory</string>\n    <string name=\"type_archive\">Archive</string>\n    <string name=\"type_certificate\">Certificate</string>\n    <string name=\"type_directory\">Directory</string>\n    <string name=\"type_document\">File</string>\n    <string name=\"type_drawing\">Picture</string>\n    <string name=\"type_excel\">Spreadsheet</string>\n    <string name=\"type_image\">Image</string>\n    <string name=\"type_music\">Music</string>\n    <string name=\"type_video\">Video</string>\n    <string name=\"type_pdf\">PDF document</string>\n    <string name=\"type_power_point\">Presentation</string>\n    <string name=\"type_word\">Document</string>\n    <string name=\"type_apk\" translatable=\"false\">APK</string>\n    <string name=\"type_json\" translatable=\"false\">JSON</string>\n</resources>"
  },
  {
    "path": "filepicker-lib/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <style name=\"MaterialFileTheme\" parent=\"MaterialFileTheme.Base\" />\n\n    <style name=\"MaterialFileTheme.Base\" parent=\"Theme.Material3.Dark.NoActionBar\">\n\n    </style>\n\n    <style name=\"MaterialFileTheme.AppBar\" parent=\"@style/ThemeOverlay.Material3.Dark\">\n    </style>\n\n    <style name=\"MaterialFileTheme.Toolbar\" parent=\"@style/ThemeOverlay.Material3\" />\n\n</resources>"
  },
  {
    "path": "filepicker-lib/src/main/res/values-cs/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"empty_directory_hint\">Prázdná složka</string>\n    <string name=\"action_close\">Zavřít</string>\n    <string name=\"type_archive\">Archiv</string>\n    <string name=\"type_certificate\">Certifikát</string>\n    <string name=\"type_directory\">Složka</string>\n    <string name=\"type_document\">Soubor</string>\n    <string name=\"type_drawing\">Kresba</string>\n    <string name=\"type_excel\">Tabulka</string>\n    <string name=\"type_image\">Obrázek</string>\n    <string name=\"type_music\">Skladba</string>\n    <string name=\"type_video\">Video</string>\n    <string name=\"type_pdf\">PDF dokument</string>\n    <string name=\"type_power_point\">Prezentace</string>\n    <string name=\"type_word\">Dokument</string>\n</resources>"
  },
  {
    "path": "filepicker-lib/src/main/res/values-de/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"empty_directory_hint\">Leeres Verzeichnis</string>\n    <string name=\"action_close\">Schließen</string>\n    <string name=\"type_archive\">Archiv</string>\n    <string name=\"type_certificate\">Zertifikat</string>\n    <string name=\"type_directory\">Verzeichnis</string>\n    <string name=\"type_document\">Dokument</string>\n    <string name=\"type_drawing\">Bild</string>\n    <string name=\"type_excel\">Excel Tabelle</string>\n    <string name=\"type_image\">Bild</string>\n    <string name=\"type_music\">Musik</string>\n    <string name=\"type_video\">Video</string>\n    <string name=\"type_pdf\">PDF Dokument</string>\n    <string name=\"type_power_point\">Presentation</string>\n    <string name=\"type_word\">Dokument</string>\n</resources>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/values-es/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"type_certificate\">Certificado</string>\n    <string name=\"action_close\">Cerrar</string>\n    <string name=\"type_archive\">Archivo</string>\n    <string name=\"empty_directory_hint\">Carpeta vacía</string>\n    <string name=\"type_directory\">Carpeta</string>\n    <string name=\"type_document\">Archivo</string>\n    <string name=\"type_drawing\">Foto</string>\n    <string name=\"type_excel\">Excel</string>\n    <string name=\"type_image\">Imagen</string>\n    <string name=\"type_music\">Música</string>\n    <string name=\"type_pdf\">Documento PDF</string>\n    <string name=\"type_power_point\">Powerpoint</string>\n    <string name=\"type_video\">Vídeo</string>\n    <string name=\"type_word\">Documento Word</string>\n</resources>"
  },
  {
    "path": "filepicker-lib/src/main/res/values-night/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <!-- Default Colors -->\n    <color name=\"textColorPrimary\">#FFFFFFFF</color>\n    <color name=\"colorControlHighlight\">#33ffffff</color>\n\n    <color name=\"empty_directory_hint\">#c7c7c7</color>\n\n    <color name=\"compatibility_pressed_color\">#1a000000</color>\n    <color name=\"compatibility_focus_color\">#48000000</color>\n    <color name=\"transparent\">#00000000</color>\n\n</resources>"
  },
  {
    "path": "filepicker-lib/src/main/res/values-night/styles.xml",
    "content": "<resources>\n\n    <style name=\"MaterialFileTheme\" parent=\"MaterialFileTheme.Base\" />\n\n    <style name=\"MaterialFileTheme.Base\" parent=\"Theme.Material3.Dark.NoActionBar\">\n\n    </style>\n\n    <style name=\"MaterialFileTheme.AppBar\" parent=\"@style/ThemeOverlay.Material3\" >\n        <item name=\"colorSurface\">@android:color/transparent</item>\n    </style>\n\n    <style name=\"MaterialFileTheme.Toolbar\" parent=\"@style/ThemeOverlay.Material3.Light\" />\n\n</resources>"
  },
  {
    "path": "filepicker-lib/src/main/res/values-night-v21/styles.xml",
    "content": "<resources>\n\n    <style name=\"MaterialFileTheme\" parent=\"MaterialFileTheme.Base\">\n        <!-- StatusBarColor -->\n        <item name=\"android:statusBarColor\">@color/colorPrimaryDarkVariant</item>\n\n        <!-- NavigationBarColor -->\n        <item name=\"android:navigationBarColor\">@color/colorPrimaryDarkVariant</item>\n\n        <!-- Flag indicating whether this Window is responsible for drawing the background -->\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/values-night-v23/styles.xml",
    "content": "<resources>\n\n    <style name=\"MaterialFileTheme\" parent=\"MaterialFileTheme.Base\">\n        <!-- StatusBarColor -->\n        <item name=\"android:statusBarColor\">@color/colorPrimaryDarkVariant</item>\n\n        <!-- NavigationBarColor -->\n        <item name=\"android:navigationBarColor\">@color/colorPrimaryDarkVariant</item>\n\n        <!-- Flag indicating whether this Window is responsible for drawing the background -->\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/values-night-v27/styles.xml",
    "content": "<resources>\n\n    <style name=\"MaterialFileTheme\" parent=\"MaterialFileTheme.Base\">\n        <!-- StatusBarColor -->\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n\n        <!-- Status Bar Icons Light Active -->\n        <item name=\"android:windowLightStatusBar\">false</item>\n\n        <!-- NavigationBarColor -->\n        <item name=\"android:navigationBarColor\">?attr/colorSurface</item>\n\n        <!-- NavigationBar Icons Light Active -->\n        <item name=\"android:windowLightNavigationBar\">false</item>\n\n        <!-- Flag indicating whether this Window is responsible for drawing the background -->\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n\n    </style>\n\n</resources>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/values-night-v29/styles.xml",
    "content": "<resources>\n\n    <style name=\"MaterialFileTheme\" parent=\"MaterialFileTheme.Base\">\n        <!-- StatusBarColor -->\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n\n        <!-- Status Bar Icons Light Active -->\n        <item name=\"android:windowLightStatusBar\">false</item>\n\n        <!-- NavigationBarColor -->\n        <item name=\"android:navigationBarColor\">?attr/colorSurface</item>\n\n        <!-- NavigationBar Icons Light Active -->\n        <item name=\"android:windowLightNavigationBar\">false</item>\n\n        <!-- Flag indicating whether this Window is responsible for drawing the background -->\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n\n    </style>\n\n</resources>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/values-ru/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"empty_directory_hint\">Файлов нет</string>\n    <string name=\"action_close\">Закрыть</string>\n    <string name=\"type_directory\">Каталог</string>\n    <string name=\"type_document\">Файл</string>\n    <string name=\"type_certificate\">Сертификат</string>\n    <string name=\"type_drawing\">Рисунок</string>\n    <string name=\"type_excel\">Таблица</string>\n    <string name=\"type_image\">Картинка</string>\n    <string name=\"type_music\">Аудиофайл</string>\n    <string name=\"type_video\">Видео</string>\n    <string name=\"type_pdf\">Документ PDF</string>\n    <string name=\"type_power_point\">Презентация</string>\n    <string name=\"type_word\">Документ</string>\n    <string name=\"type_archive\">Архив</string>\n</resources>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/values-sk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"empty_directory_hint\">Prázdny priečinok</string>\n    <string name=\"action_close\">Zavrieť</string>\n    <string name=\"type_archive\">Archív</string>\n    <string name=\"type_certificate\">Certifikát</string>\n    <string name=\"type_directory\">Priečinok</string>\n    <string name=\"type_document\">Súbor</string>\n    <string name=\"type_drawing\">Kresba</string>\n    <string name=\"type_excel\">Tabuľka</string>\n    <string name=\"type_image\">Obrázok</string>\n    <string name=\"type_music\">Skladba</string>\n    <string name=\"type_video\">Video</string>\n    <string name=\"type_pdf\">PDF dokument</string>\n    <string name=\"type_power_point\">Prezentácia</string>\n    <string name=\"type_word\">Dokument</string>\n</resources>"
  },
  {
    "path": "filepicker-lib/src/main/res/values-v19/styles.xml",
    "content": "<resources>\n\n    <style name=\"MaterialFileTheme\" parent=\"MaterialFileTheme.Base\">\n        <item name=\"android:windowTranslucentStatus\">true</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/values-v21/styles.xml",
    "content": "<resources>\n\n    <style name=\"MaterialFileTheme\" parent=\"MaterialFileTheme.Base\">\n        <!-- StatusBarColor -->\n        <item name=\"android:statusBarColor\">@color/colorPrimaryDarkVariant</item>\n\n        <!-- NavigationBarColor -->\n        <item name=\"android:navigationBarColor\">@color/colorPrimaryDarkVariant</item>\n\n        <!-- Flag indicating whether this Window is responsible for drawing the background -->\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/values-v23/styles.xml",
    "content": "<resources>\n\n    <style name=\"MaterialFileTheme\" parent=\"MaterialFileTheme.Base\">\n        <!-- StatusBarColor -->\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n\n        <!-- Status Bar Icons Light Active -->\n        <item name=\"android:windowLightStatusBar\">true</item>\n\n        <!-- NavigationBarColor -->\n        <item name=\"android:navigationBarColor\">@color/colorPrimaryDarkVariant</item>\n\n        <!-- Flag indicating whether this Window is responsible for drawing the background -->\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/values-v27/styles.xml",
    "content": "<resources>\n\n    <style name=\"MaterialFileTheme\" parent=\"MaterialFileTheme.Base\">\n        <!-- StatusBarColor -->\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n\n        <!-- Status Bar Icons Light Active -->\n        <item name=\"android:windowLightStatusBar\">true</item>\n\n        <!-- NavigationBarColor -->\n        <item name=\"android:navigationBarColor\">?attr/colorSurface</item>\n\n        <!-- NavigationBar Icons Light Active -->\n        <item name=\"android:windowLightNavigationBar\">true</item>\n\n        <!-- Flag indicating whether this Window is responsible for drawing the background -->\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n\n    </style>\n\n</resources>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/values-v29/styles.xml",
    "content": "<resources>\n\n    <style name=\"MaterialFileTheme\" parent=\"MaterialFileTheme.Base\">\n        <!-- StatusBarColor -->\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n\n        <!-- Status Bar Icons Light Active -->\n        <item name=\"android:windowLightStatusBar\">true</item>\n\n        <!-- NavigationBarColor -->\n        <item name=\"android:navigationBarColor\">@android:color/transparent</item>\n\n        <!-- NavigationBar Icons Light Active -->\n        <item name=\"android:windowLightNavigationBar\">true</item>\n\n        <!-- Flag indicating whether this Window is responsible for drawing the background -->\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n\n    </style>\n\n</resources>\n"
  },
  {
    "path": "filepicker-lib/src/main/res/values-zh/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"empty_directory_hint\">无可选文件</string>\n    <string name=\"action_close\">关</string>\n    <string name=\"type_archive\">压缩包</string>\n    <string name=\"type_certificate\">证书</string>\n    <string name=\"type_directory\">目录</string>\n    <string name=\"type_document\">文件</string>\n    <string name=\"type_drawing\">图片</string>\n    <string name=\"type_excel\">EXCEL</string>\n    <string name=\"type_image\">照片</string>\n    <string name=\"type_music\">音乐</string>\n    <string name=\"type_pdf\">PDF</string>\n    <string name=\"type_power_point\">PPT</string>\n    <string name=\"type_word\">WORD</string>\n    <string name=\"type_video\">视频</string>\n</resources>"
  },
  {
    "path": "fragment-1.1.0/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "fragment-1.1.0/LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "fragment-1.1.0/build.gradle",
    "content": "apply from: gradle.ext.sharedModulesConstants\r\napply plugin: 'com.android.library'\r\n\r\nandroid {\r\n    // FIX: Default interface methods are only supported starting with Android N (--min-api 24)\r\n    compileOptions {\r\n        sourceCompatibility JavaVersion.VERSION_1_8\r\n        targetCompatibility JavaVersion.VERSION_1_8\r\n    }\r\n\r\n    compileSdkVersion project.properties.compileSdkVersion\r\n    buildToolsVersion project.properties.buildToolsVersion\r\n    testOptions.unitTests.includeAndroidResources = true\r\n\r\n    defaultConfig {\r\n        minSdkVersion project.properties.minSdkVersion\r\n        targetSdkVersion project.properties.targetSdkVersion\r\n        versionCode 10\r\n        versionName \"1.0.0\"\r\n\r\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\r\n        consumerProguardFiles 'consumer-rules.pro'\r\n    }\r\n\r\n    buildTypes {\r\n        release {\r\n            minifyEnabled false\r\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\r\n        }\r\n    }\r\n}\r\n\r\ndependencies {\r\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\r\n\r\n    api 'androidx.activity:activity:' + activityXVersion // used inside other app modules\r\n    implementation 'androidx.annotation:annotation:' + annotationXVersion\r\n    implementation 'androidx.core:core:' + coreXVersion\r\n    implementation 'androidx.viewpager:viewpager:' + viewpagerXVersion\r\n    implementation 'androidx.loader:loader:' + loaderXVersion\r\n}\r\n"
  },
  {
    "path": "fragment-1.1.0/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\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-->\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"androidx.fragment\" >\n\n    <!--<uses-sdk android:minSdkVersion=\"17\" />-->\n\n</manifest>"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/BackStackRecord.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.core.util.LogWriter;\nimport androidx.lifecycle.Lifecycle;\n\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\n\n/**\n * Entry of an operation on the fragment back stack.\n */\nfinal class BackStackRecord extends FragmentTransaction implements\n        FragmentManager.BackStackEntry, FragmentManagerImpl.OpGenerator {\n    static final String TAG = FragmentManagerImpl.TAG;\n\n    final FragmentManagerImpl mManager;\n\n    boolean mCommitted;\n    int mIndex = -1;\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder(128);\n        sb.append(\"BackStackEntry{\");\n        sb.append(Integer.toHexString(System.identityHashCode(this)));\n        if (mIndex >= 0) {\n            sb.append(\" #\");\n            sb.append(mIndex);\n        }\n        if (mName != null) {\n            sb.append(\" \");\n            sb.append(mName);\n        }\n        sb.append(\"}\");\n        return sb.toString();\n    }\n\n    public void dump(String prefix, PrintWriter writer) {\n        dump(prefix, writer, true);\n    }\n\n    public void dump(String prefix, PrintWriter writer, boolean full) {\n        if (full) {\n            writer.print(prefix); writer.print(\"mName=\"); writer.print(mName);\n                    writer.print(\" mIndex=\"); writer.print(mIndex);\n                    writer.print(\" mCommitted=\"); writer.println(mCommitted);\n            if (mTransition != FragmentTransaction.TRANSIT_NONE) {\n                writer.print(prefix); writer.print(\"mTransition=#\");\n                        writer.print(Integer.toHexString(mTransition));\n                        writer.print(\" mTransitionStyle=#\");\n                        writer.println(Integer.toHexString(mTransitionStyle));\n            }\n            if (mEnterAnim != 0 || mExitAnim !=0) {\n                writer.print(prefix); writer.print(\"mEnterAnim=#\");\n                        writer.print(Integer.toHexString(mEnterAnim));\n                        writer.print(\" mExitAnim=#\");\n                        writer.println(Integer.toHexString(mExitAnim));\n            }\n            if (mPopEnterAnim != 0 || mPopExitAnim !=0) {\n                writer.print(prefix); writer.print(\"mPopEnterAnim=#\");\n                        writer.print(Integer.toHexString(mPopEnterAnim));\n                        writer.print(\" mPopExitAnim=#\");\n                        writer.println(Integer.toHexString(mPopExitAnim));\n            }\n            if (mBreadCrumbTitleRes != 0 || mBreadCrumbTitleText != null) {\n                writer.print(prefix); writer.print(\"mBreadCrumbTitleRes=#\");\n                        writer.print(Integer.toHexString(mBreadCrumbTitleRes));\n                        writer.print(\" mBreadCrumbTitleText=\");\n                        writer.println(mBreadCrumbTitleText);\n            }\n            if (mBreadCrumbShortTitleRes != 0 || mBreadCrumbShortTitleText != null) {\n                writer.print(prefix); writer.print(\"mBreadCrumbShortTitleRes=#\");\n                        writer.print(Integer.toHexString(mBreadCrumbShortTitleRes));\n                        writer.print(\" mBreadCrumbShortTitleText=\");\n                        writer.println(mBreadCrumbShortTitleText);\n            }\n        }\n\n        if (!mOps.isEmpty()) {\n            writer.print(prefix); writer.println(\"Operations:\");\n            final int numOps = mOps.size();\n            for (int opNum = 0; opNum < numOps; opNum++) {\n                final Op op = mOps.get(opNum);\n                String cmdStr;\n                switch (op.mCmd) {\n                    case OP_NULL: cmdStr=\"NULL\"; break;\n                    case OP_ADD: cmdStr=\"ADD\"; break;\n                    case OP_REPLACE: cmdStr=\"REPLACE\"; break;\n                    case OP_REMOVE: cmdStr=\"REMOVE\"; break;\n                    case OP_HIDE: cmdStr=\"HIDE\"; break;\n                    case OP_SHOW: cmdStr=\"SHOW\"; break;\n                    case OP_DETACH: cmdStr=\"DETACH\"; break;\n                    case OP_ATTACH: cmdStr=\"ATTACH\"; break;\n                    case OP_SET_PRIMARY_NAV: cmdStr=\"SET_PRIMARY_NAV\"; break;\n                    case OP_UNSET_PRIMARY_NAV: cmdStr=\"UNSET_PRIMARY_NAV\";break;\n                    case OP_SET_MAX_LIFECYCLE: cmdStr = \"OP_SET_MAX_LIFECYCLE\"; break;\n                    default: cmdStr = \"cmd=\" + op.mCmd; break;\n                }\n                writer.print(prefix); writer.print(\"  Op #\"); writer.print(opNum);\n                writer.print(\": \"); writer.print(cmdStr);\n                writer.print(\" \"); writer.println(op.mFragment);\n                if (full) {\n                    if (op.mEnterAnim != 0 || op.mExitAnim != 0) {\n                        writer.print(prefix); writer.print(\"enterAnim=#\");\n                        writer.print(Integer.toHexString(op.mEnterAnim));\n                        writer.print(\" exitAnim=#\");\n                        writer.println(Integer.toHexString(op.mExitAnim));\n                    }\n                    if (op.mPopEnterAnim != 0 || op.mPopExitAnim != 0) {\n                        writer.print(prefix); writer.print(\"popEnterAnim=#\");\n                        writer.print(Integer.toHexString(op.mPopEnterAnim));\n                        writer.print(\" popExitAnim=#\");\n                        writer.println(Integer.toHexString(op.mPopExitAnim));\n                    }\n                }\n            }\n        }\n    }\n\n    public BackStackRecord(FragmentManagerImpl manager) {\n        mManager = manager;\n    }\n\n    @Override\n    public int getId() {\n        return mIndex;\n    }\n\n    @Override\n    public int getBreadCrumbTitleRes() {\n        return mBreadCrumbTitleRes;\n    }\n\n    @Override\n    public int getBreadCrumbShortTitleRes() {\n        return mBreadCrumbShortTitleRes;\n    }\n\n    @Override\n    @Nullable\n    public CharSequence getBreadCrumbTitle() {\n        if (mBreadCrumbTitleRes != 0) {\n            return mManager.mHost.getContext().getText(mBreadCrumbTitleRes);\n        }\n        return mBreadCrumbTitleText;\n    }\n\n    @Override\n    @Nullable\n    public CharSequence getBreadCrumbShortTitle() {\n        if (mBreadCrumbShortTitleRes != 0) {\n            return mManager.mHost.getContext().getText(mBreadCrumbShortTitleRes);\n        }\n        return mBreadCrumbShortTitleText;\n    }\n\n    @Override\n    void doAddOp(int containerViewId, Fragment fragment, @Nullable String tag, int opcmd) {\n        super.doAddOp(containerViewId, fragment, tag, opcmd);\n        fragment.mFragmentManager = mManager;\n    }\n\n    @NonNull\n    @Override\n    public FragmentTransaction remove(@NonNull Fragment fragment) {\n        if (fragment.mFragmentManager != null && fragment.mFragmentManager != mManager) {\n            throw new IllegalStateException(\"Cannot remove Fragment attached to \"\n                    + \"a different FragmentManager. Fragment \" + fragment.toString() + \" is already\"\n                    + \" attached to a FragmentManager.\");\n        }\n        return super.remove(fragment);\n    }\n\n    @NonNull\n    @Override\n    public FragmentTransaction hide(@NonNull Fragment fragment) {\n        if (fragment.mFragmentManager != null && fragment.mFragmentManager != mManager) {\n            throw new IllegalStateException(\"Cannot hide Fragment attached to \"\n                    + \"a different FragmentManager. Fragment \" + fragment.toString() + \" is already\"\n                    + \" attached to a FragmentManager.\");\n        }\n        return super.hide(fragment);\n    }\n\n    @NonNull\n    @Override\n    public FragmentTransaction show(@NonNull Fragment fragment) {\n        if (fragment.mFragmentManager != null && fragment.mFragmentManager != mManager) {\n            throw new IllegalStateException(\"Cannot show Fragment attached to \"\n                    + \"a different FragmentManager. Fragment \" + fragment.toString() + \" is already\"\n                    + \" attached to a FragmentManager.\");\n        }\n        return super.show(fragment);\n    }\n\n    @NonNull\n    @Override\n    public FragmentTransaction detach(@NonNull Fragment fragment) {\n        if (fragment.mFragmentManager != null && fragment.mFragmentManager != mManager) {\n            throw new IllegalStateException(\"Cannot detach Fragment attached to \"\n                    + \"a different FragmentManager. Fragment \" + fragment.toString() + \" is already\"\n                    + \" attached to a FragmentManager.\");\n        }\n        return super.detach(fragment);\n    }\n\n    @NonNull\n    @Override\n    public FragmentTransaction setPrimaryNavigationFragment(@Nullable Fragment fragment) {\n        if (fragment != null\n                && fragment.mFragmentManager != null && fragment.mFragmentManager != mManager) {\n            throw new IllegalStateException(\"Cannot setPrimaryNavigation for Fragment attached to \"\n                    + \"a different FragmentManager. Fragment \" + fragment.toString() + \" is already\"\n                    + \" attached to a FragmentManager.\");\n        }\n        return super.setPrimaryNavigationFragment(fragment);\n    }\n\n    @NonNull\n    @Override\n    public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment,\n            @NonNull Lifecycle.State state) {\n        if (fragment.mFragmentManager != mManager) {\n            throw new IllegalArgumentException(\"Cannot setMaxLifecycle for Fragment not attached to\"\n                    + \" FragmentManager \" + mManager);\n        }\n        if (!state.isAtLeast(Lifecycle.State.CREATED)) {\n            throw new IllegalArgumentException(\"Cannot set maximum Lifecycle below \"\n                    + Lifecycle.State.CREATED);\n        }\n        return super.setMaxLifecycle(fragment, state);\n    }\n\n    void bumpBackStackNesting(int amt) {\n        if (!mAddToBackStack) {\n            return;\n        }\n        if (FragmentManagerImpl.DEBUG) Log.v(TAG, \"Bump nesting in \" + this\n                + \" by \" + amt);\n        final int numOps = mOps.size();\n        for (int opNum = 0; opNum < numOps; opNum++) {\n            final Op op = mOps.get(opNum);\n            if (op.mFragment != null) {\n                op.mFragment.mBackStackNesting += amt;\n                if (FragmentManagerImpl.DEBUG) Log.v(TAG, \"Bump nesting of \"\n                        + op.mFragment + \" to \" + op.mFragment.mBackStackNesting);\n            }\n        }\n    }\n\n    public void runOnCommitRunnables() {\n        if (mCommitRunnables != null) {\n            for (int i = 0; i < mCommitRunnables.size(); i++) {\n                mCommitRunnables.get(i).run();\n            }\n            mCommitRunnables = null;\n        }\n    }\n\n    @Override\n    public int commit() {\n        return commitInternal(false);\n    }\n\n    @Override\n    public int commitAllowingStateLoss() {\n        return commitInternal(true);\n    }\n\n    @Override\n    public void commitNow() {\n        disallowAddToBackStack();\n        mManager.execSingleAction(this, false);\n    }\n\n    @Override\n    public void commitNowAllowingStateLoss() {\n        disallowAddToBackStack();\n        mManager.execSingleAction(this, true);\n    }\n\n    int commitInternal(boolean allowStateLoss) {\n        if (mCommitted) throw new IllegalStateException(\"commit already called\");\n        if (FragmentManagerImpl.DEBUG) {\n            Log.v(TAG, \"Commit: \" + this);\n            LogWriter logw = new LogWriter(TAG);\n            PrintWriter pw = new PrintWriter(logw);\n            dump(\"  \", pw);\n            pw.close();\n        }\n        mCommitted = true;\n        if (mAddToBackStack) {\n            mIndex = mManager.allocBackStackIndex(this);\n        } else {\n            mIndex = -1;\n        }\n        mManager.enqueueAction(this, allowStateLoss);\n        return mIndex;\n    }\n\n    /**\n     * Implementation of {@link FragmentManagerImpl.OpGenerator}.\n     * This operation is added to the list of pending actions during {@link #commit()}, and\n     * will be executed on the UI thread to run this FragmentTransaction.\n     *\n     * @param records Modified to add this BackStackRecord\n     * @param isRecordPop Modified to add a false (this isn't a pop)\n     * @return true always because the records and isRecordPop will always be changed\n     */\n    @Override\n    public boolean generateOps(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop) {\n        if (FragmentManagerImpl.DEBUG) {\n            Log.v(TAG, \"Run: \" + this);\n        }\n\n        records.add(this);\n        isRecordPop.add(false);\n        if (mAddToBackStack) {\n            mManager.addBackStackState(this);\n        }\n        return true;\n    }\n\n    boolean interactsWith(int containerId) {\n        final int numOps = mOps.size();\n        for (int opNum = 0; opNum < numOps; opNum++) {\n            final Op op = mOps.get(opNum);\n            final int fragContainer = op.mFragment != null ? op.mFragment.mContainerId : 0;\n            if (fragContainer != 0 && fragContainer == containerId) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    boolean interactsWith(ArrayList<BackStackRecord> records, int startIndex, int endIndex) {\n        if (endIndex == startIndex) {\n            return false;\n        }\n        final int numOps = mOps.size();\n        int lastContainer = -1;\n        for (int opNum = 0; opNum < numOps; opNum++) {\n            final Op op = mOps.get(opNum);\n            final int container = op.mFragment != null ? op.mFragment.mContainerId : 0;\n            if (container != 0 && container != lastContainer) {\n                lastContainer = container;\n                for (int i = startIndex; i < endIndex; i++) {\n                    BackStackRecord record = records.get(i);\n                    final int numThoseOps = record.mOps.size();\n                    for (int thoseOpIndex = 0; thoseOpIndex < numThoseOps; thoseOpIndex++) {\n                        final Op thatOp = record.mOps.get(thoseOpIndex);\n                        final int thatContainer = thatOp.mFragment != null\n                                ? thatOp.mFragment.mContainerId : 0;\n                        if (thatContainer == container) {\n                            return true;\n                        }\n                    }\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Executes the operations contained within this transaction. The Fragment states will only\n     * be modified if optimizations are not allowed.\n     */\n    void executeOps() {\n        final int numOps = mOps.size();\n        for (int opNum = 0; opNum < numOps; opNum++) {\n            final Op op = mOps.get(opNum);\n            final Fragment f = op.mFragment;\n            if (f != null) {\n                f.setNextTransition(mTransition, mTransitionStyle);\n            }\n            switch (op.mCmd) {\n                case OP_ADD:\n                    f.setNextAnim(op.mEnterAnim);\n                    mManager.addFragment(f, false);\n                    break;\n                case OP_REMOVE:\n                    f.setNextAnim(op.mExitAnim);\n                    mManager.removeFragment(f);\n                    break;\n                case OP_HIDE:\n                    f.setNextAnim(op.mExitAnim);\n                    mManager.hideFragment(f);\n                    break;\n                case OP_SHOW:\n                    f.setNextAnim(op.mEnterAnim);\n                    mManager.showFragment(f);\n                    break;\n                case OP_DETACH:\n                    f.setNextAnim(op.mExitAnim);\n                    mManager.detachFragment(f);\n                    break;\n                case OP_ATTACH:\n                    f.setNextAnim(op.mEnterAnim);\n                    mManager.attachFragment(f);\n                    break;\n                case OP_SET_PRIMARY_NAV:\n                    mManager.setPrimaryNavigationFragment(f);\n                    break;\n                case OP_UNSET_PRIMARY_NAV:\n                    mManager.setPrimaryNavigationFragment(null);\n                    break;\n                case OP_SET_MAX_LIFECYCLE:\n                    mManager.setMaxLifecycle(f, op.mCurrentMaxState);\n                    break;\n                default:\n                    throw new IllegalArgumentException(\"Unknown cmd: \" + op.mCmd);\n            }\n            if (!mReorderingAllowed && op.mCmd != OP_ADD && f != null) {\n                mManager.moveFragmentToExpectedState(f);\n            }\n        }\n        if (!mReorderingAllowed) {\n            // Added fragments are added at the end to comply with prior behavior.\n            mManager.moveToState(mManager.mCurState, true);\n        }\n    }\n\n    /**\n     * Reverses the execution of the operations within this transaction. The Fragment states will\n     * only be modified if reordering is not allowed.\n     *\n     * @param moveToState {@code true} if added fragments should be moved to their final state\n     *                    in ordered transactions\n     */\n    void executePopOps(boolean moveToState) {\n        for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) {\n            final Op op = mOps.get(opNum);\n            Fragment f = op.mFragment;\n            if (f != null) {\n                f.setNextTransition(FragmentManagerImpl.reverseTransit(mTransition),\n                        mTransitionStyle);\n            }\n            switch (op.mCmd) {\n                case OP_ADD:\n                    f.setNextAnim(op.mPopExitAnim);\n                    mManager.removeFragment(f);\n                    break;\n                case OP_REMOVE:\n                    f.setNextAnim(op.mPopEnterAnim);\n                    mManager.addFragment(f, false);\n                    break;\n                case OP_HIDE:\n                    f.setNextAnim(op.mPopEnterAnim);\n                    mManager.showFragment(f);\n                    break;\n                case OP_SHOW:\n                    f.setNextAnim(op.mPopExitAnim);\n                    mManager.hideFragment(f);\n                    break;\n                case OP_DETACH:\n                    f.setNextAnim(op.mPopEnterAnim);\n                    mManager.attachFragment(f);\n                    break;\n                case OP_ATTACH:\n                    f.setNextAnim(op.mPopExitAnim);\n                    mManager.detachFragment(f);\n                    break;\n                case OP_SET_PRIMARY_NAV:\n                    mManager.setPrimaryNavigationFragment(null);\n                    break;\n                case OP_UNSET_PRIMARY_NAV:\n                    mManager.setPrimaryNavigationFragment(f);\n                    break;\n                case OP_SET_MAX_LIFECYCLE:\n                    mManager.setMaxLifecycle(f, op.mOldMaxState);\n                    break;\n                default:\n                    throw new IllegalArgumentException(\"Unknown cmd: \" + op.mCmd);\n            }\n            if (!mReorderingAllowed && op.mCmd != OP_REMOVE && f != null) {\n                mManager.moveFragmentToExpectedState(f);\n            }\n        }\n        if (!mReorderingAllowed && moveToState) {\n            mManager.moveToState(mManager.mCurState, true);\n        }\n    }\n\n    /**\n     * Expands all meta-ops into their more primitive equivalents. This must be called prior to\n     * {@link #executeOps()} or any other call that operations on mOps for forward navigation.\n     * It should not be called for pop/reverse navigation operations.\n     *\n     * <p>Removes all OP_REPLACE ops and replaces them with the proper add and remove\n     * operations that are equivalent to the replace.</p>\n     *\n     * <p>Adds OP_UNSET_PRIMARY_NAV ops to match OP_SET_PRIMARY_NAV, OP_REMOVE and OP_DETACH\n     * ops so that we can restore the old primary nav fragment later. Since callers call this\n     * method in a loop before running ops from several transactions at once, the caller should\n     * pass the return value from this method as the oldPrimaryNav parameter for the next call.\n     * The first call in such a loop should pass the value of\n     * {@link FragmentManager#getPrimaryNavigationFragment()}.</p>\n     *\n     * @param added Initialized to the fragments that are in the mManager.mAdded, this\n     *              will be modified to contain the fragments that will be in mAdded\n     *              after the execution ({@link #executeOps()}.\n     * @param oldPrimaryNav The tracked primary navigation fragment as of the beginning of\n     *                      this set of ops\n     * @return the new oldPrimaryNav fragment after this record's ops would be run\n     */\n    @SuppressWarnings(\"ReferenceEquality\")\n    Fragment expandOps(ArrayList<Fragment> added, Fragment oldPrimaryNav) {\n        for (int opNum = 0; opNum < mOps.size(); opNum++) {\n            final Op op = mOps.get(opNum);\n            switch (op.mCmd) {\n                case OP_ADD:\n                case OP_ATTACH:\n                    added.add(op.mFragment);\n                    break;\n                case OP_REMOVE:\n                case OP_DETACH: {\n                    added.remove(op.mFragment);\n                    if (op.mFragment == oldPrimaryNav) {\n                        mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, op.mFragment));\n                        opNum++;\n                        oldPrimaryNav = null;\n                    }\n                }\n                break;\n                case OP_REPLACE: {\n                    final Fragment f = op.mFragment;\n                    final int containerId = f.mContainerId;\n                    boolean alreadyAdded = false;\n                    for (int i = added.size() - 1; i >= 0; i--) {\n                        final Fragment old = added.get(i);\n                        if (old.mContainerId == containerId) {\n                            if (old == f) {\n                                alreadyAdded = true;\n                            } else {\n                                // This is duplicated from above since we only make\n                                // a single pass for expanding ops. Unset any outgoing primary nav.\n                                if (old == oldPrimaryNav) {\n                                    mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, old));\n                                    opNum++;\n                                    oldPrimaryNav = null;\n                                }\n                                final Op removeOp = new Op(OP_REMOVE, old);\n                                removeOp.mEnterAnim = op.mEnterAnim;\n                                removeOp.mPopEnterAnim = op.mPopEnterAnim;\n                                removeOp.mExitAnim = op.mExitAnim;\n                                removeOp.mPopExitAnim = op.mPopExitAnim;\n                                mOps.add(opNum, removeOp);\n                                added.remove(old);\n                                opNum++;\n                            }\n                        }\n                    }\n                    if (alreadyAdded) {\n                        mOps.remove(opNum);\n                        opNum--;\n                    } else {\n                        op.mCmd = OP_ADD;\n                        added.add(f);\n                    }\n                }\n                break;\n                case OP_SET_PRIMARY_NAV: {\n                    // It's ok if this is null, that means we will restore to no active\n                    // primary navigation fragment on a pop.\n                    mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, oldPrimaryNav));\n                    opNum++;\n                    // Will be set by the OP_SET_PRIMARY_NAV we inserted before when run\n                    oldPrimaryNav = op.mFragment;\n                }\n                break;\n            }\n        }\n        return oldPrimaryNav;\n    }\n\n    /**\n     * Removes fragments that are added or removed during a pop operation.\n     *\n     * @param added Initialized to the fragments that are in the mManager.mAdded, this\n     *              will be modified to contain the fragments that will be in mAdded\n     *              after the execution ({@link #executeOps()}.\n     * @param oldPrimaryNav The tracked primary navigation fragment as of the beginning of\n     *                      this set of ops\n     * @return the new oldPrimaryNav fragment after this record's ops would be popped\n     */\n    Fragment trackAddedFragmentsInPop(ArrayList<Fragment> added, Fragment oldPrimaryNav) {\n        for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) {\n            final Op op = mOps.get(opNum);\n            switch (op.mCmd) {\n                case OP_ADD:\n                case OP_ATTACH:\n                    added.remove(op.mFragment);\n                    break;\n                case OP_REMOVE:\n                case OP_DETACH:\n                    added.add(op.mFragment);\n                    break;\n                case OP_UNSET_PRIMARY_NAV:\n                    oldPrimaryNav = op.mFragment;\n                    break;\n                case OP_SET_PRIMARY_NAV:\n                    oldPrimaryNav = null;\n                    break;\n                case OP_SET_MAX_LIFECYCLE:\n                    op.mCurrentMaxState = op.mOldMaxState;\n                    break;\n            }\n        }\n        return oldPrimaryNav;\n    }\n\n    boolean isPostponed() {\n        for (int opNum = 0; opNum < mOps.size(); opNum++) {\n            final Op op = mOps.get(opNum);\n            if (isFragmentPostponed(op)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    void setOnStartPostponedListener(Fragment.OnStartEnterTransitionListener listener) {\n        for (int opNum = 0; opNum < mOps.size(); opNum++) {\n            final Op op = mOps.get(opNum);\n            if (isFragmentPostponed(op)) {\n                op.mFragment.setOnStartEnterTransitionListener(listener);\n            }\n        }\n    }\n\n    private static boolean isFragmentPostponed(Op op) {\n        final Fragment fragment = op.mFragment;\n        return fragment != null && fragment.mAdded && fragment.mView != null && !fragment.mDetached\n                && !fragment.mHidden && fragment.isPostponed();\n    }\n\n    @Override\n    @Nullable\n    public String getName() {\n        return mName;\n    }\n\n    @Override\n    public boolean isEmpty() {\n        return mOps.isEmpty();\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/BackStackState.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.annotation.SuppressLint;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport androidx.lifecycle.Lifecycle;\n\nimport java.util.ArrayList;\n\n@SuppressLint(\"BanParcelableUsage\")\nfinal class BackStackState implements Parcelable {\n    final int[] mOps;\n    final ArrayList<String> mFragmentWhos;\n    final int[] mOldMaxLifecycleStates;\n    final int[] mCurrentMaxLifecycleStates;\n    final int mTransition;\n    final int mTransitionStyle;\n    final String mName;\n    final int mIndex;\n    final int mBreadCrumbTitleRes;\n    final CharSequence mBreadCrumbTitleText;\n    final int mBreadCrumbShortTitleRes;\n    final CharSequence mBreadCrumbShortTitleText;\n    final ArrayList<String> mSharedElementSourceNames;\n    final ArrayList<String> mSharedElementTargetNames;\n    final boolean mReorderingAllowed;\n\n    public BackStackState(BackStackRecord bse) {\n        final int numOps = bse.mOps.size();\n        mOps = new int[numOps * 5];\n\n        if (!bse.mAddToBackStack) {\n            throw new IllegalStateException(\"Not on back stack\");\n        }\n\n        mFragmentWhos = new ArrayList<>(numOps);\n        mOldMaxLifecycleStates = new int[numOps];\n        mCurrentMaxLifecycleStates = new int[numOps];\n        int pos = 0;\n        for (int opNum = 0; opNum < numOps; opNum++) {\n            final BackStackRecord.Op op = bse.mOps.get(opNum);\n            mOps[pos++] = op.mCmd;\n            mFragmentWhos.add(op.mFragment != null ? op.mFragment.mWho : null);\n            mOps[pos++] = op.mEnterAnim;\n            mOps[pos++] = op.mExitAnim;\n            mOps[pos++] = op.mPopEnterAnim;\n            mOps[pos++] = op.mPopExitAnim;\n            mOldMaxLifecycleStates[opNum] = op.mOldMaxState.ordinal();\n            mCurrentMaxLifecycleStates[opNum] = op.mCurrentMaxState.ordinal();\n        }\n        mTransition = bse.mTransition;\n        mTransitionStyle = bse.mTransitionStyle;\n        mName = bse.mName;\n        mIndex = bse.mIndex;\n        mBreadCrumbTitleRes = bse.mBreadCrumbTitleRes;\n        mBreadCrumbTitleText = bse.mBreadCrumbTitleText;\n        mBreadCrumbShortTitleRes = bse.mBreadCrumbShortTitleRes;\n        mBreadCrumbShortTitleText = bse.mBreadCrumbShortTitleText;\n        mSharedElementSourceNames = bse.mSharedElementSourceNames;\n        mSharedElementTargetNames = bse.mSharedElementTargetNames;\n        mReorderingAllowed = bse.mReorderingAllowed;\n    }\n\n    public BackStackState(Parcel in) {\n        mOps = in.createIntArray();\n        mFragmentWhos = in.createStringArrayList();\n        mOldMaxLifecycleStates = in.createIntArray();\n        mCurrentMaxLifecycleStates = in.createIntArray();\n        mTransition = in.readInt();\n        mTransitionStyle = in.readInt();\n        mName = in.readString();\n        mIndex = in.readInt();\n        mBreadCrumbTitleRes = in.readInt();\n        mBreadCrumbTitleText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);\n        mBreadCrumbShortTitleRes = in.readInt();\n        mBreadCrumbShortTitleText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);\n        mSharedElementSourceNames = in.createStringArrayList();\n        mSharedElementTargetNames = in.createStringArrayList();\n        mReorderingAllowed = in.readInt() != 0;\n    }\n\n    public BackStackRecord instantiate(FragmentManagerImpl fm) {\n        BackStackRecord bse = new BackStackRecord(fm);\n        int pos = 0;\n        int num = 0;\n        while (pos < mOps.length) {\n            BackStackRecord.Op op = new BackStackRecord.Op();\n            op.mCmd = mOps[pos++];\n            if (FragmentManagerImpl.DEBUG) Log.v(FragmentManagerImpl.TAG,\n                    \"Instantiate \" + bse + \" op #\" + num + \" base fragment #\" + mOps[pos]);\n            String fWho = mFragmentWhos.get(num);\n            if (fWho != null) {\n                Fragment f = fm.mActive.get(fWho);\n                op.mFragment = f;\n            } else {\n                op.mFragment = null;\n            }\n            op.mOldMaxState = Lifecycle.State.values()[mOldMaxLifecycleStates[num]];\n            op.mCurrentMaxState = Lifecycle.State.values()[mCurrentMaxLifecycleStates[num]];\n            op.mEnterAnim = mOps[pos++];\n            op.mExitAnim = mOps[pos++];\n            op.mPopEnterAnim = mOps[pos++];\n            op.mPopExitAnim = mOps[pos++];\n            bse.mEnterAnim = op.mEnterAnim;\n            bse.mExitAnim = op.mExitAnim;\n            bse.mPopEnterAnim = op.mPopEnterAnim;\n            bse.mPopExitAnim = op.mPopExitAnim;\n            bse.addOp(op);\n            num++;\n        }\n        bse.mTransition = mTransition;\n        bse.mTransitionStyle = mTransitionStyle;\n        bse.mName = mName;\n        bse.mIndex = mIndex;\n        bse.mAddToBackStack = true;\n        bse.mBreadCrumbTitleRes = mBreadCrumbTitleRes;\n        bse.mBreadCrumbTitleText = mBreadCrumbTitleText;\n        bse.mBreadCrumbShortTitleRes = mBreadCrumbShortTitleRes;\n        bse.mBreadCrumbShortTitleText = mBreadCrumbShortTitleText;\n        bse.mSharedElementSourceNames = mSharedElementSourceNames;\n        bse.mSharedElementTargetNames = mSharedElementTargetNames;\n        bse.mReorderingAllowed = mReorderingAllowed;\n        bse.bumpBackStackNesting(1);\n        return bse;\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.writeIntArray(mOps);\n        dest.writeStringList(mFragmentWhos);\n        dest.writeIntArray(mOldMaxLifecycleStates);\n        dest.writeIntArray(mCurrentMaxLifecycleStates);\n        dest.writeInt(mTransition);\n        dest.writeInt(mTransitionStyle);\n        dest.writeString(mName);\n        dest.writeInt(mIndex);\n        dest.writeInt(mBreadCrumbTitleRes);\n        TextUtils.writeToParcel(mBreadCrumbTitleText, dest, 0);\n        dest.writeInt(mBreadCrumbShortTitleRes);\n        TextUtils.writeToParcel(mBreadCrumbShortTitleText, dest, 0);\n        dest.writeStringList(mSharedElementSourceNames);\n        dest.writeStringList(mSharedElementTargetNames);\n        dest.writeInt(mReorderingAllowed ? 1 : 0);\n    }\n\n    public static final Parcelable.Creator<BackStackState> CREATOR\n            = new Parcelable.Creator<BackStackState>() {\n        @Override\n        public BackStackState createFromParcel(Parcel in) {\n            return new BackStackState(in);\n        }\n\n        @Override\n        public BackStackState[] newArray(int size) {\n            return new BackStackState[size];\n        }\n    };\n}"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/DialogFragment.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;\n\nimport android.app.Activity;\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.Window;\nimport android.view.WindowManager;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RestrictTo;\nimport androidx.annotation.StyleRes;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Static library support version of the framework's {@link android.app.DialogFragment}.\n * Used to write apps that run on platforms prior to Android 3.0.  When running\n * on Android 3.0 or above, this implementation is still used; it does not try\n * to switch to the framework's implementation.  See the framework SDK\n * documentation for a class overview.\n */\npublic class DialogFragment extends Fragment\n        implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener {\n\n    /** @hide */\n    @RestrictTo(LIBRARY_GROUP_PREFIX)\n    @IntDef({STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT})\n    @Retention(RetentionPolicy.SOURCE)\n    private @interface DialogStyle {}\n\n    /**\n     * Style for {@link #setStyle(int, int)}: a basic,\n     * normal dialog.\n     */\n    public static final int STYLE_NORMAL = 0;\n\n    /**\n     * Style for {@link #setStyle(int, int)}: don't include\n     * a title area.\n     */\n    public static final int STYLE_NO_TITLE = 1;\n\n    /**\n     * Style for {@link #setStyle(int, int)}: don't draw\n     * any frame at all; the view hierarchy returned by {@link #onCreateView}\n     * is entirely responsible for drawing the dialog.\n     */\n    public static final int STYLE_NO_FRAME = 2;\n\n    /**\n     * Style for {@link #setStyle(int, int)}: like\n     * {@link #STYLE_NO_FRAME}, but also disables all input to the dialog.\n     * The user can not touch it, and its window will not receive input focus.\n     */\n    public static final int STYLE_NO_INPUT = 3;\n\n    private static final String SAVED_DIALOG_STATE_TAG = \"android:savedDialogState\";\n    private static final String SAVED_STYLE = \"android:style\";\n    private static final String SAVED_THEME = \"android:theme\";\n    private static final String SAVED_CANCELABLE = \"android:cancelable\";\n    private static final String SAVED_SHOWS_DIALOG = \"android:showsDialog\";\n    private static final String SAVED_BACK_STACK_ID = \"android:backStackId\";\n\n    private Handler mHandler;\n    private Runnable mDismissRunnable = new Runnable() {\n        @Override\n        public void run() {\n            if (mDialog != null) {\n                onDismiss(mDialog);\n            }\n        }\n    };\n    int mStyle = STYLE_NORMAL;\n    int mTheme = 0;\n    boolean mCancelable = true;\n    boolean mShowsDialog = true;\n    int mBackStackId = -1;\n\n    @Nullable Dialog mDialog;\n    boolean mViewDestroyed;\n    boolean mDismissed;\n    boolean mShownByMe;\n\n    public DialogFragment() {\n    }\n\n    /**\n     * Call to customize the basic appearance and behavior of the\n     * fragment's dialog.  This can be used for some common dialog behaviors,\n     * taking care of selecting flags, theme, and other options for you.  The\n     * same effect can be achieve by manually setting Dialog and Window\n     * attributes yourself.  Calling this after the fragment's Dialog is\n     * created will have no effect.\n     *\n     * @param style Selects a standard style: may be {@link #STYLE_NORMAL},\n     * {@link #STYLE_NO_TITLE}, {@link #STYLE_NO_FRAME}, or\n     * {@link #STYLE_NO_INPUT}.\n     * @param theme Optional custom theme.  If 0, an appropriate theme (based\n     * on the style) will be selected for you.\n     */\n    public void setStyle(@DialogStyle int style, @StyleRes int theme) {\n        mStyle = style;\n        if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) {\n            mTheme = android.R.style.Theme_Panel;\n        }\n        if (theme != 0) {\n            mTheme = theme;\n        }\n    }\n\n    /**\n     * Display the dialog, adding the fragment to the given FragmentManager.  This\n     * is a convenience for explicitly creating a transaction, adding the\n     * fragment to it with the given tag, and {@link FragmentTransaction#commit() committing} it.\n     * This does <em>not</em> add the transaction to the fragment back stack.  When the fragment\n     * is dismissed, a new transaction will be executed to remove it from\n     * the activity.\n     * @param manager The FragmentManager this fragment will be added to.\n     * @param tag The tag for this fragment, as per\n     * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}.\n     */\n    public void show(@NonNull FragmentManager manager, @Nullable String tag) {\n        mDismissed = false;\n        mShownByMe = true;\n        FragmentTransaction ft = manager.beginTransaction();\n        ft.add(this, tag);\n        ft.commit();\n    }\n\n    /**\n     * Display the dialog, adding the fragment using an existing transaction\n     * and then {@link FragmentTransaction#commit() committing} the transaction.\n     * @param transaction An existing transaction in which to add the fragment.\n     * @param tag The tag for this fragment, as per\n     * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}.\n     * @return Returns the identifier of the committed transaction, as per\n     * {@link FragmentTransaction#commit() FragmentTransaction.commit()}.\n     */\n    public int show(@NonNull FragmentTransaction transaction, @Nullable String tag) {\n        mDismissed = false;\n        mShownByMe = true;\n        transaction.add(this, tag);\n        mViewDestroyed = false;\n        mBackStackId = transaction.commit();\n        return mBackStackId;\n    }\n\n    /**\n     * Display the dialog, immediately adding the fragment to the given FragmentManager.  This\n     * is a convenience for explicitly creating a transaction, adding the\n     * fragment to it with the given tag, and calling {@link FragmentTransaction#commitNow()}.\n     * This does <em>not</em> add the transaction to the fragment back stack.  When the fragment\n     * is dismissed, a new transaction will be executed to remove it from\n     * the activity.\n     * @param manager The FragmentManager this fragment will be added to.\n     * @param tag The tag for this fragment, as per\n     * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}.\n     */\n    public void showNow(@NonNull FragmentManager manager, @Nullable String tag) {\n        mDismissed = false;\n        mShownByMe = true;\n        FragmentTransaction ft = manager.beginTransaction();\n        ft.add(this, tag);\n        ft.commitNow();\n    }\n\n    /**\n     * Dismiss the fragment and its dialog.  If the fragment was added to the\n     * back stack, all back stack state up to and including this entry will\n     * be popped.  Otherwise, a new transaction will be committed to remove\n     * the fragment.\n     */\n    public void dismiss() {\n        dismissInternal(false, false);\n    }\n\n    /**\n     * Version of {@link #dismiss()} that uses\n     * {@link FragmentTransaction#commitAllowingStateLoss()\n     * FragmentTransaction.commitAllowingStateLoss()}. See linked\n     * documentation for further details.\n     */\n    public void dismissAllowingStateLoss() {\n        dismissInternal(true, false);\n    }\n\n    void dismissInternal(boolean allowStateLoss, boolean fromOnDismiss) {\n        if (mDismissed) {\n            return;\n        }\n        mDismissed = true;\n        mShownByMe = false;\n        if (mDialog != null) {\n            // Instead of waiting for a posted onDismiss(), null out\n            // the listener and call onDismiss() manually to ensure\n            // that the callback happens before onDestroy()\n            mDialog.setOnDismissListener(null);\n            mDialog.dismiss();\n            if (!fromOnDismiss) {\n                // onDismiss() is always called on the main thread, so\n                // we mimic that behavior here. The difference here is that\n                // we don't post the message to ensure that the onDismiss()\n                // callback still happens before onDestroy()\n                if (Looper.myLooper() == mHandler.getLooper()) {\n                    onDismiss(mDialog);\n                } else {\n                    mHandler.post(mDismissRunnable);\n                }\n            }\n        }\n        mViewDestroyed = true;\n        if (mBackStackId >= 0) {\n            requireFragmentManager().popBackStack(mBackStackId,\n                    FragmentManager.POP_BACK_STACK_INCLUSIVE);\n            mBackStackId = -1;\n        } else {\n            FragmentTransaction ft = requireFragmentManager().beginTransaction();\n            ft.remove(this);\n            if (allowStateLoss) {\n                ft.commitAllowingStateLoss();\n            } else {\n                ft.commit();\n            }\n        }\n    }\n\n    /**\n     * Return the {@link Dialog} this fragment is currently controlling.\n     *\n     * @see #requireDialog()\n     */\n    @Nullable\n    public Dialog getDialog() {\n        return mDialog;\n    }\n\n    /**\n     * Return the {@link Dialog} this fragment is currently controlling.\n     *\n     * @throws IllegalStateException if the Dialog has not yet been created (before\n     * {@link #onCreateDialog(Bundle)}) or has been destroyed (after {@link #onDestroyView()}.\n     * @see #getDialog()\n     */\n    @NonNull\n    public final Dialog requireDialog() {\n        Dialog dialog = getDialog();\n        if (dialog == null) {\n            throw new IllegalStateException(\"DialogFragment \" + this + \" does not have a Dialog.\");\n        }\n        return dialog;\n    }\n\n    @StyleRes\n    public int getTheme() {\n        return mTheme;\n    }\n\n    /**\n     * Control whether the shown Dialog is cancelable.  Use this instead of\n     * directly calling {@link Dialog#setCancelable(boolean)\n     * Dialog.setCancelable(boolean)}, because DialogFragment needs to change\n     * its behavior based on this.\n     *\n     * @param cancelable If true, the dialog is cancelable.  The default\n     * is true.\n     */\n    public void setCancelable(boolean cancelable) {\n        mCancelable = cancelable;\n        if (mDialog != null) mDialog.setCancelable(cancelable);\n    }\n\n    /**\n     * Return the current value of {@link #setCancelable(boolean)}.\n     */\n    public boolean isCancelable() {\n        return mCancelable;\n    }\n\n    /**\n     * Controls whether this fragment should be shown in a dialog.  If not\n     * set, no Dialog will be created in {@link #onActivityCreated(Bundle)},\n     * and the fragment's view hierarchy will thus not be added to it.  This\n     * allows you to instead use it as a normal fragment (embedded inside of\n     * its activity).\n     *\n     * <p>This is normally set for you based on whether the fragment is\n     * associated with a container view ID passed to\n     * {@link FragmentTransaction#add(int, Fragment) FragmentTransaction.add(int, Fragment)}.\n     * If the fragment was added with a container, setShowsDialog will be\n     * initialized to false; otherwise, it will be true.\n     *\n     * @param showsDialog If true, the fragment will be displayed in a Dialog.\n     * If false, no Dialog will be created and the fragment's view hierarchy\n     * left undisturbed.\n     */\n    public void setShowsDialog(boolean showsDialog) {\n        mShowsDialog = showsDialog;\n    }\n\n    /**\n     * Return the current value of {@link #setShowsDialog(boolean)}.\n     */\n    public boolean getShowsDialog() {\n        return mShowsDialog;\n    }\n\n    @Override\n    public void onAttach(@NonNull Context context) {\n        super.onAttach(context);\n        if (!mShownByMe) {\n            // If not explicitly shown through our API, take this as an\n            // indication that the dialog is no longer dismissed.\n            mDismissed = false;\n        }\n    }\n\n    @Override\n    public void onDetach() {\n        super.onDetach();\n        if (!mShownByMe && !mDismissed) {\n            // The fragment was not shown by a direct call here, it is not\n            // dismissed, and now it is being detached...  well, okay, thou\n            // art now dismissed.  Have fun.\n            mDismissed = true;\n        }\n    }\n\n    @Override\n    public void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        // This assumes that onCreate() is being called on the main thread\n        mHandler = new Handler();\n\n        mShowsDialog = mContainerId == 0;\n\n        if (savedInstanceState != null) {\n            mStyle = savedInstanceState.getInt(SAVED_STYLE, STYLE_NORMAL);\n            mTheme = savedInstanceState.getInt(SAVED_THEME, 0);\n            mCancelable = savedInstanceState.getBoolean(SAVED_CANCELABLE, true);\n            mShowsDialog = savedInstanceState.getBoolean(SAVED_SHOWS_DIALOG, mShowsDialog);\n            mBackStackId = savedInstanceState.getInt(SAVED_BACK_STACK_ID, -1);\n        }\n    }\n\n    @Override\n    @NonNull\n    public LayoutInflater onGetLayoutInflater(@Nullable Bundle savedInstanceState) {\n        if (!mShowsDialog) {\n            return super.onGetLayoutInflater(savedInstanceState);\n        }\n\n        mDialog = onCreateDialog(savedInstanceState);\n\n        if (mDialog != null) {\n            setupDialog(mDialog, mStyle);\n\n            return (LayoutInflater) mDialog.getContext().getSystemService(\n                    Context.LAYOUT_INFLATER_SERVICE);\n        }\n        return (LayoutInflater) mHost.getContext().getSystemService(\n                Context.LAYOUT_INFLATER_SERVICE);\n    }\n\n    /** @hide */\n    @RestrictTo(LIBRARY_GROUP_PREFIX)\n    public void setupDialog(@NonNull Dialog dialog, int style) {\n        switch (style) {\n            case STYLE_NO_INPUT:\n                dialog.getWindow().addFlags(\n                        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |\n                                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);\n                // fall through...\n            case STYLE_NO_FRAME:\n            case STYLE_NO_TITLE:\n                dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);\n        }\n    }\n\n    /**\n     * Override to build your own custom Dialog container.  This is typically\n     * used to show an AlertDialog instead of a generic Dialog; when doing so,\n     * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} does not need\n     * to be implemented since the AlertDialog takes care of its own content.\n     *\n     * <p>This method will be called after {@link #onCreate(Bundle)} and\n     * before {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.  The\n     * default implementation simply instantiates and returns a {@link Dialog}\n     * class.\n     *\n     * <p><em>Note: DialogFragment own the {@link Dialog#setOnCancelListener\n     * Dialog.setOnCancelListener} and {@link Dialog#setOnDismissListener\n     * Dialog.setOnDismissListener} callbacks.  You must not set them yourself.</em>\n     * To find out about these events, override {@link #onCancel(DialogInterface)}\n     * and {@link #onDismiss(DialogInterface)}.</p>\n     *\n     * @param savedInstanceState The last saved instance state of the Fragment,\n     * or null if this is a freshly created Fragment.\n     *\n     * @return Return a new Dialog instance to be displayed by the Fragment.\n     */\n    @NonNull\n    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {\n        return new Dialog(requireContext(), getTheme());\n    }\n\n    @Override\n    public void onCancel(@NonNull DialogInterface dialog) {\n    }\n\n    @Override\n    public void onDismiss(@NonNull DialogInterface dialog) {\n        if (!mViewDestroyed) {\n            // Note: we need to use allowStateLoss, because the dialog\n            // dispatches this asynchronously so we can receive the call\n            // after the activity is paused.  Worst case, when the user comes\n            // back to the activity they see the dialog again.\n            dismissInternal(true, true);\n        }\n    }\n\n    @Override\n    public void onActivityCreated(@Nullable Bundle savedInstanceState) {\n        super.onActivityCreated(savedInstanceState);\n\n        if (!mShowsDialog) {\n            return;\n        }\n\n        View view = getView();\n        if (view != null) {\n            if (view.getParent() != null) {\n                throw new IllegalStateException(\n                        \"DialogFragment can not be attached to a container view\");\n            }\n            mDialog.setContentView(view);\n        }\n        final Activity activity = getActivity();\n        if (activity != null) {\n            mDialog.setOwnerActivity(activity);\n        }\n        mDialog.setCancelable(mCancelable);\n        mDialog.setOnCancelListener(this);\n        mDialog.setOnDismissListener(this);\n        if (savedInstanceState != null) {\n            Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);\n            if (dialogState != null) {\n                mDialog.onRestoreInstanceState(dialogState);\n            }\n        }\n    }\n\n    @Override\n    public void onStart() {\n        super.onStart();\n\n        if (mDialog != null) {\n            mViewDestroyed = false;\n            mDialog.show();\n        }\n    }\n\n    @Override\n    public void onSaveInstanceState(@NonNull Bundle outState) {\n        super.onSaveInstanceState(outState);\n        if (mDialog != null) {\n            Bundle dialogState = mDialog.onSaveInstanceState();\n            if (dialogState != null) {\n                outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState);\n            }\n        }\n        if (mStyle != STYLE_NORMAL) {\n            outState.putInt(SAVED_STYLE, mStyle);\n        }\n        if (mTheme != 0) {\n            outState.putInt(SAVED_THEME, mTheme);\n        }\n        if (!mCancelable) {\n            outState.putBoolean(SAVED_CANCELABLE, mCancelable);\n        }\n        if (!mShowsDialog) {\n            outState.putBoolean(SAVED_SHOWS_DIALOG, mShowsDialog);\n        }\n        if (mBackStackId != -1) {\n            outState.putInt(SAVED_BACK_STACK_ID, mBackStackId);\n        }\n    }\n\n    @Override\n    public void onStop() {\n        super.onStop();\n        if (mDialog != null) {\n            mDialog.hide();\n        }\n    }\n\n    /**\n     * Remove dialog.\n     */\n    @Override\n    public void onDestroyView() {\n        super.onDestroyView();\n        if (mDialog != null) {\n            // Set removed here because this dismissal is just to hide\n            // the dialog -- we don't want this to cause the fragment to\n            // actually be removed.\n            mViewDestroyed = true;\n            // Instead of waiting for a posted onDismiss(), null out\n            // the listener and call onDismiss() manually to ensure\n            // that the callback happens before onDestroy()\n            mDialog.setOnDismissListener(null);\n            mDialog.dismiss();\n            if (!mDismissed) {\n                // Don't send a second onDismiss() callback if we've already\n                // dismissed the dialog manually in dismissInternal()\n                onDismiss(mDialog);\n            }\n            mDialog = null;\n        }\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/Fragment.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;\n\nimport android.animation.Animator;\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.ComponentCallbacks;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentSender;\nimport android.content.res.Configuration;\nimport android.content.res.Resources;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.util.AttributeSet;\nimport android.util.SparseArray;\nimport android.view.ContextMenu;\nimport android.view.ContextMenu.ContextMenuInfo;\nimport android.view.LayoutInflater;\nimport android.view.Menu;\nimport android.view.MenuInflater;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.View.OnCreateContextMenuListener;\nimport android.view.ViewGroup;\nimport android.view.animation.Animation;\nimport android.widget.AdapterView;\n\nimport androidx.annotation.CallSuper;\nimport androidx.annotation.ContentView;\nimport androidx.annotation.LayoutRes;\nimport androidx.annotation.MainThread;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RestrictTo;\nimport androidx.annotation.StringRes;\nimport androidx.core.app.SharedElementCallback;\nimport androidx.core.util.DebugUtils;\nimport androidx.core.view.LayoutInflaterCompat;\nimport androidx.lifecycle.Lifecycle;\nimport androidx.lifecycle.LifecycleEventObserver;\nimport androidx.lifecycle.LifecycleOwner;\nimport androidx.lifecycle.LifecycleRegistry;\nimport androidx.lifecycle.LiveData;\nimport androidx.lifecycle.MutableLiveData;\nimport androidx.lifecycle.ViewModelStore;\nimport androidx.lifecycle.ViewModelStoreOwner;\nimport androidx.loader.app.LoaderManager;\nimport androidx.savedstate.SavedStateRegistry;\nimport androidx.savedstate.SavedStateRegistryController;\nimport androidx.savedstate.SavedStateRegistryOwner;\n\nimport java.io.FileDescriptor;\nimport java.io.PrintWriter;\nimport java.lang.reflect.InvocationTargetException;\nimport java.util.UUID;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * Static library support version of the framework's {@link android.app.Fragment}.\n * Used to write apps that run on platforms prior to Android 3.0.  When running\n * on Android 3.0 or above, this implementation is still used; it does not try\n * to switch to the framework's implementation. See the framework {@link android.app.Fragment}\n * documentation for a class overview.\n *\n * <p>The main differences when using this support version instead of the framework version are:\n * <ul>\n *  <li>Your activity must extend {@link FragmentActivity}\n *  <li>You must call {@link FragmentActivity#getSupportFragmentManager} to get the\n *  {@link FragmentManager}\n * </ul>\n *\n */\npublic class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner,\n        ViewModelStoreOwner, SavedStateRegistryOwner {\n\n    static final Object USE_DEFAULT_TRANSITION = new Object();\n\n    static final int INITIALIZING = 0;     // Not yet created.\n    static final int CREATED = 1;          // Created.\n    static final int ACTIVITY_CREATED = 2; // Fully created, not started.\n    static final int STARTED = 3;          // Created and started, not resumed.\n    static final int RESUMED = 4;          // Created started and resumed.\n\n    int mState = INITIALIZING;\n\n    // When instantiated from saved state, this is the saved state.\n    Bundle mSavedFragmentState;\n    SparseArray<Parcelable> mSavedViewState;\n    // If the userVisibleHint is changed before the state is set,\n    // it is stored here\n    @Nullable Boolean mSavedUserVisibleHint;\n\n    // Internal unique name for this fragment;\n    @NonNull\n    String mWho = UUID.randomUUID().toString();\n\n    // Construction arguments;\n    Bundle mArguments;\n\n    // Target fragment.\n    Fragment mTarget;\n\n    // For use when retaining a fragment: this is the who of the last mTarget.\n    String mTargetWho = null;\n\n    // Target request code.\n    int mTargetRequestCode;\n\n    // Boolean indicating whether this Fragment is the primary navigation fragment\n    private Boolean mIsPrimaryNavigationFragment = null;\n\n    // True if the fragment is in the list of added fragments.\n    boolean mAdded;\n\n    // If set this fragment is being removed from its activity.\n    boolean mRemoving;\n\n    // Set to true if this fragment was instantiated from a layout file.\n    boolean mFromLayout;\n\n    // Set to true when the view has actually been inflated in its layout.\n    boolean mInLayout;\n\n    // True if this fragment has been restored from previously saved state.\n    boolean mRestored;\n\n    // True if performCreateView has been called and a matching call to performDestroyView\n    // has not yet happened.\n    boolean mPerformedCreateView;\n\n    // Number of active back stack entries this fragment is in.\n    int mBackStackNesting;\n\n    // The fragment manager we are associated with.  Set as soon as the\n    // fragment is used in a transaction; cleared after it has been removed\n    // from all transactions.\n    FragmentManagerImpl mFragmentManager;\n\n    // Host this fragment is attached to.\n    FragmentHostCallback mHost;\n\n    // Private fragment manager for child fragments inside of this one.\n    @NonNull\n    FragmentManagerImpl mChildFragmentManager = new FragmentManagerImpl();\n\n    // If this Fragment is contained in another Fragment, this is that container.\n    Fragment mParentFragment;\n\n    // The optional identifier for this fragment -- either the container ID if it\n    // was dynamically added to the view hierarchy, or the ID supplied in\n    // layout.\n    int mFragmentId;\n\n    // When a fragment is being dynamically added to the view hierarchy, this\n    // is the identifier of the parent container it is being added to.\n    int mContainerId;\n\n    // The optional named tag for this fragment -- usually used to find\n    // fragments that are not part of the layout.\n    String mTag;\n\n    // Set to true when the app has requested that this fragment be hidden\n    // from the user.\n    boolean mHidden;\n\n    // Set to true when the app has requested that this fragment be deactivated.\n    boolean mDetached;\n\n    // If set this fragment would like its instance retained across\n    // configuration changes.\n    boolean mRetainInstance;\n\n    // If set this fragment changed its mRetainInstance while it was detached\n    boolean mRetainInstanceChangedWhileDetached;\n\n    // If set this fragment has menu items to contribute.\n    boolean mHasMenu;\n\n    // Set to true to allow the fragment's menu to be shown.\n    boolean mMenuVisible = true;\n\n    // Used to verify that subclasses call through to super class.\n    private boolean mCalled;\n\n    // The parent container of the fragment after dynamically added to UI.\n    ViewGroup mContainer;\n\n    // The View generated for this fragment.\n    View mView;\n\n    // The real inner view that will save/restore state.\n    View mInnerView;\n\n    // Whether this fragment should defer starting until after other fragments\n    // have been started and their loaders are finished.\n    boolean mDeferStart;\n\n    // Hint provided by the app that this fragment is currently visible to the user.\n    boolean mUserVisibleHint = true;\n\n    // The animation and transition information for the fragment. This will be null\n    // unless the elements are explicitly accessed and should remain null for Fragments\n    // without Views.\n    AnimationInfo mAnimationInfo;\n\n    // Runnable that is used to indicate if the Fragment has a postponed transition that is on a\n    // timeout.\n    Runnable mPostponedDurationRunnable = new Runnable() {\n        @Override\n        public void run() {\n            startPostponedEnterTransition();\n        }\n    };\n\n    // True if the View was added, and its animation has yet to be run. This could\n    // also indicate that the fragment view hasn't been made visible, even if there is no\n    // animation for this fragment.\n    boolean mIsNewlyAdded;\n\n    // True if mHidden has been changed and the animation should be scheduled.\n    boolean mHiddenChanged;\n\n    // The alpha of the view when the view was added and then postponed. If the value is less\n    // than zero, this means that the view's add was canceled and should not participate in\n    // removal animations.\n    float mPostponedAlpha;\n\n    // The cached value from onGetLayoutInflater(Bundle) that will be returned from\n    // getLayoutInflater()\n    LayoutInflater mLayoutInflater;\n\n    // Keep track of whether or not this Fragment has run performCreate(). Retained instance\n    // fragments can have mRetaining set to true without going through creation, so we must\n    // track it separately.\n    boolean mIsCreated;\n\n    // Max Lifecycle state this Fragment can achieve.\n    Lifecycle.State mMaxState = Lifecycle.State.RESUMED;\n\n    LifecycleRegistry mLifecycleRegistry;\n\n    // This is initialized in performCreateView and unavailable outside of the\n    // onCreateView/onDestroyView lifecycle\n    @Nullable FragmentViewLifecycleOwner mViewLifecycleOwner;\n    MutableLiveData<LifecycleOwner> mViewLifecycleOwnerLiveData = new MutableLiveData<>();\n\n    SavedStateRegistryController mSavedStateRegistryController;\n\n    @LayoutRes\n    private int mContentLayoutId;\n\n    /**\n     * {@inheritDoc}\n     * <p>\n     * Overriding this method is no longer supported and this method will be made\n     * <code>final</code> in a future version of Fragment.\n     */\n    @Override\n    @NonNull\n    public Lifecycle getLifecycle() {\n        return mLifecycleRegistry;\n    }\n\n    /**\n     * Get a {@link LifecycleOwner} that represents the {@link #getView() Fragment's View}\n     * lifecycle. In most cases, this mirrors the lifecycle of the Fragment itself, but in cases\n     * of {@link FragmentTransaction#detach(Fragment) detached} Fragments, the lifecycle of the\n     * Fragment can be considerably longer than the lifecycle of the View itself.\n     * <p>\n     * Namely, the lifecycle of the Fragment's View is:\n     * <ol>\n     * <li>{@link Lifecycle.Event#ON_CREATE created} after {@link #onViewStateRestored(Bundle)}</li>\n     * <li>{@link Lifecycle.Event#ON_START started} after {@link #onStart()}</li>\n     * <li>{@link Lifecycle.Event#ON_RESUME resumed} after {@link #onResume()}</li>\n     * <li>{@link Lifecycle.Event#ON_PAUSE paused} before {@link #onPause()}</li>\n     * <li>{@link Lifecycle.Event#ON_STOP stopped} before {@link #onStop()}</li>\n     * <li>{@link Lifecycle.Event#ON_DESTROY destroyed} before {@link #onDestroyView()}</li>\n     * </ol>\n     *\n     * The first method where it is safe to access the view lifecycle is\n     * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} under the condition that you must\n     * return a non-null view (an IllegalStateException will be thrown if you access the view\n     * lifecycle but don't return a non-null view).\n     * <p>The view lifecycle remains valid through the call to {@link #onDestroyView()}, after which\n     * {@link #getView()} will return null, the view lifecycle will be destroyed, and this method\n     * will throw an IllegalStateException. Consider using\n     * {@link #getViewLifecycleOwnerLiveData()} or {@link FragmentTransaction#runOnCommit(Runnable)}\n     * to receive a callback for when the Fragment's view lifecycle is available.\n     * <p>\n     * This should only be called on the main thread.\n     * <p>\n     * Overriding this method is no longer supported and this method will be made\n     * <code>final</code> in a future version of Fragment.\n     *\n     * @return A {@link LifecycleOwner} that represents the {@link #getView() Fragment's View}\n     * lifecycle.\n     * @throws IllegalStateException if the {@link #getView() Fragment's View is null}.\n     */\n    @MainThread\n    @NonNull\n    public LifecycleOwner getViewLifecycleOwner() {\n        if (mViewLifecycleOwner == null) {\n            throw new IllegalStateException(\"Can't access the Fragment View's LifecycleOwner when \"\n                    + \"getView() is null i.e., before onCreateView() or after onDestroyView()\");\n        }\n        return mViewLifecycleOwner;\n    }\n\n    /**\n     * Retrieve a {@link LiveData} which allows you to observe the\n     * {@link #getViewLifecycleOwner() lifecycle of the Fragment's View}.\n     * <p>\n     * This will be set to the new {@link LifecycleOwner} after {@link #onCreateView} returns a\n     * non-null View and will set to null after {@link #onDestroyView()}.\n     * <p>\n     * Overriding this method is no longer supported and this method will be made\n     * <code>final</code> in a future version of Fragment.\n     *\n     * @return A LiveData that changes in sync with {@link #getViewLifecycleOwner()}.\n     */\n    @NonNull\n    public LiveData<LifecycleOwner> getViewLifecycleOwnerLiveData() {\n        return mViewLifecycleOwnerLiveData;\n    }\n\n    /**\n     * Returns the {@link ViewModelStore} associated with this Fragment\n     * <p>\n     * Overriding this method is no longer supported and this method will be made\n     * <code>final</code> in a future version of Fragment.\n     *\n     * @return a {@code ViewModelStore}\n     * @throws IllegalStateException if called before the Fragment is attached i.e., before\n     * onAttach().\n     */\n    @NonNull\n    @Override\n    public ViewModelStore getViewModelStore() {\n        if (mFragmentManager == null) {\n            throw new IllegalStateException(\"Can't access ViewModels from detached fragment\");\n        }\n        return mFragmentManager.getViewModelStore(this);\n    }\n\n    @NonNull\n    @Override\n    public final SavedStateRegistry getSavedStateRegistry() {\n        return mSavedStateRegistryController.getSavedStateRegistry();\n    }\n\n    /**\n     * State information that has been retrieved from a fragment instance\n     * through {@link FragmentManager#saveFragmentInstanceState(Fragment)\n     * FragmentManager.saveFragmentInstanceState}.\n     */\n    @SuppressLint(\"BanParcelableUsage\")\n    public static class SavedState implements Parcelable {\n        final Bundle mState;\n\n        SavedState(Bundle state) {\n            mState = state;\n        }\n\n        SavedState(@NonNull Parcel in, @Nullable ClassLoader loader) {\n            mState = in.readBundle();\n            if (loader != null && mState != null) {\n                mState.setClassLoader(loader);\n            }\n        }\n\n        @Override\n        public int describeContents() {\n            return 0;\n        }\n\n        @Override\n        public void writeToParcel(@NonNull Parcel dest, int flags) {\n            dest.writeBundle(mState);\n        }\n\n        @NonNull\n        public static final Parcelable.Creator<SavedState> CREATOR =\n                new Parcelable.ClassLoaderCreator<SavedState>() {\n            @Override\n            public SavedState createFromParcel(Parcel in) {\n                return new SavedState(in, null);\n            }\n\n            @Override\n            public SavedState createFromParcel(Parcel in, ClassLoader loader) {\n                return new SavedState(in, loader);\n            }\n\n            @Override\n            public SavedState[] newArray(int size) {\n                return new SavedState[size];\n            }\n        };\n    }\n\n    /**\n     * Thrown by {@link FragmentFactory#instantiate(ClassLoader, String)} when\n     * there is an instantiation failure.\n     */\n    @SuppressWarnings(\"JavaLangClash\")\n    public static class InstantiationException extends RuntimeException {\n        public InstantiationException(@NonNull String msg, @Nullable Exception cause) {\n            super(msg, cause);\n        }\n    }\n\n    /**\n     * Constructor used by the default {@link FragmentFactory}. You must\n     * {@link FragmentManager#setFragmentFactory(FragmentFactory) set a custom FragmentFactory}\n     * if you want to use a non-default constructor to ensure that your constructor\n     * is called when the fragment is re-instantiated.\n     *\n     * <p>It is strongly recommended to supply arguments with {@link #setArguments}\n     * and later retrieved by the Fragment with {@link #getArguments}. These arguments\n     * are automatically saved and restored alongside the Fragment.\n     *\n     * <p>Applications should generally not implement a constructor. Prefer\n     * {@link #onAttach(Context)} instead. It is the first place application code can run where\n     * the fragment is ready to be used - the point where the fragment is actually associated with\n     * its context. Some applications may also want to implement {@link #onInflate} to retrieve\n     * attributes from a layout resource, although note this happens when the fragment is attached.\n     */\n    public Fragment() {\n        initLifecycle();\n    }\n\n    /**\n     * Alternate constructor that can be used to provide a default layout\n     * that will be inflated by {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.\n     *\n     * @see #Fragment()\n     * @see #onCreateView(LayoutInflater, ViewGroup, Bundle)\n     */\n    @ContentView\n    public Fragment(@LayoutRes int contentLayoutId) {\n        this();\n        mContentLayoutId = contentLayoutId;\n    }\n\n    private void initLifecycle() {\n        mLifecycleRegistry = new LifecycleRegistry(this);\n        mSavedStateRegistryController = SavedStateRegistryController.create(this);\n        if (Build.VERSION.SDK_INT >= 19) {\n            mLifecycleRegistry.addObserver(new LifecycleEventObserver() {\n                @Override\n                public void onStateChanged(@NonNull LifecycleOwner source,\n                        @NonNull Lifecycle.Event event) {\n                    if (event == Lifecycle.Event.ON_STOP) {\n                        if (mView != null) {\n                            mView.cancelPendingInputEvents();\n                        }\n                    }\n                }\n            });\n        }\n    }\n\n    /**\n     * Like {@link #instantiate(Context, String, Bundle)} but with a null\n     * argument Bundle.\n     * @deprecated Use {@link FragmentManager#getFragmentFactory()} and\n     * {@link FragmentFactory#instantiate(ClassLoader, String)}\n     */\n    @SuppressWarnings(\"deprecation\")\n    @Deprecated\n    @NonNull\n    public static Fragment instantiate(@NonNull Context context, @NonNull String fname) {\n        return instantiate(context, fname, null);\n    }\n\n    /**\n     * Create a new instance of a Fragment with the given class name.  This is\n     * the same as calling its empty constructor, setting the {@link ClassLoader} on the\n     * supplied arguments, then calling {@link #setArguments(Bundle)}.\n     *\n     * @param context The calling context being used to instantiate the fragment.\n     * This is currently just used to get its ClassLoader.\n     * @param fname The class name of the fragment to instantiate.\n     * @param args Bundle of arguments to supply to the fragment, which it\n     * can retrieve with {@link #getArguments()}.  May be null.\n     * @return Returns a new fragment instance.\n     * @throws InstantiationException If there is a failure in instantiating\n     * the given fragment class.  This is a runtime exception; it is not\n     * normally expected to happen.\n     * @deprecated Use {@link FragmentManager#getFragmentFactory()} and\n     * {@link FragmentFactory#instantiate(ClassLoader, String)}, manually calling\n     * {@link #setArguments(Bundle)} on the returned Fragment.\n     */\n    @Deprecated\n    @NonNull\n    public static Fragment instantiate(@NonNull Context context, @NonNull String fname,\n            @Nullable Bundle args) {\n        try {\n            Class<? extends Fragment> clazz = FragmentFactory.loadFragmentClass(\n                    context.getClassLoader(), fname);\n            Fragment f = clazz.getConstructor().newInstance();\n            if (args != null) {\n                args.setClassLoader(f.getClass().getClassLoader());\n                f.setArguments(args);\n            }\n            return f;\n        } catch (java.lang.InstantiationException e) {\n            throw new InstantiationException(\"Unable to instantiate fragment \" + fname\n                    + \": make sure class name exists, is public, and has an\"\n                    + \" empty constructor that is public\", e);\n        } catch (IllegalAccessException e) {\n            throw new InstantiationException(\"Unable to instantiate fragment \" + fname\n                    + \": make sure class name exists, is public, and has an\"\n                    + \" empty constructor that is public\", e);\n        } catch (NoSuchMethodException e) {\n            throw new InstantiationException(\"Unable to instantiate fragment \" + fname\n                    + \": could not find Fragment constructor\", e);\n        } catch (InvocationTargetException e) {\n            throw new InstantiationException(\"Unable to instantiate fragment \" + fname\n                    + \": calling Fragment constructor caused an exception\", e);\n        }\n    }\n\n    final void restoreViewState(Bundle savedInstanceState) {\n        if (mSavedViewState != null) {\n            mInnerView.restoreHierarchyState(mSavedViewState);\n            mSavedViewState = null;\n        }\n        mCalled = false;\n        onViewStateRestored(savedInstanceState);\n        if (!mCalled) {\n            throw new SuperNotCalledException(\"Fragment \" + this\n                    + \" did not call through to super.onViewStateRestored()\");\n        }\n        if (mView != null) {\n            mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);\n        }\n    }\n\n    final boolean isInBackStack() {\n        return mBackStackNesting > 0;\n    }\n\n    /**\n     * Subclasses can not override equals().\n     */\n    @Override public final boolean equals(@Nullable Object o) {\n        return super.equals(o);\n    }\n\n    /**\n     * Subclasses can not override hashCode().\n     */\n    @Override public final int hashCode() {\n        return super.hashCode();\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder(128);\n        DebugUtils.buildShortClassTag(this, sb);\n        sb.append(\" (\");\n        sb.append(mWho);\n        sb.append(\")\");\n        if (mFragmentId != 0) {\n            sb.append(\" id=0x\");\n            sb.append(Integer.toHexString(mFragmentId));\n        }\n        if (mTag != null) {\n            sb.append(\" \");\n            sb.append(mTag);\n        }\n        sb.append('}');\n        return sb.toString();\n    }\n\n    /**\n     * Return the identifier this fragment is known by.  This is either\n     * the android:id value supplied in a layout or the container view ID\n     * supplied when adding the fragment.\n     */\n    final public int getId() {\n        return mFragmentId;\n    }\n\n    /**\n     * Get the tag name of the fragment, if specified.\n     */\n    @Nullable\n    final public String getTag() {\n        return mTag;\n    }\n\n    /**\n     * Supply the construction arguments for this fragment.\n     * The arguments supplied here will be retained across fragment destroy and\n     * creation.\n     * <p>This method cannot be called if the fragment is added to a FragmentManager and\n     * if {@link #isStateSaved()} would return true.</p>\n     */\n    public void setArguments(@Nullable Bundle args) {\n        if (mFragmentManager != null && isStateSaved()) {\n            throw new IllegalStateException(\"Fragment already added and state has been saved\");\n        }\n        mArguments = args;\n    }\n\n    /**\n     * Return the arguments supplied when the fragment was instantiated,\n     * if any.\n     */\n    @Nullable\n    final public Bundle getArguments() {\n        return mArguments;\n    }\n\n    /**\n     * Return the arguments supplied when the fragment was instantiated.\n     *\n     * @throws IllegalStateException if no arguments were supplied to the Fragment.\n     * @see #getArguments()\n     */\n    @NonNull\n    public final Bundle requireArguments() {\n        Bundle arguments = getArguments();\n        if (arguments == null) {\n            throw new IllegalStateException(\"Fragment \" + this + \" does not have any arguments.\");\n        }\n        return arguments;\n    }\n\n    /**\n     * Returns true if this fragment is added and its state has already been saved\n     * by its host. Any operations that would change saved state should not be performed\n     * if this method returns true, and some operations such as {@link #setArguments(Bundle)}\n     * will fail.\n     *\n     * @return true if this fragment's state has already been saved by its host\n     */\n    public final boolean isStateSaved() {\n        if (mFragmentManager == null) {\n            return false;\n        }\n        return mFragmentManager.isStateSaved();\n    }\n\n    /**\n     * Set the initial saved state that this Fragment should restore itself\n     * from when first being constructed, as returned by\n     * {@link FragmentManager#saveFragmentInstanceState(Fragment)\n     * FragmentManager.saveFragmentInstanceState}.\n     *\n     * @param state The state the fragment should be restored from.\n     */\n    public void setInitialSavedState(@Nullable SavedState state) {\n        if (mFragmentManager != null) {\n            throw new IllegalStateException(\"Fragment already added\");\n        }\n        mSavedFragmentState = state != null && state.mState != null\n                ? state.mState : null;\n    }\n\n    /**\n     * Optional target for this fragment.  This may be used, for example,\n     * if this fragment is being started by another, and when done wants to\n     * give a result back to the first.  The target set here is retained\n     * across instances via {@link FragmentManager#putFragment\n     * FragmentManager.putFragment()}.\n     *\n     * @param fragment The fragment that is the target of this one.\n     * @param requestCode Optional request code, for convenience if you\n     * are going to call back with {@link #onActivityResult(int, int, Intent)}.\n     */\n    @SuppressWarnings(\"ReferenceEquality\")\n    public void setTargetFragment(@Nullable Fragment fragment, int requestCode) {\n        // Don't allow a caller to set a target fragment in another FragmentManager,\n        // but there's a snag: people do set target fragments before fragments get added.\n        // We'll have the FragmentManager check that for validity when we move\n        // the fragments to a valid state.\n        final FragmentManager mine = getFragmentManager();\n        final FragmentManager theirs = fragment != null ? fragment.getFragmentManager() : null;\n        if (mine != null && theirs != null && mine != theirs) {\n            throw new IllegalArgumentException(\"Fragment \" + fragment\n                    + \" must share the same FragmentManager to be set as a target fragment\");\n        }\n\n        // Don't let someone create a cycle.\n        for (Fragment check = fragment; check != null; check = check.getTargetFragment()) {\n            if (check == this) {\n                throw new IllegalArgumentException(\"Setting \" + fragment + \" as the target of \"\n                        + this + \" would create a target cycle\");\n            }\n        }\n        if (fragment == null) {\n            mTargetWho = null;\n            mTarget = null;\n        } else if (mFragmentManager != null && fragment.mFragmentManager != null) {\n            // Just save the reference to the Fragment\n            mTargetWho = fragment.mWho;\n            mTarget = null;\n        } else {\n            // Save the Fragment itself, waiting until we're attached\n            mTargetWho = null;\n            mTarget = fragment;\n        }\n        mTargetRequestCode = requestCode;\n    }\n\n    /**\n     * Return the target fragment set by {@link #setTargetFragment}.\n     */\n    @Nullable\n    final public Fragment getTargetFragment() {\n        if (mTarget != null) {\n            // Ensure that any Fragment set with setTargetFragment is immediately\n            // available here\n            return mTarget;\n        } else if (mFragmentManager != null && mTargetWho != null) {\n            // Look up the target Fragment from the FragmentManager\n            return mFragmentManager.mActive.get(mTargetWho);\n        }\n        return null;\n    }\n\n    /**\n     * Return the target request code set by {@link #setTargetFragment}.\n     */\n    final public int getTargetRequestCode() {\n        return mTargetRequestCode;\n    }\n\n    /**\n     * Return the {@link Context} this fragment is currently associated with.\n     *\n     * @see #requireContext()\n     */\n    @Nullable\n    public Context getContext() {\n        return mHost == null ? null : mHost.getContext();\n    }\n\n    /**\n     * Return the {@link Context} this fragment is currently associated with.\n     *\n     * @throws IllegalStateException if not currently associated with a context.\n     * @see #getContext()\n     */\n    @NonNull\n    public final Context requireContext() {\n        Context context = getContext();\n        if (context == null) {\n            throw new IllegalStateException(\"Fragment \" + this + \" not attached to a context.\");\n        }\n        return context;\n    }\n\n    /**\n     * Return the {@link FragmentActivity} this fragment is currently associated with.\n     * May return {@code null} if the fragment is associated with a {@link Context}\n     * instead.\n     *\n     * @see #requireActivity()\n     */\n    @Nullable\n    final public FragmentActivity getActivity() {\n        return mHost == null ? null : (FragmentActivity) mHost.getActivity();\n    }\n\n    /**\n     * Return the {@link FragmentActivity} this fragment is currently associated with.\n     *\n     * @throws IllegalStateException if not currently associated with an activity or if associated\n     * only with a context.\n     * @see #getActivity()\n     */\n    @NonNull\n    public final FragmentActivity requireActivity() {\n        FragmentActivity activity = getActivity();\n        if (activity == null) {\n            throw new IllegalStateException(\"Fragment \" + this + \" not attached to an activity.\");\n        }\n        return activity;\n    }\n\n    /**\n     * Return the host object of this fragment. May return {@code null} if the fragment\n     * isn't currently being hosted.\n     *\n     * @see #requireHost()\n     */\n    @Nullable\n    final public Object getHost() {\n        return mHost == null ? null : mHost.onGetHost();\n    }\n\n    /**\n     * Return the host object of this fragment.\n     *\n     * @throws IllegalStateException if not currently associated with a host.\n     * @see #getHost()\n     */\n    @NonNull\n    public final Object requireHost() {\n        Object host = getHost();\n        if (host == null) {\n            throw new IllegalStateException(\"Fragment \" + this + \" not attached to a host.\");\n        }\n        return host;\n    }\n\n    /**\n     * Return <code>requireActivity().getResources()</code>.\n     */\n    @NonNull\n    final public Resources getResources() {\n        return requireContext().getResources();\n    }\n\n    /**\n     * Return a localized, styled CharSequence from the application's package's\n     * default string table.\n     *\n     * @param resId Resource id for the CharSequence text\n     */\n    @NonNull\n    public final CharSequence getText(@StringRes int resId) {\n        return getResources().getText(resId);\n    }\n\n    /**\n     * Return a localized string from the application's package's\n     * default string table.\n     *\n     * @param resId Resource id for the string\n     */\n    @NonNull\n    public final String getString(@StringRes int resId) {\n        return getResources().getString(resId);\n    }\n\n    /**\n     * Return a localized formatted string from the application's package's\n     * default string table, substituting the format arguments as defined in\n     * {@link java.util.Formatter} and {@link java.lang.String#format}.\n     *\n     * @param resId Resource id for the format string\n     * @param formatArgs The format arguments that will be used for substitution.\n     */\n    @NonNull\n    public final String getString(@StringRes int resId, @Nullable Object... formatArgs) {\n        return getResources().getString(resId, formatArgs);\n    }\n\n    /**\n     * Return the FragmentManager for interacting with fragments associated\n     * with this fragment's activity.  Note that this will be non-null slightly\n     * before {@link #getActivity()}, during the time from when the fragment is\n     * placed in a {@link FragmentTransaction} until it is committed and\n     * attached to its activity.\n     *\n     * <p>If this Fragment is a child of another Fragment, the FragmentManager\n     * returned here will be the parent's {@link #getChildFragmentManager()}.\n     *\n     * @see #requireFragmentManager()\n     */\n    @Nullable\n    final public FragmentManager getFragmentManager() {\n        return mFragmentManager;\n    }\n\n    /**\n     * Return the FragmentManager for interacting with fragments associated\n     * with this fragment's activity.  Note that this will available slightly\n     * before {@link #getActivity()}, during the time from when the fragment is\n     * placed in a {@link FragmentTransaction} until it is committed and\n     * attached to its activity.\n     *\n     * <p>If this Fragment is a child of another Fragment, the FragmentManager\n     * returned here will be the parent's {@link #getChildFragmentManager()}.\n     *\n     * @throws IllegalStateException if not associated with a transaction or host.\n     * @see #getFragmentManager()\n     */\n    @NonNull\n    public final FragmentManager requireFragmentManager() {\n        FragmentManager fragmentManager = getFragmentManager();\n        if (fragmentManager == null) {\n            throw new IllegalStateException(\n                    \"Fragment \" + this + \" not associated with a fragment manager.\");\n        }\n        return fragmentManager;\n    }\n\n    /**\n     * Return a private FragmentManager for placing and managing Fragments\n     * inside of this Fragment.\n     */\n    @NonNull\n    final public FragmentManager getChildFragmentManager() {\n        if (mHost == null) {\n            throw new IllegalStateException(\"Fragment \" + this + \" has not been attached yet.\");\n        }\n        return mChildFragmentManager;\n    }\n\n    /**\n     * Returns the parent Fragment containing this Fragment.  If this Fragment\n     * is attached directly to an Activity, returns null.\n     */\n    @Nullable\n    final public Fragment getParentFragment() {\n        return mParentFragment;\n    }\n\n    /**\n     * Returns the parent Fragment containing this Fragment.\n     *\n     * @throws IllegalStateException if this Fragment is attached directly to an Activity or\n     * other Fragment host.\n     * @see #getParentFragment()\n     */\n    @NonNull\n    public final Fragment requireParentFragment() {\n        Fragment parentFragment = getParentFragment();\n        if (parentFragment == null) {\n            Context context = getContext();\n            if (context == null) {\n                throw new IllegalStateException(\"Fragment \" + this + \" is not attached to\"\n                        + \" any Fragment or host\");\n            } else {\n                throw new IllegalStateException(\"Fragment \" + this + \" is not a child Fragment, it\"\n                        + \" is directly attached to \" + getContext());\n            }\n        }\n        return parentFragment;\n    }\n\n    /**\n     * Return true if the fragment is currently added to its activity.\n     */\n    final public boolean isAdded() {\n        return mHost != null && mAdded;\n    }\n\n    /**\n     * Return true if the fragment has been explicitly detached from the UI.\n     * That is, {@link FragmentTransaction#detach(Fragment)\n     * FragmentTransaction.detach(Fragment)} has been used on it.\n     */\n    final public boolean isDetached() {\n        return mDetached;\n    }\n\n    /**\n     * Return true if this fragment is currently being removed from its\n     * activity.  This is  <em>not</em> whether its activity is finishing, but\n     * rather whether it is in the process of being removed from its activity.\n     */\n    final public boolean isRemoving() {\n        return mRemoving;\n    }\n\n    /**\n     * Return true if the layout is included as part of an activity view\n     * hierarchy via the &lt;fragment&gt; tag.  This will always be true when\n     * fragments are created through the &lt;fragment&gt; tag, <em>except</em>\n     * in the case where an old fragment is restored from a previous state and\n     * it does not appear in the layout of the current state.\n     */\n    final public boolean isInLayout() {\n        return mInLayout;\n    }\n\n    /**\n     * Return true if the fragment is in the resumed state.  This is true\n     * for the duration of {@link #onResume()} and {@link #onPause()} as well.\n     */\n    final public boolean isResumed() {\n        return mState >= RESUMED;\n    }\n\n    /**\n     * Return true if the fragment is currently visible to the user.  This means\n     * it: (1) has been added, (2) has its view attached to the window, and\n     * (3) is not hidden.\n     */\n    final public boolean isVisible() {\n        return isAdded() && !isHidden() && mView != null\n                && mView.getWindowToken() != null && mView.getVisibility() == View.VISIBLE;\n    }\n\n    /**\n     * Return true if the fragment has been hidden.  By default fragments\n     * are shown.  You can find out about changes to this state with\n     * {@link #onHiddenChanged}.  Note that the hidden state is orthogonal\n     * to other states -- that is, to be visible to the user, a fragment\n     * must be both started and not hidden.\n     */\n    final public boolean isHidden() {\n        return mHidden;\n    }\n\n    /** @hide */\n    @RestrictTo(LIBRARY_GROUP_PREFIX)\n    final public boolean hasOptionsMenu() {\n        return mHasMenu;\n    }\n\n    /** @hide */\n    @RestrictTo(LIBRARY_GROUP_PREFIX)\n    final public boolean isMenuVisible() {\n        return mMenuVisible;\n    }\n\n    /**\n     * Called when the hidden state (as returned by {@link #isHidden()} of\n     * the fragment has changed.  Fragments start out not hidden; this will\n     * be called whenever the fragment changes state from that.\n     * @param hidden True if the fragment is now hidden, false otherwise.\n     */\n    public void onHiddenChanged(boolean hidden) {\n    }\n\n    /**\n     * Control whether a fragment instance is retained across Activity\n     * re-creation (such as from a configuration change). If set, the fragment\n     * lifecycle will be slightly different when an activity is recreated:\n     * <ul>\n     * <li> {@link #onDestroy()} will not be called (but {@link #onDetach()} still\n     * will be, because the fragment is being detached from its current activity).\n     * <li> {@link #onCreate(Bundle)} will not be called since the fragment\n     * is not being re-created.\n     * <li> {@link #onAttach(Activity)} and {@link #onActivityCreated(Bundle)} <b>will</b>\n     * still be called.\n     * </ul>\n     */\n    public void setRetainInstance(boolean retain) {\n        mRetainInstance = retain;\n        if (mFragmentManager != null) {\n            if (retain) {\n                mFragmentManager.addRetainedFragment(this);\n            } else {\n                mFragmentManager.removeRetainedFragment(this);\n            }\n        } else {\n            mRetainInstanceChangedWhileDetached = true;\n        }\n    }\n\n    final public boolean getRetainInstance() {\n        return mRetainInstance;\n    }\n\n    /**\n     * Report that this fragment would like to participate in populating\n     * the options menu by receiving a call to {@link #onCreateOptionsMenu}\n     * and related methods.\n     *\n     * @param hasMenu If true, the fragment has menu items to contribute.\n     */\n    public void setHasOptionsMenu(boolean hasMenu) {\n        if (mHasMenu != hasMenu) {\n            mHasMenu = hasMenu;\n            if (isAdded() && !isHidden()) {\n                mHost.onSupportInvalidateOptionsMenu();\n            }\n        }\n    }\n\n    /**\n     * Set a hint for whether this fragment's menu should be visible.  This\n     * is useful if you know that a fragment has been placed in your view\n     * hierarchy so that the user can not currently seen it, so any menu items\n     * it has should also not be shown.\n     *\n     * @param menuVisible The default is true, meaning the fragment's menu will\n     * be shown as usual.  If false, the user will not see the menu.\n     */\n    public void setMenuVisibility(boolean menuVisible) {\n        if (mMenuVisible != menuVisible) {\n            mMenuVisible = menuVisible;\n            if (mHasMenu && isAdded() && !isHidden()) {\n                mHost.onSupportInvalidateOptionsMenu();\n            }\n        }\n    }\n\n    /**\n     * Set a hint to the system about whether this fragment's UI is currently visible\n     * to the user. This hint defaults to true and is persistent across fragment instance\n     * state save and restore.\n     *\n     * <p>An app may set this to false to indicate that the fragment's UI is\n     * scrolled out of visibility or is otherwise not directly visible to the user.\n     * This may be used by the system to prioritize operations such as fragment lifecycle updates\n     * or loader ordering behavior.</p>\n     *\n     * <p><strong>Note:</strong> This method may be called outside of the fragment lifecycle.\n     * and thus has no ordering guarantees with regard to fragment lifecycle method calls.</p>\n     *\n     * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),\n     *                        false if it is not.\n     *\n     * @deprecated Use {@link FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)}\n     * instead.\n     */\n    @Deprecated\n    public void setUserVisibleHint(boolean isVisibleToUser) {\n        if (!mUserVisibleHint && isVisibleToUser && mState < STARTED\n                && mFragmentManager != null && isAdded() && mIsCreated) {\n            mFragmentManager.performPendingDeferredStart(this);\n        }\n        mUserVisibleHint = isVisibleToUser;\n        mDeferStart = mState < STARTED && !isVisibleToUser;\n        if (mSavedFragmentState != null) {\n            // Ensure that if the user visible hint is set before the Fragment has\n            // restored its state that we don't lose the new value\n            mSavedUserVisibleHint = isVisibleToUser;\n        }\n    }\n\n    /**\n     * @return The current value of the user-visible hint on this fragment.\n     * @see #setUserVisibleHint(boolean)\n     *\n     * @deprecated Use {@link FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)}\n     * instead.\n     */\n    @Deprecated\n    public boolean getUserVisibleHint() {\n        return mUserVisibleHint;\n    }\n\n    /**\n     * Return the LoaderManager for this fragment.\n     *\n     * @deprecated Use\n     * {@link LoaderManager#getInstance(LifecycleOwner) LoaderManager.getInstance(this)}.\n     */\n    @Deprecated\n    @NonNull\n    public LoaderManager getLoaderManager() {\n        return LoaderManager.getInstance(this);\n    }\n\n    /**\n     * Call {@link Activity#startActivity(Intent)} from the fragment's\n     * containing Activity.\n     */\n    public void startActivity(@SuppressLint(\"UnknownNullness\") Intent intent) {\n        startActivity(intent, null);\n    }\n\n    /**\n     * Call {@link Activity#startActivity(Intent, Bundle)} from the fragment's\n     * containing Activity.\n     */\n    public void startActivity(@SuppressLint(\"UnknownNullness\") Intent intent,\n            @Nullable Bundle options) {\n        if (mHost == null) {\n            throw new IllegalStateException(\"Fragment \" + this + \" not attached to Activity\");\n        }\n        mHost.onStartActivityFromFragment(this /*fragment*/, intent, -1, options);\n    }\n\n    /**\n     * Call {@link Activity#startActivityForResult(Intent, int)} from the fragment's\n     * containing Activity.\n     */\n    public void startActivityForResult(@SuppressLint(\"UnknownNullness\") Intent intent,\n            int requestCode) {\n        startActivityForResult(intent, requestCode, null);\n    }\n\n    /**\n     * Call {@link Activity#startActivityForResult(Intent, int, Bundle)} from the fragment's\n     * containing Activity.\n     */\n    public void startActivityForResult(@SuppressLint(\"UnknownNullness\") Intent intent,\n            int requestCode, @Nullable Bundle options) {\n        if (mHost == null) {\n            throw new IllegalStateException(\"Fragment \" + this + \" not attached to Activity\");\n        }\n        mHost.onStartActivityFromFragment(this /*fragment*/, intent, requestCode, options);\n    }\n\n    /**\n     * Call {@link Activity#startIntentSenderForResult(IntentSender, int, Intent, int, int, int,\n     * Bundle)} from the fragment's containing Activity.\n     */\n    public void startIntentSenderForResult(@SuppressLint(\"UnknownNullness\") IntentSender intent,\n            int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues,\n            int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException {\n        if (mHost == null) {\n            throw new IllegalStateException(\"Fragment \" + this + \" not attached to Activity\");\n        }\n        mHost.onStartIntentSenderFromFragment(this, intent, requestCode, fillInIntent, flagsMask,\n                flagsValues, extraFlags, options);\n    }\n\n    /**\n     * Receive the result from a previous call to\n     * {@link #startActivityForResult(Intent, int)}.  This follows the\n     * related Activity API as described there in\n     * {@link Activity#onActivityResult(int, int, Intent)}.\n     *\n     * @param requestCode The integer request code originally supplied to\n     *                    startActivityForResult(), allowing you to identify who this\n     *                    result came from.\n     * @param resultCode The integer result code returned by the child activity\n     *                   through its setResult().\n     * @param data An Intent, which can return result data to the caller\n     *               (various data can be attached to Intent \"extras\").\n     */\n    public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {\n    }\n\n    /**\n     * Requests permissions to be granted to this application. These permissions\n     * must be requested in your manifest, they should not be granted to your app,\n     * and they should have protection level {@link android.content.pm.PermissionInfo\n     * #PROTECTION_DANGEROUS dangerous}, regardless whether they are declared by\n     * the platform or a third-party app.\n     * <p>\n     * Normal permissions {@link android.content.pm.PermissionInfo#PROTECTION_NORMAL}\n     * are granted at install time if requested in the manifest. Signature permissions\n     * {@link android.content.pm.PermissionInfo#PROTECTION_SIGNATURE} are granted at\n     * install time if requested in the manifest and the signature of your app matches\n     * the signature of the app declaring the permissions.\n     * </p>\n     * <p>\n     * If your app does not have the requested permissions the user will be presented\n     * with UI for accepting them. After the user has accepted or rejected the\n     * requested permissions you will receive a callback on {@link\n     * #onRequestPermissionsResult(int, String[], int[])} reporting whether the\n     * permissions were granted or not.\n     * </p>\n     * <p>\n     * Note that requesting a permission does not guarantee it will be granted and\n     * your app should be able to run without having this permission.\n     * </p>\n     * <p>\n     * This method may start an activity allowing the user to choose which permissions\n     * to grant and which to reject. Hence, you should be prepared that your activity\n     * may be paused and resumed. Further, granting some permissions may require\n     * a restart of you application. In such a case, the system will recreate the\n     * activity stack before delivering the result to {@link\n     * #onRequestPermissionsResult(int, String[], int[])}.\n     * </p>\n     * <p>\n     * When checking whether you have a permission you should use {@link\n     * android.content.Context#checkSelfPermission(String)}.\n     * </p>\n     * <p>\n     * Calling this API for permissions already granted to your app would show UI\n     * to the user to decided whether the app can still hold these permissions. This\n     * can be useful if the way your app uses the data guarded by the permissions\n     * changes significantly.\n     * </p>\n     * <p>\n     * A sample permissions request looks like this:\n     * </p>\n     * <code><pre><p>\n     * private void showContacts() {\n     *     if (getActivity().checkSelfPermission(Manifest.permission.READ_CONTACTS)\n     *             != PackageManager.PERMISSION_GRANTED) {\n     *         requestPermissions(new String[]{Manifest.permission.READ_CONTACTS},\n     *                 PERMISSIONS_REQUEST_READ_CONTACTS);\n     *     } else {\n     *         doShowContacts();\n     *     }\n     * }\n     *\n     * {@literal @}Override\n     * public void onRequestPermissionsResult(int requestCode, String[] permissions,\n     *         int[] grantResults) {\n     *     if (requestCode == PERMISSIONS_REQUEST_READ_CONTACTS\n     *             && grantResults[0] == PackageManager.PERMISSION_GRANTED) {\n     *         doShowContacts();\n     *     }\n     * }\n     * </code></pre></p>\n     *\n     * @param permissions The requested permissions.\n     * @param requestCode Application specific request code to match with a result\n     *    reported to {@link #onRequestPermissionsResult(int, String[], int[])}.\n     *\n     * @see #onRequestPermissionsResult(int, String[], int[])\n     * @see android.content.Context#checkSelfPermission(String)\n     */\n    public final void requestPermissions(@NonNull String[] permissions, int requestCode) {\n        if (mHost == null) {\n            throw new IllegalStateException(\"Fragment \" + this + \" not attached to Activity\");\n        }\n        mHost.onRequestPermissionsFromFragment(this, permissions, requestCode);\n    }\n\n    /**\n     * Callback for the result from requesting permissions. This method\n     * is invoked for every call on {@link #requestPermissions(String[], int)}.\n     * <p>\n     * <strong>Note:</strong> It is possible that the permissions request interaction\n     * with the user is interrupted. In this case you will receive empty permissions\n     * and results arrays which should be treated as a cancellation.\n     * </p>\n     *\n     * @param requestCode The request code passed in {@link #requestPermissions(String[], int)}.\n     * @param permissions The requested permissions. Never null.\n     * @param grantResults The grant results for the corresponding permissions\n     *     which is either {@link android.content.pm.PackageManager#PERMISSION_GRANTED}\n     *     or {@link android.content.pm.PackageManager#PERMISSION_DENIED}. Never null.\n     *\n     * @see #requestPermissions(String[], int)\n     */\n    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,\n            @NonNull int[] grantResults) {\n        /* callback - do nothing */\n    }\n\n    /**\n     * Gets whether you should show UI with rationale for requesting a permission.\n     * You should do this only if you do not have the permission and the context in\n     * which the permission is requested does not clearly communicate to the user\n     * what would be the benefit from granting this permission.\n     * <p>\n     * For example, if you write a camera app, requesting the camera permission\n     * would be expected by the user and no rationale for why it is requested is\n     * needed. If however, the app needs location for tagging photos then a non-tech\n     * savvy user may wonder how location is related to taking photos. In this case\n     * you may choose to show UI with rationale of requesting this permission.\n     * </p>\n     *\n     * @param permission A permission your app wants to request.\n     * @return Whether you can show permission rationale UI.\n     *\n     * @see Context#checkSelfPermission(String)\n     * @see #requestPermissions(String[], int)\n     * @see #onRequestPermissionsResult(int, String[], int[])\n     */\n    public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {\n        if (mHost != null) {\n            return mHost.onShouldShowRequestPermissionRationale(permission);\n        }\n        return false;\n    }\n\n    /**\n     * Returns the LayoutInflater used to inflate Views of this Fragment. The default\n     * implementation will throw an exception if the Fragment is not attached.\n     *\n     * @param savedInstanceState If the fragment is being re-created from\n     * a previous saved state, this is the state.\n     * @return The LayoutInflater used to inflate Views of this Fragment.\n     */\n    @NonNull\n    public LayoutInflater onGetLayoutInflater(@Nullable Bundle savedInstanceState) {\n        // TODO: move the implementation in getLayoutInflater to here\n        return getLayoutInflater(savedInstanceState);\n    }\n\n    /**\n     * Returns the cached LayoutInflater used to inflate Views of this Fragment. If\n     * {@link #onGetLayoutInflater(Bundle)} has not been called {@link #onGetLayoutInflater(Bundle)}\n     * will be called with a {@code null} argument and that value will be cached.\n     * <p>\n     * The cached LayoutInflater will be replaced immediately prior to\n     * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} and cleared immediately after\n     * {@link #onDetach()}.\n     *\n     * @return The LayoutInflater used to inflate Views of this Fragment.\n     */\n    @NonNull\n    public final LayoutInflater getLayoutInflater() {\n        if (mLayoutInflater == null) {\n            return performGetLayoutInflater(null);\n        }\n        return mLayoutInflater;\n    }\n\n    /**\n     * Calls {@link #onGetLayoutInflater(Bundle)} and caches the result for use by\n     * {@link #getLayoutInflater()}.\n     *\n     * @param savedInstanceState If the fragment is being re-created from\n     * a previous saved state, this is the state.\n     * @return The LayoutInflater used to inflate Views of this Fragment.\n     */\n    @NonNull\n    LayoutInflater performGetLayoutInflater(@Nullable Bundle savedInstanceState) {\n        LayoutInflater layoutInflater = onGetLayoutInflater(savedInstanceState);\n        mLayoutInflater = layoutInflater;\n        return mLayoutInflater;\n    }\n\n    /**\n     * Override {@link #onGetLayoutInflater(Bundle)} when you need to change the\n     * LayoutInflater or call {@link #getLayoutInflater()} when you want to\n     * retrieve the current LayoutInflater.\n     *\n     * @hide\n     * @deprecated Override {@link #onGetLayoutInflater(Bundle)} or call\n     * {@link #getLayoutInflater()} instead of this method.\n     */\n    @Deprecated\n    @NonNull\n    @RestrictTo(LIBRARY_GROUP_PREFIX)\n    public LayoutInflater getLayoutInflater(@Nullable Bundle savedFragmentState) {\n        if (mHost == null) {\n            throw new IllegalStateException(\"onGetLayoutInflater() cannot be executed until the \"\n                    + \"Fragment is attached to the FragmentManager.\");\n        }\n        LayoutInflater result = mHost.onGetLayoutInflater();\n        LayoutInflaterCompat.setFactory2(result, mChildFragmentManager.getLayoutInflaterFactory());\n        return result;\n    }\n\n    /**\n     * Called when a fragment is being created as part of a view layout\n     * inflation, typically from setting the content view of an activity.  This\n     * may be called immediately after the fragment is created from a <fragment>\n     * tag in a layout file.  Note this is <em>before</em> the fragment's\n     * {@link #onAttach(Activity)} has been called; all you should do here is\n     * parse the attributes and save them away.\n     *\n     * <p>This is called every time the fragment is inflated, even if it is\n     * being inflated into a new instance with saved state.  It typically makes\n     * sense to re-parse the parameters each time, to allow them to change with\n     * different configurations.</p>\n     *\n     * <p>Here is a typical implementation of a fragment that can take parameters\n     * both through attributes supplied here as well from {@link #getArguments()}:</p>\n     *\n     * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentArgumentsSupport.java\n     *      fragment}\n     *\n     * <p>Note that parsing the XML attributes uses a \"styleable\" resource.  The\n     * declaration for the styleable used here is:</p>\n     *\n     * {@sample frameworks/support/samples/Support4Demos/src/main/res/values/attrs.xml fragment_arguments}\n     *\n     * <p>The fragment can then be declared within its activity's content layout\n     * through a tag like this:</p>\n     *\n     * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_arguments_support.xml from_attributes}\n     *\n     * <p>This fragment can also be created dynamically from arguments given\n     * at runtime in the arguments Bundle; here is an example of doing so at\n     * creation of the containing activity:</p>\n     *\n     * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentArgumentsSupport.java\n     *      create}\n     *\n     * @param context The Activity that is inflating this fragment.\n     * @param attrs The attributes at the tag where the fragment is\n     * being created.\n     * @param savedInstanceState If the fragment is being re-created from\n     * a previous saved state, this is the state.\n     */\n    @CallSuper\n    public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,\n            @Nullable Bundle savedInstanceState) {\n        mCalled = true;\n        final Activity hostActivity = mHost == null ? null : mHost.getActivity();\n        if (hostActivity != null) {\n            mCalled = false;\n            onInflate(hostActivity, attrs, savedInstanceState);\n        }\n    }\n\n    /**\n     * Called when a fragment is being created as part of a view layout\n     * inflation, typically from setting the content view of an activity.\n     *\n     * @deprecated See {@link #onInflate(Context, AttributeSet, Bundle)}.\n     */\n    @Deprecated\n    @CallSuper\n    public void onInflate(@NonNull Activity activity, @NonNull AttributeSet attrs,\n            @Nullable Bundle savedInstanceState) {\n        mCalled = true;\n    }\n\n    /**\n     * Called when a fragment is attached as a child of this fragment.\n     *\n     * <p>This is called after the attached fragment's <code>onAttach</code> and before\n     * the attached fragment's <code>onCreate</code> if the fragment has not yet had a previous\n     * call to <code>onCreate</code>.</p>\n     *\n     * @param childFragment child fragment being attached\n     */\n    public void onAttachFragment(@NonNull Fragment childFragment) {\n    }\n\n    /**\n     * Called when a fragment is first attached to its context.\n     * {@link #onCreate(Bundle)} will be called after this.\n     */\n    @CallSuper\n    public void onAttach(@NonNull Context context) {\n        mCalled = true;\n        final Activity hostActivity = mHost == null ? null : mHost.getActivity();\n        if (hostActivity != null) {\n            mCalled = false;\n            onAttach(hostActivity);\n        }\n    }\n\n    /**\n     * Called when a fragment is first attached to its activity.\n     * {@link #onCreate(Bundle)} will be called after this.\n     *\n     * @deprecated See {@link #onAttach(Context)}.\n     */\n    @Deprecated\n    @CallSuper\n    public void onAttach(@NonNull Activity activity) {\n        mCalled = true;\n    }\n\n    /**\n     * Called when a fragment loads an animation. Note that if\n     * {@link FragmentTransaction#setCustomAnimations(int, int)} was called with\n     * {@link Animator} resources instead of {@link Animation} resources, {@code nextAnim}\n     * will be an animator resource.\n     *\n     * @param transit The value set in {@link FragmentTransaction#setTransition(int)} or 0 if not\n     *                set.\n     * @param enter {@code true} when the fragment is added/attached/shown or {@code false} when\n     *              the fragment is removed/detached/hidden.\n     * @param nextAnim The resource set in\n     *                 {@link FragmentTransaction#setCustomAnimations(int, int)},\n     *                 {@link FragmentTransaction#setCustomAnimations(int, int, int, int)}, or\n     *                 0 if neither was called. The value will depend on the current operation.\n     */\n    @Nullable\n    public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {\n        return null;\n    }\n\n    /**\n     * Called when a fragment loads an animator. This will be called when\n     * {@link #onCreateAnimation(int, boolean, int)} returns null. Note that if\n     * {@link FragmentTransaction#setCustomAnimations(int, int)} was called with\n     * {@link Animation} resources instead of {@link Animator} resources, {@code nextAnim}\n     * will be an animation resource.\n     *\n     * @param transit The value set in {@link FragmentTransaction#setTransition(int)} or 0 if not\n     *                set.\n     * @param enter {@code true} when the fragment is added/attached/shown or {@code false} when\n     *              the fragment is removed/detached/hidden.\n     * @param nextAnim The resource set in\n     *                 {@link FragmentTransaction#setCustomAnimations(int, int)},\n     *                 {@link FragmentTransaction#setCustomAnimations(int, int, int, int)}, or\n     *                 0 if neither was called. The value will depend on the current operation.\n     */\n    @Nullable\n    public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {\n        return null;\n    }\n\n    /**\n     * Called to do initial creation of a fragment.  This is called after\n     * {@link #onAttach(Activity)} and before\n     * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.\n     *\n     * <p>Note that this can be called while the fragment's activity is\n     * still in the process of being created.  As such, you can not rely\n     * on things like the activity's content view hierarchy being initialized\n     * at this point.  If you want to do work once the activity itself is\n     * created, see {@link #onActivityCreated(Bundle)}.\n     *\n     * <p>Any restored child fragments will be created before the base\n     * <code>Fragment.onCreate</code> method returns.</p>\n     *\n     * @param savedInstanceState If the fragment is being re-created from\n     * a previous saved state, this is the state.\n     */\n    @CallSuper\n    public void onCreate(@Nullable Bundle savedInstanceState) {\n        mCalled = true;\n        restoreChildFragmentState(savedInstanceState);\n        if (!mChildFragmentManager.isStateAtLeast(Fragment.CREATED)) {\n            mChildFragmentManager.dispatchCreate();\n        }\n    }\n\n    /**\n     * Restore the state of the child FragmentManager. Called by either\n     * {@link #onCreate(Bundle)} for non-retained instance fragments or by\n     * {@link FragmentManagerImpl#moveToState(Fragment, int, int, int, boolean)}\n     * for retained instance fragments.\n     *\n     * <p><strong>Postcondition:</strong> if there were child fragments to restore,\n     * the child FragmentManager will be instantiated and brought to the {@link #CREATED} state.\n     * </p>\n     *\n     * @param savedInstanceState the savedInstanceState potentially containing fragment info\n     */\n    void restoreChildFragmentState(@Nullable Bundle savedInstanceState) {\n        if (savedInstanceState != null) {\n            Parcelable p = savedInstanceState.getParcelable(\n                    FragmentActivity.FRAGMENTS_TAG);\n            if (p != null) {\n                mChildFragmentManager.restoreSaveState(p);\n                mChildFragmentManager.dispatchCreate();\n            }\n        }\n    }\n\n    /**\n     * Called to have the fragment instantiate its user interface view.\n     * This is optional, and non-graphical fragments can return null. This will be called between\n     * {@link #onCreate(Bundle)} and {@link #onActivityCreated(Bundle)}.\n     * <p>A default View can be returned by calling {@link #Fragment(int)} in your\n     * constructor. Otherwise, this method returns null.\n     *\n     * <p>It is recommended to <strong>only</strong> inflate the layout in this method and move\n     * logic that operates on the returned View to {@link #onViewCreated(View, Bundle)}.\n     *\n     * <p>If you return a View from here, you will later be called in\n     * {@link #onDestroyView} when the view is being released.\n     *\n     * @param inflater The LayoutInflater object that can be used to inflate\n     * any views in the fragment,\n     * @param container If non-null, this is the parent view that the fragment's\n     * UI should be attached to.  The fragment should not add the view itself,\n     * but this can be used to generate the LayoutParams of the view.\n     * @param savedInstanceState If non-null, this fragment is being re-constructed\n     * from a previous saved state as given here.\n     *\n     * @return Return the View for the fragment's UI, or null.\n     */\n    @Nullable\n    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,\n            @Nullable Bundle savedInstanceState) {\n        if (mContentLayoutId != 0) {\n            return inflater.inflate(mContentLayoutId, container, false);\n        }\n        return null;\n    }\n\n    /**\n     * Called immediately after {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}\n     * has returned, but before any saved state has been restored in to the view.\n     * This gives subclasses a chance to initialize themselves once\n     * they know their view hierarchy has been completely created.  The fragment's\n     * view hierarchy is not however attached to its parent at this point.\n     * @param view The View returned by {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.\n     * @param savedInstanceState If non-null, this fragment is being re-constructed\n     * from a previous saved state as given here.\n     */\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n    }\n\n    /**\n     * Get the root view for the fragment's layout (the one returned by {@link #onCreateView}),\n     * if provided.\n     *\n     * @return The fragment's root view, or null if it has no layout.\n     */\n    @Nullable\n    public View getView() {\n        return mView;\n    }\n\n    /**\n     * Get the root view for the fragment's layout (the one returned by {@link #onCreateView}).\n     *\n     * @throws IllegalStateException if no view was returned by {@link #onCreateView}.\n     * @see #getView()\n     */\n    @NonNull\n    public final View requireView() {\n        View view = getView();\n        if (view == null) {\n            throw new IllegalStateException(\"Fragment \" + this + \" did not return a View from\"\n                    + \" onCreateView() or this was called before onCreateView().\");\n        }\n        return view;\n    }\n\n    /**\n     * Called when the fragment's activity has been created and this\n     * fragment's view hierarchy instantiated.  It can be used to do final\n     * initialization once these pieces are in place, such as retrieving\n     * views or restoring state.  It is also useful for fragments that use\n     * {@link #setRetainInstance(boolean)} to retain their instance,\n     * as this callback tells the fragment when it is fully associated with\n     * the new activity instance.  This is called after {@link #onCreateView}\n     * and before {@link #onViewStateRestored(Bundle)}.\n     *\n     * @param savedInstanceState If the fragment is being re-created from\n     * a previous saved state, this is the state.\n     */\n    @CallSuper\n    public void onActivityCreated(@Nullable Bundle savedInstanceState) {\n        mCalled = true;\n    }\n\n    /**\n     * Called when all saved state has been restored into the view hierarchy\n     * of the fragment.  This can be used to do initialization based on saved\n     * state that you are letting the view hierarchy track itself, such as\n     * whether check box widgets are currently checked.  This is called\n     * after {@link #onActivityCreated(Bundle)} and before\n     * {@link #onStart()}.\n     *\n     * @param savedInstanceState If the fragment is being re-created from\n     * a previous saved state, this is the state.\n     */\n    @CallSuper\n    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {\n        mCalled = true;\n    }\n\n    /**\n     * Called when the Fragment is visible to the user.  This is generally\n     * tied to {@link Activity#onStart() Activity.onStart} of the containing\n     * Activity's lifecycle.\n     */\n    @CallSuper\n    public void onStart() {\n        mCalled = true;\n    }\n\n    /**\n     * Called when the fragment is visible to the user and actively running.\n     * This is generally\n     * tied to {@link Activity#onResume() Activity.onResume} of the containing\n     * Activity's lifecycle.\n     */\n    @CallSuper\n    public void onResume() {\n        mCalled = true;\n    }\n\n    /**\n     * Called to ask the fragment to save its current dynamic state, so it\n     * can later be reconstructed in a new instance of its process is\n     * restarted.  If a new instance of the fragment later needs to be\n     * created, the data you place in the Bundle here will be available\n     * in the Bundle given to {@link #onCreate(Bundle)},\n     * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}, and\n     * {@link #onActivityCreated(Bundle)}.\n     *\n     * <p>This corresponds to {@link Activity#onSaveInstanceState(Bundle)\n     * Activity.onSaveInstanceState(Bundle)} and most of the discussion there\n     * applies here as well.  Note however: <em>this method may be called\n     * at any time before {@link #onDestroy()}</em>.  There are many situations\n     * where a fragment may be mostly torn down (such as when placed on the\n     * back stack with no UI showing), but its state will not be saved until\n     * its owning activity actually needs to save its state.\n     *\n     * @param outState Bundle in which to place your saved state.\n     */\n    public void onSaveInstanceState(@NonNull Bundle outState) {\n    }\n\n    /**\n     * Called when the Fragment's activity changes from fullscreen mode to multi-window mode and\n     * visa-versa. This is generally tied to {@link Activity#onMultiWindowModeChanged} of the\n     * containing Activity.\n     *\n     * @param isInMultiWindowMode True if the activity is in multi-window mode.\n     */\n    public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {\n    }\n\n    /**\n     * Called by the system when the activity changes to and from picture-in-picture mode. This is\n     * generally tied to {@link Activity#onPictureInPictureModeChanged} of the containing Activity.\n     *\n     * @param isInPictureInPictureMode True if the activity is in picture-in-picture mode.\n     */\n    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {\n    }\n\n    @Override\n    @CallSuper\n    public void onConfigurationChanged(@NonNull Configuration newConfig) {\n        mCalled = true;\n    }\n\n    /**\n     * Callback for when the primary navigation state of this Fragment has changed. This can be\n     * the result of the {@link #getFragmentManager() containing FragmentManager} having its\n     * primary navigation fragment changed via\n     * {@link androidx.fragment.app.FragmentTransaction#setPrimaryNavigationFragment} or due to\n     * the primary navigation fragment changing in a parent FragmentManager.\n     *\n     * @param isPrimaryNavigationFragment True if and only if this Fragment and any\n     * {@link #getParentFragment() parent fragment} is set as the primary navigation fragment\n     * via {@link androidx.fragment.app.FragmentTransaction#setPrimaryNavigationFragment}.\n     */\n    public void onPrimaryNavigationFragmentChanged(boolean isPrimaryNavigationFragment) {\n    }\n\n    /**\n     * Called when the Fragment is no longer resumed.  This is generally\n     * tied to {@link Activity#onPause() Activity.onPause} of the containing\n     * Activity's lifecycle.\n     */\n    @CallSuper\n    public void onPause() {\n        mCalled = true;\n    }\n\n    /**\n     * Called when the Fragment is no longer started.  This is generally\n     * tied to {@link Activity#onStop() Activity.onStop} of the containing\n     * Activity's lifecycle.\n     */\n    @CallSuper\n    public void onStop() {\n        mCalled = true;\n    }\n\n    @Override\n    @CallSuper\n    public void onLowMemory() {\n        mCalled = true;\n    }\n\n    /**\n     * Called when the view previously created by {@link #onCreateView} has\n     * been detached from the fragment.  The next time the fragment needs\n     * to be displayed, a new view will be created.  This is called\n     * after {@link #onStop()} and before {@link #onDestroy()}.  It is called\n     * <em>regardless</em> of whether {@link #onCreateView} returned a\n     * non-null view.  Internally it is called after the view's state has\n     * been saved but before it has been removed from its parent.\n     */\n    @CallSuper\n    public void onDestroyView() {\n        mCalled = true;\n    }\n\n    /**\n     * Called when the fragment is no longer in use.  This is called\n     * after {@link #onStop()} and before {@link #onDetach()}.\n     */\n    @CallSuper\n    public void onDestroy() {\n        mCalled = true;\n    }\n\n    /**\n     * Called by the fragment manager once this fragment has been removed,\n     * so that we don't have any left-over state if the application decides\n     * to re-use the instance.  This only clears state that the framework\n     * internally manages, not things the application sets.\n     */\n    void initState() {\n        initLifecycle();\n        mWho = UUID.randomUUID().toString();\n        mAdded = false;\n        mRemoving = false;\n        mFromLayout = false;\n        mInLayout = false;\n        mRestored = false;\n        mBackStackNesting = 0;\n        mFragmentManager = null;\n        mChildFragmentManager = new FragmentManagerImpl();\n        mHost = null;\n        mFragmentId = 0;\n        mContainerId = 0;\n        mTag = null;\n        mHidden = false;\n        mDetached = false;\n    }\n\n    /**\n     * Called when the fragment is no longer attached to its activity.  This\n     * is called after {@link #onDestroy()}.\n     */\n    @CallSuper\n    public void onDetach() {\n        mCalled = true;\n    }\n\n    /**\n     * Initialize the contents of the Fragment host's standard options menu.  You\n     * should place your menu items in to <var>menu</var>.  For this method\n     * to be called, you must have first called {@link #setHasOptionsMenu}.  See\n     * {@link Activity#onCreateOptionsMenu(Menu) Activity.onCreateOptionsMenu}\n     * for more information.\n     *\n     * @param menu The options menu in which you place your items.\n     *\n     * @see #setHasOptionsMenu\n     * @see #onPrepareOptionsMenu\n     * @see #onOptionsItemSelected\n     */\n    public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {\n    }\n\n    /**\n     * Prepare the Fragment host's standard options menu to be displayed.  This is\n     * called right before the menu is shown, every time it is shown.  You can\n     * use this method to efficiently enable/disable items or otherwise\n     * dynamically modify the contents.  See\n     * {@link Activity#onPrepareOptionsMenu(Menu) Activity.onPrepareOptionsMenu}\n     * for more information.\n     *\n     * @param menu The options menu as last shown or first initialized by\n     *             onCreateOptionsMenu().\n     *\n     * @see #setHasOptionsMenu\n     * @see #onCreateOptionsMenu\n     */\n    public void onPrepareOptionsMenu(@NonNull Menu menu) {\n    }\n\n    /**\n     * Called when this fragment's option menu items are no longer being\n     * included in the overall options menu.  Receiving this call means that\n     * the menu needed to be rebuilt, but this fragment's items were not\n     * included in the newly built menu (its {@link #onCreateOptionsMenu(Menu, MenuInflater)}\n     * was not called).\n     */\n    public void onDestroyOptionsMenu() {\n    }\n\n    /**\n     * This hook is called whenever an item in your options menu is selected.\n     * The default implementation simply returns false to have the normal\n     * processing happen (calling the item's Runnable or sending a message to\n     * its Handler as appropriate).  You can use this method for any items\n     * for which you would like to do processing without those other\n     * facilities.\n     *\n     * <p>Derived classes should call through to the base class for it to\n     * perform the default menu handling.\n     *\n     * @param item The menu item that was selected.\n     *\n     * @return boolean Return false to allow normal menu processing to\n     *         proceed, true to consume it here.\n     *\n     * @see #onCreateOptionsMenu\n     */\n    public boolean onOptionsItemSelected(@NonNull MenuItem item) {\n        return false;\n    }\n\n    /**\n     * This hook is called whenever the options menu is being closed (either by the user canceling\n     * the menu with the back/menu button, or when an item is selected).\n     *\n     * @param menu The options menu as last shown or first initialized by\n     *             onCreateOptionsMenu().\n     */\n    public void onOptionsMenuClosed(@NonNull Menu menu) {\n    }\n\n    /**\n     * Called when a context menu for the {@code view} is about to be shown.\n     * Unlike {@link #onCreateOptionsMenu}, this will be called every\n     * time the context menu is about to be shown and should be populated for\n     * the view (or item inside the view for {@link AdapterView} subclasses,\n     * this can be found in the {@code menuInfo})).\n     * <p>\n     * Use {@link #onContextItemSelected(android.view.MenuItem)} to know when an\n     * item has been selected.\n     * <p>\n     * The default implementation calls up to\n     * {@link Activity#onCreateContextMenu Activity.onCreateContextMenu}, though\n     * you can not call this implementation if you don't want that behavior.\n     * <p>\n     * It is not safe to hold onto the context menu after this method returns.\n     * {@inheritDoc}\n     */\n    @Override\n    public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v,\n            @Nullable ContextMenuInfo menuInfo) {\n        requireActivity().onCreateContextMenu(menu, v, menuInfo);\n    }\n\n    /**\n     * Registers a context menu to be shown for the given view (multiple views\n     * can show the context menu). This method will set the\n     * {@link OnCreateContextMenuListener} on the view to this fragment, so\n     * {@link #onCreateContextMenu(ContextMenu, View, ContextMenuInfo)} will be\n     * called when it is time to show the context menu.\n     *\n     * @see #unregisterForContextMenu(View)\n     * @param view The view that should show a context menu.\n     */\n    public void registerForContextMenu(@NonNull View view) {\n        view.setOnCreateContextMenuListener(this);\n    }\n\n    /**\n     * Prevents a context menu to be shown for the given view. This method will\n     * remove the {@link OnCreateContextMenuListener} on the view.\n     *\n     * @see #registerForContextMenu(View)\n     * @param view The view that should stop showing a context menu.\n     */\n    public void unregisterForContextMenu(@NonNull View view) {\n        view.setOnCreateContextMenuListener(null);\n    }\n\n    /**\n     * This hook is called whenever an item in a context menu is selected. The\n     * default implementation simply returns false to have the normal processing\n     * happen (calling the item's Runnable or sending a message to its Handler\n     * as appropriate). You can use this method for any items for which you\n     * would like to do processing without those other facilities.\n     * <p>\n     * Use {@link MenuItem#getMenuInfo()} to get extra information set by the\n     * View that added this menu item.\n     * <p>\n     * Derived classes should call through to the base class for it to perform\n     * the default menu handling.\n     *\n     * @param item The context menu item that was selected.\n     * @return boolean Return false to allow normal context menu processing to\n     *         proceed, true to consume it here.\n     */\n    public boolean onContextItemSelected(@NonNull MenuItem item) {\n        return false;\n    }\n\n    /**\n     * When custom transitions are used with Fragments, the enter transition callback\n     * is called when this Fragment is attached or detached when not popping the back stack.\n     *\n     * @param callback Used to manipulate the shared element transitions on this Fragment\n     *                 when added not as a pop from the back stack.\n     */\n    public void setEnterSharedElementCallback(@Nullable SharedElementCallback callback) {\n        ensureAnimationInfo().mEnterTransitionCallback = callback;\n    }\n\n    /**\n     * When custom transitions are used with Fragments, the exit transition callback\n     * is called when this Fragment is attached or detached when popping the back stack.\n     *\n     * @param callback Used to manipulate the shared element transitions on this Fragment\n     *                 when added as a pop from the back stack.\n     */\n    public void setExitSharedElementCallback(@Nullable SharedElementCallback callback) {\n        ensureAnimationInfo().mExitTransitionCallback = callback;\n    }\n\n    /**\n     * Sets the Transition that will be used to move Views into the initial scene. The entering\n     * Views will be those that are regular Views or ViewGroups that have\n     * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend\n     * {@link android.transition.Visibility} as entering is governed by changing visibility from\n     * {@link View#INVISIBLE} to {@link View#VISIBLE}. If <code>transition</code> is null,\n     * entering Views will remain unaffected.\n     *\n     * @param transition The Transition to use to move Views into the initial Scene.\n     *         <code>transition</code> must be an\n     *         {@link android.transition.Transition} or\n     *         {@link androidx.transition.Transition}.\n     */\n    public void setEnterTransition(@Nullable Object transition) {\n        ensureAnimationInfo().mEnterTransition = transition;\n    }\n\n    /**\n     * Returns the Transition that will be used to move Views into the initial scene. The entering\n     * Views will be those that are regular Views or ViewGroups that have\n     * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend\n     * {@link android.transition.Visibility} as entering is governed by changing visibility from\n     * {@link View#INVISIBLE} to {@link View#VISIBLE}.\n     *\n     * @return the Transition to use to move Views into the initial Scene.\n     */\n    @Nullable\n    public Object getEnterTransition() {\n        if (mAnimationInfo == null) {\n            return null;\n        }\n        return mAnimationInfo.mEnterTransition;\n    }\n\n    /**\n     * Sets the Transition that will be used to move Views out of the scene when the Fragment is\n     * preparing to be removed, hidden, or detached because of popping the back stack. The exiting\n     * Views will be those that are regular Views or ViewGroups that have\n     * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend\n     * {@link android.transition.Visibility} as entering is governed by changing visibility from\n     * {@link View#VISIBLE} to {@link View#INVISIBLE}. If <code>transition</code> is null,\n     * entering Views will remain unaffected. If nothing is set, the default will be to\n     * use the same value as set in {@link #setEnterTransition(Object)}.\n     *\n     * @param transition The Transition to use to move Views out of the Scene when the Fragment\n     *         is preparing to close due to popping the back stack. <code>transition</code> must be\n     *         an {@link android.transition.Transition} or\n     *         {@link androidx.transition.Transition}.\n     */\n    public void setReturnTransition(@Nullable Object transition) {\n        ensureAnimationInfo().mReturnTransition = transition;\n    }\n\n    /**\n     * Returns the Transition that will be used to move Views out of the scene when the Fragment is\n     * preparing to be removed, hidden, or detached because of popping the back stack. The exiting\n     * Views will be those that are regular Views or ViewGroups that have\n     * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend\n     * {@link android.transition.Visibility} as entering is governed by changing visibility from\n     * {@link View#VISIBLE} to {@link View#INVISIBLE}. If nothing is set, the default will be to use\n     * the same transition as {@link #getEnterTransition()}.\n     *\n     * @return the Transition to use to move Views out of the Scene when the Fragment\n     *         is preparing to close due to popping the back stack.\n     */\n    @Nullable\n    public Object getReturnTransition() {\n        if (mAnimationInfo == null) {\n            return null;\n        }\n        return mAnimationInfo.mReturnTransition == USE_DEFAULT_TRANSITION ? getEnterTransition()\n                : mAnimationInfo.mReturnTransition;\n    }\n\n    /**\n     * Sets the Transition that will be used to move Views out of the scene when the\n     * fragment is removed, hidden, or detached when not popping the back stack.\n     * The exiting Views will be those that are regular Views or ViewGroups that\n     * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend\n     * {@link android.transition.Visibility} as exiting is governed by changing visibility\n     * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will\n     * remain unaffected.\n     *\n     * @param transition The Transition to use to move Views out of the Scene when the Fragment\n     *          is being closed not due to popping the back stack. <code>transition</code>\n     *          must be an\n     *          {@link android.transition.Transition} or\n     *          {@link androidx.transition.Transition}.\n     */\n    public void setExitTransition(@Nullable Object transition) {\n        ensureAnimationInfo().mExitTransition = transition;\n    }\n\n    /**\n     * Returns the Transition that will be used to move Views out of the scene when the\n     * fragment is removed, hidden, or detached when not popping the back stack.\n     * The exiting Views will be those that are regular Views or ViewGroups that\n     * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend\n     * {@link android.transition.Visibility} as exiting is governed by changing visibility\n     * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will\n     * remain unaffected.\n     *\n     * @return the Transition to use to move Views out of the Scene when the Fragment\n     *         is being closed not due to popping the back stack.\n     */\n    @Nullable\n    public Object getExitTransition() {\n        if (mAnimationInfo == null) {\n            return null;\n        }\n        return mAnimationInfo.mExitTransition;\n    }\n\n    /**\n     * Sets the Transition that will be used to move Views in to the scene when returning due\n     * to popping a back stack. The entering Views will be those that are regular Views\n     * or ViewGroups that have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions\n     * will extend {@link android.transition.Visibility} as exiting is governed by changing\n     * visibility from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null,\n     * the views will remain unaffected. If nothing is set, the default will be to use the same\n     * transition as {@link #getExitTransition()}.\n     *\n     * @param transition The Transition to use to move Views into the scene when reentering from a\n     *          previously-started Activity due to popping the back stack. <code>transition</code>\n     *          must be an\n     *          {@link android.transition.Transition} or\n     *          {@link androidx.transition.Transition}.\n     */\n    public void setReenterTransition(@Nullable Object transition) {\n        ensureAnimationInfo().mReenterTransition = transition;\n    }\n\n    /**\n     * Returns the Transition that will be used to move Views in to the scene when returning due\n     * to popping a back stack. The entering Views will be those that are regular Views\n     * or ViewGroups that have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions\n     * will extend {@link android.transition.Visibility} as exiting is governed by changing\n     * visibility from {@link View#VISIBLE} to {@link View#INVISIBLE}. If nothing is set, the\n     * default will be to use the same transition as {@link #getExitTransition()}.\n     *\n     * @return the Transition to use to move Views into the scene when reentering from a\n     *                   previously-started Activity due to popping the back stack.\n     */\n    @Nullable\n    public Object getReenterTransition() {\n        if (mAnimationInfo == null) {\n            return null;\n        }\n        return mAnimationInfo.mReenterTransition == USE_DEFAULT_TRANSITION ? getExitTransition()\n                : mAnimationInfo.mReenterTransition;\n    }\n\n    /**\n     * Sets the Transition that will be used for shared elements transferred into the content\n     * Scene. Typical Transitions will affect size and location, such as\n     * {@link android.transition.ChangeBounds}. A null\n     * value will cause transferred shared elements to blink to the final position.\n     *\n     * @param transition The Transition to use for shared elements transferred into the content\n     *          Scene.  <code>transition</code> must be an\n     *          {@link android.transition.Transition android.transition.Transition} or\n     *          {@link androidx.transition.Transition androidx.transition.Transition}.\n     */\n    public void setSharedElementEnterTransition(@Nullable Object transition) {\n        ensureAnimationInfo().mSharedElementEnterTransition = transition;\n    }\n\n    /**\n     * Returns the Transition that will be used for shared elements transferred into the content\n     * Scene. Typical Transitions will affect size and location, such as\n     * {@link android.transition.ChangeBounds}. A null\n     * value will cause transferred shared elements to blink to the final position.\n     *\n     * @return The Transition to use for shared elements transferred into the content\n     *                   Scene.\n     */\n    @Nullable\n    public Object getSharedElementEnterTransition() {\n        if (mAnimationInfo == null) {\n            return null;\n        }\n        return mAnimationInfo.mSharedElementEnterTransition;\n    }\n\n    /**\n     * Sets the Transition that will be used for shared elements transferred back during a\n     * pop of the back stack. This Transition acts in the leaving Fragment.\n     * Typical Transitions will affect size and location, such as\n     * {@link android.transition.ChangeBounds}. A null\n     * value will cause transferred shared elements to blink to the final position.\n     * If no value is set, the default will be to use the same value as\n     * {@link #setSharedElementEnterTransition(Object)}.\n     *\n     * @param transition The Transition to use for shared elements transferred out of the content\n     *          Scene. <code>transition</code> must be an\n     *          {@link android.transition.Transition android.transition.Transition} or\n     *          {@link androidx.transition.Transition androidx.transition.Transition}.\n     */\n    public void setSharedElementReturnTransition(@Nullable Object transition) {\n        ensureAnimationInfo().mSharedElementReturnTransition = transition;\n    }\n\n    /**\n     * Return the Transition that will be used for shared elements transferred back during a\n     * pop of the back stack. This Transition acts in the leaving Fragment.\n     * Typical Transitions will affect size and location, such as\n     * {@link android.transition.ChangeBounds}. A null\n     * value will cause transferred shared elements to blink to the final position.\n     * If no value is set, the default will be to use the same value as\n     * {@link #setSharedElementEnterTransition(Object)}.\n     *\n     * @return The Transition to use for shared elements transferred out of the content\n     *                   Scene.\n     */\n    @Nullable\n    public Object getSharedElementReturnTransition() {\n        if (mAnimationInfo == null) {\n            return null;\n        }\n        return mAnimationInfo.mSharedElementReturnTransition == USE_DEFAULT_TRANSITION\n                ? getSharedElementEnterTransition()\n                : mAnimationInfo.mSharedElementReturnTransition;\n    }\n\n    /**\n     * Sets whether the the exit transition and enter transition overlap or not.\n     * When true, the enter transition will start as soon as possible. When false, the\n     * enter transition will wait until the exit transition completes before starting.\n     *\n     * @param allow true to start the enter transition when possible or false to\n     *              wait until the exiting transition completes.\n     */\n    public void setAllowEnterTransitionOverlap(boolean allow) {\n        ensureAnimationInfo().mAllowEnterTransitionOverlap = allow;\n    }\n\n    /**\n     * Returns whether the the exit transition and enter transition overlap or not.\n     * When true, the enter transition will start as soon as possible. When false, the\n     * enter transition will wait until the exit transition completes before starting.\n     *\n     * @return true when the enter transition should start as soon as possible or false to\n     * when it should wait until the exiting transition completes.\n     */\n    public boolean getAllowEnterTransitionOverlap() {\n        return (mAnimationInfo == null || mAnimationInfo.mAllowEnterTransitionOverlap == null)\n                ? true : mAnimationInfo.mAllowEnterTransitionOverlap;\n    }\n\n    /**\n     * Sets whether the the return transition and reenter transition overlap or not.\n     * When true, the reenter transition will start as soon as possible. When false, the\n     * reenter transition will wait until the return transition completes before starting.\n     *\n     * @param allow true to start the reenter transition when possible or false to wait until the\n     *              return transition completes.\n     */\n    public void setAllowReturnTransitionOverlap(boolean allow) {\n        ensureAnimationInfo().mAllowReturnTransitionOverlap = allow;\n    }\n\n    /**\n     * Returns whether the the return transition and reenter transition overlap or not.\n     * When true, the reenter transition will start as soon as possible. When false, the\n     * reenter transition will wait until the return transition completes before starting.\n     *\n     * @return true to start the reenter transition when possible or false to wait until the\n     *         return transition completes.\n     */\n    public boolean getAllowReturnTransitionOverlap() {\n        return (mAnimationInfo == null || mAnimationInfo.mAllowReturnTransitionOverlap == null)\n                ? true : mAnimationInfo.mAllowReturnTransitionOverlap;\n    }\n\n    /**\n     * Postpone the entering Fragment transition until {@link #startPostponedEnterTransition()}\n     * or {@link FragmentManager#executePendingTransactions()} has been called.\n     * <p>\n     * This method gives the Fragment the ability to delay Fragment animations\n     * until all data is loaded. Until then, the added, shown, and\n     * attached Fragments will be INVISIBLE and removed, hidden, and detached Fragments won't\n     * be have their Views removed. The transaction runs when all postponed added Fragments in the\n     * transaction have called {@link #startPostponedEnterTransition()}.\n     * <p>\n     * This method should be called before being added to the FragmentTransaction or\n     * in {@link #onCreate(Bundle)}, {@link #onAttach(Context)}, or\n     * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}}.\n     * {@link #startPostponedEnterTransition()} must be called to allow the Fragment to\n     * start the transitions.\n     * <p>\n     * When a FragmentTransaction is started that may affect a postponed FragmentTransaction,\n     * based on which containers are in their operations, the postponed FragmentTransaction\n     * will have its start triggered. The early triggering may result in faulty or nonexistent\n     * animations in the postponed transaction. FragmentTransactions that operate only on\n     * independent containers will not interfere with each other's postponement.\n     * <p>\n     * Calling postponeEnterTransition on Fragments with a null View will not postpone the\n     * transition. Likewise, postponement only works if\n     * {@link FragmentTransaction#setReorderingAllowed(boolean) FragmentTransaction reordering} is\n     * enabled.\n     *\n     * @see Activity#postponeEnterTransition()\n     * @see FragmentTransaction#setReorderingAllowed(boolean)\n     */\n    public void postponeEnterTransition() {\n        ensureAnimationInfo().mEnterTransitionPostponed = true;\n    }\n\n    /**\n     * Postpone the entering Fragment transition for a given amount of time and then call\n     * {@link #startPostponedEnterTransition()}.\n     * <p>\n     * This method gives the Fragment the ability to delay Fragment animations for a given amount\n     * of time. Until then, the added, shown, and attached Fragments will be INVISIBLE and removed,\n     * hidden, and detached Fragments won't be have their Views removed. The transaction runs when\n     * all postponed added Fragments in the transaction have called\n     * {@link #startPostponedEnterTransition()}.\n     * <p>\n     * This method should be called before being added to the FragmentTransaction or\n     * in {@link #onCreate(Bundle)}, {@link #onAttach(Context)}, or\n     * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}}.\n     * <p>\n     * When a FragmentTransaction is started that may affect a postponed FragmentTransaction,\n     * based on which containers are in their operations, the postponed FragmentTransaction\n     * will have its start triggered. The early triggering may result in faulty or nonexistent\n     * animations in the postponed transaction. FragmentTransactions that operate only on\n     * independent containers will not interfere with each other's postponement.\n     * <p>\n     * Calling postponeEnterTransition on Fragments with a null View will not postpone the\n     * transition. Likewise, postponement only works if\n     * {@link FragmentTransaction#setReorderingAllowed(boolean) FragmentTransaction reordering} is\n     * enabled.\n     *\n     * @param duration The length of the delay in {@code timeUnit} units\n     * @param timeUnit The units of time for {@code duration}\n     * @see Activity#postponeEnterTransition()\n     * @see FragmentTransaction#setReorderingAllowed(boolean)\n     */\n    public final void postponeEnterTransition(long duration, @NonNull TimeUnit timeUnit) {\n        ensureAnimationInfo().mEnterTransitionPostponed = true;\n        Handler handler;\n        if (mFragmentManager != null) {\n            handler = mFragmentManager.mHost.getHandler();\n        } else {\n            handler = new Handler(Looper.getMainLooper());\n        }\n        handler.removeCallbacks(mPostponedDurationRunnable);\n        handler.postDelayed(mPostponedDurationRunnable, timeUnit.toMillis(duration));\n    }\n\n    /**\n     * Begin postponed transitions after {@link #postponeEnterTransition()} was called.\n     * If postponeEnterTransition() was called, you must call startPostponedEnterTransition()\n     * or {@link FragmentManager#executePendingTransactions()} to complete the FragmentTransaction.\n     * If postponement was interrupted with {@link FragmentManager#executePendingTransactions()},\n     * before {@code startPostponedEnterTransition()}, animations may not run or may execute\n     * improperly.\n     *\n     * @see Activity#startPostponedEnterTransition()\n     */\n    public void startPostponedEnterTransition() {\n        if (mFragmentManager == null || mFragmentManager.mHost == null) {\n            ensureAnimationInfo().mEnterTransitionPostponed = false;\n        } else if (Looper.myLooper() != mFragmentManager.mHost.getHandler().getLooper()) {\n            mFragmentManager.mHost.getHandler().postAtFrontOfQueue(new Runnable() {\n                @Override\n                public void run() {\n                    callStartTransitionListener();\n                }\n            });\n        } else {\n            callStartTransitionListener();\n        }\n    }\n\n    /**\n     * Calls the start transition listener. This must be called on the UI thread.\n     */\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    void callStartTransitionListener() {\n        final OnStartEnterTransitionListener listener;\n        if (mAnimationInfo == null) {\n            listener = null;\n        } else {\n            mAnimationInfo.mEnterTransitionPostponed = false;\n            listener = mAnimationInfo.mStartEnterTransitionListener;\n            mAnimationInfo.mStartEnterTransitionListener = null;\n        }\n        if (listener != null) {\n            listener.onStartEnterTransition();\n        }\n    }\n\n    /**\n     * Print the Fragments's state into the given stream.\n     *\n     * @param prefix Text to print at the front of each line.\n     * @param fd The raw file descriptor that the dump is being sent to.\n     * @param writer The PrintWriter to which you should dump your state.  This will be\n     * closed for you after you return.\n     * @param args additional arguments to the dump request.\n     */\n    public void dump(@NonNull String prefix, @Nullable FileDescriptor fd,\n            @NonNull PrintWriter writer, @Nullable String[] args) {\n        writer.print(prefix); writer.print(\"mFragmentId=#\");\n                writer.print(Integer.toHexString(mFragmentId));\n                writer.print(\" mContainerId=#\");\n                writer.print(Integer.toHexString(mContainerId));\n                writer.print(\" mTag=\"); writer.println(mTag);\n        writer.print(prefix); writer.print(\"mState=\"); writer.print(mState);\n                writer.print(\" mWho=\"); writer.print(mWho);\n                writer.print(\" mBackStackNesting=\"); writer.println(mBackStackNesting);\n        writer.print(prefix); writer.print(\"mAdded=\"); writer.print(mAdded);\n                writer.print(\" mRemoving=\"); writer.print(mRemoving);\n                writer.print(\" mFromLayout=\"); writer.print(mFromLayout);\n                writer.print(\" mInLayout=\"); writer.println(mInLayout);\n        writer.print(prefix); writer.print(\"mHidden=\"); writer.print(mHidden);\n                writer.print(\" mDetached=\"); writer.print(mDetached);\n                writer.print(\" mMenuVisible=\"); writer.print(mMenuVisible);\n                writer.print(\" mHasMenu=\"); writer.println(mHasMenu);\n        writer.print(prefix); writer.print(\"mRetainInstance=\"); writer.print(mRetainInstance);\n                writer.print(\" mUserVisibleHint=\"); writer.println(mUserVisibleHint);\n        if (mFragmentManager != null) {\n            writer.print(prefix); writer.print(\"mFragmentManager=\");\n                    writer.println(mFragmentManager);\n        }\n        if (mHost != null) {\n            writer.print(prefix); writer.print(\"mHost=\");\n                    writer.println(mHost);\n        }\n        if (mParentFragment != null) {\n            writer.print(prefix); writer.print(\"mParentFragment=\");\n                    writer.println(mParentFragment);\n        }\n        if (mArguments != null) {\n            writer.print(prefix); writer.print(\"mArguments=\"); writer.println(mArguments);\n        }\n        if (mSavedFragmentState != null) {\n            writer.print(prefix); writer.print(\"mSavedFragmentState=\");\n                    writer.println(mSavedFragmentState);\n        }\n        if (mSavedViewState != null) {\n            writer.print(prefix); writer.print(\"mSavedViewState=\");\n                    writer.println(mSavedViewState);\n        }\n        Fragment target = getTargetFragment();\n        if (target != null) {\n            writer.print(prefix); writer.print(\"mTarget=\"); writer.print(target);\n                    writer.print(\" mTargetRequestCode=\");\n                    writer.println(mTargetRequestCode);\n        }\n        if (getNextAnim() != 0) {\n            writer.print(prefix); writer.print(\"mNextAnim=\"); writer.println(getNextAnim());\n        }\n        if (mContainer != null) {\n            writer.print(prefix); writer.print(\"mContainer=\"); writer.println(mContainer);\n        }\n        if (mView != null) {\n            writer.print(prefix); writer.print(\"mView=\"); writer.println(mView);\n        }\n        if (mInnerView != null) {\n            writer.print(prefix); writer.print(\"mInnerView=\"); writer.println(mView);\n        }\n        if (getAnimatingAway() != null) {\n            writer.print(prefix);\n            writer.print(\"mAnimatingAway=\");\n            writer.println(getAnimatingAway());\n            writer.print(prefix);\n            writer.print(\"mStateAfterAnimating=\");\n            writer.println(getStateAfterAnimating());\n        }\n        if (getContext() != null) {\n            LoaderManager.getInstance(this).dump(prefix, fd, writer, args);\n        }\n        writer.print(prefix);\n        writer.println(\"Child \" + mChildFragmentManager + \":\");\n        mChildFragmentManager.dump(prefix + \"  \", fd, writer, args);\n    }\n\n    @Nullable\n    Fragment findFragmentByWho(@NonNull String who) {\n        if (who.equals(mWho)) {\n            return this;\n        }\n        return mChildFragmentManager.findFragmentByWho(who);\n    }\n\n    void performAttach() {\n        mChildFragmentManager.attachController(mHost, new FragmentContainer() {\n            @Override\n            @Nullable\n            public View onFindViewById(int id) {\n                if (mView == null) {\n                    throw new IllegalStateException(\"Fragment \" + this + \" does not have a view\");\n                }\n                return mView.findViewById(id);\n            }\n\n            @Override\n            public boolean onHasView() {\n                return (mView != null);\n            }\n        }, this);\n        mCalled = false;\n        onAttach(mHost.getContext());\n        if (!mCalled) {\n            throw new SuperNotCalledException(\"Fragment \" + this\n                    + \" did not call through to super.onAttach()\");\n        }\n    }\n\n    void performCreate(Bundle savedInstanceState) {\n        mChildFragmentManager.noteStateNotSaved();\n        mState = CREATED;\n        mCalled = false;\n        mSavedStateRegistryController.performRestore(savedInstanceState);\n        onCreate(savedInstanceState);\n        mIsCreated = true;\n        if (!mCalled) {\n            throw new SuperNotCalledException(\"Fragment \" + this\n                    + \" did not call through to super.onCreate()\");\n        }\n        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);\n    }\n\n    void performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,\n            @Nullable Bundle savedInstanceState) {\n        mChildFragmentManager.noteStateNotSaved();\n        mPerformedCreateView = true;\n        mViewLifecycleOwner = new FragmentViewLifecycleOwner();\n        mView = onCreateView(inflater, container, savedInstanceState);\n        if (mView != null) {\n            // Initialize the view lifecycle\n            mViewLifecycleOwner.initialize();\n            // Then inform any Observers of the new LifecycleOwner\n            mViewLifecycleOwnerLiveData.setValue(mViewLifecycleOwner);\n        } else {\n            if (mViewLifecycleOwner.isInitialized()) {\n                throw new IllegalStateException(\"Called getViewLifecycleOwner() but \"\n                        + \"onCreateView() returned null\");\n            }\n            mViewLifecycleOwner = null;\n        }\n    }\n\n    void performActivityCreated(Bundle savedInstanceState) {\n        mChildFragmentManager.noteStateNotSaved();\n        mState = ACTIVITY_CREATED;\n        mCalled = false;\n        onActivityCreated(savedInstanceState);\n        if (!mCalled) {\n            throw new SuperNotCalledException(\"Fragment \" + this\n                    + \" did not call through to super.onActivityCreated()\");\n        }\n        mChildFragmentManager.dispatchActivityCreated();\n    }\n\n    void performStart() {\n        mChildFragmentManager.noteStateNotSaved();\n        mChildFragmentManager.execPendingActions();\n        mState = STARTED;\n        mCalled = false;\n        onStart();\n        if (!mCalled) {\n            throw new SuperNotCalledException(\"Fragment \" + this\n                    + \" did not call through to super.onStart()\");\n        }\n        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);\n        if (mView != null) {\n            mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START);\n        }\n        mChildFragmentManager.dispatchStart();\n    }\n\n    void performResume() {\n        mChildFragmentManager.noteStateNotSaved();\n        mChildFragmentManager.execPendingActions();\n        mState = RESUMED;\n        mCalled = false;\n        onResume();\n        if (!mCalled) {\n            throw new SuperNotCalledException(\"Fragment \" + this\n                    + \" did not call through to super.onResume()\");\n        }\n        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);\n        if (mView != null) {\n            mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);\n        }\n        mChildFragmentManager.dispatchResume();\n        mChildFragmentManager.execPendingActions();\n    }\n\n    void noteStateNotSaved() {\n        mChildFragmentManager.noteStateNotSaved();\n    }\n\n    void performPrimaryNavigationFragmentChanged() {\n        boolean isPrimaryNavigationFragment = mFragmentManager.isPrimaryNavigation(this);\n        // Only send out the callback / dispatch if the state has changed\n        if (mIsPrimaryNavigationFragment == null\n                || mIsPrimaryNavigationFragment != isPrimaryNavigationFragment) {\n            mIsPrimaryNavigationFragment = isPrimaryNavigationFragment;\n            onPrimaryNavigationFragmentChanged(isPrimaryNavigationFragment);\n            mChildFragmentManager.dispatchPrimaryNavigationFragmentChanged();\n        }\n    }\n\n    void performMultiWindowModeChanged(boolean isInMultiWindowMode) {\n        onMultiWindowModeChanged(isInMultiWindowMode);\n        mChildFragmentManager.dispatchMultiWindowModeChanged(isInMultiWindowMode);\n    }\n\n    void performPictureInPictureModeChanged(boolean isInPictureInPictureMode) {\n        onPictureInPictureModeChanged(isInPictureInPictureMode);\n        mChildFragmentManager.dispatchPictureInPictureModeChanged(isInPictureInPictureMode);\n    }\n\n    void performConfigurationChanged(@NonNull Configuration newConfig) {\n        onConfigurationChanged(newConfig);\n        mChildFragmentManager.dispatchConfigurationChanged(newConfig);\n    }\n\n    void performLowMemory() {\n        onLowMemory();\n        mChildFragmentManager.dispatchLowMemory();\n    }\n\n    /*\n    void performTrimMemory(int level) {\n        onTrimMemory(level);\n        if (mChildFragmentManager != null) {\n            mChildFragmentManager.dispatchTrimMemory(level);\n        }\n    }\n    */\n\n    boolean performCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {\n        boolean show = false;\n        if (!mHidden) {\n            if (mHasMenu && mMenuVisible) {\n                show = true;\n                onCreateOptionsMenu(menu, inflater);\n            }\n            show |= mChildFragmentManager.dispatchCreateOptionsMenu(menu, inflater);\n        }\n        return show;\n    }\n\n    boolean performPrepareOptionsMenu(@NonNull Menu menu) {\n        boolean show = false;\n        if (!mHidden) {\n            if (mHasMenu && mMenuVisible) {\n                show = true;\n                onPrepareOptionsMenu(menu);\n            }\n            show |= mChildFragmentManager.dispatchPrepareOptionsMenu(menu);\n        }\n        return show;\n    }\n\n    boolean performOptionsItemSelected(@NonNull MenuItem item) {\n        if (!mHidden) {\n            if (mHasMenu && mMenuVisible) {\n                if (onOptionsItemSelected(item)) {\n                    return true;\n                }\n            }\n            if (mChildFragmentManager.dispatchOptionsItemSelected(item)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    boolean performContextItemSelected(@NonNull MenuItem item) {\n        if (!mHidden) {\n            if (onContextItemSelected(item)) {\n                return true;\n            }\n            if (mChildFragmentManager.dispatchContextItemSelected(item)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    void performOptionsMenuClosed(@NonNull Menu menu) {\n        if (!mHidden) {\n            if (mHasMenu && mMenuVisible) {\n                onOptionsMenuClosed(menu);\n            }\n            mChildFragmentManager.dispatchOptionsMenuClosed(menu);\n        }\n    }\n\n    void performSaveInstanceState(Bundle outState) {\n        onSaveInstanceState(outState);\n        mSavedStateRegistryController.performSave(outState);\n        Parcelable p = mChildFragmentManager.saveAllState();\n        if (p != null) {\n            outState.putParcelable(FragmentActivity.FRAGMENTS_TAG, p);\n        }\n    }\n\n    void performPause() {\n        mChildFragmentManager.dispatchPause();\n        if (mView != null) {\n            mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);\n        }\n        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);\n        mState = STARTED;\n        mCalled = false;\n        onPause();\n        if (!mCalled) {\n            throw new SuperNotCalledException(\"Fragment \" + this\n                    + \" did not call through to super.onPause()\");\n        }\n    }\n\n    void performStop() {\n        mChildFragmentManager.dispatchStop();\n        if (mView != null) {\n            mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP);\n        }\n        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);\n        mState = ACTIVITY_CREATED;\n        mCalled = false;\n        onStop();\n        if (!mCalled) {\n            throw new SuperNotCalledException(\"Fragment \" + this\n                    + \" did not call through to super.onStop()\");\n        }\n    }\n\n    void performDestroyView() {\n        mChildFragmentManager.dispatchDestroyView();\n        if (mView != null) {\n            mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);\n        }\n        mState = CREATED;\n        mCalled = false;\n        onDestroyView();\n        if (!mCalled) {\n            throw new SuperNotCalledException(\"Fragment \" + this\n                    + \" did not call through to super.onDestroyView()\");\n        }\n        // Handles the detach/reattach case where the view hierarchy\n        // is destroyed and recreated and an additional call to\n        // onLoadFinished may be needed to ensure the new view\n        // hierarchy is populated from data from the Loaders\n        LoaderManager.getInstance(this).markForRedelivery();\n        mPerformedCreateView = false;\n    }\n\n    void performDestroy() {\n        mChildFragmentManager.dispatchDestroy();\n        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);\n        mState = INITIALIZING;\n        mCalled = false;\n        mIsCreated = false;\n        onDestroy();\n        if (!mCalled) {\n            throw new SuperNotCalledException(\"Fragment \" + this\n                    + \" did not call through to super.onDestroy()\");\n        }\n    }\n\n    void performDetach() {\n        mCalled = false;\n        onDetach();\n        mLayoutInflater = null;\n        if (!mCalled) {\n            throw new SuperNotCalledException(\"Fragment \" + this\n                    + \" did not call through to super.onDetach()\");\n        }\n\n        // Destroy the child FragmentManager if we still have it here.\n        // This is normally done in performDestroy(), but is done here\n        // specifically if the Fragment is retained.\n        if (!mChildFragmentManager.isDestroyed()) {\n            mChildFragmentManager.dispatchDestroy();\n            mChildFragmentManager = new FragmentManagerImpl();\n        }\n    }\n\n    void setOnStartEnterTransitionListener(OnStartEnterTransitionListener listener) {\n        ensureAnimationInfo();\n        if (listener == mAnimationInfo.mStartEnterTransitionListener) {\n            return;\n        }\n        if (listener != null && mAnimationInfo.mStartEnterTransitionListener != null) {\n            throw new IllegalStateException(\"Trying to set a replacement \"\n                    + \"startPostponedEnterTransition on \" + this);\n        }\n        if (mAnimationInfo.mEnterTransitionPostponed) {\n            mAnimationInfo.mStartEnterTransitionListener = listener;\n        }\n        if (listener != null) {\n            listener.startListening();\n        }\n    }\n\n    private AnimationInfo ensureAnimationInfo() {\n        if (mAnimationInfo == null) {\n            mAnimationInfo = new AnimationInfo();\n        }\n        return mAnimationInfo;\n    }\n\n    int getNextAnim() {\n        if (mAnimationInfo == null) {\n            return 0;\n        }\n        return mAnimationInfo.mNextAnim;\n    }\n\n    void setNextAnim(int animResourceId) {\n        if (mAnimationInfo == null && animResourceId == 0) {\n            return; // no change!\n        }\n        ensureAnimationInfo().mNextAnim = animResourceId;\n    }\n\n    int getNextTransition() {\n        if (mAnimationInfo == null) {\n            return 0;\n        }\n        return mAnimationInfo.mNextTransition;\n    }\n\n    void setNextTransition(int nextTransition, int nextTransitionStyle) {\n        if (mAnimationInfo == null && nextTransition == 0 && nextTransitionStyle == 0) {\n            return; // no change!\n        }\n        ensureAnimationInfo();\n        mAnimationInfo.mNextTransition = nextTransition;\n        mAnimationInfo.mNextTransitionStyle = nextTransitionStyle;\n    }\n\n    int getNextTransitionStyle() {\n        if (mAnimationInfo == null) {\n            return 0;\n        }\n        return mAnimationInfo.mNextTransitionStyle;\n    }\n\n    SharedElementCallback getEnterTransitionCallback() {\n        if (mAnimationInfo == null) {\n            return null;\n        }\n        return mAnimationInfo.mEnterTransitionCallback;\n    }\n\n    SharedElementCallback getExitTransitionCallback() {\n        if (mAnimationInfo == null) {\n            return null;\n        }\n        return mAnimationInfo.mExitTransitionCallback;\n    }\n\n    View getAnimatingAway() {\n        if (mAnimationInfo == null) {\n            return null;\n        }\n        return mAnimationInfo.mAnimatingAway;\n    }\n\n    void setAnimatingAway(View view) {\n        ensureAnimationInfo().mAnimatingAway = view;\n    }\n\n    void setAnimator(Animator animator) {\n        ensureAnimationInfo().mAnimator = animator;\n    }\n\n    Animator getAnimator() {\n        if (mAnimationInfo == null) {\n            return null;\n        }\n        return mAnimationInfo.mAnimator;\n    }\n\n    int getStateAfterAnimating() {\n        if (mAnimationInfo == null) {\n            return 0;\n        }\n        return mAnimationInfo.mStateAfterAnimating;\n    }\n\n    void setStateAfterAnimating(int state) {\n        ensureAnimationInfo().mStateAfterAnimating = state;\n    }\n\n    boolean isPostponed() {\n        if (mAnimationInfo == null) {\n            return false;\n        }\n        return mAnimationInfo.mEnterTransitionPostponed;\n    }\n\n    boolean isHideReplaced() {\n        if (mAnimationInfo == null) {\n            return false;\n        }\n        return mAnimationInfo.mIsHideReplaced;\n    }\n\n    void setHideReplaced(boolean replaced) {\n        ensureAnimationInfo().mIsHideReplaced = replaced;\n    }\n\n    /**\n     * Used internally to be notified when {@link #startPostponedEnterTransition()} has\n     * been called. This listener will only be called once and then be removed from the\n     * listeners.\n     */\n    interface OnStartEnterTransitionListener {\n        void onStartEnterTransition();\n        void startListening();\n    }\n\n    /**\n     * Contains all the animation and transition information for a fragment. This will only\n     * be instantiated for Fragments that have Views.\n     */\n    static class AnimationInfo {\n        // Non-null if the fragment's view hierarchy is currently animating away,\n        // meaning we need to wait a bit on completely destroying it.  This is the\n        // view that is animating.\n        View mAnimatingAway;\n\n        // Non-null if the fragment's view hierarchy is currently animating away with an\n        // animator instead of an animation.\n        Animator mAnimator;\n\n        // If mAnimatingAway != null, this is the state we should move to once the\n        // animation is done.\n        int mStateAfterAnimating;\n\n        // If app has requested a specific animation, this is the one to use.\n        int mNextAnim;\n\n        // If app has requested a specific transition, this is the one to use.\n        int mNextTransition;\n\n        // If app has requested a specific transition style, this is the one to use.\n        int mNextTransitionStyle;\n\n        Object mEnterTransition = null;\n        Object mReturnTransition = USE_DEFAULT_TRANSITION;\n        Object mExitTransition = null;\n        Object mReenterTransition = USE_DEFAULT_TRANSITION;\n        Object mSharedElementEnterTransition = null;\n        Object mSharedElementReturnTransition = USE_DEFAULT_TRANSITION;\n        Boolean mAllowReturnTransitionOverlap;\n        Boolean mAllowEnterTransitionOverlap;\n\n        SharedElementCallback mEnterTransitionCallback = null;\n        SharedElementCallback mExitTransitionCallback = null;\n\n        // True when postponeEnterTransition has been called and startPostponeEnterTransition\n        // hasn't been called yet.\n        boolean mEnterTransitionPostponed;\n\n        // Listener to wait for startPostponeEnterTransition. After being called, it will\n        // be set to null\n        OnStartEnterTransitionListener mStartEnterTransitionListener;\n\n        // True if the View was hidden, but the transition is handling the hide\n        boolean mIsHideReplaced;\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentActivity.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.ActivityNotFoundException;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentSender;\nimport android.content.res.Configuration;\nimport android.os.Bundle;\nimport android.os.Parcelable;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.view.LayoutInflater;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.Window;\nimport android.widget.Toast;\n\nimport androidx.activity.ComponentActivity;\nimport androidx.activity.OnBackPressedDispatcher;\nimport androidx.activity.OnBackPressedDispatcherOwner;\nimport androidx.annotation.CallSuper;\nimport androidx.annotation.ContentView;\nimport androidx.annotation.LayoutRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RestrictTo;\nimport androidx.collection.SparseArrayCompat;\nimport androidx.core.app.ActivityCompat;\nimport androidx.core.app.SharedElementCallback;\nimport androidx.lifecycle.Lifecycle;\nimport androidx.lifecycle.LifecycleOwner;\nimport androidx.lifecycle.LifecycleRegistry;\nimport androidx.lifecycle.ViewModelStore;\nimport androidx.lifecycle.ViewModelStoreOwner;\nimport androidx.loader.app.LoaderManager;\n\nimport java.io.FileDescriptor;\nimport java.io.PrintWriter;\nimport java.util.Collection;\n\n/**\n * Base class for activities that want to use the support-based\n * {@link Fragment Fragments}.\n *\n * <p>Known limitations:</p>\n * <ul>\n * <li> <p>When using the <code>&lt;fragment></code> tag, this implementation can not\n * use the parent view's ID as the new fragment's ID.  You must explicitly\n * specify an ID (or tag) in the <code>&lt;fragment></code>.</p>\n * </ul>\n */\npublic class FragmentActivity extends ComponentActivity implements\n        ActivityCompat.OnRequestPermissionsResultCallback,\n        ActivityCompat.RequestPermissionsRequestCodeValidator {\n    private static final String TAG = \"FragmentActivity\";\n\n    static final String FRAGMENTS_TAG = \"android:support:fragments\";\n    static final String NEXT_CANDIDATE_REQUEST_INDEX_TAG = \"android:support:next_request_index\";\n    static final String ALLOCATED_REQUEST_INDICIES_TAG = \"android:support:request_indicies\";\n    static final String REQUEST_FRAGMENT_WHO_TAG = \"android:support:request_fragment_who\";\n    static final int MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS = 0xffff - 1;\n\n    final FragmentController mFragments = FragmentController.createController(new HostCallbacks());\n    /**\n     * A {@link Lifecycle} that is exactly nested outside of when the FragmentController\n     * has its state changed, providing the proper nesting of Lifecycle callbacks\n     * <p>\n     * TODO(b/127528777) Drive Fragment Lifecycle with LifecycleObserver\n     */\n    final LifecycleRegistry mFragmentLifecycleRegistry = new LifecycleRegistry(this);\n\n    boolean mCreated;\n    boolean mResumed;\n    boolean mStopped = true;\n\n    boolean mRequestedPermissionsFromFragment;\n\n    // We need to keep track of whether startIntentSenderForResult originated from a Fragment, so we\n    // can conditionally check whether the requestCode collides with our reserved ID space for the\n    // request index (see above). Unfortunately we can't just call\n    // super.startIntentSenderForResult(...) to bypass the check when the call didn't come from a\n    // fragment, since we need to use the ActivityCompat version for backward compatibility.\n    boolean mStartedIntentSenderFromFragment;\n    // We need to keep track of whether startActivityForResult originated from a Fragment, so we\n    // can conditionally check whether the requestCode collides with our reserved ID space for the\n    // request index (see above). Unfortunately we can't just call\n    // super.startActivityForResult(...) to bypass the check when the call didn't come from a\n    // fragment, since we need to use the ActivityCompat version for backward compatibility.\n    boolean mStartedActivityFromFragment;\n\n    // A hint for the next candidate request index. Request indicies are ints between 0 and 2^16-1\n    // which are encoded into the upper 16 bits of the requestCode for\n    // Fragment.startActivityForResult(...) calls. This allows us to dispatch onActivityResult(...)\n    // to the appropriate Fragment. Request indicies are allocated by allocateRequestIndex(...).\n    int mNextCandidateRequestIndex;\n    // A map from request index to Fragment \"who\" (i.e. a Fragment's unique identifier). Used to\n    // keep track of the originating Fragment for Fragment.startActivityForResult(...) calls, so we\n    // can dispatch the onActivityResult(...) to the appropriate Fragment. Will only contain entries\n    // for startActivityForResult calls where a result has not yet been delivered.\n    SparseArrayCompat<String> mPendingFragmentActivityResults;\n\n    /**\n     * Default constructor for FragmentActivity. All Activities must have a default constructor\n     * for API 27 and lower devices or when using the default\n     * {@link android.app.AppComponentFactory}.\n     */\n    public FragmentActivity() {\n        super();\n    }\n\n    /**\n     * Alternate constructor that can be used to provide a default layout\n     * that will be inflated as part of <code>super.onCreate(savedInstanceState)</code>.\n     *\n     * <p>This should generally be called from your constructor that takes no parameters,\n     * as is required for API 27 and lower or when using the default\n     * {@link android.app.AppComponentFactory}.\n     *\n     * @see #FragmentActivity()\n     */\n    @ContentView\n    public FragmentActivity(@LayoutRes int contentLayoutId) {\n        super(contentLayoutId);\n    }\n\n    // ------------------------------------------------------------------------\n    // HOOKS INTO ACTIVITY\n    // ------------------------------------------------------------------------\n\n    /**\n     * Dispatch incoming result to the correct fragment.\n     */\n    @Override\n    @CallSuper\n    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {\n        mFragments.noteStateNotSaved();\n        int requestIndex = requestCode>>16;\n        if (requestIndex != 0) {\n            requestIndex--;\n\n            String who = mPendingFragmentActivityResults.get(requestIndex);\n            mPendingFragmentActivityResults.remove(requestIndex);\n            if (who == null) {\n                Log.w(TAG, \"Activity result delivered for unknown Fragment.\");\n                return;\n            }\n            Fragment targetFragment = mFragments.findFragmentByWho(who);\n            if (targetFragment == null) {\n                Log.w(TAG, \"Activity result no fragment exists for who: \" + who);\n            } else {\n                targetFragment.onActivityResult(requestCode & 0xffff, resultCode, data);\n            }\n            return;\n        }\n        ActivityCompat.PermissionCompatDelegate delegate =\n                ActivityCompat.getPermissionCompatDelegate();\n        if (delegate != null && delegate.onActivityResult(this, requestCode, resultCode, data)) {\n            // Delegate has handled the activity result\n            return;\n        }\n\n        super.onActivityResult(requestCode, resultCode, data);\n    }\n\n    /**\n     * Reverses the Activity Scene entry Transition and triggers the calling Activity\n     * to reverse its exit Transition. When the exit Transition completes,\n     * {@link #finish()} is called. If no entry Transition was used, finish() is called\n     * immediately and the Activity exit Transition is run.\n     *\n     * <p>On Android 4.4 or lower, this method only finishes the Activity with no\n     * special exit transition.</p>\n     */\n    public void supportFinishAfterTransition() {\n        ActivityCompat.finishAfterTransition(this);\n    }\n\n    /**\n     * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity,\n     * android.view.View, String)} was used to start an Activity, <var>callback</var>\n     * will be called to handle shared elements on the <i>launched</i> Activity. This requires\n     * {@link Window#FEATURE_CONTENT_TRANSITIONS}.\n     *\n     * @param callback Used to manipulate shared element transitions on the launched Activity.\n     */\n    public void setEnterSharedElementCallback(@Nullable SharedElementCallback callback) {\n        ActivityCompat.setEnterSharedElementCallback(this, callback);\n    }\n\n    /**\n     * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity,\n     * android.view.View, String)} was used to start an Activity, <var>listener</var>\n     * will be called to handle shared elements on the <i>launching</i> Activity. Most\n     * calls will only come when returning from the started Activity.\n     * This requires {@link Window#FEATURE_CONTENT_TRANSITIONS}.\n     *\n     * @param listener Used to manipulate shared element transitions on the launching Activity.\n     */\n    public void setExitSharedElementCallback(@Nullable SharedElementCallback listener) {\n        ActivityCompat.setExitSharedElementCallback(this, listener);\n    }\n\n    /**\n     * Support library version of {@link android.app.Activity#postponeEnterTransition()} that works\n     * only on API 21 and later.\n     */\n    public void supportPostponeEnterTransition() {\n        ActivityCompat.postponeEnterTransition(this);\n    }\n\n    /**\n     * Support library version of {@link android.app.Activity#startPostponedEnterTransition()}\n     * that only works with API 21 and later.\n     */\n    public void supportStartPostponedEnterTransition() {\n        ActivityCompat.startPostponedEnterTransition(this);\n    }\n\n    /**\n     * {@inheritDoc}\n     *\n     * <p><strong>Note:</strong> If you override this method you must call\n     * <code>super.onMultiWindowModeChanged</code> to correctly dispatch the event\n     * to support fragments attached to this activity.</p>\n     *\n     * @param isInMultiWindowMode True if the activity is in multi-window mode.\n     */\n    @Override\n    @CallSuper\n    public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {\n        mFragments.dispatchMultiWindowModeChanged(isInMultiWindowMode);\n    }\n\n    /**\n     * {@inheritDoc}\n     *\n     * <p><strong>Note:</strong> If you override this method you must call\n     * <code>super.onPictureInPictureModeChanged</code> to correctly dispatch the event\n     * to support fragments attached to this activity.</p>\n     *\n     * @param isInPictureInPictureMode True if the activity is in picture-in-picture mode.\n     */\n    @Override\n    @CallSuper\n    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {\n        mFragments.dispatchPictureInPictureModeChanged(isInPictureInPictureMode);\n    }\n\n    /**\n     * Dispatch configuration change to all fragments.\n     */\n    @Override\n    public void onConfigurationChanged(@NonNull Configuration newConfig) {\n        super.onConfigurationChanged(newConfig);\n        mFragments.noteStateNotSaved();\n        mFragments.dispatchConfigurationChanged(newConfig);\n    }\n\n    /**\n     * Perform initialization of all fragments.\n     */\n    @SuppressWarnings(\"deprecation\")\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        mFragments.attachHost(null /*parent*/);\n\n        if (savedInstanceState != null) {\n            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);\n            mFragments.restoreSaveState(p);\n\n            // Check if there are any pending onActivityResult calls to descendent Fragments.\n            if (savedInstanceState.containsKey(NEXT_CANDIDATE_REQUEST_INDEX_TAG)) {\n                mNextCandidateRequestIndex =\n                        savedInstanceState.getInt(NEXT_CANDIDATE_REQUEST_INDEX_TAG);\n                int[] requestCodes = savedInstanceState.getIntArray(ALLOCATED_REQUEST_INDICIES_TAG);\n                String[] fragmentWhos = savedInstanceState.getStringArray(REQUEST_FRAGMENT_WHO_TAG);\n                if (requestCodes == null || fragmentWhos == null ||\n                            requestCodes.length != fragmentWhos.length) {\n                    Log.w(TAG, \"Invalid requestCode mapping in savedInstanceState.\");\n                } else {\n                    mPendingFragmentActivityResults = new SparseArrayCompat<>(requestCodes.length);\n                    for (int i = 0; i < requestCodes.length; i++) {\n                        mPendingFragmentActivityResults.put(requestCodes[i], fragmentWhos[i]);\n                    }\n                }\n            }\n        }\n\n        if (mPendingFragmentActivityResults == null) {\n            mPendingFragmentActivityResults = new SparseArrayCompat<>();\n            mNextCandidateRequestIndex = 0;\n        }\n\n        super.onCreate(savedInstanceState);\n\n        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);\n        mFragments.dispatchCreate();\n    }\n\n    /**\n     * Dispatch to Fragment.onCreateOptionsMenu().\n     */\n    @Override\n    public boolean onCreatePanelMenu(int featureId, @NonNull Menu menu) {\n        if (featureId == Window.FEATURE_OPTIONS_PANEL) {\n            boolean show = super.onCreatePanelMenu(featureId, menu);\n            show |= mFragments.dispatchCreateOptionsMenu(menu, getMenuInflater());\n            return show;\n        }\n        return super.onCreatePanelMenu(featureId, menu);\n    }\n\n    @Override\n    @Nullable\n    public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context,\n            @NonNull AttributeSet attrs) {\n        final View v = dispatchFragmentsOnCreateView(parent, name, context, attrs);\n        if (v == null) {\n            return super.onCreateView(parent, name, context, attrs);\n        }\n        return v;\n    }\n\n    @Override\n    @Nullable\n    public View onCreateView(@NonNull String name, @NonNull Context context,\n            @NonNull AttributeSet attrs) {\n        final View v = dispatchFragmentsOnCreateView(null, name, context, attrs);\n        if (v == null) {\n            return super.onCreateView(name, context, attrs);\n        }\n        return v;\n    }\n\n    @Nullable\n    final View dispatchFragmentsOnCreateView(@Nullable View parent, @NonNull String name,\n            @NonNull Context context, @NonNull AttributeSet attrs) {\n        return mFragments.onCreateView(parent, name, context, attrs);\n    }\n\n    /**\n     * Destroy all fragments.\n     */\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        mFragments.dispatchDestroy();\n        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);\n    }\n\n    /**\n     * Dispatch onLowMemory() to all fragments.\n     */\n    @Override\n    public void onLowMemory() {\n        super.onLowMemory();\n        mFragments.dispatchLowMemory();\n    }\n\n    /**\n     * Dispatch context and options menu to fragments.\n     */\n    @Override\n    public boolean onMenuItemSelected(int featureId, @NonNull MenuItem item) {\n        if (super.onMenuItemSelected(featureId, item)) {\n            return true;\n        }\n\n        switch (featureId) {\n            case Window.FEATURE_OPTIONS_PANEL:\n                return mFragments.dispatchOptionsItemSelected(item);\n\n            case Window.FEATURE_CONTEXT_MENU:\n                return mFragments.dispatchContextItemSelected(item);\n\n            default:\n                return false;\n        }\n    }\n\n    /**\n     * Call onOptionsMenuClosed() on fragments.\n     */\n    @Override\n    public void onPanelClosed(int featureId, @NonNull Menu menu) {\n        switch (featureId) {\n            case Window.FEATURE_OPTIONS_PANEL:\n                mFragments.dispatchOptionsMenuClosed(menu);\n                break;\n        }\n        super.onPanelClosed(featureId, menu);\n    }\n\n    /**\n     * Dispatch onPause() to fragments.\n     */\n    @Override\n    protected void onPause() {\n        super.onPause();\n        mResumed = false;\n        mFragments.dispatchPause();\n        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);\n    }\n\n    /**\n     * Handle onNewIntent() to inform the fragment manager that the\n     * state is not saved.  If you are handling new intents and may be\n     * making changes to the fragment state, you want to be sure to call\n     * through to the super-class here first.  Otherwise, if your state\n     * is saved but the activity is not stopped, you could get an\n     * onNewIntent() call which happens before onResume() and trying to\n     * perform fragment operations at that point will throw IllegalStateException\n     * because the fragment manager thinks the state is still saved.\n     */\n    @Override\n    @CallSuper\n    protected void onNewIntent(@SuppressLint(\"UnknownNullness\") Intent intent) {\n        super.onNewIntent(intent);\n        mFragments.noteStateNotSaved();\n    }\n\n    /**\n     * Hook in to note that fragment state is no longer saved.\n     */\n    @Override\n    public void onStateNotSaved() {\n        mFragments.noteStateNotSaved();\n    }\n\n    /**\n     * Dispatch onResume() to fragments.  Note that for better inter-operation\n     * with older versions of the platform, at the point of this call the\n     * fragments attached to the activity are <em>not</em> resumed.\n     */\n    @Override\n    protected void onResume() {\n        super.onResume();\n        mResumed = true;\n        mFragments.noteStateNotSaved();\n        mFragments.execPendingActions();\n    }\n\n    /**\n     * Dispatch onResume() to fragments.\n     */\n    @Override\n    protected void onPostResume() {\n        super.onPostResume();\n        onResumeFragments();\n    }\n\n    /**\n     * This is the fragment-orientated version of {@link #onResume()} that you\n     * can override to perform operations in the Activity at the same point\n     * where its fragments are resumed.  Be sure to always call through to\n     * the super-class.\n     */\n    protected void onResumeFragments() {\n        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);\n        mFragments.dispatchResume();\n    }\n\n    /**\n     * Dispatch onPrepareOptionsMenu() to fragments.\n     */\n    @Override\n    public boolean onPreparePanel(int featureId, @Nullable View view, @NonNull Menu menu) {\n        if (featureId == Window.FEATURE_OPTIONS_PANEL) {\n            boolean goforit = onPrepareOptionsPanel(view, menu);\n            goforit |= mFragments.dispatchPrepareOptionsMenu(menu);\n            return goforit;\n        }\n        return super.onPreparePanel(featureId, view, menu);\n    }\n\n    /**\n     * @hide\n     * @deprecated Override {@link #onPreparePanel(int, View, Menu)}.\n     */\n    @RestrictTo(LIBRARY_GROUP_PREFIX)\n    @Deprecated\n    protected boolean onPrepareOptionsPanel(@Nullable View view, @NonNull Menu menu) {\n        return super.onPreparePanel(Window.FEATURE_OPTIONS_PANEL, view, menu);\n    }\n\n    /**\n     * Save all appropriate fragment state.\n     */\n    @Override\n    protected void onSaveInstanceState(@NonNull Bundle outState) {\n        super.onSaveInstanceState(outState);\n        markFragmentsCreated();\n        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);\n        Parcelable p = mFragments.saveAllState();\n        if (p != null) {\n            outState.putParcelable(FRAGMENTS_TAG, p);\n        }\n        if (mPendingFragmentActivityResults.size() > 0) {\n            outState.putInt(NEXT_CANDIDATE_REQUEST_INDEX_TAG, mNextCandidateRequestIndex);\n\n            int[] requestCodes = new int[mPendingFragmentActivityResults.size()];\n            String[] fragmentWhos = new String[mPendingFragmentActivityResults.size()];\n            for (int i = 0; i < mPendingFragmentActivityResults.size(); i++) {\n                requestCodes[i] = mPendingFragmentActivityResults.keyAt(i);\n                fragmentWhos[i] = mPendingFragmentActivityResults.valueAt(i);\n            }\n            outState.putIntArray(ALLOCATED_REQUEST_INDICIES_TAG, requestCodes);\n            outState.putStringArray(REQUEST_FRAGMENT_WHO_TAG, fragmentWhos);\n        }\n    }\n\n    /**\n     * Dispatch onStart() to all fragments.\n     */\n    @Override\n    protected void onStart() {\n        super.onStart();\n\n        mStopped = false;\n\n        if (!mCreated) {\n            mCreated = true;\n            mFragments.dispatchActivityCreated();\n        }\n\n        mFragments.noteStateNotSaved();\n        mFragments.execPendingActions();\n\n        // NOTE: HC onStart goes here.\n\n        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);\n        mFragments.dispatchStart();\n    }\n\n    /**\n     * Dispatch onStop() to all fragments.\n     */\n    @Override\n    protected void onStop() {\n        super.onStop();\n\n        mStopped = true;\n        markFragmentsCreated();\n\n        mFragments.dispatchStop();\n        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);\n    }\n\n    // ------------------------------------------------------------------------\n    // NEW METHODS\n    // ------------------------------------------------------------------------\n\n    /**\n     * Support library version of {@link Activity#invalidateOptionsMenu}.\n     *\n     * <p>Invalidate the activity's options menu. This will cause relevant presentations\n     * of the menu to fully update via calls to onCreateOptionsMenu and\n     * onPrepareOptionsMenu the next time the menu is requested.\n     *\n     * @deprecated Call {@link Activity#invalidateOptionsMenu} directly.\n     */\n    @Deprecated\n    public void supportInvalidateOptionsMenu() {\n        invalidateOptionsMenu();\n    }\n\n    /**\n     * Print the Activity's state into the given stream.  This gets invoked if\n     * you run \"adb shell dumpsys activity <activity_component_name>\".\n     *\n     * @param prefix Desired prefix to prepend at each line of output.\n     * @param fd The raw file descriptor that the dump is being sent to.\n     * @param writer The PrintWriter to which you should dump your state.  This will be\n     * closed for you after you return.\n     * @param args additional arguments to the dump request.\n     */\n    @Override\n    public void dump(@NonNull String prefix, @Nullable FileDescriptor fd,\n            @NonNull PrintWriter writer, @Nullable String[] args) {\n        super.dump(prefix, fd, writer, args);\n        writer.print(prefix); writer.print(\"Local FragmentActivity \");\n                writer.print(Integer.toHexString(System.identityHashCode(this)));\n                writer.println(\" State:\");\n        String innerPrefix = prefix + \"  \";\n        writer.print(innerPrefix); writer.print(\"mCreated=\");\n                writer.print(mCreated); writer.print(\" mResumed=\");\n                writer.print(mResumed); writer.print(\" mStopped=\");\n                writer.print(mStopped);\n\n        if (getApplication() != null) {\n            LoaderManager.getInstance(this).dump(innerPrefix, fd, writer, args);\n        }\n        mFragments.getSupportFragmentManager().dump(prefix, fd, writer, args);\n    }\n\n    // ------------------------------------------------------------------------\n    // FRAGMENT SUPPORT\n    // ------------------------------------------------------------------------\n\n    /**\n     * Called when a fragment is attached to the activity.\n     *\n     * <p>This is called after the attached fragment's <code>onAttach</code> and before\n     * the attached fragment's <code>onCreate</code> if the fragment has not yet had a previous\n     * call to <code>onCreate</code>.</p>\n     */\n    @SuppressWarnings(\"unused\")\n    public void onAttachFragment(@NonNull Fragment fragment) {\n    }\n\n    /**\n     * Return the FragmentManager for interacting with fragments associated\n     * with this activity.\n     */\n    @NonNull\n    public FragmentManager getSupportFragmentManager() {\n        return mFragments.getSupportFragmentManager();\n    }\n\n    /**\n     * @deprecated Use\n     * {@link LoaderManager#getInstance(LifecycleOwner) LoaderManager.getInstance(this)}.\n     */\n    @Deprecated\n    @NonNull\n    public LoaderManager getSupportLoaderManager() {\n        return LoaderManager.getInstance(this);\n    }\n\n    /**\n     * Modifies the standard behavior to allow results to be delivered to fragments.\n     * This imposes a restriction that requestCode be <= 0xffff.\n     */\n    @Override\n    public void startActivityForResult(@SuppressLint(\"UnknownNullness\") Intent intent,\n            int requestCode) {\n        // If this was started from a Fragment we've already checked the upper 16 bits were not in\n        // use, and then repurposed them for the Fragment's index.\n        if (!mStartedActivityFromFragment) {\n            if (requestCode != -1) {\n                checkForValidRequestCode(requestCode);\n            }\n        }\n        // MOD: swallow activity not found in Editor\n        try {\n            super.startActivityForResult(intent, requestCode);\n        } catch (ActivityNotFoundException e) {\n            // ActivityNotFoundException: No Activity found to handle Intent { act=com.android.settings.USER_DICTIONARY_INSERT flg=0x10000000 (has extras) }\n            Log.e(TAG, e.getMessage());\n            Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();\n        }\n    }\n\n    @Override\n    public void startActivityForResult(@SuppressLint(\"UnknownNullness\") Intent intent,\n            int requestCode, @Nullable Bundle options) {\n        // If this was started from a Fragment we've already checked the upper 16 bits were not in\n        // use, and then repurposed them for the Fragment's index.\n        if (!mStartedActivityFromFragment) {\n            if (requestCode != -1) {\n                checkForValidRequestCode(requestCode);\n            }\n        }\n        super.startActivityForResult(intent, requestCode, options);\n    }\n\n    @Override\n    public void startIntentSenderForResult(@SuppressLint(\"UnknownNullness\") IntentSender intent,\n            int requestCode, @Nullable Intent fillInIntent, int flagsMask,\n            int flagsValues, int extraFlags) throws IntentSender.SendIntentException {\n        // If this was started from a Fragment we've already checked the upper 16 bits were not in\n        // use, and then repurposed them for the Fragment's index.\n        if (!mStartedIntentSenderFromFragment) {\n            if (requestCode != -1) {\n                checkForValidRequestCode(requestCode);\n            }\n        }\n        super.startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues,\n                extraFlags);\n    }\n\n    @Override\n    public void startIntentSenderForResult(@SuppressLint(\"UnknownNullness\") IntentSender intent,\n            int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues,\n            int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException {\n        // If this was started from a Fragment we've already checked the upper 16 bits were not in\n        // use, and then repurposed them for the Fragment's index.\n        if (!mStartedIntentSenderFromFragment) {\n            if (requestCode != -1) {\n                checkForValidRequestCode(requestCode);\n            }\n        }\n        super.startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues,\n                extraFlags, options);\n    }\n\n    /**\n     * Checks whether the given request code is a valid code by masking it with 0xffff0000. Throws\n     * an {@link IllegalArgumentException} if the code is not valid.\n     */\n    static void checkForValidRequestCode(int requestCode) {\n        if ((requestCode & 0xffff0000) != 0) {\n            throw new IllegalArgumentException(\"Can only use lower 16 bits for requestCode\");\n        }\n    }\n\n    @Override\n    public final void validateRequestPermissionsRequestCode(int requestCode) {\n        // We use 16 bits of the request code to encode the fragment id when\n        // requesting permissions from a fragment. Hence, requestPermissions()\n        // should validate the code against that but we cannot override it as\n        // we can not then call super and also the ActivityCompat would call\n        // back to this override. To handle this we use dependency inversion\n        // where we are the validator of request codes when requesting\n        // permissions in ActivityCompat.\n        if (!mRequestedPermissionsFromFragment\n                && requestCode != -1) {\n            checkForValidRequestCode(requestCode);\n        }\n    }\n\n    /**\n     * Callback for the result from requesting permissions. This method\n     * is invoked for every call on {@link #requestPermissions(String[], int)}.\n     * <p>\n     * <strong>Note:</strong> It is possible that the permissions request interaction\n     * with the user is interrupted. In this case you will receive empty permissions\n     * and results arrays which should be treated as a cancellation.\n     * </p>\n     *\n     * @param requestCode The request code passed in {@link #requestPermissions(String[], int)}.\n     * @param permissions The requested permissions. Never null.\n     * @param grantResults The grant results for the corresponding permissions\n     *     which is either {@link android.content.pm.PackageManager#PERMISSION_GRANTED}\n     *     or {@link android.content.pm.PackageManager#PERMISSION_DENIED}. Never null.\n     *\n     * @see #requestPermissions(String[], int)\n     */\n    @Override\n    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,\n            @NonNull int[] grantResults) {\n        mFragments.noteStateNotSaved();\n        int index = (requestCode >> 16) & 0xffff;\n        if (index != 0) {\n            index--;\n\n            String who = mPendingFragmentActivityResults.get(index);\n            mPendingFragmentActivityResults.remove(index);\n            if (who == null) {\n                Log.w(TAG, \"Activity result delivered for unknown Fragment.\");\n                return;\n            }\n            Fragment frag = mFragments.findFragmentByWho(who);\n            if (frag == null) {\n                Log.w(TAG, \"Activity result no fragment exists for who: \" + who);\n            } else {\n                frag.onRequestPermissionsResult(requestCode & 0xffff, permissions, grantResults);\n            }\n        }\n    }\n\n    /**\n     * Called by Fragment.startActivityForResult() to implement its behavior.\n     */\n    public void startActivityFromFragment(@NonNull Fragment fragment,\n            @SuppressLint(\"UnknownNullness\") Intent intent, int requestCode) {\n        startActivityFromFragment(fragment, intent, requestCode, null);\n    }\n\n    /**\n     * Called by Fragment.startActivityForResult() to implement its behavior.\n     */\n    public void startActivityFromFragment(@NonNull Fragment fragment,\n            @SuppressLint(\"UnknownNullness\") Intent intent, int requestCode,\n            @Nullable Bundle options) {\n        mStartedActivityFromFragment = true;\n        try {\n            if (requestCode == -1) {\n                ActivityCompat.startActivityForResult(this, intent, -1, options);\n                return;\n            }\n            checkForValidRequestCode(requestCode);\n            int requestIndex = allocateRequestIndex(fragment);\n            ActivityCompat.startActivityForResult(\n                    this, intent, ((requestIndex + 1) << 16) + (requestCode & 0xffff), options);\n        } finally {\n            mStartedActivityFromFragment = false;\n        }\n    }\n\n    /**\n     * Called by Fragment.startIntentSenderForResult() to implement its behavior.\n     */\n    public void startIntentSenderFromFragment(@NonNull Fragment fragment,\n            @SuppressLint(\"UnknownNullness\") IntentSender intent, int requestCode,\n            @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,\n            @Nullable Bundle options) throws IntentSender.SendIntentException {\n        mStartedIntentSenderFromFragment = true;\n        try {\n            if (requestCode == -1) {\n                ActivityCompat.startIntentSenderForResult(this, intent, requestCode, fillInIntent,\n                        flagsMask, flagsValues, extraFlags, options);\n                return;\n            }\n            checkForValidRequestCode(requestCode);\n            int requestIndex = allocateRequestIndex(fragment);\n            ActivityCompat.startIntentSenderForResult(this, intent,\n                    ((requestIndex + 1) << 16) + (requestCode & 0xffff), fillInIntent,\n                    flagsMask, flagsValues, extraFlags, options);\n        } finally {\n            mStartedIntentSenderFromFragment = false;\n        }\n    }\n\n    // Allocates the next available startActivityForResult request index.\n    private int allocateRequestIndex(@NonNull Fragment fragment) {\n        // Sanity check that we havn't exhaused the request index space.\n        if (mPendingFragmentActivityResults.size() >= MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS) {\n            throw new IllegalStateException(\"Too many pending Fragment activity results.\");\n        }\n\n        // Find an unallocated request index in the mPendingFragmentActivityResults map.\n        while (mPendingFragmentActivityResults.indexOfKey(mNextCandidateRequestIndex) >= 0) {\n            mNextCandidateRequestIndex =\n                    (mNextCandidateRequestIndex + 1) % MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS;\n        }\n\n        int requestIndex = mNextCandidateRequestIndex;\n        mPendingFragmentActivityResults.put(requestIndex, fragment.mWho);\n        mNextCandidateRequestIndex =\n                (mNextCandidateRequestIndex + 1) % MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS;\n\n        return requestIndex;\n    }\n\n    /**\n     * Called by Fragment.requestPermissions() to implement its behavior.\n     */\n    void requestPermissionsFromFragment(@NonNull Fragment fragment, @NonNull String[] permissions,\n            int requestCode) {\n        if (requestCode == -1) {\n            ActivityCompat.requestPermissions(this, permissions, requestCode);\n            return;\n        }\n        checkForValidRequestCode(requestCode);\n        try {\n            mRequestedPermissionsFromFragment = true;\n            int requestIndex = allocateRequestIndex(fragment);\n            ActivityCompat.requestPermissions(this, permissions,\n                    ((requestIndex + 1) << 16) + (requestCode & 0xffff));\n        } finally {\n            mRequestedPermissionsFromFragment = false;\n        }\n    }\n\n    class HostCallbacks extends FragmentHostCallback<FragmentActivity> implements\n            ViewModelStoreOwner,\n            OnBackPressedDispatcherOwner {\n        public HostCallbacks() {\n            super(FragmentActivity.this /*fragmentActivity*/);\n        }\n\n        @NonNull\n        @Override\n        public Lifecycle getLifecycle() {\n            // Instead of directly using the Activity's Lifecycle, we\n            // use a LifecycleRegistry that is nested exactly outside of\n            // when Fragments get their lifecycle changed\n            // TODO(b/127528777) Drive Fragment Lifecycle with LifecycleObserver\n            return mFragmentLifecycleRegistry;\n        }\n\n        @NonNull\n        @Override\n        public ViewModelStore getViewModelStore() {\n            return FragmentActivity.this.getViewModelStore();\n        }\n\n        @NonNull\n        @Override\n        public OnBackPressedDispatcher getOnBackPressedDispatcher() {\n            return FragmentActivity.this.getOnBackPressedDispatcher();\n        }\n\n        @Override\n        public void onDump(@NonNull String prefix, @Nullable FileDescriptor fd,\n                @NonNull PrintWriter writer, @Nullable String[] args) {\n            FragmentActivity.this.dump(prefix, fd, writer, args);\n        }\n\n        @Override\n        public boolean onShouldSaveFragmentState(@NonNull Fragment fragment) {\n            return !isFinishing();\n        }\n\n        @Override\n        @NonNull\n        public LayoutInflater onGetLayoutInflater() {\n            return FragmentActivity.this.getLayoutInflater().cloneInContext(FragmentActivity.this);\n        }\n\n        @Override\n        public FragmentActivity onGetHost() {\n            return FragmentActivity.this;\n        }\n\n        @Override\n        public void onSupportInvalidateOptionsMenu() {\n            FragmentActivity.this.supportInvalidateOptionsMenu();\n        }\n\n        @Override\n        public void onStartActivityFromFragment(@NonNull Fragment fragment, Intent intent,\n                int requestCode) {\n            FragmentActivity.this.startActivityFromFragment(fragment, intent, requestCode);\n        }\n\n        @Override\n        public void onStartActivityFromFragment(@NonNull Fragment fragment, Intent intent,\n                int requestCode, @Nullable Bundle options) {\n            FragmentActivity.this.startActivityFromFragment(fragment, intent, requestCode, options);\n        }\n\n        @Override\n        public void onStartIntentSenderFromFragment(\n                @NonNull Fragment fragment, IntentSender intent, int requestCode,\n                @Nullable Intent fillInIntent, int flagsMask, int flagsValues,\n                int extraFlags, Bundle options) throws IntentSender.SendIntentException {\n            FragmentActivity.this.startIntentSenderFromFragment(fragment, intent, requestCode,\n                    fillInIntent, flagsMask, flagsValues, extraFlags, options);\n        }\n\n        @Override\n        public void onRequestPermissionsFromFragment(@NonNull Fragment fragment,\n                @NonNull String[] permissions, int requestCode) {\n            FragmentActivity.this.requestPermissionsFromFragment(fragment, permissions,\n                    requestCode);\n        }\n\n        @Override\n        public boolean onShouldShowRequestPermissionRationale(@NonNull String permission) {\n            return ActivityCompat.shouldShowRequestPermissionRationale(\n                    FragmentActivity.this, permission);\n        }\n\n        @Override\n        public boolean onHasWindowAnimations() {\n            return getWindow() != null;\n        }\n\n        @Override\n        public int onGetWindowAnimations() {\n            final Window w = getWindow();\n            return (w == null) ? 0 : w.getAttributes().windowAnimations;\n        }\n\n        @Override\n        public void onAttachFragment(@NonNull Fragment fragment) {\n            FragmentActivity.this.onAttachFragment(fragment);\n        }\n\n        @Nullable\n        @Override\n        public View onFindViewById(int id) {\n            return FragmentActivity.this.findViewById(id);\n        }\n\n        @Override\n        public boolean onHasView() {\n            final Window w = getWindow();\n            return (w != null && w.peekDecorView() != null);\n        }\n    }\n\n    private void markFragmentsCreated() {\n        boolean reiterate;\n        do {\n            reiterate = markState(getSupportFragmentManager(), Lifecycle.State.CREATED);\n        } while (reiterate);\n    }\n\n    private static boolean markState(FragmentManager manager, Lifecycle.State state) {\n        boolean hadNotMarked = false;\n        Collection<Fragment> fragments = manager.getFragments();\n        for (Fragment fragment : fragments) {\n            if (fragment == null) {\n                continue;\n            }\n            if (fragment.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {\n                fragment.mLifecycleRegistry.setCurrentState(state);\n                hadNotMarked = true;\n            }\n\n            if (fragment.getHost() != null) {\n                FragmentManager childFragmentManager = fragment.getChildFragmentManager();\n                hadNotMarked |= markState(childFragmentManager, state);\n            }\n        }\n        return hadNotMarked;\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentContainer.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.View;\n\nimport androidx.annotation.IdRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\n\n/**\n * Callbacks to a {@link Fragment}'s container.\n */\npublic abstract class FragmentContainer {\n    /**\n     * Return the view with the given resource ID. May return {@code null} if the\n     * view is not a child of this container.\n     */\n    @Nullable\n    public abstract View onFindViewById(@IdRes int id);\n\n    /**\n     * Return {@code true} if the container holds any view.\n     */\n    public abstract boolean onHasView();\n\n\n    /**\n     * Creates an instance of the specified fragment, can be overridden to construct fragments\n     * with dependencies, or change the fragment being constructed. By default just calls\n     * {@link Fragment#instantiate(Context, String, Bundle)}.\n     * @deprecated Use {@link FragmentManager#setFragmentFactory} to control how Fragments are\n     * instantiated.\n     */\n    @SuppressWarnings(\"deprecation\")\n    @Deprecated\n    @NonNull\n    public Fragment instantiate(@NonNull Context context, @NonNull String className,\n            @Nullable Bundle arguments) {\n        return Fragment.instantiate(context, className, arguments);\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentController.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport static androidx.core.util.Preconditions.checkNotNull;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.res.Configuration;\nimport android.os.Parcelable;\nimport android.util.AttributeSet;\nimport android.view.Menu;\nimport android.view.MenuInflater;\nimport android.view.MenuItem;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.collection.SimpleArrayMap;\nimport androidx.lifecycle.ViewModelStoreOwner;\nimport androidx.loader.app.LoaderManager;\n\nimport java.io.FileDescriptor;\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Provides integration points with a {@link FragmentManager} for a fragment host.\n * <p>\n * It is the responsibility of the host to take care of the Fragment's lifecycle.\n * The methods provided by {@link FragmentController} are for that purpose.\n */\npublic class FragmentController {\n    private final FragmentHostCallback<?> mHost;\n\n    /**\n     * Returns a {@link FragmentController}.\n     */\n    @NonNull\n    public static FragmentController createController(@NonNull FragmentHostCallback<?> callbacks) {\n        return new FragmentController(checkNotNull(callbacks, \"callbacks == null\"));\n    }\n\n    private FragmentController(FragmentHostCallback<?> callbacks) {\n        mHost = callbacks;\n    }\n\n    /**\n     * Returns a {@link FragmentManager} for this controller.\n     */\n    @NonNull\n    public FragmentManager getSupportFragmentManager() {\n        return mHost.mFragmentManager;\n    }\n\n    /**\n     * Returns a {@link LoaderManager}.\n     *\n     * @deprecated Loaders are managed separately from FragmentController and this now throws an\n     * {@link UnsupportedOperationException}. Use {@link LoaderManager#getInstance} to obtain a\n     * LoaderManager.\n     * @see LoaderManager#getInstance\n     */\n    @Deprecated\n    @SuppressLint(\"UnknownNullness\")\n    public LoaderManager getSupportLoaderManager() {\n        throw new UnsupportedOperationException(\"Loaders are managed separately from \"\n                + \"FragmentController, use LoaderManager.getInstance() to obtain a LoaderManager.\");\n    }\n\n    /**\n     * Returns a fragment with the given identifier.\n     */\n    @Nullable\n    public Fragment findFragmentByWho(@NonNull String who) {\n        return mHost.mFragmentManager.findFragmentByWho(who);\n    }\n\n    /**\n     * Returns the number of active fragments.\n     */\n    public int getActiveFragmentsCount() {\n        return mHost.mFragmentManager.getActiveFragmentCount();\n    }\n\n    /**\n     * Returns the list of active fragments.\n     */\n    @NonNull\n    public List<Fragment> getActiveFragments(@SuppressLint(\"UnknownNullness\")\n            List<Fragment> actives) {\n        return mHost.mFragmentManager.getActiveFragments();\n    }\n\n    /**\n     * Attaches the host to the FragmentManager for this controller. The host must be\n     * attached before the FragmentManager can be used to manage Fragments.\n     */\n    public void attachHost(@Nullable Fragment parent) {\n        mHost.mFragmentManager.attachController(\n                mHost, mHost /*container*/, parent);\n    }\n\n    /**\n     * Instantiates a Fragment's view.\n     *\n     * @param parent The parent that the created view will be placed\n     * in; <em>note that this may be null</em>.\n     * @param name Tag name to be inflated.\n     * @param context The context the view is being created in.\n     * @param attrs Inflation attributes as specified in XML file.\n     *\n     * @return view the newly created view\n     */\n    @Nullable\n    public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context,\n            @NonNull AttributeSet attrs) {\n        return mHost.mFragmentManager.onCreateView(parent, name, context, attrs);\n    }\n\n    /**\n     * Marks the fragment state as unsaved. This allows for \"state loss\" detection.\n     */\n    public void noteStateNotSaved() {\n        mHost.mFragmentManager.noteStateNotSaved();\n    }\n\n    /**\n     * Saves the state for all Fragments.\n     *\n     * @see #restoreSaveState(Parcelable)\n     */\n    @Nullable\n    public Parcelable saveAllState() {\n        return mHost.mFragmentManager.saveAllState();\n    }\n\n    /**\n     * Restores the saved state for all Fragments. The given Fragment list are Fragment\n     * instances retained across configuration changes.\n     *\n     * @deprecated Have your {@link FragmentHostCallback} implement {@link ViewModelStoreOwner}\n     * to automatically restore the Fragment's non configuration state and use\n     * {@link #restoreSaveState(Parcelable)} to restore the Fragment's save state.\n     */\n    @Deprecated\n    public void restoreAllState(@Nullable Parcelable state,\n            @Nullable List<Fragment> nonConfigList) {\n        mHost.mFragmentManager.restoreAllState(state,\n                new FragmentManagerNonConfig(nonConfigList, null, null));\n    }\n\n    /**\n     * Restores the saved state for all Fragments. The given FragmentManagerNonConfig are Fragment\n     * instances retained across configuration changes, including nested fragments\n     *\n     * @deprecated Have your {@link FragmentHostCallback} implement {@link ViewModelStoreOwner}\n     * to automatically restore the Fragment's non configuration state and use\n     * {@link #restoreSaveState(Parcelable)} to restore the Fragment's save state.\n     */\n    @Deprecated\n    public void restoreAllState(@Nullable Parcelable state,\n            @Nullable FragmentManagerNonConfig nonConfig) {\n        mHost.mFragmentManager.restoreAllState(state, nonConfig);\n    }\n\n    /**\n     * Restores the saved state for all Fragments.\n     *\n     * @param state the saved state containing the Parcelable returned by {@link #saveAllState()}\n     * @see #saveAllState()\n     */\n    public void restoreSaveState(@Nullable Parcelable state) {\n        if (!(mHost instanceof ViewModelStoreOwner)) {\n            throw new IllegalStateException(\"Your FragmentHostCallback must implement \"\n                    + \"ViewModelStoreOwner to call restoreSaveState(). Call restoreAllState() \"\n                    + \" if you're still using retainNestedNonConfig().\");\n        }\n        mHost.mFragmentManager.restoreSaveState(state);\n    }\n\n    /**\n     * Returns a list of Fragments that have opted to retain their instance across\n     * configuration changes.\n     *\n     * @deprecated Have your {@link FragmentHostCallback} implement {@link ViewModelStoreOwner}\n     * to automatically retain the Fragment's non configuration state.\n     */\n    @Deprecated\n    @Nullable\n    public List<Fragment> retainNonConfig() {\n        FragmentManagerNonConfig nonconf = mHost.mFragmentManager.retainNonConfig();\n        return nonconf != null && nonconf.getFragments() != null\n                ? new ArrayList<>(nonconf.getFragments())\n                : null;\n    }\n\n    /**\n     * Returns a nested tree of Fragments that have opted to retain their instance across\n     * configuration changes.\n     *\n     * @deprecated Have your {@link FragmentHostCallback} implement {@link ViewModelStoreOwner}\n     * to automatically retain the Fragment's non configuration state.\n     */\n    @Deprecated\n    @Nullable\n    public FragmentManagerNonConfig retainNestedNonConfig() {\n        return mHost.mFragmentManager.retainNonConfig();\n    }\n\n    /**\n     * Moves all Fragments managed by the controller's FragmentManager\n     * into the create state.\n     * <p>Call when Fragments should be created.\n     *\n     * @see Fragment#onCreate(Bundle)\n     */\n    public void dispatchCreate() {\n        mHost.mFragmentManager.dispatchCreate();\n    }\n\n    /**\n     * Moves all Fragments managed by the controller's FragmentManager\n     * into the activity created state.\n     * <p>Call when Fragments should be informed their host has been created.\n     *\n     * @see Fragment#onActivityCreated(Bundle)\n     */\n    public void dispatchActivityCreated() {\n        mHost.mFragmentManager.dispatchActivityCreated();\n    }\n\n    /**\n     * Moves all Fragments managed by the controller's FragmentManager\n     * into the start state.\n     * <p>Call when Fragments should be started.\n     *\n     * @see Fragment#onStart()\n     */\n    public void dispatchStart() {\n        mHost.mFragmentManager.dispatchStart();\n    }\n\n    /**\n     * Moves all Fragments managed by the controller's FragmentManager\n     * into the resume state.\n     * <p>Call when Fragments should be resumed.\n     *\n     * @see Fragment#onResume()\n     */\n    public void dispatchResume() {\n        mHost.mFragmentManager.dispatchResume();\n    }\n\n    /**\n     * Moves all Fragments managed by the controller's FragmentManager\n     * into the pause state.\n     * <p>Call when Fragments should be paused.\n     *\n     * @see Fragment#onPause()\n     */\n    public void dispatchPause() {\n        mHost.mFragmentManager.dispatchPause();\n    }\n\n    /**\n     * Moves all Fragments managed by the controller's FragmentManager\n     * into the stop state.\n     * <p>Call when Fragments should be stopped.\n     *\n     * @see Fragment#onStop()\n     */\n    public void dispatchStop() {\n        mHost.mFragmentManager.dispatchStop();\n    }\n\n    /**\n     * @deprecated This functionality has been rolled into {@link #dispatchStop()}.\n     */\n    @Deprecated\n    public void dispatchReallyStop() {\n    }\n\n    /**\n     * Moves all Fragments managed by the controller's FragmentManager\n     * into the destroy view state.\n     * <p>Call when the Fragment's views should be destroyed.\n     *\n     * @see Fragment#onDestroyView()\n     */\n    public void dispatchDestroyView() {\n        mHost.mFragmentManager.dispatchDestroyView();\n    }\n\n    /**\n     * Moves Fragments managed by the controller's FragmentManager\n     * into the destroy state.\n     * <p>\n     * If the {@link androidx.fragment.app.FragmentHostCallback} is an instance of {@link ViewModelStoreOwner},\n     * then retained Fragments and any other non configuration state such as any\n     * {@link androidx.lifecycle.ViewModel} attached to Fragments will only be destroyed if\n     * {@link androidx.lifecycle.ViewModelStore#clear()} is called prior to this method.\n     * <p>\n     * Otherwise, the FragmentManager will look to see if the\n     * {@link FragmentHostCallback host's} Context is an {@link Activity}\n     * and if {@link Activity#isChangingConfigurations()} returns true. In only that case\n     * will non configuration state be retained.\n     * <p>Call when Fragments should be destroyed.\n     *\n     * @see Fragment#onDestroy()\n     */\n    public void dispatchDestroy() {\n        mHost.mFragmentManager.dispatchDestroy();\n    }\n\n    /**\n     * Lets all Fragments managed by the controller's FragmentManager know the multi-window mode of\n     * the activity changed.\n     * <p>Call when the multi-window mode of the activity changed.\n     *\n     * @see Fragment#onMultiWindowModeChanged\n     */\n    public void dispatchMultiWindowModeChanged(boolean isInMultiWindowMode) {\n        mHost.mFragmentManager.dispatchMultiWindowModeChanged(isInMultiWindowMode);\n    }\n\n    /**\n     * Lets all Fragments managed by the controller's FragmentManager know the picture-in-picture\n     * mode of the activity changed.\n     * <p>Call when the picture-in-picture mode of the activity changed.\n     *\n     * @see Fragment#onPictureInPictureModeChanged\n     */\n    public void dispatchPictureInPictureModeChanged(boolean isInPictureInPictureMode) {\n        mHost.mFragmentManager.dispatchPictureInPictureModeChanged(isInPictureInPictureMode);\n    }\n\n    /**\n     * Lets all Fragments managed by the controller's FragmentManager\n     * know a configuration change occurred.\n     * <p>Call when there is a configuration change.\n     *\n     * @see Fragment#onConfigurationChanged(Configuration)\n     */\n    public void dispatchConfigurationChanged(@NonNull Configuration newConfig) {\n        mHost.mFragmentManager.dispatchConfigurationChanged(newConfig);\n    }\n\n    /**\n     * Lets all Fragments managed by the controller's FragmentManager\n     * know the device is in a low memory condition.\n     * <p>Call when the device is low on memory and Fragment's should trim\n     * their memory usage.\n     *\n     * @see Fragment#onLowMemory()\n     */\n    public void dispatchLowMemory() {\n        mHost.mFragmentManager.dispatchLowMemory();\n    }\n\n    /**\n     * Lets all Fragments managed by the controller's FragmentManager\n     * know they should create an options menu.\n     * <p>Call when the Fragment should create an options menu.\n     *\n     * @return {@code true} if the options menu contains items to display\n     * @see Fragment#onCreateOptionsMenu(Menu, MenuInflater)\n     */\n    public boolean dispatchCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {\n        return mHost.mFragmentManager.dispatchCreateOptionsMenu(menu, inflater);\n    }\n\n    /**\n     * Lets all Fragments managed by the controller's FragmentManager\n     * know they should prepare their options menu for display.\n     * <p>Call immediately before displaying the Fragment's options menu.\n     *\n     * @return {@code true} if the options menu contains items to display\n     * @see Fragment#onPrepareOptionsMenu(Menu)\n     */\n    public boolean dispatchPrepareOptionsMenu(@NonNull Menu menu) {\n        return mHost.mFragmentManager.dispatchPrepareOptionsMenu(menu);\n    }\n\n    /**\n     * Sends an option item selection event to the Fragments managed by the\n     * controller's FragmentManager. Once the event has been consumed,\n     * no additional handling will be performed.\n     * <p>Call immediately after an options menu item has been selected\n     *\n     * @return {@code true} if the options menu selection event was consumed\n     * @see Fragment#onOptionsItemSelected(MenuItem)\n     */\n    public boolean dispatchOptionsItemSelected(@NonNull MenuItem item) {\n        return mHost.mFragmentManager.dispatchOptionsItemSelected(item);\n    }\n\n    /**\n     * Sends a context item selection event to the Fragments managed by the\n     * controller's FragmentManager. Once the event has been consumed,\n     * no additional handling will be performed.\n     * <p>Call immediately after an options menu item has been selected\n     *\n     * @return {@code true} if the context menu selection event was consumed\n     * @see Fragment#onContextItemSelected(MenuItem)\n     */\n    public boolean dispatchContextItemSelected(@NonNull MenuItem item) {\n        return mHost.mFragmentManager.dispatchContextItemSelected(item);\n    }\n\n    /**\n     * Lets all Fragments managed by the controller's FragmentManager\n     * know their options menu has closed.\n     * <p>Call immediately after closing the Fragment's options menu.\n     *\n     * @see Fragment#onOptionsMenuClosed(Menu)\n     */\n    public void dispatchOptionsMenuClosed(@NonNull Menu menu) {\n        mHost.mFragmentManager.dispatchOptionsMenuClosed(menu);\n    }\n\n    /**\n     * Execute any pending actions for the Fragments managed by the\n     * controller's FragmentManager.\n     * <p>Call when queued actions can be performed [eg when the\n     * Fragment moves into a start or resume state].\n     * @return {@code true} if queued actions were performed\n     */\n    public boolean execPendingActions() {\n        return mHost.mFragmentManager.execPendingActions();\n    }\n\n    /**\n     * Starts the loaders.\n     *\n     * @deprecated Loaders are managed separately from FragmentController\n     */\n    @Deprecated\n    public void doLoaderStart() {\n    }\n\n    /**\n     * Stops the loaders, optionally retaining their state. This is useful for keeping the\n     * loader state across configuration changes.\n     *\n     * @param retain When {@code true}, the loaders aren't stopped, but, their instances\n     * are retained in a started state\n     *\n     * @deprecated Loaders are managed separately from FragmentController\n     */\n    @Deprecated\n    public void doLoaderStop(boolean retain) {\n    }\n\n    /**\n     * Retains the state of each of the loaders.\n     *\n     * @deprecated Loaders are managed separately from FragmentController\n     */\n    @Deprecated\n    public void doLoaderRetain() {\n    }\n\n    /**\n     * Destroys the loaders and, if their state is not being retained, removes them.\n     *\n     * @deprecated Loaders are managed separately from FragmentController\n     */\n    @Deprecated\n    public void doLoaderDestroy() {\n    }\n\n    /**\n     * Lets the loaders know the host is ready to receive notifications.\n     *\n     * @deprecated Loaders are managed separately from FragmentController\n     */\n    @Deprecated\n    public void reportLoaderStart() {\n    }\n\n    /**\n     * Returns a list of LoaderManagers that have opted to retain their instance across\n     * configuration changes.\n     *\n     * @deprecated Loaders are managed separately from FragmentController\n     */\n    @Deprecated\n    @Nullable\n    public SimpleArrayMap<String, LoaderManager> retainLoaderNonConfig() {\n        return null;\n    }\n\n    /**\n     * Restores the saved state for all LoaderManagers. The given LoaderManager list are\n     * LoaderManager instances retained across configuration changes.\n     *\n     * @see #retainLoaderNonConfig()\n     *\n     * @deprecated Loaders are managed separately from FragmentController\n     */\n    @Deprecated\n    public void restoreLoaderNonConfig(@SuppressLint(\"UnknownNullness\")\n            SimpleArrayMap<String, LoaderManager> loaderManagers) {\n    }\n\n    /**\n     * Dumps the current state of the loaders.\n     *\n     * @deprecated Loaders are managed separately from FragmentController\n     */\n    @Deprecated\n    public void dumpLoaders(@NonNull String prefix, @Nullable FileDescriptor fd,\n            @NonNull PrintWriter writer, @Nullable String[] args) {\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentFactory.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport androidx.annotation.NonNull;\nimport androidx.collection.SimpleArrayMap;\n\nimport java.lang.reflect.InvocationTargetException;\n\n/**\n * Interface used to control the instantiation of {@link Fragment} instances.\n * Implementations can be registered with a {@link FragmentManager} via\n * {@link FragmentManager#setFragmentFactory(FragmentFactory)}.\n *\n * @see FragmentManager#setFragmentFactory(FragmentFactory)\n */\npublic class FragmentFactory {\n    private static final SimpleArrayMap<String, Class<?>> sClassMap = new SimpleArrayMap<>();\n\n    /**\n     * Determine if the given fragment name is a support library fragment class.\n     *\n     * @param classLoader The default classloader to use for loading the Class\n     * @param className Class name of the fragment to load\n     * @return Returns the parsed Class\n     */\n    @NonNull\n    private static Class<?> loadClass(@NonNull ClassLoader classLoader,\n            @NonNull String className) throws ClassNotFoundException {\n        Class<?> clazz = sClassMap.get(className);\n        if (clazz == null) {\n            // Class not found in the cache, see if it's real, and try to add it\n            clazz = Class.forName(className, false, classLoader);\n            sClassMap.put(className, clazz);\n        }\n        return clazz;\n    }\n\n    /**\n     * Determine if the given fragment name is a valid Fragment class.\n     *\n     * @param classLoader The default classloader to use for loading the Class\n     * @param className Class name of the fragment to test\n     * @return true if <code>className</code> is <code>androidx.fragment.app.Fragment</code>\n     *         or a subclass, false otherwise.\n     */\n    static boolean isFragmentClass(@NonNull ClassLoader classLoader,\n            @NonNull String className) {\n        try {\n            Class<?> clazz = loadClass(classLoader, className);\n            return Fragment.class.isAssignableFrom(clazz);\n        } catch (ClassNotFoundException e) {\n            return false;\n        }\n    }\n\n    /**\n     * Parse a Fragment Class from the given class name. The resulting Class is kept in a global\n     * cache, bypassing the {@link Class#forName(String)} calls when passed the same\n     * class name again.\n     *\n     * @param classLoader The default classloader to use for loading the Class\n     * @param className The class name of the fragment to parse.\n     * @return Returns the parsed Fragment Class\n     * @throws Fragment.InstantiationException If there is a failure in parsing\n     * the given fragment class.  This is a runtime exception; it is not\n     * normally expected to happen.\n     */\n    @SuppressWarnings(\"unchecked\")\n    @NonNull\n    public static Class<? extends Fragment> loadFragmentClass(@NonNull ClassLoader classLoader,\n            @NonNull String className) {\n        try {\n            Class<?> clazz = loadClass(classLoader, className);\n            return (Class<? extends Fragment>) clazz;\n        } catch (ClassNotFoundException e) {\n            throw new Fragment.InstantiationException(\"Unable to instantiate fragment \" + className\n                    + \": make sure class name exists\", e);\n        } catch (ClassCastException e) {\n            throw new Fragment.InstantiationException(\"Unable to instantiate fragment \" + className\n                    + \": make sure class is a valid subclass of Fragment\", e);\n        }\n    }\n\n    /**\n     * Create a new instance of a Fragment with the given class name. This uses\n     * {@link #loadFragmentClass(ClassLoader, String)} and the empty\n     * constructor of the resulting Class by default.\n     *\n     * @param classLoader The default classloader to use for instantiation\n     * @param className The class name of the fragment to instantiate.\n     * @return Returns a new fragment instance.\n     * @throws Fragment.InstantiationException If there is a failure in instantiating\n     * the given fragment class.  This is a runtime exception; it is not\n     * normally expected to happen.\n     */\n    @NonNull\n    public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {\n        try {\n            Class<? extends Fragment> cls = loadFragmentClass(classLoader, className);\n            return cls.getConstructor().newInstance();\n        } catch (java.lang.InstantiationException e) {\n            throw new Fragment.InstantiationException(\"Unable to instantiate fragment \" + className\n                    + \": make sure class name exists, is public, and has an\"\n                    + \" empty constructor that is public\", e);\n        } catch (IllegalAccessException e) {\n            throw new Fragment.InstantiationException(\"Unable to instantiate fragment \" + className\n                    + \": make sure class name exists, is public, and has an\"\n                    + \" empty constructor that is public\", e);\n        } catch (NoSuchMethodException e) {\n            throw new Fragment.InstantiationException(\"Unable to instantiate fragment \" + className\n                    + \": could not find Fragment constructor\", e);\n        } catch (InvocationTargetException e) {\n            throw new Fragment.InstantiationException(\"Unable to instantiate fragment \" + className\n                    + \": calling Fragment constructor caused an exception\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentHostCallback.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentSender;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.view.LayoutInflater;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.core.app.ActivityCompat;\nimport androidx.core.util.Preconditions;\n\nimport java.io.FileDescriptor;\nimport java.io.PrintWriter;\n\n/**\n * Integration points with the Fragment host.\n * <p>\n * Fragments may be hosted by any object; such as an {@link Activity}. In order to\n * host fragments, implement {@link FragmentHostCallback}, overriding the methods\n * applicable to the host.\n */\npublic abstract class FragmentHostCallback<E> extends FragmentContainer {\n    @Nullable private final Activity mActivity;\n    @NonNull private final Context mContext;\n    @NonNull private final Handler mHandler;\n    private final int mWindowAnimations;\n    final FragmentManagerImpl mFragmentManager = new FragmentManagerImpl();\n\n    public FragmentHostCallback(@NonNull Context context, @NonNull Handler handler,\n            int windowAnimations) {\n        this(context instanceof Activity ? (Activity) context : null, context, handler,\n                windowAnimations);\n    }\n\n    FragmentHostCallback(@NonNull FragmentActivity activity) {\n        this(activity, activity /*context*/, new Handler(), 0 /*windowAnimations*/);\n    }\n\n    FragmentHostCallback(@Nullable Activity activity, @NonNull Context context,\n            @NonNull Handler handler, int windowAnimations) {\n        mActivity = activity;\n        mContext = Preconditions.checkNotNull(context, \"context == null\");\n        mHandler = Preconditions.checkNotNull(handler, \"handler == null\");\n        mWindowAnimations = windowAnimations;\n    }\n\n    /**\n     * Print internal state into the given stream.\n     *\n     * @param prefix Desired prefix to prepend at each line of output.\n     * @param fd The raw file descriptor that the dump is being sent to.\n     * @param writer The PrintWriter to which you should dump your state. This will be closed\n     *                  for you after you return.\n     * @param args additional arguments to the dump request.\n     */\n    public void onDump(@NonNull String prefix, @Nullable FileDescriptor fd,\n            @NonNull PrintWriter writer, @Nullable String[] args) {\n    }\n\n    /**\n     * Return {@code true} if the fragment's state needs to be saved.\n     */\n    public boolean onShouldSaveFragmentState(@NonNull Fragment fragment) {\n        return true;\n    }\n\n    /**\n     * Return a {@link LayoutInflater}.\n     * See {@link Activity#getLayoutInflater()}.\n     */\n    @NonNull\n    public LayoutInflater onGetLayoutInflater() {\n        return LayoutInflater.from(mContext);\n    }\n\n    /**\n     * Return the object that's currently hosting the fragment. If a {@link Fragment}\n     * is hosted by a {@link FragmentActivity}, the object returned here should be\n     * the same object returned from {@link Fragment#getActivity()}.\n     */\n    @Nullable\n    public abstract E onGetHost();\n\n    /**\n     * Invalidates the activity's options menu.\n     * See {@link FragmentActivity#supportInvalidateOptionsMenu()}\n     */\n    public void onSupportInvalidateOptionsMenu() {\n    }\n\n    /**\n     * Starts a new {@link Activity} from the given fragment.\n     * See {@link FragmentActivity#startActivityForResult(Intent, int)}.\n     */\n    public void onStartActivityFromFragment(@NonNull Fragment fragment,\n            @SuppressLint(\"UnknownNullness\") Intent intent, int requestCode) {\n        onStartActivityFromFragment(fragment, intent, requestCode, null);\n    }\n\n    /**\n     * Starts a new {@link Activity} from the given fragment.\n     * See {@link FragmentActivity#startActivityForResult(Intent, int, Bundle)}.\n     */\n    public void onStartActivityFromFragment(\n            @NonNull Fragment fragment, @SuppressLint(\"UnknownNullness\") Intent intent,\n            int requestCode, @Nullable Bundle options) {\n        if (requestCode != -1) {\n            throw new IllegalStateException(\n                    \"Starting activity with a requestCode requires a FragmentActivity host\");\n        }\n        mContext.startActivity(intent);\n    }\n\n    /**\n     * Starts a new {@link IntentSender} from the given fragment.\n     * See {@link Activity#startIntentSender(IntentSender, Intent, int, int, int, Bundle)}.\n     */\n    public void onStartIntentSenderFromFragment(@NonNull Fragment fragment,\n            @SuppressLint(\"UnknownNullness\") IntentSender intent, int requestCode,\n            @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,\n            @Nullable Bundle options) throws IntentSender.SendIntentException {\n        if (requestCode != -1) {\n            throw new IllegalStateException(\n                    \"Starting intent sender with a requestCode requires a FragmentActivity host\");\n        }\n        ActivityCompat.startIntentSenderForResult(mActivity, intent, requestCode, fillInIntent,\n                flagsMask, flagsValues, extraFlags, options);\n    }\n\n    /**\n     * Requests permissions from the given fragment.\n     * See {@link FragmentActivity#requestPermissions(String[], int)}\n     */\n    public void onRequestPermissionsFromFragment(@NonNull Fragment fragment,\n            @NonNull String[] permissions, int requestCode) {\n    }\n\n    /**\n     * Checks whether to show permission rationale UI from a fragment.\n     * See {@link FragmentActivity#shouldShowRequestPermissionRationale(String)}\n     */\n    public boolean onShouldShowRequestPermissionRationale(@NonNull String permission) {\n        return false;\n    }\n\n    /**\n     * Return {@code true} if there are window animations.\n     */\n    public boolean onHasWindowAnimations() {\n        return true;\n    }\n\n    /**\n     * Return the window animations.\n     */\n    public int onGetWindowAnimations() {\n        return mWindowAnimations;\n    }\n\n    @Nullable\n    @Override\n    public View onFindViewById(int id) {\n        return null;\n    }\n\n    @Override\n    public boolean onHasView() {\n        return true;\n    }\n\n    @Nullable\n    Activity getActivity() {\n        return mActivity;\n    }\n\n    @NonNull\n    Context getContext() {\n        return mContext;\n    }\n\n    @NonNull\n    Handler getHandler() {\n        return mHandler;\n    }\n\n    void onAttachFragment(@NonNull Fragment fragment) {\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManager.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.IdRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RestrictTo;\nimport androidx.annotation.StringRes;\n\nimport java.io.FileDescriptor;\nimport java.io.PrintWriter;\nimport java.util.List;\n\n/**\n * Static library support version of the framework's {@link android.app.FragmentManager}.\n * Used to write apps that run on platforms prior to Android 3.0.  When running\n * on Android 3.0 or above, this implementation is still used; it does not try\n * to switch to the framework's implementation.  See the framework {@link FragmentManager}\n * documentation for a class overview.\n *\n * <p>Your activity must derive from {@link FragmentActivity} to use this. From such an activity,\n * you can acquire the {@link FragmentManager} by calling\n * {@link FragmentActivity#getSupportFragmentManager}.\n */\npublic abstract class FragmentManager {\n    static final FragmentFactory DEFAULT_FACTORY = new FragmentFactory();\n\n    /**\n     * Representation of an entry on the fragment back stack, as created\n     * with {@link FragmentTransaction#addToBackStack(String)\n     * FragmentTransaction.addToBackStack()}.  Entries can later be\n     * retrieved with {@link FragmentManager#getBackStackEntryAt(int)\n     * FragmentManager.getBackStackEntryAt()}.\n     *\n     * <p>Note that you should never hold on to a BackStackEntry object;\n     * the identifier as returned by {@link #getId} is the only thing that\n     * will be persisted across activity instances.\n     */\n    public interface BackStackEntry {\n        /**\n         * Return the unique identifier for the entry.  This is the only\n         * representation of the entry that will persist across activity\n         * instances.\n         */\n        public int getId();\n\n        /**\n         * Get the name that was supplied to\n         * {@link FragmentTransaction#addToBackStack(String)\n         * FragmentTransaction.addToBackStack(String)} when creating this entry.\n         */\n        @Nullable\n        public String getName();\n\n        /**\n         * Return the full bread crumb title resource identifier for the entry,\n         * or 0 if it does not have one.\n         */\n        @StringRes\n        public int getBreadCrumbTitleRes();\n\n        /**\n         * Return the short bread crumb title resource identifier for the entry,\n         * or 0 if it does not have one.\n         */\n        @StringRes\n        public int getBreadCrumbShortTitleRes();\n\n        /**\n         * Return the full bread crumb title for the entry, or null if it\n         * does not have one.\n         */\n        @Nullable\n        public CharSequence getBreadCrumbTitle();\n\n        /**\n         * Return the short bread crumb title for the entry, or null if it\n         * does not have one.\n         */\n        @Nullable\n        public CharSequence getBreadCrumbShortTitle();\n    }\n\n    /**\n     * Interface to watch for changes to the back stack.\n     */\n    public interface OnBackStackChangedListener {\n        /**\n         * Called whenever the contents of the back stack change.\n         */\n        public void onBackStackChanged();\n    }\n\n    private FragmentFactory mFragmentFactory = null;\n\n    /**\n     * Start a series of edit operations on the Fragments associated with\n     * this FragmentManager.\n     *\n     * <p>Note: A fragment transaction can only be created/committed prior\n     * to an activity saving its state.  If you try to commit a transaction\n     * after {@link FragmentActivity#onSaveInstanceState FragmentActivity.onSaveInstanceState()}\n     * (and prior to a following {@link FragmentActivity#onStart FragmentActivity.onStart}\n     * or {@link FragmentActivity#onResume FragmentActivity.onResume()}, you will get an error.\n     * This is because the framework takes care of saving your current fragments\n     * in the state, and if changes are made after the state is saved then they\n     * will be lost.</p>\n     */\n    @NonNull\n    public abstract FragmentTransaction beginTransaction();\n\n    /**\n     * @hide -- remove once prebuilts are in.\n     * @deprecated\n     */\n    @RestrictTo(LIBRARY_GROUP_PREFIX)\n    @Deprecated\n    @NonNull\n    public FragmentTransaction openTransaction() {\n        return beginTransaction();\n    }\n\n    /**\n     * After a {@link FragmentTransaction} is committed with\n     * {@link FragmentTransaction#commit FragmentTransaction.commit()}, it\n     * is scheduled to be executed asynchronously on the process's main thread.\n     * If you want to immediately executing any such pending operations, you\n     * can call this function (only from the main thread) to do so.  Note that\n     * all callbacks and other related behavior will be done from within this\n     * call, so be careful about where this is called from.\n     *\n     * <p>If you are committing a single transaction that does not modify the\n     * fragment back stack, strongly consider using\n     * {@link FragmentTransaction#commitNow()} instead. This can help avoid\n     * unwanted side effects when other code in your app has pending committed\n     * transactions that expect different timing.</p>\n     * <p>\n     * This also forces the start of any postponed Transactions where\n     * {@link Fragment#postponeEnterTransition()} has been called.\n     *\n     * @return Returns true if there were any pending transactions to be\n     * executed.\n     */\n    public abstract boolean executePendingTransactions();\n\n    /**\n     * Finds a fragment that was identified by the given id either when inflated\n     * from XML or as the container ID when added in a transaction.  This first\n     * searches through fragments that are currently added to the manager's\n     * activity; if no such fragment is found, then all fragments currently\n     * on the back stack associated with this ID are searched.\n     * @return The fragment if found or null otherwise.\n     */\n    @Nullable\n    public abstract Fragment findFragmentById(@IdRes int id);\n\n    /**\n     * Finds a fragment that was identified by the given tag either when inflated\n     * from XML or as supplied when added in a transaction.  This first\n     * searches through fragments that are currently added to the manager's\n     * activity; if no such fragment is found, then all fragments currently\n     * on the back stack are searched.\n     * @return The fragment if found or null otherwise.\n     */\n    @Nullable\n    public abstract Fragment findFragmentByTag(@Nullable String tag);\n\n    /**\n     * Flag for {@link #popBackStack(String, int)}\n     * and {@link #popBackStack(int, int)}: If set, and the name or ID of\n     * a back stack entry has been supplied, then all matching entries will\n     * be consumed until one that doesn't match is found or the bottom of\n     * the stack is reached.  Otherwise, all entries up to but not including that entry\n     * will be removed.\n     */\n    public static final int POP_BACK_STACK_INCLUSIVE = 1<<0;\n\n    /**\n     * Pop the top state off the back stack. This function is asynchronous -- it enqueues the\n     * request to pop, but the action will not be performed until the application\n     * returns to its event loop.\n     */\n    public abstract void popBackStack();\n\n    /**\n     * Like {@link #popBackStack()}, but performs the operation immediately\n     * inside of the call.  This is like calling {@link #executePendingTransactions()}\n     * afterwards without forcing the start of postponed Transactions.\n     * @return Returns true if there was something popped, else false.\n     */\n    public abstract boolean popBackStackImmediate();\n\n    /**\n     * Pop the last fragment transition from the manager's fragment\n     * back stack.\n     * This function is asynchronous -- it enqueues the\n     * request to pop, but the action will not be performed until the application\n     * returns to its event loop.\n     *\n     * @param name If non-null, this is the name of a previous back state\n     * to look for; if found, all states up to that state will be popped.  The\n     * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether\n     * the named state itself is popped. If null, only the top state is popped.\n     * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}.\n     */\n    public abstract void popBackStack(@Nullable String name, int flags);\n\n    /**\n     * Like {@link #popBackStack(String, int)}, but performs the operation immediately\n     * inside of the call.  This is like calling {@link #executePendingTransactions()}\n     * afterwards without forcing the start of postponed Transactions.\n     * @return Returns true if there was something popped, else false.\n     */\n    public abstract boolean popBackStackImmediate(@Nullable String name, int flags);\n\n    /**\n     * Pop all back stack states up to the one with the given identifier.\n     * This function is asynchronous -- it enqueues the\n     * request to pop, but the action will not be performed until the application\n     * returns to its event loop.\n     *\n     * @param id Identifier of the stated to be popped. If no identifier exists,\n     * false is returned.\n     * The identifier is the number returned by\n     * {@link FragmentTransaction#commit() FragmentTransaction.commit()}.  The\n     * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether\n     * the named state itself is popped.\n     * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}.\n     */\n    public abstract void popBackStack(int id, int flags);\n\n    /**\n     * Like {@link #popBackStack(int, int)}, but performs the operation immediately\n     * inside of the call.  This is like calling {@link #executePendingTransactions()}\n     * afterwards without forcing the start of postponed Transactions.\n     * @return Returns true if there was something popped, else false.\n     */\n    public abstract boolean popBackStackImmediate(int id, int flags);\n\n    /**\n     * Return the number of entries currently in the back stack.\n     */\n    public abstract int getBackStackEntryCount();\n\n    /**\n     * Return the BackStackEntry at index <var>index</var> in the back stack;\n     * entries start index 0 being the bottom of the stack.\n     */\n    @NonNull\n    public abstract BackStackEntry getBackStackEntryAt(int index);\n\n    /**\n     * Add a new listener for changes to the fragment back stack.\n     */\n    public abstract void addOnBackStackChangedListener(\n            @NonNull OnBackStackChangedListener listener);\n\n    /**\n     * Remove a listener that was previously added with\n     * {@link #addOnBackStackChangedListener(OnBackStackChangedListener)}.\n     */\n    public abstract void removeOnBackStackChangedListener(\n            @NonNull OnBackStackChangedListener listener);\n\n    /**\n     * Put a reference to a fragment in a Bundle.  This Bundle can be\n     * persisted as saved state, and when later restoring\n     * {@link #getFragment(Bundle, String)} will return the current\n     * instance of the same fragment.\n     *\n     * @param bundle The bundle in which to put the fragment reference.\n     * @param key The name of the entry in the bundle.\n     * @param fragment The Fragment whose reference is to be stored.\n     */\n    public abstract void putFragment(@NonNull Bundle bundle, @NonNull String key,\n            @NonNull Fragment fragment);\n\n    /**\n     * Retrieve the current Fragment instance for a reference previously\n     * placed with {@link #putFragment(Bundle, String, Fragment)}.\n     *\n     * @param bundle The bundle from which to retrieve the fragment reference.\n     * @param key The name of the entry in the bundle.\n     * @return Returns the current Fragment instance that is associated with\n     * the given reference.\n     */\n    @Nullable\n    public abstract Fragment getFragment(@NonNull Bundle bundle, @NonNull String key);\n\n    /**\n     * Get a list of all fragments that are currently added to the FragmentManager.\n     * This may include those that are hidden as well as those that are shown.\n     * This will not include any fragments only in the back stack, or fragments that\n     * are detached or removed.\n     * <p>\n     * The order of the fragments in the list is the order in which they were\n     * added or attached.\n     *\n     * @return A list of all fragments that are added to the FragmentManager.\n     */\n    @NonNull\n    public abstract List<Fragment> getFragments();\n\n    /**\n     * Save the current instance state of the given Fragment.  This can be\n     * used later when creating a new instance of the Fragment and adding\n     * it to the fragment manager, to have it create itself to match the\n     * current state returned here.  Note that there are limits on how\n     * this can be used:\n     *\n     * <ul>\n     * <li>The Fragment must currently be attached to the FragmentManager.\n     * <li>A new Fragment created using this saved state must be the same class\n     * type as the Fragment it was created from.\n     * <li>The saved state can not contain dependencies on other fragments --\n     * that is it can't use {@link #putFragment(Bundle, String, Fragment)} to\n     * store a fragment reference because that reference may not be valid when\n     * this saved state is later used.  Likewise the Fragment's target and\n     * result code are not included in this state.\n     * </ul>\n     *\n     * @param f The Fragment whose state is to be saved.\n     * @return The generated state.  This will be null if there was no\n     * interesting state created by the fragment.\n     */\n    @Nullable\n    public abstract Fragment.SavedState saveFragmentInstanceState(@NonNull Fragment f);\n\n    /**\n     * Returns true if the final {@link android.app.Activity#onDestroy() Activity.onDestroy()}\n     * call has been made on the FragmentManager's Activity, so this instance is now dead.\n     */\n    public abstract boolean isDestroyed();\n\n    /**\n     * Registers a {@link FragmentLifecycleCallbacks} to listen to fragment lifecycle events\n     * happening in this FragmentManager. All registered callbacks will be automatically\n     * unregistered when this FragmentManager is destroyed.\n     *\n     * @param cb Callbacks to register\n     * @param recursive true to automatically register this callback for all child FragmentManagers\n     */\n    public abstract void registerFragmentLifecycleCallbacks(@NonNull FragmentLifecycleCallbacks cb,\n            boolean recursive);\n\n    /**\n     * Unregisters a previously registered {@link FragmentLifecycleCallbacks}. If the callback\n     * was not previously registered this call has no effect. All registered callbacks will be\n     * automatically unregistered when this FragmentManager is destroyed.\n     *\n     * @param cb Callbacks to unregister\n     */\n    public abstract void unregisterFragmentLifecycleCallbacks(\n            @NonNull FragmentLifecycleCallbacks cb);\n\n    /**\n     * Return the currently active primary navigation fragment for this FragmentManager.\n     * The primary navigation fragment is set by fragment transactions using\n     * {@link FragmentTransaction#setPrimaryNavigationFragment(Fragment)}.\n     *\n     * <p>The primary navigation fragment's\n     * {@link Fragment#getChildFragmentManager() child FragmentManager} will be called first\n     * to process delegated navigation actions such as {@link #popBackStack()} if no ID\n     * or transaction name is provided to pop to.</p>\n     *\n     * @return the fragment designated as the primary navigation fragment\n     */\n    @Nullable\n    public abstract Fragment getPrimaryNavigationFragment();\n\n    /**\n     * Set a {@link FragmentFactory} for this FragmentManager that will be used\n     * to create new Fragment instances from this point onward.\n     *\n     * @param fragmentFactory the factory to use to create new Fragment instances\n     */\n    public void setFragmentFactory(@NonNull FragmentFactory fragmentFactory) {\n        mFragmentFactory = fragmentFactory;\n    }\n\n    /**\n     * Gets the current {@link FragmentFactory} used to instantiate new Fragment instances.\n     *\n     * @return the current FragmentFactory\n     */\n    @NonNull\n    public FragmentFactory getFragmentFactory() {\n        if (mFragmentFactory == null) {\n            mFragmentFactory = DEFAULT_FACTORY;\n        }\n        return mFragmentFactory;\n    }\n\n    /**\n     * Print the FragmentManager's state into the given stream.\n     *\n     * @param prefix Text to print at the front of each line.\n     * @param fd The raw file descriptor that the dump is being sent to.\n     * @param writer A PrintWriter to which the dump is to be set.\n     * @param args Additional arguments to the dump request.\n     */\n    public abstract void dump(@NonNull String prefix, @Nullable FileDescriptor fd,\n            @NonNull PrintWriter writer, @Nullable String[] args);\n\n    /**\n     * Control whether the framework's internal fragment manager debugging\n     * logs are turned on.  If enabled, you will see output in logcat as\n     * the framework performs fragment operations.\n     */\n    public static void enableDebugLogging(boolean enabled) {\n        FragmentManagerImpl.DEBUG = enabled;\n    }\n\n    /**\n     * Returns {@code true} if the FragmentManager's state has already been saved\n     * by its host. Any operations that would change saved state should not be performed\n     * if this method returns true. For example, any popBackStack() method, such as\n     * {@link #popBackStackImmediate()} or any FragmentTransaction using\n     * {@link FragmentTransaction#commit()} instead of\n     * {@link FragmentTransaction#commitAllowingStateLoss()} will change\n     * the state and will result in an error.\n     *\n     * @return true if this FragmentManager's state has already been saved by its host\n     */\n    public abstract boolean isStateSaved();\n\n    /**\n     * Callback interface for listening to fragment state changes that happen\n     * within a given FragmentManager.\n     */\n    public abstract static class FragmentLifecycleCallbacks {\n        /**\n         * Called right before the fragment's {@link Fragment#onAttach(Context)} method is called.\n         * This is a good time to inject any required dependencies or perform other configuration\n         * for the fragment before any of the fragment's lifecycle methods are invoked.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment changing state\n         * @param context Context that the Fragment is being attached to\n         */\n        public void onFragmentPreAttached(@NonNull FragmentManager fm, @NonNull Fragment f,\n                @NonNull Context context) {}\n\n        /**\n         * Called after the fragment has been attached to its host. Its host will have had\n         * <code>onAttachFragment</code> called before this call happens.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment changing state\n         * @param context Context that the Fragment was attached to\n         */\n        public void onFragmentAttached(@NonNull FragmentManager fm, @NonNull Fragment f,\n                @NonNull Context context) {}\n\n        /**\n         * Called right before the fragment's {@link Fragment#onCreate(Bundle)} method is called.\n         * This is a good time to inject any required dependencies or perform other configuration\n         * for the fragment.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment changing state\n         * @param savedInstanceState Saved instance bundle from a previous instance\n         */\n        public void onFragmentPreCreated(@NonNull FragmentManager fm, @NonNull Fragment f,\n                @Nullable Bundle savedInstanceState) {}\n\n        /**\n         * Called after the fragment has returned from the FragmentManager's call to\n         * {@link Fragment#onCreate(Bundle)}. This will only happen once for any given\n         * fragment instance, though the fragment may be attached and detached multiple times.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment changing state\n         * @param savedInstanceState Saved instance bundle from a previous instance\n         */\n        public void onFragmentCreated(@NonNull FragmentManager fm, @NonNull Fragment f,\n                @Nullable Bundle savedInstanceState) {}\n\n        /**\n         * Called after the fragment has returned from the FragmentManager's call to\n         * {@link Fragment#onActivityCreated(Bundle)}. This will only happen once for any given\n         * fragment instance, though the fragment may be attached and detached multiple times.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment changing state\n         * @param savedInstanceState Saved instance bundle from a previous instance\n         */\n        public void onFragmentActivityCreated(@NonNull FragmentManager fm, @NonNull Fragment f,\n                @Nullable Bundle savedInstanceState) {}\n\n        /**\n         * Called after the fragment has returned a non-null view from the FragmentManager's\n         * request to {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment that created and owns the view\n         * @param v View returned by the fragment\n         * @param savedInstanceState Saved instance bundle from a previous instance\n         */\n        public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull Fragment f,\n                @NonNull View v, @Nullable Bundle savedInstanceState) {}\n\n        /**\n         * Called after the fragment has returned from the FragmentManager's call to\n         * {@link Fragment#onStart()}.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment changing state\n         */\n        public void onFragmentStarted(@NonNull FragmentManager fm, @NonNull Fragment f) {}\n\n        /**\n         * Called after the fragment has returned from the FragmentManager's call to\n         * {@link Fragment#onResume()}.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment changing state\n         */\n        public void onFragmentResumed(@NonNull FragmentManager fm, @NonNull Fragment f) {}\n\n        /**\n         * Called after the fragment has returned from the FragmentManager's call to\n         * {@link Fragment#onPause()}.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment changing state\n         */\n        public void onFragmentPaused(@NonNull FragmentManager fm, @NonNull Fragment f) {}\n\n        /**\n         * Called after the fragment has returned from the FragmentManager's call to\n         * {@link Fragment#onStop()}.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment changing state\n         */\n        public void onFragmentStopped(@NonNull FragmentManager fm, @NonNull Fragment f) {}\n\n        /**\n         * Called after the fragment has returned from the FragmentManager's call to\n         * {@link Fragment#onSaveInstanceState(Bundle)}.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment changing state\n         * @param outState Saved state bundle for the fragment\n         */\n        public void onFragmentSaveInstanceState(@NonNull FragmentManager fm, @NonNull Fragment f,\n                @NonNull Bundle outState) {}\n\n        /**\n         * Called after the fragment has returned from the FragmentManager's call to\n         * {@link Fragment#onDestroyView()}.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment changing state\n         */\n        public void onFragmentViewDestroyed(@NonNull FragmentManager fm, @NonNull Fragment f) {}\n\n        /**\n         * Called after the fragment has returned from the FragmentManager's call to\n         * {@link Fragment#onDestroy()}.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment changing state\n         */\n        public void onFragmentDestroyed(@NonNull FragmentManager fm, @NonNull Fragment f) {}\n\n        /**\n         * Called after the fragment has returned from the FragmentManager's call to\n         * {@link Fragment#onDetach()}.\n         *\n         * @param fm Host FragmentManager\n         * @param f Fragment changing state\n         */\n        public void onFragmentDetached(@NonNull FragmentManager fm, @NonNull Fragment f) {}\n    }\n}\n\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerImpl.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorInflater;\nimport android.animation.AnimatorListenerAdapter;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.res.Configuration;\nimport android.content.res.Resources;\nimport android.content.res.TypedArray;\nimport android.os.Bundle;\nimport android.os.Looper;\nimport android.os.Parcelable;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.util.SparseArray;\nimport android.view.LayoutInflater;\nimport android.view.Menu;\nimport android.view.MenuInflater;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.animation.AlphaAnimation;\nimport android.view.animation.Animation;\nimport android.view.animation.AnimationSet;\nimport android.view.animation.AnimationUtils;\nimport android.view.animation.DecelerateInterpolator;\nimport android.view.animation.Interpolator;\nimport android.view.animation.ScaleAnimation;\nimport android.view.animation.Transformation;\n\nimport androidx.activity.OnBackPressedCallback;\nimport androidx.activity.OnBackPressedDispatcher;\nimport androidx.activity.OnBackPressedDispatcherOwner;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.collection.ArraySet;\nimport androidx.core.util.DebugUtils;\nimport androidx.core.util.LogWriter;\nimport androidx.core.view.OneShotPreDrawListener;\nimport androidx.lifecycle.Lifecycle;\nimport androidx.lifecycle.LifecycleOwner;\nimport androidx.lifecycle.ViewModelStore;\nimport androidx.lifecycle.ViewModelStoreOwner;\n\nimport java.io.FileDescriptor;\nimport java.io.PrintWriter;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\n/**\n * Container for fragments associated with an activity.\n */\nfinal class FragmentManagerImpl extends FragmentManager implements LayoutInflater.Factory2 {\n    static boolean DEBUG = false;\n    static final String TAG = \"FragmentManager\";\n\n    static final String TARGET_REQUEST_CODE_STATE_TAG = \"android:target_req_state\";\n    static final String TARGET_STATE_TAG = \"android:target_state\";\n    static final String VIEW_STATE_TAG = \"android:view_state\";\n    static final String USER_VISIBLE_HINT_TAG = \"android:user_visible_hint\";\n\n    private static final class FragmentLifecycleCallbacksHolder {\n        final FragmentLifecycleCallbacks mCallback;\n        final boolean mRecursive;\n\n        FragmentLifecycleCallbacksHolder(FragmentLifecycleCallbacks callback, boolean recursive) {\n            mCallback = callback;\n            mRecursive = recursive;\n        }\n    }\n\n    ArrayList<OpGenerator> mPendingActions;\n    boolean mExecutingActions;\n\n    int mNextFragmentIndex = 0;\n\n    final ArrayList<Fragment> mAdded = new ArrayList<>();\n    final HashMap<String, Fragment> mActive = new HashMap<>();\n    ArrayList<BackStackRecord> mBackStack;\n    ArrayList<Fragment> mCreatedMenus;\n    private OnBackPressedDispatcher mOnBackPressedDispatcher;\n    private final OnBackPressedCallback mOnBackPressedCallback =\n            new OnBackPressedCallback(false) {\n        @Override\n        public void handleOnBackPressed() {\n            FragmentManagerImpl.this.handleOnBackPressed();\n        }\n    };\n\n    // Must be accessed while locked.\n    ArrayList<BackStackRecord> mBackStackIndices;\n    ArrayList<Integer> mAvailBackStackIndices;\n\n    ArrayList<OnBackStackChangedListener> mBackStackChangeListeners;\n    private final CopyOnWriteArrayList<FragmentLifecycleCallbacksHolder>\n            mLifecycleCallbacks = new CopyOnWriteArrayList<>();\n\n    int mCurState = Fragment.INITIALIZING;\n    FragmentHostCallback mHost;\n    FragmentContainer mContainer;\n    Fragment mParent;\n    @Nullable\n    Fragment mPrimaryNav;\n\n    boolean mNeedMenuInvalidate;\n    boolean mStateSaved;\n    boolean mStopped;\n    boolean mDestroyed;\n    boolean mHavePendingDeferredStart;\n\n    // Temporary vars for removing redundant operations in BackStackRecords:\n    ArrayList<BackStackRecord> mTmpRecords;\n    ArrayList<Boolean> mTmpIsPop;\n    ArrayList<Fragment> mTmpAddedFragments;\n\n    // Temporary vars for state save and restore.\n    Bundle mStateBundle = null;\n    SparseArray<Parcelable> mStateArray = null;\n\n    // Postponed transactions.\n    ArrayList<StartEnterTransitionListener> mPostponedTransactions;\n\n    private FragmentManagerViewModel mNonConfig;\n\n    Runnable mExecCommit = new Runnable() {\n        @Override\n        public void run() {\n            execPendingActions();\n        }\n    };\n\n    private void throwException(RuntimeException ex) {\n        Log.e(TAG, ex.getMessage());\n        Log.e(TAG, \"Activity state:\");\n        LogWriter logw = new LogWriter(TAG);\n        PrintWriter pw = new PrintWriter(logw);\n        if (mHost != null) {\n            try {\n                mHost.onDump(\"  \", null, pw, new String[] { });\n            } catch (Exception e) {\n                Log.e(TAG, \"Failed dumping state\", e);\n            }\n        } else {\n            try {\n                dump(\"  \", null, pw, new String[] { });\n            } catch (Exception e) {\n                Log.e(TAG, \"Failed dumping state\", e);\n            }\n        }\n        throw ex;\n    }\n\n    @NonNull\n    @Override\n    public FragmentTransaction beginTransaction() {\n        return new BackStackRecord(this);\n    }\n\n    @Override\n    public boolean executePendingTransactions() {\n        boolean updates = execPendingActions();\n        forcePostponedTransactions();\n        return updates;\n    }\n\n    private void updateOnBackPressedCallbackEnabled() {\n        // Always enable the callback if we have pending actions\n        // as we don't know if they'll change the back stack entry count.\n        // See handleOnBackPressed() for more explanation\n        if (mPendingActions != null && !mPendingActions.isEmpty()) {\n            mOnBackPressedCallback.setEnabled(true);\n            return;\n        }\n        // This FragmentManager needs to have a back stack for this to be enabled\n        // And the parent fragment, if it exists, needs to be the primary navigation\n        // fragment.\n        mOnBackPressedCallback.setEnabled(getBackStackEntryCount() > 0\n                && isPrimaryNavigation(mParent));\n    }\n\n    /**\n     * Recursively check up the FragmentManager hierarchy of primary\n     * navigation Fragments to ensure that all of the parent Fragments are the\n     * primary navigation Fragment for their associated FragmentManager\n     */\n    boolean isPrimaryNavigation(@Nullable Fragment parent) {\n        // If the parent is null, then we're at the root host\n        // and we're always the primary navigation\n        if (parent == null) {\n            return true;\n        }\n        FragmentManagerImpl parentFragmentManager = parent.mFragmentManager;\n        Fragment primaryNavigationFragment = parentFragmentManager\n                .getPrimaryNavigationFragment();\n        // The parent Fragment needs to be the primary navigation Fragment\n        // and, if it has a parent itself, that parent also needs to be\n        // the primary navigation fragment, recursively up the stack\n        return parent == primaryNavigationFragment\n                && isPrimaryNavigation(parentFragmentManager.mParent);\n    }\n\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    void handleOnBackPressed() {\n        // First, execute any pending actions to make sure we're in an\n        // up to date view of the world just in case anyone is queuing\n        // up transactions that change the back stack then immediately\n        // calling onBackPressed()\n        execPendingActions();\n        if (mOnBackPressedCallback.isEnabled()) {\n            // We still have a back stack, so we can pop\n            popBackStackImmediate();\n        } else {\n            // Sigh. Due to FragmentManager's asynchronicity, we can\n            // get into cases where we *think* we can handle the back\n            // button but because of frame perfect dispatch, we fell\n            // on our face. Since our callback is disabled, we can\n            // re-trigger the onBackPressed() to dispatch to the next\n            // enabled callback\n            mOnBackPressedDispatcher.onBackPressed();\n        }\n    }\n\n    @Override\n    public void popBackStack() {\n        enqueueAction(new PopBackStackState(null, -1, 0), false);\n    }\n\n    @Override\n    public boolean popBackStackImmediate() {\n        checkStateLoss();\n        return popBackStackImmediate(null, -1, 0);\n    }\n\n    @Override\n    public void popBackStack(@Nullable final String name, final int flags) {\n        enqueueAction(new PopBackStackState(name, -1, flags), false);\n    }\n\n    @Override\n    public boolean popBackStackImmediate(@Nullable String name, int flags) {\n        checkStateLoss();\n        return popBackStackImmediate(name, -1, flags);\n    }\n\n    @Override\n    public void popBackStack(final int id, final int flags) {\n        if (id < 0) {\n            throw new IllegalArgumentException(\"Bad id: \" + id);\n        }\n        enqueueAction(new PopBackStackState(null, id, flags), false);\n    }\n\n    @Override\n    public boolean popBackStackImmediate(int id, int flags) {\n        checkStateLoss();\n        execPendingActions();\n        if (id < 0) {\n            throw new IllegalArgumentException(\"Bad id: \" + id);\n        }\n        return popBackStackImmediate(null, id, flags);\n    }\n\n    /**\n     * Used by all public popBackStackImmediate methods, this executes pending transactions and\n     * returns true if the pop action did anything, regardless of what other pending\n     * transactions did.\n     *\n     * @return true if the pop operation did anything or false otherwise.\n     */\n    private boolean popBackStackImmediate(String name, int id, int flags) {\n        execPendingActions();\n        ensureExecReady(true);\n\n        if (mPrimaryNav != null // We have a primary nav fragment\n                && id < 0 // No valid id (since they're local)\n                && name == null) { // no name to pop to (since they're local)\n            final FragmentManager childManager = mPrimaryNav.getChildFragmentManager();\n            if (childManager.popBackStackImmediate()) {\n                // We did something, just not to this specific FragmentManager. Return true.\n                return true;\n            }\n        }\n\n        boolean executePop = popBackStackState(mTmpRecords, mTmpIsPop, name, id, flags);\n        if (executePop) {\n            mExecutingActions = true;\n            try {\n                removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);\n            } finally {\n                cleanupExec();\n            }\n        }\n\n        updateOnBackPressedCallbackEnabled();\n        doPendingDeferredStart();\n        burpActive();\n        return executePop;\n    }\n\n    @Override\n    public int getBackStackEntryCount() {\n        return mBackStack != null ? mBackStack.size() : 0;\n    }\n\n    @Override\n    public BackStackEntry getBackStackEntryAt(int index) {\n        return mBackStack.get(index);\n    }\n\n    @Override\n    public void addOnBackStackChangedListener(OnBackStackChangedListener listener) {\n        if (mBackStackChangeListeners == null) {\n            mBackStackChangeListeners = new ArrayList<OnBackStackChangedListener>();\n        }\n        mBackStackChangeListeners.add(listener);\n    }\n\n    @Override\n    public void removeOnBackStackChangedListener(OnBackStackChangedListener listener) {\n        if (mBackStackChangeListeners != null) {\n            mBackStackChangeListeners.remove(listener);\n        }\n    }\n\n    @Override\n    public void putFragment(Bundle bundle, String key, Fragment fragment) {\n        if (fragment.mFragmentManager != this) {\n            throwException(new IllegalStateException(\"Fragment \" + fragment\n                    + \" is not currently in the FragmentManager\"));\n        }\n        bundle.putString(key, fragment.mWho);\n    }\n\n    @Override\n    @Nullable\n    public Fragment getFragment(Bundle bundle, String key) {\n        String who = bundle.getString(key);\n        if (who == null) {\n            return null;\n        }\n        Fragment f = mActive.get(who);\n        if (f == null) {\n            throwException(new IllegalStateException(\"Fragment no longer exists for key \"\n                    + key + \": unique id \" + who));\n        }\n        return f;\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public List<Fragment> getFragments() {\n        if (mAdded.isEmpty()) {\n            return Collections.emptyList();\n        }\n        synchronized (mAdded) {\n            return (List<Fragment>) mAdded.clone();\n        }\n    }\n\n    @NonNull\n    ViewModelStore getViewModelStore(@NonNull Fragment f) {\n        return mNonConfig.getViewModelStore(f);\n    }\n\n    @NonNull\n    FragmentManagerViewModel getChildNonConfig(@NonNull Fragment f) {\n        return mNonConfig.getChildNonConfig(f);\n    }\n\n    void addRetainedFragment(@NonNull Fragment f) {\n        if (isStateSaved()) {\n            if (FragmentManagerImpl.DEBUG) {\n                Log.v(TAG, \"Ignoring addRetainedFragment as the state is already saved\");\n            }\n            return;\n        }\n        boolean added = mNonConfig.addRetainedFragment(f);\n        if (added && FragmentManagerImpl.DEBUG) {\n            Log.v(TAG, \"Updating retained Fragments: Added \" + f);\n        }\n    }\n\n    void removeRetainedFragment(@NonNull Fragment f) {\n        if (isStateSaved()) {\n            if (FragmentManagerImpl.DEBUG) {\n                Log.v(TAG, \"Ignoring removeRetainedFragment as the state is already saved\");\n            }\n            return;\n        }\n        boolean removed = mNonConfig.removeRetainedFragment(f);\n        if (removed && FragmentManagerImpl.DEBUG) {\n            Log.v(TAG, \"Updating retained Fragments: Removed \" + f);\n        }\n    }\n\n    /**\n     * This is used by FragmentController to get the Active fragments.\n     *\n     * @return A list of active fragments in the fragment manager, including those that are in the\n     * back stack.\n     */\n    @NonNull\n    List<Fragment> getActiveFragments() {\n        return new ArrayList<>(mActive.values());\n    }\n\n    /**\n     * Used by FragmentController to get the number of Active Fragments.\n     *\n     * @return The number of active fragments.\n     */\n    int getActiveFragmentCount() {\n        return mActive.size();\n    }\n\n    @Override\n    @Nullable\n    public Fragment.SavedState saveFragmentInstanceState(@NonNull Fragment fragment) {\n        if (fragment.mFragmentManager != this) {\n            throwException( new IllegalStateException(\"Fragment \" + fragment\n                    + \" is not currently in the FragmentManager\"));\n        }\n        if (fragment.mState > Fragment.INITIALIZING) {\n            Bundle result = saveFragmentBasicState(fragment);\n            return result != null ? new Fragment.SavedState(result) : null;\n        }\n        return null;\n    }\n\n    @Override\n    public boolean isDestroyed() {\n        return mDestroyed;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder(128);\n        sb.append(\"FragmentManager{\");\n        sb.append(Integer.toHexString(System.identityHashCode(this)));\n        sb.append(\" in \");\n        if (mParent != null) {\n            DebugUtils.buildShortClassTag(mParent, sb);\n        } else {\n            DebugUtils.buildShortClassTag(mHost, sb);\n        }\n        sb.append(\"}}\");\n        return sb.toString();\n    }\n\n    @Override\n    public void dump(@NonNull String prefix, @Nullable FileDescriptor fd,\n                     @NonNull PrintWriter writer, @Nullable String[] args) {\n        String innerPrefix = prefix + \"    \";\n\n        if (!mActive.isEmpty()) {\n            writer.print(prefix); writer.print(\"Active Fragments in \");\n            writer.print(Integer.toHexString(System.identityHashCode(this)));\n            writer.println(\":\");\n            for (Fragment f : mActive.values()) {\n                writer.print(prefix); writer.println(f);\n                if (f != null) {\n                    f.dump(innerPrefix, fd, writer, args);\n                }\n            }\n        }\n\n        int N = mAdded.size();\n        if (N > 0) {\n            writer.print(prefix); writer.println(\"Added Fragments:\");\n            for (int i = 0; i < N; i++) {\n                Fragment f = mAdded.get(i);\n                writer.print(prefix);\n                writer.print(\"  #\");\n                writer.print(i);\n                writer.print(\": \");\n                writer.println(f.toString());\n            }\n        }\n\n        if (mCreatedMenus != null) {\n            N = mCreatedMenus.size();\n            if (N > 0) {\n                writer.print(prefix); writer.println(\"Fragments Created Menus:\");\n                for (int i=0; i<N; i++) {\n                    Fragment f = mCreatedMenus.get(i);\n                    writer.print(prefix); writer.print(\"  #\"); writer.print(i);\n                    writer.print(\": \"); writer.println(f.toString());\n                }\n            }\n        }\n\n        if (mBackStack != null) {\n            N = mBackStack.size();\n            if (N > 0) {\n                writer.print(prefix); writer.println(\"Back Stack:\");\n                for (int i=0; i<N; i++) {\n                    BackStackRecord bs = mBackStack.get(i);\n                    writer.print(prefix); writer.print(\"  #\"); writer.print(i);\n                    writer.print(\": \"); writer.println(bs.toString());\n                    bs.dump(innerPrefix, writer);\n                }\n            }\n        }\n\n        synchronized (this) {\n            if (mBackStackIndices != null) {\n                N = mBackStackIndices.size();\n                if (N > 0) {\n                    writer.print(prefix); writer.println(\"Back Stack Indices:\");\n                    for (int i=0; i<N; i++) {\n                        BackStackRecord bs = mBackStackIndices.get(i);\n                        writer.print(prefix); writer.print(\"  #\"); writer.print(i);\n                        writer.print(\": \"); writer.println(bs);\n                    }\n                }\n            }\n\n            if (mAvailBackStackIndices != null && mAvailBackStackIndices.size() > 0) {\n                writer.print(prefix); writer.print(\"mAvailBackStackIndices: \");\n                writer.println(Arrays.toString(mAvailBackStackIndices.toArray()));\n            }\n        }\n\n        if (mPendingActions != null) {\n            N = mPendingActions.size();\n            if (N > 0) {\n                writer.print(prefix); writer.println(\"Pending Actions:\");\n                for (int i=0; i<N; i++) {\n                    OpGenerator r = mPendingActions.get(i);\n                    writer.print(prefix); writer.print(\"  #\"); writer.print(i);\n                    writer.print(\": \"); writer.println(r);\n                }\n            }\n        }\n\n        writer.print(prefix); writer.println(\"FragmentManager misc state:\");\n        writer.print(prefix); writer.print(\"  mHost=\"); writer.println(mHost);\n        writer.print(prefix); writer.print(\"  mContainer=\"); writer.println(mContainer);\n        if (mParent != null) {\n            writer.print(prefix); writer.print(\"  mParent=\"); writer.println(mParent);\n        }\n        writer.print(prefix); writer.print(\"  mCurState=\"); writer.print(mCurState);\n        writer.print(\" mStateSaved=\"); writer.print(mStateSaved);\n        writer.print(\" mStopped=\"); writer.print(mStopped);\n        writer.print(\" mDestroyed=\"); writer.println(mDestroyed);\n        if (mNeedMenuInvalidate) {\n            writer.print(prefix); writer.print(\"  mNeedMenuInvalidate=\");\n            writer.println(mNeedMenuInvalidate);\n        }\n    }\n\n    static final Interpolator DECELERATE_QUINT = new DecelerateInterpolator(2.5f);\n    static final Interpolator DECELERATE_CUBIC = new DecelerateInterpolator(1.5f);\n\n    static final int ANIM_DUR = 220;\n\n    static AnimationOrAnimator makeOpenCloseAnimation(float startScale,\n                                                      float endScale, float startAlpha, float endAlpha) {\n        AnimationSet set = new AnimationSet(false);\n        ScaleAnimation scale = new ScaleAnimation(startScale, endScale, startScale, endScale,\n                Animation.RELATIVE_TO_SELF, .5f, Animation.RELATIVE_TO_SELF, .5f);\n        scale.setInterpolator(DECELERATE_QUINT);\n        scale.setDuration(ANIM_DUR);\n        set.addAnimation(scale);\n        AlphaAnimation alpha = new AlphaAnimation(startAlpha, endAlpha);\n        alpha.setInterpolator(DECELERATE_CUBIC);\n        alpha.setDuration(ANIM_DUR);\n        set.addAnimation(alpha);\n        return new AnimationOrAnimator(set);\n    }\n\n    static AnimationOrAnimator makeFadeAnimation(float start, float end) {\n        AlphaAnimation anim = new AlphaAnimation(start, end);\n        anim.setInterpolator(DECELERATE_CUBIC);\n        anim.setDuration(ANIM_DUR);\n        return new AnimationOrAnimator(anim);\n    }\n\n    AnimationOrAnimator loadAnimation(Fragment fragment, int transit, boolean enter,\n                                      int transitionStyle) {\n        int nextAnim = fragment.getNextAnim();\n        // Clear the Fragment animation\n        fragment.setNextAnim(0);\n        // If there is a transition on the container, clear those set on the fragment\n        if (fragment.mContainer != null && fragment.mContainer.getLayoutTransition() != null) {\n            return null;\n        }\n        Animation animation = fragment.onCreateAnimation(transit, enter, nextAnim);\n        if (animation != null) {\n            return new AnimationOrAnimator(animation);\n        }\n\n        Animator animator = fragment.onCreateAnimator(transit, enter, nextAnim);\n        if (animator != null) {\n            return new AnimationOrAnimator(animator);\n        }\n\n        if (nextAnim != 0) {\n            String dir = mHost.getContext().getResources().getResourceTypeName(nextAnim);\n            boolean isAnim = \"anim\".equals(dir);\n            boolean successfulLoad = false;\n            if (isAnim) {\n                // try AnimationUtils first\n                try {\n                    animation = AnimationUtils.loadAnimation(mHost.getContext(), nextAnim);\n                    if (animation != null) {\n                        return new AnimationOrAnimator(animation);\n                    }\n                    // A null animation may be returned and that is acceptable\n                    successfulLoad = true; // succeeded in loading animation, but it is null\n                } catch (Resources.NotFoundException e) {\n                    throw e; // Rethrow it -- the resource should be found if it is provided.\n                } catch (RuntimeException e) {\n                    // Other exceptions can occur when loading an Animator from AnimationUtils.\n                }\n            }\n            if (!successfulLoad) {\n                // try Animator\n                try {\n                    animator = AnimatorInflater.loadAnimator(mHost.getContext(), nextAnim);\n                    if (animator != null) {\n                        return new AnimationOrAnimator(animator);\n                    }\n                } catch (RuntimeException e) {\n                    if (isAnim) {\n                        // Rethrow it -- we already tried AnimationUtils and it failed.\n                        throw e;\n                    }\n                    // Otherwise, it is probably an animation resource\n                    animation = AnimationUtils.loadAnimation(mHost.getContext(), nextAnim);\n                    if (animation != null) {\n                        return new AnimationOrAnimator(animation);\n                    }\n                }\n            }\n        }\n\n        if (transit == 0) {\n            return null;\n        }\n\n        int styleIndex = transitToStyleIndex(transit, enter);\n        if (styleIndex < 0) {\n            return null;\n        }\n\n        switch (styleIndex) {\n            case ANIM_STYLE_OPEN_ENTER:\n                return makeOpenCloseAnimation(1.125f, 1.0f, 0, 1);\n            case ANIM_STYLE_OPEN_EXIT:\n                return makeOpenCloseAnimation(1.0f, .975f, 1, 0);\n            case ANIM_STYLE_CLOSE_ENTER:\n                return makeOpenCloseAnimation(.975f, 1.0f, 0, 1);\n            case ANIM_STYLE_CLOSE_EXIT:\n                return makeOpenCloseAnimation(1.0f, 1.075f, 1, 0);\n            case ANIM_STYLE_FADE_ENTER:\n                return makeFadeAnimation(0, 1);\n            case ANIM_STYLE_FADE_EXIT:\n                return makeFadeAnimation(1, 0);\n        }\n\n        // TODO: remove or fix transitionStyle -- it apparently never worked.\n        if (transitionStyle == 0 && mHost.onHasWindowAnimations()) {\n            transitionStyle = mHost.onGetWindowAnimations();\n        }\n        if (transitionStyle == 0) {\n            return null;\n        }\n\n        //TypedArray attrs = mActivity.obtainStyledAttributes(transitionStyle,\n        //        com.android.internal.R.styleable.FragmentAnimation);\n        //int anim = attrs.getResourceId(styleIndex, 0);\n        //attrs.recycle();\n\n        //if (anim == 0) {\n        //    return null;\n        //}\n\n        //return AnimatorInflater.loadAnimator(mActivity, anim);\n        return null;\n    }\n\n    public void performPendingDeferredStart(Fragment f) {\n        if (f.mDeferStart) {\n            if (mExecutingActions) {\n                // Wait until we're done executing our pending transactions\n                mHavePendingDeferredStart = true;\n                return;\n            }\n            f.mDeferStart = false;\n            moveToState(f, mCurState, 0, 0, false);\n        }\n    }\n\n    boolean isStateAtLeast(int state) {\n        return mCurState >= state;\n    }\n\n    @SuppressWarnings(\"ReferenceEquality\")\n    void moveToState(Fragment f, int newState, int transit, int transitionStyle,\n                     boolean keepActive) {\n        // Fragments that are not currently added will sit in the onCreate() state.\n        if ((!f.mAdded || f.mDetached) && newState > Fragment.CREATED) {\n            newState = Fragment.CREATED;\n        }\n        if (f.mRemoving && newState > f.mState) {\n            if (f.mState == Fragment.INITIALIZING && f.isInBackStack()) {\n                // Allow the fragment to be created so that it can be saved later.\n                newState = Fragment.CREATED;\n            } else {\n                // While removing a fragment, we can't change it to a higher state.\n                newState = f.mState;\n            }\n        }\n        // Defer start if requested; don't allow it to move to STARTED or higher\n        // if it's not already started.\n        if (f.mDeferStart && f.mState < Fragment.STARTED && newState > Fragment.ACTIVITY_CREATED) {\n            newState = Fragment.ACTIVITY_CREATED;\n        }\n        // Don't allow the Fragment to go above its max lifecycle state\n        // Ensure that Fragments are capped at CREATED instead of ACTIVITY_CREATED.\n        if (f.mMaxState == Lifecycle.State.CREATED) {\n            newState = Math.min(newState, Fragment.CREATED);\n        } else {\n            newState = Math.min(newState, f.mMaxState.ordinal());\n        }\n        if (f.mState <= newState) {\n            // For fragments that are created from a layout, when restoring from\n            // state we don't want to allow them to be created until they are\n            // being reloaded from the layout.\n            if (f.mFromLayout && !f.mInLayout) {\n                return;\n            }\n            if (f.getAnimatingAway() != null || f.getAnimator() != null) {\n                // The fragment is currently being animated...  but!  Now we\n                // want to move our state back up.  Give up on waiting for the\n                // animation, move to whatever the final state should be once\n                // the animation is done, and then we can proceed from there.\n                f.setAnimatingAway(null);\n                f.setAnimator(null);\n                moveToState(f, f.getStateAfterAnimating(), 0, 0, true);\n            }\n            switch (f.mState) {\n                case Fragment.INITIALIZING:\n                    if (newState > Fragment.INITIALIZING) {\n                        if (DEBUG) Log.v(TAG, \"moveto CREATED: \" + f);\n                        if (f.mSavedFragmentState != null) {\n                            f.mSavedFragmentState.setClassLoader(mHost.getContext()\n                                    .getClassLoader());\n                            f.mSavedViewState = f.mSavedFragmentState.getSparseParcelableArray(\n                                    FragmentManagerImpl.VIEW_STATE_TAG);\n                            Fragment target = getFragment(f.mSavedFragmentState,\n                                    FragmentManagerImpl.TARGET_STATE_TAG);\n                            f.mTargetWho = target != null ? target.mWho : null;\n                            if (f.mTargetWho != null) {\n                                f.mTargetRequestCode = f.mSavedFragmentState.getInt(\n                                        FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, 0);\n                            }\n                            if (f.mSavedUserVisibleHint != null) {\n                                f.mUserVisibleHint = f.mSavedUserVisibleHint;\n                                f.mSavedUserVisibleHint = null;\n                            } else {\n                                f.mUserVisibleHint = f.mSavedFragmentState.getBoolean(\n                                        FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true);\n                            }\n                            if (!f.mUserVisibleHint) {\n                                f.mDeferStart = true;\n                                if (newState > Fragment.ACTIVITY_CREATED) {\n                                    newState = Fragment.ACTIVITY_CREATED;\n                                }\n                            }\n                        }\n\n                        f.mHost = mHost;\n                        f.mParentFragment = mParent;\n                        f.mFragmentManager = mParent != null\n                                ? mParent.mChildFragmentManager : mHost.mFragmentManager;\n\n                        // If we have a target fragment, push it along to at least CREATED\n                        // so that this one can rely on it as an initialized dependency.\n                        if (f.mTarget != null) {\n                            if (mActive.get(f.mTarget.mWho) != f.mTarget) {\n                                throw new IllegalStateException(\"Fragment \" + f\n                                        + \" declared target fragment \" + f.mTarget\n                                        + \" that does not belong to this FragmentManager!\");\n                            }\n                            if (f.mTarget.mState < Fragment.CREATED) {\n                                moveToState(f.mTarget, Fragment.CREATED, 0, 0, true);\n                            }\n                            f.mTargetWho = f.mTarget.mWho;\n                            f.mTarget = null;\n                        }\n                        if (f.mTargetWho != null) {\n                            Fragment target = mActive.get(f.mTargetWho);\n                            if (target == null) {\n                                throw new IllegalStateException(\"Fragment \" + f\n                                        + \" declared target fragment \" + f.mTargetWho\n                                        + \" that does not belong to this FragmentManager!\");\n                            }\n                            if (target.mState < Fragment.CREATED) {\n                                moveToState(target, Fragment.CREATED, 0, 0, true);\n                            }\n                        }\n\n                        dispatchOnFragmentPreAttached(f, mHost.getContext(), false);\n                        f.performAttach();\n                        if (f.mParentFragment == null) {\n                            mHost.onAttachFragment(f);\n                        } else {\n                            f.mParentFragment.onAttachFragment(f);\n                        }\n                        dispatchOnFragmentAttached(f, mHost.getContext(), false);\n\n                        if (!f.mIsCreated) {\n                            dispatchOnFragmentPreCreated(f, f.mSavedFragmentState, false);\n                            f.performCreate(f.mSavedFragmentState);\n                            dispatchOnFragmentCreated(f, f.mSavedFragmentState, false);\n                        } else {\n                            f.restoreChildFragmentState(f.mSavedFragmentState);\n                            f.mState = Fragment.CREATED;\n                        }\n                    }\n                    // fall through\n                case Fragment.CREATED:\n                    // We want to unconditionally run this anytime we do a moveToState that\n                    // moves the Fragment above INITIALIZING, including cases such as when\n                    // we move from CREATED => CREATED as part of the case fall through above.\n                    if (newState > Fragment.INITIALIZING) {\n                        ensureInflatedFragmentView(f);\n                    }\n\n                    if (newState > Fragment.CREATED) {\n                        if (DEBUG) Log.v(TAG, \"moveto ACTIVITY_CREATED: \" + f);\n                        if (!f.mFromLayout) {\n                            ViewGroup container = null;\n                            if (f.mContainerId != 0) {\n                                if (f.mContainerId == View.NO_ID) {\n                                    throwException(new IllegalArgumentException(\n                                            \"Cannot create fragment \"\n                                                    + f\n                                                    + \" for a container view with no id\"));\n                                }\n                                container = (ViewGroup) mContainer.onFindViewById(f.mContainerId);\n                                if (container == null && !f.mRestored) {\n                                    String resName;\n                                    try {\n                                        resName = f.getResources().getResourceName(f.mContainerId);\n                                    } catch (Resources.NotFoundException e) {\n                                        resName = \"unknown\";\n                                    }\n                                    throwException(new IllegalArgumentException(\n                                            \"No view found for id 0x\"\n                                                    + Integer.toHexString(f.mContainerId) + \" (\"\n                                                    + resName\n                                                    + \") for fragment \" + f));\n                                }\n                            }\n                            f.mContainer = container;\n                            f.performCreateView(f.performGetLayoutInflater(\n                                    f.mSavedFragmentState), container, f.mSavedFragmentState);\n                            if (f.mView != null) {\n                                f.mInnerView = f.mView;\n                                f.mView.setSaveFromParentEnabled(false);\n                                if (container != null) {\n                                    container.addView(f.mView);\n                                }\n                                if (f.mHidden) {\n                                    f.mView.setVisibility(View.GONE);\n                                }\n                                f.onViewCreated(f.mView, f.mSavedFragmentState);\n                                dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState,\n                                        false);\n                                // Only animate the view if it is visible. This is done after\n                                // dispatchOnFragmentViewCreated in case visibility is changed\n                                f.mIsNewlyAdded = (f.mView.getVisibility() == View.VISIBLE)\n                                        && f.mContainer != null;\n                            } else {\n                                f.mInnerView = null;\n                            }\n                        }\n\n                        f.performActivityCreated(f.mSavedFragmentState);\n                        dispatchOnFragmentActivityCreated(f, f.mSavedFragmentState, false);\n                        if (f.mView != null) {\n                            f.restoreViewState(f.mSavedFragmentState);\n                        }\n                        f.mSavedFragmentState = null;\n                    }\n                    // fall through\n                case Fragment.ACTIVITY_CREATED:\n                    if (newState > Fragment.ACTIVITY_CREATED) {\n                        if (DEBUG) Log.v(TAG, \"moveto STARTED: \" + f);\n                        f.performStart();\n                        dispatchOnFragmentStarted(f, false);\n                    }\n                    // fall through\n                case Fragment.STARTED:\n                    if (newState > Fragment.STARTED) {\n                        if (DEBUG) Log.v(TAG, \"moveto RESUMED: \" + f);\n                        f.performResume();\n                        dispatchOnFragmentResumed(f, false);\n                        f.mSavedFragmentState = null;\n                        f.mSavedViewState = null;\n                    }\n            }\n        } else if (f.mState > newState) {\n            switch (f.mState) {\n                case Fragment.RESUMED:\n                    if (newState < Fragment.RESUMED) {\n                        if (DEBUG) Log.v(TAG, \"movefrom RESUMED: \" + f);\n                        f.performPause();\n                        dispatchOnFragmentPaused(f, false);\n                    }\n                    // fall through\n                case Fragment.STARTED:\n                    if (newState < Fragment.STARTED) {\n                        if (DEBUG) Log.v(TAG, \"movefrom STARTED: \" + f);\n                        f.performStop();\n                        dispatchOnFragmentStopped(f, false);\n                    }\n                    // fall through\n                case Fragment.ACTIVITY_CREATED:\n                    if (newState < Fragment.ACTIVITY_CREATED) {\n                        if (DEBUG) Log.v(TAG, \"movefrom ACTIVITY_CREATED: \" + f);\n                        if (f.mView != null) {\n                            // Need to save the current view state if not\n                            // done already.\n                            if (mHost.onShouldSaveFragmentState(f) && f.mSavedViewState == null) {\n                                saveFragmentViewState(f);\n                            }\n                        }\n                        f.performDestroyView();\n                        dispatchOnFragmentViewDestroyed(f, false);\n                        if (f.mView != null && f.mContainer != null) {\n                            // Stop any current animations:\n                            f.mContainer.endViewTransition(f.mView);\n                            f.mView.clearAnimation();\n                            AnimationOrAnimator anim = null;\n                            // If parent is being removed, no need to handle child animations.\n                            if (f.getParentFragment() == null || !f.getParentFragment().mRemoving) {\n                                if (mCurState > Fragment.INITIALIZING && !mDestroyed\n                                        && f.mView.getVisibility() == View.VISIBLE\n                                        && f.mPostponedAlpha >= 0) {\n                                    anim = loadAnimation(f, transit, false,\n                                            transitionStyle);\n                                }\n                                f.mPostponedAlpha = 0;\n                                if (anim != null) {\n                                    animateRemoveFragment(f, anim, newState);\n                                }\n                                f.mContainer.removeView(f.mView);\n                            }\n                        }\n                        f.mContainer = null;\n                        f.mView = null;\n                        // Set here to ensure that Observers are called after\n                        // the Fragment's view is set to null\n                        f.mViewLifecycleOwner = null;\n                        f.mViewLifecycleOwnerLiveData.setValue(null);\n                        f.mInnerView = null;\n                        f.mInLayout = false;\n                    }\n                    // fall through\n                case Fragment.CREATED:\n                    if (newState < Fragment.CREATED) {\n                        if (mDestroyed) {\n                            // The fragment's containing activity is\n                            // being destroyed, but this fragment is\n                            // currently animating away.  Stop the\n                            // animation right now -- it is not needed,\n                            // and we can't wait any more on destroying\n                            // the fragment.\n                            if (f.getAnimatingAway() != null) {\n                                View v = f.getAnimatingAway();\n                                f.setAnimatingAway(null);\n                                v.clearAnimation();\n                            } else if (f.getAnimator() != null) {\n                                Animator animator = f.getAnimator();\n                                f.setAnimator(null);\n                                animator.cancel();\n                            }\n                        }\n                        if (f.getAnimatingAway() != null || f.getAnimator() != null) {\n                            // We are waiting for the fragment's view to finish\n                            // animating away.  Just make a note of the state\n                            // the fragment now should move to once the animation\n                            // is done.\n                            f.setStateAfterAnimating(newState);\n                            newState = Fragment.CREATED;\n                        } else {\n                            if (DEBUG) Log.v(TAG, \"movefrom CREATED: \" + f);\n                            boolean beingRemoved = f.mRemoving && !f.isInBackStack();\n                            if (beingRemoved || mNonConfig.shouldDestroy(f)) {\n                                boolean shouldClear;\n                                if (mHost instanceof ViewModelStoreOwner) {\n                                    shouldClear = mNonConfig.isCleared();\n                                } else if (mHost.getContext() instanceof Activity) {\n                                    Activity activity = (Activity) mHost.getContext();\n                                    shouldClear = !activity.isChangingConfigurations();\n                                } else {\n                                    shouldClear = true;\n                                }\n                                if (beingRemoved || shouldClear) {\n                                    mNonConfig.clearNonConfigState(f);\n                                }\n                                f.performDestroy();\n                                dispatchOnFragmentDestroyed(f, false);\n                            } else {\n                                f.mState = Fragment.INITIALIZING;\n                            }\n\n                            f.performDetach();\n                            dispatchOnFragmentDetached(f, false);\n                            if (!keepActive) {\n                                if (beingRemoved || mNonConfig.shouldDestroy(f)) {\n                                    makeInactive(f);\n                                } else {\n                                    f.mHost = null;\n                                    f.mParentFragment = null;\n                                    f.mFragmentManager = null;\n                                    if (f.mTargetWho != null) {\n                                        Fragment target = mActive.get(f.mTargetWho);\n                                        if (target != null && target.getRetainInstance()) {\n                                            // Only keep references to other retained Fragments\n                                            // to avoid developers accessing Fragments that\n                                            // are never coming back\n                                            f.mTarget = target;\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n            }\n        }\n\n        if (f.mState != newState) {\n            Log.w(TAG, \"moveToState: Fragment state for \" + f + \" not updated inline; \"\n                    + \"expected state \" + newState + \" found \" + f.mState);\n            f.mState = newState;\n        }\n    }\n\n    /**\n     * Animates the removal of a fragment with the given animator or animation. After animating,\n     * the fragment's view will be removed from the hierarchy.\n     *\n     * @param fragment The fragment to animate out\n     * @param anim The animator or animation to run on the fragment's view\n     * @param newState The final state after animating.\n     */\n    private void animateRemoveFragment(@NonNull final Fragment fragment,\n                                       @NonNull AnimationOrAnimator anim, final int newState) {\n        final View viewToAnimate = fragment.mView;\n        final ViewGroup container = fragment.mContainer;\n        container.startViewTransition(viewToAnimate);\n        fragment.setStateAfterAnimating(newState);\n        if (anim.animation != null) {\n            Animation animation =\n                    new EndViewTransitionAnimation(anim.animation, container, viewToAnimate);\n            fragment.setAnimatingAway(fragment.mView);\n            animation.setAnimationListener(new Animation.AnimationListener() {\n                @Override\n                public void onAnimationStart(Animation animation) {\n                }\n\n                @Override\n                public void onAnimationEnd(Animation animation) {\n                    // onAnimationEnd() comes during draw(), so there can still be some\n                    // draw events happening after this call. We don't want to detach\n                    // the view until after the onAnimationEnd()\n                    container.post(new Runnable() {\n                        @Override\n                        public void run() {\n                            if (fragment.getAnimatingAway() != null) {\n                                fragment.setAnimatingAway(null);\n                                moveToState(fragment, fragment.getStateAfterAnimating(), 0, 0,\n                                        false);\n                            }\n                        }\n                    });\n                }\n\n                @Override\n                public void onAnimationRepeat(Animation animation) {\n                }\n            });\n            fragment.mView.startAnimation(animation);\n        } else {\n            Animator animator = anim.animator;\n            fragment.setAnimator(anim.animator);\n            animator.addListener(new AnimatorListenerAdapter() {\n                @Override\n                public void onAnimationEnd(Animator anim) {\n                    container.endViewTransition(viewToAnimate);\n                    // If an animator ends immediately, we can just pretend there is no animation.\n                    // When that happens the the fragment's view won't have been removed yet.\n                    Animator animator = fragment.getAnimator();\n                    fragment.setAnimator(null);\n                    if (animator != null && container.indexOfChild(viewToAnimate) < 0) {\n                        moveToState(fragment, fragment.getStateAfterAnimating(), 0, 0, false);\n                    }\n                }\n            });\n            animator.setTarget(fragment.mView);\n            animator.start();\n        }\n    }\n\n    void moveToState(Fragment f) {\n        moveToState(f, mCurState, 0, 0, false);\n    }\n\n    void ensureInflatedFragmentView(Fragment f) {\n        if (f.mFromLayout && !f.mPerformedCreateView) {\n            f.performCreateView(f.performGetLayoutInflater(\n                    f.mSavedFragmentState), null, f.mSavedFragmentState);\n            if (f.mView != null) {\n                f.mInnerView = f.mView;\n                f.mView.setSaveFromParentEnabled(false);\n                if (f.mHidden) f.mView.setVisibility(View.GONE);\n                f.onViewCreated(f.mView, f.mSavedFragmentState);\n                dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState, false);\n            } else {\n                f.mInnerView = null;\n            }\n        }\n    }\n\n    /**\n     * Fragments that have been shown or hidden don't have their visibility changed or\n     * animations run during the {@link #showFragment(Fragment)} or {@link #hideFragment(Fragment)}\n     * calls. After fragments are brought to their final state in\n     * {@link #moveFragmentToExpectedState(Fragment)} the fragments that have been shown or\n     * hidden must have their visibility changed and their animations started here.\n     *\n     * @param fragment The fragment with mHiddenChanged = true that should change its View's\n     *                 visibility and start the show or hide animation.\n     */\n    void completeShowHideFragment(final Fragment fragment) {\n        if (fragment.mView != null) {\n            AnimationOrAnimator anim = loadAnimation(fragment, fragment.getNextTransition(),\n                    !fragment.mHidden, fragment.getNextTransitionStyle());\n            if (anim != null && anim.animator != null) {\n                anim.animator.setTarget(fragment.mView);\n                if (fragment.mHidden) {\n                    if (fragment.isHideReplaced()) {\n                        fragment.setHideReplaced(false);\n                    } else {\n                        final ViewGroup container = fragment.mContainer;\n                        final View animatingView = fragment.mView;\n                        container.startViewTransition(animatingView);\n                        // Delay the actual hide operation until the animation finishes,\n                        // otherwise the fragment will just immediately disappear\n                        anim.animator.addListener(new AnimatorListenerAdapter() {\n                            @Override\n                            public void onAnimationEnd(Animator animation) {\n                                container.endViewTransition(animatingView);\n                                animation.removeListener(this);\n                                if (fragment.mView != null && fragment.mHidden) {\n                                    fragment.mView.setVisibility(View.GONE);\n                                }\n                            }\n                        });\n                    }\n                } else {\n                    fragment.mView.setVisibility(View.VISIBLE);\n                }\n                anim.animator.start();\n            } else {\n                if (anim != null) {\n                    fragment.mView.startAnimation(anim.animation);\n                    anim.animation.start();\n                }\n                final int visibility = fragment.mHidden && !fragment.isHideReplaced()\n                        ? View.GONE\n                        : View.VISIBLE;\n                fragment.mView.setVisibility(visibility);\n                if (fragment.isHideReplaced()) {\n                    fragment.setHideReplaced(false);\n                }\n            }\n        }\n        if (fragment.mAdded && isMenuAvailable(fragment)) {\n            mNeedMenuInvalidate = true;\n        }\n        fragment.mHiddenChanged = false;\n        fragment.onHiddenChanged(fragment.mHidden);\n    }\n\n    /**\n     * Moves a fragment to its expected final state or the fragment manager's state, depending\n     * on whether the fragment manager's state is raised properly.\n     *\n     * @param f The fragment to change.\n     */\n    void moveFragmentToExpectedState(Fragment f) {\n        if (f == null) {\n            return;\n        }\n        if (!mActive.containsKey(f.mWho)) {\n            if (DEBUG) {\n                Log.v(TAG, \"Ignoring moving \" + f + \" to state \" + mCurState\n                        + \"since it is not added to \" + this);\n            }\n            return;\n        }\n        int nextState = mCurState;\n        if (f.mRemoving) {\n            if (f.isInBackStack()) {\n                nextState = Math.min(nextState, Fragment.CREATED);\n            } else {\n                nextState = Math.min(nextState, Fragment.INITIALIZING);\n            }\n        }\n        moveToState(f, nextState, f.getNextTransition(), f.getNextTransitionStyle(), false);\n\n        if (f.mView != null) {\n            // Move the view if it is out of order\n            Fragment underFragment = findFragmentUnder(f);\n            if (underFragment != null) {\n                final View underView = underFragment.mView;\n                // make sure this fragment is in the right order.\n                final ViewGroup container = f.mContainer;\n                int underIndex = container.indexOfChild(underView);\n                int viewIndex = container.indexOfChild(f.mView);\n                if (viewIndex < underIndex) {\n                    container.removeViewAt(viewIndex);\n                    container.addView(f.mView, underIndex);\n                }\n            }\n            if (f.mIsNewlyAdded && f.mContainer != null) {\n                // Make it visible and run the animations\n                if (f.mPostponedAlpha > 0f) {\n                    f.mView.setAlpha(f.mPostponedAlpha);\n                }\n                f.mPostponedAlpha = 0f;\n                f.mIsNewlyAdded = false;\n                // run animations:\n                AnimationOrAnimator anim = loadAnimation(f, f.getNextTransition(), true,\n                        f.getNextTransitionStyle());\n                if (anim != null) {\n                    if (anim.animation != null) {\n                        f.mView.startAnimation(anim.animation);\n                    } else {\n                        anim.animator.setTarget(f.mView);\n                        anim.animator.start();\n                    }\n                }\n            }\n        }\n        if (f.mHiddenChanged) {\n            completeShowHideFragment(f);\n        }\n    }\n\n    /**\n     * Changes the state of the fragment manager to {@code newState}. If the fragment manager\n     * changes state or {@code always} is {@code true}, any fragments within it have their\n     * states updated as well.\n     *\n     * @param newState The new state for the fragment manager\n     * @param always If {@code true}, all fragments update their state, even\n     *               if {@code newState} matches the current fragment manager's state.\n     */\n    void moveToState(int newState, boolean always) {\n        if (mHost == null && newState != Fragment.INITIALIZING) {\n            throw new IllegalStateException(\"No activity\");\n        }\n\n        if (!always && newState == mCurState) {\n            return;\n        }\n\n        mCurState = newState;\n\n        // Must add them in the proper order. mActive fragments may be out of order\n        final int numAdded = mAdded.size();\n        for (int i = 0; i < numAdded; i++) {\n            Fragment f = mAdded.get(i);\n            moveFragmentToExpectedState(f);\n        }\n\n        // Now iterate through all active fragments. These will include those that are removed\n        // and detached.\n        for (Fragment f : mActive.values()) {\n            if (f != null && (f.mRemoving || f.mDetached) && !f.mIsNewlyAdded) {\n                moveFragmentToExpectedState(f);\n            }\n        }\n\n        startPendingDeferredFragments();\n\n        if (mNeedMenuInvalidate && mHost != null && mCurState == Fragment.RESUMED) {\n            mHost.onSupportInvalidateOptionsMenu();\n            mNeedMenuInvalidate = false;\n        }\n    }\n\n    void startPendingDeferredFragments() {\n        for (Fragment f : mActive.values()) {\n            if (f != null) {\n                performPendingDeferredStart(f);\n            }\n        }\n    }\n\n    void makeActive(Fragment f) {\n        if (mActive.get(f.mWho) != null) {\n            return;\n        }\n\n        mActive.put(f.mWho, f);\n        if (f.mRetainInstanceChangedWhileDetached) {\n            if (f.mRetainInstance) {\n                addRetainedFragment(f);\n            } else {\n                removeRetainedFragment(f);\n            }\n            f.mRetainInstanceChangedWhileDetached = false;\n        }\n        if (DEBUG) Log.v(TAG, \"Added fragment to active set \" + f);\n    }\n\n    void makeInactive(Fragment f) {\n        if (mActive.get(f.mWho) == null) {\n            return;\n        }\n\n        if (DEBUG) Log.v(TAG, \"Removed fragment from active set \" + f);\n        // Ensure that any Fragment that had this Fragment as its\n        // target Fragment retains a reference to the Fragment\n        for (Fragment fragment : mActive.values()) {\n            if (fragment != null && f.mWho.equals(fragment.mTargetWho)) {\n                fragment.mTarget = f;\n                fragment.mTargetWho = null;\n            }\n        }\n        // Don't remove yet. That happens in burpActive(). This prevents\n        // concurrent modification while iterating over mActive\n        mActive.put(f.mWho, null);\n        removeRetainedFragment(f);\n\n        if (f.mTargetWho != null) {\n            // Restore the target Fragment so that it can be accessed\n            // even after the Fragment is removed.\n            f.mTarget = mActive.get(f.mTargetWho);\n        }\n        f.initState();\n    }\n\n    public void addFragment(Fragment fragment, boolean moveToStateNow) {\n        if (DEBUG) Log.v(TAG, \"add: \" + fragment);\n        makeActive(fragment);\n        if (!fragment.mDetached) {\n            if (mAdded.contains(fragment)) {\n                throw new IllegalStateException(\"Fragment already added: \" + fragment);\n            }\n            synchronized (mAdded) {\n                mAdded.add(fragment);\n            }\n            fragment.mAdded = true;\n            fragment.mRemoving = false;\n            if (fragment.mView == null) {\n                fragment.mHiddenChanged = false;\n            }\n            if (isMenuAvailable(fragment)) {\n                mNeedMenuInvalidate = true;\n            }\n            if (moveToStateNow) {\n                moveToState(fragment);\n            }\n        }\n    }\n\n    public void removeFragment(Fragment fragment) {\n        if (DEBUG) Log.v(TAG, \"remove: \" + fragment + \" nesting=\" + fragment.mBackStackNesting);\n        final boolean inactive = !fragment.isInBackStack();\n        if (!fragment.mDetached || inactive) {\n            synchronized (mAdded) {\n                mAdded.remove(fragment);\n            }\n            if (isMenuAvailable(fragment)) {\n                mNeedMenuInvalidate = true;\n            }\n            fragment.mAdded = false;\n            fragment.mRemoving = true;\n        }\n    }\n\n    /**\n     * Marks a fragment as hidden to be later animated in with\n     * {@link #completeShowHideFragment(Fragment)}.\n     *\n     * @param fragment The fragment to be shown.\n     */\n    public void hideFragment(Fragment fragment) {\n        if (DEBUG) Log.v(TAG, \"hide: \" + fragment);\n        if (!fragment.mHidden) {\n            fragment.mHidden = true;\n            // Toggle hidden changed so that if a fragment goes through show/hide/show\n            // it doesn't go through the animation.\n            fragment.mHiddenChanged = !fragment.mHiddenChanged;\n        }\n    }\n\n    /**\n     * Marks a fragment as shown to be later animated in with\n     * {@link #completeShowHideFragment(Fragment)}.\n     *\n     * @param fragment The fragment to be shown.\n     */\n    public void showFragment(Fragment fragment) {\n        if (DEBUG) Log.v(TAG, \"show: \" + fragment);\n        if (fragment.mHidden) {\n            fragment.mHidden = false;\n            // Toggle hidden changed so that if a fragment goes through show/hide/show\n            // it doesn't go through the animation.\n            fragment.mHiddenChanged = !fragment.mHiddenChanged;\n        }\n    }\n\n    public void detachFragment(Fragment fragment) {\n        if (DEBUG) Log.v(TAG, \"detach: \" + fragment);\n        if (!fragment.mDetached) {\n            fragment.mDetached = true;\n            if (fragment.mAdded) {\n                // We are not already in back stack, so need to remove the fragment.\n                if (DEBUG) Log.v(TAG, \"remove from detach: \" + fragment);\n                synchronized (mAdded) {\n                    mAdded.remove(fragment);\n                }\n                if (isMenuAvailable(fragment)) {\n                    mNeedMenuInvalidate = true;\n                }\n                fragment.mAdded = false;\n            }\n        }\n    }\n\n    public void attachFragment(Fragment fragment) {\n        if (DEBUG) Log.v(TAG, \"attach: \" + fragment);\n        if (fragment.mDetached) {\n            fragment.mDetached = false;\n            if (!fragment.mAdded) {\n                if (mAdded.contains(fragment)) {\n                    throw new IllegalStateException(\"Fragment already added: \" + fragment);\n                }\n                if (DEBUG) Log.v(TAG, \"add from attach: \" + fragment);\n                synchronized (mAdded) {\n                    mAdded.add(fragment);\n                }\n                fragment.mAdded = true;\n                if (isMenuAvailable(fragment)) {\n                    mNeedMenuInvalidate = true;\n                }\n            }\n        }\n    }\n\n    @Override\n    @Nullable\n    public Fragment findFragmentById(int id) {\n        // First look through added fragments.\n        for (int i = mAdded.size() - 1; i >= 0; i--) {\n            Fragment f = mAdded.get(i);\n            if (f != null && f.mFragmentId == id) {\n                return f;\n            }\n        }\n        // Now for any known fragment.\n        for (Fragment f : mActive.values()) {\n            if (f != null && f.mFragmentId == id) {\n                return f;\n            }\n        }\n        return null;\n    }\n\n    @Override\n    @Nullable\n    public Fragment findFragmentByTag(@Nullable String tag) {\n        if (tag != null) {\n            // First look through added fragments.\n            for (int i=mAdded.size()-1; i>=0; i--) {\n                Fragment f = mAdded.get(i);\n                if (f != null && tag.equals(f.mTag)) {\n                    return f;\n                }\n            }\n        }\n        if (tag != null) {\n            // Now for any known fragment.\n            for (Fragment f : mActive.values()) {\n                if (f != null && tag.equals(f.mTag)) {\n                    return f;\n                }\n            }\n        }\n        return null;\n    }\n\n    public Fragment findFragmentByWho(@NonNull String who) {\n        for (Fragment f : mActive.values()) {\n            if (f != null && (f = f.findFragmentByWho(who)) != null) {\n                return f;\n            }\n        }\n        return null;\n    }\n\n    private void checkStateLoss() {\n        if (isStateSaved()) {\n            // MOD: Fix fragment state loss exception\n            //throw new IllegalStateException(\n            //        \"Can not perform this action after onSaveInstanceState\");\n        }\n    }\n\n    @Override\n    public boolean isStateSaved() {\n        // See saveAllState() for the explanation of this.  We do this for\n        // all platform versions, to keep our behavior more consistent between\n        // them.\n        return mStateSaved || mStopped;\n    }\n\n    /**\n     * Adds an action to the queue of pending actions.\n     *\n     * @param action the action to add\n     * @param allowStateLoss whether to allow loss of state information\n     * @throws IllegalStateException if the activity has been destroyed\n     */\n    public void enqueueAction(OpGenerator action, boolean allowStateLoss) {\n        if (!allowStateLoss) {\n            checkStateLoss();\n        }\n        synchronized (this) {\n            if (mDestroyed || mHost == null) {\n                if (allowStateLoss) {\n                    // This FragmentManager isn't attached, so drop the entire transaction.\n                    return;\n                }\n                throw new IllegalStateException(\"Activity has been destroyed\");\n            }\n            if (mPendingActions == null) {\n                mPendingActions = new ArrayList<>();\n            }\n            mPendingActions.add(action);\n            scheduleCommit();\n        }\n    }\n\n    /**\n     * Schedules the execution when one hasn't been scheduled already. This should happen\n     * the first time {@link #enqueueAction(OpGenerator, boolean)} is called or when\n     * a postponed transaction has been started with\n     * {@link Fragment#startPostponedEnterTransition()}\n     */\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    void scheduleCommit() {\n        synchronized (this) {\n            boolean postponeReady =\n                    mPostponedTransactions != null && !mPostponedTransactions.isEmpty();\n            boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;\n            if (postponeReady || pendingReady) {\n                mHost.getHandler().removeCallbacks(mExecCommit);\n                mHost.getHandler().post(mExecCommit);\n                updateOnBackPressedCallbackEnabled();\n            }\n        }\n    }\n\n    public int allocBackStackIndex(BackStackRecord bse) {\n        synchronized (this) {\n            if (mAvailBackStackIndices == null || mAvailBackStackIndices.size() <= 0) {\n                if (mBackStackIndices == null) {\n                    mBackStackIndices = new ArrayList<BackStackRecord>();\n                }\n                int index = mBackStackIndices.size();\n                if (DEBUG) Log.v(TAG, \"Setting back stack index \" + index + \" to \" + bse);\n                mBackStackIndices.add(bse);\n                return index;\n\n            } else {\n                int index = mAvailBackStackIndices.remove(mAvailBackStackIndices.size()-1);\n                if (DEBUG) Log.v(TAG, \"Adding back stack index \" + index + \" with \" + bse);\n                mBackStackIndices.set(index, bse);\n                return index;\n            }\n        }\n    }\n\n    public void setBackStackIndex(int index, BackStackRecord bse) {\n        synchronized (this) {\n            if (mBackStackIndices == null) {\n                mBackStackIndices = new ArrayList<BackStackRecord>();\n            }\n            int N = mBackStackIndices.size();\n            if (index < N) {\n                if (DEBUG) Log.v(TAG, \"Setting back stack index \" + index + \" to \" + bse);\n                mBackStackIndices.set(index, bse);\n            } else {\n                while (N < index) {\n                    mBackStackIndices.add(null);\n                    if (mAvailBackStackIndices == null) {\n                        mAvailBackStackIndices = new ArrayList<Integer>();\n                    }\n                    if (DEBUG) Log.v(TAG, \"Adding available back stack index \" + N);\n                    mAvailBackStackIndices.add(N);\n                    N++;\n                }\n                if (DEBUG) Log.v(TAG, \"Adding back stack index \" + index + \" with \" + bse);\n                mBackStackIndices.add(bse);\n            }\n        }\n    }\n\n    public void freeBackStackIndex(int index) {\n        synchronized (this) {\n            mBackStackIndices.set(index, null);\n            if (mAvailBackStackIndices == null) {\n                mAvailBackStackIndices = new ArrayList<Integer>();\n            }\n            if (DEBUG) Log.v(TAG, \"Freeing back stack index \" + index);\n            mAvailBackStackIndices.add(index);\n        }\n    }\n\n    /**\n     * Broken out from exec*, this prepares for gathering and executing operations.\n     *\n     * @param allowStateLoss true if state loss should be ignored or false if it should be\n     *                       checked.\n     */\n    private void ensureExecReady(boolean allowStateLoss) {\n        if (mExecutingActions) {\n            throw new IllegalStateException(\"FragmentManager is already executing transactions\");\n        }\n\n        if (mHost == null) {\n            throw new IllegalStateException(\"Fragment host has been destroyed\");\n        }\n\n        if (Looper.myLooper() != mHost.getHandler().getLooper()) {\n            throw new IllegalStateException(\"Must be called from main thread of fragment host\");\n        }\n\n        if (!allowStateLoss) {\n            checkStateLoss();\n        }\n\n        if (mTmpRecords == null) {\n            mTmpRecords = new ArrayList<>();\n            mTmpIsPop = new ArrayList<>();\n        }\n        mExecutingActions = true;\n        try {\n            executePostponedTransaction(null, null);\n        } finally {\n            mExecutingActions = false;\n        }\n    }\n\n    public void execSingleAction(OpGenerator action, boolean allowStateLoss) {\n        if (allowStateLoss && (mHost == null || mDestroyed)) {\n            // This FragmentManager isn't attached, so drop the entire transaction.\n            return;\n        }\n        ensureExecReady(allowStateLoss);\n        if (action.generateOps(mTmpRecords, mTmpIsPop)) {\n            mExecutingActions = true;\n            try {\n                removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);\n            } finally {\n                cleanupExec();\n            }\n        }\n\n        updateOnBackPressedCallbackEnabled();\n        doPendingDeferredStart();\n        burpActive();\n    }\n\n    /**\n     * Broken out of exec*, this cleans up the mExecutingActions and the temporary structures\n     * used in executing operations.\n     */\n    private void cleanupExec() {\n        mExecutingActions = false;\n        mTmpIsPop.clear();\n        mTmpRecords.clear();\n    }\n\n    /**\n     * Only call from main thread!\n     */\n    public boolean execPendingActions() {\n        ensureExecReady(true);\n\n        boolean didSomething = false;\n        while (generateOpsForPendingActions(mTmpRecords, mTmpIsPop)) {\n            mExecutingActions = true;\n            try {\n                removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);\n            } finally {\n                cleanupExec();\n            }\n            didSomething = true;\n        }\n\n        updateOnBackPressedCallbackEnabled();\n        doPendingDeferredStart();\n        burpActive();\n\n        return didSomething;\n    }\n\n    /**\n     * Complete the execution of transactions that have previously been postponed, but are\n     * now ready.\n     */\n    private void executePostponedTransaction(ArrayList<BackStackRecord> records,\n                                             ArrayList<Boolean> isRecordPop) {\n        int numPostponed = mPostponedTransactions == null ? 0 : mPostponedTransactions.size();\n        for (int i = 0; i < numPostponed; i++) {\n            StartEnterTransitionListener listener = mPostponedTransactions.get(i);\n            if (records != null && !listener.mIsBack) {\n                int index = records.indexOf(listener.mRecord);\n                if (index != -1 && isRecordPop.get(index)) {\n                    mPostponedTransactions.remove(i);\n                    i--;\n                    numPostponed--;\n                    listener.cancelTransaction();\n                    continue;\n                }\n            }\n            if (listener.isReady() || (records != null\n                    && listener.mRecord.interactsWith(records, 0, records.size()))) {\n                mPostponedTransactions.remove(i);\n                i--;\n                numPostponed--;\n                int index;\n                if (records != null && !listener.mIsBack\n                        && (index = records.indexOf(listener.mRecord)) != -1\n                        && isRecordPop.get(index)) {\n                    // This is popping a postponed transaction\n                    listener.cancelTransaction();\n                } else {\n                    listener.completeTransaction();\n                }\n            }\n        }\n    }\n\n    /**\n     * Remove redundant BackStackRecord operations and executes them. This method merges operations\n     * of proximate records that allow reordering. See\n     * {@link FragmentTransaction#setReorderingAllowed(boolean)}.\n     * <p>\n     * For example, a transaction that adds to the back stack and then another that pops that\n     * back stack record will be optimized to remove the unnecessary operation.\n     * <p>\n     * Likewise, two transactions committed that are executed at the same time will be optimized\n     * to remove the redundant operations as well as two pop operations executed together.\n     *\n     * @param records The records pending execution\n     * @param isRecordPop The direction that these records are being run.\n     */\n    private void removeRedundantOperationsAndExecute(ArrayList<BackStackRecord> records,\n                                                     ArrayList<Boolean> isRecordPop) {\n        if (records == null || records.isEmpty()) {\n            return;\n        }\n\n        if (isRecordPop == null || records.size() != isRecordPop.size()) {\n            throw new IllegalStateException(\"Internal error with the back stack records\");\n        }\n\n        // Force start of any postponed transactions that interact with scheduled transactions:\n        executePostponedTransaction(records, isRecordPop);\n\n        final int numRecords = records.size();\n        int startIndex = 0;\n        for (int recordNum = 0; recordNum < numRecords; recordNum++) {\n            final boolean canReorder = records.get(recordNum).mReorderingAllowed;\n            if (!canReorder) {\n                // execute all previous transactions\n                if (startIndex != recordNum) {\n                    executeOpsTogether(records, isRecordPop, startIndex, recordNum);\n                }\n                // execute all pop operations that don't allow reordering together or\n                // one add operation\n                int reorderingEnd = recordNum + 1;\n                if (isRecordPop.get(recordNum)) {\n                    while (reorderingEnd < numRecords\n                            && isRecordPop.get(reorderingEnd)\n                            && !records.get(reorderingEnd).mReorderingAllowed) {\n                        reorderingEnd++;\n                    }\n                }\n                executeOpsTogether(records, isRecordPop, recordNum, reorderingEnd);\n                startIndex = reorderingEnd;\n                recordNum = reorderingEnd - 1;\n            }\n        }\n        if (startIndex != numRecords) {\n            executeOpsTogether(records, isRecordPop, startIndex, numRecords);\n        }\n    }\n\n    /**\n     * Executes a subset of a list of BackStackRecords, all of which either allow reordering or\n     * do not allow ordering.\n     * @param records A list of BackStackRecords that are to be executed\n     * @param isRecordPop The direction that these records are being run.\n     * @param startIndex The index of the first record in <code>records</code> to be executed\n     * @param endIndex One more than the final record index in <code>records</code> to executed.\n     */\n    private void executeOpsTogether(ArrayList<BackStackRecord> records,\n                                    ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {\n        final boolean allowReordering = records.get(startIndex).mReorderingAllowed;\n        boolean addToBackStack = false;\n        if (mTmpAddedFragments == null) {\n            mTmpAddedFragments = new ArrayList<>();\n        } else {\n            mTmpAddedFragments.clear();\n        }\n        mTmpAddedFragments.addAll(mAdded);\n        Fragment oldPrimaryNav = getPrimaryNavigationFragment();\n        for (int recordNum = startIndex; recordNum < endIndex; recordNum++) {\n            final BackStackRecord record = records.get(recordNum);\n            final boolean isPop = isRecordPop.get(recordNum);\n            if (!isPop) {\n                oldPrimaryNav = record.expandOps(mTmpAddedFragments, oldPrimaryNav);\n            } else {\n                oldPrimaryNav = record.trackAddedFragmentsInPop(mTmpAddedFragments, oldPrimaryNav);\n            }\n            addToBackStack = addToBackStack || record.mAddToBackStack;\n        }\n        mTmpAddedFragments.clear();\n\n        if (!allowReordering) {\n            FragmentTransition.startTransitions(this, records, isRecordPop, startIndex, endIndex,\n                    false);\n        }\n        executeOps(records, isRecordPop, startIndex, endIndex);\n\n        int postponeIndex = endIndex;\n        if (allowReordering) {\n            ArraySet<Fragment> addedFragments = new ArraySet<>();\n            addAddedFragments(addedFragments);\n            postponeIndex = postponePostponableTransactions(records, isRecordPop,\n                    startIndex, endIndex, addedFragments);\n            makeRemovedFragmentsInvisible(addedFragments);\n        }\n\n        if (postponeIndex != startIndex && allowReordering) {\n            // need to run something now\n            FragmentTransition.startTransitions(this, records, isRecordPop, startIndex,\n                    postponeIndex, true);\n            moveToState(mCurState, true);\n        }\n\n        for (int recordNum = startIndex; recordNum < endIndex; recordNum++) {\n            final BackStackRecord record = records.get(recordNum);\n            final boolean isPop = isRecordPop.get(recordNum);\n            if (isPop && record.mIndex >= 0) {\n                freeBackStackIndex(record.mIndex);\n                record.mIndex = -1;\n            }\n            record.runOnCommitRunnables();\n        }\n        if (addToBackStack) {\n            reportBackStackChanged();\n        }\n    }\n\n    /**\n     * Any fragments that were removed because they have been postponed should have their views\n     * made invisible by setting their alpha to 0.\n     *\n     * @param fragments The fragments that were added during operation execution. Only the ones\n     *                  that are no longer added will have their alpha changed.\n     */\n    private void makeRemovedFragmentsInvisible(ArraySet<Fragment> fragments) {\n        final int numAdded = fragments.size();\n        for (int i = 0; i < numAdded; i++) {\n            final Fragment fragment = fragments.valueAt(i);\n            if (!fragment.mAdded) {\n                final View view = fragment.requireView();\n                fragment.mPostponedAlpha = view.getAlpha();\n                view.setAlpha(0f);\n            }\n        }\n    }\n\n    /**\n     * Examine all transactions and determine which ones are marked as postponed. Those will\n     * have their operations rolled back and moved to the end of the record list (up to endIndex).\n     * It will also add the postponed transaction to the queue.\n     *\n     * @param records A list of BackStackRecords that should be checked.\n     * @param isRecordPop The direction that these records are being run.\n     * @param startIndex The index of the first record in <code>records</code> to be checked\n     * @param endIndex One more than the final record index in <code>records</code> to be checked.\n     * @return The index of the first postponed transaction or endIndex if no transaction was\n     * postponed.\n     */\n    private int postponePostponableTransactions(ArrayList<BackStackRecord> records,\n                                                ArrayList<Boolean> isRecordPop, int startIndex, int endIndex,\n                                                ArraySet<Fragment> added) {\n        int postponeIndex = endIndex;\n        for (int i = endIndex - 1; i >= startIndex; i--) {\n            final BackStackRecord record = records.get(i);\n            final boolean isPop = isRecordPop.get(i);\n            boolean isPostponed = record.isPostponed()\n                    && !record.interactsWith(records, i + 1, endIndex);\n            if (isPostponed) {\n                if (mPostponedTransactions == null) {\n                    mPostponedTransactions = new ArrayList<>();\n                }\n                StartEnterTransitionListener listener =\n                        new StartEnterTransitionListener(record, isPop);\n                mPostponedTransactions.add(listener);\n                record.setOnStartPostponedListener(listener);\n\n                // roll back the transaction\n                if (isPop) {\n                    record.executeOps();\n                } else {\n                    record.executePopOps(false);\n                }\n\n                // move to the end\n                postponeIndex--;\n                if (i != postponeIndex) {\n                    records.remove(i);\n                    records.add(postponeIndex, record);\n                }\n\n                // different views may be visible now\n                addAddedFragments(added);\n            }\n        }\n        return postponeIndex;\n    }\n\n    /**\n     * When a postponed transaction is ready to be started, this completes the transaction,\n     * removing, hiding, or showing views as well as starting the animations and transitions.\n     * <p>\n     * {@code runtransitions} is set to false when the transaction postponement was interrupted\n     * abnormally -- normally by a new transaction being started that affects the postponed\n     * transaction.\n     *\n     * @param record The transaction to run\n     * @param isPop true if record is popping or false if it is adding\n     * @param runTransitions true if the fragment transition should be run or false otherwise.\n     * @param moveToState true if the state should be changed after executing the operations.\n     *                    This is false when the transaction is canceled when a postponed\n     *                    transaction is popped.\n     */\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    void completeExecute(BackStackRecord record, boolean isPop, boolean runTransitions,\n                         boolean moveToState) {\n        if (isPop) {\n            record.executePopOps(moveToState);\n        } else {\n            record.executeOps();\n        }\n        ArrayList<BackStackRecord> records = new ArrayList<>(1);\n        ArrayList<Boolean> isRecordPop = new ArrayList<>(1);\n        records.add(record);\n        isRecordPop.add(isPop);\n        if (runTransitions) {\n            FragmentTransition.startTransitions(this, records, isRecordPop, 0, 1, true);\n        }\n        if (moveToState) {\n            moveToState(mCurState, true);\n        }\n\n        for (Fragment fragment : mActive.values()) {\n            // Allow added fragments to be removed during the pop since we aren't going\n            // to move them to the final state with moveToState(mCurState).\n            if (fragment != null && fragment.mView != null && fragment.mIsNewlyAdded\n                    && record.interactsWith(fragment.mContainerId)) {\n                if (fragment.mPostponedAlpha > 0) {\n                    fragment.mView.setAlpha(fragment.mPostponedAlpha);\n                }\n                if (moveToState) {\n                    fragment.mPostponedAlpha = 0;\n                } else {\n                    fragment.mPostponedAlpha = -1;\n                    fragment.mIsNewlyAdded = false;\n                }\n            }\n        }\n    }\n\n    /**\n     * Find a fragment within the fragment's container whose View should be below the passed\n     * fragment. {@code null} is returned when the fragment has no View or if there should be\n     * no fragment with a View below the given fragment.\n     *\n     * As an example, if mAdded has two Fragments with Views sharing the same container:\n     * FragmentA\n     * FragmentB\n     *\n     * Then, when processing FragmentB, FragmentA will be returned. If, however, FragmentA\n     * had no View, null would be returned.\n     *\n     * @param f The fragment that may be on top of another fragment.\n     * @return The fragment with a View under f, if one exists or null if f has no View or\n     * there are no fragments with Views in the same container.\n     */\n    private Fragment findFragmentUnder(Fragment f) {\n        final ViewGroup container = f.mContainer;\n        final View view = f.mView;\n\n        if (container == null || view == null) {\n            return null;\n        }\n\n        final int fragmentIndex = mAdded.indexOf(f);\n        for (int i = fragmentIndex - 1; i >= 0; i--) {\n            Fragment underFragment = mAdded.get(i);\n            if (underFragment.mContainer == container && underFragment.mView != null) {\n                // Found the fragment under this one\n                return underFragment;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Run the operations in the BackStackRecords, either to push or pop.\n     *\n     * @param records The list of records whose operations should be run.\n     * @param isRecordPop The direction that these records are being run.\n     * @param startIndex The index of the first entry in records to run.\n     * @param endIndex One past the index of the final entry in records to run.\n     */\n    private static void executeOps(ArrayList<BackStackRecord> records,\n                                   ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {\n        for (int i = startIndex; i < endIndex; i++) {\n            final BackStackRecord record = records.get(i);\n            final boolean isPop = isRecordPop.get(i);\n            if (isPop) {\n                record.bumpBackStackNesting(-1);\n                // Only execute the add operations at the end of\n                // all transactions.\n                boolean moveToState = i == (endIndex - 1);\n                record.executePopOps(moveToState);\n            } else {\n                record.bumpBackStackNesting(1);\n                record.executeOps();\n            }\n        }\n    }\n\n    /**\n     * Ensure that fragments that are added are moved to at least the CREATED state.\n     * Any newly-added Views are inserted into {@code added} so that the Transaction can be\n     * postponed with {@link Fragment#postponeEnterTransition()}. They will later be made\n     * invisible (by setting their alpha to 0) if they have been removed when postponed.\n     */\n    private void addAddedFragments(ArraySet<Fragment> added) {\n        if (mCurState < Fragment.CREATED) {\n            return;\n        }\n        // We want to leave the fragment in the started state\n        final int state = Math.min(mCurState, Fragment.STARTED);\n        final int numAdded = mAdded.size();\n        for (int i = 0; i < numAdded; i++) {\n            Fragment fragment = mAdded.get(i);\n            if (fragment.mState < state) {\n                moveToState(fragment, state, fragment.getNextAnim(), fragment.getNextTransition(),\n                        false);\n                if (fragment.mView != null && !fragment.mHidden && fragment.mIsNewlyAdded) {\n                    added.add(fragment);\n                }\n            }\n        }\n    }\n\n    /**\n     * Starts all postponed transactions regardless of whether they are ready or not.\n     */\n    private void forcePostponedTransactions() {\n        if (mPostponedTransactions != null) {\n            while (!mPostponedTransactions.isEmpty()) {\n                mPostponedTransactions.remove(0).completeTransaction();\n            }\n        }\n    }\n\n    /**\n     * Ends the animations of fragments so that they immediately reach the end state.\n     * This is used prior to saving the state so that the correct state is saved.\n     */\n    private void endAnimatingAwayFragments() {\n        for (Fragment fragment : mActive.values()) {\n            if (fragment != null) {\n                if (fragment.getAnimatingAway() != null) {\n                    // Give up waiting for the animation and just end it.\n                    final int stateAfterAnimating = fragment.getStateAfterAnimating();\n                    final View animatingAway = fragment.getAnimatingAway();\n                    Animation animation = animatingAway.getAnimation();\n                    if (animation != null) {\n                        animation.cancel();\n                        // force-clear the animation, as Animation#cancel() doesn't work prior to N,\n                        // and will instead cause the animation to infinitely loop\n                        animatingAway.clearAnimation();\n                    }\n                    fragment.setAnimatingAway(null);\n                    moveToState(fragment, stateAfterAnimating, 0, 0, false);\n                } else if (fragment.getAnimator() != null) {\n                    fragment.getAnimator().end();\n                }\n            }\n        }\n    }\n\n    /**\n     * Adds all records in the pending actions to records and whether they are add or pop\n     * operations to isPop. After executing, the pending actions will be empty.\n     *\n     * @param records All pending actions will generate BackStackRecords added to this.\n     *                This contains the transactions, in order, to execute.\n     * @param isPop All pending actions will generate booleans to add to this. This contains\n     *              an entry for each entry in records to indicate whether or not it is a\n     *              pop action.\n     */\n    private boolean generateOpsForPendingActions(ArrayList<BackStackRecord> records,\n                                                 ArrayList<Boolean> isPop) {\n        boolean didSomething = false;\n        synchronized (this) {\n            if (mPendingActions == null || mPendingActions.size() == 0) {\n                return false;\n            }\n\n            final int numActions = mPendingActions.size();\n            for (int i = 0; i < numActions; i++) {\n                didSomething |= mPendingActions.get(i).generateOps(records, isPop);\n            }\n            mPendingActions.clear();\n            mHost.getHandler().removeCallbacks(mExecCommit);\n        }\n        return didSomething;\n    }\n\n    void doPendingDeferredStart() {\n        if (mHavePendingDeferredStart) {\n            mHavePendingDeferredStart = false;\n            startPendingDeferredFragments();\n        }\n    }\n\n    void reportBackStackChanged() {\n        if (mBackStackChangeListeners != null) {\n            for (int i=0; i<mBackStackChangeListeners.size(); i++) {\n                mBackStackChangeListeners.get(i).onBackStackChanged();\n            }\n        }\n    }\n\n    void addBackStackState(BackStackRecord state) {\n        if (mBackStack == null) {\n            mBackStack = new ArrayList<BackStackRecord>();\n        }\n        mBackStack.add(state);\n    }\n\n    @SuppressWarnings(\"unused\")\n    boolean popBackStackState(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,\n                              String name, int id, int flags) {\n        if (mBackStack == null) {\n            return false;\n        }\n        if (name == null && id < 0 && (flags & POP_BACK_STACK_INCLUSIVE) == 0) {\n            int last = mBackStack.size() - 1;\n            if (last < 0) {\n                return false;\n            }\n            records.add(mBackStack.remove(last));\n            isRecordPop.add(true);\n        } else {\n            int index = -1;\n            if (name != null || id >= 0) {\n                // If a name or ID is specified, look for that place in\n                // the stack.\n                index = mBackStack.size()-1;\n                while (index >= 0) {\n                    BackStackRecord bss = mBackStack.get(index);\n                    if (name != null && name.equals(bss.getName())) {\n                        break;\n                    }\n                    if (id >= 0 && id == bss.mIndex) {\n                        break;\n                    }\n                    index--;\n                }\n                if (index < 0) {\n                    return false;\n                }\n                if ((flags&POP_BACK_STACK_INCLUSIVE) != 0) {\n                    index--;\n                    // Consume all following entries that match.\n                    while (index >= 0) {\n                        BackStackRecord bss = mBackStack.get(index);\n                        if ((name != null && name.equals(bss.getName()))\n                                || (id >= 0 && id == bss.mIndex)) {\n                            index--;\n                            continue;\n                        }\n                        break;\n                    }\n                }\n            }\n            if (index == mBackStack.size()-1) {\n                return false;\n            }\n            for (int i = mBackStack.size() - 1; i > index; i--) {\n                records.add(mBackStack.remove(i));\n                isRecordPop.add(true);\n            }\n        }\n        return true;\n    }\n\n    /**\n     * @deprecated Ideally, all {@link androidx.fragment.app.FragmentHostCallback} instances\n     * implement ViewModelStoreOwner and we can remove this method entirely.\n     */\n    @Deprecated\n    FragmentManagerNonConfig retainNonConfig() {\n        if (mHost instanceof ViewModelStoreOwner) {\n            throwException(new IllegalStateException(\"You cannot use retainNonConfig when your \"\n                    + \"FragmentHostCallback implements ViewModelStoreOwner.\"));\n        }\n        return mNonConfig.getSnapshot();\n    }\n\n    void saveFragmentViewState(Fragment f) {\n        if (f.mInnerView == null) {\n            return;\n        }\n        if (mStateArray == null) {\n            mStateArray = new SparseArray<Parcelable>();\n        } else {\n            mStateArray.clear();\n        }\n        f.mInnerView.saveHierarchyState(mStateArray);\n        if (mStateArray.size() > 0) {\n            f.mSavedViewState = mStateArray;\n            mStateArray = null;\n        }\n    }\n\n    Bundle saveFragmentBasicState(Fragment f) {\n        Bundle result = null;\n\n        if (mStateBundle == null) {\n            mStateBundle = new Bundle();\n        }\n        f.performSaveInstanceState(mStateBundle);\n        dispatchOnFragmentSaveInstanceState(f, mStateBundle, false);\n        if (!mStateBundle.isEmpty()) {\n            result = mStateBundle;\n            mStateBundle = null;\n        }\n\n        if (f.mView != null) {\n            saveFragmentViewState(f);\n        }\n        if (f.mSavedViewState != null) {\n            if (result == null) {\n                result = new Bundle();\n            }\n            result.putSparseParcelableArray(\n                    FragmentManagerImpl.VIEW_STATE_TAG, f.mSavedViewState);\n        }\n        if (!f.mUserVisibleHint) {\n            if (result == null) {\n                result = new Bundle();\n            }\n            // Only add this if it's not the default value\n            result.putBoolean(FragmentManagerImpl.USER_VISIBLE_HINT_TAG, f.mUserVisibleHint);\n        }\n\n        return result;\n    }\n\n    Parcelable saveAllState() {\n        // Make sure all pending operations have now been executed to get\n        // our state update-to-date.\n        forcePostponedTransactions();\n        endAnimatingAwayFragments();\n        execPendingActions();\n\n        mStateSaved = true;\n\n        if (mActive.isEmpty()) {\n            return null;\n        }\n\n        // First collect all active fragments.\n        int size = mActive.size();\n        ArrayList<FragmentState> active = new ArrayList<>(size);\n        boolean haveFragments = false;\n        for (Fragment f : mActive.values()) {\n            if (f != null) {\n                if (f.mFragmentManager != this) {\n                    throwException(new IllegalStateException(\n                            \"Failure saving state: active \" + f\n                                    + \" was removed from the FragmentManager\"));\n                }\n\n                haveFragments = true;\n\n                FragmentState fs = new FragmentState(f);\n                active.add(fs);\n\n                if (f.mState > Fragment.INITIALIZING && fs.mSavedFragmentState == null) {\n                    fs.mSavedFragmentState = saveFragmentBasicState(f);\n\n                    if (f.mTargetWho != null) {\n                        Fragment target = mActive.get(f.mTargetWho);\n                        if (target == null) {\n                            throwException(new IllegalStateException(\n                                    \"Failure saving state: \" + f\n                                            + \" has target not in fragment manager: \"\n                                            + f.mTargetWho));\n                        }\n                        if (fs.mSavedFragmentState == null) {\n                            fs.mSavedFragmentState = new Bundle();\n                        }\n                        putFragment(fs.mSavedFragmentState,\n                                FragmentManagerImpl.TARGET_STATE_TAG, target);\n                        if (f.mTargetRequestCode != 0) {\n                            fs.mSavedFragmentState.putInt(\n                                    FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG,\n                                    f.mTargetRequestCode);\n                        }\n                    }\n\n                } else {\n                    fs.mSavedFragmentState = f.mSavedFragmentState;\n                }\n\n                if (DEBUG) Log.v(TAG, \"Saved state of \" + f + \": \"\n                        + fs.mSavedFragmentState);\n            }\n        }\n\n        if (!haveFragments) {\n            if (DEBUG) Log.v(TAG, \"saveAllState: no fragments!\");\n            return null;\n        }\n\n        ArrayList<String> added = null;\n        BackStackState[] backStack = null;\n\n        // Build list of currently added fragments.\n        size = mAdded.size();\n        if (size > 0) {\n            added = new ArrayList<>(size);\n            for (Fragment f : mAdded) {\n                added.add(f.mWho);\n                if (f.mFragmentManager != this) {\n                    throwException(new IllegalStateException(\n                            \"Failure saving state: active \" + f\n                                    + \" was removed from the FragmentManager\"));\n                }\n                if (DEBUG) {\n                    Log.v(TAG, \"saveAllState: adding fragment (\" + f.mWho\n                            + \"): \" + f);\n                }\n            }\n        }\n\n        // Now save back stack.\n        if (mBackStack != null) {\n            size = mBackStack.size();\n            if (size > 0) {\n                backStack = new BackStackState[size];\n                for (int i = 0; i < size; i++) {\n                    backStack[i] = new BackStackState(mBackStack.get(i));\n                    if (DEBUG) Log.v(TAG, \"saveAllState: adding back stack #\" + i\n                            + \": \" + mBackStack.get(i));\n                }\n            }\n        }\n\n        FragmentManagerState fms = new FragmentManagerState();\n        fms.mActive = active;\n        fms.mAdded = added;\n        fms.mBackStack = backStack;\n        if (mPrimaryNav != null) {\n            fms.mPrimaryNavActiveWho = mPrimaryNav.mWho;\n        }\n        fms.mNextFragmentIndex = mNextFragmentIndex;\n        return fms;\n    }\n\n    void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {\n        if (mHost instanceof ViewModelStoreOwner) {\n            throwException(new IllegalStateException(\"You must use restoreSaveState when your \"\n                    + \"FragmentHostCallback implements ViewModelStoreOwner\"));\n        }\n        mNonConfig.restoreFromSnapshot(nonConfig);\n        restoreSaveState(state);\n    }\n\n    void restoreSaveState(Parcelable state) {\n        // If there is no saved state at all, then there's nothing else to do\n        if (state == null) return;\n        FragmentManagerState fms = (FragmentManagerState)state;\n        if (fms.mActive == null) return;\n\n        // First re-attach any non-config instances we are retaining back\n        // to their saved state, so we don't try to instantiate them again.\n        for (Fragment f : mNonConfig.getRetainedFragments()) {\n            if (DEBUG) Log.v(TAG, \"restoreSaveState: re-attaching retained \" + f);\n            FragmentState fs = null;\n            for (FragmentState fragmentState : fms.mActive) {\n                if (fragmentState.mWho.equals(f.mWho)) {\n                    fs = fragmentState;\n                    break;\n                }\n            }\n            if (fs == null) {\n                if (DEBUG) {\n                    Log.v(TAG, \"Discarding retained Fragment \" + f\n                            + \" that was not found in the set of active Fragments \" + fms.mActive);\n                }\n                // We need to ensure that onDestroy and any other clean up is done\n                // so move the Fragment up to CREATED, then mark it as being removed, then\n                // destroy it.\n                moveToState(f, Fragment.CREATED, 0, 0, false);\n                f.mRemoving = true;\n                moveToState(f, Fragment.INITIALIZING, 0, 0, false);\n                continue;\n            }\n            fs.mInstance = f;\n            f.mSavedViewState = null;\n            f.mBackStackNesting = 0;\n            f.mInLayout = false;\n            f.mAdded = false;\n            f.mTargetWho = f.mTarget != null ? f.mTarget.mWho : null;\n            f.mTarget = null;\n            if (fs.mSavedFragmentState != null) {\n                fs.mSavedFragmentState.setClassLoader(mHost.getContext().getClassLoader());\n                f.mSavedViewState = fs.mSavedFragmentState.getSparseParcelableArray(\n                        FragmentManagerImpl.VIEW_STATE_TAG);\n                f.mSavedFragmentState = fs.mSavedFragmentState;\n            }\n        }\n\n        // Build the full list of active fragments, instantiating them from\n        // their saved state.\n        mActive.clear();\n        for (FragmentState fs : fms.mActive) {\n            if (fs != null) {\n                Fragment f = fs.instantiate(mHost.getContext().getClassLoader(),\n                        getFragmentFactory());\n                f.mFragmentManager = this;\n                if (DEBUG) Log.v(TAG, \"restoreSaveState: active (\" + f.mWho + \"): \" + f);\n                mActive.put(f.mWho, f);\n                // Now that the fragment is instantiated (or came from being\n                // retained above), clear mInstance in case we end up re-restoring\n                // from this FragmentState again.\n                fs.mInstance = null;\n            }\n        }\n\n        // Build the list of currently added fragments.\n        mAdded.clear();\n        if (fms.mAdded != null) {\n            for (String who : fms.mAdded) {\n                Fragment f = mActive.get(who);\n                if (f == null) {\n                    throwException(new IllegalStateException(\n                            \"No instantiated fragment for (\" + who + \")\"));\n                }\n                f.mAdded = true;\n                if (DEBUG) Log.v(TAG, \"restoreSaveState: added (\" + who + \"): \" + f);\n                if (mAdded.contains(f)) {\n                    throw new IllegalStateException(\"Already added \" + f);\n                }\n                synchronized (mAdded) {\n                    mAdded.add(f);\n                }\n            }\n        }\n\n        // Build the back stack.\n        if (fms.mBackStack != null) {\n            mBackStack = new ArrayList<BackStackRecord>(fms.mBackStack.length);\n            for (int i=0; i<fms.mBackStack.length; i++) {\n                BackStackRecord bse = fms.mBackStack[i].instantiate(this);\n                if (DEBUG) {\n                    Log.v(TAG, \"restoreAllState: back stack #\" + i\n                            + \" (index \" + bse.mIndex + \"): \" + bse);\n                    LogWriter logw = new LogWriter(TAG);\n                    PrintWriter pw = new PrintWriter(logw);\n                    bse.dump(\"  \", pw, false);\n                    pw.close();\n                }\n                mBackStack.add(bse);\n                if (bse.mIndex >= 0) {\n                    setBackStackIndex(bse.mIndex, bse);\n                }\n            }\n        } else {\n            mBackStack = null;\n        }\n\n        if (fms.mPrimaryNavActiveWho != null) {\n            mPrimaryNav = mActive.get(fms.mPrimaryNavActiveWho);\n            dispatchParentPrimaryNavigationFragmentChanged(mPrimaryNav);\n        }\n        this.mNextFragmentIndex = fms.mNextFragmentIndex;\n    }\n\n    /**\n     * To prevent list modification errors, mActive sets values to null instead of\n     * removing them when the Fragment becomes inactive. This cleans up the list at the\n     * end of executing the transactions.\n     */\n    private void burpActive() {\n        Collection<Fragment> values = mActive.values();\n        // values() provides a view into the map, so removing elements from it\n        // removes the relevant pairs in the Map\n        values.removeAll(Collections.singleton(null));\n    }\n\n    public void attachController(@NonNull FragmentHostCallback host,\n            @NonNull FragmentContainer container, @Nullable final Fragment parent) {\n        if (mHost != null) throw new IllegalStateException(\"Already attached\");\n        mHost = host;\n        mContainer = container;\n        mParent = parent;\n        if (mParent != null) {\n            // Since the callback depends on us being the primary navigation fragment,\n            // update our callback now that we have a parent so that we have the correct\n            // state by default\n            updateOnBackPressedCallbackEnabled();\n        }\n        // Set up the OnBackPressedCallback\n        if (host instanceof OnBackPressedDispatcherOwner) {\n            OnBackPressedDispatcherOwner dispatcherOwner = ((OnBackPressedDispatcherOwner) host);\n            mOnBackPressedDispatcher = dispatcherOwner.getOnBackPressedDispatcher();\n            LifecycleOwner owner = parent != null ? parent : dispatcherOwner;\n            mOnBackPressedDispatcher.addCallback(owner, mOnBackPressedCallback);\n        }\n\n        // Get the FragmentManagerViewModel\n        if (parent != null) {\n            mNonConfig = parent.mFragmentManager.getChildNonConfig(parent);\n        } else if (host instanceof ViewModelStoreOwner) {\n            ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();\n            mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore);\n        } else {\n            mNonConfig = new FragmentManagerViewModel(false);\n        }\n    }\n\n    public void noteStateNotSaved() {\n        mStateSaved = false;\n        mStopped = false;\n        final int addedCount = mAdded.size();\n        for (int i = 0; i < addedCount; i++) {\n            Fragment fragment = mAdded.get(i);\n            if (fragment != null) {\n                fragment.noteStateNotSaved();\n            }\n        }\n    }\n\n    public void dispatchCreate() {\n        mStateSaved = false;\n        mStopped = false;\n        dispatchStateChange(Fragment.CREATED);\n    }\n\n    public void dispatchActivityCreated() {\n        mStateSaved = false;\n        mStopped = false;\n        dispatchStateChange(Fragment.ACTIVITY_CREATED);\n    }\n\n    public void dispatchStart() {\n        mStateSaved = false;\n        mStopped = false;\n        dispatchStateChange(Fragment.STARTED);\n    }\n\n    public void dispatchResume() {\n        mStateSaved = false;\n        mStopped = false;\n        dispatchStateChange(Fragment.RESUMED);\n    }\n\n    public void dispatchPause() {\n        dispatchStateChange(Fragment.STARTED);\n    }\n\n    public void dispatchStop() {\n        mStopped = true;\n        dispatchStateChange(Fragment.ACTIVITY_CREATED);\n    }\n\n    public void dispatchDestroyView() {\n        dispatchStateChange(Fragment.CREATED);\n    }\n\n    public void dispatchDestroy() {\n        mDestroyed = true;\n        execPendingActions();\n        dispatchStateChange(Fragment.INITIALIZING);\n        mHost = null;\n        mContainer = null;\n        mParent = null;\n        if (mOnBackPressedDispatcher != null) {\n            // mOnBackPressedDispatcher can hold a reference to the host\n            // so we need to null it out to prevent memory leaks\n            mOnBackPressedCallback.remove();\n            mOnBackPressedDispatcher = null;\n        }\n    }\n\n    private void dispatchStateChange(int nextState) {\n        try {\n            mExecutingActions = true;\n            moveToState(nextState, false);\n        } finally {\n            mExecutingActions = false;\n        }\n        execPendingActions();\n    }\n\n    public void dispatchMultiWindowModeChanged(boolean isInMultiWindowMode) {\n        for (int i = mAdded.size() - 1; i >= 0; --i) {\n            final Fragment f = mAdded.get(i);\n            if (f != null) {\n                f.performMultiWindowModeChanged(isInMultiWindowMode);\n            }\n        }\n    }\n\n    public void dispatchPictureInPictureModeChanged(boolean isInPictureInPictureMode) {\n        for (int i = mAdded.size() - 1; i >= 0; --i) {\n            final Fragment f = mAdded.get(i);\n            if (f != null) {\n                f.performPictureInPictureModeChanged(isInPictureInPictureMode);\n            }\n        }\n    }\n\n    public void dispatchConfigurationChanged(@NonNull Configuration newConfig) {\n        for (int i = 0; i < mAdded.size(); i++) {\n            Fragment f = mAdded.get(i);\n            if (f != null) {\n                f.performConfigurationChanged(newConfig);\n            }\n        }\n    }\n\n    public void dispatchLowMemory() {\n        for (int i = 0; i < mAdded.size(); i++) {\n            Fragment f = mAdded.get(i);\n            if (f != null) {\n                f.performLowMemory();\n            }\n        }\n    }\n\n    public boolean dispatchCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {\n        if (mCurState < Fragment.CREATED) {\n            return false;\n        }\n        boolean show = false;\n        ArrayList<Fragment> newMenus = null;\n        for (int i = 0; i < mAdded.size(); i++) {\n            Fragment f = mAdded.get(i);\n            if (f != null) {\n                if (f.performCreateOptionsMenu(menu, inflater)) {\n                    show = true;\n                    if (newMenus == null) {\n                        newMenus = new ArrayList<Fragment>();\n                    }\n                    newMenus.add(f);\n                }\n            }\n        }\n\n        if (mCreatedMenus != null) {\n            for (int i=0; i<mCreatedMenus.size(); i++) {\n                Fragment f = mCreatedMenus.get(i);\n                if (newMenus == null || !newMenus.contains(f)) {\n                    f.onDestroyOptionsMenu();\n                }\n            }\n        }\n\n        mCreatedMenus = newMenus;\n\n        return show;\n    }\n\n    public boolean dispatchPrepareOptionsMenu(@NonNull Menu menu) {\n        if (mCurState < Fragment.CREATED) {\n            return false;\n        }\n        boolean show = false;\n        for (int i = 0; i < mAdded.size(); i++) {\n            Fragment f = mAdded.get(i);\n            if (f != null) {\n                if (f.performPrepareOptionsMenu(menu)) {\n                    show = true;\n                }\n            }\n        }\n        return show;\n    }\n\n    public boolean dispatchOptionsItemSelected(@NonNull MenuItem item) {\n        if (mCurState < Fragment.CREATED) {\n            return false;\n        }\n        for (int i = 0; i < mAdded.size(); i++) {\n            Fragment f = mAdded.get(i);\n            if (f != null) {\n                if (f.performOptionsItemSelected(item)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    public boolean dispatchContextItemSelected(@NonNull MenuItem item) {\n        if (mCurState < Fragment.CREATED) {\n            return false;\n        }\n        for (int i = 0; i < mAdded.size(); i++) {\n            Fragment f = mAdded.get(i);\n            if (f != null) {\n                if (f.performContextItemSelected(item)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    public void dispatchOptionsMenuClosed(@NonNull Menu menu) {\n        if (mCurState < Fragment.CREATED) {\n            return;\n        }\n        for (int i = 0; i < mAdded.size(); i++) {\n            Fragment f = mAdded.get(i);\n            if (f != null) {\n                f.performOptionsMenuClosed(menu);\n            }\n        }\n    }\n\n    @SuppressWarnings(\"ReferenceEquality\")\n    public void setPrimaryNavigationFragment(Fragment f) {\n        if (f != null && (mActive.get(f.mWho) != f\n                || (f.mHost != null && f.getFragmentManager() != this))) {\n            throw new IllegalArgumentException(\"Fragment \" + f\n                    + \" is not an active fragment of FragmentManager \" + this);\n        }\n        Fragment previousPrimaryNav = mPrimaryNav;\n        mPrimaryNav = f;\n        dispatchParentPrimaryNavigationFragmentChanged(previousPrimaryNav);\n        dispatchParentPrimaryNavigationFragmentChanged(mPrimaryNav);\n    }\n\n    private void dispatchParentPrimaryNavigationFragmentChanged(@Nullable Fragment f) {\n        if (f != null && mActive.get(f.mWho) == f) {\n            f.performPrimaryNavigationFragmentChanged();\n        }\n    }\n\n    void dispatchPrimaryNavigationFragmentChanged() {\n        updateOnBackPressedCallbackEnabled();\n        // Dispatch the change event to this FragmentManager's primary navigation fragment\n        dispatchParentPrimaryNavigationFragmentChanged(mPrimaryNav);\n    }\n\n    @Override\n    @Nullable\n    public Fragment getPrimaryNavigationFragment() {\n        return mPrimaryNav;\n    }\n\n    public void setMaxLifecycle(Fragment f, Lifecycle.State state) {\n        if ((mActive.get(f.mWho) != f\n                || (f.mHost != null && f.getFragmentManager() != this))) {\n            throw new IllegalArgumentException(\"Fragment \" + f\n                    + \" is not an active fragment of FragmentManager \" + this);\n        }\n        f.mMaxState = state;\n    }\n\n    @Override\n    @NonNull\n    public FragmentFactory getFragmentFactory() {\n        FragmentFactory factory = super.getFragmentFactory();\n        if (factory == DEFAULT_FACTORY) {\n            if (mParent != null) {\n                // This can't call setFragmentFactory since we need to\n                // compute this each time getFragmentFactory() is called\n                // so that if the parent's FragmentFactory changes, we\n                // pick the change up here.\n                return mParent.mFragmentManager.getFragmentFactory();\n            }\n            setFragmentFactory(new FragmentFactory() {\n                @SuppressWarnings(\"deprecation\")\n                @NonNull\n                @Override\n                public Fragment instantiate(@NonNull ClassLoader classLoader,\n                        @NonNull String className) {\n                    return mHost.instantiate(mHost.getContext(), className, null);\n                }\n            });\n        }\n        return super.getFragmentFactory();\n    }\n\n    @Override\n    public void registerFragmentLifecycleCallbacks(@NonNull FragmentLifecycleCallbacks cb,\n                                                   boolean recursive) {\n        mLifecycleCallbacks.add(new FragmentLifecycleCallbacksHolder(cb, recursive));\n    }\n\n    @Override\n    public void unregisterFragmentLifecycleCallbacks(@NonNull FragmentLifecycleCallbacks cb) {\n        synchronized (mLifecycleCallbacks) {\n            for (int i = 0, N = mLifecycleCallbacks.size(); i < N; i++) {\n                if (mLifecycleCallbacks.get(i).mCallback == cb) {\n                    mLifecycleCallbacks.remove(i);\n                    break;\n                }\n            }\n        }\n    }\n\n    void dispatchOnFragmentPreAttached(@NonNull Fragment f, @NonNull Context context,\n                                       boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentPreAttached(f, context, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentPreAttached(this, f, context);\n            }\n        }\n    }\n\n    void dispatchOnFragmentAttached(@NonNull Fragment f, @NonNull Context context,\n                                    boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentAttached(f, context, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentAttached(this, f, context);\n            }\n        }\n    }\n\n    void dispatchOnFragmentPreCreated(@NonNull Fragment f, @Nullable Bundle savedInstanceState,\n                                      boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentPreCreated(f, savedInstanceState, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentPreCreated(this, f, savedInstanceState);\n            }\n        }\n    }\n\n    void dispatchOnFragmentCreated(@NonNull Fragment f, @Nullable Bundle savedInstanceState,\n                                   boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentCreated(f, savedInstanceState, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentCreated(this, f, savedInstanceState);\n            }\n        }\n    }\n\n    void dispatchOnFragmentActivityCreated(@NonNull Fragment f, @Nullable Bundle savedInstanceState,\n                                           boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentActivityCreated(f, savedInstanceState, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentActivityCreated(this, f, savedInstanceState);\n            }\n        }\n    }\n\n    void dispatchOnFragmentViewCreated(@NonNull Fragment f, @NonNull View v,\n                                       @Nullable Bundle savedInstanceState, boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentViewCreated(f, v, savedInstanceState, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentViewCreated(this, f, v, savedInstanceState);\n            }\n        }\n    }\n\n    void dispatchOnFragmentStarted(@NonNull Fragment f, boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentStarted(f, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentStarted(this, f);\n            }\n        }\n    }\n\n    void dispatchOnFragmentResumed(@NonNull Fragment f, boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentResumed(f, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentResumed(this, f);\n            }\n        }\n    }\n\n    void dispatchOnFragmentPaused(@NonNull Fragment f, boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentPaused(f, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentPaused(this, f);\n            }\n        }\n    }\n\n    void dispatchOnFragmentStopped(@NonNull Fragment f, boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentStopped(f, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentStopped(this, f);\n            }\n        }\n    }\n\n    void dispatchOnFragmentSaveInstanceState(@NonNull Fragment f, @NonNull Bundle outState,\n                                             boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentSaveInstanceState(f, outState, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentSaveInstanceState(this, f, outState);\n            }\n        }\n    }\n\n    void dispatchOnFragmentViewDestroyed(@NonNull Fragment f, boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentViewDestroyed(f, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentViewDestroyed(this, f);\n            }\n        }\n    }\n\n    void dispatchOnFragmentDestroyed(@NonNull Fragment f, boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentDestroyed(f, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentDestroyed(this, f);\n            }\n        }\n    }\n\n    void dispatchOnFragmentDetached(@NonNull Fragment f, boolean onlyRecursive) {\n        if (mParent != null) {\n            FragmentManager parentManager = mParent.getFragmentManager();\n            if (parentManager instanceof FragmentManagerImpl) {\n                ((FragmentManagerImpl) parentManager)\n                        .dispatchOnFragmentDetached(f, true);\n            }\n        }\n        for (FragmentLifecycleCallbacksHolder holder : mLifecycleCallbacks) {\n            if (!onlyRecursive || holder.mRecursive) {\n                holder.mCallback.onFragmentDetached(this, f);\n            }\n        }\n    }\n\n    // Checks if fragments that belong to this fragment manager (or their children) have menus,\n    // and if they are visible.\n    boolean checkForMenus() {\n        boolean hasMenu = false;\n        for (Fragment f: mActive.values()) {\n            if (f != null) {\n                hasMenu = isMenuAvailable(f);\n            }\n            if (hasMenu) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private boolean isMenuAvailable(Fragment f) {\n        return f.mHasMenu && f.mMenuVisible || f.mChildFragmentManager.checkForMenus();\n    }\n\n    public static int reverseTransit(int transit) {\n        int rev = 0;\n        switch (transit) {\n            case FragmentTransaction.TRANSIT_FRAGMENT_OPEN:\n                rev = FragmentTransaction.TRANSIT_FRAGMENT_CLOSE;\n                break;\n            case FragmentTransaction.TRANSIT_FRAGMENT_CLOSE:\n                rev = FragmentTransaction.TRANSIT_FRAGMENT_OPEN;\n                break;\n            case FragmentTransaction.TRANSIT_FRAGMENT_FADE:\n                rev = FragmentTransaction.TRANSIT_FRAGMENT_FADE;\n                break;\n        }\n        return rev;\n\n    }\n\n    public static final int ANIM_STYLE_OPEN_ENTER = 1;\n    public static final int ANIM_STYLE_OPEN_EXIT = 2;\n    public static final int ANIM_STYLE_CLOSE_ENTER = 3;\n    public static final int ANIM_STYLE_CLOSE_EXIT = 4;\n    public static final int ANIM_STYLE_FADE_ENTER = 5;\n    public static final int ANIM_STYLE_FADE_EXIT = 6;\n\n    public static int transitToStyleIndex(int transit, boolean enter) {\n        int animAttr = -1;\n        switch (transit) {\n            case FragmentTransaction.TRANSIT_FRAGMENT_OPEN:\n                animAttr = enter ? ANIM_STYLE_OPEN_ENTER : ANIM_STYLE_OPEN_EXIT;\n                break;\n            case FragmentTransaction.TRANSIT_FRAGMENT_CLOSE:\n                animAttr = enter ? ANIM_STYLE_CLOSE_ENTER : ANIM_STYLE_CLOSE_EXIT;\n                break;\n            case FragmentTransaction.TRANSIT_FRAGMENT_FADE:\n                animAttr = enter ? ANIM_STYLE_FADE_ENTER : ANIM_STYLE_FADE_EXIT;\n                break;\n        }\n        return animAttr;\n    }\n\n    @Override\n    @Nullable\n    public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context,\n                             @NonNull AttributeSet attrs) {\n        if (!\"fragment\".equals(name)) {\n            return null;\n        }\n\n        String fname = attrs.getAttributeValue(null, \"class\");\n        TypedArray a =  context.obtainStyledAttributes(attrs, FragmentTag.Fragment);\n        if (fname == null) {\n            fname = a.getString(FragmentTag.Fragment_name);\n        }\n        int id = a.getResourceId(FragmentTag.Fragment_id, View.NO_ID);\n        String tag = a.getString(FragmentTag.Fragment_tag);\n        a.recycle();\n\n        if (fname == null || !FragmentFactory.isFragmentClass(context.getClassLoader(), fname)) {\n            // Invalid support lib fragment; let the device's framework handle it.\n            // This will allow android.app.Fragments to do the right thing.\n            return null;\n        }\n\n        int containerId = parent != null ? parent.getId() : 0;\n        if (containerId == View.NO_ID && id == View.NO_ID && tag == null) {\n            throw new IllegalArgumentException(attrs.getPositionDescription()\n                    + \": Must specify unique android:id, android:tag, or have a parent with an id for \" + fname);\n        }\n\n        // If we restored from a previous state, we may already have\n        // instantiated this fragment from the state and should use\n        // that instance instead of making a new one.\n        Fragment fragment = id != View.NO_ID ? findFragmentById(id) : null;\n        if (fragment == null && tag != null) {\n            fragment = findFragmentByTag(tag);\n        }\n        if (fragment == null && containerId != View.NO_ID) {\n            fragment = findFragmentById(containerId);\n        }\n\n        if (FragmentManagerImpl.DEBUG) Log.v(TAG, \"onCreateView: id=0x\"\n                + Integer.toHexString(id) + \" fname=\" + fname\n                + \" existing=\" + fragment);\n        if (fragment == null) {\n            fragment = getFragmentFactory().instantiate(context.getClassLoader(), fname);\n            fragment.mFromLayout = true;\n            fragment.mFragmentId = id != 0 ? id : containerId;\n            fragment.mContainerId = containerId;\n            fragment.mTag = tag;\n            fragment.mInLayout = true;\n            fragment.mFragmentManager = this;\n            fragment.mHost = mHost;\n            fragment.onInflate(mHost.getContext(), attrs, fragment.mSavedFragmentState);\n            addFragment(fragment, true);\n\n        } else if (fragment.mInLayout) {\n            // A fragment already exists and it is not one we restored from\n            // previous state.\n            throw new IllegalArgumentException(attrs.getPositionDescription()\n                    + \": Duplicate id 0x\" + Integer.toHexString(id)\n                    + \", tag \" + tag + \", or parent id 0x\" + Integer.toHexString(containerId)\n                    + \" with another fragment for \" + fname);\n        } else {\n            // This fragment was retained from a previous instance; get it\n            // going now.\n            fragment.mInLayout = true;\n            fragment.mHost = mHost;\n            // Give the Fragment the attributes to initialize itself.\n            fragment.onInflate(mHost.getContext(), attrs, fragment.mSavedFragmentState);\n        }\n\n        // If we haven't finished entering the CREATED state ourselves yet,\n        // push the inflated child fragment along. This will ensureInflatedFragmentView\n        // at the right phase of the lifecycle so that we will have mView populated\n        // for compliant fragments below.\n        if (mCurState < Fragment.CREATED && fragment.mFromLayout) {\n            moveToState(fragment, Fragment.CREATED, 0, 0, false);\n        } else {\n            moveToState(fragment);\n        }\n\n        if (fragment.mView == null) {\n            // MOD: Fragment didn't create a view fix\n            return null;\n            //throw new IllegalStateException(\"Fragment \" + fname\n            //        + \" did not create a view.\");\n        }\n        if (id != 0) {\n            fragment.mView.setId(id);\n        }\n        if (fragment.mView.getTag() == null) {\n            fragment.mView.setTag(tag);\n        }\n        return fragment.mView;\n    }\n\n    @Override\n    public View onCreateView(String name, Context context, AttributeSet attrs) {\n        return onCreateView(null, name, context, attrs);\n    }\n\n    LayoutInflater.Factory2 getLayoutInflaterFactory() {\n        return this;\n    }\n\n    static class FragmentTag {\n        public static final int[] Fragment = {\n                0x01010003, 0x010100d0, 0x010100d1\n        };\n        public static final int Fragment_id = 1;\n        public static final int Fragment_name = 0;\n        public static final int Fragment_tag = 2;\n\n        private FragmentTag() {\n        }\n    }\n\n    /**\n     * An add or pop transaction to be scheduled for the UI thread.\n     */\n    interface OpGenerator {\n        /**\n         * Generate transactions to add to {@code records} and whether or not the transaction is\n         * an add or pop to {@code isRecordPop}.\n         *\n         * records and isRecordPop must be added equally so that each transaction in records\n         * matches the boolean for whether or not it is a pop in isRecordPop.\n         *\n         * @param records A list to add transactions to.\n         * @param isRecordPop A list to add whether or not the transactions added to records is\n         *                    a pop transaction.\n         * @return true if something was added or false otherwise.\n         */\n        boolean generateOps(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop);\n    }\n\n    /**\n     * A pop operation OpGenerator. This will be run on the UI thread and will generate the\n     * transactions that will be popped if anything can be popped.\n     */\n    private class PopBackStackState implements OpGenerator {\n        final String mName;\n        final int mId;\n        final int mFlags;\n\n        PopBackStackState(String name, int id, int flags) {\n            mName = name;\n            mId = id;\n            mFlags = flags;\n        }\n\n        @Override\n        public boolean generateOps(ArrayList<BackStackRecord> records,\n                                   ArrayList<Boolean> isRecordPop) {\n            if (mPrimaryNav != null // We have a primary nav fragment\n                    && mId < 0 // No valid id (since they're local)\n                    && mName == null) { // no name to pop to (since they're local)\n                final FragmentManager childManager = mPrimaryNav.getChildFragmentManager();\n                if (childManager.popBackStackImmediate()) {\n                    // We didn't add any operations for this FragmentManager even though\n                    // a child did do work.\n                    return false;\n                }\n            }\n            return popBackStackState(records, isRecordPop, mName, mId, mFlags);\n        }\n    }\n\n    /**\n     * A listener for a postponed transaction. This waits until\n     * {@link Fragment#startPostponedEnterTransition()} is called or a transaction is started\n     * that interacts with this one, based on interactions with the fragment container.\n     */\n    static class StartEnterTransitionListener\n            implements Fragment.OnStartEnterTransitionListener {\n        final boolean mIsBack;\n        final BackStackRecord mRecord;\n        private int mNumPostponed;\n\n        StartEnterTransitionListener(BackStackRecord record, boolean isBack) {\n            mIsBack = isBack;\n            mRecord = record;\n        }\n\n        /**\n         * Called from {@link Fragment#startPostponedEnterTransition()}, this decreases the\n         * number of Fragments that are postponed. This may cause the transaction to schedule\n         * to finish running and run transitions and animations.\n         */\n        @Override\n        public void onStartEnterTransition() {\n            mNumPostponed--;\n            if (mNumPostponed != 0) {\n                return;\n            }\n            mRecord.mManager.scheduleCommit();\n        }\n\n        /**\n         * Called from {@link Fragment#\n         * setOnStartEnterTransitionListener(Fragment.OnStartEnterTransitionListener)}, this\n         * increases the number of fragments that are postponed as part of this transaction.\n         */\n        @Override\n        public void startListening() {\n            mNumPostponed++;\n        }\n\n        /**\n         * @return true if there are no more postponed fragments as part of the transaction.\n         */\n        public boolean isReady() {\n            return mNumPostponed == 0;\n        }\n\n        /**\n         * Completes the transaction and start the animations and transitions. This may skip\n         * the transitions if this is called before all fragments have called\n         * {@link Fragment#startPostponedEnterTransition()}.\n         */\n        public void completeTransaction() {\n            final boolean canceled;\n            canceled = mNumPostponed > 0;\n            FragmentManagerImpl manager = mRecord.mManager;\n            final int numAdded = manager.mAdded.size();\n            for (int i = 0; i < numAdded; i++) {\n                final Fragment fragment = manager.mAdded.get(i);\n                fragment.setOnStartEnterTransitionListener(null);\n                if (canceled && fragment.isPostponed()) {\n                    fragment.startPostponedEnterTransition();\n                }\n            }\n            mRecord.mManager.completeExecute(mRecord, mIsBack, !canceled, true);\n        }\n\n        /**\n         * Cancels this transaction instead of completing it. That means that the state isn't\n         * changed, so the pop results in no change to the state.\n         */\n        public void cancelTransaction() {\n            mRecord.mManager.completeExecute(mRecord, mIsBack, false, false);\n        }\n    }\n\n    /**\n     * Contains either an animator or animation. One of these should be null.\n     */\n    private static class AnimationOrAnimator {\n        public final Animation animation;\n        public final Animator animator;\n\n        AnimationOrAnimator(Animation animation) {\n            this.animation = animation;\n            this.animator = null;\n            if (animation == null) {\n                throw new IllegalStateException(\"Animation cannot be null\");\n            }\n        }\n\n        AnimationOrAnimator(Animator animator) {\n            this.animation = null;\n            this.animator = animator;\n            if (animator == null) {\n                throw new IllegalStateException(\"Animator cannot be null\");\n            }\n        }\n    }\n\n    /**\n     * We must call endViewTransition() before the animation ends or else the parent doesn't\n     * get nulled out. We use both startViewTransition() and startAnimation() to solve a problem\n     * with Views remaining in the hierarchy as disappearing children after the view has been\n     * removed in some edge cases.\n     */\n    private static class EndViewTransitionAnimation extends AnimationSet implements Runnable {\n        private final ViewGroup mParent;\n        private final View mChild;\n        private boolean mEnded;\n        private boolean mTransitionEnded;\n        private boolean mAnimating = true;\n\n        EndViewTransitionAnimation(@NonNull Animation animation,\n                                  @NonNull ViewGroup parent, @NonNull View child) {\n            super(false);\n            mParent = parent;\n            mChild = child;\n            addAnimation(animation);\n            // We must call endViewTransition() even if the animation was never run or it\n            // is interrupted in a way that can't be detected easily (app put in background)\n            mParent.post(this);\n        }\n\n        @Override\n        public boolean getTransformation(long currentTime, Transformation t) {\n            mAnimating = true;\n            if (mEnded) {\n                return !mTransitionEnded;\n            }\n            boolean more = super.getTransformation(currentTime, t);\n            if (!more) {\n                mEnded = true;\n                OneShotPreDrawListener.add(mParent, this);\n            }\n            return true;\n        }\n\n        @Override\n        public boolean getTransformation(long currentTime, Transformation outTransformation,\n                                         float scale) {\n            mAnimating = true;\n            if (mEnded) {\n                return !mTransitionEnded;\n            }\n            boolean more = super.getTransformation(currentTime, outTransformation, scale);\n            if (!more) {\n                mEnded = true;\n                OneShotPreDrawListener.add(mParent, this);\n            }\n            return true;\n        }\n\n        @Override\n        public void run() {\n            if (!mEnded && mAnimating) {\n                mAnimating = false;\n                // Called while animating, so we'll check again on next cycle\n                mParent.post(this);\n            } else {\n                mParent.endViewTransition(mChild);\n                mTransitionEnded = true;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerNonConfig.java",
    "content": "/*\n * Copyright 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\n\npackage androidx.fragment.app;\n\nimport android.os.Parcelable;\n\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.ViewModelStore;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/**\n * FragmentManagerNonConfig stores the retained instance fragments across\n * activity recreation events.\n *\n * <p>Apps should treat objects of this type as opaque, returned by\n * and passed to the state save and restore process for fragments in\n * {@link FragmentController#retainNestedNonConfig()} and\n * {@link FragmentController#restoreAllState(Parcelable, FragmentManagerNonConfig)}.</p>\n *\n * @deprecated Have your {@link FragmentHostCallback} implement\n * {@link androidx.lifecycle.ViewModelStoreOwner} to automatically retain the Fragment's\n * non configuration state.\n */\n@Deprecated\npublic class FragmentManagerNonConfig {\n    private final @Nullable Collection<Fragment> mFragments;\n    private final @Nullable Map<String, FragmentManagerNonConfig> mChildNonConfigs;\n    private final @Nullable Map<String, ViewModelStore> mViewModelStores;\n\n    FragmentManagerNonConfig(@Nullable Collection<Fragment> fragments,\n            @Nullable Map<String, FragmentManagerNonConfig> childNonConfigs,\n            @Nullable Map<String, ViewModelStore> viewModelStores) {\n        mFragments = fragments;\n        mChildNonConfigs = childNonConfigs;\n        mViewModelStores = viewModelStores;\n    }\n\n    @SuppressWarnings(\"BooleanMethodIsAlwaysInverted\")\n    boolean isRetaining(Fragment f) {\n        if (mFragments == null) {\n            return false;\n        }\n        return mFragments.contains(f);\n    }\n\n    /**\n     * @return the retained instance fragments returned by a FragmentManager\n     */\n    @Nullable\n    Collection<Fragment> getFragments() {\n        return mFragments;\n    }\n\n    /**\n     * @return the FragmentManagerNonConfigs from any applicable fragment's child FragmentManager\n     */\n    @Nullable\n    Map<String, FragmentManagerNonConfig> getChildNonConfigs() {\n        return mChildNonConfigs;\n    }\n\n    /**\n     * @return the ViewModelStores for all fragments associated with the FragmentManager\n     */\n    @Nullable\n    Map<String, ViewModelStore> getViewModelStores() {\n        return mViewModelStores;\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerState.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.annotation.SuppressLint;\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport java.util.ArrayList;\n\n@SuppressLint(\"BanParcelableUsage\")\nfinal class FragmentManagerState implements Parcelable {\n    ArrayList<FragmentState> mActive;\n    ArrayList<String> mAdded;\n    BackStackState[] mBackStack;\n    String mPrimaryNavActiveWho = null;\n    int mNextFragmentIndex;\n\n    public FragmentManagerState() {\n    }\n\n    public FragmentManagerState(Parcel in) {\n        mActive = in.createTypedArrayList(FragmentState.CREATOR);\n        mAdded = in.createStringArrayList();\n        mBackStack = in.createTypedArray(BackStackState.CREATOR);\n        mPrimaryNavActiveWho = in.readString();\n        mNextFragmentIndex = in.readInt();\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.writeTypedList(mActive);\n        dest.writeStringList(mAdded);\n        dest.writeTypedArray(mBackStack, flags);\n        dest.writeString(mPrimaryNavActiveWho);\n        dest.writeInt(mNextFragmentIndex);\n    }\n\n    public static final Parcelable.Creator<FragmentManagerState> CREATOR\n            = new Parcelable.Creator<FragmentManagerState>() {\n        @Override\n        public FragmentManagerState createFromParcel(Parcel in) {\n            return new FragmentManagerState(in);\n        }\n\n        @Override\n        public FragmentManagerState[] newArray(int size) {\n            return new FragmentManagerState[size];\n        }\n    };\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerViewModel.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\nimport androidx.lifecycle.ViewModelStore;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.Map;\n\n/**\n * FragmentManagerViewModel is the always up to date view of the Fragment's\n * non configuration state\n */\nclass FragmentManagerViewModel extends ViewModel {\n\n    private static final ViewModelProvider.Factory FACTORY = new ViewModelProvider.Factory() {\n        @NonNull\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {\n            FragmentManagerViewModel viewModel = new FragmentManagerViewModel(true);\n            return (T) viewModel;\n        }\n    };\n\n    @NonNull\n    static FragmentManagerViewModel getInstance(ViewModelStore viewModelStore) {\n        ViewModelProvider viewModelProvider = new ViewModelProvider(viewModelStore,\n                FACTORY);\n        return viewModelProvider.get(FragmentManagerViewModel.class);\n    }\n\n    private final HashSet<Fragment> mRetainedFragments = new HashSet<>();\n    private final HashMap<String, FragmentManagerViewModel> mChildNonConfigs = new HashMap<>();\n    private final HashMap<String, ViewModelStore> mViewModelStores = new HashMap<>();\n\n    private final boolean mStateAutomaticallySaved;\n    // Only used when mStateAutomaticallySaved is true\n    private boolean mHasBeenCleared = false;\n    // Only used when mStateAutomaticallySaved is false\n    private boolean mHasSavedSnapshot = false;\n\n    /**\n     * FragmentManagerViewModel simultaneously supports two modes:\n     * <ol>\n     *     <li>Automatically saved: in this model, it is assumed that the ViewModel is added to\n     *     an appropriate {@link ViewModelStore} that has the same lifecycle as the\n     *     FragmentManager and that {@link #onCleared()} indicates that the Fragment's host\n     *     is being permanently destroyed.</li>\n     *     <li>Not automatically saved: in this model, the FragmentManager is responsible for\n     *     calling {@link #getSnapshot()} and later restoring the ViewModel with\n     *     {@link #restoreFromSnapshot(FragmentManagerNonConfig)}.</li>\n     * </ol>\n     * These states are mutually exclusive.\n     *\n     * @param stateAutomaticallySaved Whether the ViewModel will be automatically saved.\n     */\n    FragmentManagerViewModel(boolean stateAutomaticallySaved) {\n        mStateAutomaticallySaved = stateAutomaticallySaved;\n    }\n\n    @Override\n    protected void onCleared() {\n        if (FragmentManagerImpl.DEBUG) {\n            Log.d(FragmentManagerImpl.TAG, \"onCleared called for \" + this);\n        }\n        mHasBeenCleared = true;\n    }\n\n    boolean isCleared() {\n        return mHasBeenCleared;\n    }\n\n    boolean addRetainedFragment(@NonNull Fragment fragment) {\n        return mRetainedFragments.add(fragment);\n    }\n\n    @NonNull\n    Collection<Fragment> getRetainedFragments() {\n        return mRetainedFragments;\n    }\n\n    boolean shouldDestroy(@NonNull Fragment fragment) {\n        if (!mRetainedFragments.contains(fragment)) {\n            // Always destroy non-retained Fragments\n            return true;\n        }\n        if (mStateAutomaticallySaved) {\n            // If we automatically save our state, then only\n            // destroy a retained Fragment when we've been cleared\n            return mHasBeenCleared;\n        } else {\n            // Else, only destroy retained Fragments if they've\n            // been reaped before the state has been saved\n            return !mHasSavedSnapshot;\n        }\n    }\n\n    boolean removeRetainedFragment(@NonNull Fragment fragment) {\n        return mRetainedFragments.remove(fragment);\n    }\n\n    @NonNull\n    FragmentManagerViewModel getChildNonConfig(@NonNull Fragment f) {\n        FragmentManagerViewModel childNonConfig = mChildNonConfigs.get(f.mWho);\n        if (childNonConfig == null) {\n            childNonConfig = new FragmentManagerViewModel(mStateAutomaticallySaved);\n            mChildNonConfigs.put(f.mWho, childNonConfig);\n        }\n        return childNonConfig;\n    }\n\n    @NonNull\n    ViewModelStore getViewModelStore(@NonNull Fragment f) {\n        ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);\n        if (viewModelStore == null) {\n            viewModelStore = new ViewModelStore();\n            mViewModelStores.put(f.mWho, viewModelStore);\n        }\n        return viewModelStore;\n    }\n\n    void clearNonConfigState(@NonNull Fragment f) {\n        if (FragmentManagerImpl.DEBUG) {\n            Log.d(FragmentManagerImpl.TAG, \"Clearing non-config state for \" + f);\n        }\n        // Clear and remove the Fragment's child non config state\n        FragmentManagerViewModel childNonConfig = mChildNonConfigs.get(f.mWho);\n        if (childNonConfig != null) {\n            childNonConfig.onCleared();\n            mChildNonConfigs.remove(f.mWho);\n        }\n        // Clear and remove the Fragment's ViewModelStore\n        ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);\n        if (viewModelStore != null) {\n            viewModelStore.clear();\n            mViewModelStores.remove(f.mWho);\n        }\n    }\n\n    /**\n     * @deprecated Ideally, we only support mStateAutomaticallySaved = true and remove this\n     * code, alongside\n     * {@link FragmentController#restoreAllState(android.os.Parcelable, FragmentManagerNonConfig)}.\n     */\n    @Deprecated\n    void restoreFromSnapshot(@Nullable FragmentManagerNonConfig nonConfig) {\n        mRetainedFragments.clear();\n        mChildNonConfigs.clear();\n        mViewModelStores.clear();\n        if (nonConfig != null) {\n            Collection<Fragment> fragments = nonConfig.getFragments();\n            if (fragments != null) {\n                mRetainedFragments.addAll(fragments);\n            }\n            Map<String, FragmentManagerNonConfig> childNonConfigs = nonConfig.getChildNonConfigs();\n            if (childNonConfigs != null) {\n                for (Map.Entry<String, FragmentManagerNonConfig> entry :\n                        childNonConfigs.entrySet()) {\n                    FragmentManagerViewModel childViewModel =\n                            new FragmentManagerViewModel(mStateAutomaticallySaved);\n                    childViewModel.restoreFromSnapshot(entry.getValue());\n                    mChildNonConfigs.put(entry.getKey(), childViewModel);\n                }\n            }\n            Map<String, ViewModelStore> viewModelStores = nonConfig.getViewModelStores();\n            if (viewModelStores != null) {\n                mViewModelStores.putAll(viewModelStores);\n            }\n        }\n        mHasSavedSnapshot = false;\n    }\n\n    /**\n     * @deprecated Ideally, we only support mStateAutomaticallySaved = true and remove this\n     * code, alongside {@link FragmentController#retainNestedNonConfig()}.\n     */\n    @Deprecated\n    @Nullable\n    FragmentManagerNonConfig getSnapshot() {\n        if (mRetainedFragments.isEmpty() && mChildNonConfigs.isEmpty()\n                && mViewModelStores.isEmpty()) {\n            return null;\n        }\n        HashMap<String, FragmentManagerNonConfig> childNonConfigs = new HashMap<>();\n        for (Map.Entry<String, FragmentManagerViewModel> entry : mChildNonConfigs.entrySet()) {\n            FragmentManagerNonConfig childNonConfig = entry.getValue().getSnapshot();\n            if (childNonConfig != null) {\n                childNonConfigs.put(entry.getKey(), childNonConfig);\n            }\n        }\n\n        mHasSavedSnapshot = true;\n        if (mRetainedFragments.isEmpty() && childNonConfigs.isEmpty()\n                && mViewModelStores.isEmpty()) {\n            return null;\n        }\n        return new FragmentManagerNonConfig(\n                new ArrayList<>(mRetainedFragments),\n                childNonConfigs,\n                new HashMap<>(mViewModelStores));\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        FragmentManagerViewModel that = (FragmentManagerViewModel) o;\n\n        return mRetainedFragments.equals(that.mRetainedFragments)\n                && mChildNonConfigs.equals(that.mChildNonConfigs)\n                && mViewModelStores.equals(that.mViewModelStores);\n    }\n\n    @Override\n    public int hashCode() {\n        int result = mRetainedFragments.hashCode();\n        result = 31 * result + mChildNonConfigs.hashCode();\n        result = 31 * result + mViewModelStores.hashCode();\n        return result;\n    }\n\n    @NonNull\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder(\"FragmentManagerViewModel{\");\n        sb.append(Integer.toHexString(System.identityHashCode(this)));\n        sb.append(\"} Fragments (\");\n        Iterator<Fragment> fragmentIterator = mRetainedFragments.iterator();\n        while (fragmentIterator.hasNext()) {\n            sb.append(fragmentIterator.next());\n            if (fragmentIterator.hasNext()) {\n                sb.append(\", \");\n            }\n        }\n        sb.append(\") Child Non Config (\");\n        Iterator<String> childNonConfigIterator = mChildNonConfigs.keySet().iterator();\n        while (childNonConfigIterator.hasNext()) {\n            sb.append(childNonConfigIterator.next());\n            if (childNonConfigIterator.hasNext()) {\n                sb.append(\", \");\n            }\n        }\n        sb.append(\") ViewModelStores (\");\n        Iterator<String> viewModelStoreIterator = mViewModelStores.keySet().iterator();\n        while (viewModelStoreIterator.hasNext()) {\n            sb.append(viewModelStoreIterator.next());\n            if (viewModelStoreIterator.hasNext()) {\n                sb.append(\", \");\n            }\n        }\n        sb.append(')');\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentPagerAdapter.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.os.Parcelable;\nimport android.util.Log;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.Lifecycle;\nimport androidx.viewpager.widget.PagerAdapter;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * Implementation of {@link PagerAdapter} that\n * represents each page as a {@link Fragment} that is persistently\n * kept in the fragment manager as long as the user can return to the page.\n *\n * <p>This version of the pager is best for use when there are a handful of\n * typically more static fragments to be paged through, such as a set of tabs.\n * The fragment of each page the user visits will be kept in memory, though its\n * view hierarchy may be destroyed when not visible.  This can result in using\n * a significant amount of memory since fragment instances can hold on to an\n * arbitrary amount of state.  For larger sets of pages, consider\n * {@link FragmentStatePagerAdapter}.\n *\n * <p>When using FragmentPagerAdapter the host ViewPager must have a\n * valid ID set.</p>\n *\n * <p>Subclasses only need to implement {@link #getItem(int)}\n * and {@link #getCount()} to have a working adapter.\n *\n * <p>Here is an example implementation of a pager containing fragments of\n * lists:\n *\n * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentPagerSupport.java\n *      complete}\n *\n * <p>The <code>R.layout.fragment_pager</code> resource of the top-level fragment is:\n *\n * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager.xml\n *      complete}\n *\n * <p>The <code>R.layout.fragment_pager_list</code> resource containing each\n * individual fragment's layout is:\n *\n * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager_list.xml\n *      complete}\n */\n@SuppressWarnings(\"deprecation\")\npublic abstract class FragmentPagerAdapter extends PagerAdapter {\n    private static final String TAG = \"FragmentPagerAdapter\";\n    private static final boolean DEBUG = false;\n\n    @Retention(RetentionPolicy.SOURCE)\n    @IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})\n    private @interface Behavior { }\n\n    /**\n     * Indicates that {@link Fragment#setUserVisibleHint(boolean)} will be called when the current\n     * fragment changes.\n     *\n     * @deprecated This behavior relies on the deprecated\n     * {@link Fragment#setUserVisibleHint(boolean)} API. Use\n     * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} to switch to its replacement,\n     * {@link FragmentTransaction#setMaxLifecycle}.\n     * @see #FragmentPagerAdapter(FragmentManager, int)\n     */\n    @Deprecated\n    public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;\n\n    /**\n     * Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED}\n     * state. All other Fragments are capped at {@link Lifecycle.State#STARTED}.\n     *\n     * @see #FragmentPagerAdapter(FragmentManager, int)\n     */\n    public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;\n\n    private final FragmentManager mFragmentManager;\n    private final int mBehavior;\n    private FragmentTransaction mCurTransaction = null;\n    private Fragment mCurrentPrimaryItem = null;\n\n    /**\n     * Constructor for {@link FragmentPagerAdapter} that sets the fragment manager for the adapter.\n     * This is the equivalent of calling {@link #FragmentPagerAdapter(FragmentManager, int)} and\n     * passing in {@link #BEHAVIOR_SET_USER_VISIBLE_HINT}.\n     *\n     * <p>Fragments will have {@link Fragment#setUserVisibleHint(boolean)} called whenever the\n     * current Fragment changes.</p>\n     *\n     * @param fm fragment manager that will interact with this adapter\n     * @deprecated use {@link #FragmentPagerAdapter(FragmentManager, int)} with\n     * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}\n     */\n    @Deprecated\n    public FragmentPagerAdapter(@NonNull FragmentManager fm) {\n        this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);\n    }\n\n    /**\n     * Constructor for {@link FragmentPagerAdapter}.\n     *\n     * If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current\n     * Fragment is in the {@link Lifecycle.State#RESUMED} state. All other fragments are capped at\n     * {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is passed, all\n     * fragments are in the {@link Lifecycle.State#RESUMED} state and there will be callbacks to\n     * {@link Fragment#setUserVisibleHint(boolean)}.\n     *\n     * @param fm fragment manager that will interact with this adapter\n     * @param behavior determines if only current fragments are in a resumed state\n     */\n    public FragmentPagerAdapter(@NonNull FragmentManager fm,\n            @Behavior int behavior) {\n        mFragmentManager = fm;\n        mBehavior = behavior;\n    }\n\n    /**\n     * Return the Fragment associated with a specified position.\n     */\n    @NonNull\n    public abstract Fragment getItem(int position);\n\n    @Override\n    public void startUpdate(@NonNull ViewGroup container) {\n        if (container.getId() == View.NO_ID) {\n            throw new IllegalStateException(\"ViewPager with adapter \" + this\n                    + \" requires a view id\");\n        }\n    }\n\n    @SuppressWarnings({\"ReferenceEquality\", \"deprecation\"})\n    @NonNull\n    @Override\n    public Object instantiateItem(@NonNull ViewGroup container, int position) {\n        if (mCurTransaction == null) {\n            mCurTransaction = mFragmentManager.beginTransaction();\n        }\n\n        final long itemId = getItemId(position);\n\n        // Do we already have this fragment?\n        String name = makeFragmentName(container.getId(), itemId);\n        Fragment fragment = mFragmentManager.findFragmentByTag(name);\n        if (fragment != null) {\n            if (DEBUG) Log.v(TAG, \"Attaching item #\" + itemId + \": f=\" + fragment);\n            mCurTransaction.attach(fragment);\n        } else {\n            fragment = getItem(position);\n            if (DEBUG) Log.v(TAG, \"Adding item #\" + itemId + \": f=\" + fragment);\n            mCurTransaction.add(container.getId(), fragment,\n                    makeFragmentName(container.getId(), itemId));\n        }\n        if (fragment != mCurrentPrimaryItem) {\n            fragment.setMenuVisibility(false);\n            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {\n                mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);\n            } else {\n                fragment.setUserVisibleHint(false);\n            }\n        }\n\n        return fragment;\n    }\n\n    @Override\n    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {\n        Fragment fragment = (Fragment) object;\n\n        if (mCurTransaction == null) {\n            mCurTransaction = mFragmentManager.beginTransaction();\n        }\n        if (DEBUG) Log.v(TAG, \"Detaching item #\" + getItemId(position) + \": f=\" + object\n                + \" v=\" + (fragment.getView()));\n        mCurTransaction.detach(fragment);\n        if (fragment == mCurrentPrimaryItem) {\n            mCurrentPrimaryItem = null;\n        }\n    }\n\n    @SuppressWarnings({\"ReferenceEquality\", \"deprecation\"})\n    @Override\n    public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {\n        Fragment fragment = (Fragment)object;\n        if (fragment != mCurrentPrimaryItem) {\n            if (mCurrentPrimaryItem != null) {\n                mCurrentPrimaryItem.setMenuVisibility(false);\n                if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {\n                    if (mCurTransaction == null) {\n                        mCurTransaction = mFragmentManager.beginTransaction();\n                    }\n                    mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);\n                } else {\n                    mCurrentPrimaryItem.setUserVisibleHint(false);\n                }\n            }\n            fragment.setMenuVisibility(true);\n            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {\n                if (mCurTransaction == null) {\n                    mCurTransaction = mFragmentManager.beginTransaction();\n                }\n                mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);\n            } else {\n                fragment.setUserVisibleHint(true);\n            }\n\n            mCurrentPrimaryItem = fragment;\n        }\n    }\n\n    @Override\n    public void finishUpdate(@NonNull ViewGroup container) {\n        if (mCurTransaction != null) {\n            mCurTransaction.commitNowAllowingStateLoss();\n            mCurTransaction = null;\n        }\n    }\n\n    @Override\n    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {\n        return ((Fragment)object).getView() == view;\n    }\n\n    @Override\n    @Nullable\n    public Parcelable saveState() {\n        return null;\n    }\n\n    @Override\n    public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) {\n    }\n\n    /**\n     * Return a unique identifier for the item at the given position.\n     *\n     * <p>The default implementation returns the given position.\n     * Subclasses should override this method if the positions of items can change.</p>\n     *\n     * @param position Position within this adapter\n     * @return Unique identifier for the item at position\n     */\n    public long getItemId(int position) {\n        return position;\n    }\n\n    private static String makeFragmentName(int viewId, long id) {\n        return \"android:switcher:\" + viewId + \":\" + id;\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentState.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.annotation.SuppressLint;\nimport android.os.Bundle;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.Lifecycle;\n\n@SuppressLint(\"BanParcelableUsage\")\nfinal class FragmentState implements Parcelable {\n    final String mClassName;\n    final String mWho;\n    final boolean mFromLayout;\n    final int mFragmentId;\n    final int mContainerId;\n    final String mTag;\n    final boolean mRetainInstance;\n    final boolean mRemoving;\n    final boolean mDetached;\n    final Bundle mArguments;\n    final boolean mHidden;\n    final int mMaxLifecycleState;\n\n    Bundle mSavedFragmentState;\n\n    Fragment mInstance;\n\n    FragmentState(Fragment frag) {\n        mClassName = frag.getClass().getName();\n        mWho = frag.mWho;\n        mFromLayout = frag.mFromLayout;\n        mFragmentId = frag.mFragmentId;\n        mContainerId = frag.mContainerId;\n        mTag = frag.mTag;\n        mRetainInstance = frag.mRetainInstance;\n        mRemoving = frag.mRemoving;\n        mDetached = frag.mDetached;\n        mArguments = frag.mArguments;\n        mHidden = frag.mHidden;\n        mMaxLifecycleState = frag.mMaxState.ordinal();\n    }\n\n    FragmentState(Parcel in) {\n        mClassName = in.readString();\n        mWho = in.readString();\n        mFromLayout = in.readInt() != 0;\n        mFragmentId = in.readInt();\n        mContainerId = in.readInt();\n        mTag = in.readString();\n        mRetainInstance = in.readInt() != 0;\n        mRemoving = in.readInt() != 0;\n        mDetached = in.readInt() != 0;\n        mArguments = in.readBundle();\n        mHidden = in.readInt() != 0;\n        mSavedFragmentState = in.readBundle();\n        mMaxLifecycleState = in.readInt();\n    }\n\n    public Fragment instantiate(@NonNull ClassLoader classLoader,\n            @NonNull FragmentFactory factory) {\n        if (mInstance == null) {\n            if (mArguments != null) {\n                mArguments.setClassLoader(classLoader);\n            }\n\n            mInstance = factory.instantiate(classLoader, mClassName);\n            mInstance.setArguments(mArguments);\n\n            if (mSavedFragmentState != null) {\n                mSavedFragmentState.setClassLoader(classLoader);\n                mInstance.mSavedFragmentState = mSavedFragmentState;\n            } else {\n                // When restoring a Fragment, always ensure we have a\n                // non-null Bundle so that developers have a signal for\n                // when the Fragment is being restored\n                mInstance.mSavedFragmentState = new Bundle();\n            }\n            mInstance.mWho = mWho;\n            mInstance.mFromLayout = mFromLayout;\n            mInstance.mRestored = true;\n            mInstance.mFragmentId = mFragmentId;\n            mInstance.mContainerId = mContainerId;\n            mInstance.mTag = mTag;\n            mInstance.mRetainInstance = mRetainInstance;\n            mInstance.mRemoving = mRemoving;\n            mInstance.mDetached = mDetached;\n            mInstance.mHidden = mHidden;\n            mInstance.mMaxState = Lifecycle.State.values()[mMaxLifecycleState];\n\n            if (FragmentManagerImpl.DEBUG) {\n                Log.v(FragmentManagerImpl.TAG, \"Instantiated fragment \" + mInstance);\n            }\n        }\n        return mInstance;\n    }\n\n    @NonNull\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder(128);\n        sb.append(\"FragmentState{\");\n        sb.append(mClassName);\n        sb.append(\" (\");\n        sb.append(mWho);\n        sb.append(\")}:\");\n        if (mFromLayout) {\n            sb.append(\" fromLayout\");\n        }\n        if (mContainerId != 0) {\n            sb.append(\" id=0x\");\n            sb.append(Integer.toHexString(mContainerId));\n        }\n        if (mTag != null && !mTag.isEmpty()) {\n            sb.append(\" tag=\");\n            sb.append(mTag);\n        }\n        if (mRetainInstance) {\n            sb.append(\" retainInstance\");\n        }\n        if (mRemoving) {\n            sb.append(\" removing\");\n        }\n        if (mDetached) {\n            sb.append(\" detached\");\n        }\n        if (mHidden) {\n            sb.append(\" hidden\");\n        }\n        return sb.toString();\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.writeString(mClassName);\n        dest.writeString(mWho);\n        dest.writeInt(mFromLayout ? 1 : 0);\n        dest.writeInt(mFragmentId);\n        dest.writeInt(mContainerId);\n        dest.writeString(mTag);\n        dest.writeInt(mRetainInstance ? 1 : 0);\n        dest.writeInt(mRemoving ? 1 : 0);\n        dest.writeInt(mDetached ? 1 : 0);\n        dest.writeBundle(mArguments);\n        dest.writeInt(mHidden ? 1 : 0);\n        dest.writeBundle(mSavedFragmentState);\n        dest.writeInt(mMaxLifecycleState);\n    }\n\n    public static final Parcelable.Creator<FragmentState> CREATOR =\n            new Parcelable.Creator<FragmentState>() {\n                @Override\n                public FragmentState createFromParcel(Parcel in) {\n                    return new FragmentState(in);\n                }\n\n                @Override\n                public FragmentState[] newArray(int size) {\n                    return new FragmentState[size];\n                }\n            };\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentStatePagerAdapter.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.os.Bundle;\nimport android.os.Parcelable;\nimport android.util.Log;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.Lifecycle;\nimport androidx.viewpager.widget.PagerAdapter;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\n\n/**\n * Implementation of {@link PagerAdapter} that\n * uses a {@link Fragment} to manage each page. This class also handles\n * saving and restoring of fragment's state.\n *\n * <p>This version of the pager is more useful when there are a large number\n * of pages, working more like a list view.  When pages are not visible to\n * the user, their entire fragment may be destroyed, only keeping the saved\n * state of that fragment.  This allows the pager to hold on to much less\n * memory associated with each visited page as compared to\n * {@link FragmentPagerAdapter} at the cost of potentially more overhead when\n * switching between pages.\n *\n * <p>When using FragmentPagerAdapter the host ViewPager must have a\n * valid ID set.</p>\n *\n * <p>Subclasses only need to implement {@link #getItem(int)}\n * and {@link #getCount()} to have a working adapter.\n *\n * <p>Here is an example implementation of a pager containing fragments of\n * lists:\n *\n * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentStatePagerSupport.java\n *      complete}\n *\n * <p>The <code>R.layout.fragment_pager</code> resource of the top-level fragment is:\n *\n * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager.xml\n *      complete}\n *\n * <p>The <code>R.layout.fragment_pager_list</code> resource containing each\n * individual fragment's layout is:\n *\n * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager_list.xml\n *      complete}\n */\n@SuppressWarnings(\"deprecation\")\npublic abstract class FragmentStatePagerAdapter extends PagerAdapter {\n    private static final String TAG = \"FragmentStatePagerAdapt\";\n    private static final boolean DEBUG = false;\n\n    @Retention(RetentionPolicy.SOURCE)\n    @IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})\n    private @interface Behavior { }\n\n    /**\n     * Indicates that {@link Fragment#setUserVisibleHint(boolean)} will be called when the current\n     * fragment changes.\n     *\n     * @deprecated This behavior relies on the deprecated\n     * {@link Fragment#setUserVisibleHint(boolean)} API. Use\n     * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} to switch to its replacement,\n     * {@link FragmentTransaction#setMaxLifecycle}.\n     * @see #FragmentStatePagerAdapter(FragmentManager, int)\n     */\n    @Deprecated\n    public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;\n\n    /**\n     * Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED}\n     * state. All other Fragments are capped at {@link Lifecycle.State#STARTED}.\n     *\n     * @see #FragmentStatePagerAdapter(FragmentManager, int)\n     */\n    public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;\n\n    private final FragmentManager mFragmentManager;\n    private final int mBehavior;\n    private FragmentTransaction mCurTransaction = null;\n\n    private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();\n    private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();\n    private Fragment mCurrentPrimaryItem = null;\n\n    /**\n     * Constructor for {@link FragmentStatePagerAdapter} that sets the fragment manager for the\n     * adapter. This is the equivalent of calling\n     * {@link #FragmentStatePagerAdapter(FragmentManager, int)} and passing in\n     * {@link #BEHAVIOR_SET_USER_VISIBLE_HINT}.\n     *\n     * <p>Fragments will have {@link Fragment#setUserVisibleHint(boolean)} called whenever the\n     * current Fragment changes.</p>\n     *\n     * @param fm fragment manager that will interact with this adapter\n     * @deprecated use {@link #FragmentStatePagerAdapter(FragmentManager, int)} with\n     * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}\n     */\n    @Deprecated\n    public FragmentStatePagerAdapter(@NonNull FragmentManager fm) {\n        this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);\n    }\n\n    /**\n     * Constructor for {@link FragmentStatePagerAdapter}.\n     *\n     * If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current\n     * Fragment is in the {@link Lifecycle.State#RESUMED} state, while all other fragments are\n     * capped at {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is\n     * passed, all fragments are in the {@link Lifecycle.State#RESUMED} state and there will be\n     * callbacks to {@link Fragment#setUserVisibleHint(boolean)}.\n     *\n     * @param fm fragment manager that will interact with this adapter\n     * @param behavior determines if only current fragments are in a resumed state\n     */\n    public FragmentStatePagerAdapter(@NonNull FragmentManager fm,\n            @Behavior int behavior) {\n        mFragmentManager = fm;\n        mBehavior = behavior;\n    }\n\n    /**\n     * Return the Fragment associated with a specified position.\n     */\n    @NonNull\n    public abstract Fragment getItem(int position);\n\n    @Override\n    public void startUpdate(@NonNull ViewGroup container) {\n        if (container.getId() == View.NO_ID) {\n            throw new IllegalStateException(\"ViewPager with adapter \" + this\n                    + \" requires a view id\");\n        }\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    @NonNull\n    @Override\n    public Object instantiateItem(@NonNull ViewGroup container, int position) {\n        // If we already have this item instantiated, there is nothing\n        // to do.  This can happen when we are restoring the entire pager\n        // from its saved state, where the fragment manager has already\n        // taken care of restoring the fragments we previously had instantiated.\n        if (mFragments.size() > position) {\n            Fragment f = mFragments.get(position);\n            if (f != null) {\n                return f;\n            }\n        }\n\n        if (mCurTransaction == null) {\n            mCurTransaction = mFragmentManager.beginTransaction();\n        }\n\n        Fragment fragment = getItem(position);\n        if (DEBUG) Log.v(TAG, \"Adding item #\" + position + \": f=\" + fragment);\n        if (mSavedState.size() > position) {\n            Fragment.SavedState fss = mSavedState.get(position);\n            if (fss != null) {\n                fragment.setInitialSavedState(fss);\n            }\n        }\n        while (mFragments.size() <= position) {\n            mFragments.add(null);\n        }\n        fragment.setMenuVisibility(false);\n        if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) {\n            fragment.setUserVisibleHint(false);\n        }\n\n        mFragments.set(position, fragment);\n        mCurTransaction.add(container.getId(), fragment);\n\n        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {\n            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);\n        }\n\n        return fragment;\n    }\n\n    @Override\n    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {\n        Fragment fragment = (Fragment) object;\n\n        if (mCurTransaction == null) {\n            mCurTransaction = mFragmentManager.beginTransaction();\n        }\n        if (DEBUG) Log.v(TAG, \"Removing item #\" + position + \": f=\" + object\n                + \" v=\" + ((Fragment)object).getView());\n        while (mSavedState.size() <= position) {\n            mSavedState.add(null);\n        }\n        mSavedState.set(position, fragment.isAdded()\n                ? mFragmentManager.saveFragmentInstanceState(fragment) : null);\n        mFragments.set(position, null);\n\n        mCurTransaction.remove(fragment);\n        if (fragment == mCurrentPrimaryItem) {\n            mCurrentPrimaryItem = null;\n        }\n    }\n\n    @Override\n    @SuppressWarnings({\"ReferenceEquality\", \"deprecation\"})\n    public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {\n        Fragment fragment = (Fragment)object;\n        if (fragment != mCurrentPrimaryItem) {\n            if (mCurrentPrimaryItem != null) {\n                mCurrentPrimaryItem.setMenuVisibility(false);\n                if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {\n                    if (mCurTransaction == null) {\n                        mCurTransaction = mFragmentManager.beginTransaction();\n                    }\n                    mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);\n                } else {\n                    mCurrentPrimaryItem.setUserVisibleHint(false);\n                }\n            }\n            fragment.setMenuVisibility(true);\n            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {\n                if (mCurTransaction == null) {\n                    mCurTransaction = mFragmentManager.beginTransaction();\n                }\n                mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);\n            } else {\n                fragment.setUserVisibleHint(true);\n            }\n\n            mCurrentPrimaryItem = fragment;\n        }\n    }\n\n    @Override\n    public void finishUpdate(@NonNull ViewGroup container) {\n        if (mCurTransaction != null) {\n            mCurTransaction.commitNowAllowingStateLoss();\n            mCurTransaction = null;\n        }\n    }\n\n    @Override\n    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {\n        return ((Fragment)object).getView() == view;\n    }\n\n    @Override\n    @Nullable\n    public Parcelable saveState() {\n        Bundle state = null;\n        if (mSavedState.size() > 0) {\n            state = new Bundle();\n            Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];\n            mSavedState.toArray(fss);\n            state.putParcelableArray(\"states\", fss);\n        }\n        for (int i=0; i<mFragments.size(); i++) {\n            Fragment f = mFragments.get(i);\n            if (f != null && f.isAdded()) {\n                if (state == null) {\n                    state = new Bundle();\n                }\n                String key = \"f\" + i;\n                mFragmentManager.putFragment(state, key, f);\n            }\n        }\n        return state;\n    }\n\n    @Override\n    public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) {\n        if (state != null) {\n            Bundle bundle = (Bundle)state;\n            bundle.setClassLoader(loader);\n            Parcelable[] fss = bundle.getParcelableArray(\"states\");\n            mSavedState.clear();\n            mFragments.clear();\n            if (fss != null) {\n                for (int i=0; i<fss.length; i++) {\n                    mSavedState.add((Fragment.SavedState)fss[i]);\n                }\n            }\n            Iterable<String> keys = bundle.keySet();\n            for (String key: keys) {\n                if (key.startsWith(\"f\")) {\n                    int index = Integer.parseInt(key.substring(1));\n                    Fragment f = mFragmentManager.getFragment(bundle, key);\n                    if (f != null) {\n                        while (mFragments.size() <= index) {\n                            mFragments.add(null);\n                        }\n                        f.setMenuVisibility(false);\n                        mFragments.set(index, f);\n                    } else {\n                        Log.w(TAG, \"Bad fragment at key \" + key);\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTabHost.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.os.Bundle;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\nimport android.widget.LinearLayout;\nimport android.widget.TabHost;\nimport android.widget.TabWidget;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.util.ArrayList;\n\n/**\n * Special TabHost that allows the use of {@link Fragment} objects for\n * its tab content.  When placing this in a view hierarchy, after inflating\n * the hierarchy you must call {@link #setup(Context, FragmentManager, int)}\n * to complete the initialization of the tab host.\n *\n * @deprecated Use <a href=\"https://developer.android.com/guide/navigation/navigation-swipe-view \">\n *  TabLayout and ViewPager</a> instead.\n */\n@Deprecated\npublic class FragmentTabHost extends TabHost\n        implements TabHost.OnTabChangeListener {\n    private final ArrayList<TabInfo> mTabs = new ArrayList<>();\n\n    private FrameLayout mRealTabContent;\n    private Context mContext;\n    private FragmentManager mFragmentManager;\n    private int mContainerId;\n    private TabHost.OnTabChangeListener mOnTabChangeListener;\n    private TabInfo mLastTab;\n    private boolean mAttached;\n\n    static final class TabInfo {\n        final @NonNull String tag;\n        final @NonNull Class<?> clss;\n        final @Nullable Bundle args;\n        Fragment fragment;\n\n        TabInfo(@NonNull String _tag, @NonNull Class<?> _class, @Nullable Bundle _args) {\n            tag = _tag;\n            clss = _class;\n            args = _args;\n        }\n    }\n\n    static class DummyTabFactory implements TabHost.TabContentFactory {\n        private final Context mContext;\n\n        public DummyTabFactory(Context context) {\n            mContext = context;\n        }\n\n        @Override\n        public View createTabContent(String tag) {\n            View v = new View(mContext);\n            v.setMinimumWidth(0);\n            v.setMinimumHeight(0);\n            return v;\n        }\n    }\n\n    static class SavedState extends BaseSavedState {\n        String curTab;\n\n        SavedState(Parcelable superState) {\n            super(superState);\n        }\n\n        SavedState(Parcel in) {\n            super(in);\n            curTab = in.readString();\n        }\n\n        @Override\n        public void writeToParcel(Parcel out, int flags) {\n            super.writeToParcel(out, flags);\n            out.writeString(curTab);\n        }\n\n        @Override\n        public String toString() {\n            return \"FragmentTabHost.SavedState{\"\n                    + Integer.toHexString(System.identityHashCode(this))\n                    + \" curTab=\" + curTab + \"}\";\n        }\n\n        public static final Parcelable.Creator<SavedState> CREATOR\n                = new Parcelable.Creator<SavedState>() {\n            @Override\n            public SavedState createFromParcel(Parcel in) {\n                return new SavedState(in);\n            }\n\n            @Override\n            public SavedState[] newArray(int size) {\n                return new SavedState[size];\n            }\n        };\n    }\n\n    /**\n     * @deprecated Use\n     * <a href=\"https://developer.android.com/guide/navigation/navigation-swipe-view \">\n     *  TabLayout and ViewPager</a> instead.\n     */\n    @Deprecated\n    public FragmentTabHost(@NonNull Context context) {\n        // Note that we call through to the version that takes an AttributeSet,\n        // because the simple Context construct can result in a broken object!\n        super(context, null);\n        initFragmentTabHost(context, null);\n    }\n\n    /**\n     * @deprecated Use\n     * <a href=\"https://developer.android.com/guide/navigation/navigation-swipe-view \">\n     *  TabLayout and ViewPager</a> instead.\n     */\n    @Deprecated\n    public FragmentTabHost(@NonNull Context context, @Nullable AttributeSet attrs) {\n        super(context, attrs);\n        initFragmentTabHost(context, attrs);\n    }\n\n    private void initFragmentTabHost(Context context, AttributeSet attrs) {\n        final TypedArray a = context.obtainStyledAttributes(attrs,\n                new int[] { android.R.attr.inflatedId }, 0, 0);\n        mContainerId = a.getResourceId(0, 0);\n        a.recycle();\n\n        super.setOnTabChangedListener(this);\n    }\n\n    private void ensureHierarchy(Context context) {\n        // If owner hasn't made its own view hierarchy, then as a convenience\n        // we will construct a standard one here.\n        if (findViewById(android.R.id.tabs) == null) {\n            LinearLayout ll = new LinearLayout(context);\n            ll.setOrientation(LinearLayout.VERTICAL);\n            addView(ll, new FrameLayout.LayoutParams(\n                    ViewGroup.LayoutParams.MATCH_PARENT,\n                    ViewGroup.LayoutParams.MATCH_PARENT));\n\n            TabWidget tw = new TabWidget(context);\n            tw.setId(android.R.id.tabs);\n            tw.setOrientation(TabWidget.HORIZONTAL);\n            ll.addView(tw, new LinearLayout.LayoutParams(\n                    ViewGroup.LayoutParams.MATCH_PARENT,\n                    ViewGroup.LayoutParams.WRAP_CONTENT, 0));\n\n            FrameLayout fl = new FrameLayout(context);\n            fl.setId(android.R.id.tabcontent);\n            ll.addView(fl, new LinearLayout.LayoutParams(0, 0, 0));\n\n            mRealTabContent = fl = new FrameLayout(context);\n            mRealTabContent.setId(mContainerId);\n            ll.addView(fl, new LinearLayout.LayoutParams(\n                    LinearLayout.LayoutParams.MATCH_PARENT, 0, 1));\n        }\n    }\n\n    /**\n     * @deprecated Use\n     * <a href=\"https://developer.android.com/guide/navigation/navigation-swipe-view \">\n     *  TabLayout and ViewPager</a> instead.\n     */\n    @Override @Deprecated\n    public void setup() {\n        throw new IllegalStateException(\n                \"Must call setup() that takes a Context and FragmentManager\");\n    }\n\n    /**\n     * Set up the FragmentTabHost to use the given FragmentManager\n     *\n     * @deprecated Use\n     * <a href=\"https://developer.android.com/guide/navigation/navigation-swipe-view \">\n     *  TabLayout and ViewPager</a> instead.\n     */\n    @Deprecated\n    public void setup(@NonNull Context context, @NonNull FragmentManager manager) {\n        ensureHierarchy(context);  // Ensure views required by super.setup()\n        super.setup();\n        mContext = context;\n        mFragmentManager = manager;\n        ensureContent();\n    }\n\n    /**\n     * Set up the FragmentTabHost to use the given FragmentManager\n     *\n     * @deprecated Use\n     * <a href=\"https://developer.android.com/guide/navigation/navigation-swipe-view \">\n     *  TabLayout and ViewPager</a> instead.\n     */\n    @Deprecated\n    public void setup(@NonNull Context context, @NonNull FragmentManager manager,\n            int containerId) {\n        ensureHierarchy(context);  // Ensure views required by super.setup()\n        super.setup();\n        mContext = context;\n        mFragmentManager = manager;\n        mContainerId = containerId;\n        ensureContent();\n        mRealTabContent.setId(containerId);\n\n        // We must have an ID to be able to save/restore our state.  If\n        // the owner hasn't set one at this point, we will set it ourselves.\n        if (getId() == View.NO_ID) {\n            setId(android.R.id.tabhost);\n        }\n    }\n\n    private void ensureContent() {\n        if (mRealTabContent == null) {\n            mRealTabContent = (FrameLayout)findViewById(mContainerId);\n            if (mRealTabContent == null) {\n                throw new IllegalStateException(\n                        \"No tab content FrameLayout found for id \" + mContainerId);\n            }\n        }\n    }\n\n    /**\n     * @deprecated Use\n     * <a href=\"https://developer.android.com/guide/navigation/navigation-swipe-view \">\n     *  TabLayout and ViewPager</a> instead.\n     */\n    @Deprecated\n    @Override\n    public void setOnTabChangedListener(@Nullable OnTabChangeListener l) {\n        mOnTabChangeListener = l;\n    }\n\n    /**\n     * @deprecated Use\n     * <a href=\"https://developer.android.com/guide/navigation/navigation-swipe-view \">\n     *  TabLayout and ViewPager</a> instead.\n     */\n    @Deprecated\n    public void addTab(@NonNull TabHost.TabSpec tabSpec, @NonNull Class<?> clss,\n            @Nullable Bundle args) {\n        tabSpec.setContent(new DummyTabFactory(mContext));\n\n        final String tag = tabSpec.getTag();\n        final TabInfo info = new TabInfo(tag, clss, args);\n\n        if (mAttached) {\n            // If we are already attached to the window, then check to make\n            // sure this tab's fragment is inactive if it exists.  This shouldn't\n            // normally happen.\n            info.fragment = mFragmentManager.findFragmentByTag(tag);\n            if (info.fragment != null && !info.fragment.isDetached()) {\n                final FragmentTransaction ft = mFragmentManager.beginTransaction();\n                ft.detach(info.fragment);\n                ft.commit();\n            }\n        }\n\n        mTabs.add(info);\n        addTab(tabSpec);\n    }\n\n    /**\n     * @deprecated Use\n     * <a href=\"https://developer.android.com/guide/navigation/navigation-swipe-view \">\n     *  TabLayout and ViewPager</a> instead.\n     */\n    @Deprecated\n    @Override\n    protected void onAttachedToWindow() {\n        super.onAttachedToWindow();\n\n        final String currentTag = getCurrentTabTag();\n\n        // Go through all tabs and make sure their fragments match\n        // the correct state.\n        FragmentTransaction ft = null;\n        for (int i = 0, count = mTabs.size(); i < count; i++) {\n            final TabInfo tab = mTabs.get(i);\n            tab.fragment = mFragmentManager.findFragmentByTag(tab.tag);\n            if (tab.fragment != null && !tab.fragment.isDetached()) {\n                if (tab.tag.equals(currentTag)) {\n                    // The fragment for this tab is already there and\n                    // active, and it is what we really want to have\n                    // as the current tab.  Nothing to do.\n                    mLastTab = tab;\n                } else {\n                    // This fragment was restored in the active state,\n                    // but is not the current tab.  Deactivate it.\n                    if (ft == null) {\n                        ft = mFragmentManager.beginTransaction();\n                    }\n                    ft.detach(tab.fragment);\n                }\n            }\n        }\n\n        // We are now ready to go.  Make sure we are switched to the\n        // correct tab.\n        mAttached = true;\n        ft = doTabChanged(currentTag, ft);\n        if (ft != null) {\n            ft.commit();\n            mFragmentManager.executePendingTransactions();\n        }\n    }\n\n    /**\n     * @deprecated Use\n     * <a href=\"https://developer.android.com/guide/navigation/navigation-swipe-view \">\n     *  TabLayout and ViewPager</a> instead.\n     */\n    @Deprecated\n    @Override\n    protected void onDetachedFromWindow() {\n        super.onDetachedFromWindow();\n        mAttached = false;\n    }\n\n    /**\n     * @deprecated Use\n     * <a href=\"https://developer.android.com/guide/navigation/navigation-swipe-view \">\n     *  TabLayout and ViewPager</a> instead.\n     */\n    @Deprecated\n    @Override\n    @NonNull\n    protected Parcelable onSaveInstanceState() {\n        Parcelable superState = super.onSaveInstanceState();\n        SavedState ss = new SavedState(superState);\n        ss.curTab = getCurrentTabTag();\n        return ss;\n    }\n\n    /**\n     * @deprecated Use\n     * <a href=\"https://developer.android.com/guide/navigation/navigation-swipe-view \">\n     *  TabLayout and ViewPager</a> instead.\n     */\n    @Deprecated\n    @Override\n    protected void onRestoreInstanceState(@SuppressLint(\"UnknownNullness\") Parcelable state) {\n        if (!(state instanceof SavedState)) {\n            super.onRestoreInstanceState(state);\n            return;\n        }\n        SavedState ss = (SavedState) state;\n        super.onRestoreInstanceState(ss.getSuperState());\n        setCurrentTabByTag(ss.curTab);\n    }\n\n    /**\n     * @deprecated Use\n     * <a href=\"https://developer.android.com/guide/navigation/navigation-swipe-view \">\n     *  TabLayout and ViewPager</a> instead.\n     */\n    @Deprecated\n    @Override\n    public void onTabChanged(@Nullable String tabId) {\n        if (mAttached) {\n            final FragmentTransaction ft = doTabChanged(tabId, null);\n            if (ft != null) {\n                ft.commit();\n            }\n        }\n        if (mOnTabChangeListener != null) {\n            mOnTabChangeListener.onTabChanged(tabId);\n        }\n    }\n\n    @Nullable\n    private FragmentTransaction doTabChanged(@Nullable String tag,\n            @Nullable FragmentTransaction ft) {\n        final TabInfo newTab = getTabInfoForTag(tag);\n        if (mLastTab != newTab) {\n            if (ft == null) {\n                ft = mFragmentManager.beginTransaction();\n            }\n\n            if (mLastTab != null) {\n                if (mLastTab.fragment != null) {\n                    ft.detach(mLastTab.fragment);\n                }\n            }\n\n            if (newTab != null) {\n                if (newTab.fragment == null) {\n                    newTab.fragment = mFragmentManager.getFragmentFactory().instantiate(\n                            mContext.getClassLoader(), newTab.clss.getName());\n                    newTab.fragment.setArguments(newTab.args);\n                    ft.add(mContainerId, newTab.fragment, newTab.tag);\n                } else {\n                    ft.attach(newTab.fragment);\n                }\n            }\n\n            mLastTab = newTab;\n        }\n\n        return ft;\n    }\n\n    @Nullable\n    private TabInfo getTabInfoForTag(String tabId) {\n        for (int i = 0, count = mTabs.size(); i < count; i++) {\n            final TabInfo tab = mTabs.get(i);\n            if (tab.tag.equals(tabId)) {\n                return tab;\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransaction.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;\n\nimport android.os.Bundle;\nimport android.view.View;\n\nimport androidx.annotation.AnimRes;\nimport androidx.annotation.AnimatorRes;\nimport androidx.annotation.IdRes;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RestrictTo;\nimport androidx.annotation.StringRes;\nimport androidx.annotation.StyleRes;\nimport androidx.core.view.ViewCompat;\nimport androidx.lifecycle.Lifecycle;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.reflect.Modifier;\nimport java.util.ArrayList;\n\n/**\n * Static library support version of the framework's {@link android.app.FragmentTransaction}.\n * Used to write apps that run on platforms prior to Android 3.0.  When running\n * on Android 3.0 or above, this implementation is still used; it does not try\n * to switch to the framework's implementation.  See the framework SDK\n * documentation for a class overview.\n */\npublic abstract class FragmentTransaction {\n\n    static final int OP_NULL = 0;\n    static final int OP_ADD = 1;\n    static final int OP_REPLACE = 2;\n    static final int OP_REMOVE = 3;\n    static final int OP_HIDE = 4;\n    static final int OP_SHOW = 5;\n    static final int OP_DETACH = 6;\n    static final int OP_ATTACH = 7;\n    static final int OP_SET_PRIMARY_NAV = 8;\n    static final int OP_UNSET_PRIMARY_NAV = 9;\n    static final int OP_SET_MAX_LIFECYCLE = 10;\n\n    static final class Op {\n        int mCmd;\n        Fragment mFragment;\n        int mEnterAnim;\n        int mExitAnim;\n        int mPopEnterAnim;\n        int mPopExitAnim;\n        Lifecycle.State mOldMaxState;\n        Lifecycle.State mCurrentMaxState;\n\n        Op() {\n        }\n\n        Op(int cmd, Fragment fragment) {\n            this.mCmd = cmd;\n            this.mFragment = fragment;\n            this.mOldMaxState = Lifecycle.State.RESUMED;\n            this.mCurrentMaxState = Lifecycle.State.RESUMED;\n        }\n\n        Op(int cmd, @NonNull Fragment fragment, Lifecycle.State state) {\n            this.mCmd = cmd;\n            this.mFragment = fragment;\n            this.mOldMaxState = fragment.mMaxState;\n            this.mCurrentMaxState = state;\n        }\n    }\n\n    ArrayList<Op> mOps = new ArrayList<>();\n    int mEnterAnim;\n    int mExitAnim;\n    int mPopEnterAnim;\n    int mPopExitAnim;\n    int mTransition;\n    int mTransitionStyle;\n    boolean mAddToBackStack;\n    boolean mAllowAddToBackStack = true;\n    @Nullable String mName;\n\n    int mBreadCrumbTitleRes;\n    CharSequence mBreadCrumbTitleText;\n    int mBreadCrumbShortTitleRes;\n    CharSequence mBreadCrumbShortTitleText;\n\n    ArrayList<String> mSharedElementSourceNames;\n    ArrayList<String> mSharedElementTargetNames;\n    boolean mReorderingAllowed = false;\n\n    ArrayList<Runnable> mCommitRunnables;\n\n    void addOp(Op op) {\n        mOps.add(op);\n        op.mEnterAnim = mEnterAnim;\n        op.mExitAnim = mExitAnim;\n        op.mPopEnterAnim = mPopEnterAnim;\n        op.mPopExitAnim = mPopExitAnim;\n    }\n\n    /**\n     * Calls {@link #add(int, Fragment, String)} with a 0 containerViewId.\n     */\n    @NonNull\n    public FragmentTransaction add(@NonNull Fragment fragment, @Nullable String tag)  {\n        doAddOp(0, fragment, tag, OP_ADD);\n        return this;\n    }\n\n    /**\n     * Calls {@link #add(int, Fragment, String)} with a null tag.\n     */\n    @NonNull\n    public FragmentTransaction add(@IdRes int containerViewId, @NonNull Fragment fragment) {\n        doAddOp(containerViewId, fragment, null, OP_ADD);\n        return this;\n    }\n\n    /**\n     * Add a fragment to the activity state.  This fragment may optionally\n     * also have its view (if {@link Fragment#onCreateView Fragment.onCreateView}\n     * returns non-null) into a container view of the activity.\n     *\n     * @param containerViewId Optional identifier of the container this fragment is\n     * to be placed in.  If 0, it will not be placed in a container.\n     * @param fragment The fragment to be added.  This fragment must not already\n     * be added to the activity.\n     * @param tag Optional tag name for the fragment, to later retrieve the\n     * fragment with {@link FragmentManager#findFragmentByTag(String)\n     * FragmentManager.findFragmentByTag(String)}.\n     *\n     * @return Returns the same FragmentTransaction instance.\n     */\n    @NonNull\n    public FragmentTransaction add(@IdRes int containerViewId, @NonNull Fragment fragment,\n            @Nullable String tag) {\n        doAddOp(containerViewId, fragment, tag, OP_ADD);\n        return this;\n    }\n\n    void doAddOp(int containerViewId, Fragment fragment, @Nullable String tag, int opcmd) {\n        final Class<?> fragmentClass = fragment.getClass();\n        final int modifiers = fragmentClass.getModifiers();\n        if (fragmentClass.isAnonymousClass() || !Modifier.isPublic(modifiers)\n                || (fragmentClass.isMemberClass() && !Modifier.isStatic(modifiers))) {\n            throw new IllegalStateException(\"Fragment \" + fragmentClass.getCanonicalName()\n                    + \" must be a public static class to be  properly recreated from\"\n                    + \" instance state.\");\n        }\n\n        if (tag != null) {\n            if (fragment.mTag != null && !tag.equals(fragment.mTag)) {\n                throw new IllegalStateException(\"Can't change tag of fragment \"\n                        + fragment + \": was \" + fragment.mTag\n                        + \" now \" + tag);\n            }\n            fragment.mTag = tag;\n        }\n\n        if (containerViewId != 0) {\n            if (containerViewId == View.NO_ID) {\n                throw new IllegalArgumentException(\"Can't add fragment \"\n                        + fragment + \" with tag \" + tag + \" to container view with no id\");\n            }\n            if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) {\n                throw new IllegalStateException(\"Can't change container ID of fragment \"\n                        + fragment + \": was \" + fragment.mFragmentId\n                        + \" now \" + containerViewId);\n            }\n            fragment.mContainerId = fragment.mFragmentId = containerViewId;\n        }\n\n        addOp(new Op(opcmd, fragment));\n    }\n\n    /**\n     * Calls {@link #replace(int, Fragment, String)} with a null tag.\n     */\n    @NonNull\n    public FragmentTransaction replace(@IdRes int containerViewId, @NonNull Fragment fragment) {\n        return replace(containerViewId, fragment, null);\n    }\n\n    /**\n     * Replace an existing fragment that was added to a container.  This is\n     * essentially the same as calling {@link #remove(Fragment)} for all\n     * currently added fragments that were added with the same containerViewId\n     * and then {@link #add(int, Fragment, String)} with the same arguments\n     * given here.\n     *\n     * @param containerViewId Identifier of the container whose fragment(s) are\n     * to be replaced.\n     * @param fragment The new fragment to place in the container.\n     * @param tag Optional tag name for the fragment, to later retrieve the\n     * fragment with {@link FragmentManager#findFragmentByTag(String)\n     * FragmentManager.findFragmentByTag(String)}.\n     *\n     * @return Returns the same FragmentTransaction instance.\n     */\n    @NonNull\n    public FragmentTransaction replace(@IdRes int containerViewId, @NonNull Fragment fragment,\n            @Nullable String tag)  {\n        if (containerViewId == 0) {\n            throw new IllegalArgumentException(\"Must use non-zero containerViewId\");\n        }\n        doAddOp(containerViewId, fragment, tag, OP_REPLACE);\n        return this;\n    }\n\n    /**\n     * Remove an existing fragment.  If it was added to a container, its view\n     * is also removed from that container.\n     *\n     * @param fragment The fragment to be removed.\n     *\n     * @return Returns the same FragmentTransaction instance.\n     */\n    @NonNull\n    public FragmentTransaction remove(@NonNull Fragment fragment) {\n        addOp(new Op(OP_REMOVE, fragment));\n\n        return this;\n    }\n\n    /**\n     * Hides an existing fragment.  This is only relevant for fragments whose\n     * views have been added to a container, as this will cause the view to\n     * be hidden.\n     *\n     * @param fragment The fragment to be hidden.\n     *\n     * @return Returns the same FragmentTransaction instance.\n     */\n    @NonNull\n    public FragmentTransaction hide(@NonNull Fragment fragment) {\n        addOp(new Op(OP_HIDE, fragment));\n\n        return this;\n    }\n\n    /**\n     * Shows a previously hidden fragment.  This is only relevant for fragments whose\n     * views have been added to a container, as this will cause the view to\n     * be shown.\n     *\n     * @param fragment The fragment to be shown.\n     *\n     * @return Returns the same FragmentTransaction instance.\n     */\n    @NonNull\n    public FragmentTransaction show(@NonNull Fragment fragment) {\n        addOp(new Op(OP_SHOW, fragment));\n\n        return this;\n    }\n\n    /**\n     * Detach the given fragment from the UI.  This is the same state as\n     * when it is put on the back stack: the fragment is removed from\n     * the UI, however its state is still being actively managed by the\n     * fragment manager.  When going into this state its view hierarchy\n     * is destroyed.\n     *\n     * @param fragment The fragment to be detached.\n     *\n     * @return Returns the same FragmentTransaction instance.\n     */\n    @NonNull\n    public FragmentTransaction detach(@NonNull Fragment fragment) {\n        addOp(new Op(OP_DETACH, fragment));\n\n        return this;\n    }\n\n    /**\n     * Re-attach a fragment after it had previously been detached from\n     * the UI with {@link #detach(Fragment)}.  This\n     * causes its view hierarchy to be re-created, attached to the UI,\n     * and displayed.\n     *\n     * @param fragment The fragment to be attached.\n     *\n     * @return Returns the same FragmentTransaction instance.\n     */\n    @NonNull\n    public FragmentTransaction attach(@NonNull Fragment fragment) {\n        addOp(new Op(OP_ATTACH, fragment));\n\n        return this;\n    }\n\n    /**\n     * Set a currently active fragment in this FragmentManager as the primary navigation fragment.\n     *\n     * <p>The primary navigation fragment's\n     * {@link Fragment#getChildFragmentManager() child FragmentManager} will be called first\n     * to process delegated navigation actions such as {@link FragmentManager#popBackStack()}\n     * if no ID or transaction name is provided to pop to. Navigation operations outside of the\n     * fragment system may choose to delegate those actions to the primary navigation fragment\n     * as returned by {@link FragmentManager#getPrimaryNavigationFragment()}.</p>\n     *\n     * <p>The fragment provided must currently be added to the FragmentManager to be set as\n     * a primary navigation fragment, or previously added as part of this transaction.</p>\n     *\n     * @param fragment the fragment to set as the primary navigation fragment\n     * @return the same FragmentTransaction instance\n     */\n    @NonNull\n    public FragmentTransaction setPrimaryNavigationFragment(@Nullable Fragment fragment) {\n        addOp(new Op(OP_SET_PRIMARY_NAV, fragment));\n\n        return this;\n    }\n\n    /**\n     * Set a ceiling for the state of an active fragment in this FragmentManager. If fragment is\n     * already above the received state, it will be forced down to the correct state.\n     *\n     * <p>The fragment provided must currently be added to the FragmentManager to have it's\n     * Lifecycle state capped, or previously added as part of this transaction. The\n     * {@link Lifecycle.State} passed in must at least be {@link Lifecycle.State#CREATED}, otherwise\n     * an {@link IllegalArgumentException} will be thrown.</p>\n     *\n     * @param fragment the fragment to have it's state capped.\n     * @param state the ceiling state for the fragment.\n     * @return the same FragmentTransaction instance\n     */\n    @NonNull\n    public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment,\n            @NonNull Lifecycle.State state) {\n        addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));\n        return this;\n    }\n\n    /**\n     * @return <code>true</code> if this transaction contains no operations,\n     * <code>false</code> otherwise.\n     */\n    public boolean isEmpty() {\n        return mOps.isEmpty();\n    }\n\n    /**\n     * Bit mask that is set for all enter transitions.\n     */\n    public static final int TRANSIT_ENTER_MASK = 0x1000;\n\n    /**\n     * Bit mask that is set for all exit transitions.\n     */\n    public static final int TRANSIT_EXIT_MASK = 0x2000;\n\n    /** @hide */\n    @RestrictTo(LIBRARY_GROUP_PREFIX)\n    @IntDef({TRANSIT_NONE, TRANSIT_FRAGMENT_OPEN, TRANSIT_FRAGMENT_CLOSE, TRANSIT_FRAGMENT_FADE})\n    @Retention(RetentionPolicy.SOURCE)\n    private @interface Transit {}\n\n    /** Not set up for a transition. */\n    public static final int TRANSIT_UNSET = -1;\n    /** No animation for transition. */\n    public static final int TRANSIT_NONE = 0;\n    /** Fragment is being added onto the stack */\n    public static final int TRANSIT_FRAGMENT_OPEN = 1 | TRANSIT_ENTER_MASK;\n    /** Fragment is being removed from the stack */\n    public static final int TRANSIT_FRAGMENT_CLOSE = 2 | TRANSIT_EXIT_MASK;\n    /** Fragment should simply fade in or out; that is, no strong navigation associated\n     * with it except that it is appearing or disappearing for some reason. */\n    public static final int TRANSIT_FRAGMENT_FADE = 3 | TRANSIT_ENTER_MASK;\n\n    /**\n     * Set specific animation resources to run for the fragments that are\n     * entering and exiting in this transaction. These animations will not be\n     * played when popping the back stack.\n     *\n     * @param enter An animation or animator resource ID used for the enter animation on the\n     *              view of the fragment being added or attached.\n     * @param exit An animation or animator resource ID used for the exit animation on the\n     *             view of the fragment being removed or detached.\n     */\n    @NonNull\n    public FragmentTransaction setCustomAnimations(@AnimatorRes @AnimRes int enter,\n            @AnimatorRes @AnimRes int exit) {\n        return setCustomAnimations(enter, exit, 0, 0);\n    }\n\n    /**\n     * Set specific animation resources to run for the fragments that are\n     * entering and exiting in this transaction. The <code>popEnter</code>\n     * and <code>popExit</code> animations will be played for enter/exit\n     * operations specifically when popping the back stack.\n     *\n     * @param enter An animation or animator resource ID used for the enter animation on the\n     *              view of the fragment being added or attached.\n     * @param exit An animation or animator resource ID used for the exit animation on the\n     *             view of the fragment being removed or detached.\n     * @param popEnter An animation or animator resource ID used for the enter animation on the\n     *                 view of the fragment being readded or reattached caused by\n     *                 {@link FragmentManager#popBackStack()} or similar methods.\n     * @param popExit An animation or animator resource ID used for the enter animation on the\n     *                view of the fragment being removed or detached caused by\n     *                {@link FragmentManager#popBackStack()} or similar methods.\n     */\n    @NonNull\n    public FragmentTransaction setCustomAnimations(@AnimatorRes @AnimRes int enter,\n            @AnimatorRes @AnimRes int exit, @AnimatorRes @AnimRes int popEnter,\n            @AnimatorRes @AnimRes int popExit) {\n        mEnterAnim = enter;\n        mExitAnim = exit;\n        mPopEnterAnim = popEnter;\n        mPopExitAnim = popExit;\n        return this;\n    }\n\n    /**\n     * Used with custom Transitions to map a View from a removed or hidden\n     * Fragment to a View from a shown or added Fragment.\n     * <var>sharedElement</var> must have a unique transitionName in the View hierarchy.\n     *\n     * @param sharedElement A View in a disappearing Fragment to match with a View in an\n     *                      appearing Fragment.\n     * @param name The transitionName for a View in an appearing Fragment to match to the shared\n     *             element.\n     * @see Fragment#setSharedElementReturnTransition(Object)\n     * @see Fragment#setSharedElementEnterTransition(Object)\n     */\n    @NonNull\n    public FragmentTransaction addSharedElement(@NonNull View sharedElement, @NonNull String name) {\n        if (FragmentTransition.supportsTransition()) {\n            String transitionName = ViewCompat.getTransitionName(sharedElement);\n            if (transitionName == null) {\n                throw new IllegalArgumentException(\"Unique transitionNames are required for all\"\n                        + \" sharedElements\");\n            }\n            if (mSharedElementSourceNames == null) {\n                mSharedElementSourceNames = new ArrayList<String>();\n                mSharedElementTargetNames = new ArrayList<String>();\n            } else if (mSharedElementTargetNames.contains(name)) {\n                throw new IllegalArgumentException(\"A shared element with the target name '\"\n                        + name + \"' has already been added to the transaction.\");\n            } else if (mSharedElementSourceNames.contains(transitionName)) {\n                throw new IllegalArgumentException(\"A shared element with the source name '\"\n                        + transitionName + \"' has already been added to the transaction.\");\n            }\n\n            mSharedElementSourceNames.add(transitionName);\n            mSharedElementTargetNames.add(name);\n        }\n        return this;\n    }\n\n    /**\n     * Select a standard transition animation for this transaction.  May be\n     * one of {@link #TRANSIT_NONE}, {@link #TRANSIT_FRAGMENT_OPEN},\n     * {@link #TRANSIT_FRAGMENT_CLOSE}, or {@link #TRANSIT_FRAGMENT_FADE}.\n     */\n    @NonNull\n    public FragmentTransaction setTransition(@Transit int transition) {\n        mTransition = transition;\n        return this;\n    }\n\n    /**\n     * Set a custom style resource that will be used for resolving transit\n     * animations.\n     */\n    @NonNull\n    public FragmentTransaction setTransitionStyle(@StyleRes int styleRes) {\n        mTransitionStyle = styleRes;\n        return this;\n    }\n\n    /**\n     * Add this transaction to the back stack.  This means that the transaction\n     * will be remembered after it is committed, and will reverse its operation\n     * when later popped off the stack.\n     * <p>\n     * {@link #setReorderingAllowed(boolean)} must be set to <code>true</code>\n     * in the same transaction as addToBackStack() to allow the pop of that\n     * transaction to be reordered.\n     *\n     * @param name An optional name for this back stack state, or null.\n     */\n    @NonNull\n    public FragmentTransaction addToBackStack(@Nullable String name) {\n        if (!mAllowAddToBackStack) {\n            throw new IllegalStateException(\n                    \"This FragmentTransaction is not allowed to be added to the back stack.\");\n        }\n        mAddToBackStack = true;\n        mName = name;\n        return this;\n    }\n\n    /**\n     * Returns true if this FragmentTransaction is allowed to be added to the back\n     * stack. If this method would return false, {@link #addToBackStack(String)}\n     * will throw {@link IllegalStateException}.\n     *\n     * @return True if {@link #addToBackStack(String)} is permitted on this transaction.\n     */\n    public boolean isAddToBackStackAllowed() {\n        return mAllowAddToBackStack;\n    }\n\n    /**\n     * Disallow calls to {@link #addToBackStack(String)}. Any future calls to\n     * addToBackStack will throw {@link IllegalStateException}. If addToBackStack\n     * has already been called, this method will throw IllegalStateException.\n     */\n    @NonNull\n    public FragmentTransaction disallowAddToBackStack() {\n        if (mAddToBackStack) {\n            throw new IllegalStateException(\n                    \"This transaction is already being added to the back stack\");\n        }\n        mAllowAddToBackStack = false;\n        return this;\n    }\n\n    /**\n     * Set the full title to show as a bread crumb when this transaction\n     * is on the back stack.\n     *\n     * @param res A string resource containing the title.\n     */\n    @NonNull\n    public FragmentTransaction setBreadCrumbTitle(@StringRes int res) {\n        mBreadCrumbTitleRes = res;\n        mBreadCrumbTitleText = null;\n        return this;\n    }\n\n    /**\n     * Like {@link #setBreadCrumbTitle(int)} but taking a raw string; this\n     * method is <em>not</em> recommended, as the string can not be changed\n     * later if the locale changes.\n     */\n    @NonNull\n    public FragmentTransaction setBreadCrumbTitle(@Nullable CharSequence text) {\n        mBreadCrumbTitleRes = 0;\n        mBreadCrumbTitleText = text;\n        return this;\n    }\n\n    /**\n     * Set the short title to show as a bread crumb when this transaction\n     * is on the back stack.\n     *\n     * @param res A string resource containing the title.\n     */\n    @NonNull\n    public FragmentTransaction setBreadCrumbShortTitle(@StringRes int res) {\n        mBreadCrumbShortTitleRes = res;\n        mBreadCrumbShortTitleText = null;\n        return this;\n    }\n\n    /**\n     * Like {@link #setBreadCrumbShortTitle(int)} but taking a raw string; this\n     * method is <em>not</em> recommended, as the string can not be changed\n     * later if the locale changes.\n     */\n    @NonNull\n    public FragmentTransaction setBreadCrumbShortTitle(@Nullable CharSequence text) {\n        mBreadCrumbShortTitleRes = 0;\n        mBreadCrumbShortTitleText = text;\n        return this;\n    }\n\n    /**\n     * Sets whether or not to allow optimizing operations within and across\n     * transactions. This will remove redundant operations, eliminating\n     * operations that cancel. For example, if two transactions are executed\n     * together, one that adds a fragment A and the next replaces it with fragment B,\n     * the operations will cancel and only fragment B will be added. That means that\n     * fragment A may not go through the creation/destruction lifecycle.\n     * <p>\n     * The side effect of removing redundant operations is that fragments may have state changes\n     * out of the expected order. For example, one transaction adds fragment A,\n     * a second adds fragment B, then a third removes fragment A. Without removing the redundant\n     * operations, fragment B could expect that while it is being created, fragment A will also\n     * exist because fragment A will be removed after fragment B was added.\n     * With removing redundant operations, fragment B cannot expect fragment A to exist when\n     * it has been created because fragment A's add/remove will be optimized out.\n     * <p>\n     * It can also reorder the state changes of Fragments to allow for better Transitions.\n     * Added Fragments may have {@link Fragment#onCreate(Bundle)} called before replaced\n     * Fragments have {@link Fragment#onDestroy()} called.\n     * <p>\n     * {@link Fragment#postponeEnterTransition()} requires {@code setReorderingAllowed(true)}.\n     * <p>\n     * The default is {@code false}.\n     *\n     * @param reorderingAllowed {@code true} to enable optimizing out redundant operations\n     *                          or {@code false} to disable optimizing out redundant\n     *                          operations on this transaction.\n     */\n    @NonNull\n    public FragmentTransaction setReorderingAllowed(boolean reorderingAllowed) {\n        mReorderingAllowed = reorderingAllowed;\n        return this;\n    }\n\n    /**\n     * @deprecated This has been renamed {@link #setReorderingAllowed(boolean)}.\n     */\n    @Deprecated\n    @NonNull\n    public FragmentTransaction setAllowOptimization(boolean allowOptimization) {\n        return setReorderingAllowed(allowOptimization);\n    }\n\n    /**\n     * Add a Runnable to this transaction that will be run after this transaction has\n     * been committed. If fragment transactions are {@link #setReorderingAllowed(boolean) optimized}\n     * this may be after other subsequent fragment operations have also taken place, or operations\n     * in this transaction may have been optimized out due to the presence of a subsequent\n     * fragment transaction in the batch.\n     *\n     * <p>If a transaction is committed using {@link #commitAllowingStateLoss()} this runnable\n     * may be executed when the FragmentManager is in a state where new transactions may not\n     * be committed without allowing state loss.</p>\n     *\n     * <p><code>runOnCommit</code> may not be used with transactions\n     * {@link #addToBackStack(String) added to the back stack} as Runnables cannot be persisted\n     * with back stack state. {@link IllegalStateException} will be thrown if\n     * {@link #addToBackStack(String)} has been previously called for this transaction\n     * or if it is called after a call to <code>runOnCommit</code>.</p>\n     *\n     * @param runnable Runnable to add\n     * @return this FragmentTransaction\n     * @throws IllegalStateException if {@link #addToBackStack(String)} has been called\n     */\n    @NonNull\n    public FragmentTransaction runOnCommit(@NonNull Runnable runnable) {\n        disallowAddToBackStack();\n        if (mCommitRunnables == null) {\n            mCommitRunnables = new ArrayList<>();\n        }\n        mCommitRunnables.add(runnable);\n        return this;\n    }\n\n    /**\n     * Schedules a commit of this transaction.  The commit does\n     * not happen immediately; it will be scheduled as work on the main thread\n     * to be done the next time that thread is ready.\n     *\n     * <p class=\"note\">A transaction can only be committed with this method\n     * prior to its containing activity saving its state.  If the commit is\n     * attempted after that point, an exception will be thrown.  This is\n     * because the state after the commit can be lost if the activity needs to\n     * be restored from its state.  See {@link #commitAllowingStateLoss()} for\n     * situations where it may be okay to lose the commit.</p>\n     *\n     * @return Returns the identifier of this transaction's back stack entry,\n     * if {@link #addToBackStack(String)} had been called.  Otherwise, returns\n     * a negative number.\n     */\n    public abstract int commit();\n\n    /**\n     * Like {@link #commit} but allows the commit to be executed after an\n     * activity's state is saved.  This is dangerous because the commit can\n     * be lost if the activity needs to later be restored from its state, so\n     * this should only be used for cases where it is okay for the UI state\n     * to change unexpectedly on the user.\n     */\n    public abstract int commitAllowingStateLoss();\n\n    /**\n     * Commits this transaction synchronously. Any added fragments will be\n     * initialized and brought completely to the lifecycle state of their host\n     * and any removed fragments will be torn down accordingly before this\n     * call returns. Committing a transaction in this way allows fragments\n     * to be added as dedicated, encapsulated components that monitor the\n     * lifecycle state of their host while providing firmer ordering guarantees\n     * around when those fragments are fully initialized and ready. Fragments\n     * that manage views will have those views created and attached.\n     *\n     * <p>Calling <code>commitNow</code> is preferable to calling\n     * {@link #commit()} followed by {@link FragmentManager#executePendingTransactions()}\n     * as the latter will have the side effect of attempting to commit <em>all</em>\n     * currently pending transactions whether that is the desired behavior\n     * or not.</p>\n     *\n     * <p>Transactions committed in this way may not be added to the\n     * FragmentManager's back stack, as doing so would break other expected\n     * ordering guarantees for other asynchronously committed transactions.\n     * This method will throw {@link IllegalStateException} if the transaction\n     * previously requested to be added to the back stack with\n     * {@link #addToBackStack(String)}.</p>\n     *\n     * <p class=\"note\">A transaction can only be committed with this method\n     * prior to its containing activity saving its state.  If the commit is\n     * attempted after that point, an exception will be thrown.  This is\n     * because the state after the commit can be lost if the activity needs to\n     * be restored from its state.  See {@link #commitAllowingStateLoss()} for\n     * situations where it may be okay to lose the commit.</p>\n     */\n    public abstract void commitNow();\n\n    /**\n     * Like {@link #commitNow} but allows the commit to be executed after an\n     * activity's state is saved.  This is dangerous because the commit can\n     * be lost if the activity needs to later be restored from its state, so\n     * this should only be used for cases where it is okay for the UI state\n     * to change unexpectedly on the user.\n     */\n    public abstract void commitNowAllowingStateLoss();\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransition.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.graphics.Rect;\nimport android.os.Build;\nimport android.util.SparseArray;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.collection.ArrayMap;\nimport androidx.core.app.SharedElementCallback;\nimport androidx.core.view.OneShotPreDrawListener;\nimport androidx.core.view.ViewCompat;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Contains the Fragment Transition functionality for both ordered and reordered\n * Fragment Transactions. With reordered fragment transactions, all Views have been\n * added to the View hierarchy prior to calling startTransitions. With ordered\n * fragment transactions, Views will be removed and added after calling startTransitions.\n */\nclass FragmentTransition {\n    /**\n     * The inverse of all BackStackRecord operation commands. This assumes that\n     * REPLACE operations have already been replaced by add/remove operations.\n     */\n    private static final int[] INVERSE_OPS = {\n            BackStackRecord.OP_NULL,                // inverse of OP_NULL (error)\n            BackStackRecord.OP_REMOVE,              // inverse of OP_ADD\n            BackStackRecord.OP_NULL,                // inverse of OP_REPLACE (error)\n            BackStackRecord.OP_ADD,                 // inverse of OP_REMOVE\n            BackStackRecord.OP_SHOW,                // inverse of OP_HIDE\n            BackStackRecord.OP_HIDE,                // inverse of OP_SHOW\n            BackStackRecord.OP_ATTACH,              // inverse of OP_DETACH\n            BackStackRecord.OP_DETACH,              // inverse of OP_ATTACH\n            BackStackRecord.OP_UNSET_PRIMARY_NAV,   // inverse of OP_SET_PRIMARY_NAV\n            BackStackRecord.OP_SET_PRIMARY_NAV,     // inverse of OP_UNSET_PRIMARY_NAV\n            BackStackRecord.OP_SET_MAX_LIFECYCLE\n    };\n\n    private static final FragmentTransitionImpl PLATFORM_IMPL = Build.VERSION.SDK_INT >= 21\n            ? new FragmentTransitionCompat21()\n            : null;\n\n    private static final FragmentTransitionImpl SUPPORT_IMPL = resolveSupportImpl();\n\n    private static FragmentTransitionImpl resolveSupportImpl() {\n        try {\n            @SuppressWarnings(\"unchecked\")\n            Class<FragmentTransitionImpl> impl = (Class<FragmentTransitionImpl>) Class.forName(\n                    \"androidx.transition.FragmentTransitionSupport\");\n            return impl.getDeclaredConstructor().newInstance();\n        } catch (Exception ignored) {\n            // support-transition is not loaded; ignore\n        }\n        return null;\n    }\n\n    /**\n     * The main entry point for Fragment Transitions, this starts the transitions\n     * set on the leaving Fragment's {@link Fragment#getExitTransition()}, the\n     * entering Fragment's {@link Fragment#getEnterTransition()} and\n     * {@link Fragment#getSharedElementEnterTransition()}. When popping,\n     * the leaving Fragment's {@link Fragment#getReturnTransition()} and\n     * {@link Fragment#getSharedElementReturnTransition()} and the entering\n     * {@link Fragment#getReenterTransition()} will be run.\n     * <p>\n     * With reordered Fragment Transitions, all Views have been added to the\n     * View hierarchy prior to calling this method. The incoming Fragment's Views\n     * will be INVISIBLE. With ordered Fragment Transitions, this method\n     * is called before any change has been made to the hierarchy. That means\n     * that the added Fragments have not created their Views yet and the hierarchy\n     * is unknown.\n     *\n     * @param fragmentManager The executing FragmentManagerImpl\n     * @param records The list of transactions being executed.\n     * @param isRecordPop For each transaction, whether it is a pop transaction or not.\n     * @param startIndex The first index into records and isRecordPop to execute as\n     *                   part of this transition.\n     * @param endIndex One past the last index into records and isRecordPop to execute\n     *                 as part of this transition.\n     * @param isReordered true if this is a reordered transaction, meaning that the\n     *                    Views of incoming fragments have been added. false if the\n     *                    transaction has yet to be run and Views haven't been created.\n     */\n    static void startTransitions(FragmentManagerImpl fragmentManager,\n            ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,\n            int startIndex, int endIndex, boolean isReordered) {\n        if (fragmentManager.mCurState < Fragment.CREATED) {\n            return;\n        }\n\n        SparseArray<FragmentContainerTransition> transitioningFragments =\n                new SparseArray<>();\n        for (int i = startIndex; i < endIndex; i++) {\n            final BackStackRecord record = records.get(i);\n            final boolean isPop = isRecordPop.get(i);\n            if (isPop) {\n                calculatePopFragments(record, transitioningFragments, isReordered);\n            } else {\n                calculateFragments(record, transitioningFragments, isReordered);\n            }\n        }\n\n        if (transitioningFragments.size() != 0) {\n            final View nonExistentView = new View(fragmentManager.mHost.getContext());\n            final int numContainers = transitioningFragments.size();\n            for (int i = 0; i < numContainers; i++) {\n                int containerId = transitioningFragments.keyAt(i);\n                ArrayMap<String, String> nameOverrides = calculateNameOverrides(containerId,\n                        records, isRecordPop, startIndex, endIndex);\n\n                FragmentContainerTransition containerTransition =\n                        transitioningFragments.valueAt(i);\n\n                if (isReordered) {\n                    configureTransitionsReordered(fragmentManager, containerId,\n                            containerTransition, nonExistentView, nameOverrides);\n                } else {\n                    configureTransitionsOrdered(fragmentManager, containerId,\n                            containerTransition, nonExistentView, nameOverrides);\n                }\n            }\n        }\n    }\n\n    /**\n     * Iterates through the transactions that affect a given fragment container\n     * and tracks the shared element names across transactions. This is most useful\n     * in pop transactions where the names of shared elements are known.\n     *\n     * @param containerId The container ID that is executing the transition.\n     * @param records The list of transactions being executed.\n     * @param isRecordPop For each transaction, whether it is a pop transaction or not.\n     * @param startIndex The first index into records and isRecordPop to execute as\n     *                   part of this transition.\n     * @param endIndex One past the last index into records and isRecordPop to execute\n     *                 as part of this transition.\n     * @return A map from the initial shared element name to the final shared element name\n     * before any onMapSharedElements is run.\n     */\n    private static ArrayMap<String, String> calculateNameOverrides(int containerId,\n            ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,\n            int startIndex, int endIndex) {\n        ArrayMap<String, String> nameOverrides = new ArrayMap<>();\n        for (int recordNum = endIndex - 1; recordNum >= startIndex; recordNum--) {\n            final BackStackRecord record = records.get(recordNum);\n            if (!record.interactsWith(containerId)) {\n                continue;\n            }\n            final boolean isPop = isRecordPop.get(recordNum);\n            if (record.mSharedElementSourceNames != null) {\n                final int numSharedElements = record.mSharedElementSourceNames.size();\n                final ArrayList<String> sources;\n                final ArrayList<String> targets;\n                if (isPop) {\n                    targets = record.mSharedElementSourceNames;\n                    sources = record.mSharedElementTargetNames;\n                } else {\n                    sources = record.mSharedElementSourceNames;\n                    targets = record.mSharedElementTargetNames;\n                }\n                for (int i = 0; i < numSharedElements; i++) {\n                    String sourceName = sources.get(i);\n                    String targetName = targets.get(i);\n                    String previousTarget = nameOverrides.remove(targetName);\n                    if (previousTarget != null) {\n                        nameOverrides.put(sourceName, previousTarget);\n                    } else {\n                        nameOverrides.put(sourceName, targetName);\n                    }\n                }\n            }\n        }\n        return nameOverrides;\n    }\n\n    /**\n     * Configures a transition for a single fragment container for which the transaction was\n     * reordered. That means that all Fragment Views have been added and incoming fragment\n     * Views are marked invisible.\n     *\n     * @param fragmentManager The executing FragmentManagerImpl\n     * @param containerId The container ID that is executing the transition.\n     * @param fragments A structure holding the transitioning fragments in this container.\n     * @param nonExistentView A View that does not exist in the hierarchy. This is used to\n     *                        prevent transitions from acting on other Views when there is no\n     *                        other target.\n     * @param nameOverrides A map of the shared element names from the starting fragment to\n     *                      the final fragment's Views as given in\n     *                      {@link FragmentTransaction#addSharedElement(View, String)}.\n     */\n    private static void configureTransitionsReordered(FragmentManagerImpl fragmentManager,\n            int containerId, FragmentContainerTransition fragments,\n            View nonExistentView, ArrayMap<String, String> nameOverrides) {\n        ViewGroup sceneRoot = null;\n        if (fragmentManager.mContainer.onHasView()) {\n            sceneRoot = (ViewGroup) fragmentManager.mContainer.onFindViewById(containerId);\n        }\n        if (sceneRoot == null) {\n            return;\n        }\n        final Fragment inFragment = fragments.lastIn;\n        final Fragment outFragment = fragments.firstOut;\n        final FragmentTransitionImpl impl = chooseImpl(outFragment, inFragment);\n        if (impl == null) {\n            return;\n        }\n        final boolean inIsPop = fragments.lastInIsPop;\n        final boolean outIsPop = fragments.firstOutIsPop;\n\n        ArrayList<View> sharedElementsIn = new ArrayList<>();\n        ArrayList<View> sharedElementsOut = new ArrayList<>();\n        Object enterTransition = getEnterTransition(impl, inFragment, inIsPop);\n        Object exitTransition = getExitTransition(impl, outFragment, outIsPop);\n\n        Object sharedElementTransition = configureSharedElementsReordered(impl, sceneRoot,\n                nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn,\n                enterTransition, exitTransition);\n\n        if (enterTransition == null && sharedElementTransition == null\n                && exitTransition == null) {\n            return; // no transitions!\n        }\n\n        ArrayList<View> exitingViews = configureEnteringExitingViews(impl, exitTransition,\n                outFragment, sharedElementsOut, nonExistentView);\n\n        ArrayList<View> enteringViews = configureEnteringExitingViews(impl, enterTransition,\n                inFragment, sharedElementsIn, nonExistentView);\n\n        setViewVisibility(enteringViews, View.INVISIBLE);\n\n        Object transition = mergeTransitions(impl, enterTransition, exitTransition,\n                sharedElementTransition, inFragment, inIsPop);\n\n        if (transition != null) {\n            replaceHide(impl, exitTransition, outFragment, exitingViews);\n            ArrayList<String> inNames =\n                    impl.prepareSetNameOverridesReordered(sharedElementsIn);\n            impl.scheduleRemoveTargets(transition,\n                    enterTransition, enteringViews, exitTransition, exitingViews,\n                    sharedElementTransition, sharedElementsIn);\n            impl.beginDelayedTransition(sceneRoot, transition);\n            impl.setNameOverridesReordered(sceneRoot, sharedElementsOut,\n                    sharedElementsIn, inNames, nameOverrides);\n            setViewVisibility(enteringViews, View.VISIBLE);\n            impl.swapSharedElementTargets(sharedElementTransition,\n                    sharedElementsOut, sharedElementsIn);\n        }\n    }\n\n    /**\n     * Replace hide operations with visibility changes on the exiting views. Instead of making\n     * the entire fragment's view GONE, make each exiting view INVISIBLE. At the end of the\n     * transition, make the fragment's view GONE.\n     */\n    private static void replaceHide(FragmentTransitionImpl impl,\n            Object exitTransition, Fragment exitingFragment,\n            final ArrayList<View> exitingViews) {\n        if (exitingFragment != null && exitTransition != null && exitingFragment.mAdded\n                && exitingFragment.mHidden && exitingFragment.mHiddenChanged) {\n            exitingFragment.setHideReplaced(true);\n            impl.scheduleHideFragmentView(exitTransition,\n                    exitingFragment.getView(), exitingViews);\n            final ViewGroup container = exitingFragment.mContainer;\n            OneShotPreDrawListener.add(container, new Runnable() {\n                @Override\n                public void run() {\n                    setViewVisibility(exitingViews, View.INVISIBLE);\n                }\n            });\n        }\n    }\n\n    /**\n     * Configures a transition for a single fragment container for which the transaction was\n     * ordered. That means that the transaction has not been executed yet, so incoming\n     * Views are not yet known.\n     *\n     * @param fragmentManager The executing FragmentManagerImpl\n     * @param containerId The container ID that is executing the transition.\n     * @param fragments A structure holding the transitioning fragments in this container.\n     * @param nonExistentView A View that does not exist in the hierarchy. This is used to\n     *                        prevent transitions from acting on other Views when there is no\n     *                        other target.\n     * @param nameOverrides A map of the shared element names from the starting fragment to\n     *                      the final fragment's Views as given in\n     *                      {@link FragmentTransaction#addSharedElement(View, String)}.\n     */\n    private static void configureTransitionsOrdered(FragmentManagerImpl fragmentManager,\n            int containerId, FragmentContainerTransition fragments,\n            View nonExistentView, ArrayMap<String, String> nameOverrides) {\n        ViewGroup sceneRoot = null;\n        if (fragmentManager.mContainer.onHasView()) {\n            sceneRoot = (ViewGroup) fragmentManager.mContainer.onFindViewById(containerId);\n        }\n        if (sceneRoot == null) {\n            return;\n        }\n        final Fragment inFragment = fragments.lastIn;\n        final Fragment outFragment = fragments.firstOut;\n        final FragmentTransitionImpl impl = chooseImpl(outFragment, inFragment);\n        if (impl == null) {\n            return;\n        }\n        final boolean inIsPop = fragments.lastInIsPop;\n        final boolean outIsPop = fragments.firstOutIsPop;\n\n        Object enterTransition = getEnterTransition(impl, inFragment, inIsPop);\n        Object exitTransition = getExitTransition(impl, outFragment, outIsPop);\n\n        ArrayList<View> sharedElementsOut = new ArrayList<>();\n        ArrayList<View> sharedElementsIn = new ArrayList<>();\n\n        Object sharedElementTransition = configureSharedElementsOrdered(impl, sceneRoot,\n                nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn,\n                enterTransition, exitTransition);\n\n        if (enterTransition == null && sharedElementTransition == null\n                && exitTransition == null) {\n            return; // no transitions!\n        }\n\n        ArrayList<View> exitingViews = configureEnteringExitingViews(impl, exitTransition,\n                outFragment, sharedElementsOut, nonExistentView);\n\n        if (exitingViews == null || exitingViews.isEmpty()) {\n            exitTransition = null;\n        }\n\n        // Ensure the entering transition doesn't target anything until the views are made\n        // visible\n        impl.addTarget(enterTransition, nonExistentView);\n\n        Object transition = mergeTransitions(impl, enterTransition, exitTransition,\n                sharedElementTransition, inFragment, fragments.lastInIsPop);\n\n        if (transition != null) {\n            final ArrayList<View> enteringViews = new ArrayList<>();\n            impl.scheduleRemoveTargets(transition,\n                    enterTransition, enteringViews, exitTransition, exitingViews,\n                    sharedElementTransition, sharedElementsIn);\n            scheduleTargetChange(impl, sceneRoot, inFragment, nonExistentView, sharedElementsIn,\n                    enterTransition, enteringViews, exitTransition, exitingViews);\n            impl.setNameOverridesOrdered(sceneRoot, sharedElementsIn, nameOverrides);\n\n            impl.beginDelayedTransition(sceneRoot, transition);\n            impl.scheduleNameReset(sceneRoot, sharedElementsIn, nameOverrides);\n        }\n    }\n\n    /**\n     * This method is used for fragment transitions for ordrered transactions to change the\n     * enter and exit transition targets after the call to\n     * {@link FragmentTransitionCompat21#beginDelayedTransition(ViewGroup, Object)}. The exit\n     * transition must ensure that it does not target any Views and the enter transition must start\n     * targeting the Views of the incoming Fragment.\n     *\n     * @param sceneRoot The fragment container View\n     * @param inFragment The last fragment that is entering\n     * @param nonExistentView A view that does not exist in the hierarchy that is used as a\n     *                        transition target to ensure no View is targeted.\n     * @param sharedElementsIn The shared element Views of the incoming fragment\n     * @param enterTransition The enter transition of the incoming fragment\n     * @param enteringViews The entering Views of the incoming fragment\n     * @param exitTransition The exit transition of the outgoing fragment\n     * @param exitingViews The exiting views of the outgoing fragment\n     */\n    private static void scheduleTargetChange(final FragmentTransitionImpl impl,\n            final ViewGroup sceneRoot,\n            final Fragment inFragment, final View nonExistentView,\n            final ArrayList<View> sharedElementsIn,\n            final Object enterTransition, final ArrayList<View> enteringViews,\n            final Object exitTransition, final ArrayList<View> exitingViews) {\n        OneShotPreDrawListener.add(sceneRoot, new Runnable() {\n            @Override\n            public void run() {\n                if (enterTransition != null) {\n                    impl.removeTarget(enterTransition,\n                            nonExistentView);\n                    ArrayList<View> views = configureEnteringExitingViews(impl,\n                            enterTransition, inFragment, sharedElementsIn, nonExistentView);\n                    enteringViews.addAll(views);\n                }\n\n                if (exitingViews != null) {\n                    if (exitTransition != null) {\n                        ArrayList<View> tempExiting = new ArrayList<>();\n                        tempExiting.add(nonExistentView);\n                        impl.replaceTargets(exitTransition, exitingViews,\n                                tempExiting);\n                    }\n                    exitingViews.clear();\n                    exitingViews.add(nonExistentView);\n                }\n            }\n        });\n    }\n\n    /**\n     * Chooses the appropriate implementation depending on the Transition instances hold by the\n     * Fragments.\n     */\n    private static FragmentTransitionImpl chooseImpl(Fragment outFragment, Fragment inFragment) {\n        // Collect all transition instances\n        final ArrayList<Object> transitions = new ArrayList<>();\n        if (outFragment != null) {\n            final Object exitTransition = outFragment.getExitTransition();\n            if (exitTransition != null) {\n                transitions.add(exitTransition);\n            }\n            final Object returnTransition = outFragment.getReturnTransition();\n            if (returnTransition != null) {\n                transitions.add(returnTransition);\n            }\n            final Object sharedReturnTransition = outFragment.getSharedElementReturnTransition();\n            if (sharedReturnTransition != null) {\n                transitions.add(sharedReturnTransition);\n            }\n        }\n        if (inFragment != null) {\n            final Object enterTransition = inFragment.getEnterTransition();\n            if (enterTransition != null) {\n                transitions.add(enterTransition);\n            }\n            final Object reenterTransition = inFragment.getReenterTransition();\n            if (reenterTransition != null) {\n                transitions.add(reenterTransition);\n            }\n            final Object sharedEnterTransition = inFragment.getSharedElementEnterTransition();\n            if (sharedEnterTransition != null) {\n                transitions.add(sharedEnterTransition);\n            }\n        }\n        if (transitions.isEmpty()) {\n            return null; // No transition to run\n        }\n        // Pick the implementation that can handle all the transitions\n        if (PLATFORM_IMPL != null && canHandleAll(PLATFORM_IMPL, transitions)) {\n            return PLATFORM_IMPL;\n        }\n        if (SUPPORT_IMPL != null && canHandleAll(SUPPORT_IMPL, transitions)) {\n            return SUPPORT_IMPL;\n        }\n        if (PLATFORM_IMPL != null || SUPPORT_IMPL != null) {\n            throw new IllegalArgumentException(\"Invalid Transition types\");\n        }\n        return null;\n    }\n\n    private static boolean canHandleAll(FragmentTransitionImpl impl, List<Object> transitions) {\n        for (int i = 0, size = transitions.size(); i < size; i++) {\n            if (!impl.canHandle(transitions.get(i))) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Returns a TransitionSet containing the shared element transition. The wrapping TransitionSet\n     * targets all shared elements to ensure that no other Views are targeted. The shared element\n     * transition can then target any or all shared elements without worrying about accidentally\n     * targeting entering or exiting Views.\n     *\n     * @param inFragment The incoming fragment\n     * @param outFragment the outgoing fragment\n     * @param isPop True if this is a pop transaction or false if it is a normal (add) transaction.\n     * @return A TransitionSet wrapping the shared element transition or null if no such transition\n     * exists.\n     */\n    private static Object getSharedElementTransition(FragmentTransitionImpl impl,\n            Fragment inFragment, Fragment outFragment, boolean isPop) {\n        if (inFragment == null || outFragment == null) {\n            return null;\n        }\n        Object transition = impl.cloneTransition(isPop\n                ? outFragment.getSharedElementReturnTransition()\n                : inFragment.getSharedElementEnterTransition());\n        return impl.wrapTransitionInSet(transition);\n    }\n\n    /**\n     * Returns a clone of the enter transition or null if no such transition exists.\n     */\n    private static Object getEnterTransition(FragmentTransitionImpl impl,\n            Fragment inFragment, boolean isPop) {\n        if (inFragment == null) {\n            return null;\n        }\n        return impl.cloneTransition(isPop\n                ? inFragment.getReenterTransition()\n                : inFragment.getEnterTransition());\n    }\n\n    /**\n     * Returns a clone of the exit transition or null if no such transition exists.\n     */\n    private static Object getExitTransition(FragmentTransitionImpl impl,\n            Fragment outFragment, boolean isPop) {\n        if (outFragment == null) {\n            return null;\n        }\n        return impl.cloneTransition(isPop\n                ? outFragment.getReturnTransition()\n                : outFragment.getExitTransition());\n    }\n\n    /**\n     * Configures the shared elements of a reordered fragment transaction's transition.\n     * This retrieves the shared elements of the outgoing and incoming fragments, maps the\n     * views, and sets up the epicenter on the transitions.\n     * <p>\n     * The epicenter of exit and shared element transitions is the first shared element\n     * in the outgoing fragment. The epicenter of the entering transition is the first shared\n     * element in the incoming fragment.\n     *\n     * @param sceneRoot The fragment container View\n     * @param nonExistentView A View that does not exist in the hierarchy. This is used to\n     *                        prevent transitions from acting on other Views when there is no\n     *                        other target.\n     * @param nameOverrides A map of the shared element names from the starting fragment to\n     *                      the final fragment's Views as given in\n     *                      {@link FragmentTransaction#addSharedElement(View, String)}.\n     * @param fragments A structure holding the transitioning fragments in this container.\n     * @param sharedElementsOut A list modified to contain the shared elements in the outgoing\n     *                          fragment\n     * @param sharedElementsIn A list modified to contain the shared elements in the incoming\n     *                         fragment\n     * @param enterTransition The transition used for entering Views, modified by applying the\n     *                        epicenter\n     * @param exitTransition The transition used for exiting Views, modified by applying the\n     *                       epicenter\n     * @return The shared element transition or null if no shared elements exist\n     */\n    private static Object configureSharedElementsReordered(final FragmentTransitionImpl impl,\n            final ViewGroup sceneRoot,\n            final View nonExistentView, final ArrayMap<String, String> nameOverrides,\n            final FragmentContainerTransition fragments,\n            final ArrayList<View> sharedElementsOut,\n            final ArrayList<View> sharedElementsIn,\n            final Object enterTransition, final Object exitTransition) {\n        final Fragment inFragment = fragments.lastIn;\n        final Fragment outFragment = fragments.firstOut;\n        if (inFragment != null) {\n            inFragment.requireView().setVisibility(View.VISIBLE);\n        }\n        if (inFragment == null || outFragment == null) {\n            return null; // no shared element without a fragment\n        }\n\n        final boolean inIsPop = fragments.lastInIsPop;\n        Object sharedElementTransition = nameOverrides.isEmpty() ? null\n                : getSharedElementTransition(impl, inFragment, outFragment, inIsPop);\n\n        final ArrayMap<String, View> outSharedElements = captureOutSharedElements(impl,\n                nameOverrides, sharedElementTransition, fragments);\n\n        final ArrayMap<String, View> inSharedElements = captureInSharedElements(impl,\n                nameOverrides, sharedElementTransition, fragments);\n\n        if (nameOverrides.isEmpty()) {\n            sharedElementTransition = null;\n            if (outSharedElements != null) {\n                outSharedElements.clear();\n            }\n            if (inSharedElements != null) {\n                inSharedElements.clear();\n            }\n        } else {\n            addSharedElementsWithMatchingNames(sharedElementsOut, outSharedElements,\n                    nameOverrides.keySet());\n            addSharedElementsWithMatchingNames(sharedElementsIn, inSharedElements,\n                    nameOverrides.values());\n        }\n\n        if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {\n            // don't call onSharedElementStart/End since there is no transition\n            return null;\n        }\n\n        callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true);\n\n        final Rect epicenter;\n        final View epicenterView;\n        if (sharedElementTransition != null) {\n            sharedElementsIn.add(nonExistentView);\n            impl.setSharedElementTargets(sharedElementTransition,\n                    nonExistentView, sharedElementsOut);\n            final boolean outIsPop = fragments.firstOutIsPop;\n            final BackStackRecord outTransaction = fragments.firstOutTransaction;\n            setOutEpicenter(impl, sharedElementTransition, exitTransition, outSharedElements,\n                    outIsPop, outTransaction);\n            epicenter = new Rect();\n            epicenterView = getInEpicenterView(inSharedElements, fragments,\n                    enterTransition, inIsPop);\n            if (epicenterView != null) {\n                impl.setEpicenter(enterTransition, epicenter);\n            }\n        } else {\n            epicenter = null;\n            epicenterView = null;\n        }\n\n        OneShotPreDrawListener.add(sceneRoot, new Runnable() {\n            @Override\n            public void run() {\n                callSharedElementStartEnd(inFragment, outFragment, inIsPop,\n                        inSharedElements, false);\n                if (epicenterView != null) {\n                    impl.getBoundsOnScreen(epicenterView, epicenter);\n                }\n            }\n        });\n        return sharedElementTransition;\n    }\n\n    /**\n     * Add Views from sharedElements into views that have the transitionName in the\n     * nameOverridesSet.\n     *\n     * @param views               Views list to add shared elements to\n     * @param sharedElements      List of shared elements\n     * @param nameOverridesSet    The transition names for all views to be copied from\n     *                            sharedElements to views.\n     */\n    private static void addSharedElementsWithMatchingNames(ArrayList<View> views,\n            ArrayMap<String, View> sharedElements, Collection<String> nameOverridesSet) {\n        for (int i = sharedElements.size() - 1; i >= 0; i--) {\n            View view = sharedElements.valueAt(i);\n            if (nameOverridesSet.contains(ViewCompat.getTransitionName(view))) {\n                views.add(view);\n            }\n        }\n    }\n\n    /**\n     * Configures the shared elements of an ordered fragment transaction's transition.\n     * This retrieves the shared elements of the incoming fragments, and schedules capturing\n     * the incoming fragment's shared elements. It also maps the views, and sets up the epicenter\n     * on the transitions.\n     * <p>\n     * The epicenter of exit and shared element transitions is the first shared element\n     * in the outgoing fragment. The epicenter of the entering transition is the first shared\n     * element in the incoming fragment.\n     *\n     * @param sceneRoot The fragment container View\n     * @param nonExistentView A View that does not exist in the hierarchy. This is used to\n     *                        prevent transitions from acting on other Views when there is no\n     *                        other target.\n     * @param nameOverrides A map of the shared element names from the starting fragment to\n     *                      the final fragment's Views as given in\n     *                      {@link FragmentTransaction#addSharedElement(View, String)}.\n     * @param fragments A structure holding the transitioning fragments in this container.\n     * @param sharedElementsOut A list modified to contain the shared elements in the outgoing\n     *                          fragment\n     * @param sharedElementsIn A list modified to contain the shared elements in the incoming\n     *                         fragment\n     * @param enterTransition The transition used for entering Views, modified by applying the\n     *                        epicenter\n     * @param exitTransition The transition used for exiting Views, modified by applying the\n     *                       epicenter\n     * @return The shared element transition or null if no shared elements exist\n     */\n    private static Object configureSharedElementsOrdered(final FragmentTransitionImpl impl,\n            final ViewGroup sceneRoot,\n            final View nonExistentView, final ArrayMap<String, String> nameOverrides,\n            final FragmentContainerTransition fragments,\n            final ArrayList<View> sharedElementsOut,\n            final ArrayList<View> sharedElementsIn,\n            final Object enterTransition, final Object exitTransition) {\n        final Fragment inFragment = fragments.lastIn;\n        final Fragment outFragment = fragments.firstOut;\n\n        if (inFragment == null || outFragment == null) {\n            return null; // no transition\n        }\n\n        final boolean inIsPop = fragments.lastInIsPop;\n        Object sharedElementTransition = nameOverrides.isEmpty() ? null\n                : getSharedElementTransition(impl, inFragment, outFragment, inIsPop);\n\n        ArrayMap<String, View> outSharedElements = captureOutSharedElements(impl, nameOverrides,\n                sharedElementTransition, fragments);\n\n        if (nameOverrides.isEmpty()) {\n            sharedElementTransition = null;\n        } else {\n            sharedElementsOut.addAll(outSharedElements.values());\n        }\n\n        if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {\n            // don't call onSharedElementStart/End since there is no transition\n            return null;\n        }\n\n        callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true);\n\n        final Rect inEpicenter;\n        if (sharedElementTransition != null) {\n            inEpicenter = new Rect();\n            impl.setSharedElementTargets(sharedElementTransition,\n                    nonExistentView, sharedElementsOut);\n            final boolean outIsPop = fragments.firstOutIsPop;\n            final BackStackRecord outTransaction = fragments.firstOutTransaction;\n            setOutEpicenter(impl, sharedElementTransition, exitTransition, outSharedElements,\n                    outIsPop, outTransaction);\n            if (enterTransition != null) {\n                impl.setEpicenter(enterTransition, inEpicenter);\n            }\n        } else {\n            inEpicenter = null;\n        }\n\n\n        final Object finalSharedElementTransition = sharedElementTransition;\n        OneShotPreDrawListener.add(sceneRoot, new Runnable() {\n            @Override\n            public void run() {\n                ArrayMap<String, View> inSharedElements = captureInSharedElements(impl,\n                        nameOverrides, finalSharedElementTransition, fragments);\n\n                if (inSharedElements != null) {\n                    sharedElementsIn.addAll(inSharedElements.values());\n                    sharedElementsIn.add(nonExistentView);\n                }\n\n                callSharedElementStartEnd(inFragment, outFragment, inIsPop,\n                        inSharedElements, false);\n                if (finalSharedElementTransition != null) {\n                    impl.swapSharedElementTargets(\n                            finalSharedElementTransition, sharedElementsOut,\n                            sharedElementsIn);\n\n                    final View inEpicenterView = getInEpicenterView(inSharedElements,\n                            fragments, enterTransition, inIsPop);\n                    if (inEpicenterView != null) {\n                        impl.getBoundsOnScreen(inEpicenterView,\n                                inEpicenter);\n                    }\n                }\n            }\n        });\n\n        return sharedElementTransition;\n    }\n\n    /**\n     * Finds the shared elements in the outgoing fragment. It also calls\n     * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control\n     * of the shared element mapping. {@code nameOverrides} is updated to match the\n     * actual transition name of the mapped shared elements.\n     *\n     * @param nameOverrides A map of the shared element names from the starting fragment to\n     *                      the final fragment's Views as given in\n     *                      {@link FragmentTransaction#addSharedElement(View, String)}.\n     * @param sharedElementTransition The shared element transition\n     * @param fragments A structure holding the transitioning fragments in this container.\n     * @return The mapping of shared element names to the Views in the hierarchy or null\n     * if there is no shared element transition.\n     */\n    private static ArrayMap<String, View> captureOutSharedElements(FragmentTransitionImpl impl,\n            ArrayMap<String, String> nameOverrides, Object sharedElementTransition,\n            FragmentContainerTransition fragments) {\n        if (nameOverrides.isEmpty() || sharedElementTransition == null) {\n            nameOverrides.clear();\n            return null;\n        }\n        final Fragment outFragment = fragments.firstOut;\n        final ArrayMap<String, View> outSharedElements = new ArrayMap<>();\n        impl.findNamedViews(outSharedElements, outFragment.requireView());\n\n        final SharedElementCallback sharedElementCallback;\n        final ArrayList<String> names;\n        final BackStackRecord outTransaction = fragments.firstOutTransaction;\n        if (fragments.firstOutIsPop) {\n            sharedElementCallback = outFragment.getEnterTransitionCallback();\n            names = outTransaction.mSharedElementTargetNames;\n        } else {\n            sharedElementCallback = outFragment.getExitTransitionCallback();\n            names = outTransaction.mSharedElementSourceNames;\n        }\n\n        outSharedElements.retainAll(names);\n        if (sharedElementCallback != null) {\n            sharedElementCallback.onMapSharedElements(names, outSharedElements);\n            for (int i = names.size() - 1; i >= 0; i--) {\n                String name = names.get(i);\n                View view = outSharedElements.get(name);\n                if (view == null) {\n                    nameOverrides.remove(name);\n                } else if (!name.equals(ViewCompat.getTransitionName(view))) {\n                    String targetValue = nameOverrides.remove(name);\n                    nameOverrides.put(ViewCompat.getTransitionName(view), targetValue);\n                }\n            }\n        } else {\n            nameOverrides.retainAll(outSharedElements.keySet());\n        }\n        return outSharedElements;\n    }\n\n    /**\n     * Finds the shared elements in the incoming fragment. It also calls\n     * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control\n     * of the shared element mapping. {@code nameOverrides} is updated to match the\n     * actual transition name of the mapped shared elements.\n     *\n     * @param nameOverrides A map of the shared element names from the starting fragment to\n     *                      the final fragment's Views as given in\n     *                      {@link FragmentTransaction#addSharedElement(View, String)}.\n     * @param sharedElementTransition The shared element transition\n     * @param fragments A structure holding the transitioning fragments in this container.\n     * @return The mapping of shared element names to the Views in the hierarchy or null\n     * if there is no shared element transition.\n     */\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    static ArrayMap<String, View> captureInSharedElements(FragmentTransitionImpl impl,\n            ArrayMap<String, String> nameOverrides, Object sharedElementTransition,\n            FragmentContainerTransition fragments) {\n        Fragment inFragment = fragments.lastIn;\n        final View fragmentView = inFragment.getView();\n        if (nameOverrides.isEmpty() || sharedElementTransition == null || fragmentView == null) {\n            nameOverrides.clear();\n            return null;\n        }\n        final ArrayMap<String, View> inSharedElements = new ArrayMap<>();\n        impl.findNamedViews(inSharedElements, fragmentView);\n\n        final SharedElementCallback sharedElementCallback;\n        final ArrayList<String> names;\n        final BackStackRecord inTransaction = fragments.lastInTransaction;\n        if (fragments.lastInIsPop) {\n            sharedElementCallback = inFragment.getExitTransitionCallback();\n            names = inTransaction.mSharedElementSourceNames;\n        } else {\n            sharedElementCallback = inFragment.getEnterTransitionCallback();\n            names = inTransaction.mSharedElementTargetNames;\n        }\n\n        if (names != null) {\n            inSharedElements.retainAll(names);\n            inSharedElements.retainAll(nameOverrides.values());\n        }\n        if (sharedElementCallback != null) {\n            sharedElementCallback.onMapSharedElements(names, inSharedElements);\n            for (int i = names.size() - 1; i >= 0; i--) {\n                String name = names.get(i);\n                View view = inSharedElements.get(name);\n                if (view == null) {\n                    String key = findKeyForValue(nameOverrides, name);\n                    if (key != null) {\n                        nameOverrides.remove(key);\n                    }\n                } else if (!name.equals(ViewCompat.getTransitionName(view))) {\n                    String key = findKeyForValue(nameOverrides, name);\n                    if (key != null) {\n                        nameOverrides.put(key, ViewCompat.getTransitionName(view));\n                    }\n                }\n            }\n        } else {\n            retainValues(nameOverrides, inSharedElements);\n        }\n        return inSharedElements;\n    }\n\n    /**\n     * Utility to find the String key in {@code map} that maps to {@code value}.\n     */\n    private static String findKeyForValue(ArrayMap<String, String> map, String value) {\n        final int numElements = map.size();\n        for (int i = 0; i < numElements; i++) {\n            if (value.equals(map.valueAt(i))) {\n                return map.keyAt(i);\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Returns the View in the incoming Fragment that should be used as the epicenter.\n     *\n     * @param inSharedElements The mapping of shared element names to Views in the\n     *                         incoming fragment.\n     * @param fragments A structure holding the transitioning fragments in this container.\n     * @param enterTransition The transition used for the incoming Fragment's views\n     * @param inIsPop Is the incoming fragment being added as a pop transaction?\n     */\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    static View getInEpicenterView(ArrayMap<String, View> inSharedElements,\n            FragmentContainerTransition fragments,\n            Object enterTransition, boolean inIsPop) {\n        BackStackRecord inTransaction = fragments.lastInTransaction;\n        if (enterTransition != null && inSharedElements != null\n                && inTransaction.mSharedElementSourceNames != null\n                && !inTransaction.mSharedElementSourceNames.isEmpty()) {\n            final String targetName = inIsPop\n                    ? inTransaction.mSharedElementSourceNames.get(0)\n                    : inTransaction.mSharedElementTargetNames.get(0);\n            return inSharedElements.get(targetName);\n        }\n        return null;\n    }\n\n    /**\n     * Sets the epicenter for the exit transition.\n     *\n     * @param sharedElementTransition The shared element transition\n     * @param exitTransition The transition for the outgoing fragment's views\n     * @param outSharedElements Shared elements in the outgoing fragment\n     * @param outIsPop Is the outgoing fragment being removed as a pop transaction?\n     * @param outTransaction The transaction that caused the fragment to be removed.\n     */\n    private static void setOutEpicenter(FragmentTransitionImpl impl, Object sharedElementTransition,\n            Object exitTransition, ArrayMap<String, View> outSharedElements, boolean outIsPop,\n            BackStackRecord outTransaction) {\n        if (outTransaction.mSharedElementSourceNames != null\n                && !outTransaction.mSharedElementSourceNames.isEmpty()) {\n            final String sourceName = outIsPop\n                    ? outTransaction.mSharedElementTargetNames.get(0)\n                    : outTransaction.mSharedElementSourceNames.get(0);\n            final View outEpicenterView = outSharedElements.get(sourceName);\n            impl.setEpicenter(sharedElementTransition, outEpicenterView);\n\n            if (exitTransition != null) {\n                impl.setEpicenter(exitTransition, outEpicenterView);\n            }\n        }\n    }\n\n    /**\n     * A utility to retain only the mappings in {@code nameOverrides} that have a value\n     * that has a key in {@code namedViews}. This is a useful equivalent to\n     * {@link ArrayMap#retainAll(Collection)} for values.\n     */\n    private static void retainValues(ArrayMap<String, String> nameOverrides,\n            ArrayMap<String, View> namedViews) {\n        for (int i = nameOverrides.size() - 1; i >= 0; i--) {\n            final String targetName = nameOverrides.valueAt(i);\n            if (!namedViews.containsKey(targetName)) {\n                nameOverrides.removeAt(i);\n            }\n        }\n    }\n\n    /**\n     * Calls the {@link SharedElementCallback#onSharedElementStart(List, List, List)} or\n     * {@link SharedElementCallback#onSharedElementEnd(List, List, List)} on the appropriate\n     * incoming or outgoing fragment.\n     *\n     * @param inFragment The incoming fragment\n     * @param outFragment The outgoing fragment\n     * @param isPop Is the incoming fragment part of a pop transaction?\n     * @param sharedElements The shared element Views\n     * @param isStart Call the start or end call on the SharedElementCallback\n     */\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    static void callSharedElementStartEnd(Fragment inFragment, Fragment outFragment,\n            boolean isPop, ArrayMap<String, View> sharedElements, boolean isStart) {\n        SharedElementCallback sharedElementCallback = isPop\n                ? outFragment.getEnterTransitionCallback()\n                : inFragment.getEnterTransitionCallback();\n        if (sharedElementCallback != null) {\n            ArrayList<View> views = new ArrayList<>();\n            ArrayList<String> names = new ArrayList<>();\n            final int count = sharedElements == null ? 0 : sharedElements.size();\n            for (int i = 0; i < count; i++) {\n                names.add(sharedElements.keyAt(i));\n                views.add(sharedElements.valueAt(i));\n            }\n            if (isStart) {\n                sharedElementCallback.onSharedElementStart(names, views, null);\n            } else {\n                sharedElementCallback.onSharedElementEnd(names, views, null);\n            }\n        }\n    }\n\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    static ArrayList<View> configureEnteringExitingViews(FragmentTransitionImpl impl,\n            Object transition,\n            Fragment fragment, ArrayList<View> sharedElements, View nonExistentView) {\n        ArrayList<View> viewList = null;\n        if (transition != null) {\n            viewList = new ArrayList<>();\n            View root = fragment.getView();\n            if (root != null) {\n                impl.captureTransitioningViews(viewList, root);\n            }\n            if (sharedElements != null) {\n                viewList.removeAll(sharedElements);\n            }\n            if (!viewList.isEmpty()) {\n                viewList.add(nonExistentView);\n                impl.addTargets(transition, viewList);\n            }\n        }\n        return viewList;\n    }\n\n    /**\n     * Sets the visibility of all Views in {@code views} to {@code visibility}.\n     */\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    static void setViewVisibility(ArrayList<View> views, int visibility) {\n        if (views == null) {\n            return;\n        }\n        for (int i = views.size() - 1; i >= 0; i--) {\n            final View view = views.get(i);\n            view.setVisibility(visibility);\n        }\n    }\n\n    /**\n     * Merges exit, shared element, and enter transitions so that they act together or\n     * sequentially as defined in the fragments.\n     */\n    private static Object mergeTransitions(FragmentTransitionImpl impl, Object enterTransition,\n            Object exitTransition, Object sharedElementTransition, Fragment inFragment,\n            boolean isPop) {\n        boolean overlap = true;\n        if (enterTransition != null && exitTransition != null && inFragment != null) {\n            overlap = isPop ? inFragment.getAllowReturnTransitionOverlap() :\n                    inFragment.getAllowEnterTransitionOverlap();\n        }\n\n        // Wrap the transitions. Explicit targets like in enter and exit will cause the\n        // views to be targeted regardless of excluded views. If that happens, then the\n        // excluded fragments views (hidden fragments) will still be in the transition.\n\n        Object transition;\n        if (overlap) {\n            // Regular transition -- do it all together\n            transition = impl.mergeTransitionsTogether(exitTransition,\n                    enterTransition, sharedElementTransition);\n        } else {\n            // First do exit, then enter, but allow shared element transition to happen\n            // during both.\n            transition = impl.mergeTransitionsInSequence(exitTransition,\n                    enterTransition, sharedElementTransition);\n        }\n        return transition;\n    }\n\n    /**\n     * Finds the first removed fragment and last added fragments when going forward.\n     * If none of the fragments have transitions, then both lists will be empty.\n     *\n     * @param transitioningFragments Keyed on the container ID, the first fragments to be removed,\n     *                               and last fragments to be added. This will be modified by\n     *                               this method.\n     */\n    public static void calculateFragments(BackStackRecord transaction,\n            SparseArray<FragmentContainerTransition> transitioningFragments,\n            boolean isReordered) {\n        final int numOps = transaction.mOps.size();\n        for (int opNum = 0; opNum < numOps; opNum++) {\n            final BackStackRecord.Op op = transaction.mOps.get(opNum);\n            addToFirstInLastOut(transaction, op, transitioningFragments, false, isReordered);\n        }\n    }\n\n    /**\n     * Finds the first removed fragment and last added fragments when popping the back stack.\n     * If none of the fragments have transitions, then both lists will be empty.\n     *\n     * @param transitioningFragments Keyed on the container ID, the first fragments to be removed,\n     *                               and last fragments to be added. This will be modified by\n     *                               this method.\n     */\n    public static void calculatePopFragments(BackStackRecord transaction,\n            SparseArray<FragmentContainerTransition> transitioningFragments, boolean isReordered) {\n        if (!transaction.mManager.mContainer.onHasView()) {\n            return; // nothing to see, so no transitions\n        }\n        final int numOps = transaction.mOps.size();\n        for (int opNum = numOps - 1; opNum >= 0; opNum--) {\n            final BackStackRecord.Op op = transaction.mOps.get(opNum);\n            addToFirstInLastOut(transaction, op, transitioningFragments, true, isReordered);\n        }\n    }\n\n    static boolean supportsTransition() {\n        return PLATFORM_IMPL != null || SUPPORT_IMPL != null;\n    }\n\n    /**\n     * Examines the {@code command} and may set the first out or last in fragment for the fragment's\n     * container.\n     *\n     * @param transaction The executing transaction\n     * @param op The operation being run.\n     * @param transitioningFragments A structure holding the first in and last out fragments\n     *                               for each fragment container.\n     * @param isPop Is the operation a pop?\n     * @param isReorderedTransaction True if the operations have been partially executed and the\n     *                               added fragments have Views in the hierarchy or false if the\n     *                               operations haven't been executed yet.\n     */\n    @SuppressWarnings(\"ReferenceEquality\")\n    private static void addToFirstInLastOut(BackStackRecord transaction, BackStackRecord.Op op,\n            SparseArray<FragmentContainerTransition> transitioningFragments, boolean isPop,\n            boolean isReorderedTransaction) {\n        final Fragment fragment = op.mFragment;\n        if (fragment == null) {\n            return; // no fragment, no transition\n        }\n        final int containerId = fragment.mContainerId;\n        if (containerId == 0) {\n            return; // no container, no transition\n        }\n        final int command = isPop ? INVERSE_OPS[op.mCmd] : op.mCmd;\n        boolean setLastIn = false;\n        boolean wasRemoved = false;\n        boolean setFirstOut = false;\n        boolean wasAdded = false;\n        switch (command) {\n            case BackStackRecord.OP_SHOW:\n                if (isReorderedTransaction) {\n                    setLastIn = fragment.mHiddenChanged && !fragment.mHidden && fragment.mAdded;\n                } else {\n                    setLastIn = fragment.mHidden;\n                }\n                wasAdded = true;\n                break;\n            case BackStackRecord.OP_ADD:\n            case BackStackRecord.OP_ATTACH:\n                if (isReorderedTransaction) {\n                    setLastIn = fragment.mIsNewlyAdded;\n                } else {\n                    setLastIn = !fragment.mAdded && !fragment.mHidden;\n                }\n                wasAdded = true;\n                break;\n            case BackStackRecord.OP_HIDE:\n                if (isReorderedTransaction) {\n                    setFirstOut = fragment.mHiddenChanged && fragment.mAdded && fragment.mHidden;\n                } else {\n                    setFirstOut = fragment.mAdded && !fragment.mHidden;\n                }\n                wasRemoved = true;\n                break;\n            case BackStackRecord.OP_REMOVE:\n            case BackStackRecord.OP_DETACH:\n                if (isReorderedTransaction) {\n                    setFirstOut = !fragment.mAdded && fragment.mView != null\n                            && fragment.mView.getVisibility() == View.VISIBLE\n                            && fragment.mPostponedAlpha >= 0;\n                } else {\n                    setFirstOut = fragment.mAdded && !fragment.mHidden;\n                }\n                wasRemoved = true;\n                break;\n        }\n        FragmentContainerTransition containerTransition = transitioningFragments.get(containerId);\n        if (setLastIn) {\n            containerTransition =\n                    ensureContainer(containerTransition, transitioningFragments, containerId);\n            containerTransition.lastIn = fragment;\n            containerTransition.lastInIsPop = isPop;\n            containerTransition.lastInTransaction = transaction;\n        }\n        if (!isReorderedTransaction && wasAdded) {\n            if (containerTransition != null && containerTransition.firstOut == fragment) {\n                containerTransition.firstOut = null;\n            }\n\n            /*\n             * Ensure that fragments that are entering are at least at the CREATED state\n             * so that they may load Transitions using TransitionInflater.\n             */\n            FragmentManagerImpl manager = transaction.mManager;\n            if (fragment.mState < Fragment.CREATED && manager.mCurState >= Fragment.CREATED\n                    && !transaction.mReorderingAllowed) {\n                manager.makeActive(fragment);\n                manager.moveToState(fragment, Fragment.CREATED, 0, 0, false);\n            }\n        }\n        if (setFirstOut && (containerTransition == null || containerTransition.firstOut == null)) {\n            containerTransition =\n                    ensureContainer(containerTransition, transitioningFragments, containerId);\n            containerTransition.firstOut = fragment;\n            containerTransition.firstOutIsPop = isPop;\n            containerTransition.firstOutTransaction = transaction;\n        }\n\n        if (!isReorderedTransaction && wasRemoved\n                && (containerTransition != null && containerTransition.lastIn == fragment)) {\n            containerTransition.lastIn = null;\n        }\n    }\n\n    /**\n     * Ensures that a FragmentContainerTransition has been added to the SparseArray. If so,\n     * it returns the existing one. If not, one is created and added to the SparseArray and\n     * returned.\n     */\n    private static FragmentContainerTransition ensureContainer(\n            FragmentContainerTransition containerTransition,\n            SparseArray<FragmentContainerTransition> transitioningFragments, int containerId) {\n        if (containerTransition == null) {\n            containerTransition = new FragmentContainerTransition();\n            transitioningFragments.put(containerId, containerTransition);\n        }\n        return containerTransition;\n    }\n\n    /**\n     * Tracks the last fragment added and first fragment removed for fragment transitions.\n     * This also tracks which fragments are changed by push or pop transactions.\n     */\n    static class FragmentContainerTransition {\n        /**\n         * The last fragment added/attached/shown in its container\n         */\n        public Fragment lastIn;\n\n        /**\n         * true when lastIn was added during a pop transaction or false if added with a push\n         */\n        public boolean lastInIsPop;\n\n        /**\n         * The transaction that included the last in fragment\n         */\n        public BackStackRecord lastInTransaction;\n\n        /**\n         * The first fragment with a View that was removed/detached/hidden in its container.\n         */\n        public Fragment firstOut;\n\n        /**\n         * true when firstOut was removed during a pop transaction or false otherwise\n         */\n        public boolean firstOutIsPop;\n\n        /**\n         * The transaction that included the first out fragment\n         */\n        public BackStackRecord firstOutTransaction;\n    }\n\n    private FragmentTransition() {\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransitionCompat21.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.graphics.Rect;\nimport android.transition.Transition;\nimport android.transition.TransitionManager;\nimport android.transition.TransitionSet;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.RequiresApi;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@RequiresApi(21)\nclass FragmentTransitionCompat21 extends FragmentTransitionImpl {\n\n    @Override\n    public boolean canHandle(Object transition) {\n        return transition instanceof Transition;\n    }\n\n    @Override\n    public Object cloneTransition(Object transition) {\n        Transition copy = null;\n        if (transition != null) {\n            copy = ((Transition) transition).clone();\n        }\n        return copy;\n    }\n\n    @Override\n    public Object wrapTransitionInSet(Object transition) {\n        if (transition == null) {\n            return null;\n        }\n        TransitionSet transitionSet = new TransitionSet();\n        transitionSet.addTransition((Transition) transition);\n        return transitionSet;\n    }\n\n    @Override\n    public void setSharedElementTargets(Object transitionObj,\n            View nonExistentView, ArrayList<View> sharedViews) {\n        TransitionSet transition = (TransitionSet) transitionObj;\n        final List<View> views = transition.getTargets();\n        views.clear();\n        final int count = sharedViews.size();\n        for (int i = 0; i < count; i++) {\n            final View view = sharedViews.get(i);\n            bfsAddViewChildren(views, view);\n        }\n        views.add(nonExistentView);\n        sharedViews.add(nonExistentView);\n        addTargets(transition, sharedViews);\n    }\n\n    @Override\n    public void setEpicenter(Object transitionObj, View view) {\n        if (view != null) {\n            Transition transition = (Transition) transitionObj;\n            final Rect epicenter = new Rect();\n            getBoundsOnScreen(view, epicenter);\n\n            transition.setEpicenterCallback(new Transition.EpicenterCallback() {\n                @Override\n                public Rect onGetEpicenter(Transition transition) {\n                    return epicenter;\n                }\n            });\n        }\n    }\n\n    @Override\n    public void addTargets(Object transitionObj, ArrayList<View> views) {\n        Transition transition = (Transition) transitionObj;\n        if (transition == null) {\n            return;\n        }\n        if (transition instanceof TransitionSet) {\n            TransitionSet set = (TransitionSet) transition;\n            int numTransitions = set.getTransitionCount();\n            for (int i = 0; i < numTransitions; i++) {\n                Transition child = set.getTransitionAt(i);\n                addTargets(child, views);\n            }\n        } else if (!hasSimpleTarget(transition)) {\n            List<View> targets = transition.getTargets();\n            if (isNullOrEmpty(targets)) {\n                // We can just add the target views\n                int numViews = views.size();\n                for (int i = 0; i < numViews; i++) {\n                    transition.addTarget(views.get(i));\n                }\n            }\n        }\n    }\n\n    /**\n     * Returns true if there are any targets based on ID, transition or type.\n     */\n    private static boolean hasSimpleTarget(Transition transition) {\n        return !isNullOrEmpty(transition.getTargetIds())\n                || !isNullOrEmpty(transition.getTargetNames())\n                || !isNullOrEmpty(transition.getTargetTypes());\n    }\n\n    @Override\n    public Object mergeTransitionsTogether(Object transition1, Object transition2,\n            Object transition3) {\n        TransitionSet transitionSet = new TransitionSet();\n        if (transition1 != null) {\n            transitionSet.addTransition((Transition) transition1);\n        }\n        if (transition2 != null) {\n            transitionSet.addTransition((Transition) transition2);\n        }\n        if (transition3 != null) {\n            transitionSet.addTransition((Transition) transition3);\n        }\n        return transitionSet;\n    }\n\n    @Override\n    public void scheduleHideFragmentView(Object exitTransitionObj, final View fragmentView,\n            final ArrayList<View> exitingViews) {\n        Transition exitTransition = (Transition) exitTransitionObj;\n        exitTransition.addListener(new Transition.TransitionListener() {\n            @Override\n            public void onTransitionStart(Transition transition) {\n            }\n\n            @Override\n            public void onTransitionEnd(Transition transition) {\n                transition.removeListener(this);\n                fragmentView.setVisibility(View.GONE);\n                final int numViews = exitingViews.size();\n                for (int i = 0; i < numViews; i++) {\n                    exitingViews.get(i).setVisibility(View.VISIBLE);\n                }\n            }\n\n            @Override\n            public void onTransitionCancel(Transition transition) {\n            }\n\n            @Override\n            public void onTransitionPause(Transition transition) {\n            }\n\n            @Override\n            public void onTransitionResume(Transition transition) {\n            }\n        });\n    }\n\n    @Override\n    public Object mergeTransitionsInSequence(Object exitTransitionObj,\n            Object enterTransitionObj, Object sharedElementTransitionObj) {\n        // First do exit, then enter, but allow shared element transition to happen\n        // during both.\n        Transition staggered = null;\n        final Transition exitTransition = (Transition) exitTransitionObj;\n        final Transition enterTransition = (Transition) enterTransitionObj;\n        final Transition sharedElementTransition = (Transition) sharedElementTransitionObj;\n        if (exitTransition != null && enterTransition != null) {\n            staggered = new TransitionSet()\n                    .addTransition(exitTransition)\n                    .addTransition(enterTransition)\n                    .setOrdering(TransitionSet.ORDERING_SEQUENTIAL);\n        } else if (exitTransition != null) {\n            staggered = exitTransition;\n        } else if (enterTransition != null) {\n            staggered = enterTransition;\n        }\n        if (sharedElementTransition != null) {\n            TransitionSet together = new TransitionSet();\n            if (staggered != null) {\n                together.addTransition(staggered);\n            }\n            together.addTransition(sharedElementTransition);\n            return together;\n        } else {\n            return staggered;\n        }\n    }\n\n    @Override\n    public void beginDelayedTransition(ViewGroup sceneRoot, Object transition) {\n        TransitionManager.beginDelayedTransition(sceneRoot, (Transition) transition);\n    }\n\n    @Override\n    public void scheduleRemoveTargets(final Object overallTransitionObj,\n            final Object enterTransition, final ArrayList<View> enteringViews,\n            final Object exitTransition, final ArrayList<View> exitingViews,\n            final Object sharedElementTransition, final ArrayList<View> sharedElementsIn) {\n        final Transition overallTransition = (Transition) overallTransitionObj;\n        overallTransition.addListener(new Transition.TransitionListener() {\n            @Override\n            public void onTransitionStart(Transition transition) {\n                if (enterTransition != null) {\n                    replaceTargets(enterTransition, enteringViews, null);\n                }\n                if (exitTransition != null) {\n                    replaceTargets(exitTransition, exitingViews, null);\n                }\n                if (sharedElementTransition != null) {\n                    replaceTargets(sharedElementTransition, sharedElementsIn, null);\n                }\n            }\n\n            @Override\n            public void onTransitionEnd(Transition transition) {\n                transition.removeListener(this);\n            }\n\n            @Override\n            public void onTransitionCancel(Transition transition) {\n            }\n\n            @Override\n            public void onTransitionPause(Transition transition) {\n            }\n\n            @Override\n            public void onTransitionResume(Transition transition) {\n            }\n        });\n    }\n\n    @Override\n    public void swapSharedElementTargets(Object sharedElementTransitionObj,\n            ArrayList<View> sharedElementsOut, ArrayList<View> sharedElementsIn) {\n        TransitionSet sharedElementTransition = (TransitionSet) sharedElementTransitionObj;\n        if (sharedElementTransition != null) {\n            sharedElementTransition.getTargets().clear();\n            sharedElementTransition.getTargets().addAll(sharedElementsIn);\n            replaceTargets(sharedElementTransition, sharedElementsOut, sharedElementsIn);\n        }\n    }\n\n    @Override\n    public void replaceTargets(Object transitionObj, ArrayList<View> oldTargets,\n            ArrayList<View> newTargets) {\n        Transition transition = (Transition) transitionObj;\n        if (transition instanceof TransitionSet) {\n            TransitionSet set = (TransitionSet) transition;\n            int numTransitions = set.getTransitionCount();\n            for (int i = 0; i < numTransitions; i++) {\n                Transition child = set.getTransitionAt(i);\n                replaceTargets(child, oldTargets, newTargets);\n            }\n        } else if (!hasSimpleTarget(transition)) {\n            List<View> targets = transition.getTargets();\n            if (targets != null && targets.size() == oldTargets.size()\n                    && targets.containsAll(oldTargets)) {\n                // We have an exact match. We must have added these earlier in addTargets\n                final int targetCount = newTargets == null ? 0 : newTargets.size();\n                for (int i = 0; i < targetCount; i++) {\n                    transition.addTarget(newTargets.get(i));\n                }\n                for (int i = oldTargets.size() - 1; i >= 0; i--) {\n                    transition.removeTarget(oldTargets.get(i));\n                }\n            }\n        }\n    }\n\n    @Override\n    public void addTarget(Object transitionObj, View view) {\n        if (transitionObj != null) {\n            Transition transition = (Transition) transitionObj;\n            transition.addTarget(view);\n        }\n    }\n\n    @Override\n    public void removeTarget(Object transitionObj, View view) {\n        if (transitionObj != null) {\n            Transition transition = (Transition) transitionObj;\n            transition.removeTarget(view);\n        }\n    }\n\n    @Override\n    public void setEpicenter(Object transitionObj, final Rect epicenter) {\n        if (transitionObj != null) {\n            Transition transition = (Transition) transitionObj;\n            transition.setEpicenterCallback(new Transition.EpicenterCallback() {\n                @Override\n                public Rect onGetEpicenter(Transition transition) {\n                    if (epicenter == null || epicenter.isEmpty()) {\n                        return null;\n                    }\n                    return epicenter;\n                }\n            });\n        }\n    }\n\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransitionImpl.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;\n\nimport android.annotation.SuppressLint;\nimport android.graphics.Rect;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.RestrictTo;\nimport androidx.core.view.OneShotPreDrawListener;\nimport androidx.core.view.ViewCompat;\nimport androidx.core.view.ViewGroupCompat;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\n\n/**\n * @hide\n */\n@RestrictTo(LIBRARY_GROUP_PREFIX)\n@SuppressLint(\"UnknownNullness\")\npublic abstract class FragmentTransitionImpl {\n\n    /**\n     * Returns {@code true} if this implementation can handle the specified {@link transition}.\n     */\n    public abstract boolean canHandle(Object transition);\n\n    /**\n     * Returns a clone of a transition or null if it is null\n     */\n    public abstract Object cloneTransition(Object transition);\n\n    /**\n     * Wraps a transition in a TransitionSet and returns the set. If transition is null, null is\n     * returned.\n     */\n    public abstract Object wrapTransitionInSet(Object transition);\n\n    /**\n     * Finds all children of the shared elements and sets the wrapping TransitionSet\n     * targets to point to those. It also limits transitions that have no targets to the\n     * specific shared elements. This allows developers to target child views of the\n     * shared elements specifically, but this doesn't happen by default.\n     */\n    public abstract void setSharedElementTargets(Object transitionObj,\n            View nonExistentView, ArrayList<View> sharedViews);\n\n    /**\n     * Sets a transition epicenter to the rectangle of a given View.\n     */\n    public abstract void setEpicenter(Object transitionObj, View view);\n\n    /**\n     * Replacement for view.getBoundsOnScreen because that is not public. This returns a rect\n     * containing the bounds relative to the screen that the view is in.\n     */\n    protected void getBoundsOnScreen(View view, Rect epicenter) {\n        int[] loc = new int[2];\n        view.getLocationOnScreen(loc);\n        epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight());\n    }\n\n    /**\n     * This method adds views as targets to the transition, but only if the transition\n     * doesn't already have a target. It is best for views to contain one View object\n     * that does not exist in the view hierarchy (state.nonExistentView) so that\n     * when they are removed later, a list match will suffice to remove the targets.\n     * Otherwise, if you happened to have targeted the exact views for the transition,\n     * the replaceTargets call will remove them unexpectedly.\n     */\n    public abstract void addTargets(Object transitionObj, ArrayList<View> views);\n\n    /**\n     * Creates a TransitionSet that plays all passed transitions together. Any null\n     * transitions passed will not be added to the set. If all are null, then an empty\n     * TransitionSet will be returned.\n     */\n    public abstract Object mergeTransitionsTogether(Object transition1, Object transition2,\n            Object transition3);\n\n    /**\n     * After the transition completes, the fragment's view is set to GONE and the exiting\n     * views are set to VISIBLE.\n     */\n    public abstract void scheduleHideFragmentView(Object exitTransitionObj, View fragmentView,\n            ArrayList<View> exitingViews);\n\n    /**\n     * Combines enter, exit, and shared element transition so that they play in the proper\n     * sequence. First the exit transition plays along with the shared element transition.\n     * When the exit transition completes, the enter transition starts. The shared element\n     * transition can continue running while the enter transition plays.\n     *\n     * @return A TransitionSet with all of enter, exit, and shared element transitions in\n     * it (modulo null values), ordered such that they play in the proper sequence.\n     */\n    public abstract Object mergeTransitionsInSequence(Object exitTransitionObj,\n            Object enterTransitionObj, Object sharedElementTransitionObj);\n\n    /**\n     * Calls {@code TransitionManager#beginDelayedTransition(ViewGroup, Transition)}.\n     */\n    public abstract void beginDelayedTransition(ViewGroup sceneRoot, Object transition);\n\n    /**\n     * Prepares for setting the shared element names by gathering the names of the incoming\n     * shared elements and clearing them. {@link #setNameOverridesReordered(View, ArrayList,\n     * ArrayList, ArrayList, Map)} must be called after this to complete setting the shared element\n     * name overrides. This must be called before\n     * {@link #beginDelayedTransition(ViewGroup, Object)}.\n     */\n    ArrayList<String> prepareSetNameOverridesReordered(ArrayList<View> sharedElementsIn) {\n        final ArrayList<String> names = new ArrayList<>();\n        final int numSharedElements = sharedElementsIn.size();\n        for (int i = 0; i < numSharedElements; i++) {\n            final View view = sharedElementsIn.get(i);\n            names.add(ViewCompat.getTransitionName(view));\n            ViewCompat.setTransitionName(view, null);\n        }\n        return names;\n    }\n\n    /**\n     * Changes the shared element names for the incoming shared elements to match those of the\n     * outgoing shared elements. This also temporarily clears the shared element names of the\n     * outgoing shared elements. Must be called after\n     * {@link #beginDelayedTransition(ViewGroup, Object)}.\n     */\n    void setNameOverridesReordered(final View sceneRoot,\n            final ArrayList<View> sharedElementsOut, final ArrayList<View> sharedElementsIn,\n            final ArrayList<String> inNames, final Map<String, String> nameOverrides) {\n        final int numSharedElements = sharedElementsIn.size();\n        final ArrayList<String> outNames = new ArrayList<>();\n\n        for (int i = 0; i < numSharedElements; i++) {\n            final View view = sharedElementsOut.get(i);\n            final String name = ViewCompat.getTransitionName(view);\n            outNames.add(name);\n            if (name == null) {\n                continue;\n            }\n            ViewCompat.setTransitionName(view, null);\n            final String inName = nameOverrides.get(name);\n            for (int j = 0; j < numSharedElements; j++) {\n                if (inName.equals(inNames.get(j))) {\n                    ViewCompat.setTransitionName(sharedElementsIn.get(j), name);\n                    break;\n                }\n            }\n        }\n\n        OneShotPreDrawListener.add(sceneRoot, new Runnable() {\n            @Override\n            public void run() {\n                for (int i = 0; i < numSharedElements; i++) {\n                    ViewCompat.setTransitionName(sharedElementsIn.get(i), inNames.get(i));\n                    ViewCompat.setTransitionName(sharedElementsOut.get(i), outNames.get(i));\n                }\n            }\n        });\n    }\n\n    /**\n     * Gets the Views in the hierarchy affected by entering and exiting Activity Scene transitions.\n     *\n     * @param transitioningViews This View will be added to transitioningViews if it is VISIBLE and\n     *                           a normal View or a ViewGroup with\n     *                           {@link android.view.ViewGroup#isTransitionGroup()} true.\n     * @param view               The base of the view hierarchy to look in.\n     */\n    void captureTransitioningViews(ArrayList<View> transitioningViews, View view) {\n        if (view.getVisibility() == View.VISIBLE) {\n            if (view instanceof ViewGroup) {\n                ViewGroup viewGroup = (ViewGroup) view;\n                if (ViewGroupCompat.isTransitionGroup(viewGroup)) {\n                    transitioningViews.add(viewGroup);\n                } else {\n                    int count = viewGroup.getChildCount();\n                    for (int i = 0; i < count; i++) {\n                        View child = viewGroup.getChildAt(i);\n                        captureTransitioningViews(transitioningViews, child);\n                    }\n                }\n            } else {\n                transitioningViews.add(view);\n            }\n        }\n    }\n\n    /**\n     * Finds all views that have transition names in the hierarchy under the given view and\n     * stores them in {@code namedViews} map with the name as the key.\n     */\n    void findNamedViews(Map<String, View> namedViews, @NonNull View view) {\n        if (view.getVisibility() == View.VISIBLE) {\n            String transitionName = ViewCompat.getTransitionName(view);\n            if (transitionName != null) {\n                namedViews.put(transitionName, view);\n            }\n            if (view instanceof ViewGroup) {\n                ViewGroup viewGroup = (ViewGroup) view;\n                int count = viewGroup.getChildCount();\n                for (int i = 0; i < count; i++) {\n                    View child = viewGroup.getChildAt(i);\n                    findNamedViews(namedViews, child);\n                }\n            }\n        }\n    }\n\n    /**\n     *Applies the prepared {@code nameOverrides} to the view hierarchy.\n     */\n    void setNameOverridesOrdered(final View sceneRoot,\n            final ArrayList<View> sharedElementsIn, final Map<String, String> nameOverrides) {\n        OneShotPreDrawListener.add(sceneRoot, new Runnable() {\n            @Override\n            public void run() {\n                final int numSharedElements = sharedElementsIn.size();\n                for (int i = 0; i < numSharedElements; i++) {\n                    View view = sharedElementsIn.get(i);\n                    String name = ViewCompat.getTransitionName(view);\n                    if (name != null) {\n                        String inName = findKeyForValue(nameOverrides, name);\n                        ViewCompat.setTransitionName(view, inName);\n                    }\n                }\n            }\n        });\n    }\n\n    /**\n     * After the transition has started, remove all targets that we added to the transitions\n     * so that the transitions are left in a clean state.\n     */\n    public abstract void scheduleRemoveTargets(Object overallTransitionObj,\n            Object enterTransition, ArrayList<View> enteringViews,\n            Object exitTransition, ArrayList<View> exitingViews,\n            Object sharedElementTransition, ArrayList<View> sharedElementsIn);\n\n    /**\n     * Swap the targets for the shared element transition from those Views in sharedElementsOut\n     * to those in sharedElementsIn\n     */\n    public abstract void swapSharedElementTargets(Object sharedElementTransitionObj,\n            ArrayList<View> sharedElementsOut, ArrayList<View> sharedElementsIn);\n\n    /**\n     * This method removes the views from transitions that target ONLY those views and\n     * replaces them with the new targets list.\n     * The views list should match those added in addTargets and should contain\n     * one view that is not in the view hierarchy (state.nonExistentView).\n     */\n    public abstract void replaceTargets(Object transitionObj, ArrayList<View> oldTargets,\n            ArrayList<View> newTargets);\n\n    /**\n     * Adds a View target to a transition. If transitionObj is null, nothing is done.\n     */\n    public abstract void addTarget(Object transitionObj, View view);\n\n    /**\n     * Remove a View target to a transition. If transitionObj is null, nothing is done.\n     */\n    public abstract void removeTarget(Object transitionObj, View view);\n\n    /**\n     * Sets the epicenter of a transition to a rect object. The object can be modified until\n     * the transition runs.\n     */\n    public abstract void setEpicenter(Object transitionObj, Rect epicenter);\n\n    void scheduleNameReset(final ViewGroup sceneRoot,\n            final ArrayList<View> sharedElementsIn, final Map<String, String> nameOverrides) {\n        OneShotPreDrawListener.add(sceneRoot, new Runnable() {\n            @Override\n            public void run() {\n                final int numSharedElements = sharedElementsIn.size();\n                for (int i = 0; i < numSharedElements; i++) {\n                    final View view = sharedElementsIn.get(i);\n                    final String name = ViewCompat.getTransitionName(view);\n                    final String inName = nameOverrides.get(name);\n                    ViewCompat.setTransitionName(view, inName);\n                }\n            }\n        });\n    }\n\n    /**\n     * Uses a breadth-first scheme to add startView and all of its children to views.\n     * It won't add a child if it is already in views.\n     */\n    protected static void bfsAddViewChildren(final List<View> views, final View startView) {\n        final int startIndex = views.size();\n        if (containedBeforeIndex(views, startView, startIndex)) {\n            return; // This child is already in the list, so all its children are also.\n        }\n        views.add(startView);\n        for (int index = startIndex; index < views.size(); index++) {\n            final View view = views.get(index);\n            if (view instanceof ViewGroup) {\n                ViewGroup viewGroup = (ViewGroup) view;\n                final int childCount =  viewGroup.getChildCount();\n                for (int childIndex = 0; childIndex < childCount; childIndex++) {\n                    final View child = viewGroup.getChildAt(childIndex);\n                    if (!containedBeforeIndex(views, child, startIndex)) {\n                        views.add(child);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Does a linear search through views for view, limited to maxIndex.\n     */\n    private static boolean containedBeforeIndex(final List<View> views, final View view,\n            final int maxIndex) {\n        for (int i = 0; i < maxIndex; i++) {\n            if (views.get(i) == view) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Simple utility to detect if a list is null or has no elements.\n     */\n    protected static boolean isNullOrEmpty(List list) {\n        return list == null || list.isEmpty();\n    }\n\n    /**\n     * Utility to find the String key in {@code map} that maps to {@code value}.\n     */\n    @SuppressWarnings(\"WeakerAccess\")\n    static String findKeyForValue(Map<String, String> map, String value) {\n        for (Map.Entry<String, String> entry : map.entrySet()) {\n            if (value.equals(entry.getValue())) {\n                return entry.getKey();\n            }\n        }\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.Lifecycle;\nimport androidx.lifecycle.LifecycleOwner;\nimport androidx.lifecycle.LifecycleRegistry;\n\nclass FragmentViewLifecycleOwner implements LifecycleOwner {\n    private LifecycleRegistry mLifecycleRegistry = null;\n\n    /**\n     * Initializes the underlying Lifecycle if it hasn't already been created.\n     */\n    void initialize() {\n        if (mLifecycleRegistry == null) {\n            mLifecycleRegistry = new LifecycleRegistry(this);\n        }\n    }\n\n    /**\n     * @return True if the Lifecycle has been initialized.\n     */\n    boolean isInitialized() {\n        return mLifecycleRegistry != null;\n    }\n\n    @NonNull\n    @Override\n    public Lifecycle getLifecycle() {\n        initialize();\n        return mLifecycleRegistry;\n    }\n\n    void handleLifecycleEvent(@NonNull Lifecycle.Event event) {\n        mLifecycleRegistry.handleLifecycleEvent(event);\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/ListFragment.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.content.Context;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.view.Gravity;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.animation.AnimationUtils;\nimport android.widget.AdapterView;\nimport android.widget.FrameLayout;\nimport android.widget.LinearLayout;\nimport android.widget.ListAdapter;\nimport android.widget.ListView;\nimport android.widget.ProgressBar;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\n/**\n * Static library support version of the framework's {@link android.app.ListFragment}.\n * Used to write apps that run on platforms prior to Android 3.0.  When running\n * on Android 3.0 or above, this implementation is still used; it does not try\n * to switch to the framework's implementation.  See the framework SDK\n * documentation for a class overview.\n */\npublic class ListFragment extends Fragment {\n    static final int INTERNAL_EMPTY_ID = 0x00ff0001;\n    static final int INTERNAL_PROGRESS_CONTAINER_ID = 0x00ff0002;\n    static final int INTERNAL_LIST_CONTAINER_ID = 0x00ff0003;\n\n    final private Handler mHandler = new Handler();\n\n    final private Runnable mRequestFocus = new Runnable() {\n        @Override\n        public void run() {\n            mList.focusableViewAvailable(mList);\n        }\n    };\n\n    final private AdapterView.OnItemClickListener mOnClickListener\n            = new AdapterView.OnItemClickListener() {\n        @Override\n        public void onItemClick(AdapterView<?> parent, View v, int position, long id) {\n            onListItemClick((ListView)parent, v, position, id);\n        }\n    };\n\n    ListAdapter mAdapter;\n    ListView mList;\n    View mEmptyView;\n    TextView mStandardEmptyView;\n    View mProgressContainer;\n    View mListContainer;\n    CharSequence mEmptyText;\n    boolean mListShown;\n\n    public ListFragment() {\n    }\n\n    /**\n     * Provide default implementation to return a simple list view.  Subclasses\n     * can override to replace with their own layout.  If doing so, the\n     * returned view hierarchy <em>must</em> have a ListView whose id\n     * is {@link android.R.id#list android.R.id.list} and can optionally\n     * have a sibling view id {@link android.R.id#empty android.R.id.empty}\n     * that is to be shown when the list is empty.\n     *\n     * <p>If you are overriding this method with your own custom content,\n     * consider including the standard layout {@link android.R.layout#list_content}\n     * in your layout file, so that you continue to retain all of the standard\n     * behavior of ListFragment.  In particular, this is currently the only\n     * way to have the built-in indeterminant progress state be shown.\n     */\n    @Override\n    @Nullable\n    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,\n            @Nullable Bundle savedInstanceState) {\n        final Context context = requireContext();\n\n        FrameLayout root = new FrameLayout(context);\n\n        // ------------------------------------------------------------------\n\n        LinearLayout pframe = new LinearLayout(context);\n        pframe.setId(INTERNAL_PROGRESS_CONTAINER_ID);\n        pframe.setOrientation(LinearLayout.VERTICAL);\n        pframe.setVisibility(View.GONE);\n        pframe.setGravity(Gravity.CENTER);\n\n        ProgressBar progress = new ProgressBar(context, null,\n                android.R.attr.progressBarStyleLarge);\n        pframe.addView(progress, new FrameLayout.LayoutParams(\n                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));\n\n        root.addView(pframe, new FrameLayout.LayoutParams(\n                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));\n\n        // ------------------------------------------------------------------\n\n        FrameLayout lframe = new FrameLayout(context);\n        lframe.setId(INTERNAL_LIST_CONTAINER_ID);\n\n        TextView tv = new TextView(context);\n        tv.setId(INTERNAL_EMPTY_ID);\n        tv.setGravity(Gravity.CENTER);\n        lframe.addView(tv, new FrameLayout.LayoutParams(\n                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));\n\n        ListView lv = new ListView(context);\n        lv.setId(android.R.id.list);\n        lv.setDrawSelectorOnTop(false);\n        lframe.addView(lv, new FrameLayout.LayoutParams(\n                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));\n\n        root.addView(lframe, new FrameLayout.LayoutParams(\n                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));\n\n        // ------------------------------------------------------------------\n\n        root.setLayoutParams(new FrameLayout.LayoutParams(\n                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));\n\n        return root;\n    }\n\n    /**\n     * Attach to list view once the view hierarchy has been created.\n     */\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n        ensureList();\n    }\n\n    /**\n     * Detach from list view.\n     */\n    @Override\n    public void onDestroyView() {\n        mHandler.removeCallbacks(mRequestFocus);\n        mList = null;\n        mListShown = false;\n        mEmptyView = mProgressContainer = mListContainer = null;\n        mStandardEmptyView = null;\n        super.onDestroyView();\n    }\n\n    /**\n     * This method will be called when an item in the list is selected.\n     * Subclasses should override. Subclasses can call\n     * getListView().getItemAtPosition(position) if they need to access the\n     * data associated with the selected item.\n     *\n     * @param l The ListView where the click happened\n     * @param v The view that was clicked within the ListView\n     * @param position The position of the view in the list\n     * @param id The row id of the item that was clicked\n     */\n    public void onListItemClick(@NonNull ListView l, @NonNull View v, int position, long id) {\n    }\n\n    /**\n     * Provide the cursor for the list view.\n     */\n    public void setListAdapter(@Nullable ListAdapter adapter) {\n        boolean hadAdapter = mAdapter != null;\n        mAdapter = adapter;\n        if (mList != null) {\n            mList.setAdapter(adapter);\n            if (!mListShown && !hadAdapter) {\n                // The list was hidden, and previously didn't have an\n                // adapter.  It is now time to show it.\n                setListShown(true, requireView().getWindowToken() != null);\n            }\n        }\n    }\n\n    /**\n     * Set the currently selected list item to the specified\n     * position with the adapter's data\n     *\n     * @param position\n     */\n    public void setSelection(int position) {\n        ensureList();\n        mList.setSelection(position);\n    }\n\n    /**\n     * Get the position of the currently selected list item.\n     */\n    public int getSelectedItemPosition() {\n        ensureList();\n        return mList.getSelectedItemPosition();\n    }\n\n    /**\n     * Get the cursor row ID of the currently selected list item.\n     */\n    public long getSelectedItemId() {\n        ensureList();\n        return mList.getSelectedItemId();\n    }\n\n    /**\n     * Get the fragment's list view widget.\n     */\n    @NonNull\n    public ListView getListView() {\n        ensureList();\n        return mList;\n    }\n\n    /**\n     * The default content for a ListFragment has a TextView that can\n     * be shown when the list is empty.  If you would like to have it\n     * shown, call this method to supply the text it should use.\n     */\n    public void setEmptyText(@Nullable CharSequence text) {\n        ensureList();\n        if (mStandardEmptyView == null) {\n            throw new IllegalStateException(\"Can't be used with a custom content view\");\n        }\n        mStandardEmptyView.setText(text);\n        if (mEmptyText == null) {\n            mList.setEmptyView(mStandardEmptyView);\n        }\n        mEmptyText = text;\n    }\n\n    /**\n     * Control whether the list is being displayed.  You can make it not\n     * displayed if you are waiting for the initial data to show in it.  During\n     * this time an indeterminant progress indicator will be shown instead.\n     *\n     * <p>Applications do not normally need to use this themselves.  The default\n     * behavior of ListFragment is to start with the list not being shown, only\n     * showing it once an adapter is given with {@link #setListAdapter(ListAdapter)}.\n     * If the list at that point had not been shown, when it does get shown\n     * it will be do without the user ever seeing the hidden state.\n     *\n     * @param shown If true, the list view is shown; if false, the progress\n     * indicator.  The initial value is true.\n     */\n    public void setListShown(boolean shown) {\n        setListShown(shown, true);\n    }\n\n    /**\n     * Like {@link #setListShown(boolean)}, but no animation is used when\n     * transitioning from the previous state.\n     */\n    public void setListShownNoAnimation(boolean shown) {\n        setListShown(shown, false);\n    }\n\n    /**\n     * Control whether the list is being displayed.  You can make it not\n     * displayed if you are waiting for the initial data to show in it.  During\n     * this time an indeterminant progress indicator will be shown instead.\n     *\n     * @param shown If true, the list view is shown; if false, the progress\n     * indicator.  The initial value is true.\n     * @param animate If true, an animation will be used to transition to the\n     * new state.\n     */\n    private void setListShown(boolean shown, boolean animate) {\n        ensureList();\n        if (mProgressContainer == null) {\n            throw new IllegalStateException(\"Can't be used with a custom content view\");\n        }\n        if (mListShown == shown) {\n            return;\n        }\n        mListShown = shown;\n        if (shown) {\n            if (animate) {\n                mProgressContainer.startAnimation(AnimationUtils.loadAnimation(\n                        getContext(), android.R.anim.fade_out));\n                mListContainer.startAnimation(AnimationUtils.loadAnimation(\n                        getContext(), android.R.anim.fade_in));\n            } else {\n                mProgressContainer.clearAnimation();\n                mListContainer.clearAnimation();\n            }\n            mProgressContainer.setVisibility(View.GONE);\n            mListContainer.setVisibility(View.VISIBLE);\n        } else {\n            if (animate) {\n                mProgressContainer.startAnimation(AnimationUtils.loadAnimation(\n                        getContext(), android.R.anim.fade_in));\n                mListContainer.startAnimation(AnimationUtils.loadAnimation(\n                        getContext(), android.R.anim.fade_out));\n            } else {\n                mProgressContainer.clearAnimation();\n                mListContainer.clearAnimation();\n            }\n            mProgressContainer.setVisibility(View.VISIBLE);\n            mListContainer.setVisibility(View.GONE);\n        }\n    }\n\n    /**\n     * Get the ListAdapter associated with this fragment's ListView.\n     *\n     * @see #requireListAdapter()\n     */\n    @Nullable\n    public ListAdapter getListAdapter() {\n        return mAdapter;\n    }\n\n    /**\n     * Get the ListAdapter associated with this fragment's ListView.\n     *\n     * @throws IllegalStateException if no ListAdapter has been set.\n     * @see #getListAdapter()\n     */\n    @NonNull\n    public final ListAdapter requireListAdapter() {\n        ListAdapter listAdapter = getListAdapter();\n        if (listAdapter == null) {\n            throw new IllegalStateException(\"ListFragment \" + this\n                    + \" does not have a ListAdapter.\");\n        }\n        return listAdapter;\n    }\n\n    private void ensureList() {\n        if (mList != null) {\n            return;\n        }\n        View root = getView();\n        if (root == null) {\n            throw new IllegalStateException(\"Content view not yet created\");\n        }\n        if (root instanceof ListView) {\n            mList = (ListView)root;\n        } else {\n            mStandardEmptyView = (TextView)root.findViewById(INTERNAL_EMPTY_ID);\n            if (mStandardEmptyView == null) {\n                mEmptyView = root.findViewById(android.R.id.empty);\n            } else {\n                mStandardEmptyView.setVisibility(View.GONE);\n            }\n            mProgressContainer = root.findViewById(INTERNAL_PROGRESS_CONTAINER_ID);\n            mListContainer = root.findViewById(INTERNAL_LIST_CONTAINER_ID);\n            View rawListView = root.findViewById(android.R.id.list);\n            if (!(rawListView instanceof ListView)) {\n                if (rawListView == null) {\n                    throw new RuntimeException(\n                            \"Your content must have a ListView whose id attribute is \" +\n                            \"'android.R.id.list'\");\n                }\n                throw new RuntimeException(\n                        \"Content has view with id attribute 'android.R.id.list' \"\n                        + \"that is not a ListView class\");\n            }\n            mList = (ListView)rawListView;\n            if (mEmptyView != null) {\n                mList.setEmptyView(mEmptyView);\n            } else if (mEmptyText != null) {\n                mStandardEmptyView.setText(mEmptyText);\n                mList.setEmptyView(mStandardEmptyView);\n            }\n        }\n        mListShown = true;\n        mList.setOnItemClickListener(mOnClickListener);\n        if (mAdapter != null) {\n            ListAdapter adapter = mAdapter;\n            mAdapter = null;\n            setListAdapter(adapter);\n        } else {\n            // We are starting without an adapter, so assume we won't\n            // have our data right away and start with the progress indicator.\n            if (mProgressContainer != null) {\n                setListShown(false, false);\n            }\n        }\n        mHandler.post(mRequestFocus);\n    }\n}\n"
  },
  {
    "path": "fragment-1.1.0/src/main/java/androidx/fragment/app/SuperNotCalledException.java",
    "content": "/*\n * Copyright 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 androidx.fragment.app;\n\nimport android.util.AndroidRuntimeException;\n\nfinal class SuperNotCalledException extends AndroidRuntimeException {\n    public SuperNotCalledException(String msg) {\n        super(msg);\n    }\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Thu Jan 10 09:45:36 PST 2019\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.5-bin.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "android.enableJetifier=true\nandroid.useAndroidX=true\n# Jetifier with Robolectric transform fix:\n# Add android.jetifier.blacklist=bcprov or android.enableJetifier=false to gradle.properties. or upgrade to 7.1.x of AGP (Android Gradle Plugin).\n#android.jetifier.blacklist=bcprov\nandroid.jetifier.ignorelist=bcprov\n# Get ready for minification (NoSuchMethodError on kivi???)\n#android.enableR8.fullMode=true\n# Minification ram usage fix\n#org.gradle.jvmargs=-Xmx1024m\norg.gradle.jvmargs=-Xmx3000m\nandroid.suppressUnsupportedCompileSdk=34\nandroid.disableAutomaticComponentCreation=true"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\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\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\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\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\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\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\" ] ; 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# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\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\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\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem My tweak. Cleanup cache (fix build errors)\r\nrd /s /q \"%DIRNAME%\\.gradle\"\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 Windowz variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\nif \"%@eval[2+2]\" == \"4\" goto 4NT_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\ngoto execute\r\n\r\n:4NT_args\r\n@rem Get arguments from the 4NT Shell from JP Software\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": "leanback-1.0.0/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "leanback-1.0.0/LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "leanback-1.0.0/build.gradle",
    "content": "apply from: gradle.ext.sharedModulesConstants\r\napply plugin: 'com.android.library'\r\n\r\nandroid {\r\n    // FIX: Default interface methods are only supported starting with Android N (--min-api 24)\r\n    compileOptions {\r\n        sourceCompatibility JavaVersion.VERSION_1_8\r\n        targetCompatibility JavaVersion.VERSION_1_8\r\n    }\r\n\r\n    compileSdkVersion project.properties.compileSdkVersion\r\n    buildToolsVersion project.properties.buildToolsVersion\r\n    testOptions.unitTests.includeAndroidResources = true\r\n\r\n    defaultConfig {\r\n        minSdkVersion project.properties.minSdkVersion\r\n        targetSdkVersion project.properties.targetSdkVersion\r\n        versionCode 10\r\n        versionName \"1.0.0\"\r\n\r\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\r\n        consumerProguardFiles 'consumer-rules.pro'\r\n    }\r\n\r\n    buildTypes {\r\n        release {\r\n            minifyEnabled false\r\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\r\n        }\r\n    }\r\n}\r\n\r\ndependencies {\r\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\r\n\r\n    implementation 'androidx.annotation:annotation:' + annotationXVersion\r\n    implementation 'androidx.core:core:' + coreXVersion\r\n    implementation 'androidx.recyclerview:recyclerview:' + recyclerviewXVersion\r\n    implementation 'androidx.media:media:' + mediaXVersion\r\n    implementation 'androidx.interpolator:interpolator:' + interpolatorXVersion\r\n\r\n    implementation project(':fragment-1.1.0')\r\n    implementation project(':sharedutils')\r\n}\r\n"
  },
  {
    "path": "leanback-1.0.0/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\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-->\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"androidx.leanback\" >\n\n    <!--<uses-sdk android:minSdkVersion=\"17\" />-->\n\n</manifest>"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/animation/LogAccelerateInterpolator.java",
    "content": "/*\n * Copyright (C) 2014 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.animation;\n\nimport static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;\n\nimport android.animation.TimeInterpolator;\n\nimport androidx.annotation.RestrictTo;\n\n/**\n * @hide\n */\n@RestrictTo(LIBRARY_GROUP)\npublic class LogAccelerateInterpolator implements TimeInterpolator {\n\n    int mBase;\n    int mDrift;\n    final float mLogScale;\n\n    public LogAccelerateInterpolator(int base, int drift) {\n        mBase = base;\n        mDrift = drift;\n        mLogScale = 1f / computeLog(1, mBase, mDrift);\n    }\n\n    static float computeLog(float t, int base, int drift) {\n        return (float) -Math.pow(base, -t) + 1 + (drift * t);\n    }\n\n    @Override\n    public float getInterpolation(float t) {\n        return 1 - computeLog(1 - t, mBase, mDrift) * mLogScale;\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/animation/LogDecelerateInterpolator.java",
    "content": "/*\n * Copyright (C) 2014 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.animation;\n\nimport static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;\n\nimport android.animation.TimeInterpolator;\n\nimport androidx.annotation.RestrictTo;\n\n/**\n * @hide\n */\n@RestrictTo(LIBRARY_GROUP)\npublic class LogDecelerateInterpolator implements TimeInterpolator {\n\n    int mBase;\n    int mDrift;\n    final float mLogScale;\n\n    public LogDecelerateInterpolator(int base, int drift) {\n        mBase = base;\n        mDrift = drift;\n\n        mLogScale = 1f / computeLog(1, mBase, mDrift);\n    }\n\n    static float computeLog(float t, int base, int drift) {\n        return (float) -Math.pow(base, -t) + 1 + (drift * t);\n    }\n\n    @Override\n    public float getInterpolation(float t) {\n        return computeLog(t, mBase, mDrift) * mLogScale;\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/BackgroundFragment.java",
    "content": "/*\n * Copyright (C) 2014 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.app;\n\nimport static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;\n\nimport android.app.Fragment;\n\nimport androidx.annotation.RestrictTo;\n\n/**\n * Fragment used by the background manager.\n * @hide\n */\n@RestrictTo(LIBRARY_GROUP)\npublic final class BackgroundFragment extends Fragment {\n    private BackgroundManager mBackgroundManager;\n\n    void setBackgroundManager(BackgroundManager backgroundManager) {\n        mBackgroundManager = backgroundManager;\n    }\n\n    BackgroundManager getBackgroundManager() {\n        return mBackgroundManager;\n    }\n\n    @Override\n    public void onStart() {\n        super.onStart();\n        // mBackgroundManager might be null:\n        // if BackgroundFragment is just restored by FragmentManager,\n        // and user does not call BackgroundManager.getInstance() yet.\n        if (mBackgroundManager != null) {\n            mBackgroundManager.onActivityStart();\n        }\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        // mBackgroundManager might be null:\n        // if BackgroundFragment is just restored by FragmentManager,\n        // and user does not call BackgroundManager.getInstance() yet.\n        if (mBackgroundManager != null) {\n            mBackgroundManager.onResume();\n        }\n    }\n\n    @Override\n    public void onStop() {\n        if (mBackgroundManager != null) {\n            mBackgroundManager.onStop();\n        }\n        super.onStop();\n    }\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n        // mBackgroundManager might be null:\n        // if BackgroundFragment is just restored by FragmentManager,\n        // and user does not call BackgroundManager.getInstance() yet.\n        if (mBackgroundManager != null) {\n            mBackgroundManager.detach();\n        }\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/BaseFragment.java",
    "content": "// CHECKSTYLE:OFF Generated code\n/* This file is auto-generated from BaseSupportFragment.java.  DO NOT MODIFY. */\n\n/*\n * Copyright (C) 2014 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.app;\n\nimport android.annotation.SuppressLint;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.view.ViewTreeObserver;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.leanback.transition.TransitionHelper;\nimport androidx.leanback.transition.TransitionListener;\nimport androidx.leanback.util.StateMachine;\nimport androidx.leanback.util.StateMachine.Condition;\nimport androidx.leanback.util.StateMachine.Event;\nimport androidx.leanback.util.StateMachine.State;\n\n/**\n * Base class for leanback Fragments. This class is not intended to be subclassed by apps.\n * @deprecated use {@link BaseSupportFragment}\n */\n@Deprecated\n@SuppressWarnings(\"FragmentNotInstantiable\")\npublic class BaseFragment extends BrandedFragment {\n\n    /**\n     * The start state for all\n     */\n    final State STATE_START = new State(\"START\", true, false);\n\n    /**\n     * Initial State for ENTRNACE transition.\n     */\n    final State STATE_ENTRANCE_INIT = new State(\"ENTRANCE_INIT\");\n\n    /**\n     * prepareEntranceTransition is just called, but view not ready yet. We can enable the\n     * busy spinner.\n     */\n    final State STATE_ENTRANCE_ON_PREPARED = new State(\"ENTRANCE_ON_PREPARED\", true, false) {\n        @Override\n        public void run() {\n            mProgressBarManager.show();\n        }\n    };\n\n    /**\n     * prepareEntranceTransition is called and main content view to slide in was created, so we can\n     * call {@link #onEntranceTransitionPrepare}. Note that we dont set initial content to invisible\n     * in this State, the process is very different in subclass, e.g. BrowseFragment hide header\n     * views and hide main fragment view in two steps.\n     */\n    final State STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW = new State(\n            \"ENTRANCE_ON_PREPARED_ON_CREATEVIEW\") {\n        @Override\n        public void run() {\n            onEntranceTransitionPrepare();\n        }\n    };\n\n    /**\n     * execute the entrance transition.\n     */\n    final State STATE_ENTRANCE_PERFORM = new State(\"STATE_ENTRANCE_PERFORM\") {\n        @Override\n        public void run() {\n            mProgressBarManager.hide();\n            onExecuteEntranceTransition();\n        }\n    };\n\n    /**\n     * execute onEntranceTransitionEnd.\n     */\n    final State STATE_ENTRANCE_ON_ENDED = new State(\"ENTRANCE_ON_ENDED\") {\n        @Override\n        public void run() {\n            onEntranceTransitionEnd();\n        }\n    };\n\n    /**\n     * either entrance transition completed or skipped\n     */\n    final State STATE_ENTRANCE_COMPLETE = new State(\"ENTRANCE_COMPLETE\", true, false);\n\n    /**\n     * Event fragment.onCreate()\n     */\n    final Event EVT_ON_CREATE = new Event(\"onCreate\");\n\n    /**\n     * Event fragment.onViewCreated()\n     */\n    final Event EVT_ON_CREATEVIEW = new Event(\"onCreateView\");\n\n    /**\n     * Event for {@link #prepareEntranceTransition()} is called.\n     */\n    final Event EVT_PREPARE_ENTRANCE = new Event(\"prepareEntranceTransition\");\n\n    /**\n     * Event for {@link #startEntranceTransition()} is called.\n     */\n    final Event EVT_START_ENTRANCE = new Event(\"startEntranceTransition\");\n\n    /**\n     * Event for entrance transition is ended through Transition listener.\n     */\n    final Event EVT_ENTRANCE_END = new Event(\"onEntranceTransitionEnd\");\n\n    /**\n     * Event for skipping entrance transition if not supported.\n     */\n    final Condition COND_TRANSITION_NOT_SUPPORTED = new Condition(\"EntranceTransitionNotSupport\") {\n        @Override\n        public boolean canProceed() {\n            return !TransitionHelper.systemSupportsEntranceTransitions();\n        }\n    };\n\n    final StateMachine mStateMachine = new StateMachine();\n\n    Object mEntranceTransition;\n    final ProgressBarManager mProgressBarManager = new ProgressBarManager();\n\n    @SuppressLint(\"ValidFragment\")\n    BaseFragment() {\n    }\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        createStateMachineStates();\n        createStateMachineTransitions();\n        mStateMachine.start();\n        super.onCreate(savedInstanceState);\n        mStateMachine.fireEvent(EVT_ON_CREATE);\n    }\n\n    void createStateMachineStates() {\n        mStateMachine.addState(STATE_START);\n        mStateMachine.addState(STATE_ENTRANCE_INIT);\n        mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED);\n        mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW);\n        mStateMachine.addState(STATE_ENTRANCE_PERFORM);\n        mStateMachine.addState(STATE_ENTRANCE_ON_ENDED);\n        mStateMachine.addState(STATE_ENTRANCE_COMPLETE);\n    }\n\n    void createStateMachineTransitions() {\n        mStateMachine.addTransition(STATE_START, STATE_ENTRANCE_INIT, EVT_ON_CREATE);\n        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,\n                COND_TRANSITION_NOT_SUPPORTED);\n        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,\n                EVT_ON_CREATEVIEW);\n        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_ON_PREPARED,\n                EVT_PREPARE_ENTRANCE);\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,\n                STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,\n                EVT_ON_CREATEVIEW);\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,\n                STATE_ENTRANCE_PERFORM,\n                EVT_START_ENTRANCE);\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,\n                STATE_ENTRANCE_PERFORM);\n        mStateMachine.addTransition(STATE_ENTRANCE_PERFORM,\n                STATE_ENTRANCE_ON_ENDED,\n                EVT_ENTRANCE_END);\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_ENDED, STATE_ENTRANCE_COMPLETE);\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n        mStateMachine.fireEvent(EVT_ON_CREATEVIEW);\n    }\n\n    /**\n     * Enables entrance transition.<p>\n     * Entrance transition is the standard slide-in transition that shows rows of data in\n     * browse screen and details screen.\n     * <p>\n     * The method is ignored before LOLLIPOP (API21).\n     * <p>\n     * This method must be called in or\n     * before onCreate().  Typically entrance transition should be enabled when savedInstance is\n     * null so that fragment restored from instanceState does not run an extra entrance transition.\n     * When the entrance transition is enabled, the fragment will make headers and content\n     * hidden initially.\n     * When data of rows are ready, app must call {@link #startEntranceTransition()} to kick off\n     * the transition, otherwise the rows will be invisible forever.\n     * <p>\n     * It is similar to android:windowsEnterTransition and can be considered a late-executed\n     * android:windowsEnterTransition controlled by app.  There are two reasons that app needs it:\n     * <li> Workaround the problem that activity transition is not available between launcher and\n     * app.  Browse activity must programmatically start the slide-in transition.</li>\n     * <li> Separates DetailsOverviewRow transition from other rows transition.  So that\n     * the DetailsOverviewRow transition can be executed earlier without waiting for all rows\n     * to be loaded.</li>\n     * <p>\n     * Transition object is returned by createEntranceTransition().  Typically the app does not need\n     * override the default transition that browse and details provides.\n     */\n    public void prepareEntranceTransition() {\n        mStateMachine.fireEvent(EVT_PREPARE_ENTRANCE);\n    }\n\n    /**\n     * Create entrance transition.  Subclass can override to load transition from\n     * resource or construct manually.  Typically app does not need to\n     * override the default transition that browse and details provides.\n     */\n    protected Object createEntranceTransition() {\n        return null;\n    }\n\n    /**\n     * Run entrance transition.  Subclass may use TransitionManager to perform\n     * go(Scene) or beginDelayedTransition().  App should not override the default\n     * implementation of browse and details fragment.\n     */\n    protected void runEntranceTransition(Object entranceTransition) {\n    }\n\n    /**\n     * Callback when entrance transition is prepared.  This is when fragment should\n     * stop user input and animations.\n     */\n    protected void onEntranceTransitionPrepare() {\n    }\n\n    /**\n     * Callback when entrance transition is started.  This is when fragment should\n     * stop processing layout.\n     */\n    protected void onEntranceTransitionStart() {\n    }\n\n    /**\n     * Callback when entrance transition is ended.\n     */\n    protected void onEntranceTransitionEnd() {\n    }\n\n    /**\n     * When fragment finishes loading data, it should call startEntranceTransition()\n     * to execute the entrance transition.\n     * startEntranceTransition() will start transition only if both two conditions\n     * are satisfied:\n     * <li> prepareEntranceTransition() was called.</li>\n     * <li> has not executed entrance transition yet.</li>\n     * <p>\n     * If startEntranceTransition() is called before onViewCreated(), it will be pending\n     * and executed when view is created.\n     */\n    public void startEntranceTransition() {\n        mStateMachine.fireEvent(EVT_START_ENTRANCE);\n    }\n\n    void onExecuteEntranceTransition() {\n        // wait till views get their initial position before start transition\n        final View view = getView();\n        if (view == null) {\n            // fragment view destroyed, transition not needed\n            return;\n        }\n        view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {\n            @Override\n            public boolean onPreDraw() {\n                view.getViewTreeObserver().removeOnPreDrawListener(this);\n                if (FragmentUtil.getContext(BaseFragment.this) == null || getView() == null) {\n                    // bail out if fragment is destroyed immediately after startEntranceTransition\n                    return true;\n                }\n                internalCreateEntranceTransition();\n                onEntranceTransitionStart();\n                if (mEntranceTransition != null) {\n                    runEntranceTransition(mEntranceTransition);\n                } else {\n                    mStateMachine.fireEvent(EVT_ENTRANCE_END);\n                }\n                return false;\n            }\n        });\n        view.invalidate();\n    }\n\n    void internalCreateEntranceTransition() {\n        mEntranceTransition = createEntranceTransition();\n        if (mEntranceTransition == null) {\n            return;\n        }\n        TransitionHelper.addTransitionListener(mEntranceTransition, new TransitionListener() {\n            @Override\n            public void onTransitionEnd(Object transition) {\n                mEntranceTransition = null;\n                mStateMachine.fireEvent(EVT_ENTRANCE_END);\n            }\n        });\n    }\n\n    /**\n     * Returns the {@link ProgressBarManager}.\n     * @return The {@link ProgressBarManager}.\n     */\n    public final ProgressBarManager getProgressBarManager() {\n        return mProgressBarManager;\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/BaseRowFragment.java",
    "content": "// CHECKSTYLE:OFF Generated code\n/* This file is auto-generated from BaseRowSupportFragment.java.  DO NOT MODIFY. */\n\n/*\n * Copyright (C) 2014 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.app;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport android.app.Fragment;\nimport androidx.leanback.widget.ItemBridgeAdapter;\nimport androidx.leanback.widget.ListRow;\nimport androidx.leanback.widget.ObjectAdapter;\nimport androidx.leanback.widget.OnChildViewHolderSelectedListener;\nimport androidx.leanback.widget.PresenterSelector;\nimport androidx.leanback.widget.Row;\nimport androidx.leanback.widget.VerticalGridView;\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * An internal base class for a fragment containing a list of rows.\n * @deprecated use {@link BaseRowSupportFragment}\n */\n@Deprecated\nabstract class BaseRowFragment extends Fragment {\n    private static final String CURRENT_SELECTED_POSITION = \"currentSelectedPosition\";\n    private ObjectAdapter mAdapter;\n    VerticalGridView mVerticalGridView;\n    private PresenterSelector mPresenterSelector;\n    final ItemBridgeAdapter mBridgeAdapter = new ItemBridgeAdapter();\n    int mSelectedPosition = -1;\n    private boolean mPendingTransitionPrepare;\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    LateSelectionObserver mLateSelectionObserver = new LateSelectionObserver();\n\n    abstract int getLayoutResourceId();\n\n    private final OnChildViewHolderSelectedListener mRowSelectedListener =\n            new OnChildViewHolderSelectedListener() {\n                @Override\n                public void onChildViewHolderSelected(RecyclerView parent,\n                        RecyclerView.ViewHolder view, int position, int subposition) {\n                    if (!mLateSelectionObserver.mIsLateSelection) {\n                        mSelectedPosition = position;\n                        onRowSelected(parent, view, position, subposition);\n                    }\n                }\n            };\n\n    void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder view,\n            int position, int subposition) {\n    }\n\n    @Override\n    public View onCreateView(LayoutInflater inflater, ViewGroup container,\n            Bundle savedInstanceState) {\n        View view = inflater.inflate(getLayoutResourceId(), container, false);\n        mVerticalGridView = findGridViewFromRoot(view);\n        if (mPendingTransitionPrepare) {\n            mPendingTransitionPrepare = false;\n            onTransitionPrepare();\n        }\n        return view;\n    }\n\n    VerticalGridView findGridViewFromRoot(View view) {\n        return (VerticalGridView) view;\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        if (savedInstanceState != null) {\n            mSelectedPosition = savedInstanceState.getInt(CURRENT_SELECTED_POSITION, -1);\n        }\n        setAdapterAndSelection();\n        mVerticalGridView.setOnChildViewHolderSelectedListener(mRowSelectedListener);\n    }\n\n    /**\n     * This class waits for the adapter to be updated before setting the selected\n     * row.\n     */\n    private class LateSelectionObserver extends RecyclerView.AdapterDataObserver {\n        boolean mIsLateSelection = false;\n\n        LateSelectionObserver() {\n        }\n\n        @Override\n        public void onChanged() {\n            performLateSelection();\n        }\n\n        @Override\n        public void onItemRangeInserted(int positionStart, int itemCount) {\n            performLateSelection();\n        }\n\n        void startLateSelection() {\n            mIsLateSelection = true;\n            mBridgeAdapter.registerAdapterDataObserver(this);\n        }\n\n        void performLateSelection() {\n            clear();\n            if (mVerticalGridView != null) {\n                mVerticalGridView.setSelectedPosition(mSelectedPosition);\n            }\n        }\n\n        void clear() {\n            if (mIsLateSelection) {\n                mIsLateSelection = false;\n                mBridgeAdapter.unregisterAdapterDataObserver(this);\n            }\n        }\n    }\n\n    void setAdapterAndSelection() {\n        if (mAdapter == null) {\n            // delay until ItemBridgeAdapter has wrappedAdapter. Once we assign ItemBridgeAdapter\n            // to RecyclerView, it will not be allowed to change \"hasStableId\" to true.\n            return;\n        }\n        if (mVerticalGridView.getAdapter() != mBridgeAdapter) {\n            // avoid extra layout if ItemBridgeAdapter was already set.\n            mVerticalGridView.setAdapter(mBridgeAdapter);\n        }\n        // We don't set the selected position unless we've data in the adapter.\n        boolean lateSelection = mBridgeAdapter.getItemCount() == 0 && mSelectedPosition >= 0;\n        if (lateSelection) {\n            mLateSelectionObserver.startLateSelection();\n        } else if (mSelectedPosition >= 0) {\n            mVerticalGridView.setSelectedPosition(mSelectedPosition);\n        }\n    }\n\n    @Override\n    public void onDestroyView() {\n        super.onDestroyView();\n        mLateSelectionObserver.clear();\n        mVerticalGridView = null;\n    }\n\n    @Override\n    public void onSaveInstanceState(Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);\n    }\n\n    /**\n     * Set the presenter selector used to create and bind views.\n     */\n    public final void setPresenterSelector(PresenterSelector presenterSelector) {\n        if (mPresenterSelector != presenterSelector) {\n            mPresenterSelector = presenterSelector;\n            updateAdapter();\n        }\n    }\n\n    /**\n     * Get the presenter selector used to create and bind views.\n     */\n    public final PresenterSelector getPresenterSelector() {\n        return mPresenterSelector;\n    }\n\n    /**\n     * Sets the adapter that represents a list of rows.\n     * @param rowsAdapter Adapter that represents list of rows.\n     */\n    public final void setAdapter(ObjectAdapter rowsAdapter) {\n        if (mAdapter != rowsAdapter) {\n            mAdapter = rowsAdapter;\n            updateAdapter();\n        }\n    }\n\n    /**\n     * Returns the Adapter that represents list of rows.\n     * @return Adapter that represents list of rows.\n     */\n    public final ObjectAdapter getAdapter() {\n        return mAdapter;\n    }\n\n    /**\n     * Returns the RecyclerView.Adapter that wraps {@link #getAdapter()}.\n     * @return The RecyclerView.Adapter that wraps {@link #getAdapter()}.\n     */\n    public final ItemBridgeAdapter getBridgeAdapter() {\n        return mBridgeAdapter;\n    }\n\n    /**\n     * Sets the selected row position with smooth animation.\n     */\n    public void setSelectedPosition(int position) {\n        setSelectedPosition(position, true);\n    }\n\n    /**\n     * Gets position of currently selected row.\n     * @return Position of currently selected row.\n     */\n    public int getSelectedPosition() {\n        return mSelectedPosition;\n    }\n\n    /**\n     * Sets the selected row position.\n     */\n    public void setSelectedPosition(int position, boolean smooth) {\n        if (mSelectedPosition == position) {\n            return;\n        }\n        mSelectedPosition = position;\n        if (mVerticalGridView != null) {\n            if (mLateSelectionObserver.mIsLateSelection) {\n                return;\n            }\n            if (smooth) {\n                mVerticalGridView.setSelectedPositionSmooth(position);\n            } else {\n                mVerticalGridView.setSelectedPosition(position);\n            }\n        }\n    }\n\n    public final VerticalGridView getVerticalGridView() {\n        return mVerticalGridView;\n    }\n\n    void updateAdapter() {\n        mBridgeAdapter.setAdapter(mAdapter);\n        mBridgeAdapter.setPresenter(mPresenterSelector);\n\n        if (mVerticalGridView != null) {\n            setAdapterAndSelection();\n        }\n    }\n\n    Object getItem(Row row, int position) {\n        if (row instanceof ListRow) {\n            return ((ListRow) row).getAdapter().get(position);\n        } else {\n            return null;\n        }\n    }\n\n    public boolean onTransitionPrepare() {\n        if (mVerticalGridView != null) {\n            mVerticalGridView.setAnimateChildLayout(false);\n            mVerticalGridView.setScrollEnabled(false);\n            return true;\n        }\n        mPendingTransitionPrepare = true;\n        return false;\n    }\n\n    public void onTransitionStart() {\n        if (mVerticalGridView != null) {\n            mVerticalGridView.setPruneChild(false);\n            mVerticalGridView.setLayoutFrozen(true);\n            mVerticalGridView.setFocusSearchDisabled(true);\n        }\n    }\n\n    public void onTransitionEnd() {\n        // be careful that fragment might be destroyed before header transition ends.\n        if (mVerticalGridView != null) {\n            mVerticalGridView.setLayoutFrozen(false);\n            mVerticalGridView.setAnimateChildLayout(true);\n            mVerticalGridView.setPruneChild(true);\n            mVerticalGridView.setFocusSearchDisabled(false);\n            mVerticalGridView.setScrollEnabled(true);\n        }\n    }\n\n    public void setAlignment(int windowAlignOffsetTop) {\n        if (mVerticalGridView != null) {\n            // align the top edge of item\n            mVerticalGridView.setItemAlignmentOffset(0);\n            mVerticalGridView.setItemAlignmentOffsetPercent(\n                    VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);\n\n            // align to a fixed position from top\n            mVerticalGridView.setWindowAlignmentOffset(windowAlignOffsetTop);\n            mVerticalGridView.setWindowAlignmentOffsetPercent(\n                    VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);\n            mVerticalGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);\n        }\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/BaseRowSupportFragment.java",
    "content": "/*\n * Copyright (C) 2014 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.app;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.Fragment;\nimport androidx.leanback.widget.ItemBridgeAdapter;\nimport androidx.leanback.widget.ListRow;\nimport androidx.leanback.widget.ObjectAdapter;\nimport androidx.leanback.widget.OnChildViewHolderSelectedListener;\nimport androidx.leanback.widget.PresenterSelector;\nimport androidx.leanback.widget.Row;\nimport androidx.leanback.widget.VerticalGridView;\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * An internal base class for a fragment containing a list of rows.\n */\nabstract class BaseRowSupportFragment extends Fragment {\n    private static final String CURRENT_SELECTED_POSITION = \"currentSelectedPosition\";\n    private ObjectAdapter mAdapter;\n    VerticalGridView mVerticalGridView;\n    private PresenterSelector mPresenterSelector;\n    final ItemBridgeAdapter mBridgeAdapter = new ItemBridgeAdapter();\n    int mSelectedPosition = -1;\n    private boolean mPendingTransitionPrepare;\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    LateSelectionObserver mLateSelectionObserver = new LateSelectionObserver();\n\n    abstract int getLayoutResourceId();\n\n    private final OnChildViewHolderSelectedListener mRowSelectedListener =\n            new OnChildViewHolderSelectedListener() {\n                @Override\n                public void onChildViewHolderSelected(RecyclerView parent,\n                        RecyclerView.ViewHolder view, int position, int subposition) {\n                    if (!mLateSelectionObserver.mIsLateSelection) {\n                        mSelectedPosition = position;\n                        onRowSelected(parent, view, position, subposition);\n                    }\n                }\n            };\n\n    void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder view,\n            int position, int subposition) {\n    }\n\n    @Override\n    public View onCreateView(LayoutInflater inflater, ViewGroup container,\n            Bundle savedInstanceState) {\n        View view = inflater.inflate(getLayoutResourceId(), container, false);\n        mVerticalGridView = findGridViewFromRoot(view);\n        if (mPendingTransitionPrepare) {\n            mPendingTransitionPrepare = false;\n            onTransitionPrepare();\n        }\n        return view;\n    }\n\n    VerticalGridView findGridViewFromRoot(View view) {\n        return (VerticalGridView) view;\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        if (savedInstanceState != null) {\n            mSelectedPosition = savedInstanceState.getInt(CURRENT_SELECTED_POSITION, -1);\n        }\n        setAdapterAndSelection();\n        mVerticalGridView.setOnChildViewHolderSelectedListener(mRowSelectedListener);\n    }\n\n    /**\n     * This class waits for the adapter to be updated before setting the selected\n     * row.\n     */\n    private class LateSelectionObserver extends RecyclerView.AdapterDataObserver {\n        boolean mIsLateSelection = false;\n\n        LateSelectionObserver() {\n        }\n\n        @Override\n        public void onChanged() {\n            performLateSelection();\n        }\n\n        @Override\n        public void onItemRangeInserted(int positionStart, int itemCount) {\n            performLateSelection();\n        }\n\n        void startLateSelection() {\n            mIsLateSelection = true;\n            mBridgeAdapter.registerAdapterDataObserver(this);\n        }\n\n        void performLateSelection() {\n            clear();\n            if (mVerticalGridView != null) {\n                mVerticalGridView.setSelectedPosition(mSelectedPosition);\n            }\n        }\n\n        void clear() {\n            if (mIsLateSelection) {\n                mIsLateSelection = false;\n                mBridgeAdapter.unregisterAdapterDataObserver(this);\n            }\n        }\n    }\n\n    void setAdapterAndSelection() {\n        if (mAdapter == null) {\n            // delay until ItemBridgeAdapter has wrappedAdapter. Once we assign ItemBridgeAdapter\n            // to RecyclerView, it will not be allowed to change \"hasStableId\" to true.\n            return;\n        }\n        if (mVerticalGridView.getAdapter() != mBridgeAdapter) {\n            // avoid extra layout if ItemBridgeAdapter was already set.\n            mVerticalGridView.setAdapter(mBridgeAdapter);\n        }\n        // We don't set the selected position unless we've data in the adapter.\n        boolean lateSelection = mBridgeAdapter.getItemCount() == 0 && mSelectedPosition >= 0;\n        if (lateSelection) {\n            mLateSelectionObserver.startLateSelection();\n        } else if (mSelectedPosition >= 0) {\n            mVerticalGridView.setSelectedPosition(mSelectedPosition);\n        }\n    }\n\n    @Override\n    public void onDestroyView() {\n        super.onDestroyView();\n        mLateSelectionObserver.clear();\n        mVerticalGridView = null;\n    }\n\n    @Override\n    public void onSaveInstanceState(Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);\n    }\n\n    /**\n     * Set the presenter selector used to create and bind views.\n     */\n    public final void setPresenterSelector(PresenterSelector presenterSelector) {\n        if (mPresenterSelector != presenterSelector) {\n            mPresenterSelector = presenterSelector;\n            updateAdapter();\n        }\n    }\n\n    /**\n     * Get the presenter selector used to create and bind views.\n     */\n    public final PresenterSelector getPresenterSelector() {\n        return mPresenterSelector;\n    }\n\n    /**\n     * Sets the adapter that represents a list of rows.\n     * @param rowsAdapter Adapter that represents list of rows.\n     */\n    public final void setAdapter(ObjectAdapter rowsAdapter) {\n        if (mAdapter != rowsAdapter) {\n            mAdapter = rowsAdapter;\n            updateAdapter();\n        }\n    }\n\n    /**\n     * Returns the Adapter that represents list of rows.\n     * @return Adapter that represents list of rows.\n     */\n    public final ObjectAdapter getAdapter() {\n        return mAdapter;\n    }\n\n    /**\n     * Returns the RecyclerView.Adapter that wraps {@link #getAdapter()}.\n     * @return The RecyclerView.Adapter that wraps {@link #getAdapter()}.\n     */\n    public final ItemBridgeAdapter getBridgeAdapter() {\n        return mBridgeAdapter;\n    }\n\n    /**\n     * Sets the selected row position with smooth animation.\n     */\n    public void setSelectedPosition(int position) {\n        setSelectedPosition(position, true);\n    }\n\n    /**\n     * Gets position of currently selected row.\n     * @return Position of currently selected row.\n     */\n    public int getSelectedPosition() {\n        return mSelectedPosition;\n    }\n\n    /**\n     * Sets the selected row position.\n     */\n    public void setSelectedPosition(int position, boolean smooth) {\n        if (mSelectedPosition == position) {\n            return;\n        }\n        mSelectedPosition = position;\n        if (mVerticalGridView != null) {\n            if (mLateSelectionObserver.mIsLateSelection) {\n                return;\n            }\n            if (smooth) {\n                mVerticalGridView.setSelectedPositionSmooth(position);\n            } else {\n                mVerticalGridView.setSelectedPosition(position);\n            }\n        }\n    }\n\n    public final VerticalGridView getVerticalGridView() {\n        return mVerticalGridView;\n    }\n\n    void updateAdapter() {\n        mBridgeAdapter.setAdapter(mAdapter);\n        mBridgeAdapter.setPresenter(mPresenterSelector);\n\n        if (mVerticalGridView != null) {\n            setAdapterAndSelection();\n        }\n    }\n\n    Object getItem(Row row, int position) {\n        if (row instanceof ListRow) {\n            return ((ListRow) row).getAdapter().get(position);\n        } else {\n            return null;\n        }\n    }\n\n    public boolean onTransitionPrepare() {\n        if (mVerticalGridView != null) {\n            mVerticalGridView.setAnimateChildLayout(false);\n            mVerticalGridView.setScrollEnabled(false);\n            return true;\n        }\n        mPendingTransitionPrepare = true;\n        return false;\n    }\n\n    public void onTransitionStart() {\n        if (mVerticalGridView != null) {\n            mVerticalGridView.setPruneChild(false);\n            mVerticalGridView.setLayoutFrozen(true);\n            mVerticalGridView.setFocusSearchDisabled(true);\n        }\n    }\n\n    public void onTransitionEnd() {\n        // be careful that fragment might be destroyed before header transition ends.\n        if (mVerticalGridView != null) {\n            mVerticalGridView.setLayoutFrozen(false);\n            mVerticalGridView.setAnimateChildLayout(true);\n            mVerticalGridView.setPruneChild(true);\n            mVerticalGridView.setFocusSearchDisabled(false);\n            mVerticalGridView.setScrollEnabled(true);\n        }\n    }\n\n    public void setAlignment(int windowAlignOffsetTop) {\n        if (mVerticalGridView != null) {\n            // align the top edge of item\n            mVerticalGridView.setItemAlignmentOffset(0);\n            mVerticalGridView.setItemAlignmentOffsetPercent(\n                    VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);\n\n            // align to a fixed position from top\n            mVerticalGridView.setWindowAlignmentOffset(windowAlignOffsetTop);\n            mVerticalGridView.setWindowAlignmentOffsetPercent(\n                    VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);\n            mVerticalGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);\n        }\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/BaseSupportFragment.java",
    "content": "/*\n * Copyright (C) 2014 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.app;\n\nimport android.annotation.SuppressLint;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.view.ViewTreeObserver;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.leanback.transition.TransitionHelper;\nimport androidx.leanback.transition.TransitionListener;\nimport androidx.leanback.util.StateMachine;\nimport androidx.leanback.util.StateMachine.Condition;\nimport androidx.leanback.util.StateMachine.Event;\nimport androidx.leanback.util.StateMachine.State;\n\n/**\n * Base class for leanback Fragments. This class is not intended to be subclassed by apps.\n */\n@SuppressWarnings(\"FragmentNotInstantiable\")\npublic class BaseSupportFragment extends BrandedSupportFragment {\n\n    /**\n     * The start state for all\n     */\n    final State STATE_START = new State(\"START\", true, false);\n\n    /**\n     * Initial State for ENTRNACE transition.\n     */\n    final State STATE_ENTRANCE_INIT = new State(\"ENTRANCE_INIT\");\n\n    /**\n     * prepareEntranceTransition is just called, but view not ready yet. We can enable the\n     * busy spinner.\n     */\n    final State STATE_ENTRANCE_ON_PREPARED = new State(\"ENTRANCE_ON_PREPARED\", true, false) {\n        @Override\n        public void run() {\n            mProgressBarManager.show();\n        }\n    };\n\n    /**\n     * prepareEntranceTransition is called and main content view to slide in was created, so we can\n     * call {@link #onEntranceTransitionPrepare}. Note that we dont set initial content to invisible\n     * in this State, the process is very different in subclass, e.g. BrowseSupportFragment hide header\n     * views and hide main fragment view in two steps.\n     */\n    final State STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW = new State(\n            \"ENTRANCE_ON_PREPARED_ON_CREATEVIEW\") {\n        @Override\n        public void run() {\n            onEntranceTransitionPrepare();\n        }\n    };\n\n    /**\n     * execute the entrance transition.\n     */\n    final State STATE_ENTRANCE_PERFORM = new State(\"STATE_ENTRANCE_PERFORM\") {\n        @Override\n        public void run() {\n            mProgressBarManager.hide();\n            onExecuteEntranceTransition();\n        }\n    };\n\n    /**\n     * execute onEntranceTransitionEnd.\n     */\n    final State STATE_ENTRANCE_ON_ENDED = new State(\"ENTRANCE_ON_ENDED\") {\n        @Override\n        public void run() {\n            onEntranceTransitionEnd();\n        }\n    };\n\n    /**\n     * either entrance transition completed or skipped\n     */\n    final State STATE_ENTRANCE_COMPLETE = new State(\"ENTRANCE_COMPLETE\", true, false);\n\n    /**\n     * Event fragment.onCreate()\n     */\n    final Event EVT_ON_CREATE = new Event(\"onCreate\");\n\n    /**\n     * Event fragment.onViewCreated()\n     */\n    final Event EVT_ON_CREATEVIEW = new Event(\"onCreateView\");\n\n    /**\n     * Event for {@link #prepareEntranceTransition()} is called.\n     */\n    final Event EVT_PREPARE_ENTRANCE = new Event(\"prepareEntranceTransition\");\n\n    /**\n     * Event for {@link #startEntranceTransition()} is called.\n     */\n    final Event EVT_START_ENTRANCE = new Event(\"startEntranceTransition\");\n\n    /**\n     * Event for entrance transition is ended through Transition listener.\n     */\n    final Event EVT_ENTRANCE_END = new Event(\"onEntranceTransitionEnd\");\n\n    /**\n     * Event for skipping entrance transition if not supported.\n     */\n    final Condition COND_TRANSITION_NOT_SUPPORTED = new Condition(\"EntranceTransitionNotSupport\") {\n        @Override\n        public boolean canProceed() {\n            return !TransitionHelper.systemSupportsEntranceTransitions();\n        }\n    };\n\n    final StateMachine mStateMachine = new StateMachine();\n\n    Object mEntranceTransition;\n    final ProgressBarManager mProgressBarManager = new ProgressBarManager();\n\n    @SuppressLint(\"ValidFragment\")\n    BaseSupportFragment() {\n    }\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        createStateMachineStates();\n        createStateMachineTransitions();\n        mStateMachine.start();\n        super.onCreate(savedInstanceState);\n        mStateMachine.fireEvent(EVT_ON_CREATE);\n    }\n\n    void createStateMachineStates() {\n        mStateMachine.addState(STATE_START);\n        mStateMachine.addState(STATE_ENTRANCE_INIT);\n        mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED);\n        mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW);\n        mStateMachine.addState(STATE_ENTRANCE_PERFORM);\n        mStateMachine.addState(STATE_ENTRANCE_ON_ENDED);\n        mStateMachine.addState(STATE_ENTRANCE_COMPLETE);\n    }\n\n    void createStateMachineTransitions() {\n        mStateMachine.addTransition(STATE_START, STATE_ENTRANCE_INIT, EVT_ON_CREATE);\n        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,\n                COND_TRANSITION_NOT_SUPPORTED);\n        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,\n                EVT_ON_CREATEVIEW);\n        mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_ON_PREPARED,\n                EVT_PREPARE_ENTRANCE);\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,\n                STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,\n                EVT_ON_CREATEVIEW);\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,\n                STATE_ENTRANCE_PERFORM,\n                EVT_START_ENTRANCE);\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,\n                STATE_ENTRANCE_PERFORM);\n        mStateMachine.addTransition(STATE_ENTRANCE_PERFORM,\n                STATE_ENTRANCE_ON_ENDED,\n                EVT_ENTRANCE_END);\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_ENDED, STATE_ENTRANCE_COMPLETE);\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n        mStateMachine.fireEvent(EVT_ON_CREATEVIEW);\n    }\n\n    /**\n     * Enables entrance transition.<p>\n     * Entrance transition is the standard slide-in transition that shows rows of data in\n     * browse screen and details screen.\n     * <p>\n     * The method is ignored before LOLLIPOP (API21).\n     * <p>\n     * This method must be called in or\n     * before onCreate().  Typically entrance transition should be enabled when savedInstance is\n     * null so that fragment restored from instanceState does not run an extra entrance transition.\n     * When the entrance transition is enabled, the fragment will make headers and content\n     * hidden initially.\n     * When data of rows are ready, app must call {@link #startEntranceTransition()} to kick off\n     * the transition, otherwise the rows will be invisible forever.\n     * <p>\n     * It is similar to android:windowsEnterTransition and can be considered a late-executed\n     * android:windowsEnterTransition controlled by app.  There are two reasons that app needs it:\n     * <li> Workaround the problem that activity transition is not available between launcher and\n     * app.  Browse activity must programmatically start the slide-in transition.</li>\n     * <li> Separates DetailsOverviewRow transition from other rows transition.  So that\n     * the DetailsOverviewRow transition can be executed earlier without waiting for all rows\n     * to be loaded.</li>\n     * <p>\n     * Transition object is returned by createEntranceTransition().  Typically the app does not need\n     * override the default transition that browse and details provides.\n     */\n    public void prepareEntranceTransition() {\n        mStateMachine.fireEvent(EVT_PREPARE_ENTRANCE);\n    }\n\n    /**\n     * Create entrance transition.  Subclass can override to load transition from\n     * resource or construct manually.  Typically app does not need to\n     * override the default transition that browse and details provides.\n     */\n    protected Object createEntranceTransition() {\n        return null;\n    }\n\n    /**\n     * Run entrance transition.  Subclass may use TransitionManager to perform\n     * go(Scene) or beginDelayedTransition().  App should not override the default\n     * implementation of browse and details fragment.\n     */\n    protected void runEntranceTransition(Object entranceTransition) {\n    }\n\n    /**\n     * Callback when entrance transition is prepared.  This is when fragment should\n     * stop user input and animations.\n     */\n    protected void onEntranceTransitionPrepare() {\n    }\n\n    /**\n     * Callback when entrance transition is started.  This is when fragment should\n     * stop processing layout.\n     */\n    protected void onEntranceTransitionStart() {\n    }\n\n    /**\n     * Callback when entrance transition is ended.\n     */\n    protected void onEntranceTransitionEnd() {\n    }\n\n    /**\n     * When fragment finishes loading data, it should call startEntranceTransition()\n     * to execute the entrance transition.\n     * startEntranceTransition() will start transition only if both two conditions\n     * are satisfied:\n     * <li> prepareEntranceTransition() was called.</li>\n     * <li> has not executed entrance transition yet.</li>\n     * <p>\n     * If startEntranceTransition() is called before onViewCreated(), it will be pending\n     * and executed when view is created.\n     */\n    public void startEntranceTransition() {\n        mStateMachine.fireEvent(EVT_START_ENTRANCE);\n    }\n\n    void onExecuteEntranceTransition() {\n        // wait till views get their initial position before start transition\n        final View view = getView();\n        if (view == null) {\n            // fragment view destroyed, transition not needed\n            return;\n        }\n        view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {\n            @Override\n            public boolean onPreDraw() {\n                view.getViewTreeObserver().removeOnPreDrawListener(this);\n                if (getContext() == null || getView() == null) {\n                    // bail out if fragment is destroyed immediately after startEntranceTransition\n                    return true;\n                }\n                internalCreateEntranceTransition();\n                onEntranceTransitionStart();\n                if (mEntranceTransition != null) {\n                    runEntranceTransition(mEntranceTransition);\n                } else {\n                    mStateMachine.fireEvent(EVT_ENTRANCE_END);\n                }\n                return false;\n            }\n        });\n        view.invalidate();\n    }\n\n    void internalCreateEntranceTransition() {\n        mEntranceTransition = createEntranceTransition();\n        if (mEntranceTransition == null) {\n            return;\n        }\n        TransitionHelper.addTransitionListener(mEntranceTransition, new TransitionListener() {\n            @Override\n            public void onTransitionEnd(Object transition) {\n                mEntranceTransition = null;\n                mStateMachine.fireEvent(EVT_ENTRANCE_END);\n            }\n        });\n    }\n\n    /**\n     * Returns the {@link ProgressBarManager}.\n     * @return The {@link ProgressBarManager}.\n     */\n    public final ProgressBarManager getProgressBarManager() {\n        return mProgressBarManager;\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/BrandedFragment.java",
    "content": "// CHECKSTYLE:OFF Generated code\n/* This file is auto-generated from BrandedSupportFragment.java.  DO NOT MODIFY. */\n\n/*\n * Copyright (C) 2014 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.app;\n\nimport android.graphics.drawable.Drawable;\nimport android.os.Bundle;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport android.app.Fragment;\nimport androidx.leanback.R;\nimport androidx.leanback.widget.SearchOrbView;\nimport androidx.leanback.widget.TitleHelper;\nimport androidx.leanback.widget.TitleViewAdapter;\n\n/**\n * Fragment class for managing search and branding using a view that implements\n * {@link TitleViewAdapter.Provider}.\n * @deprecated use {@link BrandedSupportFragment}\n */\n@Deprecated\npublic class BrandedFragment extends Fragment {\n\n    // BUNDLE attribute for title is showing\n    private static final String TITLE_SHOW = \"titleShow\";\n\n    private boolean mShowingTitle = true;\n    private CharSequence mTitle;\n    private Drawable mBadgeDrawable;\n    private View mTitleView;\n    private TitleViewAdapter mTitleViewAdapter;\n    private SearchOrbView.Colors mSearchAffordanceColors;\n    private boolean mSearchAffordanceColorSet;\n    private View.OnClickListener mExternalOnSearchClickedListener;\n    private TitleHelper mTitleHelper;\n\n    /**\n     * Called by {@link #installTitleView(LayoutInflater, ViewGroup, Bundle)} to inflate\n     * title view.  Default implementation uses layout file lb_browse_title.\n     * Subclass may override and use its own layout, the layout must have a descendant with id\n     * browse_title_group that implements {@link TitleViewAdapter.Provider}. Subclass may return\n     * null if no title is needed.\n     *\n     * @param inflater           The LayoutInflater object that can be used to inflate\n     *                           any views in the fragment,\n     * @param parent             Parent of title view.\n     * @param savedInstanceState If non-null, this fragment is being re-constructed\n     *                           from a previous saved state as given here.\n     * @return Title view which must have a descendant with id browse_title_group that implements\n     *         {@link TitleViewAdapter.Provider}, or null for no title view.\n     */\n    public View onInflateTitleView(LayoutInflater inflater, ViewGroup parent,\n                                Bundle savedInstanceState) {\n        TypedValue typedValue = new TypedValue();\n        boolean found = parent.getContext().getTheme().resolveAttribute(\n                R.attr.browseTitleViewLayout, typedValue, true);\n        return inflater.inflate(found ? typedValue.resourceId : R.layout.lb_browse_title,\n                parent, false);\n    }\n\n    /**\n     * Inflate title view and add to parent.  This method should be called in\n     * {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}.\n     * @param inflater The LayoutInflater object that can be used to inflate\n     * any views in the fragment,\n     * @param parent Parent of title view.\n     * @param savedInstanceState If non-null, this fragment is being re-constructed\n     * from a previous saved state as given here.\n     */\n    public void installTitleView(LayoutInflater inflater, ViewGroup parent,\n                            Bundle savedInstanceState) {\n        View titleLayoutRoot = onInflateTitleView(inflater, parent, savedInstanceState);\n        if (titleLayoutRoot != null) {\n            parent.addView(titleLayoutRoot);\n            setTitleView(titleLayoutRoot.findViewById(R.id.browse_title_group));\n        } else {\n            setTitleView(null);\n        }\n    }\n\n    /**\n     * Sets the view that implemented {@link TitleViewAdapter}.\n     * @param titleView The view that implemented {@link TitleViewAdapter.Provider}.\n     */\n    public void setTitleView(View titleView) {\n        mTitleView = titleView;\n        if (mTitleView == null) {\n            mTitleViewAdapter = null;\n            mTitleHelper = null;\n        } else {\n            mTitleViewAdapter = ((TitleViewAdapter.Provider) mTitleView).getTitleViewAdapter();\n            mTitleViewAdapter.setTitle(mTitle);\n            mTitleViewAdapter.setBadgeDrawable(mBadgeDrawable);\n            if (mSearchAffordanceColorSet) {\n                mTitleViewAdapter.setSearchAffordanceColors(mSearchAffordanceColors);\n            }\n            if (mExternalOnSearchClickedListener != null) {\n                setOnSearchClickedListener(mExternalOnSearchClickedListener);\n            }\n            if (getView() instanceof ViewGroup) {\n                mTitleHelper = new TitleHelper((ViewGroup) getView(), mTitleView);\n            }\n        }\n    }\n\n    /**\n     * Returns the view that implements {@link TitleViewAdapter.Provider}.\n     * @return The view that implements {@link TitleViewAdapter.Provider}.\n     */\n    public View getTitleView() {\n        return mTitleView;\n    }\n\n    /**\n     * Returns the {@link TitleViewAdapter} implemented by title view.\n     * @return The {@link TitleViewAdapter} implemented by title view.\n     */\n    public TitleViewAdapter getTitleViewAdapter() {\n        return mTitleViewAdapter;\n    }\n\n    /**\n     * Returns the {@link TitleHelper}.\n     */\n    TitleHelper getTitleHelper() {\n        return mTitleHelper;\n    }\n\n    @Override\n    public void onSaveInstanceState(Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putBoolean(TITLE_SHOW, mShowingTitle);\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n        if (savedInstanceState != null) {\n            mShowingTitle = savedInstanceState.getBoolean(TITLE_SHOW);\n        }\n        if (mTitleView != null && view instanceof ViewGroup) {\n            mTitleHelper = new TitleHelper((ViewGroup) view, mTitleView);\n            mTitleHelper.showTitle(mShowingTitle);\n        }\n    }\n\n    @Override\n    public void onDestroyView() {\n        super.onDestroyView();\n        mTitleHelper = null;\n    }\n\n    /**\n     * Shows or hides the title view.\n     * @param show True to show title view, false to hide title view.\n     */\n    public void showTitle(boolean show) {\n        // TODO: handle interruptions?\n        if (show == mShowingTitle) {\n            return;\n        }\n        mShowingTitle = show;\n        if (mTitleHelper != null) {\n            mTitleHelper.showTitle(show);\n        }\n    }\n\n    /**\n     * Changes title view's components visibility and shows title.\n     * @param flags Flags representing the visibility of components inside title view.\n     * @see TitleViewAdapter#SEARCH_VIEW_VISIBLE\n     * @see TitleViewAdapter#BRANDING_VIEW_VISIBLE\n     * @see TitleViewAdapter#FULL_VIEW_VISIBLE\n     * @see TitleViewAdapter#updateComponentsVisibility(int)\n     */\n    public void showTitle(int flags) {\n        if (mTitleViewAdapter != null) {\n            mTitleViewAdapter.updateComponentsVisibility(flags);\n        }\n        showTitle(true);\n    }\n\n    /**\n     * Sets the drawable displayed in the fragment title.\n     *\n     * @param drawable The Drawable to display in the fragment title.\n     */\n    public void setBadgeDrawable(Drawable drawable) {\n        if (mBadgeDrawable != drawable) {\n            mBadgeDrawable = drawable;\n            if (mTitleViewAdapter != null) {\n                mTitleViewAdapter.setBadgeDrawable(drawable);\n            }\n        }\n    }\n\n    /**\n     * Returns the badge drawable used in the fragment title.\n     * @return The badge drawable used in the fragment title.\n     */\n    public Drawable getBadgeDrawable() {\n        return mBadgeDrawable;\n    }\n\n    /**\n     * Sets title text for the fragment.\n     *\n     * @param title The title text of the fragment.\n     */\n    public void setTitle(CharSequence title) {\n        mTitle = title;\n        if (mTitleViewAdapter != null) {\n            mTitleViewAdapter.setTitle(title);\n        }\n    }\n\n    /**\n     * Returns the title text for the fragment.\n     * @return Title text for the fragment.\n     */\n    public CharSequence getTitle() {\n        return mTitle;\n    }\n\n    /**\n     * Sets a click listener for the search affordance.\n     *\n     * <p>The presence of a listener will change the visibility of the search\n     * affordance in the fragment title. When set to non-null, the title will\n     * contain an element that a user may click to begin a search.\n     *\n     * <p>The listener's {@link View.OnClickListener#onClick onClick} method\n     * will be invoked when the user clicks on the search element.\n     *\n     * @param listener The listener to call when the search element is clicked.\n     */\n    public void setOnSearchClickedListener(View.OnClickListener listener) {\n        mExternalOnSearchClickedListener = listener;\n        if (mTitleViewAdapter != null) {\n            mTitleViewAdapter.setOnSearchClickedListener(listener);\n        }\n    }\n\n    /**\n     * Sets the {@link androidx.leanback.widget.SearchOrbView.Colors} used to draw the\n     * search affordance.\n     *\n     * @param colors Colors used to draw search affordance.\n     */\n    public void setSearchAffordanceColors(SearchOrbView.Colors colors) {\n        mSearchAffordanceColors = colors;\n        mSearchAffordanceColorSet = true;\n        if (mTitleViewAdapter != null) {\n            mTitleViewAdapter.setSearchAffordanceColors(mSearchAffordanceColors);\n        }\n    }\n\n    /**\n     * Returns the {@link androidx.leanback.widget.SearchOrbView.Colors}\n     * used to draw the search affordance.\n     */\n    public SearchOrbView.Colors getSearchAffordanceColors() {\n        if (mSearchAffordanceColorSet) {\n            return mSearchAffordanceColors;\n        }\n        if (mTitleViewAdapter == null) {\n            throw new IllegalStateException(\"Fragment views not yet created\");\n        }\n        return mTitleViewAdapter.getSearchAffordanceColors();\n    }\n\n    /**\n     * Sets the color used to draw the search affordance.\n     * A default brighter color will be set by the framework.\n     *\n     * @param color The color to use for the search affordance.\n     */\n    public void setSearchAffordanceColor(int color) {\n        setSearchAffordanceColors(new SearchOrbView.Colors(color));\n    }\n\n    /**\n     * Returns the color used to draw the search affordance.\n     */\n    public int getSearchAffordanceColor() {\n        return getSearchAffordanceColors().color;\n    }\n\n    @Override\n    public void onStart() {\n        super.onStart();\n        if (mTitleViewAdapter != null) {\n            showTitle(mShowingTitle);\n            mTitleViewAdapter.setAnimationEnabled(true);\n        }\n    }\n\n    @Override\n    public void onPause() {\n        if (mTitleViewAdapter != null) {\n            mTitleViewAdapter.setAnimationEnabled(false);\n        }\n        super.onPause();\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        if (mTitleViewAdapter != null) {\n            mTitleViewAdapter.setAnimationEnabled(true);\n        }\n    }\n\n    /**\n     * Returns true/false to indicate the visibility of TitleView.\n     *\n     * @return boolean to indicate whether or not it's showing the title.\n     */\n    public final boolean isShowingTitle() {\n        return mShowingTitle;\n    }\n\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/BrandedSupportFragment.java",
    "content": "/*\n * Copyright (C) 2014 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.app;\n\nimport android.graphics.drawable.Drawable;\nimport android.os.Bundle;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.Fragment;\nimport androidx.leanback.R;\nimport androidx.leanback.widget.SearchOrbView;\nimport androidx.leanback.widget.TitleHelper;\nimport androidx.leanback.widget.TitleViewAdapter;\n\n/**\n * Fragment class for managing search and branding using a view that implements\n * {@link TitleViewAdapter.Provider}.\n */\npublic class BrandedSupportFragment extends Fragment {\n\n    // BUNDLE attribute for title is showing\n    private static final String TITLE_SHOW = \"titleShow\";\n\n    private boolean mShowingTitle = true;\n    private CharSequence mTitle;\n    private Drawable mBadgeDrawable;\n    private View mTitleView;\n    private TitleViewAdapter mTitleViewAdapter;\n    private SearchOrbView.Colors mSearchAffordanceColors;\n    private boolean mSearchAffordanceColorSet;\n    private View.OnClickListener mExternalOnSearchClickedListener;\n    private TitleHelper mTitleHelper;\n\n    /**\n     * Called by {@link #installTitleView(LayoutInflater, ViewGroup, Bundle)} to inflate\n     * title view.  Default implementation uses layout file lb_browse_title.\n     * Subclass may override and use its own layout, the layout must have a descendant with id\n     * browse_title_group that implements {@link TitleViewAdapter.Provider}. Subclass may return\n     * null if no title is needed.\n     *\n     * @param inflater           The LayoutInflater object that can be used to inflate\n     *                           any views in the fragment,\n     * @param parent             Parent of title view.\n     * @param savedInstanceState If non-null, this fragment is being re-constructed\n     *                           from a previous saved state as given here.\n     * @return Title view which must have a descendant with id browse_title_group that implements\n     *         {@link TitleViewAdapter.Provider}, or null for no title view.\n     */\n    public View onInflateTitleView(LayoutInflater inflater, ViewGroup parent,\n                                Bundle savedInstanceState) {\n        TypedValue typedValue = new TypedValue();\n        boolean found = parent.getContext().getTheme().resolveAttribute(\n                R.attr.browseTitleViewLayout, typedValue, true);\n        return inflater.inflate(found ? typedValue.resourceId : R.layout.lb_browse_title,\n                parent, false);\n    }\n\n    /**\n     * Inflate title view and add to parent.  This method should be called in\n     * {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}.\n     * @param inflater The LayoutInflater object that can be used to inflate\n     * any views in the fragment,\n     * @param parent Parent of title view.\n     * @param savedInstanceState If non-null, this fragment is being re-constructed\n     * from a previous saved state as given here.\n     */\n    public void installTitleView(LayoutInflater inflater, ViewGroup parent,\n                            Bundle savedInstanceState) {\n        View titleLayoutRoot = onInflateTitleView(inflater, parent, savedInstanceState);\n        if (titleLayoutRoot != null) {\n            parent.addView(titleLayoutRoot);\n            setTitleView(titleLayoutRoot.findViewById(R.id.browse_title_group));\n        } else {\n            setTitleView(null);\n        }\n    }\n\n    /**\n     * Sets the view that implemented {@link TitleViewAdapter}.\n     * @param titleView The view that implemented {@link TitleViewAdapter.Provider}.\n     */\n    public void setTitleView(View titleView) {\n        mTitleView = titleView;\n        if (mTitleView == null) {\n            mTitleViewAdapter = null;\n            mTitleHelper = null;\n        } else {\n            mTitleViewAdapter = ((TitleViewAdapter.Provider) mTitleView).getTitleViewAdapter();\n            mTitleViewAdapter.setTitle(mTitle);\n            mTitleViewAdapter.setBadgeDrawable(mBadgeDrawable);\n            if (mSearchAffordanceColorSet) {\n                mTitleViewAdapter.setSearchAffordanceColors(mSearchAffordanceColors);\n            }\n            if (mExternalOnSearchClickedListener != null) {\n                setOnSearchClickedListener(mExternalOnSearchClickedListener);\n            }\n            if (getView() instanceof ViewGroup) {\n                mTitleHelper = new TitleHelper((ViewGroup) getView(), mTitleView);\n            }\n        }\n    }\n\n    /**\n     * Returns the view that implements {@link TitleViewAdapter.Provider}.\n     * @return The view that implements {@link TitleViewAdapter.Provider}.\n     */\n    public View getTitleView() {\n        return mTitleView;\n    }\n\n    /**\n     * Returns the {@link TitleViewAdapter} implemented by title view.\n     * @return The {@link TitleViewAdapter} implemented by title view.\n     */\n    public TitleViewAdapter getTitleViewAdapter() {\n        return mTitleViewAdapter;\n    }\n\n    /**\n     * Returns the {@link TitleHelper}.\n     */\n    TitleHelper getTitleHelper() {\n        return mTitleHelper;\n    }\n\n    @Override\n    public void onSaveInstanceState(Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putBoolean(TITLE_SHOW, mShowingTitle);\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n        if (savedInstanceState != null) {\n            mShowingTitle = savedInstanceState.getBoolean(TITLE_SHOW);\n        }\n        if (mTitleView != null && view instanceof ViewGroup) {\n            mTitleHelper = new TitleHelper((ViewGroup) view, mTitleView);\n            mTitleHelper.showTitle(mShowingTitle);\n        }\n    }\n\n    @Override\n    public void onDestroyView() {\n        super.onDestroyView();\n        mTitleHelper = null;\n    }\n\n    /**\n     * Shows or hides the title view.\n     * @param show True to show title view, false to hide title view.\n     */\n    public void showTitle(boolean show) {\n        // TODO: handle interruptions?\n        if (show == mShowingTitle) {\n            return;\n        }\n        mShowingTitle = show;\n        if (mTitleHelper != null) {\n            mTitleHelper.showTitle(show);\n        }\n    }\n\n    /**\n     * Changes title view's components visibility and shows title.\n     * @param flags Flags representing the visibility of components inside title view.\n     * @see TitleViewAdapter#SEARCH_VIEW_VISIBLE\n     * @see TitleViewAdapter#BRANDING_VIEW_VISIBLE\n     * @see TitleViewAdapter#FULL_VIEW_VISIBLE\n     * @see TitleViewAdapter#updateComponentsVisibility(int)\n     */\n    public void showTitle(int flags) {\n        if (mTitleViewAdapter != null) {\n            mTitleViewAdapter.updateComponentsVisibility(flags);\n        }\n        showTitle(true);\n    }\n\n    /**\n     * Sets the drawable displayed in the fragment title.\n     *\n     * @param drawable The Drawable to display in the fragment title.\n     */\n    public void setBadgeDrawable(Drawable drawable) {\n        if (mBadgeDrawable != drawable) {\n            mBadgeDrawable = drawable;\n            if (mTitleViewAdapter != null) {\n                mTitleViewAdapter.setBadgeDrawable(drawable);\n            }\n        }\n    }\n\n    /**\n     * Returns the badge drawable used in the fragment title.\n     * @return The badge drawable used in the fragment title.\n     */\n    public Drawable getBadgeDrawable() {\n        return mBadgeDrawable;\n    }\n\n    /**\n     * Sets title text for the fragment.\n     *\n     * @param title The title text of the fragment.\n     */\n    public void setTitle(CharSequence title) {\n        mTitle = title;\n        if (mTitleViewAdapter != null) {\n            mTitleViewAdapter.setTitle(title);\n        }\n    }\n\n    /**\n     * Returns the title text for the fragment.\n     * @return Title text for the fragment.\n     */\n    public CharSequence getTitle() {\n        return mTitle;\n    }\n\n    /**\n     * Sets a click listener for the search affordance.\n     *\n     * <p>The presence of a listener will change the visibility of the search\n     * affordance in the fragment title. When set to non-null, the title will\n     * contain an element that a user may click to begin a search.\n     *\n     * <p>The listener's {@link View.OnClickListener#onClick onClick} method\n     * will be invoked when the user clicks on the search element.\n     *\n     * @param listener The listener to call when the search element is clicked.\n     */\n    public void setOnSearchClickedListener(View.OnClickListener listener) {\n        mExternalOnSearchClickedListener = listener;\n        if (mTitleViewAdapter != null) {\n            mTitleViewAdapter.setOnSearchClickedListener(listener);\n        }\n    }\n\n    /**\n     * Sets the {@link androidx.leanback.widget.SearchOrbView.Colors} used to draw the\n     * search affordance.\n     *\n     * @param colors Colors used to draw search affordance.\n     */\n    public void setSearchAffordanceColors(SearchOrbView.Colors colors) {\n        mSearchAffordanceColors = colors;\n        mSearchAffordanceColorSet = true;\n        if (mTitleViewAdapter != null) {\n            mTitleViewAdapter.setSearchAffordanceColors(mSearchAffordanceColors);\n        }\n    }\n\n    /**\n     * Returns the {@link androidx.leanback.widget.SearchOrbView.Colors}\n     * used to draw the search affordance.\n     */\n    public SearchOrbView.Colors getSearchAffordanceColors() {\n        if (mSearchAffordanceColorSet) {\n            return mSearchAffordanceColors;\n        }\n        if (mTitleViewAdapter == null) {\n            throw new IllegalStateException(\"Fragment views not yet created\");\n        }\n        return mTitleViewAdapter.getSearchAffordanceColors();\n    }\n\n    /**\n     * Sets the color used to draw the search affordance.\n     * A default brighter color will be set by the framework.\n     *\n     * @param color The color to use for the search affordance.\n     */\n    public void setSearchAffordanceColor(int color) {\n        setSearchAffordanceColors(new SearchOrbView.Colors(color));\n    }\n\n    /**\n     * Returns the color used to draw the search affordance.\n     */\n    public int getSearchAffordanceColor() {\n        return getSearchAffordanceColors().color;\n    }\n\n    @Override\n    public void onStart() {\n        super.onStart();\n        if (mTitleViewAdapter != null) {\n            showTitle(mShowingTitle);\n            mTitleViewAdapter.setAnimationEnabled(true);\n        }\n    }\n\n    @Override\n    public void onPause() {\n        if (mTitleViewAdapter != null) {\n            mTitleViewAdapter.setAnimationEnabled(false);\n        }\n        super.onPause();\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        if (mTitleViewAdapter != null) {\n            mTitleViewAdapter.setAnimationEnabled(true);\n        }\n    }\n\n    /**\n     * Returns true/false to indicate the visibility of TitleView.\n     *\n     * @return boolean to indicate whether or not it's showing the title.\n     */\n    public final boolean isShowingTitle() {\n        return mShowingTitle;\n    }\n\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/BrowseFragment.java",
    "content": "// CHECKSTYLE:OFF Generated code\n/* This file is auto-generated from BrowseSupportFragment.java.  DO NOT MODIFY. */\n\n/*\n * Copyright (C) 2014 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.app;\n\nimport static androidx.recyclerview.widget.RecyclerView.NO_POSITION;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Color;\nimport android.graphics.Rect;\nimport android.os.Bundle;\nimport android.util.Log;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.ViewGroup.MarginLayoutParams;\nimport android.view.ViewTreeObserver;\n\nimport androidx.annotation.ColorInt;\nimport androidx.core.view.ViewCompat;\nimport android.app.Fragment;\nimport android.app.Activity;\nimport android.app.FragmentManager;\nimport android.app.FragmentManager.BackStackEntry;\nimport android.app.FragmentTransaction;\nimport androidx.leanback.R;\nimport androidx.leanback.transition.TransitionHelper;\nimport androidx.leanback.transition.TransitionListener;\nimport androidx.leanback.util.StateMachine.Event;\nimport androidx.leanback.util.StateMachine.State;\nimport androidx.leanback.widget.BrowseFrameLayout;\nimport androidx.leanback.widget.InvisibleRowPresenter;\nimport androidx.leanback.widget.ListRow;\nimport androidx.leanback.widget.ObjectAdapter;\nimport androidx.leanback.widget.OnItemViewClickedListener;\nimport androidx.leanback.widget.OnItemViewSelectedListener;\nimport androidx.leanback.widget.PageRow;\nimport androidx.leanback.widget.Presenter;\nimport androidx.leanback.widget.PresenterSelector;\nimport androidx.leanback.widget.Row;\nimport androidx.leanback.widget.RowHeaderPresenter;\nimport androidx.leanback.widget.RowPresenter;\nimport androidx.leanback.widget.ScaleFrameLayout;\nimport androidx.leanback.widget.TitleViewAdapter;\nimport androidx.leanback.widget.VerticalGridView;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * A fragment for creating Leanback browse screens. It is composed of a\n * RowsFragment and a HeadersFragment.\n * <p>\n * A BrowseFragment renders the elements of its {@link ObjectAdapter} as a set\n * of rows in a vertical list. The elements in this adapter must be subclasses\n * of {@link Row}.\n * <p>\n * The HeadersFragment can be set to be either shown or hidden by default, or\n * may be disabled entirely. See {@link #setHeadersState} for details.\n * <p>\n * By default the BrowseFragment includes support for returning to the headers\n * when the user presses Back. For Activities that customize {@link\n * Activity#onBackPressed()}, you must disable this default Back key support by\n * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and\n * use {@link BrowseFragment.BrowseTransitionListener} and\n * {@link #startHeadersTransition(boolean)}.\n * <p>\n * The recommended theme to use with a BrowseFragment is\n * {@link androidx.leanback.R.style#Theme_Leanback_Browse}.\n * </p>\n * @deprecated use {@link BrowseSupportFragment}\n */\n@Deprecated\npublic class BrowseFragment extends BaseFragment {\n\n    // BUNDLE attribute for saving header show/hide status when backstack is used:\n    static final String HEADER_STACK_INDEX = \"headerStackIndex\";\n    // BUNDLE attribute for saving header show/hide status when backstack is not used:\n    static final String HEADER_SHOW = \"headerShow\";\n    private static final String IS_PAGE_ROW = \"isPageRow\";\n    private static final String CURRENT_SELECTED_POSITION = \"currentSelectedPosition\";\n\n    /**\n     * State to hide headers fragment.\n     */\n    final State STATE_SET_ENTRANCE_START_STATE = new State(\"SET_ENTRANCE_START_STATE\") {\n        @Override\n        public void run() {\n            setEntranceTransitionStartState();\n        }\n    };\n\n    /**\n     * Event for Header fragment view is created, we could perform\n     * {@link #setEntranceTransitionStartState()} to hide headers fragment initially.\n     */\n    final Event EVT_HEADER_VIEW_CREATED = new Event(\"headerFragmentViewCreated\");\n\n    /**\n     * Event for {@link #getMainFragment()} view is created, it's additional requirement to execute\n     * {@link #onEntranceTransitionPrepare()}.\n     */\n    final Event EVT_MAIN_FRAGMENT_VIEW_CREATED = new Event(\"mainFragmentViewCreated\");\n\n    /**\n     * Event that data for the screen is ready, this is additional requirement to launch entrance\n     * transition.\n     */\n    final Event EVT_SCREEN_DATA_READY = new Event(\"screenDataReady\");\n\n    @Override\n    void createStateMachineStates() {\n        super.createStateMachineStates();\n        mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);\n    }\n\n    @Override\n    void createStateMachineTransitions() {\n        super.createStateMachineTransitions();\n        // when headers fragment view is created we could setEntranceTransitionStartState()\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_SET_ENTRANCE_START_STATE,\n                EVT_HEADER_VIEW_CREATED);\n\n        // add additional requirement for onEntranceTransitionPrepare()\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,\n                STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,\n                EVT_MAIN_FRAGMENT_VIEW_CREATED);\n        // add additional requirement to launch entrance transition.\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,  STATE_ENTRANCE_PERFORM,\n                EVT_SCREEN_DATA_READY);\n    }\n\n    final class BackStackListener implements FragmentManager.OnBackStackChangedListener {\n        int mLastEntryCount;\n        int mIndexOfHeadersBackStack;\n\n        BackStackListener() {\n            mLastEntryCount = getFragmentManager().getBackStackEntryCount();\n            mIndexOfHeadersBackStack = -1;\n        }\n\n        void load(Bundle savedInstanceState) {\n            if (savedInstanceState != null) {\n                mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1);\n                mShowingHeaders = mIndexOfHeadersBackStack == -1;\n            } else {\n                if (!mShowingHeaders) {\n                    getFragmentManager().beginTransaction()\n                            .addToBackStack(mWithHeadersBackStackName).commit();\n                }\n            }\n        }\n\n        void save(Bundle outState) {\n            outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack);\n        }\n\n\n        @Override\n        public void onBackStackChanged() {\n            if (getFragmentManager() == null) {\n                Log.w(TAG, \"getFragmentManager() is null, stack:\", new Exception());\n                return;\n            }\n            int count = getFragmentManager().getBackStackEntryCount();\n            // if backstack is growing and last pushed entry is \"headers\" backstack,\n            // remember the index of the entry.\n            if (count > mLastEntryCount) {\n                BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);\n                if (mWithHeadersBackStackName.equals(entry.getName())) {\n                    mIndexOfHeadersBackStack = count - 1;\n                }\n            } else if (count < mLastEntryCount) {\n                // if popped \"headers\" backstack, initiate the show header transition if needed\n                if (mIndexOfHeadersBackStack >= count) {\n                    if (!isHeadersDataReady()) {\n                        // if main fragment was restored first before BrowseFragment's adapter gets\n                        // restored: don't start header transition, but add the entry back.\n                        getFragmentManager().beginTransaction()\n                                .addToBackStack(mWithHeadersBackStackName).commit();\n                        return;\n                    }\n                    mIndexOfHeadersBackStack = -1;\n                    if (!mShowingHeaders) {\n                        startHeadersTransitionInternal(true);\n                    }\n                }\n            }\n            mLastEntryCount = count;\n        }\n    }\n\n    /**\n     * Listener for transitions between browse headers and rows.\n     * @deprecated use {@link BrowseSupportFragment}\n     */\n    @Deprecated\n    public static class BrowseTransitionListener {\n        /**\n         * Callback when headers transition starts.\n         *\n         * @param withHeaders True if the transition will result in headers\n         *        being shown, false otherwise.\n         */\n        public void onHeadersTransitionStart(boolean withHeaders) {\n        }\n        /**\n         * Callback when headers transition stops.\n         *\n         * @param withHeaders True if the transition will result in headers\n         *        being shown, false otherwise.\n         */\n        public void onHeadersTransitionStop(boolean withHeaders) {\n        }\n    }\n\n    private final class SetSelectionRunnable implements Runnable {\n        static final int TYPE_INVALID = -1;\n        static final int TYPE_INTERNAL_SYNC = 0;\n        static final int TYPE_USER_REQUEST = 1;\n\n        private int mPosition;\n        private int mType;\n        private boolean mSmooth;\n\n        SetSelectionRunnable() {\n            reset();\n        }\n\n        void post(int position, int type, boolean smooth) {\n            // Posting the set selection, rather than calling it immediately, prevents an issue\n            // with adapter changes.  Example: a row is added before the current selected row;\n            // first the fast lane view updates its selection, then the rows fragment has that\n            // new selection propagated immediately; THEN the rows view processes the same adapter\n            // change and moves the selection again.\n            if (type >= mType) {\n                mPosition = position;\n                mType = type;\n                mSmooth = smooth;\n                mBrowseFrame.removeCallbacks(this);\n                if (!mStopped) {\n                    mBrowseFrame.post(this);\n                }\n            }\n        }\n\n        @Override\n        public void run() {\n            setSelection(mPosition, mSmooth);\n            reset();\n        }\n\n        public void stop() {\n            // remove possible callback when stop, it will be re-added in start().\n            mBrowseFrame.removeCallbacks(this);\n        }\n\n        public void start() {\n            if (mType != TYPE_INVALID) {\n                mBrowseFrame.post(this);\n            }\n        }\n\n        private void reset() {\n            mPosition = -1;\n            mType = TYPE_INVALID;\n            mSmooth = false;\n        }\n    }\n\n    /**\n     * Possible set of actions that {@link BrowseFragment} exposes to clients. Custom\n     * fragments can interact with {@link BrowseFragment} using this interface.\n     * @deprecated use {@link BrowseSupportFragment}\n     */\n    @Deprecated\n    public interface FragmentHost {\n        /**\n         * Fragments are required to invoke this callback once their view is created\n         * inside {@link Fragment#onViewCreated} method. {@link BrowseFragment} starts the entrance\n         * animation only after receiving this callback. Failure to invoke this method\n         * will lead to fragment not showing up.\n         *\n         * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.\n         */\n        void notifyViewCreated(MainFragmentAdapter fragmentAdapter);\n\n        /**\n         * Fragments mapped to {@link PageRow} are required to invoke this callback once their data\n         * is created for transition, the entrance animation only after receiving this callback.\n         * Failure to invoke this method will lead to fragment not showing up.\n         *\n         * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.\n         */\n        void notifyDataReady(MainFragmentAdapter fragmentAdapter);\n\n        /**\n         * Show or hide title view in {@link BrowseFragment} for fragments mapped to\n         * {@link PageRow}.  Otherwise the request is ignored, in that case BrowseFragment is fully\n         * in control of showing/hiding title view.\n         * <p>\n         * When HeadersFragment is visible, BrowseFragment will hide search affordance view if\n         * there are other focusable rows above currently focused row.\n         *\n         * @param show Boolean indicating whether or not to show the title view.\n         */\n        void showTitleView(boolean show);\n    }\n\n    /**\n     * Default implementation of {@link FragmentHost} that is used only by\n     * {@link BrowseFragment}.\n     */\n    private final class FragmentHostImpl implements FragmentHost {\n        boolean mShowTitleView = true;\n\n        FragmentHostImpl() {\n        }\n\n        @Override\n        public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) {\n            mStateMachine.fireEvent(EVT_MAIN_FRAGMENT_VIEW_CREATED);\n            if (!mIsPageRow) {\n                // If it's not a PageRow: it's a ListRow, so we already have data ready.\n                mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);\n            }\n        }\n\n        @Override\n        public void notifyDataReady(MainFragmentAdapter fragmentAdapter) {\n            // If fragment host is not the currently active fragment (in BrowseFragment), then\n            // ignore the request.\n            if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {\n                return;\n            }\n\n            // We only honor showTitle request for PageRows.\n            if (!mIsPageRow) {\n                return;\n            }\n\n            mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);\n        }\n\n        @Override\n        public void showTitleView(boolean show) {\n            mShowTitleView = show;\n\n            // If fragment host is not the currently active fragment (in BrowseFragment), then\n            // ignore the request.\n            if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {\n                return;\n            }\n\n            // We only honor showTitle request for PageRows.\n            if (!mIsPageRow) {\n                return;\n            }\n\n            updateTitleViewVisibility();\n        }\n    }\n\n    /**\n     * Interface that defines the interaction between {@link BrowseFragment} and its main\n     * content fragment. The key method is {@link MainFragmentAdapter#getFragment()},\n     * it will be used to get the fragment to be shown in the content section. Clients can\n     * provide any implementation of fragment and customize its interaction with\n     * {@link BrowseFragment} by overriding the necessary methods.\n     *\n     * <p>\n     * Clients are expected to provide\n     * an instance of {@link MainFragmentAdapterRegistry} which will be responsible for providing\n     * implementations of {@link MainFragmentAdapter} for given content types. Currently\n     * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype\n     * of {@link Row}. We provide an out of the box adapter implementation for any rows other than\n     * {@link PageRow} - {@link androidx.leanback.app.RowsFragment.MainFragmentAdapter}.\n     *\n     * <p>\n     * {@link PageRow} is intended to give full flexibility to developers in terms of Fragment\n     * design. Users will have to provide an implementation of {@link MainFragmentAdapter}\n     * and provide that through {@link MainFragmentAdapterRegistry}.\n     * {@link MainFragmentAdapter} implementation can supply any fragment and override\n     * just those interactions that makes sense.\n     * @deprecated use {@link BrowseSupportFragment}\n     */\n    @Deprecated\n    public static class MainFragmentAdapter<T extends Fragment> {\n        private boolean mScalingEnabled;\n        private final T mFragment;\n        FragmentHostImpl mFragmentHost;\n\n        public MainFragmentAdapter(T fragment) {\n            this.mFragment = fragment;\n        }\n\n        public final T getFragment() {\n            return mFragment;\n        }\n\n        /**\n         * Returns whether its scrolling.\n         */\n        public boolean isScrolling() {\n            return false;\n        }\n\n        /**\n         * Set the visibility of titles/hover card of browse rows.\n         */\n        public void setExpand(boolean expand) {\n        }\n\n        /**\n         * For rows that willing to participate entrance transition,  this function\n         * hide views if afterTransition is true,  show views if afterTransition is false.\n         */\n        public void setEntranceTransitionState(boolean state) {\n        }\n\n        /**\n         * Sets the window alignment and also the pivots for scale operation.\n         */\n        public void setAlignment(int windowAlignOffsetFromTop) {\n        }\n\n        /**\n         * Callback indicating transition prepare start.\n         */\n        public boolean onTransitionPrepare() {\n            return false;\n        }\n\n        /**\n         * Callback indicating transition start.\n         */\n        public void onTransitionStart() {\n        }\n\n        /**\n         * Callback indicating transition end.\n         */\n        public void onTransitionEnd() {\n        }\n\n        /**\n         * Returns whether row scaling is enabled.\n         */\n        public boolean isScalingEnabled() {\n            return mScalingEnabled;\n        }\n\n        /**\n         * Sets the row scaling property.\n         */\n        public void setScalingEnabled(boolean scalingEnabled) {\n            this.mScalingEnabled = scalingEnabled;\n        }\n\n        /**\n         * Returns the current host interface so that main fragment can interact with\n         * {@link BrowseFragment}.\n         */\n        public final FragmentHost getFragmentHost() {\n            return mFragmentHost;\n        }\n\n        void setFragmentHost(FragmentHostImpl fragmentHost) {\n            this.mFragmentHost = fragmentHost;\n        }\n    }\n\n    /**\n     * Interface to be implemented by all fragments for providing an instance of\n     * {@link MainFragmentAdapter}. Both {@link RowsFragment} and custom fragment provided\n     * against {@link PageRow} will need to implement this interface.\n     * @deprecated use {@link BrowseSupportFragment}\n     */\n    @Deprecated\n    public interface MainFragmentAdapterProvider {\n        /**\n         * Returns an instance of {@link MainFragmentAdapter} that {@link BrowseFragment}\n         * would use to communicate with the target fragment.\n         */\n        MainFragmentAdapter getMainFragmentAdapter();\n    }\n\n    /**\n     * Interface to be implemented by {@link RowsFragment} and its subclasses for providing\n     * an instance of {@link MainFragmentRowsAdapter}.\n     * @deprecated use {@link BrowseSupportFragment}\n     */\n    @Deprecated\n    public interface MainFragmentRowsAdapterProvider {\n        /**\n         * Returns an instance of {@link MainFragmentRowsAdapter} that {@link BrowseFragment}\n         * would use to communicate with the target fragment.\n         */\n        MainFragmentRowsAdapter getMainFragmentRowsAdapter();\n    }\n\n    /**\n     * This is used to pass information to {@link RowsFragment} or its subclasses.\n     * {@link BrowseFragment} uses this interface to pass row based interaction events to\n     * the target fragment.\n     * @deprecated use {@link BrowseSupportFragment}\n     */\n    @Deprecated\n    public static class MainFragmentRowsAdapter<T extends Fragment> {\n        private final T mFragment;\n\n        public MainFragmentRowsAdapter(T fragment) {\n            if (fragment == null) {\n                throw new IllegalArgumentException(\"Fragment can't be null\");\n            }\n            this.mFragment = fragment;\n        }\n\n        public final T getFragment() {\n            return mFragment;\n        }\n        /**\n         * Set the visibility titles/hover of browse rows.\n         */\n        public void setAdapter(ObjectAdapter adapter) {\n        }\n\n        /**\n         * Sets an item clicked listener on the fragment.\n         */\n        public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {\n        }\n\n        /**\n         * Sets an item selection listener.\n         */\n        public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {\n        }\n\n        /**\n         * Selects a Row and perform an optional task on the Row.\n         */\n        public void setSelectedPosition(int rowPosition,\n                                        boolean smooth,\n                                        final Presenter.ViewHolderTask rowHolderTask) {\n        }\n\n        /**\n         * Selects a Row.\n         */\n        public void setSelectedPosition(int rowPosition, boolean smooth) {\n        }\n\n        /**\n         * @return The position of selected row.\n         */\n        public int getSelectedPosition() {\n            return 0;\n        }\n\n        /**\n         * @param position Position of Row.\n         * @return Row ViewHolder.\n         */\n        public RowPresenter.ViewHolder findRowViewHolderByPosition(int position) {\n            return null;\n        }\n    }\n\n    private boolean createMainFragment(ObjectAdapter adapter, int position) {\n        Object item = null;\n        if (!mCanShowHeaders) {\n            // when header is disabled, we can decide to use RowsFragment even no data.\n        } else if (adapter == null || adapter.size() == 0) {\n            return false;\n        } else {\n            if (position < 0) {\n                position = 0;\n            } else if (position >= adapter.size()) {\n                throw new IllegalArgumentException(\n                        String.format(\"Invalid position %d requested\", position));\n            }\n            item = adapter.get(position);\n        }\n\n        boolean oldIsPageRow = mIsPageRow;\n        Object oldPageRow = mPageRow;\n        mIsPageRow = mCanShowHeaders && item instanceof PageRow;\n        mPageRow = mIsPageRow ? item : null;\n        boolean swap;\n\n        if (mMainFragment == null) {\n            swap = true;\n        } else {\n            if (oldIsPageRow) {\n                if (mIsPageRow) {\n                    if (oldPageRow == null) {\n                        // fragment is restored, page row object not yet set, so just set the\n                        // mPageRow object and there is no need to replace the fragment\n                        swap = false;\n                    } else {\n                        // swap if page row object changes\n                        swap = oldPageRow != mPageRow;\n                    }\n                } else {\n                    swap = true;\n                }\n            } else {\n                swap = mIsPageRow;\n            }\n        }\n\n        if (swap) {\n            mMainFragment = mMainFragmentAdapterRegistry.createFragment(item);\n            if (!(mMainFragment instanceof MainFragmentAdapterProvider)) {\n                throw new IllegalArgumentException(\n                        \"Fragment must implement MainFragmentAdapterProvider\");\n            }\n\n            setMainFragmentAdapter();\n        }\n\n        return swap;\n    }\n\n    void setMainFragmentAdapter() {\n        mMainFragmentAdapter = ((MainFragmentAdapterProvider) mMainFragment)\n                .getMainFragmentAdapter();\n        mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());\n        if (!mIsPageRow) {\n            if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {\n                setMainFragmentRowsAdapter(((MainFragmentRowsAdapterProvider) mMainFragment)\n                        .getMainFragmentRowsAdapter());\n            } else {\n                setMainFragmentRowsAdapter(null);\n            }\n            mIsPageRow = mMainFragmentRowsAdapter == null;\n        } else {\n            setMainFragmentRowsAdapter(null);\n        }\n    }\n\n    /**\n     * Factory class responsible for creating fragment given the current item. {@link ListRow}\n     * should return {@link RowsFragment} or its subclass whereas {@link PageRow}\n     * can return any fragment class.\n     * @deprecated use {@link BrowseSupportFragment}\n     */\n    @Deprecated\n    public abstract static class FragmentFactory<T extends Fragment> {\n        public abstract T createFragment(Object row);\n    }\n\n    /**\n     * FragmentFactory implementation for {@link ListRow}.\n     * @deprecated use {@link BrowseSupportFragment}\n     */\n    @Deprecated\n    public static class ListRowFragmentFactory extends FragmentFactory<RowsFragment> {\n        @Override\n        public RowsFragment createFragment(Object row) {\n            return new RowsFragment();\n        }\n    }\n\n    /**\n     * Registry class maintaining the mapping of {@link Row} subclasses to {@link FragmentFactory}.\n     * BrowseRowFragment automatically registers {@link ListRowFragmentFactory} for\n     * handling {@link ListRow}. Developers can override that and also if they want to\n     * use custom fragment, they can register a custom {@link FragmentFactory}\n     * against {@link PageRow}.\n     * @deprecated use {@link BrowseSupportFragment}\n     */\n    @Deprecated\n    public final static class MainFragmentAdapterRegistry {\n        private final Map<Class, FragmentFactory> mItemToFragmentFactoryMapping = new HashMap<>();\n        private final static FragmentFactory sDefaultFragmentFactory = new ListRowFragmentFactory();\n\n        public MainFragmentAdapterRegistry() {\n            registerFragment(ListRow.class, sDefaultFragmentFactory);\n        }\n\n        public void registerFragment(Class rowClass, FragmentFactory factory) {\n            mItemToFragmentFactoryMapping.put(rowClass, factory);\n        }\n\n        public Fragment createFragment(Object item) {\n            FragmentFactory fragmentFactory = item == null ? sDefaultFragmentFactory :\n                    mItemToFragmentFactoryMapping.get(item.getClass());\n            if (fragmentFactory == null && !(item instanceof PageRow)) {\n                fragmentFactory = sDefaultFragmentFactory;\n            }\n\n            return fragmentFactory.createFragment(item);\n        }\n    }\n\n    static final String TAG = \"BrowseFragment\";\n\n    private static final String LB_HEADERS_BACKSTACK = \"lbHeadersBackStack_\";\n\n    static final boolean DEBUG = false;\n\n    /** The headers fragment is enabled and shown by default. */\n    public static final int HEADERS_ENABLED = 1;\n\n    /** The headers fragment is enabled and hidden by default. */\n    public static final int HEADERS_HIDDEN = 2;\n\n    /** The headers fragment is disabled and will never be shown. */\n    public static final int HEADERS_DISABLED = 3;\n\n    private MainFragmentAdapterRegistry mMainFragmentAdapterRegistry =\n            new MainFragmentAdapterRegistry();\n    MainFragmentAdapter mMainFragmentAdapter;\n    Fragment mMainFragment;\n    HeadersFragment mHeadersFragment;\n    MainFragmentRowsAdapter mMainFragmentRowsAdapter;\n    ListRowDataAdapter mMainFragmentListRowDataAdapter;\n\n    private ObjectAdapter mAdapter;\n    private PresenterSelector mAdapterPresenter;\n\n    private int mHeadersState = HEADERS_ENABLED;\n    private int mBrandColor = Color.TRANSPARENT;\n    private boolean mBrandColorSet;\n\n    BrowseFrameLayout mBrowseFrame;\n    private ScaleFrameLayout mScaleFrameLayout;\n    boolean mHeadersBackStackEnabled = true;\n    String mWithHeadersBackStackName;\n    boolean mShowingHeaders = true;\n    boolean mCanShowHeaders = true;\n    private int mContainerListMarginStart;\n    private int mContainerListAlignTop;\n    private boolean mMainFragmentScaleEnabled = true;\n    OnItemViewSelectedListener mExternalOnItemViewSelectedListener;\n    private OnItemViewClickedListener mOnItemViewClickedListener;\n    private int mSelectedPosition = -1;\n    private float mScaleFactor;\n    boolean mIsPageRow;\n    Object mPageRow;\n    boolean mStopped = true;\n\n    private PresenterSelector mHeaderPresenterSelector;\n    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();\n\n    // transition related:\n    Object mSceneWithHeaders;\n    Object mSceneWithoutHeaders;\n    private Object mSceneAfterEntranceTransition;\n    Object mHeadersTransition;\n    BackStackListener mBackStackChangedListener;\n    BrowseTransitionListener mBrowseTransitionListener;\n\n    private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + \".title\";\n    private static final String ARG_HEADERS_STATE =\n        BrowseFragment.class.getCanonicalName() + \".headersState\";\n\n    /**\n     * Creates arguments for a browse fragment.\n     *\n     * @param args The Bundle to place arguments into, or null if the method\n     *        should return a new Bundle.\n     * @param title The title of the BrowseFragment.\n     * @param headersState The initial state of the headers of the\n     *        BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link\n     *        #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.\n     * @return A Bundle with the given arguments for creating a BrowseFragment.\n     */\n    public static Bundle createArgs(Bundle args, String title, int headersState) {\n        if (args == null) {\n            args = new Bundle();\n        }\n        args.putString(ARG_TITLE, title);\n        args.putInt(ARG_HEADERS_STATE, headersState);\n        return args;\n    }\n\n    /**\n     * Sets the brand color for the browse fragment. The brand color is used as\n     * the primary color for UI elements in the browse fragment. For example,\n     * the background color of the headers fragment uses the brand color.\n     *\n     * @param color The color to use as the brand color of the fragment.\n     */\n    public void setBrandColor(@ColorInt int color) {\n        mBrandColor = color;\n        mBrandColorSet = true;\n\n        if (mHeadersFragment != null) {\n            mHeadersFragment.setBackgroundColor(mBrandColor);\n        }\n    }\n\n    /**\n     * Returns the brand color for the browse fragment.\n     * The default is transparent.\n     */\n    @ColorInt\n    public int getBrandColor() {\n        return mBrandColor;\n    }\n\n    /**\n     * Wrapping app provided PresenterSelector to support InvisibleRowPresenter for SectionRow\n     * DividerRow and PageRow.\n     */\n    private void updateWrapperPresenter() {\n        if (mAdapter == null) {\n            mAdapterPresenter = null;\n            return;\n        }\n        final PresenterSelector adapterPresenter = mAdapter.getPresenterSelector();\n        if (adapterPresenter == null) {\n            throw new IllegalArgumentException(\"Adapter.getPresenterSelector() is null\");\n        }\n        if (adapterPresenter == mAdapterPresenter) {\n            return;\n        }\n        mAdapterPresenter = adapterPresenter;\n\n        Presenter[] presenters = adapterPresenter.getPresenters();\n        final Presenter invisibleRowPresenter = new InvisibleRowPresenter();\n        final Presenter[] allPresenters = new Presenter[presenters.length + 1];\n        System.arraycopy(allPresenters, 0, presenters, 0, presenters.length);\n        allPresenters[allPresenters.length - 1] = invisibleRowPresenter;\n        mAdapter.setPresenterSelector(new PresenterSelector() {\n            @Override\n            public Presenter getPresenter(Object item) {\n                Row row = (Row) item;\n                if (row.isRenderedAsRowView()) {\n                    return adapterPresenter.getPresenter(item);\n                } else {\n                    return invisibleRowPresenter;\n                }\n            }\n\n            @Override\n            public Presenter[] getPresenters() {\n                return allPresenters;\n            }\n        });\n    }\n\n    /**\n     * Sets the adapter containing the rows for the fragment.\n     *\n     * <p>The items referenced by the adapter must be be derived from\n     * {@link Row}. These rows will be used by the rows fragment and the headers\n     * fragment (if not disabled) to render the browse rows.\n     *\n     * @param adapter An ObjectAdapter for the browse rows. All items must\n     *        derive from {@link Row}.\n     */\n    public void setAdapter(ObjectAdapter adapter) {\n        mAdapter = adapter;\n        updateWrapperPresenter();\n        if (getView() == null) {\n            return;\n        }\n\n        updateMainFragmentRowsAdapter();\n        mHeadersFragment.setAdapter(mAdapter);\n    }\n\n    void setMainFragmentRowsAdapter(MainFragmentRowsAdapter mainFragmentRowsAdapter) {\n        if (mainFragmentRowsAdapter == mMainFragmentRowsAdapter) {\n            return;\n        }\n        // first clear previous mMainFragmentRowsAdapter and set a new mMainFragmentRowsAdapter\n        if (mMainFragmentRowsAdapter != null) {\n            // RowsFragment cannot change click/select listeners after view created.\n            // The main fragment and adapter should be GCed as long as there is no reference from\n            // BrowseFragment to it.\n            mMainFragmentRowsAdapter.setAdapter(null);\n        }\n        mMainFragmentRowsAdapter = mainFragmentRowsAdapter;\n        if (mMainFragmentRowsAdapter != null) {\n            mMainFragmentRowsAdapter.setOnItemViewSelectedListener(\n                    new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter));\n            mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);\n        }\n        // second update mMainFragmentListRowDataAdapter set on mMainFragmentRowsAdapter\n        updateMainFragmentRowsAdapter();\n    }\n\n    /**\n     * Update mMainFragmentListRowDataAdapter and set it on mMainFragmentRowsAdapter.\n     * It also clears old mMainFragmentListRowDataAdapter.\n     */\n    void updateMainFragmentRowsAdapter() {\n        if (mMainFragmentListRowDataAdapter != null) {\n            mMainFragmentListRowDataAdapter.detach();\n            mMainFragmentListRowDataAdapter = null;\n        }\n        if (mMainFragmentRowsAdapter != null) {\n            mMainFragmentListRowDataAdapter = mAdapter == null\n                    ? null : new ListRowDataAdapter(mAdapter);\n            mMainFragmentRowsAdapter.setAdapter(mMainFragmentListRowDataAdapter);\n        }\n    }\n\n    public final MainFragmentAdapterRegistry getMainFragmentRegistry() {\n        return mMainFragmentAdapterRegistry;\n    }\n\n    /**\n     * Returns the adapter containing the rows for the fragment.\n     */\n    public ObjectAdapter getAdapter() {\n        return mAdapter;\n    }\n\n    /**\n     * Sets an item selection listener.\n     */\n    public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {\n        mExternalOnItemViewSelectedListener = listener;\n    }\n\n    /**\n     * Returns an item selection listener.\n     */\n    public OnItemViewSelectedListener getOnItemViewSelectedListener() {\n        return mExternalOnItemViewSelectedListener;\n    }\n\n    /**\n     * Get RowsFragment if it's bound to BrowseFragment or null if either BrowseFragment has\n     * not been created yet or a different fragment is bound to it.\n     *\n     * @return RowsFragment if it's bound to BrowseFragment or null otherwise.\n     */\n    public RowsFragment getRowsFragment() {\n        if (mMainFragment instanceof RowsFragment) {\n            return (RowsFragment) mMainFragment;\n        }\n\n        return null;\n    }\n\n    /**\n     * @return Current main fragment or null if not created.\n     */\n    public Fragment getMainFragment() {\n        return mMainFragment;\n    }\n\n    /**\n     * Get currently bound HeadersFragment or null if HeadersFragment has not been created yet.\n     * @return Currently bound HeadersFragment or null if HeadersFragment has not been created yet.\n     */\n    public HeadersFragment getHeadersFragment() {\n        return mHeadersFragment;\n    }\n\n    /**\n     * Sets an item clicked listener on the fragment.\n     * OnItemViewClickedListener will override {@link View.OnClickListener} that\n     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.\n     * So in general, developer should choose one of the listeners but not both.\n     */\n    public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {\n        mOnItemViewClickedListener = listener;\n        if (mMainFragmentRowsAdapter != null) {\n            mMainFragmentRowsAdapter.setOnItemViewClickedListener(listener);\n        }\n    }\n\n    /**\n     * Returns the item Clicked listener.\n     */\n    public OnItemViewClickedListener getOnItemViewClickedListener() {\n        return mOnItemViewClickedListener;\n    }\n\n    /**\n     * Starts a headers transition.\n     *\n     * <p>This method will begin a transition to either show or hide the\n     * headers, depending on the value of withHeaders. If headers are disabled\n     * for this browse fragment, this method will throw an exception.\n     *\n     * @param withHeaders True if the headers should transition to being shown,\n     *        false if the transition should result in headers being hidden.\n     */\n    public void startHeadersTransition(boolean withHeaders) {\n        if (!mCanShowHeaders) {\n            throw new IllegalStateException(\"Cannot start headers transition\");\n        }\n        if (isInHeadersTransition() || mShowingHeaders == withHeaders) {\n            return;\n        }\n        startHeadersTransitionInternal(withHeaders);\n    }\n\n    /**\n     * Returns true if the headers transition is currently running.\n     */\n    public boolean isInHeadersTransition() {\n        return mHeadersTransition != null;\n    }\n\n    /**\n     * Returns true if headers are shown.\n     */\n    public boolean isShowingHeaders() {\n        return mShowingHeaders;\n    }\n\n    /**\n     * Sets a listener for browse fragment transitions.\n     *\n     * @param listener The listener to call when a browse headers transition\n     *        begins or ends.\n     */\n    public void setBrowseTransitionListener(BrowseTransitionListener listener) {\n        mBrowseTransitionListener = listener;\n    }\n\n    /**\n     * @deprecated use {@link BrowseFragment#enableMainFragmentScaling(boolean)} instead.\n     *\n     * @param enable true to enable row scaling\n     */\n    @Deprecated\n    public void enableRowScaling(boolean enable) {\n        enableMainFragmentScaling(enable);\n    }\n\n    /**\n     * Enables scaling of main fragment when headers are present. For the page/row fragment,\n     * scaling is enabled only when both this method and\n     * {@link MainFragmentAdapter#isScalingEnabled()} are enabled.\n     *\n     * @param enable true to enable row scaling\n     */\n    public void enableMainFragmentScaling(boolean enable) {\n        mMainFragmentScaleEnabled = enable;\n    }\n\n    void startHeadersTransitionInternal(final boolean withHeaders) {\n        if (getFragmentManager().isDestroyed()) {\n            return;\n        }\n        if (!isHeadersDataReady()) {\n            return;\n        }\n        mShowingHeaders = withHeaders;\n        mMainFragmentAdapter.onTransitionPrepare();\n        mMainFragmentAdapter.onTransitionStart();\n        onExpandTransitionStart(!withHeaders, new Runnable() {\n            @Override\n            public void run() {\n                mHeadersFragment.onTransitionPrepare();\n                mHeadersFragment.onTransitionStart();\n                createHeadersTransition();\n                if (mBrowseTransitionListener != null) {\n                    mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);\n                }\n                TransitionHelper.runTransition(\n                        withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition);\n                if (mHeadersBackStackEnabled) {\n                    if (!withHeaders) {\n                        getFragmentManager().beginTransaction()\n                                .addToBackStack(mWithHeadersBackStackName).commit();\n                    } else {\n                        int index = mBackStackChangedListener.mIndexOfHeadersBackStack;\n                        if (index >= 0) {\n                            BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);\n                            getFragmentManager().popBackStackImmediate(entry.getId(),\n                                    FragmentManager.POP_BACK_STACK_INCLUSIVE);\n                        }\n                    }\n                }\n            }\n        });\n    }\n\n    boolean isVerticalScrolling() {\n        // don't run transition\n        return mHeadersFragment.isScrolling() || mMainFragmentAdapter.isScrolling();\n    }\n\n\n    private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =\n            new BrowseFrameLayout.OnFocusSearchListener() {\n        @Override\n        public View onFocusSearch(View focused, int direction) {\n            // if headers is running transition,  focus stays\n            if (mCanShowHeaders && isInHeadersTransition()) {\n                return focused;\n            }\n            if (DEBUG) Log.v(TAG, \"onFocusSearch focused \" + focused + \" + direction \" + direction);\n\n            if (getTitleView() != null && focused != getTitleView()\n                    && direction == View.FOCUS_UP) {\n                return getTitleView();\n            }\n            if (getTitleView() != null && getTitleView().hasFocus()\n                    && direction == View.FOCUS_DOWN) {\n                return mCanShowHeaders && mShowingHeaders\n                        ? mHeadersFragment.getVerticalGridView() : mMainFragment.getView();\n            }\n\n            boolean isRtl = ViewCompat.getLayoutDirection(focused)\n                    == ViewCompat.LAYOUT_DIRECTION_RTL;\n            int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;\n            int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;\n            if (mCanShowHeaders && direction == towardStart) {\n                if (isVerticalScrolling() || mShowingHeaders || !isHeadersDataReady()) {\n                    return focused;\n                }\n                return mHeadersFragment.getVerticalGridView();\n            } else if (direction == towardEnd) {\n                if (isVerticalScrolling()) {\n                    return focused;\n                } else if (mMainFragment != null && mMainFragment.getView() != null) {\n                    return mMainFragment.getView();\n                }\n                return focused;\n            } else if (direction == View.FOCUS_DOWN && mShowingHeaders) {\n                // disable focus_down moving into PageFragment.\n                return focused;\n            } else {\n                return null;\n            }\n        }\n    };\n\n    final boolean isHeadersDataReady() {\n        return mAdapter != null && mAdapter.size() != 0;\n    }\n\n    private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =\n            new BrowseFrameLayout.OnChildFocusListener() {\n\n        @Override\n        public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {\n            if (getChildFragmentManager().isDestroyed()) {\n                return true;\n            }\n            // Make sure not changing focus when requestFocus() is called.\n            if (mCanShowHeaders && mShowingHeaders) {\n                if (mHeadersFragment != null && mHeadersFragment.getView() != null\n                        && mHeadersFragment.getView().requestFocus(\n                                direction, previouslyFocusedRect)) {\n                    return true;\n                }\n            }\n            if (mMainFragment != null && mMainFragment.getView() != null\n                    && mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) {\n                return true;\n            }\n            return getTitleView() != null\n                    && getTitleView().requestFocus(direction, previouslyFocusedRect);\n        }\n\n        @Override\n        public void onRequestChildFocus(View child, View focused) {\n            if (getChildFragmentManager().isDestroyed()) {\n                return;\n            }\n            if (!mCanShowHeaders || isInHeadersTransition()) return;\n            int childId = child.getId();\n            if (childId == R.id.browse_container_dock && mShowingHeaders) {\n                startHeadersTransitionInternal(false);\n            } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {\n                startHeadersTransitionInternal(true);\n            }\n        }\n    };\n\n    @Override\n    public void onSaveInstanceState(Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);\n        outState.putBoolean(IS_PAGE_ROW, mIsPageRow);\n\n        if (mBackStackChangedListener != null) {\n            mBackStackChangedListener.save(outState);\n        } else {\n            outState.putBoolean(HEADER_SHOW, mShowingHeaders);\n        }\n    }\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        final Context context = FragmentUtil.getContext(BrowseFragment.this);\n        TypedArray ta = context.obtainStyledAttributes(R.styleable.LeanbackTheme);\n        mContainerListMarginStart = (int) ta.getDimension(\n                R.styleable.LeanbackTheme_browseRowsMarginStart, context.getResources()\n                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));\n        mContainerListAlignTop = (int) ta.getDimension(\n                R.styleable.LeanbackTheme_browseRowsMarginTop, context.getResources()\n                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));\n        ta.recycle();\n\n        readArguments(getArguments());\n\n        if (mCanShowHeaders) {\n            if (mHeadersBackStackEnabled) {\n                mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;\n                mBackStackChangedListener = new BackStackListener();\n                getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);\n                mBackStackChangedListener.load(savedInstanceState);\n            } else {\n                if (savedInstanceState != null) {\n                    mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW);\n                }\n            }\n        }\n\n        mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1);\n    }\n\n    @Override\n    public void onDestroyView() {\n        setMainFragmentRowsAdapter(null);\n        mPageRow = null;\n        mMainFragmentAdapter = null;\n        mMainFragment = null;\n        mHeadersFragment = null;\n        super.onDestroyView();\n    }\n\n    @Override\n    public void onDestroy() {\n        if (mBackStackChangedListener != null) {\n            getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);\n        }\n        super.onDestroy();\n    }\n\n    /**\n     * Creates a new {@link HeadersFragment} instance. Subclass of BrowseFragment may override and\n     * return an instance of subclass of HeadersFragment, e.g. when app wants to replace presenter\n     * to render HeaderItem.\n     *\n     * @return A new instance of {@link HeadersFragment} or its subclass.\n     */\n    public HeadersFragment onCreateHeadersFragment() {\n        return new HeadersFragment();\n    }\n\n    @Override\n    public View onCreateView(LayoutInflater inflater, ViewGroup container,\n            Bundle savedInstanceState) {\n\n        if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {\n            mHeadersFragment = onCreateHeadersFragment();\n\n            createMainFragment(mAdapter, mSelectedPosition);\n            FragmentTransaction ft = getChildFragmentManager().beginTransaction()\n                    .replace(R.id.browse_headers_dock, mHeadersFragment);\n\n            if (mMainFragment != null) {\n                ft.replace(R.id.scale_frame, mMainFragment);\n            } else {\n                // Empty adapter used to guard against lazy adapter loading. When this\n                // fragment is instantiated, mAdapter might not have the data or might not\n                // have been set. In either of those cases mFragmentAdapter will be null.\n                // This way we can maintain the invariant that mMainFragmentAdapter is never\n                // null and it avoids doing null checks all over the code.\n                mMainFragmentAdapter = new MainFragmentAdapter(null);\n                mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());\n            }\n\n            ft.commit();\n        } else {\n            mHeadersFragment = (HeadersFragment) getChildFragmentManager()\n                    .findFragmentById(R.id.browse_headers_dock);\n            mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame);\n\n            mIsPageRow = savedInstanceState != null\n                    && savedInstanceState.getBoolean(IS_PAGE_ROW, false);\n            // mPageRow object is unable to restore, if its null and mIsPageRow is true, this is\n            // the case for restoring, later if setSelection() triggers a createMainFragment(),\n            // should not create fragment.\n\n            mSelectedPosition = savedInstanceState != null\n                    ? savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0;\n\n            setMainFragmentAdapter();\n        }\n\n        mHeadersFragment.setHeadersGone(!mCanShowHeaders);\n        if (mHeaderPresenterSelector != null) {\n            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);\n        }\n        mHeadersFragment.setAdapter(mAdapter);\n        mHeadersFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener);\n        mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener);\n\n        View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);\n\n        getProgressBarManager().setRootView((ViewGroup)root);\n\n        mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);\n        mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);\n        mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);\n\n        installTitleView(inflater, mBrowseFrame, savedInstanceState);\n\n        mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame);\n        mScaleFrameLayout.setPivotX(0);\n        mScaleFrameLayout.setPivotY(mContainerListAlignTop);\n\n        if (mBrandColorSet) {\n            mHeadersFragment.setBackgroundColor(mBrandColor);\n        }\n\n        mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {\n            @Override\n            public void run() {\n                showHeaders(true);\n            }\n        });\n        mSceneWithoutHeaders =  TransitionHelper.createScene(mBrowseFrame, new Runnable() {\n            @Override\n            public void run() {\n                showHeaders(false);\n            }\n        });\n        mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() {\n            @Override\n            public void run() {\n                setEntranceTransitionEndState();\n            }\n        });\n\n        return root;\n    }\n\n    void createHeadersTransition() {\n        mHeadersTransition = TransitionHelper.loadTransition(FragmentUtil.getContext(BrowseFragment.this),\n                mShowingHeaders\n                        ? R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);\n\n        TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() {\n            @Override\n            public void onTransitionStart(Object transition) {\n            }\n            @Override\n            public void onTransitionEnd(Object transition) {\n                mHeadersTransition = null;\n                if (mMainFragmentAdapter != null) {\n                    mMainFragmentAdapter.onTransitionEnd();\n                    if (!mShowingHeaders && mMainFragment != null) {\n                        View mainFragmentView = mMainFragment.getView();\n                        if (mainFragmentView != null && !mainFragmentView.hasFocus()) {\n                            mainFragmentView.requestFocus();\n                        }\n                    }\n                }\n                if (mHeadersFragment != null) {\n                    mHeadersFragment.onTransitionEnd();\n                    if (mShowingHeaders) {\n                        VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView();\n                        if (headerGridView != null && !headerGridView.hasFocus()) {\n                            headerGridView.requestFocus();\n                        }\n                    }\n                }\n\n                // Animate TitleView once header animation is complete.\n                updateTitleViewVisibility();\n\n                if (mBrowseTransitionListener != null) {\n                    mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);\n                }\n            }\n        });\n    }\n\n    void updateTitleViewVisibility() {\n        if (!mShowingHeaders) {\n            boolean showTitleView;\n            if (mIsPageRow && mMainFragmentAdapter != null) {\n                // page fragment case:\n                showTitleView = mMainFragmentAdapter.mFragmentHost.mShowTitleView;\n            } else {\n                // regular row view case:\n                showTitleView = isFirstRowWithContent(mSelectedPosition);\n            }\n            if (showTitleView) {\n                showTitle(TitleViewAdapter.FULL_VIEW_VISIBLE);\n            } else {\n                showTitle(false);\n            }\n        } else {\n            // when HeaderFragment is showing,  showBranding and showSearch are slightly different\n            boolean showBranding;\n            boolean showSearch;\n            if (mIsPageRow && mMainFragmentAdapter != null) {\n                showBranding = mMainFragmentAdapter.mFragmentHost.mShowTitleView;\n            } else {\n                showBranding = isFirstRowWithContent(mSelectedPosition);\n            }\n            showSearch = isFirstRowWithContentOrPageRow(mSelectedPosition);\n            int flags = 0;\n            if (showBranding) flags |= TitleViewAdapter.BRANDING_VIEW_VISIBLE;\n            if (showSearch) flags |= TitleViewAdapter.SEARCH_VIEW_VISIBLE;\n            if (flags != 0) {\n                showTitle(flags);\n            } else {\n                showTitle(false);\n            }\n        }\n    }\n\n    boolean isFirstRowWithContentOrPageRow(int rowPosition) {\n        if (mAdapter == null || mAdapter.size() == 0) {\n            return true;\n        }\n        for (int i = 0; i < mAdapter.size(); i++) {\n            final Row row = (Row) mAdapter.get(i);\n            if (row.isRenderedAsRowView() || row instanceof PageRow) {\n                return rowPosition == i;\n            }\n        }\n        return true;\n    }\n\n    boolean isFirstRowWithContent(int rowPosition) {\n        if (mAdapter == null || mAdapter.size() == 0) {\n            return true;\n        }\n        for (int i = 0; i < mAdapter.size(); i++) {\n            final Row row = (Row) mAdapter.get(i);\n            if (row.isRenderedAsRowView()) {\n                return rowPosition == i;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Sets the {@link PresenterSelector} used to render the row headers.\n     *\n     * @param headerPresenterSelector The PresenterSelector that will determine\n     *        the Presenter for each row header.\n     */\n    public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {\n        mHeaderPresenterSelector = headerPresenterSelector;\n        if (mHeadersFragment != null) {\n            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);\n        }\n    }\n\n    private void setHeadersOnScreen(boolean onScreen) {\n        MarginLayoutParams lp;\n        View containerList;\n        containerList = mHeadersFragment.getView();\n        lp = (MarginLayoutParams) containerList.getLayoutParams();\n        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);\n        containerList.setLayoutParams(lp);\n    }\n\n    void showHeaders(boolean show) {\n        if (DEBUG) Log.v(TAG, \"showHeaders \" + show);\n        mHeadersFragment.setHeadersEnabled(show);\n        setHeadersOnScreen(show);\n        expandMainFragment(!show);\n    }\n\n    private void expandMainFragment(boolean expand) {\n        MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams();\n        params.setMarginStart(!expand ? mContainerListMarginStart : 0);\n        mScaleFrameLayout.setLayoutParams(params);\n        mMainFragmentAdapter.setExpand(expand);\n\n        setMainFragmentAlignment();\n        final float scaleFactor = !expand\n                && mMainFragmentScaleEnabled\n                && mMainFragmentAdapter.isScalingEnabled() ? mScaleFactor : 1;\n        mScaleFrameLayout.setLayoutScaleY(scaleFactor);\n        mScaleFrameLayout.setChildScale(scaleFactor);\n    }\n\n    private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener =\n        new HeadersFragment.OnHeaderClickedListener() {\n            @Override\n            public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) {\n                if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {\n                    return;\n                }\n                if (mMainFragment == null || mMainFragment.getView() == null) {\n                    return;\n                }\n                startHeadersTransitionInternal(false);\n                mMainFragment.getView().requestFocus();\n            }\n        };\n\n    class MainFragmentItemViewSelectedListener implements OnItemViewSelectedListener {\n        MainFragmentRowsAdapter mMainFragmentRowsAdapter;\n\n        public MainFragmentItemViewSelectedListener(MainFragmentRowsAdapter fragmentRowsAdapter) {\n            mMainFragmentRowsAdapter = fragmentRowsAdapter;\n        }\n\n        @Override\n        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,\n                RowPresenter.ViewHolder rowViewHolder, Row row) {\n            int position = mMainFragmentRowsAdapter.getSelectedPosition();\n            if (DEBUG) Log.v(TAG, \"row selected position \" + position);\n            onRowSelected(position);\n            if (mExternalOnItemViewSelectedListener != null) {\n                mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,\n                        rowViewHolder, row);\n            }\n        }\n    };\n\n    private HeadersFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener =\n            new HeadersFragment.OnHeaderViewSelectedListener() {\n        @Override\n        public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) {\n            int position = mHeadersFragment.getSelectedPosition();\n            if (DEBUG) Log.v(TAG, \"header selected position \" + position);\n            // Layout of Headers Fragment in hidden state may triggers the onRowSelected and\n            // reset to 0. Skip in that case.\n            if (mShowingHeaders) {\n                onRowSelected(position);\n            }\n        }\n    };\n\n    void onRowSelected(int position) {\n        // even position is same, it could be data changed, always post selection runnable\n        // to possibly swap main fragment.\n        mSetSelectionRunnable.post(\n                position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);\n    }\n\n    void setSelection(int position, boolean smooth) {\n        if (position == NO_POSITION) {\n            return;\n        }\n\n        mSelectedPosition = position;\n        if (mHeadersFragment == null || mMainFragmentAdapter == null) {\n            // onDestroyView() called\n            return;\n        }\n        mHeadersFragment.setSelectedPosition(position, smooth);\n        replaceMainFragment(position);\n\n        if (mMainFragmentRowsAdapter != null) {\n            mMainFragmentRowsAdapter.setSelectedPosition(position, smooth);\n        }\n\n        updateTitleViewVisibility();\n    }\n\n    private void replaceMainFragment(int position) {\n        if (createMainFragment(mAdapter, position)) {\n            swapToMainFragment();\n            expandMainFragment(!(mCanShowHeaders && mShowingHeaders));\n        }\n    }\n\n    final void commitMainFragment() {\n        FragmentManager fm = getChildFragmentManager();\n        Fragment currentFragment = fm.findFragmentById(R.id.scale_frame);\n        if (currentFragment != mMainFragment) {\n            fm.beginTransaction()\n                    .replace(R.id.scale_frame, mMainFragment).commit();\n        }\n    }\n\n    private final RecyclerView.OnScrollListener mWaitScrollFinishAndCommitMainFragment =\n            new RecyclerView.OnScrollListener() {\n        @SuppressWarnings(\"ReferenceEquality\")\n        @Override\n        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {\n            if (newState == RecyclerView.SCROLL_STATE_IDLE) {\n                recyclerView.removeOnScrollListener(this);\n                if (!mStopped) {\n                    commitMainFragment();\n                }\n            }\n        }\n    };\n\n    private void swapToMainFragment() {\n        if (mStopped) {\n            return;\n        }\n        final VerticalGridView gridView = mHeadersFragment.getVerticalGridView();\n        if (isShowingHeaders() && gridView != null\n                && gridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {\n            // if user is scrolling HeadersFragment,  swap to empty fragment and wait scrolling\n            // finishes.\n            getChildFragmentManager().beginTransaction()\n                    .replace(R.id.scale_frame, new Fragment()).commit();\n            gridView.removeOnScrollListener(mWaitScrollFinishAndCommitMainFragment);\n            gridView.addOnScrollListener(mWaitScrollFinishAndCommitMainFragment);\n        } else {\n            // Otherwise swap immediately\n            commitMainFragment();\n        }\n    }\n\n    /**\n     * Sets the selected row position with smooth animation.\n     */\n    public void setSelectedPosition(int position) {\n        setSelectedPosition(position, true);\n    }\n\n    /**\n     * Gets position of currently selected row.\n     * @return Position of currently selected row.\n     */\n    public int getSelectedPosition() {\n        return mSelectedPosition;\n    }\n\n    /**\n     * @return selected row ViewHolder inside fragment created by {@link MainFragmentRowsAdapter}.\n     */\n    public RowPresenter.ViewHolder getSelectedRowViewHolder() {\n        if (mMainFragmentRowsAdapter != null) {\n            int rowPos = mMainFragmentRowsAdapter.getSelectedPosition();\n            return mMainFragmentRowsAdapter.findRowViewHolderByPosition(rowPos);\n        }\n        return null;\n    }\n\n    /**\n     * Sets the selected row position.\n     */\n    public void setSelectedPosition(int position, boolean smooth) {\n        mSetSelectionRunnable.post(\n                position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth);\n    }\n\n    /**\n     * Selects a Row and perform an optional task on the Row. For example\n     * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>\n     * scrolls to 11th row and selects 6th item on that row.  The method will be ignored if\n     * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,\n     * ViewGroup, Bundle)}).\n     *\n     * @param rowPosition Which row to select.\n     * @param smooth True to scroll to the row, false for no animation.\n     * @param rowHolderTask Optional task to perform on the Row.  When the task is not null, headers\n     * fragment will be collapsed.\n     */\n    public void setSelectedPosition(int rowPosition, boolean smooth,\n            final Presenter.ViewHolderTask rowHolderTask) {\n        if (mMainFragmentAdapterRegistry == null) {\n            return;\n        }\n        if (rowHolderTask != null) {\n            startHeadersTransition(false);\n        }\n        if (mMainFragmentRowsAdapter != null) {\n            mMainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask);\n        }\n    }\n\n    @Override\n    public void onStart() {\n        super.onStart();\n        mHeadersFragment.setAlignment(mContainerListAlignTop);\n        setMainFragmentAlignment();\n\n        if (mCanShowHeaders && mShowingHeaders && mHeadersFragment != null\n                && mHeadersFragment.getView() != null) {\n            mHeadersFragment.getView().requestFocus();\n        } else if ((!mCanShowHeaders || !mShowingHeaders) && mMainFragment != null\n                && mMainFragment.getView() != null) {\n            mMainFragment.getView().requestFocus();\n        }\n\n        if (mCanShowHeaders) {\n            showHeaders(mShowingHeaders);\n        }\n\n        mStateMachine.fireEvent(EVT_HEADER_VIEW_CREATED);\n\n        mStopped = false;\n        // if main fragment wasn't commited in stopped state, do it again in onStart()\n        commitMainFragment();\n        mSetSelectionRunnable.start();\n    }\n\n    @Override\n    public void onStop() {\n        mStopped = true;\n        mSetSelectionRunnable.stop();\n        super.onStop();\n    }\n\n    private void onExpandTransitionStart(boolean expand, final Runnable callback) {\n        if (expand) {\n            callback.run();\n            return;\n        }\n        // Run a \"pre\" layout when we go non-expand, in order to get the initial\n        // positions of added rows.\n        new ExpandPreLayout(callback, mMainFragmentAdapter, getView()).execute();\n    }\n\n    private void setMainFragmentAlignment() {\n        int alignOffset = mContainerListAlignTop;\n        if (mMainFragmentScaleEnabled\n                && mMainFragmentAdapter.isScalingEnabled()\n                && mShowingHeaders) {\n            alignOffset = (int) (alignOffset / mScaleFactor + 0.5f);\n        }\n        mMainFragmentAdapter.setAlignment(alignOffset);\n    }\n\n    /**\n     * Enables/disables headers transition on back key support. This is enabled by\n     * default. The BrowseFragment will add a back stack entry when headers are\n     * showing. Running a headers transition when the back key is pressed only\n     * works when the headers state is {@link #HEADERS_ENABLED} or\n     * {@link #HEADERS_HIDDEN}.\n     * <p>\n     * NOTE: If an Activity has its own onBackPressed() handling, you must\n     * disable this feature. You may use {@link #startHeadersTransition(boolean)}\n     * and {@link BrowseTransitionListener} in your own back stack handling.\n     */\n    public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {\n        mHeadersBackStackEnabled = headersBackStackEnabled;\n    }\n\n    /**\n     * Returns true if headers transition on back key support is enabled.\n     */\n    public final boolean isHeadersTransitionOnBackEnabled() {\n        return mHeadersBackStackEnabled;\n    }\n\n    private void readArguments(Bundle args) {\n        if (args == null) {\n            return;\n        }\n        if (args.containsKey(ARG_TITLE)) {\n            setTitle(args.getString(ARG_TITLE));\n        }\n        if (args.containsKey(ARG_HEADERS_STATE)) {\n            setHeadersState(args.getInt(ARG_HEADERS_STATE));\n        }\n    }\n\n    /**\n     * Sets the state for the headers column in the browse fragment. Must be one\n     * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or\n     * {@link #HEADERS_DISABLED}.\n     *\n     * @param headersState The state of the headers for the browse fragment.\n     */\n    public void setHeadersState(int headersState) {\n        if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {\n            throw new IllegalArgumentException(\"Invalid headers state: \" + headersState);\n        }\n        if (DEBUG) Log.v(TAG, \"setHeadersState \" + headersState);\n\n        if (headersState != mHeadersState) {\n            mHeadersState = headersState;\n            switch (headersState) {\n                case HEADERS_ENABLED:\n                    mCanShowHeaders = true;\n                    mShowingHeaders = true;\n                    break;\n                case HEADERS_HIDDEN:\n                    mCanShowHeaders = true;\n                    mShowingHeaders = false;\n                    break;\n                case HEADERS_DISABLED:\n                    mCanShowHeaders = false;\n                    mShowingHeaders = false;\n                    break;\n                default:\n                    Log.w(TAG, \"Unknown headers state: \" + headersState);\n                    break;\n            }\n            if (mHeadersFragment != null) {\n                mHeadersFragment.setHeadersGone(!mCanShowHeaders);\n            }\n        }\n    }\n\n    /**\n     * Returns the state of the headers column in the browse fragment.\n     */\n    public int getHeadersState() {\n        return mHeadersState;\n    }\n\n    @Override\n    protected Object createEntranceTransition() {\n        return TransitionHelper.loadTransition(FragmentUtil.getContext(BrowseFragment.this),\n                R.transition.lb_browse_entrance_transition);\n    }\n\n    @Override\n    protected void runEntranceTransition(Object entranceTransition) {\n        TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);\n    }\n\n    @Override\n    protected void onEntranceTransitionPrepare() {\n        mHeadersFragment.onTransitionPrepare();\n        mMainFragmentAdapter.setEntranceTransitionState(false);\n        mMainFragmentAdapter.onTransitionPrepare();\n    }\n\n    @Override\n    protected void onEntranceTransitionStart() {\n        mHeadersFragment.onTransitionStart();\n        mMainFragmentAdapter.onTransitionStart();\n    }\n\n    @Override\n    protected void onEntranceTransitionEnd() {\n        if (mMainFragmentAdapter != null) {\n            mMainFragmentAdapter.onTransitionEnd();\n        }\n\n        if (mHeadersFragment != null) {\n            mHeadersFragment.onTransitionEnd();\n        }\n    }\n\n    void setSearchOrbViewOnScreen(boolean onScreen) {\n        View searchOrbView = getTitleViewAdapter().getSearchAffordanceView();\n        if (searchOrbView != null) {\n            MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();\n            lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);\n            searchOrbView.setLayoutParams(lp);\n        }\n    }\n\n    void setEntranceTransitionStartState() {\n        setHeadersOnScreen(false);\n        setSearchOrbViewOnScreen(false);\n        // NOTE that mMainFragmentAdapter.setEntranceTransitionState(false) will be called\n        // in onEntranceTransitionPrepare() because mMainFragmentAdapter is still the dummy\n        // one when setEntranceTransitionStartState() is called.\n    }\n\n    void setEntranceTransitionEndState() {\n        setHeadersOnScreen(mShowingHeaders);\n        setSearchOrbViewOnScreen(true);\n        mMainFragmentAdapter.setEntranceTransitionState(true);\n    }\n\n    private class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener {\n\n        private final View mView;\n        private final Runnable mCallback;\n        private int mState;\n        private MainFragmentAdapter mainFragmentAdapter;\n\n        final static int STATE_INIT = 0;\n        final static int STATE_FIRST_DRAW = 1;\n        final static int STATE_SECOND_DRAW = 2;\n\n        ExpandPreLayout(Runnable callback, MainFragmentAdapter adapter, View view) {\n            mView = view;\n            mCallback = callback;\n            mainFragmentAdapter = adapter;\n        }\n\n        void execute() {\n            mView.getViewTreeObserver().addOnPreDrawListener(this);\n            mainFragmentAdapter.setExpand(false);\n            // always trigger onPreDraw even adapter setExpand() does nothing.\n            mView.invalidate();\n            mState = STATE_INIT;\n        }\n\n        @Override\n        public boolean onPreDraw() {\n            if (getView() == null || FragmentUtil.getContext(BrowseFragment.this) == null) {\n                mView.getViewTreeObserver().removeOnPreDrawListener(this);\n                return true;\n            }\n            if (mState == STATE_INIT) {\n                mainFragmentAdapter.setExpand(true);\n                // always trigger onPreDraw even adapter setExpand() does nothing.\n                mView.invalidate();\n                mState = STATE_FIRST_DRAW;\n            } else if (mState == STATE_FIRST_DRAW) {\n                mCallback.run();\n                mView.getViewTreeObserver().removeOnPreDrawListener(this);\n                mState = STATE_SECOND_DRAW;\n            }\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/BrowseSupportFragment.java",
    "content": "/*\n * Copyright (C) 2014 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.app;\n\nimport static androidx.recyclerview.widget.RecyclerView.NO_POSITION;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Color;\nimport android.graphics.Rect;\nimport android.os.Bundle;\nimport android.util.Log;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.ViewGroup.MarginLayoutParams;\nimport android.view.ViewTreeObserver;\n\nimport androidx.annotation.ColorInt;\nimport androidx.core.view.ViewCompat;\nimport androidx.fragment.app.Fragment;\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.fragment.app.FragmentManager;\nimport androidx.fragment.app.FragmentManager.BackStackEntry;\nimport androidx.fragment.app.FragmentTransaction;\nimport androidx.leanback.R;\nimport androidx.leanback.transition.TransitionHelper;\nimport androidx.leanback.transition.TransitionListener;\nimport androidx.leanback.util.StateMachine.Event;\nimport androidx.leanback.util.StateMachine.State;\nimport androidx.leanback.widget.BrowseFrameLayout;\nimport androidx.leanback.widget.InvisibleRowPresenter;\nimport androidx.leanback.widget.ListRow;\nimport androidx.leanback.widget.ObjectAdapter;\nimport androidx.leanback.widget.OnItemViewClickedListener;\nimport androidx.leanback.widget.OnItemViewSelectedListener;\nimport androidx.leanback.widget.PageRow;\nimport androidx.leanback.widget.Presenter;\nimport androidx.leanback.widget.PresenterSelector;\nimport androidx.leanback.widget.Row;\nimport androidx.leanback.widget.RowHeaderPresenter;\nimport androidx.leanback.widget.RowPresenter;\nimport androidx.leanback.widget.ScaleFrameLayout;\nimport androidx.leanback.widget.TitleViewAdapter;\nimport androidx.leanback.widget.VerticalGridView;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * A fragment for creating Leanback browse screens. It is composed of a\n * RowsSupportFragment and a HeadersSupportFragment.\n * <p>\n * A BrowseSupportFragment renders the elements of its {@link ObjectAdapter} as a set\n * of rows in a vertical list. The elements in this adapter must be subclasses\n * of {@link Row}.\n * <p>\n * The HeadersSupportFragment can be set to be either shown or hidden by default, or\n * may be disabled entirely. See {@link #setHeadersState} for details.\n * <p>\n * By default the BrowseSupportFragment includes support for returning to the headers\n * when the user presses Back. For Activities that customize {@link\n * FragmentActivity#onBackPressed()}, you must disable this default Back key support by\n * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and\n * use {@link BrowseSupportFragment.BrowseTransitionListener} and\n * {@link #startHeadersTransition(boolean)}.\n * <p>\n * The recommended theme to use with a BrowseSupportFragment is\n * {@link androidx.leanback.R.style#Theme.Leanback.Browse}.\n * </p>\n */\npublic class BrowseSupportFragment extends BaseSupportFragment {\n\n    // BUNDLE attribute for saving header show/hide status when backstack is used:\n    static final String HEADER_STACK_INDEX = \"headerStackIndex\";\n    // BUNDLE attribute for saving header show/hide status when backstack is not used:\n    static final String HEADER_SHOW = \"headerShow\";\n    private static final String IS_PAGE_ROW = \"isPageRow\";\n    private static final String CURRENT_SELECTED_POSITION = \"currentSelectedPosition\";\n\n    /**\n     * State to hide headers fragment.\n     */\n    final State STATE_SET_ENTRANCE_START_STATE = new State(\"SET_ENTRANCE_START_STATE\") {\n        @Override\n        public void run() {\n            setEntranceTransitionStartState();\n        }\n    };\n\n    /**\n     * Event for Header fragment view is created, we could perform\n     * {@link #setEntranceTransitionStartState()} to hide headers fragment initially.\n     */\n    final Event EVT_HEADER_VIEW_CREATED = new Event(\"headerFragmentViewCreated\");\n\n    /**\n     * Event for {@link #getMainFragment()} view is created, it's additional requirement to execute\n     * {@link #onEntranceTransitionPrepare()}.\n     */\n    final Event EVT_MAIN_FRAGMENT_VIEW_CREATED = new Event(\"mainFragmentViewCreated\");\n\n    /**\n     * Event that data for the screen is ready, this is additional requirement to launch entrance\n     * transition.\n     */\n    final Event EVT_SCREEN_DATA_READY = new Event(\"screenDataReady\");\n\n    @Override\n    void createStateMachineStates() {\n        super.createStateMachineStates();\n        mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);\n    }\n\n    @Override\n    void createStateMachineTransitions() {\n        super.createStateMachineTransitions();\n        // when headers fragment view is created we could setEntranceTransitionStartState()\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_SET_ENTRANCE_START_STATE,\n                EVT_HEADER_VIEW_CREATED);\n\n        // add additional requirement for onEntranceTransitionPrepare()\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,\n                STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,\n                EVT_MAIN_FRAGMENT_VIEW_CREATED);\n        // add additional requirement to launch entrance transition.\n        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,  STATE_ENTRANCE_PERFORM,\n                EVT_SCREEN_DATA_READY);\n    }\n\n    final class BackStackListener implements FragmentManager.OnBackStackChangedListener {\n        int mLastEntryCount;\n        int mIndexOfHeadersBackStack;\n\n        BackStackListener() {\n            mLastEntryCount = getFragmentManager().getBackStackEntryCount();\n            mIndexOfHeadersBackStack = -1;\n        }\n\n        void load(Bundle savedInstanceState) {\n            if (savedInstanceState != null) {\n                mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1);\n                mShowingHeaders = mIndexOfHeadersBackStack == -1;\n            } else {\n                if (!mShowingHeaders) {\n                    getFragmentManager().beginTransaction()\n                            .addToBackStack(mWithHeadersBackStackName).commit();\n                }\n            }\n        }\n\n        void save(Bundle outState) {\n            outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack);\n        }\n\n\n        @Override\n        public void onBackStackChanged() {\n            if (getFragmentManager() == null) {\n                Log.w(TAG, \"getFragmentManager() is null, stack:\", new Exception());\n                return;\n            }\n            int count = getFragmentManager().getBackStackEntryCount();\n            // if backstack is growing and last pushed entry is \"headers\" backstack,\n            // remember the index of the entry.\n            if (count > mLastEntryCount) {\n                BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);\n                if (mWithHeadersBackStackName.equals(entry.getName())) {\n                    mIndexOfHeadersBackStack = count - 1;\n                }\n            } else if (count < mLastEntryCount) {\n                // if popped \"headers\" backstack, initiate the show header transition if needed\n                if (mIndexOfHeadersBackStack >= count) {\n                    if (!isHeadersDataReady()) {\n                        // if main fragment was restored first before BrowseSupportFragment's adapter gets\n                        // restored: don't start header transition, but add the entry back.\n                        getFragmentManager().beginTransaction()\n                                .addToBackStack(mWithHeadersBackStackName).commit();\n                        return;\n                    }\n                    mIndexOfHeadersBackStack = -1;\n                    if (!mShowingHeaders) {\n                        startHeadersTransitionInternal(true);\n                    }\n                }\n            }\n            mLastEntryCount = count;\n        }\n    }\n\n    /**\n     * Listener for transitions between browse headers and rows.\n     */\n    public static class BrowseTransitionListener {\n        /**\n         * Callback when headers transition starts.\n         *\n         * @param withHeaders True if the transition will result in headers\n         *        being shown, false otherwise.\n         */\n        public void onHeadersTransitionStart(boolean withHeaders) {\n        }\n        /**\n         * Callback when headers transition stops.\n         *\n         * @param withHeaders True if the transition will result in headers\n         *        being shown, false otherwise.\n         */\n        public void onHeadersTransitionStop(boolean withHeaders) {\n        }\n    }\n\n    private final class SetSelectionRunnable implements Runnable {\n        static final int TYPE_INVALID = -1;\n        static final int TYPE_INTERNAL_SYNC = 0;\n        static final int TYPE_USER_REQUEST = 1;\n\n        private int mPosition;\n        private int mType;\n        private boolean mSmooth;\n\n        SetSelectionRunnable() {\n            reset();\n        }\n\n        void post(int position, int type, boolean smooth) {\n            // Posting the set selection, rather than calling it immediately, prevents an issue\n            // with adapter changes.  Example: a row is added before the current selected row;\n            // first the fast lane view updates its selection, then the rows fragment has that\n            // new selection propagated immediately; THEN the rows view processes the same adapter\n            // change and moves the selection again.\n            if (type >= mType) {\n                mPosition = position;\n                mType = type;\n                mSmooth = smooth;\n                mBrowseFrame.removeCallbacks(this);\n                if (!mStopped) {\n                    mBrowseFrame.post(this);\n                }\n            }\n        }\n\n        @Override\n        public void run() {\n            setSelection(mPosition, mSmooth);\n            reset();\n        }\n\n        public void stop() {\n            // remove possible callback when stop, it will be re-added in start().\n            mBrowseFrame.removeCallbacks(this);\n        }\n\n        public void start() {\n            if (mType != TYPE_INVALID) {\n                mBrowseFrame.post(this);\n            }\n        }\n\n        private void reset() {\n            mPosition = -1;\n            mType = TYPE_INVALID;\n            mSmooth = false;\n        }\n    }\n\n    /**\n     * Possible set of actions that {@link BrowseSupportFragment} exposes to clients. Custom\n     * fragments can interact with {@link BrowseSupportFragment} using this interface.\n     */\n    public interface FragmentHost {\n        /**\n         * Fragments are required to invoke this callback once their view is created\n         * inside {@link Fragment#onViewCreated} method. {@link BrowseSupportFragment} starts the entrance\n         * animation only after receiving this callback. Failure to invoke this method\n         * will lead to fragment not showing up.\n         *\n         * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.\n         */\n        void notifyViewCreated(MainFragmentAdapter fragmentAdapter);\n\n        /**\n         * Fragments mapped to {@link PageRow} are required to invoke this callback once their data\n         * is created for transition, the entrance animation only after receiving this callback.\n         * Failure to invoke this method will lead to fragment not showing up.\n         *\n         * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.\n         */\n        void notifyDataReady(MainFragmentAdapter fragmentAdapter);\n\n        /**\n         * Show or hide title view in {@link BrowseSupportFragment} for fragments mapped to\n         * {@link PageRow}.  Otherwise the request is ignored, in that case BrowseSupportFragment is fully\n         * in control of showing/hiding title view.\n         * <p>\n         * When HeadersSupportFragment is visible, BrowseSupportFragment will hide search affordance view if\n         * there are other focusable rows above currently focused row.\n         *\n         * @param show Boolean indicating whether or not to show the title view.\n         */\n        void showTitleView(boolean show);\n    }\n\n    /**\n     * Default implementation of {@link FragmentHost} that is used only by\n     * {@link BrowseSupportFragment}.\n     */\n    private final class FragmentHostImpl implements FragmentHost {\n        boolean mShowTitleView = true;\n\n        FragmentHostImpl() {\n        }\n\n        @Override\n        public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) {\n            mStateMachine.fireEvent(EVT_MAIN_FRAGMENT_VIEW_CREATED);\n            if (!mIsPageRow) {\n                // If it's not a PageRow: it's a ListRow, so we already have data ready.\n                mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);\n            }\n        }\n\n        @Override\n        public void notifyDataReady(MainFragmentAdapter fragmentAdapter) {\n            // If fragment host is not the currently active fragment (in BrowseSupportFragment), then\n            // ignore the request.\n            if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {\n                return;\n            }\n\n            // We only honor showTitle request for PageRows.\n            if (!mIsPageRow) {\n                return;\n            }\n\n            mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);\n        }\n\n        @Override\n        public void showTitleView(boolean show) {\n            mShowTitleView = show;\n\n            // If fragment host is not the currently active fragment (in BrowseSupportFragment), then\n            // ignore the request.\n            if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {\n                return;\n            }\n\n            // We only honor showTitle request for PageRows.\n            if (!mIsPageRow) {\n                return;\n            }\n\n            updateTitleViewVisibility();\n        }\n    }\n\n    /**\n     * Interface that defines the interaction between {@link BrowseSupportFragment} and its main\n     * content fragment. The key method is {@link MainFragmentAdapter#getFragment()},\n     * it will be used to get the fragment to be shown in the content section. Clients can\n     * provide any implementation of fragment and customize its interaction with\n     * {@link BrowseSupportFragment} by overriding the necessary methods.\n     *\n     * <p>\n     * Clients are expected to provide\n     * an instance of {@link MainFragmentAdapterRegistry} which will be responsible for providing\n     * implementations of {@link MainFragmentAdapter} for given content types. Currently\n     * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype\n     * of {@link Row}. We provide an out of the box adapter implementation for any rows other than\n     * {@link PageRow} - {@link androidx.leanback.app.RowsSupportFragment.MainFragmentAdapter}.\n     *\n     * <p>\n     * {@link PageRow} is intended to give full flexibility to developers in terms of Fragment\n     * design. Users will have to provide an implementation of {@link MainFragmentAdapter}\n     * and provide that through {@link MainFragmentAdapterRegistry}.\n     * {@link MainFragmentAdapter} implementation can supply any fragment and override\n     * just those interactions that makes sense.\n     */\n    public static class MainFragmentAdapter<T extends Fragment> {\n        private boolean mScalingEnabled;\n        private final T mFragment;\n        FragmentHostImpl mFragmentHost;\n\n        public MainFragmentAdapter(T fragment) {\n            this.mFragment = fragment;\n        }\n\n        public final T getFragment() {\n            return mFragment;\n        }\n\n        /**\n         * Returns whether its scrolling.\n         */\n        public boolean isScrolling() {\n            return false;\n        }\n\n        /**\n         * Set the visibility of titles/hover card of browse rows.\n         */\n        public void setExpand(boolean expand) {\n        }\n\n        /**\n         * For rows that willing to participate entrance transition,  this function\n         * hide views if afterTransition is true,  show views if afterTransition is false.\n         */\n        public void setEntranceTransitionState(boolean state) {\n        }\n\n        /**\n         * Sets the window alignment and also the pivots for scale operation.\n         */\n        public void setAlignment(int windowAlignOffsetFromTop) {\n        }\n\n        /**\n         * Callback indicating transition prepare start.\n         */\n        public boolean onTransitionPrepare() {\n            return false;\n        }\n\n        /**\n         * Callback indicating transition start.\n         */\n        public void onTransitionStart() {\n        }\n\n        /**\n         * Callback indicating transition end.\n         */\n        public void onTransitionEnd() {\n        }\n\n        /**\n         * Returns whether row scaling is enabled.\n         */\n        public boolean isScalingEnabled() {\n            return mScalingEnabled;\n        }\n\n        /**\n         * Sets the row scaling property.\n         */\n        public void setScalingEnabled(boolean scalingEnabled) {\n            this.mScalingEnabled = scalingEnabled;\n        }\n\n        /**\n         * Returns the current host interface so that main fragment can interact with\n         * {@link BrowseSupportFragment}.\n         */\n        public final FragmentHost getFragmentHost() {\n            return mFragmentHost;\n        }\n\n        void setFragmentHost(FragmentHostImpl fragmentHost) {\n            this.mFragmentHost = fragmentHost;\n        }\n    }\n\n    /**\n     * Interface to be implemented by all fragments for providing an instance of\n     * {@link MainFragmentAdapter}. Both {@link RowsSupportFragment} and custom fragment provided\n     * against {@link PageRow} will need to implement this interface.\n     */\n    public interface MainFragmentAdapterProvider {\n        /**\n         * Returns an instance of {@link MainFragmentAdapter} that {@link BrowseSupportFragment}\n         * would use to communicate with the target fragment.\n         */\n        MainFragmentAdapter getMainFragmentAdapter();\n    }\n\n    /**\n     * Interface to be implemented by {@link RowsSupportFragment} and its subclasses for providing\n     * an instance of {@link MainFragmentRowsAdapter}.\n     */\n    public interface MainFragmentRowsAdapterProvider {\n        /**\n         * Returns an instance of {@link MainFragmentRowsAdapter} that {@link BrowseSupportFragment}\n         * would use to communicate with the target fragment.\n         */\n        MainFragmentRowsAdapter getMainFragmentRowsAdapter();\n    }\n\n    /**\n     * This is used to pass information to {@link RowsSupportFragment} or its subclasses.\n     * {@link BrowseSupportFragment} uses this interface to pass row based interaction events to\n     * the target fragment.\n     */\n    public static class MainFragmentRowsAdapter<T extends Fragment> {\n        private final T mFragment;\n\n        public MainFragmentRowsAdapter(T fragment) {\n            if (fragment == null) {\n                throw new IllegalArgumentException(\"Fragment can't be null\");\n            }\n            this.mFragment = fragment;\n        }\n\n        public final T getFragment() {\n            return mFragment;\n        }\n        /**\n         * Set the visibility titles/hover of browse rows.\n         */\n        public void setAdapter(ObjectAdapter adapter) {\n        }\n\n        /**\n         * Sets an item clicked listener on the fragment.\n         */\n        public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {\n        }\n\n        /**\n         * Sets an item selection listener.\n         */\n        public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {\n        }\n\n        /**\n         * Selects a Row and perform an optional task on the Row.\n         */\n        public void setSelectedPosition(int rowPosition,\n                                        boolean smooth,\n                                        final Presenter.ViewHolderTask rowHolderTask) {\n        }\n\n        /**\n         * Selects a Row.\n         */\n        public void setSelectedPosition(int rowPosition, boolean smooth) {\n        }\n\n        /**\n         * @return The position of selected row.\n         */\n        public int getSelectedPosition() {\n            return 0;\n        }\n\n        /**\n         * @param position Position of Row.\n         * @return Row ViewHolder.\n         */\n        public RowPresenter.ViewHolder findRowViewHolderByPosition(int position) {\n            return null;\n        }\n    }\n\n    private boolean createMainFragment(ObjectAdapter adapter, int position) {\n        Object item = null;\n        if (!mCanShowHeaders) {\n            // when header is disabled, we can decide to use RowsSupportFragment even no data.\n        } else if (adapter == null || adapter.size() == 0) {\n            return false;\n        } else {\n            if (position < 0) {\n                position = 0;\n            } else if (position >= adapter.size()) {\n                throw new IllegalArgumentException(\n                        String.format(\"Invalid position %d requested\", position));\n            }\n            item = adapter.get(position);\n        }\n\n        boolean oldIsPageRow = mIsPageRow;\n        Object oldPageRow = mPageRow;\n        mIsPageRow = mCanShowHeaders && item instanceof PageRow;\n        mPageRow = mIsPageRow ? item : null;\n        boolean swap;\n\n        if (mMainFragment == null) {\n            swap = true;\n        } else {\n            if (oldIsPageRow) {\n                if (mIsPageRow) {\n                    if (oldPageRow == null) {\n                        // fragment is restored, page row object not yet set, so just set the\n                        // mPageRow object and there is no need to replace the fragment\n                        swap = false;\n                    } else {\n                        // swap if page row object changes\n                        swap = oldPageRow != mPageRow;\n                    }\n                } else {\n                    swap = true;\n                }\n            } else {\n                swap = mIsPageRow;\n            }\n        }\n\n        if (swap) {\n            mMainFragment = mMainFragmentAdapterRegistry.createFragment(item);\n            if (!(mMainFragment instanceof MainFragmentAdapterProvider)) {\n                throw new IllegalArgumentException(\n                        \"Fragment must implement MainFragmentAdapterProvider\");\n            }\n\n            setMainFragmentAdapter();\n        }\n\n        return swap;\n    }\n\n    void setMainFragmentAdapter() {\n        mMainFragmentAdapter = ((MainFragmentAdapterProvider) mMainFragment)\n                .getMainFragmentAdapter();\n        mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());\n        if (!mIsPageRow) {\n            if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {\n                setMainFragmentRowsAdapter(((MainFragmentRowsAdapterProvider) mMainFragment)\n                        .getMainFragmentRowsAdapter());\n            } else {\n                setMainFragmentRowsAdapter(null);\n            }\n            mIsPageRow = mMainFragmentRowsAdapter == null;\n        } else {\n            setMainFragmentRowsAdapter(null);\n        }\n    }\n\n    /**\n     * Factory class responsible for creating fragment given the current item. {@link ListRow}\n     * should return {@link RowsSupportFragment} or its subclass whereas {@link PageRow}\n     * can return any fragment class.\n     */\n    public abstract static class FragmentFactory<T extends Fragment> {\n        public abstract T createFragment(Object row);\n    }\n\n    /**\n     * FragmentFactory implementation for {@link ListRow}.\n     */\n    public static class ListRowFragmentFactory extends FragmentFactory<RowsSupportFragment> {\n        @Override\n        public RowsSupportFragment createFragment(Object row) {\n            return new RowsSupportFragment();\n        }\n    }\n\n    /**\n     * Registry class maintaining the mapping of {@link Row} subclasses to {@link FragmentFactory}.\n     * BrowseRowFragment automatically registers {@link ListRowFragmentFactory} for\n     * handling {@link ListRow}. Developers can override that and also if they want to\n     * use custom fragment, they can register a custom {@link FragmentFactory}\n     * against {@link PageRow}.\n     */\n    public final static class MainFragmentAdapterRegistry {\n        private final Map<Class, FragmentFactory> mItemToFragmentFactoryMapping = new HashMap<>();\n        private final static FragmentFactory sDefaultFragmentFactory = new ListRowFragmentFactory();\n\n        public MainFragmentAdapterRegistry() {\n            registerFragment(ListRow.class, sDefaultFragmentFactory);\n        }\n\n        public void registerFragment(Class rowClass, FragmentFactory factory) {\n            mItemToFragmentFactoryMapping.put(rowClass, factory);\n        }\n\n        public Fragment createFragment(Object item) {\n            FragmentFactory fragmentFactory = item == null ? sDefaultFragmentFactory :\n                    mItemToFragmentFactoryMapping.get(item.getClass());\n            if (fragmentFactory == null && !(item instanceof PageRow)) {\n                fragmentFactory = sDefaultFragmentFactory;\n            }\n\n            return fragmentFactory.createFragment(item);\n        }\n    }\n\n    static final String TAG = \"BrowseSupportFragment\";\n\n    private static final String LB_HEADERS_BACKSTACK = \"lbHeadersBackStack_\";\n\n    static final boolean DEBUG = false;\n\n    /** The headers fragment is enabled and shown by default. */\n    public static final int HEADERS_ENABLED = 1;\n\n    /** The headers fragment is enabled and hidden by default. */\n    public static final int HEADERS_HIDDEN = 2;\n\n    /** The headers fragment is disabled and will never be shown. */\n    public static final int HEADERS_DISABLED = 3;\n\n    private MainFragmentAdapterRegistry mMainFragmentAdapterRegistry =\n            new MainFragmentAdapterRegistry();\n    MainFragmentAdapter mMainFragmentAdapter;\n    Fragment mMainFragment;\n    HeadersSupportFragment mHeadersSupportFragment;\n    MainFragmentRowsAdapter mMainFragmentRowsAdapter;\n    ListRowDataAdapter mMainFragmentListRowDataAdapter;\n\n    private ObjectAdapter mAdapter;\n    private PresenterSelector mAdapterPresenter;\n\n    private int mHeadersState = HEADERS_ENABLED;\n    private int mBrandColor = Color.TRANSPARENT;\n    private boolean mBrandColorSet;\n\n    BrowseFrameLayout mBrowseFrame;\n    private ScaleFrameLayout mScaleFrameLayout;\n    boolean mHeadersBackStackEnabled = true;\n    String mWithHeadersBackStackName;\n    boolean mShowingHeaders = true;\n    boolean mCanShowHeaders = true;\n    private int mContainerListMarginStart;\n    private int mContainerListAlignTop;\n    private boolean mMainFragmentScaleEnabled = true;\n    OnItemViewSelectedListener mExternalOnItemViewSelectedListener;\n    private OnItemViewClickedListener mOnItemViewClickedListener;\n    private int mSelectedPosition = -1;\n    private float mScaleFactor;\n    boolean mIsPageRow;\n    Object mPageRow;\n    boolean mStopped = true;\n\n    private PresenterSelector mHeaderPresenterSelector;\n    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();\n\n    // transition related:\n    Object mSceneWithHeaders;\n    Object mSceneWithoutHeaders;\n    private Object mSceneAfterEntranceTransition;\n    Object mHeadersTransition;\n    BackStackListener mBackStackChangedListener;\n    BrowseTransitionListener mBrowseTransitionListener;\n\n    private static final String ARG_TITLE = BrowseSupportFragment.class.getCanonicalName() + \".title\";\n    private static final String ARG_HEADERS_STATE =\n        BrowseSupportFragment.class.getCanonicalName() + \".headersState\";\n\n    /**\n     * Creates arguments for a browse fragment.\n     *\n     * @param args The Bundle to place arguments into, or null if the method\n     *        should return a new Bundle.\n     * @param title The title of the BrowseSupportFragment.\n     * @param headersState The initial state of the headers of the\n     *        BrowseSupportFragment. Must be one of {@link #HEADERS_ENABLED}, {@link\n     *        #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.\n     * @return A Bundle with the given arguments for creating a BrowseSupportFragment.\n     */\n    public static Bundle createArgs(Bundle args, String title, int headersState) {\n        if (args == null) {\n            args = new Bundle();\n        }\n        args.putString(ARG_TITLE, title);\n        args.putInt(ARG_HEADERS_STATE, headersState);\n        return args;\n    }\n\n    /**\n     * Sets the brand color for the browse fragment. The brand color is used as\n     * the primary color for UI elements in the browse fragment. For example,\n     * the background color of the headers fragment uses the brand color.\n     *\n     * @param color The color to use as the brand color of the fragment.\n     */\n    public void setBrandColor(@ColorInt int color) {\n        mBrandColor = color;\n        mBrandColorSet = true;\n\n        if (mHeadersSupportFragment != null) {\n            mHeadersSupportFragment.setBackgroundColor(mBrandColor);\n        }\n    }\n\n    /**\n     * Returns the brand color for the browse fragment.\n     * The default is transparent.\n     */\n    @ColorInt\n    public int getBrandColor() {\n        return mBrandColor;\n    }\n\n    /**\n     * Wrapping app provided PresenterSelector to support InvisibleRowPresenter for SectionRow\n     * DividerRow and PageRow.\n     */\n    private void updateWrapperPresenter() {\n        if (mAdapter == null) {\n            mAdapterPresenter = null;\n            return;\n        }\n        final PresenterSelector adapterPresenter = mAdapter.getPresenterSelector();\n        if (adapterPresenter == null) {\n            throw new IllegalArgumentException(\"Adapter.getPresenterSelector() is null\");\n        }\n        if (adapterPresenter == mAdapterPresenter) {\n            return;\n        }\n        mAdapterPresenter = adapterPresenter;\n\n        Presenter[] presenters = adapterPresenter.getPresenters();\n        final Presenter invisibleRowPresenter = new InvisibleRowPresenter();\n        final Presenter[] allPresenters = new Presenter[presenters.length + 1];\n        System.arraycopy(allPresenters, 0, presenters, 0, presenters.length);\n        allPresenters[allPresenters.length - 1] = invisibleRowPresenter;\n        mAdapter.setPresenterSelector(new PresenterSelector() {\n            @Override\n            public Presenter getPresenter(Object item) {\n                Row row = (Row) item;\n                if (row.isRenderedAsRowView()) {\n                    return adapterPresenter.getPresenter(item);\n                } else {\n                    return invisibleRowPresenter;\n                }\n            }\n\n            @Override\n            public Presenter[] getPresenters() {\n                return allPresenters;\n            }\n        });\n    }\n\n    /**\n     * Sets the adapter containing the rows for the fragment.\n     *\n     * <p>The items referenced by the adapter must be be derived from\n     * {@link Row}. These rows will be used by the rows fragment and the headers\n     * fragment (if not disabled) to render the browse rows.\n     *\n     * @param adapter An ObjectAdapter for the browse rows. All items must\n     *        derive from {@link Row}.\n     */\n    public void setAdapter(ObjectAdapter adapter) {\n        mAdapter = adapter;\n        updateWrapperPresenter();\n        if (getView() == null) {\n            return;\n        }\n\n        updateMainFragmentRowsAdapter();\n        mHeadersSupportFragment.setAdapter(mAdapter);\n    }\n\n    void setMainFragmentRowsAdapter(MainFragmentRowsAdapter mainFragmentRowsAdapter) {\n        if (mainFragmentRowsAdapter == mMainFragmentRowsAdapter) {\n            return;\n        }\n        // first clear previous mMainFragmentRowsAdapter and set a new mMainFragmentRowsAdapter\n        if (mMainFragmentRowsAdapter != null) {\n            // RowsFragment cannot change click/select listeners after view created.\n            // The main fragment and adapter should be GCed as long as there is no reference from\n            // BrowseSupportFragment to it.\n            mMainFragmentRowsAdapter.setAdapter(null);\n        }\n        mMainFragmentRowsAdapter = mainFragmentRowsAdapter;\n        if (mMainFragmentRowsAdapter != null) {\n            mMainFragmentRowsAdapter.setOnItemViewSelectedListener(\n                    new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter));\n            mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);\n        }\n        // second update mMainFragmentListRowDataAdapter set on mMainFragmentRowsAdapter\n        updateMainFragmentRowsAdapter();\n    }\n\n    /**\n     * Update mMainFragmentListRowDataAdapter and set it on mMainFragmentRowsAdapter.\n     * It also clears old mMainFragmentListRowDataAdapter.\n     */\n    void updateMainFragmentRowsAdapter() {\n        if (mMainFragmentListRowDataAdapter != null) {\n            mMainFragmentListRowDataAdapter.detach();\n            mMainFragmentListRowDataAdapter = null;\n        }\n        if (mMainFragmentRowsAdapter != null) {\n            mMainFragmentListRowDataAdapter = mAdapter == null\n                    ? null : new ListRowDataAdapter(mAdapter);\n            mMainFragmentRowsAdapter.setAdapter(mMainFragmentListRowDataAdapter);\n        }\n    }\n\n    public final MainFragmentAdapterRegistry getMainFragmentRegistry() {\n        return mMainFragmentAdapterRegistry;\n    }\n\n    /**\n     * Returns the adapter containing the rows for the fragment.\n     */\n    public ObjectAdapter getAdapter() {\n        return mAdapter;\n    }\n\n    /**\n     * Sets an item selection listener.\n     */\n    public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {\n        mExternalOnItemViewSelectedListener = listener;\n    }\n\n    /**\n     * Returns an item selection listener.\n     */\n    public OnItemViewSelectedListener getOnItemViewSelectedListener() {\n        return mExternalOnItemViewSelectedListener;\n    }\n\n    /**\n     * Get RowsSupportFragment if it's bound to BrowseSupportFragment or null if either BrowseSupportFragment has\n     * not been created yet or a different fragment is bound to it.\n     *\n     * @return RowsSupportFragment if it's bound to BrowseSupportFragment or null otherwise.\n     */\n    public RowsSupportFragment getRowsSupportFragment() {\n        if (mMainFragment instanceof RowsSupportFragment) {\n            return (RowsSupportFragment) mMainFragment;\n        }\n\n        return null;\n    }\n\n    /**\n     * @return Current main fragment or null if not created.\n     */\n    public Fragment getMainFragment() {\n        return mMainFragment;\n    }\n\n    /**\n     * Get currently bound HeadersSupportFragment or null if HeadersSupportFragment has not been created yet.\n     * @return Currently bound HeadersSupportFragment or null if HeadersSupportFragment has not been created yet.\n     */\n    public HeadersSupportFragment getHeadersSupportFragment() {\n        return mHeadersSupportFragment;\n    }\n\n    /**\n     * Sets an item clicked listener on the fragment.\n     * OnItemViewClickedListener will override {@link View.OnClickListener} that\n     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.\n     * So in general, developer should choose one of the listeners but not both.\n     */\n    public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {\n        mOnItemViewClickedListener = listener;\n        if (mMainFragmentRowsAdapter != null) {\n            mMainFragmentRowsAdapter.setOnItemViewClickedListener(listener);\n        }\n    }\n\n    /**\n     * Returns the item Clicked listener.\n     */\n    public OnItemViewClickedListener getOnItemViewClickedListener() {\n        return mOnItemViewClickedListener;\n    }\n\n    /**\n     * Starts a headers transition.\n     *\n     * <p>This method will begin a transition to either show or hide the\n     * headers, depending on the value of withHeaders. If headers are disabled\n     * for this browse fragment, this method will throw an exception.\n     *\n     * @param withHeaders True if the headers should transition to being shown,\n     *        false if the transition should result in headers being hidden.\n     */\n    public void startHeadersTransition(boolean withHeaders) {\n        if (!mCanShowHeaders) {\n            throw new IllegalStateException(\"Cannot start headers transition\");\n        }\n        if (isInHeadersTransition() || mShowingHeaders == withHeaders) {\n            return;\n        }\n        startHeadersTransitionInternal(withHeaders);\n    }\n\n    /**\n     * Returns true if the headers transition is currently running.\n     */\n    public boolean isInHeadersTransition() {\n        return mHeadersTransition != null;\n    }\n\n    /**\n     * Returns true if headers are shown.\n     */\n    public boolean isShowingHeaders() {\n        return mShowingHeaders;\n    }\n\n    /**\n     * Sets a listener for browse fragment transitions.\n     *\n     * @param listener The listener to call when a browse headers transition\n     *        begins or ends.\n     */\n    public void setBrowseTransitionListener(BrowseTransitionListener listener) {\n        mBrowseTransitionListener = listener;\n    }\n\n    /**\n     * @deprecated use {@link BrowseSupportFragment#enableMainFragmentScaling(boolean)} instead.\n     *\n     * @param enable true to enable row scaling\n     */\n    @Deprecated\n    public void enableRowScaling(boolean enable) {\n        enableMainFragmentScaling(enable);\n    }\n\n    /**\n     * Enables scaling of main fragment when headers are present. For the page/row fragment,\n     * scaling is enabled only when both this method and\n     * {@link MainFragmentAdapter#isScalingEnabled()} are enabled.\n     *\n     * @param enable true to enable row scaling\n     */\n    public void enableMainFragmentScaling(boolean enable) {\n        mMainFragmentScaleEnabled = enable;\n    }\n\n    void startHeadersTransitionInternal(final boolean withHeaders) {\n        if (getFragmentManager().isDestroyed()) {\n            return;\n        }\n        if (!isHeadersDataReady()) {\n            return;\n        }\n        mShowingHeaders = withHeaders;\n        mMainFragmentAdapter.onTransitionPrepare();\n        mMainFragmentAdapter.onTransitionStart();\n        onExpandTransitionStart(!withHeaders, new Runnable() {\n            @Override\n            public void run() {\n                mHeadersSupportFragment.onTransitionPrepare();\n                mHeadersSupportFragment.onTransitionStart();\n                createHeadersTransition();\n                if (mBrowseTransitionListener != null) {\n                    mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);\n                }\n                TransitionHelper.runTransition(\n                        withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition);\n                if (mHeadersBackStackEnabled) {\n                    if (!withHeaders) {\n                        getFragmentManager().beginTransaction()\n                                .addToBackStack(mWithHeadersBackStackName).commit();\n                    } else {\n                        int index = mBackStackChangedListener.mIndexOfHeadersBackStack;\n                        if (index >= 0) {\n                            BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);\n                            getFragmentManager().popBackStackImmediate(entry.getId(),\n                                    FragmentManager.POP_BACK_STACK_INCLUSIVE);\n                        }\n                    }\n                }\n            }\n        });\n    }\n\n    boolean isVerticalScrolling() {\n        // don't run transition\n        return mHeadersSupportFragment.isScrolling() || mMainFragmentAdapter.isScrolling();\n    }\n\n\n    private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =\n            new BrowseFrameLayout.OnFocusSearchListener() {\n        @Override\n        public View onFocusSearch(View focused, int direction) {\n            // if headers is running transition,  focus stays\n            if (mCanShowHeaders && isInHeadersTransition()) {\n                return focused;\n            }\n            if (DEBUG) Log.v(TAG, \"onFocusSearch focused \" + focused + \" + direction \" + direction);\n\n            if (getTitleView() != null && focused != getTitleView()\n                    && direction == View.FOCUS_UP) {\n                return getTitleView();\n            }\n            if (getTitleView() != null && getTitleView().hasFocus()\n                    && direction == View.FOCUS_DOWN) {\n                return mCanShowHeaders && mShowingHeaders\n                        ? mHeadersSupportFragment.getVerticalGridView() : mMainFragment.getView();\n            }\n\n            boolean isRtl = ViewCompat.getLayoutDirection(focused)\n                    == ViewCompat.LAYOUT_DIRECTION_RTL;\n            int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;\n            int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;\n            if (mCanShowHeaders && direction == towardStart) {\n                if (isVerticalScrolling() || mShowingHeaders || !isHeadersDataReady()) {\n                    return focused;\n                }\n                return mHeadersSupportFragment.getVerticalGridView();\n            } else if (direction == towardEnd) {\n                if (isVerticalScrolling()) {\n                    return focused;\n                } else if (mMainFragment != null && mMainFragment.getView() != null) {\n                    return mMainFragment.getView();\n                }\n                return focused;\n            } else if (direction == View.FOCUS_DOWN && mShowingHeaders) {\n                // disable focus_down moving into PageFragment.\n                return focused;\n            } else {\n                return null;\n            }\n        }\n    };\n\n    final boolean isHeadersDataReady() {\n        return mAdapter != null && mAdapter.size() != 0;\n    }\n\n    private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =\n            new BrowseFrameLayout.OnChildFocusListener() {\n\n        @Override\n        public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {\n            if (getChildFragmentManager().isDestroyed()) {\n                return true;\n            }\n            // Make sure not changing focus when requestFocus() is called.\n            if (mCanShowHeaders && mShowingHeaders) {\n                if (mHeadersSupportFragment != null && mHeadersSupportFragment.getView() != null\n                        && mHeadersSupportFragment.getView().requestFocus(\n                                direction, previouslyFocusedRect)) {\n                    return true;\n                }\n            }\n            if (mMainFragment != null && mMainFragment.getView() != null\n                    && mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) {\n                return true;\n            }\n            return getTitleView() != null\n                    && getTitleView().requestFocus(direction, previouslyFocusedRect);\n        }\n\n        @Override\n        public void onRequestChildFocus(View child, View focused) {\n            if (getChildFragmentManager().isDestroyed()) {\n                return;\n            }\n            if (!mCanShowHeaders || isInHeadersTransition()) return;\n            int childId = child.getId();\n            if (childId == R.id.browse_container_dock && mShowingHeaders) {\n                startHeadersTransitionInternal(false);\n            } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {\n                startHeadersTransitionInternal(true);\n            }\n        }\n    };\n\n    @Override\n    public void onSaveInstanceState(Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);\n        outState.putBoolean(IS_PAGE_ROW, mIsPageRow);\n\n        if (mBackStackChangedListener != null) {\n            mBackStackChangedListener.save(outState);\n        } else {\n            outState.putBoolean(HEADER_SHOW, mShowingHeaders);\n        }\n    }\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        final Context context = getContext();\n        TypedArray ta = context.obtainStyledAttributes(R.styleable.LeanbackTheme);\n        mContainerListMarginStart = (int) ta.getDimension(\n                R.styleable.LeanbackTheme_browseRowsMarginStart, context.getResources()\n                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));\n        mContainerListAlignTop = (int) ta.getDimension(\n                R.styleable.LeanbackTheme_browseRowsMarginTop, context.getResources()\n                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));\n        ta.recycle();\n\n        readArguments(getArguments());\n\n        if (mCanShowHeaders) {\n            if (mHeadersBackStackEnabled) {\n                mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;\n                mBackStackChangedListener = new BackStackListener();\n                getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);\n                mBackStackChangedListener.load(savedInstanceState);\n            } else {\n                if (savedInstanceState != null) {\n                    mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW);\n                }\n            }\n        }\n\n        mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1);\n    }\n\n    @Override\n    public void onDestroyView() {\n        setMainFragmentRowsAdapter(null);\n        mPageRow = null;\n        mMainFragmentAdapter = null;\n        mMainFragment = null;\n        mHeadersSupportFragment = null;\n        super.onDestroyView();\n    }\n\n    @Override\n    public void onDestroy() {\n        if (mBackStackChangedListener != null) {\n            getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);\n        }\n        super.onDestroy();\n    }\n\n    /**\n     * Creates a new {@link HeadersSupportFragment} instance. Subclass of BrowseSupportFragment may override and\n     * return an instance of subclass of HeadersSupportFragment, e.g. when app wants to replace presenter\n     * to render HeaderItem.\n     *\n     * @return A new instance of {@link HeadersSupportFragment} or its subclass.\n     */\n    public HeadersSupportFragment onCreateHeadersSupportFragment() {\n        return new HeadersSupportFragment();\n    }\n\n    @Override\n    public View onCreateView(LayoutInflater inflater, ViewGroup container,\n            Bundle savedInstanceState) {\n\n        if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {\n            mHeadersSupportFragment = onCreateHeadersSupportFragment();\n\n            createMainFragment(mAdapter, mSelectedPosition);\n            FragmentTransaction ft = getChildFragmentManager().beginTransaction()\n                    .replace(R.id.browse_headers_dock, mHeadersSupportFragment);\n\n            if (mMainFragment != null) {\n                ft.replace(R.id.scale_frame, mMainFragment);\n            } else {\n                // Empty adapter used to guard against lazy adapter loading. When this\n                // fragment is instantiated, mAdapter might not have the data or might not\n                // have been set. In either of those cases mFragmentAdapter will be null.\n                // This way we can maintain the invariant that mMainFragmentAdapter is never\n                // null and it avoids doing null checks all over the code.\n                mMainFragmentAdapter = new MainFragmentAdapter(null);\n                mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());\n            }\n\n            ft.commit();\n        } else {\n            mHeadersSupportFragment = (HeadersSupportFragment) getChildFragmentManager()\n                    .findFragmentById(R.id.browse_headers_dock);\n            mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame);\n\n            mIsPageRow = savedInstanceState != null\n                    && savedInstanceState.getBoolean(IS_PAGE_ROW, false);\n            // mPageRow object is unable to restore, if its null and mIsPageRow is true, this is\n            // the case for restoring, later if setSelection() triggers a createMainFragment(),\n            // should not create fragment.\n\n            mSelectedPosition = savedInstanceState != null\n                    ? savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0;\n\n            setMainFragmentAdapter();\n        }\n\n        mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders);\n        if (mHeaderPresenterSelector != null) {\n            mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector);\n        }\n        mHeadersSupportFragment.setAdapter(mAdapter);\n        mHeadersSupportFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener);\n        mHeadersSupportFragment.setOnHeaderClickedListener(mHeaderClickedListener);\n\n        View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);\n\n        getProgressBarManager().setRootView((ViewGroup)root);\n\n        mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);\n        mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);\n        mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);\n\n        installTitleView(inflater, mBrowseFrame, savedInstanceState);\n\n        mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame);\n        mScaleFrameLayout.setPivotX(0);\n        mScaleFrameLayout.setPivotY(mContainerListAlignTop);\n\n        // MOD: touch support improvements. Show side bar by clicking on the left padding area.\n        View dock = root.findViewById(R.id.browse_headers_dock);\n        dock.setOnClickListener(v -> startHeadersTransitionInternal(true));\n\n        if (mBrandColorSet) {\n            mHeadersSupportFragment.setBackgroundColor(mBrandColor);\n        }\n\n        mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {\n            @Override\n            public void run() {\n                showHeaders(true);\n            }\n        });\n        mSceneWithoutHeaders =  TransitionHelper.createScene(mBrowseFrame, new Runnable() {\n            @Override\n            public void run() {\n                showHeaders(false);\n            }\n        });\n        mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() {\n            @Override\n            public void run() {\n                setEntranceTransitionEndState();\n            }\n        });\n\n        return root;\n    }\n\n    void createHeadersTransition() {\n        mHeadersTransition = TransitionHelper.loadTransition(getContext(),\n                mShowingHeaders\n                        ? R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);\n\n        TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() {\n            @Override\n            public void onTransitionStart(Object transition) {\n            }\n            @Override\n            public void onTransitionEnd(Object transition) {\n                mHeadersTransition = null;\n                if (mMainFragmentAdapter != null) {\n                    mMainFragmentAdapter.onTransitionEnd();\n                    if (!mShowingHeaders && mMainFragment != null) {\n                        View mainFragmentView = mMainFragment.getView();\n                        if (mainFragmentView != null && !mainFragmentView.hasFocus()) {\n                            mainFragmentView.requestFocus();\n                        }\n                    }\n                }\n                if (mHeadersSupportFragment != null) {\n                    mHeadersSupportFragment.onTransitionEnd();\n                    if (mShowingHeaders) {\n                        VerticalGridView headerGridView = mHeadersSupportFragment.getVerticalGridView();\n                        if (headerGridView != null && !headerGridView.hasFocus()) {\n                            headerGridView.requestFocus();\n                        }\n                    }\n                }\n\n                // Animate TitleView once header animation is complete.\n                updateTitleViewVisibility();\n\n                if (mBrowseTransitionListener != null) {\n                    mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);\n                }\n            }\n        });\n    }\n\n    void updateTitleViewVisibility() {\n        if (!mShowingHeaders) {\n            boolean showTitleView;\n            if (mIsPageRow && mMainFragmentAdapter != null) {\n                // page fragment case:\n                showTitleView = mMainFragmentAdapter.mFragmentHost.mShowTitleView;\n            } else {\n                // regular row view case:\n                showTitleView = isFirstRowWithContent(mSelectedPosition);\n            }\n            if (showTitleView) {\n                showTitle(TitleViewAdapter.FULL_VIEW_VISIBLE);\n            } else {\n                showTitle(false);\n            }\n        } else {\n            // when HeaderFragment is showing,  showBranding and showSearch are slightly different\n            boolean showBranding;\n            boolean showSearch;\n            if (mIsPageRow && mMainFragmentAdapter != null) {\n                showBranding = mMainFragmentAdapter.mFragmentHost.mShowTitleView;\n            } else {\n                showBranding = isFirstRowWithContent(mSelectedPosition);\n            }\n            showSearch = isFirstRowWithContentOrPageRow(mSelectedPosition);\n            int flags = 0;\n            if (showBranding) flags |= TitleViewAdapter.BRANDING_VIEW_VISIBLE;\n            if (showSearch) flags |= TitleViewAdapter.SEARCH_VIEW_VISIBLE;\n            if (flags != 0) {\n                showTitle(flags);\n            } else {\n                showTitle(false);\n            }\n        }\n    }\n\n    boolean isFirstRowWithContentOrPageRow(int rowPosition) {\n        if (mAdapter == null || mAdapter.size() == 0) {\n            return true;\n        }\n        for (int i = 0; i < mAdapter.size(); i++) {\n            final Row row = (Row) mAdapter.get(i);\n            if (row.isRenderedAsRowView() || row instanceof PageRow) {\n                return rowPosition == i;\n            }\n        }\n        return true;\n    }\n\n    boolean isFirstRowWithContent(int rowPosition) {\n        if (mAdapter == null || mAdapter.size() == 0) {\n            return true;\n        }\n        for (int i = 0; i < mAdapter.size(); i++) {\n            final Row row = (Row) mAdapter.get(i);\n            if (row.isRenderedAsRowView()) {\n                return rowPosition == i;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Sets the {@link PresenterSelector} used to render the row headers.\n     *\n     * @param headerPresenterSelector The PresenterSelector that will determine\n     *        the Presenter for each row header.\n     */\n    public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {\n        mHeaderPresenterSelector = headerPresenterSelector;\n        if (mHeadersSupportFragment != null) {\n            mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector);\n        }\n    }\n\n    private void setHeadersOnScreen(boolean onScreen) {\n        MarginLayoutParams lp;\n        View containerList;\n        containerList = mHeadersSupportFragment.getView();\n        lp = (MarginLayoutParams) containerList.getLayoutParams();\n        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);\n        containerList.setLayoutParams(lp);\n    }\n\n    void showHeaders(boolean show) {\n        if (DEBUG) Log.v(TAG, \"showHeaders \" + show);\n        mHeadersSupportFragment.setHeadersEnabled(show);\n        setHeadersOnScreen(show);\n        expandMainFragment(!show);\n    }\n\n    private void expandMainFragment(boolean expand) {\n        MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams();\n        params.setMarginStart(!expand ? mContainerListMarginStart : 0);\n        mScaleFrameLayout.setLayoutParams(params);\n        mMainFragmentAdapter.setExpand(expand);\n\n        setMainFragmentAlignment();\n        final float scaleFactor = !expand\n                && mMainFragmentScaleEnabled\n                && mMainFragmentAdapter.isScalingEnabled() ? mScaleFactor : 1;\n        mScaleFrameLayout.setLayoutScaleY(scaleFactor);\n        mScaleFrameLayout.setChildScale(scaleFactor);\n    }\n\n    private HeadersSupportFragment.OnHeaderClickedListener mHeaderClickedListener =\n        new HeadersSupportFragment.OnHeaderClickedListener() {\n            @Override\n            public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) {\n                if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {\n                    return;\n                }\n                if (mMainFragment == null || mMainFragment.getView() == null) {\n                    return;\n                }\n                startHeadersTransitionInternal(false);\n                mMainFragment.getView().requestFocus();\n            }\n        };\n\n    class MainFragmentItemViewSelectedListener implements OnItemViewSelectedListener {\n        MainFragmentRowsAdapter mMainFragmentRowsAdapter;\n\n        public MainFragmentItemViewSelectedListener(MainFragmentRowsAdapter fragmentRowsAdapter) {\n            mMainFragmentRowsAdapter = fragmentRowsAdapter;\n        }\n\n        @Override\n        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,\n                RowPresenter.ViewHolder rowViewHolder, Row row) {\n            int position = mMainFragmentRowsAdapter.getSelectedPosition();\n            if (DEBUG) Log.v(TAG, \"row selected position \" + position);\n            onRowSelected(position);\n            if (mExternalOnItemViewSelectedListener != null) {\n                mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,\n                        rowViewHolder, row);\n            }\n        }\n    };\n\n    private HeadersSupportFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener =\n            new HeadersSupportFragment.OnHeaderViewSelectedListener() {\n        @Override\n        public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) {\n            int position = mHeadersSupportFragment.getSelectedPosition();\n            if (DEBUG) Log.v(TAG, \"header selected position \" + position);\n            // Layout of Headers Fragment in hidden state may triggers the onRowSelected and\n            // reset to 0. Skip in that case.\n            if (mShowingHeaders) {\n                onRowSelected(position);\n            }\n        }\n    };\n\n    void onRowSelected(int position) {\n        // even position is same, it could be data changed, always post selection runnable\n        // to possibly swap main fragment.\n        mSetSelectionRunnable.post(\n                position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);\n    }\n\n    void setSelection(int position, boolean smooth) {\n        if (position == NO_POSITION) {\n            return;\n        }\n\n        mSelectedPosition = position;\n        if (mHeadersSupportFragment == null || mMainFragmentAdapter == null) {\n            // onDestroyView() called\n            return;\n        }\n        mHeadersSupportFragment.setSelectedPosition(position, smooth);\n        replaceMainFragment(position);\n\n        if (mMainFragmentRowsAdapter != null) {\n            mMainFragmentRowsAdapter.setSelectedPosition(position, smooth);\n        }\n\n        updateTitleViewVisibility();\n    }\n\n    private void replaceMainFragment(int position) {\n        if (createMainFragment(mAdapter, position)) {\n            swapToMainFragment();\n            expandMainFragment(!(mCanShowHeaders && mShowingHeaders));\n        }\n    }\n\n    final void commitMainFragment() {\n        FragmentManager fm = getChildFragmentManager();\n        Fragment currentFragment = fm.findFragmentById(R.id.scale_frame);\n        if (currentFragment != mMainFragment) {\n            fm.beginTransaction()\n                    .replace(R.id.scale_frame, mMainFragment).commit();\n        }\n    }\n\n    private final RecyclerView.OnScrollListener mWaitScrollFinishAndCommitMainFragment =\n            new RecyclerView.OnScrollListener() {\n        @SuppressWarnings(\"ReferenceEquality\")\n        @Override\n        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {\n            if (newState == RecyclerView.SCROLL_STATE_IDLE) {\n                recyclerView.removeOnScrollListener(this);\n                if (!mStopped) {\n                    commitMainFragment();\n                }\n            }\n        }\n    };\n\n    private void swapToMainFragment() {\n        if (mStopped) {\n            return;\n        }\n        final VerticalGridView gridView = mHeadersSupportFragment.getVerticalGridView();\n        if (isShowingHeaders() && gridView != null\n                && gridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {\n            // if user is scrolling HeadersSupportFragment,  swap to empty fragment and wait scrolling\n            // finishes.\n            getChildFragmentManager().beginTransaction()\n                    .replace(R.id.scale_frame, new Fragment()).commit();\n            gridView.removeOnScrollListener(mWaitScrollFinishAndCommitMainFragment);\n            gridView.addOnScrollListener(mWaitScrollFinishAndCommitMainFragment);\n        } else {\n            // Otherwise swap immediately\n            commitMainFragment();\n        }\n    }\n\n    /**\n     * Sets the selected row position with smooth animation.\n     */\n    public void setSelectedPosition(int position) {\n        setSelectedPosition(position, true);\n    }\n\n    /**\n     * Gets position of currently selected row.\n     * @return Position of currently selected row.\n     */\n    public int getSelectedPosition() {\n        return mSelectedPosition;\n    }\n\n    /**\n     * @return selected row ViewHolder inside fragment created by {@link MainFragmentRowsAdapter}.\n     */\n    public RowPresenter.ViewHolder getSelectedRowViewHolder() {\n        if (mMainFragmentRowsAdapter != null) {\n            int rowPos = mMainFragmentRowsAdapter.getSelectedPosition();\n            return mMainFragmentRowsAdapter.findRowViewHolderByPosition(rowPos);\n        }\n        return null;\n    }\n\n    /**\n     * Sets the selected row position.\n     */\n    public void setSelectedPosition(int position, boolean smooth) {\n        mSetSelectionRunnable.post(\n                position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth);\n    }\n\n    /**\n     * Selects a Row and perform an optional task on the Row. For example\n     * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>\n     * scrolls to 11th row and selects 6th item on that row.  The method will be ignored if\n     * RowsSupportFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,\n     * ViewGroup, Bundle)}).\n     *\n     * @param rowPosition Which row to select.\n     * @param smooth True to scroll to the row, false for no animation.\n     * @param rowHolderTask Optional task to perform on the Row.  When the task is not null, headers\n     * fragment will be collapsed.\n     */\n    public void setSelectedPosition(int rowPosition, boolean smooth,\n            final Presenter.ViewHolderTask rowHolderTask) {\n        if (mMainFragmentAdapterRegistry == null) {\n            return;\n        }\n        if (rowHolderTask != null) {\n            startHeadersTransition(false);\n        }\n        if (mMainFragmentRowsAdapter != null) {\n            mMainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask);\n        }\n    }\n\n    @Override\n    public void onStart() {\n        super.onStart();\n        mHeadersSupportFragment.setAlignment(mContainerListAlignTop);\n        setMainFragmentAlignment();\n\n        if (mCanShowHeaders && mShowingHeaders && mHeadersSupportFragment != null\n                && mHeadersSupportFragment.getView() != null) {\n            mHeadersSupportFragment.getView().requestFocus();\n        } else if ((!mCanShowHeaders || !mShowingHeaders) && mMainFragment != null\n                && mMainFragment.getView() != null) {\n            mMainFragment.getView().requestFocus();\n        }\n\n        if (mCanShowHeaders) {\n            showHeaders(mShowingHeaders);\n        }\n\n        mStateMachine.fireEvent(EVT_HEADER_VIEW_CREATED);\n\n        mStopped = false;\n        // if main fragment wasn't commited in stopped state, do it again in onStart()\n        commitMainFragment();\n        mSetSelectionRunnable.start();\n    }\n\n    @Override\n    public void onStop() {\n        mStopped = true;\n        mSetSelectionRunnable.stop();\n        super.onStop();\n    }\n\n    private void onExpandTransitionStart(boolean expand, final Runnable callback) {\n        if (expand) {\n            callback.run();\n            return;\n        }\n        // Run a \"pre\" layout when we go non-expand, in order to get the initial\n        // positions of added rows.\n        new ExpandPreLayout(callback, mMainFragmentAdapter, getView()).execute();\n    }\n\n    private void setMainFragmentAlignment() {\n        int alignOffset = mContainerListAlignTop;\n        if (mMainFragmentScaleEnabled\n                && mMainFragmentAdapter.isScalingEnabled()\n                && mShowingHeaders) {\n            alignOffset = (int) (alignOffset / mScaleFactor + 0.5f);\n        }\n        mMainFragmentAdapter.setAlignment(alignOffset);\n    }\n\n    /**\n     * Enables/disables headers transition on back key support. This is enabled by\n     * default. The BrowseSupportFragment will add a back stack entry when headers are\n     * showing. Running a headers transition when the back key is pressed only\n     * works when the headers state is {@link #HEADERS_ENABLED} or\n     * {@link #HEADERS_HIDDEN}.\n     * <p>\n     * NOTE: If an Activity has its own onBackPressed() handling, you must\n     * disable this feature. You may use {@link #startHeadersTransition(boolean)}\n     * and {@link BrowseTransitionListener} in your own back stack handling.\n     */\n    public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {\n        mHeadersBackStackEnabled = headersBackStackEnabled;\n    }\n\n    /**\n     * Returns true if headers transition on back key support is enabled.\n     */\n    public final boolean isHeadersTransitionOnBackEnabled() {\n        return mHeadersBackStackEnabled;\n    }\n\n    private void readArguments(Bundle args) {\n        if (args == null) {\n            return;\n        }\n        if (args.containsKey(ARG_TITLE)) {\n            setTitle(args.getString(ARG_TITLE));\n        }\n        if (args.containsKey(ARG_HEADERS_STATE)) {\n            setHeadersState(args.getInt(ARG_HEADERS_STATE));\n        }\n    }\n\n    /**\n     * Sets the state for the headers column in the browse fragment. Must be one\n     * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or\n     * {@link #HEADERS_DISABLED}.\n     *\n     * @param headersState The state of the headers for the browse fragment.\n     */\n    public void setHeadersState(int headersState) {\n        if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {\n            throw new IllegalArgumentException(\"Invalid headers state: \" + headersState);\n        }\n        if (DEBUG) Log.v(TAG, \"setHeadersState \" + headersState);\n\n        if (headersState != mHeadersState) {\n            mHeadersState = headersState;\n            switch (headersState) {\n                case HEADERS_ENABLED:\n                    mCanShowHeaders = true;\n                    mShowingHeaders = true;\n                    break;\n                case HEADERS_HIDDEN:\n                    mCanShowHeaders = true;\n                    mShowingHeaders = false;\n                    break;\n                case HEADERS_DISABLED:\n                    mCanShowHeaders = false;\n                    mShowingHeaders = false;\n                    break;\n                default:\n                    Log.w(TAG, \"Unknown headers state: \" + headersState);\n                    break;\n            }\n            if (mHeadersSupportFragment != null) {\n                mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders);\n            }\n        }\n    }\n\n    /**\n     * Returns the state of the headers column in the browse fragment.\n     */\n    public int getHeadersState() {\n        return mHeadersState;\n    }\n\n    @Override\n    protected Object createEntranceTransition() {\n        return TransitionHelper.loadTransition(getContext(),\n                R.transition.lb_browse_entrance_transition);\n    }\n\n    @Override\n    protected void runEntranceTransition(Object entranceTransition) {\n        TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);\n    }\n\n    @Override\n    protected void onEntranceTransitionPrepare() {\n        mHeadersSupportFragment.onTransitionPrepare();\n        mMainFragmentAdapter.setEntranceTransitionState(false);\n        mMainFragmentAdapter.onTransitionPrepare();\n    }\n\n    @Override\n    protected void onEntranceTransitionStart() {\n        mHeadersSupportFragment.onTransitionStart();\n        mMainFragmentAdapter.onTransitionStart();\n    }\n\n    @Override\n    protected void onEntranceTransitionEnd() {\n        if (mMainFragmentAdapter != null) {\n            mMainFragmentAdapter.onTransitionEnd();\n        }\n\n        if (mHeadersSupportFragment != null) {\n            mHeadersSupportFragment.onTransitionEnd();\n        }\n    }\n\n    void setSearchOrbViewOnScreen(boolean onScreen) {\n        View searchOrbView = getTitleViewAdapter().getSearchAffordanceView();\n        if (searchOrbView != null) {\n            MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();\n            lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);\n            searchOrbView.setLayoutParams(lp);\n        }\n    }\n\n    void setEntranceTransitionStartState() {\n        setHeadersOnScreen(false);\n        setSearchOrbViewOnScreen(false);\n        // NOTE that mMainFragmentAdapter.setEntranceTransitionState(false) will be called\n        // in onEntranceTransitionPrepare() because mMainFragmentAdapter is still the dummy\n        // one when setEntranceTransitionStartState() is called.\n    }\n\n    void setEntranceTransitionEndState() {\n        setHeadersOnScreen(mShowingHeaders);\n        setSearchOrbViewOnScreen(true);\n        mMainFragmentAdapter.setEntranceTransitionState(true);\n    }\n\n    private class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener {\n\n        private final View mView;\n        private final Runnable mCallback;\n        private int mState;\n        private MainFragmentAdapter mainFragmentAdapter;\n\n        final static int STATE_INIT = 0;\n        final static int STATE_FIRST_DRAW = 1;\n        final static int STATE_SECOND_DRAW = 2;\n\n        ExpandPreLayout(Runnable callback, MainFragmentAdapter adapter, View view) {\n            mView = view;\n            mCallback = callback;\n            mainFragmentAdapter = adapter;\n        }\n\n        void execute() {\n            mView.getViewTreeObserver().addOnPreDrawListener(this);\n            mainFragmentAdapter.setExpand(false);\n            // always trigger onPreDraw even adapter setExpand() does nothing.\n            mView.invalidate();\n            mState = STATE_INIT;\n        }\n\n        @Override\n        public boolean onPreDraw() {\n            if (getView() == null || getContext() == null) {\n                mView.getViewTreeObserver().removeOnPreDrawListener(this);\n                return true;\n            }\n            if (mState == STATE_INIT) {\n                mainFragmentAdapter.setExpand(true);\n                // always trigger onPreDraw even adapter setExpand() does nothing.\n                mView.invalidate();\n                mState = STATE_FIRST_DRAW;\n            } else if (mState == STATE_FIRST_DRAW) {\n                mCallback.run();\n                mView.getViewTreeObserver().removeOnPreDrawListener(this);\n                mState = STATE_SECOND_DRAW;\n            }\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/DetailsBackgroundVideoHelper.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 */\n\npackage androidx.leanback.app;\n\nimport android.animation.Animator;\nimport android.animation.ValueAnimator;\nimport android.graphics.drawable.Drawable;\n\nimport androidx.leanback.media.PlaybackGlue;\nimport androidx.leanback.widget.DetailsParallax;\nimport androidx.leanback.widget.Parallax;\nimport androidx.leanback.widget.ParallaxEffect;\nimport androidx.leanback.widget.ParallaxTarget;\n\n/**\n * Helper class responsible for controlling video playback in {@link DetailsFragment}. This\n * takes {@link DetailsParallax}, {@link PlaybackGlue} and a drawable as input.\n * Video is played when {@link DetailsParallax#getOverviewRowTop()} moved bellow top edge of screen.\n * Video is stopped when {@link DetailsParallax#getOverviewRowTop()} reaches or scrolls above top\n * edge of screen. The drawable will change alpha to 0 when video is ready to play.\n * App does not directly use this class.\n * @see DetailsFragmentBackgroundController\n * @see DetailsSupportFragmentBackgroundController\n */\nfinal class DetailsBackgroundVideoHelper {\n    private static final long BACKGROUND_CROSS_FADE_DURATION = 500;\n    // Temporarily add CROSSFADE_DELAY waiting for video surface ready.\n    // We will remove this delay once PlaybackGlue have a callback for videoRenderingReady event.\n    private static final long CROSSFADE_DELAY = 1000;\n\n    /**\n     * Different states {@link DetailsFragment} can be in.\n     */\n    static final int INITIAL = 0;\n    static final int PLAY_VIDEO = 1;\n    static final int NO_VIDEO = 2;\n\n    private final DetailsParallax mDetailsParallax;\n    private ParallaxEffect mParallaxEffect;\n\n    private int mCurrentState = INITIAL;\n\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    ValueAnimator mBackgroundAnimator;\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    Drawable mBackgroundDrawable;\n    private PlaybackGlue mPlaybackGlue;\n    private boolean mBackgroundDrawableVisible;\n\n    /**\n     * Constructor to setup a Helper for controlling video playback in DetailsFragment.\n     * @param playbackGlue The PlaybackGlue used to control underlying player.\n     * @param detailsParallax The DetailsParallax to add special parallax effect to control video\n     *                        start/stop. Video is played when\n     *                        {@link DetailsParallax#getOverviewRowTop()} moved bellow top edge of\n     *                        screen. Video is stopped when\n     *                        {@link DetailsParallax#getOverviewRowTop()} reaches or scrolls above\n     *                        top edge of screen.\n     * @param backgroundDrawable The drawable will change alpha to 0 when video is ready to play.\n     */\n    DetailsBackgroundVideoHelper(\n            PlaybackGlue playbackGlue,\n            DetailsParallax detailsParallax,\n            Drawable backgroundDrawable) {\n        this.mPlaybackGlue = playbackGlue;\n        this.mDetailsParallax = detailsParallax;\n        this.mBackgroundDrawable = backgroundDrawable;\n        mBackgroundDrawableVisible = true;\n        mBackgroundDrawable.setAlpha(255);\n        startParallax();\n    }\n\n    void startParallax() {\n        if (mParallaxEffect != null) {\n            return;\n        }\n        Parallax.IntProperty frameTop = mDetailsParallax.getOverviewRowTop();\n        final float maxFrameTop = 1f;\n        final float minFrameTop = 0f;\n        mParallaxEffect = mDetailsParallax\n                .addEffect(frameTop.atFraction(maxFrameTop), frameTop.atFraction(minFrameTop))\n                .target(new ParallaxTarget() {\n                    @Override\n                    public void update(float fraction) {\n                        if (fraction == maxFrameTop) {\n                            updateState(NO_VIDEO);\n                        } else {\n                            updateState(PLAY_VIDEO);\n                        }\n                    }\n                });\n        // In case the VideoHelper is created after RecyclerView is created: perform initial\n        // parallax effect.\n        mDetailsParallax.updateValues();\n    }\n\n    void stopParallax() {\n        mDetailsParallax.removeEffect(mParallaxEffect);\n    }\n\n    boolean isVideoVisible() {\n        return mCurrentState == PLAY_VIDEO;\n    }\n\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    void updateState(int state) {\n        if (state == mCurrentState) {\n            return;\n        }\n        mCurrentState = state;\n        applyState();\n    }\n\n    private void applyState() {\n        switch (mCurrentState) {\n            case PLAY_VIDEO:\n                if (mPlaybackGlue != null) {\n                    if (mPlaybackGlue.isPrepared()) {\n                        internalStartPlayback();\n                    } else {\n                        mPlaybackGlue.addPlayerCallback(mControlStateCallback);\n                    }\n                } else {\n                    crossFadeBackgroundToVideo(false);\n                }\n                break;\n            case NO_VIDEO:\n                crossFadeBackgroundToVideo(false);\n                if (mPlaybackGlue != null) {\n                    mPlaybackGlue.removePlayerCallback(mControlStateCallback);\n                    mPlaybackGlue.pause();\n                }\n                break;\n        }\n    }\n\n    void setPlaybackGlue(PlaybackGlue playbackGlue) {\n        if (mPlaybackGlue != null) {\n            mPlaybackGlue.removePlayerCallback(mControlStateCallback);\n        }\n        mPlaybackGlue = playbackGlue;\n        applyState();\n    }\n\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    void internalStartPlayback() {\n        if (mPlaybackGlue != null) {\n            mPlaybackGlue.play();\n        }\n        mDetailsParallax.getRecyclerView().postDelayed(new Runnable() {\n            @Override\n            public void run() {\n                crossFadeBackgroundToVideo(true);\n            }\n        }, CROSSFADE_DELAY);\n    }\n\n    void crossFadeBackgroundToVideo(boolean crossFadeToVideo) {\n        crossFadeBackgroundToVideo(crossFadeToVideo, false);\n    }\n\n    void crossFadeBackgroundToVideo(boolean crossFadeToVideo, boolean immediate) {\n        final boolean newVisible = !crossFadeToVideo;\n        if (mBackgroundDrawableVisible == newVisible) {\n            if (immediate) {\n                if (mBackgroundAnimator != null) {\n                    mBackgroundAnimator.cancel();\n                    mBackgroundAnimator = null;\n                }\n                if (mBackgroundDrawable != null) {\n                    mBackgroundDrawable.setAlpha(crossFadeToVideo ? 0 : 255);\n                    return;\n                }\n            }\n            return;\n        }\n        mBackgroundDrawableVisible = newVisible;\n        if (mBackgroundAnimator != null) {\n            mBackgroundAnimator.cancel();\n            mBackgroundAnimator = null;\n        }\n\n        float startAlpha = crossFadeToVideo ? 1f : 0f;\n        float endAlpha = crossFadeToVideo ? 0f : 1f;\n\n        if (mBackgroundDrawable == null) {\n            return;\n        }\n        if (immediate) {\n            mBackgroundDrawable.setAlpha(crossFadeToVideo ? 0 : 255);\n            return;\n        }\n        mBackgroundAnimator = ValueAnimator.ofFloat(startAlpha, endAlpha);\n        mBackgroundAnimator.setDuration(BACKGROUND_CROSS_FADE_DURATION);\n        mBackgroundAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator valueAnimator) {\n                mBackgroundDrawable.setAlpha(\n                        (int) ((Float) (valueAnimator.getAnimatedValue()) * 255));\n            }\n        });\n\n        mBackgroundAnimator.addListener(new Animator.AnimatorListener() {\n            @Override\n            public void onAnimationStart(Animator animator) {\n            }\n\n            @Override\n            public void onAnimationEnd(Animator animator) {\n                mBackgroundAnimator = null;\n            }\n\n            @Override\n            public void onAnimationCancel(Animator animator) {\n            }\n\n            @Override\n            public void onAnimationRepeat(Animator animator) {\n            }\n        });\n\n        mBackgroundAnimator.start();\n    }\n\n    private class PlaybackControlStateCallback extends PlaybackGlue.PlayerCallback {\n        PlaybackControlStateCallback() {\n        }\n\n        @Override\n        public void onPreparedStateChanged(PlaybackGlue glue) {\n            if (glue.isPrepared()) {\n                internalStartPlayback();\n            }\n        }\n    }\n\n    PlaybackControlStateCallback mControlStateCallback = new PlaybackControlStateCallback();\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/DetailsFragmentBackgroundController.java",
    "content": "// CHECKSTYLE:OFF Generated code\n/* This file is auto-generated from {}DetailsSupportFragmentBackgroundController.java.  DO NOT MODIFY. */\n\n/*\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 androidx.leanback.app;\n\nimport android.animation.PropertyValuesHolder;\nimport android.graphics.Bitmap;\nimport android.graphics.Color;\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.Drawable;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport android.app.Fragment;\nimport androidx.leanback.R;\nimport androidx.leanback.graphics.FitWidthBitmapDrawable;\nimport androidx.leanback.media.PlaybackGlue;\nimport androidx.leanback.media.PlaybackGlueHost;\nimport androidx.leanback.widget.DetailsParallaxDrawable;\nimport androidx.leanback.widget.ParallaxTarget;\n\n/**\n * Controller for DetailsFragment parallax background and embedded video play.\n * <p>\n * The parallax background drawable is made of two parts: cover drawable (by default\n * {@link FitWidthBitmapDrawable}) above the details overview row and bottom drawable (by default\n * {@link ColorDrawable}) below the details overview row. While vertically scrolling rows, the size\n * of cover drawable and bottom drawable will be updated and the cover drawable will by default\n * perform a parallax shift using {@link FitWidthBitmapDrawable#PROPERTY_VERTICAL_OFFSET}.\n * </p>\n * <pre>\n *        ***************************\n *        *      Cover Drawable     *\n *        * (FitWidthBitmapDrawable)*\n *        *                         *\n *        ***************************\n *        *    DetailsOverviewRow   *\n *        *                         *\n *        ***************************\n *        *     Bottom Drawable     *\n *        *      (ColorDrawable)    *\n *        *         Related         *\n *        *         Content         *\n *        ***************************\n * </pre>\n * Both parallax background drawable and embedded video play are optional. App must call\n * {@link #enableParallax()} and/or {@link #setupVideoPlayback(PlaybackGlue)} explicitly.\n * The PlaybackGlue is automatically {@link PlaybackGlue#play()} when fragment starts and\n * {@link PlaybackGlue#pause()} when fragment stops. When video is ready to play, cover drawable\n * will be faded out.\n * Example:\n * <pre>\n * DetailsFragmentBackgroundController mController = new DetailsFragmentBackgroundController(this);\n *\n * public void onCreate(Bundle savedInstance) {\n *     super.onCreate(savedInstance);\n *     MediaPlayerGlue player = new MediaPlayerGlue(..);\n *     player.setUrl(...);\n *     mController.enableParallax();\n *     mController.setupVideoPlayback(player);\n * }\n *\n * static class MyLoadBitmapTask extends ... {\n *     WeakReference<MyFragment> mFragmentRef;\n *     MyLoadBitmapTask(MyFragment fragment) {\n *         mFragmentRef = new WeakReference(fragment);\n *     }\n *     protected void onPostExecute(Bitmap bitmap) {\n *         MyFragment fragment = mFragmentRef.get();\n *         if (fragment != null) {\n *             fragment.mController.setCoverBitmap(bitmap);\n *         }\n *     }\n * }\n *\n * public void onStart() {\n *     new MyLoadBitmapTask(this).execute(url);\n * }\n *\n * public void onStop() {\n *     mController.setCoverBitmap(null);\n * }\n * </pre>\n * <p>\n * To customize cover drawable and/or bottom drawable, app should call\n * {@link #enableParallax(Drawable, Drawable, ParallaxTarget.PropertyValuesHolderTarget)}.\n * If app supplies a custom cover Drawable, it should not call {@link #setCoverBitmap(Bitmap)}.\n * If app supplies a custom bottom Drawable, it should not call {@link #setSolidColor(int)}.\n * </p>\n * <p>\n * To customize playback fragment, app should override {@link #onCreateVideoFragment()} and\n * {@link #onCreateGlueHost()}.\n * </p>\n *\n * @deprecated use {@link DetailsSupportFragmentBackgroundController}\n */\n@Deprecated\npublic class DetailsFragmentBackgroundController {\n\n    final DetailsFragment mFragment;\n    DetailsParallaxDrawable mParallaxDrawable;\n    int mParallaxDrawableMaxOffset;\n    PlaybackGlue mPlaybackGlue;\n    DetailsBackgroundVideoHelper mVideoHelper;\n    Bitmap mCoverBitmap;\n    int mSolidColor;\n    boolean mCanUseHost = false;\n    boolean mInitialControlVisible = false;\n\n    private Fragment mLastVideoFragmentForGlueHost;\n\n    /**\n     * Creates a DetailsFragmentBackgroundController for a DetailsFragment. Note that\n     * each DetailsFragment can only associate with one DetailsFragmentBackgroundController.\n     *\n     * @param fragment The DetailsFragment to control background and embedded video playing.\n     * @throws IllegalStateException If fragment was already associated with another controller.\n     */\n    public DetailsFragmentBackgroundController(DetailsFragment fragment) {\n        if (fragment.mDetailsBackgroundController != null) {\n            throw new IllegalStateException(\"Each DetailsFragment is allowed to initialize \"\n                    + \"DetailsFragmentBackgroundController once\");\n        }\n        fragment.mDetailsBackgroundController = this;\n        mFragment = fragment;\n    }\n\n    /**\n     * Enables default parallax background using a {@link FitWidthBitmapDrawable} as cover drawable\n     * and {@link ColorDrawable} as bottom drawable. A vertical parallax movement will be applied\n     * to the FitWidthBitmapDrawable. App may use {@link #setSolidColor(int)} and\n     * {@link #setCoverBitmap(Bitmap)} to change the content of bottom drawable and cover drawable.\n     * This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.\n     *\n     * @see #setCoverBitmap(Bitmap)\n     * @see #setSolidColor(int)\n     * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.\n     */\n    public void enableParallax() {\n        int offset = mParallaxDrawableMaxOffset;\n        if (offset == 0) {\n            offset = FragmentUtil.getContext(mFragment).getResources()\n                    .getDimensionPixelSize(R.dimen.lb_details_cover_drawable_parallax_movement);\n        }\n        Drawable coverDrawable = new FitWidthBitmapDrawable();\n        ColorDrawable colorDrawable = new ColorDrawable();\n        enableParallax(coverDrawable, colorDrawable,\n                new ParallaxTarget.PropertyValuesHolderTarget(\n                        coverDrawable,\n                        PropertyValuesHolder.ofInt(FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,\n                                0, -offset)\n                ));\n    }\n\n    /**\n     * Enables parallax background using a custom cover drawable at top and a custom bottom\n     * drawable. This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.\n     *\n     * @param coverDrawable Custom cover drawable shown at top. {@link #setCoverBitmap(Bitmap)}\n     *                      will not work if coverDrawable is not {@link FitWidthBitmapDrawable};\n     *                      in that case it's app's responsibility to set content into\n     *                      coverDrawable.\n     * @param bottomDrawable Drawable shown at bottom. {@link #setSolidColor(int)} will not work\n     *                       if bottomDrawable is not {@link ColorDrawable}; in that case it's app's\n     *                       responsibility to set content of bottomDrawable.\n     * @param coverDrawableParallaxTarget Target to perform parallax effect within coverDrawable.\n     *                                    Use null for no parallax movement effect.\n     *                                    Example to move bitmap within FitWidthBitmapDrawable:\n     *                                    new ParallaxTarget.PropertyValuesHolderTarget(\n     *                                        coverDrawable, PropertyValuesHolder.ofInt(\n     *                                            FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,\n     *                                            0, -120))\n     * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.\n     */\n    public void enableParallax(@NonNull Drawable coverDrawable, @NonNull Drawable bottomDrawable,\n                               @Nullable ParallaxTarget.PropertyValuesHolderTarget\n                                       coverDrawableParallaxTarget) {\n        if (mParallaxDrawable != null) {\n            return;\n        }\n        // if bitmap is set before enableParallax, use it as initial value.\n        if (mCoverBitmap != null && coverDrawable instanceof FitWidthBitmapDrawable) {\n            ((FitWidthBitmapDrawable) coverDrawable).setBitmap(mCoverBitmap);\n        }\n        // if solid color is set before enableParallax, use it as initial value.\n        if (mSolidColor != Color.TRANSPARENT && bottomDrawable instanceof ColorDrawable) {\n            ((ColorDrawable) bottomDrawable).setColor(mSolidColor);\n        }\n        if (mPlaybackGlue != null) {\n            throw new IllegalStateException(\"enableParallaxDrawable must be called before \"\n                    + \"enableVideoPlayback\");\n        }\n        mParallaxDrawable = new DetailsParallaxDrawable(\n                FragmentUtil.getContext(mFragment),\n                mFragment.getParallax(),\n                coverDrawable,\n                bottomDrawable,\n                coverDrawableParallaxTarget);\n        mFragment.setBackgroundDrawable(mParallaxDrawable);\n        // create a VideoHelper with null PlaybackGlue for changing CoverDrawable visibility\n        // before PlaybackGlue is ready.\n        mVideoHelper = new DetailsBackgroundVideoHelper(null,\n                mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());\n    }\n\n    /**\n     * Enable video playback and set proper {@link PlaybackGlueHost}. This method by default\n     * creates a VideoFragment and VideoFragmentGlueHost to host the PlaybackGlue.\n     * This method must be called after calling details Fragment super.onCreate(). This method\n     * can be called multiple times to replace existing PlaybackGlue or calling\n     * setupVideoPlayback(null) to clear. Note a typical {@link PlaybackGlue} subclass releases\n     * resources in {@link PlaybackGlue#onDetachedFromHost()}, when the {@link PlaybackGlue}\n     * subclass is not doing that, it's app's responsibility to release the resources.\n     *\n     * @param playbackGlue The new PlaybackGlue to set as background or null to clear existing one.\n     * @see #onCreateVideoFragment()\n     * @see #onCreateGlueHost().\n     */\n    @SuppressWarnings(\"ReferenceEquality\")\n    public void setupVideoPlayback(@NonNull PlaybackGlue playbackGlue) {\n        if (mPlaybackGlue == playbackGlue) {\n            return;\n        }\n\n        PlaybackGlueHost playbackGlueHost = null;\n        if (mPlaybackGlue != null) {\n            playbackGlueHost = mPlaybackGlue.getHost();\n            mPlaybackGlue.setHost(null);\n        }\n\n        mPlaybackGlue = playbackGlue;\n        mVideoHelper.setPlaybackGlue(mPlaybackGlue);\n        if (mCanUseHost && mPlaybackGlue != null) {\n            if (playbackGlueHost == null\n                    || mLastVideoFragmentForGlueHost != findOrCreateVideoFragment()) {\n                mPlaybackGlue.setHost(createGlueHost());\n                mLastVideoFragmentForGlueHost = findOrCreateVideoFragment();\n            } else {\n                mPlaybackGlue.setHost(playbackGlueHost);\n            }\n        }\n    }\n\n    /**\n     * Returns current PlaybackGlue or null if not set or cleared.\n     *\n     * @return Current PlaybackGlue or null\n     */\n    public final PlaybackGlue getPlaybackGlue() {\n        return mPlaybackGlue;\n    }\n\n    /**\n     * Precondition allows user navigate to video fragment using DPAD. Default implementation\n     * returns true if PlaybackGlue is not null. Subclass may override, e.g. only allow navigation\n     * when {@link PlaybackGlue#isPrepared()} is true. Note this method does not block\n     * app calls {@link #switchToVideo}.\n     *\n     * @return True allow to navigate to video fragment.\n     */\n    public boolean canNavigateToVideoFragment() {\n        return mPlaybackGlue != null;\n    }\n\n    void switchToVideoBeforeCreate() {\n        mVideoHelper.crossFadeBackgroundToVideo(true, true);\n        mInitialControlVisible = true;\n    }\n\n    /**\n     * Switch to video fragment, note that this method is not affected by result of\n     * {@link #canNavigateToVideoFragment()}. If the method is called in DetailsFragment.onCreate()\n     * it will make video fragment to be initially focused once it is created.\n     * <p>\n     * Calling switchToVideo() in DetailsFragment.onCreate() will clear the activity enter\n     * transition and shared element transition.\n     * </p>\n     * <p>\n     * If switchToVideo() is called after {@link DetailsFragment#prepareEntranceTransition()} and\n     * before {@link DetailsFragment#onEntranceTransitionEnd()}, it will be ignored.\n     * </p>\n     * <p>\n     * If {@link DetailsFragment#prepareEntranceTransition()} is called after switchToVideo(), an\n     * IllegalStateException will be thrown.\n     * </p>\n     */\n    public final void switchToVideo() {\n        mFragment.switchToVideo();\n    }\n\n    /**\n     * Switch to rows fragment.\n     */\n    public final void switchToRows() {\n        mFragment.switchToRows();\n    }\n\n    /**\n     * When fragment is started and no running transition. First set host if not yet set, second\n     * start playing if it was paused before.\n     */\n    void onStart() {\n        if (!mCanUseHost) {\n            mCanUseHost = true;\n            if (mPlaybackGlue != null) {\n                mPlaybackGlue.setHost(createGlueHost());\n                mLastVideoFragmentForGlueHost = findOrCreateVideoFragment();\n            }\n        }\n        if (mPlaybackGlue != null && mPlaybackGlue.isPrepared()) {\n            mPlaybackGlue.play();\n        }\n    }\n\n    void onStop() {\n        if (mPlaybackGlue != null) {\n            mPlaybackGlue.pause();\n        }\n    }\n\n    /**\n     * Disable parallax that would auto-start video playback\n     * @return true if video fragment is visible or false otherwise.\n     */\n    boolean disableVideoParallax() {\n        if (mVideoHelper != null) {\n            mVideoHelper.stopParallax();\n            return mVideoHelper.isVideoVisible();\n        }\n        return false;\n    }\n\n    /**\n     * Returns the cover drawable at top. Returns null if {@link #enableParallax()} is not called.\n     * By default it's a {@link FitWidthBitmapDrawable}.\n     *\n     * @return The cover drawable at top.\n     */\n    public final Drawable getCoverDrawable() {\n        if (mParallaxDrawable == null) {\n            return null;\n        }\n        return mParallaxDrawable.getCoverDrawable();\n    }\n\n    /**\n     * Returns the drawable at bottom. Returns null if {@link #enableParallax()} is not called.\n     * By default it's a {@link ColorDrawable}.\n     *\n     * @return The bottom drawable.\n     */\n    public final Drawable getBottomDrawable() {\n        if (mParallaxDrawable == null) {\n            return null;\n        }\n        return mParallaxDrawable.getBottomDrawable();\n    }\n\n    /**\n     * Creates a Fragment to host {@link PlaybackGlue}. Returns a new {@link VideoFragment} by\n     * default. App may override and return a different fragment and it also must override\n     * {@link #onCreateGlueHost()}.\n     *\n     * @return A new fragment used in {@link #onCreateGlueHost()}.\n     * @see #onCreateGlueHost()\n     * @see #setupVideoPlayback(PlaybackGlue)\n     */\n    public Fragment onCreateVideoFragment() {\n        return new VideoFragment();\n    }\n\n    /**\n     * Creates a PlaybackGlueHost to host PlaybackGlue. App may override this if it overrides\n     * {@link #onCreateVideoFragment()}. This method must be called after calling Fragment\n     * super.onCreate(). When override this method, app may call\n     * {@link #findOrCreateVideoFragment()} to get or create a fragment.\n     *\n     * @return A new PlaybackGlueHost to host PlaybackGlue.\n     * @see #onCreateVideoFragment()\n     * @see #findOrCreateVideoFragment()\n     * @see #setupVideoPlayback(PlaybackGlue)\n     */\n    public PlaybackGlueHost onCreateGlueHost() {\n        return new VideoFragmentGlueHost((VideoFragment) findOrCreateVideoFragment());\n    }\n\n    PlaybackGlueHost createGlueHost() {\n        PlaybackGlueHost host = onCreateGlueHost();\n        if (mInitialControlVisible) {\n            host.showControlsOverlay(false);\n        } else {\n            host.hideControlsOverlay(false);\n        }\n        return host;\n    }\n\n    /**\n     * Adds or gets fragment for rendering video in DetailsFragment. A subclass that\n     * overrides {@link #onCreateGlueHost()} should call this method to get a fragment for creating\n     * a {@link PlaybackGlueHost}.\n     *\n     * @return Fragment the added or restored fragment responsible for rendering video.\n     * @see #onCreateGlueHost()\n     */\n    public final Fragment findOrCreateVideoFragment() {\n        return mFragment.findOrCreateVideoFragment();\n    }\n\n    /**\n     * Convenient method to set Bitmap in cover drawable. If app is not using default\n     * {@link FitWidthBitmapDrawable}, app should not use this method  It's safe to call\n     * setCoverBitmap() before calling {@link #enableParallax()}.\n     *\n     * @param bitmap bitmap to set as cover.\n     */\n    public final void setCoverBitmap(Bitmap bitmap) {\n        mCoverBitmap = bitmap;\n        Drawable drawable = getCoverDrawable();\n        if (drawable instanceof FitWidthBitmapDrawable) {\n            ((FitWidthBitmapDrawable) drawable).setBitmap(mCoverBitmap);\n        }\n    }\n\n    /**\n     * Returns Bitmap set by {@link #setCoverBitmap(Bitmap)}.\n     *\n     * @return Bitmap for cover drawable.\n     */\n    public final Bitmap getCoverBitmap() {\n        return mCoverBitmap;\n    }\n\n    /**\n     * Returns color set by {@link #setSolidColor(int)}.\n     *\n     * @return Solid color used for bottom drawable.\n     */\n    public final @ColorInt int getSolidColor() {\n        return mSolidColor;\n    }\n\n    /**\n     * Convenient method to set color in bottom drawable. If app is not using default\n     * {@link ColorDrawable}, app should not use this method. It's safe to call setSolidColor()\n     * before calling {@link #enableParallax()}.\n     *\n     * @param color color for bottom drawable.\n     */\n    public final void setSolidColor(@ColorInt int color) {\n        mSolidColor = color;\n        Drawable bottomDrawable = getBottomDrawable();\n        if (bottomDrawable instanceof ColorDrawable) {\n            ((ColorDrawable) bottomDrawable).setColor(color);\n        }\n    }\n\n    /**\n     * Sets default parallax offset in pixels for bitmap moving vertically. This method must\n     * be called before {@link #enableParallax()}.\n     *\n     * @param offset Offset in pixels (e.g. 120).\n     * @see #enableParallax()\n     */\n    public final void setParallaxDrawableMaxOffset(int offset) {\n        if (mParallaxDrawable != null) {\n            throw new IllegalStateException(\"enableParallax already called\");\n        }\n        mParallaxDrawableMaxOffset = offset;\n    }\n\n    /**\n     * Returns Default parallax offset in pixels for bitmap moving vertically.\n     * When 0, a default value would be used.\n     *\n     * @return Default parallax offset in pixels for bitmap moving vertically.\n     * @see #enableParallax()\n     */\n    public final int getParallaxDrawableMaxOffset() {\n        return mParallaxDrawableMaxOffset;\n    }\n\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/DetailsSupportFragmentBackgroundController.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 androidx.leanback.app;\n\nimport android.animation.PropertyValuesHolder;\nimport android.graphics.Bitmap;\nimport android.graphics.Color;\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.Drawable;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.Fragment;\nimport androidx.leanback.R;\nimport androidx.leanback.graphics.FitWidthBitmapDrawable;\nimport androidx.leanback.media.PlaybackGlue;\nimport androidx.leanback.media.PlaybackGlueHost;\nimport androidx.leanback.widget.DetailsParallaxDrawable;\nimport androidx.leanback.widget.ParallaxTarget;\n\n/**\n * Controller for DetailsSupportFragment parallax background and embedded video play.\n * <p>\n * The parallax background drawable is made of two parts: cover drawable (by default\n * {@link FitWidthBitmapDrawable}) above the details overview row and bottom drawable (by default\n * {@link ColorDrawable}) below the details overview row. While vertically scrolling rows, the size\n * of cover drawable and bottom drawable will be updated and the cover drawable will by default\n * perform a parallax shift using {@link FitWidthBitmapDrawable#PROPERTY_VERTICAL_OFFSET}.\n * </p>\n * <pre>\n *        ***************************\n *        *      Cover Drawable     *\n *        * (FitWidthBitmapDrawable)*\n *        *                         *\n *        ***************************\n *        *    DetailsOverviewRow   *\n *        *                         *\n *        ***************************\n *        *     Bottom Drawable     *\n *        *      (ColorDrawable)    *\n *        *         Related         *\n *        *         Content         *\n *        ***************************\n * </pre>\n * Both parallax background drawable and embedded video play are optional. App must call\n * {@link #enableParallax()} and/or {@link #setupVideoPlayback(PlaybackGlue)} explicitly.\n * The PlaybackGlue is automatically {@link PlaybackGlue#play()} when fragment starts and\n * {@link PlaybackGlue#pause()} when fragment stops. When video is ready to play, cover drawable\n * will be faded out.\n * Example:\n * <pre>\n * DetailsSupportFragmentBackgroundController mController = new DetailsSupportFragmentBackgroundController(this);\n *\n * public void onCreate(Bundle savedInstance) {\n *     super.onCreate(savedInstance);\n *     MediaPlayerGlue player = new MediaPlayerGlue(..);\n *     player.setUrl(...);\n *     mController.enableParallax();\n *     mController.setupVideoPlayback(player);\n * }\n *\n * static class MyLoadBitmapTask extends ... {\n *     WeakReference<MyFragment> mFragmentRef;\n *     MyLoadBitmapTask(MyFragment fragment) {\n *         mFragmentRef = new WeakReference(fragment);\n *     }\n *     protected void onPostExecute(Bitmap bitmap) {\n *         MyFragment fragment = mFragmentRef.get();\n *         if (fragment != null) {\n *             fragment.mController.setCoverBitmap(bitmap);\n *         }\n *     }\n * }\n *\n * public void onStart() {\n *     new MyLoadBitmapTask(this).execute(url);\n * }\n *\n * public void onStop() {\n *     mController.setCoverBitmap(null);\n * }\n * </pre>\n * <p>\n * To customize cover drawable and/or bottom drawable, app should call\n * {@link #enableParallax(Drawable, Drawable, ParallaxTarget.PropertyValuesHolderTarget)}.\n * If app supplies a custom cover Drawable, it should not call {@link #setCoverBitmap(Bitmap)}.\n * If app supplies a custom bottom Drawable, it should not call {@link #setSolidColor(int)}.\n * </p>\n * <p>\n * To customize playback fragment, app should override {@link #onCreateVideoSupportFragment()} and\n * {@link #onCreateGlueHost()}.\n * </p>\n *\n */\npublic class DetailsSupportFragmentBackgroundController {\n\n    final DetailsSupportFragment mFragment;\n    DetailsParallaxDrawable mParallaxDrawable;\n    int mParallaxDrawableMaxOffset;\n    PlaybackGlue mPlaybackGlue;\n    DetailsBackgroundVideoHelper mVideoHelper;\n    Bitmap mCoverBitmap;\n    int mSolidColor;\n    boolean mCanUseHost = false;\n    boolean mInitialControlVisible = false;\n\n    private Fragment mLastVideoSupportFragmentForGlueHost;\n\n    /**\n     * Creates a DetailsSupportFragmentBackgroundController for a DetailsSupportFragment. Note that\n     * each DetailsSupportFragment can only associate with one DetailsSupportFragmentBackgroundController.\n     *\n     * @param fragment The DetailsSupportFragment to control background and embedded video playing.\n     * @throws IllegalStateException If fragment was already associated with another controller.\n     */\n    public DetailsSupportFragmentBackgroundController(DetailsSupportFragment fragment) {\n        if (fragment.mDetailsBackgroundController != null) {\n            throw new IllegalStateException(\"Each DetailsSupportFragment is allowed to initialize \"\n                    + \"DetailsSupportFragmentBackgroundController once\");\n        }\n        fragment.mDetailsBackgroundController = this;\n        mFragment = fragment;\n    }\n\n    /**\n     * Enables default parallax background using a {@link FitWidthBitmapDrawable} as cover drawable\n     * and {@link ColorDrawable} as bottom drawable. A vertical parallax movement will be applied\n     * to the FitWidthBitmapDrawable. App may use {@link #setSolidColor(int)} and\n     * {@link #setCoverBitmap(Bitmap)} to change the content of bottom drawable and cover drawable.\n     * This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.\n     *\n     * @see #setCoverBitmap(Bitmap)\n     * @see #setSolidColor(int)\n     * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.\n     */\n    public void enableParallax() {\n        int offset = mParallaxDrawableMaxOffset;\n        if (offset == 0) {\n            offset = mFragment.getContext().getResources()\n                    .getDimensionPixelSize(R.dimen.lb_details_cover_drawable_parallax_movement);\n        }\n        Drawable coverDrawable = new FitWidthBitmapDrawable();\n        ColorDrawable colorDrawable = new ColorDrawable();\n        enableParallax(coverDrawable, colorDrawable,\n                new ParallaxTarget.PropertyValuesHolderTarget(\n                        coverDrawable,\n                        PropertyValuesHolder.ofInt(FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,\n                                0, -offset)\n                ));\n    }\n\n    /**\n     * Enables parallax background using a custom cover drawable at top and a custom bottom\n     * drawable. This method must be called before {@link #setupVideoPlayback(PlaybackGlue)}.\n     *\n     * @param coverDrawable Custom cover drawable shown at top. {@link #setCoverBitmap(Bitmap)}\n     *                      will not work if coverDrawable is not {@link FitWidthBitmapDrawable};\n     *                      in that case it's app's responsibility to set content into\n     *                      coverDrawable.\n     * @param bottomDrawable Drawable shown at bottom. {@link #setSolidColor(int)} will not work\n     *                       if bottomDrawable is not {@link ColorDrawable}; in that case it's app's\n     *                       responsibility to set content of bottomDrawable.\n     * @param coverDrawableParallaxTarget Target to perform parallax effect within coverDrawable.\n     *                                    Use null for no parallax movement effect.\n     *                                    Example to move bitmap within FitWidthBitmapDrawable:\n     *                                    new ParallaxTarget.PropertyValuesHolderTarget(\n     *                                        coverDrawable, PropertyValuesHolder.ofInt(\n     *                                            FitWidthBitmapDrawable.PROPERTY_VERTICAL_OFFSET,\n     *                                            0, -120))\n     * @throws IllegalStateException If {@link #setupVideoPlayback(PlaybackGlue)} was called.\n     */\n    public void enableParallax(@NonNull Drawable coverDrawable, @NonNull Drawable bottomDrawable,\n                               @Nullable ParallaxTarget.PropertyValuesHolderTarget\n                                       coverDrawableParallaxTarget) {\n        if (mParallaxDrawable != null) {\n            return;\n        }\n        // if bitmap is set before enableParallax, use it as initial value.\n        if (mCoverBitmap != null && coverDrawable instanceof FitWidthBitmapDrawable) {\n            ((FitWidthBitmapDrawable) coverDrawable).setBitmap(mCoverBitmap);\n        }\n        // if solid color is set before enableParallax, use it as initial value.\n        if (mSolidColor != Color.TRANSPARENT && bottomDrawable instanceof ColorDrawable) {\n            ((ColorDrawable) bottomDrawable).setColor(mSolidColor);\n        }\n        if (mPlaybackGlue != null) {\n            throw new IllegalStateException(\"enableParallaxDrawable must be called before \"\n                    + \"enableVideoPlayback\");\n        }\n        mParallaxDrawable = new DetailsParallaxDrawable(\n                mFragment.getContext(),\n                mFragment.getParallax(),\n                coverDrawable,\n                bottomDrawable,\n                coverDrawableParallaxTarget);\n        mFragment.setBackgroundDrawable(mParallaxDrawable);\n        // create a VideoHelper with null PlaybackGlue for changing CoverDrawable visibility\n        // before PlaybackGlue is ready.\n        mVideoHelper = new DetailsBackgroundVideoHelper(null,\n                mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());\n    }\n\n    /**\n     * Enable video playback and set proper {@link PlaybackGlueHost}. This method by default\n     * creates a VideoSupportFragment and VideoSupportFragmentGlueHost to host the PlaybackGlue.\n     * This method must be called after calling details Fragment super.onCreate(). This method\n     * can be called multiple times to replace existing PlaybackGlue or calling\n     * setupVideoPlayback(null) to clear. Note a typical {@link PlaybackGlue} subclass releases\n     * resources in {@link PlaybackGlue#onDetachedFromHost()}, when the {@link PlaybackGlue}\n     * subclass is not doing that, it's app's responsibility to release the resources.\n     *\n     * @param playbackGlue The new PlaybackGlue to set as background or null to clear existing one.\n     * @see #onCreateVideoSupportFragment()\n     * @see #onCreateGlueHost().\n     */\n    @SuppressWarnings(\"ReferenceEquality\")\n    public void setupVideoPlayback(@NonNull PlaybackGlue playbackGlue) {\n        if (mPlaybackGlue == playbackGlue) {\n            return;\n        }\n\n        PlaybackGlueHost playbackGlueHost = null;\n        if (mPlaybackGlue != null) {\n            playbackGlueHost = mPlaybackGlue.getHost();\n            mPlaybackGlue.setHost(null);\n        }\n\n        mPlaybackGlue = playbackGlue;\n        mVideoHelper.setPlaybackGlue(mPlaybackGlue);\n        if (mCanUseHost && mPlaybackGlue != null) {\n            if (playbackGlueHost == null\n                    || mLastVideoSupportFragmentForGlueHost != findOrCreateVideoSupportFragment()) {\n                mPlaybackGlue.setHost(createGlueHost());\n                mLastVideoSupportFragmentForGlueHost = findOrCreateVideoSupportFragment();\n            } else {\n                mPlaybackGlue.setHost(playbackGlueHost);\n            }\n        }\n    }\n\n    /**\n     * Returns current PlaybackGlue or null if not set or cleared.\n     *\n     * @return Current PlaybackGlue or null\n     */\n    public final PlaybackGlue getPlaybackGlue() {\n        return mPlaybackGlue;\n    }\n\n    /**\n     * Precondition allows user navigate to video fragment using DPAD. Default implementation\n     * returns true if PlaybackGlue is not null. Subclass may override, e.g. only allow navigation\n     * when {@link PlaybackGlue#isPrepared()} is true. Note this method does not block\n     * app calls {@link #switchToVideo}.\n     *\n     * @return True allow to navigate to video fragment.\n     */\n    public boolean canNavigateToVideoSupportFragment() {\n        return mPlaybackGlue != null;\n    }\n\n    void switchToVideoBeforeCreate() {\n        mVideoHelper.crossFadeBackgroundToVideo(true, true);\n        mInitialControlVisible = true;\n    }\n\n    /**\n     * Switch to video fragment, note that this method is not affected by result of\n     * {@link #canNavigateToVideoSupportFragment()}. If the method is called in DetailsSupportFragment.onCreate()\n     * it will make video fragment to be initially focused once it is created.\n     * <p>\n     * Calling switchToVideo() in DetailsSupportFragment.onCreate() will clear the activity enter\n     * transition and shared element transition.\n     * </p>\n     * <p>\n     * If switchToVideo() is called after {@link DetailsSupportFragment#prepareEntranceTransition()} and\n     * before {@link DetailsSupportFragment#onEntranceTransitionEnd()}, it will be ignored.\n     * </p>\n     * <p>\n     * If {@link DetailsSupportFragment#prepareEntranceTransition()} is called after switchToVideo(), an\n     * IllegalStateException will be thrown.\n     * </p>\n     */\n    public final void switchToVideo() {\n        mFragment.switchToVideo();\n    }\n\n    /**\n     * Switch to rows fragment.\n     */\n    public final void switchToRows() {\n        mFragment.switchToRows();\n    }\n\n    /**\n     * When fragment is started and no running transition. First set host if not yet set, second\n     * start playing if it was paused before.\n     */\n    void onStart() {\n        if (!mCanUseHost) {\n            mCanUseHost = true;\n            if (mPlaybackGlue != null) {\n                mPlaybackGlue.setHost(createGlueHost());\n                mLastVideoSupportFragmentForGlueHost = findOrCreateVideoSupportFragment();\n            }\n        }\n        if (mPlaybackGlue != null && mPlaybackGlue.isPrepared()) {\n            mPlaybackGlue.play();\n        }\n    }\n\n    void onStop() {\n        if (mPlaybackGlue != null) {\n            mPlaybackGlue.pause();\n        }\n    }\n\n    /**\n     * Disable parallax that would auto-start video playback\n     * @return true if video fragment is visible or false otherwise.\n     */\n    boolean disableVideoParallax() {\n        if (mVideoHelper != null) {\n            mVideoHelper.stopParallax();\n            return mVideoHelper.isVideoVisible();\n        }\n        return false;\n    }\n\n    /**\n     * Returns the cover drawable at top. Returns null if {@link #enableParallax()} is not called.\n     * By default it's a {@link FitWidthBitmapDrawable}.\n     *\n     * @return The cover drawable at top.\n     */\n    public final Drawable getCoverDrawable() {\n        if (mParallaxDrawable == null) {\n            return null;\n        }\n        return mParallaxDrawable.getCoverDrawable();\n    }\n\n    /**\n     * Returns the drawable at bottom. Returns null if {@link #enableParallax()} is not called.\n     * By default it's a {@link ColorDrawable}.\n     *\n     * @return The bottom drawable.\n     */\n    public final Drawable getBottomDrawable() {\n        if (mParallaxDrawable == null) {\n            return null;\n        }\n        return mParallaxDrawable.getBottomDrawable();\n    }\n\n    /**\n     * Creates a Fragment to host {@link PlaybackGlue}. Returns a new {@link VideoSupportFragment} by\n     * default. App may override and return a different fragment and it also must override\n     * {@link #onCreateGlueHost()}.\n     *\n     * @return A new fragment used in {@link #onCreateGlueHost()}.\n     * @see #onCreateGlueHost()\n     * @see #setupVideoPlayback(PlaybackGlue)\n     */\n    public Fragment onCreateVideoSupportFragment() {\n        return new VideoSupportFragment();\n    }\n\n    /**\n     * Creates a PlaybackGlueHost to host PlaybackGlue. App may override this if it overrides\n     * {@link #onCreateVideoSupportFragment()}. This method must be called after calling Fragment\n     * super.onCreate(). When override this method, app may call\n     * {@link #findOrCreateVideoSupportFragment()} to get or create a fragment.\n     *\n     * @return A new PlaybackGlueHost to host PlaybackGlue.\n     * @see #onCreateVideoSupportFragment()\n     * @see #findOrCreateVideoSupportFragment()\n     * @see #setupVideoPlayback(PlaybackGlue)\n     */\n    public PlaybackGlueHost onCreateGlueHost() {\n        return new VideoSupportFragmentGlueHost((VideoSupportFragment) findOrCreateVideoSupportFragment());\n    }\n\n    PlaybackGlueHost createGlueHost() {\n        PlaybackGlueHost host = onCreateGlueHost();\n        if (mInitialControlVisible) {\n            host.showControlsOverlay(false);\n        } else {\n            host.hideControlsOverlay(false);\n        }\n        return host;\n    }\n\n    /**\n     * Adds or gets fragment for rendering video in DetailsSupportFragment. A subclass that\n     * overrides {@link #onCreateGlueHost()} should call this method to get a fragment for creating\n     * a {@link PlaybackGlueHost}.\n     *\n     * @return Fragment the added or restored fragment responsible for rendering video.\n     * @see #onCreateGlueHost()\n     */\n    public final Fragment findOrCreateVideoSupportFragment() {\n        return mFragment.findOrCreateVideoSupportFragment();\n    }\n\n    /**\n     * Convenient method to set Bitmap in cover drawable. If app is not using default\n     * {@link FitWidthBitmapDrawable}, app should not use this method  It's safe to call\n     * setCoverBitmap() before calling {@link #enableParallax()}.\n     *\n     * @param bitmap bitmap to set as cover.\n     */\n    public final void setCoverBitmap(Bitmap bitmap) {\n        mCoverBitmap = bitmap;\n        Drawable drawable = getCoverDrawable();\n        if (drawable instanceof FitWidthBitmapDrawable) {\n            ((FitWidthBitmapDrawable) drawable).setBitmap(mCoverBitmap);\n        }\n    }\n\n    /**\n     * Returns Bitmap set by {@link #setCoverBitmap(Bitmap)}.\n     *\n     * @return Bitmap for cover drawable.\n     */\n    public final Bitmap getCoverBitmap() {\n        return mCoverBitmap;\n    }\n\n    /**\n     * Returns color set by {@link #setSolidColor(int)}.\n     *\n     * @return Solid color used for bottom drawable.\n     */\n    public final @ColorInt int getSolidColor() {\n        return mSolidColor;\n    }\n\n    /**\n     * Convenient method to set color in bottom drawable. If app is not using default\n     * {@link ColorDrawable}, app should not use this method. It's safe to call setSolidColor()\n     * before calling {@link #enableParallax()}.\n     *\n     * @param color color for bottom drawable.\n     */\n    public final void setSolidColor(@ColorInt int color) {\n        mSolidColor = color;\n        Drawable bottomDrawable = getBottomDrawable();\n        if (bottomDrawable instanceof ColorDrawable) {\n            ((ColorDrawable) bottomDrawable).setColor(color);\n        }\n    }\n\n    /**\n     * Sets default parallax offset in pixels for bitmap moving vertically. This method must\n     * be called before {@link #enableParallax()}.\n     *\n     * @param offset Offset in pixels (e.g. 120).\n     * @see #enableParallax()\n     */\n    public final void setParallaxDrawableMaxOffset(int offset) {\n        if (mParallaxDrawable != null) {\n            throw new IllegalStateException(\"enableParallax already called\");\n        }\n        mParallaxDrawableMaxOffset = offset;\n    }\n\n    /**\n     * Returns Default parallax offset in pixels for bitmap moving vertically.\n     * When 0, a default value would be used.\n     *\n     * @return Default parallax offset in pixels for bitmap moving vertically.\n     * @see #enableParallax()\n     */\n    public final int getParallaxDrawableMaxOffset() {\n        return mParallaxDrawableMaxOffset;\n    }\n\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/ErrorFragment.java",
    "content": "// CHECKSTYLE:OFF Generated code\n/* This file is auto-generated from ErrorSupportFragment.java.  DO NOT MODIFY. */\n\n/*\n * Copyright (C) 2014 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.app;\n\nimport android.graphics.Paint;\nimport android.graphics.Paint.FontMetricsInt;\nimport android.graphics.PixelFormat;\nimport android.graphics.drawable.Drawable;\nimport android.os.Bundle;\nimport android.text.TextUtils;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.Button;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.leanback.R;\n\n/**\n * A fragment for displaying an error indication.\n * @deprecated use {@link ErrorSupportFragment}\n */\n@Deprecated\npublic class ErrorFragment extends BrandedFragment {\n\n    private ViewGroup mErrorFrame;\n    private ImageView mImageView;\n    private TextView mTextView;\n    private Button mButton;\n    private Drawable mDrawable;\n    private CharSequence mMessage;\n    private String mButtonText;\n    private View.OnClickListener mButtonClickListener;\n    private Drawable mBackgroundDrawable;\n    private boolean mIsBackgroundTranslucent = true;\n\n    /**\n     * Sets the default background.\n     *\n     * @param translucent True to set a translucent background.\n     */\n    public void setDefaultBackground(boolean translucent) {\n        mBackgroundDrawable = null;\n        mIsBackgroundTranslucent = translucent;\n        updateBackground();\n        updateMessage();\n    }\n\n    /**\n     * Returns true if the background is translucent.\n     */\n    public boolean isBackgroundTranslucent() {\n        return mIsBackgroundTranslucent;\n    }\n\n    /**\n     * Sets a drawable for the fragment background.\n     *\n     * @param drawable The drawable used for the background.\n     */\n    public void setBackgroundDrawable(Drawable drawable) {\n        mBackgroundDrawable = drawable;\n        if (drawable != null) {\n            final int opacity = drawable.getOpacity();\n            mIsBackgroundTranslucent = (opacity == PixelFormat.TRANSLUCENT\n                    || opacity == PixelFormat.TRANSPARENT);\n        }\n        updateBackground();\n        updateMessage();\n    }\n\n    /**\n     * Returns the background drawable.  May be null if a default is used.\n     */\n    public Drawable getBackgroundDrawable() {\n        return mBackgroundDrawable;\n    }\n\n    /**\n     * Sets the drawable to be used for the error image.\n     *\n     * @param drawable The drawable used for the error image.\n     */\n    public void setImageDrawable(Drawable drawable) {\n        mDrawable = drawable;\n        updateImageDrawable();\n    }\n\n    /**\n     * Returns the drawable used for the error image.\n     */\n    public Drawable getImageDrawable() {\n        return mDrawable;\n    }\n\n    /**\n     * Sets the error message.\n     *\n     * @param message The error message.\n     */\n    public void setMessage(CharSequence message) {\n        mMessage = message;\n        updateMessage();\n    }\n\n    /**\n     * Returns the error message.\n     */\n    public CharSequence getMessage() {\n        return mMessage;\n    }\n\n    /**\n     * Sets the button text.\n     *\n     * @param text The button text.\n     */\n    public void setButtonText(String text) {\n        mButtonText = text;\n        updateButton();\n    }\n\n    /**\n     * Returns the button text.\n     */\n    public String getButtonText() {\n        return mButtonText;\n    }\n\n    /**\n     * Set the button click listener.\n     *\n     * @param clickListener The click listener for the button.\n     */\n    public void setButtonClickListener(View.OnClickListener clickListener) {\n        mButtonClickListener = clickListener;\n        updateButton();\n    }\n\n    /**\n     * Returns the button click listener.\n     */\n    public View.OnClickListener getButtonClickListener() {\n        return mButtonClickListener;\n    }\n\n    @Override\n    public View onCreateView(LayoutInflater inflater, ViewGroup container,\n            Bundle savedInstanceState) {\n        View root = inflater.inflate(R.layout.lb_error_fragment, container, false);\n\n        mErrorFrame = (ViewGroup) root.findViewById(R.id.error_frame);\n        updateBackground();\n\n        installTitleView(inflater, mErrorFrame, savedInstanceState);\n\n        mImageView = (ImageView) root.findViewById(R.id.image);\n        updateImageDrawable();\n\n        mTextView = (TextView) root.findViewById(R.id.message);\n        updateMessage();\n\n        mButton = (Button) root.findViewById(R.id.button);\n        updateButton();\n\n        FontMetricsInt metrics = getFontMetricsInt(mTextView);\n        int underImageBaselineMargin = container.getResources().getDimensionPixelSize(\n                R.dimen.lb_error_under_image_baseline_margin);\n        setTopMargin(mTextView, underImageBaselineMargin + metrics.ascent);\n\n        int underMessageBaselineMargin = container.getResources().getDimensionPixelSize(\n                R.dimen.lb_error_under_message_baseline_margin);\n        setTopMargin(mButton, underMessageBaselineMargin - metrics.descent);\n\n        return root;\n    }\n\n    private void updateBackground() {\n        if (mErrorFrame != null) {\n            if (mBackgroundDrawable != null) {\n                mErrorFrame.setBackground(mBackgroundDrawable);\n            } else {\n                mErrorFrame.setBackgroundColor(mErrorFrame.getResources().getColor(\n                        mIsBackgroundTranslucent\n                                ? R.color.lb_error_background_color_translucent\n                                : R.color.lb_error_background_color_opaque));\n            }\n        }\n    }\n\n    private void updateMessage() {\n        if (mTextView != null) {\n            mTextView.setText(mMessage);\n            mTextView.setVisibility(TextUtils.isEmpty(mMessage) ? View.GONE : View.VISIBLE);\n        }\n    }\n\n    private void updateImageDrawable() {\n        if (mImageView != null) {\n            mImageView.setImageDrawable(mDrawable);\n            mImageView.setVisibility(mDrawable == null ? View.GONE : View.VISIBLE);\n        }\n    }\n\n    private void updateButton() {\n        if (mButton != null) {\n            mButton.setText(mButtonText);\n            mButton.setOnClickListener(mButtonClickListener);\n            mButton.setVisibility(TextUtils.isEmpty(mButtonText) ? View.GONE : View.VISIBLE);\n            mButton.requestFocus();\n        }\n    }\n\n    @Override\n    public void onStart() {\n        super.onStart();\n        mErrorFrame.requestFocus();\n    }\n\n    private static FontMetricsInt getFontMetricsInt(TextView textView) {\n        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);\n        paint.setTextSize(textView.getTextSize());\n        paint.setTypeface(textView.getTypeface());\n        return paint.getFontMetricsInt();\n    }\n\n    private static void setTopMargin(TextView textView, int topMargin) {\n        ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) textView.getLayoutParams();\n        lp.topMargin = topMargin;\n        textView.setLayoutParams(lp);\n    }\n\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/FragmentUtil.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 androidx.leanback.app;\n\nimport android.app.Fragment;\nimport android.content.Context;\nimport android.os.Build;\n\nclass FragmentUtil {\n    static Context getContext(Fragment fragment) {\n        if (Build.VERSION.SDK_INT >= 23) {\n            return fragment.getContext();\n        }\n        return fragment.getActivity();\n    }\n\n    private FragmentUtil() {\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/HeadersFragment.java",
    "content": "// CHECKSTYLE:OFF Generated code\n/* This file is auto-generated from HeadersSupportFragment.java.  DO NOT MODIFY. */\n\n/*\n * Copyright (C) 2014 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\n\npackage androidx.leanback.app;\n\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.GradientDrawable;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.view.View.OnLayoutChangeListener;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.leanback.R;\nimport androidx.leanback.widget.ClassPresenterSelector;\nimport androidx.leanback.widget.DividerPresenter;\nimport androidx.leanback.widget.DividerRow;\nimport androidx.leanback.widget.FocusHighlightHelper;\nimport androidx.leanback.widget.HorizontalGridView;\nimport androidx.leanback.widget.ItemBridgeAdapter;\nimport androidx.leanback.widget.PresenterSelector;\nimport androidx.leanback.widget.Row;\nimport androidx.leanback.widget.RowHeaderPresenter;\nimport androidx.leanback.widget.SectionRow;\nimport androidx.leanback.widget.VerticalGridView;\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * An fragment containing a list of row headers. Implementation must support three types of rows:\n * <ul>\n *     <li>{@link DividerRow} rendered by {@link DividerPresenter}.</li>\n *     <li>{@link Row} rendered by {@link RowHeaderPresenter}.</li>\n *     <li>{@link SectionRow} rendered by {@link RowHeaderPresenter}.</li>\n * </ul>\n * Use {@link #setPresenterSelector(PresenterSelector)} in subclass constructor to customize\n * Presenters. App may override {@link BrowseFragment#onCreateHeadersFragment()}.\n * @deprecated use {@link HeadersSupportFragment}\n */\n@Deprecated\npublic class HeadersFragment extends BaseRowFragment {\n\n    /**\n     * Interface definition for a callback to be invoked when a header item is clicked.\n     * @deprecated use {@link HeadersSupportFragment}\n     */\n    @Deprecated\n    public interface OnHeaderClickedListener {\n        /**\n         * Called when a header item has been clicked.\n         *\n         * @param viewHolder Row ViewHolder object corresponding to the selected Header.\n         * @param row Row object corresponding to the selected Header.\n         */\n        void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row);\n    }\n\n    /**\n     * Interface definition for a callback to be invoked when a header item is selected.\n     * @deprecated use {@link HeadersSupportFragment}\n     */\n    @Deprecated\n    public interface OnHeaderViewSelectedListener {\n        /**\n         * Called when a header item has been selected.\n         *\n         * @param viewHolder Row ViewHolder object corresponding to the selected Header.\n         * @param row Row object corresponding to the selected Header.\n         */\n        void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row);\n    }\n\n    private OnHeaderViewSelectedListener mOnHeaderViewSelectedListener;\n    OnHeaderClickedListener mOnHeaderClickedListener;\n    private boolean mHeadersEnabled = true;\n    private boolean mHeadersGone = false;\n    private int mBackgroundColor;\n    private boolean mBackgroundColorSet;\n\n    private static final PresenterSelector sHeaderPresenter = new ClassPresenterSelector()\n            .addClassPresenter(DividerRow.class, new DividerPresenter())\n            .addClassPresenter(SectionRow.class,\n                    new RowHeaderPresenter(R.layout.lb_section_header, false))\n            .addClassPresenter(Row.class, new RowHeaderPresenter(R.layout.lb_header));\n\n    public HeadersFragment() {\n        setPresenterSelector(sHeaderPresenter);\n        FocusHighlightHelper.setupHeaderItemFocusHighlight(getBridgeAdapter());\n    }\n\n    public void setOnHeaderClickedListener(OnHeaderClickedListener listener) {\n        mOnHeaderClickedListener = listener;\n    }\n\n    public void setOnHeaderViewSelectedListener(OnHeaderViewSelectedListener listener) {\n        mOnHeaderViewSelectedListener = listener;\n    }\n\n    @Override\n    VerticalGridView findGridViewFromRoot(View view) {\n        return (VerticalGridView) view.findViewById(R.id.browse_headers);\n    }\n\n    @Override\n    void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder viewHolder,\n            int position, int subposition) {\n        if (mOnHeaderViewSelectedListener != null) {\n            if (viewHolder != null && position >= 0) {\n                ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) viewHolder;\n                mOnHeaderViewSelectedListener.onHeaderSelected(\n                        (RowHeaderPresenter.ViewHolder) vh.getViewHolder(), (Row) vh.getItem());\n            } else {\n                mOnHeaderViewSelectedListener.onHeaderSelected(null, null);\n            }\n        }\n    }\n\n    private final ItemBridgeAdapter.AdapterListener mAdapterListener =\n            new ItemBridgeAdapter.AdapterListener() {\n        @Override\n        public void onCreate(final ItemBridgeAdapter.ViewHolder viewHolder) {\n            View headerView = viewHolder.getViewHolder().view;\n            headerView.setOnClickListener(new View.OnClickListener() {\n                @Override\n                public void onClick(View v) {\n                    if (mOnHeaderClickedListener != null) {\n                        mOnHeaderClickedListener.onHeaderClicked(\n                                (RowHeaderPresenter.ViewHolder) viewHolder.getViewHolder(),\n                                (Row) viewHolder.getItem());\n                    }\n                }\n            });\n            if (mWrapper != null) {\n                viewHolder.itemView.addOnLayoutChangeListener(sLayoutChangeListener);\n            } else {\n                headerView.addOnLayoutChangeListener(sLayoutChangeListener);\n            }\n        }\n\n    };\n\n    static OnLayoutChangeListener sLayoutChangeListener = new OnLayoutChangeListener() {\n        @Override\n        public void onLayoutChange(View v, int left, int top, int right, int bottom,\n            int oldLeft, int oldTop, int oldRight, int oldBottom) {\n            v.setPivotX(v.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? v.getWidth() : 0);\n            v.setPivotY(v.getMeasuredHeight() / 2);\n        }\n    };\n\n    @Override\n    int getLayoutResourceId() {\n        return R.layout.lb_headers_fragment;\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n        final VerticalGridView listView = getVerticalGridView();\n        if (listView == null) {\n            return;\n        }\n        if (mBackgroundColorSet) {\n            listView.setBackgroundColor(mBackgroundColor);\n            updateFadingEdgeToBrandColor(mBackgroundColor);\n        } else {\n            Drawable d = listView.getBackground();\n            if (d instanceof ColorDrawable) {\n                updateFadingEdgeToBrandColor(((ColorDrawable) d).getColor());\n            }\n        }\n        updateListViewVisibility();\n    }\n\n    private void updateListViewVisibility() {\n        final VerticalGridView listView = getVerticalGridView();\n        if (listView != null) {\n            getView().setVisibility(mHeadersGone ? View.GONE : View.VISIBLE);\n            if (!mHeadersGone) {\n                if (mHeadersEnabled) {\n                    listView.setChildrenVisibility(View.VISIBLE);\n                } else {\n                    listView.setChildrenVisibility(View.INVISIBLE);\n                }\n            }\n        }\n    }\n\n    void setHeadersEnabled(boolean enabled) {\n        mHeadersEnabled = enabled;\n        updateListViewVisibility();\n    }\n\n    void setHeadersGone(boolean gone) {\n        mHeadersGone = gone;\n        updateListViewVisibility();\n    }\n\n    static class NoOverlappingFrameLayout extends FrameLayout {\n\n        public NoOverlappingFrameLayout(Context context) {\n            super(context);\n        }\n\n        /**\n         * Avoid creating hardware layer for header dock.\n         */\n        @Override\n        public boolean hasOverlappingRendering() {\n            return false;\n        }\n    }\n\n    // Wrapper needed because of conflict between RecyclerView's use of alpha\n    // for ADD animations, and RowHeaderPresenter's use of alpha for selected level.\n    final ItemBridgeAdapter.Wrapper mWrapper = new ItemBridgeAdapter.Wrapper() {\n        @Override\n        public void wrap(View wrapper, View wrapped) {\n            ((FrameLayout) wrapper).addView(wrapped);\n        }\n\n        @Override\n        public View createWrapper(View root) {\n            return new NoOverlappingFrameLayout(root.getContext());\n        }\n    };\n    @Override\n    void updateAdapter() {\n        super.updateAdapter();\n        ItemBridgeAdapter adapter = getBridgeAdapter();\n        adapter.setAdapterListener(mAdapterListener);\n        adapter.setWrapper(mWrapper);\n    }\n\n    void setBackgroundColor(int color) {\n        mBackgroundColor = color;\n        mBackgroundColorSet = true;\n\n        if (getVerticalGridView() != null) {\n            getVerticalGridView().setBackgroundColor(mBackgroundColor);\n            updateFadingEdgeToBrandColor(mBackgroundColor);\n        }\n    }\n\n    private void updateFadingEdgeToBrandColor(int backgroundColor) {\n        View fadingView = getView().findViewById(R.id.fade_out_edge);\n        Drawable background = fadingView.getBackground();\n        if (background instanceof GradientDrawable) {\n            background.mutate();\n            ((GradientDrawable) background).setColors(\n                    new int[] {Color.TRANSPARENT, backgroundColor});\n        }\n    }\n\n    @Override\n    public void onTransitionStart() {\n        super.onTransitionStart();\n        if (!mHeadersEnabled) {\n            // When enabling headers fragment,  the RowHeaderView gets a focus but\n            // isShown() is still false because its parent is INVISIBLE, accessibility\n            // event is not sent.\n            // Workaround is: prevent focus to a child view during transition and put\n            // focus on it after transition is done.\n            final VerticalGridView listView = getVerticalGridView();\n            if (listView != null) {\n                listView.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);\n                if (listView.hasFocus()) {\n                    listView.requestFocus();\n                }\n            }\n        }\n    }\n\n    @Override\n    public void onTransitionEnd() {\n        if (mHeadersEnabled) {\n            final VerticalGridView listView = getVerticalGridView();\n            if (listView != null) {\n                listView.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);\n                if (listView.hasFocus()) {\n                    listView.requestFocus();\n                }\n            }\n        }\n        super.onTransitionEnd();\n    }\n\n    public boolean isScrolling() {\n        return getVerticalGridView().getScrollState()\n                != HorizontalGridView.SCROLL_STATE_IDLE;\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/OnboardingFragment.java",
    "content": "// CHECKSTYLE:OFF Generated code\n/* This file is auto-generated from OnboardingSupportFragment.java.  DO NOT MODIFY. */\n\n/*\n * Copyright (C) 2015 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS 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 androidx.leanback.app;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorInflater;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.AnimatorSet;\nimport android.animation.ObjectAnimator;\nimport android.animation.TimeInterpolator;\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.os.Bundle;\nimport android.util.Log;\nimport android.util.TypedValue;\nimport android.view.ContextThemeWrapper;\nimport android.view.Gravity;\nimport android.view.KeyEvent;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.View.OnClickListener;\nimport android.view.View.OnKeyListener;\nimport android.view.ViewGroup;\nimport android.view.ViewTreeObserver.OnPreDrawListener;\nimport android.view.animation.AccelerateInterpolator;\nimport android.view.animation.DecelerateInterpolator;\nimport android.widget.Button;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport android.app.Fragment;\nimport androidx.leanback.R;\nimport androidx.leanback.widget.PagingIndicator;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * An OnboardingFragment provides a common and simple way to build onboarding screen for\n * applications.\n * <p>\n * <h3>Building the screen</h3>\n * The view structure of onboarding screen is composed of the common parts and custom parts. The\n * common parts are composed of icon, title, description and page navigator and the custom parts\n * are composed of background, contents and foreground.\n * <p>\n * To build the screen views, the inherited class should override:\n * <ul>\n * <li>{@link #onCreateBackgroundView} to provide the background view. Background view has the same\n * size as the screen and the lowest z-order.</li>\n * <li>{@link #onCreateContentView} to provide the contents view. The content view is located in\n * the content area at the center of the screen.</li>\n * <li>{@link #onCreateForegroundView} to provide the foreground view. Foreground view has the same\n * size as the screen and the highest z-order</li>\n * </ul>\n * <p>\n * Each of these methods can return {@code null} if the application doesn't want to provide it.\n * <p>\n * <h3>Page information</h3>\n * The onboarding screen may have several pages which explain the functionality of the application.\n * The inherited class should provide the page information by overriding the methods:\n * <p>\n * <ul>\n * <li>{@link #getPageCount} to provide the number of pages.</li>\n * <li>{@link #getPageTitle} to provide the title of the page.</li>\n * <li>{@link #getPageDescription} to provide the description of the page.</li>\n * </ul>\n * <p>\n * Note that the information is used in {@link #onCreateView}, so should be initialized before\n * calling {@code super.onCreateView}.\n * <p>\n * <h3>Animation</h3>\n * Onboarding screen has three kinds of animations:\n * <p>\n * <h4>Logo Splash Animation</a></h4>\n * When onboarding screen appears, the logo splash animation is played by default. The animation\n * fades in the logo image, pauses in a few seconds and fades it out.\n * <p>\n * In most cases, the logo animation needs to be customized because the logo images of applications\n * are different from each other, or some applications may want to show their own animations.\n * <p>\n * The logo animation can be customized in two ways:\n * <ul>\n * <li>The simplest way is to provide the logo image by calling {@link #setLogoResourceId} to show\n * the default logo animation. This method should be called in {@link Fragment#onCreateView}.</li>\n * <li>If the logo animation is complex, then override {@link #onCreateLogoAnimation} and return the\n * {@link Animator} object to run.</li>\n * </ul>\n * <p>\n * If the inherited class provides neither the logo image nor the animation, the logo animation will\n * be omitted.\n * <h4>Page enter animation</h4>\n * After logo animation finishes, page enter animation starts, which causes the header section -\n * title and description views to fade and slide in. Users can override the default\n * fade + slide animation by overriding {@link #onCreateTitleAnimator()} &\n * {@link #onCreateDescriptionAnimator()}. By default we don't animate the custom views but users\n * can provide animation by overriding {@link #onCreateEnterAnimation}.\n *\n * <h4>Page change animation</h4>\n * When the page changes, the default animations of the title and description are played. The\n * inherited class can override {@link #onPageChanged} to start the custom animations.\n * <p>\n * <h3>Finishing the screen</h3>\n * <p>\n * If the user finishes the onboarding screen after navigating all the pages,\n * {@link #onFinishFragment} is called. The inherited class can override this method to show another\n * fragment or activity, or just remove this fragment.\n * <p>\n * <h3>Theming</h3>\n * <p>\n * OnboardingFragment must have access to an appropriate theme. Specifically, the fragment must\n * receive  {@link R.style#Theme_Leanback_Onboarding}, or a theme whose parent is set to that theme.\n * Themes can be provided in one of three ways:\n * <ul>\n * <li>The simplest way is to set the theme for the host Activity to the Onboarding theme or a theme\n * that derives from it.</li>\n * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the\n * existing Activity theme can have an entry added for the attribute\n * {@link R.styleable#LeanbackOnboardingTheme_onboardingTheme}. If present, this theme will be used\n * by OnboardingFragment as an overlay to the Activity's theme.</li>\n * <li>Finally, custom subclasses of OnboardingFragment may provide a theme through the\n * {@link #onProvideTheme} method. This can be useful if a subclass is used across multiple\n * Activities.</li>\n * </ul>\n * <p>\n * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by\n * the Activity's theme. (Themes whose parent theme is already set to the onboarding theme do not\n * need to set the onboardingTheme attribute; if set, it will be ignored.)\n *\n * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTheme\n * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingHeaderStyle\n * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTitleStyle\n * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingDescriptionStyle\n * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingNavigatorContainerStyle\n * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingPageIndicatorStyle\n * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle\n * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingLogoStyle\n * @deprecated use {@link OnboardingSupportFragment}\n */\n@Deprecated\nabstract public class OnboardingFragment extends Fragment {\n    private static final String TAG = \"OnboardingF\";\n    private static final boolean DEBUG = false;\n\n    private static final long LOGO_SPLASH_PAUSE_DURATION_MS = 1333;\n\n    private static final long HEADER_ANIMATION_DURATION_MS = 417;\n    private static final long DESCRIPTION_START_DELAY_MS = 33;\n    private static final long HEADER_APPEAR_DELAY_MS = 500;\n    private static final int SLIDE_DISTANCE = 60;\n\n    private static int sSlideDistance;\n\n    private static final TimeInterpolator HEADER_APPEAR_INTERPOLATOR = new DecelerateInterpolator();\n    private static final TimeInterpolator HEADER_DISAPPEAR_INTERPOLATOR =\n            new AccelerateInterpolator();\n\n    // Keys used to save and restore the states.\n    private static final String KEY_CURRENT_PAGE_INDEX = \"leanback.onboarding.current_page_index\";\n    private static final String KEY_LOGO_ANIMATION_FINISHED =\n            \"leanback.onboarding.logo_animation_finished\";\n    private static final String KEY_ENTER_ANIMATION_FINISHED =\n            \"leanback.onboarding.enter_animation_finished\";\n\n    private ContextThemeWrapper mThemeWrapper;\n\n    PagingIndicator mPageIndicator;\n    View mStartButton;\n    private ImageView mLogoView;\n    // Optional icon that can be displayed on top of the header section.\n    private ImageView mMainIconView;\n    private int mIconResourceId;\n\n    TextView mTitleView;\n    TextView mDescriptionView;\n\n    boolean mIsLtr;\n\n    // No need to save/restore the logo resource ID, because the logo animation will not appear when\n    // the fragment is restored.\n    private int mLogoResourceId;\n    boolean mLogoAnimationFinished;\n    boolean mEnterAnimationFinished;\n    int mCurrentPageIndex;\n\n    @ColorInt\n    private int mTitleViewTextColor = Color.TRANSPARENT;\n    private boolean mTitleViewTextColorSet;\n\n    @ColorInt\n    private int mDescriptionViewTextColor = Color.TRANSPARENT;\n    private boolean mDescriptionViewTextColorSet;\n\n    @ColorInt\n    private int mDotBackgroundColor = Color.TRANSPARENT;\n    private boolean mDotBackgroundColorSet;\n\n    @ColorInt\n    private int mArrowColor = Color.TRANSPARENT;\n    private boolean mArrowColorSet;\n\n    @ColorInt\n    private int mArrowBackgroundColor = Color.TRANSPARENT;\n    private boolean mArrowBackgroundColorSet;\n\n    private CharSequence mStartButtonText;\n    private boolean mStartButtonTextSet;\n\n\n    private AnimatorSet mAnimator;\n\n    private final OnClickListener mOnClickListener = new OnClickListener() {\n        @Override\n        public void onClick(View view) {\n            if (!mLogoAnimationFinished) {\n                // Do not change page until the enter transition finishes.\n                return;\n            }\n            if (mCurrentPageIndex == getPageCount() - 1) {\n                onFinishFragment();\n            } else {\n                moveToNextPage();\n            }\n        }\n    };\n\n    private final OnKeyListener mOnKeyListener = new OnKeyListener() {\n        @Override\n        public boolean onKey(View v, int keyCode, KeyEvent event) {\n            if (!mLogoAnimationFinished) {\n                // Ignore key event until the enter transition finishes.\n                return keyCode != KeyEvent.KEYCODE_BACK;\n            }\n            if (event.getAction() == KeyEvent.ACTION_DOWN) {\n                return false;\n            }\n            switch (keyCode) {\n                case KeyEvent.KEYCODE_BACK:\n                    if (mCurrentPageIndex == 0) {\n                        return false;\n                    }\n                    moveToPreviousPage();\n                    return true;\n                case KeyEvent.KEYCODE_DPAD_LEFT:\n                    if (mIsLtr) {\n                        moveToPreviousPage();\n                    } else {\n                        moveToNextPage();\n                    }\n                    return true;\n                case KeyEvent.KEYCODE_DPAD_RIGHT:\n                    if (mIsLtr) {\n                        moveToNextPage();\n                    } else {\n                        moveToPreviousPage();\n                    }\n                    return true;\n            }\n            return false;\n        }\n    };\n\n    /**\n     * Navigates to the previous page.\n     */\n    protected void moveToPreviousPage() {\n        if (!mLogoAnimationFinished) {\n            // Ignore if the logo enter transition is in progress.\n            return;\n        }\n        if (mCurrentPageIndex > 0) {\n            --mCurrentPageIndex;\n            onPageChangedInternal(mCurrentPageIndex + 1);\n        }\n    }\n\n    /**\n     * Navigates to the next page.\n     */\n    protected void moveToNextPage() {\n        if (!mLogoAnimationFinished) {\n            // Ignore if the logo enter transition is in progress.\n            return;\n        }\n        if (mCurrentPageIndex < getPageCount() - 1) {\n            ++mCurrentPageIndex;\n            onPageChangedInternal(mCurrentPageIndex - 1);\n        }\n    }\n\n    @Nullable\n    @Override\n    public View onCreateView(LayoutInflater inflater, final ViewGroup container,\n            Bundle savedInstanceState) {\n        resolveTheme();\n        LayoutInflater localInflater = getThemeInflater(inflater);\n        final ViewGroup view = (ViewGroup) localInflater.inflate(R.layout.lb_onboarding_fragment,\n                container, false);\n        mIsLtr = getResources().getConfiguration().getLayoutDirection()\n                == View.LAYOUT_DIRECTION_LTR;\n        mPageIndicator = (PagingIndicator) view.findViewById(R.id.page_indicator);\n        mPageIndicator.setOnClickListener(mOnClickListener);\n        mPageIndicator.setOnKeyListener(mOnKeyListener);\n        mStartButton = view.findViewById(R.id.button_start);\n        mStartButton.setOnClickListener(mOnClickListener);\n        mStartButton.setOnKeyListener(mOnKeyListener);\n        mMainIconView = (ImageView) view.findViewById(R.id.main_icon);\n        mLogoView = (ImageView) view.findViewById(R.id.logo);\n        mTitleView = (TextView) view.findViewById(R.id.title);\n        mDescriptionView = (TextView) view.findViewById(R.id.description);\n\n        if (mTitleViewTextColorSet) {\n            mTitleView.setTextColor(mTitleViewTextColor);\n        }\n        if (mDescriptionViewTextColorSet) {\n            mDescriptionView.setTextColor(mDescriptionViewTextColor);\n        }\n        if (mDotBackgroundColorSet) {\n            mPageIndicator.setDotBackgroundColor(mDotBackgroundColor);\n        }\n        if (mArrowColorSet) {\n            mPageIndicator.setArrowColor(mArrowColor);\n        }\n        if (mArrowBackgroundColorSet) {\n            mPageIndicator.setDotBackgroundColor(mArrowBackgroundColor);\n        }\n        if (mStartButtonTextSet) {\n            ((Button) mStartButton).setText(mStartButtonText);\n        }\n        final Context context = FragmentUtil.getContext(OnboardingFragment.this);\n        if (sSlideDistance == 0) {\n            sSlideDistance = (int) (SLIDE_DISTANCE * context.getResources()\n                    .getDisplayMetrics().scaledDensity);\n        }\n        view.requestFocus();\n        return view;\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n        if (savedInstanceState == null) {\n            mCurrentPageIndex = 0;\n            mLogoAnimationFinished = false;\n            mEnterAnimationFinished = false;\n            mPageIndicator.onPageSelected(0, false);\n            view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {\n                @Override\n                public boolean onPreDraw() {\n                    getView().getViewTreeObserver().removeOnPreDrawListener(this);\n                    if (!startLogoAnimation()) {\n                        mLogoAnimationFinished = true;\n                        onLogoAnimationFinished();\n                    }\n                    return true;\n                }\n            });\n        } else {\n            mCurrentPageIndex = savedInstanceState.getInt(KEY_CURRENT_PAGE_INDEX);\n            mLogoAnimationFinished = savedInstanceState.getBoolean(KEY_LOGO_ANIMATION_FINISHED);\n            mEnterAnimationFinished = savedInstanceState.getBoolean(KEY_ENTER_ANIMATION_FINISHED);\n            if (!mLogoAnimationFinished) {\n                // logo animation wasn't started or was interrupted when the activity was destroyed;\n                // restart it againl\n                if (!startLogoAnimation()) {\n                    mLogoAnimationFinished = true;\n                    onLogoAnimationFinished();\n                }\n            } else {\n                onLogoAnimationFinished();\n            }\n        }\n    }\n\n    @Override\n    public void onSaveInstanceState(Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putInt(KEY_CURRENT_PAGE_INDEX, mCurrentPageIndex);\n        outState.putBoolean(KEY_LOGO_ANIMATION_FINISHED, mLogoAnimationFinished);\n        outState.putBoolean(KEY_ENTER_ANIMATION_FINISHED, mEnterAnimationFinished);\n    }\n\n    /**\n     * Sets the text color for TitleView. If not set, the default textColor set in style\n     * referenced by attr {@link R.attr#onboardingTitleStyle} will be used.\n     * @param color the color to use as the text color for TitleView\n     */\n    public void setTitleViewTextColor(@ColorInt int color) {\n        mTitleViewTextColor = color;\n        mTitleViewTextColorSet = true;\n        if (mTitleView != null) {\n            mTitleView.setTextColor(color);\n        }\n    }\n\n    /**\n     * Returns the text color of TitleView if it's set through\n     * {@link #setTitleViewTextColor(int)}. If no color was set, transparent is returned.\n     */\n    @ColorInt\n    public final int getTitleViewTextColor() {\n        return mTitleViewTextColor;\n    }\n\n    /**\n     * Sets the text color for DescriptionView. If not set, the default textColor set in style\n     * referenced by attr {@link R.attr#onboardingDescriptionStyle} will be used.\n     * @param color the color to use as the text color for DescriptionView\n     */\n    public void setDescriptionViewTextColor(@ColorInt int color) {\n        mDescriptionViewTextColor = color;\n        mDescriptionViewTextColorSet = true;\n        if (mDescriptionView != null) {\n            mDescriptionView.setTextColor(color);\n        }\n    }\n\n    /**\n     * Returns the text color of DescriptionView if it's set through\n     * {@link #setDescriptionViewTextColor(int)}. If no color was set, transparent is returned.\n     */\n    @ColorInt\n    public final int getDescriptionViewTextColor() {\n        return mDescriptionViewTextColor;\n    }\n    /**\n     * Sets the background color of the dots. If not set, the default color from attr\n     * {@link R.styleable#PagingIndicator_dotBgColor} in the theme will be used.\n     * @param color the color to use for dot backgrounds\n     */\n    public void setDotBackgroundColor(@ColorInt int color) {\n        mDotBackgroundColor = color;\n        mDotBackgroundColorSet = true;\n        if (mPageIndicator != null) {\n            mPageIndicator.setDotBackgroundColor(color);\n        }\n    }\n\n    /**\n     * Returns the background color of the dot if it's set through\n     * {@link #setDotBackgroundColor(int)}. If no color was set, transparent is returned.\n     */\n    @ColorInt\n    public final int getDotBackgroundColor() {\n        return mDotBackgroundColor;\n    }\n\n    /**\n     * Sets the color of the arrow. This color will supersede the color set in the theme attribute\n     * {@link R.styleable#PagingIndicator_arrowColor} if provided. If none of these two are set, the\n     * arrow will have its original bitmap color.\n     *\n     * @param color the color to use for arrow background\n     */\n    public void setArrowColor(@ColorInt int color) {\n        mArrowColor = color;\n        mArrowColorSet = true;\n        if (mPageIndicator != null) {\n            mPageIndicator.setArrowColor(color);\n        }\n    }\n\n    /**\n     * Returns the color of the arrow if it's set through\n     * {@link #setArrowColor(int)}. If no color was set, transparent is returned.\n     */\n    @ColorInt\n    public final int getArrowColor() {\n        return mArrowColor;\n    }\n\n    /**\n     * Sets the background color of the arrow. If not set, the default color from attr\n     * {@link R.styleable#PagingIndicator_arrowBgColor} in the theme will be used.\n     * @param color the color to use for arrow background\n     */\n    public void setArrowBackgroundColor(@ColorInt int color) {\n        mArrowBackgroundColor = color;\n        mArrowBackgroundColorSet = true;\n        if (mPageIndicator != null) {\n            mPageIndicator.setArrowBackgroundColor(color);\n        }\n    }\n\n    /**\n     * Returns the background color of the arrow if it's set through\n     * {@link #setArrowBackgroundColor(int)}. If no color was set, transparent is returned.\n     */\n    @ColorInt\n    public final int getArrowBackgroundColor() {\n        return mArrowBackgroundColor;\n    }\n\n    /**\n     * Returns the start button text if it's set through\n     * {@link #setStartButtonText(CharSequence)}}. If no string was set, null is returned.\n     */\n    public final CharSequence getStartButtonText() {\n        return mStartButtonText;\n    }\n\n    /**\n     * Sets the text on the start button text. If not set, the default text set in\n     * {@link R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle} will be used.\n     *\n     * @param text the start button text\n     */\n    public void setStartButtonText(CharSequence text) {\n        mStartButtonText = text;\n        mStartButtonTextSet = true;\n        if (mStartButton != null) {\n            ((Button) mStartButton).setText(mStartButtonText);\n        }\n    }\n\n    /**\n     * Returns the theme used for styling the fragment. The default returns -1, indicating that the\n     * host Activity's theme should be used.\n     *\n     * @return The theme resource ID of the theme to use in this fragment, or -1 to use the host\n     *         Activity's theme.\n     */\n    public int onProvideTheme() {\n        return -1;\n    }\n\n    private void resolveTheme() {\n        final Context context = FragmentUtil.getContext(OnboardingFragment.this);\n        int theme = onProvideTheme();\n        if (theme == -1) {\n            // Look up the onboardingTheme in the activity's currently specified theme. If it\n            // exists, wrap the theme with its value.\n            int resId = R.attr.onboardingTheme;\n            TypedValue typedValue = new TypedValue();\n            boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);\n            if (DEBUG) Log.v(TAG, \"Found onboarding theme reference? \" + found);\n            if (found) {\n                mThemeWrapper = new ContextThemeWrapper(context, typedValue.resourceId);\n            }\n        } else {\n            mThemeWrapper = new ContextThemeWrapper(context, theme);\n        }\n    }\n\n    private LayoutInflater getThemeInflater(LayoutInflater inflater) {\n        return mThemeWrapper == null ? inflater : inflater.cloneInContext(mThemeWrapper);\n    }\n\n    /**\n     * Sets the resource ID of the splash logo image. If the logo resource id set, the default logo\n     * splash animation will be played.\n     *\n     * @param id The resource ID of the logo image.\n     */\n    public final void setLogoResourceId(int id) {\n        mLogoResourceId = id;\n    }\n\n    /**\n     * Returns the resource ID of the splash logo image.\n     *\n     * @return The resource ID of the splash logo image.\n     */\n    public final int getLogoResourceId() {\n        return mLogoResourceId;\n    }\n\n    /**\n     * Called to have the inherited class create its own logo animation.\n     * <p>\n     * This is called only if the logo image resource ID is not set by {@link #setLogoResourceId}.\n     * If this returns {@code null}, the logo animation is skipped.\n     *\n     * @return The {@link Animator} object which runs the logo animation.\n     */\n    @Nullable\n    protected Animator onCreateLogoAnimation() {\n        return null;\n    }\n\n    boolean startLogoAnimation() {\n        final Context context = FragmentUtil.getContext(OnboardingFragment.this);\n        if (context == null) {\n            return false;\n        }\n        Animator animator = null;\n        if (mLogoResourceId != 0) {\n            mLogoView.setVisibility(View.VISIBLE);\n            mLogoView.setImageResource(mLogoResourceId);\n            Animator inAnimator = AnimatorInflater.loadAnimator(context,\n                    R.animator.lb_onboarding_logo_enter);\n            Animator outAnimator = AnimatorInflater.loadAnimator(context,\n                    R.animator.lb_onboarding_logo_exit);\n            outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS);\n            AnimatorSet logoAnimator = new AnimatorSet();\n            logoAnimator.playSequentially(inAnimator, outAnimator);\n            logoAnimator.setTarget(mLogoView);\n            animator = logoAnimator;\n        } else {\n            animator = onCreateLogoAnimation();\n        }\n        if (animator != null) {\n            animator.addListener(new AnimatorListenerAdapter() {\n                @Override\n                public void onAnimationEnd(Animator animation) {\n                    if (context != null) {\n                        mLogoAnimationFinished = true;\n                        onLogoAnimationFinished();\n                    }\n                }\n            });\n            animator.start();\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Called to have the inherited class create its enter animation. The start animation runs after\n     * logo animation ends.\n     *\n     * @return The {@link Animator} object which runs the page enter animation.\n     */\n    @Nullable\n    protected Animator onCreateEnterAnimation() {\n        return null;\n    }\n\n\n    /**\n     * Hides the logo view and makes other fragment views visible. Also initializes the texts for\n     * Title and Description views.\n     */\n    void hideLogoView() {\n        mLogoView.setVisibility(View.GONE);\n\n        if (mIconResourceId != 0) {\n            mMainIconView.setImageResource(mIconResourceId);\n            mMainIconView.setVisibility(View.VISIBLE);\n        }\n\n        View container = getView();\n        // Create custom views.\n        LayoutInflater inflater = getThemeInflater(LayoutInflater.from(\n                FragmentUtil.getContext(OnboardingFragment.this)));\n        ViewGroup backgroundContainer = (ViewGroup) container.findViewById(\n                R.id.background_container);\n        View background = onCreateBackgroundView(inflater, backgroundContainer);\n        if (background != null) {\n            backgroundContainer.setVisibility(View.VISIBLE);\n            backgroundContainer.addView(background);\n        }\n        ViewGroup contentContainer = (ViewGroup) container.findViewById(R.id.content_container);\n        View content = onCreateContentView(inflater, contentContainer);\n        if (content != null) {\n            contentContainer.setVisibility(View.VISIBLE);\n            contentContainer.addView(content);\n        }\n        ViewGroup foregroundContainer = (ViewGroup) container.findViewById(\n                R.id.foreground_container);\n        View foreground = onCreateForegroundView(inflater, foregroundContainer);\n        if (foreground != null) {\n            foregroundContainer.setVisibility(View.VISIBLE);\n            foregroundContainer.addView(foreground);\n        }\n        // Make views visible which were invisible while logo animation is running.\n        container.findViewById(R.id.page_container).setVisibility(View.VISIBLE);\n        container.findViewById(R.id.content_container).setVisibility(View.VISIBLE);\n        if (getPageCount() > 1) {\n            mPageIndicator.setPageCount(getPageCount());\n            mPageIndicator.onPageSelected(mCurrentPageIndex, false);\n        }\n        if (mCurrentPageIndex == getPageCount() - 1) {\n            mStartButton.setVisibility(View.VISIBLE);\n        } else {\n            mPageIndicator.setVisibility(View.VISIBLE);\n        }\n        // Header views.\n        mTitleView.setText(getPageTitle(mCurrentPageIndex));\n        mDescriptionView.setText(getPageDescription(mCurrentPageIndex));\n    }\n\n    /**\n     * Called immediately after the logo animation is complete or no logo animation is specified.\n     * This method can also be called when the activity is recreated, i.e. when no logo animation\n     * are performed.\n     * By default, this method will hide the logo view and start the entrance animation for this\n     * fragment.\n     * Overriding subclasses can provide their own data loading logic as to when the entrance\n     * animation should be executed.\n     */\n    protected void onLogoAnimationFinished() {\n        startEnterAnimation(false);\n    }\n\n    /**\n     * Called to start entrance transition. This can be called by subclasses when the logo animation\n     * and data loading is complete. If force flag is set to false, it will only start the animation\n     * if it's not already done yet. Otherwise, it will always start the enter animation. In both\n     * cases, the logo view will hide and the rest of fragment views become visible after this call.\n     *\n     * @param force {@code true} if enter animation has to be performed regardless of whether it's\n     *                          been done in the past, {@code false} otherwise\n     */\n    protected final void startEnterAnimation(boolean force) {\n        final Context context = FragmentUtil.getContext(OnboardingFragment.this);\n        if (context == null) {\n            return;\n        }\n        hideLogoView();\n        if (mEnterAnimationFinished && !force) {\n            return;\n        }\n        List<Animator> animators = new ArrayList<>();\n        Animator animator = AnimatorInflater.loadAnimator(context,\n                R.animator.lb_onboarding_page_indicator_enter);\n        animator.setTarget(getPageCount() <= 1 ? mStartButton : mPageIndicator);\n        animators.add(animator);\n\n        animator = onCreateTitleAnimator();\n        if (animator != null) {\n            // Header title.\n            animator.setTarget(mTitleView);\n            animators.add(animator);\n        }\n\n        animator = onCreateDescriptionAnimator();\n        if (animator != null) {\n            // Header description.\n            animator.setTarget(mDescriptionView);\n            animators.add(animator);\n        }\n\n        // Customized animation by the inherited class.\n        Animator customAnimator = onCreateEnterAnimation();\n        if (customAnimator != null) {\n            animators.add(customAnimator);\n        }\n\n        // Return if we don't have any animations.\n        if (animators.isEmpty()) {\n            return;\n        }\n        mAnimator = new AnimatorSet();\n        mAnimator.playTogether(animators);\n        mAnimator.start();\n        mAnimator.addListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                mEnterAnimationFinished = true;\n            }\n        });\n        // Search focus and give the focus to the appropriate child which has become visible.\n        getView().requestFocus();\n    }\n\n    /**\n     * Provides the entry animation for description view. This allows users to override the\n     * default fade and slide animation. Returning null will disable the animation.\n     */\n    protected Animator onCreateDescriptionAnimator() {\n        return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this),\n                R.animator.lb_onboarding_description_enter);\n    }\n\n    /**\n     * Provides the entry animation for title view. This allows users to override the\n     * default fade and slide animation. Returning null will disable the animation.\n     */\n    protected Animator onCreateTitleAnimator() {\n        return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this),\n                R.animator.lb_onboarding_title_enter);\n    }\n\n    /**\n     * Returns whether the logo enter animation is finished.\n     *\n     * @return {@code true} if the logo enter transition is finished, {@code false} otherwise\n     */\n    protected final boolean isLogoAnimationFinished() {\n        return mLogoAnimationFinished;\n    }\n\n    /**\n     * Returns the page count.\n     *\n     * @return The page count.\n     */\n    abstract protected int getPageCount();\n\n    /**\n     * Returns the title of the given page.\n     *\n     * @param pageIndex The page index.\n     *\n     * @return The title of the page.\n     */\n    abstract protected CharSequence getPageTitle(int pageIndex);\n\n    /**\n     * Returns the description of the given page.\n     *\n     * @param pageIndex The page index.\n     *\n     * @return The description of the page.\n     */\n    abstract protected CharSequence getPageDescription(int pageIndex);\n\n    /**\n     * Returns the index of the current page.\n     *\n     * @return The index of the current page.\n     */\n    protected final int getCurrentPageIndex() {\n        return mCurrentPageIndex;\n    }\n\n    /**\n     * Called to have the inherited class create background view. This is optional and the fragment\n     * which doesn't have the background view can return {@code null}. This is called inside\n     * {@link #onCreateView}.\n     *\n     * @param inflater The LayoutInflater object that can be used to inflate the views,\n     * @param container The parent view that the additional views are attached to.The fragment\n     *        should not add the view by itself.\n     *\n     * @return The background view for the onboarding screen, or {@code null}.\n     */\n    @Nullable\n    abstract protected View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container);\n\n    /**\n     * Called to have the inherited class create content view. This is optional and the fragment\n     * which doesn't have the content view can return {@code null}. This is called inside\n     * {@link #onCreateView}.\n     *\n     * <p>The content view would be located at the center of the screen.\n     *\n     * @param inflater The LayoutInflater object that can be used to inflate the views,\n     * @param container The parent view that the additional views are attached to.The fragment\n     *        should not add the view by itself.\n     *\n     * @return The content view for the onboarding screen, or {@code null}.\n     */\n    @Nullable\n    abstract protected View onCreateContentView(LayoutInflater inflater, ViewGroup container);\n\n    /**\n     * Called to have the inherited class create foreground view. This is optional and the fragment\n     * which doesn't need the foreground view can return {@code null}. This is called inside\n     * {@link #onCreateView}.\n     *\n     * <p>This foreground view would have the highest z-order.\n     *\n     * @param inflater The LayoutInflater object that can be used to inflate the views,\n     * @param container The parent view that the additional views are attached to.The fragment\n     *        should not add the view by itself.\n     *\n     * @return The foreground view for the onboarding screen, or {@code null}.\n     */\n    @Nullable\n    abstract protected View onCreateForegroundView(LayoutInflater inflater, ViewGroup container);\n\n    /**\n     * Called when the onboarding flow finishes.\n     */\n    protected void onFinishFragment() { }\n\n    /**\n     * Called when the page changes.\n     */\n    private void onPageChangedInternal(int previousPage) {\n        if (mAnimator != null) {\n            mAnimator.end();\n        }\n        mPageIndicator.onPageSelected(mCurrentPageIndex, true);\n\n        List<Animator> animators = new ArrayList<>();\n        // Header animation\n        Animator fadeAnimator = null;\n        if (previousPage < getCurrentPageIndex()) {\n            // sliding to left\n            animators.add(createAnimator(mTitleView, false, Gravity.START, 0));\n            animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.START,\n                    DESCRIPTION_START_DELAY_MS));\n            animators.add(createAnimator(mTitleView, true, Gravity.END,\n                    HEADER_APPEAR_DELAY_MS));\n            animators.add(createAnimator(mDescriptionView, true, Gravity.END,\n                    HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS));\n        } else {\n            // sliding to right\n            animators.add(createAnimator(mTitleView, false, Gravity.END, 0));\n            animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.END,\n                    DESCRIPTION_START_DELAY_MS));\n            animators.add(createAnimator(mTitleView, true, Gravity.START,\n                    HEADER_APPEAR_DELAY_MS));\n            animators.add(createAnimator(mDescriptionView, true, Gravity.START,\n                    HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS));\n        }\n        final int currentPageIndex = getCurrentPageIndex();\n        fadeAnimator.addListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                mTitleView.setText(getPageTitle(currentPageIndex));\n                mDescriptionView.setText(getPageDescription(currentPageIndex));\n            }\n        });\n\n        final Context context = FragmentUtil.getContext(OnboardingFragment.this);\n        // Animator for switching between page indicator and button.\n        if (getCurrentPageIndex() == getPageCount() - 1) {\n            mStartButton.setVisibility(View.VISIBLE);\n            Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(context,\n                    R.animator.lb_onboarding_page_indicator_fade_out);\n            navigatorFadeOutAnimator.setTarget(mPageIndicator);\n            navigatorFadeOutAnimator.addListener(new AnimatorListenerAdapter() {\n                @Override\n                public void onAnimationEnd(Animator animation) {\n                    mPageIndicator.setVisibility(View.GONE);\n                }\n            });\n            animators.add(navigatorFadeOutAnimator);\n            Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(context,\n                    R.animator.lb_onboarding_start_button_fade_in);\n            buttonFadeInAnimator.setTarget(mStartButton);\n            animators.add(buttonFadeInAnimator);\n        } else if (previousPage == getPageCount() - 1) {\n            mPageIndicator.setVisibility(View.VISIBLE);\n            Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(context,\n                    R.animator.lb_onboarding_page_indicator_fade_in);\n            navigatorFadeInAnimator.setTarget(mPageIndicator);\n            animators.add(navigatorFadeInAnimator);\n            Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(context,\n                    R.animator.lb_onboarding_start_button_fade_out);\n            buttonFadeOutAnimator.setTarget(mStartButton);\n            buttonFadeOutAnimator.addListener(new AnimatorListenerAdapter() {\n                @Override\n                public void onAnimationEnd(Animator animation) {\n                    mStartButton.setVisibility(View.GONE);\n                }\n            });\n            animators.add(buttonFadeOutAnimator);\n        }\n        mAnimator = new AnimatorSet();\n        mAnimator.playTogether(animators);\n        mAnimator.start();\n        onPageChanged(mCurrentPageIndex, previousPage);\n    }\n\n    /**\n     * Called when the page has been changed.\n     *\n     * @param newPage The new page.\n     * @param previousPage The previous page.\n     */\n    protected void onPageChanged(int newPage, int previousPage) { }\n\n    private Animator createAnimator(View view, boolean fadeIn, int slideDirection,\n            long startDelay) {\n        boolean isLtr = getView().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;\n        boolean slideRight = (isLtr && slideDirection == Gravity.END)\n                || (!isLtr && slideDirection == Gravity.START)\n                || slideDirection == Gravity.RIGHT;\n        Animator fadeAnimator;\n        Animator slideAnimator;\n        if (fadeIn) {\n            fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0.0f, 1.0f);\n            slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X,\n                    slideRight ? sSlideDistance : -sSlideDistance, 0);\n            fadeAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR);\n            slideAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR);\n        } else {\n            fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1.0f, 0.0f);\n            slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0,\n                    slideRight ? sSlideDistance : -sSlideDistance);\n            fadeAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR);\n            slideAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR);\n        }\n        fadeAnimator.setDuration(HEADER_ANIMATION_DURATION_MS);\n        fadeAnimator.setTarget(view);\n        slideAnimator.setDuration(HEADER_ANIMATION_DURATION_MS);\n        slideAnimator.setTarget(view);\n        AnimatorSet animator = new AnimatorSet();\n        animator.playTogether(fadeAnimator, slideAnimator);\n        if (startDelay > 0) {\n            animator.setStartDelay(startDelay);\n        }\n        return animator;\n    }\n\n    /**\n     * Sets the resource id for the main icon.\n     */\n    public final void setIconResouceId(int resourceId) {\n        this.mIconResourceId = resourceId;\n        if (mMainIconView != null) {\n            mMainIconView.setImageResource(resourceId);\n            mMainIconView.setVisibility(View.VISIBLE);\n        }\n    }\n\n    /**\n     * Returns the resource id of the main icon.\n     */\n    public final int getIconResourceId() {\n        return mIconResourceId;\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/PlaybackFragment.java",
    "content": "// CHECKSTYLE:OFF Generated code\n/* This file is auto-generated from PlaybackSupportFragment.java.  DO NOT MODIFY. */\n\n/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.app;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorInflater;\nimport android.animation.TimeInterpolator;\nimport android.animation.ValueAnimator;\nimport android.animation.ValueAnimator.AnimatorUpdateListener;\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.graphics.drawable.ColorDrawable;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.util.Log;\nimport android.util.TypedValue;\nimport android.view.InputEvent;\nimport android.view.KeyEvent;\nimport android.view.LayoutInflater;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.animation.AccelerateInterpolator;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RestrictTo;\nimport android.app.Fragment;\nimport androidx.leanback.R;\nimport androidx.leanback.animation.LogAccelerateInterpolator;\nimport androidx.leanback.animation.LogDecelerateInterpolator;\nimport androidx.leanback.media.PlaybackGlueHost;\nimport androidx.leanback.widget.ArrayObjectAdapter;\nimport androidx.leanback.widget.BaseOnItemViewClickedListener;\nimport androidx.leanback.widget.BaseOnItemViewSelectedListener;\nimport androidx.leanback.widget.ClassPresenterSelector;\nimport androidx.leanback.widget.ItemAlignmentFacet;\nimport androidx.leanback.widget.ItemBridgeAdapter;\nimport androidx.leanback.widget.ObjectAdapter;\nimport androidx.leanback.widget.PlaybackRowPresenter;\nimport androidx.leanback.widget.PlaybackSeekDataProvider;\nimport androidx.leanback.widget.PlaybackSeekUi;\nimport androidx.leanback.widget.Presenter;\nimport androidx.leanback.widget.PresenterSelector;\nimport androidx.leanback.widget.Row;\nimport androidx.leanback.widget.RowPresenter;\nimport androidx.leanback.widget.SparseArrayObjectAdapter;\nimport androidx.leanback.widget.VerticalGridView;\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * A fragment for displaying playback controls and related content.\n *\n * <p>\n * A PlaybackFragment renders the elements of its {@link ObjectAdapter} as a set\n * of rows in a vertical list.  The Adapter's {@link PresenterSelector} must maintain subclasses\n * of {@link RowPresenter}.\n * </p>\n * <p>\n * A playback row is a row rendered by {@link PlaybackRowPresenter}.\n * App can call {@link #setPlaybackRow(Row)} to set playback row for the first element of adapter.\n * App can call {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} to set presenter for it.\n * {@link #setPlaybackRow(Row)} and {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} are\n * optional, app can pass playback row and PlaybackRowPresenter in the adapter using\n * {@link #setAdapter(ObjectAdapter)}.\n * </p>\n * <p>\n * Auto hide controls upon playing: best practice is calling\n * {@link #setControlsOverlayAutoHideEnabled(boolean)} upon play/pause.\n * Theme attribute {@link R.attr#playbackControlsAutoHideTimeout} controls how long auto-hide will\n * wait after media starts playing.\n * The auto hiding timer will be cancelled upon {@link #tickle()} triggered by input event.\n * By default fragment does not auto hide controls after user interaction. To enable it: set\n * theme attribute {@link R.attr#playbackControlsAutoHideTickleTimeout}, an auto hide\n * timer will be created when tickle() is triggered by input event.\n * </p>\n * @deprecated use {@link PlaybackSupportFragment}\n */\n@Deprecated\npublic class PlaybackFragment extends Fragment {\n    static final String BUNDLE_CONTROL_VISIBLE_ON_CREATEVIEW = \"controlvisible_oncreateview\";\n\n    /**\n     * No background.\n     */\n    public static final int BG_NONE = 0;\n\n    /**\n     * A dark translucent background.\n     */\n    public static final int BG_DARK = 1;\n    PlaybackGlueHost.HostCallback mHostCallback;\n\n    PlaybackSeekUi.Client mSeekUiClient;\n    boolean mInSeek;\n    ProgressBarManager mProgressBarManager = new ProgressBarManager();\n\n    /**\n     * Resets the focus on the button in the middle of control row.\n     * @hide\n     */\n    @RestrictTo(RestrictTo.Scope.LIBRARY)\n    public void resetFocus() {\n        ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView()\n                .findViewHolderForAdapterPosition(0);\n        if (vh != null && vh.getPresenter() instanceof PlaybackRowPresenter) {\n            ((PlaybackRowPresenter) vh.getPresenter()).onReappear(\n                    (RowPresenter.ViewHolder) vh.getViewHolder());\n        }\n    }\n\n    private class SetSelectionRunnable implements Runnable {\n        int mPosition;\n        boolean mSmooth = true;\n\n        SetSelectionRunnable() {\n        }\n\n        @Override\n        public void run() {\n            if (mRowsFragment == null) {\n                return;\n            }\n            mRowsFragment.setSelectedPosition(mPosition, mSmooth);\n        }\n    }\n\n    /**\n     * A light translucent background.\n     */\n    public static final int BG_LIGHT = 2;\n    RowsFragment mRowsFragment;\n    ObjectAdapter mAdapter;\n    PlaybackRowPresenter mPresenter;\n    Row mRow;\n    BaseOnItemViewSelectedListener mExternalItemSelectedListener;\n    BaseOnItemViewClickedListener mExternalItemClickedListener;\n    BaseOnItemViewClickedListener mPlaybackItemClickedListener;\n\n    private final BaseOnItemViewClickedListener mOnItemViewClickedListener =\n            new BaseOnItemViewClickedListener() {\n                @Override\n                public void onItemClicked(Presenter.ViewHolder itemViewHolder,\n                                          Object item,\n                                          RowPresenter.ViewHolder rowViewHolder,\n                                          Object row) {\n                    if (mPlaybackItemClickedListener != null\n                            && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) {\n                        mPlaybackItemClickedListener.onItemClicked(\n                                itemViewHolder, item, rowViewHolder, row);\n                    }\n                    if (mExternalItemClickedListener != null) {\n                        mExternalItemClickedListener.onItemClicked(\n                                itemViewHolder, item, rowViewHolder, row);\n                    }\n                }\n            };\n\n    private final BaseOnItemViewSelectedListener mOnItemViewSelectedListener =\n            new BaseOnItemViewSelectedListener() {\n                @Override\n                public void onItemSelected(Presenter.ViewHolder itemViewHolder,\n                                           Object item,\n                                           RowPresenter.ViewHolder rowViewHolder,\n                                           Object row) {\n                    if (mExternalItemSelectedListener != null) {\n                        mExternalItemSelectedListener.onItemSelected(\n                                itemViewHolder, item, rowViewHolder, row);\n                    }\n                }\n            };\n\n    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();\n\n    public ObjectAdapter getAdapter() {\n        return mAdapter;\n    }\n\n    /**\n     * Listener allowing the application to receive notification of fade in and/or fade out\n     * completion events.\n     * @hide\n     */\n    @RestrictTo(RestrictTo.Scope.LIBRARY)\n    public static class OnFadeCompleteListener {\n        public void onFadeInComplete() {\n        }\n\n        public void onFadeOutComplete() {\n        }\n    }\n\n    private static final String TAG = \"PlaybackFragment\";\n    private static final boolean DEBUG = false;\n    private static final int ANIMATION_MULTIPLIER = 1;\n\n    private static final int START_FADE_OUT = 1;\n\n    // Fading status\n    private static final int IDLE = 0;\n    private static final int ANIMATING = 1;\n\n    int mPaddingBottom;\n    int mOtherRowsCenterToBottom;\n    View mRootView;\n    View mBackgroundView;\n    int mBackgroundType = BG_DARK;\n    int mBgDarkColor;\n    int mBgLightColor;\n    int mAutohideTimerAfterPlayingInMs;\n    int mAutohideTimerAfterTickleInMs;\n    int mMajorFadeTranslateY, mMinorFadeTranslateY;\n    int mAnimationTranslateY;\n    OnFadeCompleteListener mFadeCompleteListener;\n    View.OnKeyListener mInputEventHandler;\n    boolean mFadingEnabled = true;\n    boolean mControlVisibleBeforeOnCreateView = true;\n    boolean mControlVisible = true;\n    int mBgAlpha;\n    ValueAnimator mBgFadeInAnimator, mBgFadeOutAnimator;\n    ValueAnimator mControlRowFadeInAnimator, mControlRowFadeOutAnimator;\n    ValueAnimator mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator;\n\n    private final Animator.AnimatorListener mFadeListener =\n            new Animator.AnimatorListener() {\n                @Override\n                public void onAnimationStart(Animator animation) {\n                    enableVerticalGridAnimations(false);\n                }\n\n                @Override\n                public void onAnimationRepeat(Animator animation) {\n                }\n\n                @Override\n                public void onAnimationCancel(Animator animation) {\n                }\n\n                @Override\n                public void onAnimationEnd(Animator animation) {\n                    if (DEBUG) Log.v(TAG, \"onAnimationEnd \" + mBgAlpha);\n                    if (mBgAlpha > 0) {\n                        enableVerticalGridAnimations(true);\n                        if (mFadeCompleteListener != null) {\n                            mFadeCompleteListener.onFadeInComplete();\n                        }\n                    } else {\n                        VerticalGridView verticalView = getVerticalGridView();\n                        // reset focus to the primary actions only if the selected row was the controls row\n                        if (verticalView != null && verticalView.getSelectedPosition() == 0) {\n                            ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)\n                                    verticalView.findViewHolderForAdapterPosition(0);\n                            if (vh != null && vh.getPresenter() instanceof PlaybackRowPresenter) {\n                                ((PlaybackRowPresenter)vh.getPresenter()).onReappear(\n                                        (RowPresenter.ViewHolder) vh.getViewHolder());\n                            }\n                        }\n                        if (mFadeCompleteListener != null) {\n                            mFadeCompleteListener.onFadeOutComplete();\n                        }\n                    }\n                }\n            };\n\n    public PlaybackFragment() {\n        mProgressBarManager.setInitialDelay(500);\n    }\n\n    VerticalGridView getVerticalGridView() {\n        if (mRowsFragment == null) {\n            return null;\n        }\n        return mRowsFragment.getVerticalGridView();\n    }\n\n    private final Handler mHandler = new Handler() {\n        @Override\n        public void handleMessage(Message message) {\n            if (message.what == START_FADE_OUT && mFadingEnabled) {\n                hideControlsOverlay(true);\n            }\n        }\n    };\n\n    private final VerticalGridView.OnTouchInterceptListener mOnTouchInterceptListener =\n            new VerticalGridView.OnTouchInterceptListener() {\n                @Override\n                public boolean onInterceptTouchEvent(MotionEvent event) {\n                    return onInterceptInputEvent(event);\n                }\n            };\n\n    private final VerticalGridView.OnKeyInterceptListener mOnKeyInterceptListener =\n            new VerticalGridView.OnKeyInterceptListener() {\n                @Override\n                public boolean onInterceptKeyEvent(KeyEvent event) {\n                    return onInterceptInputEvent(event);\n                }\n            };\n\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    void setBgAlpha(int alpha) {\n        mBgAlpha = alpha;\n        if (mBackgroundView != null) {\n            mBackgroundView.getBackground().setAlpha(alpha);\n        }\n    }\n\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    void enableVerticalGridAnimations(boolean enable) {\n        if (getVerticalGridView() != null) {\n            getVerticalGridView().setAnimateChildLayout(enable);\n        }\n    }\n\n    /**\n     * Enables or disables auto hiding controls overlay after a short delay fragment is resumed.\n     * If enabled and fragment is resumed, the view will fade out after a time period.\n     * {@link #tickle()} will kill the timer, next time fragment is resumed,\n     * the timer will be started again if {@link #isControlsOverlayAutoHideEnabled()} is true.\n     *  <p>\n     *  In most cases app does not need call tickle() as it's automatically called by\n     *  {@link androidx.leanback.media.PlaybackBaseControlGlue} on user interactions.\n     */\n    public void setControlsOverlayAutoHideEnabled(boolean enabled) {\n        if (DEBUG) Log.v(TAG, \"setControlsOverlayAutoHideEnabled \" + enabled);\n        if (enabled != mFadingEnabled) {\n            mFadingEnabled = enabled;\n            if (isResumed() && getView().hasFocus()) {\n                showControlsOverlay(true);\n                if (enabled) {\n                    // StateGraph 7->2 5->2\n                    startFadeTimer(mAutohideTimerAfterPlayingInMs);\n                } else {\n                    // StateGraph 4->5 2->5\n                    stopFadeTimer();\n                }\n            } else {\n                // StateGraph 6->1 1->6\n            }\n        }\n    }\n\n    /**\n     * Returns true if controls will be auto hidden after a delay when fragment is resumed.\n     */\n    public boolean isControlsOverlayAutoHideEnabled() {\n        return mFadingEnabled;\n    }\n\n    /**\n     * @deprecated Uses {@link #setControlsOverlayAutoHideEnabled(boolean)}\n     */\n    @Deprecated\n    public void setFadingEnabled(boolean enabled) {\n        setControlsOverlayAutoHideEnabled(enabled);\n    }\n\n    /**\n     * @deprecated Uses {@link #isControlsOverlayAutoHideEnabled()}\n     */\n    @Deprecated\n    public boolean isFadingEnabled() {\n        return isControlsOverlayAutoHideEnabled();\n    }\n\n    /**\n     * Sets the listener to be called when fade in or out has completed.\n     * @hide\n     */\n    @RestrictTo(RestrictTo.Scope.LIBRARY)\n    public void setFadeCompleteListener(OnFadeCompleteListener listener) {\n        mFadeCompleteListener = listener;\n    }\n\n    /**\n     * Returns the listener to be called when fade in or out has completed.\n     * @hide\n     */\n    @RestrictTo(RestrictTo.Scope.LIBRARY)\n    public OnFadeCompleteListener getFadeCompleteListener() {\n        return mFadeCompleteListener;\n    }\n\n    /**\n     * Sets the input event handler.\n     */\n    public final void setOnKeyInterceptListener(View.OnKeyListener handler) {\n        mInputEventHandler = handler;\n    }\n\n    /**\n     * Tickles the playback controls. Fades in the view if it was faded out. {@link #tickle()} will\n     * also kill the timer created by {@link #setControlsOverlayAutoHideEnabled(boolean)}. When\n     * next time fragment is resumed, the timer will be started again if\n     * {@link #isControlsOverlayAutoHideEnabled()} is true. The timer will also be restarted if\n     * app sets a positive value on theme attribute\n     * {@link R.attr#playbackControlsAutoHideTickleTimeout}.\n     *  <p>\n     *  In most cases app does not need call tickle() as it's automatically called by\n     *  {@link androidx.leanback.media.PlaybackBaseControlGlue} on user interactions.\n     */\n    public void tickle() {\n        if (DEBUG) Log.v(TAG, \"tickle enabled \" + mFadingEnabled + \" isResumed \" + isResumed());\n        //StateGraph 2->4\n        stopFadeTimer();\n        showControlsOverlay(true);\n        // Optionally start fading out timer if it's currently playing (mFadingEnabled is true)\n        if (mAutohideTimerAfterTickleInMs > 0 && mFadingEnabled) {\n            startFadeTimer(mAutohideTimerAfterTickleInMs);\n        }\n    }\n\n    @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n    boolean onInterceptInputEvent(InputEvent event) {\n        final boolean controlsHidden = !mControlVisible;\n        if (DEBUG) Log.v(TAG, \"onInterceptInputEvent hidden \" + controlsHidden + \" \" + event);\n        boolean consumeEvent = false;\n        int keyCode = KeyEvent.KEYCODE_UNKNOWN;\n        int keyAction = 0;\n\n        if (event instanceof KeyEvent) {\n            keyCode = ((KeyEvent) event).getKeyCode();\n            keyAction = ((KeyEvent) event).getAction();\n            if (mInputEventHandler != null) {\n                consumeEvent = mInputEventHandler.onKey(getView(), keyCode, (KeyEvent) event);\n            }\n        }\n\n        switch (keyCode) {\n            case KeyEvent.KEYCODE_DPAD_CENTER:\n            case KeyEvent.KEYCODE_DPAD_DOWN:\n            case KeyEvent.KEYCODE_DPAD_UP:\n            case KeyEvent.KEYCODE_DPAD_LEFT:\n            case KeyEvent.KEYCODE_DPAD_RIGHT:\n                // Event may be consumed; regardless, if controls are hidden then these keys will\n                // bring up the controls.\n                if (controlsHidden) {\n                    consumeEvent = true;\n                }\n                if (keyAction == KeyEvent.ACTION_DOWN) {\n                    tickle();\n                }\n                break;\n            case KeyEvent.KEYCODE_BACK:\n            case KeyEvent.KEYCODE_ESCAPE:\n                if (mInSeek) {\n                    // when in seek, the SeekUi will handle the BACK.\n                    return false;\n                }\n                // If controls are not hidden, back will be consumed to fade\n                // them out (even if the key was consumed by the handler).\n                if (!controlsHidden) {\n                    consumeEvent = true;\n\n                    if (((KeyEvent) event).getAction() == KeyEvent.ACTION_UP) {\n                        hideControlsOverlay(true);\n                    }\n                }\n                break;\n            default:\n                if (consumeEvent) {\n                    if (keyAction == KeyEvent.ACTION_DOWN) {\n                        tickle();\n                    }\n                }\n        }\n        return consumeEvent;\n    }\n\n    @Override\n    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n        // controls view are initially visible, make it invisible\n        // if app has called hideControlsOverlay() before view created.\n        mControlVisible = true;\n        if (!mControlVisibleBeforeOnCreateView) {\n            showControlsOverlay(false, false);\n            mControlVisibleBeforeOnCreateView = true;\n        }\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n\n        if (mControlVisible) {\n            //StateGraph: 6->5 1->2\n            if (mFadingEnabled) {\n                // StateGraph 1->2\n                startFadeTimer(mAutohideTimerAfterPlayingInMs);\n            }\n        } else {\n            //StateGraph: 6->7 1->3\n        }\n        getVerticalGridView().setOnTouchInterceptListener(mOnTouchInterceptListener);\n        getVerticalGridView().setOnKeyInterceptListener(mOnKeyInterceptListener);\n        if (mHostCallback != null) {\n            mHostCallback.onHostResume();\n        }\n    }\n\n    private void stopFadeTimer() {\n        if (mHandler != null) {\n            mHandler.removeMessages(START_FADE_OUT);\n        }\n    }\n\n    private void startFadeTimer(int fadeOutTimeout) {\n        if (mHandler != null) {\n            mHandler.removeMessages(START_FADE_OUT);\n            mHandler.sendEmptyMessageDelayed(START_FADE_OUT, fadeOutTimeout);\n        }\n    }\n\n    private static ValueAnimator loadAnimator(Context context, int resId) {\n        ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(context, resId);\n        animator.setDuration(animator.getDuration() * ANIMATION_MULTIPLIER);\n        return animator;\n    }\n\n    private void loadBgAnimator() {\n        AnimatorUpdateListener listener = new AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator arg0) {\n                setBgAlpha((Integer) arg0.getAnimatedValue());\n            }\n        };\n\n        Context context = FragmentUtil.getContext(PlaybackFragment.this);\n        mBgFadeInAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_in);\n        mBgFadeInAnimator.addUpdateListener(listener);\n        mBgFadeInAnimator.addListener(mFadeListener);\n\n        mBgFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_out);\n        mBgFadeOutAnimator.addUpdateListener(listener);\n        mBgFadeOutAnimator.addListener(mFadeListener);\n    }\n\n    private TimeInterpolator mLogDecelerateInterpolator = new LogDecelerateInterpolator(100, 0);\n    private TimeInterpolator mLogAccelerateInterpolator = new LogAccelerateInterpolator(100, 0);\n\n    private void loadControlRowAnimator() {\n        final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator arg0) {\n                if (getVerticalGridView() == null) {\n                    return;\n                }\n                RecyclerView.ViewHolder vh = getVerticalGridView()\n                        .findViewHolderForAdapterPosition(0);\n                if (vh == null) {\n                    return;\n                }\n                View view = vh.itemView;\n                if (view != null) {\n                    final float fraction = (Float) arg0.getAnimatedValue();\n                    if (DEBUG) Log.v(TAG, \"fraction \" + fraction);\n                    view.setAlpha(fraction);\n                    view.setTranslationY((float) mAnimationTranslateY * (1f - fraction));\n                }\n            }\n        };\n\n        Context context = FragmentUtil.getContext(PlaybackFragment.this);\n        mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);\n        mControlRowFadeInAnimator.addUpdateListener(updateListener);\n        mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);\n\n        mControlRowFadeOutAnimator = loadAnimator(context,\n                R.animator.lb_playback_controls_fade_out);\n        mControlRowFadeOutAnimator.addUpdateListener(updateListener);\n        mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator);\n    }\n\n    private void loadOtherRowAnimator() {\n        final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator arg0) {\n                if (getVerticalGridView() == null) {\n                    return;\n                }\n                final float fraction = (Float) arg0.getAnimatedValue();\n                final int count = getVerticalGridView().getChildCount();\n                for (int i = 0; i < count; i++) {\n                    View view = getVerticalGridView().getChildAt(i);\n                    if (getVerticalGridView().getChildAdapterPosition(view) > 0) {\n                        view.setAlpha(fraction);\n                        view.setTranslationY((float) mAnimationTranslateY * (1f - fraction));\n                    }\n                }\n            }\n        };\n\n        Context context = FragmentUtil.getContext(PlaybackFragment.this);\n        mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);\n        mOtherRowFadeInAnimator.addUpdateListener(updateListener);\n        mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);\n\n        mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out);\n        mOtherRowFadeOutAnimator.addUpdateListener(updateListener);\n        mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator());\n    }\n\n    /**\n     * Fades out the playback overlay immediately.\n     * @deprecated Call {@link #hideControlsOverlay(boolean)}\n     */\n    @Deprecated\n    public void fadeOut() {\n        showControlsOverlay(false, false);\n    }\n\n    /**\n     * Show controls overlay.\n     *\n     * @param runAnimation True to run animation, false otherwise.\n     */\n    public void showControlsOverlay(boolean runAnimation) {\n        showControlsOverlay(true, runAnimation);\n    }\n\n    /**\n     * Returns true if controls overlay is visible, false otherwise.\n     *\n     * @return True if controls overlay is visible, false otherwise.\n     * @see #showControlsOverlay(boolean)\n     * @see #hideControlsOverlay(boolean)\n     */\n    public boolean isControlsOverlayVisible() {\n        return mControlVisible;\n    }\n\n    /**\n     * Hide controls overlay.\n     *\n     * @param runAnimation True to run animation, false otherwise.\n     */\n    public void hideControlsOverlay(boolean runAnimation) {\n        showControlsOverlay(false, runAnimation);\n    }\n\n    /**\n     * if first animator is still running, reverse it; otherwise start second animator.\n     */\n    static void reverseFirstOrStartSecond(ValueAnimator first, ValueAnimator second,\n            boolean runAnimation) {\n        if (first.isStarted()) {\n            first.reverse();\n            if (!runAnimation) {\n                first.end();\n            }\n        } else {\n            second.start();\n            if (!runAnimation) {\n                second.end();\n            }\n        }\n    }\n\n    /**\n     * End first or second animator if they are still running.\n     */\n    static void endAll(ValueAnimator first, ValueAnimator second) {\n        if (first.isStarted()) {\n            first.end();\n        } else if (second.isStarted()) {\n            second.end();\n        }\n    }\n\n    /**\n     * Fade in or fade out rows and background.\n     *\n     * @param show True to fade in, false to fade out.\n     * @param animation True to run animation.\n     */\n    void showControlsOverlay(boolean show, boolean animation) {\n        if (DEBUG) Log.v(TAG, \"showControlsOverlay \" + show);\n        if (getView() == null) {\n            mControlVisibleBeforeOnCreateView = show;\n            return;\n        }\n        // force no animation when fragment is not resumed\n        if (!isResumed()) {\n            animation = false;\n        }\n        if (show == mControlVisible) {\n            if (!animation) {\n                // End animation if needed\n                endAll(mBgFadeInAnimator, mBgFadeOutAnimator);\n                endAll(mControlRowFadeInAnimator, mControlRowFadeOutAnimator);\n                endAll(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator);\n            }\n            return;\n        }\n        // StateGraph: 7<->5 4<->3 2->3\n        mControlVisible = show;\n        if (!mControlVisible) {\n            // StateGraph 2->3\n            stopFadeTimer();\n        }\n\n        mAnimationTranslateY = (getVerticalGridView() == null\n                || getVerticalGridView().getSelectedPosition() == 0)\n                ? mMajorFadeTranslateY : mMinorFadeTranslateY;\n\n        if (show) {\n            reverseFirstOrStartSecond(mBgFadeOutAnimator, mBgFadeInAnimator, animation);\n            reverseFirstOrStartSecond(mControlRowFadeOutAnimator, mControlRowFadeInAnimator,\n                    animation);\n            reverseFirstOrStartSecond(mOtherRowFadeOutAnimator, mOtherRowFadeInAnimator, animation);\n        } else {\n            reverseFirstOrStartSecond(mBgFadeInAnimator, mBgFadeOutAnimator, animation);\n            reverseFirstOrStartSecond(mControlRowFadeInAnimator, mControlRowFadeOutAnimator,\n                    animation);\n            reverseFirstOrStartSecond(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator, animation);\n        }\n        if (animation) {\n            getView().announceForAccessibility(getString(show\n                    ? R.string.lb_playback_controls_shown\n                    : R.string.lb_playback_controls_hidden));\n        }\n    }\n\n    /**\n     * Sets the selected row position with smooth animation.\n     */\n    public void setSelectedPosition(int position) {\n        setSelectedPosition(position, true);\n    }\n\n    /**\n     * Sets the selected row position.\n     */\n    public void setSelectedPosition(int position, boolean smooth) {\n        mSetSelectionRunnable.mPosition = position;\n        mSetSelectionRunnable.mSmooth = smooth;\n        if (getView() != null && getView().getHandler() != null) {\n            getView().getHandler().post(mSetSelectionRunnable);\n        }\n    }\n\n    private void setupChildFragmentLayout() {\n        setVerticalGridViewLayout(mRowsFragment.getVerticalGridView());\n    }\n\n    void setVerticalGridViewLayout(VerticalGridView listview) {\n        if (listview == null) {\n            return;\n        }\n\n        // we set the base line of alignment to -paddingBottom\n        listview.setWindowAlignmentOffset(-mPaddingBottom);\n        listview.setWindowAlignmentOffsetPercent(\n                VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);\n\n        // align other rows that arent the last to center of screen, since our baseline is\n        // -mPaddingBottom, we need subtract that from mOtherRowsCenterToBottom.\n        listview.setItemAlignmentOffset(mOtherRowsCenterToBottom - mPaddingBottom);\n        listview.setItemAlignmentOffsetPercent(50);\n\n        // Push last row to the bottom padding\n        // Padding affects alignment when last row is focused\n        listview.setPadding(listview.getPaddingLeft(), listview.getPaddingTop(),\n                listview.getPaddingRight(), mPaddingBottom);\n        listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE);\n    }\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n\n        mOtherRowsCenterToBottom = getResources()\n                .getDimensionPixelSize(R.dimen.lb_playback_other_rows_center_to_bottom);\n        mPaddingBottom =\n                getResources().getDimensionPixelSize(R.dimen.lb_playback_controls_padding_bottom);\n        mBgDarkColor =\n                getResources().getColor(R.color.lb_playback_controls_background_dark);\n        mBgLightColor =\n                getResources().getColor(R.color.lb_playback_controls_background_light);\n        TypedValue outValue = new TypedValue();\n        FragmentUtil.getContext(PlaybackFragment.this).getTheme().resolveAttribute(\n                R.attr.playbackControlsAutoHideTimeout, outValue, true);\n        mAutohideTimerAfterPlayingInMs = outValue.data;\n        FragmentUtil.getContext(PlaybackFragment.this).getTheme().resolveAttribute(\n                R.attr.playbackControlsAutoHideTickleTimeout, outValue, true);\n        mAutohideTimerAfterTickleInMs = outValue.data;\n        mMajorFadeTranslateY =\n                getResources().getDimensionPixelSize(R.dimen.lb_playback_major_fade_translate_y);\n        mMinorFadeTranslateY =\n                getResources().getDimensionPixelSize(R.dimen.lb_playback_minor_fade_translate_y);\n\n        loadBgAnimator();\n        loadControlRowAnimator();\n        loadOtherRowAnimator();\n    }\n\n    /**\n     * Sets the background type.\n     *\n     * @param type One of BG_LIGHT, BG_DARK, or BG_NONE.\n     */\n    public void setBackgroundType(int type) {\n        switch (type) {\n            case BG_LIGHT:\n            case BG_DARK:\n            case BG_NONE:\n                if (type != mBackgroundType) {\n                    mBackgroundType = type;\n                    updateBackground();\n                }\n                break;\n            default:\n                throw new IllegalArgumentException(\"Invalid background type\");\n        }\n    }\n\n    /**\n     * Returns the background type.\n     */\n    public int getBackgroundType() {\n        return mBackgroundType;\n    }\n\n    private void updateBackground() {\n        if (mBackgroundView != null) {\n            int color = mBgDarkColor;\n            switch (mBackgroundType) {\n                case BG_DARK:\n                    break;\n                case BG_LIGHT:\n                    color = mBgLightColor;\n                    break;\n                case BG_NONE:\n                    color = Color.TRANSPARENT;\n                    break;\n            }\n            mBackgroundView.setBackground(new ColorDrawable(color));\n            setBgAlpha(mBgAlpha);\n        }\n    }\n\n    private final ItemBridgeAdapter.AdapterListener mAdapterListener =\n            new ItemBridgeAdapter.AdapterListener() {\n                @Override\n                public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) {\n                    if (DEBUG) Log.v(TAG, \"onAttachedToWindow \" + vh.getViewHolder().view);\n                    if (!mControlVisible) {\n                        if (DEBUG) Log.v(TAG, \"setting alpha to 0\");\n                        vh.getViewHolder().view.setAlpha(0);\n                    }\n                }\n\n                @Override\n                public void onCreate(ItemBridgeAdapter.ViewHolder vh) {\n                    Presenter.ViewHolder viewHolder = vh.getViewHolder();\n                    if (viewHolder instanceof PlaybackSeekUi) {\n                        ((PlaybackSeekUi) viewHolder).setPlaybackSeekUiClient(mChainedClient);\n                    }\n                }\n\n                @Override\n                public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) {\n                    if (DEBUG) Log.v(TAG, \"onDetachedFromWindow \" + vh.getViewHolder().view);\n                    // Reset animation state\n                    vh.getViewHolder().view.setAlpha(1f);\n                    vh.getViewHolder().view.setTranslationY(0);\n                    vh.getViewHolder().view.setAlpha(1f);\n                }\n\n                @Override\n                public void onBind(ItemBridgeAdapter.ViewHolder vh) {\n                }\n            };\n\n    @Override\n    public View onCreateView(LayoutInflater inflater, ViewGroup container,\n                             Bundle savedInstanceState) {\n        mRootView = inflater.inflate(R.layout.lb_playback_fragment, container, false);\n        mBackgroundView = mRootView.findViewById(R.id.playback_fragment_background);\n        mRowsFragment = (RowsFragment) getChildFragmentManager().findFragmentById(\n                R.id.playback_controls_dock);\n        if (mRowsFragment == null) {\n            mRowsFragment = new RowsFragment();\n            getChildFragmentManager().beginTransaction()\n                    .replace(R.id.playback_controls_dock, mRowsFragment)\n                    .commit();\n        }\n        if (mAdapter == null) {\n            setAdapter(new ArrayObjectAdapter(new ClassPresenterSelector()));\n        } else {\n            mRowsFragment.setAdapter(mAdapter);\n        }\n        mRowsFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);\n        mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);\n\n        mBgAlpha = 255;\n        updateBackground();\n        mRowsFragment.setExternalAdapterListener(mAdapterListener);\n        ProgressBarManager progressBarManager = getProgressBarManager();\n        if (progressBarManager != null) {\n            progressBarManager.setRootView((ViewGroup) mRootView);\n        }\n        return mRootView;\n    }\n\n    /**\n     * Sets the {@link PlaybackGlueHost.HostCallback}. Implementor of this interface will\n     * take appropriate actions to take action when the hosting fragment starts/stops processing.\n     */\n    public void setHostCallback(PlaybackGlueHost.HostCallback hostCallback) {\n        this.mHostCallback = hostCallback;\n    }\n\n    @Override\n    public void onStart() {\n        super.onStart();\n        setupChildFragmentLayout();\n        mRowsFragment.setAdapter(mAdapter);\n        if (mHostCallback != null) {\n            mHostCallback.onHostStart();\n        }\n    }\n\n    @Override\n    public void onStop() {\n        if (mHostCallback != null) {\n            mHostCallback.onHostStop();\n        }\n        super.onStop();\n    }\n\n    @Override\n    public void onPause() {\n        if (mHostCallback != null) {\n            mHostCallback.onHostPause();\n        }\n        if (mHandler.hasMessages(START_FADE_OUT)) {\n            // StateGraph: 2->1\n            mHandler.removeMessages(START_FADE_OUT);\n        } else {\n            // StateGraph: 5->6, 7->6, 4->1, 3->1\n        }\n        super.onPause();\n    }\n\n    /**\n     * This listener is called every time there is a selection in {@link RowsFragment}. This can\n     * be used by users to take additional actions such as animations.\n     */\n    public void setOnItemViewSelectedListener(final BaseOnItemViewSelectedListener listener) {\n        mExternalItemSelectedListener = listener;\n    }\n\n    /**\n     * This listener is called every time there is a click in {@link RowsFragment}. This can\n     * be used by users to take additional actions such as animations.\n     */\n    public void setOnItemViewClickedListener(final BaseOnItemViewClickedListener listener) {\n        mExternalItemClickedListener = listener;\n    }\n\n    /**\n     * Sets the {@link BaseOnItemViewClickedListener} that would be invoked for clicks\n     * only on {@link androidx.leanback.widget.PlaybackRowPresenter.ViewHolder}.\n     */\n    public void setOnPlaybackItemViewClickedListener(final BaseOnItemViewClickedListener listener) {\n        mPlaybackItemClickedListener = listener;\n    }\n\n    @Override\n    public void onDestroyView() {\n        mRootView = null;\n        mBackgroundView = null;\n        super.onDestroyView();\n    }\n\n    @Override\n    public void onDestroy() {\n        if (mHostCallback != null) {\n            mHostCallback.onHostDestroy();\n        }\n        super.onDestroy();\n    }\n\n    /**\n     * Sets the playback row for the playback controls. The row will be set as first element\n     * of adapter if the adapter is {@link ArrayObjectAdapter} or {@link SparseArrayObjectAdapter}.\n     * @param row The row that represents the playback.\n     */\n    public void setPlaybackRow(Row row) {\n        this.mRow = row;\n        setupRow();\n        setupPresenter();\n    }\n\n    /**\n     * Sets the presenter for rendering the playback row set by {@link #setPlaybackRow(Row)}. If\n     * adapter does not set a {@link PresenterSelector}, {@link #setAdapter(ObjectAdapter)} will\n     * create a {@link ClassPresenterSelector} by default and map from the row object class to this\n     * {@link PlaybackRowPresenter}.\n     *\n     * @param  presenter Presenter used to render {@link #setPlaybackRow(Row)}.\n     */\n    public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {\n        this.mPresenter = presenter;\n        setupPresenter();\n        setPlaybackRowPresenterAlignment();\n    }\n\n    void setPlaybackRowPresenterAlignment() {\n        if (mAdapter != null && mAdapter.getPresenterSelector() != null) {\n            Presenter[] presenters = mAdapter.getPresenterSelector().getPresenters();\n            if (presenters != null) {\n                for (int i = 0; i < presenters.length; i++) {\n                    if (presenters[i] instanceof PlaybackRowPresenter\n                            && presenters[i].getFacet(ItemAlignmentFacet.class) == null) {\n                        ItemAlignmentFacet itemAlignment = new ItemAlignmentFacet();\n                        ItemAlignmentFacet.ItemAlignmentDef def =\n                                new ItemAlignmentFacet.ItemAlignmentDef();\n                        def.setItemAlignmentOffset(0);\n                        def.setItemAlignmentOffsetPercent(100);\n                        itemAlignment.setAlignmentDefs(new ItemAlignmentFacet.ItemAlignmentDef[]\n                                {def});\n                        presenters[i].setFacet(ItemAlignmentFacet.class, itemAlignment);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Updates the ui when the row data changes.\n     */\n    public void notifyPlaybackRowChanged() {\n        if (mAdapter == null) {\n            return;\n        }\n        mAdapter.notifyItemRangeChanged(0, 1);\n    }\n\n    /**\n     * Sets the list of rows for the fragment. A default {@link ClassPresenterSelector} will be\n     * created if {@link ObjectAdapter#getPresenterSelector()} is null. if user provides\n     * {@link #setPlaybackRow(Row)} and {@link #setPlaybackRowPresenter(PlaybackRowPresenter)},\n     * the row and presenter will be set onto the adapter.\n     *\n     * @param adapter The adapter that contains related rows and optional playback row.\n     */\n    public void setAdapter(ObjectAdapter adapter) {\n        mAdapter = adapter;\n        setupRow();\n        setupPresenter();\n        setPlaybackRowPresenterAlignment();\n\n        if (mRowsFragment != null) {\n            mRowsFragment.setAdapter(adapter);\n        }\n    }\n\n    private void setupRow() {\n        if (mAdapter instanceof ArrayObjectAdapter && mRow != null) {\n            ArrayObjectAdapter adapter = ((ArrayObjectAdapter) mAdapter);\n            if (adapter.size() == 0) {\n                adapter.add(mRow);\n            } else {\n                adapter.replace(0, mRow);\n            }\n        } else if (mAdapter instanceof SparseArrayObjectAdapter && mRow != null) {\n            SparseArrayObjectAdapter adapter = ((SparseArrayObjectAdapter) mAdapter);\n            adapter.set(0, mRow);\n        }\n    }\n\n    private void setupPresenter() {\n        if (mAdapter != null && mRow != null && mPresenter != null) {\n            PresenterSelector selector = mAdapter.getPresenterSelector();\n            if (selector == null) {\n                selector = new ClassPresenterSelector();\n                ((ClassPresenterSelector) selector).addClassPresenter(mRow.getClass(), mPresenter);\n                mAdapter.setPresenterSelector(selector);\n            } else if (selector instanceof ClassPresenterSelector) {\n                ((ClassPresenterSelector) selector).addClassPresenter(mRow.getClass(), mPresenter);\n            }\n        }\n    }\n\n    final PlaybackSeekUi.Client mChainedClient = new PlaybackSeekUi.Client() {\n        @Override\n        public boolean isSeekEnabled() {\n            return mSeekUiClient == null ? false : mSeekUiClient.isSeekEnabled();\n        }\n\n        @Override\n        public void onSeekStarted() {\n            if (mSeekUiClient != null) {\n                mSeekUiClient.onSeekStarted();\n            }\n            setSeekMode(true);\n        }\n\n        @Override\n        public PlaybackSeekDataProvider getPlaybackSeekDataProvider() {\n            return mSeekUiClient == null ? null : mSeekUiClient.getPlaybackSeekDataProvider();\n        }\n\n        @Override\n        public void onSeekPositionChanged(long pos) {\n            if (mSeekUiClient != null) {\n                mSeekUiClient.onSeekPositionChanged(pos);\n            }\n        }\n\n        @Override\n        public void onSeekFinished(boolean cancelled) {\n            if (mSeekUiClient != null) {\n                mSeekUiClient.onSeekFinished(cancelled);\n            }\n            setSeekMode(false);\n        }\n    };\n\n    /**\n     * Interface to be implemented by UI widget to support PlaybackSeekUi.\n     */\n    public void setPlaybackSeekUiClient(PlaybackSeekUi.Client client) {\n        mSeekUiClient = client;\n    }\n\n    /**\n     * Show or hide other rows other than PlaybackRow.\n     * @param inSeek True to make other rows visible, false to make other rows invisible.\n     */\n    void setSeekMode(boolean inSeek) {\n        if (mInSeek == inSeek) {\n            return;\n        }\n        mInSeek = inSeek;\n        getVerticalGridView().setSelectedPosition(0);\n        if (mInSeek) {\n            stopFadeTimer();\n        }\n        // immediately fade in control row.\n        showControlsOverlay(true);\n        final int count = getVerticalGridView().getChildCount();\n        for (int i = 0; i < count; i++) {\n            View view = getVerticalGridView().getChildAt(i);\n            if (getVerticalGridView().getChildAdapterPosition(view) > 0) {\n                view.setVisibility(mInSeek ? View.INVISIBLE : View.VISIBLE);\n            }\n        }\n    }\n\n    /**\n     * Called when size of the video changes. App may override.\n     * @param videoWidth Intrinsic width of video\n     * @param videoHeight Intrinsic height of video\n     */\n    protected void onVideoSizeChanged(int videoWidth, int videoHeight) {\n    }\n\n    /**\n     * Called when media has start or stop buffering. App may override. The default initial state\n     * is not buffering.\n     * @param start True for buffering start, false otherwise.\n     */\n    protected void onBufferingStateChanged(boolean start) {\n        ProgressBarManager progressBarManager = getProgressBarManager();\n        if (progressBarManager != null) {\n            if (start) {\n                progressBarManager.show();\n            } else {\n                progressBarManager.hide();\n            }\n        }\n    }\n\n    /**\n     * Called when media has error. App may override.\n     * @param errorCode Optional error code for specific implementation.\n     * @param errorMessage Optional error message for specific implementation.\n     */\n    protected void onError(int errorCode, CharSequence errorMessage) {\n    }\n\n    /**\n     * Returns the ProgressBarManager that will show or hide progress bar in\n     * {@link #onBufferingStateChanged(boolean)}.\n     * @return The ProgressBarManager that will show or hide progress bar in\n     * {@link #onBufferingStateChanged(boolean)}.\n     */\n    public ProgressBarManager getProgressBarManager() {\n        return mProgressBarManager;\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/PlaybackFragmentGlueHost.java",
    "content": "// CHECKSTYLE:OFF Generated code\n/* This file is auto-generated from {}PlaybackSupportFragmentGlueHost.java.  DO NOT MODIFY. */\n\n/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.app;\n\nimport android.view.View;\n\nimport androidx.leanback.media.PlaybackGlueHost;\nimport androidx.leanback.widget.Action;\nimport androidx.leanback.widget.OnActionClickedListener;\nimport androidx.leanback.widget.OnItemViewClickedListener;\nimport androidx.leanback.widget.PlaybackRowPresenter;\nimport androidx.leanback.widget.PlaybackSeekUi;\nimport androidx.leanback.widget.Presenter;\nimport androidx.leanback.widget.Row;\nimport androidx.leanback.widget.RowPresenter;\n\n/**\n * {@link PlaybackGlueHost} implementation\n * the interaction between this class and {@link PlaybackFragment}.\n * @deprecated use {@link PlaybackSupportFragmentGlueHost}\n */\n@Deprecated\npublic class PlaybackFragmentGlueHost extends PlaybackGlueHost implements PlaybackSeekUi {\n    final PlaybackFragment mFragment;\n\n    public PlaybackFragmentGlueHost(PlaybackFragment fragment) {\n        this.mFragment = fragment;\n    }\n\n    @Override\n    public void setControlsOverlayAutoHideEnabled(boolean enabled) {\n        mFragment.setControlsOverlayAutoHideEnabled(enabled);\n    }\n\n    @Override\n    public boolean isControlsOverlayAutoHideEnabled() {\n        return mFragment.isControlsOverlayAutoHideEnabled();\n    }\n\n    @Override\n    public void setOnKeyInterceptListener(View.OnKeyListener onKeyListener) {\n        mFragment.setOnKeyInterceptListener(onKeyListener);\n    }\n\n    @Override\n    public void setOnActionClickedListener(final OnActionClickedListener listener) {\n        if (listener == null) {\n            mFragment.setOnPlaybackItemViewClickedListener(null);\n        } else {\n            mFragment.setOnPlaybackItemViewClickedListener(new OnItemViewClickedListener() {\n                @Override\n                public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,\n                                          RowPresenter.ViewHolder rowViewHolder, Row row) {\n                    if (item instanceof Action) {\n                        listener.onActionClicked((Action) item);\n                    }\n                }\n            });\n        }\n    }\n\n    @Override\n    public void setHostCallback(HostCallback callback) {\n        mFragment.setHostCallback(callback);\n    }\n\n    @Override\n    public void notifyPlaybackRowChanged() {\n        mFragment.notifyPlaybackRowChanged();\n    }\n\n    @Override\n    public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {\n        mFragment.setPlaybackRowPresenter(presenter);\n    }\n\n    @Override\n    public void setPlaybackRow(Row row) {\n        mFragment.setPlaybackRow(row);\n    }\n\n    @Override\n    public void fadeOut() {\n        mFragment.fadeOut();\n    }\n\n    @Override\n    public boolean isControlsOverlayVisible() {\n        return mFragment.isControlsOverlayVisible();\n    }\n\n    @Override\n    public void hideControlsOverlay(boolean runAnimation) {\n        mFragment.hideControlsOverlay(runAnimation);\n    }\n\n    @Override\n    public void showControlsOverlay(boolean runAnimation) {\n        mFragment.showControlsOverlay(runAnimation);\n    }\n\n    @Override\n    public void setPlaybackSeekUiClient(Client client) {\n        mFragment.setPlaybackSeekUiClient(client);\n    }\n\n    final PlayerCallback mPlayerCallback =\n            new PlayerCallback() {\n                @Override\n                public void onBufferingStateChanged(boolean start) {\n                    mFragment.onBufferingStateChanged(start);\n                }\n\n                @Override\n                public void onError(int errorCode, CharSequence errorMessage) {\n                    mFragment.onError(errorCode, errorMessage);\n                }\n\n                @Override\n                public void onVideoSizeChanged(int videoWidth, int videoHeight) {\n                    mFragment.onVideoSizeChanged(videoWidth, videoHeight);\n                }\n            };\n\n    @Override\n    public PlayerCallback getPlayerCallback() {\n        return mPlayerCallback;\n    }\n}\n"
  },
  {
    "path": "leanback-1.0.0/src/main/java/androidx/leanback/app/PlaybackSupportFragmentGlueHost.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n * in compliance 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, software distributed under the License\n * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express\n * or implied. See the License for the specific language governing permissions and limitations under\n * the License.\n */\npackage androidx.leanback.app;\n\nimport android.view.View;\n\nimport androidx.leanback.media.PlaybackGlueHost;\nimport androidx.leanback.widget.Action;\nimport androidx.leanback.widget.OnActionClickedListener;\nimport androidx.leanback.widget.OnItemViewClickedListener;\nimport androidx.leanback.widget.PlaybackRowPresenter;\nimport androidx.leanback.widget.PlaybackSeekUi;\nimport androidx.leanback.widget.Presenter;\nimport androidx.leanback.widget.Row;\nimport androidx.leanback.widget.RowPresenter;\n\n/**\n * {@link PlaybackGlueHost} implementation\n * the interaction between this class and {@link PlaybackSupportFragment}.\n */\npublic class PlaybackSupportFragmentGlueHost extends PlaybackGlueHost implements PlaybackSeekUi {\n    final PlaybackSupportFragment mFragment;\n\n    public PlaybackSupportFragmentGlueHost(PlaybackSupportFragment fragment) {\n        this.mFragment = fragment;\n    }\n\n    @Override\n    public void setControlsOverlayAutoHideEnabled(boolean enabled) {\n        mFragment.setControlsOverlayAutoHideEnabled(enabled);\n    }\n\n    @Override\n    public boolean isControlsOverlayAutoHideEnabled() {\n        return mFragment.isControlsOverlayAutoHideEnabled();\n    }\n\n    @Override\n    public void setOnKeyInterceptListener(View.OnKeyListener onKeyListener) {\n        mFragment.setOnKeyInterceptListener(onKeyListener);\n    }\n\n    @Override\n    public void setOnActionClickedListener(final OnActionClickedListener listener) {\n        if (listener == null) {\n            mFragment.setOnPlaybackItemViewClickedListener(null);\n        } else {\n            mFragment.setOnPlaybackItemViewClickedListener(new OnItemViewClickedListener() {\n                @Override\n                public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,\n                                          RowPresenter.ViewHolder rowViewHolder, Row row) {\n                    if (item instanceof Action) {\n                        listener.onActionClicked((Action) item);\n                    }\n                }\n            });\n        }\n    }\n\n    @Override\n    public void setHostCallback(HostCallback callback) {\n        mFragment.setHostCallback(callback);\n    }\n\n    @Override\n    public void notifyPlaybackRowChanged() {\n        mFragment.notifyPlaybackRowChanged();\n    }\n\n    @Override\n    public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {\n        mFragment.setPlaybackRowPresenter(presenter);\n    }\n\n    @Override\n    public void setPlaybackRow(Row row) {\n        mFragment.setPlaybackRow(row);\n    }\n\n    @Override\n    public void fadeOut() {\n        mFragment.fadeOut();\n    }\n\n    @Override\n    public boolean isControlsOverlayVisible() {\n        return mFragment.isControlsOverlayVisible();\n    }\n\n    @Override\n    public void hideControlsOverlay(boolean runAnimation) {\n        mFragment.hideControlsOverlay(runAnimation);\n    }\n\n    @Override\n    public void showControlsOverlay(boolean runAnimation) {\n        mFragment.showControlsOverlay(runAnimation);\n    }\n\n    @Override\n    public void setPlaybackSeekUiClient(Client client) {\n        mFragment.setPlaybackSeekUiClient(client);\n    }\n\n    final PlayerCallback mPlayerCallback =\n            new PlayerCallback() {\n                @Override\n                public void onBufferingStateChanged(boolean start) {\n                    mFragment.onBufferingStateChanged(start);\n                }\n\n                @Override\n                public void onError(int errorCode, CharSequence errorMessage) {\n                    mFragment.onError(errorCode, errorMessage);\n                }\n\n                @Override\n                public void onVideoSizeChanged(int videoWidth, int videoHeight) {\n                    mFragment.onVideoSizeChanged(videoWidth, videoHeight);\n                }\n            };\n\n    @Override\n    public PlayerCallback getPlayerCallback() {\n        return mPlayerCallback;\n    }\n}\n"
  }
]